3. 网关核心架构:请求处理流程、过滤器链设计、路由表管理、负载均衡策略

网关这东西,说白了就是一个「交通枢纽」。所有的外部请求都得从它这儿过,它得决定谁该去哪、怎么去、能不能去。我做了这么多年网关,见过太多把网关当成简单反向代理来用的项目,结果流量一上来就崩了。

这一章,咱们就把网关的四个核心模块拆开揉碎了讲。你理解透了这个架构,后面做方案设计心里就有底了。

3.1 请求处理流程:一条请求的完整旅程

一条请求从客户端发出来,到后端服务返回响应,中间经历了什么?我画了一张图,你先看个全貌。

网关请求处理流程 网关请求处理流程 客户端 前置过滤器链 路由匹配 负载均衡 后端服务A 后端服务B 后端服务C 响应返回 后置过滤器链 说明 请求先过前置过滤器, 再路由匹配,然后负载均衡。 响应回来走后置过滤器。

你看这个流程,其实就五步:

  1. 请求到达:客户端发起 HTTP 请求,网关接收。
  2. 前置过滤器链:做鉴权、限流、日志、协议转换等预处理。
  3. 路由匹配:根据请求的 path、header、参数,找到对应的路由规则。
  4. 负载均衡:从目标服务的实例列表中选一个,转发过去。
  5. 后置过滤器链:处理响应,比如改写 header、记录耗时、做缓存。

我的习惯:我会把「请求日志」放在前置过滤器的第一个位置。为什么?因为一旦请求出问题,日志是唯一的线索。我曾经排查过一个线上故障,就是靠前置日志里的一条 header 信息定位到的。

3.2 过滤器链设计:插拔式的处理单元

过滤器链是网关的灵魂。你想想看,如果没有过滤器链,所有逻辑都写在一个大方法里,那代码得多恐怖?

过滤器链的设计核心就一句话:每个过滤器只做一件事,并且可以自由组合。

3.2.1 过滤器的生命周期

一个标准的过滤器接口,通常包含这几个方法:

public interface GatewayFilter {
    // 前置处理:请求到达后端之前执行
    void pre(FilterContext ctx);
    
    // 后置处理:后端响应返回之后执行
    void post(FilterContext ctx);
    
    // 异常处理:链路中发生异常时执行
    void error(FilterContext ctx, Throwable t);
    
    // 过滤器的执行顺序,数值越小越优先
    int getOrder();
}

嗯,这里要注意:getOrder() 这个顺序值,我建议你从 0 开始,每 10 步留一个空位。比如 0、10、20、30。这样以后要插入新过滤器,不用改别人的顺序。

3.2.2 常见的过滤器分类

过滤器类型 典型实现 执行阶段
鉴权过滤器 JWT 校验、OAuth2 Token 验证 前置
限流过滤器 令牌桶、滑动窗口 前置
日志过滤器 请求/响应日志记录 前置 + 后置
协议转换过滤器 HTTP 转 gRPC、JSON 转 XML 前置
响应改写过滤器 添加统一 header、格式化错误响应 后置
熔断过滤器 基于 Hystrix 或 Sentinel 前置(包装整个链路)

我曾经踩过一个坑:把限流过滤器放在了鉴权过滤器后面。结果恶意请求虽然被限流了,但鉴权逻辑已经执行了一遍,白白消耗了数据库连接。正确的做法是:限流放在最前面,鉴权紧随其后。

3.2.3 过滤器链的执行模型

过滤器链的执行模型,我推荐用「责任链模式」。每个过滤器持有下一个过滤器的引用,执行完自己的逻辑后,调用 next() 传递下去。

public class FilterChain {
    private List<GatewayFilter> filters;
    private int index = 0;
    
    public void doFilter(FilterContext ctx) {
        if (index < filters.size()) {
            GatewayFilter filter = filters.get(index++);
            // 前置逻辑
            filter.pre(ctx);
            // 继续往下走
            doFilter(ctx);
            // 后置逻辑
            filter.post(ctx);
        } else {
            // 所有过滤器执行完毕,转发到后端
            ctx.forward();
        }
    }
}

你想想看,这种设计的好处是什么?每个过滤器只关心自己的那一段逻辑,新增一个过滤器就像插一个积木一样简单。

3.3 路由表管理:网关的「交通地图」

路由表就是网关的导航系统。请求来了,网关得知道「这个请求该发给谁」。

3.3.1 路由表的核心结构

一条路由规则,通常包含这些字段:

字段 说明 示例
id 路由唯一标识 user-service-route
predicates 匹配条件(path、header、query 等) Path=/user/**
filters 该路由专属的过滤器 AddRequestHeader=X-Trace, 123
uri 目标地址(可以是 lb:// 或 http://) lb://user-service
order 路由优先级 0

3.3.2 路由匹配的优先级

路由匹配不是「谁先定义谁先匹配」,而是按 order 字段排序。我见过有人把精确匹配和通配符混在一起,结果通配符总是先命中,精确匹配永远走不到。

避坑指南:精确路径的路由,order 值要小于通配符路由。比如 /user/info 的 order 设为 0,/user/** 的 order 设为 10。这样精确匹配优先。

3.3.3 路由表的动态刷新

生产环境里,路由表不能写死在配置文件里。我建议用配置中心(比如 Nacos、Apollo)来管理。当路由变更时,网关不需要重启,只需要监听配置变化,重新加载路由表即可。

// 伪代码:监听配置变化
@EventListener
public void onRouteRefresh(RouteRefreshEvent event) {
    // 从配置中心拉取最新路由
    List<RouteDefinition> newRoutes = configCenter.getRoutes();
    // 清空旧路由,加载新路由
    routeTable.clear();
    routeTable.addAll(newRoutes);
    log.info("路由表已刷新,当前路由数:{}", routeTable.size());
}

我个人习惯在刷新时加一个「灰度开关」。先让 10% 的流量走新路由,观察 5 分钟没问题再全量切换。这个机制救过我一次,有一次新路由写错了 path,灰度期间就发现了,没影响到线上。

3.4 负载均衡策略:选谁去干活?

路由匹配到了目标服务,但一个服务可能有多个实例。选哪个?这就是负载均衡的事。

3.4.1 常见的负载均衡算法

算法 原理 适用场景
轮询 按顺序轮流分配 各实例性能均匀
加权轮询 按权重分配,权重高的多分 实例配置不同(如 4C8G vs 8C16G)
最少连接 选当前活跃连接数最少的 长连接场景
一致性哈希 根据请求的 key(如 userId)哈希取模 需要会话保持的场景
随机 随机选一个 测试环境或流量均匀的场景

3.4.2 我推荐的做法

大多数业务场景,加权轮询 + 健康检查 就够用了。为什么?

  • 加权轮询能应对实例配置不均的情况。
  • 健康检查能自动摘掉挂掉的实例。

但要注意:健康检查不能只检查「进程是否活着」,还要检查「服务是否真的能处理请求」。我曾经遇到过,进程活着但数据库连接池满了,健康检查返回 200,结果请求打过去全超时。

我的改进方案:健康检查接口里,顺便检查一下依赖资源的状态。比如 Redis 连接、数据库连接、消息队列连接。任何一个挂了,就返回 503,网关自动摘掉这个实例。

3.4.3 负载均衡的代码示例

public class WeightedRoundRobinLoadBalancer implements LoadBalancer {
    private final List<Server> servers;
    private final AtomicInteger index = new AtomicInteger(0);
    
    public Server select() {
        // 先过滤掉不健康的实例
        List<Server> healthyServers = servers.stream()
            .filter(Server::isHealthy)
            .collect(Collectors.toList());
        
        if (healthyServers.isEmpty()) {
            throw new NoAvailableServerException("没有可用的服务实例");
        }
        
        // 加权轮询
        int totalWeight = healthyServers.stream()
            .mapToInt(Server::getWeight).sum();
        int currentIndex = index.getAndIncrement() % totalWeight;
        
        for (Server server : healthyServers) {
            currentIndex -= server.getWeight();
            if (currentIndex < 0) {
                return server;
            }
        }
        // fallback
        return healthyServers.get(0);
    }
}

这段代码看起来简单,但有个细节要注意:index 是 AtomicInteger,保证并发安全。我见过有人用 int 加 synchronized,性能差了不少。

3.5 小结

网关的核心架构,说白了就是四个模块的协作:

  • 请求处理流程:前置过滤器 → 路由匹配 → 负载均衡 → 后置过滤器。
  • 过滤器链设计:每个过滤器只做一件事,用责任链模式串联。
  • 路由表管理:动态刷新,精确匹配优先,用配置中心管理。
  • 负载均衡策略:加权轮询 + 健康检查,够用且稳定。

这一章的内容,是后面所有方案设计的基础。你把这些搞透了,后面讲「高可用网关设计」「灰度发布方案」的时候,理解起来会轻松很多。