云服务器CPU不高但响应变慢时,如何设计故障切换并保障数据一致性?

业务接口开始超时,云服务器 CPU 使用率不高但响应慢,可能是哪些资源出现瓶颈?常见原因包括磁盘 I/O 等待、内存不足或交换、网络接收队列、应用线程池与数据库连接池耗尽、数据库锁等待、日志落盘,以及主备复制确认延迟。CPU 百分比只反映处理器是否繁忙,不能代表请求已经顺利完成。
排查时应先从请求入口定位等待发生在哪一段,再检查主机资源、应用进程、数据库锁与复制状态,最后决定是否摘除节点或执行数据库切换。只有同时确认旧主已经停止写入、备用节点的数据状态满足 RPO、流量入口已经可控,才可以提升备用节点,否则可能形成双主写入或造成无法确认的数据分叉。
先定义切换边界和恢复目标
明确适用环境
本文适用于常见的两层或三层架构:
- 一个或多个无状态应用节点;
- 一个数据库主节点和至少一个备用节点;
- 通过负载均衡、服务发现或固定业务入口切换应用流量;
- 数据库通过同步或异步复制保持备用副本;
- 具备日志、监控、备份和管理员操作权限。
如果登录会话、临时文件或上传文件保存在单台云服务器本地,应用节点切换后可能出现“服务恢复但用户状态丢失”。这类状态应迁移到独立的共享存储或可复制的数据服务中,否则应用层仍然存在单点故障。应用节点高可用不能自动解决本地文件和本地会话的单点问题。
先约定 RPO 和 RTO
RPO 是故障后最多允许丢失的数据时间范围,RTO 是业务最多允许中断的时间范围。两者应在切换前确定,而不是等故障发生后临时决定。
| 目标取向 | 设计重点 | 主要风险 |
|---|---|---|
| 更看重数据完整性 | 使用同步复制或具备明确确认机制的提交策略,切换前检查复制状态 | 复制链路异常时,写入可能阻塞,可用性下降 |
| 更看重快速恢复 | 使用异步复制,在备用节点满足条件时快速提升 | 主节点最后一段尚未复制的数据可能无法恢复 |
| 允许人工确认 | 自动摘除故障应用节点,数据库提升由人工执行 | 恢复时间取决于确认和操作速度 |
| 允许自动切换 | 自动检测、隔离旧主、提升备用、更新流量入口 | 误判、双主和错误提升风险更高 |

同步复制也不等于所有故障场景下绝对零数据损失。还要确认提交确认覆盖哪些副本、备用节点是否真正落盘,以及旧主节点是否已经被隔离。异步复制则必须接受一个事实:主库已经返回成功、但备用节点尚未收到或回放的事务,可能在主库永久失联后无法自动恢复。
切换前至少记录以下状态:
- 故障开始时间和受影响的业务接口;
- 当前主数据库、备用数据库和应用节点;
- 复制模式、复制延迟、最后接收位置和最后回放位置;
- 是否存在长事务、锁等待或大量请求重试;
- 最近一次可用备份及备份校验结果;
- 业务是否允许临时只读或暂停写入。
这些记录用于判断是否满足切换条件,也用于切换后的数据核对和回滚决策。
按由外到内、低风险到高风险的顺序定位瓶颈
1. 先从请求入口拆分耗时
在业务入口或接近用户的一台主机上执行请求耗时测试。以下命令适用于安装了 curl 的 Linux 或 macOS 环境,/healthz 应替换为不会修改业务数据的健康检查接口:

图示对应原文命令:curl。
curl -sS -o /dev/null \
-w 'dns=%{time_namelookup} connect=%{time_connect} appconnect=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n' \
https://业务域名/healthz
重点看各阶段的相对变化:
dns较高,说明名称解析或解析链路需要排查;connect或appconnect较高,说明连接建立、TLS 握手或入口端存在等待;ttfb较高而连接时间正常,通常说明请求已经到达服务端,但应用、数据库或下游服务处理较慢;total明显高于ttfb,可能是响应体生成、传输或客户端读取过程较慢。
健康检查只能证明进程能够返回结果,不能证明数据库写入链路正常。应再选择一个经过鉴权、但可以安全重复执行的业务读请求进行对比,不要把创建订单、扣减库存或修改配置的接口作为首次探测对象。
如果只有一个应用节点的 ttfb 明显偏高,优先将该节点从流量入口摘除并保留现场;如果所有节点的 ttfb 都变高,则继续检查共享依赖、数据库、连接池和网络队列。此时不应仅凭 CPU 使用率作出切换判断。
2. 检查主机等待:磁盘、内存和网络队列
以下命令适用于 Linux:
uptime
vmstat 1 5
free -h
iostat -xz 1 5
df -h
df -i

