8.1 异常传播:流水线的质检规则


8.1 异常传播:流水线的质检规则

本节摘要:Netty 的异常沿入站方向逐工位传播,无人接手则到 tail 由 Netty 默认处理器打日志。本节讲清传播路径、默认行为的问题、分类收口方案,以及"关连接还是回错误包"的决策规则。

一、异常的行进路线

任何工位抛出异常(包括解码失败、业务空指针、写失败),Netty 会捕获并沿流水线向下游工位调用 exceptionCaught——方向与 channelRead 一致(head 到 tail),出站工位不参与。若一路无人处理,走到 tail 时由 Netty 内置兜底:

// Netty 内置 TailContext 的兜底行为(等价逻辑) protected void onUnhandledInboundException(Throwable cause) { logger.warn("An exceptionCaught() event was fired, and it reached " + "at the tail of the pipeline. It usually means the last handler " + "in the pipeline did not handle the exception.", cause); }

只打一行 WARN,连接继续活着。这有两个隐患:其一,异常量一大日志被刷爆,真正的线索被淹没;其二,出过错的连接可能已处于半解析状态(解码器内部缓冲错乱),继续用它等于带病生产。

二、先分类,再处置

不是所有异常都该一关了之。按来源与含义分四类,处置各不相同:

异常类型 典型代表 处置建议
协议错误 解码器抛出的 CorruptedFrame、非法长度 回协议错误包后关闭,客户端需重连重试
对端正常断开 IOException 中的 Connection reset / Broken pipe 静默关闭,不留 ERROR 日志
业务可恢复错误 参数校验失败、余额不足 回业务错误码,连接保留
系统级异常 空指针、数据库超时 记 ERROR 带堆栈,关闭连接防带病运行

分类的价值在于降噪:线上最怕的就是把前两类也打成 ERROR,值班同学对告警脱敏,真事故反被忽略。

三、收口 Handler:统一质检工位

把分类逻辑放进流水线最后一个工位,全线异常在此收口:

@ChannelHandler.Sharable public class ExceptionGuard extends ChannelDuplexHandler { private static final Logger log = LoggerFactory.getLogger(ExceptionGuard.class); @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { if (cause instanceof DecoderException de && de.getCause() instanceof CorruptedFrameException) { replyAndClose(ctx, ProtoErr.BAD_FRAME); // 协议错:回错误包再关 return; } if (cause instanceof IOException) { ctx.close(); // 对端断开:静默关 return; } if (cause instanceof BizException biz) { ctx.writeAndFlush(biz.toErrorPacket()); // 业务错:回包保连接 return; } log.error("未预期异常 关闭连接 remote={}", ctx.channel().remoteAddress(), cause); // 系统错:带堆栈记录 ctx.close(); } private void replyAndClose(ChannelHandlerContext ctx, ProtoErr err) { ctx.writeAndFlush(err.toPacket()) .addListener(ChannelFutureListener.CLOSE); // 写完再关,给对端收到错误包的机会 } } // 装配:放在 addLast 的最后一位,兜住全线 ch.pipeline().addLast(decoder, encoder, bizHandler, new ExceptionGuard());

三个细节值得咀嚼:ChannelFutureListener.CLOSE 保证错误包先写出去再关(直接 close 对端可能收不到);@Sharable 让一个实例服务全部连接(收口器无状态);IOException 在 Windows 与 Linux 上消息文本不同,用类型判断而不是字符串匹配。

四、出站异常的盲区

exceptionCaught 只收入站方向的异常。出站写失败(连接已断还 write)走的是另一条路:写操作返回的 ChannelFuture 会标记失败。所以两处都要设防:

ctx.writeAndFlush(resp).addListener(future -> { if (!future.isSuccess()) { future.cause().printStackTrace(); // 出站失败在这里暴露,exceptionCaught 看不见 } });

漏掉出站监听是"日志干净但消息没到"的典型成因——流水线看着毫无异常,实际上写全失败了。

五、别在异常路径里再抛异常

收口 Handler 自己抛异常会怎样?Netty 会再次捕获继续传播,极端情况下形成"异常处理递归"。守规矩的写法:收口工位内部全 try-catch,日志调用也包住(appender 故障时不能反噬业务线程):

try { log.error(...); } catch (Throwable ignore) { } // 日志系统自身故障不扩散

⚠️ 常见坑三连:收口 Handler 没放最后一位,前面的工位异常直接漏到 tail 兜底;把对端断开打成 ERROR,日志噪音淹没真事故;catch 里只 e.printStackTrace(),生产日志框架根本收不到。

💡 关键直觉:异常处理的目标不是"消灭异常",而是三件事:给每类异常一个明确出路(回包或关连接)、把线索留在正确的日志级别、保证出错后的连接不带病运行。质检工位收的不是异常,是秩序。

本节要点回顾

  • 传播路径:入站方向逐工位传递,无人接手到 tail 打 WARN,连接存活。
  • 四分类处置:协议错回包关连接、对端断开静默关、业务错回包保连接、系统错记堆栈关连接。
  • 收口工位放最后:一个 @Sharable 的 ExceptionGuard 兜全线。
  • 写完再关:ChannelFutureListener.CLOSE 保证错误包送达。
  • 出站是盲区:写失败只体现在返回的 Future,必须加监听。
  • 异常路径防递归:收口器内部全包 try,日志调用也不例外。

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