贪吃的jetty被撑死了

时间:2022-09-05 14:46:36

转自 http://benni82.iteye.com/blog/875494


在大量请求并且请求处理时间较长的情况下,jetty的nio模式会导致容器运行缓慢。

 

测试方法:

用apache ab对jetty容器发出大规模持续的并发请求,

用命令“jstat -gcutil -h 10 PID 1000"查看GC情况,等到young、old区到100%时停止施压。

 

用“jmap -histo PID | less" 可以看到大量的SelectChannelEndPoint对象。


分析一下原因:

首先介绍一下jetty的nio模式,如下图

贪吃的jetty被撑死了

 

mainReactor:jetty从线程池中分配一个线程用于接受用户的连接请求(ServerSocketChannel.accpet()),

这个线程就做一件事,接受用户的连接,将channel注册到selector中。

 

Jetty mainreactor代码   贪吃的jetty被撑死了
  1. _manager.dispatch(new Runnable()  
  2. {  
  3.     public void run()  
  4.     {  
  5.         final ServerSocketChannel server=_acceptChannel;  
  6.         while (isRunning() && _acceptChannel==server && server.isOpen())  
  7.         {  
  8.             try  
  9.             {  
  10.                 SocketChannel channel = server.accept();  
  11.                 channel.configureBlocking(false);  
  12.                 Socket socket = channel.socket();  
  13.                 configure(socket);  
  14.                 _manager.register(channel);  
  15.             }  
  16.             catch(IOException e)  
  17.             {  
  18.                 Log.ignore(e);  
  19.             }  
  20.         }  
  21.     }  
  22. });  
    

 

而jetty的subReactor线程询注册进来的channel,将channel包装成SelectChannelEndPoint对象加入到_endPoints。(可以把endpoint看作是一个连接)

 

Selectmanager.selectset代码   贪吃的jetty被撑死了
  1. private ConcurrentMap<SelectChannelEndPoint,Object> _endPoints = new ConcurrentHashMap<SelectChannelEndPoint, Object>();    
  2. public void doSelect() throws IOException {  
  3. ...  
  4.     else if (change instanceof SocketChannel)  
  5.     {  
  6.         // Newly registered channel  
  7.         final SocketChannel channel=(SocketChannel)change;  
  8.         SelectionKey key = channel.register(selector,SelectionKey.OP_READ,null);  
  9.         SelectChannelEndPoint endpoint = createEndPoint(channel,key);  
  10.         key.attach(endpoint);  
  11.         endpoint.schedule();  
  12.     }  
  13. ...  
  14. }  
 

 

为什么要加入到_endPoints,为了对所有的endpoint做空闲检查。

 

Endpoint空闲检查代码   贪吃的jetty被撑死了
  1. public void doSelect() throws IOException {  
  2. 。。。  
  3. // Idle tick  
  4. if (now-_idleTick>__IDLE_TICK)  
  5. {  
  6.     _idleTick=now;  
  7.       
  8.     final long idle_now=((_lowResourcesConnections>0 && selector.keys().size()>_lowResourcesConnections))  
  9.         ?(now+_maxIdleTime-_lowResourcesMaxIdleTime)  
  10.         :now;  
  11.           
  12.     dispatch(new Runnable()  
  13.     {  
  14.         public void run()  
  15.         {  
  16.             for (SelectChannelEndPoint endp:_endPoints.keySet())  
  17.             {  
  18.                 endp.checkIdleTimestamp(idle_now);  
  19.             }  
  20.         }  
  21.     });  
  22. }  
  23. 。。。  
  24. }  
 

 

这里有个问题,回收动作需要从线程池中分配线程处理,而如果线程池中没有空闲的线程时,那么回收动作将无法正常进行。所以尝试修改__IDLE_TICK到30毫秒(默认是400),希望能提高空闲检查频率,却无法起效。

 

还有这个可恶的_endPoints对象,它将持有大量的endpoint,而这些endpoint又得不到及时处理,内存都被它消耗光。

 

我配置的最大线程池为250,任务队列长度无限制

 

线程池配置代码   贪吃的jetty被撑死了
  1. <Set name="ThreadPool">  
  2.   <!-- Default queued blocking threadpool -->  
  3.   <New class="org.eclipse.jetty.util.thread.QueuedThreadPool">  
  4.     <Set name="minThreads">10</Set>  
  5.     <Set name="maxThreads">250</Set>  
  6.   </New>  
  7. </Set>  

在jvm的young、old区达到100%时,250线程也都已经分配(可以用命令"jstack PID | grep "\"qtp" | wc -l"查看),但是很多都block住了;因为线程运行过程中也会有对象创建,也需要一点内存空间,可已经没有内存空间,杯具就这样发生了。

 

如果把mainReactor比作人在吃东西,那么subReator就是他的胃在消化,

大部分情况都是吃个7分饱,此时胃的消化能力很强,

一旦出现暴饮暴食,就会出现胃胀,消化能力反而减弱。


一个解决方案:

分析下来觉得jetty缺了胃反射功能,胃胀信息没有即使反馈给大脑。

可以适当扩展一下mainReactor,看下面的代码:

扩展后的mainreactor代码   贪吃的jetty被撑死了
  1. _manager.dispatch(new Runnable()  
  2. {  
  3.     public void run()  
  4.     {  
  5.         final ServerSocketChannel server=_acceptChannel;  
  6.         while (isRunning() && _acceptChannel==server && server.isOpen() && !_manager.isLowResourcesConnections())  
  7.         {  
  8.             try  
  9.             {  
  10.                 SocketChannel channel = server.accept();  
  11.                 channel.configureBlocking(false);  
  12.                 Socket socket = channel.socket();  
  13.                 configure(socket);  
  14.                 _manager.register(channel);  
  15.             }  
  16.             catch(IOException e)  
  17.             {  
  18.                 Log.ignore(e);  
  19.             }  
  20.         }  
  21.     }  
  22. });  

 添加了_manager.isLowResourcesConnections()方法,嘴巴准备吃的时候要先问一下胃先。

 

subReactor添加一个新方法:

Java代码   贪吃的jetty被撑死了
  1. public boolean isLowResourcesConnections() {  
  2. // 这里的判断阀值是个大概的值。  
  3. // 拿配置的阀值和第一个selector的keys大小做比较  
  4. // 任何情况下第一个selector都是存在的,所以这个比较还是靠谱的。  
  5.     return _lowResourcesConnections < _selectSet[0].getSelector().keys().size();  
  6. }  

另外也可以配置自定义队列加以限制。

<New class="org.eclipse.jetty.util.thread.QueuedThreadPool">
      <Arg>
           <New class="java.util.concurrent.ArrayBlockingQueue">
              <Arg type="int">6000</Arg>
           </New>
      </Arg>
        <Set name="minThreads">10</Set>
        <Set name="maxThreads">200</Set>
        <Set name="detailedDump">false</Set>
      </New>