三、Netty代码相关:
(四) EventLoop与EventLoopGroup:
Netty的Nio线程是NioEventLoop。
1. Reactor线程模型:
Reactor模型的三个角色:
Reactor:把IO事件分配给对应的Handler处理,功能像是调度器。
Acceptor【饿渴赛破特儿】:处理客户端连接事件。
Handler:处理具体的任务。
Reactor模型的运行机制:(四个步骤)
连接注册:Channel建立后,注册到Reactor线程的Selector中。
事件轮询:轮询Selector选择器中已注册的所有Channel的IO事件。
事件分发:将准备就绪的 IO 事件分配相应的处理线程。
任务处理:Reactor线程还负责任务队列中的非 IO 任务,每个Worker线程从各自维护的任务队列中,取出任务异步执行。
Reactor单线程模型:
Reactor单线程模型:是指所有的IO操作都在同一个NIO线程上面完成。
NIO线程的职责如下(4个):
作为NIO的服务端,接受客户端的TCP链接;
作为NIO的客户端,向服务端发起TCP链接;
读取通信对端的请求或应答消息;
向通信对端发送消息请求或应答消息。
由于Reactor模式使用的是异步非阻塞IO,所有的IO操作不会导致阻塞,理论上一个线程可以独立处理所有IO相关的操作。
小容量应用场景可以使用单线程模型,但是高负载、大并发应用场景不合适。主要原因如下:
一个NIO线程同时处理上千链路,性能无法支持。
NIO线程负载过重之后,处理速度变慢,会导致大量客户端链接超时。
可靠性问题,得不到保障。
Reactor多线程模型:
它与Reactor单线程模型的最大区别:有一组NIO线程来处理IO操作,即Reactor线程池。
主要特点:
有专门用于监听服务端,接受客户端TCP链接请求的NIO线程(Acceptor线程)。
有专门负责网络IO操作的线程池(Reactor Pool):读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,油这些NIO线程负责消息的取读、解码、编码和发送。
一个NIO线程可以同时处理多条链路,但是一个链路只对应一个NIO线程,防止发生并发操作问题。
绝大多数场景,Reactor多线程模型可以满足性能需求。但是在个别特殊场景中,一个NIO线程负责监听和处理所有的客户端链接(Acceptor线程)可能存在性能问题,即单独一个Acceptor线程可能存在性能不足。
主从Reactor多线程模型:
为了解决Reactor多线程模型中,一个Acceptor性能不足,而诞生的模型。
特点:
服务端用于接受客户端连接的是一个独立的NIO线程池。
Acceptor接收到客户端TCP连接请求,并处理完成后(可能包含接入认证等),将新创建的SocketChannel注册到IO线程池( Reactor Pool)的某个IO线程上,由它负责SocketChannel的读写和编解码工作。
Acceptor线程池(NIO线程池)仅仅用于客户端的登录、握手和安全认证,一旦链路建立成功,就将链路注册到后端subReactor线程池的IO线程上,由IO线程负责后续的IO操作。
主从Reactor多线程模型,可以解决一个服务端监听线程(Acceptor线程)性能不足的问题。因此,在Netty的官方Demo中,推荐使用该线程模型。
2. Netty的线程模型:
Netty可以同时支持单线程、多线程和主从多线程模型。用户可以通过启动参数进行配置。
Netty服务端启动时,创建了两个NioEventLoopGroup,它实际是两个独立的线程池。一个用于接收客户端的TCP连接,另一个用于处理IO相关的读写操作,或者执行系统Task、定时任务Task等。
用于接收客户端请求线程池的职责如下(Acceptor线程池):
接收客户端TCP连接,初始化Channel参数。
将链路状态变更事件,通知给ChannelPipeline。
用于处理IO操作线程池的职责如下(Reactor线程池):
异步读取通信对端的数据报,发送读事件到ChannelPipeline。
异步发送消息到通信对端,调用ChannelPipeline的消息发送接口。
执行系统调用Task。
执行定时任务Task,例如,链路空闲状态监测定时任务。
3. EventLoop原理:
概念:
EventLoop不是Netty独有的概念,它是一种事件等待和处理的程序模型,可以解决多线程资源消耗高的问题。例如Node.js也采用了EventLoop的运行机制。
在Netty中,EventLoop是Reactor线程模型的事件处理引擎,每个EventLoop线程都维护一个Selector选择器和任务队列taskQueue。它主要负责处理 I/O 事件、普通任务和定时任务。
NioEventLoop无锁串行化的设计不仅使系统吞吐量达到最大化,而且降低了用户开发业务逻辑的难度,不需要花太多精力关心线程安全问题。
EventLoop通用的运行模式:
每当事件发生时,应用程序都会将产生的事件,放入事件队列当中,然后EventLoop会轮询,从队列中取出事件执行,或者将事件分发给相应的事件监听者执行。事件执行的方式通常分为立即执行、延后执行、定期执行几种。
NioEventLoop循环处理的流程:
NioEventLoop每次循环的处理流程都包含:事件轮询Select、事件处理ProcessSelectedKeys、任务处理RunAllTasks这个几个步骤,是典型的Reactor线程模型的运行机制。而且,Netty提供了一个参数 ioRatio,可以调整 IO 事件处理和任务处理的时间比例。
下面我们将着重从事件处理和任务处理两个核心部分出发,详细介绍Netty EventLoop 的实现原理。
事件处理机制 -> 采用无锁串行化的设计思路:
Netty的整体架构图如下:
BossEventLoopGroup和WorkerEventLoopGroup包含一个或者多个NioEventLoop。
BossEventLoopGroup负责监听客户端的Accept事件,当事件触发时,将事件注册至WorkerEventLoopGroup中的一个NioEventLoop上。
每新建一个Channel, 只选择一个NioEventLoop与其绑定。所以说Channel生命周期的所有事件处理都是线程独立的,不同的NioEventLoop线程之间不会发生任何交集。
NioEventLoop完成数据读取后,会调用绑定的ChannelPipeline进行事件传播,ChannelPipeline也是线程安全的,数据会被传递到ChannelPipeline的第一个ChannelHandler中。数据处理完成后,将加工完成的数据再传递给下一个ChannelHandler,整个过程是串行化执行,不会发生线程上下文切换的问题。
任务处理机制:
NioEventLoop不仅负责处理IO事件,还负责执行任务队列中的任务。任务队列遵循FIFO规则,可以保证任务执行的公平性。
NioEventLoop处理的任务类型(三类):
普通任务:通过NioEventLoop.execute()向任务队列taskQueue中添加任务。
例如,Netty在写数据时,会封装WriteAndFlushTask提交给taskQueue。
taskQueue的实现类是"多生产者单消费者"队列MpscChunkedArrayQueue,在多线程并发添加任务时,可以保证线程安全。
定时任务:通过调用 NioEventLoop.schedule() 向定时任务队列 scheduledTaskQueue 添加一个定时任务,用于周期性执行该任务。
例如,心跳消息发送等。
定时任务队列scheduledTaskQueue采用优先队列PriorityQueue实现。
尾部队列:tailTasks相比于普通任务队列优先级较低,在每次执行完taskQueue 中任务后,会去获取尾部队列中任务执行。
尾部任务并不常用,主要用于做一些收尾工作,例如统计事件循环的执行时间、监控信息上报等。
任务处理在runAllTasks(long timeoutNanos)中完成,具体实现步骤如下(6步):
1. fetchFromScheduledTaskQueue():将定时任务从scheduledTaskQueue中取出,聚合放入普通任务队列taskQueue中,只有定时任务的截止时间小于当前时间才可以被合并。
2. 从普通任务队列taskQueue中取出任务。
3. 计算任务执行的最大超时时间。
4. safeExecute():安全执行任务,实际直接调用的 Runnable 的 run() 方法。
5. 每执行 64 个任务进行超时时间的检查,如果执行时间大于最大超时时间,则立即停止执行任务,避免影响下一轮的 I/O 事件的处理。
6. 最后获取尾部队列中的任务执行。
4. NioEventLoop继承关系类图:
5. EventLoop最佳实践:
网络连接建立过程中三次握手、安全认证的过程会消耗不少时间。这里建议采用Boss和Worker两个EventLoopGroup,有助于分担Reactor线程的压力。
由于Reactor线程模式适合处理耗时短的任务场景,对于耗时较长的ChannelHandler可以考虑维护一个业务线程池,将编解码后的数据封装成Task进行异步处理,避免 ChannelHandler阻塞而造成EventLoop不可用。或者将数据放入内存队列中解耦,业务线程异步处理队列中的数据(Hippo就是这么做的)。
如果业务逻辑执行时间较短,建议直接在ChannelHandler中执行。例如编解码操作,这样可以避免过度设计而造成架构的复杂性。
不宜设计过多的ChannelHandler。对于系统性能和可维护性都会存在问题,在设计业务架构的时候,需要明确业务分层和Netty分层之间的界限。不要一味地将业务逻辑都添加到ChannelHandler中。
(五) ChannelFuture与Promise:
1. ChannelFuture:
ChannelFuture的由来:
由于Netty的Future都是与异步IO操作相关的,因此,命名为ChannelFuture,代表它与Channel操作相关。
ChannelFuture的两种状态:
uncompleted:开始一个IO操作时,ChannelFuture被创建,此时它处于uncompleted状态,表示非失败、非成功、非取消。
completed:IO操作完成后,ChannelFuture将会被设置成completed,它的结果有如下三种可能:操作成功、操作失败、操作被取消。
ChannelFuture的API:
它可以用于获取操作结果、添加事件监听器、取消IO操作、同步等待等。
Netty强烈建议直接通过添加监听器的方式获取IO操作结果,或者进行后续的相关操作。
原因:当我们进行异步IO操作是,完成的时间是无法预测的,如果不设置超时时间,会导致调用线程长时间被阻塞,甚至挂死。
不要再ChannelHandler中调用ChannelFuture的await(),这会导致死锁。
原因:发起IO操作之后,IO线程负责通知发起IO操作的用户线程,如果IO线程和用户线程是同一个线程,就会导致IO线程等待自己通知操作完成,这就导致了死锁。
异步IO操作有两类超时:
TCP层面的IO超时。
业务逻辑层面的操作超时,但是通常情况下业务逻辑超时时间应大于IO超时时间,它们两者是包含的关系。
ChannelFuture超时并不代表IO超时,ChannelFuture超时后,需要考虑是设置IO超时还是ChannelFuture超时。
2. ChannelFuture继承关系图:
3. Promise:
Promise由来:
Promise是可写的Future,Future自身并没有写操作相关接口,Netty通过Promise对Future进行扩展,用于设置IO操作的结果。
Promise的举例使用:
Netty发起IO操作时,会创建一个新的Promise对象,例如,调用ChannelHandlerContext的write(Object object)时,会创建一个新的ChannelPromise。当IO操作发送异常或者完成时,设置Promise的结果。
4. Promise的继承关系图:
由于IO操作种类非常多,因此对Promise子类也非常繁多。
四、Netty高级特性:
(一) Netty的架构:
1. Netty逻辑架构:
Netty采用三层网络架构进行设计和开发。
Reactor通信调度层:
Reactor通信调度层主要包括:
Reactor线程NioEventLoop及其父类、NioSocketChannel/NioServerSocketChannel及其父类、ByteBuf及其衍生出来的各种Buffer、Unsafe以及衍生出的各种内部类。
Reactor通信调度层主要职责:监听网络的读写和连接操作:
负责将网络层的数据读取到内存缓冲区中,然后触发各种网络事件,例如连接创建、连接激活、读事件、写事件等,将这些事件触发到Pipeline中,由Pipeline管理的职责链来进行后续的处理。
职责链ChannelPipeline:
ChannelPipeline负责事件在职责链中的有序传播,同时负责动态地编排职责链。
职责链可以选择监听和处理自己关心的事件,它可以拦截处理和向后/向前传播事件。
业务逻辑编排层:
业务逻辑编排层的分类(通常有两类):
纯粹的业务逻辑编排。
其他的应用层协议插件,用于特定协议相关的会话和链路管理。例如,CMPP协议,用于管理和中国移动短信系统的对接。
(二) Netty的高性能:
0. 高性能的体现(汇总):
采用异步非阻塞的IO类库,基于Reactor模式实现,解决了传统同步阻塞IO模式下,一个服务端无法平滑地处理线性增长的客户端的问题。
TCP接收和发送的缓冲区使用直接内存代替堆内存,避免了内存复制,提升了IO读取和写入的性能。
支持内存池的方式循环利用ByteBuf,避免了频繁创建和销毁ByteBuf带来的性能损耗。
可配置的IO线程数、TCP参数等,为不同的用户场景提供定制化的调优参数,满足不同的性能场景。
零拷贝。
无锁化设计:
关键资源的处理使用单线程串行化的方式,避免多线程并发访问带来的锁竞争和额外的CPU资源消耗问题。
采用环形数组缓冲区实现无锁化并发编程,代替传统的线程安全容器或者锁。(环形数组缓冲区为什么能无锁化并发编程??20221121)
高性能的序列化框架。
高效的并发编程。
通过引用计数器及时地申请释放不再被引用的对象,细粒度的内存管理降低了GC的频率,减少了频繁GC带来的时延增大和CPU损耗。
1. 异步非阻塞通信:
Netty的IO线程(NioEventLoop)聚合了多路复用器(Selector),可以同时并发处理成百上千的客户端SocketChannel。
Netty采用异步通信模式,一个IO线程可以并发处理N个客户端链接和读写操作,这个从根本上解决了传统同步阻塞IO“一链接一线程”模型架构的性能、弹性伸缩能力和可靠性的问题。
2. 高效的Reactor线程模型:
《见 (四) EventLoop与EventLoopGroup》
3. 直接内存与内存池:
《见 (一) ByteBuf和相关辅助类》
4. 灵活的TCP参数配置能力:
对性能影响比较大的TCP配置项:
SO_RCVBUF和SO_SNDBUF:通常建议值为128KB或者256KB。
SO_TCPNODELAY:NAGLE算法,通过将缓冲区内的小封包,自动相连组成较大的封包,阻止大量小封包的发送阻塞网络,从而提高网络应用效率。但是,对于时延敏感的应用场景需要关闭该优化算法。
软中断:如果Linux内核版本支持RPS(2.6.35以上版本),开启RPS后可以实现软中断,提升网络吞吐量。RPS根据数据包的源地址,目的地址以及目的和源端口,计算出一个hash值,然后根据这个hash值来选择软中断运行的CPU。从上层来看,也就是说将每个连接和CPU绑定,并通过这个hash值,来均衡软中断在多个CPU上,提升网络并行处理性能。
5. 零拷贝(3种):
Netty的零拷贝主要体现:
读写socket的零拷贝:ByteBuf使用堆外直接内存进行socket读写,不需要进行字节缓冲区的二次拷贝。Netty默认使用Direct Buffer。
文件传输的零拷贝:直接把文件缓冲区的内容,发送到目标的Channel中,不需要通过循环拷贝的方式。Netty文件传输类 DefaultFileRegion.transferTo() 将文件发送到目标Channel中。
使用CompositeByteBuf的零拷贝:它对外,将多个ByteBuf封装成一个ByteBuf,对外提供统一封装后的ByteBuf接口。在添加ByteBuf时,不需要内存拷贝,即零拷贝。
6. 无锁化的串行设计:
在IO线程内部进行串行操作,避免多线程竞争导致性能下降。通过调整NIO线程池的线程参数,同时启动多个串行化的线程并行运行,提高性能。
Netty的NioEventLoop读取到消息之后,直接调用 ChannelPipeline.fireChannelRead(Object msg),只要用户不主动切换线程,一直会由 NioEventLoop 调用到用户的Handler,期间不进行线程切换。
7. 高性能的序列化框架:
Netty 默认提供了对Google ProtoBuf的支持。
8. 高效的并发编程:
volatile的大量、正确使用;CAS和原子类的广泛使用;线程安全容器的使用;通过读写锁提升并发性能。
(三) Netty的可靠性:
0. 可靠性的体现(汇总):
网络通信类故障的解决:
Netty支持配置客户端超时的时间,NIO类库没有提供现成的链路超时接口,Netty自己封装的。
通信对端强制关闭连接(Netty在IO异常之后,会调用关闭连接),TCP是全双工,通信双方都需要关闭和释放socket句柄,才不会发生句柄的泄漏。
链路关闭:Netty在read操作返回-1后,调用方法关闭句柄,释放资源。
定制IO故障处理:Netty支持用户自定义处理IO异常,Netty具体实现的接口:ChannelHandlerAdapter.exceptionCaught()。
链路有效性检测:Netty采用链路空闲时心跳检测机制,保证长链接的链路有效性。
Reactor线程的保护:
例如,Netty策略规避了epoll bug
内存保护机制:
Netty提供了内存池和对象池,提升内存的利用率。
可设置的内存容量上限,包括:ByteBuf、线程池线程数等。
流量整形:
全局流量整形、链路级流量整形。
优雅停机:
Netty5.0版本开始优雅退出功能,做得更加完善。
1. 网络通信类故障:
1.1 客户端超时:
NIO在非阻塞模式下,会直接返回连接结果。如果没有连接成功,也没有发生IO异常,则需要将SocketChannel注册到Selector上监听连接结果。所以,异步连接超时,无法在API层面直接设置,而是需要通过定时器来主动监测。并且NIO类库并没提供现成的链接超时接口,需要NIO框架或者用户自己封装实现。
Netty支持配置连接超时时间,Netty在发起链接时,会根据超时时间创建ScheduledFuture,将它挂载到Reactor线程上,用于定时监测是否发生链接超时。创建完链接超时的定时任务后,会有NioEventLoop负责执行。如果已经连接超时,但是服务端仍然没有返回TCP握手应答,则关闭链接。
1.2 通信对端强制关闭连接:
客户端与服务端正常通信过程中,如果发生网络闪断、对方进程突然宕机或者其他非正常关闭链路事件时,TCP链路就会发生异常。由于TCP是全双工的,通信双方都需要关闭和释放Socket句柄才不会发生句柄的泄漏。
Netty在IO异常之后,会调用关闭连接。
1.3 链路关闭:
NIO编程的一种误区:认为只要是对方关闭连接,就会发生IO异常,捕获IO异常之后再关闭连接即可。实际上,连接的合法关闭不会发生IO异常,它是一种正常场景,如果遗漏了该场景的判断和处理就会导致连接句柄泄漏。
如果SocketChannel被设置为非阻塞,则它的read操作可能返回三个值:
大于0:表示读取到了字节数。
等于0:没有读取到消息,可能TCP处于Keep-Alive状态,接收到的是TCP握手消息。
-1:连接已经被对方关闭。
Netty通过判断Channel read操作的返回值进行不同的逻辑处理,如果返回-1,说明链路已经关闭,会调用closeOnRead() 关闭句柄,释放资源。
1.4 定制IO故障:
在大多数场景下,当底层网络发生故障时,应该由底层的NIO框架负责释放资源,处理异常等。上层的业务应用不需要关心底层的处理细节。但是,在一些特殊的场景下,用户可能需要感知这些异常,并针对这些异常进行定制处理,例如:
客户端的断连重连机制。
消息的缓存重发。
接口日志中详细记录故障细节。
运维相关功能,例如告警、触发邮件/短信等
Netty的I/O异常处理策略:在发生IO异常时,底层的资源由它负责释放,同时将异常堆栈信息以事件的形式通知给上层用户,用户对IO异常进行定制。这种处理机制既保证了异常处理的安全性,也向上层提供了灵活的定制能力。
Netty具体实现的接口:ChannelHandlerAdapter.exceptionCaught()。
2. 链路的有效性检测:
2.1 为什么要周期性检测链路?
当网络发生单通、连接被防火墙Hang住、长时间GC或者通信线程发生非预期异常时,会导致链路不可用,且不易被及时发现。特别是异常发生在凌晨业务低谷期间,当早晨业务高峰期到来时,由于链路不可用,导致瞬间的大批量业务失败或者超时,这将对系统的可靠性产生重大的威胁。
从技术层面看,要解决链路的可靠性问题,必须周期性的对链路进行有效性检测。目前最流行和通用的做法就是心跳检测。
2.2 心跳检测机制:
心跳检测目的:
确认当前链路可用、对方存活、并且能够正常接收和发送消息。
心跳检测机制的三个层面:
TCP层面的心跳检测:即TCP的Keep-Alive机制,它的作用域是整个TCP协议栈。
协议层的心跳检测:主要存在于长连接协议中。例如SMPP协议。
应用层的心跳检测:它主要由各业务产品通过约定方式定时给对方发送心跳消息实现。
心跳检测机制的分类:
Ping-Pong型心跳:由通信一方定时发送Ping消息,对方接收到Ping消息之后,立即返回Pong应答消息给对方,属于“请求-响应型”心跳。
Ping-Ping型心跳:不区分心跳请求和应答,由通信双方按照约定,定时向对方发送心跳Ping消息,它属于“双向”心跳。
心跳检测的策略:
心跳超时:连续N次心跳检测都没有收到对方的Pong应答消息或者Ping请求消息,则认为链路已经发生逻辑失效,这被称作心跳超时。
心跳失败:读取和发送心跳消息时,如果直接发生了IO异常,说明链路己经失效,这被称为心跳失败。
Tips:无论发生心跳超时还是心跳失败,都需要关闭链路,由客户端发起重连操作,保证链路能够恢复正常。
心跳检测原理示意图:
2.3 Netty的心跳检测机制:
Netty的心跳检测的实现:
利用链路空闲检测机制。
Netty空闲检测机制的分类:
读空闲:链路持续时间T,没有读取到任何消息,触发超时Handler,进行链路检测。
写空闲:链路持续时间T,没有发送任何消息,~~。
读写空闲:链路持续时间T,没有接收或者发送任何消息,~~。Netty的默认读写空闲机制是发生超时异常时,关闭连接。但是,我们自定义超时逻辑,支持不同的用户场景。
Netty框架,自己实现的Handler,用来实现心跳检测:
IdleStateHandler:可以处理读、写、读写空闲事件的超时,在超时之后,会触发用户 handler 的 userEventTriggered()。
ReadTimeoutHandler:在指定的时间内没有数据被读取,则抛出一个异常,并且断开通道的链接。
WriteTimeoutHandler:在指定时间内写操作没有完成,则直接抛出一个异常,并且断开通道的链接。Tips:IdleStateHandler检测的是指定事件没有发生写操作。
链路空闲时,并没有关闭链路,而是触发IdleStateEvent事件。用户可以订阅IdleStateEvent事件,自定义逻辑处理。例如关闭链路、客户端发起重新连接、告警和打印日志等。
3. Reactor线程的保护:
Reactor线程是IO操作的核心,NIO框架的发动机,一旦出现故障,将会导致挂载在其上面的多路用复用器和多个链路无法正常工作。因此它的可靠性要求非常高。
3.1 规避NIO BUG(epoll bug):
通常情况下,死循环是可检测、可预防,但是无法完全避免的。Reactor线程通常处理的都是IO相关的操作,因此我们重点关注IO层面的死循环。
JDK NIO类库最著名的就是epoll bug,它会导致Selector空轮询,IO线程CPU100%,严重影响系统的安全性和可靠性。
Netty提供了一种检测机制判断线程是否可能陷入空轮询,具体的实现方式如下:
每次执行Select操作之前记录当前时间currentTimeNanos。
time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos,如果事件轮询的持续时间大于等于 timeoutMillis,那么说明是正常的,否则表明阻塞时间并未达到预期,可能触发了空轮询的Bug。
Netty引入了计数变量 selectCnt。在正常情况下,selectCnt 会重置,否则会对selectCnt自增计数。当 selectCnt达到SELECTOR_AUTO_REBUILD_THRESHOLD(默认512) 阈值时,会触发重建 Selector 对象。
Netty 采用这种方法巧妙地规避了JDK Bug。异常的Selector中所有的SelectionKey会重新注册到新建的Selector 上,重建完成之后异常的Selector就可以废弃了。
相关代码解释:https://blog.csdn.net/hxyascx/article/details/114284075
4. 内存保护:
NIO通信的内存保护主要集中在如下几点:
链路总数的控制:每条链路都包含接收和发送缓冲区,链路个数太多容易导致内存溢出。
单个缓冲区的上限控制:防止非法长度或者消息过大导致内存溢出。
缓冲区内存释放:防止因为缓冲区使用不当导致的内存泄露。
NIO消息发送队列的长度上限控制。
4.1 缓冲区的内存泄漏保护:
为了提升内存的利用率,Netty提供了内存池和对象池。但是,基于缓存池实现以后需要对内存的申请和释放进行严格的管理,否则很容易导致内存泄漏。引入内存池机制,对象的生命周期,将由内存池负责管理。
对于Netty的用户而言,使用者的技术水平差异很大,一些对JVM内存模型和内存泄漏机制不了解的用户,可能只记得中请内存,忘记主动释放内存,特别是JAVA程序员。为了防止因为用户遗漏导致内存泄漏,Netty在Pipeline的尾Handler中自动对内存进行释放,TailHandler的内存回收代码如下:
4.2 缓冲区溢出保护:
对消息进行解码时,需要创建缓冲区。缓冲区的创建方式通常有两种:
容量预分配:在实际读写过程中如果不够再扩展。
根据协议消息长度创建缓冲区。
在实际的商用环境中,如果遇到畸形码流攻击、协议消息编码异常、消息丢包等问题时,可能会解析到一个超长的长度字段。Netty提供了编解码框架,因此对于解码缓冲区的上限保护就显得非常重要。
5. 流量整形:
大多数的商用系统都有多个网元或者部件组成,例如参与短信互动,会沙及手机、基站、短信中心、短信网关、SPCP等网元,不同网元或者部件的处理性能不同。为了防止因为浪涌业务或者下谢网元性能低,导致下游网元被压垮,有时候需要系统提供流量整形功能。
流量整形(traffic shaping)的定义:它是一种主动调整流量输出速率的措施。一个典型应用是基于下游网络结点的TP指标来控制本地流量的输出。
流量整形与流量监管的主要区别:
流量整形对流量监管中需要丢弃的报文进行缓存,通常是将它们放入缓冲区或队列内,也称流量整形(Traffic Shaping,简称TS)。当令牌桶有足够的令牌时,再均匀的向外发送这些被缓存的报文。
整形可能会增加延迟,而监管几乎不引入额外的延迟。
作为高性能的NIO框架,Netty的流量整形有两个作用:
防止由于上下游网元性能不均衡导致下游网元被压垮,业务流程中断。
防止由于通信模块接收消息过快,后端业务线程处理不及时导致的“撑死”问题。
5.1 全局流量整形:
全局流量整形的作用范围是进程级的,无论你创建了多少个Channel,它的作用域针对所有的Channel。
用户可以通过参数设置:报文的接收速率、报文的发送速率、整形周期。
Netty流量整形的原理:对每次读取到ByteBuf的可写字节数进行计算,获取当前的服文流量,然后与流量整形调值对比。如果已经达到或者超过了倒值。则计算等待时间delay,将当前的ByteBuf放到定时任务Task中缓存,由定时任务线程池在廷迟delay之后继续处理该ByteBuf。
流量整形的阈值limit越大。流量整形的精度越高,流量整形功能是可靠性的一种保障,它无法做到100%的精确。这个跟后端的编解码以及缓神区的处理策略相关,此处不再赞述。
流量整形与流控的最大区别:流控会拒绝消息,流量整形不拒绝和丢弃消息,无论接收量多大,它总能以近似恒定的速度下发消息,跟变压器的原理和功能类似。
5.2 链路级流量整形:
除了全局流量整形,Netty也支持链路级的流量整形,ChannelTrafficShapingHandler接口。
单链路流量整形与全局流量整形的最大区别:它以单个链路为作用城,可以对不同的链路设置不同的整形策略,它的实现原理与全局流量整形类似。
Netty支持用户自定义流量整形策略,通过继承 AbstractTrafficShapingHandler.doAccounting() 可以定制整形策略。
6. 优雅停机接口:
Java的优推停机通常通过注册JDK的ShutdownHook来实现,当系统接收到是出指令后,首先标记系统处于理出状态,不再接收新的消息。然后将积压的消息处理完,最后调用资源回收接口将资源销毁,最后各线程退出执行。
通常优雅退出有个时问限制,例如30S,如果到达执行时间仍然没有完成退出前的操作,则由监控脚本直接kil -9 pid,强制退出。
(四) Netty的可定制性与可拓展性:
1. Netty可定制性的提现:
责任链模式:ChannelPipeline基于责任链模式开发,便于业务逻辑的拦截、定制和扩展。
基于接口的开发:关键的类库都提供了接口或者抽象类,如果Netty自身的实现无法满足用户的需求,可以由用户自定义实现相关接口。
提供了大量工厂类,通过重载这些工厂类可以按需创建出用户实现的对象。
提供了大量的系统参数供用户按需设置,增强系统的场景定制性。
2. Netty可扩展性的提现:
基于Netty框架,可以方便地进行应用层协议定制,不需要修改Netty的源码,直接基于Netty的二进制类库即可,实现协议的扩展和定制。例如:HTTP协议栈、Thrift协议栈、FTP协议栈等。
例如基于Netty的HTTP协议、Dubbo协议、RocketMQ内部私有协议等。