DevFest 2019 !

Le DevFest s’impose petit à petit comme la conférence technique majeure des Hauts-de-France. Organisé par le Google Developer Group (GDG) Lille, c’est une journée où se succèdent des talks sur différents thèmes : cloud, développement mobile, web, …

De notre côté, c’était une participation un peu plus active que d’habitude, car Salto Consulting était sponsor bronze de l’événement. Il s’agit d’une aide financière en échange d’une visiblité du logo de l’entreprise sur les visuels, de la com’ sur les réseaux sociaux et la possibilité de fournir un goodie dans le sac offert à chaque participant. Chose faite :

Lunettes Salto Consulting

Le Kinepolis de Lomme était le décor pour cette année. On peut y voir sans doute le signe que l’événement prend de l’ampleur et que le nombre de participants est en constante augmentation. En tout cas, il est vraiment très appréciable de suivre des sessions confortablement installé dans un fauteuil de cinéma. Et on peut imaginer la joie ressentie par le speaker lorsqu’on voit ses slides projetés sur un magnifique écran géant ! Jugez un peu :

Slides sur écran de cinéma

A 9h15 commence la keynote d’ouverture. Pierre Ziemniak nous raconte l’historique des séries françaises depuis la création de la télévision. On y découvre quelques pépites oubliées, notamment un extrait de la série « Commando Spatial », datant de 1967 et faisant référence à Star Trek. C’était une keynote sympa pour lancer la journée.

Voici le résumé de quelques conférences auxquelles nous avons pu assister.

 Des microservices aux migroservices

Beaucoup de monde dans la salle pour cette session par François Teychené. Quoi ? Un talk sur les microservices en 2019 ?
François revient dans un premier temps sur les inconvénients liés aux monolithes : livraisons lentes, scalabilité difficile, complexité d’évolution.
Les gains attendus des microservices : évoluer plus vite, faciliter la scalabilité. MAIS la complexité est toujours là notamment à cause des interactions entre les services sur le réseau… non en fait, c’est encore plus complexe qu’avant !

François nous donne alors la définition du migroservice : c’est quand on essaie de faire une architecture microservice sans avoir prévu l’outillage nécessaire. Additionné à la démarche DevOps, le nombre d’outils qui peuvent entrer en jeu est impressionnant : Traefik, Consul, Vault, Kubernetes, Kafka, RabbitMQ, AWS, GCP, Docker…

On revient sur la notion de bounded context et le fait qu’il faut définir correctement les limites des microservices ! Le découpage des services doit être métier et non technique, et surtout il doit suivre les cas d’utilisation (ventes, catalogue…) plutôt que les entités (client, produit). Tiens tiens, on a déjà évoqué ces sujets ici-même à propos de l’architecture modulaire 😉

Enfin François insiste sur l’importance d’automatiser toutes les tâches afférentes aux microservices (déploiement…). En bref une bonne conférence qui fait écho aux problématiques d’architecture distribuée que nous rencontrons tous les jours.

 Kubernetes – Pimp my laptop

Sébastien Nahelou est aux commandes pour ce talk visant à améliorer la productivité à propos de l’utilisation de Kubernetes. Ce n’était clairement pas un sujet pour débutants : nous avons parcouru beaucoup de choses en 45 minutes. Pour commencer, voici en vrac les outils évoqués :

  • kubectl-aliases : série d’alias pour la ligne de commande
  • kube-PS1 pour bash et zsh : permet d’afficher le cluster et namespace courants dans le terminal
  • kubectx et kubens pour switcher rapidement de cluster et de namespace
  • k9s : interface dans le terminal pour manager les clusters Kubernetes
  • tubekit facilite certaines tâches courantes, comme récupérer le nom d’un pod sans devoir « copier-coller »
  • kube-score analyse le yaml et donne des indications de bonnes pratiques
  • kubesec effectue des vérifications de sécurité des ressources d’un cluster kubernetes
  • kubetail aggrège et affiche les logs de plusieurs pods simultanément
  • helm-unittest : sorte de tests unitaires sur les charts Helm
  • kubectl explain (commande standard) permet de récupérer la documentation et le format du yaml dans le terminal

Au niveau des bonnes pratiques quand on écrit la configuration Kubernetes, Sébastien nous encourage à utliser les options kubectl –dry-run -o yaml : en effet cela permet de récupérer un ficher yaml propre sans devoir l’écrire à la main… beaucoup sont allergiques à ce format et c’est vraiment une astuce à suivre !

Sébastien nous fait ensuite une démo en mettant en application les astuces données.
On découvre alors l’outil Telepresence. Celui-ci permet de substituer un service déployé sur un cluster distant par un service local… en d’autres termes, le poste de développeur peut prendre la place d’un élément du cluster déployé afin de faciliter le développement et le débug. ça a l’air hyper utile et aurait mérité davantage de temps.

Enfin (!) Squash permet de débugger dans son IDE une application déployée sur Kubernetes. Ici aussi l’outil fait forte impression.

Vous l’aurez compris avec ce résumé, c’était un talk très rapide avec énormément de tips & tricks concrets autour de Kubernetes. Il fallait suivre mais c’était très enrichissant.

Cela montre également que l’écosystème autour de Kubernetes n’est pas très mature. On a besoin de beaucoup d’outils pour pouvoir travailler efficacement.

 Doom, gloom or loom

Rémi Forax était là pour nous parler du projet Loom ! Intéressant d’écouter un contributeur à différents projets autour du JDK (et accessoirement Java champion). On commence par les différences entre du code impératif et réactif avec des exemples en Spring. Le souci du code réactif étant la difficulté à débugger le code, les traces étant illisibles.

Le projet Loom introduit l’objet Continuation qui permet des arrêts / redémarrages dans du code impératif. Plusieurs Continuation peuvent tourner au sein d’un même thread mais pas en parallèle.

