Schema di coda di priorità

Bus di servizio di Azure

Assegnare una priorità alle richieste inviate ai servizi in modo che le richieste con una priorità più alta vengano ricevute ed elaborate più rapidamente rispetto a quelle con priorità più bassa. Questo modello è utile nelle applicazioni che offrono diverse garanzie del livello di servizio ai singoli client.

Contesto e problema

Le applicazioni possono delegare attività specifiche ad altri servizi, ad esempio per l'elaborazione in background o l'integrazione con altre applicazioni o altri servizi. Nel cloud, una coda di messaggi viene in genere usata per delegare attività all'elaborazione in background. In molti casi, l'ordine in cui le richieste vengono ricevute da un servizio non è importante. In alcuni casi è invece necessario assegnare una priorità a richieste specifiche. Queste richieste devono essere elaborate prima delle richieste con priorità inferiore inviate in precedenza dall'applicazione.

Soluzione

Una coda in genere è una struttura FIFO (First-In, First-Out) e i consumer in genere ricevono messaggi nello stesso ordine in cui vengono pubblicati nella coda. Tuttavia alcune code di messaggi supportano l'invio dei messaggi in base a una priorità. L'applicazione che pubblica un messaggio può assegnare una priorità. I messaggi nella coda vengono riordinati automaticamente in modo che quelli con priorità più alta vengano ricevuti prima di quelli con priorità più bassa. Questo diagramma illustra il processo:

Diagramma che illustra un meccanismo di accodamento che supporta la definizione delle priorità dei messaggi.

Nota

La maggior parte delle implementazioni della coda di messaggi supporta più consumer. (Vedere il Modello consumer concorrenti. Il numero di processi consumer può essere ridimensionato in base alla domanda.

Come soluzione alternativa nei sistemi che non supportano le code di messaggi basate sulla priorità è possibile gestire una coda separata per ogni priorità. L'applicazione è responsabile dell'inserimento dei messaggi nella coda appropriata. Ogni coda può avere un pool di consumer distinto. Le code con priorità più alta possono avere un pool più ampio di consumer che vengono eseguiti su hardware più veloce rispetto alle code con priorità inferiore. Questo diagramma illustra l'uso di code di messaggi separate per ogni priorità:

Diagramma che illustra l'uso di code di messaggi separate per ogni priorità.

Una variante di questa strategia consiste nell'implementare un singolo pool di consumer che controllano la presenza di messaggi in code con priorità elevata e solo dopo che iniziano a recuperare i messaggi da code con priorità inferiore. Esistono alcune differenze semantiche tra una soluzione che usa un singolo pool di processi consumer (con una singola coda che supporta messaggi con priorità diverse o con più code che gestiscono i messaggi di una singola priorità) e una soluzione che usa più code con un pool separato per ogni coda.

Nell'approccio a pool singolo, i messaggi con priorità più alta vengono sempre ricevuti ed elaborati prima dei messaggi con priorità più bassa. In teoria, i messaggi con priorità bassa potrebbero essere continuamente sostituiti e potrebbero non essere mai elaborati. Nell'approccio a più pool, i messaggi con priorità più bassa vengono sempre elaborati, ma non altrettanto rapidamente come i messaggi con priorità più alta (a seconda delle dimensioni relative dei pool e delle risorse disponibili per tali pool).

L'uso di un meccanismo di accodamento prioritario può offrire i vantaggi seguenti:

  • Consente alle applicazioni di soddisfare i requisiti aziendali che richiedono la definizione delle priorità della disponibilità o delle prestazioni, ad esempio offrendo diversi livelli di servizio a diversi gruppi di clienti.

  • Può contribuire a ridurre al minimo i costi operativi. Se si usa l'approccio a coda singola, è possibile ridurre il numero di consumer, se necessario. I messaggi con priorità alta vengono comunque elaborati per primi (anche se probabilmente più lentamente) e i messaggi con priorità più bassa potrebbero essere ritardati per più tempo. Se si implementa l'approccio a più code di messaggi con pool separati di consumer per ogni coda, è possibile ridurre il pool di consumer per le code con priorità più bassa. È anche possibile sospendere l'elaborazione per alcune code con priorità molto bassa arrestando tutti i consumer in ascolto dei messaggi in tali code.

  • L'approccio basato su più code di messaggi può contribuire a ottimizzare le prestazioni e la scalabilità dell'applicazione eseguendo il partizionamento dei messaggi in base ai requisiti di elaborazione. Ad esempio, è possibile classificare in ordine di priorità le attività critiche in modo che vengano gestite dai ricevitori che vengono eseguite immediatamente e le attività in background meno importanti possono essere gestite dai ricevitori pianificati per l'esecuzione in momenti meno occupati.

Considerazioni

Quando si decide come implementare questo modello, tenere presente quanto segue:

  • Definire le priorità nel contesto della soluzione. Ad esempio, un messaggio ad alta priorità può essere definito come un messaggio che deve essere elaborato entro 10 secondi. Identificare i requisiti per la gestione degli elementi con priorità elevata e le risorse da allocare per soddisfare i criteri.

  • Decidere se tutti gli elementi con priorità alta devono essere elaborati prima di qualsiasi elemento con priorità più bassa. Se i messaggi vengono elaborati da un singolo pool di consumer, è necessario fornire un meccanismo che può impedire e sospendere un'attività che gestisce un messaggio con priorità bassa se un messaggio con priorità più alta entra nella coda.

  • Nell'approccio a più code, quando si usa un singolo pool di processi consumer in ascolto su tutte le code anziché su un pool di consumer dedicato per ogni coda, il consumer deve applicare un algoritmo che garantisce che servizi sempre i messaggi provenienti da code con priorità più alta prima dei messaggi provenienti da code con priorità inferiore.

  • Monitorare la velocità di elaborazione sulle code con priorità alta e bassa per assicurarsi che i messaggi in tali code vengano elaborati alle tariffe previste.

  • Se è necessario garantire che i messaggi con priorità bassa vengano elaborati, implementare l'approccio a più code di messaggi con più pool di consumer. In alternativa, in una coda che supporta la definizione delle priorità dei messaggi, è possibile aumentare dinamicamente la priorità di un messaggio in coda man mano che viene eseguita. Questo approccio dipende però dalla coda di messaggi che fornisce questa funzionalità.

  • La strategia di utilizzo di code separate in base alla priorità dei messaggi è consigliata per i sistemi con poche priorità ben definite.

  • Il sistema può determinare logicamente le priorità dei messaggi. Ad esempio, invece di avere messaggi espliciti con priorità alta e bassa, è possibile designare i messaggi come "cliente a pagamento" o "cliente non a pagamento". Il sistema potrebbe quindi allocare più risorse all'elaborazione dei messaggi dai clienti a pagamento.

  • Potrebbe esserci un costo di elaborazione e finanziario associato al controllo di una coda per un messaggio. Ad esempio, alcuni sistemi di messaggistica commerciale addebita una piccola tariffa ogni volta che un messaggio viene pubblicato o recuperato e ogni volta che viene eseguita una query su una coda per i messaggi. Questo costo aumenta quando si controllano più code.

  • È possibile modificare dinamicamente le dimensioni di un pool di consumer in base alla lunghezza della coda di manutenzione del pool. Per ulteriori informazioni, consultare Linee guida per la scalabilità automatica.

Quando usare questo modello

Questo modello è utile in scenari in cui:

  • Il sistema deve gestire più attività con priorità diverse.

  • Utenti o tenant diversi devono essere gestiti con priorità diverse.

Progettazione del carico di lavoro

Un architetto deve valutare il modo in cui il modello di coda prioritaria 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. La separazione degli elementi in base alla priorità aziendale consente di concentrare le attività di affidabilità sul lavoro più critico.

- Flussi critici RE:02
- RE:07 Processi in background
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 separazione degli elementi in base alla priorità aziendale consente di concentrare le attività di prestazioni sul lavoro più sensibile al tempo.

- Flussi critici PE:09

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 non fornisce un meccanismo di accodamento che supporta in modo nativo la definizione automatica delle priorità dei messaggi tramite l'ordinamento. Fornisce tuttavia bus di servizio di Azure argomenti, bus di servizio sottoscrizioni che supportano un meccanismo di accodamento che fornisce il filtro dei messaggi e una gamma di funzionalità flessibili che rendono Azure ideale per le implementazioni più prioritarie delle code.

Una soluzione di Azure può implementare un argomento bus di servizio in cui un'applicazione può pubblicare messaggi, esattamente come in una coda.An Azure solution can implement a bus di servizio topic that an application can post messages to, just as it would post them to a queue. I messaggi possono contenere metadati in forma di proprietà personalizzate definite dall'applicazione. È possibile associare bus di servizio sottoscrizioni all'argomento e le sottoscrizioni possono filtrare i messaggi in base alle relative proprietà. Quando un'applicazione invia un messaggio a un argomento, il messaggio viene indirizzato alla sottoscrizione appropriata, in cui un consumer può leggerlo. I processi consumer possono recuperare messaggi da una sottoscrizione usando la stessa semantica usata con una coda di messaggi. Una sottoscrizione è una coda logica. Questo diagramma illustra come implementare una coda di priorità usando bus di servizio argomenti e sottoscrizioni:

Diagramma che illustra come implementare una coda di priorità usando bus di servizio argomenti e sottoscrizioni.

Nel diagramma precedente l'applicazione crea diversi messaggi e assegna una proprietà personalizzata denominata Priority in ogni messaggio. Priority ha un valore pari High a o Low. L'applicazione inserisce questi messaggi in un argomento. L'argomento include due sottoscrizioni associate che filtrano i messaggi in base alla Priority proprietà . Una sottoscrizione accetta messaggi con la Priority proprietà impostata su High. L'altro accetta messaggi con la Priority proprietà impostata su Low. Un pool di consumer legge i messaggi di ogni sottoscrizione. La sottoscrizione ad alta priorità ha un pool di dimensioni maggiori e questi consumer potrebbero essere in esecuzione in computer più potenti con risorse più disponibili rispetto ai computer per il pool con priorità bassa.

In questo esempio non c'è niente di speciale sulla designazione di messaggi con priorità alta e bassa. Sono semplicemente etichette specificate come proprietà in ogni messaggio. Vengono usati per indirizzare i messaggi a una sottoscrizione specifica. Se sono necessarie priorità aggiuntive, è relativamente facile creare più sottoscrizioni e pool di processi consumer per gestire tali priorità.

La soluzione PriorityQueue in GitHub si basa su questo approccio. Questa soluzione contiene progetti di funzioni di Azure denominati PriorityQueueConsumerHigh e PriorityQueueConsumerLow. Questi progetti di funzioni di Azure si integrano con bus di servizio tramite trigger e associazioni. Si connettono a sottoscrizioni diverse definite in ServiceBusTrigger e reagiscono ai messaggi in arrivo.

public static class PriorityQueueConsumerHighFn
{
    [FunctionName("HighPriorityQueueConsumerFunction")]
    public static void Run(
      [ServiceBusTrigger("messages", "highPriority", Connection = "ServiceBusConnection")]string highPriorityMessage,
      ILogger log)
    {
        log.LogInformation($"C# ServiceBus topic trigger function processed message: {highPriorityMessage}");
    }
}

In qualità di amministratore, è possibile configurare il numero di istanze a cui le funzioni in app Azure Servizio possono aumentare il numero di istanze. A tale scopo, è possibile configurare l'opzione Imponi limite di scalabilità orizzontale dalla portale di Azure, impostando un limite massimo di scalabilità orizzontale per ogni funzione. In genere è necessario avere più istanze della PriorityQueueConsumerHigh funzione rispetto alla PriorityQueueConsumerLow funzione. Questa configurazione garantisce che i messaggi con priorità elevata vengano letti dalla coda più rapidamente rispetto ai messaggi con priorità bassa.

Un altro progetto, PriorityQueueSender, contiene una funzione di Azure attivata dal tempo configurata per l'esecuzione ogni 30 secondi. Questa funzione si integra con bus di servizio tramite un'associazione di output e invia batch di messaggi con priorità bassa e alta a un IAsyncCollector oggetto . Quando la funzione invia messaggi all'argomento associato alle sottoscrizioni usate dalle PriorityQueueConsumerHigh funzioni e PriorityQueueConsumerLow , specifica la priorità usando la Priority proprietà personalizzata, come illustrato di seguito:

public static class PriorityQueueSenderFn
{
    [FunctionName("PriorityQueueSenderFunction")]
    public static async Task Run(
        [TimerTrigger("0,30 * * * * *")] TimerInfo myTimer,
        [ServiceBus("messages", Connection = "ServiceBusConnection")] IAsyncCollector<ServiceBusMessage> collector)
    {
        for (int i = 0; i < 10; i++)
        {
            var messageId = Guid.NewGuid().ToString();
            var lpMessage = new ServiceBusMessage() { MessageId = messageId };
            lpMessage.ApplicationProperties["Priority"] = Priority.Low;
            lpMessage.Body = BinaryData.FromString($"Low priority message with Id: {messageId}");
            await collector.AddAsync(lpMessage);

            messageId = Guid.NewGuid().ToString();
            var hpMessage = new ServiceBusMessage() { MessageId = messageId };
            hpMessage.ApplicationProperties["Priority"] = Priority.High;
            hpMessage.Body = BinaryData.FromString($"High priority message with Id: {messageId}");
            await collector.AddAsync(hpMessage);
        }
    }
}

Passaggi successivi

Quando si implementa questo modello, possono essere utili le risorse seguenti:

  • Esempio che illustra questo modello in GitHub.

  • Introduzione alla messaggistica asincrona. Un servizio consumer che elabora una richiesta potrebbe dover inviare una risposta all'istanza dell'applicazione che ha inviato la richiesta. Questo articolo fornisce informazioni sulle strategie che è possibile usare per implementare la messaggistica di richiesta/risposta.

  • Linee guida per la scalabilità automatica. A volte è possibile ridimensionare le dimensioni del pool di processi consumer che gestiscono una coda in base alla lunghezza della coda. Questa strategia consente di migliorare le prestazioni, in particolare per i pool che gestiscono messaggi con priorità elevata.

Quando si implementa questo modello, possono essere utili i modelli seguenti:

  • Modello di consumer concorrenti. Per aumentare la velocità effettiva delle code, è possibile implementare più consumer in ascolto sulle stesse attività di accodamento ed elaborazione in parallelo. Questi consumer competono per i messaggi, ma solo uno deve essere in grado di elaborare ogni messaggio. Questo articolo fornisce altre informazioni sui vantaggi e sugli svantaggi dell'implementazione di questo approccio.

  • Modello di limitazione. È possibile implementare la limitazione delle richieste mediante le code. È possibile usare la messaggistica con priorità per garantire che le richieste provenienti da applicazioni critiche o applicazioni eseguite da clienti di alto valore abbiano priorità rispetto alle richieste provenienti da applicazioni meno importanti.