O Bugsnag recentemente adicionou suporte para processar minidumps para que os clientes possam rastrear travamentos nativos no Electron ou travamentos gerados ao usar Breakpad ou Crashpad .
A adição de suporte a minidump veio com vários desafios técnicos que tivemos que enfrentar para garantir que pudéssemos processar os arquivos de maneira eficiente e escalável, sem afetar nossa taxa de transferência normal de processamento de eventos de erro.
Um minidump é um arquivo que é gerado por alguns processos quando eles falham. Eles são menores que os dumps principais e apenas fornecem os dados necessários para executar operações básicas de depuração.
O formato de arquivo minidump foi inventado para uso no Windows quando o sistema operacional encontra um erro inesperado. Posteriormente, ferramentas como o Breakpad e o Crashpad do Google adotaram o mesmo formato para gerar dumps de aplicativos em várias plataformas. Os aplicativos criados com o Electron também geram arquivos minidump para falhas nativas (já que o Electron usa o Crashpad).
O arquivo minidump contém informações sobre o processo no momento da falha e inclui detalhes como:
Dentro do minidespejo, há uma pilha de tempo de execução de cada encadeamento ativo (no momento em que o erro ocorreu) e os valores de registro para esses encadeamentos.
Esses detalhes podem ser usados para percorrer a pilha e produzir um rastreamento de pilha para cada thread. Em alguns casos, é possível obter um rastreamento de pilha válido (embora não simbolizado) usando apenas esse método, mas em outros casos precisamos de informações adicionais do quadro de chamada (CFI) fornecidas nos arquivos de depuração.
Os registros Call Frame Information (CFI) descrevem como restaurar os valores de registro em um determinado endereço de máquina. Esses dados geralmente são fornecidos nos arquivos de depuração e permitem que um rastreamento de pilha mais confiável seja gerado ao percorrer a pilha.
Sem as informações CFI, o stack walker pode precisar escanear a pilha para tentar encontrar o quadro de chamada, mas isso pode ser menos confiável e produzir rastreamentos de pilha inválidos.
Uma vez obtido um rastreamento de pilha, ele conterá apenas os endereços de cada quadro. Para produzir um rastreamento de pilha significativo (com o nome da função, arquivo de origem e número de linha de origem para cada quadro), precisamos "simbolizá-lo", ou seja, aplicar os símbolos de depuração a eles.
O arquivo de depuração relevante do compilador (por exemplo, dSYMs para macOS) pode ser usado para isso, mas o Breakpad definiu seu próprio formato de arquivo de símbolo que pode ser usado.
O arquivo de símbolo Breakpad é mais simples do que a maioria dos arquivos de depuração gerados pelo compilador (não contém detalhes como a árvore de sintaxe abstrata), mas fornece os detalhes necessários, em um formato legível, para simbolizar o rastreamento de pilha.
O Breakpad fornece várias ferramentas para ajudar no processamento manual de um minidump ou no upload para um servidor/serviço dedicado para processamento:
minidump_stackwalk Recebe um arquivo minidump e arquivos de símbolo Breakpad opcionais e gera o rastreamento de pilha para cada thread junto com outras informações (como o motivo da falha, valores de registro para cada quadro, detalhes do sistema operacional etc.). Esta é uma ferramenta útil para analisar minidumps e obter deles rastreamentos de pilha significativos.
minidump_dump Fornece informações mais detalhadas sobre o minidump (como detalhes de cada um dos fluxos dentro do minidump).
minidump_upload Carrega arquivos minidump para um servidor dedicado para processamento (como Bugsnag).
dump_syms Gera arquivos de símbolo Breakpad a partir do binário do aplicativo e arquivos de depuração.
symupload Carrega arquivos de símbolos do Breakpad para um servidor dedicado para processamento (como o Bugsnag).
Um exemplo da saída da ferramenta minidump_stackwalk com os arquivos de símbolos relevantes pode ser visto abaixo:
-- CODE language-plaintext --Sistema operacional: Windows NT 10.0.19041 1151CPU: amd64 family 23 model 24 step 1 8 CPUs
GPU: DESCONHECIDA
Motivo da falha: Exceção C++ não tratada Endereço da falha: 0x7ff887f54ed9Tempo de atividade do processo: 4 segundos
Tópico 0 (travado) 0 KERNELBASE.dll + 0x34ed9
rax = 0x655c67616e736775 rdx = 0x6f6d2d6576697461 rcx = 0x6e2d7070635c656d rbx = 0x00007ff87f731600 rsi = 0x000000b80f3fcb70 rdi = 0x000000b80f3fca40 rbp = 0x000000b80f3fca10 rsp = 0x000000b80f3fc8d0 r8 = 0xaaaaaaaa0065646f r9 = 0xaaaaaaaaaaaaaaaa r10 = 0xaaaaaaaaaaaaaaaa r11 = 0xaaaaaaaaaaaaaaaa r12 = 0x00007ff87f6f1ba0 r13 = 0x000010ff084af60d r14 = 0xffffffff00000000 r15 = 0x0000000000000420 rip = 0x00007ff887f54ed9 Found by: given as instruction pointer in context 1 KERNELBASE.dll + 0x34ed9 rbp = 0x000000b80f3fca10 rsp = 0x000000b80f3fc908 rip = 0x00007ff887f54ed9 Found by: stack scanning 2 ntdll.dll + 0x34a5f rbp = 0x000000b80f3fca10 rsp = 0x000000b80f3fc960 rip = 0x00007ff88a6a4a5f Found by: stack scanning 3 my_example.node!CxxThrowException [throw.cpp: 131 + 0x14] rbp = 0x000000b80f3fca10 rsp = 0x000000b80f3fc9b0 rip = 0x00007ff87f6fab75 Encontrado por: varredura de pilha 4 my_example.node!RunExample(Nallan:I:Function:I: nfo v8::Value const &) [my_example.cpp : 26 + 0x22] rbp = 0x000000b80f3fca10 rsp = 0x000000b80f3fca20 rip = 0x00007ff87f6f1ec2 Encontrado por: call frame info.. .
Enfrentamos alguns desafios técnicos ao adicionar suporte minidump ao Bugsnag.
Os minidespejos variam muito de nossas cargas JSON de eventos de erro usuais, portanto, tivemos que garantir que pudéssemos processá-los de maneira eficiente, resiliente e escalável.
Os arquivos minidump podem ser muito maiores do que as cargas úteis normais que recebemos; nossas cargas úteis normais têm em média ~ 20 KB, enquanto os minidespejos têm normalmente centenas de kilobytes de tamanho. Além disso, os minidespejos podem ficar muito grandes (dezenas de megabytes) se houver muitos encadeamentos ativos ou encadeamentos com grandes pilhas.
Normalmente, quando recebemos payloads de eventos de erro, nós os adicionamos a uma fila Kafka para processamento assíncrono, de forma que possamos lidar com quaisquer pendências com uploads.
Precisávamos ter certeza de que o mecanismo de enfileiramento seria confiável se estivéssemos enfileirando arquivos de minidespejo maiores. Os arquivos minidump são bem compactados (normalmente em cerca de 10% do tamanho original), mas ainda havia o risco de que os arquivos compactados fossem muito grandes.
Fizemos alguns testes de carga com mensagens de vários tamanhos em uma instância interna do Kafka e descobrimos que:
A taxa de transferência de dados e o atraso da replicação não foram realmente afetados pelo tamanho dos arquivos.
O número de mensagens que podiam ser processadas por segundo diminuiu à medida que o tamanho médio do arquivo aumentou.
Essa taxa de transferência de mensagens reduzida foi significativa apenas ao enfileirar simultaneamente muitos arquivos particularmente grandes, mas prevemos que eles devem ser muito raros e, portanto, o Kafka será adequado para esse propósito.
Ao simbolizar um minidump usando a ferramenta minidump_stackwalk do Breakpad, pode demorar muito mais para processar o minidump quando os arquivos de símbolo do Breakpad são fornecidos (devido ao tempo que leva para carregar os arquivos de símbolo, analisá-los e procurar os dados de símbolo relevantes).
Em uma amostra do minidespejo de elétrons, demorou 20ms sem os arquivos de símbolo do Breakpad e 14s com eles!
O tempo de processamento mais lento não é uma grande preocupação ao simbolizar manualmente um único minidump, mas precisamos garantir que possamos processar e simbolizar minidumps da maneira mais eficiente possível, para que possamos manter um alto rendimento do processamento de minidump.
Para isso, implementamos nossa própria lógica de simbolização. O formato de arquivo de símbolo do Breakpad é simples e bem documentado, o que significa que podemos analisar o arquivo para produzir um arquivo de mapeamento personalizado que permite uma pesquisa de endereço fácil.
O arquivo sob medida é muito maior que o arquivo de símbolo do Breakpad, mas também é muito mais eficiente para realizar as pesquisas. Fazer esse pré-processamento do arquivo de símbolo do Breakpad antecipadamente significa que o tempo necessário para processar um minidespejo é significativamente reduzido (ao custo de maiores requisitos de armazenamento para os arquivos de símbolo).
No projeto inicial do processamento de minidump, omitimos completamente o uso de arquivos de símbolo Breakpad ao percorrer a pilha (para melhorar a eficiência), mas logo percebemos que isso às vezes resultava em rastreamentos de pilha inválidos devido à falta de dados de informações do quadro de chamada.
Sabíamos que, se passássemos os arquivos completos de símbolos do Breakpad para a caminhada da pilha, seria lento (devido ao fato de tentar simbolizar o rastreamento da pilha também), então optamos por produzir uma versão reduzida do arquivo que continha apenas o informações necessárias para percorrer a pilha.
Isso reduziu muito o tamanho do arquivo de símbolo do Breakpad, bem como o tempo que levou para processar o minidespejo, mas ainda não foi muito eficiente (leva 1,5s para um exemplo de minidespejo de elétrons).
Portanto, exploramos a opção de serializar o arquivo de símbolo do Breakpad aparado para que ele pudesse ser lido com mais eficiência (em vez de ter que analisar o arquivo toda vez). O uso de uma versão serializada do arquivo reduziu o tempo de processamento de 1,5s para 200ms.
Essa melhoria de desempenho significa que devemos ser capazes de suportar um throughput mais alto de minidumps por instância do serviço, o que significa que podemos manter os custos de infraestrutura baixos.
À medida que a adoção do novo recurso aumenta, monitoraremos o uso de nossa infraestrutura de perto para garantir que continuemos a processar minidespejos de maneira eficiente e verificar se há outras melhorias de desempenho a serem feitas.
O Bugsnag ajuda você a priorizar e corrigir bugs de software enquanto melhora a estabilidade do aplicativo.