吴忠躺衫网络科技有限公司

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

Linux內核性能剖析的方法學和主要工具

Linux閱碼場 ? 來源:Linux閱碼場 ? 作者:Linux閱碼場 ? 2022-12-07 15:27 ? 次閱讀

計算機科學的先驅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),實際的加速倍數是:

998858c6-75ff-11ed-8abf-dac502259ad0.png

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個數。

9998adac-75ff-11ed-8abf-dac502259ad0.png

舉一個栗子,軟件的童鞋很可能會天真地以為內核的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】。

99b84946-75ff-11ed-8abf-dac502259ad0.png

最開始核增加的時候,相關業務的性能在提升,到某個拐點后,再增加更多的CPU,性能不升反降,出現了collapse。

延遲

甲骨文的“山”,是一個象形字,它較好地貼合了Linux世界里的延遲模型。Linux世界的延遲往往呈現為這種multi-modal(有多個峰值而不是只分布在一個峰值周圍)或者兩極分化特性。

99cc2a38-75ff-11ed-8abf-dac502259ad0.png

由于這種multi-modal分布的存在,這個時候,我們描述平均值的意義其實不是特別大。比如一個班上有30個學生,其中10個人90分以上(學霸),還有10個人50分以下(學渣),另外還有10個在50-90分之間。我們說這個班的學生平均分60分,其實沒有任何意義,拉馬老師來和我們平均,沒意思的。這個時候,我們需要直方圖來描述這種分布,從而更加直觀地看出來這個班的學霸和學渣比率。

99dc5d54-75ff-11ed-8abf-dac502259ad0.png

如果我們想看一個真實的Linux例子,下面是我的PC在運行“sudo cat /dev/nvme0n1 > /dev/null”的過程中,我看到的BIO(block I/O)的延遲分布:

99ee2976-75ff-11ed-8abf-dac502259ad0.png

我們看到了2個峰值,一個峰值在64us-127us之間;另外一個峰值,則圍繞著1024-2047us分布。當然,還有一個值會偏移地特別遠,比如有2個采樣點,落在了131072us-262143us之間。那2個離群很遠的值,我們一般也稱呼它們為outlier,它們是夜空中最閃亮的星,天生不是凡人。

種種跡象表明,我們僅關注平均值的意義非常有限。對于偏移中線的部分,在延遲分析領域,我們還特別關注一個非常重要的概念,tail latency,中文可譯為尾延遲。比如我們說,90%的延遲落在1ms以內,99%的延遲在10ms以內,但是還有1%的延遲可能更大,甚至形成一個很長很長的尾巴,可能有的達到了1秒也說不定。在延遲分析領域,我們很可能關注這些尾部,比如大家一起競爭mutex,那些延遲很大的case,可能會形成手機系統的卡頓,因此丟幀。對于服務器、電商、云服務等領域而言,高的尾延遲,會直接影響到企業的revenue。

9a05bfbe-75ff-11ed-8abf-dac502259ad0.png

延遲的幾個主要可能的來源:

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內核,然后運行下面簡單的程序:

9a142a86-75ff-11ed-8abf-dac502259ad0.png

這個程序申請了1GB內存,然后fork出來64個進程,其中最原始那個父進程不停讀寫這個1GB的內存。代碼gcc編譯結果a.out。系統的內存是900M,并開啟了zRAM交換功能。運行起來后,我們看它的CPU消耗,a.out固然是很大,可是kswapd0這個內核線程也是非常大的CPU占用。

9a29dd86-75ff-11ed-8abf-dac502259ad0.png

kswapd0相對我們的有用功a.out,只是輔助的工作,本質屬于浪費電。此外,我們的a.out本身占用的接近100%的CPU,也主要耗費在a.out自身的動作上嗎?這個時候我們也有興趣看一下,我們“perf top -p ”一下,我們發現a.out也有大量的時間落在了內核的各種函數里面:

9a3f5878-75ff-11ed-8abf-dac502259ad0.png

所以從性能分析的角度來說,我們也要把這些紅框部分挖掘出來進行分析優化,因為本質他們也是純耗電,屬于overhead而不是real work。

90分到100分特別難

