【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>
上一篇介绍了,ribbon的组件。本篇要自己写一个灰度方案。其实就是一个很简单的思维扩散。
需求
前端header请求携带version字段。路由服务根据version去需要对应版本的服务集合,进行或轮询或hash或权重的负载。请求路由到服务上,如果还要调用下游服务,也按照version规则去路由下游服务器。前端未携带版本按照后端服务最高version版本进行路由。
分析如果自己动手写一个灰度方案。需要考虑的因素有几点?
- 服务对应的版本。key(版本号):value(对应版本号的服务集合)
- 对应版本号的服务集合需要重新排序。
- 重写负载均衡规则,就是ribbon的IRule方法。按照我们想要的负载规则去路由我们的请求
解决方案:
- 利用注册中心的metadata属性元数据,让服务携带版本信息。
- 拿到要请求的服务集合。spring cloud Alibaba nacos NamingService接口根据服务名称获取所有服务List集合,如果你使用的spring cloud 版本可以使用 ILoadBalancer 对象获取所有的服务集合
- Instance服务里面携带了,服务注册到注册中心的自定义版本信息
- 重写IRule负载规则。按照需求转发请求。
来写一下网关层的实现。 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<Void> 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:<scheme>` mechanism was used, use `<scheme>` 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<Instance> instances = namingService.selectInstances(grayscale.getServerName(), true);
if (CollectionUtils.isEmpty(instances)) {
log.warn("no instance in service {}", grayscale.getServerName());
return null;
} else {
List<Instance> instancesToChoose = buildVersion(instances,version);
//进行cluster-name分组筛选
// TODO 思考如果cluster-name 节点全部挂掉。是不是可以请求其他的分组的服务?可以根据情况在定制一份规则出来
if (StringUtils.isNotBlank(clusterName)) {
List<Instance> 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 <Instance> buildVersion(List<Instance> instances,String version){
//进行按版本分组排序
Map<String,List<Instance>> 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 <Instance> instanceList = versionMap.get( version );
return instanceList;
}
/**
* 根据version 组装一个map key value 对应 version List<Instance>
* @param instances
* @return
*/
protected Map<String,List<Instance>> getInstanceByScreen(List<Instance> instances){
Map<String,List<Instance>> versionMap = new HashMap<>( instances.size() );
instances.stream().forEach( instance -> {
String version = instance.getMetadata().get( GrayscaleConstant.GRAYSCALE_VERSION );
List <Instance> 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<String> keys){
List <String> list = sortVersion( keys );
return list.get( 0 );
}
/**
* 获取最后一个值
* @param keys
* @return
*/
protected String getLast(Set <String> keys){
List <String> list = sortVersion( keys );
return list.get( list.size()-1 );
}
/**
* 根据版本排序
* @param keys
* @return
*/
protected List<String > sortVersion(Set <String> keys){
List<String > 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<Instance> instances = namingService.selectInstances(name, true);
if (CollectionUtils.isEmpty(instances)) {
log.warn("no instance in service {}", name);
return null;
} else {
List<Instance> instancesToChoose = null;
String version = (String) ThreadLocalUtils.getKey( GrayscaleConstant.GRAYSCALE_VERSION );
List <Instance> instanceList = buildVersion( instances,version );
if (StringUtils.isNotBlank(clusterName)) {
List<Instance> 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才是企业真正想要的路由功能。
往期资料、参考资料
摘自参考 spring cloud 官方文档
往期地址 spring cloud alibaba 地址
Spring Cloud Alibaba (nacos 注册中心搭建)
Spring Cloud Alibaba 使用nacos 注册中心
Spring Cloud Alibaba nacos 配置中心使用
Spring Cloud alibaba网关 sentinel zuul 四 限流熔断
Spring Cloud gateway 网关服务二 断言、过滤器
Spring Cloud gateway 三 自定义过滤器GatewayFilter
Spring Cloud gateway 五 Sentinel整合
Spring Cloud gateway 六 Sentinel nacos存储动态刷新
Spring Cloud gateway 七 Sentinel 注解方式使用
如何喜欢可以关注分享本公众号。
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。转载请附带公众号二维码
来源:oschina
链接:https://my.oschina.net/devxlr/blog/3141476