本篇文章為大家展示了如何解析MySQL線程池內(nèi)部實現(xiàn)機制,內(nèi)容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。
公司專注于為企業(yè)提供成都網(wǎng)站制作、成都網(wǎng)站設(shè)計、微信公眾號開發(fā)、商城系統(tǒng)網(wǎng)站開發(fā),微信小程序定制開發(fā),軟件按需定制制作等一站式互聯(lián)網(wǎng)企業(yè)服務(wù)。憑借多年豐富的經(jīng)驗,我們會仔細了解各客戶的需求而做出多方面的分析、設(shè)計、整合,為客戶設(shè)計出具風(fēng)格及創(chuàng)意性的商業(yè)解決方案,創(chuàng)新互聯(lián)建站更提供一系列網(wǎng)站制作和網(wǎng)站推廣的服務(wù)。
摘要
在MySQL中,線程池指的是用來管理處理MySQL客戶端連接任務(wù)的線程的一種機制,我廠用的percona版本已經(jīng)是集成了線程池,只需要通過如下參數(shù)開啟即可。
thread_handling=pool-of-threads
在介紹MySQL線程池核心參數(shù)的基礎(chǔ)之上對線程池內(nèi)部實現(xiàn)機制進行進一步介紹。
線程池導(dǎo)讀
線程池概論
在繼續(xù)了解MySQL線程池之前,我們首先要了解為什么線程池的引入可以幫助MySQL提升性能,除了性能之外線程池還有哪些作用?如果把線程看做系統(tǒng)資源那么線程池本質(zhì)上是對系統(tǒng)資源的管理,對于操作系統(tǒng)來說線程的創(chuàng)建和銷毀是比較消耗系統(tǒng)資源的,頻繁的創(chuàng)建與銷毀線程必然給系統(tǒng)帶來不必要的資源浪費,特別是在負載高的情況下這部分開銷嚴重影響系統(tǒng)的資源使用效率從而影響系統(tǒng)的性能與吞吐量,另一方面過多的線程創(chuàng)建又會造成系統(tǒng)資源的過載消耗,同時帶來相對頻繁的線程之間上下文切換問題。系統(tǒng)資源是寶貴的,我認為性能與資源的利用率是緊密相關(guān)的:
資源利用率與性能的同向性
他們往往向著一個方向發(fā)展,好的資源利用與通??梢詭磔^優(yōu)的性能,線程池技術(shù)一方面可以減少線程重復(fù)創(chuàng)建與銷毀這部分開銷,從而更好地利用已經(jīng)創(chuàng)建的線程資源,另一方面也可以控制線程的創(chuàng)建與系統(tǒng)的負載,某些場景對系統(tǒng)起到了保護作用。
如何了解MySQL線程池
通過學(xué)習(xí)掌握MySQL有哪些參數(shù),并深刻理解每個參數(shù)的含義以及這些參數(shù)是如何影響MySQL的等問題是一種很好的學(xué)習(xí)MySQL線程池的一種方式,另外在了解MySQL基本實現(xiàn)原理的基礎(chǔ)之上再對MySQL線程池不足以及可以改進的地方進行更深層次的思考有利于更好地理解MySQL線程池技術(shù)。
線程池核心參數(shù)
MySQL線程池向用戶開放了一些參數(shù),用戶可以修改這些參數(shù)從而影響線程池的行為,下面分別介紹一下這些核心參數(shù)。
thread_pool_size
這個參數(shù)指的是線程組大小,默認是CPU核心數(shù),線程池初始化的時候會根據(jù)這個數(shù)字來生成線程組,每個線程組初始化一個poolfd句柄。
thread_pool_stall_limit
Timer Thread迭代的時間間隔,默認是500ms。
thread_pool_oversubscribe
用于計算線程組是否太過活躍或者太過繁忙,也即系統(tǒng)的負載程度,用于在一定場景決策新的工作線程是否被創(chuàng)建于和任務(wù)是否被處理,這個值默認是3。
thread_pool_max_threads
允許線程池中***的線程數(shù),默認是10000。
thread_pool_idle_timeout
工作線程***空閑時間,工作線程超過這個數(shù)還空閑的話就退出,這個值默認是60秒。
thread_pool_high_prio_mode
這個參數(shù)可用于控制任務(wù)隊列的使用,可取三個值:
transactions
statements
none
當(dāng)為值為statements的時候則線程組只使用優(yōu)先隊列,當(dāng)為值為none的時候則只使用普通隊列,當(dāng)值為transactions的時候配合thread_pool_high_prio_tickets參數(shù)生效,用于控制任務(wù)被放入優(yōu)先隊列的***次數(shù)。
thread_pool_high_prio_tickets
當(dāng)thread_pool_high_prio_mode=transactions的時候每個連接的任務(wù)最多被放入優(yōu)先隊列thread_pool_high_prio_tickets次,并且每放一次遞減,直到小于等于0的時候放入普通隊列,這個值默認是4294967295。
MySQL線程池實現(xiàn)內(nèi)幕
線程池總體架構(gòu)
與JAVA的線程池不同,JAVA線程池中是工作線程而MySQL線程池有一個線程組的概念,線程組內(nèi)部層級才是工作線程,先看看MySQL線程池的大致架構(gòu):
線程池架構(gòu)
上圖大致可以看出線程池內(nèi)部的結(jié)構(gòu),線程組為我們關(guān)注的一個較大的組件,線程組內(nèi)部每個組件的相互協(xié)調(diào)構(gòu)成了線程組,每個線程組良好地工作構(gòu)成了線程池。對于線程池內(nèi)部,我認為值得了解的內(nèi)容主要包括下面幾個方面:
線程組
Worker線程
Check Stall機制
任務(wù)隊列
Listener線程
對于上面列出的幾個方面,后文將會展開介紹。
線程組
MySQL線程池在初始化的時候根據(jù)宿主機的CPU核心數(shù)設(shè)置thread_pool_size,這也就是線程池的線程組的個數(shù)。每個線程組在初始化之后會通過底層的IO庫分配一個網(wǎng)絡(luò)特殊的句柄與之關(guān)聯(lián),IO庫可以通過這個句柄監(jiān)聽與之綁定的socket句柄就緒的IO任務(wù),線程組的結(jié)構(gòu)體定義如下:
struct thread_group_t { mysql_mutex_t mutex; connection_queue_t queue;//低優(yōu)先級任務(wù)隊列 connection_queue_t high_prio_queue;//高優(yōu)先級任務(wù)隊列 worker_list_t waiting_threads; //代表當(dāng)前線程沒有任務(wù)的時候進入等待隊里 worker_thread_t *listener;//讀取網(wǎng)絡(luò)任務(wù)線程 pthread_attr_t *pthread_attr; int pollfd;//特殊的句柄 int thread_count;//線程組中的線程數(shù) int active_thread_count;//當(dāng)前活躍的線程 int connection_count;//分配給當(dāng)前線程組的連接 int waiting_thread_count;//代表的是當(dāng)前線程在執(zhí)行命令的時候處于等待狀態(tài) /* Stats for the deadlock detection timer routine.*/ int io_event_count;//待處理任務(wù)數(shù),從句柄中獲取 int queue_event_count;//從隊列移除的網(wǎng)絡(luò)任務(wù)數(shù),意味著網(wǎng)絡(luò)任務(wù)被處理 ulonglong last_thread_creation_time;//上一次創(chuàng)建工作線程的時候 int shutdown_pipe[2]; bool shutdown; bool stalled; } MY_ALIGNED(512);
線程池由多個線程組構(gòu)成,線程池的細節(jié)基本都在線程組內(nèi)。
worker線程
線程組內(nèi)有0個或多個線程,這里與Netty有些不同,Netty中有固定的線程用于輪訓(xùn)IO事件,工作線程只負責(zé)處理IO任務(wù),而在MySQL線程池中l(wèi)istener只是一種角色,每個線程的角色可以是listener或者是worker,工作線程為listener的時候負責(zé)從poolfd中讀取就緒IO任務(wù),處于worker角色的時候負責(zé)處理這些IO任務(wù),我們需要區(qū)分工作線程的以下幾種狀態(tài)狀態(tài):
活躍狀態(tài):當(dāng)工作線程處于正在處理任務(wù)且的狀態(tài)且未被阻塞的狀態(tài),這意味著工作線程將會消耗CPU,增加系統(tǒng)的負載。如果worker線程將自己設(shè)置為listener則不算進線程組的活躍線程狀態(tài)數(shù)。
空閑狀態(tài):由于沒有任務(wù)處理而被處于的空閑狀態(tài)。
等待狀態(tài):如果工作線程在執(zhí)行命令的過程中由于IO、鎖、條件、sleep等需要等待則線程池將被通知并且將這些工作線程記作等待狀態(tài)。
在線程組中,關(guān)于線程的計數(shù)有如下關(guān)系:
thread_count = active_thread_count + waiting_thread_count + waiting_threads.length + listener.length
thread_count代表線程組中的總線程數(shù),active_thread_count代表當(dāng)前正在工作且未被阻塞的線程數(shù),waiting_thread_count代表的是工作線程任務(wù)的過程中被阻塞的個數(shù),而waiting_threads代表空閑線程列表。
在MySQL線程池中,線程組中busy的線程數(shù)是active_thread_count與waiting_thread_count的總和,因為這些線程此時都不能處理新的任務(wù),因此被認為是繁忙的。如果處于busy狀態(tài)的線程數(shù)大于一定值則線程組被任務(wù)是太繁忙(too many active)了,這會用于決策普通優(yōu)先級的任務(wù)是否能得到及時的處理,這個值被定義為:
thread_pool_oversubscribe + 1
默認值也就是4。如果active_thread_count數(shù)大于等于一定值(同上算法為4),則線程組被認為是太活躍(too busy)了,此時意味著可能過飽滿的CPU負載,這個指標(biāo)用于決策線程組是否還繼續(xù)執(zhí)行普通優(yōu)先級的任務(wù),上面的邏輯總結(jié)一句話為:
在正常工作的情況下,當(dāng)工作線程檢索任務(wù)的時候,如果線程組太活躍(too many active)則即使有任務(wù)工作線程也不會執(zhí)行,如果不是太繁忙(too busy)才會考慮高優(yōu)先級的任務(wù),對于低優(yōu)先級的任務(wù)只有當(dāng)線程組不是太繁忙(too busy)的時候才會執(zhí)行。
注意上面的條件,因此線程池對系統(tǒng)負載具有一定的保護作用,那么問題來了,如果存在一些耗時任務(wù)(如耗時查詢),會不會導(dǎo)致后來任務(wù)被延遲處理?會不會有時候覺得SQL寫得沒問題,但是卻莫名其妙的Long SQL?這就是下面要介紹的Check Stall機制。
Check Stall機制
如果后來的IO任務(wù)被前面執(zhí)行時間過長的任務(wù)影響了怎么辦?這必然會導(dǎo)致一些無辜的任務(wù)(或是一個簡單的INSERT操作,之所以舉INSERT的例子是因為INSERT通常很快)被影響,結(jié)果是有可能會被延遲處理。線程池中有一個Timer Thread,類似我們很多系統(tǒng)里面的Timeout Thread線程,這個線程每隔一定時間間隔就會進行一次迭代,迭代中做的事情包括如下兩個部分:
檢查線程組的負載情況進行工作線程的喚醒與創(chuàng)建。
檢查與處理超時的客戶端連接。
這里主要介紹***部分工作也就是Check Stall機制。Timer Thread周期性地檢查線程組內(nèi)的線程是否被阻塞(stall),所謂阻塞也就是新來了任務(wù)但是線程組內(nèi)沒有線程來處理。Timer Thread通過queue_event_count和IO任務(wù)隊列是否為空來判斷線程組是否為阻塞狀態(tài),每次工作線程檢索任務(wù)的時候queue_event_count都會累加,累加意味著任務(wù)被正常處理,工作線程正常工作,在每一次check_stall之后queue_event_count會被清零,因此如果在一定時間間隔(stall_limit)后的下一次迭代中,IO任務(wù)隊列不為空并且queue_event_count為空,則說明這段時間間隔內(nèi)都沒有工作線程來處理IO任務(wù)了,那么Check Stall機制會嘗試著喚醒或創(chuàng)建一個工作線程,喚醒線程的邏輯很簡單,如果waiting_threads中有空閑線程則喚醒一個空閑線程,否則需要嘗試創(chuàng)建一個工作線程,創(chuàng)建線程不一定會創(chuàng)建成功,我們看看創(chuàng)建線程的條件:
如果沒有空閑線程且沒有活躍線程則立馬創(chuàng)建,這個時候可能是因為沒有任何工作線程或者工作線程都被阻塞了,或者是存在潛在的死鎖。
否則如果距離上次創(chuàng)建的時間大于一定閾值才創(chuàng)建線程,這個閾值由線程組內(nèi)的線程數(shù)決定。
閾值與線程組內(nèi)線程數(shù)的關(guān)系如下:
線程數(shù) | 閾值 |
< 4 | 0 |
< 8 | 50 * 1000 |
< 16 | 100 * 1000 |
>= 16 | 200 * 1000 |
閾值機制能夠有效的防止線程創(chuàng)建過于頻繁。這里遺留個問題,為什么閾值依賴于線程池的線程數(shù)?閾值是否能依賴于thread_pool_stall_limit的值?Check Stall機制可以被認為一個專門的線程做專門的事情,畢竟線程組內(nèi)部邏輯也是蠻混亂的。
任務(wù)隊列
任務(wù)隊列也就是listener每次從poolfd輪訓(xùn)出來的就緒任務(wù),分為優(yōu)先任務(wù)隊列(high_prio_queue)和普通任務(wù)隊列(queue),優(yōu)先隊列中的IO任務(wù)會先被處理,然后普通隊列中的任務(wù)才能夠被處理。那么什么樣的任務(wù)會被認為是優(yōu)先任務(wù)呢?官方列出了兩個條件:
連接處于事務(wù)中。
連接關(guān)聯(lián)的priority tickets值大于0。
參數(shù)priority tickets(thread_pool_high_prio_tickets)的設(shè)計是為了防止高優(yōu)先級的任務(wù)總是被處理,而一些非高優(yōu)先級的任務(wù)處于較長時間的饑餓狀態(tài),畢竟工作線程的創(chuàng)建也是有條件的,當(dāng)高優(yōu)先級的任務(wù)每一次被放入高優(yōu)先級隊列之后都會對priority tickets的值進行減一,因此達到一定次數(shù)priority tickets的值必然會小于等于0,因此避免了***高優(yōu)先級的問題。另外隊列的使用受參數(shù)thread_pool_high_prio_mode影響,可參考對參數(shù)thread_pool_high_prio_mode介紹的部分。當(dāng)就緒IO任務(wù)被輪訓(xùn)出來放入隊列之后會對io_event_count進行累加,當(dāng)IO任務(wù)從隊列取出處理的時候會對queue_event_coun進行計數(shù)。
Listener線程
Listener做的事情主要是從poolfd中輪訓(xùn)與其綁定的socket句柄的就緒IO事件,事件以任務(wù)的形式被放入任務(wù)隊列并做相應(yīng)處理,如果listener讀取了一些IO任務(wù)之后,該怎么辦呢?下面基于兩個問題回答:
listener應(yīng)該自己處理這些任務(wù)嗎?還是將這些任務(wù)放入隊列讓工作線程處理?
如果任務(wù)隊列不為空,我們需要喚醒多少個工作線程?
對于***個問題,通常我們不想經(jīng)常改變listener的等待和喚醒的狀態(tài),因為listener剛被喚醒,因此我們更傾向于讓listener利用它的時間片去做一些工作。如果listener不自己處理工作,這意味著其他線程要被喚醒去做這個工作,這顯然不是很好。而讓listener去做任務(wù)潛在的問題是線程組有可能一段時間網(wǎng)絡(luò)任務(wù)無法及時被處理,這不是主要的問題,因為stall將被Timer Thread檢查。然而總是依賴Timer Thread也是不好的,因為stall_limit有可能被設(shè)置比較長的時間。我們使用下面的策略,如果任務(wù)隊列不空,我們?nèi)蝿?wù)網(wǎng)絡(luò)任務(wù)此時可能比較多,讓其他線程來處理任務(wù),否則listener自己處理任務(wù)。
對于第二個問題,我們通常為每一個線程組保持一個活動線程(活動線程包括正在做任務(wù)的線程),因此喚醒一個工作線程的條件為當(dāng)前活躍前程數(shù)為0,如果沒有線程被喚醒,在只能依靠Timer Thread來檢查stall并進行喚醒了。
上面可以看出,如果任務(wù)隊列不為空,也不一定會有線程來及時處理任務(wù),這就導(dǎo)致了耗時任務(wù)影響了后來任務(wù)的執(zhí)行,未來可能通過摒棄每個線程組只保持一個活躍線程的規(guī)則來避免網(wǎng)絡(luò)任務(wù)長時間得不到處理。
使用MySQL線程池可以提高數(shù)據(jù)庫的性能,設(shè)計者對線程池的創(chuàng)建與任務(wù)的處理機制進行精心的設(shè)計,然而同時也帶來了一些潛在的問題,最明顯的就是耗時任務(wù)對其他任務(wù)調(diào)度的影響,盡管有不足之處但是使用者仍然可以通過掌握線程池的內(nèi)部細節(jié)以及深刻了解開放參數(shù)的含義,通過參數(shù)的調(diào)整來在一定程度上對MySQL線程池的使用進行優(yōu)化。學(xué)以致用,到這里,您是否能夠利用上面介紹的一些知識來解決一些實際問題了呢?
上述內(nèi)容就是如何解析MySQL線程池內(nèi)部實現(xiàn)機制,你們學(xué)到知識或技能了嗎?如果還想學(xué)到更多技能或者豐富自己的知識儲備,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。