[Перевод] Modello di threading di Envoy (Envoy threading model)

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

Questo articolo mi è sembrato abbastanza interessante e, dato che Envoy è spesso utilizzato come parte di "istio" o semplicemente come "ingress controller" in Kubernetes, la maggior parte delle persone non ha un'interazione diretta con esso come ad esempio avviene con installazioni standard di Nginx o Haproxy. Tuttavia, se qualcosa dovesse rompersi, sarebbe utile capire come funziona internamente. Ho cercato di tradurre il maggior numero possibile di parole in russo, comprese le terminologie specifiche; per coloro a cui danno fastidio ho lasciato gli originali tra parentesi. Benvenuti sotto il tag.

La documentazione tecnica a basso livello sulla base di codice di Envoy è attualmente piuttosto scarsa. Per rimediare a ciò, ho intenzione di creare una serie di articoli sul blog riguardanti i vari sottosistemi di Envoy. Poiché questo è il primo articolo, fatemi sapere cosa ne pensate e cosa potrebbe interessarvi negli articoli successivi.

Una delle domande tecniche più comuni che ricevo riguardo a Envoy è la richiesta di una descrizione dettagliata del modello di threading utilizzato. In questo articolo descriverò come Envoy collega le connessioni ai thread, nonché il sistema di memoria locale (Thread Local Storage) utilizzato internamente per rendere il codice più parallelo e ad alte prestazioni.

Panoramica sui thread

[Перевод] Modello di threading di Envoy (Envoy threading model)

Envoy utilizza tre diversi tipi di thread:

  • Principale (Main): Questo thread gestisce l'avvio e la chiusura del processo, tutta l'elaborazione dell'API XDS (xDiscovery Service), inclusi DNS, controlli di salute (health checking), gestione complessiva del cluster e del processo di runtime, azzeramento delle statistiche, amministrazione e gestione complessiva dei processi — segnali Linux, riavvio a caldo (hot restart) e così via. Tutto ciò che avviene in questo thread è asincrono e «non bloccante». In generale, il thread principale coordina tutti i processi critici di funzionalità, per i quali non è necessaria una grande quantità di CPU. Ciò consente di scrivere gran parte del codice di gestione come se fosse monothread.
  • Worker (Lavoratore): Per impostazione predefinita, Envoy crea un thread di lavoro per ogni thread hardware nel sistema, e questo può essere controllato tramite l'opzione --concurrency. 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) del listener, l'accettazione di nuove connessioni, la creazione di un'istanza dello stack dei filtri per la connessione e la gestione di tutte le operazioni di input/output (IO) finché la connessione è attiva. Ancora una volta, questo consente di scrivere buona parte del codice per la gestione delle connessioni come se fosse monothread.
  • File flusher (Svuotatore di file): Ogni file scritto da Envoy, principalmente i log di accesso, ha attualmente un thread bloccante indipendente. Questo perché la scrittura su file, anche con l'uso di 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 alla fine vengono svuotati tramite il thread file flush. È una delle aree del codice in cui tutti i flussi di lavoro tecnici (worker threads) possono bloccare la stessa risorsa (lock) nel tentativo di riempire il buffer di memoria.

Gestione delle connessioni (Connection handling)

Come discusso brevemente sopra, tutti i worker threads ascoltano tutti i listener senza alcuna segmentazione. In questo modo, il core è utilizzato per inviare correttamente i socket ricevuti ai worker threads. I core moderni sono generalmente molto bravi in questo, utilizzando funzioni come il potenziamento della priorità I/O per cercare di riempire un flusso di lavoro con attività prima di iniziare a utilizzare altri flussi che ascoltano lo stesso socket, evitando anche l'uso di spinlock per gestire ogni richiesta.
Una volta che una connessione è accettata da un worker thread, non lascia mai quel thread. Tutta la successiva elaborazione della connessione viene eseguita interamente nel worker thread, inclusi eventuali comportamenti di inoltro (forwarding behavior).

Ciò ha diverse conseguenze importanti:

  • Tutti i pool di connessione in Envoy appartengono a un thread di lavoro. Pertanto, anche se i pool di connessione HTTP/2 stabiliscono una sola connessione con ogni host upstream alla volta, se ci sono quattro thread di lavoro, ci saranno quattro connessioni HTTP/2 verso l'host upstream in uno stato stabile.
  • Il motivo per cui Envoy funziona in questo modo è che, mantenendo tutto in un unico thread di lavoro, quasi tutto il codice può essere scritto senza blocchi e come se fosse monofilo. Questo design semplifica la scrittura di una grande quantità di codice e scala incredibilmente bene per un numero quasi illimitato di thread di lavoro.
  • Tuttavia, una delle conclusioni principali è che, in termini di efficienza, è davvero importante configurare il parametro del pool di memoria e connessioni. --concurrency. La presenza di un numero maggiore di thread di lavoro del necessario può portare a una perdita di memoria, alla creazione di un numero eccessivo di connessioni inattive e a una riduzione della velocità di accesso al pool di connessioni. In Lyft, i nostri contenitori envoy sidecar operano con un parallelismo molto basso, in modo che le prestazioni siano approssimativamente in linea con i servizi con cui sono affiancati. Eseguiamo Envoy come server proxy edge solo con il massimo parallelismo.

