Hvordan optimalisere PHP Laravel Web Application for høy ytelse?

Laravel er mange ting. Men raskt er ikke en av dem. La oss lære noen triks for å få det til å gå raskere!

Ingen PHP-utvikler er uberørt av Laravel disse dager. De er enten en junior- eller mellomnivåutvikler som elsker den raske utviklingen Laravel tilbyr, eller de er en seniorutvikler som blir tvunget til å lære Laravel på grunn av markedspress.

Uansett, det er ingen tvil om at Laravel har revitalisert PHP-økosystemet (jeg, helt sikkert, ville ha forlatt PHP-verdenen for lenge siden hvis Laravel ikke var der).

Et utdrag av (noe berettiget) selvskryt fra Laravel

Men siden Laravel bøyer seg bakover for å gjøre ting enkelt for deg, betyr det at den under gjør tonnevis med arbeid for å sikre at du har et komfortabelt liv som utvikler. Alle de «magiske» funksjonene til Laravel som bare ser ut til å fungere har lag på lag med kode som må piskes opp hver gang en funksjon kjører. Selv et enkelt unntak spor hvor dypt kaninhullet er (legg merke til hvor feilen starter, helt ned til hovedkjernen):

For det som ser ut til å være en kompileringsfeil i en av visningene, er det 18 funksjonskall å spore. Jeg har personlig kommet over 40, og det kan lett være flere hvis du bruker andre biblioteker og plugins.

Poenget er at dette lag på lag med kode som standard gjør Laravel treg.

Hvor treg er Laravel?

Ærlig talt, det er helt umulig å svare på dette spørsmålet av flere grunner.

For det første er det ingen akseptert, objektiv, fornuftig standard for å måle hastigheten til nettapper. Raskere eller langsommere sammenlignet med hva? Under hvilke forhold?

For det andre avhenger en nettapp av så mange ting (database, filsystem, nettverk, hurtigbuffer osv.) at det er rett og slett dumt å snakke om hastighet. En veldig rask nettapp med en veldig treg database er en veldig treg nettapp. 🙂

Men denne usikkerheten er nettopp grunnen til at benchmarks er populære. Selv om de ikke betyr noe (se dette og dette), gir de en referanseramme og hjelper oss fra å bli gale. Derfor, med flere klyper salt klar, la oss få en feil, grov idé om hastighet blant PHP-rammeverk.

Går etter denne ganske respektable GitHub kildeher er hvordan PHP-rammeverket er sammenliknet:

Det kan hende du ikke engang legger merke til Laravel her (selv om du myser veldig hardt) med mindre du kaster saken rett til enden av halen. Ja, kjære venner, Laravel kommer sist! Nå er gitt, de fleste av disse «rammene» er ikke særlig praktiske eller til og med nyttige, men det forteller oss hvor treg Laravel er sammenlignet med andre mer populære.

Normalt forekommer ikke denne «tregheten» i applikasjoner fordi våre daglige nettapplikasjoner sjelden treffer høye tall. Men når de først gjør det (si oppover 200-500 samtidighet), begynner serverne å kveles og dø. Det er tiden da selv å kaste mer maskinvare på problemet ikke reduserer det, og infrastrukturregningene klatrer så fort at dine høye idealer for nettskydata krasjer.

Men hei, bli glad! Denne artikkelen handler ikke om hva som ikke kan gjøres, men om hva som kan gjøres. 🙂

Gode ​​nyheter er at du kan gjøre mye for å få Laravel-appen til å gå raskere. Flere ganger fort. Ja, tuller ikke. Du kan få den samme kodebasen til å bli ballistisk og spare flere hundre dollar på infrastruktur-/vertsregninger hver måned. Hvordan? La oss komme til det.

Fire typer optimaliseringer

Etter min mening kan optimalisering gjøres på fire forskjellige nivåer (når det kommer til PHP-applikasjoner, det vil si):

  • Språknivå: Dette betyr at du bruker en raskere versjon av språket og unngår spesifikke funksjoner/stiler for koding på språket som gjør koden din treg.
  • Rammenivå: Dette er tingene vi skal dekke i denne artikkelen.
  • Infrastrukturnivå: Juster PHP-prosesslederen, webserveren, databasen osv.
  • Maskinvarenivå: Flytte til en bedre, raskere og kraftigere maskinvareleverandør.

