分布式技术原理与算法解析 01 - 分布式协调与同步

关于

  • 电商系统:最看重吞吐量,为了更多的处理用户访问和订单业务
  • IoT:最看重资源占用率,在某些设备上资源都是KB级的
  • 电信业务:最看重响应时间、完成时间、可用性,保证通话质量
  • HPC:最看重加速比,这种计算特带是耗时长
  • 大数据:最看重加速比,处理时间较HPC短,但也到达小时级
  • 云计算:最看重操作耗时、系统本身资源开销
  • 区块链:最看重吞吐量和完成时间

分布式互斥

集中式算法

例如使用redis的 SETEX 命令,使用zookeeper、etcd等。协调者可能成为性能瓶颈,容易引发单点故障。应当使用主备备份。

分布式算法

当访问临界资源时,先向其他所有程序发送一条请求消息,在接收到所有程序返回的同意消息后才可访问临界资源。请求消息包含请求的资源、请求者id、发起时间。

程序123共享资源A,程序1和程序3分别在时刻8和时刻12要访问资源。于是分别在相应时刻向其他程序发送申请请求。

程序2暂时不访问,所以同意1和3的请求。1比3提出的时间更早,于是3同意1的申请,并等待1返回同意

程序1接收到其他所有程序的同意消息后,使用资源A并结束后释放,向请求队列中要使用资源A的程序3发送同意请求,并将其中队列中删除。程序3接收到1返回的同意消息后获得权限,能够访问A

访问一次临界资源要发送 n-1 次请求、接收 n-1 次响应,至少 2*(n-1) 次交互,可能带来“信令风暴”。且一台机器故障整个系统不可用。

通信成本高,可用性低。其适合节点少且变动不频繁的系统,如P2P结构系统。Hadoop的HDFS文件系统就应用了分布式算法。

令牌环算法

所有程序组成一个环,令牌按照某个方向在程序间传递。拿到令牌才能访问资源,若不需要则直接传递给下一个。

单个程序有更高的通信效率,一个周期内每个程序都能访问到资源。但是即使系统中只有一个节点需要访问资源,令牌也要在所有节点中反复传递。令牌环算法非常适合通信模式为令牌环方式的分布式系统,如移动自组织网络、无人机通信。

集中式和单点式都存在单点故障问题,在令牌环算法中一个程序故障后,直接将令牌传递给故障程序的下一个,从而解决单点故障问题。但这要求每个程序都能记住环中所有参与者。适用于系统规模较小且每个程序都高频率使用临界资源且时间较短的场景

大规模系统中分布式互斥算法

两层结构的分布式令牌环算法,将整个网络组织成两层结构,在全局环有一个全局令牌,在局部环上有一个局部令牌。

分布式选举

主节点负责整个分布式集群中节点的协调和管理,保证节点的有序运行和一致性。

Bully算法:基于序号

简单直接,选取ID最大节点为主节点。初始时所有节点都是普通节点,选举成功后仅当主节点故障或失去联系后才重选主节点。选举中用到3种消息:

  1. Election 消息:发起选举
  2. Alive 消息:对Election的应答
  3. Victory 消息:当选主节点后的声明消息

此算法假设集群中每个节点均知道其他节点ID,选举过程如下:

  1. 每个节点判断自己节点是否是当前活着节点种ID最大的,若是则直接发送Victory消息
  2. 如果自己不是最大,向所有比自己ID大的节点发送 Election 消息,等待回复
  3. 在给定时间范围内没收到其他节点回复的Alive消息则当选成功,向其他节点发送Victory消息;
    若收到比自己ID大的节点的Alive消息,则等待其他节点发送Victory消息
  4. 若本节点收到比自己ID小的节点发送的 Election 消息,回复一个 Alive 消息,告知其自己比他大,重新选举

MongoDB 的副本集群故障转移就使用了 Bully 算法,其使用节点的最后操作时间戳作为ID。

