揭秘軟件開發(fā)中的達(dá)摩克利斯之劍
為什么你的程序總是出現(xiàn) bug?
憑什么讓改 bug 占據(jù)了你大部分的時間?
看完本文,保證你能設(shè)計出更穩(wěn)定的程序,擺脫 bug 的纏繞,做項目更安心!

記得我在學(xué)校的時候,做的那些項目,不是為了應(yīng)付課程作業(yè),就是為了參加比賽時展示用,因此對項目的質(zhì)量要求非常低。
到底有多低呢?
大部分的項目,只要基本的功能可以使用,就算完成了,完全不考慮任何的異常情況。甚至只要能成功運(yùn)行一次,讓我截幾張圖放到 PPT 或者實(shí)驗(yàn)報告里,足夠向老師交差或者應(yīng)付比賽答辯就行。
那項目出現(xiàn) bug 怎么辦呢?
如果測試的時候發(fā)現(xiàn)有些功能不可用,那很簡單,不管他,直接 PS 一張正常運(yùn)行的圖就行。
如果比賽的時候發(fā)現(xiàn)有些功能不可用,那也很簡單,把鍋甩給 “現(xiàn)場網(wǎng)絡(luò)不好” 就行。

但是,這些 “小技巧” 在企業(yè)中是行不通的,企業(yè)級項目必須為企業(yè)帶來實(shí)際的價值,容不得半點(diǎn)馬虎和欺騙。
我第一次進(jìn)入企業(yè)實(shí)習(xí)時,還保留著自己在學(xué)校開發(fā)項目的狼性,只要能夠完成基本功能就行,保證以最快的速度完成開發(fā)。
有一天,當(dāng)我洋洋得意準(zhǔn)備早點(diǎn)下班時,測試同學(xué)走過來跟我說。
“喂,你的程序有 bug,這里用戶下單怎么金額是負(fù)的?”

對于我一個初入職場的小白,這是人生中第一次有人說我的代碼有 bug,我有問題,我不對勁。
當(dāng)時,我腦海的第一個念頭竟然是怎么把這個 bug 糊弄過去,而不是怎么去更正!看來我已經(jīng)養(yǎng)成了非常不好的習(xí)慣。
那之后幾天,我又連續(xù)收到了測試提出的多個 bug,然后將他們一個個改正。如果將這樣一個漏洞百出的程序發(fā)布上線,帶來的損失是不可估量的,現(xiàn)在想想仍心有余悸。

這件事之后,我意識到,在企業(yè)中開發(fā)項目,不能只追求開發(fā)時的效率,還要注重項目的穩(wěn)定性,否則帶來的額外返工時間遠(yuǎn)比開發(fā)時節(jié)省的時間要長,而且會影響同事對你的看法。如果將開發(fā)時產(chǎn)生的 bug 遺留到線上,后果更是不堪設(shè)想!
后來,在字節(jié)跳動和騰訊這兩家大公司工作后,我進(jìn)一步認(rèn)識到了項目穩(wěn)定性有多重要,并且積累了更多只有在大公司才能學(xué)到的提升項目穩(wěn)定性的經(jīng)驗(yàn)。
我總結(jié)了 10 個開發(fā)中通常不會考慮到的風(fēng)險點(diǎn),以及 16 個減少風(fēng)險、提升項目穩(wěn)定性的方法,分享給大家~

在分享這些之前,先講個故事。
達(dá)摩克利斯之劍
古希臘傳說中,達(dá)摩克利斯是公元前 4 世紀(jì)意大利敘拉古的僭主(古希臘統(tǒng)治者獨(dú)有的稱號)狄奧尼修斯二世的朝臣,他非常喜歡奉承狄奧尼修斯。
他奉承道:“作為一個擁有權(quán)力和威信的偉人,狄奧尼修斯實(shí)在很幸運(yùn)。”
于是狄奧尼修斯提議與他交換一天的身份,那他就可以嘗試到首領(lǐng)的命運(yùn)。
在晚上舉行的宴會里,達(dá)摩克利斯非常享受成為國王的感覺。當(dāng)晚餐快結(jié)束的時候,他抬頭才注意到王位上方僅用一根馬鬃懸掛著的利劍。他立即失去了對美食和美女的興趣,并請求僭主放過他,他再也不想得到這樣的幸運(yùn)。

這個故事告訴我們什么呢?
在和平安寧之后,時刻存在著危險與不安。
當(dāng)一個人獲取多少榮譽(yù)和地位,他都要付出同樣多的代價。
地位越高,看似越安全,實(shí)則越危險。
居安思危,對隨時可能帶來的嚴(yán)重后果,要做到謹(jǐn)慎。
那么這和軟件開發(fā)又有什么關(guān)系呢?下面就讓我來揭秘軟件開發(fā)中的達(dá)摩克利斯之劍。
危機(jī)四伏
“在和平安寧之后,時刻存在著危險與不安?!?/p>
軟件開發(fā)正是如此,表面上機(jī)器是 “死” 的,只會按照人輸入的指令或編好的程序來執(zhí)行,一成不變,聽話的很。好像我們寫好代碼扔到機(jī)器上后,就可以高枕無憂。
但真的是這樣么?我們真的可以信任機(jī)器和程序么?
其實(shí),在程序世界中危機(jī)四伏,人為因素、環(huán)境因素等可能都會對我們的程序產(chǎn)生影響。因此,我們必須時刻堅守軟件開發(fā)的不信任原則,保持 overly pessimistic
(過于悲觀),把和程序有關(guān)的一切請求、服務(wù)、接口、返回值、機(jī)器、框架、中間件等等都當(dāng)做不可信的,步步為營、處處設(shè)防。

那為什么寫個代碼要這么小心翼翼,什么都不信任呢?
大項目的苦衷
“當(dāng)一個人獲取多少榮譽(yù)和地位,他都要付出同樣多的代價?!?/p>
軟件開發(fā)中,項目價值越大,需要承受的壓力也越大,來聽聽大項目的苦衷吧。

我是一個身價過億的大項目,每天服務(wù)著上千萬的用戶,幫助他們獲得知識與快樂。
我的小伙伴們只看到我身上的光環(huán)和榮耀,但是他們看不到我背負(fù)的壓力和風(fēng)險,今天終于有機(jī)會和大家傾訴我的苦衷了。
記得很多年前,我還是個孩子,只有幾個小主人開發(fā)我,那段時間,我成長的很快。雖然只有幾十個人使用我,但我感到非常輕松和快樂,偶爾偷會兒懶,也不會被人發(fā)現(xiàn)。
后來,我的功能越來越多,越來越強(qiáng)大。每天有數(shù)之不盡的新面孔來和我打招呼,并享受我提供的服務(wù)。漸漸地,更多開發(fā)者在我身上留下了印記,我感覺自己正在變得復(fù)雜,也開始感受到了壓力。我再也找不到機(jī)會偷懶,因?yàn)槲乙坏┬菹?,就會讓我的主人們損失一比不小的財富。
如今,我已經(jīng)是一個成熟的大項目了,每天有上千萬的用戶依賴我,我終于擁有了更大的價值,卻也增加了很多煩惱,感受到了更大的危險。

首先,同時服務(wù)千萬用戶,每秒鐘都可能會有幾十萬、甚至幾百萬個請求需要我來處理,因此我必須每時每刻無休止地高負(fù)載工作,且不說休息,哪怕稍微慢了一點(diǎn),就會遭到用戶的投訴,主人們也會因此受到批評。
我的運(yùn)行,必須依靠很多兄弟們的支撐,因此我必須和兄弟們好好相處,哪怕一個兄弟倒了,我都會受到影響。
在我強(qiáng)大的實(shí)力背后,有一顆非常脆弱的心。經(jīng)歷了那么多次的強(qiáng)化和改造,我的功能逐漸變多的同時,也因此被植入了各種框架和插件,體積像滾雪球一般越來越大,不知道什么時候就會爆炸。以至于主人們每次改動我時都要萬分謹(jǐn)慎,我的成長也變得十分緩慢。

然而最讓我感到恐懼的,是那些壞家伙們!
他們和正常的用戶不同,有的不斷制造請求,試圖將我擊垮。有的繞到我的背后,試圖直接控制我。有的對我虎視眈眈,監(jiān)視并記錄我的一舉一動。還有的嘗試各種非法操作,想從我身上牟取暴利。
作為一個大項目真是太累了,我不知道我還能堅持多久。
真的可信么?
“地位越高,看似越安全,實(shí)則越危險?!?/p>
如今是一個軟件開源和共享的時代,我們在開發(fā)項目時,或多或少會使用到網(wǎng)上現(xiàn)有的資源,比如依賴包、工具、組件、框架、接口、現(xiàn)成的云服務(wù)等等,這些資源能夠大大提升我們的開發(fā)效率。
就拿云服務(wù)來說,幾乎已經(jīng)成了我們開發(fā)必備的資源,以前我們想要做一個網(wǎng)站,可能需要自己買一臺物理服務(wù)器,然后連通網(wǎng)絡(luò),再把項目部署上去。而如今,直接登錄大公司的云官網(wǎng)(像騰訊云、阿里云),然后租一臺云服務(wù)器就行了,非常省事。

再說說現(xiàn)在主流的開發(fā)框架,以前做一個簡單的網(wǎng)站界面可能只會使用 HTML
、CSS
、JavaScript
這三種最基礎(chǔ)的技術(shù),而如今,網(wǎng)站的樣式和交互越來越復(fù)雜,我們不得不使用一些知名的框架來提升開發(fā)效率,比如 Vue
和 React
。
聽起來好像沒有任何問題,你也根本不會去懷疑什么,因?yàn)?strong>我們天生帶著對大公司,或者說是對名氣的信任。
但是,你知道么,當(dāng)你決定使用其他人的資源時,你就已經(jīng)把項目系統(tǒng)的部分掌控權(quán)、甚至可能是半條命,都交出去了。
那么不妨思考一下,你使用的這些資源,真的可信么?
下面 10 個問題,可能改變你對開發(fā)的認(rèn)知。

1. 開發(fā)工具可信么?
我們通常是在大而全的開發(fā)工具中編寫代碼,比如 JetBrains IDEA
或者 Vscode
。很多剛開始寫代碼的同學(xué)、甚至是一些經(jīng)驗(yàn)豐富的老手,都對開發(fā)工具保持絕對的信任。
比如你在鍵盤上敲擊 a
,那編輯器界面上顯示的一定是 a
。
但是,由于內(nèi)存不足等種種原因,開發(fā)工具其實(shí)也會抽風(fēng)。
比如你想要調(diào)用某個函數(shù),通常敲擊函數(shù)名前幾個字母后,開發(fā)工具就會自動給你提示完整的函數(shù)名,但如果開發(fā)工具沒有給你提示,你首先懷疑的是這個函數(shù)不存在,而不是編輯器沒有按預(yù)期給出提示。遇到這種情況,可以稍等編輯器一下,或者進(jìn)一步確認(rèn)函數(shù)是否真的不存在,而不是立刻創(chuàng)建一個新的函數(shù)。
又或是項目無法運(yùn)行,怎么排查都覺得沒問題,這時不妨重新啟動下開發(fā)工具,或者清理一下緩存,說不定項目就能正常運(yùn)行了!
還有很多非常有意思的情況,比如編輯器一片大紅,各種提示錯誤,但是項目依然能成功運(yùn)行。

因此,不要絕對相信開發(fā)工具。
2. 開源項目可信么?
現(xiàn)在是一個軟件開源的時代,在 GitHub
等開源項目平臺上能夠找到大量優(yōu)秀的開源項目,好的開源項目甚至可以得到 10 萬多個關(guān)注,那這些知名的開源項目可信么?
不完全可信!從每個開源項目的 Issues 就能看出這點(diǎn),而且通常越大的項目,被發(fā)現(xiàn)的問題越多,比如 Vue
項目,累積提出并關(guān)閉了 8000 多個問題。

我記得自己有一次使用知名的開源服務(wù)器 Tomcat
,就遇到了 bug,每次接受到特定的請求都會報錯。剛開始我根本沒有懷疑是 Tomcat
的問題,而是絞盡腦汁地想自己的代碼哪里寫錯了。后來經(jīng)過反復(fù)的排查和搜索,終于確認(rèn)了就是 Tomcat
本身的 bug!
雖然開源項目并不完全可信,但是相對于私有項目而言,所有對項目感興趣的同學(xué)可以共同發(fā)現(xiàn)項目中的問題,并加以解決,在一定程度上還是能夠提高項目的可靠性的。
3. 依賴庫可信么?
我們在開發(fā)項目時,通常會用到大量的依賴庫。直接在官方依賴源(比如 Maven
和 npm
)搜索依賴庫,然后使用包管理器,用一行命令或者編寫配置文件就能夠讓其自動安裝依賴,非常方便。
但是,這些發(fā)布到官方源的依賴庫,就可信么?
且不說基本每個開發(fā)者都有機(jī)會發(fā)布依賴庫到官方,就算是互聯(lián)網(wǎng)大公司的依賴庫,也未必可信。
給我印象最深刻的就是阿里巴巴的 JSON
序列化類庫 fastjson
,幾乎無人不知、無人不曉,因?yàn)槠錁O快的解析速度廣受好評。但是,這個庫被多次曝光存在高危漏洞,可以讓攻擊者遠(yuǎn)程執(zhí)行命令!一般的開發(fā)者根本不會發(fā)現(xiàn)這點(diǎn),從而給項目帶來了極大的危害。

因此,在選用依賴庫的時候,要做好充分的調(diào)研,盡量確認(rèn)依賴庫的安全,并且保證不要和已有的依賴沖突。
4. 編程語言可信么?
Java
是一種強(qiáng)類型語言,具有健壯性。這句話我相信所有學(xué)過 Java
的同學(xué)都再熟悉不過了。但是,強(qiáng)類型編程語言就一定可信么?
這里可能有同學(xué)就要表示懷疑了,如果我們一直使用的最基礎(chǔ)最底層的編程語言都存在 bug,那我們怎么去相信建立在這些編程語言上的框架呢?
然而真相是,所有的編程語言都有 bug!而且基本每次編程語言發(fā)布新版本時都會對一些歷史 bug 進(jìn)行修正。就 Java
而言,甚至還有一個專門記錄 bug 的數(shù)據(jù)庫!

但是,對于大多數(shù)開發(fā)者來說,我相信即使在程序中偶然觸發(fā)了編程語言本身的 bug,也沒有足夠的自信去質(zhì)疑,而是直接修改代碼來繞過。
確實(shí),質(zhì)疑編程語言需要一定的基礎(chǔ)和知識儲備,但是一旦發(fā)現(xiàn)了程序中莫名其妙的問題,建議大家不要直接忽略,可以花一些時間去探索研究,說不定你就成功地發(fā)現(xiàn)了一個重大的 bug,也能夠加深對這門編程語言的理解。
5. 服務(wù)器可信么?
服務(wù)器是項目賴以生存的宿主,服務(wù)器的性能和穩(wěn)定性將直接影響到項目進(jìn)程。
無論是個人開發(fā)者還是企業(yè),通常都會直接租用大公司提供的云服務(wù)器來部署項目,省去了自己搭建和維護(hù)的麻煩。
但是大公司的云服務(wù)器就可信么?
不完全可信!即使現(xiàn)在的云服務(wù)器提供商都承諾自己的服務(wù) SLA(服務(wù)級別協(xié)議)可以達(dá)到 5 個 9(99.999% 一年約宕機(jī) 5 分鐘),甚至 6 個 9(99.9999% 一年約宕機(jī) 30 秒),但是仍然存在一定的風(fēng)險。
有一個非常有名的案例,在 2013 年,中國最大的社交通訊軟件出現(xiàn)大規(guī)模的故障,多達(dá)幾億用戶受到影響。原因竟然是,市政道路建設(shè)的一個不注意,把網(wǎng)絡(luò)光纜挖斷了,就導(dǎo)致該軟件所在服務(wù)器的無法訪問。
除了可用性的不可信之外,可能還有一些安全隱私方面的問題。當(dāng)然云服務(wù)商通常是不會獲取用戶的數(shù)據(jù)的,但也沒有辦法絕對相信他們。畢竟數(shù)據(jù)的隱私對企業(yè)至關(guān)重要,這也是為什么大的公司都會搭建屬于自己的服務(wù)器機(jī)房和網(wǎng)絡(luò)。

6. 數(shù)據(jù)庫可信么?
企業(yè)中的大多數(shù)業(yè)務(wù)數(shù)據(jù)都是存放在數(shù)據(jù)庫中的,通過項目后端程序來操作和查詢數(shù)據(jù)庫中的數(shù)據(jù)。
和服務(wù)器一樣,我們可以使用軟件自己搭建數(shù)據(jù)庫,比如 MySQL
,也可以直接租用大公司的云數(shù)據(jù)庫,那么數(shù)據(jù)庫可信么?
其實(shí)在企業(yè)后端項目中,數(shù)據(jù)庫通常是性能瓶頸,相對比較脆弱,當(dāng)訪問并發(fā)量大一點(diǎn)時,數(shù)據(jù)庫的查詢性能就會下降,嚴(yán)重時可能整個宕機(jī)!即使是大公司提供的云數(shù)據(jù)庫服務(wù),遇到慢查詢(需要較長時間的查詢)時,可能也無從應(yīng)對。
數(shù)據(jù)庫中的數(shù)據(jù)其實(shí)也未必可信,有時管理員的一個誤操作,不小心刪除數(shù)據(jù)或添加了一條錯誤數(shù)據(jù),可能就會影響用戶,造成損失。更有甚者,竟然刪庫跑路,不講碼德!