Alle disse typene optimaliseringer har sin plass (for eksempel er PHP-fpm-optimalisering ganske kritisk og kraftig). Men fokuset i denne artikkelen vil være optimaliseringer utelukkende av type 2: de som er relatert til rammeverket.

Forresten, det er ingen begrunnelse bak nummereringen, og det er ikke en akseptert standard. Jeg har nettopp laget disse. Vennligst aldri siter meg og si: «Vi trenger type-3-optimalisering på serveren vår,» ellers vil teamlederen din drepe deg, finne meg og drepe meg også. 😀

Og nå, endelig, kommer vi til det lovede land.

Vær oppmerksom på n+1 databasespørringer

n+1-spørringsproblemet er vanlig når ORM-er brukes. Laravel har sin kraftige ORM kalt Eloquent, som er så vakker, så praktisk at vi ofte glemmer å se på hva som skjer.

  Hvordan endre telefonnummer på Amazon

Tenk på et veldig vanlig scenario: Vise listen over alle bestillinger som er lagt inn av en gitt liste over kunder. Dette er ganske vanlig i e-handelssystemer og alle rapporteringsgrensesnitt generelt der vi må vise alle enheter relatert til enkelte enheter.

I Laravel kan vi tenke oss en kontrollerfunksjon som gjør jobben slik:

class OrdersController extends Controller 
{
    // ... 

    public function getAllByCustomers(Request $request, array $ids) {
        $customers = Customer::findMany($ids);        
        $orders = collect(); // new collection
        
        foreach ($customers as $customer) {
            $orders = $orders->merge($customer->orders);
        }
        
        return view('admin.reports.orders', ['orders' => $orders]);
    }
}

Søt! Og enda viktigere, elegant, vakker. 🤩🤩

Dessverre er det en katastrofal måte å skrive kode i Laravel på.

Her er hvorfor.

Når vi ber ORM om å se etter de gitte kundene, genereres en SQL-spørring som dette:

SELECT * FROM customers WHERE id IN (22, 45, 34, . . .);

Noe som er akkurat som forventet. Som et resultat blir alle de returnerte radene lagret i samlingen $kunder inne i kontrollerfunksjonen.

Nå går vi over hver kunde en etter en og får bestillingene deres. Dette utfører følgende spørring. . .

SELECT * FROM orders WHERE customer_id = 22;

. . . like mange ganger som det er kunder.

Med andre ord, hvis vi trenger å få ordredata for 1000 kunder, vil det totale antallet utførte databasespørringer være 1 (for å hente alle kundenes data) + 1000 (for å hente ordredata for hver kunde) = 1001. Dette er der navnet n+1 kommer fra.

Kan vi gjøre det bedre? Sikkert! Ved å bruke det som er kjent som ivrig lasting, kan vi tvinge ORM til å utføre en JOIN og returnere alle nødvendige data i en enkelt spørring! Som dette:

$orders = Customer::findMany($ids)->with('orders')->get();

Den resulterende datastrukturen er en nestet struktur, men ordredataene kan enkelt trekkes ut. Den resulterende enkeltspørringen, i dette tilfellet, er omtrent slik:

SELECT * FROM customers INNER JOIN orders ON customers.id = orders.customer_id WHERE customers.id IN (22, 45, . . .);

Et enkelt søk er selvfølgelig bedre enn tusen ekstra søk. Tenk deg hva som ville skje hvis det var 10 000 kunder å behandle! Eller gud forby hvis vi også ønsket å vise varene som finnes i hver bestilling! Husk at navnet på teknikken er ivrig lasting, og det er nesten alltid en god idé.

Buffer konfigurasjonen!

En av grunnene til Laravels fleksibilitet er tonnevis med konfigurasjonsfiler som er en del av rammeverket. Vil du endre hvordan/hvor bildene lagres?

Vel, bare endre config/filesystems.php-filen (i hvert fall i skrivende stund). Vil du jobbe med flere kødrivere? Beskriv dem gjerne i config/queue.php. Jeg har nettopp telt og funnet ut at det er 13 konfigurasjonsfiler for forskjellige aspekter av rammeverket, noe som sikrer at du ikke blir skuffet uansett hva du vil endre.

Gitt naturen til PHP, hver gang en ny nettforespørsel kommer inn, våkner Laravel, starter opp alt og analyserer alle disse konfigurasjonsfilene for å finne ut hvordan ting kan gjøres annerledes denne gangen. Bortsett fra at det er dumt om ingenting har endret seg de siste dagene! Å gjenoppbygge konfigurasjonen på hver forespørsel er sløsing som kan (faktisk må) unngås, og veien ut er en enkel kommando som Laravel tilbyr:

php artisan config:cache

Det dette gjør er å kombinere alle de tilgjengelige konfigurasjonsfilene til én enkelt, og cachen er et sted for rask gjenfinning. Neste gang det er en nettforespørsel, vil Laravel ganske enkelt lese denne enkeltfilen og sette i gang.

Når det er sagt, er konfigurasjonsbufring en ekstremt delikat operasjon som kan blåse opp i ansiktet ditt. Det største problemet er at når du har utstedt denne kommandoen, vil env()-funksjonen kalle fra alle steder, bortsett fra konfigurasjonsfilene, returnere null!

Det gir mening når du tenker på det. Hvis du bruker konfigurasjonsbufring, forteller du rammeverket: «Vet du hva, jeg tror jeg har satt opp ting bra, og jeg er 100 % sikker på at jeg ikke vil at de skal endres.» Med andre ord forventer du at miljøet forblir statisk, og det er det .env-filer er til for.

