Mysql查詢阻塞初探


第一次值班,報警打電話給我說,數據庫復制延時一個多小時,那個時候是半夜啊,但我還是很清醒的起來,開機、vpn、登錄、show processlist,結果發現情況是這樣的:

紅線框表示的是當前每個線程已經執行、等待的時間,最長的3962其實已經超過一個小時,再看其它的操作都是查詢,另外還有一個線程在做flush table操作
從每個線程的狀態可以看出,第一個線程為Copying to tmp table,可以看出這個線程正在做操作,這是一個查詢操作。
現在的問題是數據庫復制延時,那么在這個圖片上面還有一個線程是在做插入操作,狀態為Waiting for tables flush,時間也是3900多秒。由於當時截圖只是上面一部分,所以這里說明一下。
其它的狀態 都是Waiting for tables flush,嗯?所有的操作都在等待一個查詢操作?難道查詢會阻塞其它操作么?不能確定,這個一時半會兒沒有想清楚,但現在先應該是解決問題。
 
從另一個方面,現在第一個操作時間最長,從這個方面也可以猜到應該是這個操作引起的阻塞,同時看了一下藍色框內的用戶名,看到是dm_team,我自己猜的,DM==data monitor,估計是晚上才執行的一些統計業務
根據這幾點,我決定,還是殺吧,但這可不是“寧可錯殺一千 不可放過一個”,殺錯了有可能造成業務故障,不過通過上面三點推斷,應該是這個沒錯。
 
所以執行下面這個操作:
kill 753037
殺了,通過show processlist, show slave status\G等命令得知,復制延遲已經在減小,當前執行的語句也不都是Waiting for tables flush狀態了,看來我猜對了,問題解決
但是為什么一個查詢會阻塞其它的查詢呢?我想了一會兒,沒有答案,還是睡覺吧 zzzz....
 
后來因為這個問題一直糾結,想想還是看看為什么一個查詢會導致這么多的阻塞,但最基本的結論是,一個查詢無論如何是不會阻塞的,與朋友討論了一次,說是備份操作執行的flush table會影響到查詢操作,那么這個就要細看了,至少找到一點門道,還是從源碼入手,調試一把....
首先打開一個會話,執行了flush tables操作,然后開了另一個會話,執行查詢操作,沒有任何問題,不會阻塞,而做插入操作時,一直阻塞,但這里上面的問題中沒有做update操作的,說明不是這種場景。
突然注意到,第一個查詢語句執行了3962秒,而備份操作是3959秒,說明查詢操作是先開始的,那么我知道了,應該先開始的是查詢操作,這個查詢時間比較長
構造場景:
用的調試工作是vs2010,首先通過字符串搜索功能,找到”Waiting for tables flush“的位置,它是在函數 TABLE_SHARE::wait_for_old_version中的,函數內容為:
bool  TABLE_SHARE::wait_for_old_version(THD *thd,  struct  timespec *abstime,
                                       uint deadlock_weight)
{
  MDL_context *mdl_context= &thd->mdl_context;
  Wait_for_flush ticket(mdl_context,  this  , deadlock_weight);
  MDL_wait::enum_wait_status wait_status;
 
  mysql_mutex_assert_owner(&LOCK_open);
   /*
    We should enter this method only when share's version is not
    up to date and the share is referenced. Otherwise our
    thread will never be woken up from wait.
  */
  DBUG_ASSERT(version != refresh_version && ref_count != 0);
 
  m_flush_tickets.push_front(&ticket);
 
  mdl_context->m_wait.reset_status();
 
  mysql_mutex_unlock(&LOCK_open);
 
  mdl_context->will_wait_for(&ticket);
 
  mdl_context->find_deadlock();
 
  wait_status= mdl_context->m_wait.timed_wait(thd, abstime, TRUE,
                                               "Waiting for table flush"  );
  ....
}
 
從上面可以看出,這個是關於元數據鎖的,metadata lock=MDL
那么這個時間首先執行對某一個表的查詢操作,調試執行,等到加了元數據鎖之后,也就是執行函數 open_table_get_mdl_lock之后,再在另一個會話中執行另一個操作,操作為flush tables。
此時需要一步步調試,這樣cpu才會有更多的機會被調度到去執行flush,因為此時另一個會話已經加了mdl的表鎖了,鎖類型當然為 MDL_SHARED
執行flush操作的函數是 reload_acl_and_cache中的下面一段代碼:
 
{
...
       if  (thd->global_read_lock.lock_global_read_lock(thd))
                  return  1;                                // Killed
       if  (close_cached_tables(thd, tables,
                              ((options & REFRESH_FAST) ?  FALSE : TRUE),
                              thd->variables.lock_wait_timeout))
 ... 
}
 
首先它會獲取一個全局的mdl的 MDL_SHARED鎖,這是可以的,因為上面加的鎖與這個是兼容的,這個鎖成功加上之后,接着要做的就是 close_cached_tables操作了
這個函數所做的是將表緩存中所有的表都關閉並清除。
因為這個操作會清除所有表的緩存,所以執行的操作如下:
 
{
  .....
    while  (found && ! thd->killed)
  {
    TABLE_SHARE *share;
    found= FALSE;
    mysql_ha_flush(thd);
    DEBUG_SYNC(thd,  "after_flush_unlock"  );
 
    mysql_mutex_lock(&LOCK_open);
 
     if  (!tables)
    {
       for  (uint idx=0 ; idx < table_def_cache.records ; idx++) //遍歷每一個緩存中的表
      {
        share= (TABLE_SHARE*) my_hash_element(&table_def_cache, idx);
         if  (share->has_old_version())//只要當前這個表是有版本
        {
          found= TRUE;
           break  ;
        }
      }
    }
     else
    {
       for  (TABLE_LIST *table= tables; table; table= table->next_local)
      {
        share= get_cached_table_share(table->db, table->table_name);
         if  (share && share->has_old_version())
        {
                  found= TRUE;
           break  ;
        }
      }
    }
 
     if  (found)
    {
       if  (share->wait_for_old_version(thd, &abstime,
                                    MDL_wait_for_subgraph::DEADLOCK_WEIGHT_DDL))
      {
        mysql_mutex_unlock(&LOCK_open);
        result= TRUE;
         goto  err_with_reopen;
      }
    }
 
    mysql_mutex_unlock(&LOCK_open);
  }
....
}
 
上面的代碼是將所有的 table_def_cache緩存中的表,只要是有版本差別的,就會去執行 share->wait_for_old_version函數,而這個函數就是上面給出的報出 "Waiting for table flush"的函數。
但這里有一個前提,就是 只要是有版本差別的,那么現在是不是已經有了版本差別了呢?現在可以看看 share->has_old_version()函數的實現方式:
    inline   bool   share:: has_old_version()  const
  {
     return  version != refresh_version;
  }
