Nginx worker 进程频繁崩溃,是运维和开发人员面临的棘手问题之一,尤其当崩溃由内存踩踏(Memory Corruption)引发时,问题根源往往被层层掩盖,难以追溯。本文将详细复盘一次线上 Nginx 内存被踩导致崩溃的真实案例,展示如何利用 OpenResty XRay 抽丝剥茧,精准定位到因二次开发违背 Nginx 连接池管理机制而导致的深层次 Bug,为解决同类问题提供一套行之有效的诊断思路。

一个让运维团队彻夜难眠的 Nginx 崩溃案例

客户的线上业务发生 Nginx worker 进程频繁崩溃,严重影响了服务的稳定性。这类内存错误导致的崩溃问题,通常难以定位。因为从表面的崩溃堆栈来看,出错的位置往往不是问题的根源,真正的“第一现场”早已被破坏。传统的日志分析或代码排查方法,在这种场景下无能为力。为了快速、精准地定位问题,我们利用 OpenResty XRay 的现场录制功能对线上 Nginx 工作进程进行了在线录制。通过回放录制文件分析崩溃过程,最终帮助我们还原了问题的全貌。

如何用 OpenResty XRay 精准捕获内存踩踏的“第一现场”

OpenResty XRay 专家通过分析录制文件,为客户定位了问题根原,下文将展示其核心分析过程。

初步分析:定位崩溃点

我们首先让程序运行,直到复现崩溃,然后使用 GDB 查看崩溃时的堆栈(Backtrace)。

> c
> bt
#0  0x00000000004ccd1a in ngx_http_run_posted_requests (c=0x7f5fc3a010f8) at src/http/ngx_http_request.c:3152
#1  0x00000000004d047b in ngx_http_process_request_headers (rev=rev@entry=0x7f5fbfc010e0) at src/http/ngx_http_request.c:2021
#2  0x00000000004d10fd in ngx_http_process_request_line (rev=0x7f5fbfc010e0) at src/http/ngx_http_request.c:1473
#3  0x00000000004b2d89 in ngx_epoll_process_events (cycle=0x7f60a3888b90, timer=<optimized out>, flags=<optimized out>)
...
#9  0x0000000000482c0e in main (argc=<optimized out>, argv=<optimized out>) at src/core/nginx.c:436

崩溃发生在 ngx_http_run_posted_requests 函数的 3152 行。对应的代码是 if (pr == NULL),而 pr 的值来自于前一行的 pr = r->main->posted_requests;。这说明,在访问 r->main 时发生了段错误。

  3114  void
  3115  ngx_http_run_posted_requests(ngx_connection_t *c)
  3116  {
  3117      ngx_http_request_t         *r;
  3118      ngx_http_posted_request_t  *pr;
  3119      for ( ;; ) {
  3120          if (c->destroyed) {
  3121              return;
  3122          }
  3123          r = c->data;
  3124          pr = r->main->posted_requests;
  3125          if (pr == NULL) {
  3126              return;
  3127          }
  3128          r->main->posted_requests = pr->next;
  3129          r = pr->request;
  3130          ngx_http_set_log_request(c->log, r);
  3131          ngx_log_debug2(NGX_LOG_DEBUG_HTTP, c->log, 0,
  3132                         "http posted request: \"%V?%V\"", &r->uri, &r->args);
  3133          r->write_event_handler(r);
  3134      }
  3135  }

我们尝试打印 r->main,发现它是一个无效的地址:

end 1,471,308> print r->main
$14 = (ngx_http_request_t *) 0xffffffff2e616e69
end 1,471,308> print r->main->posted_requests
Cannot access memory at address 0xffffffff2e6173b1

这证实了 r 指针或其成员 main 已经被污染。代码中 r 的值来自于 r = c->data,其中 cngx_connection_t 类型的连接对象。我们怀疑 cc->data 在某个环节被错误地修改了。

深入追踪:使用硬件观察点

为了找出谁在何时修改了 c->data,我们对该内存地址设置了硬件观察点(Hardware Watchpoint)。

end 1,471,308> print &c->data
$8 = (void **) 0x7f5fc3a010f8
end 1,471,308> watch *(void **)0x7f5fc3a010f8
Hardware watchpoint 7: *(void **)0x7f5fc3a010f8

设置完硬件观察点之后,我们执行 rc (reverse-continue) 命令让进程逆向执行,以查看 c->data 的变化过程。 在每次断点触发的时候执行 bt 查看 c->data 的修改调用栈。

end 1,471,308> rc

