共计 7528 个字符,预计需要花费 19 分钟才能阅读完成。
作者罗锦华,API7.ai 技术专家 / 技术工程师,开源我的项目 pgcat,lua-resty-ffi,lua-resty-inspect 的作者。
原文链接
为什么须要 Lua 动静调试插件?
Apache APISIX 有很多 Lua 代码,如何在运行时不触碰源代码的状况下,查看代码外面的变量值?
批改 Lua 源码来调试有如下毛病:
- 生产环境不容许也不应该批改源码
- 批改源码须要 reload,使得业务性能生效
- 容器环境难以批改源码
- 产生的长期代码容易遗记回滚,导致保护问题
很多时候咱们不仅仅须要在函数开始或完结的时候去查看变量,而且须要在满足肯定条件,例如某个循环体被循环到了肯定次数,
或者某个条件判断为真的时候咱们才查看变量值,并且也不仅仅是简略打印变量值,有时候还可能须要将相干信息发送到外围零碎。
并且,这个过程如何做到动态化呢?而且,开启调试后,是否不影响程序运行的性能呢?
Lua 动静调试插件就是辅助你实现以上需要的插件,该插件被命名为 inspect
插件。
- 断点解决可定制
- 断点设置动态化
- 多个断点
- 断点可被定义为只失效一次
- 可管制性能影响范畴
插件原理
它充分利用了 Lua 提供的 Debug API 来实现性能。解释器模式执行的每一个字节码都能够对应到它所属的文件以及行号,咱们只须要判断行号是否等于期望值,而后执行咱们定义的断点函数,对该行对应的上下文信息,包含 upvalue,局部变量,还有一些元信息,例如堆栈,进行解决即可。
APISIX 应用的是 Lua 的 JIT 实现:LuaJIT,很多热点代码门路会被编译成机器码执行,而它们是不受 Debug API 的影响的,所以咱们须要在开启断点前清空 JIT 缓存。要害就在这里了,咱们能够抉择只清空某个具体 Lua 函数的 JIT 缓存,减小对全局性能的影响。一个程序运行起来,会有很多 JIT 编译代码块,在 LuaJIT 里被称为 trace,这些 trace 跟 Lua 函数是关联起来的,一个 Lua 函数可能包含多个 trace,指代函数内不同的热点门路。
对于全局函数、模块级别的函数,咱们能够指定它们的函数对象,清空它们的 JIT 缓存。然而如果某行号对应的是其余函数类型,例如匿名函数,咱们无奈在全局获取函数的对象,那么只能清空所有 JIT 缓存了。在调试开启期间,新的 trace 无奈被生成,然而已有的未被清理的 trace 还持续运行,所以只有管制的好,程序性能不会受到影响,因为一个曾经运行很久的线上零碎,根本不会有新 trace 的生成。当调试完结后,也就是所有断点都被撤销后,零碎会恢复正常的 JIT 模式,被清理掉的 JIT 缓存,一旦从新进入热点,会被从新生成 trace。
装置与配置
该插件默认被启用。
配置好 conf/confg.yaml
启用插件:
plugins:
...
- inspect
plugin_attr:
inspect:
delay: 3
hooks_file: "/usr/local/apisix/plugin_inspect_hooks.lua"
插件默认每隔 3 秒从文件 /usr/local/apisix/plugin_inspect_hooks.lua
读取断点定义,想调试就编辑该文件即可。
倡议创立软链接到该门路,这样比拟不便地存档不同历史版本的断点文件。
留神每次该文件的更改工夫有变,插件会清空所有旧的断点,并且启用断点文件所定义的所有新断点。断点将在所有工作过程失效。
个别状况下不须要删除该文件,因为定义断点的时候,能够定义什么时候撤销断点。
删除文件会勾销所有工作过程的所有断点。
断点的启停都会通过 WARN
日志级别打印日志。
定义断点
require("apisix.inspect.dbg").set_hook(file, line, func, filter_func)
file
文件名,能够是任何无歧义的文件名局部,可蕴含门路line
文件的行号,留神断点跟行号是亲密挂钩的,所以如果代码变了,行号就得跟着变。func
要革除哪个函数的 trace,如果为 nil,则革除 luajit vm 外面所有 trace-
filter_func
解决该断点的自定义 Lua 函数-
函数的入参为一个
table
,蕴含以下内容finfo
:debug.getinfo(level, "nSlf")
的返回值uv
: upvalues hash tablevals
: local variables hash table
- 函数的返回值为
true
,则该断点主动登记,返回为false
,则该断点持续失效
-
例子:
local dbg = require "apisix.inspect.dbg"
dbg.set_hook("limit-req.lua", 88, require("apisix.plugins.limit-req").access,
function(info)
ngx.log(ngx.INFO, debug.traceback("foo traceback", 3))
ngx.log(ngx.INFO, dbg.getname(info.finfo))
ngx.log(ngx.INFO, "conf_key=", info.vals.conf_key)
return true
end)
dbg.set_hook("t/lib/demo.lua", 31, require("t.lib.demo").hot2, function(info)
if info.vals.i == 222 then
ngx.timer.at(0, function(_, body)
local httpc = require("resty.http").new()
httpc:request_uri("http://127.0.0.1:9080/upstream1", {
method = "POST",
body = body,
})
end, ngx.var.request_uri .. "," .. info.vals.i)
return true
end
return false
end)
--- more breakpoints ...
留神到 demo 这个断点,它将一些信息整顿后发送到内部的服务器上,应用的 resty.http
库是基于 cosocket
的异步库。
但凡调用 OpenResty 的异步 API,必须应用 timer 提早发送,因为在断点上执行函数是同步阻塞的,不会再返回到 nginx 的主程序做异步解决,所以须要延后发送。
应用示例
依据申请体的内容来决定路由
假如咱们有个需要,如何设置让某个路由仅承受申请体中携带了 APISIX: 666
的 POST 申请?
路由配置外面有个 vars
字段,是用来查看 nginx 变量的值来判断是否匹配该路由的,
而 $request_body
则是 nginx 提供的变量,蕴含申请体的值,那咱们能够利用这个变量来实现咱们的需要?
让咱们来尝试一下,先配置一下路由:
curl http://127.0.0.1:9180/apisix/admin/routes/var_route \
-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '{"uri":"/anything","methods": ["POST"],"vars": [["request_body","~~","APISIX: 666"]],"upstream": {"type":"roundrobin","nodes": {"httpbin.org": 1}
}
}'
而后咱们尝试一下:
curl http://127.0.0.1:9080/anything
{"error_msg":"404 Route Not Found"}
curl -i http://127.0.0.1:9080/anything -X POST -d 'hello, APISIX: 666.'
HTTP/1.1 404 Not Found
Date: Thu, 05 Jan 2023 03:53:35 GMT
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Server: APISIX/3.0.0
{"error_msg":"404 Route Not Found"}
奇怪,为什么匹配不上这个路由呢?
咱们再查看一下 NGINX 对该变量的文档阐明:
The variable’s value is made available in locations processed by the proxy_pass, fastcgi_pass, uwsgi_pass, and scgi_pass directives when the request body was read to a memory buffer.
也就是说,应用该变量前须要先读取 request body。
那是不是匹配路由的时候,这个变量为空呢?咱们能够应用 inspect
插件来验证一下。
咱们找到了匹配路由的代码行:
apisix/init.lua
...
api_ctx.var.request_uri = api_ctx.var.uri .. api_ctx.var.is_args .. (api_ctx.var.args or "")
router.router_http.match(api_ctx)
local route = api_ctx.matched_route
if not route then
...
咱们就在 515 行,也就是 router.router_http.match(api_ctx)
这行验证一下变量 request_body
吧。
设置断点
编辑文件 /usr/local/apisix/example_hooks.lua
:
local dbg = require("apisix.inspect.dbg")
dbg.set_hook("apisix/init.lua", 515, require("apisix").http_access_phase, function(info)
core.log.warn("request_body=", info.vals.api_ctx.var.request_body)
return true
end)
创立软链接到断点文件门路:
ln -sf /usr/local/apisix/example_hooks.lua /usr/local/apisix/plugin_inspect_hooks.lua
查看日志看看确认断点失效:
2023/01/05 12:02:43 [warn] 1890559#1890559: *15736 [lua] init.lua:68: setup_hooks():
set hooks: err: true, hooks: ["apisix\/init.lua#515"], context: ngx.timer
再触发一次路由匹配:
curl -i http://127.0.0.1:9080/anything -X POST -d 'hello, APISIX: 666.'
查看日志:
2023/01/05 12:02:59 [warn] 1890559#1890559: *16152
[lua] [string "local dbg = require("apisix.inspect.dbg")..."]:39:
request_body=nil, client: 127.0.0.1, server: _,
request: "POST /anything HTTP/1.1", host: "127.0.0.1:9080"
果然,request_body
是空的!
解决方案
既然咱们晓得须要读取申请体能力用 request_body
变量,那么咱们就不能通过 vars
来做了,那咱们能够通过路由外面的 filter_func
字段来实现需求。
curl http://127.0.0.1:9180/apisix/admin/routes/var_route \
-H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -i -d '{"uri":"/anything","methods": ["POST"],"filter_func":"function(_) return require(\"apisix.core\").request.get_body():find(\"APISIX: 666\") end","upstream": {"type":"roundrobin","nodes": {"httpbin.org": 1}
}
}'
验证一下:
curl http://127.0.0.1:9080/anything -X POST -d 'hello, APISIX: 666.'
{"args": {},
"data": "","files": {},"form": {"hello, APISIX: 666.":""},
"headers": {
"Accept": "*/*",
"Content-Length": "19",
"Content-Type": "application/x-www-form-urlencoded",
"Host": "127.0.0.1",
"User-Agent": "curl/7.68.0",
"X-Amzn-Trace-Id": "Root=1-63b64dbd-0354b6ed19d7e3b67013592e",
"X-Forwarded-Host": "127.0.0.1"
},
"json": null,
"method": "POST",
"origin": "127.0.0.1, xxx",
"url": "http://127.0.0.1/anything"
}
问题解决!
打印一些被日志级别屏蔽的日志
生产环境个别不会开启 INFO
级别的日志,然而有时候咱们又须要查看一些详细信息,那怎么办呢?
咱们个别不会间接设置 INFO
级别而后 reload,因为这样做有两个毛病:
- 日志太多,影响性能和加大查看难度
- reload 导致长连贯被断开,影响在线流量
个别咱们只须要查看具体某个点的日志,例如咱们都晓得 APISIX 应用 etcd 作为配置散发数据库,那么可否看看什么时候路由配置被增量更新到了数据面呢?更新了什么具体数据呢?
apisix/core/config_etcd.lua
local function sync_data(self)
...
log.info("waitdir key:", self.key, "prev_index:", self.prev_index + 1)
log.info("res:", json.delay_encode(dir_res, true), ", err:", err)
...
end
增量同步的 lua 函数是 sync_data()
,然而它是通过 INFO
级别来打印从 etcd watch 到的增量数据的。
那么咱们来试一下应用 inspect plugin 来显示一下?只显示路由资源的变动。
编辑 /usr/local/apisix/example_hooks.lua
:
local dbg = require("apisix.inspect.dbg")
local core = require("apisix.core")
dbg.set_hook("apisix/core/config_etcd.lua", 393, nil, function(info)
local filter_res = "/routes"
if info.vals.self.key:sub(-#filter_res) == filter_res and not info.vals.err then
core.log.warn("etcd watch /routes response:", core.json.encode(info.vals.dir_res, true))
return true
end
return false
end)
这个断点处理函数的逻辑很好表白了过滤能力,如果 watch 的 key
是 /routes
,以及 err
为空的状况下,就打印 etcd 返回的数据,并且打印一次就够了,就勾销断点。
留神 sync_data()
是部分函数,所以无奈获取它的援用,咱们只能设置 set_hook
的第三个参数为 nil
,这样做的副作用就是它会清空所有 trace
。
下面例子咱们曾经创立了软链接,所以编辑后保留文件即可。等几秒钟后,断点就会被启用,可察看日志确认。
查看日志,咱们能够失去咱们须要的信息,而这些信息用 WARN
日志级别打印,并且也显示了咱们在数据面获取到 etcd 增量数据的工夫。
2023/01/05 14:33:10 [warn] 1890562#1890562: *231311
[lua] [string "local dbg = require("apisix.inspect.dbg")..."]:41:
etcd watch /routes response: {"headers":{"X-Etcd-Index":"24433"},
"body":{"node":[{"value":{"uri":"\/anything",
"plugins":{"request-id":{"header_name":"X-Request-Id","include_in_response":true,"algorithm":"uuid"}},
"create_time":1672898912,"status":1,"priority":0,"update_time":1672900390,
"upstream":{"nodes":{"httpbin.org":1},"hash_on":"vars","type":"roundrobin","pass_host":"pass","scheme":"http"},
"id":"reqid"},"key":"\/apisix\/routes\/reqid","modifiedIndex":24433,"createdIndex":24429}]}}, context: ngx.timer
论断
Lua 动静调试是很重要的辅助性能。咱们能够通过 APISIX inspect
插件来做很多事件,例如:
- 排查问题,定位起因
- 打印一些被屏蔽的日志,按需获取各种信息
- 通过调试来学习 Lua 代码
更多详情请查阅相干文档介绍。
对于 API7.ai 与 APISIX
API7.ai 是一家提供 API 解决和剖析的开源根底软件公司,于 2019 年开源了新一代云原生 API 网关 — APISIX 并捐献给 Apache 软件基金会。尔后,API7.ai 始终踊跃投入反对 Apache APISIX 的开发、保护和社区经营。与千万贡献者、使用者、支持者一起做出世界级的开源我的项目,是 API7.ai 致力的指标。