Bioyino — aggregatore di metriche distribuito e scalabile

Quindi, stai raccogliendo metriche. Proprio come noi. Anche noi raccogliamo metriche. Certamente quelle necessarie per il business. Oggi parleremo del primo anello del nostro sistema di monitoraggio: un server di aggregazione compatibile con statsd bioyino, perché lo abbiamo scritto e perché abbiamo rinunciato a brubeck.

Bioyino — aggregatore di metriche distribuito e scalabile

Dai nostri articoli precedenti (1, 2) puoi scoprire che fino a un certo punto abbiamo raccolto i tag tramite brubeck. È scritto in C. Dal punto di vista del codice, è semplice come un tappo (questo è importante quando vuoi contribuire) e, soprattutto, gestisce senza particolari problemi i nostri volumi di 2 milioni di metriche al secondo (MPS) al picco. La documentazione afferma il supporto per 4 milioni di MPS con asterisco. Ciò significa che il numero dichiarato lo otterrai se configuri correttamente la rete su Linux. (Quante MPS è possibile ottenere lasciando la rete così com'è, non lo sappiamo). Nonostante questi vantaggi, avevamo diverse serie lamentele su brubeck.

Lamentela 1. Github - il sviluppatore del progetto - ha smesso di supportarlo: pubblicare patch e fix, accettare i nostri e (non solo i nostri) PR. Negli ultimi mesi (da febbraio-marzo 2018 circa) l'attività è ripresa, ma prima c'è stata quasi 2 anni di completo silenzio. Inoltre, il progetto è sviluppato per esigenze interne di Gihub, il che potrebbe rappresentare un serio ostacolo per l'implementazione di nuove funzionalità.

Lamentela 2. Precisione dei calcoli. Brubeck raccoglie per l'aggregazione solo 65536 valori. Nel nostro caso, per alcune metriche durante il periodo di aggregazione (30 secondi) possono arrivare molti più valori (1.527.392 al picco). Di conseguenza, i valori dei massimi e minimi sembrano inutili. Ad esempio, ecco come dovrebbe essere:

Bioyino — aggregatore di metriche distribuito e scalabile
Come riportato

Bioyino — aggregatore di metriche distribuito e scalabile
Come dovrebbe essere

Per lo stesso motivo, i totali vengono calcolati in modo errato. Aggiungi qui il bug del sovraccarico del float a 32 bit, che manda il server in segfault quando riceve una metrica apparentemente innocua, e diventa davvero ottimo. Il bug, tra l'altro, non è stato ancora risolto.

E, infine, Lamentela X. Al momento della stesura dell'articolo siamo pronti a presentare a tutti le 14 implementazioni di statsd funzionanti che siamo riusciti a trovare. Immaginiamo che una certa infrastruttura sia cresciuta così tanto che accettare 4 milioni di MPS non è più sufficiente. Oppure che non sia ancora cresciuta, ma che le metriche siano già così importanti che anche brevi interruzioni di 2-3 minuti nei grafici possano diventare critiche e causare attacchi di profonda depressione nei manager. Poiché curare la depressione è un compito ingrato, sono necessarie soluzioni tecniche.

In primo luogo, la tolleranza ai guasti, per evitare che un problema improvviso sul server scateni un'apocalisse zombie psichiatrica in ufficio. In secondo luogo, la scalabilità, per avere la possibilità di accettare più di 4 milioni di MPS, senza dover scavare a fondo nel stack di rete di Linux e poter crescere 'in larghezza' fino alle dimensioni necessarie.

Poiché avevamo margine in termini di scalabilità, abbiamo deciso di partire dalla tolleranza ai guasti. 'Oh! Tolleranza ai guasti! È semplice, lo sappiamo fare', abbiamo pensato e abbiamo avviato 2 server, installando su ciascuno una copia di brubeck. Per fare questo, ci è stato necessario copiare il traffico con le metriche su entrambi i server e persino scrivere per questo un piccolo utilitario. Abbiamo risolto il problema della tolleranza ai guasti, ma... non molto bene. All'inizio sembrava che andasse tutto bene: ogni brubeck raccoglie la propria versione dell'aggregazione, scrive i dati in Graphite ogni 30 secondi, sovrascrivendo l'intervallo precedente (questo avviene lato Graphite). Se un server fallisce, abbiamo sempre il secondo con la propria copia dei dati aggregati. Ma c'è un problema: se il server fallisce, nei grafici appare una 'sega'. Questo è dovuto al fatto che gli intervalli di 30 secondi di brubeck non sono sincronizzati, e nel momento in cui uno di essi va giù, non viene sovrascritto. Quando si avvia il secondo server, succede la stessa cosa. È abbastanza tollerabile, ma si desidera qualcosa di meglio! Anche il problema della scalabilità non è scomparso. Tutte le metriche continuano a 'volare' su un singolo server, e quindi siamo limitati agli stessi 2-4 milioni di MPS a seconda della potenza della rete.

Se si riflette un po' sul problema e si inizia a scavare nella neve, può venire in mente un'idea piuttosto ovvia: è necessario uno statsd in grado di funzionare in modalità distribuita. Ovvero, uno che implementi la sincronizzazione tra le nodi in base al tempo e alle metriche. "Certo, una soluzione del genere deve già esistere", abbiamo pensato, e abbiamo iniziato a cercare su Google... ma non abbiamo trovato nulla. Analizzando la documentazione di vari statsd (https://github.com/etsy/statsd/wiki#server-implementations al 11.12.2017), non abbiamo trovato assolutamente nulla. Evidentemente, né gli sviluppatori né gli utenti di queste soluzioni si sono ancora trovati con un numero COSÌ elevato di metriche, altrimenti avrebbero sicuramente inventato qualche cosa.

E qui ci siamo ricordati dello statsd "giocattolo" - bioyino, che avevamo scritto durante un hackathon solo per divertimento (il nome del progetto è stato generato da uno script prima dell'inizio dell'hackathon) e abbiamo capito che avevamo urgentemente bisogno di un nostro statsd. Perché?

  • perché nel mondo ci sono troppo pochi cloni di statsd,
  • perché possiamo garantire l'affidabilità e la scalabilità desiderate (inclusa la sincronizzazione delle metriche aggregate tra i server e la risoluzione dei conflitti durante l'invio),
  • perché possiamo calcolare le metriche con maggiore precisione rispetto a quanto fa brubeck,
  • perché possiamo raccogliere statistiche più dettagliate, che brubeck praticamente non ci forniva,
  • perché abbiamo avuto l'opportunità di programmare la nostra application distribuita ad alte prestazioni, che non ripeterà completamente l'architettura di un'altra similare.

Su cosa scrivere? Naturalmente, su Rust. Perché?

  • perché esisteva già un prototipo della soluzione,
  • perché l'autore dell'articolo all'epoca sapeva già Rust ed era desideroso di scrivere qualcosa di utile in produzione con l'intenzione di rilasciarlo come open-source,
  • perché i linguaggi con GC non ci sono adatti a causa della natura del traffico ricevuto (praticamente in tempo reale) e le pause dovute al GC sono praticamente inaccettabili,
  • perché è necessaria la massima performance, paragonabile a C,
  • perché Rust ci offre la concurrency senza paura, e iniziando a scrivere in C/C++, avremmo accumulato ancora più vulnerabilità, overflow di buffer, condizioni di race e altre parole spaventose rispetto a brubeck.

C'era anche un'argomentazione contro Rust. L'azienda non aveva esperienza nella creazione di progetti in Rust e al momento non prevediamo nemmeno di utilizzarlo nel nostro progetto principale. Pertanto, c'erano serie preoccupazioni che nulla sarebbe andato a buon fine, ma abbiamo deciso di rischiare e provare.

Il tempo passava…

Finalmente, dopo diversi tentativi falliti, la prima versione funzionante era pronta. Cosa ne è venuto fuori? È venuto fuori questo.

Bioyino — aggregatore di metriche distribuito e scalabile

Ogni nodo riceve il proprio set di metriche e le accumula, senza aggregare le metriche per quei tipi in cui per l'aggregazione finale è necessario avere il set completo. I nodi sono collegati tra loro tramite un protocollo di lock distribuito, che consente di scegliere tra di essi quello unico (qui abbiamo pianto) che è degno di inviare metriche al Grande. Attualmente, questo problema viene affrontato con Consul, ma in futuro le ambizioni dell'autore si estendono a un proprio realizzazione Raft, dove quello degno sarà, ovviamente, il nodo-leader del consenso. Oltre al consenso, i nodi inviano abbastanza frequentemente (di default una volta al secondo) ai propri vicini le parti delle metriche preaggregati che sono riusciti a raccogliere in quel secondo. Quindi, la scalabilità e la tolleranza ai guasti sono mantenute: ciascuno dei nodi conserva ancora un set completo di metriche, ma le metriche vengono inviate già aggregate, tramite TCP e con codifica in un protocollo binario, riducendo così notevolmente i costi di duplicazione rispetto a UDP. Nonostante l'ampia quantità di metriche in ingresso, l'accumulo richiede davvero poca memoria e ancor meno CPU. Per le nostre metriche ben comprimibili, si tratta di appena alcune decine di megabyte di dati. Come ulteriore vantaggio, abbiamo l'assenza di registrazioni ridondanti nei dati in Graphite, come avveniva nel caso di burbeck.

I pacchetti UDP con metriche sono bilanciati tra i nodi sull'hardware di rete attraverso un semplice Round Robin. Naturalmente, l'hardware di rete non analizza il contenuto dei pacchetti e quindi può gestire molti più di 4M pacchetti al secondo, senza contare le metriche di cui non sa nulla. Considerando che le metriche arrivano in numero maggiore a una per pacchetto, non prevediamo problemi di prestazioni in questo ambito. In caso di caduta del server, il dispositivo di rete rileva rapidamente (entro 1-2 secondi) questo fatto e rimuove il server non funzionante dal ciclo. Di conseguenza, i nodi passivi (cioè non leader) possono essere accesi e spenti praticamente senza notare cali sui grafici. Il massimo che perdiamo è una parte delle metriche arrivate nell'ultimo secondo. Una perdita/interruzione/cambio improvviso del leader porterà comunque a un'anomalia lieve (l'intervallo di 30 secondi è ancora desincronizzato), ma con la connessione tra i nodi è possibile ridurre al minimo anche questi problemi, ad esempio, inviando pacchetti di sincronizzazione.

Un po' sull'architettura interna. L'applicazione è ovviamente multithread, ma l'architettura dei thread è diversa da quella utilizzata in brubeck. I thread in brubeck sono uniformi: ciascuno di essi è responsabile sia della raccolta delle informazioni che dell'aggregazione. In bioyino, i thread di lavoro (workers) sono divisi in due gruppi: quelli responsabili della rete e quelli responsabili dell'aggregazione. Questa divisione consente una gestione più flessibile dell'applicazione a seconda del tipo di metriche: dove è necessaria un'aggregazione intensiva, si possono aggiungere aggregatori, mentre dove c'è molto traffico di rete, si può aumentare il numero di thread di rete. Attualmente, sui nostri server stiamo operando con 8 thread di rete e 4 thread di aggregazione.

La parte calcolante (responsabile dell'aggregazione) è piuttosto noiosa. I buffer riempiti dai thread di rete vengono distribuiti tra i thread di calcolo, dove vengono successivamente analizzati e aggregati. Su richiesta, le metriche vengono restituite per l'invio ad altri nodi. Tutto questo, compresa la trasmissione dei dati tra i nodi e il lavoro con Consul, avviene in modo asincrono, funziona sul framework tokio.

La parte di rete responsabile della ricezione delle metriche ha causato molte più problematiche durante lo sviluppo. L'obiettivo principale della separazione dei flussi di rete in entità distinte era cercare di ridurre il tempo impiegato dal flusso non per leggere i dati dal socket. Le opzioni con UDP asincrono e il normale recvmsg sono state rapidamente scartate: il primo utilizza troppa CPU in user-space per la gestione degli eventi, il secondo - troppe interruzioni di contesto. Pertanto, attualmente viene utilizzato recvmmsg con buffer di grandi dimensioni (e i buffer, signori ufficiali, non sono una cosa da poco!). Il supporto per il normale UDP è stato mantenuto per i casi non gravosi, dove non è necessaria l'uso di recvmmsg. In modalità multimessage, si riesce a raggiungere l'obiettivo principale: la maggior parte del tempo il flusso di rete gestisce la coda del sistema operativo - legge i dati dal socket e li copia nel buffer user-space, solo raramente interrompendosi per restituire il buffer pieno agli aggregatori. La coda nel socket praticamente non si accumula e il numero di pacchetti scartati non cresce.

Nota

Nelle impostazioni predefinite, la dimensione del buffer è impostata su un valore piuttosto grande. Se decidete di provare il server da soli, potreste scoprire che dopo l'invio di un piccolo numero di metriche, queste non arrivano in Graphite, rimanendo nel buffer del flusso di rete. Per gestire un piccolo numero di metriche, è necessario impostare nel conf bufsize e task-queue-size valori più piccoli.

Infine, qualche grafico per gli amanti dei grafici.

Statistiche sul numero di metriche in entrata per ogni server: oltre 2 milioni di MPS.

Bioyino — aggregatore di metriche distribuito e scalabile

Disattivazione di uno dei nodi e ridistribuzione delle metriche in ingresso.

Bioyino — aggregatore di metriche distribuito e scalabile

Statistiche sulle metriche in uscita: solo un nodo invia sempre - il raidboss.

Bioyino — aggregatore di metriche distribuito e scalabile

Statistiche sul funzionamento di ogni nodo tenendo conto degli errori nei vari moduli del sistema.

Bioyino — aggregatore di metriche distribuito e scalabile

Dettaglio delle metriche in ingresso (i nomi delle metriche sono nascosti).

Bioyino — aggregatore di metriche distribuito e scalabile

Cosa intendiamo fare con tutto questo in futuro? Naturalmente scrivere codice, bl...! Il progetto è stato concepito fin dall'inizio come open-source e rimarrà tale per tutto il suo ciclo di vita. I piani per il prossimo futuro includono la transizione a una versione personalizzata di Raft, la sostituzione del protocollo peer con uno più portabile, l'aggiunta di ulteriori statistiche interne, nuovi tipi di metriche, correzione di bug e altri miglioramenti.

Naturalmente, sono benvenuti tutti coloro che desiderano contribuire allo sviluppo del progetto: create PR, Issue, e risponderemo e apporteremo modifiche quando possibile, ecc.

E con questo, come si suol dire, that's all folks, acquistate i nostri elefanti!

Guarda il video


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