MySQL 線程池內幕

摘要

在MySQL中,線程池指的是用來管理處理MySQL客戶端鏈接任務的線程的一種機制,我廠用的percona版本已是集成了線程池,只須要經過以下參數開啓便可。mysql

thread_handling=pool-of-threads算法

1sql

thread_handling=pool-of-threads數據庫

本文在介紹MySQL線程池核心參數的基礎之上對線程池內部實現機制進行進一步介紹。網絡

線程池導讀

線程池概論

在繼續瞭解MySQL線程池以前,咱們首先要了解爲何線程池的引入能夠幫助MySQL提高性能,除了性能以外線程池還有哪些做用?若是把線程看作系統資源那麼線程池本質上是對系統資源的管理,對於操做系統來講線程的建立和銷燬是比較消耗系統資源的,頻繁的建立與銷燬線程必然給系統帶來沒必要要的資源浪費,特別是在負載高的狀況下這部分開銷嚴重影響系統的資源使用效率從而影響系統的性能與吞吐量,另外一方面過多的線程建立又會形成系統資源的過載消耗,同時帶來相對頻繁的線程之間上下文切換問題。系統資源是寶貴的,我認爲性能與資源的利用率是緊密相關的:架構

資源利用率與性能的同向性socket

他們每每向着一個方向發展,好的資源利用與一般能夠帶來較優的性能,線程池技術一方面能夠減小線程重複建立與銷燬這部分開銷,從而更好地利用已經建立的線程資源,另外一方面也能夠控制線程的建立與系統的負載,某些場景對系統起到了保護做用。性能

如何瞭解MySQL線程池

經過學習掌握MySQL有哪些參數,並深入理解每一個參數的含義以及這些參數是如何影響MySQL的等問題是一種很好的學習MySQL線程池的一種方式,另外在瞭解MySQL基本實現原理的基礎之上再對MySQL線程池不足以及能夠改進的地方進行更深層次的思考有利於更好地理解MySQL線程池技術。學習

線程池核心參數

MySQL線程池向用戶開放了一些參數,用戶能夠修改這些參數從而影響線程池的行爲,下面分別介紹一下這些核心參數。優化

thread_pool_size

這個參數指的是線程組大小,默認是CPU核心數,線程池初始化的時候會根據這個數字來生成線程組,每一個線程組初始化一個poolfd句柄。

thread_pool_stall_limit

Timer Thread迭代的時間間隔,默認是500ms。

thread_pool_oversubscribe

用於計算線程組是否太過活躍或者太過繁忙,也即系統的負載程度,用於在必定場景決策新的工做線程是否被建立於和任務是否被處理,這個值默認是3。

thread_pool_max_threads

容許線程池中最大的線程數,默認是10000。

thread_pool_idle_timeout

工做線程最大空閒時間,工做線程超過這個數還空閒的話就退出,這個值默認是60秒。

thread_pool_high_prio_mode

這個參數可用於控制任務隊列的使用,可取三個值:

  • transactions
  • statements
  • none

當爲值爲statements的時候則線程組只使用優先隊列,當爲值爲none的時候則只使用普通隊列,當值爲transactions的時候配合thread_pool_high_prio_tickets參數生效,用於控制任務被放入優先隊列的最大次數。

thread_pool_high_prio_tickets

當thread_pool_high_prio_mode=transactions的時候每一個鏈接的任務最多被放入優先隊列thread_pool_high_prio_tickets次,而且每放一次遞減,直到小於等於0的時候放入普通隊列,這個值默認是4294967295。

MySQL線程池實現內幕

線程池整體架構

與JAVA的線程池不一樣,JAVA線程池中是工做線程而MySQL線程池有一個線程組的概念,線程組內部層級纔是工做線程,先看看MySQL線程池的大體架構:

線程池架構

上圖大體能夠看出線程池內部的結構,線程組爲咱們關注的一個較大的組件,線程組內部每一個組件的相互協調構成了線程組,每一個線程組良好地工做構成了線程池。對於線程池內部,我認爲值得了解的內容主要包括下面幾個方面:

  • 線程組
  • Worker線程
  • Check Stall機制
  • 任務隊列
  • Listener線程

