简介:资源为Zabbix 7.0 LTS部署与数据库分区优化操作记录,面向MySQL/MariaDB环境下的Zabbix运维人员,核心解决历史表与趋势表数据膨胀、后台housekeeper清理压力过大导致的性能下降问题。文档以社区分区脚本zbx_db_partitiong.sql为主线,完整覆盖下载解压脚本、按需调整历史/趋势保留天数、执行SQL创建分区存储过程,以及通过MySQL事件调度器或Crontab实现每日自动分区维护等环节,并补充了Zabbix前端清理保留天数的配套设置,采用分区后删除旧数据比传统DELETE语句更轻量。整份资源共1个PDF文件,大小约835KB,内容紧凑,既有命令行也有配置步骤,附有大型数据库执行耗时与备份建议,可帮助中高级运维人员在不影响正常监控的前提下完成分区改造。目前已有354人学习,适合Zabbix 3.0及以上各版本环境,能有效提升数据库管理效率、降低维护成本并保障监控系统长期稳定运行。
1. Zabbix 7.0 数据库分区:为什么你的 housekeeper 一忙就是 75%
部署完 Zabbix 7.0 LTS,跑上两三个月,最早出现的告警不是主机挂掉,而是 "Zabbix housekeeper processes more than 75% busy"。这个告警的意思很直白:数据库里积压了太多待删除的旧数据,内务进程每次用 DELETE 清理都像是在搬一座山。Zabbix 的 history 表存的是每个监控项采集的原始值,trends 表存的是小时级聚合数据,这两类表是典型的时序型写入,只增不减,日积月累之后 DELETE 的代价会越来越高,甚至拖慢整个数据库的写入性能。
数据库分区就是针对这个场景的解法:按小时或按天把表切成独立的物理分区,过期数据直接 DROP PARTITION,而不是逐行 DELETE。DROP 是元数据操作,秒级完成,与数据量无关。这套思路在 Zabbix 3.0 之后的所有版本都适用,7.0 LTS 当然也在其中。本文就用一套分区脚本,带你从下载、建存储过程、配置自动调度,到把前端内务配置和分区天数对齐,完整走一遍,并把我在生产环境里踩过的坑写清楚。
2. 分区脚本拆解:zbx_db_partitiong.sql 的四个存储过程与关键参数
2.1 脚本里到底有什么:partition_create 与 partition_drop
这套分区方案的核心是四个存储过程:partition_create、partition_drop、partition_maintenance、partition_verify。先看前两个,它们负责最底层的“建分区”和“删分区”动作。
partition_create的逻辑是:先查information_schema.partitions,确认目标表当前最大的分区边界(partition_description)是否已经覆盖了要新建的时间点,如果还没有,就用ALTER TABLE ... ADD PARTITION补一个新的。它的关键点是分区名要合法,MySQL 分区名不能以数字开头,所以脚本里统一用p前缀拼时间戳。而partition_drop则用一个游标遍历所有早于保留期限的分区,拼成一条ALTER TABLE ... DROP PARTITION p1,p2,p3语句一次性执行。
-- 核心:partition_drop 用游标收集过期分区,拼成一条 DROP 语句 DECLARE myCursor CURSOR FOR SELECT partition_name FROM information_schema.partitions WHERE table_schema = SCHEMANAME AND table_name = TABLENAME AND CAST(SUBSTRING(partition_name FROM 2) AS UNSIGNED) < DELETE_BELOW_PARTITION_DATE; SET @alter_header = CONCAT("ALTER TABLE ", SCHEMANAME, ".", TABLENAME, " DROP PARTITION "); SET @drop_partitions = ""; -- 循环拼接分区名,最后统一执行 DROP,避免逐分区 DROP 带来的多次锁表这里有个值得注意的细节:partition_drop不是拿到一个过期分区就马上 DROP,而是把所有过期分区名拼接成逗号分隔的列表,最后一次性执行。这么做的好处是减少ALTER TABLE的触发次数——每次 DROP PARTITION 都会对表加元数据锁,合并成一条语句能把锁表时间压缩到最低。在大表上,这个差异可能就是分钟级和秒级的区别。
2.2 partition_maintenance:调度入口与天数映射
partition_maintenance是整个过程的总入口,它接收四个参数:SCHEMA_NAME(库名)、TABLE_NAME(表名)、KEEP_DATA_DAYS(保留数据的天数)、HOURLY_INTERVAL(分区的时间跨度,单位是小时)。脚本默认配置里,对 history 系列表调用时KEEP_DATA_DAYS=7、HOURLY_INTERVAL=24,对 trends 系列表则是KEEP_DATA_DAYS=365、HOURLY_INTERVAL=24。也就是说,默认按天分区,历史数据保留 7 天,趋势数据保留 365 天。
-- partition_maintenance 的核心逻辑:先补未来分区,再删过期分区 CALL partition_verify(SCHEMA_NAME, TABLE_NAME, HOURLY_INTERVAL); SET CUR_TIME = UNIX_TIMESTAMP(DATE_FORMAT(NOW(), '%Y-%m-%d 00:00:00')); SET @__interval = 1; create_loop: LOOP IF @__interval > CREATE_NEXT_INTERVALS THEN LEAVE create_loop; END IF; SET LESS_THAN_TIMESTAMP = CUR_TIME + (HOURLY_INTERVAL * @__interval * 3600); SET PARTITION_NAME = FROM_UNIXTIME(CUR_TIME + HOURLY_INTERVAL * (@__interval - 1) * 3600, 'p%Y%m%d%H00'); CALL partition_create(SCHEMA_NAME, TABLE_NAME, PARTITION_NAME, LESS_THAN_TIMESTAMP); SET @__interval = @__interval + 1; END LOOP; SET OLDER_THAN_PARTITION_DATE = DATE_FORMAT(DATE_SUB(NOW(), INTERVAL KEEP_DATA_DAYS DAY), '%Y%m%d0000'); CALL partition_drop(SCHEMA_NAME, TABLE_NAME, OLDER_THAN_PARTITION_DATE);逻辑拆开看就三件事:先调用partition_verify检查表是否已经做了分区(如果没分区,先补一个基础分区);然后从当天零点开始,按HOURLY_INTERVAL的步长预创建CREATE_NEXT_INTERVALS个未来分区;最后把早于KEEP_DATA_DAYS的分区全部删掉。
CUR_TIME取的是当天零点的时间戳,这个设计保证了分区边界永远是整点对齐的。HOURLY_INTERVAL=24意味着每个分区覆盖一天的数据,分区名格式是p202506170000,一眼就能看出这个分区管的是哪一天。预创建 3 个未来分区(CREATE_NEXT_INTERVALS=3)是为了给“今天”留缓冲,防止跨天瞬间出现没有可用分区的间隙——如果恰好在那几秒写入数据,会直接报Table has no partition for value错误。
2.3 四个过程的分工:verify 是容易被忽略的兜底
partition_verify干的事情比较特殊:它检查目标表是否已经处于分区状态。如果一张表从来没分过区,information_schema.partitions里会有一条PARTITION_NAME IS NULL的记录,这时候 verify 会先把整张表改造成 RANGE 分区,并建第一个分区。对新装 Zabbix 来说历史表是空的,这一步秒过;但对跑了很久的大库,首次执行ALTER TABLE ... PARTITION BY RANGE会重建整张表,耗时可能长达数小时,这也是教程里强调“大数据库上可能持续数小时”的原因。
这四个过程的调用关系是:partition_maintenance_all作为最外层入口,依次对history、history_log、history_str、history_text、history_uint、trends、trends_uint七张表分别调用partition_maintenance。如果你用的是 Zabbix 5.0 以上版本,注意history系列表的结构里多了ns字段,但这不影响分区脚本,分区只看clock字段(int 类型,存的是 Unix 时间戳),这也是分区键选clock而不是date的原因——int 比较比日期字符串快,且VALUES LESS THAN直接吃数字。
3. 从脚本到生产:Event Scheduler 与 Crontab 两条调度路线
3.1 先建存储过程:mysql 重定向导入脚本
拿到zbx_db_partitiong.sql之后,第一步是把它导入数据库。常见做法是用 shell 重定向直接喂给 mysql 客户端,语法是mysql -u'<db_username>' -p'<db_password>' <db_name> < zbx_db_partitiong.sql。这里注意用户名和密码不要留空格,否则某些版本会解析出错。
# 导入分区脚本,创建存储过程 mysql -u'zabbix' -p'zabbixDBpass' zabbix < zbx_db_partitiong.sql # 验证四个存储过程是否创建成功 mysql -u'zabbix' -p'zabbixDBpass' zabbix -e "SHOW PROCEDURE STATUS WHERE Db='zabbix'\G"导入之后,用SHOW PROCEDURE STATUS确认四个过程都在,输出里应该能看到partition_create、partition_drop、partition_maintenance、partition_verify四条记录。如果一条都没有,大概率是导入时语法报错被静默忽略了,建议加上--force重新导入并观察报错。脚本里的DELIMITER $$是必须的,存储过程体内部用分号分隔语句,如果不改分隔符,mysql 客户端会在第一个分号处截断,导致整个 CREATE PROCEDURE 失败。
3.2 调度方案一:MySQL Event Scheduler(推荐)
存储过程建好只是第一步,它不会自己跑。要让它按天执行,推荐做法是启用 MySQL 的 Event Scheduler。这个调度器默认是关闭的,需要在配置文件[mysqld]段下加一行event_scheduler = ON,然后重启 MySQL。配置文件的位置因系统而异,Debian 系一般在/etc/mysql/mariadb.conf.d/或/etc/my.cnf.d/下,Red Hat 系在/etc/my.cnf或/etc/my.cnf.d/下。不确定的话用grep -irl "\[mysqld\]" /etc搜一下。
# 检查 event_scheduler 是否已开启 mysql -u'zabbix' -p'zabbixDBpass' zabbix -e "SHOW VARIABLES LIKE 'event_scheduler';" # 期望输出 Variable_name=event_scheduler, Value=ON # 创建事件,每12小时调用一次分区存储过程 mysql -u'zabbix' -p'zabbixDBpass' zabbix -e " CREATE EVENT zbx_partitioning ON SCHEDULE EVERY 12 HOUR DO CALL partition_maintenance_all('zabbix');"EVERY 12 HOUR的意思是每 12 小时跑一次,一天两次。为什么要跑两次?因为分区是按天建的,如果调度任务在跨天时正好宕机或数据库重启,12 小时的间隔能保证在一个周期内把错过的分区补上。对于 7 天历史保留的配置来说,分区的创建和删除都是幂等操作——partition_create会先查重,已存在的分区不会重复建,所以多跑几次没有副作用。
事件创建之后,可以用SELECT * FROM INFORMATION_SCHEMA.events\G查看LAST_EXECUTED字段,确认事件是否真的被执行过。如果STATUS是ENABLED但LAST_EXECUTED一直是 NULL,说明调度器没跑起来,回到配置文件检查event_scheduler是否生效。
3.3 调度方案二:Crontab(备选)
如果 MySQL 事件调度器因为权限或策略原因用不了,Crontab 是标准的备选方案。思路是每天凌晨固定时间调用一次存储过程,并把输出重定向到日志文件。
# 编辑 crontab sudo crontab -e # 每天凌晨 03:30 执行分区维护,日志写入 /tmp 30 03 * * * /usr/bin/mysql -u'zabbix' -p'zabbixDBpass' zabbix -e "CALL partition_maintenance_all('zabbix');" > /tmp/CronDBpartitiong.log 2>&1这里两个细节值得注意。第一,mysql 路径建议写绝对路径,用which mysql查一下,有些系统上 crontab 的 PATH 环境变量不包含/usr/bin,不写绝对路径会导致作业静默失败。第二,日志重定向一定要加,分区失败时/tmp/CronDBpartitiong.log里会有明确报错,排查效率高很多。Crontab 的缺点是粒度只能到分钟级,如果当天 03:30 数据库恰好在高负载,这个任务可能会拖慢 Zabbix 的采集写入,而 Event Scheduler 是 MySQL 内部调度,可以设置ON SCHEDULE避开高峰期,这是我更推荐 Event Scheduler 的原因。
3.4 手动跑一次验证:观察分区创建输出
无论选哪种调度方式,配置完都应该手动执行一次,验证存储过程真的能跑通。直接在命令行调用partition_maintenance_all,输出会逐条打印创建分区的消息:
mysql -u'zabbix' -p'zabbixDBpass' zabbix -e "CALL partition_maintenance_all('zabbix');" # 输出示例: # +-----------------------------------------------------------+ # | msg | # +-----------------------------------------------------------+ # | partition_create(zabbix,history,p202506170000,1750089600) | # +-----------------------------------------------------------+ # 查看 history 表当前的分区结构 mysql -u'zabbix' -p'zabbixDBpass' zabbix -e "SHOW CREATE TABLE history\G"SHOW CREATE TABLE的输出里,表定义末尾会带着PARTITION BY RANGE (clock)和一系列PARTITION p202506170000 VALUES LESS THAN (1750089600)的定义。看到这些就说明分区已经生效。如果输出里只有partition_create消息但SHOW CREATE TABLE里没有分区定义,说明调用的是空跑——检查一下传入的库名和表名是否匹配,以及partition_verify是否真的执行了首次分区改造。
4. Zabbix 前端内务配置:天数对齐的三个细节
4.1 关闭内务自动处理,避免双重删除
数据库分区之后,Zabbix 前端的内务管理(Housekeeping)配置必须同步调整,否则会出现“数据库层 DROP 分区删数据,应用层 DELETE 也删数据”的双重删除,不仅浪费性能,还可能因为 DELETE 与 DROP 的竞争导致锁等待。正确做法是在“管理 → 一般 → 管家”页面里,把“开启内部管家”的勾选去掉。这一步的意思是:数据清理交给数据库分区去做,Zabbix 进程不再对 history 和 trends 表执行 DELETE。
前端界面里“历史记录和趋势”部分有几个关键项:开启内部管家(去掉勾选)、覆盖监控项趋势期间(勾选上)、数据存储期(填写天数)。其中“覆盖监控项趋势期间”这个选项容易被忽略,它的作用是让全局内务策略覆盖监控项级别的单独设置。如果不勾选,个别监控项如果设置了不同的趋势保留期,会出现前端配置与数据库分区天数不一致的情况。
4.2 天数必须与分区脚本一致,否则数据被提前删
前端“数据存储期”填的天数必须和partition_maintenance里的KEEP_DATA_DAYS完全一致。默认情况下,脚本里历史保留 7 天、趋势保留 365 天,前端也要填 7 和 365。如果前端填了 30 天而数据库分区只保留 7 天,那么第 8 天开始数据库就会把历史分区 DROP 掉,前端的图最多只能回溯 7 天,查 30 天前的数据全是空的。反过来的情况更隐蔽:前端填 7 天而数据库保留 30 天,Zabbix 内务进程因为前端配置还会以 DELETE 方式清理 7 天前的数据,分区脚本又把旧数据多留了 23 天——两边都在删,但删的边界不同,最终结果以更激进的一方为准。
| 配置项 | 数据库分区脚本 | Zabbix 前端内务 | 一致性要求 |
|---|---|---|---|
| 历史数据保留 | KEEP_DATA_DAYS(默认7) | 数据存储期-历史记录 | 必须相等 |
| 趋势数据保留 | KEEP_DATA_DAYS(默认365) | 数据存储期-趋势 | 必须相等 |
| 清理方式 | DROP PARTITION | DELETE | 只能选其一,推荐分区 |
| 执行频率 | 每天/每12小时 | 每小时 | 分区更高效 |
4.3 大表首次分区的等待时间与业务影响
如果 Zabbix 已经跑了几个月,history 表里可能积了几个 GB 甚至几十 GB 的数据。首次执行PARTITION BY RANGE时,MySQL 会重建整张表并锁住写入,这个窗口期 Zabbix 的采集数据会全部积压在内存队列里。如果是新装系统,history 表是空的,整个过程秒级完成。生产环境建议在维护窗口执行,或者提前估算一下:数据量 10GB 左右的表,机械盘上重建可能需要 20 到 40 分钟,SSD 上会快很多。执行期间可以用SHOW PROCESSLIST观察ALTER TABLE的进度,但别中途 kill——重建到一半被杀掉,表可能处于不一致状态。
5. 常见问题与排查:分区脚本运行中的六个坑
5.1 坑一:event_scheduler 配置后不生效
现象:SHOW VARIABLES LIKE 'event_scheduler'返回Value=OFF,事件创建成功但LAST_EXECUTED始终为 NULL。
原因:配置文件加在了错误的段里,或者 MySQL 没有重启。有些版本的 MySQL 支持运行时动态设置SET GLOBAL event_scheduler = ON,但这个设置在重启后会失效,不能替代配置文件。
解决:确认[mysqld]段的位置,很多系统还有[mariadb]段,加错段不生效。改完必须systemctl restart mysql,启动后再次确认变量值。
5.2 坑二:[Z3005] Table has no partition for value
现象:Zabbix 日志里出现[Z3005] query failed: [1526] Table has no partition for value ...,前端出现断图。
原因:分区脚本创建的未来分区不够用。默认CREATE_NEXT_INTERVALS=3,如果调度任务停了超过 3 天,或者 Event Scheduler 被意外关闭,当前时间点已经超出了最后一个分区的VALUES LESS THAN边界,任何 INSERT 都会直接报错。
解决:立即手动执行一次CALL partition_maintenance_all('zabbix'),补上缺失的分区。这一步能恢复写入。然后检查调度任务为什么停了——常见原因包括 MySQL 重启后event_scheduler配置丢失、Crontab 的 mysql 路径不对、或者磁盘满导致事件执行失败。
5.3 坑三:分区后 history_uint 表数据没了
现象:分区跑完,history_uint 表数据被清空,或者某一天的数据整段丢失。
原因:partition_drop的判断条件是partition_name的时间戳字符串小于DELETE_BELOW_PARTITION_DATE。如果分区名格式不对,比如时间戳位数不对或者带了下划线,SUBSTRING截取出来的数字会偏大或偏小,导致过期判断失真。另一个常见原因是调整KEEP_DATA_DAYS时前端天数没同步改,数据库层误删了还在保留期内的数据。
解决:修改脚本参数后,先手动调用存储过程观察输出,确认删除的分区列表符合预期。用SELECT partition_name, partition_description FROM information_schema.partitions WHERE table_name='history_uint' ORDER BY partition_description检查分区边界是否连续。
5.4 坑四:ALTER TABLE 锁表导致 Zabbix 采集中断
现象:分区执行期间,Zabbix 前端出现大面积超时,数据库线程堆积。
原因:ADD PARTITION虽然只改元数据,但在 MySQL 5.6 之前的版本仍然会触发表级锁。即使 5.7+ 做了优化,首次PARTITION BY RANGE重建表依然需要锁。另一个隐蔽因素:如果同一时间partition_drop和partition_create都在对同一张表操作,两个 ALTER 会互相等待。
解决:尽量把调度时间放在低峰期。Event Scheduler 可以按固定时间执行,Crontab 就选凌晨。如果大表必须在线分区,考虑用pt-online-schema-change这类工具,但要确认它支持分区操作,且 Zabbix 的写入链接能接受短暂中断。
5.5 坑五:分区脚本导入时报语法错误
现象:执行mysql < zbx_db_partitiong.sql时终端报ERROR 1064语法错误。
原因:脚本文件编码问题,或者下载时格式被 Windows 换行符污染。直接 wget 的脚本一般没问题,但从 Windows 机器传到服务器的脚本,\r\n可能导致 MySQL 解析存储过程体时出错。
解决:用dos2unix zbx_db_partitiong.sql转换换行符,再用编辑器确认文件头没有 BOM。导入时加--force可以看到所有错误而不是在第一条就停住。导入完成务必查SHOW PROCEDURE STATUS,漏建任何一个过程都会导致调用链断裂。
5.6 坑六:修改保留天数后旧分区没删干净
现象:从默认 7 天改到 30 天后,历史表里存在超过 30 天的分区,手动查看information_schema.partitions能看到大量旧分区。
原因:partition_drop的逻辑是每次执行只删“当前”超过保留期限的分区。如果旧分区的时间戳格式与脚本预期不符——比如早期手动建过p20240101这种不带时刻的分区名——SUBSTRING截取的数字与DELETE_BELOW_PARTITION_DATE格式不匹配,就会被跳过。
解决:手动写一条 DROP PARTITION 语句清理历史遗留分区,但前提是确认这些分区里的数据确实不需要了。之后每次修改KEEP_DATA_DAYS,配合SELECT partition_name FROM information_schema.partitions对比新旧边界,确认删除范围和预期一致再让调度自动跑。
6. 收尾技巧:验证分区是否真的在删表,以及变更天数的安全改法
6.1 用一张 SQL 看清分区生命周期
分区配置完,最需要盯的不是存储过程本身,而是分区表的健康状态。我一般会在数据库里建一个视图,把七张业务表的分区情况拉出来对比看:
SELECT t.table_name, COUNT(p.partition_name) AS partition_count, MIN(p.partition_description) AS oldest_partition_ts, MAX(p.partition_description) AS newest_partition_ts, (MAX(p.partition_description) - MIN(p.partition_description)) / 86400 AS span_days FROM information_schema.tables t LEFT JOIN information_schema.partitions p ON t.table_schema = p.table_schema AND t.table_name = p.table_name WHERE t.table_schema = 'zabbix' AND t.table_name IN ('history','history_uint','history_log','history_str','history_text','trends','trends_uint') GROUP BY t.table_name;跑出来的结果里,span_days大于你设置的保留天数,说明存在过期分区没被删掉;partition_count连续几天不变,说明调度可能停了。把这条 SQL 挂到 Grafana 或者每天 cron 一次输出到日志,比等告警再排查强得多。
6.2 修改保留天数的完整流程,不要只改脚本
如果你觉得默认 7 天历史太短,想改成 30 天,正确流程不是改脚本重新导,而是新建一个存储过程。因为原脚本的partition_maintenance_all已经存在,重复导入同名过程会报错,需要先DROP PROCEDURE再重建,麻烦且容易误删。安全做法是创建新过程,修改KEEP_DATA_DAYS参数:
DELIMITER $$ CREATE PROCEDURE partition_maintenance_all_30and400(SCHEMA_NAME VARCHAR(32)) BEGIN CALL partition_maintenance(SCHEMA_NAME, 'history', 30, 24, 3); CALL partition_maintenance(SCHEMA_NAME, 'history_log', 30, 24, 3); CALL partition_maintenance(SCHEMA_NAME, 'history_str', 30, 24, 3); CALL partition_maintenance(SCHEMA_NAME, 'history_text', 30, 24, 3); CALL partition_maintenance(SCHEMA_NAME, 'history_uint', 30, 24, 3); CALL partition_maintenance(SCHEMA_NAME, 'trends', 400, 24, 3); CALL partition_maintenance(SCHEMA_NAME, 'trends_uint', 400, 24, 3); END$$ DELIMITER ;然后更新事件,把旧的zbx_partitioning事件指向新过程。注意ALTER EVENT只替换调用的过程名,不会清空事件的执行历史:
ALTER EVENT zbx_partitioning ON SCHEDULE EVERY 12 HOUR DO CALL partition_maintenance_all_30and400('zabbix');改完事件之后,务必同步把 Zabbix 前端的“数据存储期”改成同样的天数(历史 30 天、趋势 400 天),并确认“开启内部管家”依然是关闭状态。两个地方一天数不一致,轻则图表少数据,重则丢失整段历史。我习惯每次改动都先在测试库跑一遍,把span_days和前端天数截图对比,再上生产。
从那以后,我每次给 Zabbix 数据库做分区调整,都强制走一遍:先查information_schema.partitions看现状,再改存储过程,再改事件调度,最后改前端内务。四步缺一不可。这套脚本和流程在 Zabbix 7.0 LTS 上跑了大半年,housekeeper 的 75% 告警再也没出现过,希望帮到你。
本文还有配套的精品资源,点击获取