需求

SaaS微服务环境中,每个租户都有自己独立的数据库,当应用服务升级时(通常数据库表结构或数据需要同步改动时),需要保证升级后的租户能够使用升级后的应用服务,没有升级的租户仍然使用旧版本的服务。

前端灰度参考: ​​OpenResty实现按租户灰度发布​​。

环境

框架使用 Spring Cloud(不带 Alibaba,抛弃 Dubbo 了)。

运行环境为 K8s 集群。

设计

K8s 中的部署名(deploy)和 Spring Cloud 服务名(​​spring.application.name​​)相同,有小bug补丁更新时直接替换部署的镜像,版本不变,此时可以通过让实例数的滚动更新可控即可实现另一个概念上的灰度更新(不涉及后端库的变化,不需要针对租户区分)。

当有大的更新时,原有部署仍然保留,假设原服务为 ​​cloud-system-v1​​​。此时会创建新的部署,服务名可以是 ​​cloud-system-v1.1​​ 版本,此时同时运行了两个版本的服务,新版本暂时没有人使用。

当给指定租户数据库升级完成后,需要让租户能使用对应的新版本,首先需要控制好网关的路由,通过网关访问后端服务时能够选择正确的版本。其次是服务间调用时,需要能够调用到正确的版本。

网关

前端请求后端时,可以在请求头中带上租户信息,也可以在网关中根据访问用户获取租户信息后追加到请求头中,网关负载均衡到后端服务时查看租户应用的版本配置,存在配置时根据配置进行路由。

服务间Feign调用

服务间Feign调用时需要定义 Feign 接口,接口上会指定服务名,假设存在 A,B 两个服务,B存在v1和v2两个版本,A只有一个服务,A调用B时需要能够选择版本,在 A 中定义 B 中的 Feign 接口时指定服务名为 ​​B​​​,此时在服务发现 ​​DiscoverClient​​ 中通过 B 无法找到任何可用的实例,这一步可以简单改造,增加一个自定义的服务注册发现客户端:

  1. 获取所有服务名
  2. 根据​​B​​ 作为前缀过滤出 B-v1, B-v2 两个服务
  3. 将 v1, v2 所有实例都获取到,交给后续的负载均衡处理

服务发现中获取不到请求信息,不在这里根据租户版本进行过滤

上面的操作首先解决了获取实例的问题,后续在 Spring Loadbalance 中根据租户配置的版本选择一个实例即可,通过下面方式配置一个 LB:

@LoadBalancerClients(defaultConfiguration = LbConfig.class)

在 ​​LbConfig​​ 中提供一个 Bean 的实现:

@Bean
public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {
String serviceId = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
ObjectProvider<ServiceInstanceListSupplier>
serviceInstanceListSupplierProvider
= loadBalancerClientFactory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class);
return new ReactorServiceInstanceLoadBalancer() {...}
}

在这里面可以获取请求头,然后选择租户版本进行过滤。

在获取请求头时还需要针对 Feign 进行处理,默认新请求是不会传递请求头的,需要通过 Feign 拦截器传递(参考 ​​Spring Cloud Alibaba 本地调试方案​​)。

实现了这两大块就能支持按租户的灰度访问了。

配置租户服务版本

上面的主要流程解决后,剩下的这个问题就简单了,可以提供简单的界面配置,也可以在升级过程中通过 API 调用设置新版本。

全部已升级到最新版本

等所有租户都升级到最新版本后,低版本的服务就可以去掉了。

此时可以保留所有租户服务的版本配置,也可以清空所有版本配置,同时通过全局的默认服务版本进行控制。