One-cloud — sistema operativo di livello data center in Odnoklassniki

One-cloud — sistema operativo di livello data center in Odnoklassniki

Aloha, gente! Mi chiamo Oleg Anastas'ev, lavoro in Odnoklassniki nel team della Piattaforma. Oltre a me, in Odnoklassniki lavora un sacco di hardware. Abbiamo quattro data center, con circa 500 rack e oltre 8.000 server. A un certo punto abbiamo capito che l'implementazione di un nuovo sistema di gestione ci avrebbe permesso di utilizzare le attrezzature in modo più efficace, semplificare la gestione degli accessi, automatizzare la (ri)distribuzione delle risorse di calcolo, accelerare il lancio di nuovi servizi e migliorare la reazione a gravi incidenti.

Cosa ne è venuto fuori?

Oltre a me e a un sacco di hardware ci sono anche persone che lavorano con questo hardware: ingegneri che si trovano direttamente nei data center; specialisti di rete che configurano l'infrastruttura di rete; amministratori, o SRE, che garantiscono la resilienza dell'infrastruttura; e team di sviluppatori, ciascuno dei quali è responsabile di una parte delle funzioni del portale. Il software creato da loro funziona più o meno così:

One-cloud — sistema operativo di livello data center in Odnoklassniki

Le richieste degli utenti arrivano sia ai front-end del portale principale www.ok.ru, sia ad altri, per esempio ai front-end dell'API musicale. Per elaborare la logica di business, chiamano un server applicazioni che, durante l'elaborazione della richiesta, chiama i necessari microservizi specializzati: one-graph (grafico delle relazioni sociali), user-cache (cache dei profili utente) e così via.

Ognuno di questi servizi è distribuito su più macchine, e ciascuno di essi ha sviluppatori responsabili del funzionamento dei moduli, della loro manutenzione e dello sviluppo tecnologico. Tutti questi servizi vengono avviati su server fisici e fino a poco tempo fa avviavamo esattamente un'attività su un server, cioè era specializzato per un compito specifico.

Perché è così? Questo approccio aveva diversi vantaggi:

  • Si semplifica la gestione di massa. Supponiamo che un compito richieda alcune librerie, alcune configurazioni. Allora il server viene assegnato a un gruppo specifico, si descrive una policy cfengine per questo gruppo (o è già stata descritta) e questa configurazione viene distribuita in modo centralizzato e automatico su tutti i server di quel gruppo.
  • Si semplifica la diagnostica. Supponiamo che stiate monitorando un carico elevato della CPU centrale e comprendiate che questo carico può essere generato solo dal compito che sta girando su questo processore hardware. Le ricerche del colpevole si concludono molto rapidamente.
  • Si semplifica monitoraggio. Se c'è qualcosa che non va con il server, il monitor lo segnala e sapete esattamente chi è il colpevole.

A un servizio composto da più repliche vengono assegnati diversi server — uno per ciascuna. Allora, la risorsa di calcolo per il servizio viene assegnata molto semplicemente: quante più server ha il servizio, tanto più può consumare risorse al massimo. "Semplice" qui non significa che sia facile da usare, ma che la distribuzione delle risorse avviene manualmente.

Questo approccio ci ha anche permesso di realizzare configurazioni hardware specializzate per i compiti eseguiti su questo server. Se un compito memorizza grandi volumi di dati, utilizziamo un server 4U con chassis per 38 dischi. Se il compito è puramente computazionale, possiamo acquistare un server 1U più economico. Questo è efficace dal punto di vista delle risorse di calcolo. Inoltre, questo approccio ci consente di utilizzare fino a quattro volte meno macchine con un carico paragonabile a quello di un nostro amico social network.

Tale efficienza nell'uso delle risorse di calcolo dovrebbe garantire anche l'efficienza economica, partendo dall'assunto che il costo maggiore riguarda i server. A lungo, i componenti hardware sono stati i più costosi, e abbiamo investito molte energie nel ridurre il costo dell'hardware, ideando algoritmi per garantire la tolleranza ai guasti per ridurre le esigenze di affidabilità dell'attrezzatura. E oggi siamo giunti a un punto in cui il prezzo dei server ha smesso di essere determinante. A meno che non si consideri l'ultimissima esotica, la configurazione specifica dei server nel rack non ha importanza. Ora abbiamo un altro problema — il costo dello spazio occupato da un server nel data center, cioè lo spazio nel rack.

Realizzando che è così, abbiamo deciso di calcolare quanto efficientemente utilizziamo i rack.
Abbiamo preso il prezzo del server più potente tra quelli economicamente giustificabili, calcolato quanti di questi server possiamo inserire negli rack, quante attività potremmo avviare su di essi in base al vecchio modello "un server = un'attività" e quanto tali attività potrebbero utilizzare l'hardware. I calcoli ci hanno fatto commuovere. È emerso che l'efficienza di utilizzo dei rack è circa dell'11%. La conclusione è ovvia: è necessario aumentare l'efficienza dei data center. Sembrerebbe che la soluzione sia chiara: bisogna eseguire più attività su un singolo server. Ma qui iniziano le complicazioni.

La configurazione massiva diventa rapidamente complessa: ora non è possibile assegnare a un server un solo gruppo. Infatti, ora su un server possono essere eseguite più attività di diversi team. Inoltre, la configurazione potrebbe essere in conflitto per diverse applicazioni. Anche la diagnostica diventa più complessa: se si osserva un aumento del consumo di CPU o dischi su un server, non si sa quale delle attività sta causando problemi.

Ma la cosa principale è che non c'è isolamento tra le attività eseguite sulla stessa macchina. Ad esempio, il grafico del tempo medio di risposta di un'attività del server prima e dopo che sullo stesso server è stata avviata un'altra applicazione di calcolo, del tutto non correlata alla prima, mostra che il tempo di risposta dell'attività principale è aumentato considerevolmente.

One-cloud — sistema operativo di livello data center in Odnoklassniki

È evidente che è necessario eseguire le attività o in contenitori o in macchine virtuali. Poiché praticamente tutte le attività vengono eseguite sotto il controllo di un unico sistema operativo (Linux) o sono adattate a esso, non abbiamo bisogno di sostenere molteplici sistemi operativi diversi. Pertanto, la virtualizzazione non è necessaria, e a causa dei costi indiretti sarà meno efficace della containerizzazione.

Come implementazione dei container per eseguire compiti direttamente sui server Docker, un buon candidato è: le immagini dei file system risolvono bene i problemi delle configurazioni in conflitto. Il fatto che le immagini possano essere composte da più strati ci consente di ridurre notevolmente il volume dei dati necessari per il loro dispiegamento sull'infrastruttura, separando le parti comuni in strati di base distinti. In questo modo, gli strati di base (e i più voluminosi) verranno memorizzati nella cache rapidamente su tutta l'infrastruttura, e per la distribuzione di molti tipi diversi di applicazioni e versioni sarà necessario trasferire solo piccoli strati di volume.

Inoltre, il registro e il tagging delle immagini in Docker ci forniscono primitive pronte per la versioning e la distribuzione del codice in produzione.

Docker, come qualsiasi altra tecnologia simile, ci fornisce un certo livello di isolamento dei container 'out of the box'. Ad esempio, l'isolamento della memoria: ogni container ha un limite sull'uso della memoria della macchina, oltre il quale non consumerà. È possibile isolare i container anche per l'uso della CPU. Per noi, però, l'isolamento standard non era sufficiente. Ma di questo parleremo più avanti.

L'esecuzione diretta dei container sui server è solo una parte dei problemi. Un'altra parte riguarda il posizionamento dei container sui server. È necessario capire quale container può essere posizionato su quale server. Non è un compito semplice, poiché i container devono essere distribuiti sui server il più densamente possibile, senza compromettere la loro velocità operativa. Tale collocazione può essere complessa anche dal punto di vista della tolleranza ai guasti. Spesso vogliamo posizionare repliche dello stesso servizio in rack diversi o addirittura in sale diverse del data center, affinché in caso di guasto di un rack o di una sala non perdiamo immediatamente tutte le repliche del servizio.

Distribuire manualmente i container non è un'opzione, quando hai 8.000 server e 8-16.000 container.

Inoltre, volevamo dare agli sviluppatori maggiore autonomia nella distribuzione delle risorse, affinché potessero posizionare i propri servizi in produzione senza l'aiuto di un amministratore. Allo stesso tempo, desideravamo mantenere il controllo, in modo che un servizio secondario non consumasse tutte le risorse dei nostri data center.

È evidente che è necessario uno strato di gestione che si occupi di questo automaticamente.

Ecco, siamo arrivati a un'immagine semplice e chiara, amata da tutti gli architetti: tre quadrati.

One-cloud — sistema operativo di livello data center in Odnoklassniki

one-cloud masters — è un cluster a prova di errore responsabile dell'orchestrazione del cloud. Lo sviluppatore invia al master un manifesto che contiene tutte le informazioni necessarie per il deployment del servizio. Il master, sulla base di questo, invia comandi ai minion selezionati (macchine destinate all'esecuzione dei container). Nei minion c'è il nostro agente, che riceve il comando, emette le proprie istruzioni a Docker e Docker configura il kernel Linux per avviare il container corrispondente. Oltre a eseguire i comandi, l'agente comunica continuamente al master le variazioni dello stato sia della macchina-minion che dei container avviati su di essa.

Distribuzione delle risorse

Ora affrontiamo il compito più complesso della distribuzione delle risorse per più minion.

La risorsa di calcolo in one-cloud è:

  • La potenza di elaborazione della CPU consumata da un compito specifico.
  • La quantità di memoria disponibile per il compito.
  • Traffico di rete. Ogni minion ha un'interfaccia di rete specifica con una larghezza di banda limitata, quindi non si possono assegnare compiti senza considerare il volume di dati trasmessi attraverso la rete.
  • Dischi. Oltre, chiaramente, allo spazio per i dati del compito, diamo anche un tipo di disco: HDD o SSD. I dischi possono gestire un numero finito di richieste al secondo — IOPS. Pertanto, per i compiti che generano più IOPS di quelli che un singolo disco può gestire, riserviamo anche degli "spindles" — cioè dispositivi di memorizzazione che devono essere strettamente riservati per il compito.

Quindi, per qualche servizio, ad esempio user-cache, possiamo registrare le risorse consumate in questo modo: 400 core della CPU, 2,5 TB di memoria, 50 Gb/s di traffico in entrambe le direzioni, 6 TB di spazio su HDD, allocato su 100 spindles. Oppure in una forma più familiari per noi così:

alloc:
    cpu: 400
    mem: 2500
    lan_in: 50g
    lan_out: 50g
    hdd:100x6T

Le risorse del servizio user-cache consumano solo una parte delle risorse disponibili nell'infrastruttura di produzione. Pertanto, vogliamo assicurarci che, all'improvviso, a causa di un errore dell'operatore o meno, user-cache non consumi più risorse di quelle che gli sono state allocate. In altre parole, dobbiamo limitare le risorse. Ma su cosa potremmo legare la quota?

Torniamo al nostro schema di interazione tra componenti molto semplificato e ridisegniamolo con più dettagli — così:

One-cloud — sistema operativo di livello data center in Odnoklassniki

Ciò che salta all'occhio:

  • Il web front-end e la musica utilizzano cluster isolati dello stesso server applicativo.
  • Si possono individuare i livelli logici a cui appartengono questi cluster: fronti, cache, livello di archiviazione e gestione dei dati.
  • Il front-end non è omogeneo, è composto da diversi sottosistemi funzionali.
  • Le cache possono anche essere distribuite tra i sottosistemi di cui memorizzano i dati.

Disegniamo di nuovo l'immagine:

One-cloud — sistema operativo di livello data center in Odnoklassniki

Ah! Vediamo un'gerarchia! Ciò significa che possiamo distribuire le risorse in blocchi più grandi: nominare uno sviluppatore responsabile per un nodo di questa gerarchia, corrispondente al sottosistema funzionale (come 'music' nell'immagine), e legare una quota a questo stesso livello di gerarchia. Questa gerarchia ci permette anche di organizzare i servizi in modo più flessibile per facilitare la gestione. Ad esempio, tutto il web, essendo un raggruppamento molto grande di server, lo suddividiamo in diversi gruppi più piccoli, mostrati nell'immagine come group1, group2.

Rimuovendo le linee superflue, possiamo annotare ogni nodo della nostra immagine in una forma più piatta: group1.web.front, api.music.front, user-cache.cache.

Così arriviamo al concetto di ‘coda gerarchica’. Essa ha un nome, come ‘group1.web.front’. Viene assegnata una quota per le risorse e diritti degli utenti. A una persona del DevOps daremo il permesso di inviare il servizio in coda, e tale dipendente potrà avviare qualcosa in coda, mentre a una persona dell'OpsDev — diritti di amministrazione, e ora potrà gestire la coda, nominando persone, concedendo a queste persone diritti, eccetera. I servizi avviati in questa coda verranno eseguiti entro i limiti della quota della coda. Se la quota computazionale della coda non è sufficiente per l'esecuzione simultanea di tutti i servizi, saranno eseguiti in sequenza, formando così la vera e propria coda.

Esaminiamo i servizi più nel dettaglio. Un servizio ha un nome completo, che include sempre il nome della coda. Quindi il servizio web front avrà il nome ok-web.group1.web.front. E il servizio del server applicativo a cui si rivolge diventerà ok-app.group1.web.front. Ogni servizio ha un manifesto, in cui sono indicate tutte le informazioni necessarie per il posizionamento su macchine specifiche: quante risorse consuma questo compito, quale configurazione è necessaria, quante repliche devono esserci, proprietà per la gestione dei guasti di questo servizio. E dopo il posizionamento del servizio direttamente sulle macchine, appaiono le sue istanze. Anche queste sono nominate in modo univoco — con il numero dell'istanza e il nome del servizio: 1.ok-web.group1.web.front, 2.ok-web.group1.web.front, …

È molto comodo: guardando solo il nome del contenitore in esecuzione, possiamo subito capire molto.

E ora conosciamo meglio cosa fanno effettivamente queste istanze: i compiti.

Classi di isolamento dei compiti

Tutti i compiti in OK (e probabilmente ovunque) possono essere divisi in gruppi:

  • Compiti con ritardo breve — prod. Per tali compiti e servizi, il ritardo di risposta (latency) è molto importante, quanto velocemente ogni richiesta sarà elaborata dal sistema. Esempi di compiti: front-end web, cache, server di applicazioni, archivi OLTP, ecc.
  • Compiti di calcolo — batch. Qui la velocità di elaborazione di ciascuna richiesta specifica non è importante. Ciò che conta è quanto calcolo totale questa attività farà in un determinato (grande) intervallo di tempo (throughput). Saranno tali qualsiasi compito MapReduce, Hadoop, machine learning, statistica.
  • Compiti in background — idle. Per tali compiti neither latency nor throughput sono molto importanti. Qui rientrano vari test, migrazioni, ricalcoli, conversioni di dati da un formato all'altro. Da un lato, assomigliano a quelli di calcolo, dall'altro — non ci interessa molto quanto velocemente si completano.

Vediamo come tali compiti consumano risorse, ad esempio, della CPU.

Compiti con ritardo breve. Per tale compito, il pattern di consumo della CPU assomiglierà a questo:

One-cloud — sistema operativo di livello data center in Odnoklassniki

Viene ricevendo una richiesta dall'utente, il compito inizia a utilizzare tutti i core CPU disponibili, elabora, restituisce una risposta, aspetta la successiva richiesta e si ferma. Arriva una nuova richiesta — di nuovo prende tutto ciò che aveva, elabora, aspettiamo il successivo.

Per garantire un ritardo minimo per tale compito, dobbiamo prendere il massimo delle risorse consumate e riservare il numero necessario di core sulla minion (la macchina che eseguirà il compito). Allora la formula di riservazione per il nostro compito sarà la seguente:

alloc: cpu = 4 (max)

E se abbiamo una macchina-minion con 16 core, possiamo eseguire esattamente quattro di queste attività. È importante notare che il consumo medio della CPU per queste attività è spesso molto basso — cosa evidente, poiché gran parte del tempo l'attività è in attesa di una richiesta e non fa nulla.

Attività di calcolo. Il loro modello sarà leggermente diverso:

One-cloud — sistema operativo di livello data center in Odnoklassniki

Il consumo medio delle risorse della CPU per queste attività è piuttosto elevato. Spesso desideriamo che l'attività di calcolo venga completata in un certo tempo, quindi è necessario riservare un numero minimo di processori necessario affinché l'intero calcolo si concluda in un tempo accettabile. La sua formula di riservazione apparirà così:

alloc: cpu = [1,*)

"Per favore, posiziona su un minion dove ci sia almeno un core libero, e poi tutto il resto — prenderà tutto."

Qui l'efficienza dell'uso è già molto migliore rispetto alle attività con bassa latenza. Ma il guadagno sarà molto maggiore se combiniamo entrambi i tipi di attività su una sola macchina-minion e distribuiamo le sue risorse on-the-fly. Quando un'attività con bassa latenza richiede la CPU — la ottiene immediatamente, e quando le risorse non sono più necessarie — vengono trasferite all'attività di calcolo, cioè più o meno così:

One-cloud — sistema operativo di livello data center in Odnoklassniki

Ma come si fa?

Iniziamo a esaminare prod e il suo alloc: cpu = 4. Dobbiamo riservare quattro core. In Docker run questo può essere fatto in due modi:

  • Utilizzando l'opzione --cpuset=1-4, cioè riservare all'attività quattro core specifici sulla macchina.
  • di utilizzare --cpuquota=400_000 --cpuperiod=100_000, assegnare una quota di tempo di CPU, cioè specificare che ogni 100 ms di tempo reale l'attività consuma non più di 400 ms di tempo di CPU. Si ottengono comunque gli stessi quattro core.

Ma quale di questi metodi è più adatto?

Il cpuset sembra piuttosto attraente. Il task ha quattro core dedicati, il che significa che le cache del processore funzioneranno al massimo dell'efficienza. Tuttavia, c'è anche un rovescio della medaglia: dovremmo assumerci il compito di distribuire i calcoli sui core non occupati della macchina anziché sull'OS, e questo è un compito piuttosto non banale, specialmente se proviamo a distribuire compiti batch su tale macchina. I test hanno mostrato che qui è meglio optare per la quota: in questo modo il sistema operativo ha più libertà nella scelta del core per eseguire il task in quel momento e il tempo di CPU viene distribuito in modo più efficace.

Esaminiamo come in docker effettuare la riservazione per il numero minimo di core. La quota per i compiti batch non è più applicabile, poiché non è necessario limitare il massimo, è sufficiente garantire solo il minimo. Qui ci si adatta bene l'opzione docker run --cpushares.

Abbiamo concordato che se un batch richiede la garanzia di un minimo di un core, specifichiamo --cpushares=1024, mentre se il minimo sono due core, allora specifichiamo --cpushares=2048. Le condivisioni di CPU non interferiscono nella distribuzione del tempo di CPU finché ce n'è a sufficienza. Così, se il prod non utilizza attualmente tutti e quattro i suoi core, nulla limita i compiti batch, e possono utilizzare ulteriore tempo di CPU. Tuttavia, in caso di carenza di CPU, se il prod ha consumato tutti e quattro i suoi core e ha raggiunto la quota, il tempo di CPU rimanente sarà suddiviso proporzionalmente alle cpushares, cioè in una situazione di tre core liberi, un core andrà al task con 1024 cpushares, mentre gli altri due andranno al task con 2048 cpushares.

Tuttavia, l'uso di quota e shares non è sufficiente. Dobbiamo assicurarci che il task con bassa latenza abbia priorità rispetto al task batch nella distribuzione del tempo di CPU. Senza tale prioritizzazione, il task batch prenderà tutto il tempo di CPU quando necessario per il prod. In Docker run non ci sono opzioni per la prioritizzazione dei container, ma ci vengono in aiuto le politiche dello scheduler della CPU in Linux. Puoi leggere in dettaglio qui, mentre in questo articolo daremo solo una panoramica:

  • SCHED_OTHER
    Per impostazione predefinita, vengono concessi a tutti i normali processi utente su una macchina Linux.
  • SCHED_BATCH
    Destinata a processi ad alta intensità di risorse. Quando un compito viene assegnato al processore, viene introdotta una sorta di penalità per l'attivazione: tale compito avrà minori probabilità di ricevere risorse del processore se in quel momento è in uso un compito con SCHED_OTHER
  • SCHED_IDLE
    Processo in background con priorità molto bassa, anche inferiore a nice -19. Usiamo la nostra libreria open source one-nio, per impostare la politica necessaria al momento dell'avvio del contenitore chiamando

one.nio.os.Proc.sched_setscheduler( pid, Proc.SCHED_IDLE )

Ma anche se non programmi in Java, puoi fare lo stesso usando il comando chrt:

chrt -i 0 $pid

Riassumiamo tutti i nostri livelli di isolamento in una tabella per chiarezza:

Classe di isolamento
Esempio alloc
Opzioni Docker run
sched_setscheduler chrt*

Prod
cpu = 4
--cpuquota=400000 --cpuperiod=100000
SCHED_OTHER

Batch
Cpu = [1, * )
--cpushares=1024
SCHED_BATCH

Idle
Cpu= [2, *)
--cpushares=2048
SCHED_IDLE

*Se esegui chrt dall'interno del contenitore, potrebbe essere necessaria la capability sys_nice, poiché per impostazione predefinita Docker revoca questa capability all'avvio del contenitore.

Ma i compiti consumano non solo il processore, ma anche il traffico, che influisce sulla latenza del compito di rete ancora di più di un errato bilanciamento delle risorse del processore. Pertanto, naturalmente, vogliamo ottenere un'immagine altrettanto chiara del traffico. Cioè, quando il compito prod invia dei pacchetti in rete, limitiamo la velocità massima (formula alloc: lan=[*,500mbps) ), con cui prod può farlo. E per batch garantiamo solo la banda minima, senza limitare quella massima (formula alloc: lan=[10Mbps,*) ) In questo modo il traffico prod deve avere priorità rispetto ai compiti batch.
Qui Docker non ha primitivi che possiamo utilizzare. Ma abbiamo l'aiuto di Linux Traffic Control. Siamo riusciti a raggiungere il risultato desiderato tramite la disciplina Hierarchical Fair Service Curve. Con questa disciplina isoliamo due classi di traffico: alta priorità prod e bassa priorità batch/idle. Alla fine, la configurazione per il traffico in uscita risulta così:

One-cloud — sistema operativo di livello data center in Odnoklassniki

qui 1:0 — «qdisc radice» della disciplina hsfc; 1:1 — classe figlia hsfc con un limite totale di larghezza di banda di 8 Gbit/s, sotto la quale si trovano le classi figlie di tutti i contenitori; 1:2 — classe figlia hsfc comune per tutti i batch e i task idle con un limite «dinamico», di cui parleremo più avanti. Le altre classi figlie hsfc sono classi dedicate per i contenitori prod attivi al momento, con limiti corrispondenti ai loro manifesti, — 450 e 400 Mbit/s. Ogni classe hsfc è assegnata a una coda qdisc fq o fq_codel, a seconda della versione del kernel linux, per evitare la perdita di pacchetti durante i picchi di traffico.

Di solito, le discipline tc servono solo per la priorizzazione del traffico in uscita. Ma vogliamo dare priorità anche al traffico in ingresso — poiché qualche compito batch può facilmente occupare tutta la banda in ingresso, ricevendo, ad esempio, un grande pacchetto di dati in ingresso per map&reduce. Per questo utilizziamo il modulo ifb, che crea un'interfaccia virtuale ifbX per ogni interfaccia di rete e reindirizza il traffico in ingresso dall'interfaccia a quella in uscita su ifbX. Successivamente, per ifbX funzionano tutte le stesse discipline per il controllo del traffico in uscita, per il quale la configurazione hsfc sarà molto simile:

One-cloud — sistema operativo di livello data center in Odnoklassniki

Durante gli esperimenti abbiamo scoperto che i migliori risultati di hsfc si ottengono quando la classe 1:2 del traffico batch/idle non prioritario è limitata sulle macchine minion a non più di una certa banda libera. Altrimenti, il traffico non prioritario influisce troppo sulla latenza dei task prod. L'attuale grandezza della banda libera è determinata da miniond ogni secondo, misurando il consumo medio di traffico da parte di tutti i task prod di quel minion One-cloud — sistema operativo di livello data center in Odnoklassniki e sottraendola dalla capacità dell'interfaccia di rete One-cloud — sistema operativo di livello data center in Odnoklassniki con un piccolo margine, cioè.

One-cloud — sistema operativo di livello data center in Odnoklassniki

Le bande sono definite per il traffico in ingresso e in uscita in modo indipendente. E in base ai nuovi valori, miniond riconfigura il limite per la classe non prioritaria 1:2.

In questo modo abbiamo implementato tutte e tre le classi di isolamento: prod, batch e idle. Queste classi influenzano notevolmente le prestazioni dei task. Pertanto, abbiamo deciso di collocare questo attributo in cima all'gerarchia, in modo che guardando il nome della coda gerarchica sia immediatamente chiaro di cosa si tratta:

One-cloud — sistema operativo di livello data center in Odnoklassniki

Tutti i nostri familiari web e music fronti vengono quindi collocati in gerarchia sotto prod. Per esempio, sotto batch poniamo il servizio music catalog, che periodicamente compila un catalogo di brani da un insieme di file mp3 caricati su «Одноклассники». Un esempio di servizio in idle potrebbe essere music transformer, che normalizza il livello di volume della musica.

Rimuovendo di nuovo le righe superflue, possiamo scrivere i nomi dei nostri servizi in modo più piatto, aggiungendo la classe di isolamento del compito alla fine del nome completo del servizio: web.front.prod, catalog.music.batch, transformer.music.idle.

E ora, guardando il nome del servizio, comprendiamo non solo quale funzione svolge, ma anche la sua classe di isolamento, e quindi la sua criticità e così via.

Tutto è fantastico, ma c'è una dura verità. Isolare completamente i compiti che operano su una sola macchina è impossibile.

Cosa siamo riusciti a ottenere: se il batch consuma intensamente solo risorse della CPU, il pianificatore integrato della CPU Linux svolge molto bene il suo compito, e non c'è praticamente alcun impatto sulla task di prod. Ma se questo task batch inizia a lavorare attivamente con la memoria, allora l'influenza reciproca si manifesta già. Questo accade perché i cache della memoria della task di prod vengono 'svuotati' — di conseguenza, gli errori nel cache aumentano, e la CPU elabora la task di prod più lentamente. Un task batch del genere può aumentare i ritardi del nostro tipico container di prod del 10%.

Isolare il traffico è ancora più difficile perché le moderne schede di rete hanno una coda interna di pacchetti. Se un pacchetto da un task batch arriva prima in coda, significa che sarà trasmesso per primo via cavo, e non c'è nulla da fare.

Inoltre, finora siamo riusciti a risolvere solo la questione della priorizzazione del traffico TCP: per UDP l'approccio con hsfc non funziona. E anche nel caso del traffico TCP, se un task batch genera molto traffico, ciò comporta anche un aumento di circa il 10% dei ritardi nella task di prod.

Resilienza

Uno degli obiettivi nella progettazione di one-cloud era migliorare la resilienza di Одноклассники. Pertanto, vorrei esaminare più dettagliatamente i possibili scenari di guasto e malfunzionamento. Iniziamo con uno scenario semplice: il guasto di un container.

Il contenitore può fallire in diversi modi. Potrebbe essere un esperimento, un bug o un errore nel manifesto, che porta al fatto che il compito in produzione inizia a consumare più risorse di quanto indicato nel manifesto. Abbiamo avuto un caso: un sviluppatore ha implementato un algoritmo complesso, lo ha modificato molte volte, si è confuso e ha creato un ciclo non banale. Poiché il compito in produzione è più prioritario di tutti gli altri sugli stessi nodi, ha iniziato a consumare tutte le risorse disponibili della CPU. In questa situazione, l'isolamento ha fatto la differenza, più precisamente, il limite sul tempo della CPU. Se al compito viene assegnato un limite, non consumerà di più. Pertanto, i compiti batch e altri compiti in produzione che lavoravano sulla stessa macchina non hanno notato nulla.

Un altro possibile problema è il crash del contenitore. Qui ci salvano le politiche di riavvio, che tutti conoscono, e Docker si occupa perfettamente della cosa. Praticamente tutti i compiti in produzione hanno una politica di riavvio sempre attiva. A volte utilizziamo on_failure per i compiti batch o per il debugging dei contenitori di produzione.

E cosa si può fare in caso di inattività di un intero nodo?

Ovviamente, avviare il contenitore su un'altra macchina. La parte interessante qui è cosa succede all'indirizzo IP (agli indirizzi) assegnati al contenitore.

Possiamo assegnare ai contenitori gli stessi indirizzi IP delle macchine nodi su cui vengono eseguiti. Quindi, quando avviamo un contenitore su un'altra macchina, il suo indirizzo IP cambia, e tutti i client devono comprendere che il contenitore si è spostato, e ora devono accedere a un altro indirizzo, il che richiede un servizio di Service Discovery separato.

Il Service Discovery è comodo. Sul mercato ci sono molte soluzioni di diversa resilienza per organizzare un registro dei servizi. Spesso in queste soluzioni viene implementata la logica di un bilanciatore di carico, conservazione di configurazioni aggiuntive in forma di KV-store, ecc.
Tuttavia, ci piacerebbe fare a meno della necessità di implementare un registro separato, poiché questo significherebbe introdurre un sistema critico, utilizzato da tutti i servizi in produzione. Significa che questo è un potenziale punto di failure e bisogna scegliere o sviluppare una soluzione molto affidabile, il che, ovviamente, è molto complesso, lungo e costoso.

E un altro grosso svantaggio: per far funzionare la nostra vecchia infrastruttura con la nuova, sarebbe stato necessario riscrivere assolutamente tutte le attività per utilizzare qualche sistema di Service Discovery. Ci sono moltissimi lavori, a volte impossibili, soprattutto quando si tratta di dispositivi a basso livello che operano a livello del nucleo del sistema operativo o direttamente con l'hardware. Implementare questa funzionalità utilizzando schemi di soluzione consolidati, come ad esempio side-car significherebbe, in alcuni casi, un carico aggiuntivo e, in altri, una maggiore complessità nella gestione e scenari di guasto aggiuntivi. Non volevamo complicare le cose, quindi abbiamo deciso di rendere opzionale l'uso di Service Discovery.

In one-cloud, l'IP segue il contenitore, cioè ogni istanza del task ha il proprio indirizzo IP. Questo indirizzo è "statico": viene assegnato a ciascuna istanza al momento del primo invio del servizio nel cloud. Se nel corso della vita del servizio ci sono stati vari istanze, alla fine avrà un numero di indirizzi IP pari al numero massimo di istanze esistite.

Successivamente, questi indirizzi non cambiano: vengono assegnati una sola volta e continuano ad esistere per tutta la vita del servizio in produzione. Gli indirizzi IP seguono i contenitori attraverso la rete. Se un contenitore viene spostato su un altro minion, anche l'indirizzo verrà trasferito con esso.

Pertanto, l'associazione tra il nome del servizio e la lista dei suoi indirizzi IP cambia molto raramente. Se guardiamo di nuovo ai nomi delle istanze del servizio che abbiamo menzionato all'inizio dell'articolo (1.ok-web.group1.web.front.prod, 2.ok-web.group1.web.front.prod, …), notiamo che assomigliano a FQDN utilizzati nel DNS. È proprio così, per visualizzare i nomi degli istanti dei servizi nei loro indirizzi IP utilizziamo il protocollo DNS. Inoltre, questo DNS restituisce tutti gli indirizzi IP riservati di tutti i container — sia attivi che fermati (per esempio, se si utilizzano tre repliche e abbiamo cinque indirizzi riservati, tutti e cinque verranno restituiti). I client, ricevuta questa informazione, tenteranno di connettersi a tutte e cinque le repliche — e in questo modo identificheranno quelle attive. Questo metodo di determinazione della disponibilità è notevolmente più affidabile, poiché non coinvolge né il DNS né il Service Discovery, il che significa che non ci sono problemi difficili da risolvere relativi all'aggiornamento delle informazioni e alla resilienza di questi sistemi. Inoltre, nei servizi critici, da cui dipende il funzionamento dell'intero portale, possiamo non utilizzare affatto il DNS, e semplicemente inserire gli indirizzi IP nella configurazione.

L'implementazione di questo trasferimento di IP dietro ai container può non essere banale — e ci fermeremo a come funziona nel seguente esempio:

One-cloud — sistema operativo di livello data center in Odnoklassniki

Supponiamo che il master one-cloud dia il comando al minion M1 di avviare 1.ok-web.group1.web.front.prod con l'indirizzo 1.1.1.1. Sul minion gira BIRD, che annuncia questo indirizzo a server specializzati route reflector. Ce ne sono tre di server route reflector, poiché questa è una parte molto critica dell'infrastruttura one-cloud — senza di essi, la rete in one-cloud non funzionerebbe. Li collocchiamo in diverse rack, per quanto possibile situati in diverse sale del data center, per ridurre la possibilità di un guasto simultaneo di tutti e tre.

Ora supponiamo che la connessione tra il master one-cloud e il minion M1 sia andata persa. Il master one-cloud agirà ora sulla base dell'ipotesi che M1 sia completamente guasto. Cioè darà il comando al minion M2 di avviare web.group1.web.front.prod con lo stesso indirizzo 1.1.1.1. Ora abbiamo due percorsi in conflitto nella rete per 1.1.1.1: su M1 e su M2. Per risolvere tali conflitti, utilizziamo il Multi Exit Discriminator, che viene specificato nell'annuncio BGP. Questo numero indica il peso del percorso annunciato. Verrà scelto il percorso con il valore MED più basso fra quelli in conflitto. Il master one-cloud supporta il MED come parte integrante degli indirizzi IP dei contenitori. La prima volta che l'indirizzo viene assegnato, ha un MED abbastanza alto = 1.000.000. In caso di un'emergenza di trasferimento del contenitore, il master diminuisce il MED, e M2 riceverà l'istruzione di annunciare l'indirizzo 1.1.1.1 con MED = 999.999. L'istanza operante su M1 rimarrà così disconnessa, e il suo destino ci interessa poco fino al momento in cui ripristineremo la connessione con il master, momento in cui verrà fermata come vecchio duplicato.

Incidenti

Tutti i sistemi di gestione dei data center gestiscono sempre in modo accettabile i piccoli guasti. L'uscita di un contenitore è una norma praticamente ovunque.

Consideriamo come gestiamo un incidente, ad esempio un'interruzione di corrente in una o più sale del data center.

Cosa significa incidente per un sistema di gestione di un data center? In primo luogo, si tratta di un guasto massivo simultaneo di molte macchine, e il sistema di gestione deve migrare simultaneamente un gran numero di contenitori. Ma se l'incidente è molto esteso, può succedere che tutte le attività non possano essere riposizionate su altri minion, perché la capacità del data center scende sotto il 100% di carico.

Spesso gli incidenti sono accompagnati da un guasto dello strato di gestione. Questo può accadere a causa del malfunzionamento dell'attrezzatura, ma più spesso perché gli incidenti non vengono testati, e lo strato di gestione crolla a causa dell'aumentato carico.

Cosa si può fare con tutto questo?

Le migrazioni di massa significano che nell'infrastruttura si verifica un gran numero di azioni, migrazioni e posizionamenti. Ogni migrazione può richiedere un certo tempo, necessario per consegnare e decomprimere le immagini dei contenitori ai minion, avviare e inizializzare i contenitori, ecc. Pertanto, è preferibile che le attività più importanti vengano avviate prima di quelle meno importanti.

Rivediamo nuovamente l'ormai familiare gerarchia dei servizi e cerchiamo di decidere quali attività vogliamo avviare per prime.

One-cloud — sistema operativo di livello data center in Odnoklassniki

Certo, questi sono i processi che partecipano direttamente all'elaborazione delle richieste degli utenti, cioè prod. Lo indichiamo con priorità di assegnazione — un numero che può essere assegnato alla coda. Se qualche coda ha una priorità più alta, i suoi servizi vengono assegnati per primi.

Su prod diamo priorità più elevate, 0; su batch — leggermente più basse, 100; su idle — ancora più basse, 200. Le priorità sono applicate in modo gerarchico. Tutte le attività sotto nella gerarchia avranno la priorità corrispondente. Se vogliamo che all'interno di prod le cache vengano avviate prima dei frontend, allora assegniamo priorità a cache = 0 e a front sotto-coda = 1. Se, ad esempio, vogliamo che il portale principale venga avviato prima dai frontend e il frontend musicale dopo, possiamo assegnare una priorità più bassa a quest'ultimo — 10.

Il problema successivo è la mancanza di risorse. Quindi, abbiamo avuto un guasto di un gran numero di attrezzature, intere sale del data center, e abbiamo avviato così tanti servizi che ora non ci sono abbastanza risorse per tutti. Dobbiamo decidere quali attività sacrificare affinché i servizi critici principali funzionino.

One-cloud — sistema operativo di livello data center in Odnoklassniki

A differenza della priorità di assegnazione, non possiamo sacrificare indiscriminatamente tutte le attività batch, alcune di esse sono importanti per il funzionamento del portale. Pertanto, abbiamo separato la priorità di preemption delle attività. Durante l'assegnazione, un'attività con priorità più alta può preemption, cioè fermare un'attività con priorità più bassa, se non ci sono più minion disponibili. Allo stesso tempo, l'attività a bassa priorità probabilmente rimarrà non assegnata, cioè non ci sarà più un minion adeguato con risorse sufficienti disponibili.

Nella nostra gerarchia è molto semplice impostare tale priorità di preemption, affinchè le attività prod e batch preemptano o fermino le attività idle, ma non tra di loro, impostando per idle una priorità pari a 200. Così come nel caso della priorità di assegnazione, possiamo utilizzare la nostra gerarchia per descrivere regole più complesse. Ad esempio, indichiamo che sacrificiamo la funzione musicale se non abbiamo abbastanza risorse per il portale web principale, impostando una priorità più bassa per i nodi corrispondenti: 10.

Incidenti del DC in generale

Perché potrebbe guastarsi l'intero data center? Cause naturali. C'era un buon post su come l'uragano ha influenzato il funzionamento del data center. Gli homeless possono essere considerati un elemento distruttivo, dato che hanno bruciato un giorno la fibra ottica in un collettore, causando la completa perdita di connettività del data center con gli altri siti. Le cause di guasto possono includere anche il fattore umano: un operatore può emettere un comando tale da far cadere l'intero data center. Questo può succedere a causa di un grosso bug. In generale, i data center si bloccano — non è una rarità. Da noi, ciò accade una volta ogni pochi mesi.

Ecco cosa facciamo per evitare che nessuno #okzhivi pubblichi sui Twitter.

La prima strategia è l'isolamento. Ogni istanza di one-cloud è isolata e può gestire solo le macchine di un singolo data center. Quindi, la perdita di un cloud a causa di bug o di un comando incorretto dell'operatore comporta solo la perdita di un data center. Siamo pronti a questo: abbiamo una politica di riserva dove le repliche di applicazioni e dati sono distribuite in tutti i data center. Utilizziamo database a prova di guasto e testiamo regolarmente i malfunzionamenti.
Poiché oggi abbiamo quattro data center, ci sono quindi quattro istanze separate e completamente isolate di one-cloud.

Questo approccio non solo protegge dai guasti fisici, ma può anche tutelare dagli errori degli operatori.

Cosa si può fare ancora riguardo al fattore umano? Quando un operatore dà al cloud un comando strano o potenzialmente pericoloso, potrebbe improvvisamente essere richiesto di risolvere un piccolo compito per verificare quanto bene ha pensato. Ad esempio, se si tratta di un arresto di massa di molte repliche o semplicemente di un comando strano — ridurre il numero di repliche o cambiare il nome dell'immagine, non solo il numero di versione nel nuovo manifesto.

One-cloud — sistema operativo di livello data center in Odnoklassniki

Conclusioni

Caratteristiche distintive di one-cloud:

  • Schema gerarchico e chiaro per la denominazione di servizi e contenitori, che consente di capire molto rapidamente di cosa si tratta, a cosa è correlato e come funziona e chi ne è responsabile.
  • Applichiamo la nostra tecnica di combinazione di attività prod- e batch-sui minions, per aumentare l'efficienza dell'uso condiviso delle macchine. Invece di cpuset, utilizziamo quote CPU, shares, politiche del piano di CPU e Linux QoS.
  • Non siamo riusciti a isolare completamente i contenitori che lavorano su una singola macchina, ma la loro influenza reciproca rimane entro il 20%.
  • L'organizzazione dei servizi in una gerarchia aiuta nella rimozione automatica delle emergenze tramite priorità di collocazione e di espulsione..

FAQ

Perché non abbiamo adottato una soluzione pronta.

  • Diverse classi di isolamento dei compiti richiedono logiche diverse per la distribuzione sui mini-server. Se i compiti di produzione possono essere distribuiti semplicemente riservando risorse, i compiti batch e idle devono essere distribuiti monitorando l'effettivo utilizzo delle risorse sui mini-server.
  • La necessità di considerare le risorse consumate dai compiti, come:
    • la larghezza di banda della rete;
    • tipi e ‘spindles’ dei dischi.
  • La necessità di specificare le priorità dei servizi durante la risoluzione di guasti, diritti e quote delle squadre sulle risorse, che può essere gestita tramite code gerarchiche in one-cloud.
  • La necessità di avere nomi umani per i container per ridurre il tempo di reazione a guasti e incidenti.
  • L'impossibilità di implementare simultaneamente il Service Discovery ovunque; necessità di convivere a lungo con compiti distribuiti su server fisici, il che è risolvibile tramite indirizzi IP 'statici' seguiti dai container, e conseguentemente la necessità di un'integrazione unica con una grande infrastruttura di rete.

Tutte queste funzioni richiederebbero significative modifiche alle soluzioni esistenti, e valutando il volume di lavoro, abbiamo capito che avremmo potuto sviluppare la nostra soluzione con circa lo stesso impiego di sforzi. Tuttavia, la nostra soluzione sarà decisamente più semplice da gestire e sviluppare: non contiene astrazioni non necessarie che supportano funzionalità superflue.

A coloro che leggono queste ultime righe, - grazie per la pazienza e l'attenzione!

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