在所有的性能優化領域,我們都不得不正視一點,無論你多么地不愿意:就是前期的優化是相對比較容易的,越到后來越難。一個考10分的學渣,也許經過努力比較容易考到60分,再繼續挑燈夜戰,可能也能考到90,但是哪怕本著“只要學不死,就往死里學”的極限熱情,他也不一定能從90分考到100分,當然它可能考到91分。

9a618524-75ff-11ed-8abf-dac502259ad0.png

努力到一定程度后,有沒有可能越努力成績越差呢?我覺得是可能的,因為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的信息

9a715abc-75ff-11ed-8abf-dac502259ad0.png

我們把采樣到的數據,通過火焰圖工具進行繪制:

9a805a9e-75ff-11ed-8abf-dac502259ad0.png

我們得到如下火焰圖:

9a8f61e2-75ff-11ed-8abf-dac502259ad0.png

火焰圖上我們發現,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的功能。

9a9f0dea-75ff-11ed-8abf-dac502259ad0.png

啥也不說了,盯著perf report里面排名第1,第2的整起來。相關的這種數據報告在Linux社區非常常見,比如MGLRU的patch 【2】里面就列出了MGLRU中某一特性優化前后的CPU消耗對比數據:

9abf6fcc-75ff-11ed-8abf-dac502259ad0.png

我們看到屬于overhead的page_vma_mapped_walk()的減小,但是屬于lzo1x_1_do_compress()的real work的增大。所以,我們看數據說話,沒有數據支撐的性能優化,是很難形成任何說服力的。這也就是為什么我們在內核社區發性能優化的patch,必然會被要求大量benchmark數據支撐。

最后一公里

前面我們發現try_to_unmap()后調用的ptep_clear_flush()是熱點函數,但是它熱在哪里,具體熱在哪一行代碼呢?這個時候,我們需要進一步“perf annotate ptep_clear_flush”。

9ad44e74-75ff-11ed-8abf-dac502259ad0.png

從annotate的結果可以看出,至少,在筆者運行的qemu平臺上,tlbi附近的開銷是非常大的,其他的代碼開銷幾乎可以忽略不計。當然,真實的ARM64硬件上,tlbi也絕對不便宜。

中斷屏蔽的問題

在一個類似如下spin_lock_irqsave()、spin_unlock_irqrestore()的區間里,由于采樣的中斷都是被屏蔽的,所以中間perf的采樣結果,都會在spin_unlock_irqrestore()開啟中斷的這一刻爆出來,導致ARM64平臺下,采樣的熱點落在類似spin_unlock_irqrestore()這樣的地方。這是不準確的。

9affd9fe-75ff-11ed-8abf-dac502259ad0.png

我們在調試時,可以使能ARM64_PSEUDO_NMI選項,并傳遞irqchip.gicv3_pseudo_nmi=1這樣的bootargs參數。這樣,內核會用GIC的高優先級中斷模擬NMI,在local_irq_disable()、spin_lock_irqsave()、spin_lock_irq()這樣的API里面只是屏蔽低優先級中斷。

9b23bc34-75ff-11ed-8abf-dac502259ad0.png

舉一個典型的栗子,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()這個函數,但是這個函數長成這個樣子,進去的時候就關中斷,出來的時候才開中斷:

9b338b00-75ff-11ed-8abf-dac502259ad0.png

這個時候,你不開啟irqchip.gicv3_pseudo_nmi=1是不可能perf annonate出來這個函數哪句話是熱點的,所有熱點都會落在末尾的local_irq_restore()這句話。但是開啟后,你才會抓到真正的熱點:

9b4b735a-75ff-11ed-8abf-dac502259ad0.png

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:

9b5f5e4c-75ff-11ed-8abf-dac502259ad0.png

比如運行gcc main.c,可以看到IPC是1.36:

9b789dd0-75ff-11ed-8abf-dac502259ad0.png

Ahmand Yasin在它的IEEE論文《A top-down method for performance analysis and counter architercture》中,革命性地給出了一個從CPU指令執行的順暢程度來評估和發現瓶頸的方法,允許我們從黑盒的角度以諸葛孔明“隆中對”式的格局來看問題。

9b8baaf6-75ff-11ed-8abf-dac502259ad0.png

現在處理器,一般在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:

9bad9576-75ff-11ed-8abf-dac502259ad0.png

