pgstream: 使用 WAL 日志实现逻辑复制 DDL 事件

John Doe 三月 10, 2026

最近 pgstream 针对 DDL 事件的逻辑复制,进行了一次重要的重新设计实现。

image

旧方案

旧版 pgstream 依靠自定义事件触发器 + Postgres 内置函数来识别 DDL 变更:

  • pg_event_trigger_dropped_objects 处理 DROP 命令
  • pg_event_trigger_ddl_commands 处理其他所有 DDL

该触发器会:

  1. 查询 Postgres 系统表
  2. 重建一份不完整的元数据视图
  3. 将该视图写入 pgstream.schema_log

这行 schema_log 记录会和普通表数据一样,以 WAL 日志的形式被复制。

这套设计的一个关键优点是:元数据变更与数据变更的顺序完全一致,因为所有变更都走 WAL。

但它也带来了明显缺点:

  • 元数据视图需要手动维护,并与 Postgres 内部结构保持同步

  • 只能维护部分元数据,因为是从系统表反推,而非直接来自 DDL 本身,仅支持:

    • 表信息(列、索引、序列)
    • 物化视图
  • 每次元数据变更都多一层间接流程:DDL → 元数据视图 → 表记录 → WAL → 下游

  • 元数据差异必须通过对比多条 schema_log 记录计算得出

简单说:旧方案能用,但复杂且脆弱。

pgstream.schema_log 表示例

下面是 pgstream.schema_log 表中单行数据的示例。

每一行代表某个时间点的部分元数据的快照。

元数据行

id          | d61ibsq380kg0jqp3g90
version     | 1
schema_name | public
created_at  | 2026-02-04 10:56:51.39665
acked       | True

元数据内容(JSONB 格式)

{
  "tables"; [
    {
      "oid": "20846",
      "name": "test",
      "pgstream_id": "d61ibsq380kg0jqp3g9g",
      "primary_key_columns": ["id"],
      "columns": [
        {
          "name": "id",
          "type": "bigint",
          "nullable": false,
          "unique": true,
          "identity": "a",
          "generated": false,
          "pgstream_id": "d61ibsq380kg0jqp3g9g-1"
        },
        {
          "name": "name",
          "type": "text",
          "nullable": true,
          "generated": false,
          "pgstream_id": "d61ibsq380kg0jqp3g9g-2"
        },
        {
          "name": "count",
          "type": "integer",
          "nullable": false,
          "default": "nextval('public.test_count_seq'::regclass)",
          "generated": false,
          "pgstream_id": "d61ibsq380kg0jqp3g9g-4"
        }
      ],
      "indexes": [
        {
          "name": "test_pkey",
          "unique": true,
          "columns": ["id"],
          "definition": "CREATE UNIQUE INDEX test_pkey ON public.test USING btree (id)"
        }
      ]
    }
  ],
  "sequences"; [
    {
      "oid": "21439",
      "name": "test_count_seq",
      "data_type": "integer",
      "increment": "1",
      "start_value": "1",
      "minimum_value": "1",
      "maximum_value": "2147483647",
      "cycle_option": "NO"
    }
  ],
  "materialized_views"; []
}

新方案

在 pgstream v1.0.0 中,中间层被彻底移除。

不再把元数据状态物化到表里,新版流程是:

  1. 仍使用自定义事件触发器捕获 DDL(函数与旧版相同)
  2. 直接构建包含 DDL 语句与上下文的消息
  3. 通过pg_logical_emit_message直接写入 WAL
  4. 下游直接从事件解析变更

结果:

  • 没有schema_log
  • 没有需要重建的元数据视图
  • 不需要在源库维护元数据状态

image

为什么这很重要

这次架构升级带来了几个关键变化:

  1. 所有 DDL 都会被复制,不再只是精选的部分信息

    对 Postgres 目标端尤其有价值,可以完整、忠实地重放 DDL。

  2. 无需维护元数据视图

    pgstream 不再需要镜像 Postgres 系统表结构,也不用追踪元数据形态变化。

  3. 中间层更少,故障点更少

    元数据变更链路:DDL 执行WAL下游

  4. 元数据复制从 “状态驱动” 变为 “事件驱动”

    事实来源是 DDL 本身,而非推导出来的表示。

  5. 基于元数据的增量处理,无需状态对比

    DDL 语句本身就是元数据增量(例如 ALTER TABLE … ADD COLUMN),无需计算差异。

简单说:pgstream 不再尝试重建元数据变更,而是直接把变更当作事件原样复制。元数据演进变成完全声明式、事件驱动。

参考

pgstream:https://github.com/xataio/pgstream