不當愣頭青、聊聊軟體架構中的那些慣用的保命手段

是Vzn呀發表於2024-11-13

大家好,我是vzn呀,又見面了。

前不久出了個有意思的事情:

某平臺UP主釋出了一段小米SU7碰撞的測評影片,表示碰撞後小米SU7的小電瓶出現故障導致車門打不開、緊急呼叫系統失靈等問題,引起不小轟動。就在大家都在吃瓜看小米如何應對時,小米官方丟擲了一份內部調查報告,重點就一個:我還藏了個備用電源!我把過程上報到國家監控平臺了!!你在黑我!!!。這劇情反轉程度,像極了重生小說中的橋段,小米設計師似乎預料會有這麼一出,提前藏了個小電瓶,就是為了等待這一刻的絕地反殺。

拋去吃瓜的爽文成分,深入其核心,我們不難發現,小米SU7備用電源的設計思路,在軟體領域有著廣泛的共鳴和深刻的意義。在軟體開發與設計的廣闊天地裡,面對的是一個充滿變數的世界,其中既包括普通使用者的日常操作,也涵蓋了網路故障硬體崩潰乃至惡意攻擊的種種挑戰。按照墨菲定律的闡述,只要機率大於0%的事情就很容易會發生(說的通俗點,就是怕什麼就會來什麼)。做好異常場景的應對,是一個成熟程式設計師的進階必修課,也是一個軟體系統線上平穩執行的內在基石。

說到這裡,就不得不提到系統架構設計的一個深層哲學,它強調的是對未知風險的敬畏與思考。一個優秀的系統架構設計,應當能夠預見並應對未來可能出現的各種挑戰,要容忍並接受區域性錯誤存在客觀性並努力將區域性錯誤控制在一定範圍內,同時當系統出現不可逆災難時可以儘量最大程度的保障系統核心業務的持續可用。

本篇文章,我們就來聊聊軟體開發中針對系統容錯能力以及災難應對能力的考量。

1. 容錯設計,再給一次機會

我們開發的一款軟體,上線執行後面臨的情況是極其複雜的,說不出錯,幾乎是不可能的:

上游輸入異常引數怎麼辦
外部介面掛了怎麼辦
依賴出現故障怎麼辦
網路抖動導致請求失敗了怎麼辦?
硬碟壞了怎麼辦?

前面也說過,異常是必定存在的客觀事實。如果我們一味的追求絕對的0偏差,其實是自己為難自己。所以呢,為了提升系統應對異常情況的能力,考慮增加一些容錯設計,便是一個不錯的思路。允許有限範圍的異常、嘗試去包容這些異常,儘量保證最終符合預期的目標達成即可。

容錯能力的實現,有很多種方式,典型的就是重試機制補償策略

1.1. 重試:浪子回頭金不換

重試是為了降低異常情況的出現給業務請求造成的影響,儘量保障請求按預期被處理的一種常用方式。那是否所有的失敗場景都需要重試呢?顯然不是,比如登入密碼校驗的時候,輸入了一個錯誤的密碼導致鑑權失敗,這種不管重試多少次永遠都依舊是失敗。一般而言,只有受一些瞬時偶然因素干擾的失敗場景,才需要考慮重試策略。比如:

  1. 某次對外網路請求的時候,因為網路抖動等原因導致的請求失敗,或者是請求處理超時,可以透過有限次數的重試,來提升對外互動處理的成功率
  2. 分散式系統中,某個節點服務異常,閘道器將請求重新分給另一個節點進行重新處理,提升整個叢集的容錯能力
  3. 搶奪分散式鎖的排隊處理場景中,某次沒有獲取到鎖,等待一段時間後再次嘗試獲取鎖

按照重試觸發時機的不同,重試策略可以分為立即重試延時重試

觸發時機 適用場景
立即重試 適用於一些因為偶然因素導致的失敗,比如請求的時候如果因為網路抖動導致的連結失敗,可以嘗試立即重試。
延時重試 適用於因為資源受限引發的失敗場景。比如對外請求的時候,由於下游介面流量過大觸發限流導致的失敗,如果立即重試,大機率重試依舊失敗,這種場景就可以考慮等待一定時間後再重試,以提升成功率。

而根據重試操作的具體實現邏輯,還可以分為原路重試差異重試

重試類別 場景舉例
原路重試 (1)呼叫三方系統HTTP介面,出現響應超時或者網路不通等異常情況,重新發出一次請求
(2)獲取分散式鎖失敗的時候,嘗試重新請求獲取
差異重試 (1)分散式系統中,一個節點的請求處理失敗後,閘道器將請求分發到另一個節點進行重試
(2)從Redis中獲取資料失敗,嘗試從MySQL中撈取資料

在重試機制落地的時候,還有2個基礎的原則不能忽略:

  1. 限定重試次數: 保證極端情況下,系統不會陷入無限迴圈重試。
  2. 重試次數要合理:避免過多的重試,浪費系統資源,不要為了重試而重試。

此外,在一個較長的處理鏈路上,如果涉及到重試的環節過多,還需要考慮引發請求風暴的風險。比如下圖的場景,假定重試的最大次數限制為N:

所以重試手段並非是零成本的,它的使用也是有副作用的,尤其是在一些複雜鏈路場景中。為了規避連環重試可能導致的連環風暴隱患,還需要引入一些輔助手段來應對。

  • 打破鏈式重試

請求風暴的形成,是因為最末端的異常被無限制透傳給了所有上游環節,然後觸發了上游環節的反覆重試,將請求數量指數級放大。但是實際上,僅僅是最後一個節點與DB之間的請求出現問題,其實只需要重試這個操作即可,上游節點並不需要重試。為了實現這一效果,需在請求互動層面進行規劃,透過返回值、返回碼等方式,告知上游節點是否需要重試,將重試的範圍限定在故障發生位置,而非全鏈路的鏈式連鎖反應。

  • 結合熔斷策略

結合熔斷機制,根據該路請求處理的失敗率進行判斷,達到一定閾值的時候,直接執行熔斷操作。後續透過一定的探測機制,分配少量的試探性流量,如果成功率達到設定閾值,則恢復此鏈路的後續處理。

1.2. 補償:亡羊補牢猶未晚

上面介紹的重試手段,主要目的是為了儘可能的提升當次操作的成功率。但是,總有一些異常場景不是即時重試就可以解決的。比如在一些大型的微服務分散式系統中,一個請求流程會跨越多個服務進行處理,且請求的處理往往是非同步的,如果出現重試也無法解決的異常問題,就需要額外的補償機制,對處理結果的最終一致性進行保障。

補償機制經常被使用在分散式系統中,它的一個核心前提是,允許並接受過程中的暫時性資料問題,並透過補償措施,保證最終的資料一致性。那麼,如何知曉是否需要執行補償操作、哪些資料需要執行補償操作呢?這就需要“對賬”了。

所謂“對賬”,就是定期將此前一段時間內的業務處理資料進行盤點比對一下,找到資料層面不符合預期的資料。基於對賬發現的異常記錄,再執行對應的補償修正處理。

舉個例子:

一個電商平臺系統,其訂單系統的設計,買家的訂單和賣家的訂單是分庫儲存的。一個訂單建立並付款完成之後,訂單資訊會流轉到下游消費服務中被各自處理,並分別寫入到買家訂單庫和賣家訂單庫中。

在微服務化場景下,雖然可以透過一些分散式事務等手段來加以防範,但依舊可能會因為一些極端情況,導致一個訂單沒有被同時成功寫入到買家訂單庫和賣家訂單庫中,這樣就可能會使用者的使用造成影響。這種情況下,就可以考慮搞個定時任務,定期掃描下一段時間內的訂單資料,校準下兩邊的差異,然後針對異常資料進行處理修正。如下所示:

這樣,基於事後對賬+補償的雙重手段,保障了系統的“最終一致性”目標達成。

2. 顧全大局,舍小義而謀大利

還有一些業務場景,它可能是牽扯到多個並列的依賴方,並最終訴求是將多個依賴方的結果混合在一起。這種情形下,某一個依賴方出現問題,對終端使用者的使用體驗而言影響很有限、甚至是無感的。一損俱損顯然不是最優解,棄卒保車會更為合理些。

舉個例子, 一款新聞資訊類的軟體,首頁的內容流列表由多路資料來源彙總而成:

  1. 即時突發新聞
  2. 熱門時政要文
  3. 關注賬號發文
  4. 可能感興趣內容
  5. 付費推廣內容
  6. xxx

最終多個來源的資料,會被混合成一個列表內容流展示給使用者。這個過程中,如果其中某一路(比如:即時突發新聞)出現異常未獲取到資料,對使用者而言其實是無感的,因為使用者也不知道究竟是系統出問題了、還是確實沒有即時突發新聞。但是因為某一路資料的獲取失敗,直接給使用者報錯異常、或者給使用者一個白屏顯示,反而是將使用者給放大了。

在實際的專案中,當故障的出現已經不可避免且無法規避或者重試解決的時候,為了避免問題的進一步擴大,透過一定程度的“妥協”與“捨棄”,以儘量小的損失、避免故障影響面的放大,也是一種常規操作,實現手段有很多,主流的有降級、限流、熔斷、放通、隔離等。

2.1. 降級

降級作為一種兜底策略,通常是在故障場景下從業務層面作出的一種妥協策略。 一般是遇到區域性功能障礙、或者資源負載層面問題的時候的一種應對方案。當出現某些突發情況,導致系統資源不足以支撐全量業務功能的正常開展時,為了將有限資源集中起來保障核心功能的可用,而主動將一些非核心的功能停用的思路。

降級的使用場景很多,比如:

  1. 電商每年的618或者雙11等大促時節,為了保障搶購下單的正常推進,將訂單評價、歷史訂單查詢等非核心功能先降級停用,所有資源全力支撐商品的瀏覽、下單、付款等操作
  2. 互動社交平臺,突發超級流量明星的大瓜新聞時,降低一些非核心功能(推廣、關注流)的更新頻率,將更多資源用以支撐爆炸性話題的訪問與互動瀏覽操作
  3. 對於即時通訊IM類場景,如果出現網路故障原因導致機房頻寬承壓有限,那就降級讓影片和語音類服務不可用,盡力保障文字訊息功能依舊可用

降級的本質,就是一個取捨的過程,捨棄不在乎的部分,保住最在乎的部分。捨棄誰、保全誰,需要根據自身業務的特徵來判斷。一般而言,有幾個維度:

降級維度 場景舉例
降低使用者體驗 介面重新整理不及時、不展示動效、不展示高畫質圖、不顯示系統推送通知
捨棄部分功能 不允許檢視歷史訂單、不允許資料匯出操作、不允許上傳檔案操作
安全性讓步 不做複雜二次校驗、跳過風控判斷、不記錄操作日誌
降低準確性 列表資料更新不及時、統計報表更新不及時
降低一致性 列表顯示的評論數與點選進去正文顯示的評論數不一致,已經刪除的文章依舊出現在列表中
降低資料量 訂單中心只顯示最近100條記錄,僅可以查詢最近1年資料

實施降級操作的前提,需要系統業務規劃層面進行配合,要做好系統業務功能的SLA規劃,劃分出核心功能與非核心功能。同時,在系統的架構層面要做好核心功能與非核心功能的解耦與隔離。

2.2. 限流

一般在春節、五一、或者國慶等節假日,一些熱門的景區都會限制進入景區的客流量,以此保證遊客的遊覽體驗與人身安全。同樣道理,軟體系統受限於自身實現、業務規劃以及硬體資源承載能力等諸多限制,其承壓能力也是有上限的。如果請求流量突增且明顯超出系統規劃的可承受範圍的時候,可能會引發系統當機等事故。為了保障系統安全,避免突發流量對系統的正常執行造成衝擊,就需要對進入系統的流量進行限制管控。

限流一般可以依據兩個維度進行實施:

限制維度 場景舉例
限制併發數 比如限制連線池的連線數、執行緒池的執行緒數等。
限制QPS 限制每秒進入的請求量。

限流操作的實現,離不開限流演算法,主流的有漏桶演算法令牌桶演算法

  1. 漏桶

漏桶演算法的原理很簡單,它不限制流入的請求量,但是會以一個相對受限的速度從漏桶中獲取請求進行消費處理,如果流出速度小於流入速度,請求就會在漏桶中積壓暫存等待順序被處理,一旦漏桶容量被積壓的請求撐滿,便會發生溢位,無法進入漏桶的請求將被丟棄。

正如其名字一般,漏桶的原理,像極了生活中使用的漏斗。這也是一個示例,再次印證了軟體架構設計中的很多實現與處理策略,都是來源於最質樸的生活。

  1. 令牌桶

令牌桶的邏輯與漏桶略有不同,它會有個令牌發放模組負責勻速生成令牌並放入到令牌桶中,然後每次請求處理前先嚐試獲取一個令牌,只有獲取到令牌了才會去處理對應的請求。

值得注意的一點是,雖然令牌是設計成勻速生成並放入到令牌桶中的,但這並沒法保證請求一定會被勻速處理。極端情況下,可能會出現短暫請求量突破限速值的情況(比如:大部分時候請求量小於令牌生成量,導致桶內蓄滿令牌,突然來波大流量,會一口氣消耗掉令牌桶中全部的存量令牌),所以需要根據系統設計的承壓負載情況,合理設定限流的閾值。但這一設計也有其優勢,偶爾短暫的脈衝波動可以儘量消化掉,同時又保證長期整體處理速率處於一個受控狀態。

