面對性能調優問題,很多人往往只是單純的套用既往的經驗:先試試一個,不行再試試另一個。面對簡單的問題,如此通常能事半功倍;但是當面對復雜問題的時候,單憑經驗往往并不能達到立竿見影的效果,此時我們需要更精準的判斷性能短板在哪里。
一個 openresty 項目,不了解 openresty 的可以參考我以前的文章,從 top 運行結果看,軟中斷 si 分配不均,絕大部分壓在了 CPU5 上,導致 CPU5 的空閑 id 接近于零,最終的結果是其它 CPU 雖然還有空閑 id,但是卻礙于 CPU5 的限制而使不上勁兒:
top 顯示 si 不均衡
既然知道了軟中斷是系統的性能短板,那么讓我們看看軟中斷都消耗在哪:
shell> watch -d -n 1 'cat /proc/softirqs'
watch 顯示軟中斷集中在 NET_RX
通過 watch 命令,我們可以確認 CPU5 的軟中斷接種在 NET_RX 上,也就是網卡上,除了 CPU5,其它 CPU 的 NET_RX 普遍低了一個數量級,由此可以判斷,此網卡工作在單隊列模式上,我們不妨通過 ethtool 命令來確認一下:
shell> ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
RX: 0
TX: 0
Other: 0
Combined: 8
Current hardware settings:
RX: 0
TX: 0
Other: 0
Combined: 1
主要留意結果中的 Combined 即可,其中 Channel parameters 里的 Combined 表示硬件支持的最大隊列數,而 Current hardware settings 里的 Combined 表示當前值。如果硬件只支持單隊列,那么可以通過 RPS 之類的方式來模擬多隊列;如果硬件支持多隊列,那么激活它就行了。結果顯示:本例中的網卡支持 8 個隊列,當前開啟了 1 個隊列,激活網卡的多隊列功能后再觀察 top 運行結果會發現 si 均衡了,其它 CPU 也能使上勁兒了:
shell> ethtool -L eth0 combined 8
top 顯示 si 均衡了
至此我們搞定了網卡多隊列功能,其實說白了這是一個資源分配不均衡的問題,那么除了網卡多隊列以外,還有其它資源分配不均衡的問題么,讓我們繼續看 top 運行結果:
top 顯示 nginx 的 time 不均衡
如上所示,會發現 nginx 多進程間的 time 分配并不均衡(此 time 是 cpu time),有的干活多,有的干活少,相關問題在「 Why does one NGINX worker take all the load? 」一文中有相關描述:在原本的nginx 模型中,一個 socket 接收所有的請求,不同的 worker 按照accet_mutext 的設置來爭搶請求,不過因為 Linux 的 epoll-and-accept 負載均衡算法采取了類似 LIFO 的行為,結果導致請求在不同進程間的分配變得十分不均衡:
使用 reuseport 前
為了解決此類問題,nginx 實現了 reuseport 指令,每個進程都有對應自己的 socket:
使用 reuseport 后
激活了 reuseport 指令后,我們通過 top 命令觀察會發現 time 分配變得均衡了:
http {
server {
listen 80 reuseport;
...
}
}
top 顯示 nginx 的 time 均衡了
雖然我們沒有改動一行代碼,但是僅僅通過激活網卡多隊列和 nginx reuseport,就提升了性能,但是如果想更進一步提升性能,必須深入代碼層面才行,下面讓我們看看如何發現代碼層面的短板在哪里,是時候請出火焰圖了,關于火焰圖的概念可以參考我以前的文章,如下是一個 on-CPU ( sample-bt )的火焰圖,同時采樣用戶態和內核態數據:
火焰圖顯示 cjson 吃掉了大量 cpu
如圖所示,cjson 吃掉了大量 cpu,同時發現寬大的火苗基本都是用戶態行為,于是我們去掉采樣內核態數據,從而降低噪音,重新繪制用戶態 on-CPU 火焰圖:
火焰圖顯示 cjson 吃掉了大量 cpu
說明:不了解火焰圖用法的話,可以參考 iresty 示例,另外,本例中因為服務器缺少 luajit debug symbol,采樣的是 C 語言數據,而不是 Lua 語言數據,結果可能有失精準。
如圖所示,確實 cjson 吃掉了大量 CPU。對照代碼,發現存在若干次解碼 json 數據的操作,于是我們可以判斷 CPU 是被 cjson.decode 吃掉的,這也正常,不管是什么語言,高效解碼 json 數據都是一個讓人頭疼的問題,群里問問別人有什么銀彈可用,結果有人推薦了 lua-resty-json ,從官網說明看,相對于 cjson,它把解碼 json 數據的性能提升了 10%~50%,實際具體大小取決于數據的復雜程度,使用上也很簡單:
shell> cd /path/to/lua-resty-json/
shell> make
shell> cp json_decoder.lua /usr/local/openresty/lualib/
shell> cp libljson.so /usr/local/openresty/lualib/
剩下的具體用法參考 測試用例 就可以了,需要說明的是 lua-resty-json 只實現了解碼。
不過我并沒有采用把 cjson 替換為 lua-resty-json 的方法來提升性能,這是因為通過數據分析,我發現在本例中,存在明顯的熱數據,如果把這些熱數據直接緩存在進程中,那么對熱數據而言,就完全不需要解碼 json 數據了,可以利用 lua-resty-mlcache 來實現:
mlcache 的多級緩存結構
至此,本次性能調優告一段落,實際上這并不是一次嚴謹的性能調優,我只是利用一些項目的間歇期偶爾搞一下,不過最終我們把服務器數量降低了一半以上。
原文轉自:https://blog.huoding.com/2020/09/12/850