前言
本章学习SpringCloudGateway的权重路由。权重路由算是Gateway里一个比较实用的功能,无论是ABTest还是灰度发布,都可以利用Gateway的权重路由功能。
一、案例
方式一:编码构建RouteLocator
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(r ->
r.path("/app/v1").and().weight("appV1", 3).uri("http://127.0.0.1:15001/app")
)
.route(r ->
r.path("/app/v1").and().weight("appV1", 5).uri("http://127.0.0.1:15101/app")
)
.route(r ->
r.path("/app/v1").and().weight("appV1", 2).uri("http://127.0.0.1:15201/app")
)
.build();
}
方式二:配置文件配置RouteDefinition
spring:
cloud:
gateway:
routes:
- id: serviceA
uri: lb://httpbin
predicates:
- Path=/get
- Weight=ABCTest, 20
- id: serviceB
uri: lb://httpbin2
predicates:
- Path=/get
- Weight=ABCTest, 30
- id: serviceC
uri: lb://httpbin3
predicates:
- Path=/get
- Weight=ABCTest, 50
二、理解WeightConfig与GroupWeightConfig
WeightConfig
WeightConfig针对的是单个路由的权重配置。
@Validated
public class WeightConfig {
// 分组
@NotEmpty
private String group;
// 路由ID
private String routeId;
// 权重
@Min(0)
private int weight;
}
GroupWeightConfig
GroupWeightConfig针对的是某个分组的权重配置,WeightConfig:GroupWeightConfig可以认为是n:1的关系,即一个GroupWeightConfig对应多个WeightConfig。先来看一下GroupWeightConfig的成员变量。
static class GroupWeightConfig {
// 分组
String group;
// 路由ID - 权重
LinkedHashMap<String, Integer> weights = new LinkedHashMap<>();
// 路由ID - 标准化权重(一个0-1之间的Double)
LinkedHashMap<String, Double> normalizedWeights = new LinkedHashMap<>();
// 区间起始下标 - 路由ID
LinkedHashMap<Integer, String> rangeIndexes = new LinkedHashMap<>();
// 区间抽象
List<Double> ranges = new ArrayList<>();
}
GroupWeightConfig是如何计算的,来个案例。假设现在有一个分组叫demoGroup,组内有三个WeightConfig,它们路由ID-权重的关系是:RouteA-2、RouteB-3、RouteC-5。
GroupWeightConfig.group = demoGroup
GroupWeightConfig.weights = {"RouteA": 2, "RouteB": 3, "RouteC": 5}
GroupWeightConfig.normalizedWeights = {"RouteA": 0.2, "RouteB": 0.3, "RouteC": 0.5}
GroupWeightConfig.ranges = [0, 0.2, 0.3, 0.5]
GroupWeightConfig.rangeIndexes = {0: "RouteA", 1: "RouteB", 2: "RouteC"}
当请求进来后,生成一个0-1的随机小数,判断小数在ranges的哪个区间内,确定所在区间的开始位置在ranges的下标,rangeIndexes通过下标映射到实际的路由ID。
比如随机小数为0.25,位于[0.2, 0.3)区间内,起始下标为1(0.2元素的下标),通过rangeIndexes得到下标1对应的RouteID是RouteB。
三、生成GroupWeightConfig
GroupWeightConfig的生成一般是在Spring容器启动阶段,收到Spring事件后,放入WeightCalculatorWebFilter#groupWeights
。
WeightCalculatorWebFilter接收事件
WeightCalculatorWebFilter#onApplicationEvent
收到Spring事件。
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof PredicateArgsEvent) {
// 对于Definition创建路由的情况,需要转换为WeightConfig,再执行addWeightConfig
handle((PredicateArgsEvent) event);
}
else if (event instanceof WeightDefinedEvent) {
// 对于编码直接创建路由的情况,执行addWeightConfig
addWeightConfig(((WeightDefinedEvent) event).getWeightConfig());
}
}
public void handle(PredicateArgsEvent event) {
Map<String, Object> args = event.getArgs();
// 如果args不包含weight开头的key,忽略
if (args.isEmpty() || !hasRelevantKey(args)) {
return;
}
// 组装WeightConfig,具体逻辑省略
WeightConfig config = new WeightConfig(event.getRouteId());
this.configurationService.with(config).name(WeightConfig.CONFIG_PREFIX)
.normalizedProperties(args).bind();
// 组装GroupWeightConfig
addWeightConfig(config);
}
WeightCalculatorWebFilter生成GroupWeightConfig
生成GroupWeightConfig
的核心方法是WeightCalculatorWebFilter#addWeightConfig
。
public class WeightCalculatorWebFilter
implements WebFilter, Ordered, SmartApplicationListener {
// 分组 - GroupWeightConfig
private Map<String, GroupWeightConfig> groupWeights = new ConcurrentHashMap<>();
// 每次事件发生,都会传入某个Route对于权重的配置
void addWeightConfig(WeightConfig weightConfig) {
// 分组
String group = weightConfig.getGroup();
GroupWeightConfig config;
if (groupWeights.containsKey(group)) {
// 分组对应的配置已经存在,拷贝一个新的分组配置
config = new GroupWeightConfig(groupWeights.get(group));
}
else {
// 分组对应的配置不存在,直接new一个,只有group属性
config = new GroupWeightConfig(group);
}
// 把RouteID-权重的原始信息保存到weights里
config.weights.put(weightConfig.getRouteId(), weightConfig.getWeight());
// 计算当前分组的权重之和
int weightsSum = 0;
for (Integer weight : config.weights.values()) {
weightsSum += weight;
}
final AtomicInteger index = new AtomicInteger(0);
// 循环RouteID-权重
for (Map.Entry<String, Integer> entry : config.weights.entrySet()) {
String routeId = entry.getKey();
Integer weight = entry.getValue();
// 标准化权重 = 当前权重 / 总权重
Double nomalizedWeight = weight / (double) weightsSum;
// 记录 RouteID - 标准化权重
config.normalizedWeights.put(routeId, nomalizedWeight);
// 记录 下标 - RouteID
config.rangeIndexes.put(index.getAndIncrement(), routeId);
}
// 清空之前的区间
config.ranges.clear();
// 区间第一个值一定是0.0
config.ranges.add(0.0);
// 构造区间ranges
List<Double> values = new ArrayList<>(config.normalizedWeights.values());
for (int i = 0; i < values.size(); i++) {
Double currentWeight = values.get(i);
Double previousRange = config.ranges.get(i);
Double range = previousRange + currentWeight;
config.ranges.add(range);
}
groupWeights.put(group, config);
}
}
四、运行阶段
WeightCalculatorWebFilter
WeightCalculatorWebFilter#filter
遍历所有分组,通过随机数确定每个分组实际匹配的routeId,将group-routeId的映射关系,保存到ServerWebExchange的attributes里。
private Random random = new Random();
// 分组 - GroupWeightConfig
private Map<String, GroupWeightConfig> groupWeights = new ConcurrentHashMap<>();
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
// ServerWebExchange的WEIGHT_ATTR对应的引用
// key是分组名,value是本次请求的RouteID
Map<String, String> weights = getWeights(exchange);
// 循环所有分组
for (String group : groupWeights.keySet()) {
GroupWeightConfig config = groupWeights.get(group);
if (config == null) {
continue;
}
// 获取一个0-1的随机数
double r = this.random.nextDouble();
// 获得当前分组的区间划分
List<Double> ranges = config.ranges;
// 循环所有区间,看当前随机数落入哪个区间
for (int i = 0; i < ranges.size() - 1; i++) {
// 确定落入区间
if (r >= ranges.get(i) && r < ranges.get(i + 1)) {
// 通过区间下标获取对应的RouteID
String routeId = config.rangeIndexes.get(i);
// 放入WEIGHT_ATTR
weights.put(group, routeId);
break;
}
}
}
return chain.filter(exchange);
}
GatewayPredicate
WeightRoutePredicateFactory
创建的GatewayPredicate
根据ServerWebExchange确定路由是否匹配。
@Override
public Predicate<ServerWebExchange> apply(WeightConfig config) {
return new GatewayPredicate() {
// 校验ServerWebExchange是否匹配当前路由
@Override
public boolean test(ServerWebExchange exchange) {
// 从WEIGHT_ATTR获取分组和路由ID的映射关系(from WeightCalculatorWebFilter)
Map<String, String> weights = exchange.getAttributeOrDefault(WEIGHT_ATTR,
Collections.emptyMap());
// 从GATEWAY_PREDICATE_ROUTE_ATTR获取当前判断的路由ID
// 这个属性是在RoutePredicateHandlerMapping#lookupRoute进入断言匹配之前放入的
String routeId = exchange.getAttribute(GATEWAY_PREDICATE_ROUTE_ATTR);
// 通过当前分组配置,获取分组名称
String group = config.getGroup();
if (weights.containsKey(group)) {
// 通过分组名称获取随机选择的路由ID
String chosenRoute = weights.get(group);
// 判断当前路由id与随机选择的路由ID是否一致
return routeId.equals(chosenRoute);
}
return false;
}
};
}
总结
- 权重配置的抽象是
WeightConfig
和GroupWeightConfig
。 - 权重路由的配置初始化由Spring事件触发,由
WeightCalculatorWebFilter
处理并组装成员变量private Map<String, GroupWeightConfig> groupWeights
。 - 运行阶段
WeightCalculatorWebFilter
作为一个WebFilter
通过随机数确定每个分组对应的路由,放入ServerWebExchange
的attribute(WEIGHT_ATTR)中。WeightRoutePredicateFactory
创建的GatewayPredicate
,通过当前处理路由(GATEWAY_PREDICATE_ROUTE_ATTR)与分组对应的路由(WEIGHT_ATTR),确定当前路由是否匹配。