MySQL具體解釋(7)-----------MySQL線程池總結(一)


線程池是Mysql5.6的一個核心功能。對於server應用而言,不管是web應用服務還是DB服務,高並發請求始終是一個繞不開的話題。當有大量請求並發訪問時,一定伴隨着資源的不斷創建和釋放。導致資源利用率低。減少了服務質量。

線程池是一種通用的技術,通過預先創建一定數量的線程,當有請求達到時,線程池分配一個線程提供服務,請求結束后,該線程又去服務其它請求。 通過這樣的方式。避免了線程和內存對象的頻繁創建和釋放,減少了服務端的並發度,減少了上下文切換和資源的競爭,提高資源利用效率。全部服務的線程池本質都是位了提高資源利用效率,而且實現方式也大體同樣。

本文主要說明Mysql線程池的實現原理。

在Mysql5.6出現曾經,Mysql處理連接的方式是One-Connection-Per-Thread,即對於每個數據庫連接,Mysql-Server都會創建一個獨立的線程服務。請求結束后。銷毀線程。

再來一個連接請求,則再創建一個連接,結束后再進行銷毀。這樣的方式在高並發情況下,會導致線程的頻繁創建和釋放。當然,通過thread-cache。我們能夠將線程緩存起來。以供下次使用,避免頻繁創建和釋放的問題,可是無法解決高連接數的問題。

One-Connection-Per-Thread方式隨着連接數暴增。導致須要創建相同多的服務線程。高並發線程意味着高的內存消耗。很多其它的上下文切換(cpu cache命中率減少)以及很多其它的資源競爭,導致服務出現抖動。

相對於One-Thread-Per-Connection方式,一個線程相應一個連接。Thread-Pool實現方式中,線程處理的最小單位是statement(語句),一個線程能夠處理多個連接的請求。

這樣,在保證充分利用硬件資源情況下(合理設置線程池大小)。能夠避免瞬間連接數暴增導致的server抖動。

調度方式實現

Mysql-Server同一時候支持3種連接管理方式,包含No-Threads,One-Thread-Per-Connection和Pool-Threads。No-Threads表示處理連接使用主線程處理。不額外創建線程,這樣的方式主要用於調試;One-Thread-Per-Connection是線程池出現曾經最經常使用的方式,為每個連接創建一個線程服務;Pool-Threads則是本文所討論的線程池方式。Mysql-Server通過一組函數指針來同一時候支持3種連接管理方式。對於特定的方式,將函數指針設置成特定的回調函數,連接管理方式通過thread_handling參數控制。代碼例如以下:

if (thread_handling <= SCHEDULER_ONE_THREAD_PER_CONNECTION)   
  one_thread_per_connection_scheduler(thread_scheduler,
                                      &max_connections,
                                      &connection_count);
else if (thread_handling == SCHEDULER_NO_THREADS)
  one_thread_scheduler(thread_scheduler);
else                                 
  pool_of_threads_scheduler(thread_scheduler, &max_connections,&connection_count);

連接管理流程

  1. 通過poll監聽mysqlport的連接請求
  2. 收到連接后,調用accept接口,創建通信socket
  3. 初始化thd實例,vio對象等
  4. 依據thread_handling方式設置。初始化thd實例的scheduler函數指針
  5. 調用scheduler特定的add_connection函數新建連接

以下代碼展示了scheduler_functions模板和線程池對模板回調函數的實現,這個是多種連接管理的核心。

struct scheduler_functions 
{ 
uint max_threads;

uint *connection_count; 

ulong *max_connections; 

bool (*init)(void); 

bool (*init_new_connection_thread)(void);

void (*add_connection)(THD *thd);

void (*thd_wait_begin)(THD *thd, int wait_type);

void (*thd_wait_end)(THD *thd);

void (*post_kill_notification)(THD *thd);

bool (*end_thread)(THD *thd, bool cache_thread);

void (*end)(void);
};
static scheduler_functions tp_scheduler_functions=

{ 
0, // max_threads
NULL,
NULL, 
tp_init, // init
NULL, // init_new_connection_thread
tp_add_connection, // add_connection
tp_wait_begin, // thd_wait_begin 
tp_wait_end, // thd_wait_end
tp_post_kill_notification, // post_kill_notification 
NULL, // end_thread
tp_end // end

};

