← Back to blog

NGINX: Memory Model

⚙️ Теория

🧠 Ключевой принцип

Память живет столько же, сколько request.

Большая часть временных структур выделяется из пула запроса и освобождается одним действием в конце его жизненного цикла.


🧩 request memory pool

Для каждого HTTP request NGINX создаёт memory pool (r->pool):

  • мелкие объекты (структуры, строки, контексты модулей) берутся из этого пула;
  • при завершении request пул уничтожается целиком;
  • нет необходимости делать free() для каждого маленького объекта отдельно.

Если данные должны жить дольше request, их нельзя хранить в r->pool — для более долгого срока жизни используют другие контексты (например pool соединения).


🧱 slab allocator

slab используется для долгоживущей shared memory между worker-процессами:

  • limit_req_zone;
  • limit_conn_zone;
  • keys_zone у proxy_cache;
  • upstream zone.

Это другой класс памяти: не на один request, а на общие структуры и статистику.


📦 ngx_buf_t

ngx_buf_t описывает кусок данных:

  • где лежат данные (RAM или файл);
  • диапазон байт (pos/last, file_pos/file_last);
  • можно ли буфер переиспользовать/отправлять/сохранять.

Важно: ngx_buf_t обычно метаданные, а не “владелец всей памяти”.


🔗 chain buffers

NGINX передаёт тело ответа как цепочки буферов (ngx_chain_t):

  • модуль добавляет куски в chain;
  • следующий фильтр читает chain и передаёт дальше;
  • это удобно для стриминга и больших ответов без копирования всего тела в один большой блок.

🧹 cleanup handlers

cleanup handler регистрируется в request pool и вызывается при завершении request:

  • закрыть временный файл;
  • освободить внешний ресурс;
  • отменить побочный state.

Это safety-механизм: cleanup вызывается при уничтожении pool, в том числе в аварийных путях завершения request.


Что нужно уметь объяснить

Почему нет free() на каждый объект?

Потому что объекты запроса живут примерно одинаково долго.
Вместо тысяч malloc/free NGINX делает много быстрых выделений из pool и потом один общий destroy pool в конце request.

Что произойдет, если сохранить указатель вне request lifetime?

После завершения request память pool уже недействительна.
Такой указатель становится “висячим” (dangling pointer): чтение/запись через него может привести к крашу, повреждению данных или редким плавающим багам.

Почему это быстрее malloc/free?

  • меньше вызовов аллокатора общего назначения;
  • меньше фрагментации памяти;
  • лучше locality (данные request лежат ближе друг к другу);
  • дешевле очистка: один pool destroy вместо множества free.

🧪 Практика

1. Включить большой client_body

http {
    client_max_body_size 100m;
    client_body_buffer_size 128k;
    client_body_temp_path /var/lib/nginx/body 1 2;
}

И перезагрузить:

nginx -t && sudo nginx -s reload

2. Посмотреть временные файлы

Когда тело запроса не помещается в RAM-буфер, NGINX пишет его во временные файлы.

sudo ls -lah /var/lib/nginx/body
sudo find /var/lib/nginx/body -type f | head

3. Изменить proxy_buffering on/off

location /api/ {
    proxy_pass http://127.0.0.1:18080;
    proxy_buffering on;   # потом сравнить с off
}

Сравнить два режима:

  • on: больше буферизации в NGINX, меньше pressure на медленных клиентов;
  • off: ближе к стримингу, меньше внутренней буферизации, но выше зависимость от скорости клиента.

4. Наблюдать память через top

top -H -p $(pgrep -d',' -f 'nginx: worker process')

Под нагрузкой сравнивать:

  • RES/MEM% у worker;
  • изменения при proxy_buffering on/off;
  • рост временных файлов при больших request body.

🧾 Вывод

Модель памяти NGINX строится вокруг жизненного цикла request: быстрые выделения из pool, cleanup в конце и минимум лишней ручной деаллокации.
slab решает отдельную задачу общей памяти между worker, а ngx_buf_t + chain buffers обеспечивают эффективную передачу данных.
На практике поведение памяти лучше всего видно при больших body и переключении proxy_buffering, с параллельным наблюдением top и temp-файлов.


📚 Ссылки


Проверка источников

NGINX: Memory Model | Aleksandr Suprun