修改触发器GDQ

来自 PostgreSQL wiki
跳转到导航跳转到搜索

通用修改触发器和广义数据队列规范

问题陈述

解决不同的问题

  • 像 Slony/Londiste 事务队列一样的复制
  • 复制到异构系统,可能是非事务性的
  • 物化视图 中的差异更新。

当前“队列表”实施的问题

  • 数据写入和查找/扫描开销。
    • 以及真空开销
  • 没有好的方法来包含事务性 DDL 更改
  • 大型数据的问题
  • 无法确保复制触发器最后被触发
  • “轮询”模型固有的延迟

“队列表”实施的好处

  • 可以确保只有已提交的数据被复制
  • 持久存储

实施建议

按 Itagaki 的说法

  1. 用于行修改的非表格存储
  2. 将项目发送到另一个服务器
  3. 实施非持久性选项(全局临时表)

按 Josh 的说法

  1. 基于当前约束触发器代码编写一个可延迟触发器。
  2. 研究用于行存储的紧凑格式(protobuf?)。
  3. 使用第三方队列 (AMQAMQPSpread?) 进行传输。
  4. 想出一种方法将 DDL 在适当的时候注入到队列中。
    建议(cbbrowne)... 这在两个方面似乎是一个正交问题
    1. 捕获 DDL 信息与捕获数据修改不同
    2. 捕获将 DDL 注入队列的时间点是独立的,也许可以使用以下组合
      1. 共享序列指示排序
      2. 提交时间戳信息(也可能是一种序列号)来控制对更改集之间的边界的访问

按 Marko 的说法

  1. 复制触发器不能最后触发,而应该在所有 BEFORE 触发器之后和所有(非复制)AFTER 触发器之前触发...
  2. DDL 事件定位是一个已解决的问题——通常的 AFTER 触发器“一致排序”逻辑也适用于此
    1. 启动事务。
    2. 执行 DDL,这将对适当的对象进行锁定。
    3. 从 seq 中获取事件编号——这里所有可能冲突的操作都在等待锁,因此还没有从 seq 中分配它们的事件 ID。
    4. 将事件插入队列。
    5. 提交,释放锁。
  3. 队列表效率似乎也是一个已解决的问题
    1. 使用高效的获取查询 (PgQ)
    2. 表格应该是只插入的,并且经常轮换——例如每 5-10 分钟——以使表格尽可能小。查看 PgQ 如何在没有锁定问题的情况下做到这一点。
      1. 最小的读取开销——活动数据在内存中。
      2. 最小的 VACUUM 开销。
      3. 最小的索引维护开销。

设计

触发器

修改触发器必须在所有 BEFORE 触发器之后和所有 AFTER 触发器之前触发。它们可以访问语句类型(INSERT/UPDATE/DELETE)和旧元组和新元组,但不能修改新元组,也不能取消语句,除非出现错误情况。

DDL 触发器在 独立部分 中进行了讨论。但是,我们可能在队列层处理 TRUNCATE,因为在某些情况下,TRUNCATE 之前的修改事件将被丢弃。

队列

我们有一些选择来记录队列中的修改;完整描述与仅主键。它们有权衡,因此我们最好有选择使用哪一种的选项。

完整描述队列
它们不仅包括修改元组的主键,还包括非键字段。它们被 SlonyLondiste 使用。
优点
  • 在回放时,我们可以避免对队列和原始表进行 JOIN。
  • 我们可以回放所有实际的操作。
缺点
  • 与仅 PK 队列相比,队列的大小将相对较大。
仅 PK 队列
它们只包括修改元组的主键。PK 的更新将以 DELETE 和 INSERT 的组合记录。它们被 Oracle 的物化视图日志使用。
优点
  • 与完整描述队列相比,队列的大小将相对较小。
  • 我们可以轻松地合并对相同元组的多个 UPDATE 操作。
缺点
  • 在回放时,我们需要对队列和原始表进行 JOIN 以检索实际数据。
  • 我们只能重建表的最终状态。过渡状态是不可能的。

实施

另请参阅 GDQ 实施 中的讨论。

