2019独角兽企业重金招聘Python工程师标准>>>

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

几个宏定义如下:

#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) + 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中进行更新

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定义如下:

#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-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直接反应的是系统中的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 = 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的计算方法:

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

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 & 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进行分析(以下分析基于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。

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 0r: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。

计算CPU利用率

一般来说对于需要大量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活动的信息,该文件中的所有值都是从系统启动开始累计到当前时刻。
如:

[test@pc1 ~]$ cat /proc/stat
cpu 432661 13295 86656 422145968 171474 233 5346
cpu0 123075 2462 23494 105543694 16586 0 4615
cpu1 111917 4124 23858 105503820 69697 123 371
cpu2 103164 3554 21530 105521167 64032 106 334
cpu3 94504 3153 17772 105577285 21158 4 24
intr 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 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 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 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 0 0 0 0 0 0 0 0 0 0 0 0 0 0 7376958 0 0 0 0 0 0 0 1054602 0 0 0 0 0 0 0 30 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
ctxt 19067887
btime 1139187531
processes 270014
procs_running 1
procs_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利用率可以使用以下两个方法。先取两个采样点,然后计算其差值:

cpu usage=(idle2-idle1)/(cpu2-cpu1)*100
cpu usage=[(user_2 +sys_2+nice_2) - (user_1 + sys_1+nice_1)]/(total_2 - total_1)*100

以下用分别用bash和perl做的一个cpu利用率的计算:

total_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实现

#!/bin/sh
## echo user nice system idle iowait irq softirq
CPULOG_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 1
CPULOG_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实现

#!/usr/bin/perl
use 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=<JIFF>;
print JIFF_TMP $jiff_0[0] ;
close (JIFF);
sleep $SLEEPTIME;
open (JIFF, "/proc/stat") || die "Can't open /proc/stat file!\n";
@jiff_1=<JIFF>;
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);

转载于:https://my.oschina.net/fileoptions/blog/1649492

CPU使用率和负载Load计算方法相关推荐

  1. Top,vmstat命令排查CPU使用率,负载问题

    ##Top,vmstat命令排查CPU使用率,负载问题 LINUX服务器CPU持续保持高使用率,则会对系统稳定性和业务运行造成影响. CPU使用率/负载查看命令: 第一种方法:使用vmstat命令查看 ...

  2. 一次详尽的问题定位记录:CPU使用率低负载高的排查过程

    历史原因,当前有一个服务专门用于处理mq消息,mq使用的阿里云rocketmq,sdk版本1.2.6(2016年). 随着业务的发展,该应用上的consumer越来越多,接近200+,导致该应用所在的 ...

  3. CPU 使用率低 负载高的原因

    原因总结 产生的原因一句话总结就是:等待磁盘I/O完成的进程过多,导致进程队列长度过大,但是cpu运行的进程却很少,这样就体现到负载过大了,cpu使用率低. 下面内容是具体的原理分析: 在分析负载为什 ...

  4. cpu使用率低负载高,原因分析(转)

    原因总结 产生的原因一句话总结就是:等待磁盘I/O完成的进程过多,导致进程队列长度过大,但是cpu运行的进程却很少,这样就体现到负载过大了,cpu使用率低. 下面内容是具体的原理分析:在分析负载为什么 ...

  5. cpu使用率低负载高,原因分析-----举例命令排查过程

    原因总结 产生的原因一句话总结就是:等待磁盘I/O完成的进程过多,导致进程队列长度过大,但是cpu运行的进程却很少,这样就体现到负载过大了,cpu使用率低. 下面内容是具体的原理分析:在分析负载为什么 ...

  6. cpu使用率低负载高

    cpu低而负载高也就是说等待磁盘I/O完成的进程过多,就会导致队列长度过大,这样就体现到负载过大了,但实际是此时cpu被分配去执行别的任务或空闲,具体场景有如下几种. 场景一:磁盘读写请求过多就会导致 ...

  7. linux load averages 和 cpu 使用率

    Load Averages 这里的 Load Averages 指的是系统平均负载(System Load Averages),包括正在运行的进程和正在等待的进程对于系统的压力,不仅包括正在运行的进程 ...

  8. linux php cpu,理解Linux CPU负载和 CPU使用率

    CPU负载和 CPU使用率 这两个从一定程度上都可以反映一台机器的繁忙程度. cpu使用率反映的是当前cpu的繁忙程度,忽高忽低的原因在于占用cpu处理时间的进程可能处于io等待状态但却还未释放进入w ...

  9. Linux CPU 100%问题 | 理解 CPU负载和 CPU使用率

    CPU 负载和 CPU 使用率 这两个从一定程度上都可以反映一台机器的繁忙程度. CPU 使用率反映的是当前 CPU 的繁忙程度,忽高忽低的原因在于占用 CPU 处理时间的进程可能处于 IO 等待状态 ...

  10. 1、cpu平均负载和cpu使用率 排查cpu使用高问题

    自定义标题 1. CPU 2. CPU 使用率 3. 测试: 4. 总结 1. CPU cpu平均负载 uptime 或 top 查看 cpu 平均负载(平均负载是指单位时间内,系统处于可运行状态和不 ...

最新文章

  1. Android开发之解决ListView和ScrollView滑动冲突的方法
  2. 中台唯一的胜利果实:大数据中台架构详解
  3. Intel Skylake (Server) 架构/微架构/流水线 (5) - 非时效存储
  4. oracle 10g安装企业版,企业版Oracle10g的安装-过程
  5. Lotus Notes 中导航的键盘快捷方式
  6. js 递归树结构数组
  7. 数据结构图之二(最小生成树--普里姆算法)
  8. 清华大学计算机系网络课程视屏,清华大学计算机系网络课程
  9. TCP/IP四层协议模型与ISO七层模型(TCP/IP系统学习(2))
  10. 计算机毕业设计-springboot协同办公管理系统-公文流转系统代码设计-多人协同办公讨论管理系统
  11. 计算主波长色纯度色温和色坐标转换CIE1931图色坐标显示NTSC色饱和度
  12. hihocoder 1378(有向图求最小割集)
  13. PostGIS几何图形操作
  14. 百度 嵌入式Linux软件研发工程师面试记录
  15. Caesar解密-number theroy
  16. ipv6 无状态地址管理
  17. 读取OSGB数据的几种方式
  18. lol服务器修复失败,LOL客户端BUG遍地,服务器瘫痪长达一天!官方修复后毫无补偿引不满...
  19. 【PPT】绘制表格和多样式圆圈
  20. python 读取数据出现UnicodeDecodeError:: ‘utf-8‘ codec can‘t decode byte 0xc8 in position 0: invalid contin

热门文章

  1. 基于组态王和三菱PLC的modbus仿真(一)——安装三菱PLC环境
  2. qlistview 自定义控件_QT中QListView中放置自定义控件并添加滚动条
  3. Ubuntu上完美运行QQ
  4. 数据恢复工具 winhex使用教程
  5. 缓冲器(跟随器)电路
  6. 字符串:输入与缓冲区问题
  7. SQL语句多表连接查询语法
  8. 使用 Python爬虫+OpenCV 通过摄像头 二维码识别 来得到官方接口的反馈数据 从而实现实时准确地 检测健康码状态(获取JS动态数据)
  9. PDF转换工具安装教程
  10. C4D阿诺德Arnold渲染器介绍