背景

在常见的PostgreSQL双节点高可用构架中,如果主库挂了且主备无延迟,高可用系统会提升老备库为新主库对外服务。而对于老主库,则可以有很多处理策略,例如:

  1. 删掉,重搭新备库。
  2. 降级为备库,继续服务。

很显然,相比来说第一种不是个很好的方案。当数据量比较大时,重搭备库的时间成本太高,系统的可用性降低。但是因为老的主库挂掉的原因多种多样,甚至有可能是高可用系统的误判,而老主库也有可能是在挂掉之后又重新作为主库启动起来,这个时候降级并重搭流复制关系的操作就有可能失败(新的备库比新主库数据更超前)。

为了解决这种情况,PostgreSQL 引入了pg_rewind工具。

功能介绍

PostgreSQL 官方文档的介绍中,pg_rewind 不光可以针对上面说的failover 的场景,理论上,它可以使基于同一集群复制的任意两个副本(集群)进行同步。为了更容易理解,本文把这里的源副本称为源集群,目的副本称为目的集群。

pg_rewind 工具主要实现了从源集群到目的集群的文件级别数据同步。但是和rsync的区别是,pg_rewind 不需要去读那些未变化的文件块,当数据量比较大而变化较小的时候,pg_rewind会更快。

值的注意的是,pg_rewind为了能够支持文件级别的数据同步,两个集群都打开如下参数:

  • 初始化集群的时候打开 data checksums 或者 wal_log_hints=on,参数说明详见文档
  • full_page_writes=on,参数说明详见文档

以上几个参数打开,才能够保证通过WAL 日志恢复出来的数据是完整的,一致的,从而才能够实现文件级别的数据同步。

具体实现

为了在PostgreSQL 实现文件级别数据同步的功能,pg_rewind 主要进行了如下的处理步骤:

  1. 在目的集群中找到源集群和目的集群的分叉点之前的最近一次checkpoint 点。这样相当于找到了在两个副本数据产生不同前的最后一个一致性位点。目的集群在这个位点之后所有的表数据变化都记录在这个位点之后的WAL 日志中。
  2. pg_rewind 会将目的集群这些变化的数据页从源集群复制过来。这里会有2种方式:
    • 使用文件系统方式拷贝
    • 使用 libpq 建立连接的方式拷贝 我们会在pg_rewind 的用法当中详细说明两者的区别。注意:这里拷贝的只是表数据文件,下文会具体将满足什么条件的才是表数据文件。
  3. 除了变化的数据块之外,拷贝其他所有的文件比如说pg_xact 文件和配置文件。
  4. 生成backup label 文件,指定恢复开始的WAL 位点为1步骤中获得的位点,目的集群启动时会从该点开始应用WAL 日志。
  5. 更新目的集群的pg_control文件。
  6. 使用initdb -S 把目的集群所有的修改刷到磁盘上去。

一句话概括,pg_rewind 可以快速找到两个集群数据开始分叉的点,然后找到目的集群从该点之后的数据变化,通过拷贝源集群的对应数据页,再通过应用源集群的WAL 日志达到数据一致。

不过这里有几个问题值得我们探究:

  • 如何判断两个集群来源于同一份数据副本?
  • 如何找到目的集群和源集群的分叉点?
  • 如何找到目的集群的数据变化?
  • 目的集群生成的label 文件的内容是什么?

接下来,我们将深入代码(代码分析基于10.0版本)分析这几个问题。

唯一集群系统标识

根据上文可知,基于同一集群的两个副本是可以执行pg_rewind的。而在PostgreSQL 中,使用pg_control 文件中的system_identifier 唯一标示一次PostgreSQL数据库的initdb 过程,其生成方式如下:

  1. gettimeofday(&tv, NULL);
  2. sysidentifier = ((uint64) tv.tv_sec) << 32;
  3. sysidentifier |= ((uint64) tv.tv_usec) << 12;
  4. sysidentifier |= getpid() & 0xFFF;

