首页 / 帮助文档 / 索引维护低峰期执行

索引维护低峰期执行

索引维护到底该在什么时候跑,这个问题很多技术团队都搞错了。最常见的做法是把全量索引重建或者大批量更新直接挂在业务低峰期,凌晨两三点开始跑,早上八点前跑完。听起来合理,实际上隐患很大。索引维护的“低峰期”不应该是业务流量的低峰期,而应该是数据写入的低峰期。这两个概念经常被混淆,但区分清楚直接决定了你的数据库会不会在关键时刻崩溃。

先说清楚一个底层逻辑:索引维护消耗的核心资源不是查询带宽,而是写入IO和锁竞争。你凌晨三点跑全量索引重建,业务查询量确实最低,但如果这个时间段恰好是离线数据导入、批量结算、日志回放的高峰期,那你的索引维护就和这些重型写入操作撞车了。结果就是写入阻塞、主从延迟飙升、甚至触发死锁。所以真正的索引维护低峰期,是要找到数据写入量最小的窗口,而不是简单看接口请求量曲线。

如何精准定位索引维护的真实低峰期

不要凭感觉,也不要只看应用层的QPS监控。你需要拉取数据库层的写入指标,具体看三个数据:InnoDB行插入/更新/删除的速率、binlog产生速率、以及主从复制延迟曲线。把这三个指标按小时粒度做成热力图,连续观察至少一周,你会发现写入的真正低谷往往不在凌晨,而是在某个业务间歇期,比如下午某个整点结算后的半小时,或者晚上业务收尾后的一小段窗口。这个窗口才是索引维护应该抢占的位置。

如果你的业务是全球化的,没有明显的全局低峰期,那就需要按分区或者分表维度做滚动维护。比如按用户ID哈希分64个分表,每次只维护一个分表的索引,控制在5分钟内完成,然后间隔一段时间再处理下一个。这样把维护操作打散,避免集中冲击。代价是管理复杂度上升,但换来的是系统稳定性的质变。

索引维护操作本身的风险控制

很多人以为索引维护就是一条ALTER TABLE语句丢进去等结果,这种操作方式在生产环境极其危险。不管你用的是MySQL还是PostgreSQL,大表索引创建或重建过程中都会产生大量磁盘IO和临时文件,甚至导致整个表被锁住。MySQL 5.6之后的Online DDL虽然号称不阻塞读写,但在索引创建的初始化和收尾阶段仍然需要获取元数据锁,这个锁如果拿不到,后续所有查询都会被阻塞,形成雪崩。

控制风险的第一原则是:永远不要在生产环境直接执行没有验证过的DDL。先在同等数据量和硬件配置的预发布环境跑一遍,记录下耗时、IO峰值、临时表空间占用、以及是否触发了任何锁等待。拿到这些数据后再决定生产环境的执行策略。

第二原则是使用pt-online-schema-change或者gh-ost这类在线变更工具。它们的工作原理是创建一个影子表,在影子表上完成索引变更,然后通过触发器或者binlog同步增量数据,最后在极短的锁表时间内完成表切换。gh-ost尤其值得推荐,因为它不依赖触发器,而是伪装成从库拉取binlog来同步增量,对主库的侵入性更小。使用这些工具时有一个关键参数必须设置:最大负载阈值。比如设置Threads_running超过某个值就自动暂停迁移,避免影响线上业务。

# gh-ost 常用参数示例
gh-ost \
  --host=127.0.0.1 \
  --user=ghost \
  --password=your_password \
  --database=your_db \
  --table=your_table \
  --alter="ADD INDEX idx_created_at (created_at)" \
  --max-load=Threads_running=30 \
  --critical-load=Threads_running=100 \
  --chunk-size=1000 \
  --cut-over=atomic \
  --execute
索引类型选择对维护窗口的影响

索引不是建完就完事了,不同类型的索引在维护时的开销天差地别。B+Tree索引在插入新数据时需要分裂节点、调整树结构,如果页分裂频繁,写入性能会断崖式下降。这就是为什么自增主键的写入性能远好于随机UUID主键——自增ID的插入总是在索引的最右侧,几乎不触发页分裂;而UUID的随机性导致每次插入都可能需要在索引中间开辟新空间,页分裂率极高。

如果你的业务必须使用UUID作为主键,那么索引维护的策略就要调整。一个有效的做法是使用有序UUID,比如UUID v7或者ULID,它们把时间戳放在前面,保证了一定程度的单调递增性,大幅减少页分裂。另一个做法是定期做索引碎片整理,通过OPTIMIZE TABLE或者重建索引来回收因页分裂产生的碎片空间,这个操作当然也要放在写入低峰期执行。

全文索引和空间索引的维护成本更高。全文索引的倒排表在大量更新后会出现碎片,查询性能逐步下降,需要定期重建。空间索引的R-Tree结构在数据分布变化后同样需要维护。这些特殊索引的维护窗口要比普通B+Tree索引留得更宽裕,因为它们的重建过程更消耗CPU和内存。

分区表索引维护的特殊处理

分区表在索引维护上有独特的优势,但用不好反而会放大问题。分区表允许你对单个分区执行索引操作,这意味着你可以把一个大表的索引维护拆解成多个小任务,每次只处理一个分区。比如按月份分区的订单表,你只需要对当月或者上月的分区做索引重建,历史分区可以保持不动。这样维护窗口大幅缩短,风险也分散了。

但分区表的陷阱在于:如果你执行的是影响全表的DDL,比如新增一个全局索引,那分区表的优势就荡然无存,反而因为分区元数据的复杂性导致执行时间更长。所以设计分区策略时就要考虑索引维护的边界,尽量让索引变更可以限制在单个分区范围内。另外,分区表的TRUNCATE和DROP分区操作虽然很快,但会瞬间释放大量磁盘空间,可能触发文件系统层面的IO抖动,这个抖动如果发生在业务高峰期同样危险,所以即使删除分区也要放在低峰期。

监控与自动化执行体系

靠人工判断低峰期、手动执行索引维护的时代早就该过去了。你需要建立一套自动化的索引维护调度系统,核心逻辑是:持续采集数据库写入负载指标,当指标连续N分钟低于预设阈值时,自动触发预定义的索引维护任务队列。任务执行过程中持续监控负载,一旦超过安全线就自动暂停或回滚。

这个系统的技术栈可以这样搭建:采集层用Prometheus加MySQL Exporter抓取数据库指标,或者直接用云厂商提供的监控API。决策层写一个定时任务脚本,查询过去15分钟的写入速率和活跃线程数,如果低于阈值就标记为可维护状态。执行层把待执行的DDL任务串行化放入队列,通过gh-ost或者pt-osc逐个执行,每个任务执行前再次检查当前负载,执行中实时监控Threads_running和复制延迟。告警层接入企业微信或者钉钉,任务开始、暂停、完成、失败都推送通知。

这里有一个容易被忽略的细节:索引维护任务的优先级需要动态调整。比如一个索引因为碎片化严重导致查询性能急剧下降,那它的维护优先级就应该高于那些只是定期重建的索引。优先级模型可以综合索引碎片率、查询频率、以及该索引关联的业务重要性来打分,分数高的先执行。

云原生环境下的索引维护差异

如果你用的是云数据库,比如阿里云RDS或者腾讯云TDSQL,索引维护的部分底层操作被云厂商封装了,但你不能因此放松警惕。云数据库通常提供了Online DDL的无锁变更能力,但背后仍然是同样的原理,只是云厂商帮你处理了影子表和切换逻辑。你需要关注的是云数据库的维护窗口设置,很多云数据库允许你指定自动维护的时间段,这个时间段默认可能就是凌晨,但正如前面所说,凌晨不一定是写入低峰期。你需要根据自己业务的写入特征手动调整这个窗口。

另外云数据库的只读实例和读写分离架构下,索引维护的主从延迟问题更加复杂。主库执行DDL产生的binlog同步到只读实例时,只读实例需要重放这些操作,如果只读实例的配置低于主库,重放速度跟不上,延迟就会拉大。所以在维护前要确认只读实例的规格是否足够,必要时临时提升配置,维护完成后再降回来。

索引维护的灰度与回滚策略

任何索引变更都必须有回滚能力。新建索引的回滚相对简单,删除即可。但删除索引的回滚就麻烦了,因为删除索引后如果发现查询性能骤降,你需要重新创建,而这个创建过程可能需要几个小时。所以删除索引的正确做法是:先在表中设置一个隐藏周期,比如把索引标记为不可见(MySQL 8.0支持Invisible Index),观察一周业务查询性能没有异常后再真正删除。这个特性很多人不知道,但它是索引维护中最实用的安全机制之一。

-- 将索引设为不可见,查询优化器不会使用它,但索引数据仍然被维护
ALTER TABLE your_table ALTER INDEX idx_name INVISIBLE;

-- 观察一段时间后,确认无影响再删除
ALTER TABLE your_table ALTER INDEX idx_name VISIBLE;  -- 如果需要恢复
DROP INDEX idx_name ON your_table;  -- 确认后删除

对于索引重建操作,回滚意味着恢复到旧的索引结构,这在物理上几乎不可能,因为数据已经变更了。所以重建索引前一定要做好备份,不是全量数据备份,而是表结构加索引定义的备份,至少保证你能在出问题后快速重建一个功能等价但不一定完全相同的索引。更稳妥的做法是在预发布环境用真实数据流量回放来验证新索引的效果,确认查询计划符合预期后再上生产。

索引维护低峰期执行这件事,本质上是对数据库资源调度的精细化管理。它考验的不是你SQL写得好不好,而是你对业务写入模式的理解深度、对数据库内部机制的掌握程度、以及自动化体系的建设水平。把这三个维度都做到位,索引维护就不会再是半夜把人叫起来处理故障的元凶,而是一个安静运行的后台任务,默默保障着查询性能的稳定。