hashedwheeltimer 不适合百万级心跳检查,因其内存模型面向中低频延时任务,百万级 timeout 实例引发 oom、gc 压力与任务延迟;应改用状态快照+批量扫描方案。

HashedWheelTimer 为什么不适合百万级心跳检查
直接用 HashedWheelTimer 管理百万级心跳活跃度检查,大概率会在几小时内 OOM 或出现严重任务延迟。根本原因不是它“慢”,而是它的内存模型和设计目标不匹配:它为中低频、长周期(秒级+)、数量有限(通常 ≤10k)的延时任务优化,底层是固定大小的环形数组 + 每槽一个 LinkedBlockingQueue。当任务量达百万级,哪怕平均分布在 512 个槽上,单槽也承载近 2000 个节点——每次 tick 扫描、过期判断、链表遍历开销陡增;更致命的是,每个 Timeout 对象持有强引用的 Runnable 和堆外资源(如 Netty 的 Channel),GC 压力剧增。
心跳场景下 HashedWheelTimer.newTimeout() 的典型误用
常见错误是为每个连接/设备都调用一次 newTimeout() 注册独立 timeout,比如:
timer.newTimeout(timeoutTask, heartbeatInterval, TimeUnit.SECONDS);
这会导致:
- 每秒新增上万
Timeout实例,对象分配速率远超 CMS/G1 的常规处理能力 - 心跳续期需先 cancel 再 new,而
Timeout.cancel()在高并发下存在锁竞争(Netty 4.1.x 中HashedWheelTimer的内部workerState变更和队列操作非完全无锁) - 任务实际执行时间抖动大:若某 tick 耗时 > tickDuration(如默认 100ms),后续槽位会堆积,导致部分心跳判定延迟数秒甚至丢弃
真正可行的替代方案:分层状态管理 + 批量扫描
把“心跳是否活跃”从「每个连接绑定一个定时器」转为「统一维护连接状态快照 + 定期批量探测」:
- 用
ConcurrentHashMap<connectionid long></connectionid>存储最后收到心跳的时间戳(System.nanoTime()),写入无锁、O(1) - 启动一个固定周期(如 500ms)的
ScheduledExecutorService,每次执行时遍历 map 中的 keySet() 快照(避免遍历时修改异常),计算now - lastHeartbeat > timeoutThreshold - 对超时连接批量触发下线逻辑(关闭 channel、发通知等),并从 map 中 remove —— 这比逐个 cancel timeout 更轻量
- 若需亚秒级精度,可将时间戳存为
long并用SystemClock或Ticker(Guava)对齐,避免System.currentTimeMillis()的系统时钟跳变风险
如果必须用 HashedWheelTimer,至少做三件事
硬要用它,必须绕过其原始语义,把它降级为“粗粒度 tick 触发器”,而非“每任务一 timer”:
- 只创建 **1 个全局 timeout**,周期性触发(如
timer.newTimeout(tickTask, 1, TimeUnit.SECONDS)),在tickTask中执行上面说的批量扫描逻辑 - 禁用
HashedWheelTimer的 pending timeouts 队列(设pendingTimeouts = 0),防止用户误调newTimeout()堆积 - 调大
ticksPerWheel(如 1024)但**绝不调大轮数或 tickDuration**:增大 wheel 大小只为降低单槽冲突,而非延长精度;tickDuration 保持 10–100ms,确保探测频率可控
真正的瓶颈从来不在“定时器快不快”,而在“你让定时器去做什么”。百万连接的心跳不是延时任务,是状态一致性问题——用状态机思维,别用定时器思维。










