Il y a 11 ans -
Temps de lecture 10 minutes
Kafka
Kafka est un Message-Oriented Middleware (MOM) développé par LinkedIn et passé en incubation chez Apache. Ce projet se démarque beaucoup de ses confrères comme les serveurs JMS, AMQP ou autres par des choix d’architecture radicaux que nous allons exposer dans cet article.
Écrire pour toujours, consommer vite
La structure de persistance de Kafka s’appuie sur le slogan Don’t fear the filesystem ! qui suit la tendance de certaines applications, comme Varnish, fortement contraintes par une stabilité de leurs performances en écriture et par la confiance accordée au disque dur plutôt qu’à la mémoire vive du serveur pour maintenir les données. L’idée vient du constat que l’écriture en RAM peut avoir des comportements difficilement prévisibles dus par exemple au garbage collection sur de grands heap spaces alors que l’écriture en append only sur un fichier peut être très performante tout en assurant un temps constant. L’article qui a inspiré Kafka décrit avec plus de détails ces aspects.
Ce principe se concrétise dans Kafka par l’écriture des messages dans des fichiers de logs, appelés aussi partitions, où chaque ligne contiendra un en-tête assez réduit (essentiellement le checksum) et le message sous forme de string ou encodé avec un serialiseur de son choix (Kafka prend l’exemple d’Avro). La taille de ces fichiers, le nombre de roulements et leur durée de vie sont configurables. Kafka va même plus loin en proposant de configurer la durée et/ou la taille maximale du buffer dans le page cache. Les messages sont donc écrits séquentiellement et sont gardés pour une durée déterminée par l’utilisateur qui n’a pas de raison de supprimer des messages tant qu’il a de l’espace sur le disque dur. Par contre il n’est pas possible d’écrire au milieu d’un fichier ou de supprimer unitairement un message. Cette règle garantit l’efficacité de l’écriture.
Ce design apporte un avantage immédiat : il n’y a plus de crainte à avoir lorsqu’on arrête un serveur, il peut récupérer l’ensemble de ses messages au redémarrage. De plus, il a la possibilité de relire la file de messages autant de fois qu’il le veut, il lui suffit de parcourir à nouveau les fichiers de logs, et ce, même plusieurs jours après.
Les ingénieurs de Kafka sont partis également du postulat que LinkedIn avait surtout besoin de gérer un gros volume de messages, qu’il y aurait toujours au moins un consommateur et qu’il fallait donc privilégier la consommation à la production. Dans l’architecture, cela se concrétise par deux points :
- l’envoi d’un ensemble de messages (MessageSet) plutôt que de messages unitaires, ce qu’on peut faire en JMS avec les regroupements logiques sur la propriété JMSXGroupId du message;
- l’utilisation de l’API Java NIO et plus particulièrement la méthode
FileChannel.transferTo
qui permet de sauter l’étape d’écriture dans un buffer côté applicatif et donc d’écrire plus rapidement sur le réseau.
Penser distribué
La gestion des messages sur des serveurs distribués est au cœur de la conception de Kafka. Ce choix est un vrai atout pour la scalabilité de l’application mais complique par ailleurs la compréhension des concepts sous-jacents. Les enjeux étaient de pouvoir augmenter la capacité des serveurs en ajoutant de nouveaux nœuds de façon transparente. Si la quantité de messages augmente, la seule contrainte pour Kafka est d’augmenter l’espace de stockage, ce qui en général ne pose plus de problème dans nos datacenters.
La distribution est en grande partie assurée grâce à Zookeeper, le service de coordination centralisé d’Apache. Les différents serveurs Kafka communiquent avec Zookeeper pour signaler la topologie du cluster, que ce soit le nombre de partitions qu’ils gèrent ou leur taille.
De même chaque client s’enregistre dans Zookeeper et peut former des groupes. Un groupe peut être vu comme une queue en JMS, chaque consommateur du même groupe dépilant le même ensemble de messages, ce qui revient à faire du multithreading sur des queues. Si on veut que deux consommateurs lisent indépendamment cet ensemble de messages, comme on aurait plusieurs subscribers sur un même topic, il suffit de créer deux groupes.
S’il n’est pas évident de calquer notre vision traditionnelle queue/topic sur la conception de Kafka, c’est qu’il n’existe pas de notion de file de messages, mais plutôt de partitions de fichiers distribuées sur plusieurs serveurs. Il est pourtant assez important de comprendre ces concepts sinon on pourrait passer à côté de certains messages !
Faire moins mais le faire bien
Il existe un certain nombre de fonctionnalités courantes dans les MOM qu’on ne retrouvera pas dans Kafka. Quelques-unes d’entre elles:
- pas d’acquittement : il n’y a pas de certitude que les clients ont tous consommé chaque message (voir le prochain chapitre);
- pas de sélecteur comme en JMS, un client s’abonne juste à un sujet donné. En effet, on ne retrouve que des informations techniques dans le header du message, comme sa taille ou le CRC. Ce qui fournit peu d’éléments pour filtrer tel ou tel message;
- pas de request/reply : comme le point précédent, le message ne contient pas d’identifiant de corrélation.
Cette minimisation des fonctionnalités est le prix à payer pour favoriser avant tout le débit des messages. Dans un MOM “classique”, pour s’assurer que tous les consommateurs d’un topic ou d’une queue ont bien récupéré leurs messages suivant les différents scénarios possibles (auto acknowledgement, client acknowledgement, transactions…), les serveurs auront besoin de maintenir des états complexes. A cela, on peut ajouter la persistance des messages ou la contrainte d’au plus un envoi et on imagine assez bien où les serveurs peuvent perdre du temps.
Kafka prend les choses beaucoup plus simplement. Le serveur écrit le message sur une de ses partitions et ensuite le consommateur retrouve cette partition pour y lire le message. Par ce design assez simple en apparence, Kafka résout facilement les problématiques d’ordonnancement des messages.
Consommer intelligemment
Comme nous l’avons vu précédemment, Kafka ne possède pas de notion d’acquittement. Ce concept est remplacé par celui d’offset. Cet offset représente le high-water mark pour un consommateur, c’est-à-dire l’emplacement du dernier message lu dans le fichier de log. Cet offset est géré par le client lui-même, libre à lui de le déplacer manuellement.
Encore une fois Kafka transforme une idée simple en un outil puissant. En effet la gestion manuelle de l’offset permet de reparcourir autant de fois qu’on veut une liste de messages. On imagine assez bien l’utilité dans le cas du crash d’un client par exemple, mais on pourrait aussi imaginer qu’un message déclenche un retraitement des n derniers messages pour les envoyer vers une file de messages d’audit.
Étonnamment, la gestion des transactions côté client devient également assez légère. Par exemple si, à la réception d’un message, le client doit persister des informations dans une base de donnée, à l’intérieur d’une transaction, l’offset peut également être persisté en base dans cette même transaction. Par conséquent, en cas de rollback de la transaction, le client récupérera l’ancien offset et pourra tenter de reconsommer le même message.
Alors, on se lance ?
Est-ce que ça s’adresse à mon projet ?
Est-ce que Kafka peut répondre à vos besoins ? Pour orienter votre choix, nous voyons déjà trois idées fortes qui sous-tendent cet outil :
- les messages sont de la donnée, ce que Kafka appelle l’activity stream data. Dans un MOM classique le message n’a qu’un état provisoire; une fois consommé et acquitté il a pour vocation de disparaître. Dans Kafka, le message reste, peut être relu et renvoyé. Si on pousse le concept jusqu’au bout, il n’est plus réellement nécessaire de stocker ces informations ailleurs (dans des archives de logs ou une base de monitoring par exemple), Kafka peut jouer le rôle de _datasource_;
- une donnée doit pouvoir être consommée immédiatement (alerte de sécurité par exemple) et aussi analysée à froid dans des outils de Business Intelligence sous forme de batch. Avec ce schéma, il n’est plus nécessaire de dupliquer les workflows;
- le volume de données ne doit pas être un problème, je dois pouvoir scaler horizontalement en toute transparence.
Si notre activité contient beaucoup de petites mises à jour, des notifications entre utilisateurs ou des scénarios d’utilisation qu’il est important de savoir rejouer, Kafka est une bonne solution. Il n’est pas étonnant que, outre LinkedIn, les premiers utilisateurs, Tagged et Mate1, soient aussi des réseaux sociaux.
On pourrait aussi imaginer une modélisation pour gérer les flux financiers : d’un côté la mise à jour permanente du cours de la bourse est récupérée par le front office, de l’autre les outils d’analyse de risque, de calcul de PnL (profits et pertes) et d’audit peuvent être déroulés à froid.
La peinture est-elle sèche ?
Lors de nos journées d’échange à Xebia, nous avons pu expérimenter les fonctionnalités de Kafka. La compréhension du workflow a été la difficulté la plus souvent rencontrée. On met du temps à comprendre les concepts et on a la sensation que nos consommateurs relisent les mêmes messages ou au contraire qu’on en perd. La courbe d’apprentissage sera sûrement plus longue que pour utiliser JMS ou AMQP.
Par ailleurs, l’API java proposée n’est pour l’instant pas très riche même si elle permet déjà de faire l’essentiel. Il y a par exemple des méthodes permettant de reparcourir les partitions mais elles sont d’assez bas niveau, on aurait aimé des fonctions de recherches plus poussées. Nous n’avons pas testé l’API Scala (Kafka est écrit en Scala !) mais elle semble plus riche de ce point de vue. Néanmoins il ne s’agit que de la version 0.6 et la roadmap de Kafka contient déjà pas mal d’améliorations qui vont dans le bon sens. Par exemple, l’arrivée de la compression et de la réplication devrait contribuer à la maturité de la solution.
Conclusion
Malgré sa jeunesse, Kafka est sans aucun doute un outil à surveiller. Son passage en projet Apache, s’il passe la phase d’incubation, devrait apporter une meilleure assise en l’ouvrant à de nouveaux cas d’utilisation et en le décloisonnant de l’environnement de LinkedIn. De plus certains projets seront sûrement intéressés par le support d’Hadoop, utilisé également pour leurs besoins en interne.
La complexité de l’architecture peut par contre être un gros frein surtout si le dialogue est difficile avec les équipes opérationnelles. Car même si Zookeeper permet de lier automatiquement les brokers aux consommateurs, il semble préférable que ces derniers connaissent un minimum l’architecture pour certains modes de consommation. De même, pour des questions de performances ou de sécurité, les opérationnels auront peut-être besoin de rapprocher tel serveur à tel producteur ou consommateur de message. Il s’agit d’un vrai défi pour le DevOps.
Néanmoins Kafka apporte un regard nouveau sur un vieux sujet, les MOM, en proposant des solutions radicalement différentes de ce qu’on trouve ailleurs. Peut-être germera-t-il de nouvelles idées pour de nouvelles solutions de messaging ?
Commentaire