面试原问
"你们线上 MySQL 是怎么扛住流量的?什么时候发现单机扛不住了?你怎么证明瓶颈确实在数据库而不是应用层?"
一、场景与约束
在讨论"MySQL 什么时候到顶"之前,先画一个坐标系------不同业务形态,触顶的姿势完全不同:
| 维度 | 典型值 | 影响 |
|---|---|---|
| 业务类型 | OLTP(短事务、高并发)vs OLAP(长查询、大扫描) | OLTP 先打满连接/锁;OLAP 先打满 IO/CPU |
| 峰值 QPS | 数千~数万 | 决定连接池、线程模型是否够用 |
| 数据量 | 单表百万~亿级 | 决定索引深度、Buffer Pool 命中率 |
| 一致性要求 | 强一致(金融)vs 最终一致(社交) | 决定 sync_binlog/innodb_flush_log 的代价 |
| 成本与组织 | 能否加机器、能否改架构 | 决定是"优化"还是"拆分" |
一句话:没有脱离场景的"MySQL 上限",只有"在你的约束下,哪个资源先不够用"。
二、结论先行
对于典型的 OLTP 业务(电商、SaaS、IM),单体 MySQL 的触顶顺序通常是:
-
连接数 / 线程数先饱和 ------ 应用连接池打满,新请求排队或拒绝。
-
锁竞争导致吞吐坍塌 ------ 热点行更新引发行锁等待链,TPS 断崖式下跌。
-
磁盘 IO 带宽见顶 ------ redo log 刷盘、数据页随机读写打满 IOPS。
-
CPU 被复杂查询或排序吃满 ------ 慢查询堆积,线程全部陷入计算。
-
主从复制延迟不可控 ------ 写入量超过从库单线程回放能力。
注意:这不是绝对顺序。如果你的业务是"大量复杂报表查询",CPU 可能比连接数先到顶。
三、机制与数据路径
3.1 一条 SQL 的生命周期(理解瓶颈的基础)
┌─────────────────────────────────────────────────────────────────┐
│ 客户端 │
└──────────────────────────┬──────────────────────────────────────┘
│ TCP 连接
▼
┌─────────────────────────────────────────────────────────────────┐
│ 连接层(Connection Layer) │
│ ┌─────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ 线程池/连接池 │ │ 认证 & 权限 │ │ max_connections 限制│ │
│ └─────────────┘ └──────────────┘ └───────────────────┘ │
└──────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ SQL 层(Server Layer) │
│ ┌────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │
│ │ Parser │→│ Optimizer │→│ Executor │→│ Query Cache* │ │
│ └────────┘ └──────────┘ └──────────┘ └─────────────┘ │
│ │
│ 瓶颈点:CPU(优化器/排序/临时表) │
└──────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 存储引擎层(InnoDB) │
│ ┌──────────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Buffer Pool │ │ Redo Log │ │ Undo Log │ │ Lock Sys │ │
│ │ (内存缓存页) │ │ (WAL刷盘) │ │ (MVCC) │ │ (行锁) │ │
│ └──────────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ 瓶颈点:IO(刷盘)、锁(行锁等待)、内存(BP 不够大) │
└──────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 磁盘(Disk) │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ 数据文件 (.ibd) │ │ 日志文件 (ib_logfile)│ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ 瓶颈点:IOPS(随机读写)、吞吐(顺序写) │
└─────────────────────────────────────────────────────────────────┘
每一层都可能成为瓶颈,但概率分布不均匀。下面逐一拆解。
3.2 连接数触顶:最常见的"第一枪"
为什么连接数往往最先出问题?
MySQL 的连接模型是"一连接一线程"(thread-per-connection)。每个连接占用约 256KB~1MB 内存,且线程切换有 CPU 开销。
应用服务器集群(10 台)
每台连接池 max=50
│
▼
总连接数 = 10 × 50 = 500
│
▼
┌──────────────────────────────┐
│ MySQL max_connections = 500 │
│ │
│ 实际可用 ≈ 500 - 保留连接 │
│ (super_read_only/监控/复制) │
└──────────────────────────────┘
触顶表现:
-
应用日志出现
Too many connections -
连接池获取超时(HikariCP 报
Connection is not available) -
监控上看到
Threads_connected逼近max_connections
为什么不能简单调大 max_connections?
| max_connections | 内存占用(估算) | 线程切换开销 | 实际吞吐 |
|---|---|---|---|
| 500 | ~500MB | 可控 | 正常 |
| 2000 | ~2GB | 明显 | 开始下降 |
| 5000 | ~5GB | 严重 | 可能比 500 还低 |
这就是经典的"连接数悖论":连接越多,每个连接分到的 CPU 时间片越少,上下文切换越频繁,整体吞吐反而下降。
3.3 锁竞争:OLTP 的隐形杀手
当多个事务同时更新同一行(热点行),InnoDB 的行锁机制会让后来者排队:
时间线 ──────────────────────────────────────────────────►
事务A: ├── BEGIN ── UPDATE row_1 ──────────── COMMIT ──┤
事务B: ├── BEGIN ── UPDATE row_1 (等待A释放锁...) ─────── 获得锁 ── COMMIT ──┤
事务C: ├── BEGIN ── UPDATE row_1 (等待B释放锁......) ──────────────── 获得锁 ── COMMIT ──┤
▲
│
锁等待链越长,延迟越高
当等待超过 innodb_lock_wait_timeout(默认50s),事务回滚
触顶表现:
-
SHOW ENGINE INNODB STATUS中LATEST DETECTED DEADLOCK频繁出现 -
information_schema.INNODB_TRX中大量LOCK WAIT状态事务 -
TPS 曲线呈"锯齿形"------周期性坍塌再恢复
3.4 磁盘 IO:redo log 和数据页的双重压力
InnoDB 的写入路径:
UPDATE 语句
│
▼
┌───────────────────────┐
│ 修改 Buffer Pool 中 │ ← 内存操作,很快
│ 的数据页(脏页) │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ 写 Redo Log Buffer │ ← 内存操作,很快
└───────────┬───────────┘
│
▼ (COMMIT 时)
┌───────────────────────┐
│ fsync Redo Log 到磁盘 │ ← 磁盘 IO!关键瓶颈
│ (innodb_flush_log_at │
│ _trx_commit = 1) │
└───────────┬───────────┘
│
▼ (后台线程,异步)
┌───────────────────────┐
│ Checkpoint:脏页刷盘 │ ← 磁盘 IO!随机写
└───────────────────────┘
两种 IO 压力:
-
顺序写 :Redo Log 刷盘,受
innodb_flush_log_at_trx_commit控制 -
随机写 :脏页刷盘(Checkpoint),受
innodb_io_capacity控制
触顶表现:
-
iostat显示磁盘%util持续 > 90% -
await(IO 等待时间)从 1ms 飙升到 50ms+ -
MySQL 的
Innodb_buffer_pool_wait_free计数器增长(Buffer Pool 没有空闲页,必须等刷盘)
3.5 CPU:被低估的瓶颈
CPU 通常不是第一个到顶的,但一旦到顶,影响面最广:
CPU 消耗来源分布(典型 OLTP):
┌────────────────────────────────────────────────┐
│ 排序 & 临时表(filesort / tmp table) 30% │
├────────────────────────────────────────────────┤
│ 索引查找 & 数据解析 25% │
├────────────────────────────────────────────────┤
│ 锁管理 & 事务协调 20% │
├────────────────────────────────────────────────┤
│ 网络协议解析 & 结果集序列化 15% │
├────────────────────────────────────────────────┤
│ 优化器 & 执行计划生成 10% │
└────────────────────────────────────────────────┘
触顶表现:
-
top显示 mysqld 进程 CPU 使用率 > 90% -
SHOW PROCESSLIST中大量Sorting result/Creating tmp table状态 -
慢查询日志中出现大量
filesort和Using temporary
四、权衡与反例
上面说的"连接→锁→IO→CPU"顺序并非铁律。以下场景会打破这个顺序:
| 场景 | 先触顶的资源 | 原因 |
|---|---|---|
| 报表系统(大量 JOIN + GROUP BY) | CPU | 复杂计算远超 IO 压力 |
| 日志写入(append-only,无更新) | 磁盘顺序写带宽 | 无锁竞争,纯写入 |
| 高并发秒杀(单行扣库存) | 行锁 | 所有请求打同一行 |
| 连接池配置不当(每台应用 max=200) | 连接数 | 10 台应用 = 2000 连接 |
| SSD + 大内存(Buffer Pool 覆盖全部数据) | CPU 或锁 | IO 不再是瓶颈 |
关键认知:硬件升级会改变瓶颈顺序。当你把 HDD 换成 NVMe SSD,IO 瓶颈消失,锁和 CPU 就会暴露出来。
五、失败模式与定界:如何证明瓶颈在 DB?
这是面试的核心考点------不是"我觉得慢",而是"我能证明慢在哪"。
5.1 排除法:先证伪"不在 DB"
用户请求 → 应用服务器 → MySQL
│ │ │
网络延迟? 应用逻辑? DB 处理?
│ │ │
▼ ▼ ▼
┌─────────────────────────────────┐
│ 总延迟 = 网络 + 应用 + DB │
│ │
│ 如果 DB 耗时占总延迟 > 70% │
│ → 瓶颈在 DB │
└─────────────────────────────────┘
定界三步法:
-
应用侧埋点:记录"发出 SQL"到"收到结果"的耗时(排除网络和应用逻辑)
-
DB 侧慢查询日志 :
long_query_time = 0.1,看 DB 自己认为的执行时间 -
对比:如果应用侧 200ms,DB 侧 180ms → 瓶颈在 DB;如果 DB 侧 5ms → 瓶颈在应用或网络
5.2 USE 方法(Utilization / Saturation / Errors)
Brendan Gregg 的 USE 方法是定界的黄金标准:
┌─────────────────────────────────────────────────────────────┐
│ 资源 │ 利用率(U) │ 饱和度(S) │ 错误(E) │
├─────────────────────────────────────────────────────────────┤
│ CPU │ %CPU > 80% │ 运行队列 > 核数 │ — │
│ 磁盘 │ %util > 90% │ avgqu-sz > 4 │ IO错误 │
│ 内存(BP) │ BP命中率 < 95% │ wait_free > 0 │ OOM │
│ 连接 │ 连接数/max > 80% │ 排队等待连接 │ 拒绝连接 │
│ 锁 │ — │ 锁等待事务 > 10 │ 死锁 │
└─────────────────────────────────────────────────────────────┘
判定规则:哪个资源的"饱和度"最先非零,瓶颈就在哪。
5.3 反证法:排除常见误判
| 误判 | 真相 | 如何识别 |
|---|---|---|
| "DB 慢" | 实际是应用 GC 停顿 | DB 慢查询日志无对应记录 |
| "DB 慢" | 实际是网络抖动 | tcpdump 看 TCP 重传 |
| "DB 慢" | 实际是连接池等待 | 应用日志显示"获取连接超时" |
| "IO 瓶颈" | 实际是 redo log 太小导致频繁 checkpoint | Innodb_log_waits > 0 |
六、观测与演练
6.1 关键监控指标
-- 连接数监控
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Threads_running';
SHOW GLOBAL STATUS LIKE 'Max_used_connections';
-- 锁监控
SELECT * FROM information_schema.INNODB_TRX WHERE trx_state = 'LOCK WAIT';
SHOW GLOBAL STATUS LIKE 'Innodb_row_lock_waits';
SHOW GLOBAL STATUS LIKE 'Innodb_row_lock_time_avg';
-- IO 监控
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_wait_free';
SHOW GLOBAL STATUS LIKE 'Innodb_log_waits';
SHOW GLOBAL STATUS LIKE 'Innodb_data_reads';
SHOW GLOBAL STATUS LIKE 'Innodb_data_writes';
-- Buffer Pool 命中率
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read_requests'; -- 逻辑读
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads'; -- 物理读(未命中)
-- 命中率 = 1 - (reads / read_requests)
6.2 操作系统层观测
# CPU 使用率 & 运行队列
vmstat 1 10
# 关注:r(运行队列)、us+sy(CPU使用率)
# 磁盘 IO
iostat -xm 1 10
# 关注:%util、await、r/s、w/s
# 内存
free -h
# 关注:available 是否足够覆盖 Buffer Pool
# 网络
ss -s
# 关注:TCP 连接数、TIME_WAIT 堆积
6.3 InnoDB 状态深度诊断
SHOW ENGINE INNODB STATUS\G
重点关注段落:
-
SEMAPHORES:互斥锁等待(内部锁竞争) -
TRANSACTIONS:活跃事务和锁等待 -
FILE I/O:IO 线程状态 -
BUFFER POOL AND MEMORY:命中率、脏页比例 -
LOG:redo log 写入速度、checkpoint 位置
6.4 压测验证(演练)
# 使用 sysbench 模拟 OLTP 负载
sysbench oltp_read_write \
--mysql-host=127.0.0.1 \
--mysql-port=3306 \
--mysql-user=root \
--mysql-password=xxx \
--mysql-db=sbtest \
--tables=10 \
--table-size=1000000 \
--threads=64 \
--time=300 \
--report-interval=10 \
run
观察重点 :随着 --threads 从 16→32→64→128→256 递增,TPS 在哪个点开始不再增长甚至下降?那个拐点就是你的瓶颈。
七、落地清单
7.1 容量规划规范
| 指标 | 黄线(预警) | 红线(告警) | 动作 |
|---|---|---|---|
| Threads_connected / max_connections | > 70% | > 85% | 扩容连接池或加从库 |
| CPU 使用率 | > 60% | > 80% | 优化慢查询或拆分 |
| 磁盘 %util | > 70% | > 90% | 升级 SSD 或拆分 |
| Buffer Pool 命中率 | < 98% | < 95% | 加内存或优化查询 |
| 行锁等待平均时间 | > 100ms | > 500ms | 优化热点行或拆分 |
| 慢查询数量(/min) | > 10 | > 50 | 紧急优化 |
7.2 发布流程中的 DB 检查
代码发布前:
✅ 新 SQL 必须经过 EXPLAIN 审查
✅ 新索引必须评估对写入的影响
✅ 大表 DDL 必须使用 gh-ost / pt-osc
✅ 预估流量增长是否超过当前容量黄线
7.3 回滚策略
-
慢查询导致的 CPU 飙升 :
KILL对应线程 + 回滚代码发布 -
连接数打满 :紧急调大
max_connections(临时)+ 排查连接泄漏 -
锁等待链过长 :
KILL持锁最久的事务 + 分析业务逻辑 -
IO 打满 :降低
innodb_io_capacity减缓刷盘(牺牲恢复速度换取当前吞吐)
高频追问
Q1:CPU/IO/锁/复制谁先到顶?
答:取决于业务模型。OLTP 高并发短事务 → 连接/锁先到顶;OLAP 复杂查询 → CPU 先到顶;写密集型 → IO 先到顶。没有标准答案,但有标准方法------USE 方法逐一排查。
Q2:如何用延迟、吞吐、饱和度定界?
答:
-
延迟升高 + 吞吐不变 → 某个资源开始排队(饱和),但还没到极限
-
延迟升高 + 吞吐下降 → 资源已过饱和,系统进入"过载坍塌"
-
延迟正常 + 吞吐到顶 → 资源利用率 100%,但没有排队(理想状态,几乎不存在)
找到"饱和度最先非零"的资源,就是瓶颈所在。