Aller au contenu

Architecture distribuée

Introduction

Une application s'exécutant dans un seul processus, peut avoir besoin d'échanger avec d’autres applications. Dès que son fonctionnement dépend de composants qui communiquent sur un réseau, la vision de l’architecture change. La consommation d'un service distant peut mettre plus de temps que prévu, ou alors un service peut être indisponible pendant un durée indéterminée.

L’architecture distribuée permet de répartir des responsabilités et des traitements. Elle apporte de la souplesse dans certains contextes, au prix de nouvelles contraintes de conception et d’exploitation.

Présentation

Une architecture distribuée est un système composé de plusieurs applications qui s’exécutent sur des machines distinctes, dans des conteneurs distincts ou dans des processus distincts sur une même machine. Elles s'offrent des services dans leur périmètre métier respectif basés sur des échanges sur un réseau réseau.

Par exemple, une application de vente peut exposer un service Web API chargé de traiter les commandes, de traiter des mails et accéder à une base de données. Une commande reçue par l’API peut être enregistrée, qui pourra lever un évènement avertissant d'autres applications de la présence de cette commande, au travers d'un service de notification.

Architecture Distribuée - Exemple

Distribué ne veut pas nécessairement dire « microservices ». Une architecture orientée services, une application Web associée à des workers et à une messagerie, ou un système de microservices sont tous des systèmes distribués. La taille des composants, leur autonomie et la propriété de leurs données varient selon l’architecture choisie. Un monolithe peut lui-même participer à un système distribué lorsqu’il échange avec des services externes. Les différents éléments collaborent suivant différents aspects :

  • Échanges synchrones : HTTP/REST ou gRPC lorsque l’application appelante a besoin d’une réponse pour poursuivre son traitement. Elle dépend alors de la disponibilité et du temps de réponse de l’autre application.
  • Échanges asynchrones : messages ou événements lorsque le traitement peut être différé. Une file peut absorber un pic de charge et découpler l’émetteur du consommateur, mais il est nécessaire de gérer le suivi des messages, les doublons et les erreurs.
  • Données : les périmètres métier déterminent qui peut écrire quelles données. Si plusieurs services modifient directement les mêmes tables d'une base de données, il peut être difficile de gérer les accès concurrents. Selon les besoins, on peut exposer les données et leur gestion au travers d'une API.

Le choix entre ces modes dépend du besoin métier : réponse immédiate, délai acceptable, volume des données traitées, garanties de livraison et cohérence attendue entre les services.

Les avantages

Gestion des ressources ciblée

Les composants les plus sollicités peuvent recevoir davantage de ressources sans augmenter systématiquement celles de toute l’application.

Des déploiements indépendants

Des composants isolés peuvent évoluer et être publiés séparément, si leurs contrats restent compatibles.

Une meilleure isolation de certains incidents

La défaillance d’un composant peut être contenue lorsque des mécanismes de repli ont été prévus.

Une répartition claire des responsabilités

Des périmètres métier cohérents permettent d’affecter à chaque application un périmètre précis.

Une intégration facilitée

Des API et des événements explicites permettent aux applications de collaborer, et d'avoir des traitements qui évoluent à des rythmes de mises à jour différents.

Ces avantages ne sont pas automatiques : ils dépendent du découpage, des contrats et de la capacité à exploiter le système sur lesquelles les applications s'exécutent.

Les limites

Des échanges réseau incertains

Chaque appel ajoute de la latence et peut parfois échouer ou expirer (*timeout*), même lorsque les deux composants fonctionnent de manière attendue.

Une cohérence des données plus complexe

Une opération répartie entre plusieurs services ne bénéficie pas d’une transaction unique.

Un diagnostic plus difficile

Une opération traverse plusieurs processus : sans traces corrélées par un identifiant unique et des journaux structurés, retrouver la cause d’une anomalie peut être chronophage.

Un coût d’exploitation supérieur

Déploiements, sécurité, supervision, communication et infrastructure multiplient les éléments à configurer et à maintenir.

Des contrats d'API à versionner

Une modification d’API ou de message peut perturber des consommateurs encore déployés avec une version antérieure, nécessitant de versionner les API.

Des tests plus exigeants

Les tests unitaires locaux ne suffisent pas à couvrir les délais d'exécution, les incidents, les doublons et les interactions entre applications.

Quand cette architecture est-elle un choix pertinent ?

Elle devient intéressante lorsque la séparation des composants répond à un besoin concret, tel que :

  • Un traitement doit monter en charge indépendamment du reste de l’application.
  • Plusieurs équipes doivent livrer à des rythmes différents, avec des responsabilités métier bien délimitées.
  • Des tâches longues ou irrégulières gagnent à être confiées à des workers et à une file de messages.
  • Des applications existantes doivent être intégrées à une solution différente; avec des rythmes de mises à jour différents.

Pour une petite application gérée par une seule équipe, dont les besoins sont homogènes et les déploiements simples, la distribution peut coûter davantage qu’elle n'apporte de plus-values. Dans ce cas, une architecture monolithe modulaire permet souvent de définir des périmètres métier avant d’introduire des communications réseau.

Les bonnes pratiques avec .NET

Définir des périmètres métier cohérents

Regrouper les règles et les données qui évoluent ensemble, puis limiter les échanges entre composants.

Attribuer les responsabilités sur les données

Réserver les écritures à un composant / une application et partager les informations nécessaires par API ou événement.

Choisir les échanges adaptés

Utiliser ASP.NET Core avec HTTP ou gRPC pour une réponse immédiate et la messagerie pour les traitements différés.

Prévoir les défaillances réseau

Configurer les délais, les annulations les *politiques de retry* ; éviter de relancer sans précaution un service non idempotent.

Fiabiliser la publication

Utiliser une Outbox (telle qu'une table dans laquelle une application enregistre les messages qu’elle doit publier), lorsque la persistance avec Entity Framework Core et la publication doivent rester liées.

Observer les parcours

Corréler les journaux, les métriques et les traces avec une solution telle qu'OpenTelemetry.

Sécuriser chaque composant

Authentifier les appelants, vérifier qu'ils possèdent les autorisations nécessaires et les informations secrètes en dehors du code source (en utilisant un Vault, des UserSecrets, etc.).

Faire évoluer les contrats

Documenter les API avec OpenAPI, versionner lorsque c’est nécessaire et vérifier la compatibilité avec les consommateurs.

Dans l'écosystème .NET, ASP.NET Core fournit le nécessaire (Web API, gRPC, HttpClient, etc.) pour implémenter des appels directs. Les Background Services sont adaptés aux workers, Entity Framework Core prend en charge les transactions locales et OpenTelemetry aide à suivre les corrélations dans l'exécution des services. Microsoft Aspire peut faciliter la description, l’exécution locale et l’observation des composants.