再谈CPU的电源管理(如何做到稳定全核睿频?)

在之前的一篇Blog: 服务器的能耗控制以及高性能模式配置(Dell)中,说到在Dell的服务器BIOS中打开Performance模式。就可以实现真正非软件管理的高性能模式,让CPU时刻处在最高性能状态上。但是呢,最近的一批机器,升级到CentOS 7.7系统之后,这个行为发生了一些变化,而针对这批机器的两个供应商的表现呢,也不完全一致,这个现象驱使我研究了一下到底怎么样设置,可以实现期望的运行状态。也就是说,希望CPU稳定的运行在全核睿频上,既不需要他提升单核频率到单核睿频,也不希望他降频,这样,至少在我们当前的业务场景下,能获得比较稳定的性能预期。

首先呢,当我们升级到CentOS 7.7系统之后,执行cpupower frequency-set -g performance或者cpupower frequency-info的时候,不再像之前那样报错了,也就是说,系统能正确加载频率调整驱动,只是加载的驱动从原来的acpi-cpufreq变成了现在的intel_pstate,这是什么原因呢?

~]# cpupower frequency-info
analyzing CPU 0:
  driver: intel_pstate
  CPUs which run at the same hardware frequency: 0
  CPUs which need to have their frequency coordinated by software: 0
  maximum transition latency:  Cannot determine or is not supported.
  hardware limits: 1000 MHz - 4.00 GHz
  available cpufreq governors: performance powersave
  current policy: frequency should be within 1000 MHz and 4.00 GHz.
                  The governor "performance" may decide which speed to use
                  within this range.
  current CPU frequency: 3.20 GHz (asserted by call to hardware)
  boost state support:
    Supported: yes
    Active: yes

于是就去查询了CentOS或者说Redhat 7.7的Release Note,以及咨询了一下厂商,发现CentOS 7.7的kernel有个修改,The intel_pstate driver loads on the Intel Skylake-X systems with HWP disabled:也就是说在之前的版本里,如果关闭了HWP(Hardware-Controlled Performance States,BIOS里设置Performance模式会关闭HWP),则intel_pstate驱动就不会加载,而切换到7.7之后,这个问题被修复,也就是说在7.7里,内核会加载intel_pstate驱动,这也是为什么cpupower命令可以正常的原因。

具体到这个修改很简单,其实就是这个patch:[cpufreq: intel_pstate: Add Skylake servers support](cpufreq: intel_pstate: Add Skylake servers support),这个patch是在Linux 4.18版本被merge到主线的,也就说,如果用主线内核,前后版本跨4.18,也会遇到同样的问题。

再看看intel_pstate驱动,如其名,是用来控制CPU的P-State的,那什么是P-State?Intel有个Slide:Balancing Power and Performance in the Linux Kernel简单介绍了P-State和intel_pstate驱动的工作原理。这里我们就不深入了,实际上仅仅通过系统设置只能控制到一部分频率区间,真正的频率还是由硬件决定的,在没有HWP和有HWP支持的情况下,会有不同的寄存器组合,来给硬件提示当前的性能偏好,具体的需要参考Intel的Intel® 64 and IA-32 Architectures Software Developer’s ManualPOWER AND THERMAL MANAGEMENT部分了。有点复杂,而且我也没有完全弄明白,这里就不分析了。

所以呢,我们能做的事情很简单,就是执行cpupower frequency-set -g performance,告诉intel_pstate驱动当前需要使用最高性能模式就好了。

但是事情往往没有这么简单,按照上面的分析,设置高性能模式应该就可以获得稳定的CPU频率了,然而在一台测试机器上设置之后,使用turbostat命令依然观察到比较明显的降频:

~]# turbostat
Package Core    CPU     Avg_MHz Busy%   Bzy_MHz TSC_MHz IRQ     SMI     POLL    C1      C1E     C6      POLL%   C1%     C1E%    C6%     CPU%c1  CPU%c6  CoreTmp PkgTmp  PkgWatt RAMWatt PKG_%R
AM_%
-       -       -       2       0.08    2284    2395    5790    0       1       104     2991    6482    0.00    0.10    3.81    96.02   9.98    89.93   60      61      79.09   83.82   0.00 0
.00
0       0       0       7       0.33    2181    2395    139     0       0       2       26      253     0.00    0.00    0.52    99.17   5.03    94.64   53      56      40.44   41.06   0.00 0
.00
0       0       48      0       0.01    2033    2395    11      0       0       0       2       11      0.00    0.00    0.01    100.01  5.35
0       1       4       2       0.11    2107    2395    74      0       0       0       7       94      0.00    0.00    0.09    99.83   2.25    97.64   52
0       1       52      2       0.05    3213    2395    18      0       0       0       4       25      0.00    0.00    0.01    99.97   2.31
0       2       8       5       0.19    2844    2395    85      0       0       5       40      119     0.00    0.02    0.29    99.53   2.87    96.94   52
0       2       56      0       0.02    1494    2395    33      0       0       0       1       29      0.00    0.00    0.00    100.01  3.05
0       3       12      3       0.16    1989    2395    56      0       0       7       11      146     0.00    0.61    0.58    98.67   4.79    95.05   48
0       3       60      1       0.05    2429    2395    28      0       0       0       1       38      0.00    0.00    0.00    99.98   4.90
0       4       10      3       0.11    2856    2395    25      0       0       0       3       76      0.00    0.00    0.03    99.89   1.29    98.60   48
0       4       58      0       0.01    2520    2395    9       0       0       0       0       10      0.00    0.00    0.00    100.02  1.39
0       5       6       4       0.17    2404    2395    99      0       0       3       15      140     0.00    0.79    0.07    98.99   3.45    96.38   50
0       5       54      0       0.01    2149    2395    11      0       0       0       2       10      0.00    0.00    0.01    100.01  3.61
0       6       2       8       0.34    2274    2395    96      0       0       6       28      263     0.00    0.11    4.54    95.03   11.11   88.56   51

可以看到Bzy_MHz这一列的数据,高低不齐,并且显著低于预期的3.2GHz的全核睿频频率。再看一台正常的机器的结果:

Package Core    CPU     Avg_MHz Busy%   Bzy_MHz TSC_MHz IRQ     SMI     CPU%c1  CPU%c6  CoreTmp PkgTmp  Pkg%pc2 Pkg%pc6 PkgWatt RAMWatt PKG_%   RAM_%
-       -       -       93      2.90    3200    2494    52874   0       97.10   0.00    58      58      0.00    0.00    146.46  86.52   0.00    0.00
0       0       0       3193    100.00  3200    2494    5050    0       0.00    0.00    58      58      0.00    0.00    79.50   42.71   0.00    0.00
0       0       40      1       0.04    3200    2494    9       0       99.96
0       1       1       3193    100.00  3200    2494    5034    0       0.00    0.00    56
0       1       41      0       0.00    3201    2494    11      0       100.00
0       2       2       66      2.08    3200    2494    5267    0       97.92   0.00    45
0       2       42      2       0.06    3200    2494    26      0       99.94
0       3       3       8       0.24    3200    2494    459     0       99.76   0.00    47
0       3       43      84      2.63    3200    2494    663     0       97.37
0       4       4       30      0.94    3200    2494    582     0       99.06   0.00    45
0       4       44      17      0.53    3200    2494    319     0       99.47
0       8       5       16      0.51    3200    2494    731     0       99.49   0.00    45
0       8       45      2       0.06    3200    2494    95      0       99.94
0       9       6       7       0.21    3200    2494    273     0       99.79   0.00    48
0       9       46      3       0.09    3200    2494    62      0       99.91
0       10      7       7       0.23    3200    2494    376     0       99.77   0.00    47

仔细看一下两台机器的区别,发现出现降频的机器多出了很多状态:POLLC1C1EC6,而且对比CPU%c6这一列,一个大部分都在C6状态,而另外一个从不进入C6状态。那么很有可能就是因为这边的不同导致的频率的不同。

首先先说明一下C-States,Redhat有个KB:What are CPU “C-states” and how to disable them if needed?做了比较详细解释,或者也可以看CPU省电的秘密(二):CStates这篇,简单来说,C-States代表着CPU的工作状态,C0代表CPU处于正常运行状态,C1和以上代表CPU处于空闲状态,数字越大,CPU越省电,当然省电还是有副作用的,也就是会导致频率降低,并且从省电状态恢复到工作状态花费的时间越长。根据Intel的这个帖子里的说法:

  • state0 “POLL” (cpu spin-waits because it is expected to get more work very soon – < 10 microseconds)
  • state1 “C1-SKX” – default behavior of MWAIT with argument EAX=0x00 – has 2 microsecond wakeup latency
  • state2 “C1E-SKX” – MWAIT with argument EAX=0x01 – has 10 microsecond wakeup latency and drops core to maximum efficiency frequency
  • state3 “C6-SKX” – MWAIT with argument EAX=0x20 – has 133 microsecond wakeup latency and turns off core (allowing more power to other cores)

进入C1E就意味着CPU会降频到最高效率频率,并且恢复需要10us,而进入C6状态意味着核心关闭,并且从C6状态恢复需要133us。具体支持的状态信息可以在/sys/devices/system/cpu/cpu0/cpuidle/state*/中看到:

[/sys/devices/system/cpu/cpu0/cpuidle]# ls
ls *
state0:
desc  disable  latency  name  power  time  usage

state1:
desc  disable  latency  name  power  time  usage

state2:
desc  disable  latency  name  power  time  usage

state3:
desc  disable  latency  name  power  time  usage
[/sys/devices/system/cpu/cpu0/cpuidle]# cat state3/latency 
133
[root@yf-10-79-73-80.yfvm-1.node.kubernetes /sys/devices/system/cpu/cpu0/cpuidle]# cat state3/desc 
MWAIT 0x20
[root@yf-10-79-73-80.yfvm-1.node.kubernetes /sys/devices/system/cpu/cpu0/cpuidle]# cat state3/name 
C6-SKX

到这里,就会有两个问题了,第一:这些状态到底是怎么管理的?第二:如何关闭或者不进入这些状态呢?

从路径中可以看到,这些状态是由cpuidle驱动报告给操作系统的,具体使用的驱动可以在/sys/devices/system/cpu/cpuidle/current_driver这个文件中看到,出现降频的机器,用的驱动是intel_idle,而不降频的机器,这个驱动是none,也就是没加载。

为什么一个加载另一个却没加载呢?原因出在了mwait上,简单看了一下这个驱动加载的代码:

static int __init intel_idle_probe(void)
{
	unsigned int eax, ebx, ecx;
	const struct x86_cpu_id *id;

	if (max_cstate == 0) {
		pr_debug(PREFIX "disabled\n");
		return -EPERM;
	}

	id = x86_match_cpu(intel_idle_ids);
	if (!id) {
		if (boot_cpu_data.x86_vendor == X86_VENDOR_INTEL &&
		    boot_cpu_data.x86 == 6)
			pr_debug(PREFIX "does not run on family %d model %d\n",
				boot_cpu_data.x86, boot_cpu_data.x86_model);
		return -ENODEV;
	}

	if (boot_cpu_data.cpuid_level < CPUID_MWAIT_LEAF)
		return -ENODEV;

	cpuid(CPUID_MWAIT_LEAF, &eax, &ebx, &ecx, &mwait_substates);

	if (!(ecx & CPUID5_ECX_EXTENSIONS_SUPPORTED) ||
	    !(ecx & CPUID5_ECX_INTERRUPT_BREAK) ||
	    !mwait_substates)
			return -ENODEV;

	pr_debug(PREFIX "MWAIT substates: 0x%x\n", mwait_substates);

	icpu = (const struct idle_cpu *)id->driver_data;
	cpuidle_state_table = icpu->state_table;

	pr_debug(PREFIX "v" INTEL_IDLE_VERSION
		" model 0x%X\n", boot_cpu_data.x86_model);

	return 0;
}

代码里会通过cpuid查询CPUID_MWAIT_LEAF也就是Monitor/MWait的支持,如果不支持,就不会加载驱动了。而出现降频的机器上执行cpuid -1 -l5 -s5

~]# cpuid -1 -l5 -s5
Disclaimer: cpuid may not support decoding of all cpuid registers.
CPU:
   MONITOR/MWAIT (5):
      smallest monitor-line size (bytes)       = 0x40 (64)
      largest monitor-line size (bytes)        = 0x40 (64)
      enum of Monitor-MWAIT exts supported     = true
      supports intrs as break-event for MWAIT  = true
      number of C0 sub C-states using MWAIT    = 0x0 (0)
      number of C1 sub C-states using MWAIT    = 0x2 (2)
      number of C2 sub C-states using MWAIT    = 0x0 (0)
      number of C3 sub C-states using MWAIT    = 0x2 (2)
      number of C4 sub C-states using MWAIT    = 0x0 (0)
      number of C5 sub C-states using MWAIT    = 0x0 (0)
      number of C6 sub C-states using MWAIT    = 0x0 (0)
      number of C7 sub C-states using MWAIT    = 0x0 (0)

发现enum of Monitor-MWAIT exts supportedsupports intrs as break-event for MWAIT都是支持的,那应该是BIOS里没有关闭MONITOR/MWAIT相关选项,于是乎进入BIOS,关闭了MONITOR/MWAIT之后,发现cpuid依然显示支持!难道是厂商BIOS有问题?至少这个选项上是不符合预期的,而且其他厂家是可以正常关闭的。不过已经没办法去把所有的BIOS刷到最新了,只能等着后期再测试了。

