
Artem Denisov ( , )
Badoo è il più grande sito di incontri al mondo. Attualmente abbiamo circa 330 milioni di utenti registrati in tutto il mondo. Ma ciò che è molto più importante nel contesto della nostra conversazione di oggi è che conserviamo circa 3 petabyte di fotografie degli utenti. Ogni giorno i nostri utenti caricano circa 3,5 milioni di nuove foto, e il carico di lettura si attesta intorno a 80.000 richieste al secondo. Questo è un carico considerevole per il nostro backend, e a volte presentiamo alcune difficoltà.

Parlerò del design di questo sistema, che archivia e restituisce le foto in generale, e darò uno sguardo dal punto di vista dello sviluppatore. Faremo una breve retrospettiva sulla sua evoluzione, dove segnerò i principali traguardi, ma parlerò in modo più dettagliato solo delle soluzioni che utilizziamo attualmente.
E ora iniziamo.

Come ho già detto, questa sarà una retrospettiva, e per cominciare prendiamo l'esempio più banale.

Abbiamo un compito generale: dobbiamo ricevere, archiviare e restituire le fotografie degli utenti. In questo formato, il compito è generale, possiamo utilizzare qualsiasi cosa:
- un moderno cloud storage,
- una soluzione preconfezionata, ce ne sono molte;
- possiamo configurare diverse macchine nel nostro data center e installare grandi dischi rigidi per archiviare le foto lì.
Badoo storicamente — e ora, come allora (ai tempi in cui è nato) — vive sui propri server, all'interno dei nostri data center. Quindi per noi questa opzione era ottimale.

Abbiamo semplicemente preso alcune macchine, le abbiamo chiamate 'photos' e ci siamo creati un cluster che archivia le foto. Ma sembra che ci sia qualcosa che manca. Per far funzionare tutto questo, dobbiamo in qualche modo definire su quale macchina archivieremo quali foto. E qui non c'è bisogno di scoprire l'America.

Aggiungiamo al nostro storage con informazioni sugli utenti un campo. Questo sarà la chiave di sharding. Nel nostro caso lo abbiamo chiamato place_id, e questo id indica il luogo in cui sono archiviate le foto degli utenti. Creiamo delle mappe.
Nella prima fase, possiamo farlo anche manualmente: diciamo che la foto di questo utente con tale luogo sarà archiviata su tale server. Grazie a questa mappa, sappiamo sempre dove salvare una foto quando un utente la carica e da dove restituirla.
È uno schema del tutto banale, ma presenta vantaggi significativi. Primo, è semplice, come ho già detto, e secondo, con questo approccio possiamo facilmente scalare orizzontalmente, semplicemente aggiungendo nuove macchine e inserendole nella mappa. Non serve fare altro.
Così è stato per un certo periodo.

Era intorno al 2009. Consegnavamo macchine, ne portavamo...
E a un certo punto abbiamo iniziato a notare che questo schema presentava alcuni svantaggi. Quali svantaggi?
Prima di tutto, la capacità limitata. Non possiamo inserire tanti dischi rigidi in un singolo server fisico come vorremmo. E col passare del tempo e l'aumento del dataset è diventato un problema.
Secondo. Si tratta di una configurazione non comune delle macchine, poiché tali macchine sono difficili da riutilizzare in altri cluster; sono piuttosto specifiche, cioè devono avere prestazioni basse, ma allo stesso tempo un grande disco rigido.
Tutto ciò risale al 2009, ma in realtà queste esigenze sono attuali anche oggi. Abbiamo una retrospettiva, quindi nel 2009 tutto andava male sotto questo aspetto.
E l'ultimo punto è il costo.

All'epoca i costi erano molto alti e dovevamo trovare delle alternative. Dovevamo ottimizzare meglio sia lo spazio nei data center che i server fisici su cui tutto era collocato. I nostri ingegneri di sistema avviarono una grande ricerca, esaminando vari scenari. Guardarono anche ai file system clusterizzati, come PolyCeph e Lustre. Lì c'erano problemi di prestazioni e manutenzione piuttosto gravosa. Decisero di non proseguire. Provarono a montare l'intero dataset tramite NFS su ogni macchina, per cercare di scalare in questo modo. Anche la lettura risultò problematica, tentarono diverse soluzioni da vari fornitori.
E alla fine ci decidemmo per l'uso di quello che chiamiamo Storage Area Network.

Questi sono grandi SHD, progettati per gestire grandi volumi di dati. Si tratta di scaffalature con dischi montati su macchine di erogazione finali tramite fibra ottica. Abbiamo quindi un pool di macchine abbastanza ridotto e questi SHD, che sono trasparenti per la nostra logica di erogazione, cioè per il nostro nginx o per qualsiasi altro, gestiscono le richieste per queste foto.
Questo sistema presenta vantaggi evidenti. Si tratta di SHD. È progettato per memorizzare foto. Risulta più economico che allestire semplicemente macchine con dischi rigidi.
Un secondo vantaggio.

