web-dev-qa-db-fra.com

Spark organisation du code et meilleures pratiques

Donc, après avoir passé de nombreuses années dans un monde orienté objet avec la réutilisation du code, les modèles de conception et les meilleures pratiques toujours pris en compte, je me retrouve quelque peu en difficulté avec l'organisation et la réutilisation du code dans World of Spark.

Si j'essaie d'écrire du code de manière réutilisable, il a presque toujours un coût de performance et je finis par le réécrire dans ce qui est optimal pour mon cas d'utilisation particulier. Cette constante "écrire ce qui est optimal pour ce cas d'utilisation particulier" affecte également l'organisation du code, car la division du code en différents objets ou modules est difficile lorsque "tout appartient vraiment ensemble" et je me retrouve donc avec très peu d'objets "Dieu" contenant de longues chaînes de transformations complexes. En fait, je pense souvent que si j'avais jeté un coup d'œil à la plupart des codes Spark que j'écris maintenant lorsque je travaillais dans le monde orienté objet, j'aurais grimacé et rejeté comme "code spaghetti".

J'ai navigué sur Internet en essayant de trouver une sorte d'équivalent aux meilleures pratiques du monde orienté objet, mais sans beaucoup de chance. Je peux trouver quelques "meilleures pratiques" pour la programmation fonctionnelle mais Spark ajoute juste une couche supplémentaire, parce que les performances sont un facteur majeur ici.

Donc, ma question est la suivante: certains d'entre vous ont-ils Spark gourous trouvé quelques bonnes pratiques pour écrire Spark code que vous pouvez recommander?)

MODIFIER

Comme écrit dans un commentaire, je ne m'attendais pas vraiment à ce que quelqu'un poste une réponse sur la façon de résoudre ce problème, mais j'espérais plutôt que quelqu'un dans ce La communauté avait rencontré un type de Martin Fowler, qui avait écrit des articles ou des articles de blog quelque part sur la façon de résoudre les problèmes d'organisation du code dans le monde de Spark.

@DanielDarabos a suggéré que je pourrais mettre dans un exemple d'une situation où l'organisation du code et les performances sont en conflit. Bien que je trouve que j'ai souvent des problèmes avec cela dans mon travail quotidien, je trouve un peu difficile de le résumer à un bon exemple minimal;) mais je vais essayer.

Dans le monde orienté objet, je suis un grand fan du principe de responsabilité unique, donc je m'assurerais que mes méthodes n'étaient responsables que d'une chose. Il les rend réutilisables et facilement testables. Donc, si je devais, par exemple, calculer la somme de certains nombres dans une liste (correspondant à certains critères) et que je devais calculer la moyenne du même nombre, je créerais très certainement deux méthodes - une qui calculait la somme et une qui calculé la moyenne. Comme ça:

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
}

Je peux bien sûr continuer à honorer SRP dans Spark:

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)
}

Mais parce que mon df peut contenir des milliards de lignes, je préfère ne pas avoir à effectuer le filter deux fois. En fait, les performances sont directement couplées au coût du DME, donc je ne veux vraiment pas cela. Pour le surmonter, je décide donc de violer SRP et simplement de mettre les deux fonctions en une et de m'assurer d'appeler persist sur le DataFrame filtré par pays, comme ceci:

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)
}

Maintenant, cet exemple est bien sûr une énorme simplification de ce qui se rencontre dans la vie réelle. Ici, je pourrais simplement le résoudre en filtrant et en persistant df avant de le remettre aux fonctions sum et avg (qui seraient également plus SRP ), mais dans la vie réelle, il peut y avoir un certain nombre de calculs intermédiaires en cours qui sont nécessaires encore et encore. En d'autres termes, la fonction filter ici est simplement une tentative de faire un exemple simple de quelque chose qui bénéficiera de la persistance. En fait, je pense que les appels à persist sont un mot clé ici. Appeler persist accélérera considérablement mon travail, mais le coût est que je dois coupler étroitement tout le code qui dépend du DataFrame persistant - même s'ils sont logiquement séparés.

65
15
taotao.li