基於Dubbo框架構建分佈式服務【未完待續】

Dubbo是Alibaba開源的分佈式服務框架,我們可以非常容易地通過Dubbo來構建分佈式服務,並根據自己實際業務應用場景來選擇合適的集羣容錯模式,這個對於很多應用都是迫切希望的,只需要通過簡單的配置就能夠實現分佈式服務調用,也就是說服務提供方(Provider)發佈的服務可以天然就是集羣服務,比如,在實時性要求很高的應用場景下,可能希望來自消費方(Consumer)的調用響應時間最短,只需要選擇Dubbo的Forking Cluster模式配置,就可以對一個調用請求並行發送到多臺對等的提供方(Provider)服務所在的節點上,只選擇最快一個返回響應的,然後將調用結果返回給服務消費方(Consumer),顯然這種方式是以冗餘服務爲基礎的,需要消耗更多的資源,但是能夠滿足高實時應用的需求。
有關Dubbo服務框架的簡單使用,可以參考我的其他兩篇文章(《基於Dubbo的Hessian協議實現遠程調用》,《Dubbo實現RPC調用使用入門》,後面參考鏈接中已給出鏈接),這裏主要圍繞Dubbo分佈式服務相關配置的使用來說明與實踐。

Dubbo服務集羣容錯

假設我們使用的是單機模式的Dubbo服務,如果在服務提供方(Provider)發佈服務以後,服務消費方(Consumer)發出一次調用請求,恰好這次由於網絡問題調用失敗,那麼我們可以配置服務消費方重試策略,可能消費方第二次重試調用是成功的(重試策略只需要配置即可,重試過程是透明的);但是,如果服務提供方發佈服務所在的節點發生故障,那麼消費方再怎麼重試調用都是失敗的,所以我們需要採用集羣容錯模式,這樣如果單個服務節點因故障無法提供服務,還可以根據配置的集羣容錯模式,調用其他可用的服務節點,這就提高了服務的可用性。
首先,根據Dubbo文檔,我們引用文檔提供的一個架構圖以及各組件關係說明,如下所示:
dubbo-cluster-architecture
上述各個組件之間的關係(引自Dubbo文檔)說明如下:

  • 這裏的Invoker是Provider的一個可調用Service的抽象,Invoker封裝了Provider地址及Service接口信息。
  • Directory代表多個Invoker,可以把它看成List,但與List不同的是,它的值可能是動態變化的,比如註冊中心推送變更。
  • Cluster將Directory中的多個Invoker僞裝成一個Invoker,對上層透明,僞裝過程包含了容錯邏輯,調用失敗後,重試另一個。
  • Router負責從多個Invoker中按路由規則選出子集,比如讀寫分離,應用隔離等。
  • LoadBalance負責從多個Invoker中選出具體的一個用於本次調用,選的過程包含了負載均衡算法,調用失敗後,需要重選。

我們也簡單說明目前Dubbo支持的集羣容錯模式,每種模式適應特定的應用場景,可以根據實際需要進行選擇。Dubbo內置支持如下6種集羣模式:

  • Failover Cluster模式

配置值爲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" />
4 </dubbo:service>

上述配置使用Failover Cluster模式,如果調用失敗一次,可以再次重試2次調用,服務級別調用超時時間爲100ms,調用方法queryRoomUserCount的超時時間爲80ms,允許重試2次,最壞情況調用花費時間160ms。如果該服務接口org.shirdrn.dubbo.api.ChatRoomOnlineUserCounterService還有其他的方法可供調用,則其他方法沒有顯式配置則會繼承使用dubbo:service配置的屬性值。

  • Failfast Cluster模式

配置值爲failfast。這種模式稱爲快速失敗模式,調用只執行一次,失敗則立即報錯。這種模式適用於非冪等性操作,每次調用的副作用是不同的,如寫操作,比如交易系統我們要下訂單,如果一次失敗就應該讓它失敗,通常由服務消費方控制是否重新發起下訂單操作請求(另一個新的訂單)。

  • Failsafe Cluster模式

配置值爲failsafe。失敗安全模式,如果調用失敗, 則直接忽略失敗的調用,而是要記錄下失敗的調用到日誌文件,以便後續審計。

  • Failback Cluster模式

配置值爲failback。失敗自動恢復,後臺記錄失敗請求,定時重發。通常用於消息通知操作。

  • Forking Cluster模式

配置值爲forking。並行調用多個服務器,只要一個成功即返回。通常用於實時性要求較高的讀操作,但需要浪費更多服務資源。

  • Broadcast Cluster模式

