gRPC Keepalive 机制

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 连接断开或服务端不可达

相关推荐
RainCity2 小时前
Java Swing 自定义组件库分享(五)
java·笔记·后端
脆皮炸鸡7552 小时前
库制作与原理~静态库&静态链接
linux·经验分享·笔记·学习方法
wangjialelele2 小时前
Linux SystemV 消息队列 + 责任链模式:实现客户端消息处理流水线
linux·服务器·c语言·网络·c++·责任链模式
书生的梦2 小时前
《神经网络与深度学习》学习笔记(一)
笔记·深度学习·神经网络
袁小皮皮不皮2 小时前
HCIP-BFD 学习笔记
运维·服务器·网络·笔记·网络协议·学习·智能路由器
智者知已应修善业2 小时前
51单片机4按键控制共阳LED霓虹灯切换1整体闪烁2流水下3流水上4间隔闪烁】2023-10-27
c++·经验分享·笔记·算法·51单片机
洛水水2 小时前
结构性设计模式详解
c++·设计模式
Stream_Silver2 小时前
【 libusb4java实战:跨平台USB设备通信完全指南】
java·笔记·嵌入式硬件·microsoft
瑶光守护者2 小时前
【学习笔记】Ku终端本振同源频偏分析与上行中频补偿计算报告
笔记·学习