RPC 线程模型
1.1 BIO 线程模型
在 JDK 1.4 推出 Java NIO 之前,基于 Java 的所有 Socket 通信都采用了同步阻塞模式(BIO),这种一请求一应答的通信模型简化了上层的应用开发,但是在性能和可靠性方面却存在着巨大的瓶颈。
因此,在很长一段时间里,大型的应用服务器都采用 C 或者 C++ 语言开发,因为它们可以直接使用操作系统提供的异步 I/O 或者 AIO 能力。
当并发访问量增大、响应时间延迟增大之后,采用 Java BIO 开发的服务端软件只有通过硬件的不断扩容来满足高并发和低时延。
它极大地增加了企业的成本,并且随着集群规模的不断膨胀,系统的可维护性也面临巨大的挑战,只能通过采购性能更高的硬件服务器来解决问题,这会导致恶性循环。
传统采用 BIO 的 Java Web 服务器如下所示(典型的如 Tomcat 的 BIO 模式):
采用该线程模型的服务器调度特点如下:
服务端监听线程 Acceptor 负责客户端连接的接入,每当有新的客户端接入,就会创建一个新的 I/O 线程负责处理 Socket;
客户端请求消息的读取和应答的发送,都有 I/O 线程负责;
除了 I/O 读写操作,默认情况下业务的逻辑处理,例如 DB 操作等,也都在 I/O 线程处理;
I/O 操作采用同步阻塞操作,读写没有完成,I/O 线程会同步阻塞。
BIO 线程模型主要存在如下三个问题:
性能问题:一连接一线程模型导致服务端的并发接入数和系统吞吐量受到极大限制;
可靠性问题:由于 I/O 操作采用同步阻塞模式,当网络拥塞或者通信对端处理缓慢会导致 I/O 线程被挂住,阻塞时间无法预测;
可维护性问题:I/O 线程数无法有效控制、资源无法有效共享(多线程并发问题),系统可维护性差。
为了解决同步阻塞 I/O 面临的一个链路需要一个线程处理的问题,通常会对它的线程模型进行优化,后端通过一个线程池来处理多个客户端的请求接入,形成客户端个数 "M" 与线程池最大线程数 "N" 的比例关系,其中 M 可以远远大于 N,通过线程池可以灵活的调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽,它的工作原理如下所示:
优化之后的 BIO 模型采用了线程池实现,因此避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题。但是由于它底层的通信依然采用同步阻塞模型,阻塞的时间取决于对方 I/O 线程的处理速度和网络 I/O 的传输速度。
本质上来讲,无法保证生产环境的网络状况和对端的应用程序能足够快,如果应用程序依赖对方的处理速度,它的可靠性就非常差,优化之后的 BIO 线程模型仍然无法从根本上解决性能线性扩展问题。
1.2 异步非阻塞线程模型
从 JDK1.0 到 JDK1.3,Java 的 I/O 类库都非常原始,很多 UNIX 网络编程中的概念或者接口在 I/O 类库中都没有体现,例如 Pipe、Channel、Buffer 和 Selector 等。2002 年发布 JDK1.4 时,NIO 以 JSR-51 的身份正式随 JDK 发布。它新增了个 java.nio 包,提供了很多进行异步 I/O 开发的 API 和类库,主要的类和接口如下: