Citus总结:分布式事务及死锁检测

2022-04-13 00:00:00 事务 分布式 节点 死锁 可用性

1. 基础理论

1.1 ACID原则

数据库系统为了保证事务执行的正确可靠,必须具备ACID四个原则,百度百科中对事务ACID的说明如下:

  • Atomicity(原子性):一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

  • Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的度、串联性以及后续数据库可以自发性地完成预定的工作。

  • Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。

  • Durability(持久性):事务处理结束后,对数据的修改就是的,即便系统故障也不会丢失。

1.2 CAP理论

在分布式数据库场景下,还要结合分布式系统的CAP理论,定义如下:
  • Consistency(一致性):对某个指定的客户端来说,读操作能返回新的写操作。对于数据分布在不同节点上的数据上来说,如果在某个节点更新了数据,那么在其他节点如果都能读取到这个新的数据,那么就称为强一致,如果有某个节点没有读取到,那就是分布式不一致。
  • Availability(可用性):非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。可用性的两个关键一个是合理的时间,一个是合理的响应。合理的时间指的是请求不能无限被阻塞,应该在合理的时间给出返回。合理的响应指的是系统应该明确返回结果并且结果是正确的。
  • Partition tolerance(分区容忍性):当出现网络分区后,系统能够继续工作。打个比方,集群有多台机器,有台机器网络出现了问题,但是这个集群仍然可以正常工作。
CAP理论表明,在存在网络分区的情况下,一致性和可用性必须二选一。而在没有发生网络故障时,即分布式系统正常运行时,一致性和可用性是可以同时被满足的。但是,对于分布式数据库来说,网络故障是不可避免的,可用性是必须要保证的,所以只有舍弃一致性来保证服务的 AP。但是对于一些金融相关行业,它有很多场景需要确保一致性,这种情况通常会权衡 CA 和 CP 模型,CA 模型网络故障时完全不可用,CP 模型具备部分可用性。

1.3 BASE理论

分布式数据库一般采用2PC(两阶段提交)来提供跨越多个数据库分片的ACID保证。但是引入2PC的直接影响就是可用性下降。假设数据分布在两个数据库实例上,每个数据库实例的可用性是99.9%,那么数据库分片后可用性为99.8%。作为商用软件,可用性下降是不可容忍的。
BASE理论就是对CAP理论中的一致性和可用性权衡的结果,核心思想是即使无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到终一致性(Eventual consistency)。
BASE理论定义如下:
  • Basically Available(基本可用):指分布式系统在出现不可预知故障的时候,允许损失部分可用性,如:响应时间上的损失,或者功能上的损失。
  • Soft state(软状态):和硬状态相对,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
  • Eventually consistent(终一致性):强调的是系统中所有的数据副本,在经过一段时间的同步后,终能够达到一个一致的状态。因此,终一致性的本质是需要系统保证终数据能够达到一致,而不需要实时保证系统数据的强一致性。

2. Citus的分布式事务方案

2.1 执行步骤

如上图所示,由Coordinator节点负责协调分布式事务的执行,各Worker节点作为参与者参与分布式事务的执行:
  • 步骤1和2:Coordinator在本地,以及两个worker节点分别启动一个事务,并把worker节点的事务和Coordiantor上的全局事务进行关联;然后把相关SQL语句推送到对应worker执行;
  • 步骤3和4:客户端发起commit后,由Coordinator发起2PC提交协议:
    • PREPARE TRANSACTION
      :通知所有参与Worker节点做好事务提交准备,各Worker节点持久化相关数据,反馈可以提交或者需要回滚;
    • COMMIT PREPARED
      :当Coordinator节点收到所有Worker节点都投票可以提交事务后,发起提交事务命令,Worker节点收到消息后完成本地事务提交;如果前一阶段有一个Worker节点反馈需要归滚事务,则Coordiantor在本阶段会发送ROLLBACK
      命令;

2.2 故障处理

分布式事务执行过程中,可能会碰到节点或网络异常,2PC协议为了保证数据的强一致,实际上选择的是CAP理论中的CA部分,不能容忍网络分区异常,实际上降低了分布式系统的可用性。为了解决这个问题,Citus建议针对Coordinator和Worker节点,都采用Streaming Replication的方式保证节点的高可用,当一个节点故障时,可以快速由另外一个Standby节点接管并继续推进事务的执行。
还有一种故障场景,2PC不能很好的解决,就是当所有Worker节点都做出反馈后(可以是提交或回滚),Coordinator节点故障,此时接管的Coordinator节点不知道该如何进一步推进事务(不知道之前的投票结果)。为了解决这个问题,Citus使用了类似3PC的解决思路,当Coordinator节点收到所有Worker节点都投票可以提交时,在本地pg_dist_xact
表中记录该全局事务可以提交,同时还增加定时检测机制,将可以提交但尚未成功提交的事务继续推进执行,直到成功提交后,删除pg_dist_xact
表中对应记录。

2.3 数据一致性

虽然Citus采用2PC协议,保证了在同一个分布式事务中操作的多条记录的原子性更新。但从客户端读取的视角来看,可能会读到中间态的数据(Base中的软状态),是终一致性的模型。
原因在于Citus并没有引入集群级的全局授时机制,对于只读事务,无法获取集群级的全局一致性快照,而是由各Worker节点自己进行可见性判断,当一个分布式事务正在提交时,在不同的Worker节点提交存在时间差,在这个时间间隙内发起的查询事务,就会在已经提交的Worker节点看到已提交数据,在尚未完成提交的Worker节点看不到同一个事务写入的数据,等所有Worker节点都完成提交后,再次查询又能看到新一致的数据,因此严格说Citus提供的是终一致性模型。

2.4 隔离级别

因为没有集群级全局一致性快照的原因,Citus提供的隔离级别只有Read Commited,不管客户端是如何设置的。

3. 死锁检测

3.1 发生死锁的场景

在PostgreSQL中,当事务更新一行时,需要获取该行的锁并一直持有到事务提交为止,如果有需要获取相同锁集的并发事务,但获取锁的顺序不同,那么就会出现死锁。
下面是一个发生死锁的例子:
    S1:  UPDATE table SET value = 1 WHERE key = 'hello'; A takes 'hello’ lockS2:  UPDATE table SET value = 2 WHERE key = 'world'; B takes 'world’ lockS1:  UPDATE table SET value = 1 WHERE key = 'world'; wait for 'hello’ lock held by 2S2:  UPDATE table SET value = 2 WHERE key = 'hello'; wait for 'world’ lock held by 1
    为了检测并解决死锁的问题,数据库都会提供定期死锁检测机制,如果会话等待锁持续一段时间,PostgreSQL依赖Depedency Graph技术检查进程间是否在锁上形成循环依赖。如果是这种情况,将强制终止其中部分事务,直到死锁问题解除。

    3.2 分布式死锁检测

    上面的例子,如果发生在Citus分布式数据库场景下,则会变成这样:
      -- 在Worker A节点执行:


      S1: UPDATE table_123 SET value = 5 WHERE key = 'hello';
      S2: UPDATE table_123 SET value = 6 WHERE key = 'hello'; waits for 'hello’ lock held by 1


      -- 在Worker B节点执行:


      S1: UPDATE table_234 SET value = 6 WHERE key = 'world';
      S2: UPDATE table_234 SET value = 5 WHERE key = 'world'; waits for 'world’ lock held by 2

      如上图所示,从每个Worker节点来看,都不知道已经发生死锁情况,但在集群层面,Session1和Session2处于相互阻塞等待状态,且不能自行解除,已经发生分布式死锁现象。
      为了解决分布式死锁问题,Citus在Coordiantor节点运行分布式死锁检测后台任务来负责分布式死锁检测:Coordinator定期检测各个Worker节点是否存在长时间(1s)等锁的现象,如果存在,则从所有Worker节点搜集锁表信息,并在Coordinator节点构建事务间的Dependency Graph,检测是否存在相互依赖并形成死锁,如果存在,则按照策略取消部分事务来解锁。

      3.3 如何避免死锁发生

      一旦死锁现象发生,不只是相应的事务无法推进执行,还会因为持有锁进一步阻塞更多的事务执行,进而产生类似交通拥塞的现象,严重影响数据库的性能。因此,除了死锁检测技术外,还会结合降低死锁概率甚至避免死锁的技术,以获得更好的性能。

      3.3.1 Predicate Locks 谓词锁

      针对Update语句的谓词进行加锁,这样可以避免可能会产生死锁的事务并行执行,例如:
        UPDATE ... WHERE key = `hello`
        针对key='hello'
        加谓词锁,可以避免其他按照同样谓词条件(key='hello'
        )的UPDATE事务并行执行,也就避免了可能产生的死锁现象。
        在Coordinator节点加谓词锁,同样还可以起到死锁检测的效果,在多个Worker节点形成分布式死锁现象前,Coordiantor节点会先在谓词锁上形成死锁并被提前检测出来。
        Spanner/F1系统就采用了谓词锁技术来降低死锁现象的发生。

        3.3.2 Wait-Die or Wound-Wait

        给事务定义优先级的思路,可以认为先发起的事务(较小的事务ID)拥有较高的优先级,后发起的事务(较大的事务ID)拥有较低的优先级。当事务执行过程中,发生了先发起的事务被后发起的事务阻塞现象称为优先级反转,可以采用两种策略中的一种主动解锁:
        • Wait-Die:取消并重启先发起的优先级更高的事务;
        • Wound-Wait:取消并重启后发起的优先级更低的事务;
        一般来说,先发起的事务会更早的获取锁,因此发生优先级反转的概率不高,Wound-Wait策略的效率会更好些。
        Spanner/F1系统采用的是Wound-Wait策略。

        参考

        https://dzone.com/articles/how-citus-executes-distributed-transactions-on-pos
        https://www.citusdata.com/blog/2017/08/31/databases-and-distributed-deadlocks-a-faq/
        https://zhuanlan.zhihu.com/p/143504602
        http://matt33.com/2018/07/08/distribute-system-consistency-protocol/

        相关文章