[Traduzione] Modello di thread Envoy (Envoy threading model)

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

Questo articolo mi è sembrato piuttosto interessante, e dato che Envoy viene frequentemente utilizzato come parte di "istio" o semplicemente come "ingress controller" di Kubernetes, la maggior parte delle persone non ha interazioni dirette con esso come ad esempio con le installazioni standard di Nginx o Haproxy. Tuttavia, se qualcosa si rompe, sarebbe utile comprendere come è strutturato internamente. Ho cercato di tradurre il maggior numero possibile di testi in russo, compresi termini speciali, e per chi trova difficile vederli, ho lasciato gli originali tra parentesi. Benvenuti dopo il tag.

La documentazione tecnica di basso livello sulla base di codice di Envoy è attualmente piuttosto scarsa. Per correggere questo, intendo realizzare una serie di articoli nel blog sulle varie sottosistemi di Envoy. Essendo 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 è la richiesta di una descrizione di basso livello del modello di thread utilizzato. In questo post descriverò come Envoy associa le connessioni ai thread, oltre a una descrizione del sistema di archiviazione locale dei thread (Thread Local Storage), utilizzato internamente per rendere il codice più parallelo e ad alte prestazioni.

Panoramica dei thread (Threading overview)

[Traduzione] Modello di thread Envoy (Envoy threading model)

Envoy utilizza tre diversi tipi di thread:

  • Principale (Main): Questo thread gestisce l'avvio e la terminazione del processo, l'intera elaborazione delle API XDS (xDiscovery Service), compreso il DNS, il controllo dell'integrità (health checking), la gestione generale del cluster e del processo di runtime, il ripristino delle statistiche, l'amministrazione e la gestione generale dei processi — segnali Linux, riavvio a caldo (hot restart), ecc. Tutto ciò che accade in questo thread è asincrono e 'non bloccante'. In generale, il thread principale coordina tutti i processi critici di funzionalità, per i quali non è richiesto un elevato consumo di CPU. Questo consente di scrivere gran parte del codice di gestione come se fosse monothread.
  • Lavoratore (Worker): Per impostazione predefinita, Envoy crea un thread lavoratore (worker thread) per ogni thread hardware nel sistema, e questo può essere controllato tramite opzione --concorrenza. Ogni thread di lavoro avvia un ciclo di eventi "non bloccante" (event loop), responsabile dell'ascolto di ogni listener; al momento della scrittura di questo articolo (29 luglio 2017), non esiste segmentazione (sharding) dei listener, né accettazione di nuove connessioni, né creazione di un'istanza dello stack di filtri per la connessione e la gestione di tutte le operazioni di input-output (IO) durante la vita della connessione. Anche in questo caso, ciò consente di scrivere gran parte del codice di gestione delle connessioni come se fosse univoco.
  • File flusher: Ogni file scritto da Envoy, principalmente i log di accesso, attualmente ha un thread bloccante indipendente. Questo è dovuto al fatto che la scrittura su file cached dalla filesystem, anche con l'uso di O_NONBLOCK , può occasionalmente bloccarsi (sospirando). Quando ai thread di lavoro è necessario scrivere su un file, i dati sono effettivamente spostati in un buffer in memoria, dove alla fine vengono svuotati tramite il thread file flush. Questa è una delle aree del codice in cui tecnicamente tutti i thread di lavoro possono bloccarsi nello stesso lock, cercando di riempire il buffer di memoria.

Gestione delle connessioni

Come discusso brevemente sopra, tutti i thread di lavoro ascoltano tutti i listener senza alcuna segmentazione. Pertanto, il kernel viene utilizzato per indirizzare in modo efficace i socket accettati ai thread di lavoro. I kernel moderni sono generalmente molto bravi a fare questo, utilizzano funzionalità come l'aumento della priorità dell'I/O, cercando di riempire un thread di lavoro con attività prima di iniziare ad utilizzare altri thread che ascoltano lo stesso socket, evitando inoltre l'uso di spinlock per gestire ogni richiesta.
Una volta che una connessione è accettata su un thread di lavoro, non lascia mai quel thread. Tutta la successiva elaborazione della connessione è completamente gestita nel thread di lavoro, compreso qualsiasi comportamento di inoltro.

