SpringCloudGateway源码阅读(三)权重路由

3,123 阅读2分钟

前言

本章学习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;
		}
	};
}

总结

  • 权重配置的抽象是WeightConfigGroupWeightConfig
  • 权重路由的配置初始化由Spring事件触发,由WeightCalculatorWebFilter处理并组装成员变量private Map<String, GroupWeightConfig> groupWeights
  • 运行阶段WeightCalculatorWebFilter作为一个WebFilter通过随机数确定每个分组对应的路由,放入ServerWebExchange的attribute(WEIGHT_ATTR)中。WeightRoutePredicateFactory创建的GatewayPredicate,通过当前处理路由(GATEWAY_PREDICATE_ROUTE_ATTR)与分组对应的路由(WEIGHT_ATTR),确定当前路由是否匹配。