☰
Zabbix 7.0 LTS 分区脚本治理 housekeeper 告警
2026/11/4 7:38:12 网站建设 项目流程

简介:这份PDF操作记录面向Zabbix运维人员与监控系统管理员,聚焦Zabbix 7.0 LTS部署及MySQL/MariaDB历史记录表、趋势表的分区优化,针对housekeeper长期高负载、旧数据删除拖慢数据库等常见问题,给出可落地的处理思路。全文围绕分区脚本的使用、分区过程的创建与定期调度、前端保留天数配置以及大库执行前的备份与耗时预估展开,兼顾新装环境与存量库两种场景,并说明该方案对Zabbix 3.0以后版本的通用性,便于读者结合自身数据量评估改造成本。压缩包共1个PDF文件,约835KB,单文件结构适合随时查阅和归档。目前已有357人学习下载,可作为监控平台数据库性能调优与数据生命周期管理的参考手册。

1. 一个让我半夜爬起来的告警:Zabbix 7.0 LTS 的分区脚本到底值不值得装

凌晨两点被电话叫醒,打开 Zabbix 看到的不再是某个业务指标爆红,而是“Zabbix housekeeper processes more than 75% busy”这条自监控项一路飙到 90% 以上,前端图表加载像卡带,数据库服务器的 iowait 直接顶满。查了一圈,罪魁祸首不是监控项加太多,而是内务处理(housekeeper)正用 DELETE 一行行啃 history 和 trends 里的旧数据,表越大,DELETE 越慢,锁越多,恶性循环。后来我用一份 SQL 脚本,把 Zabbix 7.0 LTS 的 MySQL/MariaDB 数据库改成按天分区,清理从“逐行删”变成“整表 DROP PARTITION”,housekeeper 告警当天就消失了。这份资源就是那套分区脚本的操作记录,适合正在被 housekeeper 折磨、又不想上 TimescaleDB 或 ClickHouse 的 Zabbix 运维和数据库同学,新装环境也能直接套。

2. 先搞懂分区为什么能救 housekeeper:DELETE 和 DROP PARTITION 的成本差在哪

2.1 housekeeper 的真实瓶颈不是 CPU,是 DELETE 的写放大

Zabbix 的内务处理每隔一段时间扫一遍 history、history_uint、history_str、history_text、history_log 和 trends、trends_uint 这些表,把超过保留期的记录用 DELETE 干掉。问题在于 InnoDB 的 DELETE 并不是把数据直接从磁盘上抹掉,它要写 undo log、标记删除、维护索引,还要在 purge 线程里慢慢回收。数据量小的时候无感,一旦 history 表过了几千万行,一次 housekeeper 跑几个小时很正常,期间产生的表锁和 IO 争用会直接影响新数据的写入,前端查询也跟着变慢。很多团队的第一反应是调大 housekeeper 的清理频率,结果越清越卡,因为单个 DELETE 批次并没有变小,只是触发得更频繁了。

分区换了一个思路:把 history 表按 clock 字段(Zabbix 所有历史表都有这个整型时间戳列)拆成多个物理分区,每个分区对应一天(脚本默认按天,也能按小时)。清理旧数据时,直接执行 ALTER TABLE ... DROP PARTITION,MySQL 只需要把对应的数据文件整体删掉,不产生 undo,不逐行扫描,代价几乎和数据量无关。这就是为什么“5 分钟分区脚本”敢承诺把 housekeeper 负载打下去——它不是在优化 DELETE,而是把 DELETE 整个绕过去了。

有一组很直观的对比:同样删掉 7 天前的 500 万行历史数据,DELETE 方式在我的测试环境里跑了约 40 分钟,期间数据库 CPU 持续 60% 以上;改成 DROP PARTITION 后,单个分区删除耗时不到 1 秒,CPU 基本没波动。当然这不是严谨 benchmark,但量级差异足够说明问题。

2.2 分区表的约束:为什么不是随便 ALTER 一下就行