Questo ha diverse importanti conseguenze:

  • Tutte le pool di connessioni in Envoy appartengono a un flusso di lavoro. Pertanto, sebbene le pool di connessioni HTTP/2 stabiliscano solo una connessione con ciascun host upstream alla volta, se ci sono quattro flussi di lavoro, ci saranno quattro connessioni HTTP/2 all'host upstream in uno stato stabile.
  • Il motivo per cui Envoy funziona in questo modo è che mantenendo tutto in un unico flusso di lavoro, quasi tutto il codice può essere scritto senza blocchi e come se fosse monoritico. Questo design semplifica la scrittura di una grande quantità di codice e scala incredibilmente bene per quasi un numero illimitato di flussi di lavoro.
  • Tuttavia, una delle principali conclusioni è che, in termini di efficienza, la pool di memoria e di connessioni è davvero molto importante configurare il parametro --concorrenza. Avere più flussi di lavoro del necessario porterà a perdite di memoria, alla creazione di un numero maggiore di connessioni inattive e a una diminuzione della velocità di ingresso nella pool di connessioni. In Lyft, i nostri contenitori envoy sidecar funzionano con una concorrenza molto bassa, quindi le prestazioni sono all'incirca corrispondenti ai servizi accanto ai quali si trovano. Eseguiamo Envoy come proxy di confine (edge) solo al massimo della concorrenza.

Cosa significa non bloccante (What non-blocking means)

Il termine 'non bloccante' è stato utilizzato più volte nella discussione su come funzionano i flussi principale e di lavoro. Tutto il codice è scritto assumendo che nulla venga mai bloccato. Tuttavia, questo non è del tutto corretto (cosa non è del tutto corretto?).

Envoy utilizza diversi lunghi blocchi di processo:

  • Come già accennato, durante la registrazione dei log di accesso, tutti i flussi di lavoro ricevono lo stesso blocco prima di riempire il buffer del log in memoria. Il tempo di mantenimento del blocco deve essere molto basso, ma è possibile che questo blocco venga contestato con alta concorrenza e alta larghezza di banda.
  • Envoy utilizza un sistema molto complesso per elaborare le statistiche, che è locale per il flusso. Questo sarà l'argomento di un post separato. Tuttavia, accennerò brevemente che, come parte dell'elaborazione locale delle statistiche del flusso, a volte è necessario ottenere un blocco per il "magazzino delle statistiche" centrale. Questo blocco non dovrebbe mai essere richiesto.
  • Il flusso principale ha periodicamente bisogno di coordinarsi con tutti i flussi di lavoro. Questo avviene tramite la "pubblicazione" dal flusso principale ai flussi di lavoro e, talvolta, dai flussi di lavoro di nuovo al flusso principale. Per inviare è necessario un blocco, affinché il messaggio pubblicato possa essere inserito nella coda per la successiva consegna. Questi blocchi non dovrebbero mai essere soggetti a competizione seria, ma possono comunque tecnicamente essere bloccati.
  • Quando Envoy registra nel flusso di errore standard (standard error), ottiene un blocco dell'intero processo. In generale, la registrazione locale di Envoy è considerata terribile in termini di prestazioni, quindi non si presta molta attenzione al suo miglioramento.
  • Ci sono alcuni altri blocchi occasionali, ma nessuno di essi è critico per le prestazioni e non dovrebbe mai essere contestato.

Memoria locale del flusso (Thread local storage)

A causa del modo in cui Envoy separa le responsabilità del flusso principale da quelle del flusso di lavoro, c'è una richiesta che un'elaborazione complessa possa essere eseguita nel flusso principale e poi fornita a ciascun flusso di lavoro con un alto grado di parallelismo. In questa sezione viene descritta a un alto livello la sistemazione della Memoria Locale del Flusso di Envoy (TLS). Nella sezione successiva descriverò come viene utilizzata per la gestione del cluster.
[Traduzione] Modello di thread Envoy (Envoy threading model)

Come già descritto, il flusso principale gestisce praticamente tutte le funzioni di gestione e la funzionalità del piano di controllo nel processo Envoy. Il piano di controllo qui è leggermente sovraccarico, ma se considerato nell'ambito del processo Envoy stesso e confrontato con il forwarding eseguito dai flussi di lavoro, appare sensato. Come regola generale, il processo del flusso principale esegue un certo lavoro e poi deve aggiornare ogni flusso di lavoro in base al risultato di quel lavoro. In questo modo, il flusso di lavoro non ha bisogno di stabilire un blocco ad ogni accesso..

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

  • Il codice in esecuzione nel thread principale può allocare uno slot TLS per l'intero processo. Anche se questo è astratto, in pratica è un indice in un vettore che fornisce accesso O(1).
  • Il thread principale può impostare dati arbitrari nel suo slot. Una volta fatto questo, i dati vengono pubblicati in ogni thread di lavoro come un normale evento del ciclo degli eventi.
  • I thread di lavoro possono leggere dal loro slot TLS ed estrarre qualsiasi dato locale dei thread disponibile lì.

Sebbene questo sia un paradigma molto semplice e incredibilmente potente, simile al concetto di blocco RCU (Read-Copy-Update). In sostanza, i thread di lavoro non vedono mai cambiamenti nei dati negli slot TLS durante l'esecuzione del lavoro. La modifica avviene solo durante i periodi di inattività tra gli eventi di lavoro.

Envoy utilizza questo in due modi diversi:

  • Mantenendo diversi dati in ogni thread di lavoro, l'accesso a questi dati avviene senza alcun blocco.
  • Mantenendo un puntatore condiviso ai dati globali in modalità 'solo lettura' in ogni thread di lavoro. In questo modo, ogni thread di lavoro ha un contatore dei riferimenti ai dati che non può essere diminuito durante l'esecuzione del lavoro. Solo quando tutti i lavoratori si rilassano e caricano nuovi dati condivisi, i vecchi dati vengono distrutti. Questo è identico a RCU.

Thread di aggiornamento del cluster (Cluster update threading)

In questa sezione descriverò come TLS (Thread local storage) è usato per gestire il cluster. La gestione del cluster include l'elaborazione dell'API xDS e/o DNS, così come la verifica della salute (health checking).
[Traduzione] Modello di thread Envoy (Envoy threading model)

La gestione dei thread del cluster comprende i seguenti componenti e fasi:

  1. Il manager del cluster è un componente interno di Envoy che gestisce tutti gli upstream noti del cluster, le interfacce API CDS (Cluster Discovery Service), le interfacce API SDS (Secret Discovery Service) e EDS (Endpoint Discovery Service), DNS e verifiche attive della salute (health checking). È responsabile della creazione di una visione 'eventualmente consistente' (eventually consistent) di ogni upstream del cluster, che include gli host scoperti e lo stato di salute (health status).
  2. Lo strumento di verifica della salute (health checker) esegue un controllo attivo dello stato e comunica le modifiche dello stato al gestore del 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 manager del cluster.
  4. Ogni thread di lavoro esegue costantemente un ciclo di elaborazione degli eventi.
  5. Quando il manager del cluster determina che lo stato del cluster è cambiato, crea una nuova istantanea dello stato del cluster, disponibile solo in modalità di sola lettura, e la invia a ogni thread di lavoro.
  6. Durante il successivo periodo di inattività, il thread di lavoro aggiornerà l'istantanea nello slot TLS dedicato.
  7. Durante un evento di input/output che deve determinare l'host per il bilanciamento del carico, il bilanciatore di carico richiederà uno slot TLS (Thread local storage) per ottenere informazioni sull'host. Non sono necessarie chiusure. Si noti anche che il TLS può anche avviare eventi durante l'aggiornamento, in modo che i sottosistemi di bilanciamento del carico e altri componenti possano ricalcolare le cache, le strutture dati, ecc. Questo va oltre lo scopo di questo post, ma è utilizzato in vari punti del codice.

