跳转至

课程十:分布式计算

课程十:分布式计算框架架构演进案例

目标:大数据的处理离不开强大的分布式计算框架。本课程将追随计算框架的演进脉络,从经典的 MapReduce 到流批一体的 Flink,带你深入理解批处理、流计算、内存计算优化、资源调度、以及编程模型的统一等核心技术变革,掌握不同计算范式的适用场景与设计哲学。


阶段 0:单机时代(脚本小作坊)

系统描述

  • 场景:最初的数据分析需求,处理一些不太大的日志文件或数据集。
  • 功能:例如,使用 Python 脚本结合 Pandas 库分析单机上的 Nginx 访问日志,统计每日的 PV/UV,并将结果输出到 CSV 文件。
  • 技术栈:
  • 语言/库:Python + Pandas / Shell (AWK, grep)
  • 调度:Cron 定时任务

当前架构图

graph LR
    LogFile("本地日志文件 Log File") -- 读取 --> Script("Python/Shell 脚本");
    Script -- 输出 --> ResultCSV("结果 CSV 文件");

此刻的痛点: - 处理能力瓶颈:数据量一旦超过单机内存大小,脚本直接崩溃。 - 处理速度瓶颈:单机单线程处理,速度极其缓慢,无法应对 TB/PB 级数据。 - 扩展性为零:无法通过增加机器来提升处理能力。


阶段 1:拥抱分布式 → MapReduce 范式与 Hadoop 生态

挑战浮现

  • 日志数据量增长到 TB 甚至 PB 级别,单机处理彻底不可行。
  • 需要一种能够横向扩展的、容错的分布式计算模型来处理海量数据。

❓ 架构师的思考时刻:

如何让成百上千台机器协同处理一个大任务?如何处理节点故障?(分而治之是核心思想。怎么分?怎么合?机器坏了怎么办?Google 的三驾马车提供了什么思路?)

✅ 演进方向:引入 MapReduce 计算模型 + HDFS 分布式存储

  1. 分布式存储基础 (HDFS):
  2. 首先需要一个能存储海量数据、并提供高吞吐访问的分布式文件系统。HDFS (Hadoop Distributed File System) 应运而生。
  3. 它将大文件切分成固定大小的数据块 (Block),分散存储在多个 DataNode 上,并通过 NameNode 管理元数据。

  4. MapReduce 编程模型:

  5. Google 提出的 MapReduce 模型将分布式计算抽象为两个核心阶段:

    • Map 阶段:将原始输入数据切分成多个分片 (Split),每个 Map Task 处理一个分片,进行映射转换,输出 <Key, Value> 键值对。
    • Reduce 阶段:将 Map 输出的具有相同 Key 的 <Key, Value> 聚合到一起,交给一个 Reduce Task 进行处理,输出最终结果。
    • 框架负责处理数据分发、任务调度、节点通信 (Shuffle)、容错等复杂细节。
  6. 容错机制:

  7. MapReduce 具有良好的容错性。如果某个 Task (Mapper 或 Reducer) 运行失败,框架会自动在其他节点上重新调度执行该 Task,数据会从 HDFS 重新读取。

  8. Hadoop 生态:

  9. 围绕 HDFS 和 MapReduce,发展出了庞大的 Hadoop 生态系统,包括 Hive (SQL on Hadoop), Pig (Data Flow Language), HBase (NoSQL Database) 等。

架构调整(引入 Hadoop MapReduce):

graph TD
    subgraph "数据存储"
        Logs --> HDFS("HDFS 分布式存储");
    end
    subgraph "计算流程 (MapReduce Job)"
        HDFS -- 输入分片 --> MapTasks("Map Tasks 并行执行");
        MapTasks -- "K/V 对" --> Shuffle("Shuffle & Sort - 框架处理");
        Shuffle -- 分组数据 --> ReduceTasks("Reduce Tasks 并行执行");
        ReduceTasks -- 输出结果 --> HDFS;
    end
    subgraph "资源调度"
        JobTracker("JobTracker/ResourceManager - Master") -- 调度 --> TaskTrackers("TaskTracker/NodeManager - Worker");
    end

实现了海量数据的分布式批处理,但存在明显性能瓶颈:MapReduce 是基于磁盘的计算模型,每个阶段的中间结果都需要写入 HDFS,磁盘 I/O 开销巨大,对于需要多轮迭代计算的算法(如机器学习、图计算)性能很差。


阶段 2:追求极致性能 → Spark 内存计算

