A série anterior foi dedicada ao
isolamento e à multi-versão do PostgreSQL, e hoje estamos iniciando uma nova -
sobre o mecanismo de registro write-ahead. Deixe-me lembrá-lo de que o material é baseado em
cursos de treinamento administrativo que Pavel
pluzanov e eu
fazemos , mas não os repete literalmente e se destinam a uma leitura atenta e a experimentações independentes.
Este ciclo consistirá em quatro partes:
- Cache de buffer (este artigo);
- Diário de pré-registro - como é organizado e como é usado durante a recuperação;
- Ponto de verificação e gravação em segundo plano - por que eles são necessários e como são configurados;
- Ajuste de log - níveis e tarefas a serem resolvidos, confiabilidade e desempenho.
Por que o diário é necessário?
No processo, parte dos dados com os quais o DBMS lida é armazenada na RAM e gravada no disco (ou outra mídia não volátil) de maneira diferida. Quanto menos vezes isso acontece, menos entrada e saída e mais rápido o sistema funciona.
Mas o que acontecerá no caso de uma falha, por exemplo, quando a energia for desligada ou se ocorrer um erro no código DBMS ou no sistema operacional? Todo o conteúdo da RAM será perdido e apenas os dados gravados no disco permanecerão (com alguns tipos de falhas, o disco também poderá sofrer, mas nesse caso, apenas uma cópia de backup ajudará). Em princípio, a E / S pode ser organizada de forma que os dados no disco sejam sempre mantidos em um estado consistente, mas isso é difícil e não é muito eficiente (tanto quanto eu sei, apenas o Firebird foi por esse caminho).
Geralmente, incluindo o PostgreSQL, os dados gravados no disco são inconsistentes e, ao se recuperar de uma falha, são necessárias ações especiais para restaurar a consistência. O registro no diário é o próprio mecanismo que torna isso possível.
Cache de buffer
Estranhamente, começaremos a falar sobre registro no diário com um cache de buffer. O cache do buffer não é a única estrutura armazenada na RAM, mas uma das mais importantes e complexas. Compreender o princípio de sua operação é importante por si só; além disso, neste exemplo, vamos nos familiarizar com a forma como os dados são trocados entre a RAM e o disco.
O cache é usado em todos os lugares nos sistemas de computação modernos; um processador sozinho pode contar três ou quatro níveis de cache. Em geral, qualquer cache é necessário para suavizar a diferença de desempenho entre os dois tipos de memória, um dos quais é relativamente rápido, mas não é suficiente para todos, e o outro é relativamente lento, mas abundante. Portanto, o cache do buffer suaviza a diferença entre o tempo de acesso à RAM (nanossegundos) e ao disco (milissegundos).
Observe que o sistema operacional também possui um cache de disco que resolve o mesmo problema. Portanto, o DBMS geralmente tenta evitar o armazenamento em cache duplo acessando o disco diretamente, ignorando o cache do SO. Mas no caso do PostgreSQL, não é assim: todos os dados são lidos e gravados usando operações comuns de arquivos.
Além disso, as matrizes de disco e até os próprios discos também têm seu próprio cache. Esse fato ainda é útil para nós quando chegamos à questão da confiabilidade.
Mas voltando ao cache do buffer do DBMS.
É chamado assim porque é uma matriz de
buffers . Cada buffer é um local para uma página de dados (bloco), mais um cabeçalho. O título, entre outras coisas, contém:
- localização no disco da página no buffer (número do arquivo e bloco);
- um sinal de que os dados na página foram alterados e, mais cedo ou mais tarde, devem ser gravados no disco (esse buffer é chamado de sujo );
- número de chamadas para o buffer (contagem de uso);
- bandeira de fixar o buffer (contagem de pinos).
O cache do buffer está localizado na memória compartilhada do servidor e é acessível a todos os processos. Para trabalhar com dados - leitura ou modificação, - processa páginas de leitura no cache. Enquanto a página está em cache, trabalhamos com ela na RAM e economizamos nos acessos ao disco.

