天天看點

MySQL多線程備份工具mydumper 之 RDS外部執行個體遷移平台

此文已由作者溫正湖授權網易雲社群釋出。

歡迎通路網易雲社群,了解更多網易技術産品營運經驗。

1、Format_description_event問題:

BINLOG '
kTXkUxMKAAAALQAAAPoDAAAAAEgAAAAAAAEABHRlc3QAAnQzAAEDAAFS1mTW
kTXkUyAKAAAALQAAAOgEAAAAAEgAAAAAAAEAAgAB//4BAAAA/gIAAAC52uck'/*!*/;      

假設以上是使用 ../mysql/bin/mysqlbinlog -v --start-position=1094  mysql-bin.000036生成的一個BINLOG,類型為 DELETE_ROW_LOG_EVENT。内容如下:

### DELETE FROM `test`.`t3`### WHERE###  
 @1=1### DELETE FROM `test`.`t3`### WHERE### 
   @1=2      

mysql用戶端,單獨執行這個binlog無法達到删除t3中 (1), (2)這兩行的目的。報錯如下:

mysql> BINLOG '
kTXkUxMKAAAALQAAAPoDAAAAAEgAAAAAAAEABHRlc3QAAnQzAAEDAAFS1mTW
kTXkUyAKAAAALQAAAOgEAAAAAEgAAAAAAAEAAgAB//4BAAAA/gIAAAC52uck'/*!*/;
ERROR 1609 (HY000): The BINLOG statement of type `Table_map` was not preceded by a format description BINLOG statement.
mysql>      

DBA剛開始誤以為是個mysql bug。其實不然。

錯誤提示需要先執行format description BINLOG,即需要先執行Format_description_event,該binlog用于描述接下來要執行的BINLOG的版本等資訊,如下:

# at 4#140808 10:26:53 server id 10  end_log_pos 121 CRC32 0x5731b21e Start: binlog v 4, server v 5.6.19-log created 140808 10:26:53# Warning: this binlog is either in use or was not closed properly.BINLOG '
bTXkUw8KAAAAdQAAAHkAAAABAAQANS42LjE5LWxvZwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAEzgNAAgAEgAEBAQEEgAAXQAEGggAAAAICAgCAAAACgoKGRkADQEe
sjFX'/*!*/;      

該BINLOG位于每個binlog檔案偏移位置4開始。

似乎是合理的,在mysql-5.5.30版本中,DBA第二次執行原來的DELETE_ROW_LOG_EVENT時,卻能夠執行成功。這似乎不合理。其實第二次執行也不應該成功,這是mysql-5.5.30的一個bug

連結為:https://bugs.launchpad.net/percona-server/+bug/966148

在mysql 5.6.19驗證了下,該bug已經修複:

mysql> BINLOG 'kTXkUxMKAAAALQAAAPoDAAAAAEgAAAAAAAEABHRlc3QAAnQzAAEDAAFS1mTW 
kTXkUx4KAAAALQAAACcEAAAAAEgAAAAAAAEAAgAB//4BAAAA/gIAAAC52uck
'/*!*/;
ERROR 1609 (HY000): The BINLOG statement of type `Table_map` was not preceded by a format description BINLOG statement.
mysql> BINLOG 'kTXkUxMKAAAALQAAAPoDAAAAAEgAAAAAAAEABHRlc3QAAnQzAAEDAAFS1mTW 
kTXkUx4KAAAALQAAACcEAAAAAEgAAAAAAAEAAgAB//4BAAAA/gIAAAC52uck
'/*!*/;
ERROR 1609 (HY000): The BINLOG statement of type `Table_map` was not preceded by a format description BINLOG statement.
mysql> 
mysql> BINLOG 'kTXkUxMKAAAALQAAAPoDAAAAAEgAAAAAAAEABHRlc3QAAnQzAAEDAAFS1mTW 
kTXkUx4KAAAALQAAACcEAAAAAEgAAAAAAAEAAgAB//4BAAAA/gIAAAC52uck
'/*!*/;
ERROR 1609 (HY000): The BINLOG statement of type `Table_map` was not preceded by a format description BINLOG statement.
mysql>      

即不管執行多少次同一個BINLOG,都無法成功。除非先執行了Format_description_event:

mysql> BINLOG '                                                                                                                                       
    '> bTXkUw8KAAAAdQAAAHkAAAABAAQANS42LjE5LWxvZwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA    '> AAAAAAAAAAAAAAAAAAAAAAAAEzgNAAgAEgAEBAQEEgAAXQAEGggAAAAICAgCAAAACgoKGRkADQEe
    '> sjFX    '> '/*!*/;  //這是一個Format_description_eventQuery OK, 0 rows affected (0.00 sec)

mysql> BINLOG '    '> kTXkUxMKAAAALQAAAPoDAAAAAEgAAAAAAAEABHRlc3QAAnQzAAEDAAFS1mTW
    '> kTXkUx4KAAAALQAAACcEAAAAAEgAAAAAAAEAAgAB//4BAAAA/gIAAAC52uck
    '> '/*!*/;
Query OK, 0 rows affected (0.00 sec)      

2、執行binlog時“0 rows affected”問題

這樣就成功得将對應的t3中對應的(1),(2)删掉。但問題也來了,為什麼執行完顯示 “Query OK, 0 rows affected (0.00 sec)”。這個讓人很費解。

“0 rows affected” 這個問題,也是有DBA提出來了,他們在使用mysqlbinlog -B 進行flashback操作時,執行過程一直是“Query OK, 0 rows affected (0.00 sec)”,懷疑

flashback功能出了問題。其實是因為執行BINLOG直接調用存儲引擎的相關接口,跟正常的sql語句執行走的代碼路徑不完全相同,是以rows affected沒有得到正确統計。

繼續回到Format_description_event問題上,需要注意的是,在一次用戶端連接配接期間,隻需執行一次Format_description_event即可。這個也是好了解的。

3、flashback功能需要與-v配合使用問題

不過mysql的flashback工具确實存在一個問題,就是在flashback update 操作時,需要-v和-B一起作用才能夠成功。這是因為:

Rows_log_event::exchange_update_rows在進行set和where内容調換時,需要計算這兩段内容的長度,該功能由Rows_log_event::print_verbose_one_row完成,

該函數會根據表定義table_def (td)來擷取表中每個field的length,并通過binlog中的記錄擷取set和where中那幾個field是NULL的。

而td是從Table_map_log_event中擷取的,且隻有在-v時才會生成td:

if (print_event_info->verbose)
  {

    Rows_log_event *ev= NULL;
    Log_event_type et= (Log_event_type) ptr[EVENT_TYPE_OFFSET];    if (checksum_alg != BINLOG_CHECKSUM_ALG_UNDEF &&
        checksum_alg != BINLOG_CHECKSUM_ALG_OFF)
      size-= BINLOG_CHECKSUM_LEN; // checksum is displayed through the header
    
    switch(et)
    {    case TABLE_MAP_EVENT:
    {
      Table_map_log_event *map; 
      map= new Table_map_log_event((const char*) ptr, size, 
                                   glob_description_event);
      print_event_info->m_table_map.set_table(map->get_table_id(), map);break;
    }      

通過set_table(map->get_table_id(), map)來将對應的map( td從這裡來map->create_table_def() )放入table的哈希表中;

是以,不加-v進行update操作的flashback解析時,由于無法通過map= print_event_info->m_table_map.get_table(m_table_id)來擷取map進而擷取td。

導緻無法知道表定義,進而無法進行set和where長度的計算并進行内容調換。

為了修複這個bug,修改如下:

if (is_flashback) //Flashback
  {
    uint32 fb_size = size;if (checksum_alg != BINLOG_CHECKSUM_ALG_UNDEF &&
 checksum_alg != BINLOG_CHECKSUM_ALG_OFF)
fb_size-= BINLOG_CHECKSUM_LEN; // checksum is displayed through the header
    switch (ptr[EVENT_TYPE_OFFSET]) { //add TABLE_MAP_EVENT, as UPDATE_ROWS_EVENT need to get_table from map
 case TABLE_MAP_EVENT:
 {
Table_map_log_event *map; 
map= new Table_map_log_event((const char*) ptr, fb_size, 
glob_description_event);
print_event_info->m_table_map.set_table(map->get_table_id(), map);break;
 }      case WRITE_ROWS_EVENT:
        ptr[EVENT_TYPE_OFFSET]= DELETE_ROWS_EVENT;        break;
....  if (print_event_info->verbose)
  {
    Rows_log_event *ev= NULL;
    Log_event_type et= (Log_event_type) ptr[EVENT_TYPE_OFFSET];    if (checksum_alg != BINLOG_CHECKSUM_ALG_UNDEF &&
        checksum_alg != BINLOG_CHECKSUM_ALG_OFF)
      size-= BINLOG_CHECKSUM_LEN; // checksum is displayed through the header
    
    switch(et)
    {    case TABLE_MAP_EVENT:
    { //if flashback, map has already set into hash above
 if (!is_flashback) 
 {
      Table_map_log_event *map; 
      map= new Table_map_log_event((const char*) ptr, size, 
                                   glob_description_event);
      print_event_info->m_table_map.set_table(map->get_table_id(), map);
 } break;
    }      

4、mysqlbinlog解析relay log報“unknown table”問題

組内的小夥伴在做基于relay-log進行的資料庫執行個體故障恢複時,發現mysqlbinlog的又一個bug,社群之前也已提及,如下:http://bugs.mysql.com/bug.php?id=60964。大意是在使用mysqlbinlog解析高可用資料庫執行個體從庫的relay log時,提示“### Row event for unknown table #16#”,簡單分析可知該提示是說DML操作(UPDATE/INSERT/DELETE)所需的table map找不到了,而table map可以将table id(上述的16)跟表定義建立關聯。沒有table map當然就無法解析DML操作。

那麼table map真的不見了嗎?其實不然,隻是table map位于上一個relay log檔案而已。那麼,既然mysqlbinlog能夠同時解析多個檔案,能不能将上一個檔案跟目前檔案一起解析,這樣應該就不會報這個錯誤了吧。我剛開始也是這麼認為的,還跟小夥伴争了,結果應了那句:“沒有調查就沒有發言權”。小夥伴可是試驗過了,結果還是無法解析。個人覺得這不合理啊。

為此,看了mysqlbinlog的代碼,很快就找到了問題所在,下面進行簡單分析

1、mysqlbinlog用一個for循環來處理一個或多個binlog檔案,如下:

// main loop:
 for (save_stop_position= stop_position, stop_position= ~(my_off_t)0 ; (--argc >= 0) ; ) 
 { 
 if (argc == 0) // last log, --stop-position applies stop_position= save_stop_position; 
 if ((retval= dump_log_entries(*argv++)) != OK_CONTINUE) 
     break; 
     
// For next log, --start-position does not apply 
start_position= BIN_LOG_HEADER_SIZE; 
}      

這個循環代碼很是簡練,以至于剛開始看代碼的還是,根本不知道是什麼意思。其實,循環中argc表示有幾個binlog檔案需要解析,*argv表示具體的檔案名稱。start-position和stop_position處理得也很奇妙,在此不細說。

 2、有以上的鋪墊可以知道,dump_log_entries函數是用來處理每個binlog檔案的。

/**
  High-level function for dumping a named binlog.
  This function calls dump_remote_log_entries() or
  dump_local_log_entries() to do the job.
  @param[in] logname Name of input binlog.
  @retval ERROR_STOP An error occurred - the program should terminate.
  @retval OK_CONTINUE No error, the program should continue.
  @retval OK_STOP No error, but the end of the specified range of
  events to process has been reached and the program should terminate.
*/static Exit_status dump_log_entries(const char* logname)
{
  Exit_status rc;
  PRINT_EVENT_INFO print_event_info;  if (!print_event_info.init_ok())    return ERROR_STOP;  if (!only_event_info)
  {  /*
     Set safe delimiter, to dump things
     like CREATE PROCEDURE safely
  */
  fprintf(result_file, "DELIMITER /*!*/;\n");
  strmov(print_event_info.delimiter, "/*!*/;");
  }
  
  print_event_info.verbose= short_form ? 0 : verbose;
  rc= (remote_opt ? dump_remote_log_entries(&print_event_info, logname) :
       dump_local_log_entries(&print_event_info, logname));  if (!only_event_info)
  {  if (!flashback_opt)
  {  /* Set delimiter back to semicolon */
    fprintf(result_file, "DELIMITER ;\n");
    strmov(print_event_info.delimiter, ";");
  }
  }  return rc;
}      

根據解析的binlog是否為遠端的,又分為dump_remote_log_entries和dump_local_log_entries,我們主要用了dump_local_log_entries,進入函數後我們會發現,mysqlbinlog使用Log_event::read_log_event(file, glob_description_event)和process_event(print_event_info, ev, old_off, logname)兩個函數來讀取、建構和解析每個binlog。我們應該關注的是print_event_info,這個是控制整個解析過程的關鍵對象。比如說,如果binlog的類型是TABLE_MAP_EVENT (這個就是問題所在,儲存table map的地方),那麼在解析時,會将table id和具體的表定義聯系起來。并記錄到print_event_info中。process_event在處理WRITE_ROWS_EVENT/UPDATE_ROWS_EVENT/DELETE_ROWS_EVENT(對應前述的DML操作)時,由于event中僅記錄所操作的表的table id,是以就需要借助print_event_info來擷取對應的映射關系。跨relay log的事務在使用mysqlbinlog進行解析時出錯的原因就在于,print_event_info是在dump_log_entries中定義的。在完成一個binlog檔案的解析後會自動銷毀掉,無法繼承給下一個待解析的binlog檔案。

是以,解決問題的辦法很簡單,将PRINT_EVENT_INFO print_event_info定義提取出來,設為mysqlbinlog的全局變量即可解決問題。

之前有小夥伴提問,為什麼不把表定義直接記錄到DML操作中,答案是顯而易見的,如果每個DML的binlog都記錄表定義,一是沒有必要,二是太浪費存儲空間了。

網易雲免費體驗館,0成本體驗20+款雲産品! 

更多網易技術、産品、營運經驗分享請點選。

相關文章:

【推薦】 測試環境docker化(一)—基于ndp部署模式的docker基礎鏡像制作