Cosa significa modalità non bloccante

Il termine «non bloccante» è stato utilizzato più volte nella discussione su come funzionano i thread principali e di lavoro. Tutto il codice è scritto con la premessa che nulla venga mai bloccato. Tuttavia, ciò non è del tutto corretto.

Envoy utilizza diverse lunghe operazioni di blocco del processo:

  • Come già accennato, quando si registrano i log di accesso, tutti i thread di lavoro ricevono lo stesso blocco prima di riempire il buffer del log in memoria. Il tempo di mantenimento del blocco deve essere molto ridotto, ma è possibile che questo blocco venga conteso in situazioni di alta concorrenza e alta capacità di throughput.
  • Envoy utilizza un sistema molto complesso per l'elaborazione delle statistiche, che è locale per ogni thread. Questa sarà materia di un post a parte. Tuttavia, accennerò brevemente che, come parte dell'elaborazione locale delle statistiche del thread, a volte è necessario ottenere un blocco per il "magazzino statistico" centrale. Questo blocco non dovrebbe mai essere richiesto.
  • Il flusso principale ha occasionalmente bisogno di coordinarsi con tutti i flussi di lavoro. Questo avviene tramite la "pubblicazione" dal flusso principale ai flussi di lavoro e talvolta viceversa, dai flussi di lavoro al flusso principale. È necessaria una sincronizzazione per inviare un messaggio, in modo che il messaggio pubblicato possa essere accodato per una successiva consegna. Queste sincronizzazioni non dovrebbero mai subire competizioni significative, ma tecnicamente possono essere bloccate.
  • Quando Envoy scrive un log nel flusso di errore standard (standard error), acquisisce un blocco su tutto il processo. In generale, la registrazione locale di Envoy è considerata terribile dal punto di vista delle prestazioni, quindi non si presta molta attenzione al suo miglioramento.
  • Ci sono alcune altre sincronizzazioni occasionali, ma nessuna di esse è critica per le prestazioni e non deve mai essere contestata.

Memoria locale del thread (Thread local storage)

A causa del modo in cui Envoy separa le responsabilità del thread principale da quelle del thread di lavoro, c'è la necessità che il trattamento complesso possa essere eseguito nel thread principale e poi fornito a ciascun thread di lavoro con un alto grado di parallelismo. In questa sezione viene descritta a un alto livello la sistema di Envoy Thread Local Storage (TLS). Nella sezione successiva descriverò come viene utilizzato per gestire il cluster.
[Перевод] Modello di threading di Envoy (Envoy threading model)

Come già descritto, il thread principale gestisce praticamente tutte le funzioni di gestione e la funzionalità del piano di controllo nel processo Envoy. Il piano di controllo qui è un po' sovraccarico, ma considerandolo nel contesto del processo stesso di Envoy e confrontandolo con il forwarding effettuato dai thread di lavoro, questo appare sensato. In generale, il processo del thread principale esegue un certo lavoro, dopodiché deve aggiornare ogni thread di lavoro in base al risultato di quel lavoro, mentre il thread di lavoro non deve impostare un blocco ad ogni accesso..

Il sistema TLS (Thread Local Storage) di Envoy funziona come segue:

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

Sebbene sia una paradigma molto semplice e incredibilmente potente, è simile al concetto di lock RCU (Read-Copy-Update). Fondamentalmente, i thread di lavoro non vedono mai modifiche ai dati negli slot TLS durante l'esecuzione del lavoro. Le modifiche avvengono solo durante i periodi di inattività tra gli eventi di lavoro.

Envoy lo utilizza in due modi diversi:

  • Mantenendo dati diversi in ciascun thread di lavoro, l'accesso a questi dati avviene senza alcun blocco.
  • Mantenendo un puntatore globale ai dati in modalità 'sola lettura' su ogni thread di lavoro. In questo modo, ogni thread di lavoro ha un contatore di riferimenti ai dati che non può essere ridotto durante l'esecuzione del lavoro. Solo quando tutti i lavoratori si fermeranno e caricheranno nuovi dati comuni, i dati precedenti verranno distrutti. Questo è equivalente a RCU.

Thread di aggiornamento del cluster

In questa sezione descriverò come TLS (Thread Local Storage) è utilizzato per gestire il cluster. La gestione del cluster include la gestione delle API xDS e/o DNS, e il controllo della salute (health checking).
[Перевод] Modello di threading di Envoy (Envoy threading model)

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

  1. Il manager del cluster è un componente all'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 controlli attivi di salute. È responsabile della creazione di una rappresentazione 'eventualmente consistente' di ogni upstream del cluster, che include gli host scoperti e lo stato di salute.
  2. Il controllore di salute esegue controlli attivi di salute e riporta le variazioni nello stato di salute al manager 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 variazione di stato viene restituita al manager del cluster.
  4. Ogni thread di lavoro esegue continuamente 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 lettura, e la invia a ogni thread di lavoro.
  6. Durante il prossimo periodo di inattività, il flusso di lavoro aggiornerà lo snapshot nella slot TLS dedicata.
  7. Durante un evento di input/output, che deve identificare l'host per il bilanciamento del carico, il bilanciatore di carico richiederà la slot TLS (Thread local storage) per ottenere informazioni sull'host. Non sono necessarie blocchi per questo. Si noti inoltre che il TLS può anche generare eventi durante l'aggiornamento, in modo che i sottosistemi di bilanciamento del carico e altri componenti possano ricalcolare cache, strutture dati, ecc. Questo va oltre l'ambito di questo post, ma viene utilizzato in vari punti del codice.

Utilizzando la procedura descritta sopra, Envoy può gestire ogni richiesta senza blocchi (a parte quelli descritti 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 in modalità monocanale. 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 d'uso:

  • Meccanismo di modifica della funzionalità durante l'esecuzione: L'attuale elenco delle funzionalità abilitate viene calcolato nel thread principale. Ogni thread di lavoro riceve quindi un'istantanea di sola lettura utilizzando la semantica RCU.
  • Sostituzione delle tabelle di routing: per le tabelle di routing fornite dal RDS (Route Discovery Service), le tabelle di routing vengono create nel thread principale. Un'istantanea di sola lettura sarà successivamente fornita a ciascun thread di lavoro utilizzando la semantica RCU (Read Copy Update). Ciò rende l'aggiornamento delle tabelle di routing atomicamente efficiente.
  • Caching delle intestazioni HTTP: Si scopre che calcolare l'intestazione HTTP per ogni richiesta (con ~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.

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

Conosciuti punti critici delle prestazioni

Sebbene in generale Envoy funzioni piuttosto bene, ci sono alcune aree note che richiedono attenzione quando viene utilizzato con un alto livello di parallelismo e capacità:

  • Come già descritto in questo articolo, attualmente tutti i thread di lavoro vengono bloccati durante la scrittura nel buffer della memoria del log di accesso. Con un alto parallelismo e capacità, sarà necessario impacchettare i log di accesso per ogni thread di lavoro a discapito della consegna non ordinata durante la scrittura nel file finale. In alternativa, è possibile creare un log di accesso separato per ogni thread di lavoro.
  • Sebbene le statistiche siano state molto ottimizzate, con un alto parallelismo e capacità, è probabile che ci siano concorrenze atomiche sulle statistiche individuali. La soluzione a questo problema consiste in contatori per un singolo thread di lavoro con reset periodici dei contatori centrali. Questo sarà discusso in un post successivo.
  • L'architettura esistente non funzionerà bene se Envoy viene distribuito in uno scenario in cui ci sono poche connessioni che richiedono risorse significative per l'elaborazione. Non c'è garanzia che le connessioni saranno distribuite uniformemente tra i thread di lavoro. Questo può essere risolto implementando un bilanciamento delle connessioni di lavoro, consentendo lo scambio di connessioni tra i thread.

Conclusione

Il modello di thread di Envoy è progettato per garantire semplicità di programmazione e parallelismo massiccio, a costo di un possibile uso eccessivo di memoria e connessioni se non configurato correttamente. Questo modello permette a Envoy di funzionare molto bene con un numero estremamente elevato di thread e larghezza di banda.
Come ho accennato brevemente su Twitter, il design può anche funzionare sopra uno stack di rete full-feature in modalità utente, come DPDK (Data Plane Development Kit), il che può consentire a normali server di gestire milioni di richieste al secondo con un'elaborazione completa L7. Sarà molto interessante vedere cosa verrà costruito nei prossimi anni.
Un'ultima breve osservazione: mi è stato chiesto più volte perché abbiamo scelto C++ per Envoy. Il motivo rimane che è ancora l'unico linguaggio di livello industriale ampiamente utilizzato per costruire l'architettura descritta in questo post. C++ non è sicuramente adatto a tutti o anche a molti progetti, ma per alcuni casi d'uso è ancora l'unico strumento per portare a termine il lavoro.

Link al codice

Link ai file delle interfacce e delle implementazioni dei header discussi in questo post:

Fonte: habr.com

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