1. 程式人生 > >Mycat開發實踐---MyCAT執行緒模型分析

Mycat開發實踐---MyCAT執行緒模型分析

MyCAT執行緒模型

這裡寫圖片描述

Mycat執行緒介紹

1 Timer
Timer單執行緒僅僅負責排程,任務的具體動作交給timerExecutor。
2 TimerExecutor執行緒池,
預設大小N=2
任務通過timer單執行緒和timerExecutor執行緒池共同完成。這個1+N的設計方式比較巧妙!
但是timerExecutor跟aioExecutor大小預設一樣,不太合理,定時任務沒有那麼大的運算量。
3 NIOConnect主動連線事件分離器
一個執行緒,負責作為客戶端連線MySQL的主動連線事件
4 Server被動連線事件分離器
一個執行緒,負責作為服務端接收來自業務系統的連線事件
5 Manager被動連線事件分離器


一個執行緒,負責作為服務端接收來自管理系統的連線事件
6 NIOReactor讀寫事件分離器
默認個數N=processor size,通道建立連線後處理NIO讀寫事件。
由於寫是採用通道空閒時其它執行緒直接寫,只有寫繁忙時才會註冊寫事件,再由NIOReactor分發。所以NIOReactor主要處理讀操作
7 BusinessExecutor執行緒池
預設大小N=processor size,任務佇列採用的LinkedTransferQueue
所有的NIOReactor把讀出的資料交給BusinessExecutor做下一步的業務操作
全域性只有一個BusinessExecutor執行緒池,所有連結通道隨機分成多個組,然後每組的多個通道共享一個Reactor,所有的Reactor讀取且解碼後的資料下一步處理操作,又共享一個BusinessExecutor執行緒池
8 一個SQL請求的執行緒切換

這裡寫圖片描述
9 MyCAT的執行緒快照

