版本的故事(三)取个好名字

当我们发布一个版本的时候,需要为这个版本取一个名字,也称作版本号。本文说明编制版本号的方法。

版本编号有很多不同的方法,其中最常见的是“三段式”版本号,我们使用的操作系统、开发工具、编译器、库函数、中间件,很多都使用这种版本编号方式。比如:Zookeeper 3.4.13、Maven 3.6.2、Tomcat 9.0.2、MacOS 10.15.6、CentOS 7.3.1611。

以上软件涵盖了产品、组件两大类别,这两类对象都有“版本”,但是概念是有差别的,编号方式也不一样。本文只介绍组件版本,产品版本以后细说。

三段版本号是由三个数字组成的,分别称为 MAJOR、MINOR、PATCH。对于这个三个值有没有具体的编号标准呢?一个普遍的说法是:大修改升级 MAJOR,中等修改升级 MINOR,小修改升级 PATCH。但是多大的修改算大,多小的修改算小呢?以下是多年前看到的一本书上的描述,以下摘自《未雨绸缪——理解软件配置管理》(董越,2008):

按照书中的描述总结一下:

  • MAJOR:大量的修改,“完全不同”的新版本
  • MINOR:增加少量新功能
  • PATCH:Bug 修复

关于 PATCH 的描述已经很清楚了,但是 MAJOR 和 MINOR 还是有一些模糊。比如“完全不同”是界面完全不同,还是架构完全不同?再找一本书翻翻,以下摘自《微服务设计》(Sam Newman,2015):

 总结如下:

  • MAJOR:包含向后不兼容的修改
  • MINOR:有新功能的增加,但应该是向后兼容的
  • PATCH:对已有功能的缺陷修复

这种编号方式是以不同的数字段表示 API 的修改状态:MAJOR 升级表示发生了 API 不兼容的修改,通常称之为 Breaking change;MINOR 升级表示 API 有更改,但是仍然兼容上一个版本;PATCH 升级表示 API 没有变动,只有软件内部的变化。这样一来,版本号就不再是一个主观随意的标准了,而是非常明确客观的标准。这个规范就是著名的“语义化版本”。按照语义化版本,版本号体现的是 API 的变化状态,能够表达相邻版本之间的底层代码和修改内容的信息。遵守语义化版本,对于依赖管理有实质的意义。

举个例子说明语义化版本可以怎样改善我们的依赖管理:我们有个服务名叫 order-service,它需要依赖 product-service。开发时的依赖关系是这样的:

部署的时候,我们可以使用高版本的 product-service(大于 3.1.0 并且小于 4.0.0)代替 3.1.0 版本,比如下面这个依赖关系也是可以成立的:

这样我们就做到了:“一个客户端能够仅仅通过查看服务的版本号,就知道它是否能够与之进行集成”。这个信息给部署架构就带来了“弹性”,这种弹性对于部署工作是非常重要的。如果依赖关系是一个僵硬的整体,产品可能会陷入“版本锁死”的状态,给维护工作带来巨大的烦恼。我们为了完成一次升级,就要对每一个依赖包都做一次改版。

大型软件系统是由数量众多的组件和外部库组成的,每个组件都有多个版本。在这些组件中选择好用的版本,组成一个完整的系统是一个极具难度的事。一个令人十分痛苦的场景是:一个组件依赖两个组件,这两个组件都依赖另外一个组件,但它们所依赖的版本各不相同,在运行的时候可能就会出问题。这个问题我们称之为“菱形依赖问题”。很多项目组在菱形依赖中挣扎,一遍遍的部署调试,只是为了找到一个能在现场部署的版本。

语义化版本就是帮助我们脱离苦海的好办法,我们来举个例子,下面是一个菱形依赖的典型场景:

可以看到 B 和 C 都依赖 D,但是依赖的版本各有不同,这样就没有办法安装了。能不能找到 C 的某个版本,既能满足 A 的依赖关系,又可以满足 D 的依赖关系呢?我们翻阅 C 的发布历史(是的,一定要编写发布文档,并且永远保留它们),然后发现了这么一个版本:“C-1.8.0,2020年8月1日发布,依赖 D-2.5.2”,太好了,找到了!

现在集成 C-1.8.0,新的依赖关系是这样的:

分析一下:C-1.8.0 是 C-1.5.7 的兼容升级版,因此能够满足 A 的依赖要求;D-2.7.11 是 D-2.5.2 的兼容升级版,因此我们只要部署一个 D-2.7.11,就能同时满足 B 和 D 的依赖条件。

是的,我们运气很好,恰好有一个合适的 C 版本。但是如果没有语义化版本,我们根本不可能有这个机会。

使用与语义化版本,组件就有了一个简单、准确的 API 规格。版本现在有了一个好名字。

posted on 2020-09-02 22:26  小陆  阅读(395)  评论(0编辑  收藏  举报