灰度发布--Spring Cloud Gray
灰度发布--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: trueapplication:name: gray-Server#通用数据源配置datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://116.196.83.187:65525/gray_server?charset=utf8mb4&useSSL=falseusername: rootpassword: Zhulong123# Hikari 数据源专用配置hikari:maximum-pool-size: 20minimum-idle: 5# JPA 相关配置jpa:open-in-view: falsedatabase-platform: org.hibernate.dialect.MySQL5InnoDBDialectshow-sql: truegenerate-ddl: truehibernate:ddl-auto: update
eureka:client:register-with-eureka: truefetch-registry: trueserviceUrl:defaultZone: http://localhost:20001/eureka/registry-fetch-interval-seconds: 5gray:server:discovery:evictionEnabled: trueevictionIntervalTimerInMs: 60000instance:#正常的实例状态,默认为STARTING, UP normalInstanceStatus: STARTING,UPeviction:enabled: trueevictionIntervalTimerInMs: 86400000evictionInstanceStatus: DOWN,UNKNOWNlastUpdateDateExpireDays: 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分配管理者)
维护服务列表
- 配置服务灰度,关联策略,可以关联多个;
只要任一策略满足要求即可;
- 配置多版本灰度
version应当与eureka.instance.metadata-map中的version相对应,如
eureka:instance:metadata-map:version: v3instanceId: s
- 维护灰度实例
- 点击实例,在实例列表中维护;
- 选择实例,点击策略,维护实例与策略间的关联关系;
注意:
灰度状态是用来控制实例是被灰度,当灰度状态打开时,只有匹配该实例的任意灰度策略的请求,才会被转到到该实例上。
灰度实例支持自动注册到服务端,默认不自动注册;
一个实例可以有多个灰度策略,策略与策略之间是"或"的关系。就是说,一个请求只要 满足实例的任意一个灰度策略,这个请求被路由到该实例上。
- 维护灰度追踪
灰度追踪分为两部分,这里配置的是透传项,对比部分在灰度策略
设置灰度追踪的目的是为了将用户请求的最初的信息透传到服务链,比如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配置为准
表结构
流程浅析
服务选择过程
- 配置eureka自定义metadata
eureka:instance:metadata-map:version: v3# zone: gray3instanceId: s
# initial-status: starting
- 获取eureka.metadata
org.springframework.cloud.netflix.ribbon.eureka.EurekaServerIntrospector#getMetadata
- 将所有实例划分为灰度服务、正常服务
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
- 在客户端根据服务列表选择服务实例去调用
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: trueserver:url: http://gray-Serverloadbalanced: trueretryable: trueretryNumberOfRetries: 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#LongPollingWorkerexecutor = 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