05 mai 2014

Small is beautiful

Faire des tests rapides, un build qui ne dépasse pas quelques minutes, ça reste un voeu pieux confronté à une appli de centaines de milliers (millions?) de lignes avec une architecture alambiquée. Je ne jette la pierre à personne, j'ai moi même largement contribué à rendre ces applications alambiquées à grand coup de Spring-truc et d'AOP.



Aujourd'hui, avec du recul et surtout en travaillant dans une équipe totalement distribuée (donc où il faut savoir limiter les besoins de synchronisation et  de communication), j'apprend les mérite d'une autre approche.

Vive les micro-services.

Le concept de micro-service est expliqué en détail par Martin Fowler et James Lewis. 

Une application mastodonte-monolithique est délicate à faire vivre et ardue à appréhender. Le découpage en couches à la JavaEE et les modules maven associés ne changent pas grand chose à la donne, et au contraire ont tendance à figer de possibles mauvais choix. Si par exemple j'ai développé mon UserRepository avec plein de beaux tests, et que je veux faire un savant refactoring de mon modèle, je vais m'assoir sur tous mes tests, car ils sont ciblés sur la vision de mon architecture à un instant t, alors que le besoin de départ est fonctionnel. On touche ici au distingo test unitaire / test fonctionnel. 

Selon une approche micro-service, plusieurs applications spécialisées prennent en charge des domaines métier restreints. Une équipe plus spécialisée et plus restreinte s'occupe de ce "fragment" et peut plus facilement le faire évoluer. Dans le cas d'une équipe distribuée à la CloudBees, ça donne des équipes de 1 :-)  Chaque service étant très spécialisé et ciblé il est facile de rentrer dans le code "en renfort".

Ces micro-services communiquent ensemble via des APIs, et peuvent utiliser des protocoles binaires pour limiter l'impact d'un appel HTTP sur les performances, mais ce qui compte ce sont les frontières qu'on a défini dans le modèle métier.


Ces frontières et la réduction du scope allègent grandement chaque service et permettent de mettre en place des pratiques de développement efficaces, et d'augmenter l'agilité de chaque service. L'agilité de l'application dans son ensemble s'améliore aussi, malgré un surcout de synchronisation des développements de chaque service. C'est un peu le principe des scrums-de-scrum.

Autre bénéfice qui émerge rapidement, chaque micro-service peut avoir sa propre architecture, voir son propre langage d'implémentation, et donc mieux répondre au besoin "local". 

Ce n'est pas forcement lié, mais si on ajoute Docker dans l'équation, tous ces services peuvent tourner sur la même machine et la communication entre micro-services a alors un coût minimal. Docker permet de donner des quotas à chaque service, et donc d'identifier facilement lequel part en vrille lorsque c'est le cas - ce qui est souvent un challenge ardu sur une grosse appli monolithique. Il permet aussi de gérer un dimensionnement différent de chaque service, un cycle de vie différent, etc.

Bref, vous l'aurez compris, je suis fan :)

2 commentaires:

Socrate a dit…

Salut !
D'accord dans l'ensemble, cependant il faut considérer les bémols suivants :
- si un module est responsable d'une notion métier utilisée par un autre module, il peut y avoir un couplage fort entre 2 modules qui annule les bénéfices de la modularité (quand bien même elle serait micro)
- une notion partagée par plusieurs modules qui évolue va nécessiter une évolution synchrone de ces modules qui, communiquant à travers un idiome différent du langage de base (http plutôt que java), ne pourra bénéficier des outils de refactoring
- passer par du http, même minimaliste, peut engendrer -lorsque la pile des appels http grossit- une réduction non négligeable des performances. Cet aspect peut emmener à son tour l'émergence de librairies de cache côté client avec toute la galère que ça entraîne.
- l'émergence de mécanismes pub-sub dûs aux cas ci-dessus. pub-sub over http n'est pas forcément adapté au besoin (encore une fois, ça dépend des cas, de la perf requise, ...)
- la duplication des données lorsque qu'un micro-service doit gérer du versionning des objets récupérés par des micro-services tiers (du fait d'une architecture event-driven par exemple).

