Sistema operativo a nivel de centro de datos dunha nube en Odnoklassniki

Sistema operativo a nivel de centro de datos dunha nube en Odnoklassniki

Ai, xente! Chámome Oleg Anastasyev, traballo en Odnoklassniki no equipo da Plataforma. E ademais de min, hai moito hardware traballando en Odnoklassniki. Temos catro centros de datos con preto de 500 racks con máis de 8 mil servidores. En certo momento, decatámonos de que a introdución dun novo sistema de xestión permitiríanos cargar equipos de forma máis eficiente, facilitar a xestión de accesos, automatizar a (re)distribución dos recursos informáticos, acelerar o lanzamento de novos servizos e acelerar as respostas. a accidentes a gran escala.

Que saíu?

Ademais de min e dunha chea de hardware, tamén hai xente que traballa con este hardware: enxeñeiros que están situados directamente en centros de datos; usuarios de rede que configuran software de rede; administradores, ou SRE, que proporcionan resiliencia da infraestrutura; e equipos de desenvolvemento, cada un deles é responsable de parte das funcións do portal. O software que crean funciona algo así:

Sistema operativo a nivel de centro de datos dunha nube en Odnoklassniki

As solicitudes dos usuarios recíbense tanto nas fachadas do portal principal www.ok.ru, e noutros, por exemplo nas frontes da API de música. Para procesar a lóxica empresarial, chaman ao servidor de aplicacións que, ao procesar a solicitude, chama aos microservizos especializados necesarios: un gráfico (gráfico de conexións sociais), caché de usuarios (caché de perfís de usuario), etc.

Cada un destes servizos está implantado en moitas máquinas, e cada un deles conta con desenvolvedores responsables do funcionamento dos módulos, do seu funcionamento e do desenvolvemento tecnolóxico. Todos estes servizos funcionan en servidores de hardware, e ata hai pouco lanzábamos exactamente unha tarefa por servidor, é dicir, estaba especializada para unha tarefa específica.

Por que é iso? Este enfoque tiña varias vantaxes:

  • Aliviado xestión masiva. Digamos que unha tarefa require algunhas bibliotecas, algunhas opcións de configuración. E entón o servidor asígnase exactamente a un grupo específico, descríbese a política cfengine para este grupo (ou xa se describiu) e esta configuración desprázase de forma centralizada e automática a todos os servidores deste grupo.
  • Simplificado diagnósticos. Digamos que observa o aumento da carga do procesador central e dáse conta de que esta carga só podería ser xerada pola tarefa que se executa neste procesador de hardware. A procura de alguén a quen culpar remata moi rápido.
  • Simplificado vixilancia. Se algo falla co servidor, o monitor infórmao e sabes exactamente quen é o culpable.

A un servizo que consta de varias réplicas asígnaselles varios servidores, un para cada un. Entón, o recurso informático para o servizo atribúese de forma moi sinxela: o número de servidores que ten o servizo, a cantidade máxima de recursos que pode consumir. "Fácil" aquí non significa que sexa fácil de usar, senón no sentido de que a asignación de recursos faise manualmente.

Este enfoque tamén nos permitiu facelo configuracións de ferro especializadas para unha tarefa en execución neste servidor. Se a tarefa almacena grandes cantidades de datos, entón usamos un servidor 4U cun chasis con 38 discos. Se a tarefa é puramente computacional, podemos mercar un servidor 1U máis barato. Isto é computacionalmente eficiente. Entre outras cousas, este enfoque permítenos utilizar catro veces menos máquinas cunha carga comparable á dunha rede social amigable.

Tal eficiencia no uso dos recursos informáticos tamén debería garantir a eficiencia económica, se partimos da premisa de que o máis caro son os servidores. Durante moito tempo, o hardware foi o máis caro e esforzámonos moito en reducir o prezo do hardware, creando algoritmos de tolerancia a fallos para reducir os requisitos de fiabilidade do hardware. E hoxe chegamos á fase na que o prezo do servidor deixou de ser determinante. Se non considera os últimos exóticos, a configuración específica dos servidores do rack non importa. Agora temos outro problema: o prezo do espazo que ocupa o servidor no centro de datos, é dicir, o espazo no rack.

Ao entender que era así, decidimos calcular a eficacia con que estabamos usando os bastidores.
Tomamos o prezo do servidor máis potente dos que se xustificaban economicamente, calculamos cantos servidores deste tipo podíamos colocar en racks, cantas tarefas executaríamos neles en función do modelo antigo "un servidor = unha tarefa" e canto tal. tarefas poderían utilizar o equipo. Contaron e derramaron bágoas. Descubriuse que a nosa eficiencia no uso de bastidores é dun 11%. A conclusión é obvia: necesitamos aumentar a eficiencia do uso dos centros de datos. Parece que a solución é obvia: cómpre executar varias tarefas nun servidor á vez. Pero aquí é onde comezan as dificultades.

A configuración masiva faise drasticamente máis complicada: agora é imposible asignar un grupo a un servidor. Despois de todo, agora pódense lanzar varias tarefas de diferentes comandos nun servidor. Ademais, a configuración pode ser conflitiva para diferentes aplicacións. O diagnóstico tamén se fai máis complicado: se ves un aumento do consumo de CPU ou disco nun servidor, non sabes que tarefa está a causar problemas.

Pero o principal é que non hai illamento entre as tarefas que se executan na mesma máquina. Aquí, por exemplo, hai un gráfico do tempo medio de resposta dunha tarefa do servidor antes e despois de que se lanzase outra aplicación computacional no mesmo servidor, de ningún xeito relacionado coa primeira: o tempo de resposta da tarefa principal aumentou significativamente.

Sistema operativo a nivel de centro de datos dunha nube en Odnoklassniki

Obviamente, cómpre executar tarefas en contedores ou en máquinas virtuais. Dado que case todas as nosas tarefas execútanse nun SO (Linux) ou están adaptadas para iso, non necesitamos admitir moitos sistemas operativos diferentes. En consecuencia, a virtualización non é necesaria; debido á sobrecarga adicional, será menos eficiente que a contenerización.

Como implementación de contedores para executar tarefas directamente nos servidores, Docker é un bo candidato: as imaxes do sistema de ficheiros resolven ben os problemas con configuracións conflitivas. O feito de que as imaxes poidan estar compostas por varias capas permítenos reducir significativamente a cantidade de datos necesarios para implantalas na infraestrutura, separando as partes comúns en capas base separadas. Entón, as capas básicas (e máis voluminosas) almacenaranse na caché con bastante rapidez en toda a infraestrutura e, para ofrecer moitos tipos diferentes de aplicacións e versións, só haberá que transferir pequenas capas.

Ademais, un rexistro prefabricado e unha etiquetaxe de imaxes en Docker ofrécennos primitivos preparados para o versionado e entrega de código á produción.

Docker, como calquera outra tecnoloxía similar, ofrécenos un certo nivel de illamento de contedores fóra da caixa. Por exemplo, illamento da memoria: cada recipiente ten un límite no uso da memoria da máquina, máis aló do cal non se consumirá. Tamén pode illar os contedores en función do uso da CPU. Para nós, porén, o illamento estándar non era suficiente. Pero máis sobre iso a continuación.

A execución directa de contedores nos servidores é só parte do problema. A outra parte está relacionada co hospedaxe de contedores en servidores. Debe entender que contenedor se pode colocar en que servidor. Non é unha tarefa tan sinxela, porque os contedores deben colocarse nos servidores o máis densamente posible sen reducir a súa velocidade. Tal colocación tamén pode ser difícil desde o punto de vista da tolerancia a fallos. Moitas veces queremos colocar réplicas do mesmo servizo en diferentes racks ou mesmo en diferentes salas do centro de datos, de xeito que se falla un rack ou sala, non perdamos inmediatamente todas as réplicas do servizo.

Distribuír os contedores manualmente non é unha opción cando tes 8 mil servidores e 8-16 mil contedores.

Ademais, queriamos dar aos desenvolvedores máis independencia na asignación de recursos para que eles mesmos puidesen aloxar os seus servizos en produción, sen a axuda dun administrador. Ao mesmo tempo, queriamos manter o control para que algún servizo menor non consumise todos os recursos dos nosos centros de datos.

Obviamente, necesitamos unha capa de control que o faga automaticamente.

Así chegamos a unha imaxe sinxela e comprensible que adoran todos os arquitectos: tres prazas.

Sistema operativo a nivel de centro de datos dunha nube en Odnoklassniki

one-cloud masters é un clúster de conmutación por fallo responsable da orquestración na nube. O programador envía un manifesto ao mestre, que contén toda a información necesaria para aloxar o servizo. En base a el, o mestre dá ordes aos minions seleccionados (máquinas deseñadas para executar contedores). Os minions teñen o noso axente, que recibe o comando, envía os seus comandos a Docker e Docker configura o núcleo de Linux para lanzar o contedor correspondente. Ademais de executar comandos, o axente informa continuamente ao mestre sobre os cambios no estado tanto da máquina minion como dos contedores que se executan nela.

Asignación de recursos

Agora vexamos o problema da asignación de recursos máis complexa para moitos esbirros.

Un recurso informático nunha nube é:

  • A cantidade de enerxía do procesador consumida por unha tarefa específica.
  • A cantidade de memoria dispoñible para a tarefa.
  • Tráfico da rede. Cada un dos minions ten unha interface de rede específica cun ancho de banda limitado, polo que é imposible distribuír tarefas sen ter en conta a cantidade de datos que transmiten pola rede.
  • Discos. Ademais, obviamente, ao espazo para estas tarefas, tamén asignamos o tipo de disco: HDD ou SSD. Os discos poden atender un número finito de solicitudes por segundo: IOPS. Polo tanto, para as tarefas que xeran máis IOPS das que pode xestionar un só disco, tamén asignamos "spindles", é dicir, dispositivos de disco que deben reservarse exclusivamente para a tarefa.

Despois, para algún servizo, por exemplo para a caché do usuario, podemos rexistrar os recursos consumidos deste xeito: 400 núcleos de procesador, 2,5 TB de memoria, 50 Gbit/s de tráfico en ambas direccións, 6 TB de espazo no disco duro situado en 100 ejes. Ou nunha forma máis familiar coma esta:

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

Os recursos do servizo de caché do usuario só consomen unha parte de todos os recursos dispoñibles na infraestrutura de produción. Polo tanto, quero asegurarme de que de súpeto, debido a un erro do operador ou non, a caché do usuario non consume máis recursos dos que lle están asignados. É dicir, debemos limitar os recursos. Pero a que poderiamos atar a cota?

