One-cloud - sistema operacional no nível do data center em Odnoklassniki

One-cloud - sistema operacional no nível do data center em Odnoklassniki

Alô, pessoal! Meu nome é Oleg Anastasyev, trabalho na Odnoklassniki na equipe da Plataforma. E além de mim, há muito hardware funcionando em Odnoklassniki. Possuímos quatro data centers com cerca de 500 racks com mais de 8 mil servidores. A certa altura, percebemos que a introdução de um novo sistema de gestão permitiria carregar equipamentos de forma mais eficiente, facilitar a gestão de acessos, automatizar a (re)distribuição de recursos informáticos, acelerar o lançamento de novos serviços e agilizar as respostas. a acidentes de grande escala.

O que aconteceu?

Além de mim e de um monte de hardware, também tem gente que trabalha com esse hardware: engenheiros que ficam diretamente em data centers; networkers que configuram software de rede; administradores, ou SREs, que fornecem resiliência de infraestrutura; e equipes de desenvolvimento, cada uma delas é responsável por parte das funções do portal. O software que eles criam funciona mais ou menos assim:

One-cloud - sistema operacional no nível do data center em Odnoklassniki

As solicitações dos usuários são recebidas tanto nas frentes do portal principal www.ok.rue em outros, por exemplo, nas frentes da API de música. Para processar a lógica de negócio, eles chamam o servidor de aplicação, que, ao processar a solicitação, chama os microsserviços especializados necessários - one-graph (gráfico de conexões sociais), user-cache (cache de perfis de usuário), etc.

Cada um desses serviços é implantado em diversas máquinas, e cada uma delas conta com desenvolvedores responsáveis ​​​​pelo funcionamento dos módulos, seu funcionamento e desenvolvimento tecnológico. Todos esses serviços rodam em servidores de hardware, e até recentemente lançávamos exatamente uma tarefa por servidor, ou seja, era especializado para uma tarefa específica.

Por que é que? Essa abordagem teve várias vantagens:

  • Aliviado gestão de massa. Digamos que uma tarefa exija algumas bibliotecas, algumas configurações. E então o servidor é atribuído a exatamente um grupo específico, a política cfengine para esse grupo é descrita (ou já foi descrita) e essa configuração é implementada de forma central e automática para todos os servidores desse grupo.
  • Simplificado diagnósticos. Digamos que você observe o aumento da carga no processador central e perceba que essa carga só poderia ser gerada pela tarefa executada neste processador de hardware. A busca por alguém para culpar termina muito rapidamente.
  • Simplificado monitoramento. Se algo estiver errado com o servidor, o monitor reportará e você saberá exatamente quem é o culpado.

Um serviço que consiste em várias réplicas recebe vários servidores - um para cada. Então o recurso computacional para o serviço é alocado de forma muito simples: o número de servidores que o serviço possui, a quantidade máxima de recursos que ele pode consumir. “Fácil” aqui não significa que seja fácil de usar, mas sim no sentido de que a alocação de recursos é feita manualmente.

Esta abordagem também nos permitiu fazer configurações de ferro especializadas para uma tarefa em execução neste servidor. Se a tarefa armazenar grandes quantidades de dados, utilizamos um servidor 4U com chassi com 38 discos. Se a tarefa for puramente computacional, podemos comprar um servidor 1U mais barato. Isso é computacionalmente eficiente. Entre outras coisas, esta abordagem permite-nos utilizar quatro vezes menos máquinas com uma carga comparável a uma rede social amigável.

Tal eficiência na utilização dos recursos computacionais deverá garantir também a eficiência económica, se partirmos da premissa de que o mais caro são os servidores. Durante muito tempo, o hardware foi o mais caro, e nos esforçamos muito para reduzir o preço do hardware, criando algoritmos de tolerância a falhas para reduzir os requisitos de confiabilidade do hardware. E hoje chegamos ao ponto em que o preço do servidor deixou de ser decisivo. Se você não considerar os exóticos mais recentes, a configuração específica dos servidores no rack não importa. Agora temos outro problema - o preço do espaço ocupado pelo servidor no data center, ou seja, o espaço no rack.

Percebendo que era esse o caso, decidimos calcular a eficácia com que estávamos usando os racks.
Pegamos o preço do servidor mais poderoso daqueles economicamente justificáveis, calculamos quantos desses servidores poderíamos colocar em racks, quantas tarefas executaríamos neles com base no antigo modelo “um servidor = uma tarefa” e quanto tal tarefas poderiam utilizar o equipamento. Eles contaram e derramaram lágrimas. Descobrimos que nossa eficiência no uso de racks é de cerca de 11%. A conclusão é óbvia: precisamos aumentar a eficiência do uso dos data centers. Parece que a solução é óbvia: você precisa executar várias tarefas em um servidor ao mesmo tempo. Mas é aqui que começam as dificuldades.

A configuração em massa torna-se dramaticamente mais complicada - agora é impossível atribuir qualquer grupo a um servidor. Afinal, agora várias tarefas de diferentes comandos podem ser executadas em um servidor. Além disso, a configuração pode ser conflitante para diferentes aplicativos. O diagnóstico também se torna mais complicado: se você observar um aumento no consumo de CPU ou de disco em um servidor, não saberá qual tarefa está causando problemas.

