Como resolvemos o problema de memória no PostgreSQL sem adicionar um byte


Uma breve história sobre a solicitação "pesada" e a solução elegante para o problema


Recentemente, à noite, os alertas começaram a nos acordar: não havia espaço em disco suficiente. Logo descobrimos que o problema está nas tarefas de ETL.


A tarefa ETL foi executada em uma tabela na qual os registros e despejos binários são armazenados. Todas as noites, essa tarefa era remover despejos duplicados e liberar espaço.


Para procurar dumps duplicados, usamos esta consulta:


id, MIN(id) OVER (PARTITION BY blob ORDER BY id) FROM dumps 

A consulta combina os mesmos despejos pelo campo BLOB. Usando a função window, obtemos o identificador da primeira aparência de cada despejo. Então, com essa solicitação, excluímos todos os dumps duplicados.


A solicitação foi executada por algum tempo e, como pode ser visto nos logs, consumiu muita memória. O gráfico mostra como ele obteve espaço livre em disco todas as noites:



Com o tempo, a solicitação exigiu mais memória, as falhas se aprofundaram. E, olhando o plano de execução, vimos imediatamente onde tudo vai:


  Buffers: shared hit=3916, temp read=3807 written=3816 -> Sort (cost=69547.50..70790.83 rows=497332 width=36) (actual time=107.607..127.485 rows=39160) Sort Key: blob, id Sort Method: external merge Disk: 30456kB Buffers: shared hit=3916, temp read=3807 written=3816 -> Seq Scan on dumps (cost=0..8889.32 rows=497332 width=36) (actual time=0.022..8.747 rows=39160) Buffers: shared hit=3916 Execution time: 159.960 ms 

A classificação ocupa muita memória. Em termos de execução, a classificação requer aproximadamente 30 MB de memória de um conjunto de dados de teste.


Porque


O PostgreSQL aloca memória para hash e classificação. A quantidade de memória é controlada pelo parâmetro work_mem . O tamanho padrão de work_mem é 4 MB. Se forem necessários mais de 4 MB para hash ou classificação, o PostgreSQL consome temporariamente espaço em disco.


Nossa consulta consome claramente mais de 4 MB, portanto, o banco de dados usa muita memória. Decidimos: não nos apressaremos e não aumentamos o parâmetro e expandimos o armazenamento. É melhor procurar outra maneira de aparar a memória para classificação .


Classificação econômica


"A quantidade de classificação consumida depende do tamanho do conjunto de dados e da chave de classificação. Você não pode reduzir o conjunto de dados, mas o tamanho da chave é possível .


Para o ponto de referência, usamos o tamanho médio da chave de classificação:


  avg ---------- 780 

Cada chave pesa 780. Para reduzir a chave binária, ela pode ser hash. No PostgreSQL, há md5 para isso (sim, não segurança, mas, para o nosso propósito, ele fará). Vamos ver quanto pesa o BLOB com o MD5:


  avg ----------- 36 

O tamanho da chave com hash no md5 é de 36 bytes. Uma chave com hash pesa apenas 4% da opção original .


Em seguida, lançamos a solicitação original com uma chave de hash:


  id, MIN(id) OVER ( PARTITION BY md5(array_to_string(blob, '') ) ORDER BY id) FROM dumps; 

E o plano de implementação:


  Buffers: shared hit=3916 -> Sort (cost=7490.74..7588.64 rows=39160 width=36) (actual time=349.383..353.045 rows=39160) Sort Key: (md5(array_to_string(blob, ''::text))), id Sort Method: quicksort Memory: 4005kB Buffers: shared hit=3916 -> Seq Scan on dumps (cost=0..4503.40 rows=39160 width=36) (actual time=0.055..292.070 rows=39160) Buffers: shared hit=3916 Execution time: 374.125 ms 

Com uma chave com hash, a solicitação consome apenas 4 megabytes extras, ou seja, pouco mais de 10% dos 30 MB anteriores. Portanto, o tamanho da chave de classificação afeta bastante a quantidade de memória que a classificação consome .


Ainda mais


Neste exemplo, fizemos o hash do BLOB usando o md5 . Hashes criados com o MD5 devem pesar 16 bytes. E nós temos mais:


 md5_size ------------- 32 

Nosso hash tinha exatamente o dobro do tamanho, porque o md5 produz um hash na forma de texto hexadecimal.


No PostgreSQL, você pode usar o MD5 para fazer hash com a extensão pgcrypto . pgcrypto cria MD5 do tipo bytea (em binário) :


 select pg_column_size( digest('foo', 'md5') ) as crypto_md5_size crypto_md5_size --------------- 20 

O hash ainda é 4 bytes a mais do que deveria. Só que o tipo bytea usa esses 4 bytes para armazenar o comprimento do valor, mas não o deixaremos assim.


Acontece que o tipo de uuid no PostgreSQL pesa exatamente 16 bytes e suporta qualquer valor arbitrário, então nos livramos dos quatro bytes restantes:


 uuid_size --------------- 16 

Isso é tudo. 32 bytes com md5 transformam em 16 com uuid .


Eu verifiquei os efeitos da mudança usando um conjunto de dados maior. Os dados em si não podem ser mostrados, mas compartilharei os resultados:



Como você pode ver na tabela, a solicitação problemática original pesava 300 MB (e nos acordou no meio da noite). Com a chave uuid , a classificação levou apenas 7 MB.


Considerações de acompanhamento


Uma solicitação com uma chave de classificação de memória com hash consome menos, mas funciona muito mais devagar:



O hash usa mais CPU; portanto, uma solicitação com um hash é mais lenta. Mas tentamos resolver o problema com espaço em disco; além disso, a tarefa é realizada à noite, para que o tempo não seja um problema. Comprometemo-nos a economizar memória.


Este é um ótimo exemplo do fato de que você nem sempre precisa tentar acelerar as consultas ao banco de dados . É melhor usar o que é equilibrado e extrair o máximo de um mínimo de recursos.

Source: https://habr.com/ru/post/pt437730/


All Articles