数据库内核月报

数据库内核月报 - 2025 / 11

浅析Xtrabackup备份工具

Author: 昭晨

引言

在MySQL数据库运维过程中,为保证业务的完整性,经常需要对数据库进行备份。虽然mysqldump是最常用的逻辑备份工具,但在面对TB级大表或需要不停机备份的场景时,其存在较明显的性能瓶颈。

针对这种备份场景,一种更高效的解决方案是使用热备份工具,即Percona XtraBackup,对MySQL数据库的全量备份和增量备份。

本文将带你全面了解 XtraBackup的常用命令和典型使用场景,并从代码层面简述其工作原理。

什么是XtraBackup

XtraBackup是Percona提供的一款开源免费的MySQL热备份工具,用于MySQL数据库的备份场景。其主要有以下优点:

Xtrabackup常用于日常的全量/增量备份,主从复制的初始化,数据库迁移与克隆以及灾难恢复等场景。

如无特别说明,本文基于MySQL之后的版本进行论述。需要注意的是,xtrabackup的版本和MySQL的版本有较强的对应关系。对于更早的MySQL版本(如5.6, 5.7等版本),需要使用xtrabackup 2.4版本。在8.0及之后的MySQL版本中,xtrabackup的版本应大于等于MySQL的版本,且尽量保持一致。

主要流程和原理

以一主一备的两个实例,并且希望通过全量备份主库的数据来搭建备库的使用场景为例,主要的流程是backup + prepare + copy-back / move-back。本文中涉及的代码为Percona XtraBackup 8.0.22-15版本的代码。

backup

这一过程主要是将主库中的各类数据备份到用户指定的路径中。

常用命令

这一过程的命令和常用的参数如下:

xtrabackup –defaults-file=[your cnf file] –backup -u[user] -S[socket] -H[host] -P[port] -p[password] –target-dir=[your backup dir] –stream=xbstream

这些参数的含义如下:

如果使用了xbstream流式备份工具,参考命令如下:

接收端的命令如下,其中port表示nc工具监听的端口,-x表示xbstream使用解压操作,-C指定解压后的文件存放的位置,等同于指定target-dir。

nc -l4 [port] 2>> [xbstream.log] | xbstream -x -C [target-dir]  2>> [xbstream.log]

发送端的命令如下,和不使用流式备份不同的是,这里可以不指定target-dir。

xtrabackup --defaults-file=[your cnf file] -u[user] -S[socket] -H[host] -P[port] -p[password] --stream=xbstream --backup  2>> [backup.log] | nc [ip] [port]

backup结束后会在stdout打印出”completed OK!”,可以在target-dir中看到备份完成的文件。

原理

