了解双活冲突 - Amazon Relational Database Service
Amazon Web Services 文档中描述的 Amazon Web Services 服务或功能可能因区域而异。要查看适用于中国区域的差异,请参阅 中国的 Amazon Web Services 服务入门 (PDF)

了解双活冲突

如果您在双活模式下使用 pgactive,在从多个节点写入同一个表时可能会造成数据冲突。一些集群化系统会使用分布式锁来防止并发访问,不过 pgactive 采用的是乐观方法,更适合分布在不同地理位置的应用程序。

一些数据库集群系统使用分布式锁来防止并发数据访问。但是,此方法只适合距离非常近的服务器,不支持分布在不同地理位置的应用程序,因为此方法需要极低的延迟才能实现良好的性能。pgactive 扩展没有使用分布式锁(这是一种悲观方法),而是使用乐观方法。这意味着:

  • 可帮助您尽可能避免冲突。

  • 允许出现某些类型的冲突。

  • 发生冲突时会提供冲突解决方案。

通过这种方法,您在构建分布式应用程序时具有更好的灵活性。

发生冲突的原因

节点间冲突是由事件序列引起的,如果涉及的所有事务同时发生在同一个节点上,则不会出现节点间冲突。由于节点仅在事务提交后交换更改,因此每个事务在其提交的节点上都是分别有效的,但如果事务在另一个同时完成其他工作的节点上运行,就会无效。由于 pgactive 的应用操作本质上是在其他节点上重播事务,因此,如果正在应用的事务与在接收节点上提交的事务之间存在冲突,则重播操作会失败。

当所有事务都运行在单个节点上时,大多数冲突不会发生的原因是 PostgreSQL 采用了事务间通信机制来防止冲突,包括:

  • UNIQUE 索引

  • SEQUENCE

  • 行和关系锁定

  • SERIALIZABLE 依赖关系跟踪

所有这些机制都是在事务之间进行通信的方法,用于防止出现意外并发问题

pgactive 不使用分布式事务管理器或锁管理器,因此可以实现低延迟并很好地处理网络分区。但是,这意味着不同节点上事务的运行是彼此完全隔离的。虽然隔离通常可以提高数据库的一致性,但在这种情况下,您需要减少隔离以防止冲突。

冲突类型

可能发生的冲突包括:

PRIMARY KEY 或 UNIQUE 冲突

当多个操作试图以单个节点上不可能出现的方式修改同一个行键时,就会发生行冲突。这些冲突体现了最常见的数据冲突类型。

pgactive 采用上次更新获胜处理方法或者您的自定义冲突处理程序,来解决检测到的冲突。

行冲突包括:

  • INSERT 与 INSERT

  • INSERT 与 UPDATE

  • UPDATE 与 DELETE

  • INSERT 与 DELETE

  • DELETE 与 DELETE

  • INSERT 与 DELETE

INSERT/INSERT 冲突

这是最常见的冲突,当两个不同节点上的 INSERT 操作创建具有相同 PRIMARY KEY 值的元组(或在没有 PRIMARY KEY 时,使用相同的 UNIQUE 约束值),就会发生这种冲突。

pgactivelink 使用来自原始主机的时间戳来保留最新的元组,以此解决 INSERT 冲突。您可以使用自定义冲突处理程序覆盖此默认行为。虽然此过程不需要管理员专门采取操作,但请注意,pgactivelink 会丢弃所有节点上的其中一个 INSERT 操作。除非您的自定义处理程序实施自动数据合并,否则不会自动进行数据合并。

pgactivelink 只能解决涉及单个约束违规的冲突。如果某个 INSERT 违反了多个 UNIQUE 约束,您必须实施其他冲突解决策略。

违反多个 UNIQUE 约束的 INSERT

INSERT/INSERT 冲突可能违反多个 UNIQUE 约束,包括 PRIMARY KEY。pgactivelink 只能处理涉及单个 UNIQUE 约束的冲突。当冲突违反多个 UNIQUE 约束时,应用工作线程会失败并返回以下错误:

multiple unique constraints violated by remotely INSERTed tuple.

在较早的版本中,这种情况生成的是“分歧唯一性冲突”错误。

要解决这些冲突,您必须手动采取操作。您可以对发生冲突的本地元组执行 DELETE,或者对元组执行 UPDATE 以使用新的远程元组来消除冲突。请注意,您可能需要解决多个相互冲突的元组。目前,pgactivelink 没有提供内置功能用于忽略、丢弃或合并违反多个唯一约束的元组。

注意

有关更多信息,请参阅“违反多个 UNIQUE 约束的 UPDATE”。

UPDATE/UPDATE 冲突

当两个节点并发修改同一个元组而没有更改其 PRIMARY KEY 时,就会发生此冲突。pgactivelink 使用上次更新获胜逻辑或您的自定义冲突处理程序(如果已定义)来解决这些冲突。PRIMARY KEY 对于元组匹配和冲突解决至关重要。对于没有 PRIMARY KEY 的表,pgactivelink 拒绝 UPDATE 操作,并显示以下错误:

Cannot run UPDATE or DELETE on table (tablename) because it does not have a primary key.

PRIMARY KEY 上的 UPDATE 冲突

pgactive 在处理 PRIMARY KEY 更新时存在限制。虽然您可以对 PRIMARY KEY 执行 UPDATE 操作,但对于这些操作,pgactive 无法使用最后更新获胜逻辑自动解决冲突。您必须确保 PRIMARY KEY 更新不会与现有值冲突。如果在 PRIMARY KEY 更新期间发生冲突,这些冲突就会成为分歧冲突,需要您手动干预。有关处理这些情况的更多信息,请参阅分歧冲突

违反多个 UNIQUE 约束的 UPDATE

当传入的 UPDATE 违反多个 UNIQUE 约束或 PRIMARY KEY 值时,pgactivelink 无法应用上次更新获胜冲突解决方案。此行为类似于出现多个约束冲突的 INSERT 操作。这些情况会造成分歧冲突,需要您手动干预。有关更多信息,请参阅分歧冲突

UPDATE/DELETE 冲突

当一个节点对一行执行 UPDATE 而另一个节点同时对该行执行 DELETE 时,就会出现此冲突。在这种情况下,重播时会发生 UPDATE/DELETE 冲突。除非您的自定义冲突处理程序另行规定,否则解决方案是丢弃在 DELETE 之后到达的所有 UPDATE。

pgactivelink 需要一个 PRIMARY KEY 来匹配元组并解决冲突。对于没有 PRIMARY KEY 的表,表会拒绝 DELETE 操作,并显示以下错误:

Cannot run UPDATE or DELETE on table (tablename) because it does not have a primary key.

注意

pgactivelink 无法区分 UPDATE/DELETE 和 INSERT/UPDATE 冲突。在这两种情况下,UPDATE 均会影响不存在的行。由于异步复制以及节点之间缺乏重播排序,pgactivelink 无法确定是在对新行(尚未收到 INSERT)还是对已删除行执行 UPDATE 操作。在这两种情况下,pgactivelink 都会丢弃 UPDATE。

INSERT/UPDATE 冲突

这种冲突可能发生在多节点环境中。当一个节点 INSERT 一行数据,第二个节点对该行执行 UPDATE,而第三个节点在收到原始 INSERT 之前就接收到 UPDATE 时,就会发生这种情况。默认情况下,除非您的自定义冲突触发器另行规定,否则 pgactivelink 会通过放弃 UPDATE 来解决这些冲突。请注意,这种解决方法可能会导致节点间的数据不一致。有关类似场景及其处理方法的更多信息,请参阅 UPDATE/DELETE 冲突

DELETE/DELETE 冲突

当两个不同的节点并发删除同一个元组时,就会发生这种冲突。pgactivelink 认为这些冲突无害,因为两个 DELETE 操作会产生相同的最终结果。在此场景中,pgactivelink 可以安全地忽略其中一个 DELETE 操作,而不会影响数据一致性。

外键约束冲突

