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