在 OpenResty 或 Nginx 程序中追蹤最慢的 PCRE 正規表示式
正規表示式在 Web 應用中無處不在,包括在 OpenResty 或 Nginx(使用 OpenResty 的核心元件,如 Lua Nginx 模組)上執行的 Lua 應用。OpenResty 提供了 ngx.re.*
Lua API 函式,如 ngx.re.match 和 ngx.re.find,將 PCRE 正規表示式庫的 C API 暴露給 Lua 環境。PCRE 為相容 Perl 的正規表示式提供了一個非常高效的實現,並配備即時(JIT)編譯器。PCRE 被各種著名的系統軟體廣泛使用,包括 Nginx 核心和 PHP 執行時。
然而,許多 Web 開發人員很少關注他們的正規表示式的效率。對於編寫不當的正規表示式,特別是在像 PCRE 這樣的回溯實現中,可能會出現病態行為。更糟糕的是,這種病態行為可能只有在具有特定特徵的相對較大的資料輸入時才會觸發。一些 DoS 攻擊者甚至可能專門針對這些易受攻擊的正規表示式模式。
幸運的是,我們的 OpenResty XRay 產品提供了動態追蹤工具,可以實時分析線上 OpenResty 和 Nginx 程序,並快速定位那些易受攻擊和執行緩慢的正規表示式。目標 OpenResty 或 Nginx 應用不需要特殊的外掛或模組,而且當 OpenResty XRay 不進行取樣時,絕對不會產生任何開銷。
在本教程中,我們將透過實際例子演示這些高階分析器。最後,我們還將探討具有較少病態行為的替代方案,包括 RE2 和 OpenResty Regex 引擎。
系統環境
在此,我們以 RedHat Enterprise Linux 7 系統為例。任何受 OpenResty XRay 支援的 Linux 發行版都應該同樣適用,如 Ubuntu、Debian、Fedora、Rocky、Alpine 等。
我們使用開源的 OpenResty 二進位制構建作為目標應用。您可以使用任何 OpenResty 或 Nginx 二進位制檔案,包括自行編譯的版本。您現有的伺服器安裝或程序無需特殊的構建選項、外掛或庫。這正是 動態追蹤 技術的優勢所在。它是真正非侵入式的。
我們還在同一系統上執行 OpenResty XRay 的 Agent 守護程序,並已 安裝和配置 了 openresty-xray-cli
包中的命令列工具。
一個示例 OpenResty/Nginx Lua 應用
我們有一個效率低下的 OpenResty/Nginx Lua 應用,在伺服器中 CPU 使用率達到 100%,這一點可以透過 top
命令列工具得到證實:
$ top -p 3584441
...
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
3584441 nobody 20 0 41284 7184 4936 R 100.0 0.0 1:43.97 nginx
請注意,Nginx 程序的 CPU 使用率為 100.0%,其 PID 為 3584441。我們將在後續的動態跟蹤分析中引用此 PID。
無需猜測,精準定位問題根源
C 語言級別的 CPU 火焰圖
為分析任何具有高 CPU 使用率的 C/C++ 程序(包括 Nginx 程序),我們首先應該從該程序獲取 C 語言級別的 CPU 火焰圖1。儘管 OpenResty XRay 可以自動檢測 OpenResty/Nginx 應用,對其進行取樣並生成各種型別的火焰圖,但在此,我們將僅出於演示目的使用手動方式執行命令列工具。
$ orxray analyzer run lj-c-on-cpu -p 3584441
Start tracing...
Go to https://x5vrki.xray.openresty.com/targets/68/history/664712 for charts.
我們使用 -p
選項來指定目標 Nginx 工作程序的 PID。然後我們執行標準的 OpenResty XRay 工具,名為 lj-c-on-cpu
。我們在此不使用更通用的 c-on-cpu
工具,因為前者可以自動展開來自 LuaJIT 和 PCRE 即時編譯器(JIT)的動態機器程式碼幀。
從這個 C 語言級別的 CPU 火焰圖中,我們可以看出:
- 幾乎所有的 CPU 時間都用於執行 Lua 程式碼(注意火焰圖中高亮顯示的
ngx_http_lua_run_thread
C 函式幀)。 - Lua 程式碼也在執行 PCRE 匹配器(注意圖中的
pcre_exec
函式幀)。 - 此外,PCRE JIT 編譯的程式碼正在被執行,這可以從圖中的
_pcre_jit_exec
函式幀看出。
請注意這個動態跟蹤工具的強大之處,它可以在不需要目標應用任何幫助的情況下透明地展開 JIT 編譯的程式碼幀2。
下一步是取樣並生成一個 Lua 級別的 CPU 火焰圖,這樣我們就可以看到 Lua 程式碼中發生了甚麼。
Lua 級別的 CPU 火焰圖
為了生成 Lua 級別的 CPU 火焰圖,我們可以使用標準分析器 lj-lua-on-cpu
,如下所示:
$ orxray analyzer run lj-lua-on-cpu -p 3584441
Start tracing...
Go to https://x5vrki.xray.openresty.com/targets/68/history/664870 for charts.
Lua 級別的火焰圖還顯示了經過 JIT 編譯的 Lua 程式碼,這一點從圖中的 trace#3:regex.lua:721
幀可以看出。這表示一個 LuaJIT 跟蹤物件,其跟蹤 ID 為 3
,起始於 regex.lua
原始檔的第 721 行 Lua 程式碼。我們可以看到,幾乎所有的 CPU 時間都集中在單一的 Lua 程式碼路徑上。在這個主導的 Lua 程式碼路徑中,最值得關注的 Lua 函式幀是 regex.lua:re_match
。在該 Lua 檔案中,我們有:
local re_match = ngx.re.match
因此,re_match
符號名指向 ngx.re.match
API 函式。接下來的問題是:哪個正規表示式如此耗時?我們可以直接檢視火焰圖中顯示的 content_by_lua(nginx.conf:69):10
所對應的 Lua 原始碼行。但如果 Lua 程式碼是通用的,僅僅是遍歷包含多個正規表示式的 Lua 表格進行匹配,這還不足以精確定位問題所在的正規表示式。
最慢的正規表示式
既然我們知道正規表示式匹配佔用了大部分 CPU 時間,現在我們可以使用另一個標準分析器,名為 lj-slowest-ngx-re
,來找出當前活躍程序中最慢的正規表示式匹配,方法如下:
$ orxray analyzer run lj-slowest-str-match-find -p 3584441 --y-var threshold_ns=1 --y-var max_hits=1000
Start tracing...
sub_regex_cache_count: 0
http: match
cache: ptr: 0x154bbc0, type: match, pat: "\w"
cache: ptr: 0x1517890, type: match, pat: "(?:.*)*css"
cache: ptr: 0x154a3a0, type: match, pat: ".*?js"
http: substitution
http: function substitution
...WARNING: reach samples count: 1000
=== start ===
max latency: 15844343 ns, ptr: 0x1517890
max latency: 13555 ns, ptr: 0x154a3a0
max latency: 8075 ns, ptr: 0x154bbc0
=== finish ===
Go to https://x5vrki.xray.openresty.com/targets/68/history/799249 for charts.
我們在網路瀏覽器中開啟 URL,以檢視生成的條形圖,如下所示。
此條形圖顯示了在取樣時間視窗內執行的最慢正規表示式的最大執行延遲。延遲以納秒為單位。顯然,(?:.*)*css
是絕對最慢的正規表示式,耗時超過 20.6 毫秒(或 20,671,282 納秒)。它也遠比其他正規表示式慢得多。有經驗的使用者可以迅速識別出,在正規表示式引擎中,使用貪婪量詞 .*
和常量字尾可能導致瘋狂的回溯。我們可以使用量詞的非貪婪版本來最佳化正規表示式:(?:.*?)css
。
最佳化最慢正規表示式後
在將最慢的正規表示式 (?:.*)*css
最佳化為 (?:.*?)css
後,我們重新載入應用並使用最佳化後的正規表示式重新生成新的 C 語言級別的 CPU 火焰圖。
我們可以看到新火焰圖的形狀和特徵與之前的有很大不同。瓶頸不再是執行 Lua 程式碼的路徑。相反,現在是負責寫出 HTTP 響應資料的 writev
系統呼叫(如上圖中紅色高亮所示)。請注意圖中的 ngx_http_output_filter
C 函式幀。該函式負責呼叫 Nginx 輸出過濾器鏈來傳送 HTTP 響應頭和正文資料。
讓我們對最佳化後的應用重新執行 lj-slowest-ngx-re
工具。結果圖表如下。
我們可以看到,正規表示式的最大延遲大大縮短,僅為 58,660 納秒,約 59 微秒。
限制 PCRE 的執行開銷
OpenResty 的 Lua Nginx 模組提供了 lua_regex_match_limit 指令,用於為單個正規表示式匹配設定基本操作的硬限制。
例如,我們可以在 nginx.conf
檔案中新增以下行:
lua_regex_match_limit 100000;
And make sure we log any error returned by the ngx.re.match
API function call:
local m, err = re_match(content, regexes[i], "jo")
if err ~= nil then
ngx.log(ngx.ERR, "re_math failed, re: ", regexes[i], ", error: ", err)
return
end
並確保我們記錄 ngx.re.match
API 函式呼叫返回的任何錯誤:
2022/07/13 21:05:13 [error] 3617128#3617128: *1 [lua] content_by_lua(nginx.conf:69):11: re_math failed, re: (.*)*css, error: pcre_exec() failed: -8, client: 127.0.0.1, server: localhost, request: "GET / HTTP/1.1", host: "127.0.0.1"
非回溯正規表示式引擎
PCRE 幾乎完全支援 Perl 正規表示式。這種靈活性伴隨著回溯帶來的成本。使用者可能還想考慮其他非回溯的正規表示式實現,如 Google 的 RE2 或 OpenResty Inc 的商業 OpenResty Regex 引擎。這些引擎通常採用基於自動機理論的演算法。遺憾的是,對於典型的正規表示式,RE2 的平均匹配時間比 PCRE 慢約 50%,儘管它在慢速匹配方面幾乎沒有病態情況。另一方面,OpenResty Regex 引擎實現了與 PCRE 相似的平均效能(有時甚至更快),並且也沒有回溯問題。
Lua 的內建字串模式
標準 Lua 5.1 語言定義了自己的正規表示式語言語法,由其標準 API 函式 string.match 和 string.find 支援。OpenResty XRay 還提供了用於分析此類模式匹配操作的動態跟蹤工具。我們將在另一個教程中介紹這個主題。
跟蹤容器內的應用
OpenResty XRay 工具支援透明地跟蹤容器化應用。Docker 和 Kubernetes (K8s) 容器都可以透明地工作。與普通應用程序一樣,目標容器不需要任何應用或額外許可權。OpenResty XRay Agent 守護程序應該在目標容器外部執行(如直接在主機作業系統中或在其自己的特權容器中)。
讓我們看一個例子。我們首先使用 docker ps
命令檢查容器名稱或容器 ID。
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
4465297209d9 openresty/openresty:1.19.3.1-2-alpine-fat "/usr/local/openrest…" 18 months ago Up 45 minutes angry_mclaren
在這裡,容器名稱是 angry_mclaren
。然後我們可以在這個容器中找出目標程序的 PID。
$ docker top angry_mclaren
UID PID PPID C STIME TTY TIME CMD
root 3605106 3605085 0 19:40 ? 00:00:00 nginx: master process /usr/local/openresty/bin/openresty -g daemon off;
nobody 3605692 3605106 0 19:44 ? 00:00:20 nginx: worker process
openresty
工作程序的 PID 是 3605692
。接下來,我們像往常一樣對這個 PID 執行 OpenResty XRay 分析器。
$ orxray analyzer run lj-slowest-ngx-re -p 3605692
Start tracing...
...
Go to https://x5vrki.xray.openresty.com/targets/68/history/684552 for charts.
OpenResty XRay 還能夠自動檢測長時間執行的程序,將其識別為特定型別的“應用”(如 “OpenResty”、“Python” 等)。
工具的實現方式
所有工具都是用 Y 語言實現的。OpenResty XRay 透過 Stap+2 或 eBPF3 後端執行這些工具,這兩種後端都使用了 100% 非侵入式的動態追蹤技術,基於 Linux 核心的 uprobes
和 kprobes
功能。
我們不需要目標應用和程序的任何配合。不使用也不需要任何日誌資料或指標資料。我們直接以嚴格的只讀方式分析執行中程序的程序空間。而且我們絕不向目標程序注入任何位元組碼或其他可執行程式碼。這是 100% 乾淨和安全的。
工具的開銷
本教程中演示的動態追蹤工具 lj-slowest-ngx-re
非常高效,適合線上執行。
當工具未執行且不進行主動取樣時,對系統和目標程序的開銷嚴格為零。我們從不向目標應用和程序注入任何額外的程式碼或外掛;因此,不存在固有的開銷。
在取樣期間,在典型的伺服器硬體上,請求延遲平均僅增加約 7 微秒(us)。對於每個 CPU 核心每秒處理數萬個請求的最快 OpenResty/Nginx 伺服器來說,最大請求吞吐量的降低是不可測量的。
關於作者
章亦春是開源 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. 公司的部落格網站 。也歡迎掃碼關注我們的微信公眾號:
翻譯
我們提供了英文版原文和中譯版(本文)。我們也歡迎讀者提供其他語言的翻譯版本,只要是全文翻譯不帶省略,我們都將會考慮採用,非常感謝!
-
OpenResty XRay 的 C 語言級別 CPU 火焰圖比大多數開源解決方案更為強大。它支援在不需要目標程序協作的情況下展開 JIT 編譯的機器程式碼,並且支援內聯 C 函式以及詳細的原始檔名和行號。 ↩︎
-
開源的 Linux
perf
工具鏈對 JIT 編譯的程式碼展開支援有限,它需要目標應用建立一個特殊的perf.map
檔案。這需要目標應用的特殊幫助和協作,即使使用者不需要取樣和分析,也可能導致檔案資料洩露和額外的執行時開銷。 ↩︎