Go Product Development: Um Histórico do Projeto



Olá pessoal! Meu nome é Maxim Ryndin, sou o líder de duas equipes em Gett - Faturamento e Infraestrutura. Quero falar sobre o desenvolvimento de produtos na Web, que nós da Gett usamos principalmente o Go. Vou lhe contar como, em 2015-2017, mudamos para esse idioma, por que o escolhemos, quais problemas encontramos durante a transição e que soluções encontramos. E vou falar sobre a situação atual no próximo artigo.

Para quem não sabe: Gett é um serviço internacional de táxi que foi fundado em Israel em 2011. A Gett agora está representada em 4 países: Israel, Grã-Bretanha, Rússia e EUA. Os principais produtos de nossa empresa são aplicativos móveis para clientes e motoristas, um portal da Web para clientes corporativos onde você pode pedir um carro e vários painéis de administração internos através dos quais nossos funcionários estabelecem planos tarifários, conectam novos motoristas, monitoram casos de fraude e muito mais. No final de 2016, foi aberto um escritório global de P&D em Moscou, que trabalha no interesse de toda a empresa.

Como chegamos a Go


Em 2011, o principal produto da empresa era uma aplicação monolítica no Ruby on Rails, porque na época esse quadro era muito popular. Houve exemplos bem-sucedidos de negócios que foram rapidamente desenvolvidos e lançados no Ruby on Rails, por isso foram associados ao sucesso nos negócios. A empresa estava se desenvolvendo, novos drivers e usuários chegaram até nós, cargas cresceram. E os primeiros problemas começaram a aparecer.

Para que o aplicativo cliente exiba a localização do carro e que seu movimento pareça uma curva suave, os motoristas geralmente enviam suas coordenadas. Portanto, o ponto final responsável por receber coordenadas dos motoristas era quase sempre o mais carregado. E a estrutura do servidor web no Ruby on Rails fez um péssimo trabalho nisso. Foi possível escalar apenas extensivamente, adicionando novos servidores de aplicativos, que são caros e ineficientes. Como resultado, retiramos a coleção funcional de coordenadas em um serviço separado, originalmente escrito em JS. Por um tempo, isso resolveu o problema. No entanto, com o aumento da carga, quando atingimos 80 mil RPMs, o serviço no Node.js parou de nos salvar.

Então declaramos um hackathon. Todos os funcionários da empresa tiveram a oportunidade de escrever um protótipo em um dia, que era coletar as coordenadas dos motoristas. Aqui estão os benchmarks de duas versões desse serviço: executando no prod e reescrito no Go.


Em quase todos os aspectos, o serviço no Go mostrou os melhores resultados. O serviço no Node.js usava um cluster, é uma tecnologia para usar todos os núcleos de uma máquina. Ou seja, o experimento foi mais ou menos honesto. Embora o Node.js possua a desvantagem de um tempo de execução de thread único, ele não afeta os resultados.

Gradualmente, as demandas de nossos produtos aumentaram. Desenvolvemos cada vez mais funcionalidades e, quando encontramos um problema: quando você adiciona algum pedaço de código em um local, algo pode ocorrer em outro local onde o projeto está fortemente conectado. Decidimos superar esse flagelo mudando para uma arquitetura orientada a serviços. Mas o desempenho se deteriorou como resultado: quando uma solicitação de rede é encontrada ao executar o código Ruby on Rails, ela é bloqueada e o trabalhador fica ocioso. E as operações de E / S de rede se tornaram cada vez mais.

Como resultado, decidimos adotar o Go como uma das principais linguagens de desenvolvimento.

Características do nosso desenvolvimento de produtos


Em primeiro lugar, temos requisitos de produtos muito diferentes. Como nossos carros dirigem em três países com leis completamente diferentes, é necessário implementar conjuntos de funcionalidades muito diferentes. Por exemplo, em Israel, é exigido legislativamente que o custo de uma viagem seja considerado por um taxímetro - este é um dispositivo que passa na certificação obrigatória a cada poucos anos. Quando o motorista inicia a viagem, ele pressiona o botão "ir" e, quando termina, ele pressiona o botão "parar" e insere o preço mostrado pelo taxímetro no aplicativo.

Não existem leis tão rígidas na Rússia. Aqui nós mesmos podemos configurar a política de preços. Por exemplo, amarre-o à duração da viagem ou à distância. Às vezes, quando queremos implementar a mesma funcionalidade, primeiro a implementamos em um país e, em seguida, adaptamos e implementamos em outros países.

Nossos gerentes de produto definem requisitos na forma de histórias de produtos, tentamos seguir exatamente essa abordagem. Isso deixa automaticamente sua marca no teste: usamos a metodologia de desenvolvimento orientada a comportamento para que os requisitos de produtos recebidos possam ser projetados em situações de teste. É mais fácil para as pessoas que estão longe da programação ler os resultados do teste e entender o que é o quê.

Também queríamos nos livrar da duplicação de algum trabalho. Afinal, se tivermos um serviço que implementa algum tipo de funcionalidade e precisarmos escrever um segundo serviço, resolvendo todos os problemas que resolvemos no primeiro, reintegrando-nos às ferramentas de monitoramento e migração, isso será ineficaz.

Resolvemos problemas


Enquadramento


O Ruby on Rails é construído na arquitetura MVC. No momento da transição, realmente não queríamos desistir, a fim de facilitar a vida dos desenvolvedores que só podem programar nessa estrutura. Alterar as ferramentas não agrega conforto sem isso, e se você também alterar a arquitetura do aplicativo, é o mesmo que empurrar uma pessoa que não sabe nadar de um barco. Como não queríamos prejudicar os desenvolvedores dessa maneira, adotamos um dos poucos frameworks MVC da época chamado Beego .

Tentamos usar o Beego, como no Ruby on Rails, para fazer a renderização do servidor. No entanto, a página renderizada no servidor realmente não nos agradou. Eu tive que jogar fora um componente, e hoje o Beego produz apenas JSON a partir do back-end, e toda a renderização é realizada pelo React na frente.

O Beego permite criar um projeto automaticamente. Era muito difícil para alguns desenvolvedores mudar de uma linguagem de script para a necessidade de compilar. Havia histórias engraçadas quando uma pessoa implementava algum recurso, e somente por revisão de código ou mesmo acidentalmente descobrimos que você precisa criar um Go-build. E a tarefa já está fechada.

No Beego, um roteador é gerado a partir de um comentário no qual o desenvolvedor grava o caminho para as ações do controlador. Temos uma atitude ambígua em relação a essa idéia, porque, se um erro de digitação, por exemplo, foi rotulado novamente, é difícil para quem não é sofisticado nessa abordagem encontrar um erro. As pessoas, às vezes, não conseguiam descobrir os motivos, mesmo depois de várias horas de depuração emocionante.

Banco de Dados


Usamos o PostgreSQL como banco de dados. Existe essa prática - para controlar o esquema do banco de dados a partir do código do aplicativo. Isso é conveniente por vários motivos: todo mundo sabe sobre eles; eles são fáceis de implantar, o banco de dados está sempre sincronizado com o código. E nós também queríamos manter esses pães.

Quando você tem vários projetos e equipes, às vezes para implementar a funcionalidade, é necessário rastrear os projetos de outras pessoas. E é muito tentador adicionar uma coluna à tabela, na qual 10 milhões de registros podem aparecer. E uma pessoa que não está imersa neste projeto pode não estar ciente do tamanho da tabela. Para evitar isso, emitimos um aviso sobre migrações perigosas que poderiam bloquear o banco de dados para gravação e fornecemos aos desenvolvedores os meios para remover esse aviso.

A migração


Decidimos migrar usando o Swan , um ganso com patches , no qual fizemos algumas melhorias. Esses dois, como muitas ferramentas de migração, desejam fazer tudo em uma transação, para que, em caso de problemas, você possa reverter facilmente. Às vezes acontece que você precisa criar um índice e a tabela está bloqueada. O PostgreSQL possui um parâmetro concurrently que evita isso. O problema é que, no PostgreSQL, você começa a criar um índice concurrently e mesmo em uma transação, um erro será exibido. Inicialmente, queríamos adicionar uma bandeira para não abrir uma transação. E no final, eles fizeram o seguinte:

 COMMIT; CREATE INDEX CONCURRENTLY huge_index ON huge_table (column_one, column_two); BEGIN; 

Agora, quando alguém adiciona um índice com o parâmetro concurrently , ele recebe essa dica. Observe que commit e begin não begin confusos. Esse código fecha a transação que a ferramenta de migração abriu, depois rola o índice com o parâmetro concurrently e, em seguida, abre outra transação para que a ferramenta feche algo.

Teste


Tentamos aderir ao desenvolvimento orientado pelo comportamento. No Go, isso pode ser feito usando a ferramenta Ginkgo . É bom porque possui as palavras-chave usuais para BDD, "descreva", "quando" e outras, e também permite que você simplesmente projete o texto escrito pelo gerente de produto em situações de teste que são armazenadas no código-fonte. Mas estávamos diante de um problema: as pessoas que vieram do mundo do Ruby on Rails acreditam que em qualquer linguagem de programação há algo semelhante a uma garota de fábrica - uma fábrica para criar condições iniciais. No entanto, não havia nada parecido com isso no Go. Como resultado, decidimos que não reinventaríamos a roda: imediatamente antes de cada teste, nos ganchos antes e depois do teste, enchemos o banco de dados com os dados necessários e depois o limpamos para que não haja efeitos colaterais.

Monitoramento


Se você possui um serviço de produção que as pessoas estão acessando, é necessário rastrear seu trabalho: existem quinhentos erros ou pedidos sendo processados ​​rapidamente. No mundo do Ruby on Rails, o NewRelic é frequentemente usado para isso, e muitos de nossos desenvolvedores o possuem bem. Eles entenderam como a ferramenta funcionava, onde procurar se havia algum problema. O NewRelic permite analisar o tempo de processamento de solicitações via HTTP, identificar chamadas e solicitações externas lentas para o banco de dados, monitorar fluxos de dados, fornecer análises e alertas de erros inteligentes.

O NewRelic possui a função agregada Apdex, que depende do histograma da distribuição da duração das respostas e de alguns valores que você considera normais e que são definidos desde o início. Esse recurso também depende do nível de erros no aplicativo. NewRelic calcula Apdex e emite um aviso se seu valor cair abaixo de algum nível.
O NewRelic também é bom em ter um agente Go oficial recentemente. É assim que a visão geral do monitoramento se parece:



À esquerda, está um diagrama de processamento de consultas, cada um dos quais dividido em segmentos. Os segmentos incluem enfileiramento de pedidos, processamento de middleware, tempo de permanência no interpretador Ruby on Rails e acesso aos repositórios.

O gráfico do Apdex é exibido no canto superior direito. Em baixo à direita - a frequência das solicitações de processamento.

A intriga é que, no Ruby on Rails, para conectar o NewRelic, você precisa adicionar uma linha de código e adicionar suas credenciais à configuração. E tudo funciona magicamente. Isso é possível devido ao fato de que no Ruby on Rails há patch de macacos, o que não está no Go, então há muito o que fazer manualmente.

Antes de tudo, queríamos medir a duração do processamento da solicitação. Isso foi feito usando os ganchos fornecidos pela Beego.

 beego.InsertFilter("*", beego.BeforeRouter, StartTransaction, false) beego.InsertFilter("*", beego.AfterExec, NameTransaction, false) beego.InsertFilter("*", beego.FinishRouter, EndTransaction, false) 

O único ponto não trivial foi que compartilhamos a abertura da transação e seu nome. Por que fizemos isso? Eu queria medir a duração do processamento da solicitação, levando em consideração o tempo gasto no roteamento. Ao mesmo tempo, precisamos de relatórios agregados pelos pontos de extremidade aos quais os pedidos foram recebidos. Porém, no momento da abertura da transação, ainda não definimos um padrão de URL pelo qual uma correspondência ocorrerá. Portanto, quando uma solicitação chega, abrimos uma transação e, em seguida, no gancho, após a execução do controlador, nomeie-a e, após o processamento, feche-a. Portanto, hoje nossos relatórios são assim:


Usamos um ORM chamado GORM porque queríamos manter a abstração e não forçar os desenvolvedores a escrever SQL puro. Essa abordagem tem vantagens e desvantagens. No mundo do Ruby on Rails, existe um ORM Active Record que realmente mima as pessoas. Os desenvolvedores esquecem que você pode escrever SQL puro e operar apenas com chamadas ORM.

 db.Callback().Create().Before("gorm:begin_transaction"). Register("newrelicStart", startSegment) db.Callback().Create().After("gorm:commit_or_rollback_transaction"). Register("newrelicStop", endSegment) 

Para medir a duração da execução da consulta no banco de dados ao usar o GORM, é necessário levar o objeto db . O retorno de chamada diz que queremos registrar um retorno de chamada. Deve ser chamado ao criar uma nova entidade - uma chamada para Create . Em seguida, indicamos exatamente quando iniciar o retorno de chamada. Before é responsável por isso com o argumento gorm : begin_transaction é um ponto no momento em que a transação é aberta. Em seguida, com o nome newrelicStart registramos a função startSegment , que simplesmente chama o agente Go e abre um novo segmento para acessar o banco de dados.

O ORM chamará essa função antes de abrirmos a transação e, assim, abrir o segmento. Devemos fazer o mesmo para fechar o segmento: basta travar o retorno de chamada.

Além do PostgreSQL, usamos Redis, que também não é suave. Para esse monitoramento, escrevemos um wrapper em um cliente padrão e fizemos o mesmo para chamar serviços externos. Aqui está o que aconteceu:



É assim que o monitoramento é para um aplicativo escrito em Go. À esquerda, há um relatório sobre a duração do processamento de consultas, composto por segmentos: execução do código em Go, acesso aos bancos de dados PostgreSQL e Replica. As chamadas para serviços externos não são exibidas neste gráfico, porque existem muito poucas e são simplesmente invisíveis quando a média é calculada. Também temos informações sobre o Apdex e a frequência do processamento de solicitações. Em geral, o monitoramento acabou sendo bastante informativo e útil para uso.

Quanto aos fluxos de dados, graças aos nossos wrappers pelo cliente HTTP, podemos rastrear solicitações para serviços externos. O esquema de solicitação de serviço de promoção é indicado aqui: refere-se a quatro de nossos outros serviços e dois repositórios.



Conclusão


Hoje, temos mais de 75% dos serviços de produção escritos em Go, não realizamos desenvolvimento ativo em Ruby, mas apenas o apoiamos. E a esse respeito, quero observar:

  • Os receios de que a velocidade do desenvolvimento diminua não foram confirmados. Os programadores entraram na nova tecnologia, cada um no seu próprio modo, mas, em média, após algumas semanas de trabalho ativo, o desenvolvimento no Go se tornou tão previsível e rápido quanto no Ruby on Rails.
  • O desempenho dos aplicativos Go sob carga é agradavelmente surpreendente em comparação com as experiências anteriores. Economizamos significativamente no uso da infraestrutura na AWS, reduzindo significativamente o número de instâncias usadas.
  • A mudança de tecnologia incentivou significativamente os programadores, e essa é uma parte importante de um projeto bem-sucedido.
  • Hoje já deixamos Beego e Gorm, mais sobre isso no próximo artigo.

Resumindo, quero dizer que se você não escreve no Go, sofre de problemas de altas cargas de trabalho e está entediado com o tráfego, vá para esse idioma. Só não se esqueça de negociar com o negócio.

Source: https://habr.com/ru/post/pt463097/


All Articles