J006-Java中的可扩展IO (Scalable IO in Java)

status
Published
slug
J006
type
Post
category
Technology
date
Feb 25, 2025
tags
翻译
Java
IO
J计划
summary
尝试翻译Doug Lea大牛的经典文章《Scalable IO in Java》,详细介绍了几种服务模式
💡
尝试翻译Doug Lea大牛的经典文章《Scalable IO in Java》,详细介绍了几种服务模式
蓝色部分为自己添加的内容,为了实现更好理解
PS:在尝试翻译的过程中,发现自己能力实在有限,很多东西无法表述清楚,附上优秀译文作为补充:https://www.cnblogs.com/dafanjoy/p/11217708.html

网络服务(Network Services)

Web服务、分布式服务等应用程序的通信过程,大都有相同的流程结构:
  1. 读取请求(Read request)
  1. 解码请求(Decode request)
  1. 应用处理(Process service)
  1. 编码应答(Encode reply)
  1. 发送应答(Send reply)
不同的应用程序如XML解析、文件传输、网页生成、计算服务等,主要区别在每一步的性质和成本。

经典的服务设计模式(Classic Service Designs)

notion image
  • 对于每一个连接到Server的Client,服务器都会为其单独开启一个线程,执行handler
如图所示,每一个处理程序(handler)都在对应的线程中启动,经典的Server端Socket服务代码如下:

可伸缩性目标(Scalability Goals)

  • 能够在负载增加的情况下优雅降级
  • 能够随着资源的增加而不断改进
  • 满足可用性和性能目标
    • 低延时
    • 满足高峰需求(高吞吐量
    • 可调的服务质量
分治(Divide and Conquer)通常是实现任何可伸缩性目标的最佳方法

分治设计(Divide and Conquer)

网络服务的分治设计如下:
  • 将处理过程分成若干任务
    • 每个任务执行一个行为且不阻塞
  • 在启用时执行每个任务
    • IO事件通常用作触发器,如handler
    • notion image
java.nio支持的基本机制如下:
  • 非阻塞的读和写
  • 根据检测到的IO事件分发相关联的任务
通过提供一系列事件驱动的设计(Event-driven Designs),能够给网络服务的设计带来无限的可能

事件驱动设计(Event-driven Designs)

事件驱动通常是性能更好的一种设计,原因如下:
  • 更少的资源:不需要为每一个client创建线程
  • 更少的开销:更少的上下文切换和加锁
此外,事件驱动还有一些缺点:
  • 更慢的调度:由于必须手动将操作绑定到事件上,事件驱动的调度通常会更慢
  • 编程难度更大:类似于GUI事件驱动的操作
    • 必须分解成简单的非阻塞动作
      • 但并不是所有阻塞都能消除,如GC, page faults等
    • 必须跟踪服务的逻辑状态

AWT中事件驱动设计

如图所示,是AWT中的事件驱动设计,事件驱动的IO使用了类似的思想但不同的设计
notion image
AWT是 Java 的一个图形用户界面(GUI)工具包
  • AWT使用事件监听器和事件处理程序来处理用户交互
  • 当发生点击事件时,该事件会进入AWT事件序列
  • AWT线程负责从事件队列中取出事件,并将它们分发给相应的事件监听器进行处理。

Reactor 模式(Reactor Pattern)

Reactor(反应器)模式是高性能网络编程在设计和架构层面的基础模式
  • Reactor会调度合适的handler来应答IO事件(类似于AWT 线程)
  • Handler执行非阻塞操作 (类似于AWT的ActionListener)
  • 通过将handler绑定到事件进行管理(类似于AWT的addActionListener)

基本的Reactor模式设计(Basic Reactor Design)

单线程版本

notion image
  • 如图所示,在Reactor单线程模型中,所有的I/O操作(read、send)与非IO操作(decode、compute、encode)都在同一个线程中完成
  • 存在的问题:
    • 一个线程支持处理的连接数非常有限,CPU 很容易打满,性能方面有明显瓶颈;
    • 当多个事件被同时触发时,只要有一个事件没有处理完,其他后面的事件就无法执行,这就会造成消息积压及请求超时;
    • 线程在处理 I/O 事件时,Select 无法同时处理连接建立、事件分发等操作;
    • 如果 I/O 线程一直处于满负荷状态,很可能造成服务端节点不可用。
Java NIO实现了如下类
  • 管道(Channel):支持非阻塞读取的文件、套接字等的连接
  • 缓存(Buffer):类数组对象,可以被Channel直接读写
  • 选择器(Selector):用于判断一组Channel中哪一个有IO事件
  • SelectionKeys:维护IO事件状态和绑定
Reactor代码示例如下:
Handler代码示例如下:
GoF状态对象模式对Handler类的优化
  • 将sender重新绑定为附件

多线程设计

在多处理器场景下,可以策略性地添加线程以提高应用程序的可伸缩性
主要会从两个方面的改进考虑:
  • 工作线程 (Worker Threads)
    • Reactor会迅速触发处理程序Handler,handler处理会减慢Reactor的速度
      • 由于单线程模型的Reactor和Handler在同一个线程中进行操作
    • 因此,需要将handler的非IO处理的转交给工作线程进行,以加快速度Reactor线程的速度
    • 比将计算绑定的处理重新加工成事件驱动的形式更简单
    • 很难将处理过程与IO过程重叠,最好是能先将所有输入读入缓冲区
    • 可以使用线程池进行调优和控制
      • 使用线程池后,能使用比客户端少得多的线程
  • 多Reactor线程(Multiple Reactor Threads)
    • Reactor线程可以饱和处理IO
    • 也可以将负荷分配给其他reactor
      • 负载均衡以匹配CPU和IO速率

使用Worker Thread Pools的模式

  • 从图中可以看出,decode、compute和encode这三个非IO操作被移到了Worker线程中执行
  • 该模式使用一个线程池(worker 线程池)来处理业务操作
notion image
需要注意的问题如下:
  • 任务之间的协调:
    • 每个任务的启用、触发或调用下一个任务
    • 速度很快,不容易控制
  • 对每个处理程序调度程序的回调:
    • 设置状态、attachment等
  • 跨级传递缓冲区
  • 当每个任务产生结果时
    • 在join或wait/notify之上的协调层

PooledExecutor

一种可调的工作线程池,主要方法为execute(Runnable r),可调参数如下:
  • 任务队列的类型(任何通道)
  • 最大线程数、最小线程数
  • 预热与按需线程
  • 线程空闲时间
    • 保持活动间隔,直到空闲线程死亡
  • 饱和/拒绝的政策
    • 抛出异常(AbortPolicy)、丢弃任务(DiscardPolicy)or由提交任务的线程自行执行该任务(CallerRunsPolicy

使用Multiple Reactor Threads的模式(主从Reactor模式)

该模式采用了主Reactor和从Reactor,分别用于处理新建立的连接,IO读写事件/事件分发
  • 主 Reactor 可以解决同一时间大量新连接,将其注册到从 Reactor 上进行IO事件监听处理
  • IO事件监听相对新连接处理更加耗时,此处我们可以考虑使用线程池来处理。这样能充分利用多核 CPU 的特性,能使更多就绪的IO事件及时处理。
  • MainReactor 仅负责处理客户端连接的 Accept 事件,连接建立成功后将新创建的连接对象注册至 SubReactor
  • 由 SubReactor 分配线程池中的 I/O 线程与其连接绑定,它将负责连接生命周期内所有的 I/O 事件。

  • 使用Reactor线程池,用于匹配CPU和IO速率,
  • 采用静态或动态构造,每个Reactor都有自己的选择器、线程和调度循环
  • 主acceptor分配到其他reactors
notion image

Java.NIO的其他特性

  • 每个Reactor有多个Selectors
    • 将不同的处理程序绑定到不同的IO事件
    • 可能需要仔细的同步来协调
  • 文件转换
    • 自动从文件到网络or网络到文件的拷贝
  • 内存映射文件
    • 通过缓冲区访问文件
  • 直接缓冲区
    • 有时可以实现零拷贝传输,但是有设置和结束开销
    • 最适合具有长连接的应用程序

基于连接的扩展

  • 不仅仅是单体服务请求
    • 客户端连接
    • 客户端发送一系列消息/请求
    • 客户端断开连接
示例
  • 数据库和事务监控器
  • 多人游戏,聊天等
可以扩展基本的网络服务模式
  • 处理许多相对长连接的客户端
  • 跟踪客户端和会话状态(包括断开)
  • 在多个主机上分发服务

© Shansan 2021 - 2025