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, щоб вони періодично перезапускалися.
Масив у 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-задачах, імпортах і алгоритмах, де елементів сотні тисяч і більше.
Головне правило - ніколи не тримати в пам'яті весь файл і не накопичувати результати. Читати рядок за рядком і одразу обробляти.
Читання потоком:
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).
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): фреймворк завантажується один раз, але з'являються проблеми стану між запитами.