大数据组件学习笔记

49 阅读8分钟

大数据组件学习笔记

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懒惰执行,返回新RDDmap, 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 优势

  1. 减少资源创建:每个分区只创建一次资源
  2. 批量处理:提高处理效率
  3. 更好的资源管理:分区级别管理资源生命周期
  4. 适合有状态操作:数据库连接、文件写入、索引创建

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 的本质:

  1. 不是数据容器:RDD 不直接存储数据
  2. 是计算描述:记录如何从数据源或父RDD计算出当前数据
  3. 分布式抽象:将分布在多台机器上的数据统一抽象
  4. 懒惰计算:只有Action操作才触发真正的数据计算
  5. 血缘关系:记录RDD之间的依赖关系,用于容错恢复

形象比喻:

  • RDD 像一张"菜谱",记录了如何做菜的步骤
  • 分区像"食材分组",不同的食材放在不同的厨房
  • 只有真正要吃饭时(Action),才按菜谱开始做菜(计算)

10. 学习要点

  1. 理解抽象:RDD是计算描述,不是数据容器
  2. 掌握分区:分区是并行计算的基本单位
  3. 善用批量:mapPartitions适合资源密集型操作
  4. 关注容错:血缘关系是Spark容错的核心
  5. 优化性能:合理分区、缓存、检查点