Како оптимизовати ПХП Ларавел веб апликацију за високе перформансе?

Ubrzavanje Laravel aplikacija: Praktični saveti i trikovi

Laravel je izvanredan alat, ali brzina nije njegova najjača strana. Hajde da istražimo neke tehnike za poboljšanje performansi Laravel aplikacija.

Danas je teško naći PHP programera koji nije imao kontakt sa Laravelom. Bilo da su u pitanju mlađi programeri koji cene brz razvoj, ili iskusniji koji su pod pritiskom tržišta, Laravel je neizbežan.

Laravel je definitivno oživeo PHP ekosistem. Bez njega, mnogi bi već napustili PHP svet.

Laravel se ponosi time što olakšava život programerima.

Međutim, zbog te lakoće, Laravel obavlja veliki posao iza kulisa. „Magija“ koju Laravel nudi, zapravo su slojevi koda koji se izvršavaju pri svakom pozivu funkcije. Čak i jednostavna greška pokreće lanac od mnogih poziva funkcija:

Ono što se čini kao greška u prikazu, zapravo uključuje 18 poziva funkcija. U praksi, taj broj može biti i veći ako koristite dodatne biblioteke.

Dakle, po defaultu, ovi slojevi koda čine Laravel sporijim.

Koliko je Laravel spor?

Nemoguće je precizno odgovoriti na to pitanje iz više razloga.

Prvo, ne postoji opšteprihvaćen standard za merenje brzine web aplikacija. Brzina je relativan pojam. U poređenju sa čime? Pod kojim uslovima?

Drugo, performanse web aplikacije zavise od mnogih faktora (baze podataka, fajl sistema, mreže, keša itd.). Aplikacija sa brzom logikom, ali sporom bazom podataka, biće spora.

Ipak, merenja nam pomažu da steknemo neku sliku. Zato, hajde da pogledamo kako Laravel stoji u poređenju sa drugim PHP frameworkovima.

Prema ovom GitHub izvoru, poređenje PHP frameworkova izgleda ovako:

Laravel se nalazi na samom kraju liste. Mnogi od navedenih frameworkova nisu praktični, ali ovo pokazuje koliko je Laravel sporiji od drugih.

U normalnim uslovima, ova sporost nije očigledna. Međutim, kada se broj istovremenih korisnika poveća (npr. preko 200-500), serveri počinju da se preopterećuju. Tada ni dodatni hardver ne rešava problem, a troškovi infrastrukture rastu.

Ali ne brinite! Ovaj članak ne govori o problemima, već o rešenjima. 🙂

Postoji mnogo načina da se ubrza Laravel aplikacija. I to značajno. Možete postići da vaša aplikacija radi mnogo brže i da uštedite novac na računima za hosting. Hajde da vidimo kako.

Četiri tipa optimizacije

Optimizaciju možemo podeliti na četiri nivoa, kada su u pitanju PHP aplikacije:

  • Nivo jezika: Korišćenje brže verzije jezika i izbegavanje sporih stilova kodiranja.
  • Nivo frameworka: Ovo su tehnike koje ćemo pokriti u ovom članku.
  • Nivo infrastrukture: Podešavanje PHP proces menadžera, web servera, baze podataka itd.
  • Nivo hardvera: Prelazak na bolji i brži hardver.

Svi ovi nivoi su važni, ali ovaj članak fokusira se na optimizacije tipa 2: one koje se tiču samog frameworka.

Numeracija nivoa nije standard, već je samo radi lakšeg razumevanja. Nemojte ovo koristiti u razgovorima sa kolegama.

I sada, prelazimo na stvar.

Pazite na n+1 upite baze podataka

Problem n+1 upita često se javlja kada koristimo ORM-ove. Laravel koristi Eloquent, koji je toliko intuitivan, da često zaboravimo šta se zapravo dešava ispod haube.

Razmotrimo čest scenario: prikaz liste porudžbina za listu kupaca. Ovo je uobičajeno u sistemima za e-trgovinu i izveštavanju.

U Laravelu, kontroler to može uraditi ovako:

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]);
    }
}

Ovaj kod je elegantan, ali je katastrofalan u smislu performansi.

Kada tražimo kupce, generiše se SQL upit:

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

Zatim, za svakog kupca se pojedinačno dobijaju njegove porudžbine. Ovo izvršava sledeći upit:

SELECT * FROM orders WHERE customer_id = 22;

Ovaj upit se izvršava onoliko puta koliko imamo kupaca.

Ako treba da dobijemo podatke o porudžbinama za 1000 kupaca, ukupno će biti izvršeno 1001 upit (1 za kupce, 1000 za porudžbine). Zato se ovo zove n+1 problem.

Može li bolje? Naravno! Korišćenjem „eager loading“, možemo naterati ORM da izvrši JOIN i vrati sve podatke u jednom upitu:

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

Rezultujuća struktura podataka je ugnježdena, ali su podaci o porudžbinama lako dostupni. SQL upit u ovom slučaju bi bio:

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

Jedan upit je bolji od hiljadu upita. Zamislite šta bi se desilo sa 10.000 kupaca. Eager loading je skoro uvek dobra ideja.

Keširajte konfiguraciju

Laravel je veoma fleksibilan zahvaljujući konfiguracionim fajlovima. Želite da promenite gde se slike čuvaju? Promenite fajl `config/filesystems.php`. Želite da koristite više drajvera za redove čekanja? Opisite ih u `config/queue.php`. Postoji 13 konfiguracionih fajlova za različite aspekte frameworka.

Zbog prirode PHP-a, svaki put kada stigne novi web zahtev, Laravel učitava i analizira sve konfiguracione fajlove. Ovo je nepotrebno, ako se ništa nije promenilo. Ponovno učitavanje konfiguracije na svaki zahtev je gubitak vremena, koji se može izbeći komandom:

php artisan config:cache

Ova komanda kombinuje sve konfiguracione fajlove u jedan, koji se brzo učitava. Međutim, keširanje konfiguracije je osetljiva operacija. Kada jednom izvršite ovu komandu, `env()` funkcija će vraćati `null` svuda osim u konfiguracionim fajlovima!

Ako koristite keširanje konfiguracije, govorite frameworku: „Neću više ništa menjati.“ Okruženje ostaje statično. To je poenta `.env` fajlova.

Zato sledite ova pravila:

  • Koristite ovu komandu samo na produkcionom sistemu.
  • Uradite to samo ako ste sigurni da nećete menjati konfiguraciju.
  • Ako nešto krene po zlu, poništite podešavanje pomoću `php artisan cache:clear`.
  • Nadajte se da neće biti ozbiljne štete!

Smanjite broj automatski učitanih servisa

Laravel učitava veliki broj servisa kada se pokrene. Oni su definisani u `config/app.php` pod ključem `’providers’`. Pogledajmo listu servisa:

/*
    |--------------------------------------------------------------------------
    | 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...
         */
        Illuminate\Auth\AuthServiceProvider::class,
        Illuminate\Broadcasting\BroadcastServiceProvider::class,
        Illuminate\Bus\BusServiceProvider::class,
        Illuminate\Cache\CacheServiceProvider::class,
        Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
        Illuminate\Cookie\CookieServiceProvider::class,
        Illuminate\Database\DatabaseServiceProvider::class,
        Illuminate\Encryption\EncryptionServiceProvider::class,
        Illuminate\Filesystem\FilesystemServiceProvider::class,
        Illuminate\Foundation\Providers\FoundationServiceProvider::class,
        Illuminate\Hashing\HashServiceProvider::class,
        Illuminate\Mail\MailServiceProvider::class,
        Illuminate\Notifications\NotificationServiceProvider::class,
        Illuminate\Pagination\PaginationServiceProvider::class,
        Illuminate\Pipeline\PipelineServiceProvider::class,
        Illuminate\Queue\QueueServiceProvider::class,
        Illuminate\Redis\RedisServiceProvider::class,
        Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
        Illuminate\Session\SessionServiceProvider::class,
        Illuminate\Translation\TranslationServiceProvider::class,
        Illuminate\Validation\ValidationServiceProvider::class,
        Illuminate\View\ViewServiceProvider::class,

        /*
         * Package Service Providers...
         */

        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        // App\Providers\BroadcastServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,

    ],

Na listi je 27 servisa. Možda su vam svi potrebni, ali je malo verovatno.

Na primer, ako pravite REST API, nije vam potreban servis za sesije, preglede, itd. Takođe, možete onemogućiti i servis za autentifikaciju, paginaciju i prevode. Skoro pola ovih servisa je nepotrebno.

Dobro analizirajte svoju aplikaciju. Da li su joj potrebni svi ovi servisi? Ali nemojte ih slepo uklanjati! Pokrenite sve testove i proverite stvari pre nego što primenite izmene na produkciju.

Budite pažljivi sa middleware slojevima

Kada je potrebno prilagođeno procesiranje web zahteva, kreirate novi middleware. Možete ga dodati u `app/Http/Kernel.php` i on će biti dostupan u celoj aplikaciji.

Međutim, kada aplikacija raste, veliki broj globalnih middleware-a može usporiti aplikaciju. Pogotovo ako se izvršavaju i kada nisu potrebni.

Zato pazite gde dodajete middleware. Dodavanje nečega globalno je jednostavno, ali dugoročno utiče na performanse. Selektivna primena middleware-a je bolja, iako je teža za održavanje.

Izbegavajte ORM (ponekad)

Eloquent olakšava interakciju sa bazom podataka, ali to ima svoju cenu u brzini. ORM ne samo da preuzima zapise iz baze, već i instancira objekte modela i popunjava ih podacima.

Ako uradite `$users = User::all()` i imate 10.000 korisnika, framework će preuzeti 10.000 redova iz baze i instancirati 10.000 novih `User()` objekata. Ako je baza podataka usko grlo, dobra je ideja zaobići ORM.

Ovo posebno važi za složene SQL upite. U tom slučaju, bolje je koristiti `DB::raw()` i ručno napisati upit.

Prema ovoj studiji, Eloquent je sporiji od direktnih upita, pogotovo kada je broj zapisa veliki:

Koristite keširanje što je više moguće

Keširanje je jedan od najboljih načina za optimizaciju web aplikacija.

Keširanje znači prethodno izračunavanje i čuvanje rezultata koji su skupi za izračunavanje. Kada se isti upit ponovi, rezultat se vraća iz keša umesto da se ponovo računa.

Na primer, u prodavnici e-trgovine sa 2 miliona proizvoda, korisnike zanimaju oni koji su novi, u određenom cenovnom rangu i za određenu starosnu grupu. Upiti u bazu za ove informacije su nepotrebni. Bolje je te rezultate čuvati u kešu.

Laravel ima podršku za razne tipove keširanja. Možete koristiti ugrađene drajvere, ili pakete koji olakšavaju keširanje modela i keširanje upita. Ipak, pre-built paketi za keširanje ponekad mogu izazvati više problema nego što ih reše.

Preferirajte keširanje u memoriji

Kada keširate nešto u Laravelu, imate više opcija gde da smestite rezultate. Ove opcije se zovu keš drajveri. Iako je moguće koristiti fajl sistem za keš, to nije idealno rešenje.

Najbolje je koristiti keš memoriju koja se nalazi u RAM-u, kao što su Redis, Memcached, MongoDB itd. Na taj način izbegavate da keš postane usko grlo.

SSD diskovi nisu brzi kao RAM. Poređenja pokazuju da je RAM 10-20 puta brži od SSD-a.

Redis je odličan sistem za keširanje. Veoma je brz (100.000 operacija čitanja u sekundi je normalno), i lako se može skalirati u klaster.

Keširajte rute

Kao i konfiguracija aplikacije, rute se retko menjaju i dobar su kandidat za keširanje. Pogotovo ako imate puno ruta. Laravel komanda `php artisan route:cache` pakuje sve rute i čuva ih za brzi pristup.

Kada dodate ili promenite rute, koristite `php artisan route:clear`.

Optimizacija slika i CDN

Slike su bitan deo mnogih web aplikacija, ali su i veliki potrošači propusnog opsega. Ako samo čuvate slike na serveru i šaljete ih nazad, propuštate priliku za optimizaciju.

Moja prva preporuka je da ne čuvate slike lokalno. Postoji rizik od gubitka podataka, a prenos može biti spor.

Koristite rešenje kao što je Cloudinary, koji automatski menja veličinu i optimizuje slike.

Ako to nije moguće, koristite Cloudflare za keširanje i servisiranje slika dok su na vašem serveru.

Ako ni to nije moguće, podešavanje web servera da komprimuje datoteke i usmerava pretraživač da kešira stvari, čini veliku razliku. Evo kako izgleda deo Nginx konfiguracije:

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";
    }
}

Optimizacija slika nema veze sa Laravelom, ali je jednostavan i moćan trik koji se često zanemaruje.

Optimizacija autoloader-a

Autoloading je praktična funkcija u PHP-u koja je spasila jezik. Međutim, proces pronalaženja i učitavanja klasa zahteva vreme. U produkcijskim okruženjima je poželjno izbeći ovaj proces. Laravel ima komandu koja ovo rešava:

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

Rad sa redovima čekanja

Redovi čekanja su način za obradu poslova kada ih ima mnogo, i kada svaki od njih traje neko vreme. Primer je slanje email-ova. Kada korisnik izvrši neku akciju, šalju se emailovi.

Ako recimo, menadžment treba da dobije email obaveštenje kada neko izvrši porudžbinu iznad određene vrednosti, a vaš email gateway reaguje na SMTP zahtev za 500ms, korisnik mora čekati 3-4 sekunde, što je loše korisničko iskustvo.

Rešenje je da posao pošaljete u red čekanja i kažete korisniku da je sve u redu. Poslovi će biti obrađeni kasnije. Ako dođe do greške, posao u redu čekanja može se ponovo izvršiti.

Zasluge: Microsoft.com

Sistem čekanja komplikuje setup, ali je neophodan u modernoj web aplikaciji.

Optimizacija sredstava (Laravel Mix)

Za sva frontend sredstva, koristite pipeline koji kompajlira i minimizira sve datoteke. Ako koristite Webpack, Gulp, Parcel, nemate problem. Ako ne, preporučujem Laravel Mix.

Mix je lagan omotač oko Webpack-a. Tipičan `.mix.js` fajl može izgledati ovako:

const mix = require('laravel-mix');

mix.js('resources/js/app.js', 'public/js')
    .sass('resources/sass/app.scss', 'public/css');

Ovo automatski rešava sve import-e, minimizaciju i optimizaciju kada ste spremni za produkciju i pokrenete `npm run production`. Mix radi sa JS, CSS i Vue/React komponentama.

Više informacija ovde!

Zaključak

Optimizacija performansi je više umetnost nego nauka. Važno je znati kako i koliko raditi, a ne samo šta treba raditi. U Laravel aplikaciji možete optimizovati mnogo toga.

Optimizaciju treba raditi kada postoji razlog, a ne zato što to zvuči dobro ili zato što ste paranoični oko performansi za 100.000+ korisnika, ako ih imate samo 10.

Ako niste sigurni da li treba da optimizujete svoju aplikaciju, nemojte to raditi. Bolje je imati aplikaciju koja radi ono što treba, nego aplikaciju koja je optimizovana i ne radi kako treba.

Za nove Laravel programere, pogledajte ovaj online kurs.

Neka vaše aplikacije rade brže! 🙂