負載均衡淺析

語言: CN / TW / HK

前言

負載均衡這個概念在我們工作中經常被提及到,因為縱觀我們系統的整個鏈路層,每層都會用到負載均衡,從接入層,服務層,到最後的資料層,當然還有MQ,分散式快取等等都會存在一些負載均衡的思路在裡面;給負載均衡做一個簡短的定義:就是將請求分攤到多個操作單元上進行執行;其實就是一種分而治之的思想,面對高併發的情況下,這是一種非常行之有效的方法。

核心功能

上面簡短的定義中我們大致可以看到兩個內容:將請求分發,操作單元;其實就是控制器+執行器模式、Master+Worker模式等等,是不是很熟悉;當然一個成熟的負載均衡器不光有這兩個核心功能,還有一些其他的功能,下面看看都有哪些核心功能:

  • 操作單元配置 這裡的操作單元其實就是上游的伺服器,是真正來處理業務的執行者,這個需要可配置的(最好能支援動態配置),方便使用者新增和刪除操作單元;這些操作單元就是負載均衡器分發訊息的物件;

  • 負載均衡演算法 既然需要分發,那具體通過何種方式把訊息分給配置的執行器,這就需要有相關的分發演算法了,比如我們常見的輪詢、隨機、一致性雜湊等等;

  • 失敗重試 既然配置了多個執行單元,所以某臺伺服器宕機是大概率事件,這樣我們在分發請求給某臺已經宕機的伺服器時,需要有失敗重試功能,將請求重新分發給正常的執行器;

  • 健康檢查 上面的失敗重試是隻有真正轉發的時候才知道伺服器宕機了,是一種惰性策略,健康檢查就是提前將宕機的機器排除掉,比如常見的通過心跳的方式去檢查執行器是否還存活;

有了以上幾個核心的功能,一個負載均衡器大致就形成了,可以把這幾個原則用在很多地方,形成不同的中介軟體或者說內嵌在各種中介軟體中,比如接入層的LVS,F5,Nginx等,服務層各種RPC框架,訊息佇列RocketMQ、Kafka,分散式快取Redis、memcached,資料庫中介軟體shardingsphere、mycat等等,這種分而治之的思路在各種中介軟體中廣泛使用,下面對一些常見的中介軟體是如何做負載均衡的進行分析,大體上可以分為有狀態和無狀態兩種型別;

無狀態

執行單元本身沒有狀態,其實是更加容易去做負載均衡,每個執行單元都是一樣的,常見的無狀態的中介軟體有Nginx,RPC框架,分散式排程等;

接入層

Nginx可以說是我們最常見的接入層中介軟體了,提供四層到七層的負載均衡功能,提供了高效能的轉發,對以上的幾個核心功能提供了支援;

  • 操作單元配置 Nginx提供了簡單的靜態的操作單元配置,如下:
upstream tomcatTest {
     server 127.0.0.1:8081;   #tomcat-8081
     server 127.0.0.1:8082;   #tomcat-8082
}
location / {
     proxy_pass http://tomcatTest;
}

以上配置是靜態的,如果需要新增或者刪除,需要對Nginx重啟,很不方便,當然也提供了動態的單元配置,需要藉助第三方的服務註冊中心比如Consul,etcd等;原理大致如下:

操作單元啟動就會註冊到Consul中,同樣宕機會從Consul中移除;Nginx側會啟動一個Consul-template監聽程式,監聽Consul上操作單元的變更,然後更新Nginx的upstream,最好重載入upstream;

  • 負載均衡演算法 常見的比如:ip_hash,round-robin,hash;配置也很簡單:
upstream tomcatTest {
     ip_hash  //根據ip負載均衡,也就是常說的ip繫結
     server 127.0.0.1:8081;   #tomcat-8081
     server 127.0.0.1:8082;   #tomcat-8082
}
  • 失敗重試
upstream tomcatTest {
     server 127.0.0.1:8081 max_fails=2 fail_timeout=20s;
}
location / {
     proxy_pass http://tomcatTest;
     proxy_next_upstream error timeout http_500;
}

當在fail_timeout內出現了max_fails次失敗,表示此執行單元不可用;通過proxy_next_upstream配置,當出現配置的錯誤時,會重試下一臺執行單元;

  • 健康檢查 Nginx通過整合nginx_upstream_check_module模組來進行健康檢查;支援TCP心跳和Http心跳檢測;
upstream tomcatTest {
     server 127.0.0.1:8081;
     check interval=3000 rise=2 fall=5 timeout=5000 type=tcp;
}

interval:檢測間隔時間; rise:檢測成功多少次後,操作單元標識為可用; fall:檢測失敗多少次後,操作單元標識為不可用; timeout:檢測請求超時時間; type:檢測型別包括tcp,http;

服務層

服務層主要的就是微服務框架比如Dubbo,Spring Cloud等,內部都集成了負載均衡策略,使用起來也是非常方便;

  • 操作單元配置 RPC框架一般都依賴註冊中心元件,其實和Nginx通過註冊中心來動態改變操作單元是一樣的,RPC框架預設就已經依賴註冊中心了,服務啟動就註冊到中心,服務不可用就移除,並且會自動同步到消費端,使用者完全無感知,消費端要做的就是根據註冊中心提供的服務列表,然後使用分發演算法進行負載均衡;

  • 負載均衡演算法 Spring Cloud提供了Ribbon元件來實現負載均衡,而Dubbo直接內建均衡策略,常見的演算法包括:輪詢,隨機,最少活躍呼叫數,一致性 Hash等等;比如dubbo配置輪詢演算法:

<dubbo:reference interface="" loadbalance="roundrobin" />

Ribbon配置隨機規則:

@Bean
public IRule loadBalancer(){
	return new RandomRule();
}
  • 失敗重試 對於RPC框架來說其實就是容錯機制,比如Dubbo內建了多種容錯機制包括:Failover、Failfast、Failsafe、Failback、Forking、Broadcast;預設的容錯機制就是Failover失敗自動切換,當出現失敗重試其它伺服器;配置容錯機制也很簡單:
<dubbo:reference cluster="failback" retries="2"/>
  • 健康檢查 註冊中心一般都有健康檢查功能,會實時檢測伺服器是否可用,如果不可用會移除,同時將更新推送給消費端;對使用者來說完全無感知;

分散式排程將排程器和執行器分離,執行器也是通過註冊中心的方式提供給排程器,然後由排程器進行負載均衡操作,流程已基本相似,此處不再一一介紹; 可以發現無狀態的負載均衡其實更多情況以來註冊中心,通過註冊中心來動態的增減執行單元,從而很方便的達到擴容縮容;

有狀態

有狀態的執行單元相對於無狀態來說更加有難度,因為每個節點的狀態是整個系統的一部分,不是能隨意增減的節點的;常見的有狀態中介軟體有:訊息佇列,分散式快取,資料庫中介軟體等;

訊息佇列

現在高吞吐量,高效能的訊息佇列越來越成為主流,比如RocketMQ,Kafka等,有強大的水平擴充套件能力;RocketMQ中引入Message Queue機制,Kafka引入分割槽(Partition),一個Topic對應多個分割槽,採用分而治之的思路來提高吞吐量,效能;可以看一個RocketMQ的簡易圖:

  • 操作單元配置 訊息佇列裡面的操作單元其實就是這裡的分割槽或者說Message Queue,比如RocketMQ是可以動態去修改讀寫佇列的數量;RocketMQ還提供了rocketmq-console控制檯,可以直接修改;

  • 負載均衡演算法 訊息佇列一般都有生產端和消費端,生產端預設是輪流給每個Message Queue傳送訊息,當然也可以自定義傳送策略可以通過MessageQueueSelector來實現;消費端分配策略包括:分頁模式(隨機分配模式)、手動配置模式、指定機房模式、就近機房模式、統一雜湊模式、環型模式;

  • 失敗重試 對於有狀態的執行單元來說,不是說宕機就可以直接移除的,需要保證資料的完整性,正常來說一般都會做主備處理,主機掛了備機接管;以RocketMQ為例,每個分割槽都有各自的備份,RocketMQ採取的策略是,備區僅僅是做資料的完整性保證,消費者能訊息備區的資料,但是並不會重新來接收資料;

  • 健康檢查 訊息佇列也有一個核心元件,可以理解為協調者,或者可以理解為註冊中心,Kafka使用zookeeper,RocketMQ使用NameServer,裡面其實就是儲存了相關的對應資訊比如Topic對應Message Queue,如果發現某臺broker不可用,會將資訊告知生產者,方式和註冊中心類似;

