小Cの已经记不起来的博客

一次线上 分库分表 故障的复盘记录

背景

我们订单库是 4 库 × 4 表,按 user_id 取模分片,中间件用的 ShardingSphere-JDBC。平时查询、更新都带 user_id,上线快一年没出过大的幺蛾子。结果上周四下午,一个历史数据修复任务把整个订单库差点拖死。

故障现象

14:07 开始告警,订单查询接口大量 500,P99 直接拉满。应用日志里一片:

HikariPool-1 - Connection is not available, request timed out after 30000ms.

第一反应是数据库崩了?登上监控看,MySQL CPU 没满,磁盘也没炸,但是 16 个分片的连接数都顶到了上限,活跃连接里全是同一条 SQL。

慢日志里也是它:

UPDATE t_order
SET status = 3, update_time = NOW()
WHERE status = 1
  AND update_time < DATE_SUB(NOW(), INTERVAL 30 DAY);

这条 SQL 没带 user_id。在分库分表里,没带分片键的 UPDATE 会被广播到所有分片。ShardingSphere 日志里 Actual SQL 打了 16 条,ds_0 到 ds_3 的每个 t_order_0 到 t_order_3 都跑了一遍。说实话,平时看日志知道有广播路由,但真到线上看到 16 条 Actual SQL 一起锁表,感觉还是不一样。

排查过程

先别急着找根因,先止血。我们第一时间去 xxl-job 后台把 orderRepairJob 停了,不然任务重试还会继续打。这里有个小坑:一开始只停了一台实例上的本地定时任务,后来发现调度中心还有重试,等于没停干净...

停任务后开始看连接。在每台 MySQL 上执行:

SHOW FULL PROCESSLIST;

SELECT id, db, command, time, state, LEFT(info, 120) AS sql_text
FROM information_schema.processlist
WHERE info LIKE 'UPDATE t_order%';

能看到不少会话卡在 updating,还有一些在等锁。当时不敢一把梭全 kill,先 kill 掉非核心业务里等得最久的会话,再观察连接数。这里说真的,kill 完不是立刻就好,InnoDB 回滚也要时间,有一两个分片回滚了差不多几分钟。

应用侧也做了降级,把订单非核心接口先限流,避免新请求继续把连接池占满。等连接数下来后,再把应用实例分批重启,连接池才慢慢恢复。

根因

根因不复杂,甚至有点蠢:

  1. 修复任务确实写了时间范围,但条件还是太宽,30 天前的 status = 1 订单量比预想大很多。
  2. SQL 没带 user_id,ShardingSphere 直接广播到 16 个分片,每个分片都在更新和锁行。
  3. 任务本身没有分批,也没有分布式锁。调度重试加上重复触发,等于把同一条 SQL 又补了几刀。
  4. 订单接口和这个任务共用一个连接池,任务把连接占满,正常请求只能干等,最后 HikariPool 超时。

修复方式

止血完成后,我们把任务改成了按 user_id 分批处理。先查出一批需要修复的用户,再按用户维度循环更新,每批 200 个用户,批次之间 sleep 一下。伪代码大概这样:

List<Long> userIds = orderRepairMapper.selectRepairUserIds(lastId, 200);
for (Long userId : userIds) {
    orderRepairMapper.updateByUserId(userId);
    lastId = userId;
}

对应 SQL 必须带分片键:

UPDATE t_order
SET status = 3, update_time = NOW()
WHERE user_id = #{userId}
  AND status = 1
  AND update_time < DATE_SUB(NOW(), INTERVAL 30 DAY);

另外给这个任务单独配了一个小连接池,加了 Redis 分布式锁,调度中心只允许单实例执行。重试次数也从 3 次改成了 0 次,失败就告警,人工确认后再跑。

后续改进

这次之后我们加了几个比较土但有用的限制:

复盘写到这里,最想提醒自己的是:分库分表不是配完中间件就完事了,最危险的往往不是正常业务 SQL,而是那些临时修复、跑批、补数据。它们看起来只是“跑一下”,但没带分片键的时候,跑的可是全部分片。

评论

还没有评论。

发表评论

提交后评论将经过自动审核,审核通过后公开展示。

未在播放