
Artem Denisov ( , )
Badoo è il sito di incontri più grande 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, è il fatto che memorizziamo circa 3 petabyte di foto degli utenti. Ogni giorno i nostri utenti caricano circa 3,5 milioni di nuove foto e il carico di lettura è di circa 80.000 richieste al secondo. È un bel po' per il nostro backend, e a volte ci sono delle difficoltà.

Parlerò del design di questo sistema, che memorizza e restituisce le foto in generale, e darò uno sguardo a esso dal punto di vista del programmatore. Farò una breve retrospettiva su come si è evoluto, dove segnerò i principali traguardi, ma parlerò più dettagliatamente solo delle soluzioni che utilizziamo attualmente.
E ora cominciamo.

Come ho già detto, sarà una retrospettiva, e per cominciarla, prendiamo il più semplice degli esempi.

Abbiamo un compito generale, dobbiamo ricevere, memorizzare e restituire le foto degli utenti. In questo modo il compito è generale, possiamo usare qualsiasi cosa:
- un moderno storage cloud,
- una soluzione preconfezionata, di cui ce ne sono molte in questo momento;
- possiamo configurare alcune macchine nel nostro data center e installare grandi dischi rigidi per memorizzare le foto lì.
Badoo storicamente — e sia allora che ora (nel periodo in cui tutto ciò ha appena preso piede) — vive sui propri server, all'interno dei nostri data center. Pertanto, per noi questa opzione era ottimale.

Abbiamo semplicemente preso alcune macchine, le abbiamo chiamate "photos", ed è nato un cluster che memorizza le foto. Ma sembra che manchi qualcosa. Per fare in modo che tutto ciò funzioni, dobbiamo in qualche modo determinare su quale macchina memorizzeremo quali foto. E non c'è bisogno di scoprire l'America nemmeno qui.

Aggiungiamo al nostro storage con informazioni sugli utenti un campo. Questo sarà la chiave per lo sharding. Nel nostro caso, lo abbiamo chiamato place_id, e questo id dei luoghi indica il posto in cui sono memorizzate le foto degli utenti. Creiamo le mappe.
Nella prima fase, si può anche fare a mano: diciamo che la foto di questo utente con questo posto atterrerà su questo server. Grazie a questa mappa sappiamo sempre quando l'utente carica una foto, dove salvarla e sappiamo da dove restituirla.
È uno schema assolutamente banale, ma ha vantaggi piuttosto significativi. Il primo è che è semplice, come ho già detto, e il secondo è che con questo approccio possiamo facilmente scalare orizzontalmente, semplicemente consegnando nuove macchine e aggiungendole alla mappa. Non c'è bisogno di fare altro.
Così è stato per un certo periodo da noi.

Era più o meno nel 2009. Consegnavamo macchine, consegnavamo...
E a un certo punto abbiamo cominciato a notare che questo schema presentava alcuni svantaggi. Quali svantaggi?
In primo luogo, la capacità limitata. Su un singolo server fisico non possiamo mettere così tanti dischi rigidi come vorremmo. E nel tempo, con la crescita del dataset, è diventato un problema.
E in secondo luogo. Questa è una configurazione non tipica delle macchine, poiché tali macchine sono difficili da riutilizzare in altri cluster, sono abbastanza specifiche, cioè devono essere a bassa potenza, ma allo stesso tempo con ampi dischi rigidi.
Tutto ciò risale al 2009, ma in linea di massima queste richieste sono ancora valide oggi. Abbiamo una retrospettiva, quindi nel 2009 tutto andava male in questo senso.
E l'ultimo punto è il prezzo.

Il prezzo allora era davvero elevato, e dovevamo cercare delle alternative. Cioè, dovevamo in qualche modo ottimizzare meglio sia lo spazio nei data center che i server fisici su cui tutto questo era ospitato. E i nostri ingegneri di sistema iniziarono una grande ricerca, rivedendo un sacco di diverse opzioni. Esaminarono anche i file system cluster, come PolyCeph e Lustre. C'erano problemi di prestazioni e una gestione piuttosto pesante. Hanno rinunciato. Hanno provato a montare l'intero dataset tramite NFS su ciascuna macchina, per cercare di scalare in questo modo. Anche la lettura non è andata bene, hanno provato diverse soluzioni da diversi fornitori.
E alla fine abbiamo deciso di utilizzare quella che chiamiamo Storage Area Network.

