常用高併發網路執行緒模型效能優化實現-體驗百萬級高併發執行緒模型設計

y123456yzzyz發表於2020-10-16

關於作者

前滴滴出行技術專家,現任OPPO 文件資料庫 mongodb 負責人,負責 oppo 千萬級峰值 TPS/ 十萬億級資料量文件資料庫 mongodb 研發和運維工作,一直專注於分散式快取、高效能服務端、資料庫、中介軟體等相關研發。後續持續分享《 MongoDB 核心原始碼設計、效能優化、最佳運維實踐》, Github 賬號地址 : https://github.com/y123456yz


前言:
   服務端通常需要支援高併發業務訪問,如何設計優秀的服務端網路IO 工作執行緒 / 程式模型對業務的高併發訪問需求起著至關重要的核心作用。

   本文總結了了不同場景下的多種網路IO 執行緒 / 程式模型,並給出了各種模型的優缺點及其效能優化方法,非常適合服務端開發、中介軟體開發、資料庫開發等開發人員借鑑。

1. 執行緒模型一 . 單執行緒網路 IO 複用模型

說明:

1. 所有網路 IO 事件 (accept 事件、讀事件、寫事件 ) 註冊到 epoll 事件集

2. 主迴圈中通過 epoll_wait 一次性獲取核心態收集到的 epoll 事件資訊,然後輪詢執行各個事件對應的回撥。

3. 事件註冊、 epoll_wait 事件獲取、事件回撥執行全部由一個執行緒處理

 

1.1 一個完整請求組成

   一個完整的請求處理過程主要包含以下幾個部分:

    步驟1 :通過 epoll_wait 一次性獲取網路 IO 事件

    步驟2 :讀取資料及協議解析

    步驟3 :解析成功後進行業務邏輯處理,然後應答客戶端

1.2 該網路執行緒模型缺陷

1.   所有工作都由一個執行緒執行,包括 epoll 事件獲取、事件處理 ( 資料讀寫 ) 、只要任一一個請求的事件回撥處理阻塞,其他請求都會阻塞。例如 redis hash 結構,如果 filed 過多,假設一個 hash key 包含數百萬 filed ,則該 Hash key 過期的時候,整個 redis 阻塞。

2. 單執行緒工作模型, CPU 會成為瓶頸,如果 QPS 過高,整個 CPU 負載會達到 100% ,時延抖動厲害。

 

1.3 典型案例

1.  redis 快取

2.  推特快取中介軟體twemproxy

 

1.4 主迴圈工作流程

1.  while (1) {  

2.      //epoll_wait等待網路事件,如果有網路事件則返回,或者超時範圍  

3.      size_t numevents=  epoll_wait();  

4.    

5.      //遍歷前面epoll獲取到的網路事件,執行對應事件回撥  

6.      for (j = 0; j < numevents; j++) {  

7.           if(讀事件) {  

8.              //讀資料  

9.              readData()  

10.              //解析  

11.              parseData()  

12.              //讀事件處理、讀到資料後的業務邏輯處理  

13.              requestDeal()  

14.           } else if(寫事件) {  

15.              //寫事件處理,寫資料邏輯處理  

16.              writeEentDeal()  

17.           } else {  

18.                  //異常事件處理  

19.                  errorDeal()  

20.           }  

21.      }  

22.  }  

 

     說明: 後續多執行緒/ 程式模型中,每個執行緒 / 程式的主流程和該 while() 流程一致。

1.5 redis 原始碼分析及非同步網路 IO 複用精簡版 demo

由於之前工作需要,需要對redis 核心做二次優化開發,因此對整個 redis 程式碼做了部分程式碼註釋,同時把 redis 的網路模組獨立出來做成了簡單 demo ,該 demo 對理解 epoll 網路事件處理及 Io 複用實現會有幫助,程式碼比較簡短,可以參考如下地址 :

redis 原始碼詳細註釋分析

redis 網路模組精簡版 demo

推特快取中介軟體twemproxy 原始碼分析實現

2. 執行緒模型二 . listener+ 固定 worker 執行緒

    該執行緒模型圖如下圖所示:

說明:

1. listener 執行緒負責接受所有的客戶端連結

