Databricks Spark 知识库

1 最佳实践

1.1 避免使用 GroupByKey

  让我们看一下使用两种不同的方式去计算单词的个数,第一种方式使用 reduceByKey, 另外一种方式使用 groupByKey

  1. val words = Array("one", "two", "two", "three", "three", "three")
  2. val wordPairsRDD = sc.parallelize(words).map(word => (word, 1))
  3. //reduce
  4. val wordCountsWithReduce = wordPairsRDD
  5. .reduceByKey(_ + _)
  6. .collect()
  7. //group
  8. val wordCountsWithGroup = wordPairsRDD
  9. .groupByKey()
  10. .map(t => (t._1, t._2.sum))
  11. .collect()

  虽然两个函数都能得出正确的结果,reduceByKey 更适合使用在大数据集上。 这是因为 Spark 知道它可以在每个分区shuffle数据之前,聚合key值相同的数据。

  借助下图可以理解在 reduceByKey 里发生了什么。 注意在数据对被shuffle前同一机器上同样 key的数据是怎样被组合的(reduceByKey 中的 lamdba 函数)。然后 lamdba 函数在每个区上被再次调用来将所有值 reduce 成一个最终结果。

reduce_by

  但是,当调用 groupByKey 时,所有的键值对(key-value pair) 都会被shuffle。在网络上传输这些数据非常没有必要。

  为了确定将数据对shuffle到哪台主机,Spark 会对数据对的 key 调用一个分区函数。 当shuffle的数据量大于单台执行机器内存总量时,Spark 会把数据保存到磁盘上。 不过在保存时每次只会处理一个 key 的数据,所以当单个 key 的键值对超过内存容量会存在内存溢出的可能。
我们应避免将数据保存到磁盘上,这会严重影响性能。

group_by

  你可以想象一个非常大的数据集,在使用 reduceByKeygroupByKey 时他们的差别会被放大更多倍。

  以下函数应该优先于 groupByKey

  • combineByKey 组合数据,但是组合之后的数据类型与输入时值的类型不一样。

  • foldByKey 合并每一个 key 的所有值,在级联函数和“零值”中使用。

1.2 不要将大型 RDD 的所有元素拷贝到driver

  如果你的driver内存容量不能容纳一个大型 RDD 里面的所有数据,不要做以下操作:

  1. val values = myVeryLargeRDD.collect()

  Collect 操作会试图将 RDD 里面的每一条数据复制到driver上,这时候会发生内存溢出和崩溃。相反,你可以调用 take 或者 takeSample 来确保数据大小的上限。或者在你的 RDD 中使用过滤或抽样。
同样,要谨慎使用下面的操作,除非你能确保数据集小到足以存储在内存中:

  • countByKey

  • countByValue

  • collectAsMap

  如果你确实需要将 RDD 里面的大量数据保存在内存中,你可以将 RDD 写成一个文件或者把 RDD 导出到一个容量足够大的数据库中。

1.3 优雅地处理坏的输入数据

  当处理大量的数据的时候,一个常见的问题是有些数据格式不对或者内容有误。使用filter方法可以很容易丢弃坏的输入或者使用map方法可以修复可能修复的坏的数据。当你尝试着修复坏的数据,但是丢弃无法被修复的数据时,
flatMap函数是最好的选择。让我们考虑下面的输入json串。

  1. input_rdd = sc.parallelize(["{\"value\": 1}", # Good
  2. "bad_json", # Bad
  3. "{\"value\": 2}", # Good
  4. "{\"value\": 3" # Missing an ending brace.
  5. ])

  当我们尝试着在SqlContext中使用这个输入串时,很明显它会因为格式不对而报错。

  1. sqlContext.jsonRDD(input_rdd).registerTempTable("valueTable")
  2. # The above command will throw an error.

  让我妈用下面的python代码修复输入数据。

  1. def try_correct_json(json_string):
  2. try:
  3. # First check if the json is okay.
  4. json.loads(json_string)
  5. return [json_string]
  6. except ValueError:
  7. try:
  8. # If not, try correcting it by adding a ending brace.
  9. try_to_correct_json = json_string + "}"
  10. json.loads(try_to_correct_json)
  11. return [try_to_correct_json]
  12. except ValueError:
  13. # The malformed json input can't be recovered, drop this input.
  14. return []

  经过上面函数的处理之后,我们就可以使用这些数据了。

  1. corrected_input_rdd = input_rdd.flatMap(try_correct_json)
  2. sqlContext.jsonRDD(corrected_input_rdd).registerTempTable("valueTable")
  3. sqlContext.sql("select * from valueTable").collect()
  4. # Returns [Row(value=1), Row(value=2), Row(value=3)]

2 常规故障处理

2.1 Job aborted due to stage failure: Task not serializable

  如果你看到以下错误:

  1. org.apache.spark.SparkException: Job aborted due to stage failure:
  2. Task not serializable: java.io.NotSerializableException: ...

  上述的错误在这种情况下会发生:当你在 master 上初始化一个变量,但是试图在 worker 上使用。
在这个示例中, Spark Streaming 试图将对象序列化之后发送到 worker 上,如果这个对象不能被序列化就会失败。思考下面的代码片段:

  1. NotSerializable notSerializable = new NotSerializable();
  2. JavaRDD<String> rdd = sc.textFile("/tmp/myfile");
  3. rdd.map(s -> notSerializable.doSomething(s)).collect();

  这段代码会触发上面的错误。这里有一些建议修复这个错误:

  • class 实现序列化
  • 在作为参数传递给 map 方法的 lambda 表达式内部声明实例
  • 在每一台机器上创建一个 NotSerializable 的静态实例
  • 调用 rdd.forEachPartition 并且像下面这样创建 NotSerializable 对象:
  1. rdd.forEachPartition(iter -> {
  2. NotSerializable notSerializable = new NotSerializable();
  3. // ...Now process iter
  4. });

2.2 缺失依赖

  在默认状态下,Mavenbuild 的时候不会包含所依赖的 jar 包。当运行一个 Spark 任务时,如果 Spark worker 机器上没有包含所依赖的 jar 包会发生类无法找到的错误(ClassNotFoundException)。

  有一个简单的方式,在 Maven 打包的时候创建 shadeduber 任务可以让那些依赖的 jar 包很好地打包进去。

  使用 <scope>provided</scope> 可以排除那些没有必要打包进去的依赖,对 Spark 的依赖必须使用 provided 标记,因为这些依赖已经包含在 Spark cluster中。在你的 worker 机器上已经安装的 jar 包你同样需要排除掉它们。

  下面是一个 Maven pom.xml 的例子,工程了包含了一些需要的依赖,但是 Sparklibraries 不会被打包进去,因为它使用了 provided

  1. <project>
  2. <groupId>com.databricks.apps.logs</groupId>
  3. <artifactId>log-analyzer</artifactId>
  4. <modelVersion>4.0.0</modelVersion>
  5. <name>Databricks Spark Logs Analyzer</name>
  6. <packaging>jar</packaging>
  7. <version>1.0</version>
  8. <repositories>
  9. <repository>
  10. <id>Akka repository</id>
  11. <url>http://repo.akka.io/releases</url>
  12. </repository>
  13. </repositories>
  14. <dependencies>
  15. <dependency> <!-- Spark -->
  16. <groupId>org.apache.spark</groupId>
  17. <artifactId>spark-core_2.10</artifactId>
  18. <version>1.1.0</version>
  19. <scope>provided</scope>
  20. </dependency>
  21. <dependency> <!-- Spark SQL -->
  22. <groupId>org.apache.spark</groupId>
  23. <artifactId>spark-sql_2.10</artifactId>
  24. <version>1.1.0</version>
  25. <scope>provided</scope>
  26. </dependency>
  27. <dependency> <!-- Spark Streaming -->
  28. <groupId>org.apache.spark</groupId>
  29. <artifactId>spark-streaming_2.10</artifactId>
  30. <version>1.1.0</version>
  31. <scope>provided</scope>
  32. </dependency>
  33. <dependency> <!-- Command Line Parsing -->
  34. <groupId>commons-cli</groupId>
  35. <artifactId>commons-cli</artifactId>
  36. <version>1.2</version>
  37. </dependency>
  38. </dependencies>
  39. <build>
  40. <plugins>
  41. <plugin>
  42. <groupId>org.apache.maven.plugins</groupId>
  43. <artifactId>maven-compiler-plugin</artifactId>
  44. <version>2.3.2</version>
  45. <configuration>
  46. <source>1.8</source>
  47. <target>1.8</target>
  48. </configuration>
  49. </plugin>
  50. <plugin>
  51. <groupId>org.apache.maven.plugins</groupId>
  52. <artifactId>maven-shade-plugin</artifactId>
  53. <version>2.3</version>
  54. <executions>
  55. <execution>
  56. <phase>package</phase>
  57. <goals>
  58. <goal>shade</goal>
  59. </goals>
  60. </execution>
  61. </executions>
  62. <configuration>
  63. <filters>
  64. <filter>
  65. <artifact>*:*</artifact>
  66. <excludes>
  67. <exclude>META-INF/*.SF</exclude>
  68. <exclude>META-INF/*.DSA</exclude>
  69. <exclude>META-INF/*.RSA</exclude>
  70. </excludes>
  71. </filter>
  72. </filters>
  73. <finalName>uber-${project.artifactId}-${project.version}</finalName>
  74. </configuration>
  75. </plugin>
  76. </plugins>
  77. </build>
  78. </project>

2.3 执行 start-all.sh 错误: Connection refused

  如果是使用 Mac 操作系统运行 start-all.sh 发生下面错误时:

  1. % sh start-all.sh
  2. starting org.apache.spark.deploy.master.Master, logging to ...
  3. localhost: ssh: connect to host localhost port 22: Connection refused

  你需要在你的电脑上打开 “远程登录” 功能。进入 系统偏好设置 ---> 共享 勾选打开 远程登录

2.4 Spark 组件之间的网络连接问题

  Spark 组件之间的网络连接问题会导致各式各样的警告或错误:

  • SparkContext <-> Spark Standalone Master

  如果 SparkContext 不能连接到 Spark standalone master,会显示下面的错误:

  1. ERROR AppClient$ClientActor: All masters are unresponsive! Giving up.
  2. ERROR SparkDeploySchedulerBackend: Spark cluster looks dead, giving up.
  3. ERROR TaskSchedulerImpl: Exiting due to error from cluster scheduler:
  4. Spark cluster looks down

  如果 driver 能够连接到 master 但是 master 不能回连到 driver,这时 Master 的日志会记录多次尝试连接 driver 失败并且会报告不能连接:

  1. INFO Master: Registering app SparkPi
  2. INFO Master: Registered app SparkPi with ID app-XXX-0000
  3. INFO: Master: Removing app app-app-XXX-0000
  4. [...]
  5. INFO Master: Registering app SparkPi
  6. INFO Master: Registered app SparkPi with ID app-YYY-0000
  7. INFO: Master: Removing app app-YYY-0000
  8. [...]

  在这样的情况下,master 报告应用已经被成功地注册了。但是注册成功的通知 driver 接收失败了, 这时 driver 会自动尝试几次重新连接直到失败的次数太多而放弃重试。
其结果是 Master web UI 会报告多个失败的应用,即使只有一个 SparkContext 被创建。

  如果你遇到上述的错误,有两条可以遵循的建议:

  • 检查 workersdrivers 配置的 Spark master 的地址
  • 设置 driver,master,worker 的 SPARK_LOCAL_IP 为集群的可寻地址主机名。

配置 hostname/port

  这节将描述我们如何绑定 Spark 组件的网络接口和端口。在每节里,配置会按照优先级降序的方式排列。如果前面所有配置没有提供则使用最后一条作为默认配置。

SparkContext actor system:

Hostname:

  • spark.driver.host 属性
  • 如果 SPARK_LOCAL_IP 环境变量的设置是主机名(hostname),就会使用设置时的主机名。如果 SPARK_LOCAL_IP 设置的是一个 IP 地址,这个 IP 地址会被解析为主机名。
  • 使用默认的 IP 地址,这个 IP 地址是Java 接口 InetAddress.getLocalHost 方法的返回值。

Port:

  • spark.driver.port 属性。
  • 从操作系统(OS)选择一个临时端口。
Spark Standalone Master / Worker actor systems:

Hostname:

  • 当 Master 或 Worker 进程启动时使用 —host 或 -h 选项(或是过期的选项 —ip 或 -i)。
  • SPARK_MASTER_HOST 环境变量(仅应用在 Master 上)。
  • 如果 SPARK_LOCAL_IP 环境变量的设置是主机名(hostname),就会使用设置时的主机名。如果 SPARK_LOCAL_IP 设置的是一个 IP 地址,这个 IP 地址会被解析为主机名。
  • 使用默认的 IP 地址,这个 IP 地址是Java 接口 InetAddress.getLocalHost 方法的返回值.

Port:

  • 当 Master 或 Worker 进程启动时使用 —port 或 -p 选项。
  • SPARK_MASTER_PORT 或 SPARK_WORKER_PORT 环境变量(分别应用到 Master 和 Worker 上)。
  • 从操作系统(OS)选择一个临时端口。

3 性能和优化

3.1 一个 RDD 有多少分区

  在调试和故障处理的时候,我们通常有必要知道 RDD 有多少个分区。这里有几个方法可以找到这些信息:

使用 UI 查看在分区上执行的任务数

  当 stage 执行的时候,你可以在 Spark UI 上看到这个 stage 上的分区数。 下面的例子中的简单任务在 4 个分区上创建了共 100 个元素的 RDD ,然后在这些元素被收集到 driver 之前分发一个 map 任务:

  1. scala> val someRDD = sc.parallelize(1 to 100, 4)
  2. someRDD: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at <console>:12
  3. scala> someRDD.map(x => x).collect
  4. res1: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
  5. 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
  6. 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100)

  在 Spark的应用 UI 里,从下面截图上看到的 “Total Tasks” 代表了分区数。

partitions-as-tasks

使用 UI 查看分区缓存

  持久化RDD 时通常需要知道有多少个分区被存储。下面的这个例子和之前的一样,除了现在我们要对 RDD 做缓存处理。操作完成之后,我们可以在 UI 上看到这个操作导致什么被我们存储了。

  1. scala> someRDD.setName("toy").cache
  2. res2: someRDD.type = toy ParallelCollectionRDD[0] at parallelize at <console>:12
  3. scala> someRDD.map(x => x).collect
  4. res3: Array[Int] = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37,
  5. 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
  6. 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100)

  注意:下面的截图有 4 个分区被缓存。

cached-partitions

编程查看 RDD 分区

  在 Scala API 里,RDD 持有一个分区数组的引用, 你可以使用它找到有多少个分区:

  1. scala> val someRDD = sc.parallelize(1 to 100, 30)
  2. someRDD: org.apache.spark.rdd.RDD[Int] = ParallelCollectionRDD[0] at parallelize at <console>:12
  3. scala> someRDD.partitions.size
  4. res0: Int = 30

  在 Python API 里, 有一个方法可以明确地列出有多少个分区:

  1. In [1]: someRDD = sc.parallelize(range(101),30)
  2. In [2]: someRDD.getNumPartitions()
  3. Out[2]: 30

3.2 数据本地性

  Spark 是一个并行数据处理框架,这意味着任务应该在离数据尽可能近的地方执行(即最少的数据传输)。

检查本地性

  检查任务是否在本地运行的最好方式是在 Spark UI 上查看 stage 信息,注意下面截图中的 Locality Level 列显示任务运行在哪个地方。

locality

调整本地性配置

  你可以调整 Spark 在每个数据本地性level(data local --> process local --> node local --> rack local --> Any)上等待的时长。更多详细的参数信息请查看程序配置文档的 Scheduling 章节里类似于 spark.locality.* 的配置。

4 Spark Streaming

ERROR OneForOneStrategy

  如果你在 Spark Streaming 里启用 checkpointingforEachRDD 函数使用的对象都应该可以被序列化(Serializable)。否则会出现这样的异常 “ERROR OneForOneStrategy: … java.io.NotSerializableException:”

  1. JavaStreamingContext jssc = new JavaStreamingContext(sc, INTERVAL);
  2. // This enables checkpointing.
  3. jssc.checkpoint("/tmp/checkpoint_test");
  4. JavaDStream<String> dStream = jssc.socketTextStream("localhost", 9999);
  5. NotSerializable notSerializable = new NotSerializable();
  6. dStream.foreachRDD(rdd -> {
  7. if (rdd.count() == 0) {
  8. return null;
  9. }
  10. String first = rdd.first();
  11. notSerializable.doSomething(first);
  12. return null;
  13. }
  14. );
  15. // This does not work!!!!

  按照下面的方式之一进行修改,上面的代码才能正常运行:

  • 在配置文件里面删除 jssc.checkpoint 这一行关闭 checkpointing。
  • 让对象能被序列化。
  • 在 forEachRDD 函数里面声明 NotSerializable,下面的示例代码是可以正常运行的:
  1. JavaStreamingContext jssc = new JavaStreamingContext(sc, INTERVAL);
  2. jssc.checkpoint("/tmp/checkpoint_test");
  3. JavaDStream<String> dStream = jssc.socketTextStream("localhost", 9999);
  4. dStream.foreachRDD(rdd -> {
  5. if (rdd.count() == 0) {
  6. return null;
  7. }
  8. String first = rdd.first();
  9. NotSerializable notSerializable = new NotSerializable();
  10. notSerializable.doSomething(first);
  11. return null;
  12. }
  13. );
  14. // This code snippet is fine since the NotSerializable object
  15. // is declared and only used within the forEachRDD function.