在 Redis 中调试 Lua 脚本
如何使用内置的 Lua 调试器
从 3.2 版本开始,Redis 包含了一个完整的 Lua 调试器,可用于简化编写复杂 Redis 脚本的任务。
Redis Lua 调试器,代号为 LDB,具有以下重要特性
- 它使用服务器-客户端模型,因此是一个远程调试器。Redis 服务器充当调试服务器,而默认客户端是
redis-cli
。然而,可以通过遵循服务器实现的简单协议来开发其他客户端。 - 默认情况下,每个新的调试会话都是一个分叉会话。这意味着在调试 Redis Lua 脚本时,服务器不会阻塞,并且可以用于开发或并行执行多个调试会话。这也意味着脚本调试会话结束后,更改会被 回滚,因此可以再次启动新的调试会话,使用与之前调试会话完全相同的 Redis 数据集。
- 可以按需使用同步(非分叉)调试模型,以便保留对数据集的更改。在这种模式下,服务器会在调试会话处于活动状态期间保持阻塞。
- 支持单步执行。
- 支持静态和动态断点。
- 支持将调试脚本日志记录到调试器控制台。
- 检查 Lua 变量。
- 跟踪脚本执行的 Redis 命令。
- 美化打印 Redis 和 Lua 值。
- 检测无限循环和长时间执行,这会模拟一个断点。
快速入门
了解 Lua 调试器的一个简单方法是观看此视频介绍
重要提示:请务必避免在 Redis 生产服务器上调试 Lua 脚本。请改用开发服务器。另请注意,使用同步调试模式(非默认模式)会导致 Redis 服务器在整个调试会话期间处于阻塞状态。
要使用 redis-cli
启动新的调试会话,请执行以下操作
-
使用您喜欢的编辑器创建脚本。假设您正在编辑位于
/tmp/script.lua
的 Redis Lua 脚本。 -
使用以下命令启动调试会话
./redis-cli --ldb --eval /tmp/script.lua
请注意,使用 redis-cli
的 --eval
选项,您可以将键名和参数传递给脚本,用逗号分隔,示例如下
./redis-cli --ldb --eval /tmp/script.lua mykey somekey , arg1 arg2
您将进入一个特殊模式,在此模式下 redis-cli
不再接受其常规命令,而是打印帮助屏幕并将未修改的调试命令直接传递给 Redis。
不会传递给 Redis 调试器的命令只有
quit
-- 这将终止调试会话。它类似于移除所有断点并使用continue
调试命令。此外,此命令将退出redis-cli
。restart
-- 调试会话将从头开始重新启动,从文件中重新加载新版本的脚本。因此,正常的调试周期包括在调试后修改脚本,然后调用restart
以便使用新的脚本更改再次开始调试。help
-- 此命令将传递给 Redis Lua 调试器,它将打印如下命令列表
lua debugger> help
Redis Lua debugger help:
[h]elp Show this help.
[s]tep Run current line and stop again.
[n]ext Alias for step.
[c]ontinue Run till next breakpoint.
[l]ist List source code around current line.
[l]ist [line] List source code around [line].
line = 0 means: current position.
[l]ist [line] [ctx] In this form [ctx] specifies how many lines
to show before/after [line].
[w]hole List all source code. Alias for 'list 1 1000000'.
[p]rint Show all the local variables.
[p]rint <var> Show the value of the specified variable.
Can also show global vars KEYS and ARGV.
[b]reak Show all breakpoints.
[b]reak <line> Add a breakpoint to the specified line.
[b]reak -<line> Remove breakpoint from the specified line.
[b]reak 0 Remove all breakpoints.
[t]race Show a backtrace.
[e]val <code> Execute some Lua code (in a different callframe).
[r]edis <cmd> Execute a Redis command.
[m]axlen [len] Trim logged Redis replies and Lua var dumps to len.
Specifying zero as <len> means unlimited.
[a]bort Stop the execution of the script. In sync
mode dataset changes will be retained.
Debugger functions you can call from Lua scripts:
redis.debug() Produce logs in the debugger console.
redis.breakpoint() Stop execution as if there was a breakpoint in the
next line of code.
请注意,当您启动调试器时,它将以单步模式启动。它将在脚本中实际执行的第一行之前停止,等待执行。
从此时起,您通常会调用 step
以执行当前行并转到下一行。在您单步执行时,Redis 将显示服务器执行的所有命令,示例如下
* Stopped at 1, stop reason = step over
-> 1 redis.call('ping')
lua debugger> step
<redis> ping
<reply> "+PONG"
* Stopped at 2, stop reason = step over
<redis>
和 <reply>
行显示刚执行的行所执行的命令,以及服务器的回复。请注意,这仅在单步模式下发生。如果您使用 continue
命令让脚本一直执行到下一个断点,命令将不会转储到屏幕上,以避免输出过多。
终止调试会话
当脚本自然终止时,调试会话结束,redis-cli
返回其正常的非调试模式。您可以照常使用 restart
命令重新启动会话。
停止调试会话的另一种方法是手动按 Ctrl+C
中断 redis-cli
。另请注意,任何中断 redis-cli
和 redis-server
之间连接的事件也会中断调试会话。
服务器关闭时,所有分叉的调试会话都将终止。
缩写调试命令
调试可能是一项非常重复的任务。因此,每个 Redis 调试器命令都以不同的字符开头,您可以使用单个首字母来引用该命令。
例如,您不必键入 step
,只需键入 s
即可。
断点
添加和移除断点非常简单,如在线帮助中所述。只需使用 b 1 2 3 4
在第 1、2、3、4 行添加断点。命令 b 0
移除所有断点。可以通过提供要移除断点所在行的行号作为参数,并在前面加上减号来移除选定的断点。例如,b -3
移除第 3 行的断点。
请注意,在 Lua 从不执行的行(如局部变量声明或注释)上添加断点将不起作用。断点会被添加,但由于脚本的这部分永远不会执行,程序将永远不会停止。
动态断点
使用 breakpoint
命令可以在特定行上添加断点。然而,有时我们只想在发生特殊情况时停止程序的执行。为此,您可以在 Lua 脚本中使用 redis.breakpoint()
函数。调用此函数时,它会在将要执行的下一行模拟一个断点。
if counter > 10 then redis.breakpoint() end
此功能在调试时非常有用,这样我们就可以避免手动多次继续脚本执行,直到遇到特定条件。
同步模式
如前所述,默认情况下 LDB 使用分叉会话,并在脚本调试期间回滚所有数据更改。确定性在调试期间通常是一件好事,这样就可以在不将数据库内容重置为其原始状态的情况下启动连续的调试会话。
然而,为了跟踪某些 bug,您可能希望保留每个调试会话对键空间执行的更改。在这种情况下,您应该在 redis-cli
中使用特殊选项 ldb-sync-mode
启动调试器。
./redis-cli --ldb-sync-mode --eval /tmp/script.lua
注意:在此模式下,Redis 服务器在调试会话期间将无法访问,请谨慎使用。
在这种特殊模式下,abort
命令可以在中途停止脚本并保留对数据集的操作更改。请注意,这与正常结束调试会话不同。如果您只是中断 redis-cli
,脚本将完全执行,然后会话终止。而使用 abort
,您可以在脚本执行过程中中断并根据需要启动新的调试会话。
脚本中的日志记录
redis.debug()
命令是一个强大的调试工具,可以在 Redis Lua 脚本内部调用,用于将信息记录到调试控制台
lua debugger> list
-> 1 local a = {1,2,3}
2 local b = false
3 redis.debug(a,b)
lua debugger> continue
<debug> line 3: {1; 2; 3}, false
如果脚本在调试会话之外执行,redis.debug()
将没有任何效果。请注意,该函数接受多个参数,在输出中用逗号和空格分隔。
表和嵌套表会正确显示,以便于调试脚本的程序员观察值。
使用 print
和 eval
检查程序状态
虽然 redis.debug()
函数可以直接在 Lua 脚本内部用于打印值,但通常在单步执行或在断点处停止时,观察程序的局部变量非常有用。
print
命令正是做这件事的,它会从当前调用帧开始向后查找直到顶层调用帧。这意味着即使我们在 Lua 脚本中的嵌套函数中,仍然可以使用 print foo
来查看调用函数上下文中的 foo
的值。如果在没有变量名的情况下调用,print
将打印所有变量及其各自的值。
eval
命令执行小段 Lua 脚本,但不在当前调用帧的上下文中执行(由于当前的 Lua 内部机制,无法在当前调用帧的上下文中进行评估)。然而,您可以使用此命令来测试 Lua 函数。
lua debugger> e redis.sha1hex('foo')
<retval> "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33"
调试客户端
LDB 使用客户端-服务器模型,其中 Redis 服务器充当调试服务器,使用 RESP 进行通信。虽然 redis-cli
是默认的调试客户端,但任何满足以下条件之一的客户端都可以用于调试
- 客户端提供用于设置调试模式和控制调试会话的原生接口。
- 客户端提供通过 RESP 发送任意命令的接口。
- 客户端允许向 Redis 服务器发送原始消息。
例如,ZeroBrane Studio 的 Redis 插件使用 redis-lua 与 LDB 集成。以下 Lua 代码是该插件如何实现此功能的简化示例
local redis = require 'redis'
-- add LDB's Continue command
redis.commands['ldbcontinue'] = redis.command('C')
-- script to be debugged
local script = [[
local x, y = tonumber(ARGV[1]), tonumber(ARGV[2])
local result = x * y
return result
]]
local client = redis.connect('127.0.0.1', 6379)
client:script("DEBUG", "YES")
print(unpack(client:eval(script, 0, 6, 9)))
client:ldbcontinue()