探秘Netty7:一篇文章,读懂Netty的高性能架构之道

简介: Netty是一个高性能、异步事件驱动的NIO框架,它提供了对TCP、UDP和文件传输的支持,作为一个异步NIO框架,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。

Netty是一个高性能、异步事件驱动的NIO框架,它提供了对TCP、UDP和文件传输的支持,作为一个异步NIO框架,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。 

作为当前最流行的NIO框架,Netty在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,一些业界著名的开源组件也基于Netty的NIO框架构建。

为什么选择Netty

Netty是业界最流行的NIO框架之一,它的健壮性、功能、性能、可定制性和可扩展性在同类框架中都是首屈一指的,它已经得到成百上千的商用项目验证,例如Hadoop的RPC框架avro使用Netty作为底层通信框架;很多其他业界主流的RPC框架,也使用Netty来构建高性能的异步通信能力。

Netty架构分析

Netty 采用了比较典型的三层网络架构进行设计,逻辑架构图如下所示: 

#第一层,Reactor 通信调度层,它由一系列辅助类完成,包括 Reactor 线程 NioEventLoop 以及其父类、NioSocketChannel/NioServerSocketChannel 以及其父 类、ByteBuffer 以及由其衍生出来的各种 Buffer、Unsafe 以及其衍生出的各种内部类等。该层的主要职责就是监听网络的读写和连接操作,负责将网络层的数据读取到内存缓冲区中,然后触发各种网络事件,例如连接创建、连接激活、读事 件、写事件等等,将这些事件触发到 PipeLine 中,由 PipeLine 充当的职责链来进行后续的处理。

#第二层,职责链 PipeLine,它负责事件在职责链中的有序传播,同时负责动态的编排职责链,职责链可以选择监听和处理自己关心的事件,它可以拦截处理和向后/向前传播事件,不同的应用的 Handler 节点的功能也不同,通常情况下,往往会开发编解码 Hanlder 用于消息的编解码,它可以将外部的协议消息转换成内部的 POJO 对象,这样上层业务侧只需要关心处理业务逻辑即可,不需要感知底层的协议差异和线程模型差异,实现了架构层面的分层隔离。

#第三层,业务逻辑处理层。可以分为两类:纯粹的业务逻辑 处理,例如订单处理;应用层协议管理,例如 HTTP 协议、FTP 协议等。

I/O模型

传统同步阻塞I/O模式如下图所示:

几种I/O模型的功能和特性对比:

Netty的I/O模型基于非阻塞I/O实现,底层依赖的是JDK NIO框架的Selector。Selector提供选择已经就绪的任务的能力。简单来讲,Selector会不断地轮询注册在其上的Channel,如果某个Channel上面有新的TCP连接接入、读和写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。

一个多路复用器Selector可以同时轮询多个Channel,由于JDK1.5_update10版本(+)使用了epoll()代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制。这也就意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端,这确实是个非常巨大的技术进步。使用非阻塞I/O模型之后,Netty解决了传统同步阻塞I/O带来的性能、吞吐量和可靠性问题。

线程调度模型

常用的Reactor线程模型有三种,分别如下:

#Reactor单线程模型:Reactor单线程模型,指的是所有的I/O操作都在同一个NIO线程上面完成。对于一些小容量应用场景,可以使用单线程模型。

#Reactor多线程模型:Rector多线程模型与单线程模型最大的区别就是有一组NIO线程处理I/O操作。主要用于高并发、大业务量场景。

#主从Reactor多线程模型:主从Reactor线程模型的特点是服务端用于接收客户端连接的不再是个1个单独的NIO线程,而是一个独立的NIO线程池。利用主从NIO线程模型,可以解决1个服务端监听线程无法有效处理所有客户端连接的性能不足问题。

事实上,Netty的线程模型并非固定不变,通过在启动辅助类中创建不同的EventLoopGroup实例并通过适当的参数配置,就可以支持上述三种Reactor线程模型.

在大多数场景下,并行多线程处理可以提升系统的并发性能。但是,如果对于共享资源的并发访问处理不当,会带来严重的锁竞争,这最终会导致性能的下降。为了尽可能的避免锁竞争带来的性能损耗,可以通过串行化设计,即消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。

为了尽可能提升性能,Netty采用了串行无锁化设计,在I/O线程内部进行串行操作,避免多线程竞争导致的性能下降。表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。

Reactor模型

Java NIO非堵塞技术实际是采取反应器模式,或者说是观察者(observer)模式为我们监察I/O端口,如果有内容进来,会自动通知我们,这样,我们就不必开启多个线程死等,从外界看,实现了流畅的I/O读写,不堵塞了。

NIO 有一个主要的类Selector,这个类似一个观察者,只要我们把需要探知的socketchannel告诉Selector,我们接着做别的事情,当有事件发生时,他会通知我们,传回一组SelectionKey,我们读取这些Key,就会获得我们刚刚注册过的socketchannel,然后,我们从这个Channel中读取数据,接着我们可以处理这些数据。