因此,不要過于信任數(shù)據(jù)庫,應(yīng)當(dāng)使用緩存之類的技術(shù)幫助數(shù)據(jù)庫分擔(dān)壓力,并定期備份。否則一旦數(shù)據(jù)庫宕機(jī)或數(shù)據(jù)丟失,帶來的損失是不可估量的!
7. 緩存服務(wù)可信么?
緩存是開發(fā)高性能程序必備的技術(shù),通過將數(shù)據(jù)庫等查詢較慢的數(shù)據(jù)存放在內(nèi)存中,直接從內(nèi)存中讀取數(shù)據(jù),以提升查詢性能。有了緩存之后,項目不僅能夠支持更多人同時查詢數(shù)據(jù),還能夠保護(hù)數(shù)據(jù)庫。
目前比較主流的緩存技術(shù)有 Redis
、Memcached
等,可以自己在服務(wù)器搭建,也可以直接租用大公司提供的云緩存服務(wù)。

那么緩存服務(wù)是否可信呢?
項目的并發(fā)量不是特別大的話,一般的緩存技術(shù)就足以支持了,但是如果項目的量級很大,可能緩存也無法承受住壓力,嚴(yán)重時就會宕機(jī)。而一旦緩存掛掉,大量的查詢命令會直接請求數(shù)據(jù)庫,于是數(shù)據(jù)庫也會在瞬間掛掉,嚴(yán)重時還會導(dǎo)致整個項目癱瘓!
因此,在使用緩存時,需要對并發(fā)量進(jìn)行評估,通過搭建集群和數(shù)據(jù)同步保證高可用性。此外,還要預(yù)防緩存雪崩、緩存穿透、緩存擊穿等問題,簡單解釋一下。
緩存雪崩:指大量緩存在同一時間過期,請求都訪問不到緩存,全部打到數(shù)據(jù)庫上,導(dǎo)致數(shù)據(jù)庫掛掉。
緩存穿透:持續(xù)訪問緩存中不存在的 key,導(dǎo)致請求繞過緩存,直接打到數(shù)據(jù)庫上,導(dǎo)致數(shù)據(jù)庫掛掉。
緩存擊穿:一個被大量請求高頻訪問的熱點(diǎn) key 突然過期,導(dǎo)致請求瞬間全部打到數(shù)據(jù)庫上,導(dǎo)致數(shù)據(jù)庫掛掉。
如果不預(yù)防這三個問題,即使是租用大公司的緩存服務(wù),也一樣吹彈可破。
8. 對象存儲可信么?
項目中,經(jīng)常會有用戶上傳圖片或文件的功能,這類數(shù)據(jù)通常較大,用數(shù)據(jù)庫存儲不太方便。雖然我們可以將文件直接存到服務(wù)器上,但更好的做法是使用專門的對象存儲服務(wù)。
可以簡單地把對象存儲當(dāng)做一個大的文件夾,我們可以通過它直接上傳和下載文件。大的云服務(wù)商也都提供了專業(yè)的對象存儲服務(wù),而無需自己搭建,那么對象存儲可信么?
一般情況下,上傳到對象存儲的文件是不會缺失或丟失的,而且還可以將已上傳的數(shù)據(jù)進(jìn)行跨園區(qū)同步,起到備份的作用。
但是,記得有一次,上傳到對象存儲上的文件和源文件竟然不一致,大小足足少了 1M。起初我以為是文件上傳到對象存儲時,會自動被壓縮,但是將對象存儲中的文件下載到本地后,發(fā)現(xiàn)的確和源文件不一致!雖然出現(xiàn)這種情況的概率極其小,但從那一刻起,我再也不相信對象存儲了。
再用自己的真實(shí)經(jīng)歷來聊聊對象存儲的跨園區(qū)同步。因?yàn)閭€人負(fù)責(zé)的業(yè)務(wù)比較重要,萬一單個機(jī)房整體掛掉,可能分分鐘是幾十萬元的損失!因此我為對象存儲配置了自動跨園區(qū)同步,將文件先上傳至廣州機(jī)房,然后數(shù)據(jù)會自動同步到上海機(jī)房,且運(yùn)維同學(xué)承諾自動同步的延遲不超過 15 分鐘。
我相信大部分開發(fā)者配置數(shù)據(jù)同步后也就不管了,相信它一定會自動同步的。結(jié)果后面我編寫程序去做同步監(jiān)控、對比數(shù)據(jù)時,發(fā)現(xiàn)經(jīng)常出現(xiàn)數(shù)據(jù)未同步的情況,比例高達(dá) 10%!
因此,不能完全相信對象存儲,雖然大部分情況下大公司的對象存儲服務(wù)很可靠,但不能確保萬無一失。尤其是同步備份的場景下,是否真的同步成功了,又有多少同學(xué)關(guān)心過呢?不妨寫個程序去驗(yàn)證和保障。
9. API 接口可信么?
在開發(fā)中,我們經(jīng)常會調(diào)用其他系統(tǒng)提供的 API 接口來輕松實(shí)現(xiàn)某種功能。比如查詢某地的天氣,可以直接調(diào)用其他人提供的天氣查詢接口,而無需自己編寫。我們也可以提供 API 接口給其他人使用,尤其是在微服務(wù)架構(gòu)中,各服務(wù)之間都是以接口調(diào)用的形式實(shí)現(xiàn)交互協(xié)作的。
幾乎所有的 API 接口提供者都會說自己的接口有多安全、請放心使用,那么 API 接口真的可信么?
其實(shí),API 接口是最不可信的資源!
首先,API 接口的提供方可以是任何開發(fā)者,很難通過他們的一面之詞來確定接口的穩(wěn)定性和安全性。
即使這個接口性能很高、也很安全,但是你并不了解有多少人和你在同時使用這個接口,也許只有你,又也許是 100 萬個其他的開發(fā)者呢?在這個競爭條件下,接口的 qps
(query per second 每秒查詢數(shù))還能達(dá)到預(yù)期么?接口返回時長真的不會超時么?
更有甚者,偷偷地把 API 接口改動了,卻沒有給調(diào)用者發(fā)送通知,這樣接口的調(diào)用方全部都會調(diào)用失敗,嚴(yán)重影響項目的運(yùn)行!
因此,我們在調(diào)用第三方 API 接口時,一定要慎重、慎重、再慎重!
此外,如果我們是 API 接口的提供者,也要注意保護(hù)好自己的 API 接口,避免同時被太多的開發(fā)者調(diào)用,導(dǎo)致接口掛掉。
10. Serverless 可信么?
如果說服務(wù)器不可信,那我們干脆就不租服務(wù)器了,直接租用大公司提供的 Serverless 服務(wù)來作為項目的后臺不就行了?
Serverless 指無服務(wù)器架構(gòu),并不是真的不需要服務(wù)器,而是將項目接口的部署、運(yùn)維等需要對服務(wù)器的操作交給服務(wù)商去做,讓開發(fā)者無需關(guān)心服務(wù)器,專心寫代碼就好。
聽起來非常爽,那 Serverless可信么?
使用 Serverless,雖然能夠大大提升開發(fā)和運(yùn)維效率,但是其相對服務(wù)器等資源而言,更不可信!
首先,Serverless 本身就是部署在服務(wù)器上的,難免會受到服務(wù)器的影響。
其次,Serverless 服務(wù)不會長期保持應(yīng)用的狀態(tài),而是隨著請求的到來而啟動,存在冷啟動時期,雖然也有很多相關(guān)的優(yōu)化和解決方案,但仍無法精確地保證接口的性能,尤其是在高并發(fā)場景下,性能往往達(dá)不到預(yù)期。
最重要的是,當(dāng)你選擇使用 Serverless 服務(wù)時,你就和某云服務(wù)提供商綁定了,后續(xù)想要遷移是非常困難的!試想一下,你項目的所有功能都交給別人來維護(hù),真的是好事么?一旦云服務(wù)提供商改造了架構(gòu)或接口,你的代碼也要隨之改動,而這種改動卻不是由自己控制的!
當(dāng)然,Serverless 具有非常多的優(yōu)點(diǎn),也是云計算技術(shù)發(fā)展的必然趨勢,只是希望大家在使用前,考慮到那些可能的風(fēng)險,并做好應(yīng)對措施。
總結(jié):正是因?yàn)槲覀兲^信任那些名氣大、看似安全的資源,所以其背后的危險才更難以被察覺,帶來的后果往往也更致命!
防御性編程
“居安思危,對隨時可能帶來的嚴(yán)重后果,要做到謹(jǐn)慎。”
在軟件開發(fā)中,雖然項目表面上能夠正常運(yùn)行,但風(fēng)險無處不在,因此我們要學(xué)習(xí)防御性編程思想。把自己當(dāng)成一個杠精,不要相信任何人,盡力去發(fā)現(xiàn)程序中的風(fēng)險,積極防御。
下面給大家分享 16 個防御性編程的方法,學(xué)習(xí)之后,能夠大大減少程序中的風(fēng)險。
1. 編程習(xí)慣
要減少程序中的風(fēng)險,首先要養(yǎng)成良好的編程習(xí)慣。
首先,在寫代碼時,一定要保持良好的心態(tài),不要倉促或者以完成任務(wù)的心態(tài)去寫代碼。如果僅僅是為了完成需求,那么很有可能不會注意到代碼中的風(fēng)險,甚至是發(fā)現(xiàn)了風(fēng)險也懶得去修補(bǔ),這樣確實(shí)能夠節(jié)約開發(fā)的時間,但是后面出現(xiàn)問題后,你還是要花費(fèi)更多的時間去排查、溝通和修復(fù) bug。拔苗助長,適得其反。
在寫代碼時,如果在一個地方多次使用相同且復(fù)雜的變量名或字符串,建議不要手動去敲,而是用大家最喜歡的 “復(fù)制粘貼”,防止因?yàn)槭终`而導(dǎo)致的 bug。
此外,我們在代碼中應(yīng)該加強(qiáng)對返回值的檢查,并且選擇安全的語法和數(shù)據(jù)結(jié)構(gòu),避免使用被廢棄的語法。不同的編程語言也有不同的最佳編程習(xí)慣,比如在 Java
語言中,應(yīng)該對所有可能為 NULL
的變量進(jìn)行檢查,防止 NPE
(NULL Pointer Error 空指針異常),在開發(fā)多線程程序時,選用線程安全的 ConcurrentHashMap
而不是 HashMap
等等。還可以利用 Assert
(斷言)來保證程序運(yùn)行中的變量值符合預(yù)期。
推薦使用一個自帶檢查功能的編輯器來書寫代碼,在我們編寫代碼時會自動檢查出錯誤,還能給出好的編碼風(fēng)格的建議,能夠大大減少開發(fā)時的風(fēng)險。此外,在代碼提交前,一定要多次檢查代碼,尤其是那些復(fù)制粘貼過來的文件,經(jīng)常會出現(xiàn)遺漏的修改。提交代碼后,也可以找有經(jīng)驗(yàn)的同事幫忙閱讀和檢查下代碼(代碼審查),進(jìn)一步保證沒有語法和邏輯錯誤。
2. 異常處理
程序的運(yùn)行風(fēng)云變幻,同一段代碼在不同情況下也可能會產(chǎn)生不同的結(jié)果,甚至是異常。因此很多主流的編程語言中都有異常處理機(jī)制,比如在 Java
中,先用 try
捕獲異常、再用 catch
處理異常、最后用 finally
釋放資源和善后。
在編程時,要合理利用異常處理機(jī)制,來防御代碼中可能出現(xiàn)的種種問題。通常在異常處理中,我們會記錄錯誤日志、執(zhí)行錯誤上報和告警、重試等。
比如不信任數(shù)據(jù)庫,那就在查詢和操作數(shù)據(jù)時添加異常處理,一旦數(shù)據(jù)庫抽風(fēng)導(dǎo)致操作失敗,就在日志中記錄失敗信息,并通過郵件、短信等告警方式通知到開發(fā)者,就能第一時間發(fā)現(xiàn)問題并排查。必要時還可以實(shí)現(xiàn)自動重試,省去一部分人工操作。
3. 請求校驗(yàn)
所有的請求都是不可信的,哪怕是在公司內(nèi)網(wǎng),也有可能因?yàn)橐恍┦д`,導(dǎo)致發(fā)出了錯誤的請求。
因此我們編寫的每個接口,在實(shí)現(xiàn)具體的業(yè)務(wù)邏輯前,一定要先對請求參數(shù)加上校驗(yàn),下面列舉幾種常見的校驗(yàn)方式:
參數(shù)類型校驗(yàn):比如請求參數(shù)應(yīng)該是
Integer
整型而不是Long
長整數(shù)類型。值合法性校驗(yàn):比如整數(shù)的范圍大于等于 0、字符串長度大于 5,或者滿足某種特定格式,比如手機(jī)號、身份證等。
用戶權(quán)限校驗(yàn):很多接口需要登錄用戶或者管理員才能調(diào)用,因此必須通過請求參數(shù)(請求頭)來判斷當(dāng)前用戶的身份,被一個普通用戶下載了 VIP 付費(fèi)電影肯定是不合理的!
4. 流量控制
上面提到,所有的請求都是不可信的,不僅僅是請求的值,還有請求的量和頻率。對于所有接口,都要限制它的調(diào)用頻率,防止接口被大量瞬時的請求刷爆。對于付費(fèi)接口,還要防止用戶對接口的請求數(shù)超過原購買數(shù)。
此外,還有一種容易被忽視的情況,假如你的接口 A 中又調(diào)用了其他人的接口 B,也許你的接口 A 自身的邏輯能夠承受每秒 1000 個請求,但是你確定接口 B 可以承受么?
因此,需要進(jìn)行流量控制,不僅僅是預(yù)防接口被刷爆,還可以保護(hù)內(nèi)部的服務(wù)和調(diào)用。
什么,你說你的接口很牛逼,每秒能抗 100 萬個請求,也沒有調(diào)用其他的服務(wù),那我就找 100 萬 + 1 個人同時請求你的接口,看你怕不怕!
常用的流量控制按照不同的粒度可分為:
用戶流控:限制每個用戶在一定時間內(nèi)對某個接口的調(diào)用數(shù)。
接口流控:限制一定時間內(nèi)某個接口的總調(diào)用數(shù)。
單機(jī)流控:限制一定時間內(nèi)單臺服務(wù)器上的項目所有接口的總調(diào)用數(shù)。
分布式流控:限制一定時間內(nèi)項目所有服務(wù)器的總請求數(shù)。
當(dāng)然,除了上面提到的幾種方式外,流控可以非常靈活,也有很多優(yōu)秀的限流工具。比如 Java
語言 Guava
庫的 RateLimiter
令牌桶單機(jī)限流、阿里的 Sentinel
分布式限流框架等。
5. 回滾
有時,我們對項目的操作可能是錯誤的,可能是人工操作,也可能是機(jī)器操作,從而導(dǎo)致了一些線上故障。這時,可以選擇回滾。
回滾是指撤銷某個操作,將項目還原到之前的狀態(tài),這里介紹幾種常見的回滾操作。
數(shù)據(jù)回滾
有時,我們想要批量插入數(shù)據(jù),但是數(shù)據(jù)插入到一半時,程序突然出現(xiàn)異常,這個時候我們就需要把之前插入成功的數(shù)據(jù)進(jìn)行回滾,就好像什么都沒發(fā)生過一樣。否則可能存在數(shù)據(jù)不一致的風(fēng)險。
最常見的方式就是使用事務(wù)來處理數(shù)據(jù)庫的批量操作,當(dāng)出現(xiàn)異常時,執(zhí)行數(shù)據(jù)庫客戶端的回滾方法即可。
配置回滾
如果將項目的配置信息,比如數(shù)據(jù)庫鏈接地址,寫死到代碼中,一旦配置錯了或者地址發(fā)生變更,就要重新修改代碼,非常麻煩。
比較好的方式是將配置發(fā)布到配置中心進(jìn)行管理,讓項目去動態(tài)讀取配置中心的配置。如果不小心發(fā)布了錯誤的配置,可以直接在配置中心進(jìn)行回滾,將配置還原。
發(fā)布回滾
沒有人能保證自己的代碼正確無誤,很多時候,項目在測試環(huán)境驗(yàn)證時沒有發(fā)現(xiàn)任何問題,但是一上線,就漏洞百出。這就說明我們最新發(fā)布的代碼是存在問題的。
這時,最簡單的做法就是進(jìn)行版本回滾,將之前能夠正常運(yùn)行的代碼重新打包發(fā)布。大公司一般都有自己的項目發(fā)布平臺,能夠使用界面一鍵回滾,自動發(fā)布以前版本的項目包。
6. 多級緩存
上面提到,緩存對項目是非常重要的,不僅是提升性能的利器,也是數(shù)據(jù)庫的保護(hù)傘。
但如果緩存掛掉怎么辦呢?
有兩種方案,第一種是為緩存搭建集群,從而保證緩存的高可用。

但是一切都不可信,集群也有可能掛掉!
那么可以用第二種方案,一級緩存掛掉,我們就再搞一個二級緩存頂上!
通常,在高并發(fā)項目中,我們會設(shè)計多級緩存,即分布式緩存 + 本地緩存。當(dāng)請求需要獲取數(shù)據(jù)時,先從分布式緩存(比如 Redis
) 中查詢,如果分布式緩存集體宕機(jī),那就從本地緩存中獲取數(shù)據(jù)。這樣,即使緩存掛掉,也能夠幫助系統(tǒng)支撐一段時間。
這里可能和一些多級緩存的設(shè)計不同,有時,我們會把本地緩存作為一級緩存,緩存一些熱點(diǎn)數(shù)據(jù),本地緩存找不到值時,才去訪問分布式緩存。這種設(shè)計主要解決的問題是,減少對分布式緩存的請求量,并進(jìn)一步提升性能,和上面的設(shè)計目的不同。

7. 服務(wù)熔斷和降級
每年的雙十一,我們會準(zhǔn)時守著屏幕上的搶購頁面,只為等待那一個 “請稍后再試!”

我們的項目其實(shí)遠(yuǎn)比想象的要脆弱,很多服務(wù)經(jīng)常因?yàn)楦鞣N原因出現(xiàn)問題。比如搞活動時,大量用戶同時訪問會導(dǎo)致對項目服務(wù)的請求增多,如果項目頂不住壓力,就會掛掉。
為了防止這種風(fēng)險,我們可以采用服務(wù)降級策略,如果系統(tǒng)實(shí)在無法為所有用戶提供服務(wù),那就退而求其次,給用戶直接返回一個 “友好的” 提示或界面,而不是強(qiáng)行讓項目頂著壓力過勞死。
配合服務(wù)熔斷技術(shù),可以根據(jù)系統(tǒng)的負(fù)載等指標(biāo)來動態(tài)開啟或關(guān)閉降級。比如機(jī)器的 CPU 被占用爆滿時,就開啟降級,直接返回錯誤;當(dāng)機(jī)器 CPU 恢復(fù)正常時,再正常返回數(shù)據(jù)、執(zhí)行操作。
Hystrix
就是比較有名的微服務(wù)熔斷降級框架。

