segunda-feira, 13 de outubro de 2025

Zabbix — da Migração ao Fine-Tuning - capítulo 2

Tabelas gigantes no Zabbix? Como otimizar e sobreviver ao Housekeeper

Publicado por Sysadmin Urbano | Infraestrutura, SysOps e DevOps

Um guia prático para quem vive na linha de frente da operação de sistemas.

Capítulo 2 da série “Zabbix — da Migração ao Fine-Tuning” no Sysadmin Urbano.

O crescimento do histórico no Zabbix é implacável. Quando o housekeeper não acompanha, surgem sintomas como banco inchado, rotinas lentas e timeouts em DELETE/TRUNCATE. Neste guia, mostro estratégias seguras para reduzir o tamanho das tabelas, preservar apenas o período necessário e preparar o terreno para particionamento (tema do próximo capítulo).

Resumo do que você vai aplicar: expurgo seguro sem travar o servidor (swap de tabela), particionamento opcional, deleções em lote quando não há janela de manutenção e consultas rápidas para identificar “vilões”.

1) Antes de tudo: defina a retenção

Revise os períodos de retenção nativos em Administration → General → Housekeeping (ou via parâmetros), alinhando com sua necessidade de auditoria/compliance:

  • History: por ex. 7–90 dias (conforme criticidade)
  • Trends: por ex. 180–365 dias

Atenção: reduzir o período não apaga magicamente o passado enorme já gravado; você ainda precisará das técnicas abaixo para “enxugar” o banco atual.


2) Estratégias de expurgo (MySQL/MariaDB)

2.1) Swap: recriar tabela com dados recentes e renomear (rápido e previsível)

Quando a tabela está gigantesca, o caminho mais seguro é criar uma cópia, carregar apenas o período que quer manter e fazer RENAME atômico. O downtime se restringe a segundos (no RENAME).

-- (Janela de manutenção curta recomendada; pare o zabbix-server no RENAME)
CREATE TABLE history_new LIKE history;

-- manter só últimos 90 dias (ajuste)
INSERT /*+ MAX_EXECUTION_TIME(600000) */ INTO history_new
SELECT * FROM history
WHERE clock >= UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 90 DAY));

-- troca atômica
RENAME TABLE history TO history_old, history_new TO history;

-- depois, remova a antiga
DROP TABLE history_old;

Repita para history_uint, history_str, history_text, history_log e (se aplicável) trends/trends_uint.

2.2) Particionar e dropar partições (limpeza em segundos)

Se você já usa (ou deseja usar) particionamento por tempo, a limpeza vira DROP PARTITIONinstantâneo perto de um DELETE massivo. A migração para tabela particionada segue a mesma ideia do swap (criar cópia particionada → carregar período → RENAME).

CREATE TABLE history_p LIKE history;
ALTER TABLE history_p
PARTITION BY RANGE (clock) (
  PARTITION p2025_10_10 VALUES LESS THAN (UNIX_TIMESTAMP('2025-10-11')),
  PARTITION p2025_10_11 VALUES LESS THAN (UNIX_TIMESTAMP('2025-10-12')),
  PARTITION pMAX       VALUES LESS THAN (MAXVALUE)
);

INSERT INTO history_p
SELECT * FROM history
WHERE clock >= UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 90 DAY));

RENAME TABLE history TO history_old, history_p TO history;
DROP TABLE history_old;

-- depois, a limpeza é assim:
ALTER TABLE history DROP PARTITION p2025_07_01;  -- exemplo

2.3) Deletar em lotes (quando não há janela)

Sem parada, mas mais lento. Apaga em “pedaços” para não saturar I/O e reduzir locks longos.

SET @cutoff = UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 90 DAY));
SET @rows = 1;
WHILE @rows > 0 DO
  DELETE FROM history
  WHERE clock < @cutoff
  LIMIT 100000;
  SET @rows = ROW_COUNT();
  DO SLEEP(0.2);
END WHILE;

ANALYZE TABLE history;

Para recuperar espaço físico de verdade no InnoDB, o swap (2.1) é mais eficaz do que apenas DELETE.


3) PostgreSQL: opções equivalentes (com e sem TimescaleDB)

3.1) Swap no Postgres

BEGIN;

CREATE TABLE history_new (LIKE history INCLUDING ALL);

INSERT INTO history_new
SELECT *
FROM history
WHERE clock >= EXTRACT(EPOCH FROM (NOW() - INTERVAL '90 days'));

ALTER TABLE history RENAME TO history_old;
ALTER TABLE history_new RENAME TO history;

COMMIT;

DROP TABLE history_old;
VACUUM (ANALYZE) history;

3.2) Deletar em lotes com ctid

WITH old AS (
  SELECT ctid
  FROM history
  WHERE clock < EXTRACT(EPOCH FROM (NOW() - INTERVAL '90 days'))
  LIMIT 10000
)
DELETE FROM history h
USING old
WHERE h.ctid = old.ctid;

