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

↑ ???? 萬字長文不想看,那就聽一聽叭 ↑

爲什麼你的程序總是出現 bug?

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

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


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

到底有多低呢?

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

那項目出現 bug 怎麼辦呢?

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

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

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

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

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

“喂,你的程序有 bug,這裏用戶下單怎麼金額是負的?”

寫個 bug

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

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

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

二八原則 開發 1 天,改 bug 4 天

這件事之後,我意識到,在企業中開發項目,不能只追求開發時的效率,還要注重項目的穩定性,否則帶來的額外返工時間遠比開發時節省的時間要長,而且會影響同事對你的看法。如果將開發時產生的 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 多個問題。

Vue 項目的問題

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

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

3. 依賴庫可信麼?

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

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

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

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

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

4. 編程語言可信麼?

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

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

然而真相是,所有的編程語言都有 bug!而且基本每次編程語言發佈新版本時都會對一些歷史 bug 進行修正。就 Java 而言,甚至還有一個專門記錄 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 接口,避免同時被太多的開發者調用,導致接口掛掉。

API 存在複雜的調用關係

10. Serverless 可信麼?

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

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

docker 容器

聽起來非常爽,那 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 個人同時請求你的接口,看你怕不怕!

DDOS 分佈式拒絕服務攻擊

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

  1. 用戶流控:限制每個用戶在一定時間內對某個接口的調用數。

  2. 接口流控:限制一定時間內某個接口的總調用數。

  3. 單機流控:限制一定時間內單臺服務器上的項目所有接口的總調用數。

  4. 分佈式流控:限制一定時間內項目所有服務器的總請求數。

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

Sentinel 流控面板

5. 回滾

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

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

數據回滾

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

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

配置回滾

如果將項目的配置信息,比如數據庫鏈接地址,寫死到代碼中,一旦配置錯了或者地址發生變更,就要重新修改代碼,非常麻煩。

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

發佈回滾

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

這時,最簡單的做法就是進行版本回滾,將之前能夠正常運行的代碼重新打包發佈。大公司一般都有自己的項目發佈平臺,能夠使用界面一鍵回滾,自動發佈以前版本的項目包。

6. 多級緩存

上面提到,緩存對項目是非常重要的,不僅是提升性能的利器,也是數據庫的保護傘。

但如果緩存掛掉怎麼辦呢?

有兩種方案,第一種是爲緩存搭建集羣,從而保證緩存的高可用。

Redis 集羣

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

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

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

這裏可能和一些多級緩存的設計不同,有時,我們會把本地緩存作爲一級緩存,緩存一些熱點數據,本地緩存找不到值時,纔去訪問分佈式緩存。這種設計主要解決的問題是,減少對分佈式緩存的請求量,並進一步提升性能,和上面的設計目的不同。

多級緩存設計

7. 服務熔斷和降級

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

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

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

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

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

Hystrix

8. 主動檢測

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

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

主動檢測

9. 數據補償

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

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

比如調用某個接口查詢數據失敗後,停頓一段時間,然後自動重試,或者從其他地方獲取數據。又如消息隊列的生產者發送消息失敗時,應該自動進行補發和記錄,而不是直接把這條消息作廢。

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

10. 數據備份

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

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

數據備份

11. 心跳機制

接口可是個複雜多變的傢伙,如果我們的項目依賴其他的接口來完成功能,那麼最好保證該接口一直活着,否則可能會影響項目的運行。

舉個例子,我們在使用銀行卡支付時,肯定需要調用銀行提供的接口來獲取銀行卡的餘額信息,如果這個接口掛了,獲取不到餘額,用戶也就無法支付,也就損失了一筆收入!

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

心跳檢測

12. 冗餘設計

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

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

13. 彈性擴縮容

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

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

夢想破碎了

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

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

14. 異地多活

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

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

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

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

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

餓了麼異地多活技術實現(一)總體介紹

15. 監控告警

項目的運行不可能一直正常,但是我們不可能 24 小時盯着電腦屏幕來監視項目的運行情況吧?又不能完全不管項目,出了 bug 等着用戶來投訴。

因此,最好的方式是給業務添加監控告警,當程序出現異常時,信息會上報到監控平臺,並第一時間給開發者發送通知。還可以通過監控平臺實時查看項目的運行情況,出了問題也能更快地定位。

Grafana 監控平臺

16. 線上診斷和熱修復

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

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

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

爲提高效率,我們可以使用線上診斷和熱修復技術。在出現 bug 時,先用線上診斷工具輕鬆獲取項目的各運行狀態和代碼執行信息,提升排查效率。發現問題後,使用熱修復技術直接修改運行時的代碼,無需重新構建和重啓項目!

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

Arthas Logo

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

超兇

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

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

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

點小花花,讓他們知道你 “在看” 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章