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

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

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

8 питань

Оптимізація йде кількома шарами:

База даних

  • Усунення N+1 (eager loading with()), правильні індекси, аналіз через EXPLAIN.
  • Read/write репліки, кешування важких запитів.

Кешування

  • Cache::remember() для дорогих обчислень; повне кешування сторінок/фрагментів.
  • php artisan optimize (config/route/view/event cache), OPcache.

Фонова робота

  • Винесення повільних завдань (email, обробка медіа, виклики API) у черги.

Інфраструктура

  • Laravel Octane (Swoole/FrankenPHP) тримає застосунок у пам'яті.
  • CDN для статики, горизонтальне масштабування за load balancer, спільні сесії/кеш у Redis.

Перед оптимізацією - профілювання (Telescope, Clockwork, Debugbar), щоб бити по реальних вузьких місцях.

Докладніше в документації: Оптимізація для деплою

Octane запускає застосунок через high-performance сервери (Swoole, FrankenPHP, RoadRunner): фреймворк бутстрапиться один раз і тримається в пам'яті, обслуговуючи наступні запити без повторної ініціалізації.

php artisan octane:start --server=frankenphp

Це усуває оверхед завантаження на кожному запиті й дає кратний приріст RPS.

Підводні камені: оскільки процес довготривалий, треба уникати витоків стану між запитами - статичні властивості, синглтони з накопиченим станом, глобальні змінні можуть «протікати» від запиту до запиту. Octane надає хуки flush і перезапускає воркери для безпеки.

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

Ключове правило - не тримати весь файл у пам'яті.

  • Стрімінг замість читання цілком:
    return Storage::disk('s3')->response($path); // стрім на скачування
    Storage::writeStream($path, fopen($source, 'r')); // стрім на запис
    
  • Direct uploads на S3 - клієнт вантажить напряму в сховище за pre-signed URL, минаючи PHP-процес (не блокує воркер, обходить ліміти upload_max_filesize).
  • Chunked upload - великі файли частинами (resumable).
  • Фонова обробка - конвертацію відео/зображень виносити в черги.
  • Враховувати max_execution_time, таймаути nginx і ліміти пам'яті воркера.

Докладніше в документації: Зберігання файлів (стрімінг)

У кластері з кількох інстансів локальний лічильник не годиться - потрібен спільний стан у Redis. Фасад RateLimiter під капотом використовує атомарні операції Redis (INCR + EXPIRE), тож підрахунок точний між усіма серверами.

RateLimiter::attempt(
    key: 'send-sms:'.$user->id,
    maxAttempts: 5,
    callback: fn () => $this->sendSms(),
    decaySeconds: 60,
);

Для складніших схем - алгоритми sliding window чи token bucket на Lua-скриптах (атомарність на стороні Redis, без гонок). Ключове: лічильник і його TTL мають змінюватися атомарно, інакше за конкурентного доступу ліміт «протікає».

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

Одна команда робить більшість:

php artisan optimize        # config + route + view + event cache

Окремо:

php artisan config:cache # об'єднує конфіг у один файл
php artisan route:cache # компілює маршрути
php artisan view:cache # прекомпілює Blade
php artisan event:cache # кеш мапінгу подій/слухачів
composer install --no-dev --optimize-autoloader

Додатково: увімкнений OPcache (а краще з JIT), prebuilt ассети (npm run build).

Підводний камінь: після config:cache виклики env() поза config/ повертають null - усі env-значення мають читатися лише у конфіг-файлах. На деплої не забути php artisan optimize:clear перед повторним кешуванням.

Докладніше в документації: Оптимізація для деплою

PHP-FPM - «shared nothing»: кожен запит стартує з чистого стану, наприкінці все звільняється. Просто, безпечно, але є оверхед бутстрапу фреймворку на кожному запиті.

Octane тримає застосунок у пам'яті між запитами → кратно вищий throughput і нижча латентність.

PHP-FPM Octane
Стан між запитами чистий зберігається
Throughput нижчий значно вищий
Ризик витоків стану немає є
Складність деплою проста вища (воркери, рестарти)

Ціна Octane: треба остерігатися «протікання» стану (статика, синглтони, глобальні змінні), правильно скидати/перезапускати воркери, уважно з пам'яттю. FPM лишається розумним дефолтом, доки немає потреби в екстремальній продуктивності.

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

chunk() гортає сторінки через OFFSET. Якщо в циклі змінювати колонку, за якою фільтрується запит, записи «з'їжджають», і частину буде пропущено.

// Погано: після першої порції активних стало на 100 менше,
// а друга порція бере OFFSET 100 - і перескакує через 100 записів
User::where('active', false)->chunk(100, function ($users) {
    $users->each->update(['active' => true]);
});

// Добре: курсор за id, а не номер сторінки
User::where('active', false)->chunkById(100, function ($users) {
    $users->each->update(['active' => true]);
});

chunkById() бере наступну порцію за id > останній, тож зміни в уже обробленому не впливають.

Як обирати інструмент для великих обсягів:

  • chunkById() / lazyById() - порціями з постійною пам'яттю; lazyById() віддає LazyCollection, з якою зручніше працювати як з потоком;
  • cursor() - один запит і по одній моделі в пам'яті, але драйвер бази часто буферизує весь результат, а жадібне завантаження зв'язків неможливе;
  • масовий update() - якщо логіка вміщається в SQL, один запит UPDATE ... WHERE у сотні разів швидший за будь-який цикл (але без подій моделей).

Що ще важливо на мільйонах рядків:

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

Докладніше в документації: Розбиття результатів на частини

Через заголовки Cache-Control і ETag - тоді браузер чи CDN не звертаються до застосунку або отримують коротку відповідь «не змінилося».

Route::middleware('cache.headers:public;max_age=3600;etag')->group(function () {
    Route::get('/privacy', PrivacyController::class);
    Route::get('/sitemap.xml', SitemapController::class);
});

Що означають директиви:

  • public - відповідь можна зберігати в спільних кешах (CDN, проксі); private - лише в браузері;
  • max_age / s_maxage - скільки секунд вважати свіжою; s_maxage - лише для спільних кешів, зручно дати CDN довший термін, ніж браузеру;
  • etag - Laravel рахує хеш тіла відповіді. Якщо браузер надіслав той самий If-None-Match, відповідь буде 304 Not Modified без тіла.

Чого ETag не дає: застосунок усе одно виконує весь запит і рендеринг - економиться лише трафік. Справжня економія серверу - коли CDN віддає відповідь сам за max_age.

Головна небезпека - персоналізовані сторінки. Якщо сторінку з ім'ям користувача чи CSRF-токеном позначити public, CDN віддасть її наступному відвідувачу. Тому:

  • public лише для сторінок, однакових для всіх;
  • сесійні cookie у відповіді зазвичай роблять її некешованою для CDN, і це правильно;
  • для авторизованих - private, no-store.

Статичні ассети з хешем у назві (Vite) кешують на рік з immutable - ім'я зміниться разом зі вмістом.

Докладніше в документації: Middleware Cache-Control