Jak optimalizovat webovou aplikaci PHP Laravel pro vysoký výkon?

Laravel je mnoho věcí. Ale rychlý mezi ně nepatří. Pojďme se naučit některé triky obchodu, aby to šlo rychleji!

Žádný PHP vývojář není nedotčen Laravel tyto dny. Buď jsou to vývojáři na nižší nebo střední úrovni, kteří milují rychlý vývoj, který Laravel nabízí, nebo jsou to starší vývojáři, kteří jsou nuceni se naučit Laravel kvůli tlakům trhu.

Ať tak či onak, nelze popřít, že Laravel oživil ekosystém PHP (já bych už dávno opustil svět PHP, kdyby tam Laravel nebyl).

Úryvek (poněkud oprávněné) sebechvály od Laravel

Jelikož se však Laravel ohýbá dozadu, aby vám to usnadnil, znamená to, že pod ním dělá spousty a tuny práce, abyste se ujistili, že máte jako vývojář pohodlný život. Všechny „magické“ funkce Laravelu, které se zdají fungovat, mají vrstvy za vrstvami kódu, které je třeba vylepšit pokaždé, když se funkce spustí. Dokonce i jednoduchá výjimka ukazuje, jak hluboká je králičí nora (všimněte si, kde chyba začíná, až k hlavnímu jádru):

Pro to, co se zdá být chybou kompilace v jednom z pohledů, existuje 18 volání funkcí ke sledování. Osobně jsem narazil na 40 a klidně by jich mohlo být více, pokud používáte jiné knihovny a pluginy.

Pointa je, že ve výchozím nastavení tyto vrstvy po vrstvách kódu zpomalují Laravel.

Jak pomalý je Laravel?

Upřímně řečeno, na tuto otázku je nemožné odpovědět z několika důvodů.

Za prvé, neexistuje žádný přijatý, objektivní a rozumný standard pro měření rychlosti webových aplikací. Rychlejší nebo pomalejší v porovnání s čím? Za jakých podmínek?

Za druhé, webová aplikace závisí na tolika věcech (databáze, souborovém systému, síti, mezipaměti atd.), že je hloupé mluvit o rychlosti. Velmi rychlá webová aplikace s velmi pomalou databází je velmi pomalá webová aplikace. 🙂

Ale právě tato nejistota je důvodem, proč jsou benchmarky oblíbené. I když nic neznamenají (viz tento a tento), poskytují určitý referenční rámec a pomáhají nám zešílet. Proto, když máme připraveno několik špetek soli, udělejme si mylnou, hrubou představu o rychlosti mezi frameworky PHP.

Jde o tento poměrně slušný GitHub zdrojZde je návod, jak se rámce PHP porovnávají:

Zde si Laravela možná ani nevšimnete (i když pořádně přimhouříte oči), pokud nenahodíte pouzdro až na konec ocasu. Ano, drazí přátelé, Laravel je poslední! Samozřejmě, většina těchto „rámců“ není příliš praktická nebo dokonce užitečná, ale říká nám, jak pomalý je Laravel ve srovnání s jinými populárnějšími.

Obvykle se tato „pomalost“ v aplikacích nevyskytuje, protože naše každodenní webové aplikace zřídka dosahují vysokých čísel. Ale jakmile to udělají (řekněme, více než 200-500 souběžných), servery se začnou dusit a umírat. Je to doba, kdy problém nevyřeší ani nasazení dalšího hardwaru a účty za infrastrukturu rostou tak rychle, že se vaše vysoké ideály cloud computingu zhroutí.

Ale hej, mějte se! Tento článek není o tom, co se nedá, ale o tom, co se dělat dá. 🙂

Dobrou zprávou je, že můžete udělat hodně pro to, aby vaše aplikace Laravel běžela rychleji. Několikrát rychle. Ano, bez legrace. Můžete udělat stejnou kódovou základnu balistickou a každý měsíc ušetřit několik stovek dolarů na účtech za infrastrukturu/hosting. Jak? Jdeme na to.

Čtyři typy optimalizací

Podle mého názoru lze optimalizaci provést na čtyřech různých úrovních (pokud jde o aplikace PHP, tj.):

  • Jazyková úroveň: To znamená, že používáte rychlejší verzi jazyka a vyhnete se specifickým funkcím/stylům kódování v jazyce, který zpomaluje váš kód.
  • Rámcová úroveň: To jsou věci, kterým se budeme v tomto článku věnovat.
  • Úroveň infrastruktury: Vyladění správce procesů PHP, webový server, databáze atd.
  • Hardwarová úroveň: Přechod k lepšímu, rychlejšímu a výkonnějšímu poskytovateli hostingu hardwaru.

