有时你的程序慢不是因为你的代码,而是因为它运行的地方。如果你有其他进程在竞争同样有限的硬件资源,你的代码会运行得更慢。
一旦你把虚拟化添加到组合中,那些竞争的进程可能是看不见的......但它们仍然存在。
在这篇文章中,我们将介绍。
- CPU的核心和 "超线程 "的硬件限制。
- 操作系统如何处理想要使用有限数量CPU核心的多个进程。
- 虚拟化对这些不同资源的影响,包括云vCPU的含义。
- 通过容器提供的CPU共享限制。
你的CPU的并行性限制
作为一种粗略的简化,在过去,许多计算机只有一个核心,这意味着在任何时候都只能进行一次计算。现代计算机通常有多个内核,所以你可以并行地运行多个计算。
例如,我有一台装有英特尔i7-6700TCPU的电脑。根据链接的规格,它有4个核心,所以它可以并行运行4个不同的计算,每个核心一个。
如果我在Linux上查看/proc/cpuinfo ,我可以得到关于这些核心的信息。
$ cat /proc/cpuinfo
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 94
model name : Intel(R) Core(TM) i7-6700T CPU @ 2.80GHz
stepping : 3
microcode : 0xea
cpu MHz : 2800.000
cache size : 8192 KB
physical id : 0
...
processor : 1
...
processor : 2
... etc. ...
操作系统如何处理有限的内核
无论你有多少个CPU核心,在通用计算机上,你几乎总是在运行比核心更多的进程。例如,我为这篇文章所使用的相当安静的Linux机器,如果我运行ps xa ,从ssh 到屏幕保护程序到bash 会话,有260个不同的进程ID。
260比4大得多,那么所有这些进程如何进行?它们不可能同时运行,在任何时候只有4个可以使用CPU核心。为了确保所有的代码都能运行,操作系统会根据需要安排它们在核心上运行和离开。
举个简单的例子,如果你有一个单核和两个进程P0 和P1 ,这两个进程正在全速运行做一些计算,操作系统就会运行。
Core 0: P0, P1, P0, P1, P0, P1
如果你有两个内核和四个进程,它可能会运行。
Core 0: P0, P2, P1, P3, P1
Core 1: P1, P3, P2, P1, P0
当然,如果一个进程在等待什么(从RAM或磁盘中读取数据、网络、锁......),它根本不需要使用一个核心;这就给了其他进程一个被安排的机会。在实践中,典型计算机上的大多数进程只是坐在那里等待;例如,ssh 进程只有在有消息发送给它的时候才做一些事情,而按照计算机CPU的标准,我的打字速度并不快。
注意:一个Linux进程可以有多个操作系统线程,每一个线程--从Linux内核的角度来看--其实和一个进程没有什么区别,而且调度方式也很相似。然而,我试图避免过多地使用 "线程 "这个词,因为我们将在文章的后面遇到它的一个非常不同的含义。
共享CPU会减慢单个进程的速度
考虑到进程的数量大于内核的数量,如果所有这些进程都想在CPU上运行计算,那么每个进程的运行时间就会更长。让我们看看一些例子。
如果我在一个线程上运行一个小的基准,我得到的性能如下(为了得到完整的性能,--privileged 标志是必要的)。
$ docker run --privileged -v $PWD:/home python:3.10-slim python /home/pystone.py
Pystone(1.1) time for 50000 passes = 0.18486
This machine benchmarks at 270476 pystones/second
我也可以使用以下脚本在不同的CPU核心上并行运行两个基准,该脚本利用docker run'的--cpusets-cpus 标志。
from subprocess import check_call
from threading import Thread
from sys import argv
def run_on_cpu(cpu):
cpu = int(cpu)
check_call(
f"docker run --privileged -v /home/itamarst:/home --cpuset-cpus={cpu} python:3.10-slim python /home/pystone.py 100_000".split()
)
Thread(target=lambda: run_on_cpu(argv[1])).start()
run_on_cpu(argv[2])
如果我在Linux识别为1和2的处理器核心上并行运行两个基准,两个基准都以全速运行。
$ python3 parallel.py 1 2
Pystone(1.1) time for 100000 passes = 0.377883
This machine benchmarks at 264632 pystones/second
Pystone(1.1) time for 100000 passes = 0.370839
This machine benchmarks at 269659 pystones/second
如果我在同一个核心上运行它们,速度就会减半。Linux调度器为了公平起见,不断地将它们来回交换,所以每个人在CPU上运行的时间大约只有一半。
$ python3 parallel.py 2 2
Pystone(1.1) time for 100000 passes = 0.75122
This machine benchmarks at 133117 pystones/second
Pystone(1.1) time for 100000 passes = 0.720972
This machine benchmarks at 138702 pystones/second
虚拟内核("超线程")给你更多的并行性
到目前为止,我们一直在讨论内核,我们在英特尔网站上看到我的CPU有4个内核。让我们看看Linux认为我的电脑有多少个内核。
$ cat /proc/cpuinfo | grep processor
processor : 0
processor : 1
processor : 2
processor : 3
processor : 4
processor : 5
processor : 6
processor : 7
这比核心数多了一倍。这到底是怎么回事?
这个CPU有4个核心,但它有8个 "线程"、"超线程 "或 "虚拟核心"。这是一个硬件特征,不能与操作系统的线程混淆,所以我在文章的其余部分将坚持使用 "虚拟核心",以防止产生歧义。
从本质上讲,CPU内核有一堆潜在的并行性,但并不总是被充分利用。因此,硬件设计者将每个核心分成两个虚拟核心:它们看起来像普通的核心,但实际上是在共享同一个硬件。在某些情况下,这可以从CPU中提取更多的性能。
**注意:**在PC上,你通常可以通过在BIOS中禁用 "超线程 "来禁用虚拟核心。
/proc/cpuinfo 信息告诉我们,Linux识别的 "处理器 "数字(即虚拟核心)下有哪些物理核心。
$ cat /proc/cpuinfo | grep 'processor\|core id'
processor : 0
core id : 0
processor : 1
core id : 1
processor : 2
core id : 2
processor : 3
core id : 3
processor : 4
core id : 0
processor : 5
core id : 1
processor : 6
core id : 2
processor : 7
core id : 3
因此,"处理器 "0和4有相同的核心ID,1/5、2/6和3/7也是如此。
让我们尝试在同一个物理核心上并行运行我们的基准,并将其分成两个虚拟核心。
$ python3 parallel.py 3 7
Pystone(1.1) time for 100000 passes = 0.704348
This machine benchmarks at 141975 pystones/second
Pystone(1.1) time for 100000 passes = 0.680433
This machine benchmarks at 146965 pystones/second
之前,我们在两个独立的物理核心上运行,所以我们从每个并行的基准中得到了充分的性能。在这里,我们是在两个虚拟核心上共享同一个物理核心,所以性能受到了影响。
这是否意味着超线程是无用的?不一定:你是否得到性能提升取决于你运行的工作负载。但就增加并行性的能力而言,虚拟内核在很大程度上不是完整的内核。
总结:进程位置对性能的影响
为了大量简化,这里总结了一对进程在哪个核心上运行的性能影响。100%意味着你从单个核心以最大速度运行单个进程所得到的性能;数字越大越好。
| 配置 | 进程1的性能 | 进程2的性能 | 总数 |
|---|---|---|---|
| 不同的物理核心 | 100% | 100% | 200% |
| 同一物理核心 | 50% | 50% | 100% |
| 同一物理核心上的不同虚拟核心 | 50%-60% | 50-60% | 100%-120% |
50%-60%有点手忙脚乱,因为虚拟核心的好处因工作负载和CPU型号而异,但理论上在某些情况下你会得到性能提升。
附注:除了内核,还有其他共享的CPU资源
请注意,核心只是有限的共享资源之一,在不同的进程之间被分割。还有,例如,内存缓存。其中一些缓存是按核计算的,但最大的L3缓存通常是由多个核共享的。
因此,如果L3缓存是你的性能瓶颈,即使CPU核心不是瓶颈,进程也会互相拖累。
云计算,虚拟化,和嘈杂的邻居
到目前为止,我们谈论的是物理计算机。然而,当你在云中运行时,你通常是在一个虚拟机上运行。虚拟机假装(在某种程度上是真实的)是一个完全成熟的计算机,但它实际上只得到物理计算机资源的一个子集。
通常情况下,云计算供应商以你得到多少个 "vCPU "来描述他们的虚拟机。什么是vCPU?
这是 "虚拟核心 "中的 "线程":云中的vCPU是一个虚拟核心,而不是一个完整的物理核心。还记得当我们有两个进程运行在映射到同一个物理核心的两个虚拟核心上时,它们都经历了一个显著的减速吗?
这意味着。
- 如果你在单个vCPU上运行,其他云客户的一些虚拟机上的进程可能会导致你的进程变慢,因为它在共享同一个物理核心。
- 如果你在一对vCPU上运行,我不清楚它们是否是同一个物理核心。无论哪种方式,如果你试图最大化两个vCPU,都会有保证(来自你的进程)或可能(来自其他虚拟机)的减速。
根据实例的类型,云计算供应商也会对CPU的可用性有各种其他限制;例如,一些实例会允许较低的CPU使用率,但允许短期内较高的CPU使用率爆发。另外,在某些情况下,一个vCPU实际上映射到一个物理核心;请看你的云供应商的具体实例文档。
还有其他共享资源,如L3内存缓存,可能意味着其他虚拟机的进程会拖累你。硬件供应商有各种方法让云计算和托管供应商缓解这个问题,比如英特尔的资源目录技术。
这个普遍的问题被称为 "嘈杂的邻居 "问题:在同一台物理机上运行的其他虚拟机会拖累你的进程。这个名字有点名不副实,因为在现实世界中,你确实可以听到你的邻居,知道那是个问题。在云计算背景下,其他虚拟机被设计成对你的虚拟机尽可能的不透明,这主要是出于安全考虑。
对批处理的影响
如果你正在运行长期运行的CPU绑定的批处理程序,你需要了解你正在运行的硬件和环境,并适当地利用它们。
在物理机上。每个核心的单一进程是一个合理的起点,但你需要弄清楚利用虚拟核心是否真的有帮助。可能你最好是每个物理核心只有一个进程;尝试测量这两个选项进行比较。
在云中。请记住,单个vCPU并不是物理CPU核心,它们是具有相应注意事项的虚拟核心。此外,不同的实例类型可能有非常不同的长期性能特征。许多云实例为网络应用进行了优化,短期的CPU使用量可能已经足够;你的需求可能不同,需要适当的实例类型和vCPU数量。