๐ŸŸฅ Niveau 3 : Avancรฉ

๐Ÿš€ Spark & Scala Deep Dive

Bienvenue dans ce module avancรฉ oรน tu vas maรฎtriser Scala pour Spark et comprendre les internals de Spark pour รฉcrire des jobs performants. Tu apprendras ร  dรฉvelopper, builder et dรฉployer des applications Spark professionnelles.


Prรฉrequis

Niveau Compรฉtence
โœ… Requis PySpark DataFrame API (M19)
โœ… Requis Spark sur Kubernetes (M21)
โœ… Requis Notions de programmation fonctionnelle
๐Ÿ’ก Recommandรฉ Expรฉrience avec un IDE (VS Code, PyCharm)

๐ŸŽฏ Objectifs du module

ร€ la fin de ce module, tu seras capable de :

  • ร‰crire du code Scala idiomatique pour Spark
  • Utiliser les ADT (Algebraic Data Types) et Either/Try pour des pipelines robustes
  • Configurer un environnement de dรฉveloppement complet (Notebook + IntelliJ)
  • Builder et dรฉployer des applications Spark avec sbt et spark-submit
  • Comprendre Catalyst, AQE et Tungsten pour optimiser tes jobs
  • Diagnostiquer et rรฉsoudre les problรจmes de performance

1. Scala pour Data Engineers

1.1 Pourquoi Scala pour Spark ?

Aspect Scala Python
Performance Natif JVM, pas de sรฉrialisation Sรฉrialisation Python โ†” JVM
Typage Statique, erreurs ร  la compilation Dynamique, erreurs au runtime
API Spark API native, toutes les features Wrapper, parfois en retard
ร‰cosystรจme Kafka, Flink, Akka ML, Data Science
Courbe d'apprentissage Plus raide Plus accessible

Rรจgle pratique :

  • Python : Exploration, prototypage, Data Science, petits pipelines
  • Scala : Production, gros volumes, performance critique, รฉquipe backend Java/Scala

1.2 Syntaxe Essentielle

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// Variables : val (immutable) vs var (mutable)
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

val name: String = "Spark"     // Immutable (prรฉfรฉrรฉ)
var counter: Int = 0           // Mutable (ร  รฉviter)
val inferred = 42              // Type infรฉrรฉ automatiquement

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// Fonctions
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

// Fonction classique
def add(a: Int, b: Int): Int = {
  a + b
}

// Fonction one-liner (return implicite)
def multiply(a: Int, b: Int): Int = a * b

// Fonction anonyme (lambda)
val double = (x: Int) => x * 2

// Paramรจtres par dรฉfaut
def greet(name: String, greeting: String = "Hello"): String = 
  s"$greeting, $name!"

greet("Alice")              // "Hello, Alice!"
greet("Bob", "Bonjour")     // "Bonjour, Bob!"

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// String interpolation
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

val version = 3.5
println(s"Spark version: $version")           // Simple
println(s"Next version: ${version + 0.1}")    // Expression
println(f"Pi = ${math.Pi}%.2f")               // Formatรฉ

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// Conditions et boucles
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

// if est une expression (retourne une valeur)
val status = if (counter > 0) "positive" else "zero or negative"

// for-comprehension
val squares = for (i <- 1 to 5) yield i * i  // Vector(1, 4, 9, 16, 25)

// for avec filtres
val evenSquares = for {
  i <- 1 to 10
  if i % 2 == 0
} yield i * i  // Vector(4, 16, 36, 64, 100)

1.3 Collections Fonctionnelles

Les collections Scala sont la base pour comprendre les transformations Spark (RDD, DataFrame).

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// Types de collections
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

val list = List(1, 2, 3, 4, 5)           // Immutable, linked list
val vector = Vector(1, 2, 3, 4, 5)       // Immutable, indexed
val set = Set(1, 2, 3, 3, 3)             // Immutable, unique: Set(1, 2, 3)
val map = Map("a" -> 1, "b" -> 2)        // Immutable, key-value

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// Transformations (comme Spark !)
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

val numbers = List(1, 2, 3, 4, 5)

// map : transformer chaque รฉlรฉment
numbers.map(x => x * 2)              // List(2, 4, 6, 8, 10)
numbers.map(_ * 2)                   // Syntaxe courte

// filter : garder les รฉlรฉments qui matchent
numbers.filter(x => x > 2)           // List(3, 4, 5)
numbers.filter(_ > 2)                // Syntaxe courte

// flatMap : map + flatten
val words = List("hello world", "scala spark")
words.flatMap(_.split(" "))          // List("hello", "world", "scala", "spark")

// reduce : agrรฉger en une valeur
numbers.reduce((a, b) => a + b)      // 15
numbers.reduce(_ + _)                // Syntaxe courte

// fold : reduce avec valeur initiale
numbers.fold(0)(_ + _)               // 15
numbers.fold(10)(_ + _)              // 25 (commence ร  10)

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// Chaรฎnage (comme les pipelines Spark)
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

val result = numbers
  .filter(_ % 2 == 0)    // Garder les pairs
  .map(_ * 10)           // Multiplier par 10
  .sum                   // Sommer : 60 (20 + 40)

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// groupBy (comme Spark groupBy !)
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

case class Sale(product: String, amount: Double)

val sales = List(
  Sale("laptop", 1000),
  Sale("phone", 500),
  Sale("laptop", 1200),
  Sale("phone", 600)
)

val byProduct = sales.groupBy(_.product)
// Map(
//   "laptop" -> List(Sale("laptop", 1000), Sale("laptop", 1200)),
//   "phone"  -> List(Sale("phone", 500), Sale("phone", 600))
// )

val totalByProduct = sales
  .groupBy(_.product)
  .map { case (product, sales) => 
    (product, sales.map(_.amount).sum) 
  }
// Map("laptop" -> 2200.0, "phone" -> 1100.0)

1.4 Option : Gรฉrer les valeurs absentes

// Option = Some(valeur) ou None (jamais null !)

def findUser(id: Int): Option[String] = {
  val users = Map(1 -> "Alice", 2 -> "Bob")
  users.get(id)  // Retourne Option[String]
}

findUser(1)  // Some("Alice")
findUser(99) // None

// Pattern matching
findUser(1) match {
  case Some(name) => println(s"Found: $name")
  case None       => println("User not found")
}

// getOrElse : valeur par dรฉfaut
val user = findUser(99).getOrElse("Unknown")

// map sur Option (sรปr, pas d'exception)
val upperName = findUser(1).map(_.toUpperCase)  // Some("ALICE")
val noName = findUser(99).map(_.toUpperCase)    // None (pas d'erreur !)

// flatMap pour chaรฎner
def findEmail(name: String): Option[String] = {
  if (name == "Alice") Some("alice@example.com") else None
}

