Skip to content

09. 使用支付服务为例实现灰度发布(金丝雀测试)(0.0.6)

zhengyangyong edited this page Jul 5, 2018 · 5 revisions

0.0.5-SNAPSHOT中我们已经完成了支付服务,现在我们以它为例在0.0.6-SNAPSHOT中修改边缘服务实现灰度发布(金丝雀测试)。

什么是灰度发布(金丝雀测试)

以前矿工下矿洞前,会先放一只金丝雀进去试探氧气含量以及是否有有毒气体,金丝雀测试由此得名;生产环境中微服务都是多实例运行的,更新(发布)微服务,为避免风险,往往先更新1个实例,或一个很小的比例例如1%,承担很低的流量做验证,如果没有问题,则使用手工或自动的方式逐步更新剩余的实例,完成整体升级。

发布前:

Start

发布中:

Do

发布结束:

Finish

修改边缘服务支持灰度发布

路由策略

灰度发布需要边缘服务支持动态设置路由策略,常见的路由策略有:

  1. 基于匹配特定Header的策略

这个策略是通过匹配指定Header的值,决定转发微服务的版本;例如我们上线新版本后,用户界面提示是否尝试新版本,如果用户选择切换,则将特定的值写入请求的Header,此用户的所有后继请求都路由至新版本。这种方式给予了用户很大的灵活性和选择权,缺点是无法控制流量(因为完全要看大多数用户的选择)。

  1. 基于用户标识的策略

这个策略是通过匹配用户的标识例如ID,决定转发微服务的版本;例如我们上线新版本后,只将以Test开头的测试账号的请求路由至新版本。这种方式对客户端透明,也能够控制流量(限制了账号数),缺点是需要结合用户系统做一定的修改才能够支持,因为边缘服务无法直接获取用户信息。

  1. 基于流量控制的策略

这个策略是通过定制弹性伸缩(loadbalance)策略,决定转发微服务的版本;例如如果使用的是RoundRobin,则路由比例与版本实例数成正比,如果我们希望路由到新版本的流量为一个比较低的值例如1%但实例数比例远高于此值,则需要做定制。

  1. 基于匹配调用方IP的策略

这个策略是通过匹配调用发起方的IP,决定转发微服务的版本;例如在多地域多集群环境下,可以通过IP识别对方大致身份(例如所属省份、子系统等)。

我们先实现第一种策略。

增加灰度配置

我们改进微服务配置中与edge相关的部分,增加灰度发布策略:

edge:
  routing-short-path:
    user: user-service
    payment: payment-service
  dark-launch-rules:
    #如果请求的User-Agent中包含vip,则指向0.0.2版本的payment-service(payment-service-canary)
    payment-service: "{\"headerRules\":{\"User-Agent\":[{\"operator\":\"CONTAINS\",\"value\":\"vip\",\"version\":\"0.0.2\"}]},\"defaultVersion\":\"0.0.1\"}"

edge.dark-launch-rules.payment-service配置定义了payment-service对应的灰度规则,JSON格式,层次结构说明如下:

headerRules:Header匹配规则集合
  {Header名}:要匹配的Header名,例如User-Agent
    [匹配规则集合]:规则集合,每一个规则包含operator,value和version
      operator:匹配操作符,支持START_WITH,END_WITH,CONTAINS和EQUALS
      value:匹配的值
      version:如果匹配,路由版本
defaultVersion:如果没有匹配任何规则,则默认版本(一般设置为老版本)

因此可以看出上面的payment-service微服务配置的灰度策略为:如果存在User-Agent头并且其中的内容包含vip这个字符串,则路由到0.0.2版本,否则默认路由到0.0.1版本

修改EdgeDispatcher增加灰度逻辑

通过策略配置决定路由版本:

private void onRequest(RoutingContext context) {
  Map<String, String> pathParams = context.pathParams();
  //从匹配的param0拿到{ServiceComb微服务Name}
  final String service = pathParams.get("param0");
  //从匹配的param1拿到{服务路径&参数}
  String operationPath = "/" + pathParams.get("param1");

  //还记得我们之前说的做出一点点改进吗?引入一个自定义配置edge.routing-short-path.{简称},映射微服务名;如果简称没有配置,那么就认为直接是微服务的名
  final String serviceName = DynamicPropertyFactory.getInstance()
      .getStringProperty("edge.routing-short-path." + service, service).get();

  darkLaunchRules.computeIfAbsent(serviceName, s -> {
    DynamicStringProperty property = DynamicPropertyFactory.getInstance()
        .getStringProperty("edge.dark-launch-rules." + serviceName, "");
    return parseRule(property.getValue());
  });

  //创建一个Edge转发
  EdgeInvocation edgeInvocation = new EdgeInvocation();
  //设定灰度版本策略
  edgeInvocation.setVersionRule(darkLaunchRules.get(serviceName).matchVersion(context.request().headers().entries()));
  edgeInvocation.init(serviceName, context, operationPath, httpServerFilters);

  //处理Filter链并转发请求
  loopExecuteEdgeFilterInChain(0, serviceName, operationPath, context, edgeInvocation);
}

private DarkLaunchRule parseRule(String config) {
  try {
    if (StringUtils.isNotEmpty(config)) {
      return OBJ_MAPPER.readValue(config, DarkLaunchRule.class);
    }
  } catch (IOException e) {
    LOGGER.error("parse rule failed", e);
  }
  return new DarkLaunchRule();
}

实现一个新版本的支付服务

场景

老版本的支付服务做风控,授信额度不能超过一百万,现在经济形势较好,信贷宽松,因此能尝试对vip用户将授信额度提升为两百万,为稳妥起见,采用灰度发布逐步升级。

创建新的支付服务payment-service-canary

为了能使两个版本的代码并存,我们将payment-service中的代码和配置全部拷贝进入新的微服务payment-service-canary(实际的开发并不需要这么做),并修改:

升级微服务版本

首先我们需要修改微服务的版本:

APPLICATION_ID: scaffold
service_description:
  name: payment-service
  #版本从0.0.1升级为0.0.2
  version: 0.0.2

修改风控逻辑

//信用额度
private static final double CREDIT_LIMIT = 2000000;

可以看到,相比0.0.1版本中的CREDIT_LIMIT,授信额度审查已经被放宽到两百万。

验证效果

我们启动边缘服务、用户服务和老版本的支付服务,使用zhengyangyong账户登录系统,然后做一笔一百万的支付:

第一次支付

支付成功,现在我们再来做一笔金额为888的支付:

失败

结果返回余额不足,因为老版本的支付服务只允许一百万额度,现在我们启动一个新版本的支付服务实例(模拟灰度发布),之后在Service Center UI中,我们已经可以看到新旧版本的支付系统并存:

新旧并存

再次请求:

仍然失败

仍然失败,我们在Header中增加User-Agent并设置值为vip:

成功

返回成功,表明请求已经被路由至额度检查更宽松的0.0.2版本微服务实例。