還有一種簡陋的基於計數器的“偽限速”方案,這一思想很簡單,每個計數週期維護一個計數器,然後來一個請求計數器就累加1次,計數滿閾值後便拒絕後續請求,直到下一週計數器重新計數。這種本質上只能控制流量、無法控制過程流速,極端情況下的一些請求峰值,極有可能會擊垮系統,要儘可能將流控計數週期設定的短一些,儘量避免在核心重要系統中使用此方案。

此外,對於一些叢集化多節點部署的場景,規劃限流的時候,還需要關注是單機的流量限制,還是叢集整體的流量限制,選擇適合自己業務的實現方案。

2.3. 熔斷

熔斷在現實世界中最直觀的應用,就是家裡強電箱裡空氣開關中的保險絲了。當電流超載的時候,保險絲就會斷開,以此來保護家裡的整體電路不會燒掉,以及各種電器不受損壞。

同樣道理,在軟體實現中,也有類似電路保險絲一般的設計思路,透過在服務的對外請求呼叫處增加熔斷器,當符合預設條件時,就會將對應依賴的服務從自身的請求鏈路中剔除,來避免自身節點耗費大量的資源在等待一個大機率錯誤的響應。

熔斷,是自身的一種自保手段,目的是防止外部節點異常將自己耗死。熔斷的策略一般有兩種:

  1. 按照請求的失敗率進行熔斷

短期內發往某個目標服務的請求失敗率高於某個閾值則執行熔斷策略,不再繼續呼叫此目標服務。然後透過定期的心跳探測機制,或者少量試探流量的方式,決定繼續熔斷還是恢復請求。

  1. 按照請求響應時長進行熔斷

對於一些高併發量的處理場景,如果呼叫的目標服務的請求時延過大,勢必會拖累整體系統的吞吐量。這種情況下,為了保障自身節點的處理效能,也可以按照請求響應時長,決定是否觸發熔斷操作。

此外,在叢集部署環境下,閘道器節點也經常會將熔斷作為基礎功能進行提供,實現比服務熔斷更細粒度的一種控制,當服務叢集中某個節點出現故障時,直接將該節點剔出叢集,待其恢復之後再加入到叢集中。

具體應用的時候,可以直接使用一些成熟的開源方案,比如Hystrix或者Sentinel等。需要強調的一點是,熔斷一般針對的目標是一些非核心、非必須的依賴服務,本質上,熔斷也是降級的一種實現形式。

2.4. 隔離

隔離作為一種故障控制手段, 其設計思想是透過將資源分隔開,互不干擾,這樣系統出現故障的時候,就可以將故障限定在一定的傳播範圍內,避免出現滾雪球效應、波及全域性。常見的隔離措施,有資料隔離機器隔離執行緒池隔離以及訊號量隔離等等。

  • 資料隔離

最直觀的表現,就是分庫分表了。比如對系統的資料,按照業務維度進行分庫儲存,或者按照業務的重要度進行識別,將資料識別為重點資料/非重點資料,亦或是保密資料/非保密資料,然後按照細分後的結果實施差異化的資料儲存保障策略。比如,對於非重點資料,簡單的搞個一主一從雙副本即可, 而重點資料,可能得考慮異地多副本可靠儲存與備份。

  • 機器隔離

不同的業務使用不同的機器,從硬體資源層面進行隔離。透過將機器分組的方式,針對重點服務或者是高危服務實現專機專用,而對應一般普通服務,則可以多個業務混用同一套機器,從而實現了差異化的隔離處置。

  • 執行緒池隔離

隔離的思想,不僅僅是體現在資料層面或者是程序機器節點等宏觀層面,該思想同樣適用於對單個程序內部的實現。因為同一個程序內處理很多不同的邏輯,如果某個處理邏輯無限制的建立執行執行緒,佔據了全部的系統CPU資源,則整個程序中其餘的邏輯就會受到影響。

為了應對這種情況,可以基於執行緒池進行隔離設計,為主要業務處理方法指定對應的執行執行緒池,限定具體業務方法僅可以按照分配執行緒池提供的執行緒資源進行排程與使用,禁止業務方法自行無度佔用系統的執行緒與CPU執行資源。這樣一來,即使某個業務佔用了自己全部執行緒池資源,依舊不會影響到其餘執行緒池的正常處理,保障了其餘業務的正常開展。

因為執行緒池的維護也會佔用額外的資源,所以隔離的粒度的把控也要做到適可而止,遵循適度原則。

3. 硬體災備:鈔能力帶來的超能力

軟體的順暢執行與硬體的穩健支撐密不可分。儘管軟體層面透過巧妙的容錯設計、靈活的降級策略以及精準的限流機制等手段,能夠顯著提升其自恢復能力和可用性,但在面對硬體故障這一硬性挑戰時,單純依賴軟體手段就顯得力不從心。所以,在設計規劃建設一套可靠的軟體服務的整體架構時,硬體部署規劃時的可靠性設計,也是無法迴避的話題。

相較於軟體層面的各種容錯策略,硬體層面的應對就顯得簡單且粗暴————堆資源!即透過資源的冗餘部署來增強系統的容錯能力。當然,這一策略的實施不可避免地會增加經濟成本,所以具體實施與規劃的時候,還需要結合預算情況,在成本許可範圍內實現可靠性的最大化保障能力。

常見的硬體層冗備的實踐,一個是保障業務應用高可用的多活機制,另一個是為了保障資料可靠儲存的多副本儲存機制

3.1. 多活

隨著越來越多的生活場景被搬到線上處理,網際網路白熱化時代,對與業務的7*24小時持續可用提出了嚴峻的挑戰。但對於一個軟體服務來說,不管架構多麼完美、程式碼多麼優雅,最終程式都得執行在硬體基礎之上,而硬體層面的風險,是程式碼無法左右的。那麼如何應對硬體的各種損壞或者不可用風險呢?很簡單,花錢消災!多花點錢,多搞點硬體資源,多部署幾套服務就行咯。但是這個部署多套,實際也是有講究的。

為了應對不同層級的風險,也引申出了多種不同的堆硬體的方式:

  • 叢集化

為了應對單臺伺服器硬體的損壞、比如硬碟損壞、電源燒燬等, 單個機房內部署多個節點,由多個不同的機器,共同組成一個叢集,這樣其中一個節點故障,其餘節點依舊可以正常處理業務,有效避免了單點故障的出現機率,提升了業務的可靠性。

  • 同城雙活

上述在同一機房內利用多個節點組成叢集的方式,雖然能應對單臺機器的故障場景,但如果機房出現整體故障,比如停電、著火、光纜被挖斷等情況,依舊會導致全軍覆沒。為了應對這一可能的風險,自然而然的解決方案就是再建一個機房,這樣兩個機房互為備份,風險就大大降低了。通常而言,兩套機房之間會涉及到資料的同步,所以對機房之間的網路傳輸速度與時延有極高要求,這就要求兩個機房不能離得太遠,最好在同一個城市。 —— 這便形成了常說的同城雙活架構。

  • 兩地三中心

基於同城雙活的模式,其可靠性已經可以滿足大部分普通業務場景對於系統可靠性的訴求了。但若業務系統極其重要,尤其是一些金融、社交、基礎服務提供商等牽扯到國計民生的領域,對系統的可靠性與資料的安全性有更加苛刻的要求。在同城雙活架構中,為了控制機房間網路時延,兩個機房的距離都不會太遠,萬一出現某些不可抗力的自然災害(比如地震)造成兩個機房全部損壞,依舊會導致業務或資料受損。所以如何應對?答案已經呼之欲出了,跨不同城市多建一些機房唄!於是乎,兩地三中心三地五中心等等解決方案應運而生。

看到沒?系統的可靠程度,一定程度上,取決於堆的鈔票的厚度。

3.2. 多副本

冗餘備份,又叫做多副本。本質上就是為了防止單點故障造成資料層面的丟失,而採取的將同一份資料分散在多個位置儲存多份的一種方式。這種方式會造成額外的資源成本支出,但其所帶來的資料可靠性與高可用性,是“孤本”無法比擬的。

多副本的策略,廣泛的被應用在各種資料儲存元件中。比如:

  1. 本地快取多副本
  2. Redis多副本
  3. MySQL的多副本
  4. Kafka多副本

最常見且最簡單的多副本策略,就是Master-Slave這種架構,類似MySQL的一主多從架構。在這種架構中,通常由Master節點負責資料的寫操作,然後透過內在的資料同步機制,將資料變更同步更新到各個Slave節點進行資料的多副本儲存。為了提升硬體的利用率,Slave節點除了用於資料內容的多副本可靠儲存,還可以對外提供只讀查詢操作,在必要場景下支撐業務的讀寫分離訴求。

Master-Slave這種主從架構的多副本策略有個致命的問題,就是每臺節點儲存的都是全量的資料檔案,這使得資料總量受限於單機儲存,存在瓶頸。對於超大資料量場景,還會需要更加複雜的多副本方案,對總體資料進行切片,對每個切片資料進行多副本支援,進而可以支援容量的水平擴充套件。像Redis叢集或者是kafka所採用的便是這種策略。

  • 分片儲存形態1:分散在不同機器上儲存

  • 分片儲存形態2: 多叢集承載分片模式

這種對資料進行分片並分散在多臺物理儲存節點的方式,打破了單機容量的限制,但是也增加了資料讀寫與資料同步的複雜度。因為資料分散在多個節點上,所以在讀寫的時候,需要支援將請求路由分發到資料分片所在的節點上,比較常見的是使用一致性Hash演算法來進行分片。此外,各節點上分片資料的同步與一致性保障也需要更加複雜的處理邏輯來支撐,比如Kafka就專門設計了ISR演算法來處理多副本之間的資料同步。

4. 人工干預,保證對系統的控制權

我們按照業務場景與業務訴求進行功能實現的時候,會預先設想好各個場景的處理與應對,也會考慮一些可能異常場景的程式碼層面自動相容與應對。但可能會有一些場景,它就是突破了我們預先對系統設定的一切合理規劃,或者系統出現一些未曾預料到的場景無法自恢復的時候、亦或是自動恢復或回滾處理的影響面太大的時候,都可能需要人工介入處理。所以,系統在規劃與實現的時候,很有必要構建一些人工干預手段與能力。

這種人工干預能力,有很多實際的應用場景,可以用來提升運維人員對系統的高度掌控力從而更好的應對各種突發場景,也可以作為運營人員的一種高許可權後臺處置權進行預留。

4.1. 人工介入應急處置能力

先看個例子:

背景:
某個業務需要從遠端資料來源獲取資料並進行業務邏輯處理,由於業務本身屬於特別核心且重要的服務,遠端資料來源數屬於外部依賴,資料準確性與服務可用性不可控。

實現:

  1. 業務處理的時候,定期從遠端進行資料來源拉取更新,優先使用遠端資料來源的資料。
  2. 為了應對遠端資料來源不可控風險,定期更新的時候都會將遠端獲取到的資料寫一份到本地磁碟中進行備份,本地磁碟儲存最近N個的備份。
  3. 如果遠端服務資料拉取失敗,則業務自動嘗試從本地讀取最近的備份檔案以支撐自身業務的繼續執行。如果最近檔案處理失敗或資料異常,則自動載入前一個備份檔案,以此類推,直到重試完本地所有備份檔案後,如果依舊處理失敗,則系統不可用,放棄掙扎。

結合上述背景,可以看出實現應對策略想的還算周到,做到了使用本地備份進行遠端資料請求失敗情況下的兜底處理,還考慮到了資料載入異常的情況增加了自動重試機制,自動往前載入直到嘗試載入到一份可用的歷史備份檔案為止。但是考慮一種場景:假設遠端服務介面正常,返回的資料響應格式也正確,但是由於遠端資料來源的服務開發人員昨天夜裡升級了個版本,導致下發的資料內容本身存在嚴重的錯誤,這導致下游業務使用該資料的時候業務受損。這種情況下,前面規劃的實現中的所有自恢復與自保手段都是失效的。

所以呢,如果在規劃階段,在上述實現的幾點保障措施的基礎上,再額外規劃一條人工指令干預通道,在緊急情況下,可以人工下發指令,強制要求系統斷開與服務端的實時更新邏輯,並強制載入第X份本地備份檔案,便可以快速讓自身服務擺脫遠端資料來源的故障影響,等到其故障修復後,再下發指令恢復對遠端資料來源的實時更新。改造後的示意圖如下所示:

4.2. 非預期場景的人工處置權

人工干預能力,也可以算作是管理端系統的一個“特權功能”,為後臺人員提供更高的操作許可權,解決某些看似不合理、卻極可能出現的業務層問題,比如處理某些難纏的客訴問題。

舉個簡單的例子:

一個證券公司開發了一款炒股APP,並提供了一個投顧付費功能,使用者付費之後就可以使用對應的高階功能。業務規劃的策略是使用者購買之後不允許退訂,並且介面以及使用者協議裡面也明確提示了購買之後不允許退訂。

A使用者購買之後就非要退訂退款,然後不停地纏著客服並威脅不處理就去監管機構、證監會等地方去投訴。

理想情況下,我們預期是使用者按照產品規劃的策略進行購買,並且也已經盡到告知義務,不支援使用者退訂。但面對客戶的胡攪蠻纏,本著維護公司形象的角度出發,為儘快平息爭議,客服部門會私下同意後臺操作為這個使用者退單退款。如果系統設計與實現的時候,沒有規劃構建對應的後臺人工退單退款能力,處理起來就會很被動了————正所謂:可以不用、但不能沒有

5. 監控預警,防患於未然

前面提及的容錯設計以及一些災備方案,其面向的是故障已經發生的情況下,如何去應對故障來保障系統業務的可用性。而更為穩妥的一種預期,是能夠在問題剛暴露一點苗頭的時候,就能被發現並及時化解掉,這裡就需要在系統實現的時候進行一些必要的資料埋點指標採集監測,及時將系統的預警資訊告知具體維護人員,提醒維護人員及早介入處理。

並非“看不到的問題就是沒問題、看不見的故障就是沒故障”,作為系統的負責人員而言,應該是要知曉系統的整體執行狀態以及系統的健康度,透過狀態監控、指標監測等手段,讓線上系統的執行狀態從黑盒變為白盒。

5.1. 監控告警

一般而言,監控平臺都是獨立於業務進行構建,且提供Push或者Pull兩種指標資料獲取機制。在監控內容方面,可以涵蓋資源使用情況、系統狀態、業務執行資料等各維度。

監控告警是開發與運維人員知曉線上系統異常狀態的一個重要手段,實施的時候需要注意不要濫用告警通道。告警訊息的傳送最好支援分組聚合訊息抑制等能力,避免出現無用告警訊息的狂轟濫炸,麻痺接收人員的神經、淹沒真正重要的“求救”訊號。同時,在構建監控告警平臺的時候,考慮儘量獨立於業務,讓告警相關的邏輯從業務中解耦,降低監控對業務邏輯的侵蝕性。

關於如何設計與規劃構建監控告警平臺,有興趣的可以看下我此前的一篇文章《搭建一個通用監控告警平臺,架構上需要有哪些設計》

5.2. 實時Dashboard

既然要防患於未然,首先是要對系統整體的健康狀態有個清晰的認知。這個時候,系統健康監測相關的能力的價值就會顯現出來。這就像是一份系統的實施體檢報告,基於這份體檢報告,可以發現系統中潛在的壓力點位、風險環節、可疑趨勢,然後可以提前介入進行應對,將故障消滅在萌芽階段。

5.3. 災備演練

如前所述,系統在構建的時候規劃了一系列高大上的異常應對與災難恢復手段,但如何確保這些手段在異常出現的時候能達到預期效果呢?這就得透過災備演練來檢驗了。如同和平年代的軍事演習,災備演練也是很多大型專案的定期“軍演”。透過模擬一些可能的災難故障場景,去驗證系統的容錯與異常保障手段的有效性,發現應急方案中存在的問題並及時修復。

對於一些大型系統而言,其整個業務流程的處理會牽扯到上下游以及周邊等眾多依賴,一些災備預案的實施也是上下游聯動觸發的。所以定期災備演練的另一個目的,也是鍛鍊開發運維人員的應急預案實施的默契度。

6. 意識養成,保持對風險的敏銳識別力

如前所述,在實現層面有很多種成熟且可落地的方案可以將系統的異常應對與災難恢復等場景變為現實,但這些其實都是具體的“”,是我們已知有這個風險或者訴求的前提下,為了應對這些已知可能場景而作出的具體應對之法。而身為一名IT從業者,一方面要經歷將業務訴求變為現實的落地過程,同時也是與各種異常情況博弈的歷險之旅。對風險的敏銳洞察力,應該是一個優秀程式設計師刻在骨子裡的品質。這種品質,不僅體現在編碼層面、亦非侷限於架構設計,而是各個方面的,它是一種思維模式、是一種本能的條件反射

保持對風險的敏銳識別力,才能讓自己看見潛在風險,才能讓各種風險防備之術得以落地。

舉個簡單的、非技術實現層面的例子。

線上系統臨時有個問題,需要緊急手動換個包並重啟下程序修復。
  • 頭鐵勇士的梭哈

多簡單的一件事,程序停掉,刪掉舊包,上傳新包,然後程序已啟動,完美解決。

也許,大部分情況,的確也沒出現過問題。但對於有一定資歷的人員而言,看到對線上環境的這一操作,往往會有點“心驚膽戰”的感覺。比如:

萬一上傳的包有問題,啟動失敗了,這個時候線上包也刪了,服務也沒法啟動了,線上服務直接就報廢了

  • 吃過小虧之後的低頭

結合上述操作可能存在的風險點,改良版的做法,自然就是舊包不刪、改為重新命名備份起來,這樣新包萬一有問題,可以直接用舊包回滾恢復線上服務即可。

這種改良之後的操作方法,從可靠性層面而言,的確有很大改進,給自己留了充足的回滾與回退的餘地。但是仔細審視一下,依舊有改進的餘地:

  1. 上傳新包的操作,由於要走網路傳輸、受網路波動的影響較大,存在失敗風險。
  2. 如果包的體積很大、或者上傳的時候要走vpn、堡壘機等層層關卡,可能速度會比較慢,整個傳輸過程的耗時會很長。這種情況下,可能會導致線上程序停機時間太久。
  • 受盡毒打而倖存後的謹慎

進一步的最佳化上述操作的步驟,可以將整個動作分為前置準備環節和線上操作環節兩部分,將一些比較耗時且風險比較大的操作,放在前置準備環節中預先完成。

這樣一來,在正式線上操作的環節中,僅需要執行一些確定性較高的動作,這樣既可以保證執行動作的快速結束,也可以降低執行動作的不確定性。透過風險前置操作,降低了整個操作過程中出現問題的機率。

說到這裡,也許有的小朋友會反駁,覺得公司頻寬很高、傳輸檔案很快,不需要這麼麻煩,直接梭哈幹就行了。這其實就是一個意識層面的共識問題,也是一種對風險的應對策略問題。其實還是之前的那句話,能意識到的風險並非真正的風險,往往是那些看似不可能的風險才是真正的風險。所有行為的出發點其實就一條:這個動作操作失敗的後果,是不是你能夠承擔的。如果可以,那可以直接梭哈,否則的話,就要三思。

7. 再論本心:扁鵲三兄弟的故事

最後,講個故事吧。

傳說扁鵲周遊到魏國的時候,魏文王接待他並問他:你家兄弟三人都是學醫的,那麼你們三個人中誰的醫術最高呢?扁鵲回答說:“我大哥醫術最高,二哥次之,我醫術最差”。魏文王很困惑:“為何世人皆尊你為神醫、卻不曾聽聞你大哥二哥?”,扁鵲解釋道:

  • 我大哥的醫術最好,是因為他能夠在你沒有發病之前就能看出你是否有病。那個時候,病人是不會覺得自己患病了的,我大哥就在病人發現之前就將病給治好了。這是因為這個緣故,大哥的醫術一直不被他人認可,也沒有什麼名氣。

  • 二哥是家中醫術第二好的,因為他能夠在病人發病初期就看出來,然後將病人給治好,這樣一來,病人們都認為我二哥只擅長治療一些小病症。

  • 病人找我治病時,已經到了中晚期,病情已經十分的嚴重了。我將那些患了重病的病人給醫治好後,我就更加出名了。但從根本上來講,我的醫術比不上我的兩位哥哥。

放到當前日益內卷的IT行業,扁鵲大哥、扁鵲二哥這種人,也許是屬於技術高超的一類人,他們默默守護自己的程式碼、不給異常爆發的機會。於是呢?始終穩定的線上服務,讓人慢慢淡忘了相關開發人員的存在,使其反而成為被邊緣化的透明人。真正可以有機會嶄露頭角、博得領導青睞的,往往都是團隊裡面的救火隊員般存在的人,這些人,不停的在前線衝鋒陷陣,去解決線上那些按起葫蘆起了瓢的問題,久而久之便成為領導心中信賴的柱石,相關的機會與資源也向其傾斜。

技術之外的事情,雖然發人深省,卻也似乎無解。正所謂聖人治未病,不治已亂、治未亂,反觀我們自身,如何抉擇,主動權在個人、遵從本心最重要。但是相信的是,時間會證明一切,一切的堅守與技術上的追求,最終一定會被看見(有點雞湯的味道)。所以呢,技術上有點追求,永遠是個正解。

8. 小結

好啦,關於軟體開發與設計過程中的異常應對與災備能力的探討,暫且告一段落。這裡所提到的容錯應對災難應對能力,其重要性猶如生活購買的保險——平日裡或許顯得默默無聞,甚至讓人有些“成本浪費”的錯覺,但在關鍵時刻,它們卻能成為抵禦風險的堅固防線,其價值無可估量。

正如小米SU7配備的備用電池,這一設計在緊急情況下為使用者多了一層求生保障。在軟體開發與設計的世界裡,是否需要構建類似的備用方案災備系統,取決於業務對潛在損失的容忍度。若災難性後果是業務所無法承受的,那麼增加一些額外的成本,構建一套完善的災備兜底、異常保障以及監控告警機制,就顯得尤為重要了。

亦如古語云:未雨綢繆,有備無患

我是vzn呀,聊技術、又不僅僅聊技術~

如果覺得有用,請點個關注,也可以關注下我的公眾號【是vzn呀】,獲取更及時的更新。

期待與你一起探討,一起成長為更好的自己。

相關文章