Há um período crítico no ciclo de lançamento de um serviço - desde o momento em que uma nova versão é preparada até o momento em que fica disponível para os usuários. As ações da equipe entre esses dois pontos de controle devem ser consistentes de liberação para liberação e, se possível, automatizadas. Em seu relatório, o albergue de Sergey Pomazanov descreveu os processos que seguem cada solicitação de pool do Yandex.Taxi.
Boa noite! Meu nome é Sergey, sou o chefe do grupo de automação da Yandex.Taxi. Em suma, a principal tarefa do nosso grupo é minimizar o tempo que os desenvolvedores gastam na solução de seus problemas. Isso inclui tudo, desde o CI até os processos de desenvolvimento e teste.
O que nosso desenvolvimento faz quando o código é escrito?
Para testar a nova funcionalidade, primeiro verificamos tudo localmente. Para testes locais, temos um grande conjunto de testes. Se um novo código aparecer, ele também precisará ser coberto com testes.

Nossa cobertura de teste não é tão boa quanto gostaríamos, mas tentamos mantê-la em um nível suficiente.
Para o teste, usamos o Google Test e uma estrutura auto-escrita pytest, com a qual testamos não apenas a parte “python”, mas também a parte “plus”. Nossa estrutura permite que você inicie serviços, faça upload de dados no banco de dados antes de cada teste, atualize caches, limpe todas as solicitações externas etc. Uma estrutura suficientemente funcional que permite que você execute o que quiser, bloqueie qualquer coisa para que não possamos obter acidentalmente pedidos fora.
Além dos testes funcionais, temos testes de integração. Eles permitem que você resolva outro problema. Se você não tiver certeza de que seu serviço irá interagir corretamente com outros serviços, poderá executar o suporte e executar um conjunto de testes. Até agora, temos um conjunto básico de testes, mas ele está se expandindo lentamente.
O suporte é construído com a tecnologia do Docker e do Docker Compose, em que cada contêiner possui seus próprios serviços e todos interagem entre si. Isso acontece em um ambiente isolado. Eles têm sua própria rede isolada, seu próprio banco de dados, seu próprio conjunto de dados. E os testes passam dessa maneira, como se alguém estivesse iniciando um aplicativo móvel, clica em botões, faz um pedido. Nesse momento, os carros virtuais dirigem, trazem o passageiro, o dinheiro é debitado do passageiro e assim por diante. Basicamente, todos os testes exigem a interação de muitos serviços e componentes ao mesmo tempo.
Naturalmente, testamos apenas nossos serviços e apenas nossos componentes, porque não devemos testar serviços externos e molhamos tudo externo.
O suporte era conveniente o suficiente para operar localmente e pegar um táxi de bolso. Você pode assumir essa posição, executá-la em uma máquina local ou virtual ou em qualquer outra máquina de desenvolvimento. Após o lançamento do suporte, você pode pegar um aplicativo móvel adaptado para um táxi de bolso, configurá-lo no seu computador e fazer pedidos. Tudo é exatamente o mesmo que na produção ou em outro lugar. Se você precisar testar a nova funcionalidade, basta inserir seu código, ele será ativado e executado em todo o ambiente.
Novamente, você pode simplesmente executar e executar o serviço desejado. Para fazer isso, você precisa elevar o banco de dados, preenchê-lo com o conteúdo necessário ou obter uma base dos ambientes existentes e conectá-lo ao serviço. E então você pode simplesmente contatá-lo, fazer algumas perguntas, ver se funciona corretamente ou não.
Outro ponto importante é a verificação de estilo. Se tudo é simples para as vantagens, usamos o formato clang e verificamos se o código corresponde ou não; em Python, usamos até quatro analisadores: Flake8, Pylint, Mypy e, ao que parece, autopep8.
Usamos esses analisadores principalmente na entrega padrão. Se houver uma oportunidade de escolher algum estilo de design, usaremos o estilo do Google. A única coisa que corrigimos adicionando as nossas próprias é uma verificação para classificar as importações, para que as importações sejam classificadas corretamente.
Depois de criar o código, verificado localmente, você pode fazer uma solicitação de pool. Solicitações de pool são criadas no GitHub.

