灰度发布--Spring Cloud Gray

特性

  • 支持灰度调用
  • 通过feign,restTemplate(通过注解@LoadBalanced 基于ribbon实现负载均衡)调用,支持灰度追踪
  • 支持自动注册为灰度服务,默认不自动注册
  • 优先走灰度服务,其次走正常服务
  • 支持修改服务状态,以此实现破窗能力
  • 通过破窗能力,实现蓝绿发布
  • 其它待补充

介绍

设计思想见
Spring Cloud Gray - 微服务灰度中间件

结构划分

  • spring-cloud-gray-client
    定义了一套灰度路由决策模型,灰度信息追踪模型,以及和spring-cloud-gray-server的基本通信功能。

  • spring-cloud-gray-client-netflix
    在spring-cloud-gray-client的基础上集成了微服务注册中心eureka,扩展ribbon的负载均衡规则,提供了对zuul,feign,RestTemplate的灰度路由能力,并且无缝支持hystrix线程池隔离。

  • spring-cloud-gray-server 管控端
    负责灰度决策、灰度追踪等信息的管理以及持久化。

  • spring-cloud-gray-webui 管控端
    提供操作界面。

GrayServer 部署、配置、使用

部署操作界面web-ui

部署后端服务

  • 添加pom依赖
<dependency>
    <groupId>cn.springcloud.gray</groupId>
    <artifactId>spring-cloud-starter-gray-server</artifactId>
    <version>A.1.1.2</version>
</dependency>
  • application.yaml
    只需修改spring.datasource、eureka-default-Zone即可
server:
  port: 20202
spring:
  main:
    allow-bean-definition-overriding: true
  application:
    name: gray-Server

  #通用数据源配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://xxxxxxxx:xxxx/gray_server?charset=utf8mb4&useSSL=false
    username: xxxxxx
    password: xxxxxx
    # Hikari 数据源专用配置
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
  # JPA 相关配置
  jpa:
    open-in-view: false
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
    show-sql: true
    generate-ddl: true
    hibernate:
      ddl-auto: update
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    serviceUrl:
      defaultZone: http://localhost:20001/eureka/
    registry-fetch-interval-seconds: 5

gray:
  server:
    discovery:
      evictionEnabled: true
      evictionIntervalTimerInMs: 60000
    instance:
      #正常的实例状态,默认为STARTING, UP  
      normalInstanceStatus: STARTING,UP
      eviction:
        enabled: true
        evictionIntervalTimerInMs: 86400000
        evictionInstanceStatus: DOWN,UNKNOWN
        lastUpdateDateExpireDays: 1
  • 创建Application类(注解 @EnableGrayServer)
  • 初始化信息
--- 添加一个用户名为admin,密码是abc123的用户
insert into `user` ( `user_id`, `account`, `name`, `password`, `roles`, `status`, `create_time`, `operator`, `operate_time`) values ( 'admin', 'admin', 'Admin', 'e7a57e51394e91cba19deca3337bfab0', 'admin', '1', now(), 'admin', now());
--- 添加默认的namespace
INSERT INTO `gray_server`.`namespace`(`code`, `create_time`, `creator`, `del_flag`, `name`) VALUES ('default', '2021-07-19 09:55:19', 'admin', b'0', 'default');
--- 为namespace配置权限
INSERT INTO `gray_server`.`default_namespace`(`user_id`, `ns_code`) VALUES ('admin', 'default');
--- 为用户设置资源权限
INSERT INTO `gray_server`.`user_resource_authority`(`id`, `authority_flag`, `del_flag`, `operate_time`, `operator`, `resource`, `resource_id`, `user_id`) VALUES (1, 9, b'0', '2021-07-19 09:55:19', 'admin', 'namespace', 'default', 'admin');

服务端配置

用户管理

在这里插入图片描述

配置namespace

可以通过初始化信息录入,默认为default;
在这里插入图片描述

配置策略

在这里插入图片描述

  • 定义灰度策略
  • 为灰度策略设置决策(track开头的一般为灰度追踪),可以同时设置多个决策

注意:决策是灰度中进行比对的最小项。它定义一种规则,对请求进行比对,返回 true/false。当请求调用时,灰度调用端可以根据灰度实例的灰度决策,进行对比,以判断灰度实例是否可以受理该请求。多个决策是"与"的关系。

配置灰度服务
定义服务owner

在这里插入图片描述
权限控制是以服务为对象的,拥有服务的权限,就可以操作服务的所有灰度信息。
(service_owner、user_service_authority)

在服务的权限控制中,分为两种角色,owner和管理者,owner拥有最大的权限,管理者除了不能删除owner的权限,其它权限同owner一样。(注意:必须先有owner,service列表才能查询出来;然后再由owner分配管理者)

维护服务列表

在这里插入图片描述

  1. 配置服务灰度,关联策略,可以关联多个;
    只要任一策略满足要求即可;
    在这里插入图片描述
  2. 配置多版本灰度

在这里插入图片描述
version应当与eureka.instance.metadata-map中的version相对应,如

eureka:
  instance:
    metadata-map:
      version: v3
      instanceId: s
  1. 维护灰度实例
  • 点击实例,在实例列表中维护;
  • 选择实例,点击策略,维护实例与策略间的关联关系;

注意:
灰度状态是用来控制实例是被灰度,当灰度状态打开时,只有匹配该实例的任意灰度策略的请求,才会被转到到该实例上。
灰度实例支持自动注册到服务端,默认不自动注册;
一个实例可以有多个灰度策略,策略与策略之间是"或"的关系。就是说,一个请求只要 满足实例的任意一个灰度策略,这个请求被路由到该实例上。

  1. 维护灰度追踪
    灰度追踪分为两部分,这里配置的是透传项,对比部分在灰度策略

设置灰度追踪的目的是为了将用户请求的最初的信息透传到服务链,比如version参数能够从网关一直透传到后面的服务中。
弹出添加面板,输入追踪类型(Name)和追踪字段(Infos)。
Name: HttpParameter Infos: version
Infos 可以追踪多个字段,多个字段用逗号(,)分隔

注意
在服务灰度、灰度实例修改决策时是全局的,会影响到该决策的所有服务、实例;
对灰度实例、服务灰度、灰度策略的修改,请通过服务端去处理,而不要直接通过数据库去修改。因为客户端会将数据缓存到内存中(concurrenthashmap及caffine中),在服务端修改后会发送事件,而客户端会通过定时任务访问服务端的事件日志(gray_event_log)来更新本地缓存。

客户端配置、使用

eureka

<dependency>
    <artifactId>spring-cloud-gray-utils</artifactId>
    <groupId>cn.springcloud.gray</groupId>
    <version>${project.version}</version>
</dependency>

gateway

<dependency>
    <groupId>cn.springcloud.gray</groupId>
    <artifactId>spring-cloud-starter-gray-client</artifactId>
    <version>D.0.0.2</version>
    <exclusions>
        <exclusion>
            <artifactId>spring-cloud-gray-plugin-webmvc</artifactId>
            <groupId>cn.springcloud.gray</groupId>
        </exclusion>
<!--                <exclusion>-->
<!--                    <groupId>cn.springcloud.gray</groupId>-->
<!--                    <artifactId>spring-cloud-gray-plugin-eureka</artifactId>-->
<!--                </exclusion>-->
    </exclusions>
</dependency>

<dependency>
    <groupId>cn.springcloud.gray</groupId>
    <artifactId>spring-cloud-gray-plugin-gateway</artifactId>
    <version>D.0.0.2</version>
</dependency>

client

<dependency>
    <groupId>cn.springcloud.gray</groupId>
    <artifactId>spring-cloud-starter-gray-client</artifactId>
    <version>D.0.0.2</version>
</dependency>
<dependency>
    <groupId>cn.springcloud.gray</groupId>
    <artifactId>spring-cloud-gray-plugin-feign</artifactId>
    <version>D.0.0.2</version>
</dependency>

注意:灰度追踪配置,同时支持在gray-server及在client的yaml中配置,优先以gray-server配置为准

表结构

在这里插入图片描述

流程浅析

服务选择过程

  1. 配置eureka自定义metadata
eureka:
 instance:
   metadata-map:
     version: v3
     #      zone: gray3
     instanceId: s
#    initial-status: starting
  1. 获取eureka.metadata
org.springframework.cloud.netflix.ribbon.eureka.EurekaServerIntrospector#getMetadata
  1. 将所有实例划分为灰度服务、正常服务
    3.1. 先做服务级别的拆分
    1)、只有在服务级别配置服务灰度或多版本灰度,才会走服务级别的拆分
    2)、如果配置了服务灰度,则实例需要满足灰度策略(filterServiceGrayPolicies判断),才会是有效的,可以被继续筛选
    3)、如果配置了服务灰度,但没有配置多版本灰度,则grayServer为空,2)筛选后的列表都会是正常实例;
    4)、如果配置了多版本灰度,则eureka.instance.metadata-map中的version与多版本灰度的version一致,才能被列为灰度实例;
    5)、根据多版本灰度中关联的灰度策略来判断,实例是否是有效的,可以被继续筛选;
    6)、如果灰度策略没有配置灰度决策,那么任一灰度都可以判断通过;
cn.springcloud.gray.choose.ServiceGrayServerSorter#distinguishServerSpecList
@Override
protected ServerListResult<ServerSpec<SERVER>> distinguishServerSpecList(String serviceId, List<ServerSpec<SERVER>> serverSpecs) {
    List<ServerSpec<SERVER>> serverSpecList = serverSpecs;
    GrayManager grayManager = getGrayManager();
    GrayService grayService = grayManager.getGrayService(serviceId);
    if (Objects.nonNull(grayService) && !grayService.getRoutePolicies().isEmpty()) {
        serverSpecList = filterServiceGrayPolicies(grayService.getRoutePolicies().getDatas(), serverSpecs);
    }

    Collection<String> multiVersions = getMultiVersions(grayService);
    if (CollectionUtils.isEmpty(multiVersions)) {
        return new ServerListResult<>(serviceId, Collections.EMPTY_LIST, serverSpecList);
    }

    List<ServerSpec<SERVER>> grayServerSpecs = new ArrayList<>(serverSpecList.size());
    List<ServerSpec<SERVER>> normalServerSpecs = new ArrayList<>(serverSpecList.size());
    serverSpecList.forEach(serverSpec -> {
        if (StringUtils.isNotEmpty(serverSpec.getVersion()) && multiVersions.contains(serverSpec.getVersion())) {
            grayServerSpecs.add(serverSpec);
        } else {
            normalServerSpecs.add(serverSpec);
        }
    });
    return new ServerListResult<>(serviceId, grayServerSpecs, normalServerSpecs);
}

策略

一个实例可以有多个灰度策略,策略与策略之间是"或"的关系。就是说,一个请求只要 满足实例的任意一个灰度策略,这个请求被路由到该实例上。
cn.springcloud.gray.choose.AbstractPolicyPredicate#testPolicies

@Override
public boolean testPolicies(Collection<Policy> policies, DecisionInputArgs decisionInputArgs) {
    if (Objects.isNull(policies) || policies.size() < 1) {
        return false;
    }
    for (Policy policy : policies) {
        if (policy.predicateDecisions(decisionInputArgs)) {
            return true;
        }
    }
    return false;
}

决策

决策是灰度中进行比对的最小项。它定义一种规则,对请求进行比对,返回 true/false。当请求调用时,灰度调用端可以根据灰度实例的灰度决策,进行对比,以判断灰度实例是否可以受理该请求。多个决策是"与"的关系。

public boolean predicateDecisions(DecisionInputArgs args) {
    for (GrayDecision decision : decisions) {
        if (!decision.test(args) ) {
            return false;
        }
    }
    return true;
}
5.根据多版本灰度中关联的灰度策略来判断
cn.springcloud.gray.choose.ServiceGrayServerSorter#filterServerSpecAccordingToRoutePolicy(java.lang.String, java.util.List<cn.springcloud.gray.servernode.ServerSpec<SERVER>>)

cn.springcloud.gray.choose.ServiceGrayServerSorter#filterServerSpecAccordingToRoutePolicy(cn.springcloud.gray.model.GrayService, cn.springcloud.gray.choose.PolicyPredicate, java.util.List<cn.springcloud.gray.servernode.ServerSpec<SERVER>>)

//断言service 多版本灰度策略,并过滤不匹配的server返回。
private List<ServerSpec<SERVER>> filterServerSpecAccordingToRoutePolicy(
        GrayService grayService, PolicyPredicate policyPredicate, List<ServerSpec<SERVER>> serverSpecs) {
    Map<String, DataSet<String>> multiVersionRoutePoliciesMap = grayService.getMultiVersionRotePolicies();
    Map<String, List<Policy>> multiVersionPolicies = new HashMap<>();
    return serverSpecs.stream().filter(serverSpec -> {
        String version = serverSpec.getVersion();
        List<Policy> policies = multiVersionPolicies.get(version);
        if (Objects.isNull(policies)) {
            policies = Collections.EMPTY_LIST;
            DataSet<String> routePolicies = multiVersionRoutePoliciesMap.get(version);
            if (Objects.nonNull(routePolicies)) {
                policies = policyDecisionManager.getPolicies(routePolicies.getDatas());
            }
            multiVersionPolicies.put(version, policies);
        }

        return !CollectionUtils.isEmpty(policies) && policyPredicate.testPolicies(policies, createDecisionInputArgs(serverSpec));
    }).collect(Collectors.toList());
}

3.2. 再做实例级别的拆分
1)、先根据配置区分灰度实例、正常实例
2)、再判断灰度实例是否符合策略(filterServerSpecAccordingToRoutePolicy)

cn.springcloud.gray.choose.InstanceGrayServerSorter#distinguishServerSpecList
  1. 在客户端根据服务列表选择服务实例去调用
    chooseServer::轮询(无论是正常服务,灰度服务,或是所有服务)
cn.springcloud.gray.choose.DefaultServerChooser#chooseServer
@Override
public Object chooseServer(List<Object> servers, ListChooser<Object> chooser) {
    if (!graySwitcher.state()) {
        log.debug("灰度未开启,从servers列表挑选");
        return chooser.choose(ChooseGroup.ALL, servers);
    }

    String serviceId = serverIdExtractor.getServiceId(servers);
    List<Object> serverList = serverListProcessor.process(serviceId, servers);

    if (!grayManager.hasServiceGray(serviceId)) {
        log.debug("{} 服务没有相关灰度策略, 从serverList列表进行灰度策选, serverList.size={}", serviceId, serverList.size());
        return chooseInstanceServer(serverList, chooser);
    }


    List<ServerSpec<Object>> serverSpecs = serverExplainer.apply(servers);
    //区分灰度实例和正常实例,并比对灰度实例的灰度决策
    ServerListResult<ServerSpec<Object>> serviceServerSpecListResult =
            serviceGrayServerSorter.distinguishAndMatchGrayServerSpecList(serverSpecs);

    if (Objects.isNull(serviceServerSpecListResult)) {
        log.debug("区分 {} 服务灰度列表和正常列表失败, 从serverList列表进行灰度策选, serverList.size={}", serviceId, serverList.size());
        return chooseInstanceServer(serverList, chooser);
    }

    return chooseServiceSpecServer(serviceServerSpecListResult, chooser);
}


如果灰度服务不满足要求,则走正常服务
protected Object chooseServiceSpecServer(ServerListResult<ServerSpec<Object>> serviceServerSpecListResult, ListChooser<Object> chooser) {
    Object server = null;
    if (GrayClientHolder.getGraySwitcher().isEanbleGrayRouting()) {
        server = chooseInstanceSpecServer(serviceServerSpecListResult.getGrayServers(), chooser);
        log.debug("从{}服务的灰度实例列表中挑选到 {}", serviceServerSpecListResult.getServiceId(), server);
    }
    if (Objects.isNull(server)) {
        server = chooseInstanceSpecServer(serviceServerSpecListResult.getNormalServers(), chooser);
        log.debug("从{}服务的正常实例列表中挑选到 {}", serviceServerSpecListResult.getServiceId(), server);
    }
    return server;
}


protected Object chooseInstanceServer(List<Object> servers, ListChooser<Object> chooser) {
    ServerListResult<Object> serverListResult = instanceGrayServerSorter.distinguishAndMatchGrayServerList(servers);
    if (serverListResult == null) {
        return chooser.choose(ChooseGroup.ALL, servers);
    }

    if (GrayClientHolder.getGraySwitcher().isEanbleGrayRouting()
            && CollectionUtils.isNotEmpty(serverListResult.getGrayServers())) {
        Object server = chooser.choose(ChooseGroup.GRAY, serverListResult.getGrayServers());
        if (server != null) {
            return server;
        }
    }

    return chooser.choose(ChooseGroup.NORMAL, serverListResult.getNormalServers());
}

chooseServer::轮询(无论是正常服务,灰度服务,或是所有服务)
并不直接使用RoundRobinRule
而是基于ClientConfigEnabledRoundRobinRule的抽象类PredicateBasedRule;在choose方法中,通过AbstractServerPredicate的chooseRoundRobinAfterFiltering函数来选择具体的服务实例。(支持轮询或随机)
cn.springcloud.gray.client.netflix.ribbon.GrayChooserRule#choose

public Server choose(Object key) {
    try {
        return serverChooser.chooseServer(getLoadBalancer().getAllServers(), (group, servers) -> {
            Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(servers, key);
            if (server.isPresent()) {
                return server.get();
            } else {
                return null;
            }
        });
    } catch (Exception e) {
        log.warn("gray choose server occur exception:{}, execute super method.", e.getMessage(), e);
        return super.choose(key);
    }
}

灰度追踪

首先,服务拆分是相同的
其次,灰度追踪采用的策略是哪种呢??
举个例子,以httpHeader为例,满足策略的不是httpHeaderPolicy,而是httpTrackHeadPolicy。

通过网关请求service-b,service-b通过feign访问service-a
localhost:20401/ser-b/api/test/feignGet
即service-b配置灰度追踪,service-a 灰度实例上配置灰度策略为httpTrackHeader

HttpTrackHeaderGrayDecisionFactory : 
[HttpTrackHeaderGrayDecision] serviceId:service-a, uri:http://service-a/api/test/get 没有获取到灰度追踪信息, testReslut:false

在这里插入图片描述
在这里插入图片描述

事件监听

gray:
  enabled: true
  server:
    url: http://gray-Server
    loadbalanced: true
    retryable: true
    retryNumberOfRetries: 3

cn.springcloud.gray.refresh.GrayInformationRefresher#publishRefreshedEvent
启动完成后,会发布jvm级别的一个事件(cn.springcloud.gray.refresh.GrayRefreshedEvent),让服务准备去监听跨服务的事件

cn.springcloud.gray.client.plugin.event.longpolling.GrayRefreshedSortMarkListener#onApplicationEvent(GrayRefreshedEvent event)

启动cn.springcloud.gray.client.plugin.event.longpolling.LongPollingWorker#LongPollingWorker

executor = Executors.newScheduledThreadPool(1,
        new DefaultThreadFactory("gray.client.event.longPolling"));

executorService = Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors(),
        new DefaultThreadFactory("gray.client.event.retrieveResult.process"));

启动监听cn.springcloud.gray.client.plugin.event.longpolling.LongPollingWorker#listenEvents

监听gray-server配置的事件(即日志)
cn.springcloud.gray.client.plugin.event.longpolling.GrayEventRemoteClient#listeningNewestStatus
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值