百度资深敏捷教练:深度解析持续交付之全面配置管理

张乐 2016-10-28 10:27:03

 

作者介绍

张乐百度资深敏捷教练、架构师,Intelligent Software Development团队成员。超过13年项目管理和敏捷实战经验,曾任职于惠普、埃森哲等大型外企,负责大规模团队敏捷转型和工程效率提升,积累了丰厚的知识体系和实战案例。加入百度后,作为公司内先进软件工程方法和生产力的践行者、布道者,主导了百度云、百度金融、云安全等新技术产品的敏捷转型和DevOps实施,帮助团队从业务、过程、技术、组织和工具等多个层面建立起全面的持续交付能力。Gdevops全球敏捷运维峰会、TiD中国质量竞争力大会等演讲嘉宾。

 

一、持续交付与配置管理

 

持续交付和DevOps是当前敏捷开发和运维圈讨论最多和最热门话题之一,虽然距离这些概念的提出已经有段时间了,但是国内很多企业仍处于对相关实践如何落地和实施的探索阶段。

 

在今年举办的一些技术大会上,我分享过关于持续交付方法与实践的话题,从互联网时代对软件研发和运维的挑战出发,分析了行业普遍面临的问题和痛点,并提出了持续交付体系化的实施模型。但限于分享时间的限制,很多内容无法充分开展详细介绍。今天这篇文章,我们就来详细谈谈持续交付中非常关键的一个实践,只有这个实践做到位了才能确保持续交付的实施效果,同时它也是后续一系列其它实践的基础,这就是『全面配置管理』。

 

在正式开始之前,我们先来快速回顾下持续交付的实施模型。

 

 

上图是我整理的持续交付体系实施模型,我在公司内部都是用这个模型来做推广和实施的。这个模型从业务、过程、技术、组织四个大的方向入手,对实现持续交付的关键要素及其内部逻辑关系进行整理。实现思路是通过在业务层面形成需求分解和迭代机制,在过程层面实现可靠、可视化、可管控的全自动化交付流水线和一系列关键实践,在技术层面提供基础架构和应用架构的支撑,在组织层面强调跨职能团队和数据驱动改进机制,从而全面地建立起快速、低风险、持续的向用户交付价值的能力。

 

持续交付是对整个软件交付模式的变革,涉及到的内容非常多、非常广,在这个模型中大概有二十多个关键点。今天我们就聚焦一下,仅仅来谈模型中一个关键实践『配置管理』。

 

二、全面配置管理的组成部分

 

配置管理的概念大家肯定非常熟悉了,这是一个非常广泛使用的名词,但也经常容易被狭义的理解,比如被当做版本控制的同义词等。在持续交付领域,我们强调的是『全面配置管理』,也就是对项目所有的相关产物及其之间的关系都要进行有效管理,通过这种方式管理项目中的一切变化,实现项目中不同角色成员的高效协作,能够在任何时刻、使用标准化的方法,完整而可靠的构建出可正常运行的系统(而不仅仅是可工作的软件),并且整个交付过程的所有信息能够相互关联、可审计、可追踪,最终实现持续、高效、高质量的交付。

 

为了做到全面配置管理,并为持续交付后续实践奠定良好的基础,一般来讲至少要做好以下三个方面:

 

  • 代码和构建产物的配置管理:包括制定有效的分支管理策略,使用高效的版本控制系统,并对构建产物及其依赖进行管理;

  • 应用的配置管理:对应用的配置信息进行管理,包括如何存取配置、如何针对不同环境差异提升配置的灵活性;

  • 环境的配置管理:对应用所依赖的硬件、软件、基础设施和外部系统进行管理,确保不仅交付了可工作的软件,而且整个应用系统能够正常、稳定地运行;

 

三、代码和构建产物的配置管理

 

1、制定有效的分支管理策略

 

我们首先要强调,需要进行版本控制的不仅是源代码,还有测试代码、数据库脚本、构建和部署脚本、依赖的库文件等,并且对构建产物的版本控制也同样重要。只有这些内容都纳入版本控制了,才能够确保所有的开发、测试、运维活动能够正常开展,系统能够被完整的搭建。

 

