大家好,我是小富~

最近同事找我帮忙排查一个"诡异"的 Bug,说困扰了他们一年多一直没解决。我接手后花了一些时间定位到了问题根源,今天就来跟大家分享一下这个问题的排查过程和解决方案。

问题描述

同事使用的是 SpringCloud Gateway 3.0.1 + JDK8,整合了 Nacos 做动态路由配置。问题是:每次修改 Nacos 的路由配置后,网关的 API 请求就会出现 404 错误,但重启网关后又能恢复正常。

听到这个问题,我的第一反应是:Nacos 配置更新后,网关的缓存数据可能没有及时更新。带着这个猜想,我开始深入排查。

环境准备

首先准备了 3 个后端服务实例,端口分别为 81031204012041,在 Nacos 中配置了对应的网关路由:xiaofu-8103xiaofu-12040xiaofu-12041,并将它们放在同一个权重组 xiaofu-group 中,实现基于权重的负载均衡。

- id: xiaofu-8103
uri: http://127.0.0.1:8103/
predicates:
- Weight=xiaofu-group, 2
- Path=/test/version1/**
filters:
- RewritePath=/test/version1/(?<segment>.*),/$\{segment}
- id: xiaofu-12040
uri: http://127.0.0.1:12040/
predicates:
- Weight=xiaofu-group, 1
- Path=/test/version1/**
filters:
- RewritePath=/test/version1/(?<segment>.*),/$\{segment}
- id: xiaofu-12041
uri: http://127.0.0.1:12041/
predicates:
- Weight=xiaofu-group, 2
- Path=/test/version1/**
filters:
- RewritePath=/test/version1/(?<segment>.*),/$\{segment}

使用 JMeter 进行持续请求测试,为了便于日志追踪,给每个请求参数都添加了随机数。

准备完成后启动 JMeter 循环请求,观察到三个实例都有日志输出,说明网关的负载均衡功能正常。

问题排查

为了获取更详细的日志信息,我将网关的日志级别调整为 TRACE

启动 JMeter 后,随机修改三个实例的路由属性(uri、port、predicates、filters),请求没有出现报错,网关控制台也显示了更新后的路由属性,说明 Nacos 配置变更已成功同步到网关。

接下来尝试去掉一个实例 xiaofu-12041,这时发现 JMeter 请求开始出现 404 错误,成功复现问题!

查看网关控制台日志时,惊奇地发现已删除的实例 xiaofu-12041 的路由配置仍然存在,甚至还被选中(chosen)处理请求。问题根源找到了:虽然 Nacos 中删除了实例路由配置,但网关在实际负载均衡时仍然使用旧的路由数据。

继续深入排查,发现在路由的权重信息(Weights attr)中也存在旧的路由数据。至此基本确定问题:在计算实例权重和负载均衡时,网关使用了陈旧的缓存数据。

源码分析

通过分析源码,发现了一个专门计算权重的过滤器 WeightCalculatorWebFilter。它内部维护了一个 groupWeights 变量来存储路由权重信息。当配置变更事件发生时,会执行 addWeightConfig(WeightConfig weightConfig) 方法来更新权重配置。

@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof PredicateArgsEvent) {
handle((PredicateArgsEvent) event);
}
else if (event instanceof WeightDefinedEvent) {
addWeightConfig(((WeightDefinedEvent) event).getWeightConfig());
}
else if (event instanceof RefreshRoutesEvent && routeLocator != null) {
if (routeLocatorInitialized.compareAndSet(false, true)) {
routeLocator.ifAvailable(locator -> locator.getRoutes().blockLast());
}
else {
routeLocator.ifAvailable(locator -> locator.getRoutes().subscribe());
}
} }

addWeightConfig 方法的注释明确说明:该方法仅创建新的 GroupWeightConfig,而不进行修改。这意味着它只能新建或覆盖路由权重,无法清理已删除的路由权重信息。

void addWeightConfig(WeightConfig weightConfig) {
String group = weightConfig.getGroup();
GroupWeightConfig config;
// only create new GroupWeightConfig rather than modify
// and put at end of calculations. This avoids concurency problems
// later during filter execution.
if (groupWeights.containsKey(group)) {
config = new GroupWeightConfig(groupWeights.get(group));
}
else {
config = new GroupWeightConfig(group);
} final AtomicInteger index = new AtomicInteger(0);
....省略..... if (log.isTraceEnabled()) {
log.trace("Recalculated group weight config " + config);
}
// only update after all calculations
groupWeights.put(group, config);
}

解决方案

找到问题根源后,解决方案就清晰了

开始我怀疑可能是springcloud gateway 版本问题,将版本升级到了4.1.0,但结果还是存在这个问题。

看来只能手动更新缓存,需要监听 Nacos 路由配置变更事件,获取最新路由配置,并更新 groupWeights 中的权重数据。

以下是实现的解决方案代码:

@Slf4j
@Configuration
public class WeightCacheRefresher { @Autowired
private WeightCalculatorWebFilter weightCalculatorWebFilter; @Autowired
private RouteDefinitionLocator routeDefinitionLocator; @Autowired
private ApplicationEventPublisher publisher; /**
* 监听路由刷新事件,同步更新权重缓存
*/
@EventListener(RefreshRoutesEvent.class)
public void onRefreshRoutes() {
log.info("检测到路由刷新事件,准备同步更新权重缓存");
syncWeightCache();
} /**
* 同步权重缓存与当前路由配置
*/
public void syncWeightCache() {
try {
// 获取 groupWeights 字段
Field groupWeightsField = WeightCalculatorWebFilter.class.getDeclaredField("groupWeights");
groupWeightsField.setAccessible(true); // 获取当前的 groupWeights 值
@SuppressWarnings("unchecked")
Map<String, Object> groupWeights = (Map<String, Object>) groupWeightsField.get(weightCalculatorWebFilter); if (groupWeights == null) {
log.warn("未找到 groupWeights 缓存");
return;
} log.info("当前 groupWeights 缓存: {}", groupWeights.keySet()); // 获取当前所有路由的权重组和路由ID
final Set<String> currentRouteIds = new HashSet<>();
final Map<String, Map<String, Integer>> currentGroupRouteWeights = new HashMap<>(); routeDefinitionLocator.getRouteDefinitions()
.collectList()
.subscribe(definitions -> {
definitions.forEach(def -> {
currentRouteIds.add(def.getId()); def.getPredicates().stream()
.filter(predicate -> predicate.getName().equals("Weight"))
.forEach(predicate -> {
Map<String, String> args = predicate.getArgs();
String group = args.getOrDefault("_genkey_0", "unknown");
int weight = Integer.parseInt(args.getOrDefault("_genkey_1", "0")); // 记录每个组中当前存在的路由及其权重
currentGroupRouteWeights.computeIfAbsent(group, k -> new HashMap<>())
.put(def.getId(), weight);
});
}); log.info("当前路由配置中的路由ID: {}", currentRouteIds);
log.info("当前路由配置中的权重组: {}", currentGroupRouteWeights); // 检查每个权重组,移除不存在的路由,更新权重变化的路由
Set<String> groupsToRemove = new HashSet<>();
Set<String> groupsToUpdate = new HashSet<>(); for (String group : groupWeights.keySet()) {
if (!currentGroupRouteWeights.containsKey(group)) {
// 整个权重组不再存在
groupsToRemove.add(group);
log.info("权重组 [{}] 不再存在于路由配置中,将被移除", group);
continue;
} // 获取该组中当前配置的路由ID和权重
Map<String, Integer> configuredRouteWeights = currentGroupRouteWeights.get(group); // 获取该组中缓存的权重配置
Object groupWeightConfig = groupWeights.get(group); try {
// 获取 weights 字段
Field weightsField = groupWeightConfig.getClass().getDeclaredField("weights");
weightsField.setAccessible(true); @SuppressWarnings("unchecked")
LinkedHashMap<String, Integer> weights = (LinkedHashMap<String, Integer>) weightsField.get(groupWeightConfig); // 找出需要移除的路由ID
Set<String> routesToRemove = weights.keySet().stream()
.filter(routeId -> !configuredRouteWeights.containsKey(routeId))
.collect(Collectors.toSet()); // 找出权重发生变化的路由ID
Set<String> routesWithWeightChange = new HashSet<>();
for (Map.Entry<String, Integer> entry : weights.entrySet()) {
String routeId = entry.getKey();
Integer cachedWeight = entry.getValue(); if (configuredRouteWeights.containsKey(routeId)) {
Integer configuredWeight = configuredRouteWeights.get(routeId);
if (!cachedWeight.equals(configuredWeight)) {
routesWithWeightChange.add(routeId);
log.info("路由 [{}] 的权重从 {} 变为 {}", routeId, cachedWeight, configuredWeight);
}
}
} // 找出新增的路由ID
Set<String> newRoutes = configuredRouteWeights.keySet().stream()
.filter(routeId -> !weights.containsKey(routeId))
.collect(Collectors.toSet()); if (!routesToRemove.isEmpty() || !routesWithWeightChange.isEmpty() || !newRoutes.isEmpty()) {
log.info("权重组 [{}] 中有变化:删除 {},权重变化 {},新增 {}",
group, routesToRemove, routesWithWeightChange, newRoutes); // 如果有任何变化,我们将重新计算整个组的权重
groupsToUpdate.add(group);
} // 首先,移除需要删除的路由
for (String routeId : routesToRemove) {
weights.remove(routeId);
} // 如果权重组中没有剩余路由,则移除整个组
if (weights.isEmpty()) {
groupsToRemove.add(group);
log.info("权重组 [{}] 中没有剩余路由,将移除整个组", group);
}
} catch (Exception e) {
log.error("处理权重组 [{}] 时出错", group, e);
}
} // 移除不再需要的权重组
for (String group : groupsToRemove) {
groupWeights.remove(group);
log.info("已移除权重组: {}", group);
} // 更新需要重新计算的权重组
for (String group : groupsToUpdate) {
try {
// 获取该组中当前配置的路由ID和权重
Map<String, Integer> configuredRouteWeights = currentGroupRouteWeights.get(group); // 移除旧的权重组配置
groupWeights.remove(group);
log.info("已移除权重组 [{}] 以便重新计算", group); // 为每个路由创建 WeightConfig 并调用 addWeightConfig 方法
Method addWeightConfigMethod = WeightCalculatorWebFilter.class.getDeclaredMethod("addWeightConfig", WeightConfig.class);
addWeightConfigMethod.setAccessible(true); for (Map.Entry<String, Integer> entry : configuredRouteWeights.entrySet()) {
String routeId = entry.getKey();
Integer weight = entry.getValue(); WeightConfig weightConfig = new WeightConfig(routeId);
weightConfig.setGroup(group);
weightConfig.setWeight(weight); addWeightConfigMethod.invoke(weightCalculatorWebFilter, weightConfig);
log.info("为路由 [{}] 添加权重配置:组 [{}],权重 {}", routeId, group, weight);
}
} catch (Exception e) {
log.error("重新计算权重组 [{}] 时出错", group, e);
}
} log.info("权重缓存同步完成,当前缓存的权重组: {}", groupWeights.keySet());
}); } catch (Exception e) {
log.error("同步权重缓存失败", e);
}
}
}

网上找一圈并没发现官方的修改意见,可能是咱们使用方式不对导致的,要不如此明显的BUG早就有人改了吧!

Gateway 网关坑我! 被这个404 问题折腾了一年?的更多相关文章

  1. 引入 Gateway 网关,这些坑一定要学会避开!!!

    Spring cloud gateway是替代zuul的网关产品,基于Spring 5.Spring boot 2.0以上.Reactor, 提供任意的路由匹配和断言.过滤功能.上一篇文章谈了一下Ga ...

  2. Spring Cloud gateway 网关服务二 断言、过滤器

    微服务当前这么火爆的程度,如果不能学会一种微服务框架技术.怎么能升职加薪,增加简历的筹码?spring cloud 和 Dubbo 需要单独学习.说没有时间?没有精力?要学俩个框架?而Spring C ...

  3. Spring Cloud Gateway入坑记

    Spring Cloud Gateway入坑记 前提 最近在做老系统的重构,重构完成后新系统中需要引入一个网关服务,作为新系统和老系统接口的适配和代理.之前,很多网关应用使用的是Spring-Clou ...

  4. SpringCloud Alibaba(三) - GateWay网关

    1.基本环境搭建 1.1 依赖 <!-- Gatway 网关会和springMvc冲突,不能添加web依赖 --> <dependency> <groupId>or ...

  5. .net core下,Ocelot网关与Spring Cloud Gateway网关的对比测试

    有感于 myzony 发布的 针对 Ocelot 网关的性能测试 ,并且公司下一步也需要对.net和java的应用做一定的整合,于是对Ocelot网关.Spring Cloud Gateway网关做个 ...

  6. Spring Cloud gateway 网关四 动态路由

    微服务当前这么火爆的程度,如果不能学会一种微服务框架技术.怎么能升职加薪,增加简历的筹码?spring cloud 和 Dubbo 需要单独学习.说没有时间?没有精力?要学俩个框架?而Spring C ...

  7. 0.9.0.RELEASE版本的spring cloud alibaba sentinel+gateway网关实例

    sentinel除了让服务提供方.消费方用之外,网关也能用它来限流.我们基于上次整的网关(参见0.9.0.RELEASE版本的spring cloud alibaba nacos+gateway网关实 ...

  8. SpringCloud + Consul服务注册中心 + gateway网关

    1  启动Consul 2  创建springcloud-consul项目及三个子模块 2.1 数据模块consul-producer 2.2 数据消费模块consul-consumer 2.3 ga ...

  9. 实战四:Gateway网关作全局路由转发

    Gateway网关的作用主要是两个:路由转发,请求过滤.此篇讲的是路由转发,下篇介绍请求过滤. 一,创建网关module,添加依赖 1,new -> module -> maven 或直接 ...

  10. Spring Cloud实战 | 第十一篇:Spring Cloud Gateway 网关实现对RESTful接口权限控制和按钮权限控制

    一. 前言 hi,大家好,这应该是农历年前的关于开源项目 的最后一篇文章了. 有来商城 是基于 Spring Cloud OAuth2 + Spring Cloud Gateway + JWT实现的统 ...

随机推荐

  1. 一文读懂Cookie、Session和Token:原理、区别与应用场景

    咱上网冲浪的时候,天天和各种网站.APP打交道.但HTTP协议有个"毛病"--它记性不好,每次请求都像第一次见面,根本不记得之前和你聊过啥.为了解决这个问题,程序员们整出了Cook ...

  2. ChatGPT学习之旅 (6) 聊聊AI人设

    大家好,我是Edison. 上一篇:Prompt终极用法 通过前面5篇内容我们了解了从基础到高级的Prompt用法,今天我们来聊聊AI的人设! 打造人设的步骤:只要3步 在Prompt实践中,我们了解 ...

  3. Elastic学习之旅 (3) ES必备基本概念

    大家好,我是Edison. 上一篇:快速安装ELK ES都有哪些基本概念 在学习ES时,需要掌握一些必备概念,有了这些基本概念,后续的学习才会轻松.我们可以从下图中了解,ES都有哪些基本概念. 从上图 ...

  4. poi处理excel的合并单元格写的工具类,支持xlsx和xls

    1.判断当前单元格是不是合并单元格 1 ... 2 private boolean isMergedRegion(Sheet sheet, int row, int column) { 3 //获取合 ...

  5. Codeforces Round #563 (Div. 2) ABCD 题解

    A. Ehab Fails to Be Thanos 题意:问你能否对a数组任意排序,使得前n段和不等于后n段和. 思路:水题,直接从小到大排序.这个情况都相等就一定无解. view code #in ...

  6. openwrtPackage *** is missing dependencies for the following libraries: 解决方案

    参考文献 开源中国博客 你得在 staging_dir/target-arm_cortex-a7+neon_glibc-2.22_eabi/ 目录中确实找到你的哪一个库 如果找到了还报错了, 尝试 在 ...

  7. 知网下载 pdf 而不是caj

    简介 主要通过F12实现 第一步 按下F12 CAJ全文下载 第二步 将 http://search.cnki.net/down/default.aspx?filename=2004072993.nh ...

  8. 面试官:谈谈你AI项目的具体实现?

    在如今比较卷的 Java 面试市场,会 AI 技术是比较吃香的,它也可以作为一个技术或项目亮点,帮你拿到更多的面试机会,当然,也会帮你大大的提升面试的通过率. 但是在面试的过程中,如何展现你的 AI ...

  9. CF1365G Secure Password

    \(\mathbf{Part. -1}\) 这是一个交互题. Ayush 又想出了一个设置密码的新方法.他的锁有 \(n\) 个槽位,每个槽位可以放置任意非负整数.密码 \(P\) 是一个长度为 \( ...

  10. .NET 10 中的新增功能系列文章1——运行时中的新增功能

    引言 随着 .NET 10 预览版6的发布,微软在运行时层面带来了一系列重要的性能改进和新功能.这些改进主要集中在JIT编译器优化.硬件指令集支持.内存管理等方面,旨在进一步提升应用程序的执行效率和资 ...