玩转 GPU 实例 – 我的 Linux 工具箱之三 – 系统优化

阅读数:1 2020 年 4 月 19 日 17:17

玩转GPU实例 – 我的Linux 工具箱之三 – 系统优化

前言

在该系列的前面两篇文章中,我们已经介绍了建立一个 EC2 的基于 Ubuntu 18.04 的 GPU 实例的方法以及针对这个实例完成了基础的设置。接下来的部分对于我们来说将会有很大的挑战,因为我们希望能够针对 P3 这个实例进行系统优化。我们的目的就是将实例的全部资源尽可能的榨取出来。

系统优化是一个很大的话题,在篇幅不大的文章中能够涉及的领域是非常有限的。考虑到我们最常用的场景,我将这个话题锁定在这样几个内容:

  • 系统时钟源 (clocksource) 的优化
  • Intel 处理器状态控制的优化
  • Linux Kernel 5.0 的升级
  • AWS System Manager Agent (SSM) 的安装配置
  • Linux 程序包自动升级的设定
  • Linux sysctl 的优化设定

系统时钟源的优化

运行在 X86 系统上的 Linux 可以使用不同的设备来获得时间信息。例如:

但是在虚拟化的环境下情况却有所不同。简单来说,宿主机上运行的虚拟机(VM)共享相同的时间源,但是每个 VM 不可能在同一时刻更新其时间。此外,VM 可能在执行内核的关键部分时禁用了中断,而虚拟机监控程序则会生成计时器中断。在虚拟机中某些计时系统(例如时间戳记计数器)本身是虚拟的。从 TSC 寄存器读取数据可能会导致系统性能的下降,从而导致读取结果不准确和时间倒退。

如果计时系统依赖处理器的时钟速率,则在具有不同 CPU 的虚拟机管理程序之间迁移 VM 可能会出现问题。VMWare 公司曾经发表过一篇关于这个问题的文章《 Timekeeping in VMware Virtual Machines 》,有兴趣的朋友不妨一读。

为了解决上述这些在虚拟化中时钟的问题,常见的 Hypervisor 诸如 KVM 和 XEN 都提供了自己的计时系统 PVclock。而在 LINUX 内核中,则有一组驱动程序来提供一个通用接口,这个接口的实现可以是一条指令,也可以从特殊的内存位置或寄存器中读取,我们将这个接口称为“时钟源”(clocksource)。系统中通常会有多个时钟源可供使用。以 EC2 P3 实例为例,我们用这条命令获得当前可用的时钟源:

Bash

复制代码
$cat /sys/devices/system/clocksource/clocksource0/available_clocksource
xen tsc hpet acpi_pm

这里的 xen 是 AWS 在其上一代 EC2 实例上使用的虚拟机管理程序 (Hypervisor),负责管理在物理服务器上运行的一个或多个虚拟化的实例。我们所要使用的 P3 实例的虚拟机管理程序就是来源自 Xen,缺省情况下使用 Xen pvclock 的实例其时钟源被设置为 xen。这意味着 Linux 中的 xen 时钟源获取 Xen 虚拟机管理程序 (Hypervisor) 在主机上运行的时间。如果我们不清楚实例所使用的虚拟机管理程序究竟是哪一个?这里有一个小的技巧,用这条命来得到答案 :

Bash

复制代码
$ lscpu | grep Hypervisor | awk {'print $3'}

性能问题

Xen 的时钟源有什么问题吗?在回答这个问题之前,我们先做一个实验。在不同的时钟源环境下,执行这一段代码,比较一下性能的差异。

C

复制代码
#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 的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 而不必有任何的顾虑。在 AWS 官方的文档中这也是推荐的优化方法。

优化方法

改变系统的时钟源的方法非常简单,我们可以在命令行下通过这样的一个脚本来实现 –

C

复制代码
#!/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 年的 AWS re:Invent 大会上,Nitro 作为新的一代 Hypervisor 被介绍给我们。Nitro Hypervisor 是基于 KVM 定制开发而成。而全新的 Nitro 实例缺省使用了 Kvm 的时钟源。基于 Nitro 实例上缺省的 kvm-clock 时钟源提供了与前一代基于 Xen 实例上的 tsc 类似的性能优势。此外,使用 AMD 处理器的实例也同样使用 Nitro 系统,这就意味着不需要我们去求改时钟源了。

至于如何判断我们的实例究竟是否为 Nitro 支持的实例,这里有一个检测的脚本可供参考 – nitro_check_script.sh

Intel 处理器状态控制的优化

以往,大多数计算机都是为最佳性能而设计的,它们的 CPU 频率是固定的。而现代的 CPU,功耗管理比性能峰值更重要。CPU 从单一内核发展到多个物理内核,并获得了新的特性: 超线程(Hyper-threading);Turbo Boost 可以最大限度地提高性能;还可以暂时完全关闭 CPU 内核 (CPU 停机,频率为 0),以降低功耗。内核的频率可以根据工作负载、温度等因素定期变化。为了更有效的进行针对 CPU 进行管理,就出现了 ACPI(高级配置与电源接口)标准。这是在 1997 年由英特尔、微软、东芝共同提出、制定的操作系统电源管理、硬件配置接口。到了 2011 年推出 ACPI 5.0 规格,就包括了我们将要提到的 C-State 与 P-State。

  • C-State – 处理器电源状态
    处理器电源状态(C0,C1,C2,C3……Cn 状态)是指在 G0 状态下的处理器电能消耗和温度管理的状态。只有 C0 状态下 CPU 才会执行指令,C1 到 Cn 状态下 CPU 都处于各种不同程度的睡眠状态(Sleeping States),在这睡眠状态下,CPU 都有一个恢复到 C0 的唤醒时间(latency),它是和 CPU 的电能消耗有关的,通常,用电能量越小意味着得花更长的时间恢复到 C0 状态,也就是唤醒时间越长。

  • P-State – 设备和处理器性能状态
    设备和处理器性能状态(Px 状态)是在 C0(对于处理器)和 D0(对于设备)下定义的电源消耗和能力的状态。性能状态允许 OSPM 在性能和能源消耗之间获取平衡。P0 是最高性能状态,从 P1 到 Pn 是连续的低性能状态,最高限制 n 为 16。

改变 C-State 或 P-State 设置可以增加处理器性能的一致性,减少延迟,还可以针对特定工作负载对实例进行调校。默认 C-State 和 P-State 设置可提供最大性能,是大多数工作负载的最佳选择。但是,如果应用程序更适合以牺牲较高的单核或双核频率的方式来降低延迟,或需要在较低频率下保持稳定性能 (而不适合使用突发式睿频加速频率),那么可以考虑运用对这些实例可用的 C 状态或 P 状态设置。

优化方法

C-State 控制当核心处于非活动状态时可能进入的睡眠级别。可以通过控制 C-State 来调校系统的延迟与性能。将核心置于睡眠状态需要时间,尽管睡眠中的核心可为其他核心提供更多空间以加速至更高频率,但该睡眠中的核心也需要时间来重新唤醒并执行工作。例如,如果某个负责处理网络数据包中断的核心处于睡眠状态,那么在处理此类中断时可能会出现延迟。我们可以将系统配置为不使用深层 C-State,这可以降低处理器的反应延迟,但反过来也会减少其他核心达到睿频加速频率可用的空间。

针对 P3 实例而言,我们需要禁用深层睡眠状态来确保最快的响应。具体的做法是将 C-State 设置为 C1。所谓的 C1,拥有最短的唤醒时间,这个延时必须短到操作系统软件使用 CPU 的时候不会考虑到唤醒时间方面的因素。

具体实现可以通过以下脚本 –

C

复制代码
#!/bin/bash
Hypervisor=$(lscpu | grep Hypervisor | cut -d ' ' -f5)
if [ $Hypervisor = "Xen" ]; then
sudo cp /etc/default/grub /etc/default/grub.original
sudo sed -i 's|GRUB_CMDLINE_LINUX=""|GRUB_CMDLINE_LINUX="clocksource=tsc tsc=reliable xen_nopvspin=1 intel_idle.max_cstate=1"|g' /etc/default/grub
sudo update-grub
echo "Reboot system and enable changes."
else
echo "Your Hypervisor is not Xen."
fi
echo "Done."

在这个脚本中,通过修改 /etc/default/grub 文件,加入 clocksource=tsc tsc=reliable xen_nopvspin=1 intel_idle.max_cstate=1 这几个 Grub 启动参数来实现

  • 修改时钟源为 tsc
  • CPU 的 C-State 设置为 C1

需要注意的是,修改完成以后需要重新启动设置才能生效。AWS 官方的文档也有对此的介绍,可以参考“您的 EC2 实例的处理器状态控制”。

Linux Kernel 5.0 的升级

2019 年 3 月 4 日,Linus Torvalds 在 linux-kernel 邮件列表宣布了 Linux 5.0 版本内核的正式发布。

玩转GPU实例 – 我的Linux 工具箱之三 – 系统优化
尽管尽管 Linus 戏称“如果你想有正式的理由,那就是我的手指和脚趾都用完了,所以 4.21 变成了 5.0”,但事实上 Linux 5.0 版本内核还是具有许多新功能,包括了备受期待的 Wireguard 的集成 ; 为 Y2038 问题所作的准备 ; 在 cgroupv2 中添加了对 cpuset 资源控制器的支持 ; 增加了对 btrfs 中交换文件的支持… 等等。尽管今天主流的 Linux 分发版本中 Kernel 的版本大多维持在 4.14- 4.19 这些版本之间,我们正在使用的这个 Ubuntu 18.04 缺省的 Linux Kernel 版本就是 4.15,但从过去一年多的反馈来说 Linux kernerl 5.x 可以称得上是发展飞速的版本了。从我使用的体验来看,算得上是个稳定的版本。至于名为“Focal Fossa” 的 Ubuntu LTS 下一代版本(20.04)缺省的 kernel 版本就将是 5.3。这个月底,我们就可以体验到这个新的变化了。

将 Ubuntu 18.04 的 Kernel 升级到 5.x 是一件简单不过的事情。并不需要我们手工编译 Kernel,而只是简单的安装一个名为 Ubuntu LTS enablement (也称作 HWE 或者 Hardware Enablement) 下的 linux-generic-hwe-18.04 包即可。安装的脚本如下 –

C

复制代码
#!/bin/bash
lsb_release -a
uname -a
export DEBIAN_FRONTEND=noninteractive
sudo apt update
sudo apt install -y linux-aws-edge
#sudo apt-get -o Dpkg::Options::="--force-confold" install --install-recommends linux-generic-hwe-18.04 -q -y --allow
#sudo apt-get autoremove
echo "Please reboot system."
echo "Done."

安装之后需要重新启动。这时,当我们输入这条命令就可以看到当前 Kernel 的版本

$ uname -r
5.3.0-47.39

AWS System Manager Agent (SSM) 的安装配置

AWS Systems Manager 代理(SSM agent)是一个 Amazon 软件,可以在 Amazon EC2 实例、本地服务器或虚拟机 (VM) 上安装和配置。SSM 代理 让 Systems Manager 可以更新、管理和配置这些资源。代理处理来自 AWS 云中的 Systems Manager 服务的请求,然后按照请求中指定的方式运行它们。SSM 代理之后使用 Amazon Message Delivery Service 将状态和执行信息发送回 Systems Manager 服务。这样,我们就可以通过 Systems Manager 服务来对 EC2 实例进行有效的管理。

如果我们需要 System Manager 的功能,并且 SSM agent 没有为我们安装的情况下,我们就需要通过这个脚本完成安装 –

C

复制代码
#!/bin/bash
set-e
sudo apt update
sudo apt-get install -y snap
#check agent status
sudo snap list amazon-ssm-agent
#start agent
sudo systemctl start snap.amazon-ssm-agent.amazon-ssm-agent.service
sudo systemctl enable snap.amazon-ssm-agent.amazon-ssm-agent.service
echo "Done."

Linux 程序包自动升级的设定

如果你与我一样患有软件版本的“洁癖”,总是要将软件升级到最新版本。那么这个技巧绝对是治病的良药。方法的关键是安装配置了一款名为 unattended-upgrades 的软件包。它的作用是自动安装更新的包,并可配置为更新所有包或只安装安全更新。安装配置的方法如下 –

C

复制代码
#!/bin/bash
set -e
sudo apt update
sudo apt-get install -y unattended-upgrades
#upgrade all updates
sudo sed -i 's|// "${distro_id}:${distro_codename}-updates";| "${distro_id}:${distro_codename}-updates";|' /etc/apt/apt.conf.d/50unattended-upgrades|
#autoremove unused kernel
sudo sed -i 's|//Unattended-Upgrade::Remove-Unused-Kernel-Packages "false";|Unattended-Upgrade::Remove-Unused-Dependencies "false";|' /etc/apt/apt.conf.d/50unattended-upgrades
#auto remove unused dependencies
sudo sed -i 's|//Unattended-Upgrade::Remove-Unused-Dependencies "false";|Unattended-Upgrade::Remove-Unused-Dependencies "false";|' /etc/apt/apt.conf.d/50unattended-upgrades
#enable autoreboot
sudo sed -i 's|//Unattended-Upgrade::Automatic-Reboot "false";|Unattended-Upgrade::Automatic-Reboot "true";|' /etc/apt/apt.conf.d/50unattended-upgrades
#autoreboot at 2:00am
sudo sed -i 's|//Unattended-Upgrade::Automatic-Reboot-Time "02:00";|Unattended-Upgrade::Automatic-Reboot-Time "02:00";|' /etc/apt/apt.conf.d/50unattended-upgrades
sudo tee -a /etc/apt/apt.conf.d/20auto-upgrades >/dev/null << EOT
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
EOT
echo "Done."

在这个脚本当中,我的设置是自动完成全部的升级,自动删除不使用的 kernel,自动删除不使用的依赖项,允许自动重新启动,自动启动的时间设定为凌晨 2 点等等。

如果你有不同的需要,可以在此之上作出适当的修改即可。

Linux sysctl 的优化设定

P3 作为 GPU 实例很多时候被用于使用各种深度学习的框架来训练模型。对于模型过大的情况,就需要利用多个 EC2 实例来进行分布式的模型训练。在分布式计算的术语中,这些实例通常被称为节点(node),这些节点的集合就是集群。为了实现分布式训练,除了节点的资源以外,还需要解决网络通讯的问题。我们还必须了解另一个术语—消息传递接口(MPI)。MPI 是一个开放标准,它定义了一系列关于节点互相通信的规则,MPI 也是一个编程模型 /API。

缺省的 Ubuntu 的设置并不会考虑到上述的这些场景,于是我们就必须要针对 MPI 网络通讯以及 P3 实例的系统性能作必要的优化。我的优化设置是这样的

C

复制代码
#!/bin/bash
#network optimization
sudo tee -a /etc/sysctl.conf << 'EOF'
#Performance settings
#Increasing the size of the TCP receive queue
net.core.netdev_max_backlog = 100000
net.core.netdev_budget = 50000
net.core.netdev_budget_usecs = 5000
#Increase the maximum connections
net.core.somaxconn = 1024
#Increase the memory dedicated to the network interfaces
net.core.rmem_default = 1048576
net.core.rmem_max = 16777216
net.core.wmem_default = 1048576
net.core.wmem_max = 16777216
net.core.optmem_max = 65536
net.ipv4.tcp_rmem = 4096 1048576 2097152
net.ipv4.tcp_wmem = 4096 65536 16777216
#Increase UDP limits, default is 4096
net.ipv4.udp_rmem_min = 8192
net.ipv4.udp_wmem_min = 8192
#Enable TCP Fast Open
net.ipv4.tcp_fastopen = 3
#Tweak the pending connection handling
net.ipv4.tcp_max_syn_backlog = 30000
net.ipv4.tcp_max_tw_buckets = 2000000
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_fin_timeout = 10
net.ipv4.tcp_slow_start_after_idle = 0
#Change TCP keepalive parameters
net.ipv4.tcp_keepalive_time = 60
net.ipv4.tcp_keepalive_intvl = 10
net.ipv4.tcp_keepalive_probes = 6
#Enable MTU probing
net.ipv4.tcp_mtu_probing = 1
#TCP timestamps
net.ipv4.tcp_timestamps = 0
#Enable BBR
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
#TCP SYN cookie protection
net.ipv4.tcp_syncookies = 1
#TCP rfc1337
net.ipv4.tcp_rfc1337 = 1
#Reverse path filtering
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.all.rp_filter = 1
#Log martian packets
net.ipv4.conf.default.log_martians = 1
net.ipv4.conf.all.log_martians = 1
#Disable ICMP redirects
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.secure_redirects = 0
net.ipv4.conf.default.secure_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
#Ignore ICMP echo requests
net.ipv4.icmp_echo_ignore_all = 1
net.ipv6.icmp.echo_ignore_all = 1
#Virtual memory, default is 20,10
vm.dirty_ratio = 10
vm.dirty_background_ratio = 5
#Decreasing the VFS cache parameter value,default is 100
vm.vfs_cache_pressure = 50
#change Swappiness,default is 60. cat /proc/sys/vm/swappiness
vm.swappiness = 10
EOF
sudo sysctl -p
echo "Done."

如果我们顺利的完成了全部的优化,相信我们的这台实例的潜力已经被成功的挖掘出来。这些方法不仅适用于 P3 实例,对于大多数的 EC2 实例都具有参考作用。需要强调的一点,关于时钟源的优化方法仅限于基于 Xen Hypervisor 的实例,不适用于基于 Nitro 的新类型的实例。至于 Nitro 实例的优化方法,也许要等到本系列结束之后我们再来探讨。

本文转载自 AWS 技术博客。

原文链接: https://amazonaws-china.com/cn/blogs/china/using-rekognition-realize-serverless-intelligent-album-playing-with-gpu-instance-iii-system-optimization/

评论

发布