此算法优点是选举速度快、算法复杂度低、简单易实现。缺点:每个节点要保存全局信息,故额外信息多;任一比当前主节点ID大的新节点加入集群时都可能触发重新选举,如果其因网络原因频繁退出加入集群会导致频繁切主。

Raft算法

3种角色:leader、candidate、follower。选举流出如下:

  1. 初始时都为follower状态
  2. 开始选举时,所有节点由follower变为candidate,并向其他节点发送请求
  3. 其他节点根据接收请求的先后顺序投票,每轮每个节点只投一次票
  4. 节点获得超过一半投票变leader,其他变follower。leader与follower间定期发送心跳包检测是否活着
  5. leader任期到后降级为follower,进入新一轮

ZAB算法

通过节点ID和数据ID作为参考进行选举,节点ID和数据ID越大表示数据越新,优先成主。3种角色:leader、follower、observer(观察者,无投票权)。节点有4种状态:looking(选举状态,认为集群中没有leader)、leading、following、observing。

投票过程种每个节点都有唯一三元组 (server_id, server_szID, epoch) 分别表示(本节点唯一ID;本节点存放的数据ID,越大则数据越新选举权重越大;当前选取轮数,一般用逻辑时钟表示),用于表示自己的信息。

采取“少数服从多数,ID大的节点优先”,通过 (epoch, vote_id, vote_zxID) 表明投票给哪个节点,二元组表示(轮数;被投票节点id;被投票节点的服务器zxID)。ZAB中先比较 server_szID,再比较 server_id,大的为leader。

以3个节点为例:

  1. 刚启动,当前投票均为第一轮,epoch为1,zxID为0,每个节点推选自己,将选票信息广播出去。
  2. 由于3个节点的 epoch、zxID 都相同,因此比较 server_id,较大的推选为leader,因此Server1和Server2将 vote_id 改为3,更新自己投票箱并重新投票
  3. 所有机器推选Server3,其当选leader并通过心跳包维护连接

ZAB采用广播方式,若有n个节点则信息量为 n*(n-1)。投票时要知道所有节点的节点ID和数据ID,所以需要时间较长。当稳定性较好,加入新节点时会触发选主,但不一定切换。

分布式事务

在分布式系统中运行的事务,由多个本地事务组合而成。实现方法:

  • 基于XA协议的二阶段提交协议方法
  • 三阶段提交协议方法
  • 基于消息的最终一致性方法

基于XA协议的二阶段提交 2PC

简单概括:协调者下发请求事务操作,参与者将操作结果通知协调者,协调者根据所有参与者的反馈结果决定各参与者是要提交操作还是撤销操作。

  • 事务管理器:负责各个本地资源的提交和回滚
  • 本地资源管理器:分布式事务的参与者,通常由数据库实现

为了保证不同节点分布式事务一致性,选哟一个协调者管理所有节点。分2个阶段:投票、提交

  1. 投票
    协调者向所有参与者发送 CanCommit 询问请求,并等待响应。参与者接收请求后执行请求中事务操作,记录日志但不提交,等参与者执行成功则返回Yes消息表示同意。返回No则终止操作。
  2. 提交
    所有参与者返回Yes或No后进入提交阶段,协调者根据所有参与者返回的信息向参与者发送 DoCommit 或 DoAbort 指令
    1. 收到信息全为Yes则发送DoCommit,参与者完成剩余操作并释放资源,返回 HaveCommitted
    2. 收到信息包含No则发 DoAbort,参与者根据之前执行操作时的回滚日志进行回滚,向协调者发送 HaveCommitted
  3. 协调者收到 HaveCommitted 意味着事务结束

其满足了ACID但仍有不足:

  • 同步阻塞:所有节点都是事务阻塞型。本地资源管理器占有资源时其他资源管理器要访问呢同一临界资源会阻塞
  • 单点故障:一旦事务管理器故障,整个系统都瘫痪。尤其在提交阶段,一旦事务管理器故障,本地资源管理器会一直等待并锁定事务资源,导致系统死锁
  • 数据不一致:提交阶段,发送 DoCommit 请求后,若局部网络异常或协调者发生其他故障,会导致只有部分参与者接收到提交请求并执行操作,未接收到的则无法提交。于是分布式系统出现不一致

