大数据组件学习笔记
1. 大数据组件概述
1.1 四大核心组件
| 组件 | 简单理解 | 主要作用 |
|---|---|---|
| HDFS | 分布式文件柜 | 存储大文件,分片备份 |
| Hive | 大数据的Excel | 用SQL查询大数据 |
| HBase | 超大号Excel表格 | NoSQL快速查询 |
| RDD | 分布式数据集合 | 并行处理数据 |
1.2 组件关系图
graph TD
A[原始数据] --> B[HDFS存储]
B --> C[Hive查询]
B --> D[HBase快速访问]
B --> E[Spark RDD处理]
C --> F[分析结果]
D --> F
E --> F
2. RDD 深度解析
2.1 RDD 的本质
RDD ≠ 数据容器,RDD = 计算描述
// RDD 内部结构
class RDD {
Partition[] partitions; // 分区信息
Function compute; // 计算函数
Dependency[] dependencies; // 依赖关系
Partitioner partitioner; // 分区器
String[] preferredLocations; // 首选位置
}
2.2 RDD 三大特性
2.2.1 分布式 (Distributed)
数据分散存储:
机器A: [1, 2, 3]
机器B: [4, 5, 6, 7]
机器C: [8, 9, 10]
2.2.2 弹性 (Resilient)
容错机制:
1. 记录血缘关系
2. 检测节点故障
3. 根据血缘关系重新计算
4. 程序继续运行
2.2.3 懒惰计算 (Lazy Evaluation)
// 这些操作不会立即执行
JavaRDD<Integer> rdd1 = sc.parallelize(data);
JavaRDD<Integer> rdd2 = rdd1.filter(x -> x > 5);
JavaRDD<Integer> rdd3 = rdd2.map(x -> x * 2);
// 只有Action操作才真正计算
List<Integer> result = rdd3.collect(); // 现在才开始计算
2.3 RDD 操作分类
| 操作类型 | 特点 | 示例 |
|---|---|---|
| Transformation | 懒惰执行,返回新RDD | map, filter, mapPartitions |
| Action | 立即执行,返回结果 | collect, count, reduce |
3. RDD 的内部结构详解
3.1 RDD 到底是什么?
RDD 本质上是一个数据结构的抽象描述,不是真正存储数据的容器
3.2 RDD 的内部组成
3.2.1 RDD 就像一张"数据地图"
// RDD 内部主要包含这些信息:
class RDD {
// 1. 分区信息 - 数据被分成几块,每块在哪里
Partition[] partitions;
// 2. 计算函数 - 如何从父RDD计算出当前RDD
Function compute;
// 3. 依赖关系 - 当前RDD依赖哪些父RDD
Dependency[] dependencies;
// 4. 分区器 - 数据如何分区(可选)
Partitioner partitioner;
// 5. 首选位置 - 每个分区最好在哪台机器上计算
String[] preferredLocations;
}
3.3 用具体例子理解 RDD 结构
3.3.1 创建第一个 RDD
List<Integer> data = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
JavaRDD<Integer> rdd1 = sc.parallelize(data, 3);
rdd1 的内部结构:
RDD1 {
partitions: [
Partition0 -> [1, 2, 3] (在机器A)
Partition1 -> [4, 5, 6, 7] (在机器B)
Partition2 -> [8, 9, 10] (在机器C)
]
compute: 从本地集合读取数据
dependencies: [] (没有父RDD)
preferredLocations: [机器A, 机器B, 机器C]
}
3.3.2 转换操作创建新 RDD
JavaRDD<Integer> rdd2 = rdd1.filter(x -> x > 5);
rdd2 的内部结构:
RDD2 {
partitions: [
Partition0 -> 计算规则: 对RDD1的Partition0执行filter(x > 5)
Partition1 -> 计算规则: 对RDD1的Partition1执行filter(x > 5)
Partition2 -> 计算规则: 对RDD1的Partition2执行filter(x > 5)
]
compute: 对父RDD的每个分区执行filter操作
dependencies: [RDD1] (依赖RDD1)
preferredLocations: 继承自RDD1
}
注意:RDD2 此时还没有真正的数据,只是记录了"如何计算"
3.4 RDD 不存储数据,只存储计算逻辑
3.4.1 RDD 链条示例
JavaRDD<Integer> rdd1 = sc.parallelize(Arrays.asList(1,2,3,4,5,6,7,8,9,10), 3);
JavaRDD<Integer> rdd2 = rdd1.filter(x -> x > 5); // [6,7,8,9,10]
JavaRDD<Integer> rdd3 = rdd2.map(x -> x * 2); // [12,14,16,18,20]
JavaRDD<Integer> rdd4 = rdd3.filter(x -> x > 15); // [16,18,20]
内存中的实际情况:
RDD1: 记录"从数组[1,2,3,4,5,6,7,8,9,10]创建,分3个分区"
↓
RDD2: 记录"对RDD1执行filter(x > 5)"
↓
RDD3: 记录"对RDD2执行map(x -> x * 2)"
↓
RDD4: 记录"对RDD3执行filter(x > 15)"
实际数据:还没有计算!只是记录了计算步骤
3.4.2 只有 Action 操作才真正计算
// 这时才开始真正计算
List<Integer> result = rdd4.collect();
// Spark 的计算过程:
// 1. 从 RDD1 读取数据 [1,2,3,4,5,6,7,8,9,10]
// 2. 应用 filter(x > 5) -> [6,7,8,9,10]
// 3. 应用 map(x -> x * 2) -> [12,14,16,18,20]
// 4. 应用 filter(x > 15) -> [16,18,20]
// 5. 收集结果返回
3.5 RDD 分区的物理存储
3.5.1 分区在集群中的分布
// 假设有3台机器的集群
JavaRDD<String> textRDD = sc.textFile("hdfs://large_file.txt", 6);
物理分布:
机器A:
├── Partition0: 指向 HDFS Block0 的计算逻辑
├── Partition1: 指向 HDFS Block1 的计算逻辑
机器B:
├── Partition2: 指向 HDFS Block2 的计算逻辑
├── Partition3: 指向 HDFS Block3 的计算逻辑
机器C:
├── Partition4: 指向 HDFS Block4 的计算逻辑
├── Partition5: 指向 HDFS Block5 的计算逻辑
3.6 用代码查看 RDD 内部结构
public class RDDInternalStructure {
public static void main(String[] args) {
SparkConf conf = new SparkConf().setAppName("RDD内部结构").setMaster("local[4]");
JavaSparkContext sc = new JavaSparkContext(conf);
// 创建RDD
List<Integer> data = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
JavaRDD<Integer> rdd1 = sc.parallelize(data, 3);
// 查看RDD内部信息
System.out.println("=== RDD1 信息 ===");
System.out.println("分区数: " + rdd1.getNumPartitions());
System.out.println("RDD ID: " + rdd1.id());
System.out.println("血缘关系: " + rdd1.toDebugString());
// 创建转换RDD
JavaRDD<Integer> rdd2 = rdd1.filter(x -> x > 5);
JavaRDD<Integer> rdd3 = rdd2.map(x -> x * 2);
System.out.println("\n=== RDD3 信息 ===");
System.out.println("分区数: " + rdd3.getNumPartitions());
System.out.println("RDD ID: " + rdd3.id());
System.out.println("血缘关系: " + rdd3.toDebugString());
// 查看分区内容(这时才真正计算)
System.out.println("\n=== 分区实际数据 ===");
List<String> partitionContents = rdd3.mapPartitionsWithIndex((index, iterator) -> {
List<Integer> partitionData = new ArrayList<>();
while (iterator.hasNext()) {
partitionData.add(iterator.next());
}
return Arrays.asList("分区" + index + ": " + partitionData).iterator();
}).collect();
partitionContents.forEach(System.out::println);
sc.close();
}
}
输出示例:
=== RDD1 信息 ===
分区数: 3
RDD ID: 1
血缘关系: (3) ParallelCollectionRDD[1] at parallelize
=== RDD3 信息 ===
分区数: 3
RDD ID: 3
血缘关系: (3) MapPartitionsRDD[3] at map
| MapPartitionsRDD[2] at filter
| ParallelCollectionRDD[1] at parallelize
=== 分区实际数据 ===
分区0: [12, 14]
分区1: [16, 18]
分区2: [20]
3.7 RDD 的内存模型
// RDD 在内存中的表示
class RDDImpl {
int id; // RDD的唯一标识
Partition[] partitions; // 分区数组
Function compute; // 计算函数
RDD[] dependencies; // 依赖的父RDD
// 分区信息
class Partition {
int index; // 分区索引
String[] locations; // 首选计算位置
// 注意:这里不存储实际数据!
}
}
3.8 缓存时的数据存储
JavaRDD<Integer> cachedRDD = rdd3.cache();
cachedRDD.count(); // 触发计算并缓存
// 缓存后,RDD结构变化:
// RDD3 {
// partitions: [
// Partition0 -> 缓存在内存: [12, 14]
// Partition1 -> 缓存在内存: [16, 18]
// Partition2 -> 缓存在内存: [20]
// ]
//
// compute: 如果缓存存在则直接返回,否则重新计算
// isCache: true
// }
4. mapPartitions 详解
4.1 核心理解
mapPartitions = 分区内批量操作
4.2 map vs mapPartitions
4.2.1 map - 逐个处理
// 每个元素单独处理
rdd.map(x -> {
Connection conn = openDB(); // 每次都开连接
String result = process(x); // 处理一个
conn.close(); // 每次都关连接
return result;
});
// 1000个元素 = 1000次开关连接
4.2.2 mapPartitions - 批量处理
// 整个分区一起处理
rdd.mapPartitions(iterator -> {
Connection conn = openDB(); // 只开一次连接
List<String> results = new ArrayList<>();
while (iterator.hasNext()) {
results.add(process(iterator.next()));
}
conn.close(); // 只关一次连接
return results.iterator();
});
// 1000个元素分3个分区 = 只需3次开关连接
4.3 mapPartitions 优势
- 减少资源创建:每个分区只创建一次资源
- 批量处理:提高处理效率
- 更好的资源管理:分区级别管理资源生命周期
- 适合有状态操作:数据库连接、文件写入、索引创建
4.4 适用场景
- 数据库批量操作
- 文件批量处理
- 索引批量创建
- 需要初始化昂贵资源的操作
5. 实际应用示例
5.1 索引创建场景
// 索引创建的 mapPartitions 应用
JavaRDD<String> creatorRDD = shardPartitionRDD.mapPartitions(creator);
// 处理流程:
// 1. 每个分区创建一个索引写入器
// 2. 批量写入分区内所有文档
// 3. 完成索引创建,返回索引路径
// 4. 收集所有分区的索引路径用于后续合并
5.2 数据库批量操作
// 高效的数据库批量插入
rdd.mapPartitions(iterator -> {
Connection conn = getConnection();
PreparedStatement stmt = conn.prepareStatement("INSERT INTO table VALUES (?)");
// 批量添加
while (iterator.hasNext()) {
stmt.setString(1, iterator.next());
stmt.addBatch();
}
stmt.executeBatch(); // 一次性执行
conn.close();
return Arrays.asList("batch_inserted").iterator();
});
6. RDD 容错机制
6.1 血缘关系 (Lineage)
// Spark记录RDD的"家谱"
original_rdd → filter(x > 5) → map(x * 2) → result_rdd
6.2 故障恢复过程
正常情况:
机器A: [数据1] → 机器B: [数据2] → 机器C: [数据3]
机器B故障:
机器A: [数据1] → 机器B: ❌ → 机器C: [数据3]
自动恢复:
1. 检测到机器B故障
2. 根据血缘关系重新计算数据2
3. 在其他机器上恢复计算
4. 程序继续正常运行
6.3 优化策略
6.3.1 缓存 (Cache)
// 缓存经常使用的RDD
frequently_used_rdd.cache();
6.3.2 检查点 (Checkpoint)
// 对于复杂血缘关系设置检查点
sc.setCheckpointDir("hdfs://checkpoint");
complex_rdd.checkpoint();
7. 性能优化要点
7.1 分区策略
// 合理设置分区数
// 经验法则:分区数 = CPU核心数 * 2-4
int optimalPartitions = cpuCores * 3;
JavaRDD<String> rdd = sc.parallelize(data, optimalPartitions);
7.2 资源管理
- 使用
mapPartitions代替map进行批量操作 - 合理使用缓存避免重复计算
- 设置检查点缩短血缘关系链
7.3 数据本地性
- 数据和计算尽量在同一节点
- 减少网络传输开销
8. 关键概念总结
| 概念 | 定义 | 重要性 |
|---|---|---|
| 分区 | 数据的逻辑分割单元 | 并行计算的基础 |
| 血缘关系 | RDD之间的依赖关系 | 容错恢复的依据 |
| 懒惰计算 | 延迟到Action才执行 | 优化计算效率 |
| 批量处理 | 分区内数据一起处理 | 提高资源利用率 |
9. RDD 内部结构总结
RDD 的本质:
- 不是数据容器:RDD 不直接存储数据
- 是计算描述:记录如何从数据源或父RDD计算出当前数据
- 分布式抽象:将分布在多台机器上的数据统一抽象
- 懒惰计算:只有Action操作才触发真正的数据计算
- 血缘关系:记录RDD之间的依赖关系,用于容错恢复
形象比喻:
- RDD 像一张"菜谱",记录了如何做菜的步骤
- 分区像"食材分组",不同的食材放在不同的厨房
- 只有真正要吃饭时(Action),才按菜谱开始做菜(计算)
10. 学习要点
- 理解抽象:RDD是计算描述,不是数据容器
- 掌握分区:分区是并行计算的基本单位
- 善用批量:mapPartitions适合资源密集型操作
- 关注容错:血缘关系是Spark容错的核心
- 优化性能:合理分区、缓存、检查点