详解 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:
...- 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.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 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; 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 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 努力的目标。
相关文章:
详解 APISIX Lua 动态调试插件 inspect
作者罗锦华,API7.ai 技术专家/技术工程师,开源项目 pgcat,lua-resty-ffi,lua-resty-inspect 的作者。 原文链接 为什么需要 Lua 动态调试插件? Apache APISIX 有很多 Lua 代码,如何在运行时不触碰源代码的…...
#科研筑基# python初学自用笔记 第五篇 函数
调用函数python有很多内置函数,我们可以直接调用,详见python官方文档:内置函数 — Python 3.11.2 文档,也可以在命令行中输入help(函数名)来查看该函数的使用法则。函数名的本质就是指向一个函数对象的引用,完全可以用…...
设计模式之策略模式
一.基本内容1 . 实例有各种鸭子(野鸭,北京鸭子,水鸭等,鸭子有各种行为,比如飞,叫等显示鸭子的信息传统方法解决:鸭子为抽象类,具体鸭子继承抽象类2.传统方法的不足:其他鸭…...
dbdeployer 使用札记
https://github.com/datacharmer/dbdeployer默认配置文件为当前用户的$HOME/.dbdeployer/config.json作为配置文件,可以通过dbdeplyoer defaults export导出并修改配置或者直接通过dbdeployer defaults update来更新默认文件,配置文件包含MySQL初始信息。…...
MATLAB算法实战应用案例精讲-【图像处理】数字图像模糊化(附Java、python和matlab代码实现)
目录 前言 几个相关概念 噪声 滤波器 算法原理 算法思想 噪...

搭建Hexo博客-第1章-Git和GitHub以及Coding的简单用法
搭建Hexo博客-第1章-Git和GitHub以及Coding的简单用法 搭建Hexo博客-第1章-Git和GitHub以及Coding的简单用法 Coding GitHub Hexo Markdown 搭建博客 大家好,这是我第一次写博客。使用 GitHub Hexo 创建最基本的博客很容易,网上有很多现成的教程。…...

【C++修行之路】C/C++内存管理
文章目录程序区域内存划分C语言动态内存分配:new和delete:new、delete和malloc、free的区别:程序区域内存划分 C/C程序内存区域划分非常相似。 C语言动态内存分配: malloc、calloc、realloc都是C语言动态开辟内存的常用函数 其中 malloc 开…...

spring cloud alibaba Sentinel(四)
服务雪崩 在分布式系统中,由于网络原因或自身的原因,服务一般无法保证 100% 可用。 如果一个服务出现了问题,调用这个服务就会出现线程阻塞的情况, 此时若有大量的请求涌入,就会出现多条 线程阻塞等待,进而导致服务瘫痪。 由于服…...

Redis第三讲
目录 三、Redis03 3.1 Redis持久化之RDB 3.1.1 什么是RDB 3.1.2 备份是如何执行的 3.1.3 Fork 3.1.4 RDB持久化流程 3.1.5 dump.rdb文件 3.1.6 配置rdb文件生成位置 3.1.7 如何触发RDB快照以及保持策略 3.2 Redis持久化之AOF 3.2.1 什么是AOF 3.2.2 AOF持久化流程 …...

JAVA线程池的使用
一、池化思想和JAVA线程池 池化是很重要的思想;池化的好处是提供缓冲和统一的管理。这个笔者在本人的数据库连接池的博客中已经提到过了(JAVA常用数据库连接池_王者之路001的博客-CSDN博客 )。 线程池是另一种池化思想的运用,把…...

力扣56.合并区间
文章目录力扣56.合并区间题目描述排序合并力扣56.合并区间 题目描述 以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中…...
代码随想录二刷Day03链表: 24.两两交换链表中的节点,19.删除链表的倒数第N个节点,面试题 02.07. 链表相交,142.环形链表||
24.两两交换链表中的节点 文章链接:代码随想录 (programmercarl.com) 思路: (1)首先如果要处理相邻两个节点的话,一定需要操作两个节点的前一个节点才可以,因此,本题需要设定一个虚拟头节点 …...

我应该在我的博客上写什么? 介绍如何撰写初学者容易担心的文章
我想有很多人开了博客,但想不起来写作,无法取得进展。 博客的主题和文章的内容不会仅仅通过写你想做的事情来工作。 重要的是要了解用户想要阅读的内容以及人们可能收集的内容,并将其与您想要编写的内容很好地匹配。 这一次,我…...

嵌入式C语言设计模式 --- 外观模式
1 - 什么是外观模式? 外观模式(Facade Pattern),是一种比较简单的结构型模式,它存在的目的,也是为了简单。 外观模式隐藏了一系列接口的复杂性,旨在为外部客户端提供一个更高层次且统一简单的接口,简化了客户端调用某些模块的一系列操作。 外观模式应该是软件工程师…...

若依ruoyi——手把手教你制作自己的管理系统【三、代码生成】
昨天情人节一( ̄︶ ̄*)) 送给赛利亚一((* ̄3 ̄)╭ ********* 专栏略长 爆肝万字 细节狂魔 请准备好一键三连 ********* 修改后的页面: 干干净净贼舒服一Ψ( ̄∀ ̄)Ψ——Ψ( ̄∀&#x…...

SCI论文写作神器集合 —— 超级实用
特此声明: 本文拷贝多处别人的内容,并给出具体的链接 本文所提到的软件都为博主在文章撰写过程中发掘的比较实用的工具,旨在帮助小伙伴们更快更有效率的完成文章发表,如果其他好用的工具,欢迎各位交流~~ 一、文献搜索神…...
MAC 系统安装多版本 JDK 并任意切换
1、背景 在进行 Java 开发的过程中,我们可能需要使用不同版本的 JDK。例如:一些旧的 Java 应用程序只能在旧版本的 JDK 上运行,而一些新的 Java 应用程序需要较新的 JDK 才能运行。 在 MAC 系统上,如何安装多个版本的 JDK 并配置…...
配置 Smart Link 接口时需注意的互斥命令
配置 Smart Link 接口时需注意的互斥命令 一、接口加入Smart Link组功能与以下功能互斥一、接口加入Smart Link组功能与以下功能互斥 注:当接口已经加入Smart Link组,则不能再配置以下功能;反之,当接口已经配置以下功能ÿ…...

QT的下载和安装
这里介绍的是QT官方方式下载,每次都让我很糊涂,就记载一下。先是下载QT online installerhttps://www.qt.io/download 在下方有Go Open Sourcehttps://www.qt.io/download-open-source 在下方有Download the Qt Online installerhttps://www.qt.io/downl…...
nacos配置中心与服务注册中心
文章目录 目录 文章目录 前言 一、服务注册与发现中心 二、配置中心 总结 前言 Nacos是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。它是 Spring Cloud Alibaba 组件之一,负责服务注册发现和服务配置. [服务治理的作用和微服务配置管理] Na…...
Vue记事本应用实现教程
文章目录 1. 项目介绍2. 开发环境准备3. 设计应用界面4. 创建Vue实例和数据模型5. 实现记事本功能5.1 添加新记事项5.2 删除记事项5.3 清空所有记事 6. 添加样式7. 功能扩展:显示创建时间8. 功能扩展:记事项搜索9. 完整代码10. Vue知识点解析10.1 数据绑…...

聊聊 Pulsar:Producer 源码解析
一、前言 Apache Pulsar 是一个企业级的开源分布式消息传递平台,以其高性能、可扩展性和存储计算分离架构在消息队列和流处理领域独树一帜。在 Pulsar 的核心架构中,Producer(生产者) 是连接客户端应用与消息队列的第一步。生产者…...
质量体系的重要
质量体系是为确保产品、服务或过程质量满足规定要求,由相互关联的要素构成的有机整体。其核心内容可归纳为以下五个方面: 🏛️ 一、组织架构与职责 质量体系明确组织内各部门、岗位的职责与权限,形成层级清晰的管理网络…...

CocosCreator 之 JavaScript/TypeScript和Java的相互交互
引擎版本: 3.8.1 语言: JavaScript/TypeScript、C、Java 环境:Window 参考:Java原生反射机制 您好,我是鹤九日! 回顾 在上篇文章中:CocosCreator Android项目接入UnityAds 广告SDK。 我们简单讲…...
实现弹窗随键盘上移居中
实现弹窗随键盘上移的核心思路 在Android中,可以通过监听键盘的显示和隐藏事件,动态调整弹窗的位置。关键点在于获取键盘高度,并计算剩余屏幕空间以重新定位弹窗。 // 在Activity或Fragment中设置键盘监听 val rootView findViewById<V…...
CMake控制VS2022项目文件分组
我们可以通过 CMake 控制源文件的组织结构,使它们在 VS 解决方案资源管理器中以“组”(Filter)的形式进行分类展示。 🎯 目标 通过 CMake 脚本将 .cpp、.h 等源文件分组显示在 Visual Studio 2022 的解决方案资源管理器中。 ✅ 支持的方法汇总(共4种) 方法描述是否推荐…...

10-Oracle 23 ai Vector Search 概述和参数
一、Oracle AI Vector Search 概述 企业和个人都在尝试各种AI,使用客户端或是内部自己搭建集成大模型的终端,加速与大型语言模型(LLM)的结合,同时使用检索增强生成(Retrieval Augmented Generation &#…...

并发编程 - go版
1.并发编程基础概念 进程和线程 A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。C.一个进程可以创建和撤销多个线程;同一个进程中…...

Linux nano命令的基本使用
参考资料 GNU nanoを使いこなすnano基础 目录 一. 简介二. 文件打开2.1 普通方式打开文件2.2 只读方式打开文件 三. 文件查看3.1 打开文件时,显示行号3.2 翻页查看 四. 文件编辑4.1 Ctrl K 复制 和 Ctrl U 粘贴4.2 Alt/Esc U 撤回 五. 文件保存与退出5.1 Ctrl …...

C# 表达式和运算符(求值顺序)
求值顺序 表达式可以由许多嵌套的子表达式构成。子表达式的求值顺序可以使表达式的最终值发生 变化。 例如,已知表达式3*52,依照子表达式的求值顺序,有两种可能的结果,如图9-3所示。 如果乘法先执行,结果是17。如果5…...