Rémi nous montre l’utilisation du mot clé async en Kotlin et ses limites, notamment par le fait qu’on doit déclarer explicitement les fonctions qui peuvent être bloquées avec le mot clé suspend. Et cela a tendance à créer deux mondes distincts. Rémi fait le parallèle avec les exceptions en Java, où les exceptions de type checked sont délaissées car contraignantes.

Autre objet introduit par loom : Fiber. C’est un objet similaire au Thread. La différence majeure est que quand il est suspendu, l’état du fiber est enregistré dans la JVM, et peut être repris plus tard. Par rapport aux threads traditionnels, on réserve beaucoup moins de mémoire car seules les choses utilisées sont stockées dans le heap quand l’exécution est suspendue.

Le projet Loom n est pas encore finalisé et il sera officiellement dans Java en… on ne sait pas !

Un talk très sympa, bien touffu pour ne pas s’endormir après la pause déjeuner 🙂

Il faut sauver le soldat Jenkins

Nicolas De Loof nous parle de Jenkins X. Ce n’est pas « juste » un jenkins sur kubernetes. Ce n’est pas non plus un jenkins amélioré ou visant à remplacer l’original.

Jenkins X is not...

Jenkins X est un outil opinionated ce qui signifie qu’il fait des choses automatiquement quand on fait les choses « comme tout le monde ».

Explication du Terme GitOps : Git est la seule source de vérité, que ce soit pour le code, l’infrastructure ou le déploiement. On va par exemple avoir un repository pour versionner les templates de déploiements Helm pour Kubernetes, les push sur ce repository sont analysés par un pipeline jenkins X et sont exécutés.

Nicolas nous fait ensuite une petite démo. Jenkins X s’installe sur un cluster Kubernetes. Pour sa démo il utilise l’outil en ligne de commande jx.

jx create quickstart : initialise un projet « hello-world » dans le langage/framework de notre choix. Jenkins X utilise ensuite le mécanisme de buildpack : par rapport à notre stack technique, l’ensemble des fichiers nécessaires à un déploiement par Docker sur Kubernetes sont initialisés (Dockerfile, templates helm…)
Quand on soumet une Pull Request sur GitHub, l’outil déploie automatiquement la version correspondante de l’application.

Contrairement à un Jenkins traditionnel, Jenkins X est plutôt fait pour être administré par la ligne de commande. On comprend avec la démo que Jenkins X est en effet très orienté : il permet d’aller très vite dans un cadre précis, mais on se pose la question de son utilisation si on veut déployer d’une façon spécifique.

A voir, je pense qu’il faut vraiment l’essayer pour se faire une idée. Il peut sans doute faire gagner beaucoup de temps à condition de ne pas faire des choses exotiques.

 Micronaut, le framework JVM ultra-light du futur

Olivier Revial nous présente ce framework que l’on a déjà évoqué sur notre blog.
Micronaut a pour but de prendre le « bon » des frameworks existants, tels que Spring, tout en s’afranchissant des contraintes de lourdeur.

Olivier nous affiche une démo de la création d’une application Micronaut avec la ligne de commande. Avec une configuration très légère on peut facilement interagir avec une base MongoDb en mode réactif, exposer un endpoint HTTP. Les métriques sont exposées simplement via la configuration.

Enfin on évoque GraalVM pour compiler des images en mode natif. La compilation est beaucoup plus longue, plusieurs minutes, mais l’application démarre en 30ms!

Olivier conclut sur le faut que Micronaut est un framework jeune, mais dont l’adoption est croissante. La faible consommation mémoire peut aider à aller vers le serverless.

De notre côté, on aime beaucoup ce framework. Mais entre les améliorations constantes apportées à Spring notamment sur les performances, et Quarkus qui est supporté par RedHat et va plus loin dans sur le concept « Java natif », il a du travail pour conserver une place de choix dans cet écosystème.

 De Java à un exécutable natif : GraalVM et Quarkus changent la donne

Emmanuel Bernard et Guillaume Smet sont sur scène pour nous parler du gros buzz du moment dans le monde Java : Quarkus. La philosophie de Quarkus est de cibler les applications cloud native, déployées sur le cloud.

On rentre tout de suite dans le vif du sujet avec une application web. Emmanuel lance l’application en mode dev qui permet de rafraîchir le code à chaud, sans devoir redémarrer. Ce mode apporte une vraie plus-value car c’est super rapide, on ne se rend pas compte du rechargement en arrière-plan.

On voit ensuite le système d’extensions qui permet d’ajouter des composants à notre application (connexion à la base, parser JSON…) via la ligne de commande.

Sur les développements d’API HTTP, l’interface swagger-ui est activée par défaut en mode développement avec l’extension OpenAPI, très pratique pour tester rapidement son API HTTP.

Au niveau de la Developer Experience, on est assez proche d’un Spring Boot mais plus conforme aux standards JEE (par exemple, notons l’utilisation de Jax-RS pour définir les routes HTTP plutôt que des annotations spécifiques).

Emmanuel fait un apparté sur Java et les containers : Java date des années 1990 où les programmes étaient généralement lancés sur un seul serveur, dans un seul process. Avec l’explosion du cloud, les ressources consommées par les applications deviennent critiques.
Attention à la confusion souvent faite : en Java, la RAM consommée n’est pas du tout équivalente à la taille max du heap (la fameuse option -Xmx). En réalité le resident set size (RSS) est très élevé sur les applications JEE/Spring y compris quand le heap est petit.

RSS size in Java

Du coup, l’accent est mis sur la consommation mémoire et le temps de démarrage de Quarkus en mode « natif », avec GraalVM. Les chiffres donnés sont plutôt impressionnants !

Guillaume évoque alors plus en détail GraalVM et son fonctionnement. La compilation en mode « natif » élimine les classes non utilisées. Comme on utilise très peu du code embarqué par les librairies, cela permet d’économiser énormément de mémoire.

La démo est assez impressionnante. Nous sommes de notre côté déjà convaincus que Quarkus est un outil incroyable et tout développeur Java devrait s’y intéresser.

