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