Comment tester du Kafka via des TU.
Une des nombreuses raisons pour lesquelles je préfère éviter l'écosystème Spring Boot le plus possible. Tout étant basé sur le scanne du classpath, rien n'est vérifiable au build et tous les imports sont validés/calculés au runtime.
Bref, avant Spring et Spring Boot nous avions un environnement Java où tout était vérifié à la compilation, fortement typé statiquement et à présent nous avons une sorte de clone verbeux de JavaScript avec des temps de build (ce que JavaScript n'a pas puisque interprété) et des temps de démarrage hyper-longs (ce que JavaScript n'a pas puisque interprété).
Bref si c'était pour transformer Java en JavaScript merci mais non merci.
Sinon Kotlin + Jooby + Ktorm sur JVM ça remplace totalement Java + Spring Boot + Hibernate, c'est plus simple (pas de conflits internes de versions, pas de mécaniques implicites obscurs à connaître), ça prend littéralement 10 fois moins d'espace disque pour faire la même chose et c'est plus rapide (démarrages instantanés, tient plus de 100 fois mieux la charge d'après techempower).
J'avais déjà expliqué mon désamour pour Kafka ici mais comme toujours, le client est roi et des architectes poussent cet outil même si leurs besoins en terme de charge et de volume ne sont pas suffisamment élevés pour justifier Kafka.
Ici, un bon article expliquant les stratégies de partitionnement des topics Kafka. Merci à son auteur, ça m'aide.
P.S : au regard de ma critique vive de Kafka, cet outil possède un avantage qui est extrêmement intéressant : le découplage temporel, c'est-à-dire qu'en forçant l'asynchronisme des messages, Kafka retire du SI les problèmes d'indisponibilités des services appelés, et ça c'est une très bonne chose (que l'on peut toujours compenser par un cluster de micro-services + Kubernetes mais bon).
Je vais rentrer directement dans le vif du sujet en prenant argument par argument et en répondant à chaque fois. Puis j'apporterai les miens à la fin.
1) Les bus applicatifs permettraient de coder en évènementiel.
Oui mais ça ne veut pas dire que c'est la seule technologie permettant de le faire. Vous en connaissez sûrement une autre : les requêtes AJAX sur HTTP. Vous savez ce truc à la base de tous les internets "modernes" sorti en 1996.
Par essence une requête AJAX est asynchrone et c'est lorsque le serveur notifie le client qu'il a traité sa demande (ce qui est la définition même d'un évènement) qu'une fonction de call-back est invoquée.
2) Passer par un bus applicatif réduirait le couplage entre les services.
En général on me sort cet argument face à RESTful ce à quoi je réponds : le degré de couplage est le même.
Effectivement en RESTful ce qui compte c'est le chemin utilisé et un fichier JSON qui contient les données. Il ne s'agit pas là d'invoquer une méthode via RMI, ni d'employer la même technologie côté client et côté serveur, il s'agit d'envoyer un fichier texte contenant les bonnes informations sur une URL. Wouah mais quel couplage ! #Unbelievable
Nous sommes déjà au degré utile et essentiel de découplage. Commencer à vouloir s'abstraire de l'URL est un non-sens d'autant qu'elle se retrouvera quand même dans les messages des bus comme Kafka mais sous une autre forme : le type du message.
Je m'explique : pour qu'un message soit dépilé par le bon service, il faut qu'il soit typé (ie. je contiens tel contenu et j'incarne telle demande), or le typage d'une demande en RESTful c'est ni plus ni moins que l'URL. Avec les bus applicatifs comme Kafka, nous représentons ce typage sous la forme d'une entrée de type clef-valeur dans un header mais cela revient à la même chose que de contacter une URL. C'est juste moins facile à relire car les protocoles sont binaires et non textuels (cf. KafkaMessage) donc ça paraît "plus découplé" mais ça ne l'est ni plus ni moins.
3) Les bus applicatifs permettraient de gérer des queues de messages.
En réalité cet argument n'en est pas un. Nous avons ajouté de la puissance de calcul dans le bus dont le rôle sera d'encaisser les pics de charge ; or pourquoi ne pas affecter cette puissance de calcul (en générale inhumaine lorsque nous parlons d'un cluster Kafka) directement dans les µ-services ? (ndr. qui sont eux aussi un anti-pattern d'architecture)
Dit autrement, nous préférons affecter de la puissance de calcul à des unités de stockage de messages, rajoutant ainsi des latences et augmentant les coûts d'infrastructure plutôt qu'aux unités de traitement qui fournissent elles des calculs utiles.
Je suis favorable aux queues lorsqu'elles sont à l'intérieur d'une application, car elles aident à mettre en place des algorithmes de calculs mutli-threads et parallèles pour ainsi tirer parti de 100% des hardwares mutli-cpus / multi-cores.
Au niveau inter-applicatif c'est une très mauvaise idée car cela fait reposer nos SI sur l'élément le moins fiable du monde : le réseau.
4) Les bus applicatifs seraient plus sécurisés en forçant une rupture protocolaire.
L'argument du changement de protocole dépend de comment fonctionne le bus et de son implémentation. Par exemple les bus qui travaillent en SOAP sur HTTP fonctionnent sur HTTP... Comme nos requêtes REST en somme... #ChangementDeProtocoleSur20
Ensuite, la sécurité ce n'est pas qu'une simple histoire de protocole, c'est toute l'architecture qui doit être pensée pour être résistante aux attaques. Cet argument laisse accroire que la sécurité est une chose facile et que nous aurions magiquement augmenté le niveau juste par la présence d'un sacro-saint bus, ce qui est faux.
Enfin, en quoi un changement de protocole sécurise quoi que ce soit ? Si j'envoie un message via un bus, quoi qu'il arrive ce dernier sera traduit dans un format interprétable par l'application, si je peux effectuer une injection parce que l'application tolère les injections, je pourrai quand même attaquer le système.
Souvent, on me rétorque à cet argument que le bus peut faire des contrôles anti-injections par exemple, mais ce sont les mêmes personnes qui me disent de l'autre côté qu'un bon bus reste au niveau du réseau et qu'il doit être neutre au niveau application : décide-toi camarade.
Pour moi ce qui transporte un message ou une requête ne doit transporter que le message ou la requête et ne rien faire d'autre. Quand j'envoie ou que je reçois une lettre par la poste je ne veux pas que la poste ouvre ma lettre et en modifie le contenu "pour me protéger". #Censure Pour mes applications c'est pareil, je décide seule de ce qui est une attaque et de ce qui est une demande, pas le bus, donc pas de sécurité à son niveau. #SegragationOfConcerns
5) Le monitoring réseau est plus facile avec un bus.
Le bus est un endroit centralisé par lequel tout transite, donc enregistrer qui s'adresse à qui semble plus simple que faire la même chose dans un réseau maillé à la sauce internet où n'importe qui peut contacter n'importe qui d'autre.
Alors cela est vrai lorsque les bus ne sont faits que d'une seule instance... Or si une seule et unique machine peut encaisser toute la charge de vos messages, déjà je vous pose la question de l'intérêt d'avoir mis en place un bus pour un SI aussi peu complexe avec une volumétrie aussi faible mais admettons...
En général les bus applicatifs sont de très gros clusters avec des systèmes de réplication inter-instances... Les gens qui me parlent de monitoring vont me dire que "grâce au bus je sais qui envoie des messages au cluster de mon bus et qui reçoit des messages du cluster de mon bus". Ok... Mais que se passe-t-il à l'intérieur du cluster ? Quel trajet le message a-t-il effectué dans cette #BoiteNoire ?
En y regardant de plus prêt nous nous rendons compte qu'il existe une case à l'intérieur de laquelle nous n'analysons plus rien et nous décrétons que cette case n'est pas à superviser puisque c'est le bus lui-même ; ce qui revient à superviser un routeur.
Or ne regarder que ce qui est extérieur au bus se rapporte à monitorer la périphérie du SI, c'est-à-dire nos services et c'est déjà chose possible avec des logs produits par les services eux-mêmes + ELK pour un coût et une complexité d'infrastructure dix fois moindre.
Je sais que Kafka est plus intelligente et permet de se monitorer elle-même, mais cela ne répond pas à la question du nombre d'instances supplémentaires à gérer du fait de sa seule présence, ni même de l'expertise que cela requiert ou encore de la manière dont Kafka va contraindre la façon de coder les services.
6) Les bus me permettraient de changer un service sans impacter les autres.
L'idée sous-jacente est de dire que comme tout le monde parle au bus alors les services ne se connaissent pas entre-eux. Il devient donc possible de changer un service sans devoir patcher les autres.
En réalité ce coût de transformation est déporté non pas pendant la mise à jour du service mais au moment de la migration du bus A vers un autre bus B.
Sauf que l'économie de ces petits coûts traités au fil de l'eau se métamorphosera en supra-giga-facture lors de la big-bang transformation qui consistera à sortir totalement le SI du bus A. C'est évident, puisque tout le monde parlant au bus, aussitôt que l'on changera de bus, il faudra mettre à jour tout le monde avec la complexité et le risque que ce genre de "migration" impose. Vous le sentez l'anti-pattern du point de vue du CTO ou d'une DSI ?
Je préfère disposer d'une communication ad-hoc entre plusieurs services REST s'appuyant sur des Swagger faisant office de contrat d'interfaçage. Ainsi tant que tout le monde parle le bon JSON, ça marche point.
Pas de protocole particulier, pas de lib "kafka-client" à tirer dans mon application, pas de dépendances vers des tiers, juste des fichiers textes ayant une structure (CSV, JSON, XML, YAML, etc) et que j'envoie ou reçois sur le réseau ; avec des mises à jours petites, fréquentes et réalisées en continue pour faire virer notre paquebot de SI sereinement.
Je peux en plus choisir un format adapté qu'il soit plat (properties) ou arborescent (JSON) là ou Kafka m'impose du plat en clef-valeur. Cette contrainte rend juste votre vie difficile pour représenter une hiérarchie d'objets, heureusement ça n'arrive que tout le temps...
Il faut bien comprendre que lorsqu'un Swagger change pour ajouter un nouvel attribut par exemple, il faut alors changer tous les clients. Mais c'est également le cas avec des bus comme Kafka ! En effet, si un message nécessite une valeur supplémentaire, il faut que tous les clients émettant ce message intègrent cette nouvelle valeur. Mais alors où se trouve le découplage des bus par rapport à REST ? Où se trouvent el famoso "mise à jour sans impact" ? Réponse : nulle part, c'est un mythe.
Nous voici arrivés à la partie contre-arguments (qui va être courte car j'en ai marre d'écrire) :
Les bus sont des SPOF (Single Point Of Failure) mais pourquoi ?
Au niveau du réseau
Comme tout passe par le bus, alors si le bus tombe tout tombe. C'est la raison pour laquelle Kafka a émergé ; puisque si un nœud Kafka tombe, alors il en existe d'autres pour assurer le travail et l'engouement pour Kafka est à mon sens le témoin du fait que les bus mono-instances étaient encore plus des anti-patterns. Mais quid des difficultés d'administration de Kafka ?
Je rappelle qu'un service doit déclarer les adresses de tous les nœuds Kafka pour se connecter au cluster (au cas où le seul nœud déclaré aurait lâché). Je dois donc paramétrer mon application pour qu'elle ait une connaissance de l'infrastructure alors que celle-ci devrait être agnostique de cette infrastructure. Nous revenons en arrière de 20 ans (promis je ferai un poste expliquant en quoi Docker fait la même chose vis-à-vis du build lié à la sécurité).
Le nombre d'instances à administrer augmentent mais pas seulement... Le type d'instances aussi !
Et oui il faut administrer les instances du bus en plus des instances applicatives mais en plus il faut disposer d'admin-sys ayant des compétences sur les applications ET sur le bus !
Alors du point de vue du cabinet de conseil c'est merveilleux puisque je peux fourrer encore plus de prestas ! #Money Mais du point de vue du CTO ou de la DSI, c'est mon porte-feuille qui se fait mettre à mal.
Et là je ne parle pas que de compétences, arguons que chaque instance tourne sur du hardware en dernier recours et que ce hardware a un coût. Plus d'instances => Plus de hardware ou du hardware plus gros et donc plus cher ! C'est le second effet kiss-cool au porte-monnaie.
Mais il y en a un troisième !! Il faut que je recrute des développeurs qui maîtrisent le bus. Vous savez les développeurs Kafka par exemple... Sauf que compétences en plus => tarifs plus élevés. Et cela engendre des difficultés de recrutement d'une part et une sensibilité accrue au turner-over d'autre part surtout quand l'expert Kafka nous quitte...
Les schémas du SI intégrant un bus masquent l'horreur de la réalité.
Avant les bus applicatifs, les architectes fournissaient des schémas avec plein de boites et encore plus de flèches le tout partant dans tous les sens. C'était alors triviale de constater l'ampleur du grand merdier et donc de réclamer une simplification du SI aux architectes dont le travail était manifestement défaillant.
Avec un bus, nous nous retrouvons avec les boites applicatives en périphérie de ces schémas et une plus grosse au centre : celle du bus lui-même. Là le schéma est propre, harmonisé, au moyen une belle structure en étoile où chaque composant ne s'adresse qu'au bus... #PowerPointSur20
Soyons clairs, c'est le même bordel à l'intérieur de la grosse boi-boite qui représente le bus que ça ne l'était avant sans la présence du bus. Sauf que ce souk devient invisible quand on ne se rend pas sur le terrain, en observant les flux et le code... Mais ce que le CTO ne voit pas ne lui pose pas de problème n'est-ce pas ?
Illustration du problème AVANT
Illustration du problème APRES
Comment identifier l'endroit où la complexité est la plus dense ? Comment y affecter des personnes et mesurer sans effort l'ampleur des difficultés qu'elles vont affronter si tous ces schémas avec ces flèches dans tous les sens ont été effacés du regard ? Comment justifier les coûts budgétaires aussi élevés à chaque mise à jour alors que notre SI paraît si simple, si joli, si sophistiqué avec sa structure en étoile dans de beaux Power-Point animés ?
Si certains architectes aiment autant les bus, je pense que c'est parce qu'ils peuvent montrer à quel point ils brillent, leurs schémas devenant clairs, limpides, dignes des experts qu'ils sont au vu de leurs tarifs. Et toute la difficulté retombera alors sur le dos des développeurs, vous savez ces "tocards" payés à pas cher et importés de l'étranger... De parfaits coupables pour des experts influents et bien-pensants face à la certitude que les devs soient des incompétents.
Je résumerais ma position sur un principe : KISS (Keep It Simple, Stupid !). Cela vaut pour du code, cela vaut aussi pour le SI.