三阶段提交方法 3PC

引入了超时机制准备阶段,用于解决同步阻塞和数据不一致问题

  • 在协调者和参与者中引入超时机制。协调者在规定时间内没接收到其他节点响应则根据当前状态提交或终止事务,不像2PC那样被阻塞住
  • 引入预提交阶段。期间排除一些不一致情况,保证最后提交之前各参与节点的状态一致
  1. 准备阶段
    发送CanCommit请求询问是否可执行事务,参与者响应Yes或No
    参与者根据自身资源是否足以支撑事务、是否存在故障等预估自己是否可执行事务,但不会执行,根据预估信息返回Yes或No
  2. 预提交阶段
    根据参与者回复决定是否可进行 PreCommit操作
    1. 回复都是 Yes,执行事务预执行
      1. 发送预提交请求。协调者发送 PreCommit,进入提交阶段
      2. 事务预响应。参与者接收 PreCommit 后执行事务,将 Undo 和 Redo 记录到日志
      3. 响应反馈。成功则返回Ack
    2. 回复有No,或等待超时,中断事务
      1. 向所有参与者发送中断请求
      2. 终断事务。参与者收到 Abort 消息,或等待超时,事务回滚
  3. 提交阶段
    参与者向协调者发送Ack后,长时间没有响应则自动提交超时事务,不像二阶段那样阻塞
    实际业务中阻塞是不可避免的,3PC只是相对2PC缩短了阻塞时间
    1. 执行提交
      1. 发送提交请求。收到所有的Ack则进入提交状态并向所有参与者发送 DoCommit
      2. 事务提交。参与者接收 DoCommit 后正式提交事务、释放所有锁住的资源
      3. 完成反馈。完成后向协调者发送Ack响应
      4. 完成事务。收到Ack后完成
    2. 事务中断
      1. 发送中断请求。向所有参与者发送Abort
      2. 事务回滚。参与者利用Undo日志回滚,释放资源
      3. 反馈结果。回滚后发送Ack
      4. 中断事务。协调者接收到Ack后结束事务

基于分布式消息的最终一致性方案

前者需要锁住资源,且没有解决数据不一致问题。这里将要处理的事务通过消息或日志的方式异步执行,消息或日志可存入本地文件、db、mq,再通过业务规则进行失败重试。这使用基于分布式消息的最终一致性解决方案解决分布式事务问题。

例如电商平台下单支付,通过支付系统支付成功后订单状态修改为支付成功,然后通知厂库发货。三个相互独立系统通过rpc调用。

购物流程如下:

  1. 订单系统将订单消息发给MQ,消息状态为“待确认”
  2. MQ收到消息后进行持久化,即存储中增加一个“待发送”的消息
  3. MQ返回消息持久化结果(成功/失败),订单系统根据其放弃订单或创建订单
  4. 订单创建后将结果(成功/失败)发送给MQ
  5. MQ根据结果处理:失败则删除存储中的消息;成功则将存储中消息状态改为“待发送”并投递
  6. MQ将消息发给支付系统,支付系统也按上述方式进行订单支付
  7. 支付完成后返回消息给MQ,MQ再将消息传递给订单系统,订单系统再调用库存系统,进行出货

分布式锁

锁是实现多线程同时访问同一共享资源,保证同一时刻只有一个线程可访问共享资源所做的一种标记。
分布式锁是指分布式环境下,系统部署在多个机器中,实现分布式互斥的一种机制

例如某电商售卖吹风机,库存2个,但有5个买家。最简单方案就是给吹风机库存数加一个锁,每个用户提交订单后,后台服务器给库存加一个锁,根据订单修改库存。但是这个方法仍存在问题。例如:理想情况下,A买了1个,库存还剩1个,此时应当提示B库存不足,B购买失败。但如果是A和B同时获取到库存还剩2个的数据。A买走1个,B买走2个,库存还剩0或1个。此时总共只有两个吹风机,但卖出去3个。(可以考虑MySQL的 for update 语句)。

