转转微服务框架的连接管理
转转 RPC 框架 SCF(Service Communication Framework)继承自 58 集团 RPC 框架,在转转经历多版本的迭代及重构。作为一款网络应用,连接在其中占据重要地位,转转 SCF 在连接管理上做了大量的优化与功能新增。本文以连接的生命周期为主线谈谈 RPC 框架连接管理的具体实现。
1 连接的建立时机
RPC 框架客户端通过动态代理机制实现和本地方法调用同样的体验。从创建代理到发起调用还需要经过从注册中心发现节点、建立连接两个步骤。SCF 支持立即发现/延迟发现节点,立即建立/延迟建立连接,可通过参数配置。
如果应用在启动时创建代理类,立即发现节点、立即建立连接可减少初次请求的耗时,避免请求超时。而延迟发现节点、延迟建立连接则会导致初次请求耗时加长,有可能造成请求的超时。
延迟发现节点主要是为了解决服务的循环依赖问题,虽然服务的循环依赖是个坏味道,但现实中仍难以完全避免。例如有服务 A、B 互相依赖,如果服务 A、B 都未部署,且未配置延迟发现参数,则 A、B 都无法部署成功,均会抛出无服务节点可用异常。
延迟发现节点和延迟建立连接虽然会导致初次请求耗时加长,但是在某些场景下可以提升效率。例如单个接口测试场景下,并不需要连接所有依赖服务,仅需发现并连接测试接口所依赖的服务即可,缩短服务启动时间,提升测试效率。
2 需要多少条连接
2.1 连接池
连接池的概念想必各位读者都不陌生,数据库连接池、Redis 连接池、HTTP 连接池等。使用连接池的其中一个目的是为了连接的复用。为什么要复用连接呢,因为建立 TCP 连接是个重量级操作,不仅 3 次握手消耗时间,而且需要操作系统为连接分配文件描述符和缓冲区。
使用连接池的另外一个重要目的是提升并发数,可以在多条连接上并发地发起请求。并发地发起请求为什么需要使用多条连接呢,单条连接无法实现吗还是受制于单连接的带宽限制呢。单条连接无法并发地发起请求的主要原因是应用层协议限制。以 HTTP1.1 为例,如下图所示,协议要求在一条连接上只能有一个未完成的请求,也就是说 HTTP1.1 协议要求客户端在接收到上一个 HTTP 响应之后才能发起下一个 HTTP 请求。为什么会有这样的协议要求呢,因为请求和响应是一一对应的,如果不遵守这个协议,有可能先发出的请求耗时比较长,后返回,而后发出的请求耗时比较短,先返回,这样就造成了请求和响应的错乱。
为了提升这种请求-响应-请求-响应协议的的性能,有些协议支持了管线化(PipeLine),比如 Redis。管线化支持同时发起多个请求,但是响应的顺序仍然需要和请求的顺序保持一致,如下图所示。管线化优化了网络性能,可以减少一些网络开销,但是仍然解决不了队头阻塞问题,要想提高并发数量,仍需要使用多条连接。事实上 HTTP1.1 也支持管线化,只是平时很少接触罢了,因为 HTTP 协议常用于前后端通信场景,对耗时的容忍度比较高,管线化请求未得到大规模使用。
2.2 多路复用
在同一条 TCP 连接上并发地发起请求,也就是连接的多路复用,需要协议的支持,在应用层协议需要使用 requestId 或其他类似的概念将请求和响应一一对应起来,有了 requestId 的支持请求和响应就可以在连接上乱序发送了。
例如 HTTP2.0 使用 streamId 将请求和响应对应起来,HTTP2.0 的通信模型如下图所示。HTTP2.0 允许将同一个请求/响应拆分成不同的帧,到达应用之后再组合成完整的请求/响应,所以图中出现了重复了 StreamId。
转转 RPC 框架的协议设计思路同 HTTP2.0 类似,只是不允许将请求/响应拆分成多帧,如下图所示。
在协议支持多路复用的情况下,连接池就可以被淘汰了,默认情况下 SCF 客户端与服务端只建立一条 TCP 连接。如果需要传输的数据量比较大,可以通过参数设置更多连接数量,此时虽然有多条连接,但是这仍然与连接池有着本质的不同。连接池内的多条连接有借出与归还的概念,已经借出的连接不会被其他请求使用。而多路复用下的多条连接没有借出与归还的概念,同一条连接仍然可以被多个请求共同使用。
在协议支持多路复用的情况下,单条连接上请求和响应完全可以是乱序的,如下图所示。
3 连接保活
3.1 TCP 保活机制
保活并不是 TCP 规范的一部分,理由是:(1)在出现短暂差错的情况下,这可能会使一个非常好的连接释放掉;(2)耗费不必要的带宽;(3)在按分组计费的情况下会在互联网上花掉更多的钱。然而,许多实现提供了保活功能。这说明保活是一个有争论的功能,许多人认为保活功能不应该由 TCP 提供,而应该由应用程序来完成。——TCP/IP 详解卷 1,谢希仁
在建立 TCP 连接时可以通过 SO_KEEPALIVE 参数来指定是否开启 TCP 保活功能,一般情况下 TCP 层的默认保活时间是 2 个小时,保活时间参数是操作系统级别的参数,无法对单独的 TCP 连接设置。
3.2 应用层保活
转转 RPC 框架提供了应用层保活机制,虽然 TCP 实现上提供了保活机制,但是 TCP 的保活时间太长(默认 2 小时)无法及时发现不健康的连接。
SCF 客户端使用两个参数 readerIdleTime(默认 3 秒)和 idleTimeout(默认 10 秒)来进行保活控制。如果一条连接上超过 readerIdleTime 时间未发生读事件,则认为该连接为 idle 状态。如果一条连接上超过 idleTimeout 时间未发生读事件,则关闭连接。idle 状态的检测与事件触发使用 Netty 提供的 IdleStateHandler 完成。转转 RCP 框架底层使用 Netty 通信,Handler 的排列如下。
IdleStateHandler 仅提供了在连接为 idle 状态时触发 IdleStateEvent 的能力,判断连接的健康状态仍需要获取连接上的 idleTime。在 BizHandler 中发生 channelActive 事件或者 channelRead 事件时使用 Channel 提供的属性功能将 Channel 的 lastReadTime 属性设置为当前时间,即记录该连接上最后一次读事件的发生时间。在收到 IdleStateEvent 时,首先获取连接的 lastReadTime 属性,如果 lastReadTime 和当前时间的时间差未超过 idleTimeout 则在连接上发送心跳请求,在连接健康的情况下服务端收到心跳请求后回复心跳响应,lastReadTime 得以更新。在连接不健康的情况下,服务端无法收到心跳请求或者客户端无法收到心跳响应,lastReadTime 得不到更新,在后续的某次 IdleStateEvent 触发时,lastReadTime 与和当前时间的差值超过 idleTimeout 则关闭连接。
SCF 服务端默认的 readerIdleTime 为 20 秒,在收到客户端发来的心跳请求时,回复心跳响应,而收到 IdleStateEvent 时关闭连接。
一条健康的连接,心跳时序图如下图所示。
一条不健康的连接,心跳时序图如下图所示。
4 自动修复
保活机制可以及时发现并关闭不健康的连接,但是连接的不健康原因有多种。常见的如网络故障、机器宕机、GC 等。如果故障修复完成或者应用从 GC 中恢复过来,需要重新建立连接。
SCF 客户端为每一条连接添加一个定时任务每 5 秒钟检测连接的状态,如果发现连接断开,则尝试重连。
5 优雅关闭
Netty 服务端 IO 线程分为 bossEventLoopGroup(线程组)和 workerEventLoopGroup(线程组),bossEventLoopGroup 负责监听端口及新连接的建立,连接建立后交给 workerEventLoopGroup 处理。
服务端的优雅关闭首先要关闭 bossEventLoopGroup 以拒绝新的连接。
然后向所有的现有连接发送关闭事件,但是并不立即关闭连接,因为已经建立的连接上可能还有未处理的请求。
客户端在收到服务端发送的关闭事件之后首先将状态设置为关闭中,以避免新的请求负载到该连接上。
同时开启定时任务每秒检测连接上剩余未返回的请求数量和获取连接的 writerIdleTime,如果 writerIdleTime 超过了静默时间并且未返回的请求数量为 0,则关闭连接。之所以不能仅仅以未返回的请求数量为 0 就关闭连接,是因为在将状态设置为关闭中之后,可能有线程正在执行负载均衡之后到发送数据之前的代码,需要为这段代码的执行设置静默时间。
服务端在向所有的连接发送关闭事件之后,每隔一秒检测一次剩余的连接数量,等到连接数量为 0 时关闭 workerEventLoopGroup。
6 总结
本文详细介绍了转转 RPC 框架 SCF 的连接管理,包括连接的建立时机、连接数量、保活、自动修复、优雅关闭。其中的重点在于对于连接数量的思考和优雅关闭,希望对读者在自行设计协议时能有所帮助。
关于作者
王建新,转转架构部服务治理负责人,主要负责服务治理、RPC 框架、分布式调用跟踪、监控系统等。爱技术、爱学习,欢迎联系交流。
转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。
关注公众号「转转技术」(综合性)、「大转转 FE」(专注于 FE)、「转转 QA」(专注于 QA),更多干货实践,欢迎交流分享~
评论