Spark 整体感知

为了能够深入使用 Spark ,那么必须对 Spark 有更为深入的理解。整体的结构的把握会显得非常重要。整体结构在脑海中成型后,别如同脑海中有了一个 Spark 的地图,后续深入了解的每个模块都能在整体上找到对应的位置,开发对应功能也会更加了然于胸。 网上的结构图非常丰富,但是我觉得都太过抽象,缺少一些细节。我这里尝试做的就是帮助 Spark 新手读者能构建一个 Spark 的直观感受,同时也是我自己知识的一个整理。

小Demo

下面是一段简单的 demo 程序。首先从数据源中取数,这里是从文本中读取数据。然后进行一系列的转换,最终得到自己想要的结果。

public static void main(String[] args) {
    SparkConf conf = new SparkConf().setAppName("Spark WordCount").setMaster("local");
    JavaSparkContext sc = new JavaSparkContext(conf);
    JavaRDD<String> lines = sc.textFile("your/file/path");
    lines.flatMap((String s) -> Arrays.asList(s.split(" ")).iterator())
            .mapToPair((String word) -> new Tuple2<>(word, 1))
            .reduceByKey((Integer v1, Integer v2) -> v1 + v2)
            .foreach((Tuple2<String, Integer> p) -> System.out.println(p._1 + " : " + p._2));
    sc.close();
}

这里代码内部有一个函数式计算 中非常重要的内容,流计算。曾经看过一个对流计算的形容,大意为流计算如同一个高高挂起的一桶水。每当增加一个计算,就如同在下方放置了个工具(比如滤网 filter),水桶依然高高挂起没有任何动作,直到触发一个按钮,那桶水倾倒处理,经过一个个计算节点,最后达到终点。 在下面的代码中,这里我们进入到最后的 foreache 方法,发现了触发倒水的按钮runJob。

 def foreach(f: T => Unit): Unit = withScope {
  val cleanF = sc.clean(f)
  sc.runJob(this, (iter: Iterator[T]) => iter.foreach(cleanF))
}

重要概念

  • RDD 全称是Resilient Distributed Datasets。RDD 中包含由 N 个 partition
  • partition是计算的最小单元。每个 partition 都是相互独立的。
  • 一个 RDD 和另外一个 RDD 的依赖关系分为宽依赖和窄依赖
  • 宽依赖。一个 RDD_a给一个计算得到 RDD_b。如果b 中的一块 partition 数据时计算a 中 N 块 partition 得出的。那么就是 RDD_b宽依赖 RDD_a
  • 窄依赖。如果b 中的一块 partition 数据时计算a 中 1 块 partition 得出的。那么就是 RDD_b窄依赖 RDD_a
  • 窄依赖可以在同一台机器上完成。
  • 宽依赖必须进行 shuffle,将计算需要的数据放到一起计算才行。

执行你的程序

首先,记住经过你的一系列计算后,你拿到的是包含了一系列的计算过程的RDD。当把这个RDD提交给 spark 后,spark 会做一下操作来得到最终结果。

  1. 解析出 RDD 中每一步的依赖关系。却别出子 RDD 对于父RDD 是,同一个partition 依赖(窄依赖)还是跨多个 partition(宽依赖)
  2. 按照宽依赖来切割 stage。需要进行shuffle 将不同机器不同partition的数据进行整理,以便同一key的数据发到同一台机器进行计算。
  3. 创建 Task。Task 只有两种,一种是 ShuffleMapTask,就是做 shuffle 的。另外一种是 ResultTask,就是计算最终结果的 Task。同样可以知道一个 Task 的数据来源也有两个,一个是文本,数据库之类的数据源。还有一个就是 上一个ShuffleMapTask 的结果。
  4. 以上操作都是在 Driver 端完成,下面Driver需要将安排好的 Task 提交到 Executor上执行。 当然,在提交之前首先要Master申请executor资源,获得资源后才能提交。
  5. Executor拿到 Task 后,便开始计算。这里就是上面水桶比喻中的倒水操作。executor 计算好之后,告知 Driver 计算结束。Driver 会将余下的 Task 依次提交,直到得出最终结果。至此你的计算就在 Spark 的帮助下完成了。
  6. 需要注意的是,Executor 返回个 Driver 的计算成功与否的结果,并非数据的计算结果。每一个 ShuffleMapTask的计算结果都是由 Spark 管理的,下一个 Task 需要上一个 Task 的结果时候,只需找 Spark 获取即可。

后面的话

从宏观上看大数据本质上还是非常朴素的,但是由于实现细节比较复杂,导致了很多门槛从而难以下手。当有了一个全局的感受后就会容易很多。

最近的文章

多版本并发控制MVCC

多版本控制MVCC调度冲突串行化在中 ACID 的 C ,指的是数据库中记录或映射的数据与真实世界中要一致,通俗一点说就是准确。事务的原子性,隔离性和持久性最终的目的都是为了数据的准确。串行化是保证数据库一致性的最高级别。但是为了获得更好的数据库性能,多个事务需要同时执行。这时数据库的调度显得尤为重要,也就是数据库的并发控制。串行化是指同一个事务的操作被同时调度,同一时间内只执行同一事务内的操作。 如果两个事务间存在对同一数据项的写操作(write),那么认为这俩事务是冲突的,那么不同的...…

继续阅读
更早的文章

Spark 窗口函数内存溢出

[流水]Spark 窗口函数内存溢出场景系统的 ETL 中有步操作,是给原表增加一列排序内容。利用spark rank 函数。实际计算过程发现,数据量大的情况,稳定会出现内存溢出。根据上面溢出位置可以发现一下代码其中Utils.copyStream(input, out) 的注释也明确说明这里有内存溢出的风险。下面我们分析一下这里为什么会内存溢出。问题分析这里不去深究窗口函数的算法细节(详见:窗口函数计算)。这里我们将重点放在对于内存溢出的内部原因和如何快速定位问题。分析溢出处逻辑从上图中...…

继续阅读