分布式理论进阶大全

728 阅读17分钟

在分布式系统中,各个节点之间在物理上相互独立,通过网络进行沟通和协调。由于存在事务机制,可以保证每个独立节点上的数据操作可以满足ACID。但是,相互独立的节点之间无法准确的知道其他节点中的事务执行情况。所以从理论上讲,两台机器理论上无法达到一致的状态。如果想让分布式部署的多台机器中的数据保持一致性,那么就要保证在所有节点的数据写操作,要么全部都执行,要么全部的都不执行。所以,常规的解决办法就是引入一个“协调者”的组件来统一调度所有分布式节点的执行,分布式事务转换为多个本地事务,然后依靠阶梯重试等方式达到最终一致性。分布式问题的解决问题的路径可以总结为:业务规避->Base柔性事务->CP刚性事务,尽量使用最前面的方案。

分布式一致性分类

强一致性 (paxos raft zap zk)

这种一致性级别是最符合用户直觉的,它要求系统写入什么,读出来的也会是什么,用户体验好,但实现起来往往对系统的性能影响大。

弱一致性

这种一致性级别约束了系统在写入成功后,不承诺立即可以读到写入的值,也不久承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态。

最终一致性(DNS,eureka)

最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较推崇的模型。

CAP理论

一个分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容错性(P:Partition tolerance)这三个基本需求,最多只能同时满足其中两项

一致性 C

在分布式环境下,一致性是指数据在多个副本之间能否保持一致的特性。在一致性的需求下,当一个系统在数据一致的状态下执行更新操作后,应该保证系统的数据仍然处于一致的状态。

可用性 A

可用性是指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。这里的重点是"有限时间内"和"返回结果"。

分区容错性 P

分区容错性约束了一个分布式系统具有如下特性:分布式系统在遇到任何网络分区故障的时候,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。

  • CA:放弃分区容错性,加强一致性和可用性,就是传统的单机数据库的选择,放弃了分布式
  • AP:放弃强一致性,追求分区容错性和可用性,这是很多分布式系统设计时的选择
  • CP:刚性事物,放弃可用性,追求一致性和分区容错性,通常用在金融行业要求强一致性,面临性能上线,无法满足高并发的互联网场景

BASE理论

BASE是Basically Available(基本可用)、Soft state(软状态)和Eventually consistent(最终一致性)三个短语的缩写。BASE理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。接下来看一下BASE中的三要素:

  • 基本可用 基本可用是指分布式系统在出现不可预知故障的时候,允许损失部分可用性。注意,这绝不等价于系统不可用。比如:
    • 响应时间上的损失。正常情况下,一个在线搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障,查询结果的响应时间增加了1~2秒
    • 系统功能上的损失:正常情况下,在一个电子商务网站上进行购物的时候,消费者几乎能够顺利完成每一笔订单,但是在一些节日大促购物高峰的时候,由于消费者的购物行为激增,为了保护购物系统的稳定性,部分消费者可能会被引导到一个降级页面。
  • 软状态 软状态指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
  • 最终一致性 最终一致性强调的是所有的数据副本,在经过一段时间的同步之后,最终都能够达到一个一致的状态。因此,最终一致性的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。

分布式事务解决方案

  • 刚性事务:遵循ACID原则,强一致性
  • 柔性事务:遵循BASE理论,最终一致性;与刚性事务不同,柔性事务允许一定时间内,不同节点的数据不一致,但要求最终一致。
    • 补偿型事务-同步(TCC,Saga)
    • 通知型事务-异步(最大努力通知型,Mq事务消息)

2PC

基于XA协议的两阶段提交方案 第一阶段是表决阶段,所有参与者都将本事务能否成功的信息反馈发给协调者;第二阶段是执行阶段,协调者根据所有参与者的反馈,通知所有参与者,步调一致地在所有分支上提交或者回滚

3PC(TCC)

相比2pc,try锁的资源力度更小 事务开始时,业务应用会向事务协调器注册启动事务。之后业务应用会调用所有服务的try接口,完成一阶段准备。之后事务协调器会根据try接口返回情况,决定调用confirm接口或者cancel接口。如果接口调用失败,会进行重试。 TCC方案让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。 当然TCC方案也有不足之处,集中表现在以下两个方面: 对应用的侵入性强。业务逻辑的每个分支都需要实现try、confirm、cancel三个操作,应用侵入性较强,改造成本高。 实现难度较大。需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,confirm和cancel接口必须实现幂等。 上述原因导致TCC方案大多被研发实力较强、有迫切需求的大公司所采用。微服务倡导服务的轻量化、易部署,而TCC方案中很多事务的处理逻辑需要应用自己编码实现,复杂且开发量大。

基于消息的最终一致性方案(可靠消息最终一致,本地消息表)

消息一致性方案是通过消息中间件保证上、下游应用数据操作的一致性。基本思路是将本地操作和发送消息放在一个事务中,保证本地操作和消息发送要么两者都成功或者都失败。下游应用向消息系统订阅该消息,收到消息后执行相应操作。 消息方案从本质上讲是将分布式事务转换为两个本地事务,然后依靠下游业务的重试机制达到最终一致性。基于消息的最终一致性方案对应用侵入性也很高,应用需要进行大量业务改造,成本较高。

seata

image

AT

对业务的无侵入,拦截sql,出现脏写就需要转人工处理 用户只需关注自己的“业务SQL”,用户的 “业务SQL” 作为一阶段,Seata 框架会自动生成事务的二阶段提交和回滚操作

TCC

Try操作作为一阶段,负责资源的检查和预留,Confirm操作作为二阶段提交操作,执行真正的业务,Cancel是预留资源的取消 业务侵入性。TCC模式无AT模式的全局行锁,TCC性能会比AT模式高很多。

  • 允许空回滚:try超时,分布式回滚,触发cancel。否则会不断重试
  • 防悬挂控制:拒绝空回滚后的try操作,在实现TCC服务时,应当允许空回滚,但是要拒绝执行空回滚之后到来的一阶段Try请求。Cancel空回滚返回成功之前先记录该条事务xid或业务主键,标识这条记录已经回滚过.Try接口先检查这条事务xid或业务主键如果已经标记为回滚成功过,则不执行Try 的业务操作
  • 幂等控制:3个方法需保证幂等性,因为网络拥堵,超时,事务管理器可能会重试
  • 业务数据可见性控制:TCC服务的一阶段Try操作会做资源的预留,在二阶段操作执行之前,如果其他事务需要读取被预留的资源数据,那么处于中间状态的业务数据该如何向用户展示,需要业务在实现时考虑清楚;通常的设计原则是“宁可不展示、少展示,也不多展示、错展示”
  • 业务数据并发访问控制:TCC服务的一阶段Try操作预留资源之后,在二阶段操作执行之前,预留的资源都不会被释放;如果此时其他分布式事务修改这些业务资源,会出现分布式事务的并发问题

Saga

长事务,异步。原方法,补偿方法。允许空补偿,防悬挂。不具备隔离性,通过业务角度保证我方数据不出问题 一阶段提交本地数据库事务,无锁,高性能; 参与者可以采用事务驱动异步执行,高吞吐; 补偿服务即正向服务的“反向”,易于理解,易于实现

XA

分布式强一致性的解决方案,但性能低而使用较少

选举共识算法

image

  • 拜占庭将军问题 在存在消息丢失的不可靠信道上,试图通过消息传递的方式达到一致性是不可能的 解决方案:非对称加密技术

  • Paxos协议 难于理解 应用:ZooKeeper

  • ZAB原子消息广播协议,因为paxos太过于复杂,zk基于paxos实现了ZAB协议 把有最大事务ID的节点选为主,类似二阶段提交 原子广播过程: 1、leader从客户端接收一个请求 2、leader生成一个新事务,并为此事务生成一个提议-事务Id,接着把事务通知给其它follower;follower接收到此请求后,把事务ID加入一个队列里,并执行事务,最后响应leader 3、leader收到半数确定,发送提交请求;follower在提交此事务前,会判断此事务ID是不是比队列中所有的事务ID都小,如果是则提交;如果不是,等待更小的事务的提交命令。 ZAB除了事务ID,还在一个LeaderID

  • Raft算法 应用:Etcd、TiDB、Consul,Kubernetes 类似民主投票,核心思想是“少数服从多数”。 跟随者(follower),候选人(candidate)和领导者(leader) 随机时钟选主,master不断给其他单元发送心跳包,如果有请求过来,心跳包里同时把数据 发过去。只解决了节点故障问题,不支持作恶节点 集群中的一个节点在某一时刻只能是这三种状态的其中一种,这三种角色是可以随着时间和条件的变化而互相转换的。 raft算法主要有两个过程:一个过程是领导者选举,另一个过程是日志复制,其中日志复制过程会分记录日志和提交数据两个阶段。raft算法支持最大的容错故障节点是(N-1)/2,其中N为集群中总的节点数量

  • pbft算法 支持作恶节点,最大的容错故障节点是(N-1)/3

共识

如果一个节点当前的数据是 X,现在有了 add+1 的操作日志来了,那么现在的状态就是 X+1,好了,状态(X)有了,变化(操作日志)有了,这就是状态机。 共识,现实:共识就是一群人对一件或者多件事情达成一致的看法或者协议。 计算机世界:多个节点对某个数据达成一致共识。多个节点对多个数据的顺序达成一致共识 共识模型:主从同步,多数派

  • POW:Proof of Work 工作量证明,用力气干活
  • POS:Proof of Stake股权证明,钱生钱
  • DPOS,Delegated Proof of Stake 委任权益证明, 每一个持有数字货币的人进行投票,选举各自支持的机构或是个人,得票数靠前的成为超级节点
  • 总结: POW非常强调去中心化;POS是表面上看去中心化,实则很容易中心化;DPOS 则是有一个明显的中心,通过带来部分中心,来得到效率的提升

