分布式事务的原子性提交

来自 PostgreSQL wiki
跳至导航跳至搜索

原子性提交提供提交所有外部分发服务器或不进行任何提交的功能。这确保联合数据库中数据库数据始终保持一致性状态。

讨论此处

为了实现分布式事务的原子性提交,我们采用两阶段提交协议 (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 长度有限的情况。

同步与异步

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

外部事务解析也可以是异步的。本地提交后,后台立即向客户端返回确认。这被称为早期确认技术。缺点是希望查看先前事务结果的用户需要确保该先前事务已在所有外部服务器上提交。

谁负责外部事务解析?

对于谁负责在事务提交时进行外部事务解析(即 2pc 的第二阶段),有两种想法:后台和解析器。

1. 后台执行解析。

这是一个简单且直接的想法。收到来自客户端的 SQL 的后台进程准备外部事务,然后在本地提交后提交这些事务。

2. 解析器执行解析。

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

性能

在第 2 种方案中,如果有多个并发进程想要使用 2PC,则解析器进程可能会成为瓶颈。让解析器进程的数量等于同时使用 2PC 的后端进程的数量在性能方面等同于第 1 种方案,但是第 1 种方案可能仍然胜出,因为它没有进程间通信开销。

错误处理

外部事务解析是在本地事务提交之后执行的。因此,即使 FDW 的提交例程在此期间引发错误,也为时已晚,用户会感到困惑,因为尽管已经提交了本地事务,但还是遇到了错误。第 2 种方案旨在应对这个问题;让解析器进程执行 2PC 的第 2 阶段,而从客户端接收 SQL 的进程不会受到解析期间发生的任何错误的影响。

查询取消

2PC 的第 2 阶段可能需要很长时间,因为它涉及网络通信和磁盘访问。如果用户想要取消等待外部事务解析,该怎么办?鉴于某些客户端(例如,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

示例

根据# 如何使用部分进行设置后,我们创建了两个表;ft1 位于 fs1 服务器上,ft2 位于 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() 可以停止特定的解决程序进程。
  • 能够禁用事务自动不明确解决吗?
    • 截至目前,不行。