一个跑了一年多的 Zabbix 3.0.10,数据库膨胀几乎必然会发生。尤其当你打开 MySQL 看到history_uint和alerts几个表占了十几个 GB,前端查询历史告警越来越慢,甚至在 Zabbix 前端里点开“问题”都会转圈。这个版本不像后面 4.0/5.0 自带那么完善的分区支持,维护老版本,清理历史告警问题数据几乎是必修课。写这篇文章,是因为我最近刚帮一个朋友处理了同样的场景:Zabbix 3.0.10,数据库后端是 MySQL,某天磁盘告警,一查全是告警历史表和问题事件表,最后用一套手工脚本安全把几个大表从 60GB 缩到 20GB 以内。下面把整个思路、具体 SQL、自动脚本和踩过的坑都整理出来,供同样被困在 3.0.x 老版本里的运维参考。
1. 先搞清楚 Zabbix 3.0.10 的数据都放在哪儿
很多人在清理前容易犯一个错误:上来就DELETE FROM history;,结果发现数据量没降多少,还误删了想要的趋势数据。Zabbix 里的“历史数据”和“告警数据”其实是两套体系,先分清楚,后面才不会把该留的也删了。
1.1 监控历史数据与告警数据是两套体系
history系列表存的是监控项采集到的原始值,比如 CPU、内存、流量、磁盘 IO。Zabbix 按数值类型拆成了多张表:history存浮点数,history_uint存整数,history_str存短字符串,history_text存长文本,history_log存日志文件。这些表是增长最快的大户,但它们属于“监控历史”,不完全等于告警数据。
另一套是与“问题/告警”相关的表。events表记录触发器状态变化的事件,也就是问题产生、恢复、发现、自动注销等完整事件流;alerts表记录每一次告警动作的发送情况,比如发邮件、发脚本、标记动作执行结果;problem表是 Zabbix 3.0 开始新增的“问题”视图表,前端“问题”页面能看到的内容基本都来自它。你说的“历史告警问题数据”,实际指的就是alerts、events、problem以及它们关联的acknowledges这类表。
从这里可以得出一个结论:如果是磁盘空间告急,清理优先级最高的是history和trends;如果是要让前端“问题/告警”历史变清爽,重心则在alerts、events、problem。两者可以一起做,但 SQL 条件不能混着写。
1.2 3.0.10 版本的表结构特征
Zabbix 3.0.10 的表结构和高版本有区别。在 3.0.x 里,events表字段大概是eventid、source、object、objectid、value、clock、ns、value_changed、acknowledged。problem表里会保存与问题状态相关的r_clock、r_eventid、r_ns这类恢复时间字段。alerts表则记录了eventid、mediatypeid、sendto、status、retries、error等动作发送信息。
如果对这些字段不熟,动手前先跑两条查询看一眼实际结构:
SHOW CREATE TABLE events; SHOW CREATE TABLE problem; SHOW CREATE TABLE alerts;把字段结构确认清楚再写删除 SQL,能少走很多弯路。老版本的官方文档和前端页面不一定对得上,实际库里到底有没有某个列,以SHOW CREATE TABLE为准。
下面这张表是 3.0.10 里最常见的几类大表,以及它们在清理时的定位:
| 表名 | 存储内容 | 数据量增长特点 | 清理建议 |
|---|---|---|---|
history/history_uint | 监控项原始采集值 | 每秒都可能写入,增长最猛 | 按保留天数清理,严格控制 |
history_str/history_text | 长文本型监控项 | 字符型主机名、告警信息多 | 视情况保留更短周期 |
trends/trends_uint | 小时级趋势数据 | 相对稳定,但久了也大 | 建议保留一年以上 |
alerts | 告警动作发送记录 | 依赖动作配置和告警频率 | 保留 90~180 天即可 |
events | 所有触发器/发现事件 | 每次问题产生、恢复都插入 | 保留 90~180 天即可 |
problem | 前端问题列表数据 | 如果长期不清理会留大量已恢复记录 | 只保留当前问题和最近历史问题 |
auditlog | 用户操作日志 | 登录、改配置都写 | 按需保留 |
这张表也解释了为什么我们待会要按“先删子表、再删主表”的顺序,而不是把events一下清掉。
2. 动手清理前,先想清楚保留策略和备份方案
这一步别省。Zabbix 3.0.10 的数据库清理不像高版本有可视化按钮,全得靠 SQL。一旦删错,可能把正在告警的问题也删了,或者把审计需要的历史记录弄丢。
2.1 保留多久合适:结合容量和审计需求
没有统一的天数,我的建议是:
history/history_uint保留 7~30 天。如果 Grafana 有长期趋势需求,可以通过trends来补,不用靠原始历史。trends/trends_uint保留 365 天左右,个别需要长期回溯的监控项可以在前端单独调整。alerts和events保留 90~180 天。如果公司有告警审计要求,千万不能小于 180 天,否则审计要历史记录时很被动。problem只保留当前未恢复问题和近 90 天已恢复问题,避免前端问题列表越翻越长。
我见过一个极端案例:某环境每天产生 8~10 万条告警动作记录,一个月alerts表就增加了近 300 万行。算下来一年光alerts就超 3000 万行,按单行 200 字节估算,光这一张表就要 6GB 以上,还不算索引。如果不定保留策略,清理速度肯定赶不上增长速度。
2.2 备份与影响范围评估
清理前要做备份,但大数据库做全量mysqldump不现实。比较稳妥的做法是:
- 如果有从库或备份系统,先确认最近一次备份可用,再在从库上做一次快照验证。
- 单库超过 50GB 时,优先在从库上把备份导出到一个独立文件,而不是在主库上
mysqldump。 - 如果没有从库,至少要针对要删除的
alerts、events、problem表做一次独立备份,比如导出这些表今天之前的数据。
清理期间的影响范围也要提前评估。alerts、events都是大表,DELETE 时会占用较多数据库 IO,可能导致 Zabbix Server 写入变慢、前端查询变卡,甚至告警动作发送延迟。所以一定要选在低峰期,比如凌晨 2 点到 5 点,并通告相关同事避免误报。
2.3 检查当前数据库引擎和空间占用
在清理前先定位最占空间的表。MySQL 后端可以用这段 SQL:
SELECT table_name AS '表名', engine AS '引擎', ROUND((data_length + index_length) / 1024 / 1024, 2) AS '总大小MB', table_rows AS '估算行数' FROM information_schema.tables WHERE table_schema = 'zabbix' ORDER BY (data_length + index_length) DESC LIMIT 20;如果后端是 PostgreSQL,换这条:
SELECT relname AS table_name, pg_size_pretty(pg_total_relation_size(c.oid)) AS total_size, n_live_tup AS live_rows FROM pg_class c JOIN pg_stat_user_tables t ON c.relname = t.relname WHERE c.relname NOT LIKE 'pg_%' ORDER BY pg_total_relation_size(c.oid) DESC LIMIT 20;另外要确认innodb_file_per_table参数。MySQL 5.6/5.7 默认是开启的,每个 InnoDB 表有独立的.ibd文件,这样删除后执行OPTIMIZE TABLE才能释放磁盘空间。如果没开,所有表都挤在共享表空间里,删除后即便OPTIMIZE也很难缩容。3.0.10 时代很多环境还在用 MySQL 5.5,这一点尤其要留意。
3. 手工清理历史告警问题数据的标准流程
清理告警问题数据,我建议按这个顺序执行:先删alerts,再处理problem里的已恢复历史记录,最后删events。原因很简单:alerts和problem都关联了events.eventid,先删子表可以避免留下孤儿数据,也避免后面删除events时出现外键关联报错。
3.1 先拿“告警动作记录”开刀:alerts 表
alerts表是你确认动作是否发送成功、邮件是否有报错的关键记录,但它不是唯一的数据来源。只要 Zabbix Server 的日志和审计系统独立保留,alerts没必要留太久。
删除前先看一眼要删多少:
SELECT COUNT(*) AS cnt FROM alerts WHERE clock < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 180 DAY));确认数量之后,分块删除。一次性DELETE几百万行在 InnoDB 里会产生超大事务,持有大量行锁,副作用是你可能把 Zabbix Server 的写事务也堵死。分块的标准姿势是每次只删 5000 行:
DELETE FROM alerts WHERE clock < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 180 DAY)) LIMIT 5000;重复执行直到影响行数为 0。如果担心删除顺序不稳定,也可以加ORDER BY alertid LIMIT 5000,让每次删除的是一个递增区间:
DELETE FROM alerts WHERE clock < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 180 DAY)) ORDER BY alertid LIMIT 5000;3.2 再清理历史事件表:events
events表保存了所有事件流水。光看容量,它可能没有history_uint大,但它直接影响前端“事件”和“问题”历史页面。
在删除前,需要确认一个问题:当前problem表里还有哪些事件正在使用。我们可以用LEFT JOIN把所有当前仍被综合问题表引用的eventid排除掉:
DELETE FROM events WHERE eventid NOT IN ( SELECT eventid FROM problem ) AND clock < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 180 DAY));这种写法在大表上性能不理想,尤其是problem表很大时,NOT IN会让优化器做大量扫描。更稳妥的写法是分块删除,每次先挑一批要删的eventid:
DELETE FROM events WHERE eventid IN ( SELECT eventid FROM ( SELECT e.eventid FROM events e LEFT JOIN problem p ON e.eventid = p.eventid WHERE p.eventid IS NULL AND e.clock < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 180 DAY)) ORDER BY e.eventid LIMIT 5000 ) tmp );如果这步报“You can't specify target table for update in FROM clause”,可以把内层子查询改成 JOIN 形式:
DELETE e FROM events e JOIN ( SELECT e2.eventid FROM events e2 LEFT JOIN problem p ON e2.eventid = p.eventid WHERE p.eventid IS NULL AND e2.clock < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 180 DAY)) ORDER BY e2.eventid LIMIT 5000 ) x ON e.eventid = x.eventid;这里有个细节:如果你之前已经用OPTIMIZE TABLE或者别的方式把problem表的时间范围缩短了,但events里还有对应的事件,这条语句会把对应事件保留下来。换句话说,清理顺序很重要,必须先把不再需要的problem历史问题记录清掉,再删events。
3.3 处理 problem 表里已经恢复但残留的问题记录
Zabbix 3.0.10 的problem表容易出现两类问题:
- 正常场景下,问题恢复后
r_clock会写入恢复时间,但表里仍然保留这条已恢复问题记录,用于前端“问题历史”显示。 - 异常场景下,Zabbix Server 由于数据库抖动或处理异常,问题已经恢复了,但
problem表还留着r_clock = 0的记录,前端就一直显示“正在处理”。
对第一类,我们要清理的是已恢复且超过保留期的记录。可以用这条 SQL:
DELETE FROM problem WHERE r_clock > 0 AND r_clock < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 90 DAY));不要动r_clock = 0的记录,因为r_clock = 0通常代表当前仍未恢复,一旦误删,前端会丢失正在告警的问题,并且在触发器仍处于异常状态时,你短期内看不到告警。
对第二类,问题已经实际恢复但problem表状态没更新,最安全的做法不是直接 DELETE,而是先重启 Zabbix Server 让它重新计算一次问题状态:
systemctl restart zabbix-server如果重启后仍然显示问题,再去人工确认事件历史。确认主机对应触发器确实已经产生了“恢复”事件,再删除残留的problem记录。比如查到残留的eventid = 123456,可以执行:
DELETE FROM problem WHERE eventid = 123456; DELETE FROM acknowledges WHERE eventid = 123456;这里是“确认后”再操作,别拿这条命令当成通用清理手段。
3.4 如果 history 和 trends 也想清
磁盘压力太大时,history系列表肯定要处理。删除命令很简单,但问题在于表太大,直接 DELETE 会长时间锁表。建议也是分块:
DELETE FROM history_uint WHERE clock < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 30 DAY)) LIMIT 5000;这里有几个坑要提一下。history_uint的写入频率非常高,如果 Zabbix Server 还在运行,删除事务和写入事务混在一起,优化器可能选择奇怪的执行计划。我的实操经验是,清理大表前,临时停掉 Zabbix Server 的 housekeeper,甚至停掉 Server 本体的采集进程一段时间,不然很容易出现“一直删不完”的假象。
另外,删除后空间不会立刻释放。MySQL InnoDB 的表空间默认不会因为 DELETE 变小,你必须在删除后执行:
OPTIMIZE TABLE alerts, events, problem, history_uint, history, trends, trends_uint;如果是 PostgreSQL:
VACUUM (FULL, ANALYZE) alerts; VACUUM (FULL, ANALYZE) events; REINDEX TABLE events;OPTIMIZE TABLE是重打表数据并重建索引,耗时和表大小成正比。执行期间会有短暂锁表,请务必放在低峰期,并提前算好磁盘可用空间,因为重建过程需要临时占用一些额外空间。
4. 把清理写成自动化脚本,并接入定时任务
手敲 SQL 只适合一次性清理。如果已经形成“三个月清一次”的固定维护周期,直接把清理动作写成脚本,要比每次都人工查、人工删省事得多。下面给一个我实际在用的 MySQL 后端清理脚本,核心思路是:设置保留天数、分块删除、记录日志。
4.1 MySQL 版本的一键清理脚本
#!/bin/bash # zabbix_cleanup.sh # 针对 Zabbix 3.0.10 + MySQL 后端,清理历史/告警/问题数据 DB_HOST=127.0.0.1 DB_PORT=3306 DB_USER=zabbix DB_PASS='your_password' DB_NAME=zabbix EVENT_DAYS=180 ALERT_DAYS=180 PROBLEM_DAYS=90 HISTORY_DAYS=30 TRENDS_DAYS=365 MYSQL="mysql -h${DB_HOST} -P${DB_PORT} -u${DB_USER} -p${DB_PASS} ${DB_NAME} -N -s" EVENT_TS=$(date -d "${EVENT_DAYS} days ago" +%s) ALERT_TS=$(date -d "${ALERT_DAYS} days ago" +%s) PROBLEM_TS=$(date -d "${PROBLEM_DAYS} days ago" +%s) HISTORY_TS=$(date -d "${HISTORY_DAYS} days ago" +%s) TRENDS_TS=$(date -d "${TRENDS_DAYS} days ago" +%s) log() { echo "$(date '+%F %T') $1" } delete_by_chunk() { local table=$1 local ts=$2 while :; do local cnt cnt=$(${MYSQL} -e "SELECT COUNT(*) FROM ${table} WHERE clock < ${ts};") if [ "${cnt}" -le 0 ]; then break fi log "清理 ${table} 剩余 ${cnt} 行" ${MYSQL} -e "DELETE FROM ${table} WHERE clock < ${ts} LIMIT 5000;" sleep 1 done } log "开始清理 alerts" delete_by_chunk alerts "$ALERT_TS" log "清理已恢复且超过保留期的 problem" while :; do cnt=$(${MYSQL} -e "SELECT COUNT(*) FROM problem WHERE r_clock > 0 AND r_clock < ${PROBLEM_TS};") if [ "${cnt}" -le 0 ]; then break fi ${MYSQL} -e "DELETE FROM problem WHERE r_clock > 0 AND r_clock < ${PROBLEM_TS} LIMIT 5000;" sleep 1 done log "清理 events(保留 problem 仍在引用的记录)" while :; do cnt=$(${MYSQL} -e "SELECT COUNT(*) FROM events e LEFT JOIN problem p ON e.eventid = p.eventid WHERE p.eventid IS NULL AND e.clock < ${EVENT_TS};") if [ "${cnt}" -le 0 ]; then break fi ${MYSQL} -e "DELETE e FROM events e JOIN (SELECT e2.eventid FROM events e2 LEFT JOIN problem p ON e2.eventid = p.eventid WHERE p.eventid IS NULL AND e2.clock < ${EVENT_TS} ORDER BY e2.eventid LIMIT 5000) x ON e.eventid = x.eventid;" sleep 1 done for table in history history_uint history_str history_text history_log; do log "开始清理 ${table}" delete_by_chunk "$table" "$HISTORY_TS" done for table in trends trends_uint; do log "开始清理 ${table}" delete_by_chunk "$table" "$TRENDS_TS" done log "开始优化表" ${MYSQL} -e "OPTIMIZE TABLE alerts, events, problem, history, history_uint, history_str, history_text, history_log, trends, trends_uint;" log "清理完成"这个脚本把password直接放在脚本里有安全隐患,推荐改用~/.my.cnf或者环境变量。比如在配置文件里写:
[client] host=127.0.0.1 user=zabbix password=your_password然后把脚本里的-p${DB_PASS}去掉,脚本看起来更干净,也不会在进程列表中暴露密码。
4.2 用 crontab 定时执行
把这个脚本放到/opt/scripts/下,给执行权限:
chmod +x /opt/scripts/zabbix_cleanup.sh然后加 crontab,比如每个月 1 号凌晨执行:
0 3 1 * * /opt/scripts/zabbix_cleanup.sh > /var/log/zabbix_cleanup.log 2>&1日志一定要保留。下一次清理前先看上一次跑完过了多久、每张表删了多少,才能判断这个清理频率是否跟得上数据增长速度。如果每次跑完不到半个月就又爆空间,说明保留天数设得太长,或者 Zabbix 采集 item 数量本身需要优化,而不是继续靠清理脚本硬扛。
4.3 清理完怎么验证
清理完不要直接走人,建议做三件事:
- 看磁盘空间释放了多少,确认
OPTIMIZE TABLE执行成功。 - 打开 Zabbix 前端,分别看“问题”“事件”“报表”页面是否有异常。
- 跑一条最近数据查询,确认数据写入无异常,当天监控值正常展示。
如果前端某个页面查历史变空白,先检查保留天数是否把“最近”也误删了。比如clock存的 Unix 时间戳,date +%s换算这一步写错,很容易删掉近几天的数据。
5. 常见问题与排查技巧实录
这节收集几个我在清理过程中真正遇到过的问题,按速查表的方式整理出来,遇到同类情况可以直接照做。
| 现象 | 常见原因 | 解决办法 |
|---|---|---|
| 执行 DELETE 卡住不动 | 大事务锁等待,Zabbix Server 正在写入同一张表 | 低峰期执行,分块删除;必要时临时停 Server |
| 删完空间没释放 | InnoDB 表空间没有重建 | 执行OPTIMIZE TABLE,并确保innodb_file_per_table=ON |
| 前端“问题”仍显示已解决的告警 | problem表残留未更新记录 | 先重启 Zabbix Server 让状态重算;确认恢复后再清理残留 |
alerts删了,events还很大 | 两个表没同步清理 | 按顺序先删alerts,再删events |
| housekeeper 总是和手动清理抢资源 | 没有关闭 housekeeper | 临时配置StartHousekeepers=0,清理完再改回 1 |
| 误删了当天数据 | 时间阈值计算有误 | 从备份恢复对应表,后续用UNIX_TIMESTAMP时先SELECT确认 |
| PostgreSQL 删除后空间不释放 | 缺少 VACUUM FULL | 执行VACUUM (FULL, ANALYZE) |
这里单独说一下StartHousekeepers=0的操作。Zabbix 3.0.10 的 housekeeper 是 Zabbix Server 自带的清理线程,默认开启,它会定期按保留策略删除历史数据。手动清理时如果不关,会出现两个进程同时 DELETE 同一张表,互相争锁,轻则删除变慢,重则产生大量锁等待。操作方式是修改zabbix_server.conf:
StartHousekeepers=0然后重启zabbix-server。清理完再改回 1,再次重启。这个方法只建议在维护窗口做,不要长期关闭 housekeeper,否则之后还得全靠手动清理。
另一个容易被忽略的点是分区表。如果你的环境已经把history_uint、events按天或按月做了分区,那清理效率会高很多,因为可以直接DROP PARTITION,而不是逐行 DELETE。但 3.0.10 官方原生没有默认分区,很多人的分区脚本是自己写的。如果你准备以后长期维护 3.0.10,建议尽早把大表改造成分区表,否则数据量到亿级之后,再跑这种清理脚本也会非常痛苦。
最后再分享一个个人经验:清理 Zabbix 老版本数据库这种事,最怕的不是数据库太大,而是“这表到底是什么含义”没搞清楚。我在现场踩过最狠的坑,是直接执行了DELETE FROM events WHERE clock < ...,结果虽然没有外键约束,但前端在查看历史问题时能看到事件记录,problem表里又留着对应的已恢复记录,两边对不上,查询结果非常怪。后来所有操作统一成“先删 alerts 和已恢复 problem,再删 events”的顺序,才稳定下来。如果你也要在 3.0.10 上清理历史告警问题数据,强烈建议先跑一遍只读统计,仔细核对表结构和保留周期,再动手。这样清完不翻车,磁盘也干净。