
Olá, Habr! Sou Artem Karamyshev, chefe da equipe de administração do sistema . Tivemos muitos lançamentos de novos produtos no ano passado. Queríamos garantir que os serviços de API fossem facilmente escalonáveis, tolerantes a falhas e prontos para o rápido crescimento na carga de usuários. Nossa plataforma é implementada em OpenStack, e quero contar quais problemas de tolerância a falhas de componentes tivemos que resolver para obter um sistema tolerante a falhas. Acho que isso será interessante para quem também desenvolve produtos em OpenStack.
A tolerância geral a falhas de uma plataforma consiste na resiliência de seus componentes. Então vamos passar gradativamente por todos os níveis onde identificamos riscos e os fechamos.
Versão em vídeo desta história, cuja fonte principal foi um relatório da conferência Uptime day 4, organizada por , você pode ver .
Resiliência da arquitetura física
A parte pública da nuvem MCS está agora baseada em dois data centers Tier III, entre eles existe uma fibra escura própria, reservada a nível físico por diferentes rotas, com um débito de 200 Gbit/s. O Tier III fornece o nível necessário de tolerância a falhas para a infraestrutura física.
A fibra escura é reservada tanto no nível físico quanto no lógico. O processo de reserva de canais foi iterativo, surgiram problemas e estamos constantemente melhorando a comunicação entre os data centers.
Por exemplo, não muito tempo atrás, enquanto trabalhava em um poço próximo a um dos data centers, uma escavadeira quebrou um cano, e dentro desse cano havia um cabo óptico principal e um de reserva. Nosso canal de comunicação tolerante a falhas com o data center revelou-se vulnerável em determinado ponto, no poço. Assim, perdemos parte da infraestrutura. Tiramos conclusões e tomamos uma série de ações, incluindo a instalação de óptica adicional no poço adjacente.
Nos data centers existem pontos de presença de provedores de comunicação para os quais transmitimos nossos prefixos via BGP. Para cada direção da rede é selecionada a melhor métrica, o que permite que diferentes clientes tenham a melhor qualidade de conexão. Se a comunicação através de um provedor falhar, reconstruímos nosso roteamento através dos provedores disponíveis.
Se um provedor falhar, mudamos automaticamente para o próximo. Em caso de falha de um dos data centers, temos uma cópia espelhada dos nossos serviços no segundo data center, que assume toda a carga.

Resiliência da infraestrutura física
O que usamos para tolerância a falhas em nível de aplicação
Nosso serviço é baseado em vários componentes de código aberto.
ExaBGP é um serviço que implementa uma série de funções usando o protocolo de roteamento dinâmico baseado em BGP. Nós o usamos ativamente para anunciar nossos endereços IP na lista de permissões por meio dos quais os usuários acessam a API.
HAPROxy é um balanceador de alta carga que permite configurar regras de balanceamento de tráfego muito flexíveis em diferentes níveis do modelo OSI. Nós o usamos para equilibrar todos os serviços: bancos de dados, corretores de mensagens, serviços de API, serviços web, nossos projetos internos - tudo está por trás do HAProxy.
Aplicação API — uma aplicação web escrita em python, com a qual o usuário gerencia sua infraestrutura e seu serviço.
Aplicativo de trabalhador (doravante simplesmente trabalhador) - nos serviços OpenStack, este é um daemon de infraestrutura que permite transmitir comandos de API para a infraestrutura. Por exemplo, a criação do disco ocorre no trabalhador e a solicitação de criação ocorre na API do aplicativo.
Arquitetura de aplicativo OpenStack padrão
A maioria dos serviços desenvolvidos para OpenStack tenta seguir um único paradigma. Um serviço geralmente consiste em 2 partes: API e trabalhadores (executores de backend). Via de regra, uma API é um aplicativo WSGI em python, que é iniciado como um processo independente (daemon) ou usando um servidor web Nginx ou Apache pronto. A API processa a solicitação do usuário e passa instruções adicionais ao aplicativo de trabalho para execução. A transferência ocorre através de um message broker, geralmente RabbitMQ, os demais são mal suportados. Quando as mensagens chegam ao corretor, elas são processadas pelos trabalhadores e, se necessário, retornam uma resposta.
Este paradigma envolve pontos comuns isolados de falha: RabbitMQ e o banco de dados. Mas o RabbitMQ está isolado dentro de um serviço e, em teoria, pode ser individual para cada serviço. Portanto, na MCS separamos esses serviços tanto quanto possível; para cada projeto individual criamos um banco de dados separado, um RabbitMQ separado. Esta abordagem é boa porque em caso de acidente em alguns pontos vulneráveis, nem todo o serviço quebra, mas apenas parte dele.
O número de aplicativos de trabalho é ilimitado, portanto a API pode ser facilmente dimensionada horizontalmente atrás dos balanceadores para aumentar o desempenho e a tolerância a falhas.
Alguns serviços exigem coordenação dentro do serviço quando ocorrem operações sequenciais complexas entre APIs e trabalhadores. Neste caso, é utilizado um único centro de coordenação, um sistema de cluster como Redis, Memcache, etcd, que permite a um trabalhador dizer a outro que esta tarefa está atribuída a ele (“por favor, não aceite”). Usamos etcd. Via de regra, os trabalhadores se comunicam ativamente com o banco de dados, escrevem e leem informações dele. Usamos mariadb como banco de dados, que está localizado em um cluster multimaster.
Este serviço único clássico é organizado de uma maneira geralmente aceita para OpenStack. Pode ser considerado um sistema fechado, para o qual os métodos de escalonamento e tolerância a falhas são bastante óbvios. Por exemplo, para tolerância a falhas de APIs, basta colocar um balanceador na frente delas. O dimensionamento dos trabalhadores é conseguido aumentando o seu número.
O ponto fraco de todo o esquema é RabbitMQ e MariaDB. Sua arquitetura merece um artigo separado. Neste artigo quero me concentrar na tolerância a falhas da API.

