Square从Netty 3升级到Netty 4的经验

Tracon是Square公司的反向代理软件,最初它主要用于协调后段架构从传统单体架构向微服务架构的转换。作为反向代理前端,Tracon需要有非常优秀的性能,同时能够支撑微服务架构下的各种功能定制,例如:服务发现、配置和生命周期管理等。因此Tracon网络层基于Netty构建,以提供高效代理服务。

Tracon已经上线运行3年,其代码行数也增加到30000行。基于Netty 3的代理模块在如此庞大复杂的应用中运转正常,并抽离成独立模块应用到Square内部认证代理服务中。

Netty 4?

Netty 4已经发布3年了,相比于Netty 3,Netty 4在内存模型和线程模型上都进行了修改。现在Netty 4已经非常成熟,并且对于Square公司来说,Netty 4还有一个重大特性:对HTTP/2协议的原生支持。Square期望其移动设备都使用HTTP/2协议,并且正在将后台RPC框架切换到gRPC:一个基于HTTP/2协议的RPC框架。因此,Tracon作为代理服务,必须支持HTTP/2协议。

Tracon已经完成了到Netty 4的升级,整个升级过程也不是一帆风顺的,以下着重介绍一些在升级过程中容易遇到的问题。

单线程channel

和Netty 3不同,Netty 4的inbound(数据输入)事件和outbound(数据输出)事件的所有处理器(handler)都在同一个线程中。这是得在编写处理器的时候,可以移除线程安全相关的代码。但是,这个变化也使得在升级过程中遇到条件竞争导致的问题。

在Netty 3中,针对pipeline的操作都是线程安全的,但是在Netty 4中,所有操作都会以事件的形式放入事件循环中异步执行。作为代理服务的Tracon,会有一个独立的inbound channel和上游服务器进行交互,一个独立的outbound channel和下游服务器进行交互。为了提高性能,和下游服务器的连接会被缓存起来,因此当事件循环中的事件触发了写操作时,这些写操作可能会并发进行。这对于Netty 3来说没有问题,每个写操作都会完成后再返回;但是对于Netty 4,这些操作都进入了事件循环,可能会导致消息的乱序。

因此,在分块测试中,偶尔会遇到发出去的数据不是按照顺序到达,导致测试失败。

当从Netty 3升级到Netty 4时,如果有事件在事件循环外触发时,必须特别注意这些事件会被异步的调度。

连接何时真正建立?

Netty 3中,连接建立之后会发出channelConnected事件;而在Netty 4中,这个事件变成了channelActive。对于一般应用程序来说,这个改动变化不大,修改一下对应的事件处理方法即可。但是Tracon使用了双向TLS认证以确认对方身份。

对于两个版本的SslHandler,TLS握手完成消息处理方式完全不同。在Netty 3中,SslHandler在channelConnected事件处理方法中阻塞,并完成整个TLS握手。因此后续的处理器在channelConnected事件处理方法中就可以获得完成握手的SSLSession。Netty 4则不同,由于其事件机制,SslHandler完成TLS握手也是异步进行的,因此直接在channelConnected事件中,是无法获取到SSLSession的,此时TLS握手还没有完成。对应的SslHandler会在TLS握手完成之后,发出自定义的SslHandshakeCompletionEvent事件。

对于Netty 4,TLS握手完成后的逻辑应该改成:

@Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws if (evt.equals(SslHandshakeCompletionEvent.SUCCESS)) { Principal peerPrincipal = engine.getSession().getPeerPrincipal(); // 身份验证 // ... } super.userEventTriggered(ctx, evt); }

NIO内存泄漏

由于NIO使用direct内存,对于Netty这类网络库,监控direct内存是很有必要的,这可以通过使用JMX beanjava.nio:type=BufferPool,name=direct来进行。

Netty 4引入了基于线程局部变量的回收器(thread-local Recyler)来回收对象池。默认情况下,一个回收器可以最多持有262k个对象,对于ByteBuf来说,小于64k的对象都默认共用缓存。也就是说,每个回收器最多可以持有17G的direct内存。

通常情况下,NIO缓存足够应付瞬间的数据量。但是如果有一个读取速度很慢的后端,会大大增加内存使用。另外,当缓存中的NIO内存在被其他线程读写时,分配该内存的线程会无法回收这些内存。

对于回收器无法回收导致内存耗尽的问题,Netty项目也做了一些修正,以解决限制对象增长的问题:

从升级Netty 4的经验来看,建议所有开发者基于可用内存和线程数来配置回收器。回收器最大持有对象数可以通过-Dio.netty.recycler.maxCapacity参数设置,共用内存最大限制可以通过-Dio.netty.threadLocalDirectBufferSize参数设置。如果要完全关闭回收器,可以将-Dio.netty.recycler.maxCapacity设置为0,从Tracon的使用过程来看,使用回收器并没有对性能又多大的提升。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/14866.html