對於上面列出的幾個方面,後文將會展開介紹。

線程組

MySQL線程池在初始化的時候根據宿主機的CPU核心數設置thread_pool_size,這也就是線程池的線程組的個數。每一個線程組在初始化以後會經過底層的IO庫分配一個網絡特殊的句柄與之關聯,IO庫能夠經過這個句柄監聽與之綁定的socket句柄就緒的IO任務,線程組的結構體定義以下:

struct thread_group_t { mysql_mutex_t mutex; connection_queue_t queue;//低優先級任務隊列 connection_queue_t high_prio_queue;//高優先級任務隊列 worker_list_t waiting_threads; //表明當前線程沒有任務的時候進入等待隊裏 worker_thread_t *listener;//讀取網絡任務線程 pthread_attr_t *pthread_attr; int pollfd;//特殊的句柄 int thread_count;//線程組中的線程數 int active_thread_count;//當前活躍的線程 int connection_count;//分配給當前線程組的鏈接 int waiting_thread_count;//表明的是當前線程在執行命令的時候處於等待狀態 /* Stats for the deadlock detection timer routine.*/ int io_event_count;//待處理任務數,從句柄中獲取 int queue_event_count;//從隊列移除的網絡任務數,意味着網絡任務被處理 ulonglong last_thread_creation_time;//上一次建立工做線程的時候 int shutdown_pipe[2]; bool shutdown; bool stalled; } MY_ALIGNED(512);

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

struct thread_group_t

{

  mysql_mutex_t mutex;

  connection_queue_t queue;//低優先級任務隊列

  connection_queue_t high_prio_queue;//高優先級任務隊列

  worker_list_t waiting_threads; //表明當前線程沒有任務的時候進入等待隊裏

  worker_thread_t *listener;//讀取網絡任務線程

  pthread_attr_t *pthread_attr;

  int  pollfd;//特殊的句柄

  int  thread_count;//線程組中的線程數

  int  active_thread_count;//當前活躍的線程

  int  connection_count;//分配給當前線程組的鏈接

  int  waiting_thread_count;//表明的是當前線程在執行命令的時候處於等待狀態

  /* Stats for the deadlock detection timer routine.*/

  int io_event_count;//待處理任務數,從句柄中獲取

  int queue_event_count;//從隊列移除的網絡任務數,意味着網絡任務被處理  

  ulonglong last_thread_creation_time;//上一次建立工做線程的時候

  int  shutdown_pipe[2];

  bool shutdown;

  bool stalled;

} MY_ALIGNED(512);

線程池由多個線程組構成,線程池的細節基本都在線程組內。

worker線程

線程組內有0個或多個線程,這裏與Netty有些不一樣,Netty中有固定的線程用於輪訓IO事件,工做線程只負責處理IO任務,而在MySQL線程池中listener只是一種角色,每一個線程的角色能夠是listener或者是worker,工做線程爲listener的時候負責從poolfd中讀取就緒IO任務,處於worker角色的時候負責處理這些IO任務,咱們須要區分工做線程的如下幾種狀態狀態:

  • 活躍狀態:當工做線程處於正在處理任務且的狀態且未被阻塞的狀態,這意味着工做線程將會消耗CPU,增長系統的負載。若是worker線程將本身設置爲listener則不算進線程組的活躍線程狀態數。
  • 空閒狀態:因爲沒有任務處理而被處於的空閒狀態。
  • 等待狀態:若是工做線程在執行命令的過程當中因爲IO、鎖、條件、sleep等須要等待則線程池將被通知而且將這些工做線程記做等待狀態。

在線程組中,關於線程的計數有以下關係:

thread_count = active_thread_count + waiting_thread_count + waiting_threads.length + listener.length

1

thread_count = active_thread_count + waiting_thread_count + waiting_threads.length + listener.length

thread_count表明線程組中的總線程數,active_thread_count表明當前正在工做且未被阻塞的線程數,waiting_thread_count表明的是工做線程任務的過程當中被阻塞的個數,而waiting_threads表明空閒線程列表。
在MySQL線程池中,線程組中busy的線程數是active_thread_count與waiting_thread_count的總和,由於這些線程此時都不能處理新的任務,所以被認爲是繁忙的。若是處於busy狀態的線程數大於必定值則線程組被任務是太繁忙(too many active)了,這會用於決策普通優先級的任務是否能獲得及時的處理,這個值被定義爲:

thread_pool_oversubscribe + 1

1

thread_pool_oversubscribe + 1

默認值也就是4。若是active_thread_count數大於等於必定值(同上算法爲4),則線程組被認爲是太活躍(too busy)了,此時意味着可能過飽滿的CPU負載,這個指標用於決策線程組是否還繼續執行普通優先級的任務,上面的邏輯總結一句話爲:

在正常工做的狀況下,當工做線程檢索任務的時候,若是線程組太活躍(too many active)則即便有任務工做線程也不會執行,若是不是太繁忙(too busy)纔會考慮高優先級的任務,對於低優先級的任務只有當線程組不是太繁忙(too busy)的時候纔會執行。

注意上面的條件,所以線程池對系統負載具備必定的保護做用,那麼問題來了,若是存在一些耗時任務(如耗時查詢),會不會致使後來任務被延遲處理?會不會有時候以爲SQL寫得沒問題,可是卻莫名其妙的Long SQL?這就是下面要介紹的Check Stall機制。

Check Stall機制

若是後來的IO任務被前面執行時間過長的任務影響了怎麼辦?這必然會致使一些無辜的任務(或是一個簡單的INSERT操做,之因此舉INSERT的例子是由於INSERT一般很快)被影響,結果是有可能會被延遲處理。線程池中有一個Timer Thread,相似咱們不少系統裏面的Timeout Thread線程,這個線程每隔必定時間間隔就會進行一次迭代,迭代中作的事情包括以下兩個部分:

  • 檢查線程組的負載狀況進行工做線程的喚醒與建立。
  • 檢查與處理超時的客戶端鏈接。

這裏主要介紹第一部分工做也就是Check Stall機制。Timer Thread週期性地檢查線程組內的線程是否被阻塞(stall),所謂阻塞也就是新來了任務可是線程組內沒有線程來處理。Timer Thread經過queue_event_count和IO任務隊列是否爲空來判斷線程組是否爲阻塞狀態,每次工做線程檢索任務的時候queue_event_count都會累加,累加意味着任務被正常處理,工做線程正常工做,在每一次check_stall以後queue_event_count會被清零,所以若是在必定時間間隔(stall_limit)後的下一次迭代中,IO任務隊列不爲空而且queue_event_count爲空,則說明這段時間間隔內都沒有工做線程來處理IO任務了,那麼Check Stall機制會嘗試着喚醒或建立一個工做線程,喚醒線程的邏輯很簡單,若是waiting_threads中有空閒線程則喚醒一個空閒線程,不然須要嘗試建立一個工做線程,建立線程不必定會建立成功,咱們看看建立線程的條件:

  • 若是沒有空閒線程且沒有活躍線程則立馬建立,這個時候多是由於沒有任何工做線程或者工做線程都被阻塞了,或者是存在潛在的死鎖。
  • 不然若是距離上次建立的時間大於必定閾值才建立線程,這個閾值由線程組內的線程數決定。

閾值與線程組內線程數的關係以下:

線程數 閾值
 < 4 0
 < 8 50 * 1000
 < 16 100 * 1000
>= 16 200 * 1000

閾值機制可以有效的防止線程建立過於頻繁。這裏遺留個問題,爲何閾值依賴於線程池的線程數?閾值是否能依賴於thread_pool_stall_limit的值?Check Stall機制能夠被認爲一個專門的線程作專門的事情,畢竟線程組內部邏輯也是蠻混亂的。

任務隊列

任務隊列也就是listener每次從poolfd輪訓出來的就緒任務,分爲優先任務隊列(high_prio_queue)和普通任務隊列(queue),優先隊列中的IO任務會先被處理,而後普通隊列中的任務纔可以被處理。那麼什麼樣的任務會被認爲是優先任務呢?官方列出了兩個條件:

  • 鏈接處於事務中。
  • 鏈接關聯的priority tickets值大於0。

參數priority tickets(thread_pool_high_prio_tickets)的設計是爲了防止高優先級的任務老是被處理,而一些非高優先級的任務處於較長時間的飢餓狀態,畢竟工做線程的建立也是有條件的,當高優先級的任務每一次被放入高優先級隊列以後都會對priority tickets的值進行減一,所以達到必定次數priority tickets的值必然會小於等於0,所以避免了永久高優先級的問題。另外隊列的使用受參數thread_pool_high_prio_mode影響,可參考對參數thread_pool_high_prio_mode介紹的部分。當就緒IO任務被輪訓出來放入隊列以後會對io_event_count進行累加,當IO任務從隊列取出處理的時候會對queue_event_coun進行計數。

Listener線程

Listener作的事情主要是從poolfd中輪訓與其綁定的socket句柄的就緒IO事件,事件以任務的形式被放入任務隊列並作相應處理,若是listener讀取了一些IO任務以後,該怎麼辦呢?下面基於兩個問題回答:

  • listener應該本身處理這些任務嗎?仍是將這些任務放入隊列讓工做線程處理?
  • 若是任務隊列不爲空,咱們須要喚醒多少個工做線程?

對於第一個問題,一般咱們不想常常改變listener的等待和喚醒的狀態,由於listener剛被喚醒,所以咱們更傾向於讓listener利用它的時間片去作一些工做。若是listener不本身處理工做,這意味着其餘線程要被喚醒去作這個工做,這顯然不是很好。而讓listener去作任務潛在的問題是線程組有可能一段時間網絡任務沒法及時被處理,這不是主要的問題,由於stall將被Timer Thread檢查。然而老是依賴Timer Thread也是很差的,由於stall_limit有可能被設置比較長的時間。咱們使用下面的策略,若是任務隊列不空,咱們任務網絡任務此時可能比較多,讓其餘線程來處理任務,不然listener本身處理任務。

對於第二個問題,咱們一般爲每個線程組保持一個活動線程(活動線程包括正在作任務的線程),所以喚醒一個工做線程的條件爲當前活躍前程數爲0,若是沒有線程被喚醒,在只能依靠Timer Thread來檢查stall並進行喚醒了。

上面能夠看出,若是任務隊列不爲空,也不必定會有線程來及時處理任務,這就致使了耗時任務影響了後來任務的執行,將來可能經過摒棄每一個線程組只保持一個活躍線程的規則來避免網絡任務長時間得不處處理。

總結

使用MySQL線程池能夠提升數據庫的性能,設計者對線程池的建立與任務的處理機制進行精心的設計,然而同時也帶來了一些潛在的問題,最明顯的就是耗時任務對其餘任務調度的影響,儘管有不足之處可是使用者仍然能夠經過掌握線程池的內部細節以及深入瞭解開放參數的含義,經過參數的調整來在必定程度上對MySQL線程池的使用進行優化。

 

關於線程總結有點疑問:

在正常工做的狀況下,當工做線程檢索任務的時候,若是線程組太活躍(too many active)則即便有任務工做線程也不會執行,若是不是太繁忙(too busy)纔會考慮高優先級的任務,對於低優先級的任務只有當線程組不是太繁忙(too busy)的時候纔會執行。

個人理解:

在正常工做的狀況下,lister_thread  檢索任務時,

若是線程組太活躍(too busy),則即便有任務工做線程也不會執行。不然,

若是線程組太繁忙(too many active),執行高優先級的任務,

若是線程組不太繁忙,就執行低優先級任務 。

相關文章
相關標籤/搜索