Comment la GCP m’a permis de sauver ma prod chez Mamie

Sylvain Nieuwlandt commence par nous démontrer comment basculer son infrastructure vers GCP pour… gagner de l’argent.

Ensuite on voit comment débugger en live une application déployée sur GCP grâce à Stackdriver. Avec quelques modifications dans les fichiers de propriétés, et avec les fichiers de code déployés sur GCP, il est possible par exemple d’injecter du log à chaud en production.

Suit ensuite une démo de l’application modbile « Cloud console ». C’est une application Android/iOS qui permet de se connecter à la plateforme GCP. Bien sûr on n’a pas accès à tous les services comme sur l’interface web, mais un shell est disponible. Ici Sylvain nous montre comment redéployer des machines virtuelles en production depuis son téléphone.

Cette conférence était assez fun et enrichissante sur certaines possibilités de Stackdriver notamment.

 En conclusion

Le DevFest devient un événement incontournable et ce n’est pas par hasard. L’organisation est très bien huilée, et malgré le nombre grandissant de participants cela reste d’une grande fluidité. Voici une photo de l’espace principal au cours de la journée pour rendre compte de la foule (photo de Damien Cavaillès) :

du monde au devfest...

Du côté des conférences, c’est très varié et de qualité. Le fun est également présent avec la keynote de clôture à la sauce Burger Quizz.
Un grand bravo aux organisateurs pour leur travail !

On reviendra avec plaisir en 2020 !

Architecture modulaire, microservices : on en est où ?

Les principes de l’architecture modulaire ne sont pas vraiment nouveaux. Par contre, l’implémentation de cette architecture dans les SI devient monnaie courante. En effet on voit aujourd’hui les fameux microservices fleurir un peu partout. Ce qui était un buzzword il y a quelques années est devenu la norme sur beaucoup de nouveaux développements. Ce billet dresse un constat sur les pratiques rencontrées dans les équipes, du côté métier et du côté technique.

Adoption des microservices

Cet article m’a été inspiré au départ par un sondage réalisé par O’Reilly en Juillet 2018 : The State of Microservices Maturity. Le sondage porte sur des entreprises ayant déjà implémenté des microservices dans leurs développements. Le rapport rend compte à la fois des aspects techniques liés aux microservices, du succès des projets, ainsi que des contraintes pour le métier. On y apprend notamment que pour les entreprises interrogées :

  • Les architectures microservices sont largement adoptées sur les nouveaux projets, ainsi que les technologies nécessaires : conteneurs, intégration continue, tests automatisés…
  • Les microservices sont considérés en majorité comme des succès
  • Le découpage des microservices selon le périmètre métier n’est pas fait correctement.

Il est assez étonnant de constater qu’un élément primordial, la gestion des bounded contexts, n’est pas abouti alors même que l’adoption est massive et couronnée de succès !

 DDD, Bounded context ?

La notion de bounded context (contexte délimité ?) vient du Domain-Driven Design (DDD).

Il s’agit de définir clairement les contours d’un domaine métier. Un domaine important sera séparé en différents sous-domaines. Ils peuvent parfois interagir les uns avec les autres, permettant l’aboutissement de processus métier impliquant plusieurs domaines.

Appliqué aux microservices, il s’agit « simplement » de spécifier un périmètre métier cohérent et unifié pour chaque service : pose de la problématique métier, objets et vocabulaire, comportements, interactions. Ce travail permet de mettre du sens sur les mots : un terme générique comme « client » peut avoir une signification très différente selon le contexte. Voici un exemple simpliste de 3 bounded contexts pour illustrer le propos :

Ce travail est fondamental : la conception des microservices découle des bounded contexts. C’est ce qui garantit que les services conçus sont autonomes et indépendants. Si les contextes définis sont trop fins et trop couplés, il faudra livrer régulièrement plusieurs services en même temps, ce qui est très coûteux. On parle alors de nanoservices.

Le métier d’abord !

On l’aura compris, le point de départ d’une architecture microservices est un travail métier et non technique ! On doit avoir une isolation fonctionnelle claire pour chaque composant. Il semble que cet élément est souvent trop peu travaillé.

Il est nécessaire de faire des ateliers impliquant des personnes de différentes équipes, des experts métier et des développeurs. Il faut installer de nouvelles pratiques comme par exemple les ateliers d’event storming.

N’oublions pas que ce qui tourne en production est le code écrit par les développeurs. Le code doit donc correspondre au maximum avec le métier que l’on cherche à aider. En bref, les concepts du DDD (Domain-Driven Design) devraient être adoptés plus massivement.

 Cohérence oui, Dépendance non

Un des gros risques en modélisant de nouveaux systèmes modulaires, est la tentation de grouper les services dans une roadmap globale et de faire dépendre fortement les services entre eux. Certes, il y a forcément des interactions entre les composants. Mais chaque équipe doit pouvoir avancer sans dépendre du travail des autres. Il faut préserver l’autonomie de chaque composant et des équipes associées. Pour cela on pourra :

  • Concevoir chaque service comme pouvant fonctionner en mode seul au monde (le cas nominal peut être rendu sans dépendance externe)
  • Utiliser des mocks (bouchons) pour les services externes non développés.
  • Limiter les échanges de données en prenant en compte le contexte. Par exemple, un client dans le service panier n’a probablement pas besoin de tous les champs du référentiel.
  • Eviter l’utilisation de librairies partagées.

La cohérence de l’ensemble est garantie par le respect des interfaces d’entrée/sortie de chaque service.

Alternative aux microservices : le monolithe modulaire

