pg-osc: 在 PostgreSQL 中以零停机的模式更改表结构

七月 9, 2024

摘要pg-osc是一个简单的命令行工具,用于在 PostgreSQL 中以零停机的模式更改表结构和回填数据。

目录

介绍

pg-online-schema-change (pg-osc) 是一个工具,用于以最小的锁在 PostgreSQL 表中进行模式更改(任何ALTER语句),以帮助实现在生产环境工作负载下进行零停机的模式更改。

pg-osc使用了影子表的概念来执行模式更改。在较高级别上,它会创建一个在结构上与主表相同的影子表,对影子表执行模式更改,将内容从主表复制到影子表,并在最后交换表名称,同时使用触发器(通过审计表)保留对主表的所有更改。

pg-osc受到了pt-online-schema-change (MySQL) 和pg_repack等工具的设计和工作原理的启发。可在下面阅读更多内容,关于它是如何工作的亮点特性注意事项示例

用法

pg-online-schema-change help perform

Usage:
  pg-online-schema-change perform -a, --alter-statement=ALTER_STATEMENT -d, --dbname=DBNAME -h, --host=HOST -p, --port=N -s, --schema=SCHEMA -u, --username=USERNAME

Options:
  -a, --alter-statement=ALTER_STATEMENT # The ALTER statement to perform the schema change
  -s, --schema=SCHEMA                   # The schema in which the table is. Default: public
  -d, --dbname=DBNAME                   # Name of the database
  -h, --host=HOST                       # Server host where the Database is located
  -u, --username=USERNAME               # Username for the Database
  -p, --port=N                          # Port for the Database. Default: 5432
  -w, [--password=PASSWORD]             # DEPRECATED: Password for the Database. Please pass PGPASSWORD environment variable instead.
  -v, [--verbose], [--no-verbose]       # Emit logs in debug mode
  -f, [--drop], [--no-drop]             # Drop the original table in the end after the swap
  -k, [--kill-backends], [--no-kill-backends] # Kill other competing queries/backends when trying to acquire lock for the shadow table creation and swap. It will wait for --wait-time-for-lock duration before killing backends and try upto 3 times.
  -w, [--wait-time-for-lock=N]          # Time to wait before killing backends to acquire lock and/or retrying upto 3 times. Default: 10. It will kill backends if --kill-backends is true, otherwise try upto 3 times and exit if it cannot acquire a lock.
  -c, [--copy-statement=COPY_STATEMENT] # Takes a .sql file location where you can provide a custom query to be played (ex: backfills) when pgosc copies data from the primary to the shadow table. More examples in README.
  -b, [--pull-batch-count=N]            # Number of rows to be replayed on each iteration after copy. Default: 1000. This can be tuned for faster catch up and swap. Best used with delta-count.
  -e, [--delta-count=N]                 # Indicates how many rows should be remaining before a swap should be performed. Default: 20. This can be tuned for faster catch up and swap, especially on highly volume tables. Best used with pull-batch-count.
  -o, [--skip-foreign-key-validation], [--no-skip-foreign-key-validation] # Skip foreign key validation after swap. You shouldn't need this unless you have a very specific use case, like manually validating foreign key constraints after swap.
Usage:
  pg-online-schema-change --version, -v

print the version

亮点特性

  • pg-osc支持在添加、删除或重命名列时运行,而不会丢失数据。
  • pg-osc在整个过程中只获取最少的锁(阅读下面的警告)。
  • 复制索引和外键。
  • 可以在最后删除或保留旧表(可选)。
  • 减少膨胀(因为 pg-osc 会创建一个新表,并在交换后删除旧表)。
  • 调整审计/日志表中重放速度的快慢(重放更大的工作负载)。
  • 在将数据从主表复制到影子表时,回填旧列/新列,然后执行交换。示例

示例

重命名列

export PGPASSWORD=""
pg-online-schema-change perform \
  --alter-statement 'ALTER TABLE books RENAME COLUMN email TO new_email' \
  --dbname "postgres" \
  --host "localhost" \
  --username "jamesbond"

多个 ALTER 语句

export PGPASSWORD=""
pg-online-schema-change perform \
  --alter-statement 'ALTER TABLE books ADD COLUMN "purchased" BOOLEAN DEFAULT FALSE; ALTER TABLE books RENAME COLUMN email TO new_email;' \
  --dbname "postgres" \
  --host "localhost" \
  --username "jamesbond" \
  --drop

5 秒后杀死其他后端

如果操作是在一个繁忙的表上执行的,则可以使用pg-osckill-backend功能,来终止可能与pg-osc操作竞争的其他后端,以便在短时间内获取锁。pg-osc获得的ACCESS EXCLUSIVE锁只持有一小段时间,然后就释放了。您可以调整在杀死其他后端之前pg-osc应该等待多长时间(或者pg-osc是否应该在一开始就杀死后端)。

