第16章 案例研究
本章是一个系统性能案例研究:讲述了一个真实环境中的性能问题,从最初报告到最终解决的全过程。这个特定的问题发生在一个生产云计算环境中;我选择它作为系统性能分析的一个常规示例。
我在本章的意图不是引入新的技术内容,而是通过讲故事的方式,展示在实际工作环境中如何应用工具和方法论。这对于尚未处理过真实系统性能问题的新手尤其有用,它提供了一个“站在专家身后”观察的视角,展示了专家是如何处理这些问题的,并对专家在分析过程中可能的想法及原因进行了评论。这不一定是在记录可能存在的最佳方法,而是说明为什么选择了某一种方法。
16.1 无法解释的性能提升
Netflix 的一个微服务在一个新的基于容器的平台上进行了测试,结果发现请求延迟降低了三到四倍。虽然容器平台有很多好处,但如此巨大的性能提升是出乎意料的!这听起来好得令人难以置信,我被要求调查并解释这是如何发生的。
为了进行分析,我使用了多种工具,包括基于计数器、静态配置、PMC(性能监控计数器)、软件事件和跟踪的工具。所有这些工具类型都发挥了作用,并提供了能够拼凑在一起的线索。由于这构成了系统性能分析的广泛巡礼,我将其作为我在 USENIX LISA 2019 上关于系统性能演讲的开场故事 [Gregg 19h],并将其收录在此作为案例研究。
16.1.1 问题描述
通过与服务团队交流,我了解了该微服务的详细信息:它是一个用于计算客户推荐的 Java 应用程序,当前运行在 AWS EC2 云中的虚拟机实例上。该微服务由两个组件组成,其中一个正在 Netflix 名为 Titus 的新容器平台上进行测试,该平台也运行在 AWS EC2 上。该组件在 VM 实例上的请求延迟为三到四秒,而在容器上变为一秒:快了三到四倍!
原书页码标识
784 第16章 案例研究
问题是要解释这种性能差异。如果这仅仅是由于迁移到容器造成的,那么微服务可以通过迁移来获得永久的 3-4 倍性能提升。如果是由于其他因素造成的,那么值得了解这个因素是什么,以及它是否是永久的。也许它还可以应用于其他地方,并在更大程度上发挥作用。
我立刻想到的是将工作负载的一个组件隔离运行的好处:它将能够使用整个 CPU 缓存,而不会受到另一个组件的争用,从而提高缓存命中率,进而提升性能。另一个猜测是容器平台上的突发性能,即容器可以使用其他容器的空闲 CPU 资源。
16.1.2 分析策略
由于流量由负载均衡器(AWS ELB)处理,因此可以在 VM 和容器之间分配流量,这样我就可以同时登录到两者。这是进行比较分析的理想情况:我可以在一天中的同一时间(相同的流量组合和负载)在两者上运行相同的分析命令,并立即比较输出。
在这种情况下,我可以访问容器宿主机,而不仅仅是容器,这允许我使用任何分析工具,并为这些工具提供进行任何系统调用的权限。如果我只拥有容器访问权限,由于有限的可观测性来源和内核权限,分析将更加耗时,需要从有限的指标中进行更多的推断,而不是直接测量。目前有些性能问题仅从容器内部分析是不切实际的(参见第11章,云计算)。
至于方法论,我计划从 60 秒检查清单(第1章,简介,第 1.10.1 节,Linux 60秒性能分析)和 USE 方法(第2章,方法论,第 2.5.9 节,USE方法)开始,并根据它们提供的线索执行下钻分析(第 2.5.12 节,下钻分析)和其他方法论。
我在以下各节中包含了我运行的命令及其输出,使用 “serverA#” 提示符表示 VM 实例,使用 “serverB#” 提示符表示容器宿主机。
16.1.3 统计信息
我首先运行 uptime(1) 来检查负载平均值的统计信息。在两个系统上:
serverA# uptime
22:07:23 up 15 days, 5:01, 1 user, load average: 85.09, 89.25, 91.26serverB# uptime
22:06:24 up 91 days, 23:52, 1 user, load average: 17.94, 16.92, 16.62这表明负载大致稳定,VM 实例上的负载稍微变轻了一些(85.09 相比 91.26),而容器上的负载稍微变重了一些(17.94 相比 16.62)。我检查了趋势以查看问题是正在增加、减少还是稳定:这在可以自动将负载从不健康实例迁移走的云环境中尤为重要。我不止一次登录到一个有问题的实例,却发现几乎没有活动,一分钟负载平均值接近于零。
原书页码标识
16.1 无法解释的性能提升 785
负载平均值还显示 VM 的负载远高于容器宿主机(85.09 对比 17.94),尽管我需要其他工具的统计信息来理解这意味着什么。高负载平均值通常指向 CPU 需求,但也可能与 I/O 相关(参见第6章,CPU,第 6.6.1 节,uptime)。
为了探究 CPU 负载,我转向了 mpstat(1),从系统范围的平均值开始。在虚拟机上:
serverA# mpstat 10
Linux 4.4.0-130-generic (...) 07/18/2019 _x86_64_ (48 CPU)
10:07:55 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
10:08:05 PM all 89.72 0.00 7.84 0.00 0.00 0.04 0.00 0.00 0.00 2.40
10:08:15 PM all 88.60 0.00 9.18 0.00 0.00 0.05 0.00 0.00 0.00 2.17
10:08:25 PM all 89.71 0.00 9.01 0.00 0.00 0.05 0.00 0.00 0.00 1.23
10:08:35 PM all 89.55 0.00 8.11 0.00 0.00 0.06 0.00 0.00 0.00 2.28
10:08:45 PM all 89.87 0.00 8.21 0.00 0.00 0.05 0.00 0.00 0.00 1.86
^C
Average: all 89.49 0.00 8.47 0.00 0.00 0.05 0.00 0.00 0.00 1.99在容器上:
serverB# mpstat 10
Linux 4.19.26 (...) 07/18/2019 _x86_64_ (64 CPU)
09:56:11 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
09:56:21 PM all 23.21 0.01 0.32 0.00 0.00 0.10 0.00 0.00 0.00 76.37
09:56:31 PM all 20.21 0.00 0.38 0.00 0.00 0.08 0.00 0.00 0.00 79.33
09:56:41 PM all 21.58 0.00 0.39 0.00 0.00 0.10 0.00 0.00 0.00 77.92
09:56:51 PM all 21.57 0.01 0.39 0.02 0.00 0.09 0.00 0.00 0.00 77.93
09:57:01 PM all 20.93 0.00 0.35 0.00 0.00 0.09 0.00 0.00 0.00 78.63
^C
Average: all 21.50 0.00 0.36 0.00 0.00 0.09 0.00 0.00 0.00 78.04mpstat(1) 将 CPU 数量打印在第一行。输出显示虚拟机有 48 个 CPU,而容器宿主机有 64 个。这帮助我进一步解释了负载平均值:如果它们是基于 CPU 的,这将表明 VM 实例已经严重进入 CPU 饱和状态,因为负载平均值大约是 CPU 数量的两倍,而容器宿主机则未充分利用。mpstat(1) 指标支持了这一假设:VM 上的空闲时间约为 2%,而容器宿主机上约为 78%。
原书页码标识
786 第16章 案例研究
通过检查其他 mpstat(1) 统计信息,我发现了其他线索:
- CPU 利用率(%usr + %sys + …)显示 VM 为 98%,而容器为 22%。这些处理器每个 CPU 核心有两个超线程,因此跨越 50% 利用率标记通常意味着超线程核心争用,从而降低性能。VM 已深深陷入这一区域,而容器宿主机可能仍然受益于每个核心只有一个繁忙的超线程。
- VM 上的系统时间(%sys)要高得多:大约 8% 相比于 0.38%。如果 VM 在 CPU 饱和状态下运行,这部分额外的 %sys 时间可能包括内核上下文切换代码路径。内核跟踪或性能分析可以确认这一点。
我继续执行 60 秒检查清单上的其他命令。vmstat(8) 显示的运行队列长度与负载平均值相似,证实了负载平均值是基于 CPU 的。iostat(1) 显示磁盘 I/O 很少,sar(1) 显示网络 I/O 很少。(这些输出未包含在此处。)这证实了 VM 在 CPU 饱和状态下运行,导致可运行线程等待轮到它们执行,而容器宿主机则不是这样。容器宿主机上的 top(1) 也显示只有一个容器在运行。
这些命令提供了 USE 方法的统计信息,同样也识别出了 CPU 负载问题。
我解决这个问题了吗?
我发现 VM 在 48 CPU 的系统上负载平均值为 85,并且该负载平均值是基于 CPU 的。这意味着线程大约有 77% 的时间在等待轮到自己执行 (85/48 – 1),消除这段等待时间将产生大约 4 倍的加速 (1 / (1 – 0.77))。虽然这个量级与问题相符,但我还无法解释为什么负载平均值更高:还需要更多的分析。
16.1.4 配置
知道存在 CPU 问题后,我检查了 CPU 及其限制的配置(静态性能调优:第 2.5.17 节和第 6.5.7 节)。VM 和容器之间的处理器本身是不同的。以下是虚拟机的 /proc/cpuinfo:
serverA# cat /proc/cpuinfo
processor : 47
vendor_id : GenuineIntel
cpu family : 6
model : 85
model name : Intel(R) Xeon(R) Platinum 8175M CPU @ 2.50GHz
stepping : 4
microcode : 0x200005e
cpu MHz : 2499.998
cache size : 33792 KB
physical id : 0
siblings : 48
core id : 23
cpu cores : 24
apicid : 47原书页码标识
16.1 无法解释的性能提升 787
initial apicid : 47
fpu : yes
fpu_exception : yes
cpuid level : 13
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat
pse36 clflush mmx fxsr sse sse2 ss ht syscall nx pdpe1gb rdtscp lm constant_tsc
arch_perfmon rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq
monitor ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes
xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_single kaiser
fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm mpx avx512f rdseed adx
smap clflushopt clwb avx512cd xsaveopt xsavec xgetbv1 ida arat
bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass
bogomips : 4999.99
clflush size : 64
cache_alignment : 64
address sizes : 46 bits physical, 48 bits virtual
power management:以及容器的:
serverB# cat /proc/cpuinfo
processor : 63
vendor_id : GenuineIntel
cpu family : 6
model : 79
model name : Intel(R) Xeon(R) CPU E5-2686 v4 @ 2.30GHz
stepping : 1
microcode : 0xb000033
cpu MHz : 1200.601
cache size : 46080 KB
physical id : 1
siblings : 32
core id : 15
cpu cores : 16
apicid : 95
initial apicid : 95
fpu : yes
fpu_exception : yes
cpuid level : 13
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat
pse36 clflush mmx fxsr sse sse2 ht syscall nx pdpe1gb rdtscp lm constant_tsc arch_
perfmon rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq monitor
est ssse3 fma cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes 原书页码标识
788
16.1 案例研究
xsave avx f16c rdrand hypervisor lahf_lm abm 3dnowprefetch cpuid_fault invpcid_single pti fsgsbase bmi1 hle avx2 smep bmi2 erms invpcid rtm rdseed adx xsaveopt ida bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf bogomips : 4662.22 clflush size : 64 cache_alignment : 64 address sizes : 46 bits physical, 48 bits virtual power management:
容器主机的 CPU 具有略低的基础频率(2.30 GHz 对比 2.50 GHz);然而,它们拥有大得多的末级缓存(45 MB 对比 33 MB)。根据工作负载的不同,更大的缓存容量可以对 CPU 性能产生显著影响。为了进一步调查,我需要使用 性能监控计数器。
16.1.5 PMCs
性能监控计数器(Performance monitoring counters,PMCs)可以解释 CPU 周期性能,并且在 AWS EC2 的某些实例上可用。我已经发布了一个用于云端 PMC 分析的工具包 [Gregg 20e],其中包含 pmcarch(8)(第 6.6.11 节,pmcarch)。pmcarch(8) 显示了 Intel 的“架构集”PMCs,这是最基础且通常可用的计数器集合。
在虚拟机上:
serverA# ./pmcarch -p 4093 10
K_CYCLES K_INSTR IPC BR_RETIRED BR_MISPRED BMR% LLCREF LLCMISS LLC%
982412660 575706336 0.59 126424862460 2416880487 1.91 15724006692 10872315070 30.86
999621309 555043627 0.56 120449284756 2317302514 1.92 15378257714 11121882510 27.68
991146940 558145849 0.56 126350181501 2530383860 2.00 15965082710 11464682655 28.19
996314688 562276830 0.56 122215605985 2348638980 1.92 15558286345 10835594199 30.35
979890037 560268707 0.57 125609807909 2386085660 1.90 15828820588 11038597030 30.26
[...]
在容器实例上:
serverB# ./pmcarch -p 1928219 10
K_CYCLES K_INSTR IPC BR_RETIRED BR_MISPRED BMR% LLCREF LLCMISS LLC%
147523816 222396364 1.51 46053921119 641813770 1.39 8880477235 968809014 89.09
156634810 229801807 1.47 48236123575 653064504 1.35 9186609260 1183858023 87.11
152783226 237001219 1.55 49344315621 692819230 1.40 9314992450 879494418 90.56
140787179 213570329 1.52 44518363978 631588112 1.42 8675999448 712318917 91.79
136822760 219706637 1.61 45129020910 651436401 1.44 8689831639 617678747 92.89
[...]
结果显示,虚拟机的每周期指令数(IPC)约为 0.57,而容器约为 1.52:存在 2.6 倍的差异。
页码标记:789
IPC 较低的一个原因可能是超线程争用,因为虚拟机主机的 CPU 利用率已超过 50%。最后一列显示了另一个原因:虚拟机的末级缓存(Last-Level Cache)命中率仅为 30%,而容器约为 90%。这将导致虚拟机上的指令频繁在主存访问上停顿,从而拉低 IPC 和指令吞吐量(性能)。
虚拟机上较低的 LLC 命中率可能由至少三个因素导致:
- 更小的 LLC 容量(33 MB 对比 45 MB)。
- 运行完整的工作负载而不是子组件(如问题描述中所述);子组件可能会有更好的缓存表现:更少的指令和数据。
- CPU 饱和导致更多的上下文切换,并在代码路径(包括用户态和内核态)之间跳转,增加了缓存压力。
最后一个因素可以使用跟踪工具来进一步调查。
16.1.6 软件事件
为了调查上下文切换,我首先使用 perf(1) 命令来统计系统范围的上下文切换率。这使用了一个软件事件,它类似于硬件事件(PMC),但是是在软件中实现的(见第 4 章,可观测性工具,图 4.5,以及第 13 章,perf,第 13.5 节,软件事件)。
在虚拟机上:
serverA# perf stat -e cs -a -I 1000
# time counts unit events
1.000411740 2,063,105 cs
2.000977435 2,065,354 cs
3.001537756 1,527,297 cs
4.002028407 515,509 cs
5.002538455 2,447,126 cs
6.003114251 2,021,182 cs
7.003665091 2,329,157 cs
8.004093520 1,740,898 cs
9.004533912 1,235,641 cs
10.005106500 2,340,443 cs
^C 10.513632795 1,496,555 cs
此输出显示每秒大约有 200 万次上下文切换。然后我在容器主机上运行了它,这次匹配了容器应用的 PID 以排除其他可能的容器(我在虚拟机上做了类似的 PID 匹配,并没有显著改变之前的结果^1):
脚注 1
1 那么为什么我不把虚拟机的 PID 匹配输出包含进来呢?因为我没有了。
页码标记:790
在容器主机上:
serverB# perf stat -e cs -p 1928219 -I 1000
# time counts unit events
1.001931945 1,172 cs
2.002664012 1,370 cs
3.003441563 1,034 cs
4.004140394 1,207 cs
5.004947675 1,053 cs
6.005605844 955 cs
7.006311221 619 cs
8.007082057 1,050 cs
9.007716475 1,215 cs
10.008415042 1,373 cs
^C 10.584617028 894 cs
此输出显示每秒只有大约 1000 次上下文切换。
高上下文切换率的影响
高频率的上下文切换会给 CPU 缓存带来更大压力,因为缓存需要在不同的代码路径之间切换,包括用于管理上下文切换的内核代码,以及可能的不同进程的代码^2。为了进一步调查上下文切换,我使用了跟踪工具。
16.1.7 跟踪
有几种基于 BPF 的跟踪工具可以进一步分析 CPU 使用和上下文切换,包括来自 BCC 的:cpudist(8)、cpuwalk(8)、runqlen(8)、runqlat(8)、runqslower(8)、cpuunclaimed(8) 等等(见图 15.1)。
cpudist(8) 显示线程的在 CPU 上(on-CPU)的持续时间。在虚拟机上:
serverA# cpudist -p 4093 10 1
Tracing on-CPU time... Hit Ctrl-C to end.
usecs : count distribution
0 -> 1 : 3618650 |****************************************|
2 -> 3 : 2704935 |***************************** |
4 -> 7 : 421179 |**** |
8 -> 15 : 99416 |* |
16 -> 31 : 16951 | |
32 -> 63 : 6355 | |
64 -> 127 : 3586 | |
128 -> 255 : 3400 | |
256 -> 511 : 4004 | |
512 -> 1023 : 4445 | |
脚注 2
2 对于某些处理器和内核配置,上下文切换可能还会刷新 L1 缓存。
页码标记:791
1024 -> 2047 : 8173 | |
2048 -> 4095 : 9165 | |
4096 -> 8191 : 7194 | |
8192 -> 16383 : 11954 | |
16384 -> 32767 : 1426 | |
32768 -> 65535 : 967 | |
65536 -> 131071 : 338 | |
131072 -> 262143 : 93 | |
262144 -> 524287 : 28 | |
524288 -> 1048575 : 4 | |
此输出显示应用程序在 CPU 上花费的时间通常非常短,通常不到 7 微秒。其他工具(t:sched:sched_switch 的 stackcount(8),以及 /proc/PID/status)显示,应用程序通常是由于非自愿^3上下文切换而离开 CPU 的。
脚注 3
3
/proc/PID/status称它们为nonvoluntary_ctxt_switches。
在容器主机上:
serverB# cpudist -p 1928219 10 1
Tracing on-CPU time... Hit Ctrl-C to end.
usecs : count distribution
0 -> 1 : 0 | |
2 -> 3 : 16 | |
4 -> 7 : 6 | |
8 -> 15 : 7 | |
16 -> 31 : 8 | |
32 -> 63 : 10 | |
64 -> 127 : 18 | |
128 -> 255 : 40 | |
256 -> 511 : 44 | |
512 -> 1023 : 156 |* |
1024 -> 2047 : 238 |** |
2048 -> 4095 : 4511 |****************************************|
4096 -> 8191 : 277 |** |
8192 -> 16383 : 286 |** |
16384 -> 32767 : 77 | |
32768 -> 65535 : 63 | |
65536 -> 131071 : 44 | |
131072 -> 262143 : 9 | |
262144 -> 524287 : 14 | |
524288 -> 1048575 : 5 | |
页码标记:792
现在,应用程序在 CPU 上花费的时间通常在 2 到 4 毫秒之间。其他工具显示它很少被非自愿上下文切换中断。
性能问题根源定位
虚拟机上的非自愿上下文切换,以及随后观察到的高上下文切换率,导致了性能问题。导致应用程序在通常不到 10 微秒后就离开 CPU,也让 CPU 缓存没有足够的时间为当前代码路径进行预热。
16.1 案例研究
16.1.8 结论
我得出的结论是,性能提升的原因如下:
-
无容器邻居:容器主机除了这一个容器外处于空闲状态。这使得该容器可以独占整个 CPU 缓存,并且在没有 CPU 争用的情况下运行。虽然这在测试期间产生了有利于容器的结果,但这并不是长期生产使用的预期情况,因为在生产环境中相邻容器将是常态。当其他租户搬入时,该微服务可能会发现 3-4 倍的性能优势消失殆尽。
-
LLC 大小与工作负载差异:VM 上的 IPC 降低了 2.6 倍,这可以解释 2.6 倍的降速。原因之一可能是超线程争用,因为 VM 主机的运行利用率超过了 50%(并且每个核心有两个超线程)。然而,主要原因可能是较低的 LLC 命中率:VM 上为 30%,而容器上为 90%。这种低 LLC 命中率有三个可能的原因:
- VM 上的 LLC 容量较小:33 MB 对比 45 MB。
- VM 上的工作负载更复杂:完整的应用程序需要更多的指令文本和数据,而容器上只运行了该组件。
- VM 上的上下文切换速率很高:每秒约 200 万次。这阻止了线程长时间在 CPU 上运行,干扰了缓存预热。在 VM 上,CPU 上的运行持续时间通常不到 10 μs,而在容器主机上为 2-4 ms。
-
CPU 负载差异:指向 VM 的负载更高,驱使 CPU 达到饱和:在一个 48 CPU 的系统上,基于 CPU 的负载平均值为 85。这导致了每秒约 200 万次的上下文切换速率,以及线程等待轮到自己的运行队列延迟。由负载平均值推算出的运行队列延迟表明,VM 的运行速度大约慢了 4 倍。
这些问题解释了观察到的性能差异。
案例总结回顾
本案例展示了如何综合运用统计、配置、PMC(性能监控计数器)和跟踪技术,解决了看似无法解释的性能提升问题。真实环境中的性能差异往往由多重因素(如缓存命中率、上下文切换、CPU 争用等)共同导致,需要系统性地逐层剖析。
16.2 附加信息
要获取更多系统性能分析方面的案例研究,可以查看贵公司缺陷数据库(或工单系统)中以前与性能相关的问题,以及您所使用的应用程序和操作系统的公开缺陷数据库。这些问题通常以问题描述开始,以最终修复结束。许多缺陷数据库系统还包括带有时间戳的评论历史,可以通过研究这些历史来查看分析的演进过程,包括探索过的假设和走过的弯路。走弯路以及识别出多种促成因素,这都是很正常的。
一些系统性能案例研究偶尔会被发布,例如在我的博客上 [Gregg 20j]。注重实践的技术期刊,如 USENIX ;login: [USENIX 20] 和 ACM Queue [ACM 20],在描述解决问题的新技术方案时,也经常以案例研究作为上下文。
16.3 参考文献
- [Gregg 19h] Gregg, B., “LISA2019 Linux Systems Performance,” USENIX LISA, http://www.brendangregg.com/blog/2020-03-08/lisa2019-linux-systems-performance.html, 2019.
- [ACM 20] “acmqueue,” http://queue.acm.org, accessed 2020.
- [Gregg 20e] Gregg, B., “PMC (Performance Monitoring Counter) Tools for the Cloud,” https://github.com/brendangregg/pmc-cloud-tools, last updated 2020.
- [Gregg 20j] “Brendan Gregg’s Blog,” http://www.brendangregg.com/blog, last updated 2020.
- [USENIX 20] “;login: The USENIX Magazine,” https://www.usenix.org/publications/login, accessed 2020.
本页故意留白