图灵学院Java架构师五期笔记

时间:2025-04-09 14:14:46

缘起
日前在看netty的工作原理,对netty的线程模型很是不能理解,查阅了诸多资料,终于有了一些眉目。特此记录,以备查阅。

阅读对象
netty中的NIO编程模型是基于java Nio的封装,所以需要读者对java NIO有一定的了解,篇幅所限,本文不会对NIO再做详述,有需要的读者可以查看​​JAVA BIO,NIO,AIO详解(附代码实现)以及Netty的简介​​

Netty的NIO线程模型
说netty的线程模型之前,先说传统NIO的使用方式中的关键代码

可以看到是当前线程获得了客户端的连接之后再重新把channel注册到selector上,同时把事件注册为读,这样就可以开始读消息了。也就是说获取tcp连接和读取消息都是在同一个线程里面处理的。那么这种方式有什么问题呢?对于一些小流量应用场景,可以使用单线程模型。但是对于高负载、大并发的应用场景却不合适,主要原因如下:


一个 NIO 线程同时处理成百上千的链路,性能上无法支撑,即便 NIO 线程的 CPU 负荷达到 100%,也无法满足海量消息的编码、解码、读取和发送;
当 NIO 线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了 NIO 线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈;
可靠性问题:一旦 NIO 线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。

那么你可能会想,既然一个线程不行,那我在读取消息的时候采用线程池不就可以了吗?的确是可行的,事实上,在Netty中也确实是这么做的。

netty的一个启动程序如下

可以看到开头new了两个EventLoopGroup,EventLoopGroup可以暂时理解为一个线程组。其中bossGroup负责处理客户端的 TCP 连接请求,如果系统只有一个服务端端口需要监听,则建议 bossGroup 线程组线程数设置为 1,workerGroup 是真正负责 I/O 读写操作的线程组

先看一张netty的执行流程图

图中可以看到boosGroup在接收到请求后会重新注册到workerGroup上,也就是对应了前面说的bossGroup负责处理客户端的 TCP 连接请求,而workerGroup是真正负责 I/O 读写操作的线程组。就像软件开发里面的boss负责分配任务,而worker即程序员负责写代码。那么bossGroup是如何处理tcp的连接请求同时把他注册到workerGroup上的呢?

bossGroup是如何分配任务的
workerGroup中的每一个线程,都有一个多路复用器 Selector,bossGroup每接收到一个客户端连接,就会从workerGroup选择一个线程然后把channel注册到它的Selector上。

伪代码实现

boss中的代码

可以看到,bossGroup接收到了新客户端的请求后,就会调用一个算法,从多个worker中选取一个worker,然后调用worker的注册方法,把这个新客户端的channel注册上去。我们再看workerGroup中的注册方法实现

可以看到,就是把新的channel注册到自己的Selector上,注册好后就会触发workerGroup的堵塞代码块,这样这个workerGroup中的这个线程就会开始读取数据,下面看看worker线程读取数据的伪代码实现(其实就是普通的NIO中读取数据的方式)

总结:bossGroup负责处理客户端的 TCP 连接请求,bossGroup每接收到一个客户端连接,就会从workerGroup选择一个线程然后把channel注册到它的Selector上,这样的话请求接收和请求处理就通过不同的线程分开了,这也是netty高效的原因之一。当然Netty高效的原因绝不仅仅是由于优秀的线程模型的设计,与Netty的编码协议,virtual buffer,以及Zero-Copy(零拷贝)也息息相关 。