干运维这些年,第一次在监控系统上做这么大的改动——把 Zabbix 的后端数据库从 MySQL 换成 TiDB,说实话一开始心里也没底。当时我们这套 Zabbix 已经跑了三年多,监控着两千多台服务器和网络设备,item 数量超过二十万,历史数据量接近 400GB,MySQL 单实例早就喘不过气了。慢查询、主从延迟、磁盘告警轮着来,最难受的是每次登录图形界面查历史曲线都要等半天,告警事件多的时候,前端页面直接转圈圈。
后来实在忍不了,花了两周时间做方案调研,最后决定把数据库层迁移到 TiDB。整个过程踩了不少坑,也积累了一些一手经验。这篇文章就把完整的选型思路、迁移步骤、调优方案和排坑记录梳理一遍,给准备做同类替换的运维同学一个参考。
1. 项目背景:监控库撑不住后的选型思考
1.1 最初的痛点:一台 MySQL 撑不住 Zabbix 的读写
我们先看看 Zabbix 的数据模型。监控系统最核心的数据不是配置表,而是历史数据表。每台主机每 30 秒采集一次,一个 item 一天就产生 2880 条记录,二十万个 item 每天新增的记录量在五千万到一亿条左右。这些数据按照保留策略,一般要存 30 天到 90 天甚至更久。我们当时保留了 180 天,历史库里光 history_uint 一张表就接近 200GB,加上 trends、history、alerts 这些表,总量干到了 400GB 级别。
MySQL 单实例在这种写入压力下,最先出问题的是磁盘 IO。数据文件越大,InnoDB 缓冲池命中率越低,随机写入和后台刷脏互相争抢 IO。我们当时的架构是主从复制,从库只读,所有历史数据写入都压在主库上,高峰期主库 CPU 经常飙到 80% 以上,从库复制延迟动辄几百秒。一旦主库做备份或者大查询,业务直接受影响。
我当时的排查方法很简单,先看哪些表占空间最多:
SELECT table_name, ROUND((data_length + index_length) / 1024 / 1024 / 1024, 2) AS size_gb FROM information_schema.tables WHERE table_schema = 'zabbix' ORDER BY size_gb DESC LIMIT 10;结果毫无悬念,history_uint 排第一,trends_uint 排第二。再看慢查询日志,排前面的基本都是对历史表的大范围扫描。比如前端画一张 7 天趋势图,可能会扫几百万行数据;触发告警关联查询时,又要对同一批数据做聚合。单机数据库的算力和存储容量在这个量级已经到顶了。
1.2 为什么是 TiDB 而不是其他方案
当时我列了三个候选方向,逐个做了对比。
| 方案 | 优势 | 劣势 | 结论 |
|---|---|---|---|
| MySQL 继续纵向扩容 | 技术栈不变,团队熟悉 | 单机天花板明显,磁盘和 CPU 扩上去成本高,写瓶颈无法根治 | 只能短期缓解 |
| 分库分表中间件 | 能横向扩展写入 | Zabbix 的 SQL 是写死的,中间件对复杂 JOIN 和子查询支持有限,改造成本极高 | 不现实 |
| TiDB | 高度兼容 MySQL 协议和语法,原生分布式水平扩展,支持 HTAP | 对硬件资源有一定要求,运维体系要重新建立 | 最终选择 |
选 TiDB 最关键的一点是它兼容 MySQL 协议。Zabbix 后端用的是标准 SQL,只要 TiDB 能兼容绝大部分语法,Zabbix 几乎不用改业务代码。TiDB 内部是行存加列存的混合架构,TiKV 负责行存储,TiFlash 负责列存储分析,正好匹配 Zabbix 这种写入重、查询分析也重的场景。另外,TiDB 的扩容方式是加节点而不是加配置,以后监控规模再翻一倍,直接加 TiKV 节点就行,不需要再把数据导来导去。
当然,Zabbix 官方文档里并没有把 TiDB 列为受支持数据库,这一点必须有清醒认识。我们当时评估过,Zabbix 用到的数据库功能以 CRUD、索引、事务为主,没有用到 MySQL 特有的存储过程、全文索引、事件调度器这些高级特性,TiDB 兼容这些基础能力,可行性是有的。但敢在生产环境这么搞,必须提前把兼容性坑摸清楚。
2. 迁移前准备:Zabbix 对 TiDB 的兼容性盘点
2.1 Zabbix 数据库访问模型拆解
Zabbix 对数据库的访问可以分成几类,不同类别对数据库的要求完全不同,迁移时要分开看待。
第一类是配置类表,包括 hosts、items、triggers、users 等。这类表数据量小,但是读多写少,并且有大量关联查询,对事务一致性要求高。Zabbix 配置变更时会同时更新多张表,依赖数据库事务保证原子性。TiDB 的分布式事务支持是完整的,这类表基本不会有问题。
第二类是历史数据表,包括 history、history_uint、history_str、history_log、history_text。这是压力最大的部分,特征是持续高并发写入,单条数据很小,但总量巨大。前端查询历史曲线时,又会对这些表做范围扫描。TiDB 在并发写入和分布式扫描上的表现,明显优于单机 MySQL。
第三类是趋势表,包括 trends、trends_uint。这些表是 Zabbix server 每小时对历史数据做聚合生成的,数据量是 history 表的几十分之一,查询频率也不算高,压力不大。
还有一类是非业务数据,比如 sessions、auditlog,量级很小,正常处理即可。
理清这个模型之后,我意识到迁移的重点不是“能不能跑起来”,而是“历史表在高并发写和大型查询下的表现”。这直接决定后面 TiDB 集群参数的调优方向。
2.2 TiDB 集群部署规划和关键参数
TiDB 集群由三个核心组件组成:TiDB Server、PD、TiKV。TiDB Server 负责接受 SQL 请求、解析执行计划;PD 负责整体集群的调度和事务时间戳分配;TiKV 负责实际的数据存储。生产环境最小规格是三台机器,每台机器上部署一个 TiDB、一个 PD、一个 TiKV 实例,形成三副本存储。
我们当时用的部署拓扑如下:
- 3 台数据库服务器,每台配置 16 核 CPU、64GB 内存、2 块 NVMe 数据盘
- 每台机器分别部署 TiDB、PD、TiKV 各一个实例
- TiDB 客户端端口 4000,状态端口 10080
- PD 端口 2379
- TiKV 端口 20160
部署工作直接用 TiUP 完成,版本选择的是 TiDB 7.1 LTS。选 LTS 版本是因为监控系统讲究稳定,不想频繁跟随大版本升级。
一个容易被忽略的点是,PD 和 TiKV 不要和 Zabbix server 部署在同一台物理机上,避免资源争抢。Zabbix server 本身对 CPU 和内存消耗不低,尤其是 housekeeper 跑删除任务的时候,IO 和 CPU 都有明显波动。
2.3 必须提前处理的兼容性细节
即使 TiDB 宣称兼容 MySQL,也有一批细节需要提前确认。我踩过的几个重点如下。
第一个是数据库用户认证方式。TiDB 对 MySQL 的mysql_native_password和caching_sha2_password支持程度不同,Zabbix 连接器在部分版本下可能会出现认证失败。稳妥做法是创建用户时显式指定认证插件:
CREATE USER 'zabbix'@'%' IDENTIFIED WITH mysql_native_password BY 'Zabbix@Pass123'; GRANT ALL PRIVILEGES ON zabbix.* TO 'zabbix'@'%';第二个是字符集和排序规则。Zabbix 官方建库语句默认使用 utf8mb4,而 TiDB 对排序规则的处理和 MySQL 5.7 存在差异。我们统一使用utf8mb4_bin排序规则,避免大小写比较结果不一致导致的查询问题。
第三个是 SQL 模式。TiDB 支持大部分 MySQL 的 sql_mode,但个别模式配置下行为不完全一致。迁移前把原 MySQL 的@@sql_mode查出来,然后在 TiDB 里设置成一致的值:
SELECT @@sql_mode; SET GLOBAL sql_mode = 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION';第四个是时区。Zabbix 的 clock 字段存的是 Unix 时间戳,理论上是 UTC 无关的,但 PHP 前端和数据库连接时区不一致会导致展示结果偏移。后面我会专门讲这个坑。
3. 数据迁移实战:从 MySQL 到 TiDB 完整过程
3.1 迁移策略与备份验证
这种切换不是小操作,必须选业务低峰期。我们把维护窗口定在周末凌晨 2 点,预计停机时间 4 小时。迁移前一周做了一次演练,在测试环境把整套流程跑通,记录每一步的耗时,确认没有阻塞点后才正式排期。
正式迁移前,先对原 MySQL 做物理备份和逻辑备份双保险:
innobackupex --user=zabbix --password='Zabbix@Pass123' --no-timestamp /data/backup/mysql_full_backup备份完成之后,重点确认备份文件可以恢复,不要出现备份完了才发现备份本身是坏的。这一步花的时间不长,但能救命。
3.2 全量导出导入:工具选型与参数
数据导出工具我们用的是 Dumpling,这是 TiDB 生态里的逻辑导出工具,比 mysqldump 更擅长并行导出大表。导出的 SQL 文件可以直接在 TiDB 上执行,兼容性好。
导出命令的参考写法:
dumpling \ -h 旧MySQL地址 -P 3306 \ -u zabbix -p 'Zabbix@Pass123' \ -B zabbix \ -t 8 -F 256MiB \ -o /data/backup/zabbix_dump参数解释一下:-t 8表示 8 个线程并行导出,-F 256MiB表示每个文件拆分到 256MB,避免生成超大 SQL 文件导致后续导入困难。400GB 的库,8 线程导出大约花了 70 分钟,速度还可以。
导入到 TiDB 之前,先在集群上创建数据库和账号:
CREATE DATABASE zabbix CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;然后把 Zabbix 官方提供的 schema 文件导入,再导入业务数据:
mysql -h TiDB地址 -P 4000 -u zabbix -p zabbix < zabbix_schema.sql mysql -h TiDB地址 -P 4000 -u zabbix -p zabbix < /data/backup/zabbix_dump/zabbix.sql这里有个非常重要的参数:导入大数据时,必须把 TiDB 的 GC 时间调长。TiDB 的 MVCC 会清理过期版本,默认 GC lifetime 是 10 分钟。导入过程中如果某个事务跨越多个 Region,执行时间超过这个限制,就会报 GC life time 相关错误导致导入失败。我们导入前执行了:
SET GLOBAL tidb_gc_life_time = '6h';导入完成后,再改回默认值:
SET GLOBAL tidb_gc_life_time = '10m';TiDB 也提供了 Lightning 物理导入方式,速度比逻辑导入快很多,但对表结构有一些限制。如果数据量超过 1TB,建议优先考虑 Lightning;我们当时 400GB 用逻辑导入一个小时左右,可以接受。
3.3 Zabbix 配置切换
数据导完以后,把 Zabbix server 的连接指向 TiDB。Zabbix server 的配置文件是/etc/zabbix/zabbix_server.conf,只需要修改几个参数:
DBHost=TiDB地址 DBPort=4000 DBName=zabbix DBUser=zabbix DBPassword=Zabbix@Pass123前端 PHP 配置在/etc/zabbix/web/zabbix.conf.php,修改对应的数据库连接:
$DB['SERVER'] = 'TiDB地址'; $DB['PORT'] = '4000'; $DB['DATABASE'] = 'zabbix'; $DB['USER'] = 'zabbix'; $DB['PASSWORD'] = 'Zabbix@Pass123';服务启动顺序有讲究。先启动 TiDB 集群,确认 4000 端口能正常连接;然后启动 zabbix_server,观察日志有没有数据库连接错误;最后才启动 Nginx 和 PHP-FPM。PHP 前端配置如果修改后不生效,记得清理一下 opcache 缓存,否则旧的连接配置还会留在内存里。
我们当时犯过一个低级错误:先启动了前端,页面直接报数据库连接失败,排查了半天才发现 zabbix_server 还没起来。顺序不对会误导排查方向。
3.4 配置切换后的校验手段
这一步不能省,要有系统性验证。我分成四层来做。
第一层是数据完整性。挑几张核心表做行数对比,比如 history_uint、trends_uint、hosts、items。用 count 查询两边比对,允许有很小的时间差,但不能有数量级差异。
第二层是功能验证。登录 Zabbix 前端,看仪表盘、最新数据、主机列表、告警事件、历史趋势图,重点确认历史曲线能正常画出来。前端页面能打开不代表图表没问题,一定要点开几个耗时最长的监控项实际看一下。
第三层是写入验证。观察 zabbix_server 日志,正常情况下会持续收到新数据。再等至少 10 分钟,确认新的历史数据能正常写入 TiDB,并能在前端看到最新数据点。
第四层是告警验证。把几个关键主机的监控项调整为“禁止”再恢复,看告警能否正常触发和恢复,确保触发器、事件、动作这些链路在 TiDB 下没有异常。
校验完成后再把旧 MySQL 停掉,但数据盘保留一段时间,确认新环境完全稳定后才做清理。
4. 迁移后的性能调优
4.1 基础参数调优:让 TiDB 更适合高并发写入
数据迁移完成只是第一步,让 TiDB 在 Zabbix 场景下跑得稳才是关键。第一个要动的就是 GC 参数。
Zabbix housekeeper 每天会删除大量过期历史数据,这个过程中会产生大量 MVCC 版本。如果 GC 太活跃,容易和正常读写抢资源;如果 GC 太慢,磁盘上会堆积大量待清理版本。我们最终把tidb_gc_life_time调整为 1 小时左右,并在低峰期执行手动 GC:
SET GLOBAL tidb_gc_life_time = '1h';第二个是开启异步提交。TiDB 的分布式事务开销比 MySQL 单机事务高,对高频小写入不友好。打开异步提交和并行提交能显著降延迟:
SET GLOBAL tidb_enable_async_commit = ON; SET GLOBAL tidb_enable_parallel_commit = ON;这两个参数在 TiDB 5.0 之后加入,7.1 LTS 上已经很稳定。Zabbix 写入历史数据是典型的小事务高频场景,开启之后明显能降低峰值写入延迟。
第三个是连接数。Zabbix server 和 PHP 前端会建立大量数据库连接,TiDB 默认的最大连接数对监控系统可能不够。通过 TiDB 的监控面板看连接数如果接近上限,就调大:
[status] max-connections = 3000同时要注意,连接数不是越大越好,每个连接会占用 TiDB Server 的内存,连接数过高会导致 OOM。建议结合实例内存逐步调整。
4.2 历史表热点消除:SHARD_ROW_ID_BITS 改造
这个坑是我们在压测中发现的,非常典型。
MySQL 的 InnoDB 如果没有显式主键,会生成一个不可见的自增主键。TiDB 也一样,对于没有主键的表,会自动分配一个_tidb_rowid。问题是,这个值在写入时是单调递增的,新插入的数据会连续写到同一个 Region 上,形成写热点。Zabbix 的 history 表就是典型的无主键大表,迁移后第一周,TiKV 的写入流量集中在少数几个 Region 上,单 Region 写入延迟明显偏高。
解决方法是给这些大表开启SHARD_ROW_ID_BITS,把 rowid 的递增序列打散到多个区间:
ALTER TABLE history_uint SHARD_ROW_ID_BITS = 4; ALTER TABLE history SHARD_ROW_ID_BITS = 4; ALTER TABLE trends_uint SHARD_ROW_ID_BITS = 4;SHARD_ROW_ID_BITS = 4表示将 rowid 空间分成 16 个分片,写入会均匀分布在 16 个分片上,热点问题大幅缓解。需要注意这个操作只影响后续新写入的数据,对存量数据不会重新分布,所以建议尽早执行。另外,ALTER 操作本身会消耗一定资源,要在低峰期做。
4.3 历史数据清理优化:分区表玩法
Zabbix 默认靠 housekeeper 定期 DELETE 过期历史数据。单机上这么干还能接受,但到了分布式数据库上,DELETE 操作会跨多个 Region 执行分布式事务,大批量删除时效率并不高,而且容易造成 Region 分裂和合并的抖动。
更好的办法是直接把 history 表改造成分区表,按月份做 RANGE COLUMNS 分区,然后用 DROP PARTITION 代替 DELETE。DROP PARTITION 是元数据操作,秒级完成,不会产生大规模写入放大。
建表示例:
ALTER TABLE history_uint PARTITION BY RANGE COLUMNS(clock) ( PARTITION p202501 VALUES LESS THAN (1738368000), PARTITION p202502 VALUES LESS THAN (1740787200), PARTITION p202503 VALUES LESS THAN (1743465600) );这里的边界值是每个月 1 号对应的 Unix 时间戳,用date -d '2025-02-01' +%s就能算出来。
清理任务就变成定时的 DROP PARTITION,写个脚本每个月执行一次:
#!/bin/bash month_start=$(date -d "3 months ago" +%Y%m) mysql -h TiDB地址 -P 4000 -u zabbix -p'Zabbix@Pass123' zabbix \ -e "ALTER TABLE history_uint DROP PARTITION p${month_start};"这个方案有一个前提:Zabbix 的 housekeeper 配置的保留期要大于分区保留期,或者直接把 housekeeper 针对历史表的清理任务调大到不参与实际删除,否则两边同时删数据会造成重复劳动。我当时是把 history 的 housekeeper 保留时间调到了和分区策略一致,实际删除动作交给脚本完成。
5. 疑难问题与避坑记录
5.1 连接认证不兼容导致前端直接报错
切完配置第一次启动前端,页面直接提示Access denied for user 'zabbix'@'%'。查了一圈,问题出在 TiDB 的用户认证方式。TiDB 默认创建的 MySQL 兼容用户存在认证插件兼容差异,Zabbix 的连接驱动在部分环境下只支持mysql_native_password。
解决方式前面提过:创建用户时显式带上IDENTIFIED WITH mysql_native_password,然后重新授权。如果是已经创建的用户,可以用 ALTER USER 改回来:
ALTER USER 'zabbix'@'%' IDENTIFIED WITH mysql_native_password BY 'Zabbix@Pass123';5.2 时区偏移导致历史曲线错乱
迁移完成当天没发现异常,第二天看历史曲线,发现所有监控项的数据整体偏移了 8 个小时。一开始以为是采集端出了问题,后来确认是时区配置不一致。
原 MySQL 的time_zone是 SYSTEM,实际用东八区。TiDB 默认时区是 UTC,连接层和数据库层时区不一致,加上 PHP 侧的时区设置也有差异,导致数据展示时整体平移。
处理方法是把 TiDB 时区统一设置为东八区:
SET GLOBAL time_zone = '+08:00';同时检查 Zabbix server 和 PHP 的时区配置,确保系统时区、PHP 时区、数据库时区三者一致。这个问题不解决,后续告警时间也全是错的。
5.3 Housekeeper 删除效率低,历史表只增不减
迁移后第二周,发现历史表空间增长很快,housekeeper 似乎在正常工作,但删除速度跟不上写入速度。通过 TiDB 监控看到,DELETE 语句的执行时间普遍在毫秒级,但事务冲突率偏高。
这个问题的本质是:Zabbix 的 housekeeper 批量删除,默认一次删除的数据量有限,在分布式数据库上每个批次都是一个独立事务。如果删除区间和正在写入的数据区间重叠,会产生锁等待和冲突重试。
两个方向改善。一是在 Zabbix 配置里加大 housekeeper 的删除力度:
HousekeepingFrequency=1 MaxHousekeeperDelete=50000二是用前面说的分区表方案,每个月 DROP 一个分区,彻底绕开逐条删除。这个方案实施后,历史表的清理再也没成为瓶颈。
5.4 慢查询定位手段
TiDB 和 MySQL 的慢查询排查方式完全不同。MySQL 看 slow log 就行,TiDB 有内置的 Dashboard,里面提供了慢查询、Top SQL、KV 耗时等维度的分析。
迁移初期我们发现有个别历史图表的查询耗时超过 5 秒,点开 Dashboard 慢查询页面,能看到完整的执行计划和各阶段耗时。定位到问题是 Zabbix 前端某个报表页面发起的 SQL 扫描了过多历史分区。解决办法是在 SQL 条件里缩小时间范围,并且确认查询能命中 itemid 和 clock 的联合索引,而不是全表扫描。
TiDB Dashboard 还可以看每个 Region 的读流量分布,配合热点表打散操作效果非常直观。
6. 迁移效果复盘与后续运维建议
6.1 迁移前后关键指标对比
迁移完成并运行三个月后,我们做了前后对比,效果基本符合预期。
| 指标 | 迁移前 MySQL | 迁移后 TiDB |
|---|---|---|
| 历史数据查询 p95 | 6.8 秒 | 1.2 秒 |
| 高峰期主库 CPU | 经常 80% 以上 | 45% 左右 |
| 主从复制延迟 | 经常几百秒 | 无主从架构 |
| 磁盘空间扩展方式 | 停机加盘 | 在线添加 TiKV 节点 |
| 历史表清理方式 | DELETE 大事务 | DROP PARTITION 秒级完成 |
最明显的变化不是单条 SQL 变快了,而是系统整体不再出现“某个查询拖垮全局”的情况。TiDB 的分布式架构天然隔离了查询和写入的资源争抢,这对监控类系统特别关键。
6.2 把 TiDB 纳入 Zabbix 监控
数据库换了,监控体系也要跟着升级。TiDB 集群本身就提供 Prometheus 格式的 metrics 接口,Zabbix 6.0 以上的版本支持 Prometheus 数据采集,可以直接用 HTTP agent 类型抓取。
做法是在每台 TiDB、PD、TiKV 节点上配置抓取任务,把 tidb_server_handle_query_count、tikv_engine_size_bytes、pd_cluster_status 这些关键指标纳进 Zabbix 模板。这样既能用 Zabbix 监控数据库自身,又能保留原来的告警通道,不需要额外搭一套 Prometheus 告警。
6.3 后续运维建议
备份方案一定要重新设计。MySQL 时代我们习惯用物理备份,TiDB 生态推荐用 BR 工具,备份速度快,恢复也方便。我们每周做一次全量备份,每天做一次增量备份,并定期在测试环境做恢复演练。
版本升级要克制。TiDB 版本升级不同于 MySQL 的小版本升级,组件多、升级顺序有讲究,而且可能涉及 Zabbix 兼容性变化。建议只跟随 LTS 版本升级,升级前先在测试环境验证 Zabbix 全功能。
扩容要提前规划。TiDB 加 TiKV 节点虽然在线操作,但数据迁移会占用网络和磁盘 IO,建议在业务低峰期进行。扩容完成后观察 Region 分布是否均匀,必要时手动调整调度策略。
这次迁移给我最大的感受是:换数据库不是终点,把配套的监控、备份、清理机制一起重建才是。我们当初只想着解决 MySQL 撑不住的问题,结果顺手把历史数据清理和数据库自身监控也理顺了。如果你也在评估类似的替换,建议先用一个小规模的测试环境跑通全流程,把兼容性、时区、认证、热点这些细节都摸清楚再上生产,这样一次性成功的概率会高很多。