Netty版本升级及线程模型详解

做者 李林锋 发布于 2015年2月7日 | 注意:GTLC全球技术领导力峰会,500+CTO技聚从新定义技术领导力!18 讨论后端

1. 背景

1.1. Netty 3.X系列版本现状

根据对Netty社区部分用户的调查,结合Netty在其它开源项目中的使用状况,咱们能够看出目前Netty商用的主流版本集中在3.X和4.X上,其中以Netty 3.X系列版本使用最为普遍。promise

Netty社区很是活跃,3.X系列版本从2011年2月7日发布的netty-3.2.4 Final版本到2014年12月17日发布的netty-3.10.0 Final版本,版本跨度达3年多,期间共推出了61个Final版本。安全

1.2. 升级仍是坚守老版本

相比于其它开源项目,Netty用户的版本升级之路更加艰辛,最根本的缘由就是Netty 4对Netty 3没有作到很好的前向兼容。服务器

相关厂商内容微信

经过探针技术,实现Java应用程序自我防御

新Java,新将来

你离成为一位合格的技术领导者还有多远?

你了解技术领导与技术管理的差异吗?

相关赞助商网络

QCon全球软件开发大会上海站,2016年10月20日-22日,上海宝华万豪酒店,精彩内容抢先看多线程

因为版本不兼容,大多数老版本使用者的想法就是既然升级这么麻烦,我暂时又不须要使用到Netty 4的新特性,当前版本还挺稳定,就暂时先不升级,之后看看再说。架构

坚守老版本还有不少其它的理由,例如考虑到线上系统的稳定性、对新版本的熟悉程度等。不管如何升级Netty都是一件大事,特别是对Netty有直接强依赖的产品。并发

从上面的分析能够看出,坚守老版本彷佛是个不错的选择;可是,“理想是美好的,现实倒是残酷的”,坚守老版本并不是老是那么容易,下面咱们就看下被迫升级的案例。

1.3. “被迫”升级到Netty 4.X

除了为了使用新特性而主动进行的版本升级,大多数升级都是“被迫的”。下面咱们对这些升级缘由进行分析。

  1. 公司的开源软件管理策略:对于那些大厂,不一样部门和产品线依赖的开源软件版本常常不一样,为了对开源依赖进行统一管理,下降安全、维护和管理成本,每每会指定优选的软件版本。因为Netty 4.X 系列版本已经很是成熟,由于,不少公司都优选Netty 4.X版本。
  2. 维护成本:不管是依赖Netty 3.X,仍是Netty4.X,每每须要在原框架之上作定制。例如,客户端的短连重连、心跳检测、流控等。分别对Netty 4.X和3.X版本实现两套定制框架,开发和维护成本都很是高。根据开源软件的使用策略,当存在版本冲突的时候,每每会选择升级到更高的版本。对于Netty,依然遵循这个规则。
  3. 新特性:Netty 4.X相比于Netty 3.X,提供了不少新的特性,例如优化的内存管理池、对MQTT协议的支持等。若是用户须要使用这些新特性,最简便的作法就是升级Netty到4.X系列版本。
  4. 更优异的性能:Netty 4.X版本相比于3.X老版本,优化了内存池,减小了GC的频率、下降了内存消耗;经过优化Rector线程池模型,用户的开发更加简单,线程调度也更加高效。

1.4. 升级不当付出的代价

表面上看,类库包路径的修改、API的重构等彷佛是升级的重头戏,你们每每把注意力放到这些“明枪”上,但真正隐藏和致命的倒是“暗箭”。若是对Netty底层的事件调度机制和线程模型不熟悉,每每就会“中枪”。

本文以几个比较典型的真实案例为例,经过问题描述、问题定位和问题总结,让这些隐藏的“暗箭”再也不伤人。

因为Netty 4线程模型改变致使的升级事故还有不少,限于篇幅,本文不一一枚举,这些问题万变不离其宗,只要抓住线程模型这个关键点,所谓的疑难杂症都将迎刃而解。

2. Netty升级以后遭遇内存泄露

2.1. 问题描述

随着JVM虚拟机和JIT即时编译技术的发展,对象的分配和回收是个很是轻量级的工做。可是对于缓冲区Buffer,状况却稍有不一样,特别是对于堆外直接内存的分配和回收,是一件耗时的操做。为了尽可能重用缓冲区,Netty4.X提供了基于内存池的缓冲区重用机制。性能测试代表,采用内存池的ByteBuf相比于朝生夕灭的ByteBuf,性能高23倍左右(性能数据与使用场景强相关)。

业务应用的特色是高并发、短流程,大多数对象都是朝生夕灭的短生命周期对象。为了减小内存的拷贝,用户指望在序列化的时候直接将对象编码到PooledByteBuf里,这样就不须要为每一个业务消息都从新申请和释放内存。

业务的相关代码示例以下:

//在业务线程中初始化内存池分配器,分配非堆内存
 ByteBufAllocator allocator = new PooledByteBufAllocator(true);
 ByteBuf buffer = allocator.ioBuffer(1024);
//构造订购请求消息并赋值,业务逻辑省略
SubInfoReq infoReq = new SubInfoReq ();
infoReq.setXXX(......);
//将对象编码到ByteBuf中
codec.encode(buffer, info);
//调用ChannelHandlerContext进行消息发送
ctx.writeAndFlush(buffer);

业务代码升级Netty版本并重构以后,运行一段时间,Java进程就会宕机,查看系统运行日志发现系统发生了内存泄露(示例堆栈):

图2-1 OOM内存溢出堆栈

对内存进行监控(切换使用堆内存池,方便对内存进行监控),发现堆内存一直飙升,以下所示(示例堆内存监控):

图2-2 堆内存监控

2.2. 问题定位

使用jmap -dump:format=b,file=netty.bin PID 将堆内存dump出来,经过IBM的HeapAnalyzer工具进行分析,发现ByteBuf发生了泄露。

由于使用了内存池,因此首先怀疑是否是申请的ByteBuf没有被释放致使?查看代码,发现消息发送完成以后,Netty底层已经调用ReferenceCountUtil.release(message)对内存进行了释放。这是怎么回事呢?难道Netty 4.X的内存池有Bug,调用release操做释放内存失败?

考虑到Netty 内存池自身Bug的可能性不大,首先从业务的使用方式入手分析:

  1. 内存的分配是在业务代码中进行,因为使用到了业务线程池作I/O操做和业务操做的隔离,实际上内存是在业务线程中分配的;
  2. 内存的释放操做是在outbound中进行,按照Netty 3的线程模型,downstream(对应Netty 4的outbound,Netty 4取消了upstream和downstream)的handler也是由业务调用者线程执行的,也就是说释放跟分配在同一个业务线程中进行。

初次排查并无发现致使内存泄露的根因,束手无策之际开始查看Netty的内存池分配器PooledByteBufAllocator的Doc和源码实现,发现内存池实际是基于线程上下文实现的,相关代码以下:

final ThreadLocal<PoolThreadCache> threadCache = new ThreadLocal<PoolThreadCache>() {
        private final AtomicInteger index = new AtomicInteger();
        @Override
        protected PoolThreadCache initialValue() {
            final int idx = index.getAndIncrement();
            final PoolArena<byte[]> heapArena;
            final PoolArena<ByteBuffer> directArena;
            if (heapArenas != null) {
                heapArena = heapArenas[Math.abs(idx % heapArenas.length)];
            } else {
                heapArena = null;
            }
            if (directArenas != null) {
                directArena = directArenas[Math.abs(idx % directArenas.length)];
            } else {
                directArena = null;
            }
            return new PoolThreadCache(heapArena, directArena);
        }

也就是说内存的申请和释放必须在同一线程上下文中,不能跨线程。跨线程以后实际操做的就不是同一块内存区域,这会致使不少严重的问题,内存泄露即是其中之一。内存在A线程申请,切换到B线程释放,实际是没法正确回收的。

经过对Netty内存池的源码分析,问题基本锁定。保险起见进行简单验证,经过对单条业务消息进行Debug,发现执行释放的果真不是业务线程,而是Netty的NioEventLoop线程:当某个消息被彻底发送成功以后,会经过ReferenceCountUtil.release(message)方法释放已经发送成功的ByteBuf。

问题定位出来以后,继续溯源,发现Netty 4修改了Netty 3的线程模型:在Netty 3的时候,upstream是在I/O线程里执行的,而downstream是在业务线程里执行。当Netty从网络读取一个数据报投递给业务handler的时候,handler是在I/O线程里执行;而当咱们在业务线程中调用write和writeAndFlush向网络发送消息的时候,handler是在业务线程里执行,直到最后一个Header handler将消息写入到发送队列中,业务线程才返回。

Netty4修改了这一模型,在Netty 4里inbound(对应Netty 3的upstream)和outbound(对应Netty 3的downstream)都是在NioEventLoop(I/O线程)中执行。当咱们在业务线程里经过ChannelHandlerContext.write发送消息的时候,Netty 4在将消息发送事件调度到ChannelPipeline的时候,首先将待发送的消息封装成一个Task,而后放到NioEventLoop的任务队列中,由NioEventLoop线程异步执行。后续全部handler的调度和执行,包括消息的发送、I/O事件的通知,都由NioEventLoop线程负责处理。

下面咱们分别经过对比Netty 3和Netty 4的消息接收和发送流程,来理解两个版本线程模型的差别:

Netty 3的I/O事件处理流程:

图2-3 Netty 3 I/O事件处理线程模型

Netty 4的I/O消息处理流程:

图2-4 Netty 4 I/O事件处理线程模型

2.3. 问题总结

Netty 4.X版本新增的内存池确实很是高效,可是若是使用不当则会致使各类严重的问题。诸如内存泄露这类问题,功能测试并无异常,若是相关接口没有进行压测或者稳定性测试而直接上线,则会致使严重的线上问题。

内存池PooledByteBuf的使用建议:

  1. 申请以后必定要记得释放,Netty自身Socket读取和发送的ByteBuf系统会自动释放,用户不须要作二次释放;若是用户使用Netty的内存池在应用中作ByteBuf的对象池使用,则须要本身主动释放;
  2. 避免错误的释放:跨线程释放、重复释放等都是非法操做,要避免。特别是跨线程申请和释放,每每具备隐蔽性,问题定位难度较大;
  3. 防止隐式的申请和分配:以前曾经发生过一个案例,为了解决内存池跨线程申请和释放问题,有用户对内存池作了二次包装,以实现多线程操做时,内存始终由包装的管理线程申请和释放,这样能够屏蔽用户业务线程模型和访问方式的差别。谁知运行一段时间以后再次发生了内存泄露,最后发现原来调用ByteBuf的write操做时,若是内存容量不足,会自动进行容量扩展。扩展操做由业务线程执行,这就绕过了内存池管理线程,发生了“引用逃逸”。该Bug只有在ByteBuf容量动态扩展的时候才发生,所以,上线很长一段时间没有发生,直到某一天......所以,你们在使用Netty 4.X的内存池时要格外小心,特别是作二次封装时,必定要对内存池的实现细节有深入的理解。

3. Netty升级以后遭遇数据被篡改

3.1. 问题描述

某业务产品,Netty3.X升级到4.X以后,系统运行过程当中,偶现服务端发送给客户端的应答数据被莫名“篡改”。

业务服务端的处理流程以下:

  1. 将解码后的业务消息封装成Task,投递到后端的业务线程池中执行;
  2. 业务线程处理业务逻辑,完成以后构造应答消息发送给客户端;
  3. 业务应答消息的编码经过继承Netty的CodeC框架实现,即Encoder ChannelHandler;
  4. 调用Netty的消息发送接口以后,流程继续,根据业务场景,可能会继续操做原发送的业务对象。

业务相关代码示例以下:

//构造订购应答消息
SubInfoResp infoResp = new SubInfoResp();
//根据业务逻辑,对应答消息赋值
infoResp.setResultCode(0);
infoResp.setXXX();
后续赋值操做省略......
//调用ChannelHandlerContext进行消息发送
ctx.writeAndFlush(infoResp);
//消息发送完成以后,后续根据业务流程进行分支处理,修改infoResp对象
infoResp.setXXX();
后续代码省略......

3.2. 问题定位

首先对应答消息被非法“篡改”的缘由进行分析,通过定位发现当发生问题时,被“篡改”的内容是调用writeAndFlush接口以后,由后续业务分支代码修改应答消息致使的。因为修改操做发生在writeAndFlush操做以后,按照Netty 3.X的线程模型不该该出现该问题。

在Netty3中,downstream是在业务线程里执行的,也就是说对SubInfoResp的编码操做是在业务线程中执行的,当编码后的ByteBuf对象被投递到消息发送队列以后,业务线程才会返回并继续执行后续的业务逻辑,此时修改应答消息是不会改变已完成编码的ByteBuf对象的,因此确定不会出现应答消息被篡改的问题。

初步分析应该是因为线程模型发生变动致使的问题,随后查验了Netty 4的线程模型,果真发生了变化:当调用outbound向外发送消息的时候,Netty会将发送事件封装成Task,投递到NioEventLoop的任务队列中异步执行,相关代码以下:

@Override
 public void invokeWrite(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
        if (msg == null) {
            throw new NullPointerException("msg");
        }
        validatePromise(ctx, promise, true);
        if (executor.inEventLoop()) {
            invokeWriteNow(ctx, msg, promise);
        } else {
            AbstractChannel channel = (AbstractChannel) ctx.channel();
            int size = channel.estimatorHandle().size(msg);
            if (size > 0) {
                ChannelOutboundBuffer buffer = channel.unsafe().outboundBuffer();
                // Check for null as it may be set to null if the channel is closed already
                if (buffer != null) {
                    buffer.incrementPendingOutboundBytes(size);
                }
            }
            safeExecuteOutbound(WriteTask.newInstance(ctx, msg, size, promise), promise, msg);
        }
    }

经过上述代码能够看出,Netty首先对当前的操做的线程进行判断,若是操做自己就是由NioEventLoop线程执行,则调用写操做;不然,执行线程安全的写操做,即将写事件封装成Task,放入到任务队列中由Netty的I/O线程执行,业务调用返回,流程继续执行。

经过源码分析,问题根源已经很清楚:系统升级到Netty 4以后,线程模型发生变化,响应消息的编码由NioEventLoop线程异步执行,业务线程返回。这时存在两种可能:

  1. 若是编码操做先于修改应答消息的业务逻辑执行,则运行结果正确;
  2. 若是编码操做在修改应答消息的业务逻辑以后执行,则运行结果错误。

因为线程的执行前后顺序没法预测,所以该问题隐藏的至关深。若是对Netty 4和Netty3的线程模型不了解,就会掉入陷阱。

Netty 3版本业务逻辑没有问题,流程以下:

图3-1 升级以前的业务流程线程模型

升级到Netty 4版本以后,业务流程因为Netty线程模型的变动而发生改变,致使业务逻辑发生问题:

图3-2 升级以后的业务处理流程发生改变

3.3. 问题总结

不少读者在进行Netty 版本升级的时候,只关注到了包路径、类和API的变动,并无注意到隐藏在背后的“暗箭”- 线程模型变动。

升级到Netty 4的用户须要根据新的线程模型对已有的系统进行评估,重点须要关注outbound的ChannelHandler,若是它的正确性依赖于Netty 3的线程模型,则极可能在新的线程模型中出问题,多是功能问题或者其它问题。

4. Netty升级以后性能严重降低

4.1. 问题描述

相信不少Netty用户都看过以下相关报告:

在Twitter,Netty 4 GC开销降为五分之一:Netty 3使用Java对象表示I/O事件,这样简单,但会产生大量的垃圾,尤为是在咱们这样的规模下。Netty 4在新版本中对此作出了更改,取代生存周期短的事件对象,而以定义在生存周期长的通道对象上的方法处理I/O事件。它还有一个使用池的专用缓冲区分配器。

每当收到新信息或者用户发送信息到远程端,Netty 3均会建立一个新的堆缓冲区。这意味着,对应每个新的缓冲区,都会有一个‘new byte[capacity]’。这些缓冲区会致使GC压力,并消耗内存带宽:为了安全起见,新的字节数组分配时会用零填充,这会消耗内存带宽。然而,用零填充的数组极可能会再次用实际的数据填充,这又会消耗一样的内存带宽。若是Java虚拟机(JVM)提供了建立新字节数组而又无需用零填充的方式,那么咱们原本就能够将内存带宽消耗减小50%,可是目前没有那样一种方式。

在Netty 4中,代码定义了粒度更细的API,用来处理不一样的事件类型,而不是建立事件对象。它还实现了一个新缓冲池,那是一个纯Java版本的 jemalloc (Facebook也在用)。如今,Netty不会再由于用零填充缓冲区而浪费内存带宽了。

咱们比较了两个分别创建在Netty 3和4基础上echo协议服务器。(Echo很是简单,这样,任何垃圾的产生都是Netty的缘由,而不是协议的缘由)。我使它们服务于相同的分布式echo协议客户端,来自这些客户端的16384个并发链接重复发送256字节的随机负载,几乎使千兆以太网饱和。

根据测试结果,Netty 4:

  • GC中断频率是原来的1/5: 45.5 vs. 9.2次/分钟
  • 垃圾生成速度是原来的1/5: 207.11 vs 41.81 MiB/秒

正是看到了相关的Netty 4性能提高报告,不少用户选择了升级。过后一些用户反馈Netty 4并无跟产品带来预期的性能提高,有些甚至还发生了很是严重的性能降低,下面咱们就以某业务产品的失败升级经历为案例,详细分析下致使性能降低的缘由。

4.2. 问题定位

首先经过JMC等性能分析工具对性能热点进行分析,示例以下(信息安全等缘由,只给出分析过程示例截图):

图4-1 JMC性能监控分析

经过对热点方法的分析,发如今消息发送过程当中,有两处热点:

  1. 消息发送性能统计相关Handler;
  2. 编码Handler。

对使用Netty 3版本的业务产品进行性能对比测试,发现上述两个Handler也是热点方法。既然都是热点,为啥切换到Netty4以后性能降低这么厉害呢?

经过方法的调用树分析发现了两个版本的差别:在Netty 3中,上述两个热点方法都是由业务线程负责执行;而在Netty 4中,则是由NioEventLoop(I/O)线程执行。对于某个链路,业务是拥有多个线程的线程池,而NioEventLoop只有一个,因此执行效率更低,返回给客户端的应答时延就大。时延增大以后,天然致使系统并发量下降,性能降低。

找出问题根因以后,针对Netty 4的线程模型对业务进行专项优化,性能达到预期,远超过了Netty 3老版本的性能。

Netty 3的业务线程调度模型图以下所示:充分利用了业务多线程并行编码和Handler处理的优点,周期T内能够处理N条业务消息。

图4-2 Netty 3业务调度性能模型

切换到Netty 4以后,业务耗时Handler被I/O线程串行执行,所以性能发生比较大的降低:

图4-3 Netty 4业务调度性能模型

4.3. 问题总结

该问题的根因仍是因为Netty 4的线程模型变动引发,线程模型变动以后,不只影响业务的功能,甚至对性能也会形成很大的影响。

对Netty的升级须要从功能、兼容性和性能等多个角度进行综合考虑,切不可只盯着API变动这个芝麻,而丢掉了性能这个西瓜。API的变动会致使编译错误,可是性能降低却隐藏于无形之中,稍不留意就会中招。

对于讲究快速交付、敏捷开发和灰度发布的互联网应用,升级的时候更应该要小心。

5. Netty升级以后上下文丢失

5.1. 问题描述

为了提高业务的二次定制能力,下降对接口的侵入性,业务使用线程变量进行消息上下文的传递。例如消息发送源地址信息、消息Id、会话Id等。

业务同时使用到了一些第三方开源容器,也提供了线程级变量上下文的能力。业务经过容器上下文获取第三方容器的系统变量信息。

升级到Netty 4以后,业务继承自Netty的ChannelHandler发生了空指针异常,不管是业务自定义的线程上下文、仍是第三方容器的线程上下文,都获取不到传递的变量值。

5.2. 问题定位

首先检查代码,看业务是否传递了相关变量,确认业务传递以后怀疑跟Netty 版本升级相关,调试发现,业务ChannelHandler获取的线程上下文对象和以前业务传递的上下文不是同一个。这就说明执行ChannelHandler的线程跟处理业务的线程不是同一个线程!

查看Netty 4线程模型的相关Doc发现,Netty修改了outbound的线程模型,正好影响了业务消息发送时的线程上下文传递,最终致使线程变量丢失。

5.3. 问题总结

一般业务的线程模型有以下几种:

  1. 业务自定义线程池/线程组处理业务,例如使用JDK 1.5提供的ExecutorService;
  2. 使用J2EE Web容器自带的线程模型,常见的如JBoss和Tomcat的HTTP接入线程等;
  3. 隐式的使用其它第三方框架的线程模型,例如使用NIO框架进行协议处理,业务代码隐式使用的就是NIO框架的线程模型,除非业务明确的实现自定义线程模型。

在实践中咱们发现不少业务使用了第三方框架,可是只熟悉API和功能,对线程模型并不清楚。某个类库由哪一个线程调用,糊里糊涂。为了方便变量传递,又随意的使用线程变量,实际对背后第三方类库的线程模型产生了强依赖。当容器或者第三方类库升级以后,若是线程模型发生了变动,则原有功能就会发生问题。

鉴于此,在实际工做中,尽可能不要强依赖第三方类库的线程模型,若是确实没法避免,则必须对它的线程模型有深刻和清晰的了解。当第三方类库升级以后,须要检查线程模型是否发生变动,若是发生变化,相关的代码也须要考虑同步升级。

6. Netty3.X VS Netty4.X 之线程模型

经过对三个具备典型性的升级失败案例进行分析和总结,咱们发现有个共性:都是线程模型改变惹的祸!

下面小节咱们就详细得对Netty3和Netty4版本的I/O线程模型进行对比,以方便你们掌握二者的差别,在升级和使用中尽可能少踩雷。

6.1 Netty 3.X 版本线程模型

Netty 3.X的I/O操做线程模型比较复杂,它的处理模型包括两部分:

  1. Inbound:主要包括链路创建事件、链路激活事件、读事件、I/O异常事件、链路关闭事件等;
  2. Outbound:主要包括写事件、链接事件、监听绑定事件、刷新事件等。

咱们首先分析下Inbound操做的线程模型:

图6-1 Netty 3 Inbound操做线程模型

从上图能够看出,Inbound操做的主要处理流程以下:

  1. I/O线程(Work线程)将消息从TCP缓冲区读取到SocketChannel的接收缓冲区中;
  2. 由I/O线程负责生成相应的事件,触发事件向上执行,调度到ChannelPipeline中;
  3. I/O线程调度执行ChannelPipeline中Handler链的对应方法,直到业务实现的Last Handler;
  4. Last Handler将消息封装成Runnable,放入到业务线程池中执行,I/O线程返回,继续读/写等I/O操做;
  5. 业务线程池从任务队列中弹出消息,并发执行业务逻辑。

经过对Netty 3的Inbound操做进行分析咱们能够看出,Inbound的Handler都是由Netty的I/O Work线程负责执行。

下面咱们继续分析Outbound操做的线程模型:

图6-2 Netty 3 Outbound操做线程模型

从上图能够看出,Outbound操做的主要处理流程以下:

业务线程发起Channel Write操做,发送消息;

  1. Netty将写操做封装成写事件,触发事件向下传播;
  2. 写事件被调度到ChannelPipeline中,由业务线程按照Handler Chain串行调用支持Downstream事件的Channel Handler;
  3. 执行到系统最后一个ChannelHandler,将编码后的消息Push到发送队列中,业务线程返回;
  4. Netty的I/O线程从发送消息队列中取出消息,调用SocketChannel的write方法进行消息发送。

6.2 Netty 4.X 版本线程模型

相比于Netty 3.X系列版本,Netty 4.X的I/O操做线程模型比较简答,它的原理图以下所示:

图6-3 Netty 4 Inbound和Outbound操做线程模型

从上图能够看出,Outbound操做的主要处理流程以下:

  1. I/O线程NioEventLoop从SocketChannel中读取数据报,将ByteBuf投递到ChannelPipeline,触发ChannelRead事件;
  2. I/O线程NioEventLoop调用ChannelHandler链,直到将消息投递到业务线程,而后I/O线程返回,继续后续的读写操做;
  3. 业务线程调用ChannelHandlerContext.write(Object msg)方法进行消息发送;
  4. 若是是由业务线程发起的写操做,ChannelHandlerInvoker将发送消息封装成Task,放入到I/O线程NioEventLoop的任务队列中,由NioEventLoop在循环中统一调度和执行。放入任务队列以后,业务线程返回;
  5. I/O线程NioEventLoop调用ChannelHandler链,进行消息发送,处理Outbound事件,直到将消息放入发送队列,而后唤醒Selector,进而执行写操做。

经过流程分析,咱们发现Netty 4修改了线程模型,不管是Inbound仍是Outbound操做,统一由I/O线程NioEventLoop调度执行。

6.3. 线程模型对比

在进行新老版本线程模型PK以前,首先仍是要熟悉下串行化设计的理念:

咱们知道当系统在运行过程当中,若是频繁的进行线程上下文切换,会带来额外的性能损耗。多线程并发执行某个业务流程,业务开发者还须要时刻对线程安全保持警戒,哪些数据可能会被并发修改,如何保护?这不只下降了开发效率,也会带来额外的性能损耗。

为了解决上述问题,Netty 4采用了串行化设计理念,从消息的读取、编码以及后续Handler的执行,始终都由I/O线程NioEventLoop负责,这就意外着整个流程不会进行线程上下文的切换,数据也不会面临被并发修改的风险,对于用户而言,甚至不须要了解Netty的线程细节,这确实是个很是好的设计理念,它的工做原理图以下:

图6-4 Netty 4的串行化设计理念

一个NioEventLoop聚合了一个多路复用器Selector,所以能够处理成百上千的客户端链接,Netty的处理策略是每当有一个新的客户端接入,则从NioEventLoop线程组中顺序获取一个可用的NioEventLoop,当到达数组上限以后,从新返回到0,经过这种方式,能够基本保证各个NioEventLoop的负载均衡。一个客户端链接只注册到一个NioEventLoop上,这样就避免了多个I/O线程去并发操做它。

Netty经过串行化设计理念下降了用户的开发难度,提高了处理性能。利用线程组实现了多个串行化线程水平并行执行,线程之间并无交集,这样既能够充分利用多核提高并行处理能力,同时避免了线程上下文的切换和并发保护带来的额外性能损耗。

了解完了Netty 4的串行化设计理念以后,咱们继续看Netty 3线程模型存在的问题,总结起来,它的主要问题以下:

  1. Inbound和Outbound实质都是I/O相关的操做,它们的线程模型居然不统一,这给用户带来了更多的学习和使用成本;
  2. Outbound操做由业务线程执行,一般业务会使用线程池并行处理业务消息,这就意味着在某一个时刻会有多个业务线程同时操做ChannelHandler,咱们须要对ChannelHandler进行并发保护,一般须要加锁。若是同步块的范围不当,可能会致使严重的性能瓶颈,这对开发者的技能要求很是高,下降了开发效率;
  3. Outbound操做过程当中,例如消息编码异常,会产生Exception,它会被转换成Inbound的Exception并通知到ChannelPipeline,这就意味着业务线程发起了Inbound操做!它打破了Inbound操做由I/O线程操做的模型,若是开发者按照Inbound操做只会由一个I/O线程执行的约束进行设计,则会发生线程并发访问安全问题。因为该场景只在特定异常时发生,所以错误很是隐蔽!一旦在生产环境中发生此类线程并发问题,定位难度和成本都很是大。

讲了这么多,彷佛Netty 4 完胜 Netty 3的线程模型,其实并不尽然。在特定的场景下,Netty 3的性能可能更高,就如本文第4章节所讲,若是编码和其它Outbound操做很是耗时,由多个业务线程并发执行,性能确定高于单个NioEventLoop线程。

可是,这种性能优点不是不可逆转的,若是咱们修改业务代码,将耗时的Handler操做前置,Outbound操做不作复杂业务逻辑处理,性能一样不输于Netty 3,可是考虑内存池优化、不会反复建立Event、不须要对Handler加锁等Netty 4的优化,总体性能Netty 4版本确定会更高。

总而言之,若是用户真正熟悉并掌握了Netty 4的线程模型和功能类库,相信不只仅开发会更加简单,性能也会更优!

6.4. 思考

就Netty 而言,掌握线程模型的重要性不亚于熟悉它的API和功能。不少时候我遇到的功能、性能等问题,都是因为缺少对它线程模型和原理的理解致使的,结果咱们就以讹传讹,认为Netty 4版本不如3好用等。

不能说全部开源软件的版本升级必定都赛过老版本,就Netty而言,我认为Netty 4版本相比于老的Netty 3,确实是历史的一大进步。

7. 做者简介

李林锋,2007年毕业于东北大学,2008年进入华为公司从事高性能通讯软件的设计和开发工做,有7年NIO设计和开发经验,精通Netty、Mina等NIO框架和平台中间件,现任华为软件平台架构部架构师,《Netty权威指南》做者

相关文章
相关标签/搜索