轉自 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模式,如下圖
mainReactor:jetty從線程池中分配一個線程用於接受用戶的連接請求(ServerSocketChannel.accpet()),
這個線程就做一件事,接受用戶的連接,將channel註冊到selector中。
- _manager.dispatch(new Runnable()
- {
- public void run()
- {
- final ServerSocketChannel server=_acceptChannel;
- while (isRunning() && _acceptChannel==server && server.isOpen())
- {
- try
- {
- SocketChannel channel = server.accept();
- channel.configureBlocking(false);
- Socket socket = channel.socket();
- configure(socket);
- _manager.register(channel);
- }
- catch(IOException e)
- {
- Log.ignore(e);
- }
- }
- }
- });
而jetty的subReactor線程詢註冊進來的channel,將channel包裝成SelectChannelEndPoint對象加入到_endPoints。(可以把endpoint看作是一個連接)
- private ConcurrentMap<SelectChannelEndPoint,Object> _endPoints = new ConcurrentHashMap<SelectChannelEndPoint, Object>();
- public void doSelect() throws IOException {
- ...
- else if (change instanceof SocketChannel)
- {
- // Newly registered channel
- final SocketChannel channel=(SocketChannel)change;
- SelectionKey key = channel.register(selector,SelectionKey.OP_READ,null);
- SelectChannelEndPoint endpoint = createEndPoint(channel,key);
- key.attach(endpoint);
- endpoint.schedule();
- }
- ...
- }
爲什麼要加入到_endPoints,爲了對所有的endpoint做空閒檢查。
- public void doSelect() throws IOException {
- 。。。
- // Idle tick
- if (now-_idleTick>__IDLE_TICK)
- {
- _idleTick=now;
- final long idle_now=((_lowResourcesConnections>0 && selector.keys().size()>_lowResourcesConnections))
- ?(now+_maxIdleTime-_lowResourcesMaxIdleTime)
- :now;
- dispatch(new Runnable()
- {
- public void run()
- {
- for (SelectChannelEndPoint endp:_endPoints.keySet())
- {
- endp.checkIdleTimestamp(idle_now);
- }
- }
- });
- }
- 。。。
- }
這裏有個問題,回收動作需要從線程池中分配線程處理,而如果線程池中沒有空閒的線程時,那麼回收動作將無法正常進行。所以嘗試修改__IDLE_TICK到30毫秒(默認是400),希望能提高空閒檢查頻率,卻無法起效。
還有這個可惡的_endPoints對象,它將持有大量的endpoint,而這些endpoint又得不到及時處理,內存都被它消耗光。
我配置的最大線程池爲250,任務隊列長度無限制
- <Set name="ThreadPool">
- <!-- Default queued blocking threadpool -->
- <New class="org.eclipse.jetty.util.thread.QueuedThreadPool">
- <Set name="minThreads">10</Set>
- <Set name="maxThreads">250</Set>
- </New>
- </Set>
在jvm的young、old區達到100%時,250線程也都已經分配(可以用命令"jstack PID | grep "\"qtp" | wc -l"查看),但是很多都block住了;因爲線程運行過程中也會有對象創建,也需要一點內存空間,可已經沒有內存空間,杯具就這樣發生了。
如果把mainReactor比作人在喫東西,那麼subReator就是他的胃在消化,
大部分情況都是喫個7分飽,此時胃的消化能力很強,
一旦出現暴飲暴食,就會出現胃脹,消化能力反而減弱。
一個解決方案:
分析下來覺得jetty缺了胃反射功能,胃脹信息沒有即使反饋給大腦。
可以適當擴展一下mainReactor,看下面的代碼:
- _manager.dispatch(new Runnable()
- {
- public void run()
- {
- final ServerSocketChannel server=_acceptChannel;
- while (isRunning() && _acceptChannel==server && server.isOpen() && !_manager.isLowResourcesConnections())
- {
- try
- {
- SocketChannel channel = server.accept();
- channel.configureBlocking(false);
- Socket socket = channel.socket();
- configure(socket);
- _manager.register(channel);
- }
- catch(IOException e)
- {
- Log.ignore(e);
- }
- }
- }
- });
添加了_manager.isLowResourcesConnections()方法,嘴巴準備喫的時候要先問一下胃先。
subReactor添加一個新方法:
- public boolean isLowResourcesConnections() {
- // 這裏的判斷閥值是個大概的值。
- // 拿配置的閥值和第一個selector的keys大小做比較
- // 任何情況下第一個selector都是存在的,所以這個比較還是靠譜的。
- return _lowResourcesConnections < _selectSet[0].getSelector().keys().size();
- }
另外也可以配置自定義隊列加以限制。
<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>