Bioyino: aggregatore di parametri distribuito e scalabile

Quindi raccogli metriche. Come siamo. Raccogliamo anche parametri. Naturalmente, necessario per gli affari. Oggi parleremo del primo collegamento del nostro sistema di monitoraggio: un server di aggregazione compatibile con Statsd bioyino, perché l'abbiamo scritto e perché abbiamo abbandonato Brubeck.

Bioyino: aggregatore di parametri distribuito e scalabile

Dai nostri articoli precedenti (1, 2) puoi scoprire che fino a qualche tempo raccoglievamo marchi utilizzando brubecco. È scritto in C. Dal punto di vista del codice, è semplice come una spina (questo è importante quando vuoi contribuire) e, cosa più importante, gestisce i nostri volumi di 2 milioni di metri al secondo (MPS) al massimo senza alcun problema. La documentazione indica il supporto per 4 milioni di MPS con un asterisco. Ciò significa che otterrai la cifra indicata se configuri correttamente la rete su Linux. (Non sappiamo quanti MPS puoi ottenere se lasci la rete così com'è). Nonostante questi vantaggi, abbiamo ricevuto numerose lamentele nei confronti di Brubeck.

Rivendicazione 1. Github, lo sviluppatore del progetto, ha smesso di supportarlo: pubblicando patch e correzioni, accettando le nostre e (non solo le nostre) PR. Negli ultimi mesi (tra febbraio e marzo 2018) l’attività è ripresa, ma prima c’erano quasi 2 anni di completa calma. Inoltre il progetto è in fase di sviluppo per le esigenze interne di Gihub, che può diventare un serio ostacolo all'introduzione di nuove funzionalità.

Rivendicazione 2. Precisione dei calcoli. Brubeck raccoglie un totale di 65536 valori per l'aggregazione. Nel nostro caso, per alcune metriche, durante il periodo di aggregazione (30 secondi), potrebbero arrivare molti più valori (1 al picco). A seguito di questo campionamento i valori massimo e minimo risultano inutili. Ad esempio, in questo modo:

Bioyino: aggregatore di parametri distribuito e scalabile
Com'era

Bioyino: aggregatore di parametri distribuito e scalabile
Come avrebbe dovuto essere

Per lo stesso motivo, gli importi vengono generalmente calcolati in modo errato. Aggiungi qui un bug con un overflow float a 32 bit, che generalmente manda il server in segfault quando riceve una metrica apparentemente innocente, e tutto diventa fantastico. Il bug, tra l'altro, non è stato risolto.

E, infine, Rivendica X. Nel momento in cui scriviamo siamo pronti a presentarlo a tutte le 14 implementazioni di statistiche più o meno funzionanti che siamo riusciti a trovare. Immaginiamo che una singola infrastruttura sia cresciuta così tanto che accettare 4 milioni di MPS non sia più sufficiente. O anche se non è ancora cresciuto, ma i parametri sono già così importanti per te che anche brevi cali di 2-3 minuti nei grafici possono già diventare critici e causare attacchi di depressione insormontabile tra i manager. Poiché curare la depressione è un compito ingrato, sono necessarie soluzioni tecniche.

In primo luogo, la tolleranza agli errori, in modo che un problema improvviso sul server non provochi un'apocalisse psichiatrica di zombie in ufficio. In secondo luogo, scalare per poter accettare più di 4 milioni di MPS, senza scavare in profondità nello stack di rete Linux e crescere con calma “in ampiezza” fino alla dimensione richiesta.

Dato che avevamo spazio per la scalabilità, abbiamo deciso di iniziare con la tolleranza agli errori. "DI! Tolleranza ai guasti! È semplice, possiamo farcela", abbiamo pensato e abbiamo lanciato 2 server, sollevando una copia di Brubeck su ciascuno. Per fare ciò, abbiamo dovuto copiare il traffico con le metriche su entrambi i server e persino scriverlo piccola utilità. Con questo abbiamo risolto il problema della tolleranza agli errori, ma... non molto bene. All'inizio tutto sembrava perfetto: ogni Brubeck raccoglie la propria versione di aggregazione, scrive i dati su Graphite una volta ogni 30 secondi, sovrascrivendo il vecchio intervallo (questo viene fatto sul lato Graphite). Se un server si guasta improvvisamente, ne abbiamo sempre un secondo con la propria copia dei dati aggregati. Ma ecco il problema: se il server si guasta, sui grafici appare una “sega”. Ciò è dovuto al fatto che gli intervalli di 30 secondi di Brubeck non sono sincronizzati e al momento dell'incidente uno di essi non viene sovrascritto. Quando si avvia il secondo server, accade la stessa cosa. Abbastanza tollerabile, ma voglio di meglio! Anche il problema della scalabilità non è scomparso. Tutte le misurazioni continuano a “volare” su un singolo server e quindi siamo limitati agli stessi 2-4 milioni di MPS, a seconda del livello di rete.

Se pensi un po 'al problema e allo stesso tempo scavi la neve con una pala, allora potrebbe venirti in mente la seguente idea ovvia: hai bisogno di una statistica che possa funzionare in modalità distribuita. Cioè, uno che implementa la sincronizzazione tra i nodi nel tempo e nelle metriche. “Certo, probabilmente una soluzione del genere esiste già”, abbiamo detto e siamo andati su Google…. E non hanno trovato nulla. Dopo aver esaminato la documentazione per diverse statistiche (https://github.com/etsy/statsd/wiki#server-implementations all'11.12.2017 dicembre XNUMX), non abbiamo trovato assolutamente nulla. A quanto pare, né gli sviluppatori né gli utenti di queste soluzioni hanno ancora riscontrato così tanti parametri, altrimenti avrebbero sicuramente inventato qualcosa.

E poi ci siamo ricordati del "giocattolo" statsd - bioyino, che è stato scritto all'hackathon Just for Fun (il nome del progetto è stato generato dalla sceneggiatura prima dell'inizio dell'hackathon) e ci siamo resi conto che avevamo urgentemente bisogno delle nostre statistiche. Per quello?

  • perché ci sono troppo pochi cloni di statistiche al mondo,
  • perché è possibile fornire la tolleranza agli errori e la scalabilità desiderate o prossime a quelle desiderate (inclusa la sincronizzazione dei parametri aggregati tra i server e la risoluzione del problema dell'invio dei conflitti),
  • perché è possibile calcolare le metriche in modo più accurato di quanto fa Brubeck,
  • perché puoi raccogliere tu stesso statistiche più dettagliate, che Brubeck praticamente non ci ha fornito,
  • perché ho avuto la possibilità di programmare la mia applicazione di laboratorio su scala distribuita con iperprestazioni, che non ripeterà completamente l'architettura di un altro iperfor simile... beh, questo è tutto.

Su cosa scrivere? Ovviamente in Rust. Perché?

  • perché esisteva già una soluzione prototipo,
  • perché l'autore dell'articolo conosceva già Rust a quel tempo ed era ansioso di scriverci qualcosa per la produzione con l'opportunità di metterlo in open source,
  • perché le lingue con GC non sono adatte a noi a causa della natura del traffico ricevuto (quasi in tempo reale) e le pause GC sono praticamente inaccettabili,
  • perché hai bisogno di massime prestazioni paragonabili a C
  • perché Rust ci fornisce una concorrenza senza paura, e se avessimo iniziato a scriverlo in C/C++, avremmo rastrellato ancora più vulnerabilità, buffer overflow, race conditions e altre parole spaventose rispetto a Brubeck.

C'era anche una discussione contro Rust. L'azienda non aveva esperienza nella creazione di progetti in Rust e ora non prevediamo di utilizzarlo nel progetto principale. Pertanto, c'erano seri timori che nulla avrebbe funzionato, ma abbiamo deciso di rischiare e di provarci.

Il tempo passò...

Finalmente, dopo diversi tentativi falliti, la prima versione funzionante era pronta. Quello che è successo? Questo è quello che è successo.

Bioyino: aggregatore di parametri distribuito e scalabile

Ogni nodo riceve il proprio set di parametri e li accumula e non aggrega i parametri per quei tipi in cui è richiesto il loro set completo per l'aggregazione finale. I nodi sono collegati tra loro da una sorta di protocollo di blocco distribuito, che consente di selezionare tra loro l'unico (qui abbiamo pianto) che è degno di inviare parametri al Grande. Questo problema è attualmente in fase di risoluzione da parte di Console, ma in futuro le ambizioni dell'autore si estendono a proprio implementazione Raft, dove il più degno sarà, ovviamente, il nodo leader del consenso. Oltre al consenso, i nodi abbastanza spesso (una volta al secondo per impostazione predefinita) inviano ai loro vicini quelle parti di metriche preaggregate che sono riusciti a raccogliere in quel secondo. Si scopre che la scalabilità e la tolleranza agli errori vengono preservate: ogni nodo contiene ancora un set completo di parametri, ma i parametri vengono inviati già aggregati, tramite TCP e codificati in un protocollo binario, quindi i costi di duplicazione sono significativamente ridotti rispetto a UDP. Nonostante il numero piuttosto elevato di parametri in entrata, l’accumulo richiede pochissima memoria e ancor meno CPU. Per i nostri prodotti altamente comprimibili, si tratta solo di poche decine di megabyte di dati. Come bonus aggiuntivo, in Graphite non otteniamo riscritture di dati non necessarie, come nel caso di Burbeck.

I pacchetti UDP con metriche vengono sbilanciati tra i nodi sulle apparecchiature di rete attraverso un semplice Round Robin. Naturalmente, l'hardware di rete non analizza il contenuto dei pacchetti e quindi può estrarre molto più di 4 milioni di pacchetti al secondo, per non parlare delle metriche di cui non sa nulla. Se teniamo conto del fatto che le metriche non vengono fornite una alla volta in ciascun pacchetto, non prevediamo alcun problema di prestazioni in questo luogo. Se un server si blocca, il dispositivo di rete rileva rapidamente (entro 1-2 secondi) questo fatto e rimuove dalla rotazione il server bloccato. Di conseguenza, i nodi passivi (cioè non leader) possono essere attivati ​​e disattivati ​​praticamente senza notare prelievi sui grafici. Il massimo che perdiamo fa parte dei parametri arrivati ​​all'ultimo secondo. Una perdita/spegnimento/cambio improvviso di un leader creerà comunque un'anomalia minore (l'intervallo di 30 secondi è ancora fuori sincronia), ma se c'è comunicazione tra i nodi, questi problemi possono essere minimizzati, ad esempio, inviando pacchetti di sincronizzazione .

Un po 'sulla struttura interna. L'applicazione è, ovviamente, multithread, ma l'architettura del threading è diversa da quella utilizzata in Brubeck. I thread a Brubeck sono gli stessi: ognuno di essi è responsabile sia della raccolta che dell'aggregazione delle informazioni. In bioyino i lavoratori sono divisi in due gruppi: quelli responsabili della rete e quelli responsabili dell'aggregazione. Questa divisione consente di gestire l'applicazione in modo più flessibile a seconda del tipo di metriche: dove è richiesta un'aggregazione intensiva si possono aggiungere aggregatori, dove c'è molto traffico di rete si può aggiungere il numero di flussi di rete. Al momento, sui nostri server lavoriamo in 8 flussi di rete e 4 flussi di aggregazione.

La parte di conteggio (responsabile dell'aggregazione) è piuttosto noiosa. I buffer riempiti dai flussi di rete vengono distribuiti tra i flussi di conteggio, dove vengono successivamente analizzati e aggregati. Su richiesta vengono fornite le metriche per l'invio ad altri nodi. Tutto ciò, incluso l'invio di dati tra i nodi e il lavoro con Consul, viene eseguito in modo asincrono, in esecuzione sul framework Tokyo.

Molti più problemi durante lo sviluppo sono stati causati dalla parte di rete responsabile della ricezione delle metriche. L'obiettivo principale di separare i flussi di rete in entità separate era il desiderio di ridurre il tempo trascorso da un flusso no per leggere i dati dal socket. Le opzioni che utilizzano UDP asincrono e recvmsg regolare sono rapidamente scomparse: la prima consuma troppa CPU in spazio utente per l'elaborazione degli eventi, la seconda richiede troppi cambi di contesto. Pertanto è ora utilizzato recvmmsg con grandi respingenti (e i respingenti, signori ufficiali, non sono niente per voi!). Il supporto per UDP regolare è riservato ai casi leggeri in cui recvmmsg non è necessario. In modalità multimessaggio, è possibile ottenere la cosa principale: nella stragrande maggioranza delle volte, il thread di rete rastrella la coda del sistema operativo: legge i dati dal socket e li trasferisce nel buffer dello spazio utente, solo occasionalmente passando a fornire il buffer pieno a aggregatori. La coda nel socket praticamente non si accumula, il numero di pacchetti eliminati praticamente non aumenta.

Nota

Nelle impostazioni predefinite, la dimensione del buffer è impostata su un valore piuttosto grande. Se all'improvviso decidi di provare tu stesso il server, potresti riscontrare il fatto che dopo aver inviato un numero limitato di parametri, questi non arriveranno in Graphite, rimanendo nel buffer del flusso di rete. Per lavorare con un numero limitato di parametri, è necessario impostare bufsize e task-queue-size su valori più piccoli nel file config.

Infine, alcuni schemi per gli amanti degli schemi.

Statistiche sul numero di metriche in entrata per ciascun server: più di 2 milioni di MPS.

Bioyino: aggregatore di parametri distribuito e scalabile

Disabilitare uno dei nodi e ridistribuire le metriche in entrata.

Bioyino: aggregatore di parametri distribuito e scalabile

Statistiche sulle metriche in uscita: invia sempre solo un nodo: il raid boss.

Bioyino: aggregatore di parametri distribuito e scalabile

Statistiche del funzionamento di ciascun nodo, tenendo conto degli errori nei vari moduli del sistema.

Bioyino: aggregatore di parametri distribuito e scalabile

Dettagli delle metriche in entrata (i nomi delle metriche sono nascosti).

Bioyino: aggregatore di parametri distribuito e scalabile

Cosa abbiamo intenzione di fare con tutto questo dopo? Certo, scrivi il codice, cavolo...! Il progetto era originariamente previsto per essere open source e rimarrà tale per tutta la sua vita. I nostri piani immediati includono il passaggio alla nostra versione di Raft, la modifica del protocollo peer in uno più portabile, l'introduzione di statistiche interne aggiuntive, nuovi tipi di parametri, correzioni di bug e altri miglioramenti.

Naturalmente, tutti sono benvenuti ad aiutare nello sviluppo del progetto: creare PR, problemi, se possibile risponderemo, miglioreremo, ecc.

Detto questo è tutto gente, comprate i nostri elefanti!



Fonte: habr.com

Aggiungi un commento