CPU负载均衡之PELT

前言

记录一个知识点,三个action:

  1. PELT 是用来做什么的?
  2. PELT 计算方式?
  3. code流程?

1. PELT概念说明

PELT 即 per-entrity load tracing,用来统计各个TASK 实体的负载情况;
task 实体即task和task group,task可以等同于进程;

1.1 前置概念

负载:负载是一个瞬时量,即表示在某一个时间点,runnable状态的task对CPU造成的压力;
CPU使用率:是一个累积量,表示一段时间内某个task占用总时间片的比例;

则我们平时看的/proc/loadavg是负载,但是它是计算的一段时间的值,是个趋势变化;

1.2 CFS 中load 统计方式:

CFS中原来的负载检测是通过per-rq load tracing来进行的:
- 可以了解到每个组或者cpu对于load的贡献;
- 该load可用于后续group task的分配;
- 不够细化,无法确认到具体load高的程序;
所以为了更好的调度(通过过去负载情况来预测未来的负载情况),在Linux3.18中引入PELT,将上述统计细化到与每个task entrity相关;

1.3 PELT的优势

  1. 了解每个实体的负载情况,并且知道每个CPU的负载情况,则可以推算出某一个task迁移后的情况是怎样的,即负载均衡措施更加有效;
  2. small-task packing patch的目标是将“小”进程收集到系统中的部分CPU上,从而允许系统中的其他处理器进入低功耗模式,利用per-entity load tracking,内核可以知道哪些是小的进程,并将其集中到CPU上
  3. 可以轻松统计每个CPU的使用情况

2. 计算

整体的计算思路:

  1. 计算当前周期内的CPU使用率作为负载
  2. 添加衰减因子,即某周期的负载值对CPU的影响随周期进行逐步减小
  3. task 实体对于CPU的影响为,每个周期负载影响值的累加

2.1 理论公式

公式:
L = L0 + L1*y + L2 * y2 + L3*y3 + … + L32*y32 + …
Ln = Ln-1 * y + L0

其中:

  • L0 表示task实体当前周期(1024us)内的实际使用率
  • y表示衰减因子,即每经过一个周期,Ln*y
  • 32个周期后衰减为原来的1/2,即code中规定y32=1/2
    周期以及task耗时示意

2.2 实际计算

实际计算时需要考虑不满足一个周期的情况,则将整体的task running时间划分为三个阶段:d1、d2、d3

  • d1 为开始运行时不足一个周期的阶段
  • d2 为中间多个完整的周期
  • d3 为最新的不足一个周期的阶段

则完整负载sum=d1+d2+d3,如下图:
三个阶段划分

2.3 关于衰减因子计算

在这个过程中为了减少计算耗时,将涉及次方运算的相关过程,用数组存储了起来:

  1. y32=1/2 ,提供y的n次方常数数组:
static const u32 runnable_avg_yN_inv[] __maybe_unused = {
	0xffffffff, 0xfa83b2da, 0xf5257d14, 0xefe4b99a, 0xeac0c6e6, 0xe5b906e6,
	0xe0ccdeeb, 0xdbfbb796, 0xd744fcc9, 0xd2a81d91, 0xce248c14, 0xc9b9bd85,
	0xc5672a10, 0xc12c4cc9, 0xbd08a39e, 0xb8fbaf46, 0xb504f333, 0xb123f581,
	0xad583ee9, 0xa9a15ab4, 0xa5fed6a9, 0xa2704302, 0x9ef5325f, 0x9b8d39b9,
	0x9837f050, 0x94f4efa8, 0x91c3d373, 0x8ea4398a, 0x8b95c1e3, 0x88980e80,
	0x85aac367, 0x82cd8698,
};
  1. 运行周期内,多个周期累加和使用数组存储, 1024 * (y + y2 + … + yn
/*
 * Precomputed \Sum y^k { 1<=k<=n }.  These are floor(true_value) to prevent
 * over-estimates when re-combining.
 */

static const u32 runnable_avg_yN_sum[] = {
	    0, 1002, 1982, 2941, 3880, 4798, 5697, 6576, 7437, 8279, 9103,
	 9909,10698,11470,12226,12966,13690,14398,15091,15769,16433,17082,
	17718,18340,18949,19545,20128,20698,21256,21802,22336,22859,23371,
};
  1. 以32个period累加和为基准,再次套娃:
/*
 * Precomputed \Sum y^k { 1<=k<=n, where n%32=0). Values are rolled down to
 * lower integers. See Documentation/scheduler/sched-avg.txt how these
 * were generated:
 */32个周期为基准的累加和 
static const u32 __accumulated_sum_N32[] = {
	    0, 23371, 35056, 40899, 43820, 45281,
	46011, 46376, 46559, 46650, 46696, 46719,
};

需要注意:

  • 此处设计基本是以32位一次循环,猜测是因为彼时CPU主要为32bit的;
  • 上述公式累加和在一直运行的时候为等比数列,在等比为1/2时的图样
    • 具有极限值

3. code 跟踪

本部分主要为查看code部分,上述公式被实现的过程

3.1 核心函数图示

在这里插入图片描述

3.2 核心结构

  1. sched_avg
变量说明
last_update_time上一次负载更新时间。用于计算时间间隔。
load_sum基于可运行(runnable)时间的负载贡献总和。runnable时间包含两部分:一是在rq中等待cpu调度运行的时间,二是正在cpu上运行的时间
util_sum基于正在运行(running)时间的负载贡献总和。running时间是指调度实体se正在cpu上执行时间
load_avg基于可运行(runnable)时间的平均负载贡献
util_avg基于正在运行(running)时间的平均负载贡献
  1. decayed 表示是否衰减
  2. delta表示task运行的时间
  3. period_contrib表示上次剩余的时间
3.1.3 decay_load 计算经过n个period衰减的值

decay_load

/*
 * Approximate:
 *   val * y^n,    where y^32 ~= 0.5 (~1 scheduling period)
 */
static __always_inline u64 decay_load(u64 val, u64 n)
{
// this function is to calc y^n^ 计算y的n次
	unsigned int local_n;
// y^0^ = 1, y^32^ ^*^ ^63^ = 0;
	if (!n)
		return val;
	else if (unlikely(n > LOAD_AVG_PERIOD * 63))
		return 0;

	/* after bounds checking we can collapse to 32-bit */
	local_n = n;

	/*
	 * As y^PERIOD = 1/2, we can combine
	 *    y^n = 1/2^(n/PERIOD) * y^(n%PERIOD)
	 * With a look-up table which covers y^n (n<PERIOD)
	 *
	 * To achieve constant time decay_load.
	 */
//这里将计算过程划分为两块:
// 1. 完整32周期数:val值右移对应周期数;
// 2. 不足32周期的数值,查表取值;	 
	if (unlikely(local_n >= LOAD_AVG_PERIOD)) {
		val >>= local_n / LOAD_AVG_PERIOD;
		local_n %= LOAD_AVG_PERIOD;
	}
// 计算不足32周期的数值,为避免浮点运算,直接使用上述数组,这里是涉及到32bit和64bit的转换
	val = mul_u64_u32_shr(val, runnable_avg_yN_inv[local_n], 32);
	return val;
}

总结下函数内容:

  1. 判断n值,对于界限之外的值直接返回;
  2. y32=1/2,根据2进制运算机制,32次右移一位;
  3. 对于不足32次方的情况,查表取值避免浮点运算耗时;
3.14 __compute_runnable_contrib 计算连续完整period(ps:即上述图示d2)的累加和

封装出来此函数通过数组计算,避免运算耗时,对于runnable状态的task,计算出来如下两个数组:

  1. 1~32个period周期的累加和;
  2. 以32个period为基准计算出11个周期的累加和;
    d2
/*
 * For updates fully spanning n periods, the contribution to runnable
 * average will be: \Sum 1024*y^n
 *
 * We can compute this reasonably efficiently by combining:
 *   y^PERIOD = 1/2 with precomputed \Sum 1024*y^n {for  n <PERIOD}
 */
static u32 __compute_runnable_contrib(u64 n)
{
	u32 contrib = 0;
//n值判断:
//对于32 period以内,直接查表取值
//对于345周期以上,直接返回最大值,等比数列求和,比值<1具有极限值;
	if (likely(n <= LOAD_AVG_PERIOD))
		return runnable_avg_yN_sum[n];
	else if (unlikely(n >= LOAD_AVG_MAX_N))
		return LOAD_AVG_MAX;

	/* Since n < LOAD_AVG_MAX_N, n/LOAD_AVG_PERIOD < 11 */
//n/32 查表取值,然后再 * y^n%32^ 次,以n=35为例,就是每个周期的val再*y^3^
	contrib = __accumulated_sum_N32[n/LOAD_AVG_PERIOD];	
	n %= LOAD_AVG_PERIOD;
	contrib = decay_load(contrib, n);
//对 n%32 查表取值,然后求和	
	return contrib + runnable_avg_yN_sum[n];
}

总结该函数内容:

  1. 判断周期数
    1. <=32 则查表返回
    2. “>=345 则取极限值”
  2. 对于周期数在中间的情况:
    1. n/32 查表取值
    2. n%32查表取值
    3. 对上述第一步和第二步的结果求和;
      这个函数抽离出来封装就是为了求去完整周期数的结果,并且将其中次方运算都转换为查表,效率极高;
3.1.5 __update_load_avg

task实体负载更新计算的核心函数

图示
关键步骤:

period计算:
1. delta = (now - last_update_time) >> 10 // 这里转换为1024ns(作为1us)
2. delta_w = sa->period_contrib // 这个是上次更新时完整周期之外的数值,假设第一次此值为A,则第二次为C(对应上图)
3. delta + delta_w > 1024 period
1. delta_w = 1024 - delta_w
2. delta -= delta_w
3. period = delta / 1024
4. delta %= 1024

load计算:
load_sum = decay_load(load_sum, period + 1)
runnable_load_sum = decay_load(runnable_load_sum, period + 1)
util_sum = decay_load(util_sum, period + 1)

完整周期load计算:
contrib = __compute_runnable_contrib(periods)
contrib = contrib * scale_freq / 1024

更新新的period_contrib
period_contrib += delta


//更新的入口函数
static __always_inline int __update_load_avg(u64 now, int cpu, struct sched_avg *sa, unsigned long weight, int running, struct cfs_rq *cfs_rq)
{
	u64 delta, scaled_delta, periods;
	u32 contrib;
	unsigned int delta_w, scaled_delta_w, decayed = 0;
	unsigned long scale_freq, scale_cpu;

//计算当前时间与上次统计时间的差值,即上图中B+C+d2
	delta = now - sa->last_update_time;
	/*
	 * This should only happen when time goes backwards, which it
	 * unfortunately does during sched clock init when we swap over to TSC.
	 */
//如果delta<0 则直接返回	 
	if ((s64)delta < 0) {
		sa->last_update_time = now;
		return 0;
	}

	/*
	 * Use 1024ns as the unit of measurement since it's a reasonable
	 * approximation of 1us and fast to compute.
	 */
// 以1024ns为delta方便计算,更接近us	 
	delta >>= 10;
	if (!delta)
		return 0;
	sa->last_update_time = now;

//获取freq和capacity
	scale_freq = arch_scale_freq_capacity(NULL, cpu);
	scale_cpu = arch_scale_cpu_capacity(NULL, cpu);

	/* delta_w is the amount already accumulated against our next period */
	delta_w = sa->period_contrib;//这个值是上次统计时间整数周期以外剩余的time,假设为上图中A,则对应下次此值为C
//超过一个period	
	if (delta + delta_w >= 1024) {
		decayed = 1;

		/* how much left for next period will start over, we don't know yet */
		sa->period_contrib = 0;

		/*
		 * Now that we know we're crossing a period boundary, figure
		 * out how much from delta we need to complete the current
		 * period and accrue it.
		 */
		delta_w = 1024 - delta_w;
		scaled_delta_w = cap_scale(delta_w, scale_freq);
		if (weight) {
			sa->load_sum += weight * scaled_delta_w;
			if (cfs_rq) {
				cfs_rq->runnable_load_sum += weight * scaled_delta_w;
			}
		}
		if (running)
			sa->util_sum += scaled_delta_w * scale_cpu;

		delta -= delta_w;// delta = delta - 1024 + period_contrib,即上图中A+B+C+d2 -1 

		/* Figure out how many additional periods this update spans */
		periods = delta / 1024;
		delta %= 1024;

		sa->load_sum = decay_load(sa->load_sum, periods + 1);
		if (cfs_rq) {
			cfs_rq->runnable_load_sum = decay_load(cfs_rq->runnable_load_sum, periods + 1);
		}
		sa->util_sum = decay_load((u64)(sa->util_sum), periods + 1);

		/* Efficiently calculate \sum (1..n_period) 1024*y^i */
	//计算完整周期数的负载情况,即上述d2	
		contrib = __compute_runnable_contrib(periods);
		contrib = cap_scale(contrib, scale_freq);
		if (weight) {
			sa->load_sum += weight * contrib;
			if (cfs_rq)
				cfs_rq->runnable_load_sum += weight * contrib;
		}
		if (running)
			sa->util_sum += contrib * scale_cpu;
	}

	/* Remainder of delta accrued against u_0` */
	scaled_delta = cap_scale(delta, scale_freq);
	if (weight) {
		sa->load_sum += weight * scaled_delta;
		if (cfs_rq)
			cfs_rq->runnable_load_sum += weight * scaled_delta;
	}
	if (running)
		sa->util_sum += scaled_delta * scale_cpu;

	sa->period_contrib += delta;//如果这次不够一个周期则累加,如果多余一个周期则做减法后取模

	if (decayed) {
		sa->load_avg = div_u64(sa->load_sum, LOAD_AVG_MAX);
		if (cfs_rq) {
			cfs_rq->runnable_load_avg =
				div_u64(cfs_rq->runnable_load_sum, LOAD_AVG_MAX);
		}
		sa->util_avg = sa->util_sum / LOAD_AVG_MAX;
	}

	return decayed;
}

此函数功能总结:

  1. 计算周期数
  2. 根据之前的load情况,计算现在最新的load情况,上文结构体部分有说明主要的几个变量;
    1. 通过decay_load计算load_sum、load_avg、util_sum、util_avg
    2. 计算完整周期中的负载情况,并根据配置确认是否添加;
  3. 返回是否衰减

4. 附录

4.1 Android中PELT

PELT在Linux 3.8引进,4.18后独立文件出来,但是在android kernel 4.9中并没有对应文件,android中采用的是WALT?
==> kernel 4.9中pelt部分在fair.c中,没有详细跟踪其他版本,不确认是否有其他差异,不过原理肯定是一致的;
2020.07.04 update:
1. V3.8 中引入在fair.c中,v 4.9 同样在fair.c中
2. V4.19中独立出来在 pelt中,已经拆离为 d1 + d2 + d3 的计算方式了

4.2 参考资料:

https://blog.csdn.net/liglei/article/details/82896765
https://baijiahao.baidu.com/s?id=1658677148287234806&wfr=spider&for=pc

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值