Všechny tyto typy optimalizací mají své místo (například optimalizace PHP-fpm je docela kritická a výkonná). Ale tento článek se zaměří na optimalizace čistě typu 2: ty související s frameworkem.

Mimochodem, pro číslování není žádné zdůvodnění a není to přijímaný standard. Právě jsem je vymyslel. Prosím, nikdy mě necitujte a neříkejte: „Potřebujeme optimalizaci typu 3 na našem serveru,“ nebo vás vedoucí týmu zabije, najde mě a pak zabije také mě. 😀

A nyní se konečně dostáváme do země zaslíbené.

Uvědomte si n+1 databázových dotazů

Problém dotazu n+1 je při použití ORM běžný. Laravel má svůj výkonný ORM s názvem Eloquent, který je tak krásný a pohodlný, že se často zapomínáme dívat na to, co se děje.

  Jak spouštět bash skripty pomocí Pythonu?

Zvažte velmi běžný scénář: zobrazení seznamu všech objednávek zadaných daným seznamem zákazníků. To je docela běžné v systémech elektronického obchodování a obecně ve všech rozhraních pro vykazování, kde potřebujeme zobrazit všechny entity související s některými entitami.

V Laravelu si můžeme představit funkci ovladače, která tuto práci dělá takto:

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

Bonbón! A co je důležitější, elegantní, krásné. 🤩🤩

Bohužel je to katastrofální způsob psaní kódu v Laravelu.

Zde je důvod.

Když požádáme ORM, aby hledal dané zákazníky, vygeneruje se dotaz SQL, jako je tento:

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

Což je přesně podle očekávání. V důsledku toho se všechny vrácené řádky uloží do kolekce $customers uvnitř funkce kontroleru.

Nyní procházíme každého zákazníka jednoho po druhém a dostáváme jeho objednávky. Tím se provede následující dotaz. . .

SELECT * FROM orders WHERE customer_id = 22;

. . . tolikrát, kolik je zákazníků.

Jinými slovy, pokud potřebujeme získat data objednávky pro 1000 zákazníků, celkový počet provedených databázových dotazů bude 1 (pro načtení všech dat zákazníků) + 1000 (pro načtení dat objednávky pro každého zákazníka) = 1001. odtud pochází název n+1.

Můžeme to udělat lépe? Rozhodně! Pomocí toho, co je známé jako eager loading, můžeme donutit ORM, aby provedl JOIN a vrátil všechna potřebná data v jediném dotazu! Takhle:

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

Výsledná datová struktura je samozřejmě vnořená, ale data objednávky lze snadno extrahovat. Výsledný jediný dotaz je v tomto případě něco takového:

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

Jediný dotaz je samozřejmě lepší než tisíc dalších dotazů. Představte si, co by se stalo, kdyby bylo ke zpracování 10 000 zákazníků! Nebo nedej bože, kdybychom chtěli zobrazit položky obsažené v každé objednávce! Pamatujte, že název této techniky je dychtivé načítání a téměř vždy je to dobrý nápad.

Uložte konfiguraci do mezipaměti!

Jedním z důvodů flexibility Laravelu jsou tuny konfiguračních souborů, které jsou součástí rámce. Chcete změnit, jak/kde se obrázky ukládají?

No, stačí změnit soubor config/filesystems.php (alespoň při psaní). Chcete pracovat s více ovladači fronty? Klidně je popište v config/queue.php. Právě jsem počítal a zjistil jsem, že existuje 13 konfiguračních souborů pro různé aspekty rámce, což zajišťuje, že nebudete zklamáni bez ohledu na to, co chcete změnit.

Vzhledem k povaze PHP se pokaždé, když přijde nový webový požadavek, Laravel probudí, vše nabootuje a analyzuje všechny tyto konfigurační soubory, aby zjistil, jak to tentokrát udělat jinak. Až na to, že je hloupé, když se za posledních pár dní nic nezměnilo! Přestavba konfigurace při každém požadavku je plýtvání, kterému se lze (ve skutečnosti musí) vyhnout a cesta ven je jednoduchý příkaz, který Laravel nabízí:

php artisan config:cache

Co to dělá, je spojit všechny dostupné konfigurační soubory do jednoho a mezipaměť je někde pro rychlé načtení. Až se příště objeví webový požadavek, Laravel jednoduše přečte tento jediný soubor a začne pracovat.

To znamená, že ukládání konfigurace do mezipaměti je extrémně delikátní operace, která vám může vybuchnout do obličeje. Největší problém je, že jakmile zadáte tento příkaz, volání funkce env() odkudkoli kromě konfiguračních souborů vrátí hodnotu null!

Dává to smysl, když se nad tím zamyslíte. Pokud používáte ukládání do mezipaměti konfigurace, říkáte frameworku: „Víte co, myslím, že jsem věci nastavil pěkně a jsem si 100% jistý, že nechci, aby se měnily.“ Jinými slovy, očekáváte, že prostředí zůstane statické, k čemuž slouží soubory .env.

S tím, co bylo řečeno, zde jsou některá železná, posvátná a nerozbitná pravidla ukládání do mezipaměti konfigurace:

  • Udělejte to pouze na produkčním systému.
  • Udělejte to, pouze pokud jste si opravdu, opravdu jisti, že chcete konfiguraci zmrazit.
  • V případě, že se něco pokazí, zrušte nastavení pomocí php artisan cache:clear
  • Modlete se, aby škoda způsobená podniku nebyla významná!
  • Omezte automaticky načítané služby

    Aby byl Laravel užitečný, načte po probuzení spoustu služeb. Ty jsou k dispozici v souboru config/app.php jako součást klíče pole ‚providers‘. Pojďme se podívat na to, co mám v mém případě:

    /*
        |--------------------------------------------------------------------------
        | 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,
    
        ],

    Ještě jednou jsem počítal a je zde uvedeno 27 služeb! Nyní je možná budete potřebovat všechny, ale je to nepravděpodobné.

      6 Online notářský software pro vytváření a úpravu právních dokumentů

    Například v tuto chvíli stavím REST API, což znamená, že nepotřebuji poskytovatele služeb relace, poskytovatele služeb zobrazení atd. A protože dělám pár věcí po svém a neřídím se výchozím nastavením rámce , mohu také zakázat poskytovatele autentizačních služeb, poskytovatele stránkovacích služeb, poskytovatele překladových služeb atd. Celkově vzato je téměř polovina z nich pro můj případ použití zbytečná.

    Podívejte se dlouze a důkladně na svou žádost. Potřebuje všechny tyto poskytovatele služeb? Ale proboha prosím nekomentujte slepě tyto služby a tlačte do výroby! Proveďte všechny testy, zkontrolujte věci ručně na vývojářských a inscenačních strojích a buďte velmi paranoidní, než stisknete spoušť. 🙂

    Buďte moudří se zásobníky middlewaru

    Když potřebujete nějaké vlastní zpracování příchozího webového požadavku, řešením je vytvoření nového middlewaru. Nyní je lákavé otevřít app/Http/Kernel.php a umístit middleware do zásobníku webu nebo rozhraní API; tímto způsobem bude k dispozici v celé aplikaci a pokud nedělá něco rušivého (například protokolování nebo upozornění).

    Jak se však aplikace rozrůstá, může se tato sbírka globálního middlewaru stát tichou zátěží aplikace, pokud jsou všechny (nebo většina) obsaženy v každém požadavku, i když pro to neexistuje žádný obchodní důvod.

    Jinými slovy, buďte opatrní, kam přidáváte/aplikujete nový middleware. Může být pohodlnější něco globálně přidat, ale výkonnostní penalizace je dlouhodobě velmi vysoká. Vím, jakou bolest byste museli podstoupit, kdybyste selektivně aplikovali middleware pokaždé, když dojde k nové změně, ale je to bolest, kterou bych ochotně přijal a doporučil!

    Vyhněte se ORM (občas)

    Zatímco Eloquent dělá mnoho aspektů interakce s DB příjemnými, jde to na úkor rychlosti. Jako mapovač musí ORM nejen načíst záznamy z databáze, ale také vytvořit instanci objektů modelu a hydratovat (vyplnit je) daty sloupců.

    Pokud tedy provedete jednoduchý $users = User::all() a je zde například 10 000 uživatelů, framework načte 10 000 řádků z databáze a interně provede 10 000 nových User() a naplní jejich vlastnosti relevantními daty. . V zákulisí se odvádí obrovské množství práce, a pokud se databáze nachází v místě vaší aplikace, stává se úzkým hrdlem, obcházení ORM je občas dobrý nápad.

    To platí zejména pro složité dotazy SQL, kde byste museli přeskakovat spoustu obručí a psát uzávěry po uzávěrkách a přesto skončit s efektivním dotazem. V takových případech je preferováno provedení DB::raw() a zapsání dotazu ručně.

    Procházím tento výkonnostní studie, a to i pro jednoduché vložky Výmluvná je mnohem pomalejší, protože počet záznamů stoupá:

    Používejte ukládání do mezipaměti co nejvíce

    Jedním z nejlépe střežených tajemství optimalizace webových aplikací je ukládání do mezipaměti.

    Pro nezasvěcené znamená ukládání do mezipaměti předvýpočet a ukládání drahých výsledků (drahé z hlediska využití CPU a paměti) a jejich jednoduché vrácení, když se opakuje stejný dotaz.

    Například v e-shopu může narazit na to, že z 2 milionů produktů se lidé většinou zajímají o ty, které jsou čerstvě naskladněné, v určitém cenovém rozpětí a pro určitou věkovou skupinu. Dotazování se na tyto informace v databázi je plýtvání — protože dotaz se často nemění, je lepší tyto výsledky uložit někam, kde k nim máme rychlý přístup.

    Laravel má vestavěnou podporu pro několik typů ukládání do mezipaměti. Kromě použití ovladače pro ukládání do mezipaměti a budování systému ukládání do mezipaměti od základů můžete chtít použít některé balíčky Laravel, které usnadňují model mezipaměti, ukládání dotazů do mezipamětiatd.

    Uvědomte si však, že kromě určitého zjednodušeného případu použití mohou předem sestavené balíčky pro ukládání do mezipaměti způsobit více problémů, než vyřešit.

    Preferujte ukládání do mezipaměti

    Když něco ukládáte do mezipaměti v Laravelu, máte několik možností, kam uložit výsledný výpočet, který je třeba uložit do mezipaměti. Tyto možnosti jsou také známé jako ovladače mezipaměti. I když je tedy možné a naprosto rozumné používat souborový systém pro ukládání výsledků mezipaměti, není to ve skutečnosti tím, čím by ukládání do mezipaměti mělo být.

    V ideálním případě chcete použít mezipaměť v paměti (zcela žijící v paměti RAM), jako je Redis, Memcached, MongoDB atd., aby při vyšším zatížení sloužilo ukládání do mezipaměti životně důležitému použití, než aby se samo stalo úzkým hrdlem.

    Nyní si možná myslíte, že mít SSD disk je téměř totéž jako používat RAM, ale není tomu ani zdaleka. Dokonce i neformální benchmarky ukazují, že RAM překonává SSD 10-20krát, pokud jde o rychlost.

    Můj oblíbený systém, pokud jde o ukládání do mezipaměti, je Redis. Své směšně rychle (běžné je 100 000 operací čtení za sekundu) a pro velmi velké systémy mezipaměti se může rozvinout do shluk snadno.

      Jak importovat kontakty LinkedIn a zároveň zachovat soukromí vašeho e-mailu

    Uložte trasy do mezipaměti

    Stejně jako konfigurace aplikace se trasy v průběhu času příliš nemění a jsou ideálním kandidátem pro ukládání do mezipaměti. To platí zejména v případě, že nemůžete vystát velké soubory jako já a nakonec rozdělíte web.php a api.php na několik souborů. Jediný příkaz Laravel sbalí všechny dostupné trasy a uchová je po ruce pro budoucí přístup:

    php artisan route:cache

    A když nakonec přidáte nebo změníte trasy, jednoduše udělejte:

    php artisan route:clear

    Optimalizace obrazu a CDN

    Obrázky jsou srdcem a duší většiny webových aplikací. Shodou okolností jsou také největšími spotřebiteli šířky pásma a jedním z největších důvodů pomalých aplikací/webů. Pokud nahrané obrázky naivně uložíte na server a pošlete je zpět v HTTP odpovědích, necháte uniknout obrovskou příležitost k optimalizaci.

    Moje první doporučení je neukládat obrázky lokálně – je zde problém se ztrátou dat a v závislosti na zeměpisné oblasti, ve které se váš zákazník nachází, může být přenos dat bolestně pomalý.

    Místo toho jděte na řešení jako Oblačno která automaticky mění velikost a optimalizuje obrázky za běhu.

    Pokud to není možné, použijte něco jako Cloudflare pro ukládání do mezipaměti a zobrazování obrázků, když jsou uloženy na vašem serveru.

    A pokud ani to není možné, trochu vyladění softwaru webového serveru pro komprimaci aktiv a nasměrování prohlížeče návštěvníka k ukládání věcí do mezipaměti má velký význam. Zde je návod, jak by vypadal úryvek konfigurace Nginx:

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

    Jsem si vědom, že optimalizace obrázků nemá s Laravelem nic společného, ​​ale je to tak jednoduchý a mocný trik (a je tak často opomíjen), který si nemohl pomoci.

    Optimalizace autoloaderu

    Automatické načítání je elegantní, nepříliš stará funkce v PHP, která pravděpodobně zachránila jazyk před zkázou. Proces nalezení a načtení příslušné třídy dešifrováním daného řetězce jmenného prostoru však vyžaduje čas a lze se mu vyhnout v produkčních nasazeních, kde je žádoucí vysoký výkon. Laravel má na to opět řešení s jediným příkazem:

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

    Spřátelit se s frontami

    Fronty je způsob, jakým zpracováváte věci, když je jich mnoho a dokončení každé z nich trvá několik milisekund. Dobrým příkladem je odesílání e-mailů – rozšířeným případem použití ve webových aplikacích je odstřelit několik e-mailů s upozorněním, když uživatel provede nějakou akci.

    Například u nově spuštěného produktu můžete chtít, aby vedení společnosti (asi 6-7 e-mailových adres) bylo informováno, kdykoli někdo zadá objednávku nad určitou hodnotu. Za předpokladu, že vaše e-mailová brána dokáže odpovědět na váš požadavek SMTP za 500 ms, mluvíme o dobrém 3-4 sekundovém čekání na uživatele, než dojde k potvrzení objednávky. Opravdu špatný kus UX, jsem si jistý, že budete souhlasit.

    Řešením je uložit úlohy tak, jak přicházejí, sdělit uživateli, že vše proběhlo v pořádku, a zpracovat je (o několik sekund) později. Pokud dojde k chybě, lze úlohy ve frontě několikrát opakovat, než budou prohlášeny za neúspěšné.

    Kredity: Microsoft.com

    Zatímco systém řazení do front trochu komplikuje nastavení (a přidává určitou režii na monitorování), je v moderní webové aplikaci nepostradatelný.

    Optimalizace aktiv (Laravel Mix)

    U všech front-endových aktiv ve vaší Laravel aplikaci se prosím ujistěte, že existuje kanál, který zkompiluje a minifikuje všechny soubory aktiv. Ti, kterým vyhovuje systém balíčků jako Webpack, Gulp, Parcel atd., se nemusí obtěžovat, ale pokud to ještě neděláte, Laravel Mix je solidní doporučení.

    Mix je lehký (a ve vší upřímnosti nádherný!) obal kolem Webpacku, který se stará o všechny vaše soubory CSS, SASS, JS atd. pro produkci. Typický soubor .mix.js může být tak malý jako tento a přesto dokáže zázraky:

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

    To se automaticky postará o importy, minifikaci, optimalizaci a celou dobu, kdy jste připraveni na výrobu a spustíte výrobu npm run. Mix se stará nejen o tradiční soubory JS a CSS, ale také o komponenty Vue a React, které můžete mít ve svém pracovním postupu aplikace.

    Více informací tady!

    Závěr

    Optimalizace výkonu je více umění než věda – vědět, jak a kolik dělat, je důležité než to, co dělat. To znamená, že není konce, kolik a co všechno můžete v aplikaci Laravel optimalizovat.

    Ale ať už děláte cokoli, rád bych vám dal pár rad na rozloučenou – optimalizace by se měla provádět, když je k tomu pádný důvod, a ne proto, že to zní dobře nebo protože jste paranoidní z výkonu aplikace pro 100 000+ uživatelů, zatímco ve skutečnosti je jich jen 10.

    Pokud si nejste jisti, zda potřebujete aplikaci optimalizovat, nebo ne, nemusíte kopat do pověstného sršního hnízda. Funkční aplikace, která působí nudně, ale dělá přesně to, co má, je desetkrát žádanější než aplikace, která byla optimalizována do podoby mutantního hybridního superstroje, ale tu a tam upadá.

    A aby se nováček stal mistrem Laravelu, podívejte se na toto online kurz.

    Ať vaše aplikace běží mnohem, mnohem rychleji! 🙂