val email = findUser(1).flatMap(findEmail)  // Some("alice@example.com")

1.5 Case Classes et Pattern Matching

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// Case Class = classe de donnรฉes immuable
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

case class Customer(
  id: Long,
  name: String,
  email: String,
  country: String = "France"  // Valeur par dรฉfaut
)

// Crรฉation (pas besoin de "new")
val alice = Customer(1, "Alice", "alice@example.com")
val bob = Customer(2, "Bob", "bob@example.com", "USA")

// Accesseurs automatiques
println(alice.name)    // "Alice"
println(alice.country) // "France"

// equals automatique (compare les valeurs)
val alice2 = Customer(1, "Alice", "alice@example.com")
println(alice == alice2)  // true !

// copy : crรฉer une copie modifiรฉe (immutabilitรฉ)
val aliceUSA = alice.copy(country = "USA")

// toString automatique
println(alice)  // Customer(1,Alice,alice@example.com,France)

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// Pattern Matching (switch++ ultra puissant)
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

def describeCustomer(c: Customer): String = c match {
  case Customer(_, "Alice", _, _)     => "C'est Alice !"
  case Customer(id, _, _, "France")   => s"Client franรงais #$id"
  case Customer(_, name, _, country)  => s"$name de $country"
}

// Pattern matching avec guards
def customerTier(c: Customer, totalSpent: Double): String = c match {
  case Customer(_, _, _, "France") if totalSpent > 10000 => "VIP France"
  case _ if totalSpent > 5000                             => "Gold"
  case _                                                  => "Standard"
}

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// Case classes et Spark
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

// Spark utilise les case classes pour crรฉer des Datasets typรฉs !
import spark.implicits._

case class Sale(product: String, amount: Double, date: String)

val sales = Seq(
  Sale("laptop", 1000, "2024-01-15"),
  Sale("phone", 500, "2024-01-16")
).toDS()  // Dataset[Sale] - typรฉ !

// Accรจs typรฉ
sales.filter(_.amount > 600).show()

1.6 Sealed Traits et ADT (Algebraic Data Types) ๐Ÿ”ฅ

Les ADT permettent de modรฉliser tous les รฉtats possibles d'un systรจme. Le compilateur garantit l'exhaustivitรฉ !

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// sealed trait = toutes les sous-classes dans le mรชme fichier
// Le compilateur CONNAรŽT tous les cas possibles
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

// Modรฉliser le rรฉsultat d'un job ETL
sealed trait JobResult
case class Success(recordsProcessed: Long, durationMs: Long) extends JobResult
case class Failure(error: String, stage: String) extends JobResult
case object Skipped extends JobResult  // object = singleton

def handleResult(result: JobResult): Unit = result match {
  case Success(n, d) => println(s"โœ… $n records traitรฉs en ${d}ms")
  case Failure(e, s) => println(s"โŒ Erreur ร  l'รฉtape $s: $e")
  case Skipped       => println(s"โญ๏ธ Job ignorรฉ")
}
// Si tu oublies un cas, le compilateur te PRร‰VIENT !

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// Exemple : Sources de donnรฉes
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

sealed trait DataSource
case class JdbcSource(url: String, table: String, user: String) extends DataSource
case class S3Source(bucket: String, path: String, format: String) extends DataSource
case class KafkaSource(brokers: String, topic: String) extends DataSource
case class LocalSource(path: String) extends DataSource

def readData(source: DataSource)(implicit spark: SparkSession): DataFrame = source match {
  case JdbcSource(url, table, user) =>
    spark.read
      .format("jdbc")
      .option("url", url)
      .option("dbtable", table)
      .option("user", user)
      .load()
      
  case S3Source(bucket, path, format) =>
    spark.read
      .format(format)
      .load(s"s3a://$bucket/$path")
      
  case KafkaSource(brokers, topic) =>
    spark.read
      .format("kafka")
      .option("kafka.bootstrap.servers", brokers)
      .option("subscribe", topic)
      .load()
      
  case LocalSource(path) =>
    spark.read.parquet(path)
}

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// Exemple : Modes de traitement
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

sealed trait WriteMode
case object Overwrite extends WriteMode
case object Append extends WriteMode
case class MergeByKey(keys: Seq[String]) extends WriteMode

def writeData(df: DataFrame, path: String, mode: WriteMode): Unit = mode match {
  case Overwrite       => df.write.mode("overwrite").parquet(path)
  case Append          => df.write.mode("append").parquet(path)
  case MergeByKey(keys) => 
    // Logique Delta Lake MERGE
    println(s"Merge on keys: ${keys.mkString(", ")}")
}

1.7 Gestion d'Erreurs : Either et Try ๐Ÿ”ฅ

Fini les try/catch partout ! Scala offre des types pour gรฉrer les erreurs proprement.

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// Try[T] : capturer les exceptions (code legacy, I/O)
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

import scala.util.{Try, Success, Failure}

def parseNumber(s: String): Try[Int] = Try {
  s.toInt  // Peut lancer NumberFormatException
}

parseNumber("42")   // Success(42)
parseNumber("abc")  // Failure(NumberFormatException)

// Pattern matching
parseNumber("42") match {
  case Success(n)  => println(s"Nombre: $n")
  case Failure(ex) => println(s"Erreur: ${ex.getMessage}")
}

// getOrElse
val num = parseNumber("abc").getOrElse(0)  // 0

// map / flatMap (chaรฎnage sรปr)
val doubled = parseNumber("21").map(_ * 2)  // Success(42)

// recover : transformer l'erreur
val safe = parseNumber("abc").recover {
  case _: NumberFormatException => -1
}  // Success(-1)

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// Either[L, R] : erreurs mรฉtier typรฉes (pas d'exceptions)
// Left = erreur, Right = succรจs
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

// Dรฉfinir les types d'erreurs
sealed trait ETLError
case class SourceNotFound(path: String) extends ETLError
case class SchemaError(expected: String, actual: String) extends ETLError
case class WriteError(message: String) extends ETLError
case class ConfigError(param: String) extends ETLError

// Fonctions qui retournent Either
def readSource(path: String): Either[ETLError, DataFrame] = {
  if (!new java.io.File(path).exists())
    Left(SourceNotFound(path))
  else
    Right(spark.read.parquet(path))
}

def validateSchema(df: DataFrame, expected: Seq[String]): Either[ETLError, DataFrame] = {
  val actual = df.columns.toSeq
  if (expected.forall(actual.contains))
    Right(df)
  else
    Left(SchemaError(expected.mkString(","), actual.mkString(",")))
}

def writeOutput(df: DataFrame, path: String): Either[ETLError, Long] = {
  try {
    df.write.parquet(path)
    Right(df.count())
  } catch {
    case e: Exception => Left(WriteError(e.getMessage))
  }
}

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// Chaรฎnage avec for-comprehension (le pattern ultime !)
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

