負載均衡簡介:👇
負載均衡改善了跨多個計算資源(例如計算機,計算機集羣,網絡鏈接,中央處理單元或磁盤驅動的的工作負載分佈。
負載平衡旨在優化資源使用,最大化吞吐量,最小化響應時間,並避免任何單個資源的過載。
使用具有負載平衡而不是單個組件的多個組件可以通過冗餘提高可靠性和可用性。負載平衡通常涉及專用軟件或硬件。
簡單解釋:👇
這個概念如何理解呢?通俗點來說假如一個請求從客戶端發起,比如(查詢訂單列表),要選擇服務器進行處理,但是我們的集羣環境提供了5個服務器A\B\C\D\E,每個服務器都有處理這個請求的能力,此
時客戶端就必須選擇一個服務器來進行處理(不存在先選擇A,處理一會又選擇C,又跳到D).說白了就是一個選擇的問題。當請求多了的話,就要考慮各服務器的負載,一共5個服務器,不可能每次都讓一個服
務器都來處理吧,比如把讓其他服務器來分壓。這就是負載均衡的優點:避免單個服務器響應同一請求,容易造成服務器宕機、崩潰等問題。
dubbo 的 loadBalance 接口:👇
loadBalance:
dubbo 的負載均衡策略,主體向外暴露出來是一個接口,名字叫做 loadBlace,位於com.alibaba.dubbo.rpc.cluster包下,很明顯根據包名就可以看出它是用來管理集羣的:
這個接口就一個方法,select方法的作用就是從衆多的調用的List選擇出一個調用者,Invoker可以理解爲客戶端的調用者,dubbo專門封裝一個類來表示,URL就是調用者發起的URL請求鏈接,
從這個URL中可以獲取很多請求的具體信息,Invocation表示的是調用的具體過程,dubbo用這個類模擬調用具體細節過程:
package org.apache.dubbo.rpc.cluster; @SPI("random") public interface LoadBalance { @Adaptive({"loadbalance"}) <T> Invoker<T> select(List<Invoker<T>> var1, URL var2, Invocation var3) throws RpcException; }
由 @SPI 註解可以看到,dubbo默認的負載均衡策略是隨機調用法。
AbstractLoadBlance:
該接口在下面的子類都會對其進行實現。接口下是一個抽象類 AbstractLoadBlance
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.apache.dubbo.rpc.cluster.loadbalance; import java.util.List; import org.apache.dubbo.common.URL; import org.apache.dubbo.common.utils.CollectionUtils; import org.apache.dubbo.rpc.Invocation; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.cluster.LoadBalance; public abstract class AbstractLoadBalance implements LoadBalance { public AbstractLoadBalance() { } static int calculateWarmupWeight(int uptime, int warmup, int weight) { int ww = (int)((float)uptime / ((float)warmup / (float)weight)); return ww < 1 ? 1 : Math.min(ww, weight); } public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) { if (CollectionUtils.isEmpty(invokers)) { return null; } else { return invokers.size() == 1 ? (Invoker)invokers.get(0) : this.doSelect(invokers, url, invocation); } } protected abstract <T> Invoker<T> doSelect(List<Invoker<T>> var1, URL var2, Invocation var3); int getWeight(Invoker<?> invoker, Invocation invocation) { int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), "weight", 100); if (weight > 0) { long timestamp = invoker.getUrl().getParameter("timestamp", 0L); if (timestamp > 0L) { long uptime = System.currentTimeMillis() - timestamp; if (uptime < 0L) { return 1; } int warmup = invoker.getUrl().getParameter("warmup", 600000); if (uptime > 0L && uptime < (long)warmup) { weight = calculateWarmupWeight((int)uptime, warmup, weight); } } } return Math.max(weight, 0); } }
AbstractLoadBlance 抽象類繼承自 LoadBalance,其中有個static方法表明它在類加載的時候就會運行。
它表示的含義是計算預熱加載權重,參數是uptime,這裏可以理解爲服務啓動的時間,warmup就是預熱時間,weight是權重的值。
下面會對比進行詳細解釋:
1、select 方法:
抽象類方法中有個有方法體的方法 select,先判斷調用者組成的List是否爲null,如果是null就返回null。
再判斷調用者的大小,如果只有一個就返回那個唯一的調用者(試想,如果服務調用另一個服務時,當服務的提供者機器只有一個,那麼就可以返回那一個,因爲沒有選擇了!) 如果這些都不成立,就繼續往下走,走doSelect方法:
public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) { if (CollectionUtils.isEmpty(invokers)) { return null; } else { return invokers.size() == 1 ? (Invoker)invokers.get(0) : this.doSelect(invokers, url, invocation); } }
2、doSelect 方法:
該方法是抽象的,交給具體的子類去實現,由此也可以思考出一個問題就是:dubbo爲什麼要將一個接口首先做出一個實現抽象類,再由不同的子類去實現?原因是抽象類中的非抽象方法,再子類中都是必須要實現的,而他們子類的不同點就是具體做出選擇的策略不同,將公共的邏輯提取出來放在抽象類裏,子類不用寫多餘的代碼,只用維護和實現最終要的自己的邏輯。
protected abstract <T> Invoker<T> doSelect(List<Invoker<T>> var1, URL var2, Invocation var3);
3、getWeight 方法:
顧名思義,這個方法的含義就是獲取權重,首先通過URL(URL爲dubbo封裝的一個實體)獲取基本的權重,如果權重大於0,會獲取服務啓動時間,再用當前的時間-啓動時間就是服務到目前爲止運行了多久,因此這個upTime就可以理解爲服務啓動時間,再獲取配置的預熱時間,如果啓動時間小於預熱時間,就會再次調用獲取權重。這個預熱的方法其實dubbo針對JVM做出的一個很契合的優化,因爲JVM從啓動到起來都運行到最佳狀態是需要一點時間的,這個時間叫做warmup,而dubbo就會對這個時間進行設定,然後等到服務運行時間和warmup相等時再計算權重,這樣就可以保障服務的最佳運行狀態!
int getWeight(Invoker<?> invoker, Invocation invocation) { int weight = invoker.getUrl().getMethodParameter(invocation.getMethodName(), "weight", 100); if (weight > 0) { long timestamp = invoker.getUrl().getParameter("timestamp", 0L); if (timestamp > 0L) { long uptime = System.currentTimeMillis() - timestamp; if (uptime < 0L) { return 1; } int warmup = invoker.getUrl().getParameter("warmup", 600000); if (uptime > 0L && uptime < (long)warmup) { weight = calculateWarmupWeight((int)uptime, warmup, weight); } } } return Math.max(weight, 0); }
dubbo 的幾種負載均衡策略:👇
整體架構圖:
可以看出抽象的負載均衡下的類分爲4個,這4個類表示了4種負載均衡策略,分別是一致性Hash均衡算法、隨機調用法、輪詢法、最少活動調用法。
RandomLoadBalance:隨機調用
隨機調用負載均衡,該類實現了抽象的AbstractLoadBalance接口,重寫了doSelect方法,看方法的細節就是首先遍歷每個提供服務的機器,獲取每個服務的權重,然後累加權重值,判斷每個服務的提供者權重是否相同,如果每個調用者的權重不相同,並且每個權重大於0,那麼就會根據權重的總值生成一個隨機數,再用這個隨機數,根據調用者的數量每次減去調用者的權重,直到計算出當前的服務提供者隨機數小於0,就選擇那個提供者!另外,如果每個機器的權重的都相同,那麼權重就不會參與計算,直接選擇隨機算法生成的某一個選擇,完全隨機。
可以看出,隨機調用法
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.apache.dubbo.rpc.cluster.loadbalance; import java.util.List; import java.util.concurrent.ThreadLocalRandom; import org.apache.dubbo.common.URL; import org.apache.dubbo.rpc.Invocation; import org.apache.dubbo.rpc.Invoker; public class RandomLoadBalance extends AbstractLoadBalance { public static final String NAME = "random"; public RandomLoadBalance() { } protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { int length = invokers.size(); boolean sameWeight = true; int[] weights = new int[length]; int firstWeight = this.getWeight((Invoker)invokers.get(0), invocation); weights[0] = firstWeight; int totalWeight = firstWeight; int offset; int i; for(offset = 1; offset < length; ++offset) { i = this.getWeight((Invoker)invokers.get(offset), invocation); weights[offset] = i; totalWeight += i; if (sameWeight && i != firstWeight) { sameWeight = false; } } if (totalWeight > 0 && !sameWeight) { offset = ThreadLocalRandom.current().nextInt(totalWeight); for(i = 0; i < length; ++i) { offset -= weights[i]; if (offset < 0) { return (Invoker)invokers.get(i); } } } return (Invoker)invokers.get(ThreadLocalRandom.current().nextInt(length)); } }
RoundRobinLoadBlance:輪詢調用
輪詢調用,輪詢調用的過程主要是維護了局部變量的一個LinkdesHashMap(有順序的Map)去存儲調用者和權重值的對應關係,然後遍歷每個調用者,把調用者和當前大於0的權重值放進去,再累加權重值。還有一個全局變量的map,找到第一個服務調用者,首先是找到每個服務的key值和method,這裏可以理解爲標識第一個調用者的唯一key,然後再給它對應的值保證原子性的+1(AtomicPositiveInteger是原子的),再對這個值取模總權重,再每次對其權重值-1,知道它取模與總權重值等於0就選擇該調用者,可以稱之爲"降權取模"(只是一種的計算層面,而不是真正降權)。
總結:輪詢調用並不是簡單的一個接着一個依次調用,它是根據權重的值進行循環的。
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.apache.dubbo.rpc.cluster.loadbalance; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import org.apache.dubbo.common.URL; import org.apache.dubbo.rpc.Invocation; import org.apache.dubbo.rpc.Invoker; public class RoundRobinLoadBalance extends AbstractLoadBalance { public static final String NAME = "roundrobin"; private static final int RECYCLE_PERIOD = 60000; private ConcurrentMap<String, ConcurrentMap<String, RoundRobinLoadBalance.WeightedRoundRobin>> methodWeightMap = new ConcurrentHashMap(); private AtomicBoolean updateLock = new AtomicBoolean(); public RoundRobinLoadBalance() { } protected <T> Collection<String> getInvokerAddrList(List<Invoker<T>> invokers, Invocation invocation) { String key = ((Invoker)invokers.get(0)).getUrl().getServiceKey() + "." + invocation.getMethodName(); Map<String, RoundRobinLoadBalance.WeightedRoundRobin> map = (Map)this.methodWeightMap.get(key); return map != null ? map.keySet() : null; } protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { String key = ((Invoker)invokers.get(0)).getUrl().getServiceKey() + "." + invocation.getMethodName(); ConcurrentMap<String, RoundRobinLoadBalance.WeightedRoundRobin> map = (ConcurrentMap)this.methodWeightMap.get(key); if (map == null) { this.methodWeightMap.putIfAbsent(key, new ConcurrentHashMap()); map = (ConcurrentMap)this.methodWeightMap.get(key); } int totalWeight = 0; long maxCurrent = -9223372036854775808L; long now = System.currentTimeMillis(); Invoker<T> selectedInvoker = null; RoundRobinLoadBalance.WeightedRoundRobin selectedWRR = null; int weight; for(Iterator var13 = invokers.iterator(); var13.hasNext(); totalWeight += weight) { Invoker<T> invoker = (Invoker)var13.next(); String identifyString = invoker.getUrl().toIdentityString(); RoundRobinLoadBalance.WeightedRoundRobin weightedRoundRobin = (RoundRobinLoadBalance.WeightedRoundRobin)map.get(identifyString); weight = this.getWeight(invoker, invocation); if (weightedRoundRobin == null) { weightedRoundRobin = new RoundRobinLoadBalance.WeightedRoundRobin(); weightedRoundRobin.setWeight(weight); map.putIfAbsent(identifyString, weightedRoundRobin); } if (weight != weightedRoundRobin.getWeight()) { weightedRoundRobin.setWeight(weight); } long cur = weightedRoundRobin.increaseCurrent(); weightedRoundRobin.setLastUpdate(now); if (cur > maxCurrent) { maxCurrent = cur; selectedInvoker = invoker; selectedWRR = weightedRoundRobin; } } if (!this.updateLock.get() && invokers.size() != map.size() && this.updateLock.compareAndSet(false, true)) { try { ConcurrentMap<String, RoundRobinLoadBalance.WeightedRoundRobin> newMap = new ConcurrentHashMap(map); newMap.entrySet().removeIf((item) -> { return now - ((RoundRobinLoadBalance.WeightedRoundRobin)item.getValue()).getLastUpdate() > 60000L; }); this.methodWeightMap.put(key, newMap); } finally { this.updateLock.set(false); } } if (selectedInvoker != null) { selectedWRR.sel(totalWeight); return selectedInvoker; } else { return (Invoker)invokers.get(0); } } protected static class WeightedRoundRobin { private int weight; private AtomicLong current = new AtomicLong(0L); private long lastUpdate; protected WeightedRoundRobin() { } public int getWeight() { return this.weight; } public void setWeight(int weight) { this.weight = weight; this.current.set(0L); } public long increaseCurrent() { return this.current.addAndGet((long)this.weight); } public void sel(int total) { this.current.addAndGet((long)(-1 * total)); } public long getLastUpdate() { return this.lastUpdate; } public void setLastUpdate(long lastUpdate) { this.lastUpdate = lastUpdate; } } }
LeastActiveLoadBlance:最少活躍數調用
最少活躍數調用法:這個方法的主要作用根據服務的提供者的運行狀態去選擇服務器,主要的思路就是遍歷每個調用者,然後獲取每個服務器的運行狀態,如果當前運行的運行狀態小於最小的狀態-1,把它保存在leastIndexs中的第一個位置,並且認定所有的調用者權重都相同,然後直接返回那個調用者(這裏的邏輯是:找到最少活躍數(在代碼層反應就是:active的值))。如果計算出的權重值和最少的權重值相同,那麼把它保存在leastIndexs數組裏面,累加權重值,如果當前的權重值不等於初始值firstWeight,那麼就認定不是所有的調用者的權重不同。然後再遍歷lestIndexs,取權重累加值的隨機數生成權重偏移量,在累減它,到它小於0的時候返回那個調用者。如果這些都不符合,就從leastIndexs隨機選一個index,返回那個調用者!
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.apache.dubbo.rpc.cluster.loadbalance; import java.util.List; import java.util.concurrent.ThreadLocalRandom; import org.apache.dubbo.common.URL; import org.apache.dubbo.rpc.Invocation; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.RpcStatus; public class LeastActiveLoadBalance extends AbstractLoadBalance { public static final String NAME = "leastactive"; public LeastActiveLoadBalance() { } protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { int length = invokers.size(); int leastActive = -1; int leastCount = 0; int[] leastIndexes = new int[length]; int[] weights = new int[length]; int totalWeight = 0; int firstWeight = 0; boolean sameWeight = true; int offsetWeight; int leastIndex; for(offsetWeight = 0; offsetWeight < length; ++offsetWeight) { Invoker<T> invoker = (Invoker)invokers.get(offsetWeight); leastIndex = RpcStatus.getStatus(invoker.getUrl(), invocation.getMethodName()).getActive(); int afterWarmup = this.getWeight(invoker, invocation); weights[offsetWeight] = afterWarmup; if (leastActive != -1 && leastIndex >= leastActive) { if (leastIndex == leastActive) { leastIndexes[leastCount++] = offsetWeight; totalWeight += afterWarmup; if (sameWeight && offsetWeight > 0 && afterWarmup != firstWeight) { sameWeight = false; } } } else { leastActive = leastIndex; leastCount = 1; leastIndexes[0] = offsetWeight; totalWeight = afterWarmup; firstWeight = afterWarmup; sameWeight = true; } } if (leastCount == 1) { return (Invoker)invokers.get(leastIndexes[0]); } else { if (!sameWeight && totalWeight > 0) { offsetWeight = ThreadLocalRandom.current().nextInt(totalWeight); for(int i = 0; i < leastCount; ++i) { leastIndex = leastIndexes[i]; offsetWeight -= weights[leastIndex]; if (offsetWeight < 0) { return (Invoker)invokers.get(leastIndex); } } } return (Invoker)invokers.get(leastIndexes[ThreadLocalRandom.current().nextInt(leastCount)]); } } }
ConsistentHashLoadBalance:一致性Hash算法
一致性Hash算法,doSelect方法進行選擇。一致性Hash負載均衡涉及到兩個主要的配置參數爲hash.arguments與hash.nodes
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.apache.dubbo.rpc.cluster.loadbalance; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Iterator; import java.util.List; import java.util.TreeMap; import java.util.Map.Entry; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.apache.dubbo.common.URL; import org.apache.dubbo.common.constants.CommonConstants; import org.apache.dubbo.rpc.Invocation; import org.apache.dubbo.rpc.Invoker; import org.apache.dubbo.rpc.support.RpcUtils; public class ConsistentHashLoadBalance extends AbstractLoadBalance { public static final String NAME = "consistenthash"; public static final String HASH_NODES = "hash.nodes"; public static final String HASH_ARGUMENTS = "hash.arguments"; private final ConcurrentMap<String, ConsistentHashLoadBalance.ConsistentHashSelector<?>> selectors = new ConcurrentHashMap(); public ConsistentHashLoadBalance() { } protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) { String methodName = RpcUtils.getMethodName(invocation); String key = ((Invoker)invokers.get(0)).getUrl().getServiceKey() + "." + methodName; int identityHashCode = System.identityHashCode(invokers); ConsistentHashLoadBalance.ConsistentHashSelector<T> selector = (ConsistentHashLoadBalance.ConsistentHashSelector)this.selectors.get(key); if (selector == null || selector.identityHashCode != identityHashCode) { this.selectors.put(key, new ConsistentHashLoadBalance.ConsistentHashSelector(invokers, methodName, identityHashCode)); selector = (ConsistentHashLoadBalance.ConsistentHashSelector)this.selectors.get(key); } return selector.select(invocation); } private static final class ConsistentHashSelector<T> { private final TreeMap<Long, Invoker<T>> virtualInvokers = new TreeMap(); private final int replicaNumber; private final int identityHashCode; private final int[] argumentIndex; ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) { this.identityHashCode = identityHashCode; URL url = ((Invoker)invokers.get(0)).getUrl(); this.replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160); String[] index = CommonConstants.COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, "hash.arguments", "0")); this.argumentIndex = new int[index.length]; for(int i = 0; i < index.length; ++i) { this.argumentIndex[i] = Integer.parseInt(index[i]); } Iterator var14 = invokers.iterator(); while(var14.hasNext()) { Invoker<T> invoker = (Invoker)var14.next(); String address = invoker.getUrl().getAddress(); for(int i = 0; i < this.replicaNumber / 4; ++i) { byte[] digest = this.md5(address + i); for(int h = 0; h < 4; ++h) { long m = this.hash(digest, h); this.virtualInvokers.put(m, invoker); } } } } public Invoker<T> select(Invocation invocation) { String key = this.toKey(invocation.getArguments()); byte[] digest = this.md5(key); return this.selectForKey(this.hash(digest, 0)); } private String toKey(Object[] args) { StringBuilder buf = new StringBuilder(); int[] var3 = this.argumentIndex; int var4 = var3.length; for(int var5 = 0; var5 < var4; ++var5) { int i = var3[var5]; if (i >= 0 && i < args.length) { buf.append(args[i]); } } return buf.toString(); } private Invoker<T> selectForKey(long hash) { Entry<Long, Invoker<T>> entry = this.virtualInvokers.ceilingEntry(hash); if (entry == null) { entry = this.virtualInvokers.firstEntry(); } return (Invoker)entry.getValue(); } private long hash(byte[] digest, int number) { return ((long)(digest[3 + number * 4] & 255) << 24 | (long)(digest[2 + number * 4] & 255) << 16 | (long)(digest[1 + number * 4] & 255) << 8 | (long)(digest[number * 4] & 255)) & 4294967295L; } private byte[] md5(String value) { MessageDigest md5; try { md5 = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException var4) { throw new IllegalStateException(var4.getMessage(), var4); } md5.reset(); byte[] bytes = value.getBytes(StandardCharsets.UTF_8); md5.update(bytes); return md5.digest(); } } }
如何改變dubbo的負載均衡策略?👇
如果是springboot項目,直接註解在@Reference中引用,然後註明loadblance="xx".其中xx爲每個實現類中的name的值
/** * loadbalance:輪詢策略 * retries:重試次數 */ @Resource @Reference(loadbalance = "roundrobin",retries = 2) private Bank1Service bank1Service;
xml 配置的方式:
<dubbo:service ref="bank1Service" loadbalance="roundrobin"
interface="com.mlq.integration.zk.api.service.Bank1Service"/>
小結:👇
本篇博客講述了dubbo的負載均衡機制,其中可以看到除了一致性Hash算法,其它都是根據權重進行計算的,在實際的分佈式應用中,理解dubbo如何與zookeeper進行通信選擇,如何實現負載均衡,如何維護服務的高可用性,理解負載均衡對於微服務的重要意義,將對於我們學習分佈式的開發起着推波助瀾的作用。
引文:https://www.cnblogs.com/wyq178/p/9822731.html