本来想先写“测试”的,但是如果要讲测试,就要讲如何部署测试环境,这个是需要“运维部署”相关的观念,所以还是先写“持续集成”比较好。而且,上一篇刚好是版本管理,和这个话题也关系密切
我最初为客户提供软件的方法,是带着一张 Delphi 的安装光盘,以及几张我写的 Delphi 工程源码目录。走到客户的运行电脑上,先安装一个 Delphi 开发工具,然后把需要的工程源码目录再拷贝上去,最后直接在客户的电脑上编译并且运行。最后,把应用程序的快捷方式放到桌面上,就大功告成了。之所以要在客户电脑上安装 Delphi,最主要的原因是,每次给客户试用应用软件,总会立刻提出一些修改意见,其中有一些简单的修改,譬如界面调整,我会希望立刻改完给客户再次试用。所以安装一套开发 IDE 是多次操作之后的一个经验。
“持续集成”的概率提出,是为了解决现代软件开发过程中,因为运行环境造成的问题,而提出来的。在这个“方法论”被提出之前,软件行业对这个问题的解决其实已经有很多年的探索和尝试。
软件从开发完成,到具体在生产环境上运行起来,其实还有很多工作要做:
一些软件需要以某种媒介进行分发,现在的软件大部分通过互联网分发,所以打包上传到各种软件商店,是一个重要的步骤。譬如我们常见的 Apple AppStore/Steam 等等,都需要按照商店的格式打包文件传到服务器,填写说明和各种配置等等。
有一些软件是服务器端软件,需要编译之后,按照运行要求部署到具体的生产环境的服务器上去。这里除了拷贝文件到成千上万的服务器硬盘上,然后启动软件以外,还需要做好数据库配置、打开防火墙端口、修改 DNS 或者其他目录服务器配置等等。
就算是一个简单的“单机”程序,你也需要构造一个软件安装包,然后放到类似 github 或者网盘之类的地方,更重要的是需要和客户说明,这个软件的可运行操作系统,需要提前安装的依赖库等等。
上面这些工作如果没有做好,很可能会出现大量的用户问题。我们经常可以发现,软件在客户的家里就是有问题,但是在程序员的开发环境就是好的。在一些软件的客服论坛上,也经常会有咨询某个 dll 文件找不到的问题帖子。而对于服务器端程序,我个人多年统计下来的结果,是线上事故有 70% 是出现在运维环节,只有 30% 的事故是真正由代码造成的。除了会影响软件运行过程中的故障率,发布部署及其环境造成的问题,还会严重影响我们的开发速度,很多大型软件每次编译都要好几个小时,有的祖传代码,甚至只能在某个固定的机器上编译,因为依赖的各种库的版本太旧了,已经找不到源码了;有的软件如果要搭建一套测试环境,可能需要耗费数天,我还经历过一个项目,负责开发部署程序的程序员离职后,根本没人可以自己部署一套内网的测试环境;
为了解决以上这些问题,我们必须对于软件开发完成之后的整个运行过程,有一套完整的方法论,来指导我们的工作。
正确的软件版本 :由于现代软件开发,往往是持续不断的进行迭代,所以每个版本里面的功能,是否都是经过测试而且确认是应该发布的,并不是一件简单的事情。我经常碰到,软件夹带了还未开发完全的功能,就发布出去了。
正确的运行环境 :软件的运行环境,除了操作系统以外,现在还会有很多其他各种依赖。譬如很多游戏需要 DirectX 库;C# 和 Java 的程序需要对应的虚拟机(Runtime Env);服务器程序需要合适的 Web Server/Database/MessageQueue 等等中间件。
正确的安装、部署过程 :很多软件对于安装的目录有要求,有些需要从环境变量或者命令行参数获得正确的数据。而很多程序还不知一个可执行程序,需要多个程序都被正确的运行或者杀死。这些都需要准确无误的安装、部署过程,才能让软件正确的运行起来。
可靠的故障发现和处理工具 :没有任何软件是完美的,所以我们需要一些工具,来帮助开发者了解故障的信息、系统的运行情况、以及出了问题后可以进行故障排除的工具。
这些要素,就是“持续集成”特别关注,并且着力通过专业化的工具,进行管理的目标。
相信大家多少都会接触过所谓软件版本号的概念,譬如什么“工业4.0”、“Web2.0”,这里面的“4.0”“2.0”就是来源于软件的版本号。但其实软件版本号并没明确的标准化定义,譬如微软就用过“DOS 3.3”“Windows 4.2”这种“X.X”的版本号,也用过“Windows NT”“Windows XP”这种类似产品型号作为版本号,现在又回到了数字,但是没有小数点“Winows 10”“Winows 11”。
一般来说,现在的软件开发者比较喜欢用“三段式”的数字来编写版本号,如“2.33.5”这种写法:这里的第一段“2”表示软件的比较大的功能更新,第二段“33”表示比较小的软件特性修改和更新;第三段“5”则用来作为 BUG 修正的次数。不过,什么叫做大的功能更新,什么叫做小的更新,这就非常随便了。譬如 "Python 2.7" 和 "Python 3.3",第一位的版本号不一样,代表了语法都变化了,差异大到无法兼容。而有些软件则在第一段版本号狂加,譬如 AutoCAD 这类。
确定标识 :在开发过程中,对于某段时间的开发目标或者里程碑,应该定一个大家都知道的、唯一的标识,我们可以叫他“发布版本号”。这个“发布版本号”的内容可以随便设计,可以用两段式数字如 2.34 。一旦确定了,就需要通知所有人使用这个标志。
分支标识 :我们应该用这个标志作为发布前集成功能代码分支的名字,如 release/2.34 ;如果有修补 bug 的版本,则使用分支 hotfix/2.34.x ;
环境标志 :如果要进行发布验收测试,其测试环境的名字,以及配置所在的版本分支,都应该统一使用“发布版本号”,如 2.34 。如果在不同的服务器(集群)上,部署了不同版本的软件,这些服务器(集群)的配置数据上,也应该使用这个“发布版本号”。
利用 SVN/GIT 这类版本管理工具,对源码进行管理是一个常见的方法。更重要的是,要对分支功能的合理规划。一般来说,最少应该有两个稳定的分支,一个代表用户现在使用的版本,一个代表现在开发的最新进度版本。对于比较复杂的项目来说,多个功能同时开发、测试的情况也是经常存在的,利用分支来区别多个开发"生产线”是必须的。而且必须在开发团队中,采用必要的手段和角色来让这些事情规范化。
init 初始化过程,建立项目最初的分支结构:master/develop
feature start/feature finish 每个特性开始,都会从 develop 新建一个 feature/xxxx 的分支;当开发工作完成,则会把这个分支合并到 develop,并且删除当前 feature 分支
release start/relase finish 每次发布一个正式版本时, 用 start 命令输入对外的“发布版本号”,如 2.34 ;此命令会从 develop 新建一个 release/xxxx 分支。这个分支是发布前的测试、bugfix 专用分支。一个产品可以同时有多个 feature 分支和 release 分支在运行。一旦 release 分支的测试完成,就可以用 finish 命令,来向 master 分支进行合并。合并完成后,当前的 master 分支将打上一个 tag,名字就是“发布版本号”。后续的安装部署,就是根据这个 tag 名字,从 git 库中提取代码进行编译。
hotfix start/hotfix finish 如果发现正式版软件有 bug,就可以根据发布版本号的 tag,拉出一个 hotfix/xxxx 的分支进行问题复现和修复,完成之后的 finish 命令,会自动把修改合并到 master 和 develop,确保修过的 bug 不会重新出现。合并后的 master 分支会自动打上一个标记的 tag。然后的修正版的,就可以从这个 tag 开始提取代码编译发布。
尽管 git flow 不能称之为完美的版本管理流程,但确实是比较完整的覆盖了开发的各个步骤。在开源社区上,还有一种叫做 Merge Request 的版本管理策略:每个开发者都可以从 master 上拉取分支,然后开发完之后通过网络,向社区提交一次“合并申请”,如果通过了,代码就会进入“主”分支。这种方式比较适合松散的,以 bugfix 为主要开发目标的项目。GitHub 就是提供这种开发模式的一个网站,吸引了大量的开源软件入驻。如果软件有明确的特性需要开发,可能还是要专门拉出特性分支来开发。
任何可以运行我们开发的软件的软硬件设备系统,都可以称为一个“环境”。这包括了硬件、操作系统、中间件、网络配置等等。
开发环境 :一般我们会运行 IDE,编译器或者其他开发工具。大多数情况下是程序员自己的电脑,也有一些程序是通过网络在服务器上进行开发。有时候我们的电脑还会连接手机或者其他设备,用以运行我们的开发程序。
测试环境 :提供给各类测试所使用的专用环境,可能是一台专用的服务器,也可能是一台专用的测试手机。虽然我们可以直接在开发电脑上运行诸如单元测试一类的测试,但是很多测试工作是需要专门的环境的,特别是由专职的 QA 人员运行操作过程的情况下。
生产环境 :就是最终软件使用客户的环境,有些是客户的手机或者电脑,有些则是拥有互联网接入的服务器。
一般来说,每个开发者都会有至少一套自己的开发环境,这个环境应该和其他开发者分开,避免互相干扰。要能在自己的专用电脑、服务器上直接编译、运行,而不是把代码弄到其他什么系统上编译或者运行,才是效率最高的做法。我见过有一些可编程或者可配置的系统,并不能在自己的电脑搞一套,一定要到指定的服务器上运行,这可太折磨人了。有些云厂商,尤其喜欢这样做,殊不知这是严重影响开发效率的做法。正面的例子也有,譬如 Google 的 AppEngine,在你上传代码之前,是可以在本地环境中编译和运行的。如果不能在本地环境运行,还有一个问题就是难以调试。我们知道大多数的调试器都是支持本地运行调试,而远程的调试除了容易受网络影响外,还有很多厂商根本就不支持。对于由客户端程序和服务器端程序组成的软件来说,最好的方法也是让本地的开发环境,可以同时运行这两类软件,这样有通信问题的时候,可以一个人自己完成联调工作,这比两人来回联调要效率高很多。归根到底,开发环境除了满足开发者自己的喜好以外, 最重要的就是能独立的运行所开发的程序 。
出于安全角度,有一些公司的开发环境,是和互联网隔离的。这种做法确实能减少大量来自外网的黑客和木马攻击,但是也会让那些设计使用互联网作为包管理和软件管理的系统举步维艰。譬如 python、go 这些语言的模块库就依赖于互联网。其中一个做法是在开发环境的网络中全部做一套镜像,然后搭建一套对外请求的代理服务器;对于员工无法接入实体公司内网的情况,则提供一套 VPN 系统给员工。——这也是很多互联网大厂的做法。前些年,由于苹果公司官网特别慢,所以国内有的 XCode 被坏人下毒植入了木马,导致了大量知名软件爆出安全问题,确实不可不防。优秀的开发团队,会高度重视对开发环境的建设,而不仅仅依赖程序员自己捣鼓一下,甚至放任大家使用互联网上来路不明的各种资源。——每年都有一些不小心的程序员,把自己公司的项目代码放到 GitHub 上泄漏了出去,有的还代码还包含了数据库密码之类的敏感信息。
根据软件工程的另外一门知识:软件测试理论,测试包含很多方面,譬如单元测试、集成测试、验收测试、冒烟测试、性能和压力测试、探索性测试等等。由于每种测试,需要准备的输入数据,测试用例的不同,所以测试环境也非常不同。对于软件版本来说,一般会设计一些专门的代码分支,来专门用于测试验证,因为测试的过程必定伴随 bugfix 等修改代码的过程,为了避免和其他的特性开发或者性能优化、重构的代码修改搅合到一块,肯定是需要单独的代码分支的。
如果项目不太复杂,测试环境也建议最少保留两套,一套所谓 内测环境 ,提供给开发团队的程序员、QA、产品人员使用,一般用于联调、功能初步验收、冒烟等测试,其软硬件环境以速度快效率高为标准,通常都会部署在公司内部。第二套环境叫做 外测环境 ,这个环境又叫老板环境,简单来说就是给外部用户来展示开发进度的,这个环境往往需要部署的和真实的生产环境尽量的接近,以免影响对软件的评估。
需要特别注意的是性能和压测环境,这种环境一般软硬件都比较特殊,因为要模拟大量、高频的使用,所以最少要和其他环境隔离开,避免大量的网络流量冲击其他人的系统。我就碰到过有人直接在生产环境指定几个电脑开始做压力测试的,由于没有隔离,导致压力测试所在电脑的交换机满负荷了,整个网络几乎都堵塞了。性能测试环境还需要对软硬件配置进行仔细的记录,因为性能调优很可能会通过修改这些配置来验证是否有效。这些配置也是一种性能的基准数据,用于指导我们准备正式环境的资源。
生产环境对于服务器端程序来说,有可能存在很多个“服”。很多软件针对不同的客户、运营渠道甚至是功能,会同时部署很多套环境。譬如很多游戏会部署“IOS 微信”“IOS QQ”“安卓微信”“安卓 QQ”四套生产环境,以应对运营商的用户隔离要求。如果是这种情况,就必须要有一种可靠的区分不同生产环境的方法。原始的方法是由运维人员,根据服务器的 IP 地址来确定应该部署何种环境的系统。更好的做法,是对所有的服务器资源,编制一份完整的、全局的资源列表,然后统一对所有的资源进行分配。不同环境的资源,都记录在同一个配置中之后,就可以通过一些自动化手段,根据这份统一的资源配置表,进行软件的部署。
对于客户端程序,生产环境可能是和客户端操作系统和硬件有关的,譬如 PC,IOS 手机,安卓 Pad 等等。除了这种系统级别的差别,有时候我们还会区分“高性能环境”和“低性能环境”,譬如比较便宜的或者老旧的手机,就算“低性能环境”。客户端程序的发布,也是需要按照这些定义的环境进行各自准备。同样的,维护一份统一的客户端环境定义,也是相当重要的。
所有的这些关于生产环境的识别、区分、标记,都最终需要在软件的安装部署环节,根据这些环境信息针对性的进行处理。
一个软件从源代码变成一套可以运行的系统,除了程序以外,就是要和依赖环境交互。最基本的依赖环境就是操作系统,还有网络、中间件等等。任何的软件安装部署之前,都应该有一些识别所在环境的能力。所需要识别的信息,一般包含以下几个层次:
基础信息 :包括硬件的 CPU、内存,或者是生成厂商品牌;所在机器的 IPv4/IPv6 地址、MAC 地址、网络联通性信息(如 P2P 打通情况)等等;操作系统名字和版本;磁盘剩余空间;
依赖软件信息 :包括已经安装的动态库、虚拟机、解析器;数据库、消息队列、Web 服务器;共享内存地址;
协作软件信息 :本软件需要合作的其他进程,如集群目录服务器(Zookeeper/Consul)、安全和崩溃守护进程等等;本软件需要的数据,如存档数据目录、数据库内容(如 SQLite 文件)、热更新文件等等;
发布管理信息 :当前的软硬件环境,在本次安装部署的业务定义上,属于哪一套。譬如是“开发环境”,还是“测试环境”的压力测试环境,又或者是“生成环境”的“苹果 app store 审核环境”等等。
软件的安装部署之前,要对以上的信息能有一个准确的掌握,才继续下去。否则就很容易出现程序运行故障的情况。原始的处理方法,是在团队里面找一个倒霉蛋(我就经常干这个),专门负责维护以上信息,然后亲自去做对应环境的安装部署。更好的处理方法,是写一个程序来检查这些信息:在简单的输入发布的版本号之后,这个程序会从一个提取准备好的《发布计划》数据表中,自动对发布版本号对应的环境进行检查,一切正常后“亮起绿灯”。而这份《发布计划》,往往需要我们通过配置文件一类形式来准备。——因此下面我们需要对“配置”有清醒的认识。
如果有一定软件使用经验的人,都会接触过所谓“配置文件”。很多功能比较复杂的软件,都会通过配置文件来控制软件的运行。譬如你要搭建一个 Web 网站,著名的 Apache 软件就提供了一份 httpd.conf 文件,你可以通过这个文件修改网站的首页文件、监听端口等几乎一切 Apache 支持的功能。
除了配置文件,很多软件还会从“环境变量”和“命令行参数”来获取一些数据,来改变程序的运行功能。这些在软件运行时输入的数据,都可以称为“配置”。早期的软件,由于网络速度低,导致重新发布一款软件的难度是比较大的。因此有一些程序员,会倾向把程序做成“一切皆可配置”,认为这样可以更灵活的应对需求变更。然而,配置文件、环境变量、命令行参数或者其他渠道获得“配置信息”,其实并不能特别好的被非专业人员所使用,这导致了两个方向的演变:
不管现在你手上的项目,是选择上述两个方向的哪一个,其实一直都会有一个问题需要解决:
“配置”到底如何才能正确的和对应的代码进行关联起来,然后一起正确的运行?
软件本身要设计为“ 零配置 ”情况也能运行,如果有些配置无法准备默认值,则需要通过启动时的检查程序给与用户提示,不要直接使用无效配置而给出奇怪的错误信息,甚至直接退出进程。我比较喜欢把零配置设置成“开发环境”的所需值,这样可以方便开发时运行调试。
为每个可预见的“运行环境”,编写单独的一套配置,并且把这种配置至于“ 版本管理 ”(SVN/GIT)之下。有一些团队会通过一个网络服务器来提供配置内容,这样的服务也应该从版本管理软件里对应运行环境的“分支”里面读内容,而不应该让人可以随便修改。
如果能通过程序自动生成、推断配置内容,就不要依赖配置。典型的就是很多服务器软件都会配置 IP 地址,每次公司通知迁移机房,就是一顿猛改,然后爆出很多故障,其实 IP 地址这种,完全可以通过相对固定的网卡 ID 进行读取。另外譬如数据文件目录这种,完全可以检查默认位置是否存在,没有的话新建一个目录就好。
需要特别注意的是,有两种典型的对配置的错误认知需要避免:
把软件运行所需的数据,误以为是配置来对待 。典型的就是把游戏中的数值配置表,视为“配置”进行处理。尽管动态调整游戏数值这个能力很诱人,但是实践证明,在没有完整的测试和发布流程下,随便改游戏配置数据可能造成灾难性后果。这些由开发者生产的数据,应该是和软件源码一起,作为标准发布流程的管理对象一起对待的,不要认为只有源码才是程序,数据也是程序,需要同样的版本管理和严格测试。
把配置文件作为用户以外操作软件的一种手段 。譬如软件的某些功能,可以通过修改配置文件来打开和关闭,于是就把热更新配置文件的流程,作为控制用户软件的手段。且不说这种在用户不知情的情况下修改软件是否道德,光是应对那些使用模拟器、网盘共享目录来运行软件的情况,就足够让你崩溃好久。如果需要一些应急手段,或者监控手段来改变运行中、发布后的软件,正确的做法是专门做一个这样的功能,通过专门的网络协议或者交互手段,认认真真的实现一个这样的功能,然后再通过完整的测试来确认这是可靠的。因为只有这样,才能明确的从代码说明,这些“外部操作”是真实存在的,而不是隐藏在读取配置然后生效的代码背后,这对于软件的可维护性非常重要。
如果你观察过承接定制家具的木匠,那么你就会发现,他们到达工作场地做的第一件事,就是先搭建一个木工桌,安装电锯和各种夹具,有的甚至会打造一套临时工具架,把工具整整齐齐的挂在上面。专业人士对于自己的工作环境,总是会试图自己改造到自己满意为止。
如果我们把上面的规定写成一个手册,然后用人工的方式进行发布操作,那么这些所有的设定都一定是没用的。因为对于这些枯燥重复的工作,人是很容易犯错的;而且,在实践中一定会增加或者修改各种规定,这对于人脑来说,更是容易变成一本糊涂账。所以不管你的发布流程是怎样,最重要的让这个工作变成程序代码,从而自动化运作。软件开发者也需要有自己为项目打造的专门工具,其中很重要的就是自动化发布的程序。
如果一件事你害怕出问题,那就应该多去面对它。自动化发布大大节省了重复执行发布流程的成本,让你有机会不断的去改进它
自动化之后,我们可以专门对“发布软件”这件事进行测试,而不必时刻担心发布事故。甚至我们可以专门编写自动化的测试程序,来测试“发布”
自动化发布的代码可以通过版本管理软件(GIT/SVN)进行记录,从而让我们在修改发布流程的时候可以没有顾虑,这样每次的经验教训,都可以用代码的形式给保存下来。
软件发布具体要包含的流程,根据不同的项目是有不一样的地方。但是大体可以分为基本部分:
生成可发布的产物。包括编译、打包、存储、生成一些说明文档等等。
对发布的产物进行部署。包括复制到对应的服务器,修改可访问的地址信息等等。
对发布的结果进行检查和测试。这个环节非常重要,包含人工的检查测试和自动的检查测试,其中自动检查、测试最为关键。
根据发布的结果,做最后配置以迎接用户。一般包括软件的上架(各种商店),填写上架说明,通知发行商和客服团队等等。当然最后还应该以用户身份进行一些人工检查和测试,以确保这些流程都最终成功。
显然上面的 4 个步骤,不是每次软件发布都必须全部做完,但是其中的前三部,在开发过程中是比较频繁发生的,自动化程序应该尽量接管这三步。如果有可能还应该生成一些方便最后一步工作的数据和文档。
这种自动化过程,我们一般称为“ 流水线 ”,意思就是把源码制作成可运行软件的一条生产线。很多相关的软件,都会,让你把多个自动化步骤编入一个自动化脚本,完成组装一个发布流水线。
一旦你拥有了自动化的软件部署发布工具,你就可以采用多种日常的自动化发布工作流程。对于 Google 来说,他们提倡的是三个环境发布:
日构建:每天凌晨4点,系统自动开始对所有提交的代码进行构建,并且发布到内部测试环境。
周构建:每周一个时间点,机器提醒或者手工操作,开启一次构建和发布,可以发布到内部测试环境或者外部测试环境
目标构建:手工操作,对于有业务目标意义的版本进行构建和发布
对于任何一次自动化发布,如果发现了任何的失败,都应该首先修复问题,再继续其他的开发工作。对于多个模块的集成、以及复杂环境的配置工作的问题,解决的最好时机是刚刚发生,否则等到最后集成发布的时候,解决难度会大的多。我相信有一定工作经验的人,都会对通宵“发版”深有体会。要解决集成和发布的问题,最基本的解决态度就是多去进行集成和发布。
如果自动化发布过程中,加入了静态代码检查,或者自动化冒烟测试等流程,那么这些问题出现之后,也是应该尽快的修复,尽力做到“每日构建”都是能成功的。这也是避免所谓“技术债务”无限制积累的一个手段。
对于其他公司,特别是那些崇尚所谓小步快跑的公司,有可能会设置成:只要某分支的代码被提交,就自动构建发布。——一般这种项目都是服务器端项目,譬如某个电子商务网站之类的。如果是这样,那么选定正确的分支进行自动发布就很重要的,譬如 gitflow 中的 develop 分支,因为一般的 feature 开发并不需要每次都提交代码到它身上。
整个过程是自动化,可重复的。如果其中有的环节不能自动化,那么就要想办法让它自动化。
如果自动化环节中断或者失败,应该尽快修复这个问题,而不是拖延
为了让软件能可靠的运行,反而是应该先“估计”到运行故障的情况。所以应对故障的各种措施,应该作为“非功能性”需求,在软件设计的时候,就予以重视。而软件发布流程,虽然说本身并不应该关心某些特性和功能,但是很多“可靠性”措施,会让发布的过程,以及相关环境变的复杂——我们在发布的目标,不仅仅是让用户能正常使用,也需要让很多“保障功能”也正常运行。下面就重点说一些常见的“保障功能”和软件发布工作之间的关系。
让运行中的程序可以接受监控,是在问题发生后搜集足够信息,从而改进问题的重要系统。几乎所有的编程语言、操作系统、容器都会对监控能力有一定支持。
几乎所有的操作系统都有日志记录和查找的能力,开发者只需要拥有写文件的能力,就可以使用。一般日志系统需要考虑几个部分:
记录日志。软件开发者需要选择或者开发一个生成日志的模块,尽量全面的记录软件的运行情况,同时还要注意对硬件性能的消耗。
搜集、存储。由于客户和开发者的运行环境通常是不一致的,开发者需要从客户的运行环境,搜集所有日志,然后集中存储。很多软件都有通过网络上报日志的功能,这个搜集软件为了保证自己的稳定,往往和真正的应用软件是不同的文件和进程。
搜索和统计日志。日志被集中到某些存储设备后,开发者需要能通过一些关键字进行搜索,用来找到出现问题的完整记录。因此日志除了时间以外,可能需要包含一定的用户信息,譬如 IP 地址,用户 ID 等等。如果一系列日志是有关联的,还需要一些“日志ID”来把所有的信息串联起来。对日志进行统计也很重要,有一些开发者难以直接重新的问题,可能只能通过日志统计来发现。
具体关于日志系统的设计,可能会比较复杂,这里不多展开。但对于软件发布流程来说,发布的过程也许也需要以日志记录下来,并且在软件运行起来后,通过软件的日志,作为发布结果测试和检查的条件,以确保发布成功。
由于日志系统往往需要对运行环境进行预先配置,以及安装一些诸如搜集日志的 Agent 程序之类的配套程序,所以自动化发布程序就需要先识别目标环境上,这些对日志功能配套的东西是否已经准备好,否则可能需要自动重新再准备一次。这些对于日志系统配套软件的安装部署,在不同的项目之间是可以共用的,因此部署安装程序也是有一些代码可以作为“库”进行积累的。
我们最经常关注的软件运行指标,包括了诸如:服务的在线人数、软件的安装数量、用户的消费情况等等。尽管通过日志收集和统计,往往也能获得很多指标数据,但是往往这个过程需要一个小时甚至一天,如果想实时的知道现在发生的情况,就需要开发和安装关于实时上报的系统。
实时上报的系统有时候被称为“埋点”系统。它一般包含:
在软件内部记录或统计一些数据。
把这些数据上报到某个网络服务器。
这些数据经过一些次数统计、或者内容数值的最大最小平均值的统计,记录到实时数据库中
数据库中的信息,在一个监视网站中以图表的方式展示。通常可以根据时间段来筛选这些数据。
和日志系统类似,实时上报往往也需要安装部署一些专用的服务器和 Agent 上报程序。这些设施同样应该用自动化的发布程序进行检查、部署、和最终测试。
很多实时上报数据,最后还会进入专门的“数据仓库”,用来进行数据挖掘和其他一些事情。对于发布程序来说,确保数据能上报成功是很重要的,即便有些不可控的环境,上报是受阻的,那么受阻这件事最好也要能上报出来——当然这不是100%能成功的。
前文说过,不要拿配置文件当操作指令使用。但现实中,对于已经发布的程序,一般都需要保留一些应急操作的可能。譬如我们需要紧急关闭某一部分功能,紧急更新某一部数据或者代码,实时通知用户一些事情等等。这些应急操作需要提前规划,设计的软件当中。这些功能中和发布程序关系最紧密的,就是所谓热更新部分。
由于发布程序就是一种“软件更新”的过程,所以对于设计了“热更新”的软件来说,发布流程的最后一步上架、接入软件,还需要对“热更新”系统进行修改。比较常见的是需要去修改热更新检查的“版本号”数据,或者是离线、或者实时的通知用户进行更新和重启等。这个时候,自动化发布流程就更加显得重要,因为热更新系统的数据变更,可能会影响大量用户,是一个非常关键的操作,如果出错了要恢复的代价往往很大。譬如发布错了一个下载包,可能要付出高昂的服务器带宽流量费用。所以让自动化发布流程接受严格的测试验证,就能最大程度的降低这种损失的可能性——尽量不要让预备故障的应急操作功能自己成为故障。
在一些用户量很大的业务领域,每次更新软件都可能带来很大的影响,所以不会一次把全部用户的软件都更改成新版本。而是往往会把用户分为多个部分,一部分一部分的发布,如果其间出现问题,就立刻回滚。如果出现了一些 bug,影响的用户数量也比较小。这种策略可以有效软件的发布降低风险,而且也可以对产品设计提供一定的效果对比试验的机会。一般我们会采用两种策略:
蓝绿发布
金丝雀发布
这种发布主要针对服务器端软件,意思就是把提供服务的服务器,分为“蓝”和“绿”两部分,其中“蓝”部分服务器,是在发布时会关闭用户进入的入口,等待完全没有访问量之后,开始升级部署。这个过程中,另外一部分服务器正常提供服务,称为“绿”服务器。等到“蓝”部分服务器全部部署升级完毕后,打开用户进入的入口,随后对原来的“绿”服务器进行关闭入口,等待用户退出后进行部署升级。这种做法的好处是尽量降低对在线用户的影响。对于客户端软件,其实也可以利用这个思想,通常是热更新的策略:先后台下载新版本软件,安装到和当前软件并行的另外一个目录,等待用户退出当前软件运行。下一次用户启动软件,就直接运行新安装目录的程序。
这种发布方式在不同的公司有其他的别名:灰度发布、小流量发布等等。具体做法就是先向一部分用户进行发布(过程也可以使用“蓝绿发布”),然后观察一段时间,如果没有严重的问题,用户反应还可以,就可以继续扩大发布的用户范围。有些公司会对新、老版本搜集一些用户行为或者其他统计数据进行对比,从而决策新版本的“收益”,如果感觉不理想,可能会对相关用户进行回滚——这种利用不同群体用户进行软件效果对比的工作,类似于“AB实验”,这对产品的探索性迭代更新很有价值。当然,如果你希望“AB实验”的结果尽量真实,对于选择怎样的群体进入实验、AB部分用户要如何随机,都需要一些专门的设计和开发。
前文描述了大量的概念和过程,这些概念和过程,都需要一些技术手段来实现,下面来介绍一些相关的技术工具。
软件的发布部署过程的自动化,显然需要非常高的灵活性,因为每个项目的开发技术、运行环境都有很大差异。而自动化的过程需要用到的工具,也林林总总,想要让这些事情都能串起来运行,技术的最大公约数就是命令行。——很多开发工具都具备命令行模式,就是为了使用者可以把它放在自动化流程中。而组装这个自动化流程——流水线,的最容易找到的工具,正式“脚本”。
在所有常见的操作系统上,我们都能找到支持编写命令行软件的脚本,譬如 Windows 上有 .bat 脚本和 .ps1 脚本(PowerShell);Linux/MacOS 上则有 bash/zsh 等等。这些脚本系统,除了一般的流程控制语句以外,一般都可以很方便的读取操作系统变量、使用管道进行进程通信、读写文本文件等等。因此我们用脚本几乎是肯定能做出软件发布的流水线的,唯一的缺点是使用的时候缺乏图形化,或者说缺乏一定信息的提示。不过,就算你使用一些专门的 CI (持续集成)软件,还是会需要写一些脚本,去定制每个具体的发布步骤。所以学习一些脚本知识,肯定是很有必要的。
我曾经开发并使用过一套完全用脚本搭建的自动化发布系统。它运行在我们公司的“跳板机”上——这个服务器是仅有的,可以同时连接开发环境和运营环境的服务器。这个自动化发布脚本的工作分为几步:
从源码版本管理库(svn/git)下载对应的分支代码,并且使用工程中的编译脚本编译目标版本的软件包。
读取一个文本文件,这个文件内是预先编制好的发布目标服务器 IP 列表
使用 expect 脚本,登录上述服务器,并且把编译好的软件包通过 SSH 拷贝过去
使用 expect 脚本,对新的软件包进行解压、配置;把旧的服务进程停掉,用新的软件包把服务启动起来
上面使用的 expect 脚本,是一个可以读取 SSH 登录后返回的数据,并且发送对应命令的叫偶本编辑工具。这类脚本采用“推送”的方式来部署软件,有一定的安全风险。下面这个脚本模拟了手工登录 ssh 输入密码的操作,因此用这种方法可能会导致服务器密码被写在脚本里,不是特别安全。你也可以预先在所有服务器上部署你的 ssh 公钥,来进行免密码登录,这样稍微会安全一些。
#!/usr/bin/expect
spawn ssh root@192.168.2.161 df -h
expect "*password:"
send "winner@001\n"
expect eof
网上有一款叫做 Ansible 的工具,可以做 expect 脚本类似的事情,而且更加方便。你可以在你的“主控”服务器上,定义一种叫 playbook 的脚本文件。这个文件里面,可以书写安装不同的软件的脚本、以及执行任何你要的动作。而你在主控服务器上,通过修改 /etc/hosts 可以输入所有想要部署软件的服务器 IP 和 hostname,相同的 hostname 的服务器可以视为同一组。然后你通过 ansible 的命令行程序,就可以对任何一个 hostname 下的所有服务器,执行你编写的 playbook 脚本,完成远程安装部署软件。
expect 和 ansible 这类工具,都是用“推”的方法来部署软件,好处是不需要对目标服务器预先安装什么软件,也不需要配置太多东西,只要收到新的服务器,就立刻可以派上用场。但缺点就是你需要自己维护一套“数据文件”,里面记录了很多 IP 地址,以及这些 IP 分别对应什么功能,要安装什么软件。——在大型互联网公司中,因为机房和各种管理原因,服务器的 IP 地址是经常会更换的,你需要频繁的部署到不同 IP 的服务器上,这样维护这种 IP 列表是一个容易出错且枯燥的工作。
Jekings 是流行最早的开源的持续集成软件。它提供了组装自动化流水线的能力,你也可以视为一种“定义了一定规则的图形化脚本组装器”。使用 Jekins 一般都是配置以下三步:
提供一个 GIT/SVN 的源码版本控制库地址(这类东西统称 SCM)
定义你的工程如何进行编译。你可以在工程目录里加一个 jekinsfile 的脚本来定义编译过程,也可以使用 jeckins 内置(或者通过插件安装)的编译工具进行编译。由于 jeckins 是 Java 写的,所以对于 ANT\MAVEN 这类 Java 常用编译工具都内置了。
定义你的工程要如何部署。你可以通过“SSH”等方法对目标服务器进行部署,并且进行重启运行。 除了上面三个主要的过程,你还可以插入各种测试和检查的步骤。这些步骤大多数可以通过 jekins 的各种插件来完成。你也可以自己编写 shell 脚本在流水线工作的服务器来执行任何的步骤。
Jekins 可以用来搭建比较完整的发布流水线,用于构建流水线各个部件的功能插件也比较多。因此很多团队会直接用 Jekins 作为基本的“持续集成”工具。不过也有一些从来不使用 Java 技术体系的团队会觉得安装一套 JSK 很麻烦。
Jekins 在“发布”这个功能上,除了支持 SSH 方式拷贝文件、启动程序外,也支持 Docker 技术。而对于“发布”功能,网上也有很多不同的方案。
Chef 就是一个互联网上比较常见的部署工具。这是一个 ruby 写的工具,所以你可以用 ruby 语言来编写每个目标机器在部署软件时应该做的事情。这种集合了各个不同作用的机器,上面各自要如何安装软件的脚本,称为 Cookbook。你可以通过 Chef 服务器,把 Cookbook 分发到所有部署目标的服务器上运行。——这些部署目标服务器,需要先安装一个叫 knife 的软件(属于 Chef 软件的一部分)作为部署 Agent 软件。这样只要在 Chef 的主服务器上配置好 Cookbook,在部署目标服务器上安装好 knife 和配置好这个服务器的用途,就可以很简单的通过 Chef 服务器控制所有目标服务器自己来下载 Cookbook 并且运行安装部署命令。
Chef 这种发布部署的方式称为“拉”,和 Ansible、exepct 这种完全相反。优点是每个部署目标服务器,可以各自去配置自己的功能,而不需要集中在某个地方维护他们的 IP 列表和账号密码,会更加灵活。类似也是用“拉”的方式进行部署的软件还有 Puppet:安装部署脚本使用它的语法编写 site.pp 文件,这个文件里面按照 hostname 对机器用途进行分组,然后运行在一个 master 服务器上。每个目标服务器运行 puppet 命令,把自己的 hostname 发给 master 服务器,就可以下载并运行自己需要的部署脚本。Chef 和 Puppet 的最大区别,就是安装脚本的 Cookbook 和 site.pp 的语法差别,以及背后支持的安装插件库的不同。
虽然 Docker 本身是一种 Linux 的虚拟机工具(现在 Windows 通过 WSL 也可以了),但是大多数情况下,我们还是拿它当一个部署工具使用。Dockfile 就是我们的部署安装脚本,只不过这个脚本的部署安装过程不需要到目标服务器上运行,而是可以在任何机器上运行,然后生成虚拟机的“镜像文件(image)”就可以了。随后我们把 image 镜像文件弄到目标服务器,然后再通过 docker 命令运行这些 image 即可。镜像文件中可以包含一个完整的操作系统的所有依赖库、软件、配置,非常的方便。——这和流行一时的 ghost 有异曲同工之妙。
持续集成的核心是构造流水线,在流水线中,让软件的编译、部署过程自动化。在流水线当中,还有一个重要的步骤,本文没有太多的提起,就是“测试”——这个步骤是用来确保一切正确的被处理,也是现代软件开发最重要的概念。下一个篇章我希望能就此进行一些论述。
评论区
共 4 条评论热门最新