在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
和
行显示了刚刚执行的命令以及服务器的回复。请注意,这仅在单步模式下发生。如果您使用 continue
来执行脚本直到下一个断点,命令将不会显示在屏幕上,以防止输出过多。
调试会话的终止
当脚本自然终止时,调试会话结束,redis-cli
返回到其正常的非调试模式。你可以像往常一样使用 restart
命令重新启动会话。
另一种停止调试会话的方法是手动按下Ctrl+C
来中断redis-cli
。请注意,任何中断redis-cli
和redis-server
之间连接的事件也会中断调试会话。
当服务器关闭时,所有分叉的调试会话都将终止。
缩写调试命令
调试可能是一项非常重复的任务。因此,每个 Redis 调试器命令都以不同的字符开头,您可以使用单个初始字符来引用该命令。
因此,例如,您只需输入 s
而不是输入 step
。
断点
添加和删除断点非常简单,如在线帮助中所述。
只需使用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使用分叉会话,并在调试脚本时回滚所有数据更改。在调试期间,确定性通常是一个很好的特性,这样可以在不将数据库内容重置为原始状态的情况下启动连续的调试会话。
然而,为了追踪某些错误,您可能希望保留每次调试会话对键空间所做的更改。当这是一个好主意时,您应该使用一个特殊选项ldb-sync-mode
在redis-cli
中启动调试器。
./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服务器发送原始消息。
例如,Redis插件为ZeroBrane Studio使用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()