复制、分片、路由

  • 复制(replication):将同一份数据拷贝到多个节点(主从master-slave方式、对等式peer-to-peer)
  • 分片(sharding):将不同数据存放在不同节点 如果想增加系统的读取性能,复制,增加slave节点即可; 如果想提升写入性能,则对数据进行分片。
  • 路由:
  1. 哈希分片,点查询,采用哈希函数建立Key-Partition映射(大多数KV数据库都支持此方式)

    1. Round Robbin 俗称哈希取模算法,H(key) = hash(key) mode K(其中对物理机进行从0到K-1编号,key为某个记录的主键,H(key)为存储该数据的物理机编号)。好处是简单,缺点是增减机器要重新hash,缺乏灵活性。它实际上是将物理机和数据分片两个功能点合二为一了,因而缺乏灵活性。

    2. 虚拟桶 membase在待存储记录和物理机之间引入了虚拟桶,形成两级映射。其中key-partition映射采用哈希函数,partition-machine采用表格管理实现。新加入机器时,只需要将原来一些虚拟桶划分给新的机器,只要修改partition-machine映射即可,具有灵活性

    3. 一致性哈希 一致性哈希是分布式哈希表的一种实现算法,将哈希数值空间按照大小组成一个首尾相接的环状序列,对于每台机器,可以根据IP和端口号经过哈希函数映射到哈希数值空间内。通过有向环顺序查找或路由表(Finger Table)来查找。对于一致性哈希可能造成的各个节点负载不均衡的情况,可以采用虚拟节点的方式来解决。一个物理机节点虚拟成若干虚拟节点,映射到环状结构的不同位置。

  2. 范围分片,范围查询

本地消息表-事务消息场景

image 第一,订单系统需要创建一个新订单,订单关联的商品就是购物车中选择的那些商品。

第二,创建订单成功后,购物车系统需要把订单中的这些商品从购物车里删掉。 创建订单和清空购物车这两个数据更新操作需要保证,要么都成功,要么都失败。但是,清空购物车这个操作,它对一致性要求就没有扣减优惠券那么高,订单创建成功后,晚几秒钟再清空购物车,完全是可以接受的。只要保证经过一个小的延迟时间后,最终订单数据和购物车数据保持一致就可以了。

本地消息表非常适合解决这种分布式最终一致性的问题。我们一起来看一下,如何使用本地消息表来解决订单与购物车的数据一致性问题。

本地消息表的实现思路是这样的,订单服务在收到下单请求后,正常使用订单库的事务去更新订单的数据,并且,在执行这个数据库事务过程中,在本地记录一条消息。这个消息就是一个日志,内容就是“清空购物车”这个操作。因为这个日志是记录在本地的,这里面没有分布式的问题,那这就是一个普通的单机事务,那我们就可以让订单库的事务,来保证记录本地消息和订单库的一致性。完成这一步之后,就可以给客户端返回成功响应了。 然后,我们再用一个异步的服务,去读取刚刚记录的清空购物车的本地消息,调用购物车系统的服务清空购物车。购物车清空之后,把本地消息的状态更新成已完成就可以了。异步清空购物车这个过程中,如果操作失败了,可以通过重试来解决。最终,可以保证订单系统和购物车系统它们的数据是一致的。

创建了订单,没有清理购物车;订单没创建成功,购物车里面的商品却被清掉了。 创建订单和发送消息这两个步骤要么都操作成功,要么都操作失败,不允许一个成功而另一个失败的情况出现

事务消息适用的场景主要是那些需要异步更新数据,并且对数据实时性要求不太高的场景。比如我们在开始时提到的那个例子,在创建订单后,如果出现短暂的几秒,购物车里的商品没有被及时清空,也不是完全不可接受的,只要最终购物车的数据和订单数据保持一致就可以了

自己设计一个可靠消息平台

消息独立子系统

消息状态: 待确认,可发送,已完成

接口设计:

  • 发送方 消息状态查询接口

  • 接收方 保证接口幂等性

  • 消息独立子系统

  • 前台接口

  1. 直接发送消息
  2. 存储并发送消息
  3. 可靠消息发送
  4. 预存储消息-待确认
  5. 确认并发送消息-可发送
  6. 更新消息状态为-已完成
  • 运营接口
  1. 分页查询消息数据
  2. 重发某个消息队列中的全部已死亡的消息
  3. 根据MqMessageEntity重发消息
  4. 将消息标记为死亡消息
  5. 根据消息ID获取消息
  • 定时任务接口
  1. 根据消息ID删除消息
  2. 根据msgObjId重发某条消息
  • 定时任务
  1. 消息发送方-消息状态确认后台线程:分页查询5分钟前asc-待确认消息,根据业务扩张字段调用发送方消息状态确认接口。 成功(确认并发送消息) 失败:删除消息
  2. 消息接收方-消息恢复系统后台线程:分页查询5分钟前asc-可发送消息, 判断发送次数(最大6次) 根据次数对应的时间间隔(3,5,10,15,30,60), 重新发送(根据msgObjId重发某条消息)

image

image

参考: