Bioyino - agregador de métricas distribuídas e escaláveis

Então você coleta métricas. Como nós somos. Também coletamos métricas. Claro, necessário para os negócios. Hoje falaremos sobre o primeiro link do nosso sistema de monitoramento - um servidor de agregação compatível com statsd bioyino, por que o escrevemos e por que abandonamos Brubeck.

Bioyino - agregador de métricas distribuídas e escaláveis

De nossos artigos anteriores (1, 2) você pode descobrir que até algum tempo coletávamos marcas usando Brubeque. Está escrito em C. Do ponto de vista do código, é tão simples quanto um plug (isso é importante quando você deseja contribuir) e, o mais importante, lida com nossos volumes de 2 milhões de métricas por segundo (MPS) no pico sem quaisquer problemas. A documentação indica suporte para 4 milhões de MPS com um asterisco. Isso significa que você obterá o valor indicado se configurar a rede corretamente no Linux. (Não sabemos quantos MPS você pode obter se deixar a rede como está). Apesar dessas vantagens, tivemos diversas reclamações sérias sobre Brubeck.

Reivindicação 1. O Github, desenvolvedor do projeto, parou de apoiá-lo: publicando patches e correções, aceitando o nosso e (não apenas o nosso) PR. Nos últimos meses (de fevereiro a março de 2018), a atividade foi retomada, mas antes disso foram quase 2 anos de calma total. Além disso, o projeto está sendo desenvolvido para necessidades internas do Gihub, o que pode se tornar um sério obstáculo à introdução de novos recursos.

Reivindicação 2. Precisão dos cálculos. Brubeck coleta um total de 65536 valores para agregação. No nosso caso, para algumas métricas, durante o período de agregação (30 segundos), podem chegar muito mais valores (1 no pico). Como resultado desta amostragem, os valores máximo e mínimo parecem inúteis. Por exemplo, assim:

Bioyino - agregador de métricas distribuídas e escaláveis
Como foi

Bioyino - agregador de métricas distribuídas e escaláveis
Como deveria ter sido

Pela mesma razão, os montantes são geralmente calculados incorretamente. Adicione aqui um bug com um float overflow de 32 bits, que geralmente envia o servidor para segfault ao receber uma métrica aparentemente inocente, e tudo fica ótimo. O bug, aliás, não foi corrigido.

E, finalmente, Reivindicar X. No momento em que este artigo foi escrito, estamos prontos para apresentá-lo a todas as 14 implementações de statsd mais ou menos funcionais que conseguimos encontrar. Vamos imaginar que alguma infraestrutura única tenha crescido tanto que aceitar 4 milhões de MPS não seja mais suficiente. Ou mesmo que ainda não tenha crescido, mas as métricas já são tão importantes para você que mesmo quedas curtas de 2 a 3 minutos nos gráficos já podem se tornar críticas e causar crises de depressão intransponível entre os gestores. Como tratar a depressão é uma tarefa ingrata, são necessárias soluções técnicas.

Em primeiro lugar, tolerância a falhas, para que um problema repentino no servidor não provoque um apocalipse zumbi psiquiátrico no escritório. Em segundo lugar, dimensionar para poder aceitar mais de 4 milhões de MPS, sem se aprofundar na pilha de rede Linux e crescendo calmamente “em amplitude” até o tamanho necessário.

Como tínhamos espaço para dimensionamento, decidimos começar com tolerância a falhas. "SOBRE! Tolerância ao erro! É simples, podemos fazer isso”, pensamos e lançamos 2 servidores, levantando uma cópia do brubeck em cada um. Para fazer isso, tivemos que copiar o tráfego com métricas para ambos os servidores e até escrever para isso pequeno utilitário. Resolvemos o problema de tolerância a falhas com isso, mas... não muito bem. No início tudo parecia ótimo: cada Brubeck coleta sua própria versão de agregação, grava os dados no Graphite uma vez a cada 30 segundos, substituindo o intervalo antigo (isso é feito no lado do Graphite). Se um servidor falhar repentinamente, sempre teremos um segundo com sua própria cópia dos dados agregados. Mas aqui está o problema: se o servidor falhar, uma “serra” aparece nos gráficos. Isso se deve ao fato de que os intervalos de 30 segundos do Brubeck não são sincronizados e, no momento da colisão, um deles não é sobrescrito. Quando o segundo servidor é iniciado, acontece a mesma coisa. Bastante tolerável, mas quero melhor! O problema da escalabilidade também não desapareceu. Todas as métricas ainda “voam” para um único servidor e, portanto, estamos limitados aos mesmos 2 a 4 milhões de MPS, dependendo do nível da rede.

Se você pensar um pouco sobre o problema e ao mesmo tempo desenterrar neve com uma pá, a seguinte ideia óbvia pode vir à sua mente: você precisa de um statsd que possa funcionar em modo distribuído. Ou seja, aquele que implementa sincronização entre nós em tempo e métricas. “É claro que essa solução provavelmente já existe”, dissemos e fomos ao Google…. E eles não encontraram nada. Depois de passar pela documentação para diferentes statsd (https://github.com/etsy/statsd/wiki#server-implementations em 11.12.2017 de dezembro de XNUMX), não encontramos absolutamente nada. Aparentemente, nem os desenvolvedores nem os usuários dessas soluções encontraram tantas métricas ainda, caso contrário, eles definitivamente criariam algo.

E então nos lembramos do “brinquedo” statsd - bioyino, que foi escrito no hackathon Just for Fun (o nome do projeto foi gerado pelo script antes do início do hackathon) e percebemos que precisávamos urgentemente de nosso próprio statsd. Para que?

  • porque existem poucos clones statsd no mundo,
  • porque é possível fornecer a tolerância a falhas e escalabilidade desejada ou próxima da desejada (incluindo a sincronização de métricas agregadas entre servidores e a resolução do problema de envio de conflitos),
  • porque é possível calcular métricas com mais precisão do que Brubeck,
  • porque você mesmo pode coletar estatísticas mais detalhadas, que Brubeck praticamente não nos forneceu,
  • porque tive a oportunidade de programar minha própria aplicação de escala distribuída de hiperdesempenho, que não repetirá completamente a arquitetura de outra hiperforça semelhante... bem, é isso.

Sobre o que escrever? Claro, em Rust. Por que?

  • porque já havia uma solução protótipo,
  • porque o autor do artigo já conhecia Rust naquela época e estava ansioso para escrever algo nele para produção com a oportunidade de colocá-lo em código aberto,
  • porque os idiomas com GC não são adequados para nós devido à natureza do tráfego recebido (quase em tempo real) e as pausas do GC são praticamente inaceitáveis,
  • porque você precisa de desempenho máximo comparável ao C
  • porque Rust nos fornece simultaneidade destemida, e se começássemos a escrevê-lo em C/C++, teríamos acumulado ainda mais vulnerabilidades, buffer overflows, condições de corrida e outras palavras assustadoras do que brubeck.

Houve também um argumento contra Rust. A empresa não tinha experiência na criação de projetos em Rust e agora também não planejamos utilizá-lo no projeto principal. Portanto, havia sérios temores de que nada desse certo, mas decidimos arriscar e tentamos.

Tempo passou...

Finalmente, após várias tentativas fracassadas, a primeira versão funcional ficou pronta. O que aconteceu? Isso é o que aconteceu.

Bioyino - agregador de métricas distribuídas e escaláveis

Cada nó recebe seu próprio conjunto de métricas e as acumula, e não agrega métricas para os tipos onde seu conjunto completo é necessário para a agregação final. Os nós são conectados entre si por algum tipo de protocolo de bloqueio distribuído, que permite selecionar entre eles o único (aqui choramos) que vale a pena enviar métricas ao Grande. Este problema está sendo resolvido atualmente por Cônsul, mas no futuro as ambições do autor estendem-se a possuir implementação Jangada, onde o mais digno será, claro, o nó líder do consenso. Além do consenso, os nós frequentemente (uma vez por segundo por padrão) enviam aos seus vizinhos as partes das métricas pré-agregadas que conseguiram coletar naquele segundo. Acontece que o escalonamento e a tolerância a falhas são preservados - cada nó ainda contém um conjunto completo de métricas, mas as métricas são enviadas já agregadas, via TCP e codificadas em um protocolo binário, de modo que os custos de duplicação são significativamente reduzidos em comparação ao UDP. Apesar do número bastante grande de métricas recebidas, a acumulação requer muito pouca memória e ainda menos CPU. Para nossos mertics altamente compressíveis, isso representa apenas algumas dezenas de megabytes de dados. Como bônus adicional, não obtemos reescritas desnecessárias de dados no Graphite, como foi o caso do Burbeck.

Pacotes UDP com métricas são desequilibrados entre nós em equipamentos de rede através de um simples Round Robin. É claro que o hardware da rede não analisa o conteúdo dos pacotes e, portanto, pode extrair muito mais do que 4 milhões de pacotes por segundo, sem mencionar as métricas sobre as quais ele nada sabe. Se levarmos em conta que as métricas não vêm uma de cada vez em cada pacote, então não prevemos problemas de desempenho neste local. Se um servidor travar, o dispositivo de rede rapidamente (dentro de 1 a 2 segundos) detecta esse fato e remove o servidor travado da rotação. Como resultado disso, nós passivos (ou seja, não líderes) podem ser ativados e desativados praticamente sem notar rebaixamentos nos gráficos. O máximo que perdemos faz parte das métricas que chegaram no último segundo. Uma perda/desligamento/troca repentina de um líder ainda criará uma pequena anomalia (o intervalo de 30 segundos ainda está fora de sincronia), mas se houver comunicação entre os nós, esses problemas podem ser minimizados, por exemplo, enviando pacotes de sincronização .

Um pouco sobre a estrutura interna. O aplicativo é, obviamente, multithread, mas a arquitetura de threading é diferente daquela usada em Brubeck. Os threads em brubeck são os mesmos - cada um deles é responsável pela coleta e agregação de informações. No bioyino, os trabalhadores são divididos em dois grupos: os responsáveis ​​pela rede e os responsáveis ​​pela agregação. Esta divisão permite gerenciar a aplicação com mais flexibilidade dependendo do tipo de métrica: onde é necessária agregação intensiva, você pode adicionar agregadores, onde há muito tráfego de rede, você pode adicionar o número de fluxos de rede. No momento, em nossos servidores trabalhamos em 8 redes e 4 fluxos de agregação.

A parte da contagem (responsável pela agregação) é bastante chata. Os buffers preenchidos pelos fluxos de rede são distribuídos entre os fluxos de contagem, onde são posteriormente analisados ​​e agregados. Mediante solicitação, as métricas são fornecidas para envio a outros nós. Tudo isso, incluindo o envio de dados entre nós e o trabalho com o Consul, é feito de forma assíncrona, rodando no framework Tóquio.

Muito mais problemas durante o desenvolvimento foram causados ​​pela parte da rede responsável por receber as métricas. O principal objetivo de separar os fluxos de rede em entidades separadas era o desejo de reduzir o tempo que um fluxo gasta não para ler dados do soquete. As opções que usavam UDP assíncrono e recvmsg regular desapareceram rapidamente: a primeira consome muita CPU do espaço do usuário para processamento de eventos, a segunda requer muitas alternâncias de contexto. Portanto agora é usado recvmmsg com grandes amortecedores (e amortecedores, senhores oficiais, não são nada para vocês!). O suporte para UDP regular é reservado para casos leves em que recvmmsg não é necessário. No modo multimensagem, é possível alcançar o principal: na grande maioria das vezes, o thread de rede vasculha a fila do sistema operacional - lê os dados do soquete e os transfere para o buffer do espaço do usuário, apenas ocasionalmente alternando para fornecer o buffer preenchido para agregadores. A fila no soquete praticamente não acumula, o número de pacotes descartados praticamente não aumenta.

Nota

Nas configurações padrão, o tamanho do buffer é definido como bastante grande. Se de repente você decidir experimentar o servidor sozinho, poderá se deparar com o fato de que, após enviar um pequeno número de métricas, elas não chegarão ao Graphite, permanecendo no buffer de fluxo da rede. Para trabalhar com um pequeno número de métricas, você precisa definir bufsize e task-queue-size para valores menores na configuração.

Finalmente, alguns gráficos para os amantes de gráficos.

Estatísticas sobre o número de métricas recebidas para cada servidor: mais de 2 milhões de MPS.

Bioyino - agregador de métricas distribuídas e escaláveis

Desativando um dos nós e redistribuindo as métricas recebidas.

Bioyino - agregador de métricas distribuídas e escaláveis

Estatísticas sobre métricas de saída: apenas um nó sempre envia - o chefe do ataque.

Bioyino - agregador de métricas distribuídas e escaláveis

Estatísticas de funcionamento de cada nó, levando em consideração erros em diversos módulos do sistema.

Bioyino - agregador de métricas distribuídas e escaláveis

Detalhamento das métricas recebidas (os nomes das métricas estão ocultos).

Bioyino - agregador de métricas distribuídas e escaláveis

O que planejamos fazer com tudo isso a seguir? Claro, escreva código, caramba...! O projeto foi originalmente planejado para ser de código aberto e assim permanecerá durante toda a sua vida. Nossos planos imediatos incluem mudar para nossa própria versão do Raft, mudar o protocolo peer para um mais portátil, introduzir estatísticas internas adicionais, novos tipos de métricas, correções de bugs e outras melhorias.

Claro que todos são bem-vindos para ajudar no desenvolvimento do projeto: criar PR, Issues, se possível responderemos, melhoraremos, etc.

Dito isso, é tudo pessoal, comprem nossos elefantes!



Fonte: habr.com

Adicionar um comentário