線程池的相關參數

  1. thread_handling:表示線程池模型。
  2. thread_pool_size:表示線程池的group個數,一般設置為當前CPU核心數目。理想情況下,一個group一個活躍的工作線程,達到充分利用CPU的目的。
  3. thread_pool_stall_limit:用於timer線程定期檢查group是否“停滯”,參數表示檢測的間隔。
  4. thread_pool_idle_timeout:當一個worker空暇一段時間后會自己主動退出。保證線程池中的工作線程在滿足請求的情況下,保持比較低的水平。
  5. thread_pool_oversubscribe:該參數用於控制CPU核心上“超頻”的線程數。

    這個參數設置值不含listen線程計數。

  6. threadpool_high_prio_mode:表示優先隊列的模式。

線程池實現

上面描寫敘述了Mysql-Server怎樣管理連接。這節重點描寫敘述線程池的實現框架。以及關鍵接口。如圖1

1

圖 1(線程池框架圖)

每個綠色的方框代表一個group,group數目由thread_pool_size參數決定。每個group包括一個優先隊列和普通隊列,包括一個listener線程和若干個工作線程,listener線程和worker線程能夠動態轉換,worker線程數目由工作負載決定,同一時候受到thread_pool_oversubscribe設置影響。

此外。整個線程池有一個timer線程監控group。防止group“停滯”。

關鍵接口

1. tp_add_connection[處理新連接]

1) 創建一個connection對象

2) 依據thread_id%group_count確定connection分配到哪個group

3) 將connection放進相應group的隊列

4) 假設當前活躍線程數為0,則創建一個工作線程

2. worker_main[工作線程]

1) 調用get_event獲取請求

2) 假設存在請求。則調用handle_event進行處理

3) 否則。表示隊列中已經沒有請求,退出結束。

3. get_event[獲取請求]

1) 獲取一個連接請求

2) 假設存在。則馬上返回,結束

3) 若此時group內沒有listener。則線程轉換為listener線程。堵塞等待

4) 若存在listener,則將線程增加等待隊列頭部

5) 線程休眠指定的時間(thread_pool_idle_timeout)

6) 假設依舊沒有被喚醒,是超時,則線程結束。結束退出

7) 否則,表示隊列里有連接請求到來,跳轉1

備注:獲取連接請求前,會推斷當前的活躍線程數是否超過了

thread_pool_oversubscribe+1,若超過了,則將線程進入休眠狀態。

4. handle_event[處理請求]

1) 推斷連接是否進行登錄驗證。若沒有,則進行登錄驗證

2) 關聯thd實例信息

3) 獲取網絡數據包,分析請求

4) 調用do_command函數循環處理請求

5) 獲取thd實例的套接字句柄。推斷句柄是否在epoll的監聽列表中

6) 若沒有,調用epoll_ctl進行關聯

7) 結束

5.listener[監聽線程]

1) 調用epoll_wait進行對group關聯的套接字監聽,堵塞等待

2) 若請求到來,從堵塞中恢復

3) 依據連接的優先級別,確定是放入普通隊列還是優先隊列

4) 推斷隊列中任務是否為空

5) 若隊列為空,則listener轉換為worker線程

6) 若group內沒有活躍線程。則喚醒一個線程

備注:這里epoll_wait監聽group內全部連接的套接字。然后將監聽到的連接

請求push到隊列,worker線程從隊列中獲取任務,然后運行。

6. timer_thread[監控線程]

1) 若沒有listener線程,而且近期沒有io_event事件

2) 則創建一個喚醒或創建一個工作線程

3) 若group近期一段時間沒有處理請求,而且隊列里面有請求。則

4) 表示group已經stall,則喚醒或創建線程

5)檢查是否有連接超時

備注:timer線程通過調用check_stall推斷group是否處於stall狀態。通過調用timeout_check檢查client連接是否超時。

7.tp_wait_begin[進入等待狀態流程]

1) active_thread_count減1,waiting_thread_count加1

2)設置connection->waiting= true

3) 若活躍線程數為0。而且任務隊列不為空,或者沒有監聽線程。則

4) 喚醒或創建一個線程

8.tp_wait_end[結束等待狀態流程]

1) 設置connection的waiting狀態為false

2) active_thread_count加1。waiting_thread_count減1

備注:

