MySQL線程池(THREAD POOL)的原理

MySQL經常使用(目前線上使用)的線程調度方式是one-thread-per-connection(每鏈接一個線程),server爲每個鏈接建立一個線程來服務,鏈接斷開後,這個線程進入thread_cache或者直接退出(取決於thread_cache設置及系統當前已經cache的線程數目),one-thread-per-connection調度的好處是實現簡單,並且可以在系統沒有遇到瓶頸以前保證較小的響應時間,比較適合活躍的長鏈接的應用場景,而在大量短鏈接或者高併發狀況下,one-thread-per-connection須要建立/調度大量的線程,產生較高的的context-switch代價,從而使得系統性能降低mysql

爲了解決這個問題,Oracle和MariaDB分別推出了threadpool方案,目前Oracle的threadpool實現爲plugin方式,而且只添加到在Enterprise版本中,沒有公佈代碼,MariaDB threadpool在5.5版本中引入,咱們一直密切關注社區動態並在第一時間測試了MariaDB threapool性能,而且發現了一些其中的問題,好比:要像發揮線程池的優點,須要儘可能控制線程池中線程數目,不然會退化成one-thread-per-connection,而若是嚴格控制線程池中線程數據,可能會出現調度上的死鎖,percona在移植MariaDB threadpool的實現後進一步優化了線程池性能,經過引入優先隊列很好解決了這個問題,經測試效果明顯,所以咱們將這個特性port到了AliMySQL中
實現簡介
一、threadpool中worker線程處理單位爲一個statement,而不是one-thread-per-connection對應的一個鏈接;當worker線程處理完A鏈接發送來的一個sql後,A鏈接沒有馬上發送第二條sql,worker線程回去服務其它鏈接發送來的sql,所以worker線程工做效率更高,系統須要的線程數也更少
二、threadpool本質上是一個生產者-消費者模型,爲了減少競爭,threadpool被劃分爲N個group(n默認爲cpu核心數),鏈接發送的sql根據鏈接id分配到不一樣的group中,所以,同一個鏈接發送的全部sql是被同一個group中的worker線程處理的
三、每一個group都有2個任務隊列, 即優先隊列和普通隊列 ,若是一個sql所在的事務已經開啓,則將任務放到優先隊列中,不然放到普通隊列中,worker線程優先從優先隊列中取任務執行,當優先隊列爲空則從普通隊列取任務執行,這個能夠保證已經開啓的事務優先獲得執行,從而儘早釋放其佔用的資源(主要是鎖),能夠有效減少響應時間,別且避免調度上的死鎖(A和B被分到不一樣的group中,A事務已經開啓,而且得到了鎖,可能沒法當即獲得調度執行,B事務依賴A事務釋放鎖資源,可是先於A獲得調度)
四、每一個group中每一個worker線程地位同樣,若是遇到任務隊列爲空的狀況,線程會調用epoll_wait批量取任務
五、threadpool額外建立了一個timer線程,每隔一段時間檢查一遍全部的group,若是發現group出現異常(堵塞/超時/worker線程數目不夠),及時喚醒線程
關於MySQL線程調度方案接口以及threadpool實現細節能夠參考以前我寫的兩篇博客: mysql thread sheduler 源碼分析 , mariadb 5.5 threadpool 源碼分析web

+-------------------------------+--------------+
| Variable_name | Value |
+-------------------------------+--------------+
| thread_pool_high_prio_mode | transactions |
| thread_pool_high_prio_tickets | 4294967295 |
| thread_pool_idle_timeout | 60 |
| thread_pool_max_threads | 100000 |
| thread_pool_oversubscribe | 3 |
| thread_pool_size | 24 |
| thread_pool_stall_limit | 500 |
+-------------------------------+--------------+
7 rows in set (0.00 sec)sql


threadpool相關參數> thread_pool_high_prio_mode
有三個取值:transactions / statements / none
transactions(default): 使用優先隊列和普通隊列,對於事務已經開啓的statement,放到優先隊列中,不然放到普通隊列中
statements:只使用優先隊列
none: 只是用普通隊列,本質上和statements相同,都是隻是用一個隊列
> thread_pool_high_prio_tickets
取值0~4294967295,當開啓了優先隊列模式後(thread_pool_high_prio_mode=transactions),每一個鏈接最多容許thread_pool_high_prio_tickets次被放到優先隊列中,以後放到普通隊列中,默認爲4294967295
> thread_pool_idle_timeout
worker線程最大空閒時間,單位爲秒,超過限制後會退出,默認60
> thread_pool_max_threads
threadpool中最大線程數目,全部group中worker線程總數超過該限制後不能繼續建立更多線程,默認100000
> thread_pool_oversubscribe
一個group中線程數過載限制,當一個group中線程數超過次限制後,繼續建立worker線程會被延遲,默認3
> thread_pool_size
threadpool中group數量,默認爲cpu核心數,server啓動時自動計算
> thread_pool_stall_limit
timer線程檢測間隔,單位爲毫秒,默認500數據庫

-----------------------------------------------------------------------------------------------------------------------------------------------緩存


線程池是MySQL5.6企業版的一個核心功能,對於服務器應用而言,不管是web應用服務仍是DB服務,高併發請求始終是一個繞不開的話題。應用發起一個對數據庫的操做時,在整個應用中是一個不小的開銷,從創建鏈接之初,CPU要給它劃分必定的thread stack,而後進行用戶身份認證,創建上下文信息,最後請求完成,關閉鏈接,同時釋放資源,能夠稱的上是秒級的過程,當有大量請求併發訪問時,必定伴隨着資源的不斷建立和釋放,致使資源利用率低,下降了服務質量。線程池是一種通用的技術,經過預先建立必定數量的線程,當有請求達到時,線程池分配一個線程提供服務,請求結束後,該線程又去服務其餘請求。 經過這種方式,避免了線程和內存對象的頻繁建立和釋放,下降了服務端的併發度,減小了上下文切換和資源的競爭,提升資源利用效率。全部服務的線程池本質都是位了提升資源利用效率,而且實現方式也大致相同。本文主要說明MySQL線程池的實現原理。服務器


mysql線程池,就是I/O多路複用的體現。至於I/O多路複用是什麼,請查看http://blog.csdn.net/stubborn_cow/article/details/50247269網絡


在MySQL5.6出現之前,MySQL處理鏈接的方式是One-Connection-Per-Thread,即對於每個數據庫鏈接,MySQL-Server都會建立一個獨立的線程服務,請求結束後,銷燬線程。再來一個鏈接請求,則再建立一個鏈接,結束後再進行銷燬。這種方式在高併發狀況下,會致使線程的頻繁建立和釋放。固然,經過thread-cache,咱們能夠將線程緩存起來,以供下次使用,避免頻繁建立和釋放的問題,可是沒法解決高鏈接數的問題。One-Connection-Per-Thread方式隨着鏈接數暴增,致使須要建立一樣多的服務線程,高併發線程意味着高的內存消耗,更多的上下文切換(cpu cache命中率下降)以及更多的資源競爭,致使服務出現抖動。相對於One-Thread-Per-Connection方式,一個線程對應一個鏈接,Thread-Pool實現方式中,線程處理的最小單位是statement(語句),一個線程能夠處理多個鏈接的請求。這樣,在保證充分利用硬件資源狀況下(合理設置線程池大小),能夠避免瞬間鏈接數暴增致使的服務器抖動。
調度方式實現
MySQL-Server同時支持3種鏈接管理方式,包括No-Threads,One-Thread-Per-Connection和Pool-Threads。No-Threads表示處理鏈接使用主線程處理,不額外建立線程,這種方式主要用於調試;One-Thread-Per-Connection是線程池出現之前最經常使用的方式,爲每個鏈接建立一個線程服務;Pool-Threads則是本文所討論的線程池方式。MySQL-Server經過一組函數指針來同時支持3種鏈接管理方式,對於特定的方式,將函數指針設置成特定的回調函數,鏈接管理方式經過thread_handling參數控制,代碼以下:
if (thread_handling <= SCHEDULER_ONE_THREAD_PER_CONNECTION)
one_thread_per_connection_scheduler(thread_scheduler,
&max_connections,
&connection_count);
else if (thread_handling == SCHEDULER_NO_THREADS)
one_thread_scheduler(thread_scheduler);
else
pool_of_threads_scheduler(thread_scheduler, &max_connections,&connection_count);
鏈接管理流程
1.經過poll監聽mysql端口的鏈接請求
2.收到鏈接後,調用accept接口,建立通訊socket
3.初始化thd實例,vio對象等
4.根據thread_handling方式設置,初始化thd實例的scheduler函數指針
5.調用scheduler特定的add_connection函數新建鏈接
下面代碼展現了scheduler_functions模板和線程池對模板回調函數的實現,這個是多種鏈接管理的核心。
struct scheduler_functions
{
uint max_threads;
uint *connection_count;

ulong *max_connections;

bool (*init)(void);

bool (*init_new_connection_thread)(void);
void (*add_connection)(THD *thd);
void (*thd_wait_begin)(THD *thd, int wait_type);
void (*thd_wait_end)(THD *thd);
void (*post_kill_notification)(THD *thd);
bool (*end_thread)(THD *thd, bool cache_thread);
void (*end)(void);
};