上面的version是表緩存對象share中的值,表示當前表的一個版本,而 refresh_version表示的是當前數據庫服務器全局的一個版本,這里只要將所有表關閉一次,那么這個值會加1,代碼如下:
bool  close_cached_tables(THD *thd, TABLE_LIST *tables,
                          bool  wait_for_refresh, ulong timeout)
{
   bool  result= FALSE;
   bool  found= TRUE;
   struct  timespec abstime;
  DBUG_ENTER(  "close_cached_tables"  );
  DBUG_ASSERT(thd || (!wait_for_refresh && !tables));
 
  mysql_mutex_lock(&LOCK_open);
   if  (!tables)//如果是要關閉所有表
  {
     /*
      Force close of all open tables.
 
      Note that code in TABLE_SHARE::wait_for_old_version() assumes that
      incrementing of refresh_version and removal of unused tables and
      shares from TDC happens atomically under protection of LOCK_open,
      or putting it another way that TDC does not contain old shares
      which don't have any tables used.
    */
    refresh_version++;//這里就是將當前系統中全局版本號加1
    DBUG_PRINT(  "tcache"  , ( "incremented global refresh_version to: %lu"  ,
                          refresh_version));
    ......
  }
表緩存對象中的版本version與 refresh_version的關系是,每次打開一個表,都將表的版本設置為當前 refresh_version的值,所以如果沒有被修改掉或者沒有被全部關閉,則2個值是一樣的。
那么現在可以知道,在 close_cached_tables函數一進來就將系統版本加1,而當前這個表沒有做任何修改,則它的版本還是1(假設),而 refresh_version已經是2,所以版本是不同的。
那么現在說回來,正因為我們之前在第一個會話中正在執行一個已經加了表mdl鎖的操作,所以在這里會去執行 share->wait_for_old_version函數,函數體內容最上面已經給出。
 
因為表已經被第一個會話加了讀鎖,所以這里需要去等那個讀鎖被釋放,然后才能關閉,所以要執行 wait_status= mdl_context->m_wait.timed_wait(thd, abstime, TRUE, "Waiting for table flush"  );語句。
這也就是為什么在最上面的圖片中出現的第二個backupdb用戶做備份的時候出現的狀態信息。
 
那么這個問題已經搞清楚,flush table阻塞被阻塞,我們可以理解,因為它必須要等待第一個查詢做完才行。
但下面還有更多的是查詢語句,狀態也是在 Waiting for table flush,查詢會被阻塞?為什么?
 
 
那么接着,再啟動另一個會話,再執行一個查詢,還是一樣的,在第一個會話中慢慢的一步步的調試,讓cpu有機會去做第三個會話的查詢操作,等走到 open_table_get_mdl_lock函數后可以慢慢看,因為這里是在獲取鎖
不出乎意料的是,這個元數據讀鎖是獲得了,因為讀鎖是可以共享的,第一個會話已經得到了,所以第三個會話直接用就行了。
 
到這里,發現沒有出現圖片中的 Waiting for table flush狀態信息啊,繼續往下走吧。。。
在函數open_table中,有下面一段代碼:
      if  (share->has_old_version())
    {
       /*
        We already have an MDL lock. But we have encountered an old
        version of table in the table definition cache which is possible
        when someone changes the table version directly in the cache
        without acquiring a metadata lock (e.g. this can happen during
        "rolling" FLUSH TABLE(S)).
        Release our reference to share, wait until old version of
        share goes away and then try to get new version of table share.
      */
      MDL_deadlock_handler mdl_deadlock_handler(ot_ctx);
       bool  wait_result;
 
      release_table_share(share);
      mysql_mutex_unlock(&LOCK_open);
 
      thd->push_internal_handler(&mdl_deadlock_handler);
      wait_result= tdc_wait_for_old_version(thd, table_list->db,
                                            table_list->table_name,
                                            ot_ctx->get_timeout(),
                                            mdl_ticket->get_deadlock_weight());
      thd->pop_internal_handler();
  ....
 
這里判斷了一次版本,哦哦哦,這里當然是有版本差別的啊,這里先將已經得到的表緩存放掉,然后再次去獲取鎖,通過函數 tdc_wait_for_old_version實現,這個函數內容如下:
static   bool
tdc_wait_for_old_version(THD *thd,  const   char  *db,  const   char  *table_name,
                         ulong wait_timeout, uint deadlock_weight)
{
  TABLE_SHARE *share;
   bool  res= FALSE;
 
  mysql_mutex_lock(&LOCK_open);
   if  ((share= get_cached_table_share(db, table_name)) &&
      share->has_old_version())
  {
     struct  timespec abstime;
    set_timespec(abstime, wait_timeout);
    res= share->wait_for_old_version(thd, &abstime, deadlock_weight);
  }
  mysql_mutex_unlock(&LOCK_open);
   return  res;
}
一看就明白了,現在又回到 wait_for_old_version函數上面了,那一切都可以解決了。
 
總結:
1. 問題的根源不止是一個查詢引起的,原來的結論不變,單一個查詢無論如何不會引起查詢操作阻塞,而是與一個flush table配合起來,將系統元數據版本修改之后一起產生的問題,正好最開始的查詢是一個很慢的查詢(mysql里面經常出現),所以才會有這樣的問題,如果不殺掉,當這個查詢完成,也就沒事了。
2. mysql這樣處理元數據鎖及版本控制似乎傷及面太大,這樣的問題很容易出現,因為晚上經常是做分析及備份的操作的,分析查詢的話很多情況下是慢的,所以這樣容易導致這個問題,所以以后最好要將備份與分析的時間段分開。
3. 有些問題很奇怪(在mysql中尤其多),同時又很難從現象層面去解決里面實現的問題,所以必須要從源碼入手。
 
一直覺得mysql服務器層實現的元數據鎖mdl是很復雜的,一直沒有去認真看,現在通過這個問題看了一下,以后還要找時間將整個mdl部分看清楚,這個在運維工作中個人認為還是很重要的。


免責聲明!

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



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