Mas o principal é que não existe isolamento entre tarefas executadas na mesma máquina. Aqui, por exemplo, está um gráfico do tempo médio de resposta de uma tarefa do servidor antes e depois de outro aplicativo computacional ser lançado no mesmo servidor, de forma alguma relacionado ao primeiro - o tempo de resposta da tarefa principal aumentou significativamente.

One-cloud - sistema operacional no nível do data center em Odnoklassniki

Obviamente, você precisa executar tarefas em contêineres ou em máquinas virtuais. Como quase todas as nossas tarefas são executadas em um sistema operacional (Linux) ou adaptadas para ele, não precisamos oferecer suporte a muitos sistemas operacionais diferentes. Conseqüentemente, a virtualização não é necessária; devido à sobrecarga adicional, será menos eficiente que a conteinerização.

Como implementação de contêineres para execução de tarefas diretamente nos servidores, o Docker é um bom candidato: imagens do sistema de arquivos resolvem bem problemas com configurações conflitantes. O fato de as imagens poderem ser compostas por várias camadas nos permite reduzir significativamente a quantidade de dados necessários para implantá-las na infraestrutura, separando as partes comuns em camadas base separadas. Então, as camadas básicas (e mais volumosas) serão armazenadas em cache com bastante rapidez em toda a infraestrutura e, para fornecer muitos tipos diferentes de aplicativos e versões, apenas pequenas camadas precisarão ser transferidas.

Além disso, um registro pronto e marcação de imagem no Docker nos fornecem primitivos prontos para controle de versão e entrega de código para produção.

O Docker, como qualquer outra tecnologia semelhante, nos fornece algum nível de isolamento de contêiner pronto para uso. Por exemplo, isolamento de memória - cada contêiner recebe um limite de uso de memória da máquina, além do qual não consumirá. Você também pode isolar contêineres com base no uso da CPU. Para nós, porém, o isolamento padrão não foi suficiente. Mas mais sobre isso abaixo.

A execução direta de contêineres em servidores é apenas parte do problema. A outra parte está relacionada à hospedagem de containers em servidores. Você precisa entender qual contêiner pode ser colocado em qual servidor. Esta não é uma tarefa tão fácil, pois os contêineres precisam ser colocados nos servidores da forma mais densa possível, sem reduzir sua velocidade. Tal posicionamento também pode ser difícil do ponto de vista da tolerância a falhas. Muitas vezes queremos colocar réplicas do mesmo serviço em racks diferentes ou mesmo em salas diferentes do data center, para que se um rack ou sala falhar, não percamos imediatamente todas as réplicas do serviço.

Distribuir contêineres manualmente não é uma opção quando você tem 8 mil servidores e 8 a 16 mil contêineres.

Além disso, queríamos dar aos desenvolvedores mais independência na alocação de recursos para que eles próprios pudessem hospedar seus serviços em produção, sem a ajuda de um administrador. Ao mesmo tempo, queríamos manter o controle para que algum serviço menor não consumisse todos os recursos dos nossos data centers.

Obviamente, precisamos de uma camada de controle que faça isso automaticamente.

Chegamos então a uma imagem simples e compreensível que todos os arquitetos adoram: três quadrados.

One-cloud - sistema operacional no nível do data center em Odnoklassniki

one-cloud masters é um cluster de failover responsável pela orquestração da nuvem. O desenvolvedor envia um manifesto ao master, que contém todas as informações necessárias para hospedar o serviço. Com base nele, o mestre dá comandos aos minions selecionados (máquinas projetadas para executar contêineres). Os minions têm nosso agente, que recebe o comando, emite seus comandos para o Docker, e o Docker configura o kernel do Linux para iniciar o contêiner correspondente. Além de executar comandos, o agente reporta continuamente ao mestre sobre mudanças no estado da máquina minion e dos contêineres em execução nela.

Alocação de recursos

Agora vamos examinar o problema da alocação de recursos mais complexa para muitos lacaios.

Um recurso de computação em uma nuvem é:

  • A quantidade de energia do processador consumida por uma tarefa específica.
  • A quantidade de memória disponível para a tarefa.
  • Tráfego de rede. Cada um dos minions possui uma interface de rede específica com largura de banda limitada, sendo impossível distribuir tarefas sem levar em conta a quantidade de dados que transmitem pela rede.
  • Discos. Além, obviamente, do espaço para estas tarefas, também atribuímos o tipo de disco: HDD ou SSD. Os discos podem atender a um número finito de solicitações por segundo – IOPS. Portanto, para tarefas que geram mais IOPS do que um único disco pode suportar, também alocamos “spindles” – ou seja, dispositivos de disco que devem ser reservados exclusivamente para a tarefa.

Então, para algum serviço, por exemplo, para cache do usuário, podemos registrar os recursos consumidos desta forma: 400 núcleos de processador, 2,5 TB de memória, 50 Gbit/s de tráfego em ambas as direções, 6 TB de espaço em HDD localizado em 100 eixos. Ou de uma forma mais familiar como esta:

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

Os recursos de serviço de cache do usuário consomem apenas uma parte de todos os recursos disponíveis na infraestrutura de produção. Portanto, quero ter certeza de que, de repente, devido a um erro do operador ou não, o cache do usuário não consuma mais recursos do que os alocados para ele. Ou seja, devemos limitar os recursos. Mas a que poderíamos vincular a cota?

