ARTICLE · 1161223
Netty 优雅停机源码:shutdownGracefully 的 quiet period 为什么重要(系列第16篇)
shutdownGracefully(quietPeriod, timeout)的语义不是“最多等待 quietPeriod”,而是停止接收新常规任务后,持续执行已有任务;只有连续 quietPeriod 没有新任务才终止,同时受 timeout 硬上限约束。真正的服务优雅下线还必须先摘除流量、关闭监听端口、处理在途请求和连接,最后才结束 EventLoop。
很多 Netty 程序在 finally 中调用 bossGroup.shutdownGracefully() 和 workerGroup.shutdownGracefully(),就宣称已经支持优雅停机。实际上这只处理了线程执行器的退出。若负载均衡仍在送流量、ServerChannel 仍在 accept、新请求还在进入 Pipeline,再合理的 quiet period 也无法替业务完成排水。
源码基线:
• 项目:Netty • 版本: netty-4.1.135.Final• Commit: f05f765d81460799c53123a207f665bf3b465171• 重点文件: common/src/main/java/io/netty/util/concurrent/SingleThreadEventExecutor.java• 重点方法: shutdownGracefully、confirmShutdown、runAllTasks、cleanup• 官方源码:https://github.com/netty/netty/blob/netty-4.1.135.Final/common/src/main/java/io/netty/util/concurrent/SingleThreadEventExecutor.java

这张图分成服务治理、Channel 生命周期和 EventLoop 三层。shutdownGracefully 只覆盖最下层:执行剩余任务、观察 quiet period、达到 timeout 后终止。上层的摘流量、拒绝新请求、协议通知和在途请求收敛需要应用自己编排。
1. 优雅停机有三个对象,不能只关 EventLoopGroup
一个网络服务至少包含:
1. ServerChannel:监听端口并接收新连接; 2. Child Channels:承载已有连接和请求; 3. EventLoopGroup:处理 accept、I/O、任务和定时任务。
只关闭 Worker Group,相当于直接撤走执行连接事件的线程;只关闭 ServerChannel,可以停止新连接,但老连接仍会运行;只从注册中心摘除,已有 keep-alive 连接仍可能发送请求。
推荐顺序:
标记实例 not-ready -> 从负载均衡/注册中心摘流量 -> 等待传播窗口 -> 关闭 ServerChannel,停止 accept -> 拒绝新业务请求并等待在途请求 -> 协议级通知/关闭 Child Channels -> shutdownGracefully EventLoopGroups -> 等待 terminationFuture -> 进程退出2. shutdownGracefully 先把状态推进到 SHUTTING_DOWN
SingleThreadEventExecutor.shutdownGracefully 会校验参数,其中 timeout 不能小于 quietPeriod,然后通过 CAS 推进执行器状态,并保存两个时间:
public Future<?> shutdownGracefully( long quietPeriod, long timeout, TimeUnit unit) { ObjectUtil.checkPositiveOrZero(quietPeriod, "quietPeriod"); if (timeout < quietPeriod) { throw new IllegalArgumentException("timeout < quietPeriod"); } // CAS state -> ST_SHUTTING_DOWN gracefulShutdownQuietPeriod = unit.toNanos(quietPeriod); gracefulShutdownTimeout = unit.toNanos(timeout); // 确保线程启动或被唤醒,以便进入 confirmShutdown return terminationFuture();}返回的是 termination Future,不代表方法返回时线程已经退出。调用方如果必须等待资源完全释放,需要监听或同步等待这个 Future,而且等待线程不能是被关闭的 EventLoop 自己。
3. quiet period 是“连续安静窗口”,会被新任务刷新
confirmShutdown() 的关键逻辑可概括为:
if (runAllTasks() || runShutdownHooks()) { if (isShutdown()) { return true; } if (gracefulShutdownQuietPeriod == 0) { return true; } wakeup(true); return false;}long nanoTime = getCurrentTimeNanos();if (isShutdown() || nanoTime - gracefulShutdownStartTime > gracefulShutdownTimeout) { return true;}if (nanoTime - lastExecutionTime <= gracefulShutdownQuietPeriod) { wakeup(true); Thread.sleep(100); return false;}return true;只要 shutdown 期间仍执行了任务,lastExecutionTime 就会更新,quiet period 重新计算。这个设计给已提交的回调、关闭事件和清理任务一个收敛窗口。
但它也会产生意外:如果业务定时任务不断自我续期,或者其他线程持续提交允许进入的任务,执行器可能一直达不到安静窗口,最后只能依赖 timeout 硬终止。
4. timeout 是硬上限,不保证所有任务都完成
quiet period 回答“多久没有新任务才算安静”,timeout 回答“从开始关闭起最多容忍多久”。两者关系:
因此不能把 timeout 当作“所有在途请求一定能完成”的承诺。业务需要自己的 drain deadline,并在超时后明确取消、失败或持久化未完成工作。
默认参数也不是所有服务都合适。在线请求服务可能只允许几十秒发布窗口,长文件传输可能需要分钟级,交易系统则可能要求更复杂的会话迁移。
5. 正在执行的 Handler 不会被魔法中断
EventLoop 上如果有一个阻塞 60 秒的 Handler,即使 shutdown timeout 是 10 秒,线程也无法在执行 Java 调用栈中途自动跳到 confirmShutdown。超时判断只有线程重新回到事件循环时才能发生。
这说明优雅停机的前提仍是“不阻塞 EventLoop”。如果必须控制耗时业务,应放到可取消的业务线程池,并把任务状态与 Channel 生命周期解耦。
停机问题常暴露平时的线程模型缺陷:正常运行时偶发阻塞只表现为尾延迟,发布时大量关闭与回调叠加,就会变成进程迟迟退不出去。
6. ServerChannel 应先关闭,阻断新连接入口
服务端保存 bind 返回的 ChannelFuture:
Channel serverChannel = bootstrap.bind(port).sync().channel();下线时先:
serverChannel.close().sync();关闭监听 Channel 后,Boss 不再 accept 新连接,但已有 Child Channel 不会自动全部消失。可以用 ChannelGroup 管理连接集合:
ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);// channelActive 时 addchannels.close().awaitUninterruptibly(drainTimeoutMillis);真实业务不一定立即 close 所有连接。HTTP/1.1 可在响应中加 Connection: close;WebSocket 可发送 Close frame;自定义协议可发送 server draining 控制消息,让客户端迁移。
7. 摘流量需要传播时间,Readiness 不是瞬时屏障
在 Kubernetes、注册中心或负载均衡器中把实例设为不可用,并不意味着下一微秒就没有新请求。健康检查存在周期,代理有缓存,客户端连接池也可能复用既有连接。
因此通常需要:
• 先设置 draining 状态; • Readiness 返回失败; • 等待至少一个传播窗口; • 对新请求返回可重试错误或明确拒绝; • 已在途请求继续处理; • 达到业务 deadline 后强制收敛。
如果一收到 SIGTERM 就立即关闭 EventLoop,前面的摘流量只是形式流程。

