作者:郭奧門
愛可生 DBLE 研發成員,負責分散式資料庫中介軟體的新功能開發,回答社群/客戶/內部提出的一般性問題。
本文來源:原創投稿
*愛可生開源社群出品,原創內容未經授權不得隨意使用,轉載請聯絡小編並註明來源。
背景
在實際生產環境,專案上線初期流量比較小,等後面專案流量漲上來, dble 內原有的執行緒配置可能支撐不了上游的壓力,此時可能會遇到一系列效能問題,這時就需要調大 processors、backendProcessors 等執行緒池引數,並根據預期指標及實際執行緒使用情況多次調整至最優。
在之前,修改完配置中的執行緒數之後需要重啟才能讓配置生效,但這種方式不是很靈活,甚至可能會影響上游的使用。dble也是考慮到這一點,在3.21.06.* 版本提供了不重啟調整執行緒池數目的方式
命令
update dble_information.dble_thread_pool set core_pool_size = 2 where name = 'BusinessExecutor';
注意:
- 支援動態調整的執行緒池為:businessExecutor 、writeToBackendExecutor 、processors(nio 場景下:bootstrap.cnf 中的 usingAIO 值為0)、backendProcessors(nio場景下)、backendBusinessExecutor 、complexQueryExecutor
- 不支援動態調整 AIO 場景下的 processors 、backendProcessors 對應的執行緒池
- 由於 JDK 原生執行緒池(ThreadPoolExecutor)擴縮容機制問題,新建的執行緒和即將被回收的執行緒需要一定的時機才會被處理,所以設定 core_pool_size 並不會立即通過 dble_thread_pool 表查到,但此時使用該執行緒池不受影響
- 儘管執行緒池數目可以允許不停機的方式調整,但為了防止出現未知問題,建議不要在流量大的時候調整
原理解讀
dble 內執行緒的使用方式
當前 dble 內使用執行緒池主要包含兩種方式:一是 JDK 內建執行緒池,另外是外接佇列 +JDK 內建執行緒池,下面我們簡單講一下兩種方式的原理
外接佇列+執行緒池
DBLE 為了高併發、響應快等特點,在網路 IO 層面採用了經典的主從 Reactor 多執行緒模型,這裡只說明 Reactor 模型如何在 DBLE 中落地,不清楚模型原理的同學可以參閱:徹底搞懂 Reactor 模型和 Proactor 模型(文末有連結)
DBLE 目前網路模型如上圖所示:
1、Reactor 主執行緒 —— NIOAcceptor 通過 select、accept 事件讀取並初步處理 client 連線,並通過 frontRegisterQueue 佇列傳遞給 Reactor 子執行緒
2、Reactor 子執行緒 —— RW 從外接佇列中(非執行緒池內部佇列,為了區分稱此時佇列為外接佇列)取到連線並註冊到當前子執行緒中,後續通過read方法讀取資料包並通過 frontHandlerQueue 佇列或本地佇列傳遞給工作執行緒
3、工作執行緒池內的子執行緒從外接佇列中接收到任務,經過後續的一系列分析處理後,將結果經過 writeQueue 佇列傳遞給 writeToBackendExecutor 執行緒,繼而傳送給前端 client ,或經過本地佇列傳遞給 backendBuinessExecutor/complexQueryExecutor 執行緒直接將結果返回給前端
從 DBLE 網路模型可以看出其內部使用佇列+執行緒池的方式來分發處理任務,除此之外在業務處理的過程中也是大量使用了執行緒池來處理一些耗時的任務
JDK 內建執行緒池
dble 內執行緒池是藉助了 java 的 ThreadPoolExecutor 類,其執行流程如下:
執行緒池在內部實際上構建了一個生產者消費者模型,將執行緒和任務兩者解耦,並不直接關聯,從而良好的緩衝任務,複用執行緒。執行緒池的執行主要分成兩部分:任務管理、執行緒管理。任務管理部分充當生產者的角色,當任務提交後,執行緒池會判斷該任務後續的流轉:
(1)直接申請執行緒執行該任務;
(2)緩衝到佇列中等待執行緒執行;
(3)拒絕該任務。執行緒管理部分是消費者,它們被統一維護線上程池內,根據任務請求進行執行緒的分配,當執行緒執行完任務後則會繼續獲取新的任務去執行,最終當執行緒獲取不到任務的時候,執行緒就會被回收。
結合 dble 中目前的結構,其內部主要執行緒使用的方式為:
- businessExecutor、writeToBackendExecutor 相關執行緒通過外接佇列+執行緒池實現排程,在 dble 啟動時初始化執行緒池,執行緒內部通過輪詢方式獲取任務(啟動時建立的執行緒稱之為常駐執行緒)
- processors 、backendProcessors 相關執行緒在 nio 情況下通過外接佇列+執行緒池實現排程;在 aio 情況下通過 AsynchronousChannelGroup 機制(內建執行緒池)實現執行緒管理
- backendBusinessExecutor 相關執行緒在效能模式下(usePerformanceMode=1),通過外接佇列+執行緒池實現排程;反之在後續任務到達時由執行緒池直接排程
- complexQueryExecutor 相關執行緒由執行緒池直接排程(不是在啟動時建立的執行緒稱之為非常駐執行緒,隨任務朝生暮死)
具體實現
為了動態的調整執行緒池的數目,保證擴縮容之後任務都能正常被處理,需要針對以上兩種方式作單獨處理,具體實現方式如下:
執行緒池
JDK 原生執行緒池 ThreadPoolExecutor 提供瞭如下幾個 public 的 setter 方法,如下圖所示:
JDK 允許執行緒池使用方通過 ThreadPoolExecutor 的例項來動態設定執行緒池的核心策略,以 setCorePoolSize 為方法例,在執行期執行緒池使用方呼叫此方法設定 corePoolSize 之後,執行緒池會直接覆蓋原來的 corePoolSize 值,並且基於當前值和原始值的比較結果採取不同的處理策略。對於當前值小於當前工作執行緒數的情況,說明有多餘的 worker 執行緒,此時會向當前 idle 的 worker 執行緒發起中斷請求以實現回收,多餘空閒的 worker 在任務執行完成後也會被回收(有延遲);對於當前值大於原始值且當前佇列中有待執行任務,則執行緒池會建立新的 worker 執行緒來執行佇列任務,如果當前執行緒內部佇列沒有待執行任務,則會在下次任務需要執行時新建 work 執行緒(有延遲)。
外接佇列+執行緒池
執行緒池可以藉助 JDK 提供的 set 方法動態設定池的大小,當前場景下擴容時額外需要為新建的執行緒繫結外接佇列,保證後續的任務能通過外接佇列被新建的執行緒接收並處理,那麼在程式碼中新建執行緒時需要新增外接佇列的引用
縮容時,ThreadPoolExecutor 執行緒池通過以上方式對空閒的執行緒進行回收,針對非常駐執行緒就只需要設定大小即可;針對正在執行的執行緒不會主動進行回收,解鈴還須繫鈴人,此時就需要我們手動關閉,先通過 interrupt 方法把執行緒標記為 interrupted 狀態,同時線上程內部根據狀態值判斷是否需要退出輪詢,後續再借助執行緒池內部的縮容策略進行回收執行緒
通過以上兩種方式可以實現動態的修改執行緒池數目,但是為了讓 processors 、backendProcessors 相關 IO 執行緒能在擴縮容時平穩過渡,需要額外的做一些必要的“善後”工作
“善後”
processors 、backendProcessors 所對應的執行緒為 DBLE 的 IO 執行緒,負責對註冊到當前執行緒的連線請求的接收和後端結果的接收,那麼擴容時就需要保證新建的 IO 執行緒能處理後續的 IO 請求,相應的縮容時需要對回收的執行緒所繫結的連線能夠轉移到其他 IO 執行緒,保證連線後續請求能被正常處理
在 DBLE 中對於善後工作所做的處理就是先取消當前執行緒中的連線,再將這些連線重新註冊到新的 IO 執行緒中,此時刪除和重新註冊的選取策略為:刪除時優先選擇執行緒中繫結連線數最小的,重新註冊時優先選擇執行緒中連線數最大的,並根據刪除的執行緒往下選擇
總結
dble 在3.21.06.* 版本及之後提供了可以不重啟來修改執行緒引數的命令,由於 dble 內不只是簡單的使用 JDK 內建的執行緒池,那麼動態修改的命令也不只是使用 JDK 內建的方法去實現,同時也為了相容外接佇列、IO 連線而做了額外的工作。
雖然我們在併發測試中動態調整執行緒池數目並未發現異常情況,但是仍舊建議在併發量小的時候進行調整,不僅為了執行緒間切換平穩過渡,也是為了減少執行緒調整時資源的使用。實際在使用該命令的時候遇到一些問題,可以反饋在 actiontech/dble: A High Scalability Middle-ware for MySQL Sharding (github.com),幫助我們改善
參考
- 徹底搞懂 Reactor 模型和 Proactor 模型:https://cloud.tencent.com/dev...
- Java 執行緒池實現原理及其在美團業務中的實踐:https://tech.meituan.com/2020...