Netty框架的 主要线程是IO线程,线程模型的好坏直接决定了系统的吞吐量、并发性和安全性。
Netty的线程模型遵循了Reactor的基础线程模型。下面我们先一起看下该模型
Reactor线程模型
Reactor 单线程模型
单线程模型中所有的IO操作都在一个NIO线程上操作:
包含接受客户端的请求,读取客户端的消息和应答。由于使用的是异步非阻塞的IO,所有的IO操作不会阻塞,理论上一个线程就可以处理所有的IO操作。
单线程模型适用小容量的应用。因为在高并发应用 可导致以下问题
- 一个线程同时处理成百上千的链路,性能上无法支撑。即使IO线程cpu 100%也无法满足要求。
- 当NIO线层负载过重,处理速度将变慢,会导致大量的客户端超时,重发,会更加重NIO的负载,最终导致系统大量超时
- 一旦IO线程跑飞,会导致整个系统通讯模块不可用,造成节点故障
Reactor多线程模型
该模型组织了 一组线程进行IO的操作
特点:
1. 有专门的NIO线程---acceptor线程用于监听服务器,接受客户端的TCP请求
2. 网络操作的读写 由一个IO线程池负责 负责消息的读取 接收 编码和发送
3. 一个IO线程可以同时处理N条链路,但是一条链路 只对应一个Io线程。防止并发的操作问题
适合绝大多数场景,但是对于并发百万或者服务器需要对客户端握手进行安全认证,认证非常耗性能的情况,会导致性能瓶颈!
主次Reactor多线程模型
接受客户端的连接 不在是一个单独的IO线程,而是一个Nio线程池:
Acceptor接受客户端的请求并处理完成后,将新建的socketChannel注册到IO线程池的某个线程上,由
他负责IO的读写 接编码工作。Acceptor线程池仅仅负责客户端的登录 握手 和 安全认证,一旦链路成
功,将链路注册到后端的线程池的线程上,有他进行后续的Io操作。
Netty线程模型
public void bind(int port) throws Exception {
// 配置服务端的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, 1024)
.childHandler(new ChildChannelHandler());
// 绑定端口,同步等待成功
ChannelFuture f = b.bind(port).sync();
// 等待服务端监听端口关闭
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放线程池资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel arg0) throws Exception {
arg0.pipeline().addLast(new TimeServerHandler());
}
}
netty服务器在启动的时候,创建了两个NIOEventLoopGroup 独立的Reator线程池,一个用于接收客户端的TCP连接,一个用于处理IO的相关的读写操作。
Netty线程模型就是在reactor模型的基础上建立的,线程模型并不是一成不变的,通过启动参数的配置,可以在三种中切换。
启动过程,bossGroup 会选择一个EventLoop 需要绑定serverSocketChannel 进行接收客户端连接;处理后,将准备好的socketchnanell顺利注册到workGroup下。
netty服务端的创建过程
时序图:
Netty 屏蔽NIO通信的底层细节:
- 首先创建ServerBootstrap,他是Netty服务端的启动辅助类
- 设置并绑定Reactor线程池。Netty的Reactor线程池是EventLoopGroup,它实际就是EventLoop线 程的数组。EventLoop的职责是处理所有注册到本线程多路复用器Selector上的Channel
- 设置NIOserverSocketChannel. Netty通过工厂类,利用反射创建NioServerSocketChannel对象
- 设置TCP参数
- 链路建立的时候创建并初始化ChannelPipeline.它本质就是一个负责处理网络事件的职责链,负责管理和执行ChannelHandler。网络事件以事件流的形式在ChannelPipeline中流转,由ChannelPipeline根据ChannelHandler的执行策略调度ChannelHandler的执行
- 绑定并启动监听端口
- 绑定端口,并启动。将会启动NioEventLoop负责调度和执行Selector轮询操作,选择准备就绪的Channel集合。当轮询到准备就绪的Channel之后,就由Reactor线程NioEventLoop执行ChannelPipeline的相应方法,最终调度并执行ChannelHandler。
NioEventLoop IO线程浅析
做为Netty的Reactor线程,因为要处理网络IO读写,所以聚合一个多路复用器对象,它通过open获取一个多路复用器。他的操作主要是在run方法的for循环中执行的。
- 做为bossGroup的线程 他需要绑定NioServerSocketChannel 来监听客户端的connet请求,并处理连接和校验。
- 作为workGroup线层组的线程,需要将连接就绪的SocketChannel绑定到线程中,所以一个客户端连接至对应一个线程,一个线程可以绑定多个客户端连接。
从调度层面看,也不存在在EventLoop线程中 再启动其它类型的线程用于异步执行其它的任务,这样就避免了多线程并发操作和锁竞争,提升了I/O线程的处理和调度性能。
NioEventLoop线程保护
IO操作是线程是的核心,一旦出现故障,导致其上面的多路复用器和多个链路无法正常工作,因此他需要特别的保护。他在以下两个方面做了保护处理:
- 谨慎处理异常
异常可能导致线程跑飞,会导致线程下的所有链路不可用,这时采try{}catch(Throwable) 捕获异常,防止跑飞。出现异常后,可以恢复执行。netty的原则是 某个消息的异常不会导致整个链路的不可用,某个链路的不可用,不能导致其他链路的不可用。
- 规避NIO BUG
Selector.select 没有任务执行时,可能触发JDK的epoll BUG。这就是著名的JDK epoll BUG,JDK1.7早期版本 号称解决了,但是据网上反馈,还有此BUG。服务器直接表现为 IO线程的 CPU很高,可能达到100%,可能会导致节点故障!!!
为什么会发生epoll Bug
Netty的修复策略为:
- 对Selector的select的操作周期进行统计
- 对每完成一次空的select操作进行一次计数
- 在某周期内(如100ms)连续N此空轮询, 说明触发了epoll死循环BUG
- 检测到死循环后,重建selector的方式让系统恢复正常
netty采用此策略,完美避免了此BUG的发生。
参考资料:netty权威指南2