Laravel Tips & Tricks - 7 Steps to Dramatically Improve Performance [ukr]

Talk presentation

Imagine that you've been given an old project, a Food Delivery app, with the backend written in Laravel 8 and PHP 8.0. The web and mobile interfaces communicate with the backend through an API, but it's currently performing poorly with an average response time of 600ms. The product owner has requested you to optimize the performance and wonders if it's possible to reduce it by a factor of 10. What would you do?

Topics to be Covered:

  • Nginx Cache
  • Rememberable package
  • Redis Cache
  • Queues: Redis/SQS
  • Horizon
  • Octane: Swoole / Roadrunner
  • Upgrading PHP and Laravel

While you may be familiar with most of these points and possibly already using them, the focus will be on ensuring that you're using them correctly and effectively. In a real-world example, I will demonstrate how we managed to reduce average response times by 10 times. We'll explore what's hidden under Laravel's package magic and discuss ways to mitigate the negative impact on project performance.

By the end of this performance optimization session, you will not only have improved your performance but also gained a deeper understanding of how to utilize the Laravel framework more efficiently.

Yehor Herasymchuk
Dots Platform, Founder & CTO
  • Founder/CTO at Dots Platform
  • Certified Laravel Developer
  • His team develops a saas solution for food delivery business automation
  • Has been working as a backend developer for more than 10+ years
  • Maintains a Telegram channel where he talks about the product
  • He talks about architecture and backend on his YouTube channel
  • Author and mentor of Laravel and PHP courses
  • Yehor codes for himself in free time, and as a result, he has become a winner of UaWebChallenge
  • Always tries to make things simple
  • Linkedin, Facebook

Talk transcription

Всім привіт! Дякую, дякую. Так приємно знову бути тут, і сьогодні я розкажу вам про 7 простих кроків, які можна суттєво пришвидшити ваш застосунок. Так що прищебніться, поїхали! Мене вже представили – Єгор Герасимчук, co-founder і СТО українського стартапу "Dots Platform". Сьогодні буде дуже багато слайдів, і вони будуть із кодом. Ми не будемо зупинятися, щоб детально роздивлятися кожен слайд, але для тих, хто дивиться нас у записі, конференція скоро буде на YouTube, і ви зможете переглянути всі деталі. З першого питання: "А як можна оптимізувати ваш сервіс на PHP?" Перепишіть його на клажур і можна було би закінчувати. Привіт, Олександр Соловйов, наш адепт клажур в Україні. Але не будемо зараз займатися жартами. Час оптимізації настав.

Насправді 80% вашого коду та фічей використовує менше одного відсотка користувачів. З мого досвіду випливає, що багато фічей взагалі не використовується або використовується дуже мало. Принцип Парето також нагадує, що 80% результату дається вам всього лише 20% зусиль. Так що і на цій конференції ви отримаєте 80% результату від 20% інформації. Трошки заговорився, але давайте розглянемо, куди витрачається час перед тим, як оптимізувати. Є безліч варіантів, але подивимося на типовий застосунок. Життєвий цикл від моменту, коли ми конектимося до серверів, пройшовши через мережу, вебсервер, ініціалізацію фреймворку, перевірку коду, ініціалізацію фреймворку (Symfony, Zend, Laravel), роботу з базою даних, запити до зовнішніх API, бізнес-логіка, обробка і видача даних, підготовку і компіляцію, а також завантаження даних користувачеві. І ось, якщо говорити про типовий розподіл часу, то найбільше часу йде на роботу з базою даних.

Запити до бази даних в PHP важливі, оскільки нерідко виконуються складні операції або багато циклів "for". Також важливі реквести до зовнішніх API та обробка і видача даних. Якщо ви користуєте Laravel, це підготовка даних, компіляція шаблонів. Особливо це актуально, коли ми працюємо з Laravel. Отже, є багато напрямків для оптимізації, і важливо подумати, де саме ви витрачаєте найбільше часу.Він тут повільно, тому найчастіше найбільше часу ми витрачаємо саме до бази даних, і тут це видно. На що ми можемо взагалі вплинути? Теоретично з тих 7 пунктів, які я написав, ми можемо вплинути абсолютно на все. Наприклад, ми можемо оптимізувати Network і вебсервер. Просто поставте Network ближче до серверів, і все буде працювати там на 10 мілісекунд швидше. Це може зменшити пінг, і взагалі зробить роботу швидше, але це всього лише 10 мілісекунд. Тоді у нас є ініціалізація фреймворку, на що можна впливати трошки, і ми розглянемо це сьогодні. Але основна сфера впливу – це запити до бази даних, запити до сервісів API, бізнес-логіка, обробка і видача даних. Це те, на що можна легко впливати, і ми про це розкажемо. І, звісно ж, завантаження даних користувачем.

Так який би ідеальний результат? В ідеальному світі для Google, ми хотіли б, щоб бекенд віддавав нашу сторінку за 30 мілісекунд, але краще навіть за 20 мілісекунд. Так, щоб користувач одразу бачив всі дані, іншими словами – задоволення Google та користувача. Але це ідеальний випадок, і можливо, цього досягти не так просто. Отже, перше питання – хто використовує Nginx? Підніміть руки. А хто Apache? Ще є якісь інші вебсервери? В онлайні можна висловлювати свої вибори в чаті. Nginx – це чудовий варіант для проксі-сервера, кешування та оптимізації продуктивності. Зокрема, кешування в Nginx – це просте та ефективне рішення. Ми також подивимося на приклад кешування з Nginx, яке можна застосувати до схожих випадків, таких як CDN та клауфлеєри. Кешування допомагає знизити час відповіді і полегшити життя вашому серверу. І, хоча у нас зараз не Nginx, ми розглянемо цей метод, оскільки він є ефективним та легким у використанні. Уявімо схему типового життєвого циклу запиту, який проходить через клауфлеєри, CDN та вебсервер, і як можна оптимізувати цей шлях за допомогою Nginx кешування.

Виконання nginx включає його конфігурацію у файлик. Якщо він налаштований і кеш доступний, сервер буде віддавати закешований контент користувачеві. Якщо немає налаштованого кешу, нічого не віддаватиметься. Все досить просто налаштовується. Ми, наприклад, закешовували всі публічні API-запити на 3 хвилини з використанням nginx, що призвело до значного покращення якості. У конфігурації nginx, коротко кажучи, ми кешували лише деякі запити з префіксом 'паблік API'. Всі інші запити, зокрема для фронтенду та мобільних застосунків, не кешувалися. Мобільні застосунки передавали заголовки, включаючи поняття аккаунту, для якого ці токени та дані поверталися. Ми успішно додали ці токени до кешування в Києві, використовуючи проксі-кеш.

Таким чином, ми кешували тільки ті запити, які мали публічний API, і лише на 3 хвилини. Це значно прискорило завантаження нашого застосунку. Деталі щодо цього кейсу детально розглянуті в моєму телеграм-каналі, де також є пости та посилання на інші цікаві теми. Підписуйтеся, коментуйте, і долучайтеся до обговорення! Тепер я розповім, як легко можна кешувати запити за допомогою бібліотеки Rememberable. Просто встановлюєте композером require watson/rememberable, потім вказуєте базові моделі у вашому RememberableTrait. Якщо ви цього ще не робили, ось магія: ви отримуєте статичний та не статичний метод Remember, які дозволяють вам легко кешувати дані на хвилину, секунду чи навіть 10 хвилин. Всі запити до бази даних можна кешувати дуже просто за допомогою цієї бібліотеки.

Для початку, вам просто потрібно ознайомитися з документацією. Це одна з найприкольніших бібліотек, з якою ви стикаєтеся, коли починаєте використовувати Laravel та хочете кешувати щось. Тому всім, хто ще не користувався, рекомендую випробувати. Я показав простий приклад, де кешували кількість користувачів з використанням Remember та count, і ми закешили цей результат на годину. Однак, є складніший випадок: список активних замовлень з усіма його залежностями. Ми не хочемо отримувати n+1 запитів до бази даних, тому ми повинні кешувати залежності окремо. Кожну залежність слід кешувати окремо, як показано в коді. Таким чином, при наступному входженні користувача на цю сторінку, не буде жодного реквесту до бази даних. Так ми уникнемо з'єднання з базою даних, але це ще не все. Є ще деякі оптимізації, які можна виконати, але це вже інша тема.

Як швидко оптимізувати свій код, не переписуючи його, щоб він працював набагато швидше? Можна було б сказати "перепишіть на Closure", але ні, давайте обговоримо обкешування. Якщо ви ще не ввімкнули обкешування, яке обов'язково слід робити, варто звернути на увагу, оскільки це може показати суттєвий приріст швидкості — 300 мілісекунд в рази. Не знаю, який був стан вашого проекту до цього моменту, але фактично ви можете виграти 10-20-30 мілісекунд на кожному реквесті. Оскільки ваш код все ще обкешується (компілюється і завантажується в пам'ять), ви можете заощадити час на кожному реквесті. Навіть якщо ваш код вже використовує обкешування, це також можна відключити для тестування швидкості. Обкеш — це майже "must-have" інструмент, і ви точно зекономите 20 мілісекунд, якщо ще не використовуєте його. На нашому проекті встановлення обкешу дало зниження часу виконання фреймворку на 10 мілісекунд, що суттєво не змінило характеристики проекту.

Після обкешування можна розглянути Swoole і RoadRunner. Не буду глибоко вдаватися в це питання сьогодні, але обидва ці інструменти можуть завантажувати весь код в оперативну пам'ять, щоб працювати з нею, що призводить до значного приросту продуктивності. Якщо у вас є Laravel, ви можете легко встановити Laravel Octane і налаштувати його. Це також дуже потужний інструмент, який дозволяє робити ваш код неймовірно швидким, і вам навіть не доведеться змінювати його. Встановлюєте Laravel Octane, налаштовуєте його, і все працює "з коробки", майже ідеально.

Звісно, ще є певні підводні камені, про які я розкажу пізніше, але Octane — це дійсно наркотик для ваших відносин і для розробників. Якщо ви не використовуєте це, вам точно варто подивитися. Ваш проект може працювати трошки швидше, встановивши Octane. Я встановив його на наш проект і зекономив на фреймворку 10 мілісекунд. Деталі про Octane я розповідав на одній з минулих конференцій про сервер з Laravel. Так що, хто зацікавлений, подивіться попередні доповіді та дізнайтеся більше про Laravel Octane. Воно дійсно працює — три строчки коду, та середній час відповіді вже неймовірно низький — 20 мілісекунд чи менше. Та це просто магія! Ось ще один приклад іншого нашого сервісу. Там було в районі 180-200 мілісекунд, і почали віддавати менше 50. Блин, знаєте, це просто магія! Ось ще один приклад — важкий код з великою кількістю логіки на PHP. І знову ж таки, ми суттєво зменшили роботу з нашим застосунком. Завдяки використанню постійного з'єднання з базою даних ми дуже ефективно оптимізували та прискорили стосунок. Але ось результат — колосальний.

Але є одне "але". Давайте я коротко розповім про них, оскільки код завантажується в пам'ять, всі класи повинні бути stateless. Якщо ви пишете новий проєкт, то це не проблема. Особливо якщо ви зразу знаєте, що будете працювати з Swoole або RoadRunner, і весь код буде в пам'яті. Але якщо у вас вже є існуючий фреймворк, старайтеся уникати використання view share і подивіться, як вас вони використовуються. Оскільки, якщо у вас є два контролери, один з яких представляє view компанії, а в іншому компанія не представлена, і ви використовуєте view share, то у другому реквесті компанія також може бути виведена на сторінці архіву. Це може бути проблемою, тому слід дуже обережно ставитися до view share. Я можу розповісти більше про випадки, які ми натрапляли на шляху, що було саме у нас і що робити, щоб уникнути цього.

Ще одне важливе зауваження — уникайте використання стейтфул інстансів та сінглтонів, якщо ви використовуєте Swoole або RoadRunner. Такі об'єкти мають стан, і це може призвести до проблем. Якщо у вас є middleware, який викликає об'єкт і проставляє аккаунт, і ви його використовуєте в контролері, то слід пам'ятати, що у життєвому циклі реквесту, який не закінчується після відповіді клієнту, дані залишаються в оперативній пам'яті, і це важливо враховувати. Вам слід уникати таких підходів, і передавати змінні другим аргументом функції.

Ми сподівалися, що кожен раз, коли користувач заходить на один домен, в нього буде його аккаунт. Проте, фактично, через те, що передусім карант аккаунт GET, у нас є middleware, і логіка виконується по-різному, може виникнути проблема. Зокрема, при конкурентних запитах може виникнути ситуація, коли два користувачі з різними куками отримають два різних аккаунта. Можлива ситуація, коли користувач з аккаунтом "бургер" спокійно робить замовлення аккаунтом "піца". Щоб уникнути цього, краще уникати статичних інстанцій, особливо якщо ви користуєтеся Актантом. Або робити їх під кожний реквест. Можливі випадки, коли ви кешуєте результати в статичні змінні, теж роблять проблеми при проєктуванні апі. Так що будьте обережні.

Ще один момент, на який варто звернути увагу - це інстанси. Коли ми зіткнулись із цією проблемою, в нас не було багато часу, і це вже було на продакшені. Ми просто не подумали про це раніше. Ми використовували тести, але недостатньо добре і не настільки швидко. Тоді ми просто додали інстанси під кожний реквест. Для кожного реквесту ми генеруємо свій унікальний ключ і вважаємо, що це ідентифікатор цього реквесту. Щодо переходу на Laravel Valet та впровадження Swoole, це дійсно варто результатів. Здавалося, що ця цифра для мене - це справжній меджик. Та й знову, це стало важливим аргументом для переходу на Laravel Valet.

Щодо оптимізації швидкості, є кілька рекомендацій. Одна з них - це зменшити кількість даних у відповіді. Я швидко розповім про свої випадки. Ми мали контролер, який рендерив замовлення на сторінці. Одне замовлення складало 40-50 кБ тексту, і з 200 активних замовлень це призводило до 10 МБ HTML. Ми оптимізували це, переписавши і мінімізуючи код на PHP та HTML, і отримали значний виграш в продуктивності. Ще однією рекомендацією є регулярне оновлення версій PHP і Laravel. У нашому випадку, перехід від PHP 8.0 до PHP 8.1 дав величезний приріст швидкості. Звісно, треба слідкувати за тим, щоб оновлення не призводили до нових проблем, але в загальному оновлення важливо для оптимізації продуктивності. Також, важливо відзначити, що результати можуть варіюватися в залежності від конкретного проекту і його особливостей. Бажаю успіхів у ваших оптимізаціях та розвитку проекту!

Ну, знову ж таки, це круто, але, навіть ми не виграли по швидкості після апгрейду на PHP 8.2 і Laravel 10, але фічі ми стали робити швидше. Ну, але це не точно, так що у нас залишилася одна штука, яку я хочу би розказати, і дуже небагато часу тому, так що швидко накидаю кейси. Прийшов до нас продакт і каже, що в API працює вже супершвидко, але замовлення створюється повільно, 2-3 секунди. Зробіть що-небудь. Ну, закатимо рукава і полетіли. Давайте дивитися на код. Подивіться на код.

У нас є OrderController, куди прилітає наше замовлення. Далі він викликає OrderService, в якому один метод, в якому є один метод createOrder, і в ньому метод handle. Так виглядає та ідеальна та ідея, правда? Та де проблема? А проблема ось тут. Хто зможе, то дивіться у реплеї, поставте на паузу, можете подивитися, що взагалі ми тут робимо. Але одразу видно, що робиться тут багато різних речей, і все це робиться синхронно. У нас було більше сотні запитів до бази даних на синхронному реквесті, і ще були запити до внутрішніх мікросервісів та сторонніх API. Та і повна-повна жопа. Що нам з цим робити?

Звісно, використовувати чергу, це перше, що приходить в нам на думку. Redis, RabbitMQ, або навіть базу даних в будь-якому разі черги вам сильно пришвидшать роботу. Про базу даних я пошуткував; бази даних краще не використовувати як драйвер для черг. Як це буде виглядати? Ми в OrderService, аби не змінювати OrderController і Handler, ми просто викликаємо createOrderJob і куди передамо наш карт і нашого користувача, хто створює замовлення. Але подивіться важливо, я червоним виділив там Order, де це те, що ми повертали до цього саме замовлення. Так ми так не можемо робити, тому що зараз створюємо замовлення асинхронно.

Отже, використовувати ID або ще щось замість цієї моделі. І хто був уважним, помітив, що в нас при створенні замовлення в Handler ми проставляли встався якось, і це по суті, те, що ми вставляли унікальний для замовлення. Так, і таким чином в контролері, якщо подивитися, ми вже видаємо не саме замовлення, можемо віддати одяг цього замовлення і далі з цим працювати. Інакше все круто, та, наче, все прикольно, користувач одразу бачить, що замовлення створюється, але реально воно створюється через 2-3 секунди.

Так, в нас знову ж таки ми просто винесли все. Досить багато запитів. Тому давайте ми в черзі ще викличемо інші черги. І ось тут ніхто нам не забороняє користатися таким підходом, коли є багато черг, і вони викликаються з черги. І основне, що тут треба зрозуміти, це, що основне замовлення ми створюємо одразу в методі createOrder. Ми там створили всі базові дані, і для нас було важливо зрозуміти, що користувач з таким номером телефона хотів створити замовлення. Далі навіть, якщо в нас паде черга, все піде не так, ми не будемо знати склад цього замовлення, що там в нього було. Ми вже можемо сказати: 'Пробачте, наші програмісти не косячили, затолкнувши цьому користувачеві, сказати, що ви ми бачимо ваше замовлення, але не бачимо, що ви там замовляли, будь ласка, можете продублювати.' Ми модифікуємо його, і таким чином, навіть якщо щось пало, щось пішло не так, то ми зможемо все швидко зрозуміти. Так, це магія для користувача, все стало супер, але що там у вас по навантаженню?

Ну, я не встигаю розповісти зараз про цю всю історію і останній слайд. Те, що я хотів показати, це те, що щось пішло не так. Ми за кількістю замовлень виросли, насправді виросли. Я про це розкажу у своєму телеграм-канальчику. Що ми зробили і тому ж, я проскочу до останнього слайду, скажу, що наше резюме. Ми сьогодні проговорили кешування за допомогою Nginx, проксі-кеш, проговорили кешування запитів до баз даних із Redis та Rememberable.

Проговорили, як плаває апгрейд Laravel на вашому застосунку, його швидкість. Проговорили використання PHP-FPM, об кешу та супер-наркотики Laravel Octane. Проговорили мінімізацію та як це впливає, чому це треба робити. Проговорили про черги та повернення ID, а не моделей. І ще, я хотів розказати про особливості в чергах, як там все працює, як працює трейт серіалайз моделей. Про це розкажу окремо. Я реально вибачаюсь, не встиг вкластися в час, але давайте, якщо є питання, ми зараз його швидко задамо і перейдемо в Discord-чати, де я з радістю відповім на всі ваші питання.

Sign in
Or by mail
Sign in
Or by mail
Register with email
Register with email
Forgot password?