文章目录
一、Hadoop
介绍
- 三大组件
HDFS: 解决分布式存储问题
MR:解决分布式计算问题
Yarn:负责整个集群资源的管理和调度
1. HDFS
1. 1应用场景
- 适合的:存储非常大的文件
- 不适合:大量小文件。 (文件的元数据保存在NameNode的内存中, 整个文件系统的文件数量会受限于NameNode的内存大小。)
1.2 HDFS架构
HDFS是一个 主/从(Mater/Slave体系结构 , HDFS由四部分组成: HDFS Client、NameNode、DataNode和Secondary NameNode。
- Client:客户端
文件切分。文件上传 HDFS 的时候,Client 将文件切分成 一个一个的Block,然后进行存储。- NameNode:就是 master,它是一个主管、管理者。
存储元数据信息,管理数据块映射信息。
配置副本策略
处理客户端读写请求
监控DataNode健康状况 10分钟没有收到DataNode报告认为Datanode死掉了- DataNode:就是Slave。NameNode 下达命令,DataNode 执行实际的读/写操作。
- 存储用户的文件对应的数据块(Block)
- 要定期向NN发送心跳信息,汇报本身及其所有的block信息,健康状况
- Secondary NameNode:并非 NameNode 的热备。当NameNode 挂掉的时候,它并不能马上替换 NameNode 并提供服务。
(1) 辅助 NameNode,分担其工作量。
(2) 定期合并 fsimage和fsedits,并推送给NameNode。
(3) 在紧急情况下,可辅助恢复 NameNode。
1.3 HDFS的容错机制 / 高可用(zookeeper)
HDFS如何实现高可用(HA)
- 数据存储故障容错
- 磁盘介质在存储过程中受环境或者老化影响,数据可能错乱
- 对于存储在 DataNode 上的数据块,计算并存储校验和(CheckSum)
- 读取数据的时候, 重新计算读取出来的数据校验和, 校验不正确抛出异常, 从其它DataNode上读取备份数据
- 磁盘故障容错
- DataNode 监测到本机的某块磁盘损坏
- 将该块磁盘上存储的所有 BlockID 报告给 NameNode
- NameNode 检查这些数据块在哪些DataNode上有备份,
- 通知相应DataNode, 将数据复制到其他服务器上
- DataNode故障容错
- 通过心跳和NameNode保持通讯
- 超时未发送心跳, NameNode会认为这个DataNode已经宕机
- NameNode查找这个DataNode上有哪些数据块, 以及这些数据在其它DataNode服务器上的存储情况
- 从其它DataNode服务器上复制数据
- NameNode故障容错
- 主从热备 secondary namenode
- zookeeper配合 master节点选举 负责数据一致性保证
双NameNode消除单点故障
- 概述:
- HDFS HA功能通过配置Active/Standby两个NameNodes实现在集群中对NameNode的热备来解决上述问题。如果出现故障,如机器崩溃或机器需要升级维护,这时可通过此种方式将NameNode很快的切换到另外一台机器。
- 工作要点
- 内存中各自保存一份元数据;
Edits日志只有Active状态的NameNode节点可以做写操作;两个NameNode都可以读取Edits;- 需要一个状态管理功能模块
实现了一个zkfailover,常驻在每一个namenode所在的节点,每一个zkfailover负责监控自己所在NameNode节点,利用zk进行状态标识,当需要进行状态切换时,由zkfailover来负责切换,切换时需要防止brain split现象的发生。脑裂:同时出现两个Active状态的nameNode
防止脑裂:1)ssh发送kill指令。2)调用用户自定义脚本程序。
- 必须保证两个NameNode之间能够ssh无密码登录
- 隔离(Fence),即同一时刻仅仅有一个NameNode对外提供服务
HDFS-HA自动故障转移工作机制
- 自动故障转移为HDFS部署增加了两个新组件:ZooKeeper和ZKFailoverController(ZKFC)进程
** Zookeeper (CAP理论)
CAP
Consistency(一致性) Availability(可用性) Partition tolerance(分区容忍性)
这三个性质对应了分布式系统的三个指标: 而CAP理论说的就是:一个分布式系统,不可能同时做到这三点。
有了follower,为什么还有observer?
- observer
角色与 Follower 类似,但是无投票权。 Zookeeper 需保证高可用和强一致性,为了支持更多的客户端,需要增加更多 Server; Server 增多,投票阶段延迟增大,影响性能;
引入 Observer,Observer 不参与投票; Observers 接受客户端的连接,并将写请求转发给 leader 节点; 加入更多 Observer 节点,提高伸缩性,同时不影响吞吐率。
Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议。
- Zab协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步)。
当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,
当领导者被选举出来,且大多数Server完成了和leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和Server具有相同的系统状态。
zookeeper工作原理
- Zookeeper 的核心是原子广播,这个机制保证了各个 server 之间的同步。实现这个机制的协议叫做 Zab 协议。 Zab 协议有两种模式,它们分别是恢复模式和广播模式。
- 当服务启动或者在领导者崩溃后, Zab 就进入了恢复模式,当领导者被选举出来,且大多数 server 的完成了和 leader 的状态同步以后,恢复模式就结束了。
- 状态同步保证了 leader 和 server 具有相同的系统状态
- 一旦 leader 已经和多数的 follower 进行了状态同步后,他就可以开始广播消息了,即进入广播状态。这时候当一个 server 加入 zookeeper 服务中,它会在恢复模式下启动,发现 leader,并和 leader 进行状态同步。待到同步结束,它也参与消息广播。 Zookeeper 服务一直维持在 Broadcast 状态,直到 leader 崩溃了或者 leader 失去了大部分的 followers 支持。
- 广播模式需要保证 proposal 被按顺序处理,因此 zk 采用了递增的事务 id号(zxid)来保证。所有的提议(proposal)都在被提出的时候加上了 zxid。
- 实现中 zxid 是一个 64 位的数字,它高 32 位是 epoch 用来标识 leader 关系是否改变,每次一个 leader 被选出来,它都会有一个新的 epoch。低 32 位是个递增计数。
- 当 leader 崩溃或者 leader 失去大多数的 follower,这时候 zk进入恢复模式,恢复模式需要重新选举出一个新的 leader,让所有的 server 都恢复到一个正确的状态。
zab协议
ZAB 协议 中有两个重要的元素:事务编号Zxid、epoch。
Zxid 是一个 64位的数字,其中低 32 位是一个简单的单调递增的计数器,针对客户端每一个事务请求,计数器加 1; 而高32 位则代表 Leader 周期 epoch 的编号,每个当选产生一个新的 Leader 服务器,就会从这个 Leader 服务器上取出其本地日志中的最大事务 ZXID ,并从中读取 epoch 值,然后加 1 ,以此作为新的epoch.并降低 32 位从 0 开始计数。
Zxid 类似 RDBMS 中的事务 ID ,用于标记一次更新操作的 Proposal (提议) ID, 为了保证顺序性,该 zxid 必须单调递增。epoch:可以理解为当前集群所处的年代或者周期,每个 leader 都有自己的年号,所以每次改朝换代之后,leader 变更之后,都会在前一个年代的基础上加1, 这样就算旧 leader 的崩溃恢复后,也没有人听他的,因为 follower 只听从当前代的
leader 的命令。
zab协议与raft协议的比较
- 领导者选举:
- ZAB 采用的“见贤思齐、相互推荐”的快速领导者选举(Fast Leader Election),节点间通过PK竞争(资本是所持有的信息)看哪个节点更适合做Leader,一个节点PK后,会将选票信息广播出去,最终选举出了大多数节点中数据最完整的节点。
- Raft 采用的是“一张选票、先到先得”的自定义算法(注:里面包含了一个随机等待时间的概念,来保证最多几次选举就能完整选举过程。),这里简单说一下就是一个节点发现leader挂了,就选举自己为leader,然后通知其他节点,其他节点把选票投给第一个通知它的节点。(注:这里其实也会涉及到PK,根据数据的完整性以及任期等信息,如果通知它的节点 没有当前节点的数据完整等那么 当前节点是不会将选票投给该节点,这一块概念很多,建议看之前上面的博客文章)
以上看来,Raft 的领导者选举,需要通讯的消息数更少,选举也更快。
主从架构下,leader 崩溃,数据一致性怎么保证?
leader 崩溃之后,集群会选出新的 leader,然后就会进入恢复阶段,新的 leader 具有所有已经提交的提议,因此它会保证让 followers 同步已提交的提议,丢弃未提交的提议(以 leader 的记录为准),这就保证了整个集群的数据一致性。
选举 leader 的时候,整个集群无法处理写请求的,如何快速进行 leader 选举?
这是通过 Fast Leader Election 实现的,leader 的选举只需要超过半数的节点投票即可,这样不需要等待所有节点的选票,能够尽早选出 leader。
zookeeper扩容 / 增加集群节点
- 增加集群节点后保证节点总数为奇数避免资源的浪费(遵循过半机制)
- 增加集群节点选择observer类型节点(使用follower节点会增加选举时间效率低)
1.4 HDFS文件副本机制
默认副本数=3 。 数据可靠性和成本综合考虑的结果
- 第一个副本存本机
- 第二个副本块存跟本机同机架内的其他服务器节点
- 第三个副本块存不同机架的一个服务器节点上
1.5 HDFS文件读写过程
读取
- Client向NameNode发起RPC请求,来确定请求文件block所在的位置;
- 对于每个block,NameNode 都会返回含有该 block 副本的 DataNode ==地址;
- Client 选取排序靠前的 DataNode 来读取 block
read 方法是并行的读取 block 信息,不是一块一块的读取;(与写入区分)
写入
- Client向NameNode发起文件上传请求,NameNode返回是否可以上传
- Client请求第一个block应该传到哪些DataNode上
- NameNode返回可用的DataNode的地址,A,B,C
- Client 请求 3 台 DataNode 中的一台 A 上传数据(本质上是一个 RPC 调用,建立 pipeline), A 收到请求会继续调用 B, 然后 B 调用 C, 将整个 pipeline 建立完成, 后逐级返回 client
1.6 FsImage 和 Edits、SecondNamenode
Edits
- edits 存放了客户端最近一段时间的操作日志,只进行追加操作,效率很高。
- 客户端对 HDFS 进行写文件时会首先被记录在 edits 文件中
- edits 修改时元数据也会更新
fsimage
- NameNode 中关于元数据的镜像, 一般称为检查点, fsimage 存放了一份比较完整的元数据信息
- 随着 edits 内容增大, 就需要在一定时间点和 fsimage 合并
secondNamenode
- 专门用于fsimage和Edits合并
1.7 NameNode工作机制
- 第一阶段:NameNode启动
(1)第一次启动NameNode格式化后,创建Fsimage和Edits文件。如果不是第一次启动,直接加载编辑日志和镜像文件到内存。
(2)客户端对元数据进行增删改的请求。
(3)NameNode记录操作日志,更新滚动日志。
(4)NameNode在内存中对数据进行增删改。
1.8 DataNode工作机制
- DataNode启动后向NameNode注册,通过后,周期性(1小时)的向NameNode上报所有的块信息。
- 心跳返回结果带有NameNode给该DataNode的命令如复制块数据到另一台机器,或删除某个数据块。如果超过10分钟没有收到某个DataNode的心跳,则认为该节点不可用。
DataNode节点保证数据完整性的方法
1)当DataNode读取Block的时候,它会计算CheckSum。
2)如果计算后的CheckSum,与Block创建时值不一样,说明Block已经损坏。
3)Client读取其他DataNode上的Block。
4)DataNode在其文件创建后周期验证CheckSum
1.9 HDFS上的小文件
- 什么是小文件? 远远小于hadoop block大小的文件称为小文件
- 产生原因:
- 动态分区插入数据,产生大量的小文件,从而导致map数量剧增。
- reduce数量越多,小文件也越多(reduce的个数和输出文件是对应的)。
- 数据源本身就包含大量的小文件。
- 引起的问题:
- 从Hive的角度看,小文件会开很多map,一个map开一个JVM去执行,所以这些任务的初始化,启动,执行会浪费大量的资源,严重影响性能。
- 大量的小文件会耗尽NameNode中的大部分内存。但注意,存储小文件所需要的磁盘容量和数据块的大小无关。
- NameNode的内存管理: NameNode会存储文件的元数据
- MapReduce性能:大量的小文件会导致大量的随机磁盘IO
- 解决方法:
- 解决NameNode内存问题
- Hadoop Archive (HAR) Files:
将许多小文件打包到更大的HAR文件来缓解NameNode的内存问题。HDFS存档文件对内还是一个个独立文件,对NameNode而言却是一个整体,减少了NameNode的内存。- Federated NameNodesL:
允许在集群中拥有多个NameNode,每个NameNode存储对象元数据的一个子集
- 解决MapReduce性能问题:
- 改变获取文件的间隔
- 批处理文件合并
- 通过参数进行调节,设置map/reduce端的相关参数,如下
设置map输入合并小文件的相关参数:
//每个Map最大输入大小(这个值决定了合并后文件的数量)
set mapred.max.split.size=256000000;
//一个节点上split的至少的大小(这个值决定了多个DataNode上的文件是否需要合并)
set mapred.min.split.size.per.node=100000000;
//一个交换机下split的至少的大小(这个值决定了多个交换机上的文件是否需要合并)
set mapred.min.split.size.per.rack=100000000;
//执行Map前进行小文件合并
set hive.input.format=org.apache.hadoop.hive.ql.io.CombineHiveInputFormat;
设置map输出和reduce输出进行合并的相关参数:
//设置map端输出进行合并,默认为true
set hive.merge.mapfiles = true
//设置reduce端输出进行合并,默认为false
set hive.merge.mapredfiles = true
//设置合并文件的大小
set hive.merge.size.per.task = 256*1000*1000
//当输出文件的平均大小小于该值时,启动一个独立的MapReduce任务进行文件merge。
set hive.merge.smallfiles.avgsize=16000000
2. MapReduce
1. 介绍
MapReduce是一种并行可扩展计算模型,并且有较好的容错性,主要解决海量离线数据的批处理。实现下面目标
★ 易于编程
★ 良好的扩展性
★ 高容错性
MapReduce有哪些角色?各自的作用是什么?
- MapReduce由JobTracker和TaskTracker组成。JobTracker负责资源管理和作业控制,TaskTracker负责任务的运行。
MR运行流程
MapReduce完整运行流程
- 在客户端启动一个作业。
- 向JobTracker请求一个Job ID。
- 将运行作业所需要的资源文件复制到HDFS上,包括MapReduce程序打包的jar文件、配置文件和客户端计算所得的计算划分信息。这些文件都存放在JobTracker专门为该作业创建的文件夹中。文件夹名为该作业的Job ID。jar文件默认会有10个副本(mapred.submit.replication属性控制);输入划分信息告诉了JobTracker应该为这个作业启动多少个map任务等信息。
- JobTracker接收到作业后,将其放在一个作业队列里,等待作业调度器对其进行调度(这里是不是很像微机中的进程调度呢),当作业调度器根据自己的调度算法调度到该作业时,会根据输入划分信息为每个划分创建一个map任务,并将map任务分配给TaskTracker执行。对于map和reduce任务,TaskTracker根据主机核的数量和内存的大小有固定数量的map槽和reduce槽。这里需强调的是:map任务不是随随便便地分配给某个TaskTracker的,这里有个概念叫:数据本地化(Data-Local)。意思是:将map任务分配给含有该map处理的数据块的TaskTracker上,同事将程序jar包复制到该TaskTracker上来运行,这叫“运算移动,数据不移动”。而分配reduce任务时并不考虑数据本地化。
- TaskTracker每隔一段时间会给JobTracker发送一个心跳,告诉JobTracker它依然在运行,同时心跳中还携带者很多信息,比如当前map任务完成的进度等信息。当JobTracker收到作业的最后一个任务完成信息时,便把该作业设置成“成功”。当JobTracker查询状态时,它将得知任务已完成,便显示一条消息给用户。
2. MR工作流程
大致流程
- input组件从HDFS读取数据;
- 按照规则进行分片,形成若干个spilt;
- 进行Map
- 打上分区标签(patition)
- 数据入环形缓冲区(KVbuffer)
- 原地排序(快排),并溢写(sort+spill)
- combiner+merge(归并排序),落地到磁盘
- shuffle到reduce缓存
- 继续归并排序(mergesotr)
- reduce
- 输出到HDFS
- map task
程序会根据InputFormat将输入文件分割成splits,每个split会作为一个map task的输入,每个map task会有一个内存缓冲区, 输入数据经过map阶段处理后的中间结果会写入内存缓冲区,并且决定数据写入到哪个partitioner,当写入的数据到达内存缓冲区的的阀值(默认是0.8),会启动一个线程将内存中的数据溢写入磁盘,同时不影响map中间结果继续写入缓冲区。在溢写过程中, MapReduce框架会对key进行排序,如果中间结果比较大,会形成多个溢写文件,最后的缓冲区数据也会全部溢写入磁盘形成一个溢写 文件(最少有一个溢写文件),如果是多个溢写文件,则最后合并所有的溢写文件为一个文件。- reduce task
当所有的map task完成后,每个map task会形成一个最终文件,并且该文件按区划分。reduce任务启动之前,一个map task完成后, 就会启动线程来拉取map结果数据到相应的reduce task,不断地合并数据,为reduce的数据输入做准备,当所有的map tesk完成后, 数据也拉取合并完毕后,reduce task 启动,最终将输出输出结果存入HDFS上。
map、map端shuffle、reduce端shuffle、reduce
Map端流程分析
Map端流程分析
每个输入分片会让一个map任务来处理,默认情况下,以HDFS的一个块的大小(默认64M)为一个分片,当然我们也可以设置块的大小。map输出的结果会暂且放在一个环形内存缓冲区中(该缓冲区的大小默认为100M,由io.sort.mb属性控制),当该缓冲区快要溢出时(默认为缓冲区大小的80%,由io.sort.spill.percent属性控制),会在本地文件系统中创建一个溢出文件,将该缓冲区中的数据写入这个文件。
在写入磁盘之前,线程首先根据reduce任务的数目将数据划分为相同数目的分区,也就是一个reduce任务对应一个分区的数据。这样做是为了避免有些reduce任务分配到大量数据,而有些reduce任务却分到很少数据,甚至没有分到数据的尴尬局面。其实分区就是对数据进行hash的过程。然后对每个分区中的数据进行排序,如果此时设置了Combiner,将排序后的结果进行Combiner操作,这样做的目的是让尽可能少的数据写入到磁盘。
当map任务输出最后一个记录时,可能会有很多的溢出文件,这时需要将这些文件合并。合并的过程中会不断地进行排序和combiner操作,目的有两个:
1、尽量减少每次写入磁盘的数据量;
2、尽量减少下一复制阶段网络传输的数据量。
最后合并成了一个已分区且已排序的文件。为了减少网络传输的数据量,这里可以将数据压缩,只要将mapred.compress.map.out设置为true就可以。
数据压缩:Gzip、Lzo、snappy。将分区中的数据拷贝给相对应的reduce任务。有人可能会问:分区中的数据怎么知道它对应的reduce是哪个呢?其实map任务一直和其父TaskTracker保持联系,而TaskTracker又一直和 jobTracker保持心跳。所以JobTracker中保存了整个集群中的宏观信息。只要reduce任务向JobTracker获取对应的map输出位置就OK了。
排序:
- 在缓存区是快速排序
- 多个spill文件是归并排序
Map阶段为什么要排序?
- sort是用来shuffle的,shuffle就是把key相同的东西弄一起去,其实不一定要sort也能shuffle(还可以用hash,但是sort的好处是他可以通过外排降低内存使用量
Map端shuffle分析
Map端shuffle分析
- 每个map task都有一个内存缓冲区,存储着map的输出结果,当缓冲区快满的时候需要将缓冲区的数据以一个临时文件的方式存放到磁盘,当整个map task结束后在对磁盘中这个map task产生的所有临时文件做一个合并Merge【归并排序】,生成最终的正式输出文件,然后等待reduce task来拉数据。
- MapReduce提供Partitioner接口,作用就是根据key或value及reduce的数量来决定当前的输出数据最终应该交由哪个reduce task处理。默认对key hash后再以reduce task数据取模。默认的取模方式只是为了平均reduce的处理能力,如果用户自己对Partitioner有需求,可以定制并设置到job上。
- 至此,map端的所有工作都已经结束,最终生成的这个文件也存放在TaskTracker够得到的某个本地目录中。每个reduce task不断地通过RPC从JobTRacker那获取map task是否完成的信息,如果reduce task得到通知,获知某台TaskTracker上的map task执行完成,Shuffle的后半段过程开始启动。
环形缓冲区的作用是批量收集map结果,减少磁盘IO的影响
- 为什么要有环形缓冲区?
- 环形缓冲区不需要重新申请新的内存,始终用的都是这个内存空间。避免full gc。环形缓冲
区从头到尾都在用那一个内存,不断重复利用,因此完美的规避了Full GC导致的各种问题,同时也规避了频繁申请内存引发的其他问题。- 环形缓冲区同时做了两件事情:1、排序;2、索引。在这里一次排序,将无序的数据变为有序,写磁盘的时候顺序写,读数据的时候顺序读,效率高非常多!
在这里设置索引区也是为了能够持续的处理任务。每读取一段数据,就往索引文件里也写一段,这样在排序的时候能加快速度。
每个分区的元信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。
- 写入缓存之前,key与value值都会被序列化成字节数组。
- 结构
- 环形缓冲区分为三块,空闲区、数据区、索引区。
- 环形缓冲区写入的时候,有个细节:数据是从赤道的右边开始写入,索引(每次申请4kb)是从赤道是左边开始写。这个设计很有意思,这样两个文件各是各的,互不干涉
溢写
在数据和索引的大小到了mapreduce.map.sort.spill.percent参数设置的比例时(默认80%,这个是调优的参数),会有两个动作:
- 对写入的数据进行原地排序,并把排序好的数据和索引spill到磁盘上去;
- 溢写是由单独线程来完成,不影响往缓冲区写map结果的线程。溢写线程启动时不应该阻止map的结果输出,所以整个缓冲区有个溢写的比例spill.percent。比例默认是0.8,也就是当缓冲区的数据值已经达到阈值(buffer size * spill percent = 100MB * 0.8 = 80MB),溢写线程启动,锁定这80MB的内存,执行溢写过程。map task的输出结果还可以往剩下的20MB内存中写,互不影响。
- 利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号Partition进行排序,然后按照key进行排序。这样,经过排序后,数据以分区为单位聚集在一起,且同一分区内所有数据按照key有序。
- 在空闲的20%区域中,重新算一个新的赤道,然后在新赤道的右边写入数据,左边写入索引;
- 当20%写满了,但是上一次80%的数据还没写到磁盘的时候,程序就会panding一下,等80%空间腾出来之后再继续写。
combine
- 什么是Combine?
在针对每个reduce端而合并数据时,有些数据可能像这样:“aaa”/1,“aaa”/1。 对于wordcount例子,只是简单地统计单词出现的次数,如果在同一个map task的结果中有很多像“aaa”一样出现多次的key,我们就应该把它们的值合并到一块,这个过程叫reduce也叫combine。
- combiner 和 reducer 的区别在于运行的位置。Combiner 是在每一个 maptask 所在的节点运行Reducer 是接收全局所有 Mapper 的输出结果
- 如果client设置过Combiner,那么现在就是使用Combiner的时候了。将有相同key的key/value对的value加起来,减少溢写到磁盘的数据量。Combiner会优化MapReduce的中间结果,所以它在整个模型中会多次使用。
- 哪些场景才能使用Combiner呢?
Combiner的输出是Reducer的输入,Combiner绝不能改变最终的计算结果。所以,Combiner只应该用于那种Reduce的输入key/value与输出key/value类型完全一致,且不影响最终结果的场景。比如累加,最大值等。Combiner的使用一定得慎重,如果用好,它对job执行效率有帮助,反之会影响reduce的最终结果。
Reduce端shuffle分析
- copy过程,简单地拉取数据。Reduce进程启动一些数据copy线程(Fetcher),通过http方式请求map task所在的TaskTracker获取map task的输出文件。因为map task早已结束,这些文件就归TaskTracker管理在本地磁盘中。
- Merge阶段。这里的merge和map端的merge动作相同,只是数组中存放的是不同map端copy来的数值。copy过来的数据会先放入内存缓冲区中,这里的缓冲区大小要比map端更为灵活,它基于JVM的heap size设置,因为Shuffle阶段Reducer不运行,所以应该把绝大部分的内存都给Shuffle使用。
- Merge有三种形式:1、内存到内存;2、内存到磁盘;3、磁盘到磁盘。默认情况下第一种形式不启用,让人比较困惑。当内存中的数据量到达一定阈值,就启动内存到磁盘的merge。与map端类似,这也是溢写的过程,在这个过程中如果你设置有Combiner,也是会启用的,然后在磁盘中生成了众多溢写文件。第二种merge方式一直在运行,直到没有map端的数据时才结束,然后启动第三种磁盘到磁盘的merge方式生成最终的那个文件。
Reduce端分析
- reduce会接收到不同map任务传来的数据,并且每个map传来的数据都是有序的。如果
reduce端接收的数据量相当小,则直接存储在内存中(缓冲区大小由mapred.job.shuffle.input.buffer.percent
属性控制,表示用作此用途的堆空间百分比),如果数据量超过了该缓冲区大小的一定比例(由mapred.job.shuffle.merg.percent
决定),则对数据合并后溢写到磁盘中。- 随着溢写文件的增多,后台线程会将它们合并成一个更大的有序的文件,这样做是为了给后面的合并节省空间。其实不管在map端还是在reduce端,MapReduce都是反复地执行排序,合并操作,现在终于明白了有些人为什么会说:排序是hadoop的灵魂。
- 合并的过程中会产生许多的中间文件(写入磁盘了),但MapReduce会让写入磁盘的数据尽可能地少,并且最后一次合并的结果并没有写入磁盘,而是直接输入到reduce函数。
- Reducer的输入文件。不断地merge后,最后会生成一个“最终文件”。为什么加引号?因为这个文件可能存在于磁盘上,也可能存在于内存中。对我们来说,希望它存放于内存中,直接作为Reducer的输入,但默认情况下,这个文件是存放于磁盘中的。当Reducer的输入文件已定,整个Shuffle才最终结束。然后就是Reducer执行,把结果放到HDSF上。
注意:对MapReduce的调优在很大程度上就是对MapReduce Shuffle的性能的调优。
3. MapReduce出现数据倾斜
什么是数据倾斜
- 就是大量的相同key被partition分配到一个分区里,导致整个计算过程过慢,这种情况就是发生了数据倾斜。造成了"一个人累死,其他人闲死"的情况,这违背了并行计算的初衷,整体的效率是十分低下的。
数据倾斜的原因
- 无论是MR还是Spark任务进行计算的时候,都会触发Shuffle动作,一旦触发,所有相同key的值就会拉到一个或几个节点上,就容易发生单个节点处理数据量爆增的情况。
- key分布不均匀
- 业务数据本身的特性
- 建表时考虑不周
- 某些SQL语句本身就有数据倾斜
怎么解决
- 通过对Shuffle key “加盐”(即add salt)优化,也就是在哈希函数中对Key加入随机噪声,避免出现数据倾斜
- 使用更好的分区函数,使分区尽可能均匀
- 设置Combiner函数,减少数据规模
如何定位数据倾斜
数据倾斜大多数都是大 key 问题导致的。
如何判断是大 key 导致的问题,可以通过下面方法:
- 通过时间判断:如果某个 reduce 的时间比其他 reduce 时间长的多
- 通过任务 Counter 判断:Counter 会记录整个 job 以及每个 task 的统计信息,查看输入记录数,如果远大于普通的task,则此处发生数据倾斜
- 定位 SQL 代码:
- 确定任务卡住的 stage,通过 jobname 确定 stage:一般 Hive 默认的 jobname 名称会带上 stage 阶段
- 确定 SQL 执行代码
确定了执行阶段,即 stage。通过执行计划,则可以判断出是执行哪段代码时出现了倾斜。
此外,数据倾斜只会发生在shuffle中
spark中会导致shuffle操作的有以下几种算子
- repartition类的操作:比如repartition、repartitionAndSortWithinPartitions、coalesce等
【重分区: 一般会shuffle,因为需要在整个集群中,对之前所有的分区的数据进行随机,均匀的打乱,然后把数据放入下游新的指定数量的分区内】- byKey类的操作:比如reduceByKey、groupByKey、sortByKey等
【因为你要对一个key,进行聚合操作,那么肯定要保证集群中,所有节点上相同的key,一定是到同一个节点上进行处理】- join类的操作:比如join、cogroup等
【两个rdd进行join,就必须将相同 join key的数据,shuffle到同一个节点上,然后进行相同key的两个rdd数据的笛卡尔乘积】
数据倾斜详细解决方案
参考
- 使用Hive ETL(提取、转换和加载) 预处理数据
- 方案使用场景:
导致数据倾斜的是Hive表。如果该Hive表中的数据本身很不均匀,而且业务场景需要频繁的使用Spark对Hive表执行某个分析操作,那么比较适合使用这种技术方案。- 思路: 此时可以评估,是否可以通过Hive来进行数据预处理。即通过Hive ETL 预先对数据按照Key进行聚合,或者是预先和其他表进行join,然后再Spark作业中针对的数据源就是预处理后的Hive表。此时由于数据已经预先进行过聚合或者join操作了,那么在Spark作业中也就不需要使用原先的shuffle类算子执行这类操作了。
- 原理: 从根源上解决了数据倾斜,因为彻底避免了在Spark中执行shuffle类算子。 但是因为毕竟数据本身就存在分布不均匀的问题,所以在Hive ETL中进行groubBy或者join等shuffle操作时,还是会发生数据倾斜,导致Hive ETL速度很慢。只是避免了Spark程序发生数据倾斜。
- 经验:
在一些Java系统与Spark结合使用的项目中,会出现Java代码频繁调用Spark作业的场景,而且对Spark作业的执行性能要求很高,就比较适合使用这种方案。将数据倾斜提前到上游的Hive ETL,每天仅执行一次,只有那一次是比较慢的,而之后每次Java调用Spark作业时,执行速度都会很快,能够提供更好的用户体验。
- 过滤少数导致倾斜的key
- 方案使用场景: 若发现导致倾斜的key就少数几个,并且对计算本身的影响并不大。比如99%的key对应10条数据,但只有一个key对应100万数据。
- 思路:若判断少数几个数据量特别多的key对作业的执行和计算结果不是那么特别重要,可以直接过滤掉那几个key。如在Spark SQL中就可以使用where子句过滤掉这些key,或者在Spark Core中对RDD执行filter算子过滤掉这些key。如果需要每次作业执行时,动态判定哪些key的数据量最多然后过滤,可以使用sample算子对RDD进行采样,然后计算每个key的数量,取数据量最多的key过滤即可。
- 缺点: 适用场景不多,大多数情况下,导致倾斜的key还是很多的,并不是只有少数几个。
- 提高shuffle操作的并行度
- 原理:
增加shuffle read task 的数量,可以让原本分配给一个task的多个key分配给多个task,从而让每个task处理比原来更少的数据。举例来说,如果原本有5个key,每个key对应10条数据,这5个key都是分配给一个task的,那么这个task就要处理50条数据。而增加了shuffle read task以后,每个task就分配到一个key,即每个task就处理10条数据,那么自然每个task的执行时间都会变短了。
- 两阶段聚合(局部聚合+全局聚合)
- 方案使用场景:
对RDD执行reduceByKey等聚合类shuffle算子或者在Spark SQL中使用group by语句进行分组聚合时,比较适用这种方案。- 思路:
这个方案的核心实现思路就是进行两阶段聚合。第一次是局部聚合,先给每个key都打上一个随机数,比如10以内的随机数,此时原先一样的key就变成不一样的了,比如(hello,1) (hello, 1) (hello, 1) (hello, 1),就会变成(1_hello, 1) (1_hello, 1)(2_hello, 1) (2_hello,1)。接着对打上随机数后的数据,执行reduceByKey等聚合操作,进行局部聚合,那么局部聚合结果,就会变成了(1_hello, 2) (2_hello,2)。然后将各个key的前缀给去掉,就会变成(hello,2)(hello,2),再次进行全局聚合操作,就可以得到最终结果了,比如(hello, 4)。- 方案优点:
对于聚合类的shuffle操作导致的数据倾斜,效果是非常不错的。通常都可以解决掉数据倾斜,或者至少是大幅度缓解数据倾斜,将Spark作业的性能提升数倍以上。- 方案缺点: 仅仅适用于聚合类的shuffle操作,适用范围相对较窄。如果是join类的shuffle操作,还得用其他的解决方案。
- 将reduce join 转为map join
- 方案使用场景: