主要结论
- 像对待软件代码那样对待你的基础结构配置。
- 配置代码需要避免出现“味道”,以及味道的不同类型。
- 需要注意不同粒度环境(例如实现和设计)中配置可能出现的“味道”。
- 可以在 Puppet 配置中使用 Puppet-lint 和 Puppeteer 等工具发现配置设计和实现过程中的味道。
- 配置的味道会对项目质量产生极大影响。
基础结构即代码(IaC)是一种通过代码指定计算系统配置,实现自动化系统部署,并通过传统软件工程方法管理系统配置的实践。例如,某个包含大量节点的服务器场可能使用了不同硬件配置和不同的软件包要求,此时即可使用诸如 Puppet 、 Chef 、 CFEngine ,或 Ansible 等配置管理语言进行定义,无需人工介入自动进行部署。
IaC 范式将基础结构、代码,以及用于管理基础结构的工具和服务以一种软件系统来看待。因此 IaC 实践会将配置代码视作与生产代码类似的产物,并对其应用传统的软件工程实践,例如针对配置代码进行的审查、测试、版本控制。
在传统软件工程方法中,为了编写可维护代码并实现较高的设计质量,通常需要做很多工作。与生产代码类似,如果对配置代码所做的改动不够用心或不妥善,配置代码很快也会变得不可维护。
本文中我们定义了配置的味道,并对不同“味道”进行了分类。
配置的味道
我们对“配置的味道”定义如下:
配置的味道是指不符合最佳实践要求,并有可能对程序质量产生不利影响的配置程序或脚本所具备的特征。
在传统的软件工程实践中,取决于抽象的颗粒性,以及产生的位置和影响,糟糕的味道可分为实现(或代码)的味道以及设计的味道。同理,配置的味道也可以分类为实现配置的味道,设计配置的味道,记录配置的味道等。本文将主要介绍实现配置的味道和设计配置的味道这两个主要类别。
实现配置的味道
实现配置的味道实际上是一种有关质量的问题,例如配置代码中的命名约定、风格、格式、缺陷。下文我们列出了各种实现配置时的味道和简要的介绍。
- 缺少预设条件(Default Case):在 case 或 selector 陈述中缺乏预设条件。
- 命名约定不一致:所用命名约定与推荐的命名约定不符。
- 复杂表达式:程序包含难以理解的复杂表达式。
- 重复实体:配置代码中存在重复的哈希键或重复的参数。
- 属性错位:资源或类中的属性没有使用建议的顺序(例如强制属性应先于可选属性指定)。
- 不合理的对齐:代码对齐不合理(例如在资源声明中使用箭头)或使用了制表符。
- 无效的特性值:特性或属性使用了无效的值(例如文件模式规定使用 3 位而非 4 位八进制值)。
- 任务不完整:代码中使用 FIXME 和 TODO 标记代表任务不完整。
- 使用不建议的语句:配置代码使用某些不建议使用的语句(例如“import”)。
- 使用不恰当的引号:错误使用单引号和双引号。例如布尔值无需使用引号,变量名不能用于单引号字符串。
- 长语句:代码包含(通常无法在一屏完整显示)的长语句。
- 条件不完整:使用“if…elsif”构造时不使用“else”子句作为结尾。
- 疏忽使用的变量:插入字符串的变量没有包含在大括号内。
一起用几段 Puppet 代码看看上述提到的实现配置的味道范例。
if $version == ‘4.4’ or $version == ‘4.2’ or $version != ‘4.5’ or $version == ‘4.9’ or $version == ‘5.0’{ case $::operatingsystem { 'debian': { apt::source { 'packages.dotdeb.org-repo.app': location => 'http://repo.app.com/dotdeb/', release => $::lsbdistcodename, repos => 'all', include_src => true } } } } elsif $version in ['33', '3.3'] { }
上述代码片段中包含下列“味道”:
- 第 1 行语句包含四个逻辑运算符,使其成为一条复杂表达式,这条语句能闻到“长语句”的味道。
- 第 2 行语句是一个不包含预设条件的 Case 语句,可以闻到缺少预设条件的味道。
- 第 8 行语句没有正确对齐,可以闻到不合理对齐的味道。
- 第 13 行语句有“elsif”语句但没有“else”,可以闻到条件不完整这一实现配置的味道。
上述代码片段可以重构为下列内容:
if $version in ['44', '4.2', '4.9', '5.0'] or $version != '4.5'{ case $::operatingsystem { 'debian': { apt::source { 'packages.dotdeb.org-repo.app': location => 'http://repo.app.com/dotdeb/', release => $::lsbdistcodename, repos => 'all', include_src => true } } default:{} } } elsif $version in ['33', '3.3'] { } else { }
设计配置的味道
设计配置的味道意味着模块的设计或配置项目的结构存在质量问题。
1.多方面抽象(Multifaceted Abstraction):每个抽象(例如每个资源、类、‘定义’或模块)需要设计为仅代表软件某一方面的特性。换句话说,每个抽象都应遵守单一职责这个原则。如果抽象的元素不具备内聚性,多方面抽象可能会遇到问题。
这个味道可能会以下列两种形式出现:
- 某个资源(文件、程序包,或服务)所声明的特定属性包含超过一个物理资源,或
- 一个类、‘定义’,或模块中声明的所有语言元素不具备内聚性。
2.不必要的属性:一个类、‘定义’,或模块必须包含指定所需系统特性的声明或语句。从空的类、‘定义’,或模块中可以闻到不必要属性这一味道,需要将其移除。
3.命令式抽象:Puppet 的本质是陈述式的。命令式语句(例如“exec”)的出现违背了语言的本意。包含大量命令式语句的抽象可以闻到命令式抽象的味道。
4.缺少抽象:如果封装在诸如类或‘定义’等抽象中,资源声明和语句的使用和重用会变得很容易。如果资源和语言元素的声明和使用没有将其封装到抽象中,模块就能闻到缺少抽象的味道。
5.模块化程度不足:如果抽象规模足够大或足够复杂,并可进一步进行模块化,此时该抽象将能闻到这个味道。这个味道体现为下列集中形式:
- 如果一个文件包含超过一个类或‘定义’的声明,或
- 如果类声明的大小超过某一阈值,或
- 类或‘定义’的复杂度足够高。
6.重复块:重复语句块的数量如果超出某一阈值,意味着可能缺少适当的抽象定义。此时包含这种重复块的模块就能闻到重复块的味道。
7.层次结构不完整:继承的使用必须限制在同一个模块中。如果跨越命名空间进行继承(不遵守“is-a”关系)就能闻到这个味道。
8.非结构化模块:一个配置代码库中的每个模块必须具备妥善定义且一致的模块结构。推荐使用的模块结构如下:
- manifests
- files
- templates
- lib
- facts.d
- examples
- spec
Ad-hoc 结构的代码库如果存在非结构化模块这一问题会影响代码库的可理解性和可预测性。
9.致密结构:如果配置代码的代码库存在过度密集的依赖性且不具备任何特定结构就能闻到这个味道。
10.缺乏封装:如果节点定义或 ENC(外部节点分类器)声明了一系列供定义中包含的类使用的全局变量,就能闻到这个味道。
11.削弱模块化:每个模块必须尽量做到高内聚低耦合,如果模块高耦合低内聚就能闻到这个味道。
一起用几个例子看看上述提到的设计配置的味道。
范例 1:请考虑下列代码片段(Puppet)。
#Contents of file package.pp class package::web { … } class package::mail { … } class package::environment { … } class package::user { … }
上述片段显示了一个包含四个类定义的文件。这种一个文件包含多个定义的做法违反了单一职责这个原则,并导致产生多方面抽象的味道。
将每个类定义放在独立文件中,即可重构上述代码片段。
范例 2:请考虑下列 Puppet 代码片段。
class web { exec { ‘hadoop-yarn’: … } exec { ‘apache-util-set’: … } exec { ‘smail-invoke’: … } exec { ‘postfix-set’: … } }
Puppet 是一种声明性语言,因此我们需要指定自己到底要实现“什么”,而非指定“如何”实现。然而肆意使用“exec”语句使其变为命令式代码,这样做违背了语言的用途。这样的代码可以闻到命令式抽象这一设计配置的味道。
使用 Puppet 的语言结构重新指定“what”部分并取代“exec”即可重构上述代码。
范例 3:假设一个包含配置代码的代码库中(代码库内不同模块之间的)依赖图形成了一种致密复杂的结构。由于致密、复杂的结构所导致的脆弱性,这样的代码库很容易出现大量 Bug。在我们的分类中,我们认为这样的代码库可以闻到致密结构的设计配置味道。
对于 Puppet 代码库,需要尽量确保每个模块都只有很少量的依赖(完全没有最好),大部分情况下可通过独立模块消除这种味道。
“味道”的影响
有人可能会说,上述味道本质上其实非常简单,它们对配置项目或人员的影响微乎其微。
不同味道会对配置项目产生不同影响。实际上少数一两个味道可能没什么大不了的,但如果某个项目充满各种不同味道,组合在一起可能会对可维护性产生极大的影响。这个问题其实类似于压倒骆驼的最后一根稻草。很难相信一根稻草就能压倒骆驼,然而如果骆驼背上已经放了很多稻草,骆驼的主人决定继续一根一根增加稻草时,可能随后增加的某根稻草就会把骆驼彻底压倒。每种味道就如同一根稻草,考虑到其他味道的存在,最终可能产生严重的后果。
检查配置味道的工具
为了检测是否存在上文提到的设计配置味道,我们开发了一个名为 Puppeteer 的工具,这是一款使用 Python 编写的开源工具。若要检测实现配置的味道,则可使用 Puppet-Lint。
我们还进行了一个详细的研究,共分析了 4621 个包含 Puppet 代码的开源代码库,希望从中找出与配置味道有关的信息。详细结果请参阅这篇论文,本文已提交至2016 年度Mining Software Repositories 大会。
关于本文的作者
Tushar Sharma目前是雅典经济与商业大学的研究员。之前他曾在位于印度班加罗尔的西门子研究和技术中心就职七年。他的职业生涯主要从事与软件设计、重构、代码和设计质量、技术债务、变革影响分析,以及基础结构即代码(IaC)有关的工作。他与人合作撰写了包括《重构软件设计的味道:管理技术债》在内的三本书。他是 IEEE 资深成员。
Marios Fragkoulis目前正在雅典经济与商业大学管理科学与技术系 Diomidis Spinellis 教授领导下攻读数据呈现、查询和管理领域的博士学位。他曾在伦敦帝国学院计算机部考取理科硕士学位,并在雅典经济与商业大学管理科学与技术系以优异成绩取得理学士学位。2015 年他曾兼职在 OTE S.A. 担任基础结构运营工程师,并在希腊 GRNET S.A. 担任软件工程师。他还开发了 PiCO QL 这一在线数据分析系统。
Diomidis Spinellis是希腊雅典经济与商业大学管理科学与技术系教授。他撰写过两本获奖图书《月度代码》和《代码质量:从开源的角度解读》,并已翻译为多种语言。在你读到本文的同时,他的新书《有效的调试:66 种特殊的软件和系统调试方法》应该已经正式发布。他持有伦敦帝国学院软件工程硕士和计算机科学博士学位。Diomidis 曾担任 IEEE 计算机学会理事会选任委员(2013–2015 年),同时他也是 ACM 和 IEEE 的资深成员。2015 年 1 月起他开始担任 IEEE Software 主编。
评论