En seulement 20 ans, le développement logiciel est passé des monolithes architecturaux avec une seule base de données et un état centralisé à des microservices, où tout est distribué sur de nombreux conteneurs, serveurs, centres de données et même continents. La distribution facilite la mise à l'échelle, mais elle présente également des défis entièrement nouveaux, dont beaucoup étaient auparavant résolus avec des monolithes.
Faisons un tour rapide de l'histoire des applications en réseau pour comprendre comment nous sommes arrivés ici aujourd'hui. Et puis parlons du modèle d'exécution avec état utilisé dans Temporal.et comment il résout le problème des architectures orientées services (SOA). Je suis peut-être partial parce que je dirige le département d'épicerie chez Temporal, mais je crois que cette approche est l'avenir.
Une petite leçon d'histoire
Il y a vingt ans, les développeurs créaient presque toujours des applications monolithiques. C'est un modèle simple et cohérent, similaire à la façon dont vous programmez dans votre environnement local. De par leur nature, les monolithes dépendent d'une seule base de données, c'est-à-dire que tous les états sont centralisés. En une seule transaction, le monolithe peut changer n'importe lequel de ses états, c'est-à-dire qu'il donne un résultat binaire: qu'il ait fonctionné ou non. Il n'y a pas de place pour l'incohérence. Autrement dit, la belle chose à propos du monolithe est qu'il n'y aura pas d'état incohérent en raison d'une transaction échouée. Et cela signifie que les développeurs n'ont pas besoin d'écrire du code, se demandant tout le temps l'état des différents éléments.
Pendant longtemps, les monolithes avaient du sens. Il n'y avait pas encore beaucoup d'utilisateurs connectés, donc les exigences de mise à l'échelle du logiciel étaient minimes. Même les plus grands géants du logiciel exploitaient des systèmes qui étaient dérisoires par rapport aux normes modernes. Seule une poignée d'entreprises comme Amazon et Google ont utilisé des solutions à grande échelle, mais ce sont là les exceptions à la règle.
Les gens en tant que logiciel
Au cours des 20 dernières années, les exigences logicielles n'ont cessé de croître. Aujourd'hui, les applications devraient fonctionner sur le marché mondial dès le premier jour. Des entreprises comme Twitter et Facebook ont fait du 24/7 en ligne une condition préalable. Les applications ne fournissent plus rien, elles sont devenues une expérience utilisateur elles-mêmes. Chaque entreprise doit aujourd'hui disposer de produits logiciels. «Fiabilité» et «disponibilité» ne sont plus des propriétés, mais des exigences.
Malheureusement, les monolithes ont commencé à se désagréger lorsque «l'évolutivité» et la «disponibilité» ont été ajoutées aux exigences. Les développeurs et les entreprises devaient trouver des moyens de suivre le rythme de la croissance mondiale explosive et des attentes exigeantes des utilisateurs. J'ai dû rechercher des architectures alternatives qui réduisent les problèmes émergents associés à la mise à l'échelle.
Les microservices (enfin, les architectures orientées services) étaient la réponse. Au départ, ils semblaient être une excellente solution car ils vous permettaient de diviser les applications en modules relativement autonomes pouvant être mis à l'échelle indépendamment. Et comme chaque microservice conservait son propre état, les applications n'étaient plus limitées à la capacité d'une seule machine! Les développeurs ont enfin pu créer des programmes capables d'évoluer avec le nombre croissant de connexions. Les microservices ont également donné aux équipes et aux entreprises une flexibilité dans leur travail grâce à la transparence des responsabilités et à la séparation des architectures.
Il n'y a pas de fromage gratuit
Alors que les microservices ont résolu les problèmes d'évolutivité et de disponibilité qui ont entravé la croissance des logiciels, les choses n'ont pas été sans nuages. Les développeurs ont commencé à se rendre compte que les microservices avaient de sérieux défauts.
Les monolithes ont généralement une base de données et un serveur d'applications. Et comme le monolithe ne peut pas être divisé, il n'y a que deux façons de mettre à l'échelle:
- Vertical : mise à niveau du matériel pour augmenter le débit ou la capacité. Cette mise à l'échelle peut être efficace, mais elle est coûteuse. Et cela ne résoudra certainement pas le problème pour toujours si votre application doit continuer à se développer. Et si vous vous développez suffisamment, vous ne vous retrouverez pas avec suffisamment d'équipement à mettre à niveau.
- : , . , .
C'est différent avec les microservices, leur valeur réside dans la possibilité d'avoir de nombreux «types» de bases de données, files d'attente et autres services qui sont mis à l'échelle et gérés indépendamment les uns des autres. Cependant, le premier problème qui a commencé à être remarqué lors du passage aux microservices était précisément le fait que vous devez maintenant vous occuper d'un tas de toutes sortes de serveurs et de bases de données.
Pendant longtemps, tout a été laissé au hasard, les développeurs et les opérateurs se sont débrouillés seuls. Les problèmes de gestion d'infrastructure posés par les microservices sont difficiles à résoudre, au mieux dégradant la fiabilité des applications.
Cependant, l'offre se fait sur demande. Plus les microservices se répandent, plus les développeurs sont motivés pour résoudre les problèmes d'infrastructure. Lentement mais sûrement, des outils ont commencé à émerger et des technologies comme Docker, Kubernetes et AWS Lambda ont comblé le vide. Ils ont rendu l'architecture des microservices très simple à utiliser. Au lieu d'écrire leur propre code pour orchestrer avec des conteneurs et des ressources, les développeurs peuvent s'appuyer sur des outils prédéfinis. En 2020, nous avons enfin franchi le cap où la disponibilité de notre infrastructure n'interfère plus avec la fiabilité de nos applications. À la perfection!
Bien sûr, nous ne vivons pas encore dans l'utopie d'un logiciel parfaitement stable. L'infrastructure n'est plus la source d'insécurité applicative, le code applicatif a pris sa place.
Un autre problème avec les microservices
Dans les monolithes, les développeurs écrivent du code qui change les états de manière binaire: soit quelque chose se passe, soit il ne se passe pas. Et avec les microservices, l'état est réparti sur différents serveurs. Pour modifier l'état d'une application, plusieurs bases de données doivent être mises à jour en même temps. Il y a de fortes chances qu'une base de données se mette à jour avec succès et d'autres plantent, vous laissant avec un état intermédiaire incohérent. Mais comme les services étaient la seule solution au problème de la mise à l'échelle horizontale, les développeurs n'avaient pas d'autre option.
Un problème fondamental avec l'état réparti entre les services est que chaque appel à un service externe aura un résultat aléatoire en termes de disponibilité. Bien sûr, les développeurs peuvent ignorer le problème dans leur code et considérer que chaque appel à une dépendance externe réussit toujours. Mais alors, certaines dépendances peuvent mettre l'application hors service sans avertissement. Par conséquent, les développeurs ont dû adapter leur code de l'ère des monolithes pour ajouter des vérifications de l'échec des opérations au milieu des transactions. Ce qui suit montre la récupération continue du dernier état enregistré à partir d'un magasin myDB dédié pour éviter les conditions de concurrence. Malheureusement, même cette implémentation n'aide pas. Si l'état du compte change sans mettre à jour myDB, des incohérences peuvent se produire.
public void transferWithoutTemporal(
String fromId,
String toId,
String referenceId,
double amount,
) {
boolean withdrawDonePreviously = myDB.getWithdrawState(referenceId);
if (!withdrawDonePreviously) {
account.withdraw(fromAccountId, referenceId, amount);
myDB.setWithdrawn(referenceId);
}
boolean depositDonePreviously = myDB.getDepositState(referenceId);
if (!depositDonePreviously) {
account.deposit(toAccountId, referenceId, amount);
myDB.setDeposited(referenceId);
}
}
Hélas, il est impossible d'écrire du code sans erreurs. Et plus le code est complexe, plus les bogues apparaîtront probablement. Comme vous vous en doutez, le code qui fonctionne avec le «middleware» est non seulement complexe mais aussi compliqué. Au moins une certaine fiabilité est meilleure que l'absence de fiabilité, de sorte que les développeurs ont dû écrire un code initialement bogué pour maintenir l'expérience utilisateur. Cela nous coûte du temps et des efforts, et les employeurs beaucoup d'argent. Bien que les microservices évoluent à merveille, cela se fait au prix du plaisir et de la productivité des développeurs, et de la fiabilité des applications.
Des millions de développeurs passent du temps chaque jour à réinventer l'une des roues les plus réinventées - la fiabilité du passe-partout. Les approches modernes de l'utilisation des microservices ne reflètent tout simplement pas les exigences de fiabilité et d'évolutivité des applications modernes.
Temporel
Nous arrivons maintenant à notre solution. Il n'est pas approuvé par Stack Overflow et nous ne prétendons pas être parfaits. Nous voulons simplement partager nos idées et entendre votre opinion. Quel meilleur endroit pour obtenir des commentaires sur l'amélioration de votre code que la pile?
Jusqu'à aujourd'hui, il n'existait pas de solution permettant d'utiliser des microservices sans résoudre les problèmes décrits ci-dessus. Vous pouvez tester et émuler les états de crash, écrire du code en tenant compte des plantages, mais ces problèmes se posent toujours. Nous pensons que Temporal les résout. Il s'agit d'un environnement avec état open source (MIT no-nonsense) pour l'orchestration de microservices.
Temporal a deux composants principaux: un backend avec état qui s'exécute sur la base de données de votre choix et un framework client dans l'une des langues prises en charge. Les applications sont créées à l'aide d'une infrastructure client et d'un code hérité régulier qui enregistre automatiquement les modifications d'état dans le backend lors de leur exécution. Vous pouvez utiliser les mêmes dépendances, bibliothèques et chaînes de construction que vous le feriez pour toute autre application. Pour être honnête, le backend est hautement distribué, donc ce n'est pas comme J2EE 2.0. En fait, c'est la distribution du backend qui permet une mise à l'échelle horizontale presque infinie. Temporal apporte cohérence, simplicité et fiabilité à la couche application, tout comme l'infrastructure Docker, Kubernetes et l'architecture sans serveur.
Temporal fournit un certain nombre de mécanismes hautement fiables pour l'orchestration de microservices. Mais le plus important est la préservation de l'État. Cette fonction utilise l' émission d'événements pour enregistrer automatiquement toutes les modifications avec état d'une application en cours d'exécution. Autrement dit, si l'ordinateur sur lequel Temporal s'exécute tombe en panne, le code passe automatiquement à un autre ordinateur, comme si de rien n'était. Cela s'applique même aux variables locales, aux threads d'exécution et à d'autres états spécifiques à l'application.
Laissez-moi vous donner une analogie. En tant que développeur, vous comptez probablement aujourd'hui sur la gestion des versions SVN (c'est OG Git) pour suivre les modifications que vous apportez à votre code. SVN enregistre simplement les nouveaux fichiers, puis les liens vers des fichiers existants pour éviter la duplication. Temporal est quelque chose comme SVN (analogie approximative) pour l'historique avec état des applications en cours d'exécution. Lorsque votre code change l'état de l'application, Temporal enregistre automatiquement ce changement (pas le résultat) sans erreur. Autrement dit, Temporal ne restaure pas seulement l'application en panne, il la restaure également, la fourche et fait bien plus. Les développeurs n'ont donc plus besoin de créer des applications dans l'espoir que le serveur puisse planter.
C'est comme passer de l'enregistrement manuel des documents (Ctrl + S) après chaque caractère saisi à l'enregistrement automatique dans le cloud de Google Docs. Pas dans le sens où vous ne sauvegardez plus rien manuellement, c'est juste qu'il n'y a plus aucune machine associée à ce document. L'état signifie que les développeurs peuvent écrire beaucoup moins de code passe-partout ennuyeux qui a dû être écrit en raison de microservices. De plus, vous n'avez plus besoin d'une infrastructure spéciale - files d'attente, caches et bases de données séparés. Cela facilite l'utilisation et l'ajout de nouvelles fonctionnalités. Cela facilite également la mise à jour des débutants, car ils n'ont pas besoin de comprendre un code de gestion d'état confus et spécifique.
La rétention d'état est également mise en œuvre sous la forme de «temporisateurs persistants». C'est un mécanisme de sécurité qui peut être utilisé avec une commande
Workflow.sleep
. Cela fonctionne exactement de la même manière que sleep
. Cependant, Workflow.sleep
il peut être euthanasié en toute sécurité pour n'importe quelle durée. De nombreux utilisateurs de Temporal dorment depuis des semaines, voire des années. Ceci est accompli en stockant des minuteries de longue durée dans le magasin temporel et en gardant une trace du code pour se réveiller. Encore une fois, même si le serveur tombe en panne (ou si vous venez de l'éteindre), le code ira à la machine disponible à l'expiration du délai. Les processus de mise en veille ne consomment pas de ressources, vous pouvez en avoir des millions avec une surcharge négligeable. Cela peut sembler trop abstrait, voici donc un exemple de code temporel fonctionnel:
public class SubscriptionWorkflowImpl implements SubscriptionWorkflow {
private final SubscriptionActivities activities =
Workflow.newActivityStub(SubscriptionActivities.class);
public void execute(String customerId) {
activities.onboardToFreeTrial(customerId);
try {
Workflow.sleep(Duration.ofDays(180));
activities.upgradeFromTrialToPaid(customerId);
while (true) {
Workflow.sleep(Duration.ofDays(30));
activities.chargeMonthlyFee(customerId);
}
} catch (CancellationException e) {
activities.processSubscriptionCancellation(customerId);
}
}
}
En plus de l'état persistant, Temporal propose un ensemble de mécanismes pour créer des applications robustes. Les fonctions d'activité sont appelées à partir de workflows, mais le code exécuté dans l'activité n'est pas avec état. Bien qu'elles ne persistent pas, les activités contiennent des tentatives automatiques, des délais d'expiration et des pulsations. Les activités sont très utiles pour encapsuler du code qui pourrait échouer. Supposons que votre application utilise une API bancaire qui n'est souvent pas disponible. Pour les logiciels hérités, vous devez encapsuler tout le code qui appelle cette API avec des instructions try / catch, une logique de nouvelle tentative et des délais d'expiration. Mais si vous appelez l'API bancaire à partir d'une activité, toutes ces fonctions sont fournies prêtes à l'emploi: si l'appel échoue, l'activité sera automatiquement relancée. Tout est génialmais parfois, vous possédez vous-même un service peu fiable et souhaitez le protéger contre les attaques DDoS. Par conséquent, les appels d'activité prennent également en charge les délais d'expiration, sauvegardés par des temporisations longues. Autrement dit, les pauses entre les répétitions d'activités peuvent atteindre des heures, des jours ou des semaines. Ceci est particulièrement utile pour le code qui doit s'exécuter avec succès, mais vous ne savez pas à quelle vitesse il doit se produire.
Cette vidéo explique le modèle de programmation dans Temporal en deux minutes:
Une autre force de Temporal est l'observabilité de l'application en cours d'exécution. L'API Observation fournit une interface de type SQL pour interroger les métadonnées de n'importe quel flux de travail (exécutable ou non). Vous pouvez également définir et mettre à jour vos propres valeurs de métadonnées directement dans le processus. L'API d'observation est très utile pour les opérateurs et les développeurs Temporal, en particulier lors du débogage pendant le développement. La surveillance prend même en charge les actions par lots sur les résultats des requêtes. Par exemple, vous pouvez envoyer un signal d'arrêt à tous les processus de travail qui correspondent à une demande avec une heure de création> hier. Temporal prend en charge une fonction d'extraction synchrone qui vous permet d'extraire les valeurs des variables locales des instances en cours d'exécution. C'est comme si un débogueur de votre IDE travaillait avec des applications de production. Par exemple, voici comment obtenir la valeur
greeting
dans une instance en cours d'exécution:
public static class GreetingWorkflowImpl implements GreetingWorkflow {
private String greeting;
@Override
public void createGreeting(String name) {
greeting = "Hello " + name + "!";
Workflow.sleep(Duration.ofSeconds(2));
greeting = "Bye " + name + "!";
}
@Override
public String queryGreeting() {
return greeting;
}
}
Conclusion
Les microservices sont excellents, ils ont le prix de la productivité et de la fiabilité que paient les développeurs et les entreprises. Temporal est conçu pour résoudre ce problème en fournissant un environnement qui paie des microservices pour les développeurs. L'état prêt à l'emploi, les pannes automatiques et la surveillance ne sont que quelques-unes des fonctionnalités dont dispose Temporal pour rendre le développement de microservices intelligent.