造成的複製延遲原因(從binlog入手分析)

•大事務造成的延遲,其延遲不會從0開始增加,而是直接從主庫執行了多久開始。

比如主庫執行這個事務花費的20秒,那麼延遲就會從20開始,

可以自己細心觀察一下很容易看到。這是因爲Query Event中沒有準確的執行時間

 

log_event.cc

入口:

int Query_log_event::do_apply_event(Relay_log_info const *rli,
const char *query_arg, size_t q_len_arg)

包括: sql_mode , 客戶端字符集,自增環境設置,登陸的db,結果執行時間,ddl和dml執行時間
dml: 數據被寫盤後的時間 thd->set_time(&(common_header->when)); , 一個事務對應一個Query_Event 
ddl: 實際語句執行時間 thd->set_time(&(common_header->when)); 整個語句執行後才寫入執行時間

show slave status 入口:

rpl_slave.cc:

bool show_slave_status_send_data(THD *thd, Master_info *mi,
char* io_gtid_set_buffer,
char* sql_gtid_set_buffer)


if ((mi->get_master_log_pos() == mi->rli->get_group_master_log_pos()) &&
(!strcmp(mi->get_master_log_name(), mi->rli->get_group_master_log_name())))

 

檢查SQL線程是否在中繼日誌的末尾
        檢查應使用兩個條件進行
        condition1:比較日誌位置和
        condition2:比較文件名

獲得 seconds_behind_master 時間
long time_diff= ((long)(time(0) - mi->rli->last_master_timestamp) - mi->clock_diff_with_master);

系統時間 (long)(time(0)
主從之間系統時間差值 mi->clock_diff_with_master

1. 重點:
主從服務器的操作系統時間要一致,因爲這兩個是可變不穩定因素

dml語句和ddl語句的執行時間 mi->rli->last_master_timestamp

 

log_event.cc

Query_log_event::Query_log_event(const char* buf, uint event_len,
const Format_description_event
*description_event,
Log_event_type event_type)

dml(從begin開始) 和 ddl 執行時間
exec_time= end_time.tv_sec - thd_arg->start_time.tv_sec;

rpl_slave.cc 
等於各個Event header 的 timestamp , 事務都是 GTID_EVENT 和 XID_EVENT 提交時間
rli->last_master_timestamp= ev->common_header->when.tv_sec + (time_t) ev->exec_time;

 

(題外話: cnblogs可以隨便給其他網站爬蟲( https://www.colabug.com/2020/0203/6936599/  ) , 我都還沒寫完它就爬 , cnblogs 一點保護不了作者勞動成果

 

 

binlog_event.h
class Log_event_header
·timeval:固定4字節,是新紀元時間(1970年1月1日0時0分0秒)以來的秒數。這個時
間是命令發起的時間。如何定義命令發起呢?它是執行計劃生成後開始實際執行語句的
時候。源碼可以是在dispatch一command函數的開頭設置的(thd->set-time())。言外
之意對於語法意義、權限橙查、優化器生成執行計劃的時間都包括在裏面。其次要注意
了這個時間在從庫計算Seconds一Behind一Master的時候是一個重要的依據


·type_code:固定1字節,event事件的編碼。每TEvent有自己的編碼。
·unmasked_server_id:固定4字節。就是生成這個Event服務端的serverid。即便從庫端開啓了
log-slave-updates%,從庫將主庫的Event寫到BinaryLog,這個serverid也是主庫
的serveride如果這個Event再冫欠傳到主庫,那麼需要跳過。源碼可以在


Log-event::do-shalLskip函數中找到跳過邏輯如下“

·event Len:固定4字節,整個Event的長度。
·end_log_p:固定4字節,下一個Event的開始位置。
.flags:固定2字節,某些Event包含這個標示,比如Format-descriptionlog_event中
LOG_EVENT_BINLOG_IN_USE_F 標示當前Binary log是當前寫入文件。

class Log_event_footer
·Event footer中的crc:固定4字節,這部分就是整個Event的一個crc校驗碼,用於標示Event的完整性

 


因爲能力和篇幅有限,不可能介紹所有的Event,本系列值介紹一些常用的Evnet,下面是本系 列將會介紹到了 Event:
• QUERY_EVENT=2 :在語句模式下記錄實際的語句,在行模式下不記錄任何語句相關 的倍息,
但是DDL始終是記錄的語句,本系列值考慮行模式。因此即便是行模式下的 DDL也會記錄爲語句。
• FORMAT_DESCRIPTION_EVENT= 15:說明Binary log的版本信息。總包含在每一個 Binary log的開頭。
• XID_EVENT=16:當事務提交的時候記錄這個Event其中攜帶了XID信息。
• TABLE_MAP_EVENT = 19: 包含了tablejd和具體表名的映射。
• WRITE_ROWS_EVENT = 30: INSERT語句生成的Event,包含插入的實際數據。是行 模式纔有的。
• UPDATE_ROWS_EVENT = 31: UPDATE語句生成的Event,包含數據的前後印象數 據。是行模式纔有的
• DELETE_ROWS_EVENT = 32: DELETE語句生成的Event,包含實際需要刪除的數 據。是行模式纔有的。
• GTID_LOG_EVENT = 33: 如果開啓GTID模式的時候生成關於GTID的倍息,並且攜帶 last commit和seq number作爲MTS並行回放的依據。
• ANONYMOUS_GTIDJ_OG_EVENT=34:在關閉GTID模式的時候生成,並且攜帶last commit和seq number作爲MTS並行回放的依據#。
• PREVIOUS_GIlDS_LOG_EVENT二35:說明前面所有的Binary log包含的GIlD SET, relay log則代表10線程收到的GTID SET。

參考Log_event_type


一、FORMAT DESCRIPTION EVENT
1、FORMAT_DESCRIPTION_EVENT的作用
攜帶的數據都是固定的,
包含了binary log的版本信息、MySQL的版本信息、Event_header的長度、
以及每個Event type的固定部分的長度。

下面倌患將會保存在從庫的內存中:
• 在從庫的內存中保存主庫的倍息,這個變星是Master_info.mi_description_event。
queue_event函數中case binary_log::FORMAT_DESCRIPTION_EVENT部分。

• 將從庫的relay log的FORMAT_DESCRIPTION_EVENT記錄爲和主庫相同,
即更新 RelayJogInfo.rlLdescription_event

log_event.cc:
int Format_description_log_event::do_apply_event(Relay_log_info const *rli) 函數中如下片段:

如果從站未請求該事件,即Master發送了當從站要求位置> 4時,
該事件將使rli-> group_master_log_pos前進。 
說Slave要位置1000,Format_desc事件的結尾是96。
然後在複製rli-> group_master_log_pos的開始將是0,
然後是96,然後跳到第一個真正詢問的事件(即> 96/* Save the information describing this binlog */
const_cast<Relay_log_info *>(rli)->set_rli_description_event(this);

至少下面幾個地方都會讀取其中的倍息:
1.    每次SQL線程應用Event的時候會獲取Event_header的長度和相應Even個定部分的長度。
2.    I0線程啓動的時候檢測版本,參考函數 get_master_version_and_clock 。
3.    將倌患寫入到 relay log 的開頭的FORMAT_DESCR丨PTION_EVENT中。
4. 位置必須在#4

 

rpl_slave.cc:
static int get_master_version_and_clock(MYSQL* mysql, Master_info* mi)

從庫針對不同版本不同處理 , mysql>=5.0 都能複製
switch (version_number)
{
case 0:
case 1:
case 2:
errmsg = "Master reported unrecognized MySQL version";
err_code= ER_SLAVE_FATAL_ERROR;
sprintf(err_buff, ER(err_code), errmsg);
break;
case 3:
mi->set_mi_description_event(new
Format_description_log_event(1, mysql->server_version));
break;
case 4:
mi->set_mi_description_event(new
Format_description_log_event(3, mysql->server_version));
break;
default:
/*
Master is MySQL >=5.0. Give a default Format_desc event, so that we can
take the early steps (like tests for "is this a 3.23 master") which we
have to take before we receive the real master's Format_desc which will
override this one. Note that the Format_desc we create below is garbage
(it has the format of the *slave*); it's only good to help know if the
master is 3.23, 4.0, etc.
*/
mi->set_mi_description_event(new
Format_description_log_event(4, mysql->server_version));
break;
}
}

 


將binlog event 寫入 binlog cache
bool Format_description_log_event::write(IO_CACHE* file)

if (post_header_len_size == Binary_log_event::LOG_EVENT_TYPES)
相同版本主從服務器複製

else if (post_header_len_size > Binary_log_event::LOG_EVENT_TYPES)
在新的Master和舊的Slave之間複製。但不會從內存複製,所以任何內存不足的讀取。

else
在舊的主服務器和新的從服務器之間複製。
在這種情況下,它可能導致 Master 和 Slave 上發生不同的number_of_events。 當循環relay log時,來自Master的FDE用於在Slave上創建FDE事件,該事件將在此處寫入。 在這種情況下,我們可能最終會讀取更多字節,如post_header_len.size()<Binary_log_event :: LOG_EVENT_TYPES;。 引起內存問題。


binlog_version
server_version
create_timestamp
headerjength
array of post-header

•固定部分,這部分是大小不變的。
1. binlog_version: 2字節 binary log的版本當前爲‘4’。
2. server_version: 50字節,MySQL的版本,爲字符串形式#
3. create_timestamp: 4字節,MySQL每次啓動的時候的第一^^binary log的 FORMAT_DESCRIPTION_EVENT會記錄,其他情況爲0,源碼有如下解釋:
4. headerjength: 1 字節,Event header的長度。當前爲‘19’。
5. array of post-header:當前版本爲39字節#這是一個數組用於保存每個Event類型的固 定部分的大小9

生成時機
這個Event作爲binary log的第一個Event, _般都是在binary log切換的時候發生比如:
• flush binary 丨ogs命令。
• binary log自動切換。
• 重啓MySQL實例

最後注意下在本Event的Event header中flags如果爲 LOG_EVENT_BINLOGJN_USE_F標示 說明當前binary log沒有關閉(比如本binary log爲當前寫入文件或者異常關閉MySQL實 例)^如果異常關閉MySQL實例會檢測這個值決定是否做binary log recovery。

二、PREVIOUS GTIDS LOG EVENT
1、PREVIOUS_GTIDS_LDG_EVENT的作用
這個Event只包含可變部分。通常作爲binary log的第二個Event,用於描述前面所有的binary log包含的GTID SET (包括已經刪除的)。前面我們說過初始化GTID模塊的時候也會掃描 binary log中的這個Event*在relay log同樣包含這個Event,主要用於描述I/O線程接收過哪些 GTID,我們後面能看到MySQL實例初始化的時候可能會掃描relay log中的這個Event來確認 Retrieved_Gtid_Set 。

 

3、 主體格式
整個寫入過程集中在Gtid_set::encode函數中,因爲GTID SET中可能出現多個server_uuid並 且可能出現'gap',因此是可變的。在Gtid_set::encode函數中我們也可以清晰的看到它在循環 GTID SET中的每個server_uuid和每一個GTID SET Interval,如下:
• 可變部分,這部分大小可變
• number of sids: 8字節# 小端顯示,本GITD SET有多少個server_uuid
• server_uuid: 16字節。GTID SET中的server_uuid
• n_intervals: 8字節。本server_uuid下GTID SET Interval的個數
• inter_start: 8字節。每個GTID SET Interval起始的gno
• inter_next: 8字節。每個GTID SET Interval結尾的下一個gno

注意:甶於一個GTIDSET可以包含多個server_uuid,因此第2到第5部分可能包含多個。
如果 還包含多個GTID SET Interval則第4和第5部分也可能多個。(參考圖6-2)


4、 實例解析
下面是一個PREVIOUS_GTIDS_LOG_EVENT (mysqlbinlog —hexdump 輸出>,這種情況 是我手動刪除了 auto.cnf手動構造出來的:

  (cnblog格式能否優美一下)

• 02 00 00 00 00 00 00 00:包含2個server_uuid U小端顯示就是2個。

• 24 98 54 63 a5 36 11 e8 a3 0c 52 54 00 81 38 e4:第一個server_uuid。
• 01 00 00 ◦0 00 00 00 00: njntervals表示本GTID SET Interval的個數。小端顯示就 是1個
• 01 00 00 00 00 00 00 00: inter_start,第一個GTID SET Interva丨的起始的gno爲1
• 08 00 00 00 00 00 00 00: inter_next,第一個GTID SET Interval結尾的下一個gno爲8.
• 6c ea 48 f6 92 6c 11 e9 b1 cb 52 54 00 81 38 e4:第二個server_uuid
• 01 00 00 00 00 00 00 00: njntervals表示本GT丨D SET Interval的個數。小端顯示就 是1個
• 01 00 00 00 00 00 00 00: inter_start,第一個GTID SET Interva丨的起始的gno爲 1
• 05 00 00 00 00 00 00 00: inter_next,第一個GTID SET Interval結尾的下一個gno位

我們看到解析出來的:

• 24985463-a536-11e8-a30c-5254008138e4:1-7
• 6cea48f6-926c-11e9-b1cb-5254008138e4:1-4
可以看到它們是一致的,只是inter_next應該減去了 1,因爲Event中記錄的是GTID SET Interval結尾的下一個gno*


5、生成時機
生成時機一般也是進行binary log切換的時候作爲第二個Event寫入binary log


7、重點 EventGTIDJ_LOG_EVENT
注意:本文分爲正文和附件兩部分,都是圖片格式,如果正文有圖片不清晰可以將附件的圖片 保存到本地查看。
本節比較簡單因爲GTID_LOG_EVENT和ANONYMOUS_GTID_LOG_EVENT格式一致,只是 攜帶的數據不一樣而已,我們只解釋GTID_LOG_EVENT即可
一、GTID LOG EVENT
(1) GTID_LOG_EVENT的作用
GTID的作用我們前面已經說過了,後面還會提及。這裏我們只需要知道GTID_LOG_EVENT 這個Event主要記錄的部分有下面三個:

• GTID的詳細倌息。
• 邏輯時鐘詳細倍息,即last commit和seq number
• 是否爲行模式,比如DDL語句就不是行模式的

我們需要注意顯示開啓事務的情況下GTID_LOG_EVENT和XID_EVENT Event header的 timestamp都是commit命令發起的時間,當然如果沒有顯示開啓事務那麼timestamp還是命 令發起的時間。

 

 

固定部分
• flags: 1字節。主要用於表示是否是行模式的,如果是則爲0X00。不是則爲0X01,注意 DDL都不是行模式的,而是語句模式^
• server_uuid: 16字節。server_uuid參數去掉中間的16進製表示。
• gno: 8字節。小端顯示。表示GTID的序號。
• ts type: 1字節•固定爲02。
• last commit: 8字節。小端顯示。
• seq number: 8字節。小端顯示。
 

(4)簡單解析
下面是一個的GTID_LOG_EVEN丁(mysqlbinlog—hexdump 輸出):

 

#191218 21:08:49 server id 1 end_log_pos 417 CRC32 0xac5430b1
# Position Timestamp Type Master ID Size Master Pos Flags
# 160 e1 24 fa 5d 21 01 00 00 00 41 00 00 00 a1 01 00 00 00 00
# 173 00 12 cf ee 78 e5 80 11 e6 a7 90 00 ff 05 93 af |....x...........|
# 183 ce 02 00 00 00 00 00 00 00 02 01 00 00 00 00 00 |................|
# 193 00 00 02 00 00 00 00 00 00 00 b1 30 54 ac |...........0T.|
# GTID last_committed=1 sequence_number=2 rbr_only=yes
/*!50718 SET TRANSACTION ISOLATION LEVEL READ COMMITTED*//*!*/;

mysql> show variables like '%uuid%';
+---------------+--------------------------------------+
| Variable_name | Value |
+---------------+--------------------------------------+
| server_uuid | 12cfee78-e580-11e6-a790-00ff0593afce |
+---------------+--------------------------------------+
1 row in set (0.00 sec)

• 02 00 00 00 00 00 00 00:這就是GTID的序號,小端顯示就是0X02, 10進制的2
• 02: ts type
• 01 00 00 00 00 00 00 00:即last_committed=1 , 小端顯示就是0X01
• 02 00 00 00 00 00 00 00:即sequence_nunnber=2 ,小端顯示就是0X02

 

• 40 00 00 00 00 00 00 00:這就是GTID的序號,小端顯示就是0X40, 10進制的64
• 02: ts type
• 01 00 00 00 00 00 00 00:即last_committed=1,小端顯示就是0X01
• 02 00 00 00 00 00 00 00:即sequence_nunnber=2 ,小端顯示就是0X02


(5) 生成時機
關於生成時機來講GTID_LOG_EVENT的生成和寫入binary log文件都是order commit的flush 階段,這裏就不過多解釋了

(6) ANONYMOUS_GTID_LOG_EVENT
這是匿名GTID Event, 5.7如果不開啓GTID則使用這種格式。它除了不生成GTID相關倍患外 和GTID_LOG_EVENT保持一致,即如下部分全部爲0:
• server_uuid
• gno
就不單獨解析了,有興趣的朋友可以自行解析一下,比較簡單

(7) GTID三種模式
•自動生成GTID:主庫一般是這種情況(AUTOMATIC_GROUP)
•指定GTID:從庫或者使用GTID_NEXT—般就是這種情況(GTID_GROUP>
•匿名GTID:當然也就是不開啓GTID了時候了(ANONYMOUS_GROUP)
源碼的註釋

rpl_gtid.h

/**
Specifies that the GTID has not been generated yet; it will be
generated on commit. It will depend on the GTID_MODE: if
GTID_MODE<=OFF_PERMISSIVE, then the transaction will be anonymous;
if GTID_MODE>=ON_PERMISSIVE, then the transaction will be assigned
a new GTID.

This is the default value: thd->variables.gtid_next has this state
when GTID_NEXT="AUTOMATIC".

It is important that AUTOMATIC_GROUP==0 so that the default value
for thd->variables->gtid_next.type is AUTOMATIC_GROUP.
*/
AUTOMATIC_GROUP= 0,
/**
Specifies that the transaction has been assigned a GTID (UUID:NUMBER).

thd->variables.gtid_next has this state when GTID_NEXT="UUID:NUMBER".

This is the state of GTID-transactions replicated to the slave.
*/
GTID_GROUP,
/**
Specifies that the transaction is anonymous, i.e., it does not
have a GTID and will never be assigned one.

thd->variables.gtid_next has this state when GTID_NEXT="ANONYMOUS".

This is the state of any transaction generated on a pre-GTID
server, or on a server with GTID_MODE==OFF.
*/
ANONYMOUS_GROUP,
/**
GTID_NEXT is set to this state after a transaction with
GTID_NEXT=='UUID:NUMBER' is committed.

This is used to protect against a special case of unsafe
non-transactional updates.

Background: Non-transactional updates are allowed as long as they
are sane. Non-transactional updates must be single-statement
transactions; they must not be mixed with transactional updates in
the same statement or in the same transaction. Since
non-transactional updates must be logged separately from
transactional updates, a single mixed statement would generate two
different transactions.

Problematic case: Consider a transaction, Tx1, that updates two
transactional tables on the master, t1 and t2. Then slave (s1) later
replays Tx1. However, t2 is a non-transactional table at s1. As such, s1
will report an error because it cannot split Tx1 into two different
transactions. Had no error been reported, then Tx1 would be split into Tx1
and Tx2, potentially causing severe harm in case some form of fail-over
procedure is later engaged by s1.

To detect this case on the slave and generate an appropriate error
message rather than causing an inconsistency in the GTID state, we
do as follows. When committing a transaction that has
GTID_NEXT==UUID:NUMBER, we set GTID_NEXT to UNDEFINED_GROUP. When
the next part of the transaction is being processed, an error is
generated, because it is not allowed to execute a transaction when
GTID_NEXT==UNDEFINED. In the normal case, the error is not
generated, because there will always be a Gtid_log_event after the
next transaction.
*/
UNDEFINED_GROUP,
/*
GTID_NEXT is set to this state by the slave applier thread when it
reads a Format_description_log_event that does not originate from
this server.

Background: when the slave applier thread reads a relay log that
comes from a pre-GTID master, it must preserve the transactions as
anonymous transactions, even if GTID_MODE>=ON_PERMISSIVE. This
may happen, e.g., if the relay log was received when master and
slave had GTID_MODE=OFF or when master and slave were old, and the
relay log is applied when slave has GTID_MODE>=ON_PERMISSIVE.

So the slave thread should set GTID_NEXT=ANONYMOUS for the next
transaction when it starts to process an old binary log. However,
there is no way for the slave to tell if the binary log is old,
until it sees the first transaction. If the first transaction
begins with a Gtid_log_event, we have the GTID there; if it begins
with query_log_event, row events, etc, then this is an old binary
log. So at the time the binary log begins, we just set
GTID_NEXT=NOT_YET_DETERMINED_GROUP. If it remains
NOT_YET_DETERMINED when the next transaction begins,
gtid_pre_statement_checks will automatically turn it into an
anonymous transaction. If a Gtid_log_event comes across before
the next transaction starts, then the Gtid_log_event will just set
GTID_NEXT='UUID:NUMBER' accordingly.
*/
NOT_YET_DETERMINED_GROUP
};

 

 

一、QUERY-EVENT
(1) QUERY_EVENT的作用
QUERY_EVENT不僅會記錄一些語句運行的環境比如SQL_MODE,客戶端字符集、自增環境 , 設置、當前登錄數據庫等,而且會記錄執行時間。但對於行模式的DDL和DML記錄的執行時間 會有所不同t如下:
• DML:執行時間記錄的是第一條數據更改後的時間,而不是真正本條DML語句執行的時 間(一個DML語句可能修改很多條數據),往往這個時間非常短,不能正確的表示DML 語句執行的時間 , 語句部分記錄的是’BEGIN’
• DDL:執行時間是實際語句的執行時間,語句部分記錄的是實際的語句。
執行時間是Seconds_Behind_Master計算的一個影晌因素,對於一個事務來講只有一個這樣的QUERY_EVENT。

 

格式部分(單位:bytes):

 

 

 

固定部分,這部分是大小不變的。

• slave_proxy_id:4字節,主庫生成本Event的thread丨d,它和show processlist中的id 對應,
• query_exec_time: 4字節,這就是執行時間了。但是對於行模式的DML語句來講這個執 行時間並不準確,上面已經描述了原因。而對於DDL來講還是比較準確的。
• db_len: 1字節,用於描述數據庫名的長度0
• error_code: 2字節,執行語句的錯誤碼。
• status_vars_len: 2字節,status variables部分的長度。

可変部分,這部分是可變的。
• status variables:這部分是一個環境參數的設置,其中包含了很多種格式。每種格式都 有自己的長度和數據域,因此可以輕鬆的讀取到各種值,比如SQL_MODE,客戶端字符 集、自增環堍、客戶端排序字符集等,但是其過於複雜這裏不做實際解析。
• db:當前登錄的database名字,以0x00結尾。主庫來源爲thd->db。如果是語句模式從 庫做過濾的時候會使用這個名字。下面是邏輯驗證:
• query:這就是具體的語句9對於行模式的DML來講就是‘BEGIN’,而對於DDL來講就是 具體的語句。
如果我們打開Query_log_event::do_apply_event函數就會看到這個Event在從庫應用的時候就 是設置各種環境比如客戶端字符集、自增環境設置、當前登錄數據庫等,然後執行相應的語 句,而對於行模式的DML這裏只會執行‘BEGIN、這裏注意一個細節,其中包含一句:
這句代碼會設置線程的命令執行時間爲Event header中Timestamp的吋間,因此當我們在從 庫上執行now()函數時是可以得到正確的結果的。

 

statement_events.cpp:

從Slave讀取來源:

Query_event::Query_event(const char* buf, unsigned int event_len,
const Format_description_event *description_event,
Log_event_type event_type)

 

......

// 讀取 status variables

start= (Log_event_header::Byte*) (buf + post_header_len);
end= (const Log_event_header::Byte*) (start + status_vars_len);

// 讀取 db

  db= (const char* )end;

 

一、QUERY-EVENT
(1) QUERY_EVENT的作用
QUERY_EVENT不僅會記錄一些語句運行的環境比如SQL_MODE,客戶端字符集、自增環境 設置、當前登錄數據庫等,而且會記錄執行時間。但對於行模式的DDL和DML記錄的執行時間 會有所不同t如下:
• DML:執行時間記錄的是第一條數據更改後的時間,而不是真正本條DML語句執行的時 間(一個DML語句可能修改很多條數據),往往這個時間非常短,不能正確的表示DML 語句執行的時間 , 語句部分記錄的是’BEGIN’
• DDL:執行時間是實際語句的執行時間,語句部分記錄的是實際的語句。
執行時間是Seconds_Behind_Master計算的一個影晌因素,對於一個事務來講只有一個這樣的QUERY_EVENT。


如果我們打開QueryLog_event::do_apply_event函數就會看到這個Event在從庫應用的時候就 是設罝各種環境比如客戶端字符集、自增環境設罝、當前登錄數據庫等,然後執行相應的語 句,而對於行模式的DML這裏只會執行‘BEGIN’。這裏注意一個細節,其中包含一句:

if (rpl_filter->db_ok(thd->db().str))
{
thd->set_time(&(common_header->when));

這句代碼會設罝線程的命令執行時間爲Event header中Timestamp的時間。因此當我們在從 庫上執行now()函數時是可以得到正確的結果的


(4)實例解析
下面是一個行模式的DML的QUERY_EVENT (mysqlbinlog --hexdump 輸出):

slave_proxy_id 4 bytes
query_exec_time 4 bytes
db_len 1 bytes
error_code 2 bytes
status_vars_len 2 bytes
status_variables 可變
db 可變
query 可變


固定部分,這部分是大小不變的。
• slave_proxy_id:4字節,主庫生成本Event的thread丨d,它和show processlist中的id 對應,
• query_exec_time: 4字節,這就是執行時間了。但是對於行模式的DML語句來講這個執 行時間並不準確,上面已經描述了原因。而對於DDL來講還是比較準確的。
• db_len: 1字節,用於描述數據庫名的長度0
• error_code: 2字節,執行語句的錯誤碼。
• status_vars_len: 2字節,status variables部分的長度。

可変部分,這部分是可變的。
• status variables:這部分是一個環境參數的設置,其中包含了很多種格式。每種格式都 有自己的長度和數據域,因此可以輕鬆的讀取到各種值,比如SQL_MODE,客戶端字符 集、自增環堍、客戶端排序字符集等,但是其過於複雜這裏不做實際解析。
• db:當前登錄的database名字,以0x00結尾。主庫來源爲thd->db。如果是語句模式從 庫做過濾的時候會使用這個名字。下面是邏輯驗證:

從讀取來源:
statement_events.cpp
Query_event::Query_event(const char* buf, unsigned int event_len,
const Format_description_event *description_event,
Log_event_type event_type)

end= (const Log_event_header::Byte*) (start + status_vars_len);
db= (const char* )end;
// 這裏將指針指向 Query_event header中的db-name

log_event.cc
int Query_log_event::do_apply_event(Relay_log_info const *rli,
const char *query_arg, size_t q_len_arg)

// 讀取db-name
set_thd_db(thd, db, db_len);


log_event.cc 調用 rpl_filter.cc , rpl_filter.h

bool
Rpl_filter::db_ok_with_wild_table(const char *db)

• query:這就是具體的語句。對於行模式的DML來講就是‘BEGIN’,而對於DDL來講就是 具體的語句。


• query:這就是具體的語句9對於行模式的DML來講就是‘BEGIN’,而對於DDL來講就是 具體的語句。
如果我們打開Query_log_event::do_apply_event函數就會看到這個Event在從庫應用的時候就 是設置各種環境比如客戶端字符集、自增環境設置、當前登錄數據庫等,然後執行相應的語 句,而對於行模式的DML這裏只會執行‘BEGIN、這裏注意一個細節,其中包含一句:
這句代碼會設霣線程的命令執行時間爲Event header中Timestamp的吋間,因此當我們在從 庫上執行now()函數時是可以得到正確的結果的。


(4)實例解析
下面是一個行模式的DML的QUERY_EVENT (mysqlbinlog --hexdump 輸出):

# at 417
#191218 21:08:49 server id 1 end_log_pos 490 CRC32 0x1b37cdc7
# Position Timestamp Type Master ID Size Master Pos Flags
# 1a1 e1 24 fa 5d 02 01 00 00 00 49 00 00 00 ea 01 00 00 08 00
# 1b4 03 00 00 00 00 00 00 00 05 00 00 1a 00 00 00 00 |................|
# 1c4 00 00 01 00 00 00 00 00 00 00 00 06 03 73 74 64 |.............std|
# 1d4 04 2d 00 2d 00 21 00 77 6f 72 6c 64 00 42 45 47 |.......world.BEG|
# 1e4 49 4e c7 cd 37 1b |IN..7.|
# Query thread_id=3 exec_time=0 error_code=0
SET TIMESTAMP=1576674529/*!*/;
BEGIN

固定部分
• 03 00 00 00: thread id 3.
• 00 00 00 00:執行時間,對於行模式的DML來講通常不準。
• 05:當前登錄庫名的長度。
• 00 00:錯誤碼#
• 1a 00: status variables部分的長度16進制 '1a' 就是十進制26。

可變部分
• status variables:略。
• 77 6f 72 6c 64 00:當前登錄庫名world的ASCII編碼,以0x00結尾9
• 42 45 47 49 4e:語句‘BEGIN’的ASCH編碼分

中間有部分就是我們的status variables,這部分過於複雜我沒有仔細研究,因此我就沒做解析了

(5) 生成時機
•對於行模式的DML而言,生成時機是事務的第一個DML語句第一行需要修改的數據在 Innodb引擎層修改完成後。通常來講一個事務對應了一個QUERY_EVENT。
•對於DDL而言,生成時機是在整個操作執行完成之後。

二、MAP_EVENT
(1) MAP_EVENT的作用
MAP_EVENT是行模式特有的,它主要作用是做table id和實際訪問表的映射。
同時其中還包 含了一些表的定義,如:表所在庫名、表名、字段類型、可變字段長度等。
這裏的庫名和 QUERY_EVENT的庫名不一樣,這個庫名來自表的定義,
而QUERY_EVENT的庫名來自於當 前登錄的數據庫即:thd->db。
固定部分,這部分是大小不變的。


table_id 6 bytes
Reserved 2 bytes
db len 可變
db name 可變
table len 可變
table name 可變
no of cols 可變
array of col types 可變
metadata len 可變
metadata block 可變
m_null_bits 可變


• table id: 6字節,這個table id和Innodb層的table id不一樣,他分配的時機是第一次打 開表定義的時候。它不是固定的,重啓Mysql實例或
者flush tables都會導致其改變。

sql_base.cc
TABLE_SHARE *get_table_share(THD *thd, TABLE_LIST *table_list,
const char *key, size_t key_length,
uint db_flags, int *error,
my_hash_value_type hash_value)

assign_new_table_id(share);
share->table_map_id= last_table_id++; // 遞增table id
在LOCK_open的保護下分配一個新的表ID。 這樣做不是創建新的互斥鎖
並將其僅用於序列化對 靜態變量, 在此處分配表ID。我們將其分配給
共享,然後將其插入到table_def_cache中確保沒有表就無法從緩存中讀取它
分配的ID。

 

因此它是可變的。
• Reserved: 2字節,保留以後使用。
可變部分,這部分大小是可變的《
• dblen:表所在的數據庫名長度。
• db name:表所在的數據庫名以0x00結尾。
• table丨en:表名的長度。
• table name:表名以0x00結尾》
• no of cols:表中字段數霣。
• array of col types:字段的類型數組。
• metadata len: metadata block的長度v
• metadata block:對於可變字段需要記錄字段的長度,但對於像int這種數據類型就不需 要了。下面是varchar關於可變長度的輸出,可以看到它佔用2個字節:


int Field_varstring::do_save_field_metadata(uchar *metadata_ptr)
{
DBUG_ASSERT(field_length <= 65535);
int2store((char*)metadata_ptr, field_length);
return 2;
}

如果感興趣可以査看do_save_field_metadata這個函數。
• m_null_bits:字段是否可以爲空的一個位圖。下面是位圖的獲取方式。

現在將數據的大小設置爲字段元數據數組的大小
加上1或3個字節(請參閱pack.c:net_store_length)以獲取
字段元數據數組中的元素。

log_event.cc
if (m_field_metadata_size < 251)
m_data_size+= m_field_metadata_size + 1;
else
m_data_size+= m_field_metadata_size + 3;

memset(m_null_bits, 0, num_null_bytes);
for (unsigned int i= 0 ; i < m_table->s->fields ; ++i)
if (m_table->field[i]->maybe_null())
m_null_bits[(i / 8)]+= 1 << (i % 8);


(4) 實例解析
執行如下語句:

mysql> use test;
Database changed
mysql> create table test(id int(11) not null auto_increment,
-> a int(11) default null,
-> b int(11) default null,
-> primary key(id),
-> key idxa (a)
-> ,
-> key idxb (b)
-> ) engine=innodb auto_increment=10 default charset=utf8mb4;
Query OK, 0 rows affected (0.02 sec)

mysql> insert into test values(100,100,100);
Query OK, 1 row affected (0.01 sec)

# at 634
#200206 23:27:49 server id 1 end_log_pos 683 CRC32 0xf235602e
# Position Timestamp Type Master ID Size Master Pos Flags
# 27a 75 30 3c 5e 13 01 00 00 00 31 00 00 00 ab 02 00 00 00 00
# 28d 65 01 00 00 00 00 01 00 04 74 65 73 74 00 04 74 |e........test..t|
# 29d 65 73 74 00 03 03 03 03 00 06 2e 60 35 f2 |est.........5.|
# Table_map: `test`.`test` mapped to number 357

這個‘Insert’語句的MAP_EVENT爲:
• 65 01 00 00 00 00: table id爲 16進制的0165,轉換爲 10進製爲 357
• 01 00:保留。
• 04:表所在的數據庫名長度爲4
• 74 65 73 74 00: 'test'的ASCII表示,以0x00結尾
• 04:表名的長度爲2。
• 65 73 74 00: 'test'的ASCII表示,以0x00結尾
• 03:表擁有3個字段
• 03 03 03:每個字段的類型都是03,實際上就是int這種類型了。具體可以參考 enum_field_types 這個枚舉類型
• 00: metadata長度爲0,沒有可變字段
• 06:轉換爲2進製爲110,表示第一個字段不可以爲空,其他兩個字段可以爲空

(5) 生成時機
本Event只會在行模式下生成#生成時機是事務的每條DML語句條改的第一行數據在Innodb引 擎層修改完成後,QUERY_EVENT生成之後。
通常來講每個語句的每個表(Table List)都會 包含這樣一個MAP_EVENT。
前面我們說過了table_id是可變的,甚至會出現下面的情況。我們來構造這種情況,還是使用 上面的 test.test表:

set global autocommit = off;

Session1:
begin;
insert into test.test values(1,2,3);

Session2:
flush tables;

Session1:
insert into test.test values(4,5,6);
commit;

/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=1*/;
/*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/;
DELIMITER /*!*/;
# at 4
#200207 21:18:24 server id 1 end_log_pos 123 CRC32 0x0a0e6713 Start: binlog v 4, server v 5.7.19-enterprise-commercial-advanced-log created 200207 21:18:24
# at 123
#200207 21:18:24 server id 1 end_log_pos 194 CRC32 0x7fe14b87 Previous-GTIDs
# 12cfee78-e580-11e6-a790-00ff0593afce:17-21
# at 194
#200207 21:18:49 server id 1 end_log_pos 259 CRC32 0x99027597 GTID last_committed=0 sequence_number=1 rbr_only=no
SET @@SESSION.GTID_NEXT= '12cfee78-e580-11e6-a790-00ff0593afce:22'/*!*/;
# at 259
#200207 21:18:49 server id 1 end_log_pos 338 CRC32 0xe188ca4b Query thread_id=4 exec_time=0 error_code=0
use `test`/*!*/;
SET TIMESTAMP=1581081529/*!*/;
SET @@session.pseudo_thread_id=4/*!*/;
SET @@session.foreign_key_checks=1, @@session.sql_auto_is_null=0, @@session.unique_checks=1, @@session.autocommit=1/*!*/;
SET @@session.sql_mode=0/*!*/;
SET @@session.auto_increment_increment=1, @@session.auto_increment_offset=1/*!*/;
/*!\C utf8 *//*!*/;
SET @@session.character_set_client=33,@@session.collation_connection=33,@@session.collation_server=33/*!*/;
SET @@session.lc_time_names=0/*!*/;
SET @@session.collation_database=DEFAULT/*!*/;
flush tables
/*!*/;
# at 338
#200207 21:19:00 server id 1 end_log_pos 403 CRC32 0xa13e5263 GTID last_committed=1 sequence_number=2 rbr_only=yes
/*!50718 SET TRANSACTION ISOLATION LEVEL READ COMMITTED*//*!*/;
SET @@SESSION.GTID_NEXT= '12cfee78-e580-11e6-a790-00ff0593afce:23'/*!*/;
# at 403
#200207 21:18:46 server id 1 end_log_pos 475 CRC32 0x4284ad54 Query thread_id=3 exec_time=0 error_code=0
SET TIMESTAMP=1581081526/*!*/;
BEGIN
/*!*/;
# at 475
#200207 21:18:46 server id 1 end_log_pos 524 CRC32 0xb34f27f8 Table_map: `test`.`test` mapped to number 360
# at 524
#200207 21:18:46 server id 1 end_log_pos 572 CRC32 0x8e20673a Write_rows: table id 360 flags: STMT_END_F
### INSERT INTO `test`.`test`
### SET
### @1=1 /* INT meta=0 nullable=0 is_null=0 */
### @2=2 /* INT meta=0 nullable=1 is_null=0 */
### @3=3 /* INT meta=0 nullable=1 is_null=0 */
# at 572
#200207 21:18:58 server id 1 end_log_pos 621 CRC32 0xa4f321aa Table_map: `test`.`test` mapped to number 361
# at 621
#200207 21:18:58 server id 1 end_log_pos 669 CRC32 0x3108b1d9 Write_rows: table id 361 flags: STMT_END_F
### INSERT INTO `test`.`test`
### SET
### @1=4 /* INT meta=0 nullable=0 is_null=0 */
### @2=5 /* INT meta=0 nullable=1 is_null=0 */
### @3=6 /* INT meta=0 nullable=1 is_null=0 */
# at 669
#200207 21:19:00 server id 1 end_log_pos 700 CRC32 0xc7a499f7 Xid = 21
COMMIT/*!*/;
# at 700
#200207 21:19:14 server id 1 end_log_pos 723 CRC32 0x0357cffb Stop
SET @@SESSION.GTID_NEXT= 'AUTOMATIC' /* added by mysqlbinlog */ /*!*/;
DELIMITER ;
# End of log file
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;


一個事務內 相同的表不同的 table_id ,

# at 475
#200207 21:18:46 server id 1 end_log_pos 524 CRC32 0xb34f27f8 Table_map: `test`.`test` mapped to number 360

# at 572
#200207 21:18:58 server id 1 end_log_pos 621 CRC32 0xa4f321aa Table_map: `test`.`test` mapped to number 361

 

Seconds_Behind_Master作爲判斷主從同步的一個重要它的計算方法。
同 時討論有哪些需要注意的地方。這裏還會說明Seconds_Behind_Master爲0並不一定代表沒有 延遲的原因。


一、 Seconds__Behind_Masteri十算的方式
當每次發起‘show slave status’命令的時候都會逬行一次Seconds_Behind_Master的計 算,
其計算的方式集中在函數show_slave_status_send_data中,下面是一段僞代碼,非常清晰:

rpl_slave.cc
/*
The pseudo code to compute Seconds_Behind_Master:
if (SQL thread is running)
{
if (SQL thread processed all the available relay log)
{ // sql線呈已經 apply 完所有 I/O線程寫入的Event
if (IO thread is running)
print 0; // 顯示延遲爲0
else
print NULL;
}
else
compute Seconds_Behind_Master;
}
else
print NULL;
*/

正式代碼如下,這裏有一點需要注慂就是如何判斷SQL線程應用完了所有的Event的,下面我們可以看看這部 分實際的代碼:


bool show_slave_status_send_data(THD *thd, Master_info *mi,
char* io_gtid_set_buffer,
char* sql_gtid_set_buffer)
...
if ((mi->get_master_log_pos() == mi->rli->get_group_master_log_pos()) &&
(!strcmp(mi->get_master_log_name(), mi->rli->get_group_master_log_name())))
{
if (mi->slave_running == MYSQL_SLAVE_RUN_CONNECT)
protocol->store(0LL);
else
protocol->store_null();
}
else
{
long time_diff= ((long)(time(0) - mi->rli->last_master_timestamp)
- mi->clock_diff_with_master);

條件“mi->get_master_log_pos() == mi->rli->get_group_master_log_pos()”
就是通過 10 線程讀取到主庫binary log的位罝和SQL線程應用到的主庫binary log位罝進行比較來逬行判斷。
如果主從之間的網絡狀態很糟糕的話,
從庫的SQL線程的應用Event的速度可能比10線程 讀取Event的速度更快。
那麼就會出現一種情況,雖然SQL線程應用完了所有的Event,
並且 Seconds_Behind_Master也顯示爲0,但是並不代表沒有延遲,
這個時候的延遲主要在於10線 程讀取Event過慢。
這也是如果Seconds_Behind_Master爲0不代表沒有延遲的第一個原因。

二、 影晌 Seconds_Behind_Master 計算的因素
那麼我們一般看到的正常Seconds_Behind_Master是怎麼計薄出來的呢?實際上也很簡單源 碼如下:
我們來分別解釋一下這三個因素:
(1) (long)(time(0):
取當前從庫服務器的系統時間。

(2) mi->clock_diff_with_master
這個值是從庫服務器的系統時間和主庫服務器的系統時間的差值。
實際上它只在10線程啓動的時候進行一次性計算,
言外之意就是如果我 們啓動 I/O 線程後人爲的修改從庫服務器的時間,
那麼Seconds_Behind_Master的計算必然出 現問題。更有可能出現負數,
如果出現負數延遲依然顯示爲0,如下:


protocol->store((longlong)(mi->rli->last_master_timestamp ?
max(0L, time_diff) : 0));
}
}
// max(0L,time_diff) 如果time_diff爲負數取0

因此主從服務器最好保證時鐘同步。
這也是Seconds_Behind_Master顯示0並不代表沒有延遲 的第二個原因。

(3) mi->rli->last_master_timestamp
這個值的取值就比較複雜,因爲DML和DDL下是不同的。
如果是DML在單SQL線程和MTS下 又不一樣。因此我們分幵討論。

• DML (單SQL線程)
這種情況下前面我們已經說過了,SQL線程每次執行Event的時候會獲取Event的相關信息如 下:

// event header 的timestamp
rli->last_master_timestamp= ev->common_header->when.tv_sec +
(time_t) ev->exec_time;

// query event 纔有的執行時間

實際上因爲行格式的binary log (本系只討論行格式),DML中QUERY_EVENT的ev->exec_time基本爲0因此可以忽略,
因爲QUERY_EVENT中的exec_time只記錄第一條數據更 改消耗的時間,且一般看到的是‘BEGIN’,
所以 last_master_timestamp就基本等於各個Event中header的timestamp。
但是對於一個事務而 言我們知道GTID_EVENT和XID_EVENT都是提交時刻的時間,
而對於其他Event都是命令發起 時刻的時間(顯示“begin commit”開啓事務的情況)。
因 此如果一個長時間未提交的事務在SQL線程應用的時候可能觀察到Seconds_Behind_Master 的瞬間跳動。

我們以一個‘Delete’語句爲例,假設我們主庫10分鐘後才提交這個事務,
那麼可能延遲計算如 下(T1爲語句執行時間,T1+10爲事務提交的時間,
T2爲從庫系統時間-主從時間差,
T2每 次執查詢Seconds_Behind_Master可變,因爲從庫系統時間可變):
這種情況我就能看到突然延遲跳到很大然後一下恢復正常,比如例子中爲10分鐘。

 

• DML (MTS)
實際上rli->last_master_timestamp會取值爲檢 查點位罝事務XID_EVENT的timestamp,流程大概如下:
因此last_master_timestamp就是檢查點位罝事務的XID_EVENT header中的timestamp。
如 果不開啓‘slave_preserve_commit_order’參數,那麼就可能出現‘gap’,
檢查點只能停留 在‘gap’之前的一個事務,可能後面的事務已經執行完成了。
這種情況下延遲並不是那麼準確 但是誤差也不大,
因爲默認slave_checkpoint_period參數設罝爲300毫秒。
關於‘gap’後面我們可以調高參數slave_checkpoint_period參數,
以此來 證明MTS延遲的計算是以檢查點爲準的。

 

 

Seconds_Behind_Master作爲判斷主從同步的一個重要它的計算方法。
同 時討論有哪些需要注意的地方。這裏還會說明Seconds_Behind_Master爲0並不一定代表沒有 延遲的原因。


一、 Seconds__Behind_Masteri十算的方式
當每次發起‘show slave status’命令的時候都會逬行一次Seconds_Behind_Master的計 算,
其計算的方式集中在函數show_slave_status_send_data中,下面是一段僞代碼,非常清晰:

rpl_slave.cc
/*
The pseudo code to compute Seconds_Behind_Master:
if (SQL thread is running)
{
if (SQL thread processed all the available relay log)
{ // sql線呈已經 apply 完所有 I/O線程寫入的Event
if (IO thread is running)
print 0; // 顯示延遲爲0
else
print NULL;
}
else
compute Seconds_Behind_Master;
}
else
print NULL;
*/

正式代碼如下,這裏有一點需要注慂就是如何判斷SQL線程應用完了所有的Event的,下面我們可以看看這部 分實際的代碼:


bool show_slave_status_send_data(THD *thd, Master_info *mi,
char* io_gtid_set_buffer,
char* sql_gtid_set_buffer)
...
if ((mi->get_master_log_pos() == mi->rli->get_group_master_log_pos()) &&
(!strcmp(mi->get_master_log_name(), mi->rli->get_group_master_log_name())))
{
if (mi->slave_running == MYSQL_SLAVE_RUN_CONNECT)
protocol->store(0LL);
else
protocol->store_null();
}
else
{
long time_diff= ((long)(time(0) - mi->rli->last_master_timestamp)
- mi->clock_diff_with_master);

條件“mi->get_master_log_pos() == mi->rli->get_group_master_log_pos()”
就是通過 10 線程讀取到主庫binary log的位罝和SQL線程應用到的主庫binary log位罝進行比較來逬行判斷。
如果主從之間的網絡狀態很糟糕的話,
從庫的SQL線程的應用Event的速度可能比10線程 讀取Event的速度更快。
那麼就會出現一種情況,雖然SQL線程應用完了所有的Event,
並且 Seconds_Behind_Master也顯示爲0,但是並不代表沒有延遲,
這個時候的延遲主要在於10線 程讀取Event過慢。
這也是如果Seconds_Behind_Master爲0不代表沒有延遲的第一個原因。

二、 影晌 Seconds_Behind_Master 計算的因素
那麼我們一般看到的正常Seconds_Behind_Master是怎麼計薄出來的呢?實際上也很簡單源 碼如下:
我們來分別解釋一下這三個因素:
(1) (long)(time(0):
取當前從庫服務器的系統時間。

(2) mi->clock_diff_with_master
這個值是從庫服務器的系統時間和主庫服務器的系統時間的差值。
實際上它只在10線程啓動的時候進行一次性計算,
言外之意就是如果我 們啓動 I/O 線程後人爲的修改從庫服務器的時間,
那麼Seconds_Behind_Master的計算必然出 現問題。更有可能出現負數,
如果出現負數延遲依然顯示爲0,如下:


protocol->store((longlong)(mi->rli->last_master_timestamp ?
max(0L, time_diff) : 0));
}
}
// max(0L,time_diff) 如果time_diff爲負數取0

因此主從服務器最好保證時鐘同步。
這也是Seconds_Behind_Master顯示0並不代表沒有延遲 的第二個原因。

(3) mi->rli->last_master_timestamp
這個值的取值就比較複雜,因爲DML和DDL下是不同的。
如果是DML在單SQL線程和MTS下 又不一樣。因此我們分幵討論。

• DML (單SQL線程)
這種情況下前面我們已經說過了,SQL線程每次執行Event的時候會獲取Event的相關信息如 下:

rpl_slave.cc
static int exec_relay_log_event(THD* thd, Relay_log_info* rli)
{
....
// event header 的timestamp
rli->last_master_timestamp= ev->common_header->when.tv_sec +
(time_t) ev->exec_time;

// query event 纔有的執行時間

實際上因爲行格式的binary log (本系只討論行格式),DML中QUERY_EVENT的ev->exec_time基本爲0因此可以忽略,
因爲QUERY_EVENT中的exec_time只記錄第一條數據更 改消耗的時間,且我們一般看到的是‘BEGIN’,
所以 last_master_timestamp就基本等於各個Event中header的timestamp。
但是對於一個事務而 言我們知道GTID_EVENT和XID_EVENT都是提交時刻的時間,
而對於其他Event都是命令發起 時刻的時間(顯示“begin commit”開啓事務的情況)。
因 此如果一個長時間未提交的事務在SQL線程應用的時候可能觀察到Seconds_Behind_Master 的瞬間跳動。

我們以一個‘Delete’語句爲例,假設我們主庫10分鐘後才提交這個事務,
那麼可能延遲計算如 下(T1爲語句執行時間,T1+10爲事務提交的時間,
T2爲從庫系統時間-主從時間差,
T2每 次執查詢Seconds_Behind_Master可變,因爲從庫系統時間可變):
這種情況我就能看到突然延遲跳到很大然後一下恢復正常,比如例子中爲10分鐘。

 

• DML (MTS)
實際上rli->last_master_timestamp會取值爲 checkpoint 位罝事務XID_EVENT的timestamp,流程大概如下:


log_event.cc
Slave_worker *Log_event::get_slave_worker(Relay_log_info *rli)
{

....
1. 工作線程執行到事務XID_EVENT時,取XID_EVENT的timestamp
ptr_group->checkpoint_seqno= rli->checkpoint_seqno;
ptr_group->ts= common_header->when.tv_sec + (time_t) exec_time; // Seconds_behind_master related
...

2. 當進行 checkpoint 的時候設置變量ts , 調用函數 mts_checkpoint_routine

bool mts_checkpoint_routine(Relay_log_info *rli, ulonglong period,
bool force, bool need_data_lock)

/*
Update the rli->last_master_timestamp for reporting correct Seconds_behind_master.

If GAQ is empty, set it to zero.
Else, update it with the timestamp of the first job of the Slave_job_queue
which was assigned in the Log_event::get_slave_worker() function.
*/
ts= rli->gaq->empty()
? 0
: reinterpret_cast<Slave_job_group*>(rli->gaq->head_queue())->ts;
rli->reset_notified_checkpoint(cnt, ts, need_data_lock, true);
/* e

3. 修改 last_master_timestamp 值
rpl_rli.cc
void Relay_log_info::reset_notified_checkpoint(ulong shift, time_t new_ts,
bool need_data_lock,
bool update_timestamp)
{

...

if (update_timestamp)
{
if (need_data_lock)
mysql_mutex_lock(&data_lock);
else
mysql_mutex_assert_owner(&data_lock);
last_master_timestamp= new_ts;
if (need_data_lock)
mysql_mutex_unlock(&data_lock);
}
}

因此last_master_timestamp就是檢查點位罝事務的XID_EVENT header中的timestamp。
如 果不開啓‘slave_preserve_commit_order’參數,那麼就可能出現‘gap’,
檢查點只能停留 在‘gap’之前的一個事務,可能後面的事務已經執行完成了。
這種情況下延遲並不是那麼準確 但是誤差也不大,
因爲默認slave_checkpoint_period參數設罝爲300毫秒。
關於‘gap’後面我們可以調高參數slave_checkpoint_period參數,
以此來 證明MTS延遲的計算是以檢查點爲準的。


• DDL
同樣是上面 DML (單SQL線程) 的公式:

rpl_slave.cc
static int exec_relay_log_event(THD* thd, Relay_log_info* rli)
{
....
// event header 的timestamp
rli->last_master_timestamp= ev->common_header->when.tv_sec +
(time_t) ev->exec_time;

// query event 纔有的執行時間

實際上DML和DDL計 算延遲的方式區別就在於這裏的exec_time是否參與運算。
DDL記錄Binary log中QUERY_EVENT記錄的是實際的語句,exec_time就會記錄整個語句執行完成所消耗的時間


三、 Seconds_Behind_Masteri十算方式總結


 

四、 MTS中Seconds_Behind_Masteri十算誤差的測試
證明slave_checkpoint_period( 默認 300ms) 在MTS中的Seconds_Behind_Master計算和檢查點有關。
從庫操作如下:

set slave_checkpoint_period = 60000;
stop slave;
start slave;

 

然後在主庫做手動做幾個事務。然後查看Executed_Gtid_Set如下:

insert * 3;

show master status \G

 

 檢查主庫和從庫的Executed_Gtid_Set 發現這些事務早就已經在從庫 做完了

 


用上面的公式來套的話就是mi->rli->last_master_timestamp甶於MTS沒有做 檢查點而一直不變化,
直到60秒後才做了一次檢查點,mi->rli->last_master_timestamp信 息纔得到了更新。
從庫show slave status,會發現Seconds_Behind_Master—直增長到60, 下面是延遲到了 60的截圖:

 

 

 下面是變爲0的截圖:

 

 

但是這種問題一般不需要擔心,
因爲默認情況下參數slave_checkpoint_period爲300毫秒,檢 查點非常頻繁。

 

五、 手動修改從庫系統時間,導致Seconds_Behind_Master爲0
這裏我們再來測試一下手動修改從庫系統時間的情況。
很簡單我們只需要在主庫做一個大操 作,
導致從庫有延遲。我已經做了一個大表的DDL,當前從庫的延遲如下,
我們也從圖中可以 看到gno爲31的DDL操作還沒有執行完成,導致延遲,如下:

 

 

然後我們將從庫系統時間改小,如下:

修改系統時間
# date
# date -s 01:00:00


然後再次查看延遲,我們觀察到Seconds_Behind_Master爲0,但是gno爲31的DDL操作還是 沒有執行完成,如下:

 

從庫數據的查找和參數slave_rows_search_algorithms


對於DML語句來講其數據的更改將被放到對應的Event中。
比 如‘Delete’語句會將所有刪除數據的before Image放到 DELETE_ROWS_EVENT 中,
從庫只要 讀取這些beforejmage進行數據查找,
然後調用相應的‘Delete’的操作就可以完成數據的刪除 了。
下面我們來討論一下從庫是如何進行數據查找的。
本節我們假定參數binlog_row_image設罝爲‘FULL’也就是默認值,
關於binlog_row_image參 數的影響在第11節已經描述過了。

一、沒有Primary Key 和 唯一索引有索引的案例
在開始之前我們先假定參數‘slave_rows_search_algorithms’爲默認值,即:
• TABLE_SCAN,INDEX_SCAN
因爲這個參數會直接影響到對索引的利用方式。
以‘Delete’操作爲例,實際上對於索引的選擇‘Update’操作也是一樣的,
因爲都是通 過beforejmage去查找數據。我測試的表結構、數據和操作如下:

mysql>
create Table: CREATE TABLE 'aaa' (
'a' int(ll) DEFAULT NULL,
'b' int(ll) DEFAULT NULL,
'c' int(ll) DEFAULT NULL,
KEY 'a、 (、a、)
)ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set C0.00 sec)

mysql> delete from aaa where a=1;
Query OK, 3 rows affected (6.24 sec)

對於這樣一個‘Delete’和'Update'語句來講主庫會利用到索引KEY a ,
刪除的三條數據我們實際上只需 要一次索引的定位,
然後順序掃描接下來的數據逬 行刪除就可以了。大概的流程和代碼如下:

在沒有唯一索引和Primary Key循環:
索引定位(申請 latch(空閒塊和讀取IO帶寬優先) , B樹的非葉子級別上使用修改的搜索模式 ,B樹葉子中使用原始的搜索模式 ) -> 回表 -> 刪除/修改該行 -> 訪問下一行 -> 回表 -> 刪除/修改該行

索引定位
btr0cur.cc
void
btr_cur_search_to_nth_level(
/*========================*/
dict_index_t* index, /*!< in: index */

 

這條數據刪除/修改數據的 before_Image 將會記錄到一個DELETE/UPDATE_ROWS_EVENT中。
從庫 優先使用主鍵和唯一鍵,評估使用哪個索引,
在Event中的每條數據需要進行索引定位數據,
在非唯一索引情況第一次返回的第一行數據不刪除數據,
繼續掃描下一行

如下代碼:
log_event.cc
int Rows_log_event::do_index_scan_and_update(Relay_log_info const *rli)
...
while (record_compare(m_table, &m_cols))
{
while((error= next_record_scan(false)))
{
/* We just skip records that has already been deleted */
if (error == HA_ERR_RECORD_DELETED)
continue;
DBUG_PRINT("info",("no record matching the given row found"));
goto end;
}
}

在沒有主鍵和唯一鍵時候從庫I/O的消耗代價是比主庫更高的,
因此依舊使用的是索引KEY a ,
流程如下:

索引定位(申請 latch(空閒塊和讀取IO帶寬優先) , B樹的非葉子級別上使用修改的搜索模式 ,B樹葉子中使用原始的搜索模式 ) -> 回表 -> 刪除/修改該行 -> 訪問下一行 -> 回表 -> 刪除/修改該行


在從庫增加一個主鋌,
那麼在從庫進行應用的時候流程如下:

循環:
主鍵定位 -> 刪除本行

log_event.cc
int Rows_log_event::do_index_scan_and_update(Relay_log_info const *rli)

...

if (m_key_info->flags & HA_NOSAME || m_key_index == m_table->s->primary_key)
{
/* Unique does not have non nullable part */
if (!(m_key_info->flags & (HA_NULL_PART_KEY)))
goto end; // record found
else
{
/*
Unique has nullable part. We need to check if there is any field in the
BI image that is null and part of UNNI.
*/
bool null_found= FALSE;
for (uint i=0; i < m_key_info->user_defined_key_parts && !null_found; i++)
{
uint fieldnr= m_key_info->key_part[i].fieldnr - 1;
Field **f= m_table->field+fieldnr;
null_found= (*f)->is_null();
}

if (!null_found)
goto end; // record found

/* else fall through to index scan */
}
}


主庫‘Delete/Update’操作和從庫‘Delete/Update’操作主要的區別在於:
• 從庫每條數據都需要索引定位查找數據。
• 從庫在非唯一索引情況下查找第一條數據並不立刻刪除/修改數據,而繼續進行索引定位和查找。


對於主庫來只需要一次數據定位查找即可,
接下來訪問下一條數據就可以了。
刪除/修改操作來講並沒有區別。
合理的使用主鍵和睢一鍵可以將上面提到的兩點影晌延遲性能降低。
在從庫延遲的時候,
沒有合理的使用主鍵和唯一鍵是非常重要的原因。
最後如果表上一個索引都沒有,情況變得更加嚴重,簡單的圖如下:

循環:
全表掃描 -> 刪除本行

每一行數據的更改都需要進行全表掃描,
性能問題非常嚴重。
這種情況使用 slave_rows_search_algorithms 的HASH_SCAN代替TABLE_SCAN也許可以提高性能,


二、確認查找數據的方式
slave_rows_search_algorithms 參數主要用於確認如何 查找數據。
其取值可以是下面幾個組合(來自官方文檔),源碼中體現爲一個位圖:

• TABLE_SCAN,INDEX__SCAN (默認值)
• INDEX_SCAN,HASH_SCAN
• TABLE_SCAN,HASH_SCAN
• TABLE_SCANJNDEX_SCAN,HASH_SCAN

在源碼中有如下的說明,當然官方文檔也有類似的說明:

log_event.cc
int
Rows_log_event::row_operations_scan_and_key_setup()
....

根據 ROW_LOOKUP_HASH_SCAN , ROW_LOOKUP_INDEX_SCAN ,ROW_LOOKUP_TABLE_SCAN 調用
log_event.cc
void
Rows_log_event::decide_row_lookup_algorithm_and_key()
...

/*
Decision table:
- I --> Index scan / search
- T --> Table scan
- Hi --> Hash over index
- Ht --> Hash over the entire table

|--------------+-----------+------+------+------|
| Index\Option | I , T , H | I, T | I, H | T, H |
|--------------+-----------+------+------+------|
| PK / UK | I | I | I | Hi |
| K | Hi | I | Hi | Hi |
| No Index | Ht | T | Ht | Ht |
|--------------+-----------+------+------+------|

*/

 

源碼中會有三種數據查找的方式,分別是:

• R0W_L00KUP_INDEX_SCAN
對應函數接口 :log_event.cc Rows_Log_event::do_index_scan_and_update

• R0W_L00KUP_HASH_SCAN
對應函數接口 : log_event.cc Rows_Log_event::do_hash_scan_and_update 包含:
(1) Hi --> Hash over index
(2) Ht --> Hash over the entire table
• R0W_L00KUP_TABLE_SCAN


對應函數接口 :
log_event.cc
int Rows_log_event::do_apply_event(Relay_log_info const *rli) 在源碼中如下:

...
if ((m_rows_lookup_algorithm != ROW_LOOKUP_NOT_NEEDED) &&
!is_any_column_signaled_for_table(table, &m_cols))
{
error= HA_ERR_END_OF_FILE;
goto AFTER_MAIN_EXEC_ROW_LOOP;
}
switch (m_rows_lookup_algorithm)
{
case ROW_LOOKUP_HASH_SCAN:
do_apply_row_ptr= &Rows_log_event::do_hash_scan_and_update;
break;

case ROW_LOOKUP_INDEX_SCAN:
do_apply_row_ptr= &Rows_log_event::do_index_scan_and_update;
break;

case ROW_LOOKUP_TABLE_SCAN:
do_apply_row_ptr= &Rows_log_event::do_table_scan_and_update;
break;

case ROW_LOOKUP_NOT_NEEDED:
DBUG_ASSERT(get_general_type_code() == binary_log::WRITE_ROWS_EVENT);

/* No need to scan for rows, just apply it */
do_apply_row_ptr= &Rows_log_event::do_apply_row;
break;

default:
DBUG_ASSERT(0);
error= 1;
goto AFTER_MAIN_EXEC_ROW_LOOP;
break;
}


slave_rows_search_algorithms 參數不能決定使用哪個索引。
下面這個圖就是決定的過程,可以參考函數 log_event.cc :  void Rows_log_event::decide_row_lookup_algorithm_and_key()

 

 

 

 

ROW_LOOKUP_HASH_SCAN方式的數據查找
總的來講這種方式和 R0W_L00KUPJNDEX_SCAN 和 ROW_LOOKUP_TABLE_SCAN 都不 同,
它是通過表中的數據和Event中的數據進行比對,
而不是通過Event中的數據和表中的數據 逬行比對。


將參數‘slave_rows_search_algorithms’設罝爲叫 INDEX_SCAN,HASH_SCAN 且表上沒有主鍵和唯一鍵的話,
把數據查找的方式設罝爲 ROW_LOOKUP_HASH_SCAN。


在ROW_LOOKUP_HASH_SCAN 兩種數據查找的方式:
• Hi ——> Hash over index
• Ht 一-> Hash over the entire table

判斷 slave_rows_search_algorithms 使用方式
log_event.cc void Rows_log_event::decide_row_lookup_algorithm_and_key()
....
TABLE_OR_INDEX_HASH_SCAN:
....

1. 有索引情況下: 將binlog DELETE/UPDATE Event使用 Hi方式 將每一行數據 HASH 存入到 set結構中( log_event.cc , Rows_log_event::do_hash_row ),
然後對set進行索引定位掃描,每行數據查詢 對 set結構中的數據 進行比對。重複數據越少,索引定位查找越多;重複數據越多,set元素減少,減少索引查找定位開銷。

2. 沒有索引情況下: 將binlog DELETE/UPDATE Event使用Ht 方式全表掃描,
一個Event最大8K,使用Ht會進行每個Event進行一次全表掃描(原來是每行數據 進行一次算表掃描)

考慮另外一種情況,如果每條delete/update語句一次只刪除一行數據而不是delete/update—條語句大量的數據,
這種情況每個DELETE/UPDATE_ROWS_EVENT只有一條數據存在,
那麼使用 ROW_LOOKUP_HASH_SCAN方式並不會提高性能,
因爲這條數據還是需要逬行一次全表掃 描或者索引定位才能查找到數據,
和默認TABLE_SCAN方式沒什麼區別。

 

整個過程參考如下接口:
• Rows_log_event::do_apply_event: 判斷Hash Scan, Index Scan, Table Scan 入口
• Rows_Log_event::do_hash_scan_and_update: 總接口,調用下面兩個接口。
• Rows_Log_event::do_hash_row: 將數據加入到hash結構,如果有索引還需要維護集合(set)
• Rows_log_event::do_scan_and_update: 查找並且逬行刪除操作,會調用 Rows_Log_event::next_record_scan 逬行數據查找。
• Rows_Log_event::next_record_scan: 具體的查找方式實現了Hi --> Hash over index 和Ht --> Hash over the entire table的查找方式

 

還是用Delete的列子,刪除了三條數據,
因此DELETE_ROW_EVENT中包含了 三條數據。
假設參數 slave_rows_search_algorithms 設置爲 INDEX_SCAN,HASH_SCAN。
表中沒有主鍵和唯一鍵,因此會最終使用 ROW_LOOKUP_HASH_SCAN逬行數據查找。
但是因爲有一個索引key a,因此會使用到 Hi—> Hash over index。爲了更好的描述Hi和Ht兩種方式,
假定另一種情況是表上一 個索引都沒有,
將兩種方式放到一個圖中方便大家發現不同點,
如下圖 :

 

 


總結
主庫沒有Primary Key如果在從庫建立一個Primary Key能降低延遲嗎?
肯定的,因爲從庫會根據Event中的行數據進行使用索引的選擇。

• slave_rows_search_algorithms參數設罝了 HASH_SCAN並不一定會提高性能,只有滿 足如下兩個條件纔會提高性能:
(1) (表中沒有任何索引)或者(有索引且本條update/delete的數據關鍵字重複值較 多)。
(2) —個update/delete語句刪除了大星的數據,
形成了很多個8K左右的 UPDATE_ROW_EVENT/DELETE_ROW_EVENT。
update/delete語句只修改少量的數 據(比如每個語句修改一行數據)並不能提高性能。

•從庫索引的利用是自行判斷的,順序爲主鍵-> 唯一鍵-> 普通索引。
•如果slave_rows_search_algorithms參數沒有設罝HASH_SCAN,
並且沒有主鍵/睢一鍵 那麼性能將會急劇下降造成延遲。
如果連索引都沒有那麼這個情況更加嚴重,
因爲更改 的每一行數據都會引發一次全表掃描。

 

這種問題就是我們前面說的,因爲 mi->clock__diff_with_master 只會在I0線程啓動的時候逬行 初始化,

如果手動改小從庫服務器系統的時間,那麼公式中的(long)(time(0))將變小,
如果整 個計算結果爲負數那麼Seconds_Behind_Master將顯示爲0。
因此我們需要保證主從服務器的 系統時間同步才行。

 

有了上面的分析我們應該清楚的知道在GTID AUTCLPOSITION MODE模式下,
我們應該通過 比較主庫和從庫的Executed_Gtld_Set來確保沒有主從延遲,
因爲比較 Seconds_Behind_Master是否爲0並不一定靠譜。

 

從庫的SQL線程(MTS協調線程)和參數 sql_slave_skip_counter

總的說來SQL線程有以下三個主要功能:
1. 讀取relay log中的Event。
2. 應用這些讀取到的Event,將修改作用於從庫。
3. 如果是MTS通常情況下則不會應用Event,SQL線程會蛻變爲協調線程,分發Event給工 作線程。


一、流程圖

 


二、重要步驟說明
1、如果是MTS需要啓動工作線程
每次重新啓動SQL線程(MTS協調線程)的時候 slave_parallel_workers 參數 纔會生效。


2、檢查relay_log_info參數
在GTID AUTO_POSITION MODE模式下 和POSITION MODE模式下這個參數設罝非常重要參數 relay_log_info = TABLE ,
啓動成功的關鍵就是slave_relay_log_info信息的正確性

如果沒有設置爲TABLE, MySQL就會出現Warning:
Slave SQL for channel If a crash happens this configuration does not guarantee that the relay log info will be consistent, Error_code: 0


3、 狀態reading event from the relay log
開始讀取Event的時候會逬入狀態:‘reading event from the relay log’。

4、 在MTS模式下判定是否需要進行MTS檢查點
每讀取一個Event都會去判斷是否需要進行MTS的檢查點,其條件有兩個:
• MTS的檢查點超過參數 slave_checkpoint_period 的設置
• GAQ隊列已滿


源碼中如下:
是否超過檢査點週期
rpl_slave.cc

處理rli-> gaq以找出存儲在集中恢復表中的 lwm 位置。
bool mts_checkpoint_routine(Relay_log_info *rli, ulonglong period,
bool force, bool need_data_lock)

set_timespec_nsec(&curr_clock, 0);
ulonglong diff= diff_timespec(&curr_clock, &rli->last_clock);
if (!force && diff < period)
{
/*
We do not need to execute the checkpoint now because
the time elapsed is not enough.
*/
DBUG_RETURN(FALSE);
}


rpl_slave.cc
從中繼日誌中讀取下一個事件。應該從從SQL線程中調用。
static Log_event* next_event(Relay_log_info* rli)

是否已經GAQ隊列已經滿了
//如果達到了 GAQ的大小設貿爲force強制checkpoint
bool force= (rli->checkpoint_seqno > (rli->checkpoint_group - 1));

 

5、判斷是否需要切換和清理relay log
事務是連續性,即使relay log 和binary log 當前滿了也不能切換.
當recovery_relay_log = 0時候,可以打破事務連續性這個硬性條件
例如在GTID AUTO_POSITION MODE模式同時從庫異常重啓情況下relay log會產生partial transaction,
並且啓動後恢復時候會造成額外回滾操作。
在POSITION MODE模式下繼續發送事務餘下部分的Event到當前relay log

源碼註釋如下,‘in a group’就代表在一個事務中:

/* 
If we have reached the limit of the relay space and we
are going to sleep, waiting for more events:

1. If outside a group, SQL thread asks the IO thread 
to force a rotation so that the SQL thread purges 
logs next time it processes an event (thus space is
freed).

2. If in a group, SQL thread asks the IO thread to 
ignore the limit and queues yet one more event 
so that the SQL thread finishes the group and 
is are able to rotate and purge sometime soon.
*/


rpl_slave.cc
relay_log_purge=ON, 非當前relay log讀取完了的Event會進入清理流程。

if (relay_log_purge)    //做relay log的清理
{

if (rli->relay_log.purge_first_log
(rli,
rli->get_group_relay_log_pos() == rli->get_event_relay_log_pos()
&& !strcmp(rli->get_group_relay_log_name(),rli->get_event_relay_log_name())))
//做relay log的清理
{
errmsg = "Error purging processed logs";
goto err;
}
DBUG_PRINT("info", ("next_event group master %s %lu group relay %s %lu event %s %lu\n",
rli->get_group_master_log_name(),
(ulong) rli->get_group_master_log_pos(),
rli->get_group_relay_log_name(),
(ulong) rli->get_group_relay_log_pos(),
rli->get_event_relay_log_name(),
(ulong) rli->get_event_relay_log_pos()));
}
else
{
// 錯誤時候,鎖定relay log
if (rli->relay_log.find_next_log(&rli->linfo, !hot_log))
{
errmsg = "error switching to the next log";
goto err;
}
rli->set_event_relay_log_pos(BIN_LOG_HEADER_SIZE);
rli->set_event_relay_log_name(rli->linfo.log_file_name);

rli->flush_info();
}

 

 

如果是讀取完的是當前relay log,則不能逬行清理,
會等待10線程的喚醒,
如果是MTS等待喚醒期間還需要進行MTS的檢查點。


6、獲取last_master_timestamp
在單SQL線程下計算Seconds_Behind_Master其中最重要的因素就來 自這裏。
但是對於MTS而言,last_master_timestamp來自檢查點指向 事務的XID_Event的時間,
因此MTS計算Seconds_Behind_Master的方法是不同。源碼如 下:

rpl_slave.cc
static int exec_relay_log_event(THD* thd, Relay_log_info* rli)
{
......
if ((!rli->is_parallel_exec() || rli->last_master_timestamp == 0) &&    //event header 的timestamp
!(ev->is_artificial_event() || ev->is_relay_log_event() ||
(ev->common_header->when.tv_sec == 0) ||    //query event纔有的執行時間
ev->get_type_code() == binary_log::FORMAT_DESCRIPTION_EVENT ||
ev->server_id == 0))
{
rli->last_master_timestamp= ev->common_header->when.tv_sec +
(time_t) ev->exec_time;
//但是對於MTS來講應該注意是最後一個XID EVENT的時間不是這裏設置的,
//在 mts_checkpoint_routine 裏面
DBUG_ASSERT(rli->last_master_timestamp >= 0);
}

......

 

注意到其中一個重要條件就是非MTS環境下,即!rli->is_parallel_exec()條件需要滿足。


7、進行 partial transaction的恢復
下面兩種情況可能出現這種情況:
• GTID AUTCLPOSITION MODE模式下如果 I/O 線程出現重連,DUMP線程會根據GTID SET進行重新定位,重發部分已經發送過的Event。
• GTID AUTCLPOSITION MODE模式下如果從庫異常重啓,並且recovery_relay_log=0 的情況下,
DUMP線程會根據GTID逬行重新定位,重發部分已經發送過的Event。


這兩種情況下甶於一個事務可能包含部分重暨的Event,就涉及到回滾操作。
對於MTS來講是 由協調線程進行回滾,也就是這裏。如果是非MTS環境下則在GTID Event應用的時候進行回 滾。


8、sql_slave_skip_counter參數
注意在GITD環境下不能使用sql_slave_skip_counter跳過Event
sql_slave_skip_counter參數的基本計數單位是Event,
但是如果最後一個Event正處於事務中 的時候,
整個事務也會被跳過。
源碼:

log_event.cc
Log_event::enum_skip_reason
Log_event::do_shall_skip(Relay_log_info *rli)
{
DBUG_PRINT("info", ("ev->server_id=%lu, ::server_id=%lu,"
" rli->replicate_same_server_id=%d,"
" rli->slave_skip_counter=%d",
(ulong) server_id, (ulong) ::server_id,
rli->replicate_same_server_id,
rli->slave_skip_counter));
if ((server_id == ::server_id && !rli->replicate_same_server_id) ||
(rli->slave_skip_counter == 1 && rli->is_in_group()))
return EVENT_SKIP_IGNORE;
else if (rli->slave_skip_counter > 0)
return EVENT_SKIP_COUNT;
else
return EVENT_SKIP_NOT;
}

 

 

9、MTS進行分發,單SQL線程進行應用Event處理:
• MTS進行Event分發給工作線程進行Event的apply。
• 單SQL線程進行Event的應用。

 

三、各個Event大概做了什麼
最終從庫就開始應用Event 了,
開啓GTID的情況下一個簡單的‘Delete’語句生成的Event爲例,Event包含:

Event:
1. GTID_EVENT
2. QUERY_EVENT
3. MAP_EVENT
4. DELETE_EVENT
5. XID_EVENT

1. GTID_EVENT
• 接口 log_event.cc int Gtid_log_event::do_apply_event(Relay_log_info const *rli)
• 單SQL線程下回滾partial transaction
• 設置事務的GTID 參考 set_gtid_next 函數。

2. QUERY_EVENT
參接□ int Query_log_event::do_apply_event(Relay_log_info const *rli)
• 設置線程的執行環境,比如SQL_MODE,客戶端字符集、自增環境、客戶端排序字符集、當前登錄的數據庫名等。
• 執行相應的語句。對於ROW模式的DML語句是‘BEGIN’,DDL而言就是執行實際的語句了。‘BEGIN’語句只是在MySQL層做了一個標記,如下:
lib_sql.cc
void *create_embedded_thd(int client_flag)
......
if (thd->variables.max_join_size == HA_POS_ERROR)
thd->variables.option_bits |= OPTION_BIG_SELECTS;

語句進入提交流程後會判斷這個標記如下:
sql_class.h
inline bool in_multi_stmt_transaction_mode() const
{
return variables.option_bits & (OPTION_NOT_AUTOCOMMIT | OPTION_BEGIN);
}

如果設罝了‘BEGIN’標記則不會進入order commit流程。


3. MAP_EVENT
• 接□ log_event.cc int Table_map_log_event::do_apply_event(Relay_log_info const *rli)
• 設置表的相關的屬性,比如數據庫名、表名、字段數量、字段類型、可變字段的長度等。
• 設置table id。


4. DELETE_EVENT
• 接口 log_event.cc int Rows_log_event::do_apply_event(Relay_log_info const *rli)
• 檢查本事務是否GTID,是則需要跳過。這個判斷過程位於函數 rpl_gtid_execution.cc static inline bool is_already_logged_transaction(const THD *thd)
• 打開表獲取MDL鎖。
• 設置table id和表的映射
• 開啓讀寫事務。
• Innodb行鎖。
• 對每行數據進行刪除/修改操作。

可以看到對於從庫來講同樣需要開啓讀寫事務上鎖等操作,
如果從庫有相應 的DML操作,
SQL線程(MTS工作線程)也可能因爲不能獲得鎖而堵塞。

5.XID_EVENT
• 接口log_event.cc int Xid_apply_log_event::do_apply_event(Relay_log_info const *rli) (單SQL線程)或者 int Xid_apply_log_event::do_apply_event_worker(Slave_worker *w) (MTS)
• 更新內存位點信息。
• 單SQL線程更新slave_relay_log_info表中的相關信息,MTS更新slave_worker_info表 中的相關信息。 rpl_info_table.cc int Rpl_info_table::do_flush_info(const bool force)爲相應的更新接口。
• 進行提交操作。

 

 

 

 

總結MySQL服務器層面影響主從複製情況: 

1.  從庫的系統時間被修改比當前時間更大(推薦配置時間服務器)

2.  在MTS設置不合理slave_checkpoint_period 參數

3. 主庫大事務的sql語句性能:  延遲不會從0開始 而是直接從主庫執行了當前事務多長時間開始。例如主庫執行這個事務花費的20秒,那麼延遲就會從20開始 , 導致 Query Event中沒有準確執行時間 . ( 從這裏參考 (3) mi->rli->last_master_timestamp ) 

4. 大表DDL: 延遲會從0開始,因爲Query Event記錄了準確的執行時間。

5. slave_rows_search_algorithms 參數 , 表沒有主鍵或者唯一鍵,就不要設置INDEX_SCAN,HASH_SCAN

6. slave_checkpoint_period ( 默認 300ms) 在MTS中的Seconds_Behind_Master計算和檢查點有關。( 參考 • DML (MTS) )

7. 根據 線程模式 , binlog事務模式, recovery_relay_log , sync_master_info , sync_relay_log , sync_relay_log_info 進行一個性能判斷,如下:

 8. 表沒有主鍵或者唯一鍵造成的延遲。這種情況不要以爲設置slave_rows_search_algorithms參數爲 INDEX_SCAN,HASH_SCAN

 9. 長時間不提交的事務可能造成延遲瞬間增加,因爲GTID_EVENT和XID_EVENT是提交時間,而其他Event是命令發起的時間。

 

 

待續....

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章