网上有很多描述 Load 的例子,将单核处理器比喻成一座桥,将 Load 比作车流,当桥上没有车的时候,Load = 0,当桥上塞满车的时候,Load = 1,当桥上塞满车甚至开始排队等候的时候,Load > 1,当 Load > 5 的时候,整座桥基本就是瘫痪状态了。

Load 中的 R 和 D

也有非常专业的研究,比如这个项目,平时我们关注比较多的是 Load1,Load5 和 Load15,表示一分钟、五分钟和十五分钟内系统的平均拥塞情况,为了更好地理解和分析 Load,作者开发了一个 Load5s 工具,能够感知系统 5s 内的 Load 情况。

对 Load 比较准确的描述是:
loadavg 文件中前三个字段是平均负载值,分别代表 1、5 和 15 分钟的作业数量的平均值,作业包括运行队列 (state R) 或者等待磁盘 I/O (state D) 两种类型

对于 State R 和 State D 的解释,可以这样去理解:

  • State R:正在使用 CPU 或者等待使用 CPU 的 Job
  • State D:正在等待 IO 的 Job,例如磁盘 IO、网络 IO 等等

由于这两种状态的存在,加上我们服务大多是多核多线程的,因此分析问题的时候就出现了如下几种情况:

  1. 单个线程中 R 状态的 Job 很多
  2. 多个线程中 R 状态的 Job 很多
  3. 单个线程中 D 状态的 Job 很多
  4. 多个线程中 D 状态的 Job 很多

接下来我们的分析也是围绕这几种情况来讨论。

不同 R 和 D 情况导致的 Load 飙高

1. 单线程中 R 状态的 Job 很多

单线程中 R 状态的 Job 比较多,往往是程序出问题了,比如写了一个死循环,循环中频繁地进行业务处理,再比如业务没有控制好并发,大批量地处理业务逻辑等等。这个时候使用 top 命令查看各 CPU 的情况,会发现,只有一个 CPU 是繁忙的,其他的都很空闲。

其实这类情况是最好定位和排查的,通常 CPU 也会跟着 Load 一起飙升。还有一种常见的诱因,比如业务将一个超大的 YAML 文本解析成 JSON,这个时候会导致十分频繁的 GC,频繁的 GC 会导致 CPU、内存全部暴涨,对应的 Load 也会上涨。极端的情况,这个进程会因为内存溢出而 Crash。

2. 多个线程中 R 状态的 Job 很多

这说明业务中出现了大量导致计算密集型的请求,落在了不同的进程上,这种情况容易出现在产品迭代后刚上线的时候,比如某个业务频繁请求的接口出现了异常的逻辑没有处理好。

针对大型系统的变更,上线之前一般会进行压力测试,在压测过程中可以暴露这种问题。

3. 单个线程中 D 状态的 Job 很多

这种情况出现的可能性比较多,需要具体情况具体分析。大家知道我们的系统时刻都在处理各种各样业务逻辑,一个请求过来会随机落在机器的任何一个进程上处理,很容易出现某个特殊的请求执行的逻辑十分繁重,比如执行了一个导入任务,将上百篇文档储存到 OSS,每次储存都有一次网络 IO,瞬间发起的大量网络 IO 对系统负载挑战也是比较大的。

4. 多个线程中 D 状态的 Job 很多

基本上可以断定是业务在做超频繁的 IO 操作,或者每次 IO 操作的成本巨大,比如从磁盘中读取一个大文件到内存,或者挂载的是远程磁盘,IO 操作十分缓慢,再比如网络异常,导致宿主机上的所有网络 IO 都 pending,从而导致 Load 飙高。

遇到 Load 飙高如何定位

CPU 是系统的一个瞬时状态,而 Load 代表的是系统一段时间内的负载情况,如果 Load1、Load5、Load15 陆续开始报警,那就比较危险了,这说明系统长时间内(至少 15 分钟)都有点喘不过气来了,这个时候必须结合 CPU、内存、GC 等指标去排查业务问题,否则下一秒可能系统就会出现瘫痪状态,业务站点假死,一直 loading 加载不出页面。

从前面的分析可以看出,Load 飙高主要是因为 D 状态和 R 状态的 Job 过多导致的,但是我们的分析工具又没有把各个进程的 D 和 R Job 数打印出来,即便是打印出来,相信很多同学也不一定看得懂。这个时候还是要去观察两类指标:

1. 系统是不是有很多 IO 操作

一般的业务系统很少出现频繁的磁盘 IO,这种在数据库服务器上出现的比较多,比如执行了一条没有加索引的 SQL,导致扫全表,这个时候磁盘 IO 就特别大,会出现超频繁的磁盘读写操作。

top 命令中有一个指标是用来衡量磁盘 IO 的:

Top

截图中的 wa,如果这个值比较高,那就要注意,是不是存在什么异常的读写操作,在 npm registry 这样的系统中可能会经常出现此类问题导致的 Load 飙高。

这种情况大概率还是出现在业务请求之中,看到最多的是一些宿主机网络不稳当,导致宿主机内容器的所有请求都超时,这个时候系统的表现是 CPU 良好,Load 持续报警。也可能因为上游系统吞吐量降低,响应迟钝,比如平时 100ms 就能响应,结果某段时间内需要 2~3s 才会响应,也会导致系统 Load 飙高。

2. 观察 CPU 的走向

CPU 高没啥问题,只要低于 70% 基本上都属于比较正常的水位,但是 CPU 高导致 Load 也跟随变高,那就有问题了,这意味着业务可能出现了死循环或者正在处理非常复杂的计算逻辑,比较常见的情况是,使用正则对长字符串做匹配,如果你的业务出现这种问题,赶紧去做一下 CPU Profile,看看一段时间内程序核心运行的是哪些逻辑,在 Node.js 中火焰图是针对这类问题的排查好助手。在做 Profile 之前也可以去看一下 GC 的特征,如果 GC 很频繁而且时间很长,基本上可以断定业务在做复杂的数据处理。

如果 Load 很高,但是 CPU 很低,这种情况基本是因为业务的 D 状态 Job 过多导致的,去看一看业务的超时日志,以及函数执行的时长。也可以去看看自己的服务响应时长,如果依赖的系统出现了网络卡顿,那么自己的系统响应地整体响应也会被拉长。

应急机制

Load 飙高可用的应急手段并不多,常见的应急有两类:

  • 确定是宿主机网络问题,赶紧去置换机器,换到其他机房或者地区
  • 确定是单进程中的 R 或 D 问题,可以考虑等他自己缓慢恢复,确定了无法恢复,比如遇到了死循环,可以考虑将进程直接杀死

总之,还是需要结合 CPU、GC、Memory 和业务日志综合分析,定位到最精确的问题再对症下药。