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

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

Dubbo服務集羣容錯redis

假設咱們使用的是單機模式的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種集羣模式:spring

  • Failover Cluster模式

配置值爲failover。這種模式是Dubbo集羣容錯默認的模式選擇,調用失敗時,會自動切換,從新嘗試調用其餘節點上可用的服務。對於一些冪等性操做可使用該模式,如讀操做,由於每次調用的反作用是相同的,因此能夠選擇自動切換並重試調用,對調用者徹底透明。能夠看到,若是重試調用必然會帶來響應端的延遲,若是出現大量的重試調用,可能說明咱們的服務提供方發佈的服務有問題,如網絡延遲嚴重、硬件設備須要升級、程序算法很是耗時,等等,這就須要仔細檢測排查了。
例如,能夠這樣顯式指定Failover模式,或者不配置則默認開啓Failover模式,配置示例以下:
apache

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配置的屬性值。api

  • 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,內容以下所示:

相關文章
相關標籤/搜索