为什么选Kong 3.6?百万日活社区网关选型与压测实录

前年我们团队接手了一个日活超过 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.yml (DB-less 模式声明式配置) _format_version: "3.0" _transform: true services: - name: community-api url: http://backend-service:8080 routes: - name: community-route paths: - /api/v1 plugins: - name: rate-limiting config: minute: 1000 policy: redis redis_host: 10.0.1.20 redis_port: 6379 redis_timeout: 50 # 必须设置超时,防止阻塞 - name: key-auth config: key_names: - apikey

电商订单系统实战:基于DecK的GitOps声明式配置与灰度发布

在重构电商平台的订单系统时,我面临的最大挑战不是代码逻辑,而是环境配置的一致性。之前我们使用 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 规则:

# ecommerce-kong.yaml _format_version: "3.0" services: - name: order-service host: order-upstream port: 80 protocol: http routes: - name: order-route paths: - /orders plugins: - name: canary config: percentage: 5 # 初始灰度 5% 流量 upstream_host: order-v2-upstream # 基于 Header 的灰度策略,方便内部测试 rules: - header: X-Canary value: "true" percentage: 100 upstreams: - name: order-upstream targets: - target: 10.1.10.11:8080 # 订单服务 v1 实例 weight: 95 - target: 10.1.10.12:8080 # 订单服务 v2 实例 weight: 5 - name: order-v2-upstream targets: - target: 10.1.10.12:8080 weight: 100

为什么不这么做会怎样?

如果不使用声明式配置,我们可能会陷入“配置漂移”的泥潭。比如,某次紧急修复中,运维人员直接在 Admin API 上修改了限流阈值,但忘记更新文档。几天后,另一个工程师为了扩容,重新部署了 Kong 节点并试图从配置文件恢复,结果发现配置对不上,导致限流失效,后端服务被瞬时流量打垮。

通过 DecK,我们将 Kong 的配置视为代码。上面的 YAML 文件中,我定义了 canary 插件。原因在于,单纯的权重调整不够灵活,我们还需要支持内部人员通过 X-Canary: true 请求头强制访问新版本。这种组合策略在声明式配置中一目了然,且可回溯。

在执行 deck sync 时,DecK 会计算本地文件与 Kong 当前配置的 diff,并自动应用变更。我们在 GitLab CI 中集成了如下脚本:

# .gitlab-ci.yml 片段 deploy_kong: stage: deploy image: kong/deck:1.16 script: - deck ping --kong-addr http://kong-admin:8001 - deck diff --kong-addr http://kong-admin:8001 - deck sync --kong-addr http://kong-admin:8001 only: - master

这种方式让我们在最近一次大促中,仅用 10 分钟就完成了从 5% 到 50% 的灰度流量切换,且没有出现一次配置错误。

深入Kong插件链:自定义Go插件开发与执行优先级源码解析

随着业务复杂度的提升,通用的 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 接口:

package main import ( "context" "fmt" "github.com/Kong/go-pdk" "github.com/Kong/go-pdk/server" "net/http" "time" ) // Config 定义插件配置结构 type Config struct { RiskServiceURL string `json:"risk_service_url"` Timeout int `json:"timeout"` } func New() interface{} { return &Config{} } // Access 阶段执行的逻辑 func (conf Config) Access(kong *pdk.PDK) { // 1. 获取请求头中的用户ID userID, err := kong.Request.GetHeader("X-User-Id") if err != nil || userID == "" { kong.Response.Exit(http.StatusForbidden, "Missing User ID", nil) return } // 2. 调用风控服务 (简化逻辑) client := http.Client{Timeout: time.Duration(conf.Timeout) * time.Millisecond} req, _ := http.NewRequest("GET", conf.RiskServiceURL+"/check?uid="+userID, nil) resp, err := client.Do(req) if err != nil || resp.StatusCode != 200 { // 风控服务异常时,根据策略决定是否放行,这里选择阻断 kong.Response.Exit(http.StatusServiceUnavailable, "Risk Service Error", nil) return } defer resp.Body.Close() // 3. 如果风控通过,继续执行下一个插件 kong.Log.Info("Risk check passed for user: ", userID) } func main() { // 插件优先级设置为 900,确保在认证插件之后执行 server.StartServer(New, "0.1", 900) }

执行优先级与源码逻辑:

在 Kong 的源码中(主要是 kong/runloop/handler.lua),插件执行顺序的确定逻辑如下:

我在这个 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.conf 关键配置 go_plugin_server = /usr/local/bin/go-pluginserver plugins = bundled,go-risk-control # 必须显式声明加载自定义插件

