https://mp.weixin.qq.com/s/sGS-Kw18sDnGEMfQrbPbVwjava
內核futex的BUG致使程序hang死問題排查
近日,Hadoop的同窗反映,新上的幾臺機器上的java程序出現hang死的現象,查看系統的message記錄,發現一些內存方面的錯誤輸出,懷疑是內存不足致使java程序hang死在gc的過程當中。經排查發現即便是在內存充足的狀況下也會出現程序hang死的現象。mysql
咱們又發現只有這批新上的機器纔出現hang死的問題,以前老機器上一直很正常。排查後發如今老機器上有一個監控腳本,每隔一段時間就會用jstack查看一下java程序的狀態。關了監控腳本後,老機器也出現了hang死的問題。最後咱們發現使用jstack、pstack均可以將原來hang死的程序刷活。linux
後來DBA的同窗也反映,他們的xtrabackup程序也出現了hang死的問題,最後咱們使用GDB對這個備份程序分析後發現,問題的緣由出如今內核的一個BUG上。git
結論github
出現問題的機器上的Linux內核都是linux 2.6.32-504版本,這個版本存在一個futex的BUG。sql
參見:數據庫
https://github.com/torvalds/linux/commit/76835b0ebf8a7fe85beb03c75121419a7dec52f0緩存
這個BUG會致使非共享鎖的程序體會陷入無人喚醒的等待狀態,形成程序hang死。服務器
觸發這個BUG須要具有如下幾個條件:多線程
• 內核是2.6.32-504.23.4如下的版本
• 程序體須要使用非共享鎖的鎖競爭
• CPU須要有多核,且須要有CPU緩存
知足以上條件就有機率觸發這個程序hang死的BUG。
解決方案就是升級到 2.6.32-504.23.4或更高版原本修復此BUG。
下面咱們來看一下,是如何判定問題是由這個BUG引發的。
原理分析
1. 首先要拿到進程ID
咱們要分析的xtrabackup程序的PID是715765
2. 看一下內核調用棧
cat /proc/715765/*/task/stack
發現大多數線程在停在 futex_wait_queue_me 這個內核函數中。
這個函數使當前線程主動釋放CPU進入等待狀態,若沒有被喚醒,就一直停在這個函數中。
也就是說,如今大多數線程都在等其餘資源釋放鎖,下面咱們就須要到用戶態下分析,他們到底在等待什麼鎖。
3. 分析用戶態代碼
gdb attach 715765
對於這種程序hang死的問題,最好的工具仍是gdb,附加到程序上,來獲取的實時狀態信息。
3.1 查看線程信息
首先先看一下在用戶態中線程的狀態。
能夠看到線程大致有兩類等待, pthread_cond_wait 和 __lll_lock_wait。
pthread_cond_wait是線程在等待一個條件成立,這個條件通常由另外一個線程設置;
__lll_lock_wait是線程在等另外一個線程釋放鎖,通常是搶佔鎖失敗,在等其餘線程釋放這個鎖。
3.2 查看每一個線程信息
看到大致有三類線程:
• 拷貝線程:data_copy_thread_func
• 壓縮線程:compress_wokrer_thread_func
• IO線程:io_handler_thread
爲了弄明白這些線程的做用,咱們能夠先了解下xtrabackup的工做原理。
3.3 工做原理說明
mysql數據庫備份中的一個工做就是將數據庫文件拷貝,爲節省空間,能夠經過參數來設置開啓壓縮。
在作實際分析前,咱們先梳理一遍啓用壓縮後,拷貝線程的業務邏輯:
• 拷貝線程會把文件分紅多個小塊,餵給壓縮線程
• 在喂以前,須要經過一個控制鎖來獲取這個壓縮線程的控制權
• 喂完後,會發送一個條件信號來通知壓縮線程幹活
• 而後就依次等每一個壓縮線程將活幹完
• 每等到一個壓縮線程幹完活,就將數據寫到文件中,而後釋放這個壓縮線程控制鎖
下面咱們看一個具體的拷貝線程,咱們從第1個拷貝線程開始,也就是2#線程。
3.4 拷貝線程2# 鎖分析
拷貝線程2# hang死的位置 是 在給第1個壓縮線程發送數據前,加ctrl_mutex鎖的地方
它在等其owner 715800 釋放,而715800 對應的是7#線程
3.5 拷貝線程 7# 鎖分析
咱們看到7# 線程hang死的位置與2號線程是相同,不一樣的是 它是卡在第3個壓縮線程上,且其ctrl_mutex的owner爲空。也就是說沒有與其競爭的線程,它本身就一直在這等。
雖然這個現象很奇怪,但能夠肯定這不是死鎖問題致使的。通常來說只能是內核在釋放鎖時出現問題纔會出現這種空等的狀況。
爲了更完整的還原出當時的場景,咱們須要分析一下到底都有誰有可能釋放壓縮線程的控制鎖。
3.6 拷貝線程控制鎖怎麼釋放
ctrl_mutex對應的是壓縮線程一個控制鎖,擁有這個鎖才能對壓縮線程作相應的操做
在xtrabackup中,大致有四個地方釋放這個鎖:
1. 建立壓縮線程時,會初始化這個鎖,並經過這個鎖啓動線程進入主循環
2. 壓縮線程在運行時, 會使用這個鎖設置啓動狀態(與上面的建立線程對應)
3. 拷貝線程會在往壓縮線程放原始數據時,把持這個鎖,在從壓縮線程拿完數據後,釋放對應鎖
4. 銷燬壓縮線程後,會釋放上面相關的鎖
查看日誌咱們看到,日誌是停在一個壓縮文件的過程當中,且上面完成了屢次文件的壓縮操做;
因此,能夠排除上面的一、二、4這三種狀況;
那麼咱們能夠再作出下面的假設:
前面有一個拷貝線程,取完了幾個壓縮線程的壓縮結果,釋放了這幾個壓縮線程;
這時,7#拷貝線程正好拿到了一、2兩個壓縮線程的控制鎖,往裏放完數據後,開始要拿第3個壓縮線程的控制鎖;
這時前一個拷貝線程並無釋放,因而7#只好在加鎖處等待;
但當前一個拷貝線程釋放第3個壓縮線程鎖的時候,內核並無通知到7#線程,形成其一直在等待。
而7#線程等待的過程當中,也不會釋放其餘已把持的壓縮線程的鎖,形成其餘拷貝線程一直等待其釋放,最後致使整個進程夯死。
到此咱們大概還原了程序hang死的場景,目前來說嫌疑最大的就是內核出現了問題,而當前內核版本正好有一個futex的BUG,咱們來具體看一下這個BUG是不是致使程序hang死的元兇。
4. 內核的futex的BUG分析
先看一下內核futex中的這個BUG,其實很簡單,就是少加了兩行代碼;嚴格點說是在非共享鎖分支上少加了一個mb。
mb又是什麼呢?mb的做用將上下兩部分代碼作一個嚴格的分離,通常叫屏障,主要有兩種屏障:
• 優化屏障:當gcc編譯器從O2級別的優化開始就會對指令進行重排,而mb會在其宏上加一個volatile關鍵字來告訴編譯器禁止與其餘指令重排。
• 內存屏障:如今CPU一般是並行的執行若干條指令,具可能從新安排內存訪問的次序,這種重排或亂序能夠極大地加速程序運行,但也會致使一些須要數據同步的場景緻使讀到髒數據。而mb會使用mfence彙編指令告訴CPU,必需要把前面的指令執行完,才能執行其下面的指令,保證操做同步。
那不加這個mb 會形成什麼實際影響呢?咱們來看futex_wake函數的代碼:
futex_wake函數中會查看hb變量裏有沒有須要被喚醒的鎖,若是沒有就不作喚醒操做。
若沒加 mb,將致使其獲取的數據不一致,有機率將實際有鎖在等待而誤當成沒鎖在等待,形成該喚醒的鎖,錯失惟一一次被喚醒的機會,致使其一直處在等待狀態,最終致使程序夯死!
下面咱們要肯定一件事情就是,當前程序是否命中了這個BUG,也就是說當前鎖是不是非共享鎖。咱們查看pthread的代碼,能夠看到__kind的值決定了其傳給內核的鎖是共享仍是非共享的。
經過gdb能夠看到,__kind的值爲0,必定是非共享鎖;
經過上面的分析,咱們基本能夠得出是內核BUG致使xtracbackup程序的hang死。
最後咱們將內核升到了2.6.32-504.23.4,發現xtrabackup程序能正常運行,而後咱們對hadoop服務器內核也作了升級,發現hang死問題也解決了。
結語
經過上面的分析過程,咱們能夠發現gdb對這種須要實時分析的問題場景特別契合,但通常用來調度用戶態的代碼,內核態的相關信息可能用systemtap等工具更方便一些。
此外,還有一個問題使人困惑,就是爲何使用pstack、jstack、gdb或SIGSTP+SIGCON信號能喚醒hang死的程序?
這裏要說明的一點就是jstack、gdb、pstack的原理都是經過內核的SIGTRAP等一系列調試信號來,抓取信息或調試程序的;
因此,這個問題的本質是爲何信號能喚醒程序?
咱們看到出現這種夯死現象的程序,大多線程都會停在 內核的 futex_wait_queue_me 這個函數處;而這個函數,使用 TASK_INTERRUPTIBLE 來設置本身的狀態,表示本身主動想放棄CPU,但能夠被中斷、信號或其餘程序喚醒;並在下面調用 schedule 內核調度方法,主動通知內核放棄本身的CPU。
因此,咱們從外界最簡單的就是經過向其發送信號,來喚醒那些可能永遠等待的線程,讓程序跑起來。