Economize muito dinheiro em grandes volumes no PostgreSQL

Continuando o tópico de registrar grandes fluxos de dados, levantados no artigo anterior sobre particionamento , consideramos as maneiras pelas quais você pode reduzir o tamanho "físico" do PostgreSQL armazenado no servidor e seu impacto no desempenho do servidor.

É sobre configurações do TOAST e alinhamento de dados . “Em média”, esses métodos economizarão não muitos recursos, mas sem nenhuma modificação no código do aplicativo.


No entanto, nossa experiência acabou sendo muito produtiva nesse sentido, uma vez que o repositório de quase qualquer monitoramento é, por sua natureza, principalmente apenas em termos de dados registrados. E se você estiver interessado em saber como ensinar um banco de dados a gravar em um disco, em vez da metade de 200 MB / s - peço um corte.

Pequenos segredos do Big Data


De acordo com o perfil do nosso serviço , ele recebe regularmente pacotes de texto dos logs .

E como o complexo VLSI , cujos bancos de dados estamos monitorando, é um produto multicomponente com estruturas de dados complexas, as consultas para obter o desempenho máximo são obtidas por esses "multi-volumes" com lógica algorítmica complexa . Portanto, o volume de cada instância individual da solicitação ou o plano de execução resultante no log que chega até nós acaba sendo "médio" bastante grande.

Vejamos a estrutura de uma das tabelas na qual escrevemos os dados "brutos" - ou seja, aqui está o texto original da entrada do log:

CREATE TABLE rawdata_orig(
  pack -- PK
    uuid NOT NULL
, recno -- PK
    smallint NOT NULL
, dt --  
    date
, data --  
    text
, PRIMARY KEY(pack, recno)
);

Um prato típico (já particionado, é claro, portanto, é um modelo de seção), onde o mais importante é o texto. Às vezes bastante volumoso.

Lembre-se de que o tamanho "físico" de um registro no PG não pode ocupar mais de uma página de dados, mas o tamanho "lógico" é uma questão completamente diferente. Para escrever um valor de volume (varchar / text / bytea) no campo, a tecnologia TOAST é usada :
O PostgreSQL usa um tamanho de página fixo (geralmente 8 KB) e não permite que tuplas se espalhem por várias páginas. Portanto, é impossível armazenar diretamente valores de campo muito grandes. Para superar essa limitação, valores de campo grandes são compactados e / ou divididos em várias linhas físicas. Isso passa despercebido pelo usuário e afeta a maior parte do código do servidor. Este método é conhecido como TOAST ...

De fato, para cada tabela com campos "potencialmente grandes" , uma tabela emparelhada é criada automaticamente com o "fatiamento" de cada registro "grande" em segmentos de 2 KB:

TOAST(
  chunk_id
    integer
, chunk_seq
    integer
, chunk_data
    bytea
, PRIMARY KEY(chunk_id, chunk_seq)
);

Ou seja, se precisarmos escrever uma linha com um valor "grande" data, o registro real ocorrerá não apenas na tabela principal e em sua PK, mas também em TOAST e em sua PK .

Reduza o efeito TOAST


Mas a maioria dos registros aqui ainda não é tão grande, eles devem caber em 8 KB - como você economizaria isso? ...

Aqui o atributo STORAGEna coluna da tabela vem em nosso auxílio :
  • EXTENDED permite compactação e armazenamento separado. Esta é a opção padrão para a maioria dos tipos de dados compatíveis com o TOAST. Primeiro, é feita uma tentativa de executar a compactação e, em seguida, ela é salva fora da tabela se a linha ainda for muito grande.
  • PRINCIPAL permite compactação, mas não armazenamento separado. (De fato, o armazenamento separado, no entanto, será executado para essas colunas, mas apenas como último recurso , quando não houver outra maneira de reduzir a linha para que ela caiba na página.)
Na verdade, é exatamente isso que precisamos para o texto - aperte o máximo possível e, mesmo que não se ajuste, coloque-o no TOAST . Você pode fazer isso diretamente "on the fly", com um comando:

ALTER TABLE rawdata_orig ALTER COLUMN data SET STORAGE MAIN;

Como avaliar o efeito


Como o fluxo de dados muda todos os dias, não podemos comparar números absolutos, mas, em termos relativos, quanto menor a proporção que registramos no TOAST, melhor. Mas há um perigo - quanto mais temos o volume "físico" de cada registro individual, mais "amplo" o índice se torna, porque precisamos cobrir mais páginas de dados.

Seção antes das alterações :
heap  = 37GB (39%)
TOAST = 54GB (57%)
PK    =  4GB ( 4%)

Seção após alterações :
heap  = 37GB (67%)
TOAST = 16GB (29%)
PK    =  2GB ( 4%)

De fato, começamos a escrever no TOAST 2 vezes menos , o que descarregava não apenas o disco, mas também a CPU:



Observo que também começamos a "ler" menos o disco, não apenas "gravar" - porque quando você insere um registro em alguma tabela, também é necessário "subtrair" uma parte da árvore de cada um dos índices para determinar sua posição futura neles.

Quem no PostgreSQL 11 vive bem


Após a atualização para o PG11, decidimos continuar “ajustando” o TOAST e percebemos que, começando com esta versão, o parâmetro ficou disponível para configuração toast_tuple_target:
O código de processamento TOAST é acionado somente quando o valor da linha a ser armazenada na tabela for maior que TOAST_TUPLE_THRESHOLD bytes (geralmente 2 KB). O código TOAST comprime e / ou move os valores dos campos para fora da tabela até que o valor da linha seja menor que TOAST_TUPLE_TARGET bytes (variável, geralmente também 2 KB) ou se torna impossível reduzir o tamanho.
Decidimos que os dados que normalmente temos são "muito curtos" ou imediatamente "muito longos"; portanto, decidimos nos limitar ao menor valor possível:

ALTER TABLE rawplan_orig SET (toast_tuple_target = 128);

Vamos ver como as novas configurações afetaram o carregamento do disco após a migração:


Não é ruim! A fila média de um disco foi reduzida em cerca de 1,5 vezes e a "ocupação" do disco - em 20%! Mas talvez isso tenha afetado a CPU?


Pelo menos, definitivamente não piorou. No entanto, é difícil julgar se mesmo esses volumes ainda não conseguem aumentar a carga média da CPU acima de 5% .

De uma mudança de posição, a soma ... muda!


Como você sabe, um centavo economiza um rublo e, com nossos volumes de armazenamento de cerca de 10 TB / mês, até uma pequena otimização pode gerar um bom lucro. Portanto, chamamos a atenção para a estrutura física de nossos dados - como exatamente os “campos” são dispostos dentro do registro de cada tabela.

Porque, devido ao alinhamento dos dados, isso afeta diretamente o volume resultante :
Muitas arquiteturas fornecem alinhamento de dados através dos limites das palavras de máquina. Por exemplo, em um sistema x86 de 32 bits, números inteiros (tipo inteiro, ocupa 4 bytes) serão alinhados no limite de palavras de 4 bytes, além de números de ponto flutuante de precisão dupla (tipo de precisão dupla, 8 bytes). E em um sistema de 64 bits, os valores duplos serão alinhados na borda das palavras de 8 bytes. Esse é outro motivo de incompatibilidade.

Devido ao alinhamento, o tamanho da linha da tabela depende da ordem dos campos. Normalmente, esse efeito não é muito perceptível, mas, em alguns casos, pode levar a um aumento significativo no tamanho. Por exemplo, se você colocar campos dos tipos char (1) e número inteiro misto, entre eles, como regra, 3 bytes serão desperdiçados por nada.

Vamos começar com modelos sintéticos:

SELECT pg_column_size(ROW(
  '0000-0000-0000-0000-0000-0000-0000-0000'::uuid
, 0::smallint
, '2019-01-01'::date
));
-- 48 

SELECT pg_column_size(ROW(
  '2019-01-01'::date
, '0000-0000-0000-0000-0000-0000-0000-0000'::uuid
, 0::smallint
));
-- 46 

De onde veio o par extra de bytes no primeiro caso? Tudo é simples - um smallint de 2 bytes é alinhado em um limite de 4 bytes antes do próximo campo e, quando é o último, não há nada e não há necessidade de alinhar.

Em teoria, está tudo bem e você pode reorganizar os campos como quiser. Vamos verificar dados reais no exemplo de uma das tabelas, cuja seção diária leva de 10 a 15 GB.

Estrutura de origem:

CREATE TABLE public.plan_20190220
(
--  from table plan:  pack uuid NOT NULL,
--  from table plan:  recno smallint NOT NULL,
--  from table plan:  host uuid,
--  from table plan:  ts timestamp with time zone,
--  from table plan:  exectime numeric(32,3),
--  from table plan:  duration numeric(32,3),
--  from table plan:  bufint bigint,
--  from table plan:  bufmem bigint,
--  from table plan:  bufdsk bigint,
--  from table plan:  apn uuid,
--  from table plan:  ptr uuid,
--  from table plan:  dt date,
  CONSTRAINT plan_20190220_pkey PRIMARY KEY (pack, recno),
  CONSTRAINT chck_ptr CHECK (ptr IS NOT NULL),
  CONSTRAINT plan_20190220_dt_check CHECK (dt = '2019-02-20'::date)
)
INHERITS (public.plan)

A seção após alterar a ordem das colunas é exatamente o mesmo campo, apenas a ordem é diferente :

CREATE TABLE public.plan_20190221
(
--  from table plan:  dt date NOT NULL,
--  from table plan:  ts timestamp with time zone,
--  from table plan:  pack uuid NOT NULL,
--  from table plan:  recno smallint NOT NULL,
--  from table plan:  host uuid,
--  from table plan:  apn uuid,
--  from table plan:  ptr uuid,
--  from table plan:  bufint bigint,
--  from table plan:  bufmem bigint,
--  from table plan:  bufdsk bigint,
--  from table plan:  exectime numeric(32,3),
--  from table plan:  duration numeric(32,3),
  CONSTRAINT plan_20190221_pkey PRIMARY KEY (pack, recno),
  CONSTRAINT chck_ptr CHECK (ptr IS NOT NULL),
  CONSTRAINT plan_20190221_dt_check CHECK (dt = '2019-02-21'::date)
)
INHERITS (public.plan)

O volume total da seção é determinado pelo número de "fatos" e depende apenas de processos externos; portanto, dividimos o tamanho da pilha ( pg_relation_size) pelo número de registros nela - ou seja, obtemos o tamanho médio do registro armazenado real :


Menos 6% do volume , excelente!

Mas tudo, é claro, não é tão otimista - porque nos índices não podemos mudar a ordem dos campos e, portanto, "em geral" ( pg_total_relation_size) ...


... afinal, eles economizaram 1,5% aqui , sem alterar uma única linha de código. Sim Sim!



Noto que o arranjo acima dos campos não é o mais ideal. Como alguns blocos de campo não querem ser "separados" por razões estéticas - por exemplo, um par (pack, recno), que é PK para esta tabela.

Em geral, a definição do arranjo de campo "mínimo" é uma tarefa "exaustiva" bastante simples. Portanto, você pode obter resultados em seus dados ainda melhores que os nossos - experimente!

All Articles