Este artigo contém um breve extrato de minha própria experiência e da experiência de meus colegas, com quem passei dias e noites investigando incidentes. E muitos incidentes nunca teriam ocorrido se todos gostassem de seus microsserviços, pelo menos um pouco mais precisamente.
Infelizmente, alguns programadores de baixo nível acreditam seriamente que um arquivo Docker com algum tipo de comando interno é um microsserviço e pode ser implantado agora. Os estivadores estão girando, o banco está lamacento. Essa abordagem está repleta de problemas que variam de queda no desempenho, incapacidade de depuração e negação de serviço a um pesadelo chamado Inconsistência de Dados.
Se você acha que chegou a hora de lançar outro aplicativo no Kubernetes / ECS / o que for, então tenho algo a que me opor.
Versão em inglês também está disponível .
Formei para mim um certo conjunto de critérios para avaliar a prontidão dos aplicativos para lançamento na produção. Alguns pontos desta lista de verificação não podem ser aplicados a todos os aplicativos, mas apenas aos especiais. Outros geralmente se aplicam a tudo. Tenho certeza de que você pode adicionar suas opções nos comentários ou contestar alguns desses pontos.
Se o seu microsserviço não atender a pelo menos um dos critérios, não permitirei que ele esteja no meu cluster ideal, construído em um bunker a 2000 metros de profundidade, com piso aquecido e um sistema fechado de suprimento de Internet.
Vamos lá ....
Nota: a ordem dos itens não importa. Enfim, para mim.
Leia-me Breve Descrição
Ele contém uma breve descrição de si mesmo no início do Readme.md em seu repositório.
Deus, parece tão simples. Mas com que frequência me deparei com que o repositório não contém a menor explicação sobre por que é necessário, quais tarefas ele resolve e assim por diante. Não há necessidade de falar sobre algo mais complicado, como opções de configuração.
Integração com um sistema de monitoramento
Envia métricas para DataDog, NewRelic, Prometheus e assim por diante.
Análise do consumo de recursos, vazamentos de memória, rastreamento de pilha, interdependência de serviços, taxa de erros - sem entender tudo isso (e não apenas), é extremamente difícil controlar o que está acontecendo em um aplicativo distribuído grande.
Alertas configurados
O serviço inclui alertas que cobrem todas as situações padrão, além de situações exclusivas conhecidas.
As métricas são boas, mas ninguém as seguirá. Portanto, recebemos automaticamente chamadas / push / sms se:
- O consumo de CPU / memória aumentou drasticamente.
- O tráfego aumentou / caiu acentuadamente.
- O número de transações processadas por segundo mudou dramaticamente em qualquer direção.
- O tamanho do artefato após a montagem mudou drasticamente (exe, app, jar, ...).
- A porcentagem de erros ou sua frequência excedeu o limite permitido.
- O serviço parou de enviar métricas (situação geralmente ignorada).
- A regularidade de certos eventos esperados é violada (o trabalho do cron não funciona, nem todos os eventos são processados etc.)
- ...
Runbooks criados
Um documento foi criado para o serviço que descreve contingências conhecidas ou esperadas.
- como garantir que o erro seja interno e não dependa de terceiros;
- se depende de onde, para quem e o que escrever;
- como reiniciar com segurança;
- como restaurar do backup e onde estão os backups;
- Quais painéis / consultas especiais são criados para monitorar este serviço;
- O serviço tem seu próprio painel de administração e como chegar lá;
- existe uma API / CLI e como usá-la para corrigir problemas conhecidos;
- e assim por diante.
A lista pode variar muito entre as organizações, mas pelo menos as coisas básicas devem estar lá.
Todos os logs são gravados em STDOUT / STDERR
O serviço não cria nenhum arquivo de log no modo de produção, não os envia para nenhum serviço externo, não contém abstrações redundantes para rotação de logs etc.
Quando um aplicativo cria arquivos de log, esses logs são inúteis. Você não entrará em cinco contêineres rodando em paralelo, esperando capturar o erro de que precisa (e aqui está, chorando ...). Reiniciar o contêiner resultará em uma perda completa desses logs.
Se um aplicativo gravar seus próprios logs em um sistema de terceiros, por exemplo, no Logstash, isso criará redundância inútil. O serviço vizinho não sabe como fazer isso, porque ele tem uma estrutura diferente? Você ganha um zoológico.
O aplicativo grava parte dos logs nos arquivos e parte no stdout porque é conveniente para o desenvolvedor ver INFO no console e DEBUG nos arquivos? Geralmente é a pior opção. Ninguém precisa de complexidade e código e configurações completamente redundantes que você precisa conhecer e manter.
Os logs são Json
Cada linha de log é gravada no formato Json e contém um conjunto consistente de campos
Até agora, quase todo mundo grava logs em texto simples. Este é um verdadeiro desastre. Eu ficaria feliz em nunca saber sobre Grok Patterns . Às vezes sonho com eles e congelo, tentando não me mexer, para não atrair a atenção deles. Apenas tente analisar as exceções Java nos logs uma vez.
Json é bom, é fogo dado do céu. Basta adicionar lá:
- registro de data e hora em milissegundos de acordo com a RFC 3339 ;
- nível: informações, aviso, erro, depuração
- user_id;
- app_name
- e outros campos.
Faça o download para qualquer sistema adequado (ElasticSearch corretamente configurado, por exemplo) e aproveite. Conecte os logs de muitos microsserviços e sinta novamente quais eram as boas aplicações monolíticas.
(E você pode adicionar o Request-Id e obter o rastreamento ...)
Logs com níveis de verbosidade
O aplicativo deve suportar uma variável de ambiente, por exemplo, LOG_LEVEL, com pelo menos dois modos de operação: ERRORS e DEBUG.
É desejável que todos os serviços no mesmo ecossistema suportem a mesma variável de ambiente. Não é uma opção de configuração, não é uma opção na linha de comando (embora isso seja reversível, é claro), mas imediatamente por padrão no ambiente. Você deve conseguir o maior número possível de logs se algo der errado e o menor número possível de logs, se tudo estiver bem.
Versões de dependência fixa
As dependências para gerenciadores de pacotes são corrigidas, incluindo versões secundárias (por exemplo, cool_framework = 2.5.3).
Isso já foi discutido muito, é claro. Algumas corrigem dependências nas versões principais, esperando que apenas pequenas correções de bugs e segurança estejam em versões menores. Isto está errado.
Cada alteração em cada dependência deve ser refletida em uma confirmação separada . Para que possa ser cancelado em caso de problemas. É difícil controlar com as mãos? Existem robôs úteis, como este , que acompanharão as atualizações e criarão solicitações pull para cada um de vocês.
Dockerized
O repositório contém Dockerfile pronto para produção e docker-compose.yml
O Docker tem sido o padrão para muitas empresas. Existem exceções, mas mesmo que você não tenha o Docker em produção, qualquer engenheiro deve ser capaz de compor o docker e não pensar em mais nada para obter um assembly de desenvolvimento para verificação local. E o administrador do sistema deve ter o assembly já verificado pelos desenvolvedores com as versões necessárias de bibliotecas, utilitários etc., nas quais o aplicativo, pelo menos de alguma forma, trabalha para adaptá-lo à produção.
Configuração do ambiente
Todas as opções importantes de configuração são lidas no ambiente e o ambiente tem precedência maior que os arquivos de configuração (mas menor que os argumentos da linha de comandos na inicialização).
Ninguém nunca vai querer ler seus arquivos de configuração e estudar seu formato. Apenas aceite.
Mais detalhes aqui: https://12factor.net/config
Sondas de prontidão e vivacidade
Contém terminais ou comandos cli apropriados para testar a prontidão para atender a solicitações na inicialização e no tempo de atividade ao longo da vida.
Se o aplicativo atender solicitações HTTP, ele deverá ter duas interfaces por padrão:
Para verificar se o aplicativo está ativo e sem congelamento, é usado um teste de vida. Se o aplicativo não responder, ele poderá ser automaticamente interrompido por orquestradores como o Kubernetes, " mas isso não é exato ". De fato, matar um aplicativo congelado pode causar um efeito dominó e colocar seu serviço permanentemente. Mas este não é um problema do desenvolvedor, basta fazer este endpoint.
Para verificar se o aplicativo não foi iniciado apenas, mas está pronto para aceitar solicitações, é realizado um teste de prontidão. Se o aplicativo estabeleceu uma conexão com o banco de dados, o sistema de filas e assim por diante, ele deve responder com um status de 200 a 400 (para o Kubernetes).
Limites de recursos
Contém limites para o consumo de memória, CPU, espaço em disco e quaisquer outros recursos disponíveis em um formato consistente.
A implementação específica deste item será muito diferente em diferentes organizações e para diferentes orquestradores. No entanto, esses limites devem ser definidos em um único formato para todos os serviços, diferentes para diferentes ambientes (prod, dev, test, ...) e estar fora do repositório com o código do aplicativo .
A montagem e entrega são automatizadas
O sistema de CI / CD usado em sua organização ou projeto está configurado e pode entregar o aplicativo no ambiente desejado de acordo com o fluxo de trabalho aceito.
Nada é entregue à produção manualmente.
Não importa quão difícil seja automatizar a montagem e entrega do seu projeto, isso deve ser feito antes que o projeto entre em produção. Este item inclui a criação e execução de livros de receitas Ansible / Chef / Salt / ..., a criação de aplicativos para dispositivos móveis, a criação de uma bifurcação do sistema operacional, a criação de imagens de máquinas virtuais, qualquer que seja.
Não consegue automatizar? Então você não pode publicar isso no mundo. Depois de você, ninguém irá coletá-lo.
Desligamento gracioso - desligamento correto
O aplicativo pode processar o SIGTERM e outros sinais e interromper sistematicamente seu trabalho após o término do processamento da tarefa atual.
Este é um ponto extremamente importante. Os processos do Docker ficam órfãos e funcionam por meses em segundo plano, onde ninguém os vê. As operações não transacionais são interrompidas no meio da execução, criando inconsistência de dados entre serviços e bancos de dados. Isso leva a erros que não podem ser previstos e podem ser muito, muito caros.
Se você não controla nenhuma dependência e não pode garantir que seu código processe corretamente o SIGTERM, use algo como dumb-init .
Mais informações aqui:
Conexão com o banco de dados verificada regularmente
O aplicativo envia um ping ao banco de dados constantemente e responde automaticamente à exceção de "perda de conexão" para qualquer solicitação, tentando restaurá-lo por conta própria ou termina seu trabalho corretamente
Vi muitos casos (isso não é apenas um discurso) quando os serviços criados para processar filas ou eventos perderam a conexão com o tempo limite e começaram a despejar infinitamente erros nos logs, retornando mensagens às filas, enviando-os para a fila de devoluções ou simplesmente não fazendo seu trabalho.
Dimensionado horizontalmente
Com o aumento da carga, é suficiente executar mais instâncias de aplicativos para garantir que todas as solicitações ou tarefas sejam processadas.
Nem todos os aplicativos podem ser dimensionados horizontalmente. Um exemplo impressionante é o Consumidor Kafka . Isso não é necessariamente ruim, mas se um aplicativo específico não puder ser iniciado duas vezes, todas as partes interessadas precisam saber sobre isso com antecedência. Esta informação deve ser desagradável, pendure no Leia-me e sempre que possível. Em geral, alguns aplicativos não podem ser iniciados em paralelo, o que cria sérias dificuldades em seu suporte.
É muito melhor se o aplicativo em si controla essas situações ou se um wrapper monitora efetivamente "concorrentes" e simplesmente não permite que o processo inicie ou inicie o trabalho até que outro processo conclua seu trabalho ou até que alguma configuração externa permita que N processos funcionem simultaneamente.
Filas de cartas não entregues e resiliência de mensagens ruins
Se o serviço ouvir filas ou responder a eventos, alterar o formato ou o conteúdo das mensagens não causará sua queda. Tentativas malsucedidas de processar a tarefa são repetidas N vezes, após as quais a mensagem é enviada para a Fila de mensagens não entregues.
Muitas vezes vi consumidores e linhas reiniciarem incessantemente a um tamanho que o processamento subsequente levou muitos dias. Qualquer ouvinte de fila deve estar preparado para alterar o formato, para erros aleatórios na própria mensagem (digitando dados em json, por exemplo) ou quando é processado pelo código filho. Eu até me deparei com uma situação em que a biblioteca RabbitMQ padrão para uma estrutura extremamente popular não suportava tentativas, contadores de tentativas etc.
Pior ainda, quando uma mensagem é simplesmente destruída em caso de falha.
Limitação no número de mensagens e tarefas processadas por processo
Ele suporta uma variável de ambiente, que pode ser forçada a limitar o número máximo de tarefas processadas, após o qual o serviço será desligado corretamente.
Tudo flui, tudo muda, especialmente a memória. O gráfico continuamente crescente de consumo de memória e OOM Killed no final é a norma nas mentes cúbicas modernas. A implementação de um teste primitivo que simplesmente salvaria você, mesmo a necessidade de examinar todos esses vazamentos de memória, facilitaria a vida. Vi muitas vezes pessoas gastando muito tempo e esforço (e dinheiro) para interromper essa rotatividade, mas não há garantias de que o próximo compromisso do seu colega não piorará. Se o aplicativo pode sobreviver uma semana - este é um ótimo indicador. Deixe-o terminar e reiniciar. Isso é melhor que o SIGKILL (sobre o SIGTERM, veja acima) ou a exceção "falta de memória". Por algumas décadas, esse plugue é suficiente para você.
Não usa integração de terceiros com filtragem por endereços IP
Se o aplicativo fizer solicitações para um serviço de terceiros que permita o acesso a partir de endereços IP limitados, o serviço executará essas chamadas indiretamente por meio de um proxy reverso.
Este é um caso raro, mas extremamente desagradável. É muito inconveniente quando um pequeno serviço bloqueia a possibilidade de alterar o cluster ou mover toda a infraestrutura para outra região. Se você precisar se comunicar com alguém que não sabe usar oAuth ou VPN, configure o proxy reverso antecipadamente. Não implemente em seu programa a adição / remoção dinâmica de integrações externas, pois, ao fazer isso, você se fixa no único tempo de execução disponível. É melhor automatizar imediatamente esses processos para gerenciar as configurações do Nginx e, em seu aplicativo, entre em contato com ele.
Agente de usuário HTTP óbvio
O serviço substitui o cabeçalho User-agent por um personalizado para todas as solicitações de qualquer API, e esse cabeçalho contém informações suficientes sobre o próprio serviço e sua versão.
Quando você tem 100 aplicativos diferentes conversando, pode ficar louco vendo nos logs algo como "Go-http-client / 1.1" e o endereço IP dinâmico do contêiner do Kubernetes. Sempre identifique seu aplicativo e sua versão explicitamente.
Não viola a licença
Ele não contém dependências que limitam excessivamente o aplicativo, não é uma cópia do código de outra pessoa e assim por diante.
Este é um caso evidente, mas aconteceu que até o advogado que escreveu a NDA agora soluça.
Não usa dependências não suportadas
Quando você inicia o serviço, ele não inclui dependências que já estão desatualizadas.
Se a biblioteca que você levou para o projeto não for mais suportada por ninguém - procure outra maneira de atingir a meta ou desenvolver a própria biblioteca.
Conclusão
Existem algumas verificações muito específicas na minha lista para tecnologias ou situações específicas, mas esqueci de adicionar algo. Tenho certeza que você também encontrará algo para se lembrar.