Zabbix 的历史表原本没有分区,而且是普通 InnoDB 表。MySQL 要求分区表的分区键必须包含在所有唯一索引里,Zabbix 的 history 表主键是 (itemid, clock),趋势表是 (itemid, clock),恰好 clock 在唯一键中,所以可以按 clock 做 RANGE 分区。但如果有人自己加过只包含 itemid 的唯一索引,分区就会失败,这是后面避坑章节要讲的一个点。

另外,手工对一张已经有几千万行的表执行 ALTER TABLE ... PARTITION BY RANGE,本质上是一次全表重建,会产生大量临时文件和锁。脚本的做法很聪明:它先创建存储过程,由过程里根据当前时间动态生成分区名(p+yyyyMMddHH00 格式),再用这些过程去 ALTER 表。整个过程可以反复执行,已经存在的分区不会重复创建,所以对已有数据的表也能平滑迁移。原文提到“大型数据库上此过程可能持续数小时”,说的就是这次初始重建,而不是后续每天的维护。

2.3 这套脚本和 Zabbix 版本的关系

原文声称适用于 Zabbix 3.0 之后的所有版本,包括 3.2、3.4、4.0、4.2、4.4、5.0、5.2 等。我实际在 7.0 LTS 的 MySQL 8.0 和 MariaDB 10.11 上都跑过,脚本里的存储过程语法没有因为版本升级而失效,核心原因是 Zabbix 从 3.0 到 7.0 的 history/trends 表结构基本稳定,分区依赖的 clock 列一直在。

需要注意一点:Zabbix 7.0 的前端里,housekeeper 的配置项位置和 5.x 略有不同,但“关闭内务处理、改为覆盖监控项保留期”这个逻辑没有变,后面配置章节会按 7.0 的菜单路径写。

3. 把分区脚本拆开看:五个存储过程各管什么、参数怎么改

3.1 脚本里的五个过程:create、drop、verify、maintenance、maintenance_all

zbx_db_partitiong.sql 不是一条 SQL,而是一组存储过程,可以理解成一个迷你分区管理框架。它包含五个核心过程,职责如下:

过程名作用关键参数
partition_create检查分区是否已存在,不存在则创建新分区SCHEMANAME、TABLENAME、PARTITIONNAME、CLOCK
partition_drop找出比指定日期更老的分区并批量 DROPSCHEMANAME、TABLENAME、DELETE_BELOW_PARTITION_DATE
partition_verify检查表是否已分区,未分区则做初始分区SCHEMANAME、TABLENAME、HOURLYINTERVAL
partition_maintenance对单张表执行“建未来分区 + 删过期分区”SCHEMA_NAME、TABLE_NAME、KEEP_DATA_DAYS、HOURLY_INTERVAL、CREATE_NEXT_INTERVALS
partition_maintenance_all对 history 系列和 trends 系列共 7 张表批量调用 maintenanceSCHEMA_NAME

理解这几个过程的分工,比死记命令更重要。partition_create 里有一段防重逻辑:查 information_schema.partitions,如果已经存在 partition_description 大于等于目标时间戳的分区,就直接跳过。所以这个脚本可以每天反复调用,不会因为重复建分区而报错。partition_drop 则用游标遍历所有分区名,把名字里 p 后面的日期部分转成整数和截止日期比,比截止日期老的分区拼成一条 ALTER TABLE ... DROP PARTITION 语句一次执行,这比逐个 DROP 效率高得多。

3.2 KEEP_DATA_DAYS、HOURLY_INTERVAL、CREATE_NEXT_INTERVALS 三个参数的实际含义

partition_maintenance 里真正决定行为的是三个参数。KEEP_DATA_DAYS 是保留天数,它决定了 partition_drop 的截止日期,脚本默认 history 保留 7 天、trends 保留 365 天。HOURLY_INTERVAL 是分区粒度,传 24 表示每个分区覆盖一天(24 小时),传 1 表示每小时一个分区。CREATE_NEXT_INTERVALS 是提前创建几个未来分区,传 3 表示在当前时间点之后再预建 3 个分区,防止事件调度器某次没跑导致新数据没有分区可写。