Volvamos ao noso diagrama moi simplificado da interacción dos compoñentes e redibuxámolo con máis detalles, como este:

Sistema operativo a nivel de centro de datos dunha nube en Odnoklassniki

O que chama a atención:

  • A interface web e a música usan clústeres illados do mesmo servidor de aplicacións.
  • Podemos distinguir as capas lóxicas ás que pertencen estes clusters: frontes, cachés, almacenamento de datos e capa de xestión.
  • O frontend é heteroxéneo; consta de diferentes subsistemas funcionais.
  • Os cachés tamén se poden espallar polo subsistema cuxos datos almacenan na caché.

Volvemos a debuxar a imaxe:

Sistema operativo a nivel de centro de datos dunha nube en Odnoklassniki

Bah! Si, vemos unha xerarquía! Isto significa que pode distribuír os recursos en anacos máis grandes: asignar un desenvolvedor responsable a un nodo desta xerarquía correspondente ao subsistema funcional (como "música" na imaxe) e engadir unha cota ao mesmo nivel da xerarquía. Esta xerarquía tamén nos permite organizar os servizos de forma máis flexible para facilitar a xestión. Por exemplo, dividimos toda a web, xa que se trata dunha agrupación moi grande de servidores, en varios grupos máis pequenos, mostrados na imaxe como grupo1, grupo2.

Ao eliminar as liñas adicionais, podemos escribir cada nodo da nosa imaxe nunha forma máis plana: grupo1.web.front, api.music.front, user-cache.cache.

Así é como chegamos ao concepto de "cola xerárquica". Ten un nome como "group1.web.front". Asígnaselle unha cota de recursos e dereitos de usuario. Daremos á persoa de DevOps os dereitos para enviar un servizo á cola, e un empregado deste tipo pode lanzar algo na cola, e a persoa de OpsDev terá dereitos de administrador e agora pode xestionar a cola, asignar persoas alí, darlle dereitos a estas persoas, etc. Os servizos que se executan nesta cola executaranse dentro da cota da cola. Se a cota de cálculo da cola non é suficiente para executar todos os servizos á vez, executaranse secuencialmente, formando así a propia cola.

Vexamos máis de cerca os servizos. Un servizo ten un nome totalmente cualificado, que sempre inclúe o nome da cola. A continuación, o servizo web frontal terá o nome ok-web.group1.web.front. E chamarase ao servizo do servidor de aplicacións ao que accede ok-app.group1.web.front. Cada servizo ten un manifesto, no que se especifica toda a información necesaria para a súa colocación en máquinas específicas: cantos recursos consume esta tarefa, que configuración é necesaria para ela, cantas réplicas debería haber, propiedades para xestionar fallos deste servizo. E despois de colocar o servizo directamente nas máquinas, aparecen as súas instancias. Tamén se nomean sen ambigüidades, como o número de instancia e o nome do servizo: 1.ok-web.group1.web.front, 2.ok-web.group1.web.front, …

Isto é moi cómodo: mirando só o nome do contedor en execución, podemos descubrir de inmediato moitas cousas.

Agora vexamos máis de cerca o que realmente realizan estas instancias: tarefas.

Clases de illamento de tarefas

Todas as tarefas en OK (e, probablemente, en todas partes) pódense dividir en grupos:

  • Tarefas de latencia curta - prod. Para tales tarefas e servizos, o atraso de resposta (latencia) é moi importante, a rapidez con que o sistema procesará cada unha das solicitudes. Exemplos de tarefas: frontes web, cachés, servidores de aplicacións, almacenamento OLTP, etc.
  • Problemas de cálculo - lote. Aquí, a velocidade de procesamento de cada solicitude específica non é importante. Para eles, é importante cantos cálculos fará esta tarefa nun (longo) período de tempo (rendemento). Estas serán as tarefas de MapReduce, Hadoop, aprendizaxe automática, estatísticas.
  • Tarefas en segundo plano: inactivo. Para tales tarefas, nin a latencia nin o rendemento son moi importantes. Isto inclúe varias probas, migracións, recálculos e conversión de datos dun formato a outro. Por unha banda, son similares aos calculados, por outra banda, non nos importa moito a rapidez con que se completan.

Vexamos como este tipo de tarefas consomen recursos, por exemplo, o procesador central.

Tarefas de curto atraso. Tal tarefa terá un patrón de consumo de CPU similar a este:

Sistema operativo a nivel de centro de datos dunha nube en Odnoklassniki

Recíbese unha solicitude do usuario para o seu procesamento, a tarefa comeza a utilizar todos os núcleos de CPU dispoñibles, procesa, devolve unha resposta, espera a seguinte solicitude e detense. Chegou a seguinte solicitude: de novo escollemos todo o que había alí, calculámolo e estamos esperando a seguinte.

Para garantir a latencia mínima para tal tarefa, debemos tomar os máximos recursos que consome e reservar o número necesario de núcleos no minion (a máquina que executará a tarefa). Entón, a fórmula de reserva para o noso problema será a seguinte:

alloc: cpu = 4 (max)

e se temos unha máquina minion con 16 núcleos, pódense colocar nela exactamente catro tarefas deste tipo. Temos en conta especialmente que o consumo medio do procesador destas tarefas adoita ser moi baixo, o que é obvio, xa que unha parte importante do tempo a tarefa agarda por unha solicitude e non fai nada.

