๐ฅ 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
- File โ New โ Project
- Sรฉlectionner sbt (ร gauche)
- 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)
- Name :
- 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
- Clic droit sur
Main.scalaโ Run 'Main' - 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
- Installer Almond (suivre les instructions section 2.1)
- Crรฉer un notebook Scala
- Importer Spark et Delta Lake
- Crรฉer un DataFrame, le transformer, l'รฉcrire en Delta
Exercice 4 : Projet IntelliJ Complet
- Crรฉer le projet selon la structure section 3
- Ajouter les fichiers de config
- Implรฉmenter le code
- Crรฉer le fichier CSV de test
- Exรฉcuter dans IntelliJ
- Builder avec
sbt assembly - 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 :
- Join sans broadcast โ Sort-Merge Join (shuffle de 100M rows)
- 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
- Projet IntelliJ complet avec structure
- ADT pour les erreurs et rรฉsultats
- 3 jobs : BronzeJob, SilverJob, GoldJob
- Main qui orchestre avec Either
- Tests avec ScalaTest
- 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
- IntelliJ IDEA โ IDE
- Almond โ Kernel Scala pour Jupyter
- Metals โ LSP pour VS Code
โก๏ธ Prochaine รฉtape
๐ Module suivant : 31_ml_engineering โ ML Engineering
๐ Fรฉlicitations ! Tu maรฎtrises maintenant Scala pour Spark et les optimisations avancรฉes.