pgcopydb: 并发设计

四月 3, 2024

摘要:本节文档介绍pgcopydb程序中的并发设计。

本文包含以下部分:

  1. 关于并发的注意事项
  2. 对于每个表,同时构建所有索引
  3. 同一表上的并发

开发pgcopydb的主要原因是为了允许两个方面,这两个方面是不可能直接用pg_dumppg_restore实现的,并且需要做大量的工作,没有多少脚本可以自动化。

关于并发的注意事项

pgcopydb 通过使用fork()系统调用的方式,也实现了许多操作的并发。这意味着 pgcopydb 会创建子进程,每个子进程处理一部分工作。

进程树会像下面这样:

$ pgcopydb clone --follow --table-jobs 4 --index-jobs 4 --large-objects-jobs 4
 + pgcopydb clone worker
    + pgcopydb copy supervisor [ --table-jobs 4 ]
      - pgcopydb copy queue worker
      - pgcopydb copy worker
      - pgcopydb copy worker
      - pgcopydb copy worker
      - pgcopydb copy worker

    + pgcopydb blob metadata worker [ --large-objects-jobs 4 ]
      - pgcopydb blob data worker
      - pgcopydb blob data worker
      - pgcopydb blob data worker
      - pgcopydb blob data worker

    + pgcopydb index supervisor [ --index-jobs 4 ]
      - pgcopydb index/constraints worker
      - pgcopydb index/constraints worker
      - pgcopydb index/constraints worker
      - pgcopydb index/constraints worker

    + pgcopydb vacuum supervisor [ --table-jobs 4 ]
      - pgcopydb vacuum analyze worker
      - pgcopydb vacuum analyze worker
      - pgcopydb vacuum analyze worker
      - pgcopydb vacuum analyze worker

    + pgcopydb sequences reset worker

 + pgcopydb follow worker [ --follow ]
   - pgcopydb stream receive
   - pgcopydb stream transform
   - pgcopydb stream catchup

我们看到,当使用pgcopydb clone --follow --table-jobs 4 --index-jobs 4 --large-objects-jobs 4时,pgcopydb 创建了 27 个子进程。

总数 27 个是这样计算的:

  • 1 个克隆工作进程 + 1 个复制管理进程 + 1 个复制队列工作进程 + 4 个复制工作进程 + 1 个 blob 元数据工作进程 + 4 个 blob 数据工作进程 + 1 个索引管理进程 + 4 个索引工作进程 + 1 个 vacuum 管理进程 + 4 个 vacuum 工作进程 + 1 个序列重置工作进程

    即 1 + 1 + 1 + 4 + 1 + 4 + 1 + 4 + 1 + 4 + 1 = 23

  • 1 个跟随工作进程 + 1 个流接收进程 + 1 个流转换进程 + 1 个流追赶进程

    即 1 + 1 + 1 + 1 = 4

  • 总共是 23 + 4 = 27

下面是进程树的描述:

  • 在开始复制 TABLE DATA 时,pgcopydb 会创建由--table-jobs命令行选项(或环境变量PGCOPYDB_TABLE_JOBS)指定数目的子进程,并创建一个额外的进程来将表发送到队列,并为分段 COPY 的表处理 TRUNCATE 命令。

  • pgcopydb 创建了一个子进程,将源数据库中的 PostgreSQL 大对象(BLOB)元数据复制到目标数据库,并启动多达--large-objects-jobs个进程来复制大对象数据。

  • 为了驱动在目标数据库上构建索引和约束,pgcopydb 会创建由--index-jobs命令行选项(或环境变量PGCOPYDB_INDEX_JOBS)指定数目的子进程。

    PostgreSQL 可以在同一个表上并行创建多个索引,为此,客户端只需要为每个索引打开一个单独的数据库连接,并同时在自己的连接中运行各自的 CREATE INDEX 命令。在 pgcopydb 中,这是通过为每个索引运行一个子进程来实现的。

    --index-jobs选项对于整个操作来说是全局的,这样可以轻松地将其设置为目标 PostgreSQL 实例上的可用 CPU 核心数。通常,一个给定的 CREATE INDEX 命令会 100% 占用单个核。

  • 为了驱动在目标数据库上运行 VACUUM ANALYZE 工作,pgcopydb 会创建由--table-jobs命令行选项指定数目的子进程。

  • 为了在复制表数据的同时并行重置序列,pgcopydb 会创建一个专用的子进程。

  • 当使用--follow选项时,会创建另一个子进程的领导者,来处理三个数据变更捕获进程。

    • 一个进程实现pgcopydb stream receive,来以 JSON 格式获取更改,并将其预取到 JSON 文件中。
    • 一旦 JSON 文件完成,pgcopydb 流转换工作进程就将 JSON 文件转换为 SQL,就像调用了命令pgcopydb stream transform一样。
    • 另一个进程实现pgcopydb stream catchup,以将 SQL 更改应用到目标 PostgreSQL 实例。这个进程会循环查询 pgcopydb 哨兵表,直到应用模式被启用,然后循环查询 SQL 文件,并从这些文件运行查询。

对于每个表,同时构建所有索引

pgcopydb 采取了额外的步骤,并确保并行创建所有索引,当涉及到与约束关联的索引时,会更进一步。

PostgreSQL 在很久以前的 8.3 版本中引入了配置参数 synchronize_seqscans。默认情况下,它处于打开状态,并允许以下行为:

来自 PostgreSQL 文档

这允许大表的顺序扫描相互同步,以便并发的扫描在大约相同的时间读取同一块,从而共享 I/O 工作负载。

pg_dumppg_restore不太好用的另一个方面是,它们如何处理用于支持约束的索引,特别是唯一性约束和主键。

这些索引直接使用ALTER TABLE命令导出。这很好,因为该命令会同时创建约束和基础索引,所以最终可以按预期找到模式。

也就是说,这些ALTER TABLE ... ADD CONSTRAINT命令需要一定级别的锁定,以防止任何并发。正如我们在 ALTER TABLE的文档中所读到的:

来自 PostgreSQL 文档

尽管大多数形式的 ADD table_constraint 都需要 ACCESS EXCLUSIVE 锁,但 ADD FOREIGN KEY 只需要 SHARE ROW EXCLUSIVE 锁。请注意,除了在声明约束的表上加锁外,ADD FOREIGN KEY 还会在引用的表上获取 SHARE ROW EXCLUSIVE 锁。

采用的技巧是,首先发出一个CREATE UNIQUE INDEX语句,当索引构建完成后,再发出第二个命令,格式为ALTER TABLE ... ADD CONSTRAINT ... PRIMARY KEY USING INDEX ...,如下面的例子所示,该例子摘自实际运行pgcopydb的日志:

21:52:06 68898 INFO  COPY "demo"."tracking";
21:52:06 68899 INFO  COPY "demo"."client";
21:52:06 68899 INFO  Creating 2 indexes for table "demo"."client"
21:52:06 68906 INFO  CREATE UNIQUE INDEX client_pkey ON demo.client USING btree (client);
21:52:06 68907 INFO  CREATE UNIQUE INDEX client_pid_key ON demo.client USING btree (pid);
21:52:06 68898 INFO  Creating 1 indexes for table "demo"."tracking"
21:52:06 68908 INFO  CREATE UNIQUE INDEX tracking_pkey ON demo.tracking USING btree (client, ts);
21:52:06 68907 INFO  ALTER TABLE "demo"."client" ADD CONSTRAINT "client_pid_key" UNIQUE USING INDEX "client_pid_key";
21:52:06 68906 INFO  ALTER TABLE "demo"."client" ADD CONSTRAINT "client_pkey" PRIMARY KEY USING INDEX "client_pkey";
21:52:06 68908 INFO  ALTER TABLE "demo"."tracking" ADD CONSTRAINT "tracking_pkey" PRIMARY KEY USING INDEX "tracking_pkey";

这个技巧本身会获得大量的性能提升,正如 pgloader 用户已经发现、体验和欣赏的那样。

同一表上的并发

在某些数据库模式设计中,磁盘上的大部分数据库空间占用,都在单个巨型表或少数的巨型表上面。当这种情况发生时,使用--table-jobs实现的并发模型,仍然只会分配一个进程来复制源表中的所有数据。