Med det sagt, her er noen jernkledde, hellige, ubrytelige regler for konfigurasjonsbufring:

  • Gjør det bare på et produksjonssystem.
  • Gjør det bare hvis du virkelig er sikker på at du vil fryse konfigurasjonen.
  • I tilfelle noe går galt, angre innstillingen med php artisan cache:clear
  • Be om at skaden på virksomheten ikke var betydelig!
  • Reduser automatisk lastede tjenester

    For å være nyttig laster Laravel massevis av tjenester når den våkner. Disse er tilgjengelige i config/app.php-filen som en del av array-nøkkelen «leverandører». La oss ta en titt på hva jeg har i mitt tilfelle:

    /*
        |--------------------------------------------------------------------------
        | Autoloaded Service Providers
        |--------------------------------------------------------------------------
        |
        | The service providers listed here will be automatically loaded on the
        | request to your application. Feel free to add your own services to
        | this array to grant expanded functionality to your applications.
        |
        */
    
        'providers' => [
    
            /*
             * Laravel Framework Service Providers...
             */
            IlluminateAuthAuthServiceProvider::class,
            IlluminateBroadcastingBroadcastServiceProvider::class,
            IlluminateBusBusServiceProvider::class,
            IlluminateCacheCacheServiceProvider::class,
            IlluminateFoundationProvidersConsoleSupportServiceProvider::class,
            IlluminateCookieCookieServiceProvider::class,
            IlluminateDatabaseDatabaseServiceProvider::class,
            IlluminateEncryptionEncryptionServiceProvider::class,
            IlluminateFilesystemFilesystemServiceProvider::class,
            IlluminateFoundationProvidersFoundationServiceProvider::class,
            IlluminateHashingHashServiceProvider::class,
            IlluminateMailMailServiceProvider::class,
            IlluminateNotificationsNotificationServiceProvider::class,
            IlluminatePaginationPaginationServiceProvider::class,
            IlluminatePipelinePipelineServiceProvider::class,
            IlluminateQueueQueueServiceProvider::class,
            IlluminateRedisRedisServiceProvider::class,
            IlluminateAuthPasswordsPasswordResetServiceProvider::class,
            IlluminateSessionSessionServiceProvider::class,
            IlluminateTranslationTranslationServiceProvider::class,
            IlluminateValidationValidationServiceProvider::class,
            IlluminateViewViewServiceProvider::class,
    
            /*
             * Package Service Providers...
             */
    
            /*
             * Application Service Providers...
             */
            AppProvidersAppServiceProvider::class,
            AppProvidersAuthServiceProvider::class,
            // AppProvidersBroadcastServiceProvider::class,
            AppProvidersEventServiceProvider::class,
            AppProvidersRouteServiceProvider::class,
    
        ],

    Nok en gang telte jeg, og det er oppført 27 tjenester! Nå kan det hende du trenger dem alle, men det er usannsynlig.

      Hvordan få Robux enkelt gratis

    For eksempel bygger jeg tilfeldigvis en REST API for øyeblikket, noe som betyr at jeg ikke trenger sesjonstjenesteleverandøren, visningstjenesteleverandøren osv. Og siden jeg gjør et par ting på min måte og ikke følger rammeverkets standardinnstillinger , Jeg kan også deaktivere Auth-tjenesteleverandør, pagineringstjenesteleverandør, oversettelsestjenesteleverandør og så videre. Alt i alt er nesten halvparten av disse unødvendige for min brukssituasjon.

    Ta en lang, hard titt på søknaden din. Trenger den alle disse tjenesteleverandørene? Men for guds skyld, vær så snill å ikke kommentere disse tjenestene blindt og gå til produksjon! Kjør alle testene, sjekk ting manuelt på dev- og staging-maskiner, og vær veldig veldig paranoid før du trykker på avtrekkeren. 🙂

    Vær klok med mellomvarestabler

    Når du trenger litt tilpasset behandling av den innkommende nettforespørselen, er det å lage en ny mellomvare svaret. Nå er det fristende å åpne app/Http/Kernel.php og feste mellomvaren i nettet eller api-stakken; på den måten blir den tilgjengelig på tvers av appen og hvis den ikke gjør noe påtrengende (som logging eller varsling, for eksempel).

    Men etter hvert som appen vokser, kan denne samlingen av global mellomvare bli en stille byrde for appen hvis alle (eller flertallet) av disse er til stede i hver forespørsel, selv om det ikke er noen forretningsmessig grunn til det.

    Med andre ord, vær forsiktig med hvor du legger til/bruker en ny mellomvare. Det kan være mer praktisk å legge til noe globalt, men ytelsesstraffen er veldig høy i det lange løp. Jeg vet smerten du må gjennomgå hvis du selektivt skulle bruke mellomvare hver gang det er en ny endring, men det er en smerte jeg gjerne vil ta og anbefale!

    Unngå ORM (til tider)

    Selv om Eloquent gjør mange aspekter av DB-interaksjon behagelige, kommer det på bekostning av hastighet. Som en kartlegger må ORM ikke bare hente poster fra databasen, men også instansiere modellobjektene og hydrere (fylle dem ut) dem med kolonnedata.

    Så hvis du gjør en enkel $users = User::all() og det er for eksempel 10 000 brukere, vil rammeverket hente 10 000 rader fra databasen og internt gjøre 10 000 nye User() og fylle egenskapene deres med relevante data . Dette er enorme mengder arbeid som gjøres bak kulissene, og hvis databasen er der du bruker applikasjonen blir en flaskehals, er det til tider en god idé å omgå ORM.

    Dette gjelder spesielt for komplekse SQL-spørringer, der du må hoppe over mange bøyler og skrive nedleggelser etter nedleggelser og likevel ende opp med en effektiv spørring. I slike tilfeller er det å foretrekke å gjøre en DB::raw() og skrive spørringen for hånd.

    Går forbi dette ytelsesstudie, selv for enkle innlegg Eloquent er mye tregere ettersom antall poster øker:

    Bruk caching så mye som mulig

    En av de best bevarte hemmelighetene ved optimalisering av nettapplikasjoner er caching.

    For de uinitierte betyr caching forhåndsberegning og lagring av dyre resultater (dyrt med tanke på CPU og minnebruk), og ganske enkelt returnere dem når samme spørring gjentas.

    For eksempel, i en e-handelsbutikk, kan det komme over at av de 2 millioner produktene, mesteparten av tiden er folk interessert i de som er ferske på lager, innenfor en viss prisklasse og for en bestemt aldersgruppe. Det er bortkastet å søke etter denne informasjonen i databasen – siden spørringen ikke endres ofte, er det bedre å lagre disse resultatene et sted vi raskt kan få tilgang til.

    Laravel har innebygd støtte for flere typer caching. I tillegg til å bruke en caching-driver og bygge caching-systemet fra grunnen av, kan det være lurt å bruke noen Laravel-pakker som letter modellbufring, søk cachingetc.

    Men vær oppmerksom på at utover en viss forenklet brukssituasjon, kan forhåndsbygde caching-pakker forårsake flere problemer enn de løser.

    Foretrekk caching i minnet

    Når du cacher noe i Laravel, har du flere alternativer for hvor du skal lagre den resulterende beregningen som må bufres. Disse alternativene er også kjent som cache-drivere. Så selv om det er mulig og helt rimelig å bruke filsystemet for å lagre cache-resultater, er det egentlig ikke hva caching er ment å være.

    Ideelt sett vil du bruke en cache i minnet (som lever i RAM-en helt) som Redis, Memcached, MongoDB, etc., slik at under høyere belastning tjener caching en viktig bruk i stedet for å bli en flaskehals i seg selv.

    Nå tror du kanskje at det å ha en SSD-disk er nesten det samme som å bruke en RAM-pinne, men det er ikke engang i nærheten. Til og med uformell benchmarks viser at RAM overgår SSD med 10-20 ganger når det kommer til hastighet.

    Mitt favorittsystem når det kommer til caching er Redis. Det er latterlig fort (100 000 leseoperasjoner per sekund er vanlige), og kan for svært store hurtigbuffersystemer utvikles til en klynge Enkelt.

    Buffer rutene

    Akkurat som applikasjonskonfigurasjonen, endres ikke rutene mye over tid og er en ideell kandidat for caching. Dette gjelder spesielt hvis du ikke tåler store filer som meg og ender opp med å dele web.php og api.php over flere filer. En enkelt Laravel-kommando pakker opp alle tilgjengelige ruter og holder dem tilgjengelige for fremtidig tilgang:

    php artisan route:cache

    Og når du ender opp med å legge til eller endre ruter, gjør du ganske enkelt:

    php artisan route:clear

    Bildeoptimalisering og CDN

    Bilder er hjertet og sjelen til de fleste nettapplikasjoner. Tilfeldigvis er de også de største forbrukerne av båndbredde og en av de største grunnene til trege apper/nettsteder. Hvis du bare lagrer de opplastede bildene naivt på serveren og sender dem tilbake i HTTP-svar, slipper du en enorm optimaliseringsmulighet.

      Hvordan duplisere en Spotify-spilleliste

    Min første anbefaling er å ikke lagre bilder lokalt – det er problemet med tap av data å håndtere, og avhengig av hvilken geografisk region kunden din befinner seg i, kan dataoverføringen gå smertefullt sakte.

    Gå heller for en løsning som Skyet som automatisk endrer størrelse og optimerer bilder i farten.

    Hvis det ikke er mulig, bruk noe som Cloudflare for å bufre og vise bilder mens de er lagret på serveren din.

    Og hvis selv det ikke er mulig, gjør det mye forskjell å justere nettserverprogramvaren din litt for å komprimere eiendeler og dirigere den besøkendes nettleser til å cache ting. Slik vil et utdrag av Nginx-konfigurasjonen se ut:

    server {
    
       # file truncated
        
        # gzip compression settings
        gzip on;
        gzip_comp_level 5;
        gzip_min_length 256;
        gzip_proxied any;
        gzip_vary on;
    
       # browser cache control
       location ~* .(ico|css|js|gif|jpeg|jpg|png|woff|ttf|otf|svg|woff2|eot)$ {
             expires 1d;
             access_log off;
             add_header Pragma public;
             add_header Cache-Control "public, max-age=86400";
        }
    }

    Jeg er klar over at bildeoptimalisering ikke har noe med Laravel å gjøre, men det er et så enkelt og kraftig triks (og blir så ofte neglisjert) som ikke kunne hjelpe meg selv.

    Autoloader optimering

    Autoloading er en ryddig, ikke så gammel funksjon i PHP som uten tvil reddet språket fra undergang. Når det er sagt, tar prosessen med å finne og laste den relevante klassen ved å dechiffrere en gitt navneområdestreng tid og kan unngås i produksjonsdistribusjoner der høy ytelse er ønskelig. Nok en gang har Laravel en enkeltkommandoløsning på dette:

    composer install --optimize-autoloader --no-dev

    Bli venner med køer

    Køer er hvordan du behandler ting når det er mange av dem, og hver av dem tar noen få millisekunder å fullføre. Et godt eksempel er å sende e-poster – en utbredt brukssak i nettapper er å skyte av noen få varslings-e-poster når en bruker utfører noen handlinger.

    For eksempel, i et nylig lansert produkt, vil du kanskje at bedriftsledelsen (noen 6-7 e-postadresser) skal varsles når noen legger inn en bestilling over en viss verdi. Forutsatt at e-postgatewayen din kan svare på SMTP-forespørselen din i løpet av 500 ms, snakker vi om en god 3-4 sekunders ventetid for brukeren før ordrebekreftelsen starter. En virkelig dårlig del av UX, jeg er sikker på at du vil bli enige.

    Midlet er å lagre jobber etter hvert som de kommer inn, fortelle brukeren at alt gikk bra, og behandle dem (noen sekunder) senere. Hvis det er en feil, kan jobbene i kø prøves på nytt et par ganger før de erklæres for å ha mislyktes.

    Kreditt: Microsoft.com

    Mens et køsystem kompliserer oppsettet litt (og legger til noen overvåkingskostnader), er det uunnværlig i en moderne nettapplikasjon.

    Asset optimization (Laravel Mix)

    For eventuelle grensesnittelementer i Laravel-applikasjonen din, sørg for at det er en pipeline som kompilerer og minimerer alle ressursfilene. De som er komfortable med et bundlersystem som Webpack, Gulp, Parcel, etc., trenger ikke å bry seg, men hvis du ikke gjør dette allerede, Laravel Mix er en solid anbefaling.

    Mix er en lett (og herlig, i all ærlighet!) omslag rundt Webpack som tar vare på alle dine CSS, SASS, JS, etc., filer for produksjon. En typisk .mix.js-fil kan være så liten som denne og fortsatt gjøre underverker:

    const mix = require('laravel-mix');
    
    mix.js('resources/js/app.js', 'public/js')
        .sass('resources/sass/app.scss', 'public/css');

    Denne tar seg automatisk av import, minifisering, optimalisering og hele støyten når du er klar for produksjon og kjører npm run produksjon. Mix tar seg av ikke bare tradisjonelle JS- og CSS-filer, men også Vue- og React-komponenter som du kan ha i applikasjonsarbeidsflyten.

    Mer informasjon her!

    Konklusjon

    Ytelsesoptimalisering er mer kunst enn vitenskap – det er viktig å vite hvordan og hvor mye man skal gjøre enn hva man skal gjøre. Når det er sagt, er det ingen ende på hvor mye og hva du kan optimalisere i en Laravel-applikasjon.

    Men uansett hva du gjør, vil jeg gjerne gi deg noen avskjedsråd – optimalisering bør gjøres når det er en solid grunn, og ikke fordi det høres bra ut eller fordi du er paranoid om appens ytelse for 100 000+ brukere mens den er i virkeligheten det er bare 10.

    Hvis du ikke er sikker på om du trenger å optimalisere appen din eller ikke, trenger du ikke sparke på reiret til hornets. En fungerende app som føles kjedelig, men som gjør akkurat det den skal, er ti ganger mer ønskelig enn en app som har blitt optimert til en mutant hybrid supermaskin, men som faller pladask nå og da.

    Og for at nybegynneren skal bli en Laravel-mester, sjekk ut denne nettkurs.

    Måtte appene dine kjøre mye, mye raskere! 🙂