天天看點

MySQL 線程池總結

線程池是 MySQL 5.6 的一個核心功能,對于伺服器應用而言,無論是web應用服務還是DB服務,高并發請求始終是一個繞不開的話題。當有大量請求并發通路時,一定伴随着資源的不斷建立和釋放,導緻資源使用率低,降低了服務品質。

線程池是一種通用的技術,通過預先建立一定數量的線程,當有請求達到時,線程池配置設定一個線程提供服務,請求結束後,該線程又去服務其他請求。通過這種方式,避免了線程和記憶體對象的頻繁建立和釋放,降低了服務端的并發度,減少了上下文切換和資源的競争,提高資源利用效率。所有服務的線程池本質都是位了提高資源利用效率,并且實作方式也大體相同。本文主要說明MySQL線程池的實作原理。

在 MySQL 5.6出現以前,MySQL 處理連接配接的方式是

One-Connection-Per-Thread

,即對于每一個資料庫連接配接,MySQL-Server都會建立一個獨立的線程服務,請求結束後,銷毀線程。再來一個連接配接請求,則再建立一個連接配接,結束後再進行銷毀。這種方式在高并發情況下,會導緻線程的頻繁建立和釋放。當然,通過 thread-cache,我們可以将線程緩存起來,以供下次使用,避免頻繁建立和釋放的問題,但是無法解決高連接配接數的問題。

One-Connection-Per-Thread

方式随着連接配接數暴增,導緻需要建立同樣多的服務線程,高并發線程意味着高的記憶體消耗,更多的上下文切換(cpu cache命中率降低)以及更多的資源競争,導緻服務出現抖動。相對于

One-Thread-Per-Connection

方式,一個線程對應一個連接配接,

Thread-Pool

實作方式中,線程處理的最小機關是statement(語句),一個線程可以處理多個連接配接的請求。這樣,在保證充分利用硬體資源情況下(合理設定線程池大小),可以避免瞬間連接配接數暴增導緻的伺服器抖動。

排程方式實作

MySQL-Server 同時支援3種連接配接管理方式,包括

No-Threads

One-Thread-Per-Connection

Pool-Threads

  1. No-Threads 表示處理連接配接使用主線程處理,不額外建立線程,這種方式主要用于調試;
  2. One-Thread-Per-Connection 是線程池出現以前最常用的方式,為每一個連接配接建立一個線程服務;
  3. 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); 
           

複制

連接配接管理流程

通過poll監聽mysql端口的連接配接請求 收到連接配接後,調用accept接口,建立通信socket 初始化thd執行個體,vio對象等 根據thread_handling方式設定,初始化thd執行個體的scheduler函數指針 調用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
};
           

複制

線程池的相關參數

thread_handling: 表示線程池模型。thread_pool_size:表示線程池的group個數,一般設定為目前CPU核心數目。理想情況下,一個group一個活躍的工作線程,達到充分利用CPU的目的。thread_pool_stall_limit:用于timer線程定期檢查group是否“停滞”,參數表示檢測的間隔。thread_pool_idle_timeout:當一個worker空閑一段時間後會自動退出,保證線程池中的工作線程在滿足請求的情況下,保持比較低的水準。thread_pool_oversubscribe:該參數用于控制CPU核心上“超頻”的線程數。這個參數設定值不含listen線程計數。threadpool_high_prio_mode:表示優先隊列的模式。線程池實作

上面描述了Mysql-Server如何管理連接配接,這節重點描述線程池的實作架構,以及關鍵接口。如圖

MySQL 線程池總結

圖 1(線程池架構圖)

每一個綠色的方框代表一個group,group數目由thread_pool_size參數決定。每個group包含一個優先隊列和普通隊列,包含一個listener線程和若幹個工作線程,listener線程和worker線程可以動态轉換,worker線程數目由工作負載決定,同時受到thread_pool_oversubscribe設定影響。此外,整個線程池有一個timer線程監控group,防止group“停滞”。

關鍵接口

tp_add_connection[處理新連接配接]

  1. 建立一個connection對象
  2. 根據thread_id%group_count确定connection配置設定到哪個group
  3. 将connection放進對應group的隊列
  4. 如果目前活躍線程數為0,則建立一個工作線程

worker_main[工作線程]

  1. 調用get_event擷取請求
  2. 如果存在請求,則調用handle_event進行處理
  3. 否則,表示隊列中已經沒有請求,退出結束。

get_event[擷取請求]

  1. 擷取一個連接配接請求
  2. 如果存在,則立即傳回,結束
  3. 若此時group内沒有listener,則線程轉換為listener線程,阻塞等待
  4. 若存在listener,則将線程加入等待隊列頭部
  5. 線程休眠指定的時間(thread_pool_idle_timeout)
  6. 如果依然沒有被喚醒,是逾時,則線程結束,結束退出
  7. 否則,表示隊列裡有連接配接請求到來,跳轉1
備注:擷取連接配接請求前,會判斷目前的活躍線程數是否超過thread_pool_oversubscribe+1,若超過,則将線程進入休眠狀态。

handle_event[處理請求]

  1. 判斷連接配接是否進行登入驗證,若沒有,則進行登入驗證
  2. 關聯thd執行個體資訊
  3. 擷取網絡資料包,分析請求
  4. 調用do_command函數循環處理請求
  5. 擷取thd執行個體的套接字句柄,判斷句柄是否在epoll的監聽清單中
  6. 若沒有,調用epoll_ctl進行關聯
  7. 結束

listener[監聽線程]

  1. 調用epoll_wait進行對group關聯的套接字監聽,阻塞等待
  2. 若請求到來,從阻塞中恢複
  3. 根據連接配接的優先級别,确定是放入普通隊列還是優先隊列
  4. 判斷隊列中任務是否為空
  5. 若隊列為空,則listener轉換為worker線程
  6. 若group内沒有活躍線程,則喚醒一個線程
備注:這裡epoll_wait監聽group内所有連接配接的套接字,然後将監聽到的連接配接請求push到隊列,worker線程從隊列中擷取任務,然後執行。

timer_thread[監控線程]

  1. 若沒有listener線程,并且最近沒有io_event事件
  2. 則建立一個喚醒或建立一個工作線程
  3. 若group最近一段時間沒有處理請求,并且隊列裡面有請求,則
  4. 表示group已經stall,則喚醒或建立線程
  5. 檢查是否有連接配接逾時
備注:timer線程通過調用check_stall判斷group是否處于stall狀态,通過調用timeout_check檢查用戶端連接配接是否逾時。

tp_wait_begin[進入等待狀态流程]

  1. active_thread_count減1,waiting_thread_count加1
  2. 設定connection->waiting= true
  3. 若活躍線程數為0,并且任務隊列不為空,或者沒有監聽線程,則
  4. 喚醒或建立一個線程
  5. tp_wait_end[結束等待狀态流程]
    • 設定connection的waiting狀态為false
    • 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的資訊。

tp_init/tp_end

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

線程池與連接配接池

連接配接池通常實作在 Client 端,是指應用(用戶端)建立預先建立一定的連接配接,利用這些連接配接服務于用戶端所有的DB請求。如果某一個時刻,空閑的連接配接數小于DB的請求數,則需要将請求排隊,等待空閑連接配接處理。通過連接配接池可以複用連接配接,避免連接配接的頻繁建立和釋放,進而減少請求的平均響應時間,并且在請求繁忙時,通過請求排隊,可以緩沖應用對DB的沖擊。

線程池實作在server端,通過建立一定數量的線程服務DB請求,相對于

one-conection-per-thread

的一個線程服務一個連接配接的方式,線程池服務的最小機關是語句,即一個線程可以對應多個活躍的連接配接。通過線程池,可以将 server 端的服務線程數控制在一定的範圍,減少了系統資源的競争和線程上下文切換帶來的消耗,同時也避免出現高連接配接數導緻的高并發問題。

連接配接池和線程池相輔相成,通過連接配接池可以減少連接配接的建立和釋放,提高請求的平均響應時間,并能很好地控制一個應用的DB連接配接數,但無法控制整個應用叢集的連接配接數規模,進而導緻高連接配接數,通過線程池則可以很好地應對高連接配接數,保證server端能提供穩定的服務。

MySQL 線程池總結

圖 2(連接配接池與線程池架構圖)

如圖2所示,每個web-server端維護了3個連接配接的連接配接池,對于連接配接池的每個連接配接實際不是獨占db-server的一個worker,而是可能與其他連接配接共享。這裡假設db-server隻有3個group,每個group隻有一個worker,每個worker處理了2個連接配接的請求。

線程池優化

排程死鎖解決

引入線程池解決了多線程高并發的問題,但也帶來一個隐患。假設,A,B兩個事務被配置設定到不同的group中執行,A事務已經開始,并且持有鎖,但由于A所在的group比較繁忙,導緻A執行一條語句後,不能立即獲得排程執行;而B事務依賴A事務釋放鎖資源,雖然B事務可以被排程起來,但由于無法獲得鎖資源,導緻仍然需要等待,這就是所謂的排程死鎖。由于一個group會同時處理多個連接配接,但多個連接配接不是對等的。比如,有的連接配接是第一次發送請求;而有的連接配接對應的事務已經開啟,并且持有了部分鎖資源。為了減少鎖資源争用,後者顯然應該比前者優先處理,以達到盡早釋放鎖資源的目的。是以在group裡面,可以添加一個優先級隊列,将已經持有鎖的連接配接,或者已經開啟的事務的連接配接發起的請求放入優先隊列,工作線程首先從優先隊列擷取任務執行。

大查詢處理

假設一種場景,某個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累計值,避免上述問題。

參考文檔

http://ourmysql.com/archives/1303

http://blog.chinaunix.net/uid-28364803-id-3431242.html

http://www.atatech.org/articles/31833

https://dev.mysql.com/doc/refman/5.6/en/connection-threads.html