Arquitetura de aplicativos Openstack. Balanceamento e tolerância a falhas da plataforma em nuvem
Tornando o balanceador HAProxy tolerante a falhas usando ExaBGP
Para tornar nossas APIs escaláveis, rápidas e tolerantes a falhas, colocamos um balanceador de carga na frente delas. Escolhemos HAProxy. Na minha opinião, possui todas as características necessárias para a nossa tarefa: balanceamento em vários níveis OSI, interface de gerenciamento, flexibilidade e escalabilidade, grande número de métodos de balanceamento, suporte para tabelas de sessão.
O primeiro problema que precisava ser resolvido era a tolerância a falhas do próprio balanceador. A simples instalação de um balanceador também cria um ponto de falha: o balanceador quebra e o serviço trava. Para evitar que isso acontecesse, usamos o HAProxy em conjunto com o ExaBGP.
ExaBGP permite implementar um mecanismo para verificar o estado de um serviço. Utilizamos este mecanismo para verificar a funcionalidade do HAProxy e, em caso de problemas, desabilitar o serviço HAProxy do BGP.
Esquema ExaBGP + HAProxy
- Instalamos o software necessário, ExaBGP e HAProxy, em três servidores.
- Criamos uma interface de loopback em cada servidor.
- Em todos os três servidores atribuímos o mesmo endereço IP branco a esta interface.
- Um endereço IP branco é anunciado na Internet via ExaBGP.
A tolerância a falhas é alcançada anunciando o mesmo endereço IP de todos os três servidores. Do ponto de vista da rede, o mesmo endereço é acessível a partir de três próximos saltos diferentes. O roteador vê três rotas idênticas, seleciona a prioridade mais alta delas com base em sua própria métrica (geralmente é a mesma opção) e o tráfego vai apenas para um dos servidores.
Em caso de problemas com o funcionamento 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 HAProxy
O esquema acabou sendo imperfeito: aprendemos a reservar o HAProxy, mas não aprendemos a distribuir a carga dentro dos serviços. Portanto, ampliamos um pouco esse esquema: passamos ao balanceamento entre vários endereços IP brancos.
Balanceamento baseado em DNS mais BGP
A questão do balanceamento de carga do nosso HAProxy permanece sem solução. No entanto, isso pode ser resolvido de forma bastante simples, como fizemos aqui.
Para equilibrar três servidores você precisará de 3 endereços IP brancos e o bom e velho DNS. Cada um desses endereços é determinado na interface de loopback de cada HAProxy e anunciado na Internet.
No OpenStack, para gerenciar recursos, é usado um diretório de serviço, que especifica a API do endpoint de um determinado serviço. Neste diretório registramos um nome de domínio - public.infra.mail.ru, que é resolvido via DNS por três endereços IP diferentes. Como resultado, obtemos distribuição de carga entre três endereços via DNS.
Mas como ao anunciar endereços IP brancos não controlamos as prioridades de seleção dos servidores, isso ainda não é um equilíbrio. Normalmente, apenas um servidor será selecionado com base na antiguidade do endereço IP e os outros dois ficarão ociosos porque nenhuma métrica é especificada no BGP.
Começamos a enviar rotas via ExaBGP com métricas diferentes. Cada balanceador anuncia todos os três endereços IP brancos, mas um deles, o principal deste balanceador, é anunciado com a métrica mínima. Assim, enquanto todos os três balanceadores estão em operação, as chamadas para o primeiro endereço IP vão para o primeiro balanceador, as chamadas para o segundo para o segundo e as chamadas para o terceiro para o terceiro.
O que acontece quando um dos balanceadores cai? Se algum balanceador falhar, seu endereço principal ainda será anunciado pelos outros dois e o tráfego será redistribuído entre eles. Assim, damos ao usuário vários endereços IP de uma só vez via DNS. Ao balancear por DNS e métricas diferentes, obtemos uma distribuição uniforme da carga entre todos os três balanceadores. E ao mesmo tempo não perdemos a tolerância a falhas.

