<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深入淺出負(fù)載均衡

          共 21915字,需瀏覽 44分鐘

           ·

          2021-09-23 22:32

          作者:vivo互聯(lián)網(wǎng)團(tuán)隊-Zhang Peng


          一、負(fù)載均衡簡介


          1.1. 大型網(wǎng)站面臨的挑戰(zhàn)

          大型網(wǎng)站都要面對龐大的用戶量,高并發(fā),海量數(shù)據(jù)等挑戰(zhàn)。為了提升系統(tǒng)整體的性能,可以采用垂直擴(kuò)展和水平擴(kuò)展兩種方式。

          垂直擴(kuò)展:在網(wǎng)站發(fā)展早期,可以從單機(jī)的角度通過增加硬件處理能力,比如 CPU 處理能力,內(nèi)存容量,磁盤等方面,實現(xiàn)服務(wù)器處理能力的提升。但是,單機(jī)是有性能瓶頸的,一旦觸及瓶頸,再想提升,付出的成本和代價會極高。這顯然不能滿足大型分布式系統(tǒng)(網(wǎng)站)所有應(yīng)對的大流量,高并發(fā),海量數(shù)據(jù)等挑戰(zhàn)。

          水平擴(kuò)展:通過集群來分擔(dān)大型網(wǎng)站的流量。集群中的應(yīng)用服務(wù)器(節(jié)點(diǎn))通常被設(shè)計成無狀態(tài),用戶可以請求任何一個節(jié)點(diǎn),這些節(jié)點(diǎn)共同分擔(dān)訪問壓力。水平擴(kuò)展有兩個要點(diǎn):

          • 應(yīng)用集群:將同一應(yīng)用部署到多臺機(jī)器上,組成處理集群,接收負(fù)載均衡設(shè)備分發(fā)的請求,進(jìn)行處理,并返回相應(yīng)數(shù)據(jù)。
          • 負(fù)載均衡:將用戶訪問請求,通過某種算法,分發(fā)到集群中的節(jié)點(diǎn)。

          1.2. 什么是負(fù)載均衡

          負(fù)載均衡(Load Balance,簡稱 LB)是高并發(fā)、高可用系統(tǒng)必不可少的關(guān)鍵組件,目標(biāo)是 盡力將網(wǎng)絡(luò)流量平均分發(fā)到多個服務(wù)器上,以提高系統(tǒng)整體的響應(yīng)速度和可用性。

          負(fù)載均衡的主要作用如下:
          高并發(fā):負(fù)載均衡通過算法調(diào)整負(fù)載,盡力均勻的分配應(yīng)用集群中各節(jié)點(diǎn)的工作量,以此提高應(yīng)用集群的并發(fā)處理能力(吞吐量)。

          伸縮性:添加或減少服務(wù)器數(shù)量,然后由負(fù)載均衡進(jìn)行分發(fā)控制。這使得應(yīng)用集群具備伸縮性。

          高可用:負(fù)載均衡器可以監(jiān)控候選服務(wù)器,當(dāng)服務(wù)器不可用時,自動跳過,將請求分發(fā)給可用的服務(wù)器。這使得應(yīng)用集群具備高可用的特性。

          安全防護(hù):有些負(fù)載均衡軟件或硬件提供了安全性功能,如:黑白名單處理、防火墻,防 DDos 攻擊等。

          二、負(fù)載均衡的分類


          支持負(fù)載均衡的技術(shù)很多,我們可以通過不同維度去進(jìn)行分類。

          2.1 載體維度分類

          從支持負(fù)載均衡的載體來看,可以將負(fù)載均衡分為兩類:硬件負(fù)載均衡、軟件負(fù)載均衡

          2.1.1硬件負(fù)載均衡

          硬件負(fù)載均衡,一般是在定制處理器上運(yùn)行的獨(dú)立負(fù)載均衡服務(wù)器,價格昂貴,土豪專屬。硬件負(fù)載均衡的主流產(chǎn)品有:F5 和 A10。

          硬件負(fù)載均衡的 優(yōu)點(diǎn):

          • 功能強(qiáng)大:支持全局負(fù)載均衡并提供較全面的、復(fù)雜的負(fù)載均衡算法。
          • 性能強(qiáng)悍:硬件負(fù)載均衡由于是在專用處理器上運(yùn)行,因此吞吐量大,可支持單機(jī)百萬以上的并發(fā)。
          • 安全性高:往往具備防火墻,防 DDos 攻擊等安全功能。

          硬件負(fù)載均衡的 缺點(diǎn):

          • 成本昂貴:購買和維護(hù)硬件負(fù)載均衡的成本都很高。
          • 擴(kuò)展性差:當(dāng)訪問量突增時,超過限度不能動態(tài)擴(kuò)容。

          2.1.2 軟件負(fù)載均衡

          軟件負(fù)載均衡,應(yīng)用最廣泛,無論大公司還是小公司都會使用。
          軟件負(fù)載均衡從軟件層面實現(xiàn)負(fù)載均衡,一般可以在任何標(biāo)準(zhǔn)物理設(shè)備上運(yùn)行。

          軟件負(fù)載均衡的 主流產(chǎn)品 有:Nginx、HAProxy、LVS

          • LVS 可以作為四層負(fù)載均衡器。其負(fù)載均衡的性能要優(yōu)于 Nginx。
          • HAProxy 可以作為 HTTP 和 TCP 負(fù)載均衡器。
          • Nginx、HAProxy 可以作為四層或七層負(fù)載均衡器。

          軟件負(fù)載均衡的 優(yōu)點(diǎn):

          • 擴(kuò)展性好:適應(yīng)動態(tài)變化,可以通過添加軟件負(fù)載均衡實例,動態(tài)擴(kuò)展到超出初始容量的能力。
          • 成本低廉:軟件負(fù)載均衡可以在任何標(biāo)準(zhǔn)物理設(shè)備上運(yùn)行,降低了購買和運(yùn)維的成本。

          軟件負(fù)載均衡的 缺點(diǎn):

          • 性能略差:相比于硬件負(fù)載均衡,軟件負(fù)載均衡的性能要略低一些。

          2.2 網(wǎng)絡(luò)通信分類

          軟件負(fù)載均衡從通信層面來看,又可以分為四層和七層負(fù)載均衡。

          1) 七層負(fù)載均衡:就是可以根據(jù)訪問用戶的 HTTP 請求頭、URL 信息將請求轉(zhuǎn)發(fā)到特定的主機(jī)。

          • DNS 重定向
          • HTTP 重定向
          • 反向代理

          2) 四層負(fù)載均衡:基于 IP 地址和端口進(jìn)行請求的轉(zhuǎn)發(fā)。

          • 修改 IP 地址
          • 修改 MAC 地址

          2.2.1 DNS 負(fù)載均衡

          DNS 負(fù)載均衡一般用于互聯(lián)網(wǎng)公司,復(fù)雜的業(yè)務(wù)系統(tǒng)不適合使用。大型網(wǎng)站一般使用 DNS 負(fù)載均衡作為 第一級負(fù)載均衡手段,然后在內(nèi)部使用其它方式做第二級負(fù)載均衡。DNS 負(fù)載均衡屬于七層負(fù)載均衡。

          DNS 即 域名解析服務(wù),是 OSI 第七層網(wǎng)絡(luò)協(xié)議。DNS 被設(shè)計為一個樹形結(jié)構(gòu)的分布式應(yīng)用,自上而下依次為:根域名服務(wù)器,一級域名服務(wù)器,二級域名服務(wù)器,... ,本地域名服務(wù)器。顯然,如果所有數(shù)據(jù)都存儲在根域名服務(wù)器,那么 DNS 查詢的負(fù)載和開銷會非常龐大。

          因此,DNS 查詢相對于 DNS 層級結(jié)構(gòu),是一個逆向的遞歸流程,DNS 客戶端依次請求本地 DNS 服務(wù)器,上一級 DNS 服務(wù)器,上上一級 DNS 服務(wù)器,... ,根 DNS 服務(wù)器(又叫權(quán)威 DNS 服務(wù)器),一旦命中,立即返回。為了減少查詢次數(shù),每一級 DNS 服務(wù)器都會設(shè)置 DNS 查詢緩存。

          DNS 負(fù)載均衡的工作原理就是:基于 DNS 查詢緩存,按照負(fù)載情況返回不同服務(wù)器的 IP 地址。


          DNS 重定向的 優(yōu)點(diǎn):
          使用簡單:負(fù)載均衡工作,交給 DNS 服務(wù)器處理,省掉了負(fù)載均衡服務(wù)器維護(hù)的麻煩

          提高性能:可以支持基于地址的域名解析,解析成距離用戶最近的服務(wù)器地址(類似 CDN 的原理),可以加快訪問速度,改善性能;

          DNS 重定向的 缺點(diǎn):
          可用性差:DNS 解析是多級解析,新增/修改 DNS 后,解析時間較長;解析過程中,用戶訪問網(wǎng)站將失敗;

          擴(kuò)展性低:DNS 負(fù)載均衡的控制權(quán)在域名商那里,無法對其做更多的改善和擴(kuò)展;

          維護(hù)性差:也不能反映服務(wù)器的當(dāng)前運(yùn)行狀態(tài);支持的算法少;不能區(qū)分服務(wù)器的差異(不能根據(jù)系統(tǒng)與服務(wù)的狀態(tài)來判斷負(fù)載)。

          2.2.2 HTTP 負(fù)載均衡

          HTTP 負(fù)載均衡是基于 HTTP 重定向?qū)崿F(xiàn)的。HTTP 負(fù)載均衡屬于七層負(fù)載均衡。

          HTTP 重定向原理是:根據(jù)用戶的 HTTP 請求計算出一個真實的服務(wù)器地址,將該服務(wù)器地址寫入 HTTP 重定向響應(yīng)中,返回給瀏覽器,由瀏覽器重新進(jìn)行訪問。


          HTTP 重定向的優(yōu)點(diǎn):方案簡單。

          HTTP 重定向的 缺點(diǎn):
          性能較差:每次訪問需要兩次請求服務(wù)器,增加了訪問的延遲。

          降低搜索排名:使用重定向后,搜索引擎會視為 SEO 作弊。

          如果負(fù)載均衡器宕機(jī),就無法訪問該站點(diǎn)。
          由于其缺點(diǎn)比較明顯,所以這種負(fù)載均衡策略實際應(yīng)用較少。

          2.2.3 反向代理負(fù)載均衡

          反向代理(Reverse Proxy)方式是指以 代理服務(wù)器 來接受網(wǎng)絡(luò)請求,然后 將請求轉(zhuǎn)發(fā)給內(nèi)網(wǎng)中的服務(wù)器,并將從內(nèi)網(wǎng)中的服務(wù)器上得到的結(jié)果返回給網(wǎng)絡(luò)請求的客戶端。反向代理負(fù)載均衡屬于七層負(fù)載均衡。

          反向代理服務(wù)的主流產(chǎn)品:Nginx、Apache

          正向代理與反向代理有什么區(qū)別?
          正向代理:發(fā)生在 客戶端,是由用戶主動發(fā)起的。翻墻軟件就是典型的正向代理,客戶端通過主動訪問代理服務(wù)器,讓代理服務(wù)器獲得需要的外網(wǎng)數(shù)據(jù),然后轉(zhuǎn)發(fā)回客戶端。

          反向代理:發(fā)生在 服務(wù)端,用戶不知道代理的存在。


          反向代理是如何實現(xiàn)負(fù)載均衡的呢?以 Nginx 為例,如下所示:


          首先,在代理服務(wù)器上設(shè)定好負(fù)載均衡規(guī)則。然后,當(dāng)收到客戶端請求,反向代理服務(wù)器攔截指定的域名或 IP 請求,根據(jù)負(fù)載均衡算法,將請求分發(fā)到候選服務(wù)器上。其次,如果某臺候選服務(wù)器宕機(jī),反向代理服務(wù)器會有容錯處理,比如分發(fā)請求失敗 3 次以上,將請求分發(fā)到其他候選服務(wù)器上。

          反向代理的 優(yōu)點(diǎn):
          1) 多種負(fù)載均衡算法:支持多種負(fù)載均衡算法,以應(yīng)對不同的場景需求。

          2) 可以監(jiān)控服務(wù)器:基于 HTTP 協(xié)議,可以監(jiān)控轉(zhuǎn)發(fā)服務(wù)器的狀態(tài),如:系統(tǒng)負(fù)載、響應(yīng)時間、是否可用、連接數(shù)、流量等,從而根據(jù)這些數(shù)據(jù)調(diào)整負(fù)載均衡的策略。

          反向代理的 缺點(diǎn):
          1) 額外的轉(zhuǎn)發(fā)開銷:反向代理的轉(zhuǎn)發(fā)操作本身是有性能開銷的,可能會包括創(chuàng)建連接,等待連接響應(yīng),分析響應(yīng)結(jié)果等操作。
          2) 增加系統(tǒng)復(fù)雜度:反向代理常用于做分布式應(yīng)用的水平擴(kuò)展,但反向代理服務(wù)存在以下問題,為了解決以下問題會給系統(tǒng)整體增加額外的復(fù)雜度和運(yùn)維成本:
          • 反向代理服務(wù)如果自身宕機(jī),就無法訪問站點(diǎn),所以需要有 高可用 方案,常見的方案有:主備模式(一主一備)、雙主模式(互為主備)。

          • 反向代理服務(wù)自身也存在性能瓶頸,隨著需要轉(zhuǎn)發(fā)的請求量不斷攀升,需要有 可擴(kuò)展 方案。

          2.2.4 IP負(fù)載均衡

          IP 負(fù)載均衡是在網(wǎng)絡(luò)層通過修改請求目的地址進(jìn)行負(fù)載均衡。


          如上圖所示,IP 均衡處理流程大致為:
          客戶端請求 192.168.137.10,由負(fù)載均衡服務(wù)器接收到報文。

          負(fù)載均衡服務(wù)器根據(jù)算法選出一個服務(wù)節(jié)點(diǎn) 192.168.0.1,然后將報文請求地址改為該節(jié)點(diǎn)的 IP。

          真實服務(wù)節(jié)點(diǎn)收到請求報文,處理后,返回響應(yīng)數(shù)據(jù)到負(fù)載均衡服務(wù)器。

          負(fù)載均衡服務(wù)器將響應(yīng)數(shù)據(jù)的源地址改負(fù)載均衡服務(wù)器地址,返回給客戶端。

          IP 負(fù)載均衡在內(nèi)核進(jìn)程完成數(shù)據(jù)分發(fā),較反向代理負(fù)載均衡有更好的從處理性能。但是,由于所有請求響應(yīng)都要經(jīng)過負(fù)載均衡服務(wù)器,集群的吞吐量受制于負(fù)載均衡服務(wù)器的帶寬。

          2.2.5 數(shù)據(jù)鏈路層負(fù)載均衡

          數(shù)據(jù)鏈路層負(fù)載均衡是指在通信協(xié)議的數(shù)據(jù)鏈路層修改 mac 地址進(jìn)行負(fù)載均衡。


          在 Linux 平臺上最好的鏈路層負(fù)載均衡開源產(chǎn)品是 LVS (Linux Virtual Server)。LVS 是基于 Linux 內(nèi)核中 netfilter 框架實現(xiàn)的負(fù)載均衡系統(tǒng)。netfilter 是內(nèi)核態(tài)的 Linux 防火墻機(jī)制,可以在數(shù)據(jù)包流經(jīng)過程中,根據(jù)規(guī)則設(shè)置若干個關(guān)卡(hook 函數(shù))來執(zhí)行相關(guān)的操作。

          LVS 的工作流程大致如下:
          當(dāng)用戶訪問 www.sina.com.cn 時,用戶數(shù)據(jù)通過層層網(wǎng)絡(luò),最后通過交換機(jī)進(jìn)入 LVS 服務(wù)器網(wǎng)卡,并進(jìn)入內(nèi)核網(wǎng)絡(luò)層。
          進(jìn)入 PREROUTING 后經(jīng)過路由查找,確定訪問的目的 VIP 是本機(jī) IP 地址,所以數(shù)據(jù)包進(jìn)入到 INPUT 鏈上
          IPVS 是工作在 INPUT 鏈上,會根據(jù)訪問的 vip+port 判斷請求是否 IPVS 服務(wù),如果是則調(diào)用注冊的 IPVS HOOK 函數(shù),進(jìn)行 IPVS 相關(guān)主流程,強(qiáng)行修改數(shù)據(jù)包的相關(guān)數(shù)據(jù),并將數(shù)據(jù)包發(fā)往 POSTROUTING 鏈上。
          POSTROUTING 上收到數(shù)據(jù)包后,根據(jù)目標(biāo) IP 地址(后端服務(wù)器),通過路由選路,將數(shù)據(jù)包最終發(fā)往后端的服務(wù)器上。

          開源 LVS 版本有 3 種工作模式,每種模式工作原理截然不同,說各種模式都有自己的優(yōu)缺點(diǎn),分別適合不同的應(yīng)用場景,不過最終本質(zhì)的功能都是能實現(xiàn)均衡的流量調(diào)度和良好的擴(kuò)展性。主要包括三種模式:DR 模式、NAT 模式、Tunnel 模式。

          三、負(fù)載均衡算法


          負(fù)載均衡器的實現(xiàn)可以分為兩個部分:
          根據(jù)負(fù)載均衡算法在候選服務(wù)器列表選出一個服務(wù)器;

          將請求數(shù)據(jù)發(fā)送到該服務(wù)器上。

          負(fù)載均衡算法是負(fù)載均衡服務(wù)核心中的核心。負(fù)載均衡產(chǎn)品多種多樣,但是各種負(fù)載均衡算法原理是共性的。負(fù)載均衡算法有很多種,分別適用于不同的應(yīng)用場景,本文僅介紹最為常見的負(fù)載均衡算法的特性及原理:輪詢、隨機(jī)、最小活躍數(shù)、源地址哈希、一致性哈希

          注:負(fù)載均衡算法的實現(xiàn),推薦閱讀 Dubbo 官方負(fù)載均衡算法說明 ,源碼講解非常詳細(xì),非常值得借鑒。

          3.1 隨機(jī)

          3.1.1 隨機(jī)算法

          隨機(jī)(Random) 算法將請求隨機(jī)分發(fā)到候選服務(wù)器。

          隨機(jī)算法 適合服務(wù)器硬件相同的場景。學(xué)習(xí)過概率論的都知道,調(diào)用量較小的時候,可能負(fù)載并不均勻,調(diào)用量越大,負(fù)載越均衡。


          【示例】隨機(jī)算法實現(xiàn)示例

          負(fù)載均衡接口
          public interface LoadBalance<N extends Node> {
          N select(List<N> nodes, String ip);
          }
          ?

          負(fù)載均衡抽象類
          public abstract class BaseLoadBalance<N extends Node> implements LoadBalance<N> {
          @Override public N select(List<N> nodes, String ip) { if (CollectionUtil.isEmpty(nodes)) { return null; }
          // 如果 nodes 列表中僅有一個 node,直接返回即可,無需進(jìn)行負(fù)載均衡 if (nodes.size() == 1) { return nodes.get(0); }
          return doSelect(nodes, ip); }
          protected abstract N doSelect(List<N> nodes, String ip);
          }
          ?

          服務(wù)器節(jié)點(diǎn)類
          public class Node implements Comparable<Node> {
          protected String url;
          protected Integer weight;
          protected Integer active;
          // ...}
          ?

          隨機(jī)算法實現(xiàn)
          public class RandomLoadBalance<N extends Node> extends BaseLoadBalance<N> implements LoadBalance<N> {
          private final Random random = new Random();
          @Override protected N doSelect(List<N> nodes, String ip) { // 在列表中隨機(jī)選取一個節(jié)點(diǎn) int index = random.nextInt(nodes.size()); return nodes.get(index); }
          }
          ?

          3.1.2 加權(quán)隨機(jī)算法

          加權(quán)隨機(jī)(Weighted Random 算法在隨機(jī)算法的基礎(chǔ)上,按照概率調(diào)整權(quán)重,進(jìn)行負(fù)載分配。

          【示例】加權(quán)隨機(jī)算法實現(xiàn)示例
          public class WeightRandomLoadBalance<N extends Node> extends BaseLoadBalance<N> implements LoadBalance<N> {
          private final Random random = ThreadLocalRandom.current();
          @Override protected N doSelect(List<N> nodes, String ip) {
          int length = nodes.size(); AtomicInteger totalWeight = new AtomicInteger(0); for (N node : nodes) { Integer weight = node.getWeight(); totalWeight.getAndAdd(weight); }
          if (totalWeight.get() > 0) { int offset = random.nextInt(totalWeight.get()); for (N node : nodes) { // 讓隨機(jī)值 offset 減去權(quán)重值 offset -= node.getWeight(); if (offset < 0) { // 返回相應(yīng)的 Node return node; } } }
          // 直接隨機(jī)返回一個 return nodes.get(random.nextInt(length)); }
          }
          ?

          3.2 輪詢

          3.2.1 輪詢算法

          輪詢(Round Robin)算法的策略是:將請求依次分發(fā)到候選服務(wù)器。

          如下圖所示,負(fù)載均衡器收到來自客戶端的 6 個請求,(1, 3, 5) 的請求會被發(fā)送到服務(wù)器 1,(2, 4, 6) 的請求會被發(fā)送到服務(wù)器 2。


          該算法適合場景:各服務(wù)器處理能力相近,且每個事務(wù)工作量差異不大。如果存在較大差異,那么處理較慢的服務(wù)器就可能會積壓請求,最終無法承擔(dān)過大的負(fù)載。


          【示例】輪詢算法示例

          輪詢負(fù)載均衡算法實現(xiàn)
          public class RoundRobinLoadBalance<N extends Node> extends BaseLoadBalance<N> implements LoadBalance<N> {
          private final AtomicInteger position = new AtomicInteger(0);
          @Override protected N doSelect(List<N> nodes, String ip) { int length = nodes.size(); // 如果位置值已經(jīng)等于節(jié)點(diǎn)數(shù),重置為 0 position.compareAndSet(length, 0); N node = nodes.get(position.get()); position.getAndIncrement(); return node; }
          }
          ?

          3.2.2 加權(quán)輪詢算法

          加權(quán)輪詢(Weighted Round Robbin)算法在輪詢算法的基礎(chǔ)上,增加了權(quán)重屬性來調(diào)節(jié)轉(zhuǎn)發(fā)服務(wù)器的請求數(shù)目。性能高、處理速度快的節(jié)點(diǎn)應(yīng)該設(shè)置更高的權(quán)重,使得分發(fā)時優(yōu)先將請求分發(fā)到權(quán)重較高的節(jié)點(diǎn)上。

          如下圖所示,服務(wù)器 A 設(shè)置權(quán)重為 5,服務(wù)器 B 設(shè)置權(quán)重為 1,負(fù)載均衡器收到來自客戶端的 6 個請求,那么 (1, 2, 3, 4, 5) 請求會被發(fā)送到服務(wù)器 A,(6) 請求會被發(fā)送到服務(wù)器 B。


          【示例】加權(quán)輪詢算法實現(xiàn)示例

          以下實現(xiàn)基于 Dubbo 加權(quán)輪詢算法做了一些簡化。
          public class WeightRoundRobinLoadBalance<N extends Node> extends BaseLoadBalance<N> implements LoadBalance<N> {
          /** * 60秒 */ private static final int RECYCLE_PERIOD = 60000;
          /** * Node hashcode 到 WeightedRoundRobin 的映射關(guān)系 */ private ConcurrentMap<Integer, WeightedRoundRobin> weightMap = new ConcurrentHashMap<>();
          /** * 原子更新鎖 */ private AtomicBoolean updateLock = new AtomicBoolean();
          @Override protected N doSelect(List<N> nodes, String ip) {
          int totalWeight = 0; long maxCurrent = Long.MIN_VALUE;
          // 獲取當(dāng)前時間 long now = System.currentTimeMillis(); N selectedNode = null; WeightedRoundRobin selectedWRR = null;
          // 下面這個循環(huán)主要做了這樣幾件事情: // 1. 遍歷 Node 列表,檢測當(dāng)前 Node 是否有相應(yīng)的 WeightedRoundRobin,沒有則創(chuàng)建 // 2. 檢測 Node 權(quán)重是否發(fā)生了變化,若變化了,則更新 WeightedRoundRobin 的 weight 字段 // 3. 讓 current 字段加上自身權(quán)重,等價于 current += weight // 4. 設(shè)置 lastUpdate 字段,即 lastUpdate = now // 5. 尋找具有最大 current 的 Node,以及 Node 對應(yīng)的 WeightedRoundRobin, // 暫存起來,留作后用 // 6. 計算權(quán)重總和 for (N node : nodes) { int hashCode = node.hashCode(); WeightedRoundRobin weightedRoundRobin = weightMap.get(hashCode); int weight = node.getWeight(); if (weight < 0) { weight = 0; }
          // 檢測當(dāng)前 Node 是否有對應(yīng)的 WeightedRoundRobin,沒有則創(chuàng)建 if (weightedRoundRobin == null) { weightedRoundRobin = new WeightedRoundRobin(); // 設(shè)置 Node 權(quán)重 weightedRoundRobin.setWeight(weight); // 存儲 url 唯一標(biāo)識 identifyString 到 weightedRoundRobin 的映射關(guān)系 weightMap.putIfAbsent(hashCode, weightedRoundRobin); weightedRoundRobin = weightMap.get(hashCode); } // Node 權(quán)重不等于 WeightedRoundRobin 中保存的權(quán)重,說明權(quán)重變化了,此時進(jìn)行更新 if (weight != weightedRoundRobin.getWeight()) { weightedRoundRobin.setWeight(weight); }
          // 讓 current 加上自身權(quán)重,等價于 current += weight long current = weightedRoundRobin.increaseCurrent(); // 設(shè)置 lastUpdate,表示近期更新過 weightedRoundRobin.setLastUpdate(now); // 找出最大的 current if (current > maxCurrent) { maxCurrent = current; // 將具有最大 current 權(quán)重的 Node 賦值給 selectedNode selectedNode = node; // 將 Node 對應(yīng)的 weightedRoundRobin 賦值給 selectedWRR,留作后用 selectedWRR = weightedRoundRobin; }
          // 計算權(quán)重總和 totalWeight += weight; }
          // 對 weightMap 進(jìn)行檢查,過濾掉長時間未被更新的節(jié)點(diǎn)。 // 該節(jié)點(diǎn)可能掛了,nodes 中不包含該節(jié)點(diǎn),所以該節(jié)點(diǎn)的 lastUpdate 長時間無法被更新。 // 若未更新時長超過閾值后,就會被移除掉,默認(rèn)閾值為60秒。 if (!updateLock.get() && nodes.size() != weightMap.size()) { if (updateLock.compareAndSet(false, true)) { try { // 遍歷修改,即移除過期記錄 weightMap.entrySet().removeIf(item -> now - item.getValue().getLastUpdate() > RECYCLE_PERIOD); } finally { updateLock.set(false); } } }
          if (selectedNode != null) { // 讓 current 減去權(quán)重總和,等價于 current -= totalWeight selectedWRR.decreaseCurrent(totalWeight); // 返回具有最大 current 的 Node return selectedNode; }
          // should not happen here return nodes.get(0); }
          protected static class WeightedRoundRobin {
          // 服務(wù)提供者權(quán)重 private int weight; // 當(dāng)前權(quán)重 private AtomicLong current = new AtomicLong(0); // 最后一次更新時間 private long lastUpdate;
          public long increaseCurrent() { // current = current + weight; return current.addAndGet(weight); }
          public long decreaseCurrent(int total) { // current = current - total; return current.addAndGet(-1 * total); }
          public int getWeight() { return weight; }
          public void setWeight(int weight) { this.weight = weight; // 初始情況下,current = 0 current.set(0); }
          public AtomicLong getCurrent() { return current; }
          public void setCurrent(AtomicLong current) { this.current = current; }
          public long getLastUpdate() { return lastUpdate; }
          public void setLastUpdate(long lastUpdate) { this.lastUpdate = lastUpdate; }
          }
          }
          ?

          3.3 最小活躍數(shù)

          最小活躍數(shù)(Least Active)算法 將請求分發(fā)到連接數(shù)/請求數(shù)最少的候選服務(wù)器(目前處理請求最少的服務(wù)器)。

          • 特點(diǎn):根據(jù)候選服務(wù)器當(dāng)前的請求連接數(shù),動態(tài)分配。
          • 場景:適用于對系統(tǒng)負(fù)載較為敏感或請求連接時長相差較大的場景。

          由于每個請求的連接時長不一樣,如果采用簡單的輪循或隨機(jī)算法,都可能出現(xiàn)某些服務(wù)器當(dāng)前連接數(shù)過大,而另一些服務(wù)器的連接過小的情況,這就造成了負(fù)載并非真正均衡。雖然,輪詢或算法都可以通過加權(quán)重屬性的方式進(jìn)行負(fù)載調(diào)整,但加權(quán)方式難以應(yīng)對動態(tài)變化。

          例如下圖中,(1, 3, 5) 請求會被發(fā)送到服務(wù)器 1,但是 (1, 3) 很快就斷開連接,此時只有 (5) 請求連接服務(wù)器 1;(2, 4, 6) 請求被發(fā)送到服務(wù)器 2,只有 (2) 的連接斷開。該系統(tǒng)繼續(xù)運(yùn)行時,服務(wù)器 2 會承擔(dān)過大的負(fù)載。


          最小活躍數(shù)算法會記錄當(dāng)前時刻,每個候選節(jié)點(diǎn)正在處理的連接數(shù),然后選擇連接數(shù)最小的節(jié)點(diǎn)。該策略能夠動態(tài)、實時地反應(yīng)服務(wù)器的當(dāng)前狀況,較為合理地將負(fù)責(zé)分配均勻,適用于對當(dāng)前系統(tǒng)負(fù)載較為敏感的場景。

          例如下圖中,服務(wù)器 1 當(dāng)前連接數(shù)最小,那么新到來的請求 6 就會被發(fā)送到服務(wù)器 1 上。


          加權(quán)最小活躍數(shù)(Weighted Least Connection)在最小活躍數(shù)的基礎(chǔ)上,根據(jù)服務(wù)器的性能為每臺服務(wù)器分配權(quán)重,再根據(jù)權(quán)重計算出每臺服務(wù)器能處理的連接數(shù)。

          最小活躍數(shù)算法實現(xiàn)要點(diǎn):活躍調(diào)用數(shù)越小,表明該服務(wù)節(jié)點(diǎn)處理能力越高,單位時間內(nèi)可處理更多的請求,應(yīng)優(yōu)先將請求分發(fā)給該服務(wù)。在具體實現(xiàn)中,每個服務(wù)節(jié)點(diǎn)對應(yīng)一個活躍數(shù) active。初始情況下,所有服務(wù)提供者活躍數(shù)均為 0。每收到一個請求,活躍數(shù)加 1,完成請求后則將活躍數(shù)減 1。在服務(wù)運(yùn)行一段時間后,性能好的服務(wù)提供者處理請求的速度更快,因此活躍數(shù)下降的也越快,此時這樣的服務(wù)提供者能夠優(yōu)先獲取到新的服務(wù)請求、這就是最小活躍數(shù)負(fù)載均衡算法的基本思想。

          【示例】最小活躍數(shù)算法實現(xiàn)

          以下實現(xiàn)基于 Dubbo 最小活躍數(shù)負(fù)載均衡算法做了些許改動。
          public class LeastActiveLoadBalance<N extends Node> extends BaseLoadBalance<N> implements LoadBalance<N> {
          private final Random random = new Random();
          @Override protected N doSelect(List<N> nodes, String ip) { int length = nodes.size(); // 最小的活躍數(shù) int leastActive = -1; // 具有相同“最小活躍數(shù)”的服務(wù)者提供者(以下用 Node 代稱)數(shù)量 int leastCount = 0; // leastIndexs 用于記錄具有相同“最小活躍數(shù)”的 Node 在 nodes 列表中的下標(biāo)信息 int[] leastIndexs = new int[length]; int totalWeight = 0; // 第一個最小活躍數(shù)的 Node 權(quán)重值,用于與其他具有相同最小活躍數(shù)的 Node 的權(quán)重進(jìn)行對比, // 以檢測是否“所有具有相同最小活躍數(shù)的 Node 的權(quán)重”均相等 int firstWeight = 0; boolean sameWeight = true;
          // 遍歷 nodes 列表 for (int i = 0; i < length; i++) { N node = nodes.get(i); // 發(fā)現(xiàn)更小的活躍數(shù),重新開始 if (leastActive == -1 || node.getActive() < leastActive) { // 使用當(dāng)前活躍數(shù)更新最小活躍數(shù) leastActive leastActive = node.getActive(); // 更新 leastCount 為 1 leastCount = 1; // 記錄當(dāng)前下標(biāo)值到 leastIndexs 中 leastIndexs[0] = i; totalWeight = node.getWeight(); firstWeight = node.getWeight(); sameWeight = true;
          // 當(dāng)前 Node 的活躍數(shù) node.getActive() 與最小活躍數(shù) leastActive 相同 } else if (node.getActive() == leastActive) { // 在 leastIndexs 中記錄下當(dāng)前 Node 在 nodes 集合中的下標(biāo) leastIndexs[leastCount++] = i; // 累加權(quán)重 totalWeight += node.getWeight(); // 檢測當(dāng)前 Node 的權(quán)重與 firstWeight 是否相等, // 不相等則將 sameWeight 置為 false if (sameWeight && i > 0 && node.getWeight() != firstWeight) { sameWeight = false; } } }
          // 當(dāng)只有一個 Node 具有最小活躍數(shù),此時直接返回該 Node 即可 if (leastCount == 1) { return nodes.get(leastIndexs[0]); }
          // 有多個 Node 具有相同的最小活躍數(shù),但它們之間的權(quán)重不同 if (!sameWeight && totalWeight > 0) { // 隨機(jī)生成一個 [0, totalWeight) 之間的數(shù)字 int offsetWeight = random.nextInt(totalWeight); // 循環(huán)讓隨機(jī)數(shù)減去具有最小活躍數(shù)的 Node 的權(quán)重值, // 當(dāng) offset 小于等于0時,返回相應(yīng)的 Node for (int i = 0; i < leastCount; i++) { int leastIndex = leastIndexs[i]; // 獲取權(quán)重值,并讓隨機(jī)數(shù)減去權(quán)重值 offsetWeight -= nodes.get(leastIndex).getWeight(); if (offsetWeight <= 0) { return nodes.get(leastIndex); } } } // 如果權(quán)重相同或權(quán)重為0時,隨機(jī)返回一個 Node return nodes.get(leastIndexs[random.nextInt(leastCount)]); }
          }
          ?

          3.4 源地址哈希

          源地址哈希(IP Hash)算法 根據(jù)請求源 IP,通過哈希計算得到一個數(shù)值,用該數(shù)值在候選服務(wù)器列表的進(jìn)行取模運(yùn)算,得到的結(jié)果便是選中的服務(wù)器。

          可以保證同一 IP 的客戶端的請求會轉(zhuǎn)發(fā)到同一臺服務(wù)器上,用來實現(xiàn)會話粘滯(Sticky Session)。
          特點(diǎn):保證特定用戶總是請求到相同的服務(wù)器,若服務(wù)器宕機(jī),會話會丟失。

          【示例】源地址哈希算法實現(xiàn)示例
          public class IpHashLoadBalance<N extends Node> extends BaseLoadBalance<N> implements LoadBalance<N> {
          @Override protected N doSelect(List<N> nodes, String ip) { if (StrUtil.isBlank(ip)) { ip = "127.0.0.1"; }
          int length = nodes.size(); int index = hash(ip) % length; return nodes.get(index); }
          public int hash(String text) { return HashUtil.fnvHash(text); }
          }
          ?

          3.5 一致性哈希

          一致性哈希(Consistent Hash)算法的目標(biāo)是:相同的請求盡可能落到同一個服務(wù)器上。

          一致性哈希 可以很好的解決 穩(wěn)定性問題,可以將所有的 存儲節(jié)點(diǎn) 排列在 首尾相接 的 Hash 環(huán)上,每個 key 在計算 Hash 后會 順時針 找到 臨接 的 存儲節(jié)點(diǎn) 存放。而當(dāng)有節(jié)點(diǎn) 加入 或 退出 時,僅影響該節(jié)點(diǎn)在 Hash環(huán)上順時針相鄰的后續(xù)節(jié)點(diǎn)。


          1)相同的請求是指:一般在使用一致性哈希時,需要指定一個 key 用于 hash 計算,可能是:
          用戶 ID

          請求方 IP

          請求服務(wù)名稱,參數(shù)列表構(gòu)成的串
          2)盡可能是指:服務(wù)器可能發(fā)生上下線,少數(shù)服務(wù)器的變化不應(yīng)該影響大多數(shù)的請求。

          當(dāng)某臺候選服務(wù)器宕機(jī)時,原本發(fā)往該服務(wù)器的請求,會基于虛擬節(jié)點(diǎn),平攤到其它候選服務(wù)器,不會引起劇烈變動。

          優(yōu)點(diǎn):加入 和 刪除 節(jié)點(diǎn)只影響 哈希環(huán) 中 順時針方向 的 相鄰的節(jié)點(diǎn),對其他節(jié)點(diǎn)無影響。

          缺點(diǎn):加減節(jié)點(diǎn) 會造成 哈希環(huán) 中部分?jǐn)?shù)據(jù) 無法命中。當(dāng)使用 少量節(jié)點(diǎn) 時,節(jié)點(diǎn)變化 將大范圍影響 哈希環(huán) 中 數(shù)據(jù)映射,不適合 少量數(shù)據(jù)節(jié)點(diǎn) 的分布式方案。普通 的 一致性哈希分區(qū) 在增減節(jié)點(diǎn)時需要 增加一倍 或 減去一半 節(jié)點(diǎn)才能保證 數(shù)據(jù) 和 負(fù)載的均衡。

          注意:因為 一致性哈希分區(qū) 的這些缺點(diǎn),一些分布式系統(tǒng)采用 虛擬槽 對 一致性哈希 進(jìn)行改進(jìn),比如 Dynamo 系統(tǒng)。

          【示例】一致性哈希算法示例
          public class ConsistentHashLoadBalance<N extends Node> extends BaseLoadBalance<N> implements LoadBalance<N> {
          private final ConcurrentMap<String, ConsistentHashSelector<?>> selectors = new ConcurrentHashMap<>();
          @SuppressWarnings("unchecked") @Override protected N doSelect(List<N> nodes, String ip) { // 分片數(shù),這里設(shè)為節(jié)點(diǎn)數(shù)的 4 倍 Integer replicaNum = nodes.size() * 4; // 獲取 nodes 原始的 hashcode int identityHashCode = System.identityHashCode(nodes);
          // 如果 nodes 是一個新的 List 對象,意味著節(jié)點(diǎn)數(shù)量發(fā)生了變化 // 此時 selector.identityHashCode != identityHashCode 條件成立 ConsistentHashSelector<N> selector = (ConsistentHashSelector<N>) selectors.get(ip); if (selector == null || selector.identityHashCode != identityHashCode) { // 創(chuàng)建新的 ConsistentHashSelector selectors.put(ip, new ConsistentHashSelector<>(nodes, identityHashCode, replicaNum)); selector = (ConsistentHashSelector<N>) selectors.get(ip); } // 調(diào)用 ConsistentHashSelector 的 select 方法選擇 Node return selector.select(ip); }
          /** * 一致性哈希選擇器 */ private static final class ConsistentHashSelector<N extends Node> {
          /** * 存儲虛擬節(jié)點(diǎn) */ private final TreeMap<Long, N> virtualNodes;
          private final int identityHashCode;
          /** * 構(gòu)造器 * * @param nodes 節(jié)點(diǎn)列表 * @param identityHashCode hashcode * @param replicaNum 分片數(shù) */ ConsistentHashSelector(List<N> nodes, int identityHashCode, Integer replicaNum) { this.virtualNodes = new TreeMap<>(); this.identityHashCode = identityHashCode; // 獲取虛擬節(jié)點(diǎn)數(shù),默認(rèn)為 100 if (replicaNum == null) { replicaNum = 100; } for (N node : nodes) { for (int i = 0; i < replicaNum / 4; i++) { // 對 url 進(jìn)行 md5 運(yùn)算,得到一個長度為16的字節(jié)數(shù)組 byte[] digest = md5(node.getUrl()); // 對 digest 部分字節(jié)進(jìn)行 4 次 hash 運(yùn)算,得到四個不同的 long 型正整數(shù) for (int j = 0; j < 4; j++) { // h = 0 時,取 digest 中下標(biāo)為 0 ~ 3 的4個字節(jié)進(jìn)行位運(yùn)算 // h = 1 時,取 digest 中下標(biāo)為 4 ~ 7 的4個字節(jié)進(jìn)行位運(yùn)算 // h = 2, h = 3 時過程同上 long m = hash(digest, j); // 將 hash 到 node 的映射關(guān)系存儲到 virtualNodes 中, // virtualNodes 需要提供高效的查詢操作,因此選用 TreeMap 作為存儲結(jié)構(gòu) virtualNodes.put(m, node); } } } }
          public N select(String key) { // 對參數(shù) key 進(jìn)行 md5 運(yùn)算 byte[] digest = md5(key); // 取 digest 數(shù)組的前四個字節(jié)進(jìn)行 hash 運(yùn)算,再將 hash 值傳給 selectForKey 方法, // 尋找合適的 Node return selectForKey(hash(digest, 0)); }
          private N selectForKey(long hash) { // 查找第一個大于或等于當(dāng)前 hash 的節(jié)點(diǎn) Map.Entry<Long, N> entry = virtualNodes.ceilingEntry(hash); // 如果 hash 大于 Node 在哈希環(huán)上最大的位置,此時 entry = null, // 需要將 TreeMap 的頭節(jié)點(diǎn)賦值給 entry if (entry == null) { entry = virtualNodes.firstEntry(); } // 返回 Node return entry.getValue(); }
          }
          /** * 計算 hash 值 */ public static long hash(byte[] digest, int number) { return (((long) (digest[3 + number * 4] & 0xFF) << 24) | ((long) (digest[2 + number * 4] & 0xFF) << 16) | ((long) (digest[1 + number * 4] & 0xFF) << 8) | (digest[number * 4] & 0xFF)) & 0xFFFFFFFFL; }
          /** * 計算 MD5 值 */ public static byte[] md5(String value) { MessageDigest md5; try { md5 = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException(e.getMessage(), e); } md5.reset(); byte[] bytes = value.getBytes(StandardCharsets.UTF_8); md5.update(bytes); return md5.digest(); }
          }
          ?

          以上示例基于 Dubbo 的一致性哈希負(fù)載均衡算法做了一些簡化。

          四、參考資料


          1. Comparing Load Balancing Algorithms
          2. 《大型網(wǎng)站技術(shù)架構(gòu):核心原理與案例分析》
          3. 大型網(wǎng)站架構(gòu)系列:負(fù)載均衡詳解(1)
          4. 什么是負(fù)載均衡
          5. What Is Load Balancing
          6. Dubbo 官方負(fù)載均衡算法說明
          7. 負(fù)載均衡算法及手段
          8. 利用 dns 解析來實現(xiàn)網(wǎng)站的負(fù)載均衡


          END

          猜你喜歡

          瀏覽 48
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  黄色成人网站在线播放 | 午夜色丁香 | 日逼视频网址 | 骚逼看片 | 亚洲无码专区精品 |