Riadh Mnasri
← Retour au blog
4 min de lecture

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é SparkCe qu'elle apporte à ce calcul
Partitionnement par scénario/positionChaque 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 lignageLa perte d'un exécuteur ne recommence pas tout le batch, juste sa partition
Agrégations distribuéesLe 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 :

AspectChoix pertinent pour un batch de risque
Stockage des données de marché et positionsAzure 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 stockageMicrosoft 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 nocturnePool 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.