现说明一下系统的大致结构和配置:
运行环境: apache2/jdk6/upjas2.0.3(jboss7.2.0.Final-testsuite-fix)
系统结构:apache prefork模式作为负载均衡服务器,连接后面的jboss服务器。客户端发送请求给apache,apache转发给jboss,jboss把请求处理结果返回apache,apache再返回结果给客户端。
再描述一下我遇到的这个超级奇怪的问题:
当jboss服务器几乎没有负载时(大约5分钟只有1个请求),服务器正常工作。
当jboss服务器负载很小时(大约1分钟5个请求),服务器阻塞,拒绝处理请求。
当jboss服务器继续不断增加负载到一个量后(大约1秒钟一个请求),服务器突然又从阻塞状态复活,重新开始处理请求。
下面来分析问题。
先看一下apache和jboss的配置。
apache配置:
8080端口接受外部请求,转发到jboss服务器8000端口
<VirtualHost *:8080>
DocumentRoot "/xxx/xxx"
ProxyPass /gateway http://x.x.x.x:8000/gateway
ProxyPassReverse /gateway http://x.x.x.x:8000/gateway
</VirtualHost>
线程配置,说明最多可以有500个connection连接后面的jboss
<IfModule prefork.c>
StartServers 500
MinSpareServers 500
MaxSpareServers 500
ServerLimit 500
MaxClients 500
MaxRequestsPerChild 10000
</IfModule>
jboss配置:
<subsystem xmlns="urn:jboss:domain:threads:1.1">
<thread-factory name="web-container-thread-factory" group-name="web-container-thread-group" thread-name-pattern="web-container-thread-%t"/>
<bounded-queue-thread-pool name="http-executor">
<core-threads count="20"/>
<queue-length count="50"/>
<max-threads count="1000" />
<keepalive-time time="10" unit="minutes" />
<thread-factory name="web-container-thread-factory"/>
</bounded-queue-thread-pool>
</subsystem>
<subsystem xmlns="urn:jboss:domain:web:1.4" default-virtual-server="default-host" native="false">
<connector name="http" protocol="HTTP/1.1" scheme="http" socket-binding="http" executor="http-executor" max-post-size="2097152" />
</subsystem>
配置显示jboss使用的是bounded-queue-thread-pool,其工作方式是先创建thread处理请求,直到thread数量达到core-threads,后续的请求进queue,queue满后继续创建thread处理请求直到thread数量达到max-threads,后续的请求将被拒绝。(This is the most complex executor type. When a task is accepted, if the number of running pool threads is less than the "core" size, a new thread is started to execute the task. Otherwise, if space remains in the queue, the task is placed in the queue. Otherwise, if the number of running pool threads is less than the "maximum" size, a new thread is started to execute the task. Otherwise, if blocking is enabled on the executor, the calling thread will block until space becomes available in the queue. Otherwise, the task is delegated to the handoff executor, if one is configured. Otherwise, the task is rejected.)
./jboss-cli.sh
connect
[standalone@x.x.x.x:xxxx /] /subsystem=threads/bounded-queue-thread-pool=http-executor:read-attribute(name=
core-threads largest-thread-count thread-factory handoff-executor name queue-size
current-thread-count keepalive-time queue-length max-threads rejected-count allow-core-timeout
[standalone@x.x.x.x:xxxx /] /subsystem=threads/bounded-queue-thread-pool=http-executor:read-attribute(name=current-thread-count
{
"outcome" => "success",
"result" => 20
}
[standalone@x.x.x.x:xxxx /] /subsystem=threads/bounded-queue-thread-pool=http-executor:read-attribute(name=queue-size)
{
"outcome" => "success",
"result" => 1
jboss-cli的监控结果:随着请求负载的增加,先是thread从0->20,然后queue从1->50(这期间一直拒绝服务),然后thread从20->1000(这期间服务恢复)。
这就奇怪了。。为什么在queue=1-50这段期间,服务器会拒绝服务呢?已经创建的20个thread为什么没有去处理queue中的请求呢?
我们来看一下在服务器拒绝处理请求期间jboss的线程都在干什么,使用jstack {pid}
"web-container-thread-10" prio=10 tid=0x000000004017a800 nid=0x7da5 runnable [0x00007f03516bc000]
java.lang.Thread.State: RUNNABLE
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.read(SocketInputStream.java:129)
at org.apache.coyote.http11.InternalInputBuffer.fill(InternalInputBuffer.java:713)
at org.apache.coyote.http11.InternalInputBuffer.parseRequestLine(InternalInputBuffer.java:351)
at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:821)
at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:653)
at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:511)
at org.jboss.threads.SimpleDirectExecutor.execute(SimpleDirectExecutor.java:33)
at org.jboss.threads.QueueExecutor.runTask(QueueExecutor.java:808)
at org.jboss.threads.QueueExecutor.access$100(QueueExecutor.java:45)
at org.jboss.threads.QueueExecutor$Worker.run(QueueExecutor.java:828)
at java.lang.Thread.run(Thread.java:662)
at org.jboss.threads.JBossThread.run(JBossThread.java:122)
发现很多阻塞在读请求阶段。数量刚好等于core-threads的配置值20。
找了jboss-threads的源码来看。
at org.jboss.threads.QueueExecutor$Worker.run(QueueExecutor.java:828)对应下面的代码。
Worker是jboss线程池中处理请求的thread。每次来了新{请求A}并且jboss决定需要新建thread处理的时候,就会new Worker({请求A}),Worker的运行方式是先执行一次runTask({请求A}),然后无限循环从queue中take{请求N}并执行runTask({请求N})。而stack显示worker卡在了runTask({请求A}),所以没法去处理queue中的请求。
private class Worker implements Runnable {
private volatile Runnable first;
public Worker(final Runnable command) {
first = command;
}
public void run() {
final Lock lock = QueueExecutor.this.lock;
try {
Runnable task = first;
// Release reference to task
first = null;
runTask(task);
for (;;) {
// don't hang on to task while we possibly block waiting for the next one
task = null;
lock.lock();
try {
if (stop) {
// drain queue
if ((task = pollTask()) == null) {
return;
}
Thread.currentThread().interrupt();
} else {
// get next task
if ((task = takeTask()) == null) {
return;
}
}
} finally {
lock.unlock();
}
runTask(task);
Thread.interrupted();
}
} finally {
boolean last = false;
lock.lock();
try {
workers.remove(Thread.currentThread());
last = stop && workers.isEmpty();
} finally {
lock.unlock();
}
if (last) {
shutdownListenable.shutdown();
}
}
}
}
为什么worker会卡在了runTask({请求A})呢?看一下at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:821)对应的代码(只保留了关键部分)。原来如果一个http请求是keepAlive的,这个task就会一直在while循环中处理客户端发来的后续请求,直到socket超时或者处理的请求数超过maxKeepAliveRequests。
public boolean process(InputStream input, OutputStream output) {
...
int keepAliveLeft = maxKeepAliveRequests;
keepAlive = true;
while (started && !error && keepAlive) {
...
socket.setSoTimeout(soTimeout);
inputBuffer.parseRequestLine();
inputBuffer.parseHeaders();
prepareRequest();
...
if ( useKeepAliveAlgorithm ) {
if (maxKeepAliveRequests > 0 && --keepAliveLeft == 0) {
keepAlive = false;
}
} else {
keepAlive = false;
}
...
inputBuffer.nextRequest();
outputBuffer.nextRequest();
}
}
现在让我们来解释一下我们遇到的问题的现象:
一开始几乎没有负载时,都是core-thread在处理请求,由于很久才会来一个请求,所以20个core-threads在接到新的请求之前,旧的请求就已经socket读取超时,Worker中的第一个runtask()也就结束了。后续的请求刚进入queue就被core-thread拉走并处理了。
当负载上升到一个很低的程度后,20个core-threads还阻塞在等待socket超时的阶段,新的请求已经进来,这时新进入queue的请求就没人来拉走了,导致queue从0-50不断增大,服务器却一直拒绝服务,因为所有20个core-threads都被阻塞住了。
当负载继续增大,速度超过core-threads等待socket超时的速度,queue最终到达50后,QueueExecutor在处理新进来的请求时就会创建新的thread来处理请求,这些新的thread和core-threads一样,处理完第一个请求后也阻塞在socket读取超时阶段了,但是在客户端看来服务器确实是在响应了。
那么当thread到达1000后怎么办呢?不能通过创建thread来处理请求了,jboss对后续的请求是不是又会拒绝服务呢?这里来到了关键点,apache配置了最多只能有500个进程,也就是说jboss同时只会收到最多500个并发请求。之前由于500>20,很多新的请求不会发到20个core-thread阻塞等待的那些socket上,这20个core-thread只能无限等待下去直到socket超时。现在已经有了1000个线程了,由于500<1000,那么从apache来的请求一定是发到这1000个thread等待的那些socket上的,那这些thread直接就能在worker的第一个runtask()里处理新来的请求了,而不是从queue里拉取请求再处理。
解决方案有2种:
1. 设置thread在worker的第一个runtask()处理完socket的第一个请求后,不再阻塞等待后续请求,直接推出runtask()。这样thread就会很快从worker的第一个runtask()中返回,然后就能从queue中读取新来的请求进行处理了。
<system-properties>
<property name="org.apache.coyote.http11.Http11Protocol.MAX_KEEP_ALIVE_REQUESTS" value="1" />
</system-properties>
2. 配置core-threads=max-threads=1000的值>=apache的进程数,这样就根本不会有任何请求进入queue,所有的请求都在worker的第一个runtask中完成处理。
<subsystem xmlns="urn:jboss:domain:threads:1.1">
<thread-factory name="web-container-thread-factory" group-name="web-container-thread-group" thread-name-pattern="web-container-thread-%t"/>
<bounded-queue-thread-pool name="http-executor">
<core-threads count="1000"/>
<queue-length count="50"/>
<max-threads count="1000" />
<keepalive-time time="10" unit="minutes" />
<thread-factory name="web-container-thread-factory"/>
</bounded-queue-thread-pool>
</subsystem>