做网站不给提供ftp,做分析图的地图网站,响应式网站模板代码,哪里有网站建设商家作者罗锦华#xff0c;API7.ai 技术专家/技术工程师#xff0c;开源项目 pgcat#xff0c;lua-resty-ffi#xff0c;lua-resty-inspect 的作者。 原文链接
为什么需要 Lua 动态调试插件#xff1f;
Apache APISIX 有很多 Lua 代码#xff0c;如何在运行时不触碰源代码的… 作者罗锦华API7.ai 技术专家/技术工程师开源项目 pgcatlua-resty-ffilua-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:
...- inspectplugin_attr:inspect:delay: 3hooks_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 里面所有 tracefilter_func 处理该断点的自定义 Lua 函数函数的入参为一个 table包含以下内容finfo: debug.getinfo(level, nSlf)的返回值uv: upvalues hash tablevals: local variables hash table 函数的返回值为 true则该断点自动注销返回为 false则该断点继续生效
例子
local dbg require apisix.inspect.dbgdbg.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 thenngx.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 trueendreturn 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; charsetutf-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_bodynil, 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: Root1-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 /routesif info.vals.self.key:sub(-#filter_res) filter_res and not info.vals.err thencore.log.warn(etcd watch /routes response: , core.json.encode(info.vals.dir_res, true))return trueendreturn 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 努力的目标。