目录

[隐藏]

Loadavg分析

Loadavg浅述

cat /proc/loadavg可以看到当前系统的load 
$ cat /proc/loadavg 
0.01 0.02 0.05 2/317 26207 
前面三个值分别对应系统当前1分钟、5分钟、15分钟内的平均load。load用于反映当前系统的负载情况,对于16核的系统,如果每个核上cpu利用率为30%,则在不存在uninterruptible进程的情况下,系统load应该维持在4.8左右。对16核系统,如果load维持在16左右,在不存在uninterrptible进程的情况下,意味着系统CPU几乎不存在空闲状态,利用率接近于100%。结合iowait、vmstat和loadavg可以分析出系统当前的整体负载,各部分负载分布情况。

Loadavg读取

在内核中/proc/loadavg是通过load_read_proc来读取相应数据,下面首先来看一下load_read_proc的实现:

fs/proc/proc_misc.c
static int loadavg_read_proc(char *page, char **start, off_t off,
int count, int *eof, void *data)
{
int a, b, c;
int len; a = avenrun[] + (FIXED_1/);
b = avenrun[] + (FIXED_1/);
c = avenrun[] + (FIXED_1/);
len = sprintf(page,"%d.%02d %d.%02d %d.%02d %ld/%d %d\n",
LOAD_INT(a), LOAD_FRAC(a),
LOAD_INT(b), LOAD_FRAC(b),
LOAD_INT(c), LOAD_FRAC(c),
nr_running(), nr_threads, last_pid);
return proc_calc_metrics(page, start, off, count, eof, len);
}

几个宏定义如下:

#define FSHIFT          11              /* nr of bits of precision */
#define FIXED_1 (1<<FSHIFT) /* 1.0 as fixed-point */
#define LOAD_INT(x) ((x) >> FSHIFT)
#define LOAD_FRAC(x) LOAD_INT(((x) & (FIXED_1-1)) * 100)

根据输出格式,LOAD_INT对应计算的是load的整数部分,LOAD_FRAC计算的是load的小数部分。 
将a=avenrun[0] + (FIXED_1/200)带入整数部分和小数部分计算可得:

LOAD_INT(a) = avenrun[]/(^) + /
LOAD_FRAC(a) = ((avenrun[]%(^) + ^/) * ) / (^)
= (((avenrun[]%(^)) * + ^) / (^)
= ((avenrun[]%(^) * ) / (^) + ½

由上述计算结果可以看出,FIXED_1/200在这里是用于小数部分第三位的四舍五入,由于小数部分只取前两位,第三位如果大于5,则进一位,否则直接舍去。

临时变量a/b/c的低11位存放的为load的小数部分值,第11位开始的高位存放的为load整数部分。因此可以得到a=load(1min) * 2^11 
因此有: load(1min) * 2^11 = avenrun[0] + 2^11 / 200 
进而推导出: load(1min)=avenrun[0]/(2^11) + 1/200 
忽略用于小数部分第3位四舍五入的1/200,可以得到load(1min)=avenrun[0] / 2^11,即: 
avenrun[0] = load(1min) * 2^11

avenrun是个陌生的量,这个变量是如何计算的,和系统运行进程、cpu之间的关系如何,在第二阶段进行分析。

Loadavg和进程之间的关系

内核将load的计算和load的查看进行了分离,avenrun就是用于连接load计算和load查看的桥梁。 
下面开始分析通过avenrun进一步分析系统load的计算。 
avenrun数组是在calc_load中进行更新

kernel/timer.c
/*
* calc_load - given tick count, update the avenrun load estimates.
* This is called while holding a write_lock on xtime_lock.
*/
static inline void calc_load(unsigned long ticks)
{
unsigned long active_tasks; /* fixed-point */
static int count = LOAD_FREQ;
count -= ticks;
if (count < ) {
count += LOAD_FREQ;
active_tasks = count_active_tasks();
CALC_LOAD(avenrun[], EXP_1, active_tasks);
CALC_LOAD(avenrun[], EXP_5, active_tasks);
CALC_LOAD(avenrun[], EXP_15, active_tasks);
}
}
static unsigned long count_active_tasks(void)
{
return nr_active() * FIXED_1;
}
#define LOAD_FREQ (5*HZ) /* 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) */

calc_load在每个tick都会执行一次,每个LOAD_FREQ(5s)周期执行一次avenrun的更新。 
active_tasks为系统中当前贡献load的task数nr_active乘于FIXED_1,用于计算avenrun。宏CALC_LOAD定义如下:

#define CALC_LOAD(load,exp,n) \
load *= exp; \
load += n*(FIXED_1-exp); \
load >>= FSHIFT;

用avenrun(t-1)和avenrun(t)分别表示上一次计算的avenrun和本次计算的avenrun,则根据CALC_LOAD宏可以得到如下计算:

avenrun(t)=(avenrun(t-) * EXP_N + nr_active * FIXED_1*(FIXED_1 – EXP_N)) / FIXED_1
= avenrun(t-) + (nr_active*FIXED_1 – avenrun(t-)) * (FIXED_1 -EXP_N) / FIXED_1

推导出:

avenrun(t) – avenrun(t-1) = (nr_active*FIXED_1 – avenrun(t-1)) * (FIXED_1 – EXP_N) / FIXED_1

将第一阶段推导的结果代入上式,可得:

(load(t) – load(t-1)) * FIXED_1 = (nr_active – load(t-1)) * (FIXED_1 – EXP_N)

进一步得到nr_active变化和load变化之间的关系式:

load(t) – load(t-1) = (nr_active – load(t-1)) * (FIXED_1 – EXP_N) / FIXED_1

这个式子可以反映的内容包含如下两点: 
1)当nr_active为常数时,load会不断的趋近于nr_active,趋近速率由快逐渐变缓 
2)nr_active的变化反映在load的变化上是被降级了的,系统突然间增加10个进程, 
1分钟load的变化每次只能够有不到1的增加(这个也就是权重的的分配)。

另外也可以通过将式子简化为:

load(t)= load(t-1) * EXP_N / FIXED_1 + nr_active * (1 - EXP_N/FIXED_1)

这样可以更加直观的看出nr_active和历史load在当前load中的权重关系 (多谢任震宇大师的指出)

#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) */

1分钟、5分钟、15分钟对应的EXP_N值如上,随着EXP_N的增大,(FIXED_1 – EXP_N)/FIXED_1值就越小, 
这样nr_active的变化对整体load带来的影响就越小。对于一个nr_active波动较小的系统,load会 
不断的趋近于nr_active,最开始趋近比较快,随着相差值变小,趋近慢慢变缓,越接近时越缓慢,并最 
终达到nr_active。如下图所示: 
文件:load 1515.jpg(无图)

也因此得到一个结论,load直接反应的是系统中的nr_active。 那么nr_active又包含哪些? 如何去计算 
当前系统中的nr_active? 这些就涉及到了nr_active的采样。

Loadavg采样

nr_active直接反映的是为系统贡献load的进程总数,这个总数在nr_active函数中计算:

kernel/sched.c
unsigned long nr_active(void)
{
unsigned long i, running = , uninterruptible = ; for_each_online_cpu(i) {
running += cpu_rq(i)->nr_running;
uninterruptible += cpu_rq(i)->nr_uninterruptible;
} if (unlikely((long)uninterruptible < ))
uninterruptible = ; return running + uninterruptible;
} #define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define TASK_STOPPED 4
#define TASK_TRACED 8
/* in tsk->exit_state */
#define EXIT_ZOMBIE 16
#define EXIT_DEAD 32
/* in tsk->state again */
#define TASK_NONINTERACTIVE 64

该函数反映,为系统贡献load的进程主要包括两类,一类是TASK_RUNNING,一类是TASK_UNINTERRUPTIBLE。
当5s采样周期到达时,对各个online-cpu的运行队列进行遍历,取得当前时刻该队列上running和uninterruptible的
进程数作为当前cpu的load,各个cpu load的和即为本次采样得到的nr_active。

下面的示例说明了在2.6.18内核情况下loadavg的计算方法:

18内核loadavg计算
  cpu0 cpu1 cpu2 cpu3 cpu4 cpu5 cpu6 cpu7 calc_load
0HZ+10 1 1 1 0 0 0 0 0 0
5HZ 0 0 0 0 1 1 1 1 4
5HZ+1 0 1 1 1 0 0 0 0 0
5HZ+9 0 0 0 0 0 1 1 1 0
5HZ+11 1 1 1 0 0 0 0 0 0

18内核计算loadavg存在的问题

xtime_lock解析

内核在5s周期执行一次全局load的更新,这些都是在calc_load函数中执行。追寻calc_load的调用:

kernel/timer.c
static inline void update_times(void)
{
unsigned long ticks; ticks = jiffies - wall_jiffies;
wall_jiffies += ticks;
update_wall_time();
calc_load(ticks);
}

update_times中更新系统wall time,然后执行全局load的更新。

kernel/timer.c
void do_timer(struct pt_regs *regs)
{
jiffies_64++;
/* prevent loading jiffies before storing new jiffies_64 value. */
barrier();
update_times();
}

do_timer中首先执行全局时钟jiffies的更新,然后是update_times。

void main_timer_handler(struct pt_regs *regs)
{
...
write_seqlock(&xtime_lock);
...
do_timer(regs);
#ifndef CONFIG_SMP
update_process_times(user_mode(regs));
#endif
...
write_sequnlock(&xtime_lock);
}

对wall_time和全局jiffies的更新都是在加串行锁(sequence lock)xtime_lock之后执行的。

include/linux/seqlock.h
static inline void write_seqlock(seqlock_t *sl)
{
spin_lock(&sl->lock);
++sl->sequence;
smp_wmb();
} static inline void write_sequnlock(seqlock_t *sl)
{
smp_wmb();
sl->sequence++;
spin_unlock(&sl->lock);
} typedef struct {
unsigned sequence;
spinlock_t lock;
} seqlock_t;

sequence lock内部保护一个用于计数的sequence。Sequence lock的写锁是通过spin_lock实现的, 
在spin_lock后对sequence计数器执行一次自增操作,然后在锁解除之前再次执行sequence的自增操作。 
sequence初始化时为0。这样,当锁内部的sequence为奇数时,说明当前该sequence lock的写锁正被拿, 
读和写可能不安全。如果在写的过程中,读是不安全的,那么就需要在读的时候等待写锁完成。对应读锁使用如下:

#if (BITS_PER_LONG < 64)
u64 get_jiffies_64(void)
{
unsigned long seq;
u64 ret; do {
seq = read_seqbegin(&xtime_lock);
ret = jiffies_64;
} while (read_seqretry(&xtime_lock, seq));
return ret;
} EXPORT_SYMBOL(get_jiffies_64);
#endif

读锁实现如下:

static __always_inline unsigned read_seqbegin(const seqlock_t *sl)
{
unsigned ret = sl->sequence;
smp_rmb();
return ret;
} static __always_inline int read_seqretry(const seqlock_t *sl, unsigned iv)
{
smp_rmb();
/*iv为读之前的锁计数器
* 当iv为基数时,说明读的过程中写锁被拿,可能读到错误值
* 当iv为偶数,但是读完之后锁的计数值和读之前不一致,则说明读的过程中写锁被拿,
* 也可能读到错误值。
*/
return (iv & ) | (sl->sequence ^ iv);
}

至此xtime_lock的实现解析完毕,由于对应写锁基于spin_lock实现,多个程序竞争写锁时等待者会一直循环等待, 
当锁里面处理时间过长,会导致整个系统的延时增长。另外,如果系统存在很多xtime_lock的读锁,在某个程 
序获取该写锁后,读锁就会进入类似spin_lock的循环查询状态,直到保证可以读取到正确值。因此需要尽可能 
短的减少在xtime_lock写锁之间执行的处理流程。

全局load读写分离解xtime_lock问题

在计算全局load函数calc_load中,每5s需要遍历一次所有cpu的运行队列,获取对应cpu上的load。1)由于cpu个数是不固 
定的,造成calc_load的执行时间不固定,在核数特别多的情况下会造成xtime_lock获取的时间过长。2)calc_load是 
每5s一次的采样程序,本身并不能够精度特别高,对全局avenrun的读和写之间也不需要专门的锁保护,可以将全局load的 
更新和读进行分离。 
Dimitri Sivanich提出在他们的large SMP系统上,由于calc_load需要遍历所有online CPU,造成系统延迟较大。 
基于上述原因Thomas Gleixnert提交了下述patch对该bug进行修复:

[Patch 1/2] sched, timers: move calc_load() to scheduler
[Patch 2/2] sched, timers: cleanup avenrun users

文件:rw isolate.jpg

Thomas的两个patch,主要思想如上图所示。首先将全局load的计算分离到per-cpu上,各个cpu上计算load时不加xtime_lock 
的锁,计算的load更新到全局calc_load_tasks中,所有cpu上load计算完后calc_load_tasks即为整体的load。在5s定 
时器到达时执行calc_global_load,读取全局cacl_load_tasks,更新avenrun。由于只是简单的读取calc_load_tasks, 
执行时间和cpu个数没有关系。

几个关键点:

不加xtime_lock的per cpu load计算

在不加xtime_lock的情况下,如何保证每次更新avenrun时候读取的calc_load_tasks为所有cpu已经更新之后的load?

Thomas的解决方案

Thomas的做法是将定时器放到sched_tick中,每个cpu都设置一个LOAD_FREQ定时器。 
定时周期到达时执行当前处理器上load的计算。sched_tick在每个tick到达时执行 
一次,tick到达是由硬件进行控制的,客观上不受系统运行状况的影响。

sched_tick的时机

将per-cpu load的计算放至sched_tick中执行,第一反应这不是又回到了时间处理中断之间,是否依旧 
存在xtime_lock问题? 下面对sched_tick进行分析(以下分析基于linux-2.6.32-220.17.1.el5源码)

static void update_cpu_load_active(struct rq *this_rq)
{
update_cpu_load(this_rq); calc_load_account_active(this_rq);
} void scheduler_tick(void)
{
int cpu = smp_processor_id();
struct rq *rq = cpu_rq(cpu);
...
spin_lock(&rq->lock);
...
update_cpu_load_active(rq);
...
spin_unlock(&rq->lock); ...
} void update_process_times(int user_tick)
{
...
scheduler_tick();
...
} static void tick_periodic(int cpu)
{
if (tick_do_timer_cpu == cpu) {
write_seqlock(&xtime_lock); /* Keep track of the next tick event */
tick_next_period = ktime_add(tick_next_period, tick_period); do_timer(); // calc_global_load在do_timer中被调用
write_sequnlock(&xtime_lock);
} update_process_times(user_mode(get_irq_regs()));
...
} void tick_handle_periodic(struct clock_event_device *dev)
{
int cpu = smp_processor_id();
...
tick_periodic(cpu);
...
}

交错的时间差

将per-cpu load的计算放到sched_tick中后,还存在一个问题就是何时执行per-cpu上的load计算,如何保证更新全 
局avenrun时读取的全局load为所有cpu都计算之后的? 当前的方法是给所有cpu设定同样的步进时间LOAD_FREQ, 
过了这个周期点当有tick到达则执行该cpu上load的计算,更新至全局的calc_load_tasks。calc_global_load 
的执行点为LOAD_FREQ+10,即在所有cpu load计算执行完10 ticks之后,读取全局的calc_load_tasks更新avenrun。

32内核loadavg计算
  cpu0 cpu1 cpu2 cpu3 cpu4 cpu5 cpu6 cpu7 calc_load_tasks
0HZ+10 0 0 0 0 0 0 0 0 0
5HZ 1 1 1 1 1 1 1 1 0
5HZ+1 0 1 1 1 0 0 0 0 0
    +1 +1 +1         1+1+1=3
5HZ+11 0 1 1 1 0 0 0 0 3
calc_global_load <-- -- -- -- -- -- -- -- 3

通过将calc_global_load和per-cpu load计算的时间进行交错,可以避免calc_global_load在各个cpu load计算之间执行, 
导致load采样不准确问题。

32内核Load计数nohz问题

一个问题的解决,往往伴随着无数其他问题的诞生!Per-cpu load的计算能够很好的分离全局load的更新和读取,避免大型系统中cpu 
核数过多导致的xtime_lock问题。但是也同时带来了很多其他需要解决的问题。这其中最主要的问题就是nohz问题。

为避免cpu空闲状态时大量无意义的时钟中断,引入了nohz技术。在这种技术下,cpu进入空闲状态之后会关闭该cpu对应的时钟中断,等 
到下一个定时器到达,或者该cpu需要执行重新调度时再重新开启时钟中断。

cpu进入nohz状态后该cpu上的时钟tick停止,导致sched_tick并非每个tick都会执行一次。这使得将per-cpu的load计算放在 
sched_tick中并不能保证每个LOAD_FREQ都执行一次。如果在执行per-cpu load计算时,当前cpu处于nohz状态,那么当 
前cpu上的sched_tick就会错过,进而错过这次load的更新,最终全局的load计算不准确。 
基于Thomas第一个patch的思想,可以在cpu调度idle时对nohz情况进行处理。采用的方式是在当前cpu进入idle前进行一次该cpu 
上load的更新,这样即便进入了nohz状态,该cpu上的load也已经更新至最新状态,不会出现不更新的情况。如下图所示:

32内核loadavg计算
  cpu0 cpu1 cpu2 cpu3 cpu4 cpu5 cpu6 cpu7 calc_load_tasks
0HZ+11 1 1 1 0 0 0 0 0 3
5HZ 0 0 0 0 3 2 1 3 0
  -1 -1 -1           3-3=0
5HZ+1 0 1 1 1 1 1 1 1 1
    +1 +1 +1 +1 +1 +1 +1 0+1+...+1=7
5HZ+11 0 1 1 1 1 1 1 1 7
calc_global_load <-- -- -- -- -- -- -- -- 7

理论上,该方案很好的解决了nohz状态导致全局load计数可能不准确的问题,事实上这却是一个苦果的开始。大量线上应用反馈 
最新内核的load计数存在问题,在16核机器cpu利用率平均为20%~30%的情况下,整体load却始终低于1。

解决方案

接到我们线上报告load计数偏低的问题之后,进行了研究。最初怀疑对全局load计数更新存在竞争。对16核的系统,如果都没有进入 
nohz状态,那么这16个核都将在LOAD_FREQ周期到达的那个tick内执行per-cpu load的计算,并更新到全局的load中,这 
之间如果存在竞争,整体计算的load就会出错。当前每个cpu对应rq都维护着该cpu上一次计算的load值,如果发现本次计算load 
和上一次维护的load值之间差值为0,则不用更新全局load,否则将差值更新到全局load中。正是由于这个机制,全局load如果被 
篡改,那么在各个cpu维护着自己load的情况下,全局load最终将可能出现负值。而负值通过各种观察,并没有在线上出现,最终竞 
争条件被排除。

通过/proc/sched_debug对线上调度信息进行分析,发现每个时刻在cpu上运行的进程基本维持在2~3个,每个时刻运行有进程的cpu都 
不一样。进一步分析,每个cpu上平均每秒出现sched_goidle的情况大概为1000次左右。因此得到线上每次进入idle的间隔为1ms/次。 
结合1HZ=1s=1000ticks,可以得到1tick =1ms。所以可以得到线上应用基本每一个tick就会进入一次idle!!! 这个发现就好比 
原来一直用肉眼看一滴水,看着那么完美那么纯净,突然间给你眼前架了一个放大镜,一下出现各种凌乱的杂碎物。 在原有的世界里, 
10ticks是那么的短暂,一个进程都可能没有运行完成,如今发现10ticks内调度idle的次数就会有近10次。接着用例子对应用场景进行分析:

32内核loadavg计算
  cpu0 cpu1 cpu2 cpu3 cpu4 cpu5 cpu6 cpu7 calc_load_tasks
0HZ+11 1 1 1 0 0 0 0 0 3
5HZ 0 0 0 1 1 1 0 0  
  -1 -1 -1           3-3=0
5HZ+1 1 0 0 0 0 0 1 1  
  +1           +1 +1 0+1+1+1=3
5HZ+3 0 1 1 1 0 0 0 0 3
  -1           -1 -1 3-1-1-1=0
5HZ+5 0 0 0 0 1 1 1 0 0
5HZ+11 1 0 0 0 0 0 1 1 0
calc_global_load <-- -- -- -- -- -- -- -- 0

(说明:可能你注意到了在5HZ+5到5HZ+11过程中也有CPU从非idle进入了idle,但是为什么没有-1,这里是由于每个cpu都保留 
了一份该CPU上一次计算时的load,如果load没有变化则不进行计算,这几个cpu上一次计算load为0,并没有变化)

Orz!load为3的情况直接算成了0,难怪系统整体load会偏低。这里面的一个关键点是:对已经计算过load的cpu,我们对idle进 
行了计算,却从未考虑过这给从idle进入非idle的情况带来的不公平性。这个是当前线上2.6.32系统存在的问题。在定位到问题 
之后,跟进到upstream中发现Peter Z针对该load计数问题先后提交了三个patch,最新的一个patch是在4月份提交。这三个 
patch如下:

[Patch] sched: Cure load average vs NO_HZ woes
[Patch] sched: Cure more NO_HZ load average woes
[Patch] sched: Fix nohz load accounting – again!

这是目前我们backport的patch,基本思想是将进入idle造成的load变化暂时记录起来,不是每次进入idle都导致全局load的更新。 
这里面的难点是什么时候将idle更新至全局的load中?在最开始计算per-cpu load的时候需要将之前所有的idle都计算进来, 
由于目前各个CPU执行load计算的先后顺序暂时没有定,所以将这个计算放在每个cpu里面都计算一遍是一种方法。接着用示例进行说明:

32内核loadavg计算
  cpu0 cpu1 cpu2 cpu3 cpu4 cpu5 cpu6 cpu7 calc_load_tasks tasks_idle
0HZ+11 1 1 1 0 0 0 0 0 3 0
5HZ 0 0 0 1 1 1 0 0    
  -1 -1 -1           3 -3
5HZ+1 1 0 0 0 0 0 1 1 3
  +1           +1 +1 3-3+1+1+1=3 0
5HZ+3 0 1 1 1 0 0 0 0 3
5HZ+3 -1           -1 -1 3 -1-1-1=-3
5HZ+5 0 0 0 0 1 1 1 0 3
5HZ+11 1 0 0 0 0 0 1 1 3
calc_global_load <-- -- -- -- -- -- -- -- 3 -3

至此这三个patch能够很好的处理我们的之前碰到的进入idle的问题。 
将上述三个patch整理完后,在淘客前端线上机器中进行测试,测试结果表明load得到了明显改善。

更细粒度的时间问题

将上述三个patch整理完后,似乎一切都完美了,idle进行了很好的处理,全局load的读写分离也很好实现。然而在业务线上的测试结果却出乎意料,虽然添加patch之后load计数较之前有明显改善,但是依旧偏低。下面是一个抓取的trace数据(粗体为pick_next_idle):

<...>-9195 [000] 11994.232382: calc_global_load: calc_load_task = 0
<...>-9198 [000] 11999.213365: calc_load_account_active: cpu 0 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 1
<...>-9199 [001] 11999.213379: calc_load_account_active: cpu 1 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 2
<...>-9194 [002] 11999.213394: calc_load_account_active: cpu 2 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 3
<...>-9198 [000] 11999.213406: calc_load_account_active: cpu 0 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 2
<...>-9201 [003] 11999.213409: calc_load_account_active: cpu 3 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 3
<...>-9190 [004] 11999.213424: calc_load_account_active: cpu 4 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 4
<...>-9197 [005] 11999.213440: calc_load_account_active: cpu 5 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 5
<...>-9194 [002] 11999.213448: calc_load_account_active: cpu 2 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 4
<...>-9203 [006] 11999.213455: calc_load_account_active: cpu 6 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 5
<...>-9202 [007] 11999.213471: calc_load_account_active: cpu 7 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 6
<...>-9195 [008] 11999.213487: calc_load_account_active: cpu 8 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 7
<...>-9204 [009] 11999.213502: calc_load_account_active: cpu 9 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 8
<...>-9190 [004] 11999.213517: calc_load_account_active: cpu 4 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 7
<...>-9192 [010] 11999.213519: calc_load_account_active: cpu 10 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 8
<...>-9200 [011] 11999.213533: calc_load_account_active: cpu 11 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 9
<...>-9189 [012] 11999.213548: calc_load_account_active: cpu 12 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 10
<...>-9196 [013] 11999.213564: calc_load_account_active: cpu 13 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 11
<...>-9193 [014] 11999.213580: calc_load_account_active: cpu 14 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 12
<...>-9191 [015] 11999.213596: calc_load_account_active: cpu 15 nr_run 1 nr_uni 0 nr_act 1 delta 1 calc 13
<...>-9204 [009] 11999.213610: calc_load_account_active: cpu 9 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 12<...>-9195 [008] 11999.213645: calc_load_account_active: cpu 8 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 11<...>-9203 [006] 11999.213782: calc_load_account_active: cpu 6 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 10<...>-9197 [005] 11999.213809: calc_load_account_active: cpu 5 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 9<...>-9196 [013] 11999.213930: calc_load_account_active: cpu 13 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 8<...>-9193 [014] 11999.213971: calc_load_account_active: cpu 14 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 7<...>-9189 [012] 11999.214004: calc_load_account_active: cpu 12 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 6<...>-9199 [001] 11999.214032: calc_load_account_active: cpu 1 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 5<...>-9191 [015] 11999.214164: calc_load_account_active: cpu 15 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 4<...>-9202 [007] 11999.214201: calc_load_account_active: cpu 7 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 3<...>-9201 [003] 11999.214353: calc_load_account_active: cpu 3 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 2<...>-9192 [010] 11999.214998: calc_load_account_active: cpu 10 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 1<...>-9200 [011] 11999.215115: calc_load_account_active: cpu 11 nr_run 0 nr_uni 0 nr_act 0 delta -1 calc 0
<...>-9198 [000] 11999.223342: calc_global_load: calc_load_task = 0

虽然这个是未加三个patch之前的trace数据,但是我们依旧能够发现一些问题:原来的10tick对我们来说从一个微不足道的小时间片被提升为一个大时间片,相对此低了一个数量级的1 tick却一直未真正被我们所重视。trace数据中,cpu0、2、4在计算完自己的load之后,其他cpu计算完自己的load之前,进入了idle,由于默认情况下每个cpu都会去将idle计算入全局的load中,这部分进入idle造成的cpu load发生的变化会被计算到全局load中。依旧出现了之前10ticks的不公平问题。示例如下:

32内核loadavg计算
  cpu0 cpu1 cpu2 cpu3 cpu4 cpu5 cpu6 cpu7 calc_load_tasks tasks_idle
0HZ+11 1 1 1 0 0 0 0 0 3 0
5HZ 0 0 0 1 1 1 0 0    
  -1 -1 -1           3 -3
5HZ+1.3 1 0 0 0 0 0 1 1  
  +1               3-3+1=1 0
5HZ+1.5 0 1 1 1 0 0 0 0 1 0
  -1     +1         1+1-1=1 0
5HZ+1.7 0 0 0 0 1 1 1 0 0 0
        -1     +1   1-1+1=3 0
5HZ+3 0 1 1 1 0 0 1 0    
              -1   1 -1
5HZ+5 0 0 0 0 1 1 1 0  
5HZ+11 1 1 0 0 0 0 1 -1  
calc_global_load <-- -- -- -- -- -- -- -- 1 -1

线上业务平均每个任务运行时间为0.3ms,任务运行周期为0.5ms,因此每个周期idle执行时间为0.2ms。在1个tick内,cpu执行完自己load的计算之后,很大的概率会在其他cpu执行自己load计算之前进入idle,致使整体load计算对idle和非idle不公平,load计数不准确。 针对该问题,一个简单的方案是检测第一个开始执行load计算的CPU,只在该CPU上将之前所有进入idle计算的load更新至全局的load,之后的CPU不在将idle更新至全局的load中。这个方案中检测第一个开始执行load计算的CPU是难点。另外一个解决方案是将LOAD_FREQ周期点和全局load更新至avenren的LOAD_FREQ+10时间点作为分界点。对上一次LOAD_FREQ+10到本次周期点之间的idle load,可以在本次CPU执行load计算时更新至全局的load;对周期点之后到LOAD_FREQ+10时间点之间的idle load可以在全局load更新至avenrun之后更新至全局load。 
Peter Z采用的是上述第二个解决,使用idx翻转的技术实现。通过LOAD_FREQ和LOAD_FREQ+10两个时间点,可以将idle导致的load分为两部分,一部分为LOAD_FREQ至LOAD_FREQ+10这部分,这部分load由于在各个cpu计算load之后到全局avenrun更新之间,不应该直接更新至全局load中;另一部分为LOAD_FREQ+10至下一个周期点LOAD_FREQ,这部分idle导致的load可以随时更新至全局的load中。实现中使用了一个含2个元素的数组,用于对这两部分load进行存储,但这两部分并不是分别存储在数组的不同元素中,而是每个LOAD_FREQ周期存储一个元素。如下图所示,在0~5周期中,这两部分idle都存储在数组下标为1的元素中。5~10周期内,这两个部分都存储在数组下标为0的元素中。在5~10周期中,各个cpu计算load时读取的idle为0~5周期存储的;在计算完avenrun之后,更新idle至全局load时读取的为5~10周期中前10个ticks的idle导致的load。这样在10~15周期中,各个cpu计算load时读取的idle即为更新avenrun之后产生的idle load。具体实现方案如下:

      0             5             10            15          --->HZ
+10 +10 +10 +10 ---> ticks
|-|-----------|-|-----------|-|-----------|-|
idx:0 1 1 0 0 1 1 0
w:0 1 1 1 0 0 0 1 1 1 0 0
r:0 0 1 1 1 0 0 0 1 1 1 0
说明:1)0 5 10 15代表的为0HZ、5HZ、10HZ、15HZ,这个就是各个cpu执行load计算的周期点
2)+10表示周期点之后10ticks(即为计算avenrun的时间点)
3)idx表示当前的idx值(每次只取最后一位的值,因此变化范围为0~1)
4)w后面3列值,第一列表示周期点之前idle计算值写入的数组idx;第二列表示周期点到+10之间idle导致的load变化写入的数
组idx;第三列表示计算万avenrun之后到下一个周期点之间idle写入的数组idx;

用如下示例进行说明(假定0HZ+11之后idx为0):

32内核loadavg计算
  cpu0 cpu1 cpu2 cpu3 cpu4 cpu5 cpu6 cpu7 calc_load_tasks idle[0] idle[1] idx
0HZ+11 1 1 1 0 0 0 0 0 3 0 0 0
5HZ 0 0 0 1 1 1 0 0    
  -1 -1 -1           3 -3 0 0
5HZ+1.3 1 0 0 0 0 0 1 1  
  +1               3-3+1=1 0 0 0
5HZ+1.5 0 1 1 1 0 0 0 0 1 0
  -1     +1         1+1=2 0 -1 0
5HZ+1.7 0 0 0 0 1 1 1 0 0 0
        -1     +1   2+1=3 0 -2 0
5HZ+3 0 1 1 1 0 0 1 0 0
5HZ+3                 3 0 -2 0
5HZ+5 0 0 0 0 1 1 1 0 0
5HZ+11 1 1 0 0 0 0 1 1  
calc_global_load <-- -- -- -- -- -- -- -- 3 0 -2 0
                  3-2=1 0 0 1
5HZ+15 1 1 0 0 0 0 0 1    
              -1   1 0 -1 1

再次回归到公平性问题

经过对细粒度idle调度问题进行解决,在线上业务整体load得到了很好的改善。原来平均运行进程数在16的情况下,load一直徘徊在1左右,改善之后load回升到了15左右。 
然而这个patch发布到社区,经过相关报告load计数有问题的社区人员进行测试之后,发现系统的load整体偏高,而且很多时候都是趋近于系统总运行进程数。为了验证这个patch的效果,升级了一台添加该patch的机器,进行观察,确实发现升级之后机器的load比原有18还高出1左右。 
又是一次深度的思考,是否当前这个patch中存在BUG? 是否从第一个CPU到最后一个CPU之间的idle就应该直接计算在整体load中? 对于高频度调度idle的情况,这部分idle是不应该加入到全局load中,否则无论系统运行多少进程,最终load都会始终徘徊在0左右。因此这部分idle必须不能够加入到全局load中。通过trace数据进行分析,也证明了patch运行的行为符合预期,并不存在异常。 
如果假设之前所有的patch都没有问题,是否存在其他情况会导致系统load偏高?导致load偏高,一个很可能的原因就是在该计算为idle时,计算为非idle情况。为此先后提出了负载均衡的假设、计算load时有进程wakeup到当前运行队列的假设,最终都被一一排除。 
进一步观察trace数据,发现几乎每次都是在做完该CPU上load计算之后,该CPU立即就进入idle。16个CPU,每个CPU都是在非idle的时候执行load计算,执行完load计算之后又都是立即进入idle。而且这种情况是在每一次做load计算时都是如此,并非偶然。按照采样逻辑,由于采样时间点不受系统运行状况影响,对于频繁进出idle的情况,采样时idle和非idle都应该会出现。如今只有非idle情况,意味着采样时间点选取存在问题。 
进一步分析,如果采样点处于idle内部,由于nohz导致进入idle之后并不会周期执行sched_tick,也就无法执行load计算,看起来似乎会导致idle load计算丢失。事实并不是,之前计算idle load就是为了避免进入nohz导致load计算丢失的问题,在进入idle调度前会将当前cpu上的load计算入idle load中,这样其他cpu执行load计算时会将这部分load一同计算入内。 
但是基于上述逻辑,也可以得到一个结论:如果采样点在idle内部,默认应该是将进入idle时的load作为该cpu上采样load。事实是否如此?继续分析,该CPU如果从nohz重新进入调度,这个时候由于采样时间点还存在,而且间隔上一次采样已经超过一个LOAD_FREQ周期,会再次执行load计算。再次执行load计算会覆盖原有进入idle时计算的load,这直接的一个结果是,该CPU上的采样点从idle内部变成了非idle! 问题已经变得清晰,对采样点在idle内部的情况,实际计算load应该为进入idle时该cpu上的load,然而由于该cpu上采样时间点没有更新,导致退出nohz状态之后会再次执行load计算,最终将退出nohz状态之后的load作为采样的load。

问题已经清楚,解决方案也比较简单:在退出nohz状态时检测采样时间点在当前时间点之前,如果是,则意味着这次采样时间点在idle内部,这 个周期内不需要再次计算该CPU上的load。
 
 

linux loadavg详解(top cpu load)的更多相关文章

  1. linux命令详解——top

    简介 TOP是一个动态显示过程,即可以通过用户按键来不断刷新当前状态.如果在前台执行该命令,它将独占前台,直到用户终止该程序为止.比较准确的说,top命令提供了实时的对系统处理器的状态监视.它将显示系 ...

  2. (转)Linux PS 详解

    原文:https://cn.aliyun.com/jiaocheng/162702.html 摘要:原文地址:http://www.cnblogs.com/wangkangluo1/archive/2 ...

  3. Linux 目录详解 树状目录结构图

    1.树状目录结构图 2./目录 目录 描述 / 第一层次结构的根.整个文件系统层次结构的根目录. /bin/ 需要在单用户模式可用的必要命令(可执行文件):面向所有用户,例如:cat.ls.cp,和/ ...

  4. Linux 系统结构详解

    Linux 系统结构详解 Linux系统一般有4个主要部分: 内核.shell.文件系统和应用程序.内核.shell和文件系统一起形成了基本的操作系统结构,它们使得用户可以运行程序.管理文件并使用系统 ...

  5. Linux内存详解

    --Linux内存详解 -----------------2014/05/24 Linux的内存上表现的不像windows那么直观,本文准备详细的介绍一下Linux的内存. 请看这下有linux命令f ...

  6. Docker 基础技术之 Linux cgroups 详解

    PS:欢迎大家关注我的公众号:aCloudDeveloper,专注技术分享,努力打造干货分享平台,二维码在文末可以扫,谢谢大家. 推荐大家到公众号阅读,那里阅读体验更好,也沉淀了很多篇干货. 前面两篇 ...

  7. [转帖]Linux文件系统详解

    Linux文件系统详解 https://www.cnblogs.com/alantu2018/p/8461749.html 贼复杂.. 从操作系统的角度详解Linux文件系统层次.文件系统分类.文件系 ...

  8. Linux命令详解之—tail命令

    tail命令也是一个非常常用的文件查看类的命令,今天就为大家介绍下Linux tail命令的用法. 更多Linux命令详情请看:Linux命令速查手册 Linux tail命令主要用来从指定点开始将文 ...

  9. Linux命令详解之—less命令

    Linux下还有一个与more命令非常类似的命令--less命令,相比于more命令,less命令更加灵活强大一些,今天就给大家介绍下Linux下的less命令. 更多Linux命令详情请看:Linu ...

随机推荐

  1. CentOS 6.6下PXE+Kickstart无人值守安装操作系统

    一.简介 1.1 什么是PXE PXE(Pre-boot Execution Environment,预启动执行环境)是由Intel公司开发的最新技术,工作于Client/Server的网络模式,支持 ...

  2. Managing linux Shell Jobs

    Managing Shell Jobs   When moving jobs between the foreground and background, it may be useful to ha ...

  3. MM32 备份域学习(兼容STM32)

    MM32 备份域学习(兼容STM32) 内容提要 备份域工作原理 备份域特性 备份域的保护:侵入检测 备份域侵入检测 备份域电源与主要内容 备份域特性 20字节数据后备寄存器(中容量和小容量产品),或 ...

  4. ios10下,手机连接xcode控制台不显示日志,解决办法

    按照下面设置: run- > environment variables->添加 OS_ACTIVITY_MODE ,value内容为空

  5. charAt(i) 函数

    charAt(i) 函数 是获取字符串中i位置的字符 str.charAt(i)的意思是第i个字符在字符串str中所占的位置,输出的是数字 for (var i = 0; i < str.len ...

  6. boostrap按钮

    bootstrap按钮 对应链接:http://v3.bootcss.com/css/#buttons 使用时添加基础类class:btn 默认样式class=btn-default,控制大小clas ...

  7. [JavaScript]plupload多图片上传图片

    var uploader = new plupload.Uploader({ //创建实例的构造方法     runtimes: 'html5,flash,silverlight,html4',    ...

  8. php的mq客户端获取队列方法改造

    获取mq中消息然后处理失败重试机制: 下面的代码是php连接mq客户端的获取queue队列中的消息代码: public function createDurableSubscriber($queue, ...

  9. C#中如何获取系统环境变量等

    C#中获取系统环境变量需要用到Environment 类. 其中提供了有关当前环境和平台的信息以及操作它们的方法.该类不能被继承 以下代码得到%systemdrive%的值,即“C:” string ...

  10. 关于.NET中的验证码

    常用的生成验证码程序 ,图片效果如下: 源程序如下: 复制代码 代码如下:using System; using System.IO; using System.Drawing; using Syst ...