
TL;DR: Descrizione dell'architettura client-server del nostro sistema interno di gestione della configurazione di rete, QControl. Alla base c'è un protocollo di trasporto a due livelli che opera con messaggi compressi in gzip senza decompressione tra gli endpoint. Router distribuiti e endpoint ricevono aggiornamenti di configurazione e il protocollo consente l'installazione di relais intermedi localizzati. Il sistema è costruito secondo il principio ('recent-stable', spiegato di seguito) e utilizza il linguaggio di query JMESpath insieme a Jinja come template engine per il rendering dei file di configurazione.
Qrator Labs gestisce una rete globalmente distribuita per la mitigazione degli attacchi. La nostra rete funziona sul principio anycast e le sottoreti vengono annunciate tramite BGP. Essendo una rete BGP anycast, situata fisicamente in diverse regioni del pianeta, possiamo elaborare e filtrare il traffico illegittimo più vicino al nucleo di Internet — gli operatori Tier-1.
D'altra parte, essere una rete geograficamente distribuita non è semplice. La comunicazione tra i punti di presenza della rete è fondamentale per un fornitore di servizi di sicurezza, al fine di mantenere una configurazione coerente di tutti i nodi della rete, aggiornandoli in modo tempestivo. Pertanto, per fornire il massimo livello possibile del servizio principale per il consumatore, era necessario trovare un modo per sincronizzare in modo affidabile i dati di configurazione tra i continenti.
In principio era la Parola. Essa è rapidamente diventata un protocollo di comunicazione che necessitava di un aggiornamento.
La pietra angolare dell'esistenza di QControl e allo stesso tempo la principale ragione per cui si spende un notevole tempo e risorse per costruire questo tipo di protocollo è la necessità di ottenere una fonte unica e autorevole di configurazione e, in ultima analisi, di sincronizzare le nostre piattaforme di presenza con essa. Lo storage era solo uno dei diversi requisiti durante lo sviluppo di QControl. Oltre a ciò, avevamo anche bisogno di integrazioni con i servizi esistenti e pianificati sulle piattaforme di presenza (TP), modi intelligenti (e personalizzabili) di validazione dei dati e, inoltre, di gestione degli accessi. Volevamo anche gestire tale sistema tramite comandi anziché modifiche ai file. Prima di QControl, i dati venivano inviati alle piattaforme di presenza praticamente manualmente. Se una delle piattaforme di presenza non era disponibile, e ci scordavamo di aggiornarla in seguito, la configurazione risultava desincronizzata, richiedendo tempo per rimetterla in carreggiata.
Alla fine abbiamo ideato il seguente schema:

Il server di configurazione è responsabile della validazione dei dati e dello storage, il router ha diversi endpoint che ricevono e trasmettono aggiornamenti di configurazione dai clienti e dalla squadra di supporto al server e dalle server alle piattaforme di presenza.
La qualità della connessione Internet varia ancora notevolmente in diverse parti del mondo; per illustrare questo punto, diamo un'occhiata a un semplice MTR da Praga, Repubblica Ceca, a Singapore e Hong Kong.
MTR da Praga a Singapore
Lo stesso verso Hong Kong
Le alte latenze significano una minore velocità. Inoltre, c'è perdita di pacchetti. La larghezza di banda non compensa questo problema, che deve sempre essere preso in considerazione nella costruzione di sistemi decentralizzati.
La configurazione completa della piattaforma di presenza è un notevole volume di dati che deve essere inviato a molti destinatari su connessioni inaffidabili. Fortunatamente, sebbene la configurazione cambi costantemente, ciò avviene in piccole porzioni.
Design recent-stable
Si può dire che costruire una rete distribuita secondo il principio degli aggiornamenti incrementali sia una soluzione piuttosto ovvia. Tuttavia, ci sono molte problematiche legate ai diff. Dobbiamo mantenere tutti i diff tra i punti di riferimento e anche essere in grado di inviarli nel caso qualcuno abbia perso parte dei dati. Ogni punto di destinazione deve applicarli in una sequenza rigorosamente definita. Di solito, nel caso di più punti di destinazione, tale operazione può richiedere un tempo considerevole. Il ricevente deve anche essere in grado di richiedere le parti mancanti e, naturalmente, la parte centrale deve rispondere a tale richiesta in modo corretto, inviando solo i dati mancanti.
Alla fine, siamo arrivati a una soluzione piuttosto interessante: abbiamo solo un livello di riferimento, fisso, chiamato stable, e solo un diff per esso — recent. Ogni recent si basa sull'ultimo stable generato ed è sufficiente per ricostruire i dati di configurazione. Non appena un recent fresco arriva a destinazione, il vecchio non è più necessario.
Rimane solo da inviare di tanto in tanto una nuova configurazione stable, ad esempio perché il recent è diventato troppo grande. È anche importante che inviamo tutti questi aggiornamenti in modalità broadcast/multicast, senza preoccuparci dei singoli destinatari e della loro capacità di raccogliere i pezzi di dati insieme. Una volta che ci siamo assicurati che tutti abbiano una stable corretta, inviamo solo i nuovi recent. Vale la pena sottolineare che questo funziona? Funziona. Il stable è memorizzato nella cache sui server di configurazione e sui destinatari, e il recent viene creato secondo necessità.
Architettura di trasporto a due livelli
Perché abbiamo costruito il nostro trasporto su due livelli? La risposta è piuttosto semplice: volevamo separare il routing dalla logica di alto livello, traendo ispirazione dal modello OSI con il suo livello di trasporto e il livello di applicazione. A fare da protocollo di trasporto abbiamo scelto Thrift, e per il formato di alto livello dei messaggi di controllo abbiamo adottato il formato di serializzazione msgpack. Ecco perché il router (che esegue multicast/broadcast/relay) non guarda all'interno di msgpack, non decomprime e non comprime nuovamente il contenuto ma si limita a inoltrare i dati.
Thrift (dal inglese 'thrift', pronunciato come [θrift]) è un linguaggio di descrizione delle interfacce che viene utilizzato per definire e creare servizi per vari linguaggi di programmazione. È un framework per la chiamata remota di procedure (RPC). Combina una pipeline software con un motore di generazione di codice per lo sviluppo di servizi che, in un modo o nell'altro, funzionano in modo efficace e semplice tra i linguaggi.
Abbiamo scelto il framework Thrift per l'RPC e il supporto di molti linguaggi. Come al solito, le parti più semplici sono state il client e il server. Tuttavia, il router si è rivelato un duro osso, anche per via della mancanza di una soluzione pronta durante il nostro sviluppo.
Esistono altre opzioni, come protobuf/gRPC, ma quando abbiamo avviato il nostro progetto, gRPC era ancora piuttosto nuovo e non ci siamo sentiti di adottarlo.
Certo, avremmo potuto (e in effetti sarebbe stato opportuno) creare un nostro 'bicicletta'. Sarebbe stato più semplice creare un protocollo per ciò di cui avevamo bisogno, poiché l'architettura client-server è relativamente semplice da implementare rispetto alla costruzione di un router su Thrift. In ogni caso, c'è una tradizionale avversione verso i protocolli fatti in casa e le implementazioni delle librerie popolari (non a caso), e la domanda 'Come lo porteremo su altri linguaggi?' viene sempre sollevata. Pertanto, abbiamo scartato immediatamente l'idea della bicicletta.
Msgpack è un'alternativa a JSON, ma più veloce e più piccola. È un formato binario di serializzazione dei dati che consente lo scambio di dati tra molti linguaggi.
Al primo livello abbiamo Thrift con le informazioni minime necessarie al router per inviare il messaggio. Al secondo livello – strutture msgpack imballate.
Abbiamo scelto msgpack perché è più veloce e compatto rispetto a JSON. Ma ciò che è ancora più importante, supporta tipi di dati personalizzati, permettendoci di utilizzare funzionalità interessanti come la trasmissione di binari grezzi o oggetti speciali che rappresentano l'assenza di dati, il che era importante per il nostro schema 'recent-stable'.
JMESPath
JMESPath è un linguaggio di query per JSON.
Ecco come appare la descrizione fornita dalla documentazione ufficiale di JMESPath, ma in realtà offre molto di più. JMESPath consente di cercare e filtrare sottoalberi in strutture ad albero arbitrarie e di applicare modifiche ai dati in tempo reale. Inoltre, permette di aggiungere filtri speciali e procedure per la trasformazione dei dati. Sebbene richieda una certa concentrazione per essere compreso.
Jinja
Per alcuni utenti, è necessario trasformare la configurazione in un file — per cui utilizziamo un motore di template e Jinja è la scelta ovvia. Con essa generiamo il file di configurazione da un template e dai dati ottenuti nel punto di destinazione.
Per generare un file di configurazione abbiamo bisogno di una query JMESPath, di un template per la posizione del file nel filesystem e di un template per il file di configurazione stesso. Inoltre, in questa fase è utile precisare i permessi di accesso al file. Tutto ciò è stato abilmente combinato in un unico file — all'inizio del template di configurazione posizioniamo un'intestazione in formato YAML che descrive il resto.
Ad esempio:
---
selector: "[@][?@.fft._meta.version == `42`] | items([0].fft_config || `{}`)"
destination_filename: "fft/{{ match[0] }}.json"
file_mode: 0644
reload_daemons: [fft]
...
{{ dict(match[1]) | json(indent=2, sort_keys=True) }}
Per creare un file di configurazione per un nuovo servizio, aggiungiamo semplicemente un nuovo file di template. Non sono necessarie modifiche al codice sorgente o al software nei punti di presenza.
Cosa è cambiato dopo l'introduzione di QControl nelle operazioni? La prima e più importante cosa è la consegna coerente e affidabile degli aggiornamenti di configurazione a tutti i nodi della rete. La seconda è l'acquisizione di uno strumento potente per la verifica della configurazione e per le modifiche apportate dal nostro team di supporto, oltre che dagli utenti del servizio.
Siamo riusciti a fare tutto questo implementando uno schema di aggiornamenti recent-stable per semplificare la comunicazione tra il server di configurazione e i destinatari della configurazione. Utilizzando un protocollo a due livelli per supportare un modo di instradamento dei dati indipendente dal contenuto. Abbiamo integrato con successo un motore di generazione della configurazione basato su Jinja in una rete distribuita di filtraggio. Questo sistema supporta un'ampia gamma di modalità di configurazione per la nostra periferia distribuita e variegata.
Grazie per l'aiuto nella scrittura del materiale , , .
post.
Fonte: habr.com
