一致性与共识

一致性与共识

分布式系统的不可靠性是其内在属性,为了应对这种不可靠性,我们必然会进入到一致性、共识及分布式事务的领域。

一致性

在分布式系统中,我们采用多机器进行分布式部署的方式提供服务,而为了保证系统的可用性与性能,我们必须将数据复制到分布式部署的多台机器中,以达到如下的目的:

  • 消除单点故障,防止系统由于某台(些)机器宕机导致的不可用;
  • 通过负载均衡技术,能够让分布在不同地方的数据副本全都对外提供服务,从而有效提高系统性能。

但是分布式系统引入复制机制后,不同的数据节点之间由于网络延时等原因很容易产生数据不一致的情况,譬如我们常常会面临以下具体的场景:

  • 如果你在同一时刻查看两个数据库节点,则可能在两个节点上看到不同的数据,因为写请求在不同的时间到达不同的节点。无论数据库使用何种复制方法(单主复制,多主复制或无主复制),都会出现这些不一致情况。
  • 比如在集中式系统中,有一些关键的配置信息,可以直接保存在服务器的内存中,但是在分布式系统中,如何保存这些配置信息,又如何保证所有机器上的配置信息都保持一致,又如何保证修改一个配置能够把这次修改同步到所有机器中,就是存在的问题。
  • 在集中式系统中,进行一个同步操作要写同一个数据的时候,可以直接使用事务与锁来管理保证数据的 ACID。但是,在分布式系统中如何保证多台机器不会同时写同一条数据。

数据复制面临的主要难题也是如何保证多个副本之间的分布式一致性,即分布式多个存储节点情况下怎么保证逻辑上相同的副本能够返回相同的数据,保证关联数据之间的逻辑关系是否正确和完整。如何能既保证分布式一致性,又保证系统的性能,是每一个分布式系统都需要重点考虑和权衡的。

分布式一致性模型和我们之前讨论的事务隔离级别的层次结构有一些相似之处。尽管两者有一部分内容重叠,但它们大多是无关的问题:事务隔离主要是为了,避免由于同时执行事务而导致的竞争状态,而分布式一致性主要关于,面对延迟和故障时,如何协调副本间的状态。

在数据库系统中通常用事务,即访问并可能更新数据库中各种数据项的一个程序执行单元,来保证数据的一致性和完整性。而大多数复制的数据库至少提供了最终一致性,这意味着如果你停止向数据库写入数据并等待一段不确定的时间,那么最终所有的读取请求都会返回相同的值。换句话说,不一致性是暂时的,最终会自行解决(假设网络中的任何故障最终都会被修复)。最终一致性的一个更好的名字可能是收敛(convergence),因为我们预计所有的复本最终会收敛到相同的值。然而,这是一个非常弱的保证,它并没有说什么什么时候副本会收敛。在收敛之前,读操作可能会返回任何东西或什么都没有。例如,如果你写入了一个值,然后立即再次读取,这并不能保证你能看到刚跟写入的值,因为读请求可能会被路由到另外的副本上。

在分布式系统与数据库等技术领域中,一致性都会频繁地出现,但是在不同的语境和上下文中,它其实代表着不同的东西:

  • 在事务的上下文中,比如 ACID 里的 C,指的就是通常的一致性(Consistency),即对数据的一组特定陈述必须始终成立,即不变量(invariants)。具体到分布式事务的上下文中这个不变量是:所有参与事务的节点状态保持一致:要么全部成功提交,要么全部失败回滚,不会出现一些节点成功一些节点失败的情况。
  • 在分布式系统的上下文中,例如 CAP 里的 C,实际指的是线性一致性(Linearizability),即多副本的系统能够对外表现地像只有单个副本一样(系统保证从任何副本读取到的值都是最新的),且所有操作都以原子的方式生效(一旦某个新值被任一客户端读取到,后续任意读取不会再返回旧值)。
  • 一致性哈希、最终一致性这些名词里的一致性也有不同的涵义。

