1. 程式人生 > >Tomcat-connector的微調(3): processorCache與socket.processorCache

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。