NightPxy 个人技术博客

分布式中的数据一致性

Posted on By NightPxy

概述

总结一下到目前为止对分布式数据一致性的认识

数据一致性是任何系统的基本要求,这一点无关是否是分布式系统
分布式环境下的数据一致性更为重要也更为恶劣
重要性体现在

  • 分布式系统的容灾能力体现冗机切换上,其根本前提首先是保证数据一致
    所以保证了一致性就能自然而然的依靠副本获得可靠性
    恶劣性体现在
  • 系统节点可能永久故障,也可能暂时故障且一段时间后又重新恢复了
  • 网络请求的无序性,网络问题中断或者是网络问题的集群网络分化等等

数据一致性有两种大致的思路

  • 强一致性
    所有节点在任何时候的数据都是一致的
  • 弱一致性
    根据CAP理论,保证了容错性的前提下,可用性和一致性不可兼得,所以才有了BASE理论
    即不要求所有节点在任何时候的数据都是一致性,而是保证数据最终会一致的(这个最终一致性是针对某个节点而言,在外来看任何时候集群对外提供都是一致的数据)

强一致性

DTC 事务

DTC事务是一种强一致性解决方案
它由三种角色构成

  • 应用程序
  • 事务管理器
  • 资源管理器

DTC事务的核心是将单机的事务二段提交上升到分布式环境上

2PC

2PC也就是常说的二阶段提交

  • 协调者(发起事务的节点)发起事务 其它参与者接受事务执行,并同时记录Undo和Redo(单机中第一段提交)
    并将执行的结果提交到协调者
  • 协调者等待全部事务完毕后汇总执行结果
    如果全部成功,即要求各事务管理器提交(单机的第二段提交) 如果有任何失败,即要求各事务管理器回滚事务

2PC的缺点很明显

  • 事务过程同步阻塞
  • 无法真正保证数据一致
    协调者故障后,在一段时参与者会一直阻塞,在二段时会无法保证每个参与者都接收到提交请求(部分提交部分未提交)
  • 协调者的高可用也不能解决数据一致性问题
    如果协调者在发出二段提交请求时,协调者和收到这个请求同时故障,此时新的协调者上台将无法确认是否请求成功

3PC

3PC是2PC的改良版,也就是三阶段提交
改良之处在

  • 超时机制
    既可以节点可以在某个超时时间阈值来临时,以失败的形式解除阻塞
  • 一段中多一个阶段: CanCommit
    类似2PC之前,提前询问一次节点活性,期望依次来减少节点故障带来的问题

3PC实质上未能解决2PC数据不一致的问题
原因很简单,节点故障是贯穿分布式系统始终的问题,也就是提前询问的结果,代替不了实际提交过程中发生故障的结果
但3PC的确对2PC进行了改良.提前询问至少能提前感知节点故障,这样数据不一致仅会出现在提交过程故障中,减少了数据不一致出现的几率

弱一致性

算法

Paxos

Paxos算法,是一切弱一致性解决方案的鼻祖
从某种意义上说,所有的弱一致性解决方案都是Paxos或者Paxos针对某个具体场景的改良版

Paxos 的核心就六个字:大多数的共识,集群的回答等于大多数的回答
读取Data等于大多数节点中Data的值
写入Data是随机写入一个节点然后扩散它(随机写入的目的是为了去除特定点的单点问题)

Paxos代表着一种权衡
Paxos的容错能力N/2-1 在故障率超过一半后Paxos算法下将完全不能工作(因为始终不能凑齐大多数),也意味着者只要故障率不超过一半Paxos算法下集群将始终保持良好工作的

NWR

NWR,在我的理解是一种Paxos的改良版,
它的改良之处在于,不在固化大多数这个比率,而交由用户决定

NWR中,N代表总结点数,W代表写入节点数量,R代表读取节点数量
它的核心点有两个

  • 数据版本号
  • W+R>N
    举个例子说
    如果总结点是10,然后将数据写入3个节点中,那么任取8个节点就一定可以将最新数据读出来

Raft

NWR的读取节点太多基本不可实现,想想如果写入3副本,那么1000个节点就需要读取998个,这是一件基本不可能完成的事情(NWR往往伴随着集群片化)
Raft是从另一个角度去解决的问题 选主
无论是Paxos还是NWR都是一种无主模式,解决一切问题都依靠大多数解决
Raft的方案是,选一个主出来,无论读写都依靠主来提供,但是有主就必有单点问题.
单点问题的解决方案就是冗机切换,可冗机切换又会衍生两个问题

  • 冗机数据一致性
  • 脑裂问题

Raft的大致解决方案如下

  • 一切读写问题都由主来完成
    其它节点依靠对主数据的自行同步来完成数据扩散
  • 如果主挂掉,此时集群暂时不可用,必须等待主被选举出来
  • 立即发动选举投票,选出大多数公认的主
  • 选举完成后集群重新恢复运行
    如果此时旧主恢复,因为大多数已承认别的主而失去主的身份(杜绝脑裂)

所以Raft对这三个问题的回答如下

  • 主单点问题 : 重选主
  • 冗机数据一致性: 节点数据同步
  • 脑裂问题: **大多数承认的节点才能做主

时钟向量

时钟向量是解决另一大类问题的方案 节点数据扩散时,无法控制扩散时接受顺序.此时某个节点接受到扩散数据对自己更新时,如何抉择是更新还是不更新呢

假设 A,B,C三个节点写入
写入1时B挂掉了, 只写入了A,C.之后再写2时C挂掉.之后A彻底故障只留下了B,C.
持有完整数据的A已经彻底故障, B,C各自只持有其中更新的一段,对B和C而言甚至都不知道哪一段是先提交,此时C持有最新数据2,但B因为无法感知所以要求向C扩散更新1时,C仅凭更新要求是很难决定自己是更新还是不更新的,因为它无法判断这个1是在更新2之前更新的还是更新2之后更新的

时钟向量的核心是N维递增版本号(这个版本号可以有多种形式,事务ID,时间戳等等都可以)

  • 这个N维是指当前的N个节点
  • 每次发送数据都会带上这个数据的N维递增版本号
    这个N维版本号中,对自己这个版本号加一
  • 每个节点接受到数据时,会立即与手中持有该数据的时钟向量做比较
    如果较自己老则直接抛弃,说明自己手中持有更新的数据 如果自己较新则更新自己手中的数据并同时更新为新的时钟向量
  • 递增版本号是针对自节点而言.分布式环境下很难得出一个有意义的全局时钟

总结

Paxos,NWR,Raft和时钟向量都是对分布式数据弱一致性的解决算法思路

  • Paxos 的集群几乎不会有彻底故障,代价是系统延迟都非常长
  • NWR的集群一般需要片化(集群内的小集群),否则读写之间肯定有一个压力巨大
    代价就是集群片化导致的容错性降低(常规3副本就表示3个挂了就全挂了),远低于Paxos的超大容错能力
  • Raft虽然是延迟小,但一旦进入选举过程是不能对外提供服务的
  • 时钟向量中,是需要N维版本维护的,比较麻烦

在工程落地上,其实不太可能出现使用单一算法的情况,而代之多种算法公用的形式
下面根据一些著名框架总结一下关于这些算法的应用

工程落地

ZK

ZK的一致性是ZAB协议
ZAB协议带有Paxos,NWR,向量时钟等等痕迹

  • 所有的数据操作都有趋势递增的指定事务ID(时钟向量) ZK的事务ID是一个64位Long,分高低32位代表不同意思
    高32位代表Leader更新轮次,每一次Leader切换都将加一(旧leader发出的要求会被拒绝)
    低32位代表某个Leader在位过程中的数据顺序操作序号
  • 数据的写入由Leader完成(改良版2PC)
    客户端连接任意节点的写入,都将由该节点转发Leader完成
    Leader负责为该写入生成唯一事务ID,并立即向所有follower要求同步(2PC-一段)
    与2PC不同的是,不要求阻塞到follower全部ACK,而是有超过半数ACK即视为成功
    (follower本身也会一直运行尝试同步最新)
  • 数据的读取可以由任一节点完成(Raft会要求必须从主读)
    这带来的问题是读取数据的不一致性
    ZK的解决方案是统一数据视图,即保证客户端看见的是一致的,但不一定是最新的
    它的核心是由客户端读取时提交上一个事务ID,然后发起连接时如果节点检测到这个事务ID比自己新就会拒绝连接,客户端通过多节点重试直到遇见一个至少比自己当前新的节点进行读取
  • 选主过程
    对于Raft而言,最重要的一步就是选主(leader,follower)
    首先发起第一轮投票并持有该选票,第一轮投票固定选自己,而ZK的选票设计的非常巧妙 ZXID(事务ID)+NodeID(自己的集群节点ID)
    每个节点收到选票会与自己的选票对比(时钟向量合并)
    如果该投票的事务ID比自己大,则立即选对方为主并发出投票,并更新自己的选票
    如果该投票的事务ID比自己小,则抛弃对方选票依旧选择自己
    如果该投票的事务ID比自己大,则继续比较NodeID(唯一的所以必有结果),如果比自己大则选对方为主发出投票,如果自己大则继续选自己
    节点会持续汇总收到的投票结果,如果发现某个节点已经获得了大多数的选票,即试该节点为主.
    如果某节点发现自己获得了大多数的选票,也视自己为主
  • 节点新加入
    节点加入时,将首先联系leader完成数据同步后才会正式加入集群成为follower
  • 重选举
    当leader网络不可用或崩溃时,会触发重选举.
    而在leader正常运行过程中,如果发现与自己的心跳存活的follower不能保持一半时也会立即进行重选举(可能是脑裂了,但自己还不知道)
  • 数据恢复
    选主已特意倾向了拥有最新数据的节点为leader,所有follower只需对新leader做同步即可
    这个同步有两层意思
    保证自己的事务执行进度必然不小于leader
    保证自己的事务执行进度必然不大于leader(也就是回滚数据)
    同时follower会向leader报告自己的同步进度
    leader在确认收到大多数follower的ACK确认后,知道数据已至少同步到大多数节点中,开始正式对外提供服务

Kafka(10)

Kafka(10) 的分区副本同步也是需要一致性保证的
Kafka的读写都是依靠leader完成,只是有点区别的是,Kafka的读写过程类似有两个leader

  • 写leader
    就是通俗意义上集群leader,依靠ZK选举产生
  • 读leader
    是集群的某(任)一个节点,由Kafka随机选择作读leader(消费平衡器)
    由读leader负责收集分配负责提供数据的节点
    客户端再通过这些节点去访问读取数据

下面分开说

Kafka的主从间数据同步没有使用2PC(或者说2PC改良版),而是纯异步由follower从leader同步
这带来的问题是,follower的同步过程是不受控制的(极端情况可能一个同步到最新的节点也没有)
对此,Kafka中有一个非常重要的组件或者说机制叫LSR

  • LSR是一个集合概念,它是指到目前为止能跟得上(已同步到最新)的follower集合
    最新是一个阈值,即同步差小于某个单位
  • 如果一个follower暂时未能同步到最新,它将被踢出LSR中
    follower自身也始终保持对leader同步,如果同步到最新,又会被加入到LSR中
  • Kafka对LSR有一个最小保证 min.insync.replicas(1)
  • LSR是始终保持数据最新的节点
    leader的崩溃切换必从LSR内产生,这样新leader也能保持最新数据
    但是,LSR的最新始终是一个阈值概念,不是真正数据层面的最新,也就是leader故障后是可能丢失数据的,除非能时刻保证LSR是数据最新,当然这是会损失性能
    Kafka的决定是,交由用户自行选择保证性能(可用性)还是保证容错性

用户自行选择`request.required.acks

  • 0 表示不要求响应,不考虑任何写入失败
  • 1 表示只要leader写入成功即为成功
  • -1 表示leader必须等待LSR全部完成后才视为成功.如果LSR不能满足最小保证,视为失败

此外,任何时候如果数据就算到了leader,只要LSR没有同步完成,其对消费端也是不可见的
(不管用户选择什么可靠策略,只要数据写到了Kafka(Leader)中,必须在LSR全部完成后才会视为消费有效)

  • 读取(消费)在10版本中已经摆脱了ZK,有Kafka本身提供服务
    客户端(消费组)发起访问后,由Kafka随机在一个节点作为其消费平衡器
    这个消费平衡器类似读leader,客户端加入这个消费平衡器组后,由这个消费平衡器(读leader) 搜集分配具体的数据提供节点给客户端,客户端再通过给定的数据提供节点进行数据访问
  • 这个读leader即可以说是固定的也可以说是随机的
    固定是指,针对某一个消费组而言平衡器固化(始终是它,特殊情况会发生变更(Kafka消费平衡机制))
    随机是指,针对所有的消费组而言,平衡器类似是在集群中随机挑选一个节点

HDFS

HDFS,NN与DN都有自己的数据同步方案

NN同步

NameNode的同步是一种为了保持NN高可用同步
这里不谈NN是如何保证高可用的,只说NN与它故障部分之间的关于DN元数据信息的数据同步

  • NN高可用需要选出(NameNode,JournalNode) (依靠ZK节点注册)
  • NN数据的形式是以事务记录的形式存储于EditLog中,也就是说本身是有向量时钟的特性的
  • 每一个写入NN的数据写入除了写入EditLog之外,会立即向同步向所有的JournalNode写入,其中大多数访问成功即视为成功,NN才会运作到自己内存的元数据信息中
    JournalNode自身也始终保持对leader数据的同步
  • 在NN切换时,首先保证JournalNode之间保持大多数数据一致,在此之前集群不能对外提供服务
    这个一致是包括旧主NN的, 通过对Node最新的事务同步,完成大多数数据一致后完成切换,新NN切换成功

NN的脑裂解决需要单独说
Kafka的脑裂问题非常好解决,因为Kafka的leader是放在ZK的leader的节点树上,只要ZK解决了脑裂Kafka相应也解决了脑裂问题,代价就是Kafka需要始终依靠ZK来进行访问
NN虽然也用了ZK,但只是利用了ZK节点活性检测机制,NN的主是自行对外提供服务的,所以NN必须也解决脑裂问题
NN的脑裂解决非常粗暴
NN的切换是利用ZK节点活性检测来完成,所以每一个当过leader的NN都会在ZK上留下一个节点
而每一个尝试升主的NN在切到Active之前,会检测是有遗漏下来的历史NN节点,如果有则会先调用RPC去尝试将旧的leader-NN切为StandBy,如果失败则会再次通过SSH登录到目标机器去强行杀死NN进程,完成这一步后才会正式升为NN

DN同步

DN节点写入可以视为在NN管理下的集群片中的写入,因为有leader的存在,读取写入都非常简单

  • 首先可以视为副本数(默认3)个小节点组成的小集群中的写入
  • 写入时,写入第一个节点.并且是按照同步扩散的形式立即由第一个节点向其它节点扩散
  • 写入中如果发生异常,只要达到dfs.replication.min(1),既可以视为写入成功,由主(NN)完成余下的扩散任务
  • DN的写入可以视为一个规定了最小数量的强一致性写入,即必须保证对最小数量节点的写入成功

  • 读取时由NN提供一个已经确认数据正确的节点对外提供数据
    这个正确是指,写入成功会立即对文件进行一个签名,然后NN会对所有文件校验签名,符合签名即视为数据正确

SOA

SOA架构中,不同系统的数据也需要一致性保证,这里也顺便总结下
这里的一致性仅讨论系统间的数据一致性,不考虑系统本身的高可用多副本的一致性

SOA架构中,保证数据一致性的原则和手段如下

  • 事务自治
    即对某一个系统而言,单个数据处理步骤是自治的.不应由外部系统控制
    比如订单系统的下单接口,由订单系统自身自治(订单明细,订单日志…..)等系统内数据一致,外部系统不考虑
  • 幂等保证
    对某一个系统而言,其任意一个对外服务应该始终是幂等的
    因为网络,业务甚至异常情况,重复请求是无可避免的
  • 冲正补偿
    冲正补偿即某个操作的反操作.比如扣款->退款,下单->取消等等
    SOA系统通过对外提供冲正补偿操作来完成数据变更的最终一致性
  • 最大努力通知(双向通道)
    SOA系统间一般通过消息传递传递业务
    对于消息发送方而言,需要通过阶梯重试的机制确保目标系统收到请求,比如下单请求,会多次重试直到收到对方成功响应为止
    对于消息接受方而言,应提供查询服务来对外告知自己是否已经收到了该请求,比如订单查询接口
  • 定期校对
    定期校对是一种对于特别敏感的需要强力保证不得出错的手段
    比如账户余额,会视情况(按小时或天)定期将订单与余额流水对比来保证正确