一個疑難故障,坑了我半年青春
作者介紹
林偉壕,網易遊戲資深運維工程師。現任職於網易遊戲,從事遊戲運維相關工作;曾就職於中國電信,負責資料網路維護、網路安全防禦等工作。深入研究Linux運維、虛擬化等,現致力於企業級網路安全防護自動化體系構建。
相對物理環境,虛擬化環境更加錯綜複雜。之前弄KVM虛擬化時經常遇到好多次莫名其妙的網路故障,查出來的原因要麼是作業系統核心bug,要麼是KVM與作業系統核心版本不相容,最後是通過升級作業系統核心或者KVM版本修復了。沒想到,轉型到Docker後,又重蹈覆轍了。
本文將介紹一個困擾筆者近半年的虛擬化環境下的疑難故障,最後排查出來的故障原因和修復手段也讓人啼笑皆非。並非因為這個過程有多複雜,而是分享一個心理歷程,思考在遇到故障時如何兼顧業務和技術,如何正確使用搜索引擎。
故障現象
我們有一套高效能代理叢集,之前內測階段執行穩定,結果等正式上線後不到半個月,提供代理服務的宿主突然接二連三宕機,導致宿主上的所有服務全部中斷。
故障分析
故障時宿主直接宕機,無法遠端登入,機房現場敲鍵盤業務反應。由於宿主syslog已接入ELK,所以我們採集了當時宕機前後的各種syslog。
報錯日誌
通過檢視宕機宿主的syslog發現機器宕機前有以下kernel報錯:
Nov 12 15:06:31 hello-worldkernel: [6373724.634681] BUG: unable to handle kernel NULL pointer dereferenceat 0000000000000078
Nov 12 15:06:31 hello-world kernel: [6373724.634718] IP: []pick_next_task_fair+0x6b8/0x820
Nov 12 15:06:31 hello-world kernel: [6373724.634749] PGD 10561e4067 PUDffdb46067 PMD 0
Nov 12 15:06:31 hello-world kernel: [6373724.634780] Oops: 0000 [#1] SMP
顯示訪問了核心空指標後觸發系統bug,然後引起一系列呼叫棧報錯,最後宕機。
為進一步分析故障現象,首先需要理解這套高效能代理叢集的架構。
架構介紹
單個節點,是在萬兆網絡卡的宿主機上跑Docker容器,然後在容器中跑Haproxy例項,每個節點、例項的配置資訊、業務資訊都託管在排程器上。
特別之處在於:宿主使用Linux Bridge直接給Docker容器配置IP地址,所有對外服務的IP,包括宿主自己的外網IP都綁在Linux Bridge上。
應用介紹
每臺宿主的作業系統、硬體、Docker版本全部一致,其中作業系統和Docker版本如下:
[作業系統]
System : Linux
Kernel : 3.16.0-4-amd64
Version : 8.5
Arch : x86_64[Docker版本]
Docker version 1.12.1, build 6b644ec
初步分析
該叢集的宿主配置一致,故障現象也一致,疑點有三個:
1、Docker版本與宿主核心版本不相容
三臺宿主的環境本來一致,但1臺穩定跑服務2個月才宕機,1臺跑服務1個月後宕機,另外1臺上線跑服務一週便會宕機。
發現每臺宿主除了宕機的異常日誌,平時也有相同報錯日誌:
time=”2016-09-07T20:22:19.450573015+08:00″level=warning msg=”Your kernel does not support cgroup memory limit”
time=”2016-09-07T20:22:19.450618295+08:00″ level=warningmsg=”Your kernel does not support cgroup cfs period”
time=”2016-09-07T20:22:19.450640785+08:00″ level=warningmsg=”Your kernel does not support cgroup cfs quotas”
time=”2016-09-07T20:22:19.450769672+08:00″ level=warningmsg=”mountpoint for pids not found”
根據上面提示,應該是作業系統核心版本對該版本的Docker不支援某些功能所導致。不過在搜尋引擎上搜索這並不影響Docker的功能,更不加影響系統穩定性。
比如:
time=”2017-01-19T18:16:30+08:00″level=error msg=”containerd: notify OOM events” error=”openmemory.oom_control: no such file or directory”
time=”2017-01-19T18:22:41.368392532+08:00″level=error msg=”Handler for POST /v1.23/containers/338016c68da6/stopreturned error: No such container:
338016c68da6″
是Docker 1.9以來就有的問題,1.12.3修復了。參考https://github.com/docker/docker/ issues/24211
比如Github上有人回覆:
“I have been update my docker from 1.11.2 to 1.12.3, This issue is fixed.
BTW, this error message can be ignored, it should really just be a warning.”
但這裡所說的都只是v1.12.2版本就能修復的問題,我們升級Docker版本後發現宕機依舊。
於是,我們接著通過各種Google確認了很多與我們存在相同故障現象的問題,初步確認故障與Docker的相關性:
http://serverfault.com/questions/709926/bug-unable-to-handle-kernel-null-pointer-dereference-at-on-google-compute-eng
https://support.mayfirst.org/ticket/10872
又根據以下官方issue初步確認Docker版本與系統核心版本不相容可引發宕機的關聯性:
https://github.com/docker/docker/issues/19910
接著,通過官方的changelog和issue確認宿主所使用Docker版本與系統核心版本不相容問題:
https://github.com/docker/docker/blob/v1.12.2-rc1/CHANGELOG.md
出於嘗試心理,我們把Docker版本升級到1.12.2後,未出意外仍出現宕機。
2.使用Linux bridge方式改造宿主網絡卡可能觸發bug
找了那臺宿主跑服務一週就會宕機的宿主,停止執行Docker,只改造網路,穩定跑了一週未發現異常。
3.使用pipework給Docker容器配置IP可能觸發bug
由於給容器分配IP時我們採用了開源的pipework指令碼,因此懷疑pipework的工作原理存在bug,所以嘗試不使用pipework分配IP地址,發現宿主仍出現宕機。
於是初步排查陷入困境,眼看著宿主每月至少宕機一次,非常鬱悶。
故障定位
因為還有線上業務在跑,所以沒有貿然升級所有宿主核心,而是期望能通過升級Docker或者其它熱更新的方式修復問題。但是不斷的嘗試並沒有帶來理想中的效果。
直到有一天,在跟一位對Linux核心頗有研究的老司機聊起這個問題時,他三下五除二,Google到了幾篇文章,然後提醒我們如果是這個 bug,那是在 Linux 3.18 核心才能修復的。
參考:
- https://lists.gt.net/linux/kernel/2256803
- https://lkml.org/lkml/2014/2/15/217
- https://github.com/docker/docker/issues/21081
- https://github.com/torvalds/linux/commit/eeb61e53ea19be0c4015b00b2e8b3b2185436f2b
原因:
從sched: Fix race between task_group and sched_task_group的解析來看,就是parent 程序改變了它的task_group,還沒呼叫cgroup_post_fork()去同步給child,然後child還去訪問原來的cgroup就會null。
不過這個問題發生在比較低版本的Docker,基本是Docker 1.9以下,而我們用的是Docker1.11.1/1.12.1。所以儘管報錯現象比較相似,但我們還是沒有100%把握。
但是,這個提醒卻給我們打開了思路:去看核心程式碼,實在不行就下掉所有業務,然後全部升級作業系統核心,保持一個月觀察期。
於是,我們開始啃Linux核心程式碼之路。先檢視作業系統本地是否有原始碼,沒有的話需要去Linux kernel官方網站搜尋。
“`
apt-cache search linux-image-3.16.0-4-amd64
apt-get source linux-image-3.16.0-4-amd64
“`
下載了原始碼包後,根據報錯syslog的內容進行關鍵字匹配,發現了以下內容。由於我們的機器是x86_64架構,所以那些avr32/m32r之類的可以跳過不看。結果看下來,完全沒有可用資訊。
/kernel/linux-3.16.39#grep -nri “unable to handle kernel NULL pointer dereference” *
arch/tile/mm/fault.c:530: pr_alert(“Unable to handlekernel NULL pointer dereference\n”);
arch/sparc/kernel/unaligned_32.c:221: printk(KERN_ALERT “Unable to handle kernel NULL pointerdereference in mna handler”);
arch/sparc/mm/fault_32.c:44: “Unable to handle kernel NULL pointer dereference\n”);
arch/m68k/mm/fault.c:47: pr_alert(“Unable tohandle kernel NULL pointer dereference”);
arch/ia64/mm/fault.c:292: printk(KERN_ALERT “Unable tohandle kernel NULL pointer dereference (address %016lx)\n”, address);
debian/patches/bugfix/all/mpi-fix-null-ptr-dereference-in-mpi_powm-ver-3.patch:20:BUG:unable to handle kernel NULL pointer dereference at (null)
最後,我們還是下線了所有業務,將作業系統核心和Docker版本全部升級到最新版。這個過程有些艱難,當初推廣這個系統時拉的廣告歷歷在目,現在下線業務,回爐重造,挺考驗勇氣和決心的。
故障處理
下面是整個故障處理過程中,我們進行的一些操作。
升級作業系統核心
對於Docker 1.11.1與核心4.9不相容的問題,可以刪除原有的Docker配置,然後使用官方指令碼重新安裝最新版本Docker
“`
/proxy/bin#ls /var/lib/dpkg/info/docker-engine.
docker-engine.conffiles docker-engine.md5sums docker-engine.postrm docker-engine.prerm
docker-engine.list docker-engine.postinst docker-engine.preinst
#Getthe latest Docker package.
$curl -fsSL https://get.docker.com/ | sh
#啟動
nohupdocker daemon -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock-s=devicemapper&
“`
這裡需要注意的是,Docker安裝方式在不同作業系統版本上不盡相同,甚至相同發行版上也有不同,比如原來我們使用以下方式安裝Docker:
“`
apt-get install docker-engine
“`
然後在早些時候,還有使用下面的安裝方式:
“`
apt-get install lxc-docker
“`
可能是基於原來安裝方式的千奇百怪導致問題叢出,所以Docker官方提供了一個指令碼用於適配不同系統、不同發行版本Docker安裝的問題,這也是一個比較奇怪的地方,所以Docker生態還是蠻亂的。
驗證
16:44:15 up 28 days, 23:41, 2 users, load average: 0.10, 0.13, 0.15
docker 30320 1 0 Jan11 ? 00:49:56 /usr/bin/docker daemon -p/var/run/docker.pid
Docker核心升級到1.19,Linux核心升級到3.19後,保持執行至今已經2個月多了,都是ok的。
總結
這個故障的處理時間跨度很大,都快半年了,想起今年除夕夜收到伺服器宕機報警的情景,心裡像打破五味瓶一樣五味雜陳。期間問過不少研究Docker和作業系統核心的同事,往作業系統核心版本等各個方向進行了測試,但總與正確答案背道而馳或差那麼一點點。最後發現原來是處理得不夠徹底,比如升級不徹底,環境被汙染;比如升級的版本不夠新,填的坑不夠厚。回顧了整個故障處理過程,總結下來大概如下:
迴歸運維的本質
運維要具有預見性、長期規劃,而不能僅僅滿足於眼前:
- 應急預案:針對可能系統上線後可能發生的故障型別進行總結,並提供應急預案。
- 搶通業務:優先搶通業務,再處理故障。
- 應用版本選擇等技術選型問題:在環境部署和應用選型時需要特別注意各種版本,最好採用社群通用或者公司其他同學已經測試或驗證可行的版本。
- 作業系統核心:要合理升級核心,只有定位到確定版本存在的問題,才能有針對性的升級核心版本,不然一切徒勞。
- 在我們原來的設計中,不同使用者排程器針對同一個容器同時操作沒有加鎖機制,也沒有按照對源判斷原則,也曾出現過遷移失敗的情況。遷移時判斷遷往的目的地址是否就是本地地址,如果是本地地址應該拒絕操作的。這個問題不知你是否覺得眼熟。我倒是發現,很多人程式開發過程中,就經常不對輸入源或者操作的源狀態進行判斷,結果出現了各種bug。
Google的能力
在處理這個故障的過程中,會發現不同人使用Google搜出來的東西並不一樣,為什麼呢?我覺得這就是搜尋引擎槽點滿滿,或者說靈活之處。像這次的故障,我用Linux Docker Unable to handle kernel NULL pointer dereference去搜索,與別人用”Unable to handle kernel NULL pointer dereference”結果就不同。原因在於增加了””之後,搜尋更加精確了。關於Google的正確開啟方式,建議參考:
- https://www.zhihu.com/question/20161362?rf=19798921
文章來自微信公眾號:DBAplus社群