1. 引言
随着容器技术越来越流行,Docker的应用也越来越广泛,不仅仅只有用来更快地开发或者部署应用 [1],还有了其他应用场景,比如用来实现Serverless [2],利用Docker来管理交换机的存储、计算和网络资源 [3],完成高性能多任务计算 [4] 等等。Docker容器由Docker引擎执行和控制,这与虚拟机(Virtual machine简称VM)的管理程序不同。由于它不包含完整的客户操作系统(Guest OS),所以Docker容器比VM更小,启动速度更快。Docker容器装载一个单独的根文件系统,其中包含类Unix操作系统的目录结构,以及运行用户应用程序所需的所有配置文件、二进制文件和库 [5]。可以模拟用户进行学习的实验场景。Docker也被应用于构建大规模在线实训平台,利用Docker轻量级虚拟化技术,并且拥有快速弹性伸缩的特性,能够给予用户一个完整的实训体验。
在此类场景下特点有:1) 容器生灭快,容器生存周期短。2) 容器操作并发量大。3) 容器镜像稳定,实训环境固定,容器镜像变化不频繁。4) 大量线程跟Docker宿主机进行(Http or Socket)通信,要求网络稳定,网络抖动小。5) 充分利用云平台特性,动态地给予用户云平台资源。6) 不进行高性能计算(HPC)。
本文主要针对以下问题进行研究:1) 确定高并发容器启动下的容器通信方式,确定一个可靠的数学模型可以进行通信方式选择。2) 确定影响容器并发启动的各个因素,并且确定容器并发启动的性能瓶颈。
2. 研究现状
Docker是一个新的工具,它可以在Linux容器中自动部署应用程序。它提供了操作系统级虚拟化的抽象层和自动化。容器在Linux上本地运行,并与其他容器共享主机的内核。它运行一个独立的进程,不占用比其他任何可执行程序更多的内存,使其轻量级。Docker本身并不是一项新技术,但它是一个高级工具,最初构建在LXC API之上,并提供附加功能。Docker容器由Docker引擎执行和控制,这与VM的管理程序不同。因为它不包括一个完整的客户访问的操作系统(Guest OS) Docker容器比VM更小,启动速度更快 [4]。
Docker与VM的不同之处:1) Docker容器由Docker引擎执行和控制,这与VM的管理程序不同。2) 因为它不包括一个完整的客户操作系统(Guest OS) Docker容器比VM更小,启动速度更快 [6]。见图1。
针对大规模在线实训平台的场景,我们先比较了各个虚拟化方案:1) 内核虚拟机(Kernel-based Virtual Machine,简称KVM) [7]:需要消耗的资源太大,过于重量级,不适用于快速生灭的特点。2) 容器 [8]:是轻量级虚拟化的一种形式,通过对进程制造一定的限制来完成虚拟化工作,容器化的系统都运行在主机的内核上。3) unikernels [9]:硬件级虚拟化的一种,将简约的LibOS和目标应用程序专门化为在虚拟机监控器(Virtual Machine Monitor,简称VMM)上运行的单一用途虚拟机,不适用于开放式场景。
通过各个方案比较确定使用容器作为该场景下的虚拟化方案。并且在Http代理场景下,Docker性能要优于KVM [10],符合线上运行环境要求。
Figure 1. The Container architecture versus the VM architecture
图1. Container与VM架构对比
2.1. 国外研究现状
有不少人针对虚拟化技术进行过测评,文献 [11] 在基于Linux容器的Docker和虚拟机管理程序(Hypervisor)上构建了云环境,分析了它们的大小、启动速度和CPU性能。文献 [12] 在性能和可伸缩性方面比较了Linux容器和虚拟机。文献[10]在Http代理场景中提供了两个开源虚拟化解决方案KVM和Docker的性能分析。文献 [13] 提供了在OpenStack云平台中并发地请求多个运行实例时,对KVM (虚拟机)、Docker (容器)和OSv (Unikernel)的评估。
还有很多人做了容器相关的测评工作SOCK [14] 分别对容器存储,逻辑隔离,性能隔离做了测评。SLACKER [15] 对容器镜像大小,执行需要多少数据,推、拉和运行图像需要多长时间等方面进行了测评。还有一些专注于MYSQL [16] 或者一些更复杂的网络应用程序,比如安装了Mysql的负载平衡的Wordpress [17],文献 [18] 对Docker容器的网络模式进行了评测。文献 [5] 对不同容器在HPC平台下的容器性能的评测。
2.2. 国内研究现状
国内对容器的研究也有很多,文献 [19] [20] 对Docker本身的网络架构做出了深入研究,利用多种技术和方法来对网络隔离和网络控制做出了改进。文献 [21] 将依赖库文件与可执行二进制文件单独抽取成层,实现了容器对主机内存资源的最大化共享,以较小的时间延时启动新的容器。国内大多数工作与对容器资源调度有关,提供一个有效的算法模型来提高容器的资源调度,文献 [22] 将Kubernetes结合已有Openstack云平台,提出一种基于容器的弹性调度策略,建立了一个提高集群资源利用率的优化模型,通过对云平台各个服务器节点四种类型资源的监控和应用队列预设模板匹配,选择调度资源利用率最高的服务器。还有文献 [23] 提出了一种基于三次指数平滑法和时间卷积网络的云资源预测模型,根据预测值为应用及时、准确、动态地调度和分配资源。
据我们所知极少人人对容器的并发启动进行过相关的工作,而容器并发启动在很多场景下非常有必要。许多场景下都会需要容器大规模的并发启动。
3. 通讯方式的选择
Docker deamon是Docker最核心的后台进程,他负责响应来自Docker client的请求,然后将这些请求翻译成系统调用完成容器管理操作。该进程会在后台启动一个API server,负责由Docker client发送的请求;接收到的请求将通过Docker deamon分发调度,再由具体的函数来执行请求 [24]。
要与Docker daemon通过rest API通信,有三种通信方式:1) Unix domain socket (简称Unix Socket),2) Systemd socket activation,3) TCP。其中Systemd socket activation的工作方式是让systemd守护进程代表应用程序打开监听Socket,并且仅在连接进入时才启动它。然后将套接字交给新启动的应用程序,由它负责 [25]。本质上也是Unix Socket通信。
为了研究高并发情境下哪种方式与Docker守护进程通信最佳,我们进行了多组实验。
3.1. 实验环境
本文测试环境使用服务器1台,主要是与容器通讯方式的选择和容器并发启动影响的相关因素进行研究,容器版本为Docker18.06.2-ce,服务器配置如表1。为了排除网络带宽限制等影响,我们采用直接请求本机的Docker守护进程,这样不会产生额外的消耗,使Tcp和Unix Socket结果更加真实。
3.2. 顺序执行
我们对两种通信方式进行实验,分别顺序执行200次List Images [26] 方法请求Docker deamon,时延为以向Docker demon发送请求开始到Docker deamon返回数据为止。得到的实验结果中Unix Socket通信响应时间的平均值为2499.73 µs,Tcp通信的平均响应时间为2909.11 µs,如图2。实验结果中Unix Socket与Tcp的传输时延区别不是非常明显,Unix domain socket平均通信时间比Tcp提高了大约10%,不过我们为了测量网络抖动,引用了变异系数来确定网络抖动情况,公式为:
(1)
公式(1)中x为一次请求的延时,n为请求次数。最后得出结果Unix Socket的变异系数为0.002724,Tcp的变异系数0.004679,可见Http通信方式的网络抖动比Unix Socket通信方式高了大约一倍。
因此我们可以得到结论,在顺序启动容器时与Docker demon进行通信时,Unix Socket的通信延时和网络稳定性都优于Tcp方式。
Figure 2. Unix Socket and Tcp 200 times sequential request delays
图2. Unix Socket与Tcp顺序执行200次请求时延
Figure 3. Unix Socket and Tcp 1~1000 times concurrent request delays
图3. Unix Socket与Tcp 1~1000次并发请求时延
3.3. 并发执行
我们对两种通信方式进行实验,分别并发执行1~1000次List Images [24] 方法请求Docker deamon,时延为以向docker demon发送请求开始到Docker deamon返回数据为止。我们发现,当达到一定并发量时,使用Unix Socket通信方式请求Docker deamon,Docker deamon无法正常响应。图3为Unix Socket与Tcp各并发执行1000次的时延,当Unix sock时延没有数据说明Docker deamon出现无法正常响应的情况,容器也没有启动成功,根据图中数据可以得出当前实验环境下Unix Socket能接受的最大并发请求数为80次左右,而Tcp方式不论并发请求次数多少都可以正常响应。
表2是不同并发次数的各个数据,为了降低偶然性,我们不同并发次数执行各了10次,然后将10次数据汇总起来一起统计。整体而言在并发情况下,可以看出Unix Socket通信方式响应速度更快,可以承载的并发数量低,网络抖动小。Tcp通信方式响应速度慢,可以承载的并发数量高,网络抖动大。因此我们建立了一个数学模型来确定Unix Socket和Tcp两种方式的优劣
(2)
(3)
公式(2)为计算优劣系数,其中tx为平均通信时延,tmin为通信时延最小值,tmax为通信时延最大值,cx为变异系数值,cmin为变异系数最小值,cmax为变异系数最大值。err为请求失败次数。
公式(3)为取Unix Socket和Tcp两种通信方式计算得出的优劣系数最小的值为最佳通信方式。根据我们的实验结果得出结论并发启动在一定数量之前选择Unix Socket通信方式最佳。在并发启动数量在一定数量之后选择Tcp通信方式最佳。
Table 2. Unix Socket and Tcp two kinds of communication statistics
表2. Unix Socket与Tcp两种通信统计
4. 容器并发影响因素
4.1. 容器启动过程
我们测试容器并发启动,因此容器镜像下载的时延不在我们考虑之内,所以我们提前下载好要测试的镜像,确保容器镜像传输不影响容器并发启动实验数据。
单个容器启动过程如图4,1) Client发送一个创建容器请求。2) Docker deamon接收请求进行处理后调用Linux namespace等命令进行创建容器操作。3) 容器创建成功发送创建成功的响应给Client。4) Client发送容器启动请求给Docker deamon。5) Docker deamon进行启动容器操作。6) Docker demon返回给Client端响应,同时容器进行加载库文件和启动相关服务等操作,为了避免Docker demon没有正常响应请求问题,我们统一采用Tcp方式请求Docker deamon。
测试容器启动成果条件根据容器镜像类型不同而不同,我们对容器类型进行了分类,如表3。我们利用在容器中运行最简单的任务或等待容器报告就绪来度量启动时间。作为语言类容器,任务就是的编译或者解释一个简单的“hello world”语言程序。Linux发行版的镜像就是运行一个非常简单的shell命令,通常是“echo hello”。对于长时间运行的服务器(尤其是数据库和web服务器),我们测量到容器输出“ready”的信息的时间。对于一些特别的服务器,将轮询公开端口,直到有响应为止。
Table 3. Container image classification
表3. 容器镜像分类
4.2. 容器并发启动硬件因素分析
首先我们对容器并发启动时延进行了测试,启动的镜像为ubuntu。结果如图5,可知容器的平均启动时延与并发启动的次数成正比,平均启动时延随着并发启动次数的增加而增大。
我们对启动过程影响因素进行了汇总,主要通过容器并发启动时CPU使用率,内存使用情况和磁盘IO值来确定。磁盘IO值使用磁盘工作时间占用总时间的百分比。公式为:
(4)
公式(4)中为Δio写操作消耗的时间,Δt为采样周期。为了研究并发启动时何种因素对容器启动速度影响最大。我们采集了并发启动数量从1次到10次的数据,每次测试10次取中值。
Figure 5. Containers 1~1000 concurrent startup delays
图5. 容器1~1000次并发启动时延
Figure 6. The container 1~10 times concurrently startup CPU usage
图6. 容器并发启动1~10次CPU使用率
图6为容器并发启动1~10次CPU使用率,可以看到CPU成线性增长,到最后基本达到饱和状态。图7为容器并发启动的详细时延,可以得到启动时延根据并发启动的数量增加而增加。图8为容器并发启动的内存使用情况,可以看到内存几乎稳定在3000 M到4000 M之间,变化不明显。图9为容器并发启动磁盘IO值,可以看到磁盘IO值为0.10%~0.15%之间,可以认为基本不使用磁盘IO。
综上可得,影响容器并发启动时延的主要因素为CPU使用率,如果要提高并发性能,可以考虑优化CPU使用率。
Figure 7. The container 1~10 times concurrently startup details
图7. 容器并发启动1~10次详细时延
Figure 8. The container 1~10 times concurrently startup memory usage
图8. 容器并发启动1~10次内存使用
Figure 9. The container 1~10 times concurrently startup disk IO
图9. 容器并发启动1~10次磁盘IO
4.3. 容器并发启动镜像因素
为了确定容器并发启动速度是否和镜像大小相关,我们分别并发启动了1000个镜像大小为78.43 M的ubuntu镜像和1000个镜像大小为908 MB的Node镜像。镜像为Docker hub [27] 官方镜像,并且对它们的启动时延进行对比。
结果如图10,发现Ubutu镜像和Node镜像启动时延相差不明显,得出结论镜像大小不是影响并发启动的主要因素。
Figure 10. Comparison of concurrent startup delays between different image sizes
图10. 不同镜像大小并发启动时延比较
4.4. 预先创建容器
根据docker容器启动步骤,分别为容器创建过程和容器启动过程 [1]。因此我们确定容器预先创建好之后是否能提高容器并发启动效果。结果如图11。发现预先启动容器镜像再并发启动时延比正常容器并发启动时延提升了10%,效果不是很明显。所以得出结论预先创建容器对容器并发启动效果提升不明显,容器并发启动最主要的性能损耗在容器启动阶段,不是在容器创建阶段。
Figure 11. Concurrent startup of the container image after pre-creation
图11. 预先创建容器镜像后并发启动效果对比
5. 未来展望
针对容器并发启动研究,我们可以根据得出的结论来针对这一特定场景进行专门的优化,来提高容器并发启动性能。可以采用CPU分时计算的思想来提高CPU利用率,根据时间序列预测的方法提前创建容器等操作,也可以进一步细化容器并发启动研究,针对容器启动过程的各个阶段来进行实验得出结果,并且容器并发启动应用环境不只有实训平台下的应用场景,有可能在高性能计算等领域也会出现相关场景,可以根据特定场景的特性来进行针对性研究。
基金项目
国家自然科学基金(61672384, 61772372)。