Теория
Где используется shared memory
Shared memory zone нужна, когда данные должны быть общими для всех worker.
Типичные случаи:
limit_reqlimit_connssl_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 при одинаковом профиле.