《Scalable IO in Java》翻译版

276 阅读6分钟

定义

《Scalable IO in Java》 是Doug Lea关于分析与构建可伸缩的高性能IO服务的经典文章。 原文地址: gee.cs.oswego.edu/dl/cpjslide…

目录

  • 可扩展的网络服务

  • 事件驱动处理

  • 反应堆模式

    • 基本版本
    • 多线程版本
    • 其他变体
  • java.nio非阻塞IO API演示

网络服务

  • Web服务、分布式对象等大多具有相同的基本结构:

    • 读取请求
    • 解码请求
    • 处理服务
    • 编码回复
    • 发送回复。
  • 但每个步骤的成本不同,包括XML解析、文件传输、网页生成和计算服务等。

经典的ServerSocket循环

client-server.png

  • 每个处理程序可以在自己的线程中启动。

class Server implements Runnable {
    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 ex) { 
        }
    }

    static class Handler implements Runnable {
        final Socket socket;

        Handler(Socket s) {
            socket = s;
        }

        public void run() {
            try {
                byte[] input = new byte[MAX_INPUT];
                socket.getInputStream().read(input);

                byte[] output = process(input);
                socket.getOutputStream().write(output);
            } catch (IOException ex) { 
            }
        }

        private byte[] process(byte[] cmd) { 
        }
    }
}

可扩展性目标

  • 在负载增加(更多客户端)的情况下优雅降级

  • 随着资源的增加(CPU、内存、磁盘、带宽)

  • 同时满足可用性和性能目标

    • 低延迟
    • 满足峰值需求
    • 可调整服务质量
  • "分而治之"通常是实现任何可扩展性目标的最佳方法

分而治之处理机制

  • 将处理过程分成小任务,每个任务以非阻塞的方式执行操作

  • 当启用每个任务时执行它。在这里,通常使用IO事件作为触发器。

  • Java.nio支持的基本机制

    • 非阻塞读写,
    • 感知IO事件分派相关的任务执行。
  • 结合事件驱动设计,可以有更多的变化.

事件驱动设计

  • 事件驱动通常比其他选择更有效率。

    • 更少的资源

      • 通常不需要为每个客户端创建一个线程
    • 更少的开销

      • 减少的上下文切换,通常意味着需要更少的锁
    • 分派可能会更慢。

      • 必须手动将操作绑定到事件
  • 通常更难编程

    • 必须分解成简单的非阻塞动作。

      • 类似于图形用户界面事件驱动的操作
      • 无法消除所有阻塞:GC、页面错误等。
  • 必须跟踪服务的逻辑状态

背景:AWT中的事件

awt.png

  • 事件驱动的IO使用类似的思想,但采用不同的设计。

反应器模式

  • 反应堆通过分派处理程序来响应IO事件,类似于 AWT 线程
  • 处理程序执行非阻塞操作, 类似于 AWT 的 ActionListeners
  • 通过将处理程序绑定到事件来管理, 类似于 AWT addActionListener

基本反应堆设计

single-thread.png

  • 单线程版本

java.nio 支持

  • 通道----支持非阻塞读取的文件、套接字等连接。
  • 缓存区---类似数组的对象,可以直接被通道读取或写入
  • 选择器---告诉我哪些通道有IO事件。
  • 选择键集合---负责IO事件状态和绑定

反应堆1:设置

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());
    }
}

反应堆2:循环调度

public void run() { //通常在一个新的线程中
    try {
        while (!Thread.interrupted()) {
            selector.select();
            Set selected = selector.selectedKeys();
            Iterator it = selected.iterator();
            while (it.hasNext())
                dispatch((SelectionKey)(it.next());
            selected.clear();
        }
    } catch (IOException ex) { 

     }
}
    
void dispatch(SelectionKey k) {
    Runnable r = (Runnable)(k.attachment());
    if (r != null)
        r.run();
}

反应堆3:接受者

class Acceptor implements Runnable { // inner
    public void run() {
        try {
            SocketChannel c = serverSocket.accept();

            if (c != null) {
                new Handler(selector, c);
            }
        } catch (IOException ex) { /* ... */
        }
    }
}

single-thread.png

反应堆4:处理程序设置

final class Handler implements Runnable {
    static final int READING = 0;
    static final int SENDING = 1;
    final SocketChannel socket;
    final SelectionKey sk;
    ByteBuffer input = ByteBuffer.allocate(MAXIN);
    ByteBuffer output = ByteBuffer.allocate(MAXOUT);
    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() { /* ... */ }
    boolean outputIsComplete() { /* ... */ }
    void process() { /* ... */ }
}

反应堆5:请求处理

public void run() {
    try {
        if (state == READING)read();
        else if (state == SENDING) send();
    } catch (IOException ex) { /* ... */ }
}

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 处理程序

  • 使用状态模式(GoF)进行优化,不需要再进行状态的判断
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();
        }
    }
}

多线程设计

  • 为了提升可扩展性增加加线程,主要适用于多处理器

  • 工作线程

    • 反应器应该快速触发处理程序,处理程序的处理会减慢反应器的速度,将非IO处理卸载到其他线程上
  • 多个反应器线程

    • 反应器线程可能会因为IO操作而饱和,
    • 将负载分配到其他反应器上
    • "负载均衡以匹配CPU和IO速率

工作者线程

  • 将非I/O处理卸载以加速反应器线程

  • “比重新设计计算绑定处理为事件驱动形式更简单

    • 应该仍然是纯非阻塞计算,“足够的处理能够超过开销,“但是与IO重叠处理更加困难
  • 最好的方法是先将所有输入读入缓冲区

  • “使用线程池以进行调整和控制

  • 通常需要的线程比客户端少得多

mutli-thread.png

使用线程池处理

class Handler implements Runnable {
    // uses util.concurrent thread pool
    static PooledExecutor pool = new PooledExecutor(...);
    static final int PROCESSING = 3;
    // ...
    synchronized void read() { // ...
        socket.read(input);
        if (inputIsComplete()) {
            state = PROCESSING;
            pool.execute(new Processer());
        }
    }
    synchronized void processAndHandOff() {
        process();
        state = SENDING; // or rebind attachment
        sk.interest(SelectionKey.OP_WRITE);
    }
    class Processer implements Runnable {
        public void run() { processAndHandOff(); }
    }
}

协调任务

  • 任务间交互

    • 每个任务都会启用、触发或调用下一个任务,传递通常是最快的,但难以控制
  • 每个处理器中分发器的回调设置状态,返回值等(中介者模式的变体)

  • 不同线程的缓冲区问题

  • 需要返回值时,线程需要通过join,wait/notify等方法进行协调s

使用 PooledExecutor

  • 可调节的工作线程池

  • 主方法执行(Runnable r)

  • 控制项:

    • 任务队列类型
    • 最大线程数
    • 最小线程数
    • 按需分配线程
    • 保持活动状态的时间间隔,直到空闲线程死亡
    • 饱和度策略

reactor线程的池化处理

  • 使用反应堆池,用于匹配CPU和IO速率

  • 每个reactor静态或动态构造

  • 在主接收器(acceptor)中分发给其他reactor.

    Selector[] selectors;
    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;
        }
    }
    

multi-reactor.png

使用其他的java.nio功能

  • 每个反应器多个选择器

    • 将不同的处理程序绑定到不同的IO事件可能需要仔细协调以进行同步。
  • 文件传输

    • 自动文件到网络或网络到文件的复制
  • 内存映射文件

    • 通过缓冲区访问文件
  • 直接缓冲区

基于连接的扩展

  • 不是单一的服务请求

    • 客户端连接
    • 客户端发送一系列消息/请求
    • 客户端断开连接
  • 范例

    • 数据库和事务监控器
    • 多参与者游戏、聊天等
  • 可以扩展基本网络服务模式

    • 处理许多相对长期的客户端
    • 跟踪客户端和会话状态(包括掉线)
    • 将服务分布在多个主机上