關於MySQL線程池,這也許是目前最全面的實用帖!(轉載)

關於MySQL線程池,這也許是目前最全面的實用帖!

最近出現屢次因爲上層組件異常致使DB雪崩的狀況,筆者將部分監控DB啓用了線程池功能,在使用線程池的過程當中不斷深刻學習的同時,也遇到了很多問題。服務器

本文就來詳細講述一下MySQL線程池相關的知識,以幫助廣大DBA快速瞭解MySQL的線程池機制,快速配置MySQL的線程池以及裏面存在的一些坑。 其實我想說,瞭解和使用MySQL線程池,看這篇文章就夠了。架構

1、爲什麼要使用MySQL線程池併發

在介紹爲何要使用線程池以前,咱們都知道隨着DB訪問量愈來愈大,DB的響應時間也會隨之愈來愈大,以下圖:高併發

而DB的訪問大到必定程度時,DB的吞吐量也會出現降低,而且會愈來愈差,以下圖所示:性能

那麼是否有什麼方式,能實現隨着DB的訪問量愈來愈大,DB始終表現出最佳的性能呢?相似下圖的表現:學習

答案就是今天要重點介紹的線程池功能。總結一下,使用線程池的理由有兩個:測試

一、減小線程重複建立與銷燬部分的開銷,提升性能優化

線程池技術經過預先建立必定數量的線程,在監聽到有新的請求時,線程池直接從現有的線程中分配一個線程來提供服務,服務結束後這個線程不會直接銷燬,而是又去處理其餘的請求。這樣就避免了線程和內存對象頻繁建立和銷燬,減小了上下文切換,提升了資源利用率,從而在必定程度上提升了系統的性能和穩定性。線程

二、對系統起到保護做用orm

線程池技術限制了併發線程數,至關於限制了MySQL的runing線程數,不管系統目前有多少鏈接或者請求,超過最大設置的線程數的都須要排隊,讓系統保持高性能水平,從而防止DB出現雪崩,對底層DB起到保護做用。

可能有人會問,使用鏈接池可否也達到相似的效果?

也許有的DBA會把線程池和鏈接池混淆,但其實二者是有很大區別的:鏈接池通常在客戶端設置,而線程池是在DB服務器上配置;另外鏈接池能夠起到避免了鏈接頻繁建立和銷燬,可是沒法控制MySQL活動線程數的目標,在高併發場景下,沒法起到保護DB的做用。比較好的方式是將鏈接池和線程池結合起來使用。

2、MySQL線程池介紹

MySQL線程池簡介

爲了解決one-thread-per-connection(每一個鏈接一個線程)存在的頻繁建立和銷燬大量線程以及高併發狀況下DB雪崩的問題,實現DB在高併發環境依然能保持較高的性能。

Oracle和MariaDB都推出了ThreadPool方案,目前Oracle的Thread pool實現爲Plugin方式,而且只添加到在Enterprise版本中,Percona移植了MariaDB的Thread pool功能,並作了進一步的優化。本文的環境就基於Percona MySQL 5.7版本。

MySQL線程池架構

MySQL的Thread pool(線程池)被劃分爲多個group(組),每一個組又有對應的工做線程,總體的工做邏輯仍是比較複雜,下面我試圖經過簡單的方式來介紹MySQL線程池的工做原理。

一、架構圖

首先來看看Thread Pool的架構圖。

二、Thread Pool的組成

從架構圖中能夠看到Thread Pool由一個Timer線程和多個Thread Group組成,而每一個Thread Group又由兩個隊列、一個listener線程和多個worker線程構成。下面分別來介紹各個部分的做用:

  • 隊列(高優先級隊列和低優先級隊列)

用來存放待執行的IO任務,分爲高優先級隊列和低優先級隊列,高優先級隊列的任務會優先被處理。

什麼任務會放在高優先級隊列呢?

事務中的語句會放到高優先級隊列中,好比一個事務中有兩個update的SQL,有1個已經執行,那麼另一個update的任務就會放在高優先級中。這裏須要注意,若是是非事務引擎,或者開啓了Autocommit的事務引擎,都會放到低優先級隊列中。

還有一種狀況會將任務放到高優先級隊列中,若是語句在低優先級隊列停留過久,該語句也會移到高優先級隊列中,防止餓死。

  • listener線程

listener線程監聽該線程group的語句,並肯定當本身轉變成worker線程,是當即執行對應的語句仍是放到隊列中,判斷的標準是看隊列中是否有待執行的語句。

若是隊列中待執行的語句數量爲0,而listener線程轉換成worker線程,並當即執行對應的語句。若是隊列中待執行的語句數量不爲0,則認爲任務比較多,將語句放入隊列中,讓其餘的線程來處理。這裏的機制是爲了減小線程的建立,由於通常SQL執行都很是快。

  • worker線程

worker線程是真正幹活的線程。

  • Timer線程

Timer線程是用來週期性檢查group是否處於處於阻塞狀態,當出現阻塞的時候,會經過喚醒線程或者新建線程來解決。

具體的檢測方法爲:經過queue_event_count的值和IO任務隊列是否爲空來判斷線程組是否爲阻塞狀態。

每次worker線程檢查隊列中任務的時候,queue_event_count會+1,每次Timer檢查完group是否阻塞的時候會將queue_event_count清0,若是檢查的時候任務隊列不爲空,而queue_event_count爲0,則說明任務隊列沒有被正常處理,此時該group出現了阻塞,Timer線程會喚醒worker線程或者新建一個wokrer線程來處理隊列中的任務,防止group長時間被阻塞。

三、Thread Pool的是如何運做的?

下面描述極簡的Thread Pool運做,只是簡單描述,省略了大量的複雜邏輯,請不要挑刺~

Step1:請求鏈接到MySQL,根據threadid%thread_pool_size肯定落在哪一個group;

Step2:group中的listener線程監聽到所在的group有新的請求之後,檢查隊列中是否有請求還未處理。若是沒有,則本身轉換爲worker線程當即處理該請求,若是隊列中還有未處理的請求,則將對應請求放到隊列中,讓其餘的線程處理;

Step3:group中的thread線程檢查隊列的請求,若是隊列中有請求,則進行處理,若是沒有請求,則休眠,一直沒有被喚醒,超過thread_pool_idle_timeout後就自動退出。線程結束。固然,獲取請求以前會先檢查group中的running線程數是否超過thread_pool_oversubscribe+1,若是超過也會休眠;

Step4:timer線程按期檢查各個group是否有阻塞,若是有,就對wokrer線程進行喚醒或者建立一個新的worker線程。

四、Thread Pool的分配機制

線程池會根據參數thread_pool_size的大小分紅若干的group,每一個group各自維護客戶端發起的鏈接,當客戶端發起鏈接到MySQL的時候,MySQL會跟進鏈接的線程id(thread_id)對thread_pool_size進行取模,從而落到對應的group。

thread_pool_oversubscribe參數控制每一個group的最大併發線程數,每一個group的最大併發線程數爲thread_pool_oversubscribe+1個。若對應的group達到了最大的併發線程數,則對應的鏈接就須要等待。這個分配機制在某個group中有多個慢SQL的場景下會致使普通的SQL運行時間很長,這個問題會在後面作詳細描述。

MySQL線程池參數說明

關於線程池參數很少,使用show variables like 'thread%'能夠看到以下圖的參數,下面就一個一個來解析:

  • thread_handling

該參數是配置線程模型,默認狀況是one-thread-per-connection,即不啓用線程池;將該參數設置爲pool-of-threads即啓用了線程池。

  • thread_pool_size

該參數是設置線程池的Group的數量,默認爲系統CPU的個數,充分利用CPU資源。

  • thread_pool_oversubscribe

該參數設置group中的最大線程數,每一個group的最大線程數爲thread_pool_oversubscribe+1,注意listener線程不包含在內。

  • thread_pool_high_prio_mode

高優先級隊列的控制參數,有三個值(transactions/statements/none),默認是transactions,三個值的含義以下:

transactions:對於已經啓動事務的語句放到高優先級隊列中,不過還取決於後面的thread_pool_high_prio_tickets參數。

statements:這個模式全部的語句都會放到高優先級隊列中,不會使用到低優先級隊列。

none:這個模式不使用高優先級隊列。

  • thread_pool_high_prio_tickets

該參數控制每一個鏈接最多語序多少次被放入高優先級隊列中,默認爲4294967295,注意這個參數只有在thread_pool_high_prio_mode爲transactions的時候纔有效果。

  • thread_pool_idle_timeout

worker線程最大空閒時間,默認爲60秒,超過限制後會退出。

  • thread_pool_max_threads

該參數用來限制線程池最大的線程數,超過該限制後將沒法再建立更多的線程,默認爲100000。

  • thread_pool_stall_limit

該參數設置timer線程的檢測group是否異常的時間間隔,默認爲500ms。

3、MySQL線程池的使用

線程池的使用比較簡單,只須要添加配置後重啓實例便可。

具體配置以下:

#thread pool

thread_handling=pool-of-threads

thread_pool_oversubscribe=3

thread_pool_size=24

performance_schema=off

#extra connection

extra_max_connections = 8

extra_port = 33333

備註:其餘參數默認便可

以上具體的參數在前面已作詳細說明,下面是配置中須要注意的兩個點:

一、之因此添加performance_schema=off,是因爲測試過程當中發現Thread pool和PS同時開啓的時候會出現內存泄漏問題(後文會詳細敘述);

二、添加extra connection是防止線程池滿的狀況下沒法登陸MySQL,所以特地用管理端口,以備緊急的狀況下使用;

重啓實例後,能夠經過show variables like '%thread%';來查看配置的參數是否生效。

4、使用中遇到的問題

在使用線程池的過程當中,我遇到了幾個問題,這裏也順便作個總結:

內存泄漏問題

DB啓用線程池後,內存飆升了8G左右,以下圖:

不但啓用線程池後內存飆升了8G左右,並且內存還在持續增加,很明顯啓用線程池後存在內存泄漏問題了。

