Dubbo的集群容錯策略
正常情況下,當我們進行系統設計時候,不僅要考慮正常邏輯下代碼該如何走,還要考慮異常情況下代碼邏輯應該怎么走。當服務消費方調用服務提供方的服務出現錯誤時候,Dubbo提供了多種容錯方案,缺省模式為failover,也就是失敗重試。
Dubbo提供的集群容錯模式
下面看下Dubbo提供的集群容錯模式:
Failover Cluster:失敗重試
當服務消費方調用服務提供者失敗后自動切換到其他服務提供者服務器進行重試。這通常用於讀操作或者具有冪等的寫操作,需要注意的是重試會帶來更長延遲。可通過 retries="2" 來設置重試次數(不含第一次)。
接口級別配置重試次數方法 <dubbo:reference retries="2" /> ,如上配置當服務消費方調用服務失敗后,會再重試兩次,也就是說最多會做三次調用,這里的配置對該接口的所有方法生效。當然你也可以針對某個方法配置重試次數如下:
<dubbo:reference> <dubbo:method name="sayHello" retries="2" /> </dubbo:reference>
Failfast Cluster:快速失敗
當服務消費方調用服務提供者失敗后,立即報錯,也就是只調用一次。通常這種模式用於非冪等性的寫操作。
Failsafe Cluster:失敗安全
當服務消費者調用服務出現異常時,直接忽略異常。這種模式通常用於寫入審計日志等操作。
Failback Cluster:失敗自動恢復
當服務消費端用服務出現異常后,在后台記錄失敗的請求,並按照一定的策略后期再進行重試。這種模式通常用於消息通知操作。
Forking Cluster:並行調用
當消費方調用一個接口方法后,Dubbo Client會並行調用多個服務提供者的服務,只要一個成功即返回。這種模式通常用於實時性要求較高的讀操作,但需要浪費更多服務資源。可通過 forks="2" 來設置最大並行數。
Broadcast Cluster:廣播調用
當消費者調用一個接口方法后,Dubbo Client會逐個調用所有服務提供者,任意一台調用異常則這次調用就標志失敗。這種模式通常用於通知所有提供者更新緩存或日志等本地資源信息。
如上,Dubbo本身提供了豐富的集群容錯模式,但是如果您有定制化需求,可以根據Dubbo提供的擴展接口Cluster進行定制。在后面的消費方啟動流程章節會講解何時/如何使用的集群容錯。
失敗重試策略實現分析
Dubbo中具體實現失敗重試的是FailoverClusterInvoker類,這里我們看下具體實現,主要看下doInvoke代碼:
public Result doInvoke(Invocation invocation,final List<Invoker<T>> invokers,LoadBalance loadbalance) throws RpcException{ // (1) 所有服務提供者 List<Invoker<T>> copyinvokers = invokers; checkInvokers(copyinvokers,invocation); // (2)獲取重試次數 int len = getUrl().getMethodParameter(invocation.getMethodName(),Constants.RETRIES_KEY,Constants.DEFAULT_RETRIES) + 1; if(len <= 0){ len = 1; } // (3)使用循環,失敗重試 RpcException le = null; // last exception List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyinvokers.size()); Set<String> providers = new HashSet<String>(); for(int i=0;i<len;i++){ // 重試時,進行重新選擇,避免重試時invoker列表已發生變化 // 注意:如果列表發生了變化,那么invoked判斷會失效,因為invoker示例已經改變 if(i > 0){ // (3.1) checkWhetherDestroyed(); // 如果當前實例已經被銷毀,則拋出異常 // (3.2) 重新獲取所有服務提供者 copyinvokers = list(invocation); // (3.3) 重新檢查一下 checkInvokers(copyinvokers,invocation); } // (3.4) 選擇負載均衡策略 Invoker<T> invoker = select(loadbalance,invocation,copyinvokers,invoked); invoked.add(invoker); RpcContext.getContext().setInvokers((List)invoked); // (3.5) 具體發起遠程調用 try{ Result result = invoker.invoke(invocation); if(le != null && logger.isWarnEnabled()){ ... } return result; }catch(RpcException e){ if(e.isBiz()){ // biz exception throw e; } le = e; }catch(Throwable e){ le = new RpcException(e.getMessage(),e); }finally{ providers.add(invoker.getUrl().getAddress()); } } throw new RpcException("拋出異常..."); }
- 如上代碼(2)從url參數里面獲取設置的重試次數,如果用戶沒有設置則取默認的值,默認是重試2,這里需要注意的是代碼(2)是獲取配置重試次數又+1了。這說明 總共調用次數=重試次數+1 (1是正常調用)。
- 代碼(3)循環重復試用,如果第一次調用成功則直接跳出循環返回,否則循環重試。第一次調用時不會走代碼(3.1)(3.2)(3.3)。如果第一次調用出現異常,則會循環,這時候i=1,所以會執行代碼(3.1)檢查是否有線程調用了當前ReferenceConfig的destroy()方法,銷毀了當前消費者。如果當前消費者實例已經被消費,那么重試就沒有意義了,所以會拋出RpcException異常。
- 如果當前消費者實例沒被銷毀,則執行代碼(3.2)重新獲取當前服務提供者列表,這是因為從第一次調開始到線程可能提供者列表已經變化了,獲取列表后,然后執行(3.2)又一次進行了校驗。校驗通過則執行(3.4),根據負載均衡策略選擇一個服務提供者,再次嘗試調用。負載均衡策略的選擇下節會講解。
Dubbo的負載均衡策略
當服務提供方是集群的時候,為了避免大量請求一直落到一個或幾個服務提供方機器上,從而使這些機器負載很高,甚至打死,需要做一定的負載均衡策略。Dubbo提供了多種均衡策略,缺省為random,也就是每次隨機調用一台服務提供者的機器。
Dubbo提供的負載均衡策略
- Random LoadBalance:隨機策略。按照概率設置權重,比較均勻,並且可以動態調節提供者的權重。
- RoundRobin LoadBalance:輪詢策略。輪詢,按公約后的權重設置輪詢比率。會存在執行比較慢的服務提供者堆積請求的情況,比如一個機器執行的非常慢,但是機器沒有掛調用(如果掛了,那么當前機器會從Zookeeper的服務列表刪除),當很多新的請求到達該機器后,由於之前的請求還沒有處理完畢,會導致新的請求被堆積,久而久之,所有消費者調用這台機器上的請求都被阻塞。
- LeastActive LoadBalance:最少活躍調用數。如果每個提供者的活躍數相同,則隨機選擇一個。在每個服務提供者里面維護者一個活躍數計數器,用來記錄當前同時處理請求的個數,也就是並發處理任務的個數。所以如果這個值越小說明當前服務提供者處理的速度很快或者當前機器的負載比較低,所以路由選擇時候就選擇該活躍度最小的機器。如果一個服務提供者處理速度很慢,由於堆積,那么同時處理的請求就比較多,也就是活躍調用數目越大,這也使得慢的提供者收到更少請求,因為越慢的提供者的活躍度越來越大。
- ConsistentHash LoadBalance:一致性Hash策略。一致性Hash,可以保證相同參數的請求總是發到同一提供者,當某一台提供者掛了時,原本發往該提供者的請求,基於虛擬節點,平攤到其他提供者,不會引起劇烈變動。
在spring boot dubbo中,它可以通過參數spring.dubbo.reference.loadbalance=consistentHash啟用,但是一致性哈希有一個並不是很合理的地方,它是將相同參數的請求發送到相同提供者,默認為第一個參數。如果所有接口的第一個參數為公共入參還好,如果是采用泛型或繼承設計,這就比較悲劇了,我們就遇到這個問題。分析源碼,可知它是調用com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance.ConsistentHashSelector#select生成的,如下:
public Invoker<T> select(Invocation invocation) { String key = toKey(invocation.getArguments()); byte[] digest = md5(key); Invoker<T> invoker = sekectForKey(hash(digest, 0)); return invoker; } private String toKey(Object[] args) { StringBuilder buf = new StringBuilder(); for (int i : argumentIndex) { if (i >= 0 && i < args.length) { buf.append(args[i]); } } return buf.toString(); }
所以,我們完全可以通過將toKey暴露給業務自定義實現,如下:
public Invoker<T> select(Invocation invocation) { String key = toKey(invocation.getArguments()); // zjhua 自定義一致性哈希判斷用的key,用於指定請求發送到特定節點 if (rpcLoadBalance == null) { rpcLoadBalance = SpringContextHolder.getBean(RpcLoadBalance.class); } if (rpcLoadBalance != null) { key = rpcLoadBalance.toKey(invocation,invocation.getArguments()); } byte[] digest = md5(key); Invoker<T> invoker = sekectForKey(hash(digest, 0)); return invoker; }
這樣就可以根據特定接口/參數值業務自己決定是否需要將請求發送到相同節點了。