分布式系统

Riicarus大约 8 分钟架构设计分布式系统分布式

分布式系统

课程: MIT 6.824

1 简介

分布式系统设计的挑战:

  • 并发(Concurrency)
  • 局部性错误(Partial Faults)
  • 性能(Performance)

分布式系统的基础设施(Infrastructure): --> 抽象

  • 存储(Storage)
  • 通信(Communication)
  • 计算(Computing)

重点在于如何将这些部分构建为抽象接口, 让用户对其是否为分布式系统无感知, 感知上认为是非分布式系统.
即: 复杂行为 --> 简单行为.

分布式系统基础设施的实现:

  • RPC(Remote Procedure Call): 隐藏不可靠网络通信过程.
  • Threads/Routines & Concurrency Control: 结构化并发编程方式.

分布式系统的性能问题:

  • 可扩展性(Scalability): 2 倍资源 --> 2 倍性能(简单实现加钱就能加服务).

分布式系统的容错性(Fault Tolerance):

  • 隐藏错误处理过程, 否则会有过多的错误需要手动处理.
  • 可用性(Availability): 提供范围内可控的容错.
  • 可恢复性(Recoverability): 出现错误可以恢复到正常状态.

容错性保证:

  • 非易失性存储(Non-volatile Storage): 持久化存储及其管理设备
  • 副本(Replication): 不同副本所在的节点物理环境应该隔离, 避免出现相同的故障.

分布式系统中的一致性问题:

  • 强一致性
  • 弱一致性: 更多研究如何使弱一致性对应用更可用.

MapReduce:

  • 将工作拆解为多个 Map 任务和多个 Reduce 任务.
  • Map 任务负责从输入中根据 MapFunction 筛选出需要的数据, 称为中间数据(inter-media data), 传给 Reduce 任务.
  • Reduce 汇聚多个 Map 任务传递的中间数据, 然后统计出对应的结果.
MapReduceIntro
MapReduceIntro

2 并发

本课程使用 go 来实现, 因此讨论 goroutine.

为什么要并发?

  • IO 并发
  • 并行化程序
  • 便利性(如: 定时任务)

并发的替代: 事件驱动(Event-Driven), 但是只能 IO 并发, 不能使用 CPU 的并行资源.

在 go 中, 可以使用 --race 参数来大致判断是否存在并发冲突的问题.

3 GFS

GFS == Google File System

分布式系统存储面临的问题:

  • 性能(Performance) --> 系统分片(Sharding)
  • 分片带来不可预测的错误(Faults) --> 容错系统(Fault Tolerance)
  • 容错系统 --> 副本(Replication)
  • 副本 --> 一致性问题(In Consistency)
  • 一致性保证 --> 低性能(Low Performance)

得出结论: 如果要强一致性, 会与分布式系统的性能要求相悖.

3.1 GFS 的解决方案

希望达到的效果:

  • 存储量大, 访问速度快(Big & Fast)
  • 全局的(Global)
  • 文件分片存储(Sharding)到不同的磁盘/机器中
  • 错误自动恢复(Automatic Recovery)
  • 单数据中心
  • 内部使用
  • 针对大文件顺序读取(Big Sequential Access)
GFS Architecture
GFS Architecture

块服务器(Chunk Server):

  • 一个 Master 由很多个 Chunk Server 组成.
  • 一个 Chunk Server 中保存文件的连续片段, 每个片段大小为 64MB.
  • 一个大文件会被切分(Sharding)为多个连续片段, 保存在不同的 Chunk Server 中.

在 GFS 中, 寻找特定的数据块(Chunk)需要两张表, 姑且称之为 FileMapChunkMap.
用 Go 来描述两张表:

type FileName string
type ChunkHandlerId int64

// a file is sharding into many pieces and stored into many chunk servers
type FileMap map[FileName][]ChunkHandlerId // nv

type ChunkHandler struct {
  // a chunk has some replications
  ChunkServers []ChunkServer // v
  // each chunk has a version
  Version int64 // nv
  // the primary chunk in the replications
  Primary int64 // v
  // expiration time of a primary chunk
  LeaseExpiration // v
}

type ChunkMap map[ChunkHandlerId]ChunkHandler

由于 GFS 工作时数据存储在内存, 所以需要将内存中的数据进行持久化. 为了保证内存中的数据不丢失, 会使用 Log 和 CheckPoint 来进行保证.

如果有对数据的写入操作, Log 就会向文件中写入一条日志; 并且会定期创建 CheckPoint. 使用文件而不是数据库作为日志, 因为文件的顺序写入能力更好, 每次只需要将一条或几条日志追加到文件的结尾即可. CheckPoint 则是对某一时刻 GFS 系统的快照进行持久化, 避免因为 Log 日志过多而导致恢复很慢.

这里可以参考 redis 中的 AOF 持久化和 RDB 持久化.

上面两张表中, 有一部分数据是需要持久化的, 用 nv (non-volatile)标识; 另一部分不需要持久化的数据, 用 v 标识.

每次 GFS 重启, 都会重新与所有的 Chunk Server 建立联系, 并且选出对应的 Primary 节点; 每个 Primary 节点会从 Master 中获取租约, 如果租约到期但是没有续约, 可能会重新更换 Master 节点.

GFS 文件的读取:

  1. FileName, FileOffset --> Master.
  2. Master --> ChunkHandler, List of ChunkServers(可能选择网络速度最快/版本最新的副本进行读取).
    缓存读取结果(局部性原理, 可能多次从 64MB 的块中读取数据).
  3. Client --(ChunkHandlerId, FileOffset)--> ChunkServer
    ChunkServer --(Data)--> Client.

如果要读取的数据正好横跨两个 Chunk, 那么 Master 会分两次获取两个 Chunk 中的数据, 然后以一个数据的形式返回给 Client.

GFS 文件的写入:
仅针对在文件尾部追加内容的方式(记录追加).

  1. Master 需要能够找到对应文件的最后一个 Chunk 所在的 ChunkServer List 以及 List 中的 Primary 节点. 如果当前没有 Primary 节点, 需要先找到持有最新副本的 ChunkServer 集合, 在其中选择一个作为 Primary 节点.
  2. 然后 Master 将版本号递增, 写入磁盘.
  3. Master 通知 Primary 和 Secondary(其余的具有最新版本号的) 节点, 更新版本号, 并将版本号写入磁盘.
  4. Master 告知 Client Primary 和 Secondary 节点的信息.
  5. Client 将要追加的信息发送给 Primary 和所有的 Secondary 节点, 节点将其缓存到缓冲区, 发送 ack. (这里应该是发送给某一个节点, 然后 Master 内部按照某种最小跨域交换机的处理, 分发给其余节点)
  6. 当 Client 收到所有节点的 ack, 就通知 Primary 节点.
  7. Primary 节点选择应该追加到哪一个 offset, 然后通知所有的 Secondary 将缓存中的内容写入到 offset 后.
  8. 节点将内容写入, 如果写入成功, 返回给 Primary "OK", 如果不成功, 返回 "NO".
  9. Primary 需要收到所有节点返回的 "OK", 再返回 "SUCCESS" 给 Client, 否则返回 "FAIL". "FAIL" 代表有 0 个或者多个副本成功写入了数据.
  10. 如果返回 "FAIL", Client 需要重新开始和 Master 的通信以确定最新的 ChunkServer 及其内容. Client 会一直请求直至返回 "SUCCESS".

4 容错

这一节我们思考如何通过副本来增加容错性, 示例为 VMWare Fault-Tolerance 系统.

GFS 的 Replication 只是在高层面(应用层面)对有限的数据进行复制, 很多系统的复制系统都是这么做的. 而 VMWare FT 不同, 他需要做到在低层面的完整数据复制, 来保证上层应用在任何地方都能够运行.

我们先了解一下 VMWare 的系统结构:
VMWare 直接安装在物理机上, 通过 Virtual Machine Monitor(VMM) 完成硬件与其中虚拟系统的交互. 在虚拟机中, 可以启动多个不同的操作系统, 并且独立的运行不同的应用程序.
下图中, 两台机器都安装了 VMWare, 并且分为 Primary 和 Backup, Backup 为 Primary 的镜像. Client 1 通过 LAN 发送网络包给 Primary 节点, VMM 收到网络中的包, 向对应的子系统发送模拟网络包, 同时, 向 Backup 从节点发送和它收到的网络包相同的包, 使从节点的数据操作和主节点保持一致. 完成后, 主节点向 Client 1 返回数据包, 而 Backup 节点的 VM 知道自己只是从节点, 不需要返回包, 因此直接在 VMM 中删除该数据包.

VMWareArchitecture
VMWareArchitecture

主从节点之间的信息流称为 Log Channel, 其中传递的数据是 Log Entry. 主节点收到中断将产生一条 Log Entry, 写入到 Log Channel 中.
从节点会定时从 Log Channel 获取日志(如 100 次/s). 如果在一定时间内没有收到任何日志, 说明 Primary 节点可能已经宕机, 那么该节点就不再继续执行来自主节点的指令, 也不会再等待 Log Channel 中的事件, 而是变成一个可以独立接收和执行指令的自由节点, 可以接收网络中的请求并输出返回的信息.
如果主节点发现 Log Channel 中的信息一直没有被消费, 那么会停止向从节点发送备份日志, 并认为该节点已经宕机.