【架构实战】API网关设计与演进:从Nginx到自研网关
一、场景:Nginx 配置到 5000 行后的崩溃
2022 年,我司 API 网关用的是 Nginx + OpenResty + Lua。
刚开始挺爽------十几行 location 就能把请求代理到后端服务。但两年后变成了 5000 行的 nginx.conf:
nginx
# 想象一下 5000 行这种配置
location /api/order/ {
set $target "order-service-v2";
proxy_pass http://$target;
}
location /api/payment/ {
set $target "payment-service-v1";
rewrite_by_lua_block {
-- 100 行自定义鉴权逻辑
}
proxy_pass http://$target;
}
问题接踵而来:
- 配置地狱:修改一个路由规则要在 5000 行里翻半天
- 灰度不灵活:想按用户 ID 分流,Lua 脚本写了 200 行
- 动态扩容不友好:后端服务扩缩容需要 reload Nginx
- 可观测性差 :出问题了只能
tail -f access.log,没有集中式的监控面板 - 团队维护成本高:只有两个人懂那 5000 行 Nginx 配置
最终决定:从 Nginx+OpenResty 出发,经过 Kong 的短暂过渡,最终走向自研网关。这是一条充满坑的路,也是本文要分享的全过程。
二、网关的核心职责
在做技术选型之前,先梳理清楚 API 网关到底要做什么:
┌──────────────┐
│ 客户端 │
└──────┬───────┘
│
┌──────▼───────┐
│ WAF/CDN │
└──────┬───────┘
│
┌────────────────────▼────────────────────┐
│ API 网关 │
│ │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌───────┐ │
│ │鉴权 │ │限流 │ │路由 │ │协议 │ │
│ │OAuth │ │令牌桶│ │灰度 │ │转换 │ │
│ └──────┘ └──────┘ └──────┘ └───────┘ │
│ │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌───────┐ │
│ │熔断 │ │重试 │ │日志 │ │监控 │ │
│ │降级 │ │策略 │ │审计 │ │告警 │ │
│ └──────┘ └──────┘ └──────┘ └───────┘ │
└─────────┬───────────┬───────────────────┘
│ │
┌───────▼───┐ ┌─────▼──────┐
│ 订单服务 │ │ 支付服务 │
└───────────┘ └────────────┘
网关不是在请求链路上多加一跳那么简单------它是流量治理的"咽喉要道"。
三、阶段一:Nginx + OpenResty --- 最朴素的起点
3.1 架构
客户端 → Nginx(OpenResty) → Lua 鉴权 → 上游服务
│
▼
Redis (限流计数器)
Consul (服务发现)
3.2 典型配置
nginx
# 限流配置
lua_shared_dict limit_req_store 100m;
server {
listen 80;
location /api/ {
access_by_lua_block {
-- 限流逻辑
local limit = require "resty.limit.req"
local lim, err = limit.new("limit_req_store", 200, 100)
local delay, err = lim:incoming(ngx.var.binary_remote_addr, true)
if not delay then
if err == "rejected" then
ngx.exit(503)
end
end
}
# 动态路由
set_by_lua_block $target_host {
local consul = require "resty.consul"
local instances = consul:get_service("order-service")
-- 随机选一个实例
return instances[math.random(#instances)].address
}
proxy_pass http://$target_host;
}
}
3.3 Nginx + OpenResty 的局限
本质上是配置文件驱动,不是真正的 API 管理平台:
| 功能 | Nginx + OpenResty | 期望 |
|---|---|---|
| 动态路由 | Lua 脚本硬编码 | 管理后台点选规则 |
| 灰度发布 | 需要改 Lua | 按 Request Header/Cookie 动态分流 |
| 监控面板 | 无 | 实时 QPS、延迟分布 |
| 配置热更新 | reload Nginx | 实时生效 |
| 多租户 | 多套 Nginx | 逻辑隔离 |
四、阶段二:Kong --- 成熟的过渡方案
4.1 为什么选 Kong
Kong 基于 OpenResty,提供了 Plugin 机制和管理后台。对我们来说迁移成本最低------OpenResty 的技能可以复用。
4.2 Kong 的 Plugin 体系
lua
-- 自定义 Kong Plugin:按用户等级分流
local BasePlugin = require "kong.plugins.base_plugin"
local UserLevelRouteHandler = BasePlugin:extend()
function UserLevelRouteHandler:new()
UserLevelRouteHandler.super.new(self, "user-level-route")
end
function UserLevelRouteHandler:access(conf)
UserLevelRouteHandler.super.access(self)
-- 从 Header 或 JWT 中解析用户等级
local user_level = kong.request.get_header("X-User-Level") or "normal"
-- VIP 用户路由到高性能集群
if user_level == "vip" then
kong.service.set_upstream("order-service-vip")
end
end
return UserLevelRouteHandler
4.3 Kong 暴露的问题
用了半年 Kong 后,新的痛点浮出水面:
- Plugin 开发仍然是 Lua:团队没人愿意写 Lua,招人也困难
- 数据库依赖(PostgreSQL):又多了一个需要维护的中间件
- 性能瓶颈 :高峰期 Kong 的
workers进程 CPU 打到 95%,因为 Plugin 里的 Lua 正则匹配太耗时 - 复杂路由策略不足:需要按照订单金额区间路由到不同的折扣服务,Kong 的路由表达能力不够
到这一步我们意识到:我们需要的不是"更好的 Nginx",而是一个用 Go/Java 写的、面向业务路由的网关。
五、阶段三:自研网关 --- 面向业务的设计
5.1 设计原则
1. 动态配置:配置实时热更新,不重启不中断
2. 高性能:纯异步 IO,单机支撑 10万 QPS
3. 可扩展:Plugin 链式调用,热插拔
4. 可观测:Metrics、Tracing、Logging 三件套
5. 对开发者友好:Java + 注解声明式配置
5.2 整体架构
┌─────────────────────────────────────────────────────────┐
│ 自研 API 网关 │
│ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Netty Server (NIO) │ │
│ │ Reactor 主从线程模型 │ │
│ └──────────────────────┬────────────────────────────┘ │
│ │ │
│ ┌──────────────────────▼────────────────────────────┐ │
│ │ Plugin Chain(责任链模式) │ │
│ │ │ │
│ │ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │
│ │ │鉴权 │──▶│限流 │──▶│路由 │──▶│灰度 │──▶│负载 │ │ │
│ │ │Plugin│ │Plugin│ │Plugin│ │Plugin│ │均衡 │ │ │
│ │ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────▼────────────────────────────┐ │
│ │ Route Matcher │ │
│ │ 前缀/正则匹配 → 含灰度规则 → 选后端实例 │ │
│ └────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────▼────────────────────────────┐ │
│ │ Backend Connection Pool │ │
│ │ 连接复用 / 健康检查 / 主动探活 │ │
│ └────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
5.3 核心代码:Plugin 链与路由引擎
java
/**
* Plugin 接口------所有功能模块的抽象
*/
public interface GatewayPlugin {
/** Plugin 名称 */
String name();
/** 优先级,数值越小越先执行 */
int order();
/** 是否启用 */
boolean enabled();
/** 执行 Plugin 逻辑 */
Mono<GatewayResponse> execute(GatewayRequest request, PluginChain chain);
}
/**
* 责任链:顺序调用 Plugin
*/
public class PluginChain {
private final List<GatewayPlugin> plugins;
private int currentIndex = 0;
public PluginChain(List<GatewayPlugin> plugins) {
this.plugins = plugins.stream()
.filter(GatewayPlugin::enabled)
.sorted(Comparator.comparingInt(GatewayPlugin::order))
.collect(Collectors.toList());
}
public Mono<GatewayResponse> next(GatewayRequest request) {
if (currentIndex >= plugins.size()) {
// 所有 Plugin 执行完成 → 代理到后端
return proxyToBackend(request);
}
GatewayPlugin plugin = plugins.get(currentIndex++);
return plugin.execute(request, this);
}
}
/**
* 限流 Plugin 实现:基于 Redis 滑动窗口 + Lua
*/
@Component
public class RateLimitPlugin implements GatewayPlugin {
@Override
public String name() { return "rate-limit"; }
@Override
public int order() { return 200; } // 在鉴权之后
@Override
public boolean enabled() { return true; }
@Override
public Mono<GatewayResponse> execute(GatewayRequest request, PluginChain chain) {
RateLimitRule rule = rateLimitConfig.matchRule(request);
if (rule == null) {
// 无匹配规则,放行
return chain.next(request);
}
String key = buildLimitKey(request, rule);
return redisTemplate.execute(luaScript, key, rule.getQps(), rule.getWindowSeconds())
.flatMap(allowed -> {
if (allowed) {
return chain.next(request);
}
return Mono.just(
GatewayResponse.tooManyRequests("请求频率超限"));
});
}
}
/**
* 路由 + 灰度 Plugin
*/
@Component
public class RoutePlugin implements GatewayPlugin {
@Override
public String name() { return "route"; }
@Override
public int order() { return 300; }
@Override
public Mono<GatewayResponse> execute(GatewayRequest request, PluginChain chain) {
// 匹配路由规则
RouteConfig route = routeMatcher.match(request);
// 灰度规则匹配
Backend backend = route.matchCanaryRule(request);
request.setTargetBackend(backend);
return chain.next(request);
}
}
5.4 配置动态化设计
java
/**
* 配置变更监听:基于 Nacos / Apollo
*/
@Component
public class RouteConfigListener {
@NacosConfigListener(dataId = "gateway-routes", group = "DEFAULT_GROUP")
public void onRouteConfigChange(String configJson) {
List<RouteConfig> newRoutes = JsonUtils.fromJson(configJson,
new TypeReference<List<RouteConfig>>() {});
// 原子替换路由表
routeTable.set(newRoutes);
log.info("路由配置热更新: {} 条路由规则", newRoutes.size());
}
}
六、网关演进对比
| 维度 | Nginx + OpenResty | Kong | 自研网关 |
|---|---|---|---|
| 配置管理 | 配置文件 | 管理后台 + DB | Nacos 动态配置 |
| 热更新 | reload (会有短暂中断) | 管理员 API | 配置变更实时生效 |
| 灰度发布 | Lua 脚本硬编码 | Plugin 实现 | 声明式路由规则 |
| 可观测性 | access.log | Kong Dashboard | Prometheus + Grafana |
| 开发语言 | Lua + Nginx C 模块 | Lua | Java |
| 性能 | 极高(C + LuaJIT) | 高 | 高(Netty) |
| 团队维护成本 | Lua 能力要求高 | 中 | Java 生态系统 |
| 单机 QPS | 50 万+ | 10 万+ | 15 万+ |
七、自研网关的性能基准测试
测试环境:16 核 32G, Netty (EventLoopGroup=8)
后端:静态返回 200 OK, 1ms 延迟
┌─────────────────────────────────────┐
│ QPS: 152,000 req/s │
│ P50: 0.8ms │
│ P99: 3.2ms │
│ P999: 8.5ms │
│ CPU: 65%(8 个 worker 线程) │
│ 内存: 2.3GB │
│ 连接数: 50,000 并发(HttpClient) │
└─────────────────────────────────────┘
对比 Kong 同环境下 12 万 QPS(P99 6ms),自研网关吞吐提升约 30%,延迟降低约 50%。
实际瓶颈不在网关本身,而在后端的响应速度------网关引入的额外延迟不到 1ms。
八、踩坑实录
坑1:Nginx reload 导致长连接中断
直接 kill -HUP 会导致已有连接全部断开,影响正在进行中的文件上传。改用 nginx -s quit + 新进程平滑升级,但仍有短暂中断。
解 :自研网关基于 Netty,通过 Signal.handle() 注册优雅关闭,30 秒内排干已有请求。
坑2:Plugin 链顺序写反导致鉴权绕过
限流 Plugin 排在鉴权前面------没登录的用户也能消耗限流额度,恶意利用导致大量 429。
解 :鉴权 Plugin order=100,限流 Plugin order=200,确保"先鉴权、后限流"。
坑3:Kong 数据库成为单点瓶颈
Kong 的路由查询依赖 PostgreSQL 的连接池,高峰期 DB 连接池耗尽,Kong Admin API 无响应。
解:自研网关不依赖外部数据库存储路由,配置全量加载到内存。
九、总结
从 Nginx 到自研网关的演进,核心脉络是:
- Nginx + OpenResty 是顶配起点:单机 50 万 QPS,但配置难以管理
- Kong 是成熟的过渡方案:Plugin 体系 + 管理后台,适合中等规模场景
- 自研网关是终局选择:当路由规则从几十条变成几百条、灰度策略从按 Header 变成按用户画像时,你需要一个"懂业务"的网关
教训:
- 网关的瓶颈永远不在网关本身------在后端服务的响应速度。网关引入的延迟应控制在 1ms 以内
- 配置管理比性能更重要------10 万 QPS 够了,但 5000 行 Nginx 配置的风险远超性能不足
- 自研不是目的,场景匹配才是------团队没有 Netty 高手就不要自研,Kong 或者 APISIX 一样能打
- 可观测性是生命线------网关是流量的"咽喉要道",出了问题必须能在 10 秒内定位到是哪个 Plugin、哪条路由
架构演进没有终点------下一个阶段,我们在考虑把网关接入 Service Mesh,让网关和 Sidecar 协作,走向全链路流量治理。
作者:架构实战团队
日期:2026-07-04
标签:#API网关 #Nginx #Kong #自研网关 #微服务 #架构演进