epoll+定时器轮询更可靠,因SO_KEEPALIVE默认2小时探测无法满足秒级心跳;应用层需用epoll监听事件、全局tick计数器管理心跳、分桶错峰发送、禁用Nagle、延迟清理连接,并优化内存与fd资源。

为什么 epoll + 定时器轮询比 setsockopt(SO_KEEPALIVE) 更可靠
操作系统级的 SO_KEEPALIVE 默认探测间隔长达 2 小时,超时判定窗口不可控,根本无法满足秒级心跳需求。真正能落地的方案是应用层自主管理:用 epoll 监听连接可读/可写事件,同时维护一个轻量定时器队列,每秒触发一次心跳检查。
- Linux 下必须用
epoll_ctl(EPOLL_CTL_MOD)在每次收发后重置连接的就绪时间戳,否则空闲连接会被误判为超时 - 不要用
std::chrono::steady_clock::now()做逐连接比对——高频调用开销大;改用全局单调递增 tick 计数器(如每毫秒 +1),所有连接只存last_active_tick,判断时直接整数减法 -
SO_KEEPALIVE可以开着,但仅作兜底:它不干扰应用层心跳,但能在进程崩溃、网线拔掉等极端场景下帮内核回收 socket 资源
如何避免定时器精度不足导致批量心跳风暴
百万连接若在同一个 epoll wait 超时周期(比如 100ms)里集中发送心跳包,会瞬间打满网卡 TX 队列和内核 sk_buff 分配,引发 RTT 暴涨甚至丢包。关键不是“能不能发”,而是“怎么错峰发”。
- 把 100 万连接按
fd % 1000分成 1000 个桶,每个桶分配独立的心跳触发偏移量(0–99ms),这样每毫秒最多触发 1000 次 write - 心跳包必须用
send(..., MSG_NOSIGNAL | MSG_DONTWAIT),避免 SIGPIPE 和阻塞;返回-1且errno == EAGAIN时立即跳过,留到下次 tick 再试 - 禁用 Nagle 算法:
setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)),否则小包会被缓冲合并,破坏心跳时效性
连接状态与心跳失败后的清理时机
收到心跳响应只是说明“对方还活着”,不代表业务逻辑可用;而心跳超时也不等于连接已断——可能只是瞬时拥塞或对方 GC 暂停。过早关闭 fd 会导致大量假断连。
- 单连接连续 3 次心跳无响应才标记为
DEAD,但此时不立刻close(),而是加入延迟清理队列,5 秒后再执行epoll_ctl(EPOLL_CTL_DEL)+close() - 必须在
epoll_wait()返回的就绪事件中处理EPOLLIN | EPOLLRDHUP | EPOLLHUP,而不是依赖心跳超时逻辑——真实断连往往表现为recv()返回 0 或 -1 且errno == ECONNRESET - 每个连接结构体里存一个
uint8_t heartbeat_fail_count,而非布尔值;清零时机是任意一次成功send()或recv(),不是收到心跳 ACK
内存与文件描述符的实际瓶颈在哪
百万连接不等于百万活跃 fd —— 真正卡住系统的往往不是连接数本身,而是内核为每个 socket 分配的缓冲区和用户态为每个连接维护的状态对象。
立即学习“C++免费学习笔记(深入)”;
- 调低内核参数:
net.core.wmem_max=65536、net.ipv4.tcp_rmem="4096 16384 65536",避免单连接吃掉几 MB 内存 - 用户态连接结构体必须小于 128 字节,字段全用紧凑布局(比如用
uint32_t存 tick,不用std::chrono::time_point);优先用内存池malloc(sizeof(conn_t) * 1024)批量申请,别用 new - 检查
/proc/sys/fs/file-max和进程ulimit -n,确保 >= 200 万(连接 + 日志句柄 + epoll fd + timerfd)
心跳逻辑本身不复杂,难的是在每秒千万级事件中不让任何一根毛刺漏过去——比如 clock_gettime(CLOCK_MONOTONIC) 的调用频率、timerfd 的 read 清除时机、甚至 glibc malloc 在多线程下的锁争用,都得实测压出来。











