Dubbo剖析-叢集容錯
本篇主要對dubbo叢集容錯進行剖析,主要下面幾個模組
- cluster容錯方案
- Directory目錄服務
- route 路由解析
- loadBalance 軟負載均衡
一、呼叫鏈路
二、容錯方案
叢集模式的配置
<dubbo:service cluster="failsafe" /> 服務提供方
<dubbo:reference cluster="failsafe" /> 服務消費方
叢集容錯實現
介面類 com.alibaba.dubbo.rpc.cluster.Cluster
1.AvailableCluster
獲取可用的呼叫。遍歷所有Invokers判斷Invoker.isAvalible,只要一個有為true直接呼叫返回,不管成不成功
2.BroadcastCluster
廣播呼叫。遍歷所有Invokers, 逐個呼叫每個呼叫catch住異常不影響其他invoker呼叫
3.FailbackCluster
失敗自動恢復, 對於invoker呼叫失敗, 後臺記錄失敗請求,任務定時重發, 通常用於通知
//FailbackClusterInvoker
//記錄失敗的呼叫
private final ConcurrentMap<Invocation, AbstractClusterInvoker<?>> failed = new ConcurrentHashMap<Invocation, AbstractClusterInvoker<?>>();
protected Result doInvoke(Invocation invocation, List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
try {
checkInvokers(invokers, invocation);
Invoker<T> invoker = select(loadbalance, invocation, invokers, null);
return invoker.invoke(invocation);
} catch (Throwable e) {
//失敗後呼叫 addFailed
addFailed(invocation, this);
return new RpcResult(); // ignore
}
}
private void addFailed(Invocation invocation, AbstractClusterInvoker<?> router) {
if (retryFuture == null) {
synchronized (this) {
if (retryFuture == null) {
retryFuture = scheduledExecutorService.scheduleWithFixedDelay(new Runnable() {
public void run() {
// 收集統計資訊
try {
retryFailed();
} catch (Throwable t) { // 防禦性容錯
logger.error("Unexpected error occur at collect statistic", t);
}
}
}, RETRY_FAILED_PERIOD, RETRY_FAILED_PERIOD, TimeUnit.MILLISECONDS);
}
}
}
failed.put(invocation, router);
}
//失敗的進行重試,重試成功後移除當前map
void retryFailed() {
if (failed.size() == 0) {
return;
}
for (Map.Entry<Invocation, AbstractClusterInvoker<?>> entry : new HashMap<Invocation, AbstractClusterInvoker<?>>(
failed).entrySet()) {
Invocation invocation = entry.getKey();
Invoker<?> invoker = entry.getValue();
try {
invoker.invoke(invocation);
failed.remove(invocation);
} catch (Throwable e) {
logger.error("Failed retry to invoke method " + invocation.getMethodName() + ", waiting again.", e);
}
}
}
4.FailfastCluster
快速失敗,只發起一次呼叫,失敗立即保錯,通常用於非冪等性操作
5.FailoverCluster default
失敗轉移,當出現失敗,重試其它伺服器,通常用於讀操作,但重試會帶來更長延遲
(1) 目錄服務directory.list(invocation) 列出方法的所有可呼叫服務
獲取重試次數,預設重試兩次
int len = getUrl().getMethodParameter(invocation.getMethodName(), Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1;
(2) 根據LoadBalance負載策略選擇一個Invoker
(3) 執行invoker.invoke(invocation)呼叫
(4) 呼叫成功返回
呼叫失敗小於重試次數,重新執行從3)步驟開始執行,呼叫次數大於等於重試次數丟擲呼叫失敗異常
6.FailsafeCluster
失敗安全,出現異常時,直接忽略,通常用於寫入審計日誌等操作。
7.ForkingCluster
並行呼叫,只要一個成功即返回,通常用於實時性要求較高的操作,但需要浪費更多服務資源。
注:
還有 MergeableCluster 和 MockClusterWrapper策略,但是個人沒有用過所以就不說了
三、Directory目錄服務
1. StaticDirectory
靜態目錄服務, 它的所有Invoker通過建構函式傳入, 服務消費方引用服務的時候, 服務對多註冊中心的引用,將Invokers集合直接傳入 StaticDirectory構造器
public StaticDirectory(URL url, List<Invoker<T>> invokers, List<Router> routers) {
super(url == null && invokers != null && invokers.size() > 0 ? invokers.get(0).getUrl() : url, routers);
if (invokers == null || invokers.size() == 0)
throw new IllegalArgumentException("invokers == null");
this.invokers = invokers;
}
StaticDirectory的list方法直接返回所有invoker集合
@Override
protected List<Invoker<T>> doList(Invocation invocation) throws RpcException {
return invokers;
}
2. RegistryDirectory
註冊目錄服務, 它的Invoker集合是從註冊中心獲取的, 它實現了NotifyListener介面實現了回撥介面notify(List<Url>)。
比如消費方要呼叫某遠端服務,會向註冊中心訂閱這個服務的所有服務提供方,訂閱時和服務提供方資料有變動時回撥消費方的NotifyListener服務的notify方法NotifyListener.notify(List<Url>) 回撥介面傳入所有服務的提供方的url地址然後將urls轉化為invokers, 也就是refer應用遠端服務到此時引用某個遠端服務的RegistryDirectory中有對這個遠端服務呼叫的所有invokers。
RegistryDirectory.list(invocation)就是根據服務呼叫方法獲取所有的遠端服務引用的invoker執行物件
四、服務路由
dubbo路由功能貌似用的不多,目的主要是對已註冊的服務進行過濾,比如只能呼叫某些配置的服務,或者禁用某些服務。
1. ConditionRouter條件路由
dubbo-admin 後臺進行配置。
路由程式碼入口
public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation)
throws RpcException {
if (invokers == null || invokers.size() == 0) {
return invokers;
}
try {
if (!matchWhen(url, invocation)) {
return invokers;
}
List<Invoker<T>> result = new ArrayList<Invoker<T>>();
if (thenCondition == null) {
logger.warn("The current consumer in the service blacklist. consumer: " + NetUtils.getLocalHost() + ", service: " + url.getServiceKey());
return result;
}
.............................
2. ScriptRouter指令碼路由
按照dubbo指令碼規則進行編寫,程式識別
五、軟負載均衡
1. RandomLoadBalance default
隨機,按權重設定隨機概率。權重default=100
在一個截面上碰撞的概率高,但呼叫量越大分佈越均勻,而且按概率使用權重後也比較均勻,有利於動態調整提供者權重。
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
int length = invokers.size(); // 總個數
int totalWeight = 0; // 總權重
boolean sameWeight = true; // 權重是否都一樣
for (int i = 0; i < length; i++) {
int weight = getWeight(invokers.get(i), invocation);
totalWeight += weight; // 累計總權重
if (sameWeight && i > 0
&& weight != getWeight(invokers.get(i - 1), invocation)) {
sameWeight = false; // 計算所有權重是否一樣
}
}
if (totalWeight > 0 && !sameWeight) {
// 如果權重不相同且權重大於0則按總權重數隨機
int offset = random.nextInt(totalWeight);
// 並確定隨機值落在哪個片斷上
for (int i = 0; i < length; i++) {
offset -= getWeight(invokers.get(i), invocation);
if (offset < 0) {
return invokers.get(i);
}
}
}
// 如果權重相同或權重為0則均等隨機
return invokers.get(random.nextInt(length));
}
演算法含義
如果所有的服務權重都一樣,就採用總服務數進行隨機。如果權重不一樣,則按照權重出隨機數,然後用隨機數減去服務權重,結果為負數則使用當前迴圈的服務。其實也就是一個概率性問題 每個服務的概率就是 當前服務的權重/ 總服務權重
2. RoundRobinLoadBalance
輪循,按公約後的權重設定輪循比率。
存在慢的提供者累積請求的問題,比如:第二臺機器很慢,但沒掛,當請求調到第二臺時就卡在那,久而久之,所有請求都卡在調到第二臺上。
該負載演算法維護著一個方法呼叫順序計數
private final ConcurrentMap<String, AtomicPositiveInteger> sequences = new ConcurrentHashMap<String, AtomicPositiveInteger>();
以方法名作為key
輪循分為 普通輪詢和加權輪詢。權重一樣時,採用取模運算普通輪詢,反之加權輪詢。
下面看下具體的實現
RoundRobinLoadBalance#doSelect
i.普通輪詢
AtomicPositiveInteger sequence = sequences.get(key);
if (sequence == null) {
sequences.putIfAbsent(key, new AtomicPositiveInteger());
sequence = sequences.get(key);
}
//獲取本次呼叫的伺服器序號,並+1
int currentSequence = sequence.getAndIncrement();
//當前序號和服務總數取模
return invokers.get(currentSequence % length);
ii.加權輪詢
下面貼下核心實現程式碼。注意幾個變數
weightSum
= 服務權重之和invokerToWeightMap
= 權重>0的 invoker map
int currentSequence = sequence.getAndIncrement();
if (maxWeight > 0 && minWeight < maxWeight) { // 權重不一樣
// mod < weightSum,下面for迴圈進行weight遞減,weight大的服務被呼叫的概率大
int mod = currentSequence % weightSum;
for (int i = 0; i < maxWeight; i++) {
for (Map.Entry<Invoker<T>, IntegerWrapper> each : invokerToWeightMap.entrySet()) {
final Invoker<T> k = each.getKey();
final IntegerWrapper v = each.getValue();
if (mod == 0 && v.getValue() > 0) {
return k;
}
if (v.getValue() > 0) {
v.decrement();
mod--;
}
}
}
}
可以舉個例子
兩個服務 A 和 B,權重分別是1和2
那麼 mod=[0,1,2],經過上面的邏輯,呼叫概率是 A B B A B B A B B ..... 顯然B的概率更大一些
3. LeastActiveLoadBalance
最少活躍呼叫數優先,活躍數指呼叫前後計數差。使慢的提供者收到更少請求,因為越慢的提供者的呼叫前後計數差會越大。
每個服務有一個活躍計數器,我們假如有A,B兩個提供者.計數均為0.當A提供者開始處理請求,該計數+1,此時A還沒處理完,當處理完後則計數-1.而B請求接收到請求處理得很快.B處理完後A還沒處理完,所以此時A,B的計數為1,0.那麼當有新的請求來的時候,就會選擇B提供者(B的活躍計數比A小).這就是文件說的,使慢的提供者收到更少請求。
int leastCount = 0; // 相同最小活躍數的個數
int[] leastIndexs = new int[length]; // 相同最小活躍數的下標
i.最小活躍服務個數=1, 該服務優先
if (leastCount == 1) {
// 如果只有一個最小則直接返回
return invokers.get(leastIndexs[0]);
}
ii.最小活躍服務個數>1, 最小活躍的服務按照權重隨機
if (!sameWeight && totalWeight > 0) {
// 如果權重不相同且權重大於0則按總權重數隨機
int offsetWeight = random.nextInt(totalWeight);
// 並確定隨機值落在哪個片斷上
for (int i = 0; i < leastCount; i++) {
int leastIndex = leastIndexs[i];
//權重越大,offsetWeight越快減成負數
offsetWeight -= getWeight(invokers.get(leastIndex), invocation);
if (offsetWeight <= 0)
return invokers.get(leastIndex);
}
}
iii. 最小活躍服務個數>1, 權重相同,服務個數隨機
// 如果權重相同或權重為0則均等隨機
return invokers.get(leastIndexs[random.nextInt(leastCount)]);
4. ConsistentHashLoadBalance
- 一致性 Hash,相同引數的請求總是發到同一提供者。
- 當某一臺提供者掛時,原本發往該提供者的請求,基於虛擬節點,平攤到其它提供者,不會引起劇烈變動。
- 演算法參見:http://en.wikipedia.org/wiki/Consistent_hashing
- 預設只對第一個引數 Hash,如果要修改,請配置
<dubbo:parameter key="hash.arguments" value="0,1" />
- 預設用 160 份虛擬節點,如果要修改,請配置
<dubbo:parameter key="hash.nodes" value="320" />
配置樣例
<dubbo:reference id="demoService" interface="com.youzan.dubbo.api.DemoService" loadbalance="consistenthash">
<!--預設只對第一個引數 Hash-->
<dubbo:parameter key="hash.arguments" value="0,1" />
<!--預設用 160 份虛擬節點,-->
<dubbo:parameter key="hash.nodes" value="160" />
</dubbo:reference>
演算法解析
ConsistentHashLoadBalance為使用該演算法的服務維護了一個selectors
,
key=invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName()
eg: com.youzan.dubbo.api.DemoService.sayHello
#com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance
private final ConcurrentMap<String, ConsistentHashSelector<?>> selectors = new ConcurrentHashMap<String, ConsistentHashSelector<?>>();
@SuppressWarnings("unchecked")
@Override
protected <T> Invoker<T> doSelect(List<Invoker<T>> invokers, URL url, Invocation invocation) {
String key = invokers.get(0).getUrl().getServiceKey() + "." + invocation.getMethodName();
int identityHashCode = System.identityHashCode(invokers);
//獲取該服務的ConsistentHashSelector,並跟進本次呼叫獲取對應invoker
ConsistentHashSelector<T> selector = (ConsistentHashSelector<T>) selectors.get(key);
if (selector == null || selector.getIdentityHashCode() != identityHashCode) {
selectors.put(key, new ConsistentHashSelector<T>(invokers, invocation.getMethodName(), identityHashCode));
selector = (ConsistentHashSelector<T>) selectors.get(key);
}
return selector.select(invocation);
}
ConsistentHashSelector作為ConsistentHashLoadBalance的內部類, 就是具體的一致性hash實現。
- ConsistentHashSelector內部元素
#com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance.ConsistentHashSelector
//該服務的所有hash節點
private final TreeMap<Long, Invoker<T>> virtualInvokers;
//虛擬節點數量
private final int replicaNumber;
//該服務的唯一hashcode,通過System.identityHashCode(invokers)獲取
private final int identityHashCode;
- 如何構建該服務的虛擬節點?
public ConsistentHashSelector(List<Invoker<T>> invokers, String methodName, int identityHashCode) {
// 建立TreeMap 來儲存結點
this.virtualInvokers = new TreeMap<Long, Invoker<T>>();
// 生成呼叫結點HashCode
this.identityHashCode = System.identityHashCode(invokers);
// 獲取Url
//dubbo://192.168.0.4:20880/com.youzan.dubbo.api.DemoService?anyhost=true&application=consumer-of-helloworld-app&check=false&class=com.youzan.dubbo.provider.DemoServiceImpl&dubbo=2.5.4&generic=false&hash.arguments=0,1&hash.nodes=160&interface=com.youzan.dubbo.api.DemoService&loadbalance=consistenthash&methods=sayHello&pid=32710&side=consumer×tamp=1527383363936
URL url = invokers.get(0).getUrl();
// 獲取所配置的結點數,如沒有設定則使用預設值160
this.replicaNumber = url.getMethodParameter(methodName, "hash.nodes", 160);
// 獲取需要進行hash的引數陣列索引,預設對第一個引數進行hash
String[] index = Constants.COMMA_SPLIT_PATTERN.split(url.getMethodParameter(methodName, "hash.arguments", "0"));
argumentIndex = new int[index.length];
for (int i = 0; i < index.length; i ++) {
argumentIndex[i] = Integer.parseInt(index[i]);
}
// 建立虛擬結點
// 對每個invoker生成replicaNumber個虛擬結點,並存放於TreeMap中
for (Invoker<T> invoker : invokers) {
for (int i = 0; i < replicaNumber / 4; i++) {
// 根據md5演算法為每4個結點生成一個訊息摘要,摘要長為16位元組128位。
byte[] digest = md5(invoker.getUrl().toFullString() + i);
// 隨後將128位分為4部分,0-31,32-63,64-95,95-128,並生成4個32位數,存於long中,long的高32位都為0
// 並作為虛擬結點的key。
for (int h = 0; h < 4; h++) {
long m = hash(digest, h);
virtualInvokers.put(m, invoker);
}
}
}
}
程式碼如果看的不是很懂,也不用去深究了(我就沒看懂,瞻仰了網上大神的文章貼了帖註釋),大家可以就粗略的認為,這段程式碼就是儘可能的構建出雜湊均勻的服務hash表。
- 如何從virtualInvokers選取本次呼叫的invoker?
// 選擇invoker
public Invoker<T> select(Invocation invocation) {
// 根據呼叫引數來生成Key
String key = toKey(invocation.getArguments());
// 根據這個引數生成訊息摘要
byte[] digest = md5(key);
//呼叫hash(digest, 0),將訊息摘要轉換為hashCode,這裡僅取0-31位來生成HashCode
//呼叫sekectForKey方法選擇結點。
Invoker<T> invoker = sekectForKey(hash(digest, 0));
return invoker;
}
private String toKey(Object[] args) {
StringBuilder buf = new StringBuilder();
// 由於hash.arguments沒有進行配置,因為只取方法的第1個引數作為key
for (int i : argumentIndex) {
if (i >= 0 && i < args.length) {
buf.append(args[i]);
}
}
return buf.toString();
}
//根據hashCode選擇結點
private Invoker<T> sekectForKey(long hash) {
Invoker<T> invoker;
Long key = hash;
// 若HashCode直接與某個虛擬結點的key一樣,則直接返回該結點
if (!virtualInvokers.containsKey(key)) {
// 若不一致,找到一個比傳入的key大的第一個結點。
SortedMap<Long, Invoker<T>> tailMap = virtualInvokers.tailMap(key);
// 若不存在,那麼選擇treeMap中第一個結點
// 使用TreeMap的firstKey方法,來選擇最小上界。
if (tailMap.isEmpty()) {
key = virtualInvokers.firstKey();
} else {
// 若存在則返回
key = tailMap.firstKey();
}
}
invoker = virtualInvokers.get(key);
return invoker;
}
- 一致性hash環是什麼東東?和上面的演算法什麼關係?
ConsistentHashSelector.virtualInvokers
這個東西就是我們的服務hash節點,單純的從資料結構上的確看不到什麼環狀的存在,可以先示意下,當前的資料結構
我們的服務節點只是一個普通的 map資料儲存而已,如何形成環呢?其實所謂的環只是邏輯上的展現,ConsistentHashSelector.sekectForKey()
方法裡通過 TreeMap.tailMap()、TreeMap.tailMap().firstKey、TreeMap.tailMap().firstKey() 結合case實現了環狀邏輯。下面我們畫圖說話。
第一步原始資料結構,我們按照hash從小到大排列
A,B,C表示我們提供的服務,改示意圖假設服務節點雜湊均勻
第二步選擇服務節點
i. 假設本地呼叫得到的key=2120, 程式碼邏輯(指ConsistentHashSelector.sekectForKey
)走到tailMap.firstKey()
那麼讀取到
3986
A服務
ii.假設本地呼叫得到的key=9991, tailMap為空,邏輯走到 virtualInvokers.firstKey()
回到起點
讀取到 1579 A服務
上述兩部情況基本已經能夠描述清楚節點的選擇邏輯,至於hash直接命中,那麼讀取對應的服務即可,無需多講。
最後環狀形成
上面兩部的介紹已經描述hash演算法,那麼我們所謂的環狀是怎麼一回事呢?其實也就是為了方便更好的理解這個邏輯,我們將線性的hash排列作為環狀,然後hash的選擇按照順時針方向選擇節點(等價於上面hash比較大小)
節點選擇演算法與上面等價,本圖主要用來示意,理想的hash環hash差距應該是等差,均勻的排列。
參考:
https://blog.csdn.net/column/details/learningdubbo.html?&page=1
https://blog.csdn.net/revivedsun/article/details/71022871
https://www.jianshu.com/p/53feb7f5f5d9
相關文章
- Dubbo示例——叢集容錯
- Dubbo原始碼分析-叢集容錯之Router原始碼
- Dubbo原始碼分析(八)叢集容錯機制原始碼
- dubbo原始碼解析-叢集容錯架構設計原始碼架構
- Dubbo原始碼分析(四)Dubbo呼叫鏈-消費端(叢集容錯機制)原始碼
- Dubbo學習筆記(四)叢集容錯與負載均衡筆記負載
- Java程式設計解密-Dubbo負載均衡與叢集容錯機制Java程式設計解密負載
- dubbo&nacos叢集配置
- Dubbo+Zookeeper叢集案例
- dubbo叢集和負載均衡負載
- dubbo原始碼分析之叢集Cluster原始碼
- Redis—叢集擴縮容Redis
- Dubbo原始碼解析之服務叢集原始碼
- hdfs叢集的擴容和縮容
- redis叢集報錯Redis
- 短影片app開發,叢集容錯策略的程式碼分析APP
- Openshif對叢集的擴容與縮容
- Redis Cluster 叢集搭建與擴容、縮容Redis
- dubbo容錯機制和負載均衡負載
- Kubernetes叢集排程器原理剖析及思考
- RabbitMQ叢集重啟報錯MQ
- 基於Ubuntu部署企業級kubernetes叢集---k8s叢集容部署UbuntuK8S
- Airbnb的動態 Kubernetes 叢集擴縮容AI
- 三 GBase 8a MPP Cluster叢集擴容
- 四 GBase 8a MPP Cluster叢集縮容
- 深入剖析Redis系列(二) - Redis哨兵模式與高可用叢集Redis模式
- Redis大叢集擴容效能優化實踐Redis優化
- Zookeeper叢集 + Kafka叢集Kafka
- 深入剖析Redis系列(三) - Redis叢集模式搭建與原理詳解Redis模式
- 用隧道協議實現不同dubbo叢集間的透明通訊協議
- 搭建zookeeper叢集(偽叢集)
- Redis大叢集擴容效能最佳化實踐Redis
- redis主從叢集搭建及容災部署(哨兵sentinel)Redis
- Dubbo剖析-增強SPI的實現
- zookeeper叢集及kafka叢集搭建Kafka
- 阿里雲 ACK One 多叢集管理全面升級:多叢集服務、多叢集監控、兩地三中心應用容災阿里
- 乾貨分享|GBase 8a叢集雙活容災方案
- Redis系列:搭建Redis叢集(叢集模式)Redis模式