制定有效的分支管理策略对达成持续交付的目标非常重要。持续交付建议的方式是频繁的提交代码,并且最好工作在主干上,这样一来修改对所有项目成员都快速可见,然后通过持续集成的机制,对修改触发快速的自动化验证和反馈,再往后如果能通过各种维度的验证测试,最终将成为潜在可发布和部署到生产环境的中版本。那持续交付为什么这样建议呢?下面我们来分析下几种常见的模式及其各自的优缺点。

 

在实际工作中,普遍采用的分支管理策略无外乎以下两种:

 

  • 基于分支的开发

 

 

这正是很多团队经常默认使用的模式,具体表现为接到需求后拉出分支,后面的开发都在分支上提交,每个分支生命周期较长,并且可能有多个并行分支,直到快要上线时甚至上线后才合并到主干。

 

分支开发模式,根据具体使用场景,还可以进一步细分为:

  • 分支开发、分支发布、发布后合入主干

  • 分支开发、分支测试、主干回归、主干发布

 

优势:

我曾接触过的一些团队,开始时会选择分支开发模式,因为多个功能可以完全并行开发,互不干扰。还可以按每个功能特性拉出分支,那么每次提交都是完整的功能特性,分支划分明确、版本控制的记录也会比较清晰易懂。并且由于不同需求的开发进度不同,可以选择某个先开发完成的功能特性进行合并、发布,而不会被其它分支上未完成的功能特性阻塞。

 

缺点:

那这种模式有没有问题呢?引用电影《无间道》中的一句话,“出来混,总有一天要还的”,我觉得用在分支开发模式中还挺贴切的。因为虽然使用分支暂时隔离了不同功能的代码,但系统的多个功能或者多个组成部分最终还是要集成在一起工作的。如果不同分支代码之间有交互,合并时可能会有大量冲突需要解决,包括文本冲突和语意冲突(更难发现和解决)。在实际项目中,进行代码合并时通常很容易出错,解决冲突也非常耗时。我曾见到一个团队有多个并行分支,在开发后期因为冲突太多,不得不指定两名对系统了解非常全面的高级开发人员专职合并代码,结果他们基本每天工作到后半夜,而且经常出现合并错误甚至遗漏合并的问题,这种方式非常痛苦。

 

分支开发模式,其实从本质上就是与持续集成的理念相互冲突的。持续集成是希望每次修改都尽早的提交到主干,主干总是处于最完整和最新的可用状态,充分验证后就可以用它来进行生产部署。而使用分支开发模式时,由于无法及时合并到主干,那么时间越长与主干差别越大,风险就越高,最终合并的时候就越痛苦。所以持续交付不推荐使用分支开发的模式。

 

权衡和建议:

在某些情况下,有时迫不得已要采用分支开发的模式,比如并行需求太多且相互干扰,或者在需求开发的同时有大块的重构工作要做,或者针对特定的用户开发特殊的功能,以及需要进行与主线无关的试验等等。

 

在这些场景下,拉出分支其实意味着已经在持续集成/持续交付上做出了妥协,那么我们建议至少要使用一些折中的方案:

 

  • 尽量缩短分支的周期,最长也不要超过迭代周期;

  • 每个分支上运行单独的测试流水线,保证质量。虽然这种方式浪费资源,而且其实也没进行”真正的“集成;

  • 分支只与主干合并代码,分支彼此之间尽量不做合并;

  • 分支定期合并主干上的变更;

  • 分支定期检查与主干的偏离度。有这样一种方式,就是每次分支提交时,由工具自动尝试与主干进行合并,并在这个临时的合并版本上运行自动化测试,用来检查是否可以合并以及合并后是否可正常工作,持续集成工具Bamboo好像已经提供了类似功能;

 

当然,以上这些仅仅是经过权衡的折中方案,只能一定程度上缓解问题,解决问题的最好方式还是改变分支管理策略。

 

  • 基于主干的开发

 

 

