背景
先前 PG 版本 pg_basebackup 的代码较为复杂,pg_basebackup 在备份过程中做了很多事情,但这部分代码逻辑没有完全解耦,导致一个文件里包含了很多功能的逻辑,影响了代码的可读性和可修改性。
因此,PG 15 针对这部分代码做了一个重构,在保证提供的功能没有发生变化的前提下,完成了模块间的解耦。
本文将针对重构这部分代码进行解析,建议先阅读下面 背景知识介绍 对应的文章。
- 背景知识介绍:PostgreSQL物理备份内部原理
- pg_basebackup 文档:https://www.postgresql.org/docs/15/app-pgbasebackup.html
- 社区重构 commit:https://github.com/postgres/postgres/commit/23a1c6578c87fca0e361c4f5f9a07df5ae1f9858
- 本文 PG 代码版本:15.1
整体架构
PG 15 的 pg_basebackup 部分采用 管道-过滤器 风格(数据流风格的一种),引入 bbstreamer
这个抽象结构,所有的过滤器都是 bbstreamer
的扩展。
举一个简单的例子,使用下述语句将 server 端的数据进行备份,并将输出重定向到标准输出。
pg_basebackup -h localhost -U postgres -p 5432 -Xf -Ft -D-
那么,整个数据流的处理如下图所示
执行 pg_basebackup 后,server 端会发送若干个 tar 包给 pg_basebackup 对应的进程,对于每一个 tar 包,pg_basebackup 进程都将其视作数据流处理,并让数据流经过若干个过滤器进行处理:
- bbstreamer_tar_parser:负责解析 server 端发来的 tar 文件,并将处理后的数据流向下一个过滤器;
- bbstreamer_tar_archiver: 负责组装 tar 包操作,对数据进行进一步处理并将其流向下一个过滤器;
- bbstreamer_tar_file:负责最终的文件写入操作,将数据流写入指定的文件中。
代码分析
bbstreamer 结构分析
bbstreamer 结构的详细内容参考 bbstreamer.h
文件,里面解释非常全面。这里介绍两个主要结构体
struct bbstreamer
{
const bbstreamer_ops *bbs_ops; /* 当前 bbstreamer 结构的函数指针 */
bbstreamer *bbs_next; /* 指向下一个 bbstreamer 结构 */
StringInfoData bbs_buffer; /* 临时 buffer */
};
struct bbstreamer_ops
{
/* 函数指针:实现 bbstreamer 过滤器需要针对数据流的具体操作 */
void (*content) (bbstreamer *streamer, bbstreamer_member *member,
const char *data, int len,
bbstreamer_archive_context context);
/* 函数指针:bbstreamer 清理函数 */
void (*finalize) (bbstreamer *streamer);
void (*free) (bbstreamer *streamer);
};
用面向对象的角度来看,bbstreamer
结构体可以看成基类,上文说的类似 bbstreamer_tar_parser
都可以看成它的子类,尤其扩展而来,并实现 bbstreamer_ops
里的三个函数。
处理流程
筛选出 pg_basebackup 中的关键函数后,整体的函数流程图如下所示
- 用户使用 pg_basebackup 后,经过初始化处理进入
BaseBackup
函数; - 如果 server 端为 PG 15 版本,调用
ReceiveArchieveStream
函数接收数据流; ReceiveCopyData
函数循环接收数据,并调用回调函数ReceiveArchieveStreamChunk
进行处理;ReceiveArchieveStreamChunk
函数负责一系列 bbstreamer 结构的初始化,以及使用 bbstreamer 结构体进行后续处理;- 处理完毕后,针对各 streamer 进行清理操作;
对于第 4 步,我们还是使用之前的例子来做具体分析
pg_basebackup -h localhost -U postgres -p 5432 -Xf -Ft -D-
其入口函数就是下面这个 bbstreamer_content
函数,该函数会调用当前 bbstreamer 对应实现的 content
函数。
/* Send some content to a bbstreamer. */
static inline void
bbstreamer_content(bbstreamer *streamer, bbstreamer_member *member,
const char *data, int len,
bbstreamer_archive_context context)
{
Assert(streamer != NULL);
streamer->bbs_ops->content(streamer, member, data, len, context);
}
对于上面的 pg_basebackup 命令,当到了最后一个过滤器 bbstreamer_plain_writer
时,会将内容写到 base.tar 中,此时的函数堆栈如下
(gdb) bt
#0 bbstreamer_plain_writer_content ()
at bbstreamer_file.c:110
#1 0x0000000000409c94 in bbstreamer_content ()
at bbstreamer.h:131
#2 0x000000000040a774 in bbstreamer_tar_archiver_content ()
at bbstreamer_tar.c:437
#3 0x0000000000409c94 in bbstreamer_content ()
at bbstreamer.h:131
#4 0x000000000040a47d in bbstreamer_tar_header ()
at bbstreamer_tar.c:310
#5 0x0000000000409f5f in bbstreamer_tar_parser_content ()
at bbstreamer_tar.c:142
#6 0x0000000000403607 in bbstreamer_content ()
at bbstreamer.h:131
#7 0x0000000000405de8 in ReceiveArchiveStreamChunk ()
at pg_basebackup.c:1449
下面省略若干行上层函数堆栈
可以看到,对于三个过滤器,每个过滤器执行完当前 bbstreamer 对应的函数后,都会继续调用下一个过滤器对应的 bbstreamer 函数,直到 bbstreamer_plain_writer_content
完成写 tar 文件,不再递归调用。
总结
PG 15 针对 pg_basebackup 这部分的重构,较好的完成了模块间的解耦。如果需要针对数据流做额外的修改操作,只需要按照其设计模式新增一个过滤器即可。例如不想直接写 tar 文件,想使用 gzip 额外进行压缩,只需要将最后的 bbstreamer_plain_writer
换成 bbstreamer_gzip_writer
即可。开发者在熟悉了这一架构后,可以非常快的进行开发。
参考资料
[1] http://mysql.taobao.org/monthly/2018/08/06/
[2] https://www.postgresql.org/docs/15/app-pgbasebackup.html
[3] https://github.com/postgres/postgres/commit/23a1c6578c87fca0e361c4f5f9a07df5ae1f9858