En conséquence, quand bien même j'adore cette approche, je pense qu'il faut identifier les cas qui s'y prêtent vraiment :
- authentification : oui : service central, one-shot, couplage faible, versionning d'API possible
- repository : oui et non : service central, mais pas du tout one-shot, couplage fort, mais API versionning possible
- stingutils / utils en général : je pense plutôt non (une lib est parfaite dans ce rôle)
- gestion des "constantes applicatives" : multilangue/encodage, stratégie de gestion de date/time/timezone, de logging, d'historisation : non pas du tout (une lib ou nano-framework maison est là aussi plus adaptée à mon goût). Pour toutes les problématiques transverses, je peine à imaginer comment elles pourraient s'intégrer dans cette logique.
- gestion du logging tout court : oui, même si c'est potentiellement lourd, c'est asynchrone à 100%

Ensuite vient la dernière question : chaque micro-service doit-il accéder à ses comparses via son propre micro-client http ? via une librairie dédiée à l'accès à ce micro-service ? via des appels "en dur" pour peu que le framework http permette une exploitation relativement immédiate des valeurs de retour ?

Attention également aux services où on "dump" ses données (comme du logging) : on peut rapidement se retrouver à avoir à gérer l'éventualité d'un micro-service défaillant... auquel cas c'est à l'appelant de s'assurer qu'il ne perd pas ses données... ce qui aboutit en général à la mise en place d'une file... qui se convertit en général à l'utilisation d'un bus central de communication inter-modules.

En conclusion de la réflexion d'un coin de table que ton billet m'a inspirée, je dirais que c'est définitivement une approche à considérer, bien qu'en définitive, on en revienne au fameux "adapter l'architecture au besoin", ce qui fait -peut-être- qu'en 2014 on en soit toujours à se demander "mais quelle architecture est vraiment la meilleure ?", car elles sont toutes meilleures... mais dans leurs domaines respectifs, et ces domaines s'entremêlent souvent dans à peu près toutes les applications.

Merci pour la "minute de rétrospective" ;)

Nicolas De Loof a dit…

Merci pour ton commentaire détaillé et constructif.

http est la façon simple de faire, mais du Thrift/ProtoBuf permet de faire plus efficace si nécessaire.

La duplication des données doit être vue comme un avantage : le tout est de définir le référent. Pouvoir enrichir/retravailler le modèle métier selon son point de vue local plutôt que de définir un modèle qui convienne à tous est nettement plus facile.

Sur l'aspect versioning, on peut soit exposer plusieurs version de l'API (c'est sans doute ce que tu suggère) soit avoir N versions du service qui tournent, N-1 en lecture seule, ou encore un proxy N => N+1

sur les aspects transverse, attention à ne pas confondre les éléments applicatifs (logging, configuration, securité) et les limites du langage (stringutils). Le dernier nécessite en effet des librairies et frameworks qui sont fait pour ça. Micro-service ne veut pas dire application de quelques Ko.

Et pour ces solutions transverses il existe en général des solutions toutes faites. Pour la gestion du logging par exemple, que tu considère "potentiellement lourd", utilise le syslog et les outils qui vont avec et/ou couple ça avec un service comme PaperTrail et basta. L'invraisemblable complexité du logging en Java vient de l'incapacité à comprendre la problématique du Logging, pour se focaliser sur l'API développeur (problème assez typique en java)

La gestion des échecs de communication est un vrai soucis, et clairement il faut du monitoring. Par contre c'est plus facile d'identifier quel brique par en c.. et de cerner le problème, qu'une grosse appli qui fait un OOME.

Quand à ta conclusion, je plussois dans ton sens. Les micro-services apportent une réponse qui ne correspond pas à tous les besoins, par contre ils contribuent à poser la question "mais pourquoi on fait comme ça au fait" qui manque souvent.

Quand à l'entremêlement des domaines, jusqu'ici ce que j'ai vu sur de grosses application c'est qu'on a couplé des données parce que cela a été modélisé comme ça, mais ce n'est en aucun cas une nécessité.

Encore merci pour ton retour