VACUUM (ANALYZE) history;  -- VACUUM FULL se puder ter bloqueio

3.3) TimescaleDB: retenção automática

-- transformar em hypertable (executar uma vez)
SELECT create_hypertable('history', 'clock', if_not_exists => TRUE, migrate_data => TRUE);

-- política de retenção (ex.: 90 dias)
SELECT add_retention_policy('history', INTERVAL '90 days');

-- expurgo imediato opcional
SELECT drop_chunks(INTERVAL '90 days', 'history');

4) “Lock wait timeout exceeded” em TRUNCATE/DELETE: diagnóstico rápido

Se um TRUNCATE travou, quase sempre é metadata lock segurado por outra sessão. Identifique e limpe o bloqueio:

MySQL/MariaDB

SELECT ml.LOCK_STATUS, ml.LOCK_TYPE, ml.LOCK_MODE,
       ps.ID AS thread_id, ps.USER, ps.HOST, ps.TIME, ps.STATE, ps.INFO
FROM performance_schema.metadata_locks ml
JOIN performance_schema.threads th
  ON ml.OWNER_THREAD_ID = th.THREAD_ID
JOIN information_schema.PROCESSLIST ps
  ON th.PROCESSLIST_ID = ps.ID
WHERE ml.OBJECT_SCHEMA = DATABASE()
  AND ml.OBJECT_NAME = 'history';

-- mate a sessão bloqueadora:
KILL <thread_id>;

-- aumentar o tempo de espera (opcional)
SET SESSION lock_wait_timeout = 120;

PostgreSQL

SELECT bl.pid AS blocked_pid, a.usename, a.query, a.state, a.query_start,
       ka.pid AS locker_pid, ka.usename AS locker_user, ka.query AS locker_query
FROM pg_locks bl
JOIN pg_stat_activity a ON a.pid = bl.pid
JOIN pg_locks kl ON kl.locktype = bl.locktype
                AND kl.DATABASE IS NOT DISTINCT FROM bl.DATABASE
                AND kl.relation IS NOT DISTINCT FROM bl.relation
                AND kl.pid != bl.pid
JOIN pg_stat_activity ka ON ka.pid = kl.pid
WHERE bl.GRANTED = false
  AND a.query ILIKE '%history%';

SELECT pg_terminate_backend(<locker_pid>);

5) Consultas úteis para “caçar” tabelas grandes

Top N tabelas por tamanho (MySQL/MariaDB)

SELECT 
    table_schema AS banco,
    table_name   AS tabela,
    ROUND((data_length + index_length) / 1024 / 1024, 2) AS tamanho_mb,
    ROUND(data_length / 1024 / 1024, 2) AS dados_mb,
    ROUND(index_length / 1024 / 1024, 2) AS indices_mb
FROM information_schema.tables
WHERE table_schema NOT IN ('mysql','performance_schema','information_schema','sys')
ORDER BY tamanho_mb DESC
LIMIT 20;

Apenas o schema do Zabbix

SELECT 
    table_name,
    ROUND((data_length + index_length) / 1024 / 1024, 2) AS tamanho_mb
FROM information_schema.tables
WHERE table_schema = 'zabbix'
ORDER BY tamanho_mb DESC
LIMIT 20;

Listar partições existentes (MySQL 8+)

SELECT PARTITION_NAME, PARTITION_DESCRIPTION
FROM information_schema.PARTITIONS
WHERE TABLE_SCHEMA='zabbix' AND TABLE_NAME='history'
ORDER BY PARTITION_DESCRIPTION;

6) Boas práticas finais

  • Backup antes de operações destrutivas.
  • Planeje janela curta apenas para o RENAME (swap); a cópia pode ser feita antes.
  • Alinhe a retenção do Housekeeper com a estratégia (partições ou swap periódico).
  • Monitore I/O e conexões durante expurgos em lote; reduza o LIMIT se o ambiente sentir.
  • Para Postgres, considere fortemente TimescaleDB para simplificar a retenção.

Sobre o Sysadmin Urbano

O Sysadmin Urbano nasceu da vivência real no front das operações de infraestrutura moderna. Aqui falamos de servidores, containers, automação, boas práticas e também dos desafios invisíveis da rotina de quem mantém sistemas vivos. Sem fórmulas mágicas, sem tutoriais pela metade — apenas conteúdo prático, direto e feito para quem sabe que a TI é tanto técnica quanto sobrevivência.

🔗 Confira mais artigos ou volte para a página inicial.

Gostou deste conteúdo?

Siga o Sysadmin Urbano para mais artigos técnicos sobre Infraestrutura, SysOps e DevOps.

Voltar para a página inicial

Nenhum comentário:

Postar um comentário