Tarefas de cálculo. O seu patrón será lixeiramente diferente:

Sistema operativo a nivel de centro de datos dunha nube en Odnoklassniki

O consumo medio de recursos da CPU para tales tarefas é bastante alto. Moitas veces queremos que unha tarefa de cálculo se complete nun período de tempo determinado, polo que necesitamos reservar o número mínimo de procesadores que necesita para que todo o cálculo se complete nun tempo aceptable. A súa fórmula de reserva será así:

alloc: cpu = [1,*)

"Por favor, colócao nun esbirro onde haxa polo menos un núcleo libre e, a continuación, tantos como haxa, devorará todo".

Aquí a eficiencia de uso xa é moito mellor que nas tarefas con pouco atraso. Pero a ganancia será moito maior se combinas ambos tipos de tarefas nunha máquina de esbirros e distribúes os seus recursos en calquera lugar. Cando unha tarefa cun pequeno atraso require un procesador, este recíbeo inmediatamente e, cando os recursos xa non son necesarios, transfírense á tarefa computacional, é dicir, algo así:

Sistema operativo a nivel de centro de datos dunha nube en Odnoklassniki

Pero como facelo?

Primeiro, vexamos prod e o seu alloc: cpu = 4. Necesitamos reservar catro núcleos. Na execución de Docker, isto pódese facer de dúas formas:

  • Usando a opción --cpuset=1-4, é dicir, asignar catro núcleos específicos da máquina á tarefa.
  • Usa --cpuquota=400_000 --cpuperiod=100_000, asigne unha cota para o tempo do procesador, é dicir, indique que cada 100 ms de tempo real a tarefa non consume máis de 400 ms de tempo do procesador. Obtéñense os mesmos catro núcleos.

Pero cal destes métodos é axeitado?

cpuset parece bastante atractivo. A tarefa ten catro núcleos dedicados, o que significa que as cachés do procesador funcionarán da forma máis eficiente posible. Isto tamén ten unha desvantaxe: teriamos que asumir a tarefa de distribuír os cálculos polos núcleos descargados da máquina en lugar do sistema operativo, e esta é unha tarefa pouco trivial, especialmente se tentamos colocar tarefas por lotes nun máquina. As probas demostraron que aquí é máis axeitada a opción con cota: deste xeito o sistema operativo ten máis liberdade á hora de escoller o núcleo para realizar a tarefa no momento actual e o tempo do procesador distribúese de forma máis eficiente.

Imos descubrir como facer reservas en Docker en función do número mínimo de núcleos. A cota para tarefas por lotes xa non é aplicable, porque non hai que limitar o máximo, abonda con garantir só o mínimo. E aquí a opción encaixa ben docker run --cpushares.

Acordamos que se un lote require unha garantía para polo menos un núcleo, indicamos --cpushares=1024, e se hai polo menos dous núcleos, indicamos --cpushares=2048. As accións da CPU non interfiren de ningún xeito coa distribución do tempo do procesador sempre que haxa suficiente. Así, se prod non está usando actualmente os seus catro núcleos, non hai nada que limite as tarefas por lotes e poden usar tempo adicional do procesador. Pero nunha situación na que hai escaseza de procesadores, se prod consumiu os catro núcleos e alcanzou a súa cota, o tempo restante do procesador dividirase proporcionalmente aos cpushares, é dicir, nunha situación de tres núcleos libres, un será un. asignado a unha tarefa con 1024 cpushares, e os dous restantes asignaranse a unha tarefa con 2048 cpushares.

Pero usar cota e participacións non é suficiente. Debemos asegurarnos de que unha tarefa cun pequeno atraso teña prioridade sobre unha tarefa por lotes ao asignar o tempo do procesador. Sen tal prioridade, a tarefa por lotes ocupará todo o tempo do procesador no momento en que o produto o necesite. Non hai opcións de priorización de contedores na execución de Docker, pero as políticas do programador de CPU de Linux son útiles. Podes ler sobre eles en detalle aquí, e no marco deste artigo repasarémolos brevemente:

  • SCHED_OTHER
    Por defecto, todos os procesos normais de usuario nunha máquina Linux reciben.
  • SCHED_BATCH
    Deseñado para procesos de uso intensivo de recursos. Ao colocar unha tarefa nun procesador, introdúcese a chamada penalización de activación: é menos probable que esa tarefa reciba recursos do procesador se está a utilizar actualmente unha tarefa con SCHED_OTHER
  • SCHED_IDLE
    Un proceso en segundo plano cunha prioridade moi baixa, incluso inferior ao agradable -19. Usamos a nosa biblioteca de código aberto un-nio, para establecer a política necesaria ao iniciar o contedor chamando

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

Pero aínda que non programes en Java, pódese facer o mesmo usando o comando chrt:

chrt -i 0 $pid

Imos resumir todos os nosos niveis de illamento nunha táboa para aclarar:

Clase de illamento
Exemplo de alloc
Opcións de execución de Docker
sched_setscheduler chrt*

Incitar
CPU = 4
--cpuquota=400000 --cpuperiod=100000
SCHED_OTHER

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

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

*Se estás facendo chrt desde dentro dun contedor, pode que necesites a capacidade sys_nice, porque por defecto Docker elimina esta capacidade ao iniciar o contenedor.

Pero as tarefas non só consumen o procesador, senón tamén o tráfico, o que afecta a latencia dunha tarefa de rede aínda máis que a asignación incorrecta dos recursos do procesador. Polo tanto, naturalmente queremos obter exactamente a mesma imaxe para o tráfico. É dicir, cando unha tarefa de prod envía algúns paquetes á rede, limitamos a velocidade máxima (fórmula alloc: lan=[*,500mbps) ), co que prod pode facelo. E para o lote, garantimos só o rendemento mínimo, pero non limitamos o máximo (fórmula alloc: lan=[10 Mbps,*) ) Neste caso, o tráfico de produtos debería ter prioridade sobre as tarefas por lotes.
Aquí Docker non ten ningún primitivo que poidamos usar. Pero vén na nosa axuda Control de tráfico Linux. Fomos capaces de acadar o resultado desexado coa axuda da disciplina Curva xerárquica do servizo xusto. Coa súa axuda, distinguimos dúas clases de tráfico: prod de alta prioridade e lote/inactivo de baixa prioridade. Como resultado, a configuración para o tráfico de saída é a seguinte:

Sistema operativo a nivel de centro de datos dunha nube en Odnoklassniki

aquí 1:0 é o "qdisc raíz" da disciplina hsfc; 1:1: clase secundaria hsfc cun límite de ancho de banda total de 8 Gbit/s, baixo o cal se sitúan as clases fillas de todos os contedores; 1:2: a clase filla hsfc é común a todas as tarefas por lotes e inactivas cun límite "dinámico", que se explica a continuación. As restantes clases fillas hsfc son clases dedicadas para os contedores de produtos en execución actualmente con límites correspondentes aos seus manifestos: 450 e 400 Mbit/s. A cada clase hsfc asígnaselle unha cola qdisc fq ou fq_codel, dependendo da versión do núcleo de Linux, para evitar a perda de paquetes durante as ráfagas de tráfico.

Normalmente, as disciplinas tc serven para priorizar só o tráfico de saída. Pero tamén queremos priorizar o tráfico de entrada; despois de todo, algunhas tarefas por lotes poden seleccionar facilmente toda a canle de entrada, recibindo, por exemplo, un gran lote de datos de entrada para o mapa e redución. Para iso utilizamos o módulo ifb, que crea unha interface virtual ifbX para cada interface de rede e redirixe o tráfico entrante desde a interface ao tráfico saínte en ifbX. Ademais, para ifbX, todas as mesmas disciplinas traballan para controlar o tráfico de saída, para o que a configuración de hsfc será moi similar:

Sistema operativo a nivel de centro de datos dunha nube en Odnoklassniki

Durante os experimentos, descubrimos que hsfc mostra os mellores resultados cando a clase 1:2 de tráfico por lotes/inactivo non prioritario está limitada en máquinas minion a non máis que un determinado carril libre. Se non, o tráfico non prioritario ten demasiado impacto na latencia das tarefas de prod. miniond determina a cantidade actual de ancho de banda libre cada segundo, medindo o consumo medio de tráfico de todas as tarefas de prod dun determinado minion Sistema operativo a nivel de centro de datos dunha nube en Odnoklassniki e restándoo do ancho de banda da interface de rede Sistema operativo a nivel de centro de datos dunha nube en Odnoklassniki cunha pequena marxe, é dicir.

Sistema operativo a nivel de centro de datos dunha nube en Odnoklassniki

As bandas defínense independentemente para o tráfico entrante e saínte. E segundo os novos valores, miniond reconfigura o límite de clase non prioritaria 1:2.

Así, implementamos as tres clases de illamento: prod, batch e idle. Estas clases inflúen moito nas características de rendemento das tarefas. Por iso, decidimos situar este atributo na parte superior da xerarquía, de xeito que ao mirar o nome da cola xerárquica quedara inmediatamente claro de que estamos a tratar:

Sistema operativo a nivel de centro de datos dunha nube en Odnoklassniki

Todos os nosos amigos tea и música as frontes colócanse entón na xerarquía baixo prod. Por exemplo, en lote, coloquemos o servizo catálogo musical, que compila periodicamente un catálogo de temas a partir dun conxunto de ficheiros mp3 cargados en Odnoklassniki. Un exemplo de servizo en inactividade sería transformador musical, que normaliza o nivel de volume da música.

Con as liñas adicionais eliminadas de novo, podemos escribir os nosos nomes de servizo máis liso engadindo a clase de illamento de tarefas ao final do nome completo do servizo: web.front.prod, catálogo.música.lote, transformador.música.inactivo.

E agora, mirando o nome do servizo, entendemos non só a función que realiza, senón tamén a súa clase de illamento, o que significa a súa criticidade, etc.

Todo é xenial, pero hai unha amarga verdade. É imposible illar completamente as tarefas que se executan nunha máquina.

O que conseguimos: se o lote consome intensamente recursos da CPU, entón o programador de CPU de Linux integrado fai o seu traballo moi ben e practicamente non hai ningún impacto na tarefa de produción. Pero se esta tarefa por lotes comeza a traballar activamente coa memoria, entón xa aparece a influencia mutua. Isto ocorre porque a tarefa de produción é "lavada" das cachés de memoria do procesador; como resultado, os fallos de caché aumentan e o procesador procesa a tarefa de produción máis lentamente. Tal tarefa por lotes pode aumentar a latencia do noso típico recipiente de produtos nun 10%.

Illar o tráfico é aínda máis difícil debido ao feito de que as tarxetas de rede modernas teñen unha cola interna de paquetes. Se o paquete da tarefa por lotes chega primeiro, entón será o primeiro en transmitirse polo cable e non se pode facer nada ao respecto.

Ademais, ata agora só conseguimos resolver o problema de priorizar o tráfico TCP: o enfoque hsfc non funciona para UDP. E mesmo no caso do tráfico TCP, se a tarefa por lotes xera moito tráfico, isto tamén dá un aumento de aproximadamente un 10% no atraso da tarefa de prod.

tolerancia a fallos

Un dos obxectivos ao desenvolver unha nube era mellorar a tolerancia ás fallas de Odnoklassniki. Por iso, a continuación gustaríame considerar con máis detalle posibles escenarios de fallos e accidentes. Comecemos cun escenario sinxelo: un fallo do contedor.

O propio recipiente pode fallar de varias maneiras. Pode tratarse dalgún tipo de experimento, erro ou erro no manifesto, polo que a tarefa de produción comeza a consumir máis recursos dos que se indican no manifesto. Tivemos un caso: un desenvolvedor implementou un algoritmo complexo, reelaborouno moitas veces, pensouse a si mesmo e quedou tan confuso que finalmente o problema entrou nun bucle moi non trivial. E dado que a tarefa de produción ten unha prioridade máis alta que todas as demais nos mesmos secuaces, comezou a consumir todos os recursos dispoñibles do procesador. Nesta situación, o illamento, ou máis ben a cota de tempo da CPU, salvou o día. Se a unha tarefa se lle asigna unha cota, a tarefa non consumirá máis. Polo tanto, as tarefas por lotes e outros produtos que se executaban na mesma máquina non notaron nada.

O segundo problema posible é a caída do recipiente. E aquí as políticas de reinicio sálvanos, todo o mundo as coñece, o propio Docker fai un gran traballo. Case todas as tarefas de produción teñen unha política de reinicio sempre. Ás veces usamos on_failure para tarefas por lotes ou para depurar contedores de produtos.

Que podes facer se un esbirro enteiro non está dispoñible?

Obviamente, executa o contedor noutra máquina. A parte interesante aquí é o que ocorre cos enderezos IP asignados ao contedor.

Podemos asignar aos contedores os mesmos enderezos IP que as máquinas minion nas que se executan estes contedores. Entón, cando o contedor se lanza noutra máquina, o seu enderezo IP cambia e todos os clientes deben entender que o contedor se moveu e agora deben ir a un enderezo diferente, o que require un servizo de descubrimento de servizos separado.

Service Discovery é conveniente. Existen moitas solucións no mercado de diferentes graos de tolerancia a fallos para organizar un rexistro de servizos. Moitas veces, tales solucións implementan a lóxica de balance de carga, almacenan configuración adicional en forma de almacenamento KV, etc.
Non obstante, queremos evitar a necesidade de implementar un rexistro separado, porque isto suporía introducir un sistema crítico que sexa utilizado por todos os servizos en produción. Isto significa que este é un posible punto de fallo e cómpre escoller ou desenvolver unha solución moi tolerante a fallos, que obviamente é moi difícil, lenta e custosa.

E un gran inconveniente máis: para que a nosa antiga infraestrutura funcione coa nova, teriamos que reescribir absolutamente todas as tarefas para usar algún tipo de sistema de descubrimento de servizos. Hai moito traballo, e nalgúns lugares é case imposible cando se trata de dispositivos de baixo nivel que funcionan a nivel do núcleo do sistema operativo ou directamente co hardware. Implementación desta funcionalidade utilizando patróns de solución establecidos, como sidecar significaría nalgúns lugares unha carga adicional, noutros - unha complicación da operación e escenarios de falla adicionais. Non queriamos complicar as cousas, polo que decidimos facer opcional o uso de Service Discovery.

Nunha nube, a IP segue o contedor, é dicir, cada instancia de tarefa ten o seu propio enderezo IP. Este enderezo é "estático": asígnase a cada instancia cando o servizo se envía por primeira vez á nube. Se un servizo tivo un número diferente de instancias durante a súa vida útil, ao final asignaranse tantos enderezos IP como máximo de instancias.

Posteriormente, estes enderezos non cambian: asígnanse unha vez e continúan existindo durante toda a vida do servizo en produción. Os enderezos IP seguen os contedores na rede. Se o contedor transfírese a outro esbirro, o enderezo seguirao.

Así, a asignación do nome dun servizo á súa lista de enderezos IP cambia moi raramente. Se miras de novo os nomes das instancias de servizo que mencionamos ao comezo do artigo (1.ok-web.group1.web.front.prod, 2.ok-web.group1.web.front.prod, …), notaremos que se asemellan aos FQDN usados ​​no DNS. É certo, para asignar os nomes das instancias de servizo aos seus enderezos IP, usamos o protocolo DNS. Ademais, este DNS devolve todos os enderezos IP reservados de todos os contedores, tanto en execución como parados (digamos que se usan tres réplicas e temos cinco enderezos reservados alí; os cinco serán devoltos). Os clientes, despois de recibir esta información, tentarán establecer unha conexión coas cinco réplicas e así determinar as que están funcionando. Esta opción para determinar a dispoñibilidade é moito máis fiable, non implica nin DNS nin Service Discovery, o que significa que non hai problemas difíciles de resolver para garantir a relevancia da información e a tolerancia a fallos destes sistemas. Ademais, nos servizos críticos dos que depende o funcionamento de todo o portal, non podemos usar DNS en absoluto, senón simplemente introducir enderezos IP na configuración.

Implementar tal transferencia de IP detrás dos contedores pode ser nada trivial, e veremos como funciona co seguinte exemplo:

Sistema operativo a nivel de centro de datos dunha nube en Odnoklassniki

Digamos que o mestre dunha nube dá o comando ao minion M1 para que se execute 1.ok-web.group1.web.front.prod co enderezo 1.1.1.1. Traballa nun esbirro OBRADOIRO, que anuncia este enderezo a servidores especiais reflector de ruta. Estes últimos teñen unha sesión BGP co hardware da rede, na que se traduce a ruta do enderezo 1.1.1.1 en M1. M1 envía paquetes dentro do contedor usando Linux. Hai tres servidores reflectores de ruta, xa que esta é unha parte moi crítica da infraestrutura dunha soa nube; sen eles, a rede nunha soa nube non funcionará. Colocámolos en diferentes racks, se é posible situados en diferentes salas do centro de datos, para reducir a probabilidade de que os tres fallen ao mesmo tempo.

Supoñamos agora que se perde a conexión entre o mestre dunha nube e o esbirro M1. O mestre dunha nube actuará agora asumindo que M1 fallou completamente. É dicir, dará o mando ao esbirro M2 para que se lance web.group1.web.front.prod co mesmo enderezo 1.1.1.1. Agora temos dúas rutas en conflito na rede para 1.1.1.1: na M1 e na M2. Para resolver tales conflitos, utilizamos o Discriminador de saída múltiple, que se especifica no anuncio de BGP. Este é un número que mostra o peso da ruta anunciada. Entre as rutas en conflito, seleccionarase a ruta co valor MED máis baixo. O mestre dunha soa nube admite MED como parte integrante dos enderezos IP do contedor. Por primeira vez, o enderezo escríbese cun MED suficientemente grande = 1 000 000. Na situación de tal transferencia de contedores de emerxencia, o mestre reduce o MED e M2 xa recibirá o comando para anunciar o enderezo 1.1.1.1 con MED = 999 999. A instancia que se executa na M1 permanecerá neste caso, non hai conexión, e o seu destino posterior non nos interesa ata que se restableza a conexión co mestre, cando será detido como unha vella toma.

accidentes

Todos os sistemas de xestión do centro de datos sempre xestionan fallas menores de forma aceptable. O desbordamento do contedor é a norma en case todas partes.

Vexamos como xestionamos unha emerxencia, como unha falla de enerxía nunha ou máis salas dun centro de datos.

Que significa un accidente para un sistema de xestión de centros de datos? En primeiro lugar, trátase dun fallo único e masivo de moitas máquinas e o sistema de control necesita migrar moitos contedores ao mesmo tempo. Pero se o desastre é a gran escala, pode ocorrer que todas as tarefas non se poidan reasignar a outros secuaces, porque a capacidade de recursos do centro de datos cae por debaixo do 100% da carga.

Moitas veces os accidentes van acompañados de fallas da capa de control. Isto pode ocorrer debido ao fallo do seu equipo, pero con máis frecuencia debido ao feito de que os accidentes non se proban e a propia capa de control cae debido ao aumento da carga.

Que podes facer con todo isto?

As migracións masivas significan que hai un gran número de actividades, migracións e despregamentos na infraestrutura. Cada unha das migracións pode levar algún tempo necesario para entregar e desempaquetar imaxes de contedores aos minions, lanzar e inicializar contedores, etc. Polo tanto, é desexable que as tarefas máis importantes se lancen antes que as menos importantes.

Vexamos de novo a xerarquía de servizos que coñecemos e intentemos decidir cales son as tarefas que queremos executar primeiro.

Sistema operativo a nivel de centro de datos dunha nube en Odnoklassniki

Por suposto, estes son os procesos que interveñen directamente no procesamento das solicitudes dos usuarios, é dicir, prod. Indicamos isto con prioridade de colocación — un número que se pode asignar á cola. Se unha cola ten unha prioridade máis alta, os seus servizos colócanse primeiro.

En prod asignamos prioridades máis altas, 0; por lote - un pouco máis baixo, 100; en inactividade - aínda máis baixo, 200. As prioridades aplícanse xerarquicamente. Todas as tarefas inferiores na xerarquía terán a correspondente prioridade. Se queremos que as cachés dentro de prod se inicien antes das frontends, entón asignamos prioridades á caché = 0 e ás subcolas frontales = 1. Se, por exemplo, queremos que o portal principal se inicie primeiro dende as frontes e só a frontal da música. entón, podemos asignarlle unha prioridade máis baixa a este último: 10.

O seguinte problema é a falta de recursos. Entón, fallou unha gran cantidade de equipos, salas enteiras do centro de datos, e relanzamos tantos servizos que agora non hai recursos suficientes para todos. Debes decidir que tarefas sacrificar para manter os principais servizos críticos funcionando.

Sistema operativo a nivel de centro de datos dunha nube en Odnoklassniki