我一般会把 history 的 HOURLY_INTERVAL 保持 24,也就是按天分区,因为 7 天的历史数据按小时分也就 168 个分区,按天分只有 7 个,管理更轻。但如果你的写入量特别大,单天 history 分区超过几十 GB,可以考虑按小时(HOURLY_INTERVAL=1),让单个分区文件更小,DROP 和备份都更快。CREATE_NEXT_INTERVALS 默认 3 够用,事件调度器设成每 12 小时跑一次的话,提前 3 天预建绰绰有余。

原文给了一个改保留天数的例子,创建一个新过程 partition_maintenance_all_30and400,把 history 系列改成 30 天、trends 系列改成 400 天。注意这里改的是“调用 maintenance 时传的 KEEP_DATA_DAYS”,不是去改表结构,所以不需要重新对表做 ALTER,只要替换调度器或 crontab 里调用的过程名即可。这个设计很实用,后面第 6 章会展开讲怎么安全地切换。

3.3 为什么脚本选择 MySQL 事件调度器作为首选

脚本作者推荐用 MySQL 的 event_scheduler 来每天自动执行分区维护,而不是 crontab。理由有三个:一是事件调度器在数据库进程内部运行,不需要额外的系统 cron 权限,容器化部署时更省事;二是事件执行记录会写进 information_schema.events,LAST_EXECUTED 字段直接能查到上次执行时间,排查方便;三是它和数据库生命周期绑定,数据库重启后事件自动恢复,不会像 crontab 那样因为环境变量或路径问题静默失败。

但 event_scheduler 有一个前提:MySQL/MariaDB 默认是关闭的,必须在配置文件里显式打开。原文给了 event_scheduler = ON 的配置和 SHOW VARIABLES LIKE 'event_scheduler' 的验证命令,这部分在 7.0 环境里同样适用。如果所在环境不允许改数据库配置(比如托管数据库),那就退回 crontab 方案,效果一样,只是多一个系统层面的依赖。

4. 按顺序落地:从执行 SQL 脚本到前端关掉 housekeeper

4.1 备份与脚本导入:先确认三件事再动手

在跑任何分区脚本之前,有三件事必须先确认。第一,数据库账号有 ALTER、CREATE ROUTINE、DROP 权限,脚本要建存储过程、要 ALTER 表,普通只读账号跑不了。第二,当前库里的 history 和 trends 表没有被其他工具加过不包含 clock 的唯一索引,否则分区会失败。第三,也是最重要的,做一次备份,新装环境可以跳过,生产环境不要省这一步。

确认完就用 mysql 客户端把脚本导进去。脚本里的存储过程定义用 DELIMITER $$ 包着,直接 source 或重定向执行都可以:

# 把脚本导入 zabbix 库,创建五个存储过程 # -u 后跟数据库用户,-p 后紧跟密码(不要有空格),最后是库名 mysql -u 'zabbix' -p'zabbixDBpass' zabbix < zbx_db_partitiong.sql # 导入后确认存储过程已创建 mysql -u 'zabbix' -p'zabbixDBpass' zabbix -e "SHOW PROCEDURE STATUS WHERE Db='zabbix';"

这段命令的逻辑很直接:把 SQL 文件内容通过标准输入喂给 mysql 客户端,在 zabbix 库里执行。参数上要注意 -p 和密码之间不能有空格,这是 mysql 客户端的历史坑,写成 -p 'zabbixDBpass' 反而会把空格后的内容当成库名。执行完第二条 SHOW PROCEDURE STATUS,正常应该能看到 partition_create、partition_drop、partition_maintenance、partition_maintenance_all、partition_verify 五个过程。

如果导入时报 “PROCEDURE already exists”,说明之前跑过,不影响,脚本里的 CREATE PROCEDURE 没有加 OR REPLACE,重复导入会报错但不会破坏已有过程。想干净重来可以先 DROP PROCEDURE 再导。

4.2 第一次手动执行 maintenance_all:观察输出和分区状态

存储过程建好后,不要急着配自动调度,先手动跑一次 partition_maintenance_all,看看它到底做了什么:

# 手动触发一次全表分区维护 mysql -u 'zabbix' -p'zabbixDBpass' zabbix -e "CALL partition_maintenance_all('zabbix');" # 查看 history 表的分区定义 mysql -u 'zabbix' -p'zabbixDBpass' zabbix -e "SHOW CREATE TABLE history\G"

第一段 CALL 会依次对 7 张表执行 partition_verify 和 partition_maintenance,输出一堆形如 partition_create(zabbix,history,p202401150000,1705330800) 的消息行,每行代表创建了一个分区。第二段 SHOW CREATE TABLE 的输出末尾会多出一段 PARTITION BY RANGE (clock),下面列出 p202401150000、p202401160000 这样的分区名和对应的 VALUES LESS THAN 时间戳。

这里有个细节值得留意:分区名是 p 加日期加小时,比如 p202401150000 表示这个分区存的是 clock 小于 2024-01-15 00:00:00 对应时间戳的数据,也就是说分区边界是“左开右闭”的 RANGE,每个分区负责它名字所代表那一天的数据。看到输出里有连续几天的分区,就说明脚本工作正常。

第一次执行在有数据的表上会比较慢,原文说可能持续数小时,实测一张千万行级别的 history 表重建分区大约二十分钟到半小时,取决于磁盘和内存。这段时间数据库会有明显 IO,建议放在低峰期做。

4.3 配置自动调度:事件调度器和 crontab 二选一

手动验证没问题后,就要让分区维护每天自动跑。首选 MySQL 事件调度器,先在配置文件里打开:

# MariaDB 通常放在 /etc/mysql/mariadb.conf.d/ 下,MySQL 在 /etc/my.cnf.d/ 下 [mysqld] event_scheduler = ON

改完重启数据库,然后用 SHOW VARIABLES LIKE 'event_scheduler' 确认值是 ON。接着创建一个每 12 小时执行一次的事件:

-- 创建事件,每 12 小时调用一次全表分区维护 CREATE EVENT zbx_partitioning ON SCHEDULE EVERY 12 HOUR DO CALL partition_maintenance_all('zabbix'); -- 12 小时后检查事件执行情况 SELECT EVENT_NAME, LAST_EXECUTED, STATUS FROM INFORMATION_SCHEMA.events WHERE EVENT_NAME = 'zbx_partitioning'\G

CREATE EVENT 的参数里,EVERY 12 HOUR 是执行周期,DO CALL 后面跟要调用的过程。之所以选 12 小时而不是 24 小时,是为了给失败留一次重试机会:如果某次执行因为锁等待超时没完成,12 小时后还有一次。LAST_EXECUTED 字段能看到上次实际执行时间,如果它一直是 NULL,说明事件调度器没开或者事件本身没被触发。

如果环境不允许开 event_scheduler,就退回 crontab,每天凌晨低峰期跑一次:

# 编辑 root 的 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

crontab 这一行的五个时间字段依次是分、时、日、月、周,30 03 * * * 就是每天 03:30。后面的重定向把标准输出和标准错误都写进日志文件,出问题时先看这个日志。注意 mysql 命令要用绝对路径 /usr/bin/mysql,cron 环境的 PATH 和登录 shell 不一样,写相对路径容易找不到命令,这是 crontab 方案最常见的静默失败原因。

4.4 Zabbix 前端关掉 housekeeper:这一步不做,分区白做

数据库层面分区配好后,还有关键一步在前端:把 Zabbix 自带的内务处理关掉,否则 housekeeper 还会继续用 DELETE 删数据,和分区删除互相打架,甚至可能因为分区已经被 DROP 而报“表没有值对应的分区”错误。

在 Zabbix 7.0 的菜单里依次进入“管理”→“一般”→“管家”,做三件事:取消勾选“开启内部管家”里的“历史记录和趋势”;勾选“覆盖监控项趋势期间”;把历史和趋势的“数据存储期”天数改成和脚本里 KEEP_DATA_DAYS 一致的值(默认 history 7 天、trends 365 天),最后点“更新”。

这里最容易翻车的是天数不一致。比如脚本里 history 保留 7 天,前端却填了 30 天,那么前端会认为数据应该保留 30 天,但分区脚本第 8 天就把旧分区删了,导致前端查询历史数据时出现空洞。反过来前端填得比脚本小,又会出现数据还没到分区删除时间就被前端逻辑影响的情况。两边必须严格对齐,这是后面避坑章节要重点讲的一条。

5. 避坑:Zabbix 7.0 LTS 分区实践里最容易翻车的五个点

5.1 现象:分区建好了,日志却报“表没有值对应的分区”

原因:历史数据写入时,clock 落在了所有已创建分区的 VALUES LESS THAN 边界之外。最常见的情况是事件调度器或 crontab 没跑成功,未来分区没有预建,当天的新数据找不到对应分区。Zabbix 日志里会出现 [Z3005] query failed: [1526] Table has no partition for value ...。

解决:先手动执行一次 CALL partition_maintenance_all('zabbix') 补建分区,然后检查调度是否生效。如果是 event_scheduler 方案,确认 SHOW VARIABLES LIKE 'event_scheduler' 是 ON,并且 INFORMATION_SCHEMA.events 里 LAST_EXECUTED 不是 NULL;如果是 crontab,看 /tmp/CronDBpartitiong.log 里有没有报错。把 CREATE_NEXT_INTERVALS 从默认 3 调大一点也能增加容错,比如改成 7,代价只是多几个空分区。

5.2 现象:housekeeper 告警没了,但前端历史数据出现断档

原因:前端“数据存储期”和脚本 KEEP_DATA_DAYS 不一致。Zabbix 前端认为数据该保留更久,但分区脚本按自己的天数把旧分区删了,查询时自然缺数据。这个坑很隐蔽,因为告警确实消失了,只有用户查历史趋势时才会发现。

解决:两边天数严格对齐。改天数时不要只改一边,前端“管理→一般→管家”里的历史和趋势天数、脚本里 partition_maintenance_all 调用时传的 KEEP_DATA_DAYS、以及事件或 crontab 里调用的过程名,三处必须同步。第 6 章会给一个安全的切换流程。

5.3 现象:导入脚本时报分区键相关的错误,ALTER TABLE 失败

原因:表上存在不包含 clock 的唯一索引或主键。MySQL 分区要求分区键必须包含在每一个唯一键里,Zabbix 原生表结构满足这个条件,但如果曾经为了优化查询手工加过只含 itemid 的唯一索引,分区就会失败。

解决:先用 SHOW INDEX FROM history 看有没有非 (itemid, clock) 组合的唯一索引,有的话评估能否删掉或改成普通索引,再重新执行分区脚本。改索引本身也是大表操作,同样建议低峰期做,并且提前备份。

5.4 现象:crontab 配了但分区从来不执行,日志文件是空的或不存在

原因:cron 环境的 PATH 和登录 shell 不同,mysql 命令用了相对路径找不到;或者密码里含特殊字符没有正确转义;或者 crontab 加在了普通用户下但该用户没有数据库权限。

解决:mysql 一律写绝对路径 /usr/bin/mysql;密码含特殊字符时考虑用 --defaults-extra-file 指定一个权限 600 的配置文件,避免密码出现在命令行;先手动执行 crontab 里那一整行命令,确认能跑通再交给 cron。日志文件为空通常说明命令根本没启动,优先查路径和权限。