Si tratta di grandi SHD, progettati per l'archiviazione di ampie volumetrie di dati. Rappresentano scaffali con dischi montati su macchine di output finali tramite fibra ottica. Così facendo, abbiamo un certo pool di macchine, sufficientemente ridotto, e queste SHD, che sono trasparenti per la nostra logica di output, cioè per il nostro nginx o altro, gestiscono le richieste per queste fotografie.
Questo approccio ha evidenti vantaggi. Si tratta di SHD. Sono orientati a memorizzare foto. Risulta più economico rispetto a configurare direttamente macchine con dischi rigidi.
Secondo vantaggio.

Il fatto è che la capacità è aumentata notevolmente, cioè possiamo archiviare molto più storage in un volume molto ridotto.
Ma ci sono stati anche svantaggi, che sono emersi abbastanza rapidamente. Con l'aumento del numero di utenti e del carico su questo sistema, sono iniziate a sorgere problematiche di prestazioni. E il problema è piuttosto evidente: qualsiasi SHD progettata per immagazzinare molte foto in poco spazio di solito soffre di letture intensive. Questo vale realmente anche per qualsiasi storage cloud o qualunque altra cosa. Attualmente, non esiste uno storage ideale che sia scalabile all'infinito, nel quale si possa infilarci di tutto e che gestisca molto bene le letture. Soprattutto le letture casuali.

Come nel caso delle nostre foto, perché le fotografie vengono richieste in modo non sequenziale, e questo incide notevolmente sulle loro prestazioni.
Anche secondo le attuali statistiche, se superiamo i 500 RPS per macchina per le foto collegate allo storage, inizia a sorgere qualche problema. E questo è stato piuttosto problematico per noi, poiché il numero di utenti cresce e tutto dovrebbe diventare solo peggio. Dobbiamo ottimizzarlo in qualche modo.
Per ottimizzare, abbiamo deciso di esaminare profili di carico — cosa sta accadendo e cosa deve essere ottimizzato.

E qui tutto gioca a nostro favore.
Nel primo slide ho già menzionato: abbiamo 80 mila richieste al secondo per la lettura con solo 3,5 milioni di upload al giorno. Cioè, c'è una differenza di tre ordini di grandezza. È chiaro che bisogna ottimizzare le letture e quasi è evidente come.
C'è un altro piccolo dettaglio. La specificità del servizio è tale che una persona si registra, carica una foto, quindi inizia a guardare attivamente altre persone, a metterle mi piace, e viene mostrata attivamente ad altre persone. Poi trova una coppia o non la trova, dipende, e per un certo periodo smette di utilizzare il servizio. In quel momento, quando lo usa, le sue foto sono molto richieste — vengono visualizzate da molte persone. Appena smette di farlo, rapidamente esce da quelle intense visualizzazioni per altre persone, come avveniva prima, e le sue foto praticamente non vengono più richieste.

Cioè, abbiamo un dataset molto piccolo ma molto caldo. Tuttavia, ci sono fin troppe richieste. E una soluzione ovvia qui è aggiungere una cache.
Una cache con LRU risolverà tutti i nostri problemi. Cosa facciamo?

Aggiungiamo prima del nostro grande cluster con lo storage un altro relativamente piccolo, che chiamiamo fotocache (photoscache). Questo è, in sostanza, solo un proxy che fa 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. Per esempio, con SSD. E su questo disco viene memorizzato un certo caching locale.
Come appare? L'utente invia una richiesta per una foto. NGINX la cerca prima nel cache locale. Se non c'è, semplicemente fa proxy_pass sul nostro storage, scarica la foto di lì e la fornisce all'utente.
Ma è molto banale e non è chiaro cosa succeda dentro. Funziona più o meno così.

La cache è logicamente suddivisa in tre strati. Quando dico "tre strati", non significa che ci sia un sistema complicato. No, sono semplicemente tre directory nel file system:
- Questo è un buffer, dove vanno a finire le fotografie appena caricate dal proxy.
- Questo è il cache caldo, in cui vengono conservate le foto attualmente richieste.
- E la cache fredda, dove gradualmente le foto vengono spostate da quella calda, quando ricevono meno richieste.
Affinché ciò funzioni, dobbiamo in qualche modo gestire questa cache, dobbiamo spostare le fotografie al suo interno, ecc. Anche questo è un processo molto primitivo.

Nginx registra semplicemente ogni richiesta su RAMDisk access.log, nel quale indica il percorso dell'immagine che sta servendo attualmente (il percorso relativo, ovviamente), e il modo in cui è stata servita. Cioè, può esserci scritto "foto 1" e poi un buffer, o una cache calda, o una cache fredda, o un proxy.
A seconda di questo, dobbiamo in qualche modo prendere decisioni su cosa fare con l'immagine.
Su ciascuna macchina abbiamo un piccolo demone che legge costantemente questo log e mantiene in memoria le statistiche sull'uso delle varie immagini.

Semplicemente raccoglie dati, tiene dei contatori e periodicamente fa quanto segue. Le foto più richieste, per le quali arrivano molte richieste, vengono spostate nella cache calda, ovunque si trovino.

Le foto che vengono richieste raramente e che vengono richieste sempre meno, vengono gradualmente espulse dalla cache calda a quella fredda.

E quando nella cache finisce lo spazio, iniziamo semplicemente a rimuovere tutto dalla cache fredda senza alcuna discriminazione. E questo, per inciso, funziona bene.
Per garantire che l'immagine venga salvata immediatamente durante il proxy pass nel buffer, utilizziamo la direttiva proxy_store e il buffer è anch'esso RAMDisk, cioè per l'utente funziona molto rapidamente. Questo riguarda le internes del server di caching stesso.
Resta da affrontare la questione di come distribuire le richieste tra questi server.
Supponiamo ci sia un cluster di venti macchine di storage e tre server di caching (è così che è andata).

Dobbiamo in qualche modo determinare quali richieste sono associate a quali immagini e dove indirizzarle.
La soluzione più banale è il Round Robin. O farlo casualmente?
Questo, ovviamente, ha una serie di svantaggi, perché utilizzeremo molto inefficacemente la cache in tal caso. Le richieste verranno indirizzate su macchine casuali: qui è stata memorizzata, sulla macchina accanto non c'è più. E funzionerà, se funzionerà, molto male. Anche con un numero ridotto di macchine nel cluster.
Dobbiamo in qualche modo determinare inequivocabilmente su quale server indirizzare quale richiesta.
C'è un modo banale. Prendiamo l'hash dell'URL o l'hash della chiave di sharding che è nell'URL, e lo dividiamo per il numero di server. Funzionerà? Sicuramente.

Cioè, abbiamo una richiesta al cento per cento, ad esempio, per un certo «example_url» che arriverà sempre al server con indice «2», e la cache sarà costantemente utilizzata nel miglior modo possibile.
Ma sorge un problema con il resharding in questo schema. Resharding — intendo dire la modifica del numero di server.
Supponiamo che il nostro cluster di caching non riesca più a gestire il carico e abbiamo deciso di aggiungere un'altra macchina.
Aggiungiamo.

Adesso tutto si divide in modo intero non su tre, ma su quattro. In questo modo, praticamente tutte le chiavi che avevamo prima, praticamente tutti gli URL ora vivono su altri server. Entire cache è stata invalidata in un attimo. Tutte le richieste si sono concentrate sul nostro cluster di storage, è andato in crisi, si sono avute interruzioni del servizio e utenti insoddisfatti. Non vogliamo fare così.
Questa opzione non ci piace neanche.
Quindi, cosa dobbiamo fare? Dobbiamo in qualche modo utilizzare in modo efficace la cache, mantenendo costantemente una richiesta sullo stesso server, ma allo stesso tempo essere resilienti al resharing. E c'è una soluzione per questo, non è poi così complicata. Si chiama hashing consistente.

Come si presenta?

Prendiamo qualche funzione dalla chiave di sharding e distribuiamo tutti i suoi valori su un cerchio. Cioè, nel punto 0 abbiamo i suoi valori minimi e massimi che si incontrano. Inoltre, su questo stesso cerchio disponiamo tutti i nostri server in questo modo:

Ogni server è rappresentato da un punto e il settore che si estende fino a quel punto in senso orario è quindi servito da quel host. Quando riceviamo le 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 resharing?

Non invalidiamo più l'intera cache, come prima, e non spostiamo tutte le chiavi, ma spostiamo ogni settore di una piccola distanza in modo tale che, per così dire, nel posto liberato possa entrare il nostro sesto server che vogliamo aggiungere, e lo aggiungiamo lì.

Certo, in questa situazione anche le chiavi si spostano. Ma si spostano molto meno di prima. E vediamo che le nostre prime due chiavi sono rimaste sui loro server, mentre il server di caching è cambiato solo per l'ultima chiave. Funziona in modo abbastanza efficace, 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 bene.
Resta solo una domanda in caso di guasti. Supponiamo che un nostro server sia fuori servizio.

