高效团队开发
1. 什么是团队开发
待解决的问题
- “谁”“到何时为止”做了“什么事情”,“怎样”算做“完成”等;
- 在团队内部共享代码等各类工作成果;
- 保证各成员能够利用工作成果并行作业,同时防止工作成果遭到破坏;
- 在团队中共享从项目中学到的知识;
- 证明开发出的软件,在任何时间都是可以正常运行的;
- 构建自动化的工作流程,确保任何人都可以正确的开发、测试和发布;
如何解决问题
- 版本管理
- 缺陷管理
- 持续集成
- 持续交付
- 回归测试
2. 团队开发中的问题
重要的邮件太多,无法确定处理的优先顺序
使用邮件交流问题并不是一个好的方法,因为邮件算不上一种格式化的数据,很难归类整理;
解决方法:使用版本缺陷系统进行问题的管理;
没有能用于验证的环境
如果没有用于验证的环境,将导致每次复现 BUG 需要花费很长的时间,导致效率很低;
解决方法:搭建多个环境,分别用于开发、验证、测试、发布等;
用别名目录管理分支
用于实现谁、何时、做了什么样的修改;
重新制作数据库比较困难
数据库的变更操作也应纳入版本系统进行管理,确保每次的操作内容、顺序在各个环境都是一致的,而不是各个开发环境执行自己的;
不运行系统就无法察觉的问题
测试时,需要确保将全员的代码集中到一起运行,以免发生退化;
解决方案:持续集成,每次提交新代码,就合并代码并自动化测试,在第一时间暴露问题;
覆盖了其他组员修正的代码
当合并其他成员的代码时,如果出现冲突,有些开发人员可能直接将其他成员的代码进行覆盖,导致出现问题;
解决方案:持续集成;
无法自信的地进行代码重构
缺少措施避免出现退化;
解决方案:自动化测试;
不知道 BUG 的修正日期,也不能追踪退化
缺少缺陷跟踪系统,导致需要从一堆邮件中查询当时的情况;
解决方案:CI、缺陷跟踪、版本管理三个工具是确保项目高效开发的神器;甚至还应该考虑使用自动化部署;
没有灵活使用分支和标签
导致合并的时候容易出现混乱;有时在切换分支修复某些 BUG,切换回新功能分支时,忘了合并刚才的 BUG 分支;
在测试环境、正式环境上无法运行
缺少统一管理第三方模块,确保在各个环境的依赖实现一致性的办法;
解决方案: docker;
发布太复杂,以至于需要发布手册
涉及如何更新DDL、依赖的库以及配置文件;
解决方案:持续交付;
3. 版本管理系统
Git 等分布式版本管理系统
优点
- 能将代码完整地复制到本地
- 运行速度快
- 临时作业的提交易于管理
- 分支、合并简单方便
- 可以不受地点的限制进行协作开发
缺点
- 系统中没有真正意义上的最新版本
- 没有真正意义上的版本号
- 工作流程的配置过于灵活,容易产生混乱
- 有一定的学习成本
需纳入版本管理的内容
- 代码
- 需求定义和设计等文档
- 库的依赖
- 数据库初始化命令
- 环境配置文件
标签
Git 的每次提交有一个唯一识别码,但是它比较难记,为了让它更具备可识别性,可以为该提交添加标签,通过标签来识别;
虽然分支也可以使用标签,但我发现好像并不是很有必要,貌似直接用分支名就够了;
工作模式
中央集权型工作流
Github 型工作流
分支策略模式
Git-flow
主分支
- master:为发布而建的分支,每次发布时都打上标签
- develop:开发用的分支,发布前的最新版本
临时分支
- feature:分离自 develop,开发特定功能的分支
- release:分离自 develop,为发布做准备的分支,避免混入多余的 feature;
- hotfix:分离自 master,修复 master 分支的故障;修复后,需要被合并到 master\develop\release 三个分支;
有专门的 git-flow 脚本实现以上的管理模式;
git-flow 的缺点是有些复杂,需要学习适应一下;同时不支持 GUI 可视化工具;
Github-flow
git-flow 的优点是非常清晰可靠,缺点是看上去有点复杂不易理解,因此产生了 github-flow 模式来降低学习成本;
Github-flow 流程
- master 分支的内容都可以进行发布;
- 添加内容时,直接从 master 分支新建分支
- 建立的分支在本地环境上提交,并以同名的分支定期向远程代码库进行 push
- 开发结束后向 master 分支发送 pull request
- pull request 在被审核通过后,合并到 master,并从 master 向正式环境发布;
Jenkins 可以监控仓库中中的所有分支,这样每个分支 push 到仓库后,都会触发自动构建和测试,确保代码没有问题;
数据库模式和数据的管理
问题发生的原因在于如果不同的开发人员,在设置数据库初始化的命令顺序上面,可能存在冲突,导致一些功能无法正常运行;
数据库版本管理的必要条件
- 在任意环境中,都能使用相同的步骤来构建数据库
- 能够反复执行多次
- 格式为文件
数据库迁移
基本原理:将数据库初始化需要用的 SQL 命令写入文件,这些文件按数字进行顺序命令,每次启动程序时,按照这些文件进行数据库初始化;
由于是分布式开发,每个开发人员新添加的 SQL 文件可能存在命名冲突,当合并分支时,Git 会报错,此时需要解决冲突,并再次提效修改后的版本;由于修改后的版本是最新版本,因此其他人在合并该版本时,会自动覆盖其本地的版本,因此,仍然能够实现正确的初始化;
SQL 文件中的初始化命令是成对出现的,它提供了回滚机制,当出现冲突时,先回滚当前的数据库,再按照最新的版本,重新初始化;
由于冲突是通过手工合并 SQL 命令来解决的,因此不可避免存在错误的可能性,此时就需要通过增加测试代码,来确保万无一失;
配置文件的管理
配置文件包括环境变量、密码等信息,这些信息不适合纳入版本管理,因此需要单独上传部署的服务器,此时可以通过编写一个部署脚本,来实现自动上传;而上传的信息,同样可以写在配置文件中,而脚本本身可以纳入版本管理;
常用部署的工具:Chef, Puppet, Capistrano, Fabric, ServerSepc;
依赖关系的管理
大多数语言都有自己的依赖管理工具和公共仓库,例如 Java 的 Maven,Node 的 npm,Python 的 Pypi 等;
这些现成的工具的原理:
- 设置一个中央仓库;
- 使用一个文件来定义对库的依赖;
- 执行上述依赖文件的脚本;
4. 任务管理
任务管理系统的优点
- “有须做什么”的任务定义
- “谁来做”的职责分配
- “什么时候完成”的期限管理
- “作业中或已完成”的状态管理
其他优点
- 直观性
- 方便检索
- 对信息统一管理及共享
- 能够生成各类报表
- 能够与其他系统进行关联,具备可扩展性;
任务驱动开发
将新功能或者 BUG 任务登记在缺陷管理系统中,每次代码的提交,都与某一个具体的任务单相对应,禁止没有任务单号的提交;
通过设置 Github 的 Webhook,可以实现将 commit 和相应的任务单进行关联;
开发新功能、修改BUG的工作流程
- 建立任务单
- 指定责任人
- 开发
- 提交:提交的时候,记得标注对应哪个问题单号;这样可以通过问题单,查询到代码修改了哪些内容、什么时候修改的;也可以反向查询,即找到当前代码的修改,对应到哪些问题单,从而知道当时什么要做如此的修改;
- Push 到代码库
管理对象
- epic
- story
- task
- bug
其他
Redmine 安装
1 |
|
Redmine 访问
localhost:8080
5. CI 持续集成
主要的 CI 工具
- Jenkins:插件众多,可配置性强;缺点是上手成本高;
- Travis:需要配合 Github 使用,优点是上手简单;
build 工具
以 Java 为例,常用的构建工具有:
- Maven:适用于新项目;
- Ant:适用于已开发一半的项目;
测试代码的写法
常见的测试类型:
- 单元测试
- 集成测试
- 用户验收测试
- 回归测试
编写测试代码是要付出时间成本的,理想的情况下当然是覆盖以上所有的测试场景;如果不允许,则应至少包括单元测试和集成测试;
棘手的测试
- 和外部系统有交互的测试
- 使用 mock 框架进行测试
- 使用内存数据进行测试:可避免跟数据库中的数据产生耦合;常用工具如 H2 数据库;
- UI 相关的测试
Jenkins 使用流程
- 新建任务:一个任务对应一个项目;
- 下载代码:将 Github 代码地址与项目进行关联,并设置 Github 的 Webhook,在收到 push 请求后,调用 Jenkins 接口拉取最新的代码;
- 自动执行 Build 和测试:制作一个构建的脚本,并由 Jenkins 进行调用即可;脚本中需设置退出值,以便 Jenkins 判断任务的执行是失败还是成功;
- 统计结果并生成报表:使用 JUnitXML 形式输出报表有更好的通用性和直观性,虽然它是 JUnit 设定的格式,但其他语言也有相应的库可以生成该格式的报表;
- 统计覆盖率:常用的覆盖率统计工具有 Cobertura, Jacoco, Scct, simpleCov, Rcov 等(Cobertura 已于 2011 年停止了开发);
- 静态分析:常用工具 Checkstyle, PMD, Findbugs等;
- 配置通知:对构建结果进行通知,选择常用插件即可,支持邮件、Twitter、IRC、XFD等;
构建失败的惩罚
设计一个搞笑的仪式,例如警报灯闪烁、弹射球、戴礼帽等;
当构建发生失败时,基于该构建的分支之后编写的代码,将需要禁止提交,直到构建修复成功为止;为了避免出现这种等待的情况,可考虑使用 Github flow 中的 pull request 流程;当收到 pull request 后会自动进行构建,如果成功,则合并到主分支;如果失败,则不合并;
以上功能在 Jenkins 中需要使用 Github pull request builder 插件实现;
确保可追溯性
通过相应的插件,可实现 Jenkins 和 缺陷管理系统的任务单相关,并可以方便的查看每次代码提交的差异;
6. 自动化部署–持续交付
由谁负责
由想实施部署自动化的人着手去做即可,因此一般来说是运维人员牵头;
前提条件
- 全部团队成员都采用版本管理;
- 所有的环境使用相同的方式构建;
- 实现发布工作的自动化,并事先进行验证
- 要反复多次进行测试
工具链
- 引导:服务器 OS 的配置自动化;
- 配置:服务器及中间件的配置自动化;
- 业务流程:代码部署及发布的自动化;
引导
Kickstart
原理:安装 Linux 时,给内核参数加上 ks=<…> 选项,即可开启从外部设备加载配置文件,实现安装自动化;背后的本质是将安装过程中的选项,以配置文件的形式提前写好;
仅适用于 RHEL 系统的 Linux 系统,不适用于 Debian 系列;
Vagrant
用途:用来创建和配置虚拟环境,可以最大化的利用单台机器的性能;
配置
应用程序总是运行在一定的进程环境中,复杂的应用程序,可能涉及非常多的环境配置选项,从而带来很大的工作量;
Chef
根据提前写好的配置规则(cookbooks),让服务器安装软件包和配置中间件,实现自动化;
可以为应用服务器和数据库服务器分别编写 cookbook,这样就能够复制搭建服务器的步骤,实现批量化;
Serverspec
用途:一个测试框架,可对服务器的配置进行单元测试,确保服务器如预期的正常运行;
最佳实践1:使用虚拟环境
- 使用 Vagrant 创建干净的虚拟环境;(怀疑之处可考虑结合 docker 来创建虚拟环境);
- 拉取 Chef 的 cookbook 和 Serverspec 的测试用例;
- 执行 Chef,完成服务器的配置;
- 执行 Serverspec,完成对服务器的状态测试,确保配置成功;
- 将结果反馈给 jenkins;
以上各个工具都有相应的替代器,因此无须局限于以上工具的使用,重点在于选择最适合团队使用的工具;
最佳实践2:使用物理机
Kickstart + Chef + Serverspec 的组合;
与实践1的差别在于将 Vagrant 替换为 Kickstart;
发布自动化
Capistrano
Capistrano 使用 Push 的方式,无须在应用程序和数据库服务上面安装,只需执行服务器能够通过 SSH 登录前两种服务器即可;
Fabric
功能同 Capistrano,差别在于使用 Python,而非 Ruby;另外 Fabric 任务可以顺序执行,也可以并行,甚至还可以分组顺序执行,组内则是并行;相当灵活;
Jenkins
Jenkins 同样可以用来实现发布的自动化,不过需要在从节点上安装 Jenkins 才行,还好只需能够 SSH 登录从节点,即可在主节点上远程进行安装;
相对于前面两个工具,Jenkins 的优点在于:
- 可视化的控制台;
- 可实现发布任务的权限管理
- 可查询发布的详细历史记录;
最佳实践
组合使用 Jenkins + Fabric,这样既可以利用 Jenkins 的日志功能,又可以获得 Fabric 灵活易用的功能;
手动部署的工具
如果可能,尽量所有部署工作设置为自动化;如果出现少数需要手动部署的特例,例如某台机器磁盘空间不足,则有以下工具可以使用:
- RLogin
- Tera Term
以上两个工具可以实现一个终端输入,在多台机器上实施相同操作的效果;
当机器数据很多时,有时可能存在少数几台机器的命令执行不成功,此时在执行下一条指令前,应先就上一条指令的结果进行验证,确保无误后,再执行下一条指令;
其他相关问题
不中断服务的部署方法
蓝绿部署
原理:将机器分成两组,先部署其他的一组,成功后,再部署剩下的一组;
缺点:部署过程中会暂停一半的机器资源,会给系统带来比较大的压力,除非增加备用机器,但那样做的成功过高;
云蓝绿部署
原理:机器不分组,部署前增加一批新机器进行部署,部署成功后,删除旧机器;
该方法克服了传统蓝绿部署的缺点,但会增加复杂性和出错概率,因为务必要先实现部署的自动化和自动测试;
回滚
部署失败不可避免,因此应随时有回滚的机制,包括代码回滚、数据库回滚两种;
回滚时,除了回滚源代码外,还应以服务器的环境进行验证,确保可用;同时还需考虑数据库的迁移;
数据库的回滚分为两种情况,一种是允许新数据的丢弃,一种是不允许;后一种情况比较麻烦,因此,在发布新代码前,应分成两步进行测试,先确保只更新数据库的情况下,旧代码能够顺利进行;之后再进行新代码的发布;此时如果出现问题,也只需回滚代码部分,无须回滚数据库部分;
PasS
自动化部署需要花费一定的学习和时间成本,因此也有一个选项是考虑使用 PasS 服务;只需 push 代码到相应的平台,即可实现自动化部署;
PasS 适用场景
- 没有足够资源,但希望快速推出产品,收集反馈的项目;(创新项目)
- 无法预测峰值负荷的服务;(新上线的手机游戏)
- 生命周期短的服务(例如展会);
- 与现有产品进行配合的小项目;
PasS 缺点
- 当流量很大时,费用成本出现不成比例的上升;
- 有时难以获得想要的日志进行问题分析;
- 获得的服务等级和合同约定可能不相符;
7. 回归测试
回归测试
Selenium
Selenium 并不是单个软件,而是一套工具集;
几个常用的 Selenium 工具
Selenium IDE
以 Firefox 的插件形式出现,可以录制键盘和鼠标动作,因此对于非技术人员依然非常友好;
Selenium Remote Control
在测试脚本和浏览器之间,增加了一个服务器作为中间层;由于这个抽象层后,可以使用各种语言编写测试脚本,使得测试更加灵活,例如可以实现循环和分支等;
Selenium WebDriver
出于安全考虑,主流的浏览器都会对 Javascript 的调用进行限制,WebDriver 的目标即是绕开这些限制,实现调用 OS 的原生接口;在结合 Selenium 后,推出了 Selenium2;
制作测试用例
测试由测试用例组成,多个测试用例可以组合一个测试组;测试组内部的用例是顺序执行的;而测试组之间则是并发执行的;好的测试应该能够尽量缩短测试时间,因此,应考虑将测试用例设计成可以并行测试的模式;好的测试组之间不应相互依赖;
由于测试用例的执行是代码级别的速度,而网页的加载则跟网速有关,因此 Selenium
的自动化测试会容易出现失败;解决办法在于执行下一条指令前,应对上一条指令的结果进行确认;如果确认失败,应该重复上一条指令;如果确认成功,再开始执行下一条指令;
Jenkins 和 Selenium 的协作
安装相应的插件,然后配置好相应的参数即可;
Selenium 测试的高速化
- 利用 Jenkins 的分布式机制,设置主从节点,通过启动多个浏览器,实现并发测试;
- 为每个浏览器客户端匹配相应的应用程序服务器,实现服务端测试的负载均衡(不太理解为什么不使用 nginx 之类的工具来实现,而是改动 hosts 文件)
多个应用程序版本的测试
通过 Jenkins 的 Parameterized Trigger plugin”插件,可实现对指定版本进行测试;
使用 Git 拉取相应版本的测试用例时,需要配置插件选项,为每个版本生成相应的目录,避免不同版本的测试用例混在一起;
8. 实践
在 Github 上面新建一个项目
拉取该项目的代码到本地
初始化 package.json
npm install 安装依赖
简单写一些代码,实现 hello world
运行代码,确保顺利
写一些单元测试代码
写测试脚本
设置测试脚本的文件为可执行
推送代码到 Github
登录生产服务器,新增用户,安装git,拉取代码,安装依赖,运行应用
登录 Jenkins 服务器,新增用户,安装 Jenkins,启动 Jenkins
配置 jenkins,安装插件,更改密码,新建任务,配置构建脚本;
配置 Github 的 Webhooks,以便 push 后触发 Jenkins 的构建;
生成 SSH-KEY,将公钥存放到应用程序服务器,开放存放文件夹的访问权限;
创建部署文件夹,创建部署的自动化脚本,设置脚本为可执行