CPU使用率和负载Load计算方法

CPU使用率和负载Load计算方法

2023年7月3日发(作者:)

CPU使⽤率和负载Load计算⽅法Loadavg分析Loadavg浅述 cat /proc/loadavg 可以看到当前系统的load

12$ 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的实现:17fs/proc/proc_c 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 %dn",

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);

} ⼏个宏定义如下:1234#define FSHIFT 11 /* nr of bits of precision */

#define FIXED_1 (1<

#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)带⼊整数部分和⼩数部分计算可得:1234LOAD_INT(a) = avenrun[0]/(2^11) + 1/200LOAD_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) + 1/2 由上述计算结果可以看出,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中进⾏更新17181926kernel/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定义如下:1234#define CALC_LOAD(load,exp,n)

load *= exp;

load += n*(FIXED_1-exp);

load >>= FSHIFT; ⽤avenrun(t-1)和avenrun(t)分别表⽰上⼀次计算的avenrun和本次计算的avenrun,则根据CALC_LOAD宏可以得到如下计算:12avenrun(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中的权重关系123#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直接反应的是系统中的nr_active。 那么nr_active⼜包含哪些? 如何去计算 当前系统中的nr_active? 这些就涉及到了nr_active的采样。Loadavg采样 nr_active直接反映的是为系统贡献load的进程总数,这个总数在nr_active函数中计算:17181926kernel/ned 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; // 处于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的计算⽅法:

0HZ+105HZ5HZ+15HZ+95HZ+11cpu010001cpu110101cpu210101cpu300100cpu401000cpu501010cpu601010cpu701010calc_load04000 18内核loadavg计算18内核计算loadavg存在的问题xtime_lock解析 内核在5s周期执⾏⼀次全局load的更新,这些都是在calc_load函数中执⾏。追寻calc_load的调⽤:kernel/c 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的更新。12345678kernel/ 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。1112void 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之后执⾏的。171819include/linux/c 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的写锁正被拿, 读和写可能不安全。如果在写的过程中,读是不安全的,那么就需要在读的时候等待写锁完成。对应读锁使⽤如下:1234567891#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 读锁实现如下:17static __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读写分离解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的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进⾏分析(以下分析基于5源码)82936373839464748495051static 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。 32内核loadavg计算 cpu0cpu1cpu2cpu3cpu4cpu5cpu6cpu7calc_load_tasks0HZ+105HZ5HZ+1

5HZ+11calc_global_load010

0<--011+11--011+11--011+11--010

0--010

0--010

0--010

0--0001+1+1=333 通过将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计算

0HZ+115HZ

5HZ+1

5HZ+11calc_global_loadcpu010-10

0<--cpu110-11+11--cpu210-11+11--cpu300

1+11--cpu403

1+11--cpu502

1+11--cpu601

1+11--cpu703

1+11--calc_load_tasks303-3=010+1+...+1=777 理论上,该⽅案很好的解决了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计算

0HZ+115HZ

5HZ+1

5HZ+3

5HZ+55HZ+11calc_global_loadcpu010-11+10-101<--cpu110-10

1

00--cpu210-10

1

00--cpu301

0

1

00--cpu401

0

0

10--cpu501

0

0

10--cpu600

1+10-111--cpu700

1+10-101--calc_load_tasks3

3-3=0

0+1+1+1=333-1-1-1=0000 (说明:可能你注意到了在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计算

0HZ+115HZ

5HZ+1

5HZ+35HZ+3cpu010-11+10-1cpu110-10

1

cpu210-10

1

cpu301

0

1

cpu401

0

0

cpu501

0

0

cpu600

1+10-1cpu700

1+10-1calc_load_tasks3

333-3+1+1+1=333-1-1-1=-30tasks_idle0

-35HZ+55HZ+11calc_global_load01<--00--00--00--10--10--11--01--333-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计算

0HZ+115HZ

5HZ+1.3cpu010-11cpu110-10cpu210-10cpu301

0cpu401

0cpu501

0cpu600

1cpu700

1calc_load_tasks3

3

tasks_idle0

-3 5HZ+1.5

5HZ+1.7

5HZ+3

5HZ+55HZ+11calc_global_load+10-10

0

01<--

1

0

1

01--

1

0

1

00--

1+10-11

00--

0

1

0

10--

0

1

0

10--

0

1+11-111--

0

0

0

0-1--3-3+1=111+1-1=101-1+1=3

1

100000

-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计算

0HZ+115HZ

5HZ+1.3cpu010-11cpu110-10cpu210-10cpu301

0cpu401

0cpu501

0cpu600

1cpu700

1calc_load_tasks3

3

idle[0]0

-300idle[1]0idx0 5HZ+1.5

5HZ+1.7

5HZ+35HZ+35HZ+55HZ+11calc_global_load

5HZ+15

+10-10

0

01<--

1

1

0

1

01--

1

1

0

1

00--

0

1+10-11

00--

0

0

1

0

10--

0

0

1

0

10--

0

0

1+11

11--

0-1

0

0

0

01--

1

3-3+1=111+1=202+1=3030

33-2=1

10000000-10-200-2000

0-2001-11再次回归到公平性问题 经过对细粒度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。 ⼀般来说对于需要⼤量cpu计算的进程,当前端压⼒越⼤时,CPU利⽤率越⾼。但对于I/O⽹络密集型的进程,即使请求很多,服务器的CPU也不⼀定很到,这时的服务瓶颈⼀般是在磁盘的I/O上。⽐较常见的就是,⼤⽂件频繁读写的cpu开销远⼩于⼩⽂件频繁读写的开销。因为在I/O吞吐量⼀定时,⼩⽂件的读写更加频繁,需要更多的cpu来处理I/O的中断。 在Linux/Unix下,CPU利⽤率分为⽤户态,系统态和空闲态,分别表⽰CPU处于⽤户态执⾏的时间,系统内核执⾏的时间,和空闲系统进程执⾏的时间。平时所说的CPU利⽤率是指:CPU执⾏⾮系统空闲进程的时间 / CPU总的执⾏时间。 在Linux的内核中,有⼀个全局变量:Jiffies。Jiffies代表时间。它的单位随硬件平台的不同⽽不同。系统⾥定义了⼀个常数HZ,代表每秒种最⼩时间间隔的数⽬,这个值可以在内核编译的时候修改。这样jiffies的单位就是1/HZ。Intel平台jiffies的单位是1/100秒,这就是系统所能分辨的最⼩时间间隔了。这⾥以jiffies为1/100秒为例。每个CPU时间⽚,Jiffies都要加1。CPU的利⽤率就是⽤执⾏⽤户态+系统态的Jiffies除以总的Jifffies来表⽰。 在Linux系统中,可以⽤/proc/stat⽂件来计算cpu的利⽤率。这个⽂件包含了所有CPU活动的信息,该⽂件中的所有值都是从系统启动开始累计到当前时刻。如:1112[test@pc1 ~]$ cat /proc/statcpu 432661 13295 86656 422145968 171474 233 5346cpu0 123075 2462 23494 105543694 16586 0 4615cpu1 111917 4124 23858 105503820 69697 123 371cpu2 103164 3554 21530 105521167 64032 106 334cpu3 94504 3153 17772 105577285 21158 4 24intr 1065711094 1057275779 92 0 6 6 0 4 0 3527 0 0 0 70 0 20 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0ctxt 19067887btime 1139187531processes 270014procs_running 1procs_blocked 0 输出解释 CPU以及CPU0、CPU1、CPU2、CPU3每⾏的每个参数意思(以第⼀⾏为例)为: 参数解释:user (432661) 从系统启动开始累计到当前时刻,⽤户态的CPU时间(单位:jiffies),不包含nice值为负的进程。nice (13295) 从系统启动开始累计到当前时刻,nice值为负的进程所占⽤的CPU时间(单位:jiffies)system (86656) 从系统启动开始累计到当前时刻,核⼼时间(单位:jiffies)idle (422145968) 从系统启动开始累计到当前时刻,除硬盘IO等待时间以外其它等待时间(单位:jiffies)iowait (171474) 从系统启动开始累计到当前时刻,硬盘IO等待时间(单位:jiffies)irq (233) 从系统启动开始累计到当前时刻,硬中断时间(单位:jiffies)softirq (5346) 从系统启动开始累计到当前时刻,软中断时间(单位:jiffies) CPU时间=user+system+nice+idle+iowait+irq+softirq“intr”这⾏给出中断的信息,第⼀个为⾃系统启动以来,发⽣的所有的中断的次数;然后每个数对应⼀个特定的中断⾃系统启动以来所发⽣的次数。 “ctxt”给出了⾃系统启动以来CPU发⽣的上下⽂交换的次数。“btime”给出了从系统启动到现在为⽌的时间,单位为秒。 “processes (total_forks) ⾃系统启动以来所创建的任务的个数⽬。“procs_running”:当前运⾏队列的任务的数⽬。 “procs_blocked”:当前被阻塞的任务的数⽬。 那么CPU利⽤率可以使⽤以下两个⽅法。先取两个采样点,然后计算其差值:123cpu usage=(idle2-idle1)/(cpu2-cpu1)*100cpu usage=[(user_2 +sys_2+nice_2) - (user_1 + sys_1+nice_1)]/(total_2 - total_1)*100

以下⽤分别⽤bash和perl做的⼀个cpu利⽤率的计算:123total_0=USER[0]+NICE[0]+SYSTEM[0]+IDLE[0]+IOWAIT[0]+IRQ[0]+SOFTIRQ[0]total_1=USER[1]+NICE[1]+SYSTEM[1]+IDLE[1]+IOWAIT[1]+IRQ[1]+SOFTIRQ[1]cpu usage=(IDLE[0]-IDLE[1]) / (total_0-total_1) * 100### bash实现1234567891#!/bin/sh## echo user nice system idle iowait irq softirqCPULOG_1=$(cat /proc/stat | grep 'cpu ' | awk '{print $2" "$3" "$4" "$5" "$6" "$7" "$8}')SYS_IDLE_1=$(echo $CPULOG_1 | awk '{print $4}')Total_1=$(echo $CPULOG_1 | awk '{print $1+$2+$3+$4+$5+$6+$7}')sleep 1CPULOG_2=$(cat /proc/stat | grep 'cpu ' | awk '{print $2" "$3" "$4" "$5" "$6" "$7" "$8}')SYS_IDLE_2=$(echo $CPULOG_2 | awk '{print $4}')Total_2=$(echo $CPULOG_2 | awk '{print $1+$2+$3+$4+$5+$6+$7}')SYS_IDLE=`expr $SYS_IDLE_2 - $SYS_IDLE_1`Total=`expr $Total_2 - $Total_1`SYS_USAGE=`expr $SYS_IDLE/$Total*100 |bc -l`SYS_Rate=`expr 100-$SYS_USAGE |bc -l`Disp_SYS_Rate=`expr "scale=3; $SYS_Rate/1" |bc`echo $Disp_SYS_Rate%###perl实现8293031#!/usr/bin/perluse warnings;$SLEEPTIME=5;if (-e "/tmp/stat") { unlink "/tmp/stat";}open (JIFF_TMP, ">>/tmp/stat") || die "Can't open /proc/stat file!n";open (JIFF, "/proc/stat") || die "Can't open /proc/stat file!n";@jiff_0=;print JIFF_TMP $jiff_0[0] ;close (JIFF);sleep $SLEEPTIME;open (JIFF, "/proc/stat") || die "Can't open /proc/stat file!n";@jiff_1=;print JIFF_TMP $jiff_1[0];close (JIFF);close (JIFF_TMP);

@USER=`awk '{print $2}' "/tmp/stat"`;@NICE=`awk '{print $3}' "/tmp/stat"`;@SYSTEM=`awk '{print $4}' "/tmp/stat"`;@IDLE=`awk '{print $5}' "/tmp/stat"`;@IOWAIT=`awk '{print $6}' "/tmp/stat"`;@IRQ=`awk '{print $7}' "/tmp/stat"`;@SOFTIRQ=`awk '{print $8}' "/tmp/stat"`;

$JIFF_0=$USER[0]+$NICE[0]+$SYSTEM[0]+$IDLE[0]+$IOWAIT[0]+$IRQ[0]+$SOFTIRQ[0];$JIFF_1=$USER[1]+$NICE[1]+$SYSTEM[1]+$IDLE[1]+$IOWAIT[1]+$IRQ[1]+$SOFTIRQ[1];$SYS_IDLE=($IDLE[0]-$IDLE[1]) / ($JIFF_0-$JIFF_1) * 100;$SYS_USAGE=100 - $SYS_IDLE;printf ("The CPU usage is %1.2f%%n",$SYS_USAGE);

转载于:/fileoptions/blog/1649492

发布者:admin,转转请注明出处:http://www.yc00.com/xiaochengxu/1688330051a121225.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信