Як оптимізувати веб-програму PHP Laravel для високої продуктивності?

Laravel – це багато речей. Але швидкий не є одним із них. Давайте навчимося деяких хитрощів, щоб пришвидшити роботу!

Жоден розробник PHP не залишиться без уваги Laravel ці дні. Вони або розробники молодшого чи середнього рівня, яким подобається швидка розробка, яку пропонує Laravel, або вони є розробниками старшого рівня, які змушені вивчати Laravel через тиск ринку.

У будь-якому випадку, не можна заперечувати, що Laravel оживив екосистему PHP (я, звичайно, давно б покинув світ PHP, якби там не було Laravel).

Фрагмент (дещо виправданої) самохвали від Laravel

Однак, оскільки Laravel намагається полегшити вам роботу, це означає, що він виконує масу роботи, щоб забезпечити комфортне життя розробника. Усі «чарівні» функції Laravel, які, здається, працюють, мають шари за шарами коду, який потрібно налаштовувати під час кожного запуску функції. Навіть простий виняток відстежує глибину кролячої нори (зверніть увагу, де починається помилка, аж до основного ядра):

Для того, що здається помилкою компіляції в одному з представлень, існує 18 викликів функцій для відстеження. Я особисто натрапив на 40, і їх може бути більше, якщо ви використовуєте інші бібліотеки та плагіни.

Справа в тому, що за замовчуванням цей шар коду робить Laravel повільним.

Наскільки повільно працює Laravel?

Чесно кажучи, відповісти на це питання неможливо з кількох причин.

По-перше, не існує загальноприйнятого, об’єктивного, розумного стандарту для вимірювання швидкості веб-додатків. Швидше чи повільніше порівняно з чим? За яких умов?

По-друге, веб-додаток залежить від багатьох речей (база даних, файлова система, мережа, кеш тощо), що говорити про швидкість просто безглуздо. Дуже швидка веб-програма з дуже повільною базою даних є дуже повільною веб-програмою. 🙂

Але саме через цю невизначеність тести популярні. Хоча вони нічого не значать (див це і це), вони забезпечують певну систему відліку та допомагають нам не зійти з розуму. Тому, маючи напоготові кілька щіпок солі, давайте сформулюємо неправильне, приблизне уявлення про швидкість серед фреймворків PHP.

Переходячи на цей досить респектабельний GitHub джерелоось як виглядають фреймворки PHP у порівнянні:

Ви можете навіть не помітити тут Laravel (навіть якщо сильно примружитесь), якщо не закинете свій футляр прямо до кінця хвоста. Так, дорогі друзі, Laravel приходить останнім! Звичайно, більшість із цих «фреймворків» не дуже практичні чи навіть корисні, але це говорить нам про те, наскільки млявим є Laravel порівняно з іншими більш популярними.

Зазвичай ця «повільність» не проявляється в програмах, оскільки наші повсякденні веб-програми рідко досягають високих показників. Але як тільки вони це роблять (скажімо, понад 200-500 одночасних серверів), сервери починають задихатися і вмирати. Це час, коли навіть додаткове обладнання не вирішить проблему, а рахунки за інфраструктуру зростають настільки швидко, що ваші високі ідеали хмарних обчислень руйнуються.

Але привіт, бадьоріться! Ця стаття не про те, що не можна робити, а про те, що можна зробити. 🙂

Хороша новина полягає в тому, що ви можете зробити багато, щоб ваш додаток Laravel працював швидше. Кілька разів швидко. Так, без жартів. Ви можете змусити ту саму кодову базу стати балістичною та заощаджувати кілька сотень доларів на рахунках за інфраструктуру/хостинг щомісяця. як? Давайте приступимо до цього.

Чотири види оптимізації

На мій погляд, оптимізацію можна виконати на чотирьох різних рівнях (тобто, якщо мова йде про програми PHP):

  • Рівень мови: це означає, що ви використовуєте швидшу версію мови та уникаєте певних функцій/стилів кодування в мові, які роблять ваш код повільним.
  • Рівень фреймворку: це речі, які ми розглянемо в цій статті.
  • Рівень інфраструктури: налаштування менеджера процесів PHP, веб-сервера, бази даних тощо.
  • Апаратний рівень: перехід до кращого, швидшого та потужнішого апаратного хостинг-провайдера.

