分布式 2024年12月29日

Patterns of Distributed Systems

分布式系统模式
书籍封面

Patterns of Distributed Systems

作者:Unmesh Joshi 出版日期:2023-11-14 出版社:Addison-Wesley Professional
该书通过分析Apache Kafka、Cassandra等开源项目的代码,提炼出分布式系统中常见的设计模式。书中涵盖了数据复制、数据分区以及节点间通信等方面的一系列模式,例如写前日志(Write-Ahead Log)、分段日志(Segmented Log)、领导者与追随者(Leader and Followers)、一致性核心(Consistent Core)、八卦传播(Gossip Dissemination)以及请求批处理(Request Batch)等,并结合代码示例解释了每种模式的原理、实现以及应用场景,旨在帮助读者更好地理解和构建分布式系统。

第一部分:导论与概述

  • 导论:本书的核心概念是将复杂的分布式系统抽象成可复用的模式。通过命名组件、描述行为和交互方式,构建了一个分布式系统的模式语言,使系统可以被视为可组合的“乐高积木”。 这有助于避免在讨论分布式系统时因术语歧义而产生的误解。
  • 概述:分布式系统的基本构建块包括数据分区和复制。本书首先关注数据复制,然后介绍分区。一个简单的示例数据记录被复制到多个节点,展示了分布式系统中数据一致性的挑战。例如,如果一个节点在更新数据过程中崩溃,会导致数据不一致。 使用领导者(Leader)模式可以解决这个问题,由一个领导者节点处理所有更新请求。 当领导者失效时,需要通过心跳(Heartbeat)机制来检测,并选举新的领导者。 高水位标记(High-Water Mark) 用于确保所有节点在领导者切换后保持数据一致性。

第二部分:数据复制模式

  • 写前日志(Write-Ahead Log, WAL)

    • 问题:如何在单服务器上实现持久化的原子更新?
    • 解决方案:在修改数据之前,将所有操作记录到日志中。只有在日志写入成功后,才将操作应用到数据存储。 WAL 确保了即使发生崩溃,数据也可以从日志中恢复。
    • 实现考虑:WAL 用于提供原子更新,而一致性和隔离需要通过并发控制机制(例如锁)来实现。
    • 与其他模式的比较:与事件溯源模式相比,WAL 更侧重于数据持久性,而不是记录所有事件的历史。
  • 分段日志(Segmented Log)

    • 问题:如何高效地管理大型的写前日志?
    • 解决方案:将日志分割成多个段,可以独立地管理每个段,例如,可以删除或归档旧的段。
  • 低水位标记(Low-Water Mark): - 问题:如何在复制的数据中安全地删除旧的日志条目? - 解决方案:使用低水位标记来指示所有副本都已经应用的日志索引,之后就可以安全地删除此索引之前的日志条目。低水位标记可以是基于快照的或基于时间的。
  • 心跳(Heartbeat)

    • 问题:如何检测分布式系统中节点的失败?
    • 解决方案:节点定期发送心跳消息给其他节点。如果一个节点在指定的时间内没有收到心跳,它会被认为是失败的。 心跳机制可以用于小集群(使用基于共识的系统)或大集群(使用基于 Gossip 的协议)。
    • 技术考量:使用单套接字通道时,需要避免队头阻塞
  • 领导者和追随者(Leader and Followers)

    • 问题:如何在分布式系统中实现数据复制和一致性?
    • 解决方案:选举一个领导者来处理所有的写操作,然后将这些写操作复制给追随者。这确保了所有节点上的数据保持一致。
    • 技术考量:使用单一更新队列来避免同步和锁定的麻烦。
  • Paxos

    • 问题:如何在不可靠的网络中达成共识?
    • 解决方案:Paxos 算法分为三个阶段:准备阶段接受阶段提交阶段
      • 准备阶段,提议者(proposer)向所有接受者(acceptor)发送准备请求,并获取承诺。
      • 接受阶段,提议者发送一个提议值,并请求接受者接受。
      • 提交阶段,提议者通知所有节点值已被选定。
    • 核心概念:Paxos 使用多数仲裁来确保只有一个值被选定。
  • 复制日志(Replicated Log)

    • 问题:如何保证所有节点按照相同的顺序应用更新?
    • 解决方案:通过一个复制日志记录所有更新操作,确保所有的副本按照相同的顺序来执行这些操作。 复制日志通常与领导者和追随者模式一起使用,其中领导者将日志复制到追随者。
    • 技术考量:领导者需要知道之前大多数仲裁可能已经复制的日志条目。
  • 单一更新队列(Singular Update Queue)

    • 问题:如何处理来自多个生产者线程的并发更新,并保证顺序执行?
    • 解决方案:使用一个单一的队列来处理所有更新请求,并使用一个专门的线程来从队列中读取并执行这些请求。 这确保了所有请求按照它们加入队列的顺序执行。
    • 技术考量:队列的选择和背压是重要考虑因素。
  • 请求等待列表(Request Waiting List)

    • 问题:当需要向多个节点发送请求时,如何跟踪并处理这些请求的响应?
    • 解决方案:使用一个请求等待列表来记录所有未完成的请求。当收到来自其他节点的响应时,将调用相应的回调函数。
    • 技术考量:每个请求都标记一个关联 ID,用于将响应与请求匹配。
  • 幂等接收器(Idempotent Receiver)

    • 问题:如何在网络不可靠的情况下处理重复请求?
    • 解决方案:确保服务器可以多次接收相同的请求,而不会产生副作用。 这可以通过检查请求是否已经处理过,或者使用唯一标识符来防止重复处理来实现。
    • 技术考量:需要考虑如何过期保存的客户端请求。
  • 追随者读取(Follower Reads)

    • 问题:如何允许追随者节点读取数据,同时保持一致性?
    • 解决方案:追随者可以读取数据,但如果请求的版本在追随者上不可用,则需要等待直到版本更新。
    • 技术考量: 追随者读取必须有超时,以防止无限等待。
  • 版本化值(Versioned Value)

    • 问题:如何在分布式系统中处理并发的写操作,并避免数据丢失?
    • 解决方案:为每个值分配一个版本号。当多个节点更新相同的值时,使用版本号来区分不同的更新。
  • 版本向量(Version Vector)

    • 问题:如何处理在不同节点上同时发生的并发更新?
    • 解决方案:使用版本向量来跟踪每个节点的更新。版本向量可以检测并发更新,并允许存储多个版本的值。
    • 核心概念:使用版本向量进行比较可以确定值的顺序关系:之前、之后或并发。

