首先是架构对比:
Hadoop MapReduce是一个两阶段模型,它强制分为Map和Reduce阶段:
1.在Map阶段中,它执行的操作是:读取HDFS数据 ----->本地计算------>写本地磁盘
2.在Shuffle阶段:通过网络传输排序数据
3.在Reduce阶段:拉取数据 ---->聚合计算------>写HDFS
注:HDFS全程Hadoop Distributed File System,它是专门为大数据存储设计的分布式文件系统,它设计的目标是有三个,1是超大规模存储,它的单个集群可扩展到PB级 2.具有高容错性,硬件的故障视为常态而非异常,3.流式数据访问:优化数据吞吐量而非低延迟(适合批量处理而非实时访问)
它有一个数据流缺陷的问题,即每个阶段结束后数据必须落盘(磁盘IO成为瓶颈)
接下来来了解一下HadoopMapReduce的内部结构:
NameNode是主节点,它管理文件系统命名空间(文件名,目录结构,权限),它还会记录文件块的映射(Block到DataNodde的映射)
DataNode是工作节点,它实际存储数据块(默认128MB/块),定期向NameNode发送心跳(默认3秒)和块报告,并且数据块会自动复制
数据存储机制:
从客户端的视角出发,文件写入的过程是:
1.客户端联系NameNode获取可写DataNode列表
2.建立数据通道:Client -> DataNode1 ->DataNode2 ->DataNode3(默认3副本)
3.数据以包(Packet,默认64KB)为单位传输
4.确认信息通过反向管道返回客户端
高可用方案(HA)
-
双NameNode架构
-
Active NameNode:处理客户端请求
-
Standby NameNode:同步EditLog,准备故障接管
-
使用ZooKeeper实现自动故障转移(Failover时间<30秒)
-
它支持与Spark的集成 :
典型的数据流
在企业中,我们都会使用Parquet/ORC列式存储格式,避免使用TextFile文件格式,可以节省50%的存储空间,然后HDFS块大小和Spark分区数的关系最好是1:1
Spark核心部分:DAG引擎构建的过程:
-
阶段划分规则
-
窄依赖
(Narrow Dependency):父RDD的每个分区最多被子RDD的一个分区使用
-
示例:map、filter、flatMap等转换操作
-
优化机会:多个窄依赖操作合并为单个Stage
-
-
宽依赖
(Wide Dependency):父RDD的分区被多个子RDD分区使用(Shuffle产生)
-
示例:reduceByKey、join、repartition等操作
-
触发Stage边界划分
-
-
DAG内存管理架构:
参数解释:
-
ExecutorMemory(执行器内存):
-
Spark 执行器进程的总内存
-
默认由
spark.executor.memory
配置指定
-
-
StorageMemory(存储内存)- 60%:
-
用于缓存 RDD、广播变量和数据帧
-
当内存不足时使用 LRU(最近最少使用)策略淘汰数据
-
可通过
spark.memory.storageFraction
调整比例
-
-
ExecutionMemory(执行内存)- 40%:
-
用于存储 Shuffle、Join、Sort 等计算过程中的临时数据
-
在多个任务之间共享
-
剩余比例 = 1 - storageFraction
-
-
DiskSpill(磁盘溢出):
-
当 StorageMemory 空间不足时
-
使用 LRU 策略将最近最少使用的数据块溢写到磁盘
-
-
ShuffleBuffer(Shuffle缓冲区):
-
在 Task 之间共享的执行内存空间
-
用于 Shuffle 操作的数据缓存
-
当缓冲区满时会触发溢写到磁盘
-
3. 与MapReduce的对比优势
执行模型对比
阶段 | MapReduce | Spark |
---|---|---|
任务调度 | 每个Task独立JVM进程 | 线程池复用(毫秒级启动) |
数据传递 | 必须经过磁盘 | 内存优先,可选磁盘 |
中间结果 | 多轮磁盘IO | 内存缓存复用 |
容错机制 | 重新执行整个Task | 基于RDD血统局部重算 |
什么是RDD:
RDD(Resilient Distributed Dataset)是Spark的核心抽象,代表一个不可变、可分区的分布式数据集合:
1. RDD设计哲学
产生背景
-
MapReduce的缺陷:
-
中间数据必须写磁盘(迭代算法效率低下)
-
缺乏高效的数据复用机制
-
编程模型不够灵活
-
-
RDD创新点:
-
内存计算:中间结果可缓存
-
血统(Lineage)机制:通过记录转换历史实现容错
-
丰富的操作API:支持超过20种转换操作
-
2. RDD核心特性
五大核心特征
-
分区列表(Partitions):
val partitions: Array[Partition] = rdd.partitions
-
数据被划分为多个分区(Partition)
-
每个分区对应一个Task
-
可通过
repartition()
调整分区数
-
-
依赖关系(Dependencies):
val deps: Seq[Dependency[_]] = rdd.dependencies
-
窄依赖(Narrow):父RDD的每个分区最多被一个子分区使用
-
宽依赖(Wide):父RDD的分区被多个子分区使用(触发Shuffle)
-
-
计算函数(Compute Function):
def compute(split: Partition, context: TaskContext): Iterator[T]
-
定义如何从父RDD计算当前分区的数据
-
支持链式转换操作(如
map(transformA).map(transformB)
)
-
-
分区器(Partitioner):
val partitioner: Option[Partitioner] = rdd.partitioner
-
决定数据如何分布(HashPartitioner、RangePartitioner)
-
影响Shuffle效率的关键因素
-
-
首选位置(Preferred Locations):
def getPreferredLocations(split: Partition): Seq[String]
-
实现数据本地性(Data Locality)
-
例如HDFS块的位置信息
-
3. RDD生命周期
创建到执行流程
容错机制
-
血统(Lineage)恢复:
val rdd = sc.textFile("hdfs://data") .map(parse) // Lineage记录1 .filter(_.isValid) // Lineage记录2 .cache()
-
丢失缓存数据时,根据血统重新计算
-
通过
checkpoint()
切断过长血统链
-
4. RDD操作类型
转换(Transformations)
操作类型 | 示例 | 特点 |
---|---|---|
窄依赖转换 | map() , filter() | 不触发Shuffle |
宽依赖转换 | reduceByKey() , join() | 触发Shuffle |
重分区 | repartition() , coalesce() | 改变分区数 |
动作(Actions)
操作类型 | 示例 | 输出形式 |
---|---|---|
数据收集 | collect() , take(N) | 返回Driver程序 |
数据存储 | saveAsTextFile() | 写入外部存储 |
聚合计算 | count() , reduce() | 返回标量值 |
5. 企业级应用模式
缓存策略选择
// 根据数据使用频率选择存储级别 if (rdd会被多次使用) { rdd.persist(StorageLevel.MEMORY_ONLY_SER) } else if (rdd太大无法完全放入内存) { rdd.persist(StorageLevel.MEMORY_AND_DISK) }
性能调优技巧
-
分区数优化:
// 合理设置分区数(建议每个分区128MB) val optimalPartitions = (dataSize / 128MB).toInt rdd.repartition(optimalPartitions)
-
避免数据倾斜:
// 对倾斜Key添加随机前缀 val skewedRdd = rdd.map(k => (s"${Random.nextInt(10)}_$k", v))
-
广播变量优化Shuffle:
val smallTable = sc.broadcast(lookupData) largeRdd.map { case (k, v) => (smallTable.value.getOrElse(k, default), v) }
6. RDD与DataFrame对比
特性 | RDD | DataFrame |
---|---|---|
数据类型 | 任意对象 | 结构化数据(Row类型) |
优化方式 | 开发者手动优化 | Catalyst自动优化 |
内存使用 | 较高(Java对象开销) | 较低(Tungsten二进制格式) |
API类型 | 函数式编程 | SQL风格 |
序列化 | Java序列化 | 高效编码 |
7. 适用场景
推荐使用RDD的场景:
-
非结构化数据处理(如文本、图像)
-
需要精细控制分区的场景
-
实现自定义的复杂算法
推荐使用DataFrame的场景:
-
结构化数据分析(类似SQL查询)
-
需要自动优化的场景
-
与其他大数据组件集成(如Hive)
2. Spark和Hadoop的MapReduce的性能差异:
基准测试对比
任务类型 | MapReduce耗时 | Spark耗时 | 优势倍数 |
---|---|---|---|
迭代算法(PageRank) | 120分钟 | 8分钟 | 15x |
日志分析(TB级) | 45分钟 | 6分钟 | 7.5x |
机器学习(K-means) | 90分钟 | 11分钟 | 8x |
性能差异根源
-
磁盘IO:MapReduce每个阶段写磁盘 vs Spark内存计算
-
任务启动:MapReduce每个Task启动JVM(秒级) vs Spark复用线程池(毫秒级)
-
数据交换:MapReduce必须Shuffle vs Spark可优化窄依赖操作
4. 生态系统对比
组件 | Hadoop MapReduce | Spark |
---|---|---|
SQL引擎 | 需依赖Hive(启动MR任务) | 原生Spark SQL(直接执行) |
机器学习 | 需Mahout库(效率低) | 原生MLlib(内存计算) |
流处理 | 无(需Storm等外部系统) | 结构化流(微批/连续模式) |
图计算 | 无(需Giraph) | GraphX(集成RDD) |
5. 容错机制对比
-
MapReduce
-
Task失败时重新调度(数据从HDFS重新读取)
-
可靠性高但恢复速度慢(需重新计算整个Task)
-
-
Spark
-
基于RDD血统(Lineage)的弹性恢复
-
丢失RDD分区时,只需根据DAG重新计算父分区
-
Checkpoint机制可切断过长血统链
-