Netty权威指南:Netty总结-Netty线程模型与架构剖析

第十八章 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用于接收客户端请求的线程池职责如下,

  1. 接收客户端TCP连接,初始化Channel参数;
  2. 将链路状态变更事件通知给ChannelPipeline。

Netty处理I/O操作的Reactor 线程池职责如下。

  1. 异步读取通信对端的数据报,发送读事件到ChannelPipeline;
  2. 异步发送消息到通信对端,调用ChannelPipeline的消息发送接口:
  3. 执行系统调用Task;
  4. 执行定时任务Task,例如链路空闲状态监测定时任务。

为了提升性能,Netty在多处使用了无锁化,避免多线程竞争导致性能下降。

18.1.5 最佳实践

  1. 创建两个NioEventLoopGroup,用于逻辑隔离NIO Acceptor 和 NIO I/O 线程
  2. 尽量不要在ChannelHandler中启动用户线程(解码后用于将POJO消息派发到后端业务线程的除外)
  3. 解码要放在NIO线程调用的解码 Handler中进行,不要切换到用户线程中完成消息的解码。
  4. 如果业务逻辑操作非常简单,没有复杂的业务逻辑计算,没有可能会导致线程被阻塞的磁盘操作、数据库操作、网路操作等,可以直接在NIO线程上完成业务逻辑编排不需要切换到用户线程。
  5. 如果业务逻辑处理复杂,不要在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如何实现高性能:

  1. 采用异步非阻塞的I/O类库,基于Reactor模式实现,解决传统同步阻塞I/O模式下一个服务端无法平滑地处理线性增长的客户端的问题
  2. TCP接收和发送缓冲区使用直接内存代替堆内存,避免了内存复制,提升了I/O读取和写入的性能。例如零拷贝
  3. 支持通过内存池的的方式循环利用ByteBuf,避免了频繁创建和销毁ByteBuf带来的性能损耗
  4. 可配置的I/O线程数、TCP参数等,为不同的用户场景提供定制化的调优参数,满足不同的性能场景
  5. 采用环形数组缓冲区实现无锁化并发编程,代替传统的线程安全容器或者锁
  6. 合理的使用线程安全容器、原子类,提升了并发能力
  7. 关键资源的处理使用单线程串行化的方式,避免多线程并发访问带来的锁竞争和额外的CPU资源消耗问题
  8. 通过引用计数器及时地申请不再被引用的对象,细粒度的内存管理降低了GC的频率,减少了频繁GC带来的时延增大和CPU损耗

20.2.2 可靠性

  1. 链路有效性检测:

    通过心跳检测机制保证长连接有效性,有业务时,无需心跳检测。

    为了支持心跳,Netty提供两种空闲检测机制:读空闲和写空闲

  2. 内存保护机制:

    • 通过对象引用计数器对Netty的ByteBuf等内置对象进行细粒度的的内存申请和释放,对非法的对象引用进行检测和保护
    • 通过内存池来重用ByteBuf,节省内存
    • 可设置的内存容量上限
  3. 优雅停机:

    指当系统退出时,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内存模型

  1. 工作内存和主存

    JMM规定所有变量都存储在主内存中(JVM内存的一部分),线程有自己独立的工作内存,它保存了线程需要使用的变量的主内存复制,线程不能直接操作主内存和其他线程,线程间的变量访问通过主内存完成。

  2. Java内存交互协议

    JMM定义了8种操作来完成主内存和工作内存的变量访问,如下:

    • lock:加锁
    • unlock:解锁
    • read:从主存读到线程工作内存,供load使用
    • load:把read读到的值放入工作内存中
    • use:把工作内存变量传给Jvm执行引擎
    • assign:把jvm执行引擎的值传给工作内存
    • store:把工作内存的变量值传给主存
    • write:把store的值写入主存
  3. Java线程

    主流的操作系统实现线程的方式有三种:

    1. 内核线程(KLT)实现,由内核完成线程切换
    2. 用户线程(UT),线程的创建启动运行和销毁都在用户态中完成,性能更高
    3. 混合以上两种

21.2 Netty并发编程

21.2.1 对共享的可变数据进行正确同步

使用Synchronized关键字,以ServerBootStrap的ServerSocketChannel的Socket属性为例,它的定义为LinkedHashMap,是非线程安全的,所以要上锁。

21.2.2 正确使用锁

三个多线程编程技巧:

  1. 使用wait方法使线程等待某个条件
  2. 始终使用wait循环调用wait方法,不要再循环外调用,否则可能导致意外唤醒
  3. 不知道使用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,公平性
  • 支持非阻塞的尝试获取锁
  • 最后要释放锁
相关推荐
速盾cdn34 分钟前
速盾:vue的cdn是干嘛的?
服务器·前端·网络
安於宿命38 分钟前
【Linux】简易版shell
linux·运维·服务器
丶Darling.41 分钟前
MIT 6.S081 Lab1: Xv6 and Unix utilities翻译
服务器·unix·lab·mit 6.s081·英文翻译中文
黄小耶@1 小时前
linux常见命令
linux·运维·服务器
叫我龙翔1 小时前
【计网】实现reactor反应堆模型 --- 框架搭建
linux·运维·网络
粤海科技君1 小时前
如何使用腾讯云GPU云服务器自建一个简单的类似ChatGPT、Kimi的会话机器人
服务器·chatgpt·机器人·腾讯云
傲骄鹿先生2 小时前
阿里云centos7.9服务器磁盘挂载,切换服务路径
服务器·阿里云·磁盘
洛卡卡了2 小时前
从单层到 MVC,再到 DDD:架构演进的思考与实践
架构·mvc
不爱学习的YY酱2 小时前
【计网不挂科】计算机网络期末考试——【选择题&填空题&判断题&简述题】试卷(4)
网络·计算机网络
装睡的小5郎2 小时前
家庭宽带如何开启公网ipv4和ipv6
网络