2. 网关架构基础:核心组件与请求流生命周期
做内部开发者平台,网关是第一道关口。很多团队把网关当成简单的“反向代理+路由”,觉得用 Nginx 就完事了。但说实话,当平台需要支持多团队、多协议、灰度发布、限流熔断这些能力时,这种理解远远不够。
我个人习惯把网关拆成三个层面去看:接入层、转发层、治理层。每一层负责的事情不同,但合在一起就构成了一个完整的请求生命周期。你想想看,一个 HTTP 请求从客户端发起,到后端服务响应,中间经过了多少环节?
2.1 网关的核心组件
先别急着画大图,我们把组件一个个拆开说。
| 组件 | 职责 | 典型实现 |
|---|---|---|
| 协议适配器 | 接收各种协议(HTTP/HTTPS/gRPC/WebSocket)并统一转换为内部模型 | Netty、Tomcat、Vert.x |
| 路由引擎 | 根据请求头、路径、参数匹配到目标服务 | Spring Cloud Gateway、Kong、APISIX |
| 过滤器链 | 按序执行鉴权、限流、日志、重试等横切逻辑 | 责任链模式、插件机制 |
| 负载均衡器 | 在多个上游实例中选择目标节点 | 一致性哈希、加权轮询、最小连接数 |
| 连接管理器 | 管理上下游连接池、保持长连接、处理半关闭 | 连接池、keep-alive、连接复用 |
| 缓存/降级模块 | 缓存静态响应或降级逻辑,防止雪崩 | 本地缓存、Redis、Hystrix |
| 监控与遥测 | 采集请求延迟、错误率、流量指标 | Prometheus metrics、OpenTelemetry |
这些组件里,最容易出问题的是连接管理器。我在项目中遇到过,因为默认连接池太小,双十一流量一上来,网关自己先挂了。所以搭建网关前,一定要把连接池参数(最大连接、等待队列、超时)理解透。
2.2 请求流生命周期
一个请求从进网关到出网关,大致走 9 个步骤。我给你画了张图,先看总体流程。
图里画了 9 个阶段,但实际生产中你会看到更细的切分。比如在我的团队,我们会在前置过滤里做「鉴权、限流、黑白名单、请求头改写」;后置过滤里做「响应压缩、错误码统一、跨域头注入」。
请求流的核心原则: 前置过滤只处理请求相关的逻辑,后置过滤只处理响应相关的逻辑。别把响应逻辑放前置,也别在前置里修改上游返回的数据。我见过有人在前置 filter 里直接读 upstream 的响应流……导致后置 filter 读不到数据,排查了半天。
2.3 各阶段的避坑指南
- 协议解析阶段: 注意 HTTP/2 的流控制。我曾经遇到过 HTTP/2 的 SETTINGS_MAX_CONCURRENT_STREAMS 设置太小,导致并发请求被主动拒绝。
- 路由匹配时: 路径匹配要区分精确匹配与前缀匹配。我建议优先使用精确路径,避免 /api/{version}/users/* 这种模糊匹配导致路径穿越漏洞。
- 前置过滤: 鉴权失败后务必立即返回 401/403,不要再继续往下走。曾经有团队在鉴权失败后还继续执行限流,浪费了 。
- 负载均衡: 对于需要会话保持的场景,别用轮询,用一致性哈希。不然用户刚登上去,下一个请求就跳到别的实例了。
- 上游请求阶段: 设置合理的超时时间(连接超时、读超时、写超时)。我在一个项目中用过 30s 的读超时,结果有个慢 SQL 把整个网关线程池拖垮了。
- 后置过滤: 不要在后置 filter 里做阻塞操作。比如写日志,可以异步写或扔消息队列。
- 响应缓存: 只缓存 GET 请求的幂等响应,千万别缓存 POST 或动态数据。
⚠️ 一个血的教训: 我曾经接手一个项目,网关在上游请求阶段做了一次 DNS 解析缓存,而且缓存时间设成了 5 分钟。结果上游服务发生故障扩容,新 IP 迟迟无法被识别,导致部分请求一直打到已下线的老机器上。从那以后我养成了习惯——网关层尽量少做 DNS 缓存,实在要做,TTL 不要超过 30 秒。
2.4 谈谈请求上下文的传递
你想想看,一个请求进来之后,鉴权通过后获取的用户信息、原始请求 IP、traceId 这些数据,怎么让后续的 filter 和上游服务都能拿到?
我建议用请求上下文(Request Context) 对象,在接口层创建,贯穿整个生命周期。Spring Cloud Gateway 里的 ServerWebExchange 就是一个典型的上下文容器。你可以向它里边塞 attribute,然后在各个 filter 里读取。
// 伪代码示例:在鉴权 filter 中设置用户信息
public class AuthFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 伪代码:解析
String userId = parseToken(exchange.getRequest());
exchange.getAttributes().put("userId", userId);
return chain.filter(exchange);
}
}
// 在路由转发时,将 userId 放入请求头传给上游
public class ForwardFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 从上下文取出 userId
String userId = exchange.getAttribute("userId");
ServerHttpRequest modifiedRequest = exchange.getRequest().mutate()
.header("X-User-Id", userId)
.build();
ServerWebExchange modifiedExchange = exchange.mutate().request(modifiedRequest).build();
return chain.filter(modifiedExchange);
}
}
这样上游服务只需要读取请求头 X-User-Id 就能拿到用户身份,不用再自己解析 了。我个人特别推崇这种「网关做一次鉴权,下游只管用」的模式。
2.5 连接管理与线程模型
网关的性能瓶颈往往不在 CPU,而在连接数和线程切换。你可以做个简单测试:用 wrk 并发 1000 打一个 NIO 网关和一个 BIO 网关,看谁先撑不住。
现代网关通常用 Reactor 模型(如 Netty)。事件循环线程(EventLoop)不能阻塞,一旦阻塞整个网关吞吐量急剧下降。我在优化一个网关时发现,某个 filter 里调了一个同步的 Redis 查询,导致 EventLoop 被占住,QPS 从 5 万跌到了 3000。
| 模型 | 优点 | 缺点 | 使用场景 |
|---|---|---|---|
| BIO (每个连接一个线程) | 编程简单 | 线程开销大,连接数受限 | 内部管理工具,并发低 |
| NIO (Reactor) | 高并发,少线程 | 编程复杂,须避免阻塞 | 生产级网关(APISIX、Kong、Spring Cloud Gateway) |
| 协程 (Kotlin Coroutine / Loom) | 极轻量,易编程 | 生态不够成熟 | 实验性项目,未来趋势 |
说白了,如果你是 Java 技术栈,推荐用 NIO 模型(Spring Cloud Gateway 底层就是 Reactor Netty)。但一定要记住:不要在 filter 里做任何同步阻塞操作。实在避免不了,那就把阻塞操作扔到单独的线程池里,用 subscribeOn(Schedulers.boundedElastic()) 这种办法隔离。
一个常用技巧: 网关层做日志时,千万别写同步的 log4j 追加器。用异步 Appender 或者直接上报到消息队列,这样写入磁盘的时间不会阻塞请求通路。
2.6 监控与可观测性
网关是流量入口,出了问题影响面是全平台的。所以我建议从第一天就把可观测性做进去,不要等出事故再补。
- 延迟拆解: 监控每个阶段的耗时(协议解析、路由、鉴权、上游请求、响应处理)。方便定位瓶颈。
- 错误分类: 区分 4xx 业务错误与 5xx 系统错误。超过 5% 的 5xx 就应该告警。
- 全链路追踪: 在网关层生成 traceId,放入请求头传给下游。这样从客户端到服务端到数据库,整个链路都能串起来。
我见过一个团队,网关没有做任何监控,直到用户投诉“页面打不开了”才发现网关 OOM 了。其实提前加几个指标就能预警,比如连接数、线程阻塞数、GC 频率。
好,关于网关架构基础的核心组件和请求流生命周期,就先聊到这儿。这些底层知识是后续讲网关扩展(插件开发、自定义 filter、多协议支持)的基石。理解透了,你就能在内部开发者平台上搭建一个既稳定又好扩展的网关层。