简介:Zabbix 7.0 LTS部署后,历史与趋势数据表容易快速膨胀,甚至出现Zabbix housekeeper进程繁忙告警,而数据库分区是缓解这类问题的有效手段。这份PDF操作记录基于MySQL或MariaDB环境,围绕zbx_db_partitiong.sql分区脚本展开,完整覆盖脚本下载解压、分区存储过程创建、通过MySQL事件调度器或Crontab配置定时自动执行,以及Zabbix前端历史与趋势保留天数调整等关键环节;脚本默认保留7天历史数据与365天趋势数据,读者也可按实际需求修改天数设置。通过分区方案,可显著提升大数据量下的清理效率与数据库长期稳定性,对需要在生产环境中实施分区策略的运维工程师有直接参考价值。资源为单个PDF文档,大小约835KB,命令与步骤示例均保留完整,适合作为部署调优时的速查手册。目前已有354人学习下载,教程内容同样适用于Zabbix 3.0之后的各主流版本,是监控系统性能维护方面较为实用的参考资料。
1. 为什么Zabbix 7.0 LTS要先想好数据库分区:监控系统最容易被数据淹死
Zabbix 7.0 LTS是目前最值得落地的长期支持版监控平台,但很多团队部署完才发现,真正决定监控平台能跑多久的不是server进程,而是数据库里那几张越涨越离谱的表。我接过一套监控300多台服务器、历史数据保存半年的Zabbix,innodb_buffer_pool调到16G,查询history_uint还是要好几秒,triggers反复报慢查询。最后靠着数据库分区把单表拆成按天分片,前端图表的响应终于回到秒级。这篇操作记录围绕Zabbix 7.0 LTS部署和数据库分区优化两条线展开,先把安装和初始化讲透,再给出一套能直接复制的分区方案与踩过坑,适合正在做监控架构巡检、想把Zabbix跑长线的运维工程师。
2. Zabbix 7.0 LTS部署:从仓库选择到server启动的完整链路
2.1 环境与版本选型:Ubuntu 22.04 LTS + MySQL 8.0,为什么不用CentOS 7
Zabbix 7.0 LTS发布后提供了长时间支持承诺,对生产环境来说首选用它而不是6.0。部署之前先定底座,我这次选用Ubuntu 22.04 LTS服务器版配合MySQL 8.0。原因有三个:一是Zabbix官方仓库对Ubuntu 22.04 LTS同时提供deb和rpm包,直接apt安装零折腾;二是MySQL 8.0的RANGE分区语法比5.7更统一,后面做自动化分区少踩不少坑;三是CentOS 7已经EOL,继续用会碰到OpenSSL和PHP扩展的依赖问题,没必要给自己找不痛快。
资源规划上,如果被监控节点规模在500台以内,4核8G内存足够,磁盘建议上SSD。history表的写入模式是高频小IO,机械盘在持续监控写入下容易形成瓶颈。我习惯把数据库数据目录放到单独挂载的/srv/mysql,避免binlog把根目录撑满。Zabbix server自身的CPU占用不高,但数据库磁盘和innodb_buffer_pool需要重点保障。
为什么强调LTS版本?监控平台一旦上线就是长期运行,不像业务系统可以经常发版。LTS版本能保证一年半载不用考虑大版本升级,减少迁移风险。7.0相比6.0在告警抑制和用户界面交互上有改进,但底层数据表结构没有本质变化,所以数据库分区的经验可以完全沿用,这也是本篇写分区优化而不是只讲安装的原因。
2.2 安装Zabbix server、前端和agent的实测命令(以Ubuntu 22.04 LTS为例)
Zabbix 7.0 LTS官方仓库的安装顺序是:先添加仓库,再安装server、前端和agent。以Ubuntu 22.04 LTS为例,命令如下:
wget https://repo.zabbix.com/zabbix/7.0/ubuntu/pool/main/z/zabbix-release/zabbix-release_latest+ubuntu22.04_all.deb dpkg -i zabbix-release_latest+ubuntu22.04_all.deb apt update apt install -y zabbix-server-mysql zabbix-frontend-php zabbix-nginx-conf zabbix-sql-scripts zabbix-agent参数说明:zabbix-release包会把Zabbix 7.0的apt仓库写入/etc/apt/sources.list.d/,并配置好GPG key。zabbix-frontend-php是Web前端,zabbix-nginx-conf是Nginx站点配置,zabbix-sql-scripts用来导入数据库schema。如果不想用Nginx,可以换成zabbix-apache-conf,但我习惯用Nginx,内存占用更小。
安装完先初始化MySQL数据库:
mysql -uroot -p -e "CREATE DATABASE zabbix CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;" mysql -uroot -p -e "CREATE USER 'zabbix'@'localhost' IDENTIFIED BY 'zabbix_passwd';" mysql -uroot -p -e "GRANT ALL PRIVILEGES ON zabbix.* TO 'zabbix'@'localhost';" mysql -uroot -p -e "FLUSH PRIVILEGES;"注意密码别用明文写在脚本里,上面只是示例。CREATE DATABASE指定utf8mb4和utf8mb4_bin是为了和Zabbix 7.0官方要求一致,否则导入schema时中文排序和索引匹配都可能出现异常。接着导入schema:
zcat /usr/share/zabbix-sql-scripts/mysql/server.sql.gz | mysql -uzabbix -p'zabbix_passwd' zabbix这段命令直接用zcat解压官方提供的server.sql.gz,通过管道导入zabbix库。整个过程可能持续几分钟,不要中断。如果终端假死,先检查磁盘空间和MySQL连接数,别盲目重启。
然后修改Zabbix server配置:
sed -i 's/^# DBPassword=.*/DBPassword=zabbix_passwd/' /etc/zabbix/zabbix_server.conf systemctl restart zabbix-server配置好后访问http://server_ip/zabbix,默认用户Admin,初始密码是zabbix。登录后第一件事改密码,并且把前端PHP时区调好。
2.3 配置数据库与启动服务:初始化schema和时区坑
schema导入后还需要做两件事:调整PHP时区和MySQL时区,否则监控图表显示UTC时间,跟本地时间差8小时。Zabbix前端默认读取PHP配置里的timezone,修改/etc/zabbix/php-fpm.conf或php.ini:
date.timezone = Asia/Shanghai然后重启php-fpm和nginx。同时,MySQL的time_zone也要同步:
SET GLOBAL time_zone = '+08:00'; SET GLOBAL log_timestamps = SYSTEM;如果只改PHP不改MySQL,数据库记录的时间仍然是UTC,前端最新数据展示没问题,但报表跨天统计时会出现边界错位。这一步对后面分区设计非常关键,因为Zabbix分区键是clock字段,存的是Unix秒,时区不一致会造成分区边界偏移。
agent的安装也顺手说一下,在客户端执行:
apt install -y zabbix-agent sed -i 's/^Server=127.0.0.1/Server=192.168.10.20/' /etc/zabbix/zabbix_agentd.conf systemctl restart zabbix-agent注意Server和ServerActive两个参数都要写。Zabbix 7.0支持被动和主动检查,Server是被动模式的白名单,ServerActive是主动上报的server地址,只写Server会导致主动式监控失败。到这里server和agent已经跑通,但数据库还没有分区,下面进入分区优化正题。
3. 数据库分区优化原理:为什么Zabbix的history表是分区的主战场
3.1 存储结构:history、trends、history_uint,哪些表必须分
Zabbix收集到的每个监控项都会写入历史数据表,history存浮点数指标,history_uint存整数指标(CPU使用率、内存用量、磁盘IO),history_str和history_text存字符串和日志,trends和trends_uint是趋势表,用来支持长时间范围图。默认保留策略是history存7天、trends存30天,但多数团队会调长到90天甚至一年,导致history和trends成为数据库里体积最大的两张表。
分区为什么能起作用?InnoDB索引是B+树结构,单表超过千万行后,索引层级可能从3层加深到4层,每次都多一次磁盘I/O。分区把大表物理拆成多个子表,查询SQL如果带clock条件,优化器可以直接裁剪到对应分区,扫描量大幅下降。Zabbix 7.0的监控查询SQL基本都是WHERE clock BETWEEN xxx AND xxx,天然适配分区裁剪。
注意不是所有表都需要分区。比如history_log这种数据量小、查询不频繁的表,分区反而增加管理成本。我一般只分history、history_uint、history_str、trends、trends_uint这五张,如果实际环境里history_text也超过了几百万行,再考虑纳入。
分区还有个隐藏好处:删除过期数据只用DROP PARTITION,比DELETE快几个数量级。传统清理方式在高并发监控库上会把MySQL拖到无法响应,分区表则完全没有这个压力,这也是Zabbix官方和社区都推荐分区的原因。
3.2 分区策略设计:按天RANGE分区、保留期与索引设计
常见做法是按天做RANGE分区,每个分区对应一天数据。这样的好处是与监控数据的自然时间轴一致,每天的凌晨做维护操作,删除旧数据时也直观。一天一个分区,一年就是365个分区,InnoDB完全能承受,只要不是长事务频繁访问information_schema,基本没有额外开销。
分区键选Zabbix表的clock字段,它是Unix时间戳整数类型。MySQL有硬性要求:分区字段必须包含在表的所有唯一索引里。Zabbix 7.0原始表结构如果已经有PRIMARY KEY(id,clock),那分区字段已经在主键中,直接分区即可。如果从老库升级后表没有显式主键,需要先加主键,或者把clock加进唯一索引,否则执行PARTITION BY RANGE会报错。
保留期设计我一般遵循:history保留90天,trends保留30天。分区自动创建到未来2天,自动删掉91天前的分区。这可以避免偶尔的系统停机导致没有未来分区,也可以保证数据不突破保留期。分区边界用VALUES LESS THAN,它是右开区间,意思是不包含右边界的值,边界必须是比当天晚一天的零点。
ALTER TABLE history_uint PARTITION BY RANGE (clock) ( PARTITION p20250101 VALUES LESS THAN (UNIX_TIMESTAMP('2025-01-02 00:00:00')) );这里UNIX_TIMESTAMP是MySQL函数,把本地时间转换为Unix秒。边界设置为1月2日零点,那么p20250101分区包含1月1日全天数据。注意,分区边界必须始终比当前时间晚至少一天,如果边界过期,新写入的数据会因为找不到匹配分区而报错。
3.3 用MySQL 8.0的RANGE分区实现:一个具体的分区脚本
手工为每个表建一年365个分区不现实,我习惯用存储过程动态生成。以history_uint为例,先写一个能创建未来N天分区的脚本:
DELIMITER $$ CREATE PROCEDURE dynamic_partition_create(IN dbname VARCHAR(64), IN tablename VARCHAR(64), IN days INT) BEGIN DECLARE i INT DEFAULT 1; DECLARE p_date DATE; DECLARE p_name VARCHAR(32); DECLARE p_sql TEXT; WHILE i <= days DO SET p_date = DATE_ADD(CURDATE(), INTERVAL i DAY); SET p_name = CONCAT('p', DATE_FORMAT(p_date, '%Y%m%d')); SET p_sql = CONCAT('ALTER TABLE ', dbname, '.', tablename, ' ADD PARTITION (PARTITION ', p_name, ' VALUES LESS THAN (UNIX_TIMESTAMP(''', DATE_ADD(p_date, INTERVAL 1 DAY), ''')))'); SET @sql = p_sql; PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET i = i + 1; END WHILE; END$$ DELIMITER ;这个存储过程接收数据库名、表名和天数,循环创建未来N天的分区。每次ALTER TABLE ADD PARTITION在MySQL 8.0里不会重建全表,但会有MDL锁,所以高峰期不能频繁执行。我在生产环境里每天只跑一次,一次创建2天分区,锁冲突就少很多。
注意这里用了动态SQL,是因为ADD PARTITION语法不支持直接用变量作为分区名和边界值。如果你试过把变量直接写在ALTER TABLE里,会看到语法错误提示,用PREPARE + EXECUTE是绕开这个限制的标准办法。
3.4 分区后的校验清单:分区数、边界和裁剪效果快速验证
分区做完后要验证,不能只看没有报错就认为成功。最直接的查询是看information_schema:
SELECT TABLE_NAME, PARTITION_NAME, PARTITION_DESCRIPTION FROM information_schema.PARTITIONS WHERE TABLE_SCHEMA='zabbix' AND TABLE_NAME='history_uint' ORDER BY PARTITION_DESCRIPTION DESC;PARTITION_DESCRIPTION显示分区的最大值,最新分区应大于当前时间戳,旧分区的数量要符合保留天数。如果最大分区的时间小于当前时间,说明建分区的脚本有bug,后续写入会失败。
验证分区裁剪是否生效,可以用EXPLAIN:
EXPLAIN SELECT * FROM history_uint WHERE clock BETWEEN 1750896000 AND 1750982400;EXPLAIN结果里如果看到type=ALL或者rows特别大,说明分区裁剪没有起作用。常见原因是SQL里对clock字段做了函数转换,比如FROM_UNIXTIME(clock),优化器无法把函数计算后的条件映射成分区范围。在Zabbix自己的查询中不会出现这个问题,但如果你写了外部报表查询,要特别注意分区键保持原始值。
4. 实施数据库分区:把规划变成定时任务
4.1 先备份再动手:部分数据的风险与备份策略
分区改造前一定要备份。我见过有人在几千万行的大表上直接跑ALTER TABLE PARTITION BY RANGE,执行到一半磁盘满,整个zabbix库不可用。有了备份还能回滚,否则只能从监控项的重新采集开始补数据,那是灾难。
MySQL备份用mysqldump,注意加一致性参数:
mysqldump -uzabbix -p --single-transaction --set-gtid-purged=OFF --databases zabbix > /backup/zabbix_$(date +%F).sql--single-transaction用InnoDB事务保证备份期间数据一致,不会锁表;--set-gtid-purged=OFF是为了在非GTID环境恢复时避免gtid报错。如果库实在太大,mysqldump会慢,建议用mydumper做并行备份,但最小可行方案还是mysqldump加定时任务。
对表结构做分区时,ALTER TABLE PARTITION BY RANGE会重建全表,对几十GB的表耗时长且有锁。如果业务不能忍受,可以分成两步:先做一个新分区表,再INSERT INTO SELECT导数据,最后改表名。但Zabbix监控数据中断几分钟影响不大,只要把窗口放在凌晨,直接用ALTER TABLE也能接受。我这次就是凌晨执行的,提前停了外部报表任务,保证没有长事务持有锁。
4.2 自动创建和删除分区的存储过程与事件调度器
分区表建好后,管理要自动化。我维护一个存储过程,每天凌晨处理三件事:创建未来2天的分区,删除超过保留期90天的分区,并记录每次操作日志。示例过程:
DELIMITER $$ CREATE PROCEDURE manage_zabbix_partitions() BEGIN DECLARE expire_part_name VARCHAR(32); DECLARE create_date DATE; DECLARE create_part_name VARCHAR(32); DECLARE done INT DEFAULT 0; DECLARE cur CURSOR FOR SELECT PARTITION_NAME FROM information_schema.PARTITIONS WHERE TABLE_SCHEMA='zabbix' AND TABLE_NAME='history_uint' AND PARTITION_NAME < DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 90 DAY), 'p%Y%m%d'); DECLARE CONTINUE HANDLER FOR NOT FOUND SET done=1; OPEN cur; del_loop: LOOP FETCH cur INTO expire_part_name; IF done=1 THEN LEAVE del_loop; END IF; SET @drop_sql = CONCAT('ALTER TABLE zabbix.history_uint DROP PARTITION ', expire_part_name); PREPARE stmt FROM @drop_sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; END LOOP; CLOSE cur; SET create_date = DATE_ADD(CURDATE(), INTERVAL 1 DAY); SET create_part_name = CONCAT('p', DATE_FORMAT(create_date, '%Y%m%d')); SET @add_sql = CONCAT('ALTER TABLE zabbix.history_uint ADD PARTITION (PARTITION ', create_part_name, ' VALUES LESS THAN (UNIX_TIMESTAMP(''', DATE_ADD(create_date, INTERVAL 1 DAY), ''')))'); PREPARE stmt FROM @add_sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; END$$ DELIMITER ;这个存储过程用游标扫描information_schema.PARTITIONS,找出所有分区名小于90天前日期的分区,DROP掉。然后创建次日分区,保证未来分区总是存在。DROP PARTITION是立即释放空间,不会像DELETE那样产生大量binlog和回滚段,这是分区表在空间回收上的最大优势。
创建事件调度器:
SET GLOBAL event_scheduler = ON; CREATE EVENT ev_manage_zabbix_partitions ON SCHEDULE EVERY 1 DAY STARTS '2025-01-01 03:30:00' DO CALL manage_zabbix_partitions();event_scheduler默认可能是OFF,如果不开,事件不会执行,这是后面常见的翻车点。确认开启状态用SHOW VARIABLES LIKE 'event_scheduler';。
4.3 从Zabbix 6.0迁移到7.0时分区表的注意事项
如果是从Zabbix 6.0升级到7.0,并且老库已经做过分区,升级前要检查分区边界是否覆盖到今天。7.0的schema脚本会对表结构做变更,可能涉及新索引,在分区表上ALTER TABLE ADD INDEX会一次性重建所有分区,耗时长。所以我的建议是升级前先把旧分区清到只剩最近30天,导入7.0 schema后再按新策略重建,这样迁移窗口短,历史趋势数据丢掉一部分也无所谓。
另外,Zabbix 7.0官方schema脚本里可能包含了对history表增加列的改动。分区表增加列在MySQL 8.0里有特殊限制:如果新增列的默认值不是常量,会报不支持。实际上7.0的变更都是常量默认值,问题不大,但升级过程中一旦报错,要把官方给的ALTER语句改成按分区逐个处理,或者先合并分区再升级。
还有一个容易被忽略的点:如果分区表已经运行了很久,分区数量非常多,升级时查询information_schema会变慢。可以先删掉一半历史分区再升级,减少元数据扫描时间。
5. 分区后必踩的坑:查询变慢、锁表与监控断档的排查记录
5.1 现象:分区后history查询反而变慢
分区前几千万行查询1秒,分区后变成3秒,这是分区裁剪失效的典型症状。原因通常是外部SQL对clock字段做了函数运算,比如WHERE FROM_UNIXTIME(clock) > '2025-01-01',优化器无法把函数结果转换成分区范围。解决方法是不要对分区键套函数,查询条件直接使用Unix时间戳值。
另一个原因是分区粒度过细。我曾经把分区粒度调整为每小时,一年下来分区数超过8000个,慢查询不降反升。按天分区分区数量可控,是性价比较高的方案。如果查询条件没有精确到分区键范围,优化器会扫描所有分区再合并结果,反而比普通单表更慢。
5.2 现象:ALTER TABLE导致主从延迟飙高
在master上对几十GB的分区表执行DROP PARTITION,动作本身很快,但产生的DDL传入从库后,从库需要长时间持有MetaData Lock,复制延迟会突然飙升。解决方法是把分区删除任务放到低峰期,从库配置并行复制开关,比如设置slave_parallel_workers=4,可以缓解大半。
如果延时还是压不住,就把删除动作拆分,一次只DROP一个分区,然后sleep 5秒,给从库留追数据的时间。我有一次一口气删30个历史分区,结果从库延迟到了10分钟,告警刷了一屏,这是最惨痛的教训。
5.3 现象:定时删除分区失效,事件调度器没开
部署完存储过程,第二天发现分区还在,数据已经超出保留期。最直接的原因是MySQL的event_scheduler没有开启。很多人只在会话里SET GLOBAL event_scheduler=ON,MySQL重启后又回到OFF。解决方法是把event_scheduler=ON写进my.cnf的[mysqld]段,然后重启MySQL。
另一个原因更隐蔽:存储过程里用游标查询information_schema时,如果applied到老库,有时候分区名的字符串比较规则判断失误。比如分区名p20241231和p20250101,如果过滤条件写PARTITION_NAME < 'p20250101',会正确删除旧分区,但如果是带时分秒的日期格式,就会匹配错误。解决方案是统一分区命名格式为p%Y%m%d,用DATE_FORMAT生成。
5.4 现象:新分区不在预期的时间范围,时区错位
分区的边界用UNIX_TIMESTAMP('2025-01-02')生成,如果MySQL的time_zone是UTC,UNIX_TIMESTAMP会把字符串当作UTC时间换算,分区边界和实际北京时间差8小时。更隐蔽的是,存储过程中拼SQL时,CURDATE()使用的是MySQL会话时区,而PHP监控项又用系统时区,两者不一致直接导致分区边界错位一天。
排查方法是用SELECT UNIX_TIMESTAMP('2025-01-02 00:00:00'),看结果是否等于当天的北京时间秒数。解决方法是统一MySQL时区,在my.cnf里写default-time-zone = '+08:00',同时规范所有连接串不要设置session time_zone。如果你已经被坑过,会发现所有分区边界都偏移了,只能手动合并删除老分区再重建。
5.5 现象:频繁DDL让MySQL连接堆积
分区创建和删除如果集中在一个时间点,会对MySQL产生大量短连接和元数据锁请求。现象是监控前端页面报数据库连接超时,processlist里堆满waiting for table metadata lock。原因是分区管理任务的连接没有关闭,或者与Zabbix server的采集线程形成锁等待。
解决方法是给业务连接和运维连接区分账号,分区管理专门用一个账号,并设置lock_wait_timeout。另外,事件调度器的执行计划可以加一个随机偏移量,比如每天03:30:00之后等待120秒再执行,错开整点的Zabbix housekeeping任务。这个坑不亲眼见到很难想到,但确实会让生产环境在凌晨出现短时间卡顿。
6. 用Zabbix 7.0内置监控验证分区成效:数据库层面看分区真实收益
6.1 用慢查询日志和performance_schema验证分区干净
分区后到底快多少,不能凭感觉。先开启MySQL慢查询日志:
SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 2; SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';运行前端几个折线图,等几分钟后查看slow.log里history相关SQL的执行时间。如果优化得当,超过90%的history查询应该在2秒以内。进一步用performance_schema定位锁等待:
SELECT EVENT_NAME, COUNT_STAR, SUM_TIMER_WAIT/1000000000 AS ms FROM performance_schema.events_statements_summary_by_digest WHERE DIGEST_TEXT LIKE '%history_uint%' ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;SUM_TIMER_WAIT单位默认是皮秒,除以1e9转为毫秒。只有查看history_uint相关语句,才能确认哪些SQL有分区裁剪失效的问题。如果某些SQL执行次数多但总耗时异常,就要回到5.1的坑去处理。
6.2 用Zabbix的MySQL模板监控InnoDB缓冲池和临时表
Zabbix 7.0自带MySQL by Zabbix模板,可以在server上配置一个监控用户,然后在主机里挂载模板,就能收集InnoDB缓冲池命中率、临时表数量、Table打开次数等指标。我重点盯InnoDB缓冲池命中率,分区后应该稳定在99%以上。如果发现命中率掉到95%以下,说明有大量全表扫描,优先排查外部报表和定时任务。
另外,慢查询日志里如果持续出现"Creating tmp table",说明SQL在排序或分组时产生了临时表。分区后这类语句减少,说明数据裁剪缩小了中间结果集,这是分区收益的直接证明。配合Zabbix的触发器,还可以在你设置的阈值上自动报警。
我的习惯是分区优化上线后观察至少两周,把慢查询日志和缓冲池命中率对照着看。有一次优化完缓冲池命中率没起色,查了才发现很多外部脚本每小时执行一次SELECT COUNT(*)全表扫描history_uint,分区再多也白搭。后来把这些脚本改成查information_schema的分区统计,压力顿时消失。
如果你也遇到类似情况,先别急着缩短保留期,先找出全表扫描的来源。分区不是银弹,它只对按时间范围查询有效,把不规范的SQL清理掉,Zabbix 7.0 LTS才能真正跑得长。希望这份记录能帮你把部署和分区一次做顺。
本文还有配套的精品资源,点击获取