调试 Redis 中的 Lua 脚本
如何使用内置 Lua 调试器
从 3.2 版本开始,Redis 包含一个完整的 Lua 调试器,可以用来使编写复杂的 Redis 脚本变得更加简单。
Redis Lua 调试器(代号为 LDB)具有以下重要功能
- 它使用服务器-客户端模型,因此它是一个远程调试器。Redis 服务器充当调试服务器,而默认客户端是
redis-cli
。但是,可以通过遵循服务器实现的简单协议来开发其他客户端。 - 默认情况下,每个新的调试会话都是一个分叉的会话。这意味着在调试 Redis Lua 脚本时,服务器不会阻塞,并且可用于开发或并行执行多个调试会话。这也意味着在脚本调试会话完成后更改会回滚,因此可以使用与上一个调试会话完全相同的数据集重新启动一个新的调试会话。
- 按需提供备用的同步(非分叉)调试模型,以便可以保留对数据集的更改。在此模式下,服务器在调试会话处于活动状态时会阻塞。
- 支持单步执行。
- 支持静态和动态断点。
- 支持将已调试的脚本记录到调试器控制台。
- 检查 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
命令像往常一样重新启动会话。
停止调试会话的另一种方法是手动中断redis-cli
,方法是按Ctrl+C
。请注意,任何中断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 与 LDB 集成,使用 redis-lua。以下 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()