Vamos voltar ao nosso diagrama bastante simplificado da interação dos componentes e redesenhá-lo com mais detalhes - assim:

One-cloud - sistema operacional no nível do data center em Odnoklassniki

O que chama a atenção:

  • O frontend da web e a música usam clusters isolados do mesmo servidor de aplicativos.
  • Podemos distinguir as camadas lógicas às quais pertencem esses clusters: frentes, caches, armazenamento de dados e camada de gerenciamento.
  • O frontend é heterogêneo; consiste em diferentes subsistemas funcionais.
  • Os caches também podem ser espalhados pelo subsistema cujos dados eles armazenam em cache.

Vamos redesenhar a imagem novamente:

One-cloud - sistema operacional no nível do data center em Odnoklassniki

Bah! Sim, vemos uma hierarquia! Isso significa que você pode distribuir recursos em partes maiores: atribuir um desenvolvedor responsável a um nó desta hierarquia correspondente ao subsistema funcional (como “música” na imagem) e anexar uma cota ao mesmo nível da hierarquia. Essa hierarquia também nos permite organizar os serviços de forma mais flexível para facilitar o gerenciamento. Por exemplo, dividimos toda a web, por se tratar de um agrupamento muito grande de servidores, em vários grupos menores, mostrados na imagem como grupo1, grupo2.

Ao remover as linhas extras, podemos escrever cada nó da nossa imagem de uma forma mais plana: grupo1.web.front, api.music.front, usuário-cache.cache.

É assim que chegamos ao conceito de “fila hierárquica”. Tem um nome como "group1.web.front". Uma cota de recursos e direitos de usuário é atribuída a ele. Daremos à pessoa do DevOps o direito de enviar um serviço para a fila, e esse funcionário poderá lançar algo na fila, e a pessoa do OpsDev terá direitos de administrador, e agora ele poderá gerenciar a fila, atribuir pessoas lá, conceder direitos a essas pessoas, etc. Os serviços executados nesta fila serão executados dentro da cota da fila. Caso a cota computacional da fila não seja suficiente para executar todos os serviços de uma vez, eles serão executados sequencialmente, formando assim a própria fila.

Vamos dar uma olhada nos serviços. Um serviço possui um nome totalmente qualificado, que sempre inclui o nome da fila. Então o serviço web frontal terá o nome ok-web.group1.web.front. E o serviço do servidor de aplicativos que ele acessa será chamado ok-app.group1.web.front. Cada serviço possui um manifesto, que especifica todas as informações necessárias para colocação em máquinas específicas: quantos recursos esta tarefa consome, qual configuração é necessária para ela, quantas réplicas deve haver, propriedades para tratamento de falhas deste serviço. E depois que o serviço é colocado diretamente nas máquinas, aparecem suas instâncias. Eles também são nomeados de forma inequívoca - como o número da instância e o nome do serviço: 1.ok-web.group1.web.front, 2.ok-web.group1.web.front,…

Isso é muito conveniente: olhando apenas o nome do contêiner em execução, podemos descobrir muita coisa imediatamente.

Agora vamos dar uma olhada mais de perto no que essas instâncias realmente executam: tarefas.

Classes de isolamento de tarefas

Todas as tarefas em OK (e provavelmente em todos os lugares) podem ser divididas em grupos:

  • Tarefas de curta latência - prod. Para tais tarefas e serviços, o atraso de resposta (latência), que é a rapidez com que cada uma das solicitações será processada pelo sistema, é muito importante. Exemplos de tarefas: web fronts, caches, servidores de aplicações, armazenamento OLTP, etc.
  • Problemas de cálculo - lote. Aqui, a velocidade de processamento de cada solicitação específica não é importante. Para eles, é importante quantos cálculos esta tarefa fará em um determinado (longo) período de tempo (throughput). Estas serão quaisquer tarefas de MapReduce, Hadoop, aprendizado de máquina, estatística.
  • Tarefas em segundo plano - inativas. Para tais tarefas, nem a latência nem a taxa de transferência são muito importantes. Isto inclui vários testes, migrações, recálculos e conversão de dados de um formato para outro. Por um lado, são semelhantes aos calculados, por outro lado, não nos importa muito a rapidez com que são concluídos.

Vamos ver como tais tarefas consomem recursos, por exemplo, o processador central.

Tarefas de pequeno atraso. Tal tarefa terá um padrão de consumo de CPU semelhante a este:

One-cloud - sistema operacional no nível do data center em Odnoklassniki

Uma solicitação do usuário é recebida para processamento, a tarefa começa a usar todos os núcleos de CPU disponíveis, processa, retorna uma resposta, aguarda a próxima solicitação e para. Chegou o próximo pedido - novamente escolhemos tudo o que estava lá, calculamos e estamos aguardando o próximo.

Para garantir a latência mínima para tal tarefa, devemos aproveitar o máximo de recursos que ela consome e reservar o número necessário de núcleos no minion (a máquina que irá executar a tarefa). Então a fórmula de reserva para o nosso problema será a seguinte:

alloc: cpu = 4 (max)

e se tivermos uma máquina minion com 16 núcleos, então exatamente quatro dessas tarefas podem ser colocadas nela. Observamos especialmente que o consumo médio do processador para tais tarefas costuma ser muito baixo - o que é óbvio, já que uma parte significativa do tempo a tarefa espera por uma solicitação e não faz nada.

