1. 程式人生 > 實用技巧 >揭祕軟體開發中的達摩克利斯之劍

揭祕軟體開發中的達摩克利斯之劍

為什麼你的程式總是出現 bug?

憑什麼讓改 bug 佔據了你大部分的時間?

看完本文,保證你能設計出更穩定的程式,擺脫 bug 的纏繞,做專案更安心!


記得我在學校的時候,做的那些專案,不是為了應付課程作業,就是為了參加比賽時展示用,因此對專案的質量要求非常低。

到底有多低呢?

大部分的專案,只要基本的功能可以使用,就算完成了,完全不考慮任何的異常情況。甚至只要能成功執行一次,讓我截幾張圖放到 PPT 或者實驗報告裡,足夠向老師交差或者應付比賽答辯就行。

那專案出現 bug 怎麼辦呢?

  • 如果測試的時候發現有些功能不可用,那很簡單,不管他,直接 PS 一張正常執行的圖就行。

  • 如果比賽的時候發現有些功能不可用,那也很簡單,把鍋甩給 “現場網路不好” 就行。

但是,這些 “小技巧” 在企業中是行不通的,企業級專案必須為企業帶來實際的價值,容不得半點馬虎和欺騙。

我第一次進入企業實習時,還保留著自己在學校開發專案的狼性,只要能夠完成基本功能就行,保證以最快的速度完成開發。

有一天,當我洋洋得意準備早點下班時,測試同學走過來跟我說。

“喂,你的程式有 bug,這裡使用者下單怎麼金額是負的?”

對於我一個初入職場的小白,這是人生中第一次有人說我的程式碼有 bug,我有問題,我不對勁。

當時,我腦海的第一個念頭竟然是怎麼把這個 bug 糊弄過去,而不是怎麼去更正!看來我已經養成了非常不好的習慣。

那之後幾天,我又連續收到了測試提出的多個 bug,然後將他們一個個改正。如果將這樣一個漏洞百出的程式釋出上線,帶來的損失是不可估量的,現在想想仍心有餘悸。

這件事之後,我意識到,在企業中開發專案,不能只追求開發時的效率,還要注重專案的穩定性,否則帶來的額外返工時間遠比開發時節省的時間要長,而且會影響同事對你的看法。如果將開發時產生的 bug 遺留到線上,後果更是不堪設想!

後來,在位元組跳動和騰訊這兩家大公司工作後,我進一步認識到了專案穩定性有多重要,並且積累了更多隻有在大公司才能學到的提升專案穩定性的經驗。

我總結了 10 個開發中通常不會考慮到的風險點,以及 16 個減少風險、提升專案穩定性的方法,分享給大家~

在分享這些之前,先講個故事。

達摩克利斯之劍

古希臘傳說中,達摩克利斯是公元前 4 世紀義大利敘拉古的僭主(古希臘統治者獨有的稱號)狄奧尼修斯二世的朝臣,他非常喜歡奉承狄奧尼修斯。

他奉承道:“作為一個擁有權力和威信的偉人,狄奧尼修斯實在很幸運。”

於是狄奧尼修斯提議與他交換一天的身份,那他就可以嘗試到首領的命運。

在晚上舉行的宴會裡,達摩克利斯非常享受成為國王的感覺。當晚餐快結束的時候,他抬頭才注意到王位上方僅用一根馬鬃懸掛著的利劍。他立即失去了對美食和美女的興趣,並請求僭主放過他,他再也不想得到這樣的幸運。

這個故事告訴我們什麼呢?

  1. 在和平安寧之後,時刻存在著危險與不安。

  2. 當一個人獲取多少榮譽和地位,他都要付出同樣多的代價。

  3. 地位越高,看似越安全,實則越危險。

  4. 居安思危,對隨時可能帶來的嚴重後果,要做到謹慎。

那麼這和軟體開發又有什麼關係呢?下面就讓我來揭祕軟體開發中的達摩克利斯之劍。

危機四伏

“在和平安寧之後,時刻存在著危險與不安。”

軟體開發正是如此,表面上機器是 “死” 的,只會按照人輸入的指令或編好的程式來執行,一成不變,聽話的很。好像我們寫好程式碼扔到機器上後,就可以高枕無憂。

但真的是這樣麼?我們真的可以信任機器和程式麼?

其實,在程式世界中危機四伏,人為因素、環境因素等可能都會對我們的程式產生影響。因此,我們必須時刻堅守軟體開發的不信任原則,保持 overly pessimistic(過於悲觀),把和程式有關的一切請求、服務、介面、返回值、機器、框架、中介軟體等等都當做不可信的,步步為營、處處設防。

