O desempenho é uma das tarefas mais importantes e complexas ao gerenciar um banco de dados. Ele pode ser afetado pela configuração, pelo hardware ou mesmo pelo design do sistema. Por padrão, o PostgreSQL é configurado pensando em compatibilidade e estabilidade, já que o desempenho depende muito do hardware e do próprio sistema. Podemos ter um sistema com muitos dados sendo lidos, mas as informações não mudam com frequência. Ou podemos ter um sistema que escreve continuamente. Por esse motivo, é impossível definir uma configuração padrão que funcione para todos os tipos de cargas de trabalho.
Neste blog, veremos como analisar a carga de trabalho, ou consultas, que estão em execução. Em seguida, revisaremos alguns parâmetros básicos de configuração para melhorar o desempenho do nosso banco de dados PostgreSQL. Como mencionamos, veremos apenas alguns dos parâmetros. A lista de parâmetros do PostgreSQL é extensa, apenas tocamos em alguns dos principais. No entanto, pode-se sempre consultar a documentação oficial para se aprofundar nos parâmetros e configurações que parecem mais importantes ou úteis em nosso ambiente.
EXPLICAR
Um dos primeiros passos que podemos dar para entender como melhorar o desempenho do nosso banco de dados é analisar as consultas que são feitas.
O PostgreSQL cria um plano de consulta para cada consulta que recebe. Para ver este plano, usaremos EXPLAIN.
A estrutura de um plano de consulta é uma árvore de nós de plano. Os nós no nível inferior da árvore são nós de varredura. Eles retornam linhas brutas de uma tabela. Existem diferentes tipos de nós de varredura para diferentes métodos de acesso à tabela. A saída EXPLAIN tem uma linha para cada nó na árvore do plano.
world=# EXPLAIN SELECT * FROM city t1,country t2 WHERE id>100 AND t1.population>700000 AND t2.population<7000000;
QUERY PLAN
--------------------------------------------------------------------------
Nested Loop (cost=0.00..734.81 rows=50662 width=144)
-> Seq Scan on city t1 (cost=0.00..93.19 rows=347 width=31)
Filter: ((id > 100) AND (population > 700000))
-> Materialize (cost=0.00..8.72 rows=146 width=113)
-> Seq Scan on country t2 (cost=0.00..7.99 rows=146 width=113)
Filter: (population < 7000000)
(6 rows)
Este comando mostra como as tabelas em nossa consulta serão verificadas. Vamos ver a que correspondem esses valores que podemos observar em nosso EXPLAIN.
- O primeiro parâmetro mostra a operação que o mecanismo está realizando nos dados nesta etapa.
- Custo inicial estimado. Este é o tempo gasto antes que a fase de saída possa começar.
- Custo total estimado. Isso é declarado na suposição de que o nó do plano é executado até a conclusão. Na prática, o nó pai de um nó pode parar de ler todas as linhas disponíveis.
- Número estimado de linhas geradas por este nó do plano. Novamente, supõe-se que o nó seja executado até a conclusão.
- Largura média estimada das linhas geradas por este nó do plano.
A parte mais crítica da exibição é o custo estimado de execução da instrução, que é a estimativa do planejador de quanto tempo levará para executar a instrução. Ao comparar a eficácia de uma consulta com a outra, na prática estaremos comparando os valores de custo delas.
É importante entender que o custo de um nó de nível superior inclui o custo de todos os nós filhos. Também é importante perceber que o custo reflete apenas as coisas com as quais o planejador se preocupa. Em particular, o custo não considera o tempo gasto na transmissão das linhas de resultados ao cliente, o que pode ser um fator importante no tempo real decorrido; mas o planejador o ignora porque não pode mudá-lo alterando o plano.
Os custos são medidos em unidades arbitrárias determinadas pelos parâmetros de custo do planejador. A prática tradicional é medir os custos em unidades de buscas de páginas de disco; ou seja, seq_page_cost é convencionalmente definido como 1.0 e os outros parâmetros de custo são definidos em relação a isso.
EXPLICAR ANALISAR
Com essa opção, EXPLAIN executa a consulta e, em seguida, exibe as contagens de linhas verdadeiras e o tempo de execução verdadeiro acumulado em cada nó do plano, juntamente com as mesmas estimativas que um EXPLAIN simples mostra.
Vejamos um exemplo do uso desta ferramenta.
world=# EXPLAIN ANALYZE SELECT * FROM city t1,country t2 WHERE id>100 AND t1.population>700000 AND t2.population<7000000;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------
Nested Loop (cost=0.00..734.81 rows=50662 width=144) (actual time=0.081..22.066 rows=51100 loops=1)
-> Seq Scan on city t1 (cost=0.00..93.19 rows=347 width=31) (actual time=0.069..0.618 rows=350 loops=1)
Filter: ((id > 100) AND (population > 700000))
Rows Removed by Filter: 3729
-> Materialize (cost=0.00..8.72 rows=146 width=113) (actual time=0.000..0.011 rows=146 loops=350)
-> Seq Scan on country t2 (cost=0.00..7.99 rows=146 width=113) (actual time=0.007..0.058 rows=146 loops=1)
Filter: (population < 7000000)
Rows Removed by Filter: 93
Planning time: 0.136 ms
Execution time: 24.627 ms
(10 rows)
Se não encontrarmos o motivo pelo qual nossas consultas demoram mais do que deveriam, podemos verificar este blog para obter mais informações.
VÁCUO
O processo VACUUM é responsável por diversas tarefas de manutenção dentro do banco de dados, uma delas recuperando o armazenamento ocupado por tuplas mortas. Na operação normal do PostgreSQL, as tuplas excluídas ou obsoletas por uma atualização não são removidas fisicamente de sua tabela; eles permanecem presentes até que um VACUUM seja executado. Portanto, é necessário fazer o VACUUM periodicamente, principalmente em tabelas atualizadas com frequência.
Se o VACUUM estiver consumindo muito tempo ou recursos, significa que devemos fazê-lo com mais frequência, para que cada operação tenha menos a limpar.
Em qualquer caso, você pode precisar desabilitar o VACUUM, por exemplo, ao carregar dados em grandes quantidades.
O VACUUM simplesmente recupera espaço e o disponibiliza para reutilização. Esta forma de comando pode operar em paralelo com a leitura e escrita normal da tabela, pois não se obtém um bloqueio exclusivo. No entanto, o espaço adicional não é devolvido ao sistema operacional (na maioria dos casos); ele só está disponível para reutilização na mesma tabela.
VACUUM FULL regrava todo o conteúdo da tabela em um novo arquivo de disco sem espaço adicional, o que permite que o espaço não utilizado retorne ao sistema operacional. Este formulário é muito mais lento e requer um bloqueio exclusivo em cada tabela durante o processamento.
VACUUM ANALYZE executa um VACUUM e depois um ANALYZE para cada tabela selecionada. Esta é uma maneira prática de combinar scripts de manutenção de rotina.
ANALYZE coleta estatísticas sobre o conteúdo das tabelas no banco de dados e armazena os resultados em pg_statistic. Subsequentemente, o planejador de consultas usa essas estatísticas para ajudar a determinar os planos de execução mais eficientes para consultas.
Baixe o whitepaper hoje PostgreSQL Management &Automation with ClusterControlSaiba o que você precisa saber para implantar, monitorar, gerenciar e dimensionar o PostgreSQLBaixe o whitepaper
Parâmetros de configuração
Para modificar esses parâmetros devemos editar o arquivo $ PGDATA/postgresql.conf. Devemos ter em mente que alguns deles exigem uma reinicialização do nosso banco de dados.
max_connections
Determina o número máximo de conexões simultâneas ao nosso banco de dados. Existem recursos de memória que podem ser configurados por cliente, portanto, o número máximo de clientes pode sugerir a quantidade máxima de memória utilizada.
superuser_reserved_connections
No caso de atingir o limite de max_connection, essas conexões são reservadas para superusuário.
shared_buffers
Configura a quantidade de memória que o servidor de banco de dados usa para buffers de memória compartilhada. Se você tiver um servidor de banco de dados dedicado com 1 GB ou mais de RAM, um valor inicial razoável para shared_buffers é 25% da memória do seu sistema. Configurações maiores para shared_buffers geralmente requerem um aumento correspondente em max_wal_size, para estender o processo de gravação de grandes quantidades de dados novos ou modificados por um longo período de tempo.
temp_buffers
Define o número máximo de buffers temporários usados para cada sessão. Esses são buffers de sessão locais usados apenas para acessar tabelas temporárias. Uma sessão atribuirá os buffers temporários conforme necessário até o limite fornecido por temp_buffers.
trabalho_mem
Especifica a quantidade de memória que será usada pelas operações internas de ORDER BY, DISTINCT, JOIN e tabelas de hash antes de gravar nos arquivos temporários no disco. Ao configurar este valor devemos levar em consideração que várias sessões estarão executando estas operações ao mesmo tempo e cada operação poderá usar a quantidade de memória especificada por este valor antes de começar a escrever dados em arquivos temporários.
Esta opção era chamada sort_mem nas versões mais antigas do PostgreSQL.
maintenance_work_mem
Especifica a quantidade máxima de memória que as operações de manutenção usarão, como VACUUM, CREATE INDEX e ALTER TABLE ADD FOREIGN KEY. Como apenas uma dessas operações pode ser executada ao mesmo tempo por uma sessão, e uma instalação geralmente não possui muitas delas sendo executadas simultaneamente, ela pode ser maior que o work_mem. Configurações maiores podem melhorar o desempenho para VACUUM e restaurações de banco de dados.
Quando o autovacuum é executado, esta memória pode ser atribuída o número de vezes em que o parâmetro autovacuum_max_workers está configurado, portanto devemos levar isso em consideração, ou caso contrário, configure o parâmetro autovacuum_work_mem para gerenciá-lo separadamente.
fsync
Se o fsync estiver habilitado, o PostgreSQL tentará garantir que as atualizações sejam fisicamente gravadas no disco. Isso garante que o cluster de banco de dados possa ser recuperado para um estado consistente após uma falha do sistema operacional ou do hardware.
Embora a desativação do fsync geralmente melhore o desempenho, ela pode causar perda de dados em caso de falha de energia ou falha do sistema. Portanto, só é aconselhável desativar o fsync se você puder recriar facilmente todo o seu banco de dados a partir de dados externos.
checkpoint_segments (PostgreSQL <9.5)
Número máximo de segmentos de arquivo de registro entre pontos de controle WAL automáticos (cada segmento é normalmente 16 megabytes). Aumentar este parâmetro pode aumentar o tempo necessário para recuperar falhas. Em um sistema com muito tráfego, pode afetar o desempenho se for definido com um valor muito baixo. Recomenda-se aumentar o valor de checkpoint_segments em sistemas com muitas modificações de dados.
Além disso, uma boa prática é salvar os arquivos WAL em um disco diferente de PGDATA. Isso é útil tanto para equilibrar a escrita quanto para segurança em caso de falha de hardware.
A partir do PostgreSQL 9.5, a variável de configuração "checkpoint_segments" foi removida e substituída por "max_wal_size" e "min_wal_size"
max_wal_size (PostgreSQL>=9.5)
Tamanho máximo que o WAL pode crescer entre os pontos de controle. O tamanho do WAL pode exceder max_wal_size em circunstâncias especiais. Aumentar este parâmetro pode aumentar o tempo necessário para recuperar falhas.
min_wal_size (PostgreSQL>=9.5)
Quando o arquivo WAL é mantido abaixo desse valor, ele é reciclado para uso futuro em um ponto de verificação, em vez de ser excluído. Isso pode ser usado para garantir que espaço WAL suficiente seja reservado para lidar com picos no uso de WAL, por exemplo, ao executar grandes trabalhos em lote.
wal_sync_method
Método usado para forçar as atualizações do WAL no disco. Se o fsync estiver desabilitado, esta configuração não terá efeito.
wal_buffers
A quantidade de memória compartilhada usada para dados WAL que ainda não foram gravados em disco. A configuração padrão é cerca de 3% de shared_buffers, não menos que 64 KB ou mais que o tamanho de um segmento WAL (geralmente 16 MB). Definir esse valor para pelo menos alguns MB pode melhorar o desempenho de gravação em um servidor com muitas transações simultâneas.
effective_cache_size
Esse valor é usado pelo planejador de consultas para levar em conta os planos que podem ou não caber na memória. Isso é levado em consideração nas estimativas de custo do uso de um índice; um valor alto torna mais provável que as varreduras de índice sejam usadas e um valor baixo torna mais provável que as varreduras sequenciais sejam usadas. Um valor razoável seria 50% da RAM.
default_statistics_target
O PostgreSQL coleta estatísticas de cada uma das tabelas em seu banco de dados para decidir como as consultas serão executadas nelas. Por padrão, ele não coleta muitas informações, e caso não esteja obtendo bons planos de execução, deve-se aumentar esse valor e depois executar ANALYZE no banco de dados novamente (ou aguardar o AUTOVACUUM).
synchronous_commit
Especifica se a confirmação da transação aguardará que os registros WAL sejam gravados no disco antes que o comando retorne uma indicação de "sucesso" ao cliente. Os valores possíveis são:"on", "remote_apply", "remote_write", "local" e "off". A configuração padrão está ativada". Quando está desabilitado, pode haver um atraso entre o momento em que o cliente retorna e quando a transação é garantida contra um bloqueio do servidor. Ao contrário do fsync, desabilitar este parâmetro não cria nenhum risco de inconsistência do banco de dados:uma falha no sistema operacional ou banco de dados pode resultar na perda de algumas transações recentes supostamente confirmadas, mas o estado do banco de dados será exatamente o mesmo que essas transações foi cancelado de forma limpa. Portanto, desativar o synchronous_commit pode ser uma alternativa útil quando o desempenho é mais importante do que a certeza exata sobre a durabilidade de uma transação.
Registro
Existem vários tipos de dados para registrar que podem ser úteis ou não. Vamos ver alguns deles:
- log_min_error_statement:define o nível mínimo de registro.
- log_min_duration_statement:usado para registrar consultas lentas no sistema.
- log_line_prefix:adere as informações no início de cada linha de log.
- log_statement:Você pode escolher entre NONE, DDL, MOD, ALL. Usar "todos" pode causar problemas de desempenho.
Projeto
Em muitos casos, o design do nosso banco de dados pode afetar o desempenho. Devemos ter cuidado em nosso design, normalizando nosso esquema e evitando dados redundantes. Em muitos casos, é conveniente ter várias mesas pequenas em vez de uma mesa enorme. Mas como dissemos antes, tudo depende do nosso sistema e não há uma única solução possível.
Também devemos usar os índices com responsabilidade. Não devemos criar índices para cada campo ou combinação de campos, pois, embora não precisemos percorrer toda a tabela, estamos usando espaço em disco e adicionando sobrecarga nas operações de gravação.
Outra ferramenta muito útil é o gerenciamento do pool de conexões. Se tivermos um sistema com muita carga, podemos usar isso para evitar saturar as conexões no banco de dados e poder reutilizá-las.
Hardware
Como mencionamos no início deste blog, o hardware é um dos fatores importantes que afetam diretamente o desempenho do nosso banco de dados. Vejamos alguns pontos a ter em conta.
- Memória:quanto mais RAM temos, mais dados de memória podemos manipular, e isso significa melhor desempenho. A velocidade de escrita e leitura no disco é muito mais lenta do que na memória, portanto, quanto mais informações pudermos ter na memória, melhor desempenho teremos.
- CPU:Talvez não faça muito sentido dizer isso, mas quanto mais CPU tivermos, melhor. De qualquer forma, não é o mais importante em termos de hardware, mas se pudermos ter uma boa CPU, nossa capacidade de processamento melhorará e isso impactará diretamente em nosso banco de dados.
- Disco rígido:Temos vários tipos de discos que podemos usar, SCSI, SATA, SAS, IDE. Também temos discos de estado sólido. Devemos comparar qualidade / preço, que devemos usar para comparar sua velocidade. Mas o tipo de disco não é a única coisa a considerar, também devemos ver como configurá-los. Se quisermos um bom desempenho, podemos usar o RAID10, mantendo os WALs em outro disco fora do RAID. Não é recomendado usar RAID5, pois o desempenho desse tipo de RAID para bancos de dados não é bom.
Conclusão
Após levar em consideração os pontos citados neste blog, podemos realizar um benchmark para verificar o comportamento do banco de dados.
Também é importante ter nosso banco de dados monitorado para determinar se estamos enfrentando um problema de desempenho e poder resolvê-lo o mais rápido possível. Para esta tarefa existem várias ferramentas como Nagios, ClusterControl ou Zabbix, entre outras, que nos permitem não só monitorar, mas com algumas delas, nos permite tomar ações proativas antes que o problema ocorra. Com o ClusterControl, além de monitoramento, administração e diversos outros utilitários, podemos receber recomendações sobre quais ações podemos tomar ao receber alertas de desempenho. Isso nos permite ter uma ideia de como resolver problemas potenciais.
Este blog não pretende ser um guia exaustivo sobre como melhorar o desempenho do banco de dados. Espero que dê uma imagem mais clara do que as coisas podem se tornar importantes e alguns dos parâmetros básicos que podem ser configurados. Não hesite em nos informar se perdemos algum importante.