时序图中的两个时间点最重要:lastExecutionTime 会因 shutdown 期间执行任务而后移,quiet period 从最后一次任务重新计算;gracefulShutdownStartTime 不变,最终由 timeout 限制总时长。前者追求完整,后者保证进程不会无限等待。
8. Boss 和 Worker 的关闭顺序要与连接策略一致
一般先停止 Boss 接入,再处理 Worker 上的存量连接:
serverChannel.close().sync();drainExistingChannels();bossGroup.shutdownGracefully(0, 10, SECONDS);workerGroup.shutdownGracefully(2, 15, SECONDS);但不要机械套参数。Boss 关闭后通常任务少,可以更快结束;Worker 承载在途 I/O、Promise Listener、关闭事件和定时任务,需要更长窗口。
若 Boss 与 Worker 共享同一个 EventLoopGroup,则无法按两阶段独立控制,要靠先关闭 ServerChannel、再收敛 Child Channel 实现逻辑分层。
9. 定时任务和重试是停机不能安静的常见原因
以下代码会在关闭期持续制造任务:
void scheduleReconnect(EventLoop loop) { loop.schedule(() -> { connect(); scheduleReconnect(loop); }, 1, SECONDS);}优雅停机需要一个上层停止标志:
if (!draining.get()) { scheduleReconnect(loop);}心跳、指标上报、配置轮询、重试、延迟写和连接保活都要审计。任务取消后还要释放捕获的 Channel、ByteBuf 和大对象引用,否则线程虽然结束,其他全局结构仍可能保留资源。
10. 终止 Future 是关闭完成的证据
shutdownGracefully 返回 Future,terminationFuture() 也能表示最终状态。生产代码应将它纳入关闭协调,而不是固定 sleep:
Future<?> bossStop = bossGroup.shutdownGracefully(0, 10, SECONDS);Future<?> workerStop = workerGroup.shutdownGracefully(2, 20, SECONDS);bossStop.syncUninterruptibly();workerStop.syncUninterruptibly();固定 sleep 既可能睡短导致资源未释放,也可能睡长拖慢发布。Future 还能记录失败与实际耗时,成为停机指标。
需要防止 shutdown hook 自己被无限阻塞。外层编排仍应有进程级 deadline,超过后记录未完成连接、任务和线程栈,再执行明确的强制退出策略。
11. 停机验收要看业务而不只是端口
至少验证:
还应在压测环境执行真实滚动发布,而不是只调用一次单元测试。长连接、慢请求、大响应和断线重连都会改变停机曲线。
12. 优雅停机是服务生命周期协议
源码主链:
shutdownGracefully -> state = SHUTTING_DOWN -> wakeup EventLoop -> run remaining tasks / shutdown hooks -> confirmShutdown -> quiet period satisfied OR timeout exceeded -> cleanup -> state = TERMINATED -> terminationFuture completeNetty 负责让 EventLoop 有秩序地结束;应用负责让流量、请求、连接和业务状态有秩序地结束。两层都完成,才叫优雅停机。
下一篇进入 HTTP/1.1 Pipeline。我们会沿着 HttpServerCodec 看请求行、Header、Content 和 LastHttpContent 如何从字节流中拆出,以及为什么 HttpObjectAggregator 的便利背后是明确的内存上限和慢请求风险。
参考资料
1. Netty SingleThreadEventExecutor.java:https://github.com/netty/netty/blob/netty-4.1.135.Final/common/src/main/java/io/netty/util/concurrent/SingleThreadEventExecutor.java2. Netty MultithreadEventExecutorGroup.java:https://github.com/netty/netty/blob/netty-4.1.135.Final/common/src/main/java/io/netty/util/concurrent/MultithreadEventExecutorGroup.java3. Netty ChannelGroup.java:https://github.com/netty/netty/blob/netty-4.1.135.Final/transport/src/main/java/io/netty/channel/group/ChannelGroup.java