病毒與故障:漫談計算機(jī)軟件的故障應(yīng)對
近期肆虐的新型冠狀病毒,已然成為大眾視野的焦點(diǎn)。筆者,最近趁過年之際也看了一些相關(guān)新聞和書籍,其中,有一本名為卡爾·齊默《病毒星球》讓我印象深刻。當(dāng)然,本文并不是談及新型冠狀病毒和《病毒星球》,而是將故障和病毒進(jìn)行類比,聊一聊計算機(jī)軟件的故障應(yīng)對機(jī)制,而其中關(guān)于病毒相關(guān)科普性的資料和數(shù)據(jù)來自于《病毒星球》一書。
一、故障:潛伏于計算機(jī)軟件的病毒
人鼻病毒作為普通感冒和哮喘的罪魁禍?zhǔn)祝侨祟悘V泛存在的老朋友。鼻病毒巧妙地利用鼻涕來自我擴(kuò)散。人擤鼻涕的時候,病毒會借機(jī)跑到手上,通過手再蹭到門把手和其他手碰過的地方。下次其他人碰到這些地方,病毒就會借機(jī)沾上他們的手,再進(jìn)入他們的身體——大多數(shù)時候也是借道鼻子。鼻病毒能巧妙地讓細(xì)胞對它們打開一扇“小門”,繼而入侵位于鼻腔內(nèi)部、咽喉內(nèi)部或肺臟內(nèi)部的細(xì)胞。在接下來的幾個小時里,鼻病毒利用宿主細(xì)胞,復(fù)制自己的遺傳物質(zhì)和包裹它們的蛋白外殼。隨后這些復(fù)制產(chǎn)生的病毒會從宿主細(xì)胞內(nèi)破壁而出。此外,我們每個人的基因組中攜帶了近 10 萬個內(nèi)源性逆轉(zhuǎn)錄病毒的 DNA 片段,占到人類 DNA 總量的 8%。雖然這類病毒 DNA 中的大多數(shù)都沒用,但我們的祖先也的確“征用”了一些對我們自身有好處的病毒。如果沒有這些病毒,我們甚至沒法出生。在演化史上最近的瞬間,人類脫穎而出,病毒對我們的生存功不可沒。原本就并沒有什么“它們”和“我們”之分——生物在本質(zhì)上只是一堆不斷混合、不斷閃轉(zhuǎn)騰挪的 DNA 而已。因此,鼻病毒在幾千年前就開始讓古埃及人患上感冒,內(nèi)源性逆轉(zhuǎn)錄病毒早在數(shù)千萬年前就入侵了我們靈長類祖先的基因組。(摘自《病毒星球》)
故障也與之類似,它就好似生命體的 DNA 片段纏繞于計算機(jī)軟件中,無法割舍。如今軟件開發(fā)迭代頻繁,我們很難全部排除故障,只能說盡可能多地發(fā)現(xiàn)和解決問題,避免故障發(fā)生在生產(chǎn)環(huán)境導(dǎo)致線上問題。當(dāng)我們遭到病毒感染,細(xì)胞釋放一種名為“細(xì)胞因子”的信號分子,把附近的免疫細(xì)胞都召喚過來。它們讓我們的身體產(chǎn)生炎性反應(yīng),等免疫系統(tǒng)幫我們把體內(nèi)的病毒全部干掉。而在計算機(jī)軟件,我們也會有類似的場景,我們的開發(fā)人員或測試人員一旦確認(rèn)是程序 BUG,就會立即記錄并周知相關(guān)人員進(jìn)行處理與修復(fù),并持續(xù)跟蹤,直至故障解決。
二、聽過很多案例,依然無法解決故障
感冒這么難治,一個原因是它存在形態(tài)多種多樣,由于其基于突變及快速復(fù)制帶來來遺傳多樣性。而面對故障,雖然它的底層導(dǎo)火索可能就只有哪幾種,但是由于技術(shù)的復(fù)雜性和業(yè)務(wù)的復(fù)雜性導(dǎo)致了計算機(jī)軟件的整體復(fù)雜性。
我們知道 NPE (NullPointerExcepion)會給我們帶來巨大災(zāi)難,但是我們在實(shí)際的研發(fā)中經(jīng)常遺忘或忽視。這里,由于沒有對受檢對象進(jìn)行非空判斷導(dǎo)致 NPE 故障。
- public static void npe03(){
- Person person = null;
- System.out.println(person.blog);
- }
下面的示例將會導(dǎo)致 NPE,你發(fā)現(xiàn)了嗎?
- public static void npe01(){
- Integer x = 1;
- Integer y = 2;
- Integer z = null;
- Integer val = false ? x * y : z;
- }
而這個示例,也是非常典型的由 Java 自動裝箱和拆箱導(dǎo)致的 NPE 故障。
- public static void invoke(){
- Long x = null;
- npe02(x);
- }
- public static void npe02(long x){
- System.out.println(x);
- }
再聊一個有意思的故障問題。大家都知道由于死循環(huán)會導(dǎo)致 CPU 100%。但是,導(dǎo)致 CPU 100% 導(dǎo)因是多樣性的,筆者團(tuán)隊(duì)曾經(jīng)遇到一個 JDK 8 的 BUG,它是由于 ConcurrentHashMap 遞歸創(chuàng)建對象擴(kuò)展導(dǎo)致死循環(huán),文章鏈接:《ConcurrentHashMap.computeIfAbsent 死循環(huán)》。
三、故障應(yīng)急機(jī)制:監(jiān)控、告警、預(yù)案
通常情況下,線上故障一旦發(fā)生,其后果一般都比較嚴(yán)重。所以,我們需要盡快解決,降低其帶來的影響和資損。例如,我們團(tuán)隊(duì)之前口號是:“1-5-10”,即一分鐘發(fā)現(xiàn),五分鐘處理,十分鐘解決。那么,如何做到快速的發(fā)現(xiàn)線上故障呢?搭建成熟的監(jiān)控系統(tǒng)就非常重要啦,例如通過 Zabbix 或 Prometheus 監(jiān)控各種基礎(chǔ)設(shè)施(MySQL、Redis、MongoDB、ElasticSearch 等)運(yùn)行情況,以及業(yè)務(wù)系統(tǒng)的運(yùn)行情況,以及 CPU、內(nèi)存、磁盤 I/O、網(wǎng)絡(luò) I/O 等波動情況,還有 GC 情況、binlog 同步情況等等。那么,發(fā)現(xiàn)問題后,就需要通過告警系統(tǒng)根據(jù)業(yè)務(wù)規(guī)則進(jìn)行多渠道(郵件、釘釘、電話)聯(lián)系故障處理人。要快速解決,怎么辦?首先,需要一套完備的日志排查系統(tǒng)(日志聚合 + 鏈路追蹤),此外,還需要對于 JVM 相關(guān)能快速 dump 堆棧信息,對于自助分析有 DevOps 平臺支撐。但是,最主要的還是需要有一套預(yù)案體系。什么是預(yù)案系統(tǒng)?就是針對不同的問題,有一套完整的預(yù)先方案來快速響應(yīng)。例如,某個服務(wù)不可用了,筆者排查發(fā)現(xiàn)是由于進(jìn)程假死了,那么就可以通過預(yù)案里面的執(zhí)行方案執(zhí)行 shell 腳本進(jìn)行快速拉活。再比如,筆者通過監(jiān)控告警感知到某個商家資金異常波動,那么此時通過預(yù)案里面的執(zhí)行方案將其通過動態(tài)開關(guān)將其快速熔斷。
但是,如果這次線上故障沒有應(yīng)急預(yù)案,又比較棘手,怎么辦?別無他法,只能因著頭皮來處理啦。注意的是,故障的評級一般根據(jù)業(yè)務(wù)的影響范圍面而定,事實(shí)上,最本質(zhì)的資金損失,包括直接的資損,例如前段時間某知名電商的免額優(yōu)惠劵導(dǎo)致非常嚴(yán)重的資損;間接的資損,例如挖斷電纜導(dǎo)致整個 app 無法使用,那么,轉(zhuǎn)化成正常的交易額也是大把大把的資損呀。然后呢,過了 2 個小時,故障還沒修復(fù),可能原先定級 P2 的故障就會升級到 P1。
如果發(fā)生最差的情況,就是可能短時間內(nèi)無法解決,那么就很可能需要停服維修了。例如,近期肆虐的新型冠狀病毒導(dǎo)致全國性的封城封路,事實(shí)上,也是因?yàn)槲覀儧]有特效藥(計算機(jī)軟件領(lǐng)域的預(yù)案),也還沒有研制出新藥(計算機(jī)軟件領(lǐng)域的解決方案),所以只能封城封路(計算機(jī)軟件領(lǐng)域的停服)。
四、提早發(fā)現(xiàn)故障 - 故障演練
通常情況下,偶爾感冒會提供我們的免疫力。而在計算機(jī)領(lǐng)域,偶然采取故障演練也可以盡可能確保在線上運(yùn)行的系統(tǒng)沒有缺陷和故障。這里,Netflix 為應(yīng)對不確定性的領(lǐng)域帶來了一種全新的思維模式:混沌工程。事實(shí)上,混沌工程提倡我們正面接受系統(tǒng)一定會存在缺陷和故障,然后我們通過一系列實(shí)驗(yàn)找出可能發(fā)生問題的風(fēng)險點(diǎn),進(jìn)而不斷地加固系統(tǒng)。
故障演練可以模擬 CPU 滿載、殺掉指定進(jìn)程、域名訪問不通、網(wǎng)絡(luò)延遲、網(wǎng)絡(luò)丟包、填充磁盤、磁盤 IO 高等場景,如下所示。
總結(jié)一下,故障就像潛伏于計算機(jī)軟件的病毒,由于技術(shù)的復(fù)雜性和業(yè)務(wù)的復(fù)雜性導(dǎo)致了其排查和解決的困難性,我們可以采取監(jiān)控、告警、預(yù)案,以及故障演練提早發(fā)現(xiàn)故障并解決故障。