那為什麼寫個程式碼要這麼小心翼翼,什麼都不信任呢?

大專案的苦衷

“當一個人獲取多少榮譽和地位,他都要付出同樣多的代價。”

軟體開發中,專案價值越大,需要承受的壓力也越大,來聽聽大專案的苦衷吧。

我是一個身價過億的大專案,每天服務著上千萬的使用者,幫助他們獲得知識與快樂。

我的小夥伴們只看到我身上的光環和榮耀,但是他們看不到我揹負的壓力和風險,今天終於有機會和大家傾訴我的苦衷了。

記得很多年前,我還是個孩子,只有幾個小主人開發我,那段時間,我成長的很快。雖然只有幾十個人使用我,但我感到非常輕鬆和快樂,偶爾偷會兒懶,也不會被人發現。

後來,我的功能越來越多,越來越強大。每天有數之不盡的新面孔來和我打招呼,並享受我提供的服務。漸漸地,更多開發者在我身上留下了印記,我感覺自己正在變得複雜,也開始感受到了壓力。我再也找不到機會偷懶,因為我一旦休息,就會讓我的主人們損失一比不小的財富。

如今,我已經是一個成熟的大專案了,每天有上千萬的使用者依賴我,我終於擁有了更大的價值,卻也增加了很多煩惱,感受到了更大的危險。

首先,同時服務千萬使用者,每秒鐘都可能會有幾十萬、甚至幾百萬個請求需要我來處理,因此我必須每時每刻無休止地高負載工作,且不說休息,哪怕稍微慢了一點,就會遭到使用者的投訴,主人們也會因此受到批評。

我的執行,必須依靠很多兄弟們的支撐,因此我必須和兄弟們好好相處,哪怕一個兄弟倒了,我都會受到影響。

在我強大的實力背後,有一顆非常脆弱的心。經歷了那麼多次的強化和改造,我的功能逐漸變多的同時,也因此被植入了各種框架和外掛,體積像滾雪球一般越來越大,不知道什麼時候就會爆炸。以至於主人們每次改動我時都要萬分謹慎,我的成長也變得十分緩慢。

然而最讓我感到恐懼的,是那些壞傢伙們!

他們和正常的使用者不同,有的不斷製造請求,試圖將我擊垮。有的繞到我的背後,試圖直接控制我。有的對我虎視眈眈,監視並記錄我的一舉一動。還有的嘗試各種非法操作,想從我身上牟取暴利。

作為一個大專案真是太累了,我不知道我還能堅持多久。

真的可信麼?

“地位越高,看似越安全,實則越危險。”

如今是一個軟體開源和共享的時代,我們在開發專案時,或多或少會使用到網上現有的資源,比如依賴包、工具、元件、框架、介面、現成的雲服務等等,這些資源能夠大大提升我們的開發效率。

就拿雲服務來說,幾乎已經成了我們開發必備的資源,以前我們想要做一個網站,可能需要自己買一臺物理伺服器,然後連通網路,再把專案部署上去。而如今,直接登入大公司的雲官網(像騰訊雲、阿里雲),然後租一臺雲伺服器就行了,非常省事。

再說說現在主流的開發框架,以前做一個簡單的網站介面可能只會使用 HTMLCSSJavaScript 這三種最基礎的技術,而如今,網站的樣式和互動越來越複雜,我們不得不使用一些知名的框架來提升開發效率,比如 VueReact

聽起來好像沒有任何問題,你也根本不會去懷疑什麼,因為我們天生帶著對大公司,或者說是對名氣的信任

但是,你知道麼,當你決定使用其他人的資源時,你就已經把專案系統的部分掌控權、甚至可能是半條命,都交出去了。

那麼不妨思考一下,你使用的這些資源,真的可信麼?

下面 10 個問題,可能改變你對開發的認知。

1. 開發工具可信麼?

我們通常是在大而全的開發工具中編寫程式碼,比如 JetBrains IDEA 或者 Vscode。很多剛開始寫程式碼的同學、甚至是一些經驗豐富的老手,都對開發工具保持絕對的信任。

比如你在鍵盤上敲擊 a,那編輯器介面上顯示的一定是 a

但是,由於記憶體不足等種種原因,開發工具其實也會抽風

比如你想要呼叫某個函式,通常敲擊函式名前幾個字母后,開發工具就會自動給你提示完整的函式名,但如果開發工具沒有給你提示,你首先懷疑的是這個函式不存在,而不是編輯器沒有按預期給出提示。遇到這種情況,可以稍等編輯器一下,或者進一步確認函式是否真的不存在,而不是立刻建立一個新的函式。

