1 pontos por GN⁺ 2024-05-12 | 1 comentários | Compartilhar no WhatsApp

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 web começou a aumentar de forma anormal
    • O pager começou a disparar, indicando uma situação que parecia ser um memory leak
  • 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 web apresentavam 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
  • Coleta e análise de heap dump

    • Uso de rbtrace para 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 rbtrace ao 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
    • Uso de reap para 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 sheap para investigar os objetos suspeitos
      • Descobriu-se que a Thread em questão era uma worker thread do Puma
      • Um objeto ActiveSupport::SubscriberQueueRegistry referenciava um Hash, e abaixo dele havia objetos String e Array
      • O Array problemático acumulava mais de 32 mil objetos ActiveSupport::Notifications::Event
  • Hipótese sobre a causa

    • Supôs-se que objetos Event de ActiveSupport::Notifications estavam sendo acumulados incorretamente no array #children
    • Presumiu-se que, quando um erro ocorria dentro do bloco de ActiveSupport::Notifications.instrument, o Event correspondente não era removido de #children, causando o vazamento de memória
  • 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::InvalidURIError junto com 500 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#children de ActiveSupport::Notifications, corrigido no Rails 7.1
    • Esse problema se combinou com um bug na gem Bugsnag, que levantava URI::InvalidURIError durante o URI.parse ao limpar a URL da requisição, gerando o memory leak
    • Como o erro levantado dentro do bloco de ActiveSupport::Notifications.subscribe não era tratado, o Event correspondente não era removido do array #children e continuava se acumulando, causando o vazamento
  • Solução

    • Curto prazo: atualizar a versão da gem Bugsnag para que URI::InvalidURIError não gere erro novamente
    • Longo prazo: atualizar para uma versão 7.x do Rails em que o bug de ActiveSupport::Notifications já esteja corrigido

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, sheap etc.). 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

 
GN⁺ 2024-05-12
Opiniões no Hacker News

Pode ser resolvido com treinamento de engenharia, sem medo de gerenciamento manual de memória

  • Com apenas RAII e regras claras de propriedade, o gerenciamento de memória é uma tarefa de engenharia simples
  • Na verdade, frameworks que insistem em contagem de referências e ponteiros compartilhados tornam a propriedade ambígua e deixam tudo mais difícil
  • Se criou, libera; se transferiu, não se preocupe mais com isso — isso faz parte da disciplina de engenharia
  • Bugs de memória não são diferentes de bugs de lógica, então é natural corrigi-los
  • Recursos do SO (handles, sockets etc.) também são gerenciados manualmente sem gerenciadores automáticos de recursos, então a memória pode ser tratada da mesma forma

Caso de perda de 5 milhões de dólares causada por vazamento de memória

  • Apresenta um caso ocorrido nos anos 90 por causa de um bug de vazamento de memória em um driver de impressora Solaris
  • Na época, em bancos, as transações eram confirmadas por fax, impressas, lidas por telefone para a contraparte e gravadas como validação legal
  • Por causa do vazamento de memória, o driver da impressora caiu e a confirmação não foi impressa, o que levou ao cancelamento da operação e a uma perda de 5 milhões de dólares
  • No fim, depois de reclamações do CEO da Sun, os desenvolvedores acabaram corrigindo o bug

Ferramentas de depuração de vazamento de memória e formas de resolver

  • Com o Valgrind, é fácil encontrar vazamentos em C
  • Se o design estiver bem feito, normalmente a alocação e a liberação ficam na mesma função, então é fácil corrigir
  • Apresenta um caso de vazamento de memória em um servidor de anúncios do Yahoo e uma solução paliativa
  • Uma citação em tom de piada do criador do PHP mostra uma postura mais pragmática do que perfeccionista
  • Dizem que, em Rails, é comum resolver isso com hardware para priorizar a produtividade

Elogios ao estilo de escrita

  • Comentário dizendo que a forma de escrever do autor é agradável, talvez por causa dos emoticons ou da formatação