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 PARTITION — instantâ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.
Gostou deste conteúdo?
Siga o Sysadmin Urbano para mais artigos técnicos sobre Infraestrutura, SysOps e DevOps.

Nenhum comentário:
Postar um comentário