配置值爲broadcast。廣播調用所有提供者,逐個調用,任意一臺報錯則報錯(2.1.0開始支持)。通常用於通知所有提供者更新緩存或日誌等本地資源信息。
上面的6種模式都可以應用於生產環境,我們可以根據實際應用場景選擇合適的集羣容錯模式。如果我們覺得Dubbo內置提供的幾種集羣容錯模式都不能滿足應用需要,也可以定製實現自己的集羣容錯模式,因爲Dubbo框架給我提供的擴展的接口,只需要實現接口com.alibaba.dubbo.rpc.cluster.Cluster即可,接口定義如下所示:

01 @SPI(FailoverCluster.NAME)
02 public interface Cluster {
03
04     /**
05      * Merge the directory invokers to a virtual invoker.
06      * @param <T>
07      * @param directory
08      * @return cluster invoker
09      * @throws RpcException
10      */
11     @Adaptive
12     <T> Invoker<T> join(Directory<T> directory) throws RpcException;
13
14 }

關於如何實現一個自定義的集羣容錯模式,可以參考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" />
5 </dubbo:service>

上述配置,也體現了Dubbo配置的繼承性特點,也就是dubbo:service元素配置了loadbalance=”random”,則該元素的子元素dubbo:method如果沒有指定負載均衡策略,則默認爲loadbalance=”random”,否則如果dubbo:method指定了loadbalance=”leastactive”,則使用子元素配置的負載均衡策略覆蓋了父元素指定的策略(這裏調用queryRoomUserCount方法使用leastactive負載均衡策略)。
當然,Dubbo框架也提供了實現自定義負載均衡策略的接口,可以實現com.alibaba.dubbo.rpc.cluster.LoadBalance接口,接口定義如下所示:

01 /**
02 * LoadBalance. (SPI, Singleton, ThreadSafe)
03 *
04 * <a href="http://en.wikipedia.org/wiki/Load_balancing_(computing)">Load-Balancing</a>
05 *
06 * @see com.alibaba.dubbo.rpc.cluster.Cluster#join(Directory)
07 * @author qian.lei
08 * @author william.liangf
09 */
10 @SPI(RandomLoadBalance.NAME)
11 public interface LoadBalance {
12
13      /**
14      * select one invoker in list.
15      * @param invokers invokers.
16      * @param url refer url
17      * @param invocation invocation.
18      * @return selected invoker.
19      */
20     @Adaptive("loadbalance")
21      <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throwsRpcException;
22
23 }

如何實現一個自定義負載均衡策略,可以參考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-services-architecture
上圖中,主要包括了兩大主要流程:日誌收集並實時處理流程、調用讀取實時計算結果流程,我們使用基於Dubbo框架開發的服務來提供實時計算結果讀取聊天人數的功能。上圖中,實際上業務接口服務器集羣也可以基於Dubbo框架構建服務,就看我們想要構建什麼樣的系統來滿足我們的需要。
如果不使用註冊中心,服務消費方也能夠直接調用服務提供方發佈的服務,這樣需要服務提供方將服務地址暴露給服務消費方,而且也無法使用監控中心的功能,這種方式成爲直連。
如果我們使用註冊中心,服務提供方將服務發佈到註冊中心,而服務消費方可以通過註冊中心訂閱服務,接收服務提供方服務變更通知,這種方式可以隱藏服務提供方的細節,包括服務器地址等敏感信息,而服務消費方只能通過註冊中心來獲取到已註冊的提供方服務,而不能直接跨過註冊中心與服務提供方直接連接。這種方式的好處是還可以使用監控中心服務,能夠對服務的調用情況進行監控分析,還能使用Dubbo服務管理中心,方便管理服務,我們在這裏使用的是這種方式,也推薦使用這種方式。使用註冊中心的Dubbo分佈式服務相關組件結構,如下圖所示:
dubbo-services-internal-architecture

下面,開發部署我們的應用,通過如下4個步驟來完成:

  • 服務接口定義

服務接口將服務提供方(Provider)和服務消費方(Consumer)連接起來,服務提供方實現接口中定義的服務,即給出服務的實現,而服務消費方負責調用服務。我們接口中給出了2個方法,一個是實時查詢獲取當前聊天室內人數,另一個是查詢一天中某個/某些聊天室中在線人數峯值,接口定義如下所示:

01 package org.shirdrn.dubbo.api;
02
03 import java.util.List;
04
05 public interface ChatRoomOnlineUserCounterService {
06
07      String queryRoomUserCount(String rooms);
08      
09      List<String> getMaxOnlineUserCount(List<String> rooms, String date, String dateFormat);
10 }

接口是服務提供方和服務消費方公共遵守的協議,一般情況下是服務提供方將接口定義好後提供給服務消費方。

  • 服務提供方

服務提供方實現接口中定義的服務,其實現和普通的服務沒什麼區別,我們的實現類爲ChatRoomOnlineUserCounterServiceImpl,代碼如下所示:

