Java线上问题排障:Linux内核bug引发JVM死锁导致线程假死
?Java本質上還是離不開操作系統,一來Java源碼是用C/C++實現的,二來java進程還是需要依附于操作系統和硬件資源,有時候一些問題是操作系統級別導致的,下面的整個事件是源自一則真實的線上案例。
?
過程:
JVM死鎖導致線程不可用,然后會瞬間起N個線程,當然起再多也是不可用的,因為需要的對象發生死鎖,然后耗盡文件句柄導致外部請求也就是TCP連接無法建立產生拒絕服務,看起來就像線程假死了一樣,不過巧合的是jstack之后就會恢復。
?
問題升級:
futex.c的bug->JVM死鎖->起更多的線程->達到線程上限->新的請求無線程可以使用->拒絕服務
?
原因:
是Linux內核某個switch分支缺少memory barrier的正確處理,導致外部應用如JVM的lock被錯誤鎖住;一般jstack連后就恢復,當然你線上不能老是這樣是不是,必須徹底解決這個問題。
?
解決辦法:
方法一:上層解決替換中間件類庫 ,比如httpclient的(前提是你是由此觸發的)。
方法二:下沉解決方案前面已經說了給Linux內核打patch或者升級內核到比較穩定的新版本。
?
內存屏障(英語:Memory barrier),也稱內存柵欄,內存柵障,屏障指令等,是一類同步屏障指令,是CPU或編譯器在對內存隨機訪問的操作中的一個同步點,使得此點之前的所有讀寫操作都執行后才可以開始執行此點之后的操作。 大多數現代計算機為了提高性能而采取亂序執行,這使得內存屏障成為必須。
關于內存屏障參考:User-space RCU: Memory-barrier menagerie?https://lwn.net/Articles/573436/
?
先看linux-2.6.33.1的代碼\linux-2.6.33.1\linux-2.6.33.1\kernel\futex.c
然后再看Linus的修復記錄:?
https://github.com/torvalds/linux/commit/76835b0ebf8a7fe85beb03c75121419a7dec52f0
很清楚的看到這個switch被加了default,以前是沒有這個所以導致死鎖的。
/** Take a reference to the resource addressed by a key.* Can be called while holding spinlocks.**/
static void get_futex_key_refs(union futex_key *key)
{if (!key->both.ptr)return;switch (key->both.offset & (FUT_OFF_INODE|FUT_OFF_MMSHARED)) {case FUT_OFF_INODE:ihold(key->shared.inode); /* implies MB (B) */break;case FUT_OFF_MMSHARED:futex_get_mm(key); /* implies MB (B) */break;default:smp_mb(); /* explicit MB (B) */}
} 
v3.18版修復?:?
?
?
futex: Ensure get_futex_key_refs() always implies a barrierCommit b0c29f7 (futexes: Avoid taking the hb->lock if there's
nothing to wake up) changes the futex code to avoid taking a lock when
there are no waiters. This code has been subsequently fixed in commit
11d4616 (futex: revert back to the explicit waiter counting code).
Both the original commit and the fix-up rely on get_futex_key_refs() to
always imply a barrier.However, for private futexes, none of the cases in the switch statement
of get_futex_key_refs() would be hit and the function completes without
a memory barrier as required before checking the "waiters" in
futex_wake() -> hb_waiters_pending(). The consequence is a race with a
thread waiting on a futex on another CPU, allowing the waker thread to
read "waiters == 0" while the waiter thread to have read "futex_val ==
locked" (in kernel).Without this fix, the problem (user space deadlocks) can be seen with
Android bionic's mutex implementation on an arm64 multi-cluster system.Signed-off-by: Catalin Marinas <catalin.marinas@arm.com>
Reported-by: Matteo Franchin <Matteo.Franchin@arm.com>
Fixes: b0c29f7 (futexes: Avoid taking the hb->lock if there's nothing to wake up)
Acked-by: Davidlohr Bueso <dave@stgolabs.net>
Tested-by: Mike Galbraith <umgwanakikbuti@gmail.com>
Cc: <stable@vger.kernel.org>
Cc: Darren Hart <dvhart@linux.intel.com>
Cc: Thomas Gleixner <tglx@linutronix.de>
Cc: Peter Zijlstra <peterz@infradead.org>
Cc: Ingo Molnar <mingo@kernel.org>
Cc: Paul E. McKenney <paulmck@linux.vnet.ibm.com>
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org> 
futex:確保get_futex_key_refs()始終隱含屏障
提交b0c29f7(futexes:如果有的話,避免使用hb-> lock沒有什么可以喚醒的)
更改futex代碼以避免在什么時候鎖定沒有waiter。
此代碼隨后在提交中得到修復11d4616(futex:恢復顯式waiter計數代碼)。
 原始提交和修復都依賴于get_futex_key_refs()總是意味著一個障礙。
但是,對于私有futexes,switch語句中沒有任何一種情況
 將觸發get_futex_key_refs()并且函數完成
 檢查“waiter”之前需要的內存屏障futex_wake() - > hb_waiters_pending()。
結果是一場比賽,線程在另一個CPU上的futex上等待,允許waker線程讀取“waiters == 0”,而waiter線程讀取“futex_val ==鎖定“(在內核中)。
如果沒有此修復程序,可以看到問題(用戶空間死鎖)在arm64多集群系統上實現Android bionic的互斥鎖。
?
下面是這個問題最初的發現和修復的討論,是ARM公司的人員發現的。
?https://lore.kernel.org/patchwork/patch/508701/
?參考知乎上關于這個問題的討論,類似的情況:
https://www.zhihu.com/search?type=content&q=jvm%E5%81%87%E6%AD%BB
?
https://ma.ttias.be/linux-futex_wait-bug/
?
想自己看看內核源碼可以去:
https://mirrors.edge.kernel.org/pub/linux/kernel/
http://mirrors.163.com/kernel/linux/kernel/
?
?
總結
以上是生活随笔為你收集整理的Java线上问题排障:Linux内核bug引发JVM死锁导致线程假死的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: Flink 基本原理与生产实践分享【入门
 - 下一篇: 美国电影爆炸后抽根烟回头的