def runETL(inputPath: String, outputPath: String): Either[ETLError, Long] = {
  for {
    rawData      <- readSource(inputPath)
    validated    <- validateSchema(rawData, Seq("id", "name", "amount"))
    transformed  = validated.filter(col("amount") > 0)  // = pour les transformations pures
    recordCount  <- writeOutput(transformed, outputPath)
  } yield recordCount
}

// Utilisation
runETL("data/input", "data/output") match {
  case Right(count) => println(s"โœ… ETL terminรฉ : $count records")
  case Left(SourceNotFound(p)) => println(s"โŒ Source introuvable : $p")
  case Left(SchemaError(e, a)) => println(s"โŒ Schรฉma invalide. Attendu: $e, Reรงu: $a")
  case Left(WriteError(msg))   => println(s"โŒ Erreur d'รฉcriture : $msg")
  case Left(ConfigError(p))    => println(s"โŒ Config manquante : $p")
}

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// Quand utiliser quoi ?
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
Outil Quand l'utiliser Exemple
Option[T] Valeur absente (pas une erreur) findUser(id)
Try[T] Exceptions Java/legacy, I/O Try { file.read() }
Either[E, T] Erreurs mรฉtier typรฉes Pipeline ETL, validation

2. Environnements de Dรฉveloppement

2.1 Scala dans Jupyter Notebook avec Almond

Almond est un kernel Scala pour Jupyter, idรฉal pour l'exploration et la formation.

Installation

# 1. Installer Coursier (gestionnaire de packages Scala)
curl -fL https://github.com/coursier/coursier/releases/latest/download/cs-x86_64-pc-linux.gz | gzip -d > cs
chmod +x cs
./cs setup  # Ajoute au PATH

# 2. Installer Almond
cs launch --fork almond -- --install

# 3. Vรฉrifier
jupyter kernelspec list
# Devrait afficher : scala  /home/user/.local/share/jupyter/kernels/scala

# 4. Lancer Jupyter
jupyter notebook
# โ†’ Nouveau notebook โ†’ Kernel "Scala"

Configuration Spark dans Almond

// Dans une cellule du notebook Scala

// Importer les dรฉpendances avec Ammonite
import $ivy.`org.apache.spark::spark-sql:3.5.0`
import $ivy.`io.delta::delta-spark:3.1.0`

// Crรฉer la SparkSession
import org.apache.spark.sql.SparkSession

val spark = SparkSession.builder()
  .appName("Almond Spark")
  .master("local[*]")
  .config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension")
  .config("spark.sql.catalog.spark_catalog", "org.apache.spark.sql.delta.catalog.DeltaCatalog")
  .getOrCreate()

import spark.implicits._

// Test
val df = Seq((1, "Alice"), (2, "Bob")).toDF("id", "name")
df.show()

Alternatives

Kernel Avantages Inconvรฉnients
Almond Moderne, Ammonite, bien maintenu Setup manuel
spylon-kernel Simple, pip install Moins de features
Apache Toree Officiel Apache Spark Plus lourd, moins actif

2.2 IntelliJ IDEA

Pour les projets de production, IntelliJ IDEA est l'IDE de rรฉfรฉrence pour Scala.

Installation

# 1. Tรฉlรฉcharger IntelliJ IDEA Community Edition (gratuit)
# https://www.jetbrains.com/idea/download/

# 2. Linux : extraire et lancer
tar -xzf ideaIC-*.tar.gz
cd idea-IC-*/bin
./idea.sh

# 3. Installer le plugin Scala
# File โ†’ Settings โ†’ Plugins โ†’ Marketplace โ†’ Rechercher "Scala" โ†’ Install

# 4. Configurer le JDK
# File โ†’ Project Structure โ†’ SDKs โ†’ + โ†’ Download JDK โ†’ Version 11 ou 17

2.3 Quand utiliser quoi ?

Environnement Use case
Notebook (Almond) Exploration, prototypage, formation, dรฉmos
IntelliJ + sbt Dรฉveloppement, tests, refactoring, projets production
spark-submit Exรฉcution sur cluster (YARN, K8s)

3. Projet Complet avec IntelliJ (Step-by-Step)

On va crรฉer un projet Spark/Scala complet de A ร  Z.

3.1 Crรฉer le projet

  1. File โ†’ New โ†’ Project
  2. Sรฉlectionner sbt (ร  gauche)
  3. Configurer :
    • Name : spark-etl-project
    • Location : /home/user/projects/spark-etl-project
    • JDK : 11 ou 17
    • sbt version : 1.9.x
    • Scala version : 2.12.18 (compatible Spark 3.5)
  4. Create

3.2 Structure du projet

spark-etl-project/
โ”œโ”€โ”€ build.sbt                      # Dรฉpendances et config
โ”œโ”€โ”€ project/
โ”‚   โ”œโ”€โ”€ build.properties           # Version sbt
โ”‚   โ””โ”€โ”€ plugins.sbt                # Plugins (sbt-assembly)
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ main/
โ”‚   โ”‚   โ”œโ”€โ”€ scala/
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ com/
โ”‚   โ”‚   โ”‚       โ””โ”€โ”€ example/
โ”‚   โ”‚   โ”‚           โ”œโ”€โ”€ Main.scala
โ”‚   โ”‚   โ”‚           โ”œโ”€โ”€ config/
โ”‚   โ”‚   โ”‚           โ”‚   โ””โ”€โ”€ AppConfig.scala
โ”‚   โ”‚   โ”‚           โ”œโ”€โ”€ jobs/
โ”‚   โ”‚   โ”‚           โ”‚   โ””โ”€โ”€ SalesETL.scala
โ”‚   โ”‚   โ”‚           โ”œโ”€โ”€ models/
โ”‚   โ”‚   โ”‚           โ”‚   โ””โ”€โ”€ Models.scala
โ”‚   โ”‚   โ”‚           โ””โ”€โ”€ utils/
โ”‚   โ”‚   โ”‚               โ””โ”€โ”€ SparkSessionWrapper.scala
โ”‚   โ”‚   โ””โ”€โ”€ resources/
โ”‚   โ”‚       โ”œโ”€โ”€ application.conf
โ”‚   โ”‚       โ””โ”€โ”€ log4j2.properties
โ”‚   โ””โ”€โ”€ test/
โ”‚       โ””โ”€โ”€ scala/
โ”‚           โ””โ”€โ”€ com/
โ”‚               โ””โ”€โ”€ example/
โ”‚                   โ””โ”€โ”€ jobs/
โ”‚                       โ””โ”€โ”€ SalesETLSpec.scala
โ”œโ”€โ”€ data/
โ”‚   โ””โ”€โ”€ input/
โ”‚       โ””โ”€โ”€ sales.csv
โ””โ”€โ”€ output/                        # Gรฉnรฉrรฉ

3.3 Fichiers de configuration

build.sbt :

name := "spark-etl-project"
version := "1.0.0"
scalaVersion := "2.12.18"

// Spark 3.5 (provided = dรฉjร  sur le cluster)
val sparkVersion = "3.5.0"

libraryDependencies ++= Seq(
  // Spark (provided pour le cluster, compile pour local)
  "org.apache.spark" %% "spark-sql"  % sparkVersion % "provided",
  "org.apache.spark" %% "spark-core" % sparkVersion % "provided",
  
  // Delta Lake
  "io.delta" %% "delta-spark" % "3.1.0",
  
  // Config
  "com.typesafe" % "config" % "1.4.3",
  
  // Tests
  "org.scalatest" %% "scalatest" % "3.2.17" % Test
)

// Pour exรฉcuter en local dans IntelliJ (override provided)
Compile / run := Defaults.runTask(
  Compile / fullClasspath,
  Compile / run / mainClass,
  Compile / run / runner
).evaluated

// Assembly config (fat JAR)
assembly / assemblyMergeStrategy := {
  case PathList("META-INF", xs @ _*) => MergeStrategy.discard
  case "reference.conf"              => MergeStrategy.concat
  case x                              => MergeStrategy.first
}

assembly / assemblyJarName := s"${name.value}-${version.value}.jar"

project/build.properties :

sbt.version=1.9.8

project/plugins.sbt :

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.5")

3.4 Code Scala

src/main/scala/com/example/utils/SparkSessionWrapper.scala :

package com.example.utils

import org.apache.spark.sql.SparkSession

trait SparkSessionWrapper {
  
  lazy val spark: SparkSession = SparkSession.builder()
    .appName("SparkETL")
    .master(sys.env.getOrElse("SPARK_MASTER", "local[*]"))
    .config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension")
    .config("spark.sql.catalog.spark_catalog", 
            "org.apache.spark.sql.delta.catalog.DeltaCatalog")
    .config("spark.sql.adaptive.enabled", "true")
    .config("spark.sql.adaptive.coalescePartitions.enabled", "true")
    .getOrCreate()
    
  // Rรฉduire les logs Spark
  spark.sparkContext.setLogLevel("WARN")
}

src/main/scala/com/example/models/Models.scala :

package com.example.models

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// Modรจles de donnรฉes (case classes)
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

case class Sale(
  transactionId: String,
  productCategory: String,
  amount: Double,
  quantity: Int,
  date: String,
  customerId: String
)

case class SalesSummary(
  productCategory: String,
  totalSales: Double,
  totalQuantity: Long,
  avgSale: Double,
  transactionCount: Long
)

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ADT pour les rรฉsultats de job
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

sealed trait JobResult
case class JobSuccess(
  recordsRead: Long,
  recordsWritten: Long,
  durationMs: Long
) extends JobResult

case class JobFailure(
  stage: String,
  error: String
) extends JobResult

// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ADT pour les erreurs ETL
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

sealed trait ETLError
case class SourceNotFound(path: String) extends ETLError
case class InvalidSchema(message: String) extends ETLError
case class TransformError(message: String) extends ETLError
case class WriteError(message: String) extends ETLError

src/main/scala/com/example/jobs/SalesETL.scala :

package com.example.jobs

import com.example.models._
import com.example.utils.SparkSessionWrapper
import org.apache.spark.sql.{DataFrame, Dataset}
import org.apache.spark.sql.functions._

object SalesETL extends SparkSessionWrapper {
  
  import spark.implicits._
  
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // EXTRACT
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  
  def extract(inputPath: String): Either[ETLError, Dataset[Sale]] = {
    try {
      val df = spark.read
        .option("header", "true")
        .option("inferSchema", "true")
        .csv(inputPath)
        
      // Renommer les colonnes pour matcher la case class
      val cleaned = df
        .withColumnRenamed("transaction_id", "transactionId")
        .withColumnRenamed("product_category", "productCategory")
        .withColumnRenamed("customer_id", "customerId")
        
      Right(cleaned.as[Sale])
    } catch {
      case e: org.apache.spark.sql.AnalysisException =>
        Left(SourceNotFound(inputPath))
      case e: Exception =>
        Left(InvalidSchema(e.getMessage))
    }
  }
  
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // TRANSFORM
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  
  def transform(sales: Dataset[Sale]): Either[ETLError, Dataset[SalesSummary]] = {
    try {
      val summary = sales
        .filter(_.amount > 0)
        .groupByKey(_.productCategory)
        .agg(
          sum($"amount").as[Double],
          sum($"quantity").as[Long],
          avg($"amount").as[Double],
          count("*").as[Long]
        )
        .map { case (category, total, qty, avgSale, count) =>
          SalesSummary(category, total, qty, avgSale, count)
        }
        
      Right(summary)
    } catch {
      case e: Exception =>
        Left(TransformError(e.getMessage))
    }
  }
  
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // LOAD
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  
  def load(data: Dataset[SalesSummary], outputPath: String): Either[ETLError, Long] = {
    try {
      data.write
        .format("delta")
        .mode("overwrite")
        .save(outputPath)
        
      Right(data.count())
    } catch {
      case e: Exception =>
        Left(WriteError(e.getMessage))
    }
  }
  
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  // RUN (orchestration avec for-comprehension)
  // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
  
  def run(inputPath: String, outputPath: String): Either[ETLError, JobSuccess] = {
    val startTime = System.currentTimeMillis()
    
    for {
      sales   <- extract(inputPath)
      _       = println(s"๐Ÿ“ฅ Extracted ${sales.count()} records")
      
      summary <- transform(sales)
      _       = println(s"๐Ÿ”„ Transformed to ${summary.count()} categories")
      
      count   <- load(summary, outputPath)
      _       = println(s"๐Ÿ“ค Loaded $count records to $outputPath")
      
    } yield JobSuccess(
      recordsRead = sales.count(),
      recordsWritten = count,
      durationMs = System.currentTimeMillis() - startTime
    )
  }
}

src/main/scala/com/example/Main.scala :

package com.example

import com.example.jobs.SalesETL
import com.example.models._

object Main {
  
  def main(args: Array[String]): Unit = {
    
    val inputPath = args.lift(0).getOrElse("data/input/sales.csv")
    val outputPath = args.lift(1).getOrElse("output/sales_summary")
    
    println(s"๐Ÿš€ Starting ETL Job")
    println(s"   Input:  $inputPath")
    println(s"   Output: $outputPath")
    println()
    
    SalesETL.run(inputPath, outputPath) match {
      case Right(JobSuccess(read, written, duration)) =>
        println()
        println(s"โœ… Job completed successfully!")
        println(s"   Records read:    $read")
        println(s"   Records written: $written")
        println(s"   Duration:        ${duration}ms")
        
      case Left(SourceNotFound(path)) =>
        System.err.println(s"โŒ Source not found: $path")
        System.exit(1)
        
      case Left(InvalidSchema(msg)) =>
        System.err.println(s"โŒ Invalid schema: $msg")
        System.exit(1)
        
      case Left(TransformError(msg)) =>
        System.err.println(s"โŒ Transform error: $msg")
        System.exit(1)
        
      case Left(WriteError(msg)) =>
        System.err.println(s"โŒ Write error: $msg")
        System.exit(1)
    }
    
    // Arrรชter Spark proprement
    SalesETL.spark.stop()
  }
}

3.5 Donnรฉes de test

data/input/sales.csv :

transaction_id,product_category,amount,quantity,date,customer_id
TXN001,Electronics,1299.99,1,2024-01-15,CUST001
TXN002,Clothing,89.99,3,2024-01-15,CUST002
TXN003,Electronics,599.99,1,2024-01-16,CUST001
TXN004,Food,45.50,10,2024-01-16,CUST003
TXN005,Clothing,129.99,2,2024-01-17,CUST002
TXN006,Electronics,199.99,2,2024-01-17,CUST004
TXN007,Food,78.25,5,2024-01-18,CUST001
TXN008,Clothing,299.99,1,2024-01-18,CUST005
TXN009,Electronics,899.99,1,2024-01-19,CUST003
TXN010,Food,156.00,8,2024-01-19,CUST002

3.6 Exรฉcuter dans IntelliJ

  1. Clic droit sur Main.scala โ†’ Run 'Main'
  2. Ou crรฉer une Run Configuration :
    • Run โ†’ Edit Configurations โ†’ + โ†’ Application
    • Main class : com.example.Main
    • Program arguments : data/input/sales.csv output/sales_summary
    • VM options : --add-opens java.base/sun.nio.ch=ALL-UNNAMED (Java 17)

3.7 Builder le JAR

# Dans le terminal IntelliJ (View โ†’ Tool Windows โ†’ Terminal)

# Compiler
sbt compile

# Crรฉer le fat JAR (inclut toutes les dรฉpendances sauf Spark)
sbt assembly

# Le JAR est crรฉรฉ dans :
# target/scala-2.12/spark-etl-project-1.0.0.jar

3.8 Exรฉcuter avec spark-submit (local)

spark-submit \
  --master local[*] \
  --driver-memory 2g \
  --class com.example.Main \
  --packages io.delta:delta-spark_2.12:3.1.0 \
  target/scala-2.12/spark-etl-project-1.0.0.jar \
  data/input/sales.csv \
  output/sales_summary

3.9 Exรฉcuter sur cluster (YARN/K8s)

# YARN
spark-submit \
  --master yarn \
  --deploy-mode cluster \
  --driver-memory 4g \
  --executor-memory 8g \
  --executor-cores 4 \
  --num-executors 10 \
  --class com.example.Main \
  --packages io.delta:delta-spark_2.12:3.1.0 \
  spark-etl-project-1.0.0.jar \
  hdfs:///data/input/sales.csv \
  hdfs:///data/output/sales_summary

# Kubernetes (voir M27)
spark-submit \
  --master k8s://https://k8s-api:6443 \
  --deploy-mode cluster \
  --conf spark.kubernetes.container.image=my-spark:3.5.0 \
  --class com.example.Main \
  local:///opt/spark/jars/spark-etl-project-1.0.0.jar

4. Connecteurs : JARs et Configuration

4.1 JARs requis par source de donnรฉes

Source JAR Maven coordinates
PostgreSQL postgresql-42.7.0.jar org.postgresql:postgresql:42.7.0
MySQL mysql-connector-j-8.0.33.jar com.mysql:mysql-connector-j:8.0.33
SQL Server mssql-jdbc-12.4.0.jar com.microsoft.sqlserver:mssql-jdbc:12.4.0
Oracle ojdbc11-23.3.0.0.jar com.oracle.database.jdbc:ojdbc11:23.3.0.0
Kafka spark-sql-kafka-0-10_2.12-3.5.0.jar org.apache.spark:spark-sql-kafka-0-10_2.12:3.5.0
Delta Lake delta-spark_2.12-3.1.0.jar io.delta:delta-spark_2.12:3.1.0
Iceberg iceberg-spark-runtime-3.5_2.12-1.4.0.jar org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:1.4.0
AWS S3 hadoop-aws-3.3.4.jar + aws-java-sdk-bundle org.apache.hadoop:hadoop-aws:3.3.4
GCS gcs-connector-hadoop3-2.2.17.jar โ€” (tรฉlรฉchargement manuel)

4.2 Ajouter dans build.sbt

libraryDependencies ++= Seq(
  // Spark (provided = dรฉjร  sur le cluster)
  "org.apache.spark" %% "spark-sql" % "3.5.0" % "provided",
  
  // Connecteurs (compile = inclus dans le JAR)
  "org.postgresql" % "postgresql" % "42.7.0",
  "org.apache.spark" %% "spark-sql-kafka-0-10" % "3.5.0",
  "io.delta" %% "delta-spark" % "3.1.0",
)

4.3 spark-submit : --jars vs --packages

Option Usage
--jars Chemins locaux ou HDFS vers des JARs
--packages Coordonnรฉes Maven (tรฉlรฉchargement automatique)
--repositories Repos Maven custom
--driver-class-path JARs pour le driver uniquement
# Avec JARs locaux
spark-submit \
  --jars /path/to/postgresql-42.7.0.jar,/path/to/delta-spark_2.12-3.1.0.jar \
  my-app.jar

# Avec tรฉlรฉchargement Maven
spark-submit \
  --packages org.postgresql:postgresql:42.7.0,io.delta:delta-spark_2.12:3.1.0 \
  my-app.jar

# Repo custom
spark-submit \
  --repositories https://my-company.jfrog.io/artifactory/libs-release \
  --packages com.mycompany:my-lib:1.0.0 \
  my-app.jar

4.4 Exemples de connexion

โ„น๏ธ Note : Ces exemples supposent que vous avez accรจs aux services correspondants.

PostgreSQL :

val df = spark.read
  .format("jdbc")
  .option("url", "jdbc:postgresql://localhost:5432/mydb")
  .option("dbtable", "customers")
  .option("user", "postgres")
  .option("password", "secret")
  .option("driver", "org.postgresql.Driver")
  .load()

Kafka :

val df = spark.readStream
  .format("kafka")
  .option("kafka.bootstrap.servers", "localhost:9092")
  .option("subscribe", "events")
  .option("startingOffsets", "earliest")
  .load()

Delta Lake :

// Lecture
val df = spark.read.format("delta").load("/path/to/delta")

// ร‰criture
df.write.format("delta").mode("overwrite").save("/path/to/delta")

