<b>提要</b>
mysql 5.5.39 release版本正式從源碼裡删除了全局參數timed_mutexes。timed_mutexes原本用來控制是否對innodb引擎的mutex wait進行計時統計,以友善進行性能診斷。為什麼要删除這個參數呢? 下面介紹下相關背景:
<b>innodb的同步鎖機制</b>
innodb封裝了mutex和rw_lock結構來保護記憶體的變量和結構,進行多線程同步,考慮可移植性, mutex使用lock_word或者os mutex來保證原子操作,并使用event條件變量進行阻塞和喚醒操作。
<b>innodb同步鎖引入的資料結構和開銷</b>
<b>1. 全局mutex連結清單</b>
innodb引入了一個全局的連結清單ut_list_base_node_t mutex_list,并使用一個單獨的mutex來保護連結清單。 所有的mutex在create或者free的時候來修改連結清單,有了全局連結清單,也使統計彙總有了可能性,參考指令“show engine innodb mutex”. 雖然需要維護一個全局的連結清單,但這并不會影響太多的性能,因為大部分的mutex的生命周期都是從innodb啟動一直到shutdown。
<b>2. 統計資訊</b>
mutex的結構中,有幾個統計資訊:
lock mutex的主要步驟:
在mysql5.5的版本裡,非univ_debug模式下,innodb僅僅保留了count_os_wait的次數,這也是為了性能的考慮。是以5.5的版本後, timed_mutexes在release下,其實已經不再起作用,是以5.5.39,以及5.6以後,源碼裡都不再保留timed_mutexes。 要麼在debug模式下,啟用這些統計,但上線版本又不可能使用debug模式,是以對于mutex的統計,mysql在後面的版本中使用了performance_schema的等待事件來代替,即:
<b>3. 全局等待隊列</b>
innodb為所有的等待狀态的線程準備了一個隊列,如果擷取mutex失敗,那麼就申請一個cell,進入阻塞狀态,等待signal。 sync_primary_wait_array,有了這個全局的隊列,innodb就可以對這些wait的線程進行統計,比如long semaphore waits就是根據這個隊列進行的查詢。
<b>4. signal丢失</b>
這裡再讨論下signal丢失的情況,我們重新再看下lock mutex的步驟:
如果按照這個時序,線上程2 signal event後,線程1才進入隊列,那麼線程1就永遠處在阻塞狀态,無法喚醒。為了解決signal丢失的情況, innodb啟動了一個背景線程:sync_arr_wake_threads_if_sema_free,每隔1s就輪詢wait數組,如果可以lock,就signal這個event來喚醒線程。
從上面來看,innodb為了mutex和rwlock的移植性,以及為了監控和診斷,添加了多個全局的資料結構,這樣實時的統計才有可能,但也帶來了維護資料結構的開銷。 而timed_mutexes控制的mutex wait時間統計,因為隻在debug模式下進行編譯,而且5.6以後使用performance schema的等待事件進行替代,是以參數做了删除處理。
<b>背景</b>
簡單說來,可選值的安全性從0->2->1遞增,分别對應于mysqld 程序crash可能丢失 -> os crash可能丢失 -> 事務安全。
以上是路人皆知的故事,并且似乎闆上釘釘,無可八卦。
<b>innodb_use_global_flush_log_at_trx_commit</b>
直到2010年的某一天,percona的cto vadim同學覺得這種一刀切的風格不夠靈活,最好把這個變量設定成session級别,每個session自己控制。
但同時為了保持super權限對送出行為的控制,同時增加了innodb_use_global_flush_log_at_trx_commit參數。 這兩個參數的配合邏輯為:
1、若innodb_use_global_flush_log_at_trx_commit為off,則使用session.innodb_flush_log_at_trx_commit;
2、若innodb_use_global_flush_log_at_trx_commit為on,則使用global .innodb_flush_log_at_trx_commit(此時session中仍能設定,但無效)
3、每個session建立時,以目前的global.innodb_flush_log_at_trx_commit 為預設值。
<b>業務應用</b>
這個特性可以用在一些對表的重要性做等級定義的場景。比如同一個執行個體下,某些表資料有外部資料備份,或允許丢失部分事務的情況,對這些表的更新,可以設定 session.innodb_flush_log_at_trx_commit為非1值。
在阿裡雲rds服務中,我們對資料可靠性和可用性要求更高,将 innodb_use_global_flush_log_at_trx_commit設定為on,是以修改session.innodb_flush_log_at_trx_commit也沒有作用,統一使用 global.innodb_flush_log_at_trx_commit = 1。
mysql現行版本中存在一個count(distinct)語句傳回結果錯誤的bug,表現為,實際結果存在值,但是用count(distinct)統計後傳回的是0。
<b>原因分析</b>
count(distinct f)的語義就是計算字段f的去重總數,計算流程大緻如下:
流程一:
1、 構造一個unique集合a1(用tree實作) 2、 對每個值都試圖插入集合a1中 3、 若和a1中現有item重複則直接跳過,不重複則插入并+1 4、 完成後計算集合中元素個數。
細心的同學會看到上面的語句中有一個set tmp_table_size的過程,集合a1并不能無限擴大,大小上限為tmp_table_size。若超過則上述流程變為
流程二:
1、 構造一個unique 集合a1 2、 插入item過程中若大小超過tmp_table_size,則将a1暫時寫到檔案中,再構造集合a2 3、 重複步驟2直到所有的item插入完成 是以若item很多則可能重複生成多個集合a1~an。 4、 對a1~an作合并操作。由于隻是每個集合a保證unique,是以需要做類似歸并排序的操作(實際上不需要排序,隻是掃一遍) 5、 是以合并操作需要一個臨時記憶體,長度為n,單元大小為key_length (key大小)。這個臨時記憶體,用的也是tmp_table_size定義的大小。實際上在合并過程中還需要長為key_length的預留白間作臨時記憶體儲存。是以需要的空間為 (n+1)*key_length。 6、 在進行合并前會判斷tmp_table_size >=(n+1)*key_length, 不滿足則直接放棄合并。其結果就是傳回為0。
<b>案例分析</b>
以上面這個case為例。字段v的單key大小為65 (65 = 32*2+1) 加上tree節點字占空間24位元組共89位元組。單個集合隻能放11個item (1024/89), 是以n為 24 (24>=256/11), 在合并時需要 (24+1)*65= 1625位元組的臨時空間,大于1024,放棄合并。
<b>sql_big_tables</b>
實際上在最初處理這個問題時,dba同學發現社群也有人讨論這個bug,并且指出在set sql_big_tables=on的時候,執行count(distinct)就能正确傳回結果。原因就是在sql_big_tables=on的情況下,構造集合的方式是直接生成一個臨時表,全部插入後直接計算臨時表的大小作為結果,整個過程與tmp_table_size無關。
<b>解決方法</b>
運維上,set sql_big_tables是一個方法,不過會影響性能。調高tmp_table_size算是正招。當然本質上這是一個bug。 代碼上,對于已經走到合并操作的這個邏輯,如果tmp_table_size不夠,應該直接申請新的臨時空間用于合并,完成後釋放。雖然會造成臨時征用記憶體,不過以現有的邏輯來看,臨時征用的記憶體已經不少了.
另外一種時間換空間的方法,就是作多次合并。
相比之下第一種改造比較簡單安全。該bug在rds mysql 5.5 中已經修複。
<b>bug背景</b>
在上個月釋出的新版本中,官方修複了一個mysqldump輸入庫名或表明長度越界的bug。
在mysql的目前限制中,庫名和表名字元串最大長度為name_len=192位元組。在myqldump實作中,需要對輸入的表名做處理,比如增加``防止表名中的特殊字元。這些臨時處理的記憶體,聲明為類似name_buff[name_len+3],這樣在使用者輸入的庫名或表名長度過長時,會造成數組越界讀寫,導緻不可預期的錯誤。
這個修複的邏輯也比較簡單,就是在開始dump前作參數檢查,若發現長度超過name_len的庫/表名,直接抛錯傳回“argument too long”。
<b>細節說明</b>
需要注意的是,該修複改變了mysqldump的行為。由于名字長度超過name_len的庫/表肯定不存在,是以修複之前的邏輯,是報告該表不存在。“table not exists”這個邏輯是可以通過--force 跳過的。而“argument too long”則無視force參數,直接抛錯傳回。
<b>現象描述:</b>
innodb引擎,父表和子表通過foreign constraint進行關聯,因為在更新資料時需要check外鍵constraint,當父表被大量的子表referenced時候,那麼在open innodb資料字典的時候,需要open所有的child table和所有的foreign constraint,導緻持有dict_sys->mutex時間過長,産生long semaphore wait, 然後innodb crash了。
<b>case複現</b>
<b>分析過程</b>
<b>1. 資料字典</b>
innodb使用系統表空間儲存表相關的資料字典,系統的資料字典包括:
在load某個表的時候,分别從這些表中把表相關的index,column, index_field, foreign, foreign_col資料儲存到dictionary cache中。 對應的記憶體對象分别是:dict_col_struct,dict_field_struct,dict_index_struct,dict_table_struct,dict_foreign_struct。
<b>2. open過程</b>
dict_load_table:
<b>3. load foreign的詳細過程</b>
3.1 根據表名t1 查找sys_foreign.
而sys_foreign表上一共有三個索引:
是以,根據for_name='t1', ref_name='t1'檢索出來所有相關的foreign_id.
3.2 加入cache
因為沒有專門的cache,foreign分别加入到for_name->foreign_list, ref_name->referenced_list。 問題的關鍵:因為foreign是全局唯一的,但foreign又與兩個表關聯,是以,有可能在open 其它表的時候已經打開過,是以,create foreign對象後,需要判斷以下四個list,是否已經存在,如果存在就直接使用。
dict_foreign_find:分别查詢這四個list,如果已經存在,則free建立的foreign對象,引用已經存在的。
如果不存在,把建立的foreign加入到for_name->foreign_list,ref_name->referenced_list連結清單中。
<b>4. 問題的原因:</b>
因為第一次load,是以find都沒有找到,但這四個都是list,随着open的越來越多,檢索的代價越來越大。 而整個過程中,都一直持有trx_sys->mutex,最終導緻了long semaphore wait。
<b>5. 問題改進方法:</b>
在mysql 5.5.39版本中,進行了修複,修複的方法就是,除了foreign_list,referenced_list。 另外又增加了兩個red_black tree,如下源碼所示:
這樣dict_foreign_find的過程中,通過red_black tree進行檢索,時間複雜度降到o(log n).