找回密码
 立即注册
首页 业界区 业界 Gateway 网关坑我! 被这个404 问题折腾了一年? ...

Gateway 网关坑我! 被这个404 问题折腾了一年?

缑娅瑛 2025-10-1 17:03:41
大家好,我是小富~
最近同事找我帮忙排查一个"诡异"的 Bug,说困扰了他们一年多一直没解决。我接手后花了一些时间定位到了问题根源,今天就来跟大家分享一下这个问题的排查过程和解决方案。
问题描述

同事使用的是 SpringCloud Gateway 3.0.1 + JDK8,整合了 Nacos 做动态路由配置。问题是:每次修改 Nacos 的路由配置后,网关的 API 请求就会出现 404 错误,但重启网关后又能恢复正常。
听到这个问题,我的第一反应是:Nacos 配置更新后,网关的缓存数据可能没有及时更新。带着这个猜想,我开始深入排查。
环境准备

首先准备了 3 个后端服务实例,端口分别为 8103、12040、12041,在 Nacos 中配置了对应的网关路由:xiaofu-8103、xiaofu-12040、xiaofu-12041,并将它们放在同一个权重组 xiaofu-group 中,实现基于权重的负载均衡。
  1. - id: xiaofu-8103
  2.   uri: http://127.0.0.1:8103/
  3.   predicates:
  4.     - Weight=xiaofu-group, 2
  5.     - Path=/test/version1/**
  6.   filters:
  7.     - RewritePath=/test/version1/(?<segment>.*),/$\{segment}
  8. - id: xiaofu-12040
  9.   uri: http://127.0.0.1:12040/
  10.   predicates:
  11.     - Weight=xiaofu-group, 1
  12.     - Path=/test/version1/**
  13.   filters:
  14.     - RewritePath=/test/version1/(?<segment>.*),/$\{segment}
  15. - id: xiaofu-12041
  16.   uri: http://127.0.0.1:12041/
  17.   predicates:
  18.     - Weight=xiaofu-group, 2
  19.     - Path=/test/version1/**
  20.   filters:
  21.     - RewritePath=/test/version1/(?<segment>.*),/$\{segment}
复制代码
使用 JMeter 进行持续请求测试,为了便于日志追踪,给每个请求参数都添加了随机数。
1.png

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

问题排查

为了获取更详细的日志信息,我将网关的日志级别调整为 TRACE。
启动 JMeter 后,随机修改三个实例的路由属性(uri、port、predicates、filters),请求没有出现报错,网关控制台也显示了更新后的路由属性,说明 Nacos 配置变更已成功同步到网关。
3.png

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

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

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

源码分析

通过分析源码,发现了一个专门计算权重的过滤器 WeightCalculatorWebFilter。它内部维护了一个 groupWeights 变量来存储路由权重信息。当配置变更事件发生时,会执行 addWeightConfig(WeightConfig weightConfig) 方法来更新权重配置。
  1. @Override
  2. public void onApplicationEvent(ApplicationEvent event) {
  3.     if (event instanceof PredicateArgsEvent) {
  4.         handle((PredicateArgsEvent) event);
  5.     }
  6.     else if (event instanceof WeightDefinedEvent) {
  7.         addWeightConfig(((WeightDefinedEvent) event).getWeightConfig());
  8.     }
  9.     else if (event instanceof RefreshRoutesEvent && routeLocator != null) {
  10.         if (routeLocatorInitialized.compareAndSet(false, true)) {
  11.             routeLocator.ifAvailable(locator -> locator.getRoutes().blockLast());
  12.         }
  13.         else {
  14.             routeLocator.ifAvailable(locator -> locator.getRoutes().subscribe());
  15.         }
  16.     }
  17. }
复制代码
addWeightConfig 方法的注释明确说明:该方法仅创建新的 GroupWeightConfig,而不进行修改。这意味着它只能新建或覆盖路由权重,无法清理已删除的路由权重信息。
  1. void addWeightConfig(WeightConfig weightConfig) {
  2.                 String group = weightConfig.getGroup();
  3.                 GroupWeightConfig config;
  4.                 // only create new GroupWeightConfig rather than modify
  5.                 // and put at end of calculations. This avoids concurency problems
  6.                 // later during filter execution.
  7.                 if (groupWeights.containsKey(group)) {
  8.                         config = new GroupWeightConfig(groupWeights.get(group));
  9.                 }
  10.                 else {
  11.                         config = new GroupWeightConfig(group);
  12.                 }
  13.                 final AtomicInteger index = new AtomicInteger(0);
  14.   ....省略.....
  15.                 if (log.isTraceEnabled()) {
  16.                         log.trace("Recalculated group weight config " + config);
  17.                 }
  18.                 // only update after all calculations
  19.                 groupWeights.put(group, config);
  20.         }
复制代码
解决方案

找到问题根源后,解决方案就清晰了
开始我怀疑可能是springcloud gateway 版本问题,将版本升级到了4.1.0,但结果还是存在这个问题。
7.png

看来只能手动更新缓存,需要监听 Nacos 路由配置变更事件,获取最新路由配置,并更新 groupWeights 中的权重数据。
以下是实现的解决方案代码:
  1. @Slf4j
  2. @Configuration
  3. public class WeightCacheRefresher {
  4.     @Autowired
  5.     private WeightCalculatorWebFilter weightCalculatorWebFilter;
  6.     @Autowired
  7.     private RouteDefinitionLocator routeDefinitionLocator;
  8.     @Autowired
  9.     private ApplicationEventPublisher publisher;
  10.     /**
  11.      * 监听路由刷新事件,同步更新权重缓存
  12.      */
  13.     @EventListener(RefreshRoutesEvent.class)
  14.     public void onRefreshRoutes() {
  15.         log.info("检测到路由刷新事件,准备同步更新权重缓存");
  16.         syncWeightCache();
  17.     }
  18.     /**
  19.      * 同步权重缓存与当前路由配置
  20.      */
  21.     public void syncWeightCache() {
  22.         try {
  23.             // 获取 groupWeights 字段
  24.             Field groupWeightsField = WeightCalculatorWebFilter.class.getDeclaredField("groupWeights");
  25.             groupWeightsField.setAccessible(true);
  26.             // 获取当前的 groupWeights 值
  27.             @SuppressWarnings("unchecked")
  28.             Map<String, Object> groupWeights = (Map<String, Object>) groupWeightsField.get(weightCalculatorWebFilter);
  29.             if (groupWeights == null) {
  30.                 log.warn("未找到 groupWeights 缓存");
  31.                 return;
  32.             }
  33.             log.info("当前 groupWeights 缓存: {}", groupWeights.keySet());
  34.             // 获取当前所有路由的权重组和路由ID
  35.             final Set<String> currentRouteIds = new HashSet<>();
  36.             final Map<String, Map<String, Integer>> currentGroupRouteWeights = new HashMap<>();
  37.             routeDefinitionLocator.getRouteDefinitions()
  38.                     .collectList()
  39.                     .subscribe(definitions -> {
  40.                         definitions.forEach(def -> {
  41.                             currentRouteIds.add(def.getId());
  42.                             def.getPredicates().stream()
  43.                                     .filter(predicate -> predicate.getName().equals("Weight"))
  44.                                     .forEach(predicate -> {
  45.                                         Map<String, String> args = predicate.getArgs();
  46.                                         String group = args.getOrDefault("_genkey_0", "unknown");
  47.                                         int weight = Integer.parseInt(args.getOrDefault("_genkey_1", "0"));
  48.                                         // 记录每个组中当前存在的路由及其权重
  49.                                         currentGroupRouteWeights.computeIfAbsent(group, k -> new HashMap<>())
  50.                                                 .put(def.getId(), weight);
  51.                                     });
  52.                         });
  53.                         log.info("当前路由配置中的路由ID: {}", currentRouteIds);
  54.                         log.info("当前路由配置中的权重组: {}", currentGroupRouteWeights);
  55.                         // 检查每个权重组,移除不存在的路由,更新权重变化的路由
  56.                         Set<String> groupsToRemove = new HashSet<>();
  57.                         Set<String> groupsToUpdate = new HashSet<>();
  58.                         for (String group : groupWeights.keySet()) {
  59.                             if (!currentGroupRouteWeights.containsKey(group)) {
  60.                                 // 整个权重组不再存在
  61.                                 groupsToRemove.add(group);
  62.                                 log.info("权重组 [{}] 不再存在于路由配置中,将被移除", group);
  63.                                 continue;
  64.                             }
  65.                             // 获取该组中当前配置的路由ID和权重
  66.                             Map<String, Integer> configuredRouteWeights = currentGroupRouteWeights.get(group);
  67.                             // 获取该组中缓存的权重配置
  68.                             Object groupWeightConfig = groupWeights.get(group);
  69.                             try {
  70.                                 // 获取 weights 字段
  71.                                 Field weightsField = groupWeightConfig.getClass().getDeclaredField("weights");
  72.                                 weightsField.setAccessible(true);
  73.                                 @SuppressWarnings("unchecked")
  74.                                 LinkedHashMap<String, Integer> weights = (LinkedHashMap<String, Integer>) weightsField.get(groupWeightConfig);
  75.                                 // 找出需要移除的路由ID
  76.                                 Set<String> routesToRemove = weights.keySet().stream()
  77.                                         .filter(routeId -> !configuredRouteWeights.containsKey(routeId))
  78.                                         .collect(Collectors.toSet());
  79.                                 // 找出权重发生变化的路由ID
  80.                                 Set<String> routesWithWeightChange = new HashSet<>();
  81.                                 for (Map.Entry<String, Integer> entry : weights.entrySet()) {
  82.                                     String routeId = entry.getKey();
  83.                                     Integer cachedWeight = entry.getValue();
  84.                                     if (configuredRouteWeights.containsKey(routeId)) {
  85.                                         Integer configuredWeight = configuredRouteWeights.get(routeId);
  86.                                         if (!cachedWeight.equals(configuredWeight)) {
  87.                                             routesWithWeightChange.add(routeId);
  88.                                             log.info("路由 [{}] 的权重从 {} 变为 {}", routeId, cachedWeight, configuredWeight);
  89.                                         }
  90.                                     }
  91.                                 }
  92.                                 // 找出新增的路由ID
  93.                                 Set<String> newRoutes = configuredRouteWeights.keySet().stream()
  94.                                         .filter(routeId -> !weights.containsKey(routeId))
  95.                                         .collect(Collectors.toSet());
  96.                                 if (!routesToRemove.isEmpty() || !routesWithWeightChange.isEmpty() || !newRoutes.isEmpty()) {
  97.                                     log.info("权重组 [{}] 中有变化:删除 {},权重变化 {},新增 {}",
  98.                                             group, routesToRemove, routesWithWeightChange, newRoutes);
  99.                                     // 如果有任何变化,我们将重新计算整个组的权重
  100.                                     groupsToUpdate.add(group);
  101.                                 }
  102.                                 // 首先,移除需要删除的路由
  103.                                 for (String routeId : routesToRemove) {
  104.                                     weights.remove(routeId);
  105.                                 }
  106.                                 // 如果权重组中没有剩余路由,则移除整个组
  107.                                 if (weights.isEmpty()) {
  108.                                     groupsToRemove.add(group);
  109.                                     log.info("权重组 [{}] 中没有剩余路由,将移除整个组", group);
  110.                                 }
  111.                             } catch (Exception e) {
  112.                                 log.error("处理权重组 [{}] 时出错", group, e);
  113.                             }
  114.                         }
  115.                         // 移除不再需要的权重组
  116.                         for (String group : groupsToRemove) {
  117.                             groupWeights.remove(group);
  118.                             log.info("已移除权重组: {}", group);
  119.                         }
  120.                         // 更新需要重新计算的权重组
  121.                         for (String group : groupsToUpdate) {
  122.                             try {
  123.                                 // 获取该组中当前配置的路由ID和权重
  124.                                 Map<String, Integer> configuredRouteWeights = currentGroupRouteWeights.get(group);
  125.                                 // 移除旧的权重组配置
  126.                                 groupWeights.remove(group);
  127.                                 log.info("已移除权重组 [{}] 以便重新计算", group);
  128.                                 // 为每个路由创建 WeightConfig 并调用 addWeightConfig 方法
  129.                                 Method addWeightConfigMethod = WeightCalculatorWebFilter.class.getDeclaredMethod("addWeightConfig", WeightConfig.class);
  130.                                 addWeightConfigMethod.setAccessible(true);
  131.                                 for (Map.Entry<String, Integer> entry : configuredRouteWeights.entrySet()) {
  132.                                     String routeId = entry.getKey();
  133.                                     Integer weight = entry.getValue();
  134.                                     WeightConfig weightConfig = new WeightConfig(routeId);
  135.                                     weightConfig.setGroup(group);
  136.                                     weightConfig.setWeight(weight);
  137.                                     addWeightConfigMethod.invoke(weightCalculatorWebFilter, weightConfig);
  138.                                     log.info("为路由 [{}] 添加权重配置:组 [{}],权重 {}", routeId, group, weight);
  139.                                 }
  140.                             } catch (Exception e) {
  141.                                 log.error("重新计算权重组 [{}] 时出错", group, e);
  142.                             }
  143.                         }
  144.                         log.info("权重缓存同步完成,当前缓存的权重组: {}", groupWeights.keySet());
  145.                     });
  146.         } catch (Exception e) {
  147.             log.error("同步权重缓存失败", e);
  148.         }
  149.     }
  150. }
复制代码
网上找一圈并没发现官方的修改意见,可能是咱们使用方式不对导致的,要不如此明显的BUG早就有人改了吧!

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

3 天前

举报

不错,里面软件多更新就更好了
您需要登录后才可以回帖 登录 | 立即注册