淺談高可用設計

ErnestEvan發表於2022-02-07

大家好,我是坤哥

今天我們來聊一下網際網路三高(高併發、高效能、高可用)中的高可用,看完本文相信能解開你關於高可用設計的大部分困惑

前言

高可用(High availability,即 HA)的主要目的是為了保障「業務的連續性」,即在使用者眼裡,業務永遠是正常(或者說基本正常)對外提供服務的。高可用主要是針對架構而言,那麼要做好高可用,就要首先設計好架構,第一步我們一般會採用分層的思想將一個龐大的 IT 系統拆分成為應用層,中介軟體,資料儲存層等獨立的層,每一層再拆分成為更細粒度的元件,第二步就是讓每個元件對外提供服務,畢竟每個元件都不是孤立存在的,都需要互相協作,對外提供服務才有意義。

要保證架構的高可用,就要保證架構中所有元件以及其對外暴露服務都要做高可用設計,任何一個元件或其服務沒做高可用,都意味著系統存在風險。

那麼這麼多元件該怎麼做高可用設計呢,其實任何元件要做高可用,都離不開「冗餘」和「自動故障轉移」,眾所周知單點是高可用的大敵,所以元件一般是以叢集(至少兩臺機器)的形式存在的,這樣只要某臺機器出現問題,叢集中的其他機器就可以隨時頂替,這就是「冗餘」。簡單計算一下,假設一臺機器的可用性為 90%,則兩臺機器組成的叢集可用性為 1-0.1*0.1 = 99%,所以顯然冗餘的機器越多,可用性越高。

但光有冗餘還不夠,如果機器出現問題,需要人工切換的話也是費時費力,而且容易出錯,所以我們還需要藉助第三方工具(即仲裁者)的力量來實現「自動」的故障轉移,以達到實現近實時的故障轉移的目的,近實時的故障轉移才是高可用的主要意義

怎樣的系統可以稱之為高可用呢,業界一般用幾個九來衡量系統的可用性,如下

可用性級別 系統可用性% 當機時間/年 當機時間/月 當機時間/周 當機時間/天
不可用 90% 36.5 天 73 小時 16.8 小時 144 分鐘
基本可用 99% 87.6 小時 7.3 小時 1.68 小時 14.4 分鐘
較高可用 99.9% 8.76 小時 43.8 分鐘 10.1 分鐘 1.44 分鐘
高可用 99.99% 52.56 分鐘 4.38 分鐘 1.01 秒 8.64 秒
極高可用 99.999% 5.26 分鐘 26.28 秒 6.06 秒 0.86 秒

一般實現兩個 9 很簡單,畢竟每天當機 14 分鐘已經嚴重影響業務了,這樣的公司遲早歇菜,大廠一般要求 4 個 9,其他要求嚴苛的業務要達到五個九以上,比如如果因為一個電腦的故障導致所有列車停駛,那麼就會有數以萬計的人正常生活受到阻礙,這種情況就要求五個九以上

接下來我們就來一起看看架構中的各個元件如何藉助「冗餘」和「自動故障轉移」來實現高可用。

網際網路架構剖析

目前多數網際網路都會採用微服務架構,常見架構如下:

可以看到架構主要分以下幾層

  1. 接入層:主要由 F5 硬體或 LVS 軟體來承載所有的流量入口
  2. 反向代理層:Nginx,主要負責根據 url 來分發流量,限流等
  3. 閘道器:主要負責流控,風控,協議轉換等
  4. 站點層:主要負責呼叫會員,促銷等基本服務來裝配 json 等資料並返回給客戶端
  5. 基礎 service:其實與站點層都屬於微服務,是平級關係,只不過基礎 service 屬於基礎設施,能被上層的各個業務層 server 呼叫而已
  6. 儲存層:也就是 DB,如 MySQL,Oracle 等,一般由基礎 service 呼叫返回給站點層
  7. 中介軟體:ZK,ES,Redis,MQ 等,主要起到加速訪問資料等功能,在下文中我們會簡單介紹下各個元件的作用

如前所述,要實現整體架構的高可用,必須要實現每一層元件的高可用,接下來我們就來分別看一下每一層的元件都是如何實現高可用的

接入層&反向代理層

這兩層的高可用都和 keepalived 有關,所以我們結合起來一起看

對外,兩個 LVS 以主備的形式對外提供服務,注意只有 master 在工作(即此時的 VIP 在 master 上生效),另外一個 backup 在 master 當機之後會接管 master 的工作,那麼 backup 怎麼知道 master 是否正常呢,答案是通過 keepalived,在主備機器上都裝上 keepalived 軟體,啟動後就會通過心跳檢測彼此的健康狀況,一旦檢測到 master 當機,keepalived 會檢測到,從而 backup 自動轉成 master,對外提供服務,此時 VIP 地址(即圖中的 115.204.94.139)即在 backup 上生效,也就是我們常說的「IP漂移」,通過這樣的方式即解決了 LVS 的高可用。

keepalived 的心跳檢測主要通過傳送 ICMP 報文,或者利用 TCP 的埠連線和掃描檢測來檢測的,同樣的,它也可以用來檢測 Nginx 暴露的埠,如果某些 Nginx 不正常 keepalived 也能檢測到並將其從 LVS 能轉發的服務列表中剔出。

借用 keepalived 這個第三方工具,同時實現了 LVS 和 Nginx 的高可用,同時在出現故障時也可以將當機情況傳送到對應開發人員的郵箱以讓他們及時收到通知處理,確實很方便,keepalived 應用廣泛,下文我們也能看到它也可以用在 MySQL 上來實現 MySQL 的高可用。

微服務

接下來我們再來看一下「閘道器」,「站點層」,「基礎服務層」,這三者一般就是我們所說的微服務架構元件,當然這些微服務元件還需要通過一些 RPC 框架如 Dubbo 來支撐,所以微服務要實現高可用,就意味著 dubbo 這些 RPC 框架也要提供支撐微服務高可用的能力,我們就以 dubbo 為例來看下它是如何實現高可用的

我們先來簡單地看下 dubbo 的基本架構

思路也很簡單,首先是 Provider(服務提供者)向 Registry(註冊中心,如 ZK 或 Nacos 等)註冊服務,然後 Consumer(服務消費者)向註冊中心訂閱和拉取 Provider 服務列表,獲取服務列表後,Consumer 就可以根據其負載均衡策略選擇其中一個 Provider 來向其發出請求,當其中某個 Provider 不可用(下線或者因為 GC 阻塞等)時,會被註冊中心及時監聽(通過心跳機制)到,也會及時推送給 Consumer,這樣 Consumer 就能將其從可用的 Provider 列表中剔除,也就實現了故障的自動轉移,不難看出,註冊中心就起到了類似 keepalived 的作用

中介軟體

我們再來看下這些中介軟體如 ZK,Redis 等是如何實現高可用的呢

ZK

上一節微服務中我們提到了註冊中心,那我們就以 ZK(ZooKeeper)為例來看看它的高可用是如何實現的,先來看下它的整體架構圖如下

Zookeeper 中的主要角色如下

  • Leader: 即領導者,在叢集中只有一個 Leader,主要承擔了以下的功能
    1. 事務請求的唯一排程和處理者,保證叢集事物處理的順序性,所有 Follower 的寫請求都會轉給 Leader 執行,用來保證事務的一致性
    2. 叢集內部各伺服器的排程者:處理好事務請求後,會將資料廣播同步到各個 Follower,統計 Follower 寫入成功的數量,超過半數 Follower 寫入成功,Leader 就會認為寫請求提交成功,通知所有的 Follower commit 這個這與操作,保證事後哪怕是叢集崩潰恢復或者重啟,這個寫操作也不會丟失。
  • Follower:
    1. 處理客戶端非事務請求、轉發事務請求給 leader 伺服器
    2. 參與事物請求 Proposal 的投票(需要半數以上伺服器通過才能通知 leader commit 資料; Leader 發起的提案,要求 Follower 投票)
    3. 參與 Leader 選舉的投票

畫外音:Zookeeper 3.0 之後新增了一種 Observer 的角色,不過與此處討論的 ZK 高可用關係不是很大,為了簡化問題,所以省略

可以看到由於只有一個 Leader,很顯然,此 Leader 存在單點隱患,那麼 ZK 是怎麼解決此問題的呢,首先 Follower 與 Leader 會用心跳機制保持連線,如果 Leader 出現問題了(當機或者因為 FullGC 等原因無法響應),Follower 就無法感知到 Leader 的心跳,就會認為 Leader 出問題了,於是它們就會發起投票選舉,最終在多個 Follower 中選出一個 Leader 來(這裡主要用到了 Zookeeper Atomic Broadcast,即 ZAB 協議,它是為 ZK 專門設計的一種支援崩潰恢復的一致性協議),選舉的細節不是本文重點,就不在此詳述了。

除了 ZAB 協議,業界上常用的還有 Paxos,Raft 等協議演算法,也可以用在 Leader 選舉上,也就是是在分散式架構中,這些協議演算法承擔了“第三者”也就是仲裁者的作用,以承擔故障的自動轉移

Redis

Redis 的高可用需要根據它的部署模式來看看,主要分為「主從模式」和「Cluster 分片模式」兩種

主從模式

先來看一下主從模式,架構如下

主從模式

