一、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 Cluster模式
? ? ?配置值為failover。這種模式是Dubbo集群容錯默認的模式選擇,調用失敗時,會自動切換,重新嘗試調用其他節點上可用的服務。對于一些冪等性操作可以使用該模式,如讀操作,因為每次調用的副作用是相同的,所以可以選擇自動切換并重試調用,對調用者完全透明。可以看到,如果重試調用必然會帶來響應端的延遲,如果出現大量的重試調用,可能說明我們的服務提供方發布的服務有問題,如網絡延遲嚴重、硬件設備需要升級、程序算法非常耗時,等等,這就需要仔細檢測排查了。
例如,可以這樣顯式指定Failover模式,或者不配置則默認開啟Failover模式,配置示例如下:
<dubbo:service interface="org.shirdrn.dubbo.api.ChatRoomOnlineUserCounterService" version="1.0.0"cluster="failover" retries="2" timeout="100" ref="chatRoomOnlineUserCounterService" protocol="dubbo" ><dubbo:method name="queryRoomUserCount" timeout="80" retries="2" />
</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即可,接口定義如下所示:
@SPI(FailoverCluster.NAME)
public interface Cluster {/*** Merge the directory invokers to a virtual invoker.* @param <T>* @param directory* @return cluster invoker* @throws RpcException*/@Adaptive<T> Invoker<T> join(Directory<T> directory) throws RpcException;}
? 關于如何實現一個自定義的集群容錯模式,可以參考Dubbo源碼中內置支持的汲取你容錯模式的實現,6種模式對應的實現類如下所示:
com.alibaba.dubbo.rpc.cluster.support.FailoverCluster
com.alibaba.dubbo.rpc.cluster.support.FailfastCluster
com.alibaba.dubbo.rpc.cluster.support.FailsafeCluster
com.alibaba.dubbo.rpc.cluster.support.FailbackCluster
com.alibaba.dubbo.rpc.cluster.support.ForkingCluster
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算法映射到虛擬節點上(其他服務提供方)
? ?在實際使用中,只需要選擇合適的負載均衡策略值,配置即可,下面是上述四種負載均衡策略配置的示例:
<dubbo:service interface="org.shirdrn.dubbo.api.ChatRoomOnlineUserCounterService" version="1.0.0"cluster="failover" retries="2" timeout="100" loadbalance="random"ref="chatRoomOnlineUserCounterService" protocol="dubbo" ><dubbo:method name="queryRoomUserCount" timeout="80" retries="2" loadbalance="leastactive" />
</dubbo:service>
? ?上述配置,也體現了Dubbo配置的繼承性特點,也就是dubbo:service元素配置了loadbalance=”random”,則該元素的子元素dubbo:method如果沒有指定負載均衡策略,則默認為loadbalance=”random”,否則如果dubbo:method指定了loadbalance=”leastactive”,則使用子元素配置的負載均衡策略覆蓋了父元素指定的策略(這里調用queryRoomUserCount方法使用leastactive負載均衡策略)。
? ?當然,Dubbo框架也提供了實現自定義負載均衡策略的接口,可以實現com.alibaba.dubbo.rpc.cluster.LoadBalance接口,接口定義如下所示:
/**
* LoadBalance. (SPI, Singleton, ThreadSafe)
*
* <a href="http://en.wikipedia.org/wiki/Load_balancing_(computing)">Load-Balancing</a>
*/
@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {/*** select one invoker in list.* @param invokers invokers.* @param url refer url* @param invocation invocation.* @return selected invoker.*/@Adaptive("loadbalance")<T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;}
?如何實現一個自定義負載均衡策略,可以參考Dubbo框架內置的實現,如下所示的3個實現類:
com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance
com.alibaba.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance
com.alibaba.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance
三、Dubbo服務集群容錯實踐
? ?手機應用是以聊天室為基礎的,我們需要收集用戶的操作行為,然后計算聊天室中在線人數,并實時在手機應用端顯示人數,整個系統的架構如圖所示:
?
? ? 上圖中,主要包括了兩大主要流程:日志收集并實時處理流程、調用讀取實時計算結果流程,我們使用基于Dubbo框架開發的服務來提供實時計算結果讀取聊天人數的功能。上圖中,實際上業務接口服務器集群也可以基于Dubbo框架構建服務,就看我們想要構建什么樣的系統來滿足我們的需要。
如果不使用注冊中心,服務消費方也能夠直接調用服務提供方發布的服務,這樣需要服務提供方將服務地址暴露給服務消費方,而且也無法使用監控中心的功能,這種方式成為直連。
? ? 如果我們使用注冊中心,服務提供方將服務發布到注冊中心,而服務消費方可以通過注冊中心訂閱服務,接收服務提供方服務變更通知,這種方式可以隱藏服務提供方的細節,包括服務器地址等敏感信息,而服務消費方只能通過注冊中心來獲取到已注冊的提供方服務,而不能直接跨過注冊中心與服務提供方直接連接。這種方式的好處是還可以使用監控中心服務,能夠對服務的調用情況進行監控分析,還能使用Dubbo服務管理中心,方便管理服務,我們在這里使用的是這種方式,也推薦使用這種方式。使用注冊中心的Dubbo分布式服務相關組件結構,如下圖所示:
? ?下面,開發部署我們的應用,通過如下4個步驟來完成:
- 服務接口定義
? ? ?服務接口將服務提供方(Provider)和服務消費方(Consumer)連接起來,服務提供方實現接口中定義的服務,即給出服務的實現,而服務消費方負責調用服務。我們接口中給出了2個方法,一個是實時查詢獲取當前聊天室內人數,另一個是查詢一天中某個/某些聊天室中在線人數峰值,接口定義如下所示:?
package org.shirdrn.dubbo.api;import java.util.List;public interface ChatRoomOnlineUserCounterService {String queryRoomUserCount(String rooms);List<String> getMaxOnlineUserCount(List<String> rooms, String date, String dateFormat);
}
接口是服務提供方和服務消費方公共遵守的協議,一般情況下是服務提供方將接口定義好后提供給服務消費方。
- 服務提供方
? ? 服務提供方實現接口中定義的服務,其實現和普通的服務沒什么區別,我們的實現類為ChatRoomOnlineUserCounterServiceImpl,代碼如下所示:
package org.shirdrn.dubbo.provider.service;import java.util.List;import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.shirdrn.dubbo.api.ChatRoomOnlineUserCounterService;
import org.shirdrn.dubbo.common.utils.DateTimeUtils;import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;import com.alibaba.dubbo.common.utils.StringUtils;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;public class ChatRoomOnlineUserCounterServiceImpl implements ChatRoomOnlineUserCounterService {private static final Log LOG = LogFactory.getLog(ChatRoomOnlineUserCounterServiceImpl.class);private JedisPool jedisPool;private static final String KEY_USER_COUNT = "chat::room::play::user::cnt";private static final String KEY_MAX_USER_COUNT_PREFIX = "chat::room::max::user::cnt::";private static final String DF_YYYYMMDD = "yyyyMMdd";public String queryRoomUserCount(String rooms) {LOG.info("Params[Server|Recv|REQ] rooms=" + rooms);StringBuffer builder = new StringBuffer();if(!Strings.isNullOrEmpty(rooms)) {Jedis jedis = null;try {jedis = jedisPool.getResource();String[] fields = rooms.split(",");List<String> results = jedis.hmget(KEY_USER_COUNT, fields);builder.append(StringUtils.join(results, ","));} catch (Exception e) {LOG.error("", e);} finally {if(jedis != null) {jedis.close();}}}LOG.info("Result[Server|Recv|RES] " + builder.toString());return builder.toString();}@Overridepublic List<String> getMaxOnlineUserCount(List<String> rooms, String date, String dateFormat) {// HGETALL chat::room::max::user::cnt::20150326LOG.info("Params[Server|Recv|REQ] rooms=" + rooms + ",date=" + date + ",dateFormat=" + dateFormat);String whichDate = DateTimeUtils.format(date, dateFormat, DF_YYYYMMDD);String key = KEY_MAX_USER_COUNT_PREFIX + whichDate;StringBuffer builder = new StringBuffer();if(rooms != null && !rooms.isEmpty()) {Jedis jedis = null;try {jedis = jedisPool.getResource();return jedis.hmget(key, rooms.toArray(new String[rooms.size()]));} catch (Exception e) {LOG.error("", e);} finally {if(jedis != null) {jedis.close();}}}LOG.info("Result[Server|Recv|RES] " + builder.toString());return Lists.newArrayList();}public void setJedisPool(JedisPool jedisPool) {this.jedisPool = jedisPool;}}
? ?代碼中通過讀取Redis中數據來完成調用,邏輯比較簡單。對應的Maven POM依賴配置,如下所示:
<dependencies><dependency><groupId>org.shirdrn.dubbo</groupId><artifactId>dubbo-api</artifactId><version>0.0.1-SNAPSHOT</version></dependency><dependency><groupId>org.shirdrn.dubbo</groupId><artifactId>dubbo-commons</artifactId><version>0.0.1-SNAPSHOT</version></dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>2.5.2</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId><version>2.2</version></dependency><dependency><groupId>org.jboss.netty</groupId><artifactId>netty</artifactId><version>3.2.7.Final</version></dependency>
</dependencies>
? ?有關對Dubbo框架的一些依賴,我們單獨放到一個通用的Maven Module中(詳見后面“附錄:Dubbo使用Maven構建依賴配置”),這里不再多說。服務提供方實現,最關鍵的就是服務的配置,因為Dubbo基于Spring來管理配置和實例,所以通過配置可以指定服務是否是分布式服務,以及通過配置增加很多其它特性。我們的配置文件為provider-cluster.xml,內容如下所示:
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"xmlns:p="http://www.springframework.org/schema/p"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsdhttp://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd"><bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"><property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_OVERRIDE" /><property name="ignoreResourceNotFound" value="true" /><property name="locations"><list><value>classpath*:jedis.properties</value></list></property></bean><dubbo:application name="chatroom-cluster-provider" /><dubbo:registry address="zookeeper://zk1:2181?backup=zk2:2181,zk3:2181" /><dubbo:protocol name="dubbo" port="20880" /><dubbo:service interface="org.shirdrn.dubbo.api.ChatRoomOnlineUserCounterService" version="1.0.0"cluster="failover" retries="2" timeout="1000" loadbalance="random" actives="100" executes="200"ref="chatRoomOnlineUserCounterService" protocol="dubbo" ><dubbo:method name="queryRoomUserCount" timeout="500" retries="2" loadbalance="roundrobin" actives="50" /></dubbo:service><bean id="chatRoomOnlineUserCounterService" class="org.shirdrn.dubbo.provider.service.ChatRoomOnlineUserCounterServiceImpl" ><property name="jedisPool" ref="jedisPool" /></bean><bean id="jedisPool" class="redis.clients.jedis.JedisPool" destroy-method="destroy"><constructor-arg index="0"><bean class="org.apache.commons.pool2.impl.GenericObjectPoolConfig"><property name="maxTotal" value="${redis.pool.maxTotal}" /><property name="maxIdle" value="${redis.pool.maxIdle}" /><property name="minIdle" value="${redis.pool.minIdle}" /><property name="maxWaitMillis" value="${redis.pool.maxWaitMillis}" /><property name="testOnBorrow" value="${redis.pool.testOnBorrow}" /><property name="testOnReturn" value="${redis.pool.testOnReturn}" /><property name="testWhileIdle" value="true" /></bean></constructor-arg><constructor-arg index="1" value="${redis.host}" /><constructor-arg index="2" value="${redis.port}" /><constructor-arg index="3" value="${redis.timeout}" /></bean></beans>
上面配置中,使用dubbo協議,集群容錯模式為failover,服務級別負載均衡策略為random,方法級別負載均衡策略為roundrobin(它覆蓋了服務級別的配置內容),其他一些配置內容可以參考Dubbo文檔。我們這里是從Redis讀取數據,所以使用了Redis連接池。
啟動服務示例代碼如下所示:
package org.shirdrn.dubbo.provider;import org.shirdrn.dubbo.provider.common.DubboServer;public class ChatRoomClusterServer {public static void main(String[] args) throws Exception {DubboServer.startServer("classpath:provider-cluster.xml");}}
上面調用了DubboServer類的靜態方法startServer,如下所示:
public static void startServer(String config) {ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(config);try {context.start();System.in.read();} catch (IOException e) {e.printStackTrace();} finally {context.close();}
}
方法中主要是初始化Spring IoC容器,全部對象都交由容器來管理。
- 服務消費方
? ?服務消費方就容易了,只需要知道注冊中心地址,并引用服務提供方提供的接口,消費方調用服務實現如下所示:
package org.shirdrn.dubbo.consumer;import java.util.Arrays;
import java.util.List;import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.shirdrn.dubbo.api.ChatRoomOnlineUserCounterService;
import org.springframework.context.support.AbstractXmlApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;public class ChatRoomDubboConsumer {private static final Log LOG = LogFactory.getLog(ChatRoomDubboConsumer.class);public static void main(String[] args) throws Exception {AbstractXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:consumer.xml");try {context.start();ChatRoomOnlineUserCounterService chatRoomOnlineUserCounterService = (ChatRoomOnlineUserCounterService) context.getBean("chatRoomOnlineUserCounterService"); getMaxOnlineUserCount(chatRoomOnlineUserCounterService); getRealtimeOnlineUserCount(chatRoomOnlineUserCounterService); System.in.read();} finally {context.close();}}private static void getMaxOnlineUserCount(ChatRoomOnlineUserCounterService liveRoomOnlineUserCountService) {List<String> maxUserCounts = liveRoomOnlineUserCountService.getMaxOnlineUserCount(Arrays.asList(new String[] {"1482178010" , "1408492761", "1430546839", "1412517075", "1435861734"}), "20150327", "yyyyMMdd");LOG.info("After getMaxOnlineUserCount invoked: maxUserCounts= " + maxUserCounts);}private static void getRealtimeOnlineUserCount(ChatRoomOnlineUserCounterService liveRoomOnlineUserCountService)throws InterruptedException {String rooms = "1482178010,1408492761,1430546839,1412517075,1435861734";String onlineUserCounts = liveRoomOnlineUserCountService.queryRoomUserCount(rooms);LOG.info("After queryRoomUserCount invoked: onlineUserCounts= " + onlineUserCounts);}
}
對應的配置文件為consumer.xml,內容如下所示:
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:dubbo="http://code.alibabatech.com/schema/dubbo"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsdhttp://code.alibabatech.com/schema/dubbo http://code.alibabatech.com/schema/dubbo/dubbo.xsd"><dubbo:application name="chatroom-consumer" /><dubbo:registry address="zookeeper://zk1:2181?backup=zk2:2181,zk3:2181" /><dubbo:reference id="chatRoomOnlineUserCounterService" interface="org.shirdrn.dubbo.api.ChatRoomOnlineUserCounterService" version="1.0.0"><dubbo:method name="queryRoomUserCount" retries="2" /></dubbo:reference></beans>
也可以根據需要配置dubbo:reference相關的屬性值,也可以配置dubbo:method指定調用的方法的配置信息,詳細配置屬性可以參考Dubbo官方文檔。
? 原文鏈接請參見:http://shiyanjun.cn/archives/1075.html