2. 南京航空航天大学 高安全系统的软件开发与验证技术工信部重点实验室, 南京 211106
2. Key Laboratory of the Ministry of Industry and Information Technology for Software Development and Verification Technology of High Security Systems, Nanjing University of Aeronautics and Astronautics, Nanjing 211106, China
云原生环境依靠容器镜像提供可部署的应用程序. 由于相应的执行环境封装在容器镜像中, 因此用户可以在目标平台上直接运行应用程序, 而不需要考虑配置差异. 构建容器镜像的指令按照一套语法规则在 Dockerfile中按顺序指定. 因此Dockerfile的质量对构建的镜像至关重要. 然而最近对大型开源项目的实证研究暴露了对现有Dockerfile的质量与其功能或性能的严重担忧, 研究显示, 大型开源项目中的一些Dockerfile甚至是错误且无法构建的[1, 2].
Dockerfile可以视作与其他源代码级别的软件制品一样, 需要按照基本原则、规则或其他设计模式仔细设计. 现有的几种工具为Dockerfile的检查提供了一些初级的支持, 在语法级别(例如突出显示关键字、悬停语法等)做出提示[3]. 事实上, Docker的官方网站提供了编写Dockerfile的最佳实践指南[4]. 但是这份指南的内容是一种抽象层次的指南, 只是提出了一些原则, 缺少具体的示例和规则. 同时最重要的是. 指南更关注特定于Dockerfile的指令, 但在Dockerfile中, 最常用的是 Shell脚本(即由Run命令引导的命令), 通常占所有指令的40%以上(一些研究甚至显示, 高达68.3%的 Dockerfile 更改集中在Shell命令上), 大约90%的存储库有使用Shell命令[4].
图1展示了一个例子. 通常, 我们需要从网络下载压缩文件, 然后再执行解压缩命令. 在图1中, wget命令用来下载远程网站中的压缩文件, 然后使用unzip命令来解压缩下载的文件. 执行这样的命令将保留原始的压缩文件, 并创建一个新的文件夹来放置解压缩后的文件, 虽然原始的压缩文件已经不再需要, 但是这种情况下, 原始的压缩文件将仍然保留在最后生成的容器镜像中. 诸如此类的行为, 将不必要的文件留存在容器镜像, 会导致更长的构建时间和更大的容器镜像体积. 因此, 如图2所示, 在使用unzip之后应使用额外的rm命令删除原始的下载文件.
这个例子说明了在编写Dockerfile时应该遵守的一些隐含的规则. 但实际上, 这些规则在很大程度上并没有包括在官方的最佳实践指导指南中. 违反这样的规则并不一定会导致构建失败, 但是会对非功能属性产生负面影响(增加了产生的镜像的体积), 类似于程序中的“代码气味”概念[5, 6].
已经有一些工作和工具来解决这一方面的问题, 两个有代表性的最新工具包括Hadolint[7]和Binnacle[1], 它们试图识别Dockerfile中的此类规则, 从而来检查人们在编写Dockerfile时的错误. 然而, 它们都受到各种限制, 如人为干预较多, 效率较低等.
为了提高Dockerfile的质量, 减少Dockerfile 中诸如此类“代码气味”的出现, 本文提出了一个新颖的Dockerfile规则提取工具DMiner来半自动化的提取Dockerfile 中有助于提升代码质量的潜在规则.
DMiner能够在大量Dockerfile文件中, 通过序列挖掘算法挖掘并提取Dockerfile中的隐含规则. 以大量高质量的Dockerfile作为输入, 将Dockerfile处理成统一的序列化中间表示. 引入PrefixSpan[7]序列模式挖掘算法, 设计多种启发式的规则对规则进行约简, 最后在极少人工参与的条件下自动化地得到Dockerfile中的隐含规则.
本文着重介绍了Dockerfile的规则提取和检测方法(DRIVE)中对Dockerfile进行规则挖掘和提取的工具DMiner的实现, 更多和算法和理论有关的详细信息可以参考DRIVE[8]的原始论文.
为了评估该工具的有效性, 本文选择了在Dockerfile规则挖掘领域最新的工具Binnacle作为在挖掘效果和挖掘效率上的对比, 实验表明, DMiner不仅可以更高效的挖掘出规则. 而且可以挖掘出更多的隐含规则.
1 工具的设计与实现DMiner是一个命令行程序, 其主要架构如图3所示, 主要包含3大模块, 数据过滤模块、数据解析模块、规则挖掘模块.
数据过滤模块主要涉及Dockerfile的收集与过滤, 通过自动化地采集、过滤得到高质量的Dockerfile原始数据, 作为后续模块的工作基础.
数据解析模块的主要工作包括Dockerfile的解析和依照配置文件进行的表示替换, 并转换为序列化的中间表示其输入可以是数据模块过滤得到的Dockerfile集合, 也可以是由其他工作得到的Dockerfile集合.
规则挖掘模块挖掘数据解析模块解析输出的Dockerfile的中间表示, 采用启发式地规则裁剪, 相关的规则也是由配置文件驱动的. 最后在裁剪的结果上, 通过一定的人工总结得到最后的Dockerfile规则.
DMiner也根据如上的3个模块, 由3个可以独立工作的子命令组成, 接受不同的参数和配置文件. 分别是DMiner spider、DMiner parser和DMiner miner.
其中DMiner spider可以作为一个通用的Dockerfile采集器存在, 根据设定的参数不同, 可以从GitHub自动采集不同需求的Dockerfile.
DMiner parser的输出将默认作为 DMiner miner的输入, 但是为了避免过强的耦合也为了工具的可复用性, DMiner miner 可以独立的工作, 其输入序列可以是任意的符合DMiner定义的输入.
1.1 数据采集过滤模块DMiner spider即数据采集与过滤模块主要由两部分组成, 别是采集器和过滤器. 运行时如图4所示.
采集器: 是一个使用Python编写的一个分布式爬虫, 利用GitHub官方提供的API[9]从GitHub查询包含Dockerfile的所有代码存储库. 该采集器可以根据设定的条件, 自动化地从GitHub上采集对应的数据, 在我们的实验中我们选择了截至2022年5月的所有代码仓库中的Dockerfile.
过滤器: 由采集器收集到的Dockerfile将自动地送到过滤器, 由于收集到的Dockfile数量巨大(大概40万份原始的Dockerfile), 其质量也参差不齐, 因此需要设计一个过滤器对这些Dockerfile进行过滤. 过滤器负责对采集到的Dockerfile进行过滤. 该过滤器是可以配置的, 可以根据Dockerfile使用的语言、所属仓库的知名度(star数量)、作者、提交日期等进行过滤. 同时过滤器也集成了Dockerfile的语法检查工具, 可以对收集到的Dockerfile进行基本的语法正确性检查.
采集器和过滤器都有一些默认配置, 这些默认配置都是可以修改定制的, 其中的默认配置如下.
(1)选择超过一定星星数量(GitHub的社区评价机制, 被认为质量较高)的仓库所属的Dockerfile作为第一个过滤标准. 在我们的工具中, 星星数量的默认配置为1000.
(2) 对Dockerfile的行数进行检查. 过于简短的Dockerfile通常只包含最基本的Dockerfile语句, 对Dockerfile的潜在规则提取没有帮助. 因此在这一步将过滤掉那些过于简短的Dockerfile, 在我们的工具中, 这一阈值的默认配置为4, 即少于4行的Dockerfile将会被移除.
(3) 利用Dockerfile的语法检查器来对这些Dockerfile进行进一步过滤, 删去那些有语法错误(sytax error)的Dockerfile, 并对语法检查器警告(waring)的数量进行限制, 如果一份Dockerfile出现过多的waring, 我们的工具也会将其移除. 在我们的工具中, waring的数量上限默认值为2.
可以看出, 第1个模块主要负责Dockerfile的采集和过滤, 为之后Dockerfile规则挖掘做准备. 我们使用数据过滤采集模块收集了一个规模更大也更新的高质量Dockerfile数据集, 从2022年5月之前GitHub上开源仓库中所有的约40万份Dockerfile中筛选得到了1761份高质量的Dockerfile文件并开源[8], 以供本文的实验以及未来的其他工作使用.
1.2 数据解析模块DMiner parser即数据解析模块主要由3个组件构成, 与原论文中提到的3步解析相对应. 其一是实现对Dockerfile自身的指令解析的模块, 其二是对Dockerfile中的Shell脚本进行解析的Shell解析模块, 其三是根据预定义的配置对一些变量进行替换的模块.
使用DMiner parser解析一份Dockerfile的示例如图5所示. DMiner的行为由命令行参数指定, 在这里parser格式化输出并高亮了原始的Dockerfile输入文件以及最后得到的序列化的中间表示. 图5中的格式化输出是为了提高可读性, 实际上最终进行挖掘的序列仅仅使用空格分隔, 且一条序列即代表一份Dockerfile.
Dockerfile指令解析模块将根据Dockerfile的语法说明解析Dockerfile, 解析特定的一些指令如FROM, COPY等, 得到Dockerfile的抽象语法树, 在这一步中, 如果RUN命令所引导的Shell脚本以附加的脚本文件的形式存在, 会将脚本文件读取替换作为RUN命令的参数, 但是这一步RUN命令中的Shell脚本仅作为参数字符串保留, 待后续步骤再进行解析, 遍历抽象语法的树的每个节点可以得到Dockerfile的基本序列表示, 这意味着, 除了Dockerfile中的Shell脚本没有解析以外, 其他所有的命令都将被完整地解析.
Shell解析模块将解析Dockerfile中所有没有完成解析的Shell脚本, 这一模块输出的最终结果是完成全部解析并转换成基本的序列表示的Dockerfile. 虽然这里的解析器是两个独立的模块, 但是在实际的工具实现中, 这两个模块的功能是耦合在一起的. 将Dockerfile作为输入, 将直接输出完全解析完成之后的序列表示.
替换模块将根据配置文件中预定义的启发式规则, 自动地检查解析器输出的序列表示, 替换其中的一些文件名、URL等变量. 这一配置文件中的具体规则可以参考DRIVE论文[8].
通过如此的解析与替换, 得到了便于后续挖掘应用序列挖掘算法的通用中间表示, 这一中间表示的既保留了Dockerfile文件的语义信息, 同时又尽可能地提升了利用序列挖掘算法的效果.
1.3 规则挖掘模块DMiner即规则挖掘模块主要由一个分布式并行的PrefixSpan频繁序列挖掘器和一个可定制的序列裁剪器组成.
经过第1.2节中解析模块处理的Dockerfile数据集被转换成了一种序列化的中间表示. miner会根据文献[9]提到的方法, 自动化的数据集进行分组, 拆分成若干个小数据集. 即将使用了同一个Shell命令的若干个Dockerfile的将被划分到同一个数据集. 这里的数据集之间并不互斥, 即一份Dockerfile会出现在其所有使用到的Shell命令所代表的小数据集中.
得益于之前的Dockerfile的解析工作, 我们可以非常方便地从序列化的中间表示中提取出Dockerifle中使用到的Shell命令. 分析图5可知Dockerfile中的Shell脚本中的所有出现的命令都被解析成了“SC-[命令名称]”的形式, 这里的SC是Shell Command的缩写. 那么提取序列中的这一项即可得到Dockerfile中所使用到的Shell命令.
这样的分析可以得到所有使用到的Shell命令, 在这些命令的基础上, miner会对Shell命令出现的频率进行检查, 这里的频率指的是该命令在多少份Dockerfile中出现过. 即使一份Dockerfile多次使用同一Shell命令, 对该Shell命令的频率计数也只加一.
对于那些使用频率过低的命令, miner将不会创造该命令的分组. 而其他命令, miner将会把所有使用到该命令的Dockerfile放到同一个分组. 在实现中就是一个文本文件代表一个分组, 文件的每一行代表着一份Dockerfile的序列表示. 那么显然, 文件的行数就代表这该分组的数据集的大小.
为了充分利用我们分组的特性, 我们用Go语言实现了一个修改地分布式并行的PrefixSpan频繁序列挖掘器. 该挖掘器可以自动地根据所使用的机器的CPU核数对每个分组数据集进行并行化地挖掘. 该挖掘器的输入包括输入文件(一行代表一个序列)以及一个子序列的最小支持度(百分数值). 而挖掘器的输出即该输入文件对应支持度的所有频繁子序列.
在得到了频繁子序列之后, DMiner需要对输出的频繁子序列进行筛选过滤. 因此DMiner中同时包含了一个可定制的序列过滤器, 默认将使用DRIVE论文提出的算法[8]对输出的频繁子序列进行过滤, 之后人工地对过滤后的频繁子序列进行分析. 得到由该子序列所代表的Dockerfile潜在规则. 所以DMiner也提供了一个简单的Web UI来帮助专家对于每个命令分组的数据集的输出结果进行分析总结. 该部分的展示如图6所示. 图6展示了最终在pip这个命令分组中的第3个频繁子序列, 从这个子序列中我们可以得到一条Dockerfile中的潜在规则: pip命令应该使用requirements.txt文件中来安装指定的依赖.
2 实验评估
在这一部分中, 我们进行实验来评估我们的工具, 为此我们提出了两个研究问题(RQ).
RQ1: 我们的规则挖掘工具DMiner的效果?
RQ2: 我们的规则挖掘工具DMiner的效率?
所有实验是均在同一台服务器上完成, 该服务器配备了Intel Xeon 2.3 GHz 32核CPU和32 GB RAM, 运行Arch Linux. 程序的原型是用Go v1.18 和Python v3.10.4实现的. 最新的和Dockerfile规则相关的工具包括Binnacle[1]和Hadolint[10]. Binnacle对Dockerfile进行分层解析, 并使用挖掘频繁子树的方法来对Dockerfile中的规则进行提取. Hadolint 是一个Dockerfile的规则检测工具, 其中的Dockerfile规则都来自于社区贡献, 并不能主动地挖掘并提取规则. 因此本文实验中比较的基线是主要是目前最新的工具Binnacle[1]. 为了保证实验的效果, 我们的实验中主要使用了两个数据集.
D1: 我们的数据过滤模块筛选出的高质量Dockerfile集合, 它包含1761份Dockerfile.
D2: Binnacle工具采用的高质量Dockerfile数据集[11], 是目前已知最新的公开数据集, 包含405份Dockerfile.
DMiner在挖掘时, 只需要接受两个参数, 一个是需要设置频繁子序列的最低支持度, 其二是待挖掘的Dockerfile集合. 根据序列挖掘算法的特性[12]和实验验证, 当支持度设置得过高时, 挖掘出的频繁子序列的数量会变少, 在进行Dockerfile规则筛选过滤时, 丢失了较低支持度时能挖掘出的规则. 而支持度过低的频繁子序列并不能反映出潜在的Dockerfile规则. 因此, 经过我们的多次实验, 选择将最低支持度设置为40%.
2.1 RQ1: 挖掘方法的效果如前所述, D1是我们的方法收集的高质量Dockerfile数据集. 在规则挖掘阶段, 我们根据命令对解析的Dockerfile进行分组, 在D1数据集上得到了77个命令组. 然后从每组数据中挖掘出频繁模式. 每组在经过序列过滤器之前的输出的频繁模式的平均个数为4515. 在通过序列过滤之后, 每组的平均模式只剩下4个, 减少了99.9%.
然后, 我们人工检查每组输出的模式. 由于我们的方法设置了支持度的阈值, 我们验证了这些规则的置信度[12]. 通过检查这些挖掘出的规则, 其中有25个与之前的相关工作发现的规则[1]相一致, 我们还发现9条在以前的相关工作中没有被识别到(在Binnacle或Hadolint中提及)的新规则, 意味着这几条规则是我们的工具所新发现的, 这也证明了我们的方法的有效性, 新发现的规则在表1中列出.
由于DMiner和Binnacle使用了不同的数据集来挖掘规则, 为了进行公平的比较, 我们还在相同的数据集上进行的对比. 即D1和D2. 比较结果如表2所示.
可以发现, 在D2这个较小的数据集上, 两者挖掘出的规则数量没有太大差异. 而当数据集变大, DMiner在挖掘这些隐含规则方面表现出优势. 这是因为Binnacle使用频繁子树挖掘的算法, 只能挖掘预定义的局部子树[13, 14], 当数据集扩大, 预定义的子树没有扩大, 导致效果变化不明显. 而我们的方法在替换变量后, 既保留了文本的语义, 又可以挖掘命令之间的关系, 从而可以更从容地应对更加巨大的数据集. 结果表明, 我们的方法能更有效地识别Dockerfile中的隐含语义规则.
2.2 RQ2: 挖掘方法的效率
在这个RQ中, 我们主要考虑方法的性能, 主要包括运行时间和运行时所消耗的内存. 作为比较, 我们在上面提到的两个数据集上运行Binnacle和DMiner, 并分别在解析和规则挖掘阶段收集它们的运行时间和内存占用, 运行时间和运行内存占用的对比分别如表3和表4所示.
可以看出, 在数据预处理部分和规则挖掘部分, DMiner的运行效率都高于Binnacle. 在数据预处理中, DMiner可以高效地将Dockerfile转换为序列形式. 而在规则挖掘部分, 一方面因为我们选择的序列挖掘算法在速度上具有优势, 而且我们设计的挖掘工具在每个命令分组之间的挖掘工作相互独立, 并行运行. 因此, 可以显著加快工具的运行时间.
而在内存的使用量方面, DMiner有着更少的内存占用. 当数据集规模不大时, DMiner和Binnacle占用的内存相近, 但随着数据集规模增长, 由于我们的算法对Dockerfile进行了分组, 在分组挖掘时不会使得占用的内存增大太多, 而Binnacle采用基于频繁子树的挖掘方法[14], 需要将所有的Dockerfile解析的结果缓存在内存中, 从而导致占用的内存增加.
通过以上分析, 我们可以得出结论, 我们的方法能够非常高效地提取Dockerfile规则.
3 结论与展望本文基于序列模式挖掘技术, 提出了一种从高质量Dockerfile中高效挖掘隐含规则的新工具DMiner. 我们证明了我们的工具相对于最先进的基线的有效性. DMiner可以用更少的时间, 更高的效率发现更多有用的隐含规则.
在未来, 我们计划用更多的功能来增强DMiner, 比如对Dockerfile是否符合规则的检测, 并开发成熟的工具(作为主流IDE的插件)来实时地检测用户编写的Dockerfile, 以提供更好的可用性.同时这种数据驱动的工具也可以扩展到其他密切相关的领域, 如其他配置文件模式地挖掘和代码气味地检测等.
[1] |
Henkel J, Bird C, Lahiri SK, et al. Learning from, understanding, and supporting DevOps artifacts for Docker. Proceedings of the 42nd IEEE/ACM International Conference on Software Engineering. Seoul: IEEE, 2020. 38–49.
|
[2] |
Henkel J, Silva D, Teixeira L, et al. Shipwright: A human-in-the-loop system for Dockerfile repair. Proceedings of the 43rd IEEE/ACM International Conference on Software Engineering: Companion Proceedings. Madrid: IEEE, 2021. 198–199.
|
[3] |
Visual Studio Code. Docker in visual studio code. https://code.visualstudio.com/docs/containers/overview. [2022-10-06].
|
[4] |
Cito J, Schermann G, Wittern JE, et al. An empirical analysis of the Docker container ecosystem on GitHub. Proceedings of the 14th IEEE/ACM International Conference on Mining Software Repositories (MSR). Buenos Aires: IEEE, 2017. 323–333.
|
[5] |
Rasool G, Arshad Z. A review of code smell mining techniques. Journal of Software: Evolution and Process, 2015, 27(11): 867-895. DOI:10.1002/smr.1737 |
[6] |
Wu YW, Zhang Y, Wang T, et al. Characterizing the occurrence of Dockerfile smells in open-source software: An empirical study. IEEE Access, 2020, 8: 34127-34139. DOI:10.1109/ACCESS.2020.2973750 |
[7] |
Pei J, Han JW, Mortazavi-Asl B, et al. PrefixSpan: Mining sequential patterns efficiently by prefix-projected pattern growth. Proceedings of the 17th International Conference on Data Engineering. Heidelberg: IEEE, 2001. 215–224.
|
[8] |
Zhou Y, Zhan WL, Li Z, et al. DRIVE: Dockerfile rule mining and violation detection. arXiv:2212.05648, 2022.
|
[9] |
GitHub REST API. GitHub docs. https://ghdocs-prod.azurewebsites.net/en/rest. [2022-10-06].
|
[10] |
hadolint/hadolint. Dockerfile linter, validate inline bash, written in Haskell. https://github.com/hadolint/hadolint. [2022-10-06].
|
[11] |
Henkel J, Bird C, Lahiri SK, et al. A dataset of Dockerfiles. Proceedings of the 17th International Conference on Mining Software Repositories. Seoul: ACM, 2020. 528–532.
|
[12] |
Fournier-Viger P, Lin JCW, Kiran RU, et al. A survey of sequential pattern mining. Data Science and Pattern Recognition, 2017, 1(1): 54-77. |
[13] |
Jiménez A, Berzal F, Cubero JC. Frequent tree pattern mining: A survey. Intelligent Data Analysis, 2010, 14(6): 603-622. DOI:10.3233/IDA-2010-0443 |
[14] |
Mazuran M, Quintarelli E, Tanca L. Mining tree-based association rules from XML documents. Proceedings of the 17th Italian Symposium on Advanced Database Systems. Camogli, 2009. 109–116.
|