Дякуємо за те, що ви поставили це вирішальне запитання. Чомусь, коли мова заходить про Spark, кожен настільки захоплюється аналітикою, що забуває про чудові практики програмної інженерії, що з’явилися за останні 15 років або близько того. Ось чому ми ставимо за мету обговорити тестування та постійну інтеграцію (серед іншого, як DevOps) у нашому курсі.
Короткий огляд термінології
Істинний тест блоку означає , що ви маєте повний контроль над кожним компонентом в тесті. Не може бути взаємодії з базами даних, викликами REST, файловими системами або навіть системним годинником; все повинно бути "подвоєно" (наприклад, знущатися над ним, тощо), як Gerard Mezaros вказує це в тестових шаблонах xUnit . Я знаю, що це здається семантикою, але це насправді важливо. Незрозуміння цього є однією з основних причин того, чому ви бачите періодичні невдачі тесту при постійній інтеграції.
Ми все ще можемо юніт-тест
Отже, з огляду на це розуміння, модульне тестування RDD
неможливе. Однак, все ще є місце для модульного тестування при розробці аналітики.
Розглянемо просту операцію:
rdd.map(foo).map(bar)
Тут foo
і bar
є прості функції. Їх можна протестувати в звичайному режимі, і вони повинні бути з якомога більшою кількістю кутових футлярів. Врешті-решт, чому їм цікаво, звідки вони беруть свої вклади - це тестовий прилад чи RDD
?
Не забувайте іскрову оболонку
Це не тестування як таке , але на цих ранніх етапах вам також слід експериментувати в оболонці Spark, щоб з’ясувати ваші перетворення та особливо наслідки вашого підходу. Наприклад, ви можете вивчити фізичні і логічні плани запитів, що ділять стратегії і збереження, і стан ваших даних з різними функціями , як toDebugString
, explain
, glom
, show
, printSchema
, і так далі. Я дозволю вам дослідити їх.
Ви також можете налаштувати свого майстра на local[2]
оболонку Spark та у своїх тестах, щоб виявляти будь-які проблеми, які можуть виникнути лише після того, як ви почнете розподіляти роботу.
Тестування інтеграції з Spark
Тепер про цікаві речі.
Для того, щоб перевірити інтеграцію Spark після того, як ви переконаєтесь у якості ваших допоміжних функцій та RDD
/ DataFrame
логіки перетворення, критично важливо зробити кілька речей (незалежно від інструменту побудови та тестової основи):
- Збільште пам’ять JVM.
- Увімкнути розгалуження, але вимкнути паралельне виконання.
- Використовуйте ваш тестовий фреймворк, щоб накопичувати свої тести інтеграції Spark у набори, і ініціалізувати
SparkContext
попередні тести та зупинити їх після всіх тестів.
З ScalaTest ви можете змішувати BeforeAndAfterAll
(що я переважно віддаю перевагу) або BeforeAndAfterEach
як @ShankarKoirala робить, щоб ініціалізувати та зруйнувати артефакти Spark. Я знаю, що це розумне місце, щоб зробити виняток, але мені справді не подобаються ті змінні, var
які ви повинні використовувати.
Шаблон позики
Інший підхід - використання шаблону позики .
Наприклад (за допомогою 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()
}
}
Як бачите, шаблон позики використовує функції вищого порядку, щоб "позичити" SparkContext
тест, а потім розпорядитися ним після закінчення.
Орієнтоване на страждання програмування (Дякую, Натане)
Це повністю питання переваг, але я вважаю за краще використовувати шаблон позики та підключати речі доти, доки зможу, перш ніж залучити іншу структуру. Окрім того, що просто намагаються залишатися легкими, фреймворки іноді додають багато «магії», через яку важко міркувати про помилки налагоджувальних тестів. Тому я приймаю підхід, орієнтований на страждання - де я уникаю додавання нового фреймворку, поки біль від його відсутності не буде занадто великим. Але знову ж таки, це залежить від вас.
Найкращий вибір для цього альтернативного фреймворку - це, звичайно, база для тестування іскр, як згадував @ShankarKoirala. У цьому випадку тест вище виглядатиме так:
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
}
}
}
Зверніть увагу, як мені не потрібно було нічого робити, щоб мати справу з SparkContext
. SharedSparkContext
дав мені все це - причому sc
як SparkContext
- безкоштовно. Особисто, хоча я б не залучав цю залежність саме з цією метою, оскільки шаблон позики робить саме те, що мені для цього потрібно. Крім того, з такою великою кількістю непередбачуваності, що трапляється з розподіленими системами, може бути справжньою болем прослідкувати магію, яка трапляється у вихідному коді сторонніх бібліотек, коли при постійній інтеграції все йде не так.
Зараз те, що іспитно-тестова база справді сяє, - це помічники на базі Hadoop, такі як HDFSClusterLike
і YARNClusterLike
. Змішування цих рис дійсно може заощадити вам багато болю при налаштуванні. Ще одне місце, де воно блищить, - це властивості та генератори, схожі на Scalacheck - припускаючи, звичайно, ви розумієте, як працює тестування на основі властивостей і чому це корисно. Але знову ж таки, я б особисто затримався з його використанням, поки моя аналітика та мої тести не досягнуть такого рівня витонченості.
"Тільки ситх має справу з абсолютом". - Обі-Ван Кенобі
Звичайно, вам не потрібно обирати те чи інше. Може бути , ви могли б використовувати шаблон підхід позики для більшості ваших тестів і іскрового тестуванням бази тільки для декількох, більш строгих випробувань. Вибір не є двійковим; ви можете зробити і те, і інше.
Тестування інтеграції з іскровим потоком
Нарешті, я хотів би лише представити фрагмент того, як може виглядати налаштування тесту інтеграції SparkStreaming зі значеннями в пам'яті без бази іспитового тестування :
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
Це простіше, ніж здається. Це насправді просто перетворює послідовність даних у чергу для подання до DStream
. Більшість з них насправді є лише типовим налаштуванням, яке працює з API Spark. Незважаючи на це, ви можете порівняти це з тим StreamingSuiteBase
, що знайдено в базі тестування іскр, щоб вирішити, якому саме ви віддаєте перевагу.
Це може бути мій найдовший допис, тож я залишу його тут. Я сподіваюся, що інші співпрацюють з іншими ідеями, щоб допомогти покращити якість нашої аналітики за допомогою тих самих гнучких практик інженерного програмного забезпечення, які покращили всі інші розробки додатків.
І, вибачившись за безсоромну розетку, ви можете ознайомитися з нашим курсом Analytics з Apache Spark , де ми розглядаємо багато цих ідей та багато іншого. Ми сподіваємось, що найближчим часом з’явиться онлайн-версія.