Loupe

Scale-out d'une application ASP.NET Core SignalR à l'aide d'Azure Service Bus

Le problème et les solutions existantes

ASP.NET Core SignalR est un moyen très simple d'établir une communication bidirectionnelle entre une application ASP.NET Core et ses clients. Par exemple, il peut être utilisé pour envoyer des notifications à tous les clients connectés. Cependant, dès lors qu'on fait une mise à l'échelle horizontale (scale out) de l'application en augmentant le nombre d'instances, cela ne fonctionne plus tout à fait : seuls les clients connectés à l'instance qui envoie la notification la recevront. Autrement dit, si l'instance A envoie un message à tous ses clients SignalR, les clients connectés à l'instance B ne le recevront pas. Microsoft propose deux solutions documentées à ce problème :

Le MVP Derek Comartin a donné sur son blog une bonne explication de ces solutions (Redis, Azure SignalR Service), donc je ne les détaillerai pas plus ici. Ces deux options sont tout à fait viables, cependant elles sont relativement chères. Une ressource Redis Cache dans Azure est à partir de 14€/mois environ pour la plus petite taille, et Azure SignalR Service côute environ 40€/mois pour une seule unité (je ne parle même pas du plan gratuit, qui est trop limité pour l'utiliser en dehors d'un scénario de développement). Vous me direz, ce n'est peut-être pas si cher que ça, mais pourquoi payer plus quand on peut payer moins ?

Une solution alternative

Le sujet que je veux aborder dans ce billet est une troisième option, qui sera probablement moins chère dans de nombreux scénarios : utiliser Azure Service Bus pour distribuer les messages entre les instances de serveur. En fait, cette solution était supportée dans ASP.NET classique, mais elle n'a pas été portée en .NET Core.

Voici une vue d'ensemble de comment on pourrait manuellement implémenter cette approche à base de Service Bus :

  • Quand une instance de l'application veut envoyer un message SignalR à tous les clients, elle l'envoie :

    • via son propre hub SignalR (seuls les clients connectés à cette instance le recevront)
    • et à un topic Azure Service Bus, pour le distribuer aux autres instances
    // Pseudo code...
    
    private readonly IHubContext<ChatHub, IChatClient> _hubContext;
    private readonly IServiceBusPublisher _serviceBusPublisher;
    
    public async Task SendMessageToAllAsync(string text)
    {
        // Envoi du message aux clients connectés à l'instance courante
        await _hubContext.Clients.All.ReceiveMessageAsync(text);
    
        // Notification des autres instances pour qu'elles envoient le même message
        await _serviceBusPublisher.PublishMessageAsync(new SendToAllMessage(text));
    }
    
  • Chaque instance de l'application exécute un hosted service qui s'abonne au topic et traite les messages.

    • Quand un message est reçu, il est envoyé via le hub aux clients connectés à l'instance courante, à moins que le message ne vienne de cette instance :
    // Pseudo code très simplifié...
    
    // Abonnement au topic
    var subscriptionClient = new SubscriptionClient(connectionString, topicName, subscriptionName);
    subscriptionClient.RegisterMessageHandler(OnMessageReceived, OnError);
    
    ...
    
    private async Task OnMessageReceived(Message sbMessage, CancellationToken cancellationToken)
    {
        SendToAllMessage message = DeserializeServiceBusMessage(sbMessage);
    
        if (message.SenderInstanceId == MyInstanceId)
            return; // Ignorer les messages envoyé par l'instance courante
    
        // Envoi du message aux clients connectés à l'instance courante
        await _hubContext.Clients.All.ReceiveMessageAsync(message.Text);
    }
    

Je ne montre pas tous les détails de l'implémentation de cette solution, parce que pour être honnête, elle n'est pas terrible. Elle fonctionne, mais elle est assez moche : le fait qu'on utilise un Service Bus pour partager les messages avec les autres instances est trop visible, on ne peut pas l'ignorer. Chaque fois qu'on envoie un message via SignalR, on doit aussi explicitement en envoyer un vers le service bus. Il serait mieux de cacher tout ça derrière une abstraction, ou même de le rendre complètement invisible...

Solution propre et prête à l'emploi

Si vous avez déjà utilisé les solutions basées sur Redis ou Azure SignalR mentionnées plus haut, vous avez peut-être remarqué à quel point elles sont simples à utiliser. En gros, dans la méthode Startup.ConfigureServices, il suffit d'ajouter AddRedis(...) ou AddAzureSignalR(...) derrière services.AddSignalR(), et c'est tout : on peut ensuite utiliser SignalR normalement, les détails de la gestion du scale-out sont complètement masqués. Ce serait bien de pouvoir faire la même chose pour Azure Service Bus, non ? C'est ce qu'il me semblait aussi, c'est pourquoi j'ai créé une librairie qui fait exactement ça : AspNetCore.SignalR.AzureServiceBus. Pour l'utiliser, référencez le package NuGet, et ajoutez ceci à votre méthode Startup.ConfigureServices :

services.AddSignalR()
        .AddAzureServiceBus(options =>
        {
            options.ConnectionString = "(votre chaine de connexion au service bus)";
            options.TopicName = "(le nom de votre topic)";
        });

(Notez que le topic doit avoir été créé préalablement.)

Et c'est tout ! Utilisez ensuite SignalR comme vous en avez l'habitude.

Attention : Cette librairie est encore en statut alpha, probablement pas prête pour un usage en production. Je n'ai connaissance d'aucun problème particulier pour l'instant, mais elle n'a pas encore été beaucoup testée. Utilisez-la donc à vos risques et périls, et s'il vous plait, signalez moi sur GitHub les problèmes que vous trouvez !

Photo de profil

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus