Trino pour l’interrogation de sources de données hétérogènes

Un socle commun pour les bases de données relationnelles

Les systèmes de données actuels ont tendance à se complexifier. Bien que leurs vendeurs prétendent souvent que leurs produits peuvent tout faire, la diversification des besoins et l’accentuation des attentes des utilisateurs pousse à une spécialisation des outils et techniques mis en œuvre pour collecter, traiter et stocker les données.

Cette tendance a des similitudes avec la philosophie d’Unix, selon laquelle chaque outil ne devrait faire qu’une chose mais la faire bien. On crée alors des systèmes de données avancés en combinant les outils de base. Cela suppose une socle de partage commun, un mécanisme de composition qui permette l’intégration des différents outils en dépit de leurs différences en termes d’implémentation.

Dans le domaine des bases de données relationnelles, une solution à ce besoin d’homogénéité a été apportée en 1974 par l’introduction de SQL: un langage d’interrogation qui non seulement s’est normalisé et est donc devenu (plus ou moins) commun à toutes les bases de données, mais qui de plus permet d’exprimer des requêtes sur des données à partir d’un modèle d’organisation logique des données (des relations), sans se soucier de la manière dont les données sont réellement organisées au sein des ordinateurs.

Et pour les autres bases de données?

À côté des bases de données relationnelles se trouvent d’autres dépôts de données. Leur diversité augmente avec le temps, afin de répondre au mieux aux besoins émergents et divergents. Par exemple, MongoDB propose l’interrogation de documents en utilisant une implémentation de MapReduce. Les APIs REST permettent l’exploitation de données exposées par des services Web. Prometheus peut répondre à des requêtes PromQL, un langage de requête approprié pour l’interrogation de séries chronologiques. Ces systèmes proposent des modèles de données fort différents, et permettent l’exploitation de ces modèles au travers de techniques hétérogènes, ce qui rend leur intégration plus complexe. Il est par exemple difficile de croiser les données provenant d’une base de données PostgreSQL avec celles exposées par une base de données MongoDB, autrement qu’en développement un logiciel ad hoc qui sert de glue entre elles.

Trino

Trino a été conçu pour répondre à cette problématique. Il s’agit est un moteur de traitement massivement parallèle (MPP). Il ne gère pas de données à proprement parler, mais s’interface avec des dépôts de données et peut leur soumettre des requêtes de lecture et/ou d’écriture (selon la nature de ces dépôts).

L’accès à un dépôt de données est configuré grâce à un connecteur, dont la configuration est réalisée par l’édition d’un fichier de configuration .properties (situé dans /etc/trino/catalog par défaut). La configuration comprend, en plus du type du connecteur, les paramètres d’accès et d’authentification au dépôt de données.

Trino expose des tables représentant les données de ses connecteurs. Lorsque le dépôt de données n’est pas de type tabulaire/relationnel, une conversion est réalisée par le connecteur. Par exemple, des données stockées en JSON seront converties en une table, en suivant la structuration des JSON présents dans les fichiers sous-jacents.

Lorsqu’une requête SQL lui est soumise, Trino

  1. Analyse la requête.
  2. Prépare un plan d’exécution, en convertissant la requête selon la nature des dépôts de données sous-jacents. Par exemple, si la requête porte sur des données encodées en Parquet, Trino prépare un plan consistant à parcourir la partie de l’arborescence de fichiers pertinente pour la requête considérée, et prépare la lecture des fichiers concernés afin de répondre à la requête.
  3. Exécute le plan préparé en le soumettant à des workers. Le résultat est retourné au client sous la forme d’un résultat SQL.

Trino expose un connecteur JDBC qui permet à des applications clientes de s’y connecter comme s’il s’agissait d’une authentique base de données relationnelle. Il est ainsi possible d’exposer des dépôts de données hétérogènes (bases de données relationnelles, fichiers Avro, clusters Cassandra, etc.) sous la forme de tables relationnelles interrogeables en SQL.

Optimisation et parallélisme

En production, lors de l’exécution d’un plan, plusieurs workers peuvent être exploités simultanément afin de le paralléliser. Par exemple, si les données sont enregistrées dans un ensemble de fichiers CSV, chaque workers peut se voir attribuer la lecture d’un sous-ensemble de ces fichiers.

On privilégiera les dépôts de données distribués afin que leur accès par de multiples workers ne constitue pas un goulot d’étranglement.

Trino tente de rendre l’exécution d’un plan aussi intelligente que possible. En fonction de la nature des dépôts de données sous-jacents, les prédicats (éléments de la clause WHERE de la requête) peuvent être poussés aussi bas que possible, afin que ça soit le dépôt de données qui s’occupe de les filtrer et les transformer, plutôt que de laisser Trino récupérer toutes les données et effectuer les transformations et filtrages nécessaires, ce qui nécessite davantage de ressources. Par exemple, si les données sont encodées au format Parquet, les partitions non pertinentes peuvent être écartées sans même que leur contenu ne soit lu. Si un dépôt de données propose des métadonnées facilitant l’exploitation des données (index, filtres de Bloom, etc.), ces métadonnées peuvent également être exploitées.

Les données non utiles à l’exécution du plan (parce qu’elles ont été exclues par une projection et qu’elles ne sont utilisées dans aucune jointure, par exemple) sont également ignorées si le dépôt de données sous-jacent le permet.

Hands On

Afin d’illustrer les capacités de Trino avec un minimum de préparatif, nous utiliserons une base de données Prometheus publiquement accessible. Comme indiqué précédemment, Prometheus est une base de données NoSQL pour le stockage de séries chronologiques, qui peut être interrogée en soumettant une requête HTTP. Sa réponse est en JSON. On n’interroge normalement pas Prometheus en SQL mais avec un langage propre à Prometheus: PromQL. Notre dépôt de données est donc fort différent, en termes d’exploitation, d’une base de données relationnelle.

Assurez-vous que Docker soit installé sur votre machine.

Créez un fichier de configuration prometheus.properties pour le connecteur Prometheus. Remarquez qu’il mentionne l’URI du serveur qui sera exploité:

connector.name=prometheus
prometheus.uri=https://prometheus.demo.do.prometheus.io
prometheus.query.chunk.size.duration=1d
prometheus.max.query.range.duration=21d
prometheus.cache.ttl=30s
prometheus.read-timeout=10s

Exécutez une instance d’une image Docker intégrant un connecteur pour Prometheus. Adaptez le chemin de la source montée afin de faire référence au fichier de configuration que vous venez de créer.

docker run -p 8080:8080 --mount type=bind,source="/home/mg/prometheus.properties",target="/etc/trino/catalog/prometheus.properties" --name trino trinodb/trino

Dès que vous voyez INFO main io.trino.server.Server ======== SERVER STARTED ======== dans les logs, c’est que Trino est prêt à recevoir des requêtes. Dans un autre terminal, lancez le CLI de Trino:

docker exec -it trino trino;

Une première commande nous permet de confirmer que notre catalogue bien été chargé (il aura le même nom que le fichier de configuration que vous avez monté dans le container, prometheus dans notre cas) :

trino> show catalogs;
  Catalog
------------
 jmx
 memory
 prometheus
 system
 tpcds
 tpch
(6 rows)

Query 20240925_140836_00000_gqw7z, FINISHED, 1 node
Splits: 11 total, 11 done (100.00%)
0.56 [0 rows, 0B] [0 rows/s, 0B/s]

Le connecteur Prometheus a fait le choix de représenter chaque série chronologique (identifiée par son __name__) sous la forme d’une table. Listons les tables (et donc les séries chronologiques) disponibles:

trino> SHOW TABLES FROM prometheus.default;
                                     Table
--------------------------------------------------------------------------------
 alertmanager_alerts
 alertmanager_alerts_invalid_total
 alertmanager_alerts_received_total
 alertmanager_build_info
 alertmanager_cluster_enabled
 alertmanager_config_hash
 alertmanager_config_last_reload_success_timestamp_seconds
 
[...] 

Affichons les valeurs de la série chronologique up, pour ces 10 dernières secondes:

trino> SELECT * FROM prometheus.default.up WHERE TIMESTAMP > (NOW() - INTERVAL '10' second);
                                     labels                                     |          timestamp          | value
--------------------------------------------------------------------------------+-----------------------------+-------
 {instance=demo.do.prometheus.io:8080, env=demo, job=cadvisor, __name__=up}     | 2024-09-25 14:14:16.659 UTC |   1.0
 {instance=demo.do.prometheus.io:9093, env=demo, job=alertmanager, __name__=up} | 2024-09-25 14:14:12.772 UTC |   1.0
 {instance=demo.do.prometheus.io:9100, env=demo, job=node, __name__=up}         | 2024-09-25 14:14:13.221 UTC |   1.0
 {job=grafana, __name__=up, instance=demo.do.prometheus.io:3000}                | 2024-09-25 14:14:14.752 UTC |   1.0
 {job=random, __name__=up, instance=demo.do.prometheus.io:8996}                 | 2024-09-25 14:14:13.072 UTC |   1.0
 {job=random, __name__=up, instance=demo.do.prometheus.io:8997}                 | 2024-09-25 14:14:21.225 UTC |   1.0
 {job=random, __name__=up, instance=demo.do.prometheus.io:8998}                 | 2024-09-25 14:14:17.542 UTC |   1.0
 {job=prometheus, __name__=up, instance=demo.do.prometheus.io:9090}             | 2024-09-25 14:14:12.474 UTC |   1.0
 {job=blackbox, __name__=up, instance=http://localhost:9100}                    | 2024-09-25 14:14:11.541 UTC |   1.0
 {job=caddy, __name__=up, instance=localhost:2019}                              | 2024-09-25 14:14:20.201 UTC |   1.0
(10 rows)

Query 20240925_141421_00005_gqw7z, FINISHED, 1 node
Splits: 1 total, 1 done (100.00%)
0.32 [63.4K rows, 1.27MB] [198K rows/s, 3.97MB/s]

Dans Prometheus, les « labels » sont des propriétés arbitraires associées aux séries chronologiques. Intéressons-nous aux valeurs de up, uniquement pour l’instance http://localhost:9100 . Cela nécessite de filtrer le contenu de la table sur base du dictionnaire contenu dans la colonne labels:

trino> SELECT * FROM prometheus.default.up WHERE labels['instance'] = 'http://localhost:9100' AND TIMESTAMP > (NOW() - INTERVAL '1' day);
                           labels                            |          timestamp          | value
-------------------------------------------------------------+-----------------------------+-------
 {job=blackbox, __name__=up, instance=http://localhost:9100} | 2024-09-24 14:24:11.541 UTC |   1.0
 {job=blackbox, __name__=up, instance=http://localhost:9100} | 2024-09-24 14:24:26.541 UTC |   1.0
 {job=blackbox, __name__=up, instance=http://localhost:9100} | 2024-09-24 14:24:41.541 UTC |   1.0
 {job=blackbox, __name__=up, instance=http://localhost:9100} | 2024-09-24 14:24:56.541 UTC |   1.0
 {job=blackbox, __name__=up, instance=http://localhost:9100} | 2024-09-24 14:25:11.541 UTC |   1.0
 {job=blackbox, __name__=up, instance=http://localhost:9100} | 2024-09-24 14:25:26.541 UTC |   1.0
 {job=blackbox, __name__=up, instance=http://localhost:9100} | 2024-09-24 14:25:41.541 UTC |   1.0

[...] 

Conclusion

Trino est un moteur de base de données très flexible et potentiellement très performant. En unifiant la manière d’interagir avec des dépôts de données exploitant des technologies variées grâce à un langage de requête très utilisé, il est possible de mettre rapidement sur pied des systèmes qui intègrent des données hétérogènes.

Nous n’avons fait qu’effleurer ses possibilités. En particulier, le système de pushdown des prédicats ainsi que la possibilité de distribuer les charges de traitement n’ont pas été mis en évidence. L’utilisation de connecteurs qui fonctionnent avec des flux de données (par exemple via un client Kafka) laisse entrevoir la possibilité de traitement en flux (ou plus exactement en micro-batch) plutôt que par lot.

Grâce à son driver JDBC, Trino peut s’intégrer aussi facilement avec une application cliente qu’une base de données relationnelle quelconque (du moins dans le monde Java). La création de nouveaux connecteurs Trino, couvrant des dépôts de données exotiques (Github, par exemple), pourrait être une source de valeur pour une entreprise.

Une réflexion sur “Trino pour l’interrogation de sources de données hétérogènes

  1. Pingback: Un connecteur Trino pour les flux RSS | Escaped Giraffe

Laisser un commentaire