Architecture serverless
Introduction
Une application doit parfois traiter des événements très irréguliers : quelques fichiers déposés pendant la nuit, des centaines de commandes de produits lors d'un évènement ou un traitement planifié une fois par jour. Réserver en permanence un serveur pour traiter ces tâches n'est pas toujours la meilleure solution.
L’architecture serverless permet d’exécuter du code à la demande, en réponse à un événement. Dans l’écosystème .NET, Azure Functions est l’une des solutions qui répond à ce besoin. Cette architecture concerne toutefois une architecture complète : les fonctions collaborent souvent avec des files de messages, des bases de données, des services de stockage et des outils de supervision.
Présentation
Le terme serverless ne signifie pas qu’il n’existe plus de serveurs. Il signifie que l’équipe de développement ne gère généralement pas directement les machines sur lesquelles le code s’exécute. La plateforme prend en charge une partie de l’hébergement et adapte les ressources à la demande.
Une fonction démarre lorsqu’un déclencheur (trigger) reçoit un événement. Il peut s’agir d’une requête HTTP, d’un message dans une file, de l’ajout d’un fichier ou d’une échéance planifiée. La fonction exécute alors son traitement et peut communiquer avec d’autres services au moyen du code C# ou de bindings fournis par la plateforme.
Par exemple, une application de vente peut déposer un message dans une file après la validation d’une commande. Une fonction C# reçoit ce message, prépare une confirmation et demande son envoi à un service de messagerie. Le site web et le traitement de confirmation peuvent ainsi évoluer et être dimensionnés séparément.
Serverless, fonctions et architecture : quelle différence ?
Une fonction est une unité d’exécution. Le serverless décrit plus largement un mode d’hébergement et de composition des traitements. Mettre une méthode C# dans Azure Functions ne suffit donc pas, à elle seule, à concevoir une bonne architecture.
Les règles métier peuvent rester dans des classes .NET ordinaires, indépendantes du déclencheur Azure Functions. La fonction joue alors le rôle d’adaptateur : elle reçoit l’événement, valide les données nécessaires et appelle le traitement applicatif. Cette organisation facilite les tests et évite de lier tout le métier à la plateforme.
Le serverless n’impose pas non plus l'utilisation des microservices. Une application monolithique peut déléguer quelques traitements asynchrones à des fonctions. À l’inverse, une application composée de plusieurs services peut utiliser des fonctions pour certains d’entre eux.
Dans le schéma ci-dessus, la fonction orchestre l’événement et le service applicatif porte le traitement. L’état durable réside dans les services adaptés, pas dans la mémoire d’une instance.
Implémenter le serverless
Azure Functions prend en charge C# avec le modèle de worker isolé : le code .NET s’exécute dans un processus distinct de l’hôte Functions. Ce modèle permet notamment d’utiliser les mécanismes habituels de .NET pour l’injection de dépendances et la configuration. Voici la forme simplifiée d’une fonction déclenchée par un message placé dans une file Azure Storage :
public class OrderConfirmationFunction
{
private readonly IOrderConfirmationService _confirmationService;
public OrderConfirmationFunction(
IOrderConfirmationService confirmationService)
{
_confirmationService = confirmationService;
}
[Function(nameof(OrderConfirmationFunction))]
public async Task Run([QueueTrigger("orders-to-confirm")] string message)
{
var orderId = Guid.Parse(message);
await _confirmationService.SendConfirmationAsync(orderId);
}
}
La fonction s’occupe de recevoir le message. La méthode Run implémente le traitement applicatif. Dans un projet réel, il faudrait également gérer les messages invalides, les échecs et le risque qu’un même message soit traité plusieurs fois.
Avantages
Une capacité d’adaptation à la charge
Les ressources peuvent augmenter lorsque les événements affluent et diminuer lorsque l’activité baisse, selon l’offre d’hébergement choisie.
Moins d’infrastructure à administrer
La plateforme prend en charge une partie de l’exécution et de la mise à l’échelle, ce qui permet à l’équipe de se concentrer davantage sur les traitements métier.
Un modèle adapté aux événements
Les fonctions se déclenchent en réponse à une requête, un message, un fichier ou une planification. Les files de messages aident à absorber les pics d’activité.
Un coût potentiellement intéressant pour une activité irrégulière
Certaines offres facturent en fonction de l’utilisation. L’économie réelle dépend toutefois du coût de tous les services associés.
Limites
Un délai de démarrage possible
Après une période d’inactivité, une fonction peut prendre plus de temps à répondre lors de sa première exécution.
Des limites d’exécution
La durée des traitements et leur comportement en cas d’interruption dépendent de l’offre d’hébergement. Les tâches longues demandent une conception adaptée.
Des échanges plus complexes à suivre
Lorsque plusieurs fonctions et services collaborent, il devient plus difficile de suivre un traitement et de localiser une erreur. Une bonne supervision est nécessaire.
Un risque de traitements répétés
Un événement peut être traité plusieurs fois après un échec. Les opérations sensibles doivent éviter les effets indésirables, comme une double facturation.
Une dépendance à la plateforme
Les déclencheurs et les services du fournisseur facilitent le développement, mais peuvent rendre une migration plus complexe.
Quand cette architecture est-elle un choix pertinent ?
L'architecture serverless est adaptée lorsque les traitements sont déclenchés par des événements, relativement indépendants et soumis à une charge variable. Il convient par exemple à l’import de fichiers, au traitement de messages, aux tâches planifiées, à l’envoi de notifications ou à certaines API peu sollicitées.
Cependant, il demande davantage de précautions lorsque l’application exige une latence très stable, exécute des traitements longs, entretient beaucoup d’état en mémoire ou effectue de nombreuses communications synchrones entre composants. Dans ces situations, un hébergement applicatif classique, des conteneurs ou une combinaison de plusieurs approches peuvent être plus simples à exploiter.
Le choix n’a donc pas besoin de concerner toute l’application. Une application basée sur un autre modèle d'architecture peut rester le cœur du système, tandis que quelques Azure Functions prennent en charge les tâches asynchrones ou planifiées.
Bonnes pratiques avec .NET
Garder les fonctions courtes et ciblées
Une fonction reçoit un événement et confie l’essentiel du travail à des services applicatifs.
Éviter de conserver l’état métier en mémoire
Plusieurs instances peuvent traiter des événements, et une instance peut être arrêtée. L’état nécessaire doit être stocké dans un service approprié.
Prévoir les échecs dès la conception
Définir les tentatives de traitement, le devenir des messages qui échouent durablement et les alertes associées.
Maîtriser la concurrence
Une montée en charge des fonctions peut aussi saturer SQL Server, une API partenaire ou un autre système en aval.
Tester à plusieurs niveaux
Tester les règles métier comme du code .NET ordinaire, puis vérifier les échanges réels avec les files, bases de données et autres services.
Mesurer le coût et la latence en situation réelle
Le bon choix d’hébergement dépend du volume, de la régularité du trafic et du délai de réponse attendu.
