发布于: Jul 29, 2022
宿主机上运行的虚拟机(VM)共享相同的时间源,但是每个 VM 不可能在同一时刻更新其时间。此外,VM 可能在执行内核的关键部分时禁用了中断,而虚拟机监控程序则会生成计时器中断
运行在 X86 系统上的 Linux 可以使用不同的设备来获得时间信息。例如:
- 编程中断定时器
- 实时时钟
- 高精度事件计时器
- 本地 APIC 计时器
时间戳计数器(Time Stamp Counter,简称 TSC) 但是在虚拟化的环境下情况却有所不同。简单来说,宿主机上运行的虚拟机共享相同的时间源,但是每个 VM 不可能在同一时刻更新其时间。此外,VM 可能在执行内核的关键部分时禁用了中断,而虚拟机监控程序则会生成计时器中断。在虚拟机中某些计时系统(例如时间戳记计数器)本身是虚拟的。从 TSC 寄存器读取数据可能会导致系统性能的下降,从而导致读取结果不准确和时间倒退。
如果计时系统依赖处理器的时钟速率,则在具有不同 CPU 的虚拟机管理程序之间迁移 VM 可能会出现问题。VMWare 公司曾经发表过一篇关于这个问题的文章《Timekeeping in VMware Virtual Machines》,有兴趣的朋友不妨一读。
为了解决上述这些在虚拟化中时钟的问题,常见的 Hypervisor 诸如 KVM 和 XEN 都提供了自己的计时系统 PVclock。而在 LINUX 内核中,则有一组驱动程序来提供一个通用接口,这个接口的实现可以是一条指令,也可以从特殊的内存位置或寄存器中读取,我们将这个接口称为“时钟源” (clocksource)。系统中通常会有多个时钟源可供使用。以 EC2 P3 实例为例,我们用这条命令获得当前可用的时钟源:
$cat /sys/devices/system/clocksource/clocksource0/available_clocksource xen tsc hpet acpi_pm
这里的 xen 是 Amazon Web Services 在其上一代 EC2 实例上使用的虚拟机管理程序 (Hypervisor),负责管理在物理服务器上运行的一个或多个虚拟化的实例。我们所要使用的 P3 实例的虚拟机管理程序就是来源自 Xen,缺省情况下使用 Xen pvclock 的实例其时钟源被设置为 xen。这意味着 Linux 中的 xen 时钟源获取 Xen 虚拟机管理程序 (Hypervisor) 在主机上运行的时间。如果我们不清楚实例所使用的虚拟机管理程序究竟是哪一个?这里有一个小的技巧,用这条命来得到答案:
$ lscpu | grep Hypervisor | awk {'print $3'}
Xen 的时钟源有什么问题吗?在回答这个问题之前,我们先做一个实验。在不同的时钟源环境下,执行这一段代码,比较一下性能的差异。
#include <stdio.h>#include <stdint.h>#include <stdlib.h>#include <time.h> #define BILLION 1E9 int main(){ float diff_ns; struct timespec start, end; int x; clock_gettime(CLOCK_MONOTONIC, &start); for ( x = 0; x < 100000000; x++ ) { struct timeval tv; gettimeofday(&tv, NULL); } clock_gettime(CLOCK_MONOTONIC, &end); diff_ns = (BILLION * (end.tv_sec - start.tv_sec)) + (end.tv_nsec - start.tv_nsec); printf ("Elapsed time is %.4f seconds\n", diff_ns / BILLION ); return 0;}
解释一下,函数 ”clock_gettime” 是基于 Linux C 语言的时间函数,可以用于计算时间。函数的参数 “CLOCK_MONOTONIC”,是指从系统启动这一刻起开始计时,不受系统时间被用户改变的影响。而函数 “gettimeofday” 会把目前的时间由 tv 所指的结构返回。
我们分别在两个时钟源(xen 与 tsc)下运行这个程序,结果如下:
运行的结果让我们大吃一惊!在 xen 的时钟源下这个程序的的执行时间居然是时钟源为 tsc 的 4.83 倍!其原因在于 tsc 时钟源是从时间戳计数器 (TSC) 来读取时间的。这个 TSC 是 Intel x86 架构 CPU 上的计数器,大致与自处理器启动以来的时钟周期数相对应,并可以 rdtsc 或 rdtscp 指令来读取。至关重要的是,这些都操作是非特权指令,意味着从 tsc 时钟源获取时间而无需切换到内核模式。这就是使用 tsc 性能更好的原因。
通常,我解释到此的时候总会有开发者质疑我:既然 tsc 这么好为什么不将它设置为 EC2 上缺省的时钟源?回答这个问题确实比较复杂。简单的概括起来就是通常我们认为不同的时钟源具有不同级别的稳定性,并且与频率和跨处理器同步有关。由于时钟源 tsc 直接针对 CPU 发出 rdtsc 指令,因此其属性与硬件相关,包括物理和虚拟的硬件。在一些旧的硬件上,有可能出现向后时钟漂移。关于这个问题的讨论,在 Xen 的社区有一篇文档可以给我们提供更多的答案 “how they handle timestamp counter emulation”。
至于我们用到的 P3 实例则是较新的硬件,在 Xen 的术语中是 TSC 安全的。在过去许多年的实践中,我们尽可以放心的修改时钟源到 tsc 而不必有任何的顾虑。在 Amazon Web Services 官方的文档中这也是推荐的优化方法。
改变系统的时钟源的方法非常简单,我们可以在命令行下通过这样的一个脚本来实现 –
#!/bin/bash echo "Current clocksource is :" clock=$(cat /sys/devices/system/clocksource/clocksource0/current_clocksource) echo $clock echo "available clocksource is " cat /sys/devices/system/clocksource/clocksource0/available_clocksource if [ $clock = "xen" ]; then echo "set clocksource to tsc" sudo bash -c 'echo tsc > /sys/devices/system/clocksource/clocksource0/current_clocksource' cat /sys/devices/system/clocksource/clocksource0/current_clocksource fi echo "Done."
如果我们需要将 tsc 设置为缺省的时钟源,并且在系统开机以后自动生效,那就需要修改 grub 的启动配置。我们将在下一节介绍具体的实现方法。
此外,对于 EC2 实例而言 Xen 已经成为了过去时。在 2017 年的 Amazon Web Services re:Invent 大会上,Nitro 作为新的一代 Hypervisor 被介绍给我们。Nitro Hypervisor 是基于 KVM 定制开发而成。而全新的 Nitro 实例缺省使用了 Kvm 的时钟源。基于 Nitro 实例上缺省的 kvm-clock 时钟源提供了与前一代基于 Xen 实例上的 tsc 类似的性能优势。此外,使用 AMD 处理器的实例也同样使用 Nitro 系统,这就意味着不需要我们去求改时钟源了。
至于如何判断我们的实例究竟是否为 Nitro 支持的实例,这里有一个检测的脚本可供参考 –nitro_check_script.sh