terça-feira, 14 de outubro de 2025

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

Particionamento no Zabbix: o passo final para domar seu banco

Publicado por Sysadmin Urbano | Infraestrutura, SysOps e DevOps

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

Particionamento no Zabbix: o passo final para domar seu banco

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

Este post mostra como transformar as tabelas history* e trends* em particionadas por tempo no MySQL, migrando dados de forma segura (downtime mínimo) e automatizando a criação das próximas partições e o expurgo das antigas. É a forma mais previsível e rápida de manter o crescimento sob controle.

Resumo: criar uma cópia particionada, carregar apenas o período que você quer manter, RENAME atômico, e um script diário que cria “a partição de amanhã” e remove as antigas. Sem deletes gigantes, sem sustos.

1) Pré-requisitos e planejamento

  • Banco: MySQL 8+ (MariaDB também funciona com ajustes).
  • Coluna de partição: clock (epoch, presente em history* e trends*).
  • Retenção sugerida: history = 90 dias (diária) | trends = 365 dias (mensal).
  • Espaço em disco para coexistir tabela antiga + nova durante a migração.
  • Janela curta (segundos) apenas para o RENAME.

2) Migração: criar cópia particionada e trocar

2.1) History — partição diária

Exemplo com history. Repita o mesmo para history_uint, history_str, history_text, history_log.

-- A) Criar tabela particionada (inclua alguns dias + pMAX)
CREATE TABLE zabbix.history_p LIKE zabbix.history;

ALTER TABLE zabbix.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 p2025_10_12 VALUES LESS THAN (UNIX_TIMESTAMP('2025-10-13')),
  PARTITION pMAX       VALUES LESS THAN (MAXVALUE)
);

-- B) Carregar apenas o período que deseja manter (ex.: 90 dias)
INSERT /*+ MAX_EXECUTION_TIME(600000) */ INTO zabbix.history_p
SELECT * FROM zabbix.history
WHERE clock >= UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 90 DAY));

-- C) Troca atômica (faça com zabbix-server parado apenas neste passo)
RENAME TABLE zabbix.history TO zabbix.history_old,
             zabbix.history_p TO zabbix.history;

-- D) Depois, quando confirmar, descarte a antiga
DROP TABLE zabbix.history_old;

2.2) Trends — partição mensal

Exemplo com trends. Repita para trends_uint.

CREATE TABLE zabbix.trends_p LIKE zabbix.trends;

ALTER TABLE zabbix.trends_p
PARTITION BY RANGE (clock) (
  PARTITION p2025_10 VALUES LESS THAN (UNIX_TIMESTAMP('2025-11-01')),
  PARTITION p2025_11 VALUES LESS THAN (UNIX_TIMESTAMP('2025-12-01')),
  PARTITION pMAX     VALUES LESS THAN (MAXVALUE)
);

INSERT /*+ MAX_EXECUTION_TIME(600000) */ INTO zabbix.trends_p
SELECT * FROM zabbix.trends
WHERE clock >= UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 365 DAY));

RENAME TABLE zabbix.trends TO zabbix.trends_old,
             zabbix.trends_p TO zabbix.trends;

DROP TABLE zabbix.trends_old;

Dica: se a tabela for enorme, copie por janelas (ex.: por dia/mês) com vários INSERT ... WHERE para suavizar I/O.


3) Uso diário: criar a próxima partição e remover as antigas

Depois da migração, a limpeza vira DROP PARTITION (rápido) e a prevenção vira “criar a partição do amanhã”. O script abaixo faz os dois de forma segura.

3.1) Script de manutenção automática

Salve como /opt/zbx_partitions/maintain_partitions.sh e dê chmod +x. Ajuste as variáveis no topo.

#!/usr/bin/env bash
set -euo pipefail

# ======= CONFIG =======
DB="zabbix"
MYSQL="mysql --defaults-extra-file=$HOME/.my.cnf --batch --raw"
HISTORY_TABLES=("history" "history_uint" "history_str" "history_text" "history_log")
TRENDS_TABLES=("trends" "trends_uint")

HISTORY_RETENTION_DAYS=90      # manter 90d em history*
TRENDS_RETENTION_DAYS=365      # manter 365d em trends*

# mínimo de espaço livre (em %) no filesystem do datadir
MIN_FREE_PCT=5

# ======= FUNÇÕES =======
die(){ echo "ERROR: $*" >&2; exit 1; }

check_free_space(){
  local mount
  mount="$(awk '$2=="/var/lib/mysql"{print $2}' /proc/mounts || echo "/")"
  local pct
  pct=$(df -P "$mount" | awk 'NR==2{print $5}' | tr -d '%')
  local free=$((100-pct))
  if (( free < MIN_FREE_PCT )); then
    die "Espaço livre insuficiente em $mount (${free}% < ${MIN_FREE_PCT}%). Abortado."
  fi
}

part_exists(){
  local tbl=$1 part=$2
  $MYSQL -N -e "SELECT 1 FROM information_schema.PARTITIONS
                WHERE TABLE_SCHEMA='${DB}' AND TABLE_NAME='${tbl}'
                  AND PARTITION_NAME='${part}'" | grep -q 1
}

add_daily_partition(){
  local tbl=$1 day=$2 next=$3 # day=YYYY-MM-DD, next=YYYY-MM-DD(+1)
  local pname="p${day//-/\_}"
  if part_exists "$tbl" "$pname"; then
    echo "  - $tbl: partição $pname já existe"
    return 0
  fi
  echo "  - $tbl: criando partição $pname (< ${next})"
  $MYSQL -e "ALTER TABLE ${DB}.${tbl}
             REORGANIZE PARTITION pMAX INTO (
               PARTITION ${pname} VALUES LESS THAN (UNIX_TIMESTAMP('${next}')),
               PARTITION pMAX VALUES LESS THAN (MAXVALUE)
             );"
}

add_monthly_partition(){
  local tbl=$1 first_of_month=$2 first_of_next=$3 # YYYY-MM-01
  local pname="p${first_of_month:0:4}_${first_of_month:5:2}"
  if part_exists "$tbl" "$pname"; then
    echo "  - $tbl: partição $pname já existe"
    return 0
  fi
  echo "  - $tbl: criando partição $pname (< ${first_of_next})"
  $MYSQL -e "ALTER TABLE ${DB}.${tbl}
             REORGANIZE PARTITION pMAX INTO (
               PARTITION ${pname} VALUES LESS THAN (UNIX_TIMESTAMP('${first_of_next}')),
               PARTITION pMAX VALUES LESS THAN (MAXVALUE)
             );"
}

drop_older_than_days(){
  local tbl=$1 keep_days=$2
  local cutoff_epoch
  cutoff_epoch=$($MYSQL -N -e "SELECT UNIX_TIMESTAMP(DATE_SUB(CURDATE(), INTERVAL ${keep_days} DAY))")
  while read -r pname pdescr; do
    local less
    less=$(echo "$pdescr" | sed -n "s/.*LESS THAN (\([0-9]\+\)).*/\1/p")
    [ -z "$less" ] && continue
    if (( less <= cutoff_epoch )); then
      echo "  - $tbl: DROP PARTITION $pname (boundary ${less} <= cutoff ${cutoff_epoch})"
      $MYSQL -e "ALTER TABLE ${DB}.${tbl} DROP PARTITION ${pname};"
    fi
  done < <(
    $MYSQL -N -e "SELECT PARTITION_NAME,
                         CONCAT('LESS THAN (',PARTITION_DESCRIPTION,')')
                  FROM information_schema.PARTITIONS
                  WHERE TABLE_SCHEMA='${DB}' AND TABLE_NAME='${tbl}'
                    AND PARTITION_NAME IS NOT NULL
                    AND PARTITION_NAME!='pMAX'
                  ORDER BY PARTITION_DESCRIPTION"
  )
}

# ======= MAIN =======
echo "== Zabbix partitions maintenance ($(date -Is)) =="
check_free_space

TODAY=$(date -u +%F)
TOMORROW=$(date -u -d "$TODAY +1 day" +%F)
FIRST_NEXT_MONTH=$(date -u -d "$(date -u +%Y-%m-01) +1 month" +%F)
FIRST_NEXT_NEXT_MONTH=$(date -u -d "$FIRST_NEXT_MONTH +1 month" +%F)

echo "-- Garantindo partição DIÁRIA de amanhã para history* --"
for t in "${HISTORY_TABLES[@]}"; do
  add_daily_partition "$t" "$TOMORROW" "$(date -u -d "$TOMORROW +1 day" +%F)"
done

echo "-- Garantindo partição MENSAL seguinte para trends* (se mudança de mês) --"
for t in "${TRENDS_TABLES[@]}"; do
  add_monthly_partition "$t" "$FIRST_NEXT_MONTH" "$FIRST_NEXT_NEXT_MONTH"
done

echo "-- Drop de partições antigas (retenção) --"
for t in "${HISTORY_TABLES[@]}"; do
  drop_older_than_days "$t" "$HISTORY_RETENTION_DAYS"
done
for t in "${TRENDS_TABLES[@]}"; do
  drop_older_than_days "$t" "$TRENDS_RETENTION_DAYS"
done

echo "OK."

3.2) Cron diário

Execute todo dia às 01:10 UTC, gerando log de manutenção.

# /etc/cron.d/zbx_partitions
10 1 * * * root /opt/zbx_partitions/maintain_partitions.sh >> /var/log/zbx_partitions.log 2>&1

4) Auditoria e verificação

4.1) Listar partições atuais

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

4.2) Conferir se existe pMAX

SELECT COUNT(*) FROM information_schema.PARTITIONS
WHERE TABLE_SCHEMA='zabbix' AND TABLE_NAME='history' AND PARTITION_NAME='pMAX';

4.3) Tamanho por partição (aproximação)

SELECT PARTITION_NAME,
       ROUND(DATA_LENGTH/1024/1024,2) AS data_mb,
       ROUND(INDEX_LENGTH/1024/1024,2) AS idx_mb
FROM information_schema.PARTITIONS
WHERE TABLE_SCHEMA='zabbix' AND TABLE_NAME='history'
ORDER BY PARTITION_NAME;

5) Boas práticas

  • Backup antes da migração e antes de grandes drops.
  • Janela curta apenas para o RENAME; a cópia pode rodar antes.
  • Retenção do Housekeeper alinhada com o plano de partições (para não competir).
  • UTC no script (evita dores com DST); se preferir local, ajuste os dates.
  • Monitorar espaço e I/O: o script já barra se faltar disco.

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