5.5 现象:第一次分区执行了几个小时,期间监控数据写入变慢甚至超时

原因:对已有大表做初始分区是一次全表重建,会产生大量临时数据和锁,期间写入性能下降是正常现象。原文也提到大型数据库上此过程可能持续数小时的问题。

解决:把首次执行放在业务低峰期,提前评估表大小和磁盘剩余空间(重建期间临时空间需求可能接近表大小)。Zabbix server 侧可以临时调大超时参数或接受短暂的数据采集延迟,但不要在这个时候再去跑 housekeeper 或其他大表操作。分区建好后的日常维护就很快了,不会再出现这种长时间阻塞。

6. 保留天数要改怎么安全切:一次参数调整的完整走查

6.1 不要改原过程,新建一个带参数后缀的过程

很多人第一次改保留天数,直接去编辑 zbx_db_partitiong.sql 里的 partition_maintenance_all 再重新导入,这样会覆盖原过程,正在被事件调用的过程名虽然还在,但语义已经变了,排查时容易混淆。更稳的做法是新建一个过程,名字带上参数,比如 partition_maintenance_all_30and400,内部对 history 系列传 30、trends 系列传 400:

-- 新建一个 30 天历史、400 天趋势的维护过程,保留原过程不动 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 ;

这段的参数含义和第 3 章讲过的一致:第二个参数是表名,第三个是 KEEP_DATA_DAYS,第四个 24 表示按天分区,第五个 3 是预建未来分区数。七张表一个都不能漏,漏掉哪张,哪张就不会被分区维护,时间一长又回到 DELETE 的老路。

6.2 切换调度目标,前端天数同步改

新过程建好后,把事件或 crontab 指向它。事件方案用 ALTER EVENT:

-- 把已有事件切换到新过程 ALTER EVENT zbx_partitioning ON SCHEDULE EVERY 12 HOUR DO CALL partition_maintenance_all_30and400('zabbix');

crontab 方案则是注释掉旧行、加新行,旧的保留注释便于回滚:

# 旧过程保留注释,需要时可以快速切回 # 30 03 * * * /usr/bin/mysql -u 'zabbix' -p'zabbixDBpass' zabbix -e "CALL partition_maintenance_all('zabbix');" > /tmp/CronDBpartitiong.log 2>&1 30 03 * * * /usr/bin/mysql -u 'zabbix' -p'zabbixDBpass' zabbix -e "CALL partition_maintenance_all_30and400('zabbix');" > /tmp/CronDBpartitiong.log 2>&1

切换完成后,别忘了去前端“管理→一般→管家”把历史和趋势天数改成 30 和 400,和脚本对齐。改完观察一两天,确认新分区在按预期创建、旧分区在按预期删除,再清理不再使用的旧过程。

6.3 验收时我固定看的三个指标

每次调整参数或迁移环境,我都会固定查三样东西。第一是分区清单,确认未来几天的分区已经预建:

-- 看 history 表最近的分区名和边界时间 SELECT PARTITION_NAME, PARTITION_DESCRIPTION, TABLE_ROWS FROM INFORMATION_SCHEMA.PARTITIONS WHERE TABLE_SCHEMA = 'zabbix' AND TABLE_NAME = 'history' ORDER BY PARTITION_ORDINAL_POSITION DESC LIMIT 10;

第二是事件或 crontab 的执行记录,确认调度真的在跑;第三是 Zabbix 自身的 housekeeper 相关监控项,确认 busy 值不再飙升。三个都对上,才算这次调整真正落地。

血泪经验是,分区脚本能解决的是“清理慢”,解决不了“表设计本身有问题”。如果你发现 history 表写入量异常大,先去看看监控项的采集间隔和历史保留策略是不是配得太激进,别指望分区把一切兜住。从那以后我每次给 Zabbix 做分区,都强制走一遍“备份→手动执行→看分区清单→配调度→前端对齐天数→观察 48 小时”的流程,少一步都可能在某天凌晨收到另一条告警。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询