主從模式即一主多從(一個或者多個從節點),其中主節點主要負責讀和寫,然後會將資料同步到多個從節點上,Client 也可以對多個從節點發起讀請求,這樣可以減輕主節點的壓力,但和 ZK 一樣,由於只有一個主節點,存在單點隱患,所以必須引入第三方仲裁者的機制來判定主節點是否當機以及在判定主節點當機後快速選出某個從節點來充當主節點的角色,這個第三方仲裁者在 Redis 中我們一般稱其為「哨兵」(sentinel),當然哨兵程式本身也有可能掛掉,所以為了安全起見,需要部署多個哨兵(即哨兵叢集)

哨兵叢集

這些哨兵通過 gossip(流言) 協議來接收關於主伺服器是否下線的資訊,並在判定主節點當機後使用 Raft 協議來選舉出新的主節點

Cluster 分片叢集

主從模式看似完美,但存在以下幾個問題

  1. 主節點寫的壓力難以降低:因為只有一個主節點能接收寫請求,如果在高併發的情況下,寫請求如果很高的話可能會把主節點的網路卡打滿,造成主節點對外無法服務
  2. 主節點的儲存能力受到單機儲存容量的限制:因為不管是主節點還是從節點,儲存的都是全量快取資料,那麼隨著業務量的增長,快取資料很可能直線上升,直到達到儲存瓶頸
  3. 同步風暴:因為資料都是從 master 同步到 slave 的,如果有多個從節點的話,master 節點的壓力會很大

為了解決主從模式的以上問題,分片叢集應運而生,所謂分片叢集即將資料分片,每一個分片資料由相應的主節點負責讀寫,這樣的話就有多個主節點來分擔寫的壓力,並且每個節點只儲存部分資料,也就解決了單機儲存瓶頸的問題,但需要注意的是每個主節點都存在單點問題,所以需要針對每個主節點做高可用,整體架構如下

原理也很簡單,在 Proxy 收到 client 執行的 redis 的讀寫命令後,首先會對 key 進行計算得出一個值,如果這個值落在相應 master 負責的數值範圍(一般將每個數字稱為槽,Redis 一共有 16384 個槽)之內,那就把這條 redis 命令發給對應的 master 去執行,可以看到每個 master 節點只負責處理一部分的 redis 資料,同時為了避免每個 master 的單點問題,也為其配備了多個從節點以組成叢集,當主節點當機時,叢集會通過 Raft 演算法來從從節點中選舉出一個主節點

ES

再來看一下 ES 是如何實現高可用的,在 ES 中,資料是以分片(Shard)的形式存在的,如下圖所示,一個節點中索引資料共分為三個分片儲存

但只有一個節點的話,顯然存在和 Redis 的主從架構一樣的單點問題,這個節點掛了,ES 也就掛了,所以顯然需要建立多個節點

一旦建立了多個節點,分片的優勢就體現出來了,可以將分片資料分散式儲存到其它節點上,極大提升了資料的水平擴充套件能力,同時每個節點都能承擔讀寫請求,採用負載均衡的形式避免了單點的讀寫壓力

ES 的寫機制與 Redis 和 MySQL 的主從架構有些差別(後兩者的寫都是直接向 master 節點發起寫請求,而 ES 則不是),所以這裡稍微解釋一下 ES 的工作原理

首先說下節點的工作機制,節點(Node)分為主節點(Master Node)和從結點(Slave Node),主節點的主要職責是負責叢集層面的相關操作,管理叢集變更,如建立或刪除索引,跟蹤哪些節點是叢集的一部分,並決定哪些分片分配給相關的節點,主節點也只有一個,一般通過類 Bully 演算法來選舉出來,如果主節點不可用了,則其他從節點也可以通過此演算法來選舉以實現叢集的高可用,任何節點都可以接收讀寫請求以達到負載均衡的目的

再說一下分片的工作原理,分片分為主分片(Primary Shard,即圖中 P0,P1,P2)和副本分片(Replica Shard,即圖中 R0,R1,R2),主分片負責資料的寫操作,所以雖然任何節點可以接收讀寫請求,但如果此節點接收的是寫請求並且沒有寫資料所在的主分片話,此節點會將寫請求排程到主分片所在的節點上,寫入主分片後,主分片再把資料複製到其他節點的副本分片上,以有兩個副本的叢集為例,寫操作如下

MQ

ES 中利用資料分片來提升高可用和水平擴充套件能力的思想也應用在其他元件的架構設計上,我們以 MQ 中的 kafka 為例再來看下資料分片的應用

Kafka 高可用設計,圖片來自《武哥漫談IT》

如上是 Kafka 叢集,可以看到每個 Topic 的 Partition 都分散式儲存在其它訊息伺服器上,這樣一旦某個 Partition 不可用,可以從其他 follower 選舉出 leader 繼續服務,不過與 ES 中的資料分片不同的是,follower Partition 屬於冷備,也就是說在正常情況下不會對外服務,只有在 leader 掛掉之後從多個 follower 中選出leader 後才能對外提供服務

儲存層

接下來我們再來看一下最後一層,儲存層(DB),這裡我們以 MySQL 為例來簡單地討論一下其高可用設計,其實大家如果看完了以上的高可用設計,會發現 MySQL 的高可用也不過如此,思想都是類似的,與 Redis 類似,它也分主從和分片(即我們常說的分庫分表)兩種架構

主從的話與 LVS 類似,一般使用 keepalived 的形式來實現高可用,如下所示

如果 master 當機了 ,Keepalived 也會及時發現,於是從庫會升級主庫,並且 VIP 也會“漂移”到原從庫上生效,所以說大家在工程配置的 MySQL 地址一般是 VIP 以保證高可用

資料量大了之後就要分庫分表了,於是就有了多主,就像 Redis 的分片叢集一樣,需要針對每個主配備多個從,如下

之前有讀者問分庫分表之後為啥還要做主從,現在我想大家應該都明白了,不是為了解決讀寫效能問題,主要是為了實現高可用

總結

看完了架構層面的高可用設計,相信大家對高可用的核心思想「冗餘」和「自動故障轉移」會有更深刻的體會,觀察以上架構中的元件你會發現冗餘的主要原因是因為只有一主,為什麼不能有多主呢,也不是不可以,但這樣在分散式系統下要保證資料的一致性是非常困難的,尤其是節點多了的話,資料之間的同步更是一大難題,所以多陣列件採用一主的形式,然後再在主和多從之間同步,多陣列件之所以選擇一主本質上是技術上的 tradeoff

那麼做好每個元件的高可用之後是否整個架構就真的可用了呢,非也,這隻能說邁出了第一步,在生產上還有很多突發情況會讓我們的系統面臨挑戰,比如

  1. 瞬時流量問題:比如我們可能會面臨秒殺帶來的瞬時流量激增導致系統的承載能力被壓垮,這種情況可能影響日常交易等核心鏈路,所以需要做到系統之間的隔離,如單獨為秒殺部署一套獨立的叢集
  2. 安全問題:比如 DDOS 攻擊,爬蟲頻繁請求甚至刪庫跑路等導致系統拒絕服務
  3. 程式碼問題:比如程式碼 bug 引起記憶體洩露導致 FullGC 導致系統無法響應等
  4. 部署問題:在釋出過程中如果貿然中止當前正在執行的服務也是不行的,需要做到優雅停機,平滑釋出
  5. 第三方問題:比如我們之前的服務依賴第三方系統,第三方可能出問題導致影響我們的核心業務
  6. 不可抗力:如機房斷電,所以需要做好容災,異地多活,之前我司業務就由於機房故障導致服務四小時不可用,損失慘重

所以除了做好架構的高可用之外,我們還需要在做好系統隔離,限流,熔斷,風控,降級,對關鍵操作限制操作人許可權等措施以保證系統的可用。

這裡特別提一下降級,這是為了保證系統可用性採取的常用的措施,簡單舉幾個例子

  1. 我們之前對接過一個第三方資金方由於自身原因借款功能出了問題導致無法借款,這種情況為了避免引起使用者恐慌,於是我們在使用者申請第三方借款的時候返回了一個類似「為了提升你的額度,資金方正在系統升級」這樣的文案,避免了客訴
  2. 在流媒體領域,當使用者觀看直播出現嚴重卡頓時,很多企業的第一選擇不是查 log 排查問題,而是為使用者自動降位元速率。因為比起畫質降低,卡的看不了顯然會讓使用者更痛苦
  3. 雙十一零點高峰期,我們把使用者的註冊登入等非核心功能給停掉了,以保證下單等核心流程的順利

另外我們最好能做到事前防禦,在系統出問題前把它扼殺在搖籃裡,所以我們需要做單元測試,做全鏈路壓測等來發現問題,還需要針對 CPU,執行緒數等做好監控,當其達到我們設定的域值時就觸發告警以讓我們及時發現修復問題(我司之前就碰到過一個類似的生產事故覆盤大家可以看一下),此外在做好單元測試的前提下,依然有可能因為程式碼的潛在 bug 引起線上問題,所以我們需要在關鍵時間(比如雙十一期間)封網(也就是不讓釋出程式碼)

此外我們還需要在出事後能快速定位問題,快速回滾,這就需要記錄每一次的釋出時間,釋出人等,這裡的釋出不僅包括工程的釋出,還包括配置中心等的釋出

畫外音:上圖是我司的釋出記錄,可以看到有程式碼變更,回滾等,這樣如果發現有問題的話可以一鍵回滾

最後我們以一張圖來總結一下高可用的常見手段

參考

最後,歡迎大家關注我的公眾號「碼海」,共同進步

相關文章