Performance base de données : Postgres vs. MySQL

Performance base de données : Postgres vs. MySQL

Kestra supporte entre autre Posstgres et MySQL comme base de données, nos benchmarks internes montraient que Postgres était beaucoup plus performant que MySQL pour faire tourner Kestra. Nous conseillons donc généralement de faire fonctionner Kestra en production avec Postgres.

Lors de nos benchmarks, Postgres support 4000 executions/mn avec une latence de 370ms pour nos flow type, avec une latence autour de 130ms pour un lancement unique. MySQL de son côté n’arrive même pas à soutenir 1000 executions/mn et a une latence autour de 160ms pour un lancement unique.

Mais pourtant, certains de nos utilisateurs utilisaient MySQL sans aucun souci, cela m’a poussé à me pencher sur la question.

Kestra utilise sa base de données pour trois choses :

  1. Stocker les ressources liées au workflow (flow, metadata, users, …)
  2. Stocker les données d’exécution de workflow
  3. Queue interne pour passage de messages asynchrone entre composants

La queue interne est la plus sensible des utilisations, elle est au coeur de Kestra et son Executor, responsable de la logique d’orchestration, en est très dépendant. L’Executor traite une exécution en boucle, à chaque étape il va faire avancer le workflow puis envoyer des tâches à exécuter au worker et un evènement de mise à jour de l’exécution pour se re-déclencher lui-même. Donc il lit un message dans la queue, traite le message, et en envoie un autre. La queue JDBC fonctionne via un SELECT ... FOR UPDATE, à la fin du traitement du message, il est supprimé dans la même transaction. Pour optimiser les performances, la queue lit en batch (pollSize – 100 par défault) et l’Executor traite en parallèle sur un nombre de threads configuré par défaut au nombre de coeurs CPU disponible.

Chaque exécution peut induire des dizaines voire des centaines de messages et donc de transactions DB, ce que je suspecte être la cause des différences de performances.

Après avoir vérifié que les indexes étaient identiques entre les deux bases et correctement utilisés, j’ai testé plusieurs choses:

  1. Configuration de la datasource, entre autres activer le cache des prepared statement qui est désactivé par défaut en MySQL mais activé en Postgres (voir ici pour référence). Aucun changement.
  2. Augmenter la taille du pollSize pour avoir des batch de plus grande taille (250). Aucun changement.
  3. Baisser la taille du pollSize, pour avoir des commits de plus petite taille (10). Pire!.

J’ai alors demandé à Claude ce qu’il en pensait et il m’a répondu :

La valeur par défaut de sync_binlog=1 de MySQL impose un fsync synchrone du binlog à chaque commits, et la conception de la queue JDBC de Kestra effectue des commits extrêmement fréquents (environ 52 commits par exécution, soit environ 2 600 commits/seconde lors d’un test de performance de 6 minutes avec 18 000 exécutions), car le traitement des messages s’effectue au sein de la même transaction que chaque cycle de requêtage de la file d’attente. Par défaut, Postgres effectue un fsync du WAL par commit et ne dispose pas de deuxième journal ; MySQL en effectue deux (redo log + bin log).

Pour résumer, MySQL est configuré par défaut pour faire deux fsync (appel systèm pour synchroniser sur disque les écritures de fichiers) par commit alors que Postgres n’en fait qu’un. Le deuxième est lié au binlog utilisé pour le point-in-time recovery et la réplications entre réplicas, là où le redo log est propre au moteur de stockage de MySQL (InnoDB ici), le binlog est identique quel qu’il soit.

En désactivant le binlog (--skip-log-bin) les performances sont à parité entre MySQL et Postgres ! :tada:

Mais en production, désactiver totalement le binlog n’est pas une bonne idée, on peut par contre sélectionner ce qui doit être stocké dans le binlog (via binlog_row_image) ou la fréquence d’écriture (via sync_binlog).

Configurer binlog_row_image à MINIMAL va créer des évènements plus petits dans le binlog, baissant donc le nombre d’octets écrit par seconde. Les tests ne se sont pas montrés concluants, c’est réellement le nombre d’écriture qui pose problème, pas leur taille, ce qui est cohérent avec les performance moins bonne quand on baisse le pollSize.

Configurer sync_binlog à 25 va écrire le binlog sur disque tous les 25 commits au lieu de chaque commit. Le redo log d’InnoDB reste synchronisé à chaque commit : un crash de MySQL ne fait perdre aucune donnée. En cas de crash de l’OS ou de la machine, jusqu’à 24 commits peuvent manquer dans le binlog (mais pas dans la base), ce qui n’a d’impact que pour la réplication et le point-in-time recovery.

Les performances sont alors proches de celles de Postgres. C’est un compromis entre performance et durabilité du binlog : à vous de choisir la valeur selon votre tolérance, mais la configuration du binlog est un classique du tuning MySQL.

Pour conclure, j’avais un a priori négatif sur les performances de MySQL mais sachant que certains gros utilisateurs de Kestra (des milliers de flows, des millions d’executions par mois) l’utilisaient je me suis dit qu’il fallait analyser ça. Et comme je ne suis pas expert, la petite aide de Claude m’a permis de pointer dans la bonne direction et d’en apprendre un peu plus sur le fonctionnement interne de MySQL.

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. Découvrez comment les données de vos commentaires sont traitées.