系统设计之:深入理解分布式系统(分区、复制、分布式事务以及系统一致性与共识)

分布式数据

引言

你可能会出于各种各样的原因,希望将数据库分布到多台机器上:

可扩展性
如果你的数据量、读取负载、写⼊负载超出单台机器的处理能⼒,可以将负载分散到多台计算机上。

容错/⾼可⽤性
如果你的应⽤需要在单台机器(或多台机器,⽹络或整个数据中⼼)出现故障的情况下仍然能继续⼯
作,则可使⽤多台机器,以提供冗余。⼀台故障时,另⼀台可以接管。

延迟
如果在世界各地都有⽤户,你也许会考虑在全球范围部署多个服务器,从⽽每个⽤户可以从地理上最近
的数据中⼼获取服务,避免了等待⽹络数据包穿越半个世界。

在具体实现上,⽆共享架构(shared-nothing architecture)因其较高的性价比,以及强大的功能而被广泛使用,无共享架构有时称为⽔平扩展(horizontal scale) 或向外扩展(scale out))。
在这种架构中,运⾏数据库软件的每台机器/虚拟机都称为节点(node)。每个节点只使⽤各⾃的处理器,内存和磁盘。节点之间的任何协调,都是在软件层⾯使⽤传统⽹络实现的。

虽然分布式⽆共享架构有许多优点,但它通常也会给应⽤带来额外的复杂度,有时也会限制你可⽤数据模型的表达⼒。接下来我们将详细讨论分布式系统带来的各种问题,以及问题的解决方案;

五.复制

  • 适用场景:少数据量,单库可承载的场景。

  • 目的:

    • 1.扩展性。提高吞吐量
    • 2.降低延迟。通过将数据放在离用户较近的地方,以便能够更快的访问数据。
    • 3.高可用。即使部分节点故障停机,也能保持服务正常运行。
  • 领导者&追随者

    • I.同步复制 vs 异步复制

      • a.同步复制

        • 优点

          • 从库保证有与主库⼀致的最新数据副本。如果主库突然失效,我们可以确信这些数据仍然能在从库上上找到。
        • 缺点

          • 如果同步从库没有响应(⽐如崩溃,或其它任何原因),主库就⽆法处理写⼊操作。
        • 将所有从库都设置为同步的是不切实际的:任何⼀个节点的中断都会导致整个系统停滞不前。

      • b.异步复制

        • 优点

          • 即使所有的从库都落后了,主库也可以继续处理
            写⼊。
        • 缺点

          • 会出现复制延迟问题,见下文。
        • 通常情况下,基于领导者的复制都配置为完全异步。

      • c.半同步

        • 只保证一部分节点是保持与主库一致性的数据。
    • II.设置新从库

      • 步骤:

        • 1.获取主库某时刻一致性快照。
          2.快照复制
          3.链接主库,拉取快照后的数据变更。要求主库中日志位置精确,如日志序列号,二进制日志坐标。
          4.从库赶上主库后,就可以正常处理主库的产生数据变化了。
    • III.处理节点宕机

      • 从库失效:追赶恢复
      • 主库失效:故障切换
    • IV.复制日志的实现

      • 定义:每当领导者将新数据写⼊本地存储时,它
        也会将数据变更发送给所有的追随者,称之为复制⽇志(replication log)记录或变更流
        (change stream)。

      • 实现1.基于语句的复制

        • 优点:可读性较强
          缺点:执行依赖于语句的复制会有问题,如:自增主键、随机字符串、now()函数,存储过程等。
        • 实践:mysql默认使用该模式,并在特定情况下切换至基于日志的复制模式。
      • 实现2.传输预写式日志WAL(Write Ahead Log)

        • 优点:基于追加日志的方式,记录数据的变更,可以解决以上复制问题。
          缺点:1.wal偏底层,通常记录的是磁盘某分区数据更改;2.数据与存储耦合性较强;
        • 实践:PostgreSQL和oracle使用此复制方式。
      • 实现3.基于逻辑日志(Binlog)的复制

        • 优点:数据变更的二进制字节流,传输速度快,且保证数据复制的正确型。
          缺点:不可读。
        • 实践:mysql的二进制日志使用该方式
      • 实现4.基于触发器的复制

        • 使用应⽤程序代码复制
  • 复制延迟问题

    • 最终一致性:同时对主库和从库执⾏相同的查询,可能得到不同的结果,因为并⾮所有的写⼊都反映在从库中。如果停⽌写⼊数据库并等待⼀段时间,从库最终会赶上并与主库保持⼀致,这种效应成为最终一致性。
    • 1.写后读:用户总是可以读到自己提交的数据。
    • 2.单调性:即用户在读取某个时间的数据后,不应再读取到更早时间点的数据。
    • 3.一致前缀读:用户应该将数据视为具有因果意义的状态。例如:按照正确的顺序查看问题及其答复。
  • 三种流行的复制算法

    • 1.单主复制

      • 客户端将所有的写操作发送到单个节点(领导者),该节点将数据更改事件流发送到所有的副本(追随者)。读操作可以在任何节点执行,但可能会读取到旧数据。
      • 优点:容易理解,且没有冲突问题。
    • 2.多主复制

      • 客户端的将写操作发送到几个领导者节点之一,其中任何一个都可以接受写入。然后由该节点将数据更改事件流发送给其他领导者节点及其跟随者节点。
      • 优点:出现故障节点、网络中断和延迟峰值情况下,多主和无主复制更加稳健。但以仅提供弱一致性为代价。
      • 并发问题
    • 3.无主复制

      • 客户端发送每个写入到多个节点,并从多个节点并行读取,以检测和纠正具有陈旧数据的节点。
      • 并发问题

六.分区

  • 适用场景:数据量⾮常⼤的时候,在单台机器上
    存储和处理不再可⾏,则分区⼗分必要。
    此时需要将数据进⾏分区(partitions),也称为分⽚(sharding) 。

  • 目的:分区的⽬标是在多台机器上均匀分布数据和查询负载,避免出现热点(负载不成⽐例的节点)。

  • 分区与复制:

    • 分区通常与复制结合使⽤,使得每个分区的副本存储在多个节点上。 这意味着,即使每条记录属于⼀个分区,它仍然可以存储在多个不同的节点上以获得容错能⼒。
  • 两种主要的分区⽅法:

    • 键范围分区

      • 核心思想:为每个分区指定⼀块连续的键范围(从最⼩值到最⼤值),如纸百科全书的卷)。如果知道范围之间的边界,则可以轻松确定哪个分区包含某个值。
      • 优点:键是有序的,并且分区拥有从某个最⼩值到某个最⼤值的所有键。可以进⾏有效的范围查询。
      • 缺点:如果应⽤程序经常访问相邻的主键,则存在热点和偏斜的⻛险。
    • 散列分区

      • 核心思想:散列进⾏分区,通常先提前创建固定数量的分区,为每个节点分配多个分区,并在添加或删除节点时将整个分区从⼀个节点移动到另⼀个节点。也可以使⽤动态分区。
      • 优点:可以将将偏斜的数据均匀分布
      • 缺点:散列分区破坏了键的排序,使得范围查询效率低下
    • 键范围&散列组合分区

      • 两种⽅法搭配使⽤也是可⾏的,例如使⽤复合主键:使⽤键的⼀部分来标识分区,⽽使⽤另⼀部分作为排序顺序。
  • 分区与二级索引:分区和⼆级索引之间的相互作⽤。二级索引也需要分区,有两种分区⽅法:

    • 方法1: 按⽂档分区(本地索引)。特点:
      1.⼆级索引存储在与主键和值相同的分区中。
      2.这意味着只有⼀个分区需要在写⼊时更新,
      3.但是读取⼆级索引需要在所有分区之间进⾏分散/收集。
    • 方法2: 按关键词Term分区(全局索引)。
      特点:
  1. ⼆级索引存在不同的分区。辅助索引中的条⽬可以包括来⾃主键的所有分区的记录。
    2.当⽂档写⼊时,需要更新多个分区中的⼆级索引;
    3.但是可以从单个分区中进⾏读取。
  • 分区再平衡

    • 定义:随着时间的推移,数据库会有各种变化。如查询吞吐量增加、数据集大小增加、节点机器故障下线等,此时需要数据和请求从⼀个节点移动到另⼀个节点。 将负载从集群中的⼀个节点向另⼀个节点移动的过程称为再平衡(reblancing)。

    • 平衡策略

      • 反面教材:hash mod N

        • \(N\)⽅法的问题是,如果节点数量N发⽣变化,⼤多数密钥将需要从⼀个节点移动到另⼀个节点。使得重新平衡过于昂贵。我们需要⼀种只移动必需数据的⽅法。
      • 固定数量的分区

        • ⼀种只移动必需数据的简单⽅法:创建⽐节点更多的分区,并为每个节点分配多个分区。只有分区在节点之间的移动。分区的数量不会改变,键所指定的分区也不会改变。唯⼀改变的是分区所
          在的节点。
        • 如果分区⾮常⼤,再平衡和从节点故障恢复变得昂贵。如果分区太⼩,则会产⽣太多的开销。
          当分区⼤⼩“恰到好处”的时候才能获得很好的性能,如果分区数量固定,但数据量变动很⼤,则难以达到最佳性能。
      • 动态分区

        • 每个分区分配给⼀个节点,每个节点可以处理多个分区,就像固定数量的分区⼀样。当分区增⻓到超过配置的⼤⼩时,拆分⼤型分区将其中的⼀半转移到另⼀个节点,以平衡负载;与之相反,如果⼤量数据被删除并且分区缩⼩到某个阈值以下,则可以将其与相邻分区合并。此过程与B树顶层发⽣的过程类似(参阅“B树”)。
        • 动态分区的⼀个优点是分区数量适应总数据量。如果只有少量的数据,少量的分区就⾜够了,所以开销很⼩;如果有⼤量的数据,每个分区的⼤⼩被限制在⼀个可配置的最⼤值【23】
        • 动态分区不仅适⽤于数据的范围分区,⽽且也适⽤于散列分区。从版本2.4开始,MongoDB同时⽀持范围和哈希分区,并且都是进⾏动态分割分区。
      • 按节点⽐例分区

        • Cassandra和Ketama使⽤的第三种⽅法是使分区数与节点数成正⽐——换句话说,每个节点具有固定数量的分区【23,27,28】。在这种情况下,每个分区的⼤⼩与数据集⼤⼩成⽐例地增⻓,⽽节点数量保持不变,但是当增加节点数时,分区将再次变⼩。由于较⼤的数据量通常需要较⼤数量的节点进⾏存储,因此这种⽅法也使每个分区的⼤⼩较为稳定。
  • 请求路由

    • 分区负载平衡
    • 并⾏查询执⾏引

七、事务

  • 事务是⼀个抽象层,允许应⽤程序假装某些并发问题和某些类型的硬件和软件故障不存在。各式各样的错误被简化为⼀种简单情况:事务中⽌(transaction abort),⽽应⽤需要的仅仅是重试。

  • 弱隔离级别

    • 1.读已提交Read Commit

      • 两个保证:

        • 无脏读
        • 无脏写
      • 实现读已提交

        • 防止脏写

          • 使用行锁实现:当事务想要修改特定的对象(行或文档)时,它必须首先获得该对象的锁。然后必须持有该锁直到事务提交或中止;
            一次只有一个事务可以持有任何给定对象的锁;
            如果另一事务想要写入同一个对象,则必须等到第一个事务提交或中止后才能获取该锁并继续。
        • 防止脏读

          • 方法一:使用行锁。但实践效果并不好,损失了只读事务的响应时间,并且可能因为等待锁导致连锁反应从而使整体响应迟缓。
          • 方法二:对于写入的每个对象,数据库都会记住旧的已经提交的值,和由当前持有写入锁的事务设置的新值;
            当事务正在进行时,任何其他读取对象的事务都会拿到旧值;
            只有当新值提交以后,事务才会切换到读取新值。
      • 不足:

        • 不可重复读 nonrepeatable 或读取偏差 read skew
        • 丢失更新Lost update
    • 2.快照隔离和可重复读Read Repeatable

      • 一个保证:

        • 可重复读
      • 不可重复读问题

        • 描述:事务A在对一个对象写入期间,另一个事务B分别在事务A提交前、后读取到不同值的现象。

        • 解决

          • 快照隔离:每个事务都从数据库的一致快照(consistent snapshot)中读取,即事务可以看到事务开始时在数据库中提交的所有数据,即使这些数据随后被另一个事务更改,每个事务也只能看到该特定时间点的旧数据。
          • 快照隔离对长时间运行只读查询非常有用,如备份和分析。
      • 实现快照隔离

        • 关键原则:读不阻塞写,写不阻塞读。

        • 多版本并发控制MVCC,multi-version concurrency control:数据库保留和维护同一个对象在不同时间点的多个提交版本。

        • PostSQL中MVCC的实现:

          • 当一个事务开始时会被赋予一个唯一的永远递增的事务ID,每当事务向数据库写入任何内容时,它所写入的数据都会被标记上写入者的事务ID。
          • 可见性规则:同时满足以下2个条件,则可见一个对象:
            a。读事务开始时,创建该对象的事务已经提交。
            b。对象未被标记删除或已被标记删除,请求删除的事务在读事务开始时尚未提交。
    • 3.防止丢失更新Lost update

      • 并发写入事务可能导致的问题

      • 解决方案

        • a。原子写

          • 许多数据库提供了原⼦更新操作,从⽽消除了在应⽤程序代码中执⾏读取-修改-写⼊序列的需要。如下在大多数数据库是并发安全的:
            UPDATE counters SET value = value + 1 WHERE key = 'foo';
            如果你的代码需要,那这通常是最好的解决⽅案。

          • a。实现

            • 方案一:游标稳定性技术,事务在读取对象时获取其上的排他锁,在更新操作完成之前没有其他事务可以读取该对象。通常的实现方式。
            • 方案二:简单地强制所有的原子操作在单一线程上执行。
        • b。显示锁定

          • 防⽌丢失更新的另⼀个选择是让应⽤程序显式地锁定将要更新的对象。然后应⽤程序可以执⾏读取-修改-写⼊序列,如果任何其他事务尝试同时读取同⼀个对象,则强制等待,直到第⼀个读取-修改-写⼊序列完成。
          • FOR UPDATE ⼦句告诉数据库应该对该查询返回的所有⾏加锁。
        • c。自动检测丢失的更新

          • 原⼦操作和锁是通过强制读取-修改-写⼊序列按顺序发⽣,来防⽌丢失更新的⽅法。

          • 另⼀种⽅法是允许它们并⾏执⾏,如果事务管理器检测到丢失更新,则中⽌事务并强制它们重试其读取-修改-写⼊序列。

          • 优点:数据库可以结合快照隔离⾼效地执⾏此检查。

            • Oracle可串行化和SQL server快照隔离级别都会自动检测丢失的更新。
            • MySQL/InnoDB的可重复读并不会检测丢失更新。
        • d。比较并设置(CAS, Compare And Set)

          • 此操作的⽬的是为了避免丢失更新。
          • 但是,如果数据库允许 WHERE ⼦句从旧快照中读取,则此语句可能⽆法防⽌丢失更新(MVCC)
          • 在依赖数据库的CAS操作前要检查其是否安
            全。
        • e。冲突解决和复制

          • 多节点情况

            • 防⽌丢失的更新需要考虑另⼀个维度:由于在多个节点上存在数据副本,并且在不同节点上的数据可能被并发地修改,基于锁或CAS操作的技术不适⽤于这种情况,因此需要采取⼀些额外的步骤来防⽌丢失更新。
            • ⼀种常⻅⽅法是允许并发写⼊创建多个冲突版
              本的值(也称为兄弟),并使⽤应⽤代码或特殊数据结构在事实发⽣之后解决和合并这些版本。
            • 另⼀⽅⾯,最后写⼊为准(LWW)也可解决冲突,但该⽅法很容易丢失更新。
        • f。写入偏差和幻读

          • I.不同事务并发写入相同对象情况

            • 导致的问题

              • 脏写,丢失更新
            • 解决方式

              • 通过【锁】和【原子写操作】这类手动安全措施。
          • II.不同事务并发写入不同对象情况

            • 导致的问题

              • 写偏差,幻读
            • 写偏差

              • 如果两个事务读取相同的对象,然后更新其中⼀些对象(不同的事务可能更新不同的对象),则可能发⽣写⼊偏差。
            • 导致写偏差的幻读

              • ⼀个事务中的写⼊改变另⼀个事务的搜索查询的结果,被称为幻读【3】
            • 解决方式

              • 较优:使用触发器,或者物化视图
              • 次优:使用FOR UPDATE显示锁定事务所依赖的所有行。
            • 物化冲突

              • 如果幻读的问题是没有对象可以加锁,也许可以⼈为地在数据库中引⼊⼀个锁对象?
              • 例如,在会议室预订的场景中,可以想象创建⼀个关于时间槽和房间的表。此表中的每⼀⾏对应于特定时间段(例如15分钟)的特定房间。可以提前插⼊房间和时间的所有可能组合⾏(如接下来的六个⽉)。
                现在,要创建预订的事务可以锁定( SELECT FOR UPDATE )表中与所需房间和时间段对应的⾏。在获得锁定之后,它可以检查重叠的预订并像以前⼀样插⼊新的预订。
                请注意,这个表并不是⽤来存储预订相关的信息——它完全就是⼀组锁,⽤于防⽌同时修改同⼀房间和时间范围内的预订。
              • 这种⽅法被称为物化冲突(materializing conflicts),因为它将幻读变为数据库中⼀组具体⾏上的锁冲突【11】。
                不幸的是,弄清楚如何物化冲突可能很难,也很容易出错。在⼤多数情况下。可序列化 的隔离级别是更可取的方案。
  • 4.可序列化(Serializable)

    • 目标:解决幻读

    • 实现

      • 方案一:真正的串行化

        • 顺序执⾏所有事务使并发控制简单多了,但数据库的事务吞吐量被限制为单机单核的速度。
          只读事务可以使⽤快照隔离在其它地⽅执⾏,但对于写⼊吞吐量较⾼的应⽤,单线程事务处理器可能成为⼀个严重的瓶颈。

        • 最佳实践

          • 1.每个事务都必须小而快,只要有一个缓慢的事务,就会拖慢所有事务处理。
          • II。仅限于活跃数据集可以放入内存的情况,因为访问磁盘会很慢。
          • III。写入吞吐量必须低到能在单个CPU核上处理,否则事务需要化分支单个分区,且不需要跨分区协调。
          • IV。跨分区事务是可能的,但是他们的使用成都有很大的限制。
        • 分区

          • 为了扩展到多个CPU核⼼和多个节点,可以对数据进⾏分区。
          • 找到⼀种对数据集进⾏分区的⽅法,以便每个事务只需要在单个分区中读写数据,那么每个
            分区就可以拥有⾃⼰独⽴运⾏的事务处理线程。在这种情况下可以为每个分区指派⼀个独⽴的CPU核,事务吞吐量就可以与CPU核数保持线性扩展【47】。
      • 方案二:两阶段锁定(2PL)

        • 实现

        • 读阻塞写,写阻塞读。表级别

        • 悲观锁

        • 性能如何

          • 性能非常差
        • 变形一:谓词锁

          • 它类似于2PL描述的共享/排它锁,但不属于特定的对象(例如,表中的⼀⾏),它属于所有符合某些搜索条件的对象。
          • 读阻塞写,写阻塞读。条件匹配的数据行级别
          • 性能较差
        • 变形二:索引范围锁

          • 又叫间隙锁next-key locking,大多数2PL数据的实现。近似版的谓词锁
          • 这种方法可能会锁定更大范围的对象,而不是维持可串性化所必须的范围。
            它可以有效防止幻读和写入偏差,开销也较低,是一个很好的折中选择。
          • 读阻塞写,写阻塞读。条件中索引列的级别,如果无索引则是表级别
      • 方案三:可序列化快照隔离(SSI,)

        • 序列化的隔离级别和⾼性能是从根本上相互⽭盾的吗?可序列化隔离提供了一种选择,它提供了完整的可序列化隔离级别,但与快照隔离相⽐只有只有很⼩的性能损失。

        • 乐观锁:即如果存在潜在的危险也不阻止事务,而是继续执行事务,希望一起都好起来。当一个事务想要提交时,数据库检查是否有什么不好的事情发生(即隔离是否被违反);如果是,事务将被中止,并且必须重拾。只有可序列化的事务才被允许提交。

        • 乐观锁的优点和缺点:

          • 如果存在很多争用/竞争,则表现不佳,因为这会导致很大一部分事务需要中止。如果系统已经接近最大吞吐量,来自重试事务的额外负载可能会使性能变差。
          • 但是如果有足够的备用容量,并且事务之间的争用不是太高,乐观的并发控制技术往往比悲观的性能要好。
        • 顾名思义SSI基于快照隔离,也就是事务中所有读取都是来自数据库的一致性快照。在快照隔离的基础上,SSI增加了一种算法来检测写入之间的序列化冲突,并确定要中止哪些事务。

        • 性能

          • 中⽌率显着影响SSI的整体表现。SSI要求同时读写的事务尽量短(只读⻓事务可能没问题)。此时性能较好

八.分布式系统的麻烦

  • 子主题 4

    • 时钟错误
    • 进程暂停
    • 无上限的网络延迟
  • 故障与部分失效

    • 单个计算机上的软件,通常会以⼀种相当可预测的⽅式运⾏,它没有根本性的不可靠原因。
      在分布式系统中,情况有本质上的区别。在分布式系统中,尽管系统的其他部分⼯作正常,但系统的某些部分可能会以某种不可预知的⽅式被破
      坏。这被称为部分失效(partial failure)。
    • 难点在于部分失效是不确定性的
      (nonderterministic):如果你试图做任何涉及多个节点和⽹络的事情,它有时可能会⼯作,有时会出现不可预知的失败。
    • 如果要使分布式系统⼯作,就必须接受部分故障的可能性,并在软件中建⽴容错机制。换句话说,我们需要从不可靠的组件构建⼀个可靠的系统。
      故障处理必须是软件设计的⼀部分,并且作为软件的运维,您需要知道在发⽣故障的情况下,软件可能会表现出怎样的⾏为。
  • 不可靠的网络

    • 真实的网络环境很不稳定,⽹络故障时有发生,如果⽹络故障的错误处理没有定义与测试,武断地讲,各种错误可能都会发⽣。
      您需要知道您的软件如何应对⽹络问题,并确保系统能够从中恢复。
      有意识地触发⽹络问题并测试系统响应。

    • 检测故障

      • 如果你想确保⼀个请求是成功的,你需要应⽤程序本身的积极响应【24】。
    • 超时与⽆穷的延迟

      • 如果超时是检测故障的唯⼀可靠⽅法,那么超时应该等待多久?不幸的是没有简单的答案。

        • ⽹络拥塞和排队
      • ⻓时间的超时意味着⻓时间等待,直到⼀个节点被宣告死亡。在这段时间内⽤户可能不得不等待或者看到错误信息。
        短暂的超时可以更快地检测到故障,但是实际上它只是经历了暂时的减速⽽导致错误地宣布节点失效的⻛险更⾼。例如,由于节点或⽹络上的负载峰值。

      • 更好的⼀种做法是,系统不是使⽤配置的常量超时,⽽是连续测量响应时间及其变化(抖动),并根据观察到的响应时间分布⾃动调整超时。

    • 同步网络vs异步网络

      • 电话电路

        • 使用固定的网络带宽,传输的数据量固定、可预测。可保证的最⼤往返时间。
        • 低请求时浪费带宽,超过可支持带宽的高请求时网络阻塞。
      • 分组交换协议

        • 根据网络中数据类型及大小的不同,传输的数据量也是不可预测的,是动态变化的。
        • 有利于应对突发流量的情况。
  • 不可靠的时钟

    • 时钟和时间很重要!因为很多业务场景都依赖于时钟或时间

    • 分布式系统中,时间是一个比较棘手的事情:
      1.因为网络的传输需要时间,故节点间通信是不及时的。
      2.每台节点机器都有自己的时钟,他们不是完全准确的。一组服务器通常使用“网络时间协议NTP”在一定程度上同步时钟。

    • 单调钟和时钟

      • 现代计算机中至少有两种不同的时钟:即时钟和单调钟。

      • 时钟

        • 也成为挂钟时间wall-clock time,他根据某个日历返回当前的日期和时间。如:
          Linux上的clock_gettime(CLOCK_REALTIME)和
          Java中的System.currentTimemillis()返回epoch(即一个特定的时间:1970年1月1日午夜UTC)以来的秒数或毫秒数,不包括闰秒。
        • 时钟通常与NTP同步,如果本地使用在NTP服务器之前太远,则他可能会被重置到先前的时间点,发生时钟跳回。
        • 因为时钟跳回和忽略闰秒,使用不用用于测量经过的时间。可以作为一个时间日期的参考值。
      • 单调钟

        • 单调钟永续测量持续时间,即间隔时间,这个名字来源于单调钟保证前进的,而不会像时钟一样跳回。如:
          Linux上clock_gettime(CLOCK_MONOTONIC) ,和Java中的 System.nanoTime() 都是单调时钟。

          • 在具有多个CPU插槽的服务器上,每个CPU可能有⼀个单独的计时器,但不⼀定与其他CPU同步。明智的做法是不要太把这种单调性保证当回事
        • 单调钟的绝对值是毫无意义的,因为他可能是任何值。

        • 单调钟的分辨率相当好:大多数系统中,他们能在几微秒或更短时间内测量时间间隔。

        • 单调钟不需要同步。同一机器的不同CPU间,分布式系统的不同节点间。。等

      • 时钟的不准确性:单调钟不需要同步,但是时钟需要根据NTP服务器或其他外部时间源来设置才能有⽤。但计算机中的⽯英钟不够精确:它会漂移(drifts)(运⾏速度快于或慢于预期)。时钟漂移取决于机器的温度。
        使⽤GPS接收机,精确时间协议(PTP)【52】以及仔细的部署和监测可以实现这种精确度。

      • 暂停进程?

        • GC暂停
        • 虚拟机挂起
        • 长时间的I/O操作
        • 。。。
      • 代价:如果某个软件依赖于精确同步的时钟,那么结果更可能是悄⽆声息且⾏踪渺茫数据的数据丢失,⽽不是⼀次惊天动地的崩溃【53,54】。

  • 知识、真相与谎言

    • 真理由多数所定义

      • a。分布式系统不能完全依赖于单个节点,因为节点回会是失效,可能会使系统卡死,无法恢复。相反,许多分布式算法都依赖于【法定人数】,即在多个节点之间投票决定减少对某个节点的依赖。
        也包括宣告节点死亡的决定,即使一个节点仍然感觉到自己活着,他也必须认为是死的(错误的宣告死亡)。个体节点必须遵守法定决定并下台。
      • b。通常情况下,⼀些东⻄在⼀个系统中只能有⼀个。如单主复制中的领导者节点、锁等。
        如果⼀个节点继续表现为“天选者”,即使⼤多数节点已经声明它已经死了,则在考虑不周的系统中可能会导致问题。

防护令牌:当使⽤锁或租约来保护对某些资源的访问时,需要确保⼀个被误认为⾃⼰是“天选者”的节点不能中断系统的其它部分。实现这⼀⽬标的⼀个相当简单的技术就是防护令牌。(fencing)屏蔽令牌保证它是单调递增,资源仅接受最新的写入。
- c。请注意,这种机制要求资源本身在检查令牌⽅⾯发挥积极作⽤,通过拒绝使⽤旧的令牌,⽽不是已经被处理的令牌来进⾏写操作——仅仅依靠客户端检查⾃⼰的锁状态是不够的。

- 拜占庭故障

	- 拜占庭故障:在不信任的环境中达成共识的问题被称为拜占庭将军问题。

	- 拜占庭容错

		- 拜占庭容错:当⼀个系统在部分节点发⽣故障、不遵守协议、甚⾄恶意攻击、扰乱⽹络时仍然能继续正确⼯作,称之为拜占庭容错(Byzantine fault-tolerant)
		- 拜占庭容错相当复杂&实现成本很高:在本书讨论的那些系统中,我们通常可以安全地假设没有拜占庭式的错误。在你的数据中⼼⾥,

所有的节点都是由你的组织控制的(所以他们可以信任),辐射⽔平⾜够低,内存损坏不是⼀个⼤问题。
制作拜占庭容错系统的协议相当复杂【84】,部署拜占庭容错解决⽅案的成本使其变得不切实际。

	- 弱谎言形式

		- 弱谎言形式提供简单实用的可靠性保证:尽管我们假设节点通常是诚实的,但值得向软件中添加防⽌“撒谎”弱形式的机制——例如,由硬件问题导致的⽆效消息,软件错误和错误配置。这种保护机制并不是完全的拜占庭容错,但它们仍然是简单⽽实⽤的步骤,以提⾼可靠性。

- 系统模型与实现

	- 算法

		- 有很多算法被设计以解决分布式系统问题,即容忍分布式系统的各种故障。

	- 系统模型:这个模型是⼀个抽象,描述⼀个

【算法】可能承担的事情。以某种方式将我们期望在系统中发生的错误形式化。

		- 定时假说系统模型

			- a。同步模型:假设⽹络延迟,进程暂停和和时钟误差都是有界限的,即假设⽹络延迟,暂停和时钟漂移将永远不会超过某个固定的上限。需要注意⽆限延迟的实际情况。
			- b。部分同步模型:⼀个系统在⼤多数情况下像⼀个同步系统⼀样运⾏,但有时候会超出⽹络延迟,进程暂停和时钟漂移的界限。
			- c。异步模型:不允许对时机做任何假设。

		- 节点失效系统模型

			- a。崩溃-停⽌故障(crash-stop):意味着节点可能在任意时刻突然停⽌响应,此后该节点永远消失——它永远不会回来
			- b。崩溃-恢复故障(crashrecovery):假设节点可能会在任何时候崩溃,但也许会在未知的时间之后再次开始响应。节点具有稳定的存储且会在崩溃中保留,⽽内存中的状态会丢失。
			- c。拜占庭(任意)故障:节点可以做(绝对意义上的)任何事情,包括试图戏弄和欺骗其他节点。
			- 对于真实系统的建模,具有崩溃-恢复故障(crash-recovery)的部分同步模型(partialsynchronous)通常是最有⽤的模型。

	- 算法的正确性

九.一致性与共识

  • 一致性保证

    • 最终一致性:如果你在同⼀时刻问两个不同副本相同的问题,可能会得到两个不同的答案。但如果停止向数据库写入数据并等待一段不确定时间,那么最终读取请求会得到相同的答案。
    • 分布式⼀致性模型和我们之前讨论的事务隔离级别的层次结构有⼀些相似之处。但它们⼤多是⽆关的问题:事务隔离主要是为了,避免由于【同时执⾏事务⽽导致的竞争状态】,⽽分布式⼀致性主要关于,⾯对延迟和故障时,如何【协调副本间的状态】。
  • 一致性模型

    • 线性一致性

      • 最强的一致性模型之一。
        也称为原⼦⼀致性(atomic consistency) 【7】,强⼀致性(strong consistency),⽴即⼀致性(immediate consistency)或外部⼀致性(external consistency )【8】)。
        它是⼀个新鲜度的保证(recency guarantee)。

      • 线性⼀致性背后的基本思想很简单:使系统看起来好像只有⼀个数据副本。如果数据库可以提供只有⼀个副本的假象(即,只有⼀个数据副本)。那么每个客户端都会有相同的数据视图,且不必担⼼【复制】滞后了。

      • 线性⼀致性的要求是,操作标记的连线总是按时间(从左到右)向前移动,⽽不是向后移动。
        要求新鲜性保证:⼀旦新的值被写⼊或读取,所有后续的读都会看到写⼊的值,直到它被再次覆盖。

      • 应用场景

        • 唯⼀性约束
        • 锁定和领导选举
        • 跨信道的时序依赖
      • 实现:

        • 方案一:真的只⽤⼀个数据副本。

          • 优点:简单
          • 缺点:节点失效时服务不可用、甚至有数据丢失风险。
        • 方案二:单主同步复制。

          • 它们可能(protential)是线性⼀致性的 4 。
          • 优点:可靠,单节点失效时,保持服务可用(只读)。
          • 缺点:性能低下。
        • 方案三:共识算法。

          • 共识协议包含防⽌【脑裂】和【陈旧副本】的措施。可以安全地实现线性⼀致性存储。如zookeeper
      • 线性⼀致性的代价

        • 面临的问题:

          • 如果应⽤需要线性⼀致性:当某些副本因为⽹络问题与其他副本断开连接,那么这些副本掉线时不能处理请求。请求必须等到⽹络问题解决,或直接返回错误。⽆论哪种⽅式,服务都不可⽤(unavailable)。
          • 如果应⽤不需要线性⼀致性:那么某个副本即使与其他副本断开连接,也可以独⽴处理请求(例如多主复制)。在这种情况下,应⽤可以在⽹络问题前保持可⽤,但其⾏为不是线性⼀致的。
        • CAP定理:不需要线性⼀致性的应⽤对⽹络问题有更强的容错能⼒。

          • CAP有时以这种⾯⽬出现:⼀致性,可⽤性和分区容忍:三者只能择其⼆。这种说法很有误导性【32】,因为⽹络分区并不是⼀个选项:不管你喜不喜欢它都会发⽣【38】。
          • 在⽹络正常⼯作的时候,系统可以提供⼀致性(线性⼀致性)和整体可⽤性。发⽣⽹络故障时,你必须在【线性⼀致性】和【整体可⽤性】之间做出选择。
          • ⼀个更好的表达CAP的⽅法可以是⼀致
            的,或者在分区时可⽤【39】。
        • 为了线性一致性牺牲性能和可用性 vs 或者为了提高性能、可用性而使用弱一致性。

  • 顺序保证

    • 全序

    • 偏序

    • 顺序和因果顺序

      • 因果顺序不是全序的
      • 关系:线性⼀致性隐含着(implies)因果关系:任何线性⼀致的系统都能正确保持因果性【7】。
      • 线性⼀致性简单、易懂,但可能会损害系统的性能和可⽤性,尤其是在系统具有严重的⽹络延迟的情况下。
        在许多情况下,看上去需要线性⼀致性的系统,实际上需要的只是因果⼀致性,而因果⼀致性可以更⾼效地实现。
    • 序列号顺序

      • 我们可以使⽤序列号(sequence nunber)或时间戳(timestamp)来排序事件。时间戳不⼀定来⾃时钟,它可以来⾃⼀个【逻辑时钟】(logical clock),这是【⼀个⽤来⽣成标识操作的数字序列的算法】,典型实现是使⽤⼀个每次操作⾃增的计数器。

      • 这样的序列号或时间戳是【紧凑的】(只有⼏个字节⼤⼩),它提供了⼀个【全序关系】:也就是说每操作都有⼀个唯⼀的序列号,⽽且总是可以⽐较两个序列号,确定哪⼀个更⼤(即哪些操作后发⽣)。

      • 非因果序列号生成器

        • 适用于主库不存在情况,可能因为使⽤了多主数据库或⽆主数据库,或者因为使⽤了分区的数据库。

        • 实现方案:

          • a。每个节点都可以⽣成⾃⼰独⽴的⼀组序列号。例如有两个节点,⼀个节点只能⽣成奇数,⽽另⼀个节点只能⽣成偶数。
          • b。将具有⾜够⾼分辨率的时钟时间戳附加到每个操作上。
          • c。可以预先分配序列号区块。例如,节点 A 可能要求从序列号1到1,000区块的所有权,⽽节点 B 可能要求序列号1,001到2,000区块的所有权。
        • 问题:⽣成的序列号与因果不⼀致。
          但⽐单⼀主库的⾃增计数器性能表现要好,并且更具可扩展性。

      • 兰伯特时间戳

        • 定义:兰伯特时间戳就是两者的简单组合:(计数器,节点ID)。它提供了⼀个【全序】:如果你有两个时间戳,则计数器值⼤者是更⼤的时间戳。如果计数器值相同,则节点ID越⼤的,时间戳越⼤。
        • 特点:Lamport时间戳【解决了非因果序列号生成器的问题】,它提供了与因果关系⼀致的总排序。
        • 关键思想:每个节点和每个客户端跟踪迄今为⽌所⻅到的最⼤计数器值,并在每个请求中包含这个最⼤计数器值。当⼀个节点收到最⼤计数器值⼤于⾃身计数器值的请求或响应时,它⽴即将⾃⼰的计数器设置为这个最⼤值。
        • 缺点:适用于【事后确定胜利者】场景,需要实时确定的场景会有问题。
    • 全序⼴播(total order broadcast)

      • 但是在分布式系统中,让所有节点对同⼀个全局操作顺序达成⼀致可能相当棘⼿。在上⼀节中,我们讨论了按时间戳或序列号进⾏排序,但发现它还不如单主复制给⼒(如果你使⽤时间戳排序来实现唯⼀性约束,⽽且不能容忍任何错误)。

      • 定义:如前所述,单主复制通过选择⼀个节点作为主库来确定操作的全序,并在主库的单个CPU核上对所有操作进⾏排序。
        接下来的挑战是,如果吞吐量超出单个主库的处理能⼒,这种情况下【如何扩展系统】;以及,如果主库失效,【如何处理故障切换】。这个问题被称为全序⼴播。

      • 属性:全序⼴播通常被描述为在【节点间交换消息的协议】。它要满⾜两个安全属性,即使节点或⽹络出现故障:

        • 1.可靠交付(reliable delivery)
          没有消息丢失,即如果消息被传递到⼀个节点,它将被传递到所有节点。
        • 2.全序交付(totally ordered delivery)*
          消息以相同的顺序传递给每个节点。
      • 应用:

        • 1.数据库状态机复制(state machine replication):如果每个消息都代表⼀次数据库的写⼊,且每个副本都按相同的顺序处理相同的写⼊,那么副本间将相互保持⼀致。
        • 2.实现可序列化的事务。
        • 3.实现提供【防护令牌】的锁服务:序列号可以当成防护令牌⽤,因为它是单调递增的。在ZooKeeper中,这个序列号被称为 zxid。
      • 全序广播 vs 线性一致性存储

        • 全序广播是异步的,消息保证以固定的顺序可靠地传送,但是【不能保证消息何时被送达】。
          线性一致性是【新鲜度】的保证,即读取一定能看见最新的写入值。

        • 使⽤全序⼴播实现线性⼀致的存储:

          • 可以通过将全序⼴播当成仅追加⽇志致的CAS操作来实现,选择冲突写⼊中的第⼀个作为胜利者,并中⽌后来者,以此确定所有节点对某个写⼊是提交还是中⽌达成⼀致。
        • 使⽤线性⼀致性存储实现全序⼴播:

          • 最简单的⽅法是假设你有⼀个线性⼀致的寄存器来存储⼀个整数,并且有⼀个原⼦⾃增并返回操作【28】。
          • 该算法很简单:每个要通过全序⼴播发送的消息⾸先对线性⼀致寄存器执⾏⾃增并返回操作。然后将从寄存器获得的值作为序列号附加到消息中。然后你可以将消息发送到所有节点(重新发送任何丢失的消息),⽽收件⼈将按序列号连续发送消息。
  • 分布式事务与共识

    • 共识是分布式计算中最重要也是最基本的问题之⼀。

      • 现在我们已经讨论了复制(第5章),事务(第7章),系统模型(第8章),线性⼀致以及全序(本章),我们终于准备好解决共识问题了。

      • 场景

        • 领导选举
        • 原子提交:在⽀持跨多节点或跨多分区事务的数据库中,我们必须让所有节点对事务的结果达
          成⼀致:要么全部中⽌/回滚(如果出现任何错误),要么它们全部提交(如果没有出错)。这个共识的例⼦被称为原⼦提交(atomic commit)问题
    • 原子提交与二阶段提交2PC

      • 单节点原子提交

        • 对于在单个数据库节点执⾏的事务,原⼦性通常由存储引擎实现。当客户端请求数据库节点提交事务时,数据库将使事务的写⼊持久化(通常在预写式⽇志中:参阅“使B树可靠”),然后将【提交记录】追加到磁盘中的⽇志⾥。如果数据库在这个过程中间崩溃,当节点重启时,事务会从⽇志中恢复:如果提交记录在崩溃之前成功地写⼊磁盘,则认为事务被提交;否则来⾃该事务的任何写⼊都被回滚。
        • 在单个节点上,事务的提交主要取决于数据持久化落盘的顺序:⾸先是数据,然后是【提交记录】。事务提交或终⽌的关键决定时刻是磁盘完成写⼊【提交记录】的时刻:在此之前,仍有可能中⽌(由于崩溃),但在此之后,事务已经提交,即使数据库崩溃。因此,是单⼀的设备(连接到单个磁盘驱动的控制器,且挂载在单台机器上)使得提交具有原⼦性。
      • 分布式原子提交

        • 引言:如果⼀个事务中涉及多个节点,仅向所有节点发送提交请求并独⽴提交每个节点的事务是不够的。这样很容易发⽣【违反原⼦性】的情况:提交在某些节点上成功,⽽在其他节点上失败。
          两阶段提交(two-phase commit) 是⼀种⽤于实现跨多个节点的原⼦事务提交的算法,即确保所有节点提交或所有节点中⽌。 它是分布式数据库中的经典算法。

          • PS:事务提交必须是不可撤销的 —— 事务提交之后,你不能改变主意,并追溯性地中⽌事务。这个规则的原因是,⼀旦数据被提交,其结果就对其他事务可⻅,因此其他客户端可能会开始依赖这些数据。这个原则构成了【读已提交隔离等级】的基础,在“读已提交”⼀节中讨论了这个问题。如果⼀个事务在提交后被允许中⽌,所有那些读取了已提交却⼜被追溯声明不存在数据的事务也必须回滚。
        • 两阶段提交

          • 两阶段提交(2PC, twophase commit)算法是解决原⼦提交问题最常⻅的办法。2PC是⼀种共识算法。

          • 名词

            • 协调者coordinator/事务管理器transaction manager

              • a。协调者通常不会出现在单节点事务中。
                b。协调者通常在请求事务的相同应⽤进程中以库的形式实现(例如,嵌⼊在Java EE容器中),但也可以是单ᇿ的进程或服务。
            • 参与者(participate)

              • 正常情况下,2PC事务以应⽤在多个数据库节点上读写数据开始。我们称这些数据库节点为参与者。
          • 基本流程

            • 阶段 1 : 当应⽤准备提交时,协调者发送⼀个【准备(prepare)请求】到每个节点,询问它们是否能够提交,并跟踪参与者的响。
            • 阶段 2 :
              a。如果所有参与者都回答“是”,表示它们已经准备好提交,那么协调者发出【提交(commit)请求】,然后提交真正发⽣。
              b。如果任意⼀个参与者回复了“否”,则协调者在阶段2 中向所有节点发送【中⽌(abort)请求】。
          • 类比:⻄⽅的传统婚姻仪式

            • 司仪-> 协调者, 新郎新娘 -> 参与者
        • 系统承诺:

          • 承诺1.当参与者投票“是”时,它承诺它稍后肯定能够提交(尽管协调者可能仍然选择放弃)。

            • 参与者收到准备请求时,需要确保在任意情况下都的确可以提交事务。这包括将所有事务数据写⼊磁盘(出现故障,电源故障,或硬盘空间不⾜都不能是稍后拒绝提交的理由)以及检查是否存在任何冲突或违反约束。通过向协调者回答“是”,节点承诺,只要请求,这个事务⼀定可以不出差错地提交。换句话说,参与者放弃了中⽌事务的权利,但没有实际提交。
          • 承诺2.⼀旦协调者做出决定,这⼀决定是不可撤销的。

            • 当协调者收到所有准备请求的答复时,会就提交或中⽌事务作出明确的决定(只有在所有参与者投赞成票的情况下才会提交)。协调者必须把这个决定写到磁盘上的事务⽇志中,如果它随后就崩溃,恢复后也能知道⾃⼰所做的决定。这被称为提交点(commit point)。
            • ⼀旦协调者的决定落盘,提交或放弃请求会发送给所有参与者。如果这个请求失败或超时,协调者必须永远保持重试,直到成功为⽌。没有回头路:如果已经做出决定,不管需要多少次重试它都必须被执⾏。如果参与者在此期间崩溃,事务将在其恢复后提交——由于参与者投了赞成,因此恢复后它不能拒绝提交。
          • 两阶段提交协议包含的2个关键的“不归路”点,保证了2PC的原⼦性。

          • 类比:⻄⽅的传统婚姻仪式

        • 协调者失效

          • 存疑:⼀旦参与者收到了准备请求并投了“是”,就不能再单⽅⾯放弃 —— 必须等待协调者回答事务是否已经提交或中⽌。如果此时协调者崩溃或⽹络出现故障,参与者什么也做不了只能等待。参与者的这种事务状态称为【存疑(in doubt)】的 或不确定(uncertain)的。
          • 完成2PC的唯⼀⽅法是等待协调者恢复。这就是为什么协调者必须在向参与者发送提交或中⽌请求之前,将其提交或中⽌决定写⼊磁盘上的事务⽇志:协调者恢复后,通过读取其事务⽇志来确定所有存疑事务的状态。
      • 三阶段提交

        • 两阶段提交被称为阻塞(blocking)原⼦提交协议,因为存在2PC可能卡住并等待协调者恢复的情况。
        • 三阶段提交(3PC)的算法:3PC假定⽹络延迟有界,节点响应时间有限;在⼤多数具有⽆限⽹络延迟和进程暂停的实际系统中它并不能保证原⼦性。
        • 在具有⽆限延迟的⽹络中,超时并不是⼀种可靠的故障检测机制,因为即使没有节点崩溃,请求也可能由于⽹络问题⽽超时。出于这个原因,2PC仍然被使⽤,尽管⼤家都清楚可能存在协调者故障的问题。
    • 实践中的分布式事务

      • 缺点:分布式事务的名声毁誉参半。一方面它难以实现,另一方面性能损失严重,据报告称Mysql中的分布式事务比单节点事务慢10倍以上。

        • 成本来源:两阶段提交所固有的性能成本,⼤部分是由于崩溃恢复所需的额外强制刷盘( fsync )【88】以及额外的⽹络往返。
      • XA事务

        • XA(扩展架构(eXtended Architecture)的缩写)是跨异构技术实现两阶段提交的标准。
        • XA不是⼀个⽹络协议——它只是⼀个⽤来与事务协调者连接的C API。
        • XA假定你的应⽤使⽤⽹络驱动或客户端库来与参与者进⾏通信(数据库或消息服务)。如果驱动⽀持XA,则意味着它会调⽤XA API 以查明操作是否为分布式事务的⼀部分 —— 如果是,则将必要的信息发往数据库服务器。驱动还会向协调者暴露回调接⼝,协调者可以通过回调来要求参与者准备,提交或中⽌。
        • 事务协调者需要实现XA API。
      • 存疑持有锁

        • 为什么我们这么关⼼存疑事务?系统的其他部分就不能继续正常⼯作,⽆视那些终将被清理的存疑事务吗?
          问题在于锁(locking)。如果要使⽤可序列化的隔离等级,则使⽤两阶段锁定的数据库会为事务所读取的⾏加上共享锁(参⻅“两阶段锁定(2PL)”)。当这些锁被持有时,其他事务不能修改这些⾏。直到存疑事务被解决。

        • 从协调者故障中恢复

          • 理论上,如果协调者崩溃并重新启动,它应该⼲净地从⽇志中恢复其状态,并解决任何存疑事务。然⽽在实践中,【孤⽴(orphaned)的存疑】事务确实会出现,即⽆论出于何种理由,协调者⽆法确定事务的结果(例如事务⽇志已经由于软件错误丢失或损坏)。
            这些事务⽆法⾃动解决,所以它们永远待在数据库中,持有锁并阻塞其他事务。即使重启数据库服务器也⽆法解决这个问题。
            唯⼀的出路是让管理员⼿动决定提交还是回滚事务。
          • 启发式决策(heuristic decistions):
            许多XA的实现都有⼀个叫做启发式决策的紧急逃⽣舱⼝:允许参与者单⽅⾯决定放弃或提交⼀个存疑事务,⽽⽆需协调者做出最终决定。要清楚的是,这⾥启发式是【可能破坏原⼦性】(probably breaking atomicity)的委婉说法,因为它【违背了两阶段提交的系统承诺】。