图示对应原文命令:bash uptime vmstat 1 5 free -h iostat -xz 1 5 df -h df -i 。
iostat 和 pidstat 通常由 sysstat 提供。如果系统未安装这些工具,不建议在业务高峰期临时修改生产环境,优先使用已有监控,或在维护窗口补充工具。
可以按以下方式解释结果:
| 观察结果 | 可能含义 | 处理方向 |
|---|---|---|
vmstat 中的 wa 持续偏高 | 进程在等待块设备或文件系统 I/O | 检查数据库数据盘、日志写入、临时文件和备份任务 |
si、so 持续出现 | 内存不足,正在进行交换 | 查找占用内存的进程,确认是否存在内存泄漏或异常并发 |
| load 较高但 CPU 使用率不高 | 任务可能处于不可中断等待,常见于 I/O 等待 | 结合 iostat、进程状态和磁盘监控判断 |
await、队列长度或设备利用率持续异常 | 磁盘响应或设备队列存在瓶颈 | 先降低非必要 I/O,再检查日志、数据库和临时文件 |
df -h 接近满或 df -i 的 inode 用尽 | 文件无法继续创建或写入 | 清理前确认目录用途,保留备份并准备回滚 |
| 内存、I/O 均正常 | 瓶颈可能在连接、线程、网络或外部依赖 | 继续检查应用和数据库 |
不要依据一次采样就确认磁盘或内存故障。应把采样时间与接口变慢时间对齐,并与正常时段对比。清理日志、删除临时文件或调整服务配置前,要确认文件用途、保留必要备份,并记录可以恢复的路径。
网络队列和接口错误可以继续用以下命令观察:
ss -s
ss -lnt
ip -s link
在 ss -lnt 中,如果监听端口的 Recv-Q 长时间接近或超过队列上限,可能是请求到达过快而应用接收不及时。ip -s link 中的错误包、丢包或异常增长的队列,需要结合云平台监控和应用日志确认。若网络统计正常但请求仍然慢,不要反复重启网络服务;重启会清除现场,也不能解决数据库锁等待或连接池耗尽。
还要区分连接数和请求吞吐量:并发连接数表示某一时刻保持的连接数量,每秒请求数表示单位时间内完成或进入的请求数量,二者不能互相替代。连接数增加可能来自慢请求堆积,也可能来自长连接,必须结合响应时间、连接池等待和请求日志判断。
3. 检查应用进程、线程池和数据库连接池
先确认服务是否频繁重启、是否有超时、文件描述符不足或下游重试:
systemctl status <应用服务名> --no-pager
journalctl -u <应用服务名> --since "30 min ago" --no-pager
以上命令适用于使用 systemd 管理服务的 Linux。<应用服务名> 必须替换为实际服务名,不能直接照抄。
找到进程号后,可进一步观察线程和文件描述符:
pidstat -p -t 1 5
ls /proc//fd | wc -l
重点核对:
- 工作线程是否全部处于等待状态;
- 数据库连接池是否耗尽;
- 请求队列是否持续增长;
- 是否出现大量连接创建和销毁;
- 文件描述符数量是否接近服务限制;
- 是否存在下游调用超时后重试,导致请求数量继续放大。
CPU 使用率低并不代表线程池健康。大量线程可能在等待数据库连接、锁、磁盘或外部接口;连接池耗尽时,新请求会排队,表现为 ttfb 上升而 CPU 仍然不高。
如果只有一个应用节点的线程池或连接池异常,应先将该节点设置为不可接收新请求,等待正在处理的请求结束,再进行重启或修复。重启前保留日志,并确认应用没有本地未提交的关键任务。应用节点摘除不会改变数据库角色,也不应触发数据库提升。
4. 检查数据库锁、I/O 和复制状态
要先区分“数据库执行慢”和“应用拿不到数据库连接”:
- 连接获取超时:可能是连接池不足、连接未释放或数据库拒绝新连接;
- SQL 执行超时:可能与查询计划、磁盘 I/O、锁等待或数据库负载有关;
- 提交超时:可能与事务锁、日志落盘或复制确认有关;
- 只有写请求变慢:优先检查锁、日志盘和复制确认;
- 读写都变慢:同时检查数据库资源、连接数和底层存储。
如果使用 PostgreSQL,以下只读查询可用于查看活动会话。执行前确认账号具备查看统计信息的权限:
SELECT pid,
usename,
application_name,
client_addr,
state,
wait_event_type,
wait_event,
query_start,
left(query, 200) AS query_sample
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY query_start;
如果大量会话的 wait_event_type 为 Lock,应查找持锁时间较长的事务;如果主要等待 I/O,则继续检查数据库数据目录、日志目录和底层磁盘。没有确认事务影响范围时,不要直接终止会话。终止会话属于有影响的数据库操作,应先记录会话信息、评估回滚影响,并准备业务重试或恢复方案。
在 PostgreSQL 主节点上,可以使用以下查询查看复制状态:
SELECT application_name,
client_addr,
state,
sync_state,
sent_lsn,
write_lsn,
flush_lsn,
replay_lsn,
write_lag,
flush_lag,
replay_lag
FROM pg_stat_replication;
在备用节点上,可使用:
SELECT pg_is_in_recovery();
SELECT now() AS checked_at,
pg_last_wal_receive_lsn() AS receive_lsn,
pg_last_wal_replay_lsn() AS replay_lsn,
pg_last_xact_replay_timestamp() AS last_replay_time;
这些查询只适用于 PostgreSQL,其他数据库应使用对应版本的活动会话、锁监控和复制状态命令。判断备用节点能否切换时,不能只看“连接状态正常”,还要确认它是否持续接收并回放数据,以及延迟是否符合业务设定的 RPO。
按故障类型决定是否切换
应用节点故障和数据库主节点故障必须分开处理:
| 故障类型 | 推荐动作 | 是否需要提升数据库 |
|---|---|---|
| 单个应用节点慢或进程异常 | 摘除节点、保留日志、修复后再加入 | 不需要 |
| 多个应用节点同时等待数据库 | 先检查数据库锁、I/O、连接和复制 | 不应立即提升 |
| 数据库主节点进程故障但主机仍可控 | 先确认能否安全恢复,避免备用节点同时写入 | 视恢复结果决定 |
| 数据库主机失联 | 确认旧主已隔离,再评估备用节点复制状态 | 可能需要 |
| 复制链路中断但主库仍在写 | 不应直接提升备用节点 | 先确定数据权威节点 |
| 无法确认旧主是否仍能写入 | 暂停自动提升或进入只读 | 不应强制切换 |
这张表的核心判断是:响应变慢不等于数据库主节点已经失效。锁等待、磁盘队列、连接池耗尽或复制确认变慢,都可能让业务变慢,但直接提升备用节点无法解决根因,反而可能造成数据分叉。
设计安全的故障切换流程
让应用始终只有一个可识别的写入口
应用节点可以多节点接入,但写入数据库的入口必须由数据库角色控制。应用不应把某个固定主机名永久写死为主数据库地址,而应通过角色服务、统一连接入口或可更新的服务发现记录访问当前写主。
健康检查至少应区分两类:
- 存活检查:确认进程仍在运行;
- 就绪检查:确认应用可以访问当前有效数据库,并具备处理业务请求的条件。
如果只检查进程存活,数据库已经不可用的应用节点仍可能被继续分配流量。
按“确认—隔离—提升—验证”执行
- 确认故障范围
先排除网络抖动、锁等待、磁盘暂时繁忙、连接池耗尽等可恢复问题,确认确实需要数据库角色切换。记录当前主库和备用库的复制状态、最后接收位置、最后回放位置、长事务及业务影响范围。
- 隔离旧主
通过云平台控制台、主机电源控制、网络隔离或数据库服务控制,确保旧主不能继续接受写入。隔离方式必须符合当前部署环境,执行前确认影响范围,并保留恢复旧主所需的日志和配置。
在无法确认旧主已经停止写入时,不得直接提升备用节点。此时应暂停自动提升,必要时先让业务进入只读或暂停写入状态。
- 确认备用节点状态
检查备用节点是否仍处于恢复状态、是否持续接收并回放日志,并记录最后接收和回放位置。异步复制场景下,要根据这些位置判断可能的数据缺口,确认是否符合业务 RPO。
- 暂停或限制写入
对一致性要求较高的业务,先进入维护或只读模式,避免切换期间产生无法确认状态的写请求。对于不能完全暂停的业务,应至少限制高风险写操作,并确保请求具备幂等键和审计记录。
- 提升备用节点
以 PostgreSQL 12 及以上版本为例,提升前先核对数据库版本和恢复状态:
SELECT version();
SELECT pg_is_in_recovery();
只有在旧主已经隔离、复制状态和必要备份已经记录后,才考虑执行:
SELECT pg_promote(true);
pg_promote 会改变数据库角色,不是普通查询。执行前应确认影响范围、保留必要备份和复制状态,并准备失败时的恢复路径。执行后不能简单地把旧主再次切回主库;旧主通常需要作为新主的备用节点重新构建或重新同步。
- 更新写入入口
将应用连接指向新主,清理旧连接池,避免已有连接继续指向失效或被隔离的节点。更新服务发现记录、连接配置或角色入口前,应确认配置修改范围,并准备恢复旧入口的回滚操作。
- 逐步恢复流量
先放行少量可控流量,验证读写、错误率和响应时间,再逐步恢复全部流量。若新主出现大量连接、锁或 I/O 等待,应先限制流量,不要在数据库尚未稳定时继续扩大请求量。
- 重建旧主副本
不要让旧主直接重新加入写入集群。应根据数据库复制机制,将旧主从当前权威主重新初始化或重新同步为备用节点,确认复制正常后,再将其加入故障切换候选。
处理异步复制的数据缺口和重试
异步复制下,切换后需要根据业务流水号、审计日志或事件记录核对:
- 切换前最后一段时间内已经返回成功的请求;
- 已扣款但未生成业务单据的记录;
- 已生成订单但未完成后续处理的记录;
- 客户端重试是否造成重复写入;
- 主库和备用库之间是否存在缺失或分叉数据。
不能用简单的“表行数相同”证明一致性。行数相同,仍可能存在不同记录、状态不一致或部分事务缺失。应优先核对业务唯一编号、事务状态、审计事件和关键关联关系。
订单、支付、库存、任务提交等写操作应使用业务请求号或幂等键,并在数据库中建立唯一约束或等价的去重机制。典型处理逻辑如下:
- 请求携带业务唯一编号;
- 业务数据和幂等记录处于同一事务;
- 重试时先查询该编号的处理状态;
- 已成功的请求返回原处理结果;
- 处理中或状态不明确的请求进入人工或异步核对流程。
幂等机制可以降低“客户端认为失败而重试、数据库实际已经提交”造成的重复写入风险,但不能替代数据库复制、旧主隔离和切换后的数据核对。
修复或切换后的验证方法
切换或资源修复完成后,应按以下顺序验收:
- 入口验证
再次执行安全健康检查和业务读请求,确认 ttfb 与总耗时回到正常基线。健康检查通过但业务读请求仍慢,说明应用或共享依赖仍未恢复。
- 应用验证
检查 5xx、超时、连接池等待、线程池队列和服务重启次数,确认这些指标没有持续增长。
- 主机验证
重新执行 vmstat、iostat、free、df,确认 I/O 等待、交换、磁盘空间和 inode 没有继续恶化。采样结果应与正常时间段对比,不能只看单次值。
- 数据库角色验证
确认只有一个节点接受写入,旧主仍处于隔离状态或已经重新作为备用节点加入。应用连接池和服务发现记录不能再指向旧写主。
- 复制验证
确认新主到备用节点的复制已经建立,备用节点持续接收并回放数据,延迟处于既定 RPO 范围内。
- 业务验证
使用测试或低风险业务完成一次读写闭环,检查幂等键、流水号、审计日志和关键状态。对切换窗口内的成功请求、超时请求和重试请求分别核对,不要只验证一个普通查询。
- 备份验证
确认新主已经纳入备份任务,并能找到切换后的有效恢复点。备份任务“已配置”不等于备份“可恢复”,应按既定流程检查备份结果和恢复记录。
自动切换不应只依据“主机 Ping 不通”。更稳妥的自动化条件至少包括:旧主已经完成隔离、备用节点状态有效、复制延迟符合策略、写入入口已经切换,以及切换后的健康检查能够验证数据库角色和应用可用性。
常见失败情况与回滚检查
切换后发现旧主仍可写
立即停止两侧写入或暂停业务写请求,保留两侧日志和数据库状态。不要直接把两边数据合并,也不要通过覆盖备份文件解决。应先确定哪一侧是业务权威数据源,再依据事务日志、业务流水号和审计记录恢复另一侧。
备用节点复制延迟过大
如果延迟超出业务允许的 RPO,不应自动提升。可以选择继续恢复旧主、临时只读,或在业务负责人确认数据损失范围后执行人工切换。没有明确授权时,不要把“快速恢复”当作允许丢数据的依据。
切换后应用仍然慢
检查应用是否仍缓存旧数据库地址、连接池是否未重建、读请求是否仍指向故障节点,以及新主是否受到大量重试冲击。必要时先限制流量,再重新检查数据库连接、锁、I/O 和复制状态。
误切换后需要恢复原主
数据库角色回滚不能简单执行“切回旧主”。如果新主已经产生写入,旧主与新主可能处于不同时间线。通常应按以下顺序处理:
- 暂停或限制写入;
- 确认当前权威主及其最后有效位置;
- 将原主隔离并作为待重建节点;
- 从当前权威主重新建立备用副本;
- 复制验证通过后,再把原主加入故障切换候选。
涉及删除数据目录、重新初始化副本或覆盖配置的操作,都必须先完成备份、确认影响范围并保留恢复路径。验收结束后逐项确认:
- [ ] 只有一个数据库节点接受写入;
- [ ] 应用入口已经指向正确的写主;
- [ ] 旧主已经隔离,或已重新加入为备用节点;
- [ ] 复制状态、复制延迟和备份任务正常;
- [ ] 关键业务流水号和幂等记录已经核对;
- [ ] CPU、I/O、内存、连接池和请求耗时恢复到正常基线;
- [ ] 已记录本次切换触发条件、执行时间、数据核对结果和回滚步骤。