C++ grpc 截止时间示例学习

前言

本文根据 github.com/grpc/grpc/t... 进行C++ grpc 截止时间示例学习运行。更多的是学习记录,水平不高,能力有限,错漏之处,还请见谅。欢迎友好讨论。

环境信息

  • 操作系统版本:ubuntu24.04
  • CMake版本:4.2.0
  • Git版本:2.43.0
  • GCC版本:gcc 13.3.0
  • OpenSSL版本: 3.0.13

在之前的教程mp.weixin.qq.com/s/50Tep3mq7... 中给出的是在Centos 7.6下的部署流程,现在我重装了操作系统为ubuntu 24.04,并且重新编译了grpc文件。相关二进制文件可以关注公众号 只做人间不老仙 ,后台发送 "grpc ubuntu 编译文件"获取我编译内容的压缩包 。

代码运行流程

编译

参考 mp.weixin.qq.com/s/50Tep3mq7... 克隆仓库 github.com/EarthlyImmo... 并配置grpc依赖。

配置好后,可以先修改一下 start_build.sh 中的gcc和g++的路径。

在blog_code/deadlines目录下执行:

shell 复制代码
./start_build.sh

完成编译。

运行

在blog_code/deadlines/build/server目录下运行服务器:

shell 复制代码
./server

另起一个终端,在blog_code/deadlines/build/client目录下运行客户端:

shell 复制代码
./client

代码大部分是copy的grpc官方的示例,这里对cmake文件和目录结构做了调整,对部分注释或者日志做了调整。

代码分析

客户端和服务器实现分析

代码使用hellworld示例,且为异步回调接口。关于异步回调接口更详细的介绍可以参考mp.weixin.qq.com/s/4hU0XMHne... 可以发现这个示例没有使用SSL认证。关于grpc 认证相关内容可以参考mp.weixin.qq.com/s/_54ixo8Dr...

这个示例主要测试grpc截止时间,截止时间是通过客户端调用ClientContext::set_deadline进行设置的。

在测试过程中,客户端一共发送了4个消息,下面逐个来分析。

第一个消息 的标签为Successful request,表示这个协议会正常成功;发送的信息为"world"。这个协议的消息链路就是普通链路,客户端发送消息,服务器收到消息之后返回,不超时,返回错误码为grpc::StatusCode::OK。其时序图如下所示。

第二个消息 的标签为Exceeds deadline,表示这个协议会超时;发送的信息为delay。通过服务器代码可以得到,当消息为delay的时候,服务器会故意延迟1.5s,因为之前设置的截止时间是从发出消息之后1s,因此客户端会超时,返回错误码为grpc::StatusCode::DEADLINE_EXCEEDED

客户端的超时是自己判断的。那么如果回包时已经超时了,服务器会判断超时并处理吗?是会的,当服务回包时发现超时了,底层将丢弃回包。这一点可以通过抓包来验证。抓包方法可以参考mp.weixin.qq.com/s/gKOPz3s4S...客户端和服务器实现分析一节。

可以将其他消息的发送都关闭,只留下第二条消息。

重新编译运行抓包。客户端输出如下:

抓包分析如下,可以看到只有一条从客户端到服务器的协议,并没有从服务器返回的协议。说明了服务器在回包时判定超时了,就会丢弃回包。

这表明,服务器其实可以感知到超时时间。如果服务器感知不到,那么流程应该是:客户端判定超时之后,服务器依然会将回包返回给客户端,由客户端判断超时并且丢弃。而通过抓包可以发现,服务器自己丢弃了超时的包。可见,截止时间从客户端传递给了服务器。传递是通过 HTTP/2 头部 grpc-timeout实现的,里面记录的是距离截止时间的剩余时长,也就是超时时间。在抓包结果中也可以看到这个头部。

为什么传递时传递一个超时时间而不是截止时间呢?这是因为两台服务器之间的时钟可能并不是同步的,传递超时时间可以避免时钟偏移的影响。但是传递超时时间的问题在于,无法包括网络传输时间。比如客户端设置超时时间1s,传递到服务器的超时时间基本等于1s,但是如果网络不好,传输消耗了300ms,实际上只剩下700ms了。这个问题似乎grpc并没有处理。不过这并不影响设置超时时间的目的:避免客户端无限期的等待。

