Para monitorar servidores e serviços, usamos por muito tempo, e ainda com êxito, uma solução combinada baseada em Nagios e Munin. No entanto, esse grupo tem várias desvantagens, portanto, como muitos, estamos explorando ativamente o
Zabbix . Neste artigo, falaremos sobre como você pode resolver o problema de desempenho com o mínimo de esforço ao aumentar o número de métricas retiradas e aumentar o volume do banco de dados MySQL
Problemas ao usar um banco de dados MySQL com Zabbix
Embora o banco de dados fosse pequeno e o número de métricas armazenadas fosse pequeno, tudo foi maravilhoso. O processo regular de governanta que inicia o próprio Zabbix Server excluiu com êxito registros obsoletos do banco de dados, impedindo que ele crescesse. No entanto, assim que o número de métricas capturadas aumentou e o tamanho do banco de dados atingiu um determinado tamanho, tudo ficou pior. O Houserkeeper parou de gerenciar a exclusão de dados pelo intervalo de tempo alocado; os dados antigos começaram a permanecer no banco de dados. Durante a operação da governanta, houve um aumento de carga no servidor Zabbix, que pode durar muito tempo. Tornou-se claro que era necessário resolver de alguma forma a situação atual.
Esse é um problema conhecido, quase todo mundo que trabalhou com grandes volumes de monitoramento no Zabbix enfrentou a mesma coisa. Também havia várias soluções: por exemplo, substituindo o MySQL pelo PostgreSQL ou até Elasticsearch, mas a solução mais simples e comprovada foi mudar para tabelas de particionamento que armazenam dados métricos no banco de dados MySQL. Decidimos seguir por esse caminho.
Migrando de Tabelas MySQL Regulares para Tabelas Particionadas
O Zabbix está bem documentado e as tabelas onde ele armazena métricas são conhecidas. Estas são tabelas:
history
, onde os valores flutuantes são armazenados,
history_str
, onde os valores de cadeia curta são armazenados,
history_text
, onde os valores de texto longo são armazenados e
history_uint
, onde os valores inteiros são armazenados. Há também uma tabela de
trends
que armazena a dinâmica das mudanças, mas decidimos não tocá-la, porque seu tamanho é pequeno e, um pouco mais tarde, retornaremos a ela.
Em geral, quais tabelas precisavam ser processadas eram claras. Decidimos fazer partições para cada semana, com exceção da última, com base nos números 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 (mês seguinte). A dificuldade era que precisávamos transformar as tabelas necessárias em particionadas “on the fly”, sem interromper o Zabbix Server e coletar métricas.
Curiosamente, a própria estrutura dessas tabelas veio em nosso auxílio nisso. Por exemplo, a tabela de
history
tem 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',
enquanto
KEY `history_1` (`itemid`,`clock`)
Como você pode ver, cada métrica é finalmente inserida em uma tabela com dois campos muito importantes e convenientes para nós,
itemid e
clock . Assim, podemos criar uma tabela temporária, por exemplo, com o nome
history_tmp
, configurar uma
history_tmp
para ela e transferir todos os dados da tabela de
history
lá, renomear a tabela de
history
para
history_old
e a tabela
history_tmp
para
history
, e adicionar os dados que nós
history_old
de
history_old
para
history
e
history_old
. Você pode fazer isso de forma totalmente segura, não perderemos nada, porque os campos
itemid e
clock indicados acima fornecem uma métrica de link para um horário específico, e não para algum tipo de número de série.
O próprio processo de transição
Atenção! É muito desejável, antes de iniciar qualquer ação, fazer um backup completo do banco de dados. Somos todos pessoas vivas e podemos cometer um erro no conjunto de comandos, o que pode levar à perda de dados. Sim uma cópia de backup não fornecerá a máxima relevância, mas é melhor ter uma do que nenhuma.
Portanto, não desligue nada ou pare. O principal é que, no próprio servidor MySQL, deve haver uma quantidade suficiente de espaço livre em disco, ou seja, para que, para cada uma das tabelas acima,
history
,
history_text
,
history_text
,
history_str
,
history_uint
pelo menos espaço suficiente para criar uma tabela com o sufixo "_tmp", já que será a mesma quantidade da tabela original.
Não descreveremos tudo várias vezes para cada uma das tabelas acima e consideraremos tudo com o exemplo de apenas uma delas - a tabela de
history
.
Portanto, crie uma tabela
history_tmp
vazia
history_tmp
base na estrutura da tabela de
history
.
CREATE TABLE `history_tmp` LIKE `history`;
Criamos as partições que precisamos. Por exemplo, vamos fazer isso por um mês. Cada partição é criada com base na regra de partição, com base no valor do campo de
relógio , que comparamos com o registro de data e 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 operador adiciona particionamento à tabela
history_tmp
que criamos. Esclareçamos que os dados para os quais o valor do campo do
relógio é menor que "2019-02-01 00:00:00" cairão na partição
p20190201 ; em seguida, os dados para os quais o valor do campo do
relógio for maior que "2019-02-01 00:00:00", mas menos "2019-02-07 00:00:00" cairá na festa
p20190207 e assim por diante.
Nota importante: E o que acontece se tivermos dados na tabela particionada em que o valor do campo do relógio é maior ou igual a "2019-03-01 00:00:00"? Como não há partição adequada para esses dados, eles não cairão na tabela e serão perdidos. Portanto, não se esqueça de criar partições adicionais em tempo hábil, para evitar essa perda de dados (sobre a qual abaixo).
Então, a tabela temporária está preparada. Preencha os dados. O processo pode levar um longo tempo, mas, felizmente, não bloqueia outras solicitações; portanto, você só precisa ser paciente:
INSERT IGNORE INTO `history_tmp` SELECT * FROM history;
A palavra-chave IGNORE não é necessária durante o preenchimento inicial, porque ainda não há dados na tabela; no entanto, você precisará disso ao adicionar dados. Além disso, pode ser útil se você tiver que interromper esse processo e iniciar novamente ao preencher os dados.
Então, depois de algum tempo (talvez até algumas horas), o primeiro upload de dados passou. Como você entende, agora a tabela
history_tmp
não contém todos os dados da tabela de
history
, mas apenas os dados que estavam nela no momento em que a consulta foi iniciada. Aqui, na verdade, você tem uma escolha: ou fazemos mais uma passagem (se o processo de preenchimento durar muito tempo) ou imediatamente passamos a renomear as tabelas mencionadas acima. Primeiro, vamos dar o segundo passe. Primeiro, precisamos entender a hora do último registro inserido no
history_tmp
:
SELECT max(clock) FROM history_tmp;
Suponha que você recebeu:
1551045645 . Agora usamos o valor obtido na segunda passagem do preenchimento de dados:
INSERT IGNORE INTO `history_tmp` SELECT * FROM history WHERE clock>=1551045645;
Essa passagem deve terminar muito mais rápido. Mas se a primeira passagem foi realizada horas e a segunda também foi realizada por um longo período de tempo, pode ser correto fazer a terceira passagem, que é realizada completamente semelhante à segunda.
No final, executamos novamente a operação de obter o horário da última inserção do registro no
history_tmp
fazendo:
SELECT max(clock) FROM history_tmp;
Digamos que você tenha
1551085645 . Mantenha esse valor - será necessário preenchê-lo novamente.
E agora, na verdade, quando os dados primários preenchidos em
history_tmp
, passamos a renomear as tabelas:
BEGIN; RENAME TABLE history TO history_old; RENAME TABLE history_tmp TO history; COMMIT;
Nós projetamos esse bloco como uma transação para evitar o momento de inserir dados em uma tabela inexistente, porque após o primeiro RENAME até o segundo RENAME ser executado, a tabela de
history
não existe. Mas mesmo que alguns dados cheguem entre as operações RENAME na tabela de
history
e a própria tabela ainda não exista (devido à renomeação), obteremos um pequeno número de erros de inserção que podem ser negligenciados (temos monitoramento, não o banco).
Agora, temos uma nova tabela de
history
com particionamento, mas ele não possui dados suficientes que foram recebidos durante a última passagem da inserção de dados na tabela
history_tmp
. Mas temos esses dados na tabela
history_old
e agora os compartilhamos a partir daí. Para isso, precisaremos do valor salvo anteriormente 1551085645. Por que salvamos esse valor e não usamos o tempo máximo de preenchimento já existente na tabela de
history
atual? Como novos dados já estão chegando e chegaremos na hora errada. Então, medimos os dados:
INSERT IGNORE INTO `history` SELECT * FROM history_old WHERE clock>=1551045645;
Após o final desta operação, temos na nova tabela de
history
particionada todos os dados que estavam na antiga, mais os dados que vieram após a renomeação da tabela. A tabela
history_old
não é mais necessária. Você pode excluí-lo imediatamente ou fazer uma cópia de backup (se tiver paranóia) antes de excluí-lo.
Todo o processo descrito acima precisa ser repetido para as
history_str
,
history_text
e
history_uint
.
O que precisa ser corrigido nas configurações do Servidor Zabbix
Agora, a manutenção do banco de dados referente ao histórico de dados está sobre nossos ombros. Isso significa que o Zabbix não deve mais excluir dados antigos - nós mesmos o faremos. Para que o Zabbix Server não tente limpar os dados, é necessário acessar a interface da web do Zabbix, selecionar "Administração" no menu, depois o submenu "Geral" e selecionar "Limpar histórico" na lista suspensa à direita. Na página exibida, desmarque todas as caixas de seleção do grupo "Histórico" e clique no botão "Atualizar". Isso impedirá que as tabelas do
history*
sejam limpas por nós através da governanta.
Preste atenção na mesma página ao grupo “Dinâmica das mudanças”. Esta é apenas a tabela de
trends
, à qual prometemos retornar. Se ele também se tornou muito grande para você e precisa ser particionado, desmarque esse grupo e processe esta tabela exatamente como fez nas tabelas de
history*
.
Manutenção adicional do banco de dados
Como foi escrito anteriormente, para operação normal em tabelas particionadas, é necessário criar partições no tempo. Você pode fazer isso assim:
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, a exclusão de dados antigos agora é nossa preocupação. Felizmente, não há problemas. Isso é feito simplesmente excluindo a partição cujos dados não precisamos mais.
Por exemplo:
ALTER TABLE history DROP PARTITION p20190201;
Diferentemente das instruções DELETE FROM com um intervalo de datas, a DROP PARTITION é executada em alguns segundos, não carrega o servidor e funciona da mesma maneira possível ao usar a replicação no MySQL.
Conclusão
A solução descrita é testada pelo tempo. O volume de dados está crescendo, mas não há uma desaceleração perceptível no desempenho.