java nio代理通过selector多路复用管理双向心跳连接,采用轻量二进制协议(magic+type+timestamp+node-id),独立端口隔离心跳与业务流量,结合超时驱逐与主动探测实现集群节点状态感知和故障转移。

Java 用 NIO 实现支持集群心跳检测的代理,核心在于:用 Selector 管理大量 TCP 连接、自定义二进制/文本协议收发心跳、维护节点状态并触发故障转移。它不是靠单机定时器,而是通过双向心跳 + 超时驱逐机制保障集群感知能力。
设计心跳协议与连接模型
代理需同时作为客户端(向后端服务节点发起心跳)和服务器(接收其他代理或服务的心跳上报)。建议采用轻量二进制协议,例如:
- 4 字节 magic(如 0xCAFEBABE)
- 1 字节 type(1=PING,2=PONG,3=REGISTER,4=OFFLINE)
- 8 字节 timestamp(毫秒时间戳,用于 RTT 计算)
- 可选 16 字节 node-id(UUID 字符串的 MD5 或直接存 16 字节)
每个后端节点建立一个独立的 SocketChannel,复用同一个 Selector;所有入站心跳(如其他代理上报)走监听端口的 ServerSocketChannel,统一由一个线程轮询处理。
用 NIO 多路复用管理心跳连接
避免为每个节点起线程。典型结构如下:
- 启动一个
Selector线程,注册所有SocketChannel(出向)和ServerSocketChannel(入向) - 对每个后端节点 channel,设置
OP_READ | OP_WRITE,但只在有数据要发时才关注OP_WRITE(NIO 写可能半截,需缓存 buffer) - 每次
select()后遍历selectedKeys(),区分是新连接、可读、可写还是连接就绪 - 读到完整包后解析 type,若为 PING 则立即回 PONG;若为 REGISTER,则更新本地节点注册表(ConcurrentHashMap
)
关键细节:读取时必须累积 buffer(用 ByteBuffer.allocateDirect(1024)),遇到不完整包先暂存;发送 PONG 不要等下一次 select,直接写入 channel(若 write 返回 0,再注册 OP_WRITE)。
实现超时检测与自动剔除
不能依赖 TCP keepalive(太慢且不可控),要自己维护心跳窗口:
- 每个节点对应一个
NodeInfo对象,含 lastPingTime、lastPongTime、failCount - 收到 PING 更新
lastPingTime;收到 PONG 更新lastPongTime - 每 500ms 在 selector 线程内扫描节点列表:若
System.currentTimeMillis() - lastPongTime > 3000,则标记疑似离线;连续 3 次未收到 PONG,触发onNodeDown(nodeId) - 主动探测:对超过 2s 未发 PING 的节点,构造 PING 包并尝试写入 channel(注意检查 channel 是否仍 connected)
故障回调中可刷新本地路由表、通知监控模块、或通过 ZooKeeper/Etcd 发布变更事件——代理本身不依赖外部协调服务,但可对接。
代理转发逻辑与心跳隔离
心跳流量和业务流量必须分离,否则业务阻塞会导致误判:
- 为心跳单独分配一个端口(如 9091),业务流量走另一端口(如 8080)
- 心跳 channel 设置
SO_KEEPALIVE=false、TCP_NODELAY=true,禁用 Nagle 提高响应速度 - 业务连接的
SocketChannel不参与心跳检测逻辑,仅做透传;其生命周期由独立的空闲超时(idle timeout)控制 - 心跳消息不经过业务编解码器(如 Netty 的 ByteToMessageDecoder),直接在 selector 循环里 parse,减少 GC 和延迟
这样即使某后端业务接口卡住,只要 TCP 连通且能收发心跳包,就不会被误踢出集群。
不复杂但容易忽略:selector 线程不能做耗时操作(如写磁盘、调远程 API),所有异步动作(如告警推送)应扔进业务线程池;节点状态变更需用 volatile 或 AtomicReference 保证可见性;测试时用 nc 手动模拟 PING/PONG 最快验证通路是否跑通。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











