了解双活冲突
如果您在双活模式下使用 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 设置为 LOG
或 lower
时,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 中重新构造复合类型的元组。
注意
对用户定义冲突的支持计划作为未来的扩展功能推出。