Spark代码组织和最佳实践

时间:2015-09-25 07:30:18

标签: apache-spark functional-programming code-organization

因此,在面向对象的世界中花费了多年时间,并始终考虑到代码重用,设计模式和最佳实践,我发现自己在Spark世界中的代码组织和代码重用方面有些挣扎。

如果我尝试以可重用的方式编写代码,它几乎总是带来性能成本,我最终会将其重写为适合我的特定用例的最佳代码。这个常量“为这个特定用例编写最佳内容”也会影响代码组织,因为当“它们真的属于一个整体”时,将代码拆分成不同的对象或模块是困难的,因此我最终只得到很少的“上帝”对象包含长复杂变换链。事实上,我经常认为,如果我在面向对象世界工作时看到我现在正在写的大部分Spark代码,我会畏缩并将其视为“意大利面条代码”。

我上网试图寻找某种与面向对象世界的最佳实践相当的东西,但没有太多运气。我可以找到一些函数式编程的“最佳实践”,但Spark只增加了一个额外的层,因为性能是这里的一个主要因素。

所以我的问题是,你们中有没有Spark专家发现了一些你可以推荐的编写Spark代码的最佳实践?

修改

正如评论中所写,我实际上并没有希望有人发表关于如何解决这个问题的答案,而是我希望这个社区中的某个人遇到一些Martin Fowler类型谁曾在某个地方写过关于如何解决Spark世界中代码组织问题的文章或博客文章。

@DanielDarabos建议我举一个代码组织和性能相互矛盾的例子。虽然我发现我在日常工作中经常遇到这方面的问题,但我觉得把它归结为一个很好的最小例子有点困难;)但我会尝试。

在面向对象的世界中,我是单一责任原则的忠实粉丝,所以我要确保我的方法只对一件事负责。它使它们可重复使用并且易于测试。因此,如果我不得不计算列表中某些数字的总和(匹配某些标准)并且我必须计算相同数字的平均值,我肯定会创建两个方法 - 一个计算总和,一个计算平均值。像这样:

def main(implicit args: Array[String]): Unit = {
  val list = List(("DK", 1.2), ("DK", 1.4), ("SE", 1.5))

  println("Summed weights for DK = " + summedWeights(list, "DK")
  println("Averaged weights for DK = " + averagedWeights(list, "DK")
}

def summedWeights(list: List, country: String): Double = {
  list.filter(_._1 == country).map(_._2).sum
}

def averagedWeights(list: List, country: String): Double = {
  val filteredByCountry = list.filter(_._1 == country) 
  filteredByCountry.map(_._2).sum/ filteredByCountry.length
}

我当然可以继续尊重Spark中的SRP:

def main(implicit args: Array[String]): Unit = {
  val df = List(("DK", 1.2), ("DK", 1.4), ("SE", 1.5)).toDF("country", "weight")

  println("Summed weights for DK = " + summedWeights(df, "DK")
  println("Averaged weights for DK = " + averagedWeights(df, "DK")
}


def avgWeights(df: DataFrame, country: String, sqlContext: SQLContext): Double = {
  import org.apache.spark.sql.functions._
  import sqlContext.implicits._

  val countrySpecific = df.filter('country === country)
  val summedWeight = countrySpecific.agg(avg('weight))

  summedWeight.first().getDouble(0)
}

def summedWeights(df: DataFrame, country: String, sqlContext: SQLContext): Double = {
  import org.apache.spark.sql.functions._
  import sqlContext.implicits._

  val countrySpecific = df.filter('country === country)
  val summedWeight = countrySpecific.agg(sum('weight))

  summedWeight.first().getDouble(0)
}

但是因为我的df可能包含数十亿行,所以我宁愿不必执行filter两次。事实上,性能与EMR成本直接相关,所以我真的不希望这样。为了克服它,我因此决定违反SRP并简单地将这两个功能合二为一,并确保我在国家过滤的DataFrame上保持呼叫​​,如下所示:

def summedAndAveragedWeights(df: DataFrame, country: String, sqlContext: SQLContext): (Double, Double) = {
  import org.apache.spark.sql.functions._
  import sqlContext.implicits._

  val countrySpecific = df.filter('country === country).persist(StorageLevel.MEMORY_AND_DISK_SER)
  val summedWeights = countrySpecific.agg(sum('weight)).first().getDouble(0)
  val averagedWeights = summedWeights / countrySpecific.count()

  (summedWeights, averagedWeights)
}

现在,这个例子当然是对现实生活中遇到的事情进行了大量简化。在这里,我可以通过过滤和持久化df 来解决它,然后将交给sum和avg函数(也可能是更多的SRP),但在现实生活中可能会有一些正在进行的中间计算一次又一次地需要。换句话说,此处的filter函数仅仅是尝试创建一个简单示例,这些示例将从持久化中受益。事实上,我认为persist的来电是一个关键字。调用persist将大大加快我的工作,但代价是我必须将依赖于持久DataFrame的所有代码紧密耦合 - 即使它们在逻辑上是分开的。