MVCC no PostgreSQL-8. Congelamento

Começamos com problemas relacionados ao isolamento , fizemos uma digressão sobre a organização de dados em um nível baixo e conversamos detalhadamente sobre as versões de linha e como os instantâneos são obtidos a partir das versões.

Em seguida, examinamos os diferentes tipos de limpeza: intra-página (juntamente com atualizações HOT), regular e automática .

E chegou ao último tópico deste ciclo. Hoje falaremos sobre o problema da identificação da transação e congelamento.

Estouro do contador de transações


O PostgreSQL possui 32 bits alocados para o número da transação. Este é um número bastante grande (cerca de 4 bilhões), mas com a operação ativa do servidor, pode muito bem estar esgotado. Por exemplo, com uma carga de 1000 transações por segundo, isso acontecerá após apenas um mês e meio de operação contínua.

Mas falamos sobre o fato de que o mecanismo de multi-versão depende da sequência de numeração - depois de duas transações, uma transação com um número menor pode ser considerada como iniciada anteriormente. Portanto, é claro que você não pode simplesmente redefinir o contador e continuar a numeração novamente.



Por que 64 bits não são alocados para o número da transação - porque isso eliminaria completamente o problema? O fato é que (conforme discutido anteriormente ) no cabeçalho de cada versão da linha são armazenados dois números de transação - xmin e xmax. O cabeçalho já é bastante grande, com pelo menos 23 bytes, e um aumento na profundidade de bits levaria ao aumento em outros 8 bytes. Isto não é de forma alguma.

Os números de transação de 64 bits são implementados no produto de nossa empresa, o Postgres Pro Enterprise, mas também não são completamente honestos: xmin e xmax permanecem 32 bits, e o cabeçalho da página contém um "início de uma era" comum em toda a página.

O que fazer? Em vez de um diagrama linear, todos os números de transação são repetidos. Para qualquer transação, metade dos números "anti-horário" são considerados pertencentes ao passado e metade "anti-horário" para o futuro.

A idade de uma transação é o número de transações que passaram desde que apareceram no sistema (independentemente de o contador ter passado por zero ou não). Quando queremos entender se uma transação é mais antiga que outra ou não, comparamos sua idade, não números. (Portanto, a propósito, as operações “maior” e “menor” não são definidas para o tipo de dados xid.)



Mas nesse circuito em loop, surge uma situação desagradável. Uma transação que estava no passado distante (transação 1 na figura), depois de um tempo estará na metade do círculo que se relaciona com o futuro. Obviamente, isso viola as regras de visibilidade e levaria a problemas - as alterações feitas pela transação 1 simplesmente desapareceriam de vista.



Regras de congelamento e visibilidade da versão


Para evitar essas “viagens” do passado para o futuro, o processo de limpeza (além de liberar espaço nas páginas) executa outra tarefa. Ele encontra versões bastante antigas e "frias" das linhas (que são visíveis em todas as imagens e cuja mudança já é improvável) e de uma maneira especial as marca - as "congela". A versão congelada da linha é considerada mais antiga que qualquer dado regular e está sempre visível em todas as capturas instantâneas de dados. Além disso, não é mais necessário examinar o número da transação xmin, e esse número pode ser reutilizado com segurança. Portanto, versões congeladas de strings sempre permanecem no passado.



Para marcar o número da transação xmin como congelado, os dois bits de dica são definidos ao mesmo tempo - o bit de confirmação e o cancelamento.

Observe que a transação xmax não precisa ser congelada. Sua presença significa que esta versão da string não é mais relevante. Depois de deixar de ser visível nos instantâneos de dados, esta versão da linha será limpa.

Para experimentos, crie uma tabela. Definimos o fator de preenchimento mínimo para que apenas duas linhas caibam em cada página - para que seja mais conveniente observar o que está acontecendo. E desligue a automação para controlar o tempo de limpeza.

=> CREATE TABLE tfreeze( id integer, s char(300) ) WITH (fillfactor = 10, autovacuum_enabled = off); 

Já criamos várias variantes da função que, usando a extensão pageinspect, mostravam a versão das linhas que estão na página. Agora criaremos outra variante da mesma função: agora ele exibirá várias páginas ao mesmo tempo e mostrará a idade da transação xmin (a idade da função do sistema é usada para isso):

 => CREATE FUNCTION heap_page(relname text, pageno_from integer, pageno_to integer) RETURNS TABLE(ctid tid, state text, xmin text, xmin_age integer, xmax text, t_ctid tid) AS $$ SELECT (pageno,lp)::text::tid AS ctid, CASE lp_flags WHEN 0 THEN 'unused' WHEN 1 THEN 'normal' WHEN 2 THEN 'redirect to '||lp_off WHEN 3 THEN 'dead' END AS state, t_xmin || CASE WHEN (t_infomask & 256+512) = 256+512 THEN ' (f)' WHEN (t_infomask & 256) > 0 THEN ' (c)' WHEN (t_infomask & 512) > 0 THEN ' (a)' ELSE '' END AS xmin, age(t_xmin) xmin_age, t_xmax || CASE WHEN (t_infomask & 1024) > 0 THEN ' (c)' WHEN (t_infomask & 2048) > 0 THEN ' (a)' ELSE '' END AS xmax, t_ctid FROM generate_series(pageno_from, pageno_to) p(pageno), heap_page_items(get_raw_page(relname, pageno)) ORDER BY pageno, lp; $$ LANGUAGE SQL; 

Observe que o sinal de congelamento (que mostramos com a letra f entre colchetes) é determinado pela instalação simultânea de prompts confirmados e abortados. Muitas fontes (incluindo documentação) mencionam o número especial FrozenTransactionId = 2, que marca as transações congeladas. Esse sistema funcionou até a versão 9.4, mas agora foi substituído por bits de dica de ferramenta - isso permite salvar o número da transação original na versão de linha, o que é conveniente para fins de suporte e depuração. No entanto, transações com o número 2 ainda podem ocorrer em sistemas mais antigos, mesmo atualizados para as versões mais recentes.

Também precisamos da extensão pg_visibility, que permite examinar o mapa de visibilidade:

 => CREATE EXTENSION pg_visibility; 

Antes do PostgreSQL 9.6, o mapa de visibilidade continha um bit por página; marcou páginas contendo apenas versões "bastante antigas" de strings que já são garantidas como visíveis em todas as imagens. A idéia aqui é que, se a página estiver marcada no mapa de visibilidade, então para sua versão das linhas, você não precisará verificar as regras de visibilidade.

A partir da versão 9.6, um mapa de congelamento foi adicionado à mesma camada - mais um bit por página. O mapa de congelamento marca as páginas nas quais todas as versões das linhas estão congeladas.

Nós inserimos várias linhas na tabela e imediatamente executamos a limpeza para criar um mapa de visibilidade:

 => INSERT INTO tfreeze(id, s) SELECT g.id, 'FOO' FROM generate_series(1,100) g(id); => VACUUM tfreeze; 

E vemos que as duas páginas agora estão marcadas no mapa de visibilidade (all_visible), mas ainda não estão congeladas (all_frozen):

 => SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('tfreeze',g.blkno) ORDER BY g.blkno; 
  blkno | all_visible | all_frozen -------+-------------+------------ 0 | t | f 1 | t | f (2 rows) 

A idade da transação que criou as linhas (xmin_age) é 1 - esta é a última transação que foi executada no sistema:

 => SELECT * FROM heap_page('tfreeze',0,1); 
  ctid | state | xmin | xmin_age | xmax | t_ctid -------+--------+---------+----------+-------+-------- (0,1) | normal | 697 (c) | 1 | 0 (a) | (0,1) (0,2) | normal | 697 (c) | 1 | 0 (a) | (0,2) (1,1) | normal | 697 (c) | 1 | 0 (a) | (1,1) (1,2) | normal | 697 (c) | 1 | 0 (a) | (1,2) (4 rows) 

Idade mínima para congelamento


Três parâmetros principais controlam o congelamento, e os consideraremos por sua vez.

Vamos começar com vacuum_freeze_min_age , que define a idade mínima da transação xmin na qual a versão da string pode ser congelada. Quanto menor esse valor, maiores serão os custos indiretos desnecessários: se estivermos lidando com dados "quentes", ativamente alterados, o congelamento de mais e mais novas versões desaparecerá sem nenhum benefício. Nesse caso, é melhor esperar.

O valor padrão para esse parâmetro define que as transações começam a congelar após a passagem de 50 milhões de outras transações desde que apareceram:

 => SHOW vacuum_freeze_min_age; 
  vacuum_freeze_min_age ----------------------- 50000000 (1 row) 

Para ver como ocorre o congelamento, reduzimos o valor desse parâmetro para a unidade.

 => ALTER SYSTEM SET vacuum_freeze_min_age = 1; => SELECT pg_reload_conf(); 

E atualizaremos uma linha na página zero. A nova versão chegará à mesma página devido ao pequeno valor do fator de preenchimento.

 => UPDATE tfreeze SET s = 'BAR' WHERE id = 1; 

Aqui está o que vemos agora nas páginas de dados:

 => SELECT * FROM heap_page('tfreeze',0,1); 
  ctid | state | xmin | xmin_age | xmax | t_ctid -------+--------+---------+----------+-------+-------- (0,1) | normal | 697 (c) | 2 | 698 | (0,3) (0,2) | normal | 697 (c) | 2 | 0 (a) | (0,2) (0,3) | normal | 698 | 1 | 0 (a) | (0,3) (1,1) | normal | 697 (c) | 2 | 0 (a) | (1,1) (1,2) | normal | 697 (c) | 2 | 0 (a) | (1,2) (5 rows) 

Agora as linhas anteriores a vacuum_freeze_min_age = 1 devem ser congeladas. Mas observe que a linha zero não está marcada no mapa de visibilidade (o bit foi redefinido pelo comando UPDATE, que mudou a página), e a primeira permanece marcada:

 => SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('tfreeze',g.blkno) ORDER BY g.blkno; 
  blkno | all_visible | all_frozen -------+-------------+------------ 0 | f | f 1 | t | f (2 rows) 

Já dissemos que a limpeza varre apenas as páginas que não estão marcadas no mapa de visibilidade. E assim acontece:

 => VACUUM tfreeze; => SELECT * FROM heap_page('tfreeze',0,1); 
  ctid | state | xmin | xmin_age | xmax | t_ctid -------+---------------+---------+----------+-------+-------- (0,1) | redirect to 3 | | | | (0,2) | normal | 697 (f) | 2 | 0 (a) | (0,2) (0,3) | normal | 698 (c) | 1 | 0 (a) | (0,3) (1,1) | normal | 697 (c) | 2 | 0 (a) | (1,1) (1,2) | normal | 697 (c) | 2 | 0 (a) | (1,2) (5 rows) 

Na página zero, uma versão está congelada, mas a primeira página não considerou a limpeza. Portanto, se apenas as versões atuais forem deixadas na página, a limpeza não chegará a essa página e não as congelará.

 => SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('tfreeze',g.blkno) ORDER BY g.blkno; 
  blkno | all_visible | all_frozen -------+-------------+------------ 0 | t | f 1 | t | f (2 rows) 

Idade para congelar a tabela inteira


Para congelar ainda a versão das linhas deixadas nas páginas que a limpeza simplesmente não observa, um segundo parâmetro é fornecido: vacuum_freeze_table_age . Ele determina a idade da transação, na qual a limpeza ignora o mapa de visibilidade e passa por todas as páginas da tabela para congelar.

Cada tabela armazena um número de transação, para o qual se sabe que todas as transações antigas têm garantia de congelamento (pg_class.relfrozenxid). Com a idade dessa transação lembrada, o valor do parâmetro vacuum_freeze_table_age é comparado .

 => SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 'tfreeze'; 
  relfrozenxid | age --------------+----- 694 | 5 (1 row) 

Antes do PostgreSQL 9.6, a limpeza executava uma verificação completa da tabela para garantir que todas as páginas fossem rastreadas. Para mesas grandes, essa operação foi longa e triste. O assunto foi agravado pelo fato de que, se a limpeza não chegasse ao fim (por exemplo, um administrador impaciente interrompeu a execução de um comando), era necessário começar desde o início.

A partir da versão 9.6, graças ao mapa de congelamento (que vemos na coluna all_frozen na saída pg_visibility_map), limpar ignora apenas as páginas que ainda não estão marcadas no mapa. Isso não é apenas uma quantidade muito menor de trabalho, mas também resistência a interrupções: se o processo de limpeza for interrompido e reiniciado, ele não precisará mais olhar as páginas que já conseguiu marcar no mapa de congelamento da última vez.

De uma forma ou de outra, todas as páginas da tabela são congeladas uma vez nas transações ( vacuum_freeze_table_age - vacuum_freeze_min_age ). Com valores padrão, isso acontece uma vez por milhão de transações:

 => SHOW vacuum_freeze_table_age; 
  vacuum_freeze_table_age ------------------------- 150000000 (1 row) 

Portanto, fica claro que muito vácuo_freeze_min_age não deve ser definido, porque, em vez de reduzir a sobrecarga, isso começará a aumentá-los.

Vamos ver como a tabela inteira está congelada e, para fazer isso, reduza vacuum_freeze_table_age para 5 para que a condição de congelamento seja atendida.

 => ALTER SYSTEM SET vacuum_freeze_table_age = 5; => SELECT pg_reload_conf(); 

Vamos limpar:

 => VACUUM tfreeze; 

Agora, como é garantida a verificação de toda a tabela, o número da transação congelada pode ser aumentada - temos certeza de que as páginas não possuem uma transação descongelada mais antiga.

 => SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 'tfreeze'; 
  relfrozenxid | age --------------+----- 698 | 1 (1 row) 

Agora todas as versões das linhas na primeira página estão congeladas:

 => SELECT * FROM heap_page('tfreeze',0,1); 
  ctid | state | xmin | xmin_age | xmax | t_ctid -------+---------------+---------+----------+-------+-------- (0,1) | redirect to 3 | | | | (0,2) | normal | 697 (f) | 2 | 0 (a) | (0,2) (0,3) | normal | 698 (c) | 1 | 0 (a) | (0,3) (1,1) | normal | 697 (f) | 2 | 0 (a) | (1,1) (1,2) | normal | 697 (f) | 2 | 0 (a) | (1,2) (5 rows) 

Além disso, a primeira página é marcada no mapa de congelamento:

 => SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('tfreeze',g.blkno) ORDER BY g.blkno; 
  blkno | all_visible | all_frozen -------+-------------+------------ 0 | t | f 1 | t | t (2 rows) 

Idade para resposta "agressiva"


É importante que as versões das linhas congele a tempo. Se surgir uma situação em que uma transação que ainda não foi congelada corre o risco de entrar no futuro, o PostgreSQL falhará para evitar possíveis problemas.

Qual poderia ser a razão disso? Existem várias razões.

  • A limpeza automática pode ser desativada e a limpeza regular também não é iniciada. Já dissemos que isso não é necessário, mas tecnicamente é possível.
  • Mesmo a limpeza automática incluída não chega aos bancos de dados que não são usados ​​(lembre-se do parâmetro track_counts e do banco de dados template0).
  • Como vimos na última vez , a limpeza ignora tabelas nas quais os dados são adicionados apenas, mas não excluídos ou alterados.

Nesses casos, é fornecida uma operação de limpeza automática “agressiva” e regulada pelo parâmetro autovacuum_freeze_max_age . Se em qualquer tabela de qualquer banco de dados houver uma transação descongelada com idade superior à especificada no parâmetro, a limpeza automática será iniciada à força (mesmo se estiver desativada) e, mais cedo ou mais tarde, alcançará a tabela de problemas (independentemente dos critérios usuais).

O valor padrão é bastante conservador:

 => SHOW autovacuum_freeze_max_age; 
  autovacuum_freeze_max_age --------------------------- 200000000 (1 row) 

O limite para autovacuum_freeze_max_age é de 2 bilhões de transações e um valor 10 vezes menor é usado. Isso faz sentido: aumentando o valor, aumentamos o risco de que, pelo tempo restante, a limpeza automática simplesmente não tenha tempo para congelar todas as versões necessárias das linhas.

Além disso, o valor desse parâmetro determina o tamanho da estrutura XACT: como não deve haver transações mais antigas no sistema para as quais você pode precisar descobrir o status, a limpeza automática remove os arquivos desnecessários do segmento XACT, liberando espaço.

Vamos ver como a limpeza lida com tabelas somente anexadas, usando o tfreeze como exemplo. Para esta tabela, a limpeza automática geralmente está desativada, mas isso não será um obstáculo.

Alterar o parâmetro autovacuum_freeze_max_age requer uma reinicialização do servidor. Mas todos os parâmetros discutidos acima também podem ser configurados no nível de tabelas individuais usando parâmetros de armazenamento. Geralmente, só faz sentido fazer isso em casos especiais, quando a mesa realmente requer cuidados especiais.

Portanto, definiremos autovacuum_freeze_max_age no nível da tabela (e ao mesmo tempo retornará também o fator de preenchimento normal). Infelizmente, o valor mínimo possível é 100.000:

 => ALTER TABLE tfreeze SET (autovacuum_freeze_max_age = 100000, fillfactor = 100); 

Infelizmente, porque temos que concluir 100.000 transações para reproduzir a situação que nos interessa. Mas, é claro, para fins práticos, esse é um valor muito, muito baixo.

Como adicionaremos dados, inseriremos 100.000 linhas na tabela - cada uma em nossa transação. E, novamente, tenho que fazer uma reserva de que, na prática, isso não deve ser feito. Mas agora estamos apenas explorando, podemos.

 => CREATE PROCEDURE foo(id integer) AS $$ BEGIN INSERT INTO tfreeze VALUES (id, 'FOO'); COMMIT; END; $$ LANGUAGE plpgsql; => DO $$ BEGIN FOR i IN 101 .. 100100 LOOP CALL foo(i); END LOOP; END; $$; 

Como podemos ver, a idade da última transação congelada na tabela excedeu o valor limite:

 => SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 'tfreeze'; 
  relfrozenxid | age --------------+-------- 698 | 100006 (1 row) 

Mas se você esperar um pouco agora, no log de mensagens do servidor, haverá uma entrada sobre o vácuo agressivo automático da tabela "test.public.tfreeze", o número da transação congelada será alterada e sua idade retornará à decência:

 => SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 'tfreeze'; 
  relfrozenxid | age --------------+----- 100703 | 3 (1 row) 

Também existe o congelamento de várias transações, mas ainda não falamos sobre isso - adiaremos até falarmos sobre bloqueios para não ficar à frente de nós mesmos.

Congelamento manual


Às vezes, é conveniente controlar manualmente o congelamento em vez de esperar a chegada da limpeza automática.

Você pode congelar manualmente um comando usando o comando VACUUM FREEZE - todas as versões de linha serão congeladas, independentemente da idade das transações (como se o parâmetro autovacuum_freeze_min_age = 0). Quando uma tabela é reconstruída com os comandos VACUUM FULL ou CLUSTER, todas as linhas também são congeladas.

Para congelar todos os bancos de dados, você pode usar o utilitário:

 vacuumdb --all --freeze 

Os dados também podem ser congelados durante o carregamento inicial usando o comando COPY, especificando o parâmetro FREEZE. Para fazer isso, a tabela deve ser criada (ou esvaziada com o comando TRUNCATE) na mesma
transações como COPY.

Como existem regras de visibilidade separadas para linhas congeladas, essas linhas serão visíveis nas capturas instantâneas de dados de outras transações, violando as regras de isolamento usuais (isso se aplica a transações com o nível de Leitura Repetível ou Serializável).

Para verificar isso, em outra sessão, inicie uma transação com o nível de isolamento de Leitura Repetível:

 | => BEGIN ISOLATION LEVEL REPEATABLE READ; | => SELECT txid_current(); 

Observe que essa transação criou uma captura instantânea dos dados, mas não acessou a tabela tfreeze. Agora, esvaziaremos a tabela tfreeze e carregaremos novas linhas nela em uma transação. Se uma transação paralela ler o conteúdo do tfreeze, o comando TRUNCATE ficará bloqueado até o final da transação.

 => BEGIN; => TRUNCATE tfreeze; => COPY tfreeze FROM stdin WITH FREEZE; 
 1 FOO 2 BAR 3 BAZ \. 
 => COMMIT; 

Agora, uma transação paralela vê novos dados, embora isso quebre o isolamento:

 | => SELECT count(*) FROM tfreeze; 
 | count | ------- | 3 | (1 row) 
 | => COMMIT; 

Mas, como é improvável que esse carregamento de dados ocorra regularmente, isso geralmente não é um problema.

Significativamente pior, o COPY WITH FREEZE não funciona com o mapa de visibilidade - as páginas carregadas não são marcadas como contendo apenas versões das linhas visíveis a todos. Portanto, quando você acessa a tabela pela primeira vez, a limpeza é forçada a reprocessar tudo e criar um mapa de visibilidade. Para piorar a situação, as páginas de dados têm um sinal de visibilidade total em seu próprio cabeçalho; portanto, a limpeza não apenas lê a tabela inteira, mas também a reescreve completamente, colocando o bit desejado. Infelizmente, a solução para esse problema não precisa esperar antes da versão 13 ( discussão ).

Conclusão


Isso conclui minha série de artigos sobre isolamento e multiversão do PostgreSQL. Obrigado por sua atenção e principalmente pelos comentários - eles melhoram o material e frequentemente apontam áreas que exigem uma atenção mais cuidadosa da minha parte.

Fique conosco, para continuar!

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


All Articles