2026/10/17 13:27:04

Zabbix 7.0 数据库优化:RANGE分区替换Delete解决housekeeper繁忙实战

Zabbix 7.0 数据库优化:RANGE分区替换Delete解决housekeeper繁忙实战 简介在Zabbix监控系统中历史记录和趋势数据会随时间快速增长若仅依赖内置housekeeper清理数据库压力可能持续升高。这份PDF记录了Zabbix 7.0 LTS部署后的数据库分区优化操作主要面向Zabbix运维人员和数据库管理员重点是借助MySQL/MariaDB分区表机制提升数据管理效率。文件共1个为835KB的PDF文档内容涵盖SQL分区脚本的获取、分区过程的创建与自动执行并详细比较了MySQL事件调度器和Crontab两种调度方式同时覆盖Zabbix前端清理策略、分区天数调整等配套设置还提示了操作前备份、大型数据库耗时等注意事项。通过按小时或天创建分区过期数据可直接删除效率明显优于逐条DELETE适用于Zabbix 3.0之后的多个版本。已有354人学习适合需要缓解监控库膨胀压力、降低维护成本的读者参考。1. 从「75% busy」报警到分区落地Zabbix 7.0 数据库优化到底改了什么把 Zabbix 7.0 LTS 跑起来并不难难的是跑半年之后。监控规模一旦上了几千台设备、几百个模板MySQL 里的 history、trends 表开始以每天上千万行的速度膨胀最先扛不住的往往不是监控本身而是数据库的清理任务。你会在 Zabbix 前端看到一条高频报警Zabbix housekeeper processes more than 75% busy。这条报警的根源不是 CPU 不够而是 Zabbix 默认用 DELETE 语句逐行清理过期数据表越大删得越慢删不完就堆积堆积又拖慢下一轮清理形成恶性循环。数据库分区是替换这套删除机制的常规做法把历史数据按天切成物理分区过期直接 DROP 整个分区几秒钟完成原来几十分钟甚至几小时的清理工作。这篇操作记录按 7.0 LTS 环境梳理分区脚本的导入、参数调整、自动调度与前端配置照着走一遍能把这条报警和图表查询慢的问题一起按下去。2. 分区脚本拆解RANGE 分区与四个存储过程的工作方式2.1 为什么 DELETE 扛不住housekeeper 与 InnoDB 的写入放大Zabbix 的数据模型分两层。history 系列表history、history_uint、history_str、history_log、history_text存放监控项采集到的原始值每个值一行trends 与 trends_uint 存放小时级聚合数据记录每小时的最小值、平均值、最大值。保留策略由 housekeeper 统一执行默认对过期数据执行 DELETE。这套机制在数据量小的时候没问题数据量大了以后问题就暴露出来了。DELETE 在 InnoDB 里的成本不只是「删一行」。引擎要沿着二级索引定位每一行再回表做主键删除同时写入 undo log、binlog标记删除的页还要等 purge 线程异步清理。一张几亿行的 history 表哪怕命中了索引批量 DELETE 也要持续扫描大量页还会产生索引碎片。housekeeper 任务执行时间被拉长新数据还在不断写入下一次触发时积压的过期数据更多恶性循环的最终表现就是那条 75% busy 报警。这个场景下分区方案的价值不是省一点 IO而是改变删除的粒度从「按行删」变成「按分区删」。分区方案的核心是 RANGE 分区。Zabbix 的 history、trends 表都有 clock 字段存的是 Unix 时间戳int 类型天然适合做 RANGE 分区的分区键。脚本按固定时间间隔默认按天把数据切进不同的分区分区名类似 p202601010000对应VALUES LESS THAN (某个时间戳)。清理过期数据时只需要把保留日期之前的分区通过 DROP PARTITION 一次性摘掉这个操作在 InnoDB 层面是删除整个分区段不需要逐行比较、不需要回表、不需要 purge 线程慢慢清理空间可以立刻释放。这套方案对 Zabbix 的适配性很好官方 housekeeper 对历史表和趋势表统一走 DELETE而分区方案把删除动作改由 DROP PARTITION 完成两者的保留逻辑可以对齐到同一天数。脚本不修改 Zabbix 自身的表结构逻辑只是额外建了几个存储过程来管理分区生命周期对前端完全透明。2.2 四个存储过程各管一段verify、create、drop、maintenancezbx_db_partitiong.sql 脚本的核心不是建表而是创建四个存储过程——partition_verify、partition_create、partition_drop、partition_maintenance外加一个总入口 partition_maintenance_all。理解了这四个过程的分工后面调参数、排错才有底。partition_verify 负责检查目标表当前是否已有分区。它查 information_schema.partitions如果发现表还是普通表就会发起初始化常见做法是执行ALTER TABLE ... PARTITION BY RANGE (clock)并创建第一个分区。这一步是把普通表改成分区表需要重建表结构大表在这里会耗时很长后面避坑章节会专门讲。partition_create 负责创建单个新分区。入参包含 SCHEMANAME、TABLENAME、PARTITIONNAME、CLOCK创建前先查 information_schema.partitions 里是否已存在相同边界的分区避免重复创建。确认不存在后用 PREPARE/EXECUTE 动态拼接并执行ALTER TABLE ... ADD PARTITION ... VALUES LESS THAN (...)。之所以要用动态 SQL是因为存储过程里不能直接写表名变量。partition_drop 负责删除过期分区。它用一个游标遍历 information_schema.partitions把分区名中时间戳小于保留边界的分区名收集起来拼成一条ALTER TABLE ... DROP PARTITION p202601010000, p202601020000之类的语句一次执行。如果没有过期分区就返回 N/A不会报错。partition_maintenance 是调度主流程。它先调用 verify 确保表已经是分区表然后从当前时间开始按照 HOURLY_INTERVAL 和 CREATE_NEXT_INTERVALS 两个参数循环调用 partition_create 预创建未来几天的分区最后调用 partition_drop 删除过期分区。总入口 partition_maintenance_all 则把 history、history_uint、trends、trends_uint 这七张表逐个丢给 partition_maintenance 处理。脚本默认的保留策略是历史数据 7 天、趋势数据 365 天分区间隔按天HOURLY_INTERVAL24每次预创建 3 个分区。这套默认值对小规模监控够用生产环境通常要按实际情况调调法在下一章展开。3. 部署与参数调整把 zbx_db_partitiong.sql 落进 MySQL 里3.1 下载、解压和改参数先把分区脚本下载到数据库服务器上。这里的下载链路是脚本作者的站点文件很小一个 tar 包解压出来就是一个 sql 文件wget http://bestmonitoringtools.com/dl/zbx_db_partitiong.tar.gz tar -zxvf zbx_db_partitiong.tar.gz # 解压后核对文件内容确认脚本完整 grep -n partition_maintenance_all zbx_db_partitiong.sqltar 解压完成后先别急着导入。脚本默认的保留参数不一定要直接使用打开 sql 文件找到 partition_maintenance_all 这一段里面包含了所有表的调用参数。下面这段是典型的默认结构CREATE PROCEDURE partition_maintenance_all(SCHEMA_NAME VARCHAR(32)) BEGIN CALL partition_maintenance(SCHEMA_NAME, history, 7, 24, 3); CALL partition_maintenance(SCHEMA_NAME, history_log, 7, 24, 3); CALL partition_maintenance(SCHEMA_NAME, history_str, 7, 24, 3); CALL partition_maintenance(SCHEMA_NAME, history_text, 7, 24, 3); CALL partition_maintenance(SCHEMA_NAME, history_uint, 7, 24, 3); CALL partition_maintenance(SCHEMA_NAME, trends, 365, 24, 3); CALL partition_maintenance(SCHEMA_NAME, trends_uint, 365, 24, 3); ENDpartition_maintenance 的五个参数含义要搞清楚后面所有调整都围绕它们参数含义默认值建议范围SCHEMA_NAMEZabbix 数据库名zabbix按实际库名TABLE_NAME目标表名history 系列 / trends 系列按实际表名KEEP_DATA_DAYS该表保留数据的天数历史 7趋势 365历史 7~90趋势 180~730HOURLY_INTERVAL分区间隔小时24即按天分区24 足够数据量极大可改 12CREATE_NEXT_INTERVALS每次预创建未来几个分区33~7预创建太少可能导致空窗我的经验是历史数据保留 7 天对于很多业务来说太短经常要回溯一周以上的监控数据做故障分析。如果你也有这个诉求直接编辑 sql 文件把 history 系列表的 7 改成 30把 trends 的 365 改成 400改完再保存。注意此时改的是即将创建的存储过程里的参数不是改 Zabbix 前端配置两边后面还要对齐。这里有第一个坑要提前讲如果想改保留天数不要在原脚本改完之后对同一个存储过程名重复执行导入MySQL 里 CREATE PROCEDURE 同名会报错。正确姿势是建一个带后缀的新过程方法在第 3.3 节给出。3.2 执行导入与验证留意大库的 DDL 时长文件内容确认无误后开始导入。这里必须用 mysql 命令行客户端执行因为脚本里含有 DELIMITER 指令这是客户端解释器命令不是服务端 SQL 语法用 Navicat 之类的图形工具直接执行会报语法错误。mysql -uzabbix -pzabbixDBpass zabbix zbx_db_partitiong.sql执行时间取决于脚本本身的长度通常几秒钟就结束。但注意这一步只是创建存储过程并不会立刻把 history 表改成分区表真正的分区化动作发生在第一次调用存储过程时。导入完成后先确认四个过程都建出来了SELECT ROUTINE_NAME, ROUTINE_TYPE FROM information_schema.routines WHERE ROUTINE_SCHEMA zabbix ORDER BY ROUTINE_NAME;预期能看到 partition_create、partition_drop、partition_maintenance、partition_verify、partition_maintenance_all 一共五个过程。缺哪个说明导入出了问题优先检查是不是用了图形客户端执行。接下来手动跑一次完整的分区维护过程让目标表完成从普通表到分区表的初始化mysql -uzabbix -pzabbixDBpass zabbix -e CALL partition_maintenance_all(zabbix);第一次执行时partition_verify 会发现 history 表没有分区触发ALTER TABLE ... PARTITION BY RANGE (clock)做整表重建。数据量几百 GB 的大库在这里会跑几十分钟甚至几小时期间主库 IO 和 CPU 会明显升高建议放在业务低峰期执行并且执行前确认备份可用。执行完成后查看分区结构是否已生效mysql -uzabbix -pzabbixDBpass zabbix -e SHOW CREATE TABLE history\G输出中 Create Table 部分会包含类似下面的片段/*!50100 PARTITION BY RANGE (clock) (PARTITION p202601010000 VALUES LESS THAN (1767225600) ENGINE InnoDB, PARTITION p202601020000 VALUES LESS THAN (1767312000) ENGINE InnoDB, PARTITION p202601030000 VALUES LESS THAN (1767398400) ENGINE InnoDB) */看到 PARTITION BY RANGE 就说明分区化已经完成。此时 history 表的时间戳边界都是未来时间因为脚本会预创建未来三天分区过期分区也会在本次调用中被清理。3.3 修改保留天数新建存储过程而不是重跑脚本如果默认的 7 天历史、365 天趋势不够用正确做法是创建一个新的存储过程而不是重跑原脚本。下面这个例子把历史保留改为 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 ;这里 KEEP_DATA_DAYS 改的是分区清理边界分区过程每次运行时会删除保留天数之前的所有分区因此这个值决定了数据库里物理保留的数据量。HOURLY_INTERVAL 保持 24 表示按天分区如果你希望分区更细比如每小时一个分区可以改成 1此时分区名会变成 p202601010000 到 p202601012300 这种带小时精度的格式但要注意分区数量会变成原来的 24 倍information_schema 的查询和巡检脚本都要跟着调整。新过程创建完成后还需要把调度任务从旧过程切换到新过程。使用 MySQL 事件调度器的环境执行 ALTER EVENT使用 Crontab 的环境编辑 crontab 条目把 CALL 的过程名替换成 partition_maintenance_all_30and400。旧过程保留在库里不碍事后续想回退参数时还能用。4. 自动运行与前端对齐事件调度器、Crontab 与管家设置4.1 方案一MySQL 事件调度器存储过程建好了不会自己跑必须有调度任务周期触发。第一种调度方案是 MySQL 自带的事件调度器推荐优先选这个因为它不依赖操作系统的 crond 状态只要 MySQL 实例存活就能执行。事件调度器默认是关闭的需要先在 MySQL 配置文件的 [mysqld] 段写入开启参数。配置文件位置因系统而异常见路径是 /etc/mysql/mariadb.conf.d/ 或 /etc/my.cnf.d/可以用一条命令全局搜sudo grep --include*.cnf -irl / -e \[mysqld\]找到配置文件后在 [mysqld] 段下追加一行[mysqld] event_scheduler ON然后重启 MySQL 使配置生效。重启后确认变量状态mysql -uzabbix -pzabbixDBpass -e SHOW VARIABLES LIKE event_scheduler;输出 Value 为 ON 说明事件调度器已经工作。接下来创建周期事件每 12 小时调用一次分区维护过程mysql -uzabbix -pzabbixDBpass zabbix -e \ CREATE EVENT zbx_partitioning ON SCHEDULE EVERY 12 HOUR DO CALL partition_maintenance_all(zabbix);事件名可以自定义建议带 zabbix 前缀方便识别。执行间隔 12 小时是原作者的推荐值这意味每天跑两次一次负责清理过期分区、一次负责补建未来分区。如果你的环境分区预创建间隔设置的比较大也可以调整为每天一次但至少保证 24 小时内执行一次否则可能出现分区空窗。创建之后验证事件是否正常注册重点看 LAST_EXECUTED 字段mysql -uzabbix -pzabbixDBpass zabbix -e \ SELECT EVENT_NAME, STATUS, LAST_EXECUTED FROM information_schema.events WHERE EVENT_NAME zbx_partitioning\GLAST_EXECUTED 只有等第一次调度时间到达之后才会更新刚创建完查出来是空的很正常。如果想立即验证事件能不能跑通可以手动执行一次事件体mysql -uzabbix -pzabbixDBpass zabbix -e \ CALL partition_maintenance_all(zabbix);手动执行能提前暴露存储过程里的问题而不是等 12 小时后从日志里翻报错。4.2 方案二Crontab如果你的环境不方便开启 MySQL 事件调度器或者你更习惯把定时任务放在操作系统层统一管理Crontab 是同样可行的方案。用 root 编辑 crontabsudo crontab -e添加一行每天凌晨 3 点 30 分执行分区维护30 03 * * * /usr/bin/mysql -uzabbix -pzabbixDBpass zabbix -e CALL partition_maintenance_all(zabbix); /tmp/CronDBpartitiong.log 21这行命令里/usr/bin/mysql 建议用which mysql确认实际路径避免 crond 的 PATH 环境找不到二进制文件。输出重定向到 /tmp/CronDBpartitiong.log 很有必要分区维护过程中 partition_create 和 partition_drop 会返回处理信息日志里能看出每天执行的结果。一个比较明显的隐患是mysql 密码直接写在 crontab 里通过 ps 或 /proc 能看到口令明文。生产环境我一般会改用 MySQL 的登录配置文件在数据库服务器上建一个权限受限的 ~/.my.cnf[client] userzabbix passwordzabbixDBpass然后 chmod 600 限制权限crontab 里的命令可以简化为30 03 * * * /usr/bin/mysql zabbix -e CALL partition_maintenance_all(zabbix); /tmp/CronDBpartitiong.log 21Crontab 方案的优势是执行记录直接落在系统日志里排错直观劣势是 MySQL 重启不会影响 crond但 crond 本身如果停了分区也不会有任何告警。选哪种方案取决于你已有的运维体系我个人的习惯是数据库专职 DBA 场景用事件调度器偏系统运维场景用 crontab。4.3 前端 Housekeeper 配置对齐数据库层的分区工作做完还差最后一步关键配置Zabbix 前端的 Housekeeper 设置。如果只做数据库分区而不管前端会出现两个清理机制同时工作的局面分区脚本在 DROP 过期分区housekeeper 同时用 DELETE 删同样的数据两边抢 IO问题不但没解决反而更乱。登录 Zabbix 前端依次进入「管理」→「一般」→「管家」按下面步骤配置在「历史记录和趋势」部分取消勾选「开启内部管家」。这会关闭 housekeeper 对历史表和趋势表的删除任务把清理工作完全交给数据库分区机制。勾选「覆盖监控项趋势期间」。这个选项会放开下方数据存储期的编辑权限。在「数据存储期」中填写趋势和历史的保留天数。这个值必须和数据库分区脚本里的 KEEP_DATA_DAYS 保持一致比如数据库配置了历史 30 天、趋势 400 天这里就填 30 和 400。点击「更新」保存。这里的前后端一致性是整套方案里最容易翻车的地方。如果数据库分区保留 7 天前端却设置了 30 天用户会在第 8 天的图表位置上看到一段空白——不是数据没采到而是物理分区已经被 DROP。反过来数据库保留 30 天而前端只设置了 7 天前端会在 7 天时就不再展示数据但磁盘上的分区还占着空间。我在实际维护中会把这个对应关系写进部署文档每次改动存储期必须前后端一起改否则数据断崖几乎是必然发生。5. 分区运维避坑常见的五个翻车现场与排查命令5.1 分区没跑起来Z3005 与 LAST_EXECUTED 为空现象Zabbix 前端图表出现空白日志文件里刷[Z3005] query failed: [1526] Table zabbix.history has no partition for value ...。原因这个报错的意思是查询条件里的 clock 时间戳找不到对应的物理分区。通常有两种情况一是事件调度器或 crontab 根本没配置存储过程只被手动执行过一次预创建的分区用完后就没人补建了二是 MySQL 重启后事件状态变成 DISABLED调度任务静默失效。解决先手动执行一次CALL partition_maintenance_all(zabbix)确认脚本本身没问题然后检查调度链路。事件调度器方案查看 LAST_EXECUTED 是否在最近 12 小时内更新crontab 方案查看 /tmp/CronDBpartitiong.log 有没有新记录。如果事件状态确实是 DISABLED执行ALTER EVENT zbx_partitioning ENABLE;恢复。5.2 首次分区锁表大库执行几小时的账怎么算现象第一次手动执行CALL partition_maintenance_all(zabbix)时命令长时间不返回主库 CPU 和磁盘 IO 打到接近饱和部分业务查询响应变慢。原因这是非分区表转分区表的必经过程。partition_verify 发现 history 表没有分区定义后会发起ALTER TABLE ... PARTITION BY RANGE (clock)这个操作要重建整张表把原有数据按时间戳重新分布到各分区文件里数据量越大耗时越长几小时不罕见。解决先把备份做好再选择业务低谷窗口操作。执行过程中不要并发跑其他 DDL避免锁竞争。如果表实在太大可以在测试环境用相同量级的数据先跑一遍估算耗时和磁盘占用再决定是否需要停机窗口。分区完成后日常的 ADD PARTITION 和 DROP PARTITION 都是秒级操作前期这笔耗时是一次性的。血泪经验是别在监控业务高峰期干这件事否则报警风暴会让你后悔药都没得吃。5.3 数据断崖前端存储期与分区保留天数不一致现象历史图表在某个日期突然完全空白但数据库查询SELECT COUNT(*) FROM history WHERE clock ...还能查到更早的数据或者反过来数据库里数据已经没了前端配置却还显示有存储期。原因前端「数据存储期」和分区脚本的 KEEP_DATA_DAYS 是两套独立配置。分区脚本按自己参数 DROP 分区前端按自己的设置在另一个维度取数任何一边改了、另一边没同步断崖必然出现。解决把「前端存储期 ≤ 分区 KEEP_DATA_DAYS」作为硬性规则。最稳妥的做法是前端存储期填写与分区脚本完全相同的天数并在每次调整时双端同步。我一般会在巡检脚本里对比这两个配置值发现不一致直接报警而不是等业务方来问「为什么图断了」。5.4 DELIMITER 在图形客户端里报错现象用 Navicat 或 DataGrip 打开 zbx_db_partitiong.sql 直接执行报 1064 语法错误位置指向 DELIMITER 关键字附近。原因DELIMITER 是 mysql 命令行客户端自己的指令用来更改语句分隔符服务端 SQL 解析器根本不认识它。图形客户端把整个文件当作纯 SQL 逐条提交自然在 DELIMITER 处炸掉。解决老老实实用命令行导入mysql -uzabbix -pzabbixDBpass zabbix zbx_db_partitiong.sql。如果一定要在图形客户端跑需要手动把 DELIMITER 行删掉再把每个存储过程体单独复制到查询窗口执行但注意$$分隔符也要换成客户端可识别的形式比较麻烦不如命令行一步到位。5.5 事件重启后消失或禁用配置落盘问题现象MySQL 服务重启后information_schema.events 里的事件状态变成 DISABLED 或者直接不存在分区停止增长前端开始报 Z3005。原因事件调度器如果是用SET GLOBAL event_scheduler ON临时开启的重启后就丢失了事件本身虽然创建成功但调度器关闭时不会自动执行。配置文件里没有持久化 event_scheduler 配置这是最常见的疏漏。解决确认 [mysqld] 段下写了event_scheduler ON再重启验证SHOW VARIABLES LIKE event_scheduler。事件本体如果被删了重建即可只是状态 DISABLED执行 ENABLE 恢复。从那次踩坑之后我把这项检查写进了每次 MySQL 重启后的标准巡检动作。6. 进阶验证确认分区被真正利用起来的三步巡检6.1 分区状态巡检 SQL分区方案跑起来之后不能只在出问题时才想起来看日常巡检要能看到两张关键信息分区有没有在正常增长过期分区有没有在正常清理。SELECT table_name, COUNT(*) AS partition_count, FROM_UNIXTIME(MIN(PARTITION_DESCRIPTION)) AS oldest_partition, FROM_UNIXTIME(MAX(PARTITION_DESCRIPTION)) AS newest_partition FROM information_schema.partitions WHERE table_schema zabbix AND table_name IN (history, history_uint, trends, trends_uint) GROUP BY table_name;这条语句能从 information_schema.partitions 里读出每张表的分区数量和边界时间。oldest_partition 应该落在保留天数边界附近newest_partition 应该大于当前时间因为有预创建逻辑。如果发现 newest_partition 已经小于当前时间说明调度任务停滞了需要立刻检查事件状态或 crontab 日志。6.2 EXPLAIN 看分区修剪分区优化的价值在于查询时只扫描相关分区。用 EXPLAIN 验证 history 表按时间范围查询时是否真的触发了分区修剪EXPLAIN SELECT * FROM history WHERE clock BETWEEN UNIX_TIMESTAMP(2026-01-05 00:00:00) AND UNIX_TIMESTAMP(2026-01-05 23:59:59)\G输出结果里看 partitions 字段。如果显示的是具体且较少的几个分区名说明修剪生效如果显示 ALL说明查询条件没有有效落到分区键上需要检查 WHERE 条件里是否对 clock 做了函数包装比如WHERE FROM_UNIXTIME(clock) ...会让分区键失去索引意义。6.3 把巡检固化成习惯我现在的习惯是写一个简短的巡检脚本每天定时执行检查事件执行时间和分区边界#!/bin/bash # 分区巡检检查调度是否在 24 小时内执行过 mysql -uzabbix -pzabbixDBpass zabbix -N -e \ SELECT IF(LAST_EXECUTED NOW() - INTERVAL 24 HOUR, OK, STALLED) FROM information_schema.events WHERE EVENT_NAMEzbx_partitioning;输出为 STALLED 就触发告警输出为 OK 就继续盯着分区边界。这个脚本我部署在了每台 Zabbix 数据库服务器上从那以后每次处理 Zabbix 数据库性能问题我都会强制走一遍导入分区脚本、确认调度任务在跑、核对前后端存储期一致三步做完再看下周报里的 housekeeper busy 指标几乎再没出现过这条报警。分区不是什么玄学它就是把「删数据」这件事换了一个更符合数据库特性的做法关键是前后端对齐、调度落地、巡检跟上。希望帮到你。本文还有配套的精品资源点击获取