Modello di consumer concorrenti

Funzioni di Azure
Bus di servizio di Azure

Consentire a più consumer concorrenti di elaborare i messaggi ricevuti sullo stesso canale di messaggistica. Con più consumer simultanei, un sistema può elaborare più messaggi simultaneamente per ottimizzare la velocità effettiva, migliorare la scalabilità e la disponibilità e bilanciare il carico di lavoro.

Contesto e problema

Un'applicazione in esecuzione nel cloud deve gestire un numero elevato di richieste. Una tecnica comune prevede che, anziché elaborare ogni richiesta in modo sincrono, l'applicazione le passi tramite un sistema di messaggistica a un altro servizio (un servizio consumer) che le gestisce in modo asincrono. Questa strategia consente di garantire che la logica di business nell'applicazione non venga bloccata, mentre le richieste vengono elaborate.

Il numero di richieste può variare in modo significativo nel tempo per diversi motivi. Un aumento improvviso delle attività utente o l'arrivo di richieste aggregate provenienti da più tenant può causare un carico di lavoro imprevisto. Nelle ore di punta, un sistema potrebbe dover elaborare molte centinaia di richieste al secondo, mentre in altri casi il numero potrebbe essere molto ridotto. Inoltre, la natura del lavoro eseguito per gestire le richieste può essere estremamente variabile. Usando una singola istanza del servizio consumer, è possibile che l'istanza diventi inondata dalle richieste. In alternativa, il sistema di messaggistica potrebbe essere sovraccaricato da un flusso di messaggi provenienti dall'applicazione. Per gestire questo carico di lavoro variabile, il sistema può eseguire più istanze del servizio consumer. Tuttavia, i consumer devono essere coordinati per garantire che ogni messaggio venga recapitato solo a un singolo consumer. Inoltre, occorre bilanciare il carico di lavoro tra i consumer per evitare che un'istanza diventi un collo di bottiglia.

Soluzione

Utilizzare una coda di messaggi per implementare il canale di comunicazione tra l'applicazione e le istanze del servizio consumer. L'applicazione invia le richieste alla coda sotto forma di messaggi e le istanze del servizio consumer ricevono i messaggi dalla coda e li elaborano. Questo approccio consente allo stesso pool di istanze del servizio consumer di gestire i messaggi provenienti da qualsiasi istanza dell'applicazione. La figura illustra l'uso di una coda di messaggi per distribuire il lavoro alle istanze di un servizio.

Uso di una coda di messaggi per la distribuzione del lavoro alle istanze di un servizio

Nota

Anche se sono presenti più consumer di questi messaggi, questo comportamento non corrisponde al modello Publish Subscribe (pub/sub). Con l'approccio Consumer concorrenti, ogni messaggio viene passato a un singolo consumer per l'elaborazione, mentre con l'approccio Pub/Sub, tutti i consumer vengono passati ogni messaggio.

Questa soluzione offre i vantaggi seguenti:

  • Offre un sistema con bilanciamento del carico che può gestire notevoli variazioni nel volume di richieste inviate da istanze dell'applicazione. La coda funge da buffer tra le istanze dell'applicazione e le istanze del servizio consumer. Questo buffer consente di ridurre al minimo l'impatto sulla disponibilità e sulla velocità di risposta, sia per l'applicazione che per le istanze del servizio. Per altre informazioni, vedere Modello di livellamento del carico basato su coda. La gestione di un messaggio che richiede operazioni di elaborazione a esecuzione prolungata non impedisce la gestione contemporanea di altri messaggi da parte di altre istanze del servizio consumer.

  • Migliora l'affidabilità. Se un producer comunica direttamente con un consumer invece di usare questo modello, ma non controlla il consumer, esiste una forte probabilità che messaggi vadano persi o non vengano elaborati se si verifica un errore del consumer. In questo modello, i messaggi non vengono inviati a una specifica istanza del servizio. Un'istanza del servizio non riuscita non bloccherà il producer e i messaggi possono essere elaborati da qualsiasi istanza funzionante del servizio.

  • Non richiede un coordinamento complesso tra i consumer o tra il producer e le istanze del consumer. La coda di messaggi assicura che ogni messaggio sia recapitato almeno una volta.

  • È scalabile. Quando si applica il ridimensionamento automatico, il sistema può aumentare o diminuire in modo dinamico il numero di istanze del servizio consumer man mano che il volume di messaggi varia.

  • Se la coda di messaggi fornisce operazioni di lettura transazionali, può migliorare la resilienza. Se un'istanza del servizio consumer che legge ed elabora il messaggio come parte di un'operazione transazionale ha esito negativo, questo modello può garantire che il messaggio verrà restituito alla coda per essere prelevato e gestito da un'altra istanza del servizio consumer. Per ridurre il rischio di errore continuo di un messaggio, è consigliabile usare code di messaggi non recapitabili.