并通过以下命令编译插件:

# 编译 Go 插件为共享库 go build -buildmode plugin -o go-risk-control.so main.go

这次开发经历让我深刻理解了 Kong 的扩展性。虽然 Lua 插件开发更快,但在需要复用现有 Go 库或进行高性能计算时,Go 插件是更优的选择。不过,Go 插件的调试相对困难,且会增加 Kong 进程的内存占用(每个插件实例独立),这是需要在架构设计时权衡的。

4. 生产环境DB-less模式部署:配置中心化与去数据库化踩坑记录

去年我们团队将支付核心链路的网关从 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 兼容的格式。

# 导出当前数据库中的全量配置 deck dump --kong-addr http://127.0.0.1:8001 --output-file kong-db-export.yaml # 检查导出文件,清理掉测试环境遗留的脏数据 # 然后将清理后的配置应用到 DB-less 环境 deck sync --kong-addr http://new-kong-admin:8001 --state kong-db-export.yaml

配置即代码的实战细节

在 DB-less 模式下,所有的配置变更都必须通过修改 kong.yml 文件完成。我们构建了一个 CI/CD 流水线:当 kong.yml 合并到主分支时,自动执行 deck sync

以下是一个我们在生产环境实际使用的 kong.yml 片段,包含了一个订单服务的路由和限流配置:

_format_version: "3.0" transform: - name: kong-core config: version: "3.6" services: - name: order-service url: http://order-service.prod.svc.cluster.local:8080 routes: - name: order-create-route paths: - /api/v1/orders methods: - POST - GET # 正则匹配路径参数 regex_priority: 100 plugins: - name: rate-limiting config: minute: 5000 policy: local # 我们在生产环境发现,如果 policy 设为 redis,在 DB-less 模式下虽然能用,但会增加额外的网络开销 # 对于单体应用的限流,local 策略足够且性能更好 - name: key-auth config: key_names: - apikey # 禁用消费者缓存,因为 DB-less 模式下没有数据库查询,缓存意义不大 hide_credentials: true

不这么做会怎样? 如果继续沿用数据库模式,每次发布新服务或调整路由,都需要通过 API 调用,这在 Kubernetes 环境中很难做到版本控制和回滚。一旦 PostgreSQL 挂掉,Kong 虽然能继续转发流量(因为配置在内存里),但无法更新任何配置,且重启后配置会丢失(取决于缓存策略)。

5. Kong 3.6新特性:构建AI Gateway实现LLM Token限流与语义缓存

随着团队开始接入大语言模型(LLM)处理智能客服业务,我们遇到了新的挑战。LLM 的调用成本是按 Token 计算的,且响应延迟极高(通常在 2-5 秒)。如果直接让前端调用模型接口,不仅无法控制成本,还容易因为用户频繁刷新导致 API 费用失控。

Kong 3.6 引入了对 AI Gateway 场景的深度支持,这正是我目前项目急需的。我尝试利用 Kong 的插件机制来实现 Token 限流和语义缓存。

实现基于 Token 的限流

传统的 Rate Limiting 插件是基于请求次数(Request Count)的,这对于 LLM 场景并不公平,因为有的请求可能只有 100 个 Token,有的则高达 4000 个 Token。我需要基于 Token 数量进行计费和控制。

在 Kong 3.6 中,虽然没有原生的 "Token Rate Limit" 官方插件,但我们可以利用 pre-function 插件结合 Lua 脚本来实现。我们在请求转发给上游(LLM Provider)之前,解析请求体中的 max_tokens 参数,并累加计数。

-- 在 pre-function 插件中执行的 Lua 代码片段 -- 注意:这是简化逻辑,生产环境需要处理更复杂的 JSON 解析和并发锁 local json = require "cjson" local redis = require "resty.redis" local red = redis:new() red:connect("127.0.0.1", 6379) local body = kong.request.get_body() local requested_tokens = body.max_tokens or 0 local user_id = kong.request.get_header("X-User-Id") -- 检查用户剩余 Token 配额 local quota_key = "llm:quota:" .. user_id local current_usage = red:get(quota_key) or 0 if tonumber(current_usage) + requested_tokens > 100000 then kong.response.exit(429, { message = "Token quota exceeded for today" }) end -- 记录预消耗(实际应在响应后根据 usage 修正,此处为简化演示) red:incrby(quota_key, requested_tokens)

语义缓存(Semantic Caching)的落地

LLM 响应慢且贵,对于相同的问题,如果每次都去调模型就是巨大的浪费。我利用 Kong 的 response-transformerredis 插件构建了一个简单的语义缓存层。

解决方案是 对请求的问题文本进行 Hash(或者使用向量相似度,但 Kong 层做向量计算太重,我们退而求其次使用精确匹配+参数归一化)。

我们在 kong.yml 中配置了如下逻辑:

plugins: - name: proxy-cache config: content_type: - application/json cache_ttl: 3600 # 根据请求体中的 prompt 生成缓存 Key cache_key: - "@request.body.prompt" - "@request.body.model" # 我们在实际测试中发现,如果不对 prompt 做 trim 和标准化,同一个问题 "A" 和 "A " 会被视为不同的 Key # 因此我们在上游服务或插件中需要对 prompt 进行预处理

为什么这么做? 我们之前测试过直接调用 OpenAI 的 API,一个 2000 Token 的对话请求耗时约 2.8 秒,成本约 $0.06。接入 Kong 缓存后,对于重复问题,响应时间降至 50ms 以内,且成本为 0。

不这么做会怎样? 如果没有网关层的缓存,前端应用需要自己实现缓存逻辑,这会导致代码耦合度极高,且无法跨应用共享缓存。同时,缺乏统一的 Token 限流,某次运营活动导致流量突增,直接让我们的 LLM 账单在一天内翻了三倍。

6. 线上OOM排查实录:OpenResty内存泄漏定位与LuaJIT优化技巧

这是我在去年 10 月份遇到的一次真实事故。我们的 Kong 3.6 集群部署在 4 核 8G 的 AWS EC2 上,平时内存占用稳定在 400MB 左右。但在某天下午,监控告警显示其中一个节点的内存使用率飙升到了 95%,随后进程被 OOM Killer 强制杀掉。

我立刻登录服务器查看。Kong 是基于 OpenResty 的,底层是 Nginx 和 LuaJIT。既然进程挂了,说明 LuaJIT 的 GC(垃圾回收)没有及时回收内存,或者是有 C 级别的内存泄漏。

排查过程:从 `lua_shared_dict` 到 GC64

我首先怀疑是 lua_shared_dict 配置不当。我们在 Kong 中使用了大量的共享内存字典来存储临时认证票据。

# kong.conf 片段 nginx_proxy_lua_shared_dicts = { auth_cache: 100m, rate_limit: 50m }

我通过 nginx.pid 查看了 Nginx 的内存映射,发现 auth_cache 确实占用了接近 100MB,但这在预期之内。

原因在于 我写的一个自定义 Lua 插件中,使用了 table.insert 往一个全局的 Table 里无限追加数据,且没有清理机制。LuaJIT 的 GC 对于这种循环引用的 Table 回收效率并不高,尤其是在内存压力大时。

为了定位具体的泄漏点,我启用了 OpenResty 的 stapxx 工具集(SystemTap),但在生产环境编译内核模块风险太大。我转而使用了 resty-cli 结合 collectgarbage("count") 来打印实时内存。

-- 在插件的 rewrite 阶段插入调试代码 local function log_memory_usage() local mem_kb = collectgarbage("count") if mem_kb > 500 * 1024 then -- 超过 500MB 打印警告 kong.log.err("High memory usage detected: ", mem_kb, " KB") -- 打印堆栈信息,帮助定位是哪个逻辑在分配内存 kong.log.err(debug.traceback()) end end -- 在 access_by_lua 阶段调用 log_memory_usage()

排查结果显示,我在处理 gRPC 转码(Transcoding)时,为了兼容旧客户端,在 Lua 里构建了一个巨大的 JSON 字符串。LuaJIT 在默认模式下(非 GC64),字符串和 Table 的内存上限受限于 32 位地址空间,虽然 Kong 3.6 默认启用了 GC64(支持 64 位 GC),但我的代码逻辑导致了大量的短生命周期字符串拼接。

解决方案是 优化字符串拼接方式,使用 table.concat 代替 .. 操作符,并显式地将不再使用的大 Table 设置为 nil,辅助 GC 回收。

-- 优化前:使用 .. 拼接,每次都会生成新的字符串对象 local result = "" for i = 1, #large_array do result = result .. large_array[i] -- 内存分配频繁 end -- 优化后:使用 table.concat local temp_tb = {} for i = 1, #large_array do temp_tb[#temp_tb + 1] = large_array[i] end local result = table.concat(temp_tb) temp_tb = nil -- 显式释放引用

不这么做会怎样? 如果不进行优化,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 插件。网关是流量的咽喉,你只有在它出问题的时候,才会真正理解它的每一个细节。别怕麻烦,多折腾几次,你就不再是“会用”,而是“懂了”。