目录[ 隐藏] |
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能够分析出系统当前的总体负载,各部分负载分布状况。php
在内核中/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[0] + (FIXED_1/200); b = avenrun[1] + (FIXED_1/200); c = avenrun[2] + (FIXED_1/200); 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); }
几个宏定义以下:linux
#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[0]/(2^11) + 1/200 LOAD_FRAC(a) = ((avenrun[0]%(2^11) + 2^11/200) * 100) / (2^11) = (((avenrun[0]%(2^11)) * 100 + 2^10) / (2^11) = ((avenrun[0]%(2^11) * 100) / (2^11) + ½
由上述计算结果能够看出,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之间的关系如何,在第二阶段进行分析。函数
内核将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 < 0) { count += LOAD_FREQ; active_tasks = count_active_tasks(); CALC_LOAD(avenrun[0], EXP_1, active_tasks); CALC_LOAD(avenrun[1], EXP_5, active_tasks); CALC_LOAD(avenrun[2], 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定义以下:this
#define CALC_LOAD(load,exp,n) \ load *= exp; \ load += n*(FIXED_1-exp); \ load >>= FSHIFT;
用avenrun(t-1)和avenrun(t)分别表示上一次计算的avenrun和本次计算的avenrun,则根据CALC_LOAD宏能够获得以下计算:spa
avenrun(t)=(avenrun(t-1) * EXP_N + nr_active * FIXED_1*(FIXED_1 – EXP_N)) / FIXED_1 = avenrun(t-1) + (nr_active*FIXED_1 – avenrun(t-1)) * (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的采样。
nr_active直接反映的是为系统贡献load的进程总数,这个总数在nr_active函数中计算:
kernel/sched.c unsigned long nr_active(void) { unsigned long i, running = 0, uninterruptible = 0; for_each_online_cpu(i) { running += cpu_rq(i)->nr_running; uninterruptible += cpu_rq(i)->nr_uninterruptible; } if (unlikely((long)uninterruptible < 0)) uninterruptible = 0; 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的计算方法:
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 |
内核在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 & 1) | (sl->sequence ^ iv); }
至此xtime_lock的实现解析完毕,因为对应写锁基于spin_lock实现,多个程序竞争写锁时等待者会一直循环等待,
当锁里面处理时间过长,会致使整个系统的延时增加。另外,若是系统存在不少xtime_lock的读锁,在某个程
序获取该写锁后,读锁就会进入相似spin_lock的循环查询状态,直到保证能够读取到正确值。所以须要尽量
短的减小在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
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的状况下,如何保证每次更新avenrun时候读取的calc_load_tasks为全部cpu已经更新以后的load?
Thomas的作法是将定时器放到sched_tick中,每一个cpu都设置一个LOAD_FREQ定时器。
定时周期到达时执行当前处理器上load的计算。sched_tick在每一个tick到达时执行
一次,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(1); // 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。
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采样不许确问题。
一个问题的解决,每每伴随着无数其余问题的诞生!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也已经更新至最新状态,不会出现不更新的状况。以下图所示:
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次。接着用例子对应用场景进行分析:
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里面都计算一遍是一种方法。接着用示例进行说明:
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、二、4在计算完本身的load以后,其余cpu计算完本身的load以前,进入了idle,因为默认状况下每一个cpu都会去将idle计算入全局的load中,这部分进入idle形成的cpu load发生的变化会被计算到全局load中。依旧出现了以前10ticks的不公平问题。示例以下:
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):
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 |