1. 问题的提出
长期以来,对于容器的“build once, run anywhere”的口号深信不疑。在生产环境中随意拉取公网上docker images,直接使用或在此基础上继续构建自己业务的现象比比皆是。今天我们先不谈镜像的仓库及安全更新问题,只就性能和兼容性问题,带大家看看这样做会给我们带来多大的麻烦。
2. 事故案例
现网中出现数十台物理机反应很慢,偶发性死机问题。经top发现普遍存在crond进程CPU占用高且数量庞大的问题。然而奇怪的是这种问题只出现在宿主机系统为ctyunos2的环境中。而最终我们可以把复现环境缩小到如下情况:
操作系统: CTyunOS2.0.1 vs CentOS7.9
容器环境: docker
容器镜像: CTyunos2 vs CentOS8.4 (容器内只运行一个crond)
对比后如图所示,在加载同样的基于centos8.4的业务镜像情况下,左图为CtyunOS2.0.1操作系统,有大量crond进程占用CPU资源,容易导致系统卡死;右图为CentOS7.9操作系统,则没有任何问题。
但在使用基于ctyunos2容器基础镜像构建的业务镜像时,两边都没有发现cpu 负载过高的问题。
3. 根因分析
(1) 为什么会造成性能损失,为什么ctyunos镜像没问题
打开容器环境中的/var/log/message, 可以看到很多文件关系相关的错误,直观上是因为大量fd需要关闭导致的。
利用在top中查看到的crond的pid使用gdb -p <crond pid>可以看到进程最后的调用堆栈:
根据do_command child_process 和close,推测到应该与这个github的PR有关:
cronie-crond/cronie/commit/584911514ce6aa2f16e1d79431bac816ea62cb2c
fdmax是在老版本的cronie中是由getdtablesize直接设定,而执行关闭文件关闭时会一直遍历到fdmax,可以想见,如果fdmax过大,性能将有极大损失。
新版本中fdmax将被限制在MAX_CLOSE_FD之内,但这个PR是从cronie-1.5.5版本才引入。线上正在使用的centos8.4镜像中正是在使用低于此版本的cronie-1.4.11-23,而基于我们ctyunos2的镜像使用的是高于此版本的cronie-1.5.5-2,所以ctyunos2的镜像是做过问题修复的,而centos的没有。
结论1:使用基于其它系统构建的镜像,存在安全或性能更新不及时的风险
(2) 为什么有问题的镜像在centos7.9上没问题
对比发现,centos7.9的gettabelesize()值比ctyunos2的要小很多。而这个值的来源是/proc/1/limits中的Max open files的值。
在ctyunos2上soft Limit和Hard Limit都是一个很大的数:1073741816。
而这个值在centos7.9上是1024*1024 = 1048576,足足差了1000多倍
也就是说在使用含有低版本cronie包的镜像的条件下,在遍历关闭文件过程中,ctyunos2要比centos7.9多花1000多倍的时间。当然我们知道这些重要的系统值是牵一发而动全身的,简单的改小系统的Max open files绝不是我们想要的解决方案,也许这也是当初cronie提PR修复的原因。因此根据系统情况选择正确的镜像就显得犹为重要。
结论2:宿主机系统与镜像系统的版本在某些情况下是相互挑剔的,因此业务镜像最好使用同一系统的base image进行构建,以确保业务的万无一失。
(3) 为什么ctyunos2的max open files这么大?
根据Redhat的官方文档solutions/1479623:
Note: The value of "Max open files"(ulimit -n) is limited to fs.nr_open value.
通过这个实验可以清楚的看到ulimit -n的值不能设置的大于fs.nr_open
而fs.nr_open这个值最初定义于kernel:kernel/fs/file.c
取__read_mostly和sysctl_nr_open_max相比中最大的:
27 unsigned int sysctl_nr_open __read_mostly = 1024*1024;
28 unsigned int sysctl_nr_open_min = BITS_PER_LONG;
29 /* our min() is unusable in constant expressions ;-/ */
30 #define __const_min(x, y) ((x) < (y) ? (x) : (y))
31 unsigned int sysctl_nr_open_max =
32 __const_min(INT_MAX, ~(size_t)0/sizeof(void *)) & -BITS_PER_LONG;
我们发现不只在CTyunOS2上,在RHEL9上ulimit -n(Max open files)值也都比较大。直接设置原因在于fs.nr_open被某个神秘程序更新为了很大的数:
# cat /proc/sys/fs/nr_open
1073741816
而这背后的原因应该是大规模商用系统需要更多的可打开文件数,这显然不能被随意设小。那么这个神秘程序是谁呢?
(4) 是谁更新了fs.nr_open?
简而言之,是新版本的systemd。请看github的PR
systemd/systemd/commit/a8b627aaed409a15260c25988970c795bf963812
这个merge是在systemd v240里加入的,我们ctyunos2中的版本是v243,而centos7.9的版本是v219. 这也就解释了为什么centos7.9的fs.nr_open小,而RHEL9及CTyunOS2的大
结论3:即使是同系列系统,重要系统值也会有所不同,所以要慎重选择系统。
4. 总结
基于上面的三条结论:
- 使用基于其它系统构建的镜像,存在安全或性能更新不及时的风险
- 宿主机系统与镜像系统的版本在某些情况下是相互挑剔的,因此业务镜像最好使用同一系统的base image进行构建,以确保业务的万无一失。
- 即使是同系列系统,重要系统值也会有所不同,所以要慎重选择系统。
我们的总结如下:
宿主机和容器系统版本差异较大时会存在风险,虽然这种问题概率较低,以至于我们长期没有重视,但从商业稳定角度来看,推荐从自己的业务开始推行centos/ubuntu/alpine/busybox等镜像替换工作。
使用其它系统的镜像不仅有安全漏洞隐患(尤其是centos这样的镜像),后续无法保障安全更新,还可能造成重大性能问题,潜在造成很多不明原因的系统卡死问题。推荐使用构建流水线,基于自己的base image全面构建自研业务镜像,并配套使用类似于k8s-install(可直接从gitee中搜索)的工具安装并更新相关软件包及容器镜像,全面达成安全漏洞封堵、业务100%兼容及性能优化的需要。
另一方面我们也意识到,我们无法强制客户全部使用ctyunos重构自己的镜像,无法阻止客户拉取网上centos镜像,这就和当前公有云主机我们还提供centos一样,客户习惯我们要尊重,但是风险提示我们要给到位,出现问题客户要么自行解决,要么接受建议使用我们提供的ctyunos镜像。
总之,容器层面的centos替换,应首先从存在问题的镜像做起,然后逐步推广。