在分布式系统中,我们常说的一致性模型有线性一致性、因果一致性、最终一致性等。线性一致性能使多副本数据看起来好像只有一个副本一样,并使其上所有操作都原子性地生效,它使数据库表现的好像单线程程序中的一个变量一样;但它有着速度缓慢的缺点,特别是在网络延迟很大的环境中。因果性对系统中的事件施加了顺序(什么发生在什么之前,基于因与果)。与线性一致不同,线性一致性将所有操作放在单一的全序时间线中,因果一致性为我们提供了一个较弱的一致性模型:某些事件可以是并发的,所以版本历史就像是一条不断分叉与合并的时间线。因果一致性没有线性一致性的协调开销,而且对网络问题的敏感性要低得多。

共识

另一方面,共识(Consensus)则是让所有的节点对某件事达成一致,一旦达成共识,应用可以将其用于各种目的。例如,假设你有一个单主复制的数据库。如果主库挂点,并且需要故障切换到另一个节点,剩余的数据库节点可以使用共识来选举新的领导者。在这个过程中重要的是只有一个领导者,且所有的节点都认同其领导。如果两个节点都认为自己是领导者,这种情况被称为脑裂(split brain),且经常导致数据丢失。正确实现共识有助于避免这种问题。

但即使捕获到因果顺序(例如使用兰伯特时间戳),我们发现有些事情也不能通过这种方式实现:我们需要确保用户名是唯一的,并拒绝同一用户名的其他并发注册;如果一个节点要通过注册,则需要知道其他的节点没有在并发抢注同一用户名的过程中。这个问题引领我们走向共识。分布式共识问题,简单说,就是在一个或多个进程提议了一个值应当是什么后,采用一种大家都认可的方法,使得系统中所有进程对这个值达成一致意见。

达成共识意味着以这样一种方式决定某件事:所有节点一致同意所做决定,且这一决定不可撤销。共识问题通常形式化如下:一个或多个节点可以提议(propose)某些值,而共识算法决定采用其中的某个值。在保证分布式事务一致性的场景中,每个节点可以投票提议,并对谁是新的协调者达成共识。譬如 Raft 算法解决了全序广播问题,维护多副本日志间的一致性,其实就是让所有节点对同全局操作顺序达成一致,也其实就是让日志系统具有线性一致性。因而解决了共识问题。

我们可以发现很广泛的一系列问题实际上都可以归结为共识问题,并且彼此等价。比如说选主(Leader election)问题中所有进程对 Leader 达成一致;互斥(Mutual exclusion)问题中对于哪个进程进入临界区达成一致;原子组播(Atomic broadcast)中进程对消息传递(delivery)顺序达成一致。这些等价的问题包括:

  • 线性一致性的 CAS 寄存器:寄存器需要基于当前值是否等于操作给出的参数,原子地决定是否设置新值。
  • 原子事务提交:数据库必须决定是否提交或中止分布式事务。
  • 全序广播:即保证消息不丢失,且消息以相同的顺序传递给每个节点。
  • 锁和租约:当几个客户端争抢锁或租约时,由锁来决定哪个客户端成功获得锁。
  • 成员/协调服务:给定某种故障检测器(例如超时),系统必须决定哪些节点活着,哪些节点因为会话超时需要被宣告死亡。
  • 唯一性约束:当多个事务同时尝试使用相同的键创建冲突记录时,约束必须决定哪一个被允许,哪些因为违反约束而失败。

在分布式系统中,我们常常同时讨论分布式事务与共识,这是因为分布式事务本身的一致性是通过协调者内部的原子操作与多阶段提交协议保证的,不需要共识;但解决分布式事务一致性带来的可用性问题需要用到共识。为了保证分布式事务的一致性,分布式事务通常需要一个协调者(Coordinator)/事务管理器(Transaction Manager)来决定事务的最终提交状态。但无论 2PC 还是 3PC,都无法应对协调者失效的问题,而且具有扩大故障的趋势。这就牺牲了可靠性、可维护性与可扩展性。为了让分布式事务真正可用,就需要在协调者挂点的时候能赶快选举出一个新的协调者来解决分歧,这就需要所有节点对谁是领导者达成共识(Consensus)。

Links