如果你是运维负责人,是否会经常发现,你掌管的测试环境中的服务注册中心,被一些不负责的开发人员把他本地开发环境注册上来,造成测试人员测试失败。你希望可以把本地开发环境注册给屏蔽掉,不让注册。
如果你是运维负责人,生产环境的某个微服务集群下的某个实例,暂时出了问题,但又不希望它下线。你希望可以把该实例给屏蔽掉,暂时不让它被调用。
如果你是业务负责人,鉴于业务服务的快速迭代性,微服务集群下的实例发布不同的版本。你希望根据版本管理策略进行路由,提供给下游微服务区别调用,例如访问控制快速基于版本的不同而切换,例如在不同的版本之间进行流量调拨。
如果你是业务负责人,希望灰度发布功能可以基于业务场景特色定制,例如根据用户手机号进行不同服务器的路由。
如果你是DBA负责人,希望灰度发布功能可以基于数据库切换上。
如果你是测试负责人,希望对微服务做A/B测试,那么通过动态改变版本达到该目的。
我们可以在网关上根据不同的Token查询到不同的用户,把请求路由到指定的服务器。
我们可以在服务上根据不同的业务参数,例如手机号或者身份证号,把请求路由到指定的服务器。
具有极大的灵活性——支持在任何环节做过滤控制和灰度发布。
具有极小的限制性——只要开启了服务注册发现,程序入口加了@EnableDiscoveryClient。
具有极强的可用性——当远程配置中心全部挂了,可以通过Rest方式进行灰度发布。
基于黑/白名单的IP地址过滤机制禁止对相应的微服务进行注册。
基于最大注册数的限制微服务注册。一旦微服务集群下注册的实例数目已经达到上限,将禁止后续的微服务进行注册。
基于黑/白名单的IP地址过滤机制禁止对相应的微服务被发现。
基于版本号配对,通过对消费端和提供端可访问版本对应关系的配置,在服务发现和负载均衡层面,进行多版本访问控制。
基于版本权重配对,通过对消费端和提供端版本权重(流量)对应关系的配置,在服务发现和负载均衡层面,进行多版本流量调拨访问控制。
通过版本的动态改变,实现切换灰度发布。
通过版本访问规则的改变,实现切换灰度发布。
通过版本权重规则的改变,实现平滑灰度发布。
对接远程配置中心,集成Nacos和Redis,异步接受远程配置中心主动推送规则信息,动态改变微服务的规则。
结合Spring Boot Actuator,异步接受Rest主动推送规则信息,动态改变微服务的规则,支持同步和异步推送两种方式。
结合Spring Boot Actuator,动态改变微服务的版本,支持同步和异步推送两种方式。
在服务注册层面的控制中,一旦禁止注册的条件触发,主动推送异步事件,以便使用者订阅。
使用者可以对服务注册发现核心事件进行监听,实现通过扩展,用户自定义和编程灰度路由策略。
使用者可以实现跟业务有关的路由策略,根据业务参数的不同,负载均衡到不同的服务器。
使用者可以根据内置的版本路由策略+自定义策略,随心所欲的达到需要的路由功能。
在application.properties或者application.yml中,必须为微服务定义一个版本号(version),必须为微服务自定义一个组名(group)或者应用名(application)。
使用者只需要关注相关规则推送。可以采用如下方式之一:
通过远程配置中心推送规则
通过控制台界面推送规则
通过客户端工具(例如Postman)推送
<register>
<blacklist>
<service service-name="a" filter-value="172.16"/>
blacklist>
<whitelist>
<service service-name="a" filter-value="10.10"/>
whitelist>
register>
黑名单,表示a服务注册到服务注册发现中心,如果它的IP起始为172.16,那么禁止注册。
<register>
<count>
<service service-name="a" filter-value="500"/>
count>
register>
表示a服务注册到服务注册发现中心,当达到500个后,更多的实例将无法注册。
<discovery>
<blacklist>
<service service-name="a" filter-value="172.16"/>
blacklist>
<whitelist>
<service service-name="a" filter-value="10.10"/>
whitelist>
discovery>
黑名单,表示a服务的IP起始为172.16,那么禁止被发现。
<discovery>
<version>
<service consumer-service-name="a" provider-service-name="b" consumer-version-value="1.0" provider-version-value="1.0"/>
<service consumer-service-name="a" provider-service-name="b" consumer-version-value="1.1" provider-version-value="1.1"/>
version>
discovery>
表示消费端服务a的1.0,允许访问提供端服务b的1.0版本。
<discovery>
<weight>
<service consumer-service-name="a" provider-service-name="b" provider-weight-value="1.0=90;1.1=10"/>
weight>
discovery>
表示消费端服务a访问提供端服务b的时候,提供端服务b的1.0版本提供90%的权重流量,1.1版本提供10%的权重流量。
<customization>
<service service-name="discovery-springcloud-example-b" key="database" value="prod"/>
customization>
表示服务b有两个库的配置,分别是临时数据库(database的value为temp)和生产数据库(database的value为prod)。
网关不需要配置版本
网关->服务A(V1.0),网关配给服务A(V1.0)的100%权重(流量)
上线服务A(V1.1)
在网关层调拨10%权重(流量)给A(V1.1),给A(V1.0)的权重(流量)减少到90%
通过观测确认灰度有效,把A(V1.0)的权重(流量)全部切换到A(V1.1)
下线服务A(V1.0),灰度成功
假设当前生产环境,调用路径为网关(V1.0)->服务A(V1.0)
运维将发布新的生产环境,部署新服务集群,服务A(V1.1)
由于网关(1.0)并未指向服务A(V1.1),所以它们是不能被调用的
新增用作灰度发布的网关(V1.1),指向服务A(V1.1)
灰度网关(V1.1)发布到服务注册发现中心,但禁止被服务发现,网关外的调用进来无法负载均衡到网关(V1.1)上
在灰度网关(V1.1)->服务A(V1.1)这条调用路径做灰度测试
灰度测试成功后,把网关(V1.0)指向服务A(V1.1)
下线服务A(V1.0),灰度成功
灰度网关(V1.1)可以不用下线,留作下次版本上线再次灰度发布
如果您对新服务比较自信,可以更简化,可以不用灰度网关和灰度测试,当服务A(V1.1)上线后,原有网关直接指向服务A(V1.1),然后下线服务A(V1.0)
基于服务的编程灰度路由,实现DiscoveryEnabledExtension,通过RequestContextHolder(获取来自网关的Header参数)和ServiceStrategyContext(获取来自RPC方式的方法参数)获取业务上下文参数,进行路由自定义
基于Zuul的编程灰度路由,实现DiscoveryEnabledExtension,通过Zuul自带的RequestContext(获取来自网关的Header参数)获取业务上下文参数,进行路由自定义
基于Spring Cloud Api Gateway的编程灰度路由,实现DiscoveryEnabledExtension,通过GatewayStrategyContext(获取来自网关的Header参数)获取业务上下文参数,进行路由自定义
@Bean
public MyDiscoveryEnabledExtension myDiscoveryEnabledExtension() {
return new MyDiscoveryEnabledExtension();
}
表示在网关层(以Zuul为例),编程灰度路由策略,如下代码,表示请求的Header中的token包含'abc',在负载均衡层面,对应的服务示例不会被负载均衡到。
public class MyDiscoveryEnabledExtension implements DiscoveryEnabledExtension {
private static final Logger LOG = LoggerFactory.getLogger(MyDiscoveryEnabledExtension.class);
@Override
public boolean apply(Server server, Map<String, String> metadata) {
// 对Rest调用传来的Header参数(例如Token)做策略
return applyFromHeader(server, metadata);
}
// 根据Rest调用传来的Header参数(例如Token),选取执行调用请求的服务实例
private boolean applyFromHeader(Server server, Map<String, String> metadata) {
RequestContext context = RequestContext.getCurrentContext();
String token = context.getRequest().getHeader("token");
// String value = context.getRequest().getParameter("value");
String serviceId = server.getMetaInfo().getAppName().toLowerCase();
LOG.info("Zuul端负载均衡用户定制触发:serviceId={}, host={}, metadata={}, context={}", serviceId, server.toString(), metadata, context);
String filterToken = "abc";
if (StringUtils.isNotEmpty(token) && token.contains(filterToken)) {
LOG.info("过滤条件:当Token含有'{}'的时候,不能被Ribbon负载均衡到", filterToken);
return false;
}
return true;
}
}
表示在服务层,当服务名为discovery-springcloud-example-c,同时版本为1.0,同时参数value中包含'abc',三个条件同时满足的情况下,在负载均衡层面,对应的服务示例不会被负载均衡到。
public class MyDiscoveryEnabledExtension implements DiscoveryEnabledExtension {
private static final Logger LOG = LoggerFactory.getLogger(MyDiscoveryEnabledExtension.class);
@Override
public boolean apply(Server server, Map metadata) {
// 对RPC调用传来的方法参数做策略
return applyFromMethd(server, metadata);
}
// 根据RPC调用传来的方法参数(例如接口名、方法名、参数名或参数值等),选取执行调用请求的服务。
@SuppressWarnings("unchecked")
private boolean applyFromMethd(Server server, Map<String, String> metadata) {
ServiceStrategyContext context = ServiceStrategyContext.getCurrentContext();
Map<String, Object> attributes = context.getAttributes();
String serviceId = server.getMetaInfo().getAppName().toLowerCase();
String version = metadata.get(DiscoveryConstant.VERSION);
LOG.info("Serivice端负载均衡用户定制触发:serviceId={}, host={}, metadata={}, context={}", serviceId, server.toString(), metadata, context);
String filterServiceId = "discovery-springcloud-example-b";
String filterVersion = "1.0";
String filterBusinessValue = "abc";
if (StringUtils.equals(serviceId, filterServiceId) && StringUtils.equals(version, filterVersion)) {
if (attributes.containsKey(ServiceStrategyConstant.PARAMETER_MAP)) {
Map<String, Object> parameterMap = (Map<String, Object>) attributes.get(ServiceStrategyConstant.PARAMETER_MAP);
String value = parameterMap.get("value").toString();
if (StringUtils.isNotEmpty(value) && value.contains(filterBusinessValue)) {
LOG.info("过滤条件:当serviceId={} && version={} && 业务参数含有'{}'的时候,不能被Ribbon负载均衡到", filterServiceId, filterVersion, filterBusinessValue);
return false;
}
}
}
return true;
}
}
@EventBus
public class MySubscriber {
@Autowired
private PluginAdapter pluginAdapter;
@Subscribe
public void onCustomization(CustomizationEvent customizationEvent) {
CustomizationEntity customizationEntity = customizationEvent.getCustomizationEntity();
String serviceId = pluginAdapter.getServiceId();
if (customizationEntity != null) {
Map<String, Map<String, String>> customizationMap = customizationEntity.getCustomizationMap();
Map<String, String> customizationParameter = customizationMap.get(serviceId);
// 根据customizationParameter的参数动态切换数据源
} else {
// 根据customizationParameter的参数动态切换数据源
}
}
@Subscribe
public void onRegisterFailure(RegisterFailureEvent registerFailureEvent) {
System.out.println("========== 注册失败, eventType=" + registerFailureEvent.getEventType() + ", eventDescription=" + registerFailureEvent.getEventDescription() + ", serviceId=" + registerFailureEvent.getServiceId() + ", host=" + registerFailureEvent.getHost() + ", port=" + registerFailureEvent.getPort());
}
}
用户自定义灰度发布监听
public class MyRegisterListener extends AbstractRegisterListener {
@Override
public void onRegister(Registration registration) {
}
@Override
public void onDeregister(Registration registration) {
}
@Override
public void onSetStatus(Registration registration, String status) {
}
@Override
public void onClose() {
}
}
用户实现服务发现的监听,当负服务发现过滤的时候,用户会收到下面的事件。
public class MyDiscoveryListener extends AbstractDiscoveryListener {
public void onGetInstances(String serviceId, List
}
public void onGetServices(List
}
}
public class MyLoadBalanceListener extends AbstractLoadBalanceListener {
@Override
public void onGetServers(String serviceId, List extends Server> servers) {
}
}
用户实现对注册失败的监听,当黑名单激活的时候,会触发注册失败,那么用户会收到一个注册失败的事件。
public class MySubscriber {
public void onRegisterFailure(RegisterFailureEvent registerFailureEvent) {
}
}