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

- 对于每一个连接到Server的Client,服务器都会为其单独开启一个线程,执行handler
如图所示,每一个处理程序(handler)都在对应的线程中启动,经典的Server端Socket服务代码如下:
可伸缩性目标(Scalability Goals)
- 能够在负载增加的情况下优雅降级
- 能够随着资源的增加而不断改进
- 满足可用性和性能目标
- 低延时
- 满足高峰需求(高吞吐量)
- 可调的服务质量
分治(Divide and Conquer)通常是实现任何可伸缩性目标的最佳方法
分治设计(Divide and Conquer)
网络服务的分治设计如下:
- 将处理过程分成若干任务
- 每个任务执行一个行为且不阻塞
- 在启用时执行每个任务
- IO事件通常用作触发器,如handler

java.nio支持的基本机制如下:
- 非阻塞的读和写
- 根据检测到的IO事件分发相关联的任务
通过提供一系列事件驱动的设计(Event-driven Designs),能够给网络服务的设计带来无限的可能
事件驱动设计(Event-driven Designs)
事件驱动通常是性能更好的一种设计,原因如下:
- 更少的资源:不需要为每一个client创建线程
- 更少的开销:更少的上下文切换和加锁
此外,事件驱动还有一些缺点:
- 更慢的调度:由于必须手动将操作绑定到事件上,事件驱动的调度通常会更慢
- 编程难度更大:类似于GUI事件驱动的操作
- 必须分解成简单的非阻塞动作
- 但并不是所有阻塞都能消除,如GC, page faults等
- 必须跟踪服务的逻辑状态
AWT中事件驱动设计
如图所示,是AWT中的事件驱动设计,事件驱动的IO使用了类似的思想但不同的设计

AWT是 Java 的一个图形用户界面(GUI)工具包
- AWT使用事件监听器和事件处理程序来处理用户交互
- 当发生点击事件时,该事件会进入AWT事件序列
- AWT线程负责从事件队列中取出事件,并将它们分发给相应的事件监听器进行处理。
Reactor 模式(Reactor Pattern)
Reactor(反应器)模式是高性能网络编程在设计和架构层面的基础模式
- Reactor会调度合适的handler来应答IO事件(类似于AWT 线程)
- Handler执行非阻塞操作 (类似于AWT的ActionListener)
- 通过将handler绑定到事件进行管理(类似于AWT的addActionListener)
基本的Reactor模式设计(Basic Reactor Design)
单线程版本

- 如图所示,在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 线程池)来处理业务操作

需要注意的问题如下:
- 任务之间的协调:
- 每个任务的启用、触发或调用下一个任务
- 速度很快,不容易控制
- 对每个处理程序调度程序的回调:
- 设置状态、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

Java.NIO的其他特性
- 每个Reactor有多个Selectors
- 将不同的处理程序绑定到不同的IO事件
- 可能需要仔细的同步来协调
- 文件转换
- 自动从文件到网络or网络到文件的拷贝
- 内存映射文件
- 通过缓冲区访问文件
- 直接缓冲区
- 有时可以实现零拷贝传输,但是有设置和结束开销
- 最适合具有长连接的应用程序
基于连接的扩展
- 不仅仅是单体服务请求
- 客户端连接
- 客户端发送一系列消息/请求
- 客户端断开连接
示例
- 数据库和事务监控器
- 多人游戏,聊天等
可以扩展基本的网络服务模式
- 处理许多相对长连接的客户端
- 跟踪客户端和会话状态(包括断开)
- 在多个主机上分发服务