当前位置: 首页 > 后端技术 > Java

gRPC请求超时和异常处理

时间:2023-04-01 18:13:31 Java

1。请求超时在HTTP请求中,当我们发送请求时,可以设置一个请求超时time-connectTimeout,即如果请求在规定的时间内没有到达服务器,为了避免客户端如果客户端已经不必要的等待,会抛出请求超时异常。但是在微服务系统中,我们很少设置请求超时时间,一般用另外一个概念来代替,那就是请求截止时间。是什么原因?今天我们就来简单聊聊这个话题。在微服务中,我们客户端的请求往往在服务器端有一条比较复杂的链条。想到了SpringCloudSleuth官方给出的请求链接跟踪图。我们直接看一下:在这张图中,请求来自于客户端。启动后,服务器端一共经历了4个SERVICE。对于这样的请求,如果我们还是按照之前发送普通HTTP请求的方式,设置一个connectTimeout显然是不行的。举个例子:假设我们发送一个请求,并为请求设置connectTimeout为5s,那么这个时间只对第一个服务SERVICE1有效,即5s内请求没有到达SERVICE1,则连接超时将抛出异常;如果请求在5s内到达SERVICE1,则不会抛出异常,但是!!!,对SERVICE1的请求到达并不意味着请求的结束。后来从SERVICE1到SERVICE2,从SERVICE2到SERVICE3,从SERVICE3到SERVICE4,还有4个HTTP请求需要处理。这些请求超时了怎么办?显然,connectTimeout属性对于接下来的几个请求来说是遥不可及的。所以,对于这种场景,我们一般会使用deadline来处理。deadline相当于设置整个请求生命周期的时间,也就是我这个请求要多久才能得到结果。显然,这个时间应该是在客户端发起请求的时候设置的。gRPC中提供了相应的方法,我们可以很方便的设置请求的deadlineDeadLineTime,如下:,50051).usePlaintext().build();LoginServiceGrpc.LoginServiceStubstub=LoginServiceGrpc.newStub(channel).withDeadline(Deadline.after(3,TimeUnit.SECONDS));登录(存根);}privatestaticvoidlogin(LoginServiceGrpc.LoginServiceStubstub)throwsInterruptedException{CountDownLatchcountDownLatch=newCountDownLatch(1);stub.login(LoginBody.newBuilder().setUsername("javaboy").setPassword("123").build(),newStreamObserver(){@OverridepublicvoidonNext(LoginResponseloginResponse){System.out.println("loginResponse.getToken()="+loginResponse.getToken());}@OverridepublicvoidonError(Throwablethrowable){System.out.println("throwable="+throwable);}@OverridepublicvoidonCompleted(){countDownLatch.countDown();}});countDownLatch.await();}}服务器可以通过Thread.sleep做一个简单的休眠。超时后会触发客户端的onError方法,抛出如下异常:throwable=io.grpc.StatusRuntimeException:DEADLINE_EXCEEDED:deadlineexceededafter2.939621462s。[closed=[],open=[[buffered_nanos=285550823,remote_addr=localhost/127.0.0.1:50051]]]2.服务器端处理异常在之前的文章中,我们其实也遇到过异常问题,但是我们没有和朋友详谈,匆匆写了一个案例。今天我们就来和小伙伴们仔细讨论一下这个话题。我们之前写了一个登录案例。在前面的案例中,如果用户登录时输入了错误的用户名和密码,那么我们会通过一个普通的数据流返回异常信息。其实对于异常信息,我们可以通过专门的异常通道写回客户端。我们先看看服务器是如何处理异常的。还是以我们之前的gRPC登录案例为例,修改服务端的登录逻辑如下(完整代码,朋友们可以参考之前关于如何在gRPC中使用JWT完成身份验证的文章):publicclassLoginServiceImpl扩展LoginServiceGrpc.LoginServiceImplBase{@Overridepublicvoidlogin(LoginBodyrequest,StreamObserverresponseObserver){Stringusername=request.getUsername();Stringpassword=request.getPassword();if("javaboy".equals(username)&&"123".equals(password)){System.out.println("登录成功");//登录成功StringjwtToken=Jwts.builder().setSubject(username).signWith(AuthConstant.JWT_KEY).compact();responseObserver.onNext(LoginResponse.newBuilder().setToken(jwtToken).build());responseObserver.onCompleted();}else{System.out.println("登录错误");//登录失败responseObserver.onError(Status.UNAUTHENTICATED.withDescription("登录错误").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):未实现的操作。内部(13):服务内部错误。UNAVAILABLE(14):服务不可用。DATA_LOSS(15):数据丢失或损坏。UNAUTHENTICATED(16):请求未通过身份验证。这些是系统默认给出的请求类型。当然,如果这些都不能满足你的需求,我们也可以扩展这个枚举类。3.客户端处理异常。服务器给出异常信息后,客户端的处理分为两种情况。3.1异步请求如果客户端是异步请求,可以直接在异常回调中处理,如下:",50051).usePlaintext().build();LoginServiceGrpc.LoginServiceStubstub=LoginServiceGrpc.newStub(channel).withDeadline(Deadline.after(3,TimeUnit.SECONDS));登录(存根);}privatestaticvoidlogin(LoginServiceGrpc.LoginServiceStubstub)throwsInterruptedException{CountDownLatchcountDownLatch=newCountDownLatch(1);stub.login(LoginBody.newBuilder().setUsername("javaboy").setPassword("1234").build(),newStreamObserver(){@OverridepublicvoidonNext(LoginResponseloginResponse){System.out.println("loginResponse.getToken()="+loginResponse.getToken());}@OverridepublicvoidonError(Throwablethrowable){System.out.println("throwable="+throwable);}@OverridepublicvoidonCompleted(){countDownLatch.countDown();}});countDownLatch.await();}}小伙伴可以看到在onError返回的时候可以直接处理异常3.2同步请求如果客户端请求是同步阻塞请求,那么需要通过异常捕获获取服务端返回的异常信息,如下:publicclassLoginClient2{publicstaticvoidmain(String[]args)throwsInterruptedException{ManagedChannelchannel=ManagedChannelBuilder.forAddress("localhost",50051).usePlaintext().build();LoginServiceGrpc.LoginServiceBlockingStubstub=LoginServiceGrpc.newBlockingStub(channel).withDeadline(Deadline.after(3,TimeUnit.SECONDS));登录(存根);}privatestaticvoidlogin(LoginServiceGrpc.LoginServiceBlockingStubstub)throwsInterruptedException{try{LoginResponseresp=stub.login(LoginBody.newBuilder().setUsername("javaboy").setPassword("1234").build());.out.println("resp.getToken()="+resp.getToken());}catch(Exceptione){System.out.println("e.getMessage()="+e.getMessage());}}}同步阻塞请求通过异常捕获获取服务4.题外话最后给大家说一个提高gRPC数据传输效率的小技巧,就是可以对传输的数据进行gzip压缩。具体处理方法是在客户端调用withCompression方法指定数据压缩,如下:publicclassLoginClient2{publicstaticvoidmain(String[]args)throwsInterruptedException{ManagedChannelchannel=ManagedChannelBuilder.forAddress("localhost",50051).usePlaintext().build();LoginServiceGrpc.LoginServiceBlockingStubstub=LoginServiceGrpc.newBlockingStub(channel).withDeadline(Deadline.after(3,TimeUnit.SECONDS));登录(存根);}privatestaticvoidlogin(LoginServiceGrpc.LoginServiceBlockingStubstub)throwspInterrupted{try{LoginResponseresp=stub.withCompression("gzip").login(LoginBody.newBuilder().setUsername("javaboy").setPassword("123")。建造());System.out.println("resp.getToken()="+resp.getToken());}catch(Exceptione){System.out.println("e.getMessage()="+e.getMessage());}}}好了,关于gRPC的小知识~