Tracon在内存泄漏上还做了一个小的改动:当JVM抛出错误时,通过一个全局的异常处理类(UncaughtExceptionHandler)直接退出应用。因为通常情况下,当应用程序遇到了OutOfMemoryError错误时,已经无法自我恢复。
class LoggingExceptionHandler implements Thread.UncaughtExceptionHandler { private static final Logger logger = Logger.getLogger(LoggingExceptionHandler.class); /** 注册成默认处理器 */ static void registerAsDefault() { Thread.setDefaultUncaughtExceptionHandler(new LoggingExceptionHandler()); } @Override public void uncaughtException(Thread t, Throwable e) { if (e instanceof Exception) { logger.error("Uncaught exception killed thread named '" + t.getName() + "'.", e); } else { logger.fatal("Uncaught error killed thread named '" + t.getName() + "'." + " Exiting now.", e); System.exit(1); } } }限制回收器使用解决了泄漏问题,但是一个读取速度很慢的后端还是会消耗大量缓存。Tracon中通过使用channelWritabilityChanged事件来缓解写入缓存压力。通过增加如下处理器,可以关联两个channel的读写:
/** * 监听当前inbound管道是否可写,设置关联的channel是否自动读取。 * 这可以让代理通知另外一端当前channel有一个读取很慢的消费者, * 仅当消费者准备完成后再进行数据读取。 */ public class WritabilityHandler extends ChannelInboundHandlerAdapter { private final Channel otherChannel; public WritabilityHandler(Channel otherChannel) { this.otherChannel = otherChannel; } @Override public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { boolean writable = ctx.channel().isWritable(); otherChannel.config().setOption(ChannelOption.AUTO_READ, writable); super.channelWritabilityChanged(ctx); } }当发送缓存到达高水位线时,将被标记为不可写,当发送缓存降低到低水位线时,重新被标记为可写。默认情况下,高水位线为64kb,低水位线为32kb。这些参数可以根据实际情况进行修改。
避免写异常丢失
当发生写操作失败时,如果没有对promise设置监听器,写操作失败会被忽略,这对于系统稳定性的分析会有很大影响。为了避免这种情况的发生,针对promise的监听器非常重要,但是如果每次创建promise时都需要设置一个日志记录的监听器,成本比较高,也容易遗忘。针对这种情况,Tracon中针对outbound事件设置了专门的处理器,统一为写操作的promise设置日志记录监听器:
@Singleton @Sharable public class PromiseFailureHandler extends ChannelOutboundHandlerAdapter { private final Logger logger = Logger.getLogger(PromiseFailureHandler.class); @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { promise.addListener(future -> { if (!future.isSuccess()) { logger.info("Write on channel %s failed", promise.cause(), ctx.channel()); } }); super.write(ctx, msg, promise); } }这样,只需要在pipeline中添加该处理器即可记录所有的写异常日志。
HTTP解码器重构
Netty 4对HTTP解码器做了重构,特别完善了对分块数据的支持。HTTP消息体被拆分成HttpContent对象,如果HTTP数据通过分块的方式传输,会有多个HttpContent顺序到达,当数据块传输结束时,会有一个LastHttpContent对象达到。这里需要特别注意的是,LastHttpContent继承自HttpContent,千万不能用以下方式来处理:
if (msg instanceof HttpContent) { ... } if (msg instanceof LastHttpContent) { … // 最后一个分块会重复处理,前面的if已经包含了LastHttpContent }对于LastHttpContent还有一个需要注意的是,接收到这个对象时,HTTP消息体可能已经传输完了,此时LastHttpContent只是作为HTTP传输的结束符(类似EOF)。
灰度发布
这次升级Netty 4,涉及到100多个文件共8000多行代码。并且,由于线程模型和内存模型的修改,Tracon的替换必须非常小心。
在完成了发布前的单元测试、集成测试之后,首先需要部署到生产环境,并关闭流量。这样,代理服务能够和后端服务交互,同时避免用户真实流量导入。此时,需要正对这些服务做最终的确认,确保和线上后端服务交互没有任何问题。
完成验证之后,才能够开始逐步引入用户流量,最终完成Netty 4版本的Tracon升级。经过实际验证,使用UnpooledByteBufAllocator分配内存和之前Netty 3版本性能基本相同,期待以后使用PooledByteBufAllocator会有更好的性能。
总结