不能直接用log_format做秒级状态码统计,因其仅支持日志写入、无内存聚合能力,且日志落盘存在缓冲与时间偏差;必须在LOG_PHASE阶段用共享内存滑动窗口实时计数。

为什么不能直接用 log_format 做秒级状态码统计
因为 log_format 仅支持写入日志,不提供内存内聚合能力;Nginx 日志落盘有缓冲、异步、轮转机制,无法保证时间精度,更无法支撑实时查询。想做到「秒级」且「高性能」,必须绕过日志系统,在内存中维护滑动时间窗口的计数器。
必须在 ngx_http_phase_handler_t 阶段介入,而非 access_handler
状态码(r->headers_out.status)在 content_handler 执行后才最终确定,access_handler 阶段读到的仍是初始值(通常是 0 或 200)。真正可靠的时机是 ngx_http_log_request 调用前的 LOG_PHASE —— 此时响应头已封包,状态码稳定可读。
- 开发模块需注册
ngx_http_log_handler_pt类型的 handler,挂载到ngx_http_core_main_conf_t->phases[NGX_HTTP_LOG_PHASE].handlers - 避免在
rewrite或access阶段读取status,否则 99% 的 4xx/5xx 会被统计为 200 - 若需关联 upstream 状态(如
upstream_status),要额外解析r->upstream,但该字段仅在启用 proxy_pass 后非空
ngx_slab_alloc 分配共享内存时,结构体对齐和 key 哈希冲突是最大坑
秒级统计要求每个时间戳(如 1717023480)对应一个独立 bucket,但 Nginx 共享内存 slab 不支持动态扩容,必须预估峰值 key 数量。常见错误是把 time_t 直接当 key 用,导致哈希碰撞率飙升(32 位 time_t 在 slab 中易被截断)。
- 推荐用
(t / 1) & 0xFFFFF截取最近 1M 秒(约 11.5 天)作为哈希 key,配合链表解决冲突 - 每个 bucket 内存布局:4 字节时间戳 + 10 字节 status 计数数组(支持 1xx–5xx 共 50 类,实际只存 1xx/2xx/3xx/4xx/5xx 各 1 字节)
- 务必在
shm_zone->init回调中初始化所有 bucket 的计数器为 0,否则未初始化内存会触发随机计数
暴露 HTTP 接口查数据时,ngx_http_upstream_t 不是必须,但 ngx_http_finalize_request 必须调用
用 location 配置一个内部接口(如 location = /_status/stats)返回 JSON,看似简单,但若直接 write 到 r->connection->buffer,会因 buffer 满或未设置 header 导致连接 hang 住。
- 必须调用
ngx_http_send_header(r)发送Status和Content-Type - 响应体用
ngx_chain_t构造,避免大字符串拼接;超过 4KB 建议分 chunk 输出 - 严禁在 handler 中调用
sleep或阻塞 IO;所有统计数据必须从共享内存原子读取(ngx_atomic_fetch_add用于写,普通读即可) - 如果启用了
gzip on,需在ngx_http_set_content_type后显式禁用压缩:r->gzip_tested = 1; r->gzip_ok = 0;
共享内存里的时间窗口滚动逻辑没有自动清理机制,得靠外部定时任务或模块内每秒检查并重置过期 bucket —— 这个细节没人提,但漏掉就会让内存缓慢泄漏。



















