web-dev-qa-db-ja.com

Spark=コード編成とベストプラクティス

そのため、コードの再利用、デザインパターン、ベストプラクティスを常に考慮に入れたオブジェクト指向の世界で長年を過ごしてきたため、Sparkの世界ではコードの整理とコードの再利用に多少苦労しています。

再利用可能な方法でコードを記述しようとすると、ほとんどの場合パフォーマンスコストが発生し、特定のユースケースに最適なコードに書き換えることになります。この定数「この特定のユースケースに最適なものを書き込む」は、コード編成にも影響します。「すべてが本当に一緒に属する」場合、コードを異なるオブジェクトまたはモジュールに分割するのが難しく、そのため、長い複雑な変換のチェーン。実際、私が頻繁に、オブジェクト指向の世界で働いていたときに書いているSpark=コードの大部分を見ていたなら、私はひっくり返して却下したと思います「スパゲッティコード」として。

オブジェクト指向の世界のベストプラクティスに相当するものを見つけようとしてインターネットをサーフィンしましたが、運はあまりありませんでした。関数型プログラミングのいくつかの「ベストプラクティス」を見つけることができますが、ここではパフォーマンスが大きな要因であるため、Sparkは追加のレイヤーを追加するだけです。

あなたへの私の質問は、あなたがSpark=の達人はあなたがお勧めできるコードを書くためのいくつかのベストプラクティスを見つけましたかSpark?

[〜#〜] edit [〜#〜]

コメントに書かれているように、私は実際に誰かがこの問題をsolve解決する方法についての答えを投稿することを期待していませんでしたが、むしろこの中の誰かがコミュニティは、Sparkの世界におけるコード編成の問題に対処する方法について、Somの記事やブログ投稿をどこかに書いていたMartin Fowlerタイプに出会いました。

@DanielDarabosは、コードの編成とパフォーマンスが競合する状況の例を紹介することを提案しました。私は毎日の仕事でこれに頻繁に問題を抱えていると感じていますが、それを良い最小限の例に要約するのは少し難しいと思います;)私は試してみます。

オブジェクト指向の世界では、私は単一責任原則の大ファンなので、メソッドが1つのことだけを担当するようにします。再利用可能かつ簡単にテスト可能になります。たとえば、リスト内のいくつかの数値の合計を計算し(基準に一致する)、同じ数値の平均を計算する必要がある場合、2つのメソッドを作成する必要があります。1つは合計を計算し、もう1つは平均を計算しました。このような:

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を2回実行する必要はありません。実際、パフォーマンスはEMRコストと直接連動しているため、本当にそれは望ましくありません。それを克服するために、SRPに違反し、2つの関数を1つにまとめて、次のように、国でフィルタリングされた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)
}

さて、この例はもちろん、実際の生活で遭遇することを非常に単純化した場合です。ここでは、dfbeforeをフィルター処理して永続化することで、sumおよびavg関数(より多くのSRPにもなります) )、しかし実際には、何度も何度も必要な中間計算が行われている場合があります。言い換えると、ここでのfilter関数は、永続化の恩恵を受けるもののsimpleの例を作成するための単なる試みです。実際、persistへの呼び出しはここのキーワードだと思います。 persistを呼び出すと、仕事が大幅にスピードアップしますが、コストは、永続的DataFrameに依存するすべてのコードを論理的に分離していても、しっかりと結合する必要があることです。

15
taotao.li