static scheduler_functions tp_scheduler_functions=
{
0, // max_threads
NULL,
NULL,
tp_init, // init
NULL, // init_new_connection_thread
tp_add_connection, // add_connection
tp_wait_begin, // thd_wait_begin
tp_wait_end, // thd_wait_end
tp_post_kill_notification, // post_kill_notification
NULL, // end_thread
tp_end // end
};
線程池的相關參數
1.thread_handling:表示線程池模型。
2.thread_pool_size:表示線程池的group個數,通常設置爲當前CPU核心數目。理想狀況下,一個group一個活躍的工做線程,達到充分利用CPU的目的。
3.thread_pool_stall_limit:用於timer線程按期檢查group是否「停滯」,參數表示檢測的間隔。
4.thread_pool_idle_timeout:當一個worker空閒一段時間後會自動退出,保證線程池中的工做線程在知足請求的狀況下,保持比較低的水平。
5.thread_pool_oversubscribe:該參數用於控制CPU核心上「超頻」的線程數。這個參數設置值不含listen線程計數。
6.threadpool_high_prio_mode:表示優先隊列的模式。
線程池實現
上面描述了Mysql-Server如何管理鏈接,這節重點描述線程池的實現框架,以及關鍵接口。如圖1多線程

每個綠色的方框表明一個group,group數目由thread_pool_size參數決定。每一個group包含一個優先隊列和普通隊列,包含一個listener線程和若干個工做線程,listener線程和worker線程能夠動態轉換,worker線程數目由工做負載決定,同時受到thread_pool_oversubscribe設置影響。此外,整個線程池有一個timer線程監控group,防止group「停滯」。
關鍵接口
1. tp_add_connection[處理新鏈接]
1) 建立一個connection對象
2) 根據thread_id%group_count肯定connection分配到哪一個group
3) 將connection放進對應group的隊列
4) 若是當前活躍線程數爲0,則建立一個工做線程
2. worker_main[工做線程]
1) 調用get_event獲取請求
2) 若是存在請求,則調用handle_event進行處理
3) 不然,表示隊列中已經沒有請求,退出結束。
3. get_event[獲取請求]
1) 獲取一個鏈接請求
2) 若是存在,則當即返回,結束
3) 若此時group內沒有listener,則線程轉換爲listener線程,阻塞等待
4) 若存在listener,則將線程加入等待隊列頭部
5) 線程休眠指定的時間(thread_pool_idle_timeout)
6) 若是依然沒有被喚醒,是超時,則線程結束,結束退出
7) 不然,表示隊列裏有鏈接請求到來,跳轉1
備註:獲取鏈接請求前,會判斷當前的活躍線程數是否超過了
thread_pool_oversubscribe+1,若超過了,則將線程進入休眠狀態。
4. handle_event[處理請求]
1) 判斷鏈接是否進行登陸驗證,若沒有,則進行登陸驗證
2) 關聯thd實例信息
3) 獲取網絡數據包,分析請求
4) 調用do_command函數循環處理請求
5) 獲取thd實例的套接字句柄,判斷句柄是否在epoll的監聽列表中
6) 若沒有,調用epoll_ctl進行關聯
7) 結束
5.listener[監聽線程]
1) 調用epoll_wait進行對group關聯的套接字監聽,阻塞等待
2) 若請求到來,從阻塞中恢復
3) 根據鏈接的優先級別,肯定是放入普通隊列仍是優先隊列
4) 判斷隊列中任務是否爲空
5) 若隊列爲空,則listener轉換爲worker線程
6) 若group內沒有活躍線程,則喚醒一個線程
備註:這裏epoll_wait監聽group內全部鏈接的套接字,而後將監聽到的鏈接
請求push到隊列,worker線程從隊列中獲取任務,而後執行。
6. timer_thread[監控線程]
1) 若沒有listener線程,而且最近沒有io_event事件
2) 則建立一個喚醒或建立一個工做線程
3) 若group最近一段時間沒有處理請求,而且隊列裏面有請求,則
4) 表示group已經stall,則喚醒或建立線程
備註:timer的做用是避免group處於stall狀態
7.tp_wait_begin[進入等待狀態流程]
1) 活躍線程數減1,等待線程數加1
2) 若活躍線程數爲0,而且任務隊列不爲空,或者沒有監聽線程,則
3) 喚醒或建立一個線程
備註:waiting_threads這裏面的線程是空閒線程,並不是等待線程,所謂空
閒線程是隨時能夠處理任務的線程,而等待線程則是由於等待鎖,或等待io操
做等沒法處理任務的線程。
8.tp_wait_end[結束等待狀態流程]
1) 設置connection的waiting狀態爲false
2) 活躍線程數加1,等待線程數減1
線程池與鏈接池
鏈接池一般實如今Client端,是指應用(??戶端)建立預先建立必定的鏈接,利用這些鏈接服務於客戶端全部的DB請求。若是某一個時刻,空閒的鏈接數小於DB的請求數,則須要將請求排隊,等待空閒鏈接處理。經過鏈接池能夠複用鏈接,避免鏈接的頻繁建立和釋放,從而減小請求的平均響應時間,而且在請求繁忙時,經過請求排隊,能夠緩衝應用對DB的衝擊。線程池實如今server端,經過建立必定數量的線程服務DB請求,相對於one-conection-per-thread的一個線程服務一個鏈接的方式,線程池服務的最小單位是語句,即一個線程能夠對應多個活躍的鏈接。經過線程池,能夠將server端的服務線程數控制在必定的範圍,減小了系統資源的競爭和線程上下文切換帶來的消耗,同時也避免出現高鏈接數致使的高併發問題。鏈接池和線程池相輔相成,經過鏈接池能夠減小鏈接的建立和釋放,提升請求的平均響應時間,並能很好地控制一個應用的DB鏈接數,但沒法控制整個應用集羣的鏈接數規模,從而致使高鏈接數,經過線程池則能夠很好地應對高鏈接數,保證server端能提供穩定的服務。如圖2所示,每一個web-server端維護了3個鏈接的鏈接池,對於鏈接池的每一個鏈接實際不是獨佔db-server的一個worker,而是可能與其餘鏈接共享。這裏假設db-server只有3個group,每一個group只有一個worker,每一個worker處理了2個鏈接的請求。併發