假設其中的20個slots要么是empty沒活干(stalled),或者做的是無用功(分支預測錯誤等情況),那么它的IPC可能就是2.0了。

9bbefa78-75ff-11ed-8abf-dac502259ad0.png

Intel處理器架構下,已經將top-down完全地工具化,有專門的top-down工具,有的甚至已經圖形化了,比如VTune Profiler里面就有類似功能【4】,可以具體化到每一個特定函數的Front-end Bound、Back-End Bound、Bad Speculation、Retiring等的情況:

9bd3a630-75ff-11ed-8abf-dac502259ad0.png

大家從上表可以看出,基本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可能還是不太明白,下面我們通過一個代碼栗子:

9be6af14-75ff-11ed-8abf-dac502259ad0.png

這個里面有個隨機數r = rand(),會極大地破壞分支預測的準確性,所以topdown的結果如下:

9bf70fe4-75ff-11ed-8abf-dac502259ad0.png

如果我們把里面的rand()函數變成自己的版本,讓分支結果并不那么隨機:

9c0d88f0-75ff-11ed-8abf-dac502259ad0.png

再次topdown,結果則是(retiring很大,綠色友好程序):

9c1b6e66-75ff-11ed-8abf-dac502259ad0.png

此時的IPC也很大,達到3.49,比較接近4.0了:

9c3b0a28-75ff-11ed-8abf-dac502259ad0.png

Intel方面,腳本化的工具則有pmu-tools【5】。比如筆者在自己的X86 PC(內存24GB)上面開啟zRAM后運行如下代碼:

9c5ce13e-75ff-11ed-8abf-dac502259ad0.png

假設kswapd的PID是202,我們捕獲以下kswapd的bound情況:

9c6ee776-75ff-11ed-8abf-dac502259ad0.png

由此我們可以看出,在上述場景下,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。

9c8dd794-75ff-11ed-8abf-dac502259ad0.png

Intel的平臺享受上面的工具化優勢。其他平臺的童鞋也不必懊惱,perf本身也具有topdown能力,perf stat后面有個參數是--topdown:

9c9e722a-75ff-11ed-8abf-dac502259ad0.png

查看Intel童鞋的這個patch【6】,最新版本的perf實際也可以支持td-level=2這樣更細粒度的topdown打印:

9cb239e0-75ff-11ed-8abf-dac502259ad0.png

整個on-cpu分析的過程是top-down的,過程中的某些步驟也是topdown的,我們用一個流程圖來描述:

9cc17ee6-75ff-11ed-8abf-dac502259ad0.png

三、off-cpu分析

off-cpu分析更多關注延遲問題,所以我們首先要獲知延遲的分布,這個時候我們最好使用直方圖。之后,我們可以過度到用off-cpu火焰圖等進一步分析off-cpu在等什么,而在lock contention的場合,則可以使用perf lock來進一步進行鎖的分析。

直方圖

直方圖深入人心,哪怕什么工具都沒有,純粹地用Linux內核也可以劃出直方圖。這個功能位于菜單tracer->Histogram triggers,通過內核tracepoints實現。

9cd605b4-75ff-11ed-8abf-dac502259ad0.png

下面我們隨便舉個例子,看看mm/vmscan.c中shrink_inactive_list()一般回收page個數的分布。注意,這純粹是一個栗子,不對應任何的實際工程。

9ce6d5e2-75ff-11ed-8abf-dac502259ad0.png

在直方圖中,我們關心2個點,一個是key,一個是value。比如我們以回收頁面的個數為key,回收到這一key值頁面個數的次數為value。

我們在include/trace/events/vmscan.h和mm/vmscan.c中增加這個trace:

9cf380bc-75ff-11ed-8abf-dac502259ad0.png

trace_mm_vmscan_nr_reclaimed(nr_reclaimed, 1);這句話表示回收nr_reclaimed個頁面的次數增加1。

現在我們使能tracer,以nr_reclaimed為key, times為value,按照times降序排列:

9d05bc78-75ff-11ed-8abf-dac502259ad0.png

運行系統,觸發一些內存回收動作,采樣到一些值后,直接讀取直方圖:

9d16a5b0-75ff-11ed-8abf-dac502259ad0.png

從直方圖可看出,實驗場景中,回收到32個pages的機會最多,占據76598次,其次是回收到1個、0個和2個的。

