在 OpenResty 或 Nginx 服務器中運行 Lua 代碼現在已經變得愈來愈常見,由於人們但願他們的非阻塞的 Web 服務器可以兼具超高的性能和很大的靈活性。有些人使用 Lua 完成一些很是簡單的任務,好比檢查和修改某些請求頭和響應體數據,而有些人則利用Lua 建立很是複雜的 Web 應用、 CDN 軟件和 API 網關等等。Lua 以簡單、內存佔用小和運行效率高而著稱,尤爲是在使用 LuaJIT 這樣的的即時編譯器 (JIT) 的時候。但有些時候,在 OpenResty 或 Nginx 服務器上運行的 Lua 代碼也會消耗過多的 CPU 資源。一般這是因爲程序員的編程錯誤,好比調用了一些昂貴的 C/C++ 庫代碼,或者其餘緣由。html
要想在一個在線的 OpenResty 或 Nginx 服務器中快速地定位全部的 CPU 性能瓶頸,最好的方法是使用 OpenResty XRay 產品提供的 Lua 語言級別 CPU 火焰圖的採樣工具。這個工具不 須要對 OpenResty 或 Nginx 的目標進程作任何修改,也不會對生產環境中的進程產生任何可覺察的影響。nginx
本文將解釋什麼是火焰圖,以及什麼是 Lua 級別的 CPU 火焰圖,會穿插使用多個小巧且獨立的 Lua 代碼實例來作演示。咱們將利用 OpenResty XRay 來生成這些示例的火焰圖來進行講解和分析。咱們選擇小例子的緣由是,它們更容易預測和驗證各類性能分析的結果。相同的分析方法和工具也適用於那些最複雜的 Lua 應用。過去這幾年,咱們使用這種技術和可視化方式,成功地幫助了許多擁有繁忙網站或應用的企業客戶。git
火焰圖是由 Brendan Gregg 發明的一種可視化方法,用於展現某一種系統資源或性能指標,是如何定量分佈在目標軟件裏全部的代碼路徑上的。程序員
這裏的「系統資源」或指標能夠是 CPU 時間、off-CPU 時間、內存使用、硬盤使用、延時,或者任何其餘你能想到的資源。github
而「代碼路徑」能夠定義爲目標軟件代碼中的調用棧軌跡。調用棧軌跡一般是由一組函數調用幀組成的,一般出如今 GDB 命令 bt
的輸出中,以及 Python 或 Java 程序的異常錯誤信息當中。好比下面是一個 Lua 調用棧軌跡的樣例:編程
C:ngx_http_lua_ngx_timer_at at cache.lua:43 cache.lua:record_timing router.lua:338 router.lua:route v2_routing.lua:1214 v2_routing.lua:route access_by_lua.lua:130
在這個例子中,Lua 棧是從基幀 access_by_lua.lua:130
一路生長到頂幀 C:ngx_http_lua_ngx_timer_at
。它清晰地顯示了不一樣的 Lua 或 C 函數之間是如何相互調用的,從而構成了「代碼路徑」的一種近似表示。安全
而上文中的「全部代碼路徑」,其實是從統計學的角度來看,並非要真的要去枚舉和遍歷程序中的每一條代碼路徑。顯然在現實中,後者的開銷極其高昂,由於組合爆炸的問題。咱們只要確保全部那些開銷不過小的代碼路徑,都有機會出如今咱們的圖中,而且咱們能以足夠小的偏差去量化他們的開銷。bash
本文會聚焦在一種特定類型的火焰圖上面。這種火焰圖專用於展現 CPU 時間(或 CPU 資源)是如何定量分佈在全部的 Lua 代碼路徑上的。特別地,咱們這裏只關注 OpenResty 或 Nginx 目標進程裏的 Lua 代碼。天然地,這類火焰圖被咱們命名爲「Lua 級別 CPU 火焰圖」(Lua-land CPU Flame Graphs)。服務器
本文標題圖片是一個火焰圖示例,後文將提供更多示例。微信
火焰圖僅用一張小圖,就能夠定量展現全部的性能瓶頸的全景圖,而不論目標軟件有多麼複雜。
傳統的性能分析工具一般會給用戶展現大量的細節信息和數據,而用戶很難看到全貌,反而容易去優化那些並不重要的地方,常常浪費大量時間和精力卻看不到明顯效果。傳統分析器的另外一個缺點是,它們一般會孤立地顯示每一個函數調用的延時,但很難看出各個函數調用的上下文,並且用戶還須刻意區分當前函數自己運行的時間(exclusive time)和包括了其調用其餘函數的時間在內的總時間(inclusive time)。
而相比之下,火焰圖能夠把大量信息壓縮到一個大小相對固定的圖片當中(一般一屏就能夠顯示全)。不怎麼重要的代碼路徑會在圖上天然地淡化乃至消失,而真正重要的代碼路徑則會天然地凸顯出來。越重要的,則會顯示得越明顯。火焰圖老是爲用戶提供最適當的信息量,很少,也很多。
對於新手而言,正確地解讀火焰圖可能不太容易。但經過一些簡單的解釋,用戶就會發現火焰圖其實很直觀,很容易理解。火焰圖是一張二維圖。y 軸顯示是代碼(或數據)上下文,好比目標編程語言的調用棧軌跡,而 x 軸則顯示的是各個調用棧所佔用的系統資源的比例。整個 x 軸一般表明了目標軟件所消耗的 100% 的系統資源(好比 CPU 時間)。x 軸上的各個調用棧軌跡的前後順序一般並不重要,由於這些調用棧只是根據函數幀名的字母順序來排列。固然,也會有一些例外,例如筆者發明了一種時序火焰圖,其中的 x 軸其實是時間軸,此時調用棧的前後順序就是時間順序。本文將專一於討論經典的火焰圖類型,即圖中 x 軸上的順序並不重要。
要學會讀懂一張火焰圖,最好的方法是嘗試解讀真實的火焰圖樣本。下文將提供多個火焰圖實例,針對 OpenResty 和 Nginx 服務器上運行的 Lua 應用,並提供詳細的解釋。
本節將列舉幾個簡單的有明顯性能特徵的 Lua 樣例程序,並將使用 OpenResty XRay 分析真實的 nginx 進程,生成 Lua 級別的 CPU 火焰圖,並驗證圖中顯示的性能狀況。咱們將檢查不一樣的案例,例如開啓了
JIT 即時編譯的 Lua 代碼、禁用了 JIT 編譯的 Lua 代碼(即被解釋執行),以及調用外部 C 庫代碼的 Lua 代碼。
首先,咱們來研究一個開啓了 JIT 即時編譯的 Lua 樣本程序(LuaJIT 是默認開啓 JIT)。
考慮下面這個獨立的 OpenResty 小應用。本節將一直使用這個示例,但會針對不一樣情形的討論需求,適時對這個例子進行少量修改。
咱們首先準備這個應用的目錄佈局:
mkdir -p ~/work cd ~/work mkdir conf logs lua
而後咱們建立以下所示的 conf/nginx.conf
配置文件:
master_process on; worker_processes 1; events { worker_connections 1024; } http { lua_package_path "$prefix/lua/?.lua;;"; server { listen 8080; location = /t { content_by_lua_block { require "test".main() } } } }
在 location /t
的 Lua 處理程序中,咱們加載了名爲 test
的外部 Lua 模塊,並當即調用該模塊的 main
函數。咱們使用了 lua_package_path 配置指令,來把 lua/
目錄添加到 Lua 模塊的搜索路徑列表中 ,由於咱們會把剛說起的 test
這個 Lua 模塊文件放到 lua/
目錄下。
這個 test
Lua 模塊定義在 lua/test.lua
文件中:
local _M = {} local N = 1e7 local function heavy() local sum = 0 for i = 1, N do sum = sum + i end return sum end local function foo() local a = heavy() a = a + heavy() return a end local function bar() return (heavy()) end function _M.main() ngx.say(foo()) ngx.say(bar()) end return _M
這裏咱們定義了一個計算量較大的 Lua 函數 heavy()
,計算從 1 到 1000 萬 (1e7
)的數字之和。而後咱們在函數 foo()
中調用兩次 heavy()
函數,而在 bar()
函數中只調用一次 heavy()
函數。最後,模塊的入口函數 _M.main()
前後調用 foo
和 bar
各 一次,並經過 ngx.say 向 HTTP 響應體輸出它們的返回值。
顯然,在這個 Lua 處理程序中,foo()
函數佔用的 CPU 時間應當是 bar()
函數的兩倍,由於 foo()
函數調用了 heavy()
函數兩次,而 bar()
僅調用了一次。經過下文中由 OpenResty XRay 採樣生成的 Lua 級別的 CPU 火焰圖,咱們能夠很容易地驗證這裏的觀察結果。
由於在這個示例中,咱們並無觸碰 LuaJIT 的 JIT 編譯器選項,所以 JIT 編譯便使用了默認的開啓狀態,而且現代的 OpenResty 平臺版本則老是隻使用 LuaJIT(對標準 Lua 5.1 解釋器的支持早已移除)。
如今,咱們能夠按下面的命令啓動這個 OpenResty 應用:
cd ~/work/ /usr/local/openresty/bin/openresty -p $PWD/
假設 OpenResty 安裝在當前系統的 /usr/local/openresty/
目錄下(這是默認的安裝位置)。
爲了使 OpenResty 應用忙碌起來,咱們可使用 ab
或 weighttp
這樣的壓測工具,向 URI http://localhost:8080/t
施加請求壓力,或者使用 OpenResty XRay 產品自帶的負載生成器。不管使用何種方式,當目標 OpenResty 應用的 nginx 工做進程保持活躍時,咱們能夠在 OpenResty XRay 的 Web 控制檯裏獲得相似下面這張 Lua 級別的 CPU 火焰圖:
咱們從圖上能夠觀察到下列現象:
content_by_lua(nginx.conf:24)
。這符合預期。圖中主要顯示了兩個代碼路徑,分別是
content_by_lua -> test.lua:main -> test.lua:bar -> test.lua:heavy -> trace#2:test.lua:8
以及
content_by_lua -> test.lua:main -> test.lua:foo -> test.lua:heavy -> trace#2:test.lua:8
兩個代碼路徑的惟一區別是中間的 foo
函數幀與 bar
函數幀。這也不出所料。
bar
函數的代碼路徑的寬度,是右側涉及 foo
的代碼路徑寬度的一半。換言之,這兩個代碼路徑在圖中 x 軸上的寬度比爲 1:2,即 bar
代碼路徑佔用的 CPU 時間,只有 foo
代碼路徑的50%。將鼠標移動到圖中的 test.lua:bar
幀(即方框)上,咱們能夠看到它佔據總樣本量(即總 CPU 時間)的 33.3%,而 test.lua:foo
所佔的比例爲66.7%. 顯然,與咱們以前的預測相比較,這個火焰圖提供的比例數字很是精確,儘管它所採起的是採樣和統計分析的方法。ngx.say()
等其餘代碼路徑,畢竟它們與那兩個調用了 heavy()
的 Lua 代碼路徑相比,所佔用的 CPU 時間微乎其微。在火焰圖中,那些微不足道的代碼路徑本就是小噪音,不會引發咱們的關注。咱們能夠始終專一於那些真正重要的部分,而不會爲其餘東西分心。那兩條熱代碼路徑(即調用棧軌跡)的頂部幀是徹底相同的,都是 trace#2:test.lua:8
. 它並非真正的 Lua 函數調用幀,而是一個「僞函數幀」,用於表示它正在運行一個被 JIT 編譯了的 Lua 代碼路徑。按照 LuaJIT 的術語,該路徑被稱爲」trace「(由於 LuaJIT 是一種 tracing JIT 編譯器)。這個」trace「的編號爲 2,而對應的被編譯的 Lua 代碼路徑是從 test.lua
文件的第 8 行開始的。而 test.lua:8
所指向的 Lua 代碼行是:
sum = sum + i
咱們很高興地看到,這個非侵入的採樣工具,能夠從一個沒有任何外掛模塊、沒有被修改過、也沒有使用特殊編譯選項的標準 OpenResty 二進制程序,獲得如此準確的火焰圖。這個工具沒有使用 LuaJIT 運行時的任何特殊特性或接口,甚至沒有使用它的 LUAJIT_USE_PERFTOOLS
特性或者 LuaJIT 內建的性能分析器。相反,該工具使用的是先進的動態追蹤 技術,僅讀取原始目標進程中原有的信息。咱們甚至能夠從 JIT 編譯過的 Lua 代碼中獲取足夠多的有用信息。
解釋執行的 Lua 代碼一般可以獲得最完美的的調用棧軌跡和火焰圖樣本。若是咱們的採樣工具可以正確處理 JIT 即時編譯後的 Lua 代碼,那麼在分析解釋的 Lua 代碼時,效果只會更好。LuaJIT 既有一個 JIT 編譯器,又同時有一個解釋器。它的解釋器的有趣之處在於,幾乎徹底是用手工編寫的彙編代碼實現的(固然,LuaJIT 引入了本身的一種彙編語言記法,叫作 DynASM)。
對於咱們一直在使用的那個 Lua 樣例程序,咱們須要在此作少量修改,即在 server {}
配置塊中添加下面的 nginx.conf
配置片斷:
init_by_lua_block { jit.off() }
而後從新加載(reload)或重啓服務器進程,並保持流量負載。
這回咱們獲得了下面這張 Lua 級別 CPU 火焰圖:
這張新圖與前一張圖在如下方面都極其類似:
bar
代碼路徑和 foo
代碼路徑。bar
代碼路徑依舊佔用了總 CPU 時間的三分之一左右,而 foo
佔用了餘下的全部部分(即大約三分之二)。content_by_lua
那一幀。然而,這張圖與前圖相比仍然有一個重要的區別:代碼路徑的頂幀再也不是 "trace" 僞幀了。這個變化也是預期的,由於這一回沒有 JIT 編譯過的 Lua 代碼路徑了,因而代碼路徑的頂部或頂幀變成爲 lj_BC_IFORL
和 lj_BC_ADDVV
等函數幀。而這些被 C:
前綴標記出來的 C 函數幀其實也並不是 C 語言函數,而是屬於彙編代碼幀,對應於實現各個 LuaJIT 字節碼的彙編例程,它們被標記成了 lj_BC_IFORL
等符號。天然地,lj_BC_IFORL
用於實現 LuaJIT 字節碼指令 IFORL
,而 lj_BC_ADDVV
則用於字節碼指令 ADDVV
。IFORL
用於解釋執行 Lua代碼中的 for
循環, 而 ADDVV
則用於算術加法。這些字節碼的出現,都符合咱們的 Lua 函數 heavy()
的實現方式。另外,咱們還能夠看到一些輔助的彙編例程,例如如 lj_meta_arith
和 lj_vm_foldarith
。
經過觀察這些函數幀的比例數值,咱們還得以一窺 CPU 時間在 LuaJIT 虛擬機和解釋器內部的分佈狀況,爲這個虛擬機和解釋器自己的優化鋪平道路。
Lua 代碼調用外部 C/C++ 庫函數的狀況很常見。咱們也但願經過 Lua 級別的 CPU 火焰圖,瞭解這些外部的 C 函數所佔用的 CPU 時間比例,畢竟這些 C 語言函數調用也是由 Lua 代碼發起的。這也是基於動態追蹤的性能分析的真正優點所在:這些外部 C 語言函數調用在性能分析中永遠不會成爲盲點1。
咱們一直使用的 Lua 樣例在這裏又須要做少量修改,即須要將 heavy()
這個 Lua 函數修改爲下面這個樣子:
local ffi = require "ffi" local C = ffi.C ffi.cdef[[ double sqrt(double x); ]] local function heavy() local sum = 0 for i = 1, N do -- sum = sum + i sum = sum + C.sqrt(i) end return sum end
這裏咱們使用 LuaJIT 的 FFI API ,先聲明瞭一下標準 C 庫函數 sqrt()
,並直接在 Lua 函數 heavy()
內部調用了這個 C 庫函數。它應當會顯示在對應的 Lua 級別 CPU 火焰圖中。
這次咱們獲得了下面這張火焰圖:
有趣的是,咱們果真在那兩條主要的 Lua 代碼路徑的頂部,看到了 C 語言函數幀 C:sqrt
。另外值得注意的是,咱們在頂部附近依舊看到了 trace#N
這樣的僞幀,這說明咱們經過 FFI 調用 C 函數的 Lua 代碼,也是能夠被 JIT 編譯的(這回咱們從 init_by_lua_block 指令中刪除了 jit.off()
語句)。
上文展現的火焰圖其實都是函數層面的火焰圖,由於這些火焰圖中所顯示的全部調用幀都只有函數名,而沒有發起函數調用的源代碼行的信息。
幸運的是, OpenResty XRay 的 Lua 級別性能分析工具支持生成代碼行層面的火焰圖,會在圖中添加 Lua 源代碼行的文件名和行號,以方便用戶在較大的 Lua 函數體中直接定位到某一行 Lua 源代碼。下圖是咱們一直使用的那個 Lua 樣例程序所對應的一張 Lua 代碼行層面的 CPU 火焰圖:
咱們能夠看到在每個函數幀上方都多了一個源代碼行的僞幀。例如,在函數 main
所在的 test.lua
源文件的第 32 行 Lua 代碼,調用了 foo()
函數。而在 foo()
函數所在的 test.lua:22
這一行,則調用了 heave()
函數。
代碼行層面的火焰圖對於準肯定位最熱的 Lua 源代碼行和 Lua 語句有很是大的幫助。當對應的 Lua 函數體很大的時候,代碼行層面的火焰圖能夠幫助節約排查代碼行位置的大量時間。
在多核 CPU 的系統上,爲單個 OpenResty 或 Nginx 服務器實例配置多個 nginx 工做進程是很常見的作法。OpenResty XRay 的分析工具支持同時對一個指定進程組中的全部進程進行採樣。當進來的流量不是很大,而且可能分佈在任意一個或幾個 nginx 工做進程上的時候,這種全進程組粒度的採樣分析是很是實用的。
咱們也能夠從很是複雜的 OpenResty/Lua 應用中獲得 Lua 級別的 CPU 火焰圖。例如,下面的 Lua 級別 CPU 火焰圖源自對運行了咱們的 OpenResty Edge 產品的「迷你 CDN」服務器進行了採樣。這是一款複雜的 Lua 應用,同時包含了全動態的 CDN 網關、地理敏感的 DNS 權威服務器和一個 Web 應用防火牆(WAF):
從圖上能夠看到,Web 應用防火牆(WAF)佔用的 CPU 時間最多,內置 DNS 服務器也佔用了很大一部分 CPU 時間。咱們佈署在全球範圍的」迷你 CDN「網絡爲咱們本身運營的多個網站,好比 openresty.org 和 openresty.com 提供了安全和加速支持。
它還能夠分析那些基於 OpenResty 的 API 網關軟件,例如 Kong
等等。
咱們使用的是基於採樣的方法,而不是全量埋點,所以爲生成 Lua 級別 CPU 火焰圖所產生的運行時開銷一般能夠忽略不計。不管是數據量仍是
CPU 損耗都是極小的,因此這類工具很是適合於生產環境和在線環境。
若是咱們經過固定速率的請求來訪問 nginx 目標進程,而且 Lua 級別 CPU 火焰圖工具同時在進行密集採樣,則該目標進程的 CPU 使用率隨時間的變化曲線以下所示:
該 CPU 使用率的變化曲線圖也是由 OpenResty XRay 自動生成和渲染的。
在咱們中止工具採樣以後,同一個 nginx 工做進程的 CPU 使用量曲線仍然很是類似:
咱們憑肉眼很難看出先後兩條曲線之間有什麼差別。因此,工具進行分析和採樣的開銷確實是很是低的。
而當工具不在採樣時,對目標進程的性能影響嚴格爲零,畢竟咱們並不須要對目標進程作任何的定製和修改。
因爲使用了動態追蹤技術,咱們不會改變目標進程的任何狀態,甚至不會修改其中哪怕一比特的信息2。這樣能夠確保目標進程不管是在採樣時,仍是沒有采樣時,其行爲(幾乎)是徹底相同的。這就保證了目標進程自身的可靠性(不會有意外的行爲變化或進程崩潰),其行爲不會由於分析工具的存在而受到任何影響。目標進程的表現徹底沒有變化,就像是爲一隻活體動物拍攝 X 光片同樣。
傳統的應用性能管理(APM)產品可能要求在目標軟件中加載特殊的模塊或插件,甚至在目標軟件的可執行文件或進程空間裏強行打上補丁或注入本身的機器代碼或字節碼,這均可能會嚴重影響用戶系統的穩定性和正確性。
由於這些緣由,咱們的工具能夠安全應用到生產環境中,以分析那些在離線環境中很難復現的問題。
OpenResty XRay 產品提供的 Lua 級別 CPU 火焰圖的採樣工具,同時支持 LuaJIT 的 GC64 模式 或非 GC64 模式,也支持任意的 OpenResty 或 Nginx 的二進制程序,包括用戶使用任意構建選項本身編譯的、優化或未優化的二進制程序。
OpenResty XRay 也能夠對在 Docker 或 Kubernetes 容器內運行的 OpenResty 和 Nginx 服務器進程進行透明的分析,並生成完美的 Lua 級別的 CPU 火焰圖,不會有任何問題。
咱們的工具還能夠分析由 resty 或 luajit
命令行工具運行的那些基於控制檯的用戶 Lua 程序。
咱們也支持較老的 Linux 操做系統和內核,好比使用 2.6.32 內核的 CentOS 6 老系統。
如前文所述,火焰圖能夠用於可視化任意一種系統資源或性能指標,而不只限於 CPU 時間。所以,咱們的 OpenResty XRay 產品中也提供了其餘類型的 Lua 級別火焰圖,好比 off-CPU 火焰圖、垃圾回收(GC)對象大小和數據引用路徑火焰圖、新 GC 對象分配火焰圖、Lua 協程棄權(yield)時間火焰圖、文件 I/O 延時火焰圖等等。
咱們的博客網站 將會發文詳細介紹這些不一樣類型的火焰圖。
咱們在本文中介紹了一種很是實用的可視化方法,火焰圖,能夠直觀地分析任意軟件系統的性能。咱們深刻講解了其中的一類火焰圖,即 Lua 級別 CPU 火焰圖。這種火焰圖可用於分析在 OpenResty 和 Nginx 服務器上運行的 Lua 應用。咱們分析了多個 Lua 樣例程序,簡單的和複雜的,同時使用 OpenResty XRay 生成的對應的 Lua 級別 CPU 火焰圖,展現了動態追蹤工具的威力。最後,咱們檢查了採樣分析的性能損耗,以及在線使用時的安全性和可靠性。
章亦春是開源項目 OpenResty® 的創始人,同時也是 OpenResty Inc. 公司的創始人和 CEO。他貢獻了許多 Nginx 的第三方模塊,至關多 Nginx 和 LuaJIT 核心補丁,而且設計了 OpenResty XRay 等產品。
若是您以爲本文有價值,很是歡迎關注咱們 OpenResty Inc. 公司的博客網站 。也歡迎掃碼關注咱們的微信公衆號:
咱們提供了英文版 原文和中譯版(本文)。咱們也歡迎讀者提供其餘語言的翻譯版本,只要是全文翻譯不帶省略,咱們都將會考慮採用,很是感謝!