十三、服务多版本

服务上线之后,由于功能变更、BUG修复,以及服务升级,需要对服务采用多版本管理。

1 服务多版本管理设计

管理的对象包括服务提供者和消费者:

  1. 服务提供者:发布服务的时候,支持指定服务的版本号。
  2. 服务消费者:消费服务的时候,支持指定引用的服务版本号或者版本范围。

1.1 服务版本号管理

服务的版本号是有序的,在服务名相同的情况下,两个相同服务名的不同服务版本的版本号可以比较大小。完整的版本号由“主版本号(Major)+副版本号(Minor)+微版本号(Micro)”构成:

  1. 主版本号:表示重大特性或者功能变更,接口或功能可能会不兼容。
  2. 副版本号:发生了少部分功能变更,或者新增了一些功能。
  3. 微版本号:主要用于 BUG修改,对应于常见的 SP补丁包。

1.2 服务提供者

服务开发完成之后,需要将一个或者多个服务打包成一个 jar/war 包,为了便于对服务进行物理管理,打包后的名称中会包含服务的版本号信息,例如 com.huawei.orderService_1.0.1.jar。
在微服务架构中,微服务独立开发、打包、部署和升级,因此微服务的版本和软件包的版本可以一一映射。但是在实际开发中,尤其是大规模企业应用开发,单独为每个服务打包和部署目前尚未成为主流,它会增加服务软件包的管理和线上治理成本,因此目前的主流模式仍然是多个服务提供者合一个大的 jar/war 包,这就会存在一个问题:项目开发后期,有些服务进行了版本升级,有些服务没有,这样当它们被打包成同一个软件包时,就会导致版本号不一致。

每个服务都指定一个版本号,对开发而言也比较麻烦。一个比较好的实践就是微服务+全局版本模式。对于经常发生功能变更、需要独立升级的服务,将其独立拆分出来进行微服务化,实现单个微服务级的打包和部署。

对于其它服务,服务框架提供全局版本功能,在 Maven组件工程开发时,只需要为整个工程配置一个版本号,该组件工程包含的所有服务都共用该版本号。如果组件工程包含的某个服务发生了版本变更,就统一升级全局版本号,其它未发生功能变更但是打包在一起的服务做级联升级。这样做的一个原因是服务被打包在一起后,无论其它服务是否需要升级,只要软件包中的一个服务发生了版本升级,其它合设的服务也必须与其一起打包升级,它们之间存在物理上的耦合,这也是为什么微服务架构提倡微服务独立打包、部署和升级的原因。

1.3 服务消费者

与服务提供者不同,服务消费者往往不需要指定具体依赖的服务版本,而是一版本范围,例如:version=“[1.0.1, 2.0.8]”。

  1. 消费者关心的是某个新特性从哪个服务版本中开始提供,它并不关系服务提供者的版本演进以及具体的版本号。
  2. 消费者想使用当前环境中服务的最新版本,但不清楚具体的版本号,希望自动适配最新的服务版本。

当然需要指定一个默认的服务提供者版本号。

1.4 基于版本号的服务路由

服务提供者将服务注册到服务注册中心时,将服务名+服务版本号+服务分组作为路由关键信息存放到注册中心,服务消费者在发起服务调用时,除了携带服务名、方法名、参数列表之外,还需要携带要消费的服务版本信息,由路由接口负责服务版本过滤,如下图:

1.5 服务热升级

在业务不中断的情况下,实现系统的平滑升级,考虑到版本升级的风险,往往需要做多次滚动升级,最终根据升级之后新版本服务的运行状况决定是继续升级还是回退。这就意味着在同一时刻,整个集群环境中会同时存在服务的多个版本咋线运行,这就是热升级相比于传统 AB Test等升级方式的差异,如下图:

核心点如下:

  1. 升级的节点需要重启,由于自动发现机制,停机升级的节点自动被隔离,停机并不会中断业务。
  2. 服务路由规则的定制:如果是滚动式的灰度发布,在相当长的一段时间(例如一周)内线上都会存在服务的多个版本。哪些用户或者业务需要路由到新版本上,需要通过路由策略和规则进行制定,服务框架应该支持用户配置自定义的路由规则来支持灵活的路由策略。
  3. 滚动升级和回退机制:为了降低服务热升级对业务的影响,同时考虑到可靠性,在实际工作中往往采用滚动升级的方式,分批次进行服务的热升级,实现敏捷的特性交付,滚动升级如下图:

2 与 OSGI 的对比

OSGI,成立于 1999年,全名原为:Open Services Gateway initiative,但现在这个全名已经废弃。
致力于家用设备、汽车、手机、桌面、其它环境指定下一代网络服务标准的领导者,推出了 OSGI 服务平台规范,用于提供开放和通用的架构,使得服务提供商、开发人员、软件提供商、网关操作者和设备提供商以统一的方式开发、部署和管理服务。

目前最广泛和应用是 OSGI规范5(Release 5),共由核心规范、标准服务(Standard Services)、框架服务(Framework Services)、系统服务(System Services)、协议服务(Protocol Services)、混合服务(Miscellaneous Services)等几部分共同组成。
核心规范通过一个分层的框架,实现了 OSGI最为成功的动态插件机制,它主要提供了:

  1. OSGI Bundle 的运行环境。
  2. OSGI Bundle 间的依赖管理。
  3. OSGI Bundle 的生命周期管理。
  4. OSGI 服务的动态交互模型。

OSGI 两个最核心的特性就是模块化和热插拔机制,分布式服务框架的服务多版本管理和热升级是否可以基于 OSGI来实现?下面围绕着模块化和插件热插拔这两个特性进行详细分析。

2.1 模块化开发

在 OSGI中,我们以模块化的方式去开发一个系统,每个模块被称为 Bundle,OSGI 提供了对 Bundle的整个生命周期管理,包括打包、部署、运行、升级、停止等。
模块化的核心并不是简单地把系统拆分成不同的模块,如果仅仅是拆分,原生的 Jar包+Eclipse工程就能够解决问题。更为重要的是要考虑到模块中接口的导出、隐藏、依赖、版本管理、打包、部署、运行和升级等全生命周期管理,这些对于原生的 Jar包而言是不支持的。
传统开发的模块划分通常由两种方式:

  1. 使用 package来进行隔离。
  2. 定义多个子工程,工程之间通过工程引用的方式进行依赖管理。
    存在的问题:无法实现资源的精细划分和对依赖做统一管理。以 Jar包依赖为例,依赖一个 Jar包就意味着这个 Jar包中所有 public的资源都可能被引用,但事实上也许只需要依赖该 Jar包中的某几个 public接口。无法对资源做细粒度、精确的管控,不知道 public的接口都被哪些模块依赖和使用,消费者是谁,更为复杂的场景是如果消费者需要依赖不同的接口版本,那该肿么办?

OSGI 很好地解决了这个问题,每个 OSGI工程是一个标准的插件工程,实际就是一个 Bundle。实现了 package级的管理依赖关系,而 Maven则是 Jar包级的管理依赖。
而分布式服务:

  1. 服务提供者通过 service export将某个服务接口发布出去,供消费者使用。
  2. 服务消费者通过 service import导入某个服务接口,它不关心服务提供者的具体位置,也不关心服务的具体实现细节。
    这样就比 OSGI的 package导入导出功能粒度更细。
    利用 Maven的模块化管理 + 分布式服务框架自身的服务接口导入导出功能,解决了模块化开发和精细化依赖管理难题,完成可以替代 OSGI的相关功能,

2.2 插件热部署和热升级

OSGI 另外一个非常酷的特性就是动态性,即插件的热部署和热升级,它可以在不重启 JVM的情况下安装部署一个插件,实现升级不中断业务。
OSGI 的插件热部署和热升级原理就是基于自身的网状类加载机制实现的,下面我们分析在分布式服务框架中,如何实现服务热部署和热升级:

  1. 服务是分布式集群部署的,通常也是无状态的,停掉其中某一个服务节点,并不会影响系统整体的运行质量。
  2. 服务自动发现和隔离机制,当有新的服务节点加入时,服务注册中心会向消费者集群推送新的服务地址信息;当有服务节点宕机或重启时,服务注册中心会发送服务下线通知消息给消费者集群,消费者会将下线服务自动隔离。
  3. 优雅停机功能,在进程退出之前,处理完消息队列积压的消息,不再接受新的消息,最大限度保障丢失消息。
  4. 集群容错功能,如果服务提供者正在等待应答消息时系统推出了,消费者会发生服务调用超时,集群容错功能会根据策略重试其它正常的服务节点,保证流程不会因为某个服务实例宕机而中断。
  5. 服务多版本管理,支持集群中同一个服务的多个版本同时运行,支持路由规则定制,不同的消费者可以消费不同的服务版本。

相比于 OSGI在 JVM内部通过定制类加载机制实现插件的多版本运行和升级,使用分布式服务框架自身的分布式集群特性实现服务的热部署和热升级,更加简单、灵活和可控。

3 个人总结

服务多版本在实际项目中非常实用,用于实现服务的热部署和热升级,同时支持按照消费者做差异化路由,同时也方便演进到微服务架构,来迁移到服务的独立打包、部署、运行和运维。