您好,登錄后才能下訂單哦!
本篇文章為大家展示了如何解析MySQL線程池內部實現機制,內容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。
摘要
在MySQL中,線程池指的是用來管理處理MySQL客戶端連接任務的線程的一種機制,我廠用的percona版本已經是集成了線程池,只需要通過如下參數開啟即可。
thread_handling=pool-of-threads
在介紹MySQL線程池核心參數的基礎之上對線程池內部實現機制進行進一步介紹。
線程池導讀
線程池概論
在繼續了解MySQL線程池之前,我們首先要了解為什么線程池的引入可以幫助MySQL提升性能,除了性能之外線程池還有哪些作用?如果把線程看做系統資源那么線程池本質上是對系統資源的管理,對于操作系統來說線程的創建和銷毀是比較消耗系統資源的,頻繁的創建與銷毀線程必然給系統帶來不必要的資源浪費,特別是在負載高的情況下這部分開銷嚴重影響系統的資源使用效率從而影響系統的性能與吞吐量,另一方面過多的線程創建又會造成系統資源的過載消耗,同時帶來相對頻繁的線程之間上下文切換問題。系統資源是寶貴的,我認為性能與資源的利用率是緊密相關的:
資源利用率與性能的同向性
他們往往向著一個方向發展,好的資源利用與通常可以帶來較優的性能,線程池技術一方面可以減少線程重復創建與銷毀這部分開銷,從而更好地利用已經創建的線程資源,另一方面也可以控制線程的創建與系統的負載,某些場景對系統起到了保護作用。
如何了解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);
線程池由多個線程組構成,線程池的細節基本都在線程組內。
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
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
默認值也就是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線程池的使用進行優化。學以致用,到這里,您是否能夠利用上面介紹的一些知識來解決一些實際問題了呢?
上述內容就是如何解析MySQL線程池內部實現機制,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。