高性能的HTTP引擎—— Grizzly(三) Grizzly的特点(转)
对企业级的服务器软件,高性能和可扩展性是基本的要求。除此之外,还应该有应对各种不同环境的能力。例如,一个好的服务器软件不应该假设所有的客户端都有很快的处理能力和很好的网络环境。如果一个客户端的运行速度很慢,或者网络速度很慢,这就意味着整个请求的时间变长。而对于服务器来说,这就意味着这个客户端的请求将占用更长的时间。这个时间的延迟不是由服务器造成的,因此CPU的占用不会增加什么,但是网络连接的时间会增加,处理线程的占用时间也会增加。这就造成了当前处理线程和其他资源得不到很快的释放,无法被其他客户端的请求来重用。例如Tomcat,当存在大量慢速连接的客户端时,线程资源被这些慢速的连接消耗掉,使得服务器不能响应其他的请求了。
前面介绍过,NIO的异步非阻塞的形式,使得很少的线程就能服务于大量的请求。通过Selector的注册功能,可以有选择性地返回已经准备好的频道,这样就不需要为每一个请求分配单独的线程来服务。
在一些流行的NIO的框架中,都能看到对OP_ACCEPT和OP_READ的处理。很少有对OP_WRITE的处理。我们经常看到的代码就是在请求处理完成后,直接通过下面的代码将结果返回给客户端:
【例17.7】不对OP_WRITE进行处理的样例:
while (bb.hasRemaining()) {
??? int len = socketChannel.write(bb);
??? if (len < 0) {
??????? throw new EOFException();
??? }
}
这样写在大多数的情况下都没有什么问题。但是在客户端的网络环境很糟糕的情况下,服务器会遭到很沉重的打击。
因为如果客户端的网络或者是中间交换机的问题,使得网络传输的效率很低,这时候会出现服务器已经准备好的返回结果无法通过TCP/IP层传输到客户端。这时候在执行上面这段程序的时候就会出现以下情况。
(1) bb.hasRemaining()一直为“true”,因为服务器的返回结果已经准备好了。
(2) socketChannel.write(bb)的结果一直为0,因为由于网络原因数据一直传不过去。
(3)?? 因为是异步非阻塞的方式,socketChannel.write(bb)不会被阻塞,立刻被返回。
(4)?? 在一段时间内,这段代码会被无休止地快速执行着,消耗着大量的CPU的资源。事实上什么具体的任务也没有做,一直到网络允许当前的数据传送出去为止。
这样的结果显然不是我们想要的。因此,我们对OP_WRITE也应该加以处理。在NIO中最常用的方法如下。
【例17.8】一般NIO框架中对OP_WRITE的处理:
while (bb.hasRemaining()) {
??? int len = socketChannel.write(bb);
??? if (len < 0){
??????? throw new EOFException();
??? }
??? if (len == 0) {
??????? selectionKey.interestOps(
??????????????????????? selectionKey.interestOps() | SelectionKey.OP_WRITE);
??????? mainSelector.wakeup();
??????? break;
??? }
}
上面的程序在网络不好的时候,将此频道的OP_WRITE操作注册到Selector上,这样,当网络恢复,频道可以继续将结果数据返回客户端的时候,Selector会通过SelectionKey来通知应用程序,再去执行写的操作。这样就能节约大量的CPU资源,使得服务器能适应各种恶劣的网络环境。
可是,Grizzly中对OP_WRITE的处理并不是这样的。我们先看看Grizzly的源码吧。在Grizzly中,对请求结果的返回是在ProcessTask中处理的,经过SocketChannelOutputBuffer的类,最终通过OutputWriter类来完成返回结果的动作。在OutputWriter中处理OP_WRITE的代码如下:
【例17.9】Grizzly中对OP_WRITE的处理:
public static long flushChannel(SocketChannel socketChannel,
??????? ByteBuffer bb, long writeTimeout) throws IOException
{
??? SelectionKey key = null;
??? Selector writeSelector = null;
??? int attempts = 0;
??? int bytesProduced = 0;
??? try {
??????? while (bb.hasRemaining()) {
??????????? int len = socketChannel.write(bb);
??????????? attempts++;
??????????? if (len < 0){
??????????????? throw new EOFException();
??????????? }
??????????? bytesProduced += len;
??????????? if (len == 0) {
??????????????? if (writeSelector == null){
??????????????????? writeSelector = SelectorFactory.getSelector();
??????????????????? if (writeSelector == null){
??????????????????????? // Continue using the main one
??????????????????????? continue;
??????????????????? }
??????????????? }
??????????????? key = socketChannel.register(writeSelector, key.OP_WRITE);
??????????????? if (writeSelector.select(writeTimeout) == 0) {
??????????????????? if (attempts > 2)
??????????????????????? throw new IOException("Client disconnected");
??????????????? } else {
??????????????????? attempts--;
??????????????? }
??????????? } else {
??????????????? attempts = 0;
??????????? }
??????? }
??? } finally {
??????? if (key != null) {
??????????? key.cancel();
??????????? key = null;
??????? }
??????? if (writeSelector != null) {
??????????? // Cancel the key.
??????????? writeSelector.selectNow();
??????????? SelectorFactory.returnSelector(writeSelector);
??????? }
??? }
??? return bytesProduced;
}?
上面的程序例17.9与例17.8的区别之处在于:当发现由于网络情况而导致的发送数据受阻(len==0)时,例17.8的处理是将当前的频道注册到当前的Selector中;而在例17.9中,程序从SelectorFactory中获得了一个临时的Selector。在获得这个临时的Selector之后,程序做了一个阻塞的操作:writeSelector.select(writeTimeout)。这个阻塞操作会在一定时间内(writeTimeout)等待这个频道的发送状态。如果等待时间过长,便认为当前的客户端的连接异常中断了。
这种实现方式颇受争议。有很多开发者置疑Grizzly的作者为什么不使用例17.8的模式。另外在实际处理中,Grizzly的处理方式事实上放弃了NIO中的非阻塞的优势,使用writeSelector.select(writeTimeout)做了个阻塞操作。虽然CPU的资源没有浪费,可是线程资源在阻塞的时间内,被这个请求所占有,不能释放给其他请求来使用。
Grizzly的作者对此的回应如下。
(1)?? 使用临时的Selector的目的是减少线程间的切换。当前的Selector一般用来处理OP_ACCEPT,和OP_READ的操作。使用临时的Selector可减轻主Selector的负担;而在注册的时候则需要进行线程切换,会引起不必要的系统调用。这种方式避免了线程之间的频繁切换,有利于系统的性能提高。
(2)?? 虽然writeSelector.select(writeTimeout)做了阻塞操作,但是这种情况只是少数极端的环境下才会发生。大多数的客户端是不会频繁出现这种现象的,因此在同一时刻被阻塞的线程不会很多。
(3)?? 利用这个阻塞操作来判断异常中断的客户连接。
(4)?? 经过压力实验证明这种实现的性能是非常好的。
17.3.2? 如何避免内存泄漏
在NIO的框架模型中,值得注意的是有一个API由于NIO非阻塞的特点,其使用比较频繁,那就是java.nio.channel.SelectionKey.attach()。
这是因为在非阻塞的频道中,在socketChannel.read(byteBuffer)的调用中,往往不能返回所有的请求数据,其他的部分数据可能要在下一次(或几次)的读取中才能完全返回。因此在读取一些数据之后,需要将当前的频道重新注册到Selector上:
selectionKey.interestOps(
selectionKey.interestOps() | SelectionKey.OP_READ);
这样还不够,因为前几次读取的部分数据也需要保留,将所有读取的数据综合起来才是完整的数据,因此需要调用下面的函数将部分数据保存起来供以后使用:
selectionKey.attach(...)
这个函数设计的目的也在于此,主要用于异步非阻塞的情况保存恢复与频道相关的数据。但是,这个函数非常容易造成内存泄漏。这是因为在非阻塞的情况下,你无法保证这个带有附件的SelectionKey什么时候再次返回到准备好的状态。在一些特殊的情况下(例如,客户端的突然断电或网络问题)导致代表这些连接的SelectionKey永远也不会返回到准备好状态了,而一直存放在Selector中,它们所带的附件也就不会被Java自动回收内存的机制释放掉。内存泄漏对长时间运行的服务器端软件是不能容忍的重大隐患。那么我们看看在Grizzly中是如何处理这种问题的。
事实上,在Grizzly的实现中很少看到selectionKey.attach(...)的代码。在入口程序SelectThread中的enableSelectionKeys()方法中有这个方法的调用。
【例17.10】SelectThread中的enableSelectionKeys()方法:
public void enableSelectionKeys(){
??? SelectionKey selectionKey;
??? int size = keysToEnable.size();
??? long currentTime = (Long)System.currentTimeMillis();
??? for (int i=0; i < size; i++) {
??????? selectionKey = keysToEnable.poll();
??????? selectionKey.interestOps(
??????????????? selectionKey.interestOps() | SelectionKey.OP_READ);
??????? if (selectionKey.attachment() == null)
??????????? selectionKey.attach(currentTime);
??????????? keepAlivePipeline.trap(selectionKey);??
??????? }
}
}
显而易见,这个函数的目的在这里是要给每个selectionKey加上一个时间戳。这个时间戳是为KeepAlive系统而加的。怎样防止这个long类型对象的内存泄漏呢?在SelectThread的doSelect()方法中有一个expireIdleKeys()的调用。
【例17.11】SelectThread的expireIdleKeys()方法:
protected void expireIdleKeys(){
??? if (keepAliveTimeoutInSeconds <= 0 || !selector.isOpen()) return;
??? long current = System.currentTimeMillis();
??? if (current < nextKeysExpiration) {
??????? return;
??? }
??? nextKeysExpiration = current + kaTimeout;
???????
??? Set<SelectionKey> readyKeys = selector.keys();
??? if (readyKeys.isEmpty()){
??????? return;
??? }
??? Iterator<SelectionKey> iterator = readyKeys.iterator();
??? SelectionKey key;
??? while (iterator.hasNext()) {
??????? key = iterator.next();
??????? if (!key.isValid()) {
??????????? keepAlivePipeline.untrap(key);
??????????? continue;
??????? }?
???????????????????????
??????? // Keep-alive expired
??????? if (key.attachment() != null) {
???????????????
??????????? if (!defaultAlgorithmInstalled
??????????????????????? && !(key.attachment() instanceof Long)) {
??????????????? continue;
??????????? }
???????????????????
??????????? try{
??????????????? long expire = (Long)key.attachment();
??????????????? if (current - expire >= kaTimeout) {
??????????????????? if (enableNioLogging){
??????????????????????? logger.log(Level.INFO,
????????????????????????????????? "Keep-Alive expired for SocketChannel " +
????????????????????????????????? key.channel());
??????????????????? }???????????????????
??????????????????? cancelKey(key);
??????????????? } else if (expire + kaTimeout < nextKeysExpiration){
??????????????????? nextKeysExpiration = expire + kaTimeout;
??????????????? }
??????????? } catch (ClassCastException ex){???????????????????????????
??????????????? if (logger.isLoggable(Level.FINEST)){
??????????????????? logger.log(Level.FINEST,
?????????????????????????????? "Invalid SelectionKey attachment",ex);
??????????????? }
??????????? }
??????? }
??? }???????????????????
}?
上面代码的作用显而易见:在每次doSelect()的调用中,expireIdleKeys()都会被执行一次,来查看selector中的每个SelectionKey,将它们的时间戳与当前的时间相比,判断是否当前的SelectionKey很长时间没有响应了,然后根据配置的timeout时间,强行将其释放和回收。
那么系统用来存放每一次请求读取的数据放在哪里了呢?一般来说这个存放频道数据的对象应该是ByteBuffer。在DefaultReadTask类中,可以看到ByteBuffer的使用情况。
【例17.12】DefaultReadTask中的doTask()方法:
public void doTask() throws IOException {??
??? if (byteBuffer == null) {
??????? WorkerThread workerThread = (WorkerThread)Thread.currentThread();
??????? byteBuffer = workerThread.getByteBuffer();
??????? if (workerThread.getByteBuffer() == null){
??????????? byteBuffer = algorithm.allocate(useDirectByteBuffer,
????????????????????? useByteBufferView,selectorThread.getBufferSize());
??????????? workerThread.setByteBuffer(byteBuffer);
??????? }
??? }
??? doTask(byteBuffer);
}
上面的方法透露出两个重要的信息:
l?? 对ByteBuffer的分配,并不是每个SelectionKey(或者说每个网络连接)都有自己的ByteBuffer,而是每个工作线程拥有一个ByteBuffer。
l?? ByteBuffer的分配也不是新创建的ByteBuffer对象,而是通过ByteBufferView来对原有的ByteBuffer对象进行重新分割。原因是新建一个ByteBuffer对象的系统消耗比较大,因此Grizzly在启动的时候初始创建了一个大的ByteBuffer对象。以后每个线程再需要ByteBuffer对象的时候,就通过ByteBufferView来在原有ByteBuffer之上创建一个视图,这样的性能要好得多。
如果说每个线程只使用一个ByteBuffer对象(确切地说是ByteBufferView对象),而在NIO中,每个线程是要服务于多个连接请求的,那么线程是怎样维护每个连接请求的数据的独立性呢?从DefaultReadTask中的doTask(ByteBuffer byteBuffer)方法中,我们可以看到最初始的读取调用以及对读取数据的处理过程。
【例17.13】DefaultReadTask中的doTask(ByteBuffer byteBuffer)方法:
protected void doTask(ByteBuffer byteBuffer){
??? int count = 0;
??? Socket socket = null;
??? SocketChannel socketChannel = null;
??? boolean keepAlive = false;
??? Exception exception = null;
??? key.attach(null);
????
??? try {
??????? socketChannel = (SocketChannel)key.channel();
??????? socket = socketChannel.socket();
??????? algorithm.setSocketChannel(socketChannel);???????????
??????????
??????? int loop = 0;
??????? int bufferSize = 0;
??????? while (socketChannel.isOpen() && (bytesAvailable ||
??????????????? ((count = socketChannel.read(byteBuffer))> -1))){? // [1]
??????????? ...
??????????? byteBuffer = algorithm.preParse(byteBuffer);
??????????? inputStream.setByteBuffer(byteBuffer);?? // [2]
??????????? inputStream.setSelectionKey(key);
???????????
??????????? // try to predict which HTTP method we are processing
??????????? if (algorithm.parse(byteBuffer) ){?? // [3]
??????????????? keepAlive = executeProcessorTask();? //? [4]
??????????????? if (!keepAlive) {
??????????????????? break;
??????????????? }
??????????? }
??????????? ...
??????? }
}
...
}
在例17.13的方法中,可以清楚地看到,在此方法中程序作了初始的读取动作[1]socketChannel.read(byteBuffer)。初始读取完后,其实并不知道是否所有的请求数据都已经读进来了。于是程序交给HTTP的一个解析算法类(algorithm)来决定是否所有的请求数据都已经读取进来了。接着这个请求就交给[4]executeProcessorTask()去执行了。在executeProcessorTask()中使用了一个ByteBufferInputStream类,这个类是对ByteBuffer的一个封装,并在[2]中进行了设置和初始化。事实上,在默认的解析算法中,客户端的请求在第一次读取动作中如果没有全部完成,那剩余部分的数据其实就交给ByteBufferInputStream来完成了。
【例17.14】ByteBufferInputStream中的doRead()方法:
/**
* Read bytes using the ReadSelector
*/
protected int doRead() throws IOException{???????
??? if (key == null) return -1;
???????
??? byteBuffer.clear();
??? int count = 1;
??? int byteRead = 0;
??? Selector readSelector = null;
??? SelectionKey tmpKey = null;
??? try{
??????? SocketChannel socketChannel = (SocketChannel)key.channel();
??????? while (count > 0){
??????????? count = socketChannel.read(byteBuffer);? //[1]
??????????? if (count > -1)
??????????????? byteRead += count;
??????????? else
??????????????? byteRead = count;
??????? }???????????
???????????
??????? if (byteRead == 0){
??????????? readSelector = SelectorFactory.getSelector(); //[2]
??????????? if (readSelector == null){
??????????????? return 0;
??????????? }
??????????? count = 1;
??????????? tmpKey = socketChannel
??????????????????? .register(readSelector,SelectionKey.OP_READ);??????????????
??????????? tmpKey.interestOps(
tmpKey.interestOps() | SelectionKey.OP_READ);
??????????? int code = readSelector.select(readTimeout); // [3]
??????????? tmpKey.interestOps(
??????????????????? tmpKey.interestOps() & (~SelectionKey.OP_READ));
??????????? if (code == 0){
??????????????? return 0; // Return on the main Selector and try again.
??????????? }
??????????? while (count > 0){
??????????????? count = socketChannel.read(byteBuffer); // [4]
??????????????? if (count > -1)
??????????????????? byteRead += count;
??????????????? else
??????????????????? byteRead = count;???????????????????
??????????? }
??????? }
??? } finally {
??????? if (tmpKey != null)
??????????? tmpKey.cancel();
??????? if (readSelector != null){
??????????? // Bug 6403933
??????????? try{
??????????????? readSelector.selectNow();
??????????? } catch (IOException ex){
??????????????? ;
??????????? }
??????????? SelectorFactory.returnSelector(readSelector);
??????? }
??? }
??? byteBuffer.flip();
??? return byteRead;
}
查看过这个方法之后,觉得很有意思:对每个连接保存的数据的ByteBuffer对象,在Grizzly中根本不会有什么内存泄漏的问题。因为在Grizzly中根本没有使用NIO模式中设计方法(将ByteBuffer附加到SelectionKey中,再将SelectionKey重新注册到Selector中等待下次激活)。在Grizzly中对请求数据的读取完全使用了传统的阻塞方式,根本不需要attach和将SelectionKey重新注册到Selector。
当读取数据的任务交给ByteBufferInputStream的时候,ByteBufferInputStream会再做一次最大的努力来读取可能有的数据[1]。如果还是没有读取到什么数据的话,Grizzly并没有将SelectionKey重新注册到主线程的Selector,而是从Selector池中获得一个临时的Selector[2],将SelectionKey重新注册到这个临时的Selector中。接着这个临时的Selector做了一个阻塞的操作readSelector.select(readTimeout)[3],这个动作一直会阻塞到当前频道有数据进来,或者阻塞时间超过Timeout的时间。
这种算法也颇受争议。有的人认为使用阻塞的模式性能不会比NIO中非阻塞的模式好,特别是在有很多网络速度很慢的客户端的情况下,这样会大量造成线程的占用而变得不具有很好的可扩展性。
Grizzly的作者也承认,如果在大量慢速的客户端的情况下,使用非阻塞模式肯定要好些。但是他为自己的实现算法也给了下面一些理由。
(1)?? 假设大多数客户端的速度良好是合理的。因此大多数的请求数据在一到两次都能全部读取。
(2)?? 对连接异常的客户端可以在最早时间范围内进行判断和做出放弃的决定,保护系统的资源不被浪费。
(3)?? 这样实现没有内存泄漏的问题,而且内存消耗也要小些,能够获得更好的性能。
(4)?? 因为每个连接所有的读取过程都在一个线程中完成,不用在主线程(Selector所在的线程)之间切换,可以减少操作系统的线程调度负担,并且减少主线程的消耗。
17.3.3? 使用多个Selector
经常会有人问:什么是企业级应用?也经常看到一些产品的说明书标称该软件产品为企业级产品。究竟什么样的产品才能有资格被称作企业级产品?这个问题很难被回答,每个人的标准是不一样的。最近在进行一些企业级别应用测试的时候,发现扩展性是比较重要的一个指标。
当时测试的硬件是比较高档的服务器,具有32个或64个以上的CPU。由于稳定性和安全性的原因,这些多CPU的UNIX服务器是当前大型企业应用关键业务系统所愿意使用的运行环境。但是在测试的时候发现,在少量CPU的情况下(2到6个),绝大多数软件系统都能比较充分地利用机器提供的资源,获得不错的性能指标。当测试压力不断增加,需要更多的CPU的时候,不同的应用系统所表现出来的扩展性就大不相同了。很多开源的软件,包括一些开源的数据库软件和应用服务器在CPU超过8个的时候性能不升反降,无法充分利用硬件系统提供CPU资源。而那些商业的数据库软件(包括Oracle、DB2、Informix、Sybase)和应用服务器(BEA Weblogic、Sun JES)都能够在多达64个CPU的系统上扩展得很好。并不是说开源软件不好,这是个定位问题。如果是企业级软件系统,那么就应该在最初的设计和最后的测试环节都应该考虑到扩展性的问题:当这个系统给予了更多的硬件资源的时候,是不是能够运行得更快,或者能支持更多的服务请求。
从源码中可以看到,GlassFish到处都考虑到扩展性的问题,真正将自己定位于企业级的应用了,先不提GlassFish中对负载均衡和集群的支持,在Grizzly中Selector的设计和实现就充分考虑了扩展性的要求。
一般NIO的框架结构应该是这样的:首先创建ServerSocketChannel的实例,并且获得一个Selector的实例,将它绑定到相应的端口上,再将ServerSocketChannel配置成非阻塞模式,接着将OP_ACCEPT注册到这个Selector上。
【例17.15】传统NIO中的主线程:
serverSocketChannel = ServerSocketChannel.open();
selector = Selector.open();
serverSocket = serverSocketChannel.socket();
serverSocket.setReuseAddress(true);
serverSocket.bind(new InetSocketAddress(port),ssBackLog);
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
当OP_ACCEPT事件发生的时候,需要将新产生的SocketChannel的OP_READ注册到这个Selector中去。
【例17.16】传统对OP_ACCEPT的处理:
protected void handleAccept(SelectionKey key) throws IOException{
??? ServerSocketChannel server = (ServerSocketChannel)key.channel();
??? SocketChannel channel = server.accept();
??? if (channel != null) {
??????? channel.configureBlocking(false);
??????? SelectionKey readKey =
??????????????? channel.register(selector, SelectionKey.OP_READ);
??????? setSocketOptions(((SocketChannel)readKey.channel()).socket());
??? }
??? ...
}
这种处理方式在大并发客户数量的情况下,很容易使得这个主线程变得很繁忙:它既要负责OP_ACCEPT和OP_READ的注册和监控,还需要负责OP_ACCEPT的处理(OP_READ的处理一般在另外的线程中);除此以外,主线程还有可能要负责监控客户端的连接是否异常,来保证没有内存泄漏的情况。因为单个线程只能在单个CPU中执行,在用户并发数量很多的情况下,主线程可能被延迟。一旦主线程被延迟,系统其他部分的运行都会受到很大的影响。
在Grizzly中可以配置使用多个Selector(和多个Selector线程)。在Grizzly中存在与多个Selector配置相关的参数。
【例17.17】SelectorThread中对多个Selector配置相关参数的定义:
/**
* The number of SelectorReadThread
*/
protected int multiSelectorsCount = 0;
/**
* The Selector used to register OP_READ
*/???
protected MultiSelectorThread[] readThreads;
【例17.18】SelectorThread中对多个Selector配置相关参数的使用:
protected void handleAccept(SelectionKey key) throws IOException{
??? ServerSocketChannel server = (ServerSocketChannel)key.channel();
??? SocketChannel channel = server.accept();
...
??? if (channel != null) {
??????? if (multiSelectorsCount > 1) {
??????????? MultiSelectorThread srt = getSelectorReadThread();
??????????? srt.addChannel(channel);
??????? } else {
??????????? channel.configureBlocking(false);
??????????? SelectionKey readKey =
?????????????????? channel.register(selector, SelectionKey.OP_READ);
setSocketOptions(((SocketChannel)readKey
.channel()).socket());
??????? }
??? }
}
【例17.19】MultiSelectorThread接口的实现类SelectorReadThread:
public class SelectorReadThread extends SelectorThread
??????? implements MultiSelectorThread{
??? /**
???? * List of Channel to process
???? */
??? ArrayList<SocketChannel> channels = new ArrayList<SocketChannel>();
??? /**
???? * Int used to differenciate this instance
???? */
??? public static int countName;
??? /**
???? * Add a Channel to be processed by this Selector
???? */
??? public synchronized void addChannel(SocketChannel channel)
??????????? throws IOException, ClosedChannelException {
??????? channels.add(channel);
??????? getSelector().wakeup();
}
??? /**
???? * Register all Channel with an OP_READ opeation
???? */
??? private synchronized void registerNewChannels() throws IOException{
??????? int size = channels.size();
??????? for (int i = 0; i < size; i++) {
??????????? SocketChannel sc = channels.get(i);
??????????? sc.configureBlocking(false);
??????????? try {
??????????????? SelectionKey readKey =
??????????????????????? sc.register(getSelector(), SelectionKey.OP_READ);
setSocketOptions(((SocketChannel)readKey
.channel()).socket());
??????????? } catch (ClosedChannelException cce) {}
??????? }
??????? channels.clear();
??? }
...
}
从例17.18和例17.19的代码中可以看出,当配置有多个Selector的时候,在处理OP_ACCEPT时,新建立的连接可以交给MultiSelectorThread的类来监控和管理这些连接的OP_READ事件,分担了主线程的负担。在多CPU大并发用户的情况下,使得系统具有较好的扩展性。
17.3.4? Grizzly其他的特点
1. 异步请求处理
在应用服务器中,我们通常使用的请求都是同步的请求。当客户端的请求进来以后被服务器所解析,随后Servlet或JSP被调用,运行的结果被返回到客户端。但是在一些情况下,这种同步的处理过程不能很好地满足要求。例如,被执行的业务逻辑需要调用外部的一个服务,而这个服务响应得很慢;或者客户的请求介入到一个工作流程当中,被外部的因素所中断(需要老板批准等)。在这些情况下,虽然使用同步机制也能实现,但是轮询或阻塞的方法对系统资源的消耗比较大,系统结构也因此变得复杂。
在Grizzly中有一个com.sun.enterprise.web.connector.grizzly.async包,用来实现异步的请求处理。
2. 服务器推送技术
服务器推送技术(Comet)现在非常的流行,结合AJAX和服务器推送技术,可以实现非常灵活和高性能的应用程序。在com.sun.enterprise.web.connector.grizzly.comet的包中,Grizzly将异步处理请求和服务器推送技术完美地结合在一起,使GlassFish成为支持服务器推送技术的开源产品之一。
3. 资源分配和管理
Application Resource Allocation(RAR,应用资源分配)本应该是操作系统或硬件层面上的话题。现代的计算机系统提供了各种各样的资源虚拟技术,有的在操作系统中,有的是在服务器硬件中,还有的是通过跨平台的框架(例如网格技术)将企业内部的各种资源进行划分和合理应用。这种需求非常多,因为在企业内部存在的各种应用的重要程度和优先级别都不同,级别高的重要应用不应该受到其他应用的影响,应当享有资源分配的优先权。
但是不同操作系统、不同的网格技术对应用资源分配的方式各不相同。在Grizzly中存在着三个包:com.sun.enterprise.web.ara、com.sun.enterprise.web.ara.algorithms、com.sun.enterprise. web.ara.rules。通过这三个包,在Grizzly中就可以实现对部署在它上面的应用进行资源分配和管理。管理的规则主要包括以下两类:
l?? 当前应用所占Java Heap的百分比。
l?? 当前应用所占线程数量的百分比。
4. 统一端口
在安装应用服务器的过程中,很重要的一件事情就是分配端口号。一般来说,一个应用服务器分配的端口不只一个,可能有三、四个,还可能更多。这些端口包括各种不同的服务或协议监听所在的Socket,有HTTP端口,有HTTPS端口,有IIOP端口,还有其他通信端口。如果是在一台共享的服务器上安装应用服务器,情况要更加糟糕,因为有的端口已经被别的应用所占用,有时不得不在启动服务器的时候手动修改端口号。
在Grizzly中有两个包:com.sun.enterprise.web.portunif和com.sun.enterprise.web.portunif. util,这两个包的功能是统一端口号。通过这两个包,GlassFish可以只启动一个端口,仍然可以服务于多个不同的协议。例如4848端口既是HTTP的端口,又是HTTPS的端口,还是IIOP的端口。这样就大大简化了管理员的工作。
Grizzly通过可插拔的形式来定义不同的协议和协议的处理程序。主要的接口如下。
l?? ProtocolFinder:当请求进来以后,通过ProtocolFinder来确定当前的请求是什么协议。Grizzly默认实现了HttpProtocolFinder和HttpsProtocolFinder。
l?? ProtocolHandler:当确定了使用什么协议以后,相应的协议处理单元就会被调用。在协议处理单元中可以做任何想做的事情,比如进行EJB的调用,进行负载均衡或请求转发等。
17.3.5? Grizzly的性能
Grizzly在整个设计和开发过程中,性能和高扩展性是它的核心。从源码的各个细节都可以看出Grizzly对高性能的追求。例如,对ByteBufferView的使用、对多个Selector的支持、对不同线程模型的配置、对多个HTTP解析算法的选择,以及对OP_READ和OP_WRITE的特殊处理都反映了Grizzly对高性能一丝不苟的严格要求。
通过内部的性能测试,并且和传统的Web服务器的比较,事实数据证明了Grizzly是性能和可扩展性都非常高的HTTP引擎,请参看图17-2。在大并发压力的测试下,它的性能甚至超过了两款使用C语言编写的Web服务器。与传统的Java阻塞式的HTTP引擎相比,Grizzly的性能和扩展性远远地超过它们
?