挑战再升级:磁盘 I/O 成为瓶颈,迭代计算效率低下

  • MapReduce 的磁盘 I/O 开销限制了其处理速度,尤其是在需要多次扫描数据的场景(如机器学习算法训练)。
  • 用户需要更快的交互式查询和更高效的迭代计算能力。

❓ 架构师的思考时刻:

如何摆脱频繁的磁盘读写?能否让中间结果常驻内存?(内存比磁盘快几个数量级。能不能把计算过程中的数据尽可能放到内存里?如何优化计算流程,减少不必要的步骤?)

✅ 演进方向:引入 Spark 及其核心 RDD/DAG 模型

  1. 核心抽象:RDD (弹性分布式数据集):
  2. Spark 提出了 RDD (Resilient Distributed Dataset) 的核心抽象。
  3. RDD 是一个不可变的、可分区的、可并行操作的分布式数据集。
  4. RDD 支持丰富的转换 (Transformation) 操作(如 map, filter, groupByKey)和行动 (Action) 操作(如 count, collect, save)。Transformation 是惰性求值的。
  5. 关键在于 RDD 可以被缓存到内存中 (RDD.persist() 或 RDD.cache()),后续的计算可以直接从内存读取,极大减少了磁盘 I/O。

  6. 基于 DAG (有向无环图) 的执行引擎:

  7. Spark 会将一系列 RDD 的转换操作构建成一个 DAG (Directed Acyclic Graph)。
  8. 执行引擎根据 DAG 将计算划分成多个阶段 (Stage),尽可能地将可以流水线化 (Pipeline) 的操作放在同一个 Stage 中执行,减少 Shuffle 操作(即数据需要在节点间传输和落盘的操作)。

  9. 区分宽窄依赖优化 Shuffle:

  10. 窄依赖 (Narrow Dependency):父 RDD 的每个分区最多只被子 RDD 的一个分区依赖(如 map, filter)。窄依赖可以在同一个节点内流水线执行。
  11. 宽依赖 (Wide Dependency / Shuffle Dependency):父 RDD 的每个分区可能被子 RDD 的多个分区依赖(如 groupByKey, reduceByKey, join)。宽依赖通常需要进行 Shuffle 操作,是性能优化的关键点。

架构调整(引入 Spark):

graph TD
    DataSource("数据源 HDFS/Kafka/...") --> SparkDriver("Spark Driver 程序");
    SparkDriver -- 构建 DAG & 调度 Tasks --> ClusterManager("集群管理器 YARN/Mesos/Standalone");
    ClusterManager -- 分配资源 --> SparkExecutors("Spark Executor 进程");
    subgraph "Spark Executor 内部"
        ExecutorCore("执行 Task");
        CacheMemory("内存缓存 Block Manager");
        DiskStorage("磁盘存储 可选");
        ExecutorCore -- "读写 RDD 分区" --> CacheMemory;
        CacheMemory -- "内存不足时溢写" --> DiskStorage;
        ExecutorCore -- "Shuffle 数据交换" --> OtherExecutors("其他 Executor");
    end
    SparkExecutors -- 执行结果 --> SparkDriver;
    SparkDriver -- 输出 --> ResultDestination("结果目的地");

大幅提升了批处理和迭代计算的性能,但对于需要毫秒/秒级响应的实时计算场景仍显不足。


新挑战:低延迟的实时数据处理需求

  • 业务需要对实时产生的数据流进行低延迟处理和分析,例如:实时监控网站异常流量、实时计算用户当前的地理位置、实时更新在线广告的点击率等。
  • Spark Streaming 虽然提供了微批处理 (Micro-batching) 的方式模拟流处理,但其本质仍然是批处理,延迟通常在秒级,无法满足真正的低延迟需求。

❓ 架构师的思考时刻:

如何实现真正的逐条事件处理,达到毫秒级延迟?如何处理乱序事件和保证状态一致性?(批处理的思路不适用了。需要一个原生的流计算引擎。时间概念很重要,用事件发生时间还是处理时间?计算过程中的状态怎么保存和恢复?)

Flink 是为流处理而设计的,但也具备优秀的批处理能力,被称为流批一体的计算引擎: 1. 真正的流式处理模型 (Native Streaming): - Flink 将数据视为无界的数据流 (Unbounded Stream),逐条处理到来的事件,可以实现毫秒级的处理延迟。 - 批处理被视为流处理的一个特例(有界数据流)。

  1. 强大的时间语义支持:
  2. 事件时间 (Event Time):按事件实际发生的时间进行处理,可以正确处理乱序事件,是保证结果准确性的关键。
  3. 处理时间 (Processing Time):按事件到达处理节点的时间进行处理,简单,但结果可能不准确。
  4. 摄入时间 (Ingestion Time):按事件进入 Flink 系统的时间进行处理。
  5. Watermark 机制:用于处理事件时间下的乱序和延迟问题,告诉系统事件时间的进展。

  6. 有状态计算与 Checkpoint 机制:

  7. 流计算通常需要维护状态(如窗口内的聚合值)。Flink 提供了强大的有状态计算能力。
  8. 通过分布式快照 (Checkpoint) 机制,定期将算子 (Operator) 的状态持久化到外部存储(如 HDFS, S3)。当发生故障时,可以从最近的 Checkpoint 恢复状态,保证精确一次 (Exactly-once) 的处理语义(需要 Source 和 Sink 支持)。

  9. 灵活的窗口操作 (Windowing):

  10. 提供了丰富的窗口类型来处理无界数据流,如滚动窗口 (Tumbling Window)、滑动窗口 (Sliding Window)、会话窗口 (Session Window) 等。
graph TD
    subgraph "数据源"
        KafkaStream("Kafka 实时数据流");
        HDFSBatch("HDFS 批处理数据");
    end
    subgraph "Flink 集群"
        JobManager("JobManager - Master") -- 调度 --> TaskManagers("TaskManager - Worker");
        KafkaStream -- 输入 --> TaskManagers;
        HDFSBatch -- 输入 --> TaskManagers;
        TaskManagers -- "执行 Operators (map, filter, window, aggregate)" --> TaskManagers;
    end

提供了低延迟的实时流处理能力,并具备流批一体的潜力,但资源管理和调度仍是挑战。


阶段 4:资源调度与弹性 → 拥抱 Kubernetes

挑战再临:资源利用率与运维效率

  • 传统的计算集群(如 YARN)资源分配相对静态,难以根据负载变化实现快速的弹性伸缩,导致资源利用率不高。
  • 同时维护 Hadoop/YARN、Spark、Flink 等多个集群,运维复杂度和成本很高。
  • 需要更统一、更云原生的资源管理和调度平台。

❓ 架构师的思考时刻:

如何让计算任务像微服务一样具备弹性?如何统一资源管理?(能不能把 Spark/Flink 任务跑在 K8s 上?容器化是趋势。如何实现按需分配和自动扩缩容?)

✅ 演进方向:将计算框架容器化并运行在 Kubernetes 之上

  1. 容器化计算任务:
  2. 将 Spark、Flink 的 Driver/Master 和 Executor/TaskManager 程序打包成 Docker 镜像。

  3. Kubernetes 作为统一资源调度器:

  4. 使用 Kubernetes (K8s) 作为底层的资源管理和调度平台,替代 YARN/Mesos。
  5. Spark 和 Flink 都提供了原生的 Kubernetes 支持 (Native Kubernetes Integration) 或通过 Operator 模式运行在 K8s 上。

  6. 弹性伸缩:

  7. 利用 K8s 的 HPA (Horizontal Pod Autoscaler) 或 Flink/Spark on K8s 的自动伸缩能力,根据 CPU、内存、队列积压等指标自动调整 Executor/TaskManager Pod 的数量,实现资源的弹性伸缩。

  8. 简化运维:

  9. 统一使用 K8s 管理所有类型的计算任务(批处理、流处理、甚至机器学习),降低运维复杂度。
  10. 可以利用 K8s 的生态(如 Helm 进行部署,Prometheus 进行监控)。

  11. (可选) 利用云厂商 Spot 实例降低成本:

  12. 对于容错性较好的批处理任务,可以调度到云厂商的抢占式实例 (Spot Instance) 上运行,大幅降低计算成本。

架构调整(计算层运行在 K8s):

graph TD
    subgraph "提交与调度"
        SparkSubmit("Spark Submit / Flink CLI") -- 提交 Job --> K8sAPIServer("Kubernetes API Server");
        K8sAPIServer -- 创建/管理 Pods --> K8sCluster("Kubernetes Cluster");
    end
    subgraph "K8s 集群内部署"
        DriverPod("Driver/JobManager Pod");
        ExecutorPods("Executor/TaskManager Pods");
        Operator("Spark/Flink Operator 可选");
        HPA("Horizontal Pod Autoscaler 可选");
        K8sCluster -- 运行 --> DriverPod;
        K8sCluster -- 运行 --> ExecutorPods;
        Operator -- "管理" --> DriverPod & ExecutorPods;
        HPA -- "监控指标 & 扩缩容" --> ExecutorPods;
    end
    DriverPod -- 协调 --> ExecutorPods;
    ExecutorPods -- 读写 --> Storage("HDFS/S3/Kafka...");

实现了资源的弹性管理和统一运维,但流处理和批处理的代码逻辑仍然是分开的。


最后的挑战:开发效率与逻辑复用

  • 许多业务场景需要同时处理历史数据(批处理) 和 实时数据(流处理),例如,基于全量用户行为计算用户画像(批),并实时更新用户当前状态(流)。
  • 使用两套独立的代码逻辑(一套 Spark/MapReduce for Batch, 一套 Flink for Streaming)进行开发和维护,效率低下,逻辑一致性难以保证。
  • 能否使用一套 API 或语言来同时描述批处理和流处理逻辑?

❓ 架构师的思考时刻:

如何用一套代码统一处理流和批?(需要一种更高级的抽象。SQL 是不是个好选择?流和表怎么转换?)

Flink 社区致力于通过 Flink SQL 提供统一的流批处理编程体验: 1. 统一的 SQL API: - 用户可以使用标准的 SQL 语法来编写数据处理逻辑,无论是处理有界的批处理数据还是无界的流处理数据。

  1. 流与表的二元性 (Duality):
  2. Flink SQL 将流 (Stream) 和 表 (Table) 视为同一事物的两种不同表现形式。
  3. 流可以看作是不断追加更新的动态表 (Dynamic Table)。
  4. 表也可以看作是某个时间点流的快照。

  5. 丰富的 Connectors:

  6. Flink SQL 提供了大量的 Connector,可以方便地连接各种外部系统作为 Source (数据源) 或 Sink (结果目标),如 Kafka, HDFS, MySQL, Elasticsearch, Redis 等。

  7. 统一的执行引擎:

  8. 底层的 Flink Runtime 可以根据输入数据源的类型(有界或无界)自动选择合适的执行模式(批处理模式或流处理模式)来执行 SQL 作业。
graph TD
    subgraph "数据源"
        Kafka("Kafka - Streaming Source");
        HDFS("HDFS/S3 - Batch Source");
        MySQL("MySQL - Lookup/Sink");
        ES("Elasticsearch - Sink");
    end
    subgraph "Flink SQL 应用"
        FlinkSQLApp("Flink SQL Job") -- 定义 Source/Sink/Transform --> FlinkRuntime("Flink Runtime");
        FlinkRuntime -- 读写 --> Kafka & HDFS & MySQL & ES;
    end

实现了流处理和批处理逻辑的统一,提高了开发效率和代码复用性。


总结:分布式计算框架的演进之路

阶段 核心挑战 关键解决方案 代表技术/模式
0. 单机 处理能力/速度/扩展性 (无法解决) Python/Shell Script
1. 批处理 海量数据/容错 MapReduce + HDFS Hadoop MapReduce, HDFS
2. 性能优化 磁盘 I/O 瓶颈/迭代慢 内存计算 (RDD/DataFrame) + DAG Spark (RDD/Dataset API)
3. 实时计算 低延迟需求/乱序/状态 原生流处理 + 时间语义/状态/窗口 Flink (DataStream API), Event Time, Checkpoint
4. 弹性资源 资源利用率/运维效率 容器化 + Kubernetes 调度 Docker, Kubernetes (Native/Operator), HPA
5. 流批一体 开发效率/逻辑复用 统一 SQL API + 动态表 Flink SQL, Table API

课程设计亮点与思考

  1. 范式演进清晰:课程清晰地展示了分布式计算从批处理到流处理,再到流批一体的范式演进过程。
  2. 核心技术突出:重点讲解了 MapReduce、Spark RDD/DAG、Flink Checkpoint/Watermark、Kubernetes 调度等各阶段的核心技术和设计思想。
  3. 问题驱动创新:每个阶段的技术变革都是为了解决前一阶段的核心痛点,体现了技术发展的内在逻辑。
  4. 理论与实践结合:既有核心原理的阐述(如 MapReduce 模型、RDD 概念),也有具体技术框架的介绍(Hadoop, Spark, Flink, K8s)。
  5. 面向未来趋势:流批一体是当前大数据领域的重要发展方向,课程对此进行了重点介绍。

掌握分布式计算框架的演进历程,有助于理解不同计算模式的优劣和适用场景,为在实际工作中选择合适的计算引擎和设计高效的数据处理方案打下坚实的基础。