Inicialmente, o cache contém buffers vazios e todos eles estão vinculados à lista de buffers livres. O significado do ponteiro para a "próxima vítima" ficará claro um pouco mais tarde. Para encontrar rapidamente a página desejada no cache, uma tabela de hash é usada.
Página de pesquisa no cache
Quando um processo precisa ler uma página, ele primeiro tenta encontrá-la no cache do buffer usando uma tabela de hash. A chave de hash é o número do arquivo e o número da página dentro do arquivo. Na cesta correspondente da tabela de hash, o processo localiza o número do buffer e verifica se ele realmente contém a página desejada. Como em qualquer tabela de hash, colisões são possíveis aqui; nesse caso, o processo precisará verificar várias páginas.
O uso de uma tabela de hash é criticado há muito tempo. Essa estrutura permite encontrar rapidamente o buffer na página, mas é completamente inútil se, por exemplo, você precisar encontrar todos os buffers ocupados por uma tabela específica. Mas ninguém propôs um bom substituto ainda.
Se a página desejada for encontrada no cache, o processo deve "congelar" o buffer aumentando a contagem de pinos (vários processos podem fazer isso simultaneamente). Enquanto o buffer estiver fixo (o valor do contador for maior que zero), considera-se que o buffer é usado e seu conteúdo não deve ser alterado "radicalmente". Por exemplo, uma nova versão da linha pode aparecer na página - isso não incomoda ninguém devido às regras de versão múltipla e visibilidade. Mas outra página não pode ser lida no buffer fixado.
Crowding out
Pode acontecer que a página necessária não seja encontrada no cache. Nesse caso, ele deve ser lido do disco para algum buffer.
Se ainda houver buffers livres no cache, o primeiro será selecionado. Porém, mais cedo ou mais tarde eles terminam (geralmente o tamanho do banco de dados é maior que a memória alocada para o cache) e, em seguida, você precisa escolher um dos buffers ocupados, forçar a página lá e ler um novo no espaço vago.
O mecanismo de preempção é baseado no fato de que cada vez que o buffer é acessado, os processos aumentam a contagem de uso no cabeçalho do buffer. Portanto, os buffers usados com menos frequência do que outros têm um valor de contador mais baixo e são bons candidatos à exclusão.
O algoritmo de varredura de relógio percorre todos os buffers (usando o ponteiro para a "próxima vítima"), diminuindo a contagem de acessos em um. Para exclusão, o primeiro buffer é selecionado, o que:
- tem um contador de acertos zero (contagem de uso),
- e não fixo (contagem zero de pinos).
Você pode ver que, se todos os buffers tiverem um contador de zero diferente de zero, o algoritmo precisará fazer mais de um círculo, redefinindo os contadores, até que um deles finalmente chegue a zero. Para evitar "círculos tortuosos", o valor máximo do contador de visitas é limitado a 5. Mas ainda assim, com um tamanho de cache de buffer grande, esse algoritmo pode causar uma sobrecarga significativa.
Depois que o buffer é encontrado, acontece o seguinte.
O buffer é fixado para mostrar outros processos que estão sendo usados. Além da correção, outros meios de bloqueio também são usados, mas falaremos mais sobre isso separadamente.
Se o buffer estiver sujo, ou seja, ele contém dados alterados, a página não pode ser simplesmente descartada - primeiro ela precisa ser salva no disco. Essa não é uma situação boa, pois o processo que está prestes a ler a página precisa aguardar a gravação de dados "estrangeiros", mas esse efeito é amenizado pelos processos de verificação e registro em segundo plano, que serão discutidos mais adiante.
Em seguida, uma nova página é lida do disco no buffer selecionado. O contador do número de chamadas é definido como um. Além disso, o link para a página carregada deve ser registrado na tabela de hash para que possa ser encontrado no futuro.
Agora, o link para a “próxima vítima” aponta para o próximo buffer, e o recém carregado tem tempo para aumentar o contador de ocorrências até que o ponteiro passe por todo o cache do buffer e retorne novamente.
Com meus próprios olhos
Como é habitual no PostgreSQL, há uma extensão que permite procurar dentro do cache do buffer.
=> CREATE EXTENSION pg_buffercache;
Crie uma tabela e insira uma linha nela.
=> CREATE TABLE cacheme( id integer ) WITH (autovacuum_enabled = off); => INSERT INTO cacheme VALUES (1);
O que haverá no cache do buffer? No mínimo, uma página deve aparecer nela com uma única linha adicionada. Verificaremos isso com a seguinte consulta, na qual selecionamos apenas os buffers pertencentes à nossa tabela (pelo número do arquivo relfilenode) e decodificamos o número da camada (relforknumber):
=> SELECT bufferid, CASE relforknumber WHEN 0 THEN 'main' WHEN 1 THEN 'fsm' WHEN 2 THEN 'vm' END relfork, relblocknumber, isdirty, usagecount, pinning_backends FROM pg_buffercache WHERE relfilenode = pg_relation_filenode('cacheme'::regclass);
bufferid | relfork | relblocknumber | isdirty | usagecount | pinning_backends ----------+---------+----------------+---------+------------+------------------ 15735 | main | 0 | t | 1 | 0 (1 row)
Então é isso - existe uma página no buffer. Está sujo (isdirty), o contador de ocorrências é um (usagecount) e não é corrigido por nenhum processo (pinning_backends).
Agora adicione outra linha e repita a consulta. Para salvar cartas, inserimos uma linha em outra sessão e repetimos a solicitação longa com o comando
\g
.
| => INSERT INTO cacheme VALUES (2);
=> \g
bufferid | relfork | relblocknumber | isdirty | usagecount | pinning_backends ----------+---------+----------------+---------+------------+------------------ 15735 | main | 0 | t | 2 | 0 (1 row)
Nenhum novo buffer foi adicionado - a segunda linha coube na mesma página. Observe que o contador de uso aumentou.
| => SELECT * FROM cacheme;
| id | ---- | 1 | 2 | (2 rows)
=> \g
bufferid | relfork | relblocknumber | isdirty | usagecount | pinning_backends ----------+---------+----------------+---------+------------+------------------ 15735 | main | 0 | t | 3 | 0 (1 row)
E depois de acessar a página para leitura, o contador também aumenta.
E se você limpar?
| => VACUUM cacheme;
=> \g
bufferid | relfork | relblocknumber | isdirty | usagecount | pinning_backends ----------+---------+----------------+---------+------------+------------------ 15731 | fsm | 1 | t | 1 | 0 15732 | fsm | 0 | t | 1 | 0 15733 | fsm | 2 | t | 2 | 0 15734 | vm | 0 | t | 2 | 0 15735 | main | 0 | t | 3 | 0 (5 rows)
A limpeza criou um mapa de visibilidade (uma página) e um mapa de espaço livre (três páginas - o tamanho mínimo deste mapa).
Bem e assim por diante.
Configuração de tamanho
O tamanho do cache é definido pelo parâmetro
shared_buffers . O valor padrão é ridículo 128 MB. Este é um dos parâmetros que faz sentido aumentar imediatamente após a instalação do PostgreSQL.
=> SELECT setting, unit FROM pg_settings WHERE name = 'shared_buffers';
setting | unit ---------+------ 16384 | 8kB (1 row)
Lembre-se de que alterar um parâmetro requer uma reinicialização do servidor, pois toda a memória cache necessária é alocada na inicialização do servidor.
Por que razões para escolher o valor apropriado?
Até o maior banco de dados possui um conjunto limitado de dados "quentes", com os quais o trabalho ativo é realizado a cada momento. Idealmente, esse conjunto deve ser colocado no cache do buffer (mais algum espaço para dados "únicos"). Se o tamanho do cache for menor, as páginas usadas ativamente se espremerão constantemente, criando excesso de entrada e saída. Mas aumentar o cache sem pensar também está errado. Com um tamanho grande, os custos indiretos de manutenção aumentam e, além disso, a RAM também é necessária para outras necessidades.
Assim, o tamanho ideal do cache do buffer será diferente em diferentes sistemas: depende dos dados, do aplicativo e da carga. Infelizmente, não existe um significado mágico que sirva a todos igualmente bem.
A recomendação padrão é usar 1/4 da RAM como primeira aproximação (para Windows anterior ao PostgreSQL 10, era recomendável escolher um tamanho menor).
E então você precisa olhar para a situação. É melhor fazer um experimento: aumentar ou diminuir o tamanho do cache e comparar o desempenho do sistema. Obviamente, para isso, é necessário ter uma bancada de testes e poder reproduzir a carga típica - no ambiente de produção, tais experimentos parecem prazeres duvidosos.
Não deixe de consultar o relatório de Nikolay Samokhvalov no PgConf-2019: "Uma abordagem industrial para o ajuste do PostgreSQL: experimentos de banco de dados "
Mas algumas informações sobre o que está acontecendo podem ser coletadas diretamente em um sistema ativo usando a mesma extensão pg_buffercache - o mais importante, observe o ângulo certo.
Por exemplo, você pode estudar a distribuição de buffers de acordo com o grau de uso:
=> SELECT usagecount, count(*) FROM pg_buffercache GROUP BY usagecount ORDER BY usagecount;
usagecount | count ------------+------- 1 | 221 2 | 869 3 | 29 4 | 12 5 | 564 | 14689 (6 rows)
Nesse caso, muitos valores de contador vazios são buffers livres. Não é surpresa para um sistema em que nada acontece.
Você pode ver quanto das tabelas em nosso banco de dados são armazenadas em cache e com que intensidade esses dados são usados (por uso ativo nesta consulta, queremos dizer buffers com um contador de uso superior a 3):
=> SELECT c.relname, count(*) blocks, round( 100.0 * 8192 * count(*) / pg_table_size(c.oid) ) "% of rel", round( 100.0 * 8192 * count(*) FILTER (WHERE b.usagecount > 3) / pg_table_size(c.oid) ) "% hot" FROM pg_buffercache b JOIN pg_class c ON pg_relation_filenode(c.oid) = b.relfilenode WHERE b.reldatabase IN ( 0, (SELECT oid FROM pg_database WHERE datname = current_database()) ) AND b.usagecount is not null GROUP BY c.relname, c.oid ORDER BY 2 DESC LIMIT 10;
relname | blocks | % of rel | % hot ---------------------------+--------+----------+------- vac | 833 | 100 | 0 pg_proc | 71 | 85 | 37 pg_depend | 57 | 98 | 19 pg_attribute | 55 | 100 | 64 vac_s | 32 | 4 | 0 pg_statistic | 27 | 71 | 63 autovac | 22 | 100 | 95 pg_depend_reference_index | 19 | 48 | 35 pg_rewrite | 17 | 23 | 8 pg_class | 16 | 100 | 100 (10 rows)
Aqui, por exemplo, pode-se ver que a tabela vac ocupa o maior lugar (nós a usamos em um dos tópicos anteriores), mas ninguém a abordou por um longo tempo e ainda não foi espremida simplesmente porque os buffers gratuitos ainda não se esgotaram.
Você pode criar outras seções que fornecerão informações úteis para reflexão. Só é necessário considerar que esses pedidos:
- deve ser repetido várias vezes: os números variam dentro de certos limites;
- não é necessário executá-lo constantemente (como parte do monitoramento) devido ao fato de a extensão bloquear a operação com o cache do buffer por um curto período de tempo.
E mais uma coisa. Não devemos esquecer que o PostgreSQL trabalha com arquivos por meio de chamadas regulares ao sistema operacional e, portanto, há um cache duplo: as páginas se enquadram no cache do buffer do DBMS e no cache do SO. Portanto, a “falta” no cache do buffer nem sempre leva à necessidade de E / S real. Mas a estratégia de excluir o sistema operacional é diferente da estratégia do DBMS: o sistema operacional não sabe nada sobre o significado dos dados lidos.
Deslocamento de massa
Nas operações que executam leitura ou gravação em massa de dados, existe o risco de deslocar rapidamente páginas úteis do cache do buffer com dados "únicos".
Para impedir que isso aconteça, os chamados
anéis de buffer são usados para essas operações - uma pequena parte do cache do buffer é alocada para cada operação. A extrusão atua apenas dentro do anel, para que o restante dos dados do cache do buffer não sofra.
Para leitura seqüencial de tabelas grandes (cujo tamanho excede um quarto do cache do buffer), 32 páginas são alocadas. Se outro processo também precisar desses dados durante a leitura de uma tabela, ele não começará a ler a tabela primeiro, mas se conectará a um anel de buffer existente. Após a digitalização, ele lê o início "perdido" da tabela.
Vamos conferir. Para fazer isso, crie uma tabela para que uma linha ocupe uma página inteira - é mais conveniente contar. O tamanho padrão do cache do buffer é 128 MB = 16384 páginas de 8 KB. Portanto, você precisa inserir mais de 4096 linhas de página na tabela.
=> CREATE TABLE big( id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, s char(1000) ) WITH (fillfactor=10); => INSERT INTO big(s) SELECT 'FOO' FROM generate_series(1,4096+1);
Vamos analisar a tabela.
=> ANALYZE big; => SELECT relpages FROM pg_class WHERE oid = 'big'::regclass;
relpages ---------- 4097 (1 row)
Agora temos que reiniciar o servidor para limpar o cache dos dados da tabela que a análise leu.
student$ sudo pg_ctlcluster 11 main restart
Após a reinicialização, leia a tabela inteira:
=> EXPLAIN (ANALYZE, COSTS OFF) SELECT count(*) FROM big;
QUERY PLAN --------------------------------------------------------------------- Aggregate (actual time=14.472..14.473 rows=1 loops=1) -> Seq Scan on big (actual time=0.031..13.022 rows=4097 loops=1) Planning Time: 0.528 ms Execution Time: 14.590 ms (4 rows)
E verifique se apenas 32 buffers estão ocupados por páginas tabulares no cache do buffer:
=> SELECT count(*) FROM pg_buffercache WHERE relfilenode = pg_relation_filenode('big'::regclass);
count ------- 32 (1 row)
Se a varredura seqüencial for proibida, a tabela será lida por índice:
=> SET enable_seqscan = off; => EXPLAIN (ANALYZE, COSTS OFF) SELECT count(*) FROM big;
QUERY PLAN ------------------------------------------------------------------------------------------- Aggregate (actual time=50.300..50.301 rows=1 loops=1) -> Index Only Scan using big_pkey on big (actual time=0.098..48.547 rows=4097 loops=1) Heap Fetches: 4097 Planning Time: 0.067 ms Execution Time: 50.340 ms (5 rows)
Nesse caso, o anel do buffer não é usado e a tabela inteira aparece no cache do buffer (e quase todo o índice também):
=> SELECT count(*) FROM pg_buffercache WHERE relfilenode = pg_relation_filenode('big'::regclass);
count ------- 4097 (1 row)
De maneira semelhante, anéis de buffer são usados para o processo de limpeza (também 32 páginas) e para operações de gravação em massa COPY IN e CREATE TABLE AS SELECT (geralmente 2048 páginas, mas não mais que 1/8 do cache total de buffer).
Tabelas temporárias
Uma exceção à regra geral são as tabelas temporárias. Como os dados temporários são visíveis para apenas um processo, eles não têm nada a fazer no cache do buffer compartilhado. Além disso, os dados temporários existem apenas em uma única sessão, portanto, não precisam ser protegidos contra falhas.
Para dados temporários, um cache é usado na memória local do processo que possui a tabela. Como esses dados estão disponíveis para apenas um processo, eles não precisam ser protegidos com bloqueios. O cache local usa o algoritmo preemptivo usual.
Diferentemente do cache geral do buffer, a memória do cache local é alocada conforme necessário, porque tabelas temporárias não são usadas em todas as sessões. A quantidade máxima de memória para tabelas temporárias em uma sessão é limitada pelo parâmetro
temp_buffers .
Aquecendo o cache
Após reiniciar o servidor, algum tempo deve passar antes que o cache “aqueça” - acumule dados reais usados ativamente. Às vezes, pode ser útil ler imediatamente os dados de determinadas tabelas no cache, e uma extensão especial foi criada para isso:
=> CREATE EXTENSION pg_prewarm;
Anteriormente, uma extensão podia ler apenas determinadas tabelas no cache do buffer (ou apenas no cache do SO). Mas no PostgreSQL 11, ele foi capaz de salvar o status atual do cache em disco e restaurá-lo após a reinicialização do servidor. Para tirar proveito disso, você precisa adicionar a biblioteca a
shared_preload_libraries e reiniciar o servidor.
=> ALTER SYSTEM SET shared_preload_libraries = 'pg_prewarm';
student$ sudo pg_ctlcluster 11 main restart
O campo reiniciar, se o parâmetro
pg_prewarm.autoprewarm não for alterado, o processo em segundo plano mestre do autoprewarm será iniciado automaticamente, que uma vez em
pg_prewarm.autoprewarm_interval liberará a lista de páginas em cache no disco (não se esqueça de levar em consideração o novo processo ao definir
max_parallel_processes ).
=> SELECT name, setting, unit FROM pg_settings WHERE name LIKE 'pg_prewarm%';
name | setting | unit ---------------------------------+---------+------ pg_prewarm.autoprewarm | on | pg_prewarm.autoprewarm_interval | 300 | s (2 rows)
postgres$ ps -o pid,command --ppid `head -n 1 /var/lib/postgresql/11/main/postmaster.pid` | grep prewarm
10436 postgres: 11/main: autoprewarm master
Agora não há tabela grande no cache:
=> SELECT count(*) FROM pg_buffercache WHERE relfilenode = pg_relation_filenode('big'::regclass);
count ------- 0 (1 row)
Se assumirmos que todo o seu conteúdo é muito importante, podemos lê-lo no cache do buffer chamando a seguinte função:
=> SELECT pg_prewarm('big');
pg_prewarm ------------ 4097 (1 row)
=> SELECT count(*) FROM pg_buffercache WHERE relfilenode = pg_relation_filenode('big'::regclass);
count ------- 4097 (1 row)
A lista de páginas é despejada no arquivo autoprewarm.blocks. Para vê-lo, basta aguardar até que o processo mestre do acionamento automático seja executado pela primeira vez, mas iniciamos manualmente:
=> SELECT autoprewarm_dump_now();
autoprewarm_dump_now ---------------------- 4340 (1 row)
O número de páginas descartadas é superior a 4097 - isso inclui as páginas dos objetos de catálogo do sistema já lidos pelo servidor. E aqui está o arquivo:
postgres$ ls -l /var/lib/postgresql/11/main/autoprewarm.blocks
-rw------- 1 postgres postgres 102078 29 15:51 /var/lib/postgresql/11/main/autoprewarm.blocks
Agora reinicie o servidor novamente.
student$ sudo pg_ctlcluster 11 main restart
E imediatamente após o lançamento, nossa tabela aparece novamente no cache.
=> SELECT count(*) FROM pg_buffercache WHERE relfilenode = pg_relation_filenode('big'::regclass);
count ------- 4097 (1 row)
Isso fornece o mesmo processo mestre do disparador automático: ele lê o arquivo, divide as páginas em bancos de dados, classifica-as (para que a leitura do disco seja o mais consistente possível) e passa o trabalhador do disparador automático para o fluxo de trabalho individual para processamento.
Para ser continuado .