jstack 34179|grep prio 
"Attach Listener" #32 daemon prio=9 os_prio=31 tid=0x00007f8f8ba15800 nid=0x2f07 waiting on condition [0x0000000000000000] 
"Timer1" #31 daemon prio=5 os_prio=31 tid=0x00007f8f8c0d1000 nid=0x7703 waiting on condition [0x0000000126510000] 
"Timer0" #30 daemon prio=5 os_prio=31 tid=0x00007f8f8c0d0000 nid=0x7607 waiting on condition [0x000000012640d000] 
"DestroyJavaVM" #29 prio=5 os_prio=31 tid=0x00007f8f8b01c000 nid=0x1303 waiting on condition [0x0000000000000000] "BusinessExecutor7" #28 daemon prio=5 os_prio=31 tid=0x00007f8f8b1e5800 nid=0x6f03 waiting on condition [0x000000012630a000] "BusinessExecutor6" #27 daemon prio=5 os_prio=31 tid=0x00007f8f8a3ab800 nid=0x6d03 waiting on condition [0x0000000126207000] "BusinessExecutor5" #26 daemon prio=5 os_prio=31 tid=0x00007f8f8a3b3000 nid=0x6b03 waiting on condition [0x0000000126104000] "BusinessExecutor4" #25 daemon prio=5 os_prio=31 tid=0x00007f8f89c04800 nid=0x6903 waiting on condition [0x0000000126001000] "BusinessExecutor3" #24 daemon prio=5 os_prio=31 tid=0x00007f8f89937800 nid=0x6703 waiting on condition [0x0000000125efe000] "BusinessExecutor2" #23 daemon prio=5 os_prio=31 tid=0x00007f8f8a443800 nid=0x6503 waiting on condition [0x0000000125dfb000] "BusinessExecutor1" #22 daemon prio=5 os_prio=31 tid=0x00007f8f8a43c000 nid=0x6303 waiting on condition [0x0000000125cf8000] "BusinessExecutor0" #21 daemon prio=5 os_prio=31 tid=0x00007f8f8a3ae000 nid=0x6103 waiting on condition [0x0000000125bf5000] "$_MyCatServer" #20 prio=5 os_prio=31 tid=0x00007f8f8c098000 nid=0x5f03 runnable [0x0000000125af2000] "$_MyCatManager" #19 prio=5 os_prio=31 tid=0x00007f8f8a8ce800 nid=0x5d03 runnable [0x00000001259ef000] "$_NIOConnector" #18 prio=5 os_prio=31 tid=0x00007f8f89956800 nid=0x5b03 runnable [0x00000001256ec000] "$_NIOREACTOR-3-RW" #17 prio=5 os_prio=31 tid=0x00007f8f898b9000 nid=0x5903 runnable [0x00000001255e9000] "$_NIOREACTOR-2-RW" #16 prio=5 os_prio=31 tid=0x00007f8f8a914800 nid=0x5703 runnable [0x00000001254e6000] "$_NIOREACTOR-1-RW" #15 prio=5 os_prio=31 tid=0x00007f8f8a8d9800 nid=0x5503 runnable [0x00000001253e3000] "$_NIOREACTOR-0-RW" #14 prio=5 os_prio=31 tid=0x00007f8f8a8d9000 nid=0x5303 runnable [0x00000001252e0000] "Log4jWatchdog" #13 daemon prio=5 os_prio=31 tid=0x00007f8f8a305000 nid=0x5107 waiting on condition [0x00000001251cd000] "net.sf.ehcache.CacheManager@512ddf17" #11 daemon prio=5 os_prio=31 tid=0x00007f8f8a32d000 nid=0x4f03 in Object.wait() [0x00000001250ca000] "MyCatTimer" #10 daemon prio=5 os_prio=31 tid=0x00007f8f8a162800 nid=0x4d03 in Object.wait() [0x0000000124fab000] "Thread-0" #9 prio=5 os_prio=31 tid=0x00007f8f8b082000 nid=0x4b03 waiting on condition [0x0000000124cf1000] "Service Thread" #8 daemon prio=9 os_prio=31 tid=0x00007f8f8a801000 nid=0x4703 runnable [0x0000000000000000] "C1 CompilerThread2" #7 daemon prio=9 os_prio=31 tid=0x00007f8f8b025800 nid=0x4503 waiting on condition [0x0000000000000000] "C2 CompilerThread1" #6 daemon prio=9 os_prio=31 tid=0x00007f8f8b025000 nid=0x4303 waiting on condition [0x0000000000000000] "C2 CompilerThread0" #5 daemon prio=9 os_prio=31 tid=0x00007f8f8b023800 nid=0x4103 waiting on condition [0x0000000000000000] "Signal Dispatcher" #4 daemon prio=9 os_prio=31 tid=0x00007f8f8b022000 nid=0x3017 runnable [0x0000000000000000] "Finalizer" #3 daemon prio=8 os_prio=31 tid=0x00007f8f8a00e800 nid=0x2d03 in Object.wait() [0x0000000122b34000] "Reference Handler" #2 daemon prio=10 os_prio=31 tid=0x00007f8f8a00d800 nid=0x2b03 in Object.wait() [0x0000000122a31000] "VM Thread" os_prio=31 tid=0x00007f8f8b001000 nid=0x2903 runnable "GC task thread#0 (ParallelGC)" os_prio=31 tid=0x00007f8f8980d800 nid=0x2103 runnable "GC task thread#1 (ParallelGC)" os_prio=31 tid=0x00007f8f8980e000 nid=0x2303 runnable "GC task thread#2 (ParallelGC)" os_prio=31 tid=0x00007f8f8980f000 nid=0x2503 runnable "GC task thread#3 (ParallelGC)" os_prio=31 tid=0x00007f8f8980f800 nid=0x2703 runnable "VM Periodic Task Thread" os_prio=31 tid=0x00007f8f8a840800 nid=0x4903 waiting on condition

Cobar執行緒介紹

這裡寫圖片描述
1 Timer
Timer單執行緒僅僅負責排程,任務的具體動作交給timerExecutor。
2 TimerExecutor執行緒池
預設大小N=2
任務通過timer單執行緒和timerExecutor執行緒池共同完成。這個1+N的設計方式比較巧妙!
但是timerExecutor跟aioExecutor大小預設一樣,不太合理,定時任務沒有那麼大的運算量。
3 Server被動連線事件分離器
一個執行緒,負責作為服務端接收來自業務系統的連線事件
4 Manager被動連線事件分離器
一個執行緒,負責作為服務端接收來自管理系統的連線事件
5 R讀寫事件分離器
客戶端與Server連線後,由R執行緒負責讀寫事件(寫事件大部分有W執行緒負責,只有在網路繁忙時才會由小部分寫事件是由R執行緒完成的)。
6 Handler和Executor執行緒池
R執行緒接收到讀事件後解碼出一個完整的MySQL協議包,下一步由Handler執行緒池進行SQL解析、路由計算。然後執行任務從Handler執行緒池轉移到Executor執行緒池,以阻塞方式傳送給後端MySQL Server。Executor收到MySQL Server應答後,會由最後一個Executor執行緒進行聚合,然後交給W執行緒
7 W執行緒
W執行緒不停遍歷LinkedBlockingQueue檢查是否有寫任務,若有則寫入Socket Channel。當Channel繁忙時,W執行緒會註冊OP_WRITE事件,通過R執行緒進行候補寫操作。
8 ManageExecutor執行緒池
Cobar對來自Manager的請求和來自Server的請求做了分離,來自管理系統的請求,專門由ManageExecutor執行緒池處理。
9 InitExecutor執行緒池
用來進行後端鏈路初始化。

