jOOQ tip: ne convertissez pas JSONB en String

jOOQ tip: ne convertissez pas JSONB en String

Il y a quelques semaines, en investiguant de possibles améliorations de performance pour le backend JDBC de Kestra, j’ai remarqué qu’une méthode qu’on utilisait pour mapper une entité à persister en base de données dans sa représentation JSONB prenait beaucoup de temps dans nos profiles CPU.

Dans le flame graph suivant, on peut voir que la méthode JdbcQueue.map() compte pour plus de 21% des échantillons et la méthode Repository.map() pour 3.2% des échantillons d’un profile CPU utilisant async-profiler.

Ces deux méthodes permettent de mapper une colonne de type JSONB dans le type cible de l’entité. Pour cela, on utilise un object mapper de Jackson. Le code peut se simplifier en :

MAPPER.readValue(record.get("value", String.class), MyEntity.class);

Instinctivement, je me dis qu’il faudrait plutôt lire le record en JSONB qu’en String, je réalise donc le changement suivant :

MAPPER.readValue(record.get("value", JSONB.class).data(), MyEntity.class);

Et le résultat est immédiat, une nette amélioration des performances !

JdbcQueue.map() passe de plus de 21% des échantillons à un petit 4.6% et la méthode Repository.map() de 3.2% des échantillons à 2.7%. Dans le test que je réalise, la queue de Kestra est fortement sollicitée ce qui explique que l’impact soit beaucoup plus fort sur ce composant.

Content par l’amélioration, j’ouvre une PR que je merge assez rapidement : https://github.com/kestra-io/kestra/pull/4899/files.

Mais, après réflexion je me dis que j’aimerais bien savoir le pourquoi ?

J’ouvre donc mon IDE favoris et parcourt le code :

  • Record.get(String, Class) est implémenté par AbstractRecord.get(String, Class).
  • AbstractRecord.get(String, Class) mène à AbstractRecord.get(int, Class) où une méthode converterOrFail() est appelée. Ah!, une conversion, ça peut expliquer l’impact en performance !
  • La conversion se fait via un ConvertedProvider qui fournit un Converter.
  • Après une recherche rapide, les convertisseurs utilisent la classe utilitaire Convert pour la conversion de type, on s’approche !
  • Convert.from(), en ligne 716, convertit n’importe quel type en String via appel de sa méthode toString().
// All types can be converted into String
else if (toClass == String.class) {
    if (from instanceof EnumType e)
        return (U) e.getLiteral();
    }
    return (U) from.toString();
}

On arrive donc à la méthode JSONB.toString() qui utilise JSONValue.toJSONString(parsed())pour normaliser la représentation JSON et permettre l’égalité entre deux JSON de structure différente, mais avec les mêmes attributs.

Cette méthode est clairement documentée comme étant une méthode à éviter dans un contexte sensible à la performance et c’est clairement précisé dans la JavaDoc.

uses a normalised representation of the JSON content, meaning that two equivalent JSON documents are considered equal. This impacts both behaviour and performance!

Je connais donc maintenant le pourquoi !

Par acquit de conscience, je me suis penché sur notre implémentation MySQL qui utilise le type JSON, celui-ci n’effectue pas de normalisation et retourne directement le JSON sous-jasent via String.valueOf(data), elle ne souffre en conséquence pas du même souci de performance.

Voilà toute l’histoire, j’espère que cette plongée dans le code de jOOQ vous as intéressé autant que moi 😉

Mise à jour : après publication de cet article sur les réseaux sociaux, jOOQ a répondu pointant vers deux issues GitHub à ce propos :

  1. Revert JSONB::toString to produce JSONB::data
  2. Implement faster JSONB normalisation

La deuxième issue vaut la peine d’être lue, elle pose le problème de la normalisation, évoque des pistes d’amélioration et les limites du parser JSON actuel. Un problème qui pourrait sembler simple au premier abord est souvent beaucoup plus compliqué qu’il n’y parait.

Pour participer à la discussion, j’ai ouvert une issue Switch JSONB record convertion for String to use the data() method qui propose de fixer ce cas précis de la conversion sans devoir attendre de fixer le problème plus générale de la performance de la méthode toString().

Laisser un commentaire

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.