本文翻译自 Jakob Jenkov 的 Java NIO: Non-blocking Server ,原文地址:
文中所有想法均来自原作者,学习之余,觉得很不错,对以后深入学习服务器有帮助,故翻译之,有错误还望指教
即使了解 NIO 非阻塞功能如何工作(Selector,Channel,Buffer等),设计非阻塞服务器仍然很难。 与阻塞 IO 相比,非阻塞 IO 包含若干挑战。 本文将讨论非阻塞服务器的主要挑战,并为描述一些可能的解决方案。
找到有关设计非阻塞服务器的好资料很难。 因此,本文中提供的解决方案基于 Jakob Jenkov 的工作和想法。
本文中描述的想法是围绕 Java NIO 设计的。 但是,我相信这些想法可以在其他语言中重复使用,只要它们具有某种类似 Selector 的结构。 据我所知,这些构造是由底层操作系统提供的。
非阻塞 IO 管道非阻塞 IO 管道是指处理非阻塞 IO 的一系列组件,包括以非阻塞方式读写 IO ,以下是简化的非阻塞IO管道的说明:
组件使用选择器来监听通道何时有可读数据。 然后组件读取输入数据并根据输入生成一些输出。 输出再次写入通道。
非阻塞 IO 管道不需要同时读写数据。 某些管道可能只读取数据,而某些管道可能只能写入数据。
上图仅显示单个组件。 非阻塞 IO 管道可能有多个组件处理传入数据。 非阻塞IO管道的长度取决于管道需要做什么。
非阻塞 IO 管道也可以同时从多个通道读取。 例如,从多个 SocketChannel 读取数据。
上图中的控制流程是已简化的。 它是通过 Selector 启动从 Channel 读取数据的组件。 不是 Channel 将数据推入 Selector 并从那里推入组件,即使这是上图所示。
非阻塞与阻塞 IO 管道非阻塞和阻塞 IO 管道之间的最大区别在于如何从底层通道(套接字或文件)读取数据。
IO 管道通常从某些流(来自套接字或文件)读取数据,并将该数据拆分为相干消息。 这类似于将数据流分解为令牌以使用令牌解析器进行解析。 将流分解为消息的组件叫做消息读取器(Message Reader)。 以下是将消息流分解为消息的消息读取器(Message Reader)的示意图:
阻塞 IO 管道,是使用类似于 InputStream 的接口,每次从底层 Channel 读取一个字节,并且阻塞,直到有数据可读取。 这就是阻塞 Message Reader 的实现。
使用阻塞 IO 接口流可以简化 Message Reader 的实现。 阻塞 Message Reader 不必处理从流中读取数据,但是没有数据可读的情况,或者只读取了部分消息,以及稍后回复读取消息的情况。
类似地,阻塞 Message Writer(将消息写入流的组件)也不必处理只写入部分消息的情况,以及稍后必须恢复消息写入的情况。
阻止 IO 管道的缺陷虽然阻塞的 Message Reader 更容易实现,但它有一个很大的缺点,就是需要为每个需要拆分成消息的流提供一个单独的线程,因为每个流的 IO 接口都会阻塞,直到有一些数据要从中读取。 这意味着单个线程无法胜任从一个流读取,如果没有数据,则从另一个流读取这种任务。 一旦线程尝试从流中读取数据,线程就会阻塞,直到实际上有一些数据要读取。
如果 IO 管道是必须处理大量并发连接的服务器的一部分,则服务器将需要每个活动进入连接一个线程,但是,如果服务器具有数百万个并发连接,则这种类型的设计不能很好地扩展。 每个线程将为其堆栈提供 320K(32位JVM)和 1024K(64位JVM)内存。 因此,100*10000 线程将占用 1 TB 内存!
为了减少线程数量,许多服务器使用一种设计,让服务器保留一个线程池(例如 100),该线程池一次一个地从入站连接(inbound connections)读取消息。 入站连接保留在队列中,并且线程按入站连接放入队列的顺序处理来自每个入站连接的消息。 这个设计如下图示:
但是,此设计要求入站连接合理地发送数据。 如果已连接的入站连接在较长时间内处于非活动状态,则大量非活动连接可能会阻塞(占用)线程池中的所有线程。 这意味着服务器响应缓慢甚至无响应。
某些服务器设计试图通过在线程池中的线程数量具有一定弹性来缓解此问题。 例如,如果线程池用完线程,则线程池可能会启动更多线程来处理负载。 此解决方案意味着需要更多数量的长时间连接才能使服务器无响应。 但请记住,运行的线程数仍然存在上限。 因此,这不会解决上述有 100*10000 线程的问题。
基础非阻塞 IO 管道设计