如何处理Dubbo调用超时

一、简述

同步调用是一种阻塞式的调用方式,即 Consumer 端代码一直阻塞等待,直到 Provider 端返回为止。dubbo默认的协议是netty, Netty 是 NIO 异步通讯机制,那么服务调用是怎么转化为同步的呢?Dubbo是阿里开源的RPC框架,因为基于接口开发支持负载均衡、集群容错、版本控制等特性,因此现在有很多互联网公司都在使用Dubbo。
1️⃣Dubbo有三个级别的超时设置分别为:
①针对方法设置超时时间
②在服务方设置超时时间
③在调用方设置超时时间

2️⃣一般超时是调用端发生在请求发出后,无法在指定的时间内获得对应的响应。原因大概有以下几种情况:
①服务端确实处理比较慢,无法在指定的时间返回结果,调用端就自动返回一个超时的异常响应来结束此次调用。
②服务端如果响应的比较快,但当客户端 Load 很高,负载压力很大的时候,会因为客户端请求发不出去、响应卡在 TCP Buffer 等问题,造成超时。因为客户端接收到服务端发来的数据或者请求服务端的数据,都会在系统层面排队,如果系统负载比较高,在内核态的时间占比就会加长,从而造成客户端获取到值时已经超时。
③通常是业务处理太慢,可在服务提供方机器上执行:jstack [PID] > jstack.log 分析线程都卡在哪个方法调用上,这里就是慢的原因。如果不能调优性能,请调高 timeout 阈值。

3️⃣排查和解决步骤
①两边可能有 GC,检查服务端和客户端 GC 日志,耗时很长的 GC,会导致超时。超时的发生很可能意味着调用端或者服务端的资源(CPU、内存或者网络)出现了瓶颈,需要检查服务端的问题还是调用端的问题来排除GC抖动等嫌疑。
②检查服务端的网络质量,比如重传率来排除网络嫌疑。
③借助链路跟踪的分析服务(比如阿里的 ARMS,开源的 OpenTracing 系的实现 Zipkin、SkyWalking 等)来分析下各个点的耗时情况。

4️⃣Dubbo调用超时(client-side timeout)后会有两种情况:
①客户端会收到一个TimeoutException异常
②服务端会收到一个警告The timeout response finally returned at xxx
看起来还蛮正常的,但是实际上会有这样问题:调用超时后服务端还是会继续执行,该如何处理呢?

@Service(version = "1.0")
@Slf4j
public class DubboDemoServiceImpl implements DubboDemoService {
  public String sayHi(String name) {
    try {
      Thread.sleep(3000);
     } catch (InterruptedException e) {
      throw new RuntimeException(e);
     }
     String result = "hi: " + name;
     log.info("Result: {}" , result);
     return result;
  }
}

服务非常简单,三秒后返回字符串。controller层:

@RestController
@RequestMapping
public class DubboDemoController {
  @Reference(url = "dubbo://127.0.0.1:22888?timeout=2000", version = "1.0")
  private DubboDemoService demoService;
  @GetMapping
  public ResponseEntity sayHi(@RequestParam("name") String name){
    return ResponseEntity.ok(demoService.sayHi(name));
  }
}

连接DubboDemoService服务使用的直连方式dubbo://127.0.0.1:22888?timeout=2000,演示中的超时时间都由url中的timeout指定。

二、Consumer超时处理

前面服务端的sayHi()实现休眠3秒,而连接服务时指定的超时时间是2000ms,那肯定会收到一个TimeoutException异常:

There was an unexpected error (type=Internal Server Error, status=500).
Invoke remote method timeout. method: sayHi

客户端超时处理比较简单,既然发生了异常也能捕获到异常那是该回滚还是不做处理,完全可以由开发者解决。

try{
  return ResponseEntity.ok(demoService.sayHi(name));
}catch (RpcException te){
  //do something...
  log.error("consumer", te);
  return msg;
}

重点还是解决服务方的超时异常。

三、Provider超时处理

Provider的处理没有客户端那样简单,因为Provider不会收到异常,而且线程也不会中断,这样就会导致Consumer超时数据回滚,而Provider继续执行最终执行完数据插入成功,数据不一致。

上面Provider方法休眠3000ms且Consumer的超时是参数是2000ms。调用发生2000ms后就会发生超时,而Provider的sayHi()不会中断在1000ms后打印hi xx。
如何处理Dubbo调用超时_第1张图片

很明显要保持数据一致就需要在超时后,将Provider的执行终止或回滚才行,如何做到数据一致性呢?

1️⃣重试机制
Dubbo自身有重试机制,调用超时后会发起重试,Provider端需考虑幂等性。

2️⃣最终一致性
使用补偿事务或异步MQ保持最终一致性,需要写一些与业务无关的代码来保持数据最终一致性。比如在Provider端加个check方法,检查是否成功,具体实现还需要结合自身的业务需求来处理。

@GetMapping
public ResponseEntity sayHi(String name){
   try{
       return ResponseEntity.ok(demoService.sayHi(name));
    }catch (RpcException te){
       //do something...
       try{
          demoService.check(name);
       }catch (RpcException ignore){
       }
       log.error("consumer", te);
       return msg;
    }
}

虽然可以通过添加检查来验证业务状态,但是这个调用执行时间是没办法准确预知的,所以这样简单的检测是效果不大,最好还是通过MQ来做这样的检测。

3️⃣基于时间回滚
原理比较简单,在Consumer端调用时设置两个参数ctime、ttime分别表示调用时间、超时时间,将参数打包发给Provider。Provider收到两个参数后进行操作,如果执行时间越过ttime则回滚数据,否则正常执行。改造下代码:

public ResponseEntity sayHi(@RequestParam("name") String name){
   try{
      RpcContext context = RpcContext.getContext();
      context.setAttachment("ctime", System.currentTimeMillis() + "");
      context.setAttachment("ttime", 2000 + "");
      return ResponseEntity.ok(demoService.sayHi(name));
   }catch (RpcException te){
      //do something...
      log.error("consumer", te);
      return msg;
   }
 }

将ctime、ttime两个参数传到Provider端处理:

public String sayHi(String name) {
   long curTime = System.currentTimeMillis();
   String ctime = RpcContext.getContext().getAttachment("ctime");
   String ttime = RpcContext.getContext().getAttachment("ttime");
   long ctimeAsLong = Long.parseLong(ctime);
   long ttimeAsLong = Long.parseLong(ttime);
   try {
     Thread.sleep(3000);
   } catch (InterruptedException e) {
     throw new RuntimeException(e);
   }
   long spent = System.currentTimeMillis() - curTime;
   if(spent >= (ttimeAsLong - ctimeAsLong - curTime)){
     throw new RpcException("Server-side timeout.");
   }
     String result = "hi: " + name;
     log.info("Result: {}" , result);
     return result;
 }

画个图看一下执行的时间线:
如何处理Dubbo调用超时_第2张图片

从上图在执行完成后,响应返回期间这段时间是计算不出来的,所以这种办法也不能完全解决Provider超时问题。

四、dubbo中配置的优先级

dubbo作为一个服务治理框架,功能相对比较完善,性能也挺不错。要知道dubbo中配置是有优先级的,以免出现调优参数设置了却没发现效果实际是配置被覆盖导致这样的问题。dubbo分为consumer和provider端,在配置各个参数时,其优先级如下:

1、consumer的method配置
2、provider的method配置
3、consumer的reference配置
4、provider的service配置
5、consumer的consumer节点配置
6、provider的provider节点配置

可以看到,方法级的配置优先级高于接口级,consumer的优先级高于provider。同时,在本地参数配置还存在一层优先级:

1、系统参数(-D),如-Ddubbo.protocol.port=20003
2、xml配置
3、property文件

了解了这两个优先级,调优起来才会更加清晰,省去了一些诸如配置设置了不生效这样的麻烦。注意,其实dubbo中还可以通过将配置写入注册中心的方式覆盖用户配置(优先级高于系统参数)。

你可能感兴趣的:(如何处理Dubbo调用超时)