Skip to content

服务接口耗时突然暴增的可能原因

约 2715 字大约 9 分钟

经验

2025-05-20

工作这么多年, 见过太多妖魔鬼怪, 最常见的问题之一就是: XXX, 你的接口咋这么慢? 下面是我总结的一些可能原因

业务开发层面

第三方依赖服务的原因

在日常开发中, 我们本身也会以来一些第三方的服务接口, 比如 身份认证服务 , GPS位置解析 等, 如果这些服务接口变慢, 那么我们的服务接口也会变慢。

解决方法

第三方服务的问题, 对于我们自身来讲, 严格意义上来说, 是无解, 因为服务孔子全部在我们自己手里, 只能被动等待第三方服务供应商优化. 但是, 我们可以通过业务流程的手段, 弱化这种超时带来的影响.

  • 判断依赖接口是否是核心链路上的接口: 若是, 如身份认证, 没有太好办法, 只能慢慢等, 或者超市失败, 让客户端重试.
  • 不是核心依赖, 如GPS解析, 设置一个合理的超时时间, 失败便失败了, 等待什么时候恢复了即可正常访问.
  • 数据缓存: 对于第三方读接口, 如果接收数据延迟或者一定程度的不一致性, 我们可以适当缓存第三方结果, 不是每次都调用, 有缓存时直接走缓存.

数据库慢查询

数据库慢查询由于SQL执行变慢也会导致接口变慢

解决方法

  • 查看导致慢查询的SQL, 是否是没有索引, 若是, 添加合适索引即可解决.
  • 查看导致慢查询的SQL, 应该可以命中索引, 但是 通过 EXPLAIN 发现索引没生效, 参见文章: MYSQL索引失效常见场景 来进行排查, 个人经验, 比较常见的是隐式类型转换导致
  • 查看导致慢查询的SQL, 查询条件一切正常, 并且可以命中索引, 但是依旧慢查询, 可能存在如下情况
    • 索引区分度太低, 导致无法有效过滤数据, 如用户信息在性别字段建立索引, 只能过滤一半数据, 索引监理师需要有合适的区分度
    • 翻大页, 这种常见于各种管理后台, SQL形如 select * from people limit 10 offset 100000 order by id desc , 注意, 这种sql, 不是从 100000 开始向后查 10 条, 而是直接查询 100010 条, 将最后 10 条返回. 可以将翻大页的模式调整为 游标模式 , 每一次查询都将上一次的最小ID作为参数传入, 可以使用主键索引.

不合理的全局锁(公共锁)

以golang为例, 很多并发操作为了并发安全或者某些操作串行化, 会使用全局的锁, 但是锁定逻辑的不合理, 可能会导致锁的持有时间过长, 后续操作陷入锁等待, 简单示例如下:

var globalLock *sync.RWMutex
func main() {
  
}

// 假定某一个接口请求会调用这个方法
func handle() {
  globalLock.Lock()
  defer globalLock.Unlock()
  // logic 1
  // logic 2
  // logic 3
}

像上述代码, 每次请求都会加锁, 并且锁定的范围是整个方法, 基本等于 当前逻辑完全串行处理, 无并发度可言 , 解决方法如下:

解决方法

  • 评估是否有必要加锁, 非必要不加锁
  • 评估是否必须加写锁(写锁互斥, 一旦有写锁无法加其他锁), 不是必须用写锁, 可以使用读锁, 读锁是可以反复加的
  • 评估锁的范围, 锁的范围越小, 锁的持有时间越短, 并发度越高, 尽可能按需最小范围加锁
  • 是否必须使用全局锁, 函数局部的是否足够, 足够的话, 不要使用全局锁
  • 必须使用全局锁, 也不需使用写锁, 还可以将锁实例hash化, 本质上也是降低数据锁定的范围

死锁

严格来讲, 这个属于BUG, 但是也属于可能导致的问题, 一般来说, 死锁是可以通过代码避免的, 但是如果出现死锁, 会导致服务不可用, 所以需要及时发现并解决.

解决方法

  • 通过pprof 可以查看当前的goroutine情况, 可以查看是否有死锁, 一般来说, 死锁可以通过代码避免, 但是如果出现死锁, 会导致服务不可用, 所以需要及时发现并解决.
  • GO工具链 -race 可以进行静态检测, 实用工具链排查

队列的不当使用

此处的队列是指内存的队列, 而非 kafka、redis 等中间件提供的消息队列服务。一般而言, 内存的队列为了防止数据野蛮扩张, 会限制队列容量, 队列数据一旦满了, 便会阻塞后续的入队操作, 导致后续的请求无法正常处理, 而如果队列的消费端有比较重的逻辑, 而队列数据生产速度比较快, 就很容易产生队列阻塞.

解决方法

  • 评估队列的容量是否合理, 队列容量过小, 可以适当扩容
  • 评估消费端的处理逻辑, 是否确实需要很重的逻辑处理
  • 增加消费携程数量
  • 评估使用内存队列的必要性, 可以使用 kafka、redis 等中间件提供的消息队列服务

死循环

  • 递归程序, 为判断终止条件, 或终止条件有问题导致死循环
  • for条件异常, 导致无法结束循环
func logic() {
  // someCond 永远为true
  for someCond {

  }
}
  • channel等待信号继续执行, 但是生产端无任务生产相关信号
func logic() {
  // some logic
  <- businessChan
  // some logic
}
  • WaitGroup协程组, 没有及时完成, 这个属于纯粹的BUG, 对于资源类操作, 阻塞类操作, 挨着写上 defer 语句是一个良好的开发习惯
func logic() {
  wg := &sync.WaitGroup()
  for i := 0; i< 10; i++ {
    wg.Add(1)
    go func() {
      // 缺少wg.Done
      // defer wg.Done()
    }()
  }
  wg.Wait()
}

配置层面

连接池配置不合理

数据库/redis 连接池大小配置过下, 比如QPS=1000, 但是连接池大小配置为10, 那么在高峰期, 会有大量的请求处于阻塞状态, 等待连接池中的连接释放, 导致连接池的连接数不足, 从而导致连接池满, 后续的请求无法获取连接, 从而导致请求失败.

解决方法

  • 评估连接池的大小是否合理, 连接池大小过小, 可以适当扩容, 配置好最好空闲连接数即可

超时时间配置不合理

主要包含如下几个方面:

  • 网络超时时间配置不合理, 网络超时时间过短, 会导致请求失败, 网络超时时间过长, 会导致请求超时
  • SQL超时时间配置不合理, 这个需要在mysql层面配置

运维层面

机器带宽跑满

  • 流量激增, 网络带宽跑满, 导致请求失败
  • 服务混布, 机器带宽被其他服务耗尽
  • 服务监控监控(不常见): 服务器上一般都会部署公司公共的监控组件, 用于日志采集等, 曾遇到过因为日志数据量太大, 导致机器磁盘IO飙增(未跑满,跑满也是个事), 但是机器带宽被打爆了, 导致机器无法处理其他请求.

机器CPU跑满

  • linux cpulimit/cgroup 等工具可以限制机器CPU使用率, 防止机器CPU跑满, 导致机器无法处理其他请求, 或者docker容器等分配CPU资源较少
  • 服务混布, 机器CPU资源被其他服务耗尽

解决方法

  • 合理评估CPU资源
  • 服务混布注意混布服务的分配, 如果两个服务都是使用CPU资源的大户, 那么需要注意混布的比例, 避免一个服务占用过多的CPU资源
  • 监控报警, CPU利用率达到临界值时,发出预警

机器内存跑满

内存跑满, 会导致服务不可用, 内存跑满的原因有很多, 比如: 内存缓存数据过大, 内存泄漏, 将一个大文件读到内存等, 内存超限的结果不是超时, 而是服务不可用. 直接 OOM

进程不足

这个原因有些类似于 MYSQL/Redis 连接池配置不合理。 以PHP为例, PHP-FPM 进程数配置过下, 比如QPS=1000, 但是PHP-FPM进程数配置为10, 那么在高峰期, 会有大量的请求处于阻塞状态, 等待PHP-FPM进程释放, 从而导致请求超时.

解决方法

  • 评估PHP-FPM进程数是否合理, PHP-FPM进程数过小, 可以适当扩容, 配置好最好空闲进程数即可
  • 服务集群横向

云厂商问题

CDN回源数据量过大

因为产品形态原因, 会大量通过CDN访问图片数据资源, 曾经有一次, 因为数据BUG导致大量图片链接发生变化, 导致CDN回源数据量过大, 云厂商服务被压垮, 从而导致请求失败, 已经不是超时问题了.

解决方法

  • 如果明确知道会有大量图片链接发生变化, 可以提前通知CDN分批刷新缓存, 避免CDN回源数据量过大
  • 提前和云厂商沟通,增加服务器资源, 应对流量尖峰
  • 所有重大变更, 做好演练和预案, 快速降级、回退止损等

服务器超卖

这个其实是一个比较狗的原因, 除非是自建机房, 否则大概是无法解决. 至于什么是超卖, 可以参见知乎文章: 关于云计算超卖,你需要知道的都在这里了, 只能说, 夜路走多了,总会撞见鬼的.

总结

耗时突然飙升几大类原因如下:

  • 数据库慢查询(很常见了)
  • 第三方服务不稳定,超时飙升(很常见了)
  • 死锁/锁的范围过大(常见, 但是不好调试, 不易发现)
  • 内存队列阻塞,消费能力不足
  • 连接池配置不合理
  • 服务器资源不足(CPU/带宽等)

Released under the MIT License.