最近笔者在做的一个降级功能,与机器资源情况密切相关。然而在测试时发现控制 CPU 利用率来构造测试条件,并不是一个容易的事情。借助时间片的思想,笔者用一个非常简单的 shell 一定程度上解决了这个问题。但转念一想,对于混沌测试的软件,这应该是个必备能力。查找了一下 chaosblade 的相关资料,果然支持生成指定 CPU 利用率的负载。故读了读其这部分源码,看看它是怎么实现的。

使用 chaosblade 来构造指定 CPU 利用率的负载非常简单:

blade create cpu load --cpu-percent 80

即能够生成使 CPU 利用率到达 80% 的负载。

burncpu 的核心逻辑位于这里: https://github.com/chaosblade-io/chaosblade-exec-os/blob/318c52d83a851bc75012abc7d880d4f440f1f972/exec/bin/burncpu/burncpu.go#L140-L168

func burnCpu() {

	runtime.GOMAXPROCS(cpuCount)

	var totalCpuPercent []float64
	var curProcess *process.Process
	var curCpuPercent float64
	var err error

	totalCpuPercent, err = cpu.Percent(time.Second, false)  // 获取当前所有 CPU 一秒内平均利用率
	// ...
	curProcess, err = process.NewProcess(int32(os.Getpid()))
	// ...
	curCpuPercent, err = curProcess.CPUPercent()  // 获取当前进程的 CPU 利用率
	// ...
	otherCpuPercent := (100.0 - (totalCpuPercent[0] - curCpuPercent)) / 100.0  // 除去已有进程,可操作的 CPU 利用率。值的范围为 [0, 1]
	go func() {
		t := time.NewTicker(3 * time.Second)
		for {
			select {
			// timer 3s
			case <-t.C:
				totalCpuPercent, err = cpu.Percent(time.Second, false)
				// ...
				curCpuPercent, err = curProcess.CPUPercent()
				// ...
				otherCpuPercent = (100.0 - (totalCpuPercent[0] - curCpuPercent)) / 100.0
			}
		}
	}()  // 每 3s 更新一次 totalCpuPercent, curCpuPercent 和 otherCpuPercent

	if climbTime == 0 {  // 不需要爬坡时间
		slopePercent = float64(cpuPercent)  // 爬坡值与目标值一致
	} else {
		// ...
	}

    // cpuCount 是由 runtime.NumCPU() 得来,获取的是当前 CPU 的逻辑核数量
	for i := 0; i < cpuCount; i++ {
		go func() {
			busy := int64(0)
			idle := int64(0)
			all := int64(10000000)    // 设定 10ms 为一个周期
			dx := 0.0
			ds := time.Duration(0)
			for i := 0; ; i = (i + 1) % 1000 {  // 死循环
				startTime := time.Now().UnixNano()
				if i == 0 {  // 每 1000 次进入
                    // 这个赋值语句是整个 burncpu 的灵魂。
                    // 我们最终希望获得的是 slopePercent% 的 CPU 利用率
                    // 应该生成的 CPU 压力即为 (slopePercent - totalCpuPercent[0])%
                    // 理想条件下,我们只需对 (slopePercent - totalCpuPercent[0]) 个 0.1ms 时间片设置为 busy 状态即可
                    // 但由于系统中存在其他进程,burncpu 无法真正获得到 (slopePercent - totalCpuPercent[0]) 个 0.1ms 时间片
                    // 因此需要按比例放大时间片的个数,而这个比例则是当时 burncpu 实际可用的 CPU 利用率
                    // 将这个赋值语句转换为如下方程,则更好理解了:
                    //   slopePercent  = totalCpuPercent + dx * otherCpuPercent
                    //       ^                 ^           ^                 ^
                    // 最终获得的CPU利用率 当前CPU利用率 一个周期内busy的时间片个数 burnCpu真正可操作的CPU比例
					dx = (slopePercent - totalCpuPercent[0]) / otherCpuPercent
					busy = busy + int64(dx*100000)  // 有 dx 个 0.1ms 需要为 busy 状态
					if busy < 0 {
						busy = 0
					}
					idle = all - busy
					if idle < 0 {
						idle = 0
					}
					ds, _ = time.ParseDuration(strconv.FormatInt(idle, 10) + "ns")
				}
				for time.Now().UnixNano()-startTime < busy {
				}  // 阻塞 CPU,使 CPU 位于 busy 状态,直至设定的时间片结束
				time.Sleep(ds) // 空闲 CPU,使 CPU 处于 idle 状态,直至设定的时间片结束
				runtime.Gosched()
			}
		}()
	}
	select {}  // 阻塞 burnCpu 函数,保活 goroutines
}

知识共享许可协议
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。