微信号:infoqchina

介绍:有内容的技术社区媒体

Netty版本升级血泪史之线程篇(下)

2015-02-14 12:46 InfoQ


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


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


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操作的主要处理流程如下:

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


2,Netty将写操作封装成写事件,触发事件向下传播;


3,写事件被调度到ChannelPipeline中,由业务线程按照Handler Chain串行调用支持Downstream事件的Channel Handler;


4,执行到系统最后一个ChannelHandler,将编码后的消息Push到发送队列中,业务线程返回;


5,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权威指南》作者。


联系方式:新浪微博 Nettying 微信:Nettying 微信公众号:Netty之家


 
InfoQ 更多文章 Facebook如何实现PB级别数据库自动化备份 学术派Google软件工程师Matt Welsh谈移动开发趋势 Spotify为什么要使用一些“无聊”的技术? 妹纸们放假了,汉纸们做啥? 大多数重构可以避免
猜您喜欢 记忆的盒子 简洁明了的UI交互手册 iOS应用层架构之CDD(附Demo) 拜读:大神程序员为什么牛? 微信通讯协议的学习