如何开发Nginx模块,实现秒级统计监控HTTP状态码的高性能长尾词插件?
- 内容介绍
- 文章标签
- 相关推荐
本文共计846个文字,预计阅读时间需要4分钟。
想在内+Nginx+中实现秒级系统计HTTP+
用 ngx_http_upstream_init_request 和 ngx_http_upstream_finalize_request 拦截状态码最稳
真正能可靠捕获每个请求终态状态码的位置,在 upstream 阶段——无论后端是 FastCGI、proxy_pass 还是 grpc_pass,只要走 ngx_http_upstream_t 流程,就一定会调用 finalize_request。这时 r->upstream->status 已确定,且 r 仍有效,可安全更新共享内存计数器。
- 必须在
ngx_http_upstream_init_request中为每个请求预分配一个 slot(比如用ngx_http_set_ctx存一个指向共享内存 slot 的指针),避免 finalize 时重复查找 - 共享内存区要按秒切片:比如用
ngx_slab_alloc分配一块大小为60 * sizeof(uint64_t) * 3的区域,分别存最近 60 秒的2xx、3xx、4xx+5xx计数(聚合比全量 1xx–5xx 更省空间) - 注意
finalize_request可能被多次调用(如重试场景),需用r->upstream->state->status判定是否为最终状态,而非仅看r->upstream->status
ngx_http_handler 阶段读取统计结果会阻塞 worker?
如果在 ngx_http_handler 里直接读共享内存并拼 JSON 返回,看似简单,但一不小心就掉坑里:共享内存读取本身不阻塞,但若你用 ngx_shmtx_lock 读写同一把锁,或在 handler 中调用 ngx_http_send_header 前未检查 r->header_sent,会导致该 worker 进程卡住,影响其他请求。正确做法是只读、不锁、不 malloc:
- 用
ngx_shmtx_trylock尝试获取锁,失败则返回上一秒的缓存值(状态码统计允许 1 秒误差) - JSON 输出必须用栈上 buffer(如
char out[512]),避免调用ngx_palloc触发内存池分配开销 - 响应头强制设
r->headers_out.content_length_n = len,跳过ngx_http_set_content_length的计算逻辑,减少 CPU 指令路径
如何让统计数据不因 reload 丢失
Nginx reload 时,master 进程会 fork 新 worker,但旧共享内存区不会自动继承。如果你把计数器放在模块私有 shm_zone 里,且没实现 create 和 init 回调,reload 后所有计数归零——这不是 bug,是设计如此。必须确保:
-
shm_zone->init回调中,用ngx_slab_alloc_locked初始化计数数组,并置零;但不要在create阶段初始化,因为那时 slab 尚未 ready - 若需跨 reload 持久化(比如监控告警依赖趋势),得另起一个轻量进程定期 dump 共享内存到本地文件,Nginx 本身不负责持久化
- 注意
ngx_http_upstream_finalize_request在 worker 退出前仍可能被调用,所以销毁逻辑要放在ngx_worker_process_exit钩子之后,否则出现 use-after-free
共享内存里存的是裸数字,没有时间戳字段;时间对齐靠 worker 进程自己用 ngx_time() % 60 算当前秒偏移——这个计算必须快,不能调 strftime 或 localtime_r。
本文共计846个文字,预计阅读时间需要4分钟。
想在内+Nginx+中实现秒级系统计HTTP+
用 ngx_http_upstream_init_request 和 ngx_http_upstream_finalize_request 拦截状态码最稳
真正能可靠捕获每个请求终态状态码的位置,在 upstream 阶段——无论后端是 FastCGI、proxy_pass 还是 grpc_pass,只要走 ngx_http_upstream_t 流程,就一定会调用 finalize_request。这时 r->upstream->status 已确定,且 r 仍有效,可安全更新共享内存计数器。
- 必须在
ngx_http_upstream_init_request中为每个请求预分配一个 slot(比如用ngx_http_set_ctx存一个指向共享内存 slot 的指针),避免 finalize 时重复查找 - 共享内存区要按秒切片:比如用
ngx_slab_alloc分配一块大小为60 * sizeof(uint64_t) * 3的区域,分别存最近 60 秒的2xx、3xx、4xx+5xx计数(聚合比全量 1xx–5xx 更省空间) - 注意
finalize_request可能被多次调用(如重试场景),需用r->upstream->state->status判定是否为最终状态,而非仅看r->upstream->status
ngx_http_handler 阶段读取统计结果会阻塞 worker?
如果在 ngx_http_handler 里直接读共享内存并拼 JSON 返回,看似简单,但一不小心就掉坑里:共享内存读取本身不阻塞,但若你用 ngx_shmtx_lock 读写同一把锁,或在 handler 中调用 ngx_http_send_header 前未检查 r->header_sent,会导致该 worker 进程卡住,影响其他请求。正确做法是只读、不锁、不 malloc:
- 用
ngx_shmtx_trylock尝试获取锁,失败则返回上一秒的缓存值(状态码统计允许 1 秒误差) - JSON 输出必须用栈上 buffer(如
char out[512]),避免调用ngx_palloc触发内存池分配开销 - 响应头强制设
r->headers_out.content_length_n = len,跳过ngx_http_set_content_length的计算逻辑,减少 CPU 指令路径
如何让统计数据不因 reload 丢失
Nginx reload 时,master 进程会 fork 新 worker,但旧共享内存区不会自动继承。如果你把计数器放在模块私有 shm_zone 里,且没实现 create 和 init 回调,reload 后所有计数归零——这不是 bug,是设计如此。必须确保:
-
shm_zone->init回调中,用ngx_slab_alloc_locked初始化计数数组,并置零;但不要在create阶段初始化,因为那时 slab 尚未 ready - 若需跨 reload 持久化(比如监控告警依赖趋势),得另起一个轻量进程定期 dump 共享内存到本地文件,Nginx 本身不负责持久化
- 注意
ngx_http_upstream_finalize_request在 worker 退出前仍可能被调用,所以销毁逻辑要放在ngx_worker_process_exit钩子之后,否则出现 use-after-free
共享内存里存的是裸数字,没有时间戳字段;时间对齐靠 worker 进程自己用 ngx_time() % 60 算当前秒偏移——这个计算必须快,不能调 strftime 或 localtime_r。

