乐趣区

关于grpc: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) && "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 的小小知识点~

退出移动版