// Time travel
val dfV2 = spark.read.format("delta").option("versionAsOf", 2).load("/path/to/delta")

5. Catalyst Optimizer

Catalyst est le moteur d'optimisation de Spark SQL. Il transforme ton code en un plan d'exรฉcution optimal.

5.1 Phases d'optimisation

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                         CATALYST OPTIMIZER                                  โ”‚
โ”‚                                                                             โ”‚
โ”‚   Code SQL/DataFrame                                                        โ”‚
โ”‚         โ”‚                                                                   โ”‚
โ”‚         โ–ผ                                                                   โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                                       โ”‚
โ”‚   โ”‚    ANALYSIS     โ”‚  Rรฉsoudre les noms de colonnes, tables               โ”‚
โ”‚   โ”‚                 โ”‚  Vรฉrifier les types                                   โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                                       โ”‚
โ”‚            โ–ผ                                                                โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                                       โ”‚
โ”‚   โ”‚ LOGICAL OPTIM   โ”‚  Predicate pushdown, Column pruning                  โ”‚
โ”‚   โ”‚                 โ”‚  Constant folding, Filter reordering                 โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                                       โ”‚
โ”‚            โ–ผ                                                                โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                                       โ”‚
โ”‚   โ”‚ PHYSICAL PLAN   โ”‚  Choisir les algorithmes (Sort-Merge vs Broadcast)   โ”‚
โ”‚   โ”‚                 โ”‚  Cost-Based Optimization                             โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                                       โ”‚
โ”‚            โ–ผ                                                                โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                                       โ”‚
โ”‚   โ”‚ CODE GENERATION โ”‚  Gรฉnรฉrer du bytecode Java optimisรฉ                   โ”‚
โ”‚   โ”‚   (Tungsten)    โ”‚  Whole-Stage Code Generation                         โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                                       โ”‚
โ”‚            โ–ผ                                                                โ”‚
โ”‚        EXECUTION                                                            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

5.2 Lire un plan d'exรฉcution

val df = spark.read.parquet("data/sales")
  .filter($"amount" > 100)
  .groupBy("category")
  .agg(sum("amount"))

// Plan simple
df.explain()

// Plan dรฉtaillรฉ (Parsed โ†’ Analyzed โ†’ Optimized โ†’ Physical)
df.explain(true)

// Plan formatรฉ (Spark 3.0+)
df.explain("formatted")

// Plan avec coรปts (si CBO activรฉ)
df.explain("cost")

5.3 Exemple de plan

== Physical Plan ==
AdaptiveSparkPlan isFinalPlan=false
+- HashAggregate(keys=[category#12], functions=[sum(amount#14)])
   +- Exchange hashpartitioning(category#12, 200)        โ† SHUFFLE
      +- HashAggregate(keys=[category#12], functions=[partial_sum(amount#14)])
         +- Project [category#12, amount#14]             โ† Colonnes sรฉlectionnรฉes
            +- Filter (amount#14 > 100)                  โ† Filtre pushdown
               +- FileScan parquet [category#12,amount#14]  โ† Lecture

Ce qu'il faut regarder :

  • Exchange = Shuffle (coรปteux !)
  • BroadcastHashJoin vs SortMergeJoin (broadcast = plus rapide si petite table)
  • FileScan : colonnes pruned, partitions pruned
  • Filter : poussรฉ au plus prรจs de la source

5.4 Cost-Based Optimization (CBO)

// Activer CBO
spark.conf.set("spark.sql.cbo.enabled", "true")
spark.conf.set("spark.sql.cbo.joinReorder.enabled", "true")

// Collecter les statistiques (important !)
spark.sql("ANALYZE TABLE sales COMPUTE STATISTICS")
spark.sql("ANALYZE TABLE sales COMPUTE STATISTICS FOR COLUMNS amount, category")

6. Adaptive Query Execution (AQE)

AQE (Spark 3.0+) optimise le plan d'exรฉcution pendant l'exรฉcution, en se basant sur les statistiques rรฉelles.

6.1 Fonctionnalitรฉs

Feature Description
Coalesce Partitions Fusionner les petites partitions aprรจs shuffle
Broadcast Join Conversion Convertir Sort-Merge โ†’ Broadcast si table petite
Skew Join Optimization Splitter les partitions dรฉsรฉquilibrรฉes

6.2 Configuration

// Activer AQE (activรฉ par dรฉfaut depuis Spark 3.2)
spark.conf.set("spark.sql.adaptive.enabled", "true")

// Coalesce automatique
spark.conf.set("spark.sql.adaptive.coalescePartitions.enabled", "true")
spark.conf.set("spark.sql.adaptive.coalescePartitions.minPartitionSize", "64MB")

// Broadcast dynamique
spark.conf.set("spark.sql.adaptive.autoBroadcastJoinThreshold", "10MB")

// Skew join
spark.conf.set("spark.sql.adaptive.skewJoin.enabled", "true")
spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionFactor", "5")
spark.conf.set("spark.sql.adaptive.skewJoin.skewedPartitionThresholdInBytes", "256MB")

6.3 Voir AQE en action

// Le plan montre "AdaptiveSparkPlan"
df.explain()

// == Physical Plan ==
// AdaptiveSparkPlan isFinalPlan=true    โ† AQE activรฉ !
// +- ...

// Aprรจs exรฉcution, voir le plan final
df.collect()  // Exรฉcuter d'abord
df.explain()  // Maintenant isFinalPlan=true

7. Tungsten Engine

Tungsten est le moteur d'exรฉcution bas-niveau de Spark, optimisรฉ pour le CPU et la mรฉmoire.

7.1 Optimisations Tungsten

Optimisation Description
Off-heap Memory Stockage hors JVM heap (รฉvite GC)
Binary Format Donnรฉes en format binaire compact
Cache-aware Algorithmes optimisรฉs pour le cache CPU
Whole-Stage CodeGen Compile le plan en bytecode Java

7.2 Whole-Stage Code Generation

Au lieu d'appeler des fonctions virtuelles pour chaque row, Tungsten gรฉnรจre du code spรฉcialisรฉ.

SANS CODEGEN                          AVEC CODEGEN
                                      
for (row in data) {                   // Code gรฉnรฉrรฉ automatiquement
  filter.eval(row)   โ† virtual call   for (row in data) {
  project.eval(row)  โ† virtual call     if (row.amount > 100) {
  agg.update(row)    โ† virtual call       sum += row.amount
}                                       }
                                      }

7.3 Voir le code gรฉnรฉrรฉ

// Activer le debug
spark.conf.set("spark.sql.codegen.wholeStage", "true")

// Voir le code gรฉnรฉrรฉ
df.queryExecution.debug.codegen()

8. Memory Management & Tuning

8.1 Architecture Mรฉmoire Spark

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                     EXECUTOR MEMORY (spark.executor.memory)                 โ”‚
โ”‚                                                                             โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚   โ”‚                SPARK MEMORY (spark.memory.fraction = 0.6)           โ”‚  โ”‚
โ”‚   โ”‚                                                                     โ”‚  โ”‚
โ”‚   โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚  โ”‚
โ”‚   โ”‚   โ”‚   STORAGE MEMORY    โ”‚   โ”‚      EXECUTION MEMORY           โ”‚    โ”‚  โ”‚
โ”‚   โ”‚   โ”‚                     โ”‚   โ”‚                                 โ”‚    โ”‚  โ”‚
โ”‚   โ”‚   โ”‚   Cache, Broadcast  โ”‚ โŸท โ”‚   Shuffle, Join, Sort, Agg     โ”‚    โ”‚  โ”‚
โ”‚   โ”‚   โ”‚                     โ”‚   โ”‚                                 โ”‚    โ”‚  โ”‚
โ”‚   โ”‚   โ”‚   (storageFraction  โ”‚   โ”‚   (1 - storageFraction          โ”‚    โ”‚  โ”‚
โ”‚   โ”‚   โ”‚    = 0.5)           โ”‚   โ”‚    = 0.5)                       โ”‚    โ”‚  โ”‚
โ”‚   โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚  โ”‚
โ”‚   โ”‚                                                                     โ”‚  โ”‚
โ”‚   โ”‚   โ† โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Unified Memory (frontiรจre flexible) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ†’ โ”‚  โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                                                                             โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚   โ”‚                    USER MEMORY (0.4)                                โ”‚  โ”‚
โ”‚   โ”‚   Structures internes, UDFs, mรฉtadonnรฉes                            โ”‚  โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ”‚                                                                             โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚
โ”‚   โ”‚                    RESERVED MEMORY (300MB fixe)                     โ”‚  โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

8.2 Configurations Mรฉmoire

// Mรฉmoire totale executor
spark.conf.set("spark.executor.memory", "8g")

// Fraction pour Spark (vs User)
spark.conf.set("spark.memory.fraction", "0.6")  // 60% pour Spark

// Fraction Storage dans Spark Memory
spark.conf.set("spark.memory.storageFraction", "0.5")  // 50% Storage, 50% Execution

// Off-heap (Tungsten)
spark.conf.set("spark.memory.offHeap.enabled", "true")
spark.conf.set("spark.memory.offHeap.size", "4g")

8.3 Pourquoi 80% des Jobs Spark sont Lents ๐Ÿ”ฅ

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚            LES 5 CAUSES DE 80% DES JOBS SPARK LENTS                         โ”‚
โ”‚                                                                             โ”‚
โ”‚   1๏ธโƒฃ DATA SKEW (40%)                                                        โ”‚
โ”‚      โ””โ”€ 1 partition avec 10M rows, les autres avec 1K                       โ”‚
โ”‚      โ””โ”€ Symptรดme : 199 tasks ร  2s, 1 task ร  45min                          โ”‚
โ”‚      โ””โ”€ Fix : salting, repartition par clรฉ, AQE skew join                  โ”‚
โ”‚                                                                             โ”‚
โ”‚   2๏ธโƒฃ SHUFFLE EXCESSIF (25%)                                                 โ”‚
โ”‚      โ””โ”€ Trop de groupBy/join, pas de broadcast                              โ”‚
โ”‚      โ””โ”€ Symptรดme : "Shuffle Write" รฉnorme dans Spark UI                    โ”‚
โ”‚      โ””โ”€ Fix : broadcast join, rรฉduire colonnes avant shuffle               โ”‚
โ”‚                                                                             โ”‚
โ”‚   3๏ธโƒฃ MAUVAIS PARTITIONING (15%)                                             โ”‚
โ”‚      โ””โ”€ 200 partitions par dรฉfaut, fichiers trop petits/gros                โ”‚
โ”‚      โ””โ”€ Symptรดme : 10000 tasks de 100KB ou 2 tasks de 50GB                 โ”‚
โ”‚      โ””โ”€ Fix : repartition/coalesce, AQE coalesce, tuning shuffle.partitionsโ”‚
โ”‚                                                                             โ”‚
โ”‚   4๏ธโƒฃ SPILL TO DISK (10%)                                                    โ”‚
โ”‚      โ””โ”€ Mรฉmoire insuffisante, donnรฉes รฉcrites sur disque                    โ”‚
โ”‚      โ””โ”€ Symptรดme : "Spill (Memory)" dans Stage details                     โ”‚
โ”‚      โ””โ”€ Fix : augmenter executor memory, rรฉduire taille partitions         โ”‚
โ”‚                                                                             โ”‚
โ”‚   5๏ธโƒฃ SMALL FILES PROBLEM (10%)                                              โ”‚
โ”‚      โ””โ”€ Lire 10000 fichiers de 1MB au lieu de 100 de 100MB                  โ”‚
โ”‚      โ””โ”€ Symptรดme : temps de listing S3 trรจs long, driver OOM               โ”‚
โ”‚      โ””โ”€ Fix : compaction Delta, maxPartitionBytes, bin-packing             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

8.4 Diagnostic Toolkit

// 1. Vรฉrifier le plan d'exรฉcution
df.explain(true)          // Plan complet
df.explain("cost")        // Avec coรปts estimรฉs

// 2. Voir le nombre de partitions
println(s"Partitions: ${df.rdd.getNumPartitions}")

// 3. Dรฉtecter le skew (distribution par partition)
import org.apache.spark.sql.functions._
df.withColumn("partition_id", spark_partition_id())
  .groupBy("partition_id")
  .count()
  .orderBy(desc("count"))
  .show()

// 4. Voir la taille des partitions
df.rdd.mapPartitionsWithIndex { case (idx, iter) =>
  Iterator((idx, iter.size))
}.toDF("partition", "rows").show()

// 5. Mรฉtriques pendant l'exรฉcution
spark.conf.set("spark.sql.adaptive.enabled", "true")
spark.conf.set("spark.sql.adaptive.skewJoin.enabled", "true")

8.5 Spark UI : Ce qu'il faut regarder

Onglet Ce qu'on cherche
Jobs Durรฉe totale, jobs qui traรฎnent
Stages Shuffle Read/Write, Spill, tรขches en retard
Tasks Distribution (min/median/max), stragglers
Storage Cache utilisรฉ, mรฉmoire disponible
SQL Plan physique, mรฉtriques par opรฉrateur
Executors Mรฉmoire, GC time, shuffle read/write

8.6 Debugging OOM

java.lang.OutOfMemoryError: Java heap space

CAUSES PROBABLES :
โ”œโ”€โ”€ collect() sur un gros DataFrame
โ”œโ”€โ”€ broadcast() d'une table trop grande
โ”œโ”€โ”€ Trop de partitions small โ†’ overhead
โ”œโ”€โ”€ Skew โ†’ 1 executor avec trop de donnรฉes
โ””โ”€โ”€ Driver OOM โ†’ trop de metadata / rรฉsultats

SOLUTIONS :
โ”œโ”€โ”€ Augmenter spark.executor.memory
โ”œโ”€โ”€ Augmenter spark.driver.memory (si driver OOM)
โ”œโ”€โ”€ Repartitionner les donnรฉes
โ”œโ”€โ”€ ร‰viter collect(), utiliser take() ou write()
โ””โ”€โ”€ Rรฉduire spark.sql.autoBroadcastJoinThreshold

9. Exercices Pratiques

Exercice 1 : Pratique Scala โ€” ADT Pipeline

Modรฉliser un pipeline de traitement avec des ADT.

// TODO: Crรฉer un sealed trait pour les รฉtapes du pipeline
// ร‰tapes : Extract, Validate, Transform, Load
// Chaque รฉtape peut Success ou Fail

// TODO: Implรฉmenter une fonction qui exรฉcute le pipeline
// et retourne un rapport dรฉtaillรฉ de chaque รฉtape
๐Ÿ’ก Solution
sealed trait PipelineStep
case object Extract extends PipelineStep
case object Validate extends PipelineStep
case object Transform extends PipelineStep
case object Load extends PipelineStep

sealed trait StepResult
case class StepSuccess(step: PipelineStep, durationMs: Long, records: Long) extends StepResult
case class StepFailure(step: PipelineStep, error: String) extends StepResult

case class PipelineReport(results: List[StepResult]) {
  def isSuccess: Boolean = results.forall(_.isInstanceOf[StepSuccess])
  def failedStep: Option[StepFailure] = results.collectFirst { case f: StepFailure => f }
}

Exercice 2 : Either pour ETL

Implรฉmenter un mini-ETL avec gestion d'erreurs typรฉe.

// TODO: Crรฉer une fonction qui :
// 1. Lit un fichier CSV (peut รฉchouer : FileNotFound)
// 2. Valide que la colonne "amount" existe (peut รฉchouer : SchemaError)
// 3. Filtre les montants > 0 (peut รฉchouer : DataError si 0 records)
// 4. ร‰crit en Parquet
//
// Utiliser Either[ETLError, DataFrame] ร  chaque รฉtape
// Chaรฎner avec for-comprehension

Exercice 3 : Setup Almond + Spark

  1. Installer Almond (suivre les instructions section 2.1)
  2. Crรฉer un notebook Scala
  3. Importer Spark et Delta Lake
  4. Crรฉer un DataFrame, le transformer, l'รฉcrire en Delta

Exercice 4 : Projet IntelliJ Complet

  1. Crรฉer le projet selon la structure section 3
  2. Ajouter les fichiers de config
  3. Implรฉmenter le code
  4. Crรฉer le fichier CSV de test
  5. Exรฉcuter dans IntelliJ
  6. Builder avec sbt assembly
  7. Lancer avec spark-submit

Exercice 5 : Diagnostiquer un Job Lent

// Ce code est intentionnellement lent. Pourquoi ?
val df1 = spark.read.parquet("big_table")  // 100M rows
val df2 = spark.read.parquet("small_table")  // 1000 rows

val result = df1
  .join(df2, "key")  // Quel type de join ?
  .groupBy("category")
  .agg(sum("amount"))
  .collect()  // Problรจme ?

// TODO: 
// 1. Identifier les problรจmes
// 2. Proposer des optimisations
// 3. Rรฉรฉcrire le code optimisรฉ
๐Ÿ’ก Solution

Problรจmes :

  1. Join sans broadcast โ†’ Sort-Merge Join (shuffle de 100M rows)
  2. collect() sur un rรฉsultat potentiellement gros

Solution :

import org.apache.spark.sql.functions.broadcast

val result = df1
  .join(broadcast(df2), "key")  // Broadcast join !
  .groupBy("category")
  .agg(sum("amount"))
  .write.parquet("output")  // ร‰crire au lieu de collect

10. Mini-Projet : ETL Scala Bronze โ†’ Silver โ†’ Gold

Objectif

Construire un pipeline ETL complet en Scala avec architecture Medallion.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                         MEDALLION ARCHITECTURE                              โ”‚
โ”‚                                                                             โ”‚
โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                โ”‚
โ”‚   โ”‚   BRONZE    โ”‚ โ”€โ”€โ”€โ–ถ โ”‚   SILVER    โ”‚ โ”€โ”€โ”€โ–ถ โ”‚    GOLD     โ”‚                โ”‚
โ”‚   โ”‚   (Raw)     โ”‚      โ”‚  (Cleaned)  โ”‚      โ”‚ (Aggregated)โ”‚                โ”‚
โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                โ”‚
โ”‚                                                                             โ”‚
โ”‚   CSV/JSON           Delta Lake           Delta Lake                        โ”‚
โ”‚   + metadata         + validation         + business KPIs                   โ”‚
โ”‚   + ingestion_ts     + dedup              + reporting ready                 โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Livrables

  1. Projet IntelliJ complet avec structure
  2. ADT pour les erreurs et rรฉsultats
  3. 3 jobs : BronzeJob, SilverJob, GoldJob
  4. Main qui orchestre avec Either
  5. Tests avec ScalaTest
  6. Fat JAR et script spark-submit

Donnรฉes

data/raw/transactions_*.json (plusieurs fichiers) :

{"transaction_id": "TXN001", "customer_id": "C001", "amount": 150.0, "category": "Electronics", "timestamp": "2024-01-15T10:30:00Z"}

Jobs ร  implรฉmenter

BronzeJob :

  • Lire les JSON
  • Ajouter _ingestion_timestamp, _source_file
  • ร‰crire en Delta (append)

SilverJob :

  • Lire Bronze
  • Valider schรฉma
  • Dรฉdupliquer par transaction_id
  • Filtrer amount > 0
  • ร‰crire en Delta avec MERGE

GoldJob :

  • Lire Silver
  • Agrรฉgations : ventes par catรฉgorie, par jour
  • Top customers
  • ร‰crire plusieurs tables Gold

๐Ÿ“š Ressources

Documentation

Livres

  • Programming in Scala โ€” Martin Odersky
  • Functional Programming in Scala โ€” Paul Chiusano, Rรบnar Bjarnason
  • Spark: The Definitive Guide โ€” Bill Chambers, Matei Zaharia

Outils


โžก๏ธ Prochaine รฉtape

๐Ÿ‘‰ Module suivant : 31_ml_engineering โ€” ML Engineering


๐ŸŽ‰ Fรฉlicitations ! Tu maรฎtrises maintenant Scala pour Spark et les optimisations avancรฉes.