Coordinare le azioni eseguite da una raccolta di istanze di collaborazione in un'applicazione distribuita designando un'istanza come leader, con la responsabilità di gestire le altre, consente di garantire che le istanze non siano in conflitto tra loro, non provochino una contesa per le risorse condivise o non interferiscano inavvertitamente con il lavoro di altre istanze.
Contesto e problema
Un'applicazione cloud tipica include molte attività che agiscono in modo coordinato. Queste attività potrebbero essere tutte istanze che eseguono lo stesso codice e che richiedono l'accesso alle stesse risorse o potrebbero collaborare in parallelo per eseguire le singole parti di un calcolo complesso.
Le istanze dell'attività possono essere eseguite separatamente per gran parte del tempo, ma potrebbe anche essere necessario coordinare le azioni di ogni istanza per assicurarsi che non siano in conflitto, causare conflitti per le risorse condivise o interferire accidentalmente con il lavoro eseguito da altre istanze di attività.
Ad esempio:
- In un sistema basato su cloud che implementa la scalabilità orizzontale, più istanze della stessa attività potrebbero essere in esecuzione nello stesso momento con ogni istanza che serve un utente diverso. Se queste istanze scrivono in una risorsa condivisa, è necessario coordinarne le azioni per impedire che le diverse istanze sovrascrivano le modifiche apportate dalle altre.
- Se le attività eseguono singoli elementi di un calcolo complesso in parallelo, i risultati devono essere aggregati quando sono tutte completate.
Le istanze delle attività sono tutte pari, pertanto non c'è un leader naturale che possa agire da coordinatore o da aggregatore.
Soluzione
Una singola istanza dell'attività deve essere designata per fungere da leader e questa istanza deve coordinare le azioni delle altre istanze delle attività subordinate. Se tutte le istanze delle attività eseguono lo stesso codice, ognuna di esse sarà in grado di agire come leader. Pertanto, il processo elettorale deve essere gestito attentamente per evitare che due o più istanze assumessero la posizione di leader contemporaneamente.
Il sistema deve fornire un meccanismo efficiente per la scelta del leader. Questo metodo deve essere in grado di gestire eventi come interruzioni di rete o errori dei processi. In molte soluzioni le istanze delle attività subordinate monitorano il leader usando un metodo heartbeat o tramite polling. Se il leader designato termina in modo imprevisto o un errore di rete lo rende non disponibile per le istanze delle attività subordinate, è necessario poter designare un nuovo leader.
Esistono più strategie per scegliere un leader tra un set di attività in un ambiente distribuito, tra cui:
- Competizione per acquisire un mutex condiviso e distribuito. La prima istanza dell'attività che acquisisce il mutex è il leader. Tuttavia, il sistema deve garantire che, se il leader termina o viene disconnesso dal resto del sistema, il mutex viene rilasciato per consentire a un'altra istanza dell'attività di diventare leader. Questa strategia è illustrata nell'esempio riportato di seguito.
- Implementazione di uno degli algoritmi di elezione leader comuni, ad esempio l'algoritmo Bully, l'algoritmo raft consensus o l'algoritmo circolare. Questi algoritmi presuppongono che ciascun candidato alla designazione abbia un ID univoco e sia in grado di comunicare con altri candidati in modo affidabile.
Considerazioni e problemi
Prima di decidere come implementare questo modello, considerare quanto segue:
- Il processo di designazione di un leader deve essere resiliente agli errori temporanei e permanenti.
- Deve essere possibile rilevare quando il leader ha riscontrato un errore o è diventato non disponibile, ad esempio a causa di un problema di comunicazione. La rapidità con cui è necessario il rilevamento dipende dal sistema. Alcuni sistemi potrebbero essere in grado di funzionare in assenza di un leader per un breve periodo, durante il quale un errore temporaneo potrebbe essere corretto. In altri casi potrebbe essere necessario rilevare immediatamente l'errore del leader e attivare una nuova designazione.
- In un sistema che implementa il ridimensionamento orizzontale il leader potrebbe essere terminato se il sistema riduce il numero di alcune delle risorse di calcolo o le arresta.
- L'uso di un mutex distribuito condiviso introduce una dipendenza dal servizio esterno che fornisce il mutex. Il servizio costituisce un singolo punto di guasto. Se per qualsiasi motivo non risulta disponibile, il sistema non sarà in grado di designare un leader.
- L'uso di un singolo processo dedicato come leader è un approccio molto semplice. Tuttavia, se il processo non riesce potrebbe verificarsi un notevole ritardo durante il riavvio. La latenza risultante può influire sulle prestazioni e sui tempi di risposta di altri processi se sono in attesa del leader per il coordinamento di un'operazione.
- L'implementazione manuale di uno degli algoritmi di designazione leader assicura la massima flessibilità per l'ottimizzazione del codice.
- Evitare di fare del leader un collo di bottiglia nel sistema. Lo scopo del leader è coordinare il lavoro delle attività subordinate e non deve necessariamente partecipare a questo lavoro stesso, anche se dovrebbe essere in grado di farlo se l'attività non è eletto come leader.
Quando usare questo modello
Usare questo modello quando le attività in un'applicazione distribuita, ad esempio una soluzione ospitata nel cloud, necessitano di un attento coordinamento e non è presente alcun leader naturale.
Questo modello potrebbe non essere utile se:
- È presente un leader naturale o un processo dedicato che può sempre fungere da leader. Ad esempio, potrebbe essere possibile implementare un processo singleton che coordina le istanze delle attività. Se questo processo non riesce o risulta non integro, il sistema può arrestarlo e riavviarlo.
- Il coordinamento tra le attività può essere ottenuto usando un metodo più semplice. Ad esempio, se più istanze delle attività necessitano semplicemente dell'accesso coordinato a una risorsa condivisa, una soluzione migliore consiste nell'usare il blocco ottimistico o pessimistico per controllare l'accesso.
- Una soluzione di terze parti, ad esempio Apache Zookeeper , può essere una soluzione più efficiente.
Progettazione del carico di lavoro
Un architetto deve valutare il modo in cui il modello di elezione leader 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 riduce l'effetto di malfunzionamenti del nodo reindirizzando in modo affidabile il lavoro. Implementa anche il failover tramite algoritmi di consenso quando un leader non funziona correttamente. - Ridondanza RE:05 - RE:07 Riparazione automatica |
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
L'esempio relativo alle elezioni leader in GitHub illustra come usare un lease in un BLOB Archiviazione di Azure per fornire un meccanismo per implementare un mutex condiviso e distribuito. Questo mutex può essere usato per scegliere un leader tra un gruppo di istanze di lavoro disponibili. La prima istanza di acquisire il lease viene selezionata come leader e rimane il leader fino a quando non rilascia il lease o non è in grado di rinnovare il lease. Altre istanze del ruolo di lavoro possono continuare a monitorare il lease del BLOB nel caso in cui il leader non sia più disponibile.
Un lease del BLOB è un blocco di scrittura esclusivo su un BLOB. Un singolo BLOB può essere oggetto di un solo lease in qualsiasi punto nel tempo. Un'istanza del ruolo di lavoro può richiedere un lease su un BLOB specificato e verrà concesso il lease se nessun'altra istanza del ruolo di lavoro contiene un lease sullo stesso BLOB. In caso contrario, la richiesta genererà un'eccezione.
Per evitare che un'istanza leader con errori mantenga il lease per un periodo illimitato, specificare una durata per il lease. Alla scadenza, il lease diventerà disponibile. Tuttavia, mentre un'istanza contiene il lease, può richiedere che il lease venga rinnovato e che venga concesso il lease per un ulteriore periodo di tempo. L'istanza leader può ripetere continuamente questo processo se vuole conservare il lease. Per altre informazioni sul lease di un BLOB, vedere Lease Blob (REST API) (Lease Blob (API REST)).
La BlobDistributedMutex
classe nell'esempio C# seguente contiene il RunTaskWhenMutexAcquired
metodo che consente a un'istanza del ruolo di lavoro di tentare di acquisire un lease su un BLOB specificato. I dettagli del BLOB (nome, contenitore e account di archiviazione) vengono passati al costruttore in un oggetto BlobSettings
quando viene creato l'oggetto BlobDistributedMutex
(questo oggetto è uno struct semplice incluso nel codice di esempio). Il costruttore accetta anche un Task
oggetto che fa riferimento al codice che deve essere eseguito dall'istanza del ruolo di lavoro se acquisisce correttamente il lease sul BLOB e viene eletto il leader. Si noti che il codice che gestisce i dettagli di basso livello dell'acquisizione del lease viene implementato in una classe helper distinta denominata BlobLeaseManager
.
public class BlobDistributedMutex
{
...
private readonly BlobSettings blobSettings;
private readonly Func<CancellationToken, Task> taskToRunWhenLeaseAcquired;
...
public BlobDistributedMutex(BlobSettings blobSettings,
Func<CancellationToken, Task> taskToRunWhenLeaseAcquired, ... )
{
this.blobSettings = blobSettings;
this.taskToRunWhenLeaseAcquired = taskToRunWhenLeaseAcquired;
...
}
public async Task RunTaskWhenMutexAcquired(CancellationToken token)
{
var leaseManager = new BlobLeaseManager(blobSettings);
await this.RunTaskWhenBlobLeaseAcquired(leaseManager, token);
}
...
Il metodo RunTaskWhenMutexAcquired
nell'esempio di codice precedente richiama il metodo RunTaskWhenBlobLeaseAcquired
illustrato nell'esempio di codice seguente per acquisire effettivamente il lease. Il metodo RunTaskWhenBlobLeaseAcquired
viene eseguito in modo asincrono. Se il lease viene acquisito correttamente, l'istanza del ruolo di lavoro è stata selezionata come leader. Lo scopo del taskToRunWhenLeaseAcquired
delegato è eseguire il lavoro che coordina le altre istanze del ruolo di lavoro. Se il lease non viene acquisito, un'altra istanza del ruolo di lavoro è stata selezionata come leader e l'istanza del ruolo di lavoro corrente rimane un subordinato. Si noti che il metodo TryAcquireLeaseOrWait
è un metodo helper che usa l'oggetto BlobLeaseManager
per acquisire il lease.
private async Task RunTaskWhenBlobLeaseAcquired(
BlobLeaseManager leaseManager, CancellationToken token)
{
while (!token.IsCancellationRequested)
{
// Try to acquire the blob lease.
// Otherwise wait for a short time before trying again.
string? leaseId = await this.TryAcquireLeaseOrWait(leaseManager, token);
if (!string.IsNullOrEmpty(leaseId))
{
// Create a new linked cancellation token source so that if either the
// original token is canceled or the lease can't be renewed, the
// leader task can be canceled.
using (var leaseCts =
CancellationTokenSource.CreateLinkedTokenSource(new[] { token }))
{
// Run the leader task.
var leaderTask = this.taskToRunWhenLeaseAcquired.Invoke(leaseCts.Token);
...
}
}
}
...
}
Anche l'attività avviata dal leader viene eseguita in modo asincrono. Durante l'esecuzione dell'attività, il metodo RunTaskWhenBlobLeaseAcquired
illustrato nell'esempio di codice seguente tenta periodicamente di rinnovare il lease. Ciò consente di garantire che l'istanza del ruolo di lavoro rimanga leader. Nella soluzione di esempio, il ritardo tra le richieste di rinnovo è inferiore al tempo specificato per la durata del lease per impedire che un'altra istanza del ruolo di lavoro venga selezionata come leader. Se il rinnovo non riesce per qualsiasi motivo, l'attività specifica del leader viene annullata.
Se il lease non viene rinnovato o l'attività viene annullata (probabilmente a causa dell'arresto dell'istanza del ruolo di lavoro), il lease viene rilasciato. A questo punto, questa o un'altra istanza del ruolo di lavoro può essere selezionata come leader. L'estratto di codice seguente illustra questa parte del processo.
private async Task RunTaskWhenBlobLeaseAcquired(
BlobLeaseManager leaseManager, CancellationToken token)
{
while (...)
{
...
if (...)
{
...
using (var leaseCts = ...)
{
...
// Keep renewing the lease in regular intervals.
// If the lease can't be renewed, then the task completes.
var renewLeaseTask =
this.KeepRenewingLease(leaseManager, leaseId, leaseCts.Token);
// When any task completes (either the leader task itself or when it
// couldn't renew the lease) then cancel the other task.
await CancelAllWhenAnyCompletes(leaderTask, renewLeaseTask, leaseCts);
}
}
}
}
...
}
Il metodo KeepRenewingLease
è un altro metodo helper che usa l'oggetto BlobLeaseManager
per rinnovare il lease. Il metodo CancelAllWhenAnyCompletes
annulla le attività specificate come i primi due parametri. Il diagramma seguente illustra l'uso della classe BlobDistributedMutex
per la designazione di un leader e l'esecuzione di un'attività che coordina le operazioni.
Nell'esempio di codice seguente viene illustrato come usare la BlobDistributedMutex
classe all'interno di un'istanza del ruolo di lavoro. Questo codice acquisisce un lease su un BLOB denominato MyLeaderCoordinatorTask
nel contenitore del lease Archiviazione BLOB di Azure e specifica che il codice definito nel MyLeaderCoordinatorTask
metodo deve essere eseguito se l'istanza del ruolo di lavoro è selezionata come leader.
// Create a BlobSettings object with the connection string or managed identity and the name of the blob to use for the lease
BlobSettings blobSettings = new BlobSettings(storageConnStr, "leases", "MyLeaderCoordinatorTask");
// Create a new BlobDistributedMutex object with the BlobSettings object and a task to run when the lease is acquired
var distributedMutex = new BlobDistributedMutex(
blobSettings, MyLeaderCoordinatorTask);
// Wait for completion of the DistributedMutex and the UI task before exiting
await distributedMutex.RunTaskWhenMutexAcquired(cancellationToken);
...
// Method that runs if the worker instance is elected the leader
private static async Task MyLeaderCoordinatorTask(CancellationToken token)
{
...
}
Tenere presente i punti seguenti riguardo alla soluzione di esempio:
- Il BLOB è un singolo punto di guasto potenziale. Se il servizio BLOB non è più disponibile o non è accessibile, il responsabile non sarà in grado di rinnovare il lease e nessun'altra istanza del ruolo di lavoro sarà in grado di acquisire il lease. In questo caso, nessuna istanza del ruolo di lavoro sarà in grado di fungere da leader. Tuttavia, il servizio BLOB è progettato per essere resiliente, pertanto un guasto completo del servizio BLOB è considerato estremamente improbabile.
- Se l'attività eseguita dal leader si blocca, il leader potrebbe continuare a rinnovare il lease, impedendo a qualsiasi altra istanza del ruolo di lavoro di acquisire il lease e di assumere la posizione leader per coordinare le attività. Nel mondo reale è necessario verificare l'integrità del leader a intervalli frequenti.
- Il processo di designazione è non deterministico. Non è possibile fare ipotesi su quale istanza del ruolo di lavoro acquisirà il lease del BLOB e diventerà leader.
- Il BLOB usato come destinazione del lease del BLOB non dovrebbe essere usato per altri scopi. Se un'istanza del ruolo di lavoro tenta di archiviare i dati in questo BLOB, questi dati non saranno accessibili a meno che l'istanza del ruolo di lavoro non sia leader e contenga il lease del BLOB.
Passaggi successivi
Per l'implementazione di questo modello possono risultare utili le informazioni aggiuntive seguenti:
- Questo modello ha un'applicazione di esempio scaricabile.
- Scalabilità automatica. È possibile avviare e arrestare le istanze degli host delle attività di pari passo con la variazione del carico sull'applicazione. La scalabilità automatica consente di mantenere la velocità effettiva e le prestazioni durante i periodi di massima richiesta di elaborazione.
- Modello asincrono basato su attività.
- Esempio che illustra l'algoritmo Bully.
- Esempio che illustra l'algoritmo Ring.
- Apache Curator, una libreria client per Apache ZooKeeper.
- Articolo Lease Blob (REST API) (Lease Blob (API REST)) su MSDN.