只看了服务端的线程结构,客户端还没看,不知道是不是一样,所以这篇就叫服务端线程模型吧,以后看了客户端再修改。术语和第二篇一样,不重复写了。
1. Netty与Reactor的关系
Netty是Reactor模型的实现,有关Reactor有一张经典的图:
通过前面的分析,大概能知道:
① main reactor即NioServerBoss,由它的selector负责监听端口,注册连接事件,并处理accept,充当上面的acceptor的作用,main reactor用到的线程池这个图中没画出来,这个boss线程池发挥的作用不是很重要,主要是IOT。
② sub reactor即NioWorker,也是由它的selector充当reactor的作用,构造NioServerSocketChannelFactory时传入的executor即上图的thread pool,worker threads由executor负责管理生命周期。
③ queued tasks所属的队列即NioServerBoss和NioWorker的taskQueue,selector和taskQueue是protected的,即每个NioServerBoss和NioWorker都会持有一个。
④ 通过第三点,可以看出Netty实际的结构是一个main reactor,多个sub reactor(由线程池线程数目配置决定)。
2. worker与IOT的关系
worker---taskQueue/selector---IOT
一个worker持有一个taskQueue和selector,而IOT的主要逻辑就是processTaskQueue和process,即分别处理poll taskQueue,执行任务,以及操作selector的selection key,对触发的事件进行处理。所以它们之间实际上是通过taskQueue和selector产生联系的。并且每个NioWorker一个taskQueue和selector,可以避免多个worker争用同一个taskQueue和selector的情况,也就避免了做同步(虽然selector是线程安全的,但是selection key不是,如果多个worker共用则无法避免做同步)
3. 线程启动流程
① 前面2篇提到在new NioServerSocketChannelFactory时,启动了boss和IOT。通过看源码,IOT实际上是在每次new NioWorker时,通过传入的work executor,启动了一个IOT,然后该IOT开始循环处理taskQueue
和selector,当一组NioWorker构造完成时,一组IOT也一起启动了。
② worker构造完成后,当NioServerBoss accept到新连接请求时,boss需要负责构造新channel,并分配给某一个worker。具体做法是accept时,从WorkerPool中取一个worker来new channel,取的时候是通过模算法来取的:
public E nextWorker() {
return (E) workers[Math.abs(workerIndex.getAndIncrement() % workers.length)];
}
之前看到这里的时候,忽略了取模操作,所以感觉好像channel和worker的关系是一对一,即一个channel一个线程,当时看到这里怎么也没想通,因为明明使用的是nio模式,不可能一对一的,后来细看才发现原来看丢了。。。。
取模操作产生的效果就是在new channel时,依次从数组里取worker,然后将channel绑定到该worker上,所以实际上,channel和worker的关系是多对一,channel是以worker分组的,一个worker负责一组channel的IO。
netty使用多个sub reactor去分别分组处理channel,我理解的这样设计的好处:
1) 首先就是前面提到的不用同步,避免多个IOT争用一个sub reactor。
2) 可扩展性更灵活,因为如果用多个IOT,一个taskQueue或selector,必然会用竞争,当服务端需要更多IOT来处理高并发的时候,IOT越多,竞争越激烈,竞争的开销远远超过多IOT带来的性能提升,所以虽然理论上多IOT,单taskQueue或selector也具备扩展性,但是实际情况下并没有可操作性。
③ 获取worker后,使用worker,acceptedSocket和NioServerSocketChannel去new NioAcceptedSocketChannel,将它们绑定到新channel(该channel可以理解为与物理连接对应的、位于netty层的逻辑连接)。然后投递一个注册interest事件的任务到worker.taskQueue上,由IOT负责poll并执行。
4. 由线程模型带来的特性
① messageReceived太耗时造成IOT无响应
服务端select到OP_READ后,读数据,然后fireMessageReceived,因此messageReceived方法是IOT调用的,当其中的操作太耗时时(比如大量发送消息,IOT会逐条加锁channel.writeLock,循环发送消息,发完才返回进入下一个IOT循环),IOT忙于处理messageReceived,不能响应select,导致所有绑定到该IOT的channel都无响应,所以耗时的操作必须另起业务线程。
② 业务线程(用户线程,UT)大量发送消息
因为另起了业务线程,IOT可以及时返回以进行下一次processTaskQueue和process。由于写的数据和写的操作分离的设计(writeFromUserCode中先offer到writeBufferQueue,再投递channel.writeTask给IOT),造成当UT投递writeTask和offer数据的速度大于IOT的发送速度时(比如,绑定到同一IOT的channel太多,IOT忙于处理其它channel时),此时假设已有n条数据offer到了writeBufferQueue,当前正在执行第i个任务(i+1<n),第i+1个任务仍在taskQueue中(因为任务i还未处理完),使得writeTaskInTaskQueue未被置false(该标志表示可以继续投递writeTask,仅限writeTask,其它任务类型不清楚),i+1以后的任务无法投递进去,但是此时writeBufferQueue中已有n条数据,即相当于后面的写操作的数据先于写任务被offer,写的数据和写的任务并非一一对应,同时被投递的。在这种情况下,任务i会一直poll writeBufferQueue然后发送,直到writeBufferQueue为空(AbstractNioWorker.write0就是这样实现的),造成任务i”帮忙”把后面任务的数据都发了。然后执行任务i+1:加锁channel.writeLock--->poll writeBufferQueue为空,退出循环--->fireWriteComplete--->解锁channel.writeLock,该步骤实际上并无任何数据发送,但白白多锁了一次writeLock(还fire了一次complete事件,fire一次应该关系不大,暂不考虑)。并且任务i+2也会同样多加锁一次,直到任务n+1(在投递成功的writeTask数赶上offer的数据之前,这种状况会一直持续,在任务i执行完后,可能会有n以后的数据offer进去,这些新增的数据终究会在某些任务中被发送,但中间的任务应该始终还是会有些被“浪费”掉,可能呈现出间歇性的特征;如果要完全不浪费,即一条数据由对应的写任务发送,应该是在UT和IOT处理速度比较平衡的情况下才会发生吧)。在极端情况下,可能任务1就把后面所有任务的数据都发了,这样的话相当于后面的任务全都“废了”,并且前面的写任务“帮忙”发的数据条数越多,IOT不响应selector的概率越大(因为IOT被困在write0的循环里了,直到queue取空才会退出)。
这种情况是我在调试的时候发现的,然后手工干预线程调度,进一步确定的情况,但是在运行环境中会不会存在这种情况,我还不是很确定,目前想到的就是UT入队速度远大于IOT的处理速度时,这种情况我只能猜测是在IOT绑定的channel特别多时可能会发生(如果是linux,selector能处理的连接数有限制,只能是单个进程能打开的文件描述符个数上限值,默认好像是1024),不知道并发达到多大的时候会出现这种情况,不知道1024够不够资格去触发。。。所以大家看了第二点,帮我想想有没哪里没分析对。
我又想了一下:其实也不算白白多加一次锁和多fire一次,在理想情况下,write多少次就应该锁多少次writeLock,如果连这个都想省掉,是不是太夸张了,不知道这算不算一个问题,或者说有没有优化的必要和可能性,求指点啊。。。
本人辛苦分析、码字,请尊重他人劳动成果,转载不注明出处的诅咒你当一辈子一线搬砖工,嘿嘿~
欢迎讨论、指正~~
下篇预告:客户端连接流程分析
相关推荐
Netty是一款高性能的网络应用程序框架,它使用Java编程语言开发,主要用于网络应用程序的快速和易于开发,支持TCP和UDP...通过对Netty源码的深入分析,可以更好地理解其工作机制,对开发高性能的网络应用有极大的帮助。
#### 五、Netty源码分析实战案例 1. **ChannelHandlerContext与ChannelHandlerAdaptor详解**: - 分析`ChannelHandlerContext`的生命周期及其与`ChannelHandler`之间的交互方式。 - 深入理解`...
压缩包内的文件"netty源码剖析视频教程.txt"可能是课程的详细大纲或笔记,提供了对课程内容的进一步概述,包括每个章节的重点和案例分析,是学习过程中不可或缺的参考资料。通过结合视频教程和文本资料,学习者可以...
这个"Netty源码剖析与实战配套代码.zip"文件包含了对Netty框架的深入源码分析和实战应用示例。下面将详细探讨Netty的核心概念和关键功能,以及它如何在实际项目中发挥作用。 1. **Netty概述** - Netty是由JBOSS...
不过,这只是基础,深入研究Netty源码可以帮助我们更好地理解和优化系统性能,例如了解其内部的事件调度机制、缓冲区管理以及线程模型等。 最后,标签中的"源码"提示我们要关注Netty的底层实现,通过阅读和分析源码...
NIO客户端13 3.Netty源码分析16 3.1. 服务端创建16 3.1.1. 服务端启动辅助类ServerBootstrap16 3.1.2. NioServerSocketChannel 的注册21 3.1.3. 新的客户端接入25 3.2. 客户端创建28 3.2.1. 客户端连接辅助类...
由于Netty源码较庞大,所以文章建议通过分析几个关键组件来构建对整个框架的理解,从而避免在庞大的源码海洋中迷失方向。通过理解Netty的关键点,包括NIO的使用、事件处理机制、ChannelPipeline的运作以及Handler的...
3. **EventLoop** 和 **EventLoopGroup**:EventLoop 是 Netty 中的线程模型,负责处理 I/O 事件。EventLoopGroup 是 EventLoop 的集合,通常每个 EventLoop 处理一组 Channel。 4. **ByteBuf**:Netty 自定义的...
- 分析项目源码,了解ServerBootstrap和Bootstrap的配置过程。 - 理解ChannelHandler和ChannelPipeline的事件处理机制。 - 学习如何编写自定义的Encoder和Decoder。 - 实践客户端和服务器的交互,调试运行聊天...
《Netty源码深入剖析》一书旨在帮助读者深入了解Netty框架的工作原理和技术细节,从基础知识入手,逐步过渡到高级优化技巧,使开发者能够更好地掌握并应用Netty于实际项目中。 ### 一、Netty简介与核心特性 Netty...
"解释每一步的运作"意味着开发者可以通过注释了解Netty如何处理连接、读写操作、线程模型以及编码解码等核心流程。此外,"还有jar包"表明除了源代码,还包含了编译后的可执行文件,可以直接运行示例代码,便于理解和...
Netty源码的分析主要围绕服务端和客户端的创建、读写操作等方面进行。服务端的启动涉及到ServerBootstrap类,以及NioServerSocketChannel的注册,新的客户端接入,客户端连接的建立通过Bootstrap类实现。读写操作则...
通过阅读和分析Netty源码,开发者能够更好地理解其内部工作原理,优化网络应用,以及定制适合自己项目的特性和功能。对于Java开发者来说,深入学习Netty源码不仅可以提升网络编程技能,也有助于解决实际问题,比如...
5. **线程模型**:Netty的EventLoop(事件循环)线程模型,确保了每个连接只由一个线程处理,避免了线程切换带来的开销,提升了性能。 6. **易于扩展**:Netty允许用户自定义协议编解码器,提供了高度灵活的管道...
- **源码分析**: - **初始化过程**:分析如何创建Channel、EventLoop等组件,并配置ChannelPipeline。 - **事件循环机制**:深入了解EventLoop的工作原理,包括如何调度任务、处理I/O事件等。 - **编解码器设计*...
“Netty04-优化与源码.md”将深入探讨如何优化Netty应用性能,这可能包括减少内存拷贝、选择合适的线程模型、配置合适的缓冲区大小等。同时,源码分析可以帮助理解Netty内部的工作机制,比如事件循环的运行原理,...