基于数据库

创建一张锁表,通过操作该表数据实现分布式锁。要锁住某个资源就增加一条记录,要释放时就删除这条记录。db对共享资源做唯一约束。

缺点:

  1. IO开销大,不适合高并发、性能要求高的场景
  2. 单点故障,一旦数据库不可用,整个系统崩溃
  3. 死锁问题,数据库锁没失效时间,一旦获得锁的进程挂掉或操作失败,锁会一直存在数据库中,其他进程无法获得锁

基于Redis缓存

将数据放入计算机内存,不写入磁盘减少IO。通常使用 Redis 的 setnx(key, value)

再考虑电商售卖吹风机的场景,假设库存数量充足。A先到达Server2通过setnx获得锁;B和C几乎同时到达Server1和Server3,但因为Server2获得锁,所以只能等待。Server2获得锁后用1s完成订单,删除key以释放锁。Server1获得锁可以访问资源,但是完成操作后发生故障,无法主动释放锁。Server3只能等到已有的key超时,锁自动释放后才能访问资源。

相比与使用数据库,使用缓存优点如下:

  1. 性能更好,避免了频繁IO
  2. 缓存可跨集群部署,避免了单点故障
  3. 可使用超时机制自动释放锁

缺点是超时机制不是十分靠谱,如果一个进程执行时间较长导致时间超时,从而不正确释放了锁。

基于ZooKeeper

zookeeper基于树形结构实现分布式锁,其由4种节点组成:

  • 持久节点:默认节点类型,一直存在
  • 持久顺序节点:根据创建节点的时间顺序进行编号
  • 临时节点:客户端与zookeeper断开后该进程创建的临时节点被删除
  • 临时顺序节点:按时间排序的临时节点

基于zookeeper的临时顺序节点实现了分布式锁

仍以电商售卖吹风机为例,用户ABC同时提交了购物请求:

  1. 与该方法对应的持久节点 shared_lock 目录下,为每个进程创建一个临时顺序节点。
    吹风机对应目录,每当有人买时就创建一个临时顺序节点
  2. 每个进程获取 shared_lock 目录下所有临时节点列表,注册子节点变更的 Watcher,并监听节点
  3. 每个节点确定自己的编号是否是 shared_lock 下所有子节点中最小的,如果是则获得锁
  4. 若本进程对应临时顺序节点不是最小,分两种情况
    1. 本进程为读请求,比自己小的节点中有写请求,则等待
    2. 本进程为写请求,比自己小的节点中有读请求,则等待

图中,只有再A完成购买、C完成查询后,B才可以购买。

zookeeper解决了各种问题,如单点故障、不可重入、死锁等,但由于频繁的删除添加节点,所以性能不如基于缓存实现的分布式锁。

小结

设计分布式锁应该考虑:

  • 互斥性
  • 具备锁失效机制
  • 可重入性
  • 有高可用的获取锁和释放锁的能力,且性能要好

“羊群效应”指分布式锁竞争中大量“watcher消息”和“子节点列表获取”操作重复进行,且大多数节点的结果都是自己并不是最小的节点,继续等下一次通知,而不是执行业务逻辑。这会对zookeeper造成去打性能和网络影响。解决方法如下:

  1. 与该方法对应持久节点的目录下,为每个进程创建一个临时节点
  2. 每个进程获取临时节点列表,若是最小则获得锁
  3. 若不是最小
    1. 本进程为读请求,像比自己序号小的最后一个写请求注册watch监听,该节点释放后获得锁
    2. 本进程为写请求,像比自己序号小的最后一个读请求注册watch监听,该节点释放后获得锁

 

posted @ 2023-03-26 08:26  某某人8265  阅读(119)  评论(0编辑  收藏  举报