Tarefas de cálculo. O padrão deles será um pouco diferente:

One-cloud - sistema operacional no nível do data center em Odnoklassniki

O consumo médio de recursos da CPU para tais tarefas é bastante alto. Muitas vezes queremos que uma tarefa de cálculo seja concluída em um determinado período de tempo, por isso precisamos reservar o número mínimo de processadores necessários para que todo o cálculo seja concluído em um tempo aceitável. Sua fórmula de reserva ficará assim:

alloc: cpu = [1,*)

“Por favor, coloque-o em um lacaio onde haja pelo menos um núcleo livre, e então, quantos houver, ele devorará tudo.”

Aqui a eficiência de uso já é muito melhor do que em tarefas com pequeno atraso. Mas o ganho será muito maior se você combinar os dois tipos de tarefas em uma máquina minion e distribuir seus recursos em movimento. Quando uma tarefa com pequeno atraso requer um processador, ele o recebe imediatamente, e quando os recursos não são mais necessários, eles são transferidos para a tarefa computacional, ou seja, algo assim:

One-cloud - sistema operacional no nível do data center em Odnoklassniki

Mas como fazer isso?

Primeiro, vamos dar uma olhada em prod e sua alocação: cpu = 4. Precisamos reservar quatro núcleos. Na execução do Docker, isso pode ser feito de duas maneiras:

  • Usando a opção --cpuset=1-4, ou seja, alocar quatro núcleos específicos na máquina para a tarefa.
  • Usar --cpuquota=400_000 --cpuperiod=100_000, atribua uma cota de tempo de processador, ou seja, indique que a cada 100 ms de tempo real a tarefa não consome mais que 400 ms de tempo de processador. Os mesmos quatro núcleos são obtidos.

Mas qual desses métodos é adequado?

cpuset parece bastante atraente. A tarefa possui quatro núcleos dedicados, o que significa que os caches do processador funcionarão da maneira mais eficiente possível. Isso também tem uma desvantagem: teríamos que assumir a tarefa de distribuir cálculos entre os núcleos descarregados da máquina em vez do sistema operacional, e esta é uma tarefa bastante não trivial, especialmente se tentarmos colocar tarefas em lote em tal um máquina. Os testes mostraram que a opção com cota é mais adequada aqui: assim o sistema operacional tem mais liberdade na escolha do núcleo para realizar a tarefa no momento atual e o tempo do processador é distribuído de forma mais eficiente.

Vamos descobrir como fazer reservas no Docker com base no número mínimo de núcleos. A cota para tarefas em lote não é mais aplicável, pois não há necessidade de limitar o máximo, basta apenas garantir o mínimo. E aqui a opção se encaixa bem docker run --cpushares.

Concordamos que se um lote necessitar de garantia para pelo menos um núcleo, então indicamos --cpushares=1024, e se houver pelo menos dois núcleos, indicamos --cpushares=2048. Os compartilhamentos de CPU não interferem de forma alguma na distribuição do tempo do processador, desde que haja tempo suficiente. Portanto, se o prod não estiver usando todos os seus quatro núcleos, não há nada que limite as tarefas em lote e eles podem usar tempo adicional do processador. Mas em uma situação em que há escassez de processadores, se o prod tiver consumido todos os quatro núcleos e atingido sua cota, o tempo restante do processador será dividido proporcionalmente aos cpushares, ou seja, em uma situação de três núcleos livres, um será dado a uma tarefa com 1024 cpushares, e os dois restantes serão dados a uma tarefa com 2048 cpushares.

Mas utilizar cotas e ações não é suficiente. Precisamos ter certeza de que uma tarefa com um pequeno atraso receba prioridade sobre uma tarefa em lote ao alocar o tempo do processador. Sem essa priorização, a tarefa em lote ocupará todo o tempo do processador no momento em que for necessária para o produto. Não há opções de priorização de contêineres na execução do Docker, mas as políticas do agendador de CPU do Linux são úteis. Você pode ler sobre eles em detalhes aqui, e no âmbito deste artigo iremos analisá-los brevemente:

  • SCHED_OTHER
    Por padrão, todos os processos normais do usuário em uma máquina Linux recebem.
  • SCHED_BATCH
    Projetado para processos com uso intensivo de recursos. Ao colocar uma tarefa em um processador, é introduzida uma chamada penalidade de ativação: é menos provável que tal tarefa receba recursos do processador se estiver sendo usada por uma tarefa com SCHED_OTHER
  • SCHED_IDLE
    Um processo em segundo plano com prioridade muito baixa, ainda menor que o agradável -19. Usamos nossa biblioteca de código aberto um ano, para definir a política necessária ao iniciar o contêiner chamando

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

Mas mesmo que você não programe em Java, a mesma coisa pode ser feita usando o comando chrt:

chrt -i 0 $pid

Vamos resumir todos os nossos níveis de isolamento em uma tabela para maior clareza:

Classe de isolamento
Exemplo de alocação
Opções de execução do Docker
sched_setscheduler chrt*

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

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

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

*Se você estiver fazendo chrt de dentro de um contêiner, poderá precisar do recurso sys_nice, porque por padrão o Docker remove esse recurso ao iniciar o contêiner.

Mas as tarefas consomem não apenas o processador, mas também o tráfego, o que afeta ainda mais a latência de uma tarefa de rede do que a alocação incorreta de recursos do processador. Portanto, naturalmente queremos obter exatamente a mesma imagem para o tráfego. Ou seja, quando uma tarefa prod envia alguns pacotes para a rede, limitamos a velocidade máxima (fórmula alocar: lan=[*,500mbps) ), com o qual prod pode fazer isso. E para lote garantimos apenas o rendimento mínimo, mas não limitamos o máximo (fórmula alocar: lan=[10Mbps,*) ) Nesse caso, o tráfego de produção deve receber prioridade sobre as tarefas em lote.
Aqui o Docker não possui nenhuma primitiva que possamos usar. Mas vem em nosso auxílio Controle de tráfego Linux. Conseguimos alcançar o resultado desejado com a ajuda da disciplina Curva Hierárquica de Serviço Justo. Com sua ajuda, distinguimos duas classes de tráfego: produção de alta prioridade e lote/ocioso de baixa prioridade. Como resultado, a configuração do tráfego de saída é assim:

One-cloud - sistema operacional no nível do data center em Odnoklassniki

aqui 1:0 é o “qdisc raiz” da disciplina hsfc; 1:1 - classe filha hsfc com limite de largura de banda total de 8 Gbit/s, sob a qual são colocadas as classes filhas de todos os containers; 1:2 - a classe filha hsfc é comum a todas as tarefas em lote e ociosas com um limite “dinâmico”, que é discutido abaixo. As classes filhas hsfc restantes são classes dedicadas para contêineres de produção atualmente em execução com limites correspondentes aos seus manifestos - 450 e 400 Mbit/s. Cada classe hsfc recebe uma fila qdisc fq ou fq_codel, dependendo da versão do kernel Linux, para evitar perda de pacotes durante picos de tráfego.

Normalmente, as disciplinas tc servem para priorizar apenas o tráfego de saída. Mas também queremos priorizar o tráfego de entrada - afinal, algumas tarefas em lote podem facilmente selecionar todo o canal de entrada, recebendo, por exemplo, um grande lote de dados de entrada para mapear e reduzir. Para isso usamos o módulo seb, que cria uma interface virtual ifbX para cada interface de rede e redireciona o tráfego de entrada da interface para o tráfego de saída no ifbX. Além disso, para ifbX, todas as mesmas disciplinas funcionam para controlar o tráfego de saída, para o qual a configuração do hsfc será muito semelhante:

One-cloud - sistema operacional no nível do data center em Odnoklassniki

Durante os experimentos, descobrimos que o hsfc mostra os melhores resultados quando a classe 1:2 de tráfego em lote/ocioso não prioritário é limitada em máquinas minion a não mais do que uma determinada faixa livre. Caso contrário, o tráfego não prioritário terá muito impacto na latência das tarefas de produção. miniond determina a quantidade atual de largura de banda livre a cada segundo, medindo o consumo médio de tráfego de todas as tarefas de produção de um determinado minion One-cloud - sistema operacional no nível do data center em Odnoklassniki e subtraindo-o da largura de banda da interface de rede One-cloud - sistema operacional no nível do data center em Odnoklassniki com uma pequena margem, ou seja,

One-cloud - sistema operacional no nível do data center em Odnoklassniki

As bandas são definidas independentemente para tráfego de entrada e saída. E de acordo com os novos valores, o miniond reconfigura o limite de classe não prioritária 1:2.

Assim, implementamos todas as três classes de isolamento: prod, batch e idle. Essas classes influenciam muito as características de desempenho das tarefas. Portanto, decidimos colocar este atributo no topo da hierarquia, para que ao olhar o nome da fila hierárquica ficasse imediatamente claro com o que estamos lidando:

One-cloud - sistema operacional no nível do data center em Odnoklassniki

Todos os nossos amigos web и música as frentes são então colocadas na hierarquia sob prod. Por exemplo, em lote, vamos colocar o serviço catálogo de música, que compila periodicamente um catálogo de faixas de um conjunto de arquivos mp3 carregados no Odnoklassniki. Um exemplo de serviço ocioso seria transformador de música, que normaliza o nível de volume da música.

Com as linhas extras removidas novamente, podemos escrever nossos nomes de serviço de forma mais simples, adicionando a classe de isolamento de tarefas ao final do nome completo do serviço: web.front.prod, catálogo.music.batch, transformador.música.idle.

E agora, olhando o nome do serviço, entendemos não só qual função ele desempenha, mas também sua classe de isolamento, o que significa sua criticidade, etc.

Tudo está ótimo, mas há uma verdade amarga. É impossível isolar completamente as tarefas executadas em uma máquina.

O que conseguimos alcançar: se o lote consumir intensamente apenas Recursos da CPU, então o agendador de CPU integrado do Linux faz seu trabalho muito bem e praticamente não há impacto na tarefa de produção. Mas se essa tarefa em lote começar a trabalhar ativamente com a memória, a influência mútua já aparecerá. Isso acontece porque a tarefa de produção é “lavada” dos caches de memória do processador - como resultado, as perdas de cache aumentam e o processador processa a tarefa de produção mais lentamente. Essa tarefa em lote pode aumentar a latência de nosso contêiner de produção típico em 10%.

Isolar o tráfego é ainda mais difícil devido ao fato de as placas de rede modernas possuírem uma fila interna de pacotes. Se o pacote da tarefa em lote chegar primeiro, ele será o primeiro a ser transmitido pelo cabo e nada poderá ser feito a respeito.

Além disso, até agora só conseguimos resolver o problema de priorização do tráfego TCP: a abordagem hsfc não funciona para UDP. E mesmo no caso do tráfego TCP, se a tarefa em lote gerar muito tráfego, isso também proporcionará um aumento de cerca de 10% no atraso da tarefa de produção.

tolerância ao erro

Um dos objetivos ao desenvolver a nuvem única era melhorar a tolerância a falhas do Odnoklassniki. Portanto, a seguir gostaria de considerar com mais detalhes possíveis cenários de falhas e acidentes. Vamos começar com um cenário simples: uma falha no contêiner.

O próprio contêiner pode falhar de diversas maneiras. Pode ser algum tipo de experimento, bug ou erro no manifesto, devido ao qual a tarefa prod começa a consumir mais recursos do que o indicado no manifesto. Tivemos um caso: um desenvolvedor implementou um algoritmo complexo, retrabalhou-o muitas vezes, pensou demais e ficou tão confuso que, no final das contas, o problema entrou em um ciclo nada trivial. E como a tarefa de produção tem prioridade mais alta do que todas as outras nos mesmos minions, ela começou a consumir todos os recursos disponíveis do processador. Nessa situação, o isolamento, ou melhor, a cota de tempo de CPU, salvou o dia. Se for alocada uma cota para uma tarefa, a tarefa não consumirá mais. Portanto, tarefas em lote e outras tarefas de produção executadas na mesma máquina não notaram nada.

O segundo problema possível é a queda do contêiner. E aqui as políticas de reinicialização nos salvam, todo mundo as conhece, o próprio Docker faz um ótimo trabalho. Quase todas as tarefas de produção têm uma política de reinicialização sempre. Às vezes usamos on_failure para tarefas em lote ou para depuração de contêineres de produtos.

O que você pode fazer se um lacaio inteiro estiver indisponível?

Obviamente, execute o contêiner em outra máquina. A parte interessante aqui é o que acontece com os endereços IP atribuídos ao contêiner.

Podemos atribuir aos contêineres os mesmos endereços IP das máquinas minion nas quais esses contêineres são executados. Então, quando o contêiner é iniciado em outra máquina, seu endereço IP muda e todos os clientes devem entender que o contêiner foi movido e agora precisam ir para um endereço diferente, o que requer um serviço separado de descoberta de serviço.

A descoberta de serviço é conveniente. Existem muitas soluções no mercado com vários graus de tolerância a falhas para organizar um registro de serviço. Freqüentemente, essas soluções implementam lógica de balanceador de carga, armazenam configurações adicionais na forma de armazenamento KV, etc.
No entanto, gostaríamos de evitar a necessidade de implementar um registo separado, porque isso significaria introduzir um sistema crítico que seria utilizado por todos os serviços em produção. Isto significa que este é um ponto potencial de falha, e você precisa escolher ou desenvolver uma solução muito tolerante a falhas, o que é obviamente muito difícil, demorado e caro.

E mais uma grande desvantagem: para que nossa infraestrutura antiga funcionasse com a nova, teríamos que reescrever absolutamente todas as tarefas para usar algum tipo de sistema de descoberta de serviço. Há MUITO trabalho e, em alguns lugares, é quase impossível quando se trata de dispositivos de baixo nível que funcionam no nível do kernel do sistema operacional ou diretamente com o hardware. Implementação desta funcionalidade usando padrões de solução estabelecidos, como sidecar significaria em alguns lugares uma carga adicional, em outros - uma complicação de operação e cenários de falha adicionais. Não queríamos complicar as coisas, então decidimos tornar opcional o uso do Service Discovery.

Em uma nuvem, o IP segue o contêiner, ou seja, cada instância de tarefa possui seu próprio endereço IP. Este endereço é “estático”: é atribuído a cada instância quando o serviço é enviado pela primeira vez para a nuvem. Se um serviço teve um número diferente de instâncias durante sua vida, no final serão atribuídos a ele tantos endereços IP quanto o número máximo de instâncias.

Posteriormente, esses endereços não mudam: são atribuídos uma vez e continuam a existir durante toda a vida do serviço em produção. Os endereços IP seguem os contêineres pela rede. Se o contêiner for transferido para outro lacaio, o endereço o seguirá.

Assim, o mapeamento de um nome de serviço para a sua lista de endereços IP raramente muda. Se você olhar novamente os nomes das instâncias de serviço que mencionamos no início do artigo (1.ok-web.group1.web.front.prod, 2.ok-web.group1.web.front.prod,…), notaremos que eles se assemelham aos FQDNs usados ​​no DNS. Isso mesmo, para mapear os nomes das instâncias de serviço para seus endereços IP, usamos o protocolo DNS. Além disso, esse DNS retorna todos os endereços IP reservados de todos os contêineres - tanto em execução quanto parados (digamos que três réplicas sejam usadas e tenhamos cinco endereços reservados lá - todos os cinco serão retornados). Os clientes, ao receberem essas informações, tentarão estabelecer uma conexão com todas as cinco réplicas - e assim determinar aquelas que estão funcionando. Esta opção de determinação da disponibilidade é muito mais fiável, não envolve DNS nem Service Discovery, o que significa que não existem problemas difíceis de resolver para garantir a relevância da informação e a tolerância a falhas destes sistemas. Além disso, em serviços críticos dos quais depende o funcionamento de todo o portal, não podemos utilizar DNS de forma alguma, mas simplesmente inserir endereços IP na configuração.

Implementar essa transferência de IP por trás de contêineres pode não ser trivial - e veremos como funciona com o exemplo a seguir:

One-cloud - sistema operacional no nível do data center em Odnoklassniki

Digamos que o mestre de uma nuvem dê o comando para o minion M1 executar 1.ok-web.group1.web.front.prod com endereço 1.1.1.1. Funciona em um minion PÁSSARO, que anuncia este endereço para servidores especiais refletor de rota. Estes últimos possuem uma sessão BGP com o hardware de rede, na qual é traduzida a rota do endereço 1.1.1.1 em M1. M1 roteia pacotes dentro do contêiner usando Linux. Existem três servidores refletores de rota, pois esta é uma parte muito crítica da infraestrutura de nuvem única - sem eles, a rede em nuvem única não funcionará. Nós os colocamos em racks diferentes, se possível localizados em salas diferentes do data center, para reduzir a probabilidade de todos os três falharem ao mesmo tempo.

Vamos agora supor que a conexão entre o mestre de nuvem única e o minion M1 foi perdida. O mestre de nuvem única agora agirá presumindo que M1 falhou completamente. Ou seja, dará o comando para o minion M2 lançar web.group1.web.front.prod com o mesmo endereço 1.1.1.1. Agora temos duas rotas conflitantes na rede para 1.1.1.1: em M1 e em M2. Para resolver tais conflitos, utilizamos o Multi Exit Discriminator, especificado no anúncio do BGP. Este é um número que mostra o peso da rota anunciada. Dentre as rotas conflitantes, será selecionada a rota com menor valor MED. O mestre de nuvem única oferece suporte ao MED como parte integrante dos endereços IP do contêiner. Pela primeira vez, o endereço é escrito com um MED suficientemente grande = 1. Na situação de tal transferência emergencial de contêiner, o mestre reduz o MED, e M000 já receberá o comando para anunciar o endereço 000 com MED = 2.A instância rodando em M1.1.1.1 permanecerá neste caso, não há conexão, e seu futuro destino pouco nos interessa até que a conexão com o mestre seja restaurada, quando ele será parado como um take antigo.

Acidente

Todos os sistemas de gerenciamento de data center sempre lidam com pequenas falhas de forma aceitável. O transbordamento de contêineres é a norma em quase todos os lugares.

Vejamos como lidamos com uma emergência, como uma falha de energia em uma ou mais salas de um data center.

O que um acidente significa para um sistema de gerenciamento de data center? Em primeiro lugar, esta é uma falha massiva e única de muitas máquinas, e o sistema de controle precisa migrar muitos contêineres ao mesmo tempo. Mas se o desastre for de grande escala, então pode acontecer que todas as tarefas não possam ser realocadas para outros subordinados, porque a capacidade de recursos do data center cai abaixo de 100% da carga.

Freqüentemente, os acidentes são acompanhados por falhas na camada de controle. Isso pode acontecer por falha de seus equipamentos, mas mais frequentemente pelo fato de acidentes não serem testados e a própria camada de controle cair devido ao aumento de carga.

O que você pode fazer sobre tudo isso?

As migrações em massa significam que há um grande número de atividades, migrações e implantações ocorrendo na infraestrutura. Cada uma das migrações pode levar algum tempo para entregar e descompactar imagens de contêiner para minions, lançar e inicializar contêineres, etc. Portanto, é desejável que tarefas mais importantes sejam iniciadas antes das menos importantes.

Vamos examinar novamente a hierarquia de serviços com a qual estamos familiarizados e tentar decidir quais tarefas queremos executar primeiro.

One-cloud - sistema operacional no nível do data center em Odnoklassniki

Claro, esses são os processos que estão diretamente envolvidos no processamento das solicitações dos usuários, ou seja, prod. Indicamos isso com prioridade de posicionamento — um número que pode ser atribuído à fila. Se uma fila tiver prioridade mais alta, seus serviços serão colocados primeiro.

No prod atribuímos prioridades mais altas, 0; por lote - um pouco menor, 100; em modo inativo - ainda mais baixo, 200. As prioridades são aplicadas hierarquicamente. Todas as tarefas inferiores na hierarquia terão uma prioridade correspondente. Se quisermos que os caches dentro do prod sejam lançados antes dos frontends, então atribuímos prioridades ao cache = 0 e às subfilas frontais = 1. Se, por exemplo, quisermos que o portal principal seja lançado primeiro pelos frontends, e apenas o front musical então podemos atribuir uma prioridade mais baixa a este último - 10.

O próximo problema é a falta de recursos. Então, uma grande quantidade de equipamentos, salas inteiras do data center, falharam, e relançamos tantos serviços que agora não há recursos suficientes para todos. Você precisa decidir quais tarefas sacrificar para manter os principais serviços críticos em funcionamento.

One-cloud - sistema operacional no nível do data center em Odnoklassniki

Ao contrário da prioridade de posicionamento, não podemos sacrificar indiscriminadamente todas as tarefas em lote; algumas delas são importantes para o funcionamento do portal. Portanto, destacamos separadamente prioridade de preempção tarefas. Quando colocada, uma tarefa de prioridade mais alta pode antecipar, ou seja, interromper, uma tarefa de prioridade mais baixa se não houver mais lacaios livres. Neste caso, uma tarefa com baixa prioridade provavelmente permanecerá sem colocação, ou seja, não haverá mais um lacaio adequado para ela com recursos livres suficientes.

Em nossa hierarquia, é muito simples especificar uma prioridade de preempção de modo que as tarefas de produção e de lote preemptem ou interrompam as tarefas ociosas, mas não umas às outras, especificando uma prioridade para ociosidade igual a 200. Assim como no caso da prioridade de posicionamento, nós podemos usar nossa hierarquia para descrever regras mais complexas. Por exemplo, vamos indicar que sacrificamos a função de música se não tivermos recursos suficientes para o portal web principal, definindo a prioridade para os nós correspondentes mais baixa: 10.

Acidentes inteiros em DC

Por que todo o data center pode falhar? Elemento. Foi uma boa postagem o furacão afetou o trabalho do data center. Os elementos podem ser considerados moradores de rua que uma vez queimaram a ótica do coletor e o data center perdeu completamente o contato com outros locais. A causa da falha também pode ser um fator humano: o operador emitirá um comando que fará com que todo o data center caia. Isso pode acontecer devido a um grande bug. Em geral, o colapso dos data centers não é incomum. Isso acontece conosco uma vez a cada poucos meses.

E é isso que fazemos para evitar que alguém twitte #alive.

A primeira estratégia é o isolamento. Cada instância de nuvem é isolada e pode gerenciar máquinas em apenas um data center. Ou seja, a perda de uma nuvem devido a bugs ou comandos incorretos do operador é a perda de apenas um data center. Estamos prontos para isso: temos uma política de redundância em que réplicas da aplicação e dos dados ficam localizadas em todos os data centers. Usamos bancos de dados tolerantes a falhas e testamos periodicamente falhas.
Como hoje temos quatro data centers, isso significa quatro instâncias separadas e completamente isoladas de uma nuvem.

Esta abordagem não apenas protege contra falhas físicas, mas também pode proteger contra erros do operador.

O que mais pode ser feito com o fator humano? Quando um operador dá à nuvem algum comando estranho ou potencialmente perigoso, ele pode repentinamente ser solicitado a resolver um pequeno problema para ver se ele pensou bem. Por exemplo, se for algum tipo de parada em massa de muitas réplicas ou apenas um comando estranho - reduzindo o número de réplicas ou alterando o nome da imagem, e não apenas o número da versão no novo manifesto.

One-cloud - sistema operacional no nível do data center em Odnoklassniki

Resultados de

Características distintivas da nuvem única:

  • Esquema de nomenclatura hierárquica e visual para serviços e contêineres, que permite descobrir muito rapidamente qual é a tarefa, a que se refere e como funciona e quem é o responsável por ela.
  • Aplicamos nosso técnica de combinar produtos e lotestarefas em lacaios para melhorar a eficiência do compartilhamento de máquinas. Em vez de cpuset usamos cotas de CPU, compartilhamentos, políticas de escalonador de CPU e QoS do Linux.
  • Não foi possível isolar completamente os contêineres rodando na mesma máquina, mas sua influência mútua permanece dentro de 20%.
  • Organizar os serviços em uma hierarquia ajuda na recuperação automática de desastres usando prioridades de colocação e preempção.

Perguntas frequentes

Por que não adotamos uma solução pronta?

  • Diferentes classes de isolamento de tarefas exigem lógicas diferentes quando colocadas em lacaios. Se as tarefas de produção puderem ser colocadas simplesmente reservando recursos, então as tarefas em lote e ociosas deverão ser colocadas, rastreando a utilização real dos recursos nas máquinas minion.
  • A necessidade de levar em conta os recursos consumidos pelas tarefas, tais como:
    • largura de banda da rede;
    • tipos e “fusos” de discos.
  • A necessidade de indicar as prioridades dos serviços durante o atendimento a emergências, os direitos e cotas de comandos de recursos, o que é resolvido por meio de filas hierárquicas em uma nuvem.
  • A necessidade de nomeação humana dos contêineres para reduzir o tempo de resposta a acidentes e incidentes
  • A impossibilidade de uma implementação única e generalizada de Service Discovery; a necessidade de coexistir por muito tempo com tarefas hospedadas em hosts de hardware - algo que é resolvido por endereços IP “estáticos” seguindo contêineres e, como consequência, a necessidade de integração única com uma grande infraestrutura de rede.

Todas estas funções exigiriam modificações significativas nas soluções existentes para nos adequar e, tendo avaliado a quantidade de trabalho, percebemos que poderíamos desenvolver a nossa própria solução com aproximadamente os mesmos custos de mão-de-obra. Mas sua solução será muito mais fácil de operar e desenvolver - ela não contém abstrações desnecessárias que suportam funcionalidades que não precisamos.

Aos que leram as últimas linhas, obrigado pela paciência e atenção!

Fonte: habr.com

Adicionar um comentário