之前一直沒有對semi sync做太多的關注,原因是線上的生産環境使用的非常少,最近需要提升semisync的生産環境優先級,以适應資料保護非常嚴格的場景,借此機會了解一下semisync,順便過了一下大部分代碼,以幫助後續的性能優化工作,以下分析基于代碼mysql5.6.13
semisync的配置非常簡單,采用mysql plugin的方式,在主庫和備庫上分别安裝不同的插件,當然你也可以主庫備庫全部标準安裝上,使用參數來控制sesmisync;
正常情況下,這兩個plugin都應該被标準安裝,誰知道哪天備庫會不會被切換成主庫呢。
安裝完plugin後,我們可以根據拓撲結構來定義主庫和備庫的配置,主要包括以下幾個配置項;
1.rpl_semi_sync_master_enabled —控制主庫上是否開啟semisync, 打開或關閉,立刻生效 2.rpl_semi_sync_slave_enabled —控制備庫是否開啟semisync, 當主庫打開semisync時,則必須至少要有一個連結的備庫是打開semisync的,否則主庫線程每次都會去等待,直至逾時;是以如果想關閉semisync必須要先關閉主庫配置,再關閉備庫配置
3.rpl_semi_sync_master_timeout
—控制主庫上用戶端的等待時間,當超過這麼長時間等待後,用戶端傳回,同步複制退化成原生的異步複制
機關為毫秒,預設值為10000,即10秒
預設打開,表示當備庫起來後,并跟上主庫時,自動切換到同步模式,如果關閉,即使備庫起來并跟上了,也不會啟用半同步;
— 輸出監控資訊的級别,詳細點選見文檔,不同的級别,可能輸出更詳細的資訊,用于debug
運作狀态變量也比較豐富,不細說了,網上介紹的很多,官方文檔也很詳細
采用plugin +代碼hook的方式實作,通過hook來回調在plugin中定義的函數
例如:
run_hook(transaction, after_commit, (head, all));
run_hook的定義在rpl_hander.h中:
是以上例被轉化成
transaction_delegate->after_commit(head, all);
更具體的回調接口函數在sql/rpl_hander.cc檔案中定義
有四類_delegate對象
binlog_storage_delegate、transaction_delegate、binlog_transmit_delegate、binlog_relay_io_delegate
在主庫semisync加載或初始化時,調用函數semi_sync_master_plugin_init,為transaction_delegate,binlog_transmit_delegate和binlog_transmit_delegate增加了observer,分别對應該plugin的變量為trans_observer,storage_observer,transmit_observer,這三個obeserver定義了各自的函數接口,如下:
所有從server層向plugin的函數調用,都是通過函數指針來完成的,是以我們隻需要搞清楚上述幾個函數的調用邏輯,基本就可以清楚semisync plugin在mysql裡的處理邏輯
a.備庫連接配接到主庫時
調用函數repl_semi_binlog_dump_start
hook注入點:
mysql_binlog_send用于每一個binlog dump請求,在開始dump之前,調用repl_semi_binlog_dump_start,該函數主要做以下幾件事情:
1.首先判斷連接配接過來的備庫是否是開啟semisync的,通過備庫使用者是否設定了變量rpl_semi_sync_slave來決定(replsemisyncmaster::is_semi_sync_slave())
2.如果備庫開啟了semisync,增加連接配接的備庫計數(repl_semisync.add_slave()),計數器用變量rpl_semi_sync_master_clients來維持,lock_binlog_鎖來保護
3.
repl_semisync.reportreplybinlog(param->server_id, log_file, log_pos); 後面介紹
b.當備庫從主庫上斷開時,會調用repl_semi_binlog_dump_end
将計數器rpl_semi_sync_master_clients減1
c.我們以執行一條dml為例:
執行一條簡單的sql:insert into t1 values (1);
1.當将binlog寫入到檔案中後(尚未sync),調用repl_semisync.writetranxinbinlog
hook位置(sql/binlog.cc):
repl_semisync.writetranxinbinlog會進一步的調用repl_semisync.writetranxinbinlog(如果主庫的semisync打開的話)來存儲目前的binlog檔案名和偏移量, 更新目前最大的事務binlog位置,存儲在repl_semisync對象的commit_file_name_和commit_file_pos_中(commit_file_name_inited_被設定為true);然後将該事務的位點資訊存儲到active_tranxs中(active_tranxs_->insert_tranx_node(log_file_name, log_file_pos)),這是一個連結清單,用來存儲所有活躍的事務的位點資訊,每個新加的節點都能保證位點在已有節點之後;另外還維持了一個key->value的數組,數組下标即為事務binlog坐标計算的hash,值為相同hash值的連結清單
這些操作都在鎖lock_binlog_的保護下進行的, 即使semisync退化成同步狀态,也會繼續更新位點(但不寫事務節點),主要是為了監控後續slave時候能夠跟上目前的事務bilog狀态;
事實上,有兩個變量來控制semisync是否開啟:
state_ 為true表示同步,為false表示異步狀态,semi sync退化時會修改改變量(replsemisyncmaster::switch_off)
另外一個變量是master_enabled_,這個是由參數rpl_semi_sync_master_enabled來控制的。
2.在事務commit之後,調用函數repl_semi_report_commit
hook位置 (sql/binlog.cc)
process_after_commit_stage_queue在group commit的第三個階段,即commit階段,leader線程來調用,依次為其他線程調用repl_semi_report_commit,進一步的,調用repl_semisync.committrx(binlog_name, param->log_pos)
replsemisyncmaster::committrx的參數是事務的binlog坐标, 該函數是實作用戶端同步等待的主要部分,主要做以下事情:
1)使用者線程進入新的狀态:
”waiting for semi-sync ack from slave“
從show processlist的輸出可以看到如上資訊
2)檢查目前的semisync狀态:
/* this is the real check inside the mutex. */
if (!getmasterenabled() || !is_on())
goto l_end;
這是在持有鎖的狀态下進行的檢查;
3)根據wait_timeout_ 設定逾時時間變量
随後進入while循環,
while (is_on())
{
……
}
隻要semisync沒有退化到異步狀态,就會一直在while循環中等待,直到逾時或者獲得備庫回報;
while循環内的工作包括:
(1)判斷:
當reply_file_name_inited_為true時,如果reply_file_name_及reply_file_pos_大于目前事務等待的位置,表示備庫已經收到了比目前位置更後的事務,這時候無需等待,直接傳回;
當wait_file_name_inited_為true時,比較目前事務位置和(wait_file_name_,wait_file_pos_)的大小,如果當期事務更小,則将wait_file_pos_和wait_file_name_設定為目前事務的值;
否則,若wait_file_name_inited_為false,将wait_file_name_inited_設定為true,同樣将上述兩個變量設定為目前事務的值;
這麼做的目的是為了維持目前需要等待的最小binlog位置
(2)增加等待線程計數rpl_semi_sync_master_wait_sessions++
wait_result = cond_timewait(&abstime); 線程進入condition wait
在喚醒或逾時後rpl_semi_sync_master_wait_sessions–
如果是等待逾時的,rpl_semi_sync_master_wait_timeouts++,并關閉semisync (switch_off(),将state_/wait_file_name_inited_/reply_file_name_inited_設定為false,rpl_semi_sync_master_off_times++,同時喚醒其他等待的線程(cond_binlog_send_))
如果是被喚醒的,則增加計數:rpl_semi_sync_master_trx_wait_num++、rpl_semi_sync_master_trx_wait_time += wait_time, 然後回到1),去檢查相關變量;
4).退出循環後,更新計數
然後傳回;
可以看到,上述關鍵的一步是對(reply_file_name_,reply_file_pos_)的判斷,以決定是否需要繼續等待;該變量的更新由dump線程來觸發
d.dump線程的處理邏輯
那麼在執行一條事務後,dump線程會有哪些調用邏輯呢?
1.開始發送binlog之前需要重置packet(reset_transmit_packet)
調用函數repl_semi_reserve_header,用于在packet的頭部預留位元組,以維護和備庫的互動資訊,目前共預留3個位元組
這裡在packet的頭部拷貝兩個位元組,值為replsemisyncbase::ksyncheader,固定值,作為magic number
隻有備庫開啟了semisync的情況下,才會去保留額外的packet頭部比特位,不管主庫是否開啟了semisync
2.在發送binlog事件之前調用repl_semi_before_send_event,确認是否需要備庫回報,通過設定之前預留的三個位元組的第3個位元組
hook位置(函數mysql_binlog_send)
repl_semi_before_send_event->
repl_semisync.updatesyncheader(packet,
log_file,
log_pos,
param->server_id);
該函數執行以下步驟,目的是填充上一步保留的頭部位元組:
1)檢查主庫和備庫是否同時打開了semisync,如果沒有,直接傳回
2)加鎖lock_binlog_,再次檢查主庫是否開啟semisync
設定sync為false;
3)如果目前semisync是同步狀态(即state_為true)
同樣的先檢查目前的binlog位點是否有其他備庫已經接受到(reply_file_name_inited_為true,并且<reply_file_name_, reply_file_pos_>比目前dump線程的位點要大);則sync為false,goto l_end
如果目前正在等待的事務最小位點(wait_file_name_,wait_file_pos_)比目前dump線程的位點要小(或者wait_file_name_inited_為false,隻有目前dump線程),再次檢查目前dump線程的bin log位點是否是事務的最後一個事件(通過周遊由函數repl_semisync.writetranxinbinlog産生的事務節點hash連結清單來檢查),如果是的話,則設定sync為true
4)如果目前semisync為異步狀态(state_為false)
當commit_file_name_inited_為true時(事務送出位點資訊被更新過,在函數repl_semisync.writetranxinbinlog中),如果dump線程的位點大于等于上次事務送出的位點(commit_file_name_, commit_file_pos_),表示目前dump線程已經追趕上了主庫位點,是以sync被設定為true,
當commit_file_name_inited_為false時,表示還沒有事務送出位點資訊,同樣設定sync為true;
5)當sync為true時,設定packet頭部,告訴備庫需要其提供回報
plugin/semisync/semisync.cc:
3.在發送事件後,調用函數repl_semi_after_send_event來讀取備庫的回報
hook位置(mysql_binlog_send)
如果該事件需要skip,調用replsemisyncmaster::skipslavereply,否則調用replsemisyncmaster::readslavereply;前者隻需要判斷是否設定了事件的頭部需要sync,如果是的,則調用reportreplybinlog; 後者則先讀取備庫傳遞的資料包,從中讀出備庫傳遞的binlog坐标資訊,函數replsemisyncmaster::readslavereply主要有如下流程:
1)如果無需等待,直接傳回,即沒有設定sync标記;
此時可以保證這是事務的最後一個事件;
2)做一次net_flush将緩沖的資料刷走,然後net_clear,等待備庫的回報的資料包(my_net_read)
從my_net_read傳回後,更新統計資訊
從資料包中讀取備庫傳遞過來的binlog位點資訊,然後調用reportreplybinlog:
result = reportreplybinlog(server_id, log_file_name, log_file_pos);
3)replsemisyncmaster::reportreplybinlog的流程如下:
<1>檢查主庫semisync是否打開,如果沒有,goto l_end;
<2>如果目前semisync為異步狀态,嘗試将其切換為同步狀态,(try_switch_on(server_id, log_file_name, log_file_pos);)
<3>如果reply_file_name_inited_為true(大多數情況)
比較目前dump線程接收到備庫回報的位點資訊與(reply_file_name_, reply_file_pos_)做對比,如果小于後者,說明已經有别的備庫讀到更新的事務了,這時候無需更新(reply_file_name_, reply_file_pos_)
semisync隻保證全局至少有一個備庫讀到更新的事務
<4>如果需要,更新reply_file_pos_和reply_file_name_
然後清理目前位點之前的事務節點資訊
562 active_tranxs_->clear_active_tranx_nodes(log_file_name, log_file_pos);
<5> 若目前等待的開啟semisync的備庫(rpl_semi_sync_master_wait_sessions > 0) ,且目前(reply_file_name_, reply_file_pos_) 大于(wait_file_name_, wait_file_pos_),即接收到的備庫回報位點資訊大于等于目前等待的事務的最小位點,則設定
can_release_threads=true;
wait_file_name_inited_ = false; –>這意味着新的等待事務,需要重新設定等待位點資訊
<6>釋放鎖,如果can_release_threads為true,進行一次broadcast,喚醒等待的使用者線程
當接受到主庫發送的binlog後,開啟了semisync的備庫需要為其發送回報
備庫同樣也是為binlog_relay_io_delegate增加一個observer,即relay_io_observer,通過hook的方式回調plugin的函數,主要包括如下接口函數
a.開啟io線程時,在連接配接主庫之前調用repl_semi_slave_io_start
hook位置 (handle_slave_io)
主要就是設定全局變量rpl_semi_sync_slave_status,如果開啟了備庫的semisync則設定該變量為true;
b.連接配接完主庫後,請求發起dump時,調用repl_semi_slave_request_dump
hook位置 (handle_slave_io->request_dump)
該函數使用者檢查主庫是否支援semisync(檢查是否存在rpl_semi_sync_master_enabled變量),如果支援的話,設定使用者變量為rpl_semi_sync_slave:
在主庫上就是通過@rpl_semi_sync_slave來鑒别一個dump請求的slave是否是開啟semisync的;
初始化成功後,我們給主庫一些dml,繼續debug
c.讀取事件,調用函數repl_semi_slave_read_event
由于我們在主庫上是對packet頭部有附加了3個比特的,這裡需要将其讀出來,同時需要更新event_buf及event_len的值;
如果rpl_semi_sync_slave_status為false,表示開啟io線程時未打開semisync,直接使用packet的長度即可,否則調用replsemisyncslave::slavereadsyncheader,去讀取packet的頭部資訊,如果需要給主庫一個ack,則設定semi_sync_need_reply為true
d.當将binlog寫入relaylog之後(即完成函數queue_event之後),調用repl_semi_slave_queue_event
如果備庫開啟了semisync且需要ack時(pl_semi_sync_slave_status && semi_sync_need_reply),調用replsemisyncslave::slavereply,向主庫發送資料包,包括目前的binlog檔案名及偏移量資訊
e.停止io線程時,調用函數repl_semi_slave_io_end,将rpl_semi_sync_slave_status設定為false,這裡判斷的mysql_reply實際上不會用到;
存在的問題主要集中在主庫上:
1.鎖的粒度太粗,看看能不能細化,或者使用lock-free的算法來優化
2.字元串比較的調用,大部分是對binlog檔案名的比較,實際上隻要比較後面的那一串數字就足夠了;嘗試下看看能否有性能優化