Balanceamento de HAProxy baseado em DNS + BGP
Interação entre ExaBGP e HAProxy
Assim, implementamos tolerância a falhas caso o servidor saia, baseado na interrupção do anúncio de rotas. Mas o HAProxy pode desligar por outros motivos além da falha do servidor: erros de administração, falhas no serviço. Queremos remover o balanceador quebrado sob a carga também nesses casos e precisamos de um mecanismo diferente.
Portanto, expandindo o esquema anterior, implementamos heartbeat entre ExaBGP e HAProxy. Esta é uma implementação de software da interação entre ExaBGP e HAProxy, quando ExaBGP usa scripts customizados para verificar o status dos aplicativos.
Para fazer isso, você precisa configurar um verificador de integridade na configuração do ExaBGP, que pode verificar o status do HAProxy. No nosso caso, configuramos o backend de integridade no HAProxy e, do lado do ExaBGP, verificamos com uma simples solicitação GET. Se o anúncio parar de acontecer, provavelmente o HAProxy não está funcionando e não há necessidade de anunciá-lo.

Verificação de integridade do HAProxy
Pares HAProxy: sincronização de sessão
A próxima coisa a fazer foi sincronizar as sessões. Ao trabalhar com balanceadores distribuídos, é difícil organizar o armazenamento de informações sobre sessões de clientes. Mas o HAProxy é um dos poucos balanceadores que pode fazer isso devido à funcionalidade Peers - a capacidade de transferir tabelas de sessão entre diferentes processos HAProxy.
Existem diferentes métodos de balanceamento: os mais simples, como , e estendido, quando a sessão do cliente é lembrada, e cada vez ele acaba no mesmo servidor de antes. Queríamos implementar a segunda opção.
HAProxy usa stick-tables para salvar sessões de cliente deste mecanismo. Eles salvam o endereço IP original do cliente, o endereço de destino selecionado (backend) e algumas informações de serviço. Normalmente, as tabelas stick são usadas para armazenar um par IP de origem + IP de destino, o que é especialmente útil para aplicativos que não podem transferir o contexto da sessão do usuário ao alternar para outro balanceador, por exemplo, no modo de balanceamento RoundRobin.
Se uma tabela stick for ensinada a se mover entre diferentes processos HAProxy (entre os quais ocorre o balanceamento), nossos balanceadores serão capazes de trabalhar com um conjunto de tabelas stick. Isso tornará possível alternar perfeitamente a rede do cliente se um dos balanceadores falhar; o trabalho com as sessões do cliente continuará nos mesmos back-ends selecionados anteriormente.
Para um funcionamento adequado, o problema do endereço IP de origem do balanceador a partir do qual a sessão foi estabelecida deve ser resolvido. No nosso caso, este é um endereço dinâmico na interface de loopback.
O trabalho correto dos pares só é alcançado sob certas condições. Ou seja, os tempos limite do TCP devem ser grandes o suficiente ou a comutação deve ser rápida o suficiente para que a sessão TCP não tenha tempo para terminar. No entanto, permite uma comutação perfeita.
No IaaS temos um serviço construído com a mesma tecnologia. Esse , que se chama Otávia. É baseado em dois processos HAProxy e inicialmente inclui suporte para pares. Eles provaram ser excelentes neste serviço.
A imagem mostra esquematicamente o movimento de tabelas peer entre três instâncias HAProxy, é proposta uma configuração sobre como isso pode ser configurado:

HAProxy Peers (sincronização de sessão)
Se você implementar o mesmo esquema, seu funcionamento deverá ser cuidadosamente testado. Não é fato que funcionará da mesma maneira 100% do tempo. Mas pelo menos você não perderá tabelas fixas quando precisar lembrar o IP de origem do cliente.
Limitando o número de solicitações simultâneas do mesmo cliente
Quaisquer serviços disponíveis publicamente, incluindo nossas APIs, podem estar sujeitos a avalanches de solicitações. As razões para isso podem ser completamente diferentes, desde erros do usuário até ataques direcionados. Periodicamente somos alvo de ataques DDoS por endereços IP. Os clientes muitas vezes cometem erros em seus scripts e nos fornecem mini-DDoSs.
De uma forma ou de outra, deve ser fornecida proteção adicional. A solução óbvia é limitar o número de solicitações de API e não desperdiçar tempo de CPU processando solicitações maliciosas.
Para implementar tais restrições, utilizamos limites de taxas, organizados com base no HAProxy, utilizando as mesmas tabelas stick. Configurar limites é bastante simples e permite limitar o usuário pelo número de solicitações à API. O algoritmo lembra o IP de origem a partir do qual as solicitações são feitas e limita o número de solicitações simultâneas de um usuário. Claro, calculamos o perfil médio de carga da API para cada serviço e definimos um limite de ≈ 10 vezes esse valor. Continuamos monitorando de perto a situação e mantendo o controle.
Como é isso na prática? Temos clientes que usam nossas APIs de escalonamento automático o tempo todo. Eles criam aproximadamente duzentas a trezentas máquinas virtuais pela manhã e as excluem à noite. Para o OpenStack, a criação de uma máquina virtual, também com serviços PaaS, requer no mínimo 1000 solicitações de API, já que a interação entre os serviços também ocorre por meio da API.
Essa transferência de tarefas causa uma carga bastante grande. Avaliamos essa carga, coletamos picos diários, aumentamos em dez vezes e esse se tornou nosso limite de taxa. Mantemos o dedo no pulso. Freqüentemente vemos bots e scanners tentando nos observar para ver se temos algum script CGA que possa ser executado, estamos ativamente cortando-os.
Como atualizar sua base de código sem que os usuários percebam
Também implementamos tolerância a falhas no nível dos processos de implantação de código. Pode haver falhas durante as implementações, mas o seu impacto na disponibilidade do serviço pode ser minimizado.
Atualizamos constantemente nossos serviços e devemos garantir que a base de código seja atualizada sem afetar os usuários. Conseguimos resolver este problema utilizando as capacidades de gestão do HAProxy e a implementação do Graceful Shutdown nos nossos serviços.
Para resolver este problema, foi necessário garantir o controle do balanceador e o desligamento “correto” dos serviços:
- No caso do HAProxy, o controle é realizado através de um arquivo de estatísticas, que é essencialmente um soquete e é definido na configuração do HAProxy. Você pode enviar comandos para ele via stdio. Mas nossa principal ferramenta de controle de configuração é ansible, portanto possui um módulo integrado para gerenciamento do HAProxy. Que usamos ativamente.
- A maioria de nossos serviços de API e mecanismo suportam tecnologias de desligamento gracioso: ao desligar, eles aguardam a conclusão da tarefa atual, seja uma solicitação http ou alguma tarefa de serviço. A mesma coisa acontece com o trabalhador. Ele conhece todas as tarefas que está realizando e termina quando conclui tudo com sucesso.
Graças a esses dois pontos, o algoritmo seguro para nossa implantação fica assim.
- O desenvolvedor monta um novo pacote de código (para nós é RPM), testa-o no ambiente de desenvolvimento, testa-o no palco e deixa-o no repositório do palco.
- O desenvolvedor define a tarefa de 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. Inicia o manual do Ansible, que por sua vez faz o seguinte:
- Pega um pacote do repositório de teste e o utiliza para atualizar a versão do pacote no repositório do produto.
- Compila uma lista de back-ends do serviço atualizado.
- Encerra o primeiro serviço a ser atualizado no HAProxy e aguarda a conclusão da execução de seus processos. Graças ao encerramento normal, estamos confiantes de que todas as solicitações atuais dos clientes serão concluídas com sucesso.
- Depois que a API e os trabalhadores forem completamente interrompidos e o HAProxy desativado, o código será atualizado.
- Ansible executa serviços.
- Para cada serviço, são puxados certos “alçadores”, que realizam testes unitários em uma série de testes-chave predefinidos. É realizada uma verificação básica do novo código.
- Se nenhum erro for encontrado na etapa anterior, o backend será ativado.
- Vamos passar para o próximo back-end.
- Depois que todos os back-ends forem atualizados, os testes funcionais serão lançados. Se estiverem faltando, o desenvolvedor analisa qualquer nova funcionalidade que ele criou.
Isso conclui a implantação.

Ciclo de atualização de serviço
Este esquema não funcionaria se não tivéssemos uma regra. Apoiamos as versões antigas e novas em batalha. Antecipadamente, na fase de desenvolvimento do software, está previsto que mesmo que haja alterações na base de dados do serviço, elas não quebrarão o código anterior. Como resultado, a base de código é atualizada gradualmente.
Conclusão
Compartilhando meus próprios pensamentos sobre uma arquitetura WEB tolerante a falhas, gostaria mais uma vez de observar seus pontos principais:
- tolerância a falhas físicas;
- tolerância a falhas de rede (balanceadores, BGP);
- tolerância a falhas do software utilizado e desenvolvido.
Tempo de atividade estável para todos!
Fonte: habr.com
