1 背景
社區(qū)版的MySQL的連接處理方法默認(rèn)是為每個(gè)連接創(chuàng)建一個(gè)工作線程的one-thread-per-connection(Per_thread)模式。這種模式下,由于系統(tǒng)的資源是有限的,隨著連接數(shù)的增加,資源的競(jìng)爭(zhēng)也增加,連接的響應(yīng)時(shí)間也隨之增加,如response time圖所示。對(duì)于數(shù)據(jù)庫(kù)整體吞吐而言,則是在資源未耗盡時(shí)隨著連接數(shù)增加,一旦連接數(shù)超過(guò)了某個(gè)耗盡系統(tǒng)資源的臨界點(diǎn),數(shù)據(jù)庫(kù)整體吞吐就會(huì)各連接的資源爭(zhēng)搶而下降,如下圖所示。

如何避免在連接數(shù)暴增時(shí),因資源競(jìng)爭(zhēng)而導(dǎo)致系統(tǒng)吞吐下降的問(wèn)題呢?MariaDB&&Percona中給出了簡(jiǎn)潔的答案:線程池。線程池的原理在博客中有生動(dòng)的介紹,其大致可類比為早高峰期間大量汽車想通過(guò)一座大橋,如果采用one-thread-per-connection的方式則放任汽車自由行駛,由于橋面寬度有限,最終將導(dǎo)致所有汽車寸步難行。線程池的解決方案是限制同時(shí)行駛的汽車數(shù),讓橋面時(shí)刻保持最大吞吐,盡快讓所有汽車抵達(dá)對(duì)岸。回歸到數(shù)據(jù)庫(kù)本身,線程池的思路即為限制同時(shí)運(yùn)行的線程數(shù),減少線程池間上下文切換和熱鎖爭(zhēng)用,從而對(duì)OLTP工作負(fù)載(CPU消耗較少的查詢)產(chǎn)生積極影響。當(dāng)連接數(shù)上升時(shí),在線程池的幫助下數(shù)據(jù)庫(kù)整體吞吐維持在一個(gè)較高水準(zhǔn),如右上圖所示。
2 Percona線程池實(shí)現(xiàn)
線程池的基本原理為:預(yù)先創(chuàng)建一定數(shù)量的工作線程(worker線程)。在線程池監(jiān)聽(tīng)線程(listener線程)從現(xiàn)有連接中監(jiān)聽(tīng)到新請(qǐng)求時(shí),從工作線程中分配一個(gè)線程來(lái)提供服務(wù)。工作線程在服務(wù)結(jié)束之后不銷毀線程,而是保留在線程池中繼續(xù)等待下一個(gè)請(qǐng)求來(lái)臨。下面我們將從線程池架構(gòu)、新連接的創(chuàng)建與分配、listener線程、worker線程、timer線程等幾個(gè)方面來(lái)介紹percona線程池的實(shí)現(xiàn)。
2.1 線程池的架構(gòu)
線程池由有多個(gè)線程組(thread group)組成和timer線程組成,如下圖所示。線程組的數(shù)量是線程池并發(fā)的上限,通常而言線程組的數(shù)量需要配置成數(shù)據(jù)庫(kù)實(shí)例的CPU數(shù)量,從而充分利用CPU。線程池中還有一個(gè)服務(wù)于所有線程組的timer線程,負(fù)責(zé)周期性檢查線程組是否處于阻塞狀態(tài)。當(dāng)檢測(cè)到阻塞的線程組時(shí),timer線程會(huì)通過(guò)喚醒或創(chuàng)建新的工作線程來(lái)讓線程組恢復(fù)工作。

線程組內(nèi)部由多個(gè)worker線程、0或1個(gè)listener線程、高低優(yōu)先級(jí)事件隊(duì)列(由網(wǎng)絡(luò)事件event構(gòu)成)、mutex、epollfd、統(tǒng)計(jì)信息等組成。如下圖所示:

2.2 新連接的創(chuàng)建與分配
新連接接入時(shí),線程池按照新連接的線程id取模線程組個(gè)數(shù)來(lái)確定新連接歸屬的線程組(thd→thread_id() % group_count)。這樣的分配邏輯非常簡(jiǎn)潔,但由于沒(méi)有充分考慮連接的負(fù)載情況,繁忙的連接可能會(huì)恰巧被分配到相同的線程組,從而導(dǎo)致負(fù)載不均衡的現(xiàn)象,這是percona線程池值得被優(yōu)化的點(diǎn)。
選定新連接歸屬的線程組后,新連接申請(qǐng)被被作為事件放入低優(yōu)先級(jí)隊(duì)列中,等待線程組中worker線程將高優(yōu)先級(jí)事件隊(duì)列處理完后,就會(huì)處理低優(yōu)先級(jí)的隊(duì)列中的請(qǐng)求。
2.3 listener線程
listener線程是負(fù)責(zé)監(jiān)聽(tīng)連接請(qǐng)求的線程,每個(gè)線程組都有一個(gè)listener線程。percona線程池的listener采用epoll實(shí)現(xiàn)。當(dāng)epoll監(jiān)聽(tīng)到請(qǐng)求事件時(shí),listener會(huì)根據(jù)請(qǐng)求事件的類型來(lái)決定將其放入哪個(gè)優(yōu)先級(jí)事件隊(duì)列。將事件放入高優(yōu)先級(jí)隊(duì)列的條件如下,只需要滿足其一即可:
- 當(dāng)前線程池的工作模式為高優(yōu)先級(jí)模式,在此模式下只啟用高優(yōu)先級(jí)隊(duì)列。(mode == TP_HIGH_PRIO_MODE_STATEMENTS)
- 當(dāng)前線程池的工作模式為高優(yōu)先級(jí)事務(wù)模式,在此模式下每個(gè)連接的event最多被放入高優(yōu)先級(jí)隊(duì)列threadpool_high_prio_tickets次。超過(guò)threadpool_high_prio_tickets次后,該連接的的請(qǐng)求事件只能被放入低優(yōu)先級(jí)。(mode == TP_HIGH_PRIO_MODE_TRANSACTIONS)
- 該連接持有表鎖
- 連接持有mdl鎖
- 連接持有全局讀鎖
- 接持有backup鎖
被放入高優(yōu)先級(jí)事件隊(duì)列的事件可以優(yōu)先被worker線程處理。只有當(dāng)高優(yōu)先級(jí)隊(duì)列為空,并且當(dāng)前線程組不繁忙的時(shí)候才處理低優(yōu)先級(jí)隊(duì)列中的事件。線程組繁忙(too_many_busy_threads)的判斷條件是當(dāng)前組內(nèi)活躍工作線程數(shù)+組內(nèi)處于等待狀態(tài)的線程數(shù)大于線程組工作線程額定值(thread_pool_oversubscribe+1)。這樣的設(shè)計(jì)可能帶來(lái)的問(wèn)題是在高優(yōu)先級(jí)隊(duì)列不為空或者線程組繁忙時(shí)低優(yōu)先級(jí)隊(duì)列中的事件遲遲得不到響應(yīng),這同樣也是percona線程池值得被優(yōu)化的一個(gè)點(diǎn)。listener線程將事件放入高低優(yōu)先級(jí)隊(duì)列后,如果線程組的活躍worker數(shù)量為0,則喚醒或創(chuàng)建新的worker線程來(lái)處理事件。
percona的線程池中l(wèi)istener線程和worker線程是可以互相切換的,詳細(xì)的切換邏輯會(huì)在worker線程模塊介紹。epoll監(jiān)聽(tīng)到請(qǐng)求事件時(shí),如果高低優(yōu)先級(jí)事件隊(duì)列都為空,意味著此時(shí)線程組非??臻e,大概率不存在活躍的worker線程。listener在此情況下會(huì)將除第一個(gè)事件外的所有事件按前述規(guī)則放入高低優(yōu)先級(jí)事件隊(duì)列,然后退出監(jiān)聽(tīng)任務(wù),親自處理第一個(gè)事件。這樣設(shè)計(jì)的好處在于當(dāng)線程組非??臻e時(shí),可以避免listener線程將事件放入隊(duì)列,喚醒或創(chuàng)建worker線程來(lái)處理事件的開(kāi)銷,提高工作效率。
2.4 worker線程
worker線程是線程池中真正干活的線程,正常情況下,每個(gè)線程組都會(huì)有一個(gè)活躍的worker線程。worker在理想狀態(tài)下,可以高效運(yùn)轉(zhuǎn)并且快速處理完高低優(yōu)先級(jí)隊(duì)列中的事件。但是在實(shí)際場(chǎng)景中,worker經(jīng)常會(huì)遭遇IO、鎖等等待情況而難以高效完成任務(wù),此時(shí)任憑worker線程等待將使得在隊(duì)列中的事件遲遲得不到處理、甚至可能出現(xiàn)長(zhǎng)時(shí)間沒(méi)有l(wèi)istener線程監(jiān)聽(tīng)新請(qǐng)求的情況。為此,每當(dāng)worker遭遇IO、鎖等等待情況,如果此時(shí)線程組中沒(méi)有l(wèi)istener線程或者高低優(yōu)先級(jí)事件隊(duì)列非空,并且沒(méi)有過(guò)多活躍worker,則會(huì)嘗試喚醒或者創(chuàng)建一個(gè)worker。為了避免短時(shí)間內(nèi)創(chuàng)建大量worker,帶來(lái)系統(tǒng)吞吐波動(dòng),線程池創(chuàng)建worker線程時(shí)有一個(gè)控制單位時(shí)間創(chuàng)建worker線程上限的邏輯,線程組內(nèi)連接數(shù)越多則創(chuàng)建下一個(gè)線程需要等待的時(shí)間越長(zhǎng)。
當(dāng)線程組活躍worker線程數(shù)量大于等于too_many_active_threads+1時(shí),認(rèn)為線程組的活躍worker數(shù)量過(guò)多。此時(shí)需要對(duì)worker數(shù)量進(jìn)行適當(dāng)收斂,首先判斷當(dāng)前線程組是否有l(wèi)istener線程,如果沒(méi)有則將當(dāng)前worker線程轉(zhuǎn)化為listener線程。如果當(dāng)前有l(wèi)istener線程,則在進(jìn)入休眠前嘗試通過(guò)epoll_wait獲取一個(gè)尚未進(jìn)入隊(duì)列的事件,成功獲取到后立刻處理該事件,否則進(jìn)入休眠等待被喚醒,等待threadpool_idle_timeout時(shí)間后仍未被喚醒則銷毀該worker線程。
worker線程與listener線程的切換如下圖所示:

2.5 timer線程
timer線程每隔threadpool_stall_limit時(shí)間進(jìn)行一次所有線程組的掃描(check_stall)。當(dāng)線程組高低優(yōu)先級(jí)隊(duì)列中存在事件,并且自上次檢查至今沒(méi)有新的事件被worker消費(fèi)則認(rèn)為線程組處于停滯狀態(tài)。停滯的主要原因可能是長(zhǎng)時(shí)間執(zhí)行的非阻塞請(qǐng)求, 也可能發(fā)生于線程正在等待但 wait_begin/wait_end (嘗試喚醒或創(chuàng)建新的worker線程)被上層函數(shù)忘記調(diào)用的場(chǎng)景。timer線程會(huì)通過(guò)喚醒或創(chuàng)建新的worker線程來(lái)讓停滯的線程組恢復(fù)工作。timer線程為了盡量減少對(duì)正常工作的線程組的影響,在check_stall時(shí)采用的是try_lock的方式,如果加不上鎖則認(rèn)為線程組運(yùn)轉(zhuǎn)良好,不再去打擾。
timer線程除上述工作外,還負(fù)責(zé)終止空閑時(shí)間超過(guò) wait_timeout 秒的客戶端。