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++后台相关知识。感谢感谢~

相关推荐
Rust研习社2 小时前
Weak 弱引用:如何用 Weak 打破 Rc 与 Arc 的循环引用
开发语言·后端·rust
贫民窟的勇敢爷们2 小时前
Spring Boot+Vue电商系统开发实战:架构设计与核心实现
vue.js·spring boot·后端
小码哥_常12 小时前
Spring Boot:别再重复造轮子,这些内置功能香麻了
后端
皮皮林55113 小时前
OpenFeign 首次调用卡 3 秒?八年老开发扒透 5 个坑,实战优化到 100ms!
后端
千寻girling14 小时前
《 Git 详细教程 》
前端·后端·面试
0xDevNull15 小时前
Linux 中 Nginx 代理 Redis 的详细教程
redis·后端
GetcharZp16 小时前
告别 Nginx 手动配置!这款 Go 语言开发的云原生网关,才是容器化时代的真香神器!
后端
RuoyiOffice16 小时前
SpringBoot+Vue3 企业考勤如何处理法定假期?节假日方案、调休补班与工作日判断链路拆解
spring boot·后端·vue·anti-design-vue·ruoyioffice·假期·人力