backup的核心函数是xtrabackup_backup_func,其备份过程主要可以分为以下几个步骤:

  1. 初始化数据库的必要组件,包括innodb的相关参数、os_event、block_cache等必要组件。同时,将xtrabackup的工作目录设置为数据库实例的dbs目录,以便后续备份数据。

  2. 设置datasink,确定以何种方式写备份数据。对应的函数是xtrabackup_init_datasinks。写备份数据的主要方式是DS_TYPE_LOCALDS_TYPE_STDOUT。如果没有指定xbstream流式备份,那么备份数据将写成本地的磁盘文件。如果制定了xbstream流式备份,那么备份数据将作为数据流被定向到标准输出STDOUT上,一般情况下用于跨机备份和网络传输的场景,需要结合xbstream工具使用。这一步具体在redo_mgrinitstart之间执行。

  3. 初始化redo_mgr,备份redo log

    这一过程是backup过程中几乎贯穿全程的过程,涉及到的核心函数主要是redo_mgr中的initstartstop_at三个函数。涉及到的核心结构体是Redo_Log_Data_Manager

    (3.1) Redo_Log_Data_Manager

    这一结构体主要包含reader、writer和parser三个部分,分别对应了redo log备份时的读、写和解析三大功能。这一结构体同时还包含了三个主要逻辑函数initstartstop_at,实际做备份的函数是copy_funccopy_once这两个函数。

    class Redo_Log_Data_Manager {
     public:
      bool init();    // 初始化
      bool start();   // 启动redo_mgr线程
      bool stop_at(lsn_t lsn);   // 指定redo_mgr线程拷贝到指定lsn结束
      ...
        
     private:
      void copy_func();  // redo_mgr线程的主函数,负责redo log的拷贝
      bool copy_once(bool is_last, bool *finished);  // 每次拷贝一块edo log
      IB_thread thread;  // redo_mgr线程
      std::atomic<lsn_t> stop_lsn; // redo_mgr线程拷贝到这个lsn结束,该值由stop_at函数指定
      lsn_t last_checkpoint_lsn;   // backup结束时的checkpoint lsn
      lsn_t start_checkpoint_lsn;  // backup开始时的checkpoint lsn
      lsn_t scanned_lsn;       // 最后一个扫描的lsn
      Redo_Log_Reader reader;  // 读取redo log
      Redo_Log_Writer writer;  // 写redo log
      Redo_Log_Parser parser;  // 解析redo log
      os_event_t event;        // redo_mgr结束的事件监听
      ...
    };
    

    (3.2) init函数:redo_mgr初始化

    init函数初始化了log_sys(Redo日志系统)和recv_sys(恢复系统)两大组件,用于后续的日志拷贝和解析。之后,打开数据库的redo log文件并加载到内存中。init函数同时也初始化了Redo_Log_Data_Manager的thread变量,将其启动时执行的函数设置为copy_func函数,等待后续在start函数中启动拷贝redo log的逻辑。

    bool Redo_Log_Data_Manager::init() {
      thread = os_thread_create(PFS_NOT_INSTRUMENTED, [this] { copy_func(); });    // 初始化thread,并将其主函数设置为copy_func
      // 初始化log_sys
      if (!log_sys_init(srv_n_log_files, srv_log_file_size,
                        dict_sys_t::s_log_space_first_id)) {
        return (false);
      }
        
      // 初始化recv_sys
      recv_sys_create();
      recv_sys_init(buf_pool_get_curr_size());
      ...
      for (ulong i = 0; i < srv_n_log_files; i++) {
        dberr_t err = open_or_create_log_file(&log_file_created, i, &log_space);   // 打开redo log的日志文件
        ...
      }
    }
    

    (3.3) start函数:拷贝redo log并且启动redo_mgr线程

    start函数的主要逻辑是:获取checkpoint信息,创建备份的redo log文件并写头部,按块拷贝redo log,启动redo_mgr线程。

    bool Redo_Log_Data_Manager::start() {
      error = true;
          
      // 获取checkpoint
      if (!reader.find_start_checkpoint_lsn()) {
        return (false);
      }
          
      // 创建redo log文件xtrabackup_logfile
      if (!writer.create_logfile(XB_LOG_FILENAME)) {
        return (false);
      }
          
      // 写redo log的头部元信息
      if (!writer.write_header(reader.get_header())) {
        return (false);
      }
        
      ...
        
      bool finished = false;
      while (!finished) {
        if (!copy_once(false, &finished)) {   // 拷贝redo log
          return (false);
        }
      }
        
      // 从这里开始,如果扫描到了DDL操作相关的redo,应当终止backup
      mdl_taken = true;
      thread.start();    // 启动redo_mgr线程
      error = false;
      return (true);
    }
    

    start函数是主线程执行的,函数中追赶了可能正在写的redo log,直至追上才会跳出while循环。当xtrabackup追上redo log之后,后续将不再允许DDL操作,否则会导致备份失败。

    Q: 为什么在xtrabackup运行期间,需要禁止DDL操作? A: xtrabackup在备份期间,其假设被拷贝的数据页是稳定的,且后续可以通过 replay redo log 恢复到某个一致性时间点。但DDL操作通常会变动底层物理文件(如ibd文件),并且做表空间结构的变更,从而破坏了xtrabackup的这一假设。

    在之前的init函数中,thread变量的启动函数被设置为copy_func,因此在thread.start()之后,redo_mgr线程将启动,并且运行copy_func函数继续拷贝redo log。

    (3.4) copy_func函数:redo_mgr线程的主要逻辑

    主要是在while循环中执行单次拷贝redo log的逻辑(即copy_once函数),如果已经扫描到redo log的末尾,会休眠一段时间后,再次拷贝redo log。stop_lsnstop_at函数指定。当指定了stop_lsn后,while循环将在读到超过stop_lsn的redo log后退出,并且再copy_once一次,尽可能使得读到的redo log位置和当前redo log已经写到的位置接近。

    void Redo_Log_Data_Manager::copy_func() {
      ...
      while (!aborted &&
             (stop_lsn == 0 || stop_lsn > reader.get_scanned_lsn())) {
        ...
        if (!copy_once(false, &finished)) {    // 单次拷贝redo log
          error = true;
          return;
        }
        
        if (finished) {
          ...
          // 等待一段时间,默认值是1秒
          os_event_reset(event);
          os_event_wait_time_low(event, copy_interval * 1000UL, 0);
        }
      }
        
      if (!aborted && !copy_once(true, &finished)) {
        error = true;
      }
      ...
    }
    

    (3.5) copy_once函数:单次拷贝redo log的逻辑,依次read、scan、parse和write redo log

    bool Redo_Log_Data_Manager::copy_once(bool is_last, bool *finished) {
      ...
        
      /* 读取redo log,每次读取64KB,总共读取1/2 innobase_log_buffer_size大小.
         随后,对读取的每个log block做简单扫描,检查log block中的no、data_len、
         checksum等信息,记录本次扫描的log block的大小并更新scanned_lsn */
      auto len = reader.read_logfile(is_last, finished);
      ...
        
      /* 解析redo log,核心目的有两个:
         1. 保证redo log的正确性,确保没有读到坏redo或者脏redo
         2. 在redo_mgr线程解析redo日志的过程中,如果读到了DDL操作相关的
         MLOG_INDEX_LOAD日志,需要停止backup */
      if (!parser.parse_log(reader.get_buffer(), len, start_lsn,
                            reader.get_start_checkpoint_lsn())) {
        return (false);
      }
        
      /* 将本次读取到的redo log落盘 */
      if (!writer.write_buffer(reader.get_buffer(), len)) {
        return (false);
      }
        
      return (true);
    }
    

    (3.6) stop_at函数:由主线程指定redo_mgr线程跳出while主循环的stop_lsn,这个lsn是通过log_status_get函数中的SQL语句获取的,该lsn作为stop_at函数的传入参数被指定。设置过stop_lsn后,主线程join,等待redo_mgr线程拷贝完成。该函数在后文backup_start函数之后执行。

    bool Redo_Log_Data_Manager::stop_at(lsn_t lsn) {
      bool last_checkpoint = 
          reader.find_last_checkpoint_lsn(&last_checkpoint_lsn);
      ...
        
      /* 将stop_lsn设置为lsn,会影响到redo_mgr线程中的while循环,
         使得redo_mgr线程读到超过stop_lsn的位置就会跳出循环 */
      stop_lsn = lsn;
      os_event_set(event);
      thread.join();
      ...
        
      if (!writer.close_logfile()) {
        return (false);
      }
        
      return last_checkpoint;
    }
    
  4. 主线程扫描tablespace,确定有哪些ibd文件需要备份。核心函数是xb_load_tablespaces。该函数会扫描数据库实例对应的dbs和log路径,并将合法的需要备份的文件(例如ibd文件,undo文件等)的文件名等信息存入fil_system中。后续使用datafiles_iter_new函数,创建一个文件扫描的迭代器,从fil_system中取出扫描的文件名,存入datafiles_iter_t中的vector。

    static dberr_t xb_load_tablespaces(void) {
      ...
      // 这两行主要检查ibdata文件
      err = srv_sys_space.check_file_spec(false, 0);
      err = srv_sys_space.open_or_create(false, false, 
                          &sum_of_new_sizes, &flush_lsn);
      ...
      // 扫描数据库实例,获取文件信息
      xb_scan_for_tablespaces();
      ...
      // 扫描是否有外部文件(需要备份,但是文件没有在数据库实例对应的路径里的文件)
      for (auto tablespace : Tablespace_map::instance().external_files()) {
        // 这里会扫描到undo文件,但是type是UNDO_LOG,会跳过
        if (tablespace.type != Tablespace_map::TABLESPACE) continue;
        // 扫描到外部文件,打开
        fil_open_for_xtrabackup(tablespace.file_name, tablespace.name);
      }
    }
        
    static void xb_scan_for_tablespaces() {
      // 设置扫描的路径,这里一般是dbs,存放了ibd文件
      // 扫描的路径名存放在fil_system->m_dirs.m_dirs中
      fil_set_scan_dir(MySQL_datadir_path.path());
        
      // 设置扫描的路径,这里一般是log,存放了redo、undo等文件
      fil_set_scan_dir(Fil_path::remove_quotes(srv_data_home));
      ...
        
      // 扫描之前添加的路径,打开对应的文件,并添加到fil_system的shard中
      if (fil_scan_for_tablespaces(true) != DB_SUCCESS) {
        exit(EXIT_FAILURE);
      }
    }
    
  5. 多线程拷贝用户数据。根据backup时指定的 –parallel 参数确定拷贝线程的数量,调用data_copy_thread_func函数拷贝用户的数据文件。用户的数据文件信息在上一步中已经被存入data_thread_ctxt_t类型的结构体中,从该结构体的vector中获取。

      // in xtrabackup_backup_func
      data_threads = (data_thread_ctxt_t *)ut_malloc_nokey(
          sizeof(data_thread_ctxt_t) * xtrabackup_parallel);
      count = xtrabackup_parallel;    // 拷贝线程的数量
      mutex_create(LATCH_ID_XTRA_COUNT_MUTEX, &count_mutex);
        
      for (i = 0; i < (uint)xtrabackup_parallel; i++) {
        data_threads[i].it = it;
        data_threads[i].num = i + 1;
        data_threads[i].count = &count;
        data_threads[i].count_mutex = &count_mutex;
        data_threads[i].error = &data_copying_error;
        /* 每个线程的主函数是data_copy_thread_func,函数内通过
           datafiles_iter_next获取文件信息,
           通过xtrabackup_copy_datafile拷贝数据文件 */
        os_thread_create(PFS_NOT_INSTRUMENTED, data_copy_thread_func,
                         data_threads + i)
            .start();
      }
    
  6. 多线程拷贝非InnoDB的表和文件。对应的主要函数是backup_start函数,主要备份与performance schema有关的文件。

      // in xtrabackup_backup_func
      Backup_context backup_ctxt;
      if (!backup_start(backup_ctxt)) {
        exit(EXIT_FAILURE);
      }
        
    bool backup_start(Backup_context &context) {
      ...
      // 核心函数是backup_files,备份文件
      if (!backup_files(MySQL_datadir_path.path().c_str(), false)) {
        return (false);
      }
      ...
    }
        
    bool backup_files(const char *from, bool prep_mode) {
      ...
      msg_ts("Starting %s non-InnoDB tables and files\n",
             prep_mode ? "prep copy of" : "to backup");
        
      /* 开启多线程备份非InnoDB表和文件,备份线程的函数是backup_thread_func,
         线程的数量由--parallel指定 */
      run_data_threads(from,
                       std::bind(backup_thread_func, 
                                 std::placeholders::_1,
                                 prep_mode, rsync_tmpfile),
                       xtrabackup_parallel, "backup");
      ...
    }
    
  7. 结束备份,释放各种锁,写备份元数据文件。备份的元数据文件由backup_finish函数完成。

      // in xtrabackup_backup_func
      if (!backup_finish(backup_ctxt)) {
        exit(EXIT_FAILURE);
      }
    
    bool backup_finish(Backup_context &context) {
      ...
      // 写backup-my.cnf
      if (!write_backup_config_file()) {
        return (false);
      }
      // 写xtrabackup_info
      if (!write_xtrabackup_info(mysql_connection)) {
        return (false);
      }
      ...
    }
    