继续运行程序,观察点很快触发了多次,揭示了关键的操作序列:

  1. 第一次触发:c->data 被修改。ngx_http_lua_socket_resolve_retval_handler 函数中,c->data 被赋予了一个新值。因为这个值是 ngx_http_lua_socket_tcp_upstream_t 对象而不是 ngx_http_request_t 对象,因此把 c->data 当成是 ngx_http_request_t 导致访问错误的内存地址。因此,我们继续执行 rc 命令查看这个 c->data 的变化过程。

    99% 1,471,080> bt
    #0  ngx_http_lua_socket_resolve_retval_handler (r=0x7f60a3838050, u=0x40c1ab88, L=0x40c1ab00)
        at addons/lua-nginx-module/src/ngx_http_lua_socket_tcp.c:1176
    #1  0x000000000058b3b9 in ngx_http_lua_socket_tcp_connect (L=0x40c1ab00) at addons/lua-nginx-module/src/ngx_http_lua_socket_tcp.c:744
    #2  0x0000000000647817 in lj_BC_FUNCC ()
    #3  0x0000000000573c95 in ngx_http_lua_run_thread (L=L@entry=0x41afc378, r=r@entry=0x7f60a3838050, ctx=ctx@entry=0x7f60a3917e68, 
        nrets=nrets@entry=0) at addons/lua-nginx-module/src/ngx_http_lua_util.c:1073
    #4  0x0000000000578acc in ngx_http_lua_rewrite_by_chunk (L=0x41afc378, r=0x7f60a3838050)
        at addons/lua-nginx-module/src/ngx_http_lua_rewriteby.c:338
    #5  0x0000000000578ddd in ngx_http_lua_rewrite_handler (r=0x7f60a3838050) at addons/lua-nginx-module/src/ngx_http_lua_rewriteby.c:167
    #6  0x00000000004c7763 in ngx_http_core_rewrite_phase (r=0x7f60a3838050, ph=0x7f60a2f50768) at src/http/ngx_http_core_module.c:1123
    #7  0x00000000004c2305 in ngx_http_core_run_phases (r=r@entry=0x7f60a3838050) at src/http/ngx_http_core_module.c:1069
    #8  0x00000000004c23ea in ngx_http_handler (r=r@entry=0x7f60a3838050) at src/http/ngx_http_core_module.c:1052
    #9  0x00000000004cfca1 in ngx_http_process_request (r=r@entry=0x7f60a3841050) at src/http/ngx_http_request.c:2780
    #10 0x00000000004d06f7 in ngx_http_process_request_headers (rev=rev@entry=0x7f5fbfc010e0) at src/http/ngx_http_request.c:1995
    #11 0x00000000004d10fd in ngx_http_process_request_line (rev=0x7f5fbfc010e0) at src/http/ngx_http_request.c:1473
    #12 0x00000000004b2d89 in ngx_epoll_process_events (cycle=0x7f60a3888b90, timer=<optimized out>, flags=<optimized out>)
    
  2. 第二次触发:内存被重用。 程序在 ngx_event_connect_peer -> ngx_get_connection 这个调用链路中再次触发了观察点。ngx_get_connection 从 Nginx 的连接池中获取了一个新的连接对象,在初始化新连接时,ngx_memzero 将这块内存(包括 destroyed 标志位)清零。继续执行 rc 命令观察 c->data 的变化过程。

    #0  0x0000000000494e1c in memset (__len=360, __ch=0, __dest=0x7f5fc3a010f8) at /usr/include/bits/string3.h:84
    #1  ngx_get_connection (s=s@entry=14, log=0x7f60a31c12c0) at src/core/ngx_connection.c:1190
    #2  0x00000000004a8e26 in ngx_event_connect_peer (pc=pc@entry=0x40c1abc8) at src/event/ngx_event_connect.c:53
    #3  0x0000000000587cb3 in ngx_http_lua_socket_resolve_retval_handler (r=0x7f60a3838050, u=0x40c1ab88, L=0x40c1ab00)
        at addons/lua-nginx-module/src/ngx_http_lua_socket_tcp.c:1131
    #4  0x000000000058b3b9 in ngx_http_lua_socket_tcp_connect (L=0x40c1ab00) at addons/lua-nginx-module/src/ngx_http_lua_socket_tcp.c:744
    #5  0x0000000000647817 in lj_BC_FUNCC ()
    #6  0x0000000000573c95 in ngx_http_lua_run_thread (L=L@entry=0x41afc378, r=r@entry=0x7f60a3838050, ctx=ctx@entry=0x7f60a3917e68, 
        nrets=nrets@entry=0) at addons/lua-nginx-module/src/ngx_http_lua_util.c:1073
    #7  0x0000000000578acc in ngx_http_lua_rewrite_by_chunk (L=0x41afc378, r=0x7f60a3838050)
        at addons/lua-nginx-module/src/ngx_http_lua_rewriteby.c:338
    #8  0x0000000000578ddd in ngx_http_lua_rewrite_handler (r=0x7f60a3838050) at addons/lua-nginx-module/src/ngx_http_lua_rewriteby.c:167
    #9  0x00000000004c7763 in ngx_http_core_rewrite_phase (r=0x7f60a3838050, ph=0x7f60a2f50768) at src/http/ngx_http_core_module.c:1123
    #10 0x00000000004c2305 in ngx_http_core_run_phases (r=r@entry=0x7f60a3838050) at src/http/ngx_http_core_module.c:1069
    #11 0x00000000004c23ea in ngx_http_handler (r=r@entry=0x7f60a3838050) at src/http/ngx_http_core_module.c:1052
    #12 0x00000000004cfca1 in ngx_http_process_request (r=r@entry=0x7f60a3841050) at src/http/ngx_http_request.c:2780
    #13 0x00000000004d06f7 in ngx_http_process_request_headers (rev=rev@entry=0x7f5fbfc010e0) at src/http/ngx_http_request.c:1995
    #14 0x00000000004d10fd in ngx_http_process_request_line (rev=0x7f5fbfc010e0) at src/http/ngx_http_request.c:1473
    #15 0x00000000004b2d89 in ngx_epoll_process_events (cycle=0x7f60a3888b90, timer=<optimized out>, flags=<optimized out>)
    
  3. 第三次触发:连接被释放。 程序在 ngx_http_close_connection -> ngx_free_connection 这个调用链路中触发了观察点。这说明 c 所指向的连接被正常关闭并释放了。在关闭前,c->destroyed 标志位被设置为 1。

    #0  0x000000000049511b in ngx_free_connection (c=c@entry=0x7f5fc3a010f8) at src/core/ngx_connection.c:1220
    #1  0x0000000000495551 in ngx_close_connection (c=c@entry=0x7f5fc3a010f8) at src/core/ngx_connection.c:1292
    #2  0x00000000004cbf0d in ngx_http_close_connection (c=0x7f5fc3a010f8) at src/http/ngx_http_request.c:4575
    #3  0x00000000004c8be8 in ngx_http_core_content_phase (r=0x7f60a3841050, ph=0x7f60a2f509c0) at src/http/ngx_http_core_module.c:1388
    #4  0x00000000004c2305 in ngx_http_core_run_phases (r=r@entry=0x7f60a3841050) at src/http/ngx_http_core_module.c:1069
    #5  0x00000000004c23ea in ngx_http_handler (r=r@entry=0x7f60a3841050) at src/http/ngx_http_core_module.c:1052
    #6  0x00000000004cfc69 in ngx_http_process_request (r=r@entry=0x7f60a3841050) at src/http/ngx_http_request.c:2764
    #7  0x00000000004d06f7 in ngx_http_process_request_headers (rev=rev@entry=0x7f5fbfc010e0) at src/http/ngx_http_request.c:1995
    #8  0x00000000004d10fd in ngx_http_process_request_line (rev=0x7f5fbfc010e0) at src/http/ngx_http_request.c:1473
    #9  0x00000000004b2d89 in ngx_epoll_process_events (cycle=0x7f60a3888b90, timer=<optimized out>, flags=<optimized out>)
    

此时,整个事件的链条已经清晰:在 ngx_http_process_request_headers 函数的处理流程中,当前的连接 c 先被关闭,其 destroyed 标志被设为 1。但由于客户的二次开发修改了 Nginx 框架,后续代码又立即申请了一个新连接,而 Nginx 的内存池恰好把刚刚释放的内存块分配了出去。这导致原来的 c 指针指向的内存被重新初始化,destroyed 标志位变回 0。

ngx_http_process_request_headers 的逻辑继续往下走,最后调用 ngx_http_run_posted_requests(c) 时,if (c->destroyed) 的检查因为标志位被清零而失效。函数继续执行,试图访问 c->data(即 r),但此时的 c->data 已经是属于一个新连接的值,其内容和上下文完全不匹配,最终导致访问非法内存而崩溃。

二次开发如何意外破坏了 Nginx 的生命周期管理

问题的根源在于客户的二次开发代码破坏了 Nginx 原本严谨的连接生命周期管理机制。具体来说,在 ngx_http_process_request_headers 这一个函数处理域内,发生了“释放并立即重用”同一个连接对象的冲突。

调用链如下:

  1. ngx_http_process_request_headers -> ngx_http_process_request -> … -> ngx_http_close_connection:连接被关闭,c->destroyed 被设置为 1。
  2. ngx_http_process_request_headers -> ngx_http_process_request -> … -> ngx_get_connection:由于二次开发逻辑,代码在此处申请了一个新连接,恰好重用了刚被释放的内存,导致 c->destroyed 被清零。
  3. ngx_http_process_request_headers -> ngx_http_run_posted_requests(c):检查 c->destroyed 失效,使用被污染的 c->data,导致程序崩溃。

为什么 OpenResty XRay 是您的不二之选?

在瞬息万变的线上业务中,每一次服务崩溃都可能意味着用户流失和收入损失。像本次案例中由二次开发不当引发的内存踩踏问题,堪称潜伏在系统深处的“隐形杀手”。它行踪诡秘,破坏力强,足以让最优秀的开发团队耗费数周时间追查,却依旧束手无策。

传统的日志分析、代码审查等手段,在面对这类“第一现场”早已被破坏的复杂问题时,往往显得苍白无力。这正是 OpenResty XRay 的价值所在——我们提供的不仅是一个工具,更是一套面向未来的智能诊断体系。

  • 告别大海捞针,直达问题根源 传统的排错方式如同大海捞针,耗时耗力且结果难料。OpenResty XRay 凭借其强大的自动化根本原因分析能力,能穿透层层迷雾,精准锁定导致崩溃的瞬时内存操作,将从“连接被污染”到“程序崩溃”的完整逻辑链清晰地呈现在您面前。它将您团队数周的工作,缩短为几分钟的精准分析。

  • 从“救火队”到“架构师”,释放团队潜力 让宝贵的研发资源从无休止的线上“救火”中解放出来。OpenResty XRay 能够快速定位并解决这类隐藏极深的 Bug,让您的团队能更专注于业务创新和架构优化,将精力投入到能创造更大价值的工作中去,而不是被动地响应故障。

  • 为核心业务保驾护航,避免重大损失 每一次稳定性挑战,都是对业务核心竞争力的考验。OpenResty XRay 提供的正是这种企业级的稳定性保障。通过深度、专业的洞察力,我们帮助您提前发现并根除潜在风险,确保您的核心业务 7x24 小时稳健运行,避免因底层技术问题造成难以估量的业务影响和品牌声誉损失。

选择 OpenResty XRay,就是选择了一份对核心业务稳定性的专业承诺。我们致力于成为您最可靠的技术伙伴,用最前沿的动态追踪技术,守护您业务的每一个关键时刻。

关于 OpenResty XRay

OpenResty XRay 是一款动态追踪产品,它可以自动分析运行中的应用,以解决性能问题、行为问题和安全漏洞,并提供可行的建议。在底层实现上,OpenResty XRay 由我们的 Y 语言驱动,可以在不同环境下支持多种不同的运行时,如 Stap+、eBPF+、GDB 和 ODB。

关于作者

章亦春是开源 OpenResty® 项目创始人兼 OpenResty Inc. 公司 CEO 和创始人。

章亦春(Github ID: agentzh),生于中国江苏,现定居美国湾区。他是中国早期开源技术和文化的倡导者和领军人物,曾供职于多家国际知名的高科技企业,如 Cloudflare、雅虎、阿里巴巴,是“边缘计算“、”动态追踪“和“机器编程“的先驱,拥有超过 22 年的编程及 16 年的开源经验。作为拥有超过 4000 万全球域名用户的开源项目的领导者。他基于其 OpenResty® 开源项目打造的高科技企业 OpenResty Inc. 位于美国硅谷中心。其主打的两个产品 OpenResty XRay(利用动态追踪技术的非侵入式的故障剖析和排除工具)和 OpenResty Edge(最适合微服务和分布式流量的全能型网关软件),广受全球众多上市及大型企业青睐。在 OpenResty 以外,章亦春为多个开源项目贡献了累计超过百万行代码,其中包括,Linux 内核、Nginx、LuaJITGDBSystemTapLLVM、Perl 等,并编写过 60 多个开源软件库。

关注我们

如果您喜欢本文,欢迎关注我们 OpenResty Inc. 公司的博客网站 。也欢迎扫码关注我们的微信公众号:

我们的微信公众号

翻译

我们提供了英文版原文和中译版(本文)。我们也欢迎读者提供其他语言的翻译版本,只要是全文翻译不带省略,我们都将会考虑采用,非常感谢!