队列可以使用标准表实现,Slony 和 PgQ 也使用它们,但我们可以使用另一种数据结构来实现它们,因为 GDQ 只需要 INSERT/SELECT/TRUNCATE。标准表的全部功能并不总是需要的。但是,队列的使用者(订阅者)并不总是以 FIFO 方式读取数据。一些使用者以随机顺序读取队列,因为它们需要基于快照的修改记录分组。

  • 队列表应该由多个只插入的表组成。我们以一定的周期轮换它们,并截断旧的日志记录以避免 DELETE 的开销。
  • 队列可以使用内存中或非 WAL 日志存储引擎实现。如果我们使用这种优化,复制服务器必须在主服务器崩溃后重新复制所有表内容。
  • 使用非表格存储来存储队列,比如一些 WAL 风格的平面文件,以避免开销。
  • 使用自定义 WAL 记录进行复制。WALSender 可以将复制记录发送到备用服务器。
    • 使用 WAL 的一个缺点是,在某些情况下,使用者可能需要旧记录。WALSender 需要从归档中读取旧的 WAL 记录,但这是不可能的。
  • GDQ 的自定义存储可能类似于 SQL/MED 外部表。我们可以扩展 SQL 标准以处理外部表,以处理 INSERT 命令,并将其用作存储。

语法

我们有选择用于创建队列和注册队列使用者的语法的选择。哪一个更好?

基于函数
PgQ 有 pgq.create_queue() 和 pgq.register_consumer()。
灵活,但表名以字符串文字(单引号)形式出现。
基于 SQL
Oracle 有 CREATE MATERIALIZED VIEW LOG 和 CREATE MATERIALIZED VIEW。
另一个 SQL 想法是使用 ALTER TABLE,比如 ADD MODIFICATION QUEUE。

参考文献

广义数据队列应该有足够的强大功能来支持现有的复制产品,比如 SlonyLondiste

Slony 的 sl_log

Slony 的 sl_log 存储要传播到订阅者节点的每个更改。日志由两个表组成,sl_log_1 和 sl_log_2,以有效地清除旧记录。

CREATE TABLE sl_log (
  log_origin    integer, -- Origin node from which the change came 
  log_xid       xxid,    -- Transaction ID on the origin node 
  log_tableid   integer, -- The table ID that this log entry is to affect
  log_actionseq bigint,
  log_cmdtype   char(1), -- U = Update, I = Insert, D = DELETE 
  log_cmddata   text,    -- The data needed to perform the log action
);

在版本 2.1 中,log_cmdtype 扩展了一点,并且预计在 2.2 中会进一步扩展。值是

描述
I 插入
U 更新
D 删除
T 截断
S DDL 脚本

Londiste 的 PgQ

PgQ 定义了一个基于文本的广义队列,Londiste 通过 触发器 使用这些队列。每个队列都由多个表组成(默认情况下为 3 个),原因与 Slony 相同。

CREATE TABLE pgq.event_template (
  ev_id     bigint,      -- event's id, supposed to be unique per queue
  ev_time   timestamptz, -- when the event was inserted
  ev_txid   bigint,      -- transaction id which inserted the event
  ev_owner  int4,        -- subscription id that wanted to retry this
  ev_retry  int4,        -- how many times the event has been retried, NULL for new events
  ev_type   text,        -- I/U/D
  ev_data   text,        -- partial SQL statement or column values urlencoded
  ev_extra1 text,        -- table name
  ev_extra2 text,        -- (not used in Londiste)
  ev_extra3 text,        -- (not used in Londiste)
  ev_extra4 text         -- (not used in Londiste)
);

Oracle 数据库中的物化视图日志

Oracle 数据库支持基于物化视图日志的行级复制。日志用于在 物化视图 中进行差异更新,但也用于结合 DATABASE LINK 进行复制。有关详细信息,请参阅 规划您的复制环境。PostgreSQL 可能从 Oracle 数据库中学习复制/广义数据队列的设计,其中基于行的逻辑复制基于 3 个基本模块

  1. 广义数据队列
  2. 具有差异更新的物化视图
  3. 数据库间连接

物化视图日志默认情况下只包括主键,但用户可以在日志中添加任何列。简单的物化视图只需要主键用于差异更新。实际数据似乎是使用与主键连接的原始表检索的。另一方面,复杂的物化视图(例如,包括聚合)需要引用列数据。请注意,与 Slony 和 Londiste 不同,Oracle 的日志包含修改或插入的列值。主键的更改被描述为 DELETE 和 INSERT 的组合。

CREATE TABLE oracle_mlog_pk (
  pk_1     pk_type_1,   -- first primary key
  pk_2     pk_type_2,   -- second primary key
  ...
  pk_N     pk_type_N,   -- Nth primary key
  snaptime timestamptz, -- first snapshot sets initial refresh time
  dmltype  char(1),     -- Type of DML
  old_new  char(1),     -- O/N/U (= D/I/U in Slony)
  change_vector bytea   -- Used for subquery and LOB snapshots (?)
);