Libelektra: KeySet fechar vazamento de memória

Criado em 4 nov. 2019  ·  32Comentários  ·  Fonte: ElektraInitiative/libelektra

Durante o benchmarking do novo elektrad, experimentei sintomas de vazamento de memória.
O uso de memória aumentaria após a criação de novos identificadores e chamar kdbGet() mas nunca diminuiria após ksClose() e kdbClose() .

Minha primeira suspeita foi o cache do mmap - que provavelmente também causa vazamentos de memória porque a memória do mmaped nunca é liberada - mas desabilitar o cache ao construir com -DPLUGINS="ALL;-cache" não resolveu meu problema.

Etapas para reproduzir o problema

Go> = 1,13

Criei dois testes no repositório go-elektra . Ambos criam um conjunto de chaves de teste com 100.000 chaves.

  1. TestKeySetMemory cria alças e kdbGets conjuntos de chaves em um loop e depois de esperar por 1 segundo - fecha imediatamente o conjunto de chaves + alça novamente antes de reiniciar.
  2. TestKeySetMemoryWithDelayedClose também cria identificadores e preenche conjuntos de chaves com kdbGets - mas atrasa o fechamento do identificador e do conjunto de chaves até que todos os 20 conjuntos de chaves tenham sido carregados. Isso imita o comportamento do servidor web elektrad.

Ambos os testes aguardam 20 segundos após serem concluídos para permitir que o testador veja o consumo de memória do teste por meio de htop ou ferramentas semelhantes.

O primeiro teste, que fecha imediatamente a alça e o conjunto de chaves, mantém a mesma pegada de memória durante todo o teste.

O segundo teste que começa a fechar as alças e conjuntos de chaves somente depois que cada conjunto de chaves é 'carregado' nunca libera memória. Mesmo depois de forçar a coleta de lixo e esperar 20 segundos.

No momento, não tenho ideia de por que o comportamento desses testes é diferente.

Você pode executar os testes clonando o go-elektra repo e executando estes dois comandos no subdiretório ./kdb :

PKG_CONFIG_PATH=<PATH TO elektra.pc FILE> go test  -run "^(TestKeySetMemory)\$"

PKG_CONFIG_PATH=<PATH TO elektra.pc FILE> go test  -run "^(TestKeySetMemoryWithDelayedClose)\$"

resultado esperado

A memória é liberada após kdbClose e ksClose

Resultado atual

A memória não é liberada

Informação do sistema

  • Versão Elektra: mestre
  • Sistema operacional: Arch Linux
  • Os mais recentes versão go-Elektra
bug urgent work in progress

Todos 32 comentários

Parece-me improvável que algo assim escapasse aos nossos testes de valgrind e ASAN. Tenho executado benchmarks semelhantes em C e não pude observar o mesmo. Mesmo com o mmap não consigo observar nada semelhante. Eu não estou descartando isso.

Tem certeza de que as ligações não estão vazando em algum lugar?

Sim, tenho certeza - é possível inspecionar o uso de memória de um aplicativo go durante a execução, essas estatísticas NÃO contêm alocações por código C e estão longe do uso total de memória do aplicativo / teste, o que significa que o grande parte da memória é alocada em C.

Minha suspeita é que as chaves não são liberadas porque o contador de referência é != 0 , embora devesse ser 0 .

Isso significa que as ligações chamam a API C de uma forma que desencadeia esse memleak. Infelizmente não sei ir, então não sei porque isso acontece.

Você pode extrair um pequeno exemplo de C que desencadeia o vazamento? Eu faço algo muito semelhante em benchmarks/kdb.c e não há vazamento aí, então ele precisa fazer exatamente o que seu benchmark faz para acionar o vazamento.

Obrigado por relatar o problema!

Concordo plenamente com @mpranj aqui, precisamos reduzir o problema ao mínimo, com vários idiomas envolvidos é difícil encontrar qualquer coisa.

Você pode escrever um programa C que reproduz a sequência de chamadas emitida a partir das ligações go? Assim que tivermos isso, podemos minimizar o programa C para um caso de teste.

Por falar nisso. tentando executar o comando acima, falhei bem cedo: # 3159

Vou tentar reproduzir isso em C até amanhã

Reproduzi com sucesso este problema aqui . Execute o benchmark benchmarks/memoryleak.c e observe a memória aumentar enquanto obtém os conjuntos de chaves - e NÃO diminuir ao liberar.

Não se esqueça de adicionar uma quantidade perceptível de chaves no banco de dados .. testei com 100k chaves.

Obrigado por criar o exemplo! Vou dar uma olhada.

À primeira vista, há exatamente um vazamento direto, você precisa liberar o parentKey:
https://github.com/raphi011/libelektra/blob/afcbcf5c8138be8de6ba9b1a9e559bc673ff887f/benchmarks/memoryleak.c#L22

Este pequeno vazamento provavelmente não é o motivo de suas observações.

Como uma observação lateral: usando o cache do mmap, noto menos consumo de memória do que sem o cache. Talvez eu precise adicionar isso à minha tese: sorria :

Parece que estamos "vazando" memória via dlopen() / dlclose() . O seguinte pode ser observado usando kdb mas NÃO usando kdb-static (apenas um trecho):

valgrind --leak-check=full --show-leak-kinds=all ./bin/kdb ls user
[...]
==48451== 1,192 bytes in 1 blocks are still reachable in loss record 6 of 6
==48451==    at 0x483AB1A: calloc (vg_replace_malloc.c:762)
==48451==    by 0x400BB11: _dl_new_object (in /usr/lib64/ld-2.30.so)
==48451==    by 0x400642F: _dl_map_object_from_fd (in /usr/lib64/ld-2.30.so)
==48451==    by 0x4009315: _dl_map_object (in /usr/lib64/ld-2.30.so)
==48451==    by 0x400DB24: openaux (in /usr/lib64/ld-2.30.so)
==48451==    by 0x4DFE8C8: _dl_catch_exception (in /usr/lib64/libc-2.30.so)
==48451==    by 0x400DF6A: _dl_map_object_deps (in /usr/lib64/ld-2.30.so)
==48451==    by 0x4013AC3: dl_open_worker (in /usr/lib64/ld-2.30.so)
==48451==    by 0x4DFE8C8: _dl_catch_exception (in /usr/lib64/libc-2.30.so)
==48451==    by 0x401363D: _dl_open (in /usr/lib64/ld-2.30.so)
==48451==    by 0x496139B: dlopen_doit (in /usr/lib64/libdl-2.30.so)
==48451==    by 0x4DFE8C8: _dl_catch_exception (in /usr/lib64/libc-2.30.so)
[...]

Seu exemplo abre muitas alças, mais alças, então fica assim:

==48735== 72,704 bytes in 1 blocks are still reachable in loss record 8 of 8
==48735==    at 0x483880B: malloc (vg_replace_malloc.c:309)
==48735==    by 0x4F860A9: ??? (in /usr/lib64/libstdc++.so.6.0.27)
==48735==    by 0x400FD59: call_init.part.0 (in /usr/lib64/ld-2.30.so)
==48735==    by 0x400FE60: _dl_init (in /usr/lib64/ld-2.30.so)
==48735==    by 0x4013DBD: dl_open_worker (in /usr/lib64/ld-2.30.so)
==48735==    by 0x4A088C8: _dl_catch_exception (in /usr/lib64/libc-2.30.so)
==48735==    by 0x401363D: _dl_open (in /usr/lib64/ld-2.30.so)
==48735==    by 0x48C739B: dlopen_doit (in /usr/lib64/libdl-2.30.so)
==48735==    by 0x4A088C8: _dl_catch_exception (in /usr/lib64/libc-2.30.so)
==48735==    by 0x4A08962: _dl_catch_error (in /usr/lib64/libc-2.30.so)
==48735==    by 0x48C7B08: _dlerror_run (in /usr/lib64/libdl-2.30.so)
==48735==    by 0x48C7429: dlopen@@GLIBC_2.2.5 (in /usr/lib64/libdl-2.30.so)