因此,启发式决策只是为了逃出灾难性的情况⽽准备的,⽽不是为了⽇常使⽤的。

	- 分布式事务的限制

		- 核⼼认识:事务协调者本身就是⼀种数据库(存储了事务的结果),需要像其他重要数据库⼀样⼩⼼地打交道。
		- 限制1.如果协调者没有复制,只是在单台机器上运行,那么他是整个系统的失效单点。ps:即存疑问题
		- 限制2. 应⽤服务器不再是⽆状态的。

许多服务器端应⽤都是使⽤⽆状态模式开发的(受HTTP的⻘睐),所有持久状态都存储在数据库中,因此具有应⽤服务器可随意按需添加删除的优点。
但是,当协调者成为应⽤服务器的⼀部分时,它会改变部署的性质。突然间,协调者的⽇志成为持久系统状态的关键部分—— 与数据库本身⼀样重要,因为协调者⽇志是为了在崩溃后恢复存疑事务所必需的。这样的应⽤服务器不再是⽆状态的了。
- 限制3. 由于XA需要兼容各种数据系统,因此它必须是所有系统的最⼩公分⺟。例如,它不能检测不同系统间的死锁,因为这将需要⼀个标准协议来让系统交换每个事务正在等待的锁的信息。以及无法与SSI 协同⼯作,因为这需要⼀个跨系统定冲突的协议。
- 限制4. 2PC成功提交⼀个事务需要所有参与者的响应。因此,如果系统的任何部分损坏,事务也会失败。因此,分布式事务⼜有【扩⼤失效】(amplifying failures)的趋势,这⼜与我
们构建容错系统的⽬标背道⽽驰。

- 容错共识

	- 概念

		- ⾮正式地,共识意味着让⼏个节点就某事达成⼀致。共识问题通常形式化如下:⼀个或多个节点可以提议(propose)某些值,⽽共识算法决定(decides)采⽤其中的某个值。

	- 共识算法必须满足的性质

		- a。协商一致性(Uniform agreement):所有节点都接受相同的决议。
		- b。诚实性(Integrity):所有节点不能反悔,即对一项提议不能有两次决定。
		- c。合法性(Validity):如果一个节点决定了值v,则v一定是由某个节点所提议的。
		- d。可终止性(Termination):节点如果没有崩溃,则最终一定可以达成协议。

	- 可终⽌性是⼀种活性属性,⽽另外三种是安全属性。

		- i。协商一致性和诚实性定义了共识的【核⼼思想】:决议一直的结果,⼀旦决定,你就不能改变。

ii。合法性属性束腰式为了排除一些无意义的方案。
iii。可终止性引入了【容错】思想:⼀个共识算法不能简单地永远闲坐着等死 ,它必须取得进展。即使部分节点出现故障,其他节点也必须达成⼀项决定。

	- 共识算法和全序⼴播

		- ⼤多数这些算法实际上并不直接使⽤这⾥描述的形式化模型(提议并决定单个值,同时满足以上4个属性)。取而代之的是全序⼴播算法,全序⼴播将消息按照相同的顺序发送到所有节点,有且只有一次。
		- 全序⼴播相当于重复进⾏多轮共识(每一轮共识的决定对应于一条消息):

			- 由于协商一致性,所有节点决定以相同的顺序发送相同的消息。
			- 由于诚实性,消息不能重复。
			- 由于合法性,消息不会被破坏,也不会凭空捏造。
			- 由于可终止性,消息不会丢失。

	- 时代编号和法定⼈数

		- 迄今为⽌所讨论的所有共识协议,在内部都以某种形式使⽤⼀个领导者,但它们并不能保证领导者是独⼀⽆⼆的。相反,它们可以做出更弱的保证:协议定义了⼀个时代编号(epoch number),并确保在每个时代中,领导者都是唯⼀的。

			- 时代编号在Paxos中称为投票编号(ballot number,在视图戳复制中成为视图编号(view number,以及在Raft中称为任期号码(term number)),

		- 对领导者想要做出的每⼀个决定,都必须将提议值发送给其他节点,并等待法定⼈数的节点响应并赞成提案。法定⼈数通常(但不总是)由多数节点组成【105】。只有在没有意识到任何带有更⾼时代编号的领导者的情况下,⼀个节点才会投票赞成提议。
		- 因此,我们有两轮投票:第⼀次是为了选出⼀位领导者,第⼆次是对领导者的提议进⾏表决。关键的洞察在于,这两次投票的法定⼈群必须相互【重叠】(overlap):如果⼀个提案的表决通过,则⾄少得有⼀个参与投票的节点也必须参加过最近的领导者选举【105】

	- 共识的局限性

		- 优点

			- 共识算法对于分布式系统来说是⼀个巨⼤的突破:它为其他充满不确定性的系统带来了基础的安全属性(协商一致性,诚实性和合法性),然⽽它们还能保持容错(只要多数节点正常⼯作且可达,就能取得进展)。

		- 缺点

			- 在异步复制模式下,节点发生故障切换时,一些已经提交的数据可能会丢失。
			- 共识系统通常依靠超时来检测失效的节点。在网络糟糕的情况下,会导致频繁的领导者选举,继而导致糟糕的性能表现,
			- 共识系统需要至少三个节点才能容忍单节点故障,如果⽹络故障切断了某些节点同其他节点的连接,则只有多数节点所在的⽹络可以继续⼯作,其余部分将被阻塞(参阅“线性⼀致性的代价”)。

- 成员与协调服务
  • 小结

    • 在本章中,我们从⼏个不同的⻆度审视了关于⼀致性与共识的话题。我们深⼊研究了:

      • 线性一致性:最强的一致性模型,其⽬标是使多副本数据看起来好像只有⼀个副本⼀样,并使其上所有操作都原⼦性地⽣效。线性⼀致性简单易懂,但性能低下,尤其是在网络延迟很大的环境中。
      • 因果一致性:因果⼀致性为我们提供了⼀个较弱的⼀致性模型:某些事件可以是并发的,所以版本历史就像是⼀条不断分叉与合并的时间线。因果⼀致性没有线性⼀致性的协调开销,⽽且对⽹络问题的敏感性要低得多。
      • 即使做到了因果顺序,但有些事情也需要通过达成共识才能做出决定。达成共识意味着所有节点⼀致同意所做决定,且这⼀决定不可撤销。
    • 通过深⼊挖掘,我们发现很⼴泛的⼀系列问题实际上都可以归结为共识问题,并且彼此等价。
      从这个意义上来讲,如果你有其中之⼀的解决⽅案,就可以轻易将它转换为其他问题的解决⽅案。这些等价问题包括:

      • 线性⼀致性的CAS寄存器
      • 原⼦事务提交
      • 全序⼴播
      • 锁和租约
      • 成员/协调服务
      • 唯⼀性约束
    • 单领导者数据库可以提供线性⼀致性,唯一性约束,完全有序的复制日志等,但也需要共识算法

      • 针对领导者节点失效或者网络中断导致领导者不可达的异常问题,应对方案有三种:

        • i。等待领导者节点恢复,接受系统将在这段时间阻塞的事实。但不能达成共识,因为不满足可终止性。
          ii。人工故障切换,故障切换的速度受人类行动速度的限制。
          iii。使用共识算法自动选择一个新的领导者。
    • 如果你发现⾃⼰想要解决的问题可以归结为共识,并且希望它能容错,使⽤⼀个类似ZooKeeper的东⻄是明智之举。像ZooKeeper这样的⼯具为应⽤提供了“外包”的共识、故障检测和成员服务。

posted @ 2021-12-01 11:49  xuxh120  阅读(1423)  评论(0编辑  收藏  举报