BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos Reescrevendo o serviço API Gateway do Clojure para Golang: Relatório de experiência da AppsFlyer

Reescrevendo o serviço API Gateway do Clojure para Golang: Relatório de experiência da AppsFlyer

Favoritos

Pontos Principais

  • A AppsFlyer processa quase 70 bilhões de solicitações HTTP por dia e é construída usando um estilo de arquitetura de microservices. O ponto de entrada para o sistema que envolve todos os serviços front-end é um serviço de missão crítica (não micro) chamado API Gateway.
  • O API Gateway original que foi escrito na linguagem padrão da AppsFlyer, o Clojure, começou a acumular dívida técnica.
  • A Golang foi selecionada como a linguagem para comparar com o Clojure a proposta de um novo serviço para a API Gateway.
  • O benchmarking foi conduzido com NGINX (aprimorado por LUA) como uma opção, ao lado de Golang e Clojure. A entrega melhorou em comparação com o Clojure, e isso foi selecionado como o idioma de escolha para a implementação.
  • O fato de que o API Gateway agora é construído em uma linguagem tipificada fornece a capacidade de conectar diversas funcionalidades e introduzir novas tecnologias com muito mais facilidades com o suporte e a comunidade de bibliotecas da Golang.
  • A solução recém-implantada é capaz de suportar exponencialmente mais tráfego do que hoje - e com o tráfego e as solicitações crescendo em escalas de 10X isso era importante visando o futuro.

A AppsFlyer, uma plataforma líder de análise de marketing e atribuição móvel, processa quase 70 bilhões de solicitações HTTP por dia (aproximadamente 50 milhões de solicitações por minuto) e é construída usando um estilo de arquitetura de microservice. O ponto de entrada para o sistema que envolve todos os serviços front-end é um serviço de missão crítica (não micro) chamado API Gateway. Isso serve essencialmente como um ponto único para rotear o tráfego dos clientes para os serviços de back-end, simplificando a autenticação exponencial para os clientes, mas com a desvantagem de também ser um único ponto de falha potencial.

Este artigo explora por que e como a equipe de engenharia migrou de a implementação de API Gateway baseada em Clojure para uma implementação baseada em GO.

Acumulando dívida técnica dentro da API Gateway

Foi mencionado anteriormente sobre como a dívida técnica se origina e, muitas as vezes que isso aconteceu, assim como aconteceu com o serviço API Gateway.

Originalmente os serviços da AppsFlyer eram um monolito do Python, que exigia uma única solução para autenticação e autorização como parte do próprio monolito. Com o passar do tempo, o tráfego e a complexidade aumentaram e foi migrado para uma arquitetura de Microservice. Como tal, foi preciso criar uma solução da API Gateway unificada que serviria como nosso provedor de autenticação e autorização.

O início foi apenas arregaçando as mangas e escrevendo em Clojure, pulando as fases de design e construindo um serviço em grande parte no modo de prova de conceito. A empresa é uma das maiores lojas de Clojure em produção na EMEA e, portanto, o Clojure é muitas vezes, por padrão, a linguagem preferida sem muitas outras considerações sobre o projeto específico em questão. Embora isso seja bom para a velocidade e uma mentalidade de "fazer as coisas acontecerem", é menos ideal para a manutenção de longo prazo de um projeto. Rapidamente foi verificado como o tráfego cresceu - que o código para a API de gateway recém implementado era muito complexo e precisava de refatoração constante para habilitar a taxa de transferência necessária.

Eventualmente havia uma encruzilhada em que o serviço era instável demais percebendo que era necessário reescrever o projeto completamente - seja no Clojure (mas com um design melhor) ou explorar outras opções de linguagem também. Com essa iteração, foi decidido não adotar os vieses cognitivos e reverter para a zona de conforto do Clojure, mas, em vez disso, fazer o trabalho de design adequado para construir o serviço de que era necessário, não apenas refazer um serviço já existente.

Por fim, o Golang foi selecionado como linguagem para um benchmark contra o Clojure, para o serviço de API Gateway, que também trouxe os benefícios adicionais da diversidade de linguagem e contribuiu para manter a mentalidade de código hábil, ao dominar sintaxes adicionais.

Foi constatado o outro lado da adição de outra linguagem de programação para a pilha. Defendemos fortemente a mentalidade CI/CD e a introdução de uma nova linguagem, que não é baseada em JVM (em oposição a Clojure), teve seus custos operacionais, mas foi possível resolvê-lo em pouco tempo.

Havia também as curvas de aprendizado com o domínio de um novo idioma e a necessidade de garantir que o código fosse estelar e robusto o suficiente a longo prazo, o que é difícil saber antes de escrever seu primeiro projeto em um idioma específico e ver como ele se comporta em produção.

Foi relatado um breve comentário sobre o por quê selecionar o Go para este serviço específico - apenas para um contexto. O Go tem um suporte muito forte para a criação de serviços de rede e especificamente para serviços semelhantes a proxy, com proxy reverso integrado. Sua maior vantagem em relação a outras soluções, como o http-kit que foi utilizado no Clojure, é a capacidade de transmitir os dados através do proxy em vez de armazená-los ao cliente somente após o último byte ter sido recebido do servidor. Esse recurso, juntamente com o suporte para E/S eficiente, sem o preço de um código assíncrono excessivamente complicado que teria que ser escrito em outras plataformas, como a JVM, tornou a opção do Go muito atraente. Uma vantagem adicional que se tornou aparente enquanto teve início a implantação do serviço, foi o fato de que uma linguagem com tipagem estática facilita muito a refatoração do código e sua razão, já que os tipos são uma maneira excelente de auto-documentar o código.

Avaliando nossas opções

Para que seja possível avaliar adequadamente as diferentes linguagens foi necessário examinar alguns aspectos - o desempenho e os benefícios específicos de cada idioma para a tarefa em questão. Para medir o desempenho seria necessário avaliar corretamente o Clojure versus Go na maior simulação de produção possível.

Para fazer isso foram realizados testes de estresse com o NGINX (aprimorado por Lua) como uma opção ao lado de Golang e Clojure. Go melhorou a entrega e o rendimento versus Clojure.

As estatísticas básicas do teste:

  • Foi utilizado WRK como ferramenta de benchmarking;
  • Estouros de 3 minutos;
  • 64 threads;
  • 1000 conexões;
  • Tempo limite de requisiçies de 2 minutos;
  • Cada pedido retornou um arquivo estático de 500kb;
  • Todo tráfego foi disparado ao mesmo AZ para atenuar o ruído da rede usando instancias de c4 xlarge.

Proxy solution

Req/Sec

Trans/Sec

Total requests

Total transaction size

Bad Req

Avg. Latency

Direct

190

72 MB

34500

12.8 GB

~ 400 (drop:200)

4.41 Sec

NGINX

185

73 MB

33486

12.7 GB

~ 300 (drop:37)

7.95 Sec

Clojure (basic Http-Kit implementation)

190

72 MB

34412

12.8 GB

~ 100 (drop:600)

8.48 Sec

Golang (native reverse proxy & http layer)

185

73 MB

33443

12.7 GB

~ 200 (drop: 0)

5.42 Sec

A linguagem Go foi adotada não apenas por ter mostrado um melhor desempenho, mas também pelo desafio de uma linguagem diferente e uma maneira diferente de pensamento.

As fases de design começaram descrevendo a funcionalidade que era exigida que o serviço tivesse e, após ter os conceitos básicos especificados foi examinado as considerações de compatibilidade com versões anteriores e potenciais armadilhas com a migração da base de usuários de produção para o novo serviço. Uma vez que isso foi garantido que todas as bases haviam sido cobertas, foi dado início ao trabalho atribuindo um arquiteto e um desenvolvedor ao projeto.

Do conceito a entrega

Foi uma surpresa a rapidez com que a parte de codificação do projeto foi concluída, com aproximadamente dois meses de trabalho. Como essa foi a primeira vez que Go foi apresentando in-house, a codificação do projeto teve um cuidado muito especial. Foram feitas duas iterações em cada função para garantir que tudo estava correto, além das muitas revisões de código. Esse cuidado com a codificação foi realizado pois sabia que serviria como uma fonte para outros projetos Go futuramente.

Apesar de ser o primeiro projeto em Go, foi uma boa oportunidade de entender bem a linguagem e sua funcionalidade principal, pois foi necessário compensar as bibliotecas usadas no Clojure para comunicação com partes adicionais da pilha, incluindo Redis (persistente estado de contadores de login do usuário para evitar DDos e bots) e Kafka (gerenciando um CQRS de eventos de domínio, um dos quais são logins bem-sucedidos ou malsucedidos), o que exigiu a criação de bibliotecas semelhantes no Go.

A fim de combinar o ecossistema existente no Clojure, era necessário integrar toda uma gama de bibliotecas com uma biblioteca de coleta de métricas, uma biblioteca de registro, uma biblioteca JWT, entre outras, e foi muito gratificante encontrar todas elas em um nível de maturidade que é uma indicação muito forte do nível de adoção da linguagem Go pela comunidade - o que é uma consideração importante ao tomar a decisão de migrar para uma nova linguagem. Sua sustentabilidade e maturidade da comunidade desempenham um papel importante em tal decisão.

Tudo estava pronto para a migração básica após aproximadamente dois meses, tendo a funcionalidade básica coberta e testada. Então foi iniciada a migração do serviço interativamente dentro do grupo pai (grupo de domínio) de maneira controlada para o novo API Gateway, que era basicamente um lançamento canário.

Foi decidido fazer um rollout controlado com os primeiros serviços ao longo das primeiras semanas, para que pudesse descobrir os bugs e falhas em produção e ter tempo de corrigi-los adequadamente antes de lançar todos os serviços. Era desejado aprender com o erro da mudança rápida com a solução original da API, o que acabou levando à entrega de baixa qualidade.

Uma vez que a equipe se sentiu segura e todos os bugs foram corrigidos, foi iniciado o plano de migração para todos os serviços. Isso incluiu um PDF de guia de migração para cada serviço, incluindo as etapas exatas necessárias para transferir para o novo serviço e os benefícios incluídos em tal movimento, além da maneira ideal de realizar a migração com base em sua pilha de dependências específicas.

Para distribuir o novo proxy reverso de maneira gradual, um balanceador de carga de aplicativo (ALB) foi usado para rotear o tráfego com base em um conjunto de URLs predefinidas que indicam os serviços que desejamos expor por meio do novo gateway da API versus o anterior.

Isso permitiu uma abordagem muito controlada de como rotear o tráfego com o mínimo de esforço e risco. A equipe se juntou, testaram cada serviço migrado e trabalharam de mão dadas com todas as outras equipes responsáveis por seus serviços voltados para o usuário. No total, seis meses foram levados, mas foi possível migrar ~40 microservices para usar o novo API Gateway com tempo de inatividade zero.

Resultados

O resultado final permitiu reduzir 25 instâncias (c4 xlarge) executado no código Clojure - capaz de processar 60 solicitações simultâneas, para duas instâncias (c3.2xlarge) executando o código Go capaz de suportar ~5000 solicitações simultâneas por minuto - uma grande melhoria. O novo design de arquitetura também foi robusto o bastante para o crescimento da próxima fase, oferecendo um serviço poderoso que pode suportar alta escala e a complexidade dos negócios, devido a abordagem processual e também um novo idioma para adicionar a caixa de ferramentas quando se lida com alta escala.

Tomemos por exemplo a solução de proxy reverso no Clojure e no Go.

Clojure:

;; Creating a connection manager


(let [cm (clj-http.conn-mgr/make-reusable-conn-manager {:timeout 1 :threads 20 :default-per-route 10})])


;; Creating a proxy server using cm (connection manager)
 (client/request {:method	:get
                           :url	(service/service-uri service-spec uri-match)
                           :headers	(dissoc (into {} (:headers req)) “content-length”)
                           :body	(when-let [len (get-in req [:headers “content-length”])]
                                                     (bs/to-byte-array (:body req)))
                           :follow-redirects   false
                           :throw-exceptions   false
                           :connection-manager cm
                           :as	:stream}))

E no Goland::

func NewProxy(spec *serviceSpec.ServiceSpec, director func(*http.Request), respDirector func(*http.Response) error, dialTimeout, dialKAlive, transTLSHTimeout, transRHTimeout time.Duration) *MultiReverseProxy {
	return &MultiReverseProxy{
		proxy: &httputil.ReverseProxy{
			Director:       director, //Request director function
			ModifyResponse: respDirector,
			Transport: &http.Transport{
				Dial: (&net.Dialer{
					Timeout:   dialTimeout, //limits the time spent establishing a TCP connection (if a new one is needed).
					KeepAlive: dialKAlive,  //limits idle keep a live connection.
				}).Dial,
				TLSHandshakeTimeout:   transTLSHTimeout, //limits the time spent performing the TLS handshake.
				ResponseHeaderTimeout: transRHTimeout,   //limits the time spent reading the headers of the response.
			},
		},

Observe como Golang tem muitos recursos orientados para um melhor gerenciamento de pools de conexão e recursos de proxy reverso integrados em suas classes principais.

Resumo

Optar por escrever a nova versão do API Gateway em Go provou ser uma decisão muito boa. A curva de aprendizado mínima do Go tornou uma excelente linguagem para aprender "on the fly" enquanto trabalhava em um serviço de produção real. Seu suporte a construção de rede de baixo nível, como proxy reverso e uma mentalidade geral em relação ao desempenho, tornou o resultado final uma melhoria mensurável real e também mais robusta. Todos os problemas de produção que se apresentaram em consequência do código anterior, estão agora obsoletos, é muito mais fácil adicionar novas funcionalidades ao gateway e o aumento do tráfego que agora podemos suportar permitindo-nos dormir melhor à noite.

Este artigo foi atualizado em 15 de fevereiro de 2019 para esclarecer vários pontos levantados na discussão dos comentários.

Sobre o Autor

Asaf Yonay é gerente do grupo P/D da AppsFlyer, é apaixonado por assumir desafios gerenciais e técnicos e transformá-los em histórias de sucesso, adicionando o elemento humano ao mix. Asaf acredita firmemente na definição de processos que ajudam as equipes de P/D a crescerem e escalarem sem perder a velocidade, e adotarem uma abordagem prática e integral para manter contato com os desafios - acreditando que é isso o que transforma gerentes em líderes. Ele tem trabalhado na start-up em várias funções, desde suporte, controle de qualidade e várias funções de P/D, construindo sistemas robustos e escaláveis em Clojure, Golang, Node.js e Python para fornecer serviços React e Angular, enquanto trabalhava com Kafka, Aerospike e Neo4J para lidar com estados de lógica de negócios complexos ou em larga escala.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT