分布式事务可见性
分布式 R/W 工作负载量 PostgreSQL 数据库集群的全局可见性
项目的目标
- 为跨越多个 postgres 数据库实例的分布式 R/W 事务提供一致的可见性。
- 不对本地事务造成影响。
要解决的问题
我们有二阶段提交协议,将事务组的写入一致性作为分布式事务进行维护。
在这种情况下,最终“提交”时间戳在数据库实例中各不相同。
因此,写入结果可能在某些涉及的实例中可见,而在其他实例中不可见。这称为分布式读异常。
我们需要解决这个读异常问题。
除了 Postgres-XC 和 XL 中关注的技术主题外,该项目还旨在解决
- 将本地 postgreSQL 数据库连接到数据库集群组中,
- 将属于数据库集群的数据从单独数据库中分离出来。
解决方案概述
- 我们定义全局事务标识符,它对每个分布式事务是唯一的,并且对于属于同一全局事务的事务来说是相同的。
- 在行可见性检查中对全局事务使用全局快照。
分布式事务的定义和结构
- 任何分布式事务都有一个“根”事务,它启动事务。
- 分布式事务中的其他事务必须由“根”事务或“子事务”调用,其中“子事务”由“根”事务直接或间接调用。
- “根”事务被赋予全局事务 ID(GXID),表示为
(dbid, gxid)
,其中dbid
是根事务正在运行的数据库标识符,gxid 是全局事务代理按升序指定的唯一数字,如下所述。
通常,initdb 给出的数据库系统标识符可用于此目的。这取决于实现,只要 dbid 是唯一的,我们就可以选择任何其他方式。如果是数据库系统标识符,则可以通过以下方式复制它:例如,使用 `pg_basebackup` 克隆数据库和流复制。如果此类克隆数据库应作为单独的数据库实例使用,则我们可能需要一种方法来分配不同的数据库系统标识符。
- 当“根”事务及其“子”事务调用子事务时,此类“父”事务必须将其全局事务 ID 传播到其子代。在本地,此全局事务 ID 必须与本地事务相关联。
全局事务 ID 的分配
- 我们需要一个单独的机构来管理给定 PostgreSQL 集群中的全局事务信息,称为“全局事务代理” (GTP)。
- 当事务希望成为根事务并希望提供和接收一致的可见性时,需要连接到 GTP 并请求新的 GXID。GTP 分配 gxid,并将其与根事务的 dbid 记录为
(dbid, gxid)
对。请注意,这不仅对于写入事务是必需的,对于需要在不同数据库实例之间保持一致可见性的读取事务也是必需的。 - 根事务必须将其 GXID 传播到其子代。当子代调用其子代时,它还必须传播此 GXID,而此 GXID 应与子代的本地事务关联。
- 当根事务(及其所有子代)以最终提交结束时,它必须向 GTP 报告事务已完成。GTP 将记录此完成。下面将介绍在 GTP 中维护 GXID 信息。
全局快照及其在本地事务中的处理。
- 根必须向 GTP 询问正在运行的全局事务快照。GTP 收集正在运行的全局事务的 GXID,并将此 GXID 集作为全局快照提供。
- 根必须将此全局快照传播到其子代,依此类推。这与子代的本地事务关联。
- 在可见性检查中,如果本地 XID(在 xmin/xmax 和其他部分中)与 GXID 相关联(我们可能需要在每一行中使用标志来指示这一点),则此类事务可见性应基于其出现在快照中的 GXID 来确定(请注意,在全局快照中,无论是终止的 GXID 还是正在运行的 GXID 都会出现,具体取决于其语法)。详细信息将在下面给出。
- 不与 GXID 关联的本地事务的可见性可按与当前通常的本地事务相同的方式处理。
- 对于不带 GXID 的本地事务,不存在全局快照且不将 GXID 用于可见性检查,即使目标行的 xid 与 GXID 关联。因此,对于本地事务,不会有额外的开销。
全局快照数据格式
全局快照数据可表示为以下格式
(GXID_0_0, ..., GXID_0_M) GXMIN (GXID_1_0, ..., GXID_1_N) GXMAX
GXMIN
表示较该值小的 GXID 的全局事务全部终止(提交/中止),除非出现在列表 (GXID_0_0, ... GXID_0_M)
中。这用于节省快照量,特别是在只少量全局事务处于运行状态以进行错误恢复(通常为本地提交失败)的情况时。
GXMAX
表示该值为当前已分配的 GXID 的最大值,并且带有大于 GXMAX 的 GXID 的全局事务必须视为仍然在运行。
(GXID_1_0, .... , GXOD_1_N)
表示一组 GXID,它们的 GXID 在 GXMIN
和 GXMAX
之间并且已终止。其原因是我们无法保证在关联数据库实例于事务报告开始后且报告终止之前关闭时,尚未运行该事务。
从上述内容中,我们可以看到对给定情况可能有多种快照表示。GPT 应通过计算 GXMIN
和 GXMAX
的最佳值来提供最小大小的快照。
GPT 中的全局快照维护
GTP 中的 GXID 不能立即在 GPT 中移除,因为有可能其他全局事务依赖于此。
当事务终止时,其 GXID 将标记为已完成,并与一组运行中的 GXID 关联,作为引用 GXID。每个此类 GXID 都可以在该 GXID 终止时将其从列表中移除。当引用 GXID 变为零时,该类 GXID 信息可从 GTP 中移除。
当数据库实例中断并重启时,所有关联事务都将自动中止。在这种情况下,每个数据库实例都应向 GTP 报告此情况,如果 dbid 与该数据库实例匹配,则 GTP 将清除其全局事务信息。
请注意,此类 GXID 将不会出现在终止后的请求的快照中。这可用于计算快照中的 GXMIN,从而最大程度地减少全局快照数量。
本地数据库实例中的全局快照维护
本地数据库实例不能在该 GXID 终止时立即移除与其关联的 GXID,因为其他全局事务可以在随后引用此 GXID。本地数据库实例应定期向 GTP 询问有效的 GXID 以防止数据膨胀。如果 GTP 中不存在此类 GXID,则本地数据库实例可将其移除。请注意,此类剩余的 GXID-XID 映射不会影响可见性检查的正确性,但可能会影响性能。
属于多个数据库集群的数据库实例
我们可以允许一个数据库实例属于多个集群。在这种情况下,我们需要使用群集 ID 扩展上述内容。任何全局事务都需要确定它所属的群集以及对适当的 GTP 的 GXID 和全局快照请求。在这种情况下,GXID 可以表示为 (cluster-id, dbid, gxid)。