《数据密集型应用系统设计》笔记-6-共识问题

前言

分布式系统存在许多可能出错的场景。处理错误的两种思路:停机,向用户提示出错信息;不停机,依靠系统容错机制继续运行。类似于数据库提供的事务功能,分布式系统也可以在系统内部提供一定的容错功能。
分布式系统最重要的抽象之一就是共识:所有节点就某项提议达成一致。本章的内容分两部分:介绍系统能力的边界(哪些错误可以处理,哪些不行);研究共识问题的相关算法。

一致性保证

这里一致性的概念类似于数据库的最终一致性。分布式一致性模型与多种事务隔离级别有相似之处。但有区别:事务隔离是为了处理并发执行事务的各种临界条件,而分布式一致性则主要针对延迟和故障等问题来协调副本之间的状态,最终目的是希望分布式系统的所有节点处于正确的状态
本章的主要内容:

  • 线性化,这是最强的一致性模型
  • 分布式系统中的时间顺序问题,特别是因果关系和全局顺序
  • 分布式事务与共识:探索如何自动提交分布式事务和共识问题

可线性化

客户端同时查询分布式系统的两个副本时,可能会得到两个不同的答案。如果系统能够向上层提供只有单个副本的假象,情况会简化很多。这就是可线性化(也称原子一致性,强一致性)的思想。可线性化还要求系统提供的副本是最新的那一个(尽力而为),且一旦返回了新值,系统不能再返回旧值。可线性化不同于事务中的可串行化,可线性化没有事务保证,无法避免写倾斜问题。

线性化的依赖条件(使用场景)

加锁与主节点选举
提供协调者服务的系统(如Apache ZooKeeper,etcd)等通常用来实现分布式锁和主节点选举。这类服务必须保证可线性化,否则极易造成系统混乱(如果不会导致混乱,那么可能并不需要此类服务)。
约束与唯一性保证
分布式数据库主键约束,注册用户名不能重复等。
跨通道时间依赖
分布式系统的线性化违例之所以被注意到,是因为系统中存在其他的通信渠道。通常在线性化要求严格的业务场景,可以控制只存在一个通信通道。

实现线性化系统

线性化的本质是"系统表现得好像只有一个数据副本",而系统容错则要求系统不止一个数据副本,这之间就存在冲突。
系统容错最常见的方式是复制:

  • 主从复制:部分支持可线性化。主节点负责写入,如果都从主节点上读取,则可以保证线性化,这个前提是确定哪一个节点是主节点,这也是一个共识问题。
  • 共识算法:可线性化。共识算法可以安全地实现线性化存储,这些系统包括ZooKeeper,etcd等。
  • 多主复制:不可线性化。
  • 无主复制:可能不可线性化,取决于quorum配置。例如LLW肯定是非线性化的(墙上时钟问题)。由于它缺乏标准,默认它不能线性化比较好。

线性化的代价

CAP理论
不要求线性化的应用更能容忍网络故障。如果应用要求线性化,那么在网络发生故障后,就必须等待网络修复,服务不可用。如果不要求线性化,那么断开连接之后,每个副本可以独立处理请求,此时服务可用,但结果行为不符合线性化。

实际很少有系统真正满足线性化。现代多核CPU上的内存就是非线性化的:CPU核拥有独立的cache和寄存器,内存访问受限进入cache系统,cache系统和主存是异步刷新。放弃可线性化的目的就是为了性能。
如果想要满足可线性化,那么读写请求的响应时间至少与网络延迟成正比,线性化读写性能非常差。所以目前还不存在特别有效的线性化实现方案(但存在比可线性化弱的模型)。

顺序保证

顺序、排序、可线性化、共识之间存在关联,本质上是一个问题

顺序与因果关系

顺序的重要作用是它能维持系统的因果关系
全序关系:对于任意两个元素,总是可以指出哪个更大则称元素集合满足全序关系。
不符合全序的集合称为偏序关系
在一个可线性化的系统中,存在全序操作关系。对于任何两个操作,总是可以指出哪个操作在先。而对于存在并发关系的系统,并发意味着时间线会出现分支和合并,而不同分支上的操作无法直接比较。
例:git分布式版本控制系统,它的版本历史非常类似于因果关系图,通常情况下,提交会以直线形式呈现,但有时会产生分支(特别是多人同时在一个项目上工作时),当同时有多个提交时就需要进行合并。
可线性化与因果序是什么关系?
答:可线性化一定意味着因果关系,任何可线性化的系统都将正确地保证因果关系。可线性化不是保证因果关系的唯一途径。因果一致性可以认为是不会由于网络延迟而显著影响性能,又能对网络故障提供容错的最强的一致性模型。在许多情况下,系统真正需要的是因果一致性。

序列号排序

为了兼顾性能,想单独只实现因果一致性十分困难,不切实际。使用序列号或逻辑时间戳来排序事件。序列号排序是保证因果一致性的一个手段,可以按照与因果关系一致的顺序来创建序列号。
在主从复制模型中,由于存在唯一主节点,因此可以由主节点提供序列号服务。但由此带来的问题是主节点的压力会很大,在主从切换时也会导致混乱。对于不存在唯一主节点的系统,可以由两种做法:

  • 每个节点生成的序列号有独立的特征。比如某节点只生成奇数序号,或只生成1-1000的序列号。
  • 把物理时钟附加到每个操作上。

上述两种做法都无法保证正确捕获跨节点操作的顺序:第一种做法根本上不支持跨节点比较,第二种做法依赖于节点的物理时钟是否同步。
解决该问题的方法是利用Lamport时间戳。
Lamport时间戳
在这种算法中,每个节点有节点id以及一个单调增长的计数器max。Lamport时间戳由id+max组成。每个节点和每个客户端都跟踪迄今为止所见到的最大计数器值。在节点间的每个请求中都附带节点计数器max的值,如果节点发现请求内嵌的计数器值大于自身的max值,则更新max值为该最大值。由于请求本身具有因果关系顺序,那么节点收到请求时的时间戳也会具有因果一致性。

在这里插入图片描述
例如创建并写入1000至账户A,然后从账户A转账500至账户B。如果这三个操作分别发生在节点1,2,3上,那么过程是:

  • 请求内嵌计数器max=0
  • 创建账户请求(max=0)发送至节点1,此时 m a x 1 = 99 max_1=99 max1=99,那么返回计数器值 m a x = m a x 1 = 100 max=max_1=100 max=max1=100
  • 写入请求( m a x = 100 max=100 max=100)发送到节点2,更新 m a x 2 = 101 max_2=101 max2=101,并返回 m a x = m a x 2 = 101 max=max_2=101 max=max2=101
  • 转账请求至节点3,此时 m a x 3 = 200 max_3=200 max3=200,那么返回 m a x = m a x 3 = 201 max=max_3=201 max=max3=201

从上述过程来看,无论节点1,2,3是否同步,整个请求的计数器值都是递增的,即确保了因果顺序。本质上Lamport时间戳能够保持顺序的原因是它利用了客户端充当协调者,由于客户端发送请求(用户操作)的顺序是有序的,因此保证了数据库的因果一致性。
但从上面的例子也可以看出,仅仅使用Lamport时间戳不足以解决问题,例如节点2处理写入请求时,假设它并未同步到节点1的创建账户数据,它如何处理写入请求呢?再比如节点1在创建账户A时如何知道其他节点没有已创建的同名账户呢?

全序关系广播

全序关系广播通常指节点之间交换消息的某种协议,它需要满足两个基本安全属性:

  • 可靠发送:没有消息丢失,如果消息发送到了某一个节点,则它一定要发送到所有节点
  • 严格有序:消息总是以相同的顺序发送给每个节点

ZooKeeper和etcd这样的共识服务实际就实现了全序关系广播:全序关系广播和共识之间有密切联系。
状态机复制原则:如果系统的每个副本都按相同的顺序处理消息,那么所有副本可以保持状态一致。

分布式事务与共识

原子提交与两阶段提交

问题来源:2PC(two-phase commit)是一种共识算法。事务的原子提交需要保证事务要么成功,要么全部失败。在分布式系统中,如果某几个节点提交成功,其他节点提交失败,那么节点间就会变的不一致。这里需要强调,事务提交成功就需要保证持久化,为了同提交失败的节点保持一致而"回滚"已提交事务不可行。
两阶段提交(two-phase commit,2PC)是一种在多节点之间实现事务原子提交的算法,用来确保所有节点要么全部提交,要么全部中止。分布式数据库的经典算法之一。2PC在某些数据库内部使用,或者以XA事务形式(如JavaTransaction API)或SOAP Web服务WS-AtomicTransaction的形式提供给应用程序。
2PC算法的关键点是引入了单节点事务所没有的新组件:协调者(事务管理器)。协调者的实现显然必须具有共享的特征,它一般运行在请求事务相同的进程中(如JavaEE容器中)。常见的协调者如Narayana,JOTM,BTM或MSDTC。
2PC的主要过程:

  1. 所有节点执行事务,但不提交。
  2. 一阶段,协调者向所有节点发送请求,询问是否已准备好提交
  3. 二阶段,如果回复中有"否",协调者发送放弃事务请求;如果没有"否",发送提交请求。

2PC引入的协调者,看起来仍然无法保证第3步是否能够成功。要解答这个问题,需看2PC的详细步骤:

  1. 应用程序启动事务。向协调者申请事务ID,全局唯一。
  2. 应用程序在每个节点上执行单节点事务,将全局事务ID附加到事务上。这一阶段所有读写都发生在单节点内部,如果出现问题,则协调者可以协调所有节点安全中止事务。
  3. 当应用程序准备提交时,协调者向所有节点发送准备请求,并附带事务ID。如果出现问题,协调者通知所有节点放弃事务
  4. 节点回答准备请求,如果回答"是",则节点需要保证在任何情况下都可以提交事务。
  5. 提交点。协调者收到所有节点回复"是",作出明确的决定。协调者把决定写入到磁盘事务日志(为了崩溃恢复)。
  6. 决定写入磁盘后协调者向所有节点发送提交(或放弃)请求。如果请求出现失败/超时,则协调者必须一直重试,直到成功为止。

整个过程的关键点是两个承诺:节点在一阶段回答"是",则它必须保证后续肯定提交;协调者作出"决定",则它必须保证决定不可撤销且发送给所有节点。

如果协调者发生故障,在阶段二只发送了部分提交请求,那么没有收到提交请求的节点只能等待协调者恢复正常。恢复以后,协调者根据事务日志及事务ID可以继续工作。
2PC也称阻塞式原子提交协议

实践中的分布式事务

实践中的分布式事务存在如下问题:

  • 它是一个重要的安全保证;
  • 它无法可靠地执行上述"安全保证";
  • 性能低下.MySQL的分布式事务比单节点事务慢10倍以上。2PC性能下降的主要原因是为了防崩溃恢复而做的磁盘I/O(fsync)以及额外的网络往返开销。

目前有两种不同的分布式概念:
数据库内部的分布式事务:支持跨数据库节点的内部事务。比如MySQL Cluster,所有节点都运行着相同的数据库软件
异构分布式事务:存在两种或两种以上不同的参与者实现技术。比如某节点使用MySQL,而另一个节点使用非数据库系统(如消息中间件)。

Exactly-once消息处理:这是关于确保只执行一次的问题。例如一项事务结果之一是发送一封邮件,如果邮件服务器并不支持两阶段提交,那么整个事务就都不能支持两阶段提交。

XA交易

XA交易:X/Open XA(eXtended Architecture,XA)是异构环境下实施两阶段提交的一个工业标准。目前有一部分数据库(PostgreSQL,MySQL,DB2,SQL Server,Oracle)和消息队列(ActiveMQ,HornetQ,MSMQ,IBM MQ)支持XA。
XA不是一个网络协议,而是一个与事务协调者进行通信的C(语言) API。它也支持其他语言的API绑定。例如Java中,XA事务是由Java事务API(Java Transaction API,JTA)来实现,JTA支持多种JDBC(Java Database Connectivity)驱动和消息队列驱动(通过Java消息服务,JMS)。
XA运作过程:应用程序通过网络或客户端库函数 与 参与者(数据库、消息服务)节点进行通信。如果驱动程序支持XA,那么应用可以调用XA API来确定操作是否使用异构分布式事务,如果是,则发送必要的信息给数据库服务器。它还支持回调,这样协调者可以使用回调函数通知所有参与者执行准备/提交。
事务协调者需要实现XA API。目前没有标准实现。协调者通常也是一个API库,它与生产事务的应用程序运行在相同的进程中,并在本地磁盘的日志文件里记录事务的最终决定。

几个仍然存在的棘手问题:

  • 停顿时仍持有锁:节点出现故障以后,整个事务都会陷入停顿,与该事务有关的锁被占用。
  • 协调者如何从故障中恢复:如果协调中出现故障,会导致节点出现停顿,且节点重启仍然不能解决停顿问题。唯一的出路时让管理员手动决定执行提交或终止。手动解决难度也很大。许多XA的实现都支持某种紧急情况下的启发式决策:违背原子性的协议,由参与者节点作出单方面的决定。
  • 分布式事务的限制:协调者实际上也是存储系统,如果不支持复制,那么它容易成为系统的单点故障(它的故障导致系统整个故障);协调者的日志是可靠系统的重要组成部分,它存储在应用服务器,这里产生矛盾:应用服务器是无状态的(可以随意迁移),而协调者日志是有状态的(不能随意迁移);协调者需要兼容各种数据系统,因此通常是没有实现各种数据库的功能(如死锁检测,SSI等);对于数据库内部的分布式事务(不是XA),限制少很多。

支持容错的共识

共识问题的形式化描述:一个或多个节点可以提议某些值,由共识算法来决定最终值。如多个顾客购买最后一个座位的问题
共识算法必须满足以下性质:

  • 协商一致性:Uniform agreement,所有节点都接受相同的协议
  • 诚实性:Integrity,所有节点不能反悔,对一项提议不能有两次决定
  • 合法性:Validity,如果决定了值v,则v一定由某个节点所提议的
  • 可终止性:Termination,节点如果不崩溃则最终一定可以达成协议.它强调必须完成共识,而不是原地空转。这属于容错属性。

Uniform agreement和Integrity是核心思想。Validity是有效属性,可以排除无意义的方案。Termination是容错属性。Termination暗含着假定节点发生崩溃以后就彻底消失,否则,共识算法需要等待节点恢复,那么就可能破坏Termination。这属于共识算法的默认策略,不是指现实的节点能否恢复(即使能恢复,共识算法也认为其已经消失)。共识算法也假定节点不会出现拜占庭错误。

共识算法与全序广播

最著名的容错式共识算法包括VSR,Paxos,Raft,Zab。他们的实现思想是:决定了一系列的值,然后采用全序关系广播算法。全序关系广播的要点是消息按照相同的顺序发送到所有节点,有且只有一次。

Epoch和Quorum

使用投票算法时,需要所有人都对裁定者达成共识,而选举"裁定者"也是一个共识问题,这就陷入了循环问题。现在一般使用弱化保证:协议定义一个世代编号(epoch number,对应Paxos的ballot number,VSP view number,Raft term number),并保证每个epoch里,主节点时唯一确定的。
在主节点作出决定前,它会检查是否存在比它本身更高的epoch号码。检查的过程是将提议发送给所有其他quorum节点投票决定。
这里有两轮投票:1.选举主节点 2.对主节点提议投票。其中关键点是参与两轮投票的quorum必须有重叠。

共识算法局限性
  • 投票过程是同步的,如果节点发生切换,它会丢失投票数据。
  • 共识体系需要严格的多数节点才能运行。至少5个节点才能容忍2个节点故障。
  • 共识算法假定了一组固定单于投票的节点集合,这意味着不能动态添加或删除节点。
  • 共识系统通常依靠超时机制检测节点失效。这也导致共识算法对于网络问题特别敏感。

成员与协调服务

ZooKeeper或etcd项目通常被称为"分布式键值存储"或"协调与配置服务"。它们对外提供的API和数据库非常相像。应用开发者很少直接使用ZooKeeper,绝大多数情况是其他项目间接地依赖于ZooKeeper,如HBase,Hadoop YARN,OpenStack Nova,Kafka等。
为什么这些项目需要ZooKeeper/etcd?
ZooKeeper/etcd主要针对保存少量、可完全载入内存的数据(也会写入磁盘以支持持久性)而设计。它们通常采用容错的全序广播算法在所有节点上复制数据从而实现高可靠。ZooKeeper的实现模仿了Google的Chubby分布式锁服务。它实现了全序广播(因此实现了共识),还具有以下特性(功能):

  • 线性化的原子操作:使用原子比较-设置操作,可以实现加锁服务。分布式锁通常实现为一个带有到期时间的租约。
  • 操作全序:ZooKeeper的fencing令牌需要确保每次加锁时数字总是单调增加的。ZooKeeper在实现该功能时,采用了对所有操作执行全局排序,然后为每个操作赋予一个单调递增的事务ID(zxid)和版本号(cversion)
  • 故障检测:客户端与ZooKeeper节点维护一个长期会话,周期性地交换心跳信息。长时间的心跳停止才被视为节点失效,同时节点所持有的锁全部设置为释放。ZooKeeper称之为ephemeral nodes 即临时节点。
  • 更改通知:所有客户端都可以从ZooKeeper查询当前集群的基本状态,如是否有节点加入或推出。客户端只需订阅相关信息,就可以收到对应的通知。
节点任务分配

ZooKeeper/Chubby的几个应用场景:

  • 系统有多个流程或实例,且需求其中一个实例充当主节点,当主节点失效时进行主节点切换。
  • 对于一些分区资源,需要决定将哪个分区分配个哪个节点。当新节点加入时执行"分区再平衡"。

上述任务可以借助ZooKeeper的原子操作,ephemeral nodes(故障检测)和通知机制来实现。

如果集群的数量成百上千,那么进行多数者投票将非常低效。ZooKeeper通常是在固定数量的节点(3~5)上进行投票。ZooKeeper提供一种将跨节点协调服务(共识、操作排序、故障检测)专业外包的方式。
通常ZooKeeper所管理的数据变化非常缓慢,它不适合保存那些应用实时运行的状态数据,如果有这类需求,可考虑其他工具(如Apache BooKeeper)。

服务发现

ZooKeeper/etcd/Consul还可用于服务发现,只需要利用ZooKeeper的注册/订阅即可实现。但服务发现是否需要共识还缺乏统一认识,例如DNS也具有服务发现功能。

成员服务

成员服务是用来确定当前哪些节点处于活动状态并属于集群的有效成员。一个节点是否失效不能由节点自身决定,而是需要所有节点投票达成共识。即使共识的决定发生误判,只要所有成员都认可该决定,也不会产生问题。

小结

本章主要讨论的问题是"共识"。不支持"共识"的分布式系统,通常会产生冲突。如果冲突可接受或有相应的处理策略,那么系统也可以不支持共识。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值