接下来就是第二个问题了,如何关闭这些状态,或者阻止操作系统进入这些状态呢?在上面的文件系统里,可以看到每个状态都有关disable接口,简单的,如果不想进入到这个状态,就直接echo 1 > disable就可以了,针对这台机器,把state3、state2全部关闭,就可以实现稳定频率:

~]# turbostat
Package Core    CPU     Avg_MHz Busy%   Bzy_MHz TSC_MHz IRQ     SMI     POLL    C1      C1E     C6      POLL%   C1%     C1E%    C6%     CPU%c1  CPU%c6  CoreTmp PkgTmp  PkgWatt RAMWatt PKG_%R
AM_%
-       -       -       8       0.26    3200    2394    11035   0       31      17266   0       0       0.16    99.74   0.00    0.00    99.74   0.00    64      64      125.62  83.98   0.00 0
.00
0       0       0       8       0.27    3200    2395    247     0       2       376     0       0       0.00    99.75   0.00    0.00    99.73   0.00    57      58      61.53   40.83   0.00 0
.00
0       0       48      1       0.02    3200    2395    18      0       0       23      0       0       0.00    99.99   0.00    0.00    99.98
0       1       4       6       0.18    3200    2395    240     0       0       322     0       0       0.00    99.83   0.00    0.00    99.82   0.00    55
0       1       52      4       0.14    3200    2395    150     0       0       340     0       0       0.00    99.87   0.00    0.00    99.86
0       2       8       2       0.06    3200    2395    90      0       0       132     0       0       0.00    99.95   0.00    0.00    99.94   0.00    55

可以看到关闭这两个状态之后,频率直接被锁在了3.2Ghz上。效果非常好。

但是这个方法始终感觉有点tricky。查询了一些资料,发现内核对外提供了一个接口/dev/cpu_dma_latency,这个接口是Kernel PM Quality Of Service Interface的一部分,简单来说,可以通过这个接口,声明对系统延迟的容忍度,如果一个C-state的延迟超过了容忍度,则内核就不会进入到这个C-state。怎么设置呢,这是一个二进制接口,echo就不行了,必须写代码了,可以参考这个Gist

int main(int argc, char **argv)
{
	int32_t v;
	int fd;

// ...
	v = atoi(argv[1]);

	printf("setting latency to %d.%.6d seconds\n", v/1000000, v % 1000000);

	fd = open("/dev/cpu_dma_latency", O_WRONLY);
	if (fd < 0) {
		perror("open /dev/cpu_dma_latency");
		return 1;
	}
	if (write(fd, &v, sizeof(v)) != sizeof(v)) {
		perror("write to /dev/cpu_dma_latency");
		return 1;
	}

	while (1) sleep(10);

	return 0;
}

通过程序,写入个1就可以了,需要注意的是,必须一直保持这个fd打开,如果关闭了,那又会自动变成默认值了。为什么设置成1呢?因为这是最小的int值了,如果设置成0,意味着CPU必须一直是C0状态才能满足,设置为0,就和在内核启动参数上添加idle=poll是一样了。

还有更简单的办法,使用tuned,在我们用的CentOS 7里,自带了tuned,通过设置tuned,可以获得想要的性能结果:

~]# tuned-adm profile
Available profiles:
- balanced                    - General non-specialized tuned profile
- desktop                     - Optimize for the desktop use-case
- hpc-compute                 - Optimize for HPC compute workloads
- latency-performance         - Optimize for deterministic performance at the cost of increased power consumption
- network-latency             - Optimize for deterministic performance at the cost of increased power consumption, focused on low latency network performance
- network-throughput          - Optimize for streaming network throughput, generally only necessary on older CPUs or 40G+ networks
- powersave                   - Optimize for low power consumption
- throughput-performance      - Broadly applicable tuning that provides excellent performance across a variety of common server workloads
- virtual-guest               - Optimize for running inside a virtual guest
- virtual-host                - Optimize for running KVM guests
Current active profile: powersave
~]# tuned-adm profile latency-performance

使用tuned-adm profile latency-performance设置性能模式为latency-performance,也就做了上面最重要的两个事情cpupower frequency-set -g performance以及write 1 to /dev/cpu_dma_latency。当然,tuned默认的配置还会夹带一些其他的配置,可以根据情况删减,做成单独的custom配置使用。

说了半天,其实感觉也没有非常了解Intel以及Kernel针对电源管理上的处理,个人觉得当前的硬件和软件已经发展到一个很复杂的境地,要想全盘掌握还是比较困难,好在这段时间查询了这么多资料最终也算是比较圆满的解决问题了。还算一个比较好的结果吧。