心跳检测必须由连接池主动发起,不能依赖tcp keepalive;需为每个空闲连接绑定独立心跳定时器,使用轻量二进制协议,超时未响应即标记为dead并重连,get()操作须支持懒创建与心跳验证双阶段,连接对象应区分活跃与空闲引用计数以防uaf。

心跳检测必须由连接池主动发起,不能依赖 TCP keepalive
操作系统级 TCP keepalive 延迟高、不可控,且无法区分“对端进程崩溃”和“网络中断”。真实 rpc 场景下,服务端可能已退出但连接仍处于 ESTABLISHED 状态,TCP keepalive 往往几十秒后才断开,导致请求堆积或超时误判。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 在连接池中为每个空闲
std::shared_ptr<connection></connection>绑定独立的心跳定时器(如boost::asio::steady_timer或 c++20std::chrono+ 线程轮询) - 心跳消息用轻量二进制协议(如 4 字节 magic + 1 字节 type=HEARTBEAT),避免序列化开销
- 发送心跳后必须设置
deadline_timer,超时未收到响应即标记连接为DEAD并触发重连 - 不要在心跳回调里直接 close socket——需投递到 IO 线程执行,否则可能引发
double-close或竞态
连接池的 get() 必须支持“懒创建 + 心跳验证”双阶段
常见错误是 get() 直接返回空闲连接却不校验其可用性,结果业务线程发请求时才发现连接已断,被迫重试或抛异常,破坏调用语义。
实操建议:
立即学习“C++免费学习笔记(深入)”;
-
get()第一阶段:从空闲队列取连接 → 检查是否在HEARTBEAT_TIMEOUT内收到过响应 → 若否,丢弃该连接并继续取下一个 - 第二阶段:若空闲队列为空或全失效,则同步创建新连接(注意控制并发创建数,防雪崩)
- 新连接建立后,立即发送一次心跳并等待 ACK,成功才放入空闲队列;失败则直接销毁
- 别把心跳验证逻辑塞进
Connection::send()—— 它只负责发包,健康检查必须前置
连接对象需区分“活跃引用”和“空闲生命周期”,避免析构竞争
典型坑:业务线程拿到连接发完请求就释放 shared_ptr,但此时心跳定时器还在运行,回调里尝试访问已析构的 Connection 对象,触发 UAF。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 用两个引用计数:一个给业务使用(
std::shared_ptr<connection></connection>),一个给连接池内部管理(如std::weak_ptr<connection></connection>+ 池内std::vector存活列表) - 心跳定时器绑定的是
weak_ptr,回调开头先lock();失败则直接 return,不操作资源 -
Connection析构函数中显式取消定时器(timer.cancel()),确保回调不会被调度 - 不要用
std::unique_ptr管理连接——业务需要共享所有权,且池要能跨线程回收
多线程环境下,空闲队列必须无锁或细粒度加锁
高频 RPC 场景下,std::queue 配 std::mutex 会成为瓶颈。压测时常见 pthread_mutex_lock 占用大量 CPU,吞吐卡在几千 QPS 上不去。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 优先用无锁队列,如
moodycamel::ConcurrentQueue(注意它不保证 FIFO 严格顺序,但 RPC 连接池可接受) - 若用锁,别锁整个队列:按连接所属 Event loop 分片(如哈希
fd % N),每片配独立 mutex - 避免在锁内做耗时操作——心跳验证、DNS 解析、ssl 握手等必须在锁外完成
- 定期清理过期连接(如空闲超 60s)应走后台线程,不要在
get()里同步扫描全队列
最易被忽略的是心跳报文的 payload 设计:很多团队直接复用业务请求结构体,结果心跳也触发服务端反序列化和日志,白白消耗 CPU。真正轻量的心跳,应该让服务端仅做字节匹配+回写,不进业务逻辑层。