1、参考
启动检查:http://dubbo.apache.org/en-us/docs/user/demos/preflight-check.html
容错:http://dubbo.apache.org/en-us/docs/user/demos/fault-tolerent-strategy.html
负载均衡:http://dubbo.apache.org/en-us/docs/user/demos/loadbalance.html
Dubbo中的Consumer端通过网络调用接口在Provider端的具体实现或者说是服务。因为网络是不稳定、不可靠的,另外Provider端的服务也可能因为各种其它原因变得不可用,Consumer端自然需要适当的处理这种情况。
在大的方面,Dubbo提供了两种机制,一种是事前检查:Provider是否可用,另一种是事后容错:发生Rpcexception时怎么办。
2、启动检查
事前检查也称为启动检查,意思是在Consumer端真正的调用Provider端具体实现之前,先检查一下Provider端是不是在线(网络是不是通的?Provider中的服务是否启动等),如果不在线则抛出异常。这样可以提前发现问题,避免Consumer端的任务进行到中途却发现某个服务不可用。
与启动检查相关的配置主要是在Consumer端:
<dubbo:consumer check = "false" />
这个标签是Consumer端引用的所有服务的缺省配置。
<dubbo:reference interface = "com.foo.BarService" check = "true" />
这个是某个具体服务的配置,这里是true,优先级要高一些,会覆盖掉<dubbo:consumer check="false"/>。
假如有如下Consumer端代码:
public class Consumer {
public static void main(String[] args) {
@SuppressWarnings("resource")
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
new String[] { "META-INF/spring/dubbo-demo-consumer.xml" });
context.start();
// Obtaining a remote service proxy
DemoService demoService = (DemoService) context.getBean("demoService");
// Executing remote methods
String hello = demoService.sayHello("world");
// Display the call result
System.out.println(hello);
}
}
如果配置成true或者缺省:
<dubbo:reference id="demoService" check="true"
interface="org.apache.dubbo.demo.DemoService" />
如果在启动运行Consumer时Provider不可用,则在执行代码:DemoService demoService = (DemoService) context.getBean("demoService");
时就会报错,并且返回一个NULL。
如果改成如下配置:
<dubbo:reference id="demoService" check="false"
interface="org.apache.dubbo.demo.DemoService" />
则在执行代码:DemoService demoService = (DemoService) context.getBean("demoService");
时不会报错,并且返回一个非空引用,Dubbo在后台会试图使这个非空引用变得真正有效。
如果在调用代码:String hello = demoService.sayHello("world");
时,也就是真正的调用Provider端代码时,如果服务还是不可用,这个时候才会报错,当然调用过程实际会包含后边说的容错逻辑。
我理解启动检查意义不是太大。启动检查时服务是可用的,但真正使用服务还是不能保证它仍然是可用的,网络服务本身它就不稳定,在调用它之前实际上没有办法确认它到底可用还是不可用,相对有效的处理方法是事后容错。
3、容错机制
可以想办法降低错误发生的概率,但是无论如何,错误的发生是不可避免的,所谓容错机制,就是错误发生后怎么办,如何把这个错误尽量修正过来。
实现容错的核心是多实例部署并组成集群,一个实例出错,就让另一个实例顶上去。
上图来自Dubbo官网,大概是它容错机制的概念图。首先要有一个基本的认识,Dubbo中的容错机制主要运行在Consumer端。Provider端可以提供多个服务实例(注册到Registry中),当服务实例的个数或者配置有变化时,Registry可以主动推送消息通知Consumer端。至于Consumer端如何使用多个实例、如何容错、如何负载均衡那就是Consumer端自己的事情。所以上图中的概念主要是Consumer端的概念。
各节点含义如下:
- Invoker:一个Invoker代表一个可调用的服务实例,它内部包括服务的地址、接口。有了这两个东西,Consumer端就知道如何定位这个服务实例了。在多实例部署中,将包含多个这样的Invoker。
- Directory:这个东西代表多个Invoker实例的列表。Registry推送一些变更消息,因此Consumer端的这个东西是动态变化的。
- Cluster将Directory中的多个物理的Invoker伪装成一个逻辑上的接口,容错逻辑主要在Cluster中实现。我们在写Consumer端代码时会调用某个接口,其实这个接口是一个Invoker集群。
- Router:路由器,根据各种条件对Cluster进行一次过滤。简单的例子,如果某个VIP用户在调用服务,Router可以识别到这是VIP用户,应当尽量从Cluster中选择专用的、高性能的Invoker为用户服务。反之如果是普通用户,那就选择普通的Invoker为用户服务。大概理解就是条件过滤。
- LoadBalance:负载均衡算法,从Router过滤后的多个Invoker中选一个出来并调用之。
3.1、Failover Cluster
默认容错策略,简单说就是Cluster中有至少一个可用Provider,如果一个失败,就换一个。一般多应用于读操作场景,因为多次读操作不会产生副作用。
Dubbo默认实现的重试机制比较简单,只要Rpc调用抛出RpcException异常,那么就会发起重试。实际上RpcException有很多具体类型,它的触发原因很多,有网络拥堵超时、网络被管理员禁止、Provider端代码异常、Consumer端本地异常等。细一点的话应该是根据不同的触发原因,采用不同的重试算法(是否有必要重试?重试间隔是否需要逐渐拉大等),应该根据具体业务扩展一下Dubbo。如果使用不当,重试机制可能会白白浪费资源,甚至在高负载时进步引起性能恶化。
Dubbo会记住失败的Invoker,并在重试时尽量将失败的Invoker排除在外。如果在重试时发现除了失败的invoker外,没有其它Invoker了,那么也就只好再一次调用失败过的Invoker了。
重试次数可以配置,默认是2,第一次不算重试。也就是说如果重试次数是2,那么总共可以发起三次调用。如果是小于等于0,那就是只调用一次,不进行重试。重试次数可以在多个地方配置,优先级由低到高:
<dubbo:service retries="2" />
这个是provider端配置。
<dubbo:reference retries="2" />
这个是Consumer端在引用服务时的配置。
<dubbo:reference>
<dubbo:method name="findFoo" retries="2" />
</dubbo:reference>
这个是在Consumer端为某个服务中的具体方法配置重试次数,相对优先级最高。
总结:失败重试模式是最常用、最重要的容错机制。
3.2、Failfast Cluster
只调用一次,不重试,如果调用失败则立即抛出异常,多用于非幂等的写操作。配置如下:
<dubbo:service cluster="failfast" />
<dubbo:reference cluster="failfast" />
如果一个Service中幂等与非幂等操作都有,有的需要重试、有的不需要,Failfast Cluster似乎无法应对这种情况,不如Failover Cluster灵活,因为后者可以将不能重试的非幂等操作的重试次数设置成0,这样就相当于Failfast Cluster效果。
总结:不够灵活,完全可用Failover Cluster代替。这真是个多余的玩意,就是不重试、重试次数为零,起了一个“快速失败”的名而已。
3.3、Failsafe Cluster
配置与Failfast Cluster类似,只需把前者中的"failfast"替换成"failsafe”即可,本质上就是忽略Rpc调用过程中发生的异常,多用于能够容忍失败的操作中,如审计日志的提交。这个名字起的也很奇怪,就是忽略异常,为什么要叫成“失败安全”,叫Failignore好像更直观。
总结:没什么用处。假如在实际中遇到审计日志提交这种不十分在意失败的操作,直接用Failover Cluster,将方法的重试次数设置成0。然后在调用方法时自己捕获Rpc异常,如果在代码中忽略异常,那就相当于是Failsafe Cluster模式,当然也可以选择不忽略异常,灵活得多。
3.4、Failback Cluster
如果调用失败,后台自动按固定间隔重试,调用者无需等待重试成功,一般多用于非实时性的消息传递。配置与Failfast相似,只需把"Failfast"按成"Failback"。
总结:又一个没什么用处的玩意,对于消息传递,多采用专业的消息中间件,功能强大的多。
3.5、Forking Cluster
同时对多个Provider发起调用,没有出错、成功返回最快的那个Provider生效,其它忽略。其实这种模式与容错关系不是太大,主要用于提供操作的实时性,但是会浪费资源。同样,配置的时候就是换个单词。
总结:没什么用处,用这种方式提高响应速度效率太低,只不过是简单的资源堆叠的懒汉方式。
4、负载均衡
Dubo提供多种负载均衡策略,并可自定义扩展,可在多处配置,以下优先级由低到高。一般应该由Provider端决定负载均衡策略,Consumer只管使用。
Provider端服务:
<dubbo:service interface="..." loadbalance="roundrobin" />
Consumer端服务:
<dubbo:reference interface="..." loadbalance="roundrobin" />
Provider端方法:
<dubbo:service interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:service>
Consumer端方法:
<dubbo:reference interface="...">
<dubbo:method name="..." loadbalance="roundrobin"/>
</dubbo:reference>
4.1、Random LoadBalance
默认策略,根据权重值随机选择服务实例,权重越高,则被选中的概率越大。如A\B\C三个服务实例,都实现同一接口。权重分别为10、20、30,则总的权重值为60,三个实例被选中的概率分别为1/6、2/6、3/6。权重值配置如下:
<dubbo:service interface="..." weight="10" loadbalance="roundrobin" />
优点:此策略简单实用,当调用次数很多时,调用会按服务实例的权重值分布。可灵活调整权重值,改变服务实例被调用的次数。
缺点:短时间内容易发生碰撞。比如,上例中A实例被调用的概率是1/6,最低,但是接下来的60次调用,极端情况下,可能是A连续10次,B连续20次,C连续30次。总体上按权重分布了,但在短时间内却集中到了单个服务实例上,这就是碰撞。
4.2、RoundRobin LoadBalance
RoundRobin用来解决Random的碰撞问题。还是以上例说明,它总体上保证调用按权重分布,同时又可避免碰撞,对某个服务实例的调用就是不连续。
4.3、LeastActive LoadBalance
以上两种方式中,负载策略是一成不变的。还是刚才的例子,A\B\C三个服务实例,权重分别为10、20、30,那么一定的,C实例被调用的次数会最多。但是一个服务实例的响应能力并非固定不变,因为某些原因,比如说网络,导致它在某个时间段内的响应能力变差,不如A与B,但是按上边两种负载均衡策略,仍然会有最多的调用发往C。
LeastActive也就是最少活跃数则用来解决此问题,与上述两种方式都不一样。首先Consumer端发起调用时会计数调用次数,结束调用时会计数结束次数。还是A\B\C三个服务实例,Consumer端对它们分别发起来了10、15、25次调用(计数器计下来),当调用完成后也会计下来,结束的次数分别是6、10、23次,则LeastActive数据分别为10-6=4、15-10=5、25-23=2,则C的活跃数最少,为2,那么接下来的调用还是会发往C。如果其它两个实例的最少活跃数也是2,那么就按Random方式调用。
4.4、ConsistentHash LoadBalance
哈希一致性负载均衡策略,基本原理就是调用Provider端服务的方法时,计算参数的Hash值,由计算出来的值决定调用那一个服务实例。参与Hash值计算的参数可以设置,默认是第一个参数,如:
<dubbo:parameter key="hash.arguments" value="0,1" />
这样的话就是前两个参数参与计算。此策略一般与分布式缓存有关,以前发往那个服务实例,现在应该还是发往这个服务实例,因为上边有缓存。
Hash一致性算法有一个虚拟节点的概念,主要目的是为了防止发生调用倾斜,Dubbo默认虚拟节点的个数是160,配置方法如下:
<dubbo:parameter key="hash.nodes" value="320" />
假如现在有3个真实的服务实例A\B\C,有12个虚拟节点,则虚拟节点与物理节点之间的对应关系可能是:
(1~4):A
(5~8):B
(9~12):C
当调用发生时,计算出的Hash值属于1~4时就调用A,同理调用B或者C。假如现在少了一个节点C,它变得不可用了,则对应关系可能会变成下边这样:
(1~4,9~10):A
(5~8,11~12):B
本来发往C的由A与B均分,A与B原来缓存的数据仍然有效,C损失的缓存由A与B分摊重建。
同理如果过了一段时间C又上线了,则又会恢复成:
(1~4):A
(5~8):B
(9~12):C
则A上的9、10缓存与B上的11、12失效,每人都损失一小部分,损失的部分重新由C重建。
总之Hash一致性策略往往与分布式缓存有关,缓存的节点是会变化的,有可能增多有可能减少。算法的目的就是保证数据均匀分布,当节点个数变化时尽量减少损失,过渡尽量平滑。