2. listener 執行緒每接收到一個新的客戶端連結產生一個新的 fd ,然後通過分發器傳送給對應的工作執行緒 (hash 方式 )

3. 工作執行緒獲取到對應的新連結 fd 後,後續該連結上的所有網路 IO 讀寫都由該執行緒處理

4. 假設有 32 個連結,則 32 個連結建立成功後,每個執行緒平均處理 4 個連結上的讀寫、報文處理、業務邏輯處理

 

2.1 該網路執行緒模型缺陷

  1.   進行 accept 處理的 listener 執行緒只有一個,在瞬間高併發場景容易成為瓶頸
  2. 一個執行緒通過 IO 複用方式處理多個連結 fd 的資料讀寫、報文解析及後續業務邏輯處理,這個過程會有嚴重的排隊現象,例如某個連結的報文接收解析完畢後的內部處理時間過長,則其他連結的請求就會阻塞排隊

2.2 典型案例

    memcache 快取,適用於內部處理比較快的快取場景、代理中間場景。 memcache 原始碼實現中文分析可以詳見 : memcache 原始碼實現分析

3. 執行緒模型三 . 固定 worker 執行緒模型 (reuseport)

該模型原型圖如下:

說明:

1. Linux kernel 3.9 開始支援 reuseport 功能,核心協議棧每獲取到一個新連結自動均衡分發給使用者態 worker 執行緒。

2. 該模型解決了模型一的 listener 單點瓶頸問題 多個程式/ 執行緒同時做為 listener ,都可以 accept 客戶端新連結。

 

3.1 該網路程式 / 執行緒模型缺陷
   reuseport 支援後,核心通過負載均衡的方式分發不同新連結到多個使用者態 worker 程式 / 執行緒 每個程式/ 執行緒 通過IO 複用方式處理多個 客戶端新 連結fd 的資料讀寫、報文解析、解析後的業務邏輯處理 每個工作程式/ 執行緒同時處理多個連結的請求 如果 某個連結的報文接收解析完畢後的內部處理時間 過長 ,則其他連結的請求就會阻塞排隊

   該模型雖然解決了 listener 單點瓶頸問題 但是工作執行緒內部的排隊問題沒有解決。

   不過,Nginx 作為七層轉發代理,由於都是記憶體處理,所以內部處理時間比較短,所以適用於該模型。

3.2 典型案例

1. nginx(nginx 用的是程式,模型原理一樣 ) ,該模型適用於內部業務邏輯簡單的場景,如 nginx 代理等

2. reuseport 支援效能提升過程可以參考我另一篇分享:       https://my.oschina.net/u/4087916/blog/3016162

Nginx 多程式高併發、低時延、高可靠機制在快取 (redis memcache)twemproxy 代理中的應用

nginx 原始碼中文註釋分析

4. 執行緒模型四. 一個連結一個執行緒模型

該執行緒模型圖如下圖:

說明:

   1. listener 執行緒負責接受所有的客戶端連結

   2. listener 執行緒每接收到一個新的客戶端連結就建立一個執行緒,該執行緒只負責處理該連結上的資料讀寫、報文解析、業務邏輯處理。

4.1 該網路執行緒模型缺陷:

1. 一個連結建立一個執行緒,如果 10 萬個連結,那麼就需要 10 萬個執行緒,執行緒數太多,系統負責、記憶體消耗也會很多

2. 當連結關閉的時候,執行緒也需要銷燬,頻繁的執行緒建立和消耗進一步增加系統負載

 

4.2 典型案例:

   1. mysql 預設方式、 mongodb 同步執行緒模型配置,適用於請求處理比較耗時的場景,如資料庫服務

   2. Apache web 伺服器,該模型限制了 apache 效能, nginx 優勢會更加明顯

5. 執行緒模型五 . listener+ 動態 worker 執行緒 ( 單佇列 )

該執行緒模型圖如下圖所示:

說明:

1. listener 執行緒接收到一個新連結 fd 後,把該 fd 交由執行緒池處理,後續該連結的所有讀寫、報文解析、業務處理都由執行緒池中多個執行緒處理。

2. 該模型把一次請求轉換為多個任務 ( 網路資料讀寫、 讀取資料 後的業務邏輯處理) 入隊到全域性佇列,執行緒池中的執行緒從佇列中獲取任務執行。

3. 同一個請求訪問被拆分為多個任務,一次請求可能由多個執行緒處理。

3. 當任務太多,系統壓力大的時候,執行緒池中執行緒數動態增加

4. 當任務減少,系統壓力減少的時候,執行緒池中執行緒數動態減少

5.1 工作執行緒執行時間相關的幾個統計:

     T1: 呼叫底層 asio 庫接收一個完整 mongodb 報文的時間

     T2: 接收到報文後的後續所有處理 ( 含報文解析、認證、引擎層處理、傳送資料給客戶端等 )

     T3: 執行緒等待資料的時間 ( 例如:長時間沒有流量,則現在等待讀取資料 )

5.2 單個工作執行緒如何判斷自己處於”空閒”狀態:

      執行緒執行總時間=T1 + T2 +T3 ,其中 T3 是無用等待時間。如果 T3 的無用等待時間佔比很大,則說明執行緒比較空閒。工作執行緒每一次迴圈處理後判斷有效時間佔比,如果小於指定閥值,則自己直接 exit 退出銷燬

5.3 如何判斷執行緒池中工作執行緒“太忙”:

   控制執行緒專門用於判斷執行緒池中工作執行緒的壓力情況,以此來決定是否線上程池中建立新的工作執行緒來提升效能。

   控制執行緒每過一定時間迴圈檢查執行緒池中的執行緒壓力狀態,實現原理就是簡單的實時記錄執行緒池中的執行緒當前執行情況,為以下兩類計數:匯流排程數_threadsRunning 、當前正在執行 task 任務的執行緒數 _threadsInUse 。如果 _threadsRunning=_threadsRunning ,說明所有工作執行緒當前都在處理 task 任務,執行緒池中執行緒壓力大,這時候控制執行緒就開始增加執行緒池中執行緒數。  

    該模型詳細原始碼實現過程更多細節詳見:https://my.oschina.net/u/4087916/blog/4295038

5.4 該網路執行緒模型缺陷:

1.   執行緒池獲取任務執行,有鎖競爭,這裡就會成為系統瓶頸

5.5 典型案例:

mongodb 動態 adaptive 執行緒模型,適用於請求處理比較耗時的場景,如資料庫服務

該模型詳細原始碼優化分析實現過程參考:

https://my.oschina.net/u/4087916/blog/4295038

Mongodb 網路傳輸處理原始碼實現及效能調優 - 體驗核心效能極致設計

 

6. 執行緒模型六. listener+ 動態 worker 執行緒 ( 多佇列 ) -mongodb 網路執行緒模型優化實踐

該執行緒模型圖如下:

說明:

把一個全域性佇列拆分為多個佇列,任務入隊的時候按照hash 雜湊到各自的佇列,工作執行緒獲取獲取任務的時候,同理通過 hash 的方式去對應的佇列獲取任務,通過這種方式減少鎖競爭,同時提升整體效能。

6.1 典型案例:

mongodb 核心多佇列 adaptive 執行緒模型優化,效能有很好的提升,適用於請求處理比較耗時的場景,如資料庫服務。該模型詳細原始碼優化分析實現過程參考:

Mongodb 網路傳輸處理原始碼實現及效能調優 - 體驗核心效能極致設計

 

6.2 疑問?為啥 mysql mongodb 等資料庫沒有利用核心的 reuseport 特殊 - 多執行緒同時處理 accept 請求?

   答: 實際上所有服務都可以利用這一特性,包括資料庫服務(mongodb mysql ) 。但是因為資料庫服務訪問時延一般都是 ms 級別,如果 reuseport 特性利用起來,時延會有幾十 us 的效能提升,這相比資料庫內部處理的 ms 級時延,這幾十 us 的效能提升,基本上可以忽略掉,這也是大部分資料庫服務沒有支援該功能的原因。

   快取,代理等中介軟體,由於本身內部處理時間就比較小,也是us 級別,所以需要充分利用該特性。



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69984922/viewspace-2727336/,如需轉載,請註明出處,否則將追究法律責任。

相關文章