我们生产遇到这一问题:

备份在一个从节点上执行,每周日作全量备份,周一至周六做基于周日全量的增备。每次增量备份都使从库复制延迟大大增加。

mysql 数据库有700多G, buffer pool 80G , 云磁盘IO性能不佳,经过分析监控发现,系统瓶颈在随机读。80G的buffer pool远远不能容纳数据库,许多更新操作都需要从磁盘读取数据页,这大大增加了事务的执行时间。

而我们全量备份几乎不会有延迟,而增量备份延迟剧增。说明xtrabackup的增量备份算法比全量备份消耗更多的read io。(我们有单独的备份盘,write IO不会给mysql的数据目录盘造成压力)

MySQL中的lsn

LSN(log sequence number) 用于记录日志序号,它是一个不断递增的整型变量。每一个redo log 记录都会分配一个唯一的lsn。 每一个redo log entry都是由mtr修改buffer pool中的相关page而产生的。lsn这一信息也会被记录到page中。innodb page的数据结构中有两个变量记录lsn,分别有不同的作用!见buf0buf.h中的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class buf_page_t {
...
lsn_t newest_modification;
/*!< log sequence number of
the youngest modification to
this block, zero if not
modified. Protected by block
mutex */
lsn_t oldest_modification;
/*!< log sequence number of
the START of the log entry
written of the oldest
modification to this block
which has not yet been flushed
on disk; zero if all
modifications are on disk.
Writes to this field must be
covered by both block->mutex
and buf_pool->flush_list_mutex. Hence
reads can happen while holding
any one of the two mutexes */

代码中也有较为详细的注释:

  • newest_modification
    用于记录最近一次修改该page的lsn,它是不断变化的,有新的修改都会将旧的覆盖。
  • oldest_modification
    记录最初一次修改该page的lsn. 它在该page被刷盘之前是不变的。一旦该page被刷盘,它就会被置为0.初次修改一个page时,会同时更新这两个变量,并且把这个page 挂在flush list上。下次redo做checkpoint会根据这个变量的值决定是否刷盘。

因此数据页在刷盘时,newest_modification作为该page的最后lsn存到了磁盘。 xtrabackup 就是根据这个lsn来做增量备份的。

xtrabackup 的增量备份

在Percona官方文档也也说明,增量备份的问题:它需要扫描解析所有的数据页中的内容,读取lsn,并与全备结束的lsn相对比来决定这个page要不要拷贝,这其中需要消耗大量的读IO。为此Percona Server的XtraDB引擎对此进行了优化,使用位图文件跟踪数据页的修改,xtrabackup下次增备时,会读取这一位图文件,定位到哪些page需要拷贝,这大大提高了增备的效率!

An incremental backup copies each page whose LSN is newer than the previous incremental or full backup’s LSN. There are two algorithms in use to find the set of such pages to be copied. The first one, available with all the server types and versions, is to check the page LSN directly by reading all the data pages. The second one, available with Percona Server, is to enable the changed page tracking feature on the server, which will note the pages as they are being changed. This information will be then written out in a compact separate so-called bitmap file. The xtrabackup binary will use that file to read only the data pages it needs for the incremental backup, potentially saving many read requests. The latter algorithm is enabled by default if the xtrabackup binary finds the bitmap file. It is possible to specify xtrabackup –incremental-force-scan to read all the pages even if the bitmap data is available.

Percona server通过一个变量innodb_track_changed_pages来控制是否开启这个功能。开启这个功能后,会产生这样一个位图文件ib_modified_log__.xdb。 其中seq是该文件的序列号,因为这个文件不能无限大, 变量innodb_max_bitmap_file_size控制了它的大小,达到这一设置会产生一个新文件;startlsn是指该文件的记录的起始lsn。 xtrabackup备份时会使用这一信息。Percona server也提供了一些命令来控制相关行为, xtrabackup 在备份时会用这些命令与server交互。 详细参考官方文档

对于无法使用bitmap的官方mysql,PXB备份逻辑的代码实现我也扒了一下源码。

增量备份主要代码逻辑如下write_filt.cc #L113:
关键部分我做了汉语注释,应该都能看得懂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/************************************************************************
Run the next batch of pages through incremental page write filter.

@return TRUE on success, FALSE on error. */
static my_bool
wf_incremental_process(xb_write_filt_ctxt_t *ctxt, ds_file_t *dstfile)
{
ulint i;
xb_fil_cur_t *cursor = ctxt->cursor;
ulint page_size = cursor->page_size;
byte *page;
xb_wf_incremental_ctxt_t *cp = &(ctxt->u.wf_incremental_ctxt);

for (i = 0, page = cursor->buf; i < cursor->buf_npages;
i++, page += page_size) {
//判断该page最近一次操作的LSN,是否大于增备的起始LSN
//如果不是则跳过
if (incremental_lsn >= mach_read_from_8(page + FIL_PAGE_LSN)) {

continue;
}

//一个delta_buf大小为4096(4k) 个page ,称为一个cluster
//每隔4096个page会有一个xtra标记page(cluster header),该page前4个字节是:0x78747261UL 标记为xtra,后面每4个字节存储数据页的page no.
//后面4095个page为数据页
/* updated page */
if (cp->npages == page_size / 4) {
/* flush buffer */
if (ds_write(dstfile, cp->delta_buf,
cp->npages * page_size)) {
return(FALSE);
}

/* clear buffer */
memset(cp->delta_buf, 0, page_size / 4 * page_size);
/*"xtra"*/
mach_write_to_4(cp->delta_buf, 0x78747261UL);
cp->npages = 1;
}
//写入page number,第一个页的page number为0
//每隔4096个page则会有一个这样的page,这种page的结构是
//|0x78747261UL|pageno|pageno|...
//前4个字节是0x78747261UL,后面是所拷贝page的pageNo.
//第一个page No. 是0
mach_write_to_4(cp->delta_buf + cp->npages * 4,
cursor->buf_page_no + i);
//拷贝一个page
memcpy(cp->delta_buf + cp->npages * page_size, page,
page_size);

cp->npages++;
}

return(TRUE);
}

在文件fil_cur.cc#L40定义了每次读取的chunk size

1
2
 /* Size of read buffer in pages (640 pages = 10M for 16K sized pages) */
#define XB_FIL_CUR_PAGES 640

每次从数据文件中读取640个page(10M) ,而在上面的函数wf_incremental_process中可以看到,一个delta_buf大小为4096(4k) 个page,也就是每读取4096个页面才会发生一次写入操作。这造成了多次读取才会发生一次写入。这样Read IO 就会更加密集。

全量备份拷贝页面逻辑如下(write_filt.cc 文件尾部)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/************************************************************************
Write the next batch of pages to the destination datasink.

@return TRUE on success, FALSE on error. */
static my_bool
wf_wt_process(xb_write_filt_ctxt_t *ctxt, ds_file_t *dstfile)
{
xb_fil_cur_t *cursor = ctxt->cursor;

if (ds_write(dstfile, cursor->buf, cursor->buf_read)) {
return(FALSE);
}

return(TRUE);
}

全量备份每次读取量等于每次的写入量,读写操作比较均衡。

这段代码是纯C语言代码,使用了一些函数指针做了一些抽象处理,使得datasink结构体中的一些函数指针的最终指向不好确定。经过一番浏览,我们使用的本地备份,函数ds_write的最终实现在文件ds_local.c#L122(不同的备份方式会有不同的实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
static
int
local_write(ds_file_t *file, const void *buf, size_t len)
{
File fd = ((ds_local_file_t *) file->ptr)->fd;

if (!my_write(fd, buf, len, MYF(MY_WME | MY_NABP))) {
posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED);
return 0;
}

return 1;
}

my_write函数是写入文件的最终实现函数。ctag可以正常跳转,代码就不再贴出来了,都是底层的IO实现。

总结

之所以增量备份造成的READ IO过高,是因为增量备份每次读取640个page, 每次写入操作的buffer是4096个page, 多次读操作才会填满一个写入buffer,触发一次写操作,这造成了读操作更加密集。而全量备份每次读取的量和写入的量相等,这样读写比较均衡。

(注:以上代码来自v2.4.13)