export PGPASSWORD=""
pg-online-schema-change perform \
  --alter-statement 'ALTER TABLE books ADD COLUMN "purchased" BOOLEAN DEFAULT FALSE;' \
  --dbname "postgres" \
  --host "localhost" \
  --username "jamesbond" \
  --wait-time-for-lock 5 \
  --kill-backends \
  --drop

重放更大的工作负载

如果表的写入吞吐量较高,则默认的重放速度可能不够用。也就是说,您可能会看到pg-osc从审计表中一次性重放 1000 行(pull-batch-count)。pg-osc还会等到审计表中的剩余行数(delta-count)为 20 后再进行交换。您可以将这些值调整为更高的值,以便更快地赶上此类工作负载。

export PGPASSWORD=""
pg-online-schema-change perform \
  --alter-statement 'ALTER TABLE books ADD COLUMN "purchased" BOOLEAN DEFAULT FALSE;' \
  --dbname "postgres" \
  --host "localhost" \
  --username "jamesbond" \
  --pull-batch-count 2000
  --delta-count 500
  --wait-time-for-lock 5 \
  --kill-backends \
  --drop

回填数据

在将数据插入到影子表时,您可以传入自定义 sql 文件,来执行复制和任何其他工作,而不仅仅是从主表复制所有列和行。例如:回填某些列。通过提供copy-statementpg-osc将改为运行查询以执行复制操作。

重要提示:

  • 可能会意外违反约束或不复制数据,因此请谨慎操作
    • 在自定义 SQL 中连接时,必须使用 OUTER JOIN,否则会丢失与连接表不匹配的行。
  • ALTER语句可能会更改表的结构,因此请谨慎操作
  • 保留%{shadow_table},因为它会替换为影子表的目标。
  • 强烈建议用户在生产环境上使用之前,先测试和验证结果!
-- file: /src/query.sql
INSERT INTO %{shadow_table}(foo, bar, baz, rental_id, tenant_id)
SELECT a.foo,a.bar,a.baz,a.rental_id,r.tenant_id AS tenant_id
FROM ONLY examples a
LEFT OUTER JOIN rentals r
ON a.rental_id = r.id
pg-online-schema-change perform \
  --alter-statement 'ALTER TABLE books ADD COLUMN "tenant_id" VARCHAR;' \
  --dbname "postgres" \
  --host "localhost" \
  --username "jamesbond" \
  --copy-statement "/src/query.sql" \
  --drop

使用 Docker 运行

docker run --network host -it --rm shayonj/pg-osc:latest \
    pg-online-schema-change perform \
    --alter-statement 'ALTER TABLE books ADD COLUMN "purchased" BOOLEAN DEFAULT FALSE; ALTER TABLE books RENAME COLUMN email TO new_email;' \
    --dbname "postgres" \
    --host "localhost" \
    --username "jamesbond" \
    --drop

注意事项

  • 目前尚不支持分区表。欢迎提供 PR 请求和想法。
  • 表上应存在主键;没有的话,pg-osc会引发异常
    • 这是因为,目前没有其他方法可以在重放期间唯一标识行。
  • pg-osc会在操作期间两次获取父表上的ACCESS EXCLUSIVE锁。
    • 第一次,在设置触发器和影子表时。
    • 下一次,在执行交换和更新外键引用时。
    • 注意:如果指定了kill-backends,它会尝试终止在这两个时间内的任何竞争操作。
  • 根据设计,pg-osc不会终止正在执行的任何其他 DDL。在操作过程中,最好不要对父表运行任何 DDL。
  • 由于复制表的性质,磁盘上需要有足够的空间来支持该操作。
  • 索引、约束和序列名称会被更改,并失去其原始命名。
    • 可以在将来的版本中修复。如有需要,可创建特性请求。
  • 外键会被删除,并重新以NOT VALID的方式添加到引用表。接着运行VALIDATE CONSTRAINT操作。
    • 要确保保持完整性,并在重新引入外键时不用获取额外的锁,因此才会使用NOT VALID

它是如何工作的

此工具中有 3 种类型的表:

  • 主表:可能要对其运行模式更改的表
  • 影子表:现有主表的副本表
  • 审计表:用于存储主表上的任何更新/插入/删除的表

how-it-works

1. 创建一个审计表,以记录对父表所做的更改。

2. 获取一个简短的ACCESS EXCLUSIVE锁,以在父表上添加触发器,记录插入、更新、删除操作到审计表。

3. 创建一个新的影子表,并在影子表上运行 ALTER 或迁移。

4. 复制旧表中的所有行。

5. 在新表上构建索引。

6. 将审计表中累积的所有更改重放到影子表中。

  • 在审计审计表中的行时,删除这些行。

7. 一旦差量行数(剩余行数)到 ~20,则在事务中获取父表上的ACCESS EXCLUSIVE锁,然后:

  • 交换表名称(影子表 <> 父表)。
  • 删除外键,并重新以NOT VALID的方式创建,以更新其他表中的外键引用。

8. 在新表上运行ANALYZE

9. 验证所有添加的NOT VALID的外键。

10. 删除父表(现在是旧表)(可选)。

了解更多

pg-osc 项目