閱讀740 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Tomcat-connector的微調(3): processorCache與socket.processorCache

tomcat在處理每個連接時,Acceptor角色負責將socket上下文封裝為一個任務SocketProcessor然後提交給線程池處理。在BIO和APR模式下,每次有新請求時,會創建一個新的SocketProcessor實例(在之前的tomcat對keep-alive的實現邏輯裏也介紹過可以簡單的通過SocketProcessorSocketWrapper實例數對比socket的複用情況);而在NIO裏,為了追求性能,對SocketProcessor也做了cache,用完後將對象狀態清空然後放入cache,下次有新的請求過來先從cache裏獲取對象,獲取不到再創建一個新的。

這個cache是一個ConcurrentLinkedQueue,默認最多可緩存500個對象(見SocketProperties)。可以通過socket.processorCache來設置這個緩存的大小,注意這個參數是NIO特有的。

接下來在SocketProcessor執行過程中,真正的業務邏輯是通過一個org.apache.coyote.Processor的接口來封裝的,默認這個Processor的實現是org.apache.coyote.http11.Http11Processor。我們看一下SocketProcessor.process(...)方法的大致邏輯:

public SocketState process(SocketWrapper<S> wrapper, SocketStatus status) {
    ...
    // 針對長輪詢或upgrade情況
    Processor<S> processor = connections.get(socket);
    ...

    if (processor == null) {
        // 1) 嚐試從回收隊列裏獲取對象
        processor = recycledProcessors.poll();
    }
    if (processor == null) {
        // 2) 沒有再創建新的
        processor = createProcessor();
    }

    ...
    state = processor.process(wrapper);

    ...
    release(wrapper, processor, ...);

    ...
    return SocketState.CLOSED;
}

上麵的方法是在AbstractProtocol模板類裏,所以BIO/APR/NIO都走這段邏輯,這裏使用了一個回收隊列來緩存Processor,這個回收隊列是ConcurrentLinkedQueue的一個子類,隊列的長度可通過server.xml裏connector節點的processorCache屬性來設置,默認值是200,如果不做限製的話可以設置為-1,這樣cache的上限將是最大連接數maxConnections的大小。

在原有的一張ppt上加工了一下把這兩個緩存隊列所在位置標示了一下,圖有點亂,重點是兩個綠顏色的cache隊列:

圖中位於上麵的socket.processorCache隊列是NIO獨有的,下麵的processorCache是三種連接器都可以設置的。processorCache這個參數在並發量比較大的情況下也蠻重要的,如果設置的太小,可能引起瓶頸。我們模擬一下,看看這個瓶頸是怎麼回事。先修改server.xml裏的connector節點,把processorCache設置為0:

    <Connector port="7001"
           protocol="org.apache.coyote.http11.Http11NioProtocol"
           connectionTimeout="20000"
           redirectPort="8443" 
           processorCache="0"/>

啟動tomcat後,使用ab模擬並發請求:

$ ab -n100000 -c10 http://localhost:7001/main

然後在ab的執行過程中立刻執行jstack觀察堆棧信息,會發現一大半線程阻塞在AbstractConnectionHandler.registerAbstractConnectionHandler.unregister方法上:

"http-nio-7001-exec-11" #34 daemon prio=5 os_prio=31 tid=0x00007fd05ab05000 nid=0x8903 waiting for monitor entry [0x000000012b3b7000]
 java.lang.Thread.State: BLOCKED (on object monitor)
 at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.register(AbstractProtocol.java:746)
 - waiting to lock <0x00000007403b8950> (a org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler)
 at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.createProcessor(Http11NioProtocol.java:277)
 at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.createProcessor(Http11NioProtocol.java:139)
 at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:585)
 at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1720)
 at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1679)

 ...
"http-nio-7001-exec-4" #27 daemon prio=5 os_prio=31 tid=0x00007fd0593e3000 nid=0x7b03 waiting for monitor entry [0x000000012aca2000]
 java.lang.Thread.State: BLOCKED (on object monitor)
 at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.unregister(AbstractProtocol.java:773)
 - locked <0x00000007403b8950> (a org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler)
 at org.apache.coyote.AbstractProtocol$RecycledProcessors.offer(AbstractProtocol.java:820)
 at org.apache.coyote.http11.Http11NioProtocol$Http11ConnectionHandler.release(Http11NioProtocol.java:219)
 at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:690)
 at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1720)

registerunregister分別是在創建和回收processor的時候調用的;看一下createProcessor方法裏的大致邏輯:

public Http11NioProcessor createProcessor() {
    Http11NioProcessor processor = new Http11NioProcessor(...);
    processor.setXXX(...);
    ...

    // 這裏,注冊到jmx
    register(processor);
    return processor;
}

tomcat對jmx支持的非常好,運行時信息也有很多可以通過jmx獲取,所以在每個新連接處理的時候,會在創建processor對象的時候注冊一把,然後在processor處理完回收的時候再反注冊一把;但這兩個方法的實現都是同步的,同步的鎖是一個全局的ConnectionHandler對象,造成了多個線程會在這裏串行。

絕大部分應用沒有特別高的訪問量,通常並不需要調整processorCache參數,但對於網關或代理一類的應用(尤其是使用servlet3的情況)這個地方可以設置的大一些,比如調到1000或者-1。

最後更新:2017-05-23 17:02:58

  上一篇:go  深度學習的難點:神經網絡越深,優化問題越難
  下一篇:go  AKKA文檔(java版)——什麼是AKKA?