详解 APISIX Lua 动态调试插件 inspect

这篇具有很好参考价值的文章主要介绍了详解 APISIX Lua 动态调试插件 inspect。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

作者罗锦华,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 table
      • vals: 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 努力的目标。文章来源地址https://www.toymoban.com/news/detail-412932.html

到了这里,关于详解 APISIX Lua 动态调试插件 inspect的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • APISIX 开启TCP动态代理

    1 修改config.yaml         修改 apisix-docker/example/apisix_conf/config.yaml ,开启tcp设置。如果 apisix.enable_admin 为 true,上面的配置会同时启用 HTTP 和 stream 代理。 9100 为apisix tcp的代理端口。 2 开启tcp端口映射         删除apisix容器,修改 apisix-docker/example/docker-compose.yml ,增加

    2024年02月02日
    浏览(32)
  • 基于docker的apisix网关的java插件开发

    java插件示例demo         将java插件示例demo拉取下来后,我们找到 DemoFilter.java 文件,在这个文件中可以修改过滤器的名称,为了方便测试,其他的文件暂时不需要修改。后期等过滤器调通后,可在过滤器中编写代码来实现自己的业务需求。过滤器名称为 AuthFilter ,后面在

    2024年01月19日
    浏览(39)
  • 36-Docker-常用命令详解-docker inspect

    前言 本篇来学习docker inspect命令 docker inspect 作用:获取容器/镜像的元数据。 options说明 option 说明 -f 指定返回值的模板文件 -s 如果类型为容器,则显示文件总大小 –type 返回指定类型的JSON 使用示例 获取容器ip 获取容器mac地址 获取容器日志路径 获取容器镜像名 获取容器目

    2024年02月13日
    浏览(42)
  • 【云原生网关】apisix使用详解

    目录 一、apisix介绍 1.1 apisix是什么 二、apisix特点 2.1 多平台支持 2.2 全动态能力

    2023年04月19日
    浏览(39)
  • 无涯教程-Lua - 调试语句

    Lua提供了一个调试库,该库提供了所有原始函数供无涯教程创建自己的调试器。即使没有内置的Lua调试器,也有许多针对Lua的调试器,这些调试器由各种开发人员创建,其中许多开源。 下表列出了Lua调试库中可用的函数及其用法。 Sr.No. Method Purpose 1 debug() 进入交互式模式进行

    2024年02月14日
    浏览(39)
  • Lua脚本本地调试

    这里主要使用日志的方式进行debug 项目对openresty包的依赖比较高,所以环境基础都在openresty下进行 openresty下载地址 下载完成后解压,具体使用方式和nginx没有什么区别,主要依赖文件是一下几个 这里拿lua链接mysql为例 双击 nginx.exe即可,浏览器输入 http://localhost:8765 进入启动

    2024年02月16日
    浏览(41)
  • idea调试unity里面的lua代码

    本人一名java后端开发,看到前端同事调试lua代码无脑print,甚为鄙视,百度加实操写一份调试unity的lua脚本文档 1.安装lua lua官网下载页面 最终下载页面 2.idea安装插件 emmylua 3.idea打开unity的lua脚本 idea-file-open 选择相应目录 idea-file-project structure-sdks选择+号 4.配置启动参数 5.复制

    2024年02月10日
    浏览(43)
  • 更改WordPress作者存档链接author和用户名插件Change Author Link Structure

    WordPress作者存档链接默认情况为/author/Administrator(用户名),为了防止用户名泄露,我们可以将其改为/author/1(用户ID),具体操作可参考『如何将WordPress作者存档链接中的用户名改为昵称或ID』。 如果你想要将WordPress作者存档链接中的用户名改为用户ID,同时还想要将链接中

    2024年02月21日
    浏览(47)
  • Unity中如何使用Rider调试C#和lua代码

    工作中经常需要调试lua代码,之前使用VS调试C#代码,使用Intellij Idea来调试lua代码,这样其实比较麻烦,所以查了一下,可以直接使用Rider来统一的调试C#和lua代码 Rider下载:Rider官网下载,根据需要选择对应的版本,建议使用Rider2020.1.0,参考链接 打开Unity,在preference中设置为

    2023年04月13日
    浏览(48)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包