Vielen Dank, dass Sie diese offene Frage gestellt haben. Wenn es um Spark geht, sind alle aus irgendeinem Grund so in die Analyse verwickelt, dass sie die großartigen Praktiken der Softwareentwicklung vergessen, die in den letzten 15 Jahren oder so entstanden sind. Aus diesem Grund legen wir Wert darauf, in unserem Kurs das Testen und die kontinuierliche Integration (unter anderem DevOps) zu diskutieren.
Ein kurzer Überblick über die Terminologie
Ein echter Komponententest bedeutet, dass Sie die vollständige Kontrolle über jede Komponente im Test haben. Es kann keine Interaktion mit Datenbanken, REST-Aufrufen, Dateisystemen oder sogar der Systemuhr geben. Alles muss "verdoppelt" werden (z. B. verspottet, gestoppt usw.), wie Gerard Mezaros es in xUnit-Testmustern formuliert . Ich weiß, das scheint Semantik zu sein, aber es ist wirklich wichtig. Wenn Sie dies nicht verstehen, ist dies ein Hauptgrund, warum bei der kontinuierlichen Integration zeitweise Testfehler auftreten.
Wir können noch Unit-Test
Angesichts dieses Verständnisses ist ein Unit-Test RDD
unmöglich. Bei der Entwicklung von Analysen gibt es jedoch noch einen Platz für Unit-Tests.
Betrachten Sie eine einfache Operation:
rdd.map(foo).map(bar)
Hier foo
und bar
sind einfache Funktionen. Diese können auf normale Weise auf Einheit getestet werden, und sie sollten mit so vielen Eckkoffern wie möglich versehen sein. Warum interessiert es sie schließlich, woher sie ihre Eingaben beziehen, ob es sich um ein Testgerät oder ein Testgerät handelt RDD
?
Vergessen Sie nicht die Spark Shell
Dies ist kein Test an sich , aber in diesen frühen Phasen sollten Sie auch in der Spark-Shell experimentieren, um Ihre Transformationen und insbesondere die Konsequenzen Ihres Ansatzes herauszufinden. Zum Beispiel können Sie physische und logische Abfragepläne, die Partitionierung der Strategie und die Erhaltung und den Zustand Ihrer Daten mit vielen verschiedenen Funktionen wie untersuchen toDebugString
, explain
, glom
, show
, printSchema
, und so weiter. Ich werde Sie diese erkunden lassen.
Sie können Ihren Master auch local[2]
in der Spark-Shell und in Ihren Tests festlegen , um Probleme zu identifizieren, die möglicherweise erst auftreten, wenn Sie mit der Verteilung der Arbeit beginnen.
Integrationstests mit Spark
Nun zu den lustigen Sachen.
Um Spark zu integrieren, nachdem Sie sich von der Qualität Ihrer Hilfsfunktionen und RDD
/ oder der DataFrame
Transformationslogik überzeugt haben , ist es wichtig, einige Dinge zu tun (unabhängig von Build-Tool und Test-Framework):
- Erhöhen Sie den JVM-Speicher.
- Aktivieren Sie das Gabeln, aber deaktivieren Sie die parallele Ausführung.
- Verwenden Sie Ihr Testframework, um Ihre Spark-Integrationstests in Suites zu akkumulieren, die
SparkContext
vor allen Tests zu initialisieren und nach allen Tests zu stoppen.
Mit ScalaTest können Sie mischen BeforeAndAfterAll
(was ich allgemein bevorzuge) oder BeforeAndAfterEach
wie @ShankarKoirala, um Spark-Artefakte zu initialisieren und abzubauen. Ich weiß, dass dies ein vernünftiger Ort ist, um eine Ausnahme zu machen, aber ich mag die veränderlichen var
s, die Sie verwenden müssen, wirklich nicht .
Das Kreditmuster
Ein anderer Ansatz ist die Verwendung des Kreditmusters .
Zum Beispiel (mit ScalaTest):
class MySpec extends WordSpec with Matchers with SparkContextSetup {
"My analytics" should {
"calculate the right thing" in withSparkContext { (sparkContext) =>
val data = Seq(...)
val rdd = sparkContext.parallelize(data)
val total = rdd.map(...).filter(...).map(...).reduce(_ + _)
total shouldBe 1000
}
}
}
trait SparkContextSetup {
def withSparkContext(testMethod: (SparkContext) => Any) {
val conf = new SparkConf()
.setMaster("local")
.setAppName("Spark test")
val sparkContext = new SparkContext(conf)
try {
testMethod(sparkContext)
}
finally sparkContext.stop()
}
}
Wie Sie sehen können, verwendet das Kreditmuster Funktionen höherer Ordnung, SparkContext
um den Test zu "leihen" und ihn anschließend zu entsorgen.
Leidensorientierte Programmierung (Danke, Nathan)
Es ist völlig eine Frage der Präferenz, aber ich bevorzuge es, das Kreditmuster zu verwenden und die Dinge so lange wie möglich selbst zu verkabeln, bevor ich ein anderes Framework einbringe. Abgesehen davon, dass Frameworks nur versuchen, leicht zu bleiben, fügen sie manchmal viel "Magie" hinzu, was das Debuggen von Testfehlern schwierig macht. Daher verfolge ich einen leidensorientierten Programmieransatz, bei dem ich es vermeide, ein neues Framework hinzuzufügen, bis der Schmerz, es nicht zu haben, zu groß ist, um es zu ertragen. Aber auch dies liegt an Ihnen.
Die beste Wahl für dieses alternative Framework ist natürlich die Funkenprüfungsbasis, wie @ShankarKoirala erwähnt. In diesem Fall würde der obige Test folgendermaßen aussehen:
class MySpec extends WordSpec with Matchers with SharedSparkContext {
"My analytics" should {
"calculate the right thing" in {
val data = Seq(...)
val rdd = sc.parallelize(data)
val total = rdd.map(...).filter(...).map(...).reduce(_ + _)
total shouldBe 1000
}
}
}
Beachten Sie, dass ich nichts tun musste, um mit dem umzugehen SparkContext
. SharedSparkContext
gab mir das alles - mit sc
als SparkContext
- kostenlos. Persönlich würde ich diese Abhängigkeit jedoch nicht nur für diesen Zweck einbringen, da das Kreditmuster genau das tut, was ich dafür brauche. Bei so viel Unvorhersehbarkeit, die bei verteilten Systemen auftritt, kann es ein echtes Problem sein, die Magie im Quellcode einer Drittanbieter-Bibliothek nachvollziehen zu müssen, wenn bei der kontinuierlichen Integration Probleme auftreten.
Jetzt, wo die Funkenprüfbasis wirklich glänzt, sind die Hadoop-basierten Helfer wie HDFSClusterLike
und YARNClusterLike
. Das Einmischen dieser Eigenschaften kann Ihnen wirklich viel Setup-Schmerz ersparen. Ein weiterer Ort, an dem es glänzt, sind die Scalacheck- ähnlichen Eigenschaften und Generatoren - vorausgesetzt natürlich, Sie verstehen, wie eigenschaftsbasiertes Testen funktioniert und warum es nützlich ist. Aber auch hier würde ich mich persönlich zurückhalten, bis meine Analysen und Tests diesen Grad an Raffinesse erreicht haben.
"Nur ein Sith handelt absolut." -- Obi Wan Kenobi
Natürlich müssen Sie auch nicht das eine oder andere wählen. Vielleicht könnten Sie den Kreditmusteransatz für die meisten Ihrer Tests und die Funkenprüfungsbasis nur für einige strengere Tests verwenden. Die Wahl ist nicht binär; Sie können beides tun.
Integrationstests mit Spark Streaming
Abschließend möchte ich nur einen Ausschnitt davon präsentieren, wie ein SparkStreaming-Integrationstest-Setup mit speicherinternen Werten ohne Spark-Test-Basis aussehen könnte :
val sparkContext: SparkContext = ...
val data: Seq[(String, String)] = Seq(("a", "1"), ("b", "2"), ("c", "3"))
val rdd: RDD[(String, String)] = sparkContext.parallelize(data)
val strings: mutable.Queue[RDD[(String, String)]] = mutable.Queue.empty[RDD[(String, String)]]
val streamingContext = new StreamingContext(sparkContext, Seconds(1))
val dStream: InputDStream = streamingContext.queueStream(strings)
strings += rdd
Das ist einfacher als es aussieht. Es verwandelt wirklich nur eine Folge von Daten in eine Warteschlange, um sie dem zuzuführen DStream
. Das meiste davon ist wirklich nur ein Boilerplate-Setup, das mit den Spark-APIs funktioniert. Unabhängig davon können Sie dies mit dem StreamingSuiteBase
in der Funkenprüfbasis gefundenen vergleichen , um zu entscheiden, welche Sie bevorzugen.
Dies könnte mein längster Beitrag sein, also werde ich ihn hier lassen. Ich hoffe, dass andere sich anderen Ideen anschließen, um die Qualität unserer Analysen mit denselben agilen Softwareentwicklungspraktiken zu verbessern, die alle anderen Anwendungsentwicklungen verbessert haben.
Und mit Entschuldigungen für den schamlosen Plug können Sie unseren Kurs Analytics with Apache Spark lesen , in dem wir viele dieser Ideen und mehr ansprechen. Wir hoffen, bald eine Online-Version zu haben.