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

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

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

4 питання

Перший крок - виміряти. memory_get_usage() показує поточне споживання, memory_get_peak_usage() - максимум за весь час. Розставивши їх між етапами, швидко видно, де стрибок:

$before = memory_get_usage();
$rows = $repository->all();
logger()->info('rows loaded', ['mb' => (memory_get_usage() - $before) / 1024 / 1024]);

Далі - профайлер, який покаже пам'ять у розрізі функцій: Xdebug-профайлер, Blackfire, SPX, Tideways. Вони відповідають на питання «яка функція виділила ці 300 МБ».

Звичайні винуватці:

  • Завантаження всього одразу: Model::all(), file() для великого файлу, json_decode величезної відповіді. Лікується порціями: chunkById(), lazy(), генератори, потоковий парсер.
  • Накопичення в довгому процесі: статичний масив-кеш, логер, що тримає записи, DB::enableQueryLog() у воркері.
  • Циклічні посилання між об'єктами, які звільняє лише збирач сміття, а не лічильник посилань.

У довгоживучих процесах (воркери черг, Octane) навіть невеликий витік на кожному завданні з часом вбиває процес. Тому воркерам ставлять --max-jobs чи --memory, щоб вони періодично перезапускалися.

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

Масив у PHP - це впорядкована хеш-таблиця: універсальна, але не найекономніша. SPL пропонує спеціалізовані структури для конкретних задач:

  • SplFixedArray - масив фіксованої довжини лише з цілими індексами. Займає помітно менше пам'яті за звичайний масив на мільйонах елементів, бо не зберігає хеш-таблицю.
  • SplObjectStorage - відображення «об'єкт → дані». Ключем може бути сам об'єкт, без вигадування рядкових ідентифікаторів. З PHP 8 для цього є й WeakMap, який не тримає об'єкти в пам'яті.
  • SplQueue / SplStack - черга й стек із чіткою семантикою. array_shift() на великому масиві повільний, бо перенумеровує ключі; SplQueue::dequeue() - ні.
  • SplPriorityQueue, SplMinHeap - коли потрібно щоразу брати найменший чи найважливіший елемент, без повного сортування.
$queue = new SplPriorityQueue();
$queue->insert('звичайний лист', 1);
$queue->insert('скидання пароля', 10);
$queue->extract(); // 'скидання пароля'

На практиці: у веб-запиті зі сотнею елементів різниці не буде, і звичайний масив читабельніший. SPL має сенс у CLI-задачах, імпортах і алгоритмах, де елементів сотні тисяч і більше.

Докладніше в документації: Стандартна бібліотека PHP (SPL)

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

Читання потоком:

function rows(string $path): Generator
{
    $file = new SplFileObject($path);
    $file->setFlags(SplFileObject::READ_CSV | SplFileObject::SKIP_EMPTY | SplFileObject::READ_AHEAD);

    $header = null;
    foreach ($file as $row) {
        if ($header === null) {
            $header = $row;
            continue;
        }
        yield array_combine($header, $row);
    }
}

Генератор віддає по одному рядку, тож пам'ять стала незалежно від розміру файлу.

Запис у базу - порціями:

$batch = [];
foreach (rows('import.csv') as $row) {
    $batch[] = ['email' => $row['email'], 'name' => $row['name']];

    if (count($batch) === 1000) {
        DB::table('subscribers')->insert($batch);
        $batch = [];
    }
}
if ($batch !== []) {
    DB::table('subscribers')->insert($batch);
}

Вставка по одному рядку - мільйон запитів; порціями по 1000 - тисяча. Масова вставка через query builder ще й не створює моделей і не запускає подій.

Що ще враховувати:

  • Кодування й роздільник: CSV з Excel часто в Windows-1251 і з ;. Перекодування на льоту - потоковим фільтром (php://filter/read=convert.iconv.windows-1251/utf-8/resource=...).
  • Логування запитів вимкнене: DB::enableQueryLog() у довгому імпорті накопичує всі запити в пам'яті.
  • Помилки в окремих рядках не мають зупиняти весь імпорт - збирати їх у звіт.
  • Перервність: запам'ятовувати номер обробленого рядка, щоб продовжити після збою, а не починати спочатку.
  • Черга: великий імпорт з веб-інтерфейсу - завдання в черзі, а не синхронний запит.

У Laravel те саме зручно робити через LazyCollection::make(fn () => yield from rows(...))->chunk(1000).

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

PHP-FPM (FastCGI Process Manager) - менеджер процесів, що виконують PHP. Веб-сервер (Nginx, Caddy) передає йому запити по FastCGI, а FPM роздає їх пулу воркерів. Один воркер обробляє один запит за раз.

Режими менеджера процесів (pm):

  • static - фіксована кількість воркерів (pm.max_children). Передбачувано, найкраще для виділених серверів.
  • dynamic - кількість змінюється між pm.min_spare_servers і pm.max_children, стартує з pm.start_servers. Типовий вибір.
  • ondemand - воркери створюються лише під запити й завершуються після простою. Економить пам'ять на малонавантажених сайтах, але перший запит повільніший.

Як порахувати pm.max_children: обмеження - пам'ять.

max_children ≈ (пам'ять для PHP) / (середня пам'ять одного воркера)

Сервер з 4 ГБ, з яких 1 ГБ потрібно базі й системі, і воркери по ~60 МБ: (3072 / 60) ≈ 50. Реальне споживання воркера дивляться в ps чи статусній сторінці FPM під навантаженням.

Що буде при помилці:

  • Замало воркерів: запити стають у чергу, час відповіді росте, у лозі FPM - server reached pm.max_children setting.
  • Забагато: під навантаженням пам'ять закінчується, сервер іде в swap чи ядро вбиває процеси - гірше, ніж черга.

Корисні налаштування:

  • pm.max_requests - перезапускати воркер після N запитів, щоб обмежити наслідки витоків пам'яті.
  • request_terminate_timeout - вбивати завислий запит.
  • pm.status_path - сторінка статусу: активні й вільні воркери, довжина черги.
  • slowlog з request_slowlog_timeout - стек-трейси повільних запитів.

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

Докладніше в документації: Налаштування PHP-FPM