Linux內核分析:頁回收導致的cpu load瞬間飆高的問題分析與思考 摘要 前言 Linux系統出現問題,我們該如何去分析 部署工具,蒐集現場信息 抓取現場信息:CPU到底在幹什麼 背後的知識 root cause知道了:解決問題 繼續來看另外一個問題 總結&思考 :機制與策略

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

搜索團隊的服務器前段時間頻繁出現CPU load很高( 比如load average達到80多 )的情況,正所謂術業有專攻,搜索的兄弟們對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]

而正常的處於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

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

    ps:讓內核panic的目的是爲了在panic時dump出現場信息。
  1. 在內核態,出了進程上下文外,還有中斷上下文,( PS:在我們用的2.6的內核上,中斷仍然還是使用的內核棧,它沒有自己獨立的棧空間 )。中斷也可能有異常,比如長時間被關中斷。 中斷長時間被關閉,這個現象叫做hardlockup。
    針對hardlockup,內核也有監測機制,是NMI watchdog
    。可以通過/proc/interrupts來看系統是否使能了NMI watchdog。

$ cat /proc/interrupts | grep NMI
NMI : 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/cmdline
ro 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 Crash
03000000-0b4fffff : Crash kernel

接着去看kdump內核服務是否已經開啓:

$ sudo service kdump status
Kdump is operational

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

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

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

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

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

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

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

首先sysrq得是使能的:

$ cat /proc/sys/kernel/sysrq
1

然後讓內核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 
 ...
 

大致的意思就是,現在所有需要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的關係。

buffer_cache.png

舉個簡單的例子,比如我們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_reclaim.png

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

direct_reclaim.png

所以說,我們應該要避免做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的內存。凡事有利就有弊,這樣有好處自然也可能會帶來一些壞處。

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

於是我們增大這個值(比如增大到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

我們可以看到,此時它的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
...

同時它的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

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

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

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

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

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

歡迎大家與我交流各種Linux問題。 --@亞方    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章