Principais aprendizados ao reduzir em 40% os custos do time User Notification

Robson Alves
Grupo OLX Tech
Published in
11 min readApr 1, 2021

Me chamo Robson, sou desenvolvedor no time de User Notification da OLX Brasil. Somos responsáveis por fornecer uma plataforma de comunicação para que outros times da OLX Brasil possam se comunicar com os nossos usuários, atualmente enviamos uma média de um pouco mais de 40 milhões de notificações por dia e venho compartilhar com vocês os aprendizados que resultaram em uma redução de mais de 40% do custo de nossa stack durante a pandemia do covid-19.

Você poder estar queimando dinheiro

No início de 2020, os custos da nossa stack vinham crescendo mês-a-mês, principalmente aqueles relacionados à persistência. Fato este que estava incomodando o time que vinha planejando algumas mudanças com o intuito de reduzi-los. Com a chegada da Pandemia da Covid-19, reduzir custos acabou se tornando uma prioridade da empresa como um todo e em Q2 tivemos um objetivo em nosso OKR dedicado apenas para redução de custos.

Gráfico demonstrando o aumento do custo mensal do time a partir do segundo semestre de 2019.

Impactos do Coronavírus no negócio e no consumo dos serviços de notificações

O time de User Notification (internamente conhecido como NU (Notificações dos usuários)) tem sua stack baseada principalmente em Serverless, isso se dá pelo fato de trabalharmos com eventos que não possuem um fluxo previsível, para isso precisamos de uma stack resiliente, que escale sob demanda e que tenha um custo reduzido.

A stack Serverless possui uma característica interessante na qual você paga apenas pelo recurso utilizado. Isso também significa que os nossos custos variam de acordo com a nossa demanda, quanto mais envios de notificações maior será o nosso custo.

Isso ficou bastante evidente para nós logo no primeiro mês de Q2, mesmo após algumas mudanças na nossa stack que deveriam refletir na redução de custo em alguns recursos. Olhando puramente para o nosso custo semanal, que era a métrica que estávamos acompanhando no KR, parecia que não estávamos causando impacto algum em nossos custos e os valores se mantiveram os mesmos, porém avaliando mais friamente os nossos dados, notamos que após o início da pandemia as pessoas começaram a trocar mais mensagens através do chat da OLX e consequentemente começamos a enviar mais notificações para os usuários avisando de novas mensagens.

Dado este cenário decidimos fazer um tuning de nossos KR’s e cruzando os nossos custos com o nosso volume de notificações enviadas, conseguimos normalizar os nossos custos chegando a uma estimativa de preço médio por milhão de notificações, dessa forma independentemente da quantidade de notificações enviadas por dia, nossa estimativa de custo por milhão não deveria sofrer muita flutuação.

Fomos surpreendidos com custos inesperados

No meio do quarter que estávamos com diversas iniciativas para redução de custos, fomos surpreendidos com um aumento significativo no custo do Cloudwatch, saímos de um custo de 0 para mais de 40 dólares por dia e só percebemos este aumento após duas semanas desde o início do aumento, quando o gasto com o Cloudwatch já havia ultrapassado os 500 dólares, indo na contramão do que estávamos tentando fazer.

Gráfico demonstrando o aumento do custo do Cloudwatch.

Investigando o motivo desse aumento repentino descobrimos que uma rotina interna do time de Cloud Engineering adicionou a tag do time de NU no recurso Logs groups do Cloudwatch e isso pegou todo o time de surpresa, principalmente por que não utilizamos o Cloudwatch para gerenciar os nossos logs, utilizamos a stack ELK.

Fazendo um deep dive para entender os motivos deste custo, descobrimos que por padrão a AWS gera pelo menos três logs para cada execução de uma função Lambda:

Start: Timestamp do início da execução
End: Timestamp do final da execução
Report: Este é o mais útil dos 3, ele possui a duração da execução, qual a quantidade de memória alocada para função e a memória usada no processamento da requisição. Todas essas três informações impactam no custo da execução (lembra que eu falei que Serverless você pega pelo recurso que utiliza?).
Error: Esta mensagem é gerada apenas caso ocorra algum erro durante a execução da função, ex: timeout.

E devido ao nosso throughput, apenas esses logs gerados pela AWS foram o suficiente para gerar todo este custo.

Apesar do incidente, foi muito bom que isso tenha acontecido porque o custo já existia, no entanto, só não tínhamos visibilidade. Se essa rotina não tivesse adicionado a tag de NU, a “torneira” iria continuar aberta. Infelizmente não existe uma configuração no lambda para desativar estes logs, mas a forma que encontramos para “fechar essa torneira” e impedir que esses logs fossem gerados foi bloquear a permissão de escrita do lambda no Cloudwatch utilizando uma role do IAM.

Para evitar sermos pegos de surpresa novamente com custos inesperados, criamos um bot de detecção de anomalias para monitorar os nossos custos e alertar o time em caso de anomalia, o Inspetor Bugiganga. Esse bot analisa diariamente os custos do time e utiliza a lib prophet do facebook para analisá-los, e em caso de anomalia é enviado uma notificação para o time através do slack, essa iniciativa acabou se expandindo e atualmente está rodando para todos os times da engenharia da OLX, pois o integramos com o nosso PaaS.

Iniciativas e mudanças na arquitetura do time

Durante o Q2 tivemos diversas iniciativas para reduzir custos, mas algumas geraram bons aprendizados que valem a pena ser compartilhados:

  • Zerar o custo de resquícios da stack antiga

Há bastante tempo quando o time migrou a stack que efetuava o envio de notificações para uma stack Serverless, acabou ficando alguns recursos ativos na conta de desenvolvimento da OLX na AWS gerando custos.

Utilizamos o serviço AWS Resource Groups para identificar estes recursos e em qual conta da AWS eles se encontravam, e após identificá-los, nós os removemos.

  • Reduzir os custo da stack Serverless (Lambdas, SNS, SQS, API Gateway)

Antes de iniciar qualquer mudança na stack paramos pra entender todos os fatores que impactam no custo de uma stack Serverless, a partir daí criamos criamos um dashboard no Cloudwatch metrics consolidando todas as informações que impactam nossos custos.

Uma amostra da dashboard com as métricas dos serviços Serverless

Essa dashboard nos deu diversos insights do que poderíamos fazer para reduzir os custos da nossa stack. Alguns exemplos de métricas que mapeamos nesse dashboard:

  • Quantidade total e média das invocações por função lambda
  • Duração de execução máxima e média por função lambda
  • Concurrent Execution máxima e média por função lambda
  • Memória definida x Memória utilizada por função lambda
  • Quantidade total e média por fila de mensagens publicadas no SQS
  • Média do tamanho em bytes por fila e de todos as filas juntas das mensagens publicadas no SQS

Esse dashboard nos deu uma visão clara de uso de toda a nossa stack, conseguimos tirar diversos insights e mapear alguns action itens que foram cruciais para conseguirmos reduzir os custos.

Percebemos que a duração de execução de todos os nossos lambdas chegavam ao valor máximo definido no timeout com bastante frequência e que o tempo médio de execução estava bem abaixo do valor definido, com isso reduzimos o valor do timeout de todos as nossa funções de 20s para 5s (tempo médio fica abaixo de 1s) e além disso definimos um timeout para todas as requisições externas a função.

Lembra que acima eu mencionei que desativamos o Cloudwatch? Pois é, aqui precisamos abrir uma exceção e ativá-los novamente, dos três logs que a AWS gera por para cada execução dos lambdas, queríamos analisar o que nos informa a memória configurada para a função e a memória usada no processamento da requisição, o Cloudwatch insights nos fornece uma query onde é possível avaliarmos a quantidade super-provisionada de memória para cada função, fizemos essa análise e otimizamos a memória configurada para todas as nossas funções e claro após a análise desligamos novamente os logs do Cloudwatch.

Identificamos que nem todas as notificações precisam passar por todas as etapas de população de informação de nossa stack, então tinham filas e lambdas sendo chamados sem necessidade, eles não operavam e só entregavam o payload da notificação para a fila seguinte, então demos um pouco de “inteligência” para nossas funções para que elas consigam analisar o conteúdo do payload da notificação e saber qual será a próxima fila que ela deverá entregar o payload.

Dessa forma, conseguimos reduzir o número de mensagens publicadas no SQS e o número de invocações das funções Lambdas, que são fatores que influenciam no custo do SQS e do Lambda, respectivamente.

Ao longo da stack, fazemos requisições em diversas APIs de outros times, para buscar conteúdo e popular o payload do evento da notificação que será enviada. Isso faz com que o tamanho do payload aumente a cada etapa de população e na maioria das vezes ele é inflado com diversos conteúdos que nunca serão utilizados na notificação.

Por último, trabalhamos para reduzir o tamanho deste payload que trafega pela stack removendo campos nulos e campos que não são necessários para as próximas etapas em que a notificação irá passar até ser enviada para o usuário. Por exemplo: A etapa de renderização do template de email é a etapa que mais temos possibilidade de usar alguma informação populada anteriormente no payload, e após essa etapa a maioria das informações contidas no payload não serão mais utilizadas. Passamos então a remover todas as informações que não serão mais utilizadas a partir desta etapa.

  • Reduzir o custo de persistência (S3, ElasticCache, RDS)

Storage (S3)

O nosso processo de envio de dados para o datalake funciona da seguinte forma:

  • O dado é enviado das aplicações para um Redis;
  • Uma rotina é executada de 5 em 5 minutos gerando um arquivo em um bucket do s3 com os dados do Redis;
  • Durante a madrugada uma rotina do time de Data Engineering faz a ingestão dos dados para a nossa tabela de eventos;

Uma vez que a ingestão ocorre não temos mais a necessidade de armazenar estes arquivos em nosso bucket, pois uma vez ingerido o mesmo arquivo não será processado novamente. No entanto, nós não fazíamos o expurgo deste arquivos e os arquivos se acumulavam desde quando essa nossa rotina foi implementada, aumentando o custo de armazenamento.

Alinhamos então com o time de Data Engineering para criarmos uma política de expurgo para estes arquivos, armazenando os arquivos por 30 dias a partir de sua criação (para caso haja necessidade de reprocessamento dos dados) e após esse período o arquivo será automaticamente removido do nosso bucket.

Cache (Redis)

Na mesma dashboard que criamos para a stack Serverless, adicionamos também um gráfico para acompanharmos o quanto de memória estávamos usando em cada um de nossos Redis. Após analisarmos este gráfico, revisamos todas as Instance families que estávamos usando para os Redis, reduzimos para a menor possível e criamos alertas para garantir que não iríamos causar downtimes em nossos serviços caso o “cinto ficasse muito apertado“.

O nosso Redis mais caro era o que guardava por 2h o status da comunicação e que pode ser consultado através da rota de nossa API, então investigamos com os times que se utilizavam essa rota o tempo máximo em que suas rotinas buscavam as informações sobre o status do disparo de notificação feito, e conseguimos reduzir o tempo de acúmulo para 1h, reduzindo ainda mais a família utilizada e o preço da arquitetura.

Banco de dados (RDS)

Para os nossos bancos de dados que estavam no RDS, fizemos a mesma coisa que fizemos nos Redis: analisamos cada banco e revisamos suas Instance families, exceto para um banco: o que persistimos os devices tokens dos usuários para envio de Push Notification.

O banco de devices estava crescendo exponencialmente e sendo o principal ofensor dos nossos custos. Mesmo contando com dois databases (master e réplica), o perfil de acesso ao dado da aplicação de device é alto tanto para leitura, quanto para escrita, e estávamos tendo problemas de escalabilidade principalmente para as operações de escrita. Logo, rever o Instance Family para uma menor não era a melhor opção balanceando custo x disponibilidade, então decidimos estudar uma outra opção de banco de dados que fosse mais barata e mais escalável.

Com este desafio em mente criamos um documento de proposta técnica, onde analisamos duas opções de database (CassandraDB e DynamoDB), analisando as características e os custos de cada database e o fato do Cassandra estar em beta, optamos por utilizar o DynamoDB. Tomada a decisão, conversamos com o time da AWS para apresentar a nossa proposta de migração, entender melhor os corner cases do DynamoDB e pegarmos alguns feedbacks.

A migração não foi simples. Tivemos que: 1) alterar todos os endpoints do serviço de devices, 2) migrar todos os 40 milhões de devices cadastrados de um banco para o outro e 3) garantir que não tivéssemos downtime neste serviço durante a migração.

Para atingirmos isso, criamos uma camada na aplicação para abstrair os bancos de dados e através de uma feature de rollout progressivo fomos gradualmente redirecionando o fluxo do RDS para o DynamoDB.

Todo o processo de migração precisou acontecer de forma gradual para analisarmos como o DynamoDB estava se comportando e para isso foram necessárias muitas métricas, alarmes e um plano de execução.

Resultados

Ao final do quarter conseguimos atingir bons números para cada objetivo:

  • Zerar o custo de resquícios da stack antiga
Gráfico demonstrando que conseguimos zerar o custo da stack antiga.

Conseguimos zerar os custos de resquícios da stack antiga, conseguindo uma redução de mais de 300 dólares por mês.

  • Reduzir o custo da stack Serverless (Lambdas, SNS, SQS)
Gráfico demonstrando a redução de mais de 25% no custo médio por milhão de requisições da stack Serverless.

Conseguimos reduzir a nossa estimativa de custo por milhão de notificações enviadas de um pico de pouco mais de USD 21 para USD 15, totalizando mais de 25% de redução.

  • Reduzir o custo de persistência (RDS, ElasticCache, S3, DynamoDB)
Gráfico demonstrando a redução do custo de persistência em mais de 4 mil dólares por mês.

O principal ofensor do custo de persistência era o banco de devices, após migração o nosso custo de persistência caiu de um pico de USD 6,054 para uma média de USD 2,500, isso significa que conseguimos ter uma redução de quase 60% a menos.

No final do quarter conseguimos reduzir em mais de 40% os custo da nossa stack, gerando uma economia de mais de 10 mil dólares por mês para a empresa.

Nossos principais aprendizados:

  • Na correria do dia a dia acabamos não dando muita atenção para detalhes, como: aplicações superdimensionadas, recursos que o time não está utilizando e que continua gerando gastos desnecessários, entre outras coisas. Reduzir custos é uma tarefa contínua, a stack vai evoluindo, o time vai mudando e quando menos se espera as torneiras abertas aparecem e se não olharmos ela ficará lá gerando gastos por meses.
  • Compreender a equação Custo x Escalabilidade é essencial quando se pensa em otimização, sua aplicação está tendo problemas de escalabilidade? Aumentar os recursos da máquina (escalar vertical) ou aumentar a quantidade de containers/máquinas (escalar horizontal) provavelmente resolverá o problema, mas será que estamos sendo eficazes? Saber os custos e se preocupar com eles podem nos fazer ir além disso e nos questionarmos. Será que estamos usando o melhor banco de dados para essa aplicação? Será que estamos utilizando a linguagem correta? Será que estamos utilizando a melhor infraestrutura?
  • Taguear bem os recursos na AWS são essenciais para conseguir granularizar os custos e conseguir identificar os principais ofensores.
  • Para manter um serviço escalável e com um custo reduzido pode ser necessário desapegar de ter alguns recursos importantes ligados o tempo todo (gerando custo) e utilizá-los on-demand, como fizemos com os logs do Cloudwatch.

E pra quem quer trabalhar causando impacto extraordinário e com liberdade para aprender, deixo o link das vagas abertas aqui na OLX Brasil → https://careers.smartrecruiters.com/OLXBrasil

--

--

Robson Alves
Grupo OLX Tech

Bachelors in Computer Science and Software Engineer with 5 years of experience in Web Development (PHP) and that values a code of quality, good practices.