
运维搞到一定时间十有八九会遇到这么个场景Zabbix监控系统跑了大半年某天突然收到磁盘空间告警登上服务器一看MySQL里zabbix库占了几个T其中history开头的几张表一张比一张肥。群里的同行几乎每周都有人问同样的问题——zabbix的history表怎么就这么能吃空间这到底怎么解决这篇文章我把这个问题完整拆一遍。从磁盘爆满时的紧急止血到分区表和TimescaleDB的长期治理再到采集策略的架构级优化最后附上我踩过坑之后的排查速查表。不管你是刚接手Zabbix的新人还是被history表折腾过几次的老手照着做基本都能把空间问题压下来。1. 先搞清楚history和trends到底存了什么1.1 Zabbix监控数据的存储逻辑很多人一打开zabbix库看到一堆history_xxx、trends_xxx表就发懵。其实Zabbix的监控数据存储逻辑很简单分两条线history系列表保存每一个监控项每次采集到的原始值。比如CPU使用率每30秒采一次那每次采集结果都会往history_uint这类表里写一条记录。这是最原始、最庞大的数据集合。trends系列表系统每小时把原始值聚合成一条记录存最大值、最小值、平均值。所有监控项在历史周期里都会做这个聚合用于绘制长期趋势图。这个地方有一个关键认知history是原始数据量大、增长快trends是聚合数据单条记录小、增长慢。当你发现数据库空间告急先去看history_uint表的大小十有八九就是它把磁盘撑爆的。注意Zabbix 4.0以后trends表默认保留365天history表默认保留29天。但如果你当初部署时随手把保留天数调大了比如history设成90天甚至365天那history表膨胀的速度会非常吓人。1.2 history相关的几张表分别是谁Zabbix不是把所有历史数据塞进一张表而是按监控项的数据类型拆成了好几张这一点在定位问题的时候非常重要表名存储内容体积特点history浮点型监控项的原始值中等取决于浮点型监控项数量history_uint无符号整数型监控项的原始值通常最大CPU、内存、流量等大部分监控项都在这history_str短字符串类型监控项一般较小history_text长文本类型监控项特殊场景才大history_log日志类型监控项日志量大的时候会爆炸trends浮点型聚合趋势数据中等trends_uint整数型聚合趋势数据长期积累后也不小我的经验是绝大多数Zabbix环境里业务监控项以整数型为主所以history_uint和trends_uint占据了绝大部分存储。如果哪天你的监控项里加了文本巡检输出、日志采集那history_str和history_log也可能突然膨胀。定位的时候不要只看一张表要把整个history前缀的表全部列出来对比。1.3 空间膨胀的三大根源结合实际排查经验history表变成磁盘杀手逃不出下面三个原因采集频率过高。这是个很容易被忽视的问题。一个监控项30秒采集一次一天写入2880条记录改成5秒采集一次一天就是17280条数据量直接翻6倍。很多人图监控数据“丝滑”就把采集间隔调得很小完全没有计算过存储成本。保留周期过长。前端默认的History storage period是29天但很多团队为了事后追溯会改成90天甚至半年。保留周期每翻一倍表体积就翻一倍这是倍数关系不是线性关系。监控项数量爆炸式增长。Zabbix的监控项数量会随着接入主机、模板自动发现不断膨胀。一台主机几十个监控项几百台主机就是上万个监控项。尤其是用了低级别自动发现LLD实际监控项数量可能比你想象的多得多。这三大根源往往会叠加。频率高、保留久、数量多三管齐下数据库增长速度和吃磁盘的速度都会让你措手不及。2. 动手清理前先把家底摸清楚2.1 快速定位哪些表占用了大量空间不管你想怎么处理第一步永远是搞清楚现状。我在接手任何一台出问题的Zabbix服务器时第一件事就是执行下面的SQL把zabbix库里所有表的大小按照倒序排列SELECT table_name 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;这条SQL会给你一张清晰的“空间占用排行榜”。正常情况下排在前面的就是history_uint、history、trends_uint、trends这四张表。如果你发现history_log或者history_str排到了前面那说明你的监控项里有一些日志采集、文本类监控项产生了大量数据这种情况处理思路要稍作调整但大方向一致。另外别忽略索引空间。在MySQL里InnoDB表的data_length是数据文件大小index_length是索引文件大小。history表通常有itemid和clock两个核心索引数据量一大索引体积也很可观。所以清理数据之后索引也会跟着缩水但这可能需要重建表才能完全释放具体我在后面说。2.2 计算数据增长速度决定保留周期光知道当前表多大还不够你还需要评估它每天增长多少这样才能倒推出合理的保留周期。举个例子-- 查看history_uint最近7天写入多少条记录 SELECT FROM_UNIXTIME(clock, %Y-%m-%d) AS 日期, COUNT(*) AS 记录数 FROM history_uint WHERE clock UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 7 DAY)) GROUP BY FROM_UNIXTIME(clock, %Y-%m-%d) ORDER BY 日期 DESC;假设结果中某一天写入1亿条记录每条记录大概几十字节那一天的新增空间就在5GB到10GB左右。这时你要算一笔账磁盘剩余100GB按每天5GB增长最多只能撑20天。再结合你实际需要回查多少天的历史数据就能定出保留周期。我遇到过一个典型case某客户保留周期设为90天每天增长约8GB单是history相关表就占掉700多GB。后来把保留周期压到14天同时加了分区表管理半年后整库占用稳定在200GB以内。这个数字变化会直观得多。3. 从“紧急止血”到“长期治理”的完整解决思路3.1 场景模型不同阶段该用哪种方案处理history数据占用问题没有一招鲜吃遍天的办法。我个人把方案分成三层根据紧急程度和实际情况组合使用方案层级适用场景核心手段见效速度紧急止血磁盘快满Zabbix服务都可能挂了手动删除超期数据、优化表立即见效短期缓解清理完又快速回弹调整保留周期、调低采集频率几天内稳定长期治理希望彻底不再为空间发愁分区表、TimescaleDB、采集策略优化数周内完成大部分情况下一套组合拳下来才能解决问题先紧急删除一批老数据保住磁盘空间然后调整保留周期和housekeeper配置最后实施分区表或迁移TimescaleDB让后续的数据清理变成自动化的“删分区”而不是痛苦的“DELETE大量数据”。3.2 紧急止血直接清理过期数据当你磁盘已经告急没有时间优雅地做长期方案那么直接用DELETE语句清理指定日期之前的数据是最立竿见影的。下面以清理7天前的history_uint数据为例-- 先确认要删除的数据量级心理有个数 SELECT COUNT(*) FROM history_uint WHERE clock UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 7 DAY)); -- 正式删除注意数据量大时会锁表尽量在低峰期执行 DELETE FROM history_uint WHERE clock UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 7 DAY));但这里有一个巨大的坑如果你在history_uint表还有几个亿行数据的时候直接执行DELETE轻则锁表导致Zabbix写入阻塞重则把主库拖死。MySQL处理大事务时锁定大量行、写undo log整个库的性能都会受到严重影响。我见过有同行直接把数据库DELETE到卡死最后只能重启MySQL的。所以“紧急止血”也要讲策略先停掉Zabbix Server或者至少停掉数据采集写入避免边删边写。用LIMIT分批删除每批删除几百万条循环多次避免一个超大事务压垮数据库。删除完成后执行OPTIMIZE TABLE重建表和索引让磁盘空间真正释放出来。分批删除的示意SQL可以这么写-- 循环执行下面这条直到影响行数为0 DELETE FROM history_uint WHERE clock UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 7 DAY)) LIMIT 500000;这条语句会一批一批地删除老数据每批5万条对数据库压力小很多。配合sleep间隔执行比如每执行一批休息1秒基本不会影响Zabbix的正常写入。重要提示MySQL的OPTIMIZE TABLE在执行时会重建整个表磁盘需要有足够的临时空间并且表会被锁住。如果表已经占了500GB磁盘只剩50GBOPTIMIZE TABLE大概率会失败。这种极端情况下建议直接评估分区方案或迁移TimescaleDB。3.3 调整Zabbix侧配置从源头控制数据量紧急删完数据之后如果不改配置过几周又会恢复原样。所以第二步就是调整Zabbix的数据保留策略。这个配置在前端页面就能操作Administration → General → Housekeeping关键参数就两个History storage period历史数据保留周期建议按你的实际回查需求设置一般14天到30天足够。Trend storage period趋势数据保留周期趋势数据体积小可以保留时间长一些比如180天到365天。很多人不知道这里的配置是针对所有监控项全局生效的。如果你想让某些监控项单独设更短周期可以在监控项层面覆盖但日常运维中全局配置已经能解决绝大多数问题。另外前端Housekeeping设置里要确认“Housekeeping”的开关是启用的并且没有勾选“Override item history period”导致某些监控项被设置成了无限期保存。我遇到过一次某台主机的某个监控项在模板里单独设置了History为00在Zabbix里代表不存储历史数据但有些监控项被设成了36500天等于永久保存直接把表撑爆了。排查这个问题花了我不少时间。3.4 长期治理分区表方案如果你希望一劳永逸分区表是当前Zabbix MySQL场景下最成熟的方案。分区的思路特别简单一张大表按时间拆成多个小分区比如按天分区每天一个分区。历史数据要清理的时候不再是执行DELETE一条一条删除而是直接把过期的分区整个DROP掉。DROP分区是元数据操作秒级完成和DELETE几千万行数据完全是两个量级。先看你的MySQL版本是否支持分区MySQL 5.7都支持然后用下面的方式改造history_uint表-- 对history_uint表启用按天分区 ALTER TABLE history_uint PARTITION BY RANGE (UNIX_TIMESTAMP(FROM_UNIXTIME(clock))) ( PARTITION p20240101 VALUES LESS THAN (UNIX_TIMESTAMP(2024-01-02 00:00:00)), PARTITION p20240102 VALUES LESS THAN (UNIX_TIMESTAMP(2024-01-03 00:00:00)), PARTITION p20240103 VALUES LESS THAN (UNIX_TIMESTAMP(2024-01-04 00:00:00)) );不过手工写分区语句太痛苦实际操作中一般都配合定时脚本每周自动创建未来一周的分区并且删除一个月前的分区。社区里有一个很出名的脚本叫zabbix-mysql-partitioning可以直接拿过来用它支持按小时、按天、按月分区也支持配置保留多少个分区。脚本的核心逻辑分三块定期为history*和trends*表创建新的未来分区定期删除过期分区维护一个分区的“保留清单”避免一次删过头。用cron定时跑# 每天凌晨1点执行分区维护脚本 0 1 * * * /opt/scripts/zabbix_partition.sh /dev/null 21这个方案我实测下来的效果是单张10亿行级别的history_uint表分区后每天的数据量几千万行清理老数据从以前的小时级变成秒级。你只需要确保定时脚本稳定运行磁盘占用就不会再无限膨胀。3.5 长期治理升级版PostgreSQL TimescaleDB如果你的Zabbix版本是5.0以上并且愿意迁移到PostgreSQL那TimescaleDB是当前体验最好的长期方案。Zabbix官方从5.0开始对TimescaleDB做了很好的适配6.0以后的版本集成度更高。TimescaleDB的核心概念是hypertable超表。它把一个大表自动按时间切成很多chunk底层自动管理。你不需要像MySQL那样手动建分区、手动删分区只需要创建好超表后面的按时间清理都由TimescaleDB的drop_chunks策略自动完成。使用TimescaleDB最核心的几步-- 创建TimescaleDB扩展 CREATE EXTENSION IF NOT EXISTS timescaledb; -- 把已有的history_uint表转换为超表 SELECT create_hypertable(history_uint, clock, chunk_time_interval INTERVAL 1 day); -- 添加自动清理策略保留30天数据 SELECT add_retention_policy(history_uint, INTERVAL 30 days);你看看这对比手动分区时你要写脚本创建分区、删除分区TimescaleDB里两行SQL就把自动化保留策略配置完了。而且TimescaleDB的压缩功能还可以把旧数据压缩存放磁盘占用能再降一个量级。我在生产环境里用chunk_time_interval 1 day加上压缩策略单台Zabbix服务器撑住上万监控项毫无压力。提示从Zabbix 6.0开始官方安装文档明确推荐使用TimescaleDB。如果你的环境是全新搭建直接用PostgreSQL TimescaleDB是最省心的路线。如果是存量MySQL环境可以考虑渐进式迁移先把trends库迁移过去再把history迁移过去。3.6 策略级优化有些数据根本不需要进history最后这条很多人会忽略Zabbix里不是所有监控项都必须存历史数据很多监控项你可以选择“只存趋势、不存历史”甚至“采集后立即销毁”。举个例子某个监控项只是为了做告警阈值判断你不关心它的历史曲线那就可以在监控项配置里把History设成0。这样数据采集后只参与告警判断不会写入history表。这个配置会极大减少无效数据。另外Preprocessing预处理功能也能减少存储量。比如一个设备每次返回一个JSON如果你的告警逻辑只需要其中某个字段可以用JSONPath把字段提取出来再存储而不是把整个原始内容写入history_str。这样既能保留有效数据又能让存储占用大幅下降。还有一个实践是调低非关键监控项的采集频率。比如核心业务指标30秒采集一次保留30天普通系统指标5分钟采集一次保留14天网络流量类指标1分钟采集一次保留30天用这个思路去梳理你的监控项模板你会发现采集总量能下降一半以上。数据都少采了表自然就不会膨胀到让数据库报警的程度。4. 一次完整的清理实操复盘4.1 环境现状与问题表现下面我用一个模拟案例把前面说的方案串成一条完整的操作主线方便你直接“抄作业”。假设环境是这样的Zabbix版本6.0 LTS数据库MySQL 8.0数据目录挂在/data分区下监控规模500台主机约2万个监控项症状磁盘使用率97%history_uint表占了670GBZabbix前端开始出现“Zabbix server is not running”的告警监控数据出现断档这个场景特别典型数据库磁盘满了以后Zabbix Server写不进数据库前端就会报server is not running同时告警媒介比如钉钉联动也会失效。处理优先级是先腾出空间保住服务再做长期方案。4.2 第一步紧急清理腾出磁盘空间先确认当前zabbix库里的数据总量SELECT table_name, ROUND((data_length index_length) / 1024 / 1024, 2) AS size_mb FROM information_schema.tables WHERE table_schema zabbix ORDER BY data_length index_length DESC LIMIT 10;结果排名第一的是history_uint670GB第二是history180GB。两块加起来快850GB不处理几天内磁盘就会写满。果断执行分批删除目标是把history相关的数据保留周期压缩到7天。由于history表里的数据还在持续写入我先把Zabbix Server短暂停掉避免边删边写导致更多锁表问题。systemctl stop zabbix-server然后用循环分批DELETE-- 分批删除10天前的history_uint数据 DELETE FROM history_uint WHERE clock UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 10 DAY)) LIMIT 500000;反复执行这条语句直到影响行数为0。大概执行了40多批总共删掉约2亿行。然后对history、history_uint、history_str、history_log都执行同样的清理。清理完毕后启动Zabbix Serversystemctl start zabbix-server4.3 第二步OPTIMIZE表释放磁盘空间数据删掉之后InnoDB的表文件不会自动缩小磁盘空闲空间并没有真正释放出来。必须对表执行OPTIMIZE TABLE把表和索引重建一遍让数据文件缩回到当前数据量对应的大小。OPTIMIZE TABLE history_uint; OPTIMIZE TABLE history; OPTIMIZE TABLE trends_uint; OPTIMIZE TABLE trends;这一步执行时间取决于表大小。670GB的表优化可能要跑1个多小时。最好在低峰期执行并且要看着磁盘剩余空间确保有足够空间完成临时表创建。优化完之后再看磁盘空间你会发现数据文件明显缩水原来的850GB降到了160GB左右磁盘使用率回落到60%以下前端也恢复了正常。4.4 第三步配置层面防止快速回弹临时清理只能救急接下来马上修改前端Housekeeping配置Administration → General → HousekeepingHistory storage period改成14dTrend storage period改成180d同时把Zabbix Server的housekeeper频率参数确认一下配置文件zabbix_server.conf里的HousekeepingFrequency默认是1小时也就是每1小时执行一次历史数据清理。如果发现history表里老数据删除缓慢可以把频率调高HousekeepingFrequency14.5 第四步落地分区方案为了以后不再手动删数据我在这套环境里实施了MySQL分区方案。考虑到数据量我选择按月分区每个月的分区包含一个月的数据并配置脚本自动创建下个月分区、删除6个月前的分区。脚本设计核心逻辑大致如下#!/bin/bash # zabbix_monthly_partition.sh # 为history_uint创建下个月分区删除6个月前的分区 DB_USERzabbix DB_PASSpassword DB_NAMEzabbix # 创建下个月分区 NEXT_MONTH$(date -d 1 month %Y%m) NEXT_MONTH_START$(date -d 1 month %Y-%m-01) NEXT_MONTH_END$(date -d 2 months %Y-%m-01) # 计算UNIX时间戳 START_TS$(date -d $NEXT_MONTH_START %s) END_TS$(date -d $NEXT_MONTH_END %s) mysql -u$DB_USER -p$DB_PASS $DB_NAME -e ALTER TABLE history_uint ADD PARTITION ( PARTITION p$NEXT_MONTH VALUES LESS THAN ($END_TS) ); 删除旧分区的操作也放进脚本通过cron每月1号执行。注意分区表里的历史数据是分块存在的删除分区时如果该表还有其他保留策略会按分区的键进行判断。脚本的思路就是用时间键来判断是否过期。这个方案上线后我再也没有为history表手动跑过DELETE。后来我把这套环境迁移到了TimescaleDB清理策略更是完全自动化磁盘空间长期稳定。4.6 第五步验证监控数据完整性清理和优化配置之后一定要到前端页面验证一下数据看板和告警是否正常查看最新数据是否持续更新最新数据页面能看到每个主机的采集时间查看图形页面确认历史曲线有数据、趋势图也能画出来查看队列页面确认没有大量数据等待被处理发送一条测试告警确认告警通道正常如果图形页面点开是白的先检查housekeeper是否把数据删过头了再检查监控项的History存储时间设置。很多时候不是因为清理导致空白而是因为采集停了太久或者存储设置本来就是0。5. 常见问题与排查技巧实录5.1 数据清理了磁盘空间为啥没释放这是问得最多的问题。原因就是InnoDB删除数据只是打标记物理文件不会缩小。解决方案就是我前面说的OPTIMIZE TABLE。如果表非常大你可以考虑ALTER TABLE ... ENGINEInnoDB来重建表效果类似但要评估锁表影响。还有一个办法是从源头杜绝碎片直接用分区表。每天/每月一个分区过期分区直接DROP PARTITION文件收缩立竿见影根本不需要OPTIMIZE TABLE。5.2 DELETE删数据导致数据库卡死我见过不止一次有人直接在几亿行的表上执行全量DELETE然后把数据库锁死了。解决办法停掉Zabbix Server再清理分批次LIMIT删除尽量删除“时间更早”的数据减少对最新写入的影响如果用的是ClickHouse/TimescaleDB这类带分区特性的存储更推荐用DROP分区而不是DELETE5.3 housekeeper为什么不删历史数据Housekeeper是Zabbix里的一个内部进程负责定期清理过期数据。如果你的history表一直不瘦身排查方向前端Housekeeping配置是否启用了对应的History/Trends清理开关zabbix_server.conf里HousekeepingFrequency是否为00代表禁用数据库里大多数历史数据还没到保留期限监控项数量太大housekeeper每轮清理量有限跟不上新增速度。如果是因为容量太大建议把保留周期调短并且用分区表辅助清理而不是单纯依赖housekeeper。5.4 清理后磁盘依然告警根因在binlog或备份文件有时候你清理了history表磁盘空间还是不够。这种情况不要把注意力全放在表数据上还要检查MySQL binlog日志有没有设置自动过期expire_logs_days或binlog_expire_logs_secondsZabbix前端有没有定期生成备份文件备份文件是否存放在数据盘慢查询日志、general log是否开启且体积膨胀。我在生产环境里就遇到过history表只占300GB但是binlog累计了500GB直接把磁盘干爆了。所以排查一定要全面别只盯着数据库表。5.5 Zabbix Server状态异常前端报“Zabbix server is not running”很多情况下这个报错不是Zabbix服务本身挂了而是数据库写入失败导致的连锁反应。尤其是磁盘空间不足、数据库连接池耗尽、慢查询堆积时Zabbix Server虽然进程活着但写不进数据前端就会报这个错。如果你在清理完数据后还看到这个提示重点检查MySQL连接数是否被打满Zabbix Server日志里有没有数据库连接失败的报错比如Access denied for user ...磁盘空间是否真的释放成功。5.6 清理后部分图形数据为空有一种情况是你把历史保留周期改成7天趋势保留周期改成180天然后发现有些图形7天之前的数据查不出来了。这是正常的因为历史数据已经被清掉曲线只能显示趋势数据聚合值。如果是业务需要可以单独给核心监控项设置更长的历史保留时间。写在最后我的几点实操体会说实话Zabbix的history表膨胀并不是一个“删一次就完事”的问题它本质上暴露的是监控系统设计阶段的存储规划缺陷。我个人的体会是处理这类问题一定要遵循三个原则第一先保服务再治数据。磁盘满的时候优先停掉采集、释放空间让Zabbix恢复正常运行再做长期方案不要一上来就想着优化架构。第二数据保留时间不是越长越好。很多团队不敢删历史数据怕查不到记录。实际上Zabbix有trends作为长期趋势数据的兜底历史原始数据保留两周到一个月已经足够排查问题。如果你的业务确实需要长期保存原始数据应该用归档方案而不是让它无限膨胀在在线数据库里。第三能自动化的不要手工做。手动DELETE只是应急最终一定要落到分区表或TimescaleDB上让数据清理变成自动化的周期任务。否则你每隔一两个月就要半夜起来删数据日子真的没法过。如果你用的是MySQL环境我建议今天就去看一眼zabbix库的表大小和housekeeper配置。如果已经出现了较大占用尽快规划分区方案别等磁盘告警了再着急。如果你还没有部署Zabbix或者在规划新环境直接上PostgreSQL TimescaleDB你会省下后面很多麻烦。