反应器模式与观察者模式在某些方面极为相似:当一个主体发生改变时,所有依属体都得到通知。不过,观察者模式与单个事件源关联,而反应器模式则与多个事件源关联 。

一般模型

EventLoopGroup:对应于Reactor模式中的定时器的角色,不断地检索是否有事件可用(I/O线程-BOSS),然后交给分离者将事件分发给对应的事件绑定的handler(WORK线程)。

经验分享:在客户端编程中经常容易出现在EVENTLOOP上做定时任务的,如果定时任务耗时很长或者存在阻塞,那么可能会将I/O操作挂起(因为要等到定时任务做完才能做别的操作)。解决方法:用独立的EventLoopGroup

序列化方式

影响序列化性能的关键因素总结如下:

- 序列化后的码流大小(网络带宽占用)

- 序列化&反序列化的性能(CPU资源占用)

- 并发调用的性能表现:稳定性、线性增长、偶现的时延毛刺等

对Java序列化和二进制编码分别进行性能测试,编码100万次,测试结果表明:Java序列化的性能只有二进制编码的6.17%左右。

Netty默认提供了对Google Protobuf的支持,通过扩展Netty的编解码接口,用户可以实现其它的高性能序列化框架,例如Thrift的压缩二进制编解码框架。

不同的应用场景对序列化框架的需求也不同,对于高性能应用场景Netty默认提供了Google的Protobuf二进制序列化框架,如果用户对其它二进制序列化框架有需求,也可以基于Netty提供的编解码框架扩展实现。

Netty架构剖析之可靠性

Netty面临的可靠性挑战:

1. 作为RPC框架的基础网络通信框架,一旦故障将导致无法进行远程服务(接口)调用。

2. 作为应用层协议的基础通信框架,一旦故障将导致应用协议栈无法正常工作。

3. 网络环境复杂(例如推送服务的GSM/3G/WIFI网络),故障不可避免,业务却不能中断。

从应用场景看,Netty是基础的通信框架,一旦出现Bug,轻则需要重启应用,重则可能导致整个业务中断。它的可靠性会影响整个业务集群的数据通信和交换,在当今以分布式为主的软件架构体系中,通信中断就意味着整个业务中断,分布式架构下对通信的可靠性要求非常高。

从运行环境看,Netty会面临恶劣的网络环境,这就要求它自身的可靠性要足够好,平台能够解决的可靠性问题需要由Netty自身来解决,否则会导致上层用户关注过多的底层故障,这将降低Netty的易用性,同时增加用户的开发和运维成本。

Netty的可靠性是如此重要,它的任何故障都可能会导致业务中断,蒙受巨大的经济损失。因此,Netty在版本的迭代中不断加入新的可靠性特性来满足用户日益增长的高可靠和健壮性需求。

链路有效性检测

Netty提供的心跳检测机制分为三种:

- 读空闲,链路持续时间t没有读取到任何消息

- 写空闲,链路持续时间t没有发送任何消息

- 读写空闲,链路持续时间t没有接收或者发送任何消息

当网络发生单通、连接被防火墙Hang住、长时间GC或者通信线程发生非预期异常时,会导致链路不可用且不易被及时发现。特别是异常发生在凌晨业务低谷期间,当早晨业务高峰期到来时,由于链路不可用会导致瞬间的大批量业务失败或者超时,这将对系统的可靠性产生重大的威胁。

从技术层面看,要解决链路的可靠性问题,必须周期性的对链路进行有效性检测。目前最流行和通用的做法就是心跳检测。

心跳检测机制分为三个层面:

1. TCP层面的心跳检测,即TCP的Keep-Alive机制,它的作用域是整个TCP协议栈;

2. 协议层的心跳检测,主要存在于长连接协议中。例如SMPP协议;

3. 应用层的心跳检测,它主要由各业务产品通过约定方式定时给对方发送心跳消息实现。

Keep-Alive仅仅是TCP协议层会发送连通性检测包,但并不代表设置了Keep-Alive就是长连接了。

心跳检测的目的就是确认当前链路可用,对方活着并且能够正常接收和发送消息。

做为高可靠的NIO框架,Netty也提供了基于链路空闲的心跳检测机制:

- 读空闲,链路持续时间t没有读取到任何消息

- 写空闲,链路持续时间t没有发送任何消息

- 读写空闲,链路持续时间t没有接收或者发送任何消息(netty自带心跳处理Handler IdleStateHandler)

客户端和服务端之间连接断开机制

TCP连接的建立需要三个分节(三次握手),终止则需要四个分节。 

对于大量短连接的情况下,经常出现卡在FIN_WAIT2和TIMEWAIT状态的连接,等待系统回收,但是操作系统底层回收的时间频率很长,导致SOCKET被耗尽。

TCP状态图

TCP/IP半关闭

