上一篇介绍了,ribbon的组件。本篇要自己写一个灰度方案。其实就是一个很简单的思维扩散。
前端header请求携带version字段。路由服务根据version去需要对应版本的服务集合,进行或轮询或hash或权重的负载。请求路由到服务上,如果还要调用下游服务,也按照version规则去路由下游服务器。前端未携带版本按照后端服务最高version版本进行路由。
分析如果自己动手写一个灰度方案。需要考虑的因素有几点?
解决方案:
来写一下网关层的实现。
gateway负载规则有一个拦截器
创建负载规则的类信息GrayscaleProperties
public class GrayscaleProperties implements Serializable {
private String version;
private String serverName;
private String serverGroup;
private String active;
private double weight = 1.0D;
}
因为gateway的特殊性LoadBalancerClientFilter过滤器主要解析lb:// 为前缀的路由规则,在通过LoadBalancerClient#choose(String) 方法获取到需要的服务实例,从而实现负载均衡。在这里我们要写自己的负载均衡就需要重新需要重写LoadBalancerClientFilter 过滤器
LoadBalancerClientFilter 介绍:次过滤器作用在url以lb开头的路由,然后利用loadBalancer来获取服务实例,构造目标requestUrl,设置到GATEWAY_REQUEST_URL_ATTR属性中,供NettyRoutingFilter使用。
GatewayLoadBalancerClientAutoConfiguration 在初始化会检测@ConditionalOnBean(LoadBalancerClient.class) 是否存在,如果存在就会加载LoadBalancerClientFilter负载过滤器
以下是源码
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
//判断url 前缀 如不是lb开头的就进行下一个过滤器
if (url == null || (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
return chain.filter(exchange);
}
//根据网关的原始网址。替换exchange url为 http://IP:PORT/path 路径的url
//preserve the original url
addOriginalRequestUrl(exchange, url);
log.trace("LoadBalancerClientFilter url before: " url);
// 这里呢会进行调用真正的负载均衡
final ServiceInstance instance = choose(exchange);
if (instance == null) {
String msg = "Unable to find instance for " url.getHost();
if(properties.isUse404()) {
throw new FourOFourNotFoundException(msg);
}
throw new NotFoundException(msg);
}
URI uri = exchange.getRequest().getURI();
// if the `lb:` mechanism was used, use `` as the default,
// if the loadbalancer doesn't provide one.
String overrideScheme = instance.isSecure() ? "https" : "http";
if (schemePrefix != null) {
overrideScheme = url.getScheme();
}
URI requestUrl = loadBalancer.reconstructURI(new DelegatingServiceInstance(instance, overrideScheme), uri);
log.trace("LoadBalancerClientFilter url chosen: " requestUrl);
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
return chain.filter(exchange);
}
。。。。
// 因为注入了ribbon 会使用ribbon 进行负载均衡规则进行负载
protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
return loadBalancer == null ? null : loadBalancer.chooseServer(hint != null ? hint : "default");
}
如果单单定制了 IRule 的实现类 Server choose(Object key) 方法里面的 key值就是一个默认值。就不知道转发到那个服务。所以要进行重写LoadBalancerClientFilter 这个类的 protected ServiceInstance choose(ServerWebExchange exchange) 进行key的赋值操作
public class GatewayLoadBalancerClientFilter extends LoadBalancerClientFilter {
public GatewayLoadBalancerClientFilter(LoadBalancerClient loadBalancer, LoadBalancerProperties properties) {
super(loadBalancer, properties);
}
@Override
protected ServiceInstance choose(ServerWebExchange exchange) {
if (this.loadBalancer instanceof RibbonLoadBalancerClient) {
RibbonLoadBalancerClient client = (RibbonLoadBalancerClient) this.loadBalancer;
HttpHeaders headers = exchange.getRequest().getHeaders();
String version = headers.getFirst( GrayscaleConstant.GRAYSCALE_VERSION );
String serviceId = ((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost();
GrayscaleProperties build = GrayscaleProperties.builder().version( version ).serverName( serviceId ).build();
//这里使用服务ID 和 version 做为选择服务实例的key
//TODO 这里也可以根据实际业务情况做自己的对象封装
return client.choose(serviceId,build);
}
return super.choose(exchange);
}
}
自定义gateway灰度负载规则
@Slf4j
public class GrayscaleLoadBalancerRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
//留空
}
/**
* gateway 特殊性。需要设置key值内容知道你要转发的服务名称 key已经在filter内设置了key值。
* @param key
* @return
*/
@Override
public Server choose(Object key) {
try {
GrayscaleProperties grayscale = (GrayscaleProperties) key;
String version = grayscale.getVersion();
String clusterName = this.nacosDiscoveryProperties.getClusterName();
NamingService namingService = this.nacosDiscoveryProperties.namingServiceInstance();
List instances = namingService.selectInstances(grayscale.getServerName(), true);
if (CollectionUtils.isEmpty(instances)) {
log.warn("no instance in service {}", grayscale.getServerName());
return null;
} else {
List instancesToChoose = buildVersion(instances,version);
//进行cluster-name分组筛选
// TODO 思考如果cluster-name 节点全部挂掉。是不是可以请求其他的分组的服务?可以根据情况在定制一份规则出来
if (StringUtils.isNotBlank(clusterName)) {
List sameClusterInstances = (List)instancesToChoose.stream().filter((instancex) -> {
return Objects.equals(clusterName, instancex.getClusterName());
}).collect(Collectors.toList());
if (!CollectionUtils.isEmpty(sameClusterInstances)) {
instancesToChoose = sameClusterInstances;
} else {
log.warn("A cross-cluster call occurs,name = {}, clusterName = {}, instance = {}", new Object[]{grayscale.getServerName(), clusterName, instances});
}
}
//按nacos权重获取。这个是NacosRule的代码copy 过来 没有自己实现权重随机。这个权重是nacos控制台服务的权重设置
// 如果业务上有自己特殊的业务。可以自己定制规则,黑白名单,用户是否是灰度用户,测试账号。等等一些自定义设置
Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToChoose);
return new NacosServer(instance);
}
} catch (Exception var9) {
log.warn("NacosRule error", var9);
return null;
}
}
}
以上就是gateway的定制负载规则。
启动三个cloud-discovery-client服务
对应版本1、2、3
然后postman进行接口请求 http://localhost:9000/client/client/user/service/save header 里面添加 version 字段。分别请求对应的版本服务。
gateway 路由全部请求到了对应版本的路由服务上。
其实和gateway 原理一样,只不过少了gateway 拦截器这一层。
创建自己的AbstractGrayscalLoadBalancerRule 继承AbstractLoadBalancerRule 抽象类,这个抽象类封装了一些我们需要用到的方法。
/**
* @Author: xlr
* @Date: Created in 1:03 PM 2019/11/24
*/
@Slf4j
@Data
public abstract class AbstractGrayscalLoadBalancerRule extends AbstractLoadBalancerRule {
/**
* asc 正序 反之desc 倒叙
*/
protected boolean asc = true;
/**
* 筛选想要的值
* @param instances
* @param version
* @return
*/
protected List buildVersion(List instances,String version){
//进行按版本分组排序
Map> versionMap = getInstanceByScreen(instances);
if(versionMap.isEmpty()){
log.warn("no instance in service {}", version);
}
//如果version 未传值使用最低版本服务
if(StringUtils.isBlank( version )){
if(isAsc()){
version = getFirst( versionMap.keySet() );
}else {
version = getLast( versionMap.keySet() );
}
}
List instanceList = versionMap.get( version );
return instanceList;
}
/**
* 根据version 组装一个map key value 对应 version List
* @param instances
* @return
*/
protected Map> getInstanceByScreen(List instances){
Map> versionMap = new HashMap<>( instances.size() );
instances.stream().forEach( instance -> {
String version = instance.getMetadata().get( GrayscaleConstant.GRAYSCALE_VERSION );
List versions = versionMap.get( version );
if(versions == null){
versions = new ArrayList<>( );
}
versions.add( instance );
versionMap.put( version,versions );
} );
return versionMap;
}
/**
* 获取第一个值
* @param keys
* @return
*/
protected String getFirst(Set keys){
List list = sortVersion( keys );
return list.get( 0 );
}
/**
* 获取最后一个值
* @param keys
* @return
*/
protected String getLast(Set keys){
List list = sortVersion( keys );
return list.get( list.size()-1 );
}
/**
* 根据版本排序
* @param keys
* @return
*/
protected List sortVersion(Set keys){
List list = new ArrayList <>( keys );
Collections.sort(list);
return list;
}
}
创建实现类GrayscaleLoadBalancerRule 继承自己定义的抽象类AbstractGrayscalLoadBalancerRule
/**
* fegin 负载均衡。在获取到我们想设置的对象之后,我们还可以设置 服务、用户、角色等各个维度的黑白名单,限制、转发、等策略,具体的使用场景还得需要结合工作中的实际使用场景。
* 这里只是提供一个简单的思路。希望看到这个注释的人。能够有举一反三的能力,定制自己的规则。
* @Author: xlr
* @Date: Created in 12:19 PM 2019/11/24
*/
@Slf4j
public class GrayscaleLoadBalancerRule extends AbstractGrayscalLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
//留空
}
/**
* gateway 特殊性。需要设置key值内容知道你要转发的服务名称。
* @param key
* @return
*/
@Override
public Server choose(Object key) {
log.info("GrayscaleLoadBalancerRule 执行 choose方法 ,参数 key: {}",key);
try {
String clusterName = this.nacosDiscoveryProperties.getClusterName();
DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer)this.getLoadBalancer();
String name = loadBalancer.getName();
NamingService namingService = this.nacosDiscoveryProperties.namingServiceInstance();
List instances = namingService.selectInstances(name, true);
if (CollectionUtils.isEmpty(instances)) {
log.warn("no instance in service {}", name);
return null;
} else {
List instancesToChoose = null;
String version = (String) ThreadLocalUtils.getKey( GrayscaleConstant.GRAYSCALE_VERSION );
List instanceList = buildVersion( instances,version );
if (StringUtils.isNotBlank(clusterName)) {
List sameClusterInstances = (List)instanceList.stream().filter((instancex) -> {
return Objects.equals(clusterName, instancex.getClusterName());
}).collect(Collectors.toList());
if (!CollectionUtils.isEmpty(sameClusterInstances)) {
instancesToChoose = sameClusterInstances;
} else {
log.warn("A cross-cluster call occurs,name = {}, clusterName = {}, instance = {}", new Object[]{name, clusterName, instanceList});
}
}
Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToChoose);
return new NacosServer(instance);
}
} catch (Exception var9) {
log.warn("NacosRule error", var9);
return null;
}
}
}
分别在client、server的启动类上,声明自定义的IRule
@Bean
IRule rule(){
return new GrayscaleLoadBalancerRule();
}
在启动三个server服务进行负载均衡。继续的测试效果。就不在贴图了。有兴趣的小伙伴们可以自己尝试写一下。
这里在多说一点,注意bean对象父子上下文。如果有没接触过这个的可以度娘一下这个知识点。
企业定制路由规则,在根据gateway提供的谓词、断言、过滤器这几个要素组合,
定制企业自己想要的路由规则。到此时这样gateway才是企业真正想要的路由功能。
Sentinel 官方文档地址
摘自参考 spring cloud 官方文档
Spring Cloud alibaba 官网地址
示例代码地址
往期地址 spring cloud alibaba 地址
spring cloud alibaba 简介
Spring Cloud Alibaba (nacos 注册中心搭建)
Spring Cloud Alibaba 使用nacos 注册中心
Spring Cloud Alibaba nacos 配置中心使用
spring cloud 网关服务
Spring Cloud zuul网关服务 一
Spring Cloud 网关服务 zuul 二
Spring Cloud 网关服务 zuul 三 动态路由
Spring Cloud alibaba网关 sentinel zuul 四 限流熔断
Spring Cloud gateway 网关服务 一
Spring Cloud gateway 网关服务二 断言、过滤器
Spring Cloud gateway 三 自定义过滤器GatewayFilter
Spring Cloud gateway 网关四 动态路由
Spring Cloud gateway 五 Sentinel整合
Spring Cloud gateway 六 Sentinel nacos存储动态刷新
Spring Cloud gateway 七 Sentinel 注解方式使用
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。转载请附带公众号二维码