特性
- 支持灰度调用
- 通过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分配管理者)
维护服务列表
- 配置服务灰度,关联策略,可以关联多个;
只要任一策略满足要求即可;
- 配置多版本灰度
version应当与eureka.instance.metadata-map中的version相对应,如
eureka:
instance:
metadata-map:
version: v3
instanceId: 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: gray3
instanceId: 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: 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