https://zhuanlan.zhihu.com/p/58669088
最近做了一些系統和網絡調優相關的測試,達到了期望的效果,有些感悟。同時,我也發現知乎上對Linux服務器低延遲技術的討論比較欠缺(滿嘴高並發現象);或者對現今cpu + 網卡的低延遲潛力認識不足(動輒FPGA現象),比如一篇知乎高贊的介紹FPGA的文章寫到“從延遲上講,網卡把數據包收到 CPU,CPU 再發給網卡,即使使用 DPDK 這樣高性能的數據包處理框架,延遲也有 4~5 微秒。更嚴重的問題是,通用 CPU 的延遲不夠穩定。例如當負載較高時,轉發延遲可能升到幾十微秒甚至更高”,剛好我前幾天做過類似的性能測試,發現一個tcp或udp的echo server可以把網卡到網卡的延遲穩定在1微秒以內,不會比FPGA方案慢很多吧?
因此,我覺得有必要分享下自己的見解。總的來說,我打算分兩篇文章討論相關低延遲技術:
1)系統調優(本文):一些低延遲相關的Linux系統設置,和一些原則。
2)網絡調優:使用solarflare網卡降低網絡IO延遲。
這里不打算介紹用戶空間的延遲優化,因為太廣泛了,另外我之前的文章也分享一些解決某類問題的低延遲類庫。
說到低延遲,關鍵點不在低,而在穩定,穩定即可預期,可掌控,其對於諸如高頻交易領域來說尤為重要。 而說到Linux的低延遲技術,一個不能不提的詞是"kernel bypass",也就是繞過內核,為什么呢?因為內核處理不僅慢而且延遲不穩定。可以把操作系統想象成一個龐大的框架,它和其他軟件框架並沒有什么本質的不同,只不過更加底層更加復雜而已。既然是框架,就要考慮到通用性,需要滿足各種對類型用戶的需求,有時你只需要20%的功能,卻只能take all。
因此我認為一個延遲要求很高(比如個位數微秒級延遲)的實時任務是不能觸碰內核的,(當然在程序的啟動初始化和停止階段沒有個要求,That's how linux works)。 這里的避免觸碰是一個比bypass更高的要求:不能以任何方式進入內核,不能直接或間接的執行系統調用(trap),不能出現page fault(exception),不能被中斷(interrupt)。trap和exception是主動進入內核的方式,可以在用戶程序中避免,這里不深入討論(比如在程序初始化階段分配好所有需要的內存並keep的物理內存中;讓其他非實時線程寫日志文件等)。本文的關鍵點在於避免關鍵線程被中斷,這是個比較難達到的要求,但是gain卻不小,因為它是延遲穩定的關鍵點。即使中斷發生時線程是空閑的,但重新回到用戶態后cpu緩存被污染了,下一次處理請求的延遲也會變得不穩定。
不幸的是Linux並沒有提供一個簡單的選項讓用戶完全關閉中斷,也不可能這么做(That's how linux works),我們只能想法設法避免讓關鍵任務收到中斷。我們知道,中斷是cpu core收到的,我們可以讓關鍵線程綁定在某個core上,然后避免各種中斷源(IRQ)向這個core發送中斷。綁定可以通過taskset
或 sched_setaffinity
實現,這里不贅述。 避免IRQ向某個core發中斷可以通過改寫/proc/irq/*/smp_affinity
來實現。例如整個系統有一塊cpu共8個核,我們想對core 4~7屏蔽中斷,只需把非屏蔽中斷的core(0 ~ 3)的mask "f"寫入smp_affinity文件。這個操作對硬件中斷(比如硬盤和網卡)都是有效的,但對軟中斷無效(比如local timer interrupt和work queue),對於work queue的屏蔽可以通過改寫/sys/devices/virtual/workqueue/*/cpumask
來實現,本例中還是寫入"f"。
那么剩下的主要就是local timer interrupt(LOC in /proc/interrupts)了。Linux的scheduler time slice是通過LOC實現的,如果我們讓線程獨占一個core,就不需要scheduler在這個core上切換線程了,這是可以做到的:通過isolcpus
系統啟動選項隔離一些核,讓它們只能被綁定的線程使用,同時,為了減少獨占線程收到的LOC頻率,我們還需要使用"adaptive-ticks"模式,這可以通過nohz_full
和rcu_nocbs
啟動選項實現。本例中需要在系統啟動選項加入isolcpus=4,5,6,7 nohz_full=4,5,6,7 rcu_nocbs=4,5,6,7
來使得4~7核變成adaptive-ticks。adaptive-ticks的效果是:如果core上的running task只有一個時,系統向其發送LOC的頻率會降低成每秒一次,內核文檔解釋了不能完全屏蔽LOC的原因:"Some process-handling operations still require the occasional scheduling-clock tick. These operations include calculating CPU load, maintaining sched average, computing CFS entity vruntime, computing avenrun, and carrying out load balancing. They are currently accommodated by scheduling-clock tick every second or so. On-going work will eliminate the need even for these infrequent scheduling-clock ticks."。
至此,通過修改系統設置,我們能夠把中斷頻率降低成每秒一次,這已經不錯了。如果想做的更完美些,讓關鍵線程長時間(比如幾個小時)不收到任何中斷,只能修改內核延長中斷的發送周期。不同kernel版本相關代碼有所差異,這里就不深入討論。不過大家可能會顧慮:這樣改變系統運行方式會不會導致什么問題呢?我的經驗是,這有可能會影響某些功能的正常運轉(如內核文檔提到的那些),但我尚未發現程序和系統發生任何異常,說明這項內核修改至少不會影響我需要的功能,我會繼續使用。
兩個原則:
1)如果一件事情可以被delay一段時間,那它往往能夠被delay的更久,因為它沒那么重要。
2)不要為不使用的東西付費,對於性能優化來說尤為如此。
如何檢測中斷屏蔽的效果呢?可以watch/proc/interrupts
文件的變化 。更好的方法是用簡單的測試程序來驗證延遲的穩定性:
#include <iostream> uint64_t now() { return __builtin_ia32_rdtsc(); } int main() { uint64_t last = now(); while (true) { uint64_t cur = now(); uint64_t diff = cur - last; if (diff > 300) { std::cout << "latency: " << diff << " cycles" << std::endl; cur = now(); } last = cur; } return 0; }
通過taskset綁定一個核運行程序,每進入一次內核會打印一條信息。
最后,除了進入內核以外,影響延遲穩定性的因素還有cache miss和tlb miss。
對於減少cache miss,一方面需要優化程序,minimize memory footprint,或者說減少一個操作訪問cache line的個數,一個緩存友好例子是一種能高速查找的自適應哈希表文章中的哈希表的實現方式。另一方面,可以通過分(lang)配(fei)硬件資源讓關鍵線程占有更多的緩存,比如系統有兩塊CPU,每塊8核,我們可以把第二塊CPU的所有核都隔離掉,然后把關鍵線程綁定到其中的部分核上,可能系統只有一兩個關鍵線程,但它們卻能擁有整塊CPU的L3 cache。
對於減少tlb miss,可以使用huge pages。