8.3 可观测与容错:日志、重连与降级


8.3 可观测与容错:日志、重连与降级

本节摘要:出事后的复盘能力靠日志与链路标识事前铺好;断线后的自愈能力靠带退避的重连状态机;扛不住时的生存能力靠降级与熔断。本节三块设施都给出可落地代码,最后补上优雅停机这件"最容易忘的收尾工程"。

一、日志:不是打得多,而是找得到

Netty 服务的日志三原则:关联(一条消息全链路可串)、降噪(正常路径少打、异常路径打全)、异步(日志不拖累 IO 线程)

// 连接标识贯穿:连接建立时生成短 ID,后续日志全部携带 public class TraceHandler extends ChannelInboundHandlerAdapter { public static final AttributeKey<String> TRACE_ID = AttributeKey.valueOf("traceId"); @Override public void channelActive(ChannelHandlerContext ctx) { String tid = Long.toHexString(System.nanoTime()); ctx.channel().attr(TRACE_ID).set(tid); log.info("[{}] connected from {}", tid, ctx.channel().remoteAddress()); ctx.fireChannelActive(); } @Override public void channelInactive(ChannelHandlerContext ctx) { log.info("[{}] disconnected", ctx.channel().attr(TRACE_ID).get()); ctx.fireChannelInactive(); } // 业务工位打印时先取 attr 里的 tid 拼进日志前缀 }

配套两条工程纪律:日志框架用异步 appender(同步日志在高峰期就是最大串行段,7.2 已算过账);ERROR 级别只留给"需要人介入"的事,对端断开、心跳超时这类可预期事件打 INFO——告警的信噪比决定响应速度。跨服务的链路追踪用同样的思路升级:把上游传来的 traceId 放进 attr 与协议头,跨进程透传。

二、重连:客户端的自愈状态机

长连接客户端必然面对断线。裸写"断了就重连"会有两个事故形态:服务端闪断时客户端疯狂重连(形成连接风暴的自伤版);多个客户端同步退避,形成规律性流量尖峰(羊群效应)。标准解法是指数退避加抖动

public class ReconnectHandler extends ChannelInboundHandlerAdapter { private final Bootstrap bootstrap; private int attempts = 0; @Override public void channelInactive(ChannelHandlerContext ctx) { scheduleReconnect(); } private void scheduleReconnect() { long delay = Math.min((1L << attempts) * 1000, 60_000); // 1s、2s、4s…封顶 60s delay += ThreadLocalRandom.current().nextLong(500); // 抖动:打散重连节拍 attempts++; bootstrap.config().group().schedule(this::connect, delay, TimeUnit.MILLISECONDS); } private void connect() { bootstrap.connect().addListener((ChannelFutureListener) f -> { if (f.isSuccess()) { attempts = 0; // 成功即复位 log.info("reconnected"); } else { scheduleReconnect(); // 继续退避 } }); } }

客户端连接状态机

状态机里藏着一个常被忽略的分支:重连成功后要重新走握手与订阅——连接是新的,旧连接上的登录态、订阅关系全没了。把"连接级初始化"统一放在 channelActive 里(而不是只放在启动时),重连后的重建就自动完成。

三、降级与熔断:保住核心链路

依赖下游的服务要有"断尾"能力。轻量版熔断器用三个状态实现:闭合(正常放行)→ 打开(连续失败超阈值,快速失败不再请求)→ 半开(冷却期后放一个探测请求试探)。

public class SimpleBreaker { private final AtomicInteger failures = new AtomicInteger(); private volatile boolean open = false; private volatile long openSince; public boolean allow() { if (!open) return true; // 冷却 30 秒后半开试探 if (System.currentTimeMillis() - openSince > 30_000) { open = false; failures.set(0); return true; } return false; // 打开态:快速失败 } public void onSuccess() { failures.set(0); } public void onFailure() { if (failures.incrementAndGet() >= 5) { open = true; openSince = System.currentTimeMillis(); } } }

降级与熔断配合使用:熔断器打开期间,非核心功能直接走降级路径(返回缓存、默认值或友好提示),核心链路的资源不被拖垮。判断"什么是核心"的方法是容量倒推:把第七章压测得出的极限容量,按核心与非核心的优先级排一遍,砍单顺序就是降级顺序。

四、优雅停机:最容易被忘的收尾

发布重启时直接 kill,正在处理的请求被腰斩、客户端集体断线触发重连风暴。优雅停机的标准序列:

// 1. 先摘流量:从注册中心反注册或让 LB 摘除本节点 // 2. 停止接新连接:关闭监听通道 serverChannel.close().sync(); // 3. 给存量请求留处理时间(按业务 P99 设定,如 30 秒) Thread.sleep(30_000); // 4. 释放线程组:Netty 拒绝新任务、执行完存量任务后退出 boss.shutdownGracefully(2, 15, TimeUnit.SECONDS).sync(); workers.shutdownGracefully(2, 15, TimeUnit.SECONDS).sync(); // 5. 现在进程可以安全退出

shutdownGracefully(静默期, 超时, 单位) 的两个时间分别是"确认无新任务前的观察期"与"强制退出的最后期限"。配合容器生命周期钩子(SIGTERM 触发上述序列),滚动发布就能做到用户无感。

⚠️ 常见坑:把重连延迟写成固定值且不带抖动——服务端一次重启,几十万客户端同一毫秒打回来,刚救活的服务瞬间二次倒地。退避加抖动不是优化项,是必选项。

💡 关键直觉:可观测与容错都是"负资产投资"——平时只花钱不产出,出事时才兑现价值。铺设计略:每条消息可追溯(日志)、断线必自愈(退避重连)、过载会断尾(熔断降级)、重启无感知(优雅停机)。

出厂前自查清单

  • 日志三原则:关联(traceId 贯穿)、降噪(可预期事件不刷 ERROR)、异步(不占 IO 线程)。
  • 重连状态机:指数退避加抖动防风暴与羊群,成功复位计数,channelActive 里做连接级重建。
  • 轻量熔断:闭合、打开、半开三态,连续失败开闸,冷却后半开试探。
  • 降级顺序靠容量倒推:压测极限排优先级,砍单顺序即降级顺序。
  • 优雅停机四步:摘流量、关监听、等存量、收线程组,容器钩子接 SIGTERM。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U