No exemplo, você está abrindo muitos identificadores do KDB, o que faz com que esse efeito adicione um consumo significativo de memória. Se você abrir apenas uma alça, deve ser menos. @ raphi011 Não sei se você pode usar kdb-static para seus benchmarks. Ao vincular os benchmarks à biblioteca elektra-static , não consigo mais observar o mesmo:

==54836== HEAP SUMMARY:
==54836==     in use at exit: 0 bytes in 0 blocks
==54836==   total heap usage: 6,456,631 allocs, 6,456,631 frees, 272,753,180 bytes allocated
==54836== 
==54836== All heap blocks were freed -- no leaks are possible
==54836== 
==54836== For lists of detected and suppressed errors, rerun with: -s
==54836== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

No momento, não tenho certeza do que podemos fazer sobre os problemas com dlopen() .

Esta é uma simulação simplificada do novo servidor elektrad . Como queremos oferecer suporte a vários usuários configurando o sistema ao mesmo tempo, PRECISAMOS oferecer suporte a vários identificadores (tratamento de conflitos, etc.)

Ainda acho que essa não é a raiz do problema. O problema é que as Chaves / Conjuntos de Chaves não são liberados. Se você alterar a chave pai para "system" que tem muito menos chaves, verá que o consumo de memória é muito menor.

@ markus2330 o que você acha?

EDIT: esqueça isso.

Não acho que estamos perdendo memória aí.
https://gist.github.com/mpranj/bbdf00af308ed3f5b3f0f35bc832756f~~

Posso observar a mesma coisa que você está descrevendo com HTOP e o uso de memória usando o código da essência acima.

Então você está me dizendo que é absolutamente normal que o consumo de memória de elektra nunca diminua?

Então você está me dizendo que é absolutamente normal que o consumo de memória de elektra nunca diminua?

Eu não estou dizendo isso. Estou dizendo que observei o mesmo independentemente de elektra. Acabei de notar que minha observação só é verdadeira quando rodando com valgrind, então talvez valgrind não esteja free () ing até o final por outros motivos.

https://stackoverflow.com/questions/40917024/memory-leak-after-using-calloc-and-free

Parece que é porque você está mudando o ponteiro antes de liberá-lo

Parece que é porque você está mudando o ponteiro antes de liberá-lo

Eu não estou fazendo isso e valgrind não mostra nenhum vazamento para o código na essência. Mas esqueça o exemplo, é irrelevante, pois só não é liberado imediatamente quando executado dentro do valgrind, caso contrário, a memória é free () d praticamente imediatamente.

O que é mais relevante: observei o que você está relatando simplesmente usando benchmark_createkeys que não usa o KDB de forma alguma. Nesse caso, os recursos também não são gratuitos () d imediatamente, mas o valgrind mostra absolutamente 0 vazamentos. Estou perplexo.

@ markus2330 o que você acha?

Com os KeySets apenas, definitivamente não haverá vazamento. (Com o KDB não podemos controlar totalmente tudo, pois os plug-ins podem carregar outras bibliotecas que vazam.)

@ raphi011 você pode criar um PR ou apontar para o exemplo mínimo que reproduz o problema?

https://github.com/raphi011/libelektra/tree/memoryleak aqui está. Estendi meu exemplo anterior, imprimindo quantas chaves NÃO foram liberadas por ksDel por causa das referências de chave > 0 .

Se você executar o benchmark sem valgrind, poderá ver que as chaves não são liberadas.

Não consigo compilar este repo, recebo o erro:

CMake Error: Error processing file: /home/markus/Projekte/Elektra/repos/libelektra/scripts/cmake/ElektraManpage.cmake
make[2]: *** [src/bindings/intercept/env/CMakeFiles/man-kdb-elektrify-getenv.dir/build.make:61: ../doc/man/man1/kdb-elektrify-getenv.1] Fehler 1
make[1]: *** [CMakeFiles/Makefile2:17236: src/bindings/intercept/env/CMakeFiles/man-kdb-elektrify-getenv.dir/all] Fehler 2

Você pode realocar para o mestre principal, por favor?

E, por favor, faça um PR, é muito mais fácil ver as mudanças então.

Se você executar o benchmark sem valgrind, poderá ver que as chaves não são liberadas.

Você pode copiar a saída de uma corrida aqui?

Observei o que você está relatando simplesmente usando benchmark_createkeys, que não usa o KDB. Nesse caso, os recursos também não são gratuitos () d imediatamente, mas o valgrind mostra absolutamente 0 vazamentos. Estou perplexo.

Acho que devemos primeiro seguir esse rastreamento, pois é muito mais fácil de entender se tivermos apenas KeySets.

Talvez tenhamos adicionado uma supressão errada ao valgrind?

Como uma observação lateral: usando o cache do mmap, noto até menos consumo de memória do que sem o cache. Talvez eu precise adicionar isso ao meu sorriso de tese. (cache: ~ 200M vs. sem cache: ~ 600M)

Essas são certamente uma ótima notícia.

Talvez tenhamos adicionado uma supressão errada ao valgrind?

Valgrind relatou não usar supressão quando fiz os testes, mas sua milhagem pode variar.

Acho que encontrei o problema durante meus benchmarks. Para mim, o problema é a alocação ansiosa do meta KeySet para cada chave. Depois de um teste sujo em meu branch, removendo a alocação ansiosa do meta KeySet, o consumo de memória diminui rapidamente após ksDel (). Talvez isso seja corrigido pelo # 3142, pois altera o código do qual estou falando.

Ok, então vamos ver se o # 3142 corrige o problema!

@ raphi011 o problema também ocorreu antes do # 3081?

3142 agora está mesclado. @ raphi011 você pode verificar se o problema ainda ocorre?

Infelizmente sim

Para mim parece pelo menos resolver / mitigar algum problema. Agora posso executar o benchmark com milhões de chaves usando ~ 2 GB de memória, embora tenha travado antes por usar mais de 20 GB de memória para o mesmo benchmark.

Para mim parece pelo menos resolver / mitigar algum problema. Agora posso executar o benchmark com milhões de chaves usando ~ 2 GB de memória, embora tenha travado antes por usar mais de 20 GB de memória para o mesmo benchmark.

Ainda não comparei com a versão anterior, mas tenho quase certeza de que usa menos memória do que antes. Vou verificar hoje!

A memória ainda não foi liberada.

Você precisa chamar malloc_trim(0) após cada free (leia: ksDel ). Isso força o glibc a retornar imediatamente a memória para o sistema operacional. Isso deve corrigir o "comportamento estranho" que vocês estão vendo. Ah, e divirta-se lendo e pesquisando glibc :-)

Eu criei # 3183 para mais testes / consertos.

Eu encontrei um memleak (em um kdbGet as chaves de retorno não foram liberadas).

Mas o número crescente de "ksClose NÃO liberou 532 chaves" é provavelmente apenas os avisos coletados em parentKey. Se você aumentar NUM_RUNS, por exemplo, 100, ele estagna em algum ponto, pois o número de avisos é limitado a 100. Para mim, ele vai para o máximo. "ksClose NÃO liberou 901 chaves". Ter um parentKey por identificador resolveria esse problema.

fechado por # 3183

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

mpranj picture mpranj  ·  3Comentários

mpranj picture mpranj  ·  3Comentários

dmoisej picture dmoisej  ·  3Comentários

markus2330 picture markus2330  ·  3Comentários

markus2330 picture markus2330  ·  4Comentários