再谈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 Manual的POWER 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
仔细看一下两台机器的区别,发现出现降频的机器多出了很多状态:POLL
、C1
、C1E
、C6
,而且对比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 supported
和supports 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针对电源管理上的处理,个人觉得当前的硬件和软件已经发展到一个很复杂的境地,要想全盘掌握还是比较困难,好在这段时间查询了这么多资料最终也算是比较圆满的解决问题了。还算一个比较好的结果吧。