基於eBPF監控和排查雲原生環境中的磁碟IO效能問題

語言: CN / TW / HK

作者|沈濤

編輯|陳樂

供稿|ebay cloud team 

本文共5115字,預計閱讀時間13分鐘

更多幹貨請關注“eBay技術薈”公眾號

背景

問題始於eBay Rheos Streaming團隊的工程師發現他們的Kafka服務在有些時候follower追不上leader的資料。這種情況通常發生在某個kafka broker down或者partittion reassgin(例如系統定期檢查發現有超過兩個partittion部署在同一個rack上,需要在不同rack的broker上保留一個副本來維持系統可用性)的時候,此時kafka叢集中的其他broker需要將資料相互sync來保證每個partittion至少有三個副本。如圖所示,當第三臺node crash 或者需要partittion reassign時,第四臺node的broker或者原來down掉的broker重新排程到新的節點,為了保證三副本,follower需要從其他broker的partition leader中讀取資料,此時如果單個broker的leader很多,那麼read的壓力就會很大,與此同時,leader還需要向用戶提供寫請求,很多寫請求是需要保證資料sync到所有副本才會返回成功,因此follower追不上資料可能會直接影響應用。

問題分析

根據問題的描述,我們知道在發生問題的時候,kafka的讀寫行為發生了變化,在原本處於穩定的順序寫IO行為中,插入了大量非線性的讀操作。這些讀IO來自不同partittion,可能分佈在磁碟的任何sector上,這種亂序的插入有可能造成磁頭的大量偏移。為了優化定址操作,核心既不會簡單地按請求接收次序,也不會立即將其提交給磁碟。相反,它會在提交前,先執行合併與排序的預操作,這種預操作可以極大地提高系統的整體效能。在核心中負責提交IO請求的子系統稱為IO排程程式。IO排程程式將磁碟IO資源分配給系統中所有掛起的塊IO請求,這種資源分配是通過將請求佇列中掛起的請求進行合併和排序來完成的。它決定佇列中的請求排列順序以及在什麼時候派發請求到塊裝置。如果IO經過IO排程程式仍然沒有有效地優化排程順序和次數,就會加大磁頭移動的負擔,造成IO效能下降。因此,IO排程程式對磁碟尋道有著重要影響。

於是我們懷疑Kafka這種短期特殊讀寫行為,讓當前排程器對讀的排程效能下降,讀請求沒有得到有效的IO資源分配和排程。但遺憾的是,從傳統的iops,io throughput 和 io latency 等這些指標,我們很難觀測到Block層中排程器的排程效能差異。例如我們能夠看到讀的throughput不高,以及iops上不去,但卻並不知道這背後的原因。借於此前我們已經通過eBPF幫助使用者排查和解決了網路上的一些問題,我們再次想到了通過eBPF來解決磁碟IO的問題。如果我們可以觀測到每次排程層的IO的磁碟分佈,也許可以找到一些蛛絲馬跡。

這裡先補充一些eBPF的相關知識:

eBPF的觀測過程

eBPF在核心中實現了一個虛擬機器和一組使用者的指令集,使用者通過指令集分別編寫自己的核心態和使用者態ebpf程式,通過核心呼叫將核心態程式attach到某個核心事件上(如tracepoint),對事件的處理結果通過eBPF特定的資料結構map或者ringbuffer等傳遞到使用者態程式。eBPF程式成為雲原生基礎設施的一部分,部署在叢集和節點中,生成和彙報節點相關的指標資訊,最終彙總到監控和日誌相關的儲存中。這是eBPF在雲原生環境中的整體資料處理流程。

相比傳統的觀測方法,eBPF的診斷和觀測方式主要擁有以下兩個顯著優勢:

1.所有執行在Kubernetes中的Pod或容器內的一切行為都需要與核心互動,而eBPF能夠訂閱各種核心事件,如檔案讀寫,磁碟IO,網路傳輸等,能夠監控核心的各種行為,提供更為底層和詳細的觀測資料,甚至針對不同的應用程式,開發者可以編寫不同的eBPF程式抓取自身想要的資料,以此對應用效能調優和debug提供幫助。