下面我們把所有采樣復原,改為依據nr_reclaimed升序顯示:

復原:

9d2dd050-75ff-11ed-8abf-dac502259ad0.png

開啟新的采樣:

9d3d54ee-75ff-11ed-8abf-dac502259ad0.png

運行系統,觸發一些內存回收動作,采樣到一些值后,直接讀取直方圖:

9d4b6d40-75ff-11ed-8abf-dac502259ad0.png

有的童鞋說,我現在關注的是延遲時間的分布,不是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函數):

9d6fab74-75ff-11ed-8abf-dac502259ad0.png

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之后就好:

9d849fe8-75ff-11ed-8abf-dac502259ad0.png

我們看到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的比例,只需要寫一個簡單的腳本:

9dbac00a-75ff-11ed-8abf-dac502259ad0.png

這個腳本的關鍵是涂了紅色的三行,定義了一個直方圖對象dist,然后就在里面通過dist.increment(e)增加采樣點,最后通過b["dist"].print_linear_hist("idle")把直方圖畫出來:

9dcdc4a2-75ff-11ed-8abf-dac502259ad0.png

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()的函數原型是:

9ddf66da-75ff-11ed-8abf-dac502259ad0.png

可見參數prev是被切換走的進程,而我們通過current可以拿到當前的進程,也即我們切入的進程。通過下面的算法,即可求出整個off-cpu區間的時間和backtrace:

9df17820-75ff-11ed-8abf-dac502259ad0.png

通過在內核finish_task_switch()的kprobe點上插入eBPF/BCC代碼來完成這個算法,這樣不需要修改和重新編譯內核。之后,我們可以把eBPF/BCC捕獲的數據,借助flamegraph工具,繪制出off-cpu火焰圖。

下面給一個非常簡單的案例,在我的Ubuntu x86 PC上面運行以下代碼,創建32個進程,每個進程申請1G內存,然后循環執行:16字節對齊的時候寫入一個字節,1MB對齊的時候睡眠30us。

9e002a46-75ff-11ed-8abf-dac502259ad0.png

測試內核是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:

9e0f8d9c-75ff-11ed-8abf-dac502259ad0.png

捕獲a.out 30秒的off-cpu數據,并繪制火焰圖。

9e1f6082-75ff-11ed-8abf-dac502259ad0.png

得到如下火焰圖:

9e2f5c08-75ff-11ed-8abf-dac502259ad0.png

從以上火焰圖可以看出,a.out的延遲主要是3個方面原因:

發生在其用戶態代碼本身調用的nanosleep()上;

發生在page_fault的處理上;

時間片到期后, timer中斷進來,把它切換走。

我們把這3個塊分別畫3個圓圈:

9e46968e-75ff-11ed-8abf-dac502259ad0.png

火焰圖的縱向是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()這個路徑上。

9e5c06d6-75ff-11ed-8abf-dac502259ad0.png

如果我們把焦點移到kswapd,我們還是運行上面的a.out代碼,但是我們捕獲和分析的對象換為kswapd。捕獲kswapd的off-cpu數據30秒并繪制off-cpu火焰圖。

9e80fe00-75ff-11ed-8abf-dac502259ad0.png

繪制出來的kswapd的off-cpu火焰圖如下:

9e92a0d8-75ff-11ed-8abf-dac502259ad0.png

可見多數延遲發生在kswapd各種路徑下(比如shrink_inactive_list -> shrink_page_list路徑)主動調用_cond_resched()出讓CPU,還有一部分延遲發生在最右邊的shrink_slab的i915顯卡驅動的slab shrink,點擊放大它:

9ebbdfac-75ff-11ed-8abf-dac502259ad0.png

從上圖可以看出,我們在內存回收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火焰圖:

9ed48e8a-75ff-11ed-8abf-dac502259ad0.png

得到的圖如下:

9ee24746-75ff-11ed-8abf-dac502259ad0.png

從圖上可以完整看出,kswapd off-cpu的原因和喚醒者。比如畫紅圈的區域,kswapd因為調用kswapd_try_to_sleep()而主動進入睡眠,a.out在swap in的過程中do_swap_page()因而需要alloc_pages()的時候因為申請內存的壓力,喚醒了kswapd內核線程。天藍色的是kswapd,淡藍色的是喚醒者。喚醒者的調用棧是從上到下,off-cpu的kswapd的調用棧是從下到上,中間通過灰色隔離帶隔開。這種描述方式,確實看起來比較驚艷有木有?

9f05798c-75ff-11ed-8abf-dac502259ad0.png

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情況:

9f1c7be6-75ff-11ed-8abf-dac502259ad0.png

然后生成報告:

9f2cc97e-75ff-11ed-8abf-dac502259ad0.png

看起來&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剖析鎖競爭。

9f3e4bf4-75ff-11ed-8abf-dac502259ad0.png

四、總結

性能優化經常是一個全棧的工作,對工程師的要求也比較高。它是一個很難用一篇文章完整描述清楚的話題,所以本文更多只是起一個提綱挈領的作用,許多話題有待以后有機會進一步展開。文中疏漏,在所難免,還請讀者朋友海涵。最后推薦給親愛的讀者朋友們2本書:

BrenDan Gregg的《System Performance Enterprise and the Cloud(Second Edition)》;

Denis Bakhvalor的《Performance Analysis and Tuning on Modern CPUs》

審核編輯 :李倩

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • cpu
    cpu
    +關注

    關注

    68

    文章

    10902

    瀏覽量

    213000
  • Linux
    +關注

    關注

    87

    文章

    11345

    瀏覽量

    210386
  • 內存管理
    +關注

    關注

    0

    文章

    168

    瀏覽量

    14188

原文標題:Linux內核性能剖析的方法學和主要工具

文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    騰訊云內核團隊修復Linux關鍵Bug

    騰訊云操作系統(Tencent OS)內核團隊近日在Linux社區取得了顯著成果。他們提交的兩項改進方案,成功解決了自2021年以來一直困擾眾多一線廠商,并在近期讓多個Linux頂級
    的頭像 發表于 12-31 10:58 ?284次閱讀

    嵌入式學習-飛凌嵌入式ElfBoard ELF 1板卡-Linux內核移植之內核簡介

    用戶提供移植好的板級開發包。板卡廠商也會對移植好的內核版本進行維護,例如一些BUG修復或者物料替換。接下來講一下獲取這三種源碼的方法:獲取linux官網源碼 地址:https
    發表于 12-16 13:08

    飛凌嵌入式ElfBoard ELF 1板卡-Linux內核移植之內核簡介

    用戶提供移植好的板級開發包。板卡廠商也會對移植好的內核版本進行維護,例如一些BUG修復或者物料替換。接下來講一下獲取這三種源碼的方法:獲取linux官網源碼地址:https
    發表于 12-13 09:03

    Kali Linux常用工具介紹

    Kali Linux 虛擬機中自帶了大量滲透測試工具,涵蓋了信息收集、漏洞利用、口令破解、漏洞掃描等多個方面。 以下是按分類簡要介紹一部分常用工具的使用方法: 使用
    的頭像 發表于 11-11 09:29 ?692次閱讀

    deepin社區亮相第19屆中國Linux內核開發者大會

    中國 Linux 內核開發者大會,作為中國 Linux 內核領域最具影響力的峰會之一,一直以來都備受矚目。
    的頭像 發表于 10-29 16:35 ?577次閱讀

    詳解linux內核的uevent機制

    linux內核中,uevent機制是一種內核和用戶空間通信的機制,用于通知用戶空間應用程序各種硬件更改或其他事件,比如插入或移除硬件設備(如USB驅動器或網絡接口)。uevent表示“用戶空間
    的頭像 發表于 09-29 17:01 ?932次閱讀

    Linux服務器性能查看方法

    Linux服務器性能查看是系統管理員和開發人員在日常工作中經常需要進行的任務,以確保系統穩定運行并優化資源使用。以下將詳細介紹多種Linux服務器性能查看的
    的頭像 發表于 09-02 11:15 ?1235次閱讀

    linux驅動程序如何加載進內核

    Linux系統中,驅動程序是內核與硬件設備之間的橋梁。它們允許內核與硬件設備進行通信,從而實現對硬件設備的控制和管理。 驅動程序的編寫 驅動程序的編寫是Linux驅動開發的基礎。在編
    的頭像 發表于 08-30 15:02 ?589次閱讀

    linux驅動程序的編譯方法是什么

    Linux驅動程序的編譯方法主要包括兩種: 與內核一起編譯 和 編譯成獨立的內核模塊 。以下是對這兩種
    的頭像 發表于 08-30 14:46 ?763次閱讀

    linux驅動程序的編譯方法有哪兩種

    Linux驅動程序的編譯方法主要可以歸納為兩種: 手動編譯 和 使用內核構建系統(Makefile)自動編譯 。 1. 手動編譯 手動編譯驅動程序通常涉及直接使用GCC(GNU Com
    的頭像 發表于 08-30 14:39 ?908次閱讀

    Linux內核測試技術

    內核測試技術是實現這一目標的關鍵手段。本文將詳細介紹 Linux 內核測試的各種技術,包括單元測試、集成測試、功能測試和性能測試等,并討論不同測試
    的頭像 發表于 08-13 13:42 ?583次閱讀
    <b class='flag-5'>Linux</b><b class='flag-5'>內核</b>測試技術

    Linux內核中的頁面分配機制

    Linux內核中是如何分配出頁面的,如果我們站在CPU的角度去看這個問題,CPU能分配出來的頁面是以物理頁面為單位的。也就是我們計算機中常講的分頁機制。本文就看下Linux內核是如何管
    的頭像 發表于 08-07 15:51 ?351次閱讀
    <b class='flag-5'>Linux</b><b class='flag-5'>內核</b>中的頁面分配機制

    Linux中查看IP地址的方法

    ifconfig是一個網絡管理工具,用于配置、控制和查詢網絡接口。在許多舊的Linux發行版中,ifconfig是查看和配置網絡接口的主要工具
    的頭像 發表于 08-07 15:16 ?7909次閱讀
    在<b class='flag-5'>Linux</b>中查看IP地址的<b class='flag-5'>方法</b>

    歡創播報 華為宣布鴻蒙內核已超越Linux內核

    1 華為宣布鴻蒙內核已超越Linux內核 ? 6月21日,在華為開發者大會上, HarmonyOS NEXT(鴻蒙NEXT)——真正獨立于安卓和iOS的鴻蒙操作系統,正式登場。這是HarmonyOS
    的頭像 發表于 06-27 11:30 ?903次閱讀

    使用 PREEMPT_RT 在 Ubuntu 中構建實時 Linux 內核

    盟通技術干貨構建實時Linux內核簡介盟通技術干貨Motrotech如果需要在Linux中實現實時計算性能,進而有效地將Linux轉變為RT
    的頭像 發表于 04-12 08:36 ?2740次閱讀
    使用 PREEMPT_RT 在 Ubuntu 中構建實時 <b class='flag-5'>Linux</b> <b class='flag-5'>內核</b>
    百家乐透视牌靴| 百家乐官网知识技巧玩法| 温州百家乐官网的玩法技巧和规则| BB百家乐官网HD| 百家乐官网怎么对冲打| 百家乐必胜法| 德州扑克比大小| 大发888娱乐城 df888ylc3403 | 金樽百家乐官网的玩法技巧和规则| 百家乐官网号论坛博彩正网| 大发888游戏官方下载客户端 | 百家乐官网翻天快播| 基础百家乐的玩法技巧和规则 | 博彩e天上人间| 百家乐2万| 方城县| 百家乐保单机解码| 在线百家乐官网技巧| 百家乐高手论| 百家乐官网投注外围哪里好| 大发888游戏是真的么| 百樂坊百家乐官网的玩法技巧和规则 | 百家乐官网在线投注顺势法| 百家乐光纤冼牌机| 百家乐官网平技巧| 大发888扑克场| 恒丰百家乐官网的玩法技巧和规则 | A8百家乐官网娱乐平台| 大发888娱乐城下载| 三合四局24向黄泉| 内黄县| 贝博百家乐的玩法技巧和规则 | 金银岛百家乐官网的玩法技巧和规则 | 百家乐不锈钢| 博彩网百家乐官网的玩法技巧和规则| 明升娱乐场 | 百家乐连输的时候| 百家乐官网下注的规律| 大发888客户端 运行| 百家乐赢得秘诀| 澳门百家乐官网娱乐城信誉如何|