Spark : quand faire un cache sur une DataFrame ?

Spark : quand faire un cache sur une DataFrame ?
dataframe cache pyspark spark cache persist

Pour améliorer les performances de votre job Spark, vous avez probablement déjà pensé à ajouter un cache sur une ou plusieurs de vos DataFrames. C’est même peut-être devenu une habitude.

Vous ne réfléchissez plus trop, vous ne savez plus vraiment pourquoi, mais après avoir lu une DataFrame et fait quelques opérations dessus, hop c’est mis en cache. Or, c’est une très mauvaise idée et nous allons découvrir pourquoi dans cet article.

Que se passe-t-il quand je cache une DataFrame ?

Tout d’abord, il faut savoir que la méthode cache() est un alias pour la fonction persist(StorageLevel.MEMORY_ONLY).

Contrairement à ce que l’on pourrait penser, cette fonction est une transformation. Cela signifie que le traitement sera lazy et attendra qu’une action soit effectuée pour se faire.

Quand une action sera demandée, la DataFrame va être calculé et à l’instruction cache() la donnée sera stockée dans la Spark Memory, plus précisément la Storage Memory :

Découpage de la mémoire dans un exécuteur Spark

Si vous souhaitez en savoir plus sur ce point précis, je vous invite à lire Spark memory management, un article un peu vieux (les valeurs par défaut citées ne sont plus à jour) mais qui reste quand même valable sur le principe.

Comme vous le voyez, la Storage Memory représente moins de 30% de la mémoire totale qu’on attribue à un executor. Sur 4 Go, cela représenterait 1 423.5 Mo. En cachant trop de DataFrames, même si vous avez beaucoup d’executor, la Storage Memory sera rapidement pleine.

Il reste toujours la possibilité d’augmenter le nombre d’executor ou la quantité de mémoire vive par executor, mais si vous êtes dans le cloud vous paierez plus cher votre infra, et si vous gérez votre propre infrastructure, vous atteindrez rapidement la limite. La meilleure solution est toujours d’optimiser son code quand c’est possible.

Que se passe-t-il quand La Storage Memory est pleine ?

Quand la Storage memory est pleine et qu’il reste de la place dans l’Execution memory, Spark fera déborder le cache dans l’Execution memory. Si tout est plein, Spark fera de la place dans la Storage Memory en déplaçant des blocs de mémoire sur le disque dur de l’executor. Une opération qui fait perdre du temps.

Lorsque Spark a besoin d’utiliser un bloc mémoire qui a été déplacé sur disque dur, c’est comme si le niveau du cache avait été réglé sur StorageLevel.DISK_ONLY. On gagne toujours du temps parce que la DataFrame n’a pas besoin d’être recalculé, mais moins que si le cache avait été fait en mémoire vive.

Il faut donc faire attention au nombre de DataFrames que l’on cache afin éviter de trop remplir la Storage Memory et perdre en efficacité de cache.

Quand faire un cache sur une DataFrame ?

Il y a deux questions simples à se poser pour déterminer quand il est pertinent de cacher une DataFrame. Cependant l’approche reste empirique, beaucoup de facteurs sont à prendre en compte pour bien répondre à ces questions.

Combien d’actions vont être appelées sur la DataFrame ?

Si vous n’avez pas au moins 2 actions, c’est totalement inutile. La DataFrame sera mis en cache à la première action mais jamais réutilisée par une seconde. À 2 actions, on peut se poser la question. À partir de 3 actions, cela devient pertinent dans la majorité des cas.

Quelles transformations sont appliquées sur ma DataFrame ?

Si vous ne faites que lire un fichier et filtrer une partie de la donnée, vous ne gagnerez pas beaucoup de temps à appliquer un cache sur le résultat. Sauf si vous l’utilisez de nombreuses fois derrière ou que votre lecture est couteuse (gros fichier texte non partitionné par exemple).

Dans le cas nominal, si vous lisez un fichier parquet bien partitionné, un cache devient pertinent après au moins une transformation de type reduce qui provoquera du shuffle.

La méthode unpersist

Il existe une méthode qui sert à supprimer une DataFrame du cache : unpersist(). Cette méthode prend un paramètre optionnel, un boolean qui définit s’il faut bloquer tous les traitements le temps de vider le cache ou pas.

Par défaut, c’est seulement une indication pour Spark que tous les blocs mémoires correspondant à une DataFrame peuvent être supprimés. Ce comportement est à privilégier car il laisse à Spark la liberté de supprimer un bloc seulement s’il a besoin de récupérer de l’espace mémoire. Ainsi on respecte la philosophie de Spark qui est : être feignant et ne faire quelque chose que quand c’est utile.

À titre personnel, j’utilise peu la méthode unpersist() que Spark gère très bien seul si on reste raisonnable sur l’utilisation du cache. Je recommande donc d’aider Spark à nous aider en prenant le temps de réfléchir à ces deux questions avant d’utiliser la fonction cache(). Je rappelle aussi que la Spark UI vous permettra de récupérer toutes les informations nécessaires à une meilleure prise de décision pour optimiser vos traitements.

🙏 Cliquez ici si vous avez aimé cet article, aidez nous à gagner en visibilité en partageant cet article sur Linkedin 🙏