Considerazioni e problemi

Prima di decidere come implementare questo modello, considerare quanto segue:

  • Ordinamento dei messaggi. L'ordine in cui le istanze del servizio consumer ricevono i messaggi non è garantito e non riflette necessariamente l'ordine in cui i messaggi sono stati creati. Progettare il sistema in modo da assicurarsi che l'elaborazione dei messaggi sia idempotente, perché ciò è utile per eliminare qualsiasi dipendenza rispetto all'ordine in cui vengono gestiti i messaggi. Per altre informazioni, vedere Idempotency Patterns (Modelli di Idempotenza) nel blog di Jonathon Oliver.

    Le code del bus di servizio di Microsoft Azure possono implementare l'ordinamento First In, First Out garantito dei messaggi mediante sessioni di messaggistica. Per altre informazioni, vedere Modelli di messaggistica mediante sessioni.

  • Progettazione dei servizi per la resilienza. Se il sistema è progettato per rilevare e riavviare le istanze di servizio non riuscite, può essere necessario implementare le operazioni di elaborazione eseguite dalle istanze del servizio come idempotenti, in modo da ridurre al minimo gli effetti se un singolo messaggio viene recuperato ed elaborato più di una volta.

  • Rilevamento dei messaggi non elaborabili. Un messaggio in formato non valido o un'attività che richiede l'accesso a risorse non disponibili può causare un errore in un'istanza del servizio. Il sistema deve impedire che tali messaggi siano restituiti alla coda e invece acquisirne e archiviarne i dettagli altrove, in modo che possano essere analizzati se necessario.

  • Gestione dei risultati. L'istanza del servizio che gestisce un messaggio è completamente separata dalla logica dell'applicazione che genera il messaggio e potrebbero non essere in grado di comunicare direttamente. Se l'istanza del servizio genera risultati che devono essere passati alla logica dell'applicazione, queste informazioni devono essere archiviate in un percorso accessibile da entrambe. Per evitare che logica dell'applicazione recuperi dati incompleti, il sistema deve segnalare il completamento dell'elaborazione.

    Se si usa Azure, un processo di lavoro può passare i risultati alla logica dell'applicazione usando una coda dedicata di risposte al messaggio. La logica dell'applicazione deve essere in grado di correlare questi risultati con il messaggio originale. Questo scenario è descritto più dettagliatamente in Introduzione alla messaggistica asincrona.

  • Scalabilità del sistema di messaggistica. In una soluzione su larga scala, una singola coda di messaggi può essere sovraccaricata dal numero di messaggi e diventare un collo di bottiglia nel sistema. In questo caso, è possibile partizionare il sistema di messaggistica in modo da inviare i messaggi provenienti da specifici producer a una determinata coda oppure usare il bilanciamento del carico per distribuire i messaggi tra più code di messaggi.

  • Affidabilità del sistema di messaggistica. Per garantire che dopo l'accodamento di un messaggio da parte dell'applicazione questo non vada perso, è necessario un sistema di messaggistica affidabile. Questo sistema è essenziale per garantire che tutti i messaggi vengano recapitati almeno una volta.

Quando usare questo modello

Usare questo modello quando:

  • Il carico di lavoro di un'applicazione è suddiviso in attività che è possibile eseguire in modo asincrono.
  • Le attività sono indipendenti e possono essere eseguite in parallelo.
  • Il volume di lavoro è estremamente variabile e per questo motivo è necessaria una soluzione scalabile.
  • La soluzione deve offrire disponibilità elevata e deve essere resiliente se l'elaborazione di un'attività ha esito negativo.

