亚马逊AWS官方博客

玩转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实例为例,我们用这条命令获得当前可用的时钟源:

$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)在主机上运行的时间。如果我们不清楚实例所使用的虚拟机管理程序究竟是哪一个?这里有一个小的技巧,用这条命来得到答案 :

$ 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

 

运行的结果让我们大吃一惊!在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官方的文档中这也是推荐的优化方法。

 

优化方法

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

#!/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的时候不会考虑到唤醒时间方面的因素。

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

#!/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 版本内核的正式发布。

尽管尽管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包即可。安装的脚本如下 –

#!/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没有为我们安装的情况下,我们就需要通过这个脚本完成安装 –

#!/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的软件包。它的作用是自动安装更新的包,并可配置为更新所有包或只安装安全更新。安装配置的方法如下 –

#!/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 实例的系统性能作必要的优化。我的优化设置是这样的

#!/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 Principal Developer Advocate。在过去的20多年一直从事软件架构、程序开发以及技术推广等领域的工作。他经常在各类技术会议上发表演讲进行分享,他还是多个技术社区的热心参与者。他擅长Web领域应用、移动应用以及机器学习等的开发,也从事过多个大型软件项目的设计、开发与项目管理。目前他专注与云计算以及互联网等技术领域,致力于帮助中国的 开发者构建基于云计算的新一代的互联网应用。