本文介绍了如何利用开源软件快速搭建一套微服务的持续交付系统。本文假设的环境是 Linux 操作系统,用到的软件包括 Git、Jenkins、Salt、ZooKeeper、Apache 等。开始之前,我先简单介绍下持续交付和微服务的概念,以便大家更好的理解本文的精华。
什么是持续交付?我们先举个物流的例子,现在各大电商都非常重视物流的自动化建设,在实现包括运输、装卸、包装、分拣、识别等作业过程的设备和设施自动化的同时,更在研究无人机和自动驾驶汽车送货,达到物流的全自动。
那么软件开发呢,从开发人员 check in 代码到代码仓库,到代码的构建、部署、测试、发布,我们可以形象地把这个过程称为“软件物流”,现实世界的物流实现了相当的自动化,“软件物流”也应如是,实现从开发人员 check in 代码(客户下单)到生产系统上线(送货上门)的自动化。
说到这里,我们可以给持续交付下一个“非专业”的定义,持续交付就是实现“软件物流”的自动化。
图 1. 持续交付流水线
图 1 摘自《持续交付:发布可靠软件的系统方法》,展示了持续交付具体包括的内容。本文重点讨论如何实现微服务的持续交付流程,所以会忽略掉整个流程的一些细节(如代码分析、单元测试等等)。
那什么是微服务呢?微服务的概念最初由 Martin Fowler 与 James Lewis 于 2014 年共同提出,微服务架构风格是一种使用一套小服务来开发单个应用的方式途径,每个服务运行在自己的进程中,并使用轻量级机制通信,通常是 HTTP API,这些服务基于业务能力构建,并能够通过自动化部署机制来独立部署,这些服务使用不同的编程语言书写,以及不同数据存储技术,并保持最低限度的集中式管理。目前微服务的主流实现方式有两种:RESTful API 和消息队列。
图 2 RESTful 微服务
图 3 message queue 微服务
图 2、图 3 是两种典型微服务架构的简略图。当然现实中的系统会复杂的多,比如会有微服务聚合,多级缓存,注册中心等。
微服务相对单体式应用来说有明显的好处:
- 解决了单体式应用的复杂性问题,单个微服务很容易开发、理解和维护。
- 每个微服务都可以由独立的团队来开发,可以自由选择开发语言。
- 每个微服务可以独立部署,系统可以快速演进。
- 可以对每个微服务进行独立扩展,极大的提高系统伸缩性及资源利用率。
但在一个单体式应用拆分成数十个乃至上百个微服务,由于服务数量的增加,以及微服务支持多种编程语言的特性,对软件的构建,部署,测试,监控都带来了全新的挑战。本文将讨论如何通过持续交付来降低微服务构建,部署的复杂度。
微服务的持续交付:统一方法
由于微服务的特性,微服务的持续交付会比单体式应用的持续交付复杂的多。本节列出了为了降低微服务持续交付的复杂度,我们遵循的一些原则:
- 统一方法。这里有两个层面的含义,第一是流程的统一,有很多公司对运维自动化非常重视,但在开发、测试阶段没有采用自动化的方法。随着 DevOPS 运动的兴起,大家逐渐意识到需要在开发,测试阶段采用与生产环境相同的交付方法,这样在系统部署到生产环境的时候,这一交付流程已经经过多次的检验,出错的概率大大降低了。第二层含义与微服务相关,各个微服务可能用不同的语言实现,如 Java、Python、C++、Golang、纯前端(JavaScript),我们要对采用不同语言实现的微服务使用统一的交付方法。
- 在版本控制系统中,每个微服务应该对应一个独立的仓库。以 Git 为例,一个 Project 下面,每个微服务对应一个独立 Repository。这样各个微服务可以独立 check in 代码,而不会在持续构建的时候互相影响。
- 设计持续交付系统时要考虑实现软件交付的全自动化,虽然在现实中,会存在提交测试,生产变更审核等人工环节。但在理想情况下,开发人员 check in 代码之后,能够自动触发构建多套环境的部署及测试。
- 支持单个微服务升降级,这要求持续交付系统,对每个可部署的单元(微服务)要有独立的版本号。
- 程序与配置分离。要支持一套程序(可执行包 + 配置文件包)多处部署,这里强调了一套程序,是指在开发人员 check in 代码后,构建系统只生成一份程序(可执行包 + 配置文件包)。不管是部署到开发环境、测试环境,还是生产环境我们要用同一套程序,而不是对每个环境单独打包。我们知道 Java war 包会要求把配置文件包含在里面,这会造成不同的环境要求提供不同的 war 包,这就违反了我们说的这个原则,后面我们会讨论如何处理这个问题。
- 在应用程序部署时,不得依赖外网资源。我们把部署过程独立为两个阶段:环境准备阶段和应用程序部署阶段。环境准备包括操作系统,JDK 或其他语言运行时系统级依赖库的安装,得益于 IaaS 的相对成熟,我们把这一阶段独立出来。而应用的部署需要定制化,也是本文讨论的部分。在部署应用时,要求所有的资源从内网获得,这样可以保证应用部署过程的快速、稳定、可重复。
快速搭建微服务的持续交付:持续构建
下面我们结合一个虚构的项目来介绍持续交付的实现细节,假设我们有一个项目 BetaCat,由 ms1、ms2…msN,n 个微服务构成。下面我们重点介绍 ms1 微服务如何实现持续交付,其它微服务可以类推。
本节讨论下如何实现持续构建,下一节会探讨持续部署。
图 4 Jenkins 处理仓库代码流程
如图 4 所示,开发人员 check in 代码到 Git 仓库后,Jenkins 会自动地进行构建工作,并把打好的包上传到 Repo server 上。
图 5 配置文件示例
作为统一方法的一部分,我们在每个微服务仓库上创建了 CI 目录,用于配置文件的打包,在 CI 目录里,只放入需要参数化的配置文件,执行脚本等,并会严格遵循原有系统的目录结构,如图 5 所示,我们要求有 start.sh、stop.sh 及 service(用于 Linux 的 init 启停该微服务)。
图 5 中配置文件参数化内容,参数部分用“{{”与“}}”包围起来,在持续部署的时候会根据传入的参数替换为特定的值。
我们还定义了持续构建的统一输出,对每个微服务采用 tgz 的打包格式,微服务 ms1 持续构建的输出文件示例如下:
- ms1-1.0.7.tgz (可执行包)
- ms1_config-1.0.7.tgz(配置文件包)
在可执行包里面要求把所有的依赖库(除了系统 lib 库)都包含在里面,对不同编程语言的微服务的构建工具没有强制要求,统一由 Jenkins 调用。C/C++ 我们推荐使用 CMake,Java 一般用 Maven,Python 直接打包。
配置文件包就是前面 GIT 仓库的 CI 目录直接打包而成。
图 6 Bundle 示例
同时为了在部署时不用具体指定每个微服务的版本号,我们引入了 bundle 的概念,如图 6。在任何一个微服务构建之后,会触发 bundle,sha512 校验文件生成,并上传到 Repo Server。
最后让我们看下持续交付上传到 Repo Server 的目录结构:
图 7 目录结构
这样持续构建的工作就完成了,接下来就需要进行持续部署了。
快速搭建微服务的持续交付:持续部署
在开始持续部署的讨论之前,我们先描述一下软件运行注入配置的三个时点:
图 8 配置注入的三个时间点
打包时点,典型的是 Java 的 war 包,会把配置文件打包在一起。部署时点,在部署的时候利用专门的部署工具更新配置文件,这也是我们采用的方法;运行时点,程序运行时通过环境变量或注册中心 / 配置中心获得配置信息,如用 Docker 部署微服务时就要考虑通过这种方法来获得所需要的配置信息。
图 9 采用 salt 进行部署
图 9 显示了我们对不同的环境统一采用 salt 进行部署。由于我们支持用户只输入 bundle 的版本信息来实现部署,这就要求在持续部署的时候,部署系统能自动获取每个微服务的版本号,为此我们对 salt/foreman 做了一点小改动,修改后返回的 pillar 格式包含各个微服务的版本,同时下载并解压对应的配置文件包到 salt master 的相应目录,以及关闭 salt master file_list 缓存:fileserver_list_cache_time: 0。
图 10 foreman web 界面以及 Salt 格式
图 10 左边表示我们在 foreman web 界面上设置的参数,右边表示通过 salt pillar.items 取得的格式,可以看到多了每个微服务的版本号信息。
下面我们按照部署三部曲(安装、配置注入、服务运行)来介绍部署规则文件(saltstate、sls 文件)的编写:
1、betacat_ms1.sls 第一部分:安装
在这一部分,检查并创建安装目录,下载需要的可执行包,并解压到正确的位置,可执行包直接从 Repo Server 获取,并通过 sha512 验证文件的完整性。
2、betacat_ms1.sls 第二部分:配置注入
配置注入部分,读取配置文件包,通过 salt master 转换后下发给目标机。这里用红框标出了设计的核心。通过 salt 的 file.recurse 和之前持续部署中打好的配置程序包,并把所有的配置项传入。可以做到不用对多个配置文件单独编写部署逻辑,完全参数化。
3、betacat_ms1.sls 第三部分:服务运行
在这一部分,确保微服务在运行状态,并在必要的时候重启。这里需要特别指出的一点,在整个 sls 文件中,对不同的微服务来说,只有 3 个元参数:项目名称(BeatCat)、微服务名称(ms1)以及 sig(ms1, 微服务进程的唯一识别字符串)。那么我们可以通过简单的脚本来自动生成 sls 文件,而不需要手工编写。大大降低持续部署的开发维护成本。
快速搭建微服务的持续交付:全自动化
为了支持持续交付流程的全自动化,我们引入了 ZooKeeper,如图 14。
图 14 引入 ZooKeeper 后的流程
- 代码 check in 到 Git 后,触发构建,Jenkins 会把打好的包上传到 Repository Server,并更新 ZooKeeper 的本次及 latest 包版本信息。
- 侦听到 ZooKeeper 的 latest 包版本信息变动后,会触发 saltstack 的部署命令向各个环境部署最新的程序。
- 部署完毕,会更新 ZooKeeper 上的目标机部署版本信息。
- 侦听到 ZooKeeper 上的目标机部署版本信息变动后,会触发一套或多套自动化测试脚本的运行。
- 自动化测试通过后,会更新 ZooKeeper 上的包版本的测试信息。
- 通过测试的包,可以自动上传到生产环境的 repo server,并更新生产环境 ZooKeeper 的包版本信息。
- 生产环境,侦听到 ZooKeeper 的包版本信息变动后,会触发生产环境的部署。
- 生产环境部署完毕,会更新 ZooKeeper 上的目标机部署版本信息。
总结
在前面几节我们结合一个虚构的项目 betacat 介绍了如何实现持续交付。下面让我们来做个简单的回顾:
- 我们提到了统一方法,我们通过在持续构建阶段对不同语言的微服务代码输出统一的 tgz 包格式,并引入了 bundle 文件,简化了后续的持续部署的实现;我们在开发环境、测试环境、生产环境都 salt/foreman 来进行统一部署,实现了部署方式的统一。
- 我们提到了每个微服务应该对应一个独立的仓库,在实践中,也会碰到一份代码需要生成多份微服务的例子(工具类的微服务),这个需要在持续构建的时候多做点工作。也会碰到一个仓库中包含了多个需要独立部署的微服务,一般建议把它们独立出来,当然也可以在构建这些微服务时共有一个仓库,但每次代码 check in 都会触发多个微服务的构建。
- 实现软件交付的全自动化,为此我们引入了 ZooKeeper,并通过各个脚本作为简单的粘合剂来实现各个环节的自动触发,配合完成整个交付的全自动化。
- 支持单个微服务的升降级,我们对每个微服务都有版本号,并为了简化部署,我们还引入了 bundle,用户在一般情况下只要指定一个 bundle 的版本,就可以自动把需要的多个微服务部署到目标机上,特殊情况下,也可以具体指定每个微服务的版本号。
- 程序与配置分离,在开发环境与测试环境,我们用的同一个 Repo server,实现多套环境的部署,生产环境的 Repo 也只是原始的 Repo 的一个简单子集。Java war 包要求把配置文件打包在里面,其实在原有 war 包带上版本号的基础上,再配合一个配置文件包,应用统一方法更新即可。
- 我们提到在应用程序部署时,不得依赖外网资源。把所有的外部依赖要么在环境准备阶段准备好,要么把依赖打包在可执行包内。在持续构建时候,可以把 C/C++、Java lib 库包括在可执行包里面;前端比较流行的 bower_components 等相关文件也可以在持续构建阶段打包进可执行包。
优点回顾
我们提出了一套利用开源软件快速搭建微服务的持续交付的方法,下面分析一下它的优点。
- 低侵入性
在改造已有微服务项目时,我们通过在每个微服务代码仓库添加 CI 目录来实现配置项的参数化,最大程度上降低了对既有项目的侵入性。而在项目切换到持续交付流程时,只要保持 CI 目录下的配置为最新。
2. 低维护成本
用户在微服务添加配置项或配置文件时,只要往 CI 目录 check in 文件(如果是全新的配置项需要在 foreman 里设置),就可以实现配置文件的自动更新,不需要修改持续构建,持续部署系统本身。
3. 低耦合
得益于系统的低耦合,我们可以自由地替换各个部分,Git 也可以用 SVN,现有流行的持续构建工具对主流的版本控制系统都有很好的支持;Jenkins 也可以替换为 Teamcity 或其他持续构建工具,只要保持输出到 Repo 的包保持一致;Salt 也可以用其他持续部署工具来替换(puppet 的 file 部署目录时 source 与 recurse 有冲突,实现 salt “file.recurse”的类似效果可能会有障碍);ZooKeeper 也可以用 etcd 或消息队列来轻松替换。
4. 可以快速切换到 Docker 部署。
局限性反思
1、基于部署目标机来填写配置信息,配置过程为:
选择机器 -> 选择要部署的微服务集合 -> 设置配置信息。
这需要对每台机器都要配置一遍配置信息,虽然我们之前实现了配置信息的去重。更合理的方式应该是:
选择要部署的微服务集合 -> 选择配置信息–> 选择要部署的机器
微服务集合类似于 k8s 的 pod 的概念,配置信息可以多个版本并存,这样在实现配置的集中管理的同时,可以方便地实现蓝绿发布或金丝雀发布。更进一步,目标机简单可抛弃,实现不可变基础设施(Immutable Infrastructure)。
2、暂未实现数据库 schema 的自动变更。
作者介绍
祝小华,森浦资讯 DevOPS 技术负责人,十多年软件开发工作经验,先后就职于 Alcatel-Lucent wireless,IBM DS8000 部门从事软件开发工作。关注存储,微服务架构,容器技术及数据分析。
感谢郭蕾对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论