系統呼叫導致網路收包卡頓的問題分析
前言
G行某平臺類應用系統提供高併發、低延遲的服務請求,該系統的的響應時間在1毫秒左右,目前最大TPS在2.5萬左右,為保證該系統的快速響應,系統設定的超時時間為30毫秒。在一次巡檢中發現,該系統的幾臺伺服器超時交易筆數在逐漸增加,為避免系統執行風險,協調網路、作業系統等專家一同分析,在分析過程中補充學習了大量Linux作業系統核心的知識,現將分析過程及其中用到的知識點記錄下來,以便為解決類似的問題提供一個通用的解決思路。
一、應用系統超時現象
外部系統將請求傳送到前端服務,前端服務進行業務邏輯後重新組裝報文,將請求傳送到後端服務。交易處理超時體現在前端服務對後臺服務的請求上,但是根據網路裝置的映象網路包分析,後端服務的處置時間沒有異常,前端傳送給後端的請求,後端都可以快速的處理。因此問題還是出現在前端服務上。
圖1 系統邏輯架構
由於該系統的交易量大、響應時間低,所以對處理過程的日誌記錄較少,根據前端服務的應用日誌無法定位根本原因。針對前端服務到後臺服務的請求,選一段超時交易較多的時間段的網路映象包進行分析。發現超時交易集中在兩個固定的時間點,分別為:11:29:4.200,超時9筆,平均TCPack_rtt為5毫秒;11:29:54.100,超時2筆,平均TCPack_rtt為6毫秒,在這兩個時間段超時交易的rtt都大於30毫秒,沒有超時的交易也有部分ack的rtt相對較長。其他正常交易時間段的ack_rtt在0.06毫秒左右。
圖2 網路映象包異常現象
根據這個現象初步懷疑在網路收發包上出現了擁堵問題,作業系統網路協議棧對收到的網路包沒有及時處理。具體處理過程為前端傳送請求到後端服務,後端服務立即進行了響應並返回響應包,由於協議棧沒有及時處理響應包,導致相應的ack包沒有及時返回,同時導致前端服務沒有及時收到後端服務的返回,從而出現超時的現象。
為驗證懷疑的方向,將一臺伺服器進行物理重啟,重啟後超時現象消失;同時將另外一臺伺服器只重啟應用系統,超時現象沒有出現緩解。因此可以確定交易的超時現象是由於作業系統的原因導致,和應用系統的處理效能無關。
二、問題排查
由於懷疑和作業系統的協議棧處理有關,為便於問題排查,同時避免對生產的穩定執行造成影響,我們使用perf和hping3進行異常的復現和問題分析。perf是Linux的一款效能分析工具,能夠進行系統核心函式級和指令級的熱點查詢,可以用來分析程式中熱點函式的CPU佔用率,從而定位效能瓶頸。Hping3是一個開源網路工具,通過rawSocket直接組裝icmp、udp、tcp報文,並記錄對方伺服器的ack響應時間。
圖3 問題排查方案
分析方案是找同子網的另外一臺伺服器部署hping3工具,向異常伺服器不停的傳送tcp報文,並記錄ack返回出現延遲的時間點。在異常的伺服器上部署perf工具,跟蹤作業系統的核心呼叫,以便發現異常點。通過hping3每1毫秒傳送一個網路包,記錄其收到ack包的時間,可以發現在1650140557這個時間點出現大量rtt時間較長的情況,分析perf工具採集的核心呼叫資料,可以發現在相同的時間點存在大量ss程序發起的請求,當時正在執行的系統呼叫函式為read,根據呼叫棧proc_reg_read可以定位為程式正在讀取/proc目錄下的檔案內容。
圖4 核心系統異常呼叫棧
由於生產伺服器上部署了各種代理和大量的監控指令碼,因此懷疑是由這些代理、監控指令碼發起的ss命令呼叫,在系統中查詢使用ss命令的指令碼,可以發現呼叫ss的命令列為圖5所示。
圖5 觸發異常的shell呼叫
通過strace跟蹤ss 呼叫過程,如圖6所示。
圖6 trace跟蹤ss呼叫過程
關注read處理較慢的系統呼叫,可以發現read處理時間超過10ms的地方共有5處,最長的處理時間在189ms;根據檔案控制代碼號3,可以確認正在讀取的檔案為”/proc/slabinfo”。
圖7 ss命令系統呼叫的異常
至此可以確認網路包接收卡頓,是由於監控週期性呼叫ss命令導致。ss命令會讀取/proc/slabinfo,而在讀取的時候有部分read函式的系統呼叫處理較慢,從而影響了網路接收包的處理。
為進一步驗證上述結論,我們將異常伺服器上使用ss命令的監控指令碼停止,並啟動前端服務將其加入生產佇列,經過長時間觀察,未再發現超時現象。然後手工在伺服器上連續執行幾次cat /proc/slabinfo,可以發現超時現象再次出現。
三、深入分析原因
1.第一個疑問:為什麼讀取/proc/slabinfo會導致網路包接收卡頓?
網路協議棧接收tcp報文並自動返回ack,是作業系統在中斷中處理的,優先級別較高,而使用者讀取/proc/slabinfo是一般的系統呼叫,為什麼會影響核心的中斷處理呢?首先需要了解一下Linux作業系統程式碼執行的幾個上下文場景。
硬體中斷上下文:優先級別最高,只要有硬體中斷髮生就會被立即執行,這裡包括時鐘中斷、網絡卡中斷;
軟體中斷上下文:在硬體中斷執行完成或系統呼叫完成時執行,這個是作業系統核心核心程式碼執行的主要環境,包括程序排程,網路協議棧處理等等;
核心程序上下文:執行在核心態,但是受程序排程管理,按程序排程分配的時間片執行,超過執行的時間片就有可能被別的程序搶佔而暫停執行;
使用者程序上下文:這個是大部分應用程式執行的環境,受程序排程管理,按程序排程分配的時間片執行,超過執行的時間片會強制進行程序切換。
那他們的執行優先順序是不是就是:硬體中斷> 軟體中斷> 核心程序> 使用者程序。實際並不完全是這樣,由於硬體中斷、軟體中斷、核心程序都執行在核心態,執行在相同的地址空間,所有的資料都是共享的,如果硬體中斷強制中斷軟體中斷的執行,軟體中斷強制中斷核心程序的執行,那麼有可能會導致核心資料的混亂或者導致各種死鎖,為了保護臨界資料的安全和完整性,軟體中斷可以關硬體中斷,核心程序可以關閉硬體中斷和軟體中斷。
在執行cat/proc/slabinfo時,首先程式執行在使用者程序上下文,然後執行系統呼叫read被切換到核心程序上下文,由於slabinfo中儲存的是核心的記憶體分配資訊,在讀取記憶體分配資訊時為了保證資料的一致性,在核心程序上下文中關閉了硬體中斷和軟體中斷。由於程序排程也是通過硬體中斷中的時鐘中斷觸發的,所以如果長時間的關閉硬體中斷、軟體中斷,程序排程也不會被執行。如果read執行時間較長,關閉中斷的時間就會比較長,該程序就會長時間佔用CPU不釋放,阻止了核心協議棧對網路收包的處理。
檢視Linux核心程式碼/linux-3.0.101/mm/slab.c中的s_show函式,可看到如下關閉中斷的程式碼
圖8 讀取slabinfo時關閉中斷的核心程式碼
2.第二個疑問:伺服器具有40CPU,為什麼一個ss命令就會阻止網路包的處理呢,理論上應還有另外39個CPU來處理?
這個涉及RPS(ReceivePacket Steering)和RFS(ReceiveFlowSteering)技術,其主要功能就將高速網絡卡產生的大量網路包分配到多核CPU分別進行處理,但是這個分配是預分配,根據socket四元組(源IP、源埠、目的IP、目的埠)分配到不同的CPU進行處理。ss命令在哪個CPU上執行,那麼分配到這個CPU上的網路包處理就會出現卡頓。因此在生產環境的表現上也是隻有部分網路包出現了卡頓,而不是所有的網路包都會產生卡頓。
3.第三個疑問:讀取/proc/slabinfo為什麼會慢?
Slab演算法是以位元組為單位管理記憶體,是核心的小記憶體管理演算法。特點是基於物件進行管理。slab分配演算法採用cache儲存核心物件,把相同型別的物件歸為一類,每當要申請這樣一個物件,slab分配器就從一個slab列表中分配一個這樣大小的單元出去,而當要釋放時,將其重新儲存在該列表中,而不是直接返回給夥伴系統,從而避免這些內碎片。slab分配器並不丟棄已分配的物件,而是釋放並把它們儲存在記憶體中。當以後又要請求新的物件時,就可以從記憶體直接獲取而不用重複初始化。
通過cat /proc/slabinfo可以檢視slab分配的記憶體情況,其中包含的資訊主要為統計資訊,包括每種物件記憶體的總個數、已分配的個數、每頁記憶體的物件個數、佔用的記憶體頁數等等。這些統計資訊是怎麼來的呢?是核心中現成的統計資料嗎?通過檢視程式碼可以發現,是一個個累加彙總來的,如果記憶體物件的個數較多,那將是一個非常耗時的大迴圈。其核心程式碼如下:
通過cat /proc/slabinfo | sort -n-k3的輸出,檢視一下哪些記憶體物件的個數較多。
圖9 slab記憶體物件數量
如圖9所示佔用比較高的幾項是dentry、buffer_head、proc_inode_cache。dentry 是檔案目錄對inode對映的快取;Buffer_head 是硬碟的塊快取;proc_inode_cache 是/proc目錄下inode節點資訊的快取。dentry、proc_inode_cache分別達到了797萬和388萬,這個資料量非常大,導致在輸出/proc/slabinfo時耗時較長。
4.第四個疑問:為什麼重啟伺服器後網路包卡頓現象就消失,其後為什麼會變慢呢?
重啟後卡頓現象消失這個問題現在很容易回答,那就是由於重啟後slab中的快取資訊全部釋放了,物件個數較少,再統計slabinfo時處理較快,不會影響網路包的接收了。重啟執行一段時間後會再出現卡頓現象,是由於slab中快取的資訊又變多了,那是誰導致slab快取了大量資訊呢?
slab中個數較多的記憶體物件是dentry和proc_inode_cache,在檢視/proc目錄下的檔案時,作業系統會自動將目錄資訊和inode資訊快取到這兩個物件。因此是有程式週期性的讀取/proc目錄下的檔案導致這兩個記憶體物件快取個數增多的。再次跟蹤一下ss命令可以發現ss命會掃描/proc下所有程序開啟的檔案控制代碼,包括socket連結。我們這臺伺服器經常保持數萬的連結數,這些連結的存活時間最短只有幾秒鐘,最長也就幾個小時。因此執行一次ss-lntp命令會快取數萬的proc_inode_cache和dentry,如果週期性的執行這個命令,開啟的socket控制代碼又不停的發生變化,就會導致slab快取的數量不斷增長。
四、問題解決
通過上面的分析可以確認導致網路包卡頓的原因是:週期性的呼叫ss-lntp命令,同時這臺伺服器上的網路連結不斷髮生變化,使得slab中快取了大量的記憶體物件,並且ss會讀取/proc/slabinfo中的彙總資訊,而slabinfo中的彙總資訊是每次通過效率不高的一個個累加的方式獲得,且累加時關閉了作業系統的中斷處理,導致接收到的網路包無法及時處理。
因此可採取的解決方案:
(1)執行echo "2" > /proc/sys/vm/drop_caches ,強制作業系統回收inode和dentry中的快取資訊。這個命令執行期間會造成系統卡頓,卡頓時間為幾秒到幾分鐘,關鍵業務執行期間需慎用;
(2)週期性重啟伺服器,釋放slab中的快取資訊;
(3)停止週期性呼叫ss -lntp;
(4)對於ss命令,可以去掉 -p 引數,去掉這個引數後,ss將不再掃描/proc目錄下的程序資訊,不會導致slab中記憶體物件個數的增長。另外也可以升級到高版本的ss命令,在高版本的ss命令中已不再讀取 /proc/slabinfo,因為這裡面已沒有可以獲取的有效資訊。下面是github中的commit資訊。
圖10 GitHub中commit資訊
總結
針對這個問題的分析持續了大約一、兩個月的時間,期間也曾一度陷入迷惑而毫無進展,有初步分析思路後補充了作業系統核心的各種知識,主要包括程序排程、排程優先順序、中斷處理、網路協議棧處理、虛擬檔案系統等等,最終可以將整個問題的來龍去脈解釋清楚。
參考資料:
《如何除錯Kubernetes叢集中的網路延遲問題?》Theo Julienne 分散式實驗室
《網絡卡多佇列:RPS、RFS、RSS、FlowDirector(DPDK支援)》rtoax CSDN
https://rtoax.blog.csdn.net/article/details/108987658
https://github.com/shemminger/iproute2/commit/10f687736b8dd538fb5e2bdacf6bef2c690ee99d
- Spring中實現非同步呼叫的方式有哪些?
- 帶引數的全型別 Python 裝飾器
- 整理了幾個Python正則表示式,拿走就能用!
- 設計模式之狀態模式
- 如何實現資料庫讀一致性
- SOLID:開閉原則Go程式碼實戰
- React中如何引入CSS呢
- 慢查詢 MySQL 定位優化技巧,從10s優化到300ms
- 一個新視角:前端框架們都卷錯方向了?
- 編碼中的Adapter,不僅是一種設計模式,更是一種架構理念與解決方案
- 手寫程式語言-遞迴函式是如何實現的?
- 一文搞懂模糊匹配:定義、過程與技術
- 新來個阿里 P7,僅花 2 小時,做出一個多執行緒永動任務,看完直接跪了
- Puzzlescript,一種開發H5益智遊戲的引擎
- @Autowired和@Resource到底什麼區別,你明白了嗎?
- “四招”守護個人資訊保安
- CSS transition 小技巧!如何保留 hover 的狀態?
- React如此受歡迎離不開這4個主要原則
- 我是怎麼入行做風控的
- 重溫三十年前對於 NN 的批判:神經網路無法實現可解釋 AI