load负载算法原理分析 本文将从内核中负载的计算过程进行深入分析并简单分析load高时的排查思路。1.负载的查看过程我们一般会使用top命令查看Linux系统的负载情况典型的top命令输出的负载如下#top load average: 0.18, 0.21, 0.18输出中的Load Avg就是我们常说的负载即系统的平均负载。由于单独某一个瞬间的负载值并没有太大意义所以Linux计算了过去一段时间内的平均值这三个数分别代表的是过去1分钟过去5分钟过去15分钟的平均负载。那么top命令展示的数据是从何而来的呢我们可以使用strace来跟踪top命令的系统调用看到这个过程#strace top ...... openat(AT_FDCWD, /proc/loadavg, O_RDONLY) 9 ......内核中定义了loadavg这个伪文件的open函数。在用户态访问/proc/loadavg会触发内核定义的函数在这里会读取内核中的平均负载变量经过计算便能够展示出来根据上图的流程展开看伪文件/proc/loadavg在kernel中的定义在fs/proc/loadavg.c中。该文件会创建/proc/loadavg并为其指定操作方法为loadavg_proc_show.//file:fs/proc/loadavg.c static int loadavg_proc_show(struct seq_file *m, void *v) { unsigned long avnrun[3]; get_avenrun(avnrun, FIXED_1/200, 0); seq_printf(m, %lu.%02lu %lu.%02lu %lu.%02lu %u/%d %d\n, LOAD_INT(avnrun[0]), LOAD_FRAC(avnrun[0]), LOAD_INT(avnrun[1]), LOAD_FRAC(avnrun[1]), LOAD_INT(avnrun[2]), LOAD_FRAC(avnrun[2]), nr_running(), nr_threads, idr_get_cursor(task_active_pid_ns(current)-idr) - 1); return 0; }在loadavg_proc_show函数中做了两件事调用get_avenrun读取当前负载值。按照一定格式打印输出。那么下一个问题随之而来avenrun全局数组变量中存储的数据是在何时被如何计算出来的2.内核负载计算过程avenrun全局数组变量的计算过程分为如下两步1.PerCPU定期汇总瞬时负载定时将每个CPU当前任务刷新到calc_load_tasks,将每个CPU的负载数据汇总起来得到系统当前的瞬时负载。2.定时计算系统平均负载定时器会根据当前系统整体瞬时负载使用指数加权移动平均法计算过去1515分钟的平均负载。2.1PerCPU定期汇总负载Linux内核中有一个时间子系统在这里面初始化了一个高分辨率的定时器在该定时器中会定时将每个CPU上的负载数据running进程个数uninterruptible进程个数汇总到系统全局的瞬时负载变量calc_load_tasks中。整体流程如下图简单来说在这个高分辨率定时器初始化的时候会通过设定一个到期函数使CPU定期的去执行一些周期性任务比如调度负载均衡的操作而刷新当前系统负载就是在这个时候进行的。这里不大幅度展开内核源码的过程这个到期函数最后会落到scheduler_tick方法上在这个方法里把当前的值刷新到calc_load_tasks上每个CPU都会定时刷新故此时记录的就是当前系统上的瞬时负载值。我们直接定位到最后的内核函数看是如何根据运行队列计算负载值的//filekernel/sched/loadavg.c long calc_load_fold_active(struct rq *this_rq, long adjust) { long nr_active, delta 0; nr_active this_rq-nr_running - adjust; nr_active (int)this_rq-nr_uninterruptible; if (nr_active ! this_rq-calc_load_active) { delta nr_active - this_rq-calc_load_active; this_rq-calc_load_active nr_active; } return delta;//返回一个差值 }明显是计算了当前运行队列里的nr_running和nr_uninterruptible两种状态的任务数量对应的就是用户空间中的R和D状态的任务数此外由于calc_load_tasks是一个长期存在的数据所以在刷新rq里的任务数时只需要刷新变化的量不用全部重新计算。所以上述函数返回的是一个delta。2.2 定时计算系统平均负载在传统意义上我们计算平均数时采取的方法是把过去一段时间的数据都加起来然后取平均数。但是如果用这个方法计算平均负载会存在以下的问题1️⃣需要存储过去每一个采样周期的数据假设没10毫秒采集一次就需要使用一个比较大的数组将每一次采样的数据全部存起来那么统计过去15分钟的平均负载就需要存9万个数据。而且每出现一个新的观察值就要从这个数组中减去一个最早的观察值再加上一个新的观察值那么就需要对这个内存数组进行频繁的修改和更新。2️⃣计算过程较为复杂计算的时候需要把整个数组全部加起来再除以样本总数。3️⃣不能准确的表现当前的变化趋势在传统的平均数计算过程中所有数字的权重都是一样的但是对于平均负载这种实时的应用来说其实越靠近当前时刻的数值权重应该更大一点所以在实际的Linux中使用的是指数加权移动平均法EMWA。使用这个算法计算平均数只需要上一个时间的平均数即可不需要保存所有瞬时负载值。另外越靠近现在的时间点权重越高。现在详细看一下其详细执行过程时间子系统在时钟中断中定期进行一些处理即do_timer函数//file:kernel/time/timekeeping.c void do_timer(unsigned long ticks) { jiffies_64 ticks; calc_global_load(); }其中的calc_global_load是平均负载计算的核心它会获取系统当前瞬时负载值然后计算过去1515分钟的平均负载并存在averun里。//filekernel/sched/loadavg.c void calc_global_load(void) { ...... //1.获取当前瞬时负载值 active atomic_long_read(calc_load_tasks); //2.平均负载的计算 avenrun[0] calc_load(avenrun[0], EXP_1, active); avenrun[1] calc_load(avenrun[1], EXP_5, active); avenrun[2] calc_load(avenrun[2], EXP_15, active); ....... }在calc_load函数中采用前面说的指数加权移动平均法来计算过去的平均负载。//fileinclude/sched/loadavg.h #define FSHIFT 11 /* nr of bits of precision */ #define FIXED_1 (1FSHIFT) /* 1.0 as fixed-point */ #define LOAD_FREQ (5*HZ1) /* 5 sec intervals */ #define EXP_1 1884 /* 1/exp(5sec/1min) as fixed-point */ #define EXP_5 2014 /* 1/exp(5sec/5min) */ #define EXP_15 2037 /* 1/exp(5sec/15min) */ static inline unsigned long calc_load(unsigned long load, unsigned long exp, unsigned long active) { unsigned long newload; newload load * exp active * (FIXED_1 - exp); if (active load) newload FIXED_1-1; return newload / FIXED_1; }公式如下exp是衰减系数决定历史数据和当前数据的权重比例。exp越大历史数据权重越高负载变化越平滑响应越慢。exp越小当前数据权重越高负载变化越敏感响应越快。根据上次的负载和瞬时负载值以及衰减系数计算。综上我们通过top可以观测到的load值就是这么计算出来的。Linux定时将每个CPU上的运行队列中的running和uninterruptible状态的进程数量汇总到一个全局的系统瞬时负载值中然后再定时使用指数加权移动平均法来统计过去1分钟过去5分钟过去15分钟的平均负载。3.平均负载和CPU消耗大家都容易将平均负载和CPU联系到一起简单地认为负载高CPU消耗就会高负载低CPU消耗就会低。根据上述所分析的load计算过程平均负载表现的是系统对所有资源的需求情况而不是只是表现对CPU的需求。我们所计算的D状态的进程是因等待硬件资源如磁盘 I/O、网络 IO而暂停虽不占用 CPU但处于 “被阻塞且无法被信号中断” 的状态属于系统资源的实际占用者。假设当前有一个TASK_UNINTERRUPTIBLE状态的进程因为等待磁盘的I/O而排队此时其并不消耗CPU而是在等待磁盘等硬件资源那么他是要计算在平均负载里的。所以我们看到的load是当前服务器上对系统资源的整体需求情况如果负载变高可能是CPU资源不够也可能是磁盘I/O资源不够所以要配合其他的的观测手段去解决。4.高load排查手段首先我们要知道上述的计算出负载的值是针对整个系统的所以要知道什么情况属于高负载使用nproc获取当前机器的核数即逻辑核个数。如果负载/核数1其实已经表示系统过载。其次load这个值也和请求数没有任何关系真正和load相关的是工作线程数量main线程是工作线程、Timer是工作线程、GC线程也是工作线程load是以线程/进程作为统计指标无论请求数是多少最终都需要线程去处理而工作线程的处理性能直接决定了最终的load值。举个例子来说假设一个服务中有一个线程池线程池中线程数量固定为64正常来说一个任务执行时间为10ms线程拿到任务10ms处理完很快回归线程池等待下一个任务到来自然很少有处于运行状态或者等待IO的线程从一个统计周期来看load表现为很低某段时间由于系统问题一个任务10s都处理不完相当于线程一直在处理任务在load的统计周期里面就体现出的值64不考虑这64条线程外的场景。所以搞清楚load值和请求数、线程数的关系也是非常重要。下面给出两种常见的高负载排查手段4.1 load高CPU高首先cpu高不是问题由cpu高引起的load高才是问题load是判断系统能力指标的依据。我们可以先定位到高CPU利用率的进程接着可以定位到线程。按照应用的类型进行堆栈打印。# 步骤1通过top命令查看系统整体负载与CPU使用率 top # 步骤2按P键排序查找占用CPU最高的进程PID top -c # 显示完整命令行 # 步骤3精确定位问题线程 top -Hp PID # 查看指定进程的线程CPU使用率生成线程堆栈快照# 方式1使用jstackJava应用 jstack PID|grephex_TID-A30 # 方式2使用gdbC/C应用 gdb -pPID(gdb) thread apply all bt full根据堆栈内容去做详细排查。4.2 load高CPU低1️⃣I/O wait如果是load高但是CPU利用率比较低可能是 I/O wait的情况使用top命令实时监控这里的wa指的就是CPU 等待 I/O 完成的时间百分比。于是可以针对磁盘和网络的I/O去进行具体排查找到异常进程。2️⃣fork爆炸上下文切换次数太多遂登录机器做排查但是没有CPU高的进程wa指标也很低CPU基本是空闲的所以现在就要考虑其他的情况。1.首先使用vmstat:可以看到上下文切换次数是极度异常的说明CPU正在120多万个小任务之间切换开销极大。这里可以得到的初步结论是CPU没有被打满而是大量的任务在等待运行但却没有实际的运行导致调度器被压垮初步怀疑是用户态的某些程序里有异常的行为。2.查看哪些命令创建了最多的运行态进程ps -eo stat,comm | awk $1 ~ /^R/ {print $2} | sort | uniq -c | sort -nr | head -20结果如下这里有150个处于运行态的ps进程以及56个处于运行态的cleanlog脚本。目前可以怀疑是这个脚本的问题可能在创建大量进程。使用以下命令查看是否有哪个程序在反复刷屏watch -n 1 ps -eo comm | sort | uniq -c | sort -nr | head结果如下可以确认之前的判断这里都可以看出是cleanlog.sh和crond的问题。这个系统中有一个定时的cron任务在执行cleanlog脚本造成进程数爆炸。下一步就需要去查看这个脚本里到底干了什么导致进程数爆炸。