Java NIO通信框架在电信领域的实践
Netty是业界最流行的NIO框架之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的,它已经得到成百上千的商用项目验证,例如Hadoop的RPC框架avro使用Netty作为底层通信框架;很多其他业界主流的RPC框架,也使用Netty来构建高性能的异步通信能力。
通过对Netty的分析,我们将它的优点总结如下:
1) API使用简单,开发门槛低;
2) 功能强大,预置了多种编解码功能,支持多种主流协议;
3) 定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展;
4) 性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优;
5) 成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼;
6) 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会加入;
7) 经历了大规模的商业应用考验,质量得到验证。在互联网、大数据、网络游戏、企业应用、电信软件等众多行业得到成功商用,证明了它已经完全能够满足不同行业的商业应用了。
正是因为这些优点,Netty逐渐成为Java NIO编程的首选框架,它也是华为公司首选的Java NIO通信框架,公司已经将其纳入到公司级的优选开源第三方软件库中。
3. Netty在电信领域的实践
电信行业软件的几个特点:
1) 高可靠性:5个9;
2) 高性能、低时延;
3) 大规模组网:例如中国移动、Telfonica 拉美十三国、沃达丰等,业务组网规模都非常大;
4) 复杂的网络形态:对接不同设备提供商的网元和系统。
3.1. 高性能、低时延
3.1.1. 非阻塞I/O模型
在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源。
我们采用Netty的NIO传输模式来提升I/O操作的效率,节省线程等其它资源开销,它的模型如下所示:
图3-1 Netty的非阻塞I/O调度模型
3.1.2. 高性能的序列化框架
在华为软件,对于序列化框架的选择,我们遵循如下几个原则:
1) 序列化后的码流大小(网络带宽的占用);
2) 序列化&反序列化的性能(CPU、内存等资源占用);
3) 是否支持跨语言(异构系统的对接和开发语言切换);
4) 高并发调用时的性能,是否随着线程并发数线性增长。
基于上述的指标,目前最常用的选择是:Google的ProtoBuf和Apache的Thrift。
Netty原生提供了对ProtoBuf序列化框架的支持,它的优点如下:
1) 在谷歌内部长期使用,产品成熟度高;
2) 跨语言、支持多种语言,包括C++、Java和Python;
3) 编码后的消息更小,更加有利于存储和传输;
4) 编解码的性能非常高;
5) 支持不同协议版本的前向兼容;
6) 支持定义可选和必选字段。
Netty ProtoBuf 服务端开发示例如下:
// 配置服务端的NIO线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 100) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) { ch.pipeline().addLast( new ProtobufVarint32FrameDecoder()); ch.pipeline().addLast( new ProtobufDecoder( SubscribeReqProto.SubscribeReq .getDefaultInstance())); ch.pipeline().addLast( new ProtobufVarint32LengthFieldPrepender()); ch.pipeline().addLast(new ProtobufEncoder()); ch.pipeline().addLast(new SubReqServerHandler()); } });
Thrift相对复杂一些,需要将编解码框架从Thrift中剥离出来,然后利用Netty编解码框架的扩展性定制实现,在此不再赘述。
3.1.3. 收敛的Reactor线程模型
Java线程采用抢占的方式争夺CPU等资源,当系统线程数增大到一定量级之后,性能不仅没有提升,反而下降。
对于大型的电信应用,如果使用Tomcat等做Web容器,为了保证吞吐量和性能,HTTP线程池的最大线程数往往配置为1024。在系统运行期间我们Dump线程堆栈,发现大量的线程竞争,这不仅导致HTTP协议栈的性能下降,更影响其它业务处理线程的执行效率。
使用Netty之后,我们通过控制NioEventLoopGroup的NioEventLoop个数来收敛线程,防止线程膨胀。NioEventLoop聚合了一个多路复用器Selector,可以高效的处理N个Channel,它的线程模型如下:
图3-1 Netty Reactor线程模型
3.1.4. 其它优化
为了进一步提升性能,降低时延,我们还采用了其它一些优化措施,总结如下:
1) 使用Netty 4的内存池,减少业务高峰期ByteBuf频繁创建和销毁导致的GC频率和时间;
2) 在程序中充分利用Netty提供的“零拷贝”特性,减少额外的内存拷贝,例如使用CompositeByteBuf而不是分别为Head和Body各创建一个ByteBuf对象;
3) TCP参数的优化,设置合理的Send和Receive Buffer,通常建议值为64K - 128K;
4) 软中断:如果Linux内核版本支持RPS(2.6.35以上版本),开启RPS后可以实现软中断,提升网络吞吐量;
5) 无锁化串行开发理念:使用Netty 4.X版本,天生支持串行化处理;业务开发过程中,遵循Netty 4的线程模型优化理念,防止人为增加线程竞争。
3.2. 高HA
3.2.1. 内存保护
为了提升内存的利用率,Netty提供了内存池和对象池。但是,基于缓存池实现以后需要对内存的申请和释放进行严格的管理,否则很容易导致内存泄漏。
如果不采用内存池技术实现,每次对象都是以方法的局部变量形式被创建,使用完成之后,只要不再继续引用它,JVM会自动释放。但是,一旦引入内存池机制,对象的生命周期将由内存池负责管理,这通常是个全局引用,如果不显式释放JVM是不会回收这部分内存的。
对于Netty的用户而言,使用者的技术水平差异很大,一些对JVM内存模型和内存泄漏机制不了解的用户,可能只记得申请内存,忘记主动释放内存,特别是JAVA程序员。
为了防止因为用户遗漏导致内存泄漏,Netty在Pipe line的尾Handler中自动对内存进行释放。
缓冲区内存溢出保护:做过协议栈的读者都知道,当我们对消息进行解码的时候,需要创建缓冲区。缓冲区的创建方式通常有两种:
1) 容量预分配,在实际读写过程中如果不够再扩展;
2) 根据协议消息长度创建缓冲区。
在实际的商用环境中,如果遇到畸形码流攻击、协议消息编码异常、消息丢包等问题时,可能会解析到一个超长的长度字段。笔者曾经遇到过类似问题,报文长度字段值竟然是2G多,由于代码的一个分支没有对长度上限做有效保护,结果导致内存溢出。系统重启后几秒内再次内存溢出,幸好及时定位出问题根因,险些酿成严重的事故。
Netty提供了编解码框架,因此对于解码缓冲区的上限保护就显得非常重要。下面,我们看下Netty是如何对缓冲区进行上限保护的:
1) 在内存分配的时候指定缓冲区长度上限;
2) 在对缓冲区进行写入操作的时候,如果缓冲区容量不足需要扩展,首先对最大容量进行判断,如果扩展后的容量超过上限,则拒绝扩展;
3) 在解码的时候,对消息长度进行判断,如果超过最大容量上限,则抛出解码异常,拒绝分配内存。
3.2.2. 流量整形
电信系统一般都有多个网元组成,例如参与短信互动,会涉及到手机、基站、短信中心、短信网关、SP/CP等网元。不同网元或者部件的处理性能不同。为了防止因为浪涌业务或者下游网元性能低导致下游网元被压垮,有时候需要系统提供流量整形功能。
流量整形(Traffic Shaping)是一种主动调整流量输出速率的措施。一个典型应用是基于下游网络结点的TP指标来控制本地流量的输出。流量整形与流量监管的主要区别在于,流量整形对流量监管中需要丢弃的报文进行缓存——通常是将它们放入缓冲区或队列内,也称流量整形(Traffic Shaping,简称TS)。当令牌桶有足够的令牌时,再均匀的向外发送这些被缓存的报文。流量整形与流量监管的另一区别是,整形可能会增加延迟,而监管几乎不引入额外的延迟。
流量整形的原理示意图如下:
图3-2 Netty 流量整形原理图
Netty内置两种流量整形策略,可以方便的被用户添加和使用:
1) 全局流量整形的作用范围是进程级的,无论你创建了多少个Channel,它的作用域针对所有的Channel。用户可以通过参数设置:报文的接收速率、报文的发送速率、整形周期;
2) 单链路流量整形与全局流量整形的最大区别就是它以单个链路为作用域,可以对不同的链路设置不同的整形策略,整形参数与全局流量整形相同。
3.2.3. 其它可靠性措施
其它比较重要的可靠性措施如下:
1) 客户端连接超时控制策略;
2) 链路断连重连策略;
3) 链路异常关闭资源释放;
4) 解码失败的异常处理策略;
5) 链路异常的捕获和处理;
6) I/O线程的释放。
参考:
http://www.infoq.com/cn/articles/netty-high-performance
http://normanmaurer.me/presentations/2014-facebook-eng-netty/slides.html