*Redis Lua 脚本调试器

从 3.2 版本开始,Redis 包含了一个完整的 Lua 调试器,用于简化编写复杂 Redis 脚本的任务。

Redis Lua 调试器,代号 LDB,具有以下重要特性:

  • 它使用客户端-服务器模型,因此是一个远程调试器。Redis 服务器充当调试服务器,而默认客户端是 redis-cli。当然,也可以根据服务器实现的简单协议开发其他客户端。
  • 默认情况下,每个新的调试会话都是一个 fork 出的会话。这意味着当 Redis Lua 脚本被调试时,服务器不会阻塞,可以用于开发或并行执行多个调试会话。这也意味着在脚本调试会话结束后,更改会被回滚,因此可以使用与先前调试会话完全相同的数据集重新开始一个新的调试会话。
  • 按需提供另一种同步(非 fork)调试模式,这样对数据集的更改可以保留。在这种模式下,服务器会在调试会话期间阻塞。
  • 支持单步执行。
  • 支持静态和动态断点。
  • 支持将调试脚本中的日志输出到调试器控制台。
  • 支持检查 Lua 变量。
  • 支持追踪脚本执行的 Redis 命令。
  • 支持 Redis 和 Lua 值的漂亮打印。
  • 支持无限循环和长时间执行检测,可模拟断点。

*快速开始

重要提示: 请确保避免在生产 Redis 服务器上调试 Lua 脚本。应使用开发服务器。另请注意,使用同步调试模式(非默认模式)会导致 Redis 服务器在整个调试会话期间阻塞。

要使用 redis-cli 开始一个新的调试会话,请执行以下步骤:

  1. 使用你喜欢的编辑器在某个文件中创建脚本。假设你正在编辑位于 /tmp/script.lua 的 Redis Lua 脚本。
  2. 使用以下命令启动调试会话:
./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]continue          Run till next breakpoint.
[l]list              List source code around current line.
[l]list [line]       List source code around [line].
                     line = 0 means: current position.
[l]list [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]eval <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]abort             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-cliredis-server 之间连接的事件也会中断调试会话。

所有 fork 出的调试会话都会在服务器关闭时终止。

*调试命令的缩写

调试可能是一项非常重复的任务。因此,每个 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 使用 fork 出的会话,并回滚脚本在被调试期间操作的所有数据更改。在调试过程中,确定性通常是一件好事,这样后续的调试会话可以在无需将数据库内容重置为其原始状态的情况下启动。

然而,为了追踪某些 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() 没有任何效果。请注意,该函数接受多个参数,在输出中用逗号和空格分隔。

表和嵌套表会正确显示,以便调试脚本的程序员可以轻松观察值。

*使用 printeval 检查程序状态

虽然可以使用 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 是默认的调试客户端,但任何满足以下条件之一的客户端都可以用于调试:

  1. 客户端提供了用于设置调试模式和控制调试会话的原生接口。
  2. 客户端提供了通过 RESP 发送任意命令的接口。
  3. 客户端允许向 Redis 服务器发送原始消息。

例如,ZeroBrane StudioRedis 插件 使用 redis-lua 与 LDB 集成。以下 Lua 代码是该插件如何实现这一点的简化示例:

local redis = require 'redis'

-- 添加 LDB 的 Continue 命令
redis.commands['ldbcontinue'] = redis.command('C')

-- 要调试的脚本
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()