Cobar為什麼那麼多個執行緒池?

可以發現Cobar有下面這麼多個執行緒池

  • TimerExecutor執行緒池(一個)
  • InitExecutor執行緒池(一個)
  • ManageExecutor執行緒池(一個)
  • Handler執行緒池(N個)
  • Executor執行緒池(N個)

注意上面的個數單位是執行緒池,不是執行緒!所以看起來有些眼花繚亂吧? 我不是Cobar的原作者,只能猜測最什麼設計這麼多執行緒池?那就是因為後端採用了BIO!

  • 因為後端BIO,所以每一個請求到後端查詢,都要阻塞一個執行緒,前端NIO(Reactor-R執行緒)必須要把執行任務交給Executor執行緒池。
  • 由於存在聚合要求,前端NIO的一個SQL請求可能會對應多個後端請求,所以不只要阻塞一個Executor執行緒。為此增加了Handler做中間SQL解析、路由計算,路由計算完畢後再交給Executor執行
  • 由於後端是阻塞方式,在時,會導致Executor無空閒執行緒,為了避免管理埠輸入名命令無任何響應的現象,為此增加一個ManageExecutor執行緒池,專門處理ManageExecutor執行緒
  • 在後端BIO時,除了讀寫是阻塞方式外,鏈路建立過程也是阻塞方式,若同時鏈路建立請求多,也會阻塞大量執行緒。為避免業務、管理的相互干擾,為此增加了一個InitExecutor執行緒池專門做後端鏈路建立
  • 所以如果後端BIO改為NIO,並優化邏輯執行過程,避免執行緒sleep或長時間阻塞,儘量通過Reactor直接計算,就可以大大降低執行緒上下文切換的損耗,上述各眼花繚亂的執行緒池就可以合併為一個業務執行緒池。

1 一個SQL請求的執行緒切換
下面是一個SQL請求執行過程的執行緒切換,可以看到Cobar的執行緒上下文切換還是比較多的
這裡寫圖片描述
2 Cobar的執行緒快照

Cobar>jstack 10631|grep prio 
"Processor0-E6" daemon prio=5 tid=7f931f057000 nid=0x11abcf000 waiting on condition [11abce000] 
"Processor1-E6" daemon prio=5 tid=7f931f056000 nid=0x11aacc000 waiting on condition [11aacb000] 
"TimerExecutor3" daemon prio=5 tid=7f931e206000 nid=0x119d22000 waiting on condition [119d21000] 
"CobarServer" prio=5 tid=7f931d961000 nid=0x119c1f000 runnable [119c1e000]
"CobarManager" prio=5 tid=7f931f150800 nid=0x119b1c000 runnable [119b1b000] 
"TimerExecutor2" daemon prio=5 tid=7f931d8c7800 nid=0x119a19000 waiting on condition [119a18000] 
"TimerExecutor1" daemon prio=5 tid=7f931f14f800 nid=0x119916000 waiting on condition [119915000] 
"InitExecutor1" daemon prio=5 tid=7f931f156800 nid=0x119813000 waiting on condition [119812000] 
"InitExecutor0" daemon prio=5 tid=7f931f155800 nid=0x119710000 waiting on condition [11970f000] 
"CobarConnector" prio=5 tid=7f931e203800 nid=0x11960d000 runnable [11960c000] 
"TimerExecutor0" daemon prio=5 tid=7f931e201000 nid=0x11950a000 waiting on condition [119509000] 
"Processor1-W" prio=5 tid=7f931d8c4800 nid=0x119407000 waiting on condition [119406000] 
"Processor1-R" prio=5 tid=7f931d82c800 nid=0x119304000 runnable [119303000] 
"Processor0-W" prio=5 tid=7f931d0ab800 nid=0x119201000 waiting on condition [119200000] 
"Processor0-R" prio=5 tid=7f931d0aa800 nid=0x1190fe000 runnable [1190fd000] 
"CobarTimer" daemon prio=5 tid=7f931e17f000 nid=0x118fde000 in Object.wait() [118fdd000] 
"Low Memory Detector" daemon prio=5 tid=7f931e0ab800 nid=0x118b3b000 runnable [00000000] 
"C2 CompilerThread1" daemon prio=9 tid=7f931e0aa800 nid=0x118a38000 waiting on condition [00000000] 
"C2 CompilerThread0" daemon prio=9 tid=7f931e0aa000 nid=0x118935000 waiting on condition [00000000] 
"Signal Dispatcher" daemon prio=9 tid=7f931e0a9000 nid=0x118832000 runnable [00000000] 
"Surrogate Locker Thread (Concurrent GC)" daemon prio=5 tid=7f931e0a8800 nid=0x11872f000 waiting on condition [00000000] 
"Finalizer" daemon prio=8 tid=7f931f037000 nid=0x116d52000 in Object.wait() [116d51000] 
"Reference Handler" daemon prio=10 tid=7f931f036000 nid=0x116c4f000 in Object.wait() [116c4e000] 
"VM Thread" prio=9 tid=7f931e094800 nid=0x116b4c000 runnable 
"Gang worker#0 (Parallel GC Threads)" prio=9 tid=7f931f001800 nid=0x113005000 runnable 
"Gang worker#1 (Parallel GC Threads)" prio=9 tid=7f931d001000 nid=0x113108000 runnable 
"Gang worker#2 (Parallel GC Threads)" prio=9 tid=7f931d001800 nid=0x11320b000 runnable 
"Gang worker#3 (Parallel GC Threads)" prio=9 tid=7f931d002000 nid=0x11330e000 runnable 
"Concurrent Mark-Sweep GC Thread" prio=9 tid=7f931f002000 nid=0x1167c7000 runnable 
"VM Periodic Task Thread" prio=10 tid=7f931d811800 nid=0x118c3e000 waiting on condition 
"Exception Catcher Thread" prio=10 tid=7f931f001000 nid=0x10ff01000 runnable

