3. 网关核心架构:请求处理流程、过滤器链设计、路由表管理、负载均衡策略
网关这东西,说白了就是一个「交通枢纽」。所有的外部请求都得从它这儿过,它得决定谁该去哪、怎么去、能不能去。我做了这么多年网关,见过太多把网关当成简单反向代理来用的项目,结果流量一上来就崩了。
这一章,咱们就把网关的四个核心模块拆开揉碎了讲。你理解透了这个架构,后面做方案设计心里就有底了。
3.1 请求处理流程:一条请求的完整旅程
一条请求从客户端发出来,到后端服务返回响应,中间经历了什么?我画了一张图,你先看个全貌。
你看这个流程,其实就五步:
- 请求到达:客户端发起 HTTP 请求,网关接收。
- 前置过滤器链:做鉴权、限流、日志、协议转换等预处理。
- 路由匹配:根据请求的 path、header、参数,找到对应的路由规则。
- 负载均衡:从目标服务的实例列表中选一个,转发过去。
- 后置过滤器链:处理响应,比如改写 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 小结
网关的核心架构,说白了就是四个模块的协作:
- 请求处理流程:前置过滤器 → 路由匹配 → 负载均衡 → 后置过滤器。
- 过滤器链设计:每个过滤器只做一件事,用责任链模式串联。
- 路由表管理:动态刷新,精确匹配优先,用配置中心管理。
- 负载均衡策略:加权轮询 + 健康检查,够用且稳定。
这一章的内容,是后面所有方案设计的基础。你把这些搞透了,后面讲「高可用网关设计」「灰度发布方案」的时候,理解起来会轻松很多。