A diferenza da prioridade de colocación, non podemos sacrificar indistintamente todas as tarefas por lotes; algunhas delas son importantes para o funcionamento do portal. Polo tanto, destacamos por separado prioridade de preferencia tarefas. Cando se coloca, unha tarefa de maior prioridade pode anticiparse, é dicir, deter, unha tarefa de menor prioridade se non hai máis esbirros libres. Neste caso, unha tarefa cunha baixa prioridade probablemente permanecerá sen colocar, é dicir, xa non haberá un esbirro axeitado para ela con suficientes recursos gratuítos.

Na nosa xerarquía, é moi sinxelo especificar unha prioridade de preferencia de xeito que as tarefas de prod e batch se adelantan ou deteñan as tarefas inactivas, pero non entre si, especificando unha prioridade para inactiva igual a 200. Igual que no caso da prioridade de colocación, podemos usar a nosa xerarquía para describir regras máis complexas. Por exemplo, imos indicar que sacrificamos a función de música se non dispoñemos de recursos suficientes para o portal web principal, establecendo a prioridade para os nodos correspondentes máis baixa: 10.

Accidentes de DC enteiros

Por que pode fallar todo o centro de datos? Elemento. Foi un bo post o furacán afectou o traballo do centro de datos. Os elementos poden considerarse persoas sen fogar que unha vez queimaron a óptica do colector e o centro de datos perdeu por completo o contacto con outros sitios. A causa do fallo tamén pode ser un factor humano: o operador emitirá un comando tal que todo o centro de datos caerá. Isto pode ocorrer debido a un gran erro. En xeral, o colapso dos centros de datos non é raro. Isto pásanos unha vez cada poucos meses.

E isto é o que facemos para evitar que ninguén tuitee #alive.

A primeira estratexia é o illamento. Cada instancia dunha soa nube está illada e só pode xestionar máquinas nun centro de datos. É dicir, a perda dunha nube debido a erros ou comandos incorrectos do operador é a perda dun só centro de datos. Estamos preparados para iso: temos unha política de redundancia na que as réplicas da aplicación e dos datos se sitúan en todos os centros de datos. Usamos bases de datos tolerantes a fallos e probamos periódicamente os fallos.
Xa que hoxe temos catro centros de datos, isto significa catro instancias separadas e completamente illadas dunha soa nube.

Este enfoque non só protexe contra fallos físicos, senón que tamén pode protexer contra erros do operador.

Que máis se pode facer co factor humano? Cando un operador dá á nube algún comando estraño ou potencialmente perigoso, de súpeto pódeselle pedir que resolva un pequeno problema para ver o ben que pensaba. Por exemplo, se se trata dun tipo de parada masiva de moitas réplicas ou só dun comando estraño, reducir o número de réplicas ou cambiar o nome da imaxe e non só o número de versión do novo manifesto.

Sistema operativo a nivel de centro de datos dunha nube en Odnoklassniki

Resultados de

Características distintivas de one-cloud:

  • Esquema de nomenclatura xerárquica e visual de servizos e contedores, que permite coñecer moi rapidamente cal é a tarefa, con que se relaciona e como funciona e quen é o responsable da mesma.
  • Aplicamos o noso técnica de combinación de produtos e lotestarefas en minions para mellorar a eficiencia do uso compartido de máquinas. En lugar de cpuset usamos cotas de CPU, recursos compartidos, políticas de planificador de CPU e QoS de Linux.
  • Non foi posible illar completamente os contedores que funcionan na mesma máquina, pero a súa influencia mutua mantense dentro do 20%.
  • A organización dos servizos nunha xerarquía axuda a utilizar a recuperación automática ante desastres prioridades de colocación e preferencia.

FAQ

Por que non tomamos unha solución xa preparada?

  • As diferentes clases de illamento de tarefas requiren unha lóxica diferente cando se colocan en minions. Se as tarefas de produción pódense colocar simplemente reservando recursos, entón hai que colocar tarefas por lotes e inactivas, facendo un seguimento da utilización real dos recursos nas máquinas minion.
  • A necesidade de ter en conta os recursos consumidos polas tarefas, como:
    • ancho de banda da rede;
    • tipos e “fusos” de discos.
  • A necesidade de indicar as prioridades dos servizos durante a resposta ás emerxencias, os dereitos e cotas de comandos para os recursos, que se soluciona mediante colas xerárquicas nunha soa nube.
  • A necesidade de contar con nomeamento humano dos contedores para reducir o tempo de resposta ante accidentes e incidentes
  • A imposibilidade dunha implementación única e xeneralizada de Service Discovery; a necesidade de coexistir durante moito tempo con tarefas aloxadas en hosts de hardware, algo que se soluciona mediante enderezos IP "estáticos" que seguen contenedores e, como consecuencia, a necesidade dunha integración única cunha gran infraestrutura de rede.

Todas estas funcións requirirían modificacións importantes das solucións existentes para adaptarnos a nós e, unha vez avaliada a cantidade de traballo, decatámonos de que podíamos desenvolver a nosa propia solución con aproximadamente os mesmos custos laborais. Pero a súa solución será moito máis fácil de operar e desenvolver: non contén abstraccións innecesarias que admitan unha funcionalidade que non necesitamos.

Aos que ledes as últimas liñas, grazas pola vosa paciencia e atención!

Fonte: www.habr.com

Engadir un comentario