Netty原理架构解析
转载自:http://www.sohu.com/a/272879207_463994
本文转载关于Netty的原理架构解析,方便之后巩固复习
Netty是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。JDK原生也有一套网络应用程序API,NIO,但是存在一些问题使得用起来不是很方便,主要如下:
- NIO的类库和API繁杂,使用麻烦。使用时需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等
- 需要具备其他的额外技能做铺垫。例如熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序
- 可靠性能力补齐,开发工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等。NIO编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大
- JDK NIO的Bug。例如臭名昭著的Epoll Bug,它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK 1.6版本的update 18修复了该问题,但是直到JDK1.7版本该问题仍旧存在,只不过该Bug发生概率降低了一些而已,它并没有被根本解决
Netty对JDK自带的NIO的API进行封装,解决上述问题,主要特点有:
- 设计优雅,适用于各种传输类型的统一API阻塞和非阻塞Socket;基于灵活且可扩展的事件模型,可以清晰地分析关注点;高度可定制的线程模型-单线程,一个或多个线程池;真正的无连接数据报套接字支持
- 使用方便,详细记录的Javadoc,用户指南和示例;没有其他依赖项,JDK5(Netty3.x)或6 (Netty4.x) 就足够了
- 高性能,吞吐量更高,延迟更低;减少资源消耗;最小化不必要的内存复制
- 安全,完整的SSL/TLS和StartTLS支持
- 社区活跃,不断更新,社区活跃,版本迭代周期短,发现的Bug可以被及时修复,同时,更多的新功能会被加入
Netty常见的使用场景如下:
- 互联网行业。在分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少,Netty作为异步高性能的通信框架,往往作为基础通信组件被这些RPC框架使用。典型的应用有:阿里分布式服务框架Dubbo的RPC框架使用Dubbo协议进行节点间通信,Dubbo协议默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信
- 游戏行业。无论是手游服务端还是大型的网络游戏,Java语言得到了越来越广泛的应用。Netty作为高性能的基础通信组件,它本身提供了TCP/UDP和HTTP协议栈。非常方便定制和开发私有协议栈,账号登录服务器,地图服务器之间可以方便的通过Netty进行高性能的通信
- 大数据领域。经典的Hadoop的高性能通信和序列化组件Avro的RPC框架,默认采用Netty进行跨节点通信,它的Netty Service基于Netty框架二次封装实现
Netty自以为异步事件驱动的网络,高性能之处主要来自于其I/O模型和线程处理模型,前者决定如何收发数据,后者决定如何处理数据
阻塞I/O
传统阻塞I/O(BIO)的特点是:
- 每个请求都需要独立的线程完成数据read,业务处理,数据write的完整操作问题
- 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大
- 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在read上,造成线程资源浪费
I/O复用模型
在I/O复用模型中,会用到select,这个函数也会使进程阻塞,但是和阻塞I/O所不同的是这个函数可以在一个线程中同时阻塞多个I/O操作,而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O函数。Netty的非阻塞I/O的实现关键是基于I/O复用模型,这里用selector对象表示。
Netty的IO线程NioEventLoop由于聚合了多路复用器Selector,可以同时并发处理成百上千个客户端连接。
当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务
线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道
由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁IO阻塞导致的线程挂起
一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O-连接-线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升
基于Buffer
传统的IO是面向字节流活字符流的,以流式的方式顺序地从一个stream中读取一个或多个字节,因此也就不能随意改变读取指针的位置
在NIO中,抛弃了传统的IO流,,而是引入了Channel和Buffer的概念。在NIO中,只能从channel中读取数据到Buffer中或将数据从Buffer中写入到channel中
基于Buffer操作不像传统IO的顺序操作,NIO中可以随意地读取任意位置的数据
事件驱动模型
通常,我们设计一个事件处理模型的程序有两种思路:
- 轮询方式,线程不断轮询访问相关事件发生源有没有发生事件,有发生事件就调用事件处理逻辑
- 事件驱动方式,发生事件,主线程吧事件放入事件队列,在另外线程不断循环消费事件列表中的事件,调用事件对应的处理逻辑处理事件。事件驱动方式也被成为消息通知方式,其实是设计模式中的观察者模式的思路
主要包括4个组件:
- 事件队列:接收事件的入口,存储待处理事件
- 分发器:将不同的事件分发到不同的业务逻辑单元
- 事件通道:分发器与处理器之间的联系渠道
- 事件处理器:实现业务逻辑,处理完成后会发出事件,触发下一步操作
可以看出,相对传统轮询模式,事件驱动有如下优点:
- 可扩展性好,分布式的异步架构,事件处理器之间高度解耦,可以方便扩展事件处理逻辑
- 高性能,基于队列暂存事件,能方便并行异步处理事件
Reactor线程模型
Reactor是反应堆的意思,Reactor模型是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式
服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor模式也叫Dispatcher模式,即I/O多路复用统一监听事件,收到事件后分发(Dispatch给某进程),是编写高性能网络服务器的必备技术之一
Reactor模型中有2个关键组成:
- Reactor,在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件作出反应。
- Handlers,处理程序执行IO事件要完成的实际事件,,reactor通过调用适当的处理程序来响应IO事件,处理程序执行非阻塞操作
取决于Reactor的数量和Handler线程数量的不同,Reactor模型有3个变种:
- 单reactor 单线程
- 单reactor 多线程
- 主从reactor 多线程
Netty线程模型
Netty主要是基于主从Reactors多线程模型(如下图)做了一些修改,其中主从reactor多线程模型有多个reactor:
- MainReactor负责客户端的连接请求,并将请求转交给SubReactor
- SubReactor负责相应通道的IO读写请求
- 非IO请求(具体逻辑处理)的任务则会直接进入写入队列,等到worker threads进行处理
这里引用Doug Lee大神的Reactor介绍:Scalable IO in Java里面关于主从Reactor多线程模型的图:
特别说明的是:虽然Netty的线程模型基于主从Reactor多线程,借用了MainReactor和SubReactor的结构。但是实际实现上SubReactor和Worker线程在同一个线程池中
- bossGroup线程池则只是在bind某个端口后,获得其中一个线程作为MainReactor,专门处理端口的Accept事件,每个端口对应一个boss线程
- workerGroup线程池会被各个SubReactor和Worker线程充分利用
异常处理
异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者
Netty中的IO操作是异步的,包括Bind、Write、Connect等操作会简单的返回一个channelFuture
调用者并不能立刻获得结果,而是通过Future-Listener机制,用户可以方便的主动获取或通过通知机制来获得IO操作结果
当future对象刚刚创建时,处于非完成状态,调用者可以通过返回的ChannelFuture来获取操作执行的状态,注册监听函数来执行完成后的操作
模块组件
Bootstrap、ServerBootstrap
Bootstrap 意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件,Netty 中 Bootstrap 类是客户端程序的启动引导类,ServerBootstrap 是服务端启动引导类。
Future、ChannelFuture
正如前面介绍,在 Netty 中所有的 IO 操作都是异步的,不能立刻得知消息是否被正确处理。
但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过 Future 和 ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。
Channel
Netty 网络通信的组件,能够用于执行网络 I/O 操作。Channel 为用户提供:
当前网络连接的通道的状态(例如是否打开?是否已连接?)
网络连接的配置参数 (例如接收缓冲区大小)
提供异步的网络 I/O 操作(如建立连接,读写,绑定端口),异步调用意味着任何 I/O 调用都将立即返回,并且不保证在调用结束时所请求的 I/O 操作已完成。 调用立即返回一个 ChannelFuture 实例,通过注册监听器到 ChannelFuture 上,可以 I/O 操作成功、失败或取消时回调通知调用方。
支持关联 I/O 操作与对应的处理程序。
不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应。下面是一些常用的 Channel 类型:
NioSocketChannel,异步的客户端 TCP Socket 连接。
NioServerSocketChannel,异步的服务器端 TCP Socket 连接。
NioDatagramChannel,异步的 UDP 连接。
NioSctpChannel,异步的客户端 Sctp 连接。
NioSctpServerChannel,异步的 Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件 IO。
Selector
Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 Channel 事件。
当向一个 Selector 中注册 Channel 后,Selector 内部的机制就可以自动不断地查询(Select) 这些注册的 Channel 是否有已就绪的 I/O 事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel 。
NioEventLoop
NioEventLoop 中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用 NioEventLoop 的 run 方法,执行 I/O 任务和非 I/O 任务:
I/O 任务,即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由 processSelectedKeys 方法触发。
非 IO 任务,添加到 taskQueue 中的任务,如 register0、bind0 等任务,由 runAllTasks 方法触发。
两种任务的执行时间比由变量 ioRatio 控制,默认为 50,则表示允许非 IO 任务执行的时间与 IO 任务的执行时间相等。
NioEventLoopGroup
NioEventLoopGroup,主要管理 eventLoop 的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个 Channel 上的事件,而一个 Channel 只对应于一个线程。
ChannelHandler
ChannelHandler 是一个接口,处理 I/O 事件或拦截 I/O 操作,并将其转发到其 ChannelPipeline(业务处理链)中的下一个处理程序。
ChannelHandler 本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承它的子类:
ChannelInboundHandler 用于处理入站 I/O 事件。
ChannelOutboundHandler 用于处理出站 I/O 操作。
或者使用以下适配器类:
ChannelInboundHandlerAdapter 用于处理入站 I/O 事件。
ChannelOutboundHandlerAdapter 用于处理出站 I/O 操作。
ChannelDuplexHandler 用于处理入站和出站事件。
ChannelHandlerContext
保存 Channel 相关的所有上下文信息,同时关联一个 ChannelHandler 对象。
ChannelPipline
保存 ChannelHandler 的 List,用于处理或拦截 Channel 的入站事件和出站操作。
ChannelPipeline 实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及 Channel 中各个的 ChannelHandler 如何相互交互。
下图引用 Netty 的 Javadoc 4.1 中 ChannelPipeline 的说明,描述了 ChannelPipeline 中 ChannelHandler 通常如何处理 I/O 事件。
I/O 事件由 ChannelInboundHandler 或 ChannelOutboundHandler 处理,并通过调用 ChannelHandlerContext 中定义的事件传播方法。
例如 ChannelHandlerContext.fireChannelRead(Object)和 ChannelOutboundInvoker.write(Object)转发到其最近的处理程序。
入站事件由自下而上方向的入站处理程序处理,如图左侧所示。入站 Handler 处理程序通常处理由图底部的 I/O 线程生成的入站数据。
通常通过实际输入操作(例如 SocketChannel.read(ByteBuffer))从远程读取入站数据。
出站事件由上下方向处理,如图右侧所示。出站 Handler 处理程序通常会生成或转换出站传输,例如 write 请求。
I/O 线程通常执行实际的输出操作,例如 SocketChannel.write(ByteBuffer)。
在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应,它们的组成关系如下:
一个 Channel 包含了一个 ChannelPipeline,而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。
入站事件和出站事件在一个双向链表中,入站事件会从链表 head 往后传递到最后一个入站的 handler,出站事件会从链表 tail 往前传递到最前一个出站的 handler,两种类型的 handler 互不干扰。
Netty 工作原理架构
初始化并启动 Netty 服务端过程如下:
public static void main(String[] args) {
// 创建mainReactor
NioEventLoopGroup boosGroup = newNioEventLoopGroup();
// 创建工作线程组
NioEventLoopGroup workerGroup = newNioEventLoopGroup();
final ServerBootstrap serverBootstrap = newServerBootstrap();
serverBootstrap
// 组装NioEventLoopGroup
. group(boosGroup, workerGroup)
// 设置channel类型为NIO类型
.channel(NioServerSocketChannel.class)
// 设置连接配置参数
.option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(ChannelOption.TCP_NODELAY, true)
// 配置入站、出站事件handler
.childHandler( newChannelInitializer() {
@ Override
protectedvoidinitChannel(NioSocketChannel ch) {
// 配置入站、出站事件channel
ch.pipeline().addLast(…);
ch.pipeline().addLast(…);
}
});
// 绑定端口
intport = 8080;
serverBootstrap.bind(port).addListener(future -> {
if(future.isSuccess()) {
System. out.println( newDate() + “: 端口[”+ port + “]绑定成功!”);
} else{
System.err.println( “端口[”+ port + “]绑定失败!”);
}
});
}
基本过程如下:
初始化创建 2 个 NioEventLoopGroup,其中 boosGroup 用于 Accetpt 连接建立事件并分发请求,workerGroup 用于处理 I/O 读写事件和业务逻辑。
基于 ServerBootstrap(服务端启动引导类),配置 EventLoopGroup、Channel 类型,连接参数、配置入站、出站事件 handler。
绑定端口,开始工作。
结合上面介绍的 Netty Reactor 模型,介绍服务端 Netty 的工作架构图:
服务端 Netty Reactor 工作架构图
Server 端包含 1 个 Boss NioEventLoopGroup 和 1 个 Worker NioEventLoopGroup。
NioEventLoopGroup 相当于 1 个事件循环组,这个组里包含多个事件循环 NioEventLoop,每个 NioEventLoop 包含 1 个 Selector 和 1 个事件循环线程。
每个 Boss NioEventLoop 循环执行的任务包含 3 步:
轮询 Accept 事件。
处理 Accept I/O 事件,与 Client 建立连接,生成 NioSocketChannel,并将 NioSocketChannel 注册到某个 Worker NioEventLoop 的 Selector 上。
处理任务队列中的任务,runAllTasks。任务队列中的任务包括用户调用 eventloop.execute 或 schedule 执行的任务,或者其他线程提交到该 eventloop 的任务。
每个 Worker NioEventLoop 循环执行的任务包含 3 步:
轮询 Read、Write 事件。
处理 I/O 事件,即 Read、Write 事件,在 NioSocketChannel 可读、可写事件发生时进行处理。
处理任务队列中的任务,runAllTasks。
其中任务队列中的 Task 有 3 种典型使用场景。
①用户程序自定义的普通任务
ctx.channel().eventLoop().execute( newRunnable() {
@Override
public void run(){
//…
}
});
②非当前 Reactor 线程调用 Channel 的各种方法
例如在推送系统的业务线程里面,根据用户的标识,找到对应的 Channel 引用,然后调用 Write 类方法向该用户推送消息,就会进入到这种场景。最终的 Write 会提交到任务队列中后被异步消费。
③用户自定义定时任务
ctx.channel().eventLoop().schedule( newRunnable() {
@Override
publicvoidrun(){
}
}, 60, TimeUnit.SECONDS);
总结
现在稳定推荐使用的主流版本还是 Netty4,Netty5 中使用了 ForkJoinPool,增加了代码的复杂度,但是对性能的改善却不明显,所以这个版本不推荐使用,官网也没有提供下载链接。
Netty 入门门槛相对较高,是因为这方面的资料较少,并不是因为它有多难,大家其实都可以像搞透 Spring 一样搞透 Netty。
在学习之前,建议先理解透整个框架原理结构,运行过程,可以少走很多弯路。
参考资料:
Netty入门与实战:仿写微信 IM 即时通讯系统
Netty官网
Netty 4.x学习笔记 - 线程模型
Netty入门与实战
理解高性能网络模型
Netty基本原理介绍
software-architecture-patterns.pdf
Netty高性能之道 —— 李林锋
《Netty In Action》
《Netty权威指南》
作者:陈彩华
编辑:陶家龙、孙淑娟
出处:转载自Hollis(ID:hollischuang)微信公众号