
Olá Habr! Sou Artyom Karamyshev, chefe da equipe de administração de sistemas da
Mail.Ru Cloud Solutions (MCS) . No ano passado, tivemos muitos lançamentos de novos produtos. Queríamos que os serviços da API fossem escalonados com facilidade, tolerantes a falhas e prontos para um rápido aumento na carga do usuário. Nossa plataforma é implementada no OpenStack, e quero dizer quais problemas de tolerância a falhas de componentes tivemos que fechar para obter um sistema tolerante a falhas. Eu acho que isso será interessante para quem também desenvolve produtos no OpenStack.
A tolerância geral a falhas da plataforma consiste na estabilidade de seus componentes. Então, passaremos gradualmente a todos os níveis em que descobrimos os riscos e os fechamos.
Uma versão em vídeo dessa história, cuja fonte foi um relatório na conferência Uptime day 4, organizada pela
ITSumma , pode ser visualizada
no canal do YouTube da Comunidade Uptime .
Tolerância a falhas da arquitetura física
A parte pública da nuvem MCS agora está baseada em dois data centers de nível III, entre eles existe uma fibra escura própria, reservada na camada física por diferentes rotas, com uma taxa de transferência de 200 Gb / s. O nível de camada III fornece o nível necessário de resiliência da infraestrutura física.
A fibra escura é reservada nos níveis físico e lógico. O processo de reserva de canais foi iterativo, surgiram problemas e estamos constantemente aprimorando a comunicação entre os data centers.
Por exemplo, não muito tempo atrás, ao trabalhar em um poço próximo a um dos data centers, uma escavadeira perfurou um cano; dentro desse cano havia um cabo óptico principal e um de backup. Nosso canal de comunicação tolerante a falhas com o data center ficou vulnerável em um ponto do poço. Por conseguinte, perdemos parte da infraestrutura. Tiramos conclusões, realizamos várias ações, incluindo a colocação de ópticas adicionais ao longo de um poço vizinho.
Nos datacenters, existem pontos de presença de provedores de comunicação para os quais transmitimos nossos prefixos via BGP. Para cada direção da rede, a melhor métrica é selecionada, o que permite que diferentes clientes forneçam a melhor qualidade de conexão. Se a comunicação através de um provedor for desconectada, reconstruiremos nosso roteamento através dos provedores disponíveis.
No caso de uma falha do provedor, passamos automaticamente para o próximo. Em caso de falha de um dos datacenters, temos uma cópia espelhada de nossos serviços no segundo datacenter, que assumem todo o ônus.
Resiliência da infraestrutura físicaO que usamos para tolerância a falhas no nível do aplicativo
Nosso serviço é construído em vários componentes de código aberto.
O ExaBGP é um serviço que implementa várias funções usando o protocolo de roteamento dinâmico baseado no BGP. Nós o usamos ativamente para anunciar nossos endereços IP brancos por meio dos quais os usuários obtêm acesso à API.
O HAProxy é um balanceador altamente carregado que permite configurar regras muito flexíveis para equilibrar o tráfego em diferentes níveis do modelo OSI. Usamos para equilibrar todos os serviços: bancos de dados, intermediários de mensagens, serviços de API, serviços da Web, nossos projetos internos - tudo está por trás do HAProxy.
Aplicativo de API - um
aplicativo da Web escrito em python, com o qual o usuário controla sua infraestrutura, seu serviço.
Aplicativo Worker (a seguir denominado simplesmente worker) - nos serviços OpenStack, é um daemon de infraestrutura que permite converter comandos API para a infraestrutura. Por exemplo, um disco é criado no trabalho e uma solicitação de criação está na API do aplicativo.
Arquitetura padrão de aplicativos OpenStack
A maioria dos serviços desenvolvidos para o OpenStack tenta seguir um único paradigma. Um serviço geralmente consiste em 2 partes: API e trabalhadores (executores de back-end). Normalmente, uma API é um aplicativo WSGI python que é executado como um processo independente (daemon) ou usando um servidor Web Nginx pronto, Apache. A API processa a solicitação do usuário e passa mais instruções ao aplicativo de trabalho. A transmissão ocorre usando um intermediário de mensagens, geralmente o RabbitMQ, o restante é pouco suportado. Quando as mensagens chegam ao intermediário, elas são processadas pelos trabalhadores e, se necessário, retornam uma resposta.
Este paradigma implica pontos comuns isolados de falha: RabbitMQ e o banco de dados. Mas o RabbitMQ é isolado em um serviço e, em teoria, pode ser individual para cada serviço. Então, no MCS, compartilhamos esses serviços o máximo possível, para cada projeto individual criamos um banco de dados separado, um RabbitMQ separado. Essa abordagem é boa porque, no caso de um acidente em alguns pontos vulneráveis, nem todos os serviços são interrompidos, mas apenas parte dele.
O número de aplicativos de trabalho é ilimitado, portanto a API pode ser dimensionada facilmente horizontalmente atrás dos balanceadores, a fim de aumentar a produtividade e a tolerância a falhas.
Alguns serviços exigem coordenação dentro do serviço - quando operações seqüenciais complexas ocorrem entre APIs e trabalhadores. Nesse caso, um único centro de coordenação é usado, um sistema de cluster como Redis, Memcache, etcd, que permite que um trabalhador diga ao outro que esta tarefa está atribuída a ele ("por favor, não a execute"). Nós usamos o etcd. Como regra, os trabalhadores se comunicam ativamente com o banco de dados, escrevem e leem informações a partir daí. Como banco de dados, usamos o mariadb, que temos no cluster multimaster.
Esse serviço clássico de usuário único é organizado de uma maneira geralmente aceita pelo OpenStack. Pode ser considerado como um sistema fechado, para o qual os métodos de escala e tolerância a falhas são bastante óbvios. Por exemplo, para tolerância a falhas da API, basta colocar um balanceador na frente deles. A escala de trabalhadores é alcançada aumentando seu número.
Os pontos fracos em todo o esquema são RabbitMQ e MariaDB. Sua arquitetura merece um artigo separado.Neste artigo, quero focar na tolerância a falhas da API.
Arquitetura de aplicativo Openstack Balanceamento e resiliência da plataforma em nuvemTornando o HAProxy Balancer resiliente com o ExaBGP
Para tornar nossas APIs escaláveis, rápidas e tolerantes a falhas, definimos um balanceador à sua frente. Escolhemos o HAProxy. Na minha opinião, possui todas as características necessárias para nossa tarefa: balanceamento em vários níveis OSI, interface de gerenciamento, flexibilidade e escalabilidade, um grande número de métodos de balanceamento, suporte para tabelas de sessões.
O primeiro problema que precisou ser resolvido foi a tolerância a falhas do próprio balanceador. A instalação do balanceador também cria um ponto de falha: o balanceador é interrompido - o serviço cai. Para evitar isso, usamos o HAProxy junto com o ExaBGP.
O ExaBGP permite implementar um mecanismo para verificar o status de um serviço. Usamos esse mecanismo para verificar a funcionalidade do HAProxy e, em caso de problemas, desativar o serviço HAProxy do BGP.
Esquema ExaBGP + HAProxy- Instalamos o software necessário em três servidores, ExaBGP e HAProxy.
- Em cada um dos servidores, criamos uma interface de loopback.
- Nos três servidores, atribuímos o mesmo endereço IP branco a essa interface.
- Um endereço IP branco é anunciado na Internet através do ExaBGP.
A tolerância a falhas é alcançada anunciando o mesmo endereço IP dos três servidores. Do ponto de vista da rede, o mesmo endereço é acessível a partir de três próximas esperanças diferentes. O roteador vê três rotas idênticas, seleciona a maior prioridade delas de acordo com sua própria métrica (geralmente é a mesma opção) e o tráfego é direcionado apenas a um dos servidores.
Em caso de problemas com a operação do HAProxy ou falha do servidor, o ExaBGP para de anunciar a rota e o tráfego muda suavemente para outro servidor.
Assim, alcançamos a tolerância a falhas do balanceador.
Tolerância a falhas de balanceadores de HAProxyO esquema acabou sendo imperfeito: aprendemos a reservar o HAProxy, mas não aprendemos a distribuir a carga dentro dos serviços. Portanto, expandimos um pouco esse esquema: passamos ao equilíbrio entre vários endereços IP brancos.
Balanceamento baseado em DNS Plus BGP
A questão do balanceamento de carga antes do nosso HAProxy permaneceu sem solução. No entanto, isso pode ser resolvido de maneira bastante simples, como fizemos em casa.
Para equilibrar os três servidores, você precisará de três endereços IP brancos e um bom e velho DNS. Cada um desses endereços é definido na interface de loopback de cada HAProxy e é anunciado na Internet.
O OpenStack usa um catálogo de serviços para gerenciar recursos, que define a API do terminal de um serviço. Neste diretório, prescrevemos um nome de domínio - public.infra.mail.ru, que resolve através do DNS com três endereços IP diferentes. Como resultado, obtemos o balanceamento de carga entre os três endereços por meio do DNS.
Porém, como ao anunciar endereços IP brancos, não controlamos as prioridades de seleção do servidor, até agora isso não está equilibrado. Como regra, apenas um servidor será selecionado pela precedência do endereço IP e os outros dois ficarão ociosos, pois nenhuma métrica é especificada no BGP.
Começamos a fornecer rotas através do ExaBGP com diferentes métricas. Cada balanceador anuncia todos os três endereços IP brancos, mas um deles, o principal desse balanceador, é anunciado com uma métrica mínima. Portanto, enquanto os três balanceadores estão em operação, as chamadas para o primeiro endereço IP caem no primeiro, as chamadas para o segundo para o segundo, para o terceiro e para o terceiro.
O que acontece quando um dos balanceadores cai? Em caso de falha de qualquer balanceador por sua base, o endereço ainda é anunciado pelos outros dois, o tráfego entre eles é redistribuído. Assim, damos ao usuário através do DNS vários endereços IP de uma só vez. Ao balancear o DNS e diferentes métricas, obtemos uma distribuição de carga uniforme nos três balanceadores. E, ao mesmo tempo, não perdemos a tolerância a falhas.
Balanceamento HAProxy Baseado em DNS + BGPInteração entre ExaBGP e HAProxy
Portanto, implementamos a tolerância a falhas caso o servidor saia, com base no término do anúncio de rotas. Mas o HAProxy também pode ser desconectado por outros motivos que não a falha do servidor: erros de administração, falhas de serviço. Queremos remover o balanceador quebrado de baixo da carga e, nesses casos, e precisamos de outro mecanismo.
Portanto, expandindo o esquema anterior, implementamos uma pulsação entre ExaBGP e HAProxy. Esta é uma implementação de software da interação entre o ExaBGP e o HAProxy, quando o ExaBGP usa scripts personalizados para verificar o status dos aplicativos.
Para fazer isso, na configuração do ExaBGP, você deve configurar um verificador de integridade que pode verificar o status do HAProxy. No nosso caso, configuramos o back-end de integridade no HAProxy e, no lado do ExaBGP, verificamos com uma simples solicitação GET. Se o anúncio deixar de ocorrer, é provável que o HAProxy não funcione e não é necessário anunciá-lo.
Verificação de integridade HAProxyHAProxy Peers: sincronização de sessão
A próxima coisa a fazer era sincronizar as sessões. Ao trabalhar com balanceadores distribuídos, é difícil organizar o armazenamento de informações sobre as sessões do cliente. Mas o HAProxy é um dos poucos balanceadores que podem fazer isso devido à funcionalidade Peers - a capacidade de transferir tabelas de sessões entre diferentes processos HAProxy.
Existem diferentes métodos de balanceamento: simples, como
round-robin e avançados, quando uma sessão do cliente é lembrada e cada vez que ela chega ao mesmo servidor como antes. Queríamos implementar a segunda opção.
O HAProxy usa tabelas de stick para salvar sessões do cliente para esse mecanismo. Eles salvam o endereço IP de origem do cliente, o endereço de destino selecionado (back-end) e algumas informações de serviço. Normalmente, as tabelas stick são usadas para salvar o par IP de origem + IP de destino, o que é especialmente útil para aplicativos que não podem transmitir o contexto de sessão do usuário ao alternar para outro balanceador, por exemplo, no modo de balanceamento RoundRobin.
Se a tabela de palitos for ensinada a se mover entre diferentes processos HAProxy (entre os quais ocorre o balanceamento), nossos balanceadores poderão trabalhar com um conjunto de tabelas de palitos. Isso tornará possível alternar perfeitamente a rede do cliente quando um dos balanceadores cair; o trabalho com sessões do cliente continuará nos mesmos back-end selecionados anteriormente.
Para uma operação adequada, o endereço IP de origem do balanceador a partir do qual a sessão é estabelecida deve ser resolvido. No nosso caso, este é um endereço dinâmico na interface de loopback.
A operação correta dos pares é alcançada apenas em determinadas condições. Ou seja, os tempos limite do TCP devem ser grandes o suficiente ou o comutador deve ser rápido o suficiente para que a sessão TCP não tenha tempo para interromper. No entanto, isso permite alternar sem interrupções.
Na IaaS, temos um serviço desenvolvido com a mesma tecnologia. Este é um
Balanceador de Carga como um serviço para o OpenStack chamado Octavia. Baseia-se em dois processos HAProxy, originalmente incluindo suporte de pares. Eles se provaram neste serviço.
A imagem mostra esquematicamente o movimento de tabelas de pares entre três instâncias HAProxy, é sugerida uma configuração, como isso pode ser configurado:
HAProxy Peers (sincronização de sessão)Se você implementar o mesmo esquema, seu trabalho deverá ser cuidadosamente testado. Não é o fato de que isso funcionará da mesma maneira em 100% dos casos. Mas, pelo menos, você não perderá tabelas de stick quando precisar lembrar o IP de origem do cliente.
Limitando o número de solicitações simultâneas do mesmo cliente
Quaisquer serviços que sejam de domínio público, incluindo nossas APIs, podem estar sujeitos a uma avalanche de solicitações. Os motivos para eles podem ser completamente diferentes, desde erros do usuário, até ataques direcionados. Periodicamente, somos DDoS em endereços IP. Os clientes geralmente cometem erros em seus scripts; eles nos tornam mini-DDoSs.
De uma forma ou de outra, proteção adicional deve ser fornecida. A solução óbvia é limitar o número de solicitações de API e não perder tempo na CPU processando solicitações maliciosas.
Para implementar essas restrições, usamos limites de taxa, organizados com base no HAProxy, usando as mesmas tabelas de stick. Os limites são configurados de maneira bastante simples e permitem limitar o usuário pelo número de solicitações à API. O algoritmo lembra o IP de origem do qual as solicitações são feitas e limita o número de solicitações simultâneas de um usuário. Obviamente, calculamos o perfil médio de carregamento da API para cada serviço e definimos o limite ≈ 10 vezes esse valor. Até agora, continuamos a acompanhar de perto a situação, mantemos o dedo no pulso.
Como é na prática? Temos clientes que usam constantemente nossas APIs de dimensionamento automático. Eles criam aproximadamente duzentas ou trezentas máquinas virtuais mais perto da manhã e as excluem mais perto da noite. Para o OpenStack, crie uma máquina virtual, também com serviços PaaS, pelo menos 1000 solicitações de API, pois a interação entre os serviços também ocorre por meio da API.
Esse lançamento de tarefa causa uma carga bastante grande. Nós estimamos essa carga, coletamos picos diários, aumentamos dez vezes e isso se tornou nosso limite de taxa. Mantemos o dedo no pulso. Muitas vezes vemos bots, scanners, que estão tentando nos olhar, temos algum script CGA que possa ser executado, nós os cortamos ativamente.
Como atualizar a base de código discretamente para os usuários
Também implementamos tolerância a falhas no nível dos processos de implantação de código. Há falhas durante as implementações, mas seu impacto na disponibilidade do serviço pode ser minimizado.
Estamos constantemente atualizando nossos serviços e devemos garantir o processo de atualização da base de código sem afetar os usuários. Conseguimos resolver esse problema usando os recursos de gerenciamento HAProxy e a implementação do Graceful Shutdown em nossos serviços.
Para resolver esse problema, era necessário fornecer controle do balanceador e o desligamento "correto" dos serviços:
- No caso do HAProxy, o controle é feito através do arquivo de estatísticas, que é essencialmente um soquete e é definido na configuração do HAProxy. Você pode enviar comandos para ele através do stdio. Mas nossa principal ferramenta de controle de configuração é ansible, por isso possui um módulo interno para gerenciar o HAProxy. Que estamos usando ativamente.
- A maioria dos nossos serviços de API e mecanismo oferece suporte a tecnologias de desligamento simples: após o desligamento, eles aguardam a conclusão da tarefa atual, seja uma solicitação http ou algum tipo de tarefa de utilitário. O mesmo acontece com o trabalhador. Ele conhece todas as tarefas que realiza e termina quando conclui tudo com êxito.
Graças a esses dois pontos, o algoritmo seguro de nossa implantação é o seguinte.
- O desenvolvedor cria um novo pacote de código (temos RPM), testa no ambiente de desenvolvimento, testa no estágio e o deixa no repositório do estágio.
- O desenvolvedor coloca a tarefa na implantação com a descrição mais detalhada dos "artefatos": a versão do novo pacote, uma descrição da nova funcionalidade e outros detalhes sobre a implantação, se necessário.
- O administrador do sistema inicia a atualização. Lança o manual do Ansible, que, por sua vez, faz o seguinte:
- Ele pega um pacote do repositório do estágio, atualiza a versão do pacote no repositório do produto.
- Faz uma lista de back-end do serviço atualizado.
- Desativa o primeiro serviço atualizado no HAProxy e aguarda o final de seus processos. Graças ao desligamento normal, estamos confiantes de que todas as solicitações atuais do cliente serão concluídas com êxito.
- Depois que a API, os trabalhadores e o HAProxy são completamente parados, o código é atualizado.
- Ansible lança serviços.
- Para cada serviço, ele puxa certas "canetas" que fazem testes de unidade para vários testes de teclas predefinidos. Uma verificação básica do novo código ocorre.
- Se nenhum erro foi encontrado na etapa anterior, o back-end é ativado.
- Vá para o próximo back-end.
- Após a atualização de todos os back-ends, os testes funcionais são iniciados. Se não forem suficientes, o desenvolvedor analisará qualquer nova funcionalidade que ele fez.
Nesta implantação está concluída.
Ciclo de atualização de serviçoEsse esquema não funcionaria se não tivéssemos uma regra. Apoiamos as versões antiga e nova na batalha. De antemão, no estágio de desenvolvimento do software, é estabelecido que, mesmo que haja alterações no banco de dados do serviço, eles não quebrarão o código anterior. Como resultado, a base de código é gradualmente atualizada.
Conclusão
Compartilhando meus próprios pensamentos sobre a arquitetura WEB tolerante a falhas, desejo mais uma vez observar seus pontos principais:
- tolerância a falhas físicas;
- tolerância a falhas de rede (balanceadores, BGP);
- .
uptime!