深入淺出負(fù)載均衡
作者:vivo互聯(lián)網(wǎng)團(tuán)隊-Zhang Peng
一、負(fù)載均衡簡介
應(yīng)用集群:將同一應(yīng)用部署到多臺機(jī)器上,組成處理集群,接收負(fù)載均衡設(shè)備分發(fā)的請求,進(jìn)行處理,并返回相應(yīng)數(shù)據(jù)。 負(fù)載均衡:將用戶訪問請求,通過某種算法,分發(fā)到集群中的節(jié)點(diǎn)。
高并發(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ù)載均衡的分類
功能強(qiáng)大:支持全局負(fù)載均衡并提供較全面的、復(fù)雜的負(fù)載均衡算法。 性能強(qiáng)悍:硬件負(fù)載均衡由于是在專用處理器上運(yùn)行,因此吞吐量大,可支持單機(jī)百萬以上的并發(fā)。 安全性高:往往具備防火墻,防 DDos 攻擊等安全功能。
成本昂貴:購買和維護(hù)硬件負(fù)載均衡的成本都很高。 擴(kuò)展性差:當(dāng)訪問量突增時,超過限度不能動態(tài)擴(kuò)容。
LVS 可以作為四層負(fù)載均衡器。其負(fù)載均衡的性能要優(yōu)于 Nginx。 HAProxy 可以作為 HTTP 和 TCP 負(fù)載均衡器。 Nginx、HAProxy 可以作為四層或七層負(fù)載均衡器。
擴(kuò)展性好:適應(yīng)動態(tài)變化,可以通過添加軟件負(fù)載均衡實例,動態(tài)擴(kuò)展到超出初始容量的能力。 成本低廉:軟件負(fù)載均衡可以在任何標(biāo)準(zhǔn)物理設(shè)備上運(yùn)行,降低了購買和運(yùn)維的成本。
性能略差:相比于硬件負(fù)載均衡,軟件負(fù)載均衡的性能要略低一些。
DNS 重定向 HTTP 重定向 反向代理
修改 IP 地址 修改 MAC 地址

使用簡單:負(fù)載均衡工作,交給 DNS 服務(wù)器處理,省掉了負(fù)載均衡服務(wù)器維護(hù)的麻煩 提高性能:可以支持基于地址的域名解析,解析成距離用戶最近的服務(wù)器地址(類似 CDN 的原理),可以加快訪問速度,改善性能;
可用性差: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ù)載)。

性能較差:每次訪問需要兩次請求服務(wù)器,增加了訪問的延遲。 降低搜索排名:使用重定向后,搜索引擎會視為 SEO 作弊。 如果負(fù)載均衡器宕機(jī),就無法訪問該站點(diǎn)。
正向代理:發(fā)生在 客戶端,是由用戶主動發(fā)起的。翻墻軟件就是典型的正向代理,客戶端通過主動訪問代理服務(wù)器,讓代理服務(wù)器獲得需要的外網(wǎng)數(shù)據(jù),然后轉(zhuǎn)發(fā)回客戶端。 反向代理:發(fā)生在 服務(wù)端,用戶不知道代理的存在。


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ù)載均衡的策略。
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ò)展 方案。

客戶端請求 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ù)器地址,返回給客戶端。

當(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ù)器上。
三、負(fù)載均衡算法
根據(jù)負(fù)載均衡算法在候選服務(wù)器列表選出一個服務(wù)器; 將請求數(shù)據(jù)發(fā)送到該服務(wù)器上。

public interface LoadBalance<N extends Node> {N select(List<N> nodes, String ip);}
public abstract class BaseLoadBalance<N extends Node> implements LoadBalance<N> {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);}
public class Node implements Comparable<Node> {protected String url;protected Integer weight;protected Integer active;// ...}
public class RandomLoadBalance<N extends Node> extends BaseLoadBalance<N> implements LoadBalance<N> {private final Random random = new Random();protected N doSelect(List<N> nodes, String ip) {// 在列表中隨機(jī)選取一個節(jié)點(diǎn)int index = random.nextInt(nodes.size());return nodes.get(index);}}
public class WeightRandomLoadBalance<N extends Node> extends BaseLoadBalance<N> implements LoadBalance<N> {private final Random random = ThreadLocalRandom.current();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)的 Nodereturn node;}}}// 直接隨機(jī)返回一個return nodes.get(random.nextInt(length));}}


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

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();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 += weightlong current = weightedRoundRobin.increaseCurrent();// 設(shè)置 lastUpdate,表示近期更新過weightedRoundRobin.setLastUpdate(now);// 找出最大的 currentif (current > maxCurrent) {maxCurrent = current;// 將具有最大 current 權(quán)重的 Node 賦值給 selectedNodeselectedNode = 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 -= totalWeightselectedWRR.decreaseCurrent(totalWeight);// 返回具有最大 current 的 Nodereturn selectedNode;}// should not happen herereturn 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 = 0current.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;}}}
特點(diǎn):根據(jù)候選服務(wù)器當(dāng)前的請求連接數(shù),動態(tài)分配。 場景:適用于對系統(tǒng)負(fù)載較為敏感或請求連接時長相差較大的場景。


public class LeastActiveLoadBalance<N extends Node> extends BaseLoadBalance<N> implements LoadBalance<N> {private final Random random = new Random();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ù) leastActiveleastActive = node.getActive();// 更新 leastCount 為 1leastCount = 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 置為 falseif (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)的 Nodefor (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ī)返回一個 Nodereturn nodes.get(leastIndexs[random.nextInt(leastCount)]);}}
特點(diǎn):保證特定用戶總是請求到相同的服務(wù)器,若服務(wù)器宕機(jī),會話會丟失。
public class IpHashLoadBalance<N extends Node> extends BaseLoadBalance<N> implements LoadBalance<N> {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);}}

用戶 ID 請求方 IP 請求服務(wù)名稱,參數(shù)列表構(gòu)成的串
注意:因為 一致性哈希分區(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<>();("unchecked")protected N doSelect(List<N> nodes, String ip) {// 分片數(shù),這里設(shè)為節(jié)點(diǎn)數(shù)的 4 倍Integer replicaNum = nodes.size() * 4;// 獲取 nodes 原始的 hashcodeint 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)建新的 ConsistentHashSelectorselectors.put(ip, new ConsistentHashSelector<>(nodes, identityHashCode, replicaNum));selector = (ConsistentHashSelector<N>) selectors.get(ip);}// 調(diào)用 ConsistentHashSelector 的 select 方法選擇 Nodereturn 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)為 100if (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 方法,// 尋找合適的 Nodereturn 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)賦值給 entryif (entry == null) {entry = virtualNodes.firstEntry();}// 返回 Nodereturn 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();}}
四、參考資料
END
猜你喜歡
評論
圖片
表情
