一行代碼引發(fā)恐懼,深思提高線上代碼質(zhì)量的方法
我工作的前 5 年,都是從事基礎(chǔ)系統(tǒng)研發(fā)相關(guān)的工作,做過后臺的接入層、后臺的存儲系統(tǒng)、RPC 框架。
說來不怕你笑話,那個時期,我對代碼一直有一種恐懼感。這種恐懼是怎么來的呢?且讓我慢慢說來。
我們所構(gòu)建的基礎(chǔ)系統(tǒng),都是使用在億級甚至十億級用戶產(chǎn)品的業(yè)務(wù)系統(tǒng)之上的。
從客戶端(前端)到后臺業(yè)務(wù)邏輯層,再到基礎(chǔ)架構(gòu)層,所寫的代碼是跑在整個調(diào)用鏈路的后端。你可以認為,幾乎每個用戶的每個請求都會跑到我們寫的那部分代碼。
這個對系統(tǒng)帶來的影響是:
- 代碼出問題后,影響的用戶范圍會很大。
- 在這億級甚至十億級用戶量的情況下,每天所帶來的請求可能是千億級,萬億級的。
在如此龐大請求量的情況下,幾乎各種奇葩的異常,你都會遇到,代碼要極其的健壯,一個小異常沒處理好就會帶來大麻煩。
這讓我想起來,我們故障時候的情形。幾年前,我們每做完一次版本變更,晚上基本都會睡不好,擔心變更的代碼有問題。
對手機的報警短信特別敏感,一有風吹草動,立馬就會打開電腦 VPN 看看,即使是在深夜凌晨的時候。
我自己有個習(xí)慣,每次變更完,都要間隔幾個小時去看看監(jiān)控曲線和日志,看看有沒有異常的苗頭,一旦發(fā)現(xiàn)有不對勁地方,就會立即著手排查,直到確保沒有問題為止。
不過即使如此,還是不可避免的會出現(xiàn)問題。半夜兩三點的時候,你的手機突然響起,報警語音機器人跟你說,你有一個重要的監(jiān)控曲線出現(xiàn)異常,請查看。
然后你的血壓立馬升高,心跳加速,你從床上,一躍而起,打開電腦,連上公司的 VPN,立馬著手排查起來。
幾分鐘后,QA(質(zhì)量工程師)電話過來,告知你,這個故障目前已經(jīng)上報到部門故障系統(tǒng),目前影響的用戶有 XXX 的數(shù)量,請你加快處理的速度,然后你的心跳再次加速。
半小時后,終于有了眉目,這時,你的 Leader 電話過來,詢問你是怎么回事,大概還需要多長的時間才能處理完畢。待你語焉不詳?shù)鼗貜?fù)完你的 Leader,你又開始埋頭,一行行的排查故障。
一個小時后,你終于將問題定位出來,執(zhí)行了故障處理方案,例如回滾新的代碼,或者屏蔽某些機器等。你才終于有了喘息的時間。
(PS:這里正確的流程是,出問題后,立馬回滾代碼,但存儲系統(tǒng)因為數(shù)據(jù)的關(guān)系,在沒有確定原因前不太敢回滾,怕對數(shù)據(jù)有影響。)
你趕緊爬上床去,睡上 2-3 個小時,因為第二天還要早起,趕到公司,去處理故障的后遺癥,數(shù)據(jù)損壞和數(shù)據(jù)錯亂。
那個時期,我們寫代碼都是特別小心的,變更,更是極度的謹慎。所以使得自己對代碼變更有了一種焦慮和恐懼的心理。至少在那時候,寫代碼不是一件輕松的事情。
這個事情,我現(xiàn)在回過頭來看。你可以認為有一部分是人的原因,但仔細的想想,寫代碼不出 Bug ,幾乎也是極難做到,所以這里在研發(fā)流程上,其實也是有缺失的。
前期因為業(yè)務(wù)發(fā)展太快,團隊的整體人力跟不上,所以,一開始很多流程,都是很原始的,那時候,是想做但客觀條件不允許。
后來,業(yè)務(wù)穩(wěn)定了,流程就規(guī)范了不少。比如引入了 Coverity 的代碼檢查,也推行過測試用例覆蓋,持續(xù)集成等。
但最終,并不是所有的流程都延續(xù)了下去。比如,代碼測試用例覆蓋,有的團隊到后面就放棄了,需求變化太快,測試用例成本太高。
Coverity 倒是自動化程度高,沒啥人力投入,執(zhí)行了下來。但我相信不是所有的公司,所有的團隊,都會有這種規(guī)范的流程。一個是研發(fā)流程成熟度建設(shè)的問題,但除此,還有成本,業(yè)務(wù)迭代速度。
在互聯(lián)網(wǎng),產(chǎn)品高速迭代的時候,產(chǎn)品都還沒有存活下來,成熟流程就更不太可能有了。
綜合來看,一種規(guī)范,但相對較重的研發(fā)流程的建立,應(yīng)該也是根據(jù)具體情況而定的。
需要考慮產(chǎn)品的形態(tài),產(chǎn)品迭代的速度,團隊的人力預(yù)算成本,產(chǎn)品的生命周期等等。
當然,無論怎么說,反正這不是個人可以決定的事情,如果你所在的團隊有完善的研發(fā)流程,那是好的事情,但如果沒有那么完善,自己又能夠做些什么呢?
我的經(jīng)驗來看,以下的一些措施,對于個人而言也有不錯的效果。
測試驅(qū)動的開發(fā)(TDD)
有段時間,因為業(yè)務(wù)高速發(fā)展,對性能的要求不斷提高,存儲模型也跟隨著不斷迭代改進,所以那段時間的代碼修改是比較多的,那個時間的焦慮感也特別重。
我記得是在 《重構(gòu):改善既有代碼的設(shè)計》中了解到 TDD 的。簡單來說, 就是先構(gòu)建測試用例,再開始寫你的功能代碼。
在設(shè)計測試用例之前,你需要先定義好模塊對外的接口,包括接口的種類,參數(shù),返回值等。
然后,你針對定義好的接口,編寫測試用例。這過程中,你可能會發(fā)現(xiàn)接口設(shè)計不合理的地方,也需要隨著修改。
待你測試用例寫完,基本你的接口也被修改的比較好了,所以 TDD 還能改善你的接口設(shè)計。
后續(xù)再為每個接口實現(xiàn)特定的代碼邏輯。我當時將這種方法運用到了一個磁盤存儲引擎中,發(fā)現(xiàn)相當不錯。我特地花了一周左右的時間寫測試用例。
后面,每天實現(xiàn)部分的功能后,都立馬跑測試用例,每次跑完通過,你的心里都有穩(wěn)的一 B 的感覺。有種媽媽再也不擔心我寫的代碼有 Bug,被老板叼,導(dǎo)致扣工資了。
因為有了完善的測試用例,而且隨著你測試用例不斷的增加和覆蓋,你的信心會越來越足,焦慮自然減少了很多。
不過這種方式,比較適合底層的系統(tǒng)和核心穩(wěn)定的系統(tǒng)。對于需求多變的系統(tǒng),構(gòu)建測試用例的人力付出太大,而且需求一變,已有的測試用例可能失效,導(dǎo)致投入產(chǎn)出比不夠高。
灰度發(fā)布
簡單來說,就是一個特性要上線的時候,不是一下就開放給所有的用戶使用。有點像產(chǎn)品上的內(nèi)測,只不過是用在技術(shù)上。
比如我新增加了一個產(chǎn)品需求,例如就微信里面的 “看一看”入口,不是一開始就對所有用戶開放的。
首先會上線一個新的客戶端版本,代碼邏輯已經(jīng)預(yù)埋,但設(shè)計了一個開關(guān),對所有用戶都是關(guān)閉的。
前期,可能會找個千分之一,甚至萬分之一的用戶(隨機或者特定的用戶群體),讓他們使用。
在這過程中,收集各種 Log,監(jiān)控,用戶的反饋,來確認和 Fix 系統(tǒng)存在的各種問題。
一般經(jīng)過兩三周后,如果沒有大問題,就會進一步的放開使用的用戶。比如變成百分之一,十分之一,一直迭代,直至覆蓋全部用戶。
灰度這個思想,在互聯(lián)網(wǎng)是特別常用的??蛻舳?,前端,后臺都可以使用。比如后臺,上線一個新修改后,也不是一下就開放給所有用戶。
而是按照某種規(guī)則,例如以 QQ 用戶為例,可能是這種規(guī)則:計算 Hash 值(QQ 號) % 1000 <= 灰度用戶的比例(取 0 --- 1000)。
放量的最小力度就是千分之一,被灰度到的用戶,看到新功能,沒灰度到的用戶不受影響。
這招用在新功能,用在系統(tǒng)優(yōu)化,代碼重構(gòu)上都很不錯。付出的額外成本不大,有的公司有自研的灰度系統(tǒng)。
沒有的話,在重大且沒有把握的功能上,自己加上幾行灰度控制代碼也不難。
監(jiān)控和 Log
監(jiān)控和 Log 不是什么新鮮的東西。工作第一年,我們的技術(shù)總監(jiān)在一次會議上跟我們說:你寫完的代碼是死的,只有在線上跑的代碼是活的。
監(jiān)控和 Log(特別是監(jiān)控),就像是你代碼的體征信息,隨時反應(yīng)著你代碼在實際環(huán)境中的運行情況,要高度的重視。
這段話,在后面,我深有感觸。通過完善設(shè)計的監(jiān)控和 Log,預(yù)先發(fā)現(xiàn)了很多的問題,也避免了很多,或代碼 Bug,或系統(tǒng)設(shè)計缺陷導(dǎo)致重大故障。
后面,監(jiān)控和 Log 的設(shè)計,也成了我們方案設(shè)計的一部分。一般都會在方案后,加上必須的監(jiān)控的點和 Log 點,例如請求數(shù),成功數(shù),失敗數(shù),各種異常數(shù),極端邏輯執(zhí)行次數(shù)等等。
你應(yīng)該要意識到監(jiān)控和 Log 的重要性,而且應(yīng)該要花時間特別地設(shè)計。經(jīng)過良好設(shè)計的監(jiān)控和 Log,能發(fā)揮的價值,是那種憑感覺隨便加的監(jiān)控 Log 不可比擬的。
雙寫,雙讀驗證
這招,新業(yè)務(wù)代碼用的不多。更多用于基礎(chǔ)系統(tǒng)或者核心系統(tǒng)的優(yōu)化和重構(gòu)上面。
而且有前置條件,需要一個操作可以重復(fù)執(zhí)行(例如只讀操作和冪等的數(shù)據(jù)操作)。
簡單來說,就是將新舊代碼,劃分為兩個流程(兩個接口),上線到實際環(huán)境,然后在同個模塊里面調(diào)用。
一個請求進來后,兩個流程分別執(zhí)行一次,逐字節(jié)做對比(例如 Memcmp)新舊流程的結(jié)果。新流程的結(jié)果只用于對比,返回得依舊是舊流程的結(jié)果,所以不影響線上業(yè)務(wù)。
如果對比失敗,就可能存在異常,要查找并解決,在實際環(huán)境跑了幾天后,都沒問題,就可以采用灰度的方式,進一步放量。不過,一般業(yè)務(wù)不常使用,在基礎(chǔ)系統(tǒng)上使用比較多,這里就不展開了。
另外,對于客戶端,還有熱補丁機制,客戶端 Log 收集系統(tǒng)等。不過這種需要的開發(fā)量比較大,一個人不一定可以搞定,可能需要有個小團隊來完成。
最后
軟件工程是個龐大的話題,我也沒能力論述這么大的話題。這里給大家講了個以前的故事,并且分享了我常用的一些低成本,但可以提高線上代碼質(zhì)量的方法,給大家參考參考。大家有好的做法,也歡迎在留言里分享出來。