gRPC Keepalive 机制和流重建实践
背景
在智能云服务治理平台(LSE)中使用了gRPC 作为应用服务和控制面的通讯协议,在没有进行调整的时候我们遇到了两个问题:
- gRPC 的 long-live RPC 无法长时间保活,经常在一段时间后连接自动断开。
- gRPC 连接重建之后不会自动重新启动 bi-directional stream,所以我们需要一种客户端重连机制来应对控制面故障和扩缩容的问题。
常见的 Keepalive 机制
TCP Keepalive
TCP keepalive 是一种众所周知的维护连接和检测断开连接的方法。默认情况下它被禁用,但在特定于实现的不活动持续时间(大约 1-2 小时,但系统管理员可配置)后启用时,将开始发送冗余数据包等待其 ACK。如果 ACK,那么连接似乎很好。如果重复尝试后没有ACK,则认为连接断开。其变量的配置由操作系统在每个套接字级别提供,但通常不会暴露给更高级别的 API。 TCP keepalive 需要调整三个参数:
-
time
(自上次接收到发送保活之前的时间,默认 7200s) -
interval
(没有收到回复时保持活动的间隔,默认 75s) -
retry
(重试发送keepalive的次数,默认9次)
Dubbo Keepalive
Dubbo 的 Keeplive 是基于 TCP keepive 的应用级别保活机制。它主要有以下几个机制组成
- 空闲检测
- 客户端检测读超时 60s,60后开始发送心跳
- 服务端检测读写超时 200s 后断开连接,等待客户端重连
- 发送心跳 客户端每隔 60s发送一次心跳,累计3次没有收到ACK(也就是最近一次 readTime+3*60s 小于currentTime ) ,断开连接进行重试
- 客户端重连 基于指数退避方式的重连操作
HTTP2 Keepalive
HTTP/2协议必须实现 PING 帧,要求接收端立即回复 PING ACK。除了需要及时回复之外,PING没有任何语义。它可以用来估计往返时间,带宽延迟乘积,或测试连接。
gRPC Keepalive
在 TCP keepalive 的基础上,使用 HTTP/2 的 PING 实现一个应用层的 keepalive。由于传输是可靠的,所以 interval
和retry
并不完全适用于 PING,所以它们将被 timeout
(等价于interval * retry
)取代,即发送PING到没有接收到任何字节来声明连接死亡的时间。
做某种形式的保活相对简单。但避免 DDoS 并不容易。因此,避免 DDoS 是设计中最重要的部分。为了缓解 DDoS 设计:
- 对没有未完成流的 HTTP/2 连接禁用 keepalive,并且
- 建议客户端避免配置他们的 keepalive 远低于一分钟
大多数 RPC 都是
unary
的,具有快速回复,因此不太可能触发 keepalive。它主要在存在long-live
RPC 时触发。 由于没有任何流的 HTTP/2 连接不会发生 keepalive,因此长时间不活动后新的 RPC 失败的可能性会更高。 作为可选优化,当超过_keepalive timeout_
时,不要终止连接。相反,开始一个新的连接。如果新连接准备就绪而旧连接仍未收到任何字节,则终止旧连接。如果旧连接赢得比赛,则在启动过程中终止新连接。
客户端 Keepalive 调优
由于业务故障比 TCP 故障更频繁,因此需要降低检测延迟,以减少未发现故障影响的 RPC 数量。 即: 可以在没有任何未完成流的情况下启用 keeplive,最短的 keeplive time 可以缩短到 10s. 客户端有三个 channel 级别的配置:
-
KEEPALIVE_TIME
,默认为 infinite。如果 10 秒,则将使用 10 秒。 -
KEEPALIVE_TIMEOUT
,默认为 20 seconds -
KEEPALIVE_WITHOUT_CALL
,默认为 false
managedChannelBuilder.keepAliveTime(keepAliveTime, TimeUnit.SECONDS);
managedChannelBuilder.keepAliveTimeout(keepAliveTimeOut, TimeUnit.SECONDS);
managedChannelBuilder.keepAliveWithoutCalls(keepAliveWithoutCalls);
服务端 Keepalive 调优
服务器需要通过发送带有错误代码 ENHANCE_YOUR_CALM 的 GOAWAY 和附加的 ASCII“too_many_pings”调试数据来响应行为不端的客户端,然后立即关闭连接。立即关闭连接会使任何正在进行的 RPC 失败,这增加了客户端开发者检测到错误配置的机会。 服务端有两个配置:
-
PERMIT_KEEPALIVE_TIME
,默认5 minutes
-
PERMIT_KEEPALIVE_WITHOUT_CALLS
,默认false
NettyServerBuilder nettyServerBuilder = (NettyServerBuilder) serverBuilder;
nettyServerBuilder.permitKeepAliveTime(keepAliveTime, TimeUnit.SECONDS);
nettyServerBuilder.permitKeepAliveWithoutCalls(true);
Stream 重建机制
在 gRpc 中任何的 gRpc 流都会映射到底层的 http2 流,当因为服务端故障或者网络波动而导致连接断开的时候,该流将会丢失。 所以我们需要一种应用级别的重建双向流的机制。
基本思路
-
定义健康检查的流,可以参考 health-chcking
-
连接断开后
long-alive
RPC 的 onError方法会立即得到一个状态为 UNAVAILABLE的响应,建重连事件放入重连信号队列并标记 stream 为 UNHEALTH。[v1] [2022-08-04 15:30:26.946] [ERROR] [grpc-default-executor-1] com.xxx.lsf.duplexer.DefaultGRpcDuplexer Failed to request featuress. Server is UNAVAILABLE. Make sure lsf-controller-plane is running and reachable from this network. Full error message:Connection closed after GOAWAY. HTTP/2 error code: NO_ERROR, debug data: app_requested [v1] [2022-08-04 15:30:26.946] [ERROR] [grpc-default-executor-0] com.xxx.lsf.duplexer.DefaultGRpcDuplexer Failed to request log_levels. Server is UNAVAILABLE. Make sure lsf-controller-plane is running and reachable from this network. Full error message:Connection closed after GOAWAY. HTTP/2 error code: NO_ERROR, debug data: app_requested
-
间隔 keepalive-time 的时间检查重连队列内的重事件,并检查 stream 的状态是否为UNHEALTH,如果是则进行重连并重建流然后更新 stream 状态。
参考
how to restart bi-directional stream after network disconnection
Dubbo 心跳优化
client-side-keepalive
上一篇: vivo 服务器端监控架构设计与实践
下一篇: 如何在 vivo 手机上查看系统日志