- 浏览: 34558 次
- 性别:
- 来自: 广州
-
文章分类
最新评论
-
0dragon:
...
高性能的HTTP引擎—— Grizzly(一) NIO简介 -
kaipingk:
glasshfish是不是吹得过头了哦!!!!!!!!!!!! ...
高性能的HTTP引擎—— Grizzly(三) Grizzly的特点 -
alloyer:
终于有人发Mule的东西了,我现在也在研究这方面的东西.看了好 ...
activeMQ+Mule 练习 -
daystream:
我也觉得glassfish的grizzly有点言过其实,我的项 ...
高性能的HTTP引擎—— Grizzly(三) Grizzly的特点 -
liuyu176186368:
运行出错呀
activeMQ+Mule 练习
对企业级的服务器软件,高性能和可扩展性是基本的要求。除此之外,还应该有应对各种不同环境的能力。例如,一个好的服务器软件不应该假设所有的客户端都有很快的处理能力和很好的网络环境。如果一个客户端的运行速度很慢,或者网络速度很慢,这就意味着整个请求的时间变长。而对于服务器来说,这就意味着这个客户端的请求将占用更长的时间。这个时间的延迟不是由服务器造成的,因此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的性能和扩展性远远地超过它们
前面介绍过,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的性能和扩展性远远地超过它们

