paint-brush
Diga adeus às falhas OOMpor@wydfy111
719 leituras
719 leituras

Diga adeus às falhas OOM

por Jiafeng Zhang11m2023/06/12
Read on Terminal Reader

Muito longo; Para ler

Uma solução de gerenciamento de memória mais robusta e flexível com otimizações na alocação de memória, rastreamento de memória e limite de memória.
featured image - Diga adeus às falhas OOM
Jiafeng Zhang HackerNoon profile picture

O que garante a estabilidade do sistema em grandes tarefas de consulta de dados? É um mecanismo eficaz de alocação e monitoramento de memória. É assim que você acelera a computação, evita pontos de acesso de memória, responde prontamente a memória insuficiente e minimiza erros OOM.




Do ponto de vista de um usuário de banco de dados, como eles sofrem com o gerenciamento de memória ruim? Esta é uma lista de coisas que costumavam incomodar nossos usuários:


  • Os erros de OOM causam falhas nos processos de back-end. Para citar um dos membros da nossa comunidade: Olá, Apache Doris, não há problema em desacelerar ou falhar em algumas tarefas quando você está com falta de memória, mas lançar um tempo de inatividade simplesmente não é legal.


  • Os processos de back-end consomem muito espaço de memória, mas não há como encontrar a tarefa exata para culpar ou limitar o uso de memória para uma única consulta.


  • É difícil definir um tamanho de memória adequado para cada consulta, portanto, é provável que uma consulta seja cancelada mesmo quando há muito espaço de memória.


  • As consultas de alta simultaneidade são desproporcionalmente lentas e os pontos de acesso de memória são difíceis de localizar.


  • Os dados intermediários durante a criação da HashTable não podem ser liberados para os discos, portanto, as consultas de junção entre duas tabelas grandes geralmente falham devido ao OOM.


Felizmente, esses dias sombrios ficaram para trás porque melhoramos nosso mecanismo de gerenciamento de memória de baixo para cima. Agora prepare-se; as coisas vão ser intensas.

Alocação de memória

No Apache Doris, temos uma única interface para alocação de memória: Allocator . Ele fará os ajustes que julgar apropriados para manter o uso da memória eficiente e sob controle.


Além disso, os MemTrackers estão disponíveis para rastrear o tamanho da memória alocada ou liberada, e três estruturas de dados diferentes são responsáveis pela grande alocação de memória na execução do operador (chegaremos a elas imediatamente).




Estruturas de dados na memória

Como consultas diferentes têm diferentes padrões de hotspot de memória em execução, o Apache Doris fornece três estruturas de dados na memória diferentes: Arena , HashTable e PODArray . Eles estão todos sob o reinado do Alocador.



  1. Arena

A Arena é um pool de memória que mantém uma lista de chunks, que devem ser alocados mediante solicitação do Alocador. Os pedaços suportam alinhamento de memória. Eles existem durante toda a vida útil da Arena e serão liberados após a destruição (geralmente quando a consulta é concluída).


Os pedaços são usados principalmente para armazenar os dados serializados ou desserializados durante o Shuffle ou as chaves serializadas em HashTables.


O tamanho inicial de um pedaço é 4096 bytes. Se o chunk atual for menor que a memória solicitada, um novo chunk será adicionado à lista.


Se o bloco atual for menor que 128M, o novo bloco dobrará de tamanho; se for maior que 128M, o novo chunk será, no máximo, 128M maior do que o necessário.


A pequena parte antiga não será alocada para novas solicitações. Há um cursor para marcar a linha divisória entre os chunks alocados e os não alocados.


  1. HashTable

HashTables são aplicáveis para Hash Joins, agregações, operações de conjunto e funções de janela. A estrutura PartitionedHashTable suporta não mais que 16 sub-HashTables. Ele também suporta a fusão paralela de HashTables e cada sub-Hash Join pode ser escalado independentemente.


Isso pode reduzir o uso geral da memória e a latência causada pelo dimensionamento.


Se a HashTable atual for menor que 8M, ela será dimensionada por um fator de 4;

Se for maior que 8M, será dimensionado por um fator de 2;

Se for menor que 2G, será redimensionado quando estiver 50% cheio;

e se for maior que 2G, será redimensionado quando estiver 75% cheio.


As HashTables recém-criadas serão pré-dimensionadas com base na quantidade de dados que terão. Também fornecemos diferentes tipos de HashTables para diferentes cenários. Por exemplo, para agregações, você pode aplicar PHmap.


  1. PODArray

PODArray, como o nome sugere, é um array dinâmico de POD. A diferença entre ele e std::vector é que PODArray não inicializa elementos. Ele suporta alinhamento de memória e algumas interfaces de std::vector .


Ele é escalado por um fator de 2. Na destruição, ao invés de chamar a função destruidora para cada elemento, ele libera a memória de todo o PODArray. PODArray é usado principalmente para salvar strings em colunas e é aplicável em muitos cálculos de função e filtragem de expressão.

Interface de Memória

Como a única interface que coordena Arena, PODArray e HashTable, o Allocator executa a alocação de mapeamento de memória (MMAP) para solicitações maiores que 64M.


Aqueles menores que 4K serão alocados diretamente do sistema via malloc/free; e os intermediários serão acelerados por um ChunkAllocator de armazenamento em cache de uso geral, que traz um aumento de desempenho de 10% de acordo com nossos resultados de benchmarking.


O ChunkAllocator tentará recuperar um pedaço do tamanho especificado da FreeList do núcleo atual de maneira livre de bloqueio; se tal pedaço não existir, ele tentará de outros núcleos de maneira baseada em bloqueio; se isso ainda falhar, ele solicitará o tamanho de memória especificado do sistema e o encapsulará em um bloco.


Escolhemos Jemalloc em vez de TCMalloc depois de experimentar os dois. Tentamos o TCMalloc em nossos testes de alta simultaneidade e notamos que o Spin Lock no CentralFreeList ocupava 40% do tempo total de consulta.


A desativação da "decommit de memória agressiva" tornou as coisas melhores, mas trouxe muito mais uso de memória, então tivemos que usar um thread individual para reciclar regularmente o cache. Jemalloc, por outro lado, foi mais eficiente e estável em consultas de alta simultaneidade.


Após o ajuste fino para outros cenários, ele apresentou o mesmo desempenho que o TCMalloc, mas consumiu menos memória.

Reutilização de memória

A reutilização de memória é amplamente executada na camada de execução do Apache Doris. Por exemplo, os blocos de dados serão reutilizados durante a execução de uma consulta. Durante o Shuffle, haverá dois blocos na ponta do Sender e eles funcionam alternadamente, um recebendo dados e outro no transporte RPC.


Ao ler um tablet, Doris reutilizará a coluna de predicado, implementará a leitura cíclica, filtrará, copiará os dados filtrados para o bloco superior e, em seguida, limpará.


Ao ingerir dados em uma tabela de chave agregada, uma vez que a MemTable que armazena dados em cache atinge um determinado tamanho, ela será pré-agregada e, em seguida, mais dados serão gravados.


A reutilização de memória também é executada na varredura de dados. Antes do início da varredura, um número de blocos livres (dependendo do número de scanners e threads) será alocado para a tarefa de varredura.


Durante cada agendamento do scanner, um dos blocos livres será passado para a camada de armazenamento para leitura dos dados.


Após a leitura dos dados, o bloco será colocado na fila do produtor para consumo dos operadores superiores na computação subsequente. Uma vez que um operador superior tenha copiado os dados de cálculo do bloco, o bloco voltará para os blocos livres para o próximo escalonamento do scanner.


O thread que pré-aloca os blocos livres também será responsável por liberá-los após a varredura de dados, portanto, não haverá despesas extras. O número de blocos livres de alguma forma determina a simultaneidade da varredura de dados.

Rastreamento de memória

O Apache Doris usa MemTrackers para acompanhar a alocação e liberação de memória enquanto analisa pontos de acesso de memória. Os MemTrackers mantêm registros de cada consulta de dados, ingestão de dados, tarefa de compactação de dados e o tamanho da memória de cada objeto global, como Cache e TabletMeta.


Ele suporta contagem manual e rastreamento automático MemHook. Os usuários podem visualizar o uso da memória em tempo real no back-end Doris em uma página da Web.

Estrutura do MemTrackers

O sistema MemTracker antes do Apache Doris 1.2.0 estava em uma estrutura de árvore hierárquica, consistindo em process_mem_tracker, query_pool_mem_tracker, query_mem_tracker, instance_mem_tracker, ExecNode_mem_tracker e assim por diante.


MemTrackers de duas camadas vizinhas são de um relacionamento pai-filho. Portanto, quaisquer erros de cálculo em um MemTracker filho serão acumulados e resultarão em uma escala maior de credibilidade.



No Apache Doris 1.2.0 e mais recente, tornamos a estrutura do MemTrackers muito mais simples. Os MemTrackers são divididos apenas em dois tipos com base em suas funções: MemTracker Limiter e os outros.


O MemTracker Limiter, que monitora o uso da memória, é exclusivo em cada tarefa de consulta/ingestão/compactação e objeto global; enquanto os outros MemTrackers rastreiam os hotspots de memória na execução da consulta, como HashTables nas funções Join/Aggregation/Sort/Window e dados intermediários na serialização, para fornecer uma imagem de como a memória é usada em diferentes operadores ou fornecer uma referência para controle de memória em descarga de dados.


A relação pai-filho entre o MemTracker Limiter e outros MemTrackers só se manifesta na impressão de instantâneos. Você pode pensar em tal relacionamento como um link simbólico. Eles não são consumidos ao mesmo tempo, e o ciclo de vida de um não afeta o do outro.


Isso torna muito mais fácil para os desenvolvedores entendê-los e usá-los.


Os MemTrackers (incluindo o MemTracker Limiter e outros) são colocados em um grupo de mapas. Eles permitem que os usuários imprimam instantâneos gerais do tipo MemTracker, instantâneos de tarefas de consulta/carga/compactação e descubram a consulta/carga com o maior uso de memória ou o maior uso excessivo de memória.



Como Funciona o MemTracker

Para calcular o uso de memória de uma determinada execução, um MemTracker é adicionado a uma pilha no Thread Local do thread atual. Ao recarregar o malloc/free/realloc em Jemalloc ou TCMalloc, o MemHook obtém o tamanho real da memória alocada ou liberada e o registra no Thread Local da thread atual.


Quando uma execução é concluída, o MemTracker relevante será removido da pilha. Na parte inferior da pilha está o MemTracker, que registra o uso da memória durante todo o processo de execução de consulta/carregamento.


Agora, deixe-me explicar com um processo de execução de consulta simplificado.


  • Após o início de um nó de back-end Doris, o uso de memória de todos os threads será registrado no Process MemTracker.


  • Quando uma consulta é enviada, um Query MemTracker será adicionado à pilha Thread Local Storage(TLS) no thread de execução do fragmento.


  • Depois que um ScanNode é agendado, um ScanNode MemTracker será adicionado à pilha Thread Local Storage(TLS) no thread de execução do fragmento. Então, qualquer memória alocada ou liberada neste thread será registrada tanto no Query MemTracker quanto no ScanNode MemTracker.


  • Depois que um Scanner é agendado, um Query MemTracker e um Scanner MemTracker serão adicionados à pilha TLS do encadeamento do Scanner.


  • Quando a varredura for concluída, todos os MemTrackers na pilha TLS do thread do scanner serão removidos. Quando o agendamento do ScanNode for concluído, o ScanNode MemTracker será removido do thread de execução do fragmento. Então, da mesma forma, quando um nó de agregação é agendado, um AggregationNode MemTracker será adicionado à pilha TLS do encadeamento de execução do fragmento e será removido após a conclusão do agendamento.


  • Se a consulta for concluída, o Query MemTracker será removido da pilha TLS do encadeamento de execução do fragmento. Neste ponto, esta pilha deve estar vazia. Em seguida, no QueryProfile, você pode visualizar o pico de uso de memória durante toda a execução da consulta, bem como em cada fase (varredura, agregação, etc.).



Como usar o Mem Tracker

A página da Web de back-end Doris demonstra o uso de memória em tempo real, que é dividido em tipos: Consulta/Carga/Compactação/Global. O consumo de memória atual e o consumo de pico são mostrados.



Os tipos globais incluem MemTrackers de Cache e TabletMeta.



Nos tipos de consulta, você pode ver o consumo de memória atual e o consumo de pico da consulta atual e os operadores que ela envolve (você pode dizer como eles estão relacionados pelos rótulos). Para estatísticas de memória de consultas históricas, você pode verificar os logs de auditoria Doris FE ou os logs BE INFO.



Limite de memória

Com rastreamento de memória amplamente implementado em back-ends Doris, estamos um passo mais perto de eliminar OOM, a causa do tempo de inatividade do back-end e falhas de consulta em grande escala. A próxima etapa é otimizar o limite de memória em consultas e processos para manter o uso de memória sob controle.

Limite de memória na consulta

Os usuários podem colocar um limite de memória em cada consulta. Se esse limite for excedido durante a execução, a consulta será cancelada. Mas desde a versão 1.2, permitimos Memory Overcommit, que é um controle de limite de memória mais flexível.


Se houver recursos de memória suficientes, uma consulta pode consumir mais memória do que o limite sem ser cancelada, portanto, os usuários não precisam prestar atenção extra ao uso da memória; se não houver, a consulta aguardará até que um novo espaço de memória seja alocado somente quando a memória recém-liberada não for suficiente para a consulta, a consulta será cancelada.


Enquanto no Apache Doris 2.0, percebemos a segurança de exceção para consultas. Isso significa que qualquer alocação de memória insuficiente fará com que a consulta seja imediatamente cancelada, o que evita o trabalho de verificar o status "Cancelar" nas etapas subsequentes.

Limite de memória no processo

Regularmente, o back-end Doris recupera a memória física dos processos e o tamanho da memória atualmente disponível do sistema. Enquanto isso, ele coleta instantâneos do MemTracker de todas as tarefas de Consulta/Carga/Compactação.


Se um processo de back-end exceder seu limite de memória ou se houver memória insuficiente, Doris liberará algum espaço de memória limpando o Cache e cancelando várias consultas ou tarefas de ingestão de dados. Estes serão executados por um thread individual do GC regularmente.



Se a memória do processo consumida estiver acima do SoftMemLimit (81% da memória total do sistema, por padrão) ou se a memória disponível do sistema cair abaixo do Warning Water Mark (menos de 3,2 GB), o Minor GC será acionado.


Neste momento, a execução da consulta será pausada na etapa de alocação de memória, os dados armazenados em cache nas tarefas de ingestão de dados serão descarregados à força e parte do cache da página de dados e do cache de segmento desatualizado serão liberados.


Se a memória recém-liberada não cobrir 10% da memória do processo, com Memory Overcommit habilitado, Doris começará a cancelar as consultas que são os maiores "overcommitters" até que a meta de 10% seja atingida ou todas as consultas sejam canceladas.


Em seguida, Doris reduzirá o intervalo de verificação da memória do sistema e o intervalo do GC. As consultas continuarão depois que mais memória estiver disponível.


Se a memória do processo consumida estiver além do MemLimit (90% da memória total do sistema, por padrão) ou se a memória disponível do sistema cair abaixo da marca d'água baixa (menos de 1,6 GB), o GC completo será acionado.


Neste momento, as tarefas de ingestão de dados serão interrompidas e todo o cache de página de dados e a maioria dos outros caches serão liberados.


Se, após todas essas etapas, a memória recém-liberada não cobrir 20% da memória do processo, Doris examinará todos os MemTrackers e encontrará as consultas e tarefas de ingestão que mais consomem memória e as cancelará uma a uma.


Somente depois que a meta de 20% for atingida, o intervalo de verificação de memória do sistema e o intervalo de GC serão estendidos e as consultas e tarefas de ingestão continuarão. (Uma operação de coleta de lixo geralmente leva centenas de μs a dezenas de ms.)

Influências e Resultados

Após otimizações na alocação de memória, rastreamento de memória e limite de memória, aumentamos substancialmente a estabilidade e o desempenho de alta simultaneidade do Apache Doris como uma plataforma de armazenamento de dados analíticos em tempo real. A falha do OOM no back-end é uma cena rara agora.


Mesmo se houver um OOM, os usuários podem localizar a raiz do problema com base nos logs e corrigi-lo. Além disso, com limites de memória mais flexíveis para consultas e ingestão de dados, os usuários não precisam gastar esforços extras cuidando da memória quando o espaço de memória é adequado.


Na próxima fase, planejamos garantir a conclusão das consultas em excesso de alocação de memória, o que significa que menos consultas terão que ser canceladas por falta de memória.


Dividimos esse objetivo em direções específicas de trabalho: segurança de exceção, isolamento de memória entre grupos de recursos e o mecanismo de liberação de dados intermediários.


Se você quiser conhecer nossos desenvolvedores, é aqui que você nos encontra .