Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

Middle: питання на співбесіді з теми «Продуктивність»

Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.

3 питання

Крім кешу конфігурації й маршрутів, php artisan optimize створює ще два кеші, про які часто забувають.

event:cache - мапа подій і слухачів.

Laravel може сам знаходити слухачів: сканує каталог app/Listeners і за типом параметра handle(OrderShipped $event) визначає, на яку подію слухач підписаний. Зручно, але сканування файлів і Reflection на кожен запит - зайва робота.

php artisan event:cache    # записати мапу в bootstrap/cache/events.php
php artisan event:clear

Після кешування Laravel бере мапу з файлу, нічого не скануючи.

Коли поводиться неочікувано:

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

view:cache - скомпільовані шаблони Blade.

Blade компілює кожен шаблон у звичайний PHP-файл у storage/framework/views при першому зверненні й перекомпільовує, якщо шаблон змінився. view:cache компілює всі шаблони заздалегідь:

  • перший запит після деплою не витрачає час на компіляцію;
  • помилка синтаксису Blade виявляється під час деплою, а не в користувача.

Коли поводиться неочікувано:

  • права: скомпільовані файли створені від root під час деплою, а PHP-FPM працює від www-data - при спробі перекомпілювати виникає помилка доступу;
  • кілька серверів чи контейнерів - кеш формується на кожному окремо, і він має відповідати коду саме цього сервера;
  • змінені шаблони без очищення кешу в нестандартному деплої (файли змінено на місці) - Blade перевіряє час зміни файлів, але з OPcache і validate_timestamps=0 старий скомпільований PHP може лишитися в пам'яті.

Загальне правило для всіх кешів: у деплої спершу оновити код і залежності, потім php artisan optimize (конфігурація, маршрути, події, представлення), потім перезапустити процеси, що тримають код у пам'яті (PHP-FPM reload чи OPcache reset, воркери черг, Octane). А локально - php artisan optimize:clear, якщо поведінка не відповідає коду.

Що не кешується цими командами: дані застосунку (Cache::), відповіді й запити - це окремий рівень, і optimize:clear його не чіпає.

Докладніше в документації: Події: пошук слухачів на продакшені

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

public function store(StoreOrderRequest $request): RedirectResponse
{
    $order = Order::create($request->validated());

    defer(fn () => Metrics::reportOrder($order));

    return redirect()->route('orders.show', $order);
}

Користувач не чекає на звернення до сервісу метрик, а окремий воркер черги не потрібен.

Як це працює: Laravel відправляє відповідь і закриває з'єднання, а процес PHP продовжує виконання. Для команд Artisan і завдань черги відкладені функції виконуються в кінці.

Деталі:

  • якщо відповідь з помилкою (4xx/5xx), відкладена функція за замовчуванням не виконується; ->always() змінює це;
  • defer(fn () => ..., 'name') з іменем дозволяє скасувати: defer()->forget('name');
  • у тестах їх можна виконувати одразу через $this->withoutDefer().

Коли defer(), а коли черга:

  • defer - коротка, некритична робота: метрики, логування, прогрів кешу. Якщо процес упаде, вона просто не виконається, повторів немає;
  • черга - усе, що має гарантовано виконатися, довго триває чи потребує повторів: листи, платежі, обробка файлів.

Пам'ятайте, що поки працює відкладена функція, процес PHP зайнятий: важка робота тут з'їдає воркери, які мали б обслуговувати запити.

Докладніше в документації: Відкладені функції

Через фасад Concurrency - він запускає замикання одночасно й повертає результати в тому самому порядку.

use Illuminate\Support\Facades\Concurrency;

[$userCount, $orderCount, $rates] = Concurrency::run([
    fn () => DB::table('users')->count(),
    fn () => DB::table('orders')->count(),
    fn () => Http::get('https://api.example.com/rates')->json(),
]);

Якщо кожна операція триває секунду, послідовно це три секунди, паралельно - близько однієї.

Як це працює: драйвер за замовчуванням process серіалізує кожне замикання й виконує його в окремому дочірньому процесі PHP через Artisan. Є драйвер fork (швидший, лише CLI, потрібен пакет spatie/fork) і sync для тестів.

Обмеження:

  • кожен дочірній процес завантажує застосунок - на коротких операціях ці накладні витрати з'їдають виграш;
  • замикання серіалізуються: не можна захоплювати те, що не серіалізується (з'єднання, ресурси, $this складного об'єкта);
  • результат має бути серіалізованим.

Альтернативи:

  • кілька HTTP-запитів - Http::pool(), без нових процесів;
  • робота, результат якої не потрібен відповіді, - Concurrency::defer() або черга.

Найчастіше це доречно для дашбордів з кількома незалежними важкими агрегаціями чи для зведення даних з кількох зовнішніх API.

Докладніше в документації: Паралельні завдання