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.