Une piste assez peu explorée est celle du monolithe modulaire. Mais c’est quoi exactement ? Pour produire un monolithe modulaire, il faut isoler les domaines métier comme on le ferait pour des microservices. Les deux différences notables sont que tout est dans la même base de code, et qu’on a une seule unité de déploiement. Mais la tâche est bien plus ardue qu’il n’y paraît. Pour implémenter un monolithe modulaire, il faut appliquer les principes suivants :

  • Au préalable, délimiter les domaines métier « sur le papier » (bounded contexts)
  • Délimiter les domaines métier dans le code : utiliser l’approche package by feature. Tout le code lié à un domaine est isolé.
  • Définir pour chaque domaine des interfaces publiques pour interagir avec les autres : API (au sens interface, pas HTTP)
  • Interdire les dépendances entre les domaines. On interagit uniquement avec les interfaces publiques ; on ne mélange pas les objets de différents domaines.

Au niveau des communications entre services, on n’a plus besoin de la couche réseau/protocole. Celle-ci est nécessaire dans une architecture microservices afin de transporter les données entre les services, sur le réseau. Dans le monolithe, les services communiquent directement entre eux, par exemple au sein de la même machine virtuelle Java. Cela simplifie grandement l’implémentation des interactions. En voici une illustration : monolithe modulaire versus microservices

C’est une approche très intéressante mais qui demande une grande rigueur de la part de l’équipe de développement. Voici le retour d’expérience de Shopify sur le sujet.

 Point de vue technique

 Communication entre composants : HTTP m’a tuer

L’API REST HTTP est devenue la star des systèmes d’information. Les approches API first et open API sont aujourd’hui largement répandues. A tel point que de nombreuses personnes utilisent les termes « microservices » et « API » indifféremment pour parler de la même chose.

Or, le protocole HTTP ne devrait pas être le moyen de communication par défaut entre les composants d’une architecture modulaire. Les API HTTP sont un bon moyen d’exposer des données ou des commandes vers l’extérieur. Cependant, quand il s’agit de faire communiquer des services entre eux au sein d’un process, c’est une technologie relativement peu adaptée. Le protocole HTTP est synchrone et point-à-point ; il introduit un couplage fort entre les composants. Il nécessite des éléments techniques supplémentaires : API management, service discovery, gestion des authentifications… Enfin, quand on cumule des services trop couplés, et une communication omniprésente par HTTP : on arrive au syndrôme du plat de spaghetti !

ça a l’air bon, mais pas pour mon architecture !

Cela arrive quand un geste métier, comme un clic sur un site web, a pour conséquence de nombreux appels HTTP envoyés vers différents composants. Mécaniquement, la disponibilité de l’application diminue. Il faut intégrer des mécaniques de retry, circuit breaker

Dans la mesure du possible, on privilégiera une approche événementielle pour implémenter les process : l’architecture orientée événements.

 Gestion des contrats d’événements

Dans une architecture modulaire, si on a opté pour une approche événementielle, il faut prendre un soin particulier sur les échanges de données. Chaque message émis doit avoir un contrat clairement défini. Les contrats peuvent être spécifiés de différentes façons et sérialisés en différents formats. Par exemple, Apache AVRO permet une sérialisation binaire de taille très légère, et un versionning du contrat. Le JSON schema aura l’avantage de permettre la production d’échanges au format JSON, ce format étant omniprésent aujourd’hui.

Quelque soit l’outil choisi, on veillera à implémenter le principe de robustesse :

  • Le consommateur d’un message lit uniquement les données qui l’intéressent, et ignore ce qu’il ne connaît pas
  • tolerant reader : le consommateur doit accepter une donnée même si le format n’est pas celui attendu, tant qu’il est compréhensible
  • l’émetteur d’un message respecte toujours le contrat : pas de suppression de champ par exemple.

 Les développeurs et le dogmatisme

Quelques petites phrases entendues sur le terrain :

« C’est pas un microservice… il est trop gros ! »

Le terme « microservice » est mal choisi, à mon sens. Il sous-entend que le service doit être petit (voire très petit). En réalité, la taille de la base de code n’a pas d’importance. La seule chose essentielle est que le service ait une responsabilité métier unique, clairement définie. Ensuite, selon la complexité du domaine, on peut avoir des services développés avec très peu de lignes de code et d’autres beaucoup plus volumineux.

« Ah non ! Ton batch ne peut pas interroger la base directement : elle appartient au microservice machin… »

Certes, une des premières choses qu’on apprend sur le sujet est qu’un microservice dispose de sa propre base de données, et lui seul peut y accéder. Cependant, à l’intérieur d’un domaine, on fera parfois émerger un composant technique supplémentaire pour pouvoir le scaler de façon indépendante. Par exemple, un service de notification, ou un batch de traitements de données. Si les composants sont fonctionnellement dans le même domaine, et maintenus par la même équipe, il n’y a pas de contre-indication justifiée au fait qu’ils partagent la même base de données.

« On fait une nouvelle brique ? »

C’est le développeur qui trouve ça génial et qui veut faire un nouveau composant, pour la moindre nouvelle fonctionnalité… 😀

 Conclusion

L’architecture modulaire permet à des équipes différentes de travailler en autonomie sur des composants distincts. Même si la mode est aux microservices, le monolithe modulaire est une alternative à étudier quand on n’a pas des besoins particuliers de mise à l’échelle.

La plus grosse difficulté est de faire correspondre les problématiques métier avec l’implémentation concrète dans les développements. On peut s’appuyer sur les principes du domain-driven design pour améliorer cela. L’event-storming permet par exemple de définir les domaines métier, ainsi que d’aligner les développeurs avec les experts du métier. Il facilite également l’adoption des architectures orientées événements, et du coup l’indépendance des briques applicatives.

RSocket, le protocole réactif

rsocket logo

RSocket (pour Reactive Socket) est un nouveau protocole de communication. Il spécifie des façons d’échanger des messages au format binaire entre applications. C’est un protocole de niveau applicatif qui permet des communications correspondant aux besoins modernes : push de données, échanges bi-directionnels, reprise de connexion, asynchronisme…

Il est conçu pour être utilisé autant pour de la communication de serveur à serveur, que serveur à périphérique (smartphone, navigateur web etc.).

Le protocole est open-source. Créé au départ par Netflix, il est désormais supporté par Facebook, Pivotal et Netifi. Il doit être intégré prochainement dans le framework Spring (cf issue). La spécification du protocole est actuellement en version 0.2 mais la release est proche, cette version étant considérée comme une release candidate 1.0. Rentrons un peu dans le détail !

 Les points clé de RSocket

Reactive streams – le contrôle des flux

La conception de RSocket s’appuie sur le manifeste réactif et la spécification Reactive Streams. Il s’agit d’implémenter des systèmes asynchrones et non bloquants, mais pas uniquement.

Un des apports fondamentaux apporté par le dogme réactif est la backpressure. Ce paradigme permet à un consommateur de données dans un système, d’informer les autres applications qu’il est surchargé. Les producteurs de cette donnée doivent assimiler cela et ne pas surcharger le flux. Le consommateur peut également choisir de consommer les données au rythme qu’il veut. Le but est de produire un sytème résilient sans devoir implémenter des mécanismes complexes de type circuit breaker.

RSocket introduit ces éléments dans sa spécification, le rendant indiqué pour implémenter des applications réactives.

Autres caractéristiques

  • RSocket est indépendant du transport sous-jacent. Actuellement il peut fonctionner avec TCP, WebSockets, Aeron, ou HTTP/2. Typiquement on choisira TCP pour des échanges de serveur à serveur, et Websocket pour navigateur à serveur.
  • Reprise de connexion. Si une connexion est coupée entre les 2 participants, cela peut être problématique pour les communications de type « longue durée » (abonnement à un flux de données). Le protocole fournit les moyens de reprendre la discussion au même endroit dans une nouvelle connexion, grâce à une notion de position implicite.
  • Rsocket est un protocole de haut niveau. Le but recherché par les créateurs est de fournir des impémentations directement utilisables au sein des applications. Ces librairies sont disponibles dans différents langages de programmation.
  • Les échanges sont au format binaire, afin de maximiser les performances et d’optimiser les resources. Cela peut rendre plus difficile le debug des messages. Cependant, c’est totalement cohérent quand on pense que l’immense majorité des échanges se font entre 2 machines et ne sont pas lus par un humain. Les applications devront donc implémenter la sérialisation et désérialisation de leur format natif vers du binaire.

 Modes d’interaction

La base du protocole tient dans les différents modes d’interaction proposés. RSocket fournit 4 modes distincts :

  1. Fire-and-Forget : requête unique, pas de réponse envoyée par le serveur
  2. Request/Response : « HTTP-like » : 1 requête, 1 réponse.
  3. Request/Stream : requête simple, réponse multiple. Le serveur renvoie une collection (stream) en réponse. Ce n’est pas une liste figée mais bien un flux qui arrive au fil de l’eau.
  4. Channel : échanges bi-directionnels. Les 2 participants s’envoient des flux de messages l’un à l’autre.

Ces modes d’interaction ont été pensés pour répondre aux besoins actuels des applications. Ainsi, le push de données est supporté par le mode request/stream. Cela permet par exemple, de gérer un flux d’informations à recevoir continuellement, sans avoir besoin de requêter plusieurs fois le serveur. Le mode fire-and-forget, avec sa requête unique sans réponse, permet d’optimiser dans des cas où la réponse peut être ignorée. Le mode channel implémente un dialogue complet entre deux composants.

Ces différents modes ainsi que les points clés listés ci-dessus sont le socle de RSocket.

 Les implémentations

A ce jour le protocole a des implémentations en Java, Javascript, C++ et Kotlin. Voyons un peu comment cela marche en Java dans la section suivante.

Exemples en java

L’implémentation en Java est basée sur la librairie Reactor. Au niveau du transport nous allons utiliser ici le transport TCP via le framework Netty. Les 2 dépendances suivantes sont suffisantes pour commencer à implémenter RSocket dans une application : io.rsocket:rsocket-core et io.rsocket:rsocket-transport-netty.

Démarrons un serveur en local :

RSocketFactory.receive()
    .acceptor((setup, socket) -> Mono.just(new AbstractRSocket() {})) // ne fait rien
    .transport(TcpServerTransport.create("localhost", 7000))
    .start()
    .subscribe();

Ce serveur ne va rien faire car on n’a pas spécifié de comportement concret sur la méthode acceptor. Il faut fournir une implémentation des interfaces SocketAcceptor et RSocket afin de déterminer ce que fait le serveur quand il reçoit un message. Il est intéressant de regarder l’interface RSocket pour constater qu’elle demande l’implémentation des 4 modes d’interaction évoqués plus haut :

public interface RSocket extends Availability, Closeable {
  // [...]
  Mono<Void> fireAndForget(Payload payload);
  Mono<Payload> requestResponse(Payload payload);
  Flux<Payload> requestStream(Payload payload);
  Flux<Payload> requestChannel(Publisher<Payload> payloads);
  // [...]
}

Prenons l’exemple d’un service de streaming de « news ». Lorsque le serveur reçoit une requête d’un client, il va envoyer un flux continu d’actualités qui se met à jour sans nouvelle requête. Nous allons devoir implémenter la méthode requestStream pour gérer cette interaction. La classe Payload est la classe qui représente un message binaire qui transite sur la connexion ; il faut donc effectuer les transformations entre les objets métier et ce type. Voici donc à quoi peut ressembler une implémentation du flux côté serveur :

SocketAcceptor socketAcceptor = (setup, sendingSocket) -> {
    return Mono.just(new AbstractRSocket() {
        @Override
        public Flux<Payload> requestStream(Payload payload) {
            return newsProducer.streamNews(payload) // service métier qui fournit le flux de données en fonction de la requête
                    .map(NewsMapper::toByte)        // sérialisation de l'objet métier
                    .map(DefaultPayload::create)    // creation du payload (methode fournie par l'implémentation rsocket-java)
                    ;
        }
    });
};

Sur le même exemple, créons la socket et utilisons là pour que le client puisse interroger le serveur et récupérer les news :

RSocket clientSocket = RSocketFactory.connect()
        .transport(TcpClientTransport.create("localhost", 7000))
        .start()
        .block();
        
clientSocket
    .requestStream(DefaultPayload.create("Donne moi les news s'il te plait"))
    .map(Payload::getData)            
    .map(NewsMapper::fromByteBuffer)  // désérialisation du message vers l'objet métier
    .doOnNext(newsConsumer::readNews) // appel du service métier de lecture des news reçues
    .doFinally(signalType -> clientSocket.dispose())
    .then()
    .block();

Ces quelques exemples démontrent que l’on peut utiliser RSocket dans une application Java très simplement. Cela nécessite en amont l’adoption de la programmation réactive.

Netifi proteus

Proteus est une plateforme basée sur RSocket. Elle fournit un broker auquel les applications vont se connecter, le broker se chargeant des échanges entre les applications. Il gère le routage entre les services, la sécurité, le load balancing. Une console web permet de visualiser et d’administrer la plateforme. Comme souvent, on dispose d’une version communautaire open-source avec les fonctionnalités de base, et la version enterprise contient des fonctionnalités avancées (connecteurs, métriques, alerting, …)

Les échanges sont encodés à l’aide de Protobuf. Ceci permet de spécifier les contrat d’échanges : mode d’interaction, types d’entrée/sortie, etc. Les interfaces client/serveur sont ensuite générées, et il suffit de les implémenter pour écrire notre logique métier. Proteus se charge de la sérialisation des objets et de la communication via RSocket.

Afin d’illustrer Proteus, voici quelques copies d’écran de la console web.

Gestion des brokers :
console web proteus

Statut des services connectés :
console web proteus

L’outil est intéressant mais semble encore un peu limité. Il est par exemple impossible d’envoyer un message à partir de la console, ce qui serait très pratique en développement. Je l’ai trouvé également assez lourd à démarrer via docker, sachant que c’est uniquement un « passe-plat » et qu’il ne stocke pas les messages. Les librairies sont pour l’instant disponibles pour Java, Javascript et Spring Framework mais d’autres devraient arriver prochainement. A suivre donc !

Pour conclure

Dans des systèmes de plus en plus distribués et découplés, les échanges de message asynchrones deviennent un standard de communication entre les applications. RSocket s’inscrit dans cette logique mais se démarque en apportant les principes réactifs au niveau du protocole de communication.

Supporté par des grandes entreprises du numérique, son adoption en sera peut-être facilitée. A suivre…

Découverte du framework Micronaut

Micronaut logo

 Un nouveau framework de développement d’applications JVM

Que dit le pitch ? « A modern, JVM-based, full-stack framework for building modular, easily testable microservice and serverless applications. » Hé oui, rien que ça. A priori ça sert donc à développer des applications et les exécuter sur une JVM, et que c’est orienté sur les microservices et le serverless. La version 1.0 a été releasée en Octobre dernier. C’est l’occasion de faire un essai !

 La genèse de Micronaut

Le constat fait par l’équipe de développement est que depuis quelques années, énormément de choses ont changé dans le développement des applications. Aujourd’hui, on fait des micro-services, sous forme de containers ; on veut pouvoir scaler facilement, exposer des API consommées par plusieurs types de périphériques, avec un niveau de performance élevé. Les anciens frameworks JVM tels que Spring ou Grails n’ont pas été construits avec ces problématiques.
Le but affiché par les créateurs est de créer un framework de développement léger, rapide à démarrer, peu consommateur de mémoire et modulaire ! Clairement, on veut concurrencer Spring Boot en proposant une expérience de développement similaire, mais en s’affranchissant de la lourdeur de ce dernier. L’équipe de développement est la même que celle derrière le framework Grails.

 Comment ça marche – Runtime vs Compile time

Un framework tel que Spring est apprécié par beaucoup de développeurs pour la productivité qu’il apporte, même si la courbe d’apprentissage est raide. Une fois pris en main, il est simple d’exposer et sécuriser une API, d’accéder à de multiples bases de données, d’émettre ou consommer des messages, etc. Le souci est que pour permettre tout cela, il fait énormément de choses au démarrage de l’application : scan du code, injection de dépendances, activation de profils… tout cela est fait au runtime. Cela a pour conséquence que plus un projet Spring est important, plus le temps de démarrage et la consommation mémoire sont élevés.

Micronaut prend le parti de tout faire à la compilation : il n’utilise pas la réflexion (introspection dynamique des classes). Il utilise la compilation ahead-of-time (AOT) afin de gérer tout cela lors du build. Cependant, le framework propose le même genre de facilités aux développeurs que Spring et permet donc une grande productivité. Il promet donc le meilleur des deux mondes !

On va développer quoi avec Micronaut ?

On peut l’utiliser pour construire des API REST, clients d’API, services de traitement de données, messaging, … Micronaut propose un modèle asynchrone et non bloquant et le rend donc indiqué pour développer des applications réactives. Pour cela la couche réseau est basée sur le serveur Netty qui apporte la gestion de l’event loop. C’est le serveur utilisé également par les frameworks Vert.x et Spring Webflux par exemple (version réactive de Spring MVC). Le framework supporte les langages Java, Kotlin et Groovy. Voici quelques fonctionnalités en vrac :

  • service discovery (Consul, Eureka, …)
  • gestion des paramètres de configuration externe (Consul, Amazon ParameterStore)
  • support du serverless (AWS Lambda, OpenFaas)
  • Interaction avec les bases mongoDB, Neo4j, Postgres, Cassandra, Redis…
  • Support d’Apache Kafka
  • Mécaniques intégrées de retry et circuit breaker

 Allez, jouons avec !

Micronaut s’installe très facilement grâce à SDKMAN !

$ sdk install micronaut

 Pour démarrer, le CLI

Micronaut met à disposition un CLI (Command-line interface) efficace. Pour démarrer un squelette d’application :

$ mn create-app com.mycompany.mygreatservice

Cela créée une structure, une classe de démarrage, un Dockerfile, la configuration des dépendances… Le langage est Java par défaut et les dépendances gérées par Gradle, mais il est possible d’utiliser maven avec l’option -b maven. Il y a un certain nombre de features qui permettent de pré-configurer une fonctionnalité en ajoutant les dépendances et le squelette de configuration nécessaire, par exemple :

$ mn create-app com.mycompany.mygreatservice --features discovery-consul, mongo-reactive

Malheureusement il n’est pas possible d’ajouter une feature une fois que le projet a été créé. Création d’un controller :

$ cd mygreatservice/ 
$ mn create-controller com.mycompany.cars 

Une classe com.mycompany.CarsController est créée avec une route /cars par défaut, ainsi que la classe de tests associée. Du coup, on peut tester :

$ ./gradlew run 
$ curl -i http://localhost:8080/cars

HTTP/1.1 200 OK
Date: Wed, 2 Jan 2019 23:50:42 GMT
connection: keep-alive
transfer-encoding: chunked

Regardons un peu le code généré ! Voici la classe de démarrage de l’application :

public class Application {

    public static void main(String[] args) {
        Micronaut.run(Application.class);
    }
    
}

Et le controller :

@Controller("/cars") 
public class CarsController {

    @Get("/")
    public HttpStatus index() {
        return HttpStatus.OK;
    }
    
}

On remarque déjà des similitudes avec le modèle de développement Spring. Il est à noter que le framework fait des choix modernes par défaut : par exemple, les routes consomment et produisent du JSON par défaut, pas besoin de préciser le content-type dans ce cas.

 Adoption du modèle réactif

Nous avons évoqué le fait que Micronaut utilise Netty pour la couche réseau. Pour garantir l’exécution en mode non bloquant, il faut utiliser les types fournis par une librairie implémentant la spécification Reactive Streams, comme par exemple Reactor ou RxJava.

Prenons l’exemple d’un endpoint classique de sauvegarde. On va écrire quelque chose comme :

@Post 
public HttpResponse<Customer> save(@Body Customer customer) { 
    return service.insertInDatabase(customer); 
}

Dans ce cas, Micronaut utilise un thread pool classique. L’exécution est bloquante : le code du service n’est exécuté que lorsque l’objet est reçu en totalité. La réponse n’est envoyée que lorsque le service a fini son exécution. Entre ces étapes, le thread courant est bloqué, en attente. Si on a une latence réseau importante les resources du serveur ne sont pas exploitées au mieux. Voici le même exemple en utilisant RxJava :

@Post 
public Single<Httpresponse <Customer>> save(@Body Single<Customer> customer) {
    return customer.map(c -> { 
       service.insertInDatabase(c); 
       return HttpResponse.created(c); 
     }); 
}

Ici la requête est non bloquante et le modèle event-loop de Netty est utilisé. Entre les différentes étapes, le thread est capable d’exécuter d’autres requêtes plutôt que d’être bloqué.

Si on veut être réactif de bout en bout, il est préférable d’utiliser également une librairie réactive quand il s’agit d’accéder aux données. Ce n’est pas toujours possible. JDBC est une API bloquante par exemple. Dans ce cas l’exécution des accès à la base de données sera basculée dans le thread pool « bloquant » et on reviendra en mode non bloquant ensuite.

 Accès aux données avec mongoDB réactif

Du coup on opte pour le driver réactif pour MongoDB. Pour l’activer, on ajoute la dépendance dans le fichier de dépendances, ici dans build.gradle :

compile "io.micronaut.configuration:micronaut-mongo-reactive"

Et il faut déclarer le chemin vers la base dans le fichier application.yml

mongodb:
  uri: mongodb://localhost:27017

Avec cette configuration, on dispose d’un objet MongoClient qui peut être utilisé pour interagir avec la base de données. Cet objet peut être injecté à la manière d’un bean Spring. On peut ensuite requêter dans la base de données de manière réactive :

private MongoCollection<Customer> getMongoCollection() {
    return mongoClient
            .getDatabase("my-great-database")
            .getCollection("myGreatCollection", Customer.class);
}

public Maybe<Customer> getByLogin(String login) {
    return Flowable.fromPublisher(
            getMongoCollection().find(Filters.eq("login", login)).limit(1))
                .firstElement();
}

Service discovery avec Consul

Dans un environnement micro-services il est très utile de pouvoir faire du service discovery : chaque service va s’enregistrer au sein de l’éco-système, ce qui permettra aux autres services de l’appeler sans connaître son adresse réelle. Consul permet de faire cela et son intégration est facile dans Micronaut. Il faut ici encore ajouter une dépendance dans le build.gradle :

compile "io.micronaut:micronaut-discovery-client"

Puis on ajoute quelques paramètres dans le fichier application.yml pour activer l’enregistrement du service.

consul:
  client:
    registration:
      enabled: true
    defaultZone: "localhost:8500"

On constate l’enregistrement automatique du service dans consul lorsqu’il est démarré : consul services

Il est alors possible de simplifier grandement l’appel à un service qui expose une API REST. Si on veut appeler un tel service dans une autre application, il suffit de déclarer une interface annotée @Client avec l’identifiant du service, et les méthodes correspondant aux endpoints de l’API.

@Client("customer")
public interface CustomerServiceClient {

    @Get("/customers/{login}")
    public Single<Customer> getByLogin(String login);
    
}

On peut ensuite appeler directement ce client dans notre code.

public class MySecondService {

    private final CustomerServiceClient customerClient;
    
    public MySecondService(CustomerServiceClient customerClient) {
        this.customerClient = customerClient;
    }

    public void callMyCustomerService(String login) {
        customerClient.getByLogin(login)
                .map(customer -> {
                    // do what you want here
                });
    }
}

 Et donc, les promesses de performance et de légéreté ?

Prenons un exemple d’application avec les features ci-dessus actives et développées : consul service discovery et MongoDB. Voir la taille du fat jar compilé :

$ du -h ./customer/build/libs/customer-0.1-all.jar 
14M ./customer/build/libs/customer-0.1-all.jar

L’exécutable fait une taille plutôt raisonnable de 14 Mo. Démarrons l’application pour constater le temps de démarrage :

$ java -jar ./customer/build/libs/customer-0.1-all.jar
12:02:55.340 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 1169ms. Server Running: http://localhost:8080
12:02:55.446 [nioEventLoopGroup-1-3] INFO  i.m.d.registration.AutoRegistration - Registered service [customer] with Consul

J’ai constaté un temps similaire sur plusieurs essais. Bien sûr ça n’a rien d’un benchmark dans les règles de l’art mais c’est plutôt encourageant.

Concernant l’empreinte mémoire, j’ai pu effectuer quelques tests. Sur une application de type serveur HTTP avec un seul endpoint, j’ai constaté une consommation mémoire d’environ 100Mo. Il est notable de constater que l’application fonctionne si on la lance avec une taille maximale de heap très petite (10Mo). Malheureusement c’est trop peu significatif sur une petite application et je n’ai pas eu d’effet « waouh ». Pour tirer des conclusions pertinentes et constater la plus-value réelle de Micronaut, il faudrait faire un benchmark complet sur une application plus proche d’une application de production, et comparer avec d’autres frameworks. Peut-être un sujet pour un prochain article 😉

Vous trouverez un exemple complet avec 2 services sur mon github !

 En conclusion

Micronaut est un nouveau framework JVM qui est vraiment prometteur. Il est simple à utiliser, rapide, la documentation est claire et concise. Le projet est très actif au niveau des contributions.

L’enjeu pour l’équipe de développement sera de garder le côté « micro » tout en augmentant le périmètre des fonctionnalités. Je pense qu’il peut se positionner comme une alternative crédible sur ce marché. En tout cas, je vais surveiller son évolution avec intérêt !

ADEO Dev Summit

Fin juin avait lieu Adeo Dev Summit, événement autour du développement organisé au sein du groupe Adeo. L’occasion de rassembler les personnes de l’IT des différentes entités du groupe au niveau mondial (France, Russie, Brésil, Grèce…) 4 jours durant lesquels se sont succédés conférences, codelabs et keynotes dédiés aux techniciens : on a parlé frameworks, architecture, déploiement, méthodologies, open/inner source…
On ne va pas faire ici un résumé complet, mais on a assisté à des talks de qualité que ce soit par des speakers « internes » Adeo ou du beau monde venu de l’extérieur (booking.com, github, google, confluent/kafka, traefik, …)., ainsi que des ateliers pour tous les goûts.
La keynote de fermeture nous a permis d’écouter Dirk-Willem van Gulik, un des fondateurs et ancien président de la fondation Apache, sans qui notre métier serait probablement assez différent aujourd’hui. Un grand moment.

De mon côté j’ai suivi un atelier sur le langage Go, vu une première approche pratique du serverless / Function-as-a-service, découvert les outils Skaffold et Bazel pour gérer le déploiement de briques hétérogènes dans Kubernetes, …
J’ai également participé activement : j’ai donné un talk sur les micro-services appliqués à la vraie vie, et j’ai animé des codelabs sur la programmation réactive dans Spring et les microservices en java avec RabbitMq et Spring cloud stream.
Le tout dans la langue de Shakespeare…
Cela a été une super expérience à tout point de vue. L’événement en lui-même, organisé en très peu de temps, a été une énorme réussite que ce soit au niveau de l’organisation, la qualité des intervenants, et la diversité des thèmes abordés.

Plus généralement cela m’a inspiré quelques réflexions sur le passé et le présent de notre métier…
Nous avons depuis longtemps la conviction

  • Que le développeur a quelque chose de différenciant, une plus-value à apporter dans les produits auxquels il participe.
  • Que l’on ne peut pas remplacer sans réfléchir un développeur A par un développeur B.
  • Que l’on devrait parler de personnes et non de ressources.
  • Que travailler main dans la main avec son client et son utilisateur est bien plus rentable sur le long terme que d’envoyer le développement se faire dans un centre de service à l’autre bout du monde.

La bonne nouvelle, c’est qu’aujourd’hui cette conviction est non seulement partagée, mais est partie intégrante de la culture des services informatiques des entreprises (que l’on évoque des entreprises très centrées sur leur IT ou non).
Et ce genre d’événement le démontre !
L’approche DevOps, les méthodologies Agile, le software craftsmanship, sont autant d’éléments qui rappellent que le développeur a aujourd’hui un rôle fondamental à jouer.

Il y a 10 ou 15 ans, le terme même de « développement » avait une certaine connotation négative.
Dans beaucoup d’esprits, c’était plutôt pour les juniors !
Dans cette vision, la voie normale de carrière était : obtention du diplôme > développeur (pas trop longtemps) > chef de projet > manager.
Bien sûr c’est un peu caricatural mais reflète une certaine réalité de l’époque.
Il semble que ce temps soit révolu.
Aujourd’hui le développeur ne doit pas uniquement coder :

  • Il participe aux réflexions avec les utilisateurs,
  • Il est responsable des tests, du déploiement sur les environnements distants, de l’intégration continue, du run.
  • Il explique régulièrement ce que fait le produit.
  • Il prouve que ça fonctionne.
  • Il partage ses pratiques avec les autres équipes de développement.

Bref, le technicien a gagné ses lettres de noblesse. A eux, à nous, de continuer à faire avancer les choses et les mentalités.
Le changement de mindset a opéré, enfin, le train est en marche.

En conclusion, un grand bravo à Adeo pour cette initiative ! Ce n’est que le début !

Alexandre Vandekerkhove

PS : Merci à Florian Petit pour la photo !