概述
今天分享下MySQL數據庫的 thread pool。
一、大連接問題
mysql5.6之前處理客戶端連接的方式會觸發mysql 新建一個線程來處理新的連接,新建的線程會處理該連接所發送的所有 SQL 請求,即 one-thread-per-connection 的方式,其創建連接的堆棧為:
mysqld_main handle_connections_sockets create_new_thread create_thread_to_handle_connection handle_one_connection
線程建立后,處理請求的堆棧如下:
0 mysql_execute_command 1 0x0000000000936f40 in mysql_parse 2 0x0000000000920664 in dispatch_command 3 0x000000000091e951 in do_command 4 0x00000000008c2cd4 in do_handle_one_connection 5 0x00000000008c2442 in handle_one_connection 6 0x0000003562e07851 in start_thread () from /lib64/libpthread.so.0 7 0x0000003562ae767d in clone () from /lib64/libc.so.6
二、優點及存在的問題
在連接數較小的情況下可以很快的響應客戶端的請求,但當連接數非常大時會創建很多線程,這樣會引起以下問題:
- 過多線程之間的切換會加重系統的負載,造成系統資源緊張且響應不及時;
- 頻繁的進行線程的創建及銷毀以及線程間同時無序的竟爭系統資源加重了系統的負載。
thread_pool正是為了解決以上問題而產生的;
三、thread_pool
thread_pool(線程池),是指mysql 創建若干工作線程來共同處理所有連接的用戶請求,用戶的請求的方式不再是 ‘one thread per connection’,而是多個線程共同接收并處理多個連接的請求,在數據庫的底層處理方面(mysql_execute_command),單線程的處理方式和線程池的處理方式是一致的。
四、thread_pool 的工作原理
啟動 thread_pool 的mysql 會創建thread_pool_size 個thread group , 一個timer thread, 每個thread group 最多擁有thread_pool_oversubscribe個活動線程,一個listener線程,listener線程負責監聽分配到thread group中的連接,并將監聽到的事件放入到一個queue中,worker線程從queue中取出連接的事件并執行具體的操作,執行的過程和one thread per connection 相同。timer threaad 則是為了監聽各個threadgroup的運行情況,并根據是否陰塞來創建新的worker線程。
1、thread_pool 建立連接
thread_pool 建立連接的堆棧如下:
mysqld_main handle_connections_sockets create_new_thread tp_add_connection queue_put
2、worker 處理請求
thread group中的 worker 處理請求的堆棧如下:
0 mysql_execute_command 1 0x0000000000936f40 in mysql_parse 2 0x0000000000920664 in dispatch_command 3 0x000000000091e951 in do_command 4 0x0000000000a78533 in threadpool_process_request 5 0x000000000066a10b in handle_event 6 0x000000000066a436 in worker_main 7 0x0000003562e07851 in start_thread () 8 0x0000003562ae767d in clone ()
其中worker_main函數是woker線程的主函數,調用mysql本身的do_command 進行消息解析及處理,和one_thread_per_connection 是一樣的邏輯; thread_pool 自行控制工作的線程個數,進而實現線程的管理。
3、thread_pool中線程的創建
- listener線程將監聽事件放入mysql放入queue中時,如果發現當前thread group中的活躍線程數active_thread_count為零,則創建新的worker 線程;
- 正在執行的線程阻塞時,如果發現當前thread group中的活躍線程數active_thread_count為零,則創建新的worker 線程;
- timer線程在檢測時發現沒有listener線程且自上次檢測以來沒有新的請求時會創建新的worker線程,其中檢測的時間受參數threadpool_stall_limit控制;
- timer線程在檢測時發現沒有執行過新的請求且執行隊列queue 不為空時會創建新的worker線程;
worker線程的偽碼如下:
4、thread_pool中線程的銷毀
當從隊列queue中取出的connection為空時,則此線程銷毀,取connection所等待的時間受參數thread_pool_idle_timeout的控制; 綜上,thread_pool通過線程的創建及銷毀來自動處理worker的線程個數,在負載較高時,創建的線程數目較高,負載較低時,會銷毀多余的worker線程,從而降低連接個數帶來的影響的同時,提升穩定性及性能。同時,threadpool中引入了Timer 線程,主要做兩個事情。
- 定期檢查每個thread_group是否阻塞,如果阻塞,則進行喚醒或創建線程的工作;
- 檢查每個thread_group中的連接是否超時,如果超時則關掉連接并釋放相應的資源;
五、線程池實現
下圖是線程池的實現框架,以及關鍵接口
每一個綠色的方框代表一個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,則喚醒或創建線程 5)檢查是否有連接超時
備注:timer線程通過調用check_stall判斷group是否處于stall狀態,通過調用timeout_check檢查客戶端連接是否超時。
7.tp_wait_begin[進入等待狀態流程]
1) active_thread_count減1,waiting_thread_count加1 2)設置connection->waiting= true 3) 若活躍線程數為0,并且任務隊列不為空,或者沒有監聽線程,則 4) 喚醒或創建一個線程
8.tp_wait_end[結束等待狀態流程]
1) 設置connection的waiting狀態為false 2) active_thread_count加1,waiting_thread_count減1
備注:
- waiting_threads這個list里面的線程是空閑線程,并非等待線程,所謂空閑線程是隨時可以處理任務的線程,而等待線程則是因為等待鎖,或等待io操作等無法處理任務的線程。
- tp_wait_begin和tp_wait_end的主要作用是由于匯報狀態,即使更新active_thread_count和waiting_thread_count的信息。
9. tp_init/tp_end
分別調用thread_group_init和thread_group_close來初始化和銷毀線程池
六、線程池與連接池
連接池通常實現在Client端,是指應用(客戶端)創建預先創建一定的連接,利用這些連接服務于客戶端所有的DB請求。如果某一個時刻,空閑的連接數小于DB的請求數,則需要將請求排隊,等待空閑連接處理。通過連接池可以復用連接,避免連接的頻繁創建和釋放,從而減少請求的平均響應時間,并且在請求繁忙時,通過請求排隊,可以緩沖應用對DB的沖擊。
線程池實現在server端,通過創建一定數量的線程服務DB請求,相對于one-conection-per-thread的一個線程服務一個連接的方式,線程池服務的最小單位是語句,即一個線程可以對應多個活躍的連接。通過線程池,可以將server端的服務線程數控制在一定的范圍,減少了系統資源的競爭和線程上下文切換帶來的消耗,同時也避免出現高連接數導致的高并發問題。連接池和線程池相輔相成,通過連接池可以減少連接的創建和釋放,提高請求的平均響應時間,并能很好地控制一個應用的DB連接數,但無法控制整個應用集群的連接數規模,從而導致高連接數,通過線程池則可以很好地應對高連接數,保證server端能提供穩定的服務。
如圖所示,每個web-server端維護了3個連接的連接池,對于連接池的每個連接實際不是獨占db-server的一個worker,而是可能與其他連接共享。
這里假設db-server只有3個group,每個group只有一個worker,每個worker處理了2個連接的請求。
七、threadpool相關參數
1、thread_pool_high_prio_mode
有三個取值:transactions / statements / none
- transactions(default): 使用優先隊列和普通隊列,對于事務已經開啟的statement,放到優先隊列中,否則放到普通隊列中
- statements:只使用優先隊列
- none: 只是用普通隊列,本質上和statements相同,都是只是用一個隊列
2、thread_pool_high_prio_tickets
取值0~4294967295,當開啟了優先隊列模式后(thread_pool_high_prio_mode=transactions),每個連接最多允許thread_pool_high_prio_tickets次被放到優先隊列中,之后放到普通隊列中,默認為4294967295
3、thread_pool_idle_timeout
worker線程最大空閑時間,單位為秒,超過限制后會退出,默認60
4、thread_pool_max_threads
threadpool中最大線程數目,所有group中worker線程總數超過該限制后不能繼續創建更多線程,默認100000
5、thread_pool_oversubscribe
一個group中線程數過載限制,當一個group中線程數超過次限制后,繼續創建worker線程會被延遲,默認3
6、thread_pool_size
threadpool中group數量,默認為cpu核心數,server啟動時自動計算
7、thread_pool_stall_limit
timer線程檢測間隔,單位為毫秒,默認500
八、threadpool優化
1.調度死鎖解決
引入線程池解決了多線程高并發的問題,但也帶來一個隱患。假設,A,B兩個事務被分配到不同的group中執行,A事務已經開始,并且持有鎖,但由于A所在的group比較繁忙,導致A執行一條語句后,不能立即獲得調度執行;而B事務依賴A事務釋放鎖資源,雖然B事務可以被調度起來,但由于無法獲得鎖資源,導致仍然需要等待,這就是所謂的調度死鎖。由于一個group會同時處理多個連接,但多個連接不是對等的。比如,有的連接是第一次發送請求;而有的連接對應的事務已經開啟,并且持有了部分鎖資源。為了減少鎖資源爭用,后者顯然應該比前者優先處理,以達到盡早釋放鎖資源的目的。
因此mysql數據庫在group里面添加一個優先級隊列,將已經持有鎖的連接,或者已經開啟的事務的連接發起的請求放入優先隊列,工作線程首先從優先隊列獲取任務執行。
2.大查詢處理
假設一種場景,某個group里面的連接都是大查詢,那么group里面的工作線程數很快就會達到thread_pool_oversubscribe參數設置值,對于后續的連接請求,則會響應不及時(沒有更多的連接來處理),這時候group就發生了stall。通過前面分析知道,timer線程會定期檢查這種情況,并創建一個新的worker線程來處理請求。如果長查詢來源于業務請求,則此時所有group都面臨這種問題,此時主機可能會由于負載過大,導致hang住的情況。這種情況線程池本身無能為力,因為源頭可能是爛SQL并發,或者SQL沒有走對執行計劃導致,通過其他方法,比如SQL高低水位限流或者SQL過濾手段可以應急處理。但是,還有另外一種情況,就是dump任務。很多下游依賴于數據庫的原始數據,通常通過dump命令將數據拉到下游,而這種dump任務通常都是耗時比較長,所以也可以認為是大查詢。如果dump任務集中在一個group內,并導致其他正常業務請求無法立即響應,這個是不能容忍的,因為此時數據庫并沒有壓力,只是因為采用了線程池策略,才導致了請求響應不及時,為了解決這個問題,mysql數據庫將group中處理dump任務的線程不計入thread_pool_oversubscribe累計值,避免上述問題。