[Traduzione] Modello di threading di Envoy

Traduzione dell'articolo: Modello di threading di Envoy - https://blog.envoyproxy.io/envoy-threading-model-a8d44b922310

Ho trovato questo articolo piuttosto interessante e poiché Envoy viene spesso utilizzato come parte di "istio" o semplicemente come "controllore di ingresso" di Kubernetes, la maggior parte delle persone non ha la stessa interazione diretta con esso come, ad esempio, con i tipici Installazioni Nginx o Haproxy. Se però dovesse rompersi qualcosa, sarebbe bene capirne il funzionamento dall’interno. Ho cercato di tradurre quanto più testo possibile in russo, comprese le parole speciali; per coloro che trovano doloroso guardarlo, ho lasciato gli originali tra parentesi. Benvenuto al gatto.

La documentazione tecnica di basso livello per la base di codice Envoy è attualmente piuttosto scarsa. Per rimediare a questo, ho intenzione di pubblicare una serie di post sul blog sui vari sottosistemi di Envoy. Dato che questo è il primo articolo, per favore fatemi sapere cosa ne pensate e cosa potrebbe interessarvi nei prossimi articoli.

Una delle domande tecniche più comuni che ricevo su Envoy richiede una descrizione di basso livello del modello di threading utilizzato. In questo post descriverò il modo in cui Envoy mappa le connessioni ai thread, nonché il sistema di archiviazione locale dei thread che utilizza internamente per rendere il codice più parallelo e ad alte prestazioni.

Panoramica della filettatura

[Traduzione] Modello di threading di Envoy

Envoy utilizza tre diversi tipi di flussi:

  • Principale: Questo thread controlla l'avvio e la terminazione del processo, tutta l'elaborazione dell'API XDS (xDiscovery Service), inclusi DNS, controllo dello stato, gestione generale del cluster e del runtime, ripristino delle statistiche, amministrazione e gestione generale del processo - segnali Linux, riavvio a caldo, ecc. ciò che accade in questo thread è asincrono e "non bloccante". In generale, il thread principale coordina tutti i processi di funzionalità critici che non richiedono una grande quantità di CPU per essere eseguiti. Ciò consente di scrivere la maggior parte del codice di controllo come se fosse a thread singolo.
  • Lavoratore: Per impostazione predefinita, Envoy crea un thread di lavoro per ogni thread hardware nel sistema, questo può essere controllato utilizzando l'opzione --concurrency. Ogni thread di lavoro esegue un ciclo di eventi "non bloccante", che è responsabile dell'ascolto di ciascun ascoltatore; al momento in cui scrivo (29 luglio 2017) non è previsto lo sharding dell'ascoltatore, l'accettazione di nuove connessioni, l'istanziazione di uno stack di filtri per la connessione ed elaborare tutte le operazioni di input/output (IO) durante la durata della connessione. Ancora una volta, ciò consente di scrivere la maggior parte del codice di gestione della connessione come se fosse a thread singolo.
  • Pulitore di file: Ogni file scritto da Envoy, principalmente i registri di accesso, dispone attualmente di un thread di blocco indipendente. Ciò è dovuto al fatto che la scrittura sui file memorizzati nella cache dal file system anche durante l'utilizzo O_NONBLOCK a volte può bloccarsi (sigh). Quando i thread di lavoro devono scrivere su un file, i dati vengono effettivamente spostati in un buffer in memoria dove vengono infine scaricati attraverso il thread file a filo. Questa è un'area del codice in cui tecnicamente tutti i thread di lavoro possono bloccare lo stesso blocco mentre tentano di riempire un buffer di memoria.

Gestione della connessione

Come discusso brevemente in precedenza, tutti i thread di lavoro ascoltano tutti gli ascoltatori senza alcuno sharding. Pertanto, il kernel viene utilizzato per inviare con garbo i socket accettati ai thread di lavoro. I kernel moderni sono generalmente molto bravi in ​​questo, usano funzionalità come l'aumento della priorità di input/output (IO) per provare a riempire un thread con il lavoro prima di iniziare a utilizzare altri thread che sono anche in ascolto sullo stesso socket e inoltre non utilizzano round robin blocco (Spinlock) per elaborare ogni richiesta.
Una volta accettata una connessione su un thread di lavoro, non lascia mai quel thread. Tutta l'ulteriore elaborazione della connessione viene gestita interamente nel thread di lavoro, incluso qualsiasi comportamento di inoltro.

Ciò ha diverse conseguenze importanti:

  • Tutti i pool di connessioni in Envoy vengono assegnati a un thread di lavoro. Pertanto, sebbene i pool di connessioni HTTP/2 effettuino solo una connessione alla volta a ciascun host upstream, se sono presenti quattro thread di lavoro, ci saranno quattro connessioni HTTP/2 per host upstream in uno stato stazionario.
  • Il motivo per cui Envoy funziona in questo modo è che mantenendo tutto su un singolo thread di lavoro, quasi tutto il codice può essere scritto senza blocchi e come se fosse a thread singolo. Questo design semplifica la scrittura di grandi quantità di codice e si adatta incredibilmente bene a un numero quasi illimitato di thread di lavoro.
  • Tuttavia, uno degli aspetti principali è che, dal punto di vista del pool di memoria e dell'efficienza della connessione, è in realtà molto importante configurare il --concurrency. Avere più thread di lavoro del necessario sprecherà memoria, creerà più connessioni inattive e ridurrà la velocità del pool di connessioni. A Lyft, i nostri contenitori sidecar Envoy funzionano con una concorrenza molto bassa in modo che le prestazioni corrispondano approssimativamente ai servizi accanto a cui si trovano. Eseguiamo Envoy come proxy edge solo alla massima concorrenza.

Cosa significa non bloccante?

Il termine "non bloccante" è stato utilizzato più volte finora quando si discuteva del funzionamento dei thread principale e di lavoro. Tutto il codice è scritto presupponendo che nulla venga mai bloccato. Questo però non è del tutto vero (cosa non è del tutto vero?).

Envoy utilizza diversi blocchi di processi lunghi:

  • Come discusso, durante la scrittura dei log di accesso, tutti i thread di lavoro acquisiscono lo stesso blocco prima che il buffer del log in memoria venga riempito. Il tempo di mantenimento del blocco dovrebbe essere molto basso, ma è possibile che il blocco venga contestato in caso di concorrenza elevata e velocità effettiva elevata.
  • Envoy utilizza un sistema molto complesso per gestire le statistiche locali del thread. Questo sarà argomento di un post separato. Tuttavia, menzionerò brevemente che, come parte dell'elaborazione locale delle statistiche dei thread, a volte è necessario acquisire un blocco su un "archivio statistiche" centrale. Questo bloccaggio non dovrebbe mai essere necessario.
  • Il thread principale deve periodicamente coordinarsi con tutti i thread di lavoro. Questo viene fatto "pubblicando" dal thread principale ai thread di lavoro e talvolta dai thread di lavoro al thread principale. L'invio richiede un blocco in modo che il messaggio pubblicato possa essere messo in coda per la consegna successiva. Questi blocchi non dovrebbero mai essere contestati seriamente, ma tecnicamente possono comunque essere bloccati.
  • Quando Envoy scrive un registro nel flusso di errori di sistema (errore standard), acquisisce un blocco sull'intero processo. In generale, la registrazione locale di Envoy è considerata pessima dal punto di vista delle prestazioni, quindi non è stata prestata molta attenzione al suo miglioramento.
  • Esistono alcuni altri blocchi casuali, ma nessuno di essi è critico per le prestazioni e non dovrebbe mai essere messo in discussione.

Archiviazione locale del thread

A causa del modo in cui Envoy separa le responsabilità del thread principale dalle responsabilità del thread di lavoro, è necessario che l'elaborazione complessa possa essere eseguita sul thread principale e quindi fornita a ciascun thread di lavoro in modo altamente simultaneo. Questa sezione descrive Envoy Thread Local Storage (TLS) ad alto livello. Nella prossima sezione descriverò come viene utilizzato per gestire un cluster.
[Traduzione] Modello di threading di Envoy

Come già descritto, il thread principale gestisce praticamente tutte le funzionalità del piano di gestione e controllo nel processo Envoy. Il piano di controllo è un po' sovraccarico in questo caso, ma quando lo si osserva all'interno del processo Envoy stesso e lo si confronta con l'inoltro effettuato dai thread di lavoro, ha senso. La regola generale è che il processo del thread principale esegue del lavoro e quindi deve aggiornare ciascun thread di lavoro in base al risultato di tale lavoro. in questo caso non è necessario che il thread di lavoro acquisisca un blocco ad ogni accesso.

Il sistema TLS (Thread local storage) di Envoy funziona come segue:

  • Il codice in esecuzione sul thread principale può allocare uno slot TLS per l'intero processo. Sebbene questo sia astratto, in pratica è un indice in un vettore, che fornisce l'accesso O(1).
  • Il thread principale può installare dati arbitrari nel suo slot. Al termine, i dati vengono pubblicati in ciascun thread di lavoro come un normale evento del ciclo di eventi.
  • I thread di lavoro possono leggere dal proprio slot TLS e recuperare tutti i dati locali del thread disponibili lì.

Sebbene sia un paradigma molto semplice e incredibilmente potente, è molto simile al concetto di blocco RCU (Lettura-Copia-Aggiornamento). In sostanza, i thread di lavoro non vedono mai alcuna modifica ai dati negli slot TLS mentre il lavoro è in esecuzione. Il cambiamento avviene solo durante il periodo di riposo tra eventi lavorativi.

Envoy lo utilizza in due modi diversi:

  • Memorizzando dati diversi su ciascun thread di lavoro, è possibile accedere ai dati senza alcun blocco.
  • Mantenendo un puntatore condiviso ai dati globali in modalità di sola lettura su ogni thread di lavoro. Pertanto, ogni thread di lavoro dispone di un conteggio dei riferimenti ai dati che non può essere decrementato mentre il lavoro è in esecuzione. Solo quando tutti i lavoratori si calmeranno e caricheranno nuovi dati condivisi, i vecchi dati verranno distrutti. Questo è identico all'RCU.

Threading di aggiornamento del cluster

In questa sezione descriverò come viene utilizzato TLS (Thread local storage) per gestire un cluster. La gestione del cluster include l'elaborazione API xDS e/o DNS, nonché il controllo dello stato.
[Traduzione] Modello di threading di Envoy

La gestione del flusso del cluster include i seguenti componenti e passaggi:

  1. Cluster Manager è un componente di Envoy che gestisce tutti gli upstream dei cluster conosciuti, l'API Cluster Discovery Service (CDS), le API Secret Discovery Service (SDS) ed Endpoint Discovery Service (EDS), DNS e controlli esterni attivi. È responsabile della creazione di una vista "eventualmente coerente" di ciascun cluster upstream, che include gli host rilevati e lo stato di integrità.
  2. Il controllo dello stato esegue un controllo dello stato attivo e segnala le modifiche dello stato di integrità al gestore cluster.
  3. CDS (Cluster Discovery Service)/SDS (Secret Discovery Service)/EDS (Endpoint Discovery Service)/DNS vengono eseguiti per determinare l'appartenenza al cluster. La modifica dello stato viene restituita al gestore cluster.
  4. Ogni thread di lavoro esegue continuamente un ciclo di eventi.
  5. Quando il gestore cluster determina che lo stato di un cluster è cambiato, crea una nuova istantanea di sola lettura dello stato del cluster e la invia a ciascun thread di lavoro.
  6. Durante il successivo periodo di quiete, il thread di lavoro aggiornerà lo snapshot nello slot TLS allocato.
  7. Durante un evento I/O che dovrebbe determinare quale host bilanciare il carico, il sistema di bilanciamento del carico richiederà uno slot TLS (Thread local storage) per ottenere informazioni sull'host. Ciò non richiede serrature. Tieni inoltre presente che TLS può anche attivare eventi di aggiornamento in modo che i bilanciatori del carico e altri componenti possano ricalcolare cache, strutture dati, ecc. Questo va oltre lo scopo di questo post, ma viene utilizzato in vari punti del codice.

