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,可以保證相同參數的請求總是發到同一提供者,當某一台提供者掛了時,原本發往該提供者的請求,基於虛擬節點,平攤到其他提供者,不會引起劇烈變動。
一致性Hash負載均衡策略原理
在解決分布式系統中,負載均衡的問題可以使用Hash算法讓固定的一部分請求落到同一台服務器上,這樣每台服務器固定處理一部分請求(並維護這些請求的信息),起到負載均衡的作用。
但是普通的余數Hash(Hash(比如用戶id)%服務器機器數)算法伸縮性很差,當新增或者下線服務器機器時候,用戶id與服務器的映射關系會大量失效。一致性Hash則利用Hash環對其進行了改進。
一致性Hash概述
為了能直觀的理解一致性Hash的原理,這里結合一個簡單的例子來講解,假設有4台服務器,地址為ip1/ip2/ip3/ip4 。
- 一致性Hash,首先計算四個ip地址對應的Hash值:hash(ip1) / hash(ip2) / hash(ip3) / hash(ip4) ,計算出來的Hash值是0~最大正整數之間的一個值,這四個值在一致性Hash環上呈現如下圖:
- Hash環上順時針從整數0開始,一直到最大正整數,我們根據四個ip計算的Hash值肯定會落到這個Hash環上的某一個點,至此我們把服務器的四個ip映射到了一致性Hash環。
- 當用戶在客戶端進行請求時候,首先根據Hash(用戶id)計算路由規則(Hash值),然后看Hash值落到了Hash環的哪個地方,根據Hash值在Hash環上的位置順時針找距離最近的ip作為路由ip。
如上圖可知user1 / user2的請求會落到服務器 ip2 進行處理,user3的請求會落到服務器ip3進行處理,user4的請求會落到服務器ip4進行處理,user5 / user6 的請求會落到服務器ip1進行處理。
下面考慮當ip2的服務器掛了的時候會出現什么情況?
當ip2的服務器掛了的時候,一致性Hash環大致如下圖:
根據順時針規則可知user1 / user2的請求會被服務器ip3進行處理,而其他用戶的請求對應的處理服務器不變,也就是只有之前被ip2處理的一部分用戶的映射關系被破壞了,並且其負責處理的請求被順時針下一個節點委托處理。
下面考慮當有新增機器加入時會出現什么情況?
當新增一個ip5的服務器后,一致性Hash環大致如下圖:
根據順時針規則可知之前user1的請求應該被ip1服務器處理,現在被新增的ip5服務器處理,其他用戶的請求處理服務器不變,也就是新增的服務器順時針最近的服務器的一部分請求會被新增的服務器所替代。
一致性Hash的特性
一致性Hash有要有以下特性:
- 單調性(Monotonicity),單調性是指如果已經有一些請求通過哈希分派到了相應的服務器進行處理,又有新的服務器加入到系統中時,應保證原有的請求可以被映射到原有的或者新的服務器中去,而不會被映射到原來的其他服務器上去。這一點通過上面新增服務器ip5可以證明,新增ip5后,原來被ip1處理的user6現在還是被ip1處理的user5現在被新增的ip5處理。
- 分散性(Spread):分布式環境中,客戶端請求時候可能不知道所有服務器的存在,可能只知道其中一部分服務器,在客戶端看來它看到的部分服務器會形成一個完整的Hash環,那么可能會導致,同一個用戶的請求被路由到不同的服務器進行處理。這種情況顯然是應該避免的,因為它不能保證同一個用戶的請求落到同一個服務器。所謂分散性是指上述情況發生的嚴重程度。
- 平衡性(Balance),平衡性也就是說負載均衡,是指客戶端Hash后的請求應該能夠分散到不同的服務器上去。一致性Hash可以做到每個服務器都進行處理請求,但是不能保證每個服務器處理的請求的數量大致相同,如下圖:
服務器ip1 / ip2 / ip3經過Hash后落到了一致性Hash環上,從圖中Hash值分布可知ip1會負責處理大概80%的請求,而ip2和ip3則只會負責處理大概20%的請求,雖然三個機器都在處理請求,但是明顯每個機器的負載不均衡,這樣稱為一致性Hash的傾斜,虛擬節點的出現就是為了解決這個問題。
虛擬節點
當服務器節點比較少的時候會出現上節所說的一致性Hash傾斜的問題,一個解決方法是多加機器,但是加機器是有成本的,那么就加虛擬節點,比如上面三個機器,每個機器引入1個虛擬節點后的一致性Hash環如下圖所示:
其中ip1-1是ip1的虛擬節點,ip2-1是ip2的虛擬節點,ip3-1是ip3的虛擬節點。
可知當物理機器數目為M,虛擬節點為N的時候,實際hash環上節點個數為 M*(N+1) 。比如當客戶端計算的Hash值處於ip2和ip3或者處於ip2-1和ip3-1之間時候使用ip3服務器進行處理。
均勻一致性Hash
上節我們使用虛擬節點后的圖看起來比較均衡,但是如果生成虛擬節點的算法不夠好很可能會得到下面的環:
可知每個服務節點引入1個虛擬節點后,情況相比沒有引入前均衡性有所改善,但是並不均衡。
均衡的一致性Hash應該如下圖所示:
均勻一致性Hash的目標是如果服務器有N台,客戶端的Hash值有M個,那么每個服務器應該處理大概 M/N 個用戶的請求。也就是每台服務器均衡盡量均衡。Dubbo提供的一致性Hash就是不均勻的,這個大家可以去研究下ConsistentHashLoadBalance類。