prepare

这一过程是对上一步backup生成的文件进行准备,准备后的文件才可以用于数据库实例的恢复。在backup的过程中,数据文件是在不同时间点被逐个复制的,且在复制期间可能仍在被修改,所以这些文件在时间上并不一致。prepare 的作用,就是让所有数据文件在某个单一的时间点上达到完全一致的状态,从而使 InnoDB 能够正常启动它们。

常用命令

这一过程的命令和常用参数如下:

xtrabackup –prepare –use-memory=[memory size] –target-dir=[your backup dir]

这些参数的含义如下:

原理

prepare 的执行过程可以分为以下几个步骤

  1. 切换到对应的工作目录,读取元数据,检查备份的种类

  2. 利用xtrabackup_logfile,初始化redo log

  3. 启动xtrabackup内嵌的innodb,扩展各个数据文件的大小。这一步中的innodb主要做redo log的apply,回放backup的redo log,修改数据文件,保证页的物理一致性。

  4. 关闭xtrabackup_logfile,再次启动innodb生成日志文件,并回滚一些未提交的事务

代码如下:

static void xtrabackup_prepare_func(int argc, char **argv) {
  ...
  // 切换工作目录到指定的target-dir
  if (my_setwd(xtrabackup_real_target_dir, MYF(MY_WME)))
  ...
  // 在target-dir下,读取backup的元数据xtrabackup_checkpoints
  if (!xtrabackup_read_metadata(metadata_path))
  ...
  // 一般情况下,全量备份完成后,backup的type是full-backuped
  if (!strcmp(metadata_type_str, "full-backuped"))
  ...
  
  /* 该函数做了一些redo log的准备工作,主要流程如下:
     1. 打开backup生成的redo log文件xtrabackup_logfile
     2. 从redo log中读取log header,并从中获取checkpoint_no和可以恢复到的
        最大的max_lsn,并用max_lsn更新log header
     3. 扩写文件,将redo log对齐UNIV_PAGE_SIZE_MAX,并通过填0扩写,扩写的
        最终大小与backup得到的文件大小有关
     4. 将xtrabackup_logfile命名为ib_logfile0 */
  if (xtrabackup_init_temp_log()) goto error_cleanup;
  ...
  // 读取backup-my.cnf中的配置信息,并初始化innodb的参数
  if (!validate_options(
          (std::string(xtrabackup_target_dir) + 
           "backup-my.cnf").c_str(),
          orig_argc, orig_argv)) {
    exit(EXIT_FAILURE);
  }
  ...
  if (innodb_init_param()) {
    goto error_cleanup;
  }
  ...
  /* 初始化xtrabackup内部嵌入的修改过的InnoDB,禁用了InnoDB的某些标准
     安全检查,这一步会读取target-dir中的文件信息,并回放redo log */
  if (innodb_init(true, true)) {
    goto error_cleanup;
  }
  // 获取遍历文件的迭代器
  it = datafiles_iter_new();
  while ((node = datafiles_iter_next(it)) != NULL) {
    ...

    /* 以下几行是一个标准的获取某个表空间元数据的操作 
       首先,开启一个mtr,并对表空间的latch加S锁。
       之后,获取该表空间的0号页面,读取其头部,获取size,即整个表空间的大小 
       最后,提交mtr,对表空间做扩展,扩展到size大小 */
    mtr_start(&mtr);
    mtr_s_lock(fil_space_get_latch(space->id), &mtr);
    block = buf_page_get(page_id_t(space->id, 0), 
                         page_size_t(space->flags),
                         RW_S_LATCH, &mtr);
    header = FSP_HEADER_OFFSET + buf_block_get_frame(block);
    size = mtr_read_ulint(header + FSP_SIZE, MLOG_4BYTES, &mtr);
    mtr_commit(&mtr);
    bool res = fil_space_extend(space, size);

    ...
  }
  // 释放迭代器
  datafiles_iter_free(it);

  if (innodb_end()) goto error_cleanup;   // 结束innodb
  innodb_free_param();   // 释放相关变量占用的空间

  /* 关闭临时redo log,具体表现为:
     1. 将该文件从ib_logfile0重新命名回xtrabackup_logfile
     2. 清理该文件的LOG_HEADER_CREATOR部分
     3. 还原部分在xtrabackup_init_temp_log函数中修改的参数 */
  if (xtrabackup_close_temp_log(TRUE)) exit(EXIT_FAILURE);

  ...
  // 写元数据文件xtrabackup_checkpoints
  if (!xtrabackup_write_metadata(filename))

  // 重新启动innodb,以生成log文件
  if (!xtrabackup_apply_log_only) {
    ...
    if (innodb_init_param()) {
      goto error;
    }
    ...
    if (innodb_init(false, false)) goto error;
    if (innodb_end()) goto error;
    innodb_free_param();
  }
  ...
  return;
}

copy-back / move-back

这一过程是将上一步已经准备好的各项备份数据拷贝或移动到对应的路径中,恢复实例。

常用命令

这一过程的命令和常用参数如下

xtrabackup –defaults-file=[your cnf file] –copy-back / –move-back –parallel=[parallel thread num] –target-dir=[your target dir]

原理

copy-back / move-back 的核心函数是 copy_back,其逻辑主要可以分为以下几个步骤:

  1. 根据指定的defaults-file,检查数据库实例对应的文件目录是否存在,如果不存在,则创建

  2. 按顺序依次拷贝或移动各类文件,依次是undo、redo log、系统表、binlog、ibd、performance schema 和 rocksdb文件(如有)

  3. 释放各类组件(如os_event等),释放锁

代码如下:

  // in main
  if (xtrabackup_copy_back || xtrabackup_move_back) {
    ...
    init_mysql_environment();
    // 这里执行copy-back / move-back的逻辑
    if (!copy_back(server_argc, server_defaults)) {
      exit(EXIT_FAILURE);
    }
    cleanup_mysql_environment();
  }

bool copy_back(int argc, char **argv) {
  ...
  // 检查并创建dbs文件目录,用于存放ibd等数据文件
  if (!directory_exists(mysql_data_home, true)) {
    return (false);
  }
  // 检查并创建log文件目录,用于存放undo、redo等日志文件
  if (srv_undo_dir && *srv_undo_dir &&
      !directory_exists(srv_undo_dir, true)) {
    return (false);
  }
  ...
  // 拷贝各类数据文件,这里以拷贝undo为例
  if (srv_undo_tablespaces > 0) {
    // 确定文件目录的位置,这里一般是log
    dst_dir = (srv_undo_dir && *srv_undo_dir)
              ? srv_undo_dir : mysql_data_home;
    // 创建拷贝到本地的datasink
    ds_data = ds_create(dst_dir, DS_TYPE_LOCAL);
    // 根据undo文件的数量,循环拷贝/移动
    for (ulong i = 1; i <= srv_undo_tablespaces; i++) {
      char filename[20];
      // undo文件一般以undo_001、undo_002等命名
      sprintf(filename, "undo_%03lu", i);
      if (Fil_path::get_file_type(filename) != OS_FILE_TYPE_FILE) 
        continue;
      /* 根据指定了copy-back还是move-back决定拷贝还是移动,分别对应了
         copy_file和move_file函数
         copy_file: 核心思路是将target-dir中的文件根据内存buffer的大小
                    分多次读入内存,并写入目标位置的新文件
         move_file: 本质是使用了标准库中的rename函数 */
      if (!(ret = copy_or_move_file(filename, filename, dst_dir, 1,
                                    FILE_PURPOSE_UNDO_LOG))) {
        goto cleanup;
      }
    }

    ds_destroy(ds_data);
    ds_data = NULL;
  }
  ...
  /* 与backup过程类似,copy-back / move-back同样可以多线程执行。
     由run_data_threads函数扫描dir(这里是target-dir),获取文件信息
     由copy_back_thread_func函数取出文件信息,并拷贝或移动。 */
  ret = run_data_threads(".", copy_back_thread_func, 
                         xtrabackup_parallel, "copy-back");
  if (!ret) goto cleanup;
  ...
  // 清理和关闭各类组件
  sync_check_close();
  os_event_global_destroy();

  return (ret);
}

当copy-back / move-back完成后,即可按照配置文件,启动恢复或搭建的数据库实例。

总结

本文较详细地介绍了XtraBackup这一备份工具的优点、应用场景和基本用法,并从源代码层面讲述在全量备份这一场景下,其主要工作流程和原理。

参考

[1] Xtrabackup Source Code

[2] Percona XtraBackup - Documentation