Utilizzando la procedura sopra descritta, Envoy può elaborare ogni richiesta senza alcun blocco (ad eccezione di quanto descritto in precedenza). A parte la complessità del codice TLS stesso, la maggior parte del codice non ha bisogno di comprendere come funziona il multithreading e può essere scritta a thread singolo. Ciò semplifica la scrittura della maggior parte del codice oltre a prestazioni superiori.

Altri sottosistemi che utilizzano TLS

TLS (Thread local storage) e RCU (Read Copy Update) sono ampiamente utilizzati in Envoy.

Esempi di utilizzo:

  • Meccanismo per modificare la funzionalità durante l'esecuzione: L'elenco corrente delle funzionalità abilitate viene calcolato nel thread principale. A ogni thread di lavoro viene quindi fornita uno snapshot di sola lettura utilizzando la semantica RCU.
  • Sostituzione delle tabelle di routing: per le tabelle di routing fornite da RDS (Route Discovery Service), le tabelle di routing vengono create nel thread principale. Lo snapshot di sola lettura verrà successivamente fornito a ciascun thread di lavoro utilizzando la semantica RCU (Read Copy Update). Ciò rende la modifica delle tabelle di routing atomicamente efficiente.
  • Caching dell'intestazione HTTP: A quanto pare, il calcolo dell'intestazione HTTP per ogni richiesta (durante l'esecuzione di ~25K+ RPS per core) è piuttosto costoso. Envoy calcola centralmente l'intestazione circa ogni mezzo secondo e la fornisce a ciascun lavoratore tramite TLS e RCU.

Esistono altri casi, ma gli esempi precedenti dovrebbero fornire una buona comprensione dello scopo per cui viene utilizzato TLS.

Insidie ​​prestazionali note

Sebbene Envoy funzioni abbastanza bene nel complesso, ci sono alcune aree degne di nota che richiedono attenzione quando viene utilizzato con concorrenza e throughput molto elevati:

  • Come descritto in questo articolo, attualmente tutti i thread di lavoro acquisiscono un blocco durante la scrittura nel buffer di memoria del log di accesso. In caso di concorrenza elevata e velocità effettiva elevata, sarà necessario raggruppare i log di accesso per ogni thread di lavoro a scapito del recapito fuori ordine durante la scrittura nel file finale. In alternativa, puoi creare un log di accesso separato per ogni thread di lavoro.
  • Sebbene le statistiche siano altamente ottimizzate, in caso di concorrenza e throughput molto elevati è probabile che si verifichi una contesa atomica sulle statistiche individuali. La soluzione a questo problema sono i contatori per thread di lavoro con ripristino periodico dei contatori centrali. Di questo si parlerà in un post successivo.
  • L'architettura attuale non funzionerà bene se Envoy viene distribuito in uno scenario in cui sono presenti pochissime connessioni che richiedono risorse di elaborazione significative. Non esiste alcuna garanzia che le connessioni verranno distribuite uniformemente tra i thread di lavoro. Questo problema può essere risolto implementando il bilanciamento della connessione di lavoro, che consentirà lo scambio di connessioni tra thread di lavoro.

Conclusione

Il modello di threading di Envoy è progettato per fornire facilità di programmazione e parallelismo massiccio a scapito di memoria e connessioni potenzialmente dispendiose se non configurate correttamente. Questo modello gli consente di funzionare molto bene con conteggi di thread e throughput molto elevati.
Come ho brevemente accennato su Twitter, il progetto può anche essere eseguito su uno stack di rete completo in modalità utente come DPDK (Data Plane Development Kit), che può far sì che server convenzionali gestiscano milioni di richieste al secondo con elaborazione L7 completa. Sarà molto interessante vedere cosa verrà costruito nei prossimi anni.
Un ultimo breve commento: mi è stato chiesto molte volte perché abbiamo scelto C++ per Envoy. Il motivo resta che è ancora l’unico linguaggio di livello industriale ampiamente utilizzato in cui è possibile costruire l’architettura descritta in questo post. Il C++ non è sicuramente adatto a tutti o addirittura a molti progetti, ma per alcuni casi d'uso è ancora l'unico strumento per portare a termine il lavoro.

Collegamenti al codice

Collegamenti a file con interfacce e implementazioni di intestazioni discusse in questo post:

Fonte: habr.com

Aggiungi un commento