如何處理Dubbo調用超時?

原創:Java派(微信公衆號:Java派),歡迎分享,轉載請保留出處。java

前言

Dubbo是阿里開源的RPC框架,由於他基於接口開發支持負載均衡、集羣容錯、版本控制等特性,所以如今有不少互聯網公司都在使用Dubbo。bash

本文主要解決使用超時設置以及處理進行分析,Dubbo有三個級別的超時設置分別爲:微信

  • 針對方法設置超時時間
  • 在服務方設置超時時間
  • 在調用方設置超時時間

具體設置方法可參考Dubbo的官方文檔。Dubbo調用超時後會發生啥狀況呢?目前瞭解的會有兩種狀況:app

  1. 客戶端會收到一個TimeoutException異常
  2. 服務端會收到一個警告The timeout response finally returned at xxx

看起來還蠻正常的,可是實際上會有這樣問題:調用超時後服務端仍是會繼續執行,該如何處理呢? 爲了演示超時的狀況,先作了個服務:負載均衡

@Service(version = "1.0")
@Slf4j
public class DubboDemoServiceImpl implements DubboDemoService {
    public String sayHello(String name) {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        String result = "hello: " + 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<String> sayHello(@RequestParam("name") String name){
        return ResponseEntity.ok(demoService.sayHello(name));
    }
}
複製代碼

鏈接DubboDemoService服務使用的直連方式(dubbo://127.0.0.1:22888?timeout=2000),演示中的超時時間都由url中的timeout指定。異步

Consumer超時處理

前面提到發生調用超時後,客戶端會收到一個TimeoutException異常,服務端的sayHello實現中是休眠了3秒的:ide

public String sayHello(String name) {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        ...
}
複製代碼

而鏈接服務時指定的超時時間是2000ms,那確定會收到一個TimeoutException異常:ui

There was an unexpected error (type=Internal Server Error, status=500).
Invoke remote method timeout. method: sayHello, provider: dubbo://127.0.0.1:22888/com.example.dubbo.dubbodemo.service.DubboDemoService?application=dubbo-demo&default.check=false&default.lazy=false&default.sticky=false&dubbo=2.0.2&interface=com.example.dubbo.dubbodemo.service.DubboDemoService&lazy=false&methods=sayHello&pid=28662&qos.enable=false&register.ip=192.168.0.103&remote.application=&revision=1.0&side=consumer&sticky=false&timeout=2000&timestamp=1571800026289&version=1.0, cause: Waiting server-side response timeout. start time: 2019-10-23 11:13:00.745, end time: 2019-10-23 11:13:02.751, client elapsed: 5 ms, server elapsed: 2000 ms, timeout: 2000 ms, request: Request [id=4, version=2.0.2, twoway=true, event=false, broken=false, data=RpcInvocation [methodName=sayHello, parameterTypes=[class java.lang.String], arguments=[name], attachments={path=com.example.dubbo.dubbodemo.service.DubboDemoService, interface=com.example.dubbo.dubbodemo.service.DubboDemoService, version=1.0, timeout=2000}]], channel: /192.168.0.103:56446 -> /192.168.0.103:22888
複製代碼

客戶端超時處理比較簡單,既然發生了異常也能捕獲到異常那該回滾仍是不作處理,徹底能夠由開發者解決。url

try{
    return ResponseEntity.ok(demoService.sayHello(name));
}catch (RpcException te){
     //do something...
    log.error("consumer", te);
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR.value()).body("");
}
複製代碼

重點仍是解決服務方的超時異常。

Provider超時處理

Provider的處理就不像客戶端那樣簡單呢,由於Provider不會收到異常,並且線程也不會中斷,這樣就會致使Consumer超時數據回滾,而Providerder繼續執行最終執行完數據插入成功,數據不一致。

在演示項目中,Provider方法休眠3000ms且Consumer的超時是參數是2000ms,調用發生2000ms後就會發生超時,而Provider的sayHello方法不會中斷在1000ms後打印hello xx

很明顯要保持數據一致就須要在超時後,將Provider的執行終止或回滾才行,如何作到數據一致性呢?

重試機制

Dubbo自身有重試機制,調用超時後會發起重試,Provider端需考慮冪等性。

最終一致性

使用補償事務或異步MQ保持最終一致性,須要寫一些與業務無關的代碼來保持數據最終一致性。好比在Provider端加個check方法,檢查是否成功,具體實現還須要結合自身的業務需求來處理。

@GetMapping
public ResponseEntity<String> sayHello(@RequestParam("name") String name){
    try{
        return ResponseEntity.ok(demoService.sayHello(name));
    }catch (RpcException te){
         //do something...
        try{
            demoService.check(name);
        }catch (RpcException ignore){

        }
        log.error("consumer", te);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR.value()).body("");
    }
}
複製代碼

雖然能夠經過添加檢查來驗證業務狀態,可是這個調用執行時間是沒辦法準確預知的,因此這樣簡單的檢測是效果不大,最好仍是經過MQ來作這樣的檢測。

基於時間回滾

原理比較簡單,在Consumer端調用時設置兩個參數ctimettime分別表示調用時間、超時時間,將參數打包發給Provider收到兩個參數後進行操做,若是執行時間越過ttime則回滾數據,不然正常執行。改造下咱們的代碼:

public ResponseEntity<String> sayHello(@RequestParam("name") String name){
        try{
            RpcContext context = RpcContext.getContext();
            context.setAttachment("ctime", System.currentTimeMillis() + "");
            context.setAttachment("ttime", 2000 + "");

            return ResponseEntity.ok(demoService.sayHello(name));
        }catch (RpcException te){
             //do something...
            log.error("consumer", te);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR.value()).body("");
        }
    }
複製代碼

ctimettime兩個參數傳到Provider端處理:

public String sayHello(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 = "hello: " + name;
        log.info("Result: {}" , result);
        return result;
    }
複製代碼

畫個圖看一下執行的時間線:

從上圖在執行完成後,響應返回期間這段時間是計算不出來的,因此這種辦法也不能徹底解決Provider超時問題。

總結

文中提到的方法都不能很好的解決Provider超時問題,總的來講仍是要設計好業務代碼來減小調用時長,設置準確RPC調用的超時時間才能更好的解決這個問題。

相關文章
相關標籤/搜索