計算機科學的先驅Donald Knuth(高德納)曾經說過:“過早的優化是萬惡之源”,更詳細的原文如下:“We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%. A good programmer will not be lulled into complacency by such reasoning, he will be wise to look carefully at the critical code; but only after that code has been identified.”它向我們揭示了一個道理,我們應該首先定位到那3%真正成為瓶頸的代碼,而忽略97%那些“small efficiencies”,所謂“將軍趕路,不打小鬼”,這是我們進行一切性能優化的前提。因此,剖析(profiling),成為了性能優化中最重要的環節之一。
性能剖析,要求我們的思維方式主要是top-down的,我們能全局地從頂部向下的看問題,這就像一個全科醫生,出了問題后,能大致估摸出一個方向知道是哪個器官可能出了問題。但是,我們同時也必須具備down-top的能力,正如一個專科醫生,能看到細小器官的癌細胞最終會怎樣向全身發散,從而危機到人的健康和生命。
系統的性能優化不太是一個通過review幾千萬行代碼,發現問題,然后更正問題優化的過程。而更多是一個通過某些剖析手段,把系統當成黑盒子,暴露數據,top-down地看這個系統,在發掘問題后,再深入到白盒down-top的過程。《魏略》曰:“亮在荊州,以建安初與潁川石廣元、徐元直、汝南孟公威等俱游學,三人務於精熟,而亮獨觀其大略。”性能優化本身,是一個從統帥諸葛亮逐步變身單兵戰神呂布的過程,而你首先必須是統帥。
一、性能剖析的總體認識
下面我們也來一個“觀其大略”的環節,先“不求甚解”地看一看內核性能分析關注的指標,分析角度和一些其他基本常識。
吞吐
吞吐強調單位時間里可以做多少有用功。比如,我們會用netperf來評估網絡的帶寬;用sysbench來評估MySQL的QPS(Queries Per Second)和TPS(Transactions Per Second);用vm-scalability來評估Linux內存管理的吞吐性能等;用tbench來評估內核調度器wake-up路徑上的優化是否有效等。
為了提高吞吐,我們常常采用的一個方法是橫向拓展硬件或者軟件的規模,比如增加更多的CPU、使用多線程等。然而世間萬物,不如意者十之八九,不是你愛她多一點,她就一定會愛上你。吞吐的拓展受限于著名的Amdahl's law和Universal Scalability law(USL)。
按照USL,核多(核數為p),實際的加速倍數是:
p增大的時候,不僅僅是分子增大,分母也增大,分母σ因子隨著p線性增大,k因子隨著p的平方線性增大。USL的分母中去掉+k*p*(p-1)就是Amdahl's law,所以Amdahl's law并沒有USL完整準確。
其中的σ系數是contention(核間、多線程間因為競爭等鎖、同步等而不能并行執行),k系數是coherency(核間、多線程之間的協同形成共識的開銷)。σ系數比較好理解,比如兩個CPU訪問同一個鏈表,他們需要競爭鎖,假設平均1秒里面0.1秒在等鎖,則2個cpu實際只有2*0.9=1.8秒在做有用功,而不是2.0。k系數相對難理解一點,比如我們在CPU0釋放一個spinlock,在ticket spinlock里面,這個spinlock的新值要通過cache同步網絡同步給系統的每個CPU形成所有CPU對這個新值的一致性理解,這個cache同步的開銷很大,而且隨著p的平方而增大。這就是為什么內核針對spinlock不斷在進行優化,比如從ticket spinlock變成qspinlock,其實是減小了需要coherency的CPU個數。
舉一個栗子,軟件的童鞋很可能會天真地以為內核的atomic_inc()、TLB flush之類的操作是非常便宜的,其實它們都有嚴重的k因子問題,就是coherency開銷。如果做一個簡單的操作:
lcase A: 100核,100個線程同時做atomic_inc(),做一秒鐘;
lcase B: 10核,10個線程同時做atomic_inc(),做一秒鐘。
case A原子操作的次數不會是case B的10倍,它實際遠小于10倍,比如實測結果可能是嚇死你的4倍(當然每個具體的SoC都可能不一樣),等于10倍的硬件目前這個世界還沒有做出來,未來也造不出來。
這些σ系數、k系數對服務器的影響,遠大于對桌面和手機等系統,因為服務器上的p特別大。所以,長期困擾服務器的問題,比如我從50個cpu變成100個cpu,MySQL的吞吐一定增加了一倍了嗎?不好意思,很可能只是嚇死你的1.3倍,如果運氣不好的話,還可能倒退。
再回到spinlock,大量的文獻顯示,由于ticket spinlock等在核間coherency上的巨大開銷,許多業務的性能可隨著CPU核數量的增大而減小【1】。
最開始核增加的時候,相關業務的性能在提升,到某個拐點后,再增加更多的CPU,性能不升反降,出現了collapse。
延遲
甲骨文的“山”,是一個象形字,它較好地貼合了Linux世界里的延遲模型。Linux世界的延遲往往呈現為這種multi-modal(有多個峰值而不是只分布在一個峰值周圍)或者兩極分化特性。
由于這種multi-modal分布的存在,這個時候,我們描述平均值的意義其實不是特別大。比如一個班上有30個學生,其中10個人90分以上(學霸),還有10個人50分以下(學渣),另外還有10個在50-90分之間。我們說這個班的學生平均分60分,其實沒有任何意義,拉馬老師來和我們平均,沒意思的。這個時候,我們需要直方圖來描述這種分布,從而更加直觀地看出來這個班的學霸和學渣比率。
如果我們想看一個真實的Linux例子,下面是我的PC在運行“sudo cat /dev/nvme0n1 > /dev/null”的過程中,我看到的BIO(block I/O)的延遲分布:
我們看到了2個峰值,一個峰值在64us-127us之間;另外一個峰值,則圍繞著1024-2047us分布。當然,還有一個值會偏移地特別遠,比如有2個采樣點,落在了131072us-262143us之間。那2個離群很遠的值,我們一般也稱呼它們為outlier,它們是夜空中最閃亮的星,天生不是凡人。
種種跡象表明,我們僅關注平均值的意義非常有限。對于偏移中線的部分,在延遲分析領域,我們還特別關注一個非常重要的概念,tail latency,中文可譯為尾延遲。比如我們說,90%的延遲落在1ms以內,99%的延遲在10ms以內,但是還有1%的延遲可能更大,甚至形成一個很長很長的尾巴,可能有的達到了1秒也說不定。在延遲分析領域,我們很可能關注這些尾部,比如大家一起競爭mutex,那些延遲很大的case,可能會形成手機系統的卡頓,因此丟幀。對于服務器、電商、云服務等領域而言,高的尾延遲,會直接影響到企業的revenue。
延遲的幾個主要可能的來源:
1.進程實際可以運行(TASK_RUNNING),但是由于調度延遲的原因搶不到CPU;
2.進程同步等待一個I/O動作的完成,這些I/O動作可能是syscall的read/write,也可能是mmap內存page fault后的I/O;
3.系統內存吃緊,進程陷入direct memory reclaim,直接回收內存;
4.進程等其他進程釋放鎖,這里又分2種可能性
a.等鎖隊列比較長,比如等mutex、spinlock,前面已經掛了一個連在等,等到自己的時候,心已經碎了;
b.等鎖隊列可能不長,但是持有鎖的進程遭遇了情況1、情況2和3,導致長期不放鎖。類似你在等廁位,他卻坐馬桶上看了場電影。
所有的上述不確定情況,都可能形成不確定的tail latency。我們需要某些手段把它剖析和呈現出來。
功耗
內核有cpufreq, cpuidle,意識到功耗的調度器等。這些都致力于在降低功耗的情況下,總體不降低性能。除這些以外,我們也應該認識到,降低內核本身的CPU利用率,比如內存compaction、內存swap/reclaim、鎖自旋等的開銷,也能進一步降低功耗。在一個內存受限的系統中,我們不能低估內核本身的開銷所引起的功耗增加。
比如,我在 qemu上ARM64 Linux-5.19-rc2內核,然后運行下面簡單的程序:
這個程序申請了1GB內存,然后fork出來64個進程,其中最原始那個父進程不停讀寫這個1GB的內存。代碼gcc編譯結果a.out。系統的內存是900M,并開啟了zRAM交換功能。運行起來后,我們看它的CPU消耗,a.out固然是很大,可是kswapd0這個內核線程也是非常大的CPU占用。
kswapd0相對我們的有用功a.out,只是輔助的工作,本質屬于浪費電。此外,我們的a.out本身占用的接近100%的CPU,也主要耗費在a.out自身的動作上嗎?這個時候我們也有興趣看一下,我們“perf top -p
所以從性能分析的角度來說,我們也要把這些紅框部分挖掘出來進行分析優化,因為本質他們也是純耗電,屬于overhead而不是real work。
90分到100分特別難
在所有的性能優化領域,我們都不得不正視一點,無論你多么地不愿意:就是前期的優化是相對比較容易的,越到后來越難。一個考10分的學渣,也許經過努力比較容易考到60分,再繼續挑燈夜戰,可能也能考到90,但是哪怕本著“只要學不死,就往死里學”的極限熱情,他也不一定能從90分考到100分,當然它可能考到91分。
努力到一定程度后,有沒有可能越努力成績越差呢?我覺得是可能的,因為Universal Scalability law(USL),學麻了容易走火入魔,反而出現性能的collapse。
成年人最重要的心理素質是學會和自己的平凡和解,打牌輸了不要賴著不走再打一局不如隔天換個場子打。性能優化也是一樣的,在某個角度已經搞到了91分,這個時候繼續鉆牛角尖的代價可能就比較大了。也許我們可以換另外一個角度,來做個從30分到91分的過程,最終實現總成績91+91=182分,而不是100+30=130分。
在總成績182的情況下,再去追求總成績200,心態和效率都高很多。
off-cpu和on-cpu分析同樣重要
當我們分析代碼在CPU的耗時花費在哪里的時候,我們關心系統的on-cpu profiling,但是,當我們關注延遲等問題的時候,我們不僅要關注on-cpu profiling,更多的時候,我們需要關注off-cpu profiling。off-cpu profiling的目的在于全面地評估進程不在CPU上面跑的時候,因為什么原因被調度出去。off-cpu profiling完整地打印出進程離開CPU的原因的調用棧和時間分布,比如off-cpu發生在等鎖(如mutex和rwsem)、等I/O完成、被調度搶占等情況。
性能profiling應同時著眼于on-cpu和off-cpu這兩種情況。這個和優化我們的工作效率是一樣的,我們既要上班時候盡可能降低CPU消耗,on-cpu的時候少做純耗電的無用功;也要看看是什么原因引起我們上班的時候釣魚劃水,把引起我們off-cpu的原因分析出來從而減少摸魚。
二、on-cpu分析
on-cpu分析主要著眼于2個點:一是找到占用CPU大的熱點代碼;二是提高單位運行時間內,CPU執行有效指令的條數。前一個點重心在于降低CPU利用率,后一個點則著重于提高CPU的工作效率。下面層層展開,展開過程中會直接介紹相關的工具。
on-cpu火焰圖
我們看到前述代碼中,kswapd0占據了57.7%的CPU。所以我們現在特別感興趣,它的CPU的具體走向,火焰圖可進行一個比較完整的呈現。假設kswapd0的PID是54,下面我們抓取內核線程54的信息:
我們把采樣到的數據,通過火焰圖工具進行繪制:
我們得到如下火焰圖:
火焰圖上我們發現,kswapd的時間有小部分發生在swap_writepage向zRAM寫被替換的頁面,而大部分發生在判斷頁面是否被訪問的folio_reference_one(),以及頁面向zRAM寫之前unmap這個頁面后的ptep_clear_flush()這2個動作上面。
你也許會想到要去減少folio_reference_one()和ptep_clear_flush()上面的開銷,這是一個剖析和發現的過程。
CPU消耗比例分布
火焰圖固然呈現了相關函數的CPU比例,但是,很多時候我們生成報告,尤其是向社區發patch,我們需要發送文字版的優化報告,這些報告可以突出CPU熱點在哪里。這個時候,我們可以用perf report的功能。
啥也不說了,盯著perf report里面排名第1,第2的整起來。相關的這種數據報告在Linux社區非常常見,比如MGLRU的patch 【2】里面就列出了MGLRU中某一特性優化前后的CPU消耗對比數據:
我們看到屬于overhead的page_vma_mapped_walk()的減小,但是屬于lzo1x_1_do_compress()的real work的增大。所以,我們看數據說話,沒有數據支撐的性能優化,是很難形成任何說服力的。這也就是為什么我們在內核社區發性能優化的patch,必然會被要求大量benchmark數據支撐。
最后一公里
前面我們發現try_to_unmap()后調用的ptep_clear_flush()是熱點函數,但是它熱在哪里,具體熱在哪一行代碼呢?這個時候,我們需要進一步“perf annotate ptep_clear_flush”。
從annotate的結果可以看出,至少,在筆者運行的qemu平臺上,tlbi附近的開銷是非常大的,其他的代碼開銷幾乎可以忽略不計。當然,真實的ARM64硬件上,tlbi也絕對不便宜。
中斷屏蔽的問題
在一個類似如下spin_lock_irqsave()、spin_unlock_irqrestore()的區間里,由于采樣的中斷都是被屏蔽的,所以中間perf的采樣結果,都會在spin_unlock_irqrestore()開啟中斷的這一刻爆出來,導致ARM64平臺下,采樣的熱點落在類似spin_unlock_irqrestore()這樣的地方。這是不準確的。
我們在調試時,可以使能ARM64_PSEUDO_NMI選項,并傳遞irqchip.gicv3_pseudo_nmi=1這樣的bootargs參數。這樣,內核會用GIC的高優先級中斷模擬NMI,在local_irq_disable()、spin_lock_irqsave()、spin_lock_irq()這樣的API里面只是屏蔽低優先級中斷。
舉一個典型的栗子,ARM64 IOMMU(SMMU)的map/unmap開銷大,導致dma_map_single/sg, dma_unmap_single/sg這些APIs在開啟IOMMU的情況下,吞吐率不高。這個時候,我們通過前面的火焰圖、CPU利用率分布報告很可能已經抓到了熱點在drivers/iommu/arm/arm-smmu-v3/arm-smmu-v3.c【3】的arm_smmu_cmdq_issue_cmdlist()這個函數,但是這個函數長成這個樣子,進去的時候就關中斷,出來的時候才開中斷:
這個時候,你不開啟irqchip.gicv3_pseudo_nmi=1是不可能perf annonate出來這個函數哪句話是熱點的,所有熱點都會落在末尾的local_irq_restore()這句話。但是開啟后,你才會抓到真正的熱點:
CPU執行效率topdown分析
CPU利用率相等的情況下,執行效率是不是一樣的呢?比如都是一秒里面干0.5秒的活,CPU利用率50%,干出來的活是一樣多嗎?不是的,現代計算機系統普遍采用多發射、流水線、分支預測、預取、亂序投機執行等各種復雜機制,這使得同樣時間段內能實際執行的有效指令條數,會是可變的。有兩個非常重要的概念值得所有人關注:
CPI:(Cycles Per Instruction)每個指令要多少周期。
IPC:(Instructions Per Cycle)每個周期執行多少指令。
CPI和IPC為反比例關系。在同樣CPU利用率的情況下,我們追求盡可能高的IPC。比如1個代碼跑起來IPC是2.0,另外一個1.0,那么意味著同樣的時間段,前者執行的指令是后者的2倍,CPU執行指令更順暢,stalled(被添堵、處理器空轉)的環節更少。
比如我在我的PC上面運行ls,可以看到IPC是0.9:
比如運行gcc main.c,可以看到IPC是1.36:
Ahmand Yasin在它的IEEE論文《A top-down method for performance analysis and counter architercture》中,革命性地給出了一個從CPU指令執行的順暢程度來評估和發現瓶頸的方法,允許我們從黑盒的角度以諸葛孔明“隆中對”式的格局來看問題。
現在處理器,一般在4個方面占用流水線的時間,而top-down方法,可以黑盒地呈現軟件在CPU上面運轉的時候,CPU的流水線究竟在干什么。
Front End Bound(前端依賴): 處理器的前端主要完成指令的譯碼,把獲取的指令翻譯為一系列的micro-ops(μops)。當CPU stalled在Front End,通常意味著CPU在取指慢(比如icache miss、解釋執行等),或者復雜指令的翻譯過程由于μops cache不命中等原因而變地漫長。Front End stalled多,意味著前端無法及時給后端“喂飽”μops。目前主流的x86處理器,每個cycle可以給Back End喂4個指令,如果Back End也及時執行的話,IPC最高可達4.0。
Back End Bound(后端依賴):處理器的后端主要完成前端“喂”過來的μops的執行,執行的過程可能涉及讀寫操作數(load/store)、對操作數進行加減乘除各種運算之類。Back End Bound又可再細分為2類,core bound意味著軟件更多依賴于微指令的處理能力;memory bound意味著軟件更加依賴CPU L1~L3緩存和DRAM內存性能。當CPU stalled在Back End,通常意味著復雜運算指令延遲大,或操作數從memory(包括cache和DDR)獲取的延遲大,導致部分pipeline slots為空(stall)。
Retiring:μops被執行完成,最終的retire動作,提交結果到寄存器或者內存。
Bad Speculation:處理器雖然在干活,但是投機執行的指令可能沒有用。比如分支本身應該進“else”的,預測的結果卻進了“if”執行錯誤的分支,雖然沒有stall,但是這些錯誤分支里面的指令實際白白執行了不會retire,所以也浪費了pineline的時間。這里有一個基本常識,比如“if(a) do x; else do y;”,處理器并不是等待a的判決結果后,再去做x或者y,而是先根據歷史情況投機執行x或者y,當然這個投機有可能出錯。
注意Front End Bound、Back End Bound和我們平時說的軟件是CPU bound還是I/O Bound是相似概念,比如CPU bound的軟件更加依賴計算能力并對CPU性能敏感(比如編譯Linux內核、編譯Android),I/O bound的軟件更加依賴于I/O動作本身并對I/O性能敏感(比如你把硬盤dd if=/dev/sda1 of=xxx到xxx地方去)。
我們把CPU pipeline上的任何一個硬件資源想象成一個pipeline slots,假設CPU可以同時處理4條μops,下圖共有40個slots,如果所有slots都做有用功,μops都能retiring,則IPC為4.0:
假設其中的20個slots要么是empty沒活干(stalled),或者做的是無用功(分支預測錯誤等情況),那么它的IPC可能就是2.0了。
Intel處理器架構下,已經將top-down完全地工具化,有專門的top-down工具,有的甚至已經圖形化了,比如VTune Profiler里面就有類似功能【4】,可以具體化到每一個特定函數的Front-end Bound、Back-End Bound、Bad Speculation、Retiring等的情況:
大家從上表可以看出,基本retiring的比例越低,證明pipeline slots的stall越大,CPI也就越高(IPC越低)。比如,refresh_potential()的CPI高達3.589,主要是它的Back-End Bound嚴重,其中Memory Bound高達73.2%,所以優化這個函數,要多從cache命中率(L1,L2,L3)、DDR帶寬/延遲角度考慮。而優化sort_basket()函數則要多從分支預測優化角度考慮,因為它的Bad Speculation高達50.4%。
有的童鞋對Bad Speculation可能還是不太明白,下面我們通過一個代碼栗子:
這個里面有個隨機數r = rand(),會極大地破壞分支預測的準確性,所以topdown的結果如下:
如果我們把里面的rand()函數變成自己的版本,讓分支結果并不那么隨機:
再次topdown,結果則是(retiring很大,綠色友好程序):
此時的IPC也很大,達到3.49,比較接近4.0了:
Intel方面,腳本化的工具則有pmu-tools【5】。比如筆者在自己的X86 PC(內存24GB)上面開啟zRAM后運行如下代碼:
假設kswapd的PID是202,我們捕獲以下kswapd的bound情況:
由此我們可以看出,在上述場景下,kswapd跑起來主要是一個Back End Bound,其中Back End Bound里面的memory和core bound各占25.1%和19.3%,至于memory bound的部分,它又可以細分到L1,L2,L3 cache更深層次的原因。
我們現在覺得memory bound是上述場景下kswapd的大頭,我們實際也可以采樣下kswapd的cache-misses。簡單運行“sudo perf top -e cache-misses -p 202”命令看一下,cache misses率最高的是LZ4_compress_fast_extState()、memset_erms()和isolate_lru_pages()。你也許可以怎么去優化這些函數,從而減小它們的Back-End bound的部分,提高kswapd的IPC。
Intel的平臺享受上面的工具化優勢。其他平臺的童鞋也不必懊惱,perf本身也具有topdown能力,perf stat后面有個參數是--topdown:
查看Intel童鞋的這個patch【6】,最新版本的perf實際也可以支持td-level=2這樣更細粒度的topdown打印:
整個on-cpu分析的過程是top-down的,過程中的某些步驟也是topdown的,我們用一個流程圖來描述:
三、off-cpu分析
off-cpu分析更多關注延遲問題,所以我們首先要獲知延遲的分布,這個時候我們最好使用直方圖。之后,我們可以過度到用off-cpu火焰圖等進一步分析off-cpu在等什么,而在lock contention的場合,則可以使用perf lock來進一步進行鎖的分析。
直方圖
直方圖深入人心,哪怕什么工具都沒有,純粹地用Linux內核也可以劃出直方圖。這個功能位于菜單tracer->Histogram triggers,通過內核tracepoints實現。
下面我們隨便舉個例子,看看mm/vmscan.c中shrink_inactive_list()一般回收page個數的分布。注意,這純粹是一個栗子,不對應任何的實際工程。
在直方圖中,我們關心2個點,一個是key,一個是value。比如我們以回收頁面的個數為key,回收到這一key值頁面個數的次數為value。
我們在include/trace/events/vmscan.h和mm/vmscan.c中增加這個trace:
trace_mm_vmscan_nr_reclaimed(nr_reclaimed, 1);這句話表示回收nr_reclaimed個頁面的次數增加1。
現在我們使能tracer,以nr_reclaimed為key, times為value,按照times降序排列:
運行系統,觸發一些內存回收動作,采樣到一些值后,直接讀取直方圖:
從直方圖可看出,實驗場景中,回收到32個pages的機會最多,占據76598次,其次是回收到1個、0個和2個的。
下面我們把所有采樣復原,改為依據nr_reclaimed升序顯示:
復原:
開啟新的采樣:
運行系統,觸發一些內存回收動作,采樣到一些值后,直接讀取直方圖:
有的童鞋說,我現在關注的是延遲時間的分布,不是nr_reclaimed。這完全不影響我們的原理,比如latency單位是us,我想搜集0-5ms, 5-10ms, 10-15ms, 15-20ms各個檔次的分布,我只需要trace:
trace_xxx_sys_yyy_latency(latency/5000, 1);
后面在hist中,0-5ms會是一行,5-10ms會是一行,依此類推。另外,內核里面統計延遲可用類似的代碼邏輯(來源于kernel/dma/map_benchmark.c的map_benchmark_thread函數):
eBPF/BCC中本身內嵌多個直方圖工具,可滿足許多常見的生活必須,比如之前演示的biolatency,還有這些已經自帶:
bitehist.py: Block I/O size histogram.
argdist: Display function parameter values as a histogram or frequency count.
bitesize: Show per process I/O size histogram.
btrfsdist: Summarize btrfs operation latency distribution as a histogram.
cpudist: Summarize on- and off-CPU time per task as a histogram.
dbstat: Summarize MySQL/PostgreSQL query latency as a histogram.
ext4dist: Summarize ext4 operation latency distribution as a histogram.
funcinterval: Time interval between the same function as a histogram.
runqlat: Run queue (scheduler) latency as a histogram.
runqlen: Run queue length as a histogram.
xfsdist: Summarize XFS operation latency distribution as a histogram.
zfsdist: Summarize ZFS operation latency distribution as a histogram.
funclatency這個筆者也經常用,比如看一下vfs_read()這個函數的執行時間分布,只需要把函數名加在funclatency之后就好:
我們看到vfs_read()在實驗場景,一般延遲是8-16us之間占據第一名。但是偶爾也能大到驚人的4294967296ns,也就是4.2秒,從直方圖最后一行可以看出,這些可能就是outlier了。
eBPF/BCC可以依附于kprobe、tracepoints上,在eBPF/BCC上定制直方圖,只用寫非常簡單的腳本即可,因為直方圖是其本身內嵌的功能。比如在patch【7】中,筆者想知道kernel/sched/fair.c的select_idle_cpu()在進程被喚醒的時候,統計選中與target同cluster idle CPU,不同cluster idle CPU和沒找到idle cpu的比例,只需要寫一個簡單的腳本:
這個腳本的關鍵是涂了紅色的三行,定義了一個直方圖對象dist,然后就在里面通過dist.increment(e)增加采樣點,最后通過b["dist"].print_linear_hist("idle")把直方圖畫出來:
eBPF/BCC里面有許多直方圖的例子,我們定制自己的直方圖的時候,依葫蘆畫瓢就好。
off-cpu火焰圖
實際上,eBPF/BCC的代碼倉庫已經寫好了off-cpu工具,可以直接拿來用:
https://github.com/iovisor/bcc/blob/master/tools/offcputime.py
它的原理是抓捕內核進行進程上下文切換的時間和backtrace,比如可以對finish_task_switch()內核函數施加探針。因為我們在這個點可以知道什么進程被切換走了,什么進程被切換回來的,結合這些點的backtrace搜集,我們就可以得到睡眠和喚醒的調用棧,以及時間差。可以在這個函數加tracepoint,但是沒有tracepoint的情況下,我們也可以直接attach kprobe探針。
finish_task_switch()的函數原型是:
可見參數prev是被切換走的進程,而我們通過current可以拿到當前的進程,也即我們切入的進程。通過下面的算法,即可求出整個off-cpu區間的時間和backtrace:
通過在內核finish_task_switch()的kprobe點上插入eBPF/BCC代碼來完成這個算法,這樣不需要修改和重新編譯內核。之后,我們可以把eBPF/BCC捕獲的數據,借助flamegraph工具,繪制出off-cpu火焰圖。
下面給一個非常簡單的案例,在我的Ubuntu x86 PC上面運行以下代碼,創建32個進程,每個進程申請1G內存,然后循環執行:16字節對齊的時候寫入一個字節,1MB對齊的時候睡眠30us。
測試內核是5.11.0-49-generic,內核未開啟搶占,但是允許PREEMPT_VOLUNTARY:
# CONFIG_PREEMPT_NONE is not set
CONFIG_PREEMPT_VOLUNTARY=y
# CONFIG_PREEMPT is not set
測試的環境開啟了zRAM的swap,但是關閉了磁盤相關的swap:
捕獲a.out 30秒的off-cpu數據,并繪制火焰圖。
得到如下火焰圖:
從以上火焰圖可以看出,a.out的延遲主要是3個方面原因:
發生在其用戶態代碼本身調用的nanosleep()上;
發生在page_fault的處理上;
時間片到期后, timer中斷進來,把它切換走。
我們把這3個塊分別畫3個圓圈:
火焰圖的縱向是backtrace,橫向是每一種情況的off-cpu時間,橫向越寬代表這個調用stack上的off-cpu時間越久。
假設我們shrink_inactive_list()這個函數特別感興趣,則可點擊shrink_inactive_list()這個函數,單獨查看這個函數的off-cpu細節,我們發現,它其中一半的off-cpu是因為它自己調用了一個msleep(),還有很大一部分發上在它主動call了_cond_resched(),然后CPU被別人搶走;如果我們關注mutex_lock() 的延遲,則顯然發生在shrink_inactive_list() -> shrink_page_list() -> add_to_swap -> get_swap_pages() -> mutex_lock()這個路徑上。
如果我們把焦點移到kswapd,我們還是運行上面的a.out代碼,但是我們捕獲和分析的對象換為kswapd。捕獲kswapd的off-cpu數據30秒并繪制off-cpu火焰圖。
繪制出來的kswapd的off-cpu火焰圖如下:
可見多數延遲發生在kswapd各種路徑下(比如shrink_inactive_list -> shrink_page_list路徑)主動調用_cond_resched()出讓CPU,還有一部分延遲發生在最右邊的shrink_slab的i915顯卡驅動的slab shrink,點擊放大它:
從上圖可以看出,我們在內存回收shrink_slab()的時候,被i915驅動的i915_gem_shrink()堵住了,而i915_gem_shrink()被一個mutex_lock_interruptible()堵住了,所以i915驅動持有的一個mutex實際上給shrink_slab()是添堵了。
特別有意思的是,這個off-cpu火焰圖,還可以變成雙層的off-wake火焰圖。比如A進程等一個鎖睡眠了,B進程是持有鎖的人,B喚醒了A,在off-wake火焰圖上,它會以雙層調用棧的形式進行展示。比如在a.out引起匿名頁頻繁swap的情況下,抓一下kswapd的off-wake火焰圖:
得到的圖如下:
從圖上可以完整看出,kswapd off-cpu的原因和喚醒者。比如畫紅圈的區域,kswapd因為調用kswapd_try_to_sleep()而主動進入睡眠,a.out在swap in的過程中do_swap_page()因而需要alloc_pages()的時候因為申請內存的壓力,喚醒了kswapd內核線程。天藍色的是kswapd,淡藍色的是喚醒者。喚醒者的調用棧是從上到下,off-cpu的kswapd的調用棧是從下到上,中間通過灰色隔離帶隔開。這種描述方式,確實看起來比較驚艷有木有?
Lock Contention分析
在使能內核CONFIG_LOCKDEP 和CONFIG_LOCK_STAT 選項的情況下,我們可以通過perf lock來進行lock的contention分析。其實perf lock主要是利用了內核一系列的鎖的tracepoints,比如trace_lock_acquired(lock, ip)、trace_lock_acquired(lock, ip)、trace_lock_release(lock, ip)等。
在我運行一個匿名頁頻繁swap out/in的系統里,抓取lock情況:
然后生成報告:
看起來&rq->__lock、ptlock_ptr(page)、&lruvec->lru_lock的競爭比較激烈。尤其是&lruvec->lru_lock,由于contention比較多,total wait時間比較大。這里要特別留意一點,lock contention不一定是off-cpu的,可能也有on-cpu的,對于mutex, rwsem更多是off-cpu的;spinlock,則更多是on-cpu的。
對于off-cpu以及相關的延遲問題,我們需要通過直方圖獲知延遲分布、off-cpu/off-wakeup火焰圖獲知off-cpu的原因和喚醒者,如果是鎖競爭的情況,則進一步通過內核perf lock剖析鎖競爭。
四、總結
性能優化經常是一個全棧的工作,對工程師的要求也比較高。它是一個很難用一篇文章完整描述清楚的話題,所以本文更多只是起一個提綱挈領的作用,許多話題有待以后有機會進一步展開。文中疏漏,在所難免,還請讀者朋友海涵。最后推薦給親愛的讀者朋友們2本書:
BrenDan Gregg的《System Performance Enterprise and the Cloud(Second Edition)》;
Denis Bakhvalor的《Performance Analysis and Tuning on Modern CPUs》
審核編輯 :李倩
-
cpu
+關注
關注
68文章
10902瀏覽量
213000 -
Linux
+關注
關注
87文章
11345瀏覽量
210386 -
內存管理
+關注
關注
0文章
168瀏覽量
14188
原文標題:Linux內核性能剖析的方法學和主要工具
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論