第十八章 Netty线程模型
18.1 Netty的线程模型
Netty线程模型本质上还是遵循了Reactor的基础线程模型
18.1.1 Reactor单线程模型
指所有的I/O操作都在同一个NIO线程上面完成。NIO线程职责如下:
- 作为NIO服务端,接收客户端的TCP连接;
- 作为NIO客户端,向服务端发起TCP连接:
- 读取通信对端的请求或者应答消息;
- 向通信对端发送消息请求或者应答消息。
Reactor模型采用异步非阻塞I/O,所有的I/O都不会导致阻塞,理论上一个线程可以独立处理所有I/O相关的操作。
对于高负载、大并发的应用不合适:
- 性能无法支持
- 负载过重,处理速度会变慢,导致大量连接超时重传,恶性循环,成为系统瓶颈
- 可靠性问题,一旦NIO线程出问题,整个通信系统模块将不可用
18.1.2 Reactor多线程模型
与单线程模型最大的区别在于有一组NIO线程组来处理I/O
特点:
- 有专门一个 NIO线程--Acceptor 线程用于监听服务端,接收客户端的TCP连接请求。
- 网络I/0操作--读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送。
- 一个NIO线程可以同时处理N条链路,但是一个链路只对应一个NIO线程,防止发生并发操作问题。
但一个NIO线程负责监听和处理所有的客户端连接可能会存在性能问题,如百万级的并发
18.1.3 主从Reactor多线程模型
特点:服务端用于接收客户端连接的不再是一个单独的NIO线程,而是一个独立的NIO线程池。Acceptor接收到客户端TCP连接请求并处理完成后(可能包含接入认证等),将新创建的SocketChannel注册到I/O线程池(subreactor 线程池)的某个 I/0 线程上,由它负责SocketChanne 的读写和编解码工作。Acceptor 线程池仅仅用于客户端的登录、握手和安全认证,一旦链路建立成功,就将链路注册到后端subReactor线程池的I/0线程上,由I/0线程负责后续的I/0操作。
18.1.4 Netty线程模型
取决于启动参数配置,可以同时支持Reactor单线程、多线程和主从Reactor模型
服务端启动时,创建了两个NioEventLoopGroup,两个独立的Reactor线程池,一个用于接收客户端的TCP连接,另一个用于处理I/O或者系统Task等
Netty用于接收客户端请求的线程池职责如下,
- 接收客户端TCP连接,初始化Channel参数;
- 将链路状态变更事件通知给ChannelPipeline。
Netty处理I/O操作的Reactor 线程池职责如下。
- 异步读取通信对端的数据报,发送读事件到ChannelPipeline;
- 异步发送消息到通信对端,调用ChannelPipeline的消息发送接口:
- 执行系统调用Task;
- 执行定时任务Task,例如链路空闲状态监测定时任务。
为了提升性能,Netty在多处使用了无锁化,避免多线程竞争导致性能下降。
18.1.5 最佳实践
- 创建两个NioEventLoopGroup,用于逻辑隔离NIO Acceptor 和 NIO I/O 线程
- 尽量不要在ChannelHandler中启动用户线程(解码后用于将POJO消息派发到后端业务线程的除外)
- 解码要放在NIO线程调用的解码 Handler中进行,不要切换到用户线程中完成消息的解码。
- 如果业务逻辑操作非常简单,没有复杂的业务逻辑计算,没有可能会导致线程被阻塞的磁盘操作、数据库操作、网路操作等,可以直接在NIO线程上完成业务逻辑编排不需要切换到用户线程。
- 如果业务逻辑处理复杂,不要在NIO线程上完成,建议将解码后的POJO消息封装成Task,派发到业务线程池中由业务线程执行,以保证NIO线程尽快被释放,处理其他的 I/O 操作
两个线程数公式:
公式一:线程数量=(线程总时间/瓶颈资源时间)x瓶颈资源的线程并行数
公式二:QPS=1000/线程总时间x线程数。
第二十章 Netty架构剖析
20.1 Netty逻辑架构
采用典型的三层网络架构进行设计和开发
20.1.1 Reactor通信调度层
该层的职责就是监听网络的读写和连接操作,负责将网络层的数据读取到内存缓冲区中,然后触发各种事件到PipeLine中,由PipeLine管理的职责链进行后续的处理。有一系列辅助类,比如Reactor的NioEventLoop及其父类,NioSocketChannel/NioServerSocketChannel极其父类,ByteBuffer以及衍生的buffer,Unsafe及其衍生出的各种内部类。
20.1.2 职责链 ChannelPipeline
负责事件的传播,负责动态编排职责链,可以选择监听和处理自己关心的事件,可以拦截处理和向后向前传播事件。不同应用的Handler节点的功能也不同,通常情况下,往往会开发编解码Handler用于消息的编解码。
20.1.3 业务逻辑编排层 ServiceHandler
业务逻辑排层通常有两类:一类是纯粹的业务逻辑编排,还有一类是其他的应用层协议插件,用于特定协议相关的会话和链路管理。对于开发者只需要关心服务层的业务逻辑开发,应用协议由协议开发人员关注即可,Netty框架实现了NIO框架各层的解耦
20.2 关键架构质量属性
20.2.1 高性能
"性能是设计出来的,而不是测试出来的"
Netty如何实现高性能:
- 采用异步非阻塞的I/O类库,基于Reactor模式实现,解决传统同步阻塞I/O模式下一个服务端无法平滑地处理线性增长的客户端的问题
- TCP接收和发送缓冲区使用直接内存代替堆内存,避免了内存复制,提升了I/O读取和写入的性能。例如零拷贝
- 支持通过内存池的的方式循环利用ByteBuf,避免了频繁创建和销毁ByteBuf带来的性能损耗
- 可配置的I/O线程数、TCP参数等,为不同的用户场景提供定制化的调优参数,满足不同的性能场景
- 采用环形数组缓冲区实现无锁化并发编程,代替传统的线程安全容器或者锁
- 合理的使用线程安全容器、原子类,提升了并发能力
- 关键资源的处理使用单线程串行化的方式,避免多线程并发访问带来的锁竞争和额外的CPU资源消耗问题
- 通过引用计数器及时地申请不再被引用的对象,细粒度的内存管理降低了GC的频率,减少了频繁GC带来的时延增大和CPU损耗
20.2.2 可靠性
-
链路有效性检测:
通过心跳检测机制保证长连接有效性,有业务时,无需心跳检测。
为了支持心跳,Netty提供两种空闲检测机制:读空闲和写空闲
-
内存保护机制:
- 通过对象引用计数器对Netty的ByteBuf等内置对象进行细粒度的的内存申请和释放,对非法的对象引用进行检测和保护
- 通过内存池来重用ByteBuf,节省内存
- 可设置的内存容量上限
-
优雅停机:
指当系统退出时,JVM通过注册的Shutdown Hook拦截到退出信号量,然后执行退出操作,释放相关模块的资源占用,将缓冲区的消息处理完成或清空,将待刷新的数据持久化到磁盘或者数据库中,等到资源回收和缓冲区消息处理完成后再退出
20.2.3 可定制性
提现在以下几点:
- 责任链模式:便于业务逻辑的拦截、定制和扩展
- 基于接口的开发:用户可以自定义相关接口
- 提供了大量工厂类,重载工厂类可以按需创建出用户实现的对象
- 提供了大量的系统参数供用户按需设置
20.2.4 可扩展性
可以方便的进行应用层协议定制
第二十一章 Java多线程编程在Netty的应用
21.1 Java内存模型与多线程编程
21.1.1 硬件的发展和多任务处理
CPU的计算能力很强,但是计算机的存储系统、网络通信等I/O性能跟不上,很大的执行时间被浪费在I/Owait上面
21.1.2 Java内存模型
-
工作内存和主存
JMM规定所有变量都存储在主内存中(JVM内存的一部分),线程有自己独立的工作内存,它保存了线程需要使用的变量的主内存复制,线程不能直接操作主内存和其他线程,线程间的变量访问通过主内存完成。
-
Java内存交互协议
JMM定义了8种操作来完成主内存和工作内存的变量访问,如下:
- lock:加锁
- unlock:解锁
- read:从主存读到线程工作内存,供load使用
- load:把read读到的值放入工作内存中
- use:把工作内存变量传给Jvm执行引擎
- assign:把jvm执行引擎的值传给工作内存
- store:把工作内存的变量值传给主存
- write:把store的值写入主存
-
Java线程
主流的操作系统实现线程的方式有三种:
- 内核线程(KLT)实现,由内核完成线程切换
- 用户线程(UT),线程的创建启动运行和销毁都在用户态中完成,性能更高
- 混合以上两种
21.2 Netty并发编程
21.2.1 对共享的可变数据进行正确同步
使用Synchronized关键字,以ServerBootStrap的ServerSocketChannel的Socket属性为例,它的定义为LinkedHashMap,是非线程安全的,所以要上锁。
21.2.2 正确使用锁
三个多线程编程技巧:
- 使用wait方法使线程等待某个条件
- 始终使用wait循环调用wait方法,不要再循环外调用,否则可能导致意外唤醒
- 不知道使用notify还是notifyAll,保守使用notifyAll,如果等待所有线程都在等待同一个条件,且每次只有一个线程可以被唤醒,就使用notify
21.2.3 volatile
volatile的特性:可见性、禁止指令重排序优化
但不能保证互斥性,所以不能替代锁,Netty中ioRatio定义成volatile,因为形成了一个线程写,一个线程读的情况,就可以使用volatile代替Synchronized提升性能
21.2.4 CAS和原子类
实现被volatile修饰的变量的原子更新操作
21.2.5 线程安全类使用
实际编码中,通过线程池、Task(Runnable/Callable)、原子类和线程安全容器代替传统的同步锁,提升性能。
21.2.6 读写锁的应用
使用场景:
- 用于读多写少的场景,用来替代传统的同步锁,提升性能
- 可重入,可降级,具体可见并发编程
- ReentrantReadWriteLock,公平性
- 支持非阻塞的尝试获取锁
- 最后要释放锁