MyCAT與Cobar的比較

1 MyCAT比Cobar減少了執行緒切換
Cobar的後端採用BIO通訊,後端讀與後端寫因為執行緒阻塞了,不存線上程切換,沒有可比性,所以我們只比較NIO和業務邏輯部分。
Cobar的執行緒模型中存在著大量的上下文切換,MyCAT的執行緒排程儘量減少了執行緒間的切換,以寫為例
Cobar是業務執行緒先把寫請求交給專門的W執行緒,W執行緒再寫過程中發現通道繁忙時再交給R執行緒;MyCAT對寫的做法是業務執行緒發現通道空閒直接寫,只有在通道繁忙時再交給Reactor執行緒。
2 減少執行緒切換與業務可能停頓的矛盾
MyCAT幾乎已經達到了執行緒簡化的最高境界,有一個看似可行的方法:可以配置多個NIOReactor,儘可能所有讀、解碼、業務處理都在Reactor執行緒中完成,而不必把任務交給BusinessExecutor執行緒池,從而減少執行緒的上下文切換,提高處理效率。
但是,不管配置幾個Reactor,還是要求多個通道共享一個Reactor,(為什麼?因為Reactor最多十幾個、幾十個,併發的連結通道可能上萬個!)如果Reactor在讀和解碼請求後順序處理業務邏輯,那麼在處理業務邏輯過程中,Reactor就無法響應其它通道的事件了,這個時候如果正好有共享同一個Reactor的其它通道的請求過來,就會出現停頓的現象。
那麼如何做呢,就需要具體問題具體分析,要對業務邏輯進行歸類:

  • 對於業務較重的,比如大結果集排序,則送到BusinessExecutor執行緒池進行下一步處理;
  • 於業務較輕的,比如單庫直接轉發的情況,則由Reactor直接完成,不再送執行緒池,減少上下文切換。

3 特別說明ER分片機制
如果涉到ER分片,MyCAT目前的機制:計算路由時以阻塞同步方式呼叫FetchStoreNodeOfChildTableHandler,若由Reactor直接進行路由計算,會導致其它通道停頓現象。把ER分片同步改非同步是個看似可行的方法,但這個改造工作量較大,會造成原來完整路由計算邏輯的碎片化。
即使ER分片同步改非同步了,每次子表操作都要遍歷父表對效能損耗較大,即使採用快取也不能最終解決問題。個人覺得,ER分片這個功能比較雞肋,建議生產部署時繞開這個功能,直接通過關聯欄位分片或表設計時增加冗餘欄位。
4 資料驗證
1.測試sql從收到請求到下推的總時長,如果時間可容忍,則不必切換到執行緒池。忽略ER分片。
2.對於manager埠的命令,若存在執行時間比較的,也需要改為執行緒池來執行
3.對於收到的應答,大部分都不必切換到執行緒池。
4.對於大量資料排序,只有在排序時,構造執行任務,切換到執行緒池完成。