gRPC请求超时和异常处理

简介: gRPC请求超时和异常处理

1. 请求超时

在 HTTP 请求中,我们发送请求的时候,可以设置一个请求超时时间-connectTimeout,即在指定的时间内,如果请求没有到达服务端,为了避免客户端一直进行不必要的等待,就会抛出一个请求超时异常。

但是在微服务系统中,我们却很少设置请求超时时间,一般都是用另外一个概念代替,那就是请求截止时间。

这是什么原因呢?今天我们就来简单聊一聊这个话题。

在微服务中我们客户端的请求在服务端往往会有比较复杂的链条,我想起来 Spring Cloud Sleuth 官方给的一个请求链路追踪的图,我们直接拿来看下:

ed55ba933d2600d823800158b912c709.jpg这张图中,请求从客户端发起之后,在服务端一共经历了四个 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 的小小知识点~

相关文章
Web Socket Client / UE4 / DTWebSocket 插件说明
Web Socket Client / UE4 / DTWebSocket 插件说明
384 1
|
JavaScript 前端开发 编译器
Nodejs 第二十三章(Markdown 转 html)
Nodejs 第二十三章(Markdown 转 html)
287 0
|
1月前
|
JSON 人工智能 JavaScript
cursor 如何调用 MCP server
本文介绍了如何在 Cursor 中配置并调用 MCP Server,以实现天气信息查询功能。内容涵盖 MCP 配置步骤、JSON 文件设置、MCP Server 的调用方法及结果展示,帮助开发者快速集成外部服务。
|
资源调度 监控 前端开发
第七章(原理篇) 微前端技术之依赖管理与版本控制
第七章(原理篇) 微前端技术之依赖管理与版本控制
460 0
|
11月前
|
人工智能 开发框架 Java
总计 30 万奖金,Spring AI Alibaba 应用框架挑战赛开赛
Spring AI Alibaba 应用框架挑战赛邀请广大开发者参与开源项目的共建,助力项目快速发展,掌握 AI 应用开发模式。大赛分为《支持 Spring AI Alibaba 应用可视化调试与追踪本地工具》和《基于 Flow 的 AI 编排机制设计与实现》两个赛道,总计 30 万奖金。
310 104
|
文字识别 算法 API
文档解析(大模型版)能力测评
文档解析(大模型版)能力测评
555 8
|
消息中间件 运维 监控
Kafka两种集群详解和搭建教程
Kafka两种集群详解和搭建教程
5966 0
Kafka两种集群详解和搭建教程
|
SQL 缓存 关系型数据库
SqlAlchemy 2.0 中文文档(五十四)(2)
SqlAlchemy 2.0 中文文档(五十四)
282 1
|
XML Java Android开发
Android RecyclerView用代码动态设置item的selector
Android RecyclerView用代码动态设置item的selector
158 0
|
分布式计算 大数据 MaxCompute
MaxCompute产品使用合集之在Python SDK中,可以使用什么往表中写入数据
MaxCompute作为一款全面的大数据处理平台,广泛应用于各类大数据分析、数据挖掘、BI及机器学习场景。掌握其核心功能、熟练操作流程、遵循最佳实践,可以帮助用户高效、安全地管理和利用海量数据。以下是一个关于MaxCompute产品使用的合集,涵盖了其核心功能、应用场景、操作流程以及最佳实践等内容。