Uso de particionamento no MySQL para Zabbix com um grande número de objetos de monitoramento

Para monitoramento de servidores e serviços, há muito tempo utilizamos com sucesso uma solução combinada baseada no Nagios e Munin. No entanto, essa combinação apresenta várias desvantagens, portanto, como muitos, estamos explorando ativamente Zabbix. Neste artigo, vamos explicar como, com um esforço mínimo, é possível resolver o problema de desempenho à medida que o número de métricas coletadas aumenta e os volumes do banco de dados MySQL crescem.

Problemas ao usar o banco de dados MySQL em conjunto com o Zabbix

Enquanto o banco de dados era pequeno e o número de métricas armazenadas nele era reduzido, tudo estava ótimo. O processo padrão housekeeper, que é iniciado pelo próprio Zabbix Server, excluía com sucesso os registros antigos do banco de dados, evitando que ele crescesse. No entanto, assim que o número de métricas coletadas aumentou e o volume do banco de dados atingiu um determinado tamanho, a situação começou a piorar. O housekeeper não conseguia mais excluir dados dentro do intervalo de tempo permitido, resultando na permanência de dados antigos no banco. Durante a execução do housekeeper, houve uma carga elevada no Zabbix Server, que poderia durar bastante tempo. Ficou claro que algo precisava ser feito para resolver a situação.

Esse é um problema conhecido; praticamente todos que trabalharam com grandes volumes de monitoramento no Zabbix enfrentaram o mesmo desafio. Havia várias soluções, como substituir o MySQL pelo PostgreSQL ou até mesmo Elasticsearch, mas a solução mais simples e testada foi a migração para o particionamento das tabelas que armazenam dados de métricas no banco de dados MySQL. Decidimos seguir exatamente por esse caminho.

Transição de tabelas MySQL convencionais para tabelas particionadas

O Zabbix é bem documentado e as tabelas onde ele armazena métricas são conhecidas. Estas tabelas são: histórico, onde são armazenados valores float, history_str, onde são armazenados valores curtos em string, history_text, onde são armazenados valores longos de texto e history_uint, onde são armazenados valores inteiros. Há também a tabela trends, que armazena a dinâmica das mudanças, mas optamos por não alterá-la, pois seu tamanho é pequeno e mais tarde voltaremos a ela.

Em geral, estávamos cientes de quais tabelas deveriam ser processadas. Decidimos criar partições para cada semana, com exceção da última, com base nos dias do mês, ou seja, quatro partições por mês: de 1 a 7, de 8 a 14, de 15 a 21 e de 22 a 1 (do mês seguinte). A dificuldade estava em transformar as tabelas necessárias em partições "em tempo real", sem interromper o funcionamento do Zabbix Server e a coleta de métricas.

Stranhamente, a própria estrutura dos dados das tabelas nos ajudou. Por exemplo, a tabela histórico possui a seguinte estrutura:

`itemid` bigint(20) unsigned NOT NULL,
`clock` int(11) NOT NULL DEFAULT '0',
`value` double(16,4) NOT NULL DEFAULT '0.0000',
`ns` int(11) NOT NULL DEFAULT '0',

neste caso,

KEY `history_1` (`itemid`,`clock`)

Como podemos ver, cada métrica é finalmente armazenada em uma tabela com dois campos muito importantes e convenientes para nós: itemid e clock. Assim, podemos facilmente criar uma tabela temporária, por exemplo, com o nome history_tmp, configurar a partição para ela e, em seguida, transferir todos os dados da tabela histórico, e depois renomear a tabela histórico em history_old, enquanto a tabela history_tmp em histórico, após o que podemos adicionar novamente os dados que não foram transferidos de history_old em histórico e excluir history_old. Podemos fazer isso de forma completamente segura, não perderemos nada, pois os campos mencionados acima itemid e clock garantem a ligação de uma métrica específica a um horário específico, e não a um número sequencial.

O próprio procedimento de transição

Atenção! É altamente recomendável, antes de iniciar qualquer ação, fazer uma cópia de segurança completa do banco de dados. Todos nós somos humanos e podemos cometer um erro ao digitar comandos, o que pode levar à perda de dados. Sim, a cópia de segurança pode não garantir a máxima atualidade, mas é melhor ter uma do que nenhuma.

Então, não desligamos nem interrompemos nada. O principal é garantir que no servidor MySQL haja espaço suficiente disponível em disco, ou seja, que haja espaço suficiente para cada uma das tabelas mencionadas acima histórico, history_text, history_str, history_uint, ao menos o suficiente para criar uma tabela com o sufixo "_tmp", considerando que ela terá o mesmo volume que a tabela original.

Não iremos descrever tudo várias vezes para cada uma das tabelas mencionadas acima e examinaremos tudo apenas pelo exemplo de uma delas — a tabela histórico.

Portanto, criamos uma tabela vazia history_tmp com base na estrutura da tabela histórico.

CREATE TABLE `history_tmp` LIKE `history`;

Criamos as partições necessárias. Para um exemplo, faremos isso por um mês. Cada partição é criada com base em uma regra de particionamento, baseada no valor do campo clock, que comparamos com o carimbo de data/hora:

ALTER TABLE `history_tmp` PARTITION BY RANGE( clock ) (
PARTITION p20190201 VALUES LESS THAN (UNIX_TIMESTAMP("2019-02-01 00:00:00")),
PARTITION p20190207 VALUES LESS THAN (UNIX_TIMESTAMP("2019-02-07 00:00:00")),
PARTITION p20190214 VALUES LESS THAN (UNIX_TIMESTAMP("2019-02-14 00:00:00")),
PARTITION p20190221 VALUES LESS THAN (UNIX_TIMESTAMP("2019-02-21 00:00:00")),
PARTITION p20190301 VALUES LESS THAN (UNIX_TIMESTAMP("2019-03-01 00:00:00"))
);

Este comando adiciona particionamento à tabela que criamos history_tmp. Esclarecendo, os dados cujo valor do campo clock é menor que "2019-02-01 00:00:00" irão para a partição p20190201, em seguida, os dados cujo valor do campo clock é maior que "2019-02-01 00:00:00" mas menor que "2019-02-07 00:00:00" irão para a partição p20190207 e assim por diante.

Nota importante: E o que acontece se tivermos dados na tabela particionada cujo valor do campo clock for maior ou igual a "2019-03-01 00:00:00"? Como não há partição adequada para esses dados, eles não serão inseridos na tabela e serão perdidos. Portanto, você deve não esquecer de criar partições adicionais a tempo, para evitar tais perdas de dados (como descrito abaixo).

Assim, a tabela temporária está pronta. Vamos carregar os dados. O processo pode levar um tempo considerável, mas, felizmente, ele não bloqueia quaisquer outras consultas, então só precisamos ter paciência:

INSERT IGNORE INTO `history_tmp` SELECT * FROM history;

A palavra-chave IGNORE durante o carregamento inicial não é obrigatória, já que a tabela está vazia, mas você precisará dela ao adicionar dados adicionais. Além disso, ela pode ser útil se você teve que interromper esse processo e recomeçar.

Assim, depois de algum tempo (talvez até várias horas), o primeiro carregamento de dados foi concluído. Como você sabe, agora a tabela history_tmp não contém todos os dados da tabela histórico, mas apenas aqueles que estavam nela no momento em que a consulta começou a ser executada. Aqui você realmente tem uma escolha: ou fazemos mais uma passagem (se o processo de carregamento demorou muito), ou vamos direto para a renomeação das tabelas, como mencionado acima. Vamos primeiro falar sobre a segunda passagem. Para começar, precisamos entender o horário da última gravação inserida em history_tmp:

SELECT max(clock) FROM history_tmp;

Suponha que você obteve: 1551045645. Agora usamos o valor obtido na segunda passagem de inserção de dados:

INSERT IGNORE INTO `history_tmp` SELECT * FROM history WHERE clock>=1551045645;

Essa passagem deve terminar significativamente mais rápido. Mas se a primeira passagem levar horas e a segunda também demorar, pode ser certo fazer uma terceira passagem, que será executada de maneira idêntica à segunda.

No final, novamente executamos a operação para obter o tempo da última inserção de registro em history_tmp, executando:

SELECT max(clock) FROM history_tmp;

Suponhamos que você obteve 1551085645. Salve este valor — nós precisaremos dele para a nova inserção.

Agora, de fato, quando a carga inicial de dados em history_tmp terminou, começamos a renomear as tabelas:

BEGIN;
RENAME TABLE history TO history_old;
RENAME TABLE history_tmp TO history;
COMMIT;

Agrupamos este bloco como uma única transação, para evitar o momento da inserção de dados em uma tabela inexistente, pois após o primeiro RENAME até o momento da execução do segundo RENAME, a tabela histórico não existirá. Mas mesmo que entre operações RENAME alguns dados sejam inseridos na tabela, e a tabela em si ainda não estiver presente (devido ao renomeio), teremos um pequeno número de erros de inserção que podemos ignorar (estamos monitorando, não é um banco). histórico Agora temos uma nova tabela

com particionamento, mas faltam dados que foram obtidos durante a última passagem de inserção de dados na tabela histórico . Mas esses dados estão na tabela history_tmpe vamos inseri-los agora. Para isso, precisamos do valor anteriormente salvo 1551085645. Por que salvamos esse valor e não usamos o tempo máximo de inserção já da tabela atual? history_old INSERT IGNORE INTO `history` SELECT * FROM history_old WHERE clock>=1551045645; histórico? Потому что новые данные уже в неё поступают и мы получим неверное время. Итак, дозаливаем данные:

Após a conclusão desta operação, teremos na nova tabela particionada

todos os dados que estavam na antiga, além daqueles que já chegaram após o renomeio da tabela. A tabela histórico não é mais necessária. Podemos deletá-la imediatamente ou, se você estiver paranoico, fazer um backup antes de removê-la. history_old Todo o processo descrito acima deve ser repetido para as tabelas

O que precisa ser ajustado nas configurações do Zabbix Server history_str, history_text e history_uint.

Что нужно поправить в настройках Zabbix Server

Agora, o gerenciamento do banco de dados na parte da história dos dados ficará sob nossa responsabilidade. Isso significa que o Zabbix não precisará mais excluir dados antigos — nós faremos isso. Para evitar que o Zabbix Server tente limpar os dados sozinho, você precisa acessar a interface web do Zabbix, selecionar no menu 'Administração', depois a subcategoria 'Geral', e em seguida, no menu suspenso à direita, escolher 'Limpeza da história'. Na página que aparecer, desmarque todas as caixas para o grupo 'História' e clique no botão 'Atualizar'. Isso evitará a limpeza indesejada de tabelas. história* via housekeeper.

Note nesta mesma página o grupo 'Dinâmica de Mudanças'. Esta é a tabela trends, à qual prometemos voltar. Se ela também se tornou muito grande e precisa de particionamento, desmarque as caixas neste grupo também, e então processe esta tabela exatamente como foi feito para as tabelas história*.

Manutenção contínua do banco de dados

Como mencionado anteriormente, para o funcionamento normal em tabelas particionadas, é necessário criar as partições a tempo. Isso pode ser feito da seguinte forma:

ALTER TABLE `history` ADD PARTITION (PARTITION p20190307 VALUES LESS THAN (UNIX_TIMESTAMP("2019-03-07 00:00:00")));

Além disso, como criamos tabelas particionadas e proibimos o Zabbix Server de limpá-las, agora a exclusão de dados antigos é nossa responsabilidade. Felizmente, isso não apresenta nenhum problema. É feito simplesmente excluindo a partição cujos dados não precisamos mais.

Por exemplo:

ALTER TABLE history DROP PARTITION p20190201;

Diferente dos comandos DELETE FROM com intervalo de datas, o DROP PARTITION é executado em poucos segundos, sem sobrecarregar servidor e funciona igualmente sem problemas em caso de uso de replicação MySQL.

Conclusão

A solução descrita é comprovada pelo tempo. O volume de dados está crescendo, mas não há uma desaceleração significativa no desempenho.

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster