架構師必備:如何做容量預估和調優

Java烘焙師發表於2021-12-24

為了構建高併發、高可用的系統架構,壓測、容量預估必不可少,在發現系統瓶頸後,需要有針對性地擴容、優化。結合樓主的經驗和知識,本文做一個簡單的總結,歡迎探討。

1、QPS保障目標

一開始就要明確定義QPS保障目標,以此來推算所需的服務、儲存資源。可根據歷史同期QPS,或者平時峰值的2到3倍估算。

壓測目標示例:

  • qps達到多少時,服務的負載正常,如平均響應時間、95分位響應時間、cpu使用率、記憶體使用率、消費延遲低於多少
  • 不要讓任何一個環節成為瓶頸,需考慮服務例項、資料庫、Redis、ES、Hbase等資源

2、服務注意點

2.1、服務qps上限

服務qps上限 = 工作執行緒數 * 1/平均單次請求處理耗時

主要關注以下幾點:

(1)工作執行緒數,對qps起到了直接影響。

dubbo工作執行緒數配置舉例:
<dubbo:protocol name="dubbo" threadpool="fixed" threads="1000" />

(2)cpu使用率:跟服務是I/O密集型,還是計算密集型有關。

  • I/O密集型:呼叫多個下游服務,本身邏輯較簡單,cpu使用率不會很高,因此服務例項的個數不用很多
  • 計算密集型:本身邏輯很複雜,有較重的計算,cpu使用率可能飆升,因此可適當多部署一些服務例項

(3)網路頻寬:

  • 對於大量的小請求,基本無需考慮
  • 如果請求內容較大,多個併發可能打滿網路頻寬,如上傳圖片、視訊等。

以實際壓測為準。或者線上上調整權重,引導較多流量訪問1臺例項,記錄達到閾值時的qps,可估算出單例項的最大qps。

2.2、超時時間設定

漏斗型:從上到下,timeout時間建議由大到小設定,也即底層/下游服務的timeout時間不宜設定太大;否則可能出現底層/下游服務執行緒池耗盡、然後拒絕請求的問題(丟擲java.util.concurrent.RejectedExecutionException異常)
原因是上游服務已經timeout了,而底層/下游服務仍在執行,上游請求源源不斷打到底層/下游服務,直至執行緒池耗盡、新請求被拒絕,最壞的情況是產生級聯的雪崩,上游服務也耗盡執行緒池,無法響應新請求。
具體timeout時間,取決於介面的響應時間,可參考95分位、或99分位的響應時間,略微大一些。
dubbo超時時間示例:在服務端、客戶端均可設定,推薦在服務端設定預設超時時間,客戶端也可覆蓋超時時間;
<dubbo:service id="xxxService" interface="com.xxx.xxxService" timeout=1000 />
<dubbo:reference id="xxxService" interface="com.xxx.xxxService" timeout=500 />

2.3、非同步並行呼叫

如果多個呼叫之間,沒有順序依賴關係,為了提高效能,可考慮非同步並行呼叫。
dubbo非同步呼叫示例:

  1. 首先,需要配置consumer.xml,指定介面是非同步呼叫:
    <dubbo:reference id="xxxService" interface="com.xxx.xxxService" async=true />

  2. 然後,在程式碼中通過RpcContext.getContext().getFuture()獲取非同步呼叫結果Future物件:

    // 呼叫1先執行
    interface1.xxx();

    // 呼叫2、3、4無順序依賴,可非同步並行執行
    interface2.xxx();
    future2 = RpcContext.getContext().getFuture();
    interface3.xxx();
    future3 = RpcContext.getContext().getFuture();
    interface4.xxx();
    future4 = RpcContext.getContext().getFuture();

    // 獲取呼叫2、3、4的執行結果
    result2 = future2.get();
    result3 = future3.get();
    result4 = future4.get();
    // 此處會阻塞至呼叫2、3、4都執行完成,取決於執行時間最長的那個
    handleResult2(result2);
    handleResult3(result3);
    handleResult4(result4);

    // 呼叫5最後執行,會阻塞至前序操作都完成
    interface5.xxx();

2.4、強依賴、弱依賴

  • 強依賴呼叫:決不能跳過,失敗則拋異常、快速失敗
  • 弱依賴呼叫:決不能阻塞流程,失敗可忽略

2.5 降級

  • 粗粒度:開關控制,如對整個非關鍵功能降級,隱藏入口
  • 細粒度:呼叫下游介面失敗時,返回預設值

2.6 限流

超過的部分直接拋限流異常,萬不得已為之。

3、儲存資源注意點

3.1、放大倍數:1次核心操作,對應的資源讀寫次數、介面呼叫次數

例如:1次核心操作,查了3次快取、寫了1次快取、查了2次資料庫、寫了1次資料庫、發了1次MQ訊息、調了下游服務A的介面;

則對於讀快取放大倍數為3,寫快取放大倍數為1,讀資料庫放大倍數為2,寫資料庫放大倍數為1,MQ放大倍數為1,呼叫下游服務A的放大倍數為1。針對寫放大倍數,需要單獨考慮主庫是否扛得住放大倍數的qps。
需關注:

  • 讀、寫的放大倍數,要分開考慮,因為分散式架構通常是一主多從,一主需要支撐所有的寫QPS,多從可以支撐所有的讀QPS
  • DB讀放大倍數、DB寫放大倍數
  • Redis讀放大倍數、Redis寫放大倍數
  • MQ放大倍數
  • 介面呼叫放大倍數等

3.2、儲存資源QPS估算

儲存資源的QPS上限,跟機器的具體配置有關,8C32G機型的QPS上限當然要高於4C16G機型。下表為典型值舉例。

資源型別 單例項QPS數量級(典型值) 水平擴充套件方式 叢集總QPS估算
DB 幾千 分庫分表 例項個數*單例項QPS,其中例項個數的範圍是1~分庫個數(可達數百)
Redis 幾萬 Redis叢集 例項個數*單例項QPS,其中例項個數的範圍是1~分片個數(可達數百),總QPS可達百萬級
MQ 幾萬 partition拆分,每個分片最多被1個服務併發消費 例項個數*單例項QPS,其中例項個數的範圍是1~partition個數,總QPS可達百萬級
HBase 幾千? region拆分 例項個數*單例項QPS,其中例項個數的範圍是1~region個數
ES 幾千? shard拆分 例項個數*單例項QPS,其中例項個數的範圍是1~shard個數

相關文章