gRPC Keepalive 机制
一、Keepalive 解决的是什么问题
gRPC 的 Keepalive 基于 HTTP/2 PING 帧,不是 TCP keepalive
它主要解决两个问题
1、检测死连接
客户端和服务端之间的 TCP 连接看起来还在,但对端进程可能已经崩溃,网络可能已经断开,中间 NAT 也可能已经丢了连接状态,这时如果没有探测,业务代码可能要等到下一次 RPC 请求失败才知道连接不可用,而 Keepalive 会主动发送 HTTP/2 PING,如果超过 keepalive_timeout 还没有收到 ACK,就认为连接已经不可用,然后关闭连接并重新建立连接
2、保持连接活跃
gRPC 通常基于 HTTP/2 长连接,一个连接上可以承载多个 RPC stream,如果连接长时间没有数据,中间的 LB、NAT、防火墙可能会认为这是空闲连接并回收它,所以客户端可以在无活跃 RPC 时也发送 PING,让中间设备看到连接上仍然有流量,从而维持连接活跃
二、Keepalive 是连接级别,不是流级别
gRPC 的一次调用,对应 HTTP/2 上的一个 stream
unary RPC 是一次请求一次响应
text
客户端发送 request
服务端返回 response + trailers
这个 RPC stream 结束
流式 RPC 是在一个 stream 上持续收发多条消息
text
Read() 持续读取消息
Read() 返回 false
Finish() 拿到最终状态
这个 RPC stream 结束
但是无论 unary 结束,还是流式 RPC 结束,结束的都是某个 RPC stream,不是底层 HTTP/2 连接,底层连接通常会继续保留,等待后续 RPC 复用
text
HTTP/2 connection
├── stream 1:unary RPC,已结束
├── stream 3:server streaming RPC,已结束
├── stream 5:新的 unary RPC
└── stream 7:新的 bidi streaming RPC
所以 Keepalive 维护的是这条 HTTP/2 connection,而不是某一个 stream
三、HTTP/2 PING 的底层过程
HTTP/2 PING 是连接级帧,Stream ID 必须是 0
text
Client Server
│ │
│ ─── PING frame ────────────────→ │
│ │
│ ←── PING ACK frame ───────────── │
│ │
PING 帧的 payload 固定是 8 字节,对端返回 ACK 时必须原样带回这 8 字节
text
Length = 8
Type = 0x06
Flags = 0 或 ACK
Stream ID = 0
Opaque Data = 8B
HTTP/2 帧头是 9 字节,PING payload 是 8 字节,所以一个 PING 帧总共 17 字节,非常轻量
如果发送方在 keepalive_timeout 时间内没有收到 PING ACK,就认为连接不可用
text
发送 PING
等待 keepalive_timeout
没有收到 ACK
关闭连接
重新建连
四、客户端和服务端都可以发 PING
HTTP/2 PING 不是客户端专属,客户端和服务端都可以主动发送
text
客户端 -> 服务端:PING
服务端 -> 客户端:PING ACK
服务端 -> 客户端:PING
客户端 -> 服务端:PING ACK
谁发送 PING,谁负责等待 ACK 并判断是否超时
客户端发 PING,通常是为了判断服务端连接是否可用,或者让连接穿越 LB、防火墙、NAT
服务端发 PING,通常是为了判断客户端是否还在线,避免大量僵尸连接长期占用服务端资源
五、为什么客户端和服务端的配置不是直接统一
因为两端关注点不一样
客户端更关心的是快速发现连接不可用,避免下一次 RPC 请求才发现连接已经断了
服务端更关心的是防止客户端过于频繁地发送 PING,避免大量连接带来额外压力
所以服务端可能会配置一个 enforcement 规则,例如
text
客户端最短每 60s 才能发一次 PING
这个限制不会通过 HTTP/2 自动告诉客户端,客户端不会自动改成 60s 发一次,它仍然按照自己的 keepalive_time 发送 PING
如果客户端发得太频繁,服务端通常会记录违规次数,严重时发送 GOAWAY 或直接关闭连接,常见现象是:
text
GOAWAY received
debug data: too_many_pings
所以实际配置时要保证
text
客户端 keepalive_time >= 服务端允许的最小 PING 间隔
六、核心参数说明
1 客户端参数
keepalive_time:表示多久没有活动后发送一次 PING
text
客户端连接空闲一段时间
达到 keepalive_time
发送 HTTP/2 PING
keepalive_timeout:表示 PING 发出后,最多等待多久
text
超过 keepalive_timeout 还没有收到 ACK
认为连接死亡
关闭连接并重连
permit_without_calls:表示没有活跃 RPC 时是否也发送 PING
text
false:没有 RPC 时不发 PING
true:无活跃 RPC 时也发 PING
2 服务端参数
max_connection_idle:表示连接空闲多久后关闭
text
连接长时间没有 RPC
服务端主动关闭连接
释放资源
max_connection_age:表示连接最大存活时间
text
连接存在时间达到上限
服务端发送 GOAWAY
客户端重新建连
这个参数常用于配合 LB 做连接再均衡
max_connection_age_grace:表示连接达到最大存活时间后,给在途请求留下的宽限时间
text
先发 GOAWAY
不再接收新 stream
等待已有 RPC 尽量完成
宽限期结束后关闭连接
min_time enforcement:表示服务端允许客户端发送 PING 的最小间隔
text
客户端 PING 太频繁
服务端认为违规
可能发送 GOAWAY 或关闭连接
七、为什么不用 TCP keepalive
TCP keepalive 工作在传输层,默认探测时间通常很长,例如 Linux 默认可能是 2 小时级别,这对 RPC 框架来说太慢
text
TCP keepalive
探测太慢
配置依赖操作系统
只能判断 TCP 层连接状态
gRPC Keepalive 基于 HTTP/2 PING,属于协议层探测
text
gRPC keepalive
可以设置为秒级
更贴近 RPC 连接状态
能更快发现死连接
每次只有 17 字节,开销很小
不过要注意,Keepalive 不能乱配得太激进,否则可能被服务端认为是 ping flood
八、C++ 客户端如何配置 Keepalive
下面是一个常见客户端配置
cpp
#include <grpcpp/grpcpp.h>
#include "user.grpc.pb.h"
int main() {
grpc::ChannelArguments args;
// 10 秒没有活动就发送 HTTP/2 PING
args.SetInt(GRPC_ARG_KEEPALIVE_TIME_MS, 10000);
// PING 发出后 5 秒没有收到 ACK,就认为连接不可用
args.SetInt(GRPC_ARG_KEEPALIVE_TIMEOUT_MS, 5000);
// 没有活跃 RPC 时也允许发送 PING
// 用于穿越 LB、防火墙、NAT
args.SetInt(GRPC_ARG_KEEPALIVE_PERMIT_WITHOUT_CALLS, 1);
// 允许没有数据帧时发送 PING
// 0 通常表示不限制这类 PING 次数
args.SetInt(GRPC_ARG_HTTP2_MAX_PINGS_WITHOUT_DATA, 0);
auto channel = grpc::CreateCustomChannel(
"dns:///user-service:8080",
grpc::InsecureChannelCredentials(),
args
);
auto stub = UserService::NewStub(channel);
return 0;
}
这段代码的含义是
text
客户端维护一条 HTTP/2 长连接
如果 10 秒没有活动,就发送一次 PING
如果 5 秒内没有收到 PING ACK,就认为连接死亡
即使当前没有活跃 RPC,也允许发送 PING
如果你的服务端限制客户端最短 60 秒才能发一次 PING,那客户端这里的 10000 就太激进,应该改成 60000 或更大
九、C++ 服务端如何配置 Keepalive 和 Enforcement
服务端不仅可以自己发送 PING,也可以限制客户端 PING 的频率
cpp
#include <grpcpp/grpcpp.h>
#include "user.grpc.pb.h"
class UserServiceImpl final : public UserService::Service {
// 实现 RPC 接口
};
int main() {
UserServiceImpl service;
grpc::ServerBuilder builder;
builder.AddListeningPort(
"0.0.0.0:8080",
grpc::InsecureServerCredentials()
);
builder.RegisterService(&service);
// 连接空闲 15 分钟后关闭
builder.AddChannelArgument(
GRPC_ARG_MAX_CONNECTION_IDLE_MS,
15 * 60 * 1000
);
// 连接最大存活 30 分钟
// 到时间后服务端会让客户端重连
builder.AddChannelArgument(
GRPC_ARG_MAX_CONNECTION_AGE_MS,
30 * 60 * 1000
);
// 到达最大存活时间后,再给 5 秒处理在途请求
builder.AddChannelArgument(
GRPC_ARG_MAX_CONNECTION_AGE_GRACE_MS,
5000
);
// 服务端也可以主动发送 PING
builder.AddChannelArgument(
GRPC_ARG_KEEPALIVE_TIME_MS,
5 * 60 * 1000
);
// 服务端发送 PING 后,1 秒内没有 ACK 就认为连接异常
builder.AddChannelArgument(
GRPC_ARG_KEEPALIVE_TIMEOUT_MS,
1000
);
// Enforcement:限制客户端 PING 的最小间隔
// 客户端在没有数据时,如果 PING 间隔小于 5 秒,可能被认为违规
builder.AddChannelArgument(
GRPC_ARG_HTTP2_MIN_RECV_PING_INTERVAL_WITHOUT_DATA_MS,
5000
);
// 允许无活跃 RPC 时的 PING
builder.AddChannelArgument(
GRPC_ARG_KEEPALIVE_PERMIT_WITHOUT_CALLS,
1
);
auto server = builder.BuildAndStart();
server->Wait();
return 0;
}
这里最重要的是区分两个概念
text
GRPC_ARG_KEEPALIVE_TIME_MS
表示本端多久主动发一次 PING
GRPC_ARG_HTTP2_MIN_RECV_PING_INTERVAL_WITHOUT_DATA_MS
表示本端允许对方最短多久发一次 PING
一个是主动探测策略,一个是对对方的约束策略
十、在代码中如何感知流断开和连接断开
1 unary RPC
unary RPC 调用返回后,这次 RPC stream 就结束了
cpp
grpc::ClientContext context;
GetUserRequest request;
GetUserResponse response;
grpc::Status status = stub->GetUser(&context, request, &response);
if (status.ok()) {
// 本次 unary RPC 正常结束
} else {
// 本次 RPC 异常结束
// 可能是服务端返回错误,也可能是连接异常
}
unary 的结束不代表 HTTP/2 连接断开,连接仍然可能被 channel 继续复用
2 服务端流式 RPC
cpp
grpc::ClientContext context;
ListUserRequest request;
ListUserResponse response;
auto reader = stub->ListUsers(&context, request);
while (reader->Read(&response)) {
// 持续读取服务端推送的消息
}
// Read 返回 false,说明这个 RPC stream 没有更多消息
grpc::Status status = reader->Finish();
if (status.ok()) {
// 流式 RPC 正常结束
} else {
// 流式 RPC 异常结束
}
这里的 Read() 返回 false 只说明这个 RPC stream 结束,不说明 HTTP/2 connection 一定断开
3 客户端主动取消流
cpp
grpc::ClientContext context;
auto stream = stub->Chat(&context);
// 某个时刻不想继续这个 RPC
context.TryCancel();
客户端取消某个 RPC,底层通常会让这个 stream 结束,但不一定关闭整条 HTTP/2 连接
4 服务端判断客户端取消
cpp
grpc::Status Chat(
grpc::ServerContext* context,
grpc::ServerReaderWriter<ChatResponse, ChatRequest>* stream
) override {
ChatRequest request;
while (stream->Read(&request)) {
if (context->IsCancelled()) {
// 客户端取消了 RPC,或者连接异常导致 RPC 被取消
return grpc::Status::CANCELLED;
}
// 处理消息
}
return grpc::Status::OK;
}
5 底层 HTTP/2 连接断开
gRPC 业务代码一般拿不到底层 HTTP/2 connection 对象
连接断开通常表现为这条连接上的 RPC 全部失败,常见状态码是
text
UNAVAILABLE
CANCELLED
DEADLINE_EXCEEDED
INTERNAL
例如
cpp
grpc::Status status = reader->Finish();
if (!status.ok()) {
std::cout << "error code: "
<< status.error_code()
<< std::endl;
std::cout << "error message: "
<< status.error_message()
<< std::endl;
}
如果多个 RPC 同时返回 UNAVAILABLE,很可能是底层 HTTP/2 连接断开或服务端不可达