直接 hook ngx_http_log_handler 最稳,因其在请求末尾执行、状态码已确定且不可变;需用共享内存+slab分配+原子操作存取数据,并通过无锁环形缓冲区导出秒级统计,告警应由独立进程解耦消费。

为什么直接 hook ngx_http_log_handler 是最稳的切入点
因为 Nginx 日志阶段(log phase)天然具备两个关键属性:所有请求必经、状态码已确定。比在 content_handler 或 filter 阶段统计更可靠——前者可能被子请求绕过,后者可能因缓冲或截断丢失最终状态。你不需要拦截或修改响应,只读取 r->headers_out.status 就够了。
常见错误是试图在 ngx_http_header_filter 里统计:此时状态码虽已设置,但若后续发生 error_page 跳转或子请求重写,r->headers_out.status 可能被覆盖,导致重复计数或错计。而 log handler 在请求生命周期末尾执行,数据确定且不可变。
- 必须在模块的
ngx_http_module_t中将postconfiguration回调注册为 log handler,而非用ngx_http_top_header_filter - 注意多 worker 场景:每个 worker 进程维护独立内存,需用共享内存(
ngx_shm_zone_t)存储计数器,否则各 worker 数据无法聚合 - 避免在 log handler 中做耗时操作(如写文件、发网络请求),否则会阻塞整个 worker 的事件循环
如何用 ngx_slab_alloc 在共享内存中安全初始化状态码桶
共享内存区域(shm)由 master 进程创建,所有 worker 进程映射同一块物理内存。但 slab 分配器不是线程安全的,Nginx 用 ngx_shmtx_lock 保证原子性。你不能直接用 malloc 或全局数组,否则重启后数据丢失,多 worker 下还会出现竞态。
典型结构是定义一个含 1000 个 uint64_t 元素的数组(覆盖 1xx–5xx 所有标准及常用自定义码),索引即状态码值(如 200 → index 200)。初始化时需检查 shm zone 是否已存在,防止重复分配:
if (shpool == NULL) {
shpool = ngx_shared_memory_add(cf, &name, size, &ngx_http_status_stat_module);
if (shpool == NULL) {
return NGX_ERROR;
}
}
shpool->init = ngx_http_status_stat_init_zone;
-
ngx_http_status_stat_init_zone回调里用ngx_slab_alloc分配数组内存,并用ngx_memzero清零 - 不要用
sizeof(uint64_t) * 1000硬编码大小,应通过shpool->shm.size校验是否足够,防止因配置过小导致分配失败 - 每个状态码桶用
ngx_atomic_fetch_add增量更新,而不是普通加法——这是跨 CPU cache line 同步的关键
如何从共享内存实时导出秒级数据而不卡住 worker
HTTP 接口暴露统计结果时,绝不能遍历整个共享内存并拼接 JSON 返回——一次请求就锁住 shm mutex 几十毫秒,QPS 上千时必然雪崩。正确做法是:worker 定期(如每秒)将本地副本写入 ring buffer,主接口只读 ring buffer 最新 slot,完全不碰 shm 锁。
ring buffer 结构体放在共享内存外(如 module context 的 static 变量),每个 slot 存储上一秒的快照(uint64_t counts[1000]),用原子指针切换读写 slot。这样 HTTP handler 调用 ngx_http_status_stat_handler 时,全程无锁、无内存分配,延迟稳定在微秒级。
- 定时任务用
ngx_add_timer注册到 event loop,触发函数内完成快照拷贝 + slot 切换 - HTTP handler 返回前必须调用
ngx_http_send_header和ngx_http_output_filter,否则连接不会关闭,curl 会 hang 住 - 返回 JSON 时手动构造字符串(不用 cJSON 等第三方库),避免动态内存申请;用
ngx_snprintf写入预分配的r->pool缓冲区
告警逻辑必须脱离 Nginx 主循环,用独立进程消费 ring buffer
在 Nginx 进程内做阈值判断(比如“500 码超 100 次/秒”)看似简单,但会污染 worker 性能模型:每次定时检查都要读 ring buffer、做条件跳转、可能触发 syscalls(如 sendto),严重时拖慢整个请求处理链路。真正的高可用告警必须解耦。
推荐方案是用一个轻量级 Python 或 Go 进程,通过 mmap 映射同一块 ring buffer 文件(Nginx shm 实际是 /dev/shm 下的文件),轮询最新 slot。它不依赖 Nginx 生命周期,可单独启停、升级、限流,还能对接 Prometheus、钉钉、企业微信等外部系统。
- Nginx 模块需在初始化时把 ring buffer 文件路径(如
/dev/shm/nginx-status-ring)写入固定位置(如/var/run/nginx-status-ring.path),供外部进程读取 - 外部进程必须用
MAP_SHARED | MAP_POPULATE标志 mmap,确保页表预热,避免首次访问缺页中断 - 轮询间隔设为 200ms 而非 1s:既能捕捉突增毛刺,又不会因频繁 mmap/fstat 带来额外开销
共享内存的生命周期管理最容易被忽略——master 进程退出时 shm 不自动销毁,残留文件会占满 /dev/shm;而 ring buffer 的 slot 切换若没做 ABA 保护,极端情况下可能读到脏数据。这些细节不处理,监控系统上线三天就会开始误报或漏报。











