Scalable IO in Java
Doug Lea编写的一篇关于Java NIO以及Reactor模式的经典文章
理解该文章可以帮助我们更好的理解Netty网络编程的底层设计&运行思路
原文已经上传在线文档:【腾讯文档】Scalable_IO_in_Java
为了更好的理解本文章,请参阅我的列一篇文章:Java NIO:在网络编程中的基本范式
同时也可以关注我的相关专栏,谢谢大伙啦!!!
概要
-
Scalable network services(可伸缩的网络服务)
-
Event-driven processing(事件驱动处理)
-
Reactor pattern(反应器模式)
-
Basic version(单线程模式)
-
Multithreaded versions(多线程模式)
-
Other variants(其他变体)
-
-
Walkthrough of java.nio nonblocking IO APIs(NIO重要API)
Network Services
Doug Lea在本文中归纳了 web service、分布式对象(指代分布式设计)等都存在相似的基本构成:
- Read request(读请求,指代端对端发送数据)
- Decode request(对请求解码,比如将二进制数据解码为原始数据)
- Process service(处理服务,指代实际的业务处理)
- Encode reply(对返回数据编码,遵循相应的协议对)
- Send reply(返回响应到对端)
由于上述的每一步的本质不同,相对应的成本也是不同的。
Classic Service Designs(传统的服务设计)
图示很明显,传统的服务是一种非常“垂直”的设计:
每当一个客户端与服务端建立连接后,服务端就开启一条新的线程来供相关处理单元处理该客户端的业务请求
Classic ServerSocket Loop(传统服务端代码)
package com.leolee.netty.scalableIOInJava.classicServiceDesigns;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* @ClassName Server
* @Description: 传统IO服务设计
* @Author LeoLee
* @Date 2020/10/16
* @Version V1.0
**/
public class Server implements Runnable {
private static final int PORT = 8899;
private static final int MAX_INPUT = 1024;
@Override
public void run() {
try {
ServerSocket ss = new ServerSocket(PORT);
while (!Thread.interrupted()) {
new Thread(new Handler(ss.accept())).start();
}
// or, single-threaded, or a thread pool
} catch (IOException e) {
e.printStackTrace();
}
}
static class Handler implements Runnable {
final Socket socket;
Handler(Socket s) {
socket = s;
}
@Override
public void run() {
try {
byte[] input = new byte[MAX_INPUT];
socket.getInputStream().read(input);
byte[] output = process(input);
socket.getOutputStream().write(output);
} catch (IOException e) {
e.printStackTrace();
}
}
//实际业务处理并返回处理结果
private byte[] process(byte[] cmd) {
/* ... */
return null;
}
}
}
Doug Lea用这个示例简单明了的实现了传统服务设计:
- 当该服务启动之后,首先创建了一个 ServerSocket 等待网络中的请求
- 创建了一个死循环(当前进程不中断就不停止循环)来创建对应客户端请求的线程,并承载 handler
- 之后在 handler 中处理 [Read Decode Process Encode Send] 等操作
缺点:
这样的设计最严重的缺点就是会为每一个请求创建线程,高并发情况下,过去的线程被创建,严重影响服务器的性能,服务器所承载的最大线程数也是有上限的,CPU在线程间切换的开销也是极大的。同时 ServerSocket.accept() 方法是阻塞的,该方法将监听连接在此socket上的建立请求并建立socket连接,将阻塞到建立为止。
Scalability Goals(可伸缩性目标的达成)
Doug Lea提出了实现可伸缩性的基本要求:
- Graceful degradation under increasing load (more clients):高负载情况下的优雅降级
- Continuous improvement with increasing resources (CPU, memory, disk, bandwidth):硬件资源提升的情况下,服务的处理能力也能随之提升
- Also meet availability and performance goals(同时满足高可用和高性能)
- Short latencies(更短的延迟)
- Meeting peak demand(抗高峰期能力)
- Tunable quality of service(可调节的服务质量)
- Divide-and-conquer is usually the best approach for achieving any scalability goal(使用 "分而治之" 的理念来达成可伸缩性的目标)
Divide-and-conquer (怎样“分而治之”)
- Divide processing into small tasks,Each task performs an action without blocking:将业务处理过程分散在一些小的任务,每个任务执行一个非阻塞操作(拆分处理任务,实际就是拆分handler,保证处理任务都非阻塞的执行)
- Execute each task when it is enabled.Here, an IO event usually serves as trigger:在每个任务都可用时(空闲时)就去调度该可用任务处理其他请求。IO事件、IO操作通常都是作为一个触发器来服务的,也就是说当某个IO操作的 read 事件产生时,就会触发 read 任务,这是与传统IO垂直处理请求最大的不同之一
- Basic mechanisms supported in java.nio(NIO的基本机制)
- Non-blocking reads and writes
- Dispatch tasks associated with sensed IO events(分派与监听IO的事件相关的任务,也就是分发的任务在执行前已经和相关的IO产生了绑定,当某些IO事件产生之后,就会分发相应的任务)
- Endless variation possible
- A family of event-driven designs(还存在其他一系列变化的事件驱动的设计)
Event-driven Designs(事件驱动设计)
- Usually more efficient than alternatives(通常比其他方式更有效)
- Fewer resources(占用更少的资源)
- Don't usually need a thread per client:不需要为每个客户端创建一个线程
- Less overhead(更小的代价和开销)
- Less context switching, often less locking:更少的线程间上下文切换,同时使用更少的阻塞方法
- But dispatching can be slower(分发可能会变得更慢)
- Must manually bind actions to events:必须要手动的将动作绑定到事件上
- Fewer resources(占用更少的资源)
- Usually harder to program(事件驱动的设计会使编码更加困难一些)
- Must break up into simple non-blocking actions(必须分解为简单的非阻塞动作)
- Similar to GUI event-driven actions
- Cannot eliminate all blocking: GC, page faults, etc(也并不是能消除所有阻塞的情况如:GC, page faults等)
- Must keep track of logical state of service(必须跟踪服务的逻辑状态:因为当所有操作都是异步的时候,所以要持续的跟进之前异步操作的执行结果,如 Future 的使用)
- Must break up into simple non-blocking actions(必须分解为简单的非阻塞动作)
Background: Events in AWT(AWT:Java图像化工具)
Reactor Pattern(响应器模式)
reactor模式主要的应用场景就是在网络编程上
-
Reactor responds to IO events by dispatching the appropriate handler(Reactor通过分发恰当的处理器来响应IO事件)
-
Similar to AWT thread
-
-
Handlers perform non-blocking actions(处理器执行非阻塞操作)
-
Similar to AWT ActionListeners
-
-
Manage by binding handlers to events(通过将处理器绑定到事件来管理)
-
Similar to AWT addActionListener
-
-
See Schmidt et al, Pattern-OrientedSoftwareArchitecture,Volume2(POSA2)
-
Also Richard Stevens's networking books, Matt Welsh's SEDA framework, etc
-
Basic Reactor Design(单线程版本)
实际上 Reactor 就是个 EventLoop(事件循环组),Netty 中的 EventLoopGroup 就是 Reactor的具体实现
NIO Support
NIO就不介绍了,看这篇文章的大伙应该都懂的
- Channels
- Connections to files, sockets etc that support non-blocking reads
- Buffers
- Array-like objects that can be directly read or written by Channels
- Selectors
- Tell which of a set of Channels have IO events
- SelectionKeys
- Maintain IO event status and bindings(维持IO事件状态和事件绑定关系)
Doug Lea 对 Basic Reactor Design 的具体实现
Reactor:
package com.leolee.netty.scalableIOInJava.classicServiceDesigns;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
/**
* @ClassName Reactor
* @Description: 响应器(也就是事件循环组)
* @Author LeoLee
* @Date 2020/10/16
* @Version V1.0
**/
public class Reactor implements Runnable {
//----------------------------------------------------------------
//------------------------------初始化-----------------------------
//----------------------------------------------------------------
final Selector selector;
final ServerSocketChannel serverSocket;
Reactor(int port) throws IOException {
selector = Selector.open();
serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(port));
serverSocket.configureBlocking(false);
SelectionKey sk = serverSocket.register(selector, SelectionKey.OP_ACCEPT);
sk.attach(new Acceptor());
}
//也可以用下面的方法代替上面构造器中的方法
/* Alternatively, use explicit SPI provider:
SelectorProvider p = SelectorProvider.provider();
selector = p.openSelector();
serverSocket = p.openServerSocketChannel();
*/
//----------------------------------------------------------------
//--------------------------Dispatch Loop()---------------------
//----------------------------------------------------------------
@Override
public void run() {// 通常这是在一个新的线程中执行的
while (!Thread.interrupted()) {
try {
selector.select();
Set selected = selector.selectedKeys();
Iterator it = selected.iterator();
while (it.hasNext()) {
//开始根据每一个selectedKeys派发
dispatch((SelectionKey) (it.next()));
}
selected.clear();
} catch (IOException e) {
e.printStackTrace();
}
}
}
void dispatch(SelectionKey k) {
Runnable r = (Runnable) (k.attachment());
if (r != null) {
r.run();
}
}
//----------------------------------------------------------------
//--------------------------Acceptor------------------------------
//----------------------------------------------------------------
/**
* @ClassName Acceptor
* @Description: 请求接收器
* @Author LeoLee
* @Date 2020/10/16
* @Version V1.0
**/
public class Acceptor implements Runnable {
@Override
public void run() {
try {
SocketChannel c = serverSocket.accept();
if (c != null) {
new Handler(selector, c);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Handler:
package com.leolee.netty.scalableIOInJava.classicServiceDesigns;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
/**
* @ClassName Handler
* @Description: 处理器
* @Author LeoLee
* @Date 2020/10/16
* @Version V1.0
**/
public class Handler implements Runnable {
//----------------------------------------------------------------
//--------------------------Handler setup-------------------------
//----------------------------------------------------------------
final SocketChannel socket;
final SelectionKey sk;
static int MAXIN, MAXOUT = 1024;
ByteBuffer input = ByteBuffer.allocate(MAXIN);
ByteBuffer output = ByteBuffer.allocate(MAXOUT);
static final int READING = 0, SENDING = 1;
int state = READING;
Handler(Selector sel, SocketChannel c) throws IOException {
socket = c;
c.configureBlocking(false);
//Optionally try first read now(可选择立刻开始首次读取数据)
sk = socket.register(sel, 0);
sk.attach(this);
sk.interestOps(SelectionKey.OP_READ);
sel.wakeup();
}
boolean inputIsComplete() {
/* ... */
return false;
}
boolean outputIsComplete() {
/* ... */
return false;
}
void process() {
/* ... */
}
//----------------------------------------------------------------
//--------------------------Request handling----------------------
//----------------------------------------------------------------
@Override
public void run() {
try {
if (state == READING) {
read();
} else if (state == SENDING) {
send();
}
} catch (IOException e) {
e.printStackTrace();
}
}
void read() throws IOException {
socket.read(input);
if (inputIsComplete()) {
process();
state = SENDING;
// Normally also do first write now
sk.interestOps(SelectionKey.OP_WRITE);
}
}
void send() throws IOException {
socket.write(output);
if (outputIsComplete()) {
sk.cancel();
}
}
}
上面的示例的也可以修改为下列模式
Per-State Handlers:A simple use of GoF State-Object pattern(GoF状态对象模式的简单使用)
建议先去看一下该设计模式再回来理解该模式在此处的意义
将适当的handler重新绑定为附件:
class Handler { // ...
public void run() { // initial state is reader socket.read(input);
if (inputIsComplete()) {
process();
sk.attach(new Sender());
sk.interest(SelectionKey.OP_WRITE);
sk.selector().wakeup();
}
}
class Sender implements Runnable {
public void run(){ // ...
socket.write(output);
if (outputIsComplete())
sk.cancel();
}
}
}
Multithreaded Designs(多线程版本)
- Strategically add threads for scalability(策略性的为实现可伸缩性增加线程数)
- Mainly applicable to multiprocessors(主要适用于多核心处理器)
- Worker Threads(对应Netty当中创建的 workerGroup 或者 childGroup,EventLoopGroup 对象)
- Reactors should quickly trigger handlers(多线程版本中的多个Reactor应该快速的去触发handler的执行)
- Handler processing slows down Reactor(Handler的执行时长会影响Reactor的分发执行速度,因为Reactor的分发任务执行是相对简单,不需要太多处理时间,Handler处理业务是相对繁重的)
- Offload non-IO processing to other threads(将非IO处理移交给其他线程完成)
- Reactors should quickly trigger handlers(多线程版本中的多个Reactor应该快速的去触发handler的执行)
- Multiple Reactor Threads
- Reactor threads can saturate doing IO(Reactor所属线程可饱和的处理IO任务)
- Distribute load to other reactors(分散加载压力到其他Reactor。我是这么理解的,不知道对不对)
- Load-balance to match CPU and IO rates(使用负载均衡来匹配CPU和IO的比率。因为CPU和IO的执行速度是差的很多的,不当的CPU和IO处理任务的匹配,将造成CPU大量空闲)
Worker Threads
- Offload non-IO processing to speed up Reactor thread(卸载非IO处理以提升Reactor线程的执行速度)
- Similar to POSA2 Proactor designs
- Simpler than reworking compute-bound processing into event-driven form(相比于将计算绑定处理重新转换为事件驱动的形式更简单)
- Should still be pure nonblocking computation(应当始终处于非阻塞的计算形式)
- Enough processing to outweigh overhead(相对于执行过载,提供足够的处理能力)
- Should still be pure nonblocking computation(应当始终处于非阻塞的计算形式)
- But harder to overlap processing with IO(很难将处理和IO重叠。我也不知道怎么理解这句话)
- Best when can first read all input into a buffer(最好是首先读取所有输入数据到buffer。一旦数据到了buffer中,就可以通过操作系统提供的零拷贝能力做处理)
- Use thread pool so can tune and control(使用线程池来做调节和控制)
-
Normally need many fewer threads than clients(使用线程池可以用更少的线程来应对更多的客户端)
-
Worker Thread Pools
Handler with Thread Pool(使用线程池来编写handler)
改造之前单线程版本的Handler如下:
package com.leolee.netty.scalableIOInJava;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @ClassName HandlerWithThreadPool
* @Description: 使用线程池来编写受控且可调节的处理器
* @Author LeoLee
* @Date 2020/10/17
* @Version V1.0
**/
public class HandlerWithThreadPool implements Runnable {
// uses util.concurrent thread pool
// static PooledExecutor pool = new PooledExecutor(...);
static ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
static final int PROCESSING = 3;
// ...
final SocketChannel socket;
final SelectionKey sk;
static int MAXIN, MAXOUT = 1024;
ByteBuffer input = ByteBuffer.allocate(MAXIN);
ByteBuffer output = ByteBuffer.allocate(MAXOUT);
static final int READING = 0, SENDING = 1;
int state = READING;
HandlerWithThreadPool(Selector sel, SocketChannel c) throws IOException {
socket = c;
c.configureBlocking(false);
//Optionally try first read now(可选择立刻开始首次读取数据)
sk = socket.register(sel, 0);
sk.attach(this);
sk.interestOps(SelectionKey.OP_READ);
sel.wakeup();
}
boolean inputIsComplete() {
/* ... */
return false;
}
boolean outputIsComplete() {
/* ... */
return false;
}
void process() {
/* ... */
}
synchronized void read() {
// ...
try {
socket.read(input);
if (inputIsComplete()) {
state = PROCESSING;
cachedThreadPool.execute(new Processer());
}
} catch (IOException e) {
e.printStackTrace();
}
}
synchronized void send2() {
cachedThreadPool.execute(new WriteProcesser());
}
void send() throws IOException {
socket.write(output);
if (outputIsComplete()) {
sk.cancel();
}
}
synchronized void processAndHandOff() {
process();
state = SENDING; // or rebind attachment
sk.interestOps(SelectionKey.OP_WRITE);
}
class Processer implements Runnable {
public void run() {
processAndHandOff();
}
}
class WriteProcesser implements Runnable {
@Override
public void run() {
try {
send();
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
if (state == READING) {
read();
} else if (state == SENDING) {
send2();
}
}
}
Coordinating Tasks(任务调度)
- Handoffs
- Each task enables, triggers, or calls next one(每一个任务都可以启动、触发或者调用下一个任务)
- Usually fastest but can be brittle(这是一种最快的处理方式,但是同时也是脆弱不健壮的方式)
- Callbacks to per-handler dispatcher(针对于每一个处理器分发者的回调)
- Sets state, attachment, etc
- A variant of GoF Mediator pattern
- Queues
- For example, passing buffers across stages(在每一个阶段中传递buffer)
- Futures
- When each task produces a result
- Coordination layered on top of join or wait/notify
Using PooledExecutor(线程池的优势)
- A tunable worker thread pool(一个可调节的worker线程池,相当于Netty的 worker<EventLoopGroup>)
- Main method execute(Runnable r)
- Controls for:
- The kind of task queue (any Channel)
- Maximum number of threads
- Minimum number of threads
- "Warm" versus on-demand threads
- Keep-alive interval until idle threads die
- to be later replaced by new ones if necessary
- Saturation policy
- block, drop, producer-runs, etc
Multiple Reactor Threads(多个Reactor线程)
- Using Reactor Pools
- Use to match CPU and IO rates(匹配CPU和IO处理的比例)
- Static or dynamic construction(动态静态构建)
- Each with own Selector, Thread, dispatch loop(每个Reactor拥有自己的Selector, Thread, dispatch loop)
- Main acceptor distributes to other reactors(主接收器来构建其他Reactor)
创建MainReactor类(MainReactor类似于Netty中的bossGroup或者是parentGroup,subReactor类似于workerGroup或者是childGroup),Acceptor部分如下
需要注意的是这里使用多个 Selector来管理不同的Channel,接收器将特定的Selector传递给Handler处理
Selector[] selectors; // also create threads
int next = 0;
class Acceptor { // ...
public synchronized void run() {
Socket connection = serverSocket.accept();
if (connection != null) new Handler(selectors[next], connection);
if (++next == selectors.length) next = 0;
}
}
Using other java.nio features
这一部分就不仔细分析了,根据实际情况的不同,使用不同的处理方式
- Multiple Selectors per Reactor
- To bind different handlers to different IO events
- May need careful synchronization to coordinate
- File transfer
- Automated file-to-net or net-to-file copying
- Memory-mapped files
- Access files via buffers
- Direct buffers
- Can sometimes achieve zero-copy transfer
- but have setup and finalization overhead
- Best for applications with long-lived connections
Connection-Based Extensions(基于连接的拓展)
- Instead of a single service request(相对于单个请求的服务)
- Client connects
- Client sends a series of messages/requests(客户端发送一系列的请求)
- Client disconnects
- Examples
- Databases and Transaction monitors
- Multi-participant games, chat, etc
- Can extend basic network service patterns(拓展基本的网络服务模式)
- Handle many relatively long-lived clients(保持很多长连接的客户端)
- Track client and session state (including drops) (跟中客户端会话状态)
- Distribute services across multiple hosts