Linux內核分析:頁回收導致的cpu load瞬間飆高的問題分析與思考

小米_1900 8年前發布 | 38K 次閱讀 Linux內核 Linux

來自: http://mogu.io/156-156

摘要

本文一是為了討論在Linux系統出現問題時我們能夠借助哪些工具去協助分析,二是討論出現問題時大致的可能點以及思路,三是希望能給應用層開發團隊介紹一些Linux內核機制從而選擇更合適的使用策略。

前言

搜索團隊的服務器前段時間頻繁出現CPU load很高( 比如load average達到80多 )的情況,正所謂術業有專攻,搜索的兄弟們對Linux底層技術理解的不是很深入,所以這個問題困擾了他們一段時間。

相信我們在遇到問題時都有類似的經歷,如果這個問題涉及到我們不熟悉的領域,我們往往會手足無措。

由于虛擬化團隊具備一些Linux底層背景知識,所以在知曉了這個搜索團隊遇到的困難后,就開始協助他們定位出問題根因并幫助他們解決了問題。

我希望能借助這個機會給大家介紹一下在Linux系統出現問題時我們能夠借助哪些工具去協助分析;以及介紹一下Linux在內存管理方面的一些機制以及我們的使用策略。

Linux系統出現問題,我們該如何去分析

工欲善其事,必先利其器。要解決問題,首先得去定位問題的原因。在Linux系統里面有很多的問題定位工具,可以協助我們來分析問題。于是我們就針對目前搜索服務器的現象,思考可以借助哪些工具來找到問題原因。

Linux系統響應慢,從內核的角度看,大致可能有以下幾種情況:

  1. 線程在內核態執行的時間過長,這個時間超出了它被調度算法給分配的執行時間,它在內核態長時間的占用CPU,而且也不返回用戶態。

    這種現象有個術語,叫做softlockup。

    通過一個現象來簡單說下內核態和用戶態。我們可能遇到這個現象,執行完一個命令,CTRL+C怎么都殺不死它,而且敲鍵盤也反應,這可能就是因為此時這個進程正運行在內核態,CRTL+C是給進程發signal的方式通知進程,而進程只有在從內核態返回用戶態的時候才會去檢查有沒有信號,所以如果它處在內核態的話顯然是無法被殺死的。( 當然這種情況還可以是因為進程在代碼里屏蔽掉了SIGQUIT信號 )這種現象就給我們系統很忙的感覺。

    針對softlockup,內核里有一套檢測機制,它提供給用戶一個sysctl調試接口:kernel.softlockup_panc,我們可以將該值設置為1,這樣在出現這種問題的時候,讓內核主動的去panic,從而dump出來一些現場信息。

     ps: 建議我們的服務器都使能該選項
     
  2. CPU load值高,說明處于Running狀態和D狀態的線程太多。

    線程等待資源而去睡眠,就會進入D狀態( 即Disk sleep,深度睡眠 ),進入D狀態的線程是不能夠被打斷的,他們會一直睡眠直到等待的資源被釋放時主動去喚醒他們。( 大致的原理是,這些線程在等待什么資源,比如某個信號量,它就會被加入到這個信號量的等待隊列里,然后其它的線程釋放這個信號量的時候會去檢查該信號量的等待隊列,然后把隊列里線程給喚醒。 )

    D狀態的線程可以通過“ps aux”命令來看,“D”即表示D狀態:

root 732 0.0 0.0 0 0 ? S Oct28 0:00[scsi_eh_7]

root 804 0.0 0.0 0 0 ? D Oct28 5:52[jbd2/sda1-8]

root 805 0.0 0.0 0 0 ? S Oct28 0:00[ext4-dio-unwrit]

root 806 0.0 0.0 0 0 ? D Oct28 12:16[flush-8:0]

</div>

而正常的處于CPU調度隊列上的線程,則是Sleep狀態:

$ ps aux | grep "flush \| jbd"

root 796 0.0 0.0 0 0 ? S 2014 52:39 [jbd2/sda1-8]

root 1225 0.0 0.0 0 0 ? S 2014 108:38 [flush-8:0]

yafang 15030 0.0 0.0 103228 824 pts/0 S+ 0:00 grep flush|jbd

</div>

對于這種現象,內核也有個術語,叫做hung_task, 而且也有個監測機制。默認如果線程120S都處在D狀態,內核就會打印出一個告警信息,這個時間是可以調整的。而且我們也可以讓內核在出現這個狀況時去panic。

    ps:讓內核panic的目的是為了在panic時dump出現場信息。
  1. 在內核態,出了進程上下文外,還有中斷上下文,( PS:在我們用的2.6的內核上,中斷仍然還是使用的內核棧,它沒有自己獨立的棧空間 )。中斷也可能有異常,比如長時間被關中斷。

    中斷長時間被關閉,這個現象叫做hardlockup。

    針對hardlockup,內核也有監測機制,是NMI watchdog

    。可以通過/proc/interrupts來看系統是否使能了NMI watchdog。
  2. </ol>

    $ cat /proc/interrupts | grep NMINMI : 320993 264474 196631 16737 Non-maskable interrupts

    值不為0,說明系統使能了NMI watchdog。然后我們通過sysctl將kernel.nmi_watchdog設置為1,即,在觸發了NMI watchdog的時候主動讓內核去panic。從而監測出hardlockup這種故障。

        ps: 我們的服務器上NMI watchdog應該是都使能了

    部署工具,搜集現場信息

    我們部署了前面那些工具的目的是為了搜集故障現場信息,以此來幫我們找出root cause。Linux內核搜集故障現場信息的大殺器是kdump+kexec。

    kdump的基本原理是,內核在啟動時首先會預留一部分物理內存給crash kernel,在有panic時,會調用kexec直接啟動crash kernel到該物理內存區域,由于是使用kexec啟動,從而繞過了boot loader的一系列初始化,因而整個內存的其它區域不會被更改(即事故現場得以保留),然后新啟動的這個kernel會dump出來所有的內存內容(這些內容可以裁剪),然后存儲在磁盤上。

    來看下我們的系統上是否啟用了kdump。

    $ cat /proc/cmdlinero root=UUID=1ad1b828-e9ac-4134-99ce-82268bc28887 rd_NO_LUKS rd_NO_LVM LANG=en_US.UTF-8 rd_NO_MD SYSFONT=latarcyrheb-sun16 crashkernel=133M@0M KEYBOARDTYPE=pc KEYTABLE=us rd_NO_DM rhgb quiet

    內核啟動參數"crashkernel=133M@0M"告訴我們系統已經啟用了kdump。當然Crash kernel的地址空間并不是在0M的地方,而是:

    $ cat /proc/iomem | grep Crash03000000-0b4fffff : Crash kernel

    接著去看kdump內核服務是否已經開啟:

    $ sudo service kdump statusKdump is operational

    說明已經開啟了。kdump是通過/etc/kdump.conf來配置的,默認它會把抓取到的內核現場信息(即vmcore)給生成到/var/crash目錄下,通過crash這個命令來分析該vmcore。

    ps: 建議我們的所有服務器都配置好kdump

    抓取現場信息:CPU到底在干什么

    在有一天的早晨,剛來到辦公室后( bug出現的時間點還挺人性化 ),一臺服務器出現了load高的告警信息。然后嘗試去登錄上去,很慢,還是登錄了進去;再嘗試敲命令,非常非常慢,不過還是響應了。

    perf top 觀察到很多線程都是在spinlock或者spinlock_irq(關中斷自旋)狀態,( PS:原諒我沒保存當時的圖 )。看起來內核里面有死鎖或者什么。

    ps: perf是協助分析問題的一個有力工具,建議我們的服務器都裝上perf

    這個時候CPU到底在干什么?為此我們不得不祭出終極大殺招:使用sysrq讓內核panic( 這個服務器的流量已經導走,所以可以重啟 ),然后panic觸發kdump來保存現場信息。( 還有個更終極的殺器:鍵盤的sysrq鍵+字母的組合,這可以應用于我們無法登錄到服務器的情況, 可以借助于管理卡,然后使用鍵盤中斷來觸發信息的搜集。 )

    ps: 建議我們的服務器上都配置好sysrq

    首先sysrq得是使能的:

    $ cat /proc/sys/kernel/sysrq1

    然后讓內核panic:

    $ echo c > /proc/sysrq-triggger

    于是就在/var/crash目錄下獲取到了vmcore。

    #現場信息的分析過程

    可以通過crash這個命令來分析vmcore,由于這個vmcore不是ELF格式,所以是不能用gdb之類的工具來分析的。

    以下是對該vmcore的部分關鍵信息分析:

    $ crash  /usr/lib/debug/lib/modules/2.6.32-431.el6.x86_64/vmlinux vmcore 
    crash> bt -a 
    PID: 8400   TASK: ffff880ac686b500  CPU: 0   COMMAND: "crond"
    ...

    6 [ffff88106ed1d668] _spin_lock at ffffffff8152a311

    7 [ffff88106ed1d670] shrink_inactive_list at ffffffff81139f80 <br>

    8 [ffff88106ed1d820] shrink_mem_cgroup_zone at ffffffff8113a7ae

    9 [ffff88106ed1d8f0] shrink_zone at ffffffff8113aa73

    10 [ffff88106ed1d960] zone_reclaim at ffffffff8113b661

    ... PID: 8355 TASK: ffff880e67cf2aa0 CPU: 12 COMMAND: "java" ...

    6 [ffff88106ed49598] _spin_lock_irq at ffffffff8152a235

    7 [ffff88106ed495a0] shrink_inactive_list at ffffffff8113a0c5

    8 [ffff88106ed49750] shrink_mem_cgroup_zone at ffffffff8113a7ae

    9 [ffff88106ed49820] shrink_zone at ffffffff8113aa73<br>

    10 [ffff88106ed49890] zone_reclaim at ffffffff8113b661<br>

    ... PID: 4106 TASK: ffff880103f39540 CPU: 15 COMMAND: "sshd"

    6 [ffff880103e713b8] _spin_lock at ffffffff8152a311

    7 [ffff880103e713c0] shrink_inactive_list at ffffffff81139f80

    8 [ffff880103e71570] shrink_mem_cgroup_zone at ffffffff8113a7ae

    9 [ffff880103e71640] shrink_zone at ffffffff8113aa73

    10 [ffff880103e716b0] zone_reclaim at ffffffff8113b661

    ... PID: 19615 TASK: ffff880ed279e080 CPU: 16 COMMAND: "dnsmasq" ...

    6 [ffff880ac68195a8] shrink_inactive_list at ffffffff81139daf

    7 [ffff880ac6819750] shrink_mem_cgroup_zone at ffffffff8113a7ae

    8 [ffff880ac6819820] shrink_zone at ffffffff8113aa73

    9 [ffff880ac6819890] zone_reclaim at ffffffff8113b661

    ... PID: 8356 TASK: ffff880ed267c040 CPU: 17 COMMAND: "java"

    6 [ffff88106ed4b5d8] _spin_lock at ffffffff8152a30e

    7 [ffff88106ed4b5e0] shrink_inactive_list at ffffffff81139f80

    8 [ffff88106ed4b790] shrink_mem_cgroup_zone at ffffffff8113a7ae

    9 [ffff88106ed4b860] shrink_zone at ffffffff8113aa73

    10 [ffff88106ed4b8d0] zone_reclaim at ffffffff8113b661

    ... </pre>

    大致的意思就是,現在所有需要allocate memory的線程,都得調用zone_reclaim去inactive_list上去回收pagecache,這個行為也就是所謂的direct reclaim。

    簡要的匯總信息如下:

    總共有24個CPU,我們看下每個CPU此時的狀態

    CPU 跑的程序 正在運行的函數
    0 crond _spin_lock(&zone->lru_lock)
    1 bash _spin_lock(&zone->lru_lock)
    2 crond _spin_lock(&zone->lru_lock)
    3 bash _spin_lock(&zone->lru_lock)
    4 swapper idle
    5 java _spin_lock(&zone->lru_lock)
    6 bash sysrq
    7 crond _spin_lock(&zone->lru_lock)
    8 swapper idle
    9 swapper idle
    10 swapper idle
    11 swapper idle
    12 java _spin_lock(&zone->lru_lock)
    13 sh _spin_lock(&zone->lru_lock)
    14 bash _spin_lock(&zone->lru_lock)
    15 sshd _spin_lock(&zone->lru_lock)
    16 dnsmasq shrink_inactive_list
    17 java _spin_lock(&zone->lru_lock)
    18 lldpd _spin_lock_irq(&zone->lru_lock)
    19 swapper idle
    20 sendmail _spin_lock(&zone->lru_lock)
    21 swapper idle
    22 swapper idle
    23 swapper idle

    從這個表格我們可以看到,所有申請內存的線程都在等待zone->lru_lock這把自旋鎖,而這把自旋鎖現在被CPU16上的dnsmasq這個線程持有,它現在正賣力的回收pagecache到freelist。于是從這個zone里來申請內存的線程都得在這里等待著,于是load值就高了上來。外在的表現就是,系統反映好慢啊,ssh都登不進去(因為ssh也會申請內存);即使登錄進去了,敲命令也沒有反應(因為這些命令也都是需要申請內存的)。

    背后的知識

    page cache

    導致這個情況的原因是:線程在申請內存的時候,發現該zone的freelist上已經沒有足夠的內存可用,所以不得不去從該zone的LRU鏈表里回收inactive的page,這種情況就是direct reclaim(直接回收)。direct reclaim會比較消耗時間的原因是,它在回收的時候不會去區分dirty page和clean page,如果回收的是dirty page,就會觸發磁盤IO的操作,它會首先把dirty page里面的內容給刷寫到磁盤,再去把該page給放到freelist里。

    我們先用一張圖來看下memory,page cache,Disk I/O的關系。

    舉個簡單的例子,比如我們open一個文件時,如果沒有使用O_DIRECT這個flag,那就是File I/O, 所有對磁盤文件的訪問都要經過內存,內存會把這部分數據給緩存起來;但是如果使用了O_DIRECT這個flag,那就是Direct I/O, 它會繞過內存而去直接訪問磁盤,訪問的這部分數據也不會被緩存起來,自然性能上會降低很多。

    page reclaim

    在直觀上,我們有一個認知,我們現在讀了一個文件,它會被緩存到內存里面,如果接下來的一個月我們一直都不會再次訪問它,而且我們這一個月都不會關閉或者重啟機器,那么在這一個月之后該文件就不應該再在內存里頭了。這就是內核對page cache的管理策略:LRU( 最近最少使用 )。即把最近最少使用的page cache給回收為free pages。

    內核的頁回收機制有兩種:后臺回收和直接回收。

    后臺回收是有一個內核線程kswapd來做的,當內存里free的pages低于一個水位(page_low)時,就會喚醒該內核線程,然后它從LRU鏈表里回收page cache到內存的free_list里頭,它會一直回收直至free的pages達到另外一個水位page_high. 如下圖所示,

    直接回收則是,在發生page fault時,沒有足夠可用的內存,于是線程就自己直接去回收內存,它一次性的會回收32個pages。邏輯過程如下圖所示,

    所以說,我們應該要避免做direct reclaim。

    memory zone

    對于多核NUMA系統而言,內存是分節點的,不同的CPU對不同的內存節點的訪問速度是不一樣的,所以CPU會優先去訪問靠近自己的內存節點( 即速度相對快的內存區域 )。

    CPU內部是依靠MMU來進行內存管理的,根據內存屬性的不同,MMU將一個內存節點內部又劃分了不同的zone。對64-bit系統而言( 即我們現在使用的系統 ),一個內存節點包含三個zone:Normal,DMA,DMA32. 對32-bit系統而言,一個內存節點則是包括:Normal,Highmem,DMA。Highmem存在的目的是為了解決線性地址空間不夠用的問題,在64-bit上由于有足夠的線性地址空間所以就沒了該zone。

    不同zone存在的目的是基于數據的局部性原則,我們在寫代碼的時候也知道,把相關的數據給放在一起可以提高性能,memory zone也是這個道理。于是MMU在分配內存時,也會盡量給同一個進程分配同一個zone的內存。凡事有利就有弊,這樣有好處自然也可能會帶來一些壞處。

    root cause知道了:解決問題

    為了避免direct reclaim,我們得保證在進程申請內存時有足夠可用的free pages,從前面的背景知識我們可以看出,提高watermark low可以盡早的喚醒kswapd,然后kswapd來做background reclaim。為此,內核專門提供了一個sysctl接口給用戶來使用:vm.extra_free_kbytes.

    于是我們增大這個值(比如增大到5G,hohoho),確實也解決了問題。增大該值來提高low水位,這樣在申請內存的時候,如果free的內存低于了該水位,就會喚醒kswapd去做頁回收,同時又由于還有足夠的free內存可用所以進程能夠正常申請而不觸發直接回收。

    PS:
    extra_free_kbytes是CentOS-6(kernel-2.6.32導出的一個接口),
    該接口并未合入內核主線,它是redhat自己合入的一個patch。   
    在CentOS-7(kernel-3.10)上刪除了該接口,轉而使用dirty ratio來觸發,
    因為直接回收耗時長的直接原因就是因為回收的時候會去回收dirty page,
    所以CentOS-7的這種做法更加合理一些,
    extra_free_kbytes在某種程度上也浪費了一些內存的使用。    

    但是,這還不夠。我們從前面dump出來的內核alltrace也可以看出,線程的回收跟memory zone相關。也就是說normal zone里面的free pages不夠用了,于是觸發了direct reclaim。但是,假如此時DMA zone里還有足夠的free pages呢?線程會不會從DMA zone里來申請內存呢?

    繼續來看另外一個問題

    就在解決了這個問題沒幾天后,搜索的solr服務器又遇到一個類似的問題,只不過這個問題跟前面那個問題有一點不一樣的地方.先來看下它的free memory:

    $ free -m

    total used free shared buffers cached

    Mem: 64391 62805 1586 0 230 27665

    -/+ buffers/cache: 34909 29482

    swap: 15999 0 15999

    </div>

    我們可以看到,此時它的free pages還是挺多的,有1G多。

    再來看下它有多少個dirty pages:

    $ cat /proc/vmstat

    nr_free_pages 422123

    nr_inactive_anon 1039139

    nr_active_anon 7414340

    nr_inactive_file 3827150

    nr_active_file 3295801

    ...

    nr_dirty 4846

    nr_writeback 0

    ...

    </div>

    同時它的dirty pages也很高,有4846個。

    這跟前面的問題有一些不一樣,前面的問題是free的pages很少同時dirty的pages很多,這里則是free的pages挺多而dirty的pages也很多。

    這就是我們前面對memory zone的那個疑問。free的pages都在其它的zone里頭,所以線程去回收自己zone的page cache而不去使用其它zone的free pages。對于這個內核也提供了一個接口給用戶使用:vm.zone_reclaim_mode. 這個值在該機器上本來是1( 即寧肯回收自己zone的page cache,也不去申請其它zone的free pages ),我把它更改為0( 即只要其它zone有free pages就去其它zone里申請 ),就解決了該問題( 一設置后系統就恢復了正常 )。

    將該值從1改為0后,效果是立竿見影:

    $ free -m

    total used free shared buffers cached

    Mem: 64391 64062 329 0 233 28921

    -/+ buffers/cache: 34907 29484

    swap: 15999 0 15994

    </div>

    可以看到free的pages立馬減少了,同時dirty pages也減少了(沒人跟flush線程搶zone->lru_lock這把鎖了,自然它臟頁刷的也快了)。

    總結&思考 :機制與策略

    從前面我們討論的這個問題也可以看出,Linux內核提供了各種各樣的機制,然后我們根據具體的使用場景來選擇使用的策略。我們的目的肯定是為了在不影響穩定性的前提下,盡可能的提升系統性能。然而如果真的是穩定性與性能二選一的話,毫無疑問我們要去選擇穩定性。

    Linux機制的多種多樣,也給上層的開發者帶來了一些苦惱:由于對底層了解的不深入,就很難選擇出一個很好的策略來使用這些內核機制。

    然而對這些機制的使用,也不會有一個萬能公式,還是要看具體的使用場景。由于搜索服務器存在很多批量文件操作,所以對page cache的使用很頻繁,所以我們才選擇了盡早的能夠觸發background reclaim這個策略;而如果你的文件操作不頻繁,顯然就沒有必要去盡早的喚醒后臺回收線程。另外一個,作為一個文件服務器,它對page cache的需求是很大的,越多的內存作為page cache,系統的整體性能就會越好,所以我們就沒有必要為了數據的局部性而預留DMA內存,兩相比較肯定是page cache對性能的提升大于數據的局部性對性能的提升;而如果你的文件操作不多的話,那還是打開zone_reclaim的。

    作為一個底層開發人員,我希望能給應用層開發團隊建議合理的內核使用策略。所以很歡迎應用開發團隊在遇到底層的困惑后能夠跟我們一起討論,這樣我也能夠了解應用層的實現,從而能夠給出更合理的建議。

    歡迎大家與我交流各種Linux問題。 --@亞方    
    </div>

     本文由用戶 小米_1900 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
     轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
     本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!