Sin
In a
Nutshell
微服务架构下的服务版本管理

微服务架构下的服务版本管理

July 18, 2018 /软件工程

前两天微信群里看到一句揶揄:运维是灶,上头是锅,下面是坑。感觉相当贴切。 最近也是从前人手上接了个大锅,入了微服务的天坑。

我们花了不少精力,将原先的单体应用拆成了一系列的微服务。 这肯定不是没事给自己找不自在——原先的架构难以支撑越来越错综复杂的业务代码; 并且随着技术发展,后端阵营分化出了多种编程语言和技术栈,也是对高内聚低耦合的服务组织形态提出了现实的需求。

这对我们来说是个全新的领域,自然也遇到了不少挑战。 其中比较基础的就是如何对形形色色的微服务进行版本控制和管理。 对于这个问题,进行了下面一些思考。

目前的问题

提出一个新的方案自然是为了解决一个现有的问题,那么现在遇到了什么样的问题呢?

浮云身世两相忘

为了便于管理,目前同一种语言的微服务项目基本放在同一个 monorepo 里,每个服务在各自的分支开发完后合并进主分支。 这样就造成在迭代速度很快时,各服务各自的分支推进很快,完全来不及合并; 也同样由于需求紧迫,一线开发人员急于提交代码,没有养成很好的 git 分支管理习惯。 常见的场景是开发人员在自己的 service1 分支上进行开发,打包推了个标签为 0.3.9 的 docker 镜像, 然后继续开发,在 service1 上又签入了一系列提交,打包推了个 0.4.0 的镜像,分支代码与 master 渐行渐远。

分支的问题还是小事,这样最直接的结果是,完全无法将服务器上运行的服务版本与代码库中的提交对应起来。 例如正在运行的 0.4.0 版本出了 bug,0.3.9 却是好的,我们希望通过对比两个版本的代码确认是哪里改坏了。 尴尬的事情发生了:根本不知道 0.3.9 对应的是哪一份代码,只能凭开发人员模糊的记忆一个一个提交对比过去。

亲不知子不知

另一个问题是关于服务间复杂的依赖。 每个服务的职责单一化后,必然需要依赖一系列其他服务。 由于不同的服务可能由不同的开发人员负责,产生的一个问题是:某个服务做出了不兼容的修改,必须要通知到其他开发人员,去更新依赖了这个服务的所有其他服务。 哪怕我们梳理了完整的依赖图找到了正确的人, TODO

版本号策略

针对这些问题,我们需要一个完整的微服务版本管理策略。首当其冲的就是版本号命名,我们要以一个什么样的规则去命名我们的服务呢?

语义化版本

现在最流行最被认可的版本格式就是语义化版本 SemVer 了。 在它的定义中,版本号主要由三位数字组成:主版本号.次版本号.修订号。版本号递增规则如下:

  1. 主版本号:当你做了不兼容的 API 修改,
  2. 次版本号:当你做了向下兼容的功能性新增,
  3. 修订号:当你做了向下兼容的问题修正。

通过这样的版本定义我们可以一眼看出某次版本升级做出了什么程度的变更。 对于服务的调用方来说,如果没有新功能的需要,只关注第一位主版本号即可。 当服务主版本号变更时,依赖它的其他服务就需要更新自己的依赖,做出对应调整。

奇偶版本号

在上述语义化版本提出之前,各大经典软件项目都已经有了自己的版本号定义规则。 其中相当著名的一个是 Linux 内核的版本号,它的第二位次版本号的奇偶性有着特殊的含义。 稳定版本用偶数表示(如 2.2, 2.4, 2.6),开发版本用奇数表示(如 2.3, 2.5)。

代码版本管理

合久必分

为了达成良好的分支管理,首当其冲就是进行代码库拆分,这样每个服务可以有自己的分支流程和版本号,不会互相造成影响。 现有代码仓库的拆分可以通过 git filter-branch 命令进行,参见另一篇文章。 TODO

每个代码仓库自己可以有自己的分支管理方式,对分支策略不做硬性要求。 当然这一块是有一些最佳实践的,例如 git flow 和 github flow 等。

但是版本号方面是需要做统一规定的。 通过什么途径去定义版本号呢?git 中有一个非常好的特性可以利用,就是 tag 功能。 tag 是 TODO

我们可以要求所有的版本号都遵循 SemVer 的格式,每当需要发布一个版本时,例如 2.3.4,就打一个 v2.3.4 的 git tag. 这样就可以很方便地找到某个版本对应的代码提交。

不过,通过 git tag 的方式可能会带来一些运维上的问题。 在开发阶段,可能会产生大量的 tag,它们中只有部分会通过测试阶段并最终发布上线。 这样会造成:

  1. 运维难以记录进行了多少次版本发布
  2. 当前发布版本出现问题时,不知道回滚到哪个稳定版本

解决这些问题的一种方式就是上面提到的奇偶版本。 开发人员可以只以奇数次版本号进行 tag 提交,通过测试后,将当时的次版本号 +1 作为稳定版本,并用修订号代表第几次正式发布。 例如,2.3.7 和 2.4.1 对应同一个提交,而 2.3.11 和 2.4.2 对应同一个提交。 打稳定版 tag 这一步可以添加进我们自己的更新工具,由运维人员来完成。

git tag -a <tag> <commit-hash>

分久可以合

对于微服务来说,当然是希望尽可能高内聚低耦合,外部调用者只关注服务对外暴露的接口就好了,完善的注释和文档可以解决不少问题。

但是在我们的现实环境中(小公司小团队,注释和文档也不尽完善),很多时候可能还是希望能够跳转到被调用项目中直接查看代码实现。 对于这样的需求,我们可以单独建立一个项目,通过 git 的 subtree (TODO) 功能将需要的项目都放进来。

部署版本管理

代码版本与部署版本的对应

对我们来说,代码版本指的就是 git tag. 那部署版本呢? 由于我们的微服务部署方式是 Kubernetes + Docker,部署版本指的就是 docker 镜像的 tag 了。

将代码版本与部署版本对应起来,最有效的方式就是通过持续集成, 由 CI 服务器(我们用的是 Jenkins)自动拉取 git 仓库的 tag, 构建完 docker 镜像后使用相同的名称打好标签,推到 docker 仓库里。 Jenkins 的 Git Plugin 插件在 3.6 版之前不支持按 tag 构建,不过现在已经支持了,整个流程配起来还是很轻松的。 推荐通过 Jenkinsfile 的方式进行 Jenkins 任务的配置,这样多个服务可以共用一套模板,避免多个 Job 配置和维护的繁琐。

版本识别与冲突预警

RESTful 服务的版本定义

然而,我们更多用的是 GRPC…

静态版本依赖检查

配置文件
依赖记录