01 package org.shirdrn.dubbo.provider.service;
02
03 import java.util.List;
04
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;
09
10 import redis.clients.jedis.Jedis;
11 import redis.clients.jedis.JedisPool;
12
13 import com.alibaba.dubbo.common.utils.StringUtils;
14 import com.google.common.base.Strings;
15 import com.google.common.collect.Lists;
16
17 public class ChatRoomOnlineUserCounterServiceImpl implements ChatRoomOnlineUserCounterService {
18
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";
24
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)) {
29                Jedis jedis = null;
30                try {
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) {
36                     LOG.error("", e);
37                finally {
38                     if(jedis != null) {
39                          jedis.close();
40                     }
41                }
42           }
43           LOG.info("Result[Server|Recv|RES] " + builder.toString());
44           return builder.toString();
45      }
46      
47      @Override
48      public List<String> getMaxOnlineUserCount(List<String> rooms, String date, String dateFormat) {
49           // HGETALL chat::room::max::user::cnt::20150326
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()) {
55                Jedis jedis = null;
56                try {
57                     jedis = jedisPool.getResource();
58                     return jedis.hmget(key, rooms.toArray(new String[rooms.size()]));
59                catch (Exception e) {
60                     LOG.error("", e);
61                finally {
62                     if(jedis != null) {
63                          jedis.close();
64                     }
65                }
66           }
67           LOG.info("Result[Server|Recv|RES] " + builder.toString());
68           return Lists.newArrayList();
69      }
70      
71      public void setJedisPool(JedisPool jedisPool) {
72           this.jedisPool = jedisPool;
73      }
74
75 }

代碼中通過讀取Redis中數據來完成調用,邏輯比較簡單。對應的Maven POM依賴配置,如下所示:

01 <dependencies>
02      <dependency>
03           <groupId>org.shirdrn.dubbo</groupId>
04           <artifactId>dubbo-api</artifactId>
05           <version>0.0.1-SNAPSHOT</version>
06      </dependency>
07      <dependency>
08           <groupId>org.shirdrn.dubbo</groupId>
09           <artifactId>dubbo-commons</artifactId>
10           <version>0.0.1-SNAPSHOT</version>
11      </dependency>
12      <dependency>
13           <groupId>redis.clients</groupId>
14           <artifactId>jedis</artifactId>
15           <version>2.5.2</version>
16      </dependency>
17      <dependency>
18           <groupId>org.apache.commons</groupId>
19           <artifactId>commons-pool2</artifactId>
20           <version>2.2</version>
21      </dependency>
22      <dependency>
23           <groupId>org.jboss.netty</groupId>
24           <artifactId>netty</artifactId>
25           <version>3.2.7.Final</version>
26      </dependency>
27 </dependencies>

有關對Dubbo框架的一些依賴,我們單獨放到一個通用的Maven Module中(詳見後面“附錄:Dubbo使用Maven構建依賴配置”),這裏不再多說。服務提供方實現,最關鍵的就是服務的配置,因爲Dubbo基於Spring來管理配置和實例,所以通過配置可以指定服務是否是分佈式服務,以及通過配置增加很多其它特性。我們的配置文件爲provider-cluster.xml,內容如下所示:

01 <?xml version="1.0" encoding="UTF-8"?>
02
08
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">
13                <list>
14                     <value>classpath*:jedis.properties</value>
15                </list>
16           </property>
17      </bean>
18      
19      <dubbo:application name="chatroom-cluster-provider" />
20      <dubbo:registry address="zookeeper://zk1:2181?backup=zk2:2181,zk3:2181" />
21      
22      <dubbo:protocol name="dubbo" port="20880" />
23      
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" />
28      </dubbo:service>
29      
30      <bean id="chatRoomOnlineUserCounterService"class="org.shirdrn.dubbo.provider.service.ChatRoomOnlineUserCounterServiceImpl" >
31           <property name="jedisPool" ref="jedisPool" />
32      </bean>
33      
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" />
44                </bean>
45           </constructor-arg>
46           <constructor-arg index="1" value="${redis.host}" />
47           <constructor-arg index="2" value="${redis.port}" />
48           <constructor-arg index="3" value="${redis.timeout}" />
49      </bean>
50      
51 </beans>

上面配置中,使用dubbo協議,集羣容錯模式爲failover,服務級別負載均衡策略爲random,方法級別負載均衡策略爲roundrobin(它覆蓋了服務級別的配置內容),其他一些配置內容可以參考Dubbo文檔。我們這裏是從Redis讀取數據,所以使用了Redis連接池。
啓動服務示例代碼如下所示:

01 package org.shirdrn.dubbo.provider;
02
03 import org.shirdrn.dubbo.provider.common.DubboServer;
04
05 public class ChatRoomClusterServer {
06
07      public static void main(String[] args) throws Exception {
08           DubboServer.startServer("classpath:provider-cluster.xml");
09      }
10
11 }


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章