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

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage
Corridor di Storage di St-Pete

Ciao a tutti! Sono Mons Anderson, architetto della piattaforma Mail.ru Cloud Solutions, racconterò come abbiamo costruito il nostro storage S3, come funziona, quali soluzioni si sono rivelate efficaci e quali avremmo dovuto cambiare se avessimo iniziato lo stesso progetto da zero ora.

Questo articolo è stato preparato sulla base di una presentazione a @Databases Meetup di 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 storage a oggetti e quali passi sono stati compiuti per il passaggio in produzione;
  • sulle modifiche al sistema operativo: failover e scalabilità;
  • come abbiamo implementato lo sharding e la resharing;
  • e anche sul lavoro con i certificati SSL.

Se non vuoi leggere, puoi guardare.

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

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

Lo storage cloud di Mail.ru è composto da server con dischi. In media, un server di storage moderno ha 36 dischi da 12–14 terabyte. Prima i dischi erano più piccoli, ma in tre anni la capacità dei dischi è aumentata e oggi siamo quasi a mezzo petabyte di dati grezzi.

I dischi di diversi server di storage vengono uniti in quelle che chiamiamo "coppie" (pair). Una coppia è una unità di archiviazione dei file. In sostanza, è un disco montato in una certa partizione a un certo percorso, dove possono risiedere file identificati tramite hash.

Coppia è un nome storico, rimasto fino ad oggi, anche se ora in una coppia non ci devono essere necessariamente solo due dischi. Possono essercene tre, e possono esserci anche varie forme di storage ibride, come 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 banche dati nel nostro storage, a partire dalle prime, sono Tarantool, non usiamo altre banche dati.

PairDB memorizza tutte le coppie, i loro stati, lo spazio libero, le possibilità di failover, gli ultimi errori. Può anche controllare autonomamente le coppie, aggiornare il loro stato, verificare se funzionano o meno. Quindi PairDB è una sorta di snapshot 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

Nei corsi sono archiviati dei file e, per sapere quale file si trova in quale corso, è necessaria un'altra base — FileDB. Essa memorizza il mapping, la definizione di corrispondenza: questo file si trova in questo corso, oltre a una piccola quantità di attributi necessari.

Architettura S3: 3 anni di evoluzione di Mail.ru Cloud StorageFile DB: luogo in cui è archiviato il file

Un altro anello importante è il servizio Nylon, un router per lavorare con i database. È un unico punto di accesso, permette di lavorare attraverso un'interfaccia unica sia con PairDB che con FileDB. Questo è un servizio stateless, esegue il bilanciamento delle richieste, comprende a quale shard di FileDB è necessario andare, sa quali corsi sono attivi e quali no.

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

Nylon: router per lavorare con i database

Inoltre, è necessario in qualche modo caricare i contenuti nell'archivio. A questo scopo esiste il servizio — Streamer. Esso fornisce due metodi HTTP: il metodo PUT per caricare contenuti nell'archivio e il metodo GET per prelevarli da lì. HTTP è un protocollo piuttosto popolare e comodo per la trasmissione dei dati.

Quando ci rivolgiamo a Streamer, esso, attraverso Nylon, si rivolge a PairDB, scopre a quale corso è possibile caricare il file e poi trasmette i dati tramite WebDAV a quel corso.

In sostanza, qualsiasi server di storage è un nginx più i dischi montati secondo i percorsi specificati. Possiamo caricare un file nell'archivio da Streamer, eliminarlo, rinominarlo o verificarne l'integrità. Quindi, è un'interfaccia comoda per l'interazione a basso livello con l'archivio.

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

Streamer: punto di accesso all'archivio

Cosa abbiamo aggiunto per realizzare l'archivio S3

Quindi, abbiamo esaminato la struttura di base dell'archivio al momento in cui ci siamo preparati a lanciare l'archivio S3. Con il metodo PUT potevamo caricare contenuti arbitrari e ottenere come identificatore di quei dati un hash. Con questo identificatore si poteva successivamente andare a prelevare il file originale. Ma questo non è sufficiente per implementare S3. Nel protocollo S3, oltre al semplice stoccaggio degli oggetti, ci sono:

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

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

I primi componenti

Un demone che implementa l'API S3. Questo è il standard S3 API di Amazon, che supporta l'elaborazione XML per i metadati e consente di trasmettere contenuti direttamente. Non abbiamo dovuto inventare nulla, tutto è descritto e documentato.

Davanti al servizio abbiamo anche installato Nginx. L'abbiamo utilizzato per la terminazione SSL, il bilanciamento del carico, e per alcune logiche in Lua (metriche, logging e tracciamento).

Per memorizzare i metadati S3 abbiamo scelto Tarantool. Nella prima versione, il demone S3 accedeva a questo database per i metadati, conservando il contenuto in un grande deposito tramite Streamer.

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

Modello oggetto per la memorizzazione

Vediamo come funziona S3. L'utente può creare un bucket — una raccolta di oggetti. Il bucket è indirizzato con il nome dell'host e costituisce un sottodominio del servizio. All'interno del bucket, l'utente può creare oggetti. L'identificatore dell'oggetto sarà l'URL. Il contenuto dell'oggetto è un blob, un array di dati binari che memorizzeremo nel deposito. Inoltre, l'oggetto ha attributi: il nome — appunto l'URL, ACL (lista di controllo degli accessi), altri attributi aggiuntivi o arbitrari — tutto questo è salvato nei metadati.

Uno schema normalizzato di questi dati può apparire così: ci sono progetti cui appartengono i bucket, a cui appartengono gli oggetti, e gli oggetti possono essere composti. Poiché uno dei modi per caricare un oggetto è a pezzi, 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 il billing in questo schema.
Il servizio di billing è stato implementato anche su Tarantool.

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

Modifiche allo storage S3: passi verso la produzione

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

In primo luogo, il sistema di rate limiting. Se avviassimo il servizio senza di esso, durante i picchi di carico potremmo sovraccaricare in modo imprevedibile una parte del sistema. Il rate limit dovrebbe funzionare in questo modo: ogni richiesta S3 arriva a un host specifico, questo host è l'identificatore del bucket e il bucket appartiene a un determinato cliente. Dobbiamo definire una funzione per il bucket che consenta di calcolare il rate limit.

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

Qui abbiamo nuovamente utilizzato Tarantool. I rate limit sono un cluster di 21 istanze, suddivise in gruppi, distribuite su tre nodi fisici e unite in un grande cluster topologico. Le modifiche di configurazione vengono propagate automaticamente: vengono impostati i rate limit, i valori predefiniti e la configurazione. Ogni bucket è servito da una sola istanza. Quando arriva una richiesta per un determinato bucket, viene calcolata l'istanza responsabile di quel bucket. All'interno di questo nodo, viene effettuato il conteggio del rate attuale delle richieste attraverso un algoritmo simile al Token Bucket. Successivamente, il sistema di rate limiting, in base ai dati attuali di carico e alle caratteristiche impostate per quel bucket specifico, determina se la richiesta può essere eseguita o meno. Il controllo dei limiti viene eseguito alla fase più iniziale dell'elaborazione della richiesta S3, proteggendo tutti gli altri elementi del sistema da sovraload.

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 a caldo. Di norma, l'accesso a un file singolo è gestito dall'intera catena: Streamer, FileDB, PairDB, Storage. Tuttavia, nel caso di accessi ripetuti a un file, ottimizziamo l'accesso a questo contenuto utilizzando una cache locale.

La cache è multilivello e realizzata con nginx, dischi locali, SSD e RAM. Non abbiamo utilizzato Tarantool qui, poiché è più comodo servire gli oggetti dal filesystem, consentendo di fare tiering della cache. Inoltre, abbiamo oggetti di grandi dimensioni 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 abbiamo avviato aveva una certa capacità calcolata, sufficiente per l'indagine, la comprensione del prodotto e per verificare che funzionasse.

Miglioramenti del sistema di combattimento: failover e scalabilità

Il sistema era già in esercizio, ma all'avvio abbiamo trascurato qualcosa: dovevamo aggiungere il failover e la scalabilità.

Il nostro demone S3 raccoglieva metadati tramite il protocollo Tarantool. Al posto del database originale, abbiamo implementato Tarantool, che fungeva da router proxy per le richieste di metadati. Dal punto di vista dell'applicazione che implementa l'API, nulla è cambiato: continuava a comunicare con il database tramite il protocollo Tarantool, ma il router poteva garantire un failover attivo. Ciò significava che potevamo verificare la disponibilità dei nodi, mantenere una pausa durante il passaggio e le interruzioni, e così via. Allo stesso tempo, non abbiamo modificato l'applicazione stessa.

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

Maggiore dettagli su come abbiamo implementato il partizionamento

La prossima questione che ci siamo posti era il partizionamento. Il sistema cresceva, aumentava il numero di oggetti e dovevamo garantire possibilità per una ulteriore crescita.

Torniamo allo schema dei dati: ci sono progetti, ci sono bucket, credenziali e fatturazione. Questi sono oggetti che con grande probabilità nel prossimo futuro non cresceranno oltre un'istanza, né per volume né per richieste. Pertanto, non ha senso partizionarli, e li abbiamo spostati in un'istanza separata che rimarrà non partizionata. Questo consente una gestione più coerente dei progetti e dei bucket, poiché esiste un'unica istanza non partizionata.

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

Ci sono anche oggetti nello schema che crescono linearmente: inizialmente erano centinaia di migliaia, ora il loro numero si misura in miliardi. Tali oggetti, insieme alle loro parti, dovevano essere spostati in un cluster partizionato.

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

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

Abbiamo anche aggiunto ulteriori tabelle e componenti:

  • il cestino, per distruggere vecchi progetti che vengono eliminati o congelati;
  • coda per attività di background, cioè lo storage principale può eseguire attività di background che devono essere effettuate nel cluster;
  • supporto per il lifecycle - un meccanismo che consente di lavorare con gli oggetti, gestendo il loro ciclo di vita.

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

Poiché parte dei dati è stata trasferita su shard, è necessaria una proxy di sharding. Si potrebbe riutilizzare il router per questo ruolo, ma una proxy di sharding separata, responsabile solo per lo sharding dei dati, consente di accedere ai dati nel router senza preoccuparsi dello sharding.

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

Racconterò a parte perché non abbiamo scelto una soluzione pronta, ma volevamo realizzare una funzione di sharding personalizzata.

Vediamo come è strutturata. Abbiamo 256 sharding disponibili. Per ciascun bucket, allocchiamo un intervallo usando una funzione consistente. È semplice: proprio come usate una funzione consistente per determinare a quale shard appartiene un bucket, definite lo shard iniziale e allocate un intervallo:

f(bucket, shards) = subset

Quindi, se prendiamo un bucket, possiamo dire che esso e i suoi dati risiederanno sempre su un sottoinsieme specifico di tutti gli shard. Questo riduce l'impatto di alcuni bucket su altri e semplifica il lavoro delle query map-reduce, quando ad esempio è necessario elencare gli oggetti del bucket. Per fare questo, è necessario interrogare tutti gli shard in cui questi oggetti sono memorizzati. Se gli oggetti fossero memorizzati su tutti gli shard, qualsiasi elenco influenzerebbe l'intero sistema, mentre qui influenzerebbe solo un sottoinsieme specifico.

Inoltre, ogni oggetto appartiene a un bucket specifico, quindi quando richiediamo un oggetto, ci rivolgiamo a un oggetto per nome nel bucket specifico. Possiamo quindi definire una funzione per l'oggetto non su tutto l'intervallo disponibile di shard, ma solo su un sottoinsieme del suo bucket:

f(object, subset) = shard

Prendiamo un oggetto specifico e come argomenti della funzione passiamo non tutti gli shard, ma solo il sottoinsieme del suo bucket, e otteniamo uno shard specifico.

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

Dunque, lo sharding è implementato, c'è una proxy di sharding. Resta solo da far interagire il router e il database dei metadati con la proxy di sharding. Ad esempio, per creare oggetti di copie shadow - quando creiamo un bucket, lo storage principale deve creare un rappresentante di questo bucket su tutti gli shard in cui deve essere presente.

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

Come abbiamo implementato il resharding

Il problema principale del sharding è il reshading. Era importante per noi realizzarlo senza downtime, poiché il sistema era già in produzione. Vi mostrerò come abbiamo risolto il problema con un esempio simile di migrazione dal vivo dei dati da un progetto a un altro.

Di seguito è riportato lo schema del nostro cluster, che abbiamo ottenuto dopo l'implementazione del sharding. Abbiamo nginx, l'API S3, un router, un database principale con i progetti, un proxy di sharding e, infine, gli shard.

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

In precedenza ho trascurato di menzionare che in un certo momento del progetto c'era un compito di prodotto: "Lanciare un altro storage, Icebox, come Hotbox, ma solo per i dati freddi". Fondamentalmente, uno storage simile, 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 per molto tempo senza alcun sharding. Alla fine abbiamo deciso di eliminarlo e di unire Hotbox e Icebox in un unico servizio, semplicemente separando le classi di archiviazione.

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

Per cominciare, abbiamo sincronizzato gli storage principali. Avevamo Tarantool e potevamo, durante la creazione di un oggetto, fare così:

  • il database riceve una richiesta per la creazione di un bucket, ad esempio in Hotbox;
  • Tarantool verifica in un altro database (in questo caso, Icebox) se quel bucket non esiste;
  • se il bucket esiste, il database comunica che non può essere creato, e si sincronizza come esistente.
    Architettura S3: 3 anni di evoluzione di Mail.ru Cloud Storage
    Sincronizzazione dei bucket

Nello storage che doveva ricevere tutti i dati, abbiamo introdotto per progetti e bucket un attributo che indicava dove si trovava quell'oggetto. Poteva essere archiviato localmente, cioè in Hotbox, in Icebox — in quel caso, non c'erano dati al riguardo nel nuovo storage, oppure poteva essere in stato di migrazione.

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

Successivamente 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 spostando gli host e aggiungendo le registrazioni appropriate in Nginx.

Dopo che il traffico è stato reindirizzato, Nginx e l'API di Icebox potevano essere rimossi.
Poi abbiamo rimosso Icebox nginx e S3 API — e tutto ha iniziato a funzionare:

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

Successivamente abbiamo avviato un processo di migrazione in background che lavora all'interno del database — analizza progetto per progetto e i loro bucket, imposta il segno Migrating per loro, trasferisce i dati e, al termine del trasferimento, imposta il segno Local.

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

Dopo il trasferimento dei dati, non abbiamo più bisogno del vecchio storage, e rimuoviamo le parti rimanenti del vecchio sistema, oltre a rimuovere dal codice il supporto per lo stato di migrazione.

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

Con gli stessi principi è stata effettuata anche la rishardizzazione dallo storage vecchio a quello shardato:

  • Abbiamo contrassegnato tutti i bucket come Non-sharded. Tutte le richieste a loro andavano nello storage originale, non shardato.
  • I nuovi bucket venivano creati immediatamente con stato Sharded.
  • Prendevamo i bucket uno per uno, impostavamo lo stato Migrating e trasferivamo i dati.

Le richieste venivano servite secondo il principio:

  • Leggiamo nel nuovo, poi nel vecchio.
  • Creiamo solo nel nuovo.
  • Aggiorniamo in due fasi: se non c'è nel nuovo, trasferiamo dal vecchio al nuovo, poi aggiorniamo.

Lavorare con i certificati SSL

Nel front-end utilizziamo Nginx. Nel nostro caso non si tratta di un normale Nginx, ma di OpenResty, Nginx con supporto per LuaJIT.

Un altro pezzo del sistema è il lavoro con i certificati SSL. Nel storage S3 puoi impostare un dominio personalizzato per l'accesso a un particolare bucket, semplicemente utilizzando CNAME. Ma senza HTTPS al giorno d'oggi non è possibile: un dominio personalizzato implica un certificato SSL personale.

Come ho già detto, Nginx è responsabile del bilanciamento e della terminazione SSL. Nel nostro caso non si tratta di un normale Nginx, ma di OpenResty, Nginx con supporto per LuaJIT.

Questo ci ha permesso di insegnare al nostro Nginx a fornire certificati arbitrari in modo abbastanza semplice. Inoltre, avevamo bisogno di fornire certificati dinamicamente (senza la necessità di registrarli 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 deposito di certificati abbiamo anche utilizzato Tarantool: questo consente di gestire i certificati esternamente e garantisce una resa estremamente rapida.

È stato anche implementato un demone separato, il cui compito è l'aggiornamento regolare dei certificati, che sono stati emessi utilizzando Let's Encrypt.

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

Cosa salverei e cosa farei diversamente se sviluppassi il sistema di storage da zero

Cosa sarebbe stato necessario utilizzare fin dall'inizio

Sharding immediato. Ha causato parecchi problemi il re-sharding. È facile da implementare, ma, se si avviano progetti che necessitano di scalabilità, è meglio iniziare subito con un cluster sharded, anche se con un numero minimo di nodi. L'implementazione dello sharding all'inizio è quasi gratuita rispetto all'integrazione dello sharding in un sistema esistente.

Lavorare con Tarantool tramite bilanciatori. Attualmente colleghiamo subito tutti i nuovi database attraverso i bilanciatori. Questo ci consente di espandere la funzionalità e ottenere una maggiore resilienza.

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

Funzionalità S3 «Versioning». Inizialmente sembrava che non fosse una funzionalità molto richiesta. Integrare questa possibilità nell'architettura di un sistema esistente è estremamente complicato.

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 impostarla come un servizio completamente separato.

Cosa è stata una decisione riuscita

Modello dei dati. La storia ha dimostrato che durante lo sviluppo del servizio ci allineiamo abbastanza precisamente con il modello di dati di Amazon, quindi possiamo implementare le funzionalità presenti lì.

Schema di sharding. Sosterrei gli stessi sharding a intervallo per i bucket, poiché questo consente di distribuire bene le richieste tra diversi bucket su un grande cluster.

Uso di Tarantool. Tarantool ha dato un grande supporto durante lo sviluppo e la modifica del servizio; abbiamo lavorato facilmente con i dati, trasformato e shardato lo storage senza dover salire al livello dell'applicazione.

Questa presentazione è stata pronunciata per la prima volta a @Databases Meetup da Mail.ru Cloud Solutions&Tarantool. Vedi video altre presentazioni e iscriviti agli annunci di 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, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster