前年我们团队接手了一个日活超过 200 万的用户社区系统重构任务,核心痛点是原有的 Spring Cloud Gateway 在晚高峰时段频繁出现 Full GC,导致接口响应时间从平均 80ms 飙升到 800ms 以上。作为项目负责人,我主导了 API 网关的选型与迁移工作。在对比了 Kong 3.6(2024年4月发布)、APISIX 3.8 和 Tyk 之后,我们最终选择了 Kong 3.6,原因在于其基于 OpenResty(Nginx + Lua)的云原生架构在处理高并发非阻塞 I/O 场景下的天然优势,这比基于 Java 的网关更适合我们这种读多写少的社区场景。
我直接搭建了三套环境进行压测对比。测试场景模拟了社区首页信息流加载,涉及鉴权、限流和请求转发。后端服务是一个简单的 Go 编写的 HTTP 服务,返回固定大小的 JSON 数据。压测工具使用 wrk,并发连接数设置为 1000,持续压测 10 分钟。
压测数据对比如下:
| 网关类型 | 版本 | 平均延迟 (ms) | P99 延迟 (ms) | QPS | 内存占用 (MB) |
| :--- | :--- | :--- | :--- | :--- | :--- |
| Spring Cloud Gateway | 3.1.0 | 185 | 890 | 4,200 | 780 (堆内) |
| APISIX | 3.8.0 | 45 | 120 | 18,500 | 65 |
| Kong | 3.6 | 42 | 110 | 19,200 | 60 |
数据表明,Kong 3.6 在 QPS 和延迟表现上略优于 APISIX,且内存占用极低。原因在于 Kong 的插件机制运行在 Nginx 的 worker 进程内,没有额外的 JVM 堆内存开销和 GC 停顿。对于我们的社区系统,这意味着在大促或热点事件导致流量突增时,网关层不会因为 GC 问题导致服务不可用。
一次线上延迟突增的排查过程:
在灰度上线初期,我们曾遇到一个奇怪的问题:部分用户反馈图片加载接口偶尔超时。我通过 Kong 的 Admin API 查看监控指标,发现 kong_latency 正常,但 upstream_latency 波动巨大。起初怀疑是后端问题,但后端日志显示处理时间稳定在 50ms 以内。
我重新审查了 Kong 的配置,发现我们在 rate-limiting 插件中配置了 redis 作为存储。原因在于,当时为了全局限流,我们直接复用了业务侧的一个高负载 Redis 实例。解决方案是将网关的 Redis 实例与业务 Redis 物理隔离,并配置了连接池。调整后,超时现象消失。这个经历让我意识到,虽然 Kong 本身性能强劲,但其依赖的外部组件(如数据库、Redis)往往是性能瓶颈点。
此外,Kong 3.6 原生支持 gRPC 和 WebSocket,这对我们社区后续引入 IM 即时通讯功能至关重要。我们不需要再引入额外的代理组件,统一在 Kong 层进行流量治理。
以下是我们在压测阶段使用的 Kong 3.6 核心配置片段,采用 DB-less 模式以提升性能:
在重构电商平台的订单系统时,我面临的最大挑战不是代码逻辑,而是环境配置的一致性。之前我们使用 Kong 的 Admin API 手动添加路由和插件,导致测试环境、预发环境和生产环境的配置出现了严重的漂移。有一次,生产环境因为漏配了一个 cors 插件,导致前端页面在大促前半小时无法下单。解决方案是引入 DecK 1.16(Kong 3.6 推荐配套版本)进行声明式配置管理,实现 GitOps 工作流。
DecK 的核心逻辑是将 Kong 的所有配置(Services, Routes, Plugins, Consumers)导出为一个 YAML 文件,通过版本控制来管理变更。我制定了严格的流程:任何配置变更必须提交 PR 到 Git 仓库,经过 Code Review 后,由 CI/CD 流水线自动执行 deck sync。
灰度发布实战场景:
电商订单系统上线新版本时,我们需要验证新逻辑是否会导致库存扣减异常。我利用 Kong 3.6 的 canary 插件(基于流量比例)配合 upstream 对象来实现灰度发布。
具体做法是:在 Kong 中定义两个 Target,分别对应订单服务的稳定版(v1)和新版(v2)。通过 canary 插件,我将 5% 的流量导入 v2 版本。
以下是我们实际使用的 deck 配置文件,展示了如何定义 Upstream 和 Canary 规则:
为什么不这么做会怎样?
如果不使用声明式配置,我们可能会陷入“配置漂移”的泥潭。比如,某次紧急修复中,运维人员直接在 Admin API 上修改了限流阈值,但忘记更新文档。几天后,另一个工程师为了扩容,重新部署了 Kong 节点并试图从配置文件恢复,结果发现配置对不上,导致限流失效,后端服务被瞬时流量打垮。
通过 DecK,我们将 Kong 的配置视为代码。上面的 YAML 文件中,我定义了 canary 插件。原因在于,单纯的权重调整不够灵活,我们还需要支持内部人员通过 X-Canary: true 请求头强制访问新版本。这种组合策略在声明式配置中一目了然,且可回溯。
在执行 deck sync 时,DecK 会计算本地文件与 Kong 当前配置的 diff,并自动应用变更。我们在 GitLab CI 中集成了如下脚本:
这种方式让我们在最近一次大促中,仅用 10 分钟就完成了从 5% 到 50% 的灰度流量切换,且没有出现一次配置错误。
随着业务复杂度的提升,通用的 Lua 插件有时无法满足我们对性能和特定业务逻辑的需求。比如,我们需要对接公司内部自研的风控系统,该系统提供了 Go SDK,且涉及复杂的加密运算。虽然 Kong 3.6 原生支持 Lua,但在这种 CPU 密集型场景下,LuaJIT 的性能不如 Go。因此,我尝试了 Kong 3.6 的 Go Plugin Server 机制,开发了一个自定义 Go 插件。
Kong 的插件执行链(Plugin Chain)是其核心设计。理解其执行顺序对于排查问题至关重要。Kong 的插件执行分为两个阶段:rewrite/access 阶段(请求到达时)和 header_filter/body_filter 阶段(响应返回时)。插件的优先级由插件定义中的 PRIORITY 字段决定,数值越大越先执行。
自定义 Go 插件开发实例:
我开发了一个名为 go-risk-control 的插件,用于在请求到达后端前调用风控接口。以下是核心代码结构,基于 Kong 3.6 的 Go PDK 接口:
执行优先级与源码逻辑:
在 Kong 的源码中(主要是 kong/runloop/handler.lua),插件执行顺序的确定逻辑如下:
PRIORITY。PRIORITY 降序排列插件列表。access 方法。我在这个 Go 插件中设置了 PRIORITY 为 900。原因在于,风控检查必须发生在 key-auth 认证之后(认证插件优先级通常在 1000+),否则未认证的用户也会触发风控调用,浪费资源。如果不这么做,会导致无效流量穿透到风控系统,甚至因为无法获取用户 ID 而报错。
遇到的实际问题:Go 插件加载失败
在部署这个 Go 插件时,我遇到了 plugin server failed to start 的错误。排查后发现,Kong 3.6 要求 Go 插件必须编译为特定的 .so 文件,且 Kong 配置中的 go_plugin_server 路径必须指向正确的 go-pluginserver 二进制文件。
解决方案是调整 kong.conf 配置:
并通过以下命令编译插件:
这次开发经历让我深刻理解了 Kong 的扩展性。虽然 Lua 插件开发更快,但在需要复用现有 Go 库或进行高性能计算时,Go 插件是更优的选择。不过,Go 插件的调试相对困难,且会增加 Kong 进程的内存占用(每个插件实例独立),这是需要在架构设计时权衡的。
去年我们团队将支付核心链路的网关从 Kong 2.8 升级到 Kong 3.6(2024年4月发布的最新稳定版),最大的架构调整就是全面切到 DB-less 模式。在此之前,我们一直使用 PostgreSQL 作为 Kong 的配置存储,但在一次大促前的压测中,数据库成为了明显的瓶颈。当时 QPS 达到 8000 时,PostgreSQL 的写入延迟抖动导致 Admin API 响应变慢,进而影响了新路由的生效速度。
我决定彻底去掉数据库依赖。原因在于 Kong 的 DB-less 模式直接将配置加载到内存中,通过 kong.conf 指定一个 kong.yml 声明式配置文件,配合 DecK 工具进行 GitOps 管理。这种方式消除了网络 I/O 对配置读取的影响,也避免了数据库单点故障的风险。
在迁移初期,我遇到了一个棘手的问题。我们在旧环境通过 Admin API 动态添加了很多临时路由和插件,这些配置并没有同步到 Git 仓库。当我第一次在测试环境应用 DB-less 配置时,Kong 启动后直接清空了内存中原有的动态配置。
解决方案是 先使用 DecK 的 dump 命令将现有数据库中的配置导出,再转换为 DB-less 兼容的格式。
在 DB-less 模式下,所有的配置变更都必须通过修改 kong.yml 文件完成。我们构建了一个 CI/CD 流水线:当 kong.yml 合并到主分支时,自动执行 deck sync。
以下是一个我们在生产环境实际使用的 kong.yml 片段,包含了一个订单服务的路由和限流配置:
不这么做会怎样? 如果继续沿用数据库模式,每次发布新服务或调整路由,都需要通过 API 调用,这在 Kubernetes 环境中很难做到版本控制和回滚。一旦 PostgreSQL 挂掉,Kong 虽然能继续转发流量(因为配置在内存里),但无法更新任何配置,且重启后配置会丢失(取决于缓存策略)。
随着团队开始接入大语言模型(LLM)处理智能客服业务,我们遇到了新的挑战。LLM 的调用成本是按 Token 计算的,且响应延迟极高(通常在 2-5 秒)。如果直接让前端调用模型接口,不仅无法控制成本,还容易因为用户频繁刷新导致 API 费用失控。
Kong 3.6 引入了对 AI Gateway 场景的深度支持,这正是我目前项目急需的。我尝试利用 Kong 的插件机制来实现 Token 限流和语义缓存。
传统的 Rate Limiting 插件是基于请求次数(Request Count)的,这对于 LLM 场景并不公平,因为有的请求可能只有 100 个 Token,有的则高达 4000 个 Token。我需要基于 Token 数量进行计费和控制。
在 Kong 3.6 中,虽然没有原生的 "Token Rate Limit" 官方插件,但我们可以利用 pre-function 插件结合 Lua 脚本来实现。我们在请求转发给上游(LLM Provider)之前,解析请求体中的 max_tokens 参数,并累加计数。
LLM 响应慢且贵,对于相同的问题,如果每次都去调模型就是巨大的浪费。我利用 Kong 的 response-transformer 和 redis 插件构建了一个简单的语义缓存层。
解决方案是 对请求的问题文本进行 Hash(或者使用向量相似度,但 Kong 层做向量计算太重,我们退而求其次使用精确匹配+参数归一化)。
我们在 kong.yml 中配置了如下逻辑:
为什么这么做? 我们之前测试过直接调用 OpenAI 的 API,一个 2000 Token 的对话请求耗时约 2.8 秒,成本约 $0.06。接入 Kong 缓存后,对于重复问题,响应时间降至 50ms 以内,且成本为 0。
不这么做会怎样? 如果没有网关层的缓存,前端应用需要自己实现缓存逻辑,这会导致代码耦合度极高,且无法跨应用共享缓存。同时,缺乏统一的 Token 限流,某次运营活动导致流量突增,直接让我们的 LLM 账单在一天内翻了三倍。
这是我在去年 10 月份遇到的一次真实事故。我们的 Kong 3.6 集群部署在 4 核 8G 的 AWS EC2 上,平时内存占用稳定在 400MB 左右。但在某天下午,监控告警显示其中一个节点的内存使用率飙升到了 95%,随后进程被 OOM Killer 强制杀掉。
我立刻登录服务器查看。Kong 是基于 OpenResty 的,底层是 Nginx 和 LuaJIT。既然进程挂了,说明 LuaJIT 的 GC(垃圾回收)没有及时回收内存,或者是有 C 级别的内存泄漏。
我首先怀疑是 lua_shared_dict 配置不当。我们在 Kong 中使用了大量的共享内存字典来存储临时认证票据。
我通过 nginx.pid 查看了 Nginx 的内存映射,发现 auth_cache 确实占用了接近 100MB,但这在预期之内。
原因在于 我写的一个自定义 Lua 插件中,使用了 table.insert 往一个全局的 Table 里无限追加数据,且没有清理机制。LuaJIT 的 GC 对于这种循环引用的 Table 回收效率并不高,尤其是在内存压力大时。
为了定位具体的泄漏点,我启用了 OpenResty 的 stapxx 工具集(SystemTap),但在生产环境编译内核模块风险太大。我转而使用了 resty-cli 结合 collectgarbage("count") 来打印实时内存。
排查结果显示,我在处理 gRPC 转码(Transcoding)时,为了兼容旧客户端,在 Lua 里构建了一个巨大的 JSON 字符串。LuaJIT 在默认模式下(非 GC64),字符串和 Table 的内存上限受限于 32 位地址空间,虽然 Kong 3.6 默认启用了 GC64(支持 64 位 GC),但我的代码逻辑导致了大量的短生命周期字符串拼接。
解决方案是 优化字符串拼接方式,使用 table.concat 代替 .. 操作符,并显式地将不再使用的大 Table 设置为 nil,辅助 GC 回收。
不这么做会怎样? 如果不进行优化,LuaJIT 的 GC 会频繁触发,导致 CPU 飙升,同时内存碎片增加。在我们的场景下,如果不修复这个拼接逻辑,每处理 1 万个请求,内存就会增长约 200MB,最终必然导致 OOM。经过优化后,单节点内存稳定在 450MB 左右,GC 暂停时间从平均 15ms 降低到了 2ms。
去年双十一前,我负责一个电商中台的网关重构。当时业务量暴涨,我们决定从 Nginx 切换到 Kong 3.6,主要看中了它的插件生态和动态配置能力。
业务场景很简单:订单、库存、支付三个核心服务需要统一鉴权和限流。我用了 DecK 做声明式配置,把三套服务的路由、上游、插件全部写进 Git。一开始跑得很顺,压测 8000 QPS 稳稳的。
但问题来了。上线第三天,凌晨两点,我收到告警:网关 OOM,服务不可达。我连上服务器,发现 OpenResty 进程内存一直在涨,直到被系统杀掉。
我第一反应是 Lua 代码写错了。翻遍了自定义的鉴权插件,没看出问题。后来用 resty-cli 抓内存快照,才发现是我在插件里用了个全局的 ngx.ctx 缓存,每次请求都往里塞数据,却没清理。LuaJIT 的 GC 根本回收不了这种“伪泄漏”。
最后我重构了插件逻辑,把缓存改成局部变量,内存曲线立刻平稳了。那次之后,我对 Kong 的插件开发有了阴影——Lua 写起来爽,但内存问题真的能要命。
* 适合上的场景:多团队、多服务、需要统一治理的中大型系统。Kong 的插件链能让你少写很多重复代码。
* 没必要上的场景:单体应用、QPS 不到 1000 的小项目。直接用 Nginx 加几个 Lua 脚本就够了,别为了用而用。
* 选型坑:别迷信 DB-less 模式。虽然它去除了数据库依赖,但配置复杂度和调试难度会成倍增加。除非你真的有极强的 GitOps 流程,否则还是老老实实带数据库吧。
学 Kong 别只看文档,去跑一遍 DecK 的同步流程,去写一个会报错的 Go 插件。网关是流量的咽喉,你只有在它出问题的时候,才会真正理解它的每一个细节。别怕麻烦,多折腾几次,你就不再是“会用”,而是“懂了”。