Reactor 模式的 Java 实现(feat. Scalable IO in Java - Doug Lea)

原文地址:http://hscarb.github.io/java/20240827-reactor-java.html

Reactor 模式的 Java 实现(feat. Scalable IO in Java - Doug Lea)

1. 背景

Doug Lea 在 Scalable IO in Java 的 PPT 中描述了 Reactor 编程模型的思想,大部分 NIO 框架和一些中间件的 NIO 编程都与它一样或是它的变体,包括 Netty。


1.1 Reactor 模式是什么

内核空间的网络数据收发模型:阻塞 IO(BIO)、非阻塞 IO(NIO)、IO 多路复用、信号驱动 IO、异步 IO。

而 Reactor 模式是对用户空间的 IO 线程模型进行分工的模式,它基于 IO 多路复用来实现。

1.2 本文内容

本文将介绍 Reactor 编程模型使用 Java NIO 包的三种实现,并提供对应的源码实现和解释。

我会实现一个简单的服务端逻辑:以换行符来识别每次用户输入,将每次用户输入的字符都转成大写,返回给用户。

本文的代码完整实现地址:https://github.com/HScarb/reactor

2. 传统服务端设计模式(BIO)

一般的 Web 服务端或分布式服务端等应用中,大都具备这些处理流程:读请求(send)、解码(decode)、处理和计算(compute)、编码(encode)、发送响应(send)。

在传统服务端设计中,对每个新的客户端连接都启动一个新的线程去处理,在每个线程中串行执行上述处理流程。这种编程方式也就是 BIO。

2.1 BIO 服务端

public class BioServer implements Runnable {
    public int port;

    public BioServer(int port) {
        this.port = port;
    }

    @Override
    public void run() {
        try (final ServerSocket serverSocket = new ServerSocket(port)) {
            System.out.println("Server is listening on port " + port);
            while (!Thread.interrupted()) {
                try {
                    // 当有新的客户端连接时,accept() 方法会返回一个Socket对象,表示与客户端的连接
                    // 创建一个新的线程来处理该连接
                    new Thread(new BioHandler(serverSocket.accept())).start();
                } catch (IOException e) {
                    System.out.println("Error handling client: " + e.getMessage());
                }
            }
        } catch (IOException e) {
            System.out.println("Server exception: " + e.getMessage());
        }
    }
}

上述代码中,为每个客户端连接都创建一个 Handler 线程,在 Handler 中处理读请求、解码、处理和计算、编码、发送响应的所有逻辑。

2.2 BIO Handler

/**
 * 处理单个客户端连接的具体逻辑
 */
public class BioHandler implements Runnable {

    public Socket socket;

    public BioHandler(Socket socket) {
        this.socket = socket;
    }

    @Override
    public void run() {
        System.out.println("New client connected");
        try (
            final BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            final PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
        ) {
            writer.print("bio> ");
            writer.flush();
            String input;
            // 读取客户端输入的一行内容
            while ((input = reader.readLine()) != null) {
                // 处理客户端输入的内容
                final String output = process(input);
                // 将处理后的内容写回给客户端
                writer.println(output);
                writer.print("bio> ");
                writer.flush();
            }
        } catch (IOException e) {
            System.out.println("Error handling io: " + e.getMessage());
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                System.out.println("Failed to close socket: " + e.getMessage());
            }
        }
    }

    /**
     * 将客户端输入的内容转换为大写
     */
    private String process(String requestContent) {
        return requestContent.toUpperCase(Locale.ROOT);
    }
}

启动这个服务端程序

public class Main {

    public static final int PORT = 8080;

    public static void main(String[] args) throws IOException {
        runBioServer();
    }

    public static void runBioServer() {
        final BioServer bioServer = new BioServer(PORT);

        ExecutorService mainThread = Executors.newSingleThreadExecutor();
        mainThread.submit(bioServer);
        mainThread.shutdown();
    }
}

2.3 缺陷

上述程序存在缺陷:

  1. 线程资源消耗高:每个客户端连接都会创建一个线程,在高并发场景下会导致大量线程创建和销毁,消耗大量系统资源。线程上下文切换开销也会随之增加。
  2. 阻塞式 I/Oaccept()readLine()print()方法都是阻塞式的,这意味着线程在等待I/O操作完成时会被阻塞,无法执行其他任务。这样会导致资源利用率低下。
  3. 难于管理和扩展:直接使用new Thread()的方式来处理连接,难以进行线程管理和池化,难以实现更复杂的并发控制和优化。

3. 优化思路

随着互联网的发展,对服务性能的挑战也越来越大。我们希望能构建更高性能且可伸缩的服务,能够达到:

  1. 随着客户端数量的增加而优雅降级
  2. 随着硬件资源的增加,性能持续提高
  3. 具备低延迟、高吞吐、高质量的服务

3.1 分而治之

要达到以上目标,我们先考虑将处理过程拆分成更小的任务,每个任务执行一个非阻塞操作,由一个 IO 事件来触发执行。

java.nio 包对这种机制提供了支持:

  • 非阻塞的读和写
  • 通过感知 IO 事件来分发 IO 事件关联的任务

BIO 线程是以 read->decode->process->encode->send 的顺序串行处理,NIO 将其分成了三个执行单元:读取、业务处理和发送,处理过程如下:

  • 读取(read):如果无数据可读,线程返回线程池;发生读 IO 事件,申请一个线程处理读取,读取结束后处理业务
  • 业务处理(decode、compute、encode):线程同步处理完业务后,生成响应内容并编码,返回线程池
  • 发送(send):发生写 IO 事件,申请一个线程进行发送

与 BIO 明显的区别就是,一次请求的处理过程是由多个不同的线程完成的,感觉和指令的串行执行并行执行有点类似。

分而治之的关键在于非阻塞,这样就能充分利用线程,压榨 CPU,提高系统的吞吐能力。

3.2 事件驱动

另一个优化思路是基于事件启动,它比其他模型更高效。

  • 使用的资源更少:不用为每个客户端都启动一个线程
  • 开销更少:减少上下文切换,锁的使用也更少
  • 任务分发可能会更慢:必须手动绑定事件和动作

事件驱动架构的服务实现复杂度也更高,必须将处理过程拆分成多个非阻塞的动作,且持续跟踪服务的逻辑状态。并且事件启动无法避免所有的阻塞,比如 CG、缺页中断等。

4. 前置知识:Java NIO 包

上面提

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值