O problema de vazamento de memória no Copilot
(stevenharman.net)Resumo do processo de resolução de um memory leak relacionado a ActiveSupport::Notifications
-
Situação em que ocorreu o memory leak
- A partir de certo momento, o uso de memória do Dyno
webcomeçou a aumentar de forma anormal - O pager começou a disparar, indicando uma situação que parecia ser um memory leak
- A partir de certo momento, o uso de memória do Dyno
-
Resposta imediata
- No Heroku, quando há suspeita de memory leak, reiniciar o Dyno pode servir como solução temporária
- Reiniciar de acordo com o ciclo normal de deploy ou reiniciar manualmente os Dynos próximos do limite de memória
-
Revisão de código suspeito para identificar a causa
- Revisão das mudanças de código implantadas imediatamente antes do pico de memória
- Deploy, uma a uma, de algumas partes de código suspeitas para verificar se o memory leak ocorria
- Como não havia código que parecesse ser a causa, também foram revertidas mudanças de tooling para conferir. Ainda assim, o memory leak continuou
-
Análise do padrão de aumento de memória
- O leak ocorria apenas no Dyno
web. Os Dynos de Sidekiq e Delayed::Job estavam normais - Nem todos os Dynos
webapresentavam leak o tempo todo. Após algumas horas de uso normal, um ou dois, ou até todos os Dynos, começavam a vazar - Suspeitou-se que a causa estivesse ligada a um tráfego específico, e não ao volume de tráfego
- O leak não ocorria em todos os workers do Puma dentro do Dyno; poucos workers concentravam a maior parte do uso total de memória
- O leak ocorria apenas no Dyno
-
Coleta e análise de heap dump
- Uso de
rbtracepara coletar o heap dump do processo Ruby que estava vazando- Acesso por SSH ao dyno com leak via
heroku ps:exec - Seleção, com o comando
ps, do processo worker Ruby que mais consumia memória - Attach do
rbtraceao pid correspondente e início do rastreamento de alocação de memória (ObjectSpace.trace_object_allocations_start) - Coleta do heap dump com
ObjectSpace.dump_all; compactação com gzip caso o arquivo fosse muito grande - Cópia do arquivo de dump para a máquina local com
heroku ps:copy
- Acesso por SSH ao dyno com leak via
- Uso de
reappara visualizar o heap dump como flamegraph- Foi encontrada uma Thread referenciando 1,9GB de memória e, abaixo dela, um Array referenciando 32.067 objetos
- Uso de
sheappara investigar os objetos suspeitos- Descobriu-se que a Thread em questão era uma worker thread do Puma
- Um objeto
ActiveSupport::SubscriberQueueRegistryreferenciava umHash, e abaixo dele havia objetosStringeArray - O
Arrayproblemático acumulava mais de 32 mil objetosActiveSupport::Notifications::Event
- Uso de
-
Hipótese sobre a causa
- Supôs-se que objetos
EventdeActiveSupport::Notificationsestavam sendo acumulados incorretamente no array#children - Presumiu-se que, quando um erro ocorria dentro do bloco de
ActiveSupport::Notifications.instrument, oEventcorrespondente não era removido de#children, causando o vazamento de memória
- Supôs-se que objetos
-
Reprodução local
- Envio local de requisições usando o path e os parâmetros suspeitos encontrados em produção
- Confirmação de que ocorria
URI::InvalidURIErrorjunto com500 Internal Server Error - Verificação de aumento abrupto no uso de memória do dyno de produção que havia recebido aquela requisição
-
Análise detalhada da causa
- Havia um bug relacionado a
Event#childrendeActiveSupport::Notifications, corrigido no Rails 7.1 - Esse problema se combinou com um bug na gem Bugsnag, que levantava
URI::InvalidURIErrordurante oURI.parseao limpar a URL da requisição, gerando o memory leak - Como o erro levantado dentro do bloco de
ActiveSupport::Notifications.subscribenão era tratado, oEventcorrespondente não era removido do array#childrene continuava se acumulando, causando o vazamento
- Havia um bug relacionado a
-
Solução
- Curto prazo: atualizar a versão da gem Bugsnag para que
URI::InvalidURIErrornão gere erro novamente - Longo prazo: atualizar para uma versão 7.x do Rails em que o bug de
ActiveSupport::Notificationsjá esteja corrigido
- Curto prazo: atualizar a versão da gem Bugsnag para que
Opinião do GN⁺
- O processo de descobrir o problema e identificar a causa de forma sistemática é muito impressionante. O texto organiza bem um fluxo básico de análise que vale a pena tentar quando se suspeita de memory leak
- Parece haver desenvolvimento ativo de várias ferramentas open source para coleta, visualização e análise de heap dump em Ruby (
rbtrace,reap,sheapetc.). Mesmo fora do ecossistema Ruby, é importante conhecer boas ferramentas de análise de memória por linguagem e saber aplicá-las ao problema - Muitas vezes a causa de um memory leak está em bugs de bibliotecas ou frameworks usados, mas nem sempre há condições de analisar, corrigir e publicar o patch diretamente. Nesses casos, é importante aplicar o quanto antes uma alternativa viável para contornar o problema. Fornecer um bug report junto com uma solução de mitigação também é uma boa prática
- Foi positivo que o trabalho não tenha parado em apenas resolver o memory leak, mas tenha ido fundo até a root cause do problema. Esse cuidado de investigar o código interno do framework e rastrear a causa fundamental é uma postura analítica importante para desenvolvedores
- No fim, a causa do memory leak estava em uma simples atualização de versão de biblioteca que, à primeira vista, parecia totalmente irrelevante. É um bom exemplo da importância de gerenciar dependências, acompanhar mudanças e monitorar após o deploy, mesmo em alterações aparentemente pequenas
1 comentários
Opiniões no Hacker News
Pode ser resolvido com treinamento de engenharia, sem medo de gerenciamento manual de memória
Caso de perda de 5 milhões de dólares causada por vazamento de memória
Ferramentas de depuração de vazamento de memória e formas de resolver
Elogios ao estilo de escrita