Utilizzando la procedura descritta sopra, Envoy può gestire ogni richiesta senza alcun blocco (eccetto quelli descritti in precedenza). Oltre alla complessità del codice TLS stesso, gran parte del codice non ha bisogno di comprendere come funziona il multithreading e può essere scritto in modalità thread singolo. Questo facilita la scrittura della maggior parte del codice, oltre a garantire prestazioni eccellenti.

Altri sottosistemi che utilizzano TLS

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

Esempi di utilizzo:

  • Meccanismo di cambiamento delle funzionalità a runtime: L'attuale elenco di funzionalità abilitate viene calcolato nel thread principale. Successivamente, a ciascun thread di lavoro viene fornita un'istantanea 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. Una copia di sola lettura sarà successivamente fornita a ciascun thread di lavoro utilizzando la semantica RCU (Read Copy Update). Questo rende la modifica delle tabelle di routing atomicamente efficiente.
  • Cache degli header HTTP: Come si scopre, il calcolo dell'header HTTP per ogni richiesta (con circa 25K+ RPS per core) è piuttosto costoso. Envoy calcola centralmente l'header circa ogni mezzo secondo e lo fornisce a ciascun worker tramite TLS e RCU.

Ci sono altri casi, ma gli esempi precedenti dovrebbero fornire una buona comprensione di come viene utilizzato TLS.

Conosciuti colli di bottiglia delle prestazioni

Sebbene in generale Envoy funzioni piuttosto bene, ci sono alcune aree note che richiedono attenzione quando viene utilizzato con un'alta concorrenza e larghezza di banda:

  • Come già descritto in questo articolo, attualmente tutti i thread di lavoro si bloccano quando scrivono nel buffer di memoria del log degli accessi. Con alta concorrenza e larga banda, sarà necessario eseguire il batching dei log degli accessi per ogni thread di lavoro, a scapito della consegna non ordinata quando si scrive nel file finale. Come alternativa, è possibile creare un log degli accessi separato per ciascun thread di lavoro.
  • Sebbene le statistiche siano notevolmente ottimizzate, con un'alta concorrenza e larghezza di banda ci sarà probabilmente una competizione atomica sulle statistiche individuali. La soluzione a questo problema è quella di avere contatori per un singolo thread di lavoro con il ripristino periodico dei contatori centrali. Questo verrà discusso in un post successivo.
  • L'architettura esistente non funzionerà bene se Envoy viene distribuito in uno scenario con molto poche connessioni che richiedono risorse significative per l'elaborazione. Non c'è garanzia che le connessioni siano distribuite uniformemente tra i thread di lavoro. Questo può essere risolto implementando il bilanciamento delle connessioni tra i worker, consentendo lo scambio di connessioni tra i thread di lavoro.

Conclusione

Il modello di threading di Envoy è progettato per garantire la semplicità di programmazione e il parallelismo massivo, a meno che non vengano configurati in modo appropriato, il che può portare a un uso eccessivo di memoria e connessioni. Questo modello gli consente di funzionare molto bene con un numero elevato di thread e larghezza di banda.
Come ho accennato brevemente su Twitter, il design può anche funzionare su uno stack di rete completo in modalità utente, come DPDK (Data Plane Development Kit), il che può portare i server comuni a gestire milioni di richieste al secondo con un'elaborazione completa L7. Sarà molto interessante vedere cosa verrà costruito nei prossimi anni.
Un'ultima osservazione rapida: mi è stato chiesto molte volte perché abbiamo scelto C++ per Envoy. La ragione è ancora che è l'unico linguaggio di livello industriale ampiamente adottato per costruire l'architettura descritta in questo post. C++ non è adatto per tutti o anche per molti progetti, ma per determinati casi d'uso è ancora lo strumento unico per portare a termine il lavoro.

Collegamenti al codice (Links to code)

Collegamenti ai file delle interfacce e delle implementazioni degli header discussi in questo post:

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster