MIT-6-824-2025-分布式系统工程笔记-全-

MIT 6.824 2025 分布式系统工程笔记(全)

介绍。

6.824 2015 年第 1 讲:介绍。

注意:这些讲座笔记是从 6.824 课程网站 2015 年春季发布的笔记稍作修改的。

分布式系统。

什么是分布式系统?

  • 多个网络协作计算机。

  • 示例: 互联网电子邮件,Athena 文件服务器,Google MapReduce,Dropbox 等。

为什么要分布?

  • 连接物理上分离的实体。

  • 通过物理隔离实现安全性。

  • 通过在不同地点复制来容忍故障。

  • 通过并行 CPU/内存/磁盘/网络来提高性能。

...但是:

  • 复杂,难以调试。

  • 新的问题类别,例如部分故障(他接受了我的电子邮件吗?)。

  • Leslie Lamport:“一个分布式系统是指你甚至都不知道存在的计算机的故障可能会使你自己的计算机无法使用。”

  • 建议:如果中心化系统可以解决问题,则不要进行分发。

为什么要选择这门课程?

  • 有趣的——难题,非显而易见的解决方案。

  • 活跃的研究领域——取得了很多进展,但也存在一些尚未解决的大问题。

  • 与 10 年前不同,实际系统使用了更多技术——主要是由于大型网站的兴起。

  • 实践操作——你将在实验室中构建一个真实的系统。

课程结构。

查看课程网站

课程组成部分。

  • 关于重要思想、论文和实验的讲座。

  • 阅读:研究论文作为案例研究。

    • 请在课前阅读论文。

    • 今天的论文:MapReduce 论文

    • 每篇论文都有一个问题需要你回答,还有一个问题需要你提问(参见网站)。

    • 在课堂前提交问题和答案,一两段即可。

  • 课堂上的期中测验和期末考试。

  • 实验室:构建越来越复杂的容错服务。

    • 第一个实验室作业于星期一截止。
  • 项目:设计并构建你选择的分布式系统或者我们在课程最后一个月提出的系统。

    • 两三人小组。

    • 与课程教职员工的项目会议。

    • 在最后一次课程会议上进行演示。

主要主题。

示例:

  • 共享文件系统,使用户可以像 Dropbox 一样合作。

    • 但是这节课不是特指 Dropbox。

    • 仅仅是为了感受分布式系统问题的一个目标示例。

  • 许多客户计算机。

架构。

  • 接口的选择。

    • 单 olithic 文件服务器?

    • 块服务器 -> 客户端中的文件系统逻辑?

    • 单独的命名+文件服务器?

    • 单独的文件系统+块服务器?

  • 单个机房还是统一的广域系统?

    • 广域网络明显更加困难。
  • 客户端/服务器还是点对点?

    • 与性能、安全性、故障行为交互。

实施。

  • 客户端/服务器如何通信?

    • 直接的网络通信相当痛苦。

    • 希望将网络内容隐藏在应用逻辑中。

  • 大多数系统使用一些结构化框架来组织分发。

    • RPC、RMI、DSM、MapReduce 等。

性能。

  • 分发可能会带来问题:网络带宽和延迟瓶颈。

    • 许多技巧,如缓存、多线程服务器。
  • 分发可以帮助:并行性,选择靠近客户端的服务器。

    • 想法:可扩展的设计。

      • 我们希望性能能够随机器的增加呈线性增长。

      • N x 个服务器 -> N x 总性能

  • 需要一种方法将负载按照 N 来划分

    • 按照 N 来划分状态

      • 按用户划分

      • 按文件名划分

      • "分片"或"分区"

  • 很少是完美的 -> 只能扩展到一定程度

    • 全局操作,例如搜索

    • 负载不平衡

      • 一个非常活跃的用户

      • 一个非常受欢迎的文件

        • -> 一个服务器 100%,添加的服务器大部分时间处于空闲状态

        • -> N x 个服务器 -> 1 x 性能

容错性

  • Dropbox: ~10,000 个服务器;一些失败

  • 如果有故障,我能使用我的文件吗?

    • 网络的某些部分,一些服务器集合
  • 或许:在多个服务器上复制数据

    • 或许客户端将每个操作都发送到两者

    • 或许只需要等待一个回复

  • 机会: 如果分区了,可以从两个 "副本" 独立运行吗?

  • 机会: 两台服务器能否产生 2 倍的可用性 2 倍的性能?

一致性

  • 与应用程序/用户约定操作的含义

    • 例如:"读取返回最近写入的值"

    • 由于部分故障、复制/缓存、并发性而变得困难

  • 问题: 保持副本一致

    • 如果一个服务器宕机,它将错过操作

      • 必须在重启后更新到最新状态
    • 如果网络中断,两个副本可能仍然存活,并且看到不同的操作

      • 删除文件,仍然可以通过其他副本看到

      • "分裂大脑" -- 通常不好

  • 问题: 客户端可能以不同的顺序看到更新

    • 由于缓存或复制

    • 我把 grades.txt 变得无法阅读,然后助教给它写入成绩

    • 如果操作在不同副本上以不同顺序运行会怎么样?

  • 一致性通常会损害性能(通信、阻塞)

    • 许多系统采取了捷径 -- "放松一致性"

    • 将负担转移到应用程序上

实验室

焦点:容错和一致性 -- 分布式系统的核心。

  • 实验室 1:MapReduce

  • 实验室 2/3/4:存储服务器

    • 渐进地变得更加复杂(容忍更多类型的故障)

      • 越来越难!
    • 模仿真实系统,例如 MongoDB

    • 实验室 4 具有一个用于数千台服务器的真实设计的核心

你将从实验室学到什么:

  • 听讲座或阅读论文时很容易以为你理解了

  • 构建过程迫使你真正理解

    • "我听见了,我忘记了;我看见了,我记住了;我做了,我理解了"(孔子?)
  • 你将不得不自己设计一些东西

    • 我们提供框架、需求和测试

    • 但是我们会给你留下很大的空间,让你用自己的方法解决问题

  • 你将获得调试分布式系统的经验

测试用例模拟故障场景:

  • 分布式系统很难调试:并发和故障

    • 许多客户端和服务器并行操作

    • 测试用例在最不合适的时候使服务器失败

  • 先想一想再开始编码!

    • 否则你的解决方案会一团糟

    • 或者,这将花费你很多时间

  • 代码审查

    • 向他人学习

    • 评判其他解决方案

我们试图确保困难的问题与分布式系统有关:

  • 不是例如与语言、库等对抗。

  • 因此 Go(类型安全、垃圾回收、精巧的 RPC 库)

  • 因此,相当简单的服务(MapReduce、键值存储)

实验 1:MapReduce

  • 帮助您快速掌握 Go 和分布式编程

  • 第一次接触到一些容错机制

    • 为后续实验提供更好的容错动机
  • 许多论文的激励应用

  • 流行的分布式编程框架

  • 许多后代框架

计算模型

  • 旨在处理文档的

    • 分割文档 -> K1 k, list<V1> values

    • 在每个分片上运行 Map(K1 key, list<V1> values) -> list<K2, V2> kvps

    • 在每个分区上运行 Reduce(K2 key, list<V2> values) -> list

    • 合并结果

  • 编写映射函数和减少函数

    • 框架负责并行性、分布性和容错性
  • 一些计算不是针对的,比如:

    • 任何更新文档的事情

示例:wc

  • 单词计数

  • 在 Go 的实现中,我们有:

    • func Map(value string) *list.List

      • 输入是文件 wc 被调用的一个分片

        • 一个分片只是文件的一部分,由 MapReduce 的分割器决定(可以自定义等等)
      • 返回一个 键值对 列表

        • 关键是单词(比如 'pen')

        • 值为 1(表示 'pen' 出现了一次)

      • 注意:如果 'pen' 出现多次,列表中将会有多个 < 'pen', 1 > 条目

    • func Reduce(key string, values *list.List) string

      • 输入是一个键和该键在 Map() 阶段映射到的所有(?)值的列表

      • 因此,在这里,我们期望一个 Reduce('pen', [1,1,1,1]) 调用,如果 'pen' 在输入文件中出现了 4 次

        • 待办事项:不清楚是否也可以按照以下方式获得三个减少调用:

          • Reduce('pen', [1,1]) -> 2 + Reduce('pen', [1,1]) -> 2

          • Reduce('pen', [2,2])

          • 这篇论文似乎表明 Reduce 的返回值只是一个值列表,因此在这种情况下,这些值与关键字 'pen' 的关联将丢失,这将阻止第三次 Reduce('pen') 调用

示例:grep

  • 映射阶段

    • 主节点将输入分割成 M 个分区

    • 在每个分区上调用 Map

      • map(partition) -> list(k1,v1)

      • 为单词搜索分区

      • 如果单词出现,生成一个具有一个项的列表,如果不是,则为 nil

      • 将结果分区到 R 个减少器中

  • 减少阶段

    • 减少工作,从每个 Map 作业中收集 1/R 输出

    • 所有映射作业已完成!

    • reduce(k1, v1) -> v2

      • 标识函数:v1 进,v1
  • 合并阶段

    • 主节点合并 R 个输出

性能

  • 作业数量:M x R 个映射作业

  • 我们在 N 台机器上能获得多少加速?

    • 理想情况:N

    • 瓶颈:

      • 落后者

      • 网络调用以收集 Reduce 分区

      • 与 FS 交互的网络调用

      • 磁盘 I/O 调用

容错模型

  • 主节点不具备容错性

    • 假设: 在运行 MapReduce 应用程序时,这台单机不会出错

    • 但是有很多工作进程,所以必须处理它们的故障

  • 假设:工作进程是故障停止的

    • 他们失败并停止(例如,在故障后不发送乱码的奇怪数据包)

    • 他们可能会重新启动

我们想要容忍什么样的故障?

  • 网络:

    • 丢失数据包

    • 重复数据包

    • 临时网络故障

      • 服务器断开连接

      • 网络分区

  • 服务器:

    • 服务器崩溃+重启(主服务器还是工作者?)

    • 服务器永久失败(主服务器还是工作者?)

    • 所有服务器同时失败 -- 电源/地震

    • 糟糕的情况:在复杂操作中间崩溃

      • 如果在 map 或 reduce 中间失败会发生什么?
    • 错误 -- 但不在这门课上

      • 当 map 或 reduce 中出现 bug 时会发生什么?

      • 在 Map 中反复出现相同的 bug?

      • 管理软件终止应用

  • 恶意 -- 但不在这门课上

处理故障的工具?

  • 重试 -- 例如如果数据包丢失,或服务器崩溃+重启

    • 数据包(TCP)和 MapReduce 作业

    • 可能会执行 MapReduce 作业两次:必须考虑这一点

  • 复制 -- 例如如果一个服务器或网络的一部分失败了

    • 下一个实验室
  • 替换 -- 为了长期健康

    • 例如,工作者

重试作业

  • 网络故障:糟糕的情况下执行两次作业

    • 对于 MapReduce 来说没问题,因为 map()/reduce() 产生相同的输出

      • map()/reduce() 是“功能性”的或“确定性”的

      • 中间文件怎么样?

        • 原子重命名
  • 工作者故障:可能已执行作业,也可能未执行

    • 所以,我们可能会执行作业多次!

    • 但是对于 MapReduce 来说没问题,只要 map()reduce() 函数是确定性的

    • 什么会使 map() 或 reduce() 不确定?

    • 通常执行请求两次可以吗?

      • 不。实际上,通常不会。

      • 如果你执行一笔信用卡交易多次,客户会不高兴

  • 添加服务器

    • 在 MapReduce 中很容易 -- 只需告诉主节点

    • 通常很难

      • 服务器可能已经丢失状态(需要获取新状态)

      • 服务器可能已经快速重新启动

        • 可能需要认识到这一点以使服务器更新

        • 服务器在重新启动后可能会有新角色(例如,不是主服务器)

      • 这些更难的问题你将不得不处理以使 MapReduce 主节点具有容错能力

      • 以后实验室的主题

实验 1 代码

实验 1 的应用(见 main/wc.go):

  • map() 和 reduce() 的存根

  • 你填写它们来实现单词计数(wc)

  • 你会怎样写 grep?

实验 1 的顺序实现(见 mapreduce/mapreduce.go):

  • 演示:run wc.go

  • 代码审查从 RunSingle() 开始

实验 1 的工作者(见 mapreduce/worker.go):

  • 远程过程调用(RPCs)参数和回复(见 mapreduce/common.go)。

  • RPC 的服务器端

    • RPC 处理程序具有特定的签名

      • DoJob

      • Shutdown

  • RunWorker

    • rpcs.Register:注册命名处理程序 -- 这样 Call() 可以找到它们

    • Listen:创建用于监听 RPC 请求的套接字

      • 对于分布式实现,将 "unix" 替换为 "tcp"

      • <dns,port> 元组名称替换 "me"

    • ServeConn:在单独的线程中运行(为什么?)

      • 并发地提供 RPC

      • RPC 可能会阻塞

  • RPC 的客户端

    • Register()
  • call()(见 common.go

    • 发起一个 RPC

    • 实验代码为每个请求拨号

      • 典型的代码使用网络连接进行多个请求

        • 但是,实际上必须准备好重新拨号

        • 网络连接失败,并不意味着服务器失败!

        • 我们还这样做是为了轻松地引入失败场景

        • 间歇性的网络故障

        • 只是丢失了回复,但没有请求

实验 1 的主节点(见 mapreduce/master.go

  • 你写它

  • 你将不得不处理分发作业

  • 你将不得不处理工人的失败。

RPC 和线程

6.824 2015 年第 2 讲:基础设施:RPC 和线程

注意:这些讲座笔记是从 2015 年春季 6.824 课程网站上稍作修改的。

远程过程调用(RPC)

  • 分布式系统的一个关键部件;所有实验都使用 RPC

  • 目标:易于编程的网络通信

    • 隐藏了客户端/服务器通信的大部分细节

    • 客户端调用很像普通的过程调用

    • 服务器处理程序很像普通的过程

  • RPC 被广泛使用!

RPC 理想情况下使网络通信看起来就像普通的函数调用

 Client
  ------
    z = fn(x, y)

  Server
  ------
    fn(x, y) {
      compute
      return z
    } 

RPC 旨在实现这种透明度

RPC 消息图

 Client                      Server
  ------                      ------

          "fn", x, y
  request ---------->

                          compute fn(x, y)

            z = fn(x, y)
           <------------- response 

软件结构

 Client             Server
   ------             ------

  client app         handlers
    stubs           dispatcher
   RPC lib           RPC lib
     net <-----------> net 

存根有点像假的客户端端函数,看起来像真正的f(x, y),但它们只负责打包参数、将其发送到网络上,并要求服务器计算f(x, y)。然后存根可以通过网络接收结果并将值返回给客户端代码。

来自实验 1 的例子:

  • DoJob

  • Register

RPC 的一些细节

  • 编组:将数据格式化为数据包

    • 对于数组、指针、对象等可能会有些棘手

    • Go 的 RPC 库非常强大!

    • 有些东西你无法传递/编组:例如通道、函数

  • 绑定:客户端如何知道要与谁通信?

    • 可能是一个名称服务--例如 DNS
  • 线程:

    • 客户端通常有许多线程,因此> 1个调用未完成,将回复与调用匹配

    • 处理程序可能很慢,因此服务器通常在每个线程中运行

RPC 问题:如何处理失败?

  • 例如丢失的数据包、网络中断、服务器崩溃、服务器慢

客户端的 RPC 库看到的失败是什么样的?

  • 它从未看到服务器的响应

    • 可能数据包丢失了
  • 知道服务器是否看到了请求!

    • 可能是服务器/网络在发送回复之前出现故障

最简单的方案:"至少一次"行为

 while true
        send req
        wait up to 10 seconds for reply
        if reply arrives
            return reply
        else
            continue 
  • RPC 客户端库会等待一段时间才会收到响应

  • 如果没有收到,重新发送请求

  • 多次执行这个操作

  • 仍然没有响应--向应用程序返回错误

问:"至少一次"对应用程序来说容易应对吗?

至少一次的简单问题:

  • 发生在 无副作用 的请求中

  • 客户端发送"从银行账户扣除$10"两次,因为没有收到第一次的回复

更微妙的问题:客户端程序可能出现什么问题?

  • Put("k", "v")会用v覆盖k处的值

  • Put("key", "value1")--用于在 DB 服务器中设置键值的 RPC

  • Put("key", "value2")--客户端然后对同一个键进行第二次 Put

例子:

Client                              Server
------                              ------

put k, 10
            ----\
                 \
put k, 20   --------------------->  k <- 20
                   \
                    ------------->  k <- 10

get k       --------------------->

                10
            <--------------------- 

注意:客户端发送请求,服务器进行一些工作并回复,但回复丢失的情况经常发生,并且在实验中会经常遇到。

至少一次是否可以?

  • 是的:如果重复操作是可以接受的,例如只读操作

  • 是的:如果应用程序有自己的重复检测计划

    • 这些内容将会在 Lab 1 中用到

更好的 RPC 行为:"至多一次"

  • 思路:服务器 RPC 代码检测到重复请求

    • 返回先前的回复而不是重新运行处理程序
  • 客户端在每个请求中包含唯一 ID(XID)

    • 重新发送时使用相同的 XID
  • 服务器检查 XID 是否之前已经看到

例如:

 if seen[xid]:
      r = old[xid]
    else
      r = handler()
      old[xid] = r
      seen[xid] = true 

一些至多一次的复杂性

  • 如何确保 XID 是唯一的?

    • 大随机数?

    • 将唯一客户端 ID(ip 地址?)与序列号结合?

  • 服务器最终必须丢弃有关旧 RPC 的信息

    • 何时丢弃是安全的?

    • 想法:

      • 唯一客户端 ID

      • 每个客户端 RPC 序列号

      • 客户端在每个 RPC 中包含"已看到所有回复<= X",类似于 TCP 序列号和 ACK

      • 或者只允许客户端一次只有一个未完成的 RPC,以便到达seq+1时服务器可以丢弃所有<= seq

      • 或者客户端同意在< 5分钟内不断重试,服务器在 5 分钟后丢弃

  • 在原始请求仍在执行时如何处理重复请求?

    • 服务器还不知道回复;不想运行两次

    • 想法:每个执行的 RPC 都有一个"挂起"标志;等待或忽略

如果一个至多一次服务器崩溃了怎么办?

  • 如果至多一次在内存中有重复信息,服务器将忘记

    • 并接受重复请求
  • 或许应该将重复信息写入磁盘?

  • 复制服务器也应该复制重复信息吗?

关于"仅一次"怎么办?

  • 至多一次语义加上无限重试加上容错服务

Go RPC 是"至多一次"

  • 打开 TCP 连接

  • 将请求写入 TCP 连接

  • TCP 可能会重传,但服务器的 TCP 会过滤重复数据

  • Go 代码中不重试(即不会创建第二个 TCP 连接)

  • Go RPC 代码如果没有收到回复就会返回错误

    • 或许在超时后(来自 TCP)

    • 或许服务器没有看到请求

    • 或许服务器处理了请求但在回复返回之前服务器/网络失败了

Go 的至多一次 RPC 对 Lab 1 不够

  • 它仅适用于单个 RPC 调用

  • 如果工作线程没有响应,主服务器会将其重新发送给另一个工作线程

    • 但原始工作可能没有失败,而且也在处理
  • Go RPC 无法检测到这种重复

    • 在实验 1 中没有问题,它在应用程序级别处理

    • 在实验 2 中,你将不得不防止这些重复

线程

  • 线程是一种基本的服务器结构工具

  • 你将在实验中经常使用它们

  • 它们可能会很棘手

  • 与 RPC 一起很有用

  • 在 Go 中称为 goroutines

线程=“控制线程”

  • 线程允许一个程序(逻辑上)同时执行多个任务

  • 线程共享内存

  • 每个线程包含一些线程状态:

    • 程序计数器、寄存器、堆栈

线程挑战:

  • 在线程之间共享数据

    • 如果两个线程同时修改同一变量会怎样?

    • 如果一个线程读取另一个线���正在更改的数据会怎样?

    • 这些问题通常称为竞争

    • 需要保护共享数据的不变性(Go:互斥锁

  • 线程之间的协调(Go:通道

    • 例如等待所有 Map 线程完成
  • 死锁

    • 线程 1 正在等待线程 2

    • 线程 2 正在等待线程 1

    • 容易检测(不像竞争)

  • 锁粒度

    • 粗粒度->少量并发/并行

    • 细粒度->大量并发,但竞争和死锁

  • 让我们来看一个玩具 RPC 包,以说明这些问题。

查看今天的讲义 -- l-rpc.go

在这里获取它。

  • 这是一个玩具 RPC 系统。

  • 说明了线程、互斥锁、通道。

  • 这是一个玩具。

    • 假设连接已经打开。

    • 仅支持整数参数,整数回复。

    • 不处理错误。

struct ToyClient

  • 客户端 RPC 状态。

  • 每个 ToyClient 都有一个互斥锁。

  • 与服务器的连接(例如 TCP 套接字)。

  • xid -- 每次调用的唯一标识,以匹配回复和调用方。

  • pending[] -- 多个线程可能调用,需要找到它们。

    • 调用方正在等待的通道。

Call()

  • 应用程序调用 reply := client.Call(procNum, arg)

  • procNum 指示在服务器上运行哪个函数。

  • WriteRequest 知道 RPC 消息的格式。

    • 基本上只是参数转换为数据包中的位。
  • Q: Call() 中为什么需要互斥锁?mu.Lock() 是做什么的?

  • Q: 我们可以将 xid := tc.xid 移到关键部分之外吗?

    • 毕竟,我们没有改变任何东西。

    • [见下面的图表]

  • Q: 我们需要在关键部分内部调用 WriteRequest 吗?

    • 注意:Go 说你负责防止并发的映射操作。

    • 这就是为什么更新到待处理状态被锁定的一个原因。

图表:

Listener()

  • 作为后台线程运行。

  • <- 做什么?

  • 它可能需要在通道上等待调用方不太正确。

回到 Call()...

Q: 如果回复很快回来怎么办?

  • Listener() 能在 pending[xid] 条目存在之前看到回复吗?

  • 或者在调用方等待通道之前?

Q: 我们应该将 reply := <-done 放在关键部分内吗?

  • 为什么在外面没问题?毕竟,两个线程都在使用它。

Q: 为什么每个 ToyClient 都有一个互斥锁,而不是整个 RPC 包只有一个互斥锁?

服务器的 Dispatcher()

  • 请注意,调度器将 xid 回显回客户端。

    • 这样 Listener 就知道要唤醒哪个调用。
  • Q: 为什么在单独的线程中运行处理程序?

  • Q: 调度器可以无序地回复有问题吗?

main()

  • 请注意在 handlers[] 中注册处理程序。

  • 程序会打印什么?

何时使用共享内存(和锁),何时使用通道?

  • 这是我的观点。

  • 当你想要一个线程明确等待另一个线程时,请使用通道。

    • 经常等待结果,或者等待下一个请求。

    • 例如当客户端 Call() 等待 Listener() 时。

  • 当线程不是故意时,使用共享内存和锁。

    • 直接交互,但只是碰巧读/写相同的数据。

    • 例如当 Call() 使用 tc.xid 时。

Go 的 "内存模型" 需要显式同步才能通信!

这段代码是不正确的:

 var x int
    done := false
    go func() { x = f(...); done = true }
    while done == false { } 

很诱人地编写,但 Go 规范表示应该使用通道或 sync.WaitGroup 代替。

学习关于 goroutineschannels 的 Go 教程。

主/备份复制

6.824 2015 年第 3 讲:主/备份复制

注意:这些讲座笔记略有修改自 2015 年春季 6.824 课程网站上发布的内容。

今天

  • 复制

  • Remus 案例研究

  • 实验 2 介绍

容错

  • 我们希望一个服务能够在发生故障时继续运行!

  • 定义:

    • 可用 -- 尽管[某些类别的]故障仍可使用

    • 正确 -- 对客户端的行为就像单个服务器

      • 出现的许多问题与正确性有关
  • 非常困难!

  • 非常有用!

需要一个故障模型:我们将尝试应对什么?

  • 最常见:独立的故障停止计算机故障

    • 故障停止故障:一段时间内计算正确,然后停止

      • 与计算不正确(不同情况)相反
    • 必须假设故障独立

      • (否则我们可能会有主节点故障=>备份故障=> fffffuu....)
    • Remus 进一步假设一次只有一个故障

  • 另一个模型:站点范围内的停电(以及最终重启)

  • (网络分区)

  • 没有错误,没有恶意

核心思想:复制

  • 两个服务器(或更多)

  • 每个副本保留所需的服务状态

  • 如果一个副本失败,其他副本可以继续

例如:容错 MapReduce 主节点

  • 实验室 1 的工作节点已经具有容错能力,但主节点没有

  • [图表:M1,M2,工作节点]

  • 状态:

    • 工作节点列表

    • 哪些工作已完成

    • 哪些工作节点空闲

    • TCP 连接状态

    • 程序计数器

重要问题

  • 要复制什么状态

    • 例子:Remus 复制所有 RAM 和 CPU 状态
  • 副本如何获取状态?

  • 何时切换到备份?

    • 主节点真的宕机了还是只是网络宕机了?
  • 切换时是否可见异常?

    • 客户端会看到什么?
  • 如何修复/重新集成?

    • 如何获得新的备份?

两种主要方法:

  1. 状态转移

    • “主”副本执行服务

    • 主节点向备份发送[新]状态

    • 例子:Remus

  2. 复制状态机

    • 所有副本(主节点和备份)执行所有操作

    • 如果相同的起始状态和相同的操作和相同的顺序和确定性和然后 => 相同的结束状态

    • 操作被转移而不是状态

状态转移更简单

  • 但状态可能很大,传输速度慢

  • Remus使用状态转移

复���状态机可能更有效

  • 如果操作相对于数据很小

  • 但复杂,例如多核上的顺序,确定性

    • 很难确保每个人都达到相同的状态

    • 确定性可能会有问题(时间,线程等)

  • 实验使用复制状态机

Remus:通过异步虚拟机复制实现高可用性,NSDI 2008

非常雄心勃勃的系统

  • 整个系统复制

  • 对应用程序和客户端完全透明

  • 任何现有软件的高可用性

  • 如果运行良好将是奇迹!

  • 故障模型:

    1. 独立的硬件故障

    2. 站点范围内的停电

计划 1(缓慢,破碎):

  • [图表:应用程序,操作系统,Remus 底层]

  • 两台机器,主节点备份;加上网络和其他机器

  • 主要运行操作系统和应用软件,与客户端交谈等。

  • 备份最初不执行操作系统、应用程序等。

    • 它只执行一些 Remus 代码
  • 每秒几次:

    • 暂停主要

    • 将整个 RAM、寄存器、磁盘复制到备份

      • 10Gbps = 1GB/s 网络带宽

      • 100MB/s 磁盘带宽

      • 网络带宽限制 RAM 传输速率

      • 磁盘带宽限制磁盘传输速率

    • 恢复主要

  • 如果主要失败:

    • 开始备份执行!

问: 计划 1 正确吗(如上所述)?

  • 即它看起来就像一个单一的可靠服务器吗?

  • 不:

    • 客户端将写请求发送到主要,主要在备份有机会复制新状态之前回复

    • 主要失败,备份接管,但它不反映上一个写请求。

    • 客户端将会受挫,因为他的写入被丢失了

问: 如果主要失败了,副本接管了会看到外界什么?

  • 备份是否具有与主要上次可见的相同状态?

  • 客户端请求可能丢失吗?执行两次?

  • 是的:参见上述问题

问: 如何确定主要是否失败了?

问: 客户端如何知道要与备份而不是主要交谈?

问: 如果整个站点停电怎么办?

  • 主要正在运行一些操作系统,有一个从磁盘“崩溃一致”重新启动的计划

问: 如果主要在向备份发送状态时失败了怎么办?

  • 即备份正在吸收新状态的中途?

问: 如果主要收到请求,向备份发送检查点,然后就在回复之前主要失败了怎么办?

  • TCP 层会处理这个吗?如果客户端重发请求,那可能有问题(副作用)。因此希望 TCP 介入并注意到没有回复。如何做到?主要刚要回复,但 Remus 将回复保持在缓冲区中。备份将具有相同的状态,因此它会认为自己已经回复并等待客户端的确认,但客户端什么也没收到。因此,备份将重新传输主要从未有机会发送的数据包,并最终获得客户端的确认。

问: 计划 1 有效吗?

  • 我们能消除备份状态落后于主要的事实吗?

    • 看起来非常困难!

    • 主要将不得不告诉备份(并等待)每个指令。

  • 我们能隐藏备份状态落后于主要的事实吗?

    • 防止外界看到备份落后于最后一个主要状态

      • 例如,防止主要的发送 RPC 回复,但备份状态没有反映该 RPC

      • 例如 MapReduce Register() RPC,备份忘记这个将是不好的

    • 想法: 主要“持有”输出直到备份状态赶上输出点

      • 例如主要收到 RPC 请求,处理它,创建回复数据包,但 Remus 将回复数据包保持,直到备份接收到相应的状态更新

Remus 时代,检查点

  1. 主要在 Epoch 1(E1)中运行一段时间,保存 E1 的输出

  2. 主要暂停

  3. 主要将 RAM+磁盘更改从 E1 复制到本地缓冲区

  4. 主要在 E2 中恢复执行,保存 E2 的输出

  5. 主要向备份发送 RAM+磁盘的检查点

  6. 备份将所有内容复制到单独的 RAM,然后应用,然后确认

  7. 主要释放 E1 的输出

  8. 备份服务器将 E1 的更改应用于 RAM 和磁盘

如果主服务器失败,备份服务器完成应用上一个时期的磁盘+内存,然后开始执行

问: 有任何外部可见的异常吗?

问: 如果主服务器接收并执行一个请求,然后在检查点之前崩溃了?备份服务器将不会看到请求!

  • 只要主服务器没有回复该请求,这没问题:客户端将重新发送请求

问: 如果主服务器发送了一个数据包,然后崩溃了,备份服务器是否保证具有该数据包暗示的状态更改?

  • 是的。这就是将发送的网络数据包缓冲直到备份服务器更新的全部意义。

问: 如果主服务器在释放输出的过程中部分崩溃了?备份服务器必须重新发送吗?它如何知道重新发送什么?

问: Remus 如何决定应切换到备份?

  • 天真的机制:如果主服务器停止与备份服务器通信,则出现了问题。

问: 是否存在 Remus 会错误激活备份的情况?即主服务器实际上是活着的

  • 网络分区...

问: 主服务器恢复后,Remus 如何恢复复制?这是必要的,因为最终活动的前备份服务器本身也将失败

问: 如果 两个 都失败了,例如整个站点的停电?

  • RAM 内容将丢失,但磁盘可能会幸存

  • 恢复电源后,从其中一个磁盘重新启动客户机

    • 操作系统和应用程序恢复代码将执行
  • 磁盘必须是“崩溃一致”的

    • 如果正在安装检查点,则可能不是备份服务器的磁盘
  • 磁盘不应反映任何保持的输出(...为什么不呢?)

    • 如果正在执行,则可能不是主服务器的磁盘
  • 我不理解论文的这部分(第 2.5 节)

    • 似乎有一个窗口,在此期间如果断电,则两个磁盘都不能使用

      • 主服务器在时期期间写入其磁盘

      • 与此同时,备份服务器将上一个时期的写入应用到其磁盘上

问: 在哪些情况下 Remus 可能会有良好的性能?

问: 在哪些情况下 Remus 可能会有低性能?

问: 时期应该是短的还是长的?

Remus 评估

  • 摘要: 原生速度的 1/2 到 1/4

  • 检查点很大且需要时间发送

  • 输出保持限制了客户端交互的速度

为什么这么慢?

  • 检查点很大且需要时间生成和发送

    • 100ms 用于 SPECweb2005 -- 因为有很多页面需要编写
  • 因此,检查点间隔必须很长

  • 因此,输出必须保持相当长的时间

  • 因此客户端交互速度慢

    • 每个客户端每秒只有 10 个远程过程调用

如何提高复制性能?

  • 使用特定于应用程序的方案可能会节省大量资源:

    • 只发送应用程序实际需要的状态,而不是全部状态

    • 以优化的格式发送状态,而不是整个页面

    • 如果操作比状态小,则发送操作

  • 可能 对应用程序透明

    • 也可能不是对客户端的

实验室 2 中的主-备份复制

概述

  • 简单的键/值数据库

  • 主服务器和备份服务器

  • 复制状态机: 主服务器将每个操作发送到备份服务器进行复制

  • 容忍网络问题,包括分区

    • 要么继续运行,正确执行

    • 或者暂停操作直到网络修复

  • 允许替换失败的服务器

  • 你实际上要实现所有这些(不像实验 1)

"视图服务器" 决定谁是主服务器 p 和备份 b

  • 主要目标: 避免"分裂大脑"——关于谁是主服务器的分歧

  • 客户端和服务器询问视图服务器

  • 他们不会做出独立的决定

修复

  • 视图服务器可以在旧的备份成为主服务器后将"空闲"服务器作为 b

  • 主服务器初始化新备份的状态

关键点:

  1. 一次只能有一个主服务器!

  2. 主服务器必须拥有最新的状态!

我们将制定一些规则来确保这些

视图服务器

  • 维护一系列"视图"

例子:

 view #, primary, backup
    0:      --       --
    1:      S1       --
    2:      S1       S2
    4:      S2       --
    3:      S2       S3 
  • 监控服务器的存活状态

    • 每个服务器定期发送 ping RPC(更像是心跳)

    • "死亡" 如果连续错过 N 次 ping

    • "活着" 经过单次 ping 后

  • 可以有两个以上的服务器对视图服务器进行 ping

    • 如果有两个以上的"空闲"服务器
  • 如果主服务器死了:

    • 新视图中之前的备份作为主服务器
  • 如果备份死了,或者没有备份

    • 新视图中之前的空闲服务器作为备份
  • 只有一个主服务器,没有备份也可以

    • 但是——如果有空闲服务器可用,让其成为备份

如何确保新主服务器拥有最新的状态副本?

  • 只提升之前的备份

    • 即不要让空闲服务器成为主服务器
  • 备份必须记住是否已被主服务器初始化

    • 如果没有,即使被提升也不要作为主服务器运行!

Q: 是否可能有多个服务器认为自己是主服务器?

 1: S1, S2
       net broken, so viewserver thinks S1 dead but it's alive
    2: S2, --
    now S1 alive and not aware of view #2, so S1 still thinks it is primary
    AND S2 alive and thinks it is primary
    => split brain, no good 

如何确保只有一个服务器充当主服务器?

...即使有多个可能认为自己是主服务器。

"Acts as" == 执行并响应客户端请求

基本思想:

 1: S1 S2
    2: S2 --
    S1 still thinks it is primary
    S1 must forward ops to S2
    S2 thinks S2 is primary
    so S2 must reject S1's forwarded ops 

规则:

  1. 视图 i 中的主服务器必须在视图 i-1 中是主服务器或备份

  2. 主服务器必须等待备份接受每个请求

    • Q: 如果没有备份或备份不知道自己是备份怎么办?

    • A: 如果主服务器是视图的一部分但没有备份,那么主服务器无法取得进展,因此只能等待

    • A: 如果视图已更新并且备份被移出视图,则主服务器可以在没有备份的情况下以"危险模式"运行

  3. 非备份必须拒绝转发的请求

  4. 非主服务器必须拒绝直接的客户端请求

  5. 每个操作必须在状态转移之前或之后

例子:

 1: S1, S2
       viewserver stops hearing Pings from S1
    2: S2, --
       it may be a while before S2 hears about view #2

    before S2 hears about view #2
      S1 can process ops from clients, S2 will accept forwarded requests
      S2 will reject ops from clients who have heard about view #2
    after S2 hears about view #2
      if S1 receives client request, it will forward, S2 will reject
        so S1 can no longer act as primary
      S1 will send error to client, client will ask viewserver for new view,
         client will re-send to S2
    the true moment of switch-over occurs when S2 hears about view #2 

新备份如何获取状态?

  • 例如所有的键和值

  • 如果 S2 是视图 i 中的备份,但不在视图 i-1 中,

    • S2 应该要求主服务器传输完整的状态

状态转移规则:

  • 每个操作(Put/Get/Append)必须在状态转移之前或之后

    • == 状态转移必须对操作是原子的
  • 或者

    • 操作在之前,状态转移后反映操作

    • 操作在之后,状态转移不反映操作,主服务器在状��后转发操作

Q: 主服务器需要将 Get() 转发给备份吗?

  • 毕竟,Get() 不会改变任何东西,那为什么备份需要知道呢?

  • 而额外的 RPC 会耗费时间

  • 与确保只有一个主服务器有关:

    • 假设出现两个主服务器(P 和 P' 都认为自己是主服务器)

      • 这可能发生吗?网络分区?
    • 假设客户端向错误的主节点 P' 发送 Get 请求

    • 然后 P' 将尝试将请求转发到 P(P'认为它是备份)

    • 然后 P 将告诉 P':“嘿,滚开,我是主要的”

问: 我们如何使仅主要的 Get() 起作用?

问: 在实验 2 协议中是否存在无法取得前进的情况?

  • 视图服务失败

  • 在备份获取状态之前,主要失败

  • 我们将在实验 3 中开始修复这些问题

"扁平数据中心存储" 案例研究

6.824 2015 年第 4 讲:“扁平数据中心存储”案例研究

注意:这些讲义内容与 6.824 课程网站2015 年春季发布的讲义略有修改。

扁平数据中心存储

扁平数据中心存储,Nightingale, Elson, Fan, Hofmann, Howell, Suzue,OSDI 2012

我们为什么要看这篇论文?

  • 当 Lab 2 长大后想要变成这样

    • 尽管细节各不相同
  • 出色的性能 -- 世界纪录集群排序

  • 良好的系统论文 -- 从应用程序到网络的细节

FDS 是什么?

  • 一个集群存储系统

  • 存储巨大的 blob -- 128 位 ID,多兆字节的内容

  • 客户端和服务器通过具有高叉带宽的网络连接

  • 用于大数据处理(如 MapReduce)

    • 由数千台计算机组成的并行数据处理集群

高级设计 -- 一个常见的模式

  • 很多客户端

  • 很多存储服务器("tractservers")

  • 任意两个服务器之间有很大的带宽

  • 数据存储在 blob 中

    • 通过 128 位 ID 进行寻址

    • 进一步分割成 tracts

      • 从 0 开始编号

      • 8MB 大小的

  • 对数据进行分区

  • 主节点("元数据服务器")控制分区

  • 为可靠性设置副本组

  • 区段表定位器(TLT)存储了一堆条目

    • 在一个 k 副本系统中,每个条目都有 k 个 tractservers
  • 如何找到 blob b 的 tract t 的位置?

    • 计算 TLT 条目为 (h(b) + t) mod len(tlt)

      • 您将获得该条目中的服务器列表
    • blob 元数据是 分布式的,而不是存储在 TLT 中

  • 如何从一个 blob 中写入一段(tract)?

    • 如上所述查找

    • 将写入发送到 TLT 条目中的所有服务器

    • 只有当 所有 服务器都回复时才向客户端确认写入

  • 如何从一个 blob 中读取一段?

    • 如上所述查找

    • 将读取发送到 TLT 条目中的 随机 服务器

为什么这种高层设计是有用的?

  • 成千上万的磁盘空间

    • 存储巨大的 blob,或者许多大的 blob
  • 成千上万的服务器/磁盘/并行吞吐量

  • 可以随时间扩展 -- 重新配置

  • 大型存储服务器池,用于在故障后进行即时替换

动机应用:MapReduce 风格的排序

  • 每个 mapper 读取其拆分的 1/M 部分的输入文件(例如,一个 tract)

    • 每个记录拆分发射一个 <key, record>

    • 将映射分区键分配到 R 个中间文件中(总共 M*R 个中间文件)

  • 每个 reducer 读取每个 mapper 生成的 R 个中间文件中的 1 个

    • 读取 M 个中间文件(每个大小为 1/R

    • 对其输入进行排序

    • 生成最终排序输出文件的 1/R 部分(R 个 blob)

  • FDS 排序

    • FDS 排序不会将中间文件存储在 FDS 中

    • 客户端既是 mapper 也是 reducer

    • FDS 排序不具有局部感知能力

      • 在 MapReduce 中,主节点将工作节点调度到靠近数据的机器上

      • 例如,在同一集群的后续版本中,FDS 排序使用更细粒度的工作分配,例如,mapper 不再获得输入文件的 1/N,而是获得一些更小的值,更好地处理 stragglers。

摘要的主要论点是关于性能的。

  • 他们在 2012 年创造了磁盘到磁盘排序的世界纪录,称为 MinuteSort

    • 1,033 个磁盘和 256 台计算机(136 个区块服务器,120 个客户端)

    • 59.4 秒内 1,401 Gbyte

问: 摘要中的每个客户端 2 GByte/秒看起来很吸引人吗?

  • 从 Athena AFS 读取文件有多快?(约 10 MB/秒)

  • 你能以多快的速度读取典型硬盘?

  • 典型网络能够多快地传输数据?

问: 摘要声称从丢失的磁盘(92 GB)恢复需要 6.2 秒,这是怎么做到的?

  • 那是每秒 15 GByte

  • 令人印象深刻吗?

  • 那怎么可能?那是磁盘速度的 30 倍!

  • 谁可能关心这个度量?

从这篇论文中我们应该想要了解什么?

  • 应用程序编程接口?

  • 布局?

  • 查找数据?

  • 添加一个服务器?

  • 复制?

  • 失败处理?

  • 故障模型?

  • 一致性读/写?(即读取是否看到最新写入?)

    • FDS 中没有:“复制的当前协议取决于客户端向所有副本发出所有写入。这个决定意味着 FDS 为客户端提供了弱一致性保证。例如,如果客户端将一个区块写入 3 个副本中的一个,然后崩溃,读取该区块不同副本的其他客户端将观察到不同的状态。”

    • “写入的顺序不能保证按发出顺序提交。具有排序要求的应用程序负责在收到先前确认后再发出操作,而不是同时发出。FDS 保证原子性:写入要么完全提交,要么完全失败。”

  • 配置管理器的故障处理?

  • 良好的性能吗?

  • 对应用程序有用吗?

应用程序编程接口

  • 图 1

  • 128 位 blob ID

  • blob 有一个长度

  • 只有整个区块的读取和写入 -- 8 MB

问: 为什么 128 位 blob ID 是一个好接口?

  • 为什么不使用文件名?

问: 为什么 8 MB 区块有意义?

  • (图 3...)

问: API 面向哪些客户端应用程序?

  • 不是针对哪个方向?

布局:它们如何在服务器上分布数据?

  • 第 2.2 节

  • 将每个 blob 分成 8 MB 区块

  • TLT 由元数据服务器维护

    • n 个条目

    • 对于 blob b 和区块 ti = (hash(b) + t) mod n

    • TLT[i] 包含具有区块副本的区块服务器列表

  • 客户端和服务器都有最新的 TLT 表副本

无复制的四个条目 TLT 示例:

 0: S1
  1: S2
  2: S3
  3: S4
  suppose hash(27) = 2
  then the tracts of blob 27 are laid out:
  S1: 2 6
  S2: 3 7
  S3: 0 4 8
  S4: 1 5 ...
  FDS is "striping" blobs over servers at tract granularity 

问: 为什么要有区块?为什么不只在一个服务器上存储每个 blob?

  • 什么样的应用程序将从分段中受益?

  • 什么样的应用程序不会?

问: 客户端能够以多快的速度读取单个区块?

问: 摘要中的单客户端 2 GB 数字是从哪里得来的?

问: 为什么不采用 UNIX i-node 方法?

  • 为每个区块存储一个数组,按区块号索引,得到区块服务器

  • 因此,您可以做出每个区块的放置决策

    • 例如,将新的区块写入最轻负载的服务器

问: 为什么不是 hash(b + t)

问: 应该有多少个 TLT 条目?

  • n = 区块服务器数量 怎么样?

  • 为什么他们声称这样做效果不好?第 2.2 节

系统需要选择要放入 TLT 条目的服务器对(或三元组等)

  • 用于复制

  • 第 3.3 节

问: 关于:

 0: S1 S2
   1: S2 S1
   2: S3 S4
   3: S4 S3
   ... 
  • 为什么这样做是一个坏主意?

  • 修复需要多长时间?

  • 如果两个服务器故障会有什么风险?

Q: 为什么论文的 方案更好?

示例:

 0: S1 S2
   1: S1 S3
   2: S1 S4
   3: S2 S1
   4: S2 S3
   5: S2 S4
   ... 
  • 具有 条目的 TLT,每对服务器出现一次

  • 修复需要多长时间?

  • 如果两个服务器失败会有什么风险?

Q: 他们为什么实际上使用最低复制级别为 3?

  • 与之前相同的 表,第三个服务器是随机选择的

  • 修复时间会有什么影响?

  • 两个服务器失败会有什��影响?

  • 如果三个磁盘失败会发生什么?

添加磁道服务器

  • 为了增加磁盘空间/并行吞吐量

  • 元数据服务器选择一些随机的 TLT 条目

  • 在这些 TLT 条目中为现有服务器替换新服务器

扩展磁道的大小

  • 新创建的 blob 具有 0 个磁道的长度

  • 应用程序在写入其末尾之前必须扩展 blob。

  • 扩展操作是原子的,可以安全地与其他客户端并发执行,并返回客户端调用的结果中 blob 的新大小。

  • 单独的 API 告诉客户端 blob 的当前大小。

  • 对于一个 blob 的扩展操作被发送到拥有该 blob 元数据磁道的磁道服务器。

  • 磁道服务器对其进行串行化,原子地更新元数据,并将新大小返回给每个调用者。

  • 如果所有写入者都遵循这种模式,扩展操作将提供调用者可以写入而不冲突的一系列磁道。因此,扩展 API 在功能上等同于 Google 文件系统的“原子追加”。

  • 磁道服务器上懒惰地分配空间,因此声明但未使用的磁道不会浪费存储空间。

他们如何在服务器离开和加入时保持 加一的排列?

不清楚。

Q: 添加磁道服务器需要多长时间?

Q: 当磁道正在传输时客户端 write 会发生什么?

  • 接收磁道服务器可能具有来自客户端和旧服务器的副本

  • 它如何知道哪个是最新的?

Q: 如果客户端读取/写入但具有旧的磁道表会发生什么?

  • 磁道服务器告诉他

复制

  • 写入客户端向 TLT 中的每个磁道服务器发送一份副本。

  • 读取客户端向一个磁道服务器询问。

Q: 为什么他们不通过主要发送写入?

  • 给主要带来很多工作?必须查找并了解 TLT

  • 目标不是只为主要备份,而是有效地在许多磁盘上复制和分割数据

Q: 由于缺乏主要内容,他们可能会遇到什么问题?

  • 为什么这些问题不是致命的?

磁道服务器失败后会发生什么?

  • 元数据服务器停止接收心跳 RPC。

  • 为每个 TLT 条目中失败服务器的随机替换选择

  • 新的 TLT 获得新的版本号

  • 替换服务器获取副本

每个服务器持有的磁道示例:

 S1: 0 4 8 ...
  S2: 0 1 ...
  S3: 4 3 ...
  S4: 8 2 ... 

Q: 为什么不只选择一个替代服务器?

  • 它将不得不接收大量写入以获取丢失数据 => 性能不佳。

Q: 复制所有磁道需要多长时间?

Q: 如果磁道服务器的网络中断然后修复,可能会提供旧数据吗?

Q: 如果服务器崩溃并重新启动,磁盘完好无损,内容可以使用吗?

  • 例如,如果只错过了几次写入?

  • 3.2.1 的“部分故障恢复”

  • 但是它难道不已经被替换了吗?

  • 如何知道它错过了哪些写入?

问: 何时更好地使用 3.2.1 的部分故障恢复?

元数据服务器崩溃时会发生什么?

问: 当元数据服务器宕机时,系统可以继续吗?

  • 是的,拥有 TLT 的客户端可以继续

问: 是否有备份元数据服务器?

  • 不在论文中,他们说他们可能使用 Paxos 进行复制

  • TODO: 不清楚为什么复制元数据服务器会导致一致性问题

问: 重启的元数据服务器如何获得 TLT 的副本?

  • 嗯,也许它在磁盘上?

  • 也许它只是简单地从所有的心跳中重建?

问: 他们的方案看起来正确吗?

  • 元数据服务器如何知道它已经收到了所有的 tractservers 的信息?

    • 它不会,它只会在它们发送心跳时添加服务器
  • 它怎么知道所有的 tractservers 都是最新的?

    • TODO: 与什么是最新的?

随机问题

问: 元数据服务器可能会成为一个瓶颈吗?

  • 很难说。使用案例是什么?

  • 如果你有一个客户端记住了 TLT 那么他只联系元数据服务器一次,然后开始做他所有的读写

  • 如果有很多客户端加入系统,或者回来但忘记了 TLT(可能是因为存储不足),那么元数据服务器将会被大量使用

    • 然而,一旦客户端下载了 TLT,这不会影响客户端获得的带宽

问: 为什么他们需要在 2.3 中提到的清洗程序?

  • 为什么他们在 blob 被删除时不删除 tracts?

    • 执行 GC 比调度和执行删除更快吗?
  • 在删除后可以写入 blob 吗?

    • TODO: 不确定,似乎是的,因为该 blob 的元数据位于 tract -1 中,我认为 WriteTract 不会在每次写入之前检查元数据,所以你可能会有竞争

性能

问: 我们如何知道我们看到了“良好”的性能?你能期望的最好的是什么?

  • 你能期望的最好的是利用每个磁盘的带宽,使系统的带宽为 # of disk * disk bandwidth

问: 单客户端 2 GBps 的限制资源?

  • 假设这是 5.2 的结束

  • 30 个 tractservers 意味着最多 30 * 130MB/s = 3.9GBps

  • 所以限制资源是网络带宽

问: 图 4a:为什么开始低?为什么上升?为什么水平稳定?为什么在特定性能水平上水平稳定?

  • 由于单个客户端的带宽受限,所以开始较低

  • 上升是因为随着客户端数量的增加,每个客户端都会向系统添加更多的带宽

  • 水平稳定是因为在某一点上客户端带宽 > 服务器的带宽

  • 为什么在拥有 516 个磁盘的 x 客户端上水平稳定在 32 GBps?

    • 图 3 表明 10,000 转/分钟的硬盘可以以大约 130MB/s 的速度读取 5MB 的块

      • 写入类似
    • 从对数刻度图中不清楚 x 是什么

      • 10 < x < 50(也许 25 < x < 50?)
    • 516 disks * 130MB/s = 67 GBps,所以看起来最好的情况下性能应该在 32 GBps 以上平稳?

      • 实际上并非所有的磁盘都是 130MB/s 可能?(只有 10,000 转/分钟的 SAS 才那么快)

      • 实际上,单个节点上的多个磁盘可能会使这个数字变小,也许?

      • 无论如何,大约像 x=40 个客户端会有 40 * 10Gbps = 40 * 1.25GBps = 50 Gbps,这比服务器实际(声称的)带宽 32 GBps 更高

问: 图 4b 显示随机读写与顺序读写一样快(图 4a)。这是你期望的吗?

  • 是的。不同磁道的随机读写请求会发送到不同的服务器,就像顺序请求一样 => 没有区别

问: 在图 4c 中,为什么带有复制的写入比读取慢?

  • 一次写入发送到所有磁道服务器?直到它们全部回复为止。

    • 具有更多客户端写入的情况下 => 每台服务器完成的工作更多

    • 论文中提到:"正如预期的那样,写入带宽约为读取带宽的三分之一,因为客户端必须发送每个写入的三个副本"

  • 一次读取只发送到一台?

问: 6.2 秒内的 92 GB 是从哪里来的?

  • 表 1,第四列

  • 那是每秒 15 GB,读和写都是

  • 1000 块磁盘,三重复制,128 台服务器?

  • 限制资源是什么?磁盘?CPU?网络?

每个排序桶有多大?

  • 也就是说,每个桶的排序都在内存中吗?

  • 总共 1400 GB

  • 128 台计算服务器

  • 每台服务器的 RAM 在 12 到 96 GB 之间

  • 嗯,平均说 50,所以总 RAM 可能是 6400 GB

  • 因此每个桶的排序都在内存中,不会将写入传递给 FDS

  • 因此总时间只是 1400 GB 的四次传输

    • 客户端限制:128 * 2 GB/s = 256 GB / 秒

    • 磁盘限制:1000 * 50 MB/s = 50 GB / 秒

  • 因此瓶颈很可能是磁盘吞吐量

Paxos

6.824 2015 年第 5 讲:Paxos

注意: 这些讲义笔记是从 2015 年春季 6.824 课程网站 上发布的讲义笔记中稍作修改的。

简介

开始一个新的关于更强容错性的讲座组

  • 今天:

    • 通过 Paxos 实现更干净的复制方法:RSM

    • 实验三

  • 后续讲座:

    • 如何使用 Paxos 构建系统(Harp、EPaxos、Spanner)

Paxos

链接:

回想:RSM

  • 通过以相同顺序执行操作来维护副本

  • 需要所有副本就操作的(集合和)顺序达成一致

实验二批评

  • 带有视图服务器的主/备份

  • 优点:

    • 概念上很简单

    • 每个操作只需两个消息(请求、回复)

    • 主节点可以进行计算,将结果发送到备份节点

    • 只需要两个 k/v 服务器即可容忍一个故障

    • 与网络分区配合工作

  • 缺点:

    • 视图服务器是一个 单点故障

    • 顺序可能混乱,例如新视图、数据备份、确认等

    • 如果备份节点慢 / 暂时不可用,会产生紧张

      1. 主节点可以等待备份节点 -- 较慢

      2. 视图服务器可以声明备份节点死亡 -- 昂贵,损害了容错性

我们希望有一个通用的排序方案,具有:

  • 没有单点故障

  • 优雅处理慢/间歇性副本

  • 处理网络分区

Paxos 将是这一点的关键构建块。

  • 一些节点参与 Paxos 实例

    • Q: 这个 实例 是什么?

    • A: "每个新命令都需要一个单独的 Paxos 协议,文章称之为一个实例。因此,数据库副本可能会同意首个要执行的命令是 '命令一',它们使用 Paxos 来达成一致。然后,这个 Paxos 实例就完成了。一段时间后,另一个客户端发送 '命令二';副本将启动一个完全独立的 Paxos 实例来达成对第二个客户端命令的一致意见。” --RTM

  • 每个节点都知道该实例中每个其他节点的地址

  • Paxos 的每个实例通常只能达成对一个值的共识,系统通常使用多个 Paxos 实例,每个实例通常决定一个操作的假设:异步、非拜占庭

Paxos 提供了什么?它是如何工作的?

  • “黑盒子” Paxos 实例的接口,在每个节点上:

    • 提议一个值(例如,操作)

    • 检查已决定的值,如果有的话

    • [实验三 A:src/paxos/paxos.go:启动、状态]

  • 正确性:

    • 如果达成协议,所有达成协议的节点都看到相同的值。
  • 容错:

    • 可以容忍少数节点无法访问(正确性意味着它们根本不会达成一致)。
  • 活性:

    • 大多数必须是活动的并且能够可靠地通信(少数不活动)。

如何使用 Paxos 构建系统?

  1. 主/备份类似于 Lab 2,但使用 Paxos 复制视图服务器。

    • [ 下周二的讲座将涉及这样的系统 ]
  2. Lab 3:没有视图服务器,所有复制品都使用 Paxos 而不是主/备份。

使用 Paxos 复制视图服务器或 K/V 服务器类似。

将查看如何进行基于 Paxos 的 K/V 服务器的草图。

基本思想

  • [ 图表:客户端、复制品、每个复制品中的日志、K/V 层、Paxos 层]

  • 没有视图服务器。

  • 三个复制品。

  • 客户端可以向任何复制品发送 RPC(不仅仅是主要的)。

  • 服务器将每个客户端操作附加到一组复制的操作日志中。

    • PutGet(以及稍后的更多)。
  • 日志条目(实例)按顺序编号。

  • Paxos 确保每个日志条目的内容达成一致。

  • 每个这些日志条目都有单独的 Paxos 协议。

    • 为日志条目 #i 运行单独的 Paxos 算法实例。

    • 问: 一个日志条目可以与另一个同时达成一致吗?如果它们彼此依赖,比如 Put(k1, a)Append(k1, b)

    • 答: 可以!它们可以同时达成一致。

    • 答: 你可以在达成对日志条目 #i 的同意之前达成对日志条目 #i+1 的同意。

      • 这意味着与日志条目 i+1 中的 GetPut 请求相关联的回复将不得不等待其他日志条目设置(有趣)。
  • 服务器可以丢弃所有其他服务器已经达成一致(并响应?)的日志条目。

    • 但如果一个服务器崩溃,其他服务器将知道在它恢复时保留它们的日志条目。
  • 协议需要指定的提议者或领导者来确保正确性。

    • 这些只有在性能方面有所帮助。

    • 提议者等待随机时间可以克服建议的“活锁”的低概率。

  • 一旦 Paxos 节点就某个值达成一致,就不会改变主意。

示例:

  • 客户端将 Put(a, b) 发送给 S1

  • S1 选择一个日志条目 3。

  • S1 使用 Paxos 让所有服务器都同意条目 3 包含 Put(a,b)

示例:

  • 客户端将 Get(a) 发送给 S2

  • S2 选择日志条目 4。

  • S2 使用 Paxos 让所有服务器都同意条目 4 包含 Get(a)

  • S2 扫描日志直到条目 4,以找到最新的 Put(a, ...)

    • 待办事项: 对于执行 Get,最坏情况是 O(n),因为可以有跟着一堆 PutAppendPut(或者你可以只有一个存储在很远的地方的 Put?)。

      • 复制品是否可以索引他们的日志?我想是的。如果它们都完整地存储它。
  • S2 以该值回复。

    • S2 可以缓存通过最后的日志扫描的 DB 内容。

问:为什么是一个日志?

  • 为什么不要求所有复制品在每个操作上都达成一致?

  • 允许一个复制品落后,然后赶上。

    • 比如如果它慢的话。

    • 其他复制品不必等待。

  • 允许一个复制品崩溃然后赶上。

    • 如果它在磁盘上保留状态。

    • 可以重放错过的操作。

  • 允许协议的流水线处理/重叠

    • 结果表明达成一致需要多个消息回合

问:达成一致怎么样——我们需要所有副本在每个日志槽中具有相同的操作

  • 由 Paxos 提供,我们将在下面看到

达成一致很难(1):

  • 在特定日志槽中可能有多个操作的提议

  • Sx(服务器 x)可能最初听到一个,Sy 可能听到另一个

  • 显然,必须稍后改变主意

  • 因此:多个回合,最初是暂时的

  • 我们如何知道达成一致已经是永久的——不再是暂时的?

达成一致很难(2):

  • 待办事项:如果 S1S2 同意,而 S3S4 没有回应,我们完成了吗?

  • 协议必须能够即使在服务器失败时也能完成

  • 我们无法区分失败的服务器和网络分区

  • 因此,也许 S3/S4 分区了,已经“同意”执行不同的操作!

Paxos 中的两个主要思想

  1. 可能需要许多回合,但它们将收敛于一个值

  2. 达成一致需要多数支持——防止“脑裂”

    • 关键点:任意两个多数交集

    • 因此,任何后来的多数都将与任何先前的多数共享至少一个服务器

    • 因此,任何后来的多数都可以找出先前的多数决定了什么

      • 待办事项:如何做?

实验室 3B K/V 服务器为每个客户端的 PutGet 创建一个单独的 Paxos 实例

  • 接下来的讲座重点放在了特定实例的协议上

Paxos 轮廓

  • 每个节点由三个逻辑实体组成:

    • 提议者

    • 接受者

    • 学习者

  • 每个提议者都希望就其价值达成一致

    • 可以尝试使用“指定的领导者”来避免竞争的提议者

    • 允许有多个提议者,因此领导选举可以是近似的

  • 提议者联系接受者,试图组建多数

    • 如果大多数回应,我们就完成了
  • 在我们的 K/V 服务器示例中,大致如下:

    • 提议者从客户端接收 RPC,提出操作

    • 接受者是 Paxos 内部的,帮助决定共识

    • 学习者找出达成共识的操作是什么,回应客户端

被推翻的草人:我们是否可以在一个回合内完成 Paxos?

  • 接受者“接受”它从提议者听到的第一个值

  • 何时达成共识?

    • 我们是否可以选择得票最多的值?

    • 不,需要多数接受相同的值:floor(n/2)+1

    • 否则,对 2 个不同的值达成一致(丢失/分区网络)

  • 问题:

    • 假设我们有 3 个服务器:S1S2S3

    • 如果每个服务器提出并接受自己的值会怎样?

      • 没有多数,陷入困境

      • 但也许我们可以检测到这种情况并恢复?

    • 更糟糕的是:S3 崩溃 -> 我们可能已经达到了多数,但我们永远不会知道

  • 如果尚未达成共识,需要一种使接受者改变主意的方式

基本 Paxos 交换

 proposer          acceptors

           prepare(n) ->
        <- prepare_ok(n, n_a, v_a)

           accept(n, v') ->
        <- accept_ok(n)

           decided(v') -> 

为什么 n

  • 以区分多个回合,例如,提议者崩溃,模拟提议

  • 想要后续回合取代先前的回合

  • 数字允许我们比较早期/晚期

  • n 值必须唯一且大致按时间排序

  • n = <时间,服务器 ID>

    • 例如,ID 可以是服务器的 IP 地址
  • “回合”与“提议”相同,但与“实例”完全不同

    • 回合/提议号码在特定实例内部

定义: 服务器 S 接受 n/v

  • 它对accept(n, v)做出了accept_ok响应

定义: n/v被选择的

  • 多数服务器接受了n/v

关键属性:

  • 如果一个值被选择,任何后续选择必须是相同的值

    • 即协议不能改变主意

    • 可能是不同的提议者等,但是相同的值!

    • 这使我们能够在崩溃后自由开始新的轮次

  • 棘手的原因是因为"被选择"是系统范围的属性

    • 例如多数接受,然后提议者崩溃

      • 待办事项: 这里会发生什么?
    • 没有节点可以在本地告知达成一致

所以:

  • 提议者不会在prepare中发送值

    • 待办事项: 任何值如何被接受者接受?
  • 接受者发送回他们已经接受的任何值

  • 如果有提议者,提议者提议该值

    • 为了避免改变现有选择
  • 如果没有已经被接受的值,

    • 提议者可以提议任何值(例如客户端请求)
  • 提议者必须从多数接收prepare_ok

    • 为了确保与任何先前多数的交集,

    • 为了确保提议者听到任何先前选择的值

现在协议--请参阅讲义

 proposer(v):
      choose n, unique and higher than any n seen so far
      send prepare(n) to all servers including self
      if prepare_ok(n, n_a, v_a) from majority:
        v' = v_a with highest n_a; choose own v otherwise
        send accept(n, v') to all
        if accept_ok(n) from majority:
          send decided(n, v') to all

    acceptor state:
      must persist across reboots
      n_p (highest prepare seen)
      n_a, v_a (highest accept seen)

    acceptor's prepare(n) handler:
      if n > n_p
        n_p = n
        reply prepare_ok(n, n_a, v_a)
      else
        reply prepare_reject

    acceptor's accept(n, v) handler:
      if n >= n_p
        n_p = n
        n_a = n
        v_a = v
        reply accept_ok(n)
      else
        reply accept_reject 

示例 1(正常操作):

 `S1`, `S2`, `S3` but `S3` is dead or slow

    `S1`: -> starts proposal w/ n=1 v=A
    `S1`: <- p1   <- a1vA    <- dA
    `S2`: <- p1   <- a1vA    <- dA
    `S3`: dead...

    "p1" means Sx receives prepare(n=1)
    "a1vA" means Sx receives accept(n=1, v=A)
    "dA" means Sx receives decided(v=A) 
  • S1 和 S2 将回复p1消息为prepare_ok(1, 0, null)

  • 如果dA丢失,等待的节点之一可以再次运行 Paxos 并尝试一个比之前更高的新n

    • prepare_ok(2, 1, 'A')的回复将返回,

    • 然后节点被迫发送a2vA,希望这次,在节点收到accept_ok消息后,它将发送不会再次丢失的dA消息

  • 当多数接受者在accept处理程序中接受分支并接受值时,值被认为被选择

    • 然而,并不是每个人都知道这一点,这就是为什么发送decide消息的原因

这些图表并不具体说明提议者是谁

  • 这并不重要

  • 提议者在逻辑上与接受者分开

  • 我们只关心接受者看到并回复的内容

请注意,Paxos 只需要大多数服务器

  • 因此我们可以继续即使S3宕机

  • 提议者不能永远等待任何接受者的响应

如果网络分区会发生什么?

  • S3是活动的并且有一个提议的值 B

  • S3的准备不会组成多数

作业问题

Paxos 如何确保以下事件序列不会发生?实际发生了什么,最终选择了哪个值?

 proposer 1 crashes after sending two accept() requests
  proposer 2 has a different value in mind

  A: p1 a1foo
  B: p1       p2 a2bar
  C: p1 a1foo p2 a2bar

  C's prepare_ok to B really included "foo"
    thus a2foo, and so no problem 

要点:

  • 如果系统已经达成一致,大多数将知道值

  • 任何新的准备者的多数将与该多数相交

  • 因此后续提议者将了解已经达成一致的值

  • 并在接受消息中发送它

示例 2(并发提议者)���

 A1 starts proposing n=10 by sending prepare(n=10) 
    A1 sends out just one accept v=10
    A3 starts proposing n=11
      but A1 does not receive its proposal
      A3 only has to wait for a majority of proposal responses

    A1: p10 a10v10 
    A2: p10        p11
    A3: p10        p11  a11v11

    A1 and A3 have accepted different values! 

会发生什么?

  • Q: 如果A2收到来自A1a10v10接受消息,A2会做什么?

    • a10v10表示accept(n=10,v=10),发生在发送prepare->并接收到<-prepare_ok之后

    • A: A2 会拒绝,因为它有来自p11的更高np

  • Q: 如果A1收到来自A3a11v11接受消息,A1会做什么?

    • A: A1 将回复 ACCEPT_OK 并将其值更改为 11,因为 n = 11 > np = 10

如果 A3 在这一点崩溃(并且不重新启动)会怎样?

这样怎么样:

A1: p10  a10v10               p12
A2: p10          p11  a11v11  
A3: p10          p11          p12   a12v10 

系统在这一点上是否已经同意了一个值?

什么是提交点?

  • 即何时已达成一致意见?

  • 即在什么时候更改值会造成灾难?

  • 当多数人拥有相同的 v_a 之后呢?不会——为什么?以上是反例

  • 当多数人有相同的 v_a/n_a 之后呢?是的——为什么足够?概述:

    • 假设多数人有相同的 v_a/n_a

    • 接受者将拒绝带有较低 naccept()

    • 对于任何更高的 n:准备必须已经看到我们的多数 v_a/n_a(重叠)

    • 如果重叠服务器在 accept(v_a, n_a) 之前看到了 prepare(n) 怎么办?

      • 将拒绝 v_a/n_a

      • 因此还没有多数票

      • 提议者可能可以自由选择 v != v_a

提议者为什么需要选择具有最高 n_av_a

 A1: p10  a10vA               p12
    A2: p10          p11  a11vB  
    A3: p10          p11  a11vB  p12   a12v??

    n=11 already agreed on vB
    n=12 sees both vA and vB, but must choose vB 

两种情况:

  1. n=11 之前已经有多数票了

    • n=11 的准备会看到值并重复使用它

    • 因此 n=12 安全地可以重新使用 n=11 的值

  2. n=11 之前没有多数票

    • n=11 可能已经获得了多数票

    • 因此 n=12 必须重新使用 n=11 的值

准备处理程序为什么要检查 n > n_p

  • 它正在进行 max(concurrent n's),对于接受处理程序

  • 对所有 prepare() 响应 prepare_ok() 也是可以的,

    • 但是 n < n_p 的提议者无论如何都会被 accept() 忽略

接受处理程序为什么要检查 n >= n_p

  • 确保达成协议是必要的

  • 有一个唯一的最高活跃 n

  • 每个人都支持最高的 n

  • 没有 n >= n_p 检查,你可能会得到这种糟糕的情况:

情景:

 A1: p1 p2 a1vA
    A2: p1 p2 a1vA a2vB
    A3: p1 p2      a2vB 

接受处理程序为什么要更新 n_p = n

  • 早期的 n 被接受所需的数量

  • 节点可以获得 accept(n,v),即使它从未看到过 prepare(n)

  • 没有 n_p = n,可以得到这种糟糕的情景:

情景:

 A1: p1    a2vB a1vA p3 a3vA
    A2: p1 p2           p3 a3vA
    A3:    p2 a2vB 

如果新的提议者选择 n < 旧的提议者会怎样?

  • 即使时钟未同步

  • 无法取得进展,尽管没有正确性问题

如果接受者在接收到接受之后崩溃会怎样?

A1: p1  a1v1
A2: p1  a1v1 reboot  p2  a2v?
A3: p1               p2  a2v?

A2 must remember v_a/n_a across reboot! on disk
  might be only intersection with new proposer's majority
  and thus only evidence that already agreed on v1 

如果接受者在发送 prepare_ok 后重新启动会怎样?

  • 它是否必须在磁盘上记住 n_p

  • 如果没有记住 n_p,可能会发生这种情况:

例子:

 `S1`: p10            a10v10
  `S2`: p10 p11 reboot a10v10 a11v11
  `S3`:     p11               a11v11 
  • 11 的提议者没有看到值 10,因此 11 提出了自己的值

  • 但就在那之前,已经选择了 10!

  • 因为 S2 没有记住忽略 a10v10

Paxos 能够陷入僵局吗?

  • 是的,如果没有可以通信的多数派

  • 如果有多数票可用怎么办?

    • 可能会出现活锁:对决的提议者,不断地准备更高的 n

      • 尝试选举领导者的一个原因:减少对决的提议者的机会
    • 有单个提议者和可达多数派,应该能够达成共识

Raft

6.824 2015 讲座 6:Raft

注意:这些讲座笔记稍作修改,来自 Spring 2015 的 6.824 课程网站上发布的笔记。

本讲座:Raft

  • 更大的主题是通过复制的状态机实现容错性

  • Raft--比直接的 Paxos 设计更完整

Raft 概述:

 clients -> leader -> followers -> logs -> execution 

Raft 与 Paxos 有何不同?

  • 我们使用 Paxos:

    • 对每个客户端操作分别达成一致意见
  • Raft:

    • 对每个新领导者(和日志尾部)都达成一致意见

    • 大多数客户端操作不需要协议

    • Raft 是为日志附加而优化的 Paxos(多多少少)

  • 为什么要 Raft 风格的领导者?

    • 没有对决提议者(除非领导者失败)

      • 领导者只告诉其他人要做什么
    • 较少的消息,较少的复杂性(除非领导者失败)

    • 有一个日志比另一个日志更完整的明确定义的概念

      • 简化了切换领导者(也许还有崩溃恢复)

      • 在 Paxos 中很难找到这个解决方案,因为日志有“空洞”

关于可理解性呢?

  • 你必须自己决定

  • 直接的 Paxos 比 Raft 更简单

  • 但直接的 Paxos 对于实际复制来说太简单了

    • 每个人都以自己的方式扩展它

    • 并最终得到了更多或更少像 Raft 的东西

  • Paxos+日志+领导者可能不比 Raft 简单

    • 虽然可能取决于您选择了哪个 Paxos 变体

更直接地使用 Paxos(如实验室 3)是否曾经是一种成功?

  • 即 Raft 风格的领导者是否永远是一个坏主意?

  • 地理分布的对等体

  • 单个领导者离某些客户端很远

  • 一些对等体会比其他对等体慢(Paxos 容忍滞后)

让我们从没有领导变更的 Raft 开始

  • 目前,可靠的领导者

  • 跟随者可能慢或无法访问(但它们不会丢失状态)

  • 我们想要什么?

    1. 容忍少数失败的跟随者

    2. 活跃的跟随者和死去的跟随者在相同的日志上聚合,因为复制需要相同的执行顺序

    3. 仅在无法丢失条目(已提交)时执行,因为无法轻松撤消执行或回复客户端

  • 确保相同日志的想法:

    • 领导者发送日志条目索引和有关上一个条目的信息

    • 客户端可以拒绝(例如我没有上一个条目!)

    • 领导者为该跟随者备份,发送较早的条目

      • 领导者强制跟随者的日志与领导者的日志相同
  • 执行的想法:

    • 思路#1 表示领导者知道跟随者在某个时刻是相同的

    • 一旦大多数达到某一点相同,

      • 领导者将其发送为提交点,

      • 每个人都可以通过那个点执行,

      • 领导者可以回复客户端

如果领导者崩溃怎么办?

  • 其他服务器超时(一段时间没有 AppendEntries“心跳”)

  • 如果其他服务器丢失心跳,它们开始怀疑领导者是否已下降

    • 无法真正确定网络上的领导者是下降/上升的
  • 选择新领导者!

  • Raft 将时间划分为术语

  • 大多数术语都有领导者

切换到新领导者时存在哪些危险?

  • 两个领导者

  • 没有领导者

  • 可能会忘记已执行的日志条目

  • 日志可能最终不同(分歧)

首先讨论领导者选举,然后是术语边界的日志一致性

如何确保一个任期内最多只有一个领导者?

  • (查看图 2,RequestVote RPC 和服务器规则)

  • 领导者必须从大多数服务器获得投票

  • 规则:服务器每个任期只能投一票

  • 因此最多只有一个服务器可能认为自己赢了

  • 为什么需要多数?

    • 答案总是一样的!

    • “要求多数意味着不要求少数”

    • 允许容错(少数失败不会阻碍进展)

    • 防止分裂大脑(最多只有一个候选人可以获得多数票)

    • 确保重叠(大多数中至少有一个拥有每个先前提交的日志条目)

选举可能无法选择任何领导者吗?

  • 是的!

    • = 3 个候选人平均分裂投票,或者甚至数量的活动服务器,两个候选人各获得一半

如果在一次选举中没有人获得多数票会发生什么?

  • 超时,增加任期,新选举

  • 当服务器决定可能想成为候选人时,首先会等待一个随机延迟,只有在没有收到其他人的消息时才会成为候选人

  • 更高的任期优先,较旧任期的候选人退出

  • 注意:超时必须比完成选举所需的时间长!

  • 注意:这意味着一些任期可能没有领导者,没有日志条目

Raft 如何减少由于分裂投票而导致选举失败的机会?

  • 每个服务器在开始候选人身份之前都会延迟一段随机时间

  • 随机延迟为什么有用?

    • [查看服务器延迟到期时间的时间图]

    • 一个将选择最低随机延迟

    • 希望在下一个延迟到期之前有足够的时间进行选举

    • 这个想法在分布式系统中经常出现

图表:

 20 ms                   50 ms             80 ms
|-------------*-----------------------*-----------------*-----------|
              S1                     S2                S3 

如何选择随机延迟范围?

  • 太短:第二个候选人在第一个候选人完成之前开始

  • 太长:领导者失败后系统空闲太久

  • 一个大致的指导:

    • 假设完成无阻挡选举需要 10ms

    • 并且有五个服务器

    • 我们希望延迟相隔(比如)20ms

    • 因此随机延迟从 0 到 100ms

    • 再加上几个领导者心跳间隔的倍数

记住这个随机延迟的想法!

  • 这是一种经典的分散式软选举方案;例如以太网

Raft 的选举遵循一个常见模式:将安全性与进展分开

  • 机制确保一个任期内 < 2 个领导者

    • 问题:选举可能失败(例如 3 路分裂)
  • 解决方案:在新任期中始终启动新选举是安全的

    • 问题:重复的选举可能阻止任何工作的进行
  • 解决方案:机制降低浪费选举的概率

    • 来自领导者的心跳(提醒服务器不要开始选举)

    • 超时期限(不要太早开始选举)

    • 随机延迟(给一个领导者时间来当选)

记住:有一种方法可以将问题分为“安全/正确性”和“活性/性能”两个方面

如果旧领导者不知道新领导者当选了怎么办?

  • 可能是因为旧领导者没有看到选举消息

  • 新领导者意味着大多数服务器已经增加了 currentTerm

    • 因此旧领导者(带有��任期)无法为 AppendEntries 获得多数票

    • 尽管少数可能接受旧服务器的日志条目...

    • 因此,在旧任期结束时日志可能会分歧...

现在让我们转换话题到数据处理在任期边界

我们想要确保什么?

  • 每个服务器按相同的顺序执行相同的客户端命令

    • 即如果任何服务器执行,则没有其他服务器为该日志条目执行其他操作
  • 只要有单一的领导者,我们已经看到它使日志相同,当领导者改变时怎么办?

什么是危险?

第 3 任期的领导人在发送 AppendEntries 时崩溃

S1: 3
S2: 3 3
S3: 3 3
S2 and S3 might have executed; does Raft preserve it? 

可能是一系列崩溃,例如

S1: 3
S2: 3 3 (new leader) 4
S3: 3 3                (new leader) 5 

因此相同索引的不同条目!

回滚是一个大锤子 -- 强制领导者的日志在所有人身上

  • 在上述示例中,谁被选举谁就会强加日志给所有人

  • 例如:

    • S3 被选为第 6 任期的新领导人

    • S3 想要发送一个新条目(在第 6 任期)

      • AppendEntries 表示前一个条目必须有 term 5
    • S2 回复 false(AppendEntries 步骤 2)

    • S3 减少了 nextIndex[S2]

    • S3 发送 AppendEntries 以 term=5 的操作,表示 prev 的 term=3

    • S2 删除了来自 term 4 的操作(AppendEntries 步骤 3)并替换为来自 S3 的第 5 任期的操作(S1 拒绝了,因为它在该条目中没有任何内容)

      • S2 也为第 6 任期设置操作

好的,领导人将强制其自己的日志传给追随者

  • 但这还不够!

  • 回滚能删除一个已执行的条目吗?

何时执行日志条目?

  • 当领导者推进 commitIndex/leaderCommit

  • 当多数人与领导者一直匹配到这一点时

新领导人能否撤销上一任期末执行的条目?

  • 即新领导人的日志中可能缺少已执行的条目吗?

  • Raft 需要确保新领导人的日志包含每个可能执行的条目

  • 即必须禁止选举可能缺少已执行条目的服务器

选举规则是什么?

  • 图 2 表示只有在候选人的日志“至少与最新”时才投票

  • 因此,领导人将至少与大多数一样更新

“至少与最新”是什么意思?

这是否意味着日志的长度 >=?不,例如:

S1: 5, (leader) 6, (crash + leader) 7,
S2: 5                                  (leader) 8  
S3: 5                                           8 
  • 首先,这种情况可能发生吗?怎么样?

    • S1 是第 6 时代的领导者;崩溃+重启;第 7 时代的领导者;崩溃并停留在原地

      • 两次它仅在追加到自己的日志后崩溃
    • S2 是第 8 时代的领导者,只有 S2 和 S3 存活,然后崩溃

  • 谁应该成为下一任领导人?

    • S1 拥有最长的日志,但是条目 8 已经提交!!!

      • Raft 采用领导者的日志,因此 S1 作为领导者 -> 未提交的条目 8

      • 这样做是不正确的,因为 S2 可能已经回复给客户端

    • 因此新领导人只能是 S2 或 S3 中的一个

    • 即规则不能简单地是“最长的日志”

第 5.4.1 节的结尾解释了“至少与最新”投票规则

  • 比较最后一个条目

  • 较高的任期胜出

  • 如果相等的话,更长的日志胜出

所以:

  • S1 无法从 S2 或 S3 获得任何投票,因为 7 < 8

  • S1 将投票给 S2 或 S3,因为 8 > 7

  • S1 的来自第 6 和第 7 任期的操作将被丢弃!

    • 好的,因为没有多数 -> 没有执行 -> 没有客户端回复

要点:

  • “至少与最新”规则导致新领导人的日志包含其日志中的所有已执行条目

  • 因此新领导人不会撤销任何已执行的操作

  • 类似于 Paxos:新回合最终使用上一回合选择的值(如果有的话)

问题:图 7,a/d/f 中哪个可以被选举?

  • 即来自“不那么最新”的服务器的大多数投票?

Raft 最微妙的地方(图 8)

图 8:

S1 1, L 2,    ,      L 4,
S2 1,   2,    ,      \A/,
S3 1,   <-------- 2 <-| ,
S4 1,    ,    ,         ,
S5 1,    , L 3,         , L will erase all 2's 
  • 不是百分之百真实的,大多数上的日志条目被提交

    • 即永远不会被遗忘
  • 图 8:

    • S1 在第 2 个任期中是领导者,发送了两份 2 的副本

    • S5 在第 3 个任期中是领导者

    • S1 在第 4 个任期中是领导者,发送了 2 的另一份副本(因为 S3 拒绝了操作 4)

    • 如果 S5 现在成为领导者会怎样?

      • S5 可以获得大多数(没有 S1)

      • S5 将回滚 2 并用 3 替换它

    • 2 能执行吗?

      • 它在大多数上...

      • 所以 S1 在大多数之后是否可以在 leaderCommit 中提到它?

      • 不是!图 2 的最后说"日志[N].term == currentTerm"

      • 当发送第 3 份 2 的副本时,S1 在第 4 个任期

    • Raft 的实际提交点是什么?

      • 第 310 页右下角

      • "一旦创建条目的领导者在大多数上复制,就被提交"

      • 并且一个条目的提交点提交所有在它之前的条目

        • 这就是 2 如果 S1 没有失去领导权时如何提交的方式

另一个话题:配置更改(第 6 节)

  • 配置=服务器集

  • Raft 如何更改服务器集?

  • 例如,每隔几年可能想要退休一些,增加一些

  • 或者一次性移动到一个全新的服务器集

  • 或增加/减少服务器数量

破损配置更改会如何工作?

  • 每个服务器都有当前配置中的服务器列表

  • 通过逐一更改列表来更改配置

  • 例如:想要用 S4 替换 S3

    • S1:1,2,3 1,2,4

    • S2:1,2,3 1,2,3

    • S3:1,2,3 1,2,3

    • S4:1,2,4 1,2,4

  • 糟糕!

    • 现在两个不相交的组/领导者可以形成:

      • S2 和 S3(不知道新配置)

      • S1 和 S4

    • 两者都可以处理客户端请求,所以分裂大脑

Raft 配置更改

  • 想法:包括配置的“加入共识”阶段

  • 旧组的领导者记录切换到联合共识的条目

    • 在联合共识期间,领导者分别记录在旧和新中

      • 即每个日志和每个日志条目上的两个协议

      • 这将迫使新服务器赶上并迫使新旧日志相同

  • 在大多数旧和新的都切换到联合共识之后,

    • 领导者记录切换到最终配置的条目

例子(因为原始笔记中没有正确说明,所以不会有意义):

 S1: 1,2,3  1,2,3+1,2,4
  S2: 1,2,3
  S3: 1,2,3
  S4:        1,2,3+1,2,4 
  • 如果崩溃但新领导者没有看到切换到联合共识,

    • 然后旧组将继续,没有切换,但没关系
  • 如果崩溃并且新领导者看到了切换到联合共识,

    • 它将完成配置更改

性能

  • 没有关于它可以处理请求有多快的数字

  • 瓶颈可能是什么?

  • 磁盘:

    • 需要为客户端数据的持久性写入磁盘,以及协议承诺

    • 每个客户端请求写入?所以每秒 100 个?

    • 可能批量处理并获得 10,000 到 100,000

  • 每个客户端请求几次消息交换

    • 本地 LAN 消息交换需要几十微秒?

    • 所以每秒 10 万个?

下周:在复杂应用中使用类似 Raft 的协议

Go

Russ Cox 的 Go 讲座

为什么选择 Go?

  • 解决 Google 在可扩展性方面的问题的答案

    • 10⁶+ 台机器设计点

    • 在 1000 台机器上运行是例行公事

    • 不断地编写相互协调的程序

      • 有时 MapReduce 起作用,有时不起作用

谁在谷歌使用 Go

  • Chrome 移动设备上的 SPDY 代理使用 Go 编写的 数据压缩代理

  • dl.google.com

  • YouTube MySQL 负载均衡器

  • 目标是网络服务器,但它是一种很棒的通用语言

  • Bitbucket、bitly、GitHub、Dropbox、MongoDB、Mozilla 服务、纽约时报等

并发

  • 《通信顺序进程》,由霍尔,1978

    • 强烈建议阅读

    • 在某种意义上,是 UNIX 管道的泛化

  • 贝尔实验室在 80 年代、90 年代开发了一些并发语言:

    • Pan、Promela、Newsqueak、Alef、Limbo、Libthread、Concurrent ML
  • 谷歌在 2000 年代开发了 Go

没有 goroutine ID

  • “没有 goroutine ID,所以我不能杀死我的线程”

    • 这就是通道的作用:只需通过通道告诉你的线程关闭自己

    • 此外,杀死它们有点“不合群”。

      • 我们的意思是,如果你一直像那样杀死你的线程,你的程序可能不会运行得很好

通道 vs. 互斥锁

  • 如果你需要互斥锁,请使用互斥锁

  • 如果你需要条件变量,请考虑使用通道代替

  • 不要通过共享内存进行通信,而是通过通信共享内存

网络通道

  • 拥有等效的网络通道会很棒

  • 如果你将本地抽象(如通道)用于新的上下文,比如网络,忽略了故障模式(等等),那么你将会遇到麻烦

工程工作的规模

2011 年,Google 有:

  • 5000 多名开发人员

  • 每分钟 20 多次更改

  • 每月 50% 的代码库更改(文件?可能不是行)

  • 每天执行 5000 万个测试用例

  • 单一代码树项目

需要一种新的语言来解决其他语言在这种规模的软件工程中存在的问题

编译规模很重要。

  • 当你编译依赖于 B 的包 A 时,大多数(全部?)语言需要先编译 B

  • Go 不会。

  • 如果你在谷歌项目的规模上使用传统语言,这样的依赖关系会减慢编译速度

    • 深层次依赖(A->B->C->D->...)会使情况变得更糟
  • 例子:在某个时候,他们发现一个 PostScript 解释器无缘无故地编译到了服务器二进制文件中,这是由于奇怪的依赖关系

接口 vs. 继承

  • 继承层次结构很难搞定,如果你没有搞定,以后更改起来就会很困难

  • 接口更加非正式和更清晰地表明程序的哪些部分由谁拥有和提供

可读性和简单性

  • 迪克·加布里埃尔的引言:

    “我总是对早期编程语言的轻盈和静止感到高兴。文本不多;完成了很多事情。旧程序读起来像是一个口才流利的研究人员和一个精通机械的同事之间的轻松对话,而不是与编译器的辩论。谁会想到复杂性会带来这么多噪音呢?”

  • 简化语法

  • 避免聪明的做法:三元运算符,宏

  • 不要让编写代码变成像“与编译器争论”一样

  • 6 个月后不想要再费力地解析代码

设计标准

  • 由 Rob Pike、Robert Griesemer 和 Ken Thompson 在 2007 年末创立

  • Russ Cox、Ian Lance Taylor 在 2008 年中期加入

  • 通过共识来设计(每个人都可以否决一个特性,如果他们不想要它的话)

泛型

  • Russ 说:“不要使用 *list.List,你几乎永远不需要它们。使用切片。”

    • 泛型并不是坏事,只是很难做到正确。

      • 早期的 Java 泛型设计者也同意并警告 Go 设计者要小心

        • 似乎他们后悔涉足了那个行业

工程工具

  • 当你有数百万行代码时,你需要机械帮助

    • 就像改变一个 API
  • Go 设计成易于解析(不像 C++)

  • 标准格式化程序

  • 这意味着你无法区分机械变化和手动变化

    • 实现代码的自动重写

更多自动化

  • 修复因 API 更新而产生的代码问题

    • 早期的 Go 版本 API 发生了很大变化

    • 谷歌有一个重写器,会修复使用了已更改的 API 的代码

  • 重命名结构字段,具有冲突解决的变量

  • 移动包

  • 包的分离

  • 代码清理

  • 将 C 代码改写为 Go

  • 全局分析,找出例如一个接口的所有实现者是什么

Go 的现状

  • Go 1.4 在 2014 年 12 月发布

  • Go 1.5 中的工具链是用 Go 实现的,而不是用 C 实现的

    • 并发 GC

    • 适用于移动设备的 Go

    • Go 在 PowerPC、ARM64 上

  • 很多人在使用它

  • 在 Google/Go 之外的 Go 大会

问答环节

  • Go 对比 C/C++

    • Go 是垃圾收集的,这是最大的区别,所以更慢

    • 有时候 Go 可以比 Java 更快

    • 一旦你意识到这一点,你可以编写比 C/C++ 代码运行更快的代码。

    • 没有理由不让不分配内存的代码运行得像 C/C++ 一样快

  • 目标是在 Google 之外使用 Go 吗?

    • 是的!否则语言会消亡?

    • 你得到了一批专家给你建议并编写工具等等。

      • C++ 内存模型的专家给出了关于 Go 内存模型的反馈

        • 非常有用
    • 不试图取代像语言 X 这样的东西

      • 但是他们曾经使用 C/C++,不想再使用了

      • 然而 Python 和 Ruby 用户更多地转向了 Go

        • Go 感觉起来与 C/C++ 一样轻巧,但是静态类型检查了
  • 关于 Go 的好处的研究?

    • 收集的数据不多

Harp

6.824 2015 年第 8 讲:Harp

注意:这些讲座笔记是从 2015 年春季 6.824 课程网站上发布的内容稍作修改。

论文:Harp 文件系统中的复制

  • 利斯科夫,盖马瓦特,格鲁伯,约翰逊,施里拉,威廉姆斯

  • SOSP 1991

为什么我们在读这篇论文?

  • Harp 是第一个完整的主/备份系统,处理了分区问题。

  • 它是一个完整的复制服务(文件服务器)的案例研究

  • 它使用类似 Raft 的复制技术

1991 年的论文如何仍然值得阅读?

  • Harp 引入的技术仍然被广泛使用

  • 很少有论文描述完整的复制系统

这篇论文混合了基本原理和次要内容。

  • 我们非常关心复制

  • 我们可能对 NFS 本身不太在乎

    • 但是我们非常关心将实际应用程序与复制协议集成时面临的挑战。
  • 我们关心优化的可能性所在。

我将专注于 Harp 中尚不存在的部分。

  • 但请注意,Harp 比 Raft 早了 20 多年。

  • Raft 在很大程度上是 Harp 开创性思想的教程。

    • 尽管它们在许多细节上有所不同。

Harp 论文解释了 Raft 论文没有解释的内容?

  • 将复杂服务适应状态机抽象

    • 例如应用操作两次的可能性
  • 大量优化

    • 请求流水线到备份

    • 见证者,以减少复本的数量

    • 使用租约只在主服务器上执行只读操作

  • 重新启动的服务器与大状态的高效重新集成

    • 不想做像复制整个磁盘这样的事情

    • "赶上"重新加入复制品

  • 电源故障,包括所有服务器同时失败

  • 磁盘上的高效持久性

  • 日志释放

Harp 的作者尚未实现恢复

基本设置是熟悉的

  • 客户端,主服务器,备份(S),见证者(S)。

  • 客户端 -> 主服务器

  • 主服务器 -> 备份

  • 备份(S)-> 主服务器

    • 主服务器等待当前视图中的所有备份/晋升的见证者

    • 提交点

  • 主服务器 -> 执行并回复客户端

  • 主服务器 -> 告诉备份要提交

  • 2n+1个服务器,n个备份,n个见证者,1 个主服务器。

    • 需要n+1个服务器的大多数=>

    • 容忍多达n个故障

  • 客户端将 NFS 请求发送到主服务器

    • 主服务器将每个请求转发给所有备份

    • 在所有备份回复之后,主服务器可以执行操作并将其应用于其文件系统

    • 在后续操作中,主服务器通过 ACK 告诉备份操作已提交

为什么需要2b+1个服务器来容忍b个故障?

  • (这是复习...)

  • 假设我们有N个服务器,并执行写操作。

  • 不能等待超过N-b,因为b可能已经死了。

  • 所以让我们要求每个操作等待N-b

  • 我们没有等待的b可能是活动的,并且在另一个分区中。

  • 如果N-b > b,我们可以阻止它们继续进行。

  • N > 2b => N = 2b + 1就足够了。

Harp 的见证人是什么?

  • 主要操作者和备份都有 FSs

  • 见证人不会从主要操作者接收任何内容,也没有 FSs

  • 假设我们有一个P,B和一个W

  • 如果有一个分区P | B, W,见证人充当裁决者

    • 无论哪个(P 或 B)能与见证人交流,都可以继续执行客户端操作

    • 见证人充当裁决者:谁能与其交流就能获胜并成为主要操作者

  • 见证人的第二个用途是记录操作

  • 一旦见证人成为分区B, W的一部分,它记录操作,以便大多数节点具有最新操作。

  • 见证人的最终功能是,当主要操作者复活时,自主操作者已经记录了自主消失以来发出的每一个操作,因此见证人可以重新执行每个操作给主要操作者,使主要操作者对执行的所有操作保持最新状态

    • 高效地使主要操作者跟上速度

    • 备份也可以做到,但 Harp 设计为备份将操作日志转储到磁盘,见证人保留日志本身,以便能够快速将其发送给主要操作者重新应用

      • 重新应用见证人日志比复制备份磁盘更快的假设

      • 见证人日志不会变得太大的假设

  • 见证人是与 Raft 的一个重要区别。

  • b个见证人通常不会听取操作或保留状态。

  • 为什么这样做没问题?

    • 2b+1中的b+1确实有状态

    • 因此,任何b个故障都会留下至少一个活动状态的副本。

  • 为什么需要b个见证人?

    • 如果带有状态的b个副本失败,见证人提供所需的b+1多数派支持。

    • 以确保只有一个分区在运行--没有分裂的情况。

  • 因此,在一个 3 服务器系统中,见证人用于解决主要操作者和备份位于不同分区时允许哪个分区操作的冲突。

    • 带有见证人的分区获胜。

主要操作者需要将操作发送给见证人吗?

  • 主要操作者必须从2b+1中的大多数收集每个 r/w 操作的 ACK。

    • 以确保它仍然是主要操作者--仍然处于多数派分区。

    • 以确保操作在足够多的服务器上以与任何交集

      • 形成新视图的未来多数派。
  • 如果所有备份都正常,主要操作者+备份足以形成多数派。

  • 如果m个备份宕机:

    • 主要操作者必须与m个“晋升”的见证人交流,以获得每个操作的多数派支持。

    • 这些见证人必须记录该操作,以确保与任何重叠

      • 未来的多数派。
    • 因此,每个“晋升”的见证人都会保留一个日志。

  • 因此,在一个2b+1系统中,每个视图始终有b+1个主要操作者必须为每个操作联系的服务器,并且存储每个操作。

注意:与 Raft 有些不同

  • Raft 继续将每个操作发送到所有服务器,当大多数回答时继续

    • 因此领导者必须保持完整的日志,直到失败的服务器重新加入
  • Harp 从视图中消除失败的服务器,不会将操作发送给它

    • 只有见证者必须保留一个大日志;有特殊计划(内存,磁盘,磁带)。
  • 更大的问题是,将重新加入的副本更新到最新状态可能需要大量工作;需要仔细设计。

UPS 的故事是什么?

  • 这是 Harp 设计中最有趣的方面之一

  • 每台服务器的电源线都插在 UPS 上

  • UPS 有足够的电池可以运行服务器几分钟

  • UPS 通过串口告诉服务器主交流电断电了

  • 服务器将脏文件系统块和 Harp 日志写入磁盘,然后关闭

Harp 购买 UPS 是为了什么?

  • 有效防止所有服务器的交流电故障

  • 对于最多 b 台服务器的故障,复制就足够了

  • 如果所有服务器都失败并丢失状态,那就不止 b 次故障了,

    • 所以 Harp 没有保证(实际上没有状态!)
  • 有了 UPS:

    • 每台服务器都可以在不写入磁盘的情况下回复!

    • 但仍然保证保留最新状态,尽管同时发生电源故障

  • 但请注意:

    • UPS 不能保护其他同时发生故障的原因

    • 例如错误,地震

    • Harp 对在 UPS 保护的崩溃后重新启动的服务器进行了不同处理

      • 而不是那些重新启动时丢失内存状态的崩溃
    • 因为后者可能已经忘记了已提交的操作

  • 对于独立的故障,Harp 有强大的保证,对于像软件错误这样会导致一系列崩溃的东西,它并没有真正的解决方案

更大的���点,每个容错系统都面临的问题

  • 每个复制系统都倾向于需要一个提交点

  • 副本必须保持持久状态以应对所有服务器的故障

    • 已提交的操作

    • 最新的视图编号,提案编号等

  • 必须在回复之前持久化这个状态

  • 每次写入都写入磁盘非常慢!

    • 每个磁盘写入需要 10 毫秒,所以每秒只有 100 个操作
  • 所以有几种常见模式:

    1. 低吞吐量

    2. 批处理,高延迟

      • 批量写入很多数据并同时执行它们以分摊每次写入的成本

      • 但现在你需要让客户端等待他们的写入完成更多

        • 因为它们也在等待其他客户端的写入完成
    3. 从同时发生的故障中丢失或不一致的恢复

      • 崩溃后没有保证
    4. 电池,闪存,带电容的固态硬盘等

让我们谈谈 Harp 的日志管理和操作执行

  • 主服务器和备份必须将客户端操作应用到它们的状态中

  • 这里的状态是一个文件系统--目录,文件,所有者,权限等

  • Harp 必须模拟普通的 NFS 服务器给客户端

    • 即不要忘记已发送回复的操作

典型日志记录中有什么?

  • 不仅仅是客户端发出的操作,比如chmod

日志记录存储:

  • 客户端的 NFS 操作(写入,创建目录,修改权限等)

  • 阴影状态:执行后修改的 i 节点和目录内容

    • (即执行操作后的结果)
  • 客户端 RPC 请求 ID,用于重复检测

    • 主服务器可能会重复一个 RPC,如果它认为备份服务器已经失败
  • 回复发送给客户端,用于重复检测

为什么 Harp 有这么多日志指针?

  • FP 最近的客户端请求

  • CP 提交点(主服务器中的真实点,备份中的最新听到的点)

  • AP 最高更新发送到磁盘

  • LB 磁盘已完成写入到此处

  • GLB 所有节点已将磁盘写入到此处

为什么存在 FP-CP 间隔?

  • 因此,主服务器无需等待每个备份的 ACK。

    • 在将下一个操作发送到备份之前
  • 主服务器将操作 CP..FP 流水线传输到备份。

  • 如果有并发客户端请求,则吞吐量更高。

为什么存在 AP-LB 间隔?

  • 允许 Harp 在等待磁盘之前发出许多操作作为磁盘写入

  • 如果有大量写入(例如,ARM 调度),则磁盘更高效

LB 是什么?

  • 此副本在磁盘上拥有所有小于等于LB的内容。

  • 因此,它将不再需要这些日志记录。

为什么存在 LB-GLB 间隔?

  • GLB 是所有服务器的 LB 的最小值。

  • GLB 是最早的记录,如果某个服务器失去内存,则某些服务器可能需要它。

Harp 何时执行客户端操作?

有两种答案!

  1. 当操作到达时,主服务器确切地确定应该发生什么。

    • 生成修改了 i-node、目录等的结果磁盘字节。

    • 这是影子状态。

    • 这发生在 CP 之前,因此主服务器必须查阅日志中的最近操作以找到最新的文件系统状态。

  2. 操作提交后,主服务器和备份可以将其应用到其文件系统上。

    • 它们将日志条目的影子状态复制到文件系统;

    • 它们实际上并未执行该操作。

    • 现在主服务器可以回复客户端的 RPC 了。

Harp 为何以这种方式分割执行?

  • 如果服务器崩溃并重新启动,则通过重放可能已错过的日志条目将其更新到最新状态。Harp 无法确定崩溃前的最后一个操作是什么,因此可能会重复一些操作。完全执行某些操作两次是不正确的,例如文件追加。

  • 因此,Harp 日志条目包含结果状态,这是应用的内容。

追加示例:

 /---> picks up the modified inode
                        /
 --------------------------------
      |   Append1   |  Append2  | 
 ---- * -------------------------
     / \        \
      |          \-> new inode stored here
     CP 

如果备份在将 A1 写入磁盘后崩溃,但在回复给主服务器之前,备份重新启动时没有明显的方法告知其是否执行了 A1。因此,它必须重新执行它。因此,这些日志记录必须是“可重复的”。

实际上,许多复制系统都必须处理这个问题,这是一种处理方式。这也说明了复制可能是多么不简单。

  • Harp 需要意识到 FS 级 inode,例如

关键在于,多次重放意味着复制对服务不透明。

  • 服务必须修改以生成和接受来自客户端操作的状态修改。

  • 一般而言,在将复制应用于现有服务时,服务必须进行修改以处理多次重放。

Harp 主服务器能否执行只读操作而不复制到备份?

  • 例如读取文件。

  • 这将更快--毕竟,没有新数据需要复制。

  • 我们将只读操作转发到备份的原因是确保我们找出是否分区,并且执行了我们不知道的 1000 次操作:确保我们不会用正在读取的数据的旧写入进行回复

  • 有何危险?

  • Harp 的理念:租约

  • 备份承诺不会在一段时间内形成新的视图。

    • (即不要将任何操作作为主节点处理)
  • 主节点可以在本地执行只读操作,时间减去误差。

    • 因为它知道在那段时间内备份不会作为主节点执行任何操作(备份承诺了这一点!)
  • 取决于相当同步的时钟:

    • Robert Morris:“对此并不太满意。”

    • 主节点和备份之间必须对时间流逝速度有界的不同意见。

    • 这实际上需要有界的频率偏差

      • 显然硬件在提供这个方面做得很糟糕

主节点执行只读操作时应该使用什么状态?

  • 它必须等待所有先前到达的操作都提交吗?

    • 不!那几乎和提交只读操作一样慢。
  • 它应该查看 FP 操作时的状态吗,即最新的读/写操作?

    • 不行!该操作尚未提交;不允许显示其效果。
  • 因此,Harp 使用 CP 时的状态执行只读操作。

  • 如果客户端发送了一个写操作,然后(在写操作完成之前)读取了相同的数据怎么办?

    • 读操作可能在写操作之前看到数据!

    • 为什么可以这样?

    • 客户端同时发送了读操作和写操作。它没有权利期望其中一种顺序。因此,如果读操作没有看到写操作的效果,那是可以接受的——如果读操作通过网络比写操作更快,那么你会得到相同的答案,这种情况可能发生。只有在发出写操作,等待写操作的回复,然后发出读操作时,才能期望读操作看到写操作的效果。

失败恢复是如何工作的?

  • 即,Harp 在视图更改期间如何恢复复制的状态?

为以下情景做准备:

5 个服务器:S1 通常是主节点,S2+S3 是备份,S4+S5 是见证者

情景:

 S1+S2+S3; then S1 crashes
  S2 is primary in new view (and S4 is promoted) 
  • S2 会拥有每一个已提交的操作吗?

    • 是的。
  • S2 会拥有 S1 接收的每一个操作吗?

    • 不。不,可能操作从 S1 到 S2,然后 S1 崩溃了。
  • S2 的日志尾部会与 S3 的日志尾部相同吗?

    • 不一定。

      • 可能操作已经到达了 S2,但未到达 S3,然后 S1 崩溃了。

      • 可能操作已经到达了 S2,而 S3 崩溃了,所以 S4 被提升为领导者。然后 S3 恢复了?

  • S2 和 S3 的日志尾部可以相差多远?

    • 不是由 CP 决定,因为已提交的操作可能是在提升见证者的帮助下提交的=>备份日志不同
  • 如何使 S2 和 S3 的日志保持一致?

    • 必须提交在 S2+S3 日志中都出现的操作

    • 那些只出现在一个日志中的操作怎么处理?

      • 在这种情况下,可以丢弃,因为可能没有提交。

      • 但通常已提交的操作可能仅在一个日志中可见。

  • 从什么时候开始,被提升的见证者必须开始保留日志?

如果 S1 在回复客户端之前崩溃了怎么办?

  • 客户端会收到回复吗?

S1 恢复后,磁盘完好无损,但内存丢失。

  • 它将成为主节点,但 Harp 不能立即使用其状态或日志。

    • 与 Raft 不同,领导者只有在拥有最佳日志时才会选举。

    • Harp 必须从被提升的见证者(S4)重新播放日志

  • S1 在崩溃前是否执行了一个操作,而副本在接管后没有执行?

    • 不,只执行到 CP,而 CP 在 S2+S3 上是安全的。

新场景:S2 和 S3 被分区(但仍然存活)。

  • S1+S4+S5 能够继续处理操作吗?

    • 是的,晋升为见证人 S4+S5
  • S4 移动到 S2/S3 分区

  • S1+S5 能够继续吗?

    • 不,主要的 S1 没有得到足够的备份 ACK
  • S2+S3+S4 能够继续吗?

    • 是的,新视图将日志条目从 S4 复制到 S2、S3,现在 S2 是主要的
  • 注意:

    • 新主要缺少了许多已提交的操作

    • 一般来说,一些 已提交 操作可能仅在一个服务器上

新场景:S2 和 S3 被分区(但仍然存活)。

  • S4 崩溃,丢失内存内容,重新启动到 S2/S3 分区

  • 他们可以继续吗?

    • 仅当没有形成并提交更多操作的其他视图时。
  • 如何检测?

    • 取决于 S4 的磁盘视图号是什么。

    • 如果 S4 的磁盘视图号与 S2+S3 的相同,则可以。

      • 未形成新的视图。

      • S2+S3 必须知道旧视图中所有已提交操作的情况。

每个人(S1-5)都遭遇断电。

  • S4 的磁盘和内存丢失了,但修复后会重新启动。

  • S1 和 S5 永远无法恢复。

  • S2 和 S3 在磁盘上保存所有内容,重新启动没有问题。

  • S2+S3+S4 能够继续吗?

  • (比看起来更难)

    • 相信答案是“否”:无法确定故障之前 S4 的状态。

    • 可能通过 S1+S5 形成一个新视图,并执行一些操作。

      • S2 和 S3 能知道先前的视图吗?不总是。

Harp 何时可以形成新视图?

  1. 没有其他可能的视图。

  2. 知道最近视图的视图号。

  3. 知道最近视图的所有操作。

细节:

  • 如果你有 n+1 个节点在新视图中,则 (1) 为真。

  • 如果你有 n+1 个节点,并且自上一个视图以来没有丢失视图号,则为真。

    • 视图号存储在磁盘上,所以它们只需要知道磁盘是正常的。

    • 其中之一 必须 在先前的视图中。

    • 因此只需取最高视图号。

  • 还有 #3 吗?

    • 需要一个磁盘映像和一个日志,共同反映出上一个视图结束时的所有操作。

    • 可能来自不同的服务器,例如晋升的见证人的日志,多个视图之前失败的备份。

Harp 有性能优势吗?

  • 在图 5-1 中,为什么 Harp 比非复制服务器 更快

  • 通过将 RPC 替换为磁盘操作,我们可以期待多少胜利?

为什么图形 x=load y=response-time?

  • 为什么这张图有意义?

  • 为什么不只是图形总时间执行 X 操作?

  • 一个原因是系统有时在高负载下变得更有效率/不那么有效率。

  • 我们非常关心他们在超载情况下的表现。

为什么响应时间随负载增加而增加?

  • 为什么首先是渐进的...

    • 排队和随机突发?

    • 有些操作比其他操作更昂贵,导致临时延迟。

  • 然后几乎笔直上升?

    • 可能有硬限制,如每秒磁盘 I/O 次数。

    • 一旦提供的负载 > 容量,队列长度就会发散

DSM 和顺序一致性

6.824 2015 年第 9 讲:DSM 和顺序一致性

注意: 这些讲义稍作修改,来自 2015 年春季的 6.824 课程网站上发布的讲义。

主题: 分布式计算

  • 在分布式机器上进行并行计算

  • 4 篇关于如何使用一组机器解决大型计算问题的论文

    • 我们已经阅读了其中一个:MapReduce
  • 另外 3 篇论文(IVY、TreadMarks 和 Spark)

    • 两者提供通用内存模型

    • Spark 类似于 MapReduce 风格

分布式共享内存(DSM)编程模型

  • 采用多处理器提供的相同编程模型

  • 程序员可以使用锁和共享内存

    • 程序员熟悉这种模型

    • 例如,像 goroutines 一样共享内存

  • 通用型

    • 例如,不像 MapReduce 那样有限制
  • 在多处理器上运行的应用程序可以在 IVY/TreadMarks 上运行

挑战: 分布式系统没有物理共享内存

  • 在一组廉价机器的网络上

    • [图示:LAN,带有 RAM 的机器,MGR]

图示:

 *----------*   *----------*   *----------*
    |          |   |          |   |          |
    |          |   |          |   |          |
    |          |   |          |   |          |
    *-----*----*   *-----*----*   *-----*----*
          |              |              |
  --------*--------------*--------------*-------- LAN 

图示:

 M0             M1
    *----------*   *----------*   
    | M0 acces |   | x x x x  |   
    |----------|   |----------|
    | x x x x  |   | M1 acces |   
    *-----*----*   *-----*----*   
          |              |        
  --------*--------------*------- LAN

  The 'xxxxx' pages are not accesible locally,
  they have to be fetched via the network 

方法:

  • 使用硬件支持的虚拟内存模拟共享内存

  • 用 2 台机器阐明的一般思路:

    • 地址空间的一部分映射到 M0 的物理内存的一部分

      • 在 M0 上,映射为 M0 的物理页

      • 在 M1 上,映射为不存在

    • 地址空间的一部分映射到 M1 的物理内存的一部分

      • 在 M0 上,映射为不存在

      • 在 M1 上,映射到其物理内存

  • M1 上的应用程序线程可能引用位于 M0 上的地址

    • 如果线程 LD/ST 到那个“共享”地址,M1 的硬件将发生页错误

      • 因为页被映射为不存在
    • 操作系统将页错误传播到 DSM 运行时

    • DSM 运行时可以从 M0 获取页面

    • M0 上的 DSM,将页映射为不存在,并将页发送到 M1

    • M1 上的 DSM 从 M0 接收到,将其复制到内存的某处(比如地址 4096)

    • M1 上的 DSM 将共享地址映射到物理地址 4096

    • DSM 从页错误处理程序返回

    • 硬件重试 LD/ST

  • 运行多线程代码而无需修改

    • 例如矩阵乘法、物理模拟、排序

挑战:

  • 如何高效实现它?

    • IVY 和 Treadmarks
  • 如何提供容错性?

    • 许多 DSM(分布式共享内存系统)对此进行了折中处理

    • 一些 DSM 定期对整个内存进行检查点

    • 我们在谈论 Spark 时会回到这个问题上

正确性:一致性

  • 在优化性能之前,我们需要明确什么是正确的

    • 优化应该保证正确性
  • 比看起来不那么明显!

    • 选择在性能和程序员友好性之间权衡

    • 这在许多设计中是一个巨大的因素

    • 更多内容将在下一讲中介绍

  • 今天的论文假设了一个简单的模型

    • 分布式内存应该表现得像单一内存

    • Load/stores 类似于实验 2-4 中的 put/gets

示例 1:

 x and y start out = 0

  M0:
    x = 1
    if y == 0:
      print yes
  M1:
    y = 1
    if x == 0:
      print yes

  Can they both print "yes"? 

幼稚的分布式共享内存

图示 1:

  • M0、M1、M2、LAN

  • 每台机器都有所有内存的本地副本

  • 读取:来自本地内存

  • 写入:向其他主机发送更新消息(但不等待)

  • 快速:从不等待通信

这种天真的记忆是否有效?

  • 它会对 示例 1 做什么?

    • 它可能会失败,因为 M0 和 M1 在到达它们的 if 语句时可能无法看到写入,因此它们都会打印
  • 天真的分布式内存快速但不正确

图表(破碎的方案):

 M0
    *----------*   *----------*   *----------*
    |          |   |          |   |          |
    |        ------------------------> wAx   |
    |        ----------> wAx  |   |          |
    *-----*----*   *-----*----*   *-----*----*
          |              |              |
  --------*--------------*--------------*-------- LAN 
  • M0 在本地写入并在完成后告知其他机器

  • 想象一下,如果每台机器都运行一个将地址 A 处的值增加 3 次的程序,您将获得什么输出

一致性=顺序一致性

  • 当你有多个进程时,“读取看到最新写入”的表述不够清晰

  • 需要更加精确地确定正确性

  • 顺序一致性意味着:

    • 任何执行的结果都与如果相同

      • 所有处理器的操作按某种顺序(总顺序)执行

      • 并且每个单独处理器的操作以其程序指定的顺序出现在此顺序中

        • (如果 P 在 B 之前说 A,那么在该顺序中不能有 B; A; 出现)
      • 并且读取在总顺序中看到上次写入

  • 必须存在某些操作的总顺序,使得

    1. 每台机器的指令按顺序出现在顺序中

    2. 所有机器看到与该顺序一致的结果

      • 即读取以顺序中的最新写入为准
  • 单个共享内存的行为

顺序一致性是否会导致我们的示例获得直观的结果?

顺序:

 M0: Wx1 Ry?
  M1: Wy1 Rx? 
  • 系统需要将这些合并为一个顺序,并保持每台机器操作的顺序。

  • 所以有几种可能性:

    • Wx1 Ry0 Wy1 Rx1

    • Wx1 Wy1 Ry1 Rx1

    • Wx1 Wy1 Rx1 Ry1

    • 其他人也是,但都是对称的吗?

  • 什么是被禁止的?

    • Wx1 Ry0 Wy1 Rx0 -- Rx0 读取未看到前面的 Wx1 写入(天真的系统这样做了)

    • Ry0 Wy1 Rx0 Wx1 -- M0 的指令顺序错误(一些 CPU 这样做)

Go 的内存一致性模型

  • Go 对示例的语义是什么?

  • Go 将允许两个 goroutine 都打印“是”!

  • Go 的竞争检测器无论如何都不会喜欢示例程序

  • 程序员必须使用锁定/通道来获取合理的语义

  • Go 不要求硬件/DSM 实现严格一致性

  • 关于更弱一致性的详细内容将于星期四讨论

示例:

x = 1
y = 2 
  • 如果线程 A 看到了对 y 的写入,Go 的内存模型告诉你线程 A 是否会看到对 x 的写入

    • 在 Go 中,如果 y 已被写入,就不能保证 x 的写入会被看到

顺序一致性的简单实现

获得顺序一致性的一种直接方法:在两台或三台机器之间添加一个管理器,将它们的指令交错执行

 *----------*   *----------*   
    |          |   |          |   
    |          |   |          |   
    |          |   |          |   
    *-----*----*   *-----*----*   
          |              |        
  --------*--------------*--------
                 |
                 |
           *----------*
           | inter-   |
           | leaver   |
           |          |
           *-----*----*
                 |
                -*-
                \ /
                 .
                RAM 

图表 2:

 *----------*   *----------*   
    |          |   |          |   
    |          |   |          |   
    |          |   |          |   
    *-----*----*   *-----*----*   
          |              |        
  --------*--------------*--------
                 |
                 |
           *----------*
           |          |
           |          |
           |          |
           *-----*----*
                 |
                -*-
                \ /
                 .
                RAM 
  • 单个内存服务器

  • 每台机器按顺序将 r/w 操作发送到服务器,等待回复

  • 服务器从等待操作中选择顺序

  • 服务器逐个执行,发送回复

  • 大的思路:

    • 如果人们只是读取一些数据,我们可以在所有人上复制它

    • 如果有人写入数据,我们需要阻止其他人写入它

      • 所以我们借鉴了其他人的记忆

这种简单的实现将会很慢

  • 单个服务器将会过载

  • 没有本地缓存,因此所有操作都等待服务器

这让我们谈到了 IVY

  • IVY:耶鲁大学集成共享虚拟内存

  • 共享虚拟内存系统中的内存一致性,Li 和 Hudak,PODC 1986

IVY 的大局观

 [diagram: M0 w/ a few pages of mem, M1 w/ a few pages, LAN] 
  • 操作内存页,存储在机器 DRAM 中(没有内存服务器)

  • 每个页面都存在于每台机器的虚拟地址空间中

  • 每台机器上,页面可能无效、只读或读写

  • 使用虚拟内存硬件拦截读取/写入

不变量:

  • 页面要么是:

    • 在一个机器上读/写,所有其他机器无效;或

    • \(\geq 1\) 台机器上只读,无读写

  • 在无效页面上的读取故障:

    • 降级 R/W(如果有的话)为 R/O

    • 复制页面

    • 标记本地副本 R/O

  • 在只读页面上的写故障:

    • 使所有副本无效

    • 标记本地副本 R/W

IVY 允许在写入之间存在多个读取者副本

  • 为了速度 - 本地读取速度快

  • 不需要强制读取顺序,该顺序发生在两次写入之间的读取中

  • 让它们同时发生 - 每个读者都有页面的副本

为什么在写入之前必须使所有副本无效?

  • 一旦写入完成,所有后续读取必须看到新数据

  • 否则,我们会破坏我们的示例,并且无法获得顺序一致性

IVY 在示例中的表现如何?

  • 即,M0 和 M1 可以同时打印 “是” 吗?

  • 如果 M0 看到 y == 0,

    • M1 尚未将其写入 y(无陈旧数据 == 读取先前写入的数据),

    • M1 尚未读取 x(每台机器按顺序),

    • M1 必须看到 x == 1(无陈旧数据 == 读取先前写入的数据)。

消息类型:

  • [不要在板上列出这些,仅供参考]

  • RQ 读取查询(读取者到管理器(MGR))

  • RF 读取转发(MGR 到所有者)

  • RD 读取数据(所有者到读取者)

  • RC 读取确认(读取者到 MGR)

  • 等等

(参见 ivy-code.txt 网站上的文件)

情景 1:M0 有可写副本,M1 想要读取

图 3:

 [time diagram: M 0 1] 
  1. M1 尝试读取得到页面错误

    • 因为必须将页面标记为无效,所以 M0 拥有它以进行 R/W(请参阅前面描述的不变量)
  2. M1 发送 RQ 到 MGR

  3. MGR 将 RF 发送到 M0,MGR 将 M1 添加到 copy_set

    • 什么是 copy_set

    • copy_set 字段列出了所有具有页面副本的处理器。这允许执行无需广播的失效操作。”

  4. M0 标记页面为 \(access = read\),发送 RD 到 M1

  5. M1 标记 \(access = read\),发送 RC 到 MGR

情景 2:现在 M2 想要写入

图 4:

 [time diagram: M 0 1 2] 
  1. M2 上的页面故障

  2. M2 发送 WQ 到 MGR

  3. MGR 发送 IV 到 copy_set(即 M1)

  4. M1 发送 IC 消息到 MGR

  5. MGR 将 WF 发送到 M0,设置 owner=M2,copy_set={}

  6. M0 向 M2 发送 WD,访问=无

  7. M2 标记 r/w,发送 WC 到 MGR

Q: 如果两台机器同时想要写入同一页怎么办?

Q: 如果一台机器在所有权转移时刚好读取了怎么办?

IVY 是否提供严格一致性?

  • 不:MGR 可能按照与发出时间相反的顺序处理两个 ST

  • 不:ST 可能需要很长时间来撤销其他机器上的读取访问权限

    • 因此 LDs 可能长时间获取到过时的数据,之后才发出 ST

如果没有 IC 消息会怎样?

待办事项: IC 是什么?

  • (这是新问题)

  • 即 MGR 没有等待副本持有者确认?

没有 WC?

待办事项: WC 是什么?

  • (这曾经是问题)

  • 例如,MGR 在将 WF 发送到 M0 后解锁?

  • MGR 将后续的 RF,WF 发送到 M2(新所有者)

  • 如果这样的 WF/RF 在 WD 之前到达 M2 会怎样?

    • 没问题!M2 在获得 WD 之前一直锁定ptable[p].lock
  • RC + info[p].lock防止 RF 被 WF 超越

  • 因此不清楚为什么需要 WC!

    • 但我对这个结论不太有信心

如果没有 RC 消息会怎样?

  • 即 MGR 在发送 RF 后解锁?

  • RF 是否会被后续的 WF 超越?

  • 或者受后续 IV 的限制?

IVY 在哪些情况下表现良好?

  1. 页面被许多机器读取,但没有被写入

  2. 每次只有一台机器写入页面,其他机器根本不使用

IVY 以响应不断变化的使用模式移动页面的方式很酷

例如,页面大小为 4096 字节是好还是坏?

  • 如果有空间局部性,即程序查看大块数据会怎样?

  • 如果程序只在一页中写入少量字节会怎样?

    • 后续��取者为了获取一些新字节而复制整个页面
  • 如果存在虚假共享会很糟糕

    • 即两个不相关的变量在同一页上

      • 并且至少有一个频繁写入
    • 页面将在不同机器之间反弹

      • 即使只读取不变量的用户也会收到失效
    • 即使这些计算机从不使用相同的位置

IVY 的表现如何?

  • 毕竟,目的是通过并行性获得加速

在性能方面,我们能期望什么是最好的?

  • 在 N 台机器上,速度提高 N 倍

什么可能阻止我们获得 N 倍速度提升?

  • 应用程序固有地不可扩展

    • 不能分割成并行活动
  • 应用程序通信的字节数太多

    • 因此,网络阻止了更多机器产生更多性能
  • 对共享页面进行太多小读/写

    • 即使字节数很少,IVY 也会使其变得昂贵

它们的表现如何?

  • 图 4:对于 PDE(偏微分方程)几乎是线性的

  • 图 6:排序对于非常次线性

    • 对一个巨大数组进行排序涉及移动大量数据

    • 几乎肯定会至少将所有数据通过网络传输一次

  • 图 7:矩阵乘法几乎是线性的

  • 一般来说,当读取大量页面时,你最终会受到网络吞吐量的限制

为什么排序表现不佳?

  • 这是我的猜测

  • N 台机器,数据分为 2*N 个分区

  • 第 1 阶段:对于 N 台机器,本地排序 2*N 个分区

  • 第 2 阶段:2N-1 次合并分裂;每轮次将所有数据发送到网络

  • 第 1 阶段可能会获得线性速度提升

  • 第 2 阶段可能不会 -- 受限于局域网速度

    • 同样,更多机器可能意味着更多轮次
  • 因此对于少量机器,本地排序占主导地位,更多机器有助于提高性能

  • 对于大量机器,通信占主导地位,更多机器并不会有所帮助

  • 此外,更多机器从 n*log(n)本地排序转移到 n² 冒泡式排序

如何加快 IVY 的速度?

  • 下一堂课:放宽一致性模型

    • 允许多个写者写入同一页!

论文介绍称 DSM 包含 RPC -- 这是真的吗?

  • 何时 DSM 比 RPC 更好?

    • 更透明。更易编程。
  • 何时 RPC 更好?

    • 隔离。控制通信。容忍延迟。

    • 可移植性。定义自己的语义。

    • 抽象?

  • 在您的 DSM 系统中可能仍然希望使用 RPC 吗? 为了高效的休眠/唤醒?

第 3.1 节伪代码中的已知问题

  • 故障处理程序必须等待所有者发送 p 然后再向管理器确认

  • 如果所有者具有只读页面并且发生写入错误,则会发生死锁

    • 令人担忧的是没有明确的顺序 ptable[p].lockinfo[p].lock

    • 待办事项:嗯?

  • 编写服务器/管理器必须设置 owner = request_node

  • 故障处理程序的管理器部分不会向所有者请求页面

  • 处理失效请求是否持有 ptable[p].lock

    • 可能不行 -- 死锁

一致性

6.824 2015 年第 10 讲:一致性

注意:这些讲座笔记已经从 2015 年春季 6.824 课程网站上发布的笔记略有修改。

今天:一致性

  • 懒惰释放一致性

  • 使用懒惰一致性来提高性能

  • 一致性=并发读写的含义

  • 比看起来不那么明显!

  • 选择在性能和程序员友好性之间进行权衡

    • 许多设计中的重要因素
  • 今天的论文:案例研究

许多系统具有具有并发读写器的存储器/内存

  • 多处理器,数据库,AFS,实验室

  • 您经常希望以可能改变行为的方式进行改进:

    • 添加缓存

    • 分割在多个服务器上

    • 复制以实现容错

  • 我们如何知道一个优化是否正确?

  • 我们需要一种思考分布式程序正确执行的方法

  • 大多数这些想法来自 20/30 年前的多处理器和数据库

我们如何编写正确的共享存储分布式程序?

  • 内存系统承诺按照某些规则行事

  • 我们编写程序假设这些规则

  • 规则是“一致性模型”

  • 内存系统与程序员之间的契约

什么构成了一个良好的一致性模型?

  • 没有“正确”或“错误”的模型

  • 一个模型可能会使编程变得更加困难或更容易

    • 即导致更直观或更模糊的结果
  • 一个模型可能更难或更容易实现高效

  • 同样也取决于应用程序

    • 例如 网页 vs 内存

一些一致性模型:

  • Spanner:外部一致性(表现得像一台机器)

  • 数据库世界:严格的串行化,串行化,快照隔离,读取提交

  • 分布式文件系统:打开到关闭一致性

  • 计算机架构师:TSO(总存储器排序),释放一致性等等。

  • 并发理论:顺序一致性,线性一致性

  • 相似的想法,但有时意义略有不同

DSM 是研究一致性的良好起点

  • 简单的接口:读取和写入内存位置

  • 一致性在架构社区中得到了很好的发展

例如:

 x and y start out = 0
  M0:
    x = 1
    if y == 0:
      print yes
  M1:
    y = 1
    if x == 0:
      print yes
  Can they both print "yes"? 

DSM 的性能受到内存一致性的限制

  • 使用顺序一致性,M0 的写操作必须在 M0 执行读操作之前对 M1 可见

    • 否则 M0 和 M1 都可以读取 0 并打印“是”

    • (第二个“禁止”示例)

  • 因此,在分布式系统中操作将需要一段时间

    • 他们必须逐个完成

Treadmarks 的高级目标?

  • 更好的 DSM 性能

  • 运行现有的并行代码(多线程)

    • 这段代码已经有锁了

    • TreadMarks 将在单独的机器上运行每个线程/进程,并允许其访问 DSM。

    • TreadMarks 利用代码已经使用锁定的优势

他们试图修复以前 DSM 存在的具体问题是什么?

  • 错误共享:两台机器在同一页上读/写不同的变量

    • m1 写 x,m2 写 y

    • m1 写 x,m2 只读 y

    • 问:在这种情况下 IVY 做什么?

    • 答:Ivy 将页面在 x 和 y 之间来回弹跳

  • 写入放大: 1 字节写入变成整页传输。

第一个目标:消除写放大。

  • 不发送整个页面,只发送已写入的字节

大想法:写入差异(以解决写入放大问题)。

例子:

m1 and m2 both have x's page as readable
m1 writes x
            m2 just reads x 
  • 在 M1 写入错误时:

    • 告诉其他主机使其失效,但保留隐藏副本

    • M1 也制作了隐藏副本

    • M1 将页面标记为读/写。

  • 在 M2 读取错误时:

    • M2 请求 M1 的最近修改

    • M1 将当前页面与隐藏副本进行比较“差异”

    • M1 将差异发送给 M2

    • M2 将差异应用于其隐藏的副本。

    • M2 将页面标记为只读

    • M1 将页面标记为只读。

Q: 写入差异是否提供顺序一致性?

  • 最多一个可写副本,因此写入是有序的

  • 在任何副本可读时不写入,因此没有旧的读取

  • 可读副本是最新的,因此没有旧的读取

  • 仍然是顺序一致的。

Q: 写入差异是否有助于解决虚假共享问题?

A: 不,它们有助于减少写入放大。

下一个目标:允许多个读取者+写入者应对虚假共享

  • 我们的解决方案需要允许两台机器写入同一页。

  • => 当一台机器写入时不要使其他机器失效

  • => 不要将写入者降级为只读,当另一台机器读取时。

  • => 页面的多个不同副本!

    • 读者应该查看哪个?
  • 差异有所帮助:可以合并对同一页的写入

  • 但是什么时候发送差异?

    • 没有失效->没有页面故障->什么触发发送差异

...所以我们来到发布一致性

大想法:(渴望的)发布一致性(RC)

  • 再次:什么会触发发送差异?

  • 看起来我们应该在某人读取更改的数据时发送差异。如果我们不会因为没有使其他人的页面失效而获得读取错误,那么我们怎么知道有人正在读取数据?

  • 没有人应该在没有持有锁的情况下读取数据!

    • 所以让我们假设一个锁服务器
  • 在释放(解锁)时发送写入差异

    • 对于所有具有写入页面副本的机器

例子:

lock()
x = 1
unlock() --> diffs all pages, to detect all the writes since
             the last unlock
         --> sends diffs to *all* machines 

Q: 为什么检测自上次解锁()以来的所有写入而不是自上次锁定()以来的所有写入?

A: 请参阅下面的因果一致性讨论。

示例 1(RC 和虚假共享)

x and y are on the same page
ax -- acquire lock x
rx -- release lock x

M0: a1 for(...) x++ r1
M1: a2 for(...) y++ r2  a1 print x, y r1 

RC 做什么?

  • M0 和 M1 都获取缓存的可写副本页面

  • 当它们释放时,每个都会计算相对于原始页面的差异。

  • M1a1导致它等待直到M0r1释放。

    • 所以 M1 将会看到 M0 的写入。

Q: RC 的性能优势是什么?

  • IVY 对示例 1 做了什么?

    • 如果xy在同一页上,页面在M0M1之间反弹
  • 即使有 1 个或更多个写入,多台机器也可以拥有页面的副本

    • => 由于虚假共享而导致页面的反弹

    • => 读取副本可以与写入者共存

Q: RC 是否顺序一致?不是!

  • 在 SC 中,读取看到最新的写入

  • 在 M1 释放锁之前,M1 将不会看到 M0 的写入

    • 即 M1 可能会看到一个旧的 x 的副本,在顺序一致性下是不允许的。
  • 因此机器可以暂时在内存内容上存在分歧

  • 如果您总是锁定:

    • 锁强制顺序->没有旧的读取->像顺序一致性一样

Q: 如果您不锁定会发生什么?

  • 读取可能返回过时的数据

  • 对同一变量的并发写入→麻烦

问: 如果没有写差异,RC 是否有意义?

  • 可能不会:需要差异来协调对同一页的并发写入

大想法:惰性释放一致性(LRC)

  • 一个问题是当我们unlock()时,我们更新了所有人,但并不是每个人都可能需要更改的数据。

  • 只向已释放的锁的下一个获取者发送写差异

    • (即当有人调用lock()并且他们需要数据的更新时)
  • 比 RC 更懒惰的两个方面:

    • 释放什么都不做,所以也许将工作推迟到将来的释放。

    • 只将写差异发送给获取者,而不是每个人。

示例 2(懒惰)

x and y on same page (otherwise IVY avoids copy too)

M0: a1 x=1 r1
M1:           a2 y=1 r2
M2:                     a1 print x,y r1 

LRC 是做什么的?

  • M2 询问锁管理器谁是锁 1 的上一个持有者

    • 它是 M1
  • M2 仅向持有锁 1 的上一个请求者请求写差异。

  • 即使在同一页上,M2 也看不到 M1 对y的修改。

    • 因为它没有使用a2获得锁 2。

RC 做什么?

  • RC 会向所有人广播对xy的所有更改

IVY 是做什么的?

  • IVY 将使页面失效,并确保只有写入者拥有只写副本。

  • 在读取时,写入页变为只读,并由读取者获取数据

问: LRC 带来了什么性能提升?

  • 如果你没有获取对象的锁,你就看不到对它的更新。

  • =>如果你只使用页面上的一些变量,你就看不到对其他变量的写入

  • =>网络流量减少

问: LRC 是否提供与 RC 相同的一致性模型?

  • 不! LRC 隐藏了一些 RC 显示的写入

  • 注意:如果你正确使用锁,那么你不应该注意到(E)RC 和 LRC 之间的差异。

  • 在上面的示例中,RC 向 M2 透露了y=1,而 LRC 没有透露

    • 因为 RC 在释放锁时广播更改。
  • 所以“M2:print x, y”对于 RC 来说可能打印出新鲜的数据,对于 LRC 来说可能是旧的数据。

    • 取决于 print 是在 M1 释放之前还是之后。

问: 如果每个变量在单独的页面上,LRC 是否胜过 IVY?

  • 直到程序试图读取数据时,IVY 才会移动数据

    • 所以 Ivy 已经相当懒了
  • 罗伯特:只有页面很大时,TreadMarks 才值得

  • 还是优于 IVY 加上写差异?

我们认为所有的线程/锁定代码都能与 LRC 一起工作吗?

  • 所有程序都会锁定它们读取的每个共享变量吗?

  • 论文并没有完全说明,但强烈暗示“不行”!

示例 3(因果异常)

M0: a1 x=1 r1
M1:             a1 a2 y=x r2 r1
M2:                               a2 print x, y r2 

这里可能存在的问题是什么?

  • M2 可能看到 y=1 但 x=0 的情况是反直觉的

    • 因为 M2 没有获取锁 1,它无法获得对x的更改。

“因果一致性”的违反:

  • 如果写入 W1 导致写入 W2,每个人在看到 W2 之前都会看到 W1

示例 4(有人认为这是自然代码的论点):

M0: x=7    a1 y=&x r1
M1:                     a1 a2 z=y r2 r1  
M2:                                       a2 print *z r2 

在示例 4 中,M2 是否会从 M1 那里了解到 M0 也做出了对y=&x的贡献的写入,这一点并不清楚。

  • 例如,如果x在被 M0 更改之前是 1,那么当它打印*z时,M2 会看到这一点吗?

TreadMarks 提供因果一致性

  • 当你获取一个锁时,

  • 你看到上一个持有者的所有写入

  • 以及上一个持有者看到的所有写入

如何跟踪哪些写入贡献了写入?

  • 编号每台机器的发布 - “间隔”编号

  • 每台机器跟踪它从其他每台机器看到的最高写入

    • 最高写入 = 该机器知晓的最后一次写入的间隔号

    • “向量时间戳”

  • 用当前 VT 标记每个释放

  • 在获取时,告诉前一个持有者你的 VT

    • 差异指示需要发送哪些写入
  • (注释前面的例子)

  • 什么时候可以丢弃差异?

    • 似乎你需要全局知道每个人对什么了解

    • 请参阅论文中的“垃圾收集”部分

VT 通过不同机器对同一变量的写入进行排序:

M0: a1 x=1 r1  a2 y=9 r2
M1:              a1 x=2 r1
M2:                           a1 a2 z = x + y r2 r1

M2 is going to hear "x=1" from M0, and "x=2" from M1.
TODO: what about y? 

M2 如何知道该做什么?

相同变量的两个值的 VT 是否不能被排序?

M0: a1 x=1 r1
M1:              a2 x=2 r2
M2:                           a1 a2 print x r2 r1 

程序员规则/系统保证的摘要

  1. 每个共享变量都受到某个锁的保护

  2. 在写入共享变量之前锁定以对同一变量的写入进行排序,否则“最新值”定义不清晰

  3. 在读取共享变量之前锁定以获取最新版本

  4. 如果读取没有锁定,保证看到为你锁定的变量做出贡献的值

LRC 可能过度劳累的示例。

M0: a2 z=99 r2  a1 x=1 r1
M1:                            a1 y=x r1 

TreadMarks 将 z 发送到 M1,因为它在 VT 顺序中出现在 x=1 之前。

  • 假设 x 和 z 在同一页上。

  • 即使在不同页面上,M1 也必须使 z 的页面无效。

  • 但 M1 不使用 z。

  • 系统如何理解 z 不需要?

    • 要求锁定你读取的所有数据

    • => 放宽 LRC 模型的因果部分

问: TreadMarks 能否在不使用 VM 页面保护的情况下工作?

  • 它使用 VM 来

    • 检测写入以避免制作隐藏副本(用于差异)如果不需要的话

    • 检测对页面的读取 => 知道是否获取差异

  • 两者都不是真正关键的

  • 因此 TM 不像 IVY 那样依赖 VM

    • IVY 使用 VM 故障来决定何时移动数据

    • TM 使用 acquire()/release() 和差异来实现这一目的

性能如何?

图 3 显示大部分良好的扩展性

  • 这与“好”一样吗?

  • 尽管 Water 明显进行了大量的锁定/共享

它们距离最佳性能有多接近?

  • 或许图 5 暗示只有约 20% 的脂肪需要减少

LRC 是否胜过先前的 DSM 方案?

  • 他们只与自己的稻草人急切释放一致性(ERC)进行比较

    • 不反对已知最佳的先前工作
  • 图 9 表明即使对于 Water 来说也没有太大的优势

DSM 取得成功了吗?

  • 协作机器集群取得了巨大成功

  • DSM 并不那么重要

    • 主要理由是对现有线程化代码的透明性

    • 对于新应用程序来说这并不有趣

    • 透明性使高性能难以实现

  • MapReduce 或消息传递或共享存储比 DSM 更常见

乐观主义、因果性、向量时间戳

6.824 2015 年第 15 讲:乐观主义、因果性、向量时间戳

注意:这些讲座笔记是从 2015 年春季的 6.824 课程网站上稍作修改的。

到目前为止的一致性:

  • 并发迫使我们思考读/写的含义

  • 顺序一致性:每个人都看到相同的读/写顺序(IVY)。

  • 释放一致性:每个人都按解锁顺序看到写入(TreadMarks)。

顺序一致性和释放一致性很慢:

  • 一般情况下,必须在每个操作之前询问。

  • IVY:读故障和写故障->向管理器请求。

  • TreadMarks:获取和释放->向锁管理器请求。

  • 我们是否可以通过降低一致性来获得更好的性能?

Paxos:

  • 同样缓慢;需要几条消息达成一致。

    • 不仅仅是 IVY+TreadMarks
  • 同样,“低”可用性

    • 如果没有多数,就没有进展。
  • 不适合断开连接的操作。

乐观并发控制

  • 现在执行操作(例如,读/写缓存副本)

  • 以后再检查它是否正常。

  • 如果不正常,恢复。

一个简单的例子——乐观的点对点聊天

  • 我们每个人都有一个连接到互联网的计算机。

  • 当我输入东西时,向每个参与者发送消息。

  • 收到消息->添加到聊天窗口的末尾。

图:

m0              m1              m2 
\             /\              /\
 \------------/               /
  \                          /
   \------------------------/ 

我们在聊天中关心消息顺序吗?

  • 网络可能会在不同参与者处以不同顺序传递。

  • 乔:答案是 40。

  • 弗雷德:不,是 41。

  • 爱丽丝:那是正确的

  • 或许山姆看到了不同的顺序:

    • 乔:40

    • 爱丽丝:那是正确的

这个例子出了什么问题?

  • 爱丽丝根据某些输入“计算”了她的消息。

  • 山姆只有在他也看到了这些输入时才能解释。

假设这是一个拍卖聊天程序:

Joe         Fred        Alice

$10 -->
            20
          <-- -->  

                 <-- winner is $20 

如果有第四个人,山姆:

Joe         Fred        Alice               Sam

$10 -->                                   sees $10
            20  
          <-- -->                         does not see $20 

                 <-- winner is $20 -->    sees winner is $20 

因此对山姆来说可能没有意义。他的问题是山姆在发送消息时不知道爱丽丝知道什么。

定义:x在因果上先于y

  • 如果:

    • M0 执行x,然后 M0 执行y

    • M0 执行x,M0 向 M1 发送消息,M1 执行y

  • 传递闭包

  • xy通常是写入、消息或文件版本。

  • 也"y在因果上依赖于x"。

定义:因果一致性

  • 如果x在因果上先于y,则每个人在y之前看到x

优缺点:

  • 优点:没有单一的主节点

  • 缺点:事件上不是总序

因果一致性的实现缓慢。

  • 每条消息都有一个唯一的 ID。

  • 节点保留所有收到的消息 ID 的集合——“历史记录”。

  • 发送m时,也发送当前历史集。

  • 接收者延迟接收到的消息m,直到接收到m集合中的所有内容。

历史集会变得非常庞大——我们能否缩写?

  • 每个节点对其消息进行编号,如 1、2、3 等。

  • 按顺序传递每个节点的消息

  • 那么历史记录只需要包括每个节点看到的最新编号。

    • H1/4 意味着也看到了 1、2、3。
  • 与历史集不同,此记法不会随着时间增长。

  • 称为向量时间戳版本向量

向量时间戳

  • 每个节点都对其自己的操作进行编号(例如发送的消息,在这种情况下)。

  • VT 是一个数字向量,每个节点有一个槽位。

  • 每条消息都附带一个 VT。

  • VT[i]=x => 发送者已看到节点i的所有消息,直到#x

  • 这里的假设是一个节点向所有其他节点广播消息(因为我们正在尝试有效地复制一个系统)

  • 必须知道整个系统中有多少个节点

    • 否则,复杂
  • 当你有数千台机器时,VTs 会变得非常庞大

VT 比较

  • 回答“消息 A 应该在消息 B 之前显示吗?”

  • ab表示与消息AB相关联的 VTs

  • 我们可以推断因果关系(即a < b还是它们是并发的a || b

  • 四种情况:a < b, a || b

  • 如果两个条件成立,则a < b

    1. 对于所有主机i

      • a[i] <= b[i]

        • a总结了b的一个正确前缀

        • 即要么

          • b的发送者和a的发送者都看到了来自主机i的相同数量的消息

          • b的发送者比a的发送者看到了来自主机i的更近的消息

    2. AND 存在j,使得a[j] < b[j]

      • a在因果上先于b

        • b的发送者 绝对 看到了来自主机i的更近的消息,而a的发送者看到的则不是
  • 如果:

    • 属于 i,j 的存在:a[i] < b[i]a[j] > b[j]

    • 即没有一个总结了另一个的前缀

    • 即没有因果关系的一个在先于另一个

      • 这是因为,正如我们之前所说的,没有完全的顺序

许多系统使用 VT 变体,但用途略有不同

  • TreadMarks,Ficus,Bayou,Dynamo 等

  • 简洁地表示“我已经看到了每个人到目前为止的更新”

  • 简洁地同意事件x是否在事件y之前发生

  • 我假装这里有一个基本原则

    • 但只有当你站得足够远时才成立

CBCAST -- "因果广播"协议

  • 通用排序协议,适用于点对点聊天

  • 来自康奈尔大学 Isis 研究项目

  • 关键属性:

    • 将消息以因果顺序交付给各个节点

    • 如果a在因果上先于b,CBCAST 会先交付a

[图表:节点、消息缓冲、VC、聊天应用程序]

 APP         ^
         |      |
    -----|------|-----------
        \ /     |  CBCAST
         .   
    ---------      vector
    | m3    |      clock
    ---------      VT 
    | wait  |
    ---------
    | m1    | 
  • 每个节点保留本地向量时钟,VC

    • VCi[j] = k表示节点i已经看到了来自j的所有消息,直到消息k

    • 总结了应用程序也看到的内容

  • 在节点isend(m)

    • VCi[i] += 1

    • broadcast(m, i, VCi)

  • 在节点jreceive(m, i, mv)

    • j的 CBCAST 库缓冲了消息

    • 仅在以下情况下向应用程序发布:

      • mv <= VCj,除非mv[i] = VCj[i] + 1

      • 即节点j已经看到了在因果上先于m的每个消息VCj[i] = mv[i]

      • 所以消息将反映m的接收

代码:

on receive(message m, node i, timestamp v):
    release when:
        this node's vector clock VT >= v EXCEPT FOR v[i] = VT[i] + 1 

例如:

 All VCs start <0,0,0>
    M0 sends msg1 w/ <1,0,0>
    M1 receives msg1 w/ <1,0,0>
    M1 sends msg2 w/ <1,1,0>
    M2 receives msg2 w/ <1,1,0> -- must delay because don't have msg1
    M2 receives msg1 w/ <1,0,0> -- can process, unblocks other msg 

为什么这么快?

  • 没有中央管理者,没有全局顺序

  • 如果没有因果依赖关系,CBCAST 不会延迟消息

  • 例如:

    • M0 sends <1,0>

    • M1 sends <0,1>

    • 接收方可以按任意顺序交付

因果一致性仍然允许比顺序更多的惊喜

  • 萨姆仍然可以看到:

    • 乔:40

    • 弗雷德:41

    • 鲍勃:42

    • 爱丽丝:那是正确的

  • 她是指 42 还是 41?

  • 因果一致性只表示 Alice 的消息将在之后被交付

    • 所有她在发送时看到的消息
  • 表示它将在所有她之前没看到的消息之前被交付

    • 如果 CBCAST 先呈现x然后是y,这并意味着x必然发生在y之前

TreadMarks 使用 VTs 对不同机器对同一变量的写入进行排序:

 M0: a1 x=1 r1    a2 y=9 r2
  M1:              a1 x=2 r1
  M2:                           a1 a2 z=x+y r2 r1

  Could M2 hear x=2 from M1, then x=1 from M0?
  How does M2 know what to do? 

VTs 经常用于对复制数据进行乐观更新

  • 每个人都有副本,任何人都可以写入

  • 不想要 IVY 风格的 MGR 或锁定:网络延迟,故障

  • 需要同步副本,仅接受“最新”的数据,检测冲突

  • 文件同步(Ficus,Coda,Rumor)

  • 分布式数据库(Amazon Dynamo,Voldemort,Riak)

文件同步--例如 Ficus

  • 多台计算机都有所有文件的副本

  • 每个主机都可以修改其本地副本

  • 合并更改后--乐观

  • 支持断开操作的文件同步

    • 两个人在两架不同的飞机上编辑同一个文件 😃

    • 当它们重新联机时,服务器需要检测到这一点

    • ...并解决它

    • ...并不会丢失更新(懒惰的服务器可以简单地丢弃一组更改)

情景:

  • 用户在工作、家中、笔记本电脑上都有文件的副本

  • 主机可能关闭,飞机上,等等--不总是在互联网上

  • H1 上工作一段时间,将更改同步到 H2

  • H2 上工作,将更改同步到 H3

  • H3 上工作,同步到 H1

  • 总体目标: 推动更改以保持机器的一致性

约束:不丢失更新

  • 只有在同步将版本 x2 复制到版本 x1 时才可以

    • x2 包括所有在 x1 中的更新。

示例 1:

 Focus on a single file

  H1: f=1 \----------\
  H2:      \->  f=2   \               /--> ???
  H3:                  \-> tell H2 --/

  What is the right thing to do?
  Is it enough to simply take file with latest modification time?
  Yes in this case, as long as you carry them along correctly.
    I.e. H3 remembers mtime assigned by H1, not mtime of sync. 

示例 2:

 mtime = 10 | mtime = 20 | mtime = 25

  H1: f=1 --\       f=2              /-->
  H2:        \-->             f=0 --/
  H3: 

  H2's mtime will be bigger.

  Should the file synchronizer use "0" and discard "2"?
    No! They were conflicting changes. We need to detect this case.
    Modification times are not enough by themselves 

如果存在并发更新怎么办?

  • 以便两个版本都不包含另一个的更新?

  • 复制将会丢失其中一个更新

  • 因此同步不会复制,宣布“冲突”

  • 冲突是乐观写入的必然结果

如何确定一个版本是否包含另一个版本的所有更新?

  • 我们可以记录每个文件的整个修改历史。

  • 主机名/本地时间对的列表。

  • 在主机之间同步时,同时携带历史记录。

  • 例如 1:H2: H1/T1,H2/T2 H3: H1/T1

  • 例如 2:H1: H1/T1,H1/T2 H2: H1/T1,H2/T3

  • 那么很容易确定版本 x 是否包含版本 y 的所有更新:

    • 如果 y 的历史是 x 的历史的前缀。

我们可以使用 VTs 来压缩这些历史记录!

  • 每个主机记住每个文件的一个 VT

  • 为每个主机的文件写入编号(或分配墙钟时间)

  • 只需记住每个主机上最后一次写入的数量

  • VT[i]=x => 文件版本包括主机 i 的所有更新直到 #x

示例 1 的 VTs:

  • 在 H1 的更改后:v1=<1,0,0>

  • 在 H2 的更改后:v2=<1,1,0>

  • v1 < v2,所以 H2 忽略 H3 的副本(因为 < 所以没有冲突)

  • v2 > v1,所以 H1/H3 将接受 H2 的副本(再次没有冲突)

示例 2 的 VTs:

  • 在 H1 的第一次更改后:v1=<1,0,0>

  • 在 H1 的第二次更改后:v2=<2,0,0>

  • 在 H2 的更改后:v3=<1,1,0>

  • v3 既不 < 也不 > v1

    • 因此两者都没有看到对方的所有更新

    • 因此存在冲突

如果存在冲突更新怎么办?

  • VTs 可以检测到它们,但接下来呢?

  • 取决于应用程序。

  • 简单难度: 具有不同不可变消息的邮箱文件,只需合并。

  • 中等难度: 对 C 源文件的不同行进行更改(diff+patch)。

  • 困难难度: 对 C 源代码的同一行进行更改。

  • 对于困难情况,必须手动进行调和。

  • 今天的论文都是关于解决冲突

如何考虑文件同步的 VTs?

  • 它们会检测是否存在版本的序列顺序

  • 即。当我修改文件时,我是否已经看到了你的修改?

    • 如果是的话,没有冲突

    • 如果没有,则冲突

  • 或:

    • VT(向量时戳)总结了文件的完整版本历史

    • 如果您的版本是我的版本的前缀,则不会冲突

针对文件删除的情况怎么办?

  • 如果删除文件,H1 可以忘记文件的 VT 吗?

    • 不:当 H1 与 H2 同步时,它会看起来像 H2 有一个新文件。
  • H1 必须记住已删除文件的 VT。

  • 将删除视为文件修改。

    • H1: f=1 ->H2

    • H2: del ->H1

    • 第二次同步看到 H1:<1,0> H2<1,1>,因此在 H1 处删除胜出

  • 可能会出现删除/写入冲突

    • H1: f=1 ->H2 f=2

    • H2: del ->H1

    • H1:<2,0> vs H2:<1,1> -- 冲突

    • 在 H1 删除是否可以?

如何删除已删除文件的 VTs?

等待所有主机都看到删除消息足够吗?

  • 同步将携带,对于已删除的文件,已看到删除的主机集合

“等待所有人都看到删除”行不通:

  • H1: ->H3 forget

  • H2: f=1 ->H1,H3 del,seen ->H1 ->H1

  • H3: seen ->H1

  • H2 需要重新告诉 H1 有关 f,删除和 f 的 VT

    • H2 不知道 H3 已看到删除

    • 因此 H3 可能会与 H1 同步,然后告诉 H1 f 的情况

    • H1 消失并重新出现是违法的

  • 因此 -- 此方案不能可靠地让主机忘记

图表:

 | Phase 1              | Phase 2               | Phase 3 (forget f's VT)
H1: del f  \     | seen f  -\->         | done f  -\->          |
H2:         \--> | seen f  -/-> (bcast) | done f  -/-> (bcast)  |
H3:         |--> | seen f  -\->         | done f  -\->          | 

来自 Ficus 复制文件系统的工作 VT GC 方案

  • 第 1 阶段:累积已看到删除的节点集

    • 当 == 所有节点的完整集合时终止
  • 第 2 阶段:累积已完成第 1 阶段的节点集

    • 当 == 所有节点时,可以完全忘记文件
  • 如果 H1 然后与 H2 同步,

    • H2 必须处于第 2 阶段,或已完成第 2 阶段

    • 如果处于第 2 阶段,H2 知道 H1 曾看到过删除,所以不需要告诉 H1 关于文件的情况

    • 如果 H2 已完成第 2 阶段,它也不知道文件

VTs 的一个经典问题:

  • 许多主机 -> 大 VTs

  • VT 很容易比数据大!

  • 没有非常令人满意的解决方案

许多文件同步器不使用 VTs -- 如 Unison,rsync

  • 如果只有两个当事方,或者星形,文件修改时间足够

  • 需要记住“自上次同步以来已修改”

  • 如果您想要 > 2 个主机之间的任何到任何同步,则需要 VTs

摘要

  • 复制 + 乐观更新以提高速度,高可用性

  • 因果一致性产生乐观更新的合理顺序(CBCAST)

  • 因果排序检测冲突更新

  • 向量时间戳简洁地总结了更新历史

最终一致性

6.824 2015 年第 12 讲:最终一致性

注意:这些讲义与 2015 年春季 6.824 课程网站上发布的讲义略有修改。

考试

  • 为考试带上论文和讲义

Bayou:最终一致性

  • 一组数据的副本,应用程序可以使用数据的任何副本

  • 本地读/写

  • 即使网络中断,我仍然可以使用本地副本

    • 断开操作
  • 临时同步

    • 笔记本电脑、手机、平板电脑可以在彼此之间同步,而不是依赖于互联网连接
  • 可以与具有不同数据的数据库服务器一起工作,并相互同步

  • 类似于 Ficus,但 Bayou 具有更复杂的冲突解决方法

冲突

  • 如何处理允许人们写入本地副本并稍后同步它们时发生的不可避免的冲突

会议室调度器

传统方法(中央服务器):

 PDA
|-----------------
|9am    824 staff     |----------------|
|--                   |  Server        |
|10am        -------------> | DB   |   |
|--                         | 9am  |   |
|11am                       | 10am |   |
|--                                    |
|12pm                                  |
|-- 

这不是一个好方法,因为它要求每个人都与服务器连接。

如果您能让 PDA 发送约会给笔记本电脑,然后再发送给服务器,那就太好了。

 PDA
|-----------------
|9am    824 staff     |----------------|
|--                   |  Server        |
|10am                       | DB   |   |
|--                         | 9am  |   | <-----\
|11am                       | 10am |   |        \
|--                                    |         |
|12pm                                  |      laptop
|--      \                                      /
          \----------------------------------->/ 

更新函数

主要思想:更新函数。 应用程序不再说“写入此数据库记录”,而是将函数交给根据数据库内容而表现不同的应用程序。

例如:

  • 如果 10 点有空

    • 保留 @10am
  • 否则如果 9 点有空

    • 保留 @9am
  • 否则

    • 保留

Bayou 将此功能从 PDA 获取并交给笔记本电脑。

假设 A 和 B 希望在同一时间:

  • A 希望:10 点或 11 点的员工会议

  • B 希望:10 点或 11 点的招聘会议

如果您仅将这些函数应用于节点 A 和 B 的数据库,这是不够的:

  • X 与 A 同步:X 得到 10 点的员工会议

  • X 与 B 同步:X 得到 11 点的招聘会议

  • Y 与 B 同步:Y 得到 10 点的招聘会议

  • Y 与 A 同步:Y 得到 11 点的员工会议

  • 糟糕:现在 X 和 Y 有不同的观点

=>必须以相同的顺序执行AB的更新函数

编号更新

下一个想法:编号更新函数,以便您可以将它们视为日志

  • 对事物排序的经典方法是用数字和排序标记它们

  • 最初让 Bayou 更新 ID 为<时间 T,节点 ID>

    • 可能对于时间T来说,两个更新 ID 相同,但节点 ID 将不同(据推测)
  • 排序规则:

    • 如果a.T < b.Ta.T == b.T and a.ID < b.ID,则a < b

如果我们以前的例子为例:

<T=10, nodeId=A>, A wants: either staff meeting at 10  or 11
<T=20, nodeId=B>, B wants: hiring meeting at 10 or 11 
  • 当 Y 与 B 同步然后与 A 同步时,它将看到 A 的更新发生得更早

  • 所以它撤销 B 的更新,应用 A 的更新,然后再次应用 B 的更新

我们需要能够回滚并重新执行日志。

更新是否符合因果关系?

  • PDA A 添加了一个会议

  • A 与 B 同步

  • B 删除了 A 的会议

如果第三个节点看到这些更新,有必要让会议创建时间戳小于删除时间戳。

Lamport 逻辑时钟

每个节点维护T_max,这个节点从自身或其他节点看到的最高时间戳。

当节点创建事件并将其添加到日志中时,它选择时间戳T = max(T_max + 1,壁钟时间)

  • 新时间戳始终比节点曾经看到的时间戳要高

临时条目,提交方案

令人讨厌的是,日历中的条目总是显示为临时的,因为可能会有另一个(更早的)更新进来并替换它。

  • 也许是因为新的更新发送者长时间断开连接了

我们正在寻找一种方式,让大家都同意日志中某一点以上的任何内容都永远不会改变(它被冻结,没有人可以修改那里的东西)

不好的想法:一种可能性是让所有复制品互相交换关于它们所见内容的摘要:

  • X 已经看到了 A 的更新到 20,B 的更新到 17,C 的更新到 72

    • 这些是时间戳(逻辑时钟)
  • 我们知道 X 永远不会创建小于 72 的时间戳

  • 同样,节点 Y 也有他将生成的最小时间戳

    • 说 30
  • 我们可以对所有这些最小值取最小值min(30,72)= 30并提交到该点的所有操作

  • 问题是它需要每个节点都处于启动状态并连接到所有其他节点

Bayou 的提交方案

他们有一个神奇的节点,一个主节点。每个通过主节点的更新,主节点都会用提交序列号(CSN)给它盖上时间戳,实际排序号变为:<csn,T,节点 ID>

  • 主节点不会等待较早的更新(带有较小的T)到达,它只是按照它们到来的顺序给事物打上时间戳。

  • 提交保留因果顺序

  • 提交不保留壁钟顺序

如果您没有 CSN:<-, T,nodeID>,则所有提交的操作都被认为在未提交的操作之前发生。

TODO:不清楚这个例子应该展示什么

  • A 的会议已创建

  • B 的会议已创建

  • B 与 C 同步

  • B 与 A 同步

  • C 与主节点同步

  • 主节点将 CSN 应用于 A 的操作,但不是 B 的

  • B 与主节点同步

向量时间戳

同步

  • A 有

    • <-, 10,X>

    • <-, 20,Y>

    • <-, 30,X>

    • <-, 40,X>

  • B 有

    • <-, 10,X>

    • <-, 20,Y>

    • <-, 30,X>

  • A 与 B 同步

    • 发送版本向量给 B,描述它从每个节点收到的更新。

      • A:[X 40,Y 20]

      • (记住时间戳始终由发送者增加)

      • B:[X 30,Y 20]

      • 如果 B 将 A 的 VT 与他的比较,他会注意到他需要在时间戳 30 到 40 之间由 X 更新的内容

一个新节点加入

现在一些 VT 将有一些新节点 Z 的条目。例如,在前面的示例中

  • A 可以发送[X 40,Y 20,Z 60]给 B

我们还需要一种删除节点的方法。

但是 B 不知道Z是新增加的还是新删除的?

  • Z 加入系统

  • Z 与 X 交谈

  • X 生成 Z 的唯一节点 ID

    • Z 的 ID =<Tz,X 的节点 ID>,其中 Tz 是 Z 与 X 通话的时间
  • X 发送一个时间戳为<-, Tz,X>的更新,说“新服务器 z”

    • 每个人都会在看到 Z 的更新之前看到这个

      • Z 的更新的时间戳高于 Tz
    • 请注意,ID 的大小不受限制

忘记节点:

  • Z 的 ID =<20,X>

  • A 同步-> B

  • A 有来自 Z 的日志条目<-, 25,<20,X>>

  • B 没有 Z 的 VT 条目

现在 B 需要从 A 的更新中弄清楚 Z 是否被添加或移除

Case 1: 如果 B 对于 X 的 VT 条目比 Z 的 ID 中的时间戳要小,则意味着 B 甚至还没有看到 Z 的创建,更不用说来自 Z 的任何更新了 => B 应该为 Z 创建条目,因为对于 B 来说,Z 是新的

Case 2: 如果 B 对于 X 的 VT 条目高于 Z 的 ID 中的时间戳(即 B 在创建 Z 后看到了来自 X 的更新),那么 B 必须已经看到了 Z 的创建 => B 必须已经看到了删除通知

问: 如果 B 中缺少 Z 的条目,则 Z (可能?)会说 <-, T, Z> 再见, T > Tz


6.824 记录

Bayou 中的管理更新冲突,一个弱连接的复制存储系统 Terry, Theimer, Petersen, Demers, Spreitzer, Hauser, SOSP 95

一些来自 灵活的弱一致性复制中的更新传播,SOSP 97 的材料

为什么选择这篇论文?

  • 最终一致性是非常普遍的

    • git、iPhone 同步、Dropbox、亚马逊 Dynamo
  • 为什么人们喜欢最终一致性?

    • 本地副本的快速读写(没有主副本,没有 Paxos)

    • 断开操作

  • 有什么问题?

    • 看起来不像 "单一副本"(没有主副本,没有 Paxos)

    • 写入不同副本的冲突写入

    • 发现时如何调和它们?

  • Bayou 有最复杂的调和故事

论文背景:

  • 早期的 1990 年代(像 Ficus 一样)

  • PDA、笔记本电脑、平板电脑的黎明

    • 硬件笨重但有明显的潜力

    • 商业设备没有无线功能

  • 设备可能关闭或没有网络访问

    • 这个问题还没有消失!

    • iPhone 同步、Dropbox 同步、Dynamo

让我们建立一个会议室调度程序

  • 一次只允许一个会议(一个房间)。

  • 每个条目都有一个时间和描述。

  • 我们希望每个人最终看到相同的条目集。

传统方法:一个服务器

  • 服务器一次执行一个客户端请求

  • 检查冲突时间,回答是或否

  • 更新数据库

  • 继续下一个请求

  • 服务器隐式地为并发请求选择顺序

为什么我们不满足于中央服务器?

  • 我想在断开连接的 iPhone 上使用日程安排器等

    • 因此每个节点都需要数据库副本。

    • 在任何节点上修改,以及读取。

  • 定期连接到网络。

  • 定期直接与其他日历用户联系(例如蓝牙)。

草人 1:合并数据库

  • 类似于 iPhone 日历同步,或文件同步。

  • 可能需要比较每个数据库条目 -- 大量的时间和网络带宽。

  • 仍然需要处理冲突条目的故事,即同时发生两个会议。

    • 用户可能在数据库合并时不可用于做出决定。

    • 因此需要自动调和。

冲突的想法:更新函数

  • 应用程序提供一个函数,而不是一个新值。

  • 读取数据库的当前状态,确定最佳变更。

  • 例如 "如果 9 点有空房间,则在 9 点见面,否则在 10 点,否则在 11 点。"

    • 而不只是 "9 点见面"
  • 函数可以为缺席用户做出调和决策。

  • 同步交换函数,而不是数据库内容。

问题: 不能只将更新函数应用于数据库副本

  • A 的函数:10:00 或 11:00 开员工会议

  • B 的功能:在 10:00 或 11:00 举行面试。

  • X 同步与 A,然后 B

  • Y 同步与 B,然后 A

  • X 会将 A 的会议安排在 10:00,而 Y 则将 A 的安排在 11:00?

目标: 最终一致性

  • X 和 Y 最初的不一致是可以接受的

  • 但是经过足够的同步,所有节点的数据库应该是相同的

思路: 有序的更新日志

  • 每个节点的有序更新日志。

  • 同步 == 确保两个节点在日志中具有相同的更新。

  • 数据库是按顺序应用更新函数的结果。

  • 相同的日志 => 相同的顺序 => 相同的数据库内容。

节点如何在更新顺序上达成一致?

  • 更新 ID:<时间 T,节点 ID>

  • T 是创建节点的挂钟时间。

  • 排序更新 a 和 b:

    • 如果 a.T < b.T 或者(a.T = b.Ta.ID < b.ID),则 a < b

例子:

 <10,A>: staff meeting at 10:00 or 11:00
 <20,B>: hiring meeting at 10:00 or 11:00

 what's the correct eventual outcome?
   the result of executing update functions in timestamp order
   staff at 10:00, hiring at 11:00 

同步之前的数据库内容是什么?

  • A:10:00 的员工

  • B:10:00 的招聘

  • 这是 A/B 用户在同步之前将看到的内容。

现在 A 和 B 互相同步

  • 每个节点将新条目排序到其日志中,按时间戳排序

  • 现在两者都知道了完整的更新集

  • A 可以直接运行 B 的更新函数

  • 但是 B 已经 先前 运行了 B 的操作,太早了!

回滚和重播

  • B 需要“回滚”数据库,按正确的顺序重新运行两个操作。

  • 重要观点:日志才是真相;数据库只是一种优化。

  • 我们稍后将优化回滚

显示的会议室日历条目是“暂定”的

  • B 的用户看到了 10 点的招聘,然后它变成了 11 点

更新顺序是否与挂钟时间一致?

  • 也许 A 先行(按挂钟时间)用 <10,A>

  • 节点时钟不太可能完全同步

  • 因此,B 可以生成 <9,b>

  • B 的会议优先级更高,即使 A 先发出请求

  • 不是“外部一致的”

更新顺序是否与因果一致性一致?

  • 如果 A 添加了一个会议,

    • 然后 B 看到了 A 的会议,

    • 然后 B 删除了 A 的会议。

  • 或许

    • <10,A> 添加

    • <9,B> 删除 —— B 的时钟慢了

  • 现在删除将在添加之前排序!

Lamport 逻辑时钟用于因果一致性

  • 想要给事件时间戳,例如:

    • 如果节点观察到 E1,然后生成 E2,那么 TS(E2) > TS(E1)
  • 因此,所有节点都将按顺序排序 E1,然后 E2

  • Tmax = 从任何节点(包括自己)看到的最高时间戳

  • T = max(Tmax + 1, 挂钟时间) —— 生成时间戳

  • 注意属性:

    • 在同一节点上 E1 然后 E2 => TS(E1) < TS(E2)

    • 但是

    • TS(E1) < TS(E2) 不意味着 E1 在 E2 之前发生

逻辑时钟解决了添加/删除因果关系的示例。

  • 当 B 看到 <10,A> 时,

    • B 将其 Tmax 设置为 10,所以

    • B 将为其删除生成 <11,B>

令人恼火的是始终可能存在时间较长的具有较低时间戳的更新

  • 这可能导致我的更新结果发生变化

    • 用户永远无法确定会议时间是否最终确定!
  • 如果更新最终“稳定”,那就好了

    • => 在那一点之前的更新顺序没有变化

    • => 结果永远不会再次更改 —— 你确切地知道何时开会。

    • => 不必回滚,重新运行已提交的更新

糟糕的想法: 完全分散的“提交”方案

  • 建议:如果所有节点都看到了所有时间戳小于等于 10 的更新,则 <10,A> 是稳定的。

  • 让同步始终按日志顺序发送 —— “前缀属性”

  • 如果你已经看到了来自每个节点的 TS > 10 的更新

    • 那么你将永远不会再���到一个 < <10,A>

    • 所以 <10,A> 是稳定的。

  • 为什么 Bayou 不这样做?

    • 并非所有节点都互相连接。

Bayou 如何提交更新,使其稳定?

  • 一个节点被指定为“主复制品”。

  • 它用永久 CSN 标记收到的每个更新。

    • 提交顺序号。

    • 那个更新已经提交。

    • 因此一个完整的时间戳是 <CSN, 本地时间, 节点 ID>

    • 未提交的更新(被认为)在所有已提交的更新之后使用这个新的时间戳方案

  • CSN 通知在节点之间同步。

  • CSN 为已提交的更新定义了一个总顺序。

    • 所有节点最终都会同意它。

提交顺序会匹配临时顺序吗?

  • 经常。

  • 同步按日志顺序发送(前缀属性)

    • 包括从其他节点学到的更新。
  • 所以如果 A 的更新日志说

    • <-,10,X>

    • <-,20,A>

  • A 将按照那个顺序将两者发送给主复制品

    • 主复制品将按照那个顺序分配 CSN

    • 在这种情况下,提交顺序将匹配临时顺序

提交顺序是否总是匹配临时顺序?

  • 不:主复制品可能在看到较旧的更新之前看到更新。

  • A 刚刚有:<-,10,A> W1

  • B 刚刚有:<-,20,B> W2

  • 如果 C 看到了 W1 W2 的更新

  • B 与主复制品同步,得到 CSN=5

  • 稍后 A 与主复制品同步,得到 CSN=6

  • 当 C 与主复制品同步时,它的顺序将变为 W2 W1

    • <5,20,B> W1

    • <6,10,A> W2

  • 因此:提交可能会改变顺序。

提交允许应用程序告诉用户哪些日历条目是稳定的。

  • 稳定的会议室时间是最终的。

节点可以丢弃已提交的更新。

  • 相反,保留一个截至最高已知 CSN 的数据库副本

  • 在重放临时更新日志时回滚到该数据库

  • 永远不需要再回滚更远

    • 前缀属性保证看到 CSN=x => 看到 CSN<x

    • 在已提交的更新中不会有更新顺序的更改

如果我丢弃了日志的一部分,我该如何同步?

  • 假设我已经丢弃了所有带有 CSN 的更新。

  • 我保留一个反映刚刚丢弃条目的稳定数据库副本。

  • 当我传播给节点 X 时:

    • 如果节点 X 的最高 CSN 小于我的,

      • 我可以发送给他我的稳定数据库,反映刚刚提交的更新。

      • 节点 X 可以使用我的数据库作为起点。

      • 并且 X 可以丢弃所有 CSN 日志条目。

      • 然后将他的临时更新播放到该数据库中。

    • 如果节点 X 的最高 CSN 大于我的,

      • X 不需要我的数据库。
  • 在实践中,Bayou 节点保留最近的几个已提交的更新。

    • 为了减少在同步期间必须发送整个数据库的机会。

如何同步?

  • A 发送给 B

  • 需要一种快速的方式让 B 告诉 A 发送什么

  • 已提交的更新很容易:B 将其 CSN 发送给 A

  • 那么临时更新呢?

  • A 有:<-,10,X> <-,20,Y> <-,30,X> <-,40,X>

  • B 有:<-,10,X> <-,20,Y> <-,30,X>

  • 在同步开始时,B 告诉 A “X 30, Y 20”

    • 同步前缀属性意味着 B 在 30 之前拥有所有 X 的更新,在 20 之前拥有所有 Y 的更新。
  • A 在 <-,30,X> 之后发送所有 X 的更新,<-,20,X> 之后发送所有 Y 的更新,等等

  • 这是一个版本向量 -- 它总结了日志内容

    • 这是图 4 中的“F”向量

    • A 的 F:[X:40,Y:20]

    • B 的 F:[X:30,Y:20]

我们如何应对新服务器 Z 加入系统?

  • 它可以开始生成写操作,例如 <-,1,Z> 吗?

  • 其他节点是否只是开始在 VVs 中包含 Z?

  • 如果 A 同步到 B,A 有 <-,10,Z>,但 B 的 VV 中没有 Z

    • A 应该假装 B 的 VV 是 [Z:0,...]

当 Z 退休(离开系统)时会发生什么?

  • 我们希望停止在 VVs 中包含 Z!

  • 如何宣布 Z 已经离开?

    • Z 发送更新 <-,?,Z> "退休"
  • 如果您看到一个退休更新,请在 VV 中省略 Z

  • 如何处理缺少 Z 的 VV?

  • 如果 A 有来自 Z 的日志条目,但 B 的 VV 没有 Z 条目:

    • 例如 A 有 <-,25,Z>,B 的 VV 只是 [A:20, B:21]

    • 或许 Z 已经退休,B 知道,A 不知道

    • 或许 Z 是新的,A 知道,B 不知道

  • 需要一种方式来区分:VV 中缺少 Z 是因为新的还是因为退休?

Bayou 的退休计划

  • Z 通过联系某个服务器 X 加入

  • Z 的 ID 由 X 生成为 <Tz,X>

    • Tz 是 X 加入时的逻辑时钟

    • 注意:无限制的 ID 大小

  • X 发出 <-,Tz,X> "新服务器 Z"

ID=<Tz,X> 方案如何帮助区分新的与被遗忘的?

  • 假设 Z 的 ID 是 <20,X>

  • A 同步到 B

    • A 有来自 Z <-,25,<20,X>> 的日志条目

    • B 的 VV 中没有 Z 条目

  • 一种情况:

    • B 的 VV:[X:10, ...]

    • 10 < 20 意味着 B 尚未看到 X 的 "新服务器 Z" 更新

  • 另一种情况:

    • B 的 VV:[X:30, ...]

    • 20 < 30 意味着 B 曾经知道 Z,但后来看到了一个退休更新

让我们退一步。

最终一致性是一个有用的概念吗?

  • 是的:人们希望将快速写入本地副本

  • iPhone 同步,Dropbox,Dynamo,Riak,Cassandra,等等

更新冲突是一个真正的问题吗?

  • 是的 - 所有系统都有或多或少笨拙的解决方案

Bayou 的复杂性是否合理?

  • 即更新函数的日志、版本向量、暂定操作

  • 只有在您需要点对点同步时才是关键的

    • 即断开连接的操作和临时连接性
  • 只有在人类是数据的主要使用者时才能容忍

  • 否则,您可以通过中央服务器进行同步(iPhone,Dropbox)

  • 还是在本地读取但通过主服务器发送更新(PNUTS、Spanner)?

但有一些 Bayou 可以学习的好主意

  • 用于自动应用驱动冲突解决的更新函数

  • 有序更新日志是真相,而不是数据库

  • 因果一致性的逻辑时钟

MapReduce

6.824 2015 年讲座 13:MapReduce

注意:这些讲义笔记是从 2015 年春季的 6.824课程网站上略微修改的。

介绍

  • 第二次查看这篇论文,更多地讨论容错。

    • 参见第一讲
  • 程序员的真正胜利是简单性

  • 巧妙的设计技巧以获得良好的性能。

例如:构建倒排索引

  • 你需要一个倒排索引来进行搜索索引

  • 将关键字映射到它们所在的文档

例如:

doc 31: I am Alex
doc 32: Alex at 8 am 

我们想要的输出是一个索引:对于输入中的每个单词,我们希望得到该单词出现的每个地方的列表(文档+偏移量):

alex: 31/2, 32/0 ...
am:   31/1, 32/3 ... 

用于构建倒排索引的实际映射/减少函数:

map(doc file)
    split doc into words
    for each word
        emit(word, {doc #, offset})

reduce(word string, occurrences list<doc #, offset>)
    emit(word, sorted list of ocurrences by doc # and then by offset) 

输入文件

在 MapReduce 中,输入存储在 GFS(Google 的文件系统)中

Input, M splits, one    R reduce tasks
map function for each 
split

---------               ---------------------------------
|       |               |   |   | * |           |   |   |
---------               ----------|----------------------
|       |               |   |   | * |           |   |   |
---------               ----------|----------------------
|       |               |   |   | * |           |   |   |
---------               ----------|----------------------
|       |               .   .   . |  
---------               .   .   .  \
|       |               .   .   .   \-----> data for a single reduce task
---------               .   .   .           is all the data in the column
|       |               .   .   .
---------               .   .   .
|       |               .   .   .
---------               .   .   . 

如果一个减少工作者的数据列不适合内存会发生什么?似乎会写入磁盘。

请注意,对于每个唯一的关键字,都会发生一次单独的减少调用。因此,在我们的倒排索引示例中,这意味着对于关键字“the”,可能会出现一亿次在大量文档中。因此,这会花一些时间。MapReduce 无法并行处理减少调用中的工作(对于某些可组合的减少函数(比如 f(reduce(k, l1), reduce(k, l2)) = reduce(k, l1+l2))来说是一个失去的机会)。

  • 我认为论文中提到的组合器函数可以缓解这个问题。

性能

  • 一切都是关于数据移动的

    • 在集群中推送几千兆字节的数据

    • 1000 台机器

      • 可能可以将数据推送到 RAM(1GB/s)以达到 1000GB/s

      • 可能可以将数据推送到磁盘(100MB/s)以达到 100GB/s

      • 网络在一台机器上可以以 1Gbit/s = 100MB/s 的速度运行。

        • 对于 1000 台机器,布线是昂贵的,并且会降低速度

        • 网络通常是一棵树,叶子节点上有服务器,内部节点上有更大的交换机。

        • 瓶颈是根交换机,在谷歌以 18GB/s 运行

      • 因此,网络只能以 18GB/s 的速度推送数据=> 瓶颈

设计见解

需要应对网络问题。

分布式共享内存(DSM)在任何机器都可以在(分布式)内存中写入内存方面非常灵活。问题在于,你最终会得到非常低效的带宽和延迟敏感的系统。如果允许对数据进行任意读/写,则最终会出现一堆跨网络的延迟敏感的小数据移动。

当单台机器死机时,DSM 使容错变得非常困难,因为每台机器都可以做任何想做的事情(读取或写入任何内存位置),因此很难对系统进行检查点。

关键点:

  • Map()Reduce() 仅在本地数据上运行。

  • Map()Reduce() 只对大块数据进行操作

    • 分摊发送网络成本
  • 系统各部分之间的交互非常少

    • 映射无法相互通信。

    • 减少调用不能相互通信。

    • 映射和减少调用无法相互通信

      • 除了将映射数据发送到减少函数的隐式通信之外,减少函数无法相互通信。
  • 给程序员抽象控制网络通信

    • 一些控制如何将键映射到减少分区

输入通常存储为条纹(64MB 块)在 GFS 中,分布在许多磁盘和机器上。

  • 必须要聪明,因为这将意味着 Map 任务受限于网络带宽

MapReduce 利用 GFS 的知识,实际上在存储文件块的 GFS 机器上本地运行映射任务。=>将带宽从 maps 增加到 18GB/s 到 100GB/s

map 生成的中间映射文件也会被存储在本地。缺点是数据只有一份副本存储在那台机器上,减少工作者只能与它进行通信=>带宽有限。

  • 如果机器停止或崩溃,数据将丢失,必须重新启动映射

GFS 中的数据实际上是复制的(2 或 3 个副本),这给 MapReduce 提供了一个可以在每个映射任务上运行的选择 2-3 台服务器。

  • 对负载平衡有利(MR 主节点可以将慢速映射任务移动到其他机器上)

    • 减少任务无法获得此优势

减少的输出存储在 GFS 中=>减少的输出通过网络写入。=>如果这是你的横截面带宽,MapReduce 系统的总输出为 18GB/s。

QOTD

减少在映射发出一些数据后多快可以开始?

Morris:只要一列数据填满了<=>只要所有映射完成。

显然,只要一个映射任务发出关键字,就可以通过在减少任务的迭代器中生成值来进行,但在这种情况下性能可能很难实现。

MapReduce 的扩展性好吗?

分布式系统的一个重要好处是,通过购买更多的机器,你可能能够加速它。购买机器比支付程序员更便宜。

nx硬件 => nx性能? n > 1

随着机器数量的增长(增加 10 倍),输入大小保持不变=>输入大小必须减少(减少 10 倍)。更小的切分(减小 10 倍)。

如果我们有数百万台机器,切分的大小可能是几千字节=>网络延迟会影响我们的性能。

你不能比你有的键更多的减少工作者。

可扩展性受限于

  • 映射切分大小

  • 键的数量>=减少工作者的数量

  • 网络带宽(随着购买更多机器,需要购买更多的“网络”)

    • 一个非常重要的问题

答案:肯定会获得一些扩展,但不是无限的(受网络限制)

容错性

挑战:如果在数千台计算机上运行大型作业,你肯定会遇到一些故障。因此不能简单地重新启动整个计算。必须只重做失败的机器的工作。

对于 DSM 难以实现,对于 MapReduce 较容易。

假设独立故障(也因为映射/减少是独立的)

如果工作者失败:

  • 可以重新启动

  • 可以保存中间输出并在故障后恢复

如果映射失败,我们必须重新运行它,因为它将其输出存储在同一台机器上,这已完成。主节点知道映射正在处理的内容,因此可以重新启动。

如果 reduce worker 崩溃,因为他们的输出存储在 GFS 上,分布在不同的服务器上。我们有很大的机会不需要重新计算,如果 reduce worker 已经完成了。

论文的性能评估。

论文中的图 2。为什么带宽需要 60 秒才能达到 30GB/s?

MR 作业有 1800 个 mappers,还有一些可怜的 master 需要给每个 mapper 分配工作。所以也许 master 联系每个人需要一段时间。

为什么只有 30GB/s?这些是 map 任务,所以没有网络开销。也许 CPU 是瓶颈?不太可能。看起来像是磁盘带宽问题。30GB/s / 1800 台机器 => 每个磁盘 17MB/s。

论文中的图 3。对 1TB 的数据进行排序需要 800 秒 => 1.25GB/s 的排序吞吐量。

需要注意的一件事是 1800 台机器的内存足以容纳一 terrabyte 的数据。

在一台有足够内存的单机上,Morris 推断,对 1TB 的数据进行排序需要大约 30,000 秒(对 100GB 的数据进行排序需要 2500 秒)。

中间的图表表明他们只能以 5GB/s 的速度在网络上传输数据。简单地移动 1TB 的数据将需要 200 秒。而且 MapReduce 会多次移动它:从 map 到 reduce,从 reduce 到 GFS(多次复制)。

重要洞察: 计算涉及数据的移动。不仅仅是 CPU 周期。

6.824 原始笔记。

 Why MapReduce?
      Second look for fault tolerance and performance
      Starting point in current enthusiasm for big cluster computing
      A triumph of simplicity for programmer
      Bulk orientation well matched to cluster with slow network
      Very influential, inspired many successors (Hadoop, Spark, &c)

    Cluster computing for Big Data
      1000 computers + disks
      a LAN
      split up data+computation among machines
      communicate as needed
      similar to DSM vision but much bigger, no desire for compatibilty

    Example: inverted index
      e.g. index terabytes of web pages for a search engine
      Input:
        A collection of documents, e.g. crawled copy of entire web
        doc 31: i am alex
        doc 32: alex at 8 am
      Output:
        alex: 31/3 32/1 ...
        am: 31/2 32/4 ...
      Map(document file i):
        split into words
        for each offset j
          emit key=word[j] value=i/j
      Reduce(word, list of d/o)
        emit word, sorted list of d/o

    Diagram:
      * input partitioned into M splits on GFS: A, B, C, ...
      * Maps read local split, produce R local intermediate files (A0, A1 .. AR)
      * Reduce # = hash(key) % R
      * Reduce task i fetches Ai, Bi, Ci -- from every Map worker
      * Sort the fetched files to bring same key together
      * Call Reduce function on each key's values
      * Write output to GFS
      * Master controls all:
        Map task list
        Reduce task list
        Location of intermediate data (which Map worker ran which Map task)

    Notice:
      Input is huge -- terabytes
      Info from all parts of input contributes to each output index entry
        So terabytes must be communicated between machines
      Output is huge -- terabytes

    The main challenge: communication bottleneck
      Three kinds of data movement needed:
        Read huge input
        Move huge intermediate data
        Store huge output
      How fast can one move data?
        RAM: 1000*1 GB/sec =    1000 GB/sec
        disk: 1000*0.1 GB/sec =  100 GB/sec
        net cross-section:        10 GB/sec
      Explain host link b/w vs net cross-section b/w

    How to cope with communication bottleneck
      Locality: split storage and computation the same way, onto same machines
        Because disk and RAM are faster than the network
      Batching: move megabytes at a time, not e.g. little key/value puts/gets
        Because network latency is a worse problem than network throughput
      Programming: let the developer indicate how data should move between machines
        Because the most powerful solutions lie with application structure

    The big programming idea in MapReduce is the key-driven shuffle
      Map function implicitly specifies what and where data is moved -- with keys
      Programmer can control movement, but isn't burdened with details
      Programs are pretty constrained to help with communication:
        Map can only read the local split of input data, for locality and simplicity
        Just one batch shuffle per computation
        Reduce can only look at one key, for locality and simplicity

    Where does MapReduce input come from?
      Input is striped+replicated over GFS in 64 MB chunks
      But in fact Map always reads from a local disk
        They run the Maps on the GFS server that holds the data
      Tradeoff:
        Good: Map can read at disk speed, much faster than reading over net from GFS server
        Bad: only two or three choices of where a given Map can run
             potential problem for load balance, stragglers

    Where does MapReduce store intermediate data?
      On the local disk of the Map server (not in GFS)
      Tradeoff:
        Good: local disk write is faster than writing over network to GFS server
        Bad: only one copy, potential problem for fault-tolerance and load-balance

    Where does MapReduce store output?
      In GFS, replicated, separate file per Reduce task
      So output requires network communication -- slow
      The reason: output can then be used as input for subsequent MapReduce

    The Question: How soon after it receives the first file of
      intermediate data can a reduce worker start calling the application's
      Reduce function?

    Why does MapReduce postpone choice of which worker runs a Reduce?
      After all, might run faster if Map output directly streamed to reduce worker
      Dynamic load balance!
      If fixed in advance, one machine 2x slower -> 2x delay for whole computation
        and maybe the rest of the cluster idle/wasted half the time

    Will MR scale?
      Will buying 2x machines yield 1/2 the run-time, indefinitely?
      Map calls probably scale
        2x machines -> each Map's input 1/2 as big -> done in 1/2 the time
        but: input may not be infinitely partitionable
        but: tiny input and intermediate files have high overhead
      Reduce calls probably scale
        2x machines -> each handles 1/2 as many keys -> done in 1/2 the time
        but: can't have more workers than keys
        but: limited if some keys have more values than others
          e.g. "the" has vast number of values for inverted index
          so 2x machines -> no faster, since limited by key w/ most values
      Network may limit scaling, if large intermediate data
        Must spend money on faster core switches as well as more machines
        Not easy -- a hot R+D area now
      Stragglers are a problem, if one machine is slow, or load imbalance
        Can't solve imbalance w/ more machines
      Start-up time is about a minute!!!
        Can't reduce that no matter how many machines you buy (probably makes it worse)
      More machines -> more failures

    Now let's talk about fault tolerance
      The challenge: paper says one server failure per job!
      Too frequent for whole-job restart to be attractive

    The main idea: Map and Reduce are deterministic and functional,
        so MapReduce can deal with failures by re-executing
      Often a choice:
        Re-execute big tasks, or
        Save output, replicate, use small tasks
      Best tradeoff depends on frequency of failures and expense of communication

    What if a worker fails while running Map?
      Can we restart just that Map on another machine?
        Yes: GFS keeps copy of each input split on 3 machines
      Master knows, tells Reduce workers where to find intermediate files

    If a Map finishes, then that worker fails, do we need to re-run that Map?
      Intermediate output now inaccessible on worker's local disk.
      Thus need to re-run Map elsewhere *unless* all Reduce workers have
        already fetched that Map's output.

    What if Map had started to produce output, then crashed:
      Will some Reduces see Map's output twice?
      And thus produce e.g. word counts that are too high?

    What if a worker fails while running Reduce?
      Where can a replacement worker find Reduce input?
      If a Reduce finishes, then worker fails, do we need to re-run?
        No: Reduce output is stored+replicated in GFS.

    Load balance
      What if some Map machines are faster than others?
        Or some input splits take longer to process?
      Don't want lots of idle machines and lots of work left to do!
      Solution: many more input splits than machines
      Master hands out more Map tasks as machines finish
      Thus faster machines do bigger share of work
      But there's a constraint:
        Want to run Map task on machine that stores input data
        GFS keeps 3 replicas of each input data split
        So only three efficient choices of where to run each Map task

    Stragglers
      Often one machine is slow at finishing very last task
        h/w or s/w wedged, overloaded with some other work
      Load balance only balances newly assigned tasks
      Solution: always schedule multiple copies of very last tasks!

    How many Map/Reduce tasks vs workers should we have?
      They use M = 10x number of workers, R = 2x.
      More => finer grained load balance.
      More => less redundant work for straggler reduction.
      More => spread tasks of failed worker over more machines, re-execute faster.
      More => overlap Map and shuffle, shuffle and Reduce.
      Less => big intermediate files w/ less overhead.
      M and R also maybe constrained by how data is striped in GFS.
        e.g. 64 MByte GFS chunks means M needs to total data size / 64 MBytes

    Let's look at paper's performance evaluation

    Figure 2 / Section 5.2
      Text search for rare 3-char pattern, just Map, no shuffle or reduce
      One terabyte of input
      1800 machines
      Figure 2 x-axis is time, y-axis is input read rate
      60 seconds of start-up time are *omitted*! (copying program, opening input files)
      Why does it take so long (60 seconds) to reach the peak rate?
      Why does it go up to 30,000 MB/s? Why not 3,000 or 300,000?
        That's 17 MB/sec per server.
        What limits the peak rate?

    Figure 3(a) / Section 5.3
      sorting a terabyte
      Should we be impressed by 800 seconds?
      Top graph -- Input rate
        Why peak of 10,000 MB/s?
        Why less than Figure 2's 30,000 MB/s? (writes disk)
        Why does read phase last abt 100 seconds?
      Middle graph -- Shuffle rate
        How is shuffle able to start before Map phase finishes? (more map tasks than workers)
        Why does it peak at 5,000 MB/s? (??? net cross-sec b/w abt 18 GB/s)
        Why a gap, then starts again? (runs some Reduce tasks, then fetches more)
        Why is the 2nd bump lower than first? (maybe competing w/ overlapped output writes)
      Lower graph -- Reduce output rate
        How can reduces start before shuffle has finished? (again, shuffle gets all files for some tasks)
        Why is output rate so much lower than input rate? (net rather than disk; writes twice to GFS)
        Why the gap between apparent end of output and vertical "Done" line? (stragglers?)

    What should we buy if we wanted sort to run faster?
      Let's guess how much each resource limits performance.
      Reading input from disk: 30 GB/sec = 33 seconds (Figure 2)
      Map computation: between zero and 150 seconds (Figure 3(a) top)
      Writing intermediate to disk: ? (maybe 30 Gb/sec = 33 seconds)
      Map->Reduce across net: 5 GB/sec = 200 seconds
      Local sort: 2*100 seconds (gap in Figure 3(a) middle)
      Writing output to GFS twice: 2.5 GB/sec = 400 seconds
      Stragglers: 150 seconds? (Figure 3(a) bottom tail)
      The answer: the network accounts for 600 of 850 seconds

    Is it disappointing that sort harnesses only a small fraction of cluster CPU power?
      After all, only 200 of 800 seconds were spent sorting.
      If all they did was sort, they should sell CPUs/disks and buy a faster network.

    Modern data centers have relatively faster networks
      e.g. FDS's 5.5 terabits/sec cross-section b/w vs MR paper's 150 gigabits/sec
      while CPUs are only modestly faster than in MR paper
      so today bottleneck might have shifted away from net, towards CPU

    For what applications *doesn't* MapReduce work well?
      Small updates (re-run whole computation?)
      Small unpredictable reads (neither Map nor Reduce can choose input)
      Multiple shuffles (can use multiple MR but not very efficient)
        In general, data-flow graphs with more than two stages
      Iteration (e.g. page-rank)

    MapReduce retrospective
      Single-handedly made big cluster computation popular
        (though coincident with big datacenters, cheap machines, data-oriented companies)
      Hadoop is still very popular
      Inspired better successors (Spark, DryadLINQ, &c) 

Spark

6.824 2015 年第 14 讲:Spark

注意: 这些讲义是稍作修改的,源自 2015 年春季学期的 6.824 课程网站

介绍

  • MapReduce 的好处:

    • 可扩展

    • 容错性

    • 处理落后者的策略

      • 如果一个映射任务失败,MapReduce 就会在另一台不同的机器上启动另一个任务

      • 任务之间不通信,所以容易让它们并行运行两次

  • MapReduce 的局限性:

    • 非常严格的计算形式

    • 一个映射阶段,一个映射和减少之间的通信级别,以及减少阶段

    • 如果您想要构建一个倒排索引 并且 按照最流行的关键字排序 => 您需要两个 MapReduce 作业

    • 所以,无法正确处理多阶段、迭代性或交互式作业

用户需要更复杂的计算 => Spark

以前有解决方案专门解决不同类型的计算。Spark 的目标是为足够通用的计算模型提供一个解决方案。

Spark

在保持可伸缩性和容错性的同时很难进行 DSM。

RDD:弹性分布式数据集

  • 本质上是一个 Scala 对象

  • 不可变的

  • 在机器之间分区

示例(构建一个 RDD,其中包含文本文件中以“ERROR”开头的所有行):

lines = textFile("log.txt")
errors = lines.Filter(_.startsWith("ERROR"))
errors.persist()
errors.count() 

RDD 是通过以下方式创建的:

  • 通过引用外部存储中的数据

  • 通过转换其他 RDD

    • 就像上面的 Filter 调用一样

操作 在 RDD 上启动计算,就像上面的 count() 调用一样。

persist() 调用告诉 Spark 将 RDD 保留在内存中,以便快速访问。

直到看到并执行count()操作才开始执行任何工作(惰性评估)

  • 您可以通过将所有应用于操作之前的过滤器组合来节省工作

    • 读取并过滤比完全读取文件然后再进行另一个过滤步骤要快

容错性

衍生图:RDD 之间的依赖关系

lines (file) 

    |
   \|   filter(_.startsWith("ERROR"))

errors 

机器:

file = b1 b2 b3 b4 b5

p1 p2 <-\      p3 p4       p5
-----          -----       -----
| M1|   |      |M2 |       |M3 |
-----   |      -----       -----
b1 b2 --/      b3 b4       b5 

如果丢失了 RDD 的分区,例如 p4(因为 M2 失败了),您可以通过跟踪依赖图并根据已经计算的内容决定从哪里开始和重新计算来重新构建它。

如何知道要在哪些机器上重新计算?在这个例子中,b3b4 可能会被复制到其他机器上(也许是 M1M3),所以您可以从那里重新启动计算。

比较

 |     RDDs                      |     DSM
-----------*-------------------------------|---------------------------
reads      | any type / granularity        |  fine-grained
writes     | only through transformations  |  fine-grained
           |  coarse grained
faults     | recompute                     |  checkpoint (a la Remus)
stragglers | restart job on diff. machine  |  ? no good strategy ? 

Spark 计算表达能力

Spark 非常通用:许多现有的并行计算范式,如 MapReduce,可以很容易地在其之上实现

粗粒度写入足够好的原因是因为许多并行算法简单地对所有数据应用相同的操作。

分区

可以有一个自定义的分区函数,说“这个 RDD 有 10 个分区,第一个分区是所有以a开头的元素,等等..”

如果您多次使用数据集,则正确分组数据集以使您需要的数据位于同一台机器上很重要。

PageRank 示例

示例:

 the "o"'s are webpages
    the arrows are links (sometimes directed, if not just pick a direction)

    1    2
    o<---o <-\
    |\   |   |
    | \  |   |
    |  \ |   |
    *   **   |
    o--->o --/    
    3    4 

算法:

  • 每页都从等级 1 开始

  • 每个人都将自己的等级分配给他们的邻居

    • 网站 1 将给节点 3 和节点 4 各 0.5,并从节点 2 收到 0.5
  • 迭代多少次?显然直到收敛。

数据:

RDD1 'links': (url, links)
 - can compute with a map operation

RDD2 'ranks': (url, rank)
 - links.join(ranks) -> (url, (links, rank))
 -      .flatMap( 
                (url, (links, rank))) 
                    =>
                links.map( l -> (l.dest, rank/n))
            )
 - TODO: not sure why 'rank/n` or how this transformation works
 - store result in RDD3 'contribs'
 - update ranks with contribs
 - ranks = contribs.reduceByKey( _ + _ ) 

不良分配的示例,因为我们将传输大量数据:

 the squares are machines (partitions)

            links                       ranks
    -------------------------       ------------------------
    |(a,...)|(d,...)|(c,...)|       |(d,1)  |(e,5)  |(c,3) |
    |(b,...)|       |(e,...)|       |       |(a,1)  |(b,1) |
    -------------------------       -----------------------
        \                                     /
         \------------\  /-------------------/
                       \/  
                    -------------------------       
                    |(a,...)|(d,...)|       |       
                    |       |       |       |       
                    ------------------------- 

带有分区的示例:

 links                       ranks
    -------------------------       ------------------------
    |(a,...)|(c,...)|(e,...)|       |(a,1)  |(c,3)  |(e,3) |
    |(b,...)|(d,...)|       |       |(b,1)  |(d,1)  |      |
    -------------------------       ------------------------

        contribs are easy to compute locally now 

那么 PageRank 需要通信吗?是的,contribs RDD 执行 reduceByKey

待办事项:不确定它的作用是什么

内部表示

RDD 方法:

  • partitions -- 返回一个分区列表

  • preferredLocations(p) -- 返回分区的首选位置

    • 告诉您计算速度更快的机器
  • dependencies

    • 您如何依赖其他 RDD
  • iterator(p, parentIters)

    • 要求 RDD 计算其一个分区
  • partitioner

    • 允许您指定一个分区函数

6.824 笔记

Resilient Distributed Datasets: A Fault-Tolerant Abstraction for In-Memory Cluster Computing
Zaharia, Chowdhury, Das, Dave, Ma, McCauley, Franklin, Shenker, Stoica
NSDI 2012

Had TreadMarks since 1996, and Distributed Shared Memory is a very general abstraction. Why use MapReduce? Or why even use TreadMarks?
Say looking through a log, why not implement it using the regular abstractions (sockets, files etc?)
  Saves a lot of work:
    communication between nodes
    distribute code
    schedule work
    handle failures

The MapReduce paper had a lot of impact on big data analytics: simple and powerful.
But bit too rigid. Other systems proposed fixes:

Dryad (Microsoft 2007): any directed acyclic graph, edges are communication channels, can be through disk or via TCP.
  + can implement multiple iterations
  + can pipeline through RAM, don't have to go to disk
  - very low level: 
      doesn't deal with partitioning of data, want 100,000 mappers? add 100,000 nodes
      what happens if you run out of RAM? (brief mention of "downgrading" a TCP channel to a disk file)
  - doesn't checkpoint/replicate, in the middle of the run (so failures can be expensive)

* Pig latin (Yahoo 2008): programming language that compiles to MapReduce. Adds "Database style" operators, mainly Join
Join: dataset 1 (k1,v1), dataset 2 (k1, v2). ==> (k1, v1, v2), takes cartesian product (all tuples of combinations of v1, v2 with same k1)
Example: dataset 1: all clicks on products on website, dataset 2: demographics (age of users), want average age of customer per product.
  + allows multiple iterations
  + can express more
  - still has rigidness from MR (writes to disk after map, to replicated storage after reduce, RAM)

Spark

A framework for large scale distributed computation. 
  An expressive programming model (can express iteration and joins)
  Gives user control over trade off between fault tolerance with performance
     if user frequently perist w/REPLICATE, fast recovery, but slower execution
     if infrequently, fast execution but slow recovery

Relatively recent release, but used by (partial list) IBM, Groupon, Yahoo, Baidu..
Can get substantial performance gains when dataset (or a major part of it) can fit in memory, so anticipated to get more traction.
MapReduce is simple

Abstraction of Resilient Distributed Datasets: an RDD is a collection of partitions of records.
Two operations on RDDs:
  Transformations: compute a new RDD from existing RDDs (flatMap, reduceByKey)
    this just specifies a plan. runtime is lazy - doesn't have to materialize (compute), so it doesn't
  Actions: where some effect is requested: result to be stored, get specific value, etc.
    causes RDDs to materialize.

Logistic regression (from paper):
val points = spark.textFile(...)
        .map(parsePoint).persist()
var w = // random initial vector
for (i <- 1 to ITERATIONS) {
    val gradient = points.map{ p =>
    p.x * (1/(1+exp(-p.y*(w dot p.x)))-1)*p.y
    }.reduce((a,b) => a+b)
    w -= gradient
}

* w is sent with the closure to the nodes
* materializes a new RDD in every loop iteration

PageRank (from paper):
val links = spark.textFile(...).map(...).persist() // (URL, outlinks)
var ranks = // RDD of (URL, rank) pairs
for (i <- 1 to ITERATIONS) {
  // Build an RDD of (targetURL, float) pairs
  // with the contributions sent by each page
  val contribs = links.join(ranks).flatMap {
     (url, (links, rank)) =>
    links.map(dest => (dest, rank/links.size))
}
// Sum contributions by URL and get new ranks
  ranks = contribs.reduceByKey((x,y) => x+y)
     .mapValues(sum => a/N + (1-a)*sum)
}

What is an RDD (table 3, S4)
  list of partitions
  list of (parent RDD, wide/narrow dependency)
  function to compute
  partitioning scheme
  computation placement hint
Each transformation takes (one or more) RDDs, and outputs the transformed RDD.

Q: Why does an RDD carry metadata on its partitioning?
A: so transformations that depend on multiple RDDs know whether they need to shuffle data (wide dependency) or not (narrow)
Allows users control over locality and reduces shuffles.

Q: Why the distinction between narrow and wide dependencies?
A: In case of failure.
  narrow dependency only depends on a few partitions that need to be recomputed.
  wide dependency might require an entire RDD

Handling faults.
When Spark computes, by default it only generates one copy of the result, doesn't replicate. Without replication, no matter if it's put in RAM or disk, if node fails, on permanent failure, data is gone.
When some partition is lost and needs to be recomputed, the scheduler needs to find a way to recompute it. (a fault can be detected by using a heartbeat)
  will need to compute all partitions it depends on, until a partition in RAM/disk, or in replicated storage.
  if wide dependency, will need all partitions of that dependency to recompute, if narrow just one that RDD

So two mechanisms enable recovery from faults: lineage, and policy of what partitions to persist (either to one node or replicated)
We talked about lineage before (Transformations)

The user can call persist on an RDD.
  With RELIABLE flag, will keep multiple copies (in RAM if possible, disk if RAM is full)
  With REPLICATE flag, will write to stable storage (HDFS)
  Without flags, will try to keep in RAM (will spill to disk when RAM is full)

Q: Is persist a transformation or an action?
A: neither. It doesn't create a new RDD, and doesn't cause materialization. It's an instruction to the scheduler.

Q: By calling persist without flags, is it guaranteed that in case of fault that RDD wouldn't have to be recomputed?
A: No. There is no replication, so a node holding a partition could fail.
Replication (either in RAM or in stable storage) is necessary

Currently only manual checkpointing via calls to persist.
Q: Why implement checkpointing? (it's expensive)
A: Long lineage could cause large recovery time. Or when there are wide dependencies a single failure might require many partition re-computations.

Checkpointing is like buying insurance: pay writing to stable storage so can recover faster in case of fault.
Depends on frequency of failure and on cost of slower recovery
An automatic checkpointing will take these into account, together with size of data (how much time it takes to write), and computation time.

So can handle a node failure by recomputing lineage up to partitions that can be read from RAM/Disk/replicated storage.
Q: Can Spark handle network partitions?
A: Nodes that cannot communicate with scheduler will appear dead. The part of the network that can be reached from scheduler can continue
  computation, as long as it has enough data to start the lineage from (if all replicas of a required partition cannot be reached, cluster
  cannot make progress)

What happens when there isn't enough memory?
  - LRU (Least Recently Used) on partitions
    - first on non-persisted
    - then persisted (but they will be available on disk. makes sure user cannot overbook RAM)
  - user can have control on order of eviction via "persistence priority"
  - no reason not to discard non-persisted partitions (if they've already been used)

Shouldn't throw away partitions in RAM that are required but hadn't been used.

degrades to "almost" MapReduce behavior 
In figure 7, k-means on 100 Hadoop nodes takes 76-80 seconds
In figure 12, k-means on 25 Spark nodes (with no partitions allowed in memory) takes 68.8
Difference could be because MapReduce would use replicated storage after reduce, but Spark by default would only spill to local disk, no network latency and I/O load on replicas.
no architectural reason why Spark would be slower than MR

Spark assumes it runs on an isolated memory space (multiple schedulers don't share the memory pool well).
Can be solved using a "unified memory manager"
Note that when there is reuse of RDDs between jobs, they need to run on the same scheduler to benefit anyway.

(from [P09])
Why not just use parallel databases? Commercially available: "Teradata, Aster Data, Netezza, DATAl-
legro (and therefore soon Microsoft SQL Server via Project Madi-
son), Dataupia, Vertica, ParAccel, Neoview, Greenplum, DB2 (via
the Database Partitioning Feature), and Oracle (via Exadata)"

At the time, Parallel DBMS were
 * Some are expensive and Hard to set up right
 * SQL declarative (vs. procedural)
 * Required schema, indices etc (an advantages in some cases)
 * "Not made here"

Picollo [P10] uses snapshots of a distributed key-value store to handle fault tolerance.
- Computation is comprised of control functions and kernel functions.
- Control functions are responsible for setting up tables (also locality), launching kernels, synchronization (barriers that wait for all kernels to complete), and starting checkpoints
- Kernels use the key value store. There is a function to merge conflicting writes.
- Checkpoints using Chandy-Lamport
* all data has to fit in RAM
* to recover, all nodes need to revert (expensive)
* no way to mitigate stragglers, cannot just re-run a kernel without reverting to a snapshot

[P09] "A Comparison of Approaches to Large-Scale Data Analysis", Pavlo et al. SIGMOD'09
[P10] Piccolo: Building Fast, Distributed Programs with Partitioned Tables, Power and Li, OSDI'10 

Spanner

6.824 2015 年第 15 讲 Spanner

注意:这些讲座笔记略有修改,来源于 2015 年春季 6.824 课程网站上发布的内容。

介绍

Spanner 论文,OSDI 2012

  • 打破了旧的假设:不能假设时钟紧密同步

    • 在全球范围内的分布式系统中,紧密同步的时钟现在是可行的:GPS 和原子钟作为独立的来源
  • 数据模型:不可变版本化数据

  • 在多个数据中心构建和部署系统

  • Paxos 帮助您确定事件顺序。为什么我们仍然需要时间?

  • 使用同步时间允许本地读取而无需锁定

  • 在复制之上的事务

    • 跨多个副本组的两阶段提交
  • 并发控制

    • 严格的两阶段锁定与时间戳
  • Paxos

    • 长寿领导者(定时租约)

    • 流水线(多个提案在飞行中)

    • 无序提交,有序应用

Spanner 和“研究”

  • 团队中充满了博士学位的人

  • 当我们有冲动并且有话要说时,我们会写研究论文

  • 尖端开发,令人难以置信的规模,但我们不是研究人员

历史背景

Bigtable 论文,OSDI 2006

  • 2003 年底开始开发(6 位博士)

  • 第一个客户于 2005 年中期启动了 Bigtable

  • 分布式键值存储

    • 单行事务

    • 后来添加了延迟复制

  • 价值主张

    • 扩展到大量数据

    • 自动重新分片

  • Bigtable 是“NoSQL”的鼻祖之一,或者更准确地说是“如何在不构建数据库的情况下存储大量数据”的鼻祖之一

  • 当时的基本原则(Bigtable 的设计假设):

    • 谁需要数据库?键值存储就足够了

    • 谁需要 SQL?对大多数应用程序来说是不必要的

    • 谁需要事务?两阶段提交太昂贵

为什么选择 Spanner?

  • 发现 Bigtable 太难使用

    • 用户喜欢 SQL 数据库给他们带来的强大功能

    • 工程师不应该被迫绕过

      • 缺乏事务

      • 由于延迟复制提供的弱语义而显现出的错误

    • 程序员的生产力很重要

Megastore,约 2006 年开始,建立在 Bigtable 之上

  • 乐观并发控制

  • 基于 Paxos 的复制

    • 没有长寿领导者(每次写入都进行 Paxos“选举”)

    • 每个 Paxos 消息都写入 Bigtable

  • 比 Bigtable 更广泛的事务类

  • 类似 SQL 的模式和查询语言

  • 具有一致复制

Dremel,Google 的数据分析,约 2008 年开始

事务

Percolator,通用事务

  • 快照隔离:普通事务有一个提交点(逻辑上,当您提交时,一切都发生了)

    • 待办事项:查找这意味着什么,因为我无法记下他的解释
  • 建立在 Bigtable 之上

  • 用户要求事务,但我们还没有准备好将其构建到 bigtable 中

Spanner

  • 我们知道我们需要

    • 一个数据库

    • SQL

    • 数据中心之间的一致复制

    • 通用事务

  • 其余的只是“纯粹的工程”

TrueTime 出现了...(关于他们如何发现纽约有人在研究分布式时钟,他们意识到这对于他们的并发控制可能有用的故事)

全球同步时钟

  • spanner 的行为就像单机数据库

    • 一致的复制:副本都报告相同的状态

    • 外部一致性:副本都报告相同的事件顺序

  • 良好的语义

我们对 bigtable 错了吗

是的,也不是:

  • 是的,对于长期来说是的:2003 年不知道 2009 年所知道的,没有人员或技术

  • 不,因为很多人在 Google 使用 bigtable

想象你正在经营一家初创公司。哪些长期问题可以推迟解决?

初创公司困境:

  • 太多时间花在可扩展存储上 => 浪费的努力 => 未能按时完成 => 失败

  • 在可扩展存储上花费太少时间 => 当他们变得受欢迎时无法扩展 => 失败

你有能力/意愿/愿景做什么?

  • 10 年前我们无法构建 Spanner:甚至 5 年前也不行

  • 有人告诉他们应该内置事务,但他们没有这样做,因为当时他们无法做到

有趣的问题

为什么 Bigtable 论文在研究界和技术界都产生了更大的影响?

  • 研究与实践

为什么系统研究人员坚持构建可扩展的键值存储(而不是数据库)?

教训

第零课

时机是一切。除非运气胜过时机。

当世界正在变化时,你无法计划时机:为你面前的问题设计最好的解决方案

TrueTime 的发生是由于事件和人的幸运巧合(即运气)。Bigtable 也是如此。Spanner 的初始设计(2008 年之前)与 Google 现在拥有的完全不同:直到项目在 2008 年重新启动之前,他们一直在反运气。

第一课

构建你需要的东西,不要设计过度。也不要设计不足,因为你会为此付出代价。

第二课

有时无知真的是福。或者可能是运气。

如果你戴着眼罩,你就无法超越。如果我们在 2004 年就知道我们需要一个具有外部一致性的分布式复制数据库,我们就会失败。

第三课

你的用户群很重要。

  • 当 Google < 2000名员工时,bigtable 就开始了

    • 有限的产品数量

    • 工程师并不多

  • 当 Google 有10K名员工时,spanner 就开始了

    • 更多的产品

    • 更多的工程师,更多的初级工程师,更多的收购公司

  • 员工的生产力很重要

总结

你无法购买运气。你无法计划运气。但你不能忽视运气。

你可以增加自己幸运的机会:

  • 具备强大的技术技能

  • 锻炼你的设计感(找到学习的机会!)

  • 建立一个强大的同事和朋友网络

  • 学会如何在团队中工作

  • 学会你擅长什么,以及你擅长什么

    • 对自己要毫无保留地诚实

    • 愿意寻求帮助

    • 承认当你错了

    • 人们不喜欢与经常告诉他们错误的人合作

Spanner 缺少什么?

可能的断开连接访问:我们能否构建可以使用数据库并能脱机运行的应用程序?

Coda 文件系统中的断开操作工作。

6.824 笔记

Spanner:谷歌的全球分布式数据库,Corbett 等人,OSDI 2012

为什么选择这篇论文?

  • 现代化、高性能、受现实需求驱动

  • Paxos 的复杂使用

  • 解决一致性 + 性能(将是一个重要主题)

  • Lab 4 是 Spanner 的(极大地)简化版本

有哪些重要的想法?

  • 使用 Paxos 复制的分片管理

  • 尽管进行同步的广域网复制,仍然保持高性能

  • 通过仅询问最近的复制品来快速读取

  • 尽管进行分片(这是真正的重点)仍然保持一致性

  • 巧妙使用时间来保持一致性

  • 分布式事务

这是一篇深奥的论文!我试图将一些想法简化成更简单的形式。

分片

思路:分片

  • 我们以前在 FDS 中见过这种情况

  • 真正的问题是管理配置更改

  • Spanner 对此的设计比 FDS 更有说服力

简化的分片轮廓(实验 4):

  • 复制组,Paxos 复制

    • 每个复制组中都有 Paxos 日志
  • 主节点,Paxos 复制

    • 将分片分配给组

    • 编号配置

  • 如果主节点移动了一个分片,组最终会看到新的配置

    • 在两个组的 Paxos 日志中都有 "start handoff Num=7" 操作

    • 尽管可能不是同时进行的

  • dst 在获得多数派的片段数据副本之前无法完成移交

    • 并且不能长时间等待可能失败的少数派

    • 少数派必须赶上,所以也许将分片数据放入 Paxos 日志中(!)

  • 在两个组的日志中都有 "end handoff Num=7" 操作

问:如果一个 Put 操作与移交同时发生会怎样?

  • 客户端看到新的配置,在移交开始前将其发送到新组?

  • 客户端具有陈旧的视图,并在移交后将其发送到旧的组?

  • 在移交期间到达任何一个?

问:如果移交期间发生故障?

  • 例如,旧的组认为分片已移交

    • 但新组在它认为如此之前失败了

问: 两个 组可以认为它们正在提供分片吗?

问:如果旧的组无法听到主节点,是否仍然可以提供分片?

思路:广域同步复制

  • 目标:在单一站点灾难中生存

  • 目标:靠近客户的复制品

  • 目标:不要丢失任何更新

直到几年前都被认为是不切实际的

  • Paxos 太昂贵了,那么也许是主/备份?

  • 如果主节点等待来自备份的 ACK

    • 50ms 网络将限制吞吐量并引起明显的延迟

    • 特别是如果应用程序必须在每次 50ms 进行多次读取

  • 如果主节点不等待,它将在持久之前回复给客户端

  • 分裂脑的危险;无法使网络可靠

有什么变化吗?

  • 其他站点可能只有 5ms 距离 - 旧金山 / 洛杉矶

  • 更快/更便宜的广域网

  • 应用程序编写以容忍延迟

    • 可能会发出许多慢速读取请求

    • 但并行发出

    • 可能很快超时并尝试其他地方,或者冗余获取

  • 大量并发客户端使您能够在延迟较高的情况下获得高吞吐量

    • 并行运行他们的请求
  • 人们更加欣赏 paxos 并拥有更简化的变体

    • 较少的消息

      • paxos 论文第 9 页:领导者每个操作 1 轮+大量预准备

      • 论文中的方案稍微复杂一些,因为他们必须确保最多只有一个领导者

    • 在任何副本处进行读取

实际性能?

  • 表 3

    • 假装只是为了测量写入而测量 paxos,读取任何副本以获取读取延迟

      • 为什么随着副本数量的增加,写入延迟不会增加?

      • 为什么随着副本数量增加,延迟的标准差会下降?

      • r/o 很多,因为不是 paxos 协议 + 使用最接近的副本吞吐量

      • 为什么读取吞吐量随着#副本增加而增加?

      • 为什么写入吞吐量不会增加?

      • 写入吞吐量似乎在下降吗?

    • 我们能从表 3 中得出什么结论?

      • 系统快吗?慢吗?
    • 你的 paxos 运行多快?

      • 我的协议每个协议花费 10 毫秒

      • 仅具有纯本地通信且无磁盘

      • Spanner paxos 可能会等待磁盘写入

  • 图 5

    • npaxos=5,所有领导者在同一区域

    • 为什么在每个组中杀死一个非领导者没有效果?对于杀死所有领导者(“领导者硬化”)

      • 为什么有几秒钟是平坦的?

      • 什么导致它开始上升?

      • 为什么需要 5 到 10 秒才能恢复?

      • 为什么直到重新加入时斜率更高

Spanner 从任何 paxos 副本中读取

  • 读取不涉及 paxos 协议

  • 直接从副本的 k/v 数据库中读取数据

  • 可能快 100 倍——在同一房间而不是跨国

问:我们可以只到一个副本吗?

问:从任何副本进行读取是正确的吗?

问题的例子:

  • 照片分享站点

  • 我有照片

  • 我有一个 ACL(访问控制列表)规定谁可以看我的照片

  • 我把我的妈妈从我的 ACL 中删除,然后上传新照片

  • 实际上是网络前端执行这些客户端读/写操作

事件顺序:

  1. W1:我在 G1 组上(裸多数)写 ACL,然后

  2. W2:我在 G2 组上添加图像(裸多数),然后

  3. 妈妈读取图像——可能从滞后的 G2 副本获取旧数据

  4. 妈妈读取 ACL——可能从 G1 获取新数据

该系统不像单个服务器那样运行!

  • 实际上没有任何一个时刻图像被...

    • 存在,但 ACL 还没有更新

这个问题是由以下几个因素的组合引起的

  • 分区——副本组独立运行

  • 为了性能而采取捷径——从任何副本读取

我们怎么修复这个问题?

  1. 使读取看到最新数据

    • 例如,全面的 paxos 用于读取昂贵!
  2. 使读取看到一致的数据

    • 数据如某个以前的时间点存在

    • 即在 #1 之前,#1 和#2 之间,或#2 之后

    • 这事实证明更加便宜

    • Spanner 做到了这一点

这是 spanner 一致性故事的一个超级简化版本,适用于 r/o 客户端

  • “快照”或“无锁”读取

  • 现在假设所有时钟都一致

  • 服务器(paxos 领导者)使用写入时间标记每个写入操作

  • k/v 数据库为每个键存储多个值,

    • 每个都有不同的时间
  • 阅读客户选择了时间 t

    • 每次读取

      • 在时间 t 询问相关的副本进行读取
  • 一个副本如何在时间 t 读取一个键?

    • 返回<= t的最高时间的存储值
  • 但是等等,副本可能落后了

    • 也就是说,可能有一个时间 < t 的写入,但副本还没有看到

    • 所以副本必须以某种方式确保它已经看到了所有的写入<= t

    • 思路:它是否已经看到了从时间 > t 开始的任何操作?

      • 如果是的,并且 PAXOS 组总是按时间顺序达成一致意见,那么检查/等待具有时间> t的操作就足够了

      • 这就是 Spanner 在读取时所做的(4.1.3)

  • 读取客户端应该选择什么时间?

    • 使用当前时间可能会强制滞后的副本等待

    • 所以可能有一点过去的时间

    • 客户端可能会错过最新的更新

    • 但至少它会看到一致的快照

    • 在我们的例子中,没有看到新图像也没有看到 ACL 更新

这如何修复我们的 ACL/图像例子?

  1. W1:我在 G1 上写入 ACL,G1 将其分配时间=10,然后

  2. W2:我添加图像,G2 分配时间=15(> 10,因为时钟一致)

  3. 妈妈选了一个时间,例如 t=14

  4. 妈妈从滞后的 G1 副本读取 t=14 的 ACL

    • 如果它没有看到通过 t=14 的 PAXOS 协议,它知道要等待,以便返回 G1
  5. 妈妈在 t=14 时从滞后的 G1 副本读取图像

    • 图像可能已经在那个副本上写入

    • 但是它将知道返回它,因为图像的时间是 15

    • t的其他选择也可以工作。

问:假设不同计算机的时钟一致是合理的吗?

  • 为什么他们可能不会同意?

问:如果服务器的时钟不一致会发生什么?

一个性能问题:读取客户端可能会选择未来的时间,迫使读取副本等待“赶上”

一个正确性问题:

  • 再次,ACL/图像的例子

  • G1 和 G2 对于时间的看法不一致

事件序列:

  1. W1:我在 G1 上写入 ACL -- 时间戳为 15

  2. W2:我在 G2 上添加了图像 -- 时间戳为 10

现在客户端在 t=14 时读取将看到图像但不会看到 ACL 更新

问:为什么 Spanner 不直接确保所有时钟都正确?

  • 毕竟,它拥有所有那些主 GPS / 原子钟

TrueTime(第 3 节)

  • 实际上存在一个"绝对"时间t_abs

    • 但是服务器时钟通常偏移一些未知的量

    • TrueTime 可以限制错误

  • 所以now()产生一个区间:[最早,最晚]

    • 最早和最晚是普通的标量时间

      • 可能是自 1970 年 1 月 1 日以来的微秒
  • t_abs高度可能在最早和最晚之间

问:TrueTime 如何选择时间间隔?

问:为什么 GPS 时间接收器能够避免这个问题?

  • 它们实际上是否会避免这种问题?

  • "原子钟"呢?

Spanner 为每个写入分配一个标量时间

  • 可能不是实际的绝对时间

  • 但是选择的是确保一致性

危险:

  • W1 在 G1,G1 的区间是 [20,30]

    • 在那个区间的任何时间都可以吗?
  • 然后 W2 在 G2,G2 的区间是[11,21]

    • 在那个区间的任何时间都可以吗?
  • 如果他们不小心,可能会得到 s1=25 s2=15

所以我们想要的是:

  • 如果 W2 在 W1 结束后开始,则s2 > s1

  • 从 4.1.2 简化的"外部一致性不变式"

  • 导致快照读取看到与 W1、W2 的真实顺序一致的数据

Spanner 如何为写入分配时间?

  • (同样,这是大大简化的,参见 4.1.2)

  • 写入请求到达 PAXOS 领导者

  • s将是写入的时间戳。

  • 领导者将s设置为TrueTime now().latest

    • 这是 4.1.2 中的“开始”。
  • 然后领导者延迟直到s < now().earliest

    • 即直到s被保证在过去(与绝对时间相比)。

    • 这是 4.1.2 中的“提交等待”。

  • 然后领导者运行 Paxos 以导致写入发生。

  • 然后领导者回复客户端。

这对我们的例子有效吗?

  • G1 处的 W1,TrueTime 说[20,30]。

    • s1 = 30

    • 等待提交直到 TrueTime 说[31,41]。

    • 回复客户端。

  • G2 处的 W2,TrueTime 现在必须>= [21,31]

    • (否则 TrueTime 就会出问题)。

    • s2 = 31。

    • 等待提交直到 TrueTime 说[32,43]。

    • 回复客户端。

  • 它对这个例子有效。

    • 客户端观察到 W1 在 S2 开始之前完成,

    • 而且s2 > s1

    • 即使 G2 的 TrueTime 时钟慢了最多可能的时间。

    • 所以如果我妈妈看到 S2,她也保证会看到 W1。

为什么“开始”规则?

  • 即为什么选择 TrueTime 间隔的结束时间?

  • 先前的写入者只等到他们的时间戳刚好< t_abs

  • 新的写入者必须选择s大于任何已完成写入。

  • t_abs可能高达now().latest

  • 所以s = now().latest

为什么“提交等待”规则?

  • 确保s < t_abs

  • 否则写入可能会在未来完成。

    • 并且会让“开始”规则给后续写入的s太低。

Q: 为什么提交等待;为什么不立即使用选择的时间写入值?

  • 间接地迫使后续写入具有足够高的s

    • 系统没有其他方式来通信不同副本组中写入的下一个最小可接受的s
  • 等待迫使一些外部代理正在串行化的写入具有单调递增的时间戳。

  • 没有等待,我们的例子回到了 s1=30 s2=21。

  • 你可以想象明确的方案来将上次写入的 TS 传达给下一个写入。

Q: 提交等待多长时间?

这回答了今天的问题:大的 TrueTime 不确定性需要长时间的提交等待,因此 Spanner 的作者对准确的低不确定性时间感兴趣。

让我们退一步。

  • 我们为什么要涉及所有这些时间戳的东西?

    • 我们的副本相距 100 英里或 1000 英里(为了本地性/容错)。

    • 我们想要从本地副本进行快速读取(无需完整的 Paxos)。

    • 我们的数据分布在许多具有独立时钟的副本组上。

    • 我们想要读取的一致性:

      • 如果 W1 然后 W2,读取不会看到 W2 但不看到 W1。
    • 它很复杂,但作为一个整体是有道理的。

    • Lab 3 / Lab 4 的高性能演变。

为什么这个时间戳技术很有趣?

  • 我们想要强制顺序--在实时发生的事情以某种顺序被分布式系统排序--“外部一致性”。

  • 幼稚的方法需要一个中央代理,或大量的通信。

  • Spanner 通过时间隐式地进行同步。

    • 时间可以是一种通信形式。

    • 例如,我们预先同意在晚上 6:00 见面吃晚餐。

论文中有很多额外的复杂性。

  • 事务,两阶段提交,两阶段锁定,

    • 模式更改,查询语言,等等。
  • 一些我们以后会看到更多的东西。

  • 特别是,在分布式系统中,事件排序的问题很快就会经常出现。

Facebook 上的 Memcache

6.824 2015 年第 16 讲:Facebook 上的 Memcache

注意: 这些讲义笔记是从 2015 年春季的 6.824 课程网站 上稍作修改的。

介绍

Facebook Memcached 论文:

  • 一个经验论文,而不是研究结果论文

  • 你可以将其视为一篇胜利的论文。

  • 你可以将其视为一篇警示论文:当你不考虑可扩展性时会发生什么。

  • 你可以将其视为一篇权衡的论文。

扩展一个 Web 应用程序。

任何 Web 应用程序的初始设计都是一个单个 Web 服务器机器,其中运行一个数据库服务器。

图表(单台机器:Web 服务器和数据库服务器)

Single machine
-------------------
| Web app         |
|                 | 
|                 |
|                 |
|       | DB | <-----> |Disk|
------------------ 
  • 最终他们发现他们在这台机器上使用了 100% 的 CPU。

  • top 告诉他们 CPU 时间正流向 Web 应用程序

图表(多个 Web 服务器机器,单个数据库机器):

Web app -----
Web app      \ -----> |DB| <-> Disk
...
Web app 
  • 接下来他们将面临的下一个问题是数据库将成为瓶颈

    • 数据库 CPU 是 100% 或磁盘是 100%
  • 他们决定购买一堆数据库服务器

图表:

Web app             |DB1| <-> Disk  (users A-M)
Web app             |DB2| <-> Disk  (users N-T)
...                 |DB3| <-> Disk  ...
Web app             ... 
  • 现在他们得想办法如何在数据库上 分片 数据

  • 现在应用软件必须知道要与哪个数据库服务器通信。

  • 我们不能再跨整个数据库进行事务了。

  • 我们不能再对整个数据集进行单个查询了。

    • 需要向每个服务器发送单独的查询。
  • 你不能把这一点推得太远,因为过了一段时间,分片会变得非常小,你会得到成为热点的数据库服务器

接下来,你注意到数据库中的大多数操作都是读取操作(如果是这样的话。在 Facebook 上是这样的)。

  • 原来你可以构建一个非常简单的内存缓存,可以每秒服务五十万个请求。

  • 然后你可以减少数据库上的 90% 的负载。

图表:

Web app -> |MC| --> |DB1| <-> Disk  (users A-M)
Web app    |MC|     |DB2| <-> Disk  (users N-T)
...         ..      |DB3| <-> Disk  ...
Web app             ... 
  • 如果你不断扩展你的服务,下一个瓶颈将是数据库写入。

观察:你可以使用数据库只读副本,而不是自己定制的 memcache(MC)节点。Facebook 没有这样做,因为他们想将缓存逻辑与数据库部署分开:

这是在有限的工程资源和时间内做出的最佳选择。此外,将缓存层与持久性层分开允许我们根据工作负载的变化分别调整每个层。

Facebook 的用例

非常重要: 他们并不太在意所有用户对系统的一致视图。

当 Web 应用程序客户端读取自己的写入时,唯一关心新鲜度和一致性的情况。

他们的高层次图片:

Regions (data centers):
    Master region (writable)
   ----------------- 
  | Web1 Web2 ...   |
  | MC1  MC2  ...   |
  | DB1  DB2  ... <--- complete copy of all data
   -----------------

    Slave regions (read only)
   ----------------- 
  | Web1 Web2 ...   |
  | MC1  MC2  ...   |
  | DB1  DB2  ... <--- complete copy of all data
   ----------------- 

拥有多个数据中心的原因:全球范围内的并行性

  • 也许也是为了备份目的(论文没有详细说明太多)。

大的教训

边缘缓存可能会棘手

这种看到缓存中有什么的边缘缓存样式对于现有系统非常容易添加。

  • 但是当缓存层不知道数据库中发生的情况时,会出现一些讨厌的一致性问题。

缓存是关于吞吐量而不是延迟的。

这不是为了减少用户的延迟。他们使用缓存来增加吞吐量并减轻数据库的负载。

  • 数据库无法处理负载,负载是数据库可以访问的 10 倍或 100 倍

他们可以容忍过时的数据

他们希望能够读取自己的写入

你可能认为这可以很容易地在应用程序中修复。有点惊讶的是,这并没有通过应用程序记住写入来修复。不清楚为什么他们以不同的方式解决了这个问题。

最终一致性已经足够好了

他们有巨大的扇出

他们提供的每个网页可能会生成数百次读取。有点令人惊讶。所以他们必须做一些技巧。并行发出读取请求。当单个服务器这样做时,它会收到一堆响应,交换机和 web 服务器中的缓冲区限制有限,因此如果他们不小心,他们可能会丢失数据包,从而在重试时降低性能。

性能

  • 论文中有很多关于一致性的内容

  • 但实际上他们迫切需要性能,这导致了一些技巧,也导致了一致性问题

  • 性能来自于能够并行提供大量的Get请求

实际上只有两种策略:

  • 分区数据

  • 复制数据

  • 他们两者都使用

如果键大致都一样受欢迎,分区就有效。否则,某些分区会更受欢迎并导致热点。复制有助于处理受欢迎键的需求。此外,复制有助于来自世界各地的请求。

你不能简单地在 web 应用程序服务器中缓存键,因为它们会很快填满内存,而且会重复存储大量数据。

他们处理的具体问题

每个集群都有一整套 memcache 服务器和一整套 web 服务器。每个 web 服务器都与其自己集群中的 memcache 服务器通信。

添加一个新的集群

有时,他们想要添加一个新的集群,这显然会有空的 memcache 服务器。

  • 新集群中的所有 web 服务器在每个请求上都会缺失,并且必须关闭并联系数据库,而数据库无法处理增加的负载

  • 新集群不会联系数据库,而是会联系其他集群的 memcache 服务器,直到新集群的缓存被预热

问: 添加新集群会给他们带来什么好处?而不是增加现有集群的大小?

  • 一个可能性是有一些非常受欢迎的键,因此过度分区一个集群对此没有帮助

  • 另一个可能性是通过添加新集群更容易添加更多的 memcache 服务器,因为数据移动问题

Memcache 服务器宕机

如果一个 memcache 服务器宕机,请求将被重定向到一个沟槽服务器。沟槽机器最初会错过很多,但至少它会为未来缓存结果。

作业问题:

问: 为什么写入时不使沟槽失效?

在写入时,数据库通常会向所有可能拥有该键的内存缓存服务器发送使其无效的消息。因此,大量的删除消息被发送到大量的内存缓存服务器。也许他们不想用所有的删除消息淹没槽沟服务器。

注意,槽沟键会在一定时间后过期,以应对键永远不变的情况。

如果槽沟服务器崩溃,会发生什么并不清楚。

问: 如果数据库服务器发送新值而不是使其无效,会不会更好。

  • 在内存缓存中缓存的内容可能不是数据库值,而可能是数据库值的某些函数,数据库层不知道。

    • 思考一下朋友列表在数据库中是如何存储的,与在内存缓存中是如何存储的。

针对雷鸣般的奔袭的租约

一个客户端向数据库发送更新请求,并使一个热门键失效。因此现在有很多客户端向内存缓存生成获取请求,但该键已被删除,这将导致大量的数据库查询,然后是大量的结果缓存。

如果内存缓存收到一个未找到的键的获取请求,它将在该键上设置一个租约,并说“你可以去询问数据库这个键,但请在 10 秒内完成。”当后续的获取请求到达时,它们会被告知“没有这样的键,但有另一个人正在获取它,所以请等待他而不是查询数据库”。

租约在 10 秒后或所有者设置键之后取消。

每个集群将生成一个单独的租约。

6.824 笔记

Scaling Memcache at Facebook, by Nishtala et al, NSDI 2013

why are we reading this paper?
  it's an experience paper, not about new ideas/techniques
  three ways to read it:
    cautionary tale of problems from not taking consistency seriously
    impressive story of super high capacity from mostly-off-the-shelf s/w
    fundamental struggle between performance and consistency
  we can argue with their design, but not their success

how do web sites scale up with growing load?
  a typical story of evolution over time:
  1\. one machine, web server, application, DB
     DB stores on disk, crash recovery, transactions, SQL
     application queries DB, formats, HTML, &c
     but the load grows, your PHP application takes too much CPU time
  2\. many web FEs, one shared DB
     an easy change, since web server + app already separate from storage
     FEs are stateless, all sharing (and concurrency control) via DB
     but the load grows; add more FEs; soon single DB server is bottleneck
  3\. many web FEs, data sharded over cluster of DBs
     partition data by key over the DBs
       app looks at key (e.g. user), chooses the right DB
     good DB parallelism if no data is super-popular
     painful -- cross-shard transactions and queries probably don't work
       hard to partition too finely
     but DBs are slow, even for reads, why not cache read requests?
  4\. many web FEs, many caches for reads, many DBs for writes
     cost-effective b/c read-heavy and memcached 10x faster than a DB
       memcached just an in-memory hash table, very simple
     complex b/c DB and memcacheds can get out of sync
     (next bottleneck will be DB writes -- hard to solve)

the big facebook infrastructure picture
  lots of users, friend lists, status, posts, likes, photos
    fresh/consistent data apparently not critical
    because humans are tolerant?
  high load: billions of operations per second
    that's 10,000x the throughput of one DB server
  multiple data centers (at least west and east coast)
  each data center -- "region":
    "real" data sharded over MySQL DBs
    memcached layer (mc)
    web servers (clients of memcached)
  each data center's DBs contain full replica
  west coast is master, others are slaves via MySQL async log replication

how do FB apps use mc?
  read:
    v = get(k) (computes hash(k) to choose mc server)
    if v is nil {
      v = fetch from DB
      set(k, v)
    }
  write:
    v = new value
    send k,v to DB
    delete(k)
  application determines relationship of mc to DB
    mc doesn't know anything about DB
  FB uses mc as a "look-aside" cache
    real data is in the DB
    cached value (if any) should be same as DB

what does FB store in mc?
  paper does not say
  maybe userID -> name; userID -> friend list; postID -> text; URL -> likes
  basically copies of data from DB

paper lessons:
  look-aside is much trickier than it looks -- consistency
    paper is trying to integrate mutually-oblivious storage layers
  cache is critical:
    not really about reducing user-visible delay
    mostly about surviving huge load!
    cache misses and failures can create intolerable DB load
  they can tolerate modest staleness: no freshness guarantee
  stale data nevertheless a big headache
    want to avoid unbounded staleness (e.g. missing a delete() entirely)
    want read-your-own-writes
    each performance fix brings a new source of staleness
  huge "fan-out" => parallel fetch, in-cast congestion

let's talk about performance first
  majority of paper is about avoiding stale data
  but staleness only arose from performance design

performance comes from parallel get()s by many mc servers
  driven by parallel processing of HTTP requests by many web servers
  two basic parallel strategies for storage: partition vs replication

will partition or replication yield most mc throughput?
  partition: server i, key k -> mc server hash(k)
  replicate: server i, key k -> mc server hash(i)
  partition is more memory efficient (one copy of each k/v)
  partition works well if no key is very popular
  partition forces each web server to talk to many mc servers (overhead)
  replication works better if a few keys are very popular

performance and regions (Section 5)

Q: what is the point of regions -- multiple complete replicas?
   lower RTT to users (east coast, west coast)
   parallel reads of popular data due to replication
   (note DB replicas help only read performance, no write performance)
   maybe hot replica for main site failure?

Q: why not partition users over regions?
   i.e. why not east-coast users' data in east-coast region, &c
   social net -> not much locality
   very different from e.g. e-mail

Q: why OK performance despite all writes forced to go to the master region?
   writes would need to be sent to all regions anyway -- replicas
   users probably wait for round-trip to update DB in master region
     only 100ms, not so bad
   users do not wait for all effects of writes to finish
     i.e. for all stale cached values to be deleted

performance within a region (Section 4)

multiple mc clusters *within* each region
  cluster == complete set of mc cache servers
    i.e. a replica, at least of cached data

why multiple clusters per region?
  why not add more and more mc servers to a single cluster?
  1\. adding mc servers to cluster doesn't help single popular keys
     replicating (one copy per cluster) does help
  2\. more mcs in cluster -> each client req talks to more servers
     and more in-cast congestion at requesting web servers
     client requests fetch 20 to 500 keys! over many mc servers
     MUST request them in parallel (otherwise total latency too large)
     so all replies come back at the same time
     network switches, NIC run out of buffers
  3\. hard to build network for single big cluster
     uniform client/server access
     so cross-section b/w must be large -- expensive
     two clusters -> 1/2 the cross-section b/w

but -- replicating is a waste of RAM for less-popular items
  "regional pool" shared by all clusters
  unpopular objects (no need for many copies)
  decided by *type* of object
  frees RAM to replicate more popular objects

bringing up new mc cluster was a serious performance problem
  new cluster has 0% hit rate
  if clients use it, will generate big spike in DB load
    if ordinarily 1% miss rate, and (let's say) 2 clusters,
      adding "cold" third cluster will causes misses for 33% of ops.
    i.e. 30x spike in DB load!
  thus the clients of new cluster first get() from existing cluster (4.3)
    and set() into new cluster
    basically lazy copy of existing cluster to new cluster
  better 2x load on existing cluster than 30x load on DB

important practical networking problems:
  n² TCP connections is too much state
    thus UDP for client get()s
  UDP is not reliable or ordered
    thus TCP for client set()s
    and mcrouter to reduce n in n²
  small request per packet is not efficient (for TCP or UDP)
    per-packet overhead (interrupt &c) is too high
    thus mcrouter batches many requests into each packet

mc server failure?
  can't have DB servers handle the misses -- too much load
  can't shift load to one other mc server -- too much
  can't re-partition all data -- time consuming
  Gutter -- pool of idle servers, clients only use after mc server fails

The Question:
  why don't clients send invalidates to Gutter servers?
  my guess: would double delete() traffic
    and send too many delete()s to small gutter pool
    since any key might be in the gutter pool

thundering herd
  one client updates DB and delete()s a key
  lots of clients get() but miss
    they all fetch from DB
    they all set()
  not good: needless DB load
  mc gives just the first missing client a "lease"
    lease = permission to refresh from DB
    mc tells others "try get() again in a few milliseconds"
  effect: only one client reads the DB and does set()
    others re-try get() later and hopefully hit

let's talk about consistency now

the big truth
  hard to get both consistency (== freshness) and performance
  performance for reads = many copies
  many copies = hard to keep them equal

what is their consistency goal?
  *not* read sees latest write
    since not guaranteed across clusters
  more like "not more than a few seconds stale"
    i.e. eventual
  *and* writers see their own writes
    read-your-own-writes is a big driving force

first, how are DB replicas kept consistent across regions?
  one region is master
  master DBs distribute log of updates to DBs in slave regions
  slave DBs apply
  slave DBs are complete replicas (not caches)
  DB replication delay can be considerable (many seconds)

how do we feel about the consistency of the DB replication scheme?
  good: eventual consistency, b/c single ordered write stream
  bad: longish replication delay -> stale reads

how do they keep mc content consistent w/ DB content?
  1\. DBs send invalidates (delete()s) to all mc servers that might cache
     + Do they wait for ACK? I'm guessing no.
  2\. writing client also invalidates mc in local cluster
     for read-your-writes

why did they have consistency problems in mc?
  client code to copy DB to mc wasn't atomic:
    1\. writes: DB update ... mc delete()
    2\. read miss: DB read ... mc set()
  so *concurrent* clients had races

what were the races and fixes?

Race 1: one client's cached get(k) replaces another client's updated k
  k not in cache
  C1: MC::get(k), misses
  C1: v = read k from DB
    C2: updates k in DB
    C2: and DB calls MC::delete(k) -- k is not cached, so does nothing
  C1: set(k, v)
  now mc has stale data, delete(k) has already happened
  will stay stale indefinitely, until key is next written
  solved with leases -- C1 gets a lease, but C2's delete() invalidates lease,
    so mc ignores C1's set
    key still missing, so next reader will refresh it from DB

Race 2: updating(k) in cold cluster, but getting stale k from warm cluster 
  during cold cluster warm-up
  remember clients try get() in warm cluster, copy to cold cluster
  k starts with value v1
  C1: updates k to v2 in DB
  C1: delete(k) -- in cold cluster
  C2: get(k), miss -- in cold cluster
  C2: v1 = get(k) from warm cluster, hits
  C2: set(k, v1) into cold cluster
  now mc has stale v1, but delete() has already happened
    will stay stale indefinitely, until key is next written
  solved with two-second hold-off, just used on cold clusters
    after C1 delete(), cold ignores set()s for two seconds
    by then, delete() will propagate via DB to warm cluster

Race 3: writing to master region, but reading stale from local
  k starts with value v1
  C1: is in a slave region
  C1: updates k=v2 in master DB
  C1: delete(k) -- local region
  C1: get(k), miss
  C1: read local DB  -- sees v1, not v2!
  later, v2 arrives from master DB
  solved by "remote mark"
    C1 delete() marks key "remote"
    get()/miss yields "remote"
      tells C1 to read from *master* region
    "remote" cleared when new data arrives from master region

Q: aren't all these problems caused by clients copying DB data to mc?
   why not instead have DB send new values to mc, so clients only read mc?
     then there would be no racing client updates &c, just ordered writes
A:
  1\. DB doesn't generally know how to compute values for mc
     generally client app code computes them from DB results,
       i.e. mc content is often not simply a literal DB record
  2\. would increase read-your-own writes delay
  3\. DB doesn't know what's cached, would end up sending lots
     of values for keys that aren't cached

PNUTS does take this alternate approach of master-updates-all-copies

FB/mc lessons for storage system designers?
  cache is vital to throughput survival, not just a latency tweak
  need flexible tools for controlling partition vs replication
  need better ideas for integrating storage layers with consistency 

PNUTS。

6.824 2015 年讲座 17:PNUTS。

注意: 这些讲义内容稍作修改自 2015 年春季的 6.824 课程网站 上发布的内容。

PNUTS。

  • 解决相同问题的解决方案 Spanner 和 memcached 解决了。

  • PNUTS 比 Facebook 的 memcache 设计更有原则性。

    • "它实际上是经过设计的"。
  • 使读取快速。

  • 优势:由于复制,Web 应用能够进行快速本地读取。

  • 缺点:写入会很慢,因为它们需要复制。

  • 因为写入必须分布到所有区域,所以在写入发生和更新实际传播之间会有一个基本延迟。

    • => 可能导致过期读取。
  • 如果有数据可能被并发客户端更新,那么多次写入就会有问题。

    • 需要所有区域以相同的顺序看到我们的写入。

图表:。

Region R1                        Region R2
---------                        ---------

 W1 Mesage broker                 W1 Message broker
 W2     (replicated)              W2     (replicated)
 W3                               W3
 ..         Tablet controller     ..         Tablet controller
                (replicated)                     (replicated)

    Router1 Router2 ...              Router1 Router2 ...     

    SU1 SU2 SU3 ...                  SU1 SU2 SU3 ... 
  • 每个区域都有自己的一组 Web 服务器。

  • 每个区域都存储所有数据。

  • 每个区域中的每个表都在存储单元(SUs)之间分区。

  • 路由器知道分区。

  • 每个 SU 都有一个磁盘。

更新。

  • 在 PNUTS 中,每条记录都有自己的主区域,所有写入都必须通过它进行。

    • 与 Facebook 的 memcache 不同,他们有一个 所有 记录的主区域。

    • 在 PNUTS 中,每个记录都有一个不同的主人。

    • 注意:记录只是表中的一行(并有一个额外的字段存储其主人)。

  • 更新远离用户的区域中的记录将花费更长的时间。

  • Web 服务器如何知道要将更新发送到哪里?

    • 联系一个路由器。

    • 路由器查看密钥,知道它存储在比如说 SU3 中。

    • 从 SU3 获取信息,了解不同区域 r2 具有主副本。

      • 不知道 r2 处的 SU 中记录的位置。
    • 联系 r2 中的一个路由器。

    • 路由器告诉您要将其存储在的 SU。

    • 然后 SU 需要将更新发送到所有其他区域。

    • SU 将更新发送到消息代理。

      • 不清楚 SU 是否在自己的磁盘上应用更新之前。
    • 消息代理将更新的副本写入磁盘,因为它正在 承诺 实际发送更新到每个地方。

      • 很重要,因为我们不希望失败的服务器导致更新部分传播。
    • MB 将它发送到其他站点的其他 MB。

    • 某种程度上,Web 应用需要找出写入何时完成。

      • 不清楚谁发送 ACK 回来。

      • 看起来 MB 一旦提交更新到磁盘,就会立即回复给 Web 服务器应用程序。

    • 异步写入,因为从 Web 应用的 POV 来看,写入在 MB 将其写入其磁盘时已完成。

    • MB 为什么不是瓶颈?它必须写入很多东西:

      • 不同的应用程序有不同的消息代理。

      • MB 可能能够更多地批处理写入。

      • 或许 MB 的写入也比普通数据库写入要简单,普通数据库写入需要修改 B 树,可能要经过文件系统等。

    • 因为他们将所有写入通过一些 MB 进行汇集,他们获得了一些写入的语义。

写入语义。

对单个记录的写入顺序。

Name        Where       What
----        -----       ----
Alice       home        asleep
Bob 
  • 爱丽丝写下了一个有 3 列(姓名,地点,内容)的记录。

  • 爱丽丝的应用程序说write(what=awake)

    • 写入通过 PNUTS 进行
  • 爱丽丝的应用程序说write(where=work)

    • 写入通过 PNUTS 进行
  • PNUTS 提供的有用语义

    • 不同区域的其他人可能会看到

      • 爱丽丝在家里睡着了

      • 爱丽丝在家里醒着

      • 爱丽丝在工作时醒着

    • 其他人不会看到与写入顺序不一致的记录视图

      • 爱丽丝在工作时睡着了
    • PNUTS 提供的主要一致性语义

    • 通过 MBs 对写入进行排序的结果

    • 论文将此称为每个记录的时间线一致性

    • 注意他们的模型限制了他们只能在单个记录基础上进行事务处理

什么时候您会关心陈旧数据?

  • 在将某物添加到购物车后,您会期望在那里看到它

读取 vs. 陈旧性

 read-any(key) -> fast read, just executes the read on the SU and does
                   not wait for any writes to propagate

    read-critical(key, ver) -> returns the read record where ver(record) >= ver
     - useful for reading your own writes
     - true when you have one webpage in a single tab
     - if you update your shopping cart in one tab, then the other tab
       will not be aware of that version number from the first tab

    read-latest(key) -> will always go to the master copy and read the latest
                      data there 

写入,原子更新

例如:在记录中递增一个计数器

 test-and-set-write(ver, key, newvalue) -> always gets sent to the master
        region for the key. look at the version and if it matches provided
        one then update the record with the new value

        // implementing v[k]++
        while true:
            (x, v) = read-latest(k)
            if test-and-set-write(k, v, x+1)
                break 

今日问题

爱丽丝春假回来后,她:

  • 将她妈妈从 ACL 中移除

  • 春假照片发布

由于无序写入,她妈妈能看到她的照片吗?

如果爱丽丝把她妈妈能看到的所有照片放在一个记录中,那就不行。

Alice   |   ACL     | List of photos
-------- ----------- ----------------
            mom         p7, p99 

假设她妈妈正在执行的代码在进行检查时读取完整记录(ACL + 照片),而不是先读取 ACL,等一会儿再读取照片

失败

如果 Web 应用服务器在进行一系列写操作时失败,那么只有部分信息会被写入 PNUTS,可能导致数据损坏。

  • 没有多个写操作的事务

如果 SU 崩溃并重新启动,它可以从磁盘中恢复,MB 可以继续重试

当 SU 失去其磁盘时会发生什么?它需要恢复数据。

  • 论文称 SU 将从另一个区域的 SU 克隆其数据

    • 主要挑战在于 MBs 向正在复制的记录发送更新

    • 更新要么发送到副本源,要么目标副本记住更新

    • 最终他们都需要在最后更新

性能

评估主要集中在延迟上,而不是吞吐量。也许这是特定于他们需求的。

不清楚他们如何支持只能每秒进行数百次写操作的 MBs 来满足数百万用户。

为什么他们在进行本地更新时需要 75 毫秒,而所有人都在同一区域?

  • 计算,磁盘,网络?

  • 对于数据库来说,75 毫秒的写入时间是巨大的

6.824 笔记

Brian F. Cooper, Raghu Ramakrishnan, Utkarsh Srivastava, Adam Silberstein, Philip Bohannon, Hans-Arno Jacobsen, Nick Puz, Daniel Weaver 和 Ramana Yerneni. PNUTS: 雅虎的托管数据服务平台。VLDB 会议论文集,2008 年。

为什么这篇论文?

  • 与 Facebook/memcache 论文具有相同的基本目标,但设计更有原则性

  • 多区域非常具有挑战性--100 毫秒的网络延迟

  • 一种在一致性和性能之间的明显权衡

PNUTS 的总体目标是什么?

图表:

[world, browsers, data centers] 
  • 整体故事与 Spanner 和 Facebook/memcache 类似

  • 遍布世界各地的数据中心(“区域”)

  • Web 应用程序,例如邮件,购物,社交网络

    • 每个应用程序可能在所有区域运行
  • PNUTS 为应用程序保留状态

    • 每个用户:个人资料,购物车,好友列表

    • 每个项目:图书流行度,用户评论

  • 应用程序可能需要任何数据中心的任何数据片段

  • 需要处理大量并发更新到不同数据的情况

    • 例如,许多用户必须能够同时向购物车添加项目,因此有数千个 PNUTS 服务器
  • 数千台服务器 => 崩溃可能频繁发生

概述

图表:

3 regions, browsers, web apps, tablet ctlrs, routers, storage units, MBs] 
  • 每个区域都有所有数据

  • 每个表按键在存储单元上分区

    • 平板服务器 + 路由器知道分区计划

为什么在多个区域复制所有数据的副本?

  • 多个区域 -> 每个用户的数据地理位置接近用户

  • 多个完整副本 -> 可能在整个区域故障时幸存

  • 完整副本 -> 快速读取任何内容

    • 因为一些数据被许多用户/许多区域使用

    • 一旦有了多个区域,快速读取非常重要

每个区域复制的缺点是什么?

  • 更新将会很慢,需要联系每个区域

  • 本地读取可能会过时

  • 来自多个区域的更新需要进行排序

    • 保持副本相同

    • 避免顺序异常

    • 不要丢失更新(例如用于计数器的读取-修改-写入)

  • 对于他们的用途,磁盘空间可能不是问题

数据和查询模型是什么?

  • 基本上是键/值

  • 读/写可能按列进行

    • 因此写入可能只替换一个列,而不是整个记录
  • 有序表的范围扫描

更新是如何工作的?

  • 应用服务器收到 Web 请求,需要在 PNUTS 中写入数据

  • 需要更新每个区域!

  • 为什么不让应用程序逻辑发送更新到每个区域?

    • 如果应用程序在更新了一些区域后崩溃会怎样?

    • 如果对同一记录进行并发更新会怎样?

PNUTS 为每个记录都有一个“记录主节点”

  • 所有更新必须通过该区域进行

    • 每个记录都有一个隐藏列指示记录主节点的区域
  • 负责存储单元按记录逐个执行更新

  • 告诉 MB 广播更新到所有区域

  • 每个记录的主节点可能比 Facebook/memcache 主节点区域更好

因此完整的更新故事(一些猜测):

应用程序想要更新记录的某些列,知道键

  1. 应用程序发送键和更新到本地 SU1

  2. SU1 查找键的记录主节点:SI2

  3. SU1 发送更新请求到 SI2 的路由器

  4. SI2 的路由器将更新转发给本地 SU2 以获取键

  5. SU2 发送更新到本地消息代理(MB)

  6. MB 存储在磁盘上 + 备份 MB,将版本号发送给原始应用程序,MB 如何知道版本号?也许是 SU2 告诉它,或者可能是 SU2(而不是 MB)回复给原始应用程序

  7. MB 发送更新到每个区域的路由器

  8. 每个区域更新本地副本

谜题:

  • 3.2.1 表示 MB 是提交点

    • 即 MB 写入两个磁盘上的日志,不断尝试传递,为什么 MB 磁盘不是一个糟糕的瓶颈?
  • 更新是先到 MB 还是 SU2?还是 SU2 然后 MB?还是 SU2,MB,SU2?

    • 可能是 MB 然后 SU2,因为 MB 是提交点

    • 可能是 SU2 然后 MB,因为 SU2 必须检查它是否是记录的主节点,也许选择新的版本号,尽管可能不需要

  • 谁回复客户端并附带新的版本号?

所有写入都是多区域的,因此很慢 -- 为什么这样做有意义?

  • 应用程序等待 MB 提交但不等待传播(“异步”)

  • 主节点可能是本地的(他们声称 80%的时间是这样)

    • 所以 MB 提交通常会很快

    • 应用/用户往往会很快看到自己的写入

  • 仍然,如果主节点是远程的,评估会说 300 毫秒!

  • 缺点:非主节点区域的读者可能会看到过时的数据

只读查询如何执行?

  • 多种类型的读取(第 2.2 节)——为什么?

  • 应用程序可以选择一致性的方式

  • read-any(k)

    • 从本地 SU 读取

    • 可能返回过时的数据(即使你刚刚写入!)

    • 为什么:应用需要速度但不关心新鲜度

  • read-critical(k, required_version)

    • 如果本地 SU 具有 vers >= required_version,则可能从本地 SU 读取

    • 否则从主节点 SU 读取?

    • 为什么:应用需要看到自己的写入

  • read-latest(k)

    • 总是从主节点 SU 读取(? "如果本地副本太陈旧")

    • 如果主节点是远程的,速度会慢!

    • 为什么:应用需要新鲜数据

如果应用需要递增存储在记录中的计数器怎么办?

  • 应用程序读取旧值,本地增加,写入新值

  • 如果本地读取产生了过时的数据怎么办?

  • 如果读取是 OK 的,但并发更新呢?

test-and-set-write(version#, new value) 为您提供对一条记录的原子更新

  • 如果当前版本号不等于版本号,则主节点拒绝写入

  • 所以如果并发更新,一个会失败并重试

TestAndSet 示例:

 while(1):
    (x, ver) = read-latest(k)
    if(t-a-s-w(k, ver, x+1))
      break 

这个问题

  • PNUTS 如何应对示例 1(第 2 页)?

  • 最初 Alice 的母亲在 Alice 的 ACL 中,所以母亲可以看到照片

    1. Alice 从 ACL 中移除她的母亲

    2. Alice 发布春假照片

  • 她的母亲可以看到更新#2 但看不到更新#1 吗?

    • 特别是如果母亲使用的区域与 Alice 不同,或者如果 Alice 从不同的区域进行更新
  • ACL 和照片列表必须在同一条记录中

    • 因为 PNUTS 仅保证对同一条记录的更新顺序
  • Alice 按顺序将更新发送到她记录的主区域

    • 主节点区域按顺序通过 MB 广播

    • MB 告诉其他区域按顺序应用更新

  • 如果 Alice 的母亲怎么办:

    • 读取旧的 ACL,其中包括母亲

    • 读取新的照片列表

    • 答案:只需读取 Alice 的记录一次,包含 ACL 和照片列表

      • 如果记录没有新的 ACL,那么顺序就说它也不能有新的照片
  • 存储系统如何会出现这样的问题?

    • 没有通过单一主节点进行排序(例如 Dynamo)

如果没有故障,如何更改记录的主节点?

  • 例如 我从波士顿搬到洛杉矶

  • 可能只需通过旧主节点更新记录?

    • 因为主区域的 ID 存储在记录中
  • 旧主节点通过 MB 宣布更改

  • 几次后续更新可能会发送到旧主节点

    • 它会拒绝它们,应用程序重试并找到新的主节点吗?

如果我们想要进行银行转账怎么办?

  • 从一个账户(记录)到另一个

  • t-a-s-w 可以用于这个吗?

  • 多记录更新不是原子的

    • 其他读者可以看到中间状态

    • 其他写入者不被锁定

  • 多记录读取不是原子的

    • 可能在转账之前读取一个账户,之后读取另一个账户

Web 应用程序缺乏通用事务是一个问题吗?

  • 如果程序员知道要期望它,可能不会

如何容忍故障?

应用服务器在更新集合过程中崩溃

  • 不是一个事务,所以只有一些写入会发生

  • 但主 SU/MB 要么得到了每次写入,要么没有

    • 因此每次写入都会在所有地区发生,或者都不发生

SU 短暂宕机,或网络暂时中断/丢包

  • (我猜测,可能错误)

  • MB 会不断尝试直到 SU 确认

    • SU 在安全写入磁盘之前不应该发送 ACK

SU 丢失磁盘内容,或不会自动重启

  • 应用程序可以从远程地区读取吗?

    • 论文没有提到
  • 需要从其他地区的 SU 恢复磁盘内容

    1. 订阅 MB 订阅,并暂时保存它们

    2. 从另一个地区的 SU 复制内容

    3. 重放保存的 MB 更新

  • 谜题:

    • 如何确保我们没有错过此 SU 的任��� MB 更新?

      • 例如,订阅 MB 在时间=100,但源 SU 只看到了 90?
    • 会重放应用更新两次吗?这有害吗?

    • 论文提到通过 MB 发送检查点消息

      • 可能在检查点到达时获取副本副本

      • 并且只在检查点之后重放

      • 但没有多个地区的 MB 流之间的排序

MB 在接受更新后崩溃

  • 在 ACK 之前将日志写入两个 MB 服务器的磁盘

  • 恢复查看日志,(重新)发送记录的消息

  • 记录主 SU 可能在 MB 在 ACK 之前崩溃时重新发送更新

    • 可能记录版本号将允许 SU 忽略重复

MB 是一个很好的想法

  • 原子性:更新所有副本,或者不更新

    • 而不是应用服务器更新副本(崩溃...)
  • 可靠:不断尝试,以应对暂时的 SU/地区故障

  • 异步:应用程序无需等待写入完成,适用于广域网

  • 有序:即使有多个写入者,也保持副本相同

记录的主地区失去网络连接

  • 其他地区可以指定替代 RM 吗?

    • 不:原始 RM 的 MB 可能已记录更新,只发送了一部分
  • 其他地区必须无限等待吗?是的

    • 这是有序更新/严格一致性的代价之一

评估

评估侧重于延迟和扩展性,而不是吞吐量

5.2:繁忙时插入所需时间

  • 取决于记录主的距离有多远

  • RM 本地:75.6 毫秒

  • RM 附近:131.5 毫秒

  • RM 其他海岸:315.5 毫秒

5.2 在测量什么?从什么到什么?

  • 可能 Web 服务器开始插入,RM 回复新版本?

  • 不是 MB 传播到所有地区的时间

    • 因为本地 RM 不会比远程 <

为什么是 75 毫秒?

75 毫秒是网络光速延迟吗?

  • 不:本地

75 毫秒主要是排队等待其他客户端操作吗?

  • 不:他们暗示 100 个客户端是不会导致延迟上升的最大值

5.2 结尾暗示 75 毫秒中有 40 毫秒在 SU 中

  • 为什么可能需要 40 毫秒?

    • 每个键/值是一个文件吗?

    • 创建一个文件需要 3 次磁盘写入(目录,inode,内容)?

  • 另外的 35 毫秒是什么?

    • MB 磁盘写入?

但“有序表”(MySQL/Innodb)只有 33 毫秒(不是 75)

  • 更接近我们期望的一个或两次磁盘写入

5.3 / 图 3:增加请求速率的影响

  • 对于 x 轴请求速率,y 轴延迟的图表我们期望什么?

    • 系统具有某种固有容量,例如总磁盘寻道/秒

    • 对于较低速率,恒定延迟

    • 对于更高的速率,队列迅速增长,平均延迟急剧增加

  • 爆炸应该接近硬件的最大容量

    • 例如 # 磁盘臂 / 寻道时间
  • 我们在图 3 中没有看到这一点

    • 显然他们的客户端无法产生太大的负载

    • 第 5.3 节结束时说客户端太慢了

    • 在 >= 75 ms/op 时,300 个客户端 -> 大约 4000/sec

  • 文本说最大可能的速率约为每秒 3000 次

    • 10% 的写入,因此每秒 300 次写入

    • 每个区域 5 个 SU,因此每秒 60 次写入/SU

    • 如果每次写入都执行随机磁盘 I/O,则大约合适

    • 但是您将需要大量的 SUs 来支持数百万活跃用户

回顾一下,PNUTS 的关键设计决策是什么?

  1. 在多个区域复制所有数据

    • 读取速度快,写入速度慢
  2. 松散的一致性 -- 陈旧的读取

    • 因为写入速度慢
  3. 只有单行事务带有测试和设置写入

  4. 将所有写入都按照主区域的顺序排序

    • 优点:保持副本相同,强制更新的序列顺序,易于理解

    • 缺点:慢,如果主区域断开连接则没有进展

下一步:Dynamo,一个非常不同的设计

  • 异步复制,但没有主服务器

  • 最终一致性

  • 总是允许更新

  • 如果网络分区,则版本树

  • 读者必须调和版本

亚马逊的 Dynamo 键值存储

6.824 2015 年第 18 讲:亚马逊的 Dynamo 键值存储

注意: 这些讲座笔记是从 2015 年春季 6.824 课程网站 上发布的笔记中稍作修改的。

Dynamo

  • 最终一致性

  • 比 PNUTS 或 Spanner 要不一致得多

  • 像 Cassandra 这样的成功开源项目是基于 Dynamo 的想法构建的

设计

  • 非常担心他们的服务水平协议(SLA)

    • 内部 SLA,比如 web 服务器和存储系统之间
  • 担心最坏情况的性能,而不是平均性能

  • 他们希望延迟的 99.9th 百分位数 < 300ms

    • 这个要求如何在设计中体现还不是很清楚

    • 为了满足这一点做了什么选择?

  • 应该处理不断发生的故障

    • 整个数据中心离线
  • 他们需要系统始终可写

    • => 没有单一的主节点

图表:

Datacenter

    Frontend            Dynamo server
    server
                        Dynamo server
    Frontend
    server              Dynamo server

                        Dynamo server 
  • 猜测:亚马逊有相当多的数据中心,没有一个是主要的或备份的,那么即使一个数据中心宕机,你的系统只会有一小部分宕机

    • 相比于在每个地方都复制每个记录,只在 2 或 3 个数据中心复制它要自然得多
  • Dynamo 的设计实际上并不是以数据中心为��向的

  • 与 PNUTS 的不同之处在于他们的设计中没有关于局部性的内容

    • 他们不担心这个问题:不会为了靠近每个客户端而复制数据
  • 他们需要广域网的运行非常良好

详情

  • 始终可写 => 没有单一的主节点 => 不同的服务器上有不同的 puts => 冲突的更新

  • puts 应该去哪里,gets 应该去哪里,以便它们可能看到 puts 写入的数据

一致性哈希

  • 你对键进行哈希,它告诉你应该从哪个服务器获取/存储它

  • 哈希输出空间是一个环/圆

  • 每个键的哈希值都是圆环上的一个点

  • 每个节点的哈希值也是一个点

  • => 一个键将在节点之间或在圆环上的一个节点之上

  • 在圆环上最接近键的节点(顺时针)是键的协调者

  • 如果键被复制 N 次,那么键后面的 N 个后继节点(顺时针)存储该键

  • 即使节点 ID 随机选择,一致性哈希并不能均匀地将键分布在节点之间

    • 一个节点上的键的数量与该节点与其前驱节点之间的间隙成比例

    • 间隙的分布相当宽

  • 为了弥补这一点,使用虚拟节点

    • 每个物理节点由一定数量的虚拟节点组成,与物理节点的性能/容量成比例

优先级列表:

  • 假设你有节点 A、B、C、D、E、F 和键 k,它的哈希值在节点 A 之前

  • 这个键 k 应该在 A、B 和 C 存储 3 个副本,如果 N = 3

  • 请求可能发送到第一个节点 A,该节点可能已经宕机

    • 或者请求可能发送到第一个节点 A,该节点会尝试在节点 B 和 C 上复制它,这两个节点可能已经宕机

    • => 这个第一个节点会在节点 D 和 E 上复制

    • => 可能有超过 N 个节点拥有数据

    • => 记住所有这些节点在一个优先级列表

  • 请求 k 发送到优先级列表中的第一个节点

  • 该节点作为请求的协调员,并在所有其他节点上读取/写入密钥

  • 懒惰的法定人数,

    • N协调员发送请求的节点数

    • R协调员等待获取数据返回的节点数

    • W协调员等待写入数据的节点数

  • 如果没有故障,协调员会像主节点一样运作

  • 如果出现故障,那么懒惰的法定人数确保数据被持久化,但可能会产生不一致性

  • 问题:因为没有真正的法定人数,获取可能会错过最近的放置

  • 你可以让节点 A、B、C 存储一些放置在陈旧数据上的放置,节点 D、E、F 存储另一些放置在数据上

    • D、E、F 中的协调员知道数据位置不对,并存储一个标志以指示应该将其传递给 A、B、C(hinted hand-off

冲突

  • 论文中的图 3

  • 当存在 2 个冲突版本时,客户端代码必须能够调和它们

  • dynamo 使用版本向量,就像 Ficus 一样

    • [a: 1] -> [a: 1, b: 3]

    • [a: 1] -> [a: 1, c: 3]

    • [a:1, b:3, c: 0][a:1, b:0, c:3]冲突

  • Dynamo 比 Bayou 弱

  • 两者都有解决冲突版本的方法

    • 在 dynamo 中,我们只有两个冲突的数据片段,但我们没有应用于状态的操作(比如从购物车中移除/添加某物)

    • Bayou 有操作日志

  • PNUTS 支持原子操作,比如test-and-set-write操作

    • Dynamo 中没有这样的东西

    • Dynamo 中唯一的方法是能够合并两个冲突的版本

性能

  • 关于版本向量始终要问的问题:当版本向量变得太大时会发生什么?

    • 删除了长时间修改过的节点的条目

      • v1 = [a:1, b:7] -> v1' = [b:7]

      • 会出什么问题?如果[b:7]更新为v2 = [b:8],那么v2将与v1冲突,即使它是直接从v1派生出来的,所以应用程序会获得一些错误合并

  • 他们喜欢能够调整N、R、W以获得不同的权衡

    • 标准的3,2,2

    • 3, 3, 1 ->快速写入但持久性不强,读取很少

    • 3, 1, 3 ->写入速度慢,但读取速度相当快

  • 平均延迟为 5-10ms,远小于 PNUTS 或 memcached

    • 相对于数据中心之间的光速来说太小了

    • 但数据中心在哪里以及工作负载是什么并不清楚

6.824 2015 原始笔记

Dynamo: Amazon's Highly Available Key-value Store
DeCandia et al, SOSP 2007

Why are we reading this paper?
  Database, eventually consistent, write any replica
    Like Ficus -- but a database! A surprising design.
  A real system: used for e.g. shopping cart at Amazon
  More available than PNUTS, Spanner, &c
  Less consistent than PNUTS, Spanner, &c
  Influential design; inspired e.g. Cassandra
  2007: before PNUTS, before Spanner

Their Obsessions
  SLA, e.g. 99.9th percentile of delay < 300 ms
  constant failures
  "data centers being destroyed by tornadoes"
  "always writeable"

Big picture
  [lots of data centers, Dynamo nodes]
  each item replicated at a few random nodes, by key hash

Why replicas at just a few sites? Why not replica at every site?
  with two data centers, site failure takes down 1/2 of nodes
    so need to be careful that *everything* replicated at *both* sites
  with 10 data centers, site failure affects small fraction of nodes
    so just need copies at a few sites

Consequences of mostly remote access (since no guaranteed local copy)
  most puts/gets may involve WAN traffic -- high delays
    maybe distinct Dynamo instances with limited geographical scope?
    paper quotes low average delays in graphs but does not explain
  more vulnerable to network failure than PNUTS
    again since no local copy

Consequences of "always writeable"
  always writeable => no master! must be able to write locally.
  always writeable + failures = conflicting versions

Idea #1: eventual consistency
  accept writes at any replica
  allow divergent replicas
  allow reads to see stale or conflicting data
  resolve multiple versions when failures go away
    latest version if no conflicting updates
    if conflicts, reader must merge and then write
  like Bayou and Ficus -- but in a DB

Unhappy consequences of eventual consistency
  May be no unique "latest version"
  Read can yield multiple conflicting versions
  Application must merge and resolve conflicts
  No atomic operations (e.g. no PNUTS test-and-set-write)

Idea #2: sloppy quorum
  try to get consistency benefits of single master if no failures
    but allows progress even if coordinator fails, which PNUTS does not
  when no failures, send reads/writes through single node
    the coordinator
    causes reads to see writes in the usual case
  but don't insist! allow reads/writes to any replica if failures

Where to place data -- consistent hashing
  [ring, and physical view of servers]
  node ID = random
  key ID = hash(key)
  coordinator: successor of key
    clients send puts/gets to coordinator
  replicas at successors -- "preference list"
  coordinator forwards puts (and gets...) to nodes on preference list

Why consistent hashing?
  Pro
    naturally somewhat balanced
    decentralized -- both lookup and join/leave
  Con (section 6.2)
    not really balanced (why not?), need virtual nodes
    hard to control placement (balancing popular keys, spread over sites)
    join/leave changes partition, requires data to shift

Failures
  Tension: temporary or permanent failure?
    node unreachable -- what to do?
    if temporary, store new puts elsewhere until node is available
    if permanent, need to make new replica of all content
  Dynamo itself treats all failures as temporary

Temporary failure handling: quorum
  goal: do not block waiting for unreachable nodes
  goal: put should always succeed
  goal: get should have high prob of seeing most recent put(s)
  quorum: R + W > N
    never wait for all N
    but R and W will overlap
    cuts tail off delay distribution and tolerates some failures
  N is first N *reachable* nodes in preference list
    each node pings successors to keep rough estimate of up/down
    "sloppy" quorum, since nodes may disagree on reachable
  sloppy quorum means R/W overlap *not guaranteed*

coordinator handling of put/get:
  sends put/get to first N reachable nodes, in parallel
  put: waits for W replies
  get: waits for R replies
  if failures aren't too crazy, get will see all recent put versions

When might this quorum scheme *not* provide R/W intersection?

What if a put() leaves data far down the ring?
  after failures repaired, new data is beyond N?
  that server remembers a "hint" about where data really belongs
  forwards once real home is reachable
  also -- periodic "merkle tree" sync of key range

How can multiple versions arise?
  Maybe a node missed the latest write due to network problem
  So it has old data, should be superseded by newer put()s
  get() consults R, will likely see newer version as well as old

How can *conflicting* versions arise?
  N=3 R=2 W=2
  shopping cart, starts out empty ""
  preference list n1, n2, n3, n4
  client 1 wants to add item X
    get(cart) from n1, n2, yields ""
    n1 and n2 fail
    put(cart, "X") goes to n3, n4
  client 2 wants to delete X
    get(cart) from n3, n4, yields "X"
    put(cart, "") to n3, n4
  n1, n2 revive
  client 3 wants to add Y
    get(cart) from n1, n2 yields ""
    put(cart, "Y") to n1, n2
  client 3 wants to display cart
    get(cart) from n1, n3 yields two values!
      "X" and "Y"
      neither supersedes the other -- the put()s conflicted

How should clients resolve conflicts on read?
  Depends on the application
  Shopping basket: merge by taking union?
    Would un-delete item X
    Weaker than Bayou (which gets deletion right), but simpler
  Some apps probably can use latest wall-clock time
    E.g. if I'm updating my password
    Simpler for apps than merging
  Write the merged result back to Dynamo

How to detect whether two versions conflict?
  As opposed to a newer version superseding an older one
  If they are not bit-wise identical, must client always merge+write?
  We have seen this problem before...

Version vectors
  Example tree of versions:
    [a:1]
           [a:1,b:2]
    VVs indicate v1 supersedes v2
    Dynamo nodes automatically drop [a:1] in favor of [a:1,b:2]
  Example:
    [a:1]
           [a:1,b:2]
    [a:2]
    Client must merge

get(k) may return multiple versions, along with "context"
  and put(k, v, context)
  put context tells coordinator which versions this put supersedes/merges

Won't the VVs get big?
  Yes, but slowly, since key mostly served from same N nodes
  Dynamo deletes least-recently-updated entry if VV has > 10 elements

Impact of deleting a VV entry?
  won't realize one version subsumes another, will merge when not needed:
    put@b: [b:4]
    put@a: [a:3, b:4]
    forget b:4: [a:3]
    now, if you sync w/ [b:4], looks like a merge is required
  forgetting the oldest is clever
    since that's the element most likely to be present in other branches
    so if it's missing, forces a merge
    forgetting *newest* would erase evidence of recent difference

Is client merge of conflicting versions always possible?
  Suppose we're keeping a counter, x
  x starts out 0
  incremented twice
  but failures prevent clients from seeing each others' writes
  After heal, client sees two versions, both x=1
  What's the correct merge result?
  Can the client figure it out?

What if two clients concurrently write w/o failure?
  e.g. two clients add diff items to same cart at same time
  Each does get-modify-put
  They both see the same initial version
  And they both send put() to same coordinator
  Will coordinator create two versions with conflicting VVs?
    We want that outcome, otherwise one was thrown away
    Paper doesn't say, but coordinator could detect problem via put() context

Permanent server failures / additions?
  Admin manually modifies the list of servers
  System shuffles data around -- this takes a long time!

The Question:
  It takes a while for notice of added/deleted server to become known
    to all other servers. Does this cause trouble?
  Deleted server might get put()s meant for its replacement.
  Deleted server might receive get()s after missing some put()s.
  Added server might miss some put()s b/c not known to coordinator.
  Added server might serve get()s before fully initialized.
  Dynamo probably will do the right thing:
    Quorum likely causes get() to see fresh data as well as stale.
    Replica sync (4.7) will fix missed get()s.

Is the design inherently low delay?
  No: client may be forced to contact distant coordinator
  No: some of the R/W nodes may be distant, coordinator must wait

What parts of design are likely to help limit 99.9th pctile delay?
  This is a question about variance, not mean
  Bad news: waiting for multiple servers takes *max* of delays, not e.g. avg
  Good news: Dynamo only waits for W or R out of N
    cuts off tail of delay distribution
    e.g. if nodes have 1% chance of being busy with something else
    or if a few nodes are broken, network overloaded, &c

No real Eval section, only Experience

How does Amazon use Dynamo?
  shopping cart (merge)
  session info (maybe Recently Visited &c?) (most recent TS)
  product list (mostly r/o, replication for high read throughput)

They claim main advantage of Dynamo is flexible N, R, W
  What do you get by varying them?
  N-R-W
  3-2-2 : default, reasonable fast R/W, reasonable durability
  3-3-1 : fast W, slow R, not very durable, not useful?
  3-1-3 : fast R, slow W, durable
  3-3-3 : ??? reduce chance of R missing W?
  3-1-1 : not useful?

They had to fiddle with the partitioning / placement / load balance (6.2)
  Old scheme:
    Random choice of node ID meant new node had to split old nodes' ranges
    Which required expensive scans of on-disk DBs
  New scheme:
    Pre-determined set of Q evenly divided ranges
    Each node is coordinator for a few of them
    New node takes over a few entire ranges
    Store each range in a file, can xfer whole file

How useful is ability to have multiple versions? (6.3)
  I.e. how useful is eventual consistency
  This is a Big Question for them
  6.3 claims 0.001% of reads see divergent versions
    I believe they mean conflicting versions (not benign multiple versions)
    Is that a lot, or a little?
  So perhaps 0.001% of writes benefitted from always-writeable?
    I.e. would have blocked in primary/backup scheme?
  Very hard to guess:
    They hint that the problem was concurrent writers, for which
      better solution is single master
    But also maybe their measurement doesn't count situations where
      availability would have been worse if single master

Performance / throughput (Figure 4, 6.1)
  Figure 4 says average 10ms read, 20 ms writes
    the 20 ms must include a disk write
    10 ms probably includes waiting for R/W of N
  Figure 4 says 99.9th pctil is about 100 or 200 ms
    Why?
    "request load, object sizes, locality patterns"
    does this mean sometimes they had to wait for coast-coast msg? 

Puzzle: why are the average delays in Figure 4 and Table 2 so low?
  Implies they rarely wait for WAN delays
  But Section 6 says "multiple datacenters"
    you'd expect *most* coordinators and most nodes to be remote!
    Maybe all datacenters are near Seattle?

Wrap-up
  Big ideas:
    eventual consistency
    always writeable despite failures
    allow conflicting writes, client merges
  Awkward model for some applications (stale reads, merges)
    this is hard for us to tell from paper
  Maybe a good way to get high availability + no blocking on WAN
    but PNUTS master scheme implies Yahoo thinks not a problem
  No agreement on whether eventual consistency is good for storage systems 

HubSpot

6.824 2015 年第 19 讲:HubSpot

注意: 这些讲座笔记是从 2015 年春季 6.824 课程网站上发布的笔记稍作修改。

真实世界中的分布式系统

谁构建分布式系统:

  • SaaS 市场

    • 初创公司:CustomMade,Instagram,HubSpot

    • 成熟:Akamai,Facebook,Twitter

  • 企业市场

    • 初创公司:Basho(Riak),Infinio,Hadapt

    • 成熟:VMWare,Vertica

  • ...和研究生

高级组件:

  • 前端:负载均衡路由器

  • 处理程序,缓存,存储,业务服务

  • 基础服务:日志记录,更新,身份验证

低级组件:

  • RPC(语义,故障)

  • 协调(一致性,Paxos)

  • 持久性(序列化语义)

  • 缓存

  • 抽象(队列,作业,工作流)

构建这个东西

业务需求将影响规模和架构

  • 约会网站核心数据:OkCupid 使用 2 台强大的数据库服务器

  • 分析分布式数据库:Vertica/Netezza 集群大约有 100 个节点

  • 中型 SaaS 公司:HubSpot 使用大约 100 个单节点数据库或大约 10 个节点 HBase 集群

    • 主要是 MySQL
  • Akamai,Facebook,亚马逊:成千上万台机器

小型 SaaS 初创公司:

  • 早期最好的事情是弄清楚您是否有一个人们愿意购买的好主意

  • 通常使用 Heroku,Google App Engine,AWS,Joyent,CloudFoundry 等平台

中型 SaaS:

  • 需要比 PaaS 提供的更多控制

  • 规模可能使您能够更便宜地构建更好的解决方案

  • 开源解决方案可以帮助您

成熟 SaaS:

  • Jepsen 工具

  • "确保您的设计在规模变化 10 倍或 20 倍时仍能正常工作;对于 x 的正确解决方案通常不是 100 倍的最佳解决方案",Jeff Dean

如何考虑您的设计:

  • 了解您的系统需要做什么和语义

  • 了解工作负载规模,然后估计(L2 访问时间,网络延迟)并计划了解性能

运行这个东西

  • "遥测胜过事件记录"

    • 日志可能很难理解:讲述一个好故事很困难
  • 日志记录:第一道防线,不易扩展

    • 不同机器上的日志

    • 如果时间戳无用,因为时钟不同步怎么办

    • 许多与日志记录相关的工具

    • 以可查询格式保存日志数据通常非常有用

  • 监控,遥测,警报

    • 用时间和计数事件注释代码

    • 测量内存队列的大小或请求花费的时间,您可以对其进行计数

    • 可以在多个粒度上进行遥测,因此我们可以将长时间请求分解为更小的部分并准确定位问题

管理:命令和控制

  • 在课堂设置中,您不必设置一堆机器

  • 随着您的业务规模扩大,需要设置新的机器 => 必须自动化

  • 将配置与应用程序分开

  • HubSpot 使用类似 ZooKeeper 的系统,允许应用程序获取配置值

  • Java 中的 Maven 依赖关系

  • Jenkins 用于持续集成测试

测试

  • 自动化测试使验证代码的新更改变得容易

  • UI 测试可能会有点困难(模拟点击,在不同浏览器中有不同的布局)

    • 前端变化 => 必须更改测试吗?

团队

  • 人员:如何团结在一起构建事物

  • 类比:软件工程过程有点像一个具有不可靠组件的分布式系统。

    • 以某种方式必须按时构建可靠的软件
  • 必须照顾好你的员工:文化必须适合人们成长、学习和失败

过程

  • 瀑布流:先进行大规模设计,然后实施

  • 敏捷/Scrum:不知道整个解决方案,需要在设计上进行迭代

  • 看板:

  • 精益:

问题

  • 在快速变化的代码库上进行重大更改

    • 如果你分支然后合并你的更改,代码库的变化可能会发生很大

    • 可以尝试部署两个不同的分支,以便新分支可以在生产环境中进行测试

  • 随着增长,文化也会发生变化

    • 需要关注员工的文化和幸福感

    • 测量幸福感非常重要

    • 拥有小团队可能有助于人们拥有项目的所有权

Argus

6.824 2015 年第 20 讲:Argus

注意:这些讲义内容稍作修改,来自 6.824 课程网站 2015 年春季的帖子。

原子提交:两阶段提交

  • 如何使用两阶段提交处理分布式事务

  • Argus

你有一堆做不同事情的计算机(不是副本)。就像两台计算机,一台存储 A-L 的人的事件,另一台存储 M-Z 的人的事件。如果你想为 Alice 和 Mike 创建一个事件,你需要与两个服务器进行交互,并确保事件要么同时创建在两个服务器上,要么都不创建。

挑战在于 崩溃网络故障 会引入不确定性(由于崩溃或网络故障而不响应引起的问题?)

在 Ivy 和 TreadMarks 中,如果其中一台机器崩溃,它将无法恢复。我们还看到了 MapReduce 和 Spark,它们对崩溃恢复有一套处理机制。

代码:

schedule(u1 user, u2 user, t time):
    ok1 = reserve(u1, t)    # reserve for the 1st user
    ok2 = reserve(u2, t)    # reserve for the 2nd user

    # Tricky: if the 1st reserve succeeded and the 2nd didn't => trouble
    # We'd like to deal with this in the following way:

    if ok1 and ok2
        commit
    else
        abort

    # One bad way to make this work is to let the servers chit-chat and
    # make sure they both committed.
    # At no stage in a transaction like this can the servers finish
    #   - S1: I'll do it if you do it
    #   - S2: I'll do it if you do it
    #   - S1: I'll do it if you do it
    #   - S2: I'll do it if you do it
    # (sounds like the two generals problem?) 

想法 1:临时更改

reserve(u user, t time):
    if u[t] = free          # if user's calendar is free at time t
        tent[t] = taken     # ...then tenatively schedule
commit:
    copy tent[t] to u[t]
abort:
    discard tent[t] 

想法 2:单台机器/实体(事务协调器)决定

client           TC      A       B
    \-------------------->
    \---------------------------->
                         |       |
    <--------------------/       |
    <----------------------------/
                    ------------
    ----- GO ----> |            |
                 | |            |
    <------------/ |            |
                    ------------ 

特性:

  • 状态:未知、已提交、已中止

  • 如果任何人认为 "已提交",则没有人认为 "已中止"

  • 如果任何人认为 "已中止",则没有人认为 "已提交"

两阶段提交(2PC)

在真实的分布式数据库中经常使用。

client          TC          A           B
                    .
                    .
                    .
    ---- GO ---->                             --\
                   prepare                      |
                ------------>                   |
                ------------------------>       | Phase 1
                   yes/no   |           |       |
                <-----------/           |       |
                <-----------------------/     --/

                   commit/abort
                ------------>                 --\
                ------------------------>       | Phase 2
    commit/abort                                |
  <-------------                              --/ 

准备 询问 "你还活着并愿意提交此事务吗?"

  • 服务器可能会拒绝

  • 服务器可能无法访问

终止协议

  • 或许 TC 在等待一个或多个准备消息的确认时有一个超时

    • 此时,它可以中止事务,因为没有人开始提交(因为 TC 没有发送,因为它在等待是/否)
  • B 在等待准备消息时超时

    • => B 没有回复准备 => TC 没有向参与者发送提交 => TC 可以发送中止
  • 在拒绝准备后,B 在等待提交/中止时超时

    • => B 可以中止,因为它知道 TC 将会中止所有人
  • 在同意准备后,B 在等待提交/中止时超时

    • => B 对 TC 说了是,而 TC 可能已经收到了其他人的 (或没有) => 结果可以是提交或中止 => B 必须等待

    • 在某些幸运的情况下,如果 A 通过另一个信道告诉 BB 可能会决定中止/提交

这种等待会使得 2PC 不切实际吗?人们意见分歧?

重启怎么办?如果参与者之一对准备说了是,那么它必须跨重启或崩溃记住,以便能够完成事务(提交或中止)。

  • 在日历示例中,它还需要记住 tent[] 中的临时时间表

  • 额外说明:因为在图表中 TC 没有等待提交/中止的 ACK,参与者需要在事务周围保持其锁,以便在此事务完成之前不执行后续事务

如果 TC 在发送提交过程中崩溃会发生什么?

  • 它必须记住所有已提交/未提交的事务

与 Paxos 有相似之处吗?

  • Paxos 是通过复制构建高可用系统的一种方式(所有服务器都拥有所有数据并执行相同的操作)。

    • 即使一些服务器宕机,Paxos 系统也可以继续运行
  • 即使只有一个服务器宕机,2PC 也无法取得进展

    • 每个服务器都在执行不同的操作(希望每个服务器在事务中执行自己的部分)
  • 虽然 2PC 帮助一组服务器达成一致,但它不具备容错性或可用性(当服务器宕机时无法继续进行)。

  • 你可能认为可以通过 Paxos 来进行日历调度,让两个服务器就日程安排达成一致。然而,虽然就操作达成一致是可行的,但提交操作却不行。例如,如果一个服务器的用户在预定时间忙碌,那么它无法提交操作,而另一个可能可以。Paxos 无法解决这种冲突。

原子分布式事务:编写事务代码时不考虑其他可能正在进行的事务。

银行示例:

T1:
    addToBal(x, 1)
    addToBal(y, -1)

    # Need this to be a transaction to implement a transfer correctly

T2:
    tmp1 = getBal(x)
    tmp2 = getBal(y)

    print(tmp1, tmp2)

    # We cannot have the execution of T1 interleave with the execution of
    # T2\. T2 had better see both addToBal calls or no addToBal calls from T1 

这被称为可串行性:运行一系列事务的效果与它们按某种顺序运行的效果相同(不允许交错:执行 T1 的前半部分,执行 T2 的前半部分,完成 T1 的后半部分,完成 T2)。

实现事务的一种方式是在事务开始操作这些记录之前获取每个数据记录的锁,并在提交或中止之前一直持有这些锁。这被称为两阶段锁定

如果 T1 先获取 x 然后获取 y,而 T2 先获取 y 然后获取 x,就会发生死锁。例如,数据库系统有处理这种情况的方法:

  • 在获取锁时设置超时并重试

  • 只允许事务按特定顺序获取锁

  • 如果是单机设置,则执行死锁检测

没有人喜欢使用 2PC。

  • 当服务器在准备阶段回复“否”后超时等待提交/中止时,会出现等待/阻塞问题。

当参与者获取锁时,它们在网络中跨越多个 RTT 持有这些锁,因为你必须等待提交消息。

Argus

  • 令人兴奋的是,它试图将分布式系统编程的许多细节混杂在语言内部

  • 期望是为处理 RPC 失败提供一个清晰的解决方案。

  • Argus 建立了一个框架,可以清晰地处理 RPC 失败。

    • 执行所有必要的工作以回滚事务
  • Argus 必须了解数据才能进行回滚操作。

    • 它需要创建临时更新等等

6.824 笔记

6.824 2015 Lecture 20: Two-Phase Commmit

Topics:
  distributed commit, two-phase commit
  distributed transactions
  Argus -- language for distributed programming

Distributed commit:
  A bunch of computers are cooperating on some task, e.g. bank transfer
  Each computer has a different role, e.g. src and dst bank account
  Want to ensure atomicity: all execute, or none execute
    "distributed transaction"
  Challenges: crashes and network failures

Example:
  calendar system, each user has a calendar
  want to schedule meetings with multiple participants
  one server holds calendars of users A-M, another server holds N-Z
  [diagram: client, two servers]
  sched(u1, u2, t):
    begin_transaction
      ok1 = reserve(u1, t)
      ok2 = reserve(u2, t)
      if ok1 and ok2:
        commit
      else
        abort
    end_transaction
  the reserve() calls are RPCs to the two calendar servers
  We want both to reserve, or both not to reserve.
  What if 1st reserve() returns true, and then:
    2nd reserve() returns false (time not available)
    2nd reserve() doesn't return (lost RPC msg, u2's server crashes)
    2nd reserve() returns but then crashes
    client fails before 2nd reserve()
  We need a "distributed commit protocol"

Idea: tentative changes, later commit or undo (abort)
  reserve_handler(u, t):
    if u[t] is free:
      temp_u[t] = taken -- A TEMPORARY VERSION
      return true
    else:
      return false
  commit_handler():
    copy temp_u[t] to real u[t]
  abort_handler():
    discard temp_u[t]

Idea: single entity decides whether to commit
  to prevent any chance of disagreement
  let's call it the Transaction Coordinator (TC)
  [time diagram: client, TC, A, B]
  client sends RPCs to A, B
  on end_transaction, client sends "go" to TC
  TC/A/B execute distributed commit protocol...
  TC reports "commit" or "abort" to client

We want two properties for distributed commit protocol:
  TC, A, and B start in state "unknown"
    each can move to state "abort" or "commit"
    but then each never changes mind
  Correctness:
    if any commit, none abort
    if any abort, none commit
  Performance:
    (since doing nothing is correct...)
    if no failures, and A and B can commit, then commit.
    if failures, come to some conclusion ASAP.

We're going to develop a protocol called "two-phase commit"
  Used by distributed databases for multi-server transactions
  And by Spanner and Argus

Two-phase commit without failures:
  [time diagram: client, TC, A, B]
  client sends reserve() RPCs to A, B
  client sends "go" to TC
  TC sends "prepare" messages to A and B.
  A and B respond, saying whether they're willing to commit.
    Respond "yes" if haven't crashed, timed out, &c.
  If both say "yes", TC sends "commit" messages.
  If either says "no", TC sends "abort" messages.
  A/B "decide to commit" if they get a commit message.
    I.e. they actually modify the user's calendar.

Why is this correct so far?
  Neither can commit unless they both agreed.
  Crucial that neither changes mind after responding to prepare
    Not even if failure

What about failures?
  Network broken/lossy
  Server crashes
  Both visible as timeout when expecting a message.

Where do hosts wait for messages?
  1) TC waits for yes/no.
  2) A and B wait for prepare and commit/abort.

Termination protocol summary:
  TC t/o for yes/no -> abort
  B t/o for prepare, -> abort
  B t/o for commit/abort, B voted no -> abort
  B t/o for commit/abort, B voted yes -> block

TC timeout while waiting for yes/no from A/B.
  TC has not sent any "commit" messages.
  So TC can safely abort, and send "abort" messages.

A/B timeout while waiting for prepare from TC
  have not yet responded to prepare
  so can abort
  respond "no" to future prepare

A/B timeout while waiting for commit/abort from TC.
  Let's talk about just B (A is symmetric).
  If B voted "no", it can unilaterally abort.
  So what if B voted "yes"?
  Can B unilaterally decide to abort?
    No! TC might have gotten "yes" from both,
    and sent out "commit" to A, but crashed before sending to B.
    So then A would commit and B would abort: incorrect.
  B can't unilaterally commit, either:
    A might have voted "no".

If B voted "yes", it must "block": wait for TC decision.

What if B crashes and restarts?
  If B sent "yes" before crash, B must remember!
    --- this is today's question
  Can't change to "no" (and thus abort) after restart
  Since TC may have seen previous yes and told A to commit
  Thus:
    B must remember on disk before saying "yes", including modified data.
    B reboots, disk says "yes" but no "commit", must ask TC.
    If TC says "commit", copy modified data to real data.

What if TC crashes and restarts?
  If TC might have sent "commit" or "abort" before crash, TC must remember!
    And repeat that if anyone asks (i.e. if A/B/client didn't get msg).
    Thus TC must write "commit" to disk before sending commit msgs.
  Can't change mind since A/B/client have already acted.

This protocol is called "two-phase commit".
  What properties does it have?
  * All hosts that decide reach the same decision.
  * No commit unless everyone says "yes".
  * TC failure can make servers block until repair.

What about concurrent transactions?
  We realy want atomic distributed transactions,
    not just single atomic commit.
  x and y are bank balances
  x and y start out as $10
  T1 is doing a transfer of $1 from x to y
  T1:
    add(x, 1)  -- server A
    add(y, -1) -- server B
  T2:
    tmp1 = get(x)
    tmp2 = get(y)
    print tmp1, tmp2

Problem:
  what if T2 runs between the two add() RPCs?
  then T2 will print 11, 10
  money will have been created!
  T2 should print 10,10 or 9,11

The traditional approach is to provide "serializability"
  results should be as if transactions ran one at a time in some order
  either T1, then T2; or T2, then T1

Why serializability?
  it allows transaction code to ignore the possibility of concurrency
  just write the transaction to take system from one legal state to another
  internally, the transaction can temporarily violate invariants
    but serializability guarantess no-one will notice

One way to implement serializabilty is with "two-phase locking"
  this is what Argus does
  each database record has a lock
  the lock is stored at the server that stores the record
    no need for a central lock server
  each use of a record automatically acquires the record's lock
    thus add() handler implicitly acquires lock when it uses record x or y
  locks are held until *after* commit or abort 

Why hold locks until after commit/abort?
  why not release as soon as done with the record?
  e.g. why not have T2 release x's lock after first get()?
    T1 could then execute between T2's get()s
    T2 would print 10,9
    but that is not a serializable execution: neither T1;T2 nor T2;T1

2PC perspective
  Used in sharded DBs when a transaction uses data on multiple shards
  But it has a bad reputation:
    slow because of multiple phases / message exchanges
    locks are held over the prepare/commit exchanges
    TC crash can cause indefinite blocking, with locks held
  Thus usually used only in a single small domain
    E.g. not between banks, not between airlines, not over wide area

Paxos and two-phase commit solve different problems!
  Use Paxos to high availability by replicating
    i.e. to be able to operate when some servers are crashed
    the servers must have identical state
  Use 2PC when each participant does something different
    And *all* of them must do their part
  2PC does not help availability
    since all servers must be up to get anything done
  Paxos does not ensure that all servers do something
    since only a majority have to be alive

What if you want high availability *and* distributed commit?
  [diagram]
  Each "server" should be a Paxos-replicated service
  And the TC should be Paxos-replicated
  Run two-phase commit where each participant is a replicated service
  Then you can tolerate failures and still make progress
  This is what Spanner does (for update transactions)

Case study: Argus

Argus's big ideas:
  Language support for distributed programs
    Very cool: language abstracts away ugly parts of distrib systems
    Aimed at different servers doing different jobs, cooperating
  Easy fault tolerance:
    Transactional updates
    So crash results in entire transaction un-done, not partial update
  Easy persistence ("stable"):
    Ordinary variables automatically persisted to disk
    Automatic crash recovery
  Easy concurrency:
    Implicit locking of language objects
  Easy RPC model:
    Method calls transparently turned into RPCs
    RPC failure largely hidden via transactions, two-phase commit

We've seen the fundamental problem before
  What to do if *part* of a distributed computation crashes?
  IVY/Treadmarks had no answer
  MR/Spark could re-execute *part* of computation, for big data

Picture
  "guardian" is like an RPC server
    has state (variables) and handlers
  "handler" is an RPC handler
    reads and writes local variables
  "action" is a distributed atomic transaction
  action on A
    A RPC to B
      B RPC to C
    A RPC to D
  A finishes action
    prepare msgs to B, C, D
    commit msgs to B, C, D

The style is to send RPC to where the data is
  Not to fetch the data
  Argus is not a storage system

Look at bank example
  page 309 (and 306): bank transfer

Points to notice
  stable keyword (programmer never writes to disk &c)
  atomic keyword (programmer almost never locks/unlocks)
  enter topaction (in transfer)
  coenter (in transfer)
  RPCs are hidden (e.g. f.withdraw())
  RPC error handling hidden (just aborts)

what if deposit account doesn't exist?
  but f.withdraw(from) has already been called?
  how to un-do?
  what's the guardian state when withdraw() handler returns?
    lock, temporary version, just in memory

what if an audit runs during a transfer?
  how does the audit not see the tentative new balances?

if a guardian crashes and reboots, what happens to its locks?
  can it just forget about pre-crash locks?

subactions
  each RPC is actually a sub-action
  the RPC can fail or abort w/o aborting surrounding action
  this lets actions e.g. try one server, then another
  if RPC reply lost, subaction will abort, undo
    much cleaner than e.g. Go RPC

is Argus's implicit locking the right thing?
  very convenient!
  don't have to worry about forgetting to lock!
  (though deadlocks are easy)
  databases work (and worked) this way; it's a sucessful idea

is transactions + RPC + 2PC a good design point?
  programmability pro:
    very easy to get nice fault tolerance semantics
  performance con:
    lots of msgs and disk writes
    2PC and 2PL hold locks for a while, block if failure

is Argus's language integration the right thing?
  i.e. persisting and locking language objects
  it looks very convenient (and it is)
  but it turns out to be even more valuable have relational tables
    people like queries/joins/&c over tables, rows, columns
    that is, people like a storage abstraction!
  maybe there is a better language-based scheme waiting to be found 

乐观并发控制,Thor

6.824 2015 年第 21 讲:乐观并发控制,Thor

注意: 这些讲座笔记略有修改,来自 2015 年春季 6.824 课程网站 上发布的内容。

乐观并发控制

  • 想要构建具有高速、大规模和良好语义的稳定存储系统

    • 努力使它们全部实现
  • Thor 是另一个可能帮助我们构建这样一个系统的工具

图表:

 Client              Client
 -----------         -----------
| App       |       | App       |
 -----------         -----------     ......
| Thor cache|       | Thor cache|
|           |       |           | ------
 ----------- <-  --> -----------        \
   \ write x   \/                        \   read-set
    |          / invalidate messages      \  write-set
   \|/        /                            \
    .        /                             \/
  Server    /         Server             Validation service
 -----------         -----------         -----------
| App       |       | App       |       |           |
 -----------         -----------         -----------
| Data      |       | Data      |
|   A-M     |       |   N-Z     |
 -----------         ----------- 

如果你退后一步看,这看起来很像 Facebook/memcache 论文,其中数据被分片,客户端端使用缓存。

  • 想要支持并发事务

  • 缓存使事务变得棘手:可能从缓存中读取过时数据

  • 锁定可以解决我们所有的问题

    • 但是应用程序每次想要一个项目时都必须与服务器交谈 => 这违背了缓存的目的

Thor 使用乐观并发控制:应用程序继续前进,在缓存中读取和写入任何可用的数据。当事务想要提交时,必须与某个验证服务交谈,该服务将查看写入和读取发生的顺序。

  • 乐观是指事务正在执行,忽略其他事务,并希望它不会与任何事务冲突,稍后检查是否有冲突

验证方案 1

  • 假设有一个单独的验证服务器

  • 客户端发送用于验证的读取集和写入集是事务实际读取/写入的值

  • 该方案接受事务的描述,尝试所有可能的顺序,并查看是否有任何顺序导致顺序一致。

  • 验证器不知道事务在内部做了什么

例子 1:

x = y = z = 0

# Then, 4 tx's are run

T1: Rx0 Wx1
T2: Rz0 Wz9
T3: Ry1 Rx1
T4: Rx0 Wy1 

例如 T3,T1 不可能:

但是 T4 (Rx0 Wy1), T1 (Rx0 Wx1), T3 (Ry1 Rx1), T2(Rz0 Wz9) 是可能的:顺序与事务读取的值一致。

  • 注意:如果事务来自不同的机器(显然不共享缓存/通信),那么问题就是,T3 如何读取 x 的值为 1,当数据库初始化为 0 时?答案是“这只是一个虚构的例子:想象他们实际上确实共享了一个缓存。重点是看到验证器对事务进行排序,使它们一致”

  • 注意,T2 只读/写了 z => 与任何 T1, T3, T4 都没有冲突。

    • 在这样的情况下,OCC 的性能非常好

这个方案很棒,因为它允许我们在不锁定的情况下执行事务。

例子 2:

x = y = 0

T1: Rx0 Wx1
T2: Rx0 Wy=1
T3: Ry0 Rx=1 

这 3 个事务无法串行化:

  • T1 --- 在 --> T3 之前(t3 读取 x1,t1 写入 x1)

  • T3 --- 在 --> T2 之前(t3 读取 y0,t2 写入 y1)

  • T2 --- 在 --> T1 之前(t2 读取 x0,t1 写入 x1)

    • 循环!=> 无法串行化

任何 OCC 方案都需要问的另一件事是它是否能巧妙处理只读事务(一些方案可以)。

  • Thor 和今天讨论的方案确实需要验证只读事务

例子 3:

x = y = z = 0

T1 Wx1
T2 Rx1 Wy2
T3 Ry2 Rx0

T3 read x=0 => T3 comes before T1
T3 read y=2 => T3 comes after T2
T2 read x=1 => T2 comes after T1

  /---------\
 /          \/
T2 <- T3 <- T1 

如果我们为记录版本化,并确保只有只读 tx's 读取记录的相同版本 => 我们可以将其放置在该版本之后的序列化事务的任何位置 => 可以被序列化。

为什么不使用读-写锁?

  • x=x+1 这样的简单事务首先获取读锁,然后需要将其升级为写锁。

    • 如果您有两个这样的交易=>死锁,因为没有一个会释放其读锁,直到将其升级为写锁。

分布式验证

如果我们的数据分片存储在多个服务器上,那么服务器 1(A-M)可以只验证影响记录 A-M 的交易部分,而服务器 2 可以查看影响记录 N-Z 的部分。然后客户端可以使用两阶段提交(2PC)确保两个服务器都批准了交易。

但是像这样的天真实现不会起作用:

示例 2(来自之前的):

x on server 1
y on server 2

    sv1         sv2
  ---------- ---------
T1 Rx0 Wx1  |   
T2 Rx0      |   Wy1
T3 Rx1      |   Ry0

sv1: T2 T1 T3 (yes)
sv2: T3 T2 (yes) 

但是,结果是不正确的,因为验证器对不同的顺序说“是”。

要解决此问题,请参阅验证方案 2。

验证方案 2

使用时间戳构建一个可行的分布式验证方案

每当客户端希望提交事务时,服务器根据其本地时钟(松散与真实时间同步)为此事务选择时间戳。

验证仅检查时间戳暗示的顺序是否与事务执行的读取和写入一致。

示例 2(再次)

 sv1     |   sv2
T1@100: Rx0 Wx1       Rx0 Wx1   |
T2@110: Rx0 Wy1       Rx0       |   Wy1
T3@105: Ry0 Rx1       Rx1       |   Ry0
                    T1 T3 T2       T2 T3
                      (no)          (no) 

松散同步的时钟 => 必须准备好处理 T2 在时间戳显示其稍后运行时甚至在 T3 之前运行的情况。

时间戳给服务器唯一的能力就是能够就交易的顺序达成一致,以便它们不会对不同的顺序说“是”。

  • => 强制使用时间戳顺序(即使可能存在更好的顺序也不能搜索)。

    • 但是验证的分布式算法非常简单明了。

示例:

T1@100: Rx0 Wx1
T2@ 90: Rx1 Wx2  (assigned lower timestamp due to loose sync) 

在先前的方案中,这可能已经提交了(存在有效顺序 T1,T2),但是当前方案由于时间戳的原因将顺序限制为T2, T1

  • 当然,这只是一个性能问题。

麻烦在于,读取集和写入集查看值,以检查是否存在冲突的事务。这给验证器一些权力(能够提交更多的事务),但当值很大时却是不切实际的。

  • => 使用版本号而不是值。

示例:

T1: Wx1 v105
T2: Wx2 v106
T3: Wx1 v107
T4: Rx1 v105
-> aborted becauses T4 read stale version of x (105)
 -> however, even though it read the v105 it still got the same value as
    the freshest version v107 (the value 1) => unnecessary abort 

现在需要在每个记录旁边存储版本号。Thor 不想这样做。相反,Thor 会击落读取过时记录的事务,方法是在写入这些记录时发送使其失效的消息。已缓存这些记录的客户端随后可以丢弃它们。

6.824 笔记

Paper: Efficient Optimistic Concurrency Control using Loosely
Synchronized Clocks, by Adya, Gruber, Liskov and Maheshwari.

Why this paper?
  to look at optimistic concurrency control (OCC)
  OCC might help us get large scale, high speed, *and* good semantics

Thor overview
  [clients, client caches, servers A-M N-Z]
  data sharded over servers
  code runs in clients (not like Argus; not an RPC system)
  clients read/write DB records from servers
  clients cache data locally for fast access
    on client cache miss, fetch from server

Thor arrangement is fairly close to modern big web site habits
  clients, local fast cache, slower DB servers
  like Facebook/memcache paper
  but Thor has much better semantics

Thor programs use fully general transactions
  multi-operation
  serializable
  so can do bank xfers w/o losing money, &c

Client caching makes transactions tricky
  writes have to invalidatate cached copies
  how to cope with reads of stale cached data?
  how to cope with read-modify-write races?
  clients could lock before using each record
    but that's slow -- probably need to contact server
    wrecks the whole point of fast local caching in clients
    (though caching read locks might be OK, as in paper Eval)

Thor uses optimistic concurrency control (OCC)
  an idea from the early 1980s
  just read and write the local copy
    don't worry about other transactions until commit
  when transaction wants to commit:
    send read/write info to server for "validation"
    validation decides if OK to commit -- if serializable
    if yes, send invalidates to clients with cached copies of written records
    if no, abort, discard writes
  optimistic b/c hopes for no conflict
    if turns out to be true, fast!
    if false, validation can detect, but slow

What should validation do?
  it looks at what the executing transactions read and wrote
  decides if there's a serial execution order that would have gotten
    the same results as the actual concurrent execution
  there are many OCC validation algorithms!
    i will outline a few, leading up to Thor's

Validation scheme #1
  a single validation server
  clients tell validation server the read and write VALUES
    seen by each transaction that wants to commit
    "read set" and "write set"
  validation must decide:
    would the results be serializable if we let these
      transactions commit?
  scheme #1 shuffles the transactions, looking for a serial order
    in which each read sees the value written by the most
    recent write; if one exists, the execution was serializable.

Validation example 1:
  initially, x=0 y=0 z=0
  T1: Rx0 Wx1
  T2: Rz0 Wz9
  T3: Ry1 Rx1
  T4: Rx0 Wy1
  validation needs to decide if this execution (reads, writes)
    is equivalent to some serial order
  yes: one such order is T4, T1, T3, T2; says yes to all
    (really T2 can go anywhere)
  note this scheme is far more permissive than Thor's
    e.g. it allows transactions to see uncommitted writes

OCC is neat b/c transactions didn't need to lock!
  so they can run quickly from client caches
  just one msg exchange w/ validator per transaction
    not one locking exchange per record used
  OCC excellent for T2 which didn't conflict with anything
    we got lucky for T1 T3 T4, which do conflict

Validation example 2 -- sometimes must abort:
  initially, x=0 y=0
  T1: Rx0 Wx1
  T2: Rx0 Wy1
  T3: Ry0 Rx1
  values not consistent w/ any serial order!
    T1 -> T3 (via x)
    T3 -> T2 (via y)
    T2 -> T1 (via x)
    there's a cycle, so not the same as any serial execution
  perhaps T3 read a stale y=0 from cache
    or T2 read a style x=0 from cache
  in this case validation can abort one of them
    then others are OK to commit
  e.g. abort T2
    then T1, T3 is OK (but not T3, T1)

How should client handle abort?
  roll back the program (including writes to program variables)
  re-run from start of transaction
  hopefully won't be conflicts the second time
  OCC is best when conflicts are uncommon!

Do we need to validate read-only transactions?
  example:
    initially x=0 y=0
    T1: Wx1
    T2: Rx1 Wy2
    T3: Ry2 Rx0
  i.e. T3 read a stale x=0 from its cache, hadn't yet seen invalidate.
  need to validate in order to abort T3.
  other OCC schemes can avoid validating read-only transactions
    keep multiple versions -- but Thor and my schemes don't

Is OCC better than locking?
  yes, if few conflicts
    avoids lock msgs, clients don't have to wait for locks
  no, if many conflicts
    OCC aborts, must re-start, perhaps many times
    locking waits
  example: simultaneous increment
    locking:
      T1: Rx0 Wx1
      T2: -------Rx1  Wx2
    OCC:
      T1: Rx0 Wx1
      T2: Rx0 Wx1
      fast but wrong; must abort one

We really want *distributed* OCC validation
  split storage and validation load over servers
  each storage server sees only xactions that use its data
  each storage server validates just its part of the xaction
  two-phase commit (2PC) to check that they all say "yes"
    only really commit if all relevant servers say "yes"

Can we just distribute validation scheme #1?
  imagine server S1 knows about x, server S2 knows about y
  example 2 again
    T1: Rx0 Wx1
    T2: Rx0 Wy1
    T3: Ry0 Rx1
  S1 validates just x information:
    T1: Rx0 Wx1
    T2: Rx0
    T3: Rx1
    answer is "yes" (T2 T1 T3)
  S2 validates just y information:
    T2: Wy1
    T3: Ry0
    answer is "yes" (T3 T2)
  but we know the real answer is "no"

So simple distributed validation does not work
  the validators must choose consistent orders!

Validation scheme #2
  Idea: client (or TC) chooses timestamp for committing xaction
    from loosely synchronized clocks, as in Thor
  validation checks that reads and writes are consistent with TS order
  solves distrib validation problem:
    timestamps tell the validators the order to check
    so "yes" votes will refer to the same order

Example 2 again, with timestamps:
  T1@100: Rx0 Wx1
  T2@110: Rx0 Wy1
  T3@105: Ry0 Rx1
  S1 validates just x information:
    T1@100: Rx0 Wx1
    T2@110: Rx0
    T3@105: Rx1
    timestamps say order must be T1, T3, T2
    does not validate! T2 should have seen x=1
  S2 validates just y information:
    T2@110: Wy1
    T3@105: Ry0
    timstamps say order must be T3, T2
    validates!
  S1 says no, S2 says yes, two-phase commit coordinator will abort

What have we given up by requiring timestamp order?
  example:
    T1@100: Rx0 Wx1
    T2@50: Rx1 Wx2
  T2 follows T1 in real time, and sees T1's write
  but T2 will abort, since TS says T2 comes first, so T1 should have seen x=2
    could have committed, since T1 then T2 works
  this will happen if client clocks are too far off
    if T1's client clock is ahead, or T2's behind
  so: requiring TS order can abort unnecessarily
    b/c validation no longer *searching* for an order that works
    instead merely *checking* that TS order consistent w/ reads, writes
    we've given up some optimism by requiring TS order
  maybe not a problem if clocks closely synched
  maybe not a problem if conflicts are rare

Problem with schemes so far:
  commit messages contained *values*, which can be big
  could instead use version numbers to check whether
    later xaction read earlier xaction's write
  let's use writing xaction's TS as record version number

Validation scheme #4
  tag each DB record (and cached record) with TS of xation that last wrote it
  validation requests carry TS of each record read

Our example for scheme #4:
  all values start with timestamp 0
  T1@100: Rx@0 Wx
  T2@110: Rx@0 Wy
  T3@105: Ry@0 Rx@100
  note:
    reads have timestamp that was in read record, not value
    writes don't include either value or timestamp
  S1 validates just x information:
    orders the transactions by timestamp:
    T1@100: Rx@0 Wx
    T3@105: Rx@100
    T2@110: Rx@0
    the question: does each read see the most recent write?
      T3 is ok, but T2 is not
  S2 validates just y information:
    again, sort by TS, check each read saw latest write:
    T3@105: Ry@0
    T2@110: Wy
    this does validate
  so scheme #4 abort, correctly, reasoning only about version TSs

what have we give up by thinking about version #s rather than values?
  maybe version numbers are different but values are the same
  e.g.
    T1@100: Wx1
    T2@110: Wx2
    T3@120: Wx1
    T4@130: Rx1@100
  timestamps say we should abort T4 b/c read a stale version
    should have read T3's write
    so scheme #4 will abort
  but T4 read the correct value -- x=1
    so abort wasn't necessary

Problem: per-record timestamp might use too much storage space
  Thor wants to avoid space overhead 
  maybe important, maybe not

Validation scheme #5
  Thor's invalidation scheme: no timestamps on records
  how can validation detect that a transaction read stale data?
  it read stale data b/c earlier xaction's invalidation hadn't yet arrived!
  so server can track invalidation msgs that might not have arrived yet
    "invalid set" -- one per client
    delete invalid set entry when client ACKs invalidation msg
    server aborts committing xaction if it read record in client's invalid set
    client aborts running xaction if it read record mentioned in invalidation

Example use of invalid set
  [timeline]
  Client C1:
    T2@105 ... Rx ... 2PC commit point
    imagine that client acts as 2PC coordinator
  Server:
    VQ: T1@100 Wx
    T1 committed, x in C1's invalid set
      server has sent invalidation message to C1

Three cases:
  1\. invalidation arrives before T2 reads
     Rx will miss in client cache, read from data from server
     client will (probably) return ACK before T2 commits
     server won't abort T2
  2\. invalidation arrives after T2 reads, before commit point
     client will abort T2 in response to invalidation
  3\. invalidation arrives after 2PC commit point
     i.e. after all servers replied to prepare
     this means the client was still in the invalid set when
       the server tried to validate the transaction
     so the server aborted, so the client will abort too
  so: Thor's validation detects stale reads w/o timestamp on each record

Performance

Look at Figure 5
  AOCC is Thor
  comparing to ACBL: client talks to srvr to get write-locks,
   and to commit non-r/o xactions, but can cache read locks along with data
  why does Thor (AOCC) have higher throughput?
    fewer msgs; commit only, no lock msgs
  why does Thor throughput go up for a while w/ more clients?
    apparently a single client can't keep all resources busy
    maybe due to network RTT?
    maybe due to client processing time? or think time?
    more clients -> more parallel xactions -> more completed
  why does Thor throughput level off?
    maybe 15 clients is enough to saturate server disk or CPU
    abt 100 xactions/second, about right for writing disk
  why does Thor throughput *drop* with many clients?
    more clients means more concurrent xactions at any given time
    more concurrency means more chance of conflict
    for OCC, more conflict means more aborts, so more wasted CPU

Conclusions
  fast client caching + transactions would be excellent
  distributed OCC very interesting, still an open research area
  avoiding per-record version #s doesn't seem compelling
  Thor's use of time was influential, e.g. Spanner 

对等系统

6.824 2015 年第 22 讲:对等系统

注意:这些讲义与 2015 年春季 6.824 课程网站上发布的略有不同。

P2P 系统

  • 今天:看看像 BitTorrent 和 Chord 这样的对等系统

  • 文件共享服务的经典实现:用户与集中服务器交流以下载文件

  • 用户应该可以相互交流以获取文件

  • 对等梦想:没有集中的组件,只是由人们的计算机构建

为什么使用 P2P?

  • (+)在大量 PC 上分散提供文件的工作

  • (+)可能比集中式系统更容易部署

    • 没有人必须购买带宽和存储量大的集中服务器
  • (+)如果你打好牌,资源数量应该随用户数量自然增长=> 减少过载的机会

  • (+)没有集中的服务器=>失败的可能性较小(更难以通过 DoS 攻击)

  • =>这么多优点!为什么还会有人构建非 P2P 系统?

  • (-)在百万用户的系统中查找文件需要复杂的设计<=>查找东西很难(不能只是问一个数据库)

  • (-)用户计算机不像数据中心中的服务器那样可靠。用户会关闭他们的计算机等等。

  • (-)一些 P2P 系统是开放的(BitTorrent),但有些是封闭的,比如只有亚马逊的计算机参与其中(有点像 Dynamo)。

    • =>在开放系统中,会有恶意用户=>易受攻击

结果是 P2P 软件在某些特定领域有一定的市场

  • 有很多数据的系统,比如在线视频服务

  • 聊天系统

  • 在不自然使用中心服务器的设置中,比如比特币

    • DNS 分散化将是一个好主意
  • 似乎主要用途是提供非法文件

BitTorrent

预 DHT BitTorrent

示意图:

 ---        ---
| W |      | T |
 ---        ---
/\         /\
| click on /  .torrent file
\         /
 ---/----        ---  
| C |  <------> | C |
 ---             --- 
  • 客户端访问网页服务器并下载一个 .torrent 文件

  • 种子文件存储了数据的哈希和追踪器的地址

  • 追踪器记录所有已下载该文件并且可能仍然拥有它并且可能愿意为其他客户端提供它的所有客户端

  • 客户端联系追踪器,询问其他客户端

  • 这里的巨大收益在于文件直接在客户端之间传输,网页服务器和追踪器没有太多成本

基于 DHT 的 BitTorrent(无追踪器的种子)

  • 追踪器是单点故障

  • 可以通过复制它们或使用额外的追踪器来解决这个问题

  • 下载文件的用户也形成了一个巨大的分布式键值存储(DHT)

  • 客户端仍然必须与原始网页服务器交流以获取它想要下载的infohash文件

  • 它使用它作为在 DHT 中查找的键

  • 值是拥有文件的其他客户端的 IP 地址

  • 这真的替代了追踪器

  • 如何在 DHT 中找到入口节点?也许客户端有一些硬编码的众所周知的节点

  • 也许 DHT 更可靠,更不容易受到法律(传票)和技术攻击的影响

  • BitTorrent 非常受欢迎:数以千万计的用户 => 巨大的 DHT,然而,大多数种子都由真实的追踪器支持

DHT 是如何开始的?

  • 15 年前就有了一堆关于如何构建 DHT 的研究

  • 关键是利用互联网上数百万台计算机提供接近数据库的东西

  • 接口非常简单:Put(k, v), Get(k)

    • 希望 puts 在一段时间后反映在 gets 中
  • 实际上构建一致性系统是很困难的

  • 对数据的可用性和一致性没有多少保证

    • 系统不能保证在执行 Put() 时保留您的数据
  • 即使有了这些弱保证,构建起来仍然很困难

DHT 设计

  1. 当您想要获取一个键时,向所有人发送 Get 的洪水

    • => 系统无法处理过多负载
  2. 假设每个人都同意 DHT 中的节点完整列表。

    • 然后你可以有一些散列约定,将一个键散列到一个确切的节点,并且查找是有效的。

    • 非常重要的是,所有人都同意,否则 A 发送 put 给 X,B 发送 get 给 Y,用于相同的键 k

    • 真正的问题在于很难保持表格的更新和准确性。

我们想要的是:

  • 我们正在寻找一个系统,其中每个节点只需知道其他几个节点。

  • 我们不希望节点发送太多消息来执行查找

  • 所有 DHT 采取的大致方法是定义一个跨越节点的全局数据结构

  • 不好的想法:将所有节点组织成二叉树,数据存储在叶节点中,较低的键位于最左边的节点

    • 所有流量都通过根节点(不好) => 如果根节点宕机,则分区

    • 如何替换掉宕机的节点?

Chord

  • 在 0 到 2¹⁶⁰ - 1 的循环 ID 空间中的数字(如模 p 的整数)

  • 每个节点在此空间中选择一个随机 ID 作为其节点 ID

  • 这个空间的键具有标识符,并且我们希望标识符具有均匀分布,因为我们用它来将键映射到节点标识符 => 对实际键使用哈希函数来获取它们的标识符

  • 负责键的节点是在顺时针方向上第一个最接近该键的节点:称为其后继

    • 近似距离 = |节点 ID - 键 ID|

慢但正确的方案:

  • 通过某种巫术,每个节点只需知道自己的后继(比如说,节点 2 的后继是节点 18,等等)

  • 我们始终可以通过跟随这些后继指针从每个节点开始进行查找

    • 这被称为路由

    • 所有关于将查找消息转发到环上更远节点的事情

  • 这很慢,如果有数百万个节点,可能会为单个查找发送数百万条消息

  • 需要时间对 DHT 中节点总数的对数 => 每一跳都必须能够计算它与任何目标键之间的距离

  • 在 Chord 中,每个节点都有一个包含 160 个条目的指针表

  • 节点 n 的指针表具有条目 i

    • f[i] = successor(n + 2^i)

    • => 第 159 个条目将指向某个节点 n + 2¹⁵⁹ 大致位于 ID 空间的中间位置

  • 每一跳大约是 50 毫秒,如果跳跃是环球的话

    • => 大约 1 秒钟可以通过 20 个节点 => 一些应用可能无法很好地处理这一点(BitTorrent 可以,因为它只在 DHT 中存储 IP 地址)
  • 当节点加入时,它们会得到入口节点的指纹的副本

    • 对于新节点来说不是很准确,但足够好

    • => 必须纠正表 => 对于第 i 个条目,查找 n+2^i 并将 f[i] 设置为回复的节点的地址

  • 每次查找都可能遇到一个死节点,超时需要很长时间

  • BitTorrent 中的流动性相当高

    • 1000 万人参与这个小时,下一个小时将有另外 1000 万人 => 难以保持 Finger 表的更新
  • log n 的查找时间并不那么好

  • Finger 表仅用于加速查找

  • 每个节点必须正确地知道它的后继节点才能使 Chord 正确

    • 这样,一个节点的获取会看到另一个节点的放置
  • 当一个节点首次加入时,它会对自己的标识符进行查找以找到它的后继节点

    • --> 10 -- [新节点 15] --> 20 -->

    • 15 将其后继指针设置为 20

    • 到目前为止没有人在进行查找

    • 直到 10 知道它,15 才真正成为系统的一部分

    • 每个节点定期询问其后继节点他们认为谁是他们的前驱节点

      • 10: 嘿 20,你的前驱是谁?

      • 20: 我的前驱是 15

      • 10: 哦,以为是我,所以让我把 15 设置为我的后继节点

      • 15: 哦,嗨 10,谢谢你把我添加为你的后继节点,让我把你添加为我的前驱节点

      • 这被称为稳定化

例子:

10 -> 20

12, 18 join

10 -> 20, 12->20, 18->20, 20->18

10 -> 18, 18->10, 18->20, 20->18, 12 ->18

10 -> 18,18->20, 20->18, 12 ->18, 18->12

10 -> 12, 12->10 12->18, 18->12, 18->20, 20->18 
  • 当一个节点获得一个更接近的前驱节点时,它会将更接近前驱节点的键转移到那里

如果节点失败了,我们能正确进行查找吗?

  • 假设在查找过程中一个中间节点失败了,那么发起节点可以简单地选择另一个

  • 在查找过程的末尾,不再使用 Finger 表。而是跟随后继指针 => 如果一个节点失败,那么查找无法继续 => 节点必须记住它们的后继节点的后继节点才能继续

  • 在互联网上发生分区的概率很低

6.824 2015 年的原始笔记

Lecture outline:
  peer-to-peer (P2P)
  BitTorrent
  DHTs
  Chord

Peer-to-peer
  [user computers, files, direct xfers]
  users computers talk directly to each other to implement service
    in contrast to user computers talking to central servers
  could be closed or open
  examples:
    skype, video and music players, file sharing

Why might P2P be a win?
  spreads network/caching costs over users
  absence of server may mean:
    easier to deploy
    less chance of overload
    single failure won't wreck the whole system
    harder to attack

Why don't all Internet services use P2P?
  can be hard to find data items over millions of users
  user computers not as reliable than managed servers
  if open, can be attacked via evil participants

The result is that P2P has some successful niches:
  Client-client video/music, where serving costs are high
  Chat (user to user anyway; privacy and control)
  Popular data but owning organization has no money
  No natural single owner or controller (Bitcoin)
  Illegal file sharing

Example: classic BitTorrent
  a cooperative download system, very popular!
  user clicks on download link for e.g. latest Linux kernel distribution
    gets torrent file w/ content hash and IP address of tracker
  user's BT client talks to tracker
    tracker tells it list of other user clients w/ downloaded file
  user't BT client talks to one or more client's w/ the file
  user's BT client tells tracker it has a copy now too
  user's BT client serves the file to others for a while
  the point:
    provides huge download b/w w/o expensive server/link

BitTorrent can also use a DHT instead of / as well as a tracker
  this is the topic of today's readings
  BT clients cooperatively implement a giant key/value store
  "distributed hash table"
  the key is the file content hash ("infohash")
  the value is the IP address of a client willing to serve the file
    Kademlia can store multiple values for a key
  client does get(infohash) to find other clients willing to serve
    and put(infohash, self) to register itself as willing to serve
  client also joins the DHT to help implement it

Why might the DHT be a win for BitTorrent?
  single giant tracker, less fragmented than many trackers
    so clients more likely to find each other
  maybe a classic tracker too exposed to legal &c attacks
  it's not clear that BitTorrent depends heavily on the DHT
    mostly a backup for classic trackers?

How do DHTs work?

Scalable DHT lookup:
  Key/value store spread over millions of nodes
  Typical DHT interface:
    put(key, value)
    get(key) -> value
  loose consistency; likely that get(k) sees put(k), but no guarantee
  loose guarantees about keeping data alive

Why is it hard?
  Millions of participating nodes
  Could broadcast/flood request -- but too many messages
  Every node could know about every other node
    Then hashing is easy
    But keeping a million-node table up to date is hard
  We want modest state, and modest number of messages/lookup

Basic idea
  Impose a data structure (e.g. tree) over the nodes
    Each node has references to only a few other nodes
  Lookups traverse the data structure -- "routing"
    I.e. hop from node to node
  DHT should route get() to same node as previous put()

Example: The "Chord" peer-to-peer lookup system
  By Stoica, Morris, Karger, Kaashoek and Balakrishnan; 2001

Chord's ID-space topology
  Ring: All IDs are 160-bit numbers, viewed in a ring.
  Each node has an ID, randomly chosen

Assignment of key IDs to node IDs?
  Key stored on first node whose ID is equal to or greater than key ID.
    Closeness is defined as the "clockwise distance"
  If node and key IDs are uniform, we get reasonable load balance.
  So keys IDs should be hashes (e.g. bittorrent infohash)

Basic routing -- correct but slow
  Query is at some node.
  Node needs to forward the query to a node "closer" to key.
    If we keep moving query closer, eventually we'll win.
  Each node knows its "successor" on the ring.
    n.lookup(k):
      if n < k <= n.successor
        return n.successor
      else
        forward to n.successor
  I.e. forward query in a clockwise direction until done
  n.successor must be correct!
    otherwise we may skip over the responsible node
    and get(k) won't see data inserted by put(k)

Forwarding through successor is slow
  Data structure is a linked list: O(n)
  Can we make it more like a binary search?
    Need to be able to halve the distance at each step.

log(n) "finger table" routing:
  Keep track of nodes exponentially further away:
    New state: f[i] contains successor of n + 2^i
    n.lookup(k):
      if n < k <= n.successor:
        return successor
      else:
        n' = closest_preceding_node(k) -- in f[]
        forward to n'

for a six-bit system, maybe node 8's looks like this:
  0: 14
  1: 14
  2: 14
  3: 21
  4: 32
  5: 42

Why do lookups now take log(n) hops?
  One of the fingers must take you roughly half-way to target

There's a binary lookup tree rooted at every node
  Threaded through other nodes' finger tables
  This is *better* than simply arranging the nodes in a single tree
    Every node acts as a root, so there's no root hotspot
    But a lot more state in total

Is log(n) fast or slow?
  For a million nodes it's 20 hops.
  If each hop takes 50 ms, lookups take a second.
  If each hop has 10% chance of failure, it's a couple of timeouts.
  So in practice log(n) is better than O(n) but not great.

How does a new node acquire correct tables?
  General approach:
    Assume system starts out w/ correct routing tables.
    Use routing tables to help the new node find information.
    Add new node in a way that maintains correctness.
  New node m:
    Sends a lookup for its own key, to any existing node.
      This yields m.successor
    m asks its successor for its entire finger table.
  At this point the new node can forward queries correctly
  Tweaks its own finger table in background
    By looking up each m + 2^i

Does routing *to* new node m now work?
  If m doesn't do anything,
    lookup will go to where it would have gone before m joined.
    I.e. to m's predecessor.
    Which will return its n.successor -- which is not m.
  So, for correctness, m's predecessor needs to set successor to m.
    Each node keeps track of its current predecessor.
    When m joins, tells its successor that its predecessor has changed.
    Periodically ask your successor who its predecessor is:
      If that node is closer to you, switch to that guy.
    So if we have x m y
      x.successor will be y (now incorrect)
      y.predecessor will be m
      x will ask its x.successor for predecessor
        x learns about m
        sets x.successor to m
        tells m "x is your predecessor"
        called "stabilization"
  Correct successors are sufficient for correct lookups!

What about concurrent joins?
  Two new nodes with very close ids, might have same successor.
  Example:
    Initially 40 then 70
    50 and 60 join concurrently
    at first 40, 50, and 60 think their successor is 70!
    which means lookups for e.g. 45 will yield 70, not 50
    after one stabilization, 40 and 50 will learn about 60
    then 40 will learn about 50

To maintain log(n) lookups as nodes join,
  Every one periodically looks up each finger (each n + 2^i)

Chord's routing is conceptually similar to Kademlia's
  Finger table similar to bucket levels
    Both halve the metric distance for each step
    Both are about speed and can be imprecise
  n.successor similar to Kademlia's requirement that
    each node know of all the nodes that are very close in xor-space
    in both cases care is needed to ensure that different lookups
      for same key converge on exactly the same node

What about node failures?
  Assume nodes fail w/o warning. Strictly harder than graceful departure.
  Two issues:
    Other nodes' routing tables refer to dead node.
    Dead node's predecessor has no successor.
  If you try to route via dead node, detect timeout, treat as empty table entry.
    I.e. route to numerically closer entry instead.
  For dead successor
    Failed node might have been just before key ID!
      So we need to know what its n.successor was
    Maintain a _list_ of successors: r successors.
    Lookup answer is first live successor >= key
      or forward to *any* successor < key

Kademlia has a faster plan for this
  send alpha (or k) lookup RPCs in parallel, to different nodes
  send more lookups as previous ones return info about nodes closer to key
  single non-responsive node won't cause lookup to suffer a timeout

Dealing with unreachable nodes during routing is extremely important
  "Churn" is very high in open p2p networks
  People close their laptops, move WiFi APs, &c pretty often
  Measurement of Bittorrent/Kademlia suggest lookups are not very fast

Geographical/network locality -- reducing lookup time
  Lookup takes log(n) messages.
    But they are to random nodes on the Internet!
    Will often be very far away.
  Can we route through nodes close to us on underlying network?
  This boils down to whether we have choices:
    If multiple correct next hops, we can try to choose closest.

Idea:
  to fill a finger table entry, collect multiple nodes near n+2^i on ring
  perhaps by asking successor to n+2^i for its r successors
  use lowest-ping one as i'th finger table entry

What's the effect?
  Individual hops are lower latency.
  But less and less choice (lower node density) as you get close in ID space.
  So last few hops likely to be very long. 
  Though if you are reading, and any replica will do,
    you still have choice even at the end.

What about security?
  Self-authenticating data, e.g. key = SHA1(value)
    So DHT node can't forge data
    Of course it's annoying to have immutable data...
  Can someone cause millions of made-up hosts to join?
    They don't exist, so routing will break?
    Don't believe new node unless it responds to ping, w/ random token.
  Can a DHT node claim that data doesn't exist?
    Yes, though perhaps you can check other replicas
  Can a host join w/ IDs chosen to sit under every replica?
    Or "join" many times, so it is most of the DHT nodes?
    Maybe you can require (and check) that node ID = SHA1(IP address)

Why not just keep complete routing tables?
  So you can always route in one hop?
  Danger in large systems: timeouts or cost of keeping tables up to date.

How to manage data?
  Here is the most popular plan.
  DHT doesn't guarantee durable storage
    So whoever inserted must re-insert periodically if they care
    May want to automatically expire if data goes stale (bittorrent)
  DHT does replicate each key/value item
    On the nodes with IDs closest to the key, where looks will find them
    Replication can help spread lookup load as well as tolerate faults
  When a node joins:
    successor moves some keys to it
  When a node fails:
    successor probably already has a replica
    but r'th successor now needs a copy

Retrospective
  DHTs seem very promising for finding data in large p2p systems
    Decentralization seems good for load, fault tolerance
  But: the security problems are difficult
  But: churn is a serious problem, particularly if log(n) is big
  So DHTs have not had the impact that many hoped for 

比特币

6.824 2015 讲座 23:比特币

注意:这些讲座笔记稍作修改,来自 2015 年春季 6.824 课程网站

比特币

  • 一种电子货币系统

  • 有技术方面和财务、经济、社会方面

  • 或许首先要问的是:它是在尝试做些更好的事情吗?它是否为我们解决了问题?

  • 在线支付使用信用卡,为什么不直接使用它们?

    • 优点:

      • 它们在线运作

      • 很难盗取我的信用卡(有关信用卡公司如何运作的法律,如果你的信用卡号被盗,你是受保护的)

    • 好/坏:

      • 卡背面的客服号码可以让你撤销费用

        • 这可以预防或制造欺诈
      • 与某个国家的货币挂钩

    • 缺点

      • 作为顾客或商家,没有办法独立验证关于信用卡交易的任何事情:你是否有钱,信用卡号是否有效?

        • 如果你不想让别人知道你有多少钱,这可能是个好东西
      • 依赖第三方:对所有事项收费的好方法

      • 3% 的费用

      • 结算时间相当长(商家在一个月后才确定是否收到款项)

      • 要成为信用卡商家相当困难

        • 信用卡公司承担了很大的风险,向可能不向客户发送产品的商家发送资金,导致信用卡公司不得不退还客户
  • 对于比特币:

    • 不需要第三方(好吧,不完全是这样了)

    • 费用远远低于 3%

    • 结算时间可能是 10 分钟

    • 任何人都可以成为商家

  • 比特币使每个人都能验证交易序列并达成一致 => 无需依赖第三方

OneBit

  • 简单的电子货币系统

  • 它只有一个名为 OneBank 的服务器

  • 每个用户拥有一些硬币

设计:

OneBank server 
  • 一个交易:

    1. 新所有者的公钥

    2. 这枚硬币的上一次转移记录的哈希

    3. 上一个所有者的私钥对此记录进行签名

  • 银行保留每枚硬币的交易列表

  • x 将硬币转移到 y

  • [T7: from=x, to=y; hash=h(prev tx); sig_x(this)]

  • y 将硬币转移到 z,从麦当劳买了一个汉堡包

  • [T8: from y, to=z; hash=h(T7); sig_y(this)]

  • 会出什么问题?

    • 如果有人将硬币转移到 z,似乎很不可能有人除了 z 以外花费那个硬币:因为没有人能用 z 的私钥签署新的交易,因为他们没有 z 的私钥
  • 我们必须信任一个银行不让用户重复花费钱

    • 如果银行帮助,y 也可以用同一枚硬币从汉堡王购买奶昔

    • [T8': from y, to=q'; hash=h(T7); sig_y(this)]

    • 银行可以向麦当劳展示 T8,并向汉堡王展示 T8'

    • (我爱免费的食物!)

    • 只要麦当劳和汉堡王不相互沟通并验证交易链,他们就不会发现

比特币区块链

  • 比特币有一个单一的区块链

  • 许多服务器:更多或更少的副本,拥有整个区块链的副本

  • 区块链中的每个区块看起来都像这样:

    • 上一个区块的哈希

    • 交易集合

    • 随机数

    • 当前时间

  • 交易有两个阶段

    • 首先,它被创建并发送到网络上。

    • 然后交易被纳入区块链中。

区块是如何创建的?挖矿

比特币网络中的所有节点都试图创建下一个区块:

  • 每个节点都会获取自上一个区块创建后到达的所有交易,并尝试将新的区块附加到其中。

  • 规则规定区块的哈希值必须小于某个特定数值(即,它有一定数量的前导零,使其难以找到)。

  • 每个比特币节点都会调整区块中的nonce字段,直到它们获得具有一定数量前导零的哈希。

  • 这样做的目的是让创建新区块变得昂贵。

    • 对于一台单独的计算机来说,可能需要几个月才能找到这样的随机数。
  • 领先零的数量会调整,以便平均每隔 10 分钟添加一个新区块

    • 客户端监视最后 5 笔交易中的currentTime字段,如果它们花费的时间太少,他们会将目标零的数量增加一位。

      • 每个人都遵守协议,因为如果他们不这样做,其他人将拒绝他们的区块(例如,如果区块的零的数量错误或时间戳错误)。

空区块链

  • "起初什么都没有,然后中本聪创建了第一个区块。"

  • "然后人们开始挖掘额外的区块,没有交易。"

  • "然后他们为每个挖掘出的区块获得了挖矿奖励。"

  • "这就是用户得到比特币的方式。"

  • "然后他们开始进行交易。"

  • "然后就有了光明。"

双重花费需要什么?

如果交易在区块链上,系统是否可以双重花费其硬币?

  • 分叉区块链是唯一的方法。

  • 分叉是否可以被长时间隐藏?

  • 如果发生分叉,矿工将选择其中一个继续挖矿。

  • 当一个分支变得更长时,每个人都会转移到它上面。

    • 如果他们留在较短的分支上,他们很可能会被其他人挖掘并浪费工作,所以他们会有动力转移到较长的分支上。

    • 较短分支上的交易将被合并到较长分支上。

    • 承诺的交易可能会被取消 => 人们通常会等待几个额外的区块被创建后,交易的区块才会被取消。

  • 这就是 51%规则发挥作用的地方:如果 51%的计算能力是诚实的,协议就能正常工作。

  • 如果超过 51%是不诚实的,那么他们很可能会成功地挖掘出他们想要的任何东西。

  • 比特币最聪明的地方可能是:只要你相信超过一半的计算能力没有作弊,你就可以确保没有双重支付。

设计的优点和缺点

  • (+) 公开可验证的日志

  • (-) 与一种新的货币挂钩,非常不稳定。

    • 许多人因为这个原因不使用它。
  • (+/-) 挖矿 - 分散的信任

很难说会发生什么:

  • 我们可能在 30 年后都在使用它。

  • 或者,银行可以赶上并设计出自己的可验证日志设计。

posted @ 2026-02-20 16:42  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报