gRPC請求超時和異常處理

1. 請求超時

在 HTTP 請求中,我們發送請求的時候,可以設置一個請求超時時間-connectTimeout,即在指定的時間內,如果請求沒有到達服務端,爲了避免客戶端一直進行不必要的等待,就會拋出一個請求超時異常。

但是在微服務系統中,我們卻很少設置請求超時時間,一般都是用另外一個概念代替,那就是請求截止時間。

這是什麼原因呢?今天我們就來簡單聊一聊這個話題。

在微服務中我們客戶端的請求在服務端往往會有比較複雜的鏈條,我想起來 Spring Cloud Sleuth 官方給的一個請求鏈路追蹤的圖,我們直接拿來看下:

這張圖中,請求從客戶端發起之後,在服務端一共經歷了四個 SERVICE,對於這樣的請求,如果我們還是按照之前發送普通 HTTP 請求的方式,設置一個 connectTimeout 顯然是不夠的。

我舉個例子:

假設我們發送一個請求,爲該請求設置 connectTimeout 爲 5s,那麼這個時間只對第一個服務 SERVICE1 有效,也就是請求在 5s 之內沒有到達 SERVICE1,那麼就會拋出連接超時異常;請求如果在 5s 之內到達 SERVICE1,那麼就不會拋出異常,但是!!!,請求到達 SERVICE1 並不意味着請求結束,後面從 SERVICE1 到 SERVICE2,從 SERVICE2 到 SERVICE3,從 SERVICE3 到 SERVICE4,還有四個 HTTP 請求待處理,這些請求超時了怎麼辦?很明顯,connectTimeout 屬性對於後面幾個請求就鞭長莫及了。

所以,對於這種場景,我們一般使用截止時間來處理。

截止時間相當於設置整個請求生命週期的時間,也就是這個請求,我要多久拿到結果。很明顯,這個時間應該在客戶端發起請求的時候設置。

gRPC 中提供了對應的方法,我們可以非常方便的設置請求的截止時間 DeadLineTime,如下:

public class LoginClient {
    public static void main(String[] args) throws InterruptedException {
        ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
                .usePlaintext()
                .build();
        LoginServiceGrpc.LoginServiceStub stub = LoginServiceGrpc.newStub(channel).withDeadline(Deadline.after(3, TimeUnit.SECONDS));
        login(stub);
    }

    private static void login(LoginServiceGrpc.LoginServiceStub stub) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        stub.login(LoginBody.newBuilder().setUsername("javaboy").setPassword("123").build(), new StreamObserver<loginresponse>() {
            @Override
            public void onNext(LoginResponse loginResponse) {
                System.out.println("loginResponse.getToken() = " + loginResponse.getToken());
            }

            @Override
            public void onError(Throwable throwable) {
                System.out.println("throwable = " + throwable);
            }

            @Override
            public void onCompleted() {
                countDownLatch.countDown();
            }
        });
        countDownLatch.await();
    }
}

服務端通過 Thread.sleep 做個簡單的休眠就行了,超時之後,客戶端的 onError 方法會被觸發,拋出如下異常:

throwable = io.grpc.StatusRuntimeException: DEADLINE_EXCEEDED: deadline exceeded after 2.939621462s. [closed=[], open=[[buffered_nanos=285550823, remote_addr=localhost/127.0.0.1:50051]]]

2. 服務端處理異常

在之前的幾篇文章中,其實我們也遇到過異常問題,只是當時沒有和小夥伴們細說,只是囫圇吞棗寫了一個案例而已,今天我們就來把這個話題跟小夥伴們仔細捋一捋。

我們之前寫過一個登錄的案例,在之前的案例中,如果用戶在登錄時輸入了錯誤的用戶名密碼的話,那麼我們是通過一個普通的數據流返回異常信息,其實,對於異常信息,我們可以通過專門的異常通道來寫回到客戶端。

先來看看服務端如何處理異常。

還是以我們之前的 gRPC 登錄案例爲例,我們修改服務端的登錄邏輯如下(完整代碼小夥伴們可以參考之前的 手把手教大家在 gRPC 中使用 JWT 完成身份校驗 一文):

public class LoginServiceImpl extends LoginServiceGrpc.LoginServiceImplBase {
    @Override
    public void login(LoginBody request, StreamObserver<loginresponse> responseObserver) {
        String username = request.getUsername();
        String password = request.getPassword();
        if ("javaboy".equals(username) &amp;&amp; "123".equals(password)) {
            System.out.println("login success");
            //登錄成功
            String jwtToken = Jwts.builder().setSubject(username).signWith(AuthConstant.JWT_KEY).compact();
            responseObserver.onNext(LoginResponse.newBuilder().setToken(jwtToken).build());
            responseObserver.onCompleted();
        }else{
            System.out.println("login error");
            //登錄失敗
            responseObserver.onError(Status.UNAUTHENTICATED.withDescription("login error").asException());
        }
    }
}

小夥伴們看到,在登錄失敗時我們通過 responseObserver.onError 方法將異常信息寫回到客戶端。這個方法的參數是一個 Throwable 對象,對於這個對象,在 Status 這個枚舉類中定義了一些常見的值,分別如下:

  • OK(0):請求成功。
  • CANCELLED(1):操作被取消。
  • UNKNOWN(2):未知錯誤。
  • INVALID_ARGUMENT(3):客戶端給了無效的請求參數。
  • DEADLINE_EXCEEDED(4):請求超過了截止時間。
  • NOT_FOUND(5):請求資源未找到。
  • ALREADY_EXISTS(6):添加的內容已經存在。
  • PERMISSION_DENIED(7):請求權限不足。
  • RESOURCE_EXHAUSTED(8):資源耗盡。
  • FAILED_PRECONDITION(9):服務端上爲準備好。
  • ABORTED(10):請求被中止。
  • OUT_OF_RANGE(11):請求超出範圍。
  • UNIMPLEMENTED(12):未實現的操作。
  • INTERNAL(13):服務內部錯誤。
  • UNAVAILABLE(14):服務不可用。
  • DATA_LOSS(15):數據丟失或者損毀。
  • UNAUTHENTICATED(16):請求未認證。

系統默認給出的請求類型大致上就這些。當然,如果這些並不能滿足你的需求,我們也可以擴展這個枚舉類。

3. 客戶端處理異常

當服務端給出異常信息之後,客戶端的處理分爲兩種情況。

3.1 異步請求

如果客戶端是異步請求,則直接在異常回調中處理即可,如下:

public class LoginClient {
    public static void main(String[] args) throws InterruptedException {
        ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
                .usePlaintext()
                .build();
        LoginServiceGrpc.LoginServiceStub stub = LoginServiceGrpc.newStub(channel).withDeadline(Deadline.after(3, TimeUnit.SECONDS));
        login(stub);
    }
    private static void login(LoginServiceGrpc.LoginServiceStub stub) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        stub.login(LoginBody.newBuilder().setUsername("javaboy").setPassword("1234").build(), new StreamObserver<loginresponse>() {
            @Override
            public void onNext(LoginResponse loginResponse) {
                System.out.println("loginResponse.getToken() = " + loginResponse.getToken());
            }

            @Override
            public void onError(Throwable throwable) {
                System.out.println("throwable = " + throwable);
            }

            @Override
            public void onCompleted() {
                countDownLatch.countDown();
            }
        });
        countDownLatch.await();
    }
}

小夥伴們看到,直接在 onError 回到中處理異常即可。

3.2 同步請求

如果客戶端請求是同步阻塞請求,那麼就要通過異常捕獲的方式獲取服務端返回的異常信息了,如下:

public class LoginClient2 {
    public static void main(String[] args) throws InterruptedException {
        ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
                .usePlaintext()
                .build();
        LoginServiceGrpc.LoginServiceBlockingStub stub = LoginServiceGrpc.newBlockingStub(channel).withDeadline(Deadline.after(3, TimeUnit.SECONDS));
        login(stub);
    }

    private static void login(LoginServiceGrpc.LoginServiceBlockingStub stub) throws InterruptedException {
        try {
            LoginResponse resp = stub.login(LoginBody.newBuilder().setUsername("javaboy").setPassword("1234").build());
            System.out.println("resp.getToken() = " + resp.getToken());
        } catch (Exception e) {
            System.out.println("e.getMessage() = " + e.getMessage());
        }
    }
}

同步阻塞請求就通過異常捕獲去獲取服務端返回的異常信息即可。

4. 題外話

最後,再來和小夥伴們說一個提高 gRPC 數據傳輸效率的小技巧,那就是傳輸的數據可以使用 gzip 進行壓縮。

具體處理方式就是在客戶端調用 withCompression 方法指定數據壓縮,如下:

public class LoginClient2 {
    public static void main(String[] args) throws InterruptedException {
        ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
                .usePlaintext()
                .build();
        LoginServiceGrpc.LoginServiceBlockingStub stub = LoginServiceGrpc.newBlockingStub(channel).withDeadline(Deadline.after(3, TimeUnit.SECONDS));
        login(stub);
    }

    private static void login(LoginServiceGrpc.LoginServiceBlockingStub stub) throws InterruptedException {
        try {
            LoginResponse resp = stub.withCompression("gzip").login(LoginBody.newBuilder().setUsername("javaboy").setPassword("123").build());
            System.out.println("resp.getToken() = " + resp.getToken());
        } catch (Exception e) {
            System.out.println("e.getMessage() = " + e.getMessage());
        }
    }
}

好啦,一個關於 gRPC 的小小知識點~</loginresponse></loginresponse></loginresponse>

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章