---
title: "Воркери черг Laravel тримають у пам'яті 8 копій застосунку"
url: https://laravelukraine.com/blog/vorkeri-cerg-laravel-trimaiut-u-pamiati-8-kopii-zastosunku
date: 2026-11-02
source: https://dev.to/tevkert/your-laravel-queue-workers-keep-8-copies-of-your-app-in-memory-g4d
---

# Воркери черг Laravel тримають у пам'яті 8 копій застосунку

Це переказ статті Ando Hakobyan [Your Laravel queue workers keep 8 copies of your app in memory](https://dev.to/tevkert/your-laravel-queue-workers-keep-8-copies-of-your-app-in-memory-g4d) - зі збереженням його структури, цифр і коду.

Автор додавав воркери Horizon на 2 ГБ інстанс Laravel Cloud, доки пам'ять не заповнилася приблизно на 90%. Вийшло 27 воркерів. Потім він змусив воркери ділити між собою скомпільований код і повторив тест: той самий сервер, той самий застосунок, ті самі 600 завдань на хвилину. Цього разу сервер витримав 210 воркерів.

Це був тест пам'яті, а не швидкості: за такого темпу завдань процесор ніколи не ставав вузьким місцем, а пам'ять - ставала. Щоб запустити ті самі 210 воркерів у стандартному режимі, знадобився інстанс на 16 ГБ.

Усе почалося з того, що кілька серверів автора сиділи у swap, і чималу частину пам'яті на них займав саме Horizon.

### Кожен воркер завантажує весь застосунок

Кожен `queue:work` - це окремий PHP-процес. Так само окремим процесом є кожен воркер Horizon і, що здивувало автора, кожен супервізор Horizon. Кожен із них завантажує Laravel і компілює код, якого торкається, у власну приватну пам'ять.

У прикладі конфігурації Supervisor з документації Laravel стоїть `numprocs=8`. Якщо ви його скопіювали, у вас вісім процесів, і кожен тримає власну копію того самого скомпільованого застосунку.

У бенчмарку автора в Docker стандартний воркер Horizon займав близько 63 МБ. Найцікавіше - супервізори. Супервізор ніколи не виконує завдань, він лише стежить за своїми воркерами, але все одно займав близько 56 МБ. Шість супервізорів - це приблизно 340 МБ ще до того, як виконалося перше завдання.

### PHP-FPM розв'язав це багато років тому

FPM запускає один master-процес, налаштовує в ньому спільну пам'ять opcache і вже потім форкає воркери. Усі воркери FPM бачать ту саму спільну пам'ять, тож код, скомпільований одним воркером, повторно використовують усі інші.

Master-процес ніколи не завантажує застосунок - він лише володіє кешем. Як з'ясувалося, у цьому й полягає весь фокус.

Воркери черг нічого з цього не мають. Supervisord чи systemd запускає кожен `php artisan queue:work` як окремий, зовсім новий процес, і спільного батька, який володів би кешем, немає. [Opcache у CLI за замовчуванням вимкнений](https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.enable-cli). Якщо ввімкнути його через `opcache.enable_cli=1`, кожен процес отримає власний приватний кеш - і це все одно одна копія на воркер.

А `opcache.file_cache`, прогрітий у CI, економить час компіляції, але не пам'ять: кожен воркер однаково завантажує собі власну копію.

### Перевірте свої сервери

Перш ніж вірити цифрам автора, подивіться на свої. Рахуйте PSS, а не RSS. RSS враховує спільну пам'ять повністю для кожного процесу, тож сума завищена. PSS ділить спільні сторінки між процесами, які їх використовують, і сума виходить чесною.

Запустіть скрипт від імені користувача, під яким працюють воркери (або через `sudo`):

```bash
for pid in $(pgrep -f '^[^ ]*php[^ ]* .*artisan (queue:work|horizon(:supervisor|:work)?)( |$)'); do
  awk '/^Pss:/ {print $2}' "/proc/$pid/smaps_rollup" 2>/dev/null
done | awk '
  { total += $1; count++ }
  END {
    if (count) printf "%d processes, %.0f MB total\n", count, total / 1024
    else print "No matching processes found"
  }'
```

Скрипт рахує також master і супервізори Horizon, бо пам'ять вони коштують так само. Якщо на одному сервері кілька застосунків, він складе їх усіх, тож за потреби звузьте шаблон.

Запишіть результат - це ваше «до». Яка його частина припадає на код, а яка на дані самих завдань, залежить від застосунку, і показати це може лише «після».

### Чому не Go

Перенесення воркерів на Go розв'язало б проблему з пам'яттю - [так зробили, наприклад, у Sentinel](https://laravelukraine.com/blog/go-vimiriuje-laravel-virisuje-iak-sentinel-zaminiv-regionalni-vorkeri-horizon-na-go-probi). Але в застосунках автора завдання і є застосунком: вони використовують ті самі моделі, сервіси й хелпери, що й усе інше. В одному застосунку 17 класів завдань тягнуть за собою приблизно половину кодової бази - близько 167 файлів із 400. Переписати це означає переписати пів застосунку, а потім тримати дві версії синхронними. Composer-пакет виявився дешевшим рішенням.

### Спершу fork, потім завантаження

Очевидне рішення на PHP - завантажити застосунок один раз і форкати воркери з нього. Проблема в тому, що діти успадковують. Кожен отримує відкриті сокети батька до бази даних і Redis, тож два воркери починають говорити через одне з'єднання. Кожен також стартує з тим станом, який залишило завантаження, а будь-який випадковий стан, ініціалізований до fork, може повторитися в кількох дітей.

Тому [queue-prefork](https://github.com/tevkert/queue-prefork) робить те саме, що master FPM: форкає ще до того, як з'являється застосунок. Маленький процес-шаблон стартує з opcache і без жодного коду застосунку. Кожен воркер форкається з нього і вже потім сам завантажує застосунок через звичайну точку входу `artisan`, як будь-який стандартний воркер. Перший воркер компілює код у спільний opcache шаблону, а всі наступні знаходять його вже готовим.

Кожен воркер, як і раніше, відкриває власні з'єднання і будує власний контейнер і конфігурацію. Спільний у них лише скомпільований код.

![Стандартний Horizon поруч із Horizon з queue-prefork: кожен стандартний процес тримає власну скомпільовану копію застосунку (разом 1279 МБ), а форкнуті процеси ділять одну копію (240 МБ)](https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/1ynvujja852lo7jgj8fy.gif)

Для звичайного `queue:work` процес, який запустив supervisord, залишається крихітною заглушкою `/bin/sh` з тим самим PID. Менеджер процесів і далі ним керує, а `queue:restart` працює як завжди. З Horizon master, супервізори й воркери стають легкими форками. Подробиці - у [README](https://github.com/tevkert/queue-prefork#how-it-works).

### Цифри

Автор запустив поруч два однакові Docker-стеки на тому самому потоці завдань: Laravel 13.38 з приблизно 45 продакшн-пакетами, PHP 8.4 і Horizon 5.50.1 на віртуальній машині з 2 CPU. Пам'ять - медіана сумарного PSS із вимірюванням щохвилини. Цифри prefork включають його власні шаблони, заглушки й fork-сервери.

| Конфігурація | Стандартно | Prefork |
|---|---:|---:|
| 8 × `queue:work`, типові завдання | 645 МБ | 192 МБ (-70%) |
| 8 × `queue:work`, простій | 455 МБ | 102 МБ (-78%) |
| 8 × `queue:work`, важкі завдання | 1025 МБ | 524 МБ (-49%) |
| Horizon, 6 супервізорів з автомасштабуванням | 1279 МБ | 240 МБ (-81%) |

Horizon автомасштабувався в обох прогонах: у медіані до 14 воркерів у стандартному режимі і до 17 з prefork. Тобто prefork тримав більше воркерів у меншому обсязі пам'яті. Жодне завдання не впало, пропускна здатність була однаковою.

Навантаження на процесор теж знизилося - з 21,3% до 11,9% одного ядра для Horizon з типовими завданнями, бо новий воркер не перекомпільовує те, що вже є в кеші, а лише завантажує Laravel.

Важкі завдання економлять менше, бо їхні власні дані однаково приватні. Стандартний варіант працював з вимкненим opcache у CLI, як його постачає `php.ini-production`. На Laravel Cloud він увімкнений, тож там кожен стандартний воркер тримає ще й власний приватний кеш. [Стенд для бенчмарку](https://github.com/tevkert/queue-prefork/blob/main/docker/README.md) лежить у репозиторії, якщо захочете повторити.

Це тестове середовище. У реальному продакшені одна з конфігурацій Horizon автора зменшилася приблизно з 1,2 ГБ до приблизно 280 МБ.

Повернімося до сервера Laravel Cloud з початку статті. Там стандартний воркер коштував близько 56 МБ, а форкнутий - близько 7,4 МБ. Так 27 воркерів перетворилися на 210 на інстансі Flex 2 ГБ. Ті самі 210 воркерів у стандартному режимі зайняли 11 766 МБ, а найменший інстанс, куди вони вміщаються, - Pro 16 ГБ за $128 на місяць замість $24 (зате з 4 vCPU замість одного, хоча процесора за такого темпу завдань і так вистачало).

### Чотири хвилини, які автор списав на свій код

На Cloud усі 210 воркерів з'являлися близько чотирьох хвилин, і автор був певен, що повільний його fork-сервер. Але стандартний Horizon поводиться так само - це стартовий розгін самого Horizon.

Супервізор стартує з `floor((minProcesses + maxProcesses) / 2)` воркерів. Далі він додає `balanceMaxShift` воркерів (за замовчуванням 1) кожні `balanceCooldown` секунд (за замовчуванням 3). А з `balance => 'simple'` параметр `'processes' => N` задає лише максимум - мінімум лишається 1.

Тож супервізор на 150 воркерів стартує зі 75 і ще майже чотири хвилини добирає решту. Це поведінка Horizon 5.50.1, інші версії автор не перевіряв. Щоб отримати весь пул одразу:

```php
'balance' => 'simple',
'processes' => 150,
'minProcesses' => 150,
```

З балансуванням `auto` натомість збільште `balanceMaxShift`.

Це працює і зі стандартними воркерами, але тоді кожен воркер компілює застосунок одночасно з іншими, а це чимале разове навантаження на процесор. З форкнутими воркерами це дешево. У тесті на 2 CPU 100 воркерів були готові приблизно через 1,7 секунди після старту супервізора проти 8-14 секунд у стандартному режимі. На великій багатоядерній машині, де стандартний варіант мав би показати себе краще, автор цього не перевіряв.

### Як спробувати

Пакет розрахований на Linux-сервери з Laravel 11-13, PHP 8.2+ і, якщо ви його використовуєте, Horizon 5.23+. Він не допоможе, коли в кожному контейнері чи поді працює один воркер (зокрема керовані черги Laravel Cloud і Vapor): ділитися нема з ким. Воркер, що працює як PID 1 у Docker, лишається стандартним, тож дайте контейнеру init (`init: true`).

```bash
composer require tevkert/queue-prefork
php artisan queue-prefork:doctor    # на сервері воркерів: як працюватимуть ваші воркери і чому
php artisan queue-prefork:status    # пам'ять зараз, ваш стандартний стан «до»
```

Пакет вмикається одразу після встановлення для воркерів, запущених після цього. Розгорніть код, перезапустіть воркери через `queue:restart` або `horizon:terminate`, дайте їм прогрітися і запустіть `status` ще раз - це ваше «після».

Якщо якась перевірка безпеки не пройде, наприклад, розширення відкрило сокети чи потоки під час старту, воркери просто працюватимуть у стандартному режимі, а пакет запише в лог причину. Щоб вимкнути пакет, встановіть `QUEUE_PREFORK=false`, перекешуйте конфігурацію і перезапустіть воркери.

На Laravel Cloud запускайте Horizon як власний фоновий процес командою `php -d grpc.enable_fork_support=1 artisan horizon`.

Пакет поширюється під ліцензією MIT і доступний на [GitHub](https://github.com/tevkert/queue-prefork).

### Підсумок

Автор просить читачів поділитися своїми цифрами: скільки воркерів працює на сервері, що показує скрипт і які завдання вони виконують, - бо поки що він міряв лише власні застосунки.

Від нас: навіть якщо пакет вам не потрібен, скрипт підрахунку PSS і налаштування `minProcesses` для Horizon варто взяти собі вже зараз. А сам queue-prefork з'явився лише кілька днів тому (v1.0.x), тож спершу виміряйте своє «до», перевірте пакет на staging і лише потім вмикайте на продакшені.
