討論交流
我的两分钱 2022-12-18 148 0 0 0 0
程序员,程序开发,发布反模式手工部署代码全写完了才向类生产环境部署生产环境的手工配置管理为什么要持续交付CycleTime:从决定开始变更的时刻开始,包括bugfix和增加新特性,到用户可以使用本次变更的结果(一个特性…


发布反模式

  • 手工部署
  • 代码全写完了才向类生产环境部署
  • 生产环境的手工配置管理


为什么要持续交付

  • Cycle Time: 从决定开始变更的时刻开始,包括bugfix和增加新特性,到用户可以使用本次变更的结果(一个特性只有交到用户手上才算Done)
  • 持续交付
    • 更好:通过大量的自动化来显著减少开发和发布过程的错误
    • 更快:通过持续集成、持续向客户交付构建一个高效的反馈循环,从而能尽量提前发现问题,小成本解决问题,缩短cycle time
    • 更省:像精益制造业一样,没有频繁交付的软件就是仓库中的库存。它已经花钱制造完了,却还没有为你赚钱,实际上保管它也是花钱的


配置与发布成熟度模型


持续交付的组成部分

  • 配置管理(以及基于配置自动化创建环境)
  • 部署流水线
  • 增量开发&持续集成
  • 测试,并且把能自动化的全部自动化,见测试策略部分的测试分类


配置管理

  • 配置管理是持续交付的基础,把与项目相关的所有东西都纳入版本管理
    • 用对待代码的方式对待配置,每个配置的变更都需要测试,都需要提交到版本库
    • 有些项目把项目需要的服务器、编译器、虚拟机等也都放入版本控制库中
    • 有些项目把整个项目包括操作系统做成一个镜像纳入版本管理
    • 注意,不要把源代码编译出的二进制文件也纳入版本管理库
  • 对于良好的配置管理,一个标志是二进制文件的创建过程是可重复的
  • 可配置性往往增加软件的复杂度,先专注于开发高价值、可配置性低的功能,然后再逐步增加配置项
  • 能在部署时对软件配置很重要
  • 管理配置最有效的方式就是集中管理配置信息,程序可以从一个中央服务获得其所需的配置信息
  • 配置信息
    • 配置信息和代码分别放在不同的仓库中,因为他们的变更频率不同
    • 是模块化且封闭的,对某些配置项的修改不会影响与之无关的配置项
    • 不要重复,每个配置项都有独立的、和其他配置项不重叠的含义
    • 应该尽可能简单且集中,如无必要,勿增实体
    • 需要测试,可以通过一个冒烟测试来确认相关的配置项能正确工作
  • 环境配置和管理
    • 环境的配置和应用程序的配置同等重要
    • 应用程序版本和环境版本的匹配关系也要纳入配置管理
    • 用自动化的方式,读取环境配置并自动创建目标环境
    • 要对环境的变更进行管理,尤其是生产环境,未经变更流程审批的变更要严格禁止,这看起来是官僚,但统计表明使用这种做法的组织中,其MTBF(Mean Time Between Failures,平均失败时间)和MTTR(mean time to repair,平均修复时间)更短
    • 要通过监控,时刻了解环境的健康度
    • 所有对环境的修改都应该通过自动化完成


部署流水线

  • 代码变更经过部署流水线的过程
  • 基本的部署流水线


  • 增量式构建一个部署流水线的过程
    • 先建模,创建一个简单的工作框架
    • 将构建和部署自动化
    • 将UT和代码分析自动化
    • 将验收测试自动化
    • 将发布自动化
  • 衡量部署流水线的指标
    • 最佳指标是cycle time, 最佳实践是缩短反馈周期,并将结果可视化
    • 其他指标有:自动化测试覆盖率、代码分析(重复代码、圈复杂度、afferent coupling、efferent coupling、代码风格等)、缺陷数、每天的提交次数、构建次数、构建耗时等
  • 在流水线上只生成一次二进制包
    • 如果在流水线每个节点都从源文件编译,不但速度慢,而且可能后续节点编译的二进制文件和之前环节测试的文件有差异(比如期间有源文件发生了变更)
    • 因为二进制包只生成1次,这就要求其必须可以在各个不同的环境部署,这也就要求你必须把代码和配置分开管理,促使你的程序结构更好
  • 制品库的要求是它不应该保存那些无法重现的产物,你应该可以删除制品库也不担心某个二进制产物无法重建
  • 对所有的环境都采用相同的部署方式,只需要把环境特定的配置分开管理就好了
  • 测试和持续集成环境和生产环境越接近越好
  • 只要有一个环节出错,就立刻停止整个流水线
  • 回滚流程有时候因为用的不多,所以常常没有被充分测试


将构建和部署自动化

  • 部署流水线的两个通用原则:快速反馈和状态可视化
  • 在项目刚开始的时候,可以把所有的步骤都写在一个脚本里面,即使那些尚未自动化的步骤也写一个占位步骤,随着脚本越来越大,拆分脚本,让流水线的每个步骤都有一个独立的脚本
  • 使用同样的脚本向所有的环境部署
  • 使用操作系统自带的包管理工具,手工管理文件目录非常繁琐、易错
  • 确保部署流程是幂等的
  • 总是使用相对路径
  • 对测试和生产环境的修改只能由自动化过程修改
  • 把编译后二进制文件的md5和对应源代码的版本放在数据库中跟踪
  • 在构建过程中,如果出现了某个错误,如果可以继续,就让构建流水线继续运行,以便运行后继更多的测试得到更多的反馈
  • 在部署前,先让部署脚本检查一下是否是在向目标机进行部署
  • 在超大项目中指定一个构建负责人,负责人轮值是个好的实践
  • 让构建具有可重复性,每次checkout代码,然后自动化构建,得到的二进制文件都应该是相同的
  • 尽量将需要管理的构建数量最小化
  • 如果在两次集成之间有多个组件发生了变化,那么一旦出错就很难定位,最好是每次一个组件构建成功就触发集成测试


将发布自动化

  • 每次都使用同样的方法,同样的流程,自动化地向所有的环境发布,在首次向生产环境发布的时候就应该使用自动化部署
  • 一旦部署完成,立刻运行一个冒烟测试来做一个快速验证
  • 成功发布的一个关键是做好依赖管理
  • 一些有用的发布策略:蓝绿发布、金丝雀发布
  • 频繁发布:也许你有很好的理由说不需要每次修改都发布一个版本,但仔细分析,可能实际站得住脚的理由很少
  • 对于存在客户端的情况,维护多个版本是很痛苦的,需要在程序设计的时候就考虑升级策略(比如Chrome总是在后台把更新程序下载好,一重启就完成了升级)
  • 对系统升级的测试也应该作为部署流水线的一部分
  • 有些程序冷启动需要一段时间(如预热缓存),在升级完成后,要注意冷启动期间流量分发的策略
  • 部署出现问题后,往往回滚是最有效的手段,不要尝试在线上修改bug,也不要尝试在线上修改配置
  • 努力把发布过程做成几分钟就可以完成的任务
  • 应用程序应该有一些监控的hook,以便随时了解业务异常或技术异常, 高质量的日志也有助于在应用程序发布后捕获异常


持续集成

  • 持续集成的目标是让软件始终处于可工作状态
  • 持续集成的前提:版本控制、团队共识、频繁提交、自动化构建、自动化测试
  • 没人每天至少应该向主干提交一次
  • 在提交前进行UT,UT的速度一定要足够快,最好在1分钟内完成,有时候,把一个简单的冒烟测试加到提交阶段也很好
  • 系统设计比较好的一个标志就是,很容易就能在开发机上把程序跑起来,从而进行单测
  • 在构建过程中可以加入一些检查,如测试覆盖度、重复代码、圈复杂度等,但注意执行时间一定要快,否则大家就不愿意频繁提交
  • 一些检查代码质量的开源工具:
    • Simian: 检查重复代码
    • JDepend/NDepend: 检查设计质量
    • FindBugs/CheckStyle: 检查烂代码
  • 将构建过程可视化
  • 集成失败就马上fix而不是继续提交新代码,在下班前务必保证构建处于成功状态
  • 如果某个测试运行超过一定时间就让他失败,测试的执行一定要快
  • 对分布式团队来说:首先是要建立信任
  • 在持续集成各个阶段要用到大量脚本,要注意脚本的可维护性,要注意把环境配置和脚本分离


测试策略

  • 对测试分类,然后大量应用自动化


  • 如果一个测试手工做了很多次,且这个测试不需要太多维护的话,这个测试就应该自动化
  • 每个User Story都应该有至少1个happy path的自动化验收测试,这些测试应该被开发人员用来做冒烟测试
  • UT
    • UT不应该与实际的基础设施、第三方服务、框架打交道,在系统的边界使用Mock(常见的Mock工具:Mockito, Rhino, EasyMock, JMock, NMock, Mocha),持续关注如何降低测试环境的复杂性,如果要让一个UT跑起来需要做非常复杂的准备工作,这往往意味着代码的设计需要改进
    • UT时测试替身的分类
      • dummy object 被传递但不被真正使用的对象,通常只是用于填充参数列表
      • stub 是在测试中为每个调用提供一个封装好的响应,它通常不会对测试之外的请求进行响应,只用于测试
      • spy是一种可记录一些关于它们如何被调用的信息的stub。这种形式的stub可能是记录它发出去了多少个消息的一个电子邮件服务
      • mock 是一种在编程时就设定了它预期要接收的调用。如果收到了未预期的调用,或者是预期的调用没发生,它们会抛出异常
      • fake object 是可以真正使用的实现,但是通常会利用一些捷径,所以不适合在生产环境中使用。一个很好的例子是内存数据库
    • 对于所有依赖于时间的行为,把对时间的请求抽象到一个你能控制的类中,这样UT才能方便进行(设想一个每天0点运行的定时任务该如何UT?)
    • 不要通过破坏代码的封装性来做测试,如果测试很难写,应该优化的是代码的设计
    • 如果为了让一个测试运行,需要复杂的前置工作来准备数据,那么就应该优化设计
    • 不要在UT中使用真正的DB,要么Mock要么使用内存DB
    • 在UT中避免用户界面,在代码中多用依赖注入
    • 在UT中避免异步,可以把异步调用包装(如wait and retry)成伪同步调用,让测试看起来是顺序执行的
    • 如果UT太耗时  
      • 可以在多台机器上并行执行测试
      • 如果只是个别用例特别耗时且不常失败,也可以把他们挪到验收测试阶段
  • 验收测试
    • 单元测试是从实现者的角度验证解决方案,而验收测试是从用户角度验证价值交付
    • 开发和维护自动化验收测试的成本远低于手工测试或者是质量风险
    • 不要尝试录制-回放式的自动化验收测试
    • 在没有开发人员参与的情况下,自动化的验收测试容易与UI紧耦合,应该尝试用一个抽象层来驱动UI或者直接用API来进行验收测试
    • 验收测试要用业务语言(DDD 中的UL)来表达,避免使用技术语言,避免引入与应用程序的交互细节
    • 将可测试性铭记于心,尽量不要在UI中包含业务逻辑,为应用程序提供一套API,使得GUI和测试程序都可以用它来驱动应用程序
    • 要抵制把生产数据的备份用来做验收测试的诱惑,维护一个受控的最小数据集,如果想在测试环境追踪生产系统的状态,花在数据上的时间要远远多于测试的时间
    • 理想的验收测试要具有原子性,测试用例的执行顺序应该对测试结果没有影响
    • 自动化验收的目的是为了尽早发现问题,因此自动化验收需要频繁且快速地执行,这就要求自动化验收运行在一个受控的环境上,而真正的外部环境则往往是不可控的,另一方面我们又希望验收测试的环境尽量接近生产环境,解决办法是让自动化验收运行在一个Mock的环境中频繁执行,然后低频率(比如每天1次或每周1次)让验收测试运行在真正的外部环境中
    • 要确保测试一直处于通过状态,如果发现问题,就把问题可视化,然后立即修复问题,否则这些测试就没有存在的意义
    • 验收测试同时也是对自动化部署的测试,如果发现了部署相关的错误,就让整个验收测试迅速失败,否则我们可以会得到一些难以琢磨的bug或运行时间超长的测试
    • 根据需要,可以创建一个快速失败case集,用来捕获一些高优先级bug,一旦出现这类bug,就让整个验收测试快速失败
    • 相对UT,验收测试的执行总是相对较慢,可以每次构建后,都找几个最慢的测试,花点时间优化其性能,
  • 容量测试
    • 自动化的容量测试应该作为部署流水线中一个单独的节点
    • 掌握抓大放小的技巧,避免过早优化
    • 有时在低配的机器上进行容量测试能更早地暴露问题
    • 对于性能关键的代码,可以在提交阶段的UT中加入一个衡量性能的测试,其目的不是为了评估最终的系统性能,而是通过该数据的变化趋势起到一个警示作用


数据管理

  • 数据库的初始化和迁移都需要脚本化,并放在版本管理库中,确保其可重复性和可靠性
  • 对哪个应用/服务使用了哪个数据库对象进行登记,如果有可能,可以用代码分析工具解析出这个关系
  • 向前兼容是个实用的策略,这可以保证旧版本程序可以工作在新版本的数据库schema上
  • 对数据库的修改,最好是增量修改,也就是只增加字段或新表,而尽量不修改已有的结构
  • 不要试图用生产库的一个副本来做测试,我们需要的是受控的测试数据,每次测试开始的时候,数据都应该是一样的,这样才好分析测试中发现的bug
  • 保证测试数据的独立性,每个测试需要的数据只对该测试可见,不要试图把一连串的测试case组成一个story,以使得后继的测试可以使用前面测试产生/修改的数据
  • 测试数据可以分为3类:
    • 测试专属的数据:需要专门准备,并与其他测试独立,尽可能使用应用程序的公共API来创建初始化数据
    • 测试的引用数据:和测试相关,但不真正影响测试行为,这类数据可以预先填充好
    • 应用程序的引用数据:和测试无关,但被应用程序所需要,这类数据可以随便填,反正对测试没有影响


依赖管理

  • 明确定义项目内部组件间以及和外部库的依赖关系,并把依赖关系纳入版本管理
  • 应用程序依赖的数据库schema的版本也要纳入版本管理
  • 两个好用的工具Maven, Ivy
  • 每个组件都应该有自己的构建流水线,理想情况下,一旦代码改动或上游依赖发生变化,就触发其构建,但如果你对上游依赖足够信任,也可以适当降低构建的频率,这是一个快速反馈和被打扰次数的平衡


增量开发

  • User Story要符合INVEST原则,Independent, Negotiatable, Valuable, Estimable, Small, Testable
  • 主干且增量开发的方法:
    • 把新功能隐藏起来直到其完成为止
    • 把新功能切分成一系列小功能,每个小功能都是可发布的
    • 通过抽象来模拟分支对代码库进行大的修改(抽象一个接口,把相关的地方改为对接口的调用,新老代码作为接口的两个实现)
    • 使用组件,根据不同部分的修改频率对程序解耦


分支策略

  • Feature Branching 和持续集成在根本上是矛盾的
  • 持续集成和持续发布的基础是主干开发
  • 推荐的方式是主干开发,把新功能拆解成能增量开发的点,频繁地向主干提交,每次提交前都运行单元测试集,在提交后都触发集成测试
  • 如果软件很复杂,将验收测试按照功能块分组也是可行的
  • 两个分支之间的间隔时间越长,在每个分支上工作的人越多,在合并分支时就越痛苦
  • 如果一定要创建分支,就只为发布创建长周期分支

    • 另外不要再在发布分支上创建子分支了,所以的分支都应该从主干上创建
    • 如果发布达到了一定频率,比如每周发布,就没必要为发布创建分支了
  • 在主干开发的大项目中,如果经常很多人要修改同一部分代码,这表明代码的设计耦合性太高
  • 如果你认为对代码库进行大规模修改无法用主干开发的话,我们相信你根本没有努力过
  • 按功能特性创建分支不是一个好想法,注意这和『功能团队』并不冲突,两者是正交的
    • 这个方式对有些开源项目是适合和,因为有大量的贡献者,但只有一个很小的核心团队,而且开源项目往往发布压力要小一些
  • 按团队创建分支不是一个好想法,只有各团队的组件非常独立耦合很小的时候才适用, 而且合并分支如果不频繁的话,持续集成的效果就会大打折扣

Tag: 程序员 程序开发
歡迎評論
未登錄,
請先 [ 註冊 ] or [ 登錄 ]
(一分鍾即可完成註冊!)
返回首頁     ·   返回[討論交流]   ·   返回頂部