Criar uma solicitação de pool no GitHub oferece muitas oportunidades que o TeamCity nos fornece. O TeamCity executa automaticamente todos os testes mencionados acima, os verifica automaticamente e, no próprio pedido de pool, está escrito sobre o status da passagem, independentemente de os testes terem sido aprovados ou não. Ou seja, sem visitar o TeamCity, você pode ver se foi aprovado ou não e clicando no link para entender o que deu errado e o que precisa ser corrigido.
Se você não possui táxis e testes de bolso suficientes, deseja verificar a interação real com algum serviço real, temos um ambiente de teste que repete a produção. Temos dois desses ambientes de teste. Um é para desenvolvimento móvel de testadores e o segundo é para desenvolvedores. O ambiente de teste é o mais próximo possível da produção e, se forem feitas solicitações a serviços externos, elas também serão feitas nos ambientes de teste. A única limitação é que o ambiente de teste vai para o teste de recursos externos sempre que possível. E o ambiente de produção entra em produção.
Mais sobre o ambiente de teste, fazemos isso simplesmente através do TeamCity. É necessário colocar o rótulo apropriado no GitHub e, depois de definido, clique no botão "Coletar personalizado". Então chamamos isso. Todas as solicitações de pool com esse rótulo continuarão e o conjunto automático de pacotes com armazenamento em cluster começará.
Além dos testes de rotina, o teste de carga às vezes é necessário. Se você estiver editando código que faz parte de um serviço altamente carregado, podemos fazer testes de carregamento para isso. No Python, existem poucos serviços altamente carregados, alguns deles que reescrevemos em C ++, mas, mesmo assim, eles ainda permanecem, às vezes existe um lugar para estar. O teste de carga ocorre através do sistema Lunapark. Ele usa o Yandex.Tank, está disponível gratuitamente, você pode baixar e assistir. O tanque permite disparar em algum serviço, criar gráficos, executar diferentes métodos de carregamento e mostrar qual carga estava atualmente no serviço e quais recursos foram utilizados. Basta clicar no botão no TeamCity, o pacote será coletado e, em seguida, será possível rolá-lo quando necessário. Ou simplesmente preencha e execute manualmente.

Enquanto você está testando seu código, um dos desenvolvedores pode, neste momento, começar a analisar seu código e participar da revisão.
No que prestamos atenção no processo:

Um dos pontos importantes - a funcionalidade deve estar desativada. Isso significa que, não importa qual seja o código, houve bugs ou não, talvez essa funcionalidade não funcione da maneira como foi originalmente planejada, talvez os gerentes quisessem outra coisa ou talvez essa funcionalidade estivesse tentando colocar outro serviço que não estava pronto para novas cargas, e você precisa da capacidade de desligá-lo rapidamente e colocar tudo em um estado normal.
Também temos uma regra que, ao lançar a nova funcionalidade, deve ser desativada e ativada somente depois que for lançada em todos os clusters e todos os datacenters.
Não esqueça que temos uma API usada por aplicativos móveis que podem não ser atualizados por um longo período de tempo. Se fizermos algumas alterações incompatíveis com versões anteriores em nossa API, alguns aplicativos poderão falhar e não podemos forçar todos os aplicativos a baixar e atualizar. Isso afetará negativamente nossa reputação. Portanto, todas as novas funcionalidades devem ser compatíveis com versões anteriores. Isso se aplica não apenas à API externa, mas também à interna, porque você não pode distribuir simultaneamente todo o código em todos os datacenters, máquinas e todos os clusters. De qualquer forma, o código antigo e o novo viverão conosco ao mesmo tempo. Como resultado, obtemos algumas consultas que não podem ser processadas em algum lugar e teremos erros.
Você também deve pensar no seguinte: se, de repente, seu código não funcionar ou se você tiver escrito um novo microsserviço no qual há problemas em potencial, precisará estar preparado para as conseqüências e poder se degradar. Meu colega falará sobre isso na próxima apresentação.
Se você fizer uma alteração nos serviços altamente carregados e não precisar esperar o final de algumas operações, poderá executar algumas ações de forma assíncrona em algum lugar do plano de fundo ou como um processo separado. É melhor fazê-lo dessa maneira, porque um processo separado tem menos impacto na produção e o sistema funcionará mais estável em geral.

Também é importante que todos os dados que recebemos de fora, não confiemos neles, devemos validá-los, verificá-los de alguma forma, etc. Todos os dados que temos devem ser divididos em grupos que formamos , ou dados brutos que não passaram na validação. Isso inclui todos os dados que poderiam ser obtidos de outros serviços externos ou diretamente dos usuários, porque qualquer coisa poderia vir. Talvez alguém tenha enviado uma solicitação maliciosa especialmente, e tudo deve ser verificado conosco.

Ainda existem casos em que, mediante solicitação, o serviço pode não responder no momento certo. Talvez a conexão quebrou ou algo deu errado, pode haver muitas situações. O aplicativo móvel não sabe o que acabou acontecendo, apenas faz uma solicitação novamente.
É muito importante que, no processo dessas solicitações, não importa quantas existam, no final tudo funcione como originalmente esperado com uma única solicitação. Não devemos ter efeitos especiais. Também deve-se ter em mente que temos mais de um serviço, temos muitas máquinas, muitos data centers, temos bases distribuídas e corridas são possíveis para todos. O código deve ser escrito para que, se for executado em vários locais ao mesmo tempo, não tenhamos corridas.
Um ponto igualmente importante é a capacidade de diagnosticar problemas. Os problemas sempre existem, em tudo, e você precisa entender onde eles ocorreram. Em uma situação ideal, a existência do problema não foi aprendida pelo serviço de suporte, mas pelo monitoramento. E, ao analisar algumas situações, poderíamos entender o que aconteceu simplesmente lendo os logs sem ler o código. Até a pessoa que nunca viu o código, para que, pelos registros no final, pudesse obtê-lo.
E, no caso ideal, se a situação for muito complicada, você precisará verificar pelos logs qual o caminho que o programa levou no final e o que aconteceu para simplificar bastante o interrogatório. Como a situação ocorreu como resultado no passado e agora é improvável que seja possível reproduzir, já não há dados ou outros dados ou outras situações.
Se você estiver executando novas operações no banco de dados ou criando uma nova, precisará considerar que pode haver muitos dados. Talvez você grave um número infinito de registros nesse banco de dados e, se não pensar em arquivá-los, pode haver problemas, o banco de dados simplesmente começará a crescer indefinidamente e não haverá mais recursos, discos e fragmentação. É importante poder arquivar dados e armazenar apenas os dados operacionais necessários no momento. E também é necessário fazer consultas de índice para todos os bancos de dados. Uma consulta que não seja de índice pode colocar toda a produção. Uma pequena solicitação para a coleção central mais carregada pode colocar tudo. Você tem que ter muito cuidado.
Não aceitamos otimizações prematuras. Se alguém está tentando fazer de algum tipo de fábrica um método muito universal que potencialmente lide com casos para o futuro, talvez um dia alguém queira expandi-lo - esse não é nosso costume, porque é possível que ele se desenvolva estará completamente errado e, talvez, esse código seja enterrado ou talvez não seja necessário, mas apenas complica a leitura e a compreensão do código. Porque ler e entender o código é muito importante. É importante que o código seja muito simples e fácil.
Se você adicionar um novo banco de dados ao seu código ou fizer uma alteração na API, teremos uma documentação parcialmente gerada a partir do código, parcialmente realizada no Wiki. Esta informação é importante para manter-se atualizado. Caso contrário, pode enganar alguém ou causar problemas para outros desenvolvedores. Porque o código é escrito sozinho, mas é muito suportado.
Uma parte importante é a observância do estilo geral. A principal coisa neste caso é a uniformidade. Quando todo o código é escrito de maneira uniforme, é fácil de entender, fácil de ler e você não precisa se aprofundar em todos os detalhes e nuances. Código escrito de maneira uniforme pode acelerar todo o processo de desenvolvimento potencialmente no futuro.
Outro ponto que não verificamos especificamente para análises é que não estamos procurando bugs. Porque o autor deve estar envolvido na busca de bugs. Se houver erros durante a revisão, é claro, eles escreverão sobre isso, mas não deve haver uma pesquisa proposital, isso é de inteira responsabilidade da pessoa que escreve o código.
Além disso, quando seu código é escrito, a revisão é concluída, você está pronto para congelá-lo, mas geralmente acontece que você precisa executar ações adicionais, migrar para o banco de dados.

Para migrações, escrevemos um script Python que pode se comunicar com o back-end. O back-end, por sua vez, tem uma conexão com todas as nossas bases e pode realizar todas as operações necessárias. O script é iniciado através do painel de administração de inicialização do script e, em seguida, é executado. Você pode ver o log e os resultados. E se você precisar de operações de feixe de longo prazo, não poderá atualizar tudo de uma só vez, precisará fazê-lo com pedaços de 1000 a 10000 com algumas pausas, para não colocar acidentalmente a base nessas operações.

Quando o código é escrito, revisado, testado, todas as migrações são realizadas, você pode mesclá-lo com segurança no GitHub e continuar a lançá-lo.
Para alguns serviços, temos uma regulamentação de acordo com a qual devemos implementar em um determinado momento, mas uma parte significativa dos serviços que podemos implementar a qualquer momento.
Tudo isso é feito com o TeamCity.

Tudo começa com a construção de pacotes. O TeamCity git flow ou sua aparência. Estamos lentamente nos afastando do fluxo git para nossas melhores práticas, que achamos mais convenientes. O TeamCity produz tudo isso, coleta pacotes, os preenche. Aguardamos ainda mais quando os testes passarão nesses pacotes. É necessário passar nos testes para lançar a liberação. Se os testes falharem, primeiro você precisa descobrir e ver o que acabou dando errado. Os testes utilizados são os mesmos, regulares e de integração. Eles verificam a embalagem já montada, pronta, exatamente o que entra em produção. Este é apenas o caso, de repente, há problemas no pacote montado, de repente algo não é copiado, de repente, algo está faltando.
Há também um requisito para criarmos um ticket de liberação em nosso rastreador, onde cada desenvolvedor deve cancelar a inscrição de como ele testou esse código e deve conter todas as tarefas que devem ser concluídas.
Isso também é feito automaticamente no TeamCity, que percorre a lista de confirmações. Temos o requisito de que em cada confirmação haja uma palavra-chave "Relacionados" seguida pelo nome da tarefa. Um script escrito em Python passa automaticamente por tudo isso, compila uma lista de tarefas que foram resolvidas, forma uma lista de autores e cria um ticket de lançamento, instando todos os autores a cancelar a inscrição em seus testes e confirmar que estão prontos para "ir" no lançamento.

Quando todos estão prontos, as confirmações são coletadas e, em seguida, ocorre a implantação - no pré-estábulo. Esta é uma pequena parte da produção. Para cada serviço, vários data centers são usados; em cada data center, pode haver várias máquinas. Uma das máquinas é pré-estável e o código é implementado primeiro apenas em uma ou duas máquinas.
Quando o código é esvaziado, seguimos os gráficos, os logs e o que acontece no final do serviço. Se tudo estiver bem, se os gráficos mostrarem que tudo está estável, e cada um tiver verificado que sua funcionalidade funciona como deveria, então ele rola para o resto do ambiente, que chamamos de estável. Ao sair para o estábulo, tudo é semelhante: olhamos para os gráficos, registros e verificamos que está tudo bem conosco.
O lançamento já passou, está tudo bem. E se algo desse errado, de repente problemas?

Coletamos hotfix. Isso é feito com o mesmo princípio do fluxo git, ou seja, uma ramificação da ramificação principal. Uma solicitação de pool separada é criada a partir do mestre, que faz as correções e, em seguida, o script iniciado pelo TeamCity a congela, executa todas as operações necessárias, coleta todos os pacotes da mesma maneira e continua.

No final, gostaria de falar sobre a direção em que estamos nos movendo. Estamos caminhando para um único repositório, quando muitos serviços vivem em um repositório de uma só vez. Cada um deles possui cálculos independentes: em testes, em lançamentos. Para solicitações de pool, mesmo quando o TeamCity é usado, verificamos quais arquivos foram afetados, a quais serviços eles pertencem. De acordo com o gráfico de dependência, determinamos quais testes precisamos executar e o que verificar. Nós nos esforçamos para obter o máximo isolamento dos serviços. Até o momento, isso não funciona muito bem, mas nos esforçamos para isso para que muitos serviços possam viver em um repositório, ter algum código comum e que isso não cause problemas e simplifique a vida do desenvolvimento. Isso é tudo, obrigado a todos.