Apache Spark sur Kubernetes et Azure pour le calcul d'indicateurs de risque

Dans l'article sur la VaR, je mentionne que ces calculs de risque tournent chaque nuit sur l'ensemble du portefeuille d'une banque, et doivent finir avant l'ouverture des marchés. C'est exactement ce type de contrainte, simulation massive et délai strict, qui justifie d'assembler Spark, Kubernetes et Azure plutôt que d'empiler ces trois briques par réflexe technologique.
Pourquoi Spark, spécifiquement pour ce calcul
Une simulation Monte Carlo pour une VaR ou une CVA consiste à rejouer des milliers de trajectoires de marché possibles sur chaque position d'un portefeuille, puis à agréger le résultat. C'est un problème « embarrassingly parallel » : chaque scénario se calcule indépendamment des autres, ce qui correspond exactement au modèle de partitionnement de Spark.
| Propriété Spark | Ce qu'elle apporte à ce calcul |
|---|---|
| Partitionnement par scénario/position | Chaque exécuteur calcule un sous-ensemble de trajectoires en parallèle |
| Calcul en mémoire (DataFrame/RDD) | Évite de relire le référentiel de positions à chaque étape d'agrégation |
| Tolérance aux pannes par lignage | La perte d'un exécuteur ne recommence pas tout le batch, juste sa partition |
| Agrégations distribuées | Le calcul du quantile final (la VaR elle-même) se fait après collecte des résultats partiels |
La tolérance aux pannes par lignage mérite d'être soulignée : Spark ne sauvegarde pas un état à chaque étape, il retient comment reconstruire une partition perdue à partir des données sources. Sur un batch qui tourne plusieurs heures cette nuit, perdre un nœud ne coûte que le recalcul de sa part du travail, pas la reprise depuis zéro.
Pourquoi Kubernetes comme gestionnaire de cluster
Spark supporte Kubernetes nativement depuis la version 2.3, aux côtés de YARN et Mesos. Le bénéfice concret : le cluster de calcul de risque s'exécute sur la même plateforme que le reste des services (API, ingestion de données de marché), sans opérer un cluster Hadoop/YARN séparé rien que pour ce batch.
Driver pod
┌─────────────────┐
│ planifie le │
│ DAG, collecte │
│ les résultats │
└────────┬────────┘
│ crée
┌─────────────┼─────────────┐
▼ ▼ ▼
Executor pod Executor pod Executor pod
(scénarios (scénarios (scénarios
1-1000) 1001-2000) 2001-3000)
Le driver crée dynamiquement des pods exécuteurs via l'API Kubernetes, un
par tranche de travail, puis les détruit une fois le calcul terminé. En
pratique, la déclaration se fait via le
Kubeflow Spark Operator, qui
introduit une ressource personnalisée SparkApplication plutôt que
d'invoquer spark-submit à la main :
apiVersion: sparkoperator.k8s.io/v1beta2
kind: SparkApplication
metadata:
name: var-nightly-batch
spec:
type: Scala
mode: cluster
image: registry.example.com/risk-engine:1.4.0
driver:
cores: 2
memory: 4g
executor:
cores: 4
memory: 8g
instances: 50
Le piège spécifique à Kubernetes : l'allocation dynamique
Sur YARN, l'allocation dynamique (ajuster le nombre d'exécuteurs en cours de route selon la charge) s'appuie sur un service de shuffle externe, persistant même après l'arrêt d'un exécuteur. Kubernetes ne propose pas cet équivalent nativement. Depuis Spark 3.0, la solution est le suivi de shuffle (shuffle tracking) : Spark garde en vie un exécuteur tant qu'il détient des données de shuffle utilisées par un job actif, sans dépendre d'un service externe.
spark.dynamicAllocation.enabled=true
spark.dynamicAllocation.shuffleTracking.enabled=true
Sans cette option activée explicitement, l'allocation dynamique sur Kubernetes ne fonctionne pas comme attendu : des exécuteurs peuvent être supprimés alors qu'ils détiennent encore des données de shuffle nécessaires à une étape suivante du calcul.
Ce qu'Azure change concrètement
Sur AKS, deux choix ont un impact direct sur ce type de batch :
| Aspect | Choix pertinent pour un batch de risque |
|---|---|
| Stockage des données de marché et positions | Azure Data Lake Storage Gen2, accédé via le connecteur abfss://, plutôt qu'un stockage local aux nœuds |
| Identité du driver/exécuteurs vers le stockage | Microsoft Entra Workload ID (voir l'article sur Kubernetes au quotidien), pas de clé de stockage statique dans une variable d'environnement |
| Nœuds pour le batch nocturne | Pool de nœuds Spot dédié : la fenêtre nocturne tolère la préemption, puisqu'une partition perdue se recalcule automatiquement grâce au lignage Spark |
Le pool Spot est le choix le plus contre-intuitif au premier abord, mais le plus cohérent avec les propriétés de Spark décrites plus haut : la tolérance aux pannes par lignage transforme une contrainte (des nœuds qui peuvent disparaître à tout moment) en simple ralentissement occasionnel, pas en échec du batch. Un service qui doit répondre en temps réel n'a pas cette latitude ; un batch qui dispose de plusieurs heures avant l'ouverture des marchés l'a.
Ce que je ne recommande pas par défaut : opérer soi-même Spark sur AKS
Databricks (disponible nativement sur Azure) gère l'essentiel de cette complexité opérationnelle : allocation dynamique, autoscaling, mise à jour de version Spark, sans avoir à opérer le Spark Operator ni les manifestes Kubernetes soi-même. Le choix d'un Spark auto-hébergé sur AKS se justifie quand une contrainte précise l'exige (portabilité multi-cloud, contrôle fin sur la version de Spark ou des dépendances natives, coût à très grande échelle où la marge d'un service managé pèse lourd), pas comme option par défaut. Pour la majorité des équipes qui calculent des indicateurs de risque, Databricks reste le point de départ le plus raisonnable ; Spark sur Kubernetes devient pertinent quand cette simplicité opérationnelle est échangée sciemment contre un contrôle que le managé ne permet pas.