从上述讲的TCP关闭的四个分节可以看出,被动关闭执行方,发送FIN分节的前提是TCP套接字对应应用程序调用close产生的。如果服务端有数据发送给客户端那么可能存在服务端在接受到FIN之后,需要将数据发送到客户端才能发送FIN字节。这种处于业务考虑的情形通常称为半关闭。

半关闭可能导致大量socket处于CLOSE_WAIT状态

谁负责关闭连接合理

连接关闭触发的条件通常分为如下几种:

1. 数据发送完成(发送到对端并且收到响应),关闭连接 

2. 通信过程中产生异常

3. 特殊指令强制要求关闭连接

对于第一种,通常关闭时机是,数据发送完成方发起(客户端触发居多); 对于第二种,异常产生方触发(例如残包、错误数据等)发起。但是此种情况可能也导致压根无法发送FIN。对于第三种,通常是用于运维等。由命令发起方产生。

流量整形

流量整形(Traffic Shaping)是一种主动调整流量输出速率的措施。

Netty的流量整形有两个作用:

1. 防止由于上下游网元性能不均衡导致下游网元被压垮,业务流程中断

2. 防止由于通信模块接收消息过快,后端业务线程处理不及时导致的"撑死"问题

流量整形的原理示意图如下:

流量整形(Traffic Shaping)是一种主动调整流量输出速率的措施。一个典型应用是基于下游网络结点的TP指标来控制本地流量的输出。流量整形与流量监管的主要区别在于,流量整形对流量监管中需要丢弃的报文进行缓存——通常是将它们放入缓冲区或队列内,也称流量整形(Traffic Shaping,简称TS)。当令牌桶有足够的令牌时,再均匀的向外发送这些被缓存的报文。流量整形与流量监管的另一区别是,整形可能会增加延迟,而监管几乎不引入额外的延迟。

#全局流量整形:全局流量整形的作用范围是进程级的,无论你创建了多少个Channel,它的作用域针对所有的Channel。用户可以通过参数设置:报文的接收速率、报文的发送速率、整形周期。[GlobalChannelTrafficShapingHandler]

#链路级流量整形:单链路流量整形与全局流量整形的最大区别就是它以单个链路为作用域,可以对不同的链路设置不同的整形策略。[ChannelTrafficShapingHandler针对于每个channel]

优雅停机

Netty的优雅停机三部曲: 1. 不再接收新消息 2. 退出前的预处理操作 3. 资源的释放操作

Java的优雅停机通常通过注册JDK的ShutdownHook来实现,当系统接收到退出指令后,首先标记系统处于退出状态,不再接收新的消息,然后将积压的消息处理完,最后调用资源回收接口将资源销毁,最后各线程退出执行。

通常优雅退出需要有超时控制机制,例如30S,如果到达超时时间仍然没有完成退出前的资源回收等操作,则由停机脚本直接调用kill -9 pid,强制退出。

在实际项目中,Netty作为高性能的异步NIO通信框架,往往用作基础通信框架负责各种协议的接入、解析和调度等,例如在RPC和分布式服务框架中,往往会使用Netty作为内部私有协议的基础通信框架。 当应用进程优雅退出时,作为通信框架的Netty也需要优雅退出,主要原因如下:

1. 尽快的释放NIO线程、句柄等资源

2. 如果使用flush做批量消息发送,需要将积攒在发送队列中的待发送消息发送完成

3. 正在write或者read的消息,需要继续处理

4. 设置在NioEventLoop线程调度器中的定时任务,需要执行或者清理

Netty架构剖析之安全性

Netty面临的安全挑战:

- 对第三方开放

- 作为应用层协议的基础通信框架

安全威胁场景分析:

#对第三方开放的通信框架:如果使用Netty做RPC框架或者私有协议栈,RPC框架面向非授信的第三方开放,例如将内部的一些能力通过服务对外开放出去,此时就需要进行安全认证,如果开放的是公网IP,对于安全性要求非常高的一些服务,例如在线支付、订购等,需要通过SSL/TLS进行通信。

#应用层协议的安全性:作为高性能、异步事件驱动的NIO框架,Netty非常适合构建上层的应用层协议。由于绝大多数应用层协议都是公有的,这意味着底层的Netty需要向上层提供通信层的安全传输功能。

SSL/TLS

Netty安全传输特性:

- 支持SSL V2和V3

- 支持TLS

- 支持SSL单向认证、双向认证和第三方CA认证。

SSL单向认证流程图如下:

Netty通过SslHandler提供了对SSL的支持,它支持的SSL协议类型包括:SSL V2、SSL V3和TLS。

#单向认证:单向认证,即客户端只验证服务端的合法性,服务端不验证客户端。

#双向认证:与单向认证不同的是服务端也需要对客户端进行安全认证。这就意味着客户端的自签名证书也需要导入到服务端的数字证书仓库中。

#CA认证:基于自签名的SSL双向认证,只要客户端或者服务端修改了密钥和证书,就需要重新进行签名和证书交换,这种调试和维护工作量是非常大的。因此,在实际的商用系统中往往会使用第三方CA证书颁发机构进行签名和验证。我们的浏览器就保存了几个常用的CA_ROOT。每次连接到网站时只要这个网站的证书是经过这些CA_ROOT签名过的。就可以通过验证了。

可扩展的安全特性

通过Netty的扩展特性,可以自定义安全策略:

- IP地址黑名单机制

- 接入认证

- 敏感信息加密或者过滤机制

IP地址黑名单是比较常用的弱安全保护策略,它的特点就是服务端在与客户端通信的过程中,对客户端的IP地址进行校验,如果发现对方IP在黑名单列表中,则拒绝与其通信,关闭链路。

接入认证策略非常多,通常是较强的安全认证策略,例如基于用户名+密码的认证,认证内容往往采用加密的方式,例如Base64+AES等。

Netty架构剖析之扩展性

通过Netty的扩展特性,可以自定义安全策略:

- 线程模型可扩展

- 序列化方式可扩展

- 上层协议栈可扩展

- 提供大量的网络事件切面,方便用户功能扩展

Netty的架构可扩展性设计理念如下:

1. 判断扩展点,事先预留相关扩展接口,给用户二次定制和扩展使用

2. 主要功能点都基于接口编程,方便用户定制和扩展。

粘连包解决方案

TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。

粘包情况有两种:

1. 粘在一起的包都是完整的数据包

2. 粘在一起的包有不完整的包

解决粘连包的方法大致分为如下三种:

1. 发送方开启TCP_NODELAY 

2. 接收方简化或者优化流程尽可能快的接收数据

3. 认为强制分包每次只读一个完整的包

对于以上三种方式,第一种会加重网络负担,第二种治标不治本,第三种算比较合理的。 

第三种又可以分两种方式:

1. 每次都只读取一个完整的包,不如不足一个完整的包,就等下次再接收,如果缓冲区有N个包要接受,那么需要分N次才能接收完成

2. 有多少接收多少,将接収的数据缓存在一个临时的缓存中,交由后续的专门解码的线程/进程处理

以上两种分包方式,如果强制关闭程序,数据会存在丢失,第一种数据丢失在接收缓冲区;第二种丢失在程序自身缓存。

Netty自带的几种粘连包解决方案:

1. DelimiterBasedFrameDecoder 

2. FixedLengthFrameDecoder 

3. LengthFieldBasedFrameDecoder

Netty解包组包

对于TCP编程最常遇到的就是根据具体的协议进行组包或者解包。

根据协议的不同大致可以分为如下几种类型: 

1. JAVA平台之间通过JAVA序列化进行解包组包(object->byte->object)

2. 固定长度的包结构(定长每个包都是M个字节的长度)

3. 带有明确分隔符协议的解包组包(例如HTTP协议\r\n\r\n)

4. 可动态扩展的协议,此种协议通常遵循消息头+消息体的机制,其中消息头的长度是固定的,消息体的长度根据具体业务的不同长度可能不同。例如(SMPP协议、CMPP协议)

#序列化协议组包解包

可以使用的有:MessagePack、Google Protobuf、Hessian2

#固定长度解包组包

FixedLengthFrameDecoder 解包,MessageToByteEncoder 组包

#带有分隔符协议的解包组包

DelimiterBasedFrameDecoder 解包,MessageToByteEncoder 组包

#HTTP

io.netty.codec.http

#消息头固定长度,消息体不固定长度协议解包组包

LengthFieldBasedFrameDecoder

需要注意的是:对于解码的Handler必须做到在将ByteBuf解析成Object之后,需要将ByteBuf release()。

Netty Client断网重连机制

对于长连接的程序断网重连几乎是程序的标配。

断网重连具体可以分为两类:

1. CONNECT失败,需要重连

2. 程序运行过程中断网、远程强制关闭连接、收到错误包必须重连

对于第一种解决方案是:实现ChannelFutureListener 用来启动时监测是否连接成功,不成功的话重试。

Future-Listener机制

在并发编程中,我们通常会用到一组非阻塞的模型:Promise,Future,Callback。

其中的Future表示一个可能还没有实际完成的异步任务的结果,针对这个结果添加Callback以便在执行任务成功或者失败后做出响应的操作。而经由Promise交给执行者,任务执行者通过Promise可以标记任务完成或者失败。以上这套模型是很多异步非阻塞框架的基础。具体的理解可参见JDK的FutureTask和Callable。JDK的实现版本,在获取最终结果的时候,不得不做一些阻塞的方法等待最终结果的到来。Netty的Future机制是JDK机制的一个子版本,它支持给Future添加Listener,以方便EventLoop在任务调度完成之后调用。

数据安全性之滑动窗口协议

我们假设一个场景,客户端每次请求服务端必须得到服务端的一个响应,由于TCP的数据发送和数据接收是异步的,就存在必须存在一个等待响应的过程。该过程根据实现方式不同可以分为一下几类(部分是错误案例):

1. 每次发送一个数据包,然后进入休眠(sleep)或者阻塞(await)状态,直到响应回来或者超时,整个调用链结束。此场景是典型的一问一答的场景,效率极其低下

2. 读写分离,写模块只负责写,读模块则负责接收响应,然后做后续的处理。此种场景能尽可能的利用带宽进行读写。但是此场景不坐控速操作可能导致大量报文丢失或者重复发送。

3. 实现类似于Windowed Protocol。此窗口是以上两种方案的折中版,即允许一定数量的批量发送,又能保证数据的完整性。

对于 Netty ByteBuf 的零拷贝(Zero Copy) 的理解

作者:@xys1228
本文为作者原创,转载请注明出处:https://www.cnblogs.com/xys1228/p/6088805.html
Email:yongshun1228@gmail.com


目录

通过 CompositeByteBuf 实现零拷贝
通过 wrap 操作实现零拷贝
通过 slice 操作实现零拷贝
通过 FileRegion 实现零拷贝

此文章已同步发布在我的 segmentfault 专栏.

根据 Wiki 对 Zero-copy 的定义:

"Zero-copy" describes computer operations in which the CPU does not perform the task of copying data from one memory area to another. This is frequently used to save CPU cycles and memory bandwidth when transmitting a file over a network.

即所谓的 Zero-copy, 就是在操作数据时, 不需要将数据 buffer 从一个内存区域拷贝到另一个内存区域. 因为少了一次内存的拷贝, 因此 CPU 的效率就得到的提升.

在 OS 层面上的 Zero-copy 通常指避免在 用户态(User-space) 与 内核态(Kernel-space) 之间来回拷贝数据. 例如 Linux 提供的 mmap 系统调用, 它可以将一段用户空间内存映射到内核空间, 当映射成功后, 用户对这段内存区域的修改可以直接反映到内核空间; 同样地, 内核空间对这段区域的修改也直接反映用户空间. 正因为有这样的映射关系, 我们就不需要在 用户态(User-space) 与 内核态(Kernel-space) 之间拷贝数据, 提高了数据传输的效率.

而需要注意的是, Netty 中的 Zero-copy 与上面我们所提到到 OS 层面上的 Zero-copy 不太一样, Netty的 Zero-coyp 完全是在用户态(Java 层面)的, 它的 Zero-copy 的更多的是偏向于 优化数据操作 这样的概念.

Netty 的 Zero-copy 体现在如下几个个方面:

  • Netty 提供了 CompositeByteBuf 类, 它可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了各个 ByteBuf 之间的拷贝.
  • 通过 wrap 操作, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作.
  • ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝.
  • 通过 FileRegion 包装的FileChannel.tranferTo 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel, 避免了传统通过循环 write 方式导致的内存拷贝问题.

下面我们就来简单了解一下这几种常见的零拷贝操作.

通过 CompositeByteBuf 实现零拷贝

假设我们有一份协议数据, 它由头部和消息体组成, 而头部和消息体是分别存放在两个 ByteBuf 中的, 即:

1
2
ByteBuf header = ...
ByteBuf body = ...

我们在代码处理中, 通常希望将 header 和 body 合并为一个 ByteBuf, 方便处理, 那么通常的做法是:

ByteBuf allBuf = Unpooled.buffer(header.readableBytes() + body.readableBytes());
allBuf.writeBytes(header);
allBuf.writeBytes(body);

可以看到, 我们将 header 和 body 都拷贝到了新的 allBuf 中了, 这无形中增加了两次额外的数据拷贝操作了.

那么有没有更加高效优雅的方式实现相同的目的呢? 我们来看一下 CompositeByteBuf 是如何实现这样的需求的吧.

1
2
3
4
5
ByteBuf header = ...
ByteBuf body = ...
 
CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();
compositeByteBuf.addComponents( true , header, body);

上面代码中, 我们定义了一个 CompositeByteBuf 对象, 然后调用

1
2
3
public  CompositeByteBuf addComponents( boolean  increaseWriterIndex, ByteBuf... buffers) {
...
}

方法将 header 与 body 合并为一个逻辑上的 ByteBuf, 即:

不过需要注意的是, 虽然看起来 CompositeByteBuf 是由两个 ByteBuf 组合而成的, 不过在 CompositeByteBuf 内部, 这两个 ByteBuf 都是单独存在的, CompositeByteBuf 只是逻辑上是一个整体.

上面 CompositeByteBuf 代码还以一个地方值得注意的是, 我们调用 addComponents(boolean increaseWriterIndex, ByteBuf... buffers) 来添加两个 ByteBuf, 其中第一个参数是 true, 表示当添加新的 ByteBuf 时, 自动递增 CompositeByteBuf 的 writeIndex.
如果我们调用的是

