原文链接

最近披露的CPU安全问题Meltdown and Spectre不仅是令人吃惊的安全问题,也是严重的性能问题。用于解决Meltdown漏洞的补丁引入了迄今为止最严重的内核性能下降。非常感谢那些努力开发解决方案来应对这些处理器问题的工程师们。

本文中,我们将着眼于Linux内核页表隔离(KPTI)补丁, 它解决了Meltdown问题,并关注这个补丁所引入的开销及调整开销的方法。在部署到生产环境之前,大部分的测试是基于之前的Linux 4.14.11和4.14.12。有些老内核有针对Meltdown的KAISER补丁,目前为止,性能开销看起来类似。这些测试结果并不是最终的,更多的修改仍在开发中,比如针对Spectre的补丁。

请注意,Meltdown/Spectre 存在四个潜在层面上的开销 (本文只关注第一个):

  • 客户机内核页表隔离补丁(本文)
  • Intel 处理器微码更新
  • 云供应商虚拟机程序更新(针对云上的客户机)
  • Retpoline 编译器更新

KPTI 影响的因素

为了理解KPTI的开销,至少有五个因素在起作用:

  • 系统调用的频率:开销与系统调用率是相关,虽然只有该值较高时才容易注意到。在每秒 (每CPU)50K次系统调用的情形下,开销大约为2%,并且随系统调用频率的升高而增大。以Netflix的场景 (作者的公司)为例,在云服务中高频度的系统调用是很少见的(除某些数据库服务外).

  • 上下文切换: 增加的开销与系统调用率类似,因此,作者在随后的评估中,上下文切换直接被加到系统调用中。

  • 缺页率:当缺页率较高时,略增加一点开销。

  • 工作集大小(热数据):由于TLB刷新,大于10MB的工作集将花费额外的开销。从而,导致性能开销从1%(仅有系统调用开销)升至7%。有两种方式可以缓解它,1) PCID,Linux 4.14可用,2) 大页。

  • 缓存访问模式:有些特定的访存模式将严重影响缓存命中状态, 使其从命中良好(caching well) 变得不那么好,从而导致开销被加剧。最坏情况下,额外增加10%的开销. 比如, 由之前的7%增长至17%。

为了调研这些因素,作者写了一个简单的基准测试程序,它可以改变系统调用的频率和工作集的大小。随后,作者分析了基准测试下的性能,并使用其他工具来确认结果。

1. 系统调用率

这是在系统调用路径上额外消耗的CPU时钟周期。下图为运行基准测试程序时,每CPU每秒的性能损失率与系统调用率的关系。

kpti 0m c59xl 41112

具有高系统调用率的应用包括: 代理服务,数据库,数据量小但有频繁的I/O调用的应用, 另外,还有对系统进行压测的基准测试程序,所有这些都会遭受到非常大的性能损失. 在Netflex, 许多服务在(每CPU)每秒内的系统调用都低于10k,因此,预计这种类型的开销是可以忽略的 (<0.5%).

如果你不知道你的系统用调用率,可使用如下命令进行测量:

sudo perf stat -e raw_syscalls:sys_enter -a -I 1000

该命令结果显示了整个系统的系统调用率. 用该值除以CPU个数(使用mpstat查看), 得到每CPU的平均值, 然后再除1000得到上图中的横坐标. 注意,perf stat 命令本身也会导致一些开销,尤其是在高系统调用率情形下(>100k/sec/CPU). 如需更低的开销, 你可以使用 ftrace/mcount 进行测试. 比如, 使用作者的perf-tools:

sudo ./perf-tools/bin/funccount -i 1 -d 10 '[sS]y[sS]_*'

然后,对系统调用那一列进行累加.

我们本可以取一次测量结果,基于一个模型、使用(外)差值法得到上图, 但通过更多的检查,可确保结果符合预期会更好一些. 上图的结果基本符合预期, 期望右下方的数据显示会有偏差并且会丢失数据点(这些缺点是由于对数坐标省略了小负数). 以误差边界为0.2%,彻底消除掉这种偏差是很容易的, 但它是性能剖析中会发生变化的一部分(系统调用率介于5k与10k之间). 下一节中, 我们将进一步探讨这一点.

基准测试程序中, 使用了一个循环来执行快速的系统调用, 及另一个用户级循环来模拟一个应用程序的工作集大小. 通过逐渐增加用户级循环的时间(初值为零), 系统调用率逐渐下降, 因为更多的CPU周期被用于“CPU约束型”的用户线程. 这给了我们一个范围,从>1M/sec的高系统调用率,至低系统调用率(并且用户时间占比>99%). 随后分别开启和关闭KPTI来测试系统调用(通过设置nopti,此外也使用旧的和新的内核进行复查).

我们以火焰图的方式,同时在两个系统上收集了CPU剖析的数据。但这对于一个改动而言是比较枯燥的,仅在系统调用的代码中有额外的消耗,与阅读KPTI的补丁时所估计的一样. 要进一步理解这种开销,我们需要使用指令级分析工具,比如perf annotate, 以及CPU的性能监视计数器/PMC(Performance Monitoring Counter)。(稍后再详细介绍)

2. 上下文切换与缺页率

这些数据由内核跟踪记录并且很容易通过/procsar(1)读取:

# sar -wB 1
Linux 4.14.12-virtual (bgregg-c5.9xl-i-xxx)       02/09/2018      _x86_64_     (36 CPU)

05:24:51 PM    proc/s   cswch/s
05:24:52 PM      0.00 146405.00

05:24:51 PM  pgpgin/s pgpgout/s   fault/s  majflt/s  pgfree/s pgscank/s pgscand/s pgsteal/s %vmeff
05:24:52 PM      0.00      0.00      2.00      0.00     50.00      0.00      0.00      0.00   0.00
[...]

与系统调用一样,其值越高,开销越大。 我们将在评估图中,将上下文切换率加到系统调用率上(平均到每CPU)。

3. 工作集大小(热数据)

现在,我们的测试程序模拟一个工作集大小(即:一个经常被访问的内存区域)。通过循环读取100MB数据,并且每次步进一个缓存行长度来构造这个工作集。结果发现,低系统调用率下的性能变的更糟。

kpti 100m c59xl 41112

注意到在10k和5k间性能开销上的那个跳变了吗?这个“跳跃”的特征依赖于实例类型,处理器,和工作负载,并且以不同大小出现在不同的点上。上图显示的是在一个c5.9xl(AWS实例类型)上的一个1.8%的跳变。但在m4.2xl(AWS实例类型)上,跳变出现在大约50k并且幅度更大: 10%。在下一节中,我们将对此进行分析。在这里,我们只想看看整体趋势,即:对特定的工作集(热数据)有更糟糕的性能。

你测试的性能开销是否看起来像以上这幅图,或前面那幅图,还是结果更差?这取决于你的工作集大小,上图是针对100MB,之前的那幅图中工作集大小为0。 请看作者的另一篇文章“工作集大小估算”和完整的评估。 作者的猜测是,100MB是一个用来测试系统调用的足够大的访存数据集。

Linux 4.14 引入了PCID支持,如果处理器也有PCID(看起来在EC2中很常见),就可改进性能。Gil Tene写过一篇文章讲述为什么PCID现在是x86的关键性能和安全特性.

还可以使用大页来进一步改善性能。无论是设置简单,但在老版本中在内存紧缩方面存在问题的透明大页;还是在理论上应该表现更好的静态大页。下图总结了这些选项:

multi pcid THP 100m c59xl 41112 a0

对于该测试,大页对性能的改进很大(尽管有KPTI),它反而将性能上的损失变为增益. 对数坐标轴虽然已经省略了负值,但这里,这些数据是在一个缩放过的、线性坐标轴上:

multi pcid THP 100m c59xl 41112 zoom

比方说,你的服务器上正在做每秒(每CPU)5k的系统调用,并且你推测你有一个大的工作集,类似于这个100MB测试。在最近的LTS Linux (4.4或4.9)上开启或打上KPTI(或KAISER)补丁,性能开销约为2.1%。Linux 4.14支持PCID,因此开销将变为大约0.5%。 使用大页后,原有的开销没有了并且性能提升了3.0%。

通过查看TLB相关的PMC,基本可以解释这种情况。这里作者使用的是tlbstat, 它是作者代码库pmc-cloud-tools中的一个小工具。以下是查看上图中一个点的结果(在另一个系统上,且拥有完整的PMC),其中,最糟糕的情况是从没有KPTI到无PCID支持的KPTI,开销是8.7%。

nopti:
# tlbstat -C0 1
K_CYCLES   K_INSTR      IPC DTLB_WALKS ITLB_WALKS K_DTLBCYC  K_ITLBCYC  DTLB% ITLB%
2835385    2525393     0.89 291125     4645       6458       187         0.23  0.01
2870816    2549020     0.89 269219     4922       5221       194         0.18  0.01
2835761    2524070     0.89 255815     4586       4993       157         0.18  0.01
[...]

pti, nopcid:
# tlbstat -C0 1
K_CYCLES   K_INSTR      IPC DTLB_WALKS ITLB_WALKS K_DTLBCYC  K_ITLBCYC  DTLB% ITLB%
2873801    2328845     0.81 6546554    4474231    83593      63481       2.91  2.21
2863330    2326694     0.81 6506978    4482513    83209      63480       2.91  2.22
2864374    2329642     0.81 6500716    4496114    83094      63577       2.90  2.22
[...]

pti, pcid:
# tlbstat -C0 1
K_CYCLES   K_INSTR      IPC DTLB_WALKS ITLB_WALKS K_DTLBCYC  K_ITLBCYC  DTLB% ITLB%
2862069    2488661     0.87 359117     432040     6241       9185        0.22  0.32
2855214    2468445     0.86 313171     428546     5820       9092        0.20  0.32
2869416    2488607     0.87 334598     434110     6011       9208        0.21  0.32
[...]

pti, pcid + thp:
# tlbstat -C0 1
K_CYCLES   K_INSTR      IPC DTLB_WALKS ITLB_WALKS K_DTLBCYC  K_ITLBCYC  DTLB% ITLB%
2863464    2594463     0.91 2601       298946     57         6215        0.00  0.22
2845726    2568730     0.90 3330       295951     42         6156        0.00  0.22
2872419    2597000     0.90 2746       298328     64         6211        0.00  0.22
[...]

最后两列显示了至少一个TLB未命中时,一次数据或指令TLB遍历使用的周期数. 前两个输出显示了与性能损失8.7%相关的TLB细节,额外的TLB遍历周期数导致总周期数增加了4.88%。后两个输出显示了PCID的引入,及THP的加入。PCID减少了两种类型的TLB遍历,使得遍历的数据类似于之前无KPTI的级别。最后一个输出显示了THP带来的差异,即:数据TLB遍历次数为0. 但指令TLB的遍历依然存在,通过查看/proc/PID/smaps,代码段没有使用大页内存。作者相信,通过更多的调优,还可以进一步优化性能。

为了展示它到底有多糟糕,取之前图中的第一个点,以下数据显示出从没有KPTI到无PCID支持的KPTI,开销超过了800%。

nopti:
# tlbstat -C0 1
K_CYCLES   K_INSTR      IPC DTLB_WALKS ITLB_WALKS K_DTLBCYC  K_ITLBCYC  DTLB% ITLB%
2854768    2455917     0.86 565        2777       50         40          0.00  0.00
2884618    2478929     0.86 950        2756       6          38          0.00  0.00
2847354    2455187     0.86 396        297403     46         40          0.00  0.00
[...]

pti, nopcid:
# tlbstat -C0 1
K_CYCLES   K_INSTR      IPC DTLB_WALKS ITLB_WALKS K_DTLBCYC  K_ITLBCYC  DTLB% ITLB%
2875793    276051      0.10 89709496   65862302   787913     650834     27.40 22.63
2860557    273767      0.10 88829158   65213248   780301     644292     27.28 22.52
2885138    276533      0.10 89683045   65813992   787391     650494     27.29 22.55
2532843    243104      0.10 79055465   58023221   693910     573168     27.40 22.63
[...]

一半的CPU周期花费在了遍历页表上。从来没有见过TLB表现这么差。仅IPC指标便可告诉我们,不好的事情正在发生。如IPC由0.86降至0.10,这就与性能损失相关。我仍旧建议将IPC与任何%CPU测量包含在一起,以便我们真正了解那些周期是什么. 正如作者在之前一篇文章中所讨论的,详见CPU Utilization is Wrong.

4. 缓存访问模式

根据内存访问模式和工作集大小的不同,在某些系统调用率下,系统会有额外的1%至10%的开销。 这就是从前面图表中所看到的“跳跃”。 基于PMC(Performance Monitoring Counter)分析和KPTI的变更描述,一个可疑因素是KPTI系统上附加的对页表内存需求,导致工作负载更快地从CPU缓存中退出。 以下是相关测试结果:

kpti pmcs1

这表明性能损失的那个“跳跃”对应于LLC(前两个图)类似的访问下降:这实际上并没有意义,因为较低的工作负载吞吐量预示着较低的访问速率。 真正值得关注的是,LLC命中率突然下降,从55%降至50%,这在没有KPTI补丁(最后一个nopti图表显示小的LLC命中率提高)的情况下并不会发生。 这听起来像额外的KPTI页表迫使这个工作集的数据从CPU缓存中被驱逐走,导致性能的(意外的)突然下降。

55%到50%的LLC命中率的下降不能够完全解释10%的性能损失。 另一个因素是需要通过PMC进行分析,但是,这个测试是基于m4.16xl(AWS)实例类型进行的,其支持的PMCs有限。去年年底,EC2为PMC分析增加了两个选项:提供所有PMC支持的Nitro虚拟机和裸机实例类型(目前为public preview版)。之前的TLB分析是基于c5.9xl实例进行的,一个Nitro虚拟机系统。 不幸的是,该系统上的“跳跃”只有1%左右,这么小的变化难以发现问题。

总之,KPTI会增加额外的内存开销,这会导致一部分工作负载的数据数据被逐出CPU缓存。

验证测试

测试以上这些图表(数据):用一个MySQL OLTP基准测试,运行75k系统调用/秒/CPU(8个CPU共计600k系统调用/秒),预计将有一个大型工作集(因此更像我们之前的100MB工作集测试)。根据图标,估计KPTI(无大页或PCID)的性能损失约为4%。 实测的性能损失为5%。 在一64个CPU的系统上,同样的负载和(每CPU)系统调用速率,实测的性能损失为3%。 其他情况下的测试结果也都同样相似。

与前面的图表匹配最差的测试是一个压力测试应用程序,该应用会有210k系统调用/秒/CPU + 27k上下文切换/秒/CPU和一个小型工作集(约25MB)。 预计的性能损失应该低于12%,但是却高达25%。 为了检查是否还有其他额外的开销来源,我分析了这个测试,发现25%是由TLB未命中时的页面遍历引起的,这与之前研究过的开销相同。这种较大的差异,应该是因为与我们的测试程序相比,该应用的工作负载对TLB刷新更加敏感。

修正性能

  1. 利用PCID

正如之前提到的,及在图表中显示出的差异。Linux 4.14上的PCID支持确实可行,当然,CPU也同样需要支持PCID(可通过/proc/cpuinfo来查看)。在旧的版本上,比如4.4.115也可能可行。

  1. 使用大页

相关原因及测试结果前面已经提过。由于篇幅限制,这里不便囊括如何配置和使用大页内存,及相关的注意事项。

  1. 降低系统调用

如果由于系统调用率较高而导致性能损失让你很痛苦,那么显然应该分析一下是有哪些系统调用导致的,并寻找方法消除一些系统调用。在几年前,这是进行系统性能分析常干的事,但近些年的系统性能提升已经将焦点关注在用户态(用户态性能更优)。

很多方法可以分析系统调用。这里有一些(按照开销从大到小排序):

  1. strace
  2. perf record
  3. perf trace
  4. sysdig
  5. perf stat
  6. bcc/eBPF
  7. ftrace/mcount

最快的是ftrace/mcount,之前已经有过例子,统计10秒内系统调用的次数:

# ./perf-tools/bin/funccount -d 10 '[sS]y[sS]_*'
Tracing "[sS]y[sS]_*" for 10 seconds...

FUNC                              COUNT
SyS_epoll_wait                        1
SyS_exit_group                        1
SyS_fcntl                             1
SyS_ftruncate                         1
[...]
SyS_newstat                          56
SyS_mremap                           62
SyS_rt_sigaction                     73
SyS_select                         1895
SyS_read                           1909
SyS_clock_gettime                  3791
SyS_rt_sigprocmask                 3856

Ending tracing...

还有一个功能相同的工具,bcc/eBPF版本的funccount与syscall tracepoints,只在Linux 4.14可用,感谢Yonghong Song对此的支持:

# /usr/share/bcc/tools/funccount -d 10 't:syscalls:sys_enter*'
Tracing 310 functions for "t:syscalls:sys_enter*"... Hit Ctrl-C to end.

FUNC                                    COUNT
syscalls:sys_enter_nanosleep                1
syscalls:sys_enter_newfstat                 3
syscalls:sys_enter_mmap                     3
syscalls:sys_enter_inotify_add_watch        9
syscalls:sys_enter_poll                    11
syscalls:sys_enter_write                   61
syscalls:sys_enter_perf_event_open        111
syscalls:sys_enter_close                  152
syscalls:sys_enter_open                   157
syscalls:sys_enter_bpf                    310
syscalls:sys_enter_ioctl                  395
syscalls:sys_enter_select                2287
syscalls:sys_enter_read                  2445
syscalls:sys_enter_clock_gettime         4572
syscalls:sys_enter_rt_sigprocmask        4572
Detaching...

现在你应该已经知道了那些系统调用,想办法减少它们吧。你可以使用其他工具来检查那些系统调用的参数和堆栈(例如:使用perf record,kprobe或者bcc中的trace等),挖掘可以优化的地方,并着手进行性能优化。

结论和扩展阅读

KPTI补丁可以降低Meltdown的风险,但同时引入了大量开销,从1%到超过800%不等。到底你处于图表中的哪 个位置(性能损失情况如何)取决于两个重要因素:系统调用率及缺页率和程序工作时热点内存的大小,前者由额外 的CPU运行周期引入的,后者则是由于在系统调用或上下文切换时的TLB刷新导致的。本文描述了这些内容,并通过 基准测试分析了它们。当然,没有什么比真实情况更有说服力,然而本文中所包含的分析,对于估算性能退化而言 ,在解释导致性能退化的原因方面,及如何调优和修正方面更加有用。

这只是Meltdown/Spectre的四个潜在开销中的一个:还有虚拟机的更改,英特尔微代码和编译更改。 这些KPTI数字也不是最终的,因为Linux仍在开发和改进之中。

相关阅读和参考资料:

Reference