Усі ці види оптимізації мають місце (наприклад, оптимізація PHP-fpm є досить важливою та потужною). Але в центрі уваги цієї статті будуть оптимізації виключно типу 2: ті, що пов’язані з фреймворком.

До речі, нумерація не має ніякого обґрунтування, і це не прийнятий стандарт. Я щойно їх вигадав. Будь ласка, ніколи не цитуйте мене й не кажіть: «Нам потрібна оптимізація типу 3 на нашому сервері», інакше керівник вашої команди вб’є вас, знайде мене, а потім уб’є мене. 😀

І ось, нарешті, ми прибули до землі обітованої.

Зверніть увагу на n+1 запит до бази даних

Проблема запиту n+1 є типовою при використанні ORM. У Laravel є потужна ORM під назвою Eloquent, яка настільки гарна й зручна, що ми часто забуваємо дивитися на те, що відбувається.

Розглянемо дуже поширений сценарій: відображення списку всіх замовлень, розміщених заданим списком клієнтів. Це досить поширене явище в системах електронної комерції та будь-яких інтерфейсах звітності загалом, де нам потрібно відобразити всі сутності, пов’язані з деякими сутностями.

  Як зламати пароль WiFi

У Laravel ми можемо уявити функцію контролера, яка виконує роботу так:

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

Солодкий! А головне, елегантно, красиво. 🤩🤩

На жаль, це катастрофічний спосіб написання коду в Laravel.

Ось чому.

Коли ми просимо ORM шукати вказаних клієнтів, генерується такий SQL-запит:

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

Що точно відповідає очікуванням. У результаті всі повернуті рядки зберігаються в колекції $customers у функції контролера.

Тепер ми переглядаємо кожного клієнта одного за іншим і отримуємо їхні замовлення. Це виконує наступний запит. . .

SELECT * FROM orders WHERE customer_id = 22;

. . . стільки разів, скільки клієнтів.

Іншими словами, якщо нам потрібно отримати дані про замовлення для 1000 клієнтів, загальна кількість виконаних запитів до бази даних буде 1 (для отримання всіх даних клієнтів) + 1000 (для отримання даних про замовлення для кожного клієнта) = 1001. Це звідки походить назва n+1.

Чи можемо ми зробити краще? Звичайно! Використовуючи те, що називається швидким завантаженням, ми можемо змусити ORM виконати JOIN і повернути всі необхідні дані в одному запиті! Подобається це:

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

Отримана структура даних є вкладеною, але дані замовлення можна легко витягти. Отриманий єдиний запит у цьому випадку виглядає приблизно так:

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

Один запит, звичайно, краще, ніж тисяча додаткових запитів. Уявіть, що станеться, якби потрібно обробляти 10 000 клієнтів! Або не дай Бог, якби ми також хотіли показати елементи, що містяться в кожному замовленні! Пам’ятайте, що назва техніки – «завантаження», і це майже завжди гарна ідея.

Кешуйте конфігурацію!

Однією з причин гнучкості Laravel є безліч конфігураційних файлів, які є частиною фреймворку. Хочете змінити спосіб/де зберігаються зображення?

Ну, просто змініть файл config/filesystems.php (принаймні на момент написання). Хочете працювати з кількома драйверами черги? Не соромтеся описати їх у config/queue.php. Я щойно підрахував і виявив, що існує 13 файлів конфігурації для різних аспектів фреймворку, що гарантує, що ви не розчаруєтеся, незалежно від того, що ви хочете змінити.

Враховуючи природу PHP, кожного разу, коли надходить новий веб-запит, Laravel прокидається, завантажує все та аналізує всі ці конфігураційні файли, щоб зрозуміти, як цього разу зробити щось по-іншому. Хіба що це дурість, якщо за останні дні нічого не змінилося! Переналаштування конфігурації за кожним запитом — це марна трата, якої можна (насправді, потрібно) уникнути, а вихід — це проста команда, яку пропонує Laravel:

php artisan config:cache