又或是專案無法執行,怎麼排查都覺得沒問題,這時不妨重新啟動下開發工具,或者清理一下快取,說不定專案就能正常運行了!

還有很多非常有意思的情況,比如編輯器一片大紅,各種提示錯誤,但是專案依然能成功執行。

因此,不要絕對相信開發工具。

2. 開源專案可信麼?

現在是一個軟體開源的時代,在 GitHub 等開源專案平臺上能夠找到大量優秀的開源專案,好的開源專案甚至可以得到 10 萬多個關注,那這些知名的開源專案可信麼?

不完全可信!從每個開源專案的 Issues 就能看出這點,而且通常越大的專案,被發現的問題越多,比如 Vue 專案,累積提出並關閉了 8000 多個問題。

我記得自己有一次使用知名的開源伺服器 Tomcat,就遇到了 bug,每次接受到特定的請求都會報錯。剛開始我根本沒有懷疑是 Tomcat 的問題,而是絞盡腦汁地想自己的程式碼哪裡寫錯了。後來經過反覆的排查和搜尋,終於確認了就是 Tomcat 本身的 bug!

雖然開源專案並不完全可信,但是相對於私有專案而言,所有對專案感興趣的同學可以共同發現專案中的問題,並加以解決,在一定程度上還是能夠提高專案的可靠性的。

3. 依賴庫可信麼?

我們在開發專案時,通常會用到大量的依賴庫。直接在官方依賴源(比如 Mavennpm)搜尋依賴庫,然後使用包管理器,用一行命令或者編寫配置檔案就能夠讓其自動安裝依賴,非常方便。

但是,這些釋出到官方源的依賴庫,就可信麼?

且不說基本每個開發者都有機會發布依賴庫到官方,就算是網際網路大公司的依賴庫,也未必可信。

給我印象最深刻的就是阿里巴巴的 JSON 序列化類庫 fastjson,幾乎無人不知、無人不曉,因為其極快的解析速度廣受好評。但是,這個庫被多次曝光存在高危漏洞,可以讓攻擊者遠端執行命令!一般的開發者根本不會發現這點,從而給專案帶來了極大的危害。

因此,在選用依賴庫的時候,要做好充分的調研,儘量確認依賴庫的安全,並且保證不要和已有的依賴衝突。

4. 程式語言可信麼?

Java 是一種強型別語言,具有健壯性。這句話我相信所有學過 Java 的同學都再熟悉不過了。但是,強型別程式語言就一定可信麼?

這裡可能有同學就要表示懷疑了,如果我們一直使用的最基礎最底層的程式語言都存在 bug,那我們怎麼去相信建立在這些程式語言上的框架呢?

然而真相是,所有的程式語言都有 bug!而且基本每次程式語言釋出新版本時都會對一些歷史 bug 進行修正。就 Java 而言,甚至還有一個專門記錄 bug 的資料庫!

但是,對於大多數開發者來說,我相信即使在程式中偶然觸發了程式語言本身的 bug,也沒有足夠的自信去質疑,而是直接修改程式碼來繞過。

確實,質疑程式語言需要一定的基礎和知識儲備,但是一旦發現了程式中莫名其妙的問題,建議大家不要直接忽略,可以花一些時間去探索研究,說不定你就成功地發現了一個重大的 bug,也能夠加深對這門程式語言的理解。

5. 伺服器可信麼?

伺服器是專案賴以生存的宿主,伺服器的效能和穩定性將直接影響到專案程序。

無論是個人開發者還是企業,通常都會直接租用大公司提供的雲伺服器來部署專案,省去了自己搭建和維護的麻煩。

但是大公司的雲伺服器就可信麼?

不完全可信!即使現在的雲伺服器提供商都承諾自己的服務 SLA(服務級別協議)可以達到 5 個 9(99.999% 一年約宕機 5 分鐘),甚至 6 個 9(99.9999% 一年約宕機 30 秒),但是仍然存在一定的風險。

有一個非常有名的案例,在 2013 年,中國最大的社交通訊軟體出現大規模的故障,多達幾億使用者受到影響。原因竟然是,市政道路建設的一個不注意,把網路光纜挖斷了,就導致該軟體所在伺服器的無法訪問。

除了可用性的不可信之外,可能還有一些安全隱私方面的問題。當然雲服務商通常是不會獲取使用者的資料的,但也沒有辦法絕對相信他們。畢竟資料的隱私對企業至關重要,這也是為什麼大的公司都會搭建屬於自己的伺服器機房和網路。