对现有本地数据应用远程事务时,FOREIGN KEY 约束可能会导致冲突。发生这些冲突的情况通常是应用事务的顺序,不同于事务在发起节点上的逻辑顺序。

默认情况下,pgactive 会将 session_replication_role 作为 replica 来应用更改,这会在复制过程中绕过外键检查。在双活配置中,这可能会导致外键违规。大多数违规行为都是暂时性的,当复制进度跟上时就会解决。但是,由于 pgactive 不支持跨节点行锁定,因此可能会出现悬挂外键。

这种行为是具备分区容错性的异步双活系统所固有的。例如,节点 A 可能会插入新的子行,而节点 B 同时在删除其父行。系统无法阻止节点之间这种类型的并发修改。

为尽可能减少外键冲突,我们给出了以下建议:

  • 将外键关系限制为密切相关的实体。

  • 尽可能从单个节点修改相关实体。

  • 选择极少需要进行修改的实体。

  • 针对修改实施应用程序级的并发控制。

排除约束冲突

pgactivelink 不支持排除约束,并会限制创建这种约束。

注意

在将现有的独立数据库转换为 pgactivelink 数据库时,请手动删除所有排除约束。

在分布式异步系统中,不可能保证所有行集都没有违反约束。这是因为不同节点上的所有事务都是完全隔离的。排除约束可能导致重播死锁,在这种情况下,由于排除约束违规,重播无法从任何节点进展到另一个节点。

如果您强制 pgactivelink 创建排除约束,或者在将独立数据库转换到 pgactivelink 时不删除现有排除约束,则复制很可能会中断。要恢复复制进度,请删除或更改与传入的远程元组冲突的本地元组,这样便可以应用远程事务。

全局数据冲突

使用 pgactivelink 时,当节点具有不同的全局 PostgreSQL 系统级数据(例如角色)时,就可能会发生冲突。这些冲突的结果是操作(主要是 DDL)会成功并在一个节点上提交,但无法应用于其他节点。

如果一个节点上有某个用户但另一个节点上没有,就会出现复制问题:

  • Node1 有名为 fred 的用户,但是 Node2 上没有该用户

  • fred 在 Node1 上创建表时,表在复制时的所有者是 fred

  • 将此 DDL 命令应用于 Node2 时,命令会失败,因为用户 fred 不存在

  • 在失败后,会在 Node2 上的 PostgreSQL 日志中生成 ERROR 条目并递增 pgactive.pgactive_stats.nr_rollbacks 计数器

解决方案:在 Node2 上创建用户 fred。该用户不必具备相同的权限,但必须存在于两个节点上。

如果一个节点上有某个表但另一个节点上没有,则数据修改操作会失败:

  • Node1 上有名为 foo 的表,但 Node2 上没有

  • Node1 上对 foo 表执行的任何 DML 操作,在复制到 Node2 时会失败

解决方案:在 Node2 上创建具有相同结构的表 foo

注意

pgactivelink 目前不复制 CREATE USER 命令或 DDL 操作。DDL 复制功能计划在未来版本中发布。

锁冲突和死锁中止

由于 pgactive 应用进程的运行方式类似于普通用户会话,因此这些进程遵循标准的行和表锁定规则。这可能会导致 pgactivelink 应用进程需要等待用户事务或其他应用进程持有的锁被释放。

以下类型的锁会影响应用进程:

  • 用户会话施加的显式表级锁(LOCK TABLE...)

  • 用户会话施加的显式行级锁(SELECT ... FOR UPDATE/FOR SHARE)

  • 来自外键的锁定

  • 由于行 UPDATE、INSERT 或 DELETE 而导致的隐式锁定,这可能源自本地活动,也可能是从其他服务器应用的

在下列操作之间可能会出现死锁:

  • pgactivelink 应用进程和用户事务

  • 两个应用进程

发生死锁时,PostgreSQL 的死锁检测器会终止其中一个出现问题的事务。如果 pgactivelink 应用工作线程的进程终止,则会自动重试而且通常会成功。

