- 分布式系统的定义:一组相互协作的计算机通过网络通信来共同完成某个任务。
- 课程重点:将关注大型网站存储、大数据计算(如MapReduce)以及点对点文件共享等案例研究。
- 分布式系统的重要性:许多关键基础设施建立在分布式系统之上,这些系统需要多台计算机来完成任务,或者需要物理上分散部署。
- 构建分布式系统的原因:
- 高性能:通过并行处理提升性能。
- 容错:通过复制任务在多台计算机上执行,提高系统的容错能力。
- 物理分布:某些问题天然需要在空间上分散处理,如跨银行转账。
- 安全性:通过在多台计算机上分散计算,实现隔离,增强安全性。
- 分布式系统的挑战:
- 并发编程问题:多台计算机同时执行任务,带来并发编程的复杂性。
- 意外的故障模式:分布式系统可能遇到部分故障,如网络问题或部分计算机失效。
- 性能提升难题:虽然理论上增加计算机数量可以提升性能,但实际上要实现这种线性扩展非常困难。
- 课程结构:包括讲座、论文阅读、实验和期末考试。讲座将涵盖分布式系统的两大主题:性能和容错。实验将涉及构建实际的分布式系统,重点关注性能和容错。
- MapReduce案例研究:详细介绍了MapReduce框架,它允许用户通过编写简单的map和reduce函数来执行大规模分布式计算,而无需关心底层的分布式细节。
- MapReduce的工作原理:
- Map阶段:在输入文件上并行执行map函数,生成中间的键值对。
- Shuffle阶段:根据键将所有map输出收集起来,为reduce阶段准备数据。
- Reduce阶段:对每个键对应的所有值执行reduce函数,生成最终输出。
- MapReduce的挑战:网络通信是MapReduce性能的主要瓶颈,尤其是在数据传输(shuffle阶段)时。
- 现代分布式系统:现代数据中心网络速度更快,Google等公司已经停止使用MapReduce,转而使用更现代的系统,这些系统能够更有效地处理数据流和大规模并行计算。
MapReduce
1. MapReduce概要
MapReduce是一种编程模型及其实现,用于处理和生成大型数据集。其核心思想是通过两种用户定义的函数——Map和Reduce,实现数据的分布式处理:
- Map函数:
- 输入是一个键值对(key/value),输出是一组中间键值对。
- Map函数负责将原始输入数据分解为中间数据。
- Reduce函数:
- 接收由Map阶段输出的中间数据,并将具有相同键的所有值聚合。
- Reduce函数通常生成一个更小的结果集。
2. MapReduce案例
以下总结了多种MapReduce应用场景,涵盖从单词统计到倒排索引等典型案例。
1. 单词统计(Word Count)
功能
统计每个文档中单词的出现次数。
Map阶段
输入键值对(文档名称,文档内容),对每个单词输出<word, 1>
。
Reduce阶段
对相同单词的值列表求和,生成<word, count>
。
伪代码
def map(key, value):
for word in value.split():
emit(word, 1)
def reduce(key, values):
emit(key, sum(values))
用途
分析文本数据,例如单词频率统计。
2. 分布式Grep(Distributed Grep)
功能
提取包含指定模式的行。
Map阶段
判断行是否包含指定模式,若匹配则输出该行。
Reduce阶段
直接输出中间结果(Identity Function)。
伪代码
def map(key, value):
if "pattern" in value:
emit(key, value)
def reduce(key, values):
for value in values:
emit(key, value)
用途
分布式日志搜索或数据过滤。
3. URL访问频率统计(Count of URL Access Frequency)
功能
统计每个URL的访问次数。
Map阶段
解析日志文件,每行输出<URL, 1>
。
Reduce阶段
对相同URL的值列表求和,生成<URL, total count>
。
伪代码
def map(key, value):
url = extract_url(value)
emit(url, 1)
def reduce(key, values):
emit(key, sum(values))
用途
分析网站流量或用户行为。
4. 反向链接图(Reverse Web-Link Graph)
功能
构建目标页面与其所有引用页面的映射。
Map阶段
解析网页,输出<target, source>
,表示source
页面链接了target
。
Reduce阶段
汇总目标页面的所有来源页面。
伪代码
def map(key, value):
for target in extract_links(value):
emit(target, key)
def reduce(key, values):
emit(key, list(values))
用途
构建搜索引擎中的反向索引。
5. 主机词向量统计(Term-Vector per Host)
功能
为每个主机生成关键词及其频率的向量。
Map阶段
解析文档内容,按主机输出<hostname, term vector>
。
Reduce阶段
合并同一主机的词向量,并移除低频词。
伪代码
def map(key, value):
hostname = extract_hostname(key)
term_vector = generate_term_vector(value)
emit(hostname, term_vector)
def reduce(key, values):
combined_vector = merge_term_vectors(values)
emit(key, filter_infrequent_terms(combined_vector))
用途
文本聚类或主题分析。
6. 倒排索引(Inverted Index)
功能
记录每个单词在哪些文档中出现。
Map阶段
为每个单词生成键值对<word, document ID>
。
Reduce阶段
对相同单词的文档ID列表进行排序并去重。
伪代码
def map(key, value):
for word in value.split():
emit(word, key)
def reduce(key, values):
emit(key, sorted(set(values)))
用途
实现全文搜索,用于快速定位文档。
7. 分布式排序(Distributed Sort)
功能
对记录按指定键进行全局排序。
Map阶段
提取排序键,生成<key, record>
。
Reduce阶段
直接输出分区后的排序结果。
伪代码
def map(key, value):
sort_key = extract_key(value)
emit(sort_key, value)
def reduce(key, values):
for value in values:
emit(key, value)
依赖
- 分区函数(例如
hash(key) mod R
)。 - 每个分区内保证键递增排序。
用途
大规模排序任务,如TeraSort。
这些案例展示了MapReduce的灵活性和广泛适用性,可用于文本处理、数据分析和索引构建等多种场景。
3. 实现
以下为Google设计的MapReduce实现所针对的计算环境特性。
1. 环境适配性
不同的MapReduce实现方式可以根据环境选择,适应从小型共享内存机器到大型分布式网络集群的需求。
2. Google的计算环境特点
Google针对其大规模集群设计了特定的MapReduce实现,该环境的主要特性包括:
(1) 硬件配置
- 处理器:每台机器配备双处理器的x86架构,运行Linux系统。
- 内存:每台机器内存为2-4 GB,属于普通商品硬件。
(2) 网络架构
- 使用普通的网络硬件:
- 每台机器的网络带宽为100 Mbps或1 Gbps。
- 整体网络的双向带宽有限,表现为瓶颈区域带宽不足。
(3) 集群规模
- 集群由数百至数千台机器组成。
- 由于规模庞大,机器故障非常普遍。
(4) 存储系统
- 使用廉价的IDE硬盘作为存储介质。
- 依赖自研的分布式文件系统(GFS),通过数据副本提供高可用性和可靠性,克服硬件的不可靠性。
(5) 调度机制
- 用户提交的作业通过调度系统处理。
- 每个作业由多个任务组成,调度系统将任务分配到集群中的可用机器。
3.1 工作流程
- 数据分片:输入数据被分割为多个小片,每片通常大小在16MB到64MB之间。
- 任务分配:Master节点管理任务调度,将Map任务和Reduce任务分配给工作节点。
- Map阶段:
- 每个工作节点读取分片,执行用户定义的Map函数,生成中间键值对。
- 中间结果被分区并存储在本地磁盘上。
- 数据传输和排序:Reduce节点通过网络获取中间数据,对其按键进行排序。
- Reduce阶段:Reduce节点处理中间数据,生成最终的输出结果。
- 结果输出:输出被写入到指定的分布式存储中。
-
输入数据分片:
MapReduce 库会将输入文件拆分为 M 个小块(通常每块 16MB 到 64MB,可由用户参数控制)。然后在集群中的多个机器上启动多个程序实例。
-
主节点与工作节点:
启动的程序实例中有一个是 主节点(master),其余是 工作节点(worker)。主节点负责将 M 个 Map 任务 和 R 个 Reduce 任务 分配给空闲的工作节点。
-
Map 阶段:
- 被分配到 Map 任务的工作节点读取对应的输入分片,并将其解析为键值对(key/value pairs)。
- 用户自定义的 Map 函数对这些键值对进行处理,生成中间键值对。
- 中间键值对缓存在内存中,随后按照分区函数(如
hash(key) mod R
)分为 R 个区域,并定期写入本地磁盘。写入的位置会被通知主节点。
-
Reduce 阶段的数据拉取:
- 主节点通知 Reduce 工作节点中间数据的位置。
- Reduce 节点通过远程调用从 Map 节点的本地磁盘读取中间数据。
- Reduce 节点对这些数据按照中间键进行排序(需要确保相同键的数据分组到一起)。如果数据量太大,会使用外部排序。
-
Reduce 阶段:
- Reduce 节点遍历排序后的中间数据,将每个唯一键及其对应的一组值传递给用户自定义的 Reduce 函数。
- Reduce 函数的输出被追加到对应的最终输出文件(每个 Reduce 任务对应一个文件)。
-
任务完成:
- 当所有 Map 和 Reduce 任务完成后,主节点唤醒用户程序,MapReduce 调用返回。最终输出为 R 个文件,用户可以直接使用这些文件作为下一个 MapReduce 调用的输入,或者在其他分布式应用中处理。
优势:
- 自动化并行化:无需开发者操心并行、分布式及容错等细节。
- 容错性:如果节点失败,任务会重新调度执行。
- 扩展性:可以在由普通硬件组成的大型集群上运行,处理TB甚至PB级别的数据。
3.2 Master 的数据结构
-
任务状态管理:
Master 维护所有 Map 和 Reduce 任务的状态,包括:
- 状态:空闲(idle)、执行中(in-progress)、已完成(completed)。
- 执行的工作节点(worker)的标识。
-
中间数据位置管理:
Master 记录每个已完成的 Map 任务生成的 RR 个中间文件的位置和大小,并将这些信息推送给正在进行的 Reduce 任务。
-
中间文件重分发:
如果某个 Map 任务重新执行(例如因为节点失败),Master 会将新生成的中间文件位置通知 Reduce 任务,确保 Reduce 可以正常获取数据。
3.3 容错机制
由于 MapReduce 需要在数百甚至数千台机器上运行,必须具备良好的容错能力。以下是主要的容错机制:
1. Worker 节点失败
-
检测失败:
Master 定期对每个 Worker 发送 ping 信号。如果某 Worker 在指定时间内未响应,Master 会将其标记为失败。
-
任务重置:
- 该 Worker 已完成的 Map 任务 会被重置为初始状态(idle),以便在其他节点上重新调度执行,因为中间文件存储在失败节点的本地磁盘,无法访问。
- Reduce 任务无需重新执行,因为 Reduce 的输出存储在全局文件系统中。
-
重分发任务:
如果 Map 任务先在 Worker A 上执行,后因 A 失败在 Worker B 上重新执行,Reduce 任务会从 Worker B 获取新的中间数据。
2. Master 节点失败
-
检查点机制:
Master 可以定期将其数据结构(任务状态、文件位置信息等)保存为检查点。如果 Master 失败,可从最近的检查点恢复。
-
当前实现:
Master 节点单点失败时,MapReduce 任务会中止。用户需要重新启动任务。不过,由于 Master 是单节点,失败的可能性较小。
故障条件下的语义
-
确定性函数:
如果用户定义的 Map 和 Reduce 操作是确定性函数(输入确定,输出也确定),分布式 MapReduce 保证其结果与顺序执行相同。
-
非确定性函数:
如果 Map 或 Reduce 包含非确定性操作(如随机数),分布式 MapReduce 的结果可能稍有差异:
- 相同 Reduce 任务的输出与顺序执行一致。
- 不同 Reduce 任务可能读取到 Map 任务的不同执行结果(例如某 Map 任务失败后重新执行)。
3.4 本地化 (Locality)
-
节省网络带宽:
网络带宽是集群中宝贵的资源。为了减少网络带宽消耗,MapReduce 优化了任务调度,使 Map 任务尽量在存储输入数据的机器上运行。
-
数据存储机制:
- 输入数据由 GFS(Google File System)管理,分为 64 MB 块,每个块存储 3 个副本,分布在不同机器上。
-
任务调度优化:
- Master 优先将 Map 任务分配到拥有输入数据副本的机器上。
- 如果无法安排在存储副本的机器上,则优先选择同一网络交换机下的机器。
-
结果:
在大规模任务中,大部分输入数据可以被本地读取,减少了网络带宽的使用。
3.5 任务粒度 (Task Granularity)
- 任务拆分:
- Map 阶段分为 M 个子任务,Reduce 阶段分为 R 个子任务。
- M 和 R 通常远大于 Worker 机器的数量。
- 优势:
- 动态负载均衡: 每个 Worker 执行多个小任务,可以根据空闲状态动态调度任务。
- 故障恢复: 如果某个 Worker 失败,其完成的任务可以重新分配给多个其他 Worker。
- 限制:
- Master 需要进行 O(M+R) 次调度决策,并维护 (O(M×R) 的状态信息(每对 Map 和 Reduce 任务大约需要 1 字节存储空间)。
- R 的大小通常受到用户的限制,因为每个 Reduce 任务生成一个独立的输出文件。
- 实际选择:
- M:通常选择每个 Map 任务处理 16 MB 到 64 MB 数据,以优化本地化性能。
- R:通常是 Worker 数量的几倍。
- 示例:使用 2000 个 Worker 时,常见配置为M=200000, R=5000。
3.6 备份任务 (Backup Tasks)
-
问题:
MapReduce 任务中的“尾部拖延”(Straggler)可能显著延长整个操作的完成时间,常见原因包括:
- 硬件问题(如磁盘错误导致读写速度减慢)。
- 集群调度引起的资源竞争(CPU、内存、磁盘、网络带宽)。
- 软件问题(如禁用缓存导致计算性能显著下降)。
-
解决方案:
- 备份机制:
当 MapReduce 接近完成时,Master 会为仍在执行中的任务安排备份执行(Backup Execution)。 - 标记完成:
无论是主任务还是备份任务完成,任务都会被标记为已完成。
- 备份机制:
-
性能优化:
- 备份机制只增加了少量计算资源(通常只多消耗 1% 至 3%)。
- 对于大规模 MapReduce 操作,显著减少完成时间。例如,禁用备份机制时,排序程序的运行时间增加了 44%。
通过这些优化,MapReduce 能更高效地利用计算资源,最大化数据局部性,并显著减少尾部拖延对总运行时间的影响。
4. 完善
4.1 分区函数 (Partitioning Function)
-
默认分区: 使用
hash(key) mod R
将中间键分布到 个 Reduce 任务中,通常能产生平衡的分区。RR
-
用户自定义: 支持用户定义分区函数,例如
hash(Hostname(urlkey)) mod R
,可确保相同主机的 URL 数据写入同一输出文件。
4.2 排序保证 (Ordering Guarantees)
- 每个分区内的中间键值对按照键值递增顺序处理。
- 用途: 输出文件支持按键高效随机访问或方便用户处理排序数据。
4.3 合并函数 (Combiner Function)
- 功能: 允许在 Map 任务所在机器上对中间数据进行部分合并,减少传输到 Reduce 任务的数据量。
- 适用场景: 用户定义的 Reduce 函数是交换律和结合律的,如单词计数。
- 优点: 减少网络流量并显著加速某些操作。
4.4 输入和输出类型 (Input and Output Types)
- 输入格式:
- 支持多种格式(如文本行、键值对序列)。
- 输入类型实现负责数据拆分,确保任务边界的有效性(如按行拆分)。
- 可自定义输入类型(如从数据库或内存结构读取数据)。
- 输出格式:
- 提供不同数据格式的输出支持,用户也可添加新的输出类型。
4.5 副作用文件 (Side-effects)
- 额外输出: Map 或 Reduce 操作可能需要生成辅助文件,需由应用程序保证操作的原子性和幂等性(如使用临时文件)。
- 限制: 不支持多个文件的原子提交,但实践中未引发问题。
4.6 跳过错误记录 (Skipping Bad Records)
- 问题: Bug(如第三方库问题)可能导致特定记录引发 Map 或 Reduce 函数崩溃,阻碍操作完成。
- 解决方案:
- MapReduce 可检测并跳过引发崩溃的记录,允许计算继续。
- 通过信号处理机制记录出错的记录序号,若多次失败则跳过。
4.7 本地执行 (Local Execution)
- 目的: 提供本地顺序执行的 MapReduce 实现,用于调试、性能分析或小规模测试。
- 用户控制: 通过标志限制特定 Map 任务,方便使用调试工具(如 gdb)。
4.8 状态信息 (Status Information)
- HTTP 服务:
- Master 通过内置 HTTP 服务器展示任务状态(已完成任务数、处理速度、输入/输出字节数等)。
- 包含错误日志链接和 Worker 失败信息,便于诊断用户代码问题。
- 用户收益:
- 预测计算时间,决定是否增加资源。
- 分析计算是否异常缓慢。
4.9 计数器 (Counters)
- 功能: 提供计数器机制记录事件次数(如处理的总词数、某类文档数)。
- 实现:
- 用户创建命名计数器,在 Map 或 Reduce 函数中递增。
- Master 汇总各任务计数值并排除重复执行产生的影响。
- 计数值实时显示在状态页面上。
- 用途:
- 数据校验(如确保输入与输出对数一致)。
- 监控进度与操作逻辑。
这些扩展功能提高了 MapReduce 的灵活性、调试便利性以及操作效率,适配了多种场景和需求。
5 性能分析
在本节中,我们测量了 MapReduce 在一个大型集群上运行的两个计算任务的性能。这两个计算任务分别是:在约 1TB 的数据中搜索特定模式,以及对约 1TB 的数据进行排序。这两个程序代表了 MapReduce 用户编写的大量实际程序的子集:一类程序用于将数据从一种表示形式转换为另一种,另一类则从大规模数据集中提取少量有意义的数据。
5.1 集群配置
所有程序都在一个包含约 1800 台机器的集群上运行。每台机器的配置如下:
- 处理器:两颗 2GHz 的 Intel Xeon 处理器,启用了超线程技术;
- 内存:4GB,其中 1-1.5GB 被集群中的其他任务保留;
- 磁盘:两块 160GB IDE 硬盘;
- 网络:千兆以太网。
这些机器通过一个两级树状交换网络连接,根节点的总带宽为 100-200 Gbps。所有机器位于同一个机房中,机器之间的往返延迟小于 1 毫秒。
实验在周末下午进行,当时 CPU、磁盘和网络大多处于空闲状态。
5.2 Grep 程序性能
Grep 程序扫描 10¹⁰ 个 100 字节大小的记录,寻找一个相对稀少的三字符模式(该模式出现于 92,337 条记录中)。输入数据被分为约 64MB 的块(M=15000),所有输出最终存储为一个文件(R=1)。
运行过程
图 2 展示了计算随时间的进展:
- Y 轴表示输入数据的扫描速率;
- 速率随更多机器加入逐渐增加,在 1764 个工作节点全部参与时达到峰值超 30 GB/s;
- 随着 Map 任务完成,速率逐渐下降,约 80 秒后降为零;
- 总运行时间约 150 秒,包括约 1 分钟的启动开销(如程序分发到工作节点、与 GFS 的交互延迟等)。
5.3 Sort 程序性能
Sort 程序对 10¹⁰ 个 100 字节的记录进行排序,总数据量约 1TB。该程序基于 TeraSort 基准测试模型编写,仅包含约 50 行用户代码:
- 一个 3 行的 Map 函数提取 10 字节排序键并生成中间键值对;
- Reduce 使用内置的 Identity 函数,直接输出中间键值对;
- 排序结果写入 GFS,输出文件有两个副本,总大小为 2TB。
运行过程
- 输入数据分为 64MB 块(M=15000),最终输出被分为 4000 个文件(R=4000)。
- 图 3(a) 显示正常运行时的数据传输速率:
- 输入速率:峰值约 13 GB/s,因 Map 任务同时将中间数据写入本地磁盘,速率较 Grep 程序低;
- Shuffle 阶段:约 600 秒时完成,速率峰值出现在 Reduce 任务初期;
- 输出速率:由于写入双副本,总速率较低(约 2-4 GB/s),891 秒完成整个计算,接近当前 TeraSort 基准的最佳结果(1057 秒)。
5.4 备份任务的影响
图 3(b) 显示禁用备份任务的执行情况:
- 大部分任务在 960 秒时完成,但最后 5 个 Reduce 任务完成耗时极长(约 300 秒);
- 总运行时间 增加 44%,达到 1283 秒。
5.5 机器故障的影响
图 3© 显示故意杀掉 200 个工作进程的情况:
- 几分钟后,集群调度器立即在原机器上重新启动了工作进程;
- 重新执行的 Map 工作快速完成,输入速率短暂出现负值;
- 总耗时 933 秒,仅比正常运行增加 5%。
由此可见,MapReduce 的容错机制非常有效,即使发生大规模故障也能快速恢复。