Kong Dynamic Hooks(kong.dynamic_hook):按请求粒度动态插桩网关行为的内部机制详解
【免费下载链接】kong🦍 The API and AI Gateway项目地址: https://gitcode.com/GitHub_Trending/ko/kong
Kong 内置的 dynamic hooks 模块为网关提供了一套"按需激活"的动态插桩能力:既可以给已有函数挂接 before/after 处理器进行原地补丁,也可以注册具名 hook 在生命周期特定点被显式触发,并支持按组(group)统一启用或关闭。本文以 kong/dynamic_hook/README.md 为主体,结合 kong/dynamic_hook/init.lua 的完整实现,以及它在 Kong 请求调试(request debug / timing 模块)中的真实落地代码,讲清楚这套机制的 API、执行语义、性能设计与使用边界。
一、模块定位:面向内部使用的动态插桩
README 对该模块的定义是:Dynamic hooks 用于扩展 Kong 的行为,在请求/响应生命周期的特定阶段运行代码;它既可以给被补丁(patch)的函数挂接 "before" 与 "after" 处理器,也支持把 hook 组织成组(group),从而成批地启用/禁用一组 hook。文档同时明确了两条边界:
- 该模块仅面向内部使用(intended solely for internal use),外部使用需自行承担风险;
- 模块提供三种核心操作:定义 hook(
hook_function/hook)、启用组(enable_by_default/enable_on_this_request)、执行 hook(run_hook)。
从 kong/dynamic_hook/init.lua 的源码结构看,模块内部用两张表承载全部状态:
| 内部表 | 作用 |
|---|---|
NON_FUNCTION_HOOKS | 具名 hook 注册表,结构为[group_name][hook_name] = <function>,供run_hook显式触发 |
ALWAYS_ENABLED_GROUPS | 全局"默认启用"组名单,由enable_by_default写入 |
而每个请求内的启用状态则存放在ngx.ctx.dynamic_hook.enabled_groups中(见下文enable_on_this_request),这正是"按请求粒度启停"的实现基础。
二、API 参考:五个公开函数
1.hook_function(group_name, parent, function_key, max_args, handlers)—— 给函数打补丁
签名与参数(对应 init.lua#L179-L198):
| 参数 | 类型 | 说明 |
|---|---|---|
group_name | string | 所属 hook 组名 |
parent | table | 存放目标函数的表 |
function_key | string | 目标函数在parent中的键名 |
max_args | number 或"varargs" | 函数最多接受的参数个数(0–8),或字符串"varargs"表示变参 |
handlers | table | 可含befores(原函数调用前执行的处理器数组)与afters(调用后执行的处理器数组) |
函数会assert校验各参数类型,并确认parent[function_key]确实是 function,然后把原函数替换为包装版本:
max_args == "varargs"时走wrap_function_vararg(init.lua#L82-L91),用...透传全部实参;- 否则走
wrap_function(init.lua#L140-L155),把参数固化为a1..a8八个命名变量再按max_args精确转发——这是该模块一个值得注意的性能设计:对高频调用的热路径函数(如 DNS 查询、HTTP 连接),避免每次调用都构造变参表。
2.hook(group_name, hook_name, handler)—— 注册具名 hook
把handler注册到NON_FUNCTION_HOOKS[group_name][hook_name](init.lua#L209-L221)。它不会主动执行,只有在组已启用且有人调用run_hook(group_name, hook_name, ...)时才触发,因此调用方可以精确控制触发时机。
3.run_hook(group_name, hook_name, a1..a8, ...)—— 触发具名 hook
执行流程(init.lua#L268-L297):
- 先查
is_group_enabled(group_name),组未启用直接返回——未启用时run_hook只有一次判断开销; - 从注册表取出 handler,不存在则直接返回;
- 用
pcall调用 handler,参数最多 8 个命名参数 + 剩余变参; - handler 抛错只记录 WARN 日志(
failed to run dynamic hook ...),不会向调用方传播异常,保证插桩代码永远不会破坏网关主流程。
4.enable_on_this_request(group_name, ngx_ctx)—— 仅对当前请求启用
把组标记写入ngx_ctx.dynamic_hook.enabled_groups[group_name] = true;若该请求还没有此结构则初始化(init.lua#L305-L318)。注意第二个参数可以显式传入ngx_ctx(而非默认的ngx.ctx),便于在拿到请求上下文的早期阶段预先登记。
5.enable_by_default(group_name)/disable_by_default(group_name)—— 全局默认启停
直接增删ALWAYS_ENABLED_GROUPS表项(init.lua#L325-L340)。默认启用的组对所有请求生效,且被补丁函数在该组上走完整插桩路径;disable_by_default用于撤销。
6.is_group_enabled(group_name)—— 查询组状态
判定优先级(init.lua#L231-L253):
- 在
ALWAYS_ENABLED_GROUPS中 →true; - 不在 HTTP 请求上下文(
get_request()为假,如定时器/init 阶段)→false; ngx.ctx.dynamic_hook.enabled_groups[group_name]为真 →true,否则false。
三、执行语义:未启用时的零开销快路径
被hook_function补丁后的包装函数并非无条件执行 hook。核心判定函数是should_execute_original_func(group_name)(init.lua#L31-L50),满足以下任一条件时直接调用原函数、跳过所有 hook:
- 组在
ALWAYS_ENABLED_GROUPS之外的常规路径下未对当前请求启用; - 处于
init/init_worker阶段(无请求上下文)。
只有当组已启用时才依次执行befores→ 原函数 →afters,其中afters接收的是原函数的返回值(varargs版在 init.lua#L76-L79 中return ...透传返回值)。这套"默认直通、按需插桩"的语义,使得补丁函数可以被全局打补丁而不影响未启用组的请求性能。
README 中的完整示例
README 给出的两段示例完整展示了两种 hook 模式的用法,原样保留如下:
local dynamic_hook = require "kong.dynamic_hook" ---------------------------------------- -- Define a hook handler local function before_hook(...) io.write("hello, ") end -- Hook a function dynamic_hook.hook_function("my_group", _G, "print", "varargs", { befores = { before_hook }, }) -- Enable the hook group dynamic_hook.enable_by_default("my_group") -- Call the function print("world!") -- prints "hello, world!" ---------------------------------------- -- Define another hook handler local function log_event_hook(arg1, arg2) ngx.log(ngx.INFO, "event triggered with args: ", arg1, ", ", arg2) end -- Register a new hook dynamic_hook.hook("event_group", "log_event", log_event_hook) -- Enable the hook group for this request dynamic_hook.enable_on_this_request("event_group") -- Run the hook dynamic_hook.run_hook("event_group", "log_event", 10, "test")第一段演示hook_function+enable_by_default的"函数补丁"模式(全局启用);第二段演示hook+enable_on_this_request+run_hook的"具名 hook"模式(仅当前请求生效)。
四、Kong 中的真实应用:timing 模块与请求调试功能
README 指出:Kong Gateway 定义、注册并运行了一批 dynamic hooks,全部服务于timing 模块,且在请求调试(request debugging)功能启用时才实际工作。以下两张表完整继承自 README。
4.1 已注册的 timing hooks 及其触发位置
| Hook | 说明 | 运行位置 |
|---|---|---|
| timing:auth - auth | 对满足条件的请求开启请求调试(Timing 模块) | Kong.rewrite(开头) |
| timing - before:rewrite | 进入 "rewrite" 上下文,开始计时 rewrite 阶段 | Kong.rewrite(开头) |
| timing - after:rewrite | 离开 "rewrite" 上下文,结束计时 | Kong.rewrite(结尾) |
| timing - dns:cache_lookup | 设置cache_hit上下文属性 | 每次内存 DNS 缓存查询时 |
| timing - before:balancer | 进入 "balancer" 上下文,开始计时 | Kong.balancer(开头) |
| timing - after:balancer | 离开 "balancer" 上下文 | Kong.balancer(结尾) |
| timing - before:access | 进入 "access" 上下文 | Kong.access(开头) |
| timing - before:router | 进入 router 上下文 | router 初始化前 |
| timing - after:router | 离开 router 上下文 | router 执行后 |
| timing - workspace_id:got | 设置workspace_id上下文属性 | Kong.access,workspace ID 赋值之后 |
| timing - after:access | 离开 "access" 上下文 | Kong.access(结尾) |
| timing - before:response | 进入 "response" 上下文 | Kong.response(开头) |
| timing - after:response | 离开 "response" 上下文 | Kong.response(结尾) |
| timing - before:header_filter / after:header_filter | 进出 "header_filter" 上下文 | Kong.header_filter 首尾 |
| timing - before:body_filter / after:body_filter | 进出 "body_filter" 上下文 | Kong.body_filter 首尾 |
| timing - before:log / after:log | 进出 "log" 上下文 | Kong.log 首尾 |
| timing - before:plugin_iterator / after:plugin_iterator | 进出 "plugins" 上下文 | 插件迭代开始/结束 |
| timing - before:plugin / after:plugin | 进出每个插件的上下文 | 每个插件 handler 前后 |
这些 hook 的注册集中在 kong/timing/init.lua 的register_hooks(init.lua#L229-L327),例如:
req_dyn_hook.hook("timing", "before:rewrite", function() _M.enter_context("rewrite") end) req_dyn_hook.hook("timing", "after:rewrite", function() _M.leave_context() -- leave rewrite end)而触发点散布在网关运行循环中:before:router/after:router/workspace_id:got在 kong/runloop/handler.lua 中被run_hook调用;before:plugin_iterator/before:plugin/after:plugin等插件计时 hook 在 kong/init.lua 的插件迭代路径中触发;dns:cache_lookup则在 DNS 缓存每次查询时被触发。
4.2 被hook_function补丁的函数
README 列出的函数补丁清单如下,均可在源码中逐一对应:
| 函数 | 说明 | 源码位置 |
|---|---|---|
resty.dns.client.toip | 测量 DNS 查询耗时(max_args = 4) | kong/timing/hooks/dns.lua,补丁对象为kong/resty/dns/client.lua的toip |
resty.http.connect | 测量 HTTP 建连耗时(max_args = 4,兼容新旧两种签名) | kong/timing/hooks/http.lua |
resty.http.request | 测量 HTTP 请求耗时(max_args = 2) | kong/timing/hooks/http.lua |
resty.redis.{method} | 测量 Redis 每个方法的执行耗时("varargs",遍历resty.redis表内所有函数) | kong/timing/hooks/redis.lua |
ngx.socket.tcp | 测量 TCP 建连与 SSL 握手耗时(max_args = 0,after 钩子再补丁实例方法connect/sslhandshake) | kong/timing/hooks/socket.lua |
ngx.socket.udp | 测量 UDPsetpeername执行耗时(max_args = 0,after 钩子补丁实例方法setpeername) | kong/timing/hooks/socket.lua |
各补丁模块由 kong/timing/hooks/init.lua 按固定顺序socket → dns → http → redis注册(源码注释标明 "order matters")。一个有代表性的补丁是toip(kong/timing/hooks/dns.lua):
--[[ The `toip()` function can receive <= 4 arguments (including `self`). function toip(self, qname, port, dnsCacheOnly, try_list) --]] local client = assert(package.loaded["kong.resty.dns.client"]) req_dyn_hook.hook_function("timing", client, "toip", 4, { befores = { before_toip }, afters = { after_toip }, })其中before_toip依次enter_context("dns")、enter_context(qname)、enter_context("resolve"),after_toip对称地逐层leave_context——配合 kong/timing/context.lua 中基于time_ns()的子上下文栈,最终形成一棵按阶段/目标地址组织的耗时树。
4.3 启用链路:从配置到按请求激活
timing 功能使用 dynamic hooks 的两个组,恰好覆盖了两种启用方式:
timing:auth(默认启用):kong/timing/init.lua 的init_worker中,当配置启用且子系统为http时调用req_dyn_hook.enable_by_default("timing:auth"),使鉴权 hook 对每个请求的 rewrite 阶段开头无条件触发;timing(按请求启用):authhook 内部检查请求头,仅当X-Kong-Request-Debug等于"*"、且来源为回环地址(IPv4127.0.0.0/8/ IPv6::1)或携带正确X-Kong-Request-Debug-Token时,才调用req_dyn_hook.enable_on_this_request("timing", ngx_ctx)(init.lua#L68-L114)——这正体现了"按请求粒度启停"的设计目的:调试开销只落在被显式标记的请求上。
对应的用户侧配置见 kong.conf.default:request_debug = on开启功能,request_debug_token缺省随机生成并写入{prefix}/.request_debug_token;非回环来源必须携带X-Kong-Request-Debug-Token头。启用后,请求在 header_filter 阶段会收到X-Kong-Request-Debug-Output响应头(超 2KB 截断并强制转记日志),log 阶段则把完整 JSON 分片写入 error_log(kong/timing/init.lua)。
五、使用要点与边界
- 仅内部 API:README 明确该特性仅供 Kong 内部使用,外部依赖需自担风险,其行为不承诺向后兼容;
- 错误隔离:所有 hook handler 与具名 hook 均通过
pcall执行,失败只产生 WARN 日志,绝不影响被补丁函数或网关请求的返回; - 性能语义:未启用组走直通快路径(无变参包装、无 hook 调用);
run_hook未启用时仅一次组状态判断;固定参数包装(a1..a8)专为热路径函数设计; - 两种模式选型:需要"函数一被调用就插桩"(且无法在调用点插入代码)时用
hook_function;需要"在精确时机由调用方触发"时用hook+run_hook,后者还能把调用现场参数传给 handler; - 组是启停单位:无论哪种模式,handler 都归属 group,通过
enable_on_this_request/enable_by_default统一管理,这与 timing 模块"auth 组全局开、timing 组按请求开"的实践一致。
理解kong/dynamic_hook是读懂 Kong 请求调试(X-Kong-Request-Debug)实现的关键一把钥匙:网关如何在几乎零开销的前提下,为任意一条被标记的请求织入覆盖 rewrite、router、balancer、插件、DNS、HTTP/Redis/socket 建连的完整耗时观测,答案就在上述 API 与kong/timing/的注册代码中。
【免费下载链接】kong🦍 The API and AI Gateway项目地址: https://gitcode.com/GitHub_Trending/ko/kong
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考