Dubbo系列(四)Dubbo之服务降级

1.开关 

        先讲一下开关的由来,例如京东在6月18日做店庆促销活动,在交易下单环节,可能需要调用A、B、C三个接口来完成,但是其实A和B是必须的,C只是附加的功能(例如在下单的时候做一下推荐),可有可无,在平时系统没有压力,容量充足的情况下,调用下没问题但是在类似店庆之类的大促环节,系统已经满负荷了,这时候其实完全可以不去调用C接口,怎么实现这个呢?改代码?no,no,no,这样太不敏捷,此时开关诞生了,开发人员只要简单执行一下命令或者点一下页面,就可以关掉对于C接口的调用,在大促过去之后,再把开关恢复回去即可。


2.什么是服务降级

          服务降级,当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。


3.服务降级方式:


  • 服务接口拒绝服务:无用户特定信息,页面能访问,但是添加删除提示服务器繁忙。页面内容也可在Varnish或CDN内获取。
  • 页面拒绝服务:页面提示由于服务繁忙此服务暂停。跳转到varnish或nginx的一个静态页面。
  •  延迟持久化:页面访问照常,但是涉及记录变更,会提示稍晚能看到结果,将数据记录到异步队列或log,服务恢复后执行。
  •  随机拒绝服务:服务接口随机拒绝服务,让用户重试,目前较少有人采用。因为用户体验不佳。
4.




5.



6.服务降级埋点的地方:


        消息中间件:所有API调用可以使用消息中间件进行控制
        前端页面:指定网址不可访问(NGINX+LUA)

        底层数据驱动:拒绝所有增删改动作,只允许查询


一、dubbo降级服务    

    dubbo开发中,可能由于服务没有启动或者网络不通,调用中会出现RpcException,也就是远程调用失败。如果是服务启动顺序的问题,可能加工check="false"的配置可以得到很好的解决。但是,如果是服务宕掉或者并发数太高导致的RpcException该如何处理?

    经过过12306抢票的人应该经常会遇到这个问题:在抢票高峰的时候,明明票还有,但是查询出来的列表却是为空的(如果没票列表也应该会呈现);等高峰过后再查询,列表又恢复正常。个人猜测应该是查询过程中出现了问题,要么超时,要么网络问题导致查询失败采用的服务降级处理。所以,最终呈现给用户的并不是内部系统出错之类的提示,而是一个空的列表。好了,言归正传,在dubbo中想实现服务降级,需要怎么样做可以实现?

    查看dubbo的官方文档,可以发现有个mock的配置,mock只在出现非业务异常(比如超时,网络异常等)时执行。mock的配置支持两种,一种为boolean值,默认的为false。如果配置为true,则缺省使用mock类名,即类名+Mock后缀;另外一种则是配置"return null",可以很简单的忽略掉异常。

二、结合dubbo的例子

说明下面将通过一个例子进行说明:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**接口定义*/
public  interface  IUser {
  
     public  void  addUser(User u);
      
     public  User getUserById( int  id);
      
}
  
/**实现类*/
public  class  UserImpl  implements  IUser {
      
     private  static  List<User> USER_LIST =  new  ArrayList<User>();
      
     static {
         for ( int  i= 0 ;i< 10 ;i++){
             User u =  new  User();
             u.setAddress( "address" +i);
             u.setId(i);
             u.setName( "name" +i);
              
             USER_LIST.add(u);
         }
     }
      
     public  void  addUser(User u) {
         USER_LIST.add(u);
         System.out.println( "total:" +USER_LIST.size());
     }
  
     public  User getUserById( int  id) {
         for ( int  i= 0 ;i<USER_LIST.size();i++){
             if (USER_LIST.get(i).getId() == id){
                 return  USER_LIST.get(i);
             }
         }
         return  null ;
     }
}


dubbo-provider.xml配置:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<? 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.xsd        http://code.alibabatech.com/schema/dubbo        http://code.alibabatech.com/schema/dubbo/dubbo.xsd" >
 
     <!-- 提供方应用信息,用于计算依赖关系 -->
     < dubbo:application  name = "hello-world-app"  />
 
     <!-- 使用multicast广播注册中心暴露服务地址 -->
     < dubbo:registry  address = "zookeeper://127.0.0.1:2181"  />
 
     <!-- 用dubbo协议在20880端口暴露服务 -->
     < dubbo:protocol  name = "dubbo"  port = "20880"  />
 
     <!-- 声明需要暴露的服务接口 -->
     < dubbo:service  interface = "com.zzq.test.iface.IUser"  ref = "userImpl"  timeout = "10000"  />
 
     <!-- 和本地bean一样实现服务 -->
     < bean  id = "userImpl"  class = "com.zzq.test.ifaceimpl.UserImpl"  />
 
</ beans >

调用方的配置:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<? 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.xsd        http://code.alibabatech.com/schema/dubbo        http://code.alibabatech.com/schema/dubbo/dubbo.xsd" >
  
     <!-- 消费方应用名,用于计算依赖关系,不是匹配条件,不要与提供方一样 -->
     < dubbo:application  name = "dubbo-consumer"   />
  
     < dubbo:registry  address = "zookeeper://127.0.0.1:2181"  />
  
     <!-- 生成远程服务代理,可以和本地bean一样使用demoService -->
     < dubbo:reference  id = "iUser"  interface = "com.zzq.test.iface.IUser"   timeout = "10000"  check = "false"  mock = "return null" >
     </ dubbo:reference >
  
</ beans >

调用的测试代码:

?
1
2
3
4
5
6
7
8
9
10
11
12
public  static  void  main(String[] args)  throws  Exception{
         ClassPathXmlApplicationContext context =  new  ClassPathXmlApplicationContext( new  String[] { "classpath:dubbo-consumer.xml" });
         context.start();
         
         IUser iUser = (IUser)context.getBean( "iUser" );
         User u =  new  User();
         u.setAddress( "aaa" );
         u.setId( 311 );
         u.setName( "n3" );
         iUser.addUser(u);
         System.out.println(iUser.getUserById( 1 ));
     }

通过测试,如果服务启动,则程序按照预期的运行正常;如果服务没启动,则此时运行程序,程序并未报错,打印出null。

三、思考

    通过以上的例子可以知道,通过mock的配置,可以很好的实现dubbo服务降级。但是,仔细查看上面的例子会发现,IUser本身定义了两个接口,一个是新增用户,一个是根据id查询用户信息。对于根据id查询用户信息,在调用失败的时候返回null很好理解,可能是由于验证失败或者记录删除了,但是对于新增用户,可能就需要抛出具体的业务信息,否则程序无法处理后续的业务,包括页面弹出”添加成功“或者列表刷新的时候无法查看到最新的记录,这样体验将会非常不好。所以,如果要有较好的区分,可以通过以下的方式,可以更好的实现降级:

(1)将接口进行归类,查询类和变更操作类:对于查询的分为一个接口类,变更的归类为其他的接口类,这样对于查询的可以使用mock="return null"进行降级操作;对于变更类的,可以仍旧使用try……catch进行异常捕获处理;

(2)配置mock="true",同时mock实现接口,接口名要注意命名规范:接口名+Mock后缀。此时如果调用失败会调用Mock实现。mock实现需要保证有无参的构造方法。

配置mock="true"的情况,对于上面的例子即在IUser的同个路径下,添加类IUserMock,实现如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
public  class  IUserMock  implements  IUser {
 
     @Override
     public  void  addUser(User u) {
         throw  new  RuntimeException( "add user fail!" );
     }
 
     @Override
     public  User getUserById( int  id) {
         return  null ;
     }
 
}