system_identifier 一旦生成就不会变化,而通过拷贝数据文件方式进行数据复制(包括pg_basebackup)的方法因为有相同的system_identifier,所以复制的源集群和目的集群被认为拥有相同的祖先集群,即理论上是可以使用pg_rewind 进行数据同步的。

PostgreSQL 提供了pg_controldata 工具,我们可以执行pg_controldata $PGDATA 命令解析PGDATA 下的pg_control 文件,返回结果如下:

  1. pg_control version number: 942
  2. Catalog version number: 201409291
  3. Database system identifier: 6537134048787931336
  4. Database cluster state: in production
  5. pg_control last modified: Sat 19 May 2018 06:36:03 PM CST
  6. Latest checkpoint location: 0/3A321900
  7. Prior checkpoint location: 0/3A3202D8
  8. Latest checkpoint's REDO location: 0/3A3218C8
  9. Latest checkpoint's REDO WAL file: 00000006000000000000003A
  10. Latest checkpoint's TimeLineID: 6
  11. Latest checkpoint's PrevTimeLineID: 6
  12. Latest checkpoint's full_page_writes: on
  13. Latest checkpoint's NextXID: 0/316602
  14. Latest checkpoint's NextOID: 32769
  15. Latest checkpoint's NextMultiXactId: 1
  16. Latest checkpoint's NextMultiOffset: 0
  17. Latest checkpoint's oldestXID: 1798
  18. Latest checkpoint's oldestXID's DB: 1
  19. Latest checkpoint's oldestActiveXID: 316602
  20. Latest checkpoint's oldestMultiXid: 1
  21. Latest checkpoint's oldestMulti's DB: 1
  22. Time of latest checkpoint: Sat 19 May 2018 06:36:03 PM CST
  23. Fake LSN counter for unlogged rels: 0/1
  24. Minimum recovery ending location: 0/0
  25. Min recovery ending loc's timeline: 0
  26. Backup start location: 0/0
  27. Backup end location: 0/0
  28. End-of-backup record required: no
  29. Current wal_level setting: hot_standby
  30. Current wal_log_hints setting: off
  31. Current max_connections setting: 2100
  32. Current max_worker_processes setting: 8
  33. Current max_prepared_xacts setting: 800
  34. Current max_locks_per_xact setting: 64
  35. Maximum data alignment: 8
  36. Database block size: 8192
  37. Blocks per segment of large relation: 131072
  38. WAL block size: 8192
  39. Bytes per WAL segment: 16777216
  40. Maximum length of identifiers: 64
  41. Maximum columns in an index: 32
  42. Maximum size of a TOAST chunk: 1996
  43. Size of a large-object chunk: 2048
  44. Date/time type storage: 64-bit integers
  45. Float4 argument passing: by value
  46. Float8 argument passing: by value
  47. Data page checksum version: 1

当然,在pg_rewind 的执行过程中,除了 判断源集群和目的集群的 system_identifier 一致性之外,还要判断:

  • pg_control version number 的一致性
  • Catalog version number 的一致性

其中,pg_control version number 标示pg_control 文件的文件结构版本,Catalog version number标示数据库系统表的结构版本。在PostgreSQL的版本迭代中,pg_control文件的结构和数据库系统表的结构可能有所不同,通过这两个version number 标示不同的结构版本。

目的集群和源集群的分叉点

在PostgreSQL,备库提升为主库时候会产生history 文件(关于备库提升为主库的过程这里不再详细介绍,后面的月报我们会具体分析)。而history 文件位于WAL 日志所在的目录$PGDATA/pg_wal(10.0之前的版本是pg_xlog),主要内容分为以下三列,每列之间用tab分割,比如00000006.history的具体内容如下所示:

  1. 1 0/3E40B00 no recovery target specified
  2. 2 0/72EA320 no recovery target specified
  3. 3 0/9321E28 no recovery target specified
  4. 4 0/1802D168 no recovery target specified
  5. 5 0/18063AF0 no recovery target specified

其中:

  • parentTLI 代表之前的时间线,关于时间线的分析可以参考之前的月报
  • switchpoint 代表这个时间线和下个时间线分割的WAL 日志位置
  • reason 代表时间线切换的原因

