夜雨聆风学习资料网

ARTICLE · 1161223

Netty 优雅停机源码:shutdownGracefully 的 quiet period 为什么重要(系列第16篇)

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. 1. ServerChannel:监听端口并接收新连接;
  2. 2. Child Channels:承载已有连接和请求;
  3. 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 回答“从开始关闭起最多容忍多久”。两者关系:

条件
结果
quietPeriod = 0
当前清理完成后尽快退出
连续 quietPeriod 无任务
正常终止
不断有任务但未到 timeout
继续执行和等待
总时间超过 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. 停机验收要看业务而不只是端口

至少验证:

指标
期望
新连接数
摘流量后快速归零
在途请求数
在 drain 窗口内下降
Child Channel 数
按协议关闭策略收敛
pending tasks
最终归零或只剩可取消任务
pending outbound bytes
发送完成或按超时失败
termination Future
在进程 deadline 内完成
客户端错误率
不出现集中 reset/timeout

还应在压测环境执行真实滚动发布,而不是只调用一次单元测试。长连接、慢请求、大响应和断线重连都会改变停机曲线。

12. 优雅停机是服务生命周期协议

源码主链:

shutdownGracefully  -> state = SHUTTING_DOWN  -> wakeup EventLoop  -> run remaining tasks / shutdown hooks  -> confirmShutdown  -> quiet period satisfied OR timeout exceeded  -> cleanup  -> state = TERMINATED  -> terminationFuture complete

Netty 负责让 EventLoop 有秩序地结束;应用负责让流量、请求、连接和业务状态有秩序地结束。两层都完成,才叫优雅停机。

下一篇进入 HTTP/1.1 Pipeline。我们会沿着 HttpServerCodec 看请求行、Header、Content 和 LastHttpContent 如何从字节流中拆出,以及为什么 HttpObjectAggregator 的便利背后是明确的内存上限和慢请求风险。

参考资料

  1. 1. Netty SingleThreadEventExecutor.java:https://github.com/netty/netty/blob/netty-4.1.135.Final/common/src/main/java/io/netty/util/concurrent/SingleThreadEventExecutor.java
  2. 2. Netty MultithreadEventExecutorGroup.java:https://github.com/netty/netty/blob/netty-4.1.135.Final/common/src/main/java/io/netty/util/concurrent/MultithreadEventExecutorGroup.java
  3. 3. Netty ChannelGroup.java:https://github.com/netty/netty/blob/netty-4.1.135.Final/transport/src/main/java/io/netty/channel/group/ChannelGroup.java

相关学习资料