Це об’єднує всі доступні конфігураційні файли в один, а кеш знаходиться десь для швидкого пошуку. Наступного разу, коли буде веб-запит, Laravel просто прочитає цей єдиний файл і почне роботу.

Тим не менш, кешування конфігурації є надзвичайно делікатною операцією, яка може розірватися вам прямо на обличчі. Найбільша помилка полягає в тому, що після введення цієї команди виклики функції env() звідусіль, окрім файлів конфігурації, повернуть значення null!

Це має сенс, якщо подумати про це. Якщо ви використовуєте кешування конфігурації, ви говорите структурі: «Знаєте що, я думаю, що все налаштував добре, і я на 100% впевнений, що не хочу, щоб вони змінювалися». Іншими словами, ви очікуєте, що середовище залишатиметься статичним, для чого й призначені файли .env.

З огляду на це, ось деякі залізні, священні, непорушні правила кешування конфігурації:

  • Робіть це лише на виробничій системі.
  • Робіть це, лише якщо ви справді впевнені, що хочете заморозити конфігурацію.
  • Якщо щось піде не так, скасуйте налаштування за допомогою php artisan cache:clear
  • Моліться, щоб збиток бізнесу не був значним!
  • Скоротіть автозавантаження служб

    Щоб бути корисним, Laravel завантажує масу служб, коли виходить з режиму сну. Вони доступні у файлі config/app.php як частина ключа масиву «providers». Давайте подивимося, що я маю в моєму випадку:

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

    Ще раз порахувала, а там 27 послуг! Зараз вам можуть знадобитися всі вони, але це малоймовірно.

    Наприклад, зараз я створюю REST API, що означає, що мені не потрібен постачальник послуг сеансів, постачальник послуг перегляду тощо. Оскільки я роблю кілька речей по-своєму і не дотримуюся стандартних параметрів фреймворку , я також можу вимкнути постачальника послуг автентифікації, постачальника послуг розбиття на сторінки, постачальника послуг перекладу тощо. Загалом, майже половина з них непотрібні для мого випадку використання.

      Як перетворити фотографії з HEIC в JPG

    Уважно подивіться на свою заявку. Чи потрібні всі ці постачальники послуг? Але заради Бога, будь ласка, не коментуйте сліпо ці послуги та не переходьте до виробництва! Виконуйте всі тести, перевіряйте речі вручну на машинах для розробників і тестових машинах і будьте дуже-дуже параноїком, перш ніж натиснути на курок. 🙂

    Будьте мудрими зі стеками проміжного ПЗ

    Якщо вам потрібна спеціальна обробка вхідного веб-запиту, створення нового проміжного програмного забезпечення є відповіддю. Тепер виникає спокуса відкрити app/Http/Kernel.php і вставити проміжне програмне забезпечення в веб або стек API; таким чином він стає доступним у всій програмі, і якщо він не робить щось нав’язливе (наприклад, журналювання чи сповіщення).

    Однак у міру зростання програми ця колекція глобального проміжного програмного забезпечення може стати тихим тягарем для програми, якщо всі (або більшість) з них присутні в кожному запиті, навіть якщо для цього немає бізнес-причин.

    Іншими словами, будьте обережні з тим, де ви додаєте/застосовуєте нове проміжне програмне забезпечення. Може бути зручніше додати щось глобально, але втрата продуктивності буде дуже високою в довгостроковій перспективі. Я знаю, якого болю вам довелося б зазнати, якщо б ви вибірково застосовували проміжне програмне забезпечення щоразу, коли з’являються нові зміни, але це біль, який я б охоче витримав і порекомендував!

    Уникайте ORM (іноді)

    Хоча Eloquent робить багато аспектів взаємодії з БД приємними, це відбувається за рахунок швидкості. Будучи картографом, ORM має не лише отримувати записи з бази даних, але й створювати екземпляри об’єктів моделі та гідратувати (заповнювати) їх даними стовпців.

    Отже, якщо ви виконуєте простий $users = User::all() і є, скажімо, 10 000 користувачів, фреймворк отримає 10 000 рядків із бази даних і внутрішньо виконає 10 000 нових User() і заповнить їхні властивості відповідними даними . Це величезна кількість роботи, яка виконується за лаштунками, і якщо база даних, у якій ви використовуєте програму, стає вузьким місцем, іноді варто обійти ORM.

    Це особливо вірно для складних SQL-запитів, де вам доведеться перестрибувати багато обручів і писати замикання за замиканнями, а в кінцевому підсумку отримати ефективний запит. У таких випадках бажано виконати DB::raw() і написати запит вручну.

    Проходячи це дослідження продуктивності навіть для простих вставок Eloquent набагато повільніше, оскільки кількість записів зростає:

    Використовуйте кешування якомога більше

    Одним із найбільш збережених секретів оптимізації веб-додатків є кешування.

    Для непосвячених кешування означає попереднє обчислення та зберігання дорогих результатів (дорогих з точки зору використання процесора та пам’яті) і просто повернення їх, коли той самий запит повторюється.

    Наприклад, у магазині електронної комерції можна зустріти 2 мільйони товарів, які здебільшого людей цікавлять ті, що свіжі, у певному ціновому діапазоні та для певної вікової групи. Запитувати цю інформацію в базі даних марнотратно — оскільки запит не змінюється часто, краще зберігати ці результати десь, до якого ми можемо швидко отримати доступ.

    Laravel має вбудовану підтримку кількох типів кешування. На додаток до використання драйвера кешування та побудови системи кешування з нуля, ви можете використовувати деякі пакети Laravel, які полегшують кешування моделі, кешування запитіві т.д.

    Але зауважте, що окрім певного спрощеного варіанту використання, попередньо зібрані пакети кешування можуть спричинити більше проблем, ніж вирішити.

    Віддавайте перевагу кешуванню в пам’яті

    Коли ви кешуєте щось у Laravel, у вас є кілька варіантів, де зберігати отримані обчислення, які потрібно кешувати. Ці варіанти також відомі як драйвери кешу. Таким чином, хоча використання файлової системи для зберігання результатів кешу можливо і цілком розумно, це зовсім не те, чим має бути кешування.

    В ідеалі ви бажаєте використовувати кеш у пам’яті (цілком живе в оперативній пам’яті), як-от Redis, Memcached, MongoDB тощо, щоб за високих навантажень кешування мало важливе значення, а не ставало саме вузьким місцем.

    Тепер ви можете подумати, що мати SSD-диск майже те саме, що використовувати оперативну пам’ять, але це навіть близько не так. Навіть неформальний контрольні показники показують, що оперативна пам’ять перевершує SSD у 10-20 разів, коли справа стосується швидкості.

    Моєю улюбленою системою, коли справа доходить до кешування, є Redis. Його смішно швидко (100 000 операцій читання в секунду є звичайним явищем), і для дуже великих систем кешу можна перетворити на кластер легко.

    Кешуйте маршрути

    Так само, як і конфігурація програми, маршрути не сильно змінюються з часом і є ідеальним кандидатом для кешування. Це особливо вірно, якщо ви не можете терпіти великі файли, як я, і в кінцевому підсумку розділяєте ваші web.php і api.php на кілька файлів. Одна команда Laravel збирає всі доступні маршрути та зберігає їх під рукою для майбутнього доступу:

    php artisan route:cache

    І коли ви все-таки додаєте або змінюєте маршрути, просто виконайте:

    php artisan route:clear

    Оптимізація зображення та CDN

    Зображення є серцем і душею більшості веб-програм. За збігом обставин вони також є найбільшими споживачами пропускної здатності та однією з головних причин повільної роботи програм/веб-сайтів. Якщо ви просто зберігаєте завантажені зображення на сервері та надсилаєте їх назад у відповідях HTTP, ви пропускаєте величезну можливість оптимізації.

      Як змінити зображення профілю Gmail

    Моя перша рекомендація — не зберігати зображення локально — існує проблема втрати даних, і залежно від того, в якому географічному регіоні перебуває ваш клієнт, передача даних може бути дуже повільною.

    Замість цього виберіть таке рішення, як Хмарний який автоматично змінює розмір і оптимізує зображення на льоту.

    Якщо це неможливо, використовуйте щось на зразок Cloudflare для кешування та обслуговування зображень, поки вони зберігаються на вашому сервері.

    І якщо навіть це неможливо, трішки налаштуйте програмне забезпечення веб-сервера, щоб стиснути ресурси та направити браузер відвідувача на кешування речей, що має велике значення. Ось як виглядатиме фрагмент конфігурації 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";
        }
    }

    Я знаю, що оптимізація зображень не має нічого спільного з Laravel, але це такий простий і потужний трюк (і ним так часто нехтують), що я не міг допомогти собі.

    Оптимізація автозавантажувача

    Автозавантаження — це гарна, не така стара функція в PHP, яка, мабуть, врятувала мову від загибелі. Тим не менш, процес пошуку та завантаження відповідного класу шляхом розшифровки заданого рядка простору імен потребує часу, і його можна уникнути у виробничих розгортаннях, де бажана висока продуктивність. Знову ж таки, Laravel має однокомандне рішення для цього:

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

    Подружіться з чергами

    Черги це те, як ви обробляєте речі, коли їх багато, і для виконання кожного з них потрібно кілька мілісекунд. Хорошим прикладом є надсилання електронних листів — широко поширеним випадком використання у веб-додатках є надсилання кількох сповіщень електронною поштою, коли користувач виконує певні дії.

    Наприклад, у нещодавно випущеному продукті ви можете захотіти, щоб керівництво компанії (приблизно 6-7 адрес електронної пошти) отримувало сповіщення щоразу, коли хтось розміщує замовлення вище певної вартості. Якщо припустити, що ваш шлюз електронної пошти може відповісти на ваш запит SMTP за 500 мс, ми говоримо про хороші 3-4 секунди очікування для користувача, перш ніж почнеться підтвердження замовлення. Дуже погана частина UX, я впевнений, що ви будете згоден.

    Засіб полягає в тому, щоб зберігати завдання, коли вони надходять, повідомляти користувачеві, що все пройшло добре, і обробляти їх (через кілька секунд) пізніше. Якщо сталася помилка, завдання в черзі можна повторити кілька разів, перш ніж їх буде оголошено невдалими.

    Авторство: Microsoft.com

    Хоча система масового обслуговування дещо ускладнює налаштування (і додає певні витрати на моніторинг), вона незамінна в сучасній веб-програмі.

    Оптимізація активів (Laravel Mix)

    Для будь-яких інтерфейсних активів у вашій програмі Laravel переконайтеся, що є конвеєр, який компілює та мінімізує всі файли ресурсів. Тим, кому подобається система пакетування, як-от Webpack, Gulp, Parcel тощо, не потрібно турбуватися, але якщо ви ще цього не робите, Laravel Mix є надійною рекомендацією.

    Mix — це легка (і чудова, чесно кажучи!) оболонка Webpack, яка піклується про всі ваші файли CSS, SASS, JS тощо для виробництва. Типовий файл .mix.js може бути таким маленьким, як цей, і все одно творити чудеса:

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

    Це автоматично подбає про імпорт, мініфікацію, оптимізацію та весь процес, коли ви будете готові до виробництва та запустите npm run production. Mix піклується не лише про традиційні файли JS і CSS, а й про компоненти Vue і React, які можуть бути у вашому робочому процесі програми.

    Більше інформації тут!

    Висновок

    Оптимізація продуктивності — це більше мистецтво, ніж наука — важливіше знати, як і скільки робити. Тим не менш, немає кінця тому, скільки і що можна оптимізувати в програмі Laravel.

    Але що б ви не робили, я хотів би залишити вам кілька порад на прощання — оптимізацію слід проводити, коли є вагома причина, а не тому, що це звучить добре чи тому, що ви параноїка щодо продуктивності програми для 100 000+ користувачів, а насправді є лише 10.

    Якщо ви не впевнені, чи потрібно вам оптимізувати свою програму чи ні, вам не потрібно розбивати осине гніздо. Працююча програма, яка здається нудною, але виконує саме те, що має, у десять разів більш бажана, ніж програма, оптимізована в гібридну супермашину-мутант, але час від часу падає.

    І, щоб новачок став майстром Laravel, перегляньте це онлайн курс.

    Нехай ваші програми працюють набагато, набагато швидше! 🙂