1)waiting_threads這個list里面的線程是空暇線程。並不是等待線程。所謂空暇線程是隨時能夠處理任務的線程。而等待線程則是由於等待鎖,或等待io操作等無法處理任務的線程。

2)tp_wait_begin和tp_wait_end的主要作用是因為匯報狀態,即使更新active_thread_count和waiting_thread_count的信息。

9. tp_init/tp_end

分別調用thread_group_init和thread_group_close來初始化和銷毀線程池

線程池與連接池

連接池通常實如今Client端,是指應用(client)創建預先創建一定的連接,利用這些連接服務於client全部的DB請求。如果某一個時刻。空暇的連接數小於DB的請求數。則須要將請求排隊,等待空暇連接處理。通過連接池能夠復用連接,避免連接的頻繁創建和釋放,從而降低請求的平均響應時間,而且在請求繁忙時,通過請求排隊,能夠緩沖應用對DB的沖擊。線程池實如今server端,通過創建一定數量的線程服務DB請求。相對於one-conection-per-thread的一個線程服務一個連接的方式。線程池服務的最小單位是語句,即一個線程能夠相應多個活躍的連接。

通過線程池,能夠將server端的服務線程數控制在一定的范圍,降低了系統資源的競爭和線程上下文切換帶來的消耗,同一時候也避免出現高連接數導致的高並發問題。連接池和線程池相輔相成。通過連接池能夠降低連接的創建和釋放,提高請求的平均響應時間。並能非常好地控制一個應用的DB連接數。但無法控制整個應用集群的連接數規模,從而導致高連接數。通過線程池則能夠非常好地應對高連接數,保證server端能提供穩定的服務。如圖2所看到的,每一個web-server端維護了3個連接的連接池。對於連接池的每一個連接實際不是獨占db-server的一個worker。而是可能與其它連接共享。這里如果db-server僅僅有3個group。每一個group僅僅有一個worker,每一個worker處理了2個連接的請求。

2

圖 2(連接池與線程池框架圖)

線程池優化

1.調度死鎖解決

引入線程池攻克了多線程高並發的問題,但也帶來一個隱患。

如果,A,B兩個事務被分配到不同的group中運行,A事務已經開始。而且持有鎖,但因為A所在的group比較繁忙。導致A運行一條語句后,不能馬上獲得調度運行。而B事務依賴A事務釋放鎖資源。盡管B事務能夠被調度起來。但因為無法獲得鎖資源。導致仍然須要等待,這就是所謂的調度死鎖。

因為一個group會同一時候處理多個連接,但多個連接不是對等的。比方,有的連接是第一次發送請求;而有的連接相應的事務已經開啟,而且持有了部分鎖資源。

為了降低鎖資源爭用,后者顯然應該比前者優先處理,以達到盡早釋放鎖資源的目的。因此在group里面,能夠加入一個優先級隊列。將已經持有鎖的連接,或者已經開啟的事務的連接發起的請求放入優先隊列。工作線程首先從優先隊列獲取任務運行。

2.大查詢處理

假設一種場景。某個group里面的連接都是大查詢,那么group里面的工作線程數非常快就會達到thread_pool_oversubscribe參數設置值,對於興許的連接請求,則會響應不及時(沒有很多其它的連接來處理)。這時候group就發生了stall。

通過前面分析知道。timer線程會定期檢查這樣的情況,並創建一個新的worker線程來處理請求。假設長查詢來源於業務請求,則此時全部group都面臨這樣的問題,此時主機可能會由於負載過大,導致hang住的情況。這樣的情況線程池本身無能為力,由於源頭可能是爛SQL並發。或者SQL沒有走對運行計划導致,通過其它方法。比方SQL高低水位限流或者SQL過濾手段能夠應急處理。可是。還有第二種情況,就是dump任務。非常多下游依賴於數據庫的原始數據,通常通過dump命令將數據拉到下游,而這樣的dump任務通常都是耗時比較長,所以也能夠覺得是大查詢。假設dump任務集中在一個group內,並導致其它正常業務請求無法馬上響應。這個是不能容忍的。由於此時數據庫並沒有壓力,僅僅是由於採用了線程池策略,才導致了請求響應不及時,為了解決問題。我們將group中處理dump任務的線程不計入thread_pool_oversubscribe累計值,避免上述問題。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM