Comment faire cuire la bouillie à partir de microservices

L'une des raisons de la popularité des microservices est la possibilité d'un développement autonome et indépendant. En substance, l'architecture de microservices est l'échange de la possibilité d'un développement autonome pour un déploiement, des tests, un débogage et une surveillance plus complexes (par rapport à un monolithe). Mais gardez à l'esprit que les microservices ne pardonnent pas la séparation des responsabilités. Si la séparation des tâches est incorrecte, des changements dépendants fréquents se produisent dans différents services. Et cela est beaucoup plus douloureux et plus compliqué que des changements coordonnés dans le cadre de différents modules ou packages à l'intérieur du monolithe. Des changements cohérents dans les microservices sont compliqués par la mise en page, le déploiement, les tests, etc. cohérents.

Et je voudrais parler des divers modèles et antipatrons de la division des responsabilités en microservices.

Entité de service en tant qu'antipattern


«Entité de service» est l'un des modèles (anti) possibles de conception d'architecture de microservice, ce qui conduit à un code hautement dépendant dans différents services et couplé de manière lâche au sein des services.

Pour la plupart des développeurs, il semble que lors de la sélection de services en fonction de l'essence du sujet: «deal», «person», «client», «order», «picture», il suit les principes de la responsabilité exclusive, et de plus, cela semble souvent logique. Mais l'approche de l'entité de service peut devenir un contre-modèle. Cela se produit car la plupart des fonctionnalités ou des modifications affectent plusieurs entités, et non une seule. En conséquence, chacun de ces services combine la logique de différents processus métier.

Par exemple, prenez une boutique en ligne. Nous avons décidé de mettre en avant les services «produit», «commande», «client».

Quels changements et services dois-je apporter pour ajouter la livraison à domicile?
Par exemple, vous pouvez faire ceci:

  • dans le service "commande" ajoutez l'adresse de livraison, l'heure souhaitée et le livreur
  • dans le service client ajouter une liste d'adresses de livraison sélectionnées pour le client
  • dans le service «produit» ajouter une liste d'entités de biens

Pour l'interface du fournisseur, il sera nécessaire de faire une méthode API distincte dans le service «commande», qui donnera une liste des commandes attribuées à ce fournisseur particulier. De plus, des méthodes seront nécessaires pour retirer de la commande des marchandises qui ne correspondaient pas ou que le client avait refusées au moment de la livraison.

Ou quels changements et dans quels services dois-je effectuer pour ajouter des remises sur le code promotionnel?
Au minimum, vous avez besoin de:

  • ajouter un code promotionnel au service «commande»
  • dans le service «produit», ajoutez si des remises s'appliquent sur le code promotionnel pour ce produit
  • dans le service client ajouter une liste de codes promotionnels qui ont été émis au client

Dans l'interface du responsable, l'ajout d'un code promotionnel personnalisé au client est une méthode distincte dans le service client, qui n'est disponible que pour les directeurs de magasin, mais pas pour le client lui-même. Et dans le service «produit», faites une méthode qui donne une liste des produits qui sont concernés par le code promotionnel, afin qu'il soit plus facile pour le client de choisir dans son interface.

Les sources de changements dans le service peuvent être plusieurs processus commerciaux - sélection et conception, paiement et facturation, livraison. Chacun des domaines problématiques a ses propres limites, invariants et exigences pour la commande. En conséquence, il s'avère que dans le service «produit», nous stockons des informations sur le produit, sur les remises et les soldes de produits dans les entrepôts. Et dans la «commande» est stockée la logique du livreur.

En d'autres termes, un changement de logique métier qui est réparti sur plusieurs services entraîne des changements dépendants dans plusieurs services. Et en même temps dans un service est un code qui n'est pas connecté les uns aux autres.

Services de stockage


Il semble que ce problème puisse être résolu si un service de «couche» distinct est créé sur les services d'entité, qui encapsulent la logique entière. Mais généralement, cela se termine également mal. Parce que les services d'entité deviennent alors des services de stockage, c'est-à-dire toute logique métier en est éliminée, à l'exception du stockage.

Si les données sont stockées dans différentes bases de données, sur différentes machines, nous

  • nous perdons des performances parce que nous ne fournissons pas de données directement à partir de la base de données, mais à travers la couche de service
  • nous perdons de la flexibilité car l'API de service est généralement beaucoup moins flexible que SQL ou tout autre langage de requête
  • nous perdons en flexibilité, car il est difficile de fusionner les données de différents services

Si différents services d'entité ont accès à d'autres bases de données, la communication entre les services se fait implicitement - via une base de données commune, puis pour effectuer toute modification affectant un changement de schéma de données, il n'est possible qu'après avoir vérifié que cette modification ne cassera pas tous les autres services qui utilisent cette base de données ou tablette. .

En plus d'un développement complexe, ces services deviennent trop critiques et lourdement chargés - avec presque toutes les demandes d'un service de niveau supérieur, vous devez faire plusieurs demandes à différentes entités de service, ce qui signifie que leur édition devient encore plus difficile afin de satisfaire les exigences accrues de fiabilité et de performances.

En raison de ces difficultés avec le développement et la prise en charge des services d'entité dans leur forme pure, vous voyez rarement un modèle; généralement, les services d'entité se transforment en un ou deux «microlithes-monolithes» centraux, qui changent souvent et contiennent la logique métier principale et les placers de petits microservices, généralement l'infrastructure et les petits qui changent rarement.

Séparation par zones problématiques


Les changements en eux-mêmes ne sont pas nés, ils proviennent d'une zone problématique. Une zone de problème est une zone de tâche dans laquelle les problèmes nécessitant des modifications du code sont formulés dans une langue, en utilisant un ensemble de concepts ou interconnectés par la logique métier. Par conséquent, dans le cadre d'un domaine problématique, il y aura très probablement un ensemble de contraintes, des invariants sur lesquels vous pourrez compter lors de l'écriture de code.

La séparation de la responsabilité des services par zones problématiques plutôt que par entités conduit généralement à une architecture plus supportée et compréhensible. Les domaines problématiques correspondent le plus souvent à des processus métier. Pour la boutique en ligne, les problèmes les plus probables seront «paiement et facturation», «livraison», «processus de commande».

Les modifications qui affecteraient plusieurs zones problématiques en même temps sont moindres que les modifications qui affecteraient plusieurs entités.

De plus, les services ventilés par processus métier peuvent être réutilisés à l'avenir. Par exemple, si à côté de la boutique en ligne nous voulions faire une autre vente de billets d'avion, nous pourrions réutiliser le service général «Facturation et paiement». Et n'en faites pas un autre similaire, mais spécifique à la vente de billets.

Par exemple, nous pouvons ainsi nous diviser en services:

  • Un service ou un groupe de services «Delivery», qui stockera la logique du travail avec la livraison d'une commande spécifique, l'organisation du travail des fournisseurs, l'évaluation de la qualité de leur travail, l'application mobile du fournisseur, etc.
  • Un service ou un groupe de services «Facturation et paiement», qui stockera la logique de travail avec le paiement, les comptes de paiement pour les personnes morales, la génération de contrats et les documents de clôture.
  • Service ou groupe de services «Processus de commande», qui stocke la logique du choix des produits par le client, le catalogage, les marques, la logique du panier, etc.
  • Service «autorisation et authentification».
  • Il peut même être judicieux de séparer le service de remise.

Pour interagir les uns avec les autres, les services peuvent utiliser le modèle d'événement ou échanger des objets simples entre eux (api reposant, grpc, etc.). Certes, il convient de noter qu'il n'est pas facile d'organiser correctement l'interaction entre ces services. Au minimum, la décentralisation des données a parfois des problèmes de cohérence (cohérence éventuelle) et de transactionnalité (dans le cas où elle est importante).

Décentralisation des données, l'échange d'objets simples a ses avantages, ses inconvénients et ses pièges. D'une part, la décentralisation permet de développer et d'exploiter indépendamment plusieurs services. D'autre part, le coût du stockage de deux ou trois copies de données et le maintien de la cohérence dans les différents systèmes.

Dans la vraie vie, quelque chose se produit souvent entre les deux. Entité de service avec un ensemble minimal d'attributs utilisé par tous les services par les consommateurs. Et une couche minimale de logique - par exemple, un modèle d'état et des événements dans la file d'attente avec la notification de tous les changements dans l'entité. Dans le même temps, les services aux consommateurs conservent encore assez souvent un «cache» de données. Tout est mis en œuvre pour qu'il y ait le moins de changements possible dans un tel service, ce qui, en principe, est difficile à réaliser en raison du grand nombre de consommateurs.

Dans le même temps, il est important de comprendre que toute partition - à la fois par entité et par zone de problème - n'est pas une solution miracle, il y aura toujours des fonctionnalités qui nécessiteront des changements dépendants dans plusieurs services. C'est juste qu'avec une panne il y aura beaucoup plus de tels changements qu'avec une autre. Et la tâche du développement est de minimiser le nombre de changements dépendants.

Une répartition idéale n'est possible que si vous avez deux produits complètement indépendants. Dans toute entreprise, vous avez tout connecté à tout, la seule question est de savoir combien est connecté.

Et la question est dans la séparation des responsabilités et dans la hauteur des barrières aux abstractions.

API du service de conception


La conception d'interfaces au sein du service répète l'histoire avec la répartition en services, mais à une échelle plus petite. Changer l'interface (pas seulement une extension) est complexe et prend du temps. Dans les applications complexes, l'interface doit être suffisamment universelle pour ne pas provoquer de changements constants, et doit être suffisamment spécifique et spécifique pour ne pas provoquer la propagation de la responsabilité et de la sémantique.

Par conséquent, les interfaces de service doivent être conçues de sorte que leur sémantique soit résistante aux changements. Et cela est possible si la sémantique ou le domaine de responsabilité de l'interface reposait sur les limites de la zone à problème.

Interfaces CRUD pour des services avec une logique métier complexe


Une interface trop large et non spécifique contribue à l'érosion des responsabilités ou à une complexité excessive.

Par exemple, l'API CRUD pour les services avec une logique métier complexe. Ces interfaces n'encapsulent pas le comportement. Ils permettent non seulement à la logique métier de s'infiltrer dans d'autres services et d'éroder la responsabilité du service, ils provoquent la propagation de la logique métier - les restrictions, les invariants et les méthodes de travail avec les données se trouvent désormais dans d'autres services. Les services utilisateur d'interface (API) doivent implémenter la logique eux-mêmes.

Si nous essayons, sans changer considérablement l'interface, de transférer la logique métier au service, nous obtiendrons une méthode trop universelle et trop compliquée.

Par exemple, il existe un service de billetterie. Un ticket peut être de différents types. Chaque type a un ensemble de champs différent et une validation légèrement différente. Le ticket a également un modèle d'état - une machine d'état pour la transition d'un état à un autre.

Laissez l'API ressembler à ceci: méthodes POST / PATCH / GET, url /api/v1/tickets/{ticket_idasket.json

Vous pouvez donc mettre à jour le ticket

PATCH /api/v1/tickets/{ticket_id}.json { "type": "bug", "status": "closed", "description": "   " } 

Si le modèle d'état dépend du ticket, des conflits de logique métier sont possibles. Tout d'abord, modifiez le statut en fonction de l'ancien modèle de statut, puis modifiez le type de ticket. Ou vice versa?

Il s'avère qu'à l'intérieur de la méthode API, il y aura du code qui n'est pas connecté les uns aux autres - modification des champs d'entité, une liste des champs disponibles, selon le type de ticket, et un modèle de statut. Ils changent pour diverses raisons et il est logique de les distribuer selon différentes méthodes et interfaces API.

Si la modification d'un champ dans le cadre des méthodes API CRUD n'est pas seulement un changement de données, mais une opération liée à un changement coordonné de l'état d'une entité, cette opération doit être reprise dans une méthode distincte et ne doit pas être modifiée directement. Si changer une API sans compatibilité descendante est très mauvais (pour les API publiques), il vaut mieux y penser tout de suite lors de la conception de l'API.

Par conséquent, afin d'éviter de tels problèmes, il est préférable de rendre les interfaces petites, spécifiques et aussi orientées vers les problèmes que possible, plutôt que celles universelles centrées sur les données.

Ce modèle (anti) est plus souvent caractéristique des interfaces RESTful, car il n'y a par défaut que quelques «verbes» centrés sur les données d'actions à créer, supprimer, mettre à jour, lire. Aucune opération d'entité spécifique à l'entreprise

Que peut-on faire pour rendre RESTful plus orienté vers les problèmes?
Tout d'abord, vous pouvez ajouter des méthodes aux entités. L'interface devient moins reposante. Mais il y a une telle opportunité. Nous ne luttons toujours pas pour la pureté de la course, mais résolvons des problèmes pratiques

Au lieu de la ressource universelle /api/v1/tickets.json ajoutez plus de ressources:

/api/v1/tickets/{ticket_id}/migrate.json - migrer d'un type à un autre
/api/v1/tickets/{ticket_id}/status.json - s'il existe un modèle de statut

Deuxièmement, vous pouvez imaginer toute opération comme une ressource dans le cadre de REST. Existe-t-il une opération de migration de ticket d'un type à un autre (ou d'un projet à un autre?). Ok, donc il y aura une ressource
/api/v1/tickets/migration.json

Existe-t-il une opération commerciale pour créer un abonnement d'essai?
/api/v1/subscriptions/trial.json

Y a-t-il une opération de transfert d'argent?
/api/v1/money_transfers.json

Etc.

L'antipattern avec l'API data-centric fait également référence à l'interaction rpc. Par exemple, la présence de méthodes trop générales comme editAccount () ou editTicket (). «Modifier un objet» ne porte pas la charge sémantique associée à la zone à problème. Cela signifie que cette méthode sera appelée pour diverses raisons, pour diverses raisons de changer.

Il convient de noter que les interfaces centrées sur les données sont tout à fait correctes, si la zone à problème implique uniquement le stockage, la réception et la modification des données.

Modèle d'événement


Une façon de délier des morceaux de code consiste à organiser l'interaction entre les services via une file d'attente de messages.

Par exemple, si dans le service, lors de l'enregistrement d'un utilisateur, nous devons lui envoyer une lettre de bienvenue, créer une demande dans CRM pour un gestionnaire de client, etc., alors il est logique de ne pas passer un appel de service externe, mais de mettre le message «l'utilisateur 123 est enregistré» dans le service d'enregistrement », Et tous les services nécessaires liront ce message et prendront les mesures nécessaires. Dans le même temps, la modification de la logique métier ne nécessitera pas de changer le service d'enregistrement.

Le plus souvent, non seulement les messages sont jetés dans la file d'attente, mais les événements. Comme la file d'attente n'est qu'un protocole de transport, les mêmes restrictions s'appliquent à l'interface de données qu'à l'interface synchrone normale. Par conséquent, afin d'éviter des problèmes de changement d'interface et de modifications ultérieures dans d'autres services, il est préférable de rendre les événements aussi orientés vers les problèmes que possible. De tels événements sont encore souvent appelés événements de domaine. Dans le même temps, l'utilisation du modèle d'événement n'affecte généralement pas beaucoup les frontières auxquelles les (micro) services se battent.

Étant donné que les événements de domaine sont pratiquement 1 en 1 traduits en méthodes API synchrones, ils suggèrent même parfois d'utiliser un flux d'événements au lieu d'un flux d'événements au lieu d'un appel d'API (Event Sourcing). Par le flux d'événements, vous pouvez toujours restaurer l'état des objets, mais aussi avoir un historique gratuit. En fait, cette approche n'est généralement pas très flexible - vous devez prendre en charge tous les événements, et il est souvent plus facile de garder une histoire à côté de l'API habituelle.

Microservices et performances. Cqrs


En principe, le domaine problématique implique des changements dans le code associés non seulement aux exigences fonctionnelles de l'entreprise, mais aussi à des exigences non fonctionnelles - par exemple, les performances. S'il y a deux morceaux de code avec des exigences de performances différentes, cela signifie que ces deux morceaux de code peuvent être judicieux de se séparer. Et ils sont généralement divisés en services distincts afin de pouvoir utiliser différents langages et technologies plus adaptés à la tâche.

Par exemple, il existe une méthode de calcul liée au processeur dans un service écrit en PHP qui effectue des calculs complexes. Avec une augmentation de la charge et de la quantité de données, il a cessé de faire face. Et bien sûr, comme l'une des options, il est logique de faire des calculs non pas dans du code php, mais dans un démon système haute performance distinct.

Comme l'un des exemples de la division des services par le principe de la productivité - la séparation des services en lecture et modification (CQRS). Cette séparation est souvent offerte car les exigences de performance des services de lecture et d'écriture sont différentes. La charge de lecture est souvent d'un ordre de grandeur supérieur à la charge d'écriture. Et les exigences de vitesse de réponse des demandes de lecture sont beaucoup plus élevées que pour l'écriture.

Le client passe 99% du temps dans la recherche de marchandises, et seulement 1% du temps dans le processus de commande. Pour un client dans un état de recherche, la vitesse d'affichage est importante et les fonctionnalités liées aux filtres, diverses options d'affichage des marchandises, etc. Par conséquent, il est logique de mettre en évidence un service distinct qui est responsable de la recherche, du filtrage et de l'affichage des marchandises. Un tel service fonctionnera très probablement sur une sorte d'ELK, une base de données orientée document avec des données dénormalisées.

De toute évidence, une division naïve entre les services de lecture et de modification n'est pas toujours bonne.

Un exemple. Pour un gestionnaire qui travaille à remplir la gamme de produits, les principales caractéristiques seront la possibilité d'ajouter facilement des marchandises, de les supprimer, de les modifier et de les afficher. Il n'y a pas beaucoup de charge, si nous séparons la lecture et la transformation en services séparés, nous n'obtiendrons rien de cette séparation, à l'exception des problèmes lorsque vous devez apporter des modifications coordonnées aux services.

Source: https://habr.com/ru/post/fr469001/


All Articles