请求解剖:Kong 从 Rewrite 到 Log 的运行循环全解析
前置知识
- ›第 1 篇:架构与 Nginx 集成
- ›第 2 篇:启动与初始化
- ›理解 Nginx 阶段顺序及其限制
- ›熟悉 ngx.ctx、cosocket 模型以及 ngx_lua 阶段约束
请求解剖:Kong 从 Rewrite 到 Log 的运行循环全解析
在 Kong 完成初始化、工作进程就绪之后(详见第 1 篇和第 2 篇),我们终于可以跟随一个 HTTP 请求,完整走过每一个处理阶段。这正是架构真正活起来的地方——router 匹配路由,插件完成认证与转换,balancer 选择上游目标,响应再经过过滤器和日志处理器流回客户端。
整个设计的核心模式是 before/after 三明治:在每个阶段,runloop 处理器先执行基础设施逻辑(before),接着运行插件,最后再执行清理逻辑(after)。理解这一模式,是理解 Kong 可扩展性的关键所在。
Before/After 三明治模式
kong/init.lua 中的每个请求处理阶段都遵循相同的结构:
function Kong.access()
local ctx = ngx.ctx
-- timing setup...
ctx.KONG_PHASE = PHASES.access
runloop.access.before(ctx) -- infrastructure
local plugins_iterator = runloop.get_plugins_iterator()
execute_collecting_plugins_iterator(plugins_iterator, "access", ctx) -- plugins
-- delayed_response handling...
runloop.access.after(ctx) -- infrastructure
-- timing finalization...
end
before 钩子处理那些插件无需关心的事情:路由执行、X-Forwarded-* 请求头设置、协议协商。after 钩子则负责 balancer 的准备工作以及 gRPC 的内部重定向。插件在两者之间按优先级顺序执行,并可通过 kong.response.exit() 提前中断整个处理链路。
这一模式在 runloop 处理器的返回表中定义,位于 kong/runloop/handler.lua:
return {
-- ...
init_worker = { before = function() ... end },
rewrite = { before = function(ctx) ... end },
access = { before = function(ctx) ... end, after = function(ctx) ... end },
-- ...
}
sequenceDiagram
participant Phase as Kong.<phase>()
participant Before as runloop.<phase>.before()
participant Plugins as plugins_iterator
participant After as runloop.<phase>.after()
Phase->>Phase: Set timing markers
Phase->>Before: Infrastructure logic
Before-->>Phase: Router match, context setup
Phase->>Plugins: Iterate in priority order
Plugins->>Plugins: Plugin 1 (PRIORITY=1250)
Plugins->>Plugins: Plugin 2 (PRIORITY=910)
Plugins->>Plugins: Plugin N (PRIORITY=...)
Plugins-->>Phase: All plugins executed
Phase->>After: Cleanup, balancer prep
After-->>Phase: Ready for next phase
Phase->>Phase: Finalize timing
Rewrite 与 Access:路由匹配与服务解析
rewrite 阶段(Kong.rewrite())相对轻量。它负责初始化请求上下文、存储上下文引用以供跨阶段访问、设置工作空间,并仅执行全局作用域的插件(即未绑定到任何特定路由、服务或消费者的插件)。runloop 的 rewrite.before() 位于 第 1177 行,负责捕获服务器端口并初始化链路追踪。
需要注意的是,rewrite 阶段使用的是 execute_global_plugins_iterator,而非 collecting 迭代器。原因在于:此时路由匹配尚未发生,Kong 还无法确定哪些路由或服务级别的插件应当生效。
access 阶段(Kong.access())才是真正的核心所在。runloop 的 access.before() 位于 第 1184–1404 行,依次完成以下工作:
- 路由执行:
router:exec(ctx)将请求与所有已配置的 Route 进行匹配,返回包含匹配 Route、Service、上游 URL 及 URI 转换信息的match_t表。 - 工作空间赋值:
ctx.workspace = match_t.route.ws_id - X-Forwarded- 设置*:可信 IP 检测与转发请求头传播
- 协议校验:HTTPS 重定向、gRPC/HTTP2 合法性验证
- Balancer 准备:位于 第 805–856 行 的
balancer_prepare()创建balancer_data表,其中包含重试次数、超时配置以及上游的 host/port 信息。
access.before() 执行完成后,插件通过 collecting 迭代器依次运行(详见第 4 篇)。若某个插件调用了 kong.response.exit(403),请求将通过 delayed response 机制被提前中断。
flowchart TD
A[access.before] --> B[router:exec]
B --> C{Match found?}
C -->|No| D[404 No Route matched]
C -->|Yes| E[Set workspace]
E --> F[Parse X-Forwarded-* headers]
F --> G{HTTPS required?}
G -->|Yes, HTTP request| H[Redirect/426]
G -->|No| I[balancer_prepare]
I --> J[Set upstream vars]
J --> K{gRPC service?}
K -->|Yes| L[Internal redirect @grpc]
K -->|No| M[Continue to plugins]
提示:
balancer_prepare中创建的ctx.balancer_data表是整个请求生命周期中最重要的数据结构之一。它携带了 scheme、host、port、重试次数、超时配置,以及记录每次 balancer 尝试信息的tries数组。插件可以在 balancer 执行之前修改这些值。
Balancer 阶段:上游选择与重试机制
Kong.balancer() 在 Kong 的所有阶段中独具特殊性:它在单次请求中可能被多次调用(每次上游尝试触发一次),且运行于严格的 Nginx 限制之下——不允许 yield,也不允许 cosocket I/O。
Nginx 的 proxy_pass 触发 balancer_by_lua_block 后进入 balancer 阶段。首次尝试时,通过第 1380 行的 set_more_tries(retries) 设置最大重试次数。后续的每次重试,则会记录上一次的失败信息,并重新执行 balancer:
if try_count > 1 then
local previous_try = tries[try_count - 1]
previous_try.state, previous_try.code = get_last_failure()
-- report to health checker ...
local ok, err, errcode = balancer.execute(balancer_data, ctx)
-- ...
end
kong/runloop/balancer/init.lua 中的 balancer.execute() 负责 DNS 解析和目标选择。对于使用了包含多个目标的 Upstream 实体的服务,它会应用已配置的负载均衡算法(轮询、一致性哈希、最少连接)。
连接保活(keepalive)的管理逻辑位于 第 1388–1451 行。keepalive 连接池的 key 由 IP、端口、SNI、TLS 验证设置和客户端证书共同组成,确保只有在所有安全参数完全一致时才会复用连接:
pool = fmt("%s|%s|%s|%s|%s|%s|%s",
balancer_data_ip, balancer_data_port,
var.upstream_host,
service.tls_verify and "1" or "0",
service.tls_verify_depth or "",
service.ca_certificates and concat(service.ca_certificates, ",") or "",
service.client_certificate and service.client_certificate.id or "")
flowchart TD
A[Kong.balancer] --> B{First try?}
B -->|Yes| C[set_more_tries]
B -->|No| D[Record previous failure]
D --> E[Report to health checker]
E --> F[balancer.execute - re-resolve]
C --> G[set_current_peer IP:port]
F --> G
G --> H[set_timeouts]
H --> I{Keepalive enabled?}
I -->|Yes| J[enable_keepalive with pool key]
I -->|No| K[Skip]
J --> L[Record timing]
K --> L
响应处理:header_filter 与 body_filter
Nginx 收到上游响应后,Kong.header_filter() 开始处理响应头。这一阶段使用的是 execute_collected_plugins_iterator——它直接回放 access 阶段 collecting 迭代过程中构建好的插件列表,无需重新解析哪些插件应当生效。
runloop 的 header_filter.before() 负责添加 Kong 特有的响应头,例如 X-Kong-Proxy-Latency 和 X-Kong-Upstream-Latency;header_filter.after() 则设置 Via 和 Server 响应头。在两者之间,插件可以自由修改、添加或删除任意响应头。
Kong.body_filter() 负责处理响应体。有一个关键细节需要注意:对于分块传输的响应,这一阶段会被多次调用,每次处理一个数据块,arg[2] 标志位(eof)用于标识当前是否为最后一个数据块。
第 1716 行处理了一种特殊情况——插件设置了 ctx.response_body 时:
if ctx.response_body then
arg[1] = ctx.response_body
arg[2] = true
end
这一机制允许 ai-proxy 等插件将经过转换的完整响应体一次性输出,将多个数据块合并为单次发送。
Log 阶段与请求上下文生命周期
Kong.log() 是整个流程的最后一个阶段。此时响应已经发送给客户端——log 阶段在连接关闭之后(或至少在响应完整缓冲之后)异步执行。
贯穿 kong/init.lua 的计时埋点,在 ctx 上使用 KONG_*_START 和 KONG_*_ENDED_AT 标记记录时间。log 阶段的第 1761–1842 行负责填补其中的空缺——如果某个阶段因为发生错误而未能记录结束时间,log 阶段会进行回溯计算。几个关键的计算指标包括:
KONG_PROXY_LATENCY:从请求开始到 balancer 完成的耗时KONG_WAITING_TIME:等待上游响应的耗时KONG_RECEIVE_TIME:在 header_filter 和 body_filter 阶段花费的时间KONG_RESPONSE_LATENCY:非代理响应的总耗时
插件执行完各自的 log 处理器后,上下文在 第 1849–1856 行 被清理回收:
plugins_iterator.release(ctx)
runloop.log.after(ctx)
-- ...
release_table(CTX_NS, ctx)
release_table 会将 ctx 表归还给对象池(tablepool)以供复用,从而避免后续请求带来的 GC 压力。
sequenceDiagram
participant Access as access phase
participant Balancer as balancer phase
participant Upstream as upstream
participant HF as header_filter
participant BF as body_filter
participant Log as log phase
Note over Access: KONG_ACCESS_START
Access->>Access: plugins execute
Note over Access: KONG_ACCESS_ENDED_AT
Balancer->>Upstream: connect & send
Note over Balancer: KONG_BALANCER_ENDED_AT
Upstream-->>HF: response headers
Note over HF: KONG_WAITING_TIME = now - BALANCER_ENDED_AT
HF->>BF: headers processed
BF->>BF: body chunks
Note over BF: KONG_RECEIVE_TIME computed
BF->>Log: final chunk (eof)
Log->>Log: fill timing gaps
Log->>Log: execute plugin log handlers
Log->>Log: release ctx to tablepool
Delayed Response 机制
第 377 和 414 行 的 delayed_response 机制值得单独说明。在 collecting 阶段(access)期间,ctx.delay_response 被设置为 true。当某个插件调用 kong.response.exit() 时,Kong 不会立即发送响应,而是将退出状态码和响应体暂存到 ctx.delayed_response 中:
ctx.delayed_response = {
status_code = 403,
content = { message = "Forbidden" },
}
迭代器中后续的插件检测到 ctx.delayed_response 后会被跳过。待迭代器执行完毕,flush_delayed_response 再统一发送实际响应。这一设计确保了即使请求被提前中断,所有插件仍有机会执行其下游阶段的处理器(header_filter、body_filter、log)。
提示:
execute_collecting_plugins_iterator中的协程封装(第 399–400 行)不仅仅是为了错误处理。通过在协程中运行每个插件,Kong 能够捕获运行时错误,而不会导致整个请求处理链路崩溃。错误会被记录下来,同时通过delayed_response排队发送一个 500 响应,并跳过下一个插件。
下一步
我们已经完整追踪了请求经过每个阶段的过程,也看清了 runloop 与插件如何协同工作。但有一个关键问题我们还没有深入:对于给定的请求,究竟是哪些插件在运行,又以什么顺序执行?第 4 篇将深入插件系统,剖析 collecting/collected 迭代器模式、8 级配置解析机制,以及插件处理器的内部结构。