MySQL5.5半同步複製實現原理

笑天居士發表於2013-12-06

在semi-sync replication中,master等待binlog被成功寫入到至少一個slave的relay log之後才會繼續執行commit,否則會出現等待。當超過預定的等待時間之後,semi-syncreplication會被禁用,切換回非同步再提交。

mysql複製(半同步)Semi-sync replication在一定程度上保證提交的事務至少有一個已經傳到slave上了。

google提出了兩個解決方法:full synchronous replication和semi-synchronous replication。但是full sync不僅要保證事務已經傳遞到slave上,而且還要等到事務在slave上執行完畢才能返回,因此不被推薦。

Semi-sync replication主要流程:

mysql複製(半同步)Detailed Design:

Semi-synchronous commit protocol

Our proposed semi-synchronous replication works in the followingway:

  1. commit the transaction

  2. wait for the replica databases acknowledge that they already received the transaction - this step has a timeout

  3. tell the client that the commit has been processed

在第2步中,如果replica 在預定的等待時間內沒有返回資訊,semi-sync replication會被禁用,切換回asynchronous replication。因此,一個transaction至少需要一次完成TCP環路的時間。

目前的mysqlreplication過程:

在slave端:

Slave呼叫safe_connect()去連線master;

COM_BINLOG_DUMP 獲取master上的binlog資訊,主要有:binlog_filename, binlog_pos, binlog_flag, server_id。

在master端:

處理COM_BINLOG_DUMP;

Mysql_binlog_send()向slave傳送binlog事件。

因為binlog_flag是由slave發出的,並且在master中進行了設定後返回,所以semi-sync主要在binlog_flag上加了一個額外的頭位元組,用來判斷slave是不是同步的目標。

semisync.h:

static const unsigned char kSyncHeader[2];

static constunsigned char kPacketMagicNum;

static constunsigned char kPacketFlagSync;

Packet 由三部分組成:

1.the magic num

2. thebinlog positon

3. the binlog filename

Master端的執行過程:

在master端建立了一棵搜尋樹用來儲存等待事務。當一個事務提交,並且寫到binlog中後,將(binlog_filename, binlog_pos)插入到搜尋樹中,這樣做的目的是為了方便replication thread找出當前等待事務。當事務結束等待後,該事務記錄就會被移除出搜尋樹。

Replication thread讀取binlog event,並且檢查binlog position是否在搜尋樹中,然後根據binlog position在搜尋樹中的位置,設定額外的頭位元組。

Semisync_master.cc:

int ActiveTranx::insert_tranx_node(const char*log_file_name, my_off_t log_file_pos)

{

…………………………

strncpy(ins_node->log_name_, log_file_name, FN_REFLEN-1);

ins_node->log_name_[FN_REFLEN-1] = 0;

ins_node->log_pos_ = log_file_pos;

}

mysql複製(半同步)//讀取slave返回的資訊獲取binlog的pos,用來確定slave讀取事務是否已經完成。

intrepl_semi_report_binlog_update(Binlog_storage_param *param,

const char *log_file,

my_off_t log_pos, uint32 flags)

{

………………….

//為了儲存log_file,log_pos,這樣的目的是為了確定binlog還需要多長時間才能複製到slave

error=repl_semisync.writeTranxInBinlog(log_file, log_pos);

…………………

}

int ReplSemiSyncMaster::writeTranxInBinlog(const char*log_file_name,

my_off_t log_file_pos)

{

……….

lock();

//更新最新事務提交位置

if(commit_file_name_inited_)

{

int cmp =ActiveTranx::compare(log_file_name, log_file_pos,

commit_file_name_, commit_file_pos_);

…………..

//將(log_file_name,log_file_pos)插入到search tree

if (is_on())

{

assert(active_tranxs_ != NULL);

if(active_tranxs_->insert_tranx_node(log_file_name,log_file_pos))

{

………….

}

}

……….

unlock();

………

}

int repl_semi_report_commit(Trans_param *param)

{

……………………

return repl_semisync.commitTrx(binlog_name, param->log_pos);

…………………

}

CommitTrx()的作用是:如果開啟了semi-sync,會讓binlog-dump thread去等待slave返回的資訊,如果超過了等待時間,禁用semi-sync,通知其他事務都無需等待返回資訊。

int ReplSemiSyncMaster::commitTrx(const char*trx_wait_binlog_name,

my_off_t trx_wait_binlog_pos)

//開始呼叫binlog_dump_thread,首先判斷是否有新的semi-sync slave,如果有的話新增到semi-sync master的slave表中。判斷完成後呼叫reportReplyBinlog,以確定該slave是否已經接收到所有的binlog event。

int repl_semi_binlog_dump_start(Binlog_transmit_param*param,

const char *log_file,

my_off_t log_pos)

{

bool semi_sync_slave= repl_semisync.is_semi_sync_slave();

if(semi_sync_slave)

{

repl_semisync.add_slave();

repl_semisync.reportReplyBinlog(param->server_id,log_file, log_pos);

}

…………………..

}

intreportReplyBinlog(uint32 server_id, const char* log_file_name,

my_off_t end_offset);

//傳送binlog中的event到semi-sync slave,前面已經講過,semi synchronous replication主要就是通過傳送packet到各個slave上,用來匹配判斷一個slave是否是semi-sync slave。

intrepl_semi_before_send_event(Binlog_transmit_param *param,

unsigned char*packet, unsigned long len,

const char*log_file, my_off_t log_pos)

{

return repl_semisync.updateSyncHeader(packet, log_file, log_pos, param->server_id);

}

int updateSyncHeader(unsignedchar *packet,

const char *log_file_name,

my_off_t log_file_pos,

uint32 server_id)

{

……………..

if (sync)

{

(packet)[2] =kPacketFlagSync; //即為在binlog_flag 中新增的一個byte

}

……………

}

//在傳送event完之後等待,呼叫readSlaveReply讀取semi-sync slave返回的資訊

int repl_semi_after_send_event(Binlog_transmit_param*param,

const char*event_buf, unsigned long len)

{

if(repl_semisync.is_semi_sync_slave())

{

THD *thd=current_thd;

(void) repl_semisync.readSlaveReply(&thd->net,param->server_id, event_buf);

thd->clear_error();

}

return 0;

}

int ReplSemiSyncMaster::readSlaveReply(NET *net,uint32 server_id,

constchar *event_buf)

{

const char*kWho = "ReplSemiSyncMaster::readSlaveReply";

const unsignedchar *packet;

char log_file_name[FN_REFLEN];

my_off_tlog_file_pos;

…………

//計算網路等待時間

rpl_semi_sync_master_net_wait_num++;

rpl_semi_sync_master_net_wait_time += wait_time;

…………

//如果返回資訊匹配成功,則呼叫reportReplyBinlog來返回報告資訊

result = reportReplyBinlog(server_id, log_file_name,log_file_pos);

………..

return function_exit(kWho, result);

}

Slave端執行過程分析:

//呼叫binlog dump thread去連線master,並且讀取binlog

int repl_semi_slave_request_dump(Binlog_relay_IO_param*param,

uint32 flags)

{

………

//校驗master端有沒安裝semi-sync

query="SHOW VARIABLES LIKE 'rpl_semi_sync_master_enabled'";

……….

//通知master的dump thread該slave一起開啟semi-sync

query="SET @rpl_semi_sync_slave= 1";

………

}

//semi-sync讀取master端傳遞過來的binlog,呼叫slaveReadSyncHeader來識別傳送過來的packet

int repl_semi_slave_read_event(Binlog_relay_IO_param*param,

const char *packet, unsigned long len,

const char **event_buf, unsigned long*event_len)

{

if(rpl_semi_sync_slave_status)

return repl_semisync.slaveReadSyncHeader(packet, len,

&semi_sync_need_reply,

event_buf, event_len);

*event_buf=packet;

*event_len=len;

return 0;

}

//header就是master傳遞過來的packet

//need_reply是判斷master是否會等待slave返回資訊

int ReplSemiSyncSlave::slaveReadSyncHeader(const char*header,

unsignedlong total_len,

bool *need_reply,

constchar **payload,

unsignedlong *payload_len)

{

const char*kWho = "ReplSemiSyncSlave::slaveReadSyncHeader";

int read_res =0;

function_enter(kWho);

if ((unsigned char)(header[0]) == kPacketMagicNum)

{

*need_reply =(header[1] & kPacketFlagSync);

//將header傳遞過來的flag與自身的kPacketFlagSync進行匹配,其中header[1]為slave設定的binlog_flag,傳遞給master後,進行了設定後的新值,可以在updateSyncHeader函式中找到。

………….

}

//slave執行讀取的event,執行完畢後呼叫slaveReply返回資訊給master

int repl_semi_slave_queue_event(Binlog_relay_IO_param*param,

constchar *event_buf,

unsigned long event_len,

uint32flags)

{

if(rpl_semi_sync_slave_status && semi_sync_need_reply)

{

(void) repl_semisync.slaveReply(param->mysql,

param->master_log_name,

param->master_log_pos);

}

return 0;

}

int ReplSemiSyncSlave::slaveReply(MYSQL*mysql,

const char*binlog_filename,

my_off_tbinlog_filepos)

//呼叫mysql->net介面返回結果資訊

 

 

 

在mysql 5.5版本中,半同步實現方法和google在5.0和5.1版本當中的方式有點不同,其實現為兩個plugin的方式,而且對本身程式碼的侵入更小,當然這是在原本的程式碼上進行重構的前提下

semi_sync_master 和semi_sync_slave

分別在主庫和從庫上註冊。

 
install plugin rpl_semi_sync_master soname 'semisync_master.so';
install plugin rpl_semi_sync_slave soname 'semisync_slave.so';

實現原理:

MySQL在事務函式介面,Binlog傳送接收介面,relay刷盤函式介面的實現中都提供了一個鉤子。這些鉤子實現為代理物件。通過向代理物件中註冊觀察者,可以實現在事件觸發時呼叫觀察者提供的回撥函式。從而達到控制主從同步邏輯的目的。

semi-sync中有四個觀察物件,主庫中3個,從庫中1個。

這些觀察者的成員函式會在合適的時候被MySQL內部的代理物件回撥。

master中三個觀察者:

事務觀察者,事務提交或回滾的時候回撥。

 
Trans_observer{

Uint32_t  len   

Int (*after_commit)(Trans_param *param)

Int (*after_rollback)(Trans_param *param);

}

Binlog儲存觀察者。在binlog寫盤的時候回撥。
Binlog_storage_observer{

uint32_t len;

Int (*after_flush)(Binlog_storage_param *param ,const char *log_file, my_off_t log_pos, uint32 flags)   

}

Binlog dump觀察者,會在binlog被髮送到slave的過程中被呼叫。
Binlog_transmit_obaserver{

Uint32_t len;   

Int (*transmit_start)(Binlog_transmit_param *param,const char *log_file, my_off_t log_pos);

Int (*transmit_stop)(Binlog_transmit_param *param);

Int (*reserve_header)(Binlog_transmit_param *param, unsigned char *header, unsigned long size,unsignedlong *len);

Int (*before_send_event)(Binlog_transmit_param *param,unsigned char *packet, unsigned long len,const char*log_file, my_off_t log_pos );

Int (*after_send_event)(Binlog_transmit_param *param,const char *event_buf, unsigned long len);

Int (*after_reset_master)(Binlog_transmit_param *param);

}

slave中一個觀察者

Relay 日誌IO觀察者
Binlog_relay_IO_observer{  

Uint32_t len;

int (*thread_start)(Binlog_relay_IO_param *param);

int (*thread_stop)(Binlog_relay_IO_param *param);

int (*before_request_transmit)(Binlog_relay_IO_param *param, uint32 flags);

int (*after_read_event)(Binlog_relay_IO_param *param,const char *packet, unsigned long len, const char**event_buf, unsigned long *event_len);

int (*after_queue_event)(Binlog_relay_IO_param *param, const char *event_buf, unsigned long event_len,uint32 flags);

int (*after_reset_slave)(Binlog_relay_IO_param *param);

}

mysql內部的代理物件

mysql啟動時init_server_components的過程中,會執行delegates_init()函式。該函式初始化了四個代理物件:

Trans_delegate  transaction_delegate;

Binlog_storage_delegate binlog_storage_delegate;

Binlog_transmit_delegate binlog_transmit_delegate;

Binlog_relay_IO_delegate binlog_relay_io_delegate;

這四個代理類都是Delegeate類的子類。在Delegate中有一個在其中註冊的觀察者列表。

typedef List<Observer_info> Observer_info_list;

所有對該代理中發生的動作感興趣的觀察者均可以通過add_observer(void *observer, st_plugin_int *plugin)函式來向代理註冊。

當事件發生時,代理將迴圈呼叫在其上面註冊的觀察者提供的回撥函式。

例如transaction_delegate代理物件在一個事務提交時被呼叫,則其會呼叫如下一個巨集,喚醒所有的觀察者進行相應的動作。

FOREACH_OBSERVER(ret, after_commit, thd, (&param));

mysql的程式碼中,通過一個巨集來呼叫這四個代理,該巨集定義如下

 



#define RUN_HOOK(group, hook, args) \ 
(group ##_delegate->is_empty() ? \ 
0 : group ##_delegate->hook args)

例如在儲存引擎介面的父類handler當中,ha_commit_trans介面函式的最末尾發生如下一個呼叫。

RUN_HOOK(transaction, after_commit, (thd, FALSE));

該巨集展開之後:

 


transaction_delegate->is_empty()? 0 : transaction_delegate->after_commit(thd,FALSE);

如果在transaction_delegate當中註冊有觀察者。則都會響應after_commit函式。

複製同步過程

有了以上這些介面提供的功能,要實現主從複製的同步,經如下流程。

  • 主庫事務提交時在提交前的最後一刻,進行如下呼叫。

    RUN_HOOK(transaction, after_commit, (thd, FALSE));

該函式呼叫會導致主庫SQL執行緒處於"Waiting for semi-sync ACK from slave",狀態,等 待binlog event傳送到從庫之後得到的響應。

RUN_HOOK呼叫的事務觀察代理最終會呼叫的是Trans_observer觀察者的after_commit方法,該函式的引數Trans_param當中包含了當前事務提交的binlog_name和binlog_pos。該after_commit將binlog位置和從從庫獲得的binlog位置進行對比,如果從庫已經對該binlog或該binlog之後的position傳送了確認包。則提交該事務,否則事務將等待。

主庫當中的binlog dump執行緒保持著reply_file_name和reply_file_pos兩個變數來記錄從庫當中記錄的最新的確認包。在reply_file_name.reply_file_pos之前的binlog都已經被從庫接收並同步到磁碟。

replay_file_namereply_file_pos兩個變數,則由Binlog_transmit_delegate代理呼叫Binlog_transmit_obaserver觀察者的回撥函式after_send_event來維護。即在接收到新的確認包之後就對這兩個變數進行更新。

  • 在從庫當中的binlog_relay_io_delegate代理負責呼叫relay_io_observer觀察者來響應寫relay日誌的操作。在binlog寫入從庫當中的relay日誌之後將向主庫傳送響應。響應回覆當中包含了三個欄位。這三個欄位標誌了從庫當前寫入的這個event對應主庫當中的binlog的檔名和pos

REPLY_MAGIC_NUM

REPLY_BINLOG_POS_

REPLY_BINLOG_NAME

install plugin的過程中,向主庫的三個代理物件註冊三個監聽者,分別監聽事務提交事件,binlog寫盤事件和binlog傳送事件。

在從庫的plugin載入的過程中,向一個代理物件註冊一個監聽者,監聽relay的寫盤事

相關文章