6. 資料庫可信麼?

企業中的大多數業務資料都是存放在資料庫中的,通過專案後端程式來操作和查詢資料庫中的資料。

和伺服器一樣,我們可以使用軟體自己搭建資料庫,比如 MySQL,也可以直接租用大公司的雲資料庫,那麼資料庫可信麼?

其實在企業後端專案中,資料庫通常是效能瓶頸,相對比較脆弱,當訪問併發量大一點時,資料庫的查詢效能就會下降,嚴重時可能整個宕機!即使是大公司提供的雲資料庫服務,遇到慢查詢(需要較長時間的查詢)時,可能也無從應對。

資料庫中的資料其實也未必可信,有時管理員的一個誤操作,不小心刪除資料或添加了一條錯誤資料,可能就會影響使用者,造成損失。更有甚者,竟然刪庫跑路,不講碼德!

因此,不要過於信任資料庫,應當使用快取之類的技術幫助資料庫分擔壓力,並定期備份。否則一旦資料庫宕機或資料丟失,帶來的損失是不可估量的!

7. 快取服務可信麼?

快取是開發高效能程式必備的技術,通過將資料庫等查詢較慢的資料存放在記憶體中,直接從記憶體中讀取資料,以提升查詢效能。有了快取之後,專案不僅能夠支援更多人同時查詢資料,還能夠保護資料庫。

目前比較主流的快取技術有 RedisMemcached 等,可以自己在伺服器搭建,也可以直接租用大公司提供的雲快取服務。

那麼快取服務是否可信呢?

專案的併發量不是特別大的話,一般的快取技術就足以支援了,但是如果專案的量級很大,可能快取也無法承受住壓力,嚴重時就會宕機。而一旦快取掛掉,大量的查詢命令會直接請求資料庫,於是資料庫也會在瞬間掛掉,嚴重時還會導致整個專案癱瘓!

因此,在使用快取時,需要對併發量進行評估,通過搭建叢集和資料同步保證高可用性。此外,還要預防快取雪崩、快取穿透、快取擊穿等問題,簡單解釋一下。

快取雪崩:指大量快取在同一時間過期,請求都訪問不到快取,全部打到資料庫上,導致資料庫掛掉。

快取穿透:持續訪問快取中不存在的 key,導致請求繞過快取,直接打到資料庫上,導致資料庫掛掉。

快取擊穿:一個被大量請求高頻訪問的熱點 key 突然過期,導致請求瞬間全部打到資料庫上,導致資料庫掛掉。

如果不預防這三個問題,即使是租用大公司的快取服務,也一樣吹彈可破。

8. 物件儲存可信麼?

專案中,經常會有使用者上傳圖片或檔案的功能,這類資料通常較大,用資料庫儲存不太方便。雖然我們可以將檔案直接存到伺服器上,但更好的做法是使用專門的物件儲存服務。

可以簡單地把物件儲存當做一個大的資料夾,我們可以通過它直接上傳和下載檔案。大的雲服務商也都提供了專業的物件儲存服務,而無需自己搭建,那麼物件儲存可信麼?

一般情況下,上傳到物件儲存的檔案是不會缺失或丟失的,而且還可以將已上傳的資料進行跨園區同步,起到備份的作用。

但是,記得有一次,上傳到物件儲存上的檔案和原始檔竟然不一致,大小足足少了 1M。起初我以為是檔案上傳到物件儲存時,會自動被壓縮,但是將物件儲存中的檔案下載到本地後,發現的確和原始檔不一致!雖然出現這種情況的概率極其小,但從那一刻起,我再也不相信物件儲存了。

再用自己的真實經歷來聊聊物件儲存的跨園區同步。因為個人負責的業務比較重要,萬一單個機房整體掛掉,可能分分鐘是幾十萬元的損失!因此我為物件儲存配置了自動跨園區同步,將檔案先上傳至廣州機房,然後資料會自動同步到上海機房,且運維同學承諾自動同步的延遲不超過 15 分鐘。

我相信大部分開發者配置資料同步後也就不管了,相信它一定會自動同步的。結果後面我編寫程式去做同步監控、對比資料時,發現經常出現數據未同步的情況,比例高達 10%!

因此,不能完全相信物件儲存,雖然大部分情況下大公司的物件儲存服務很可靠,但不能確保萬無一失。尤其是同步備份的場景下,是否真的同步成功了,又有多少同學關心過呢?不妨寫個程式去驗證和保障。

9. API 介面可信麼?

在開發中,我們經常會呼叫其他系統提供的 API 介面來輕鬆實現某種功能。比如查詢某地的天氣,可以直接呼叫其他人提供的天氣查詢介面,而無需自己編寫。我們也可以提供 API 介面給其他人使用,尤其是在微服務架構中,各服務之間都是以介面呼叫的形式實現互動協作的。

幾乎所有的 API 介面提供者都會說自己的介面有多安全、請放心使用,那麼 API 介面真的可信麼?

其實,API 介面是最不可信的資源!

首先,API 介面的提供方可以是任何開發者,很難通過他們的一面之詞來確定介面的穩定性和安全性。

即使這個介面效能很高、也很安全,但是你並不瞭解有多少人和你在同時使用這個介面,也許只有你,又也許是 100 萬個其他的開發者呢?在這個競爭條件下,介面的 qps(query per second 每秒查詢數)還能達到預期麼?介面返回時長真的不會超時麼?

更有甚者,偷偷地把 API 介面改動了,卻沒有給呼叫者傳送通知,這樣介面的呼叫方全部都會呼叫失敗,嚴重影響專案的執行!

因此,我們在呼叫第三方 API 介面時,一定要慎重、慎重、再慎重!

此外,如果我們是 API 介面的提供者,也要注意保護好自己的 API 介面,避免同時被太多的開發者呼叫,導致介面掛掉。

10. Serverless 可信麼?

如果說伺服器不可信,那我們乾脆就不租伺服器了,直接租用大公司提供的 Serverless 服務來作為專案的後臺不就行了?

Serverless 指無伺服器架構,並不是真的不需要伺服器,而是將專案介面的部署、運維等需要對伺服器的操作交給服務商去做,讓開發者無需關心伺服器,專心寫程式碼就好。

聽起來非常爽,那 Serverless可信麼?

使用 Serverless,雖然能夠大大提升開發和運維效率,但是其相對伺服器等資源而言,更不可信!

首先,Serverless 本身就是部署在伺服器上的,難免會受到伺服器的影響。

其次,Serverless 服務不會長期保持應用的狀態,而是隨著請求的到來而啟動,存在冷啟動時期,雖然也有很多相關的優化和解決方案,但仍無法精確地保證介面的效能,尤其是在高併發場景下,效能往往達不到預期。

最重要的是,當你選擇使用 Serverless 服務時,你就和某雲服務提供商綁定了,後續想要遷移是非常困難的!試想一下,你專案的所有功能都交給別人來維護,真的是好事麼?一旦雲服務提供商改造了架構或介面,你的程式碼也要隨之改動,而這種改動卻不是由自己控制的!

當然,Serverless 具有非常多的優點,也是雲端計算技術發展的必然趨勢,只是希望大家在使用前,考慮到那些可能的風險,並做好應對措施。

總結:正是因為我們太過信任那些名氣大、看似安全的資源,所以其背後的危險才更難以被察覺,帶來的後果往往也更致命!

防禦性程式設計

“居安思危,對隨時可能帶來的嚴重後果,要做到謹慎。”

在軟體開發中,雖然專案表面上能夠正常執行,但風險無處不在,因此我們要學習防禦性程式設計思想。把自己當成一個槓精,不要相信任何人,盡力去發現程式中的風險,積極防禦。

下面給大家分享 16 個防禦性程式設計的方法,學習之後,能夠大大減少程式中的風險。

1. 程式設計習慣

要減少程式中的風險,首先要養成良好的程式設計習慣。

首先,在寫程式碼時,一定要保持良好的心態,不要倉促或者以完成任務的心態去寫程式碼。如果僅僅是為了完成需求,那麼很有可能不會注意到程式碼中的風險,甚至是發現了風險也懶得去修補,這樣確實能夠節約開發的時間,但是後面出現問題後,你還是要花費更多的時間去排查、溝通和修復 bug。拔苗助長,適得其反。

在寫程式碼時,如果在一個地方多次使用相同且複雜的變數名或字串,建議不要手動去敲,而是用大家最喜歡的 “複製貼上”,防止因為手誤而導致的 bug。

此外,我們在程式碼中應該加強對返回值的檢查,並且選擇安全的語法和資料結構,避免使用被廢棄的語法。不同的程式語言也有不同的最佳程式設計習慣,比如在 Java 語言中,應該對所有可能為 NULL 的變數進行檢查,防止 NPE(NULL Pointer Error 空指標異常),在開發多執行緒程式時,選用執行緒安全的 ConcurrentHashMap 而不是 HashMap 等等。還可以利用 Assert(斷言)來保證程式執行中的變數值符合預期。

推薦使用一個自帶檢查功能的編輯器來書寫程式碼,在我們編寫程式碼時會自動檢查出錯誤,還能給出好的編碼風格的建議,能夠大大減少開發時的風險。此外,在程式碼提交前,一定要多次檢查程式碼,尤其是那些複製貼上過來的檔案,經常會出現遺漏的修改。提交程式碼後,也可以找有經驗的同事幫忙閱讀和檢查下程式碼(程式碼審查),進一步保證沒有語法和邏輯錯誤。

2. 異常處理

程式的執行風雲變幻,同一段程式碼在不同情況下也可能會產生不同的結果,甚至是異常。因此很多主流的程式語言中都有異常處理機制,比如在 Java 中,先用 try 捕獲異常、再用 catch 處理異常、最後用 finally 釋放資源和善後。

在程式設計時,要合理利用異常處理機制,來防禦程式碼中可能出現的種種問題。通常在異常處理中,我們會記錄錯誤日誌、執行錯誤上報和告警、重試等。

比如不信任資料庫,那就在查詢和操作資料時新增異常處理,一旦資料庫抽風導致操作失敗,就在日誌中記錄失敗資訊,並通過郵件、簡訊等告警方式通知到開發者,就能第一時間發現問題並排查。必要時還可以實現自動重試,省去一部分人工操作。

3. 請求校驗

所有的請求都是不可信的,哪怕是在公司內網,也有可能因為一些失誤,導致發出了錯誤的請求。

因此我們編寫的每個介面,在實現具體的業務邏輯前,一定要先對請求引數加上校驗,下面列舉幾種常見的校驗方式:

  1. 引數型別校驗:比如請求引數應該是 Integer 整型而不是 Long 長整數型別。

  2. 值合法性校驗:比如整數的範圍大於等於 0、字串長度大於 5,或者滿足某種特定格式,比如手機號、身份證等。

  3. 使用者許可權校驗:很多介面需要登入使用者或者管理員才能呼叫,因此必須通過請求引數(請求頭)來判斷當前使用者的身份,被一個普通使用者下載了 VIP 付費電影肯定是不合理的!

4. 流量控制

上面提到,所有的請求都是不可信的,不僅僅是請求的值,還有請求的量和頻率。對於所有介面,都要限制它的呼叫頻率,防止介面被大量瞬時的請求刷爆。對於付費介面,還要防止使用者對介面的請求數超過原購買數。

此外,還有一種容易被忽視的情況,假如你的介面 A 中又呼叫了其他人的介面 B,也許你的介面 A 自身的邏輯能夠承受每秒 1000 個請求,但是你確定介面 B 可以承受麼?

因此,需要進行流量控制,不僅僅是預防介面被刷爆,還可以保護內部的服務和呼叫。

什麼,你說你的介面很牛逼,每秒能抗 100 萬個請求,也沒有呼叫其他的服務,那我就找 100 萬 + 1 個人同時請求你的介面,看你怕不怕!

常用的流量控制按照不同的粒度可分為:

  1. 使用者流控:限制每個使用者在一定時間內對某個介面的呼叫數。

  2. 介面流控:限制一定時間內某個介面的總呼叫數。

  3. 單機流控:限制一定時間內單臺伺服器上的專案所有介面的總呼叫數。

  4. 分散式流控:限制一定時間內專案所有伺服器的總請求數。

當然,除了上面提到的幾種方式外,流控可以非常靈活,也有很多優秀的限流工具。比如 Java 語言 Guava 庫的 RateLimiter 令牌桶單機限流、阿里的 Sentinel 分散式限流框架等。

5. 回滾

有時,我們對專案的操作可能是錯誤的,可能是人工操作,也可能是機器操作,從而導致了一些線上故障。這時,可以選擇回滾。

回滾是指撤銷某個操作,將專案還原到之前的狀態,這裡介紹幾種常見的回滾操作。

資料回滾

有時,我們想要批量插入資料,但是資料插入到一半時,程式突然出現異常,這個時候我們就需要把之前插入成功的資料進行回滾,就好像什麼都沒發生過一樣。否則可能存在資料不一致的風險。

最常見的方式就是使用事務來處理資料庫的批量操作,當出現異常時,執行資料庫客戶端的回滾方法即可。

配置回滾

如果將專案的配置資訊,比如資料庫連結地址,寫死到程式碼中,一旦配置錯了或者地址發生變更,就要重新修改程式碼,非常麻煩。

比較好的方式是將配置釋出到配置中心進行管理,讓專案去動態讀取配置中心的配置。如果不小心釋出了錯誤的配置,可以直接在配置中心進行回滾,將配置還原。

釋出回滾

沒有人能保證自己的程式碼正確無誤,很多時候,專案在測試環境驗證時沒有發現任何問題,但是一上線,就漏洞百出。這就說明我們最新發布的程式碼是存在問題的。

這時,最簡單的做法就是進行版本回滾,將之前能夠正常執行的程式碼重新打包釋出。大公司一般都有自己的專案釋出平臺,能夠使用介面一鍵回滾,自動釋出以前版本的專案包。

6. 多級快取

上面提到,快取對專案是非常重要的,不僅是提升效能的利器,也是資料庫的保護傘。

但如果快取掛掉怎麼辦呢?

有兩種方案,第一種是為快取搭建叢集,從而保證快取的高可用。

但是一切都不可信,叢集也有可能掛掉!

那麼可以用第二種方案,一級快取掛掉,我們就再搞一個二級快取頂上!

通常,在高併發專案中,我們會設計多級快取,即分散式快取 + 本地快取。當請求需要獲取資料時,先從分散式快取(比如 Redis) 中查詢,如果分散式快取集體宕機,那就從本地快取中獲取資料。這樣,即使快取掛掉,也能夠幫助系統支撐一段時間。

這裡可能和一些多級快取的設計不同,有時,我們會把本地快取作為一級快取,快取一些熱點資料,本地快取找不到值時,才去訪問分散式快取。這種設計主要解決的問題是,減少對分散式快取的請求量,並進一步提升效能,和上面的設計目的不同。

7. 服務熔斷和降級

每年的雙十一,我們會準時守著螢幕上的搶購頁面,只為等待那一個 “請稍後再試!”

我們的專案其實遠比想象的要脆弱,很多服務經常因為各種原因出現問題。比如搞活動時,大量使用者同時訪問會導致對專案服務的請求增多,如果專案頂不住壓力,就會掛掉。

為了防止這種風險,我們可以採用服務降級策略,如果系統實在無法為所有使用者提供服務,那就退而求其次,給使用者直接返回一個 “友好的” 提示或介面,而不是強行讓專案頂著壓力過勞死。

配合服務熔斷技術,可以根據系統的負載等指標來動態開啟或關閉降級。比如機器的 CPU 被佔用爆滿時,就開啟降級,直接返回錯誤;當機器 CPU 恢復正常時,再正常返回資料、執行操作。

Hystrix 就是比較有名的微服務熔斷降級框架。

8. 主動檢測

上面提到,即使是大公司的同步服務,也可能會出現同步不及時甚至是資料丟失的情況。因此,為了進一步保證同步成功、資料的準確,我們可以主動檢測

比如編寫一個定時指令碼或者任務,每隔一段時間去檢查原地址和目標地址的資料是否一致,或者通過一些邏輯來檢查資料是否正確。當然也可以在每次資料同步結束後都立即去檢測,更加保險。

9. 資料補償

當檢測出資料不一致後,我們就要進行資料補償,比如將沒有同步的資料再次進行同步、將不一致的資料進行更新等。

除了用來解決主動檢測出的資料不一致,資料補償也被廣泛用於業務設計和架構設計中。

比如呼叫某個介面查詢資料失敗後,停頓一段時間,然後自動重試,或者從其他地方獲取資料。又如訊息佇列的生產者傳送訊息失敗時,應該自動進行補發和記錄,而不是直接把這條訊息作廢。

資料補償的思想本質上是保證資料的最終一致性,資料出錯不可怕,知錯能改就是好孩子。這種思想也被廣泛應用於分散式事務等場景中。

10. 資料備份

資料是企業的生命,因此我們必須儘可能地保證資料的安全和完整。

很多同學會把自己重要的檔案存放在多個地方,比如自己的電腦、網盤上等等。同樣,在軟體開發中,我們也應該把重要的資料複製多份,作為副本存放在不同的地方。這樣,即使一臺伺服器掛了,也可以從其他的伺服器上獲取到資料,減少了風險。

11. 心跳機制

介面可是個複雜多變的傢伙,如果我們的專案依賴其他的介面來完成功能,那麼最好保證該介面一直活著,否則可能會影響專案的執行。

舉個例子,我們在使用銀行卡支付時,肯定需要呼叫銀行提供的介面來獲取銀行卡的餘額資訊,如果這個介面掛了,獲取不到餘額,使用者也就無法支付,也就損失了一筆收入!

因此,我們需要時刻和重要的介面保持聯絡,防止他們不小心死了。可以採用心跳機制,定時呼叫該介面或者傳送一個心跳包,來判斷該介面是否仍然存活。一旦呼叫超時或者失敗,可以立刻進行排查和處理,從而大大減少了事故的影響時長。

12. 冗餘設計

在系統資源和容量評估時,我們要做一些冗餘設計,比如資料庫目前的總資料量有 1G,那麼如果要將資料庫的資料同步到其他儲存(比如 Elasticsearch)時,至少要多預留一倍的儲存空間,即 2G,來應對後面可能的資料增長。業務的發展潛力越大,冗餘的倍數也可以越多,但也要注意不要過分冗餘,畢竟資源也是很貴的啊!

其實,冗餘設計是一種重要的設計思想。當我們設計業務或者系統架構時,不能只侷限於當前的條件,而是要考慮到以後的發展,選擇一種相對便於擴充套件的模式。否則之後專案越做越大,每一次對專案的改動都步履維艱。

13. 彈性擴縮容

夢想還是要有的,說不定突然,我們原先只有 100 人使用的小專案突然就火了,有幾十萬新使用者要來使用。

但是,由於我們的專案只部署在一臺伺服器上,根本無法支撐那麼多人,直接掛掉,導致這些使用者非常掃興,再也不想用我們的專案了。

這也是常見的風險,我們可以使用彈性擴縮容技術,系統會根據當前專案的使用和資源佔用情況自動擴充或縮減資源。

比如當系統壓力較大時,多分配幾臺機器(容器),當系統壓力較小時,減少幾臺機器。這樣不僅能夠有效應對突發的流量增長,還能夠在平時節約成本,並省去了人工分配調整機器的麻煩。

14. 異地多活

前面提到,伺服器是不可信的,別說一個伺服器掛掉,由於一些天災人禍,整個機房都有可能集體掛掉!

和備份不同,異地多活是指在不同城市建立獨立的資料中心,正常情況下,使用者無論訪問哪一個地點的業務系統,都能夠得到正確的服務,即同時有多個 “活” 的服務。

而某個地方業務異常的時候,使用者能夠訪問其他地方正常的業務系統,從而獲得正確的服務。

如此一來,即使廣州的機房跨了,咱還有上海的,上海的跨了,咱還有北京的。

同時活著的服務越多,系統就越可靠,但同時成本也越高、越複雜,因此幾乎都是大公司才做異地多活。千萬不要讓正常情況下的投入大於故障發生的損失!

15. 監控告警

專案的執行不可能一直正常,但是我們不可能 24 小時盯著電腦螢幕來監視專案的執行情況吧?又不能完全不管專案,出了 bug 等著使用者來投訴。

因此,最好的方式是給業務新增監控告警,當程式出現異常時,資訊會上報到監控平臺,並第一時間給開發者傳送通知。還可以通過監控平臺實時檢視專案的執行情況,出了問題也能更快地定位。

16. 線上診斷和熱修復

既然程式世界一切都不可信,危險無處不在,那麼幹脆就做最壞的打算,假設線上程式一定會出 bug。

既然防不勝防,那就嚴陣以待,在 bug 出現時用最快的速度修復它,來減少影響。

通常,我們要改 bug,也需要經歷改動程式碼、提交程式碼、合併程式碼、打包構建、釋出上線等一系列流程。等流程走完了,可能系統都透心涼了。

為提高效率,我們可以使用線上診斷和熱修復技術。在出現 bug 時,先用線上診斷工具輕鬆獲取專案的各執行狀態和程式碼執行資訊,提升排查效率。發現問題後,使用熱修復技術直接修改執行時的程式碼,無需重新構建和重啟專案!

Java 中,我們可以使用阿里開源的診斷工具 Arthas,同時支援線上熱修復功能。也可以自己編寫指令碼來實現,但是相對複雜一些。


看到這裡,肯定有同學會吐槽,怎麼寫個程式要考慮那麼多和功能無關的問題。本來五分鐘就能寫完的程式碼,現在可能一個小時都寫不完!

其實,並不是所有的專案都要做到絕對的安全(當然我們也做不到),而是我們應該時刻保持居安思危的思想,把防禦性程式設計當做自己的習慣。

實際情況下,要根據專案的量級、受眾、架構、緊急程度等因素來綜合評估將專案做到何種程度的安全,而不是過度設計、杞人憂天。

讓我們把時間慢下來,在開發前先冷靜思考,預見並規避風險,不要讓達摩克利斯之劍落下。