E non ci piacerebbe molto dover rigenerare questa mappa, invalidare parte della cache e così via, se, ad esempio, il server è stato riavviato, ma dobbiamo comunque gestire le richieste. Manteniamo semplicemente un backup della cache fotografica su ogni sito, che funge da sostituto per qualsiasi server che attualmente è fuori servizio. E se un server diventa improvvisamente non disponibile, il traffico viene reindirizzato lì. Naturalmente, in questo caso non abbiamo alcuna cache, cioè è fredda, ma, almeno, le richieste degli utenti vengono elaborate. Se questo intervallo è breve, lo gestiamo tranquillamente. C'è solo un maggior carico sullo storage. Se l'intervallo è lungo, possiamo già decidere se rimuovere questo server dalla mappa o meno, o magari sostituirlo con un altro.
Questo riguarda il sistema di caching. Vediamo i risultati.
Semberebbe che non ci sia nulla di complicato. Ma questo metodo di gestione della cache ci ha dato un tasso di successo del 98%. Cioè, di queste 80.000 richieste al secondo, solo 1.600 arrivano allo storage, e questo è un carico assolutamente normale, lo gestiscono tranquillamente, abbiamo sempre un margine.
Abbiamo posizionato questi server in tre dei nostri DC, e abbiamo ottenuto tre punti di presenza: Praga, Miami e Hong Kong.

Così, sono più o meno localizzati vicino a ciascuno dei nostri mercati target.
E come piacevole bonus abbiamo ottenuto questo proxy di caching, su cui la CPU in realtà è inattiva, perché per la distribuzione dei contenuti non è così necessaria. E lì, con NGINX + Lua, abbiamo implementato molte logiche utilitaristiche.

Ad esempio, possiamo sperimentare con webp o jpeg progressivo (questi sono formati moderni ed efficienti), vedere come influiscono sul traffico, prendere decisioni, attivarli per determinati paesi, ecc.; effettuare il ridimensionamento dinamico o il ritaglio delle foto al volo.
È un buon caso d'uso, quando ad esempio hai un'app mobile che mostra foto, e l'app mobile non vuole sprecare CPU del client per richiedere una foto grande e poi ridimensionarla a una certa dimensione per inserirla nella vista. Possiamo semplicemente specificare dinamicamente alcuni parametri nell'URL in UPort, e il fotocache ridimensionerà automaticamente la foto. Di solito, sceglierà la dimensione che abbiamo fisicamente sul disco, la più vicina a quella richiesta, e la ridimensionerà nelle coordinate specifiche.
A proposito, abbiamo pubblicato registrazioni video degli ultimi cinque anni della conferenza per sviluppatori di sistemi ad alta 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, sgranarle o pixelarle. Questo è quando vogliamo mostrare la foto di una persona, ma non vogliamo mostrare il suo viso; funziona bene ed è tutto implementato qui.
Cosa abbiamo ottenuto? Abbiamo ottenuto tre punti di presenza, un buon tasso di successo, e nel contempo la CPU su queste macchine non è inoperosa. Ora è diventata, ovviamente, più importante di prima. Dobbiamo mettere macchine un po' più potenti, ma ne vale la pena.
Questo riguarda la consegna delle fotografie. 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 avere una domanda: perché non prendere semplicemente e cambiare tutto in CDN? Sarebbe più o meno la stessa cosa, tutti i moderni CDN sanno farlo. E qui ci sono vari motivi.
Il primo sono le foto.

Questo è uno dei punti chiave della nostra infrastruttura e abbiamo bisogno di avere il massimo controllo su di esse. Se si tratta di una soluzione di un fornitore esterno e non si ha alcun potere su di essa, sarà abbastanza difficile gestire questa situazione quando si dispone di un grande dataset e di un flusso molto ampio di richieste degli utenti.
Faccio un esempio. Attualmente, sulla nostra infrastruttura, possiamo, ad esempio, nel caso ci siano problemi o rumori sotterranei, entrare nella macchina, fare il debugging lì, per così dire. Possiamo aggiungere la raccolta di metriche di cui abbiamo solo bisogno, possiamo sperimentare in vari modi, osservare come questo influisce sui grafici e così via. Attualmente si raccoglie un’enorme quantità di statistiche su questo cluster di caching. E di tanto in tanto, diamo un’occhiata e esploriamo a lungo alcune anomalie. Se fosse tutto dalla parte del CDN, sarebbe molto più difficile controllare. Oppure, ad esempio, se si verifica un incidente, sappiamo cosa è successo, sappiamo come viverci e come affrontarlo. Questa è la prima conclusione.
La seconda conclusione è piuttosto storica, perché il sistema si è sviluppato da tempo, e ci sono stati molteplici requisiti aziendali in diverse fasi, e non sempre si integrano nella concezione del CDN.
E il punto che deriva dal precedente –

