從崩潰到根因:OpenResty XRay 如何將 Nginx 記憶體踩踏問題分析得明明白白
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
,其中 c
是 ngx_connection_t
型別的連線物件。我們懷疑 c
或 c->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
繼續執行程式,觀察點很快觸發了多次,揭示了關鍵的操作序列:
第一次觸發:
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>)
第二次觸發:記憶體被重用。 程式在
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>)
第三次觸發:連線被釋放。 程式在
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
這一個函式處理域內,發生了“釋放並立即重用”同一個連線物件的衝突。
呼叫鏈如下:
ngx_http_process_request_headers
->ngx_http_process_request
-> … ->ngx_http_close_connection
:連線被關閉,c->destroyed
被設定為 1。ngx_http_process_request_headers
->ngx_http_process_request
-> … ->ngx_get_connection
:由於二次開發邏輯,程式碼在此處申請了一個新連線,恰好重用了剛被釋放的記憶體,導致c->destroyed
被清零。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、LuaJIT、GDB、SystemTap、LLVM、Perl 等,並編寫過 60 多個開源軟體庫。
關注我們
如果您喜歡本文,歡迎關注我們 OpenResty Inc. 公司的部落格網站 。也歡迎掃碼關注我們的微信公眾號:
翻譯
我們提供了英文版原文和中譯版(本文)。我們也歡迎讀者提供其他語言的翻譯版本,只要是全文翻譯不帶省略,我們都將會考慮採用,非常感謝!