因为history 文件中记录了每次时间线的切换信息,PostgreSQL 通过遍历两个集群的history 文件中每一行,可以找到两者数据走向不同分支的timeline 和switchpoint。具体逻辑如下:

  1. 找到第一个Timeline不同或者switchpoint 不同的位置,这说明在这之前两者的数据还是一致的
  2. 找到1 的上一行记录的Timeline和两者中最小的switchpoint,这个位置两个集群的数据一定是一致的

通过第2步骤中找到的 一定是两个集群数据一致的位点,即前文中两个集群的分叉点。但是这个点的数据不能够保证数据的完整性和一致性,所以实际上pg_rewind 会以这个位点之前的最近的checkpoint 点作为两个集群的分叉点。

如何找到目的集群的数据变化

pg_rewind 会去遍历源集群和目的集群的目录,将每个文件(目录)的差异被记录在结构体 file_entry_t 中,其定义如下:

  1. struct file_entry_t
  2. {
  3. char *path;
  4. file_type_t type;
  5. file_action_t action;
  6. /* for a regular file */
  7. size_t oldsize;
  8. size_t newsize;
  9. bool isrelfile; /* is it a relation data file? */
  10. datapagemap_t pagemap;
  11. /* for a symlink */
  12. char *link_target;
  13. struct file_entry_t *next;
  14. };

其中,file_type_t type 代表文件类型有三种:

  • FILE_TYPE_REGULAR 常规文件
  • FILE_TYPE_DIRECTORY 目录
  • FILE_TYPE_SYMLINK 软链接

file_action_t action指示该文件(目录)对应的处理action,包括以下几种:

  • FILE_ACTION_CREATE 创建目录或者软链接
  • FILE_ACTION_COPY 复制整个文件或者重写已存在的文件
  • FILE_ACTION_COPY_TAIL 从目的集群文件尾复制源集群的增量数据
  • FILE_ACTION_NONE 无操作
  • FILE_ACTION_TRUNCATE 删除目的集群的文件,使目的集群文件和源集群文件大小相等
  • FILE_ACTION_REMOVE 删除本地文件/目录/软链接

isrefile 表示该文件是否是一个表数据文件,表数据文件的路径要满足以下几个条件:

  • global/ 目录下的文件,即数据库共享的表文件目录下的文件
  • base// 目录下的文件,即默认tablespace的表文件目录下的文件
  • pg_tblspc//PG_9.4_201403261/目录下的文件,即其他tablespace 的表文件目录下的文件,其中PG_9.4_201403261 与版本相关
  • .,文件名符合的格式

pagemap 存储了一个bitmap,每一位存储了对应的目的集群文件中的每个page 从两个集群的分叉点之后是否发生了变化,1代表发生变化,0代表未变化。

oldsize 代表目的集群该文件的大小,newsize 代表源集群该文件的大小。pg_rewind 中通过源集群和目的集群的对应文件大小比较或者文件(目录)是否存在,指定文件的处理action,例如:

  • oldsize > newsize? action=FILE_ACTION_TRUNCATE
  • oldsize < newsize? action=FILE_ACTION_COPY_TAIL
  • 如果文件不存在,则action=FILE_ACTION_COPY
  • 如果目录不存在,则action=FILE_ACTION_CREATE
  • 如果文件多余,则action=FILE_ACTION_REMOVE

之前说过,pg_rewind 工具执行需要打开full_page_writes,而打开了full_page_writes 之后,checkpoint 后每个数据页的第一次修改对应的数据页的全部内容都会写在WAL日志记录中,所以pg_rewind 可以根据WAL 日志的组织结构(详见之前的月报)很容易的找到对应已经修改的数据页信息,并把对应的file_entry_t 的bitmap 置为1。

至此,我们得到了所有目的集群需要变化的文件(目录)和对应action 以及目的集群数据文件变化的page。接下来,pg_rewind 会进行如下具体操作,完成了对目的集群所有文件的修改:

  1. 对所有的文件(目录)action 进行排序,主要是保证操作的正确性,比如先删除父目录,再删除子目录,先创建父目录,再创建子目录或者文件。
  2. 遍历所有文件(目录),循环执行3,4步骤。
  3. 根据文件的pagemap,将变化的page 从源集群拷贝到目的集群。
  4. 根据对应的action 进行相应的操作,具体操作见上文file_action_t action 的介绍。

其中,值得注意的是,第3步如果源集群没有对应的page 怎么办?经过分析代码,我们发现如果源集群没有对应的page,该page 不会被标记为发生变化的page,而该文件的action 会被置为FILE_ACTION_TRUNCATE,在第4步中将对应的文件进行相应的删减。

另外,使用文件的大小来进行数据变化的比较真的可行吗?会不会出现两个集群修改的数据是相同的,导致实际上文件大小是相同的,但是这时因为数据页头的pd_lsn 等信息可能是不同的,影响数据同步后的数据可见性。但是其实即使这种情况发生了,也对数据的同步没有影响,因为pg_rewind 结束之后,我们可以通过应用源集群的WAL 日志重新恢复该page。

可以看出,实际上只要保证目的集群的每个对应的表数据文件比源集群的表数据文件小,在pg_rewind 结束之后,这个表数据文件都可以通过应用源集群的WAL 日志恢复出相同的数据。

一句话概括,pg_rewind 通过文件的大小比较和目的集群的WAL 日志来确定哪些文件(目录)发生了变化,并使目的集群在pg_rewind 结束之后通过应用源集群的WAL 日志来完成所有的数据同步。这里需要注意的是,一定要保证目的集群从上文中两个集群的分叉点到现在的WAL 日志是连续的,没有被移除,否则在找两个集群的分叉点时就会报错。

生成的backup label内容

通过分析代码,pg_rewind 生成的backup label 的内容如下:

  1. len = snprintf(buf, sizeof(buf),
  2. "START WAL LOCATION: %X/%X (file %s)\n"
  3. "CHECKPOINT LOCATION: %X/%X\n"
  4. "BACKUP METHOD: pg_rewind\n"
  5. "BACKUP FROM: standby\n"
  6. "START TIME: %s\n",
  7. /* omit LABEL: line */
  8. (uint32) (startpoint >> 32), (uint32) startpoint, xlogfilename,
  9. (uint32) (checkpointloc >> 32), (uint32) checkpointloc,
  10. strfbuf);

而通过之前的月报分析可知,backup label 中的CHECKPOINT LOCATION 规定了在线恢复的起始位置,而pg_rewind 这个值存的就是上文中的两个集群的分叉点。而在pg_rewind 结束之后,目的集群就可以不断应用源集群从CHECKPOINT LOCATION 之后的WAL 日志,来完成两个集群的数据同步。

经过以上的分析,我们了解了pg_rewind 的一些具体实现,下面简单介绍下它的使用方法。

具体用法

pg_rewind 的具体用法如下:

  1. pg_rewind [option...] { -D | --target-pgdata } directory { --source-pgdata=directory | --source-server=connstr }

具体参数含义如下:

-D directory –target-pgdata=directory 定义目的集群的数据目录。注意:运行pg_rewind时候目的集群必须要停止。否则在执行 pg_rewind 时会报“target server must be shut down cleanly”错误。

–source-pgdata=directory 定义源集群的文件系统路径。这个参数生效时源集群必须已经停止了,否则会报“source data directory must be shut down cleanly”错误。

–source-server=connstr 定义连接到源集群的 libpq 连接串。这个连接必须是一个超级用户创建的普通连接,不是一个流复制的连接,关于PostgreSQL 的libpq的各种类型和分析,我们会在后面的月报进行代码层面的分析。当然这个参数要求源集群比较是健康的,可连接的。

-n –dry-run 增加该选项不去改变目的集群的数据目录,只是找到上文中两个集群的分叉点和生成label 文件等等。

-P –progress 增加该选项会粗略地显示整个过程的进度条。

–debug 增加该选项会显示pg_rewind 的debug 信息。

-V –version 显示pg_rewind 的版本号。

-? –help 显示help 文档。

特别注意:pg_rewind 不能使用root 用户运行,只能使用PostgreSQL superuser 用户运行。