持续交付模式下的安全活动

在上一篇文章《开发团队面临的三大安全挑战》中,我们对现如今敏捷精益团队所面临的安全挑战进行了总结和分析,这三大挑战分别是:

  1. 一次性的安全检查无法匹配持续性的交付模式
  2. 缺乏自动化、自助化的支持,安全实践落地难
  3. 高耸的部门墙让开发和安全团队难以进行高效的协作

在接下来的几篇文章中,我们将逐一为你介绍团队、组织应该如何应对这些挑战。本篇文章先来讲讲如何解决第一个挑战。

采用持续性的、轻量级的,能够融入到持续交付模式的安全活动

对于绝大多数团队而言,为了确保开发出来的应用具有足够的安全性,渗透测试是一个被广泛采用的手段,也可以说是唯一依赖的手段。然而由于渗透测试比较重量级,通常只能提供一次性的安全反馈,而这在追求快速开发、迅速响应市场变化的敏捷精益开发方式下,它的不足被放大了。

团队需要的是一个高效的安全质量反馈机制。所谓“高效”是指,这个机制必须能以更快的速度、更高的频率提供应用的安全质量反馈,并且要足够轻量级以便于无缝融入到迭代交付过程中。

那么“更快”是多快?多频繁才算“更高的频率”?“轻量级”要轻到什么程度?

快到“立等可取”,比如只需几分钟甚至更少时间,就能知道应用的安全质量如何;频繁到任意时刻都可以获取一次安全质量反馈,比如每次代码提交后,都能知道应用安全质量是否被破坏;轻量级到团队不认为这些安全活动会带来多大的额外付出,比如每日代码审查中,顺便从安全的角度对代码进行评审,就能阻止有安全风险的代码被提交到代码仓库,而整个过程可能只多花费了几分钟时间而已。

一些推荐采用的安全活动

在CI中集成自动化安全扫描工具

其实在很早以前大家就已经发现,凭借人工的力量从应用里寻找安全漏洞异常耗时,另外对于某些安全漏洞,完全可以通过特征识别的方式对脚本来自动进行检测,于是从那时起就诞生了一系列自动化安全扫描工具,以下简称“漏扫工具”。

与此同时,在应用开发这件事情上,持续集成、持续交付的理念也在迅速的被广泛接纳,越来越多的团队开始使用CI,也体会到了自动化的威力所带来的好处。

随着CI的广泛普及,团队完全可以把这些漏扫工具集成到CI当中,作为应用构建过程中的一个标准步骤来执行。这样,团队既可以借助漏扫工具以节约人工成本,又可以持续性的对应用安全质量进行监控:一旦某次构建发现安全问题,构建流水线就会失败,引起开发团队的注意,促使团队尽快对有问题的代码进行修复,从而降低漏洞修复成本。

漏扫工具数量众多,可以说是百家争鸣,不过可惜的是,易于集成到CI中的却不多。OWASP ZAP提供了RESTful API,也有Jenkins插件,算是做得比较不错的漏扫工具,而BurpSuite要做到同样的效果还需要额外的配置才行。推荐团队可以先尝试把ZAP集成到CI中。关于具体的集成细节,我们会通过后续文章进行介绍。

编写自动化安全功能性测试用例

漏扫工具不是全能的,有些类型的安全漏洞,比如身份认证、访问控制以及和业务强相关的漏洞,它就很难甚至根本无法扫出来。然而,这些漏扫工具不容易扫出来的安全漏洞,对于人来讲却正好是小菜一碟。

举个例子,人能够很好的理解下面这个API应当具备的安全行为,并对其进行有针对性的测试,然而漏扫工具则已经哭晕在厕所:

漏扫工具检查不出来的安全问题,人可以很好的进行测试,并且依然通过自动化来提高效率。团队可以像平常编写集成测试,或者端到端功能性测试那样,对于期望应用应当具备的安全行为,编写对应的测试用例进行覆盖。我们把这种做法叫做编写自动化安全功能性测试用例。

随着自动化安全功能性测试用例数量的不断累积,它的威力也将越来越明显,尤其是在对应用进行回归测试的时候,更是显露无疑。通常而言,在团队每次做应用发布之前,都会进行一次回归测试,主要目的是确认新功能工作正常,与此同时已有功能未被破坏。安全作为应用质量中的重要组成部分,也应该进行一次回归测试。相比于传统的渗透测试,自动化安全功能性测试用例再配合上CI中的自动化漏扫工具,在很短的时间里就能对应用进行比较全面的安全检查,为应用发布提供决策支持。

识别安全需求,并将其作为验收标准写入到用户故事卡

如果说把漏扫工具集成到CI,以及编写自动化安全功能性测试用例是在“把事情做对”,那么识别安全需求,并把它作为验收标准写入到用户故事卡中则是在“做正确的事情”。

团队其实是很重视安全的,他们愿意付出努力提升应用的安全性,然而这又和我们实际观察到的现状有冲突:安全的事情大家心知肚明,但就是没人主动去做。

为什么?原因是多方面的,其中一个重要的原因是,因为安全需求没有被明确提出来,它既不在故事卡的验收标准里,也没有在给故事卡估点的时候被考虑进去。于是安全需求就仿佛变成了“多出来的”工作量,一旦团队面临交付压力,这部分工作自然就会被无限期的往后推迟,最后的归宿就是不了了之。

因此,团队除了需要借助漏扫工具以及自动化安全性功能测试用例,还需要把安全需求明确出来,纳入到项目交付范围内。团队可以用威胁建模、恶意攻击场景头脑风暴等活动来梳理安全需求。

此外需要注意的是,安全需求的表现形式不是最重要的,最关键的在于把需求在团队内部明确出来,而不是大家心照不宣。比如,安全需求是写入到故事卡的验收标准里,还是单独创建安全故事卡,这本身并不重要,重要的是安全需求通过这些形式能得到明确,让团队能把安全需求和常规的业务需求一起放到Backlog里,统一对它们估点、设置优先级、安排迭代交付计划。

小结

敏捷精益团队面临的第一大安全挑战就是一次性的安全检查无法匹配持续性的交付模式。应对这一挑战,团队需要采用一系列持续性的、轻量级的,能够融入到持续交付模式的安全活动,从而使得团队建立起一个高效获取应用安全质量反馈的机制。

至于敏捷精益团队如何应对另外两大安全挑战,我们将在后续的文章里一一详解,敬请期待。


更多精彩洞见,请关注微信公众号:思特沃克

Share

DevOps实践-打造自服务持续交付-下

本文首发于InfoQ:

http://www.infoq.com/cn/articles/devops–build-self-service-continuous-delivery-part02

上一篇文章中,主要讲了DevOps转型的动机、策略和方法,本文将会为大家带来更多DevOps转型的落地策略和实践。

实践过程

下图是我们为团队设计的持续交付流水线,目的是能让Platform团队和交付团队之间的触点能够被融入到持续交付流水线中,并且以基础设施代码作为协同媒介,通过自动化的方式实现开发与运维(即基础设施与软件系统)的无缝对接。

我们来看看我们给持续交付流水线赋予了哪些能力:

  1. 站在交付团队的视角,我们决定将基础设施构建,流水线构建,部署等活动都代码化,与应用代码放在同一个代码仓库中。
  2. 交付团队通过提交我们的基础设施代码到仓库后,自动触发持续交付工具创建或更新流水线。
  3. 接着会自动触发构建,静态检查,测试覆盖率校测,代码规范验证等任务,最终输出构建产物并将构建产物推送到仓库。
  4. 然后会根据交付团队对基础设施和环境的定义到当前要部署的网络环境中去创建或更改虚拟机、网络、存储方式等
  5. 最后,当基础设施创建成功以后,就会去仓库下载指定版本的构建产物进行最终的部署活动。

但需要注意的是:

  1. 为了持续优化交付流程,我们对开发的许多活动进行的数据收集和分析,以报表的形式去分析展示代码提交频率,系统和代码的质量情况,缺陷和构建情况等,帮助团队找到自己的瓶颈或问题。
  2. 帮助团队能够实时监控自己应用的运行状态,设计和查看不同纬度的日志总汇等。

那我们来看看通过什么技术可以实现这样的持续交付流程:

我们选择了一种轻量级、低耦合的技术组合Ansible+Jenkins+AWS。我认为其核心是Ansible。

下面我们来看看Ansible可以帮助我们做些什么:

  1. 创建和更改AWS中的资源
  2. 自动化部署和基础设施测试
  3. 建立开发与平台团队之间的沟通体系

考虑到基于yaml语法的Ansible配置简洁且易读,所以我们选择直接用它作为提供给交付团队的公有DSL模板,利用Ansible Playbook的模块化思想将开发团队的职责和平台团队的职责很清晰的分离,平台团队关注Ansible提供给交付团队的服务是否满足需求和DSL模板是否易用,而交付团队只用关注如何基于公有DSL去定制自己的基础设施,环境依赖和部署等。

于此同时也满足了很多开发对于Ansible和AWS的兴趣和热情,更使得之后在交付团队落地变得更容易。

接下来通过一个实例来看看:

左边是Platform团队的仓库,这个仓库里面包含了创建基础设施、环境配置和部署的实现。

右边是交付团队的仓库,其中deployment目录下,是公有的DSL模板,其中包含多种环境(开发、测试、预生产环境等的独立配置),以及一套基于DSL的代码模板,其中包含创建基础设施和部署应用这两部分DSL代码模板。

接下来,我们来看看它们配合与集成的方式:

他们会在持续集成流水线中被动态组合到一起:

  1. 在创建基础设施和部署的时候会分别拉取基础设施代码库和应用代码库。
  2. 此时应用代码为调用入库,公有基础设施为功能框架库,两者配合,完成环境的创建和应用部署。

在做微服务的团队,接受度非常高,能够快速上手,而且甚至有团队因为自身的一些需求,自己去写一些Ansible模块,然后向我们发起pull request。

当然,我们在推广这套流程的过程中发现,一些实践能够帮助我们更快速落地:

  1. DevOps团队的成员由各交付团队和原运维团队组成,这样的组成方式,能够保证团队的视角可以关注到整个持续交付过程的每个环节。
  2. 交付团队成员与DevOps团队成员定期轮岗制,DevOps小组中的文化(如自动化优先)可以蔓延开,让交付团队更快适应。
  3. 结对、Showcase和培训,主要目的是知识的传递,让更多地团队逐步采用新的交付模式,得到更多改进中的反馈。
  4. 提供给交付团队的自服务代码仓库对每个人开放,交付团队被授权优化、新增基础设施,让DevOps文化和职责落地到交付流程中。

现在来看,集中式、审批式、被动响应请求的中央运维团队不再是整个交付流程中的依赖和瓶颈,已基本转向带自服务化、审查式、主动优化的去中心化交付团队:

我们通过技术驱动改进,让团队之间的合作方式发生了巨大改变,开发与运维之间的那道墙也渐渐消失,以前被动响应请求中央运维团队逐步被平台团队所替代,平台团队中一部分人会负责基础设施平台的发展,负责公有云与企业内部系统的对接、完善安全、灾备、提供基础设施的自服务机制,另一部分人会为产品团队提供可定制的工作、平台、并为产品团队赋能。这时交付团队开始管理自己的环境、维护流水线、负责生产环境变更。

在推广和落地自服务持续交付流程的过程中,我们也遇到了很多遗留系统和复杂部署应用的交付团队,他们无法直接对接这套交付流程。

例如有一个40-50人的团队,它是基于AEM开发整个公司所有的前端门户,AEM是Adobe公司的CMS系统,其安装和部署很复杂,以前都是通过手工安装和拷贝的方式进行部署,而且他们在开发-》测试-》部署阶段可能会动态扩张多套环境来支持,且每次代码变更的提交都会对已经安装的AEM进行修改、配置、重启等操作。

整个开发和测试流程都很复杂,而且效率很低,出现问题和故障的风险也很大,如果我们直接利用Ansible把AEM的安装和部署过程都自动化,由于AEM本身部署的复杂性,可以预见以后这部分更新和维护的工作还是很难交由交付团队自治。所以我们第一步要做的就是为其设计新的持续交付流水线,然后在这个流程中去定义和识别两个团队的职责和关注重心,最后再通过打造高效的自服务使整个交付流程得到改进。

首先我们根据校服团队提交变更的平率,从低到高依次定义了三条持续集成流水线(如下图):

  1. 创建和测试基础设施资源
  2. 配置基础设施资源和环境
  3. 部署应用程

因为AEM安装和更新很复杂,所以我们引入了镜像技术。基础设施和基础设施配置两条流水线的产物为一个image,应用流水线在部署阶段会去检查是否存在新的环境镜像,如果存在,就会基于快速创建一个新的AEM环境,然后进行应用代码的部署。

通过新的自动化持续交付流水线大大加速了AEM团队的开发和测试速度,也使得整个环境更加可控和易维护。对于交付团队来说,他们可以自己去维护包括基础设施、环境变更和应用部署等全生命周期交付活动。对于Platform团队来说,只用去考虑镜像的生命周期管理,如何去优化镜像的创建速度等,这些可以帮助到更多其它团队解决类似问题的领域。对于这种特殊情况,我们尽管引入很多与大多数团队不同的交付流程和技术,但所有的工作和优化都是基于之前打造的自服务持续交付流程、协议和工具平台之上的,保证了不同的交付团队与Platform的配合方式的一致性。

实践启示

通过在大量交付团队落地基于自服务的持续交付流程,两种团队的职责更加清晰了:

所有好的实践都必须考虑规模化的问题,如果无法大规模的被接受和落地,再好的实践也没用。对于咱们这个转型的过程,我也给出一个套路:(如下图)

有了套路,接下来总结一下应用这个套路进行DevOps转型过程中的一些经验和思考:

  1. 易用的通用DSL模板设计,提供交付与Platform团队统一的DSL模板(build and update anything)。
  2. 构建通用持续交付流水框架,提供给交付团队定制化流水线的能力,使流水线主要关注点始终在产品的成功交付。
  3. 以技术驱动DevOps文化大面积传播,让Platform团队成员走入交付团队,协作改进、知识传递,确保实践落地。
  4. 将一切自动化、自服务化。交付团队应该被授权优化、新增基础设施服务,让DevOps能力和职责在交付团队落地生根。

最后,我提取了5点对我们来说非常重要的策略或是推进方法:

  1. 小步快跑,在有大方向的基础上,需要将每一步改变都设计得足够小,这样才能足够快的去改进。
  2. 交付团队赋能,给每个人都留一扇门,在他意识到要做些事情的时候,可以很快付诸行动。
  3. 逐步用基础设施自服务化替代运维部门的审批流程。 建立持续反馈和改进机制。
  4. 以DevOps团队为杠杆,撬动更大范围自服务交付。

非常感谢你的耐心阅读,希望我的文章能够给你带来哪怕一点点启示。有任何问题或是想与我讨论的点都可以给我发Emial: jxzhong@thoughtworks.com


更多精彩洞见,请关注微信公众号:思特沃克

Share

DevOps实践-打造自服务持续交付-上

本文首发于InfoQ:

http://www.infoq.com/cn/articles/devops–build-self-service-continuous-delivery-part01

DevOps转型的动机

我们的客户是一家海外本土最大的金融保险集团,他们在发展到一定规模以后,意识到自己就像一头笨重的大象,举步维艰,通过对整个交付流程的思考和分析,发现了以下一些严重影响交付速度的问题:

  1. 一些好的关于产品改善和创新的想法很难落地。涉及到一些遗留系统的配合:调整、部署、扩展等,使团队对发布没有信心。新的服务或者应用的构建,很难快速上线,被卡在了生产环境部署阶段。
  2. 各种不同种类的应用、服务的部署方式和流程不一致。运维部门作为一个支持部门,很难为大量团队提供快速反应。运维人员对于需要部署和运营环境之上的产品也不够了 解。
  3. 微服务运营过程中,交付团队难以做到快速集成和部署。运维团队对微服务的部署运维方式不理解,依旧老瓶装新药,很难适配新架构下的交付模式。开发团队大多关注代码和架构,对于产品如何能在生产环境稳定运行、需要考虑哪些安全性和可持续性的因素并不是很了解。

问题分析和挑战

通过对这些问题和各个团队反馈的深入分析,发现其中最大的瓶颈在于交付团队与运维部门之间的各种依赖和沟通浪费,而这个瓶颈又是解决大多数问题的前提。

我们将瓶颈具象化后(如上图),可以看到两种团队之间其实是存在一堵墙的,一是因为传统的部署流程非常繁琐和低效。二是因为两种角色关注点和目标的不一致。

如果在这样的情况下,想实现微服务架构转型,实现更快速和安全的交付,只会更快的暴露出这堵墙引起的各种问题。开发阶段,系统的架构和依赖环境都是Developer说了算,对生产环境的关注度不高。部署、发布阶段,Operations会考虑如何构建一套稳定的基础设施,又如何去部署和运维开发的产物,但是往往对于产物的了解不充分,对于产物的周边生态和与它们关系的了解也不够。

那么引入DevOps文化,消除开发与运维之间的壁垒,逐步打造更高效的交付流程就成为我们破局的关键,那我们应该怎么做呢?

改革之初,我们发现并去尝试了Bimodel(双模IT), 我们看看它是否能解决我们的问题:

先简单介绍一下什么是双模IT:

它将IT系统分成了两种模式:

  1. 一种是新型的数字化、高市场适应性的IT,这部分业务聚焦企业新市场和业务的开拓,创新和发展,强调IT自身对于市场的高适应力。
  2. 另外一种模式下,我们则需要稳固发展,对于传统模式我们倾向于更加严谨和标准的流程去保护现有业务,稳定性比速度更加重要。

我们从采用这样一种模式的实践案例中发现:组织内部会出现连两种速度的交付流程,好的情况可能是采用敏捷开发流程的交付线,有着快速的交付能力,相反,对于继续采用传统开发流程和运维方式的团队,保持着稳定但低效的交付能力。

从业界很多公司的现状和发展趋势来看,双模IT确实是很多组织存在的现状或是必然经历的过程,但不是一个好的模式,从实际的交付过程来看,存在4点问题:

  1. 双模IT的划分方式更多是基于软件系统,而不是从业务活动出发进行的,所有软件系统的交付都应该是面向业务价值的。
  2. 双模IT会让我们误以为速度的提升会引起质量的下降,但是对于我们在ThoughtWorks的很多敏捷实践中学到的:随着交付的推进,质量内建是团队共同负责和持续改进的重点。交付速度的提升,往往都伴随着质量的保障,而不是忽视质量。
  3. 实际生产中,一个新的产品或者功能往往会依赖很多遗留系统提供的服务,如果它们仅仅只能达到稳定和低效的交付,对企业来说对市场整体的响应能力也会越来越低。
  4. 企业的创新不仅仅只是从零创造一个新的产品,还有很多机遇现有的资源。一个新的系统和功能往往不仅会既涉及到新服务、应用的创建,也会涉及到遗留系统的修改,调整和改造。

由此可见双模IT是在以一种试图掩盖问题的方式来逃避目前最重要的问题:开发和运维之间的壁垒。感觉像是一个病人先是放弃治疗,然后又努力的寻找延长寿命的方法,有些隐患终会爆发。

打造自服务持续交付

紧接着,我们开始采用了一种看似不可行的方式开启了DevOps转型,建立公有DevOps团队。有很多人可能会说这是一种反模式,怎么可能会建立一个团队专做DevOps相关的事情,那和以前的运维部门又有什么区别?DevOps提倡的Dev与Ops高频度合作的文化是不是就无法大面积传播了?

因此我们需要很明确的定义我们对这个团队的期望和它的职责是什么,它是怎样和交付团队合作,影响交付团队,最终能让DevOps的文化可以大面积传播。这个团队的目标是要像杠杆一样,翘起更大面积的DevOps变革。

所以我们认为公有DevOps团队应该与其它的端到端交付团队的人员构成是一样的。不同的只是目标和价值,主要体现在帮助更多的团队植入DevOps文化、优化持续交付流程。最终达到的目标是每个团队都可以自治,每个团队都可以进行端到端的开发、测试和部署,并可以自驱动的持续改进。与此同时,这个团队不仅仅只是为交付团队提供更多涉及基础设施、持续交付流水线、部署等活动所需要的自动化能力,还会支撑交付团队根据自身的上下文去定制和规划自己的持续交付流程和部署策略等。

(图片来自:http://t.cn/R9jnzHR)

现在,相比于DevOps团队的叫法,我们更愿意称呼这个团队为Platform团队,一个原因是我之前所说的避免被错误理解,另一个原因是随着各个交付团队逐步实现自服务持续交付,这个专有团队也有了更高的目标:持续打造和优化一个能够支持各交付团队快速交付的平台。

当时,我们首先为团队定义了新的工作方式:以自服务,自动化和协作 作为核心文化,希望团队通过提供便捷的基础服务,让交付团队拥有自动化的交付流水线,并通过更多的沟通协作,尽可能让每个交付团队都能够独立自主的设计、创建和更改自己的基础设施。然后再根据各个交付团队的实施情况和结果来对流程和服务持续改进。

所以第一件事,我们首先设计了一个高效的持续交付流水线,让Platform团队和交付团队建立触点:

如下图所示,蓝色的基因链为交付团队的持续交付环,红色的基因链为平台团队的持续交付环。两种团队以某种低耦合的弱连接进行全程协作,Platform团队在整个端到端的交付过程中都要能尽量通过构建自动化能力来支撑交付团队能够快速、安全的进行持续集成、部署等活动。这样的合作方式也给我们提供了优化触点的可能性,也能够通过优化和改进,缩小这个持续交付周期,让交付更高效。

请原谅我在这篇文章进入高潮时卖个关子,由于考虑到大家的阅读体验,所以文章分成了上、下两个部分,上半部分主要讲DevOps转型的动机、策略和方法,下半部分会讲我们如何实际应用基础设施即代码来建立Platform团队和交付团队的触点, 又怎样让遗留系统团队和微服务团队的交付速度成倍增加。敬请期待!


更多精彩洞见,请关注微信公众号:思特沃克

Share

遗留系统流水线的改进

持续集成(Continuous Integration)是一种软件开发实践,它倡导开发团队频繁地进行系统集成,每一次的集成都可以通过流水线(Pipeline)快速验证。

(图片来自:http://t.cn/Rajfxek)

和传统的集成方式相比,持续集成可以有效地缩短反馈周期、提高软件质量、降低开发成本。这种开发实践也越来越为更多的开发者所接受。对于一个有七年历史的项目,非常幸运的是我们在项目刚开始就使用了持续集成,这也是我们可以长期、稳定地给客户交付高质量软件的保障之一,但是有时我们在项目中也经常会听到一些这样的声音:

  • 每次想提交代码的时候都没有机会, 我们是不是要考虑引入提交“令牌”机制,拥有“令牌”的人才能提交
  • 这个Pipeline已经挂了这么久了,今天估计是无包可测了
  • 这个是测试“随机挂”,重新触发一下Pipeline就好了
  • 我的提交Break了Pipeline了吗?我确认一下
  • ….

通过分析我们发现,这些声音背后的真相更是残酷:

  • 约20对Pair依赖的核心Pipeline构建时间超过1个小时,开发的反馈周期长,大量的半成品积累在本地开发环境和Pipeline上
  • 代码提交之后大约需要2个小时才能出包,每周平均每天可供QA测试的包数量不足1个,平均每个Story的周期时间长
  • 上线前冻结代码、回归测试的时间大约需要2周左右,冻结期间产生的代码无法集成、验证
  • Pipeline不稳定测试导致某些Pipeline构建至少需要重新触发2到3次左右
  • 依赖关系复杂,牵一发而动全身,优化不知从何做起

有时“正确地做事情”比“做正确的事情”还要困难,在项目一开始便在项目中尝试实施TDD等敏捷开发实践,但是随着项目的规模的增加,功能越来越丰富,单元测试在增加、基于UI的功能性测试也在增加,流水线的构建速度却变得越来越慢。微服务架构具有易扩展,技术选择灵活和部署独立等特性,于是我们把应用拆分为不同的微服务,但是同时也带来了流水线的数量和微服务之间集成测试的增加,Pipeline的依赖关系也变得越来越复杂。

在去年,项目中的DevOps小组,在前人的基础上和团队大刀阔斧地开始了Pipeline的改进工作,希望可以通过一些必要的措施,优化流水线,保证QA每天有包可测、缩短开发期间的反馈周期。

改进什么?

这是我们在改进刚开始就面临的一个问题,本地Pipeline数量众多,每一个Pipeline平均有3-4个构建阶段,每一个阶段又有2-3个并行执行的任务,如此众多的Pipeline和任务,应该从什么地方着手?面对Pipeline构建时间长、测试不稳定、代码冻结时间长,依赖关系复杂等问题,应该如何决定改进的优先级?根据高德拉特的约束理论(ToC),所有在非约束点的改进都是假象,我们可以把整个构建流水线看作是一个完整的系统,如果我们改进的是约束点的上游,就会增加约束点的负担;如果我们改进的是约束点的下游,由于通过下游的工作量主要由约束点决定,所以任何在这个位置的改进都是徒劳无获,无益于整个系统产出的提高还造成了浪费。

现状问题树是寻找约束点的方法之一,借助于这个思维过程(Thinking Process)可以帮助我们梳理“不良效果”(Undesirable Effects )之前的因果关系,最后找到需要解决的核心问题,解决了根本问题之后,由其衍生的各种“不良效果”也大多会消失。通过内部的讨论和演练,我们最终把问题定位在以下几个方面:

  • 核心Pipeline构建时间过长
  • 集成环境测试成功率低
  • 缺少必要的监控预警机制

如何改进?

降低资源的占用时间

在零件制造车间中,每一个零件都需要按照既定工序通过车、镗、铣、磨、刨等车床,每一个阶段都需要占用车床资源进行特定的加工工作。和零件加工类似,来自客户的每一个功能性需求也同样要经过类似处理流程,从需求分析到编码开发,从构建打包到部署测试,每一个环节都需要占用一定的资源。如果在单位时间内,资源占用的比率越高,就会产生比较严重的排队现象。如下图所示,在单位时间内如果资源占用大于百分之七十,队列的长度也会呈指数型增长。

Pipeline作为整个软件交付流程重要的一环,如果每次构建资源占用比例过高,会导致大量的代码积压在开发环境等待构建、验证和打包,“在制品”数量也越来越多,而过多的“在制品”恰恰就是软件交付延期的隐形杀手之一。解决对Pipeline资源占用比例过高的途径只有一个:加速处理“在制品”的流程。在改进的过程中我们总结出了以下几种主要的加速手段:

1.并行化

假设Pipeline单元测试需要30分钟才能运行结束,可以通过切分单元测试多进程并发执行,如下图所示,可以节省近20分钟的运行时间:

同样也可以把并行化用于优化Pipeline结构,如下图所示,通过减少不必要的Pipeline依赖关系,让不同的Pipeline并行执行,可以减少大约五分钟的端到端的构建时间。

优化前: 端到端构建时间20分钟

优化后:端到端构建时间15分钟

2.使用Mock或者Stub,隔离真实服务

对于有数据库依赖的单元测试,如果在运行期间连接真实的数据库,读写速度会比较慢,除了IO操作之外,为了保证不同测试之间的隔离性,往往还需要考虑测试运行之后的数据清理工作,而这也会带来一部分的性能损耗。针对这种情况,可以考虑使用内存数据库替代真实的数据库,在提升IO操作的同时,数据清理工作也变得很简单。

为了保证代码的修改没有破坏现有的功能,一般我们会增加基于UI的回归测试,在测试运行之前部署当前应用以及其所依赖的各个服务。对于应用依赖的服务的部署和API调用,也会消耗部分时间。这是可以考虑使用SinatraMoco等工具隔离部分第三方服务,从而缩短部署时间和API的调用时间。

但是隔离真实服务的同时也掩盖了测试替身和真实组件之间的差异性,比如我们在API测试中使用Sqlite替代SQL Server,但是SQLite并没有datatime字段类型,需要在测试代码中需要做额外的映射配置,这种差异性同样也会导致潜在的产品缺陷。所以我们在选择Mock或者Stub时需要权衡利弊,如果使用则需要额外的手段来验证这种差异性

3.优化基础设施和运行环境

增加硬件配置如CPU,内存、替换固态硬盘等也可以一定程度地降低Pipeline的构建时间。对于由于语言或者框架本身带来的性能约束,也可以通过升级到新版本来解决,比如把Ruby从1.8.7版本升级到2.0版本。

提高构建成功率

团队在改进的过程中发现可以通过下面的公式大致估算出平均每天Pipeline产出的可用包的数量:

根据这个公式,如果要增加平均每天出包的数量,除了降低每次构建的时间之外还需要提高Pipeline的构建成功率,而影响构建成功率最常见的问题就是“非确定性”测试。在项目的Pipeline上曾经出现过下面这些情况:

  • 一些UI测试每次至少需要被重新执行一次才能通过
  • 部分单元测试在特定的时间段会稳定失败
  • 构建结构和测试被执行的顺序有关

Martin Fowler在Eradicating Non-Determinism in Tests中指出了这种非确定性测试存在的两个问题:首先它们属于无用测试,由于测试本身的不确定性,它们已经无法用来描述、验证对应的功能。测试运行的结果也无法给开发人员提供正确的反馈,如果测试失败,开发人员无法直接判断这个测试是由于产品缺陷导致还是由于非确定行为导致。其次这些测试就像“致命的传染病菌”一样,降低正常测试的存在价值。假设一个测试套件中有100个测试,其中10个测试为非确定性测试,这些非确定测试会给开发团队带来很多的“噪音”,团队对于Pipeline失败会觉得司空见惯、习以为常,剩余90个测试的作用也会大打折扣。

1.保证隔离性

在Pipeline的结构方面,由于非确定性测试的“传染性“,在着手解决非确定性测试之前可以考虑从测试套件中隔离这种类型的测试,这种隔离一方面可以保证正常的测试可以继续提供正确的反馈,另一方面也方便开发人员解决非确定性的测试问题,如果被隔离的测试失败,只需要重新执行部分测试而不是整个测试套件,很大程度地缩短了修复过程中的反馈周期。

从测试代码级别也需要保证不同测试之间的隔离性,构建的结果不应该依赖于测试被执行的顺序。在优化过程中我们遇到过这样的情况:基于UI的功能性测试依赖于一部分用户基础数据,其中测试T1在运行过程中需要修改特定用户的角色,在测试T2需要使用该用户完成其他的业务操作。如果T1在T2之前执行可以构建成功,反之则会构建失败。解决这类问题通常有两种做法,测试运行之前创建不同的用户或者测试运行结束之后恢复用户数据。对于第二种方法,如果当前测试没有正确地清理数据会导致下一个执行测试失败,增加了定位问题的难度,所以更推荐使用前者来保证不同测试之前数据隔离。

2.增加必要的等待

在UI测试中很多操作都依赖于页面元素出现的时间、位置等,在不同的网络环境、机器性能不同,页面的加载速度也不一样,测试运行的结果也会有所不同。通常web driver会提供一系列的方法来帮助开发者判断元素是否已经加载完成、是否可见、页面是否已经加载完成等(比如Watir的when_present, wait_until_present等),在测试代码中合适的地方使用这些方法可以让测试代码更加健壮,从而提升Pipeline构建的成功率。

3.正确测试异步行为

系统中的异步操作可以为用户提供更好的使用体验,系统不需要等待当前操作完成就可以继续处理其他操作,但是异步操作也增加了测试的复杂度。在项目的集成测试代码中我们发现类似这样的等待操作:sleep 10, 这种原始的等待策略不够稳定,对于网络状况、机器性能、数据量等外部因素依赖较大。回调(callback)和轮询(loop)是两种推荐的测试异步的方法,回调不会有任何尝试任何多余的等待时间,但是使用场景比较有限;轮询通用性更高但是会产生一定的多余等待时间,对于轮询操作,建议使用更小的等待时间间隔(interval)和重试(retry)上限。

调整测试结构

不合理的测试结构也是影响Pipeline性能的重要因素,根据测试金字塔理论,就测试数量来说,从低层级到高层级的测试需要保证金字塔状的结构。测试的运行时间呈现的却是一个倒金字塔状,测试的层级越高测试运行的时间越长,对应Pipeline的构建时间也越长。所以改进Pipeline也可以从调整测试层级结构开始。

1.梳理业务流程,简化测试结构

新的功能在不断增加,已有的需求也不断在变动,产品本身也不断接受来自最终用户和市场的反馈,现有的测试有可能并没有覆盖那些最有价值的场景而已经覆盖的场景也许在真正的产品环境下使用率很低;有些场景已经在低层级的单元测试覆盖,在高层级测试中出现了很多重复的用例。

调整测试结构可以和领域专家一起,重新梳理业务流程,把测试的重点放在那些最有价值的业务场景上,在高层级增加适当的UI测试保证核心功能没有被破坏。对于出现重复或者价值不大的测试,可以考虑删除高层级的测试,用更多的单元测试来替代,从而降低测试的运行时间。关于自动化测试更多的优化手段可以参考一个遗留系统自动化测试的七年之痒

除此之外还需要构建有效的反馈回路,通过Google Analytics等网站分析平台收集来自于最终用户和市场的数据、用户使用习惯、时区语言等地域性信息对应地调整现有的测试结构,让测试环境下的业务场景更加接近真实的产品环境。

2.用契约测试替代集成测试

“Integrated tests are a scam. A self replicating virus that threatens the very health of your codebase, your sanity, and I’m not exaggerating when I say, your life.” – JB Rainsberger

JB Rainsberger的这个说法一点也不夸张,在项目中总是有关于集成测试的各种“吐槽”,构建时间慢、问题难以复现和定位、修复难以验证、不稳定等。而契约测试就是那个可以拯救你,让你脱离“苦海”的利器。

契约测试是“单元级别”的集成测试,基于消费者驱动的契约测试把契约测试分为了两个阶段:消费者(Consumer)生成契约和提供方(Provider)验证契约,在生成契约时通过Mock隔离真实的服务提供方,运行单元测试生成用JSON描述的契约文件;服务端验证只需要部署自身就可以验证契约文件的正确性。

契约测试有很多优点,首先它不依赖于完整的集成环境,部署成功率高,其实在测试运行期间无真实的API调用和模拟的UI操作,测试运行的速度快,成功率高;而且在本地开发环境就可以验证契约测试,问题容易定位,修复的反馈周期短。引入契约测试不但给Pipeline性能带来大幅度的改善,还可以提升整个团队的开发效率。

如何保护改进成果

Pipeline是软件交付的命脉,为了保证每一个功能需求可以长期稳定、源源不断地通过,在优化过程中我们引入了关于Pipeline性能的监控机制,我们基于ThoughtWorks的开源产品GoCD 提供的API开发了一个监控工具,每一次构建之后可以自动统计该次构建的时间和成功率,如果超过这两个指标超过了阈值,则让该次构建失败,提醒代码提交者检查是否引入了影响Pipeline性能的变更,避免性能的进一步恶化。考虑到网络和机器性能的问题,在设置实际的阈值的时候可以稍大于期望值。

我们还构建了基于邮件通知的预警机制,每个工作日的下午发送出包数量通知,提醒团队解决影响出包的问题,同时我们把Pipeline的性能可视化并纳入每周的周报中。

写在最后

优化Pipeline除了Pipeline结构、测试策略和监控可视化手段之外,还需要关注软件架构和团队组织结构,下面是我们项目架构的局部依赖图:

核心的微服务OrderAPI依赖复杂,和RatingSrv之间甚至出现了双向依赖,领域上下文(Bounded Context)的不合理切分使得业务逻辑散落在不同的服务之间,不管我们增加集成测试还是契约测试,这种依赖关系也同样会体现在Pipeline之间的依赖关系上,增加Pipeline的复杂度。

在ThoughtWorks技术雷达上A single CI instance for all teams目前处于Hold状态,在一个组织中多个团队共享一个臃肿的CI会导致很多的问题,比如上文中提到的构建队列过长,构建时间长等,一旦这个共享的Pipeline出现问题会造成多个团队工作 的中断。技术雷达建议在具有多团队的组织中由各个团队分布式地管理自己独立的CI。这种分布式的CI同样也依赖于整洁的软件架构和与之相契合的团队组织形式。

在项目的DevOps小组解散之后,我们成立的项目内部的DevOps Community,以保证产品交付为目标,同时肩负着项目提高内部DevOps技能的职责。项目内部的成员有跨多个开发团队的不同角色组成,DevOps community产生的相关Task,最后都会分配到不同的开发团队中。DevOps是一种文化而不应该是一个单独的小组,DevOps主旨在于构建整个团队中的责任共享的文化,改进现有的流水线是每一个开发团队都需要具有的技能之一。


更多精彩洞见,请关注微信公众号思特沃克

Share

CoreOS那些事之系统升级

前段时间在DockerOne回复了一个关于 CoreOS 升级的提问。仔细琢磨来,这个问题还有不少可深入之处,因此有了此文,供已经在国内使用 CoreOS 的玩家们参考。

具有CoreOS特色的系统升级

CoreOS的设计初衷之一就是“解决互联网上普遍存在的服务器系统及软件由于没有及时升级和应用补丁,造成已知漏洞被恶意利用导致的安全性问题”。因此,它的升级方式在各种Linux发型版中可以说是独树一帜的,特别是与主流的服务器端系统相比。

平滑升级

一方面来说,常用的服务器系统如RedHat、CentOS、Debian、Ubuntu甚至FreeBSD和Windows Server都存在明确的版本界限,要么不能支持直接在线升级至新的发行版本,要么(如Debian/Ubuntu和Windows 7以后的版本)虽能够跨版本升级却容易出现兼容性风险,一旦升级后出现故障往往面临进退两难的局面。

这个问题在一些新兴的Linux发行版,如Arch Linux已经有了较先进的解决方法:将过去累计许多补丁再发行一次大版本的做法变为以月或更短周期的快速迭代更新,并由系统本身提供平滑升级和回滚的支持。这样,用户可以在任何时候、从任何版本直接更新至修了最新安全补丁的系统。然而,这些以 Arch 为代表的平滑升级系统还是带来了一些更新系统后无法使用的事故,不妨在百度以“Arch 升级问题”关键字搜索会发现许多类似的抱怨。事实上,Arch的目标用户主要是喜爱尝鲜的Linux爱好者而不是服务器管理员或者服务端应用架构师。

那么平滑升级的思路是不是在服务器系统就走不通了呢。其实仔细分析平滑升级出现问题的原因,当中最关键的一个因素在于,系统设计时最多只能确保从一个干净的系统顺利升级的途径,如果用户对系统中的某些核心组件做了修改(比如将系统中的Python2升级成了Python3),它就不属于操作系统设计者控制范围内的工作了。这样相当于设定了一个售后服务霸王条款(只是个比喻,这些Linux系统其实都是免费的):自行改装,不予保修。

在过去,用户要使用服务器系统,他就必然需要在上面安装其他的提供对外服务软件和程序,因此对系统本身有意无意的修改几乎是无法避免的。这个问题直到近些年来应用容器(特别是Docker)的概念被大规模的推广以后才出现了新的解决思路。而CoreOS就是通过容器巧妙的避开了用户篡改系统的问题,提出了另一种解决思路:让系统分区只读,用户通过容器运行服务。不得不说,这简直就是以一个霸王条款替代了另一个霸王条款,然而这个新的“条款”带来的附加好处,使得它被对稳定性安全性都要求很高的服务器领域而言接受起来要心安理得的多。

“反正许多东西都要自动化的,套个容器又何妨。” 恩,就这么愉快的决定了。

自动更新

另一方面来说,除了系统的大版本升级,平时的系统和关键软件的小幅补丁更新也时常由于系统管理员的疏忽而没有得到及时运用,这同样是导致系统安全问题的一个重要因素(比如2014年BrowserStack中招的这个例子)。

这个解决思路就比较简单了:自动更新。这么简单的办法当然早就被人用过了。即便在操作系统层面还见得不多,在应用软件上早都是烂熟的套路。那么,为了不落俗套,怎样把自动更新做得创意一些呢。先来看看系统升级都会有哪些坑。

乍一看来,操作系统这个东东和普通应用在升级时候会遇到的问题还是有几分相似。比如软件正在使用的时候一般是不可以直接热修补的,系统也一样(Linux 4.0 内核已经在着手解决这个痛点了,因此它在未来可能会成为伪命题)。又比如软件运行会有依赖,而系统的核心组件之间也是有依赖的,因此一旦涉及升级就又涉及了版本匹配问题。除此之外,它们之间还是有些不一样的地方。比如许多应用软件其实可以直接免安装的,升级时候直接把新文件替换一下旧的就算完成了。操作系统要想免安装,则需要些特别的技巧。

下面依次来说说CoreOS是怎样应对这几个坑的。

既然系统不能热修补,就一定会牵扯到重启的情况,这在服务器系统是比较忌讳的,为了避免系统重启时对外服务中断,CoreOS设计了服务自动迁移的内置功能,由其核心组件Fleet提供。当然这个并不是一个完美的方案,相信未来还会有更具创意的办法替代它的。

版本匹配的问题在应用软件层面比较好的解决方法还是容器,即把所有依赖打包在一起部署,每次更新就更新整个容器的镜像。同样的思路用到操作系统上,CoreOS每次更新都是一次整体升级,下载完整的系统镜像,然后做MD5校验,最后重启一下系统,把内核与外围依赖整个儿换掉。这样带来的额外好处是,每次升级必然是全部成功或者全部失败,不会存在升级部分成功的尴尬情况。

要想免安装软件那样直接重启换系统会遇到什么问题呢?两个方面,其一是,应用软件是由操作系统托管和启动的,可以通过系统来替换他的文件。那么操作系统自己呢,是由引导区的几行启动代码带动的,想在这么一亩三分地上提取镜像、替换系统、还想搞快点别太花时间,额,那真是螺蛳壳里做道场——排不出场面(还记得Window或者Mac电脑每次升级系统时候的等待界面么)。其二是,系统升级出问题是要能回滚的啊,不然怎么在生产环境用?即便不考虑启动时替换文件所需要的时间,万一更新过后启动不起来,原来的系统又已经被覆盖了,我天,这简直是给自己埋了一个地雷。由此可见,想要实现快速安全的升级,在重启后安装更新的做法从启动时间和回滚难度的两个方面看都不是最佳的办法。

0410001

为此,CoreOS又有一招绝活。进过CoreOS的主页的读者应该都见过上面这个A/B双系统分区的设计图。正如图中所示,CoreOS安装时就会在硬盘上划出两块独立的系统分区(空间大致为每个1GB),并且每次只将其中一个在作为系统内核使用,而后台下载好的新系统镜像会在系统运行期间就部署到备用的那个分区上。重启的时候只需要设计个逻辑切换两个分区的主次分工即可,不到分分钟就完成了升级的过程,要是真出现启动失败的情况,CoreOS会自动检测到并切换回原来的能正常工作的分区。用事先部署好的分区直接替换启动的方法避免重启后临时安装更新,这种思路的转换,确实有点神来之笔的意思。

说个题外话。之前有一次我和其他的CoreOS爱好者在Meetup活动时聊到对于双系统分区的看法,当时大家得出较一致的结论是:既然还是必须重启,用不用两个分区用户都没有实际获益,相比之下,“平滑升级才是卖点,双分区只是噱头”。我在《CoreOS实践指南》系列里也曾表达过类似的观点。一直到后来自己仔细反思了这种设计的巧妙,才发觉原先想法的片面性,实在贻笑大方。

这些方法说起来蛮轻松,若要真的实施出来,就不是拍拍脑袋那么容易了。纵观Linux开源系统百家争鸣,真正实现了这样后台更新设计的系统也仅CoreOS一枝独秀。

升级参数配置

理解了CoreOS的自升级方式,继续来说说与升级相关的配置。CoreOS系统升级有关的选项通常会在首次启动服务器时通过 cloud-initcoreos.update 项指定,系统启动后也可以在 /etc/coreos/update.conf 文件里修改。可配置的属性包括三个:升级通道升级策略升级服务器。这三个属性在DockerOne的回答中都已经提到,下面将在此基础上再略作深化。

初始化升级配置

这是最常用的配置升级参数的方式,系统首次启动时cloud-init将完成大多数节点和集群相关的初始化任务。与CoreOS升级有关的部分是coreos.update下面的三个键,其内容举例如下:

coreos:
  update:
    reboot-strategy: best-effort
    group: alpha
    server: https://example.update.core-os.net

其中只有group一项是必须的,它指定了系统的升级通道。升级策略 reboot-strategy的默认值是best-effort,而升级服务器server的默认值是CoreOS的官方升级服务器。

修改升级配置

对于已经启动的集群,可以在/etc/coreos/update.conf配置文件中对升级参数进行修改,其内容格式简单明了。举例如下:

GROUP=alpha
REBOOT_STRATEGY=best-effort
SERVER=https://example.update.core-os.net

同样,大多数情况下用户只会看到GROUP这一个值,因为只有它是必须的。其余的两行可以没有,此时会使用默认值代替。

需要注意的是:

  • 每次修改完成以后需要执行sudo systemctl restart update-engine命令使配置生效
  • 修改一个节点的配置并不会影响集群其他节点的升级配置,需要逐一单独修改
  • 最好让集群中的节点使用相同的升级通道,方便管理,虽然混用通道一般不会直接导致问题
  • 优先选择用cloud-init。在初始化时就将系统参数设计好,减少额外修改的工作量

升级通道

升级通道间接的定义了CoreOS每次升级的目标版本号。这个思路大概是从Chrome浏览器借鉴来的,官方提供三个升级通道:Alpha(内测版)、Beta(公测版 )和 Stable(正式发行版)。举个例子来说,如果用户配置的是Alpha通道,那么他的每次更新就会升级到当前最新的内测系统版本上。内存版本类似于Chrome浏览器的所谓“开发版”,会第一时间获得新的功能更新,稳定性一般还是蛮可以的,但不适合做为产品服务器,主要面向的对象是喜爱新鲜的开发者和玩家。公测版稳定性略高,也会比较快的获得新功能的推送,适合作为项目开发测试环境把玩。正式发行版中的组件往往都不是最新版本的,但其稳定性最高,适合作为产品服务器使用。CoreOS目前采用一个整数数字来表示版本号,数字越大则相对发布时间越新。

各通道发布更新的频率依次为(见官方博客声明):

  • Alpha:每周星期四发布
  • Beta:每两周发布一次
  • Stable:每个月发布一次

每个通道当前的系统版本号及内置组件版本号可以在这个网页上查看到。

除了三个公开的通道,订阅了CoreUpdate服务的用户还可以定制升级自己的通道,但这个服务是付费的。此外,使用了企业版托管CoreOS系统的用户也可以免费使用此功能,企业版的起步费用是10个节点以内 $100/月,见这个链接。还有另一个土豪企业版服务起步价是25个节点以内 $2100/月,差别就是提供额外的人工技术支持服务,果然技术人才是最贵的东东。

升级策略

升级策略主要与自动升级后的重启更新方式有关。它的值可以是 best-effort(默认值)、 etcd-lockrebootoff。其作用依次解释如下:

  • best-effort:如果Etcd运行正常则相当于 etcd-lock,否则相当于reboot
  • etcd-lock:自动升级后自动重启,使用LockSmith 服务调度重启过程
  • reboot:自动升级后立即自动重启系统
  • off:自动升级后等待用户手工重启

默认的方式是best-effort,通常它相当于etcd-lock策略,重启过程会使用到CoreOS的LockSmith服务调度升级过程。主要是防止过多的节点同时重启导致对外服务中断和Etcd的Leader节点选举无法进行。它的工作原理本身很简单,通过在Etcd的 coreos.com/updateengine/rebootlock/semaphore 路径可用看到它的全部配置:

$ etcdctl get coreos.com/updateengine/rebootlock/semaphore
{
    "semaphore": 0,
    "max": 1,
    "holders":
    [
        "010a2e41e747415ba51212fa995801dd"
    ]
}

通过设定固定数量的锁,只有获得锁的主机才能够进行重启升级,否则就继续监听锁的变化。重启升级后的节点会释放它占用的锁,从而通知其他节点开始下一轮获取升级锁的竞争。

除了直接修改Etcd的内容,CoreOS还提供了 locksmithctl 命令更直观的查看LockSmith服务的状态或设置升级锁的数量。

查看升级锁的状态信息:

$ locksmithctl status
Available: 0     <-- 剩余的锁数量
Max: 1           <-- 锁的总数
MACHINE ID
010a2e41e747415ba51212fa995801dd  <-- 获得锁的节点

其中获得锁的节点就是已经已经下载部署好新版本系统,等待或即将重启(与升级策略有关)的节点的Machine ID。用locksmithctl set-max 命令可用修改升级锁数量(即允许同时重启升级的节点数量):

$ locksmithctl set-max 3
Old: 1
New: 3

此时若再次用locksmithctl status查看状态就会看到 Max 的数量变成3了。

此外,locksmithctl unlock 命令可以将升级锁从获得锁的节点上释放,这个命令很少会用到,除非一个节点获得锁后由于特殊的原因无法重启(例如磁盘错误等硬件故障),因而始终占用这个锁。这种情况下才会需要手工释放。

升级服务器

许多希望在内网中使用CoreOS的用户都比较关心能否在内网搭建自己的升级服务器?答案是肯定的。

比较可惜的是,CoreOS 升级服务器是属于CoreUpdate服务的一部分,也就是说,它是需要付费使用的。不过考虑到通常会在自己内网搭建服务器集群的大都是企业级用户,收费也还算公道。

从文档资料来看,CoreOS所用升级服务器协议与Google的ChromeOS升级服务器是完全兼容的,甚至可以相互替代。比较有趣的是,两者都开源了各自的操作系统,但都没有开源其升级服务器实现,这个中意思仿佛是如果让用户去自己架设升级服务器,谁来保证这些升级服务器的镜像是最新的呢,那么自动升级提供的系统安全性的意义又何在了呢。

顺带说一句,在CoreOS的SDK中有一个 start_devserver工具 用于测试部署用户自己构建的CoreOS镜像(系统是开源的嘛),因此如果用户直接下载官方镜像提供给这个工具,应当是可以自己构建内网升级服务器的。但是官方文档对这方面的介绍比较模糊,我暂且抛砖引玉了,待高人给出具体方案。

手动升级系统

CoreOS始终会自动在后台下载和部署新版本系统,即使将升级策略设为off(这样只是禁止自动重启)。因此在绝大多数情况下,除非处于测试目的和紧急的版本修复,用户是不需要手动触发系统升级的。不过,大概是考虑到总是有新版本强迫症用户的需求(其实主要是系统测试的需求啦),CoreOS还是提供了手动更新的途径。

查看当前系统版本

相比手动更新,用户也许更想看到的仅仅是:现在的系统到底是部署的哪个版本啦。方法很简单,查看一下etc目录下面的 os-release 文件就可以了。

$ cat /etc/os-release
NAME=CoreOS
ID=coreos
VERSION=607.0.0
VERSION_ID=607.0.0
BUILD_ID=
PRETTY_NAME="CoreOS 607.0.0"
ANSI_COLOR="1;32"
HOME_URL="https://coreos.com/"
BUG_REPORT_URL="https://github.com/coreos/bugs/issues"

这个文件实际上是一个软链接,指向系统分区的 /usr/lib/os-release 文件,而后者是只读分区的一部分,因此不用担心这个文件中的内容会被外部篡改。

自动升级的频率

CoreOS会在 启动后10分钟 以及之后的 每隔1个小时 自动检测系统版本,如果检查到新版本就会自动下载下来放到备用分区上,然后依据之前的那个升级策略决定是否自动重启节点。OK,就这么简单。

具体的升级检测记录可以通过 journalctl -f -u update-engine 命令查看到。

手动触发升级

恩,下面这个命令是给升级强迫症用户准备滴。

命令非常简单:update_engine_client -update,如果提示 “Update failed” 则表示当前已经是最新版本(搞不懂CoreOS那班人为啥不弄个友好点的提示信息)。如果检测到有新版本的系统则会立即将其下载和部署到备用系统分区上。

$ update_engine_client -update
[0404/032058:INFO:update_engine_client.cc(245)] Initiating update check and install.
[0404/032058:INFO:update_engine_client.cc(250)] Waiting for update to complete.
LAST_CHECKED_TIME=1428117554
PROGRESS=0.000000
CURRENT_OP=UPDATE_STATUS_UPDATE_AVAILABLE
NEW_VERSION=0.0.0.0
... ...
CURRENT_OP=UPDATE_STATUS_FINALIZING
NEW_VERSION=0.0.0.0
NEW_SIZE=129636481
Broadcast message from locksmithd at 2015-04-04 03:22:56.556697323 +0000 UTC:
System reboot in 5 minutes!
LAST_CHECKED_TIME=1428117554
PROGRESS=0.000000
CURRENT_OP=UPDATE_STATUS_UPDATED_NEED_REBOOT
NEW_VERSION=0.0.0.0
NEW_SIZE=129636481
[0404/032258:INFO:update_engine_client.cc(193)] Update succeeded -- reboot needed.

部署完成后,如果用户的升级策略不是 off,系统会发送消息给所有登录当前的用户:“5分钟后系统将重启”。当然,你自己也会在5分钟后被踢出SSH登录,等再次登录回来的时候,就会发现系统已经变成新的版本了。

更好的升级策略

在看到CoreOS的4种升级策略时候,不晓得读者有没发现一个问题。前3种策略都会让新的系统版本下载部署后马上重启服务器,如果这个时候恰好是系统访问的高峰期,即使重启过程中,服务会自动迁移到其他的节点继续运行,仍然可能会造成短暂的服务中断的情况。而第4种策略索性等待管理员用户来重启系统完成升级,又引入了额外的人工干预,如果重启不及时还会使得集群得不到必要的安全更新。

有没有办法既让服务器不要在服务高峰期重启,又不至于很长时间没有更新呢?CoreOS给出了一种推荐的解决方法。我将它称为第5种升级策略:基于定时检测的自动重启。

这种升级策略没有在内置的选项当中,我们需要做些额外的工作:

  • 将升级策略设置成 off
  • 增加一个服务用来检测备用分区是否已经部署新的系统版本,如果部署了就进行重启
  • 增加一个定时器在集群的低峰时段触发执行上面那个服务

检测和重启服务

首先来看最关键的这个服务update-window.service,它会去执行放在 /opt/bin 目录下面的update-window.sh脚本文件。

[Unit]
Description=Reboot if an update has been downloaded
[Service]
ExecStart=/opt/bin/update-window.sh

这个脚本首先使用 update_engine_client -status 检测了备份分区是否已经部署好了新版本的系统。如果发现新的版本已经部署好,就根据 Etcd 服务是否启动来选择通过 Locksmith 调度重启节点(先获取锁然后重启动)或延迟一个随机的时间后重启节点,这样做的目的是防止太多节点在同一个时间重启导致集群不稳定。

#!/bin/bash
# If etcd is active, this uses locksmith. Otherwise, it randomly delays. 
delay=$(/usr/bin/expr $RANDOM % 3600 )
rebootflag='NEED_REBOOT'
if update_engine_client -status | grep $rebootflag; then
    echo -n "etcd is "
    if systemctl is-active etcd; then
        echo "Update reboot with locksmithctl."
        locksmithctl reboot
    else
        echo "Update reboot in $delay seconds."
        sleep $delay
        reboot
    fi
fi

定时触发服务

接下来添加定时器update-window.timer在集群访问的低峰时段触发前面那个服务,

[Unit]
Description=Reboot timer
[Timer]
OnCalendar=*-*-* 05,06:00,30:00

这个定时器Unit文件的功能类似于一个crontab记录,只不过对于用了Systemd启动的系统比较推荐使用这样的方式。上面的配置表示每天早上的5:00, 5:30, 6:00 和 6:30。

写到 cloud-init 里

既然是每个节点都要有的东东,当然要放到cloud-init 配置里面。把上面的内容统统写进去,看起来就是这个样子的了:

#cloud-config
coreos:
  update:
    reboot-strategy: off
  units:
    - name: update-window.service
      runtime: true
      content: |
        [Unit]
        Description=Reboot if an update has been downloaded
        [Service]
        ExecStart=/opt/bin/update-window.sh 
    - name: update-window.timer
      runtime: true
      command: start
      content: |
        [Unit]
        Description=Reboot timer
        [Timer]
        OnCalendar=*-*-* 05,06:00,30:00
write_files:
  - path: /opt/bin/update-window.sh
    permissions: 0755
    owner: root
    content: |
        #!/bin/bash
        # If etcd is active, this uses locksmith. Otherwise, it randomly delays. 
        delay=$(/usr/bin/expr $RANDOM % 3600 )
        rebootflag='NEED_REBOOT'
        if update_engine_client -status | grep $rebootflag; then
            echo -n "etcd is "
            if systemctl is-active etcd; then
                echo "Update reboot with locksmithctl."
                locksmithctl reboot
            else
                echo "Update reboot in $delay seconds."
                sleep $delay
                reboot
            fi
        fi
        exit 0

到这里,CoreOS升级相关的事儿已经侃得差不多了。不过总觉得还差点什么。

具有国内特色的CoreOS升级问题

在国内服务器用过CoreOS的用户大约都会发现一个比较忧伤的现象:好像CoreOS的自动升级没有生效捏?

相信不少用户大概已经猜到原因了吧。为了验证猜测,不妨做个手动升级试试。下面是我在国内的一个CoreOS集群上得到的结果:

$ update_engine_client -check_for_update
[0328/091247:INFO:update_engine_client.cc(245)] Initiating update check and install.
[0328/092033:WARNING:update_engine_client.cc(59)] Error getting dbus proxy for com.coreos.update1: 
GError(3): Could not get owner of name 'com.coreos.update1': no such name
[0328/092033:INFO:update_engine_client.cc(50)] Retrying to get dbus proxy. Try 2/4
... ...
[0328/092053:INFO:update_engine_client.cc(50)] Retrying to get dbus proxy. Try 4/4
[0328/092103:WARNING:update_engine_client.cc(59)] Error getting dbus proxy for com.coreos.update1: 
GError(3): Could not get owner of name 'com.coreos.update1': no such name
[0328/092103:ERROR:update_engine_client.cc(64)] Giving up -- unable to get dbus proxy for com.coreos.update1

看到最后输出Giving up的时候,整个人都不好了。现在来说说怎么解决这个问题。

使用HTTP代理升级CoreOS

既然是访问不到升级服务器,解决办法就很干脆了:翻墙。

首先得找一个能用的墙外HTTP代理服务器,这个…大家各显神通吧,记录下找到的地址和端口,下面来配置通过代理升级服务器。

创建一个配置文件 /etc/systemd/system/update-engine.service.d/proxy.conf,内容为:

[Service]
Environment=ALL_PROXY=http://your.proxy.address:port

ALL_PROXY的值换成实际的代理服务器地址,重启一下update-engine服务即可:

sudo systemctl restart update-engine

这个工作也可以在cloud-config里面用write_files命令在节点启动时候就完成:

#cloud-config
write_files:
  - path: /etc/systemd/system/update-engine.service.d/proxy.conf
    content: |
        [Service]
        Environment=ALL_PROXY=http://your.proxy.address:port
coreos:
    units:
      - name: update-engine.service
        command: restart

官方的服务器呢

其实一直有小道消息说,CoreOS公司已经在积极解决这个问题,预计2015年下半年会在国内架设专用的升级服务器。只能是期待一下了。

小结

“平滑而安全的滚动升级”和“无需干预的自动更新”既是CoreOS系统设计的初衷,也是一直是许多用户青睐CoreOS的原因。特别是在需要长期运行的服务器集群上,这些特性不仅节省了手工安装补丁和升级系统的成本,更避免了系统和核心软件升级不及时带来的安全性隐患。

希望这篇文章中介绍的内容能对大家理解CoreOS的系统升级相关问题提供一定参考和帮助。欢迎通过评论参与讨论。


本文转自:InfoQ ,作者为ThoughtWorks – 林帆

Share