第三部分:数据分区模式

  • 固定分区(Fixed Partitions)

    • 问题:如何将数据均匀地分布在多个节点上?
    • 解决方案:将数据划分成固定数量的逻辑分区,然后将这些分区映射到集群节点。 分区数量通常是节点数量的倍数。 使用哈希函数来确定哪个分区存储特定的数据。
    • 技术考量:客户端库通过一致性核心获取分区到集群节点的映射。
  • 按节点数比例分配分区: - 问题:如何在节点数量变化时,保持数据分区均匀分配? - 解决方案:根据集群中节点的数量动态地调整分区数量。

  • 键范围分区(Key-Range Partitions)

    • 问题:如何支持范围查询,并允许按键的范围进行数据分区?
    • 解决方案:将数据按照键的范围划分成多个分区。 每个分区都包含一个特定范围内的键。
    • 技术考量:键范围分区支持自动分裂,以处理数据量增长。
  • 事务性键值存储

    • 问题:如何在分布式环境中提供ACID事务保证?
    • 解决方案:通过两阶段提交协议来协调跨多个节点的事务。
    • 技术考量:可以使用不同的等待策略来处理锁冲突,例如Wound-WaitWait-Die
  • 多版本并发控制(MVCC)

    • 问题:如何在不阻塞读取操作的情况下,实现并发的写操作?
    • 解决方案:为数据的每个版本分配一个时间戳,并且保留多个版本的数据。 读操作可以读取过去版本的数据,而不会被写操作阻塞。
  • Lamport 时钟

    • 问题:如何在分布式系统中维护事件的因果关系?
    • 解决方案:为每个事件分配一个逻辑时间戳,并确保如果一个事件发生在另一个事件之前,则它的时间戳小于另一个事件的时间戳。
    • 技术考量:Lamport 时钟为分布式系统提供了一种部分顺序
  • 混合时钟(Hybrid Clock)

    • 问题:如何在分布式系统中维护单调的时间戳,并反映真实时间?
    • 解决方案:结合系统时间和逻辑计数器来生成混合时间戳,可以确保时间的单调性,同时又能大致反映事件的真实时间。
    • 技术考量:混合时钟可用于实现多版本存储。
  • 时钟边界等待(Clock-Bound Wait)

    • 问题:如何在具有不确定时钟的分布式系统中,保证读写操作的一致性?
    • 解决方案:每个节点都维护一个时钟边界,并使用这个边界来等待写入操作完成,确保写入时间在过去。
    • 技术考量:读操作也需要等待直到确定可以读取到最新的数据。

第四部分:分布式系统模式

  • 一致性核心(Consistent Core)

    • 问题:如何在分布式系统中实现强一致性的元数据存储?
    • 解决方案:使用共识算法(例如 Raft)来实现一个强一致的键值存储,用于存储元数据。 一致性核心通常用于跟踪集群成员、注册租约以及实现服务发现。
    • 技术考量:使用单套接字通道与一致性核心通信。
  • 租约(Lease)

    • 问题:如何为分布式资源分配访问权限?
    • 解决方案:使用租约来授权访问特定的资源。 租约在一段时间后到期,需要定期刷新。 租约可以用于节点注册和故障检测。
    • 技术考量:租约通常在一致性核心上实现。
  • 状态监视(State Watch)

    • 问题:如何在集群状态发生变化时通知其他节点?
    • 解决方案:使用状态监视来注册对特定状态变化的监听器。当状态发生变化时,监听器会收到通知。
    • 技术考量:状态监视通常由一致性核心提供。
  • Gossip 传播

    • 问题:如何在分布式系统中传播节点的状态信息?
    • 解决方案:节点定期与其他节点交换状态信息,确保所有节点最终获得相同的状态。 Gossip 协议可以用于组播和故障检测。
    • 技术考量:Gossip 协议是最终一致性的。
  • 涌现领导者 (Emergent Leader)

    • 问题:如何在一个去中心化的集群中选举领导者?
    • 解决方案:集群节点通过互相通信来选择领导者,而不是依赖于一个中心化的协调器。
    • 技术考量:需要处理脑裂问题。
  • 单套接字通道(Single-Socket Channel):

    • 问题:如何在服务器之间建立高效的通信通道?
    • 解决方案:使用单个套接字通道来发送和接收请求,避免了多余的连接开销。
    • 技术考量:单套接字通道使用阻塞的 IO 操作。
  • 请求批处理(Request Batch)

    • 问题:如何减少网络请求的开销?
    • 解决方案:将多个请求打包到一个批次中,然后将这个批次发送到服务器。 服务器解包这个批次,并处理其中的每个请求。

总结

这本书深入探讨了构建可靠、可扩展的分布式系统所需的各种模式。它不仅解释了每个模式如何解决特定问题,还提供了技术考量和实际应用示例。 这些模式可以作为构建分布式系统的基础,帮助开发者更好地理解和设计复杂的系统。