同表并发允许 pgcopydb 同时使用多个进程来处理单个源表。然后,数据被动态逻辑分区,并在进程之间拆分:

  • 要从源数据库获取数据,COPY 进程会使用 SELECT 查询,如以下示例所示:

    COPY (SELECT * FROM source.table WHERE id BETWEEN      1 AND 123456)
    COPY (SELECT * FROM source.table WHERE id BETWEEN 123457 AND 234567)
    COPY (SELECT * FROM source.table WHERE id BETWEEN 234568 AND 345678)
    ...
    

    只有当源表中至少有一个整数类型的列(支持int2int4int8),并且具有 UNIQUE 或 PRIMARY KEY 约束时,才可能这样做。我们必须确保任何给定的行只被选中一次,以避免在目标数据库中出现重复记录。

    当一个表缺少这样一个整数数据类型的主键列时,pgcopydb 会自动使用基于 CTID 的比较。有关 PostgreSQL CTID 的更多信息,请参见 PostgreSQL 文档中关于系统列的部分

    然后,COPY 进程会使用如下面示例所示的 SELECT 查询:

    COPY (SELECT * FROM source.table WHERE ctid >= '(0,0)'::tid and ctid < '(5925,0)'::tid)
    COPY (SELECT * FROM source.table WHERE ctid >= '(5925,0)'::tid and ctid < '(11850,0)'::tid)
    COPY (SELECT * FROM source.table WHERE ctid >= '(11850,0)'::tid and ctid < '(17775,0)'::tid)
    COPY (SELECT * FROM source.table WHERE ctid >= '(17775,0)'::tid and ctid < '(23698,0)'::tid)
    COPY (SELECT * FROM source.table WHERE ctid >= '(23698,0)'::tid)
    
  • 要决定是否应该拆分表进行复制,可以使用命令行选项split-tables-larger-than或环境变量PGCOPYDB_SPLIT_TABLES_LARGER_THAN

    选项值可以是一个普通的字节数,也可以是一个简单易记的字节数,如250 GB

    当使用此选项时,那么至少具有此数据量以及有用于复制拆分的候选键的表,就可以分布到多个复制进程中。

    复制进程的数量,是通过将表大小除以使用拆分选项设置的阈值来计算的。例如,如果阈值是 250 GB,那么 400 GB 的表会分布到 2 个复制进程中。

    命令pgcopydb list table-parts,可用于列出 pgcopydb 在给定源表和阈值的情况下计算出的复制分区。

在同一表上并发 COPY 时的显著差异

当源表发生同表并发时,某些操作会和没有同表并发时实现不一样。具体说来:

  • TRUNCATE 和 COPY FREEZE 的优化

    使用单个复制进程时,可以在与 COPY 命令相同的事务中截取目标表,如以下语法样例所示:

    BEGIN;
    TRUNCATE table ONLY;
    COPY table FROM stdin WITH (FREEZE);
    COMMIT
    

    这种技术允许 PostgreSQL 实现多项优化,在 COPY 期间执行工作,否则在执行表上的第一个查询时,需要稍后进行。

    使用同表并发时,目标系统上会同时发生多个事务,这些事务用于从源表复制数据。这意味着我们必须单独进行 TRUNCATE,并且不能使用 FREEZE 选项。

  • CREATE INDEX 和 VACUUM

    即使启用了同表 COPY 并发,也只有在复制了整个数据集后,才会在目标系统上创建索引。这意味着只有当最后一个进程完成 COPY 后,此进程才会创建索引和执行 vacuum analyze 操作。

同一表上并发 COPY 的性能限制

最后,在某些场景中,同表并发可能根本没有效果。以下是选择使用此功能时,要注意的一些限制:

  • 网络带宽

    与数据库迁移相关的最常见性能瓶颈是网络带宽。当带宽饱和(完全使用)时,同表并发将不会提供任何性能优势。

  • 磁盘 IOPS

    与数据库迁移相关的第二大性能瓶颈是磁盘 IOPS,在云上是突增的容量。当磁盘带宽被完全使用时,同表并发将不会提供任何性能优势。

    源数据库系统要使用读取 IOPS,目标数据库系统会同时使用读和写的 IOPS(将数据写入复制到磁盘,创建索引时既要从磁盘读取表数据,又会将索引数据写入磁盘)。

  • 磁盘数据组织

    使用单个 COPY 进程时,目标系统可能会以聚集方式填充 PostgreSQL 表,在按顺序分配下一个磁盘页之前完整使用每个磁盘页。

    使用同表 COPY 并发时,目标 PostgreSQL 系统需要处理对同一表的并发写入,可能会导致磁盘使用效率降低。

    这会如何影响您的应用程序性能,有待测试。

  • synchronize_seqscans

    PostgreSQL 早在 8.3 版本中就实现了这个选项。该选项现在记录在版本和平台兼容性部分中。

    文档内容如下:

    这允许大表的顺序扫描相互同步,以便并发扫描在大约相同的时间读取同一块,从而共享 I/O 工作负载。

    并发 COPY 进程同时读取同一源表,对性能的影响需要评估。目前,pgcopydb 中没有选项可用于,在使用同表 COPY 并发时将 synchronize_seqscans 设置为关闭。

    使用您常用的 PostgreSQL 配置编辑进行测试。

了解更多

pgcopydb: 复制 PostgreSQL 数据库到目标服务器