2.eBPF的方式是無侵入式的,不需要修改和重啟使用者的應用程式,特別是在生產環境,不需要走繁瑣的釋出流程,高度儲存了事故現場,也不需要重新編譯和重啟核心,可以直接開箱即用地快速部署on-demand eBPF程式驗證想法,拿到第一手診斷資訊。

社群已經有不少eBPF開源專案,如bcc,ebpf-exporter,bpftrace,cilium等。在bcc中提供了不少bio相關的檢測工具,我們發現了叫biopattern的工具,可以展示disk IO pattern。通過找到一個出現問題的磁碟,執行biopattern結果如下:

能夠看到,biopattern可以返回單位時間中對磁碟的隨機IO和順序IO的比例以及總的讀寫數量。在上述結果中,顯然有大量的隨機IO進入,使得整體的IO效能遭到下降,在順序IO升高的情況下,讀寫的bytes高很多,但當隨機IO達到100%時,完成的讀寫bytes和counts都大幅下降。這說明,當前的biopattern使得磁碟整體的讀寫效能降低了。

但這個工具仍有 幾個不足:

1. 儘管知道隨機IO的比例,但依然不清楚磁碟的分佈情況;

2. 結果顯示不夠直觀,需要同時結合多個數據觀測;

3. biopattern是統計block_rq_complete tracepoint拿到的資料,缺少不同層次的比較。

於是我們改寫了biopattern的程式碼,通過計算前後進入tracepoint時的sector差值,再取2的對數統計到不同slot中,核心程式碼如下:

改進的思路在於,原始的biopattern很難比較兩個相同random百分比的情況下的效能差異,以及在random情況下的具體磁碟落點分佈。通過計算每一次請求完成時的磁頭偏移,得到磁碟整體的IO分佈,這更加接近IO pattern的定義,能根據結果看到更加詳細的IO變化。

為了讓資料更容易被收集和視覺化,我們藉助於ebpf_exporter工具。在編寫完ebpf程式之後,只需要編寫簡單的yaml檔案,自定義展示的資料型別,ebpf_exporter就能在叢集中不斷收集到節點的磁碟IO資料併發送到promethues:

如果理解了eBPF的基本原理,那麼整個eBPF程式開發和釋出過程都非常順滑流暢,真正做到了對應用和節點的無侵入,比編寫kernel module等其他觀測方式要輕量地多,展示出了eBPF強大的應用潛力。

在Grafana中我們將剛才通過ebpf_exporter收集到的資料通過熱力圖的方式展現出來:

在這個圖中,橫軸是時間,縱軸表示每一個slot,顏色越偏向紅色,表示某個時間段內落在這個slot的數量越多。顯然,slot的值越大,偏移量越大。

既然已經發現在問題發生的時候,使用者的IO行為發生了變化,為了驗證當前的IO排程程式是否能夠很好的處理這種狀況,我們給所在結點部署了新的biopattern並抓取了上述資料。Tess (eBay kubernetes 專案)線上叢集已經升級到了5.4的kernel,kafka叢集所在節點預設使用了mq-deadline[6]的IO排程器,官方推薦的hdd磁碟排程器為mq-deadline與bfq[4],兩者都對hdd磁碟效能有著不少優化,都是基於多核多佇列框架的IO排程器[2,3]。於是我們嘗試將排程器切換到bfq[7],看一看各項metrics的變化。

如上圖所示,在17:00 到17:50之間,我們切換到了bfq排程器,在17:50到18:10分,我們切換到了mq-deadline,隨後繼續切換到bfq,以此來縱向對比。可以看到在切換到bfq期間,儘管有一些IO落到了不同的slot,但大多數都集中在0,也即線性的sectors。但是在mq-deadline排程期間,0 slot的數量下降明顯,並且  4-8 和 24-28的slot數量也有所增加。相同的資料我們還可檢視p75的分佈圖:

在切換到mq-deadline期間,p75也有顯著上升,這說明在這期間,排程器沒有很好的將不同IO分配地足夠線性,這加大了磁碟的尋道負擔。從其他傳統效能指標可以看到此時IO讀的效能有所下降:

如前文所述,僅通過一個觀測點拿到的資料缺乏縱向的解剖能力,於是通過在kafka叢集執行blktrace, 並統計IO的執行過程,可以知道大量的IO都會經過以下幾個階段:

可見kafka的讀和寫都經過了Q-G-I-D-C的階段,當然有些IO還有其他階段,例如bio太大需要做切分的X階段等,但總體來講,block:block_bio_queue(Q: 將bio請求入隊) -> block:block_getrq (G: 拿到一個空的request) -> block:block_rq_insert(I: 將request插入佇列) -> block:block_rq_issue(D: request開始向裝置驅動發起請求) -> block:block_rq_complete(C: 裝置驅動完成IO請求) 的過程能夠代表絕大部分的IO過程。於是我們在block:block_rq_complete之外,加入了block:block_bio_queue和block:block_rq_insert 兩個觀測點,前者用來觀測從bio進入到Block層的初始狀態,後者用來觀察IO進入Scheduler層並插入request時的狀態。與此同時,我們再分別給出各自觀測點的IO數量和p75分佈來協助觀測,以此拿到整個IO過程較為完整的biopattern:

問題解決與後續

我們幫助使用者部署了上述觀測工具,並修改了所有kafka叢集的排程器,在運行了一段時間之後,使用者發現由讀效能帶來的問題大幅減少,follower可以較為快速的追上資料。並且由於觀測了不同階段的IO數量,我們還幫助使用者發現了另一種特殊case,kafka節點IO不夠但bio數量不足的問題,最後通過更改kafka讀的吞吐量解決。

儘管問題解決,但我們依然需要為客戶解釋為何bfq比mq-deadlone能夠更好應對大量隨機讀帶來的效能問題。於是我們翻看了bfq和mq-deadline各自的程式碼和文件,並模擬出類似的場景,還原比較了兩者之間的效能差異。以下是這部分的一些報告總結。

Mq-deadline[6]是deadline的多佇列架構版本,本身和deadline排程器相似,它分別提供了讀和寫的佇列,並設定了不同的過期時間:

static const int read_expire = HZ / 2;  /* max time before a read is submitted. */

static const int write_expire = 5 * HZ; /* ditto for writes, these limits are SOFT! */

它還為寫IO設定了最大飢餓數來保證bio寫不被餓死:

static const int writes_starved = 2;   /* max times reads can starve a write */

mq-deadline的策略非常簡單,它按照FIFO的順序派發IO,並通過紅黑樹快速找到合適的插入和合並的位置:

struct rb_root sort_list[2];

struct list_head fifo_list[2];

sort_list 用於排序和合並,fifo_list用於派發請求,而在派發之前,mq-deadline不但設定了過期時間,而且會優先讓read先派發,這也是為什麼要給write設定最大飢餓數:

所以mq-deadline就是一個具有merge和sort的fifo 佇列,並分別給read和write佇列都設定了不同的deadline以保證不被餓死,尤其對read做了一定的優化,優先讓read先排程。那既然如此,為何隨機讀在與順序寫的競爭過程中依然效能沒有bfq好呢?我們再看看bfq的實現。

bfq[7]來源於cfq,與此不同的是,cfq是按照時間片的公平排程策略,即給不同的佇列設定時間片來保證絕對的公平。但這種排程策略的效能會比較差,因為很顯然順序IO比隨機IO在相同時間內訪問的磁軌要高得多,所以順序IO可能在cfq中讀寫更多的磁碟。於是在bfq中,不再按照時間片分配排程,而是分配budget,這裡的budget通過sectors來計算,即給不同的佇列分配不同的sectors,噹噹前佇列消耗完分配的sectors,就切換到其他的佇列,而每個佇列又有自己的weight來決定排程的先後順序,如圖所示:

這種由基於時間到基於服務量的變化,讓bfq可以根據需要在佇列之間分配合適的吞吐量,在throughput波動或者磁碟裝置內部排隊的情況中不會失真。bfq使用名為B-WF2Q+[5]的中間排程器來給佇列分配budget和weight從而控制整個排程策略,程式碼位置在block/bfq-wf2q.c.

bfq比較複雜,簡單來說,bfq有以下一些規則:

  • 每一個正在磁碟上處理IO的計算單元(process)都有一個權重(weight)和佇列(queue)

  • 任何時候裝置只會有一個佇列獨佔訪問,訪問模型是根據sectors計算出的budget. 每一次請求的派發(dispatch)都會讓budget減小。佇列只會在三種情況下讓出排程資源:1)佇列用完budget, 2) 佇列為空,3)佇列budget超時。budget超時是為了防止部分緩慢的隨機IO長期佔用磁碟引起整體吞吐量大幅下降。

  • 當同時有多個process競爭並且具備相同的weight,bfq會選擇throughput更高的process排程。它根據這些process在曾經執行的過程中磁碟的空閒程度來判斷。

  • bfq預設開啟低延時模式(在/sys/block//queue/iosched/low_latency中可以配置), 這時bfq會啟發式的檢測互動式和軟實時應用,從而減小他們的延時。最簡單的減小延時方法便是提高他們的weight使得優先被排程。檢測的程式碼實現在bfq_bfqq_softrt_next_start函式中。

  • bfq使用EQM(Early Queue Merge) 機制來進行佇列的合併和插入,從而能夠讓隨機IO更加線性,提高吞吐。

以上只是列出了bfq的大體規則和框架,實際有更多細節,此處不再贅述,相關程式碼和文件位置在block/bfq-iosched.c/和Documentation/block/bfq-iosched.rst,可以自行查閱。

比較bfq和mq-deadline的各自實現,儘管mq-deadline在競爭的過程中給於read更高的優先順序,並且過期的時間也比write更短,但mq-deadline依然是基於時間在分配讀與寫,並且也沒有基於random IO做更多的優化,本質上是以總體吞吐量為第一位來設計的排程器。而bfq則是基於服務量來分配,使得隨機IO可以獲得更加公平的排程機會,而且bfq對隨機IO和軟實時應用有特殊的優化,從設計理念上,bfq就犧牲了部分效能,以求在吞吐量和響應速度之間找到更好的平衡。

為了比較實際的執行情況,我們在開發環境中模擬了使用者的case,分別啟動了一個順序寫的Buffer IO和一個隨機讀的Direct IO,以此比較兩種排程器的讀寫情況,可以發現bfq犧牲了順序寫的效能,以此來滿足更多隨機讀的需求:

作為對照組,在沒有競爭,單獨進行順序寫和隨機讀的情況下,兩者的差距並不大,mq-deadline的順序效能要更高於bfq:

由此可以理解為,在kafka的特殊case中,一方面,bfq能夠比mq-deadline提供給follower讀取leader資料更多的IO,另一方面,bfq能夠對更為隨機的IO提供更好的排序和合並策略,使得每一次的IO更為線性。但我們依然要注意,bfq的純順序的情況中,效能是不如mq-deadline,並且在競爭的過程中也會犧牲大量的順序效能,這需要使用者根據自己的應用情況進行權衡和取捨。畢竟,沒有絕對完美的演算法,演算法都是tradeoff的藝術。

最後感謝Tess團隊CY的技術指導以及Rheos團隊Wang Yu的積極配合,共同解決這個問題。這是我們首次嘗試利用eBPF解決kubernetes叢集磁碟IO相關的問題,希望是一個好的開始,給使用者和業界提供一些新的思路。

參考文獻

1.https://ebpf.io/what-is-ebpf

2.https://kernel.dk/systor13-final18.pdf

3.https://lwn.net/Articles/552904/

4.https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/monitoring_and_managing_system_status_and_performance/setting-the-disk-scheduler_monitoring-and-managing-system-status-and-performance

5.Jon C.R. Bennett and H. Zhang, "Hierarchical Packet Fair Queueing Algorithms", IEEE/ACM Transactions on Networking, 5(5):675-689, Oct 1997.

6.https://www.kernel.org/doc/Documentation/block/deadline-iosched.txt

7.https://www.kernel.org/doc/html/latest/block/bfq-iosched.html

往期推薦

eBay大資料安全合規系列 - EB級叢集升級挑戰和實踐

eBay大資料安全合規系列 - 系統篇

Elasticsearch叢集容量的自適應管理

點選 “閱讀原文” ,  一鍵投遞

eBay大量優質職位虛席以待

我們的身邊,還缺一個你