Questo modello potrebbe non essere utile quando:

  • Non è semplice separare il carico di lavoro dell'applicazione in attività distinte o esiste un livello elevato di dipendenza tra le attività.
  • Le attività devono essere eseguite in modo sincrono e la logica dell'applicazione deve attendere il completamento di un'attività prima di continuare.
  • Le attività devono essere eseguite in una sequenza specifica.

Alcuni sistemi di messaggistica supportano sessioni che consentono a un producer di raggruppare i messaggi e assicurarsi che siano tutti gestiti dallo stesso consumer. Questo meccanismo può essere usato con i messaggi classificati in ordine di priorità, se supportati, per implementare una forma di ordinamento che prevede il recapito dei messaggi in sequenza da un producer a un singolo consumer.

Progettazione del carico di lavoro

Un architetto deve valutare il modo in cui il modello Consumer concorrenti può essere usato nella progettazione del carico di lavoro per soddisfare gli obiettivi e i principi trattati nei pilastri di Azure Well-Architected Framework. Ad esempio:

Concetto fondamentale Come questo modello supporta gli obiettivi di pilastro
Le decisioni di progettazione dell'affidabilità consentono al carico di lavoro di diventare resilienti a malfunzionamenti e di assicurarsi che venga ripristinato in uno stato completamente funzionante dopo che si verifica un errore. Questo modello crea ridondanza nell'elaborazione delle code considerando i consumer come repliche, pertanto un errore dell'istanza non impedisce ad altri consumer di elaborare i messaggi della coda.

- Ridondanza RE:05
- RE:07 Processi in background
L'ottimizzazione dei costi è incentrata sul mantenimento e sul miglioramento del ritorno del carico di lavoro sugli investimenti. Questo modello consente di ottimizzare i costi abilitando il ridimensionamento basato sulla profondità della coda, fino a zero quando la coda è vuota. Può anche ottimizzare i costi consentendo di limitare il numero massimo di istanze consumer simultanee.

- Ottimizzazione della velocità co:05
- Costi dei componenti CO:07
L'efficienza delle prestazioni consente al carico di lavoro di soddisfare in modo efficiente le richieste tramite ottimizzazioni in termini di scalabilità, dati, codice. La distribuzione del carico tra tutti i nodi consumer aumenta l'utilizzo e il ridimensionamento dinamico in base alla profondità della coda, riducendo al minimo il provisioning eccessivo.

- PE:05 Ridimensionamento e partizionamento
- PE:07 Codice e infrastruttura

Come per qualsiasi decisione di progettazione, prendere in considerazione eventuali compromessi rispetto agli obiettivi degli altri pilastri che potrebbero essere introdotti con questo modello.

Esempio

Azure fornisce bus di servizio code e trigger della coda di funzioni di Azure che, se combinati, sono un'implementazione diretta di questo modello di progettazione cloud. Funzioni di Azure l'integrazione con bus di servizio di Azure tramite trigger e associazioni. L'integrazione con bus di servizio consente di creare funzioni che utilizzano i messaggi della coda inviati dai server di pubblicazione. Le applicazioni di pubblicazione inseriranno messaggi in una coda e i consumer, implementati come Funzioni di Azure, possono recuperare i messaggi da questa coda e gestirli.

Per la resilienza, una coda di bus di servizio consente a un consumer di usare PeekLock la modalità quando recupera un messaggio dalla coda. Questa modalità non rimuove effettivamente il messaggio, ma lo nasconde semplicemente ad altri consumer. Il runtime di Funzioni di Azure riceve un messaggio in modalità PeekLock, se la funzione termina correttamente chiama Complete sul messaggio oppure può chiamare Abandon se la funzione ha esito negativo e il messaggio diventerà nuovamente visibile, consentendo a un altro consumer di recuperarlo. Se la funzione viene eseguita per un periodo più lungo del timeout peekLock, il blocco viene rinnovato automaticamente finché la funzione è in esecuzione.