注意
  • 这些问题是暂时性的,通常无需管理员干预。如果由于空闲用户会话锁定导致某个应用进程被长时间阻止,您可以终止该用户会话来恢复复制。这种情况类似于某个用户持有的长时间锁定影响到了其他用户会话。

  • 要识别与锁定相关的重播延迟,请在 PostgreSQL 中启用 log_lock_waits 功能。

分歧冲突

当各节点之间本应相同的数据意外地变得不同时,就会发生分歧冲突。虽然这些冲突不应该发生,但在当前的实施中,无法可靠地预防所有冲突。

注意

在一行上,如果有某个节点在所有节点处理更改之前更改该行的主键,则修改行的 PRIMARY KEY 会导致分歧冲突。请避免更改主键,或将更改限制在某个指定节点上。有关更多信息,请参阅PRIMARY KEY 上的 UPDATE 冲突

涉及行数据的分歧冲突通常需要管理员干预。要解决这些冲突,您必须手动调整一个节点上的数据来与其他节点相匹配,同时使用 pgactive.pgactive_do_not_replicate 暂时禁用复制。如果您按照文档说明使用 pgactive 并避免使用标记为不安全的设置或函数,则不应出现这些冲突。

作为管理员,您必须手动解决这些冲突。根据冲突类型,您需要使用诸如 pgactive.pgactive_do_not_replicate 之类的高级选项。请谨慎使用这些选项,因为使用不当会导致情况恶化。由于可能出现的冲突各种各样,我们无法提供通用的解决方案说明。

当不同节点之间本应相同的数据意外地变得不同时,就会发生分歧冲突。虽然这些冲突不应该发生,但在当前的实施中,无法可靠地预防所有此类冲突。

避免或容忍冲突

在大多数情况下,您可以使用合适的应用程序设计,从而避免冲突或使应用程序具备容忍冲突的能力。

只有在多个节点上同时进行多个操作时才会发生冲突。为避免冲突,请采取以下方法:

  • 仅向一个节点写入

  • 写入各个节点上的独立数据库子集(例如,向每个节点分配一个单独的架构)

对于 INSERT 与 INSERT 冲突,请使用全局序列来彻底防止冲突。

如果您的应用场景不能接受冲突,请考虑在应用程序级别实施分布式锁定。通常,最好的方法是对应用程序进行设计,使其与 pgactive 的冲突解决机制配合使用,而不是尝试预防所有冲突。有关更多信息,请参阅冲突类型

冲突日志记录

pgactivelink 会将冲突事件记录到 pgactive.pgactive_conflict_history 表中,供您诊断和处理双活冲突。仅当 pgactive.log_conflicts_to_table 设置为 true 时,才会将冲突记录到此表。无论 pgactive.log_conflicts_to_table 的设置如何,当 log_min_messages 设置为 LOGlower 时,pgactive 扩展还会将冲突记录到 PostgreSQL 日志文件中。

使用冲突历史记录表可以:

  • 衡量您的应用程序造成冲突的频率

  • 确定在什么位置发生冲突

  • 改进应用程序以降低冲突率

  • 检测冲突解决方案无法达成所需结果的情况

  • 确定哪些地方需要用户定义的冲突触发器或更改应用程序设计

对于行冲突,您可以选择记录行值。这由 pgactive.log_conflicts_to_table 设置控制。请注意:

  • 这是一个全局数据库级选项

  • 无法按各个表来控制对行值的记录

  • 字段数量、数组元素或字段长度没有限制

  • 如果您在处理数 MB 的行而这些行可能会触发冲突,则不建议启用此功能

由于冲突历史记录表包含来自数据库中各个表的数据(每个表都可能采用不同架构),因此记录的行值存储为 JSON 字段。JSON 使用 row_to_json 创建,类似于直接从 SQL 中调用。PostgreSQL 不提供 json_to_row 函数,因此您需要特定于表的代码(采用 PL/pgSQL、PL/Python、PL/Perl 等)才能从记录的 JSON 中重新构造复合类型的元组。

注意

对用户定义冲突的支持计划作为未来的扩展功能推出。