一天下午,负责订单域的洪世贤正在核对发布单上的操作项,马上就要点下确认。突然,他接到另一个团队的技术负责人文彦打来的电话:两边共同维护的库存服务“品如珊”的写接口开始大面积超时。十分钟后,两个人在应急群里的动作变成一条直线——先暂停发布,再查数据库锁等待,确认根因后统一恢复写入。这种处理过程,才是跨团队故障里最需要的“统一战线”:不是几十个人在群里各说各话,而是一套所有人都看得懂的启动流程和决策次序。
这篇博客会围绕一个典型场景展开:多个团队共同维护的核心服务或核心表,在运行过程中被长事务、锁等待或连接池占满“卡住”。我会把它拆成六个部分:故障为什么难查、响应基础如何提前准备、最小复现怎么搭、关键参数怎么调、常见误判怎么规避、最后怎么变成一套预防规范。
1. 先理解“品如珊”这类核心依赖为什么会变成事故中心
1.1 多团队共享资源,天然存在“定位盲区”
这里把“品如珊”当一个内部项目代号。它可以是一张订单表、一个库存服务、一个 Redis 锁,也可以是一段所有业务都依赖的消息队列。它的特点很明确:不是某个团队的私有资源,而是多个团队都在写入和读取的公共依赖。
单团队维护私有服务时,出了问题只要查自己的日志、自己的发布记录,通常很快能定位。多团队共享资源时,问题就复杂了:
- A 团队认为自己只是发了一个普通批量任务,不会影响线上。
- B 团队发现自己调用“品如珊”超时,但看不到 A 团队会话在做什么。
- DBA 能看到数据库锁等待,但不知道哪个业务动作触发了长事务。
- 值班工程师互相拉群,确认“是不是你们那边在压测”这类问题,比状态排查本身花的时间还多。
“品如珊”这类依赖一旦被锁住,影响是扇形的:调用方超时,超时触发重试,重试加重连接池压力,最终多个团队的系统会同时出现异常。到这个时候再去分辨“谁先开始的”已经没有意义,需要做的是快速形成统一动作。
1.2 “被锁住”和“进程挂掉”是两种完全不同的故障
很多团队在响应初期会走弯路,是因为把资源被锁当成进程崩溃来处理。两者表现相似,但底层逻辑不同。
进程崩溃的典型特征:
- 端口长时间无响应。
- 应用日志中大量连接拒绝、线程池拒绝。
- 重启或重新发布后,进程恢复,问题通常消失。
资源被锁的典型特征:
- 进程还活着,端口也通着,CPU 可能不高。
- 部分接口超时,部分请求能成功,响应极不稳定。
- 重启应用后短期看似恢复,大量重试流量一进来又超时。
- 真正的问题在数据库层或中间件层,不在应用进程本身。
这两种情况经常被混为一谈。遇到“品如珊”大面积超时,第一反应应该是确认它处于什么状态,而不是盲目扩缩容或重启。
下面这张表可以帮助核心依赖负责人快速区分当前故障属于哪一种:
| 判断维度 | 进程崩溃或机器异常 | 资源被锁或连接被占 |
|---|---|---|
| 进程状态 | 进程退出,端口不可用 | 进程存活,端口可通 |
| 资源使用率 | CPU、内存、磁盘可能飙升或掉零 | 通常不突出,连接数可能打满 |
| 应用日志 | 连接拒绝、初始化失败 | 超时、锁等待、连接池耗尽 |
| 重启效果 | 通常能恢复 | 短暂恢复后可能再次超时 |
| 恢复手段 | 重启、扩容、回滚发布 | 需要定位持有方,解除锁和连接占用 |
遇到第二种情况时,不能简单重启。
1.3 为什么跨团队故障必须“统一战线”
跨团队故障最怕的不是技术复杂,而是动作互相冲突。一个团队判断应该扩容,另一个团队判断应该回滚,第三个团队正准备执行定时清理任务。如果没有统一决策,任何操作都可能把现场“二次破坏”。
“统一战线”在技术上的表现,是三件事:
- 统一目标:先把影响面控制住,而不是先追究谁的责任。
- 统一动作:所有团队暂停发布、停止批量任务,只允许一个人下达恢复指令。
- 统一状态源:所有人员看同一个告警台、同一个状态页、同一份锁等待视图。
这套机制不是等到电话打完才临时拼出来的,而是要提前定义好响应角色和通知路径。否则紧急情况下来来回回确认“现在到底谁负责”,会浪费掉最宝贵的几分钟。
2. 想在紧急来电中找到方向,先建好统一的响应基础
2.1 告警分级,不把每个信号都当成最高级别
很多团队在接入监控时,把所有文件都配成“电话加急”。结果是值班人员一天收到几十个电话,时间久了形成告警疲劳,真正严重的“品如珊被锁死”反而被忽略。
建议将告警按影响范围分成三级,并把每级的触发条件、通知方式和响应时限提前写清楚。
| 级别 | 典型触发条件 | 通知方式 | 响应时限 | 负责人 |
|---|---|---|---|---|
| L1 重大故障 | 核心服务不可用、核心数据不可写、用户交易失败率明显上升 | 电话 + 群内 @ 所有人 | 5 分钟内建立应急群 | 值班主管 |
| L2 功能受损 | 部分接口超时、连接池水位持续上升、次要模块异常 | 群内通知 + 值班人确认 | 15 分钟内完成评估 | 模块负责人 |
| L3 资源预警 | CPU、内存、磁盘、队列堆积超过阈值但未影响业务 | 群内通知 | 当天处理 | 对应团队 |
按这个分级,“品如珊”出现大面积写超时,至少是 L2 级别。出现库存扣减失败、订单状态无法更新,则直接升级为 L1。
2.2 共享状态页,比微信群排着队发消息更可靠
微信群里很容易出现信息互相覆盖。A 团队发了一条推断,B 团队转发一条日志,C 团队贴了个截图,后续的人要花很多时间才能拼出完整时间线。
推荐的做法是使用一个共享状态页,不管是维基页面、在线表格,还是专门的事件管理工具。状态页中必须固定包含以下字段。
| 字段 | 填写说明 | 示例 |
|---|---|---|
| 发生时间 | 首个异常告警的时间 | 2025-01-06 14:03 |
| 当前状态 | 排查中 / 已恢复 / 待复盘 | 排查中 |
| 影响范围 | 哪个服务、哪个接口、影响哪些业务 | 库存写接口超时,订单扣减失败 |
| 责任团队 | 负责人名字,而不是团队名 | 值班负责人:洪世贤 |
| 根因进度 | 当前发现了什么、正在查什么 | 正在查长事务和锁等待 |
| 恢复时间 | 实际恢复时间点 | 待填写 |
状态页的更新频率要固定,比如每 10 分钟更新一次。即使在最紧张的时候,也要留出一个人专门维护这个页面,让后来参与的人能快速了解现场。
2.3 一个最简单的统一通知脚本模板
如果你们的监控系统已经能发群通知,可以直接跳过这一步。如果团队刚开始建设,可以先用一个很小的脚本把监控事件推到群机器人,把第一版“电话链”改成机器通知。
#!/usr/bin/env bash # 统一告警转发示例:接收监控系统传入的事件文本 MESSAGE="${1}" curl -sS -X POST 'https://your-webhook.example.com/hook/xx' \ -H 'Content-Type: application/json' \ -d "{\"msgtype\":\"text\",\"text\":{\"content\":\"${MESSAGE}\"}}"调用方式可以非常简单:
./notify.sh "[L1] 品如珊写接口超时率 60%,请立即进应急群"也可以使用 Python 生成更结构化的负载,方便后续接状态页:
import requests payload = { "title": "品如珊", "severity": "critical", "summary": "共享表锁等待超过 5 秒,连接池水位 90%", "start_time": "2025-01-06T14:03:00+08:00", "responsible": ["团队A-值班", "团队B-值班"], "page": "https://state.example.com/incident/12", } requests.post( "https://your-webhook.example.com/hook/xx", json=payload, timeout=3, )这段代码的重点不是发送本身,而是它要求监控系统在告警时就把影响范围、开始时间和负责人一起带出来,而不是只甩一句“CPU 超过 90%”。收到这种结构化通知的人,不需要再去翻监控平台才能判断是否进入紧急状态。
3. 最小复现:共享核心表被长事务锁死后的排查与恢复
3.1 场景定义和表结构
假设“品如珊”对外的核心数据存储是 MySQL 中的一张库存表。两个团队在操作它:
- A 团队正在跑一个批量盘点任务,逐条更新库存数量。
- B 团队负责订单扣减,每次下单都会更新同一行库存。
表结构可以简化成这样:
CREATE TABLE `t_stock` ( `id` bigint NOT NULL AUTO_INCREMENT, `item_no` varchar(32) NOT NULL COMMENT '商品编码', `available_qty` int NOT NULL DEFAULT '0' COMMENT '可用库存', `version` int NOT NULL DEFAULT '0' COMMENT '版本号,用于幂等或乐观锁', `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_item_no` (`item_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表';这个结构里,item_no是唯一的商品编码。多个团队如果同时更新同一商品,InnoDB 会在行锁层面发生等待。
3.2 用两个会话复现“被锁住”
在测试环境开两个连接,模拟 A 团队的长事务。
第一个连接代表 A 团队,先开启事务并更新一行,然后故意保持不提交:
-- 会话 A:模拟批量任务中的长事务 BEGIN; UPDATE t_stock SET available_qty = available_qty - 10 WHERE item_no = 'ITEM001'; -- 这里模拟长事务,先不提交 -- 实际生产环境里,可能是出现了一个异常路径,事务没提交就被挂住了 SELECT SLEEP(20); COMMIT;第二个连接代表 B 团队,更新同一行库存:
-- 会话 B:订单扣减 UPDATE t_stock SET available_qty = available_qty - 1 WHERE item_no = 'ITEM001';此时第二个连接会一直卡住,不会立即返回。原因是它请求的这行数据上的排他锁仍被第一个连接持有,InnoDB 默认会等待锁释放,直到innodb_lock_wait_timeout到期。
3.3 检查链路:发现阻塞源在哪
第一个动作是查看当前进程列表。登录数据库执行:
SHOW FULL PROCESSLIST;正常情况下会看到第二个连接状态处于“Updating”或“Waiting for table metadata lock”附近,Info 字段是那条卡住的 UPDATE。
接下来查看当前运行中的事务:
SELECT * FROM information_schema.INNODB_TRX\G;这条查询会返回trx_started、trx_query、trx_state、trx_mysql_thread_id、trx_rows_locked等关键信息。如果发现某个事务启动时间很早,而且一直显示RUNNING,大概率就是阻塞源头。
如果 MySQL 开启了 sys 库,可以用专门视图直接查看锁等待关系:
SELECT * FROM sys.innodb_lock_waits\G;这个视图会把等待事务和阻塞事务一起展示出来,字段包括waiting_trx_id、waiting_pid、blocking_trx_id、blocking_pid。在高并发场景下,它比人工对比INNODB_TRX快得多。
3.4 统一恢复动作先于技术手段
在真正执行 KILL 之前,团队要先统一一个动作:由同一个人决定是终止等待方,还是终止持有长事务的阻塞方。
一种常见做法是先终止一直卡住的等待方,让应用不再堆积请求:
-- 注意:生产环境执行前要确认当前 session 信息 KILL <waiting_thread_id>;如果阻塞方是一个真正失控的长事务,则可能需要终止它:
-- 由 DBA 确认后执行 KILL <blocking_thread_id>;这里有一个非常重要的细节:不要一开始就把进程列表里看起来“异常”的连接全部杀掉。如果杀掉的不是真实阻塞源,只是另一个团队正在执行正常任务的连接,反而会让故障范围扩大。正确顺序是:
- 先用
sys.innodb_lock_waits确认等待方和阻塞方。 - 确认阻塞方对应的业务动作是什么。
- 由事件负责人决定终止哪一侧。
- 执行 KILL 后观察连接池水位是否回落。
- 确认恢复后,再通知调用方放量。
4. 锁死、超时、连接池,这些参数怎么调才能稳
4.1 关键参数速查表
下面这些参数和“品如珊被锁住”的场景直接相关,也是一线值班人员最容易接触到的配置。
| 参数 | 默认值 | 含义 | 调大的影响 | 调小的影响 |
|---|---|---|---|---|
innodb_lock_wait_timeout | 50 秒 | InnoDB 事务等待行锁的超时时间 | 长事务等待更久,接口长时间挂起 | 快速报锁超时,误伤正常等待 |
lock_wait_timeout | 86400 秒 | 元数据锁等待超时 | 等待释放时间更长 | 更容易提前失败,避免长时间卡住 |
max_connections | 151 | MySQL 最大连接数 | 可容纳更多会话,但占用内存 | 连接不足,应用直接报拒绝 |
wait_timeout | 28800 秒 | 非交互连接的空闲超时 | 闲置会话保留更久 | 会话更容易被断开 |
| Hikari 最大连接数 | 10 | 应用侧连接池最大连接数 | 能并发更多数据库会话 | 高并发时等待连接 |
| Hikari 连接超时 | 30000 毫秒 | 从连接池获取连接的等待时间 | 应用层等待更久 | 快速失败,触发重试 |
这些参数单独看都容易理解,但真正排查时,它们会组合成一个现象链:连接池占满,是因为 SQL 长时间不返回;SQL 长时间不返回,是因为行锁被长事务持有;而长事务一直没结束,可能是因为某个团队写了一个未提交的异常分支。
4.2 把超时时间调大,不等于解除故障
很多团队在第一次遇到锁等待时,会立刻修改innodb_lock_wait_timeout,希望让 SQL 多等一会儿。这个动作只能缓解症状,不能解决根因。
如果锁本来会被释放,调大超时时间确实能让业务平滑一些。如果锁永远不释放,比如事务因为程序 bug 没提交,调大超时时间只会让大量请求全部排队等待,最终把连接池耗尽,造成整个应用不可用。
推荐的做法是:
- 保持锁等待超时在一个合理范围,比如 5 到 30 秒,根据业务特性调整。
- 遇到持续超时,第一时间查锁等待关系,而不是先调参数。
- 应用侧获取连接超时不能设置得过长,否则调用方线程全部阻塞。
4.3 学习环境与生产环境差别
学习环境可以直接用命令观察锁等待现象,生产环境则需要更谨慎。
| 配置项 | 学习环境 | 生产环境 |
|---|---|---|
| 锁等待超时 | 可临时调小为 5 秒,方便复现 | 不随意修改,需走配置评审 |
| 长事务模拟 | 可用SELECT SLEEP模拟 | 禁止模拟,用只读快照观察 |
| 连接池大小 | 随意调整测试 | 根据压测和慢日志评估 |
| KILL 操作 | 可以随意执行 | 需要身份确认和变更记录 |
| 监控指标 | 可有可无 | 锁等待、慢查询、连接数必须覆盖 |
如果现场已经发生故障,生产环境不要先去改一堆参数。最稳妥的路径是先找到阻塞源并解除,再复盘参数是否需要调整。否则,改参数本身也会变成一次未知变更,给故障恢复增加新的不确定性。
5. 常见误判:核心资源被占,别急着重启扩缩容
5.1 最常见的三条错误动作
第一个误判是看到应用日志超时,就认为是慢 SQL 导致 CPU 飙高。实际上,锁等待时 CPU 可能很低。尤其是在 InnoDB 行锁等待期间,事务只是被动等锁,并不消耗大量 CPU。此时盯着 CPU 看,什么问题也看不出来。
第二个误判是重启应用。重启后,应用连接池被重建,很多之前积压的请求会丢失,表面看起来“恢复了”。但数据库层的长事务可能还在,或者锁还没释放。调用方一旦继续重试,新建立的连接又会全部排进锁等待队列,超时现象马上重现。
第三个误判是盲目加大连接池。连接池变大之后,同一时间会有更多会话去打锁等待,数据库连接数会被迅速占满。更大的连接数还可能挤压 DBA 用于诊断的连接,导致排查工具都连不上库。
5.2 按通路排查,不要跳步
遇到“品如珊”这类核心依赖异常,排查顺序优先级如下:
- 确认资源是否被占:用
SHOW FULL PROCESSLIST看 Session 状态。 - 确认是否有长事务:查
information_schema.INNODB_TRX。 - 确认锁等待链路:查
sys.innodb_lock_waits或performance_schema。 - 确认连接池水位:看应用指标,确认线程是阻塞在等待连接还是等待锁。
- 确认业务源头:长事务到底来自哪个应用节点、哪个接口、哪个批量任务。
- 确认变动历史:最近 10 分钟内是否有发布、批处理任务或慢日志激增。
其中最容易被忽略的是第 5 步。数据库只能看到连接来自哪个 IP,不一定能直接看到接口名。需要应用团队提供运行时线程堆栈,才能把数据库信息和业务代码对应起来。
5.3 排错速查表
| 现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 接口超时但 CPU 很低 | 应用线程阻塞在锁等待或连接池等待 | 看线程堆栈、Hikari 活跃连接、SHOW FULL PROCESSLIST | 定位锁等待链,终止阻塞事务 |
| 某条 UPDATE 长时间不返回 | 行锁被长事务持有 | INNODB_TRX+sys.innodb_lock_waits | 确认阻塞事务业务归属后再处理 |
| 重启应用后马上又堵 | 数据库层长事务未结束 | 重启前查锁等待状态 | 先解除数据库阻塞,再重启应用 |
| 连接池数持续上涨 | 请求排队等待锁,线程池与连接池同时占满 | 看应用线程状态和 DB 活跃会话 | 先限流或摘节点,再查阻塞源 |
| 大量连接被拒绝 | max_connections打满 | show status like 'Threads_connected' | 找到占用连接最多的会话,避免直接调大 |
这张表的核心思路是一致的:出现大面积超时,先判断请求是“没到数据库”还是“到了数据库但等着锁”。这两种情况的处理方式完全不同。
6. 从应急协作变成预防规范,避免每次都靠“临时统一战线”
6.1 跨团队应急角色和责任要提前定义
“两边马上统一动作”很理想,但如果没有提前定义角色,遇到紧急情况时就会出现多人同时发指令的局面。建议在每个核心依赖中都提前定义一个最小应急角色表。
| 角色 | 负责内容 | 建议人选 |
|---|---|---|
| 事件负责人 | 下达统一动作、决定是否暂停发布、对外同步状态 | 值班主管或技术负责人 |
| 数据操作人 | 执行只读查询、确认锁等待、在授权范围内执行 KILL | DBA 或有库权限的工程师 |
| 应用定位人 | 回溯应用日志、提供线程堆栈、确认业务动作 | 各调用团队负责人 |
| 状态记录人 | 维护状态页和事件时间线 | 值班助理或后加入的工程师 |
这个角色表不需要写成公司制度那么重,但要在应急文档里写清楚:谁有一锤定音的权力。跨团队协作中最怕两个团队各派一个负责人,两个人意见不一致,现场执行的人不知道该听谁的。
6.2 发布前检查清单,把故障挡在变更之前
大部分“品如珊”被锁死的事故都源于变更:批量任务上线、数据订正、依赖版本升级、缓存预热脚本。建议每次发布前都跑一遍下面这个清单。
- 本次变更是否会影响共享表或共享服务,是否有团队成员同时在操作同一份数据。
- 是否有长时间批量更新任务,任务是否会长时间持有事务。
- 是否存在循环调用或重试机制,重试上限是否设置合理。
- 关键 SQL 是否走索引,是否可能产生全表扫描或大范围锁。
- 是否有回滚方案,数据订正脚本是否先备份了原始数据。
- 是否确认了监控告警责任人,异常时能联系到人。
- 是否需要提前申请数据库查询权限或紧急变更权限。
这份清单不是流于形式的打钩表。它的真正价值是让发布人意识到:你不是在改一段自己的代码,而是在动一个所有团队都依赖的共享底座。
6.3 复盘文档要有时间轴和 Action Item
故障恢复后,复盘文档不是用来追责的。建议采用下面的结构,把时间和动作精确记录下来。
| 时间 | 事件 | 负责人 | 影响 |
|---|---|---|---|
| 14:03 | 品如珊写接口超时率上升 | 监控告警 | 订单扣减开始失败 |
| 14:05 | 进入应急群,状态页建立 | 洪世贤 | 确定统一动作 |
| 14:10 | 确认存在长事务,锁等待链路可见 | DBA | 暂不执行 KILL |
| 14:16 | 确认长事务来自批量盘点任务 | 文彦 | 暂停批量任务 |
| 14:20 | 终止阻塞会话,连接池开始回落 | DBA | 部分请求恢复 |
| 14:26 | 链路验证通过,恢复正常写入 | 洪世贤 | 影响消除 |
复盘正文至少回答四个问题:
- 根因是什么,是代码逻辑问题、人工操作问题,还是依赖导致。
- 为什么没有提前发现,监控盲区在哪。
- 恢复过程中有没有新的误判或延迟决策。
- 后续要补齐哪个监控指标、哪段自动化恢复脚本。
6.4 工程层面的彻底规避
除了应急协调和复盘,工程上也有一些硬手段可以降低“品如珊”被锁死的概率。
第一,拆分核心表或分库。如果一张表同时支撑多个团队的多个业务场景,拆成按业务域隔离的表,能从根源上减少锁竞争。
第二,批量任务低峰执行,并且分批提交。不要在业务高峰用一条大事务更新核心表,尽量按主键分批,每次只提交一小批。
第三,所有数据库 UPDATE 都走短事务。事务里不要夹杂远程调用、文件读写、外部接口同步等慢操作。锁的持有时间越短,锁等待概率越低。
第四,有限重试,而不是无限重试。调用方遇到超时,要退避重试,并设最大重试次数。无限重试会把一次锁等待放大成连接池雪崩。
第五,使用 Online DDL 或低峰期变更。需要变更大表结构时,不要直接ALTER TABLE长时间持有元数据锁,尽量使用在线变更工具。
第六,定期做故障演练。在测试环境故意开一个长事务,让值班团队用标准 SQL 查锁、定位、恢复,把“看到锁等待不紧张”变成一种基本技能。
真正让团队放心的不是一套完美的应急预案,而是在每一次核心依赖告警时,大家都能马上知道该看什么、不该做什么。跨团队协作里的“统一战线”,不应该等到事发后才开始谈,而应该提前固化成一章简短、清晰、可执行的应急文档。