在学习istio的过程中,发现istio的权重控制和灰度控制实在太好用了,虽然istio现在用在生产中还是不太成熟,但是可以吸取相关的优点来整合到现有的springcloud中.
springcloud已经是个成熟的框架了,其有ribbon组件负责提供负载均衡,还有gateway组件进行路由转发,但是使用的时候还是有不方便的地方,比如ribbon没有基于权重的轮询算法,这个就很蛋疼了,因为我们部署的机子可能每个性能都不一样,即使用的云服务,但也不能每个云服务器都是一样的,所以我们需要有一种权重来控制流量的方向(不是限流哦!);还有就是gateway的路由的动态配置,我们是希望能够存储在redis或者mysql中,所有也是需要自己进行一定的开发才行.
想要实现的效果如下
那么我这次的目的就是让我们的springcloud有带权重的轮询功能和灰度控制的路由, 以及网关路由的redis持久化功能,首先开始开发轮询.
首先负载均衡功能在springcloud的ribbon组件提供,那么我们肯定要对其进行自定义的扩展,最主要的就是IRule接口了,其实我们简单的开发只要实现IRule接口就行了,但是我这里因为还要做灰度控制的功能,所以我实现的抽象类是PredicateBasedRule, 这个类可以很方便的结合Predicate来进行灰度判定. 主要的代码如下
// 一个抽象类,提供Predicate
public abstract class DiscoveryEnabledRule extends PredicateBasedRule {
private final DiscoveryEnabledPredicate predicate;
public DiscoveryEnabledRule(DiscoveryEnabledPredicate discoveryEnabledPredicate) {
this.predicate =discoveryEnabledPredicate;
}
@Override
public AbstractServerPredicate getPredicate() {
return predicate;
}
}
// 这里主要介绍和nacos的整合时的规则
public class NacosMetadataAwareRule extends DiscoveryEnabledRule {
public NacosMetadataAwareRule() {
super(new NacosMetadataAwarePredicate());
}
}
public abstract class DiscoveryEnabledPredicate extends AbstractServerPredicate {
@Override
public boolean apply(@Nullable PredicateKey input) {
return input != null
&& apply(input.getServer());
}
protected abstract boolean apply(Server server);
}
public class NacosMetadataAwarePredicate extends DiscoveryEnabledPredicate {
//带权重的负载均衡算法
WeightRoundRobin weightRoundRobin = new NacosWeightRoundRobin();
/**
* 这里主要是进行各种筛选, 暂时是通过metainfo中的version来进行灰度删选
*/
@Override
protected boolean apply(Server server) {
final RibbonFilterContext context = RibbonFilterContextHolder.getCurrentContext();
final Set> attributes = Collections.unmodifiableSet(context.getAttributes().entrySet());
final Map metadata = ((NacosServer)server).getMetadata();
System.out.println("-------- 需要的meta信息 --------");
attributes.forEach(e-> System.out.println(e.getKey()+":"+e.getValue()));
System.out.println("-------- 存在的meta信息 --------"+server.getPort());
metadata.forEach((k,v)-> System.out.println(k + ":" + v));
return metadata.entrySet().containsAll(attributes);
}
@Override
public Optional chooseRoundRobinAfterFiltering(List servers, Object loadBalancerKey) {
List eligible = getEligibleServers(servers, loadBalancerKey);
if (eligible.size() == 0) {
return Optional.absent();
}
//根据权重轮询获取一个服务器
return Optional.of(weightRoundRobin.choose(eligible));
}
}
以上两个类就是核心的ribbon的自定义规则实现,然后就是写个自动装配的类就可以了
@Configuration
@AutoConfigureBefore(RibbonClientConfiguration.class)
@ConditionalOnProperty(value = "ribbon.filter.metadata.enabled", matchIfMissing = true)
public class RibbonDiscoveryRuleAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnClass(DiscoveryEnabledNIWSServerList.class)
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public DiscoveryEnabledRule metadataAwareRuleEureka() {
DiscoveryEnabledRule metadataAwareRule = new EurekaMetadataAwareRule();
return metadataAwareRule;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnClass(NacosServer.class)
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public DiscoveryEnabledRule metadataAwareRuleNacos() {
DiscoveryEnabledRule metadataAwareRule = new NacosMetadataAwareRule();
return metadataAwareRule;
}
}
这样的话,一个带权重的负载均衡的ribbon的starter就可以说是完成了
网关这边主要是对路由做一个持久化,然后在整一个过滤器用于从请求中获取version传递给ribbon
@Component
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {
public static final String GATEWAY_ROUTES = "geteway_routes";
@Resource
private StringRedisTemplate redisTemplate;
@Override
public Flux getRouteDefinitions() {
List routeDefinitions = new ArrayList<>();
redisTemplate.opsForHash().values(GATEWAY_ROUTES)
.forEach(routeDefinition -> routeDefinitions.add(JSON.parseObject(routeDefinition.toString(), RouteDefinition.class)));
return Flux.fromIterable(routeDefinitions);
}
@Override
public Mono save(Mono route) {
return route.flatMap(r -> {
redisTemplate.opsForHash().put(GATEWAY_ROUTES, r.getId(), JSON.toJSONString(r));
return Mono.empty();
});
}
@Override
public Mono delete(Mono routeId) {
return routeId.flatMap(id -> {
if (redisTemplate.opsForHash().get(GATEWAY_ROUTES, id) != null) {
redisTemplate.opsForHash().delete(GATEWAY_ROUTES, id);
return Mono.empty();
}
return Mono.defer(() -> Mono.error(new NotFoundException("RouteDefinition not found: " + routeId)));
});
}
}
没啥好讲的就是增删改查,只不过是通过Reactor的响应式模型进行处理.
@Component
public class WeightRoundRibonFilter implements GlobalFilter, Ordered {
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println(Thread.currentThread().getName());
String version = exchange.getRequest().getHeaders().getFirst("version");
if (StringUtils.isNotBlank(version)) {
RibbonFilterContext currentContext = RibbonFilterContextHolder.getCurrentContext();
currentContext.add("version", version);
}
Mono mono = chain.filter(exchange).subscriberContext(ctx -> ctx.put("version", version));
return mono;
}
@Override
public int getOrder() {
return 10000; //这里要注意loadBalanceClientFilter的order是10100,这里要比他小一点
}
}
server:
port: 13000
spring:
application:
name: gateway
cloud:
gateway:
discovery:
locator:
enabled: false #表明gateway开启服务注册和发现的功能,并且spring cloud gateway自动根据服务发现为每一个服务创建了一个router,这个router将以服务名开头的请求路径转发到对应的服务。
lowerCaseServiceId: true #是将请求路径上的服务名配置为小写(因为服务注册的时候,向注册中心注册时将服务名转成大写的了),比如以/service-hi/*的请求路径被路由转发到服务名为service-hi的服务上。
filters:
- StripPrefix=1
routes:
- id: client2
uri: lb://client2
predicates:
- Path=/client2/**
filters:
- StripPrefix=1
nacos:
config:
server-addr: 127.0.0.1:8848
discovery:
server-addr: 127.0.0.1:8848
首先是关闭本身的自动路由配置,然后手动配置serviceId为client2的路由
首先看一下实例注册情况,直接在nacos控制台上看
这里直接在浏览器上访问地址
可以看到是1:2的比例
使用postman或者别的,在请求头上加上version: client2-v2, 那么在请求时就会固定路由到metainfo中存在版本client2-v2的实例, 通过这个功能,就是可以让v1的请求走v1实例,就能实现灰度的功能
首先在redis添加路由
直接从actuator的http://localhost:13000/actuator/gateway/routes 路径就能看到已经从redis中读取出来并配置了
// 20190630140142
// http://localhost:13000/actuator/gateway/routes
[
{
"route_id": "id",
"route_definition": {
"id": "id",
"predicates": [
{
"name": "Path",
"args": {
"pattern": "/jd"
}
}
],
"filters": [
{
"name": "AddRequestHeader",
"args": {
"_genkey_0": "header",
"_genkey_1": "addHeader"
}
},
{
"name": "AddRequestParameter",
"args": {
"_genkey_0": "param",
"_genkey_1": "addParam"
}
}
],
"uri": "http://127.0.0.1:8888/header",
"order": 0
},
"order": 0
},
{
"route_id": "client2",
"route_definition": {
"id": "client2",
"predicates": [
{
"name": "Path",
"args": {
"_genkey_0": "/client2/**"
}
}
],
"filters": [
{
"name": "StripPrefix",
"args": {
"_genkey_0": "1"
}
}
],
"uri": "lb://client2",
"order": 0
},
"order": 0
}
]
{appID}
/{instanceID}
/metadata?key=value)
文章的代码已上传github (https://github.com/cdy1996/sample-springcloud/tree/nacos-weight-route) 代码部分参考自其他的出处.