Tomcat 第五篇:請求處理流程(下)

1. 請求處理流程 AprEndPoint

順著上一篇接著聊,當一個請求發送到 Tomcat 以後,會由連接器 Connector 轉送至 AprEndPoint ,在 AprEndPoint 中調用了 startInternal() 方法,這個方法總共做了做了四件事兒:

  • LimitLatch 限制連接次數。
  • 創建了 poller 執行緒。
  • 創建了 sendfile 執行緒。
  • 創建了 acceptor 。

其中, pollersendfileacceptor 都是 AprEndPoint 的內部類,因為他們的父類都實現了 Runnable ,所以核心邏輯都在他們自己的 run() 方法中。

其中的涉及到的源程式碼太多了,我就是懶得往出列了,所以畫了下面這個圖給各位做個示意。

  • LimitLatch 是連接控制器,它負責控制最大連接數。
  • Acceptor 跑在一個單獨的執行緒中,它在一個死循環裡面通過調用 accept() 方法來接收新連接,會返回一個 long 類型的 socket ,然後將這個 socket 封裝成 AprSocketWrapper 對象。
  • Poller 本身也跑在一個單獨的執行緒中,它早內部維護了一個 SocketList 對象,這個對象中含有 socket 數組,它在一個死循環里不斷檢測 socket 的數據就緒狀態,一旦有 socket 可讀,就生成一個 SocketProcessor 任務對象扔給 Executor 去處理。
  • Executor 就是一個執行緒池,負責運行 SocketProcessor 任務類, SocketProcessorrun() 方法會調用 Http11Processor 來讀取和解析請求數據。

肯能有的朋友看完了,都不知道 AprEndPoint 或者說 Apr 這種連接模式是什麼。

稍微做下簡介:

APR(Apache Portable Runtime Libraries)是 Apache 可移植運行時庫,它是用 C 語言實現的,其目的是向上層應用程式提供一個跨平台的作業系統介面庫。Tomcat 可以用它來處理包括文件和網路 I/O,從而提升性能。

在 Tomcat8.5.x 中,默認的 I/O 模式使用的是 NIO ,使用的鏈接器是 org.apache.coyote.http11.Http11NioProtocol ,當然,由於是默認的,無需顯示配置,在 server.xml 中只需要這麼寫就可以了:

<Connector port="8080" protocol="HTTP/1.1"
            connectionTimeout="20000"
            redirectPort="8443" />

但是如果要換成 APR ,就需要這麼寫了:

<Connector port="8443" protocol="org.apache.coyote.http11.Http11AprProtocol"
            maxThreads="150" SSLEnabled="true" >
    <UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" />
    <SSLHostConfig>
        <Certificate certificateKeyFile="conf/localhost-rsa-key.pem"
                        certificateFile="conf/localhost-rsa-cert.pem"
                        certificateChainFile="conf/localhost-rsa-chain.pem"
                        type="RSA" />
    </SSLHostConfig>
</Connector>

接下來聊一個拷問靈魂的問題, APR 是如何提升性能的?

NioEndpoint 一樣, AprEndpoint 也實現了非阻塞 I/O,它們的區別是:NioEndpoint 通過調用 Java 的 NIO API 來實現非阻塞 I/O,而 AprEndpoint 是通過 JNI 調用 APR 本地庫而實現非阻塞 I/O 的。

Tomcat 的 Endpoint 組件在接收網路數據時需要預先分配好一塊 Buffer,所謂的 Buffer 就是位元組數組 byte[] ,Java 通過 JNI 調用把這塊 Buffer 的地址傳給 C 程式碼,C 程式碼通過作業系統 API 讀取 Socket 並把數據填充到這塊 Buffer。

Java NIO API 提供了兩種 Buffer 來接收數據: HeapByteBuffer 和 DirectByteBuffer 。

HeapByteBuffer 對象本身在 JVM 堆上分配,並且它持有的位元組數組 byte[] 也是在 JVM 堆上分配。但是如果用 HeapByteBuffer 來接收網路數據,需要把數據從內核先拷貝到一個臨時的本地記憶體,再從臨時本地記憶體拷貝到 JVM 堆,而不是直接從內核拷貝到 JVM 堆上。

數據從內核拷貝到 JVM 堆的過程中,JVM 可能會發生 GC , GC 過程中對象可能會被移動,也就是說 JVM 堆上的位元組數組可能會被移動,這樣的話 Buffer 地址就失效了。如果這中間經過本地記憶體中轉,從本地記憶體到 JVM 堆的拷貝過程中 JVM 可以保證不做 GC。

Tomcat 的 AprEndpoint 通過作業系統層面的 sendfile 特性解決了這個問題,sendfile 系統調用方式非常簡潔。

2. 請求處理流程 NioEndPoint

前面介紹了 AprEndpoint 的請求處理流程,我們在順便看下 Tomcat 默認的 NioEndPoint 處理流程。

實際上這兩個處理流程非常的相似,區別基本上是因為非阻塞 I/O 的實現方式。

  • Acceptor 中的 accept() 方法返回一個 Channel 對象,接著把 Channel 對象交給 Poller 去處理。
  • Poller 在內部維護一個 Channel 數組,它在一個死循環里不斷檢測 Channel 的數據就緒狀態,一旦有 Channel 可讀,就生成一個 SocketProcessor 任務對象扔給 Executor 去處理。每個 Poller 執行緒都有自己的 Queue 。每個 Poller 執行緒可能同時被多個 Acceptor 執行緒調用來註冊 PollerEventPoller 不斷的通過內部的 Selector 對象向內核查詢 Channel 的狀態,一旦可讀就生成任務類 SocketProcessor 交給 Executor 去處理。 Poller 的另一個重要任務是循環遍歷檢查自己所管理的 SocketChannel 是否已經超時,如果有超時就關閉這個 SocketChannel
  • Executor 是執行緒池,負責運行 SocketProcessor 任務類, SocketProcessorrun() 方法會調用 Http11Processor 來讀取和解析請求數據。 ServerSocketChannel 通過 accept() 接受新的連接, accept() 方法返回獲得 SocketChannel 對象,然後將 SocketChannel 對象封裝在一個 PollerEvent 對象中,並將 PollerEvent 對象壓入 PollerQueue 里,這是個典型的生產者 – 消費者模式, AcceptorPoller 執行緒之間通過 Queue 通訊。

參考

//jonhuster.blog.csdn.net/article/details/93297251

Tags: