Sistema operativo one-cloud a livello di data center in Odnoklassniki

Sistema operativo one-cloud a livello di data center in Odnoklassniki

Aloha, gente! Mi chiamo Oleg Anastasyev, lavoro presso Odnoklassniki nel team Platform. E oltre a me, c'è molto hardware che funziona in Odnoklassniki. Disponiamo di quattro data center con circa 500 rack con più di 8mila server. Ad un certo punto, ci siamo resi conto che l’introduzione di un nuovo sistema di gestione ci avrebbe permesso di caricare le apparecchiature in modo più efficiente, facilitare la gestione degli accessi, automatizzare la (ri)distribuzione delle risorse informatiche, accelerare il lancio di nuovi servizi e velocizzare le risposte. ad incidenti su larga scala.

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; networker che configurano software di rete; amministratori, o SRE, che forniscono la resilienza dell'infrastruttura; e team di sviluppo, ciascuno di essi è responsabile di una parte delle funzionalità del portale. Il software che creano funziona in questo modo:

Sistema operativo one-cloud a livello di data center in Odnoklassniki

Le richieste degli utenti giungono sia sui fronti del portale principale www.ok.rue su altri, ad esempio sul fronte delle API musicali. Per elaborare la logica aziendale, chiamano il server dell'applicazione che, durante l'elaborazione della richiesta, chiama i microservizi specializzati necessari: one-graph (grafico delle connessioni social), user-cache (cache dei profili utente), ecc.

Ciascuno di questi servizi è distribuito su molte macchine e ognuno di essi ha sviluppatori responsabili responsabili del funzionamento dei moduli, del loro funzionamento e dello sviluppo tecnologico. Tutti questi servizi vengono eseguiti su server hardware e fino a poco tempo fa lanciavamo esattamente un'attività per server, ovvero era specializzato per un'attività specifica.

Perché? Questo approccio presentava diversi vantaggi:

  • Sollevato gestione di massa. Diciamo che un'attività richiede alcune librerie, alcune impostazioni. Quindi il server viene assegnato esattamente a un gruppo specifico, viene descritta la politica di cfengine per questo gruppo (o è già stata descritta) e questa configurazione viene distribuita centralmente e automaticamente a tutti i server di questo gruppo.
  • Semplificato diagnostica. Supponiamo che tu osservi l'aumento del carico sul processore centrale e ti rendi conto che questo carico può essere generato solo dall'attività eseguita su questo processore hardware. La ricerca di qualcuno da incolpare finisce molto rapidamente.
  • Semplificato monitoraggio. Se c'è qualcosa che non va nel server, il monitor lo segnala e tu sai esattamente di chi è la colpa.

A un servizio composto da più repliche vengono assegnati più server, uno per ciascuno. Quindi la risorsa di calcolo per il servizio viene allocata in modo molto semplice: il numero di server di cui dispone il servizio, la quantità massima di risorse che può consumare. “Facile” qui non significa che sia facile da usare, ma nel senso che l’allocazione delle risorse viene effettuata manualmente.

Questo approccio ci ha anche permesso di farlo configurazioni specializzate del ferro per un'attività in esecuzione su questo server. Se l'attività memorizza grandi quantità di dati, utilizziamo un server 4U con uno chassis con 38 dischi. Se l'attività è puramente computazionale, possiamo acquistare un server 1U più economico. Questo è computazionalmente efficiente. Tra l'altro, questo approccio ci consente di utilizzare quattro volte meno macchine con un carico paragonabile a un social network amichevole.

Tale efficienza nell'uso delle risorse informatiche dovrebbe garantire anche l'efficienza economica, se si parte dal presupposto che la cosa più costosa sono i server. Per molto tempo, l'hardware è stato il più costoso e ci siamo impegnati molto per ridurre il prezzo dell'hardware, ideando algoritmi di tolleranza agli errori per ridurre i requisiti di affidabilità dell'hardware. E oggi siamo arrivati ​​alla fase in cui il prezzo del server non è più determinante. Se non si considerano gli ultimi esotici, la configurazione specifica dei server nel rack non ha importanza. Ora abbiamo un altro problema: il prezzo dello spazio occupato dal server nel data center, ovvero lo spazio nel rack.

Rendendoci conto che era così, abbiamo deciso di calcolare l'efficacia con cui stavamo utilizzando gli scaffali.
Abbiamo preso il prezzo del server più potente da quelli economicamente giustificabili, abbiamo calcolato quanti di questi server avremmo potuto posizionare nei rack, quante attività avremmo eseguito su di essi in base al vecchio modello “un server = un compito” e quanto le attività potrebbero utilizzare l'attrezzatura. Contarono e versarono lacrime. Si è scoperto che la nostra efficienza nell'utilizzo dei rack è di circa l'11%. La conclusione è ovvia: dobbiamo aumentare l’efficienza nell’utilizzo dei data center. Sembrerebbe che la soluzione sia ovvia: è necessario eseguire più attività contemporaneamente su un server. Ma è qui che iniziano le difficoltà.

La configurazione di massa diventa notevolmente più complicata: ora è impossibile assegnare un gruppo qualsiasi a un server. Dopotutto, ora è possibile avviare diverse attività di comandi diversi su un server. Inoltre, la configurazione potrebbe essere in conflitto per diverse applicazioni. Anche la diagnosi diventa più complicata: se noti un aumento del consumo della CPU o del disco su un server, non sai quale attività sta causando il problema.

Ma la cosa principale è che non esiste isolamento tra le attività in esecuzione sulla stessa macchina. Ecco, ad esempio, un grafico del tempo di risposta medio di un'attività del server prima e dopo l'avvio di un'altra applicazione di calcolo sullo stesso server, in nessun modo correlata alla prima: il tempo di risposta dell'attività principale è aumentato in modo significativo.

Sistema operativo one-cloud a livello di data center in Odnoklassniki

Ovviamente, è necessario eseguire attività in contenitori o macchine virtuali. Poiché quasi tutte le nostre attività vengono eseguite sotto un unico sistema operativo (Linux) o sono adattate ad esso, non abbiamo bisogno di supportare molti sistemi operativi diversi. Di conseguenza, la virtualizzazione non è necessaria; a causa del sovraccarico aggiuntivo, sarà meno efficiente della containerizzazione.

Come implementazione di contenitori per l'esecuzione di attività direttamente sui server, Docker è un buon candidato: le immagini del file system risolvono bene i problemi con configurazioni contrastanti. Il fatto che le immagini possano essere composte da più livelli ci consente di ridurre significativamente la quantità di dati necessari per distribuirle sull'infrastruttura, separando le parti comuni in livelli di base separati. Quindi i livelli di base (e più voluminosi) verranno memorizzati nella cache abbastanza rapidamente in tutta l'infrastruttura e per fornire molti tipi diversi di applicazioni e versioni, sarà necessario trasferire solo piccoli livelli.

Inoltre, un registro già pronto e il tagging delle immagini in Docker ci forniscono primitive già pronte per il controllo delle versioni e la consegna del codice alla produzione.

Docker, come qualsiasi altra tecnologia simile, ci fornisce un certo livello di isolamento del contenitore fin dall'inizio. Ad esempio, l'isolamento della memoria: a ciascun contenitore viene assegnato un limite all'utilizzo della memoria della macchina, oltre il quale non verrà consumata. Puoi anche isolare i contenitori in base all'utilizzo della CPU. Per noi, però, l’isolamento standard non era sufficiente. Ma ne parleremo più avanti.

L'esecuzione diretta dei contenitori sui server è solo una parte del problema. L'altra parte è relativa all'hosting dei contenitori sui server. È necessario capire quale contenitore può essere posizionato su quale server. Questo non è un compito così facile, perché i contenitori devono essere posizionati sui server il più densamente possibile senza ridurne la velocità. Tale posizionamento può anche essere difficile dal punto di vista della tolleranza agli errori. Spesso desideriamo posizionare repliche dello stesso servizio in rack diversi o anche in stanze diverse del data center, in modo che se un rack o una stanza si guasta, non perdiamo immediatamente tutte le repliche del servizio.

La distribuzione manuale dei contenitori non è un'opzione quando si hanno 8mila server e 8-16mila contenitori.

Inoltre, volevamo dare agli sviluppatori una maggiore indipendenza nell'allocazione delle risorse in modo che potessero ospitare i propri servizi in produzione da soli, senza l'aiuto di un amministratore. Allo stesso tempo, volevamo mantenere il controllo in modo che alcuni servizi minori non consumassero tutte le risorse dei nostri data center.

Ovviamente, abbiamo bisogno di un livello di controllo che lo faccia automaticamente.

Quindi siamo arrivati ​​a un'immagine semplice e comprensibile che tutti gli architetti adorano: tre quadrati.

Sistema operativo one-cloud a livello di data center in Odnoklassniki

one-cloud master è un cluster di failover responsabile dell'orchestrazione del cloud. Lo sviluppatore invia un manifest al master, che contiene tutte le informazioni necessarie per ospitare il servizio. Sulla base di ciò, il maestro impartisce comandi ai servitori selezionati (macchine progettate per far funzionare i contenitori). I servitori hanno il nostro agente, che riceve il comando, invia i suoi comandi a Docker e Docker configura il kernel Linux per avviare il contenitore corrispondente. Oltre a eseguire i comandi, l'agente riferisce continuamente al master sui cambiamenti nello stato sia della macchina minion che dei contenitori in esecuzione su di essa.

Risorsa per la raccolta

Ora diamo un'occhiata al problema dell'allocazione delle risorse più complessa per molti servitori.

Una risorsa informatica in un cloud è:

  • La quantità di potenza del processore consumata da un'attività specifica.
  • La quantità di memoria disponibile per l'attività.
  • Traffico di rete. Ciascuno dei servitori ha un'interfaccia di rete specifica con larghezza di banda limitata, quindi è impossibile distribuire i compiti senza tenere conto della quantità di dati che trasmettono sulla rete.
  • Dischi. Oltre, ovviamente, allo spazio per queste attività, assegniamo anche il tipo di disco: HDD o SSD. I dischi possono servire un numero finito di richieste al secondo: IOPS. Pertanto, per le attività che generano più IOPS di quelli che un singolo disco può gestire, allochiamo anche dei "mandrini", ovvero dispositivi disco che devono essere riservati esclusivamente all'attività.

Quindi per alcuni servizi, ad esempio per la cache utente, possiamo registrare le risorse consumate in questo modo: 400 core del processore, 2,5 TB di memoria, 50 Gbit/s di traffico in entrambe le direzioni, 6 TB di spazio su HDD posizionato su 100 assi. O in una forma più familiare come questa:

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

Le risorse del servizio cache utente consumano solo una parte di tutte le risorse disponibili nell'infrastruttura di produzione. Pertanto, voglio assicurarmi che improvvisamente, a causa di un errore dell'operatore o meno, la cache dell'utente non consumi più risorse di quelle ad essa assegnate. Dobbiamo cioè limitare le risorse. Ma a cosa potremmo legare la quota?

Torniamo al nostro diagramma notevolmente semplificato dell'interazione dei componenti e ridisegnalo con maggiori dettagli, in questo modo:

Sistema operativo one-cloud a livello di data center in Odnoklassniki

Cosa salta all'occhio:

  • Il frontend web e la musica utilizzano cluster isolati dello stesso server applicativo.
  • Possiamo distinguere gli strati logici a cui appartengono questi cluster: fronti, cache, archiviazione dei dati e livello di gestione.
  • Il frontend è eterogeneo; è costituito da diversi sottosistemi funzionali.
  • Le cache possono anche essere sparse nel sottosistema di cui memorizzano i dati.

Ridisegniamo nuovamente l'immagine:

Sistema operativo one-cloud a livello di data center in Odnoklassniki

Bah! Sì, vediamo una gerarchia! Ciò significa che puoi distribuire le risorse in blocchi più grandi: assegnare uno sviluppatore responsabile a un nodo di questa gerarchia corrispondente al sottosistema funzionale (come "musica" nell'immagine) e allegare una quota allo stesso livello della gerarchia. Questa gerarchia ci consente inoltre di organizzare i servizi in modo più flessibile per facilitarne la gestione. Ad esempio, dividiamo tutto il web, poiché si tratta di un raggruppamento di server molto ampio, in diversi gruppi più piccoli, mostrati nell'immagine come gruppo1, gruppo2.

Rimuovendo le righe extra, possiamo scrivere ogni nodo della nostra immagine in una forma più piatta: gruppo1.web.front, api.music.front, utente-cache.cache.

Si arriva così al concetto di “coda gerarchica”. Ha un nome come "group1.web.front". Ad esso viene assegnata una quota per risorse e diritti utente. Daremo alla persona di DevOps i diritti per inviare un servizio alla coda, e un tale dipendente potrà avviare qualcosa in coda, e la persona di OpsDev avrà diritti di amministratore, e ora potrà gestire la coda, assegnare persone lì, dare a queste persone i diritti, ecc. I servizi in esecuzione su questa coda verranno eseguiti entro la quota della coda. Se la quota di calcolo della coda non è sufficiente per eseguire tutti i servizi contemporaneamente, allora questi verranno eseguiti in sequenza, formando così la coda stessa.

Diamo uno sguardo più da vicino ai servizi. Un servizio ha un nome completo, che include sempre il nome della coda. Quindi il servizio web frontale avrà il nome ok-web.group1.web.front. E verrà chiamato il servizio del server delle applicazioni a cui accede ok-app.group1.web.front. Ogni servizio ha un manifest, che specifica tutte le informazioni necessarie per il posizionamento su macchine specifiche: quante risorse consuma questa attività, quale configurazione è necessaria, quante repliche dovrebbero esserci, proprietà per gestire gli errori di questo servizio. E dopo che il servizio è stato installato direttamente sulle macchine, vengono visualizzate le sue istanze. Sono anche nominati in modo inequivocabile, come numero di istanza e nome del servizio: 1.ok-web.gruppo1.web.front, 2.ok-web.gruppo1.web.front, …

Questo è molto comodo: guardando solo il nome del contenitore in esecuzione, possiamo subito scoprire molto.

Ora diamo uno sguardo più da vicino a ciò che effettivamente svolgono queste istanze: le attività.

Classi di isolamento delle attività

Tutte le attività in OK (e, probabilmente, ovunque) possono essere divise in gruppi:

  • Task a latenza breve - prod. Per tali attività e servizi, il ritardo della risposta (latenza), ovvero la velocità con cui ciascuna richiesta verrà elaborata dal sistema, è molto importante. Esempi di attività: fronti Web, cache, server applicazioni, archiviazione OLTP, ecc.
  • Problemi di calcolo - batch. In questo caso non è importante la velocità di elaborazione di ogni specifica richiesta. Per loro, è importante quanti calcoli eseguirà questa attività in un determinato (lungo) periodo di tempo (velocità effettiva). Questi saranno tutti i compiti di MapReduce, Hadoop, machine learning, statistiche.
  • Attività in background - inattive. Per tali attività, né la latenza né il throughput sono molto importanti. Ciò include vari test, migrazioni, ricalcoli e conversione di dati da un formato all'altro. Da un lato sono simili a quelli calcolati, dall’altro non ci importa quanto velocemente vengono completati.

Vediamo come tali attività consumano risorse, ad esempio il processore centrale.

Compiti a breve ritardo. Tale attività avrà un modello di consumo della CPU simile a questo:

Sistema operativo one-cloud a livello di data center in Odnoklassniki

Viene ricevuta una richiesta da parte dell'utente per l'elaborazione, l'attività inizia a utilizzare tutti i core della CPU disponibili, la elabora, restituisce una risposta, attende la richiesta successiva e si interrompe. È arrivata la richiesta successiva: ancora una volta abbiamo scelto tutto quello che c'era, lo abbiamo calcolato e stiamo aspettando la successiva.

Per garantire la latenza minima per un compito del genere, dobbiamo prendere il massimo delle risorse consumate e riservare il numero richiesto di core sul minion (la macchina che eseguirà l'attività). Quindi la formula di prenotazione per il nostro problema sarà la seguente:

alloc: cpu = 4 (max)

e se disponiamo di una macchina minion con 16 core, è possibile assegnarvi esattamente quattro di questi compiti. Notiamo in particolare che il consumo medio del processore per tali attività è spesso molto basso, il che è ovvio, poiché per una parte significativa del tempo l'attività attende una richiesta e non fa nulla.

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

Sistema operativo one-cloud a livello di data center in Odnoklassniki

Il consumo medio di risorse della CPU per tali attività è piuttosto elevato. Spesso desideriamo che un'attività di calcolo venga completata in un certo periodo di tempo, quindi dobbiamo riservare il numero minimo di processori necessari in modo che l'intero calcolo venga completato in un tempo accettabile. La sua formula di prenotazione sarà simile alla seguente:

alloc: cpu = [1,*)

"Per favore, posizionalo su un servitore dove c'è almeno un nucleo libero, e poi quanti ce ne sono, divorerà tutto."

Qui l'efficienza d'uso è già molto migliore rispetto alle attività con un breve ritardo. Ma il guadagno sarà molto maggiore se combini entrambi i tipi di compiti su una macchina servitore e distribuisci le sue risorse mentre sei in movimento. Quando un'attività con un breve ritardo richiede un processore, lo riceve immediatamente e quando le risorse non sono più necessarie, vengono trasferite all'attività computazionale, ad es. qualcosa del genere:

Sistema operativo one-cloud a livello di data center in Odnoklassniki

Ma come si fa?

Per prima cosa, diamo un'occhiata a prod e al suo alloc: cpu = 4. Dobbiamo riservare quattro core. In Docker Run ciò può essere fatto in due modi:

  • Utilizzando l'opzione --cpuset=1-4, ovvero assegnare quattro core specifici sulla macchina all'attività.
  • Da usare --cpuquota=400_000 --cpuperiod=100_000, assegnare una quota per il tempo del processore, ovvero indicare che ogni 100 ms di tempo reale l'attività non consuma più di 400 ms di tempo del processore. Si ottengono gli stessi quattro core.

Ma quale di questi metodi è adatto?

cpuset sembra piuttosto attraente. L'attività ha quattro core dedicati, il che significa che le cache del processore funzioneranno nel modo più efficiente possibile. Ciò ha anche uno svantaggio: dovremmo assumerci il compito di distribuire i calcoli sui core non caricati della macchina invece che sul sistema operativo, e questo è un compito piuttosto non banale, soprattutto se proviamo a collocare attività batch su un tale macchina. I test hanno dimostrato che l'opzione con quota è più adatta in questo caso: in questo modo il sistema operativo ha più libertà nella scelta del core per eseguire l'attività in quel momento e il tempo del processore viene distribuito in modo più efficiente.

Scopriamo come effettuare prenotazioni in Docker in base al numero minimo di core. La quota per le attività batch non è più applicabile, perché non è necessario limitare il massimo, è sufficiente garantire solo il minimo. E qui l'opzione si adatta bene docker run --cpushares.

Abbiamo concordato che se un lotto richiede una garanzia per almeno un nucleo, lo indicheremo --cpushares=1024e se ci sono almeno due core, indichiamo --cpushares=2048. Le condivisioni della CPU non interferiscono in alcun modo con la distribuzione del tempo del processore purché ce ne sia abbastanza. Pertanto, se prod non utilizza attualmente tutti i suoi quattro core, non c'è nulla che limiti le attività batch e possono utilizzare tempo di processore aggiuntivo. Ma in una situazione in cui c'è carenza di processori, se prod ha consumato tutti e quattro i suoi core e ha raggiunto la sua quota, il tempo rimanente del processore sarà diviso proporzionalmente a cpushares, cioè in una situazione di tre core liberi, uno sarà assegnati a un'attività con 1024 cpushares, mentre i restanti due verranno assegnati a un'attività con 2048 cpushares.

Ma utilizzare quote e condivisioni non è sufficiente. Dobbiamo assicurarci che un'attività con un breve ritardo riceva la priorità rispetto a un'attività batch quando si alloca il tempo del processore. Senza tale definizione delle priorità, l'attività batch occuperà tutto il tempo del processore nel momento in cui sarà necessaria al prodotto. Non sono presenti opzioni di definizione delle priorità dei contenitori nell'esecuzione di Docker, ma le policy di pianificazione della CPU Linux risultano utili. Puoi leggerli in dettaglio qui, e nell'ambito di questo articolo li esamineremo brevemente:

  • SCHED_ALTRO
    Per impostazione predefinita, tutti i normali processi utente su una macchina Linux ricevono.
  • SCHED_BATCH
    Progettato per processi ad uso intensivo di risorse. Quando si posiziona un'attività su un processore, viene introdotta una cosiddetta penalità di attivazione: tale attività ha meno probabilità di ricevere risorse del processore se è attualmente utilizzata da un'attività con SCHED_OTHER
  • SCHED_IDLE
    Un processo in background con una priorità molto bassa, addirittura inferiore a nice -19. Usiamo la nostra libreria open source uno-nio, per impostare la policy necessaria all'avvio del contenitore chiamando

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

Ma anche se non programmi in Java, la stessa cosa può essere fatta usando il comando chrt:

chrt -i 0 $pid

Riassumiamo tutti i nostri livelli di isolamento in un'unica tabella per chiarezza:

Classe di isolamento
Esempio di alloc
Opzioni di esecuzione Docker
sched_setscheduler grafico*

Prod
CPU = 4
--cpuquota=400000 --cpuperiod=100000
SCHED_ALTRO

Partita
CPU = [1, *)
--cpushares=1024
SCHED_BATCH

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

*Se stai eseguendo chrt dall'interno di un contenitore, potresti aver bisogno della funzionalità sys_nice, perché per impostazione predefinita Docker rimuove questa funzionalità all'avvio del contenitore.

Ma le attività consumano non solo il processore, ma anche il traffico, il che influisce sulla latenza di un'attività di rete ancor più dell'allocazione errata delle risorse del processore. Pertanto, vogliamo naturalmente ottenere esattamente la stessa immagine per il traffico. Cioè, quando un'attività di produzione invia alcuni pacchetti alla rete, limitiamo la velocità massima (formula allocazione: lan=[*,500Mbps) ), con quale prod può farlo. E per batch garantiamo solo la produttività minima, ma non limitiamo quella massima (formula alloca: lan=[10Mbps,*) ) In questo caso, il traffico di produzione dovrebbe avere la priorità rispetto alle attività batch.
Qui Docker non ha primitive che possiamo usare. Ma ci viene in aiuto Controllo del traffico Linux. Siamo stati in grado di ottenere il risultato desiderato con l'aiuto della disciplina Curva gerarchica del servizio equo. Con il suo aiuto, distinguiamo due classi di traffico: prod ad alta priorità e batch/idle a bassa priorità. Di conseguenza, la configurazione per il traffico in uscita è così:

Sistema operativo one-cloud a livello di data center in Odnoklassniki

qui 1:0 è la “radice qdisc” della disciplina hsfc; 1:1 - classe figlia hsfc con un limite di larghezza di banda totale di 8 Gbit/s, sotto il quale vengono poste le classi figlie di tutti i contenitori; 1:2 - la classe figlia hsfc è comune a tutte le attività batch e inattive con un limite "dinamico", discusso di seguito. Le restanti classi figlie hsfc sono classi dedicate per i contenitori prod attualmente in esecuzione con limiti corrispondenti ai loro manifest: 450 e 400 Mbit/s. A ciascuna classe hsfc viene assegnata 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.

In genere, le discipline tc servono a dare priorità solo al traffico in uscita. Ma vogliamo dare priorità anche al traffico in entrata: dopo tutto, alcune attività batch possono facilmente selezionare l'intero canale in entrata, ricevendo, ad esempio, un grosso batch di dati di input 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 entrata dall'interfaccia al traffico in uscita su ifbX. Inoltre, per ifbX, tutte le stesse discipline funzionano per controllare il traffico in uscita, per cui la configurazione hsfc sarà molto simile:

Sistema operativo one-cloud a livello di data center in Odnoklassniki

Durante gli esperimenti, abbiamo scoperto che hsfc mostra i migliori risultati quando la classe 1:2 del traffico batch/idle non prioritario è limitata sulle macchine minion a non più di una certa corsia libera. Altrimenti, il traffico non prioritario avrà un impatto eccessivo sulla latenza delle attività di produzione. miniond determina la quantità attuale di larghezza di banda libera ogni secondo, misurando il consumo medio di traffico di tutte le attività di produzione di un dato minion Sistema operativo one-cloud a livello di data center in Odnoklassniki e sottraendolo dalla larghezza di banda dell'interfaccia di rete Sistema operativo one-cloud a livello di data center in Odnoklassniki con un piccolo margine, ad es.

Sistema operativo one-cloud a livello di data center in Odnoklassniki

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

Pertanto, abbiamo implementato tutte e tre le classi di isolamento: prod, batch e idle. Queste classi influenzano notevolmente le caratteristiche prestazionali delle attività. Pertanto, abbiamo deciso di posizionare questo attributo in cima alla gerarchia, in modo che guardando il nome della coda gerarchica fosse immediatamente chiaro con cosa abbiamo a che fare:

Sistema operativo one-cloud a livello di data center in Odnoklassniki

Tutti i nostri amici sito web и musica i frontali vengono poi inseriti nella gerarchia sotto prod. Ad esempio, in batch, inseriamo il servizio catalogo musicale, che compila periodicamente un catalogo di brani da una serie di file mp3 caricati su Odnoklassniki. Potrebbe essere un esempio di un servizio inattivo trasformatore musicale, che normalizza il livello del volume della musica.

Rimosse nuovamente le righe extra, possiamo scrivere i nomi dei nostri servizi in modo più piatto aggiungendo la classe di isolamento delle attività alla fine del nome completo del servizio: web.front.prod, catalogo.music.batch, trasformatore.music.idle.

E ora, guardando il nome del servizio, capiamo non solo quale funzione svolge, ma anche la sua classe di isolamento, ovvero la sua criticità, ecc.

Tutto è fantastico, ma c'è un'amara verità. È impossibile isolare completamente le attività in esecuzione su una macchina.

Cosa siamo riusciti a ottenere: se il batch consuma intensamente solo Risorse della CPU, quindi lo scheduler della CPU Linux integrato fa molto bene il suo lavoro e non c'è praticamente alcun impatto sull'attività di produzione. Ma se questa attività batch inizia a lavorare attivamente con la memoria, appare già l'influenza reciproca. Ciò accade perché l'attività di produzione viene "lavata via" dalle cache di memoria del processore: di conseguenza, i mancati riscontri nella cache aumentano e il processore elabora l'attività di produzione più lentamente. Un'attività batch di questo tipo può aumentare del 10% la latenza del nostro tipico contenitore di prodotti.

Isolare il traffico è ancora più difficile a causa del fatto che le moderne schede di rete hanno una coda interna di pacchetti. Se il pacchetto dell'attività batch arriva per primo, sarà il primo a essere trasmesso via cavo e non si potrà fare nulla al riguardo.

Inoltre, finora siamo riusciti a risolvere solo il problema della definizione delle priorità del traffico TCP: l'approccio hsfc non funziona per UDP. E anche nel caso del traffico TCP, se l'attività batch genera molto traffico, ciò comporta anche un aumento di circa il 10% nel ritardo dell'attività prod.

tolleranza ai guasti

Uno degli obiettivi durante lo sviluppo di one-cloud era migliorare la tolleranza agli errori di Odnoklassniki. Pertanto, vorrei considerare più in dettaglio i possibili scenari di guasti e incidenti. Cominciamo con uno scenario semplice: un guasto del contenitore.

Il contenitore stesso può guastarsi in diversi modi. Potrebbe trattarsi di una sorta di esperimento, bug o errore nel manifest, a causa del quale l'attività di produzione inizia a consumare più risorse di quanto indicato nel manifest. Abbiamo avuto un caso: uno sviluppatore ha implementato un algoritmo complesso, lo ha rielaborato molte volte, ci ha ripensato ed è diventato così confuso che alla fine il problema è entrato in un ciclo molto non banale. E poiché l'attività di produzione ha una priorità più alta rispetto a tutte le altre sugli stessi servitori, ha iniziato a consumare tutte le risorse disponibili del processore. In questa situazione, l'isolamento, o meglio il limite di tempo della CPU, ha salvato la situazione. Se a un'attività viene assegnata una quota, l'attività non ne consumerà di più. Pertanto, le attività batch e altre attività di produzione eseguite sulla stessa macchina non hanno notato nulla.

Il secondo possibile problema è la caduta del contenitore. E qui le politiche di ripartenza ci salvano, le conoscono tutti, la stessa Docker fa un ottimo lavoro. Quasi tutte le attività di produzione hanno una politica di riavvio sempre. A volte utilizziamo on_failure per attività batch o per il debug dei contenitori di prodotti.

Cosa puoi fare se un intero servitore non è disponibile?

Ovviamente, esegui il contenitore su un'altra macchina. La parte interessante qui è cosa succede agli indirizzi IP assegnati al contenitore.

Possiamo assegnare ai contenitori gli stessi indirizzi IP delle macchine minion su cui vengono eseguiti questi contenitori. Quindi, quando il contenitore viene avviato su un'altra macchina, il suo indirizzo IP cambia e tutti i client devono capire che il contenitore si è spostato e ora devono andare a un indirizzo diverso, il che richiede un servizio Service Discovery separato.

Il Service Discovery è conveniente. Esistono molte soluzioni sul mercato con vari gradi di tolleranza agli errori per l'organizzazione di un registro di servizi. Spesso tali soluzioni implementano la logica del bilanciamento del carico, memorizzano configurazioni aggiuntive sotto forma di storage KV, ecc.
Vorremmo tuttavia evitare la necessità di implementare un registro separato, perché ciò significherebbe introdurre un sistema critico utilizzato da tutti i servizi in produzione. Ciò significa che questo è un potenziale punto di fallimento ed è necessario scegliere o sviluppare una soluzione molto tollerante ai guasti, il che è ovviamente molto difficile, dispendioso in termini di tempo e costoso.

E un altro grande svantaggio: affinché la nostra vecchia infrastruttura funzioni con quella nuova, dovremmo riscrivere assolutamente tutte le attività per utilizzare una sorta di sistema di Service Discovery. C'è MOLTO lavoro e in alcuni punti è quasi impossibile quando si tratta di dispositivi di basso livello che funzionano a livello del kernel del sistema operativo o direttamente con l'hardware. Implementazione di questa funzionalità utilizzando modelli di soluzioni consolidati, come ad esempio sidecar in alcuni punti significherebbe un carico aggiuntivo, in altri una complicazione operativa e ulteriori scenari di guasto. Non volevamo complicare le cose, quindi abbiamo deciso di rendere facoltativo l'utilizzo di Service Discovery.

In one-cloud, l'IP segue il contenitore, ovvero ogni istanza dell'attività ha il proprio indirizzo IP. Questo indirizzo è “statico”: viene assegnato a ciascuna istanza quando il servizio viene inviato per la prima volta sul cloud. Se un servizio ha avuto un numero diverso di istanze durante la sua vita, alla fine gli verranno assegnati tanti indirizzi IP quante sono le istanze massime.

Successivamente questi indirizzi non cambiano: vengono assegnati una volta e continuano ad esistere per tutta la vita del servizio in produzione. Gli indirizzi IP seguono i contenitori attraverso la rete. Se il contenitore viene trasferito a un altro servitore, l'indirizzo lo seguirà.

Pertanto, la mappatura di un nome di servizio nel suo elenco di indirizzi IP cambia molto raramente. Se guardi di nuovo i 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, …), noteremo che assomigliano agli FQDN utilizzati nel DNS. Esatto, per mappare i nomi delle istanze del servizio sui loro indirizzi IP, utilizziamo il protocollo DNS. Inoltre, questo DNS restituisce tutti gli indirizzi IP riservati di tutti i contenitori, sia in esecuzione che interrotti (diciamo che vengono utilizzate tre repliche e che abbiamo cinque indirizzi riservati lì: verranno restituiti tutti e cinque). I client, dopo aver ricevuto queste informazioni, proveranno a stabilire una connessione con tutte e cinque le repliche, determinando così quelle che funzionano. Questa opzione per determinare la disponibilità è molto più affidabile; non coinvolge né DNS né Service Discovery, il che significa che non ci sono problemi difficili da risolvere per garantire la pertinenza delle informazioni e la tolleranza agli errori di questi sistemi. Inoltre, nei servizi critici da cui dipende il funzionamento dell'intero portale, non possiamo utilizzare affatto il DNS, ma semplicemente inserire gli indirizzi IP nella configurazione.

L'implementazione di tale trasferimento IP dietro i contenitori può non essere banale e vedremo come funziona con il seguente esempio:

Sistema operativo one-cloud a livello di data center in Odnoklassniki

Supponiamo che il master di una nuvola dia il comando al servitore M1 di correre 1.ok-web.group1.web.front.prod con indirizzo 1.1.1.1. Funziona su un servitore BIRD, che pubblicizza questo indirizzo su server speciali riflettore del percorso. Questi ultimi hanno una sessione BGP con l'hardware di rete, in cui viene tradotto il percorso dell'indirizzo 1.1.1.1 su M1. M1 instrada i pacchetti all'interno del contenitore utilizzando Linux. Esistono tre server riflettori di percorso, poiché questa è una parte molto critica dell'infrastruttura one-cloud: senza di essi, la rete in one-cloud non funzionerà. Li collochiamo in rack diversi, se possibile situati in stanze diverse del data center, per ridurre la probabilità che tutti e tre si guastino contemporaneamente.

Supponiamo ora che la connessione tra il master di una nuvola e il servitore M1 sia persa. Il one-cloud master ora agirà partendo dal presupposto che M1 abbia completamente fallito. Cioè, darà il comando al servitore M2 di lanciarsi web.group1.web.front.prod con lo stesso indirizzo 1.1.1.1. Ora abbiamo due percorsi in conflitto sulla rete per 1.1.1.1: su M1 e su M2. Per risolvere tali conflitti, utilizziamo il Multi Exit Discriminator, specificato nell'annuncio BGP. Questo è un numero che mostra il peso del percorso pubblicizzato. Tra le rotte in conflitto verrà selezionata la rotta con il valore MED più basso. Il master one-cloud supporta MED come parte integrante degli indirizzi IP del contenitore. Per la prima volta l'indirizzo viene scritto con un MED sufficientemente grande = 1 In caso di trasferimento di emergenza di un container, il master riduce il MED e M000 riceverà già il comando di pubblicizzare l'indirizzo 000 con MED = 2 L'istanza in esecuzione su M1.1.1.1 rimarrà in questo caso non c'è connessione, e il suo ulteriore destino ci interessa poco finché non verrà ripristinata la connessione con il master, quando verrà fermato come un vecchio take.

Fallimenti

Tutti i sistemi di gestione dei data center gestiscono sempre in modo accettabile i guasti minori. Il traboccamento dei contenitori è la norma quasi ovunque.

Diamo un'occhiata a come gestiamo un'emergenza, come un'interruzione di corrente in una o più stanze di un data center.

Cosa significa un incidente per un sistema di gestione del data center? Prima di tutto, si tratta di un enorme guasto occasionale di molte macchine e il sistema di controllo deve migrare molti container contemporaneamente. Ma se il disastro è su larga scala, può accadere che tutte le attività non possano essere riassegnate ad altri servitori, perché la capacità delle risorse del data center scende al di sotto del 100% del carico.

Spesso gli incidenti sono accompagnati dal fallimento del livello di controllo. Ciò può accadere a causa del guasto delle sue apparecchiature, ma più spesso a causa del fatto che gli incidenti non vengono testati e lo stesso livello di controllo cade a causa dell'aumento del carico.

Cosa puoi fare riguardo a tutto questo?

Le migrazioni di massa implicano un gran numero di attività, migrazioni e implementazioni che si verificano nell'infrastruttura. Ognuna delle migrazioni potrebbe richiedere del tempo necessario per consegnare e decomprimere le immagini dei contenitori ai servitori, lanciare e inizializzare i contenitori, ecc. Pertanto, è auspicabile che le attività più importanti vengano avviate prima di quelle meno importanti.

Esaminiamo nuovamente la gerarchia dei servizi con cui abbiamo familiarità e proviamo a decidere quali attività vogliamo eseguire per prime.

Sistema operativo one-cloud a livello di data center in Odnoklassniki

Naturalmente questi sono i processi direttamente coinvolti nell'elaborazione delle richieste degli utenti, ovvero prod. Lo indichiamo con priorità di posizionamento — un numero che può essere assegnato alla coda. Se una coda ha una priorità più alta, i suoi servizi vengono posizionati per primi.

Su prod assegniamo priorità più alte, 0; in batch: leggermente inferiore, 100; inattivo - anche inferiore, 200. Le priorità vengono applicate gerarchicamente. Tutte le attività più in basso nella gerarchia avranno una priorità corrispondente. Se vogliamo che le cache all'interno di prod vengano lanciate prima dei frontend, allora assegniamo priorità a cache = 0 e a front subqueues = 1. Se, ad esempio, vogliamo che il portale principale venga lanciato prima dai fronti, e solo dal fronte musicale quindi possiamo assegnare a quest'ultimo una priorità inferiore - 10.

Il problema successivo è la mancanza di risorse. Quindi, una grande quantità di attrezzature, interi padiglioni del data center, sono andati in panne, e abbiamo rilanciato così tanti servizi che ora non ci sono risorse sufficienti per tutti. È necessario decidere quali attività sacrificare per mantenere in funzione i principali servizi critici.

Sistema operativo one-cloud a livello di data center in Odnoklassniki

A differenza della priorità di posizionamento, non possiamo sacrificare indiscriminatamente tutte le attività batch; alcune di esse sono importanti per il funzionamento del portale. Pertanto, abbiamo evidenziato separatamente priorità di prelazione compiti. Una volta piazzato, un compito con priorità più alta può anticipare, cioè fermare, un compito con priorità più bassa se non ci sono più servitori liberi. In questo caso, un compito con una priorità bassa probabilmente rimarrà non assegnato, cioè non ci sarà più un servitore adatto con abbastanza risorse libere.

Nella nostra gerarchia, è molto semplice specificare una priorità di prelazione tale che le attività di produzione e batch precedano o interrompano le attività inattive, ma non tra loro, specificando una priorità per inattiva pari a 200. Proprio come nel caso della priorità di posizionamento, noi può utilizzare la nostra gerarchia per descrivere regole più complesse. Ad esempio, indichiamo che sacrifichiamo la funzione musica se non disponiamo di risorse sufficienti per il portale web principale, impostando la priorità per i nodi corrispondenti più bassa: 10.

Interi incidenti DC

Perché l'intero data center potrebbe fallire? Elemento. È stato un buon post l'uragano ha influenzato il lavoro del data center. Gli elementi possono essere considerati senzatetto che una volta bruciavano l'ottica nel collettore e il data center perdeva completamente il contatto con altri siti. La causa del guasto può anche essere un fattore umano: l'operatore impartirà un comando tale da far crollare l'intero data center. Ciò potrebbe accadere a causa di un grosso bug. In generale, il collasso dei data center non è raro. Questo ci succede una volta ogni pochi mesi.

E questo è quello che facciamo per evitare che qualcuno twitti #vivo.

La prima strategia è l’isolamento. Ogni istanza one-cloud è isolata e può gestire le macchine in un solo data center. Cioè, la perdita di un cloud a causa di bug o comandi errati dell'operatore equivale alla perdita di un solo data center. Siamo pronti per questo: abbiamo una politica di ridondanza in cui le repliche dell’applicazione e dei dati si trovano in tutti i data center. Utilizziamo database tolleranti agli errori e testiamo periodicamente la presenza di errori.
Dato che oggi abbiamo quattro data center, ciò significa quattro istanze separate e completamente isolate di un unico cloud.

Questo approccio non solo protegge dai guasti fisici, ma può anche proteggere dagli errori dell'operatore.

Cos’altro si può fare con il fattore umano? Quando un operatore dà al cloud un comando strano o potenzialmente pericoloso, gli può essere improvvisamente chiesto di risolvere un piccolo problema per vedere se ha pensato bene. Ad esempio, se si tratta di una sorta di arresto di massa di molte repliche o semplicemente di uno strano comando: ridurre il numero di repliche o modificare il nome dell'immagine e non solo il numero di versione nel nuovo manifest.

Sistema operativo one-cloud a livello di data center in Odnoklassniki

Risultati di

Caratteristiche distintive di un cloud:

  • Schema di denominazione gerarchico e visivo per servizi e contenitori, che ti consente di scoprire molto rapidamente qual è l'attività, a cosa si riferisce, come funziona e chi ne è responsabile.
  • Applichiamo il ns tecnica di combinare prodotto e lottocompiti sui servitori per migliorare l'efficienza della condivisione della macchina. Invece di cpuset utilizziamo quote CPU, condivisioni, policy di pianificazione della CPU e QoS Linux.
  • Non è stato possibile isolare completamente i contenitori in funzione sulla stessa macchina, ma la loro influenza reciproca rimane entro il 20%.
  • L'organizzazione dei servizi in una gerarchia aiuta con l'utilizzo del ripristino di emergenza automatico priorità di posizionamento e di prelazione.

FAQ

Perché non abbiamo adottato una soluzione già pronta?

  • Diverse classi di isolamento delle attività richiedono una logica diversa quando vengono assegnate ai servitori. Se le attività di produzione possono essere posizionate semplicemente prenotando risorse, allora devono essere posizionate attività batch e inattive, monitorando l'effettivo utilizzo delle risorse sulle macchine minion.
  • La necessità di tenere conto delle risorse consumate da attività, come ad esempio:
    • larghezza di banda della rete;
    • tipologie e “mandrini” dei dischi.
  • La necessità di indicare le priorità dei servizi durante la risposta alle emergenze, i diritti e le quote di comando per le risorse, che viene risolta utilizzando code gerarchiche in un unico cloud.
  • La necessità di dare una denominazione umana ai contenitori per ridurre i tempi di risposta a incidenti e inconvenienti
  • L'impossibilità di un'implementazione diffusa e una tantum del Service Discovery; la necessità di coesistere per lungo tempo con attività ospitate su host hardware, cosa che viene risolta con indirizzi IP “statici” che seguono i contenitori e, di conseguenza, la necessità di un'integrazione unica con una grande infrastruttura di rete.

Tutte queste funzioni richiederebbero modifiche significative delle soluzioni esistenti per adattarle alle nostre esigenze e, dopo aver valutato la quantità di lavoro, ci siamo resi conto che avremmo potuto sviluppare la nostra soluzione con approssimativamente gli stessi costi di manodopera. Ma la tua soluzione sarà molto più semplice da utilizzare e sviluppare: non contiene astrazioni non necessarie che supportano funzionalità di cui non abbiamo bisogno.

A chi ha letto le ultime righe, grazie per la pazienza e l'attenzione!

Fonte: habr.com

Aggiungi un commento