← Back to notes

NGINX: Shared Memory
и Concurrency


Теория

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

Shared memory zone нужна, когда данные должны быть общими для всех 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);
  • критические секции оборачиваются ngx_shmtx_lock() / ngx_shmtx_unlock();
  • при высокой конкуренции время ожидания lock растёт и становится bottleneck.

atomic operations

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

  • инкремент/декремент счётчиков;
  • 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 vs 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;
        }
    }
}
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 при одинаковом профиле.


Ссылки

NGINX: Shared Memory и Concurrency | Aleksandr Suprun