1.背景

有时候我们很难精准的评估一份程序在运行时的性能表现。

要分析问题得出结论,往往需要控制变量,而性能问题的变量往往会非常多。

  • 同一程序在不同主频/架构的CPU上或者使用不同指令集运行,CPU占用可能相差很大
  • 同一程序在同一运行环境下,使用不同的输入数据,CPU占用、内存占用相差都可能很大
  • 同一程序在同一运行环境下,使用相同的输入数据,程序自身使用不同的启动选项,CPU占用、内存占用相差都可能很大

这些变量在某些比较复杂的工具软件上尤为明显。官方给出的参考 benchmark,往往只是一个或几个主流平台和主流使用场景下的数据。

实际项目中会遇到很多不一样的场景,当我们自己使用时的性能数据和官方性能数据有出入时,有可能会下意识的认为这就是前面提到的变量差异带来的,正常的性能差异。

本文记录一下容器环境逻辑核数判断对一些程序性能的影响。造成这种影响的原因是非常简单的,但是可能具有一定普遍性,且很容易因为前面所述的性能问题复杂性而被忽略,所以单独拿出来说一说。

2.容器环境逻辑核资源判断问题

容器化部署的时代,很多场景下,我们的应用不再是直接运行在物理机/虚拟机上,而容器中通过 /proc/cpuinfo 获取逻辑核数,或者通过

1
2
3
4
5
6
7
#include <unistd.h>
long sysconf(int name); // _SC_NPROCESSORS_ONLN
...
#include <sys/sysinfo.h>
int get_nprocs(void);
int get_nprocs_conf(void);
...

这些 posix 接口函数来获取逻辑核数,返回的都是宿主机的核数。

有一些软件/库中,会内置一种自动配置选项的操作:获取默认的逻辑核数,然后根据逻辑核数初始化最佳的工作线程数。

这种操作在非容器时代,能让程序使用代价降低,在内部自动做好最佳性能选项配置。然而在容器部署的时代,就可能因为错用了宿主机的核数,导致了大量的线程切换开销、多余缓存数据、锁竞争等问题,出现反向优化的情况。

3.逻辑核数判断问题对性能的影响

3.1 MKL 计算线程数

现象

服务中在使用 tensorflow / libtorch 等库做深度学习模型的前向推理运算时,若使用 CPU 模式,则它们的底层都会使用 MKL 库来做矩阵运算。

MKL库内置了一个逻辑,会根据运行环境的逻辑核数自动设置其内部 OpenMP 的计算线程数。若容器被分配了 16 核,实际宿主机 80 核,那么按 MKL 的原本设计思路,应该默认设置为 16 核,能达到最佳性能表现,实际上给设置为了 80 线程,导致计算性能反而下降。

当设置不当时,带来的影响:

  • 压满 CPU 时,前向推理运算最大并发数比实际最佳性能要低很多
  • 单次推理任务耗时比实际最佳性能长很多

由于不同类型的模型推理表现本来就存在很大的性能差异,且 CPU 推理并发性能也好,推理速度也好,确实比 GPU 模式都要差很多,所以很容易认为这就是 CPU 下的真实表现,而漏掉问题。

解法

在容器中运行时,不再让 MKL 通过自动检测的逻辑核数自动设置线程数,而是主动

  • 通过 MKL_NUM_THREADS 环境变量设置 MKL 线程数
  • 通过 mkl-set-num-threads 方法设置 MKL 线程数

参考文档: https://www.intel.com/content/www/us/en/docs/onemkl/developer-reference-c/2024-0/mkl-set-num-threads.html

3.2 ffmpeg 工作线程数

现象

ffmpeg 在做视频编码时,默认会自动根据运行环境的逻辑核数,设置 -thread 参数所指定的工作线程数。

当设置不当时,带来的影响:

  • 慢速编码(输入帧较慢)时,ffmpeg进程内存占用过大。
  • 正常速度编码时(25fps),CPU 占用偏大

由于 ffmpeg 编码输出视频时,输入数据规格、输出数据规格、各种选项参数非常繁杂,没有完全控制变量对比的情况下,一时间有点不好察觉到性能上的不对劲。

例如我这边遇到的一个场景下:

  • 40逻辑核机器,容器分配16核,做4K视频的编码输出,每 1.3s 输入一帧数据。ffmpeg 进程工作线程数 122,内存占用 5.1G;修改 -threads 参数为 8 后,ffmpeg 进程工作线程数 22,内存占用 1.6G,编码速度不变。
  • 80逻辑核机器,容器分配4核,做2K视频的编码输出,每秒输入 25 帧数据。ffmpeg 进程工作线程数 200+,CPU占用4.5 核;修改 -threads 参数为 4 后,ffmpeg 进程工作线程数 10,CPU占用 4 核,编码速度不变。

解法

手动根据实际情况设置工作线程数。

3.3 普遍性

除了上面列举的两个之外,很多软件会根据运行环境的逻辑核心数自动配置选项以优化多处理器下的性能,如 Nginx、Apache HTTP Server、Mysql、Golang 等。

它们利用系统的逻辑核数来对内部的一些选项进行默认配置,在容器中运行时往往不但达不到最优性,反而会起一些反作用,但是又不会影响程序的正常运行,性能也不会下降到不可用的地步,因而不太容易发现。

所以我推测这种情况具有一定普遍性,值得注意。

4. 容器中正确判断资源

可以使用 lxcfs 获取真实资源配置

此文不再赘述,参考此文:https://cloud.tencent.com/developer/article/1807333

☞ 参与评论