由于客户端和服务器各自判断超时,因此判断可能会有不一致的情况,不过这个问题不大,只要有一个判断超时了,就超时即可。

这里还有一个问题,服务器等待了1.5s,但是实际上在1s的时候,客户端就已经超时了,此时再继续等待已经没有意义了,服务器业务层应该提前结束,而不是等到最后回包的时候由底层判断。那么业务层如何感知呢?根据grpc官方资料,当客户端或者服务器某一方判断超时之后,会发送RPC取消信号。那么就可以根据mp.weixin.qq.com/s/OeIN7uzd1...服务器端要主动检查取消状态并进行处理,添加对取消状态的检查,避免无效等待,比如可以在服务器中修改代码:

同样的,只注释其他消息,只剩下第二个消息,重新编译运行抓包分析。客户端输出与之前一致,服务器输出如下,可以发现,因为超时而提前结束了,不再进行无意义的等待。

抓包分析,过滤查找grpc,可以发现仍然是只有一条请求协议,没有取消协议。询问AI,这是因为取消信号并不是通过独立grpc消息发出的,而是通过HTTP/2RST_STREAM 帧发送的。在wireshark过滤时,可通过http2.type == 3进行过滤。

过滤http2.type == 3 结果如下,可以发现有两个RST_STREAM 帧,客户端到服务器一个,服务器到客户端一个。正常来说,应该只有1方通知就够了。这里可能是因为客户端和服务器几乎同时判定超时,同时向对方发送消息。

再尝试一次抓包分析,只有一个从服务器到客户端的RST_STREAM 帧。根据我的几次测试结果,几乎都是服务器先判定超时,然后给客户端发送RST_STREAM 帧。

第二个消息的时序图如下:

第三个消息 的标签为Successful request with propagated deadline,表示经过了一次转发之后,依然没有超时。发送的消息为[propagate me]world。通过服务器代码可以得知,当消息以[propagate me]开头时,服务器会先等待800ms,然后将消息去掉[propagate me]头部转发给自己。因此这个消息经历一次延时800ms和一次转发,然后返回给客户端,总耗时应该略高于800ms,不到1s,因此客户端不超时,返回错误码为grpc::StatusCode::OK

通过前面的讨论,我们知道客户端会以超时时间的形式传递截止时间信息。那么在转发的过程中,这个信息会实时修改吗?是会的。通过服务器代码可以看到,转发的时候,通过ClientContext::FromCallbackServerContext继承了上下文,也就继承了超时时间。这一点可以通过抓包验证。

可以将其他的消息发送都关闭,只剩下第三条。

编译运行,客户端输出:

抓包分析,可以看到。客户端到服务器请求中的超时时间为994ms,而服务器转发到自己的请求为190ms,因此可以验证,超时时间被继承了。

第三个消息的时序图为:

第四个消息 的标签为 Exceeds propagated deadline,表示转发之后超时了。发送的消息为[propagate me][propagate me]world。根据服务器代码可知,这个消息会经历两次转发,每次延迟为800ms,总延迟就是1600ms,超过了1s,因此客户端会超时,返回错误码为grpc::StatusCode::DEADLINE_EXCEEDED

可以对整个流程进行抓包分析,观察每个请求包的超时时间。抓包前可以将其他的消息发送都关闭,只剩下第四条。

编译运行,客户端输出:

抓包分析,可以看到,总共只有两条grpc请求消息,一条是从客户端到服务器,超时时间为991ms。另外一条是从服务器转发到服务器,超时时间为187ms。没有任何一条回包消息。这也比较容易理解。整个流程为:客户端发送消息到服务器,此时超时时间还有991ms,服务器延时800ms,然后进行第一次转发;第一次转发再次到服务器,此时超时时间还有187ms,服务器延时800ms,再发送消息时已超时,则不会发送消息。因此没有任何回包消息,只有客户端到服务器和服务器第一次转发两个请求消息。

在进行上述测试过程中,为了跟踪服务器转发请求的最后错误码是多少,我在服务器代码中加入一行输出代码:

测试完之后服务器输出为:

两次输出可以理解,毕竟还是经历了两次转发,只是第一次转发真的转发了,第二次转发请求消息发出时已经超时了。值得注意的是,当服务器判断超时时,返回的错误码不是 grpc::StatusCode::DEADLINE_EXCEEDED(4),而是grpc::StatusCode::CANCELLED(1)。

如前所述,当客户端或者服务器某一方判断超时之后,会通过HTTP/2RST_STREAM 帧发送RPC取消信号,在wireshark过滤时,可通过http2.type == 3进行过滤。

过滤结果如下,可以发现,应该是服务器首先判定消息超时了,然后发送给客户端RST_STREAM 帧,并且作为第一次转发的客户端,也发送给了自己一个RST_STREAM 帧。

第4个消息的时序图如下:

总结

根据上述测试结果以及grpc截止时间相关资料( grpc.io/docs/guides...grpc.io/blog/deadli... ),总结如下:

  • 截止时间(Deadline)用于指定一个特定的时间点,一旦超过该时间点,客户端便不再继续等待服务器的响应。这主要是为了避免异常情况下客户端陷入无限期的等待状态。
  • 截止时间的设立的理想做法:首先基于对系统特性的了解(例如网络延迟、服务器处理耗时等)进行合理的预估,随后通过负载测试对该预估值进行验证与调整。
  • 客户端和服务器都会去判断是否超时。客户端判断已经超时,返回状态码DEADLINE_EXCEEDED;服务器判断已超时,返回状态码CANCELLED。不管是客户端还是服务器,判断超时后都会通过HTTP/2RST_STREAM 帧发送RPC取消信号。
  • 服务器端应用程序有责任主动终止其为响应RPC 调用而派生(或启动)的任何后台任务或活动。如果应用程序启动了某个耗时较长的处理流程,应该在流程执行期间定期检查发起该流程的 RPC 调用是否已被取消;一旦检测到调用已被取消,即应立即终止当前正在进行的处理任务。
  • 截止时间在调用链中以超时时间(grpc-timeout头部)的形式进行传播,并且在传播过程中会自动扣除已消耗的时间。这是因为截止时间是一个固定的时间点,若将其原封不动地传播给另一台服务器,可能会引发问题,因为这两台服务器的时钟可能并未同步。

参考资料

欢迎关注公众号:只做人间不老仙

如果觉得文章还不错的话,欢迎点赞、关注、评论、转发,我将持续更新C++后台相关知识。感谢感谢~

相关推荐
㳺三才人子3 小时前
初探 Flask
后端·python·flask·html
星栈独行3 小时前
我在 Rust 全栈项目里用 JWT 做无状态认证
开发语言·后端·rust·前端框架·开源·github·web
Java爱好狂.4 小时前
Java程序员体系化学习路线(2026最新版)
java·后端·java面试·java架构师·java程序员·java八股文·java学习路线
陈随易4 小时前
Redis 8.8发布,一定要更新
前端·后端·程序员
装不满的克莱因瓶4 小时前
SpringBoot 如何将 lib 目录中jar包打包进最终的jar包里面
spring boot·后端·maven·jar·mvn
ltl5 小时前
Transformer 原论文实验结果:为什么 28.4 BLEU 足以改写路线图
后端
excel6 小时前
为什么我推荐使用 Termius:现代 SSH 工具的完整体验
前端·后端
卷毛的技术笔记6 小时前
Java后端硬核实战:用Spring AI Alibaba+Redis给LLM装上“超强记忆中枢”
java·人工智能·redis·后端·spring·ai·系统架构
IT_陈寒7 小时前
Java的Optional差点让我掉坑里,这几个坑你别踩
前端·人工智能·后端
子兮曰8 小时前
Harness 驾驭工程深度教程:从 AGENTS.md 到全链路 AI 编码基础设施
前端·后端·ai编程