1
compositeByteBuf.addComponents(header, body);

那么其实 compositeByteBuf 的 writeIndex 仍然是0, 因此此时我们就不可能从 compositeByteBuf 中读取到数据, 这一点希望大家要特别注意.

除了上面直接使用 CompositeByteBuf 类外, 我们还可以使用 Unpooled.wrappedBuffer 方法, 它底层封装了 CompositeByteBuf 操作, 因此使用起来更加方便:

1
2
3
4
ByteBuf header = ...
ByteBuf body = ...
 
ByteBuf allByteBuf = Unpooled.wrappedBuffer(header, body);

通过 wrap 操作实现零拷贝

例如我们有一个 byte 数组, 我们希望将它转换为一个 ByteBuf 对象, 以便于后续的操作, 那么传统的做法是将此 byte 数组拷贝到 ByteBuf 中, 即:

1
2
3
byte [] bytes = ...
ByteBuf byteBuf = Unpooled.buffer();
byteBuf.writeBytes(bytes);

显然这样的方式也是有一个额外的拷贝操作的, 我们可以使用 Unpooled 的相关方法, 包装这个 byte 数组, 生成一个新的 ByteBuf 实例, 而不需要进行拷贝操作. 上面的代码可以改为:

1
2
byte [] bytes = ...
ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);

可以看到, 我们通过 Unpooled.wrappedBuffer 方法来将 bytes 包装成为一个 UnpooledHeapByteBuf 对象, 而在包装的过程中, 是不会有拷贝操作的. 即最后我们生成的生成的 ByteBuf 对象是和 bytes 数组共用了同一个存储空间, 对 bytes 的修改也会反映到 ByteBuf 对象中.

Unpooled 工具类还提供了很多重载的 wrappedBuffer 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public  static  ByteBuf wrappedBuffer( byte [] array)
public  static  ByteBuf wrappedBuffer( byte [] array, int  offset, int  length)
 
public  static  ByteBuf wrappedBuffer(ByteBuffer buffer)
public  static  ByteBuf wrappedBuffer(ByteBuf buffer)
 
public  static  ByteBuf wrappedBuffer( byte []... arrays)
public  static  ByteBuf wrappedBuffer(ByteBuf... buffers)
public  static  ByteBuf wrappedBuffer(ByteBuffer... buffers)
 
public  static  ByteBuf wrappedBuffer( int  maxNumComponents, byte []... arrays)
public  static  ByteBuf wrappedBuffer( int  maxNumComponents, ByteBuf... buffers)
public  static  ByteBuf wrappedBuffer( int  maxNumComponents, ByteBuffer... buffers)

这些方法可以将一个或多个 buffer 包装为一个 ByteBuf 对象, 从而避免了拷贝操作.

通过 slice 操作实现零拷贝

slice 操作和 wrap 操作刚好相反, Unpooled.wrappedBuffer 可以将多个 ByteBuf 合并为一个, 而 slice 操作可以将一个 ByteBuf 切片 为多个共享一个存储区域的 ByteBuf 对象.
ByteBuf 提供了两个 slice 操作方法:

1
2
public  ByteBuf slice();
public  ByteBuf slice( int  index, int  length);

不带参数的 slice 方法等同于 buf.slice(buf.readerIndex(), buf.readableBytes()) 调用, 即返回 buf 中可读部分的切片. 而 slice(int index, int length) 方法相对就比较灵活了, 我们可以设置不同的参数来获取到 buf 的不同区域的切片.

下面的例子展示了 ByteBuf.slice 方法的简单用法:

1
2
3
ByteBuf byteBuf = ...
ByteBuf header = byteBuf.slice( 0 , 5 );
ByteBuf body = byteBuf.slice( 5 , 10 );

用 slice 方法产生 header 和 body 的过程是没有拷贝操作的, header 和 body 对象在内部其实是共享了 byteBuf 存储空间的不同部分而已. 即:

通过 FileRegion 实现零拷贝

Netty 中使用 FileRegion 实现文件传输的零拷贝, 不过在底层 FileRegion 是依赖于 Java NIO FileChannel.transfer 的零拷贝功能.

首先我们从最基础的 Java IO 开始吧. 假设我们希望实现一个文件拷贝的功能, 那么使用传统的方式, 我们有如下实现:

1
2
3
4
5
6
7
8
9
10
11
12
public  static  void  copyFile(String srcFile, String destFile) throws  Exception {
     byte [] temp = new  byte [ 1024 ];
     FileInputStream in = new  FileInputStream(srcFile);
     FileOutputStream out = new  FileOutputStream(destFile);
     int  length;
     while  ((length = in.read(temp)) != - 1 ) {
         out.write(temp, 0 , length);
     }
 
     in.close();
     out.close();
}

上面是一个典型的读写二进制文件的代码实现了. 不用我说, 大家肯定都知道, 上面的代码中不断中源文件中读取定长数据到 temp 数组中, 然后再将 temp 中的内容写入目的文件, 这样的拷贝操作对于小文件倒是没有太大的影响, 但是如果我们需要拷贝大文件时, 频繁的内存拷贝操作就消耗大量的系统资源了.
下面我们来看一下使用 Java NIO 的 FileChannel 是如何实现零拷贝的:

1
2
3
4
5
6
7
8
9
10
11
12
public  static  void  copyFileWithFileChannel(String srcFileName, String destFileName) throws  Exception {
     RandomAccessFile srcFile = new  RandomAccessFile(srcFileName, "r" );
     FileChannel srcFileChannel = srcFile.getChannel();
 
     RandomAccessFile destFile = new  RandomAccessFile(destFileName, "rw" );
     FileChannel destFileChannel = destFile.getChannel();
 
     long  position = 0 ;
     long  count = srcFileChannel.size();
 
     srcFileChannel.transferTo(position, count, destFileChannel);
}

可以看到, 使用了 FileChannel 后, 我们就可以直接将源文件的内容直接拷贝(transferTo) 到目的文件中, 而不需要额外借助一个临时 buffer, 避免了不必要的内存操作.

有了上面的一些理论知识, 我们来看一下在 Netty 中是怎么使用 FileRegion 来实现零拷贝传输一个文件的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Override
public  void  channelRead0(ChannelHandlerContext ctx, String msg) throws  Exception {
     RandomAccessFile raf = null ;
     long  length = - 1 ;
     try  {
         // 1. 通过 RandomAccessFile 打开一个文件.
         raf = new  RandomAccessFile(msg, "r" );
         length = raf.length();
     } catch  (Exception e) {
         ctx.writeAndFlush( "ERR: "  + e.getClass().getSimpleName() + ": "  + e.getMessage() + '\n' );
         return ;
     } finally  {
         if  (length < 0  && raf != null ) {
             raf.close();
         }
     }
 
     ctx.write( "OK: "  + raf.length() + '\n' );
     if  (ctx.pipeline().get(SslHandler. class ) == null ) {
         // SSL not enabled - can use zero-copy file transfer.
         // 2. 调用 raf.getChannel() 获取一个 FileChannel.
         // 3. 将 FileChannel 封装成一个 DefaultFileRegion
         ctx.write( new  DefaultFileRegion(raf.getChannel(), 0 , length));
     } else  {
         // SSL enabled - cannot use zero-copy file transfer.
         ctx.write( new  ChunkedFile(raf));
     }
     ctx.writeAndFlush( "\n" );
}

上面的代码是 Netty 的一个例子, 其源码在 netty/example/src/main/java/io/netty/example/file/FileServerHandler.java
可以看到, 第一步是通过 RandomAccessFile 打开一个文件, 然后 Netty 使用了 DefaultFileRegion 来封装一个 FileChannel 即:

1
new  DefaultFileRegion(raf.getChannel(), 0 , length)

当有了 FileRegion 后, 我们就可以直接通过它将文件的内容直接写入 Channel 中, 而不需要像传统的做法: 拷贝文件内容到临时 buffer, 然后再将 buffer 写入 Channel. 通过这样的零拷贝操作, 无疑对传输大文件很有帮助.


精彩问答

问:据我之前了解到,Java的NIO selector底层在Windows下的实现是起两个随机端口互联来监测连接或读写事件,在Linux上是利用管道实现的;我有遇到过这样的需求,需要占用很多个固定端口做服务端,如果在Windows下,利用NIO框架(Mina或Netty)就有可能会造成端口冲突,这种情况有什么好的解决方案吗?

你说的问题确实存在,Linux使用Pipe实现网络监听,Windows要启动端口。目前没有更好的办法,建议的方式是作为服务端的端口可以规划一个范围,然后根据节点和进程信息动态生成,如果发现端口冲突,可以在规划范围内基于算法重新生成一个新的端口。

问:请我,我现在将Spring与Netty做了整合,使用Spring的Service开启 Netty主线程,但是停止整个运行容器的时候,Netty的TCP Server端口不能释放?退出处理时,有什么好的办法释放Netty Server端口么?

实际上,由谁拉起Netty 主线程并不重要。我们需要做的就是当应用容器退出的时候(Spring Context销毁),在退出之前调用Netty 的优雅退出接口即可实现端口、NIO线程资源的释放。请参考这篇文章:http://www.infoq.com/cn/articles/netty-elegant-exit-mechanism-and-principles

问:适合用Netty写Web通信么?

Netty不是Web框架,无法解析JSP、HTML、JS等,但是它可以做Web 通信,例如可以使用Netty重写Tomcat的HTTP/HTTPS 通信协议栈。

问:能不能讲解一下Netty的串行无锁化设计,如何在串行和并行中达到最优?

为了尽可能提升性能,Netty采用了串行无锁化设计,在IO线程内部进行串行操作,避免多线程竞争导致的性能下降。表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。Netty的NioEventLoop读取到消息之后,直接调用ChannelPipeline的fireChannelRead(Object msg),只要用户不主动切换线程,一直会由NioEventLoop调用到用户的Handler,期间不进行线程切换,这种串行化处理方式避免了多线程操作导致的锁的竞争,从性能角度看是最优的。


微信公众号【黄小斜】大厂程序员,互联网行业新知,终身学习践行者。关注后回复「Java」、「Python」、「C++」、「大数据」、「机器学习」、「算法」、「AI」、「Android」、「前端」、「iOS」、「考研」、「BAT」、「校招」、「笔试」、「面试」、「面经」、「计算机基础」、「LeetCode」 等关键字可以获取对应的免费学习资料。 


                     wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

相关文章
|
13天前
|
编解码 分布式计算 网络协议
Netty高性能网络框架(一)
Netty高性能网络框架(一)
|
25天前
|
机器学习/深度学习 测试技术 数据处理
KAN专家混合模型在高性能时间序列预测中的应用:RMoK模型架构探析与Python代码实验
Kolmogorov-Arnold网络(KAN)作为一种多层感知器(MLP)的替代方案,为深度学习领域带来新可能。尽管初期测试显示KAN在时间序列预测中的表现不佳,近期提出的可逆KAN混合模型(RMoK)显著提升了其性能。RMoK结合了Wav-KAN、JacobiKAN和TaylorKAN等多种专家层,通过门控网络动态选择最适合的专家层,从而灵活应对各种时间序列模式。实验结果显示,RMoK在多个数据集上表现出色,尤其是在长期预测任务中。未来研究将进一步探索RMoK在不同领域的应用潜力及其与其他先进技术的结合。
66 4
|
2月前
|
开发者
Netty运行原理问题之Netty高性能实现的问题如何解决
Netty运行原理问题之Netty高性能实现的问题如何解决
|
2月前
|
前端开发 网络协议
Netty实战巅峰:从零构建高性能IM即时通讯系统,解锁并发通信新境界
【8月更文挑战第3天】Netty是一款高性能、异步事件驱动的网络框架,适用于开发高并发网络应用,如即时通讯(IM)系统。本文将指导你利用Netty从零构建高性能IM程序,介绍Netty基础及服务器/客户端设计。服务器端使用`ServerBootstrap`启动,客户端通过`Bootstrap`连接服务器。示例展示了简单的服务器启动过程。通过深入学习,可进一步实现用户认证等功能,打造出更完善的IM系统。
80 1
|
2月前
|
编解码 NoSQL Redis
(十一)Netty实战篇:基于Netty框架打造一款高性能的IM即时通讯程序
关于Netty网络框架的内容,前面已经讲了两个章节,但总归来说难以真正掌握,毕竟只是对其中一个个组件进行讲解,很难让诸位将其串起来形成一条线,所以本章中则会结合实战案例,对Netty进行更深层次的学习与掌握,实战案例也并不难,一个非常朴素的IM聊天程序。
|
2月前
|
消息中间件 缓存 Kafka
图解Kafka:架构设计、消息可靠、数据持久、高性能背后的底层原理
【8月更文挑战第15天】在构建高吞吐量和高可靠性的消息系统时,Apache Kafka 成为了众多开发者和企业的首选。其独特的架构设计、消息可靠传输机制、数据持久化策略以及高性能实现方式,使得 Kafka 能够在分布式系统中大放异彩。本文将通过图解的方式,深入解析 Kafka 的这些核心特性,帮助读者更好地理解和应用这一强大的消息中间件。
100 0
|
3月前
|
前端开发 Java 数据处理
使用Netty构建高性能的网络应用
使用Netty构建高性能的网络应用
|
3月前
|
负载均衡 安全 Cloud Native
云上负载均衡:构建高可用、高性能的网络应用架构
与云原生技术深度融合:随着云原生技术的普及和发展未来的云上负载均衡将更加紧密地与云原生技术相结合。例如与Kubernetes等容器编排平台集成实现自动化的服务发现和路由管理;与Serverless架构结合提供无缝的流量接入和请求处理能力。 安全性能提升:面对日益严峻的网络安全威胁云上负载均衡将更加注重安全性能的提升。通过引入加密传输、访问控制、DDoS防护等安全措施确保网络流量的安全性和隐私性;同时还将建立完善的安全监控和应急响应机制以应对各种安全事件和突发事件。 支持多协议和多场景:未来的云上负载均衡将支持更多种类的网络协议和应用场景以满足不同用户和业务的需求。例如支持HTTP/2、
155 0
|
3月前
|
消息中间件 Java API
解析Java微服务架构:从零构建高性能系统
解析Java微服务架构:从零构建高性能系统
|
3月前
|
前端开发 Java 数据处理
使用Netty构建高性能的网络应用
使用Netty构建高性能的网络应用
下一篇
无影云桌面