Per-Entity Load Tracking

调度实体负载跟踪

Posted by icecube on January 21, 2020

简介

根据调度实体统计的历史负载数据 计算当前系统瞬时负载
v3.8之前的负载统计方式是基于队列的 per cfs-rq, 实际上并不清楚负载究竟来自哪个任务
pelt处理方式,跟踪记录每一个调度实体的负载,运行队列 cfs-rq 负载来源于队列中所有成员的负载总和

好处

  • 跟踪的粒度更小,更精确,调度器可以清晰的掌握每一个任务对系统的负载贡献度
  • 没有增加调度开销,并没有记录保存每一个历史周期的负载数值。
  • 负载均衡调整时,任务在cpu之间迁移时更容易计算效能
  • 动态电压频率调整,电源管理器更容易统计出需要多少算力(消耗多少电量)

衰减公式 

负载是一个瞬时量,时间越靠近当下,影响越大,负载贡献度越高,时间越久远,影响越小,负载贡献度越低
衰减因子就用于在时间维度上调节负载贡献值
负载统计衰减公式

L = L0 + L1y + L2y^2 + L3y^3 + … + Lny^n

在3.8版本的代码中,y已经确定:y^32 = 0.5
L0 是当前时间的load,y是衰减因子,Ln是基于now往前第n个period的load
实际上 L1 = L2 = … Ln ≈ 1ms(运行时间)

跟踪时间粒度大小为1024us,即以1024us(约等于1ms)为一个周期,
这样选定的y值,一个调度实体的负荷贡献经过32个周期后,对当前时间的的负载贡献值会衰减一半。

跟踪和更新负载从最初运行开始一直都只是保存了一个统计数据,后续更新都是在此数值基础上的乘法运算,都是针对一个数值在更新,不需要使用数组来记录过去的负载贡献,所以并没有保存多余的数据, 只要把上次的负载总和的贡献值乘以y再加上新的负载L0就可以了

更新负载的时机

  • 加入运行队列
  • 离开运行队列
  • 周期性调度器

相关代码

struct sched_avg {
  u64 last_update_time, load_sum;
  u32 util_sum, period_contrib;
  unsigned long load_avg, util_avg;
};

跟踪调度实体的负载  

  • last_update_time 上次负载统计时间,单位ns
  • load_sum 基于runnable时间的负载贡献总和,包括 在rq上runnable等待运行时间 和 在cpu上的running运行时间的 负载贡献总和
  • util_sum 在cpu上的running运行时间的负载贡献总和
  • period_contrib 初始值为1023, 更新负载过程用于保存上一次更新负载之后,没有达到一个周期的1024us的一个余数
  • load_avg 基于runnable时间的平均负载贡献
  • util_avg 基于running时间的平均负载贡献

优化常量

static const u32 runnable_avg_yN_inv[] = {
    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,
};    

runnable_avg_yN_inv 的每个元素分别表示从当前到过去32个period对应的衰减因子
之所以每个元素数值是当前这种形式,是因为内核为避免浮点运算,把负载计算时涉及到的浮点运算转换成了乘法运算,而且乘法运算使用的乘数也已经计算好了,就是runnable_avg_yN_inv数组的各个元素值.

因为规定y^32 = 0.5 即过去第32个毫秒的负载衰减因子约为0.5
转换为乘数,即runnable_avg_yN_inv[31],就是 0x82cd8698  
换算 0x82cd8698 / pow(2, 32) = 0.511 ≈ 0.5 以此类推其余各个元素值

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,
};  

根据衰减因子预先计算好在不超过32个周期时对应的负载贡献度总和
runnable_avg_yN_inv[1] 0xfa83b2da / pow(2, 32) ≈ 0.978
runnable_avg_yN_inv[2] 0xf5257d14 / pow(2, 32) ≈ 0.957
runnable_avg_yN_sum[2] = 1024(us) * (0.978 + 0.957) ≈ 1982

__update_load_avg v4.4

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;

	delta = now - sa->last_update_time; // 增量运行时间 图中所示delta = (delta_w` + [0,n]*period + delta`)
	/*
	 * This should only happen when time goes backwards, which it
	 * unfortunately does during sched clock init when we swap over to TSC.
	 */
	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.
	 */
	delta >>= 10;  // 转换为us微妙单位
	if (!delta)
		return 0;
	sa->last_update_time = now; // 记录本次更新时间

	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;  //  上一次更新时不足一个周期的余数
	if (delta + delta_w >= 1024) { // 超过一个周期period 1ms
		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; // 即图中 delta_w`
		scaled_delta_w = cap_scale(delta_w, scale_freq);
		if (weight) {  // load [0 - prev] prev包含了delta_w` 是period整数倍
			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_w`

		/* Figure out how many additional periods this update spans */
		periods = delta / 1024; // 整数倍period个数
		delta %= 1024;  // 不到一个周期,多出的余数,对应图中 delta`

		sa->load_sum = decay_load(sa->load_sum, periods + 1); // 衰减负载 [0 - prev] prev包含了delta_w` 是period整数倍
		if (cfs_rq) {
			cfs_rq->runnable_load_sum =
				decay_load(cfs_rq->runnable_load_sum, periods + 1); // cfs_rq负载衰减
		}
		sa->util_sum = decay_load((u64)(sa->util_sum), periods + 1);

		/* Efficiently calculate \sum (1..n_period) 1024*y^i */
		contrib = __compute_runnable_contrib(periods); // 计算连续periods
  个周期的累计负载贡献值
		contrib = cap_scale(contrib, scale_freq);
		if (weight) {
			sa->load_sum += weight * contrib; // 更新sa负载总和
			if (cfs_rq)
				cfs_rq->runnable_load_sum += weight * contrib; // 更新cfs_rq负载总和
		}
		if (running) // running 负载
			sa->util_sum += contrib * scale_cpu;
	}

	/* Remainder of delta accrued against u_0` */
	scaled_delta = cap_scale(delta, scale_freq); // 对应图中 delta`
	if (weight) {  // load of [0 - now]
		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; // 记录不足一个周期的余数,下次更新负载时使用,对应图中 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;
}

[参考链接]
https://lwn.net/Articles/513135/
https://lwn.net/Articles/531853/
http://www.wowotech.net/process_management/PELT.html
http://www.wowotech.net/process_management/450.html
http://osdc.hanyang.ac.kr/sitedata/2015_Linux_Seminar/5_Per_entity_Load_Tracking.pdf