網上也有很多的人遇到這個問題,確認是percona的bug致使(jira.percona.com/browse/PS-3…

下面是關閉PS後的內存使用狀況對比:

備註:目前Percona server 5.7.21-20版本已經修復了線程池和PS同時打開內存泄漏的問題,從我測試的狀況來看問題也獲得瞭解決,你們能夠直接使用Percona server 5.7.21-20的版本,以下圖。

撥測異常問題

啓用線程池之後,至關於限制了MySQL的併發線程數,當達到最大線程數的時候,其餘的線程須要等待,新鏈接也會卡在鏈接驗證那一步,這時候會形成撥測程序鏈接MySQL超時,撥測返回錯誤以下:

撥測程序鏈接實例超時後,就會認爲master已經出現問題。極端狀況下,重試屢次都有異常後,就啓動自動切換的操做,將業務切換到從機。

這種狀況有兩種解決辦法:

一、啓用MySQL的旁路管理端口,監控和高可用相關直接使用MySQL的旁路管理端口。

具體作法爲:在my.cnf中添加以下配置後重啓,就能夠經過旁路端口登陸MySQL了,不受線程池最大線程數的影響:

extra_max_connections = 8

extra_port = 33333

備註:建議啓用線程池後,把這個也添加上,方便緊急狀況下進行故障處理。

二、修改高可用探測腳本,將達到線程池最大活動線程數返回的錯誤作異常處理,看成超過最大鏈接數的場景。(備註:超過最大鏈接數只告警,不進行自動切換)

慢SQL引入的問題

隨着對撥測超時的問題的深刻分析,線程池滿只是監控撥測出現超時的其中一種狀況,還有一種狀況是線程池並無滿,線上的兩個配置:

thread_pool_oversubscribe=3

thread_pool_size=24

按照上面的兩個配置來計算的話,總共能併發運行24x(3+1)=96,可是根據屢次問題的追中,發現有屢次線程池並無達到96,也就是說總體的線程池並無滿。那會是什麼問題致使撥測失敗呢?

鑑於線程池的結構和分配機制,經過前面線程池部分的描述,你們都知道了在內部是將線程池分紅一個一個的group,咱們線上配置了24個group,而線程池的分配機制是對Threadid進行取模,而後肯定該線程是落在哪一個group。

出現超時的時候,有不少的load線程到導入數據。也就是說那個時候有部分線程比較慢的狀況。那麼會不會是某個group的線程滿了,從而致使新分配的線程等待?

有了這個猜測之後,接下來就是來驗證這個問題。驗證分爲兩步:

一、抓取線上運行的processlist,而後對threadid取模,看看是否有多個load線程落在同一個group的狀況;

二、在測試環境模擬這種場景,看看是否符合預期。

線上場景分析

先來看線上的場景,經過抓取撥測超時時間點的processlist,找出當時正在load的線程,根據threadid進行去模,並進行彙總統計後,得出以下結果:

能夠看出,當時第4和第7個group的請求個數都超過了4個,說明是單個group滿致使的撥測異常。固然,也會致使部分運行很快的SQL變慢。

測試環境模擬場景分析

爲了構建快速重現環境,我將參數調整以下:

thread_pool_oversubscribe=1

thread_pool_size=2

經過上面參數的調整,能夠計算出最大併發線程爲2x(1+1)=4,以下圖,當活動線程數超過4個後,其餘的線程就必須等待:

我模擬線上環境的方法爲開啓1個線程的慢SQL,這時測試環境的線程池狀況以下:

按照以前的推測,這時Group1的處理能力至關於Group2的處理能力的50%,若是以前的推論是正確的,那麼分配在Group1上的線程就會出現阻塞。

好比此時來了20個線程請求,按照線程池的分配原則,此時Group1和Group2都會分到10個線程請求。若是全部的線程請求耗時都是同樣的,那麼分配到Group1上的線程請求總體處理時間應該是分配到Group2上總體處理時間的2倍。

我使用腳本,併發起12個線程請求,每一個線程請求都運行select sleep(2),那麼在Group1和Group2都空閒的狀況下,運行狀況以下:

2018-03-18-20:23:53

2018-03-18-20:23:53

2018-03-18-20:23:53

2018-03-18-20:23:53

2018-03-18-20:23:55

2018-03-18-20:23:55

2018-03-18-20:23:55

2018-03-18-20:23:55

2018-03-18-20:23:57

2018-03-18-20:23:57

2018-03-18-20:23:57

2018-03-18-20:23:57

每次4個線程,總共運行了6秒。

接下來在Group1被1個長時間運行的線程沾滿之後,看看測試結果是怎麼樣的:

2018-03-18-20:24:35

2018-03-18-20:24:35

2018-03-18-20:24:35

2018-03-18-20:24:37

2018-03-18-20:24:37

2018-03-18-20:24:37

2018-03-18-20:24:39

2018-03-18-20:24:39

2018-03-18-20:24:39

2018-03-18-20:24:41

2018-03-18-20:24:43

2018-03-18-20:24:45

從上面的結果中能夠看出,在沒有阻塞的時候,每次都是4個線程,然後面有1個線程長時間運行的時候,就會出現那個長時間線程對應的group出現排隊的狀況,最後雖然有3個空閒的線程,可是卻只有1個線程在處理(標紅部分結果)。

解決方法有兩個:

一、將thread_pool_oversubscribe適當調大,這個辦法只能緩解相似問題,沒法根治;

二、找到慢的SQL,解決慢的問題。

相關文章
相關標籤/搜索