Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage
Corridoio di Archiviazione da St-Pete

Ciao a tutti! Sono Mons Anderson, architetto della piattaforma Mail.ru Cloud Solutions, condividerò come abbiamo costruito il nostro storage S3, come funziona, quali soluzioni si sono rivelate efficaci e quali cambieremmo se iniziassimo questo progetto da zero oggi.

Articolo redatto sulla base di una presentazione a @Databases Meetup da Mail.ru Cloud Solutions & Tarantool. In questo articolo parleremo di:

  • come era strutturato lo storage di Mail.ru, su cui abbiamo costruito lo storage S3;
  • cosa abbiamo aggiunto per creare Mail.ru Cloud Storage;
  • come funziona il modello di archiviazione ad oggetti e quali passaggi sono stati svolti per il rilascio in produzione;
  • sulle ottimizzazioni del sistema operativo: failover e scalabilità;
  • come abbiamo implementato lo sharding e il resharding;
  • e anche sul lavoro con i certificati SSL.

Se non vuoi leggere, puoi scoprire.

come era strutturato lo storage di Mail.ru, su cui abbiamo costruito lo storage S3

Lo sviluppo del nostro S3 è iniziato sopra lo storage del Cloud Mail.ru, quindi è opportuno raccontare prima come è strutturato e cosa può fare.

Lo storage del Cloud Mail.ru è composto da server con dischi. In media, un moderno server di archiviazione ha 36 dischi da 12–14 terabyte. In passato i dischi erano più piccoli, ma negli ultimi tre anni le capacità dei dischi sono aumentate e oggi si avvicinano a mezzo petabyte di dati grezzi.

I dischi dei vari server di archiviazione vengono uniti in quelle che chiamiamo «coppie» (pair). Una coppia è un'unità di archiviazione dei file. In sostanza, è un disco montato in una certa partizione su un percorso specifico, dove possono essere collocati file identificati da hash.

Coppia è un nome storico, che è rimasto fino ad oggi, anche se attualmente in una coppia non ci sono necessariamente solo due dischi. Possono essercene tre, così come varie configurazioni ibride, per esempio 3/2.

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage

Coppie (pair) — unità di archiviazione degli oggetti

Tutte le coppie sono memorizzate in PairDB — un'applicazione basata su Tarantool. Tutte le basi nel nostro storage, fin dalle origini, sono Tarantool; non utilizziamo altri database.

PairDB memorizza tutte le coppie, il loro stato, lo spazio libero, le capacità di failover, gli ultimi errori. Può anche accedere autonomamente alle coppie, aggiornare il loro stato e controllare se sono operative o meno. In altre parole, PairDB è un'istantanea generale dello stato di tutti i dischi del nostro sistema.

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage

Pair DB: database con lo stato delle coppie

Sui pair sono memorizzati i file, e per sapere su quale coppia si trova un file, è necessario un ulteriore database — FileDB. Questo memorizza il mapping, la corrispondenza: il file X è memorizzato sulla coppia Y, assieme a un piccolo numero di attributi necessari.

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud StorageFile DB: luogo dove viene memorizzato il file

Un altro elemento importante è il servizio Nylon, un router per l'interazione con i database. È un unico punto di accesso, permette di lavorare attraverso un'interfaccia unificata sia con PairDB che con FileDB. Si tratta di un servizio stateless, esegue il bilanciamento delle richieste, comprende su quale shard di FileDB bisogna andare, e sa quali coppie sono attive e quali no.

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage

Nylon: router per l'interazione con i database

Inoltre, è necessario un modo per caricare i contenuti nello storage. A tale scopo, esiste un servizio chiamato Streamer. Questo fornisce due metodi HTTP: il metodo PUT per caricare contenuti nello storage e il metodo GET per scaricarli. HTTP è un protocollo piuttosto popolare e comodo per il trasferimento di dati.

Quando ci rivolgiamo a Streamer, questo, tramite Nylon, interroga PairDB per determinare su quale coppia possa essere caricato il file, quindi trasferisce i dati tramite WebDAV su quella coppia.

In sostanza, qualsiasi server di archiviazione è nginx più dischi montati su percorsi specificati. Possiamo caricare un file nello storage da Streamer, eliminarlo, rinominarlo o verificarne l'integrità. Quindi, questo è un'interfaccia comoda per interagire a basso livello con lo storage.

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage

Streamer: punto di accesso nello storage

Cosa abbiamo aggiunto per creare lo storage S3

Quindi, abbiamo esaminato la struttura di base dello storage al momento in cui ci siamo preparati a lanciare lo storage S3. Usando il metodo PUT, potevamo collocare contenuti arbitrari e ricevere come identificatore di questi dati un hash. Con questo identificatore, era quindi possibile tornare a recuperare il file originale. Ma ciò non è sufficiente per realizzare S3. Nel protocollo S3, oltre alla semplice archiviazione degli oggetti, ci sono:

  • archiviazione dei metadati — proprietà aggiuntive degli oggetti;
  • organizzazione dell'accesso agli oggetti tramite HTTP;
  • raggruppamento degli oggetti in collezioni — bucket;
  • Endpoint HTTP-S3. S3 organizza i dati in strutture specifiche — bucket, ognuno dei quali fornisce un punto di accesso per memorizzare file.

Per realizzare questa logica, era necessario un servizio separato. Volevamo anche prevedere l'architettura per una futura crescita del servizio con scalabilità lineare.

I primi componenti

Demonio che implementa l'API S3. Si tratta dell'API S3 standard di Amazon, che supporta operazioni XML per i metadati e consente di trasferire direttamente i contenuti. Non abbiamo dovuto inventare nulla, tutto è descritto e documentato.

Abbiamo anche posizionato Nginx davanti al servizio. Lo abbiamo utilizzato per la terminazione SSL, il bilanciamento del carico e per alcune logiche in Lua (metriche, logging e tracing).

Per la memorizzazione dei metadati S3 abbiamo scelto anche Tarantool. Nella prima versione, il demone S3 accedeva a questo database per i metadati, mentre i contenuti venivano memorizzati in un grande storage tramite Streamer.

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage
Nginx + API S3 + metadati

Modello oggetti di memorizzazione

Vediamo come funziona S3. L'utente può creare un bucket, una collezione di oggetti. Il bucket è indirizzato tramite il nome host e rappresenta un sottodominio del servizio. All'interno del bucket, l'utente può creare oggetti. L'identificatore dell'oggetto è l'URL. Il contenuto dell'oggetto è un blob, un array di dati binari che memorizzeremo nello storage. Ogni oggetto ha anche attributi: il nome – quell'URL, ACL (Access Control List), altri attributi aggiuntivi o arbitrari – tutto questo viene conservato nei metadati.

Uno schema normalizzato di questi dati potrebbe apparire così: ci sono progetti che possiedono bucket, che possiedono oggetti e gli oggetti possono essere composti. Poiché uno dei modi per caricare un oggetto è a parti, ci sono due tabelle ausiliarie per il caricamento: uploads e chunks. Inoltre, i progetti hanno credenziali di accesso e billing.

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage
Schema dei dati

Poiché stavamo creando un servizio B2B con accesso a pagamento, era necessario un sistema di billing in questo schema.
Il servizio di billing è stato anch'esso implementato su Tarantool.

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage

Sviluppi dello storage S3: passi verso la produzione

Abbiamo già realizzato un modello funzionante che può essere utilizzato: gli oggetti e i metadati erano memorizzati, ma mancavano ancora alcuni aspetti per il lancio in produzione.

Innanzitutto, la necessità di un sistema di rate limiting. Se avessimo avviato il servizio senza di esso, in caso di carico massimo avremmo potuto sovraccaricare imprevedibilmente qualche parte del sistema. Il rate limit deve funzionare in questo modo: ogni richiesta S3 arriva su un host specifico, quest'host è l'identificatore del bucket, e il bucket appartiene a un cliente. Dobbiamo definire una funzione per il bucket che permetta di calcolare il rate limit.

Inoltre, il sistema di rate limiting deve essere sufficientemente performante per gestire il carico che arriva su S3.

Qui abbiamo di nuovo utilizzato Tarantool. I rate limit sono un cluster di 21 istanze, le istanze sono suddivise in gruppi, distribuite su tre nodi fisici e unite in un grande cluster topologico. Le modifiche di configurazione vengono propagate automaticamente su di esso: si impostano i rate limit, i default e la configurazione. Ogni bucket è servito esclusivamente da un'istanza. Quando una richiesta per un bucket specifico arriva, si calcola l'istanza responsabile per quel bucket. All'interno di questo nodo si conta il rate attuale delle richieste utilizzando un algoritmo simile al Token Bucket. Successivamente, il sistema di rate limiting, sulla base delle attuali metriche di carico e delle proprietà stabilite per il bucket specifico, decide se la richiesta può essere eseguita o meno. Il controllo dei limiti viene effettuato nella fase più precoce dell'esecuzione della richiesta S3, proteggendo tutti gli altri elementi del sistema da un carico eccessivo.

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage

Inoltre, sotto carico è piuttosto difficile fare a meno della cache. In S3 si prevede l'accesso ripetuto agli stessi oggetti, quindi si tratta di uno storage caldo. Di norma, l'accesso a un singolo file viene gestito da un'intera catena: Streamer, FileDB, PairDB, Storage. Ma durante i ripetuti accessi a un file, ottimizziamo l'accesso a questi contenuti tramite una cache locale.

La cache è multilivello e implementata utilizzando nginx, dischi SSD locali e RAM. Qui non abbiamo utilizzato Tarantool, perché è più comodo restituire oggetti dal filesystem, in questo modo possiamo fare tiering della cache. Inoltre, abbiamo grandi oggetti con una dimensione massima di 32 gigabyte, mentre in Tarantool è possibile memorizzare solo oggetti di piccole dimensioni.

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage

Questo primo sistema, con cui siamo partiti, aveva una capacity (capacità) calcolata, sufficiente per esplorare e comprendere il prodotto e dimostrare che avrebbe funzionato.

Sviluppi del sistema in produzione: failover e scaling

Il sistema era già operativo, ma all'inizio avevamo trascurato alcune cose – era necessario aggiungere failover e scaling.

Il nostro demone S3 ha recuperato i metadati tramite il protocollo Tarantool. Abbiamo sostituito il database originale con Tarantool, che fungeva da proxy-router per le richieste di metadati. Dal punto di vista dell'applicazione che implementa l'API, nulla è cambiato: ha continuato a interagire con il database tramite il protocollo Tarantool, ma il router è stato in grado di garantire un failover attivo. Ciò significa che abbiamo potuto monitorare l'accessibilità del nodo, gestire le pause durante i passaggi e guasti, e così via. In tutto questo, non abbiamo modificato l'applicazione stessa.

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage

Scopri di più su come abbiamo implementato lo sharding

La prossima questione a cui abbiamo dovuto pensare è stata lo sharding. Il sistema stava crescendo, il numero di oggetti aumentava e bisognava garantire opportunità per una futura crescita.

Torniamo allo schema dei dati: ci sono progetti, bucket, credenziali e fatturazione. Questi sono oggetti che, con alta probabilità, non cresceranno oltre un singolo'istanza sia in termini di volume che di richieste. Pertanto, non ha senso shardarli, così abbiamo trasferito questi oggetti su un'istanza separata che rimarrà non shardata. Questo consente una gestione più coerente di progetti e bucket, poiché c'è un'unica entità non shardata.

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage

Ci sono anche oggetti nel sistema che crescono linearmente: inizialmente ce n'erano centinaia di migliaia, ora il loro numero si misura in miliardi. Tali oggetti e le loro parti dovevano essere trasferiti in un cluster shardato.

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage

Abbiamo diviso lo schema, ma gli oggetti devono lavorare con i bucket: ogni oggetto appartiene a un bucket specifico, in più sull'bucket è attivo il controllo ACL. Pertanto, per ogni shard di oggetti manteniamo una copia ombra di ogni bucket. Inoltre, durante la modifica degli oggetti e l'esecuzione delle richieste, è necessario calcolare il volume per la fatturazione, quindi su ogni shard ci sono contatori per la fatturazione.

Abbiamo anche aggiunto alcune altre tabelle e componenti:

  • un cestino, per eliminare vecchi progetti che vengono rimossi o messi in pausa;
  • una coda per attività in background, cioè l'archiviazione principale può eseguire attività in background che è necessario svolgere nel cluster;
  • supporto per lifecycle — un meccanismo che consente di lavorare con oggetti e gestire il loro ciclo di vita.

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage

Poiché parte dei dati è stata trasferita sugli shard, è stata necessaria una proxy di sharding. Si sarebbe potuto riutilizzare il router per questo ruolo, ma una proxy di sharding separata, responsabile esclusivamente dello sharding dei dati, consente al router di recuperare i dati in modo completo, senza pensare allo sharding.

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage

Spiegherò separatamente perché non abbiamo scelto una soluzione pronta, ma volevamo creare una funzione di sharding personalizzata.

Vediamo come è strutturata. Abbiamo 256 shard disponibili. Per ogni bucket, assegniamo un intervallo utilizzando una certa funzione di coerenza. È semplice: proprio come si determina l'appartenenza a uno shard tramite una funzione di coerenza, così si determina lo shard iniziale e si assegna un intervallo:

f(bucket, shards) = subset

Ciò significa che, prendendo un bucket, si può dire che esso e i suoi dati saranno sempre collocati in un sottoinsieme specifico di tutti gli shard. Questo riduce l'impatto di alcuni bucket sugli altri e semplifica l'esecuzione delle richieste map-reduce, quando ad esempio è necessario elencare gli oggetti di un bucket. Per farlo, è necessario interrogare tutti gli shard dove questi oggetti sono memorizzati. Se gli oggetti fossero distribuiti su tutti gli shard, qualsiasi elencazione influenzerebbe l'intero sistema; qui invece colpisce solo un sottoinsieme specifico.

Dopo, ogni oggetto appartiene a un bucket specifico, quindi quando richiediamo un oggetto, lo facciamo attraverso il suo nome in un bucket specifico. Possiamo quindi definire una funzione per l'oggetto non su tutto l'intervallo di shard disponibili, ma solo su un sottoinsieme del suo bucket:

f(object, subset) = shard

Prendiamo un oggetto specifico, passando come argomenti alla funzione non tutti gli shard, ma un sottoinsieme del suo bucket - e otteniamo uno shard specifico.

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage

Quindi, lo sharding è implementato, c'è una proxy di sharding. Rimane da utilizzare il router e il database con i metadati per interagire con la proxy di sharding. Ad esempio, per la creazione di oggetti di copie ombra: quando creiamo un bucket, lo storage principale deve creare un rappresentante di questo bucket su tutti gli shard dove deve essere presente.

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage

Come abbiamo implementato il resharding

Il problema più grande dello sharding è il resharding. Era importante farlo senza downtime, poiché il sistema era già in produzione. Mostrerò come abbiamo risolto il problema utilizzando un esempio di un compito simile con la migrazione dei dati da un progetto a un altro.

Di seguito è riportato lo schema del nostro cluster, realizzato dopo l'implementazione del sharding. Abbiamo nginx, S3 API, un router, un database primario con progetti, un proxy di sharding e i relativi shard.

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage

Sopra ho trascurato di menzionare che, a un certo punto del progetto, c'era un obiettivo di prodotto: "Avviare un ulteriore storage, Icebox, simile a Hotbox, ma per dati freddi". In sostanza, si trattava dello stesso tipo di storage, ma con URL diversi e senza cache.

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage

Icebox è stato utilizzato meno di Hotbox, quindi è rimasto senza sharding per un lungo periodo. Alla fine, abbiamo deciso di eliminarlo e unire Hotbox e Icebox in un unico servizio, semplicemente separando le classi di storage.

I bucket negli storage non si sovrapponevano, potevano essere facilmente fusi e spostati, ma i clienti utilizzavano entrambi gli storage, quindi era necessario risolvere il problema dell'assenza di downtime. Non era possibile semplicemente spegnere e copiare. Abbiamo effettuato la migrazione in più fasi.

Per iniziare, abbiamo sincronizzato gli storage primari. Avevamo Tarantool e potevamo, al momento della creazione di un oggetto, fare in modo che:

  • una richiesta di creazione di un bucket arrivasse al database, ad esempio in Hotbox;
  • Tarantool controllava in un altro database (in questo caso, in Icebox) se esisteva già quel bucket;
  • se il bucket esisteva, il database segnalava che non poteva essere creato e si sincronizzava come esistente.
    Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage
    Sincronizzazione dei bucket

Nel database che doveva ricevere tutti i dati, è stato introdotto un segnale per progetti e bucket che indicava dove si trovava quell'oggetto. Poteva essere memorizzato localmente, cioè in Hotbox, in Icebox— in questo caso, non vi erano dati nel nuovo storage— oppure poteva essere in uno stato di migrazione.

Se un progetto o un bucket aveva il segnale 'Migrating', durante la migrazione la richiesta veniva eseguita prima nel nuovo storage, dove i dati dovevano trovarsi, e se non erano presenti, le richieste venivano reindirizzate allo storage alternativo.

Dopo abbiamo reindirizzato il traffico. Poiché l'API poteva gestire sia le richieste di Icebox che quelle di Hotbox, siamo riusciti a reindirizzare il traffico senza downtime, semplicemente trasferendo gli host e aggiungendo le relative voci in Nginx.

Dopo che il traffico è stato reindirizzato, abbiamo potuto rimuovere Nginx e l'API di Icebox.
Successivamente, abbiamo rimosso Icebox nginx e S3 API— e tutto ha iniziato a funzionare:

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage

Dopo la migrazione dei dati, non abbiamo più bisogno del vecchio storage e rimuoviamo i resti del sistema precedente, così come il supporto per lo stato di migrazione nel codice.

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage

Utilizzando gli stessi principi, è stata effettuata anche la re-shardizzazione dal vecchio storage a quello sharded:

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage

Abbiamo contrassegnato tutti i bucket come

  • Non-sharded . Tutte le richieste ad essi andavano all'originale, storage non sharded.I nuovi bucket venivano creati immediatamente con lo stato
  • Sharded . Uno per uno, prendevamo i bucket, impostavamo lo stato.
  • Migrating e trasferivamo i dati. Le richieste venivano gestite secondo il principio:

Leggiamo nel nuovo, poi nel vecchio.

  • Creiamo solo nel nuovo.
  • Aggiorniamo in due fasi: se nel nuovo non c'è nulla, trasferiamo dal vecchio al nuovo, poi aggiorniamo.
  • Gestione dei certificati SSL

Sul frontend utilizziamo Nginx. Nel nostro caso, non è il solito Nginx, ma OpenResty, Nginx con supporto per LuaJIT.

Un altro elemento del sistema è la gestione dei certificati SSL. In uno storage S3 puoi impostare un tuo dominio per accedere a un bucket specifico, semplicemente attraverso

. Ma senza HTTPS al giorno d'oggi non è possibile: un dominio personale implica un certificato SSL personale. CNAMECome ho già detto, per il bilanciamento e la terminazione SSL ci occupiamo di Nginx. Nel nostro caso, non si tratta del consueto Nginx, ma di OpenResty, Nginx con supporto per LuaJIT.

Questo ci ha permesso di insegnare al nostro Nginx a restituire certificati arbitrari in modo abbastanza semplice. Inoltre, era necessario restituire i certificati in modo dinamico (senza bisogno di scriverli nel file di configurazione). Abbiamo utilizzato l'estensione

ssl_certificate_by_lua , che consente di leggere il certificato da una fonte arbitraria direttamente durante il handshake TLS. Come archivio di certificati abbiamo anche utilizzato Tarantool: questo permette di gestire i certificati da remoto e assicura una restituzione estremamente veloce.È stato implementato anche un demone separato, il cui compito è il regolare aggiornamento dei certificati rilasciati tramite Let's Encrypt.

Cosa conserverei e cosa farei diversamente se sviluppassi lo storage da zero

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage

Cosa avremmo dovuto utilizzare fin dall'inizio

Sharding subito

Sharding immediato. Resharding ha presentato diverse problematiche. È facile da realizzare, ma se si avviano progetti che necessitano di scalabilità, è meglio partire direttamente con un cluster sharded, anche se con il numero minimo di nodi. Implementare il resharding all'inizio è quasi gratuito rispetto a farlo in un sistema operativo.

Lavorare con Tarantool attraverso i bilanciatori. Ora colleghiamo subito tutti i nuovi database ai bilanciatori. Questo consente di ampliare le funzionalità e ottenere una maggiore resilienza.

Failover automatico. Avrei installato tutti gli strumenti necessari per il failover automatico, poiché i primi fallimenti dopo il lancio erano dovuti alla sua assenza. Dopo l'esperienza con S3, tutti i prodotti successivi sono stati lanciati tenendo conto di questo.

Funzionalità di versionamento di S3. Inizialmente sembrava che non fosse una funzionalità molto richiesta. Integrare questa possibilità nell'architettura di un sistema operativo esistente è estremamente complesso.

Fatturazione separata. Il modo in cui abbiamo integrato la fatturazione nel nostro sistema ha funzionato bene all'inizio, ma in seguito è diventato un ostacolo; sarebbe stato meglio integrarlo come servizio completamente separato.

Cosa si è rivelato essere una buona soluzione

Modello dati. La storia ha dimostrato che, man mano che il servizio cresceva, ci si adattava abbastanza bene al modello di dati di Amazon, permettendo così di implementare le funzionalità presenti lì.

Schema di sharding. Sosterrei operative simili per lo sharding a intervallo per i bucket, poiché ciò consente di distribuire bene le richieste tra i diversi bucket in un grande cluster.

Utilizzo di Tarantool. Tarantool ha fornito un grande aiuto nello sviluppo e nella modifica del servizio; abbiamo lavorato facilmente con i dati, trasformando e shardando il deposito, senza dover salire al livello dell'applicazione.

Questa relazione è stata presentata per la prima volta al @Databases Meetup by Mail.ru Cloud Solutions&Tarantool. Vedi video altri interventi e iscriviti agli avvisi degli eventi su Telegram Attorno a Kubernetes in Mail.ru Group.

Puoi anche guardare la mia vecchia presentazione su S3 o leggere l'articolo del mio collega sullo storage a blocchi.

Fonte: habr.com

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