Executor en OutOfMemory : comment diagnostiquer ?

Questions d’entrevue Apache Spark

Seniormemoirediagnosticperformancecluster

Commencer par la bonne question : quel processus est tombé ?

C’est le premier tri, et beaucoup de candidats le sautent. Un OOM sur le driver et un OOM sur un executor n’ont ni les mêmes causes ni les mêmes remèdes.

Sur le driver, les suspects sont peu nombreux :

  • collect() ou toPandas() sur un DataFrame volumineux — les données remontent dans la mémoire du driver.
  • Un broadcast d’une table trop grosse : elle est collectée par le driver avant d’être redistribuée.
  • Un plan devenu gigantesque, typiquement dans un traitement itératif où la lignée s’allonge à chaque tour. C’est le cas que seul checkpoint() résout.

Sur un executor, les causes sont différentes :

  • Une clé chaude après un shuffle : une partition contient l’essentiel des données. C’est un problème de skew, pas de mémoire.
  • Un groupByKey là où un reduceByKey suffisait : toutes les valeurs d’une clé doivent tenir en mémoire sur une seule machine.
  • Un cache qui occupe la mémoire nécessaire aux shuffles.
  • Des partitions trop grosses, souvent après une lecture de fichiers volumineux non découpables (un CSV gzippé, par exemple, n’est pas splittable).

Ce qu’on regarde, dans l’ordre

  1. La Spark UI, onglet Stages. Comparer la durée et le volume de la tâche médiane à ceux de la tâche maximale. Un rapport de 1 à 50 désigne un skew, pas un manque de mémoire.
  2. La colonne Spill. Un déversement massif sur disque signale des partitions trop grosses pour la mémoire allouée aux tâches.
  3. Le compte de clés. groupBy("cle").count().orderBy(desc("count")) sur les vingt premières lignes tranche immédiatement entre une clé pathologique et une distribution naturelle.
  4. L’onglet Storage. Combien de mémoire le cache consomme-t-il réellement, et est-il utilisé ?

Les remèdes, dans le bon ordre

L’erreur d’instinct est d’augmenter spark.executor.memory. C’est le dernier levier, pas le premier : il masque le problème et double la facture.

Le bon ordre :

  1. Plus de partitions. Augmenter spark.sql.shuffle.partitions réduit la taille de chacune. C’est souvent la seule chose à faire, et elle est gratuite.
  2. Traiter le skew — AQE, isolation des valeurs sentinelles, broadcast ou salting.
  3. Remplacer l’opération coupable : reduceByKey au lieu de groupByKey, agrégation en DataFrame au lieu de RDD.
  4. Libérer le cache inutile avec unpersist(), ou passer en MEMORY_AND_DISK.
  5. Puis, seulement alors, revoir le dimensionnement.

La relance probable

« Pourquoi ne pas simplement mettre 64 Go par executor ? »

Parce qu’au-delà d’environ 32 Go de tas, le ramasse-miettes de la JVM devient une source de latence, et qu’on perd la compression des pointeurs d’objets. La pratique courante est plutôt de multiplier les executors de taille moyenne — 4 à 5 cœurs et 16 à 32 Go — que d’en faire un énorme.

Et il faut mentionner la mémoire hors tas : spark.executor.memoryOverhead couvre les tampons de shuffle et les processus Python. Un OOM tué par YARN ou Kubernetes avec un code de sortie 143 vient souvent de là, pas du tas.

Toutes les questions Apache Spark