È che sui fotocaching abbiamo molta logica specifica, che non sempre può essere aggiunta su richiesta. È poco probabile che un CDN aggiunga qualcosa di personalizzato su richiesta. Ad esempio, la crittografia degli URL, se non vuoi che il cliente possa modificarli. Vuoi cambiare l’URL sul server e crittografarlo, e poi restituire qui alcuni parametri dinamici.
Qual è la conclusione che si impone? Nel nostro caso, il CDN non è una buona alternativa.

Nel vostro caso, se avete requisiti aziendali specifici, potete tranquillamente implementare ciò che vi ho mostrato. E questo funzionerà benissimo con un profilo di carico simile.
Ma se avete una soluzione generale, e la questione non è molto particolare, potete tranquillamente usare un CDN. Oppure, se è molto più importante per voi avere tempo e risorse piuttosto che controllo.

E i moderni CDN hanno praticamente tutto ciò di cui vi ho parlato ora. Con eccezione di alcune funzionalità più o meno.
Questo riguardo alla consegna delle fotografie.
Ora spostiamoci un po' avanti nella nostra retrospettiva e parliamo di archiviazione.
Era il 2013.

I server di caching sono stati aggiunti, i problemi di performance sono scomparsi. Tutto va bene. Il dataset cresce. Nel 2013 avevamo circa 80 server connessi agli storage e circa 40 server di caching in ogni data center. Questo equivale a 560 terabyte di dati in ciascun data center, cioè circa un petabyte in totale.

E con la crescita del dataset, anche i costi operativi sono aumentati notevolmente. In cosa si è concretizzato questo?

In questo schema, che è disegnato — con SAN, le macchine connesse e i cache — ci sono molteplici punti di guasto. Se con il guasto dei server di caching eravamo già riusciti a gestire la situazione, lì tutto era relativamente prevedibile e chiaro, dall'altro lato, quello dello storage, la situazione era molto peggiore.
Innanzitutto, l'Storage Area Network (SAN) stesso, che può guastarsi.
In secondo luogo, è connesso in fibra ottica alle macchine finali. Possono esserci problemi con le schede ottiche e gli switch.

Naturalmente, non ce ne sono così tanti come con il SAN stesso, ma, comunque, queste sono anche punti di guasto.
Dopo, c'è la macchina stessa, che è collegata allo storage. Anche questa può guastarsi.

In totale abbiamo tre punti di guasto.
Inoltre, a parte i punti di guasto, c'è anche un pesante mantenimento stesso degli storage.
È un sistema complesso e multicomponente, e per gli ingegneri di sistema può essere faticoso.
E l'ultimo, il punto più importante. Se si verifica un guasto in uno dei questi tre punti, c'è una probabilità non trascurabile di perdere dati degli utenti, poiché il file system potrebbe danneggiarsi.

Supponiamo che il file system si sia danneggiato. Il suo recupero richiede, innanzitutto, molto tempo — può impiegare anche una settimana, a seconda del volume dei dati. E in secondo luogo, alla fine, è probabile che otteniamo un sacco di file incomprensibili, che dovranno in qualche modo essere abbinati alle fotografie degli utenti. E rischiamo di perdere dati. Il rischio è piuttosto elevato. E più spesso si verificano tali situazioni e più problemi sorgono in tutta questa catena, maggiore è il rischio.
Dovevamo fare qualcosa al riguardo. E abbiamo deciso che era necessario semplicemente fare un backup dei dati. In realtà, è una soluzione ovvia e buona. Cosa abbiamo fatto?

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

Abbiamo installato un secondo storage accanto (per fortuna, non è costato molto) e l'abbiamo chiamato sezione di backup. È collegato anch'esso in fibra ottica e si trova sulla stessa macchina. Tuttavia, dobbiamo in qualche modo sincronizzare i dati tra di loro.
Qui creiamo semplicemente una coda asincrona accanto.

Non è molto sovraccarica. Sappiamo che abbiamo pochi record. La coda è semplicemente una tabella in MySQL dove vengono scritte righe del tipo "devo fare il backup di questa foto". A ogni modifica o durante l'upload, copiamo dalla sezione principale a quella di backup tramite un lavoratore asincrono o semplicemente un background worker.
In questo modo abbiamo sempre due sezioni consistenti. Anche se una parte di questo sistema si guasta, possiamo sempre sostituire la sezione principale con quella di backup e tutto continuerà a funzionare.
Tuttavia, ciò aumenta notevolmente il carico di lettura, poiché oltre ai clienti che leggono dalla sezione principale, perché inizialmente guardano la foto lì (è più aggiornata), poi cercano nel backup se non la trovano (ma questo lo fa semplicemente NGINX), in più la nostra sistema di backup ora legge dalla sezione principale. Non si può dire che fosse un collo di bottiglia, ma non volevo aumentare il carico, essenzialmente, senza motivo.
E abbiamo aggiunto un terzo disco, che è un piccolo SSD, e l'abbiamo chiamato buffer.

Ecco come funziona adesso.
L'utente carica la foto sul buffer, quindi viene inviato un evento in coda che deve essere copiata su entrambe le sezioni. Viene copiata e la foto rimane per un certo periodo (diciamo un giorno) nel buffer, e solo dopo viene eliminata. Questo migliora notevolmente l'esperienza utente, perché di solito quando l'utente carica una foto, subito dopo iniziano a arrivare le richieste, oppure lui stesso aggiorna la pagina, la ricarica. Ma tutto dipende dall'applicazione che fa l'upload.
Oppure, ad esempio, altre persone a cui viene mostrata immediatamente la foto inviano subito richieste. Non è ancora nella cache, la prima richiesta avviene molto rapidamente. In pratica, è lo stesso che con la foto nella cache. Lo storage lento non partecipa affatto a questo. E quando dopo un giorno verrà eliminata, sarà già o nella nostra cache di livello, oppure probabilmente non serve più a nessuno. Quindi, l'esperienza utente qui è notevolmente migliorata grazie a queste semplici manipolazioni.
E, soprattutto, abbiamo smesso di perdere dati.

Diciamo che abbiamo smesso potenzialmente perdere dati, perché in effetti non li abbiamo persi. Ma c'era un pericolo. Vediamo che una soluzione del genere è, ovviamente, buona, ma somiglia un po' a un trattamento dei sintomi del problema, invece di risolverlo definitivamente. E alcuni problemi sono rimasti.
Innanzitutto, c'è un punto di fallimento rappresentato dal server fisico stesso su cui funziona tutto questo, che non è scomparso.

In secondo luogo, ci sono ancora problemi con gli SAN, rimane la loro pesante manutenzione, ecc. Non era un fattore critico, ma volevamo provare a vivere senza di essi.
E abbiamo realizzato la terza versione (di fatto la seconda) — la versione di backup. Come si presentava?
Questo è ciò che c'era –

I principali problemi che abbiamo sono legati al fatto che si tratta di un server fisico.
Innanzitutto, eliminiamo gli SAN, perché vogliamo sperimentare, vogliamo provare a utilizzare solo dischi rigidi locali.

Siamo già nel 2014-2015, e a quel tempo la situazione con i dischi e la loro capacità in un singolo server è migliorata notevolmente. Abbiamo deciso, perché non provare.
Poi prendiamo semplicemente la nostra partizione di backup e la spostiamo fisicamente su una macchina separata.

In questo modo, otteniamo questo schema. Abbiamo due macchine che memorizzano set di dati identici. Si riservano completamente a vicenda e sincronizzano i dati in rete tramite una coda asincrona nello stesso MySQL.

Perché funziona bene: perché abbiamo poche scritture. Cioè, se la scrittura fosse paragonabile alla lettura, avremmo probabilmente avuto qualche sovraccarico di rete e problemi. Ci sono poche scritture, molte letture — questo metodo funziona bene, cioè copiamo abbastanza raramente le fotografie tra questi due server.
In che modo funziona, se ci guardiamo più da vicino.

Upload. Il bilanciatore di carico seleziona semplicemente server casuali in coppia e carica su di essi. Naturalmente, esegue controlli sulla salute, assicurandosi che la macchina non sia caduta. Cioè, carica foto solo su un server vivo, e poi attraverso una coda asincrona tutto viene copiato sul vicino. Per quanto riguarda l'upload, è tutto estremamente semplice.
Con il compito è un po' più complicato.

Qui ci ha aiutato Lua, perché fare una logica del genere con NGINX vanilla può essere piuttosto complicato. Iniziamo facendo una richiesta al primo server, vediamo se lì c'è una foto, perché potenzialmente potrebbe essere caricata, ad esempio, sul vicino e non essere ancora arrivata qui. Se la foto è lì, è un buon segno. La diamo immediatamente al cliente e, possibilmente, la memorizziamo in cache.

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

Così possiamo dire ancora una volta: potrebbero esserci problemi con le prestazioni, perché i continui round trip — la foto è stata caricata, qui non c'è, facciamo due richieste anziché una, dovrebbe funzionare lentamente.
Nella nostra situazione non funziona lentamente.

Raccogliamo una quantità di metriche su questo sistema, e il tasso di hit di questo meccanismo è di circa il 95%. Cioè, il ritardo di questo backup è minimo, e grazie a ciò otteniamo praticamente garantito, dopo che la foto è stata caricata, di riceverla al primo colpo e non dobbiamo fare due viaggi.
Cos'altro abbiamo ottenuto e che è davvero fantastico?
In passato avevamo un'area di backup principale e leggevamo sequenzialmente da esse. Cioè, cercavamo sempre prima nel principale e poi nel backup. Era un solo passaggio.
Ora utilizziamo la lettura da due macchine contemporaneamente. Distribuiamo le richieste in Round Robin. In una piccola percentuale di casi facciamo due richieste. Ma in generale ora abbiamo il doppio della capacità di lettura rispetto a prima. E il carico è decisamente diminuito sia sulle macchine di erogazione sia sugli storage, che a quel tempo avevamo.
Per quanto riguarda la tolleranza ai guasti. In effetti, su questo ci siamo concentrati principalmente. La tolleranza ai guasti qui è risultata fantastica.

Una macchina 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.
Se anche con il guasto di questa macchina la coda smette di funzionare, nessun problema, basta che il log si accumuli prima sulla macchina funzionante, e poi raggiunga la coda, e poi sulla macchina che sarà riattivata dopo un po'.

La stessa cosa vale per la manutenzione. Spegniamo semplicemente una delle macchine, la estraiamo manualmente da tutti i pool, smette di ricevere traffico, facciamo una sorta di manutenzione, sistemiamo qualcosa, dopo di che la rimettiamo in funzione, e il backup si recupera abbastanza rapidamente. Cioè, in un giorno di inattività di una macchina, si recupera nell'ordine di pochi minuti. È davvero molto poco. Con l'affidabilità, ripeto, qui siamo a posto.
Quali conclusioni possiamo trarre da questo schema di riserva?
Abbiamo ottenuto l'affidabilità.
Semplice utilizzo. Poiché le macchine hanno dischi rigidi locali, è molto più comodo dal punto di vista operativo per gli ingegneri che ci lavorano.
Abbiamo ottenuto una doppia capacità di lettura.
È un ottimo bonus in aggiunta all'affidabilità.
Ma ci sono anche dei problemi. Ora abbiamo uno sviluppo di alcune funzionalità molto più complesso legato a questo, perché il sistema è diventato 100% eventual consistency.

Dobbiamo, diciamo, in qualche job di background pensare costantemente: «Su quale server siamo attualmente attivi?», «C'è davvero qui l'immagine attuale?» ecc. Naturalmente, tutto questo è ben incapsulato, e per il programmatore che scrive la logica di business è trasparente. Tuttavia, è emerso uno strato molto complesso. Ma siamo disposti a tollerarlo in cambio dei vantaggi che abbiamo ottenuto.
E qui sorge nuovamente un certo conflitto.
All'inizio dicevo che conservare tutto su dischi rigidi locali è sbagliato. E ora dico che ci è piaciuto.
Sì, in effetti, con il passare del tempo la situazione è cambiata notevolmente, e ora questo approccio ha molti vantaggi. Prima di tutto, otteniamo un utilizzo decisamente più semplice.
In secondo luogo, è più efficiente, perché non abbiamo quegli automatic controllers, le connessioni ai rack di archiviazione.
Lì c'è una grande macchina, mentre qui ci sono semplicemente alcuni dischi che sono stati messi in RAID proprio qui sulla macchina.
Ma ci sono anche svantaggi.

È circa 1,5 volte più costoso rispetto all'utilizzo di SAN, anche ai prezzi attuali. Pertanto, abbiamo deciso di non convertire così audacemente tutto il nostro grande cluster in macchine con dischi rigidi locali e abbiamo optato per una soluzione ibrida.
La metà delle nostre macchine lavora con dischi rigidi (beh, non metà, ma circa il 30%, credo). E la parte rimanente è composta da vecchie macchine che avevano precedentemente la prima configurazione di backup. Le abbiamo semplicemente rimontate, poiché non abbiamo bisogno di dati nuovi o altro, abbiamo solo spostato i mount da un host fisico a due.
E abbiamo ottenuto un grande margine di lettura, e ci siamo espansi. Se prima montavamo un solo storage su una macchina, ora ne montiamo quattro su una coppia, ad esempio. E questo funziona bene.
Diamo una breve sintesi di ciò che abbiamo realizzato, per cosa abbiamo lottato e se abbiamo raggiunto i nostri obiettivi.
Conclusioni
Abbiamo utenti — ben 33 milioni.
Abbiamo tre punti di presenza — Praga, Miami, Hong Kong.
In questi punti è presente uno strato di caching, che consiste in macchine con dischi locali veloci (SSD), su cui gira un semplice sistema basato su NGINX, il suo access.log e demoni in Python che gestiscono e amministrano il 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 il controllo e la velocità di sviluppo e uso delle risorse pende dall'altra parte, allora potete tranquillamente sostituirlo con un CDN, i moderni CDN lo fanno molto bene.
Successivamente c'è lo strato di storage, su cui abbiamo cluster di coppie di macchine che si riservano a vicenda, con file che vengono copiati in modo asincrono da una all'altra in caso di qualsiasi modifica.
Parte di queste macchine lavora con dischi rigidi locali.
Parte di queste macchine sono collegate a SAN.

Da un lato, è più comodo da gestire e un po' più performante, dall'altro lato, è vantaggioso in termini di densità e prezzo per gigabyte.
Questa è una breve panoramica dell'architettura che abbiamo sviluppato e come si è evoluta.
Alcuni ulteriori consigli da parte del capo, molto semplici.
In primo luogo, se all'improvviso decidete che avete urgenza di migliorare la vostra infrastruttura di immagini, prima misurate, perché, probabilmente, non c'è bisogno di miglioramenti.

Faccio un esempio. Abbiamo un cluster di macchine che restituiscono fotografie dagli allegati nelle chat, e lì funziona ancora lo schema del 2009, e nessuno ne risente. Tutti stanno bene, a tutti piace.
Per misurare, prima appendete un sacco di metriche, date un'occhiata e poi decidete cosa non vi soddisfa e cosa bisogna migliorare. Per fare queste misurazioni, abbiamo uno strumento fantastico chiamato Pinba.
Esso consente di raccogliere statistiche molto dettagliate da NGINX per ogni richiesta e codici di risposta, e distribuzione dei tempi — tutto ciò che desiderate. Ha binding per vari sistemi di analisi, e potete quindi visualizzare tutto in modo chiaro.
Prima misurate — poi migliorate.
Avanti. Ottimizziamo la lettura con la cache, la scrittura con lo sharding, ma questo è un punto ovvio.

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

Supponiamo di aver caricato cento foto, poi avendole ruotate, assicuratevi che si tratti fisicamente di un file diverso. Cioè, non pensate: ora risparmio un po' di spazio, scrivo nello stesso file, cambio versione. Funziona sempre male, e poi ci si ritrova con molti mal di testa.
Punto successivo. Riguardo al resize al volo.
In passato, quando gli utenti caricavano una fotografia, creavamo subito una serie di dimensioni per ogni evenienza, per diversi clienti, e tutto ciò era salvato su disco. Ora abbiamo abbandonato questo approccio.
Abbiamo mantenuto solo tre dimensioni principali: piccola, media e grande. Tutto il resto lo riduciamo dalla dimensione che ci è richiesta in Uport, facciamo semplicemente un downscale e restituiamo all'utente.
Il costo della cache della CPU qui risulta molto più economico rispetto a se generassimo continuamente queste dimensioni su ogni storage. Supponiamo di voler aggiungere una nuova dimensione, ci vorrà un mese — eseguire uno script ovunque che faccia tutto questo senza bloccare il cluster. Cioè, se c'è la possibilità di scegliere ora, è meglio creare il minor numero possibile di dimensioni fisiche, ma affinché ci sia almeno una certa distribuzione, diciamo, tre. E tutto il resto semplicemente ridimensionato al volo tramite moduli già pronti. Adesso è tutto molto facile e accessibile.
E il backup incrementale asincrono è una buona cosa.
Come ha mostrato la nostra esperienza, uno schema del genere funziona molto bene con la copia differita dei file modificati.

L'ultimo punto è anche ovvio. Se nella tua infrastruttura non ci sono attualmente problemi simili, ma c'è qualcosa che potrebbe rompersi, si romperà sicuramente quando ce ne sarà un po' di più. Quindi è meglio pensarci in anticipo e non affrontare problemi. Per me è tutto.
Contatti
»
»
Questa relazione è la trascrizione di una delle migliori presentazioni alla conferenza degli sviluppatori di sistemi ad alta carga . Manca meno di un mese alla conferenza HighLoad++ 2017.
Abbiamo già pronto , ora ci stiamo attivamente organizzando.
Quest'anno continuiamo a esplorare il tema delle architetture e della scalabilità:
- / Игорь Васильев
- / Дмитрий Егоров
- / Анатолий Пласковский
- / Роман Шеховцов, Алексей Громатчиков
- / Филипп Дельгядо
Alcuni di questi materiali sono utilizzati anche nel nostro corso online di formazione sullo sviluppo di sistemi ad alta carga è una serie di email, articoli, materiali, video appositamente selezionati. Già ora nel nostro manuale ci sono oltre 30 materiali unici. Unisciti a noi!
Fonte: habr.com
