CPU负载均衡之loadavg计算

前言

本文分析关于loadavg 使用、计算相关内容(基于kernel 4.9),如下图:

其中重点介绍CPU负载计算过程相关内容(源码网站参考:opengrok 和 bootlin);

文章目录

  • CPU负载均衡之loadavg计算
    • 前言
    • 1. CPU负载
      • 1.1 一个味道很重的比喻
      • 1.2 核心概念说明
    • 2. loadavg使用
      • 2.1 查看CPU 负载
      • 2.2 CPU负载数据的分析说明
    • 3. 负载计算
      • 3.1 流程示意
      • 3.2 code跟踪
        • 3.2.1 文件目录:
        • 3.2.2 loadavg 显示流程
        • 3.2.3 avenrun 计算
        • 3.2.4 计算active
        • 3.2.4 这里有个很好玩的计算
    • 问题记录

1. CPU负载

负载:当前队列中排队的Task+正在running的Task数量之和与CPU能力的比值

1.1 一个味道很重的比喻

这个概念又一个很形象的比喻:

  1. 游乐场很好玩,人超多,对应Task;
  2. 只有一个厕所(也可能有2、3、4个),对应CPU的核数;
  3. 一旦节假日去到游乐场,就会发现厕所排了很长的队;

这种情况就是满负载了,嗯,有内个味儿了!!!

1.2 核心概念说明

name 说明
active task 对应每个CPU上nr_running和nr_uninterruptible的task,在runqueue结构体中维护
clac_load_tasks 对应所有CPU中active的task之和
avnrun sched中提供数组,用于记录和计算1、5、15分钟时的平均负载值,与直观的load显示比值为2048
NO_HZ kernel中定义的系统频率,倒数即1s的节拍数,每个节拍提供中断用于各类task使用,NO_HZ即在CPU空闲时关闭周期性的中断

2. loadavg使用

2.1 查看CPU 负载

  1. Linux、android的debug系统已经很好的集成了loadavg功能,再出现ANR、SWT等异常时,抓取的log信息中均会有当时loadavg情况;
  2. 可以通过系统提供命令查看
    • cat /proc/loadavg
    • uptime
    • dumpsys cpuinfo
    • top (top中没有显示loadavg)

2.2 CPU负载数据的分析说明

  1. 三个数值分别代表:1分钟、5分钟、15分钟内的平均负载;
  2. 一个核满负载为1,一般的4核CPU满负载就是4,也存在只有一个核但是负载为3的情况,这说明CPU成为了block点;
  3. 可以看到的是统计的最小粒度为1min,其实这个统计主要代表一个趋势性的说明,即偶尔出现满负载并不可怕,如果一直长时间持续,则说明或者系统出现问题或者系统与HW资源不匹配了;
  4. 按照以往经验,如果负载一直维持在70%以内,那说明系统整体状况还是比较健康的,但是并不说明系统没有问题(可能这70%是异常task造成的)
  5. DVFS就是根据这个负载情况调节频率和电压的;

3. 负载计算

核心计算步骤如下三步:

计算每个CPU当前活跃task数量:active task = nr_running和nr_uninterruptible
在update的周期到来时,将nr_active更新到全局变量calc_load_tasks中;
根据oldload和当前活跃task计算新负载:newload = load * exp + active * (FIXED_1 - exp);

3.1 流程示意


上述图示展示了整个数据更新的过程,如下详细解释说明下:

  1. 涉及的数据:

active:每个CPU上的运行Task
calc_load_idle:在cpu进入idle时更新此值,避免进入idle后没有中断,在update周期到来时无法更新
calc_load_tasks:全局变量,记录所有Task sum,实际做指数平均的active;
avenrun:每个period计算出来的最终load,/proc/loadavg即读取这个值;

  1. 更新时机

每次shcedule_tick调用calc_load_fold_active,在到达更新时机(即5s)时获取每个CPU的running和uninterruptible数值,更新每个CPU的active;
每次tick_nohz_stop_sched_tick()进入Idle时调用calc_load_enter_idle获取当前CPU的active更新到calc_load_idle,提供给到之后使用;
每次do_timer调用在到达更新时机(即5s+10tick)时根据calc_load_tasks、EXP(权重)、当前load计算新的load值;

所谓更新时机,可参照这个图:

3.2 code跟踪

3.2.1 文件目录:

id 路径
1 /kernel/fs/proc/loadavg.c
2 /kernel/kernel/shcedule/loadavg.c
3 /kernel/sched/sched.h
4 /kernel/sched/core.c
5 /kernel/timer/timekeeping.c

3.2.2 loadavg 显示流程

所有loadavg的使用其实都是基于/proc/loadavg,首先将这里做为入口:

//ps:这里应该是基于5.72的,核心步骤不影响
static int loadavg_proc_show(struct seq_file *m, void *v){unsigned long avnrun[3];get_avenrun(avnrun, FIXED_1/200, 0);//获取avnrun//写入loadavg,分别计算整数部分和小数部分       seq_printf(m, "%lu.%02lu %lu.%02lu %lu.%02lu %ld/%d %d\n",LOAD_INT(avnrun[0]), LOAD_FRAC(avnrun[0]),LOAD_INT(avnrun[1]), LOAD_FRAC(avnrun[1]),LOAD_INT(avnrun[2]), LOAD_FRAC(avnrun[2]),nr_running(), nr_threads,idr_get_cursor(&task_active_pid_ns(current)->idr) - 1);//这里就是最后last_pidreturn 0;}static int __init proc_loadavg_init(void){proc_create_single("loadavg", 0, NULL, loadavg_proc_show);return 0;}//创建节点,显示内容为loadavg_proc_show
fs_initcall(proc_loadavg_init);//系统启动时fs_initcall中注册init函数
  1. 在系统启动的时候将proc_loadavg_init启动起来;
  2. 核心函数为获取avenrun,然后分别计算整数部分和小数部分;

这里需要理解的几个概念:

avnrun,这个是从sched中直接获取过来的,中间经过一个简单转换(添加了10.24)
nr_running 理解为number of runnable task
LOAD_IN 右移11位,即取高位
LOAD_FRAC 取低11位,运算乘100

定义如下:

#define FSHIFT       11      /* nr of bits of precision */
#define FIXED_1     (1<<FSHIFT)   /* 1.0 as fixed-point */
#define LOAD_FREQ   (5*HZ+1)   /* 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) */#define LOAD_INT(x) ((x) >> FSHIFT) //计算整数
#define LOAD_FRAC(x) LOAD_INT(((x) & (FIXED_1-1)) * 100)//计算小数

3.2.3 avenrun 计算

  1. proc下的avenrun数组是从sched下的avenrun经过运算得到的:
void get_avenrun(unsigned long *loads, unsigned long offset, int shift)
{loads[0] = (avenrun[0] + offset) << shift;loads[1] = (avenrun[1] + offset) << shift;loads[2] = (avenrun[2] + offset) << shift;//则对于运算来说就是添加了一个offset,即2048 / 200 / 2048=0.005 , 负载只取小数点后2位,所以添加了一个类似于四舍五入操作;
}
  1. 在code中搜索,avenrun数组的计算赋值操作,即每个周期的计算load操作:

    • loadavg.c中calc_global_load函数中,完整计算;
      则可以很直观的看到avenrun(load average array)是通过calc_load相关函数更新的,此部分为详细计算过程:
/** calc_load - update the avenrun load estimates 10 ticks after the* CPUs have updated calc_load_tasks.** Called from the global timer code.*/
void calc_global_load(unsigned long ticks)
{unsigned long sample_window;long active, delta;sample_window = READ_ONCE(calc_load_update);
//没到更新时间5s + 10tick 则退出    if (time_before(jiffies, sample_window + 10))return;/** Fold the 'old' NO_HZ-delta to include all NO_HZ CPUs.*/
//这里首先会去读取load idle 的值,并将其更新到calc_load_tasks;  delta = calc_load_nohz_read();if (delta)atomic_long_add(delta, &calc_load_tasks);
//获取active,实质就是读取calc_load_tasksactive = atomic_long_read(&calc_load_tasks);active = active > 0 ? active * FIXED_1 : 0;
//根据权重运算avenrunavenrun[0] = calc_load(avenrun[0], EXP_1, active);avenrun[1] = calc_load(avenrun[1], EXP_5, active);avenrun[2] = calc_load(avenrun[2], EXP_15, active);
//周期更新;WRITE_ONCE(calc_load_update, sample_window + LOAD_FREQ);/** In case we went to NO_HZ for multiple LOAD_FREQ intervals* catch up in bulk.*/
//这里是用来cover一种情况,存在多个周期(n * 5s)在Idle,则一直没有更新;calc_global_nohz();
}/** a1 = a0 * e + a * (1 - e)*/
static inline unsigned long
calc_load(unsigned long load, unsigned long exp, unsigned long active)
{unsigned long newload;
//核心计算公式:newload = load * exp + active * (FIXED_1 - exp);
//如果新增active比之前的完整还多,则newload + 1,体现趋势   if (active >= load)newload += FIXED_1-1;return newload / FIXED_1;
}

这里的核心内容就是计算avenrun,即获取到active的和,使用指数平均的公式计算最终的load:

  1. 确认已经达到了统计时间,默认为5*HZ+10tick
  2. 获取active数量,直接读取calc_load_tasks(所有增加的和);
  3. 更新数组内容,通过公式计算:a1 = a0 * e + a * (1 - e) ,则这个公式:
    1. 分别在1、5、15分钟的条件下历史load的权重是不断增加的,也就是说新增加的active数量对于越久时间的统计影响越小(1884、2014、2037)
    2. 新增加的active task的影响是慢慢变缓的,即时间越久,整体计算出来的数据越趋向于active_task

3.2.4 计算active

在code中搜索,active的计算赋值操作为:

active = atomic_long_read(&calc_load_tasks);
active = active > 0 ? active * FIXED_1 : 0;

是直接读取的calc_load_tasks,更新这个变量的位置:

  1. atomic_long_add(delta, &calc_load_tasks),每次scheduler_tick时检测到达更新周期时add;
  2. delta = calc_load_fold_idle(),进入idle更新到数组中,在do_timer进入global计算时更新;
/*
* Called from scheduler_tick() to periodically update this CPU's
* active count.
*/
void calc_global_load_tick(struct rq *this_rq)
{long delta;
// now is befor update tick, do nothingif (time_before(jiffies, this_rq->calc_load_update))return;
// now is in update tick ,read the per-cpu nr_activedelta  = calc_load_fold_active(this_rq, 0);if (delta)atomic_long_add(delta, &calc_load_tasks);
//normal update, is rqthis_rq->calc_load_update += LOAD_FREQ;
}

上述函数是per-cpu更新的位置;

//进入idle的时候将数据更新到calc_load_idle数组中,需要根据idx和时间判断具体添加位置
void calc_load_enter_idle(void)
{struct rq *this_rq = this_rq();long delta;// when the cpu is enter idle, update the load to calc_load_idle;
// we will use idx to choose if this load is used in this update tick;delta = calc_load_fold_active(this_rq, 0);if (delta) {int idx = calc_load_write_idx();atomic_long_add(delta, &calc_load_idle[idx]);}
}
//决策添加的数组,这部分内容看下图理解会更方便
static inline int calc_load_write_idx(void)
{int idx = calc_load_idx;/** See calc_global_nohz(), if we observe the new index, we also* need to observe the new update time.*/smp_rmb();/** If the folding window started, make sure we start writing in the* next idle-delta.*/
//qyp :core, if per-cpu is calc load and then this perid idle update is used in next freqif (!time_before(jiffies, calc_load_update))idx++;return idx & 1;
}//在global更新的时候读取对应的数组内容;
static long calc_load_fold_idle(void)
{int idx = calc_load_read_idx();long delta = 0;
// use idx to choose the calc_load_idle, and return itif (atomic_long_read(&calc_load_idle[idx]))delta = atomic_long_xchg(&calc_load_idle[idx], 0);return delta;
}
//读取idx
static inline int calc_load_read_idx(void)
{return calc_load_idx & 1;
}
//退出idle的时候判断当前jiffies,即如果已经进入下个周期,则更新时间点
void calc_load_exit_idle(void)
{struct rq *this_rq = this_rq();
// use the globle calc_load_update to forbidden the rq update tick is multi period no update      this_rq->calc_load_update = calc_load_update;
//if now is before per-cpu update tick : return     if (time_before(jiffies, this_rq->calc_load_update))return;
//if now is before avnrun update tick : update the update tick,because we can not enter the tick to update it;      if (time_before(jiffies, this_rq->calc_load_update + 10))this_rq->calc_load_update += LOAD_FREQ;
}

这个过程比较比较复杂,首先查看比较古老的版本,学习思想,再回来查看其设计方法:

  1. 实质:计算每个CPU上的nr_running和uninterrupt_sleep的数量,并返回;
  2. 过程中添加了一个记录idle状态的全局变量,用于更新当前的active数值,这里搞这么复杂实际上是为了解决进入IDLE后没有中断更新load的情况:
    1. 在进入Idle时,将load 数量更新到idle数组中;
    2. 到达更新点的时候,统一计算完整的idle数组中值,通过数组以及idx自增的方式判断实际的进入Idle时间以及是否需要计算;
    3. 退出idle时需要判断此时是否已经到达cpu计算load的时间点,如果已经到达,则更新update时间,避免重复计算 ;

3.2.4 这里有个很好玩的计算

对于可能存在的多个周期的idle情况,会调用到这里,这里就是会计算N个周期的load情况
通过数学公式计算可以发现其实N个周期后的计算公式为:
an = a0 * e^n + a * (1 - e) * (1 + e + … + e^n-1) [1]
= a0 * e^n + a * (1 - e) * (1 - e^n)/(1 - e)
= a0 * e^n + a * (1 - e^n)
所以这里会直接实现对应en的效果:

static void calc_global_nohz(void)
{long delta, active, n;
// in globle update tickif (!time_before(jiffies, calc_load_update + 10)) {/** Catch-up, fold however many we are behind still*/
//calc the perioddelta = jiffies - calc_load_update - 10;n = 1 + (delta / LOAD_FREQ);active = atomic_long_read(&calc_load_tasks);active = active > 0 ? active * FIXED_1 : 0;
//calc the n avenrunavenrun[0] = calc_load_n(avenrun[0], EXP_1, active, n);avenrun[1] = calc_load_n(avenrun[1], EXP_5, active, n);avenrun[2] = calc_load_n(avenrun[2], EXP_15, active, n);
//update globle tickcalc_load_update += n * LOAD_FREQ;}/** Flip the idle index...** Make sure we first write the new time then flip the index, so that* calc_load_write_idx() will see the new time when it reads the new* index, this avoids a double flip messing things up.*/smp_wmb();calc_load_idx++;
}

上述函数是计算周期N的值,然后调用calc_load_n来计算上述公式:

static unsigned long
calc_load_n(unsigned long load, unsigned long exp,unsigned long active, unsigned int n)
{
// math calc: n period = load * exp ^ n + active * (1-exp^n)return calc_load(load, fixed_power_int(exp, FSHIFT, n), active);
}

那么en是怎么计算的呢?就在下边(通过右移减少计算次数):

static unsigned long
fixed_power_int(unsigned long x, unsigned int frac_bits, unsigned int n)
{unsigned long result = 1UL << frac_bits;// * 2048
//use bit to calc e^n = e^1 * e^2 * e^4if (n) {for (;;) {if (n & 1) {//对应bit有则*xresult *= x;result += 1UL << (frac_bits - 1);result >>= frac_bits;}n >>= 1;if (!n)break;//更新x x^2 x^4 .....        x *= x;x += 1UL << (frac_bits - 1);x >>= frac_bits;}}return result;
}

问题记录

  1. 为什么avenrun计算的三个权重分别为1884、2014、2037,这个是如何决定的?

    这里的系数是按照上述公式决定的即:2048 * (1 / e ^ (5s / period) )
  2. 为什么在5s一次的统计过程中要+10 tick,而且核心计算公式其实很简单,为什么流程要搞这么复杂?
    A:

    1. 由于存在多核情况,在计算各个核的load时和整体load更新时存在竞争,所以修改为在5HZ时各个CPU计算,而在+10tick之后更新给到AVGRUN;
    2. 由于存在nohz的情况,即IDLE时不会有中断更新计算load,所以要引入新的计算补足这一部分;
  3. 关于do_timer和scheduler_tick相关调度的整体逻辑是怎样的?
    A: 实质就是在周期性中断到来的时候,调度对应函数检测是否到达更新周期进行计算;

CPU负载均衡之loadavg计算相关推荐

  1. 查看linux cpu负载均衡,关于linux内核cpu进程的负载均衡

    2.6内核中进程调度模块的负载均衡行为分为"拉"和"推",推这里不考虑,关于拉均衡有一篇文章特别好,具体出处就不记得了,我当时用的百度快照,那篇文章我认为最精彩 ...

  2. 一文读懂 | CPU负载均衡实现

    在<一文读懂 | 进程怎么绑定 CPU>这篇文章中介绍过,在 Linux 内核中会为每个 CPU 创建一个可运行进程队列,由于每个 CPU 都拥有一个可运行进程队列,那么就有可能会出现每个 ...

  3. linux内核死锁检测机制 | oenhan,Linux内核CPU负载均衡机制 | OenHan

    还是神奇的进程调度问题引发的,参看Linux进程组调度机制分析,组调度机制是看清楚了,发现在重启过程中,很多内核调用栈阻塞在了double_rq_lock函数上,而double_rq_lock则是lo ...

  4. linux进程网络均衡,linux多CPU进程负载均衡解析

    在linux中,支持对称smp的处理器模型,在多处理器的情况下,每个处理器都有自己的一个运行队列,这样就存在着分配不均的情况,有的cpu运行队列很多进程,导致一直很忙,有的cpu运行队列可能很少的进程 ...

  5. (2)Linux进程调度器-CPU负载

    目录 背景 1. 概述 2. 全局CPU平均负载 2.1 基础概念 2.2 流程 2.3 计算方法 3. 运行队列CPU负载 4. PELT 4.1 PELT计算方法 4.2 PELT计算调用 背景 ...

  6. linux在多核处理器上的负载均衡原理

    原文出处:http://donghao.org/uii/ [原理] 现在互联网公司使用的都是多CPU(多核)的服务器了,Linux操作系统会自动把任务分配到不同的处理器上,并尽可能的保持负载均衡.那L ...

  7. 分布式基础-负载均衡

    前言 我理解要分布式系统,无非两个原因数据和计算,单机系统无法保存这么大的数据量,所以要分布式系统来保存; 计算嘛,就是说单机计算无法达到性能要求,比如双十一一台机器肯定处理不过来,所以需要后台一系列 ...

  8. Linux系统cpu负载浅析

    cpu负载就是cpu在一段时间内正在处理以及等待cpu处理的进程数之和的统计信息,也就是cpu使用队列的长度统计信息,这个数字越小越好. 维基百科关于系统负载的解释: the system Load ...

  9. 现代网络负载均衡和代理技术

    作者:Matt Klein 译者:杨泽 原题:Introduction to modern network load balancing and proxying 关于现代网络负载均衡和代理(prox ...

  10. CPU使用率和CPU负载的区别

    1.CPU使用率 CPU使用率指的是程序在运行期间实时占用的CPU百分比,这是对一个时间段内CPU使用状况的统计.通过这个指标可以看出在某一个时间段内CPU被占用的情况. 2.cpu负载 2.1 lo ...

最新文章

  1. Odoo进销存业务学习笔记
  2. 未来教育python视频百度云-2019年计算机二级Python语言程序设计考试大纲
  3. matlab0-255的颜色映射到0-1,python中的matlab颜色映射
  4. javascript控制html高,Javascript可以控制css吗?
  5. Linux:-bash: ***: command not found
  6. Android异常总结---W/System.err(661): java.lang.IllegalStateException: End of document.
  7. POJ2987 Firing 最大权闭合图
  8. python整型为空的情况_深度剖析凭什么python中整型不会溢出
  9. 【RDMA】技术详解(四):RDMA之Verbs和编程步骤
  10. 【数据结构与算法】之深入解析“香槟塔”的求解思路与算法示例
  11. 关于cocoapods遇到xxxx file not found和No such module 'XXX'的问题
  12. w7提示无法关闭计算机,番茄花园windows7关闭电脑关机时提示强制关闭程序怎么办...
  13. Excel导入CSV文件(解决数值转换文本问题)
  14. 值得珍藏的免费观影网站
  15. python 两个word文档之间的相似度_如何用 word2vec 计算两个句子之间的相似度?
  16. 计算机组成原理—储存器的层次结构
  17. java程序员自己的图片转文字OCR识图工具
  18. mysql握手_详细介绍mysql 协议的服务端握手包及对其解析
  19. 随机算法 之随机数的产生
  20. 各种饼的做法,先存起来,老了慢慢学着做

热门文章

  1. spring中用到了哪些设计模式
  2. centos6.9安装虚拟机kvm
  3. 半边数据结构(The_Half-Edge_Data_Structure)
  4. Java设计模式-Builder生成器模式
  5. 一句实现jquery导航栏
  6. js对特殊字符转义、时间格式化、获取URL参数
  7. 数据结构——单人名单
  8. 用VS调试 javascript
  9. hdu 1072 Nightmare BFS搜索(可重走)
  10. python3.7.4安装chrome_Python3.7安装chrome驱动方法