分散式快取

常見的分散式快取有redis、memcached,為了能夠容納更多的資料一般都會做分片處理,分片的方式也是多種多樣,就拿redis來說可以客戶端做分片,基於代理的分片,還有官方提供的Cluster方案;

  • 操作單元配置 快取雖然也有有狀態的,但是有其特殊性,其更多關注的是命中率,其實是可以容忍資料丟失的,比如基於代理的分片中介軟體codis,對客戶端全透明不影響服務的情況下可以完成增減redis例項;

  • 負載均衡演算法 基於保證命中率的前提下,基於代理分片的方式一般都會採用一致性雜湊演算法;而redis官方提供的Cluster方案,因為其內建有16384個虛擬槽,所以直接使用取模即完成分片;

  • 失敗重試 有狀態的分片一般都有會備區,在主區宕機後,備區接管實現故障遷移,比如redis的哨兵模式,或者codis這種中介軟體內建的功能;也無需去切換其他分割槽,對使用者來說這種接管完全是無感知的;

  • 健康檢查 以redis為例,哨兵模式中,sentinel通過心跳的方式實時監測節點,通過客觀下線來實施故障遷移;可以發現健康檢查基本都是通過心跳來檢測的方式;

資料庫層

資料庫層做均衡處理應該說是最複雜的,首先是有狀態的,其次是資料的安全性至關重要,常見的資料庫中介軟體包括:mycat,shardingjdbc等;

  • 操作單元配置 以分表為例,這裡的操作單元其實就是一個個分片資料表,資料量有時候往往出乎我們的預料,一般很少說固定給它分配多少個分片,最好是通過負載演算法自動生成資料表,而且最好事先就評估好某種負載演算法,不然後期如果想改變是很難的;

  • 負載均衡演算法 以mycat為例提供了多種負載演算法:範圍約定,取模,按日期分片,hash,一致性hash,分片列舉等等;比如下面的按天分割槽配置:

<tableRule name="sharding-by-date">
    <rule>
        <columns>create_time</columns>
        <algorithm>sharding-by-date</algorithm>
    </rule>
</tableRule>
<function name="sharding-by-date" class="io.mycat.route.function.PartitionByDate">
    <property name="dateFormat">yyyy-MM-dd</property>
    <property name="sBeginDate">2021-01-01</property>
    <property name="sEndDate">2051-01-01</property>
    <property name="sPartionDay">10</property>
</function>

指定了開始時間,結束時間,以及分割槽的天數;因為資料是隨時間連續的,所以這種方式擴充套件性是很好的;如果是取模的方式就要考慮清楚分片的數量了,後面如果想改變分片數量就很麻煩了,不像快取可以使用一致性hash來保證命中率就行了;

  • 失敗重試 有狀態的節點,備庫是少不了的,比如mycat提供了故障的主從切換功能,主宕機切換到從,基本都是這個套路,資料是不能丟的;

  • 健康檢查 同樣的主動檢測也必不可少,一般也是基於心跳語句去定時檢測,然後做故障主從切換;

以上是三種常見的有狀態中介軟體,可以發現雖然都是有狀態,但是根據資料的不同狀態(臨時的、最終的狀態),處理的方式也很不一樣;

其實還有一種有狀態的中介軟體:註冊中心,支援同時啟動多個節點,但是每個節點儲存的資料都是全量,因為註冊中心往往儲存的資料量很少,其提供的均衡策略可以像無狀態一樣簡單。

總結

綜上可以發現分而治之這種思想已經廣泛的被用在各種軟體中,遇到大的問題、大的資料量、大的併發量等等,其實核心思想就是拆分,至於如何拆分就要根據不同的業務需求使用不同的拆分演算法或者說均衡演算法,而且你需要保證上面介紹的幾個基本功能。

感謝關注

可以關注微信公眾號「回滾吧程式碼」,第一時間閱讀,文章持續更新;專注Java原始碼、架構、演算法和麵試。