Dubbo是Alibaba開源的分佈式服務框架,我們可以非常容易地通過Dubbo來構建分佈式服務,並根據自己實際業務應用場景來選擇合適的集羣容錯模式,這個對於很多應用都是迫切希望的,只需要通過簡單的配置就能夠實現分佈式服務調用,也就是說服務提供方(Provider)發佈的服務可以天然就是集羣服務,比如,在實時性要求很高的應用場景下,可能希望來自消費方(Consumer)的調用響應時間最短,只需要選擇Dubbo的Forking
Cluster模式配置,就可以對一個調用請求並行發送到多臺對等的提供方(Provider)服務所在的節點上,只選擇最快一個返回響應的,然後將調用結果返回給服務消費方(Consumer),顯然這種方式是以冗餘服務爲基礎的,需要消耗更多的資源,但是能夠滿足高實時應用的需求。
有關Dubbo服務框架的簡單使用,可以參考我的其他兩篇文章(《基於Dubbo的Hessian協議實現遠程調用》,《Dubbo實現RPC調用使用入門》,後面參考鏈接中已給出鏈接),這裏主要圍繞Dubbo分佈式服務相關配置的使用來說明與實踐。
Dubbo服務集羣容錯
假設我們使用的是單機模式的Dubbo服務,如果在服務提供方(Provider)發佈服務以後,服務消費方(Consumer)發出一次調用請求,恰好這次由於網絡問題調用失敗,那麼我們可以配置服務消費方重試策略,可能消費方第二次重試調用是成功的(重試策略只需要配置即可,重試過程是透明的);但是,如果服務提供方發佈服務所在的節點發生故障,那麼消費方再怎麼重試調用都是失敗的,所以我們需要採用集羣容錯模式,這樣如果單個服務節點因故障無法提供服務,還可以根據配置的集羣容錯模式,調用其他可用的服務節點,這就提高了服務的可用性。
首先,根據Dubbo文檔,我們引用文檔提供的一個架構圖以及各組件關係說明,如下所示:
上述各個組件之間的關係(引自Dubbo文檔)說明如下:
-
這裏的Invoker是Provider的一個可調用Service的抽象,Invoker封裝了Provider地址及Service接口信息。
-
Directory代表多個Invoker,可以把它看成List,但與List不同的是,它的值可能是動態變化的,比如註冊中心推送變更。
-
Cluster將Directory中的多個Invoker僞裝成一個Invoker,對上層透明,僞裝過程包含了容錯邏輯,調用失敗後,重試另一個。
-
Router負責從多個Invoker中按路由規則選出子集,比如讀寫分離,應用隔離等。
-
LoadBalance負責從多個Invoker中選出具體的一個用於本次調用,選的過程包含了負載均衡算法,調用失敗後,需要重選。
我們也簡單說明目前Dubbo支持的集羣容錯模式,每種模式適應特定的應用場景,可以根據實際需要進行選擇。Dubbo內置支持如下6種集羣模式:
配置值爲failover。這種模式是Dubbo集羣容錯默認的模式選擇,調用失敗時,會自動切換,重新嘗試調用其他節點上可用的服務。對於一些冪等性操作可以使用該模式,如讀操作,因爲每次調用的副作用是相同的,所以可以選擇自動切換並重試調用,對調用者完全透明。可以看到,如果重試調用必然會帶來響應端的延遲,如果出現大量的重試調用,可能說明我們的服務提供方發佈的服務有問題,如網絡延遲嚴重、硬件設備需要升級、程序算法非常耗時,等等,這就需要仔細檢測排查了。
例如,可以這樣顯式指定Failover模式,或者不配置則默認開啓Failover模式,配置示例如下:
1 |
< dubbo:service interface = "org.shirdrn.dubbo.api.ChatRoomOnlineUserCounterService" version = "1.0.0" |
2 |
cluster = "failover" retries = "2" timeout = "100" ref = "chatRoomOnlineUserCounterService" protocol = "dubbo" > |
3 |
< dubbo:method name = "queryRoomUserCount" timeout = "80" retries = "2" /> |
上述配置使用Failover Cluster模式,如果調用失敗一次,可以再次重試2次調用,服務級別調用超時時間爲100ms,調用方法queryRoomUserCount的超時時間爲80ms,允許重試2次,最壞情況調用花費時間160ms。如果該服務接口org.shirdrn.dubbo.api.ChatRoomOnlineUserCounterService還有其他的方法可供調用,則其他方法沒有顯式配置則會繼承使用dubbo:service配置的屬性值。
配置值爲failfast。這種模式稱爲快速失敗模式,調用只執行一次,失敗則立即報錯。這種模式適用於非冪等性操作,每次調用的副作用是不同的,如寫操作,比如交易系統我們要下訂單,如果一次失敗就應該讓它失敗,通常由服務消費方控制是否重新發起下訂單操作請求(另一個新的訂單)。
配置值爲failsafe。失敗安全模式,如果調用失敗, 則直接忽略失敗的調用,而是要記錄下失敗的調用到日誌文件,以便後續審計。
配置值爲failback。失敗自動恢復,後臺記錄失敗請求,定時重發。通常用於消息通知操作。
配置值爲forking。並行調用多個服務器,只要一個成功即返回。通常用於實時性要求較高的讀操作,但需要浪費更多服務資源。
配置值爲broadcast。廣播調用所有提供者,逐個調用,任意一臺報錯則報錯(2.1.0開始支持)。通常用於通知所有提供者更新緩存或日誌等本地資源信息。
上面的6種模式都可以應用於生產環境,我們可以根據實際應用場景選擇合適的集羣容錯模式。如果我們覺得Dubbo內置提供的幾種集羣容錯模式都不能滿足應用需要,也可以定製實現自己的集羣容錯模式,因爲Dubbo框架給我提供的擴展的接口,只需要實現接口com.alibaba.dubbo.rpc.cluster.Cluster即可,接口定義如下所示:
01 |
@SPI (FailoverCluster.NAME) |
02 |
public interface Cluster
{ |
05 |
*
Merge the directory invokers to a virtual invoker. |
08 |
*
@return cluster invoker |
09 |
*
@throws RpcException |
12 |
<T>
Invoker<T> join(Directory<T> directory) throws RpcException; |
關於如何實現一個自定義的集羣容錯模式,可以參考Dubbo源碼中內置支持的汲取你容錯模式的實現,6種模式對應的實現類如下所示:
1 |
com.alibaba.dubbo.rpc.cluster.support.FailoverCluster |
2 |
com.alibaba.dubbo.rpc.cluster.support.FailfastCluster |
3 |
com.alibaba.dubbo.rpc.cluster.support.FailsafeCluster |
4 |
com.alibaba.dubbo.rpc.cluster.support.FailbackCluster |
5 |
com.alibaba.dubbo.rpc.cluster.support.ForkingCluster |
6 |
com.alibaba.dubbo.rpc.cluster.support.AvailableCluster |
可能我們初次接觸Dubbo時,不知道如何在實際開發過程中使用Dubbo的集羣模式,後面我們會以Failover Cluster模式爲例開發我們的分佈式應用,再進行詳細的介紹。
Dubbo服務負載均衡
Dubbo框架內置提供負載均衡的功能以及擴展接口,我們可以透明地擴展一個服務或服務集羣,根據需要非常容易地增加/移除節點,提高服務的可伸縮性。Dubbo框架內置提供了4種負載均衡策略,如下所示:
-
Random LoadBalance:隨機策略,配置值爲random。可以設置權重,有利於充分利用服務器的資源,高配的可以設置權重大一些,低配的可以稍微小一些
-
RoundRobin LoadBalance:輪詢策略,配置值爲roundrobin。
-
LeastActive LoadBalance:配置值爲leastactive。根據請求調用的次數計數,處理請求更慢的節點會受到更少的請求
-
ConsistentHash LoadBalance:一致性Hash策略,具體配置方法可以參考Dubbo文檔。相同調用參數的請求會發送到同一個服務提供方節點上,如果某個節點發生故障無法提供服務,則會基於一致性Hash算法映射到虛擬節點上(其他服務提供方)
在實際使用中,只需要選擇合適的負載均衡策略值,配置即可,下面是上述四種負載均衡策略配置的示例:
1 |
< dubbo:service interface = "org.shirdrn.dubbo.api.ChatRoomOnlineUserCounterService" version = "1.0.0" |
2 |
cluster = "failover" retries = "2" timeout = "100" loadbalance = "random" |
3 |
ref = "chatRoomOnlineUserCounterService" protocol = "dubbo" > |
4 |
< dubbo:method name = "queryRoomUserCount" timeout = "80" retries = "2" loadbalance = "leastactive" /> |
上述配置,也體現了Dubbo配置的繼承性特點,也就是dubbo:service元素配置了loadbalance=”random”,則該元素的子元素dubbo:method如果沒有指定負載均衡策略,則默認爲loadbalance=”random”,否則如果dubbo:method指定了loadbalance=”leastactive”,則使用子元素配置的負載均衡策略覆蓋了父元素指定的策略(這裏調用queryRoomUserCount方法使用leastactive負載均衡策略)。
當然,Dubbo框架也提供了實現自定義負載均衡策略的接口,可以實現com.alibaba.dubbo.rpc.cluster.LoadBalance接口,接口定義如下所示:
02 |
*
LoadBalance. (SPI, Singleton, ThreadSafe) |
06 |
*
@see com.alibaba.dubbo.rpc.cluster.Cluster#join(Directory) |
08 |
*
@author william.liangf |
10 |
@SPI (RandomLoadBalance.NAME) |
11 |
public interface LoadBalance
{ |
14 |
*
select one invoker in list. |
15 |
*
@param invokers invokers. |
16 |
*
@param url refer url |
17 |
*
@param invocation invocation. |
18 |
*
@return selected invoker. |
20 |
@Adaptive ( "loadbalance" ) |
21 |
<T>
Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException; |
如何實現一個自定義負載均衡策略,可以參考Dubbo框架內置的實現,如下所示的3個實現類:
1 |
com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance |
2 |
com.alibaba.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance |
3 |
com.alibaba.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance |
Dubbo服務集羣容錯實踐
手機應用是以聊天室爲基礎的,我們需要收集用戶的操作行爲,然後計算聊天室中在線人數,並實時在手機應用端顯示人數,整個系統的架構如圖所示:
上圖中,主要包括了兩大主要流程:日誌收集並實時處理流程、調用讀取實時計算結果流程,我們使用基於Dubbo框架開發的服務來提供實時計算結果讀取聊天人數的功能。上圖中,實際上業務接口服務器集羣也可以基於Dubbo框架構建服務,就看我們想要構建什麼樣的系統來滿足我們的需要。
如果不使用註冊中心,服務消費方也能夠直接調用服務提供方發佈的服務,這樣需要服務提供方將服務地址暴露給服務消費方,而且也無法使用監控中心的功能,這種方式成爲直連。
如果我們使用註冊中心,服務提供方將服務發佈到註冊中心,而服務消費方可以通過註冊中心訂閱服務,接收服務提供方服務變更通知,這種方式可以隱藏服務提供方的細節,包括服務器地址等敏感信息,而服務消費方只能通過註冊中心來獲取到已註冊的提供方服務,而不能直接跨過註冊中心與服務提供方直接連接。這種方式的好處是還可以使用監控中心服務,能夠對服務的調用情況進行監控分析,還能使用Dubbo服務管理中心,方便管理服務,我們在這裏使用的是這種方式,也推薦使用這種方式。使用註冊中心的Dubbo分佈式服務相關組件結構,如下圖所示:
下面,開發部署我們的應用,通過如下4個步驟來完成:
服務接口將服務提供方(Provider)和服務消費方(Consumer)連接起來,服務提供方實現接口中定義的服務,即給出服務的實現,而服務消費方負責調用服務。我們接口中給出了2個方法,一個是實時查詢獲取當前聊天室內人數,另一個是查詢一天中某個/某些聊天室中在線人數峯值,接口定義如下所示:
01 |
package org.shirdrn.dubbo.api; |
03 |
import java.util.List; |
05 |
public interface ChatRoomOnlineUserCounterService
{ |
07 |
String
queryRoomUserCount(String rooms); |
09 |
List<String>
getMaxOnlineUserCount(List<String> rooms, String date, String dateFormat); |
接口是服務提供方和服務消費方公共遵守的協議,一般情況下是服務提供方將接口定義好後提供給服務消費方。
服務提供方實現接口中定義的服務,其實現和普通的服務沒什麼區別,我們的實現類爲ChatRoomOnlineUserCounterServiceImpl,代碼如下所示:
01 |
package org.shirdrn.dubbo.provider.service; |
03 |
import java.util.List; |
05 |
import org.apache.commons.logging.Log; |
06 |
import org.apache.commons.logging.LogFactory; |
07 |
import org.shirdrn.dubbo.api.ChatRoomOnlineUserCounterService; |
08 |
import org.shirdrn.dubbo.common.utils.DateTimeUtils; |
10 |
import redis.clients.jedis.Jedis; |
11 |
import redis.clients.jedis.JedisPool; |
13 |
import com.alibaba.dubbo.common.utils.StringUtils; |
14 |
import com.google.common.base.Strings; |
15 |
import com.google.common.collect.Lists; |
17 |
public class ChatRoomOnlineUserCounterServiceImpl implements ChatRoomOnlineUserCounterService
{ |
19 |
private static final Log
LOG = LogFactory.getLog(ChatRoomOnlineUserCounterServiceImpl. class ); |
20 |
private JedisPool
jedisPool; |
21 |
private static final String
KEY_USER_COUNT = "chat::room::play::user::cnt" ; |
22 |
private static final String
KEY_MAX_USER_COUNT_PREFIX = "chat::room::max::user::cnt::" ; |
23 |
private static final String
DF_YYYYMMDD = "yyyyMMdd" ; |
25 |
public String
queryRoomUserCount(String rooms) { |
26 |
LOG.info( "Params[Server|Recv|REQ]
rooms=" +
rooms); |
27 |
StringBuffer
builder = new StringBuffer(); |
28 |
if (!Strings.isNullOrEmpty(rooms))
{ |
31 |
jedis
= jedisPool.getResource(); |
32 |
String[]
fields = rooms.split( "," ); |
33 |
List<String>
results = jedis.hmget(KEY_USER_COUNT, fields); |
34 |
builder.append(StringUtils.join(results, "," )); |
35 |
} catch (Exception
e) { |
43 |
LOG.info( "Result[Server|Recv|RES]
" +
builder.toString()); |
44 |
return builder.toString(); |
48 |
public List<String>
getMaxOnlineUserCount(List<String> rooms, String date, String dateFormat) { |
50 |
LOG.info( "Params[Server|Recv|REQ]
rooms=" +
rooms + ",date=" +
date + ",dateFormat=" +
dateFormat); |
51 |
String
whichDate = DateTimeUtils.format(date, dateFormat, DF_YYYYMMDD); |
52 |
String
key = KEY_MAX_USER_COUNT_PREFIX + whichDate; |
53 |
StringBuffer
builder = new StringBuffer(); |
54 |
if (rooms
!= null &&
!rooms.isEmpty()) { |
57 |
jedis
= jedisPool.getResource(); |
58 |
return jedis.hmget(key,
rooms.toArray( new String[rooms.size()])); |
59 |
} catch (Exception
e) { |
67 |
LOG.info( "Result[Server|Recv|RES]
" +
builder.toString()); |
68 |
return Lists.newArrayList(); |
71 |
public void setJedisPool(JedisPool
jedisPool) { |
72 |
this .jedisPool
= jedisPool; |
代碼中通過讀取Redis中數據來完成調用,邏輯比較簡單。對應的Maven POM依賴配置,如下所示:
03 |
< groupId >org.shirdrn.dubbo</ groupId > |
04 |
< artifactId >dubbo-api</ artifactId > |
05 |
< version >0.0.1-SNAPSHOT</ version > |
08 |
< groupId >org.shirdrn.dubbo</ groupId > |
09 |
< artifactId >dubbo-commons</ artifactId > |
10 |
< version >0.0.1-SNAPSHOT</ version > |
13 |
< groupId >redis.clients</ groupId > |
14 |
< artifactId >jedis</ artifactId > |
15 |
< version >2.5.2</ version > |
18 |
< groupId >org.apache.commons</ groupId > |
19 |
< artifactId >commons-pool2</ artifactId > |
20 |
< version >2.2</ version > |
23 |
< groupId >org.jboss.netty</ groupId > |
24 |
< artifactId >netty</ artifactId > |
25 |
< version >3.2.7.Final</ version > |
有關對Dubbo框架的一些依賴,我們單獨放到一個通用的Maven Module中(詳見後面“附錄:Dubbo使用Maven構建依賴配置”),這裏不再多說。服務提供方實現,最關鍵的就是服務的配置,因爲Dubbo基於Spring來管理配置和實例,所以通過配置可以指定服務是否是分佈式服務,以及通過配置增加很多其它特性。我們的配置文件爲provider-cluster.xml,內容如下所示:
01 |
<? xml version = "1.0" encoding = "UTF-8" ?> |
09 |
< bean class = "org.springframework.beans.factory.config.PropertyPlaceholderConfigurer" > |
10 |
< property name = "systemPropertiesModeName" value = "SYSTEM_PROPERTIES_MODE_OVERRIDE" /> |
11 |
< property name = "ignoreResourceNotFound" value = "true" /> |
12 |
< property name = "locations" > |
14 |
< value >classpath*:jedis.properties</ value > |
19 |
< dubbo:application name = "chatroom-cluster-provider" /> |
22 |
< dubbo:protocol name = "dubbo" port = "20880" /> |
24 |
< dubbo:service interface = "org.shirdrn.dubbo.api.ChatRoomOnlineUserCounterService" version = "1.0.0" |
25 |
cluster = "failover" retries = "2" timeout = "1000" loadbalance = "random" actives = "100" executes = "200" |
26 |
ref = "chatRoomOnlineUserCounterService" protocol = "dubbo" > |
27 |
< dubbo:method name = "queryRoomUserCount" timeout = "500" retries = "2" loadbalance = "roundrobin" actives = "50" /> |
30 |
< bean id = "chatRoomOnlineUserCounterService" class = "org.shirdrn.dubbo.provider.service.ChatRoomOnlineUserCounterServiceImpl" > |
31 |
< property name = "jedisPool" ref = "jedisPool" /> |
34 |
< bean id = "jedisPool" class = "redis.clients.jedis.JedisPool" destroy-method = "destroy" > |
35 |
< constructor-arg index = "0" > |
36 |
< bean class = "org.apache.commons.pool2.impl.GenericObjectPoolConfig" > |
37 |
< property name = "maxTotal" value = "${redis.pool.maxTotal}" /> |
38 |
< property name = "maxIdle" value = "${redis.pool.maxIdle}" /> |
39 |
< property name = "minIdle" value = "${redis.pool.minIdle}" /> |
40 |
< property name = "maxWaitMillis" value = "${redis.pool.maxWaitMillis}" /> |
41 |
< property name = "testOnBorrow" value = "${redis.pool.testOnBorrow}" /> |
42 |
< property name = "testOnReturn" value = "${redis.pool.testOnReturn}" /> |
43 |
< property name = "testWhileIdle" value = "true" /> |
46 |
< constructor-arg index = "1" value = "${redis.host}" /> |
47 |
< constructor-arg index = "2" value = "${redis.port}" /> |
48 |
< constructor-arg index = "3" value = "${redis.timeout}" /> |
上面配置中,使用dubbo協議,集羣容錯模式爲failover,服務級別負載均衡策略爲random,方法級別負載均衡策略爲roundrobin(它覆蓋了服務級別的配置內容),其他一些配置內容可以參考Dubbo文檔。我們這裏是從Redis讀取數據,所以使用了Redis連接池。
啓動服務示例代碼如下所示:
01 |
package org.shirdrn.dubbo.provider; |
03 |
import org.shirdrn.dubbo.provider.common.DubboServer; |
05 |
public class ChatRoomClusterServer
{ |
07 |
public static void main(String[]
args) throws Exception
{ |
08 |
DubboServer.startServer( "classpath:provider-cluster.xml" ); |