Read OSS

请求解剖: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 行,依次完成以下工作:

  1. 路由执行router:exec(ctx) 将请求与所有已配置的 Route 进行匹配,返回包含匹配 Route、Service、上游 URL 及 URI 转换信息的 match_t 表。
  2. 工作空间赋值ctx.workspace = match_t.route.ws_id
  3. X-Forwarded- 设置*:可信 IP 检测与转发请求头传播
  4. 协议校验:HTTPS 重定向、gRPC/HTTP2 合法性验证
  5. 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-LatencyX-Kong-Upstream-Latencyheader_filter.after() 则设置 ViaServer 响应头。在两者之间,插件可以自由修改、添加或删除任意响应头。

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_*_STARTKONG_*_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 级配置解析机制,以及插件处理器的内部结构。