8. 主動檢測
上面提到,即使是大公司的同步服務(wù),也可能會出現(xiàn)同步不及時甚至是數(shù)據(jù)丟失的情況。因此,為了進(jìn)一步保證同步成功、數(shù)據(jù)的準(zhǔn)確,我們可以主動檢測。
比如編寫一個定時腳本或者任務(wù),每隔一段時間去檢查原地址和目標(biāo)地址的數(shù)據(jù)是否一致,或者通過一些邏輯來檢查數(shù)據(jù)是否正確。當(dāng)然也可以在每次數(shù)據(jù)同步結(jié)束后都立即去檢測,更加保險。

9. 數(shù)據(jù)補(bǔ)償
當(dāng)檢測出數(shù)據(jù)不一致后,我們就要進(jìn)行數(shù)據(jù)補(bǔ)償,比如將沒有同步的數(shù)據(jù)再次進(jìn)行同步、將不一致的數(shù)據(jù)進(jìn)行更新等。
除了用來解決主動檢測出的數(shù)據(jù)不一致,數(shù)據(jù)補(bǔ)償也被廣泛用于業(yè)務(wù)設(shè)計和架構(gòu)設(shè)計中。
比如調(diào)用某個接口查詢數(shù)據(jù)失敗后,停頓一段時間,然后自動重試,或者從其他地方獲取數(shù)據(jù)。又如消息隊列的生產(chǎn)者發(fā)送消息失敗時,應(yīng)該自動進(jìn)行補(bǔ)發(fā)和記錄,而不是直接把這條消息作廢。
數(shù)據(jù)補(bǔ)償?shù)乃枷氡举|(zhì)上是保證數(shù)據(jù)的最終一致性,數(shù)據(jù)出錯不可怕,知錯能改就是好孩子。這種思想也被廣泛應(yīng)用于分布式事務(wù)等場景中。

10. 數(shù)據(jù)備份
數(shù)據(jù)是企業(yè)的生命,因此我們必須盡可能地保證數(shù)據(jù)的安全和完整。
很多同學(xué)會把自己重要的文件存放在多個地方,比如自己的電腦、網(wǎng)盤上等等。同樣,在軟件開發(fā)中,我們也應(yīng)該把重要的數(shù)據(jù)復(fù)制多份,作為副本存放在不同的地方。這樣,即使一臺服務(wù)器掛了,也可以從其他的服務(wù)器上獲取到數(shù)據(jù),減少了風(fēng)險。

11. 心跳機(jī)制
接口可是個復(fù)雜多變的家伙,如果我們的項目依賴其他的接口來完成功能,那么最好保證該接口一直活著,否則可能會影響項目的運(yùn)行。
舉個例子,我們在使用銀行卡支付時,肯定需要調(diào)用銀行提供的接口來獲取銀行卡的余額信息,如果這個接口掛了,獲取不到余額,用戶也就無法支付,也就損失了一筆收入!
因此,我們需要時刻和重要的接口保持聯(lián)系,防止他們不小心死了??梢圆捎眯奶鴻C(jī)制,定時調(diào)用該接口或者發(fā)送一個心跳包,來判斷該接口是否仍然存活。一旦調(diào)用超時或者失敗,可以立刻進(jìn)行排查和處理,從而大大減少了事故的影響時長。

12. 冗余設(shè)計
在系統(tǒng)資源和容量評估時,我們要做一些冗余設(shè)計,比如數(shù)據(jù)庫目前的總數(shù)據(jù)量有 1G,那么如果要將數(shù)據(jù)庫的數(shù)據(jù)同步到其他存儲(比如 Elasticsearch
)時,至少要多預(yù)留一倍的存儲空間,即 2G,來應(yīng)對后面可能的數(shù)據(jù)增長。業(yè)務(wù)的發(fā)展?jié)摿υ酱?,冗余的倍?shù)也可以越多,但也要注意不要過分冗余,畢竟資源也是很貴的?。?/p>
其實(shí),冗余設(shè)計是一種重要的設(shè)計思想。當(dāng)我們設(shè)計業(yè)務(wù)或者系統(tǒng)架構(gòu)時,不能只局限于當(dāng)前的條件,而是要考慮到以后的發(fā)展,選擇一種相對便于擴(kuò)展的模式。否則之后項目越做越大,每一次對項目的改動都步履維艱。

13. 彈性擴(kuò)縮容
夢想還是要有的,說不定突然,我們原先只有 100 人使用的小項目突然就火了,有幾十萬新用戶要來使用。
但是,由于我們的項目只部署在一臺服務(wù)器上,根本無法支撐那么多人,直接掛掉,導(dǎo)致這些用戶非常掃興,再也不想用我們的項目了。

這也是常見的風(fēng)險,我們可以使用彈性擴(kuò)縮容技術(shù),系統(tǒng)會根據(jù)當(dāng)前項目的使用和資源占用情況自動擴(kuò)充或縮減資源。
比如當(dāng)系統(tǒng)壓力較大時,多分配幾臺機(jī)器(容器),當(dāng)系統(tǒng)壓力較小時,減少幾臺機(jī)器。這樣不僅能夠有效應(yīng)對突發(fā)的流量增長,還能夠在平時節(jié)約成本,并省去了人工分配調(diào)整機(jī)器的麻煩。
14. 異地多活
前面提到,服務(wù)器是不可信的,別說一個服務(wù)器掛掉,由于一些天災(zāi)人禍,整個機(jī)房都有可能集體掛掉!
和備份不同,異地多活是指在不同城市建立獨(dú)立的數(shù)據(jù)中心,正常情況下,用戶無論訪問哪一個地點(diǎn)的業(yè)務(wù)系統(tǒng),都能夠得到正確的服務(wù),即同時有多個 “活” 的服務(wù)。
而某個地方業(yè)務(wù)異常的時候,用戶能夠訪問其他地方正常的業(yè)務(wù)系統(tǒng),從而獲得正確的服務(wù)。
如此一來,即使廣州的機(jī)房跨了,咱還有上海的,上海的跨了,咱還有北京的。
同時活著的服務(wù)越多,系統(tǒng)就越可靠,但同時成本也越高、越復(fù)雜,因此幾乎都是大公司才做異地多活。千萬不要讓正常情況下的投入大于故障發(fā)生的損失!

15. 監(jiān)控告警
項目的運(yùn)行不可能一直正常,但是我們不可能 24 小時盯著電腦屏幕來監(jiān)視項目的運(yùn)行情況吧?又不能完全不管項目,出了 bug 等著用戶來投訴。
因此,最好的方式是給業(yè)務(wù)添加監(jiān)控告警,當(dāng)程序出現(xiàn)異常時,信息會上報到監(jiān)控平臺,并第一時間給開發(fā)者發(fā)送通知。還可以通過監(jiān)控平臺實(shí)時查看項目的運(yùn)行情況,出了問題也能更快地定位。

16. 線上診斷和熱修復(fù)
既然程序世界一切都不可信,危險無處不在,那么干脆就做最壞的打算,假設(shè)線上程序一定會出 bug。
既然防不勝防,那就嚴(yán)陣以待,在 bug 出現(xiàn)時用最快的速度修復(fù)它,來減少影響。
通常,我們要改 bug,也需要經(jīng)歷改動代碼、提交代碼、合并代碼、打包構(gòu)建、發(fā)布上線等一系列流程。等流程走完了,可能系統(tǒng)都透心涼了。
為提高效率,我們可以使用線上診斷和熱修復(fù)技術(shù)。在出現(xiàn) bug 時,先用線上診斷工具輕松獲取項目的各運(yùn)行狀態(tài)和代碼執(zhí)行信息,提升排查效率。發(fā)現(xiàn)問題后,使用熱修復(fù)技術(shù)直接修改運(yùn)行時的代碼,無需重新構(gòu)建和重啟項目!
在 Java
中,我們可以使用阿里開源的診斷工具 Arthas
,同時支持線上熱修復(fù)功能。也可以自己編寫腳本來實(shí)現(xiàn),但是相對復(fù)雜一些。

看到這里,肯定有同學(xué)會吐槽,怎么寫個程序要考慮那么多和功能無關(guān)的問題。本來五分鐘就能寫完的代碼,現(xiàn)在可能一個小時都寫不完!

其實(shí),并不是所有的項目都要做到絕對的安全(當(dāng)然我們也做不到),而是我們應(yīng)該時刻保持居安思危的思想,把防御性編程當(dāng)做自己的習(xí)慣。
實(shí)際情況下,要根據(jù)項目的量級、受眾、架構(gòu)、緊急程度等因素來綜合評估將項目做到何種程度的安全,而不是過度設(shè)計、杞人憂天。
讓我們把時間慢下來,在開發(fā)前先冷靜思考,預(yù)見并規(guī)避風(fēng)險,不要讓達(dá)摩克利斯之劍落下。