持续交付更倾向使用基于主干的开发(Trunk Based Development,TBD)模式,所有项目成员把代码都提交到主干上,提交后自动触发持续集成进行验证和快速反馈,通过频繁的集成和验证,在保证质量的同时提升效率。Google就在坚持使用主干开发模式,所有人的所有更改直接提交到Trunk上。

主干开发模式,根据具体使用场景,还可以进一步细分为:

 

  • 主干开发、主干发布

  • 主干开发、分支发布

 

优势:

相比于分支开发,主干开发模式有很多优势。首先是代码提交到主干,可以做到真正的持续集成,在冲突形成的早期发现和解决问题,避免后期的”合并地狱”,这样的整体成本才是最低的。另外主干会一直保持健康的状态,每次合入代码并验证后都可进行安全的发布。

 

难点:

主干开发模式有很多优势,但在实际使用场景中,也存在一些实施难点。

 

  • 主干上功能开发完成时间有先有后,如果遇到有未完成的功能但又需要发布时,就需要一种方法屏蔽掉未完成功能,才能进行安全的发布;

  • 主干上功能开发完成后,如果需要比较长的时间进行验收测试,那么此时为了确保发布功能的稳定性且所有功能是经过验证的,可能会限制新功能的提交,有的团队采用的封版、冻结主干的做法就是这种情况,这样的确会影响开发效率;

  • 如果修改的功能非常复杂,或者要进行架构上的大范围重构,以上问题就更加明显和难以解决了;

  • 如果团队规模比较大,同时工作在主干上的开发人员比较多,那么冲突的概率会比较大,持续集成的失败率可能比较高;

 

优化和建议:

其实上面提到的实施主干开发的难点,在实践过程中大多会遇到,但也基本都能找到对应的优化和解决方案。

 

  • 增量式进行开发。通过功能拆解,把大需求分解成小的Story,每个Story很小可独立发布。把大的修改分解为一系列小的变更,增量修改的方式更可靠,风险也更低,这也与精益思想中小批量生产的理念一致;

  • 主干开发模式也并不完全排斥使用分支,比如可以创建以发布为目的的发布分支,这样在执行发布测试和缺陷修复的时候,主干就可以进行下一个迭代的开发了,相当于通过并行工作提升了效率;

  • 隐藏未完成的功能。常见的实践比如后端先行,后端系统可以先开发上线,这时由于没有前端流量,也可以实现隐藏功能的目的;另外可以引入功能开关(比如Google的Gflags,Java领域的Togglz等),通过配置来控制功能对用户的可见性。但需要注意的是,使用开关也是有成本的,包括加开关和清理开关的成本、规则的制定和遵循等;

  • 有时还需要从架构的角度考虑解决方案,比如将大的系统分解为一系列小的组件或服务,对不同修改频率的部分进行解耦,每个组件或服务独立开发和部署,这是处理复杂系统比较好的方式;

  • 另外需要强调,主干开发模式非常强调代码提交习惯,包括频繁、有规律的代码提交(比如每人每天提交一次),而提交前需要进行充分的本地验证和预测试,以保证提交质量,降低污染主干代码的概率。

 

2、使用高效的版本控制系统

 

版本控制系统是软件研发和运维过程中最常见的工具之一,经过多年的发展,涌现了很多优秀的开源工具或商业软件。目前公司内部正经历将已服务的多年的版本控制系统SVN加速转化为分布式版本控制系统Git的过程,这也与整个业界的趋势保持一致。

 

 

相比于SVN,Git在功能和效率方面拥有众多优势,比如对离线代码库的操作、本地分支的管理、本地多任务并行开发、分支创建/切换/合并/Diff的效率等等,大家应该都比较熟悉,我们就不详细展开介绍了。在这里我们重点强调在其之上的代码托管平台。

 

常见的Git代码托管平台包括Github、Gitlab等,基本都是依托于Git代码库的底层能力,在其基础上提供一套Web界面,以可视化的方式管理代码提交、拉分支等常用操作,并且提供wiki用于存储文档,提供集成的问题跟踪器等。当然,这里面还有一个我最看重的功能,就是对开发协作工作流的支持。近两年公司内部也基于业界实践和一些开源工具,开发了自己的Git代码托管平台,对于研发效率的提升作用是非常显著的。

 

我们再回头来看一下刚才介绍过的主干开发模式,在Git工具及其托管平台中的落地实例。

 

 

上图展示了在Git代码托管平台上,一种基于主干开发的协作流程。其中标记了十多个相关活动,分别由开发人员、测试人员和自动化执行的Job协作完成。首先是开发人员通过git clone方式将代码检出到本地,并进行开发和提交(git add、git commit),在经过了充分的本地构建和验证(Local Build)之后,将代码push到refs/for/master分支。代码变更的推送(Change Request)首先后触发一系列静态检查(比如代码规范、安全策略检查等),通过后进一步触发一个自动化测试的job(比如编译和单元测试),都通过后需要进行人工的Code Review。在人工评审确认代码无问题后,由具备模块Owner权限的Tech Lead一键式合入Master Branch。同样,代码的合入会触发主干上的一系列自动化测试Job,以及随代码提交或每日定时执行的集成测试作业。在准备发布时,会拉出发布分支进行发布前最后的测试。此时如果主干上提交了其它重要功能或者重大缺陷修复,则可以Cherry-Pick到发布分支上,最终从发布分支完成代码的发布操作。

 

那么,Git及其代码托管平台在其中发挥哪些作用呢?我们来梳理一下:

 

  • 对于操作的简化。比如通过界面进行代码库的创建、权限的设置、代码库的搜索、一键拉分支、一键合并代码、比较代码、冲突显示和解决等;

  • 对于代码提交规则的设置和校验。比如可以设置提交前必须经过代码规范检查,提交后必须经过自动化测试验证和Code Review检查等;

  • 与研发相关系统的打通。打通Code Review系统、需求管理系统(代码与需求建立映射关系)、编译系统(用于获取代码并编译和打包)、自动化测试系统等。系统的打通非常重要,比如Code Review系统如果不能和代码托管平台有机整合,相关评审制度就很容易坚持不下来。Google对代码评审要求就非常严苛,他们的做法就是通过系统无缝对接,从技术上保证了只有评审通过的代码才能被提交进代码仓库。

  • 对于分支的关系智能计算和提示。比如计算多个分支之间的版本差异和代码差异,以及在主干版本领先于发布分支时,提示分支需要合并最新主干代码后才能发布等;

  • 还有最重要的,对于协作流程的支持和定制。以上展示了一种主干开发模式的协作工作流,实际项目中常用的还有Git Flow工作流、Pull Request工作流、Feature Branch工作流等等,这些不同的工作流也许是基于某些特定场景的最优选择,那么最好都可以在Git代码托管平台上进行支持和定制。

 

通过以上分析可以看出,使用高效的版本控制系统,配合可进行协作流程支撑的代码托管平台,对于配置管理的优化、让复杂场景以最小代价实现,其作用是非常显著的。

 

3、对构建产物及其依赖进行管理

 

持续交付强调要对所有内容进行版本控制,除了对源代码、测试代码等配置项做好管理,还有一点非常重要,就是对构建产物进行有效管理。构建产物一般是指在编译或打包阶段,生成的可用于部署的二进制包。一般情况下,我们不推荐将构建产物存放在版本控制库中,因为这样做效率较低也确实没有必要。通常我们使用的开源制品库包括Nexus或Artifactory,或者自开发的制品库(最简单的可能就是一个共享存储),通过版本标识等信息对构建产物进行管理。

 

制品库在应用时有一些有用的实践:

 

  • 在编译阶段创建二进制部署包,并上传到制品库。为了保证一致性,二进制部署包一般只在编译阶段生成一次,后面的所有测试和部署上线都复用这个包,而不要重复编译;

  • 制品库可用来管理依赖,比如Java领域的Maven,Python领域的Pypi等,都提供了全面的依赖管理机制,可以按GAV三元组(groupId/artifactId/version)唯一标识和引用一个对象,这样当构建项目时,会自动先下载指定的依赖;

  • 制品库可以按用途分为临时制品库和正式制品库,其中的制品保存周期可以不同。一般来讲,在交付流水线中进行发布时,会执行以下三个动作:(1)制定发布的四位版本号;(2)将当前代码按版本打Tag;(3)将二进制包从临时制品库迁移到正式产品库。

 

四、应用的配置管理

 

1、部署包的结构和应用配置

 

为了能够交付可正常运行的系统,我们需要把一切应用程序需要的内容进行标准化,并且注入到部署包中。作为应用程序正常运行的关键因素,应用的配置与程序文件、数据和各类部署和控制脚本缺一不可。

 

 

上图展示了某种部署包的结构。如果产品线需要部署的服务器规模比较大,我们就需要通过标准化部署包的封装,帮助我们实现部署过程的全自动化。比如大家熟悉的Docker,它为什么能够做到build once,run anywhere?本质上就是做了封装,把应用程序和相关依赖打在一起,生成一个镜像去部署。我们可以参考这种方式,把部署包设计为一个全量包,它不仅包含了二进制的可执行程序文件,还包含应用的配置、模块数据,也同时包含运行时依赖。我们把它打包一起,这样才能做到在任何一个标准化的环境里面,能够快速的将应用部署起来。另外,在部署包中还需要提供一个稳定的控制接口,用来描述程序怎么启动、怎么停止、怎么重启,怎么监控健康状况,告知部署系统如何进行部署和运维等。

 

接下来我们重点谈一下应用配置相关的问题。

 

2、应用配置的注入方式

 

我们经常遇到的问题是,应用配置在不同环境中是不同的,比如数据库的IP地址和端口、是否打开Cache、加密所用的密钥等,这些参数与应用程序逻辑无关,而只与环境相关。那么同一个部署包如何适配不同环境所需的应用配置呢?为了解答这个问题,我们先来看一下应用配置信息的几种注入方式。

 

 

如上图所示,应用配置的注入一般有三种方式:

 

  • 打包时注入

 

在打包的时刻,构建脚本可以将配置信息与二进制文件一起,注入到部署包中。比如J2EE规范中就要求配置信息与应用程序要一起打包到war或ear包中。

 

  • 场景:少量静态配置文件,或配置每次随二进制程序变更

  • 优点:打包和部署的过程比较简单,不同环境使用特定的部署包

  • 缺点:环境、应用、配置紧耦合,灵活性低

 

  • 部署时注入

 

在进行部署时,部署脚本获取基础配置以及不同环境特定的配置项,动态生成每个环境所需的配置信息。相当于基于配置模板文件,在部署时再实例化为将要部署应用的具体环境的配置。

 

  • 场景:大量的配置项,一些配置项在不同环境中存在差异

  • 优点:可以使用部署脚本或工具,动态生成特定环境配置信息

  • 缺点:需要维护应用与配置版本的匹配关系,增加了复杂度

 

  • 运行时拉取

 

在应用应用启动或运行时,通过外部的配置服务拉取应用配置(如通过REST风格的接口)。现在很多使用容器部署的微服务架构系统,配置中心或配置服务都是其非常核心的组件之一。

 

  • 场景:频繁变更配置项,或者需要动态加载应用配置

  • 优点:部署包与配置彻底解耦,具备较高的灵活性

  • 缺点:需要考虑配置中心的高可用性和配置变更的原子性

 

从持续交付的理念来看,不推荐在打包时注入配置的方式,因为我们希望在不同的环境中使用相同的部署包,以确保发布的版本就是我们充分测试过的。在一些团队中曾经发现,为了适配测试环境,测试人员使用源代码自己进行编译和打包,那么即使这个版本测试通过了,也无法确保生产环境部署的应用版本是没有问题的。所以我们更倾向于对应用配置单独进行版本控制,并独立于部署包之外进行管理。

 

3、部署时注入配置的技术

 

部署时注入配置的技术,从实现原理上就是把通用的配置信息作为默认配置项,然后定义一系列占位符,用于替代那些特定环境有差异的配置项。然后在部署时,通过适当的方法用实际的配置项覆盖掉这些占位符。但需要注意的是,要尽量减少差异化的配置项,只保留与应用系统运行环境紧密相关的配置项。并且,最好能对配置项的覆盖过程进行校验,防止因配置失误导致整个部署失败。

 

有一个经常使用的开源工具AutoConfig,类似于Maven Filtering的工作方式,该工具与应用所采用的技术、框架完全无关,对应用完全透明,具有良好的通用性。值得一提的是,这个工具成功解决了Maven Filtering在替换配置时需要重新Build的问题,即不需要重新获取源码并Build,就可改变目标文件中所有配置占位符的值,达到部署时动态修改配置的目的。详细介绍如下表所示:

 

 

在公司内部,针对一些定制化的场景,我们也自己开发了配置注入的相关工具,从实现的原理和效果来看都是比较类似的。

 

 

五、环境的配置管理

 

上面我们介绍过,持续交付的产出应该是可正常运行的应用系统而不仅仅是可工作的软件,系统运行起来除了应用程序本身,还依赖于硬件、操作系统、中间件,以及各种库文件等。前面介绍了分支和代码、构建产物、应用配置的管理,下面我们重点介绍环境的配置管理。

 

在环境配置管理的领域,随着所管理服务器的规模不断增长以及业界新技术和新工具的应用,其发展过程中一般会经历以下三种模式:

 

 

  • 雪花片服务器

 

这可能是很多公司都经历过的,比较原始的服务器管理状态。任何一片雪花看起来差不多,但实际上细节都是不一样的。对于服务器的管理也是这样,我们的服务器经过长年累月的运行,各种操作系统和软件的升级、更新,以及手工执行的各种黑科技的补丁,都会造成服务器间有细微的差别。如果某一天一台服务器宕机了,其实很难再建出一台一模一样的出来,因为配置过程已经无从追溯了。所以这种模式的主要问题是:反复修改带来不确定性和风险,环境的重建困难。

 

  • 自动化、配置化的环境管理

 

上图中在这个模式下列举了很多环境配置管理工具,如Puppet、Chef、Ansible、SaltStack等,它们能够以自动化的方式管理操作系统及其之上的整个运行时环境。通过这些工具,可以用易于理解、声明式的方式定义环境中需要安装什么软件、启动什么服务、修改什么配置等等,这些声明具备幂等性的特点,反复运行是安全的。并且,我们可以将这些声明式的定义保存在版本控制库中,这样就记录了每次变更的完整过程,相当于对环境的所有修改具备了完整可追溯的能力。下表对这几款常见的环境配置管理工具进行的简单对比。

 

 

  • 不可变服务器(Immutable Server)

 

随着容器技术的广泛应用,很多应用的部署演进到了不可变服务器的模式,就是任何的变更都打在镜像里面,用镜像进行各级测试,最终通过测试后使用镜像上线,上线后不做任何变更,直到被新的实例替换掉。这种模式更这也符合持续交付原则:只构建一次,在所有环境运行。Docker提供了一种应用封装的标准格式,可以用来创建在开发、测试和生产中可重用的容器,并且在单机层面上实现了轻量级的资源隔离,而在管理大型服务器集群时,可以应用如Docker Swarm、Kubernetes、Mesos+Marathon等集群管理系统。关于环境管理的话题本文中就暂不展开了,有机会我在持续交付模型中另外一个关键实践『环境管理』中再做详细介绍。

 

感谢你坚持看到了这篇文章的最后:)

 

总结一下,刚才我们重点介绍了持续交付模型诸多实践的其中一个:『全面配置管理』,分别从代码和构建产物的配置管理、应用的配置管理、环境的配置管理三个层面进行了展开说明。配置管理非常重要,只有配置管理做到位了,后续的构建管理、持续集成、测试管理、环境管理、部署管理以及整个交付流水线才能够比较顺利地建设起来。而配置管理想要做到位,不仅仅是使用工具,还在于对上述介绍的一系列实践的落地和坚持。

 

希望本文对大家奠定持续交付的实施基础,以及未来真正走上持续交付之路能有所帮助,后面有机会再跟大家继续分享持续交付模型中的其他相关实践。

活动预告