高性能WEB服务器――之Jetty (N)I/O组件
?
简介:Jetty是一个当前非常活跃的并且很有前景的Servlet引擎和WEB容器。本文所讨论的主题建立在Jetty 9版本之上,通过本文对Jetty I/O的组件结构的介绍,您将充分了解到Jetty是如何做到支持高并发网络请求的。
(一)先简单谈谈I/O
Jetty是一个WEB容器,所以本文所讨论的I/O主要是面向流的I/O,即:针对网络连接的I/O。
UNIX下的五种I/O模型,分别是:
(1)阻塞式I/O模型;
(2)非阻塞式I/O模型;
(3)I/O复用模型;
(4)信号驱动I/O;
(5)异步I/O;
根据POSIX的定义:同步I/O操作导致请求进程阻塞一直到I/O操作完成;而异步I/O不会导致请求进程的阻塞。根据上述定义,前四种模型都会阻塞请求进程的处理,所以都是同步I/O模型;只有最后一种满足POSIX的定义是异步I/O模型。
Jetty的IO组件采用的是那种I/O模型呢? Jetty基于NIO非阻塞的I/O复用模型来实现了IO组件。自JDK1.4 起 Java提供的NIO 1.0框架主要是基于I/O复用模型,从JDK1.7开始 Java NIO 2.0开始支持异步I/O。所以本文的真正目的是带领大家窥视Jetty对Java NIO I/O复用模型包装的实现细节。
?
(二)为什么是NIO而不是OIO
NIO其实不是神话:通常只有在需要支持大量链接(通常数百或者数以千计的链接)和高并发场景下,NIO才能带来可观的性能优势。对于一般的应用如果连接数和并发都不高的场景中NIO 不会带来很大性能上提高,反而使用OIO也许会更快,比如桌面应用。
Jetty作为WEB容器需要支持大量链接和在高并发的场景中,所以NIO在某种程度上是Jetty作为高性能服务器的有力支点。
NIO虽然屏蔽了平台差异性,但基于不同OS平台的IO系统NIO实现仍然是存在差异的,这使得我们的编程陷阱重重。其实这也是本文的另一个理由:让我们来了解下Jetty是如何来规避这些问题。
(三)Reactor与Proactor
在JDK1.7 之前,采用Reactor + Handler模式是大多数高性能服务器通讯框架的所采用的基础模型,如Mina和Netty:主要针对于非阻塞同步I/O;相反Proactor模式是非阻塞异步I/O通讯框架采用的模型,有兴趣的读者可以研究下grizzly,在grizzly的最新版本中已经有基于异步I/O 的实现。
Reactor 的模型和Proactor模型的最大区别在于:真正的读写操作在Proactor中是由操作系统完成的(取决于操作系统支持异步API),请求进程在系统处理完之后会得到一个反馈或者是回调,这完全是系统级别的异步处理;而对于Reactor模式,实际的数据读写操作由发起请求的进程来处理:通过不断的注册感兴趣的事件操作,一直到数据处理完毕,事件取消为止。
通过在Reactor 上来模拟Proactor模式是大多数高性能服务器通讯框架通常在实际实现上采用的通用策略,无论是Mina和Netty还是Jetty I/O。
?
好了,到目前为止我们的基础知识就先聊到这里为止,在接下来让我们一起来探究Jetty I/O组件模型的实现吧。
(四)Jetty NIO实现
在具体分析代码之前先简单梳理下Jetty NIO组件的基本内容:
(1)EndPoint :网络上进行相互通信的对端实体抽象
(2)Connection:网络实体通信链接的抽象
(3)ByteBufferPool:缓冲区对象池
(3)SelectChannelEndPoint:基于NIO模型描述的EndPoint封装
(4)SelectorManager:选择器管理器
Jetty的IO组件中大概就这么多内容,简单易懂非常适合研习。这也是本文想从Jetty入手来介绍NIO的一个考虑。
整个Jetty NIO组件架构是基于Reactor 模型,并采用异步处理方式,也就是我们上文提到的通过在Reactor 上来模拟Proactor模式的实现策略。要知道在高并发的网络请求处理中,不能因为某一个TCP链接的处理异常而导致其它连接无法及时的被响应,这在高性能网络服务器设计中是不能容忍的。基于Proactor或者是伪Proactor模型可以使得网络连接的处理具备极高的扩展性和响应性。
Jetty IO是基于非阻塞同步IO来设计和实现,也就是基于NIO 1.0 架构。好了,到了该上代码的时候了。
?
???? (a)缓冲区对象池
Jetty在连接开始建立之前,内置了一个缓冲区对象池。采用缓冲池的好处一个是:当需要通讯的网络对端有读写操作时,通过从缓冲区对象池获取可用的Buffer来提高读写的响应速度,有点类似于Memcache的内存处理方式。在Mina中也有这样的设计;那另外一个就是好处就是:便于内存管理。
?
下面ByteBufferPool 的部分代码清单:
public ArrayByteBufferPool() {
?????? this(64,2048,64*1024);
}
public ArrayByteBufferPool(int minSize, int increment, int maxSize) {
????? _min=minSize;
????? _inc=increment;
???? ??? _direct=new Bucket[maxSize/increment];
????? _indirect=new Bucket[maxSize/increment];
????? int size=0;
????? for (int i=0;i<_direct.length;i++) {
? ?? ?????? size+=_inc;
??? ????? _direct[i]=new Bucket(size);
??? ????? _indirect[i]=new Bucket(size);
?? }
}
从上面代码可以看出,两个Bucket 桶分别用来存放直接缓冲区的大小和堆缓冲区的大小。Bucket 桶的大小都为32,最小的缓冲区大小为2048,最大为32 * 2048。当需要使用的时候可以直接从缓冲区中分配指定大小的缓冲区。
(b)Reactor 机制
Java NIO最大的价值在于可以通过一个线程来监控大量链接的读写就绪状态,使得高并发的网络连接可以被及时的处理和响应。试想传统服务器采用每个请求一个进程的模式,或者每个请求一个线程的模型,这种模型在连接数不大的情况下是没有问题的,但是要处理成千上万的并发请求时,如果采用上述模式,那么服务器的资源很快就会被消耗殆尽,服务很快会变得不可用。在*nix系统中很早就有select和poll函数的API供C 程序员使用,在Java中直到JDK 1.4的时才具备这样的IO处理能力。
Reactor线程在这里可以看作就是我们要将的Selector线程,用以监控大量通道的就绪状态。一般情况下启动一个Selector线程是完全可以,不仅消除了多个线程管理的额外开销,同时可以降低复杂性,提高吞吐量并可能大幅度提高性能。这对于单CPU系统来说是一个很好的策略,但是对于多于一个(比如N个)CPU的系统来说,可能就会有N-1和CPU处于空闲状态。让我们来看看Jetty是如何做的呢?
?
下面Selector线程数大小的部分代码清单:
protected SelectorManager(Executor executor, Scheduler scheduler) {
?this(executor, scheduler, (Runtime.getRuntime().availableProcessors() + 1) / 2);
}
protected SelectorManager(Executor executor, Scheduler scheduler, int selectors) {
? this.executor = executor;
? this.scheduler = scheduler;
? _selectors = new ManagedSelector[selectors];
}
?
Jetty默认Selector线程数大小为: (Runtime.getRuntime().availableProcessors() + 1) / 2。且不说Jetty这样做性能到底能提升多少,笔者这里也没有对Jetty做过实际的压测分析,不可妄加推测。但是对于大多数场景中使用一个Selector线程通常完全可以满足要求,毕竟就绪选择的大多数工作是有底层操作系统完成的,使用一个Selector线程这样可能会简单一些,而简单的就是好的^_^。
(C)异步CallBack与线程池
从上文的讨论我们得知,真正能做到系统级别的异步IO,也就是我们所说的真正的异步IO能力,这需要操作系统在体层提供支持,如Linux中 aio_read函数,这一点非常重要否则就不能说是真正的异步IO。JDK在1.7 NIO 2.0框架中为我们提供了异步IO处理的选择。我们使用异步IO方式可以简单的归纳为两种方式:(1)基于Future模型;(2)基于CallBack机制。
Jetty中虽然没有使用异步IO模型,但是Jetty采用了回调机制来处理Socket读。注意这里的回调还是建立在非阻塞同步IO之上,不同于上面所说的异步IO CallBack机制。区别在于真正处理Socket数据读的线程是Reactor线程本身还是另外一个线程。
Jetty提供了一个开关,可以选在在Reactor线程上直接执行读操作,也可以在另外一个线程上处理真正的读操作。
?
下面ReadCallback的部分代码清单:
private class ReadCallback implements Callback, Runnable {
??????? @Override
??????? public void run() {
?????????? // ...........................................................................
??????? }
??????? @Override
??????? public void succeeded() {
??????????? if (_executeOnfillable)
??????????????? _executor.execute(this);
??????????? else
??????????????? run();
??????? }
}
?
参数executeOnfillable在Jetty中默认的设置是true,这样做的好处非常明显。Reactor线程的职责定位非常清楚,仅监控大量通道连接上就绪事件,而真正繁重的IO任务处理交给其它线程处理,在高并发网络请求处理中请求响应的处理速度就会进一步的得到提升。
(D)事件通知与状态机
Jetty在处理网络连接和Socket数据读写的时候基于事件机制,也可以理解为Java中观察者模式。当Connection连接建立时,Socket数据可读,数据已写,连接通道关闭时都是以事件的方式通知给各个观察者。这使得Jetty的整个IO生命周期的控制非常的松散,低耦合。这种方式日常工作当中运用的也是非常多的,不必细说,还是直接上代码吧。
?
下面NetworkTrafficListener的部分代码清单:
public interface NetworkTrafficListener
{
public void opened(Socket socket);
public void incoming(Socket socket, ByteBuffer bytes);
public void outgoing(Socket socket, ByteBuffer bytes);
public void closed(Socket socket);
}
?
Jetty在内部的使用了大量的状态机制用于控制处理的过程。比如对于socket读也使用了状态机机制,来协调、控制和监控socket数据的读取过程。
?
下面Socket读状态机:State 的状态标志代码清单:
private enum State
{
IDLE, INTERESTED, FILLING, FILLING_INTERESTED
}
(五)总结
本文以(N)IO为主线,以Jetty NIO组件为依托,简单描述和分析了高性能WEN 容器Jetty在处理IO时的一些设计和考虑,当然本文只是抽出了Jetty IO实现的部分亮点来介绍的还不是很完整。关于Jetty IO和NIO的其它高级特性,期待我接下来的一篇技术分享吧^_^。
?
?
如需转载,请注明出处。