Dubbo中的Cluster可以將多個服務提供方僞裝成一個提供方,具體也就是將Directory中的多個Invoker僞裝成一個Invoker,在僞裝的過程中包含了容錯的處理和負載均衡的處理。這篇文章介紹下集羣相關的東西,開始先對着文檔解釋下容錯模式,負載均衡等概念,然後解析下源碼的處理。
集羣的容錯模式
Failover Cluster
這是dubbo中默認的集羣容錯模式
- 失敗自動切換,當出現失敗,重試其它服務器。
- 通常用於讀操作,但重試會帶來更長延遲。
- 可通過retries=”2”來設置重試次數(不含第一次)。
Failfast Cluster
- 快速失敗,只發起一次調用,失敗立即報錯。
- 通常用於非冪等性的寫操作,比如新增記錄。
Failsafe Cluster
- 失敗安全,出現異常時,直接忽略。
- 通常用於寫入審計日誌等操作。
Failback Cluster
- 失敗自動恢復,後臺記錄失敗請求,定時重發。
- 通常用於消息通知操作。
Forking Cluster
- 並行調用多個服務器,只要一個成功即返回。
- 通常用於實時性要求較高的讀操作,但需要浪費更多服務資源。
- 可通過forks=”2”來設置最大並行數。
Broadcast Cluster
- 廣播調用所有提供者,逐個調用,任意一臺報錯則報錯。(2.1.0開始支持)
- 通常用於通知所有提供者更新緩存或日誌等本地資源信息。
負載均衡
dubbo默認的負載均衡策略是random,隨機調用。
Random LoadBalance
- 隨機,按權重設置隨機概率。
- 在一個截面上碰撞的概率高,但調用量越大分佈越均勻,而且按概率使用權重後也比較均勻,有利於動態調整提供者權重。
RoundRobin LoadBalance
- 輪循,按公約後的權重設置輪循比率。
- 存在慢的提供者累積請求問題,比如:第二臺機器很慢,但沒掛,當請求調到第二臺時就卡在那,久而久之,所有請求都卡在調到第二臺上。
LeastActive LoadBalance
- 最少活躍調用數,相同活躍數的隨機,活躍數指調用前後計數差。
- 使慢的提供者收到更少請求,因爲越慢的提供者的調用前後計數差會越大。
ConsistentHash LoadBalance
- 一致性Hash,相同參數的請求總是發到同一提供者。
- 當某一臺提供者掛時,原本發往該提供者的請求,基於虛擬節點,平攤到其它提供者,不會引起劇烈變動。
- 缺省只對第一個參數Hash。
- 缺省用160份虛擬節點。
集羣相關源碼解析
回想一下在服務消費者初始化的過程中,在引用遠程服務的那一步,也就是RegistryProtocol的refer方法中,調用了doRefer方法,doRefer方法中第一個參數就是cluster,我們就從這裏開始解析。RegistryProtocol的refer方法:
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
url = url.setProtocol(url.getParameter(Constants.REGISTRY_KEY, Constants.DEFAULT_REGISTRY)).removeParameter(Constants.REGISTRY_KEY);
//根據url獲取註冊中心實例
//這一步連接註冊中心,並把消費者註冊到註冊中心
Registry registry = registryFactory.getRegistry(url);
//對註冊中心服務的處理
if (RegistryService.class.equals(type)) {
return proxyFactory.getInvoker((T) registry, type, url);
}
//以下是我們自己定義的業務的服務處理
// group="a,b" or group="*"
Map<String, String> qs = StringUtils.parseQueryString(url.getParameterAndDecoded(Constants.REFER_KEY));
String group = qs.get(Constants.GROUP_KEY);
//服務需要合併不同實現
if (group != null && group.length() > 0 ) {
if ( ( Constants.COMMA_SPLIT_PATTERN.split( group ) ).length > 1
|| "*".equals( group ) ) {
return doRefer( getMergeableCluster(), registry, type, url );
}
}
//這裏參數cluster是集羣的適配類,代碼在下面
return doRefer(cluster, registry, type, url);
}
接着看doRefer,真正去做服務引用的方法:
private <T> Invoker<T> doRefer(Cluster cluster, Registry registry, Class<T> type, URL url) {
//Directory中是Invoker的集合,相當於一個List
//也就是說這裏面存放了多個Invoker,那麼我們該調用哪一個呢?
//該調用哪一個Invoker的工作就是Cluster來處理的
RegistryDirectory<T> directory = new RegistryDirectory<T>(type, url);
directory.setRegistry(registry);
directory.setProtocol(protocol);
URL subscribeUrl = new URL(Constants.CONSUMER_PROTOCOL, NetUtils.getLocalHost(), 0, type.getName(), directory.getUrl().getParameters());
if (! Constants.ANY_VALUE.equals(url.getServiceInterface())
&& url.getParameter(Constants.REGISTER_KEY, true)) {
//到註冊中心註冊服務 registry.register(subscribeUrl.addParameters(Constants.CATEGORY_KEY, Constants.CONSUMERS_CATEGORY,
Constants.CHECK_KEY, String.valueOf(false)));
}
//訂閱服務,註冊中心會推送服務消息給消費者,消費者會再次進行服務的引用。 directory.subscribe(subscribeUrl.addParameter(Constants.CATEGORY_KEY,
Constants.PROVIDERS_CATEGORY
+ "," + Constants.CONFIGURATORS_CATEGORY
+ "," + Constants.ROUTERS_CATEGORY));
//服務的引用和變更全部由Directory異步完成
//Directory中可能存在多個Invoker
//而Cluster會把多個Invoker僞裝成一個Invoker
//這一步就是做這個事情的
return cluster.join(directory);
}
集羣處理的入口
入口就是在doRefer的時候最後一步:cluster.join(directory);。
首先解釋下cluster,這個是根據dubbo的擴展機制生成的,在RegistryProtocol中有一個setCluster方法,根據擴展機制可以知道,這是注入Cluster的地方,代碼如下:
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Cluster$Adpative implements com.alibaba.dubbo.rpc.cluster.Cluster {
public com.alibaba.dubbo.rpc.Invoker join(com.alibaba.dubbo.rpc.cluster.Directory arg0) throws com.alibaba.dubbo.rpc.cluster.Directory {
if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.cluster.Directory argument == null");
if (arg0.getUrl() == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.cluster.Directory argument getUrl() == null");com.alibaba.dubbo.common.URL url = arg0.getUrl();
String extName = url.getParameter("cluster", "failover");
if(extName == null) throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.cluster.Cluster) name from url(" + url.toString() + ") use keys([cluster])");
com.alibaba.dubbo.rpc.cluster.Cluster extension = (com.alibaba.dubbo.rpc.cluster.Cluster)ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.cluster.Cluster.class).getExtension(extName);
return extension.join(arg0);
}
}
可以看到,如果我們沒有配置集羣策略的話,默認是用failover模式,在Cluster接口的註解上@SPI(FailoverCluster.NAME)也可以看到默認是failover。
繼續執行cluster.join方法,會首先進入MockClusterWrapper的join方法:
public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
//先執行FailoverCluster的join方法處理
//然後將Directory和返回的Invoker封裝成一個MockCluster
return new MockClusterInvoker<T>(directory,
this.cluster.join(directory));
}
看下Failover的join方法:
public <T> Invoker<T> join(Directory<T> directory) throws RpcException {
//直接返回一個FailoverClusterInvoker的實例
return new FailoverClusterInvoker<T>(directory);
}
到這裏就算把Invoker都封裝好了,返回的Invoker是一個MockClusterInvoker,MockClusterInvoker內部包含一個Directory和一個FailoverClusterInvoker。
Invoker都封裝好了之後,就是創建代理,然後使用代理調用我們的要調用的方法。
調用方法時集羣的處理
在進行具體方法調用的時候,代理中會invoker.invoke(),這裏Invoker就是我們上面封裝好的MockClusterInvoker,所以首先進入MockClusterInvoker的invoke方法:
Result result = null;
//我們沒配置mock,所以這裏爲false
//Mock通常用於服務降級
String value = directory.getUrl().getMethodParameter(invocation.getMethodName(), Constants.MOCK_KEY, Boolean.FALSE.toString()).trim();
//沒有使用mock
if (value.length() == 0 || value.equalsIgnoreCase("false")){
//這裏的invoker是FailoverClusterInvoker
result = this.invoker.invoke(invocation);
} else if (value.startsWith("force")) {
//mock=force:return+null
//表示消費方對方法的調用都直接返回null,不發起遠程調用
//可用於屏蔽不重要服務不可用的時候,對調用方的影響
//force:direct mock
result = doMockInvoke(invocation, null);
} else {
//mock=fail:return+null
//表示消費方對該服務的方法調用失敗後,再返回null,不拋異常
//可用於對不重要服務不穩定的時候,忽略對調用方的影響
//fail-mock
try {
result = this.invoker.invoke(invocation);
}catch (RpcException e) {
if (e.isBiz()) {
throw e;
} else {
result = doMockInvoke(invocation, e);
}
}
}
return result;
}
我們這裏麼有配置mock屬性。首先進入的是AbstractClusterInvoker的incoke方法:
public Result invoke(final Invocation invocation) throws RpcException {
//檢查是否已經被銷燬
checkWheatherDestoried();
//可以看到這裏該處理負載均衡的問題了
LoadBalance loadbalance;
//根據invocation中的信息從Directory中獲取Invoker列表
//這一步中會進行路由的處理
List<Invoker<T>> invokers = list(invocation);
if (invokers != null && invokers.size() > 0) {
//使用擴展機制,加載LoadBalance的實現類,默認使用的是random
//我們這裏得到的就是RandomLoadBalance
loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invokers.get(0).getUrl()
.getMethodParameter(invocation.getMethodName(),Constants.LOADBALANCE_KEY, Constants.DEFAULT_LOADBALANCE));
} else {
loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(Constants.DEFAULT_LOADBALANCE);
}
//異步操作默認添加invocation id
RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
//調用具體的實現類的doInvoke方法,這裏是FailoverClusterInvoker
return doInvoke(invocation, invokers, loadbalance);
}
看下FailoverClusterInvoker的invoke方法:
public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
//Invoker列表
List<Invoker<T>> copyinvokers = invokers;
//確認下Invoker列表不爲空
checkInvokers(copyinvokers, invocation);
//重試次數
int len = getUrl().getMethodParameter(invocation.getMethodName(), Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1;
if (len <= 0) {
len = 1;
}
// retry loop.
RpcException le = null; // last exception.
List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyinvokers.size()); // invoked invokers.
Set<String> providers = new HashSet<String>(len);
for (int i = 0; i < len; i++) {
//重試時,進行重新選擇,避免重試時invoker列表已發生變化.
//注意:如果列表發生了變化,那麼invoked判斷會失效,因爲invoker示例已經改變
if (i > 0) {
checkWheatherDestoried();
copyinvokers = list(invocation);
//重新檢查一下
checkInvokers(copyinvokers, invocation);
}
//使用loadBalance選擇一個Invoker返回
Invoker<T> invoker = select(loadbalance, invocation, copyinvokers, invoked);
invoked.add(invoker);
RpcContext.getContext().setInvokers((List)invoked);
try {
//使用選擇的結果Invoker進行調用,返回結果
Result result = invoker.invoke(invocation);
return result;
} catch (RpcException e) {。。。} finally {
providers.add(invoker.getUrl().getAddress());
}
}
throw new RpcException(。。。);
}
先看下使用loadbalance選擇invoker的select方法:
protected Invoker<T> select(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {
if (invokers == null || invokers.size() == 0)
return null;
String methodName = invocation == null ? "" : invocation.getMethodName();
//sticky,滯連接用於有狀態服務,儘可能讓客戶端總是向同一提供者發起調用,除非該提供者掛了,再連另一臺。
boolean sticky = invokers.get(0).getUrl().getMethodParameter(methodName,Constants.CLUSTER_STICKY_KEY, Constants.DEFAULT_CLUSTER_STICKY) ;
{
//ignore overloaded method
if ( stickyInvoker != null && !invokers.contains(stickyInvoker) ){
stickyInvoker = null;
}
//ignore cucurrent problem
if (sticky && stickyInvoker != null && (selected == null || !selected.contains(stickyInvoker))){
if (availablecheck && stickyInvoker.isAvailable()){
return stickyInvoker;
}
}
}
Invoker<T> invoker = doselect(loadbalance, invocation, invokers, selected);
if (sticky){
stickyInvoker = invoker;
}
return invoker;
}
doselect方法:
private Invoker<T> doselect(LoadBalance loadbalance, Invocation invocation, List<Invoker<T>> invokers, List<Invoker<T>> selected) throws RpcException {
if (invokers == null || invokers.size() == 0)
return null;
//只有一個invoker,直接返回,不需要處理
if (invokers.size() == 1)
return invokers.get(0);
// 如果只有兩個invoker,退化成輪循
if (invokers.size() == 2 && selected != null && selected.size() > 0) {
return selected.get(0) == invokers.get(0) ? invokers.get(1) : invokers.get(0);
}
//使用loadBalance進行選擇
Invoker<T> invoker = loadbalance.select(invokers, getUrl(), invocation);
//如果 selected中包含(優先判斷) 或者 不可用&&availablecheck=true 則重試.
if( (selected != null && selected.contains(invoker))
||(!invoker.isAvailable() && getUrl()!=null && availablecheck)){
try{
//重新選擇
Invoker<T> rinvoker = reselect(loadbalance, invocation, invokers, selected, availablecheck);
if(rinvoker != null){
invoker = rinvoker;
}else{
//看下第一次選的位置,如果不是最後,選+1位置.
int index = invokers.indexOf(invoker);
try{
//最後在避免碰撞
invoker = index <invokers.size()-1?invokers.get(index+1) :invoker;
}catch (Exception e) {。。。 }
}
}catch (Throwable t){。。。}
}
return invoker;
}
接着看使用loadBalance進行選擇,首先進入AbstractLoadBalance的select方法:
public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
if (invokers == null || invokers.size() == 0)
return null;
if (invokers.size() == 1)
return invokers.get(0);
// 進行選擇,具體的子類實現,我們這裏是RandomLoadBalance
return doSelect(invokers, url, invocation);
}
接着去RandomLoadBalance中查看:
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));
}
上面根據權重之類的來進行選擇一個Invoker返回。接下來reselect的方法不在說明,是先從非selected的列表中選擇,沒有在從selected列表中選擇。
選擇好了Invoker之後,就回去FailoverClusterInvoker的doInvoke方法,接着就是根據選中的Invoker調用invoke方法進行返回結果,接着就是到具體的Invoker進行調用的過程了。