La capacità è aumentata notevolmente, cioè possiamo collocare un volume di storage molto maggiore in uno spazio molto più ridotto.
Tuttavia, ci sono stati anche svantaggi che si sono manifestati abbastanza rapidamente. Con l'aumento del numero di utenti e del carico su questo sistema, sono emerse problematiche di performance. E il problema qui è piuttosto evidente: qualsiasi SHD, destinato a memorizzare molte foto in uno spazio ridotto, tende a soffrire di letture intense. Questo è attuale sia per qualsiasi storage cloud che per qualsiasi altra cosa. Attualmente non esiste uno storage ideale che sia infinitamente scalabile, dove si possa inserire di tutto e che resista bene alle letture, soprattutto a quelle casuali.

Come nel caso delle nostre foto, poiché le foto vengono richieste in modo incoerente, ciò influisce notevolmente sulle loro performance.
Anche rispetto ai numeri di oggi, se abbiamo un picco superiore a 500 RPS per le foto su una macchina connessa allo storage, già iniziano i problemi. Questo è stato abbastanza problematico per noi, perché il numero di utenti continua a crescere e la situazione non può che peggiorare. Dobbiamo ottimizzare in qualche modo.
Per ottimizzare, allora, abbiamo deciso di esaminare il profilo di carico — cosa stava succedendo e cosa dovevamo ottimizzare.

E qui tutto gioca a nostro favore.
Nel primo slide ho già detto: abbiamo 80.000 richieste al secondo per la lettura, con soli 3,5 milioni di upload al giorno. C'è quindi una differenza di tre ordini di grandezza. Ovviamente, dobbiamo ottimizzare la lettura e si capisce praticamente come.
C'è anche un altro piccolo dettaglio. La specificità del servizio è tale che quando una persona si registra, carica una foto, inizia a guardare attivamente le foto di altre persone, a mettere 'mi piace', e viene mostrata attivamente ad altri. Poi, trova un partner o meno, come va, e per un certo periodo smette di utilizzare il servizio. In quel momento, quando lo utilizza, le sue foto sono molto 'calde' — sono richieste e vengono visualizzate da molte persone. Appena smette, però, rapidamente esce da quelle esposizioni intense ad altre persone, come avveniva prima, e le sue foto praticamente non vengono più richieste.

Quindi abbiamo un dataset molto ristretto ma con un volume elevato di richieste. Una soluzione ovvia è quella di aggiungere una cache.
Una cache con LRU risolverà tutti i nostri problemi. Cosa facciamo?

Aggiungiamo davanti al nostro grande cluster di storage un altro cluster relativamente piccolo, che chiamiamo fotocache (photoscache). È, sostanzialmente, un semplice proxy di caching.
Come funziona internamente? Ecco il nostro utente, ecco lo storage. Tutto come prima. Cosa aggiungiamo tra di loro?

È semplicemente una macchina con un disco locale fisico che è veloce. Questo è un SSD, ad esempio. E su questo disco viene memorizzata qualche cache locale.
Come appare? L'utente invia una richiesta per una foto. NGINX la cerca prima nella cache locale. Se non c'è, fa semplicemente proxy_pass verso il nostro storage, scarica la foto da lì e la fornisce all'utente.
Ma è molto semplice e poco chiaro cosa succeda all'interno. Funziona più o meno in questo modo.

La cache è logicamente separata in tre livelli. Quando parlo di «tre livelli», non significa che ci sia un sistema complesso. No, si tratta semplicemente di tre directory nel filesystem:
- Questo è un buffer, dove finiscono le fotografie appena caricate dal proxy.
- Questo è il cache caldo, dove sono memorizzate le foto attualmente richieste attivamente.
- E il cache freddo, dove le foto vengono lentamente trasferite dal caldo quando ricevono meno richieste.
Affinché ciò funzioni, dobbiamo gestire questa cache in modo da spostare le fotografie al suo interno, ecc. Anche questo è un processo molto primitivo.

Nginx scrive semplicemente su access.log su RAMDisk per ogni richiesta, specificando il percorso dell'immagine che sta servendo attualmente (naturalmente, il percorso relativo) e la sezione da cui è stata servita. Cioè, potrebbe essere scritto "foto 1" e poi o buffer, o cache calda, o cache fredda, o proxy.
A seconda di questo, dobbiamo decidere cosa fare con la foto.
Su ogni macchina abbiamo un piccolo demone che legge costantemente questo log e memorizza le statistiche sull'uso delle varie fotografie.

