分布式事务的原子提交

来自 PostgreSQL wiki
(从 Atomic Commit 重定向)
跳转到导航跳转到搜索

原子提交提供功能,可以要么提交所有外来服务器,要么全部不提交。这确保了数据库数据在联合数据库方面始终保持一致状态。

讨论在 这里

为了实现分布式事务的原子提交,我们使用 两阶段提交协议(2PC),一种原子提交协议。

分布式事务涉及原子提交、原子可见性和全局一致性。2PC 是原子提交的唯一实用解决方案。

用法

用户概述

  • 协调节点和参与节点
    • 协调节点管理参与节点上的所有外来事务。
    • max_prepared_foreign_transactions GUC 参数控制协调服务器控制的外来事务数量的上限。由于一个本地事务可以访问多个外来服务器,因此应设置此参数,
(max_connetions) * (# of foreign servers that are capable of 2PC)
  • 外来事务启动器
    • 后台工作进程启动外来事务解析器进程。
  • 外来事务解析器
    • 后台工作进程,用于解决分布式事务。
    • 一个解析器进程负责解决一个数据库上的分布式事务。
      • 这可以通过在一个数据库上拥有多个解析器来改进。
    • 解析器进程的最大数量由 max_foreign_transaction_resolvers GUC 参数控制。
    • 重试外来事务解析的间隔由 foreign_transaction_resolution_interval GUC 参数控制。
    • 解析器进程的超时时间由 foreign_transaction_resolver_timeout GUC 参数控制。
      • 当数据库上没有未解决的外来事务时,解析器进程在 foreign_transacton_resovler_timeout 秒后退出。
  • 启用和禁用分布式原子提交功能。
    • 当 foreign_twophase_commit 启用(例如,必需)并且事务满足以下任一条件时,将使用两阶段提交协议。
      • 分布式事务修改了两个以上能够进行 2PC 的服务器,包括协调节点。
      • 客户端执行 PREPARE TRANSACTION 命令。在这种情况下,无论写入次数多少,都会无条件地使用 2PC。

伪代码

RM: involved foreign transactions (excluding the transaction on the local node)

Procedure TransactionCommit()
    foreach RM
        lock RM;
        append a log of adding RM to WAL;
        prepare RM;
        if failed then call termination protocol;
    append a log of local commit to WAL;
    unlock all RMs;
    return;

Procedure TerminationProtocol()
    append a log of local abort to WAL;
    foreach RM
        rollback RM;
        unlock RM;
        append a log of removing RM to WAL;

Procedure Resolution()
    foreach prepared, unlocked RM
        get the status of the corresponding local transaction;
        if committed then
            commit prepared RM;
        else
            rollback prepared RM;
        append a log of removing RM to WAL;

从客户端接收 SQL 的后端进程调用 TransactionCommit() 和 TerminationProtocol(),而解析器进程调用 Resolution()。

用于事务管理的新的 FDW 例程

新添加了四个回调函数。

typedef void (*PrepareForeignTransaction_function) (FdwXactRslvState *frstate);
typedef void (*CommitForeignTransaction_function) (FdwXactRslvState *frstate);
typedef void (*RollbackForeignTransaction_function) (FdwXactRslvState *frstate);
typedef char *(*GetPrepareId_function) (TransactionId xid, Oid serverid,
                                        Oid userid, int *prep_id_len);

在 FDW 开发人员方面,有三种可能的选择

  • 1. 不实现事务 API
    • 外来事务不受核心管理。
  • 2. 实现提交和回滚 API
    • 核心只提交和回滚外来事务。这种类型的 FDW 无法参与外来两阶段提交。
  • 3. 实现提交、回滚和准备 API
    • 这种类型的 FDW 支持外来两阶段提交。核心提交、回滚和准备外来事务。
    • 可选地,FDW 可以支持 GetPreapreId API,用于某些情况,例如,FDW 对全局事务 ID 的长度有限制。

同步和异步

在同步解析中,后端会一直等待所有外来事务提交/回滚,直到用户显式发送取消请求。这确保了后续事务可以假定所有前置分布式事务在客户端获得提交确认时已完成。后端可以通过取消事务提交来退出等待循环(例如,按 ctl-c 或 pg_cancel_backend())。然后,外来事务将变为不确定状态,并由解析器在后台处理。

异步外来事务解析也是一种选择。后端在本地提交后立即向客户端返回确认。这被称为早期确认技术。缺点是,想要查看前置事务结果的用户需要确保前置事务已在所有外来服务器上提交。

谁负责解决外来事务?

关于谁负责解决外来事务(即 2PC 的第二阶段)在事务提交时,有两个想法:后端和解析器。

1. 后端执行解析。

这是一个简单直接的想法。从客户端接收 SQL 的后端进程准备外来事务,并在本地提交后提交它们。

2. 解析器执行解析。

有了这个想法,后端准备外来事务并在本地提交,但要求解析器进程提交/回滚已准备的事务。也就是说,不同的进程执行 2PC 的第一阶段和第二阶段。

性能

在想法 #2 中,如果有多个并发进程想要使用 2PC,解析器进程可能会成为瓶颈。拥有与并发使用 2PC 的后端进程数量一样多的解析器进程,在性能方面等同于想法 #1,但想法 #1 仍然可能获胜,因为没有进程间通信的开销。

错误处理

外来事务解析是在本地事务提交之后执行的。因此,即使 FDW 的提交例程在此期间引发错误,也为时已晚,会让用户感到困惑,因为他们会在本地事务已提交的情况下收到错误。想法 #2 是为了解决这个问题而设计的;让解析器进程执行 2PC 的第二阶段,从客户端接收 SQL 的进程不受解析期间发生的任何错误的影响。

查询取消

2PC 的第二阶段可能需要很长时间,因为它涉及网络通信和磁盘访问。如果用户想要取消等待外来事务解析怎么办?考虑到某些客户端(例如 odbc_fdw?)库不支持异步执行,对于想法 #1,用户取消在某些 FDW 中不起作用。另一方面,对于想法 #2,用户可以安全地取消,因为后端只是在等待。想法 #2 在这方面获胜,但请注意,这仅支持外来事务解析的查询取消,而不是通过 FDW 发送的所有查询。

结论

尚未得出结论。

如何使用

配置

在本节中,我们将描述如何在具有一个协调器和两个节点的情况下使用此功能。带有[C] 的步骤表示在协调节点上进行的操作,而带有[P] 的步骤表示在外来服务器(参与节点)上进行的操作。

  • 1. [C] 设置 GUC 参数 max_foreign_prepared_transactions

一个事务可以涉及多个外来服务器,并在这些服务器上进行准备,因此 max_foreign_prepared_transaction 应该至少大于 (max_connections) * (能够进行 2pc 的外来服务器数量)。为了测试,我们将 max_prepared_transactions 设置为大于 1。

$ $EDITOR postgresql.conf
max_connections = 100
max_prepared_foreign_transactions = 200 # max_connections = 100 and two foreign servers
max_foreign_transaction_resolvers = 1
foreign_twophase_commit = required
foreign_transaction_resolution_interval = 5s
froeign_transaction_resolver_timeout = 60s
  • 2. [P] 设置 GUC 参数 max_prepared_transactions

此外,为了轻松确认此功能的行为,我们将在所有外来服务器上设置 log_statement = all。

$ $EDITOR postgresql.conf
max_prepared_transactions = 100 # same as max_connections of the coordinator server
log_statement = all # for testing
log_line_prefix = '<F1> ' # for fs2 server we can set '<F2> '
  • 3. [C] 创建 postgres_fdw 扩展
  • 4. [C] 使用 two_phase_commit 参数 = on 创建外来服务器
=# CREATE SERVER fs1 FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host 'fs1', dbname 'postgres', port '5432');
CREATE SERVER
=# CREATE SERVER fs2 FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host 'fs2', dbname 'postgres', port '5342');
CREATE SERVER
=# SELECT * FROM pg_foreign_server;
 oid  | srvname | srvowner | srvfdw | srvtype | srvversion | srvacl |              srvoptions
-------+---------+----------+--------+---------+------------+--------+--------------------------------------
16451 | fs1     |       10 |  16387 |         |            |        | {host=fs1,dbname=postgres,port=5432}
16452 | fs2     |       10 |  16387 |         |            |        | {host=fs2,dbname=postgres,port=5432}

(2 行)

  • 5. [C] 创建用户映射
=# CREATE USER MAPPING FOR PUBLIC SERVER fs1;
CREATE USER MAPPING
=# CREATE USER MAPPING FOR PUBLIC SERVER fs2;
CREATE USER MAPPING

示例

按照 #如何使用 部分设置完成后,我们在 fs1 服务器上创建两个表:ft1ft2,在 fs2 服务器上也创建相应的表,并在协调节点上创建相应的外部表。

  • 示例 1. 使用两阶段提交协议提交事务
=# BEIGN;
=# INSERT INTO ft1 VALUES (1);
=# INSERT INTO ft2 VALUES (1);
=# COMMIT;

我们将在 fs1 服务器和 fs2 服务器上看到以下服务器日志。

<FS1> LOG:  statement: START TRANSACTION ISOLATION LEVEL REPEATABLE READ
<FS1> LOG:  execute pgsql_fdw_prep_1: INSERT INTO public.s1(col) VALUES ($1)
<FS1> DETAIL:  parameters: $1 = '1'
<FS1> LOG:  statement: DEALLOCATE pgsql_fdw_prep_1
<FS2> LOG:  statement: START TRANSACTION ISOLATION LEVEL REPEATABLE READ
<FS2> LOG:  execute pgsql_fdw_prep_2: INSERT INTO public.s2(col) VALUES ($1)
<FS2> DETAIL:  parameters: $1 = '1'
<FS2> LOG:  statement: DEALLOCATE pgsql_fdw_prep_2
<FS1> LOG:  statement: PREPARE TRANSACTION 'fx_68464475_515_16400_10'
<FS2> LOG:  statement: PREPARE TRANSACTION 'fx_658736079_515_16410_10'
<FS1> LOG:  statement: COMMIT PREPARED 'fx_68464475_515_16400_10'
<FS2> LOG:  statement: COMMIT PREPARED 'fx_658736079_515_16410_10'
  • 示例 2. 事务修改单个节点,然后在一阶段提交

如果事务仅修改一个外来服务器,则无需使用两阶段提交协议。

=# BEIGN;
=# INSERT INTO ft1 VALUES (1);
=# COMMIT;

在 fs1 服务器上,我们将看到以下服务器日志。

<FS1> LOG:  statement: START TRANSACTION ISOLATION LEVEL REPEATABLE READ
<FS1> LOG:  execute pgsql_fdw_prep_3: INSERT INTO public.s1(col) VALUES ($1)
<FS1> DETAIL:  parameters: $1 = '1'
<FS1> LOG:  statement: DEALLOCATE pgsql_fdw_prep_3
<FS1> LOG:  statement: COMMIT TRANSACTION

常见问题解答

  • 何时使用 2pc?
    • 基本上,当分布式事务修改了包括协调节点在内的多个服务器时。
    • 但是,当客户端执行 PREPARE TRANSACTION 命令时,会无条件地使用 2pc。
  • 并发读取器可以看到不一致的结果,例如,当读取器在写入器仅在其中一个外来服务器上提交已准备的外来事务后,在两个外来服务器上启动新的外来事务时?
    • 是的。此功能仅确保原子提交,但不确保原子可见性。为了支持全局一致的结果,需要其他机制,例如提供全局一致的快照。
  • 如果在本地提交之前涉及的服务器崩溃了怎么办?
    • 分布式事务更改将回滚。它将 ROLLBACK 发送到未准备的外来事务,并将 ROLLBACK PREPARED 发送到已准备的外来事务。对于正在准备的外来事务,它会同时发送,因为我们不确定准备是否已完成。因此,FDW 必须容忍 ERRCODE_UNDEFINED_OBJECT 错误。
  • 如果在本地提交之后涉及的服务器崩溃了怎么办?
    • 分布式事务的命运不会改变。也就是说,事务解析器进程将继续尝试提交外来事务。
  • 如果涉及的服务器崩溃并且永远无法恢复怎么办?
    • pg_remove_foreign_xact() 可用于在不解析的情况下删除外来事务条目。
  • 运行在数据库上的事务解析器会阻止 DROP DATABASE。如何停止解析器进程?
    • pg_stop_fdwxact_resolver() 可以停止指定的解析器进程。
  • 可以禁用自动不确定事务解析吗?
    • 目前不行。