
Zabbix 3.0.10一套在我手里跑了四年的监控系统最近天天被磁盘空间告警轰炸。登录数据库一看events 表十几个亿行alerts 表两亿多行光历史告警和问题数据就吃掉了将近三百 GB 空间。这个版本的 Zabbix 在主机监控和性能数据上确实稳但它有个老毛病跑久了之后数据库里的历史告警和问题数据如果不人工干预就会像老房子的储藏间一样越堆越满直到你被磁盘写满告警叫醒。这篇文章就是一次完整的“Zabbix 3.0.10 数据库清理历史告警问题数据”实操记录。我会把为什么要清、怎么清、哪些表能碰、哪些表碰了会翻车、清完怎么把磁盘空间真的拿回来以及我踩过的几个坑全部摊开来讲。适合正在维护 Zabbix 3.0.x 的运维、监控管理员和需要接手这套库的 DBA 参考尤其适合那种已经被告警数据积压搞得焦头烂额、正准备对数据库动手的同学。1. 问题背景Zabbix 3.0.10 跑久了为什么数据库必然膨胀1.1 监控系统“只管记、不管扔”的设计逻辑Zabbix 的数据库里其实混着两类性质完全不同的数据。一类是性能数据也就是 history、trends 这两类表它们记录的是监控项的历史取值和趋势汇总只要你前端里配了 History storage period 和 Trend storage periodZabbix 内置的 Housekeeper 进程就会按时间定期清理这部分一般不会积压成灾难。另一类是事件和告警数据也就是 events、alerts 这两张表它们记录的是触发器状态变化、问题产生、问题恢复、告警通知发送结果等等。这类数据在 Zabbix 里的定位是“业务记录”很多团队根本不会去想它要不要清理或者前端配置了保留期但 Housekeeper 根本删不过来。再加上 3.0 系列引入的 problem 表负责记录当前未恢复的问题它和 events 表是关联的清理时稍不留神就会把前端的问题列表搞出灵异现象。举个直观的例子。假设你有 2000 台主机每台主机挂了 50 个监控项采集周期 30 秒一次一天下来光 history 表就要写入近 3 亿行。当然 history 表会被周期性清理但 events 表和 alerts 表不一样它的写入量取决于触发器和告警动作的复杂程度一台主机一天产生几千条事件是很正常的。几万台主机跑上两三年events 和 alerts 轻松往十亿行级别迈。这时候不仅是磁盘空间告急连 Zabbix 前端打开“最近告警”页面都会卡到怀疑人生。1.2 3.0.10 这个版本特有的表结构画像Zabbix 3.0 开始引入 problem 表这是和 2.x 时代最大的结构差异。它把“当前未解决的所有问题”单独拿出来存和 events 表通过 eventid 关联。这就意味着清理历史告警时不能只盯着 events 表删还要同步考虑 problem 表和 alerts 表否则就会出现“问题列表里那条三个月前的告警永远挂着点进去却看不到任何历史事件”的诡异状态。我按自己的经验把这些表分了个类方便后面做清理时心里有数表名角色说明能不能直接清events触发器事件、恢复事件、发现事件、自动注册事件等一切问题的源头按时间分批删但要避开活跃问题关联的事件alerts告警动作的执行记录包括邮件、脚本、媒体通知的回执先于 events 删除因为它是事件的下游problem3.0 之后当前未恢复问题的清单只清理已恢复的旧记录活跃问题绝不能碰acknowledges告警确认/手动关闭的记录跟着对应事件一起删auditlog操作审计日志按时间删独立数据不影响业务逻辑很多同学清理时犯的最大错误就是把 events 和 problem 当成同一类数据一把 DELETE OVER 过去。实际上 problem 表是 Zabbix 前端“问题”页签的直接数据来源你把它删了或者把活跃问题对应的 events 删了前端就会永远显示一个不消失的红色故障或者显示一个点进去什么都没有的问题。这块后面我会专门讲。2. 动手前必须想清楚的几件事2.1 为什么不能直接把历史告警表“一刀切清空”先说结论TRUNCATE TABLE events 这种操作在 Zabbix 3.0.10 上基本等于自杀式清理不到万不得已千万别干。原因其实很好理解。events 表在这里更像是“案发记录本”alerts 表是“出警回执单”problem 表是“未结案清单”。你如果把案发记录本直接撕了出警回执单还在但已经不知道对应哪个案件了未结案清单也在但上面的案件编号全都失效了。Zabbix 前端在查询“问题”列表时会去 JOIN events 表获取问题名称、发生时间、恢复时间一旦对应的事件行没了页面显示就会错乱最典型的就是“问题一直挂红无法恢复也无法确认”。另外注意一点Zabbix 官方的建表脚本里事件相关表之间并没有太多物理外键约束MySQL 下直接 DELETE 通常不会报外键错误很多人因此大胆地一把梭。但没报错不代表逻辑正确业务层面的关联关系是隐性的删错了地方前端表现会教你做人。所以清理的第一步不是写 DELETE而是先搞清楚你清理的是哪一层数据以及哪些行必须留下。我的基本原则是alerts 先清、events 次之、problem 只清已恢复的旧记录、活跃问题关联的数据一律不动。2.2 备份策略与回滚预案清理数据这件事备份做得再足都不为过尤其是 Zabbix 这种 7×24 小时的在线监控系统。我这里说的备份不是整个实例的物理备份而是针对你要动的四张表做定向导出这样回滚速度快代价也小。mysqldump -u zabbix -p --single-transaction --quick \ zabbix events alerts problem acknowledges zabbix_event_tables_before_clean.sql注意加 --single-transaction这样 InnoDB 表可以拿到一致性的快照备份不会影响在线写入。备份文件大小可能几个 GB但对比后续误删后无法恢复的代价这点磁盘和时间花得值。备份完成后我习惯再做一个“逻辑演练”在测试库上把备份文件导入然后模拟执行你的清理 SQL确认前端查询没问题再上生产。这个步骤看着繁琐但在 3.0.10 这种老版本上特别值得因为它的前端问题列表查询逻辑和 4.0 之后的新版有不少差异脚本里的坑不是一眼能看出来的。2.3 清理方案选型分批删除、分区表、重建表怎么选针对不同量级的数据清理方案差别很大。我整理了一个选型表格基本能覆盖大多数 Zabbix 3.0.10 的清理场景方案适用规模优点缺点推荐度DELETE 分批清理百万级~千万级灵活可控可按时间精确保留数据无需停机删除速度偏慢会产生碎片需要持续监控中小规模首选分区表 DROP PARTITION亿级以上秒级删除对在线影响小清理后无碎片需要提前改造表结构维护分区规划大规模最优解新建表 导入保留数据任意规模但有停机窗口空间回收最彻底可顺便整理表结构停机时间长导入慢风险最高兜底方案如果你是第一次清理我建议无论库多大都先用 DELETE 分批的方式跑一遍一方面练手一方面能看清删除过程对数据库 IO 和主从同步的影响后续再决定要不要改成分区表方案。直接上分区表改造对 3.0.10 这种老库风险不小ALTER TABLE 重构亿级表的那段时间Zabbix server 大概率是要停掉的。另外多说一句如果你的库已经大到连 DELETE 分批都觉得吃力那说明你已经晚了半年才动手该考虑的不是“怎么清理”而是“为什么 Housekeeper 没扛住”以及要不要借这次清零的机会把 events 和 alerts 的表结构改成按月的 RANGE 分区这才是治本。3. 核心实操清掉历史告警问题数据的完整 SQL 流程3.1 第一步摸底先搞清楚表有多大、哪些数据该删清理前先做一次体检摸清家底。下面这条 SQL 能按数据量排出 Zabbix 库里最占空间的二十张表SELECT table_name, engine, table_rows, ROUND((data_length index_length) / 1024 / 1024, 2) AS total_mb FROM information_schema.tables WHERE table_schema zabbix ORDER BY (data_length index_length) DESC LIMIT 20;注意 information_schema 里的 table_rows 只是估算值InnoDB 不会给精确行数但用来判断哪些表是“大户”完全够用。然后看 events 表的时间分布确认历史数据的跨度SELECT FROM_UNIXTIME(MIN(clock)) AS earliest_time, FROM_UNIXTIME(MAX(clock)) AS latest_time, COUNT(*) AS total_rows FROM events;确定保留周期是这步的关键决策。以我自己的经验Zabbix 告警历史保留 90 天足够满足回溯需求再久的数据留着也只是占地方。3.0.10 的 clock 字段是 Unix 时间戳所以 90 天对应的时间戳下界可以直接用一个表达式算出来SELECT UNIX_TIMESTAMP() - 90 * 24 * 3600 AS deadline_ts;我在实际清理时习惯以这个值作为分界线只删 clock 小于它的老数据更近的数据全部保留即使某些事件已经没有业务价值了也不去动它给自己留足安全余量。3.2 第二步分批删除 alerts避免锁表和超大事务很多人第一次清理时犯的错误是一条 DELETE 语句删掉几百万行结果直接让数据库锁了十几分钟所有监控数据写入全部堆积Zabbix server 的队列直接飙红。所以这里必须分批。还有一个非常关键的技术细节MySQL 的多表 DELETE 语句是不支持 LIMIT 的。也就是说你不能直接写“DELETE FROM alerts JOIN events ON ... LIMIT 5000”这种语法。我采用的方案是“主键推进 IN 子查询分批”每次先拉一组要删的 alertid然后按主键删掉。下面是一个 Python 清理脚本的核心框架我实际用的是 pymysql脚本逻辑可以照搬到自己环境里慢慢调import pymysql import time conn pymysql.connect( host127.0.0.1, userzabbix, passwordyourpassword, databasezabbix, autocommitFalse ) deadline_ts 90 * 24 * 3600 # 按需调整单位秒 batch_size 5000 last_alertid 0 try: while True: with conn.cursor() as cur: cur.execute( SELECT a.alertid FROM alerts a INNER JOIN events e ON a.eventid e.eventid WHERE e.clock %s AND a.alertid %s ORDER BY a.alertid LIMIT %s , (deadline_ts, last_alertid, batch_size)) rows cur.fetchall() if not rows: print(alerts 清理完成) break ids [r[0] for r in rows] placeholders ,.join([%s] * len(ids)) with conn.cursor() as cur: cur.execute(fDELETE FROM alerts WHERE alertid IN ({placeholders}), ids) conn.commit() last_alertid max(ids) print(f已删除 alerts 批次最大 alertid{last_alertid}) time.sleep(1) finally: conn.close()脚本里有两个关键点值得解释一下。第一个是“a.alertid last_alertid”这个推进条件。它利用 alertid 唯一递增的特性每轮都把处理进度往后推确保不漏数据同时因为每批只处理 5000 行锁的粒度很小对在线写入的影响可控。第二个是每批之间 sleep 1 秒给数据库一个喘息的机会也降低对主从同步的冲击。我试过把 batch_size 拉到 20000删除速度确实更快但主库的 IO 等待和从库延迟肉眼可见地升高监控数据采集都开始出现延迟。后来我固定用 5000 一批牺牲一点时间换整个集群的稳定这在我看来是完全划算的。3.3 第三步删除旧 events并同步处理 problem 与 acknowledgesalerts 清完之后紧接着处理 acknowledges再往后才是 events 本体。删除顺序很重要先删下游表再删上游表避免留下指向不存在事件的孤儿数据。对于 Zabbix 3.0.10这里额外要过一道坎problem 表。我前面说过活跃问题的 eventid 不能删否则前端问题列表会永久挂红。所以在删 events 时筛选条件除了时间还要排除那些和活跃问题关联的事件。我的做法是先查一下当前 problem 表里有没有未恢复的记录以及它们的 eventid 范围SELECT FROM_UNIXTIME(MIN(clock)) AS oldest_active_problem, COUNT(*) AS active_problem_count FROM problem WHERE r_eventid IS NULL;如果这条 SQL 查出来的 active_problem_count 是 0那说明当前没有活动问题events 表按时间删就相对安全。如果有活动问题而我保留了最近 90 天的数据这些活动问题基本都在这 90 天内所以也不会被误伤。但如果你的环境里存在一条挂了两百多天的僵尸问题那它的关联事件就会被你删掉处理它最好的办法是先去前端确认并手动关闭它再回来执行清理。实际执行的 JavaScript 式伪代码是这样的思路因为脚本逻辑和前面类似只贴关键部分# 每轮拉取需要删除的 events 批次 # 条件是 clock 小于 deadline并且 eventid 大于上一轮进度 cur.execute( SELECT e.eventid FROM events e LEFT JOIN problem p ON p.eventid e.eventid AND p.r_eventid IS NULL WHERE e.clock %s AND e.eventid %s AND p.eventid IS NULL ORDER BY e.eventid LIMIT %s , (deadline_ts, last_eventid, batch_size)) # 拿到这一批 eventid 后依次删除后三张表的关联数据 # 1. DELETE FROM acknowledges WHERE eventid IN (...) # 2. DELETE FROM problem WHERE eventid IN (...) AND r_eventid IS NOT NULL # 3. DELETE FROM events WHERE eventid IN (...)这段逻辑里 LEFT JOIN problem 并过滤 p.eventid IS NULL就是为了强制保护活跃问题。虽然前面说了 90 天窗口基本够用但加上这道保险心里踏实很多。problem 表的处理上还有个小细节它里面可能保存着一些 r_eventid 非空的旧记录也就是已经恢复了的历史问题残留。这些记录可以跟着旧 events 一起清掉前端不会再展示它们。但注意不能把整个 problem 表清空否则当前活跃问题全部从界面上消失数据却还在写入告警状态直接错乱。3.4 第四步清理后的收尾OPTIMIZE 表空间与磁盘空间回收DELETE 删除数据有个让新手最容易困惑的问题删完几亿行之后查数据确实没了但磁盘空间一点没变。这是因为 InnoDB 的 DELETE 只是在数据页上打了删除标记物理文件不会自动收缩。所以清理流程里必须补上表空间整理这一步。在开始之前先确认一个关键配置SHOW VARIABLES LIKE innodb_file_per_table;如果这个值是 ON每个 InnoDB 表都有自己独立的 .ibd 文件那么删除数据后执行 OPTIMIZE TABLE 或者 ALTER TABLE ... ENGINEInnoDB 就能把表文件真正压小。比如OPTIMIZE TABLE events; OPTIMIZE TABLE alerts; OPTIMIZE TABLE problem; OPTIMIZE TABLE acknowledges;注意 OPTIMIZE TABLE 在 InnoDB 上本质上是重建表会耗费大量 IO 和 CPU并且长时间锁表。务必放在维护窗口执行建议凌晨业务低峰期跑一次只 OPTIMIZE 一张表不要四张并发。如果 innodb_file_per_table 是 OFF说明所有表数据都塞在共享表空间 ibdata1 里那你执行 OPTIMIZE 也缩不回来ibdata1 只会越用越大。这种库想要把空间真正还给操作系统唯一彻底的办法是把整个实例逻辑导出再导入或者停机改配置文件重建表空间。这个我在第 4 节会细说因为太多人在这里栽跟头了。4. 常见问题与排查技巧实录4.1 清完数据后Zabbix 前端“问题”列表还显示旧告警这是我见过最多的翻车现场清理脚本跑完了数据库行数也降下来了结果打开 Zabbix 前端一看问题列表里还挂着几个月前的那条告警点进去又是空的没有任何事件历史。问题根源基本都在 problem 表残留。Zabbix 3.0 的前端“问题”页签直接读的就是 problem 表你删了 events但 problem 表里还留着那条已恢复问题的记录或者删了 r_eventid 字段所指向的那个恢复事件导致前端无法通过 JOIN 找到问题详情和恢复时间。排查思路很简单执行这条 SQLSELECT p.eventid, p.name, p.severity, FROM_UNIXTIME(p.clock) AS problem_time, FROM_UNIXTIME(p.r_clock) AS recover_time FROM problem p WHERE p.r_eventid IS NOT NULL AND p.r_clock UNIX_TIMESTAMP() - 90 * 24 * 3600;如果查出来的记录还在而关联的 events 已经被你删了那前端就会出问题。解决办法是把这些已恢复且时间久远的 problem 残留记录删掉只保留活跃问题的记录。另一个不太容易想到的原因是 Zabbix server 的内存缓存。3.0.10 的 server 在启动时会加载一段时间内的事件状态清理数据后如果直接开着不重启前端可能仍然展示旧的缓存结果。我在清理完 production 库之后都会跑一遍 service zabbix-server restart再观察前端是否恢复正常。注意重启会短暂中断监控数据收集同样要放在维护窗口。4.2 删除速度越来越慢、锁等待频繁分批删除跑到中途盯着 show processlist 会发现 DELETE 语句频繁卡在 Waiting for lock一两分钟都推不动一批整个库的写入像是被按住了咽喉。这种情况最常见的原因有三个。第一个原因是子查询里的事件筛选没有走索引。events.clock 字段在官方 schema 里建了索引吗3.0.10 的默认 schema 中 events 表对 clock 有索引但如果你是后来自己加的索引或者清理了多次索引状态可能不理想。建议清理前先看 EXPLAIN确认使用了索引再放量跑。第二个原因是删除批次过大。同一批 5000 行如果每行都关联了多个 alerts那实际锁的行数远超 5000锁等待时间自然直线上升。我后来改成根据 alerts 行数动态调整批次当 alerts 关联行特别多时强制把 batch 降到 2000情况明显改善。第三个原因是被删表和正在写入的表争抢 IO。Zabbix server 每时每刻都在向 history 和 trends 表写数据清理过程会跟它抢磁盘带宽。解决方法是限制清理的并发数和速度把 sleep 从 1 秒加到 3 秒并把 batch_size 维持在一个温和的水平。这不是比赛谁删得快而是保证监控系统在清理期间还能正常工作。4.3 磁盘空间没变小排查碎片和共享表空间DELETE 之后不 OPTIMIZE文件不会缩小这个前面说了。但还有一种情况是明明 OPTIMIZE 跑了几个小时表文件还是那么大这时候要警惕 innodb_file_per_table 的配置了。在 Zabbix 3.0.10 那个年代很多安装文档为了方便用共享表空间的方式建库所有 InnoDB 表共用 ibdata1。如果你查出来 innodb_file_per_table 是 OFF那 events、alerts 这些表的数据全在 ibdata1 里OPTIMIZE TABLE 只能整理内部碎片操作系统看不到任何空间释放。这种库想真正瘦身我的建议是直接做逻辑迁移在另一台机器或者本机新建一个实例把 innodb_file_per_table 设为 ON然后用 mysqldump 只导 Zabbix 全库数据过去确认 Zabbix server 连接新库没问题后再切换使用。迁移需要不小的停机窗口所以最好结合一次完整的 Zabbix 升级或机房搬迁来一起做单独为这个操作停服成本有点高。另外提醒一句binlog 也可能让你的“空间没变小”雪上加霜。清理过程中产生的 delete 语句全部写进了 binlogbinlog 保留策略如果是七天不清理这几百 GB 的删除操作会原样躺在 binlog 文件里。清理结束后建议确认好从库追平再执行 PURGE BINARY LOGS BEFORE NOW() - INTERVAL 2 DAY 这样的命令把过期 binlog 清掉。4.4 Housekeeper 和手动清理“打架”Zabbix 3.0.10 自带 Housekeeper默认会按配置清理历史数据和事件。你手动清理完的同一批数据如果 Housekeeper 还在按它自己的老计划在跑就容易出现两边同时操作一张表的情况轻则重复扫描浪费 IO重则锁竞争导致删除进度倒退。我的处理方式是清理前先把 Housekeeper 对事件的清理开关临时关掉或者把保留期参数临时调大等手动清理和 OPTIMIZE 全部结束后再恢复原始配置。这一步虽然很多人觉得多余但实际跑下来可以避免很多莫名其妙的锁等待和 CPU 尖刺。具体操作就是登录 Zabbix 前端Administration - General - Housekeeping把 Events 相关的清理项暂时改成超大的保留天数或者直接停掉 Housekeeper 进程对事件的清理任务等一切尘埃落定再改回来。最后再分享一个实际体会这套清理流程我在 Zabbix 3.0.10 的库上实际跑了三轮从第一轮的战战兢兢、中途翻车到后两轮的轻车熟路、全程无感最大的体会就是清理历史告警数据这种事绝对不能等到磁盘爆了再动手它就是监控系统里典型的“欠账型任务”平时不还利息越滚越高。个人经验是针对 Zabbix 数据库里的 events 和 alerts 表每季度跑一次分批清理每次保留周期 90 天顺便检查一次 Housekeeper 配置和表空间使用情况比等出了事故再救火要踏实得多。如果你现在正面对一库十几亿行的历史告警别着急一把梭按文章里的步骤先备份、再小批量试跑、观察一轮 IO 和锁然后慢慢放量。等这一步走顺了你会觉得这套老掉牙的 3.0.10其实也没那么难伺候。