评论
4 楼
kaipingk
2009-03-09
glasshfish是不是吹得过头了哦!!!!!!!!!!!!!我准备用他comet技术做点东西,结果老师不稳定,不定时的给抛异常而且他的response似乎会被多个用户错乱,倒是收到的是另外个client的响应!!
还有就是他自己给的的comet例子更本就是个残废,也极其不稳定。。。。。。。。。。。。。。。。。。
还有就是他自己给的的comet例子更本就是个残废,也极其不稳定。。。。。。。。。。。。。。。。。。
3 楼
daystream
2008-10-05
我也觉得glassfish的grizzly有点言过其实,我的项目大概有60人登录上网,每个每5秒钟自动刷新2次请求,每个半个小时左右会出现20~30秒钟的中断,开始以为是垃圾回收的问题,回来采用bea的jrockit,已经解决了垃圾回收的问题,但还是会出现中断
2 楼
dltsh_mg
2008-03-22
既然Grizzly这个东西这么强大,我们项目使用的glassfish服务器,一直刷新一个没有任何操作的jsp,都会把glassfish刷死!真的怀疑是我们没有配置好,还是glassfish服务器本身的问题!
1 楼
jdk
2008-03-21
路过,今天就不发表意见了,做点别的贡献吧。。。
介绍个MSN 群:china.java@live.com
中国JAVA协会,加为好友即可。
里面都是专业java的,现在已经100多人了,
在线交流的很多。
介绍个MSN 群:china.java@live.com
中国JAVA协会,加为好友即可。
里面都是专业java的,现在已经100多人了,
在线交流的很多。
相关推荐
Grizzly通常指的是一个高性能、轻量级的Java网络应用框架,而PortUnif可能是它的一个组件,可能涉及到网络端口统一或者代理相关的功能。然而,描述中并未直接提到Grizzly或PortUnif,而是提到了“bencode.zip”和...
而“animation4j”这个名字暗示了它可能基于Java语言,与Grizzly框架有关,Grizzly是一个轻量级的Java服务器端框架,常用于构建高性能的网络应用。 在“animation4j”中,我们可以预见到它会提供一套完整的动画管理...
1. **高性能**:通过优化的I/O模型和高效的内存管理,鱼书能够在高并发环境下保持良好的性能,降低延迟,提高吞吐量。 2. **易用性**:框架的API设计简洁明了,使得开发者能够快速上手,减少学习成本。同时,丰富的...
这个标签指出了文档的主题——OpenStack Quantum,即 OpenStack 中用于管理网络资源的组件。 #### 正文内容概览及详细解析 **1. 概述** - **什么是 OpenStack Networking?** - OpenStack Networking(现更名为 ...
内容概要:本文详细介绍了基于SpringBoot和Vue开发的养老院管理系统的具体实现细节。该系统采用前后端不分离的架构,旨在快速迭代并满足中小项目的开发需求。文中涵盖了多个关键技术点,如数据库设计(组合唯一约束、触发器)、定时任务(@Scheduled、@Async)、前端数据绑定(Vue的条件渲染和动态class绑定)、权限控制(RBAC模型、自定义注解)以及报表导出(SXSSFWorkbook流式导出)。此外,还讨论了开发过程中遇到的一些常见问题及其解决方案,如CSRF防护、静态资源配置、表单提交冲突等。 适合人群:具备一定Java和前端开发经验的研发人员,尤其是对SpringBoot和Vue有一定了解的开发者。 使用场景及目标:适用于需要快速开发中小型管理系统的团队,帮助他们理解如何利用SpringBoot和Vue进行全栈开发,掌握前后端不分离架构的优势和注意事项。 其他说明:文章不仅提供了详细的代码示例和技术要点,还分享了许多实用的小技巧和避坑指南,有助于提高开发效率和系统稳定性。
家族企业如何应对人才流失问题?
员工关怀制度.doc
内容概要:本文详细探讨了对传统蚁群算法进行改进的方法,特别是在路径规划领域的应用。主要改进措施包括:采用排序搜索机制,即在每轮迭代后对所有路径按长度排序并只强化前20%的优质路径;调整信息素更新规则,如引入动态蒸发系数和分级强化策略;优化路径选择策略,增加排序权重因子;以及实现动态地图调整,使算法能够快速适应环境变化。实验结果显示,改进后的算法在收敛速度上有显著提升,在复杂地形中的表现更加稳健。 适合人群:从事路径规划研究的技术人员、算法工程师、科研工作者。 使用场景及目标:适用于需要高效路径规划的应用场景,如物流配送、机器人导航、自动驾驶等领域。目标是提高路径规划的效率和准确性,减少不必要的迂回路径,确保在动态环境中快速响应变化。 其他说明:改进后的蚁群算法不仅提高了收敛速度,还增强了对复杂环境的适应能力。建议在实际应用中结合可视化工具进行调参,以便更好地观察和优化蚂蚁的探索轨迹。此外,还需注意避免过度依赖排序机制而导致的过拟合问题。
内容概要:本文详细介绍了利用粒子群优化(PSO)算法解决配电网中分布式光伏系统的选址与定容问题的方法。首先阐述了问题背景,即在复杂的配电网环境中选择合适的光伏安装位置和确定合理的装机容量,以降低网损、减小电压偏差并提高光伏消纳效率。接着展示了具体的PSO算法实现流程,包括粒子初始化、适应度函数构建、粒子位置更新规则以及越界处理机制等关键技术细节。文中还讨论了目标函数的设计思路,将多个相互制约的目标如网损、电压偏差和光伏消纳通过加权方式整合为单一评价标准。此外,作者分享了一些实践经验,例如采用前推回代法进行快速潮流计算,针对特定应用场景调整权重系数,以及引入随机波动模型模拟光伏出力特性。最终实验结果显示,经过优化后的方案能够显著提升系统的整体性能。 适用人群:从事电力系统规划与设计的专业人士,尤其是那些需要处理分布式能源集成问题的研究人员和技术人员。 使用场景及目标:适用于希望深入了解如何运用智能优化算法解决实际工程难题的人士;旨在帮助读者掌握PSO算法的具体应用方法,从而更好地应对配电网中分布式光伏系统的选址定容挑战。 其他说明:文中提供了完整的Matlab源代码片段,便于读者理解和复现研究结果;同时也提到了一些潜在改进方向,鼓励进一步探索和创新。
内容概要:本文详细介绍了丰田Prius2004永磁同步电机的设计流程,涵盖从初始参数计算到最终温升仿真的各个环节。首先利用Excel进行基本参数计算,如铁芯叠厚、定子外径等,确保设计符合预期性能。接着使用Maxwell进行参数化仿真,通过Python脚本自动化调整磁钢尺寸和其他关键参数,优化电机性能并减少齿槽转矩。随后借助橡树岭实验室提供的实测数据验证仿真结果,确保模型准确性。最后采用MotorCAD进行温升仿真,优化冷却系统设计,确保电机运行安全可靠。文中还分享了许多实用技巧,如如何正确设置材料参数、避免常见的仿真错误等。 适合人群:从事电机设计的专业工程师和技术人员,尤其是对永磁同步电机设计感兴趣的读者。 使用场景及目标:适用于希望深入了解永磁同步电机设计全过程的技术人员,帮助他们在实际工作中提高设计效率和精度,解决常见问题,优化设计方案。 其他说明:文章提供了丰富的实战经验和具体的操作步骤,强调了理论与实践相结合的重要性。同时提醒读者注意一些容易忽视的细节,如材料参数的选择和仿真模型的准确性。
内容概要:本文详细介绍了基于DSP28335的单相逆变器的设计与实现,涵盖了多个关键技术模块。首先,ADC采样模块用于获取输入电压和电流的数据,确保后续控制的准确性。接着,PWM控制模块负责生成精确的脉宽调制信号,控制逆变器的工作状态。液晶显示模块则用于实时展示电压、电流等重要参数。单相锁相环电路实现了电网电压的频率和相位同步,确保逆变器输出的稳定性。最后,电路保护程序提供了过流保护等功能,保障系统的安全性。每个模块都有详细的代码示例和技术要点解析。 适合人群:具备一定嵌入式系统和电力电子基础知识的研发人员,尤其是对DSP28335感兴趣的工程师。 使用场景及目标:适用于单相逆变器项目的开发,帮助开发者理解和掌握各个模块的具体实现方法,提高系统的可靠性和性能。 其他说明:文中不仅提供了具体的代码实现,还分享了许多调试经验和常见问题的解决方案,有助于读者更好地理解和应用相关技术。
SecureCRT安装包
内容概要:本文详细介绍了如何利用C#、WPF和MVVM模式构建一个大屏看板3D可视化系统。主要内容涵盖WPF编程设计、自定义工业控件、数据库设计、MVVM架构应用以及典型的三层架构设计。文中不仅提供了具体的代码实例,还讨论了数据库连接配置、3D模型绑定、依赖属性注册等关键技术细节。此外,文章强调了项目开发过程中需要注意的问题,如3D坐标系换算、MVVM中命令传递、数据库连接字符串加密等。 适合人群:具备一定C#编程基础,对WPF和MVVM模式有一定了解的研发人员。 使用场景及目标:适用于希望深入了解WPF和MVVM模式在实际项目中应用的开发者,特别是那些从事工业控制系统、数据可视化平台开发的专业人士。通过学习本文,读者可以掌握如何构建高效、稳定的大屏看板3D可视化系统。 其他说明:本文提供的设计方案和技术实现方式,可以帮助开发者更好地理解和应用WPF和MVVM模式,同时也能为相关领域的项目开发提供有价值的参考。
基于ssm的系统设计,包含sql文件(Spring+SpringMVC+MyBatis)
内容概要:本文详细介绍了利用COMSOL进行非厄米超表面双参数传感器的设计与实现。首先,通过构建超表面单元并引入虚部折射率,实现了PT对称系统的增益-损耗交替分布。接着,通过频域扫描和参数化扫描,捕捉到了复频率空间中的能级劈裂现象,并找到了奇异点(Exceptional Point),从而显著提高了传感器对微小扰动的敏感度。此外,文章探讨了双参数检测的独特优势,如解耦温度和折射率变化的能力,并展示了其在病毒检测、工业流程监控等领域的潜在应用。 适合人群:从事光学传感器研究的专业人士,尤其是对非厄米系统和COMSOL仿真感兴趣的科研人员。 使用场景及目标:适用于需要高精度、多参数检测的应用场合,如生物医学检测、环境监测等。目标是提高传感器的灵敏度和分辨率,解决传统传感器中存在的参数交叉敏感问题。 其他说明:文中提供了详细的建模步骤和代码片段,帮助读者理解和重现实验结果。同时,强调了在建模过程中需要注意的关键技术和常见问题,如网格划分、参数设置等。
怎样健全员工福利体系.docx
离职证明范本.doc
6538b79724855900a9c930904a302920.part6
员工离职单.doc
内容概要:本文详细介绍了在COMSOL中进行超材料异常折射仿真的关键技术。首先解释了异常折射现象及其产生的原因,接着通过具体代码展示了如何利用相位梯度和结构色散精确计算折射角。文中还讨论了边界条件的设置、网格划分的优化以及参数化扫描的应用。此外,提供了多个实用脚本和技巧,帮助提高仿真的精度和效率。最后强调了验证结果的重要性和一些常见的注意事项。 适合人群:从事电磁仿真研究的专业人士,尤其是对超材料和异常折射感兴趣的科研人员和技术开发者。 使用场景及目标:适用于需要深入理解和解决超材料中异常折射问题的研究项目。主要目标是掌握COMSOL中异常折射仿真的完整流程,确保仿真结果的准确性并优化计算性能。 其他说明:文章不仅提供了详细的代码示例和技术细节,还分享了许多实践经验,有助于读者更好地应对实际仿真过程中可能出现的问题。