圖 2(鏈接池與線程池框架圖)
線程池優化
1.優先隊列
因爲一個group會同時處理多個鏈接,但多個鏈接不是對等的。好比,有的鏈接是第一次發送請求;而有的鏈接對應的事務已經開啓,而且持有了部分鎖資源。爲了減小鎖資源爭用,後者顯然應該比前者優先處理,以達到儘早釋放鎖資源的目的。所以在group裏面,能夠添加一個優先級隊列,將已經持有鎖的鏈接發起的請求放入優先隊列,工做線程首先從優先隊列獲取任務執行。
2.大查詢處理
假設一種場景,某個group裏面的鏈接都是大查詢,那麼group裏面的工做線程數很快就會達到thread_pool_oversubscribe參數設置值,對於後續的鏈接請求,則會響應不及時(沒有更多的鏈接來處理),這時候group就發生了stall。經過前面分析知道,timer線程會按期檢查這種狀況,並建立一個新的worker線程來處理請求。若是長查詢來源於業務請求,則此時全部group都面臨這種問題,此時主機可能會因爲負載過大,致使hang住的狀況。這種狀況線程池自己無能爲力,由於源頭多是爛SQL併發,或者SQL沒有走對執行計劃致使,經過其餘方法,好比SQL高低水位限流或者SQL過濾手段能夠應急處理。可是,還有另一種狀況,就是dump任務。不少下游依賴於數據庫的原始數據,一般經過dump命令將數據拉到下游,而這種dump任務一般都是耗時比較長,因此也能夠認爲是大查詢。若是dump任務集中在一個group內,並致使其餘正常業務請求沒法當即響應,這個是不能容忍的,由於此時數據庫並無壓力,只是由於採用了線程池策略,才致使了請求響應不及時,爲了解決這個問題,咱們將group中處理dump任務的線程不計入thread_pool_oversubscribe累計值,避免上述問題。框架

相關文章
相關標籤/搜索