在一個成熟的分散式系統中 如何下手做高可用?

weixin_34319999發表於2018-11-23

對於企業來說,隨著規模越來越大,整個系統中存在越來越多的子系統,每個子系統又被多個其他子系統依賴或者依賴於其他子系統。大部分系統在走到這一步的過程中,大概率會發生這樣的場景:作為某個子系統的負責人或者OnCall人員,休息的時候都不安穩,心裡老是忐忑著系統會不會掛。導致週末不敢長時間出門,晚上睡夢中被電話叫醒,痛苦不堪。

那麼,在一個成熟的分散式系統中,我們該如何去保證它的可用性呢?迫切的需要解放我們緊繃的神經。下面,我們就來看下做高可用的思路和關鍵部分。

如何下手做高可用?

在這個時候,我們的系統全貌大致是這樣的。

\"\"

由大大小小的多個部分組合而成的一個完整系統,可以看到包含閘道器、Web層、服務層、中介軟體、基礎設施,這每一層之間又是層層依賴。在如此的一個龐然大物面前做高可用是一個系統化的工程,除了良好的頂層設計規劃外,還需要深入到細節。由於雪崩效應的存在,軟體系統是一個完美體現“千里之堤毀於蟻穴”的地方,一個小問題導致整個系統全盤崩塌的案例也不在少數。

所以,首先我們需要擁有保持懷疑的心態。這個懷疑是指對系統的懷疑,而不是對人的懷疑。人非聖賢孰能無過,況且寫程式碼是一個精細活,還不是流水線式的那種。而且,哪怕不是寫程式碼的疏忽,其他諸如網路、作業系統等異常,甚至一些惡意的攻擊都會導致故障隨時發生。

那麼我們具體應該怎麼做呢?既然故障導致了可用性降低,那麼接下來的工作必然是圍繞解決故障展開。分為3個步驟:故障發現、故障消除、故障善後。

故障發現

所謂“故障發現”,就是通過技術手段實時採集系統中每個節點的健康狀態,以及每2個節點之間鏈路的健康狀態,包括但不限於呼叫成功率、響應時間等等。藉此代替我們的眼睛去盯著整個系統,一旦低於某個設定的閾值,就觸發報警給我們一個提醒。因為當你的系統中存在成百上千的程式時,靠肉眼去找到發生故障的位置,簡直是天方夜譚。哪怕找到了,也可能已經產生了巨大的損失。

負責故障發現的解決方案都屬於應用效能管理(APM)範疇。我們在部署這個“眼睛”的時候,需要考慮到全方位的覆蓋,要包含所有的節點。比如:

  • 在Web方面可以直接利用瀏覽器提供的導航計時(NavigationTiming)和資源計時(ResourceTiming)介面來採集效能資料,非常方便。
  • 在iOS、Android這種App方面通過原始碼插樁的方式進行。比如直接引入採集SDK然後硬編碼在原始碼中,或者通過AOP框架來進行動態程式碼注入。程式碼的注入位置就在每個方法的執行前和執行後(如下圖所示)。

\"\"

  • 後端是分散式系統的主戰場,有程式外和程式內兩個維度的解決方案。

1)程式外的解決方案,例如運用Zabbix之類的無探針解決方案,呼叫系統或者服務自身提供的狀態介面獲取採集資料(如下圖所示),以及對網路資料包的監聽來獲取網路效能方面的資料。

\"\"

由於是程式外的,所以這類方案對我們的程式是無侵入的,最友好。但弊端也很明顯,監控的粒度太粗,只能進行一些外在的監控。比如可以發現CPU突然飆高了,但是並不知道可疑的介面是哪個,更無法知道是哪行程式碼導致的問題。因此,只適合作為輔助方案。

2)後端的程式內解決方案可以解決程式外方案的短板,但是由於需要侵入到應用程式內部,所以對效能和穩定性會帶來一定的影響。關於這類方案我們有很多的選擇可以來實現它:可以同APP一樣運用採集SDK和AOP框架,還可以通過利用整個系統中的“連線”部分來進行,比如一些中介軟體(資料層訪問框架、服務呼叫框架等)。

\"\"

做好了監控,就做好了故障發現一半的工作。另外一半是什麼呢?就是故障注入測試(Fault Insertion Test)。我們需要通過技術手段來主動製造“故障”,以此來提前檢驗系統在各種故障場景下的表現情況是否符合我們預期。

監控是一雙眼睛,替你盯著故障,但是我們不能守株待兔,否則大部分突發的故障都會在生產環境發生。一旦發生就會對經營的業務產生或多或少的影響,甚至看似平靜的系統下,藏著幾個隨時會引爆的炸彈,我們也不得而知。所以我們需要主動出擊,主動去製造“故障”來鍛鍊系統。

在實際運用中,故障可以被注入到軟體,也可以被注入到硬體。注入到軟體的方式,無外乎這兩種:

  1. 架設在軟體與作業系統之間,當軟體中的資料經過作業系統時,通過篡改資料完成注入。
  2. 通過AOP之類的框架進行程式碼注入來製造故障。

如果注入到硬體中就簡單很多,直接執行一段程式碼把CPU、網路卡等吃滿即可。

故障注入測試的過程大致是這樣,在故障模型庫中選擇一個模型,然後將該模型對應的故障注入到一個在獨立的環境中執行並且被包裹了一層“炸藥包”的系統,相當於在你指定的地方去“點火”,隨後進行監測並分析結果(如下圖所示)。

\"\"

故障消除

現在已經能夠很容易的發現故障了,我們就可以通過綜合運用隔離性、橫向擴充套件、代理、負載均衡、熔斷、限流、降級等等機制來快速的“掐滅故障”。

分散式系統的規模越大,耦合越嚴重,各個子系統之間通過網路連線在一起,就如赤壁之戰中的曹軍連在一起的船舶一樣,只要其中一個著火了就會就近蔓延。所以,一旦發現某個子系統掛了,就需要儘快切斷與它的聯絡,保證自己能夠不受連累,防止雪崩的發生。

我們可以首先運用docker之類的技術將每個應用在執行時的環境層面隔離開來。然後,通過橫向擴充套件讓每個應用允許被“Copy”,以此來部署多個副本。接著,結合代理和負載均衡讓這些副本可以共同對外提供服務,使得每個應用程式本身先具備“高可用”。最後的三大防禦措施,熔斷、限流、降級來快速“掐滅故障”,避免故障在不同的應用程式間擴散。

故障善後

“故障消除”避免了級聯故障導致的系統性風險,這時整個分散式系統已經具備健壯性了。但是對正在使用系統的使用者來說,這些故障還是可見的,因為會反映成他實際操作中的錯誤提示,甚至導致流程無法繼續。這對我們“衣食父母”來說並不友好,最終可能會導致使用者的流失。

所以,我們應該通過一些補償和緩衝的方式將故障產生的影響降到最低,儘可能的去包容故障,讓使用者無感。並且,這些善後工作應該與“故障發現”、“故障消除”一起形成一個完整的體系,以及儘可能的自動化。

前面我們聊到,故障產生的原因要麼是呼叫的節點處於異常狀態,要麼是通訊鏈路異常。所以,要做好“故障善後”,就需要在節點之間的連線上做文章。根據CAP定理、BASE理論,我們已經很清楚兩個程式之間的呼叫方式。一是直接點對點的同步呼叫,或者是通過一些技術中間層進行非同步的呼叫。

那麼,針對同步呼叫我們可以有兩種方式去實施。

  • 首先是立即重試。很多時候,相同節點的所有副本可能只是由於網路原因,導致其中的某個節點無法被訪問。那麼,此時如果後端的負載均衡策略只要不是Hash類的策略,並且後端服務的方法是無狀態的且支援冪等性的,就可以立馬重試一次,大概率就能呼叫成功。不過,這個方案潛在的一個副作用是,如果後端服務總體負載很高,且無法自動彈性擴容,那麼會進一步加劇一些壓力。所以,你可以增加一個允許被重試的條件,以及為實際的重試操作增加一個約定。比如,這兩個耗時分別都不能大於1秒。
  • 方式二,將可以容忍最終一致性的同步呼叫產生的出錯訊息進行非同步重發。比如,電商網站中提交訂單中所依賴的訂單模組產生故障,我們可以將其暫存到訊息佇列中,然後再進行非同步的投遞,同時提示給使用者“訂單正在加緊建立中,稍後通知您支付”之類的語句,至少先讓訂單能夠下進來。這本質上算得是一個“降級”方案。

如果本身就是一個非同步呼叫,比如最常見的就是發往訊息佇列出現異常。因為,一個高可用的訊息佇列叢集,大多數情況下導致訊息無法被投遞的原因是網路問題。這個時候,理論上我們可以基於每個應用的本地磁碟部署一個本地MQ,可以避免很大一部分這個問題。但是實際往往不會這麼做,因為這麼做的價效比太低,原因有兩點:

  • 這麼多訊息佇列維護成本太高。
  • 如果用到的是訊息佇列叢集,本身已具備軟體層面的高可用,所以出現這個問題的概率很低。

所以,這個時候我們大多會通過定時的任務(job)去進行對賬(資料一致性檢測)。任務(job)的具體實現上儘可能做到自動修正,否則通知人工介入。

總結

這次,我們系統化的梳理了如何來應對“故障”,以此來達到做好高可用的目的。核心觀點就是:保持著懷疑的心態,去發現故障、消除故障、並且為故障做善後

至此,我們有必要開始衡量我們的高可用到底做的怎麼樣了。就是統計一下全年的故障時間,得出所謂的“幾個9”的結論。

\"\"

但是,到目前為止,我們在通往幾個9的道路上只走了一半。剩下的一半就是討論如何做到無限接近於100%的高可用。

你在工作中,還通過哪些方式為高可用作出過努力呢?主流和非主流都可以說下,一起開開腦洞。 歡迎在下方評論區留言。


延伸閱讀:分散式系統系列文章

第一篇:《撥雲見日看什麼是分散式系統?》
第二篇:《詳解分散式系統本質:“分治”和“冗餘”》
第三篇:《別忽視分散式系統這六大“暗流”》
第四篇:《跨程式通訊,到底用長連線還是短連線》
第五篇:《資料庫如何確保其操作被100%正確執行?》

相關文章