介面流量突增,如何做好效能調優?

2022-07-25 12:02:01

大家好,我是樹哥!

對於提供介面服務的應用來說,很多都是用 SpringBoot 預設的 Servlet 容器 Tomcat。在一開始上線的時候,由於大多數流量較小,我們也並不會為 Tomcat 做專門的引數調整。但隨著流量越來越大,應用的各項效能指標越來越差,此時我們大多數都會選擇擴容。

除了擴容之外,我們還可以選擇對 Tomcat 進行效能調優,從而在不增加成本的情況下提升效能。如果面試官問你,流量突增你們一般怎麼做,你只會答擴容可就太差勁了。今天樹哥就跟大家簡單分享下,如何對 Tomcat 進行簡單地效能調優,從而提升應用的效能!

元件架構

要對 Tomcat 進行效能調優,我們需要先了解其元件架構。Tomcat 的元件架構如下圖所示:

從上圖可以看到,Tomcat 將其業務抽象成了 Server、Service、Connector、Container 等等元件,每個元件都有不同的作用。

  • Server 元件。 Server 元件是 Tomcat 最外層的元件,該元件是 Tomcat 範例本身的抽象,代表著 Tomcat 自身。一個 Server 元件可以有一個或多個 Service 元件。
  • Service 元件。 Service 元件是 Tomcat 中一組提供服務、處理請求的元件,一個 Service 元件可以有多個 Connector 聯結器和一個 Container,有多個 Connector 表示其可以同時使用多種協定接收使用者請求。
  • Connector 元件。 Connector 負責處理使用者端的連線,它提供各種服務協定支援,包括:BIO、NIO、AIO 等等。其存在的價值在於,為 Container 容器遮蔽了多協定的複雜性,統一了 Container 容器的處理標準。
  • Container 元件。 Container 元件是負責具體業務邏輯處理的容器,當 Connector 元件與使用者端建立連線後,便會將請求轉發給 Container 元件的 Engine 元件處理。

到這裡,Tomcat 的核心元件基本上講完了。實際上 Container 元件裡還細分了很多元件,其實對業務的抽象,感興趣的可以繼續看看。

  • Engine 元件。 Engine 元件表示可執行的 Servlet 範例,包含了 Servlet 容器的核心功能,其可以有一個或多個虛擬主機(Host)。其主要功能是將請求委託給合適的虛擬主機處理,即根據 URL 路徑的設定匹配到合適的虛擬主機處理。
  • Host 元件。 Host 元件負責執行多個應用,其負責安裝這些應用,其主要作用是解析 web.xml 檔案,並將其匹配到對應的 Context 元件。
  • Context 元件。 Context 元件代表具體的 Web 應用程式本身,其最重要的功能就是管理裡面的 Servlet 範例。一個 Context 可以有一個或者多個 Servlet 範例。
  • Wrapper 元件。 一個 Wrapper 元件代表一個 Servlet,它負責管理一個 Servlet,包括 Servlet 的裝載、初始化、執行以及資源回收。Wrapper 是最底層的容器。

可以看到,Host 是虛擬主機的抽象,Context 是應用程式的抽象,Wrapper 是 Servlet 的抽象,而 Engine 則是處理層的抽象。

核心引數

在瞭解核心引數之前,我們我們需要大致瞭解一下 Tomcat 對於請求的處理流程。Tomcat 對請求的處理流程如下所示:

  • 首先,使用者端向 Tomcat 伺服器發起請求,Connector 元件監聽到請求,於是與使用者端建立起連線。
  • 接著,Connector 將請求封裝後轉發給 Engine 元件處理。
  • 最後,Engine 元件處理完之後將結果返回給 Connector,Connector 元件再將結果返回給使用者端。

上述過程可以用如下示意圖來表示:

在上面的示意圖中有三個非常關鍵的核心引數,這幾個關鍵的引數也是效能調優的關鍵,它們分別是:

  1. acceptCount:當 Container 執行緒池達到最大數量且沒有空閒執行緒,同時 Connector 佇列達到最大數量時,作業系統最多能接受的連線數。
  2. maxConnections:當 Container 執行緒池達到最大數量且沒有空閒執行緒時,Connector 的佇列能接收的最大執行緒數。
  3. maxThreads: Container 執行緒池的處理執行緒的最大數量。

從上面三個引數的含義我們可以知道如下幾點結論:

  1. 使用者端並不是直接與 Tomcat 的 Connector 元件建立聯絡的,而是先與作業系統建立,然後再移交給 Connector 的。這點很重要,不然你就無法理解 acceptCount 這個引數。
  2. 不僅僅 Connector 元件中有佇列,作業系統中也有佇列來臨時儲存與使用者端的連線,這也是很關鍵的點。
  3. 我們所說的執行緒池,指的是 Container 這個容器裡的執行緒池。

明白這三個核心引數的含義是非常重要的,不然沒有辦法進行後續的效能調優工作。

maxThreads

我們知道 maxThreads 指的是請求處理執行緒的最大數量,在 Tomcat7 和 Tomcat8 中都是預設 200 個。

對於這個引數的設定,需要根據任務的執行內容去調整,一般來說計算公式為:最大執行緒數 = ((IO時間 + CPU時間)/CPU時間) * CPU 核數。這個公式的思路其實很簡單,就是最大化利用 CPU 的資源。一個任務的耗時分為 IO 耗時和 CPU 耗時,基本上 IO 耗時是最多的,這時候 CPU 是沒事幹的。

因此如果可以讓 CPU 在任務等待 IO 的時候處理其他任務,那麼 CPU 利用率不就上來了麼。一般來說,由於 IO 耗時遠大於 CPU 耗時,因此根據公式計算出來的 maxThreads 數都會遠大於 CPU 核數,這是很正常的。

要注意的是,這個數值也不是越高越好。因為一旦執行緒數太多了,CPU 需要進行上下文切換,這就消耗了一部分 CPU 資源。因此最好的辦法是用上述公式去計算一個基準值,隨後再進行壓力測試,去調整到一個合理的值。一般來說,如果調高了 maxThreads 的值,但是吞吐量沒有提升或者下降的話,那麼表明可能到達了了瓶頸了。

maxConnections

maxConnections 指的是當執行緒池的執行緒達到最大值,並且都在忙的時候,Connector 中的佇列最多能容納多少個連線。一般來說,我們都要設定一個合理的數值,不能讓其無限制堆積。因為 Tomcat 的處理能力肯定是有限的,到達一定程度肯定就處理不過來了,因此你堆積太多了也沒啥用,反而會造成記憶體堆積,最終導致記憶體溢位 OOM 的發生。

一般來說,一個經驗值是可以設定成為 maxThreads 同樣的大小。 我想這樣也是比較合理的,因為在佇列中的連線最多隻需要等待執行緒處理一個任務的時間即可,不會等待太久,響應時間也不會太長。如果你想縮短響應時間,那麼可以將 maxConnections 調低於 maxThreads 一些,這樣可以降低一些響應時間。但要注意的是,如果降得太低的話,可能就會嚴重降低效能,降低吞吐量。

acceptCount

acceptCount 指的是當 Container 執行緒池達到最大數量且沒有空閒執行緒,同時 Connector 佇列達到最大數量時,作業系統最多能接受的連線數。 當佇列中的個數達到最大值後,進來的請求一律被拒絕,預設值是 100。這可以理解成是作業系統的一種自我保護機制吧,堆積太多無法處理,那就直接拒絕掉,保護自身資源。

這個引數的調優資料比較少,但根據其含義,這個值不建議比 maxConnections 大。 因為在這個佇列中的連線,是需要等待的。如果數值太大,就說明會有很多連線沒有被處理。連線越多,那麼其等待的時間就越長,其響應時間就越慢。如果你想響應時間短一些,或許應該調低一下這個值。

有同學會疑惑,為啥有了 maxConnections 了還要有 acceptCount 呢?這不是重複了麼?其實在 BIO 的時代,這兩個數值基本都是相同的。我猜是因為後面出現了 NIO、AIO 等技術,作業系統可以接受更多的使用者端連線了。於是就可以先讓作業系統先建立連線快取著,隨後 Connnector 直接從作業系統處獲取連線即可,這樣就不需要等待作業系統進行耗時的 TCP 連線了,從而提高了效率。

除了上面這三個引數之外,還有幾個非核心引數,但我覺得還是有些作用的。

  • connectionTimeout 引數, 表示建立連線後的等待超時時間,如果超過這個時間,那麼就會直接返回超時。
  • minSpareThreads 引數, 表示最小存活執行緒數,也就是如果沒有請求了,那麼最低要保持幾個執行緒存活。這個引數與是否有突發流程相關聯,在有突發流量的情況下,如果這個數值太低,那麼就會導致瞬時的響應時間比較長。

總結

今天我們分享了 Tomcat 的核心元件,接著講解了 Tomcat 處理請求過程時的 3 個核心引數及其調優經驗。

對於 maxThreads 引數而言,如果按照公式計算的話,我們需要獲取 IO 時間和 CPU 時間,但實際上這兩個值並不是很好獲取。所以一般情況下,我們可以通過壓測的方式來獲得一個比較合適的 maxThreads。

對於 maxConnections 引數而言,可以設定一個與 maxThreads 相同的值,再根據具體情況進行調整。如果想降低響應時間,那麼可以稍微調低一些,否則可以調高一些。對於 acceptCount 引數而言,其調優邏輯與 maxConnections 類似,可以設定與 maxConnections 相似,再根據對相應時間的要求,做一個微調。

好了,這就是今天的分享了。

如果你喜歡這篇文章,請幫忙點贊轉發告訴我,感謝~

參考資料