Raccoglie semplicemente le informazioni, mantiene i contatori e periodicamente esegue le seguenti operazioni. Le fotografie che ricevono molte richieste vengono spostate nella cache calda, indipendentemente da dove si trovano.

Le fotografie che vengono richieste raramente e che sono state richieste meno frequentemente vengono gradualmente spostate dalla cache calda a quella fredda.

E quando la nostra cache si riempie, iniziamo semplicemente a rimuovere tutto dalla cache fredda senza fare distinzioni. E questo, tra l'altro, funziona molto bene.
Per garantire che la foto venga salvata immediatamente durante il proxying nel buffer, usiamo la direttiva proxy_store e il buffer è anch'esso su RAMDisk, quindi per l'utente funziona molto rapidamente. Questo riguarda l'interno stesso del server di caching.
Rimane la questione di come distribuire le richieste tra questi server.
Supponiamo che ci sia un cluster di venti macchine di storage e tre server di caching (è andata così).

Dobbiamo in qualche modo determinare quali richieste sono per quali foto e dove devono andare.
La soluzione più banale è il Round Robin. Oppure farlo in modo casuale?
Questo, ovviamente, presenta diversi svantaggi, perché useremo in modo molto inefficiente la cache in tale situazione. Le richieste verranno indirizzate a macchine casuali: qui è stata memorizzata, accanto non c'è più. E tutto questo, se funziona, funzionerà molto male. Anche con un numero ridotto di macchine nel cluster.
Dobbiamo in qualche modo determinare in modo inequivocabile su quale server inviare quale richiesta.
C'è un modo banale. Prendiamo l'hash dell'URL o l'hash della nostra chiave di sharding, che è nell'URL, e lo dividiamo interamente per il numero di server. Funzionerà? Funzionerà.

Cioè, abbiamo una richiesta sicura, ad esempio, per un "example_url" che andrà sempre sul server con indice "2", e la cache sarà costantemente utilizata nel modo migliore.
Ma sorge un problema con il resharding in un simile schema. Per resharding intendo il cambiamento del numero di server.
Supponiamo che il nostro cluster di caching non riesca a gestire il carico e decidiamo di aggiungere un'altra macchina.
Aggiungiamo.

Adesso tutto viene suddiviso non più su tre, ma su quattro. Di conseguenza, praticamente tutte le chiavi che avevamo, praticamente tutte le URL, ora risiedono su altri server. Tutta la cache viene invalidata istantaneamente. Tutte le richieste si riversano sul nostro cluster di storage, ne soffre, si verificano errori di servizio e utenti insoddisfatti. Non vogliamo che succeda.
Questa opzione non ci va bene.
Quindi, cosa dobbiamo fare? Dobbiamo in qualche modo utilizzare efficacemente la cache, inviare costantemente una richiesta allo stesso server, ma essere anche resilienti al resharding. E esiste una soluzione, non è poi così complicata. Si chiama hashing consistente.

Come appare?

Prendiamo una certa funzione dalla chiave di sharding e distribuiamo tutti i suoi valori su una circonferenza. Cioè, nel punto 0 si incontrano i suoi valori minimi e massimi. Poi, sulla stessa circonferenza, posizioniamo tutti i nostri server in modo simile:

Ogni server è identificato da un punto, e il settore che va da esso in senso orario è servito da questo host. Quando ci arrivano richieste, vediamo subito che, ad esempio, la richiesta A ha un certo hash e viene servita dal server 2. La richiesta B dal server 3. E così via.

Cosa succede in questa situazione durante il resharding?

Non invalidiamo tutta la cache, come avveniva prima, e non spostiamo tutte le chiavi, ma spostiamo ciascun settore di una piccola distanza in modo che nel posto libero, per così dire, possa rientrare il nostro sesto server, che desideriamo aggiungere, e lo aggiungiamo lì.

Certamente, in tale situazione anche le chiavi si spostano. Ma si spostano molto meno rispetto a prima. E vediamo che le nostre prime due chiavi rimangono sui loro server, e solo per l'ultima chiave cambia il server di caching. Funziona abbastanza bene, e se aggiungi nuovi host in modo incrementale, non ci sono grandi problemi. Aggiungi un po' alla volta, aspetti che la cache si riempia di nuovo e tutto funziona correttamente.
L'unica domanda che rimane riguarda i guasti. Supponiamo che un nostro server si sia guastato.

E in quel momento non vorremmo davvero rigenerare questa mappa, invalidare parte della cache e così via, se, ad esempio, il server è stato riavviato e dobbiamo gestire le richieste. Manteniamo semplicemente un'istanza di backup della cache fotografica in ogni centro, che funge da sostituto per qualsiasi macchina attualmente guasta. Se un server diventa improvvisamente inaccessibile, il traffico viene indirizzato lì. Naturalmente, non abbiamo alcuna cache, quindi è fredda, ma almeno le richieste degli utenti vengono elaborate. Se questo intervallo è breve, possiamo affrontarlo tranquillamente. C'è solo un carico maggiore sullo storage. Se l'intervallo è lungo, possiamo già decidere di rimuovere quel server dalla mappa o sostituirlo con un altro.
Questo riguarda il sistema di caching. Vediamo i risultati.
A prima vista, non sembra esserci nulla di complesso qui. Ma questo metodo di gestione della cache ci ha dato un tasso di hit di circa il 98%. Ovvero, di queste 80 mila richieste al secondo, solo 1600 arrivano allo storage, e questo è un carico perfettamente normale che riescono a sostenere agevolmente, abbiamo sempre una riserva.
Abbiamo collocato questi server in tre dei nostri data center, ottenendo tre punti di presenza: Praga, Miami e Hong Kong.

In questo modo, sono più o meno localizzati vicino a ciascuno dei nostri mercati target.
E come piacevole bonus, abbiamo ottenuto questo proxy di caching, il cui CPU in realtà rimane inattivo, perché non è così necessario per la consegna dei contenuti. E lì, usando NGINX+Lua, abbiamo implementato molte logiche utilitaristiche.

Ad esempio, possiamo sperimentare con webp o jpeg progressivi (questi sono formati moderni ed efficienti), vedere come influiscono sul traffico, prendere decisioni, attivarli per determinati paesi, e così via; effettuare il ridimensionamento dinamico o il ritaglio delle foto al volo.
Questo è un buon caso d'uso, ad esempio, quando avete un'app mobile che mostra foto, e l'app non vuole far utilizzare CPU al client per richiedere una grande foto e ridimensionarla poi a una dimensione per adattarla nella vista. Possiamo semplicemente specificare dinamicamente alcuni parametri nell'URL, e la cache fotografica ridimensionerà automaticamente l'immagine. Di solito, sceglie la dimensione che abbiamo fisicamente sul disco, il più vicino possibile a quella richiesta, e la downscalerà nelle coordinate specifiche.
A proposito, abbiamo reso disponibili al pubblico le registrazioni video degli ultimi cinque anni delle conferenze degli sviluppatori di sistemi ad alto carico . Guardate, studiate, condividete e iscrivetevi al .
Possiamo anche aggiungere molta logica di prodotto. Ad esempio, possiamo aggiungere diverse filigrane in base ai parametri dell'URL, possiamo sfocare le foto, rendere tutto sfocato o pixelato. Questo quando vogliamo mostrare una foto di una persona, ma non vogliamo mostrare il suo volto, funziona bene, è tutto realizzato qui.
Cosa abbiamo ottenuto? Abbiamo ottenuto tre punti di presenza, un buon tasso di hit, e nel contempo il CPU di queste macchine non rimane inattivo. È diventato, ovviamente, più importante di prima. Dobbiamo installare macchine più potenti, ma ne vale la pena.
Questo riguarda la consegna delle foto. Qui tutto è abbastanza chiaro e ovvio. Penso di non aver scoperto l'America, così funziona praticamente qualsiasi CDN.
E, molto probabilmente, l'ascoltatore esperto potrebbe aver avuto la domanda: perché non sostituire tutto con un CDN? Sarebbe più o meno la stessa cosa, tutti i moderni CDN possono farlo. E ci sono diverse ragioni.
La prima sono le foto.

Questo è uno dei punti chiave della nostra infrastruttura, e abbiamo bisogno di avere il maggior controllo possibile su di esse. Se si tratta di una soluzione di un fornitore terzo e non si ha alcun potere su di essa, diventa abbastanza difficile viverci quando si ha un grande dataset e un flusso molto elevato di richieste degli utenti.
Facciamo un esempio. Attualmente, sulla nostra infrastruttura, possiamo accedere a una macchina, ad esempio in caso di problemi o rumori sotterranei, per fare debug, per così dire. Possiamo raccogliere delle metriche specifiche di cui abbiamo bisogno, sperimentare in vari modi, osservando come ciò influisce sui grafici e così via. Attualmente, stiamo raccogliendo una grande quantità di statistiche da questo cluster di cache. E periodicamente le esaminiamo e analizziamo a lungo alcune anomalie. Se fosse tutto lato CDN, sarebbe molto più difficile controllarlo. Oppure, ad esempio, se si verifica un incidente, sappiamo cosa è successo, sappiamo come gestirlo e superarlo. Questa è la prima conclusione.
La seconda conclusione è più di tipo storico, poiché il sistema si è sviluppato a lungo e ci sono state molte diverse esigenze aziendali in vari momenti, e non sempre queste si adattano al concetto di CDN.
E il punto che ne deriva è che

C'è molta logica specifica nei nostri foto-cache, che non sempre può essere aggiunta su richiesta. È difficile che un CDN introduca qualcosa di personalizzato per la vostra richiesta. Ad esempio, la crittografia degli URL, se non volete che il cliente possa modificare qualcosa. Volete cambiare l'URL sul server e crittografarlo, per poi restituire qui alcuni parametri dinamici.
Qual è la conclusione che si impone? Nel nostro caso, il CDN non è un'ottima alternativa.

E nel vostro caso, se avete esigenze aziendali specifiche, potete implementare tranquillamente ciò che vi ho mostrato. E con un profilo di carico simile, funzionerà benissimo.
Ma se avete una soluzione generale e la vostra esigenza non è molto specifica, potete tranquillamente usare un CDN. O se per voi è molto più importante il tempo e le risorse, piuttosto che il controllo.

E i moderni CDN hanno praticamente tutto ciò di cui vi ho parlato ora. Ad eccezione di alcune funzionalità più o meno.
Questo riguarda la distribuzione delle fotografie.
Ora spostiamoci un po' più avanti nella nostra retrospettiva e parliamo dell'archiviazione.
L'anno era il 2013.

I server di caching erano stati aggiunti, e i problemi con le performance erano scomparsi. Tutto andava bene. Il dataset cresceva. Nel 2013 avevamo circa 80 server connessi agli storage, e circa 40 server di caching in ogni data center. Parliamo di 560 terabyte di dati per ogni data center, quindi intorno a un petabyte in totale.

E con la crescita del dataset, i costi operativi sono aumentati notevolmente. In cosa consisteva questo?

Nello schema disegnato — con SAN, le macchine collegate e le cache — ci sono molti punti di guasto. Se prima abbiamo già affrontato i guasti dei server di caching, dove tutto era relativamente prevedibile e chiaro, sul lato storage era molto peggio.
In primo luogo, il Storage Area Network (SAN) stesso può guastarsi.
In secondo luogo, è collegato tramite fibra ottica alle macchine finali. Possono esserci problemi con le schede ottiche e gli switch.

Ce ne sono certamente meno rispetto al SAN stesso, ma sono comunque punti di guasto.
In seguito, la macchina stessa, collegata allo storage, può anche guastarsi.

In totale, abbiamo tre punti di guasto.
Inoltre, al di là dei punti di guasto, c'è la manutenzione pesante degli storage stessi.
È un sistema complesso e multi-componente, e per gli ingegneri di sistema può essere difficile gestirlo.
E l'ultimo, e più importante punto. Se in uno di questi tre punti si verifica un guasto, abbiamo una probabilità non nullo di perdere i dati degli utenti, poiché il filesystem potrebbe danneggiarsi.

Supponiamo che il filesystem si sia danneggiato. Il suo ripristino richiede tempo — può richiedere una settimana con grandi volumi di dati. Inoltre, alla fine, probabilmente otterremo una serie di file incomprensibili da abbinare alle fotografie degli utenti. E rischiamo di perdere dati. Il rischio è piuttosto alto. E più spesso si verificano tali situazioni, e più problemi sorgono lungo tutta questa catena, maggiore è il rischio.
Bisognava fare qualcosa a riguardo. E abbiamo deciso che era necessario semplicemente fare un backup dei dati. È davvero una soluzione ovvia e valida. Cosa abbiamo fatto?

Ecco com'era il nostro server, collegato allo storage, prima. C'era una partizione principale, un dispositivo a blocchi che rappresenta in realtà un mount su uno storage remoto tramite fibra ottica.
Abbiamo semplicemente aggiunto una seconda partizione.

Abbiamo installato un secondo storage (fortunatamente, è relativamente economico), e lo abbiamo chiamato partizione di backup. Anche questo è collegato tramite fibra ottica, sulla stessa macchina. Ma dobbiamo in qualche modo sincronizzare i dati tra di loro.
Qui abbiamo semplicemente creato una coda asincrona accanto.

Non è molto carica. Sappiamo che abbiamo pochi record. La coda è semplicemente una tabella in MySQL, dove si scrivono righe tipo "devo fare il backup di questa foto". A ogni modifica o upload copiamo dalla partizione principale al backup in modo asincrono o attraverso qualche background worker.
In questo modo abbiamo sempre due partizioni consistenti. Anche se una parte di questo sistema si guasta, possiamo sempre sostituire la partizione principale con il backup, e tutto continuerà a funzionare.
Ma a causa di questo, il carico in lettura aumenta notevolmente, poiché oltre ai clienti che leggono dalla partizione principale, perché prima guardano la foto lì (è più fresca), poi cercano sul backup, se non la trovano (ma questo lo fa semplicemente NGINX), c'è anche il nostro sistema di backup che ora legge dalla partizione principale. Non che fosse un collo di bottiglia, ma non volevamo aumentare il carico, fondamentalmente, semplicemente così.
E abbiamo aggiunto un terzo disco, che è un piccolo SSD, e l'abbiamo chiamato buffer.

Ecco come funziona ora.
L'utente carica una foto sul buffer, poi viene generato un evento nella coda che dice che deve essere copiata su entrambe le partizioni. Viene copiata, e la foto vive per un certo periodo (diciamo un giorno) sul buffer, e solo dopo viene purgata. Questo migliora notevolmente l'esperienza utente, perché l'utente carica la foto, e solitamente riceve immediatamente richieste, oppure aggiorna la pagina. Ma tutto dipende dall'applicazione che fa l'upload.
O, per esempio, altre persone a cui inizia a essere mostrata, inviano subito richieste per quella foto. Non è ancora nella cache, la prima richiesta avviene molto rapidamente. Fondamentalmente, è come con la foto-cache. Lo storage lento non partecipa affatto a questo. E quando dopo un giorno viene purgata, è già stata al 99% cacheata nel nostro layer di caching, oppure probabilmente non serve più a nessuno. Quindi, l'esperienza utente è migliorata notevolmente grazie a queste semplici manovre.
E, soprattutto, abbiamo smesso di perdere dati.

Possiamo dire che abbiamo smesso potenziali di perdere dati, perché in realtà non ne perdevamo molto. Ma c'era un pericolo. Vediamo che questa soluzione è, ovviamente, buona, ma è un po' come attenuare i sintomi del problema, invece di risolverlo completamente. E ci sono rimasti alcuni problemi.
Innanzitutto, c'è un punto di guasto rappresentato dal fisico host su cui funziona tutta questa macchina, che non è scomparso.

In secondo luogo, ci sono rimasti problemi con i SAN, il loro pesante maintenance e così via. Non era un fattore critico, ma volevamo provare a vivere senza di essi.
E abbiamo creato la terza versione (in realtà la seconda) — la versione di riserva. Come è stata fatta?
Ecco cosa c'era –

Le principali problematiche sono legate al fatto che siamo su un host fisico.
Innanzitutto, rimuoviamo i SAN, perché vogliamo sperimentare, vogliamo provare a usare semplicemente dischi rigidi locali.

Siamo già nel 2014-2015, e in quel periodo la situazione con i dischi e la loro capacità in un host era molto migliorata. Abbiamo deciso, perché non provare.
E poi prendiamo semplicemente la nostra partizione di backup e la trasferiamo fisicamente su una macchina separata.

In questo modo otteniamo questo schema. Abbiamo due macchine che memorizzano lo stesso dataset. Si riservano completamente l'un l'altra e sincronizzano i dati attraverso la rete tramite una coda asincrona nello stesso MySQL.

Perché questo funziona bene — perché abbiamo pochi record. Cioè, se la scrittura fosse comparabile alla lettura, potremmo avere qualche overhead di rete e problemi. Ci sono pochi record, molte letture — questo metodo funziona bene, quindi copiamo abbastanza raramente foto tra questi due server.
In quale modo funziona, se lo guardiamo un po' più in dettaglio.

Upload. Il bilanciatore sceglie semplicemente host casuali dalla coppia e fa l’upload su di essi. Naturalmente esegue controlli di salute, assicurandosi che la macchina non sia down. Cioè, fa upload solo su server attivi, e poi attraverso una coda asincrona tutto viene copiato sul suo vicino. Con l'upload è tutto estremamente semplice.
Con il compito è un po' più complesso.

Qui ci ha aiutato Lua, perché su NGINX vaniglia fare una logica del genere può essere difficile. Iniziamo a fare una richiesta al primo server, vediamo se la foto è lì, perché potenzialmente potrebbe essere stata caricata, per esempio, sul vicino, e qui non è ancora arrivata. Se la foto è lì, è buono. La diamo subito al cliente e, se possibile, la cache.

Se non c'è, facciamo semplicemente una richiesta al vicino e la otteniamo garantita da lì.

Quindi, possiamo dire nuovamente che potrebbero esserci problemi di performance, poiché i round trip costanti — abbiamo caricato la foto, ma qui non c'è, stiamo facendo due richieste invece di una, dovrebbe funzionare lentamente.
Nella nostra situazione non funziona lentamente.

Raccogliamo molte metriche su questo sistema, e il tasso di hit di questo meccanismo è di circa il 95%. Quindi, il ritardo di questo backup è molto ridotto e per questo motivo, dopo che la foto è stata caricata, la recuperiamo quasi sempre al primo tentativo, senza dover fare due viaggi.
Così, cosa abbiamo ottenuto di interessante?
In precedenza avevamo un'area di backup principale e leggevamo da essa in sequenza. Cioè, cercavamo sempre prima nel principale e poi nel backup. Questo era un solo passaggio.
Ora utilizziamo la lettura da entrambe le macchine simultaneamente. Distribuiamo le richieste in Round Robin. In una piccola percentuale di casi facciamo due richieste. Ma ora, in generale, abbiamo il doppio della capacità di lettura rispetto a prima. E il carico è nettamente diminuito sia sulle macchine di erogazione che direttamente sugli storage che avevamo a quel tempo.
Per quanto riguarda l'affidabilità. Fondamentalmente, è per questo che abbiamo lottato. Con l'affidabilità, qui tutto si è rivelato eccellente.

Un'unità smette di funzionare.

Nessun problema! L'ingegnere di sistema può anche non svegliarsi di notte, può aspettare fino al mattino, non ci sarà nulla di grave.
Anche se, in caso di guasto di questa macchina, la coda si ferma, non ci sono problemi, i log si accumuleranno prima sulla macchina attiva, e poi si ripristineranno nella coda per essere elaborati dalla macchina che tornerà operativa dopo un po'.

Lo stesso vale per la manutenzione. Semplicemente spegniamo una delle macchine, la rimuoviamo manualmente da tutti i pool, smette di ricevere traffico, svolgiamo un po' di manutenzione, sistemiamo delle cose e dopo che la rimettiamo in servizio, questo backup si recupera piuttosto rapidamente. Cioè, in un giorno di inattività di una macchina, si recupera entro pochi minuti. Questo è davvero poco. Con l'affidabilità, ripeto, qui tutto è fantastico.
Quali conclusioni possiamo trarre da questo schema di riserva?
Abbiamo ottenuto affidabilità.
Semplice gestione. Poiché le macchine hanno dischi rigidi locali, è molto più comodo per gli ingegneri che ci lavorano.
Abbiamo ottenuto una doppia capacità di lettura.
Questo è un ottimo bonus in aggiunta all'affidabilità.
Ma ci sono anche problemi. Ora abbiamo uno sviluppo molto più complesso di alcune funzionalità a causa del fatto che il sistema è diventato 100% eventualmente coerente.

Dobbiamo, diciamo, in qualche lavoro in background, pensare costantemente: «Su quale server siamo attualmente in esecuzione?», «È sicuro che qui ci sia la foto aggiornata?» e così via. Questo, naturalmente, è tutto incapsulato in wrapper, quindi per il programmatore che scrive la logica di business è trasparente. Tuttavia, è apparso uno strato complesso. Ma siamo disposti ad accettarlo in cambio dei vantaggi che ne abbiamo ottenuto.
E qui si presenta nuovamente un certo conflitto.
All'inizio ho detto che conservare tutto su dischi rigidi locali è sbagliato. E ora dico che ci è piaciuto.
Sì, effettivamente, col passare del tempo la situazione è cambiata notevolmente, e ora questo approccio ha molti vantaggi. In primo luogo, otteniamo una gestione molto più semplice.
In secondo luogo, è più performante, poiché non abbiamo quei controllori automatici, le connessioni ai rack di dischi.
Lì c'è una grande macchina, qui ci sono solo alcuni dischi che sono stati configurati in RAID direttamente sulla macchina.
Ma ci sono anche svantaggi.

Questo costa circa 1,5 volte di più rispetto all'uso dei SAN, anche ai prezzi attuali. Pertanto, abbiamo deciso di non convertire completamente il nostro grande cluster in macchine con dischi rigidi locali e di mantenere una soluzione ibrida.
Una parte delle macchine lavora con dischi rigidi (non la metà, circa il 30%, probabilmente). E la parte rimanente sono vecchie macchine su cui c'era il primo schema di riserva. Le abbiamo semplicemente rimontate, poiché non abbiamo bisogno né di nuovi dati né di altro, abbiamo semplicemente spostato i mount da un host fisico a due.
E abbiamo ottenuto una grande capacità di lettura, e ci siamo espansi. Prima montavamo uno storage su una macchina, ora montiamo quattro storage su una coppia, ad esempio. E questo funziona normalmente.
Facciamo un breve riassunto di cosa abbiamo ottenuto, per cosa abbiamo lottato, e se ci siamo riusciti.
Risultati
Abbiamo utenti — ben 33 milioni.
Abbiamo tre punti di presenza — Praga, Miami, Hong Kong.
In essi si trova uno strato di caching, che consiste in server con dischi locali veloci (SSD), su cui opera una semplice configurazione di NGINX, il suo access.log e demoni scritti in Python che gestiscono tutto ciò e controllano la cache.
Se lo desiderate nel vostro progetto, se per voi le immagini non sono così critiche come per noi, o se il trade-off tra controllo e velocità di sviluppo e costi delle risorse pende dalla vostra parte, allora potete tranquillamente sostituirlo con un CDN, che oggi giorno funziona bene.
Successivamente, c'è uno strato di archiviazione, dove abbiamo cluster di coppie di server che si riservano l'un l'altro, copiando file in modo asincrono da uno all'altro ad ogni modifica.
Alcuni di questi server lavorano con dischi rigidi locali.
Alcuni di questi server sono collegati a SAN.

Da un lato, questo è più comodo per la gestione e leggermente più performante, dall'altro lato, è vantaggioso anche in termini di densità di collocazione e costo per gigabyte.
Ecco una breve panoramica dell'architettura di ciò che abbiamo realizzato e come si è sviluppato.
Alcuni consigli semplici da parte del capoprogetto.
In primo luogo, se mai decidete che sia urgente migliorare la vostra infrastruttura fotografica, misurate innanzitutto, perché forse non è necessario alcun miglioramento.

Ad esempio, abbiamo un cluster di server che distribuiscono fotografie dagli allegati nelle chat, e lì funziona ancora un sistema del 2009, e nessuno ha problemi. Tutti sono soddisfatti.
Per misurare, cominciate a collegare una serie di metriche, guardatele e poi decidete cosa vi dispiace e cosa deve essere migliorato. Per misurare, abbiamo uno strumento fantastico chiamato Pinba.
Questo strumento raccoglie statistiche da NGINX in modo molto dettagliato per ogni richiesta e codice di risposta, e distribuzione dei tempi — qualsiasi cosa. Ha binding per vari sistemi di analisi, e potete poi visualizzarlo in modo elegante.
Misurate prima — poi migliorate.
Inoltre, ottimizziamo la lettura con la cache, la scrittura con il sharding, ma questo è un punto ovvio.

Se state appena iniziando a costruire il vostro sistema, è molto meglio trattare le fotografie come file immutabili. Poiché in questo modo eliminate subito tutta una serie di problemi legati all'invalidazione della cache, a come la logica deve trovare la versione corretta della fotografia, e così via.

Supponiamo di aver caricato una foto, poi di averla ruotata, fate in modo che sia un file fisicamente diverso. Cioè, non pensate: adesso risparmierò un po' di spazio, scrivo nello stesso file, cambio versione. Questo non funziona mai bene, e poi ci sono molti mal di testa.
Il punto seguente riguarda il resize al volo.
In passato, quando gli utenti caricavano una foto, creavamo immediatamente una serie di dimensioni per ogni evenienza, per diversi clienti, e tutti si trovavano sul disco. Ora ci siamo rinunciati.
Abbiamo mantenuto solo tre dimensioni principali: piccola, media e grande. Tutto il resto lo ridimensioniamo dal formato che ci viene richiesto in Uport, semplicemente adattandolo e restituendolo all'utente.
Il costo del CPU nello strato di caching risulta molto inferiore a se dovessimo continuamente rigenerare queste dimensioni su ogni storage. Supponiamo di voler aggiungere un nuovo formato, ci vorrebbe un mese per eseguire uno script ovunque che faccia tutto questo in modo preciso, senza bloccare il cluster. Cioè, se c'è la possibilità di scegliere, è meglio fare il minor numero possibile di formati fisici, ma con una certa varietà, diciamo tre. E tutto il resto da ridimensionare al volo con moduli pronti. Questo è ora molto facile e accessibile.
E il backup incrementale asincrono è eccellente.
Come ha dimostrato la nostra esperienza, questo schema funziona bene con la copia ritardata dei file modificati.

L'ultimo punto è anche ovvio. Se nella vostra infrastruttura non ci sono attualmente problemi del genere, ma c'è qualcosa che potrebbe rompersi, sicuramente si romperà quando ci sarà un po' di carico in più. Quindi è meglio pensarci in anticipo e non trovarsi a dover affrontare problemi.
Contatti
»
»
Questa presentazione è la trascrizione di uno dei migliori interventi alla conferenza per sviluppatori di sistemi ad alta scalabilità. Manca meno di un mese alla conferenza HighLoad++ 2017.
Abbiamo già pronta , ora stiamo attivamente formando il programma.
Quest'anno continuiamo a esplorare il tema delle architetture e della scalabilità:
- / Игорь Васильев
- / Дмитрий Егоров
- / Анатолий Пласковский
- / Роман Шеховцов, Алексей Громатчиков
- / Филипп Дельгядо
Alcuni di questi materiali sono usati anche nel nostro corso online di formazione per lo sviluppo di sistemi ad alto carico è una serie di email, articoli, materiali e video selezionati appositamente. Attualmente il nostro manuale contiene oltre 30 materiali unici. Unisciti a noi!
Fonte: habr.com
