Melhorando o desempenho do Zabbix + PostgreSQL com particionamento e indexação

Há cerca de um ano, meus colegas e eu fomos encarregados de resolver o problema usando o popular sistema de monitoramento de infraestrutura de rede - Zabbix. Após estudar a documentação, imediatamente procedemos ao teste de carregamento: queríamos avaliar quantos parâmetros o Zabbix pode funcionar sem quedas perceptíveis de desempenho. Somente o PostgreSQL foi usado como DBMS.

Durante os testes, foram identificados alguns recursos arquiteturais do layout do banco de dados e o comportamento do próprio sistema de monitoramento, que, por padrão, não permitem que o sistema de monitoramento alcance sua potência máxima. Como resultado, algumas medidas de otimização foram desenvolvidas, conduzidas e testadas principalmente em termos de ajuste do banco de dados.

Quero compartilhar os resultados do trabalho realizado neste artigo. Este artigo será útil para os administradores de Zabbix e PostgreSQL DBA, bem como para todos que desejam entender e entender melhor o popular DBMS PosgreSQL.

Um pequeno spoiler: em uma máquina fraca com uma carga de 200 mil parâmetros por minuto, conseguimos reduzir o iowait da CPU de 20% para 2%, reduzir o tempo de gravação em partes nas tabelas de dados primárias em 250 vezes e nas tabelas de dados agregadas em 32 vezes, reduzir o tamanho dos índices 5 a 10 vezes e acelere o recebimento de amostras históricas em alguns casos até 18 vezes.

Teste de carga


O teste de carga foi realizado de acordo com o esquema: um servidor Zabbix, um proxy Zabbix ativo, dois agentes. Cada agente foi configurado para fornecer 50 toneladas de números inteiros e 50 toneladas de parâmetros de sequência por minuto (um total de 200 toneladas de parâmetros por minuto ou 3333 parâmetros por segundo). Para gerar parâmetros do agente, usamos um plug-in para o Zabbix.Para verificar quantos parâmetros um agente pode gerar, você precisa usar um script especial do mesmo autor do plug-in zabbix_module_stress . O administrador da web do Zabbix tem dificuldades em registrar modelos grandes, portanto, dividimos os parâmetros em 20 modelos com 5 toneladas de parâmetros (2500 numérico e 2500 string).

Modelo de gerador de script para teste de carga em python
import argparse """     .   20   5000    ( 2500  :  echo,  ;  ping,  ) """ TEMP_HEAD = """ <?xml version="1.0" encoding="UTF-8"?> <zabbix_export> <version>2.0</version> <date>2015-08-17T23:15:01Z</date> <groups> <group> <name>Templates</name> </group> </groups> <templates> <template> <template>Template Zabbix Srv Stress {count} passive {char}</template> <name>Template Zabbix Srv Stress {count} passive {char}</name> <description/> <groups> <group> <name>Templates</name> </group> </groups> <applications/> <items> """ TEMP_END = """</items> <discovery_rules/> <macros/> <templates/> <screens/> </template> </templates> </zabbix_export> """ TEMP_ITEM = """<item> <name>{k}</name> <type>0</type> <snmp_community/> <multiplier>0</multiplier> <snmp_oid/> <key>{k}</key> <delay>1m</delay> <history>3</history> <trends>365</trends> <status>0</status> <value_type>{t}</value_type> <allowed_hosts/> <units/> <delta>0</delta> <snmpv3_contextname/> <snmpv3_securityname/> <snmpv3_securitylevel>0</snmpv3_securitylevel> <snmpv3_authprotocol>0</snmpv3_authprotocol> <snmpv3_authpassphrase/> <snmpv3_privprotocol>0</snmpv3_privprotocol> <snmpv3_privpassphrase/> <formula>1</formula> <delay_flex/> <params/> <ipmi_sensor/> <data_type>0</data_type> <authtype>0</authtype> <username/> <password/> <publickey/> <privatekey/> <port/> <description/> <inventory_link>0</inventory_link> <applications/> <valuemap/> <logtimefmt/> </item> """ TMP_FNAME_DEFAULT = "Template_App_Zabbix_Server_Stress_{count}_passive_{char}.xml" chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" if __name__ == "__main__": parser = argparse.ArgumentParser( description='     zabbix') parser.add_argument('--items', dest='items', type=int, default=1000, help='-   (default: 1000)') parser.add_argument('--templates', dest='templates', type=int, default=1, help=f'-  [1-{len(chars)}] (default: 1)') args = parser.parse_args() items_count = args.items tmps_count = args.templates if not (tmps_count >= 1 and tmps_count <= len(chars)): sys.exit(f"Templates must be in range 1 - {len(chars)}") for i in range(tmps_count): fname = TMP_FNAME_DEFAULT.format(count=items_count, char=chars[i]) with open(fname, "w") as output: output.write(TEMP_HEAD.format(count=items_count, char=chars[i])) for k,t in [('stress.ping[{}-I-{:06d}]',3), ('stress.echo[{}-S-{:06d}]',4)]: for j in range(int(items_count/2)): output.write(TEMP_ITEM.format(k=k.format(chars[i],j),t=t)) output.write(TEMP_END) 


A métrica da CPU iostat é um bom indicador do desempenho do Zabbix - reflete a fração da unidade de tempo durante a qual o processador aguarda o acesso ao disco. Quanto mais alto, mais o disco é ocupado com operações de leitura e gravação, o que indiretamente afeta a degradação do desempenho do sistema de monitoramento como um todo. I.e. este é um sinal claro de que algo está errado com o monitoramento. A propósito, nos espaços abertos da rede, a pergunta bastante popular é “como remover o gatilho do iostat no Zabbix”, então esse é um ponto doloroso, porque existem muitas razões para aumentar o valor da métrica iowait.

Aqui está a imagem para a métrica da CPU iowait que recebemos três dias depois inicialmente:



Mas que quadro para a mesma métrica também obtivemos dentro de três dias no final, depois de todas as medidas de otimização que foram feitas, que serão discutidas abaixo:



Como pode ser visto nos gráficos, o indicador cpu iowait caiu de quase 20% para 2%, o que indiretamente acelerou o tempo de execução de todas as solicitações de adição e leitura de dados. Agora vamos ver por que, com as configurações padrão do banco de dados, o desempenho geral do sistema de monitoramento diminui e como corrigi-lo.

Razões para a queda no desempenho do Zabbix


Com o acúmulo de mais de 10 milhões de valores de parâmetros em cada tabela de dados primários, percebeu-se que o desempenho do sistema de monitoramento cai acentuadamente, devido aos seguintes motivos:

  • a métrica iowait para a CPU do servidor é aumentada em mais de 20%, o que indica um aumento no tempo durante o qual a CPU espera acessar as operações de leitura e gravação de disco
  • índices de tabelas nas quais os dados de monitoramento são bastante inflados
  • a métrica de utilização é aumentada para 100% para um disco com dados de monitoramento, o que indica a carga completa do disco com operações de leitura e gravação
  • valores obsoletos não têm tempo para serem excluídos das tabelas de histórico ao serem limpos de acordo com a programação da governanta

A situação é agravada no início de cada hora, quando, além disso, são calculadas estatísticas horárias agregadas - ao mesmo tempo, é realizada a leitura e gravação ativas das páginas de índice do disco, a exclusão de dados desatualizados do histórico, o que leva ao mesmo resultado - uma queda no desempenho do banco de dados e um aumento no tempo de execução pedidos (no limite, um pedido com duração de até 5 minutos foi anotado!).

Uma pequena ajuda na organização de um data warehouse de monitoramento no Zabbix. Além disso, armazena dados primários e dados agregados em diferentes tabelas, com a separação dos tipos de parâmetros. Cada tabela armazena um campo itemid (uma referência implícita a um item de dados registrado no sistema), um registro de data e hora para registrar o valor do relógio no formato de registro de data e hora unix (milissegundos em uma coluna separada) e um valor em uma coluna separada (a exceção é a tabela de log, possui mais campos - semelhante ao log de eventos ):
Nome da tabelaNomeaçãoTipo de dados
históriaDados Primários de Monitoramentonumérico (16,4)
history_uintDados Primários de Monitoramentonumérico (20,0)
history_strDados Primários de Monitoramentovarchar (255)
history_textDados Primários de Monitoramentotexto
history_logsDados Primários de Monitoramentocampos de texto e int
tendênciasDados de Monitoramento Agregadosnumérico (16,4)
trends_uintDados de Monitoramento Agregadosnumérico (20,0)

Atividades de otimização


Para melhorar o desempenho do banco de dados PostgreSQL, várias medidas de otimização foram realizadas, as principais das quais são particionamento e alteração de índices. No entanto, vale mencionar algumas palavras sobre algumas medidas importantes e úteis que podem acelerar o trabalho de qualquer banco de dados no sistema de gerenciamento de banco de dados PostgreSQL.

Nota importante. No momento da coleta do material do artigo, usamos o Zabbix versão 4.0, embora a versão 4.2 já tenha sido lançada e a versão 4.4 esteja sendo preparada para a liberação. Por que é importante mencionar isso? Como a partir da versão 4.2, o Zabbix começou a oferecer suporte a uma extensão poderosa especial para trabalhar com séries temporais do TimescaleDB, mas até agora no modo experimental: para todas as vantagens de usar essa extensão, acredita-se que algumas solicitações começaram a funcionar mais lentamente e ainda existem problemas de desempenho não resolvidos (haverá resolvido na versão 4.4) - leia este artigo . No próximo artigo, pretendo escrever sobre os resultados dos testes de carga já usando a extensão TimescaleDB em comparação com este caso de solução. A versão do PostgreSQL foi usada 10, mas todas as informações fornecidas são relevantes para as versões 11 e 12 (estamos aguardando!).

Portanto, as primeiras coisas primeiro:

  • configurando um arquivo de configuração usando o utilitário pgtune
  • colocando o banco de dados em um disco físico separado
  • Particionando Tabelas de Histórico com pg_pathman
  • alterando tipos de índice de tabelas de histórico para brin (clock) e btree-gin (itemid)
  • coleta e análise de estatísticas de execução de consultas pg_stat_statements
  • definindo parâmetros de monitoramento de disco físico
  • melhoria de desempenho de hardware
  • criação de um cluster distribuído (material fora do escopo deste artigo)


Configurando um arquivo de configuração usando o utilitário pgtune


De fato, o PostgreSQL é um DBMS bastante leve. Seu arquivo de configuração padrão é configurado para que, como diz meu colega, "trabalhe até na máquina de café", ou seja, em um ferro muito modesto. Portanto, é necessário configurar o PostgreSQL para a configuração do servidor, levando em consideração a quantidade de memória, o número de processadores, o tipo de uso pretendido do banco de dados, o tipo de disco (HDD ou SSD) e o número de conexões.

Infelizmente, não existe uma fórmula única para ajustar todos os DBMSs, mas existem certas regras e padrões que são adequados para a maioria das configurações (o ajuste mais fino já é o trabalho de um especialista). Para simplificar a vida do DBA, foi escrito o utilitário pgtune , que foi complementado pela versão web por le0pard , autor de um livro interessante e útil sobre administração do PostgreSQL.

Um exemplo de execução do utilitário no console com 100 conexões (o Zabbix possui um administrador da Web exigente) para o tipo de aplicativo "Data warehouses":

  pgtune -i postgresql.conf -o new_postgresql.conf -T DW -c 100 

Os parâmetros de configuração que o utilitário pgtune altera com uma descrição da finalidade (os valores são fornecidos como exemplo)
 # DB Version: 11
 # Tipo de SO: linux
 # Tipo de banco de dados: web
 # Memória total (RAM): 8 GB
 # CPUs num: 1
 # Conexões num: 100
 # Armazenamento de dados: hdd

 max_connections = 100 # número máximo de conexões simultâneas com o banco de dados
 shared_buffers = tamanho de memória de 2 GB # para vários buffers (principalmente cache de blocos de tabela e de índice) na memória compartilhada
 effective_cache_size = 6GB # tamanho máximo de memória necessária para execução da consulta usando índices
 maintenance_work_mem = 512MB # afeta a velocidade das operações VACUUM, ANALYZE, CREATE INDEX
 checkpoint_completion_target = 0.7 # hora prevista para concluir o procedimento do ponto de verificação
 wal_buffers = 16MB # quantidade de memória usada pela memória compartilhada para manter os logs de transações
 default_statistics_target = 100 # quantidade de estatísticas coletadas pelo comando ANALYZE - ao aumentar, o otimizador cria consultas mais lentamente, mas melhor
 random_page_cost = 4 # custo condicional do acesso do índice às páginas de dados - afeta a decisão de usar o índice
 effective_io_concurrency = 2 # número de operações de E / S assíncronas que o DBMS tentará executar em uma sessão separada
 work_mem = 10485kB # a quantidade de memória usada para classificação e tabelas de hash antes de usar arquivos temporários no disco
 min_wal_size = 1GB # limites abaixo do número de arquivos WAL que serão reciclados para uso futuro
 max_wal_size = 2GB # limita no topo o número de arquivos WAL que serão reciclados para uso futuro 

Algumas opções úteis de configuração do postgresql
 # gerenciamento de manipuladores de solicitação simultâneos
 max_worker_processes = 8 # o número máximo de processos em segundo plano - pelo menos um por banco de dados
 max_parallel_workers_per_gather = 4 # número máximo de processos paralelos em uma única solicitação
 max_parallel_workers = 8 # o número máximo de processos de trabalho que o sistema pode suportar para operações paralelas

 # logging settings (uma maneira fácil de descobrir o tempo de execução das solicitações sem usar a extensão pg_stat_statements)
 log_min_duration_statement = 3000 # grava nos logs a duração da execução de todos os comandos cujo tempo de operação> = do valor especificado em ms
 log_duration = off # registra a duração de cada comando concluído
 log_statement = 'none' # qual comando SQL deve ser gravado no log, valores: none (desativado), ddl, mod e all (todos os comandos)
 debug_print_plan = off # saída da árvore do plano de consulta para análise posterior

 # extrair o máximo do banco de dados e estar pronto para obtê-lo por qualquer falha (para os mais reprimidos, que ignoram a existência de ssd e um cluster distribuído)
 #fsync = off # gravação física no disco de alterações, desativar o fsync aumenta a velocidade, mas pode levar a falhas permanentes
 #synchronous_commit = off # permite que você responda ao cliente antes mesmo de as informações da transação estarem no WAL - uma alternativa quase segura para desativar o fsync
 #full_page_writes = off # shutdown acelera as operações normais, mas pode causar corrupção ou corrupção de dados se o sistema travar 

Listando um banco de dados em um disco físico separado


Esse item é opcional e, em vez disso, é uma solução de transição para um cluster distribuído completo, mas será útil conhecer essa possibilidade. Para acelerar o banco de dados, você pode colocá-lo em um disco separado. Montamos o disco inteiro no diretório base, onde todos os bancos de dados PostgreSQL são armazenados, mas em geral isso pode ser feito de maneira diferente: crie uma nova base de tabelas e transfira o banco de dados (ou apenas parte dela - as tabelas de dados de monitoramento primário e agregado) para essa base de tabelas em um disco separado.

Exemplo de montagem
Primeiro, você precisa formatar o disco com o sistema de arquivos ext4 e conectá-lo ao servidor. Monte o disco para o banco de dados com o rótulo noatime:

  mount / dev / sdc1 / var / lib / pgsql / 10 / data / base -o noatime 

Para montagem permanente, adicione a linha ao arquivo / etc / fstab:

 # onde UUID é o identificador do disco, você pode vê-lo usando o utilitário blkid
 UUID = 121efe29-70bf-410b-bc71-90704568ce3b / var / lib / pgsql / 10 / data / banco de dados ext4 padrões, noatime 0 0 


Particionando tabelas de histórico com pg_pathman


Um dos problemas que encontramos durante o teste de estresse do Zabbix - PostgreSQL não consegue excluir dados obsoletos do banco de dados. Usando o particionamento, é possível dividir a tabela em suas partes constituintes, reduzindo assim o tamanho dos índices e partes constituintes da supertabela, o que afeta positivamente a velocidade do banco de dados como um todo.

O particionamento resolve dois problemas ao mesmo tempo:

1. acelere a remoção de dados obsoletos excluindo tabelas inteiras

2. índices de divisão para cada tabela composta

Existem quatro mecanismos para particionar no PostgreSQL:

1. restrição_exclusão padrão

2. extensão pg_partman ( não confunda com pg_pathman )

3. extensão pg_pathman

4. criar e manter manualmente partições por nós mesmos

A solução de particionamento mais conveniente, confiável e otimizada, em nossa opinião, é a extensão pg_pathman . Com esse método de particionamento, o planejador de consultas determina de maneira flexível em quais partições procurar dados. Há rumores de que na 12ª versão do PostgreSQL haverá uma excelente partição já pronta para uso.

Assim, começamos a escrever dados de monitoramento para cada dia em uma tabela herdada separada da supertabela e a remoção de valores de parâmetros obsoletos começou a ocorrer através da remoção de todas as tabelas obsoletas de uma só vez, o que é muito mais fácil para um DBMS para custos de mão-de-obra. A exclusão foi feita chamando a função de usuário do banco de dados como um parâmetro de monitoramento do servidor Zabbix às 2 da manhã, com uma indicação do intervalo aceitável de armazenamento de estatísticas.

Instale e configure o particionamento para o PostgreSQL 10
Instale e configure a extensão pg_pathman a partir do repositório OS padrão (para obter instruções sobre como construir a versão mais recente da extensão a partir das fontes, procure no mesmo repositório no github):

 yum install pg_pathman10
 nano /var/pgsqldb/postgresql.conf
 shared_preload_libraries = 'pg_pathman' # important - escreva aqui pg_pathman por último na lista

Reinicializamos o DBMS, criamos a extensão para o banco de dados e configuramos o particionamento (1 dia para os dados primários de monitoramento e 3 dias para os dados de monitoramento agregados - isso pode ser feito por 1 dia):

 systemctl restart postgresql-10.service
 psql -d zabbix -U postgres
 CRIAR EXTENSÃO pg_pathman;
 # configure um dia para as tabelas de dados de monitoramento primário
 # 1552424400 - contagem regressiva como carimbo de data e hora unix, 86400 - segundos em dias
 selecione create_range_partitions ('history', 'clock', 1552424400, 86400);
 selecione create_range_partitions ('history_uint', 'clock', 1552424400, 86400);
 selecione create_range_partitions ('history_text', 'clock', 1552424400, 86400);
 selecione create_range_partitions ('history_str', 'clock', 1552424400, 86400);
 selecione create_range_partitions ('history_log', 'clock', 1552424400, 86400);
 # configure por três dias para tabelas de dados de monitoramento agregadas
 # 1552424400 - contagem regressiva como carimbo de data e hora unix, 259200 - segundos em três dias
 selecione create_range_partitions ('tendências', 'relógio', 1545771600, 259200);  
 selecione create_range_partitions ('trends_uint', 'clock', 1545771600, 259200); 

Se ainda não houver dados em nenhuma das tabelas, ao chamar a função create_range_partitions, mais um argumento adicional p_count = 0_ deve ser passado.

Consultas úteis para monitorar e gerenciar partições:

 # lista geral de tabelas particionadas, armazenamento de configuração principal:
 selecione * em pathman_config;
 # representação com todas as seções existentes, assim como seus pais e limites de intervalo:
 selecione * em pathman_partition_list;
 # parâmetros adicionais que substituem o comportamento padrão do pg_pathman:
 selecione * em pathman_config_params;
 # copie o conteúdo de volta para a tabela pai e exclua as partições:
 selecione drop_partitions ('table_name' :: regclass, false);

Script útil para visualizar estatísticas sobre o número e tamanho das partições:
 /*       */ SELECT nspname AS schemaname, relname, relkind, cast (reltuples as int), pg_size_pretty(pg_relation_size(C.oid)) AS "size" FROM pg_class C LEFT JOIN pg_namespace N ON (N.oid = C.relnamespace) WHERE nspname NOT IN ('pg_catalog', 'information_schema') and (relname like 'history%' or relname like 'trends%') and relkind = 'r' -- and reltuples > 0 -- and pg_relation_size(C.oid) >= 0 ORDER BY schemaname, relname 

Ajuste automático da exclusão de partições obsoletas (ahtung - uma grande função SQL)
Para configurar a exclusão automática de partições, você precisa criar uma função no banco de dados
(texto amplo, removi o realce da sintaxe):

 CRIAR OU SUBSTITUIR FUNÇÃO public.delete_old_partitions (history_days inteiro, trends_days inteiro, str_days inteiro)
  DEVOLUÇÃO texto
  LANGUAGE plpgsql
 Função $ AS $
 / *
 A função exclui todas as partições anteriores ao número de dias especificado:
 history_days - para partições history_x, history_uint_x
 trends_days - para partições trends_x, trends_uint_x
 str_days - para partições history_str_x, history_text_x, history_log_x
 * /
 declarar clock_today_start int;
 declarar clock_delete_less_history int = 0;
 declarar clock_delete_less_trends int = 0;
 declarar clock_delete_less_strings int = 0;
 clock_delete_less int = 0;
 declarar iterador int = 0;
 declarar result_str text = '';
 declare buf_table_size text;
 declarar buf_table_len text;
 declarar texto nome_da_partição;
 declare clock_max text;
 declarar texto err_detail;
 declarar t_start timestamp = clock_timestamp ();
 declarar t_end timestamp;
 começar
     se $ 1 <= 0 retornar 'ups, algo errado: o argumento history_days deve ser um valor inteiro positivo';  fim se;
     se $ 2 <= 0 retornar 'ups, algo errado: o argumento trends_days deve ser um valor inteiro positivo';  fim se;
     se $ 3 <= 0 retornar 'ups, algo errado: o argumento str_days deve ser um valor inteiro positivo';  fim se;
     clock_today_start = extract (época de date_trunc ('dia', agora ())) :: int;
     clock_delete_less_history = extract (época de date_trunc ('dia', agora ()) - ($ 1 :: texto || 'dias') :: intervalo) :: int;
     clock_delete_less_trends = extract (época de date_trunc ('dia', agora ()) - ($ 2 :: texto || 'dias') :: intervalo) :: int;
     clock_delete_less_strings = extract (época de date_trunc ('dia', agora ()) - ($ 3 :: texto || 'dias') :: intervalo) :: int;
     clock_delete_less = menos (clock_delete_less_history, clock_delete_less_trends, clock_delete_less_strings);
     - aviso de aumento 'clock_today_start% (%)', to_timestamp (clock_today_start), clock_today_start;
     - aviso de aumento 'clock_delete_less_history% (%)% days', to_timestamp (clock_delete_less_history), clock_delete_less_history, $ 1;
     - aviso de aumento 'clock_delete_less_trends% (%)% days', to_timestamp (clock_delete_less_trends), clock_delete_less_trends, $ 2;
     - aviso de aumento 'clock_delete_less_strings% (%)% days', to_timestamp (clock_delete_less_strings), clock_delete_less_strings, $ 3;
     para partition_name, clock_max na partição selecionada, range_max de pathman_partition_list em que 
     range_max :: int <= greatest (clock_delete_less_history, clock_delete_less_trends, clock_delete_less_strings) e 
     (partition :: texto como 'history%' ou partition :: texto como 'trends%') ordene pela partição asc
     laço
         if (nome da partição ~ 'history_uint_ \ d' e clock_max :: int <= clock_delete_less_history)
         ou (partition_name ~ 'history_ \ d' e clock_max :: int <= clock_delete_less_history)
         ou (partition_name ~ 'trends_ \ d' e clock_max :: int <= clock_delete_less_trends)
         ou (partition_name ~ 'history_log_ \ d' e clock_max :: int <= clock_delete_less_strings)
         ou (partition_name ~ 'history_str_ \ d' e clock_max :: int <= clock_delete_less_strings)
         ou (partition_name ~ 'history_text_ \ d' e clock_max :: int <= clock_delete_less_strings)
         então 
             iterador = iterador + 1;
             aumentar aviso '%', formato ('!!! excluir% s% s', nome da partição, clock_max);
             selecione max (reltuples :: int), pg_size_pretty (sum (pg_relation_size (pg_class.oid)))) como "tamanho" de pg_class, em que relname como partition_name ||  '%' em estrito buf_table_len, buf_table_size;
             if result_str! = '' então result_str = result_str ||  ',';  fim se;
             result_str = result_str ||  formato ('% s (dt <% s, len% s,% s)', nome_da_partição, to_char (to_timestamp (clock_max :: int), 'AAAA-MM-DD'), buf_table_len, buf_table_size);
             executar formato ('soltar tabela se existir% s', nome_da_partição);
         fim se;
     loop final;
     se iterador = 0 então result_str = format ('não há partições para excluir mais antigas, então% s data', to_char (to_timestamp (clock_delete_less), 'YYYY-MM-DD')); 
     else result_str = formato ('% s excluídas partições em% s segundos:', iterador, trunc (extract (segundos de (clock_timestamp () - t_start)) :: numeric, 3)) ||  result_str;
     fim se;
     - aviso de aumento '%', result_str;
     retornar result_str;
 exceção quando outros
    obter diagnósticos empilhados err_detail = PG_EXCEPTION_CONTEXT;
    formato de retorno ('ups, algo errado:% s [código de erro% s],% s', sqlerrm, sqlstate, err_detail);
 fim; 
 $ função $;

Para chamar automaticamente a função de partição de limpeza automática, você precisa criar um item de dados para o host do servidor zabbix do tipo "Database Monitor" com as seguintes configurações:

 - tipo: monitor de banco de dados
 - nome: delete_old_history_partitions
 - chave: db.odbc.select [delete_old_history_partitions, zabbix]
 - expressão sql: selecione delete_old_partitions (3, 30, 30);
 # aqui, os parâmetros da chamada da função delete_old_partitions indicam o tempo de armazenamento em dias 
 # para valores numéricos, valores numéricos agregados e valores de sequência
 - tipo de dados: texto
 - intervalo de atualização: 0
 - intervalo do usuário: agendado em h2
 - período de armazenamento histórico: 90 dias
 - grupo de elementos de dados: banco de dados

Como resultado, obteremos estatísticas sobre a limpeza de partições aproximadamente do seguinte tipo:

  2019-09-16 02:00:00, excluído 3 partições em 0,024 segundos: trends_78 (dt <2019-08-17, len 1, 48 kB), history_193 (dt <2019-09-13, len 85343, 9448 kB ), history_uint_186 (dt <2019-09-13, len 27969, 3480 kB)

