了解一点·分布式理论(4)·有副本才有高可用

1,159 阅读11分钟

前言

上一篇文章我们讨论了单机运算能力不足的问题,了解了分布式系统也是基于分治思想来实现运算分布式化,达到提高系统整体算力的最终目标。分布式系统的出现弥补了集中式系统在运算和存储方面存在的不足,做到了集中式系统不方便实现甚至做不到的事,但是有一件事是包括分布式系统在内的所有系统都可能遇到的,那就是宕机。让分布式系统保证不宕机则是本篇的主题。

这是系列文章的第四篇,讨论分布式系统如何达到高可用的目标。分布式系统具有的扩展性提升了系统整体算力和存储能力,解决了这两个核心问题后要考虑的就是如何让系统平稳地运行下去,使我们能够持续地感受到分布式系统的强大,我们管这份强大叫高可用性。实现高可用性的唯一策略便是备份副本。

备份副本

一个系统的软实力再强大、应用的设计思想再精妙,都挡不住硬条件的不足(如机房停电、网络故障等)对自身的打击,物理设备因各种事故瘫痪的可能性时刻存在,所以要想让一个系统具有高可用性只有一个方法——副本(Replication)。一台机器在故障后马上将服务切换到副本机器上,才能实现服务不断。

副本(Replication)

副本(Replication)是分布式系统必备的两种能力之一(另一种是分片(partition/sharding)),对于海量数据来说,replication一方面可以使系统具有高可用性,另一方面还可以提升读取的效率。

假设我们从来没有听说过分布式系统和副本策略,单单给一个思路:通过复制一份原来的系统来提高系统可用性。那么我们最容易往下想的思路是:以原本提供服务的系统为蓝本,复制出一份副本备用(standby),蓝本持续提供服务,当蓝本出现故障无法工作时,失效转移(failover)到副本继续对外提供服务,不使系统陷入瘫痪。

上述思路提到的蓝本、副本很容易可以看出来是一主一从的身份,很多算法都有使用这种理念设计高可用性算法,它有很多不同的叫法:leader & followermaster & slaveactive & standby,本篇统一叫master & slavemaster & slave的关系并非永久不变的,failover操作会转换二者身份。

replication有三种玩法,接下来会通过推导来串讲这三种玩法。

首先,上述的master & slave是第一种类型,我们叫它单主复制(single leader replication),或者另一个更生动的叫法:一主多从。

单主复制(Single-Leader Replication)

原理:一个节点(node),其他节点作为副本(slave)。所有写操作由master处理,处理结果由master记录到replication log,slave通过与replication log同步使数据与master保持一致,读操作的处理不做限制,master和slave都可以处理。

副本产生策略(leader-based replication way)

基于single leader replication原理,副本复制蓝本的数据有三种:

  • 同步复制(sync replication)

    蓝本的数据完全复制到副本才算服务完成。这种策略具有强一致性,但在多副本情况下,同步成本很高,一般很少使用。

    优点:保证了强一致性,slave的数据始终与master保持一致,因此即使master故障造成数据丢失也没关系。

    缺点:master需要等待所有slave同步过程结束,如果slave在同步过程中宕机,那么master会永久阻塞下去(不考虑超时机制(timeout))。

  • 异步复制(async replication)

    副本异步复制蓝本的数据,蓝本无需等待这个过程执行完毕。这种策略的写入效率高,但可能造成数据脏读。

    优点:弥补了sync replication的不足,即使slave在同步过程中宕机也不会影响master对外服务。

    缺点:由于没有“监视”slave同步,若master在slave还未同步数据前宕机,则会丢失部分数据,且可能发生脏读的情况(如果客户端(client)在slave未同步数据前去读取了slave的数据,就会产生脏读)。

  • 半同步复制(semi sync replication)

    在多副本的前提下,约定一个数量,当蓝本的数据复制到约定个副本,就算写入成功。

    semi sync replication是对sync replicationasync replication的折中策略。如图所示的例子中,约定好只要有一个slave同步数据成功(此处为slave1),master就认为replication操作完成,进而继续处理后续,无需等待所有slave同步完成。

    那么问题来了,如果只有slave1同步成功,那肯定会造成多个slave之间数据不一致,client读取的时候怎么才能不脏读呢?解决方案是:通过并发请求多个节点(node)来满足读取的一致性

节点宕机(Node Failure)

有了slave,那么节点宕机后怎么处理呢?

  • master宕机。思路:
    1. 确认master宕机情况:即确认master是否真的挂了,通常使用timeout判定)
    2. 竞选master(master election):从slave中选出一个升为master
    3. 令其他slave向新master同步,已宕机的master恢复后降为slave,向新master同步
  • slave宕机。在恢复后同步数据思路:
    1. master生成快照(snapshot)供slave同步数据使用
    2. 如果slave落后master太多,可以只同步slave的最新LSN(Log Sequence Number,日志序号)之后的日志
    3. 如果slave落后master不多,则可以直接复制日志

如果Single-Leader Replication足够好,就不会出现其它Replication类型,所以我们来扯一扯单主复制的不足:

  1. master宕机后需要进行failover操作,slave竞选和将服务切换到新master都需要时间,在这段时间内系统处于阻塞状态,哪怕只有几秒。
  2. 竞选成功的slave成为新master,新master尚未同步到已宕机的旧master的最新数据,当旧master恢复为新slave后,新slave就会比新master多出一部分数据,这就产生了数据不一致的情况。

不仅如此,我们还可以从经济成本上考虑。既然已经掏钱去多备份了slave,只让它当standby的话就只能在master宕机时才能发挥作用,所以不妨让slave也来接收读请求,既分担了master的压力,也不浪费slave的性能,在semi sync replication图示中就可以看到我们确实可以这么用。

单主复制的failover会短时阻塞甚至有超时的风险,也有可能造成数据不一致的问题,这都是因为一主多从。那么如果不是一主的情况,不就可以避开上述的问题了吗?于是有了多主复制(Multi-Leader Replication)

多主复制(Multi-Leader Replication)

原理:每个node都是master,都可以对外提供服务,node之间互为主从。写操作可以先由任意一个master处理,再同步到其它master。

由于每个节点都是master,都可提供服务,一个master执行完写操作后,其它master再去同步该master的最新数据。你这会儿可能会想:诶?这不是和single-leader replicationasync replication一样吗?并非完全一样,muti-leader replication与前者最大的区别就是不指定唯一一个master。用大白话概括:single-leader replication就是独裁,一台机器更新了数据,其他机器要向它看齐;muti-leader replication就是民主,每台机器都可以更新数据,然后其他机器再去同步。

但是muti-leader replication也有不足:它很容易导致数据冲突,也就是一致性问题

举例1:在一段时间内有client1、client2发出update请求。

过程说明:

  1. client1发出update请求,master1进行处理。执行完update操作后,master1响应client1。

  2. master1做了写操作,其他master要去同步master1的最新数据,其中master2同步得比较快,数据与master1保持一致,而master3同步得较慢。

  3. 在master3还在同步的时候,client2又针对client1处理过的数据又发起一次update请求,这次由同步完成的master2处理。执行完update操作后,master2响应client2。

  4. master2做了写操作,master1和master3很快就同步完master2的最新数据,而这时master3依然没有同步完上一次master1的数据。

  5. 等到最终master3也同步完master1的数据,由于比同步master2慢,master3的数据最终采用后来同步的,也就是从master2更新过来的数据。

然而这是有问题的,从时间上看client2的update要后于client1的,最终最新的数据应该是client2提交的才对。

举例2:在并发的情况下,client1向多个master提交了写操作请求。

多个master各自修改数据将数据修改成不同的值,由于是并发环境,多个master的执行对同一条数据的修改不一致但又都是作数的。你可能会说,那就比较返回的先后(比如比较时间戳),以最后返回的那一个为准,但实践中这个问题的解决方案并没有那么想当然。

多主复制比较适合多数据中心的场景,但是却不擅长应对并发写操作,而这都是因为多主互从,可谓是成也多主败也多主。因此我们可以继续想:单主复制有短时阻塞甚至有超时的风险,多主复制又难以解决并发写导致的数据一致性问题,那么如果没有主,是不是可以避开上述两种复制方式会遇到的问题?于是有了无主复制(Leaderless Replication)

无主复制(Leaderless Replication)

原理:client直接向多个replica发出写请求,读取数据时同样对多个replica发出读请求。

leaderless replication中没有master和slave之说,每个node都可以看做是副本(replica),client需要update数据时,向多个replica发出写请求、client需要查询数据时,向多个replica发出读请求。

leaderless replication往往采用如下两种方法补偿(repair),使得数据最终保持一致:

  1. Read repair:由于client会从多个replica读数据,它可以发现哪个副本的数据是旧的,可以用client把从其他replica里读到的新数据写到旧数据的replica中,实现最终一致。

  2. 开一个守护进程负责数据的补偿。

使用leaderless replication还是有采坑的时候。举个例子:如果总共有6个replica(A、B、C、D、E、F),执行写操作时向3个replica(A、B、C)发送请求,执行读操作时也向3个replica(D、E、F)发送请求,这种情况下就完美错过新数据。

所以,假设replica总个数为n,client发起读、写请求会发往这n个replica,写请求要求保证w个成功返回,读请求要求保证r个成功返回,我们一般要保证w + r > n,才可以尽量满足不会读到旧数据,常见的选择是n为奇数, w=r=(n+1)/2。

当然,即使是保证了w + r > n,还是有一定可能碰到脏读的情况,比如并发环境、比如写操作在一部分replica成功,一部分replica失败,失败的那部分replica可以回滚,但是已经写成功的replica不会回滚,这样一来依然有数据一致性问题。

总结

  • 一个系统要实现高可用的唯一办法是副本(replication)。
  • replication有三种玩法:单主复制(Single-Leader Replication)、多主复制(Muti-Leader Replication)、无主复制(Leaderless Replication)。
  • 单主复制有同步复制(sync replication)、异步复制(async replication)、半同步复制(semi sync replication)三种方式,总体来说它有着短时阻塞甚至超时的风险,也有可能导致数据一致性问题。
  • 多主复制不擅长解决因于并发写操作带来的数据一致性问题。无主复制也有可能导致数据一致性问题。

引出问题

综合全篇,无论使用哪种玩法生成副本都有可能产生数据一致性问题,但是别忘了,副本的重点是搞多一份系统、数据备用,至于数据会不会冲突和如何解决冲突,那就是另一个话题了,下一篇文章我们就来讨论数据一致性的问题。