← Back to blog

NGINX: Shared Memory и Concurrency

⚙️ Теория

🧠 Где используется shared memory

Shared memory zone в NGINX нужна, когда данные должны быть общими для всех worker-процессов.

Типичные случаи:

  • limit_req
  • limit_conn
  • ssl_session_cache shared:...
  • upstream zone

Без shared зоны каждый worker видел бы только свою локальную статистику, и глобальные лимиты/состояние работали бы некорректно.


🧱 slab allocator

Внутри shared zone память выдаётся slab-аллокатором:

  • память заранее выделена крупным регионом;
  • делится на блоки фиксированных классов размеров;
  • уменьшает фрагментацию и ускоряет повторные выделения в shared сегменте.

🌳 rbtree

Для быстрого поиска ключей (IP, session id, upstream peer state) часто используется red-black tree:

  • вставка/поиск/удаление примерно O(log N);
  • предсказуемое поведение даже при большом числе записей;
  • обычно сочетается с дополнительными структурами (очереди/ttl/счётчики).

🔒 shared mutex

Так как shared zone трогают разные worker-процессы, доступ синхронизируется lock-ами:

  • в slab pool есть общий mutex (ngx_shmtx_t);
  • критические секции в shared memory оборачиваются ngx_shmtx_lock() / ngx_shmtx_unlock();
  • при высокой конкуренции время ожидания lock растёт и становится заметным bottleneck.

⚛️ atomic operations

Atomic-инструкции в NGINX используются как строительные блоки синхронизации (в том числе в lock-механизмах) и для простых конкурентных обновлений:

  • инкремент/декремент счётчиков;
  • CAS-проверки;
  • барьеры памяти при синхронизации.

Они дешевле тяжёлых lock-ов, но не заменяют их для сложных структур.


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

Где возможен lock contention?

  • горячие ключи в limit_req/limit_conn (много запросов в один и тот же key);
  • частые вставки/удаления в shared rbtree под burst-нагрузкой;
  • интенсивные обновления ssl session cache при коротких TLS-сессиях;
  • upstream zone при большом числе worker и частых изменениях peer state.

Признаки: рост CPU без роста полезного RPS, ухудшение p95/p99, высокая системная активность.

Почему shared zone может стать bottleneck?

Потому что это общий ресурс для всех worker:

  • каждое обращение конкурирует за общий lock-path;
  • размер зоны ограничен, при нехватке памяти растут издержки на вытеснение/ошибки;
  • при "горячих" ключах нагрузка концентрируется в одном участке структуры.

Итог: масштабирование по числу worker перестаёт давать линейный прирост.

Чем per-worker state отличается от shared state?

per-worker state:

  • локально в процессе;
  • без межпроцессных lock;
  • быстрее, но не даёт глобально согласованной картины.

shared state:

  • единое состояние для всех worker;
  • требует синхронизации;
  • немного дороже по CPU, но необходимо для глобальных лимитов и общей статистики.

🧪 Практика

1. Включить limit_req

http {
    limit_req_zone $binary_remote_addr zone=req_zone:10m rate=20r/s;

    server {
        listen 8080;
        server_name _;

        location / {
            limit_req zone=req_zone burst=100 nodelay;
            proxy_pass http://127.0.0.1:18080;
        }
    }
}

Проверка и reload:

nginx -t && sudo nginx -s reload

2. Подать burst-нагрузку

wrk -t4 -c400 -d30s http://127.0.0.1:8080/

Дополнительно можно увеличить пиковую конкуренцию:

wrk -t8 -c800 -d30s http://127.0.0.1:8080/

3. Посмотреть CPU

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

Смотреть вместе:

  • requests/sec и latency в wrk;
  • 503 в ответах и логах (по умолчанию для limit_req; 429 только если задан limit_req_status 429);
  • загрузку CPU по worker.

4. Изменить размер zone

Сравнить, например, так:

limit_req_zone $binary_remote_addr zone=req_zone:1m rate=20r/s;   # маленькая зона
# vs
limit_req_zone $binary_remote_addr zone=req_zone:50m rate=20r/s;  # большая зона

После каждого изменения:

nginx -t && sudo nginx -s reload

Сравнивать:

  • стабильность RPS под burst;
  • ошибки лимитера;
  • CPU при одинаковом профиле нагрузки.

🧾 Вывод

Shared memory в NGINX даёт глобально согласованное состояние между worker, но за это платят синхронизацией.
При burst-нагрузке узким местом становятся горячие ключи, lock contention и недостаточный размер zone.
Практический баланс: держать только действительно общий state в shared зонах, а размер zone и лимиты подбирать по замерам CPU/latency под реальным трафиком.


📚 Ссылки


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

NGINX: Shared Memory и Concurrency | Aleksandr Suprun