Importante! Após configurar a exclusão automática de partições através do elemento de dados e da função do usuário, é necessário desativar o histórico e a limpeza de tendências no agendador de tarefas do Zabbix: através do item de menu do zabbix, selecione "Administração" -> "Geral" -> selecione "Limpar histórico" na lista no canto -> desativar todas as caixas de seleção nas seções "Histórico" e "Dinâmica das alterações".

Alterando tipos de índice de tabelas de histórico para brin (clock) e btree-gin (itemid)


Agradecimentos especiais a erogov pela excelente série de artigos sobre os índices do PostgreSQL . E de fato toda a equipe do PostgresPRO. .

, btree(itemid, clock) — , , «» , — 10 .

, , .

: brin clock btree-gin itemid .

brin , - , .. . btree-gin — gin , btree .. gin , . btree-gin PostgreSQL.

Zabbix . :
Nome da PartiçãoO número de linhas na MLNTamanho em MB
history_uint_181,34119
history_uint_274,94426
history_uint_3100,75387

Para avaliar os resultados, três tipos de consultas foram realizadas:

  1. para um parâmetro específico itemid data do último mês, de fato os últimos três dias (total de 1660 registros)

    explicar analisar selecionar * do history_uint em que itemid = 313300
    and clock> = extract (época de '2019-03-09 00:00:00' :: timestamp) :: int
    e clock <= extract (época de '2019-04-09 12:00:00' :: timestamp) :: int;
    
  2. para um dado de parâmetro específico por 12 horas de um dia (649 entradas no total)

    explicar analisar selecionar * do histórico_texto em que itemid = 310650
    and clock> = extract (época de '2019-04-09 00:00:00' :: timestamp) :: int
    e clock <= extract (época de '2019-04-09 12:00:00' :: timestamp) :: int;
    
  3. para um dado de parâmetro específico por uma hora (61 registros no total):

    explicar analisar contar contagem (*) de history_text em que itemid = 336540
    and clock> = extract (época de '2019-04-08 11:00:00' :: timestamp) :: int
    e clock <= extract (época de '2019-04-08 12:00:00' :: timestamp) :: int;
    

Os resultados dos testes foram tabulados abaixo:
tipo de índicetamanho em MB *solicitação 1 ** em mssolicitação 2 ** em mssolicitação 3 ** em ms
btree (relógio, itemid)147417154,32205,31860,4
brin (relógio),
btree-gin (itemid)
0,42 e 13292958,21820,4102,1
*
** 1 — 3 , 2 — 12 , 3 —

, 100 , btree brin btree-gin .

history_uint trends_uint ( 2000 ).
,,
trends_uint2201.488.72
trends_uint1997.2762.16

zabbix , zabbix , 10 . «» btree — ( utilization), CPU ( iowait).

, para que o índice btree-gin possa trabalhar com o tipo de dados bigint (in8), que é a coluna itemid, é necessário registrar uma família de operadores bigint para o índice btree-gin.

Registrando uma família de operadores bigint para o índice btree-gin
/*
     gin    biginteger  integer    .
 -   gin     int2, int4, int8,
       bigint     ,     bigint (<= 2147483647)
        intger_ops,  :
create index on tablename using gin(columnname int8_family_ops) with (fastupdate = false);
*/

--       btree_gin
CREATE EXTENSION btree_gin;

CREATE OPERATOR FAMILY integer_ops using gin;

CREATE OPERATOR CLASS int4_family_ops
FOR TYPE int4 USING gin FAMILY integer_ops
AS
    OPERATOR 1 <,
    OPERATOR 2 <=,
    OPERATOR 3 =,
    OPERATOR 4 >=,
    OPERATOR 5 >,
    FUNCTION 1 btint4cmp(int4,int4),
    FUNCTION 2 gin_extract_value_int4(int4, internal),
    FUNCTION 3 gin_extract_query_int4(int4, internal, int2, internal, internal),
    FUNCTION 4 gin_btree_consistent(internal, int2, anyelement, int4, internal, internal),
    FUNCTION 5 gin_compare_prefix_int4(int4,int4,int2, internal),
STORAGE int4;

CREATE OPERATOR CLASS int8_family_ops
FOR TYPE int8 USING gin FAMILY integer_ops
AS
    OPERATOR 1 <,
    OPERATOR 2 <=,
    OPERATOR 3 =,
    OPERATOR 4 >=,
    OPERATOR 5 >,
    FUNCTION 1 btint8cmp(int8,int8),
    FUNCTION 2 gin_extract_value_int8(int8, internal),
    FUNCTION 3 gin_extract_query_int8(int8, internal, int2, internal, internal),
    FUNCTION 4 gin_btree_consistent(internal, int2, anyelement, int4, internal, internal),
    FUNCTION 5 gin_compare_prefix_int8(int8,int8,int2, internal),
STORAGE int8;

ALTER OPERATOR FAMILY integer_ops USING gin add
  OPERATOR 1 <(int4,int8),
  OPERATOR 2 <=(int4,int8),
  OPERATOR 3 =(int4,int8),
  OPERATOR 4 >=(int4,int8),
  OPERATOR 5 >(int4,int8);

ALTER OPERATOR FAMILY integer_ops USING gin add
  OPERATOR 1 <(int8,int4),
  OPERATOR 2 <=(int8,int4),
  OPERATOR 3 =(int8,int4),
  OPERATOR 4 >=(int8,int4),
  OPERATOR 5 >(int8,int4);


Este script redistribui todos os índices no banco de dados PostgreSQL para Zabbix da configuração padrão para a configuração ideal descrita acima.
/*
        
*/

--   
drop index history_1;
drop index history_uint_1;
drop index history_str_1;
drop index history_text_1;
drop index history_log_1;
--          PK 
-- (   ,         )
alter table trends drop constraint trends_pk;
alter table trends_uint drop constraint trends_uint_pk;

--     bree-gin   itemid    
--   btree-gin  bigint       
-- https://habr.com/ru/company/postgrespro/blog/340978/#comment_10545932
--    create extension btree_gin;
create index on history using gin(itemid int8_family_ops) with (fastupdate = false);
create index on history_uint using gin(itemid int8_family_ops) with (fastupdate = false);
create index on history_str using gin(itemid int8_family_ops) with (fastupdate = false);
create index on history_text using gin(itemid int8_family_ops) with (fastupdate = false);
create index on history_log using gin(itemid int8_family_ops) with (fastupdate = false);
create index on trends using gin(itemid int8_family_ops) with (fastupdate = false);
create index on trends_uint using gin(itemid int8_family_ops) with (fastupdate = false);

--     bree-gin   itemid    
--     brin    128 ,    
--           ,
--      https://habr.com/ru/company/postgrespro/blog/346460/
create index on history using brin(clock) with (pages_per_range = 128);
create index on history_uint using brin(clock) with (pages_per_range = 128);
create index on history_str using brin(clock) with (pages_per_range = 128);
create index on history_text using brin(clock) with (pages_per_range = 128);
create index on history_log using brin(clock) with (pages_per_range = 128);
create index on trends using brin(clock) with (pages_per_range = 128);
create index on trends_uint using brin(clock) with (pages_per_range = 128);


brin 100 . (100 . history 100 . history_uint) , 512 , 128 , . brin , - , , .

, , Zabbix: « » . . , . , history btree(itemid, clock desc) , «» , , .

:

  1. « » 100 (.. , « » )
  2. Zabbix , , « »
  3. , , « » ( , web- Zabbix « » — , 5000 , web- ).

pg_stat_statements


Pg_stat_statements — . , PostgreSQL.

pg_stat_statements
psql:

 CREATE EXTENSION pg_stat_statements; 

postgresql.conf:

shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.max = 10000 #   sql ,     (     );
pg_stat_statements.track = all # all -   (    ), top -   /, none -  
pg_stat_statements.save = true #     

:

 SELECT pg_stat_statements_reset(); 

:
 select substring(query from '[^(]*') as query_sub, sum(calls) as calls, avg(mean_time) as mean_time from pg_stat_statements where query ~ 'insert into' or query ~ 'update trends' group by substring(query from '[^(]*') order by calls desc 


Zabbix vfs.dev.read vfs.dev.write. . utilization, await .

iowait cpu sql , zabbix . , lesovsky : iostat json , .

Pull request , fork .

Zabbix iowait cpu utiliztion ( ). (sda — , sdc — ):




Depois de configurar o DBMS, a indexação e o particionamento, você pode prosseguir para a escala vertical - para melhorar as características de hardware do servidor: adicione RAM, altere as unidades para estado sólido e adicione núcleos de processador. Esse é um aumento de desempenho garantido, mas é melhor fazer isso somente após a otimização do software.

Criando um cluster distribuído


— : , -. ( ) , Zabbix pg_pathman TimescaleDB.

, !

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


All Articles