Funzioni di Azure può aumentare o ridurre il numero di istanze in base alla profondità della coda, che funge da consumer concorrenti della coda. Se vengono create più istanze delle funzioni, tutte competono eseguendo il pull e l'elaborazione dei messaggi in modo indipendente.

Per informazioni dettagliate sull'uso delle code del bus di servizio di Azure, vedereCode, argomenti e sottoscrizioni del bus di servizio.

Per informazioni sulle Funzioni di Azure attivate dalla coda, vedere trigger di bus di servizio di Azure per Funzioni di Azure.

Il codice seguente illustra come creare un nuovo messaggio e inviarlo a una coda bus di servizio usando un'istanza ServiceBusClient di .

private string serviceBusConnectionString = ...;
...

  public async Task SendMessagesAsync(CancellationToken  ct)
  {
   try
   {
    var msgNumber = 0;

    var serviceBusClient = new ServiceBusClient(serviceBusConnectionString);

    // create the sender
    ServiceBusSender sender = serviceBusClient.CreateSender("myqueue");

    while (!ct.IsCancellationRequested)
    {
     // Create a new message to send to the queue
     string messageBody = $"Message {msgNumber}";
     var message = new ServiceBusMessage(messageBody);

     // Write the body of the message to the console
     this._logger.LogInformation($"Sending message: {messageBody}");

     // Send the message to the queue
     await sender.SendMessageAsync(message);

     this._logger.LogInformation("Message successfully sent.");
     msgNumber++;
    }
   }
   catch (Exception exception)
   {
    this._logger.LogException(exception.Message);
   }
  }

L'esempio di codice seguente mostra un consumer, scritto come funzione di Azure C#, che legge i metadati dei messaggi e registra un messaggio di bus di servizio coda. Si noti come l'attributo ServiceBusTrigger viene usato per associarlo a una coda bus di servizio.

[FunctionName("ProcessQueueMessage")]
public static void Run(
    [ServiceBusTrigger("myqueue", Connection = "ServiceBusConnectionString")]
    string myQueueItem,
    Int32 deliveryCount,
    DateTime enqueuedTimeUtc,
    string messageId,
    ILogger log)
{
    log.LogInformation($"C# ServiceBus queue trigger function consumed message: {myQueueItem}");
    log.LogInformation($"EnqueuedTimeUtc={enqueuedTimeUtc}");
    log.LogInformation($"DeliveryCount={deliveryCount}");
    log.LogInformation($"MessageId={messageId}");
}

Passaggi successivi

  • Introduzione alla messaggistica asincrona. Le code di messaggi sono un meccanismo di comunicazione asincrona. Se un servizio consumer deve inviare una risposta a un'applicazione, può essere necessario implementare una forma di messaggistica di risposta. L'articolo Introduzione alla messaggistica asincrona contiene informazioni sull'implementazione della messaggistica di richiesta/risposta tramite code di messaggi.

  • Scalabilità automatica. Può essere possibile avviare e arrestare le istanze di un servizio consumer in base alla variazione della lunghezza della coda a cui le applicazioni inviano messaggi. La scalabilità automatica consente di mantenere la velocità effettiva durante i periodi di massima richiesta di elaborazione.

Per l'implementazione di questo modello possono risultare utili i modelli e le informazioni aggiuntive seguenti:

  • Compute Resource Consolidation pattern (Modello di consolidamento delle risorse di calcolo). Può essere possibile consolidare più istanze di un servizio consumer in un singolo processo, per ridurre i costi e il sovraccarico di gestione. L'articolo Modello di consolidamento delle risorse di calcolo descrive i vantaggi e gli svantaggi di questo approccio.

  • Schema di livellamento del carico basato sulle code. Introdurre una coda di messaggi può aggiungere resilienza al sistema, consentendo alle istanze del servizio di gestire un'ampia gamma di volumi di richieste provenienti dalle istanze dell'applicazione. La coda di messaggi funge da buffer, livellando il carico. L'articolo Modello di livellamento del carico basato sulle code descrive in dettaglio i vantaggi e gli svantaggi di questo scenario.