Comment Uber a optimisé son architecture de journalisation
![Uber journalisation](https://www.silicon.fr/wp-content/uploads/2024/08/Uber-journalisation.jpeg)
Uber a modernisé sa journalisation, à commencer par celle de son écosystème Spark, avec le compresseur CLP au cœur de la démarche.
À quand l’intégration de CLP avec l’infra de l’équipe observabilité ? Uber ne communique pas d’échéance, mais c’est sur sa feuille de route « journalisation ».
Voilà deux ans, l’entreprise américaine avait évoqué ses travaux en la matière. Plus particulièrement sur la compression des logs… grâce à CLP (Compressed Log Processor). Elle est récemment revenue sur l’extension de cette initiative au-delà de la cible initiale, qui était sa plate-forme Spark.
Au départ, 200 To de logs par jour
Sur la partie Spark, le chantier a consisté à intégrer CLP dans la bibliothèque de journalisation native, à savoir Log4j. Au démarrage du projet, la plate-forme big data exécutait, chaque jour, 250 000 jobs comprenant chacun des centaines de milliers de processus. Le volume quotidien au niveau de verbosité par défaut (INFO) pouvait atteindre 200 To. Uber avait alors limité la durée de conservation à 3 jours. L’allonger à un mois – ce que demandaient beaucoup d’utilisateurs internes – aurait fait exploser la facture. Rien que le stockage HDFS aurait coûté plusieurs millions de dollars par an, selon Uber. Et à une telle échelle, un outil comme Elasticsearch aurait engendré des coûts de matériel et d’exploitation prohibitifs.
À ce moment-là, pour les autres journaux, Uber utilisait ClickHouse. Une solution pas idéale pour traiter les logs essentiellement non structurés produits par Spark.
Une première solution a consisté à passer du niveau INFO au niveau WARN. Puis l’attention s’est portée sur la compression avec CLP. Ce dernier a requis des adaptations, entre autres parce que conçu pour traiter les journaux par lots alors que Log4J écrivait un fichier à la fois. Il en a résulté un pipeline à deux phases. La première pour compresser un log à la fois avec un taux de compression modeste. La deuxième pour agréger les fichiers et produire le format final.
Pourquoi Uber a préféré CLP aux compresseurs génériques
La seule implémentation de la phase 1 a nettement boosté le ratio de compression. En tout cas sur la foi d’un test interne étalé sur 30 jours. L’écosystème Spark avait produit un volume brut de 5,38 Po de logs INFO… qui ne pesaient plus que 31,4 To une fois compressés (169 fois moins).
Pourquoi CLP plutôt que des compresseurs génériques comme Gzip ou Zstandard ? Ces derniers n’exploitent pas pleinement les attributs spécifiques aux logs, explique Uber. Notamment leur caractère répétitif. Le logtype, par exemple, est statique. Et des variables se retrouvent, comme les identifiants de tâches et de conteneurs. Le problème, c’est l’interlacement de ces éléments avec d’autres. Il limite la capacité de compresseurs génériques à repérer les doublons (ils ne peuvent les identifier que dans une fenêtre glissante).
Autre limite de ces solutions : une inadaptation à la recherche. Ils nécessitent, du moins, de décompresser entièrement les fichiers. CLP, au contraire, analyse les logs et en fait une table structurée. Chaque message y est une ligne ; les différentes composantes (horodatage, logtype, variables), des colonnes. Ces dernières sont dédupliquées à renfort de dictionnaires. Ensuite intervient la compression avec Zstandard, une fois atteint un certain nombre de messages en mémoire tampon.
Un système en flux avec représentation intermédiaire
Uber exécute principalement ses jobs Spark dans des conteneurs, avec YARN comme gestionnaire de ressources. L’écriture des logs se fait d’abord sur le SSD local de chaque hôte. YARN les agrège ensuite pour les copier vers HDFS. Il fut d’abord question d’implémenter toute la logique de compression dans un appender Log4j. Mais Uber comprit que pour consommer le moins possible de mémoire (d’autant plus important sur Spark), il aurait besoin d’un compresseur en flux. Et que pour maximiser l’efficacité en agrégeant les logs de multiples nœuds ou VM, il allait falloir plusieurs appenders.
Seule la première étape du processus exécute les appenders (un par JVM). Ceux-ci analysent les logs et les convertissent en une représentation intermédiaire, envoyé à du Zstandard en flux, sans buffer. Après l’upload sur HDFS, l’agrégation des représentations intermédiaires se fait hors ligne. Puis le reste de l’algo CLP s’exécute pour produire le format orienté colonnes.
Pour réduire l’empreinte mémoire des appenders, on n’alloue pas d’espace aux dictionnaires lors de la première phase. Pour limiter le phénomène d’amplification d’écriture sur les SSD, le buffer est fixé à 4 Mo pour les logs compressés. Une taille idéale, d’après Uber, car elle est un multiple de toutes les tailles de bloc communes. La distribution de l’analyse entre les appenders permet par ailleurs d’exploiter les CPU souvent sous-utilisés des conteneurs Spark.
Un algo maison pour la virgule flottante
L’appender ayant un format spécialisé, il a fallu adapter les outils de visualisation de l’écosystème Hadoop/Spark. La méthode : un branchement if aux quelques endroits où un log est lu.
Uber a exploré l’idée d’un appender de type socket pour envoyer directement les logs sur le réseau. Il y a renoncé, détaille-t-il, vu le coût des services d’ingestion et le goulet d’étranglement qu’ils peuvent représenter dans l’optique d’une montée en charge. On peut y ajouter que certains outils Hadoop/Spark demandent d’avoir les logs sur disque/HDFS.
Pour encoder les nombres en virgule flottante, Uber a développé son propre algo. Il l’a substitué au standard natif de CLP (IEEE-754), jugé lent et pas sans perte (2.3456789, encodé puis décodé, peut devenir 2.3456788).
L’algo maison encode sur :
> 1 bit la condition « est négatif »
> 25 bits la valeur hors séparateur décimal et signe
> 3 le nombre de chiffres moins 1
> 3 bits la position du séparateur décimal depuis la droite, moins 1
L’encodage séparé du nombre de chiffres et de la position du séparateur décimal permet de gérer les chaînes commençant par des zéros.
Collaboration avec CLP sur un outil de visualisation
Il y a deux ans, Uber prototypait la phase 2 de son pipeline. La société disait avoir obtenu un ratio de compression 2,16 fois meilleur qu’avec Zstandard et 2,28 fois par rapport à Gzip. Elle envisageait le stockage des logs au format Parquet et une intégration avec le moteur SQL Presto. Ainsi que, déjà, une connexion avec sa plate-forme d’observabilité.
Depuis lors, les travaux se sont portés au-delà de la recherche et de l’agrégation. Promesse : permettre aux équipes de comprendre le « pourquoi » des erreurs rencontrées, en leur donnant accès à la chaîne de journalisation. Cela a impliqué d’intégrer recherche et visualisation.
Au départ, pour voir les logs directement sur chaque hôte Spark, Uber utilisait le serveur d’historique de tâches MapReduce. Lequel, en plus d’un UI jugée pas assez réactive, ne pouvait pas passer efficacement à l’échelle vu le nombre de fichiers dépassant les limites du stockage HDFS sous-jacent. En parallèle, peu d’outils d’observabilité ciblaient les logs non structurés. Entre autres conséquences, les index avaient tendance à enfler, et les coûts avec.
Une collaboration avec les développeurs de CLP a permis d’implémenter une visionneuse web serverless. Parmi ses fonctionnalités :
– Pagination (réduit l’empreinte mémoire)
– Filtrage bas niveau
– Recherche rapide de sous-chaînes et d’expressions régulières
– Prise en charge des événements multilignes
– Visualisation synchronisée (côte à côte, avec défilement en fonction de l’horodatage)
– Permalien pour chaque événement
– Coloration de texte
Uber a greffé à ce socle une bibliothèque d’analyse. Le cœur est en C++, avec des bindings Python, Go et Java. Elle fournit un accès direct à des données structurées, CLP analysant les logs à la compression.
Transition vers du stockage objet
Outre les limites de scaling liées à HDFS, le pipeline initial présentait une authentification Kerberos complexe. Uber a fini par basculer vers du stockage objet. Il utilise, en tant que clés, une caractéristique fixe des logs : leur chemin de fichier, fondé sur la position hiérarchique des entités qui les produisent.
Les systèmes de stockage objet ne supportant généralement pas les opérations append, un défi se pose pour la « fraîcheur » des logs lorsqu’on envoie de gros fichiers. Quant aux services comme YARN et Flink, ils sont conçus pour une exécution prolongée : on ne peut pas attendre la « fin » d’une tâche pour transférer les journaux.
Dans ce contexte, Uber a mis en place une fonctionnalité de segmentation. Tous les 16 Mo, elle crée un nouveau fichier. Un choix qui, assure l’entreprise, maximise les ratios de compression. Tout en minimisant l’empreinte du stockage et de la synchro… et en limitant la hausse des coûts API.
Pour réduire le coût par requête, la bibliothèque utilise une stratégie de flush et d’upload spécifique. Elle associe un délai incompressible et un délai « soft » au sens où il peut être repoussé selon les événements qui se produisent.
Le système permet aussi l’étiquetage des logs compressés avec plusieurs identifiants (service, tâche, app, utilisateur) et des plages d’horodatage. Les tags peuvent s’incorporer dans les chemins sur le stockage objet, être enregistrés dans des bases de données externes ou attachés sous forme de métadonnées.
L’intégration de CLP à Spark se fonde sur une réimplémentation de la codebase C++ en Java. Pour l’intégration dans les apps utilisant d’autres langages de programmation et d’autres frameworks de journalisation, Uber a retenu la solution bindings natifs.
Illustration © monsitj – Adobe Stock