脚本调试
在早期支持Lua脚本功能的Redis版本中,用户为了对脚本进行调试, 通常需要重复执行同一个脚本多次,并通过查看返回值的方式来验证 计算结果,这给脚本的编写带来了很大的麻烦,也制约了用户使用Lua 脚本功能编写大型脚本的能力。
为了解决上述问题,Redis从3.2版本开始新引入了一个Lua调试器,这 个调试器被称为Redis Lua调试器,简称LDB,用户可以通过LDB实现单 步调试、添加断点、返回日志、打印调用链、重载脚本等多种功能, 本节接下来的内容就会对这些功能进行详细的介绍。
一个简单的调试示例
让我们来看一个具体的脚本调试示例。首先,假设现在我们要调试一 个名为debug.lua的脚本,代码清单14-4展示了它的具体定义。
local ping_result = redis.call("PING")
local set_result = redis.call("SET", KEYS[1], ARGV[1])
return {ping_result, set_result}
为了创建一个新的调试会话,我们需要将—ldb选项、--eval选项、脚 本文件名debug.lua、键名"msg"以及附加参数"hello world"全部传递 给redis-cli客户端:
$ redis-cli --ldb --eval debug.lua "msg" , "hello world"
Lua debugging session started, please use:
quit -- End the session.
restart -- Restart the script in debug mode again.
help -- Show Lua script debugging commands.
* Stopped at 1, stop reason = step over
-> 1 local ping_result = redis.call("PING")
注意,在键名和附加参数之间需要使用一个逗号进行分隔。
客户端首先向我们展示了3个可用的调试器操作命令,分别是:
·quit——退出调试会话并关闭客户端。 ·restart——重新启动调试会话。 ·help——列出可用的调试命令。
在此之后,调试器向我们展示了当前的调试状态:
* Stopped at 1, stop reason = step over
-> 1 local ping_result = redis.call("PING")
因为调试器目前正处于单步调试模式,所以它在程序的第一行(同时 也是程序第一个有实际作用的代码行)前面停了下来,等待我们的调 试命令。
这时,可以通过输入调试命令step或者next,让调试器运行当前的代码行:
lua debugger> next
<redis> PING
<reply> "+PONG"
* Stopped at 2, stop reason = step over
-> 2 local set_result = redis.call("SET", KEYS[1], ARGV[1])
next命令返回了4行结果:
·第1行<redis>PING展示了Redis服务器执行的命令。 ·第2行<reply>"+PONG"是服务器在执行命令之后返回的结果。 ·第3行*Stopped at 2,stop reason=step over说明调试器因为处于单步调试模式,所以停在了程序第2行的前面,等待用户的下一个指示。 ·第4行→2local set_result=redis.call("SET",KEYS[1],ARGV[1])打印出了程序第2行的具体代码,即调试器下一次单步执行将要执行的代码。
这时,我们可以通过输入调试命令print来查看程序当前已有的局部变量以及它们的值:
lua debugger> print
<value> ping_result = {["ok"]="PONG"}
从这个结果可知,局部变量 ping_result 当前的值是一个包含 ok 字段的 Lua 表格,该字段的值为 "PONG"。
现在,再次执行next命令,调试器将执行程序的第2行代码:
lua debugger> next
<redis> SET msg hello world
<reply> "+OK"
* Stopped at 3, stop reason = step over
-> 3 return {ping_result, set_result}
根据结果可知,服务器这次执行了一个SET命令,返回了结果"+OK", 并且单步调试也执行到了程序的第3行代码。
这时如果我们再次执行print命令,可以看到新增的局部变量 set_result以及它的值:
lua debugger> print
<value> ping_result = {["ok"]="PONG"}
<value> set_result = {["ok"]="OK"}
现在,再次执行next命令,将看到以下结果:
lua debugger> next
1) PONG
2) OK
(Lua debugging session ended -- dataset changes rolled back)
redis>
其中PONG以及OK为脚本语句return{ping_result,set_result}返回的 值,而之后显示的(Lua debugging session ended—dataset changes rolled back)则是调试器打印的提示信息,它告诉我们Lua 调试会话已经结束。此外,因为在调试完成之后,客户端将退出调试 模式并重新回到普通的Redis客户端模式,所以我们在最后看到了熟悉 的redis>提示符。
调试命令
除了前面展示过的next命令和print命令之外,Lua脚本调试器还支持 很多不同的调试命令,这些命令可以通过在调试客户端中执行help命 令打印出来,表14-3展示了这些命令的用法以及作用。


因为调试程序通常需要重复执行多次相同的调试命令,为了让枯燥的 调试过程变得稍微愉快和容易一些,Redis为每个调试命令都设置了一 个缩写,即执行命令的快捷方式,这些缩写就是命令开头的首个字 母,用户只需要输入这些缩写,就可以执行相应的调试命令。比如, 输入n即可执行next命令,输入p即可执行print命令,诸如此类。
接下来将会对主要的调试命令进行介绍。
断点
在一般情况下,我们将以单步执行的方式对脚本进行调试,也就是 说,使用next命令执行一个代码行,观察一下执行的结果,在确认没有问题之后,继续使用next命令执行下一个代码行,以此类推,直到整个脚本都被执行完毕为止。
但是在有需要的情况下,也可以通过break命令给脚本增加断点,然后使用continue命令执行代码,直到遇见下一个断点为止。
比如,对于代码清单14-5所示的程序:
redis.call("echo", "line 1")
redis.call("echo", "line 2")
redis.call("echo", "line 3")
redis.call("echo", "line 4")
redis.call("echo", "line 5")
我们可以通过执行命令break 35,分别在脚本的第3行和第5行添加断点:
lua debugger> break 3 5
-> 1 redis.call("echo", "line 1")
2 redis.call("echo", "line 2")
#3 redis.call("echo", "line 3")
4 redis.call("echo", "line 4")
#5 redis.call("echo", "line 5")
在break命令返回的结果中,符号→用于标识当前行,而符号#则用于标识添加了断点的行。
如果我们现在执行命令continue,那么调试器将执行脚本的第1行和第 2行,然后在脚本的第1个断点(第3个代码行)前面暂停:
lua debugger> continue
* Stopped at 3, stop reason = break point
->#3 redis.call("echo", "line 3")
之后,再次执行命令continue,这次调试器将执行脚本的第3行和第4行,然后在脚本的第2个断点(第5个代码行)前面暂停:
lua debugger> continue
* Stopped at 5, stop reason = break point
->#5 redis.call("echo", "line 5")
最后,再次执行continue命令,这次调试器将执行至脚本的末尾并退出:
lua debugger> continue
(nil)
(Lua debugging session ended -- dataset changes rolled back)
break命令除了可以用于添加断点之外,还可用于显示已有断点以及移除断点,以下是一个简单的示例:
$ redis-cli --ldb --eval breakpoint.lua
* Stopped at 1, stop reason = step over
-> 1 redis.call("echo", "line 1")
lua debugger> break 3 5 -- 添加断点
-> 1 redis.call("echo", "line 1")
2 redis.call("echo", "line 2")
#3 redis.call("echo", "line 3")
4 redis.call("echo", "line 4")
#5 redis.call("echo", "line 5")
lua debugger> break -- 显示已有断点
2 breakpoints set:
#3 redis.call("echo", "line 3")
#5 redis.call("echo", "line 5")
lua debugger> break -3 -- 移除第3个代码行的断点
Breakpoint removed.
lua debugger> break -- 结果显示第3个代码行的断点已被移除
1 breakpoints set:
#5 redis.call("echo", "line 5")
lua debugger> break 0 -- 移除所有断点
All breakpoints removed.
lua debugger> break -- 目前没有设置任何断点
No breakpoints set. Use 'b <line>' to add one.
动态断点
除了可以使用break命令在调试脚本时手动添加断点之外,Redis还允 许用户在脚本中通过调用redis.breakpoint()函数来添加动态断 点,当调试器执行至redis.breakpoint()调用所在的行时,调试器 就会暂停执行过程并等待用户的指示。
动态断点对于调试条件语句以及循环语句非常有用,比如,我们可以在变量只为真的情况下添加断点:
if condition == true then
redis.breakpoint()
-- ...
end
或者在计数器达到某个指定值时添加断点:
if counter > n then
redis.breakpoint()
-- ...
end
比如,代码清单14-6所示的脚本将在计数器的值大于ARGV[1]的值时添加断点。
local i = 1
local target = tonumber(ARGV[1])
while true do
if i > target then
redis.breakpoint()
return "bye bye"
end
i = i+1
end
以下是一个将ARGV[1]设置为50并使用调试器调试该脚本的例子:
$ redis-cli --ldb --eval dynamic_breakpoint.lua , 50
* Stopped at 1, stop reason = step over
-> 1 local i = 1
lua debugger> continue
* Stopped at 6, stop reason = redis.breakpoint() called
-> 6 return "bye bye"
lua debugger> print
<value> i = 51
<value> target = 50
由print命令的执行结果可知,在第一次执行continue命令之后,调试器将在i的值为51时添加断点。
需要注意的是,redis.breakpoint()调用只会在调试模式下产生效 果,处于普通模式下的Lua解释器将自动忽略该调用。比如,如果我们 直接使用EVAL命令去执行dynamic_breakpoint.lua脚本,那么脚本将 不会产生任何断点,而是会直接返回脚本的执行结果:
$ redis-cli --eval dynamic_breakpoint.lua , 50
"bye bye"
输出调试日志
虽然我们可以通过添加动态断点并使用print命令打印出脚本在某个特 定时期的状态,但这种做法有时还是不够动态,如果能够直接在脚本 中把特定时期的状态打印出来,那么调试高度动态的程序时就会非常 方便。
为了做到这一点,我们可以使用Lua环境内置的redis.debug()函 数,这个函数能够直接把给定的值输出到调试客户端,使得用户可以 方便地得知给定变量或者表达式的值。
作为例子,代码清单14-7展示了一个计算斐波那契数的Lua脚本,这个 脚本在每次计算出新的斐波那契数时,都会使用redis.debug()函数 将这个值输出到调试客户端,使得我们可以直观地看到整个斐波那契 数的计算过程。这种做法比在每个循环中动态添加断点,然后使用 print命令打印斐波那契数的做法要方便得多。
local n = tonumber(ARGV[1])
-- F(0) = 0 , F(1) = 1
local i = 0
local j = 1
-- F(n) = F(n-1)+F(n-2)
while n ~= 0 do
i, j = j, i+j
n = n-1
redis.debug(i)
end
return i
以下是使用调试器调试斐波那契数计算脚本的过程,可以看到,在每 次循环时,redis.debug()函数都会把斐波那契数的当前值打印出来:
$ redis-cli --ldb --eval fibonacci.lua , 10
* Stopped at 1, stop reason = step over
-> 1 local n = tonumber(ARGV[1])
lua debugger> continue
<debug> line 11: 1
<debug> line 11: 1
<debug> line 11: 2
<debug> line 11: 3
<debug> line 11: 5
<debug> line 11: 8
<debug> line 11: 13
<debug> line 11: 21
<debug> line 11: 34
<debug> line 11: 55
(integer) 55
(Lua debugging session ended -- dataset changes rolled back)
执行指定的代码或命令
Lua调试器提供了eval和redis这两个调试命令,用户可以使用前者来 执行指定的Lua代码,并使用后者来执行指定的Redis命令,也可以通 过这两个调试命令来快速地验证一些想法以及结果,这会给程序的调 试带来很多好处。
比如,如果我们在调试某个脚本时,需要知道某个字符串对应的SHA1 校验和,那么只需要使用eval命令调用Lua环境内置的 redis.sha1hex()函数即可:
lua debugger> eval redis.sha1hex('hello world')
<retval> "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"
又比如,如果我们在调试某个会对数据库进行操作的脚本时,想要知 道某个键的当前值,那么只需要使用redis命令执行相应的数据获取命 令即可:
lua debugger> redis GET msg
<redis> GET msg
<reply> "hello world"
显示调用链
trace调试命令可以打印出脚本的调用链信息,这些信息在研究脚本的 调用路径时会非常有帮助。
比如,对于代码清单14-8所示的脚本:
local f1 = function()
local f2 = function()
local f3 = function()
redis.breakpoint()
end
f3()
end
f2()
end
f1()
trace命令将产生以下信息:
$ redis-cli --ldb --eval trace.lua
* Stopped at 9, stop reason = step over
-> 9 end
lua debugger> continue
* Stopped at 5, stop reason = redis.breakpoint() called
-> 5 end
lua debugger> list
1 local f1 = function()
2 local f2 = function()
3 local f3 = function()
4 redis.breakpoint()
-> 5 end
6 f3()
7 end
8 f2()
9 end
10
lua debugger> trace
In f3:
-> 5 end
From f2:
6 f3()
From f1:
8 f2()
From top level:
11 f1()
在trace命令返回的结果中:
In f3:
-> 5 end
表示调试器停在了脚本第5行,该行位于函数f3()当中;而:
From f2:
6 f3()
则说明了函数f3()位于脚本的第6行,由函数f2()调用;与此类似,之后的:
From f1:
8 f2()
则说明了函数f2()位于脚本的第8行,由函数f1()调用;至于最后的:
From top level:
11 f1()
则说明了函数f1()位于脚本的第11行,由解释器顶层(top level)调用。
重载脚本
restart是一个非常重要的调试器操作命令,它可以让调试客户端重新 载入被调试的脚本,并开启一个新的调试会话。
一般来说,用户在调试脚本的时候,通常需要重复执行以下几个步 骤,直至排除所有问题为止: 1)调试脚本。 2)根据调试结果修改脚本。 3)使用restart命令重新载入修改后的脚本,然后继续调试。
作为例子,假设我们现在需要对代码清单14-9所示的脚本进行调试, 这个脚本的第3行和第5行分别将PING错写成了P1NG(数字1)和PONG。
redis.call('PING')
redis.call('PING')
redis.call('P1NG') -- 错别字
redis.call('PING')
redis.call('PONG') -- 错别字
在将这个脚本载入调试器并运行至第3行时,脚本出现了错误,而客户 端也由于这个错误从调试状态退回到了普通的客户端状态:
$ redis-cli --ldb --eval typo.lua
* Stopped at 1, stop reason = step over
-> 1 redis.call('PING')
lua debugger> continue
(error) ERR Error running script (call to f_cb3ff5da49083d9b7765f3c62b6bfce3b07cbdcb): @user_scr
ipt:3: @user_script: 3: Unknown Redis command called from Lua script
(Lua debugging session ended -- dataset changes rolled back)
redis>
根据调试结果可知,脚本第3行的命令调用出现了错误。于是我们修改 文件,将调用中的P1NG修改为PING,然后在客户端中输入restart,重 新开始调试:
redis> restart
* Stopped at 1, stop reason = step over
-> 1 redis.call('PING')
lua debugger>
之后,调试器继续执行脚本,并在脚本的第5行停了下来:
lua debugger> continue
(error) ERR Error running script (call to f_6f077220cdd710a5592c23dd0eedab9dee363854): @user_script:5: @user_script: 5: Unknown Redis command called from Lua script
(Lua debugging session ended -- dataset changes rolled back)
根据这次的调试结果,我们得知脚本第5行调用的命令出错了,于是修 改文件,将调用中的PONG修改为PING,然后再次在客户端中输入 restart并重新开始调试:
redis> restart
* Stopped at 1, stop reason = step over
-> 1 redis.call('PING')
lua debugger>
经历了两次修改之后,脚本终于可以顺利地执行了:
lua debugger> continue
(nil)
(Lua debugging session ended -- dataset changes rolled back)
redis>
调试模式
Redis的Lua调试器支持两种不同的调试模式,一种是异步调试,另一 种则是同步调试。当用户以ldb选项启动调试会话时,Redis服务器将 以异步方式调试脚本:
redis-cli --ldb --eval script.lua
运行在异步调试模式下的Redis服务器会为每个调试会话分别创建新的 子进程,并将其用作调试进程:
-
因为Redis服务器可以创建出任意多个子进程作为调试进程,所以异 步调试允许多个调试会话同时存在,换句话说,异步调试模式允许多 个用户同时进行调试。
-
因为异步调试是在子进程而不是服务器进程上进行,它不会阻塞服 务器进程,所以在异步调试的过程中,其他客户端可以继续访问Redis 服务器。
-
因为异步调试期间执行的所有Lua代码以及Redis命令都是在子进程 上完成的,所以在调试完成之后,调试期间产生的所有数据修改也会 随着子进程的终结而消失,它们不会对Redis服务器的数据库产生任何 影响。
当用户以ldb-sync-mode选项启动调试会话时,Redis服务器将以同步 方式调试脚本:
redis-cli --ldb-sync-mode --eval script.lua
运行在同步调试模式下的Redis服务器将直接使用服务器进程作为调试进程:
-
因为同步调试不会创建任何子进程,而是直接使用服务器进程作为 调试进程,所以同一时间内只能有一个调试会话存在。换句话说,同 步调试模式只允许单个用户进行调试。
-
因为同步调试直接在服务器进程上进行,它需要独占整个服务器, 所以在整个同步调试过程中,其他客户端对服务器的访问都会被阻塞。
-
因为在同步调试期间,所有Lua代码以及Redis命令都是直接在服务 器进程上执行的,所以调试期间产生的数据修改将保留在服务器的数 据库中。
简单来说,虽然异步调试和同步调试都能够调试Lua脚本,但它们完成 调试工作的方式却是完全相反的。
举个例子,如果我们在一个拥有空白数据库的Redis服务器上进行异步 调试,并使用redis调试命令执行一个设置操作:
$ redis-cli --ldb --eval typo.lua
* Stopped at 1, stop reason = step over
-> 1 redis.call('PING')
lua debugger> redis SET msg 'hello world'
<redis> SET msg hello world
<reply> "+OK"
lua debugger> quit
那么在调试完毕之后,msg键将不会在数据库中出现:
$ redis-cli
redis> KEYS *
(empty list or set)
与此相反,如果我们在相同的Redis服务器上进行同步调试,并执行相同的设置操作:
$ redis-cli --ldb-sync-mode --eval typo.lua
* Stopped at 1, stop reason = step over
-> 1 redis.call('PING')
lua debugger> redis SET msg 'hello world'
<redis> SET msg hello world
<reply> "+OK"
lua debugger> quit
那么在调试完毕之后,msg键将继续保留在服务器的数据库中:
$ redis-cli
redis> KEYS *
1) "msg"
终止调试会话
在调试Lua脚本时,用户有3种方法可以退出调试会话:
-
当脚本执行完毕时,调试会话将自然终止,客户端也会从调试状态 退回到普通状态。
-
当用户在调试器中按下Ctrl+C键时,调试器将在执行完整个脚本之 后终止调试会话。
-
当用户在调试器中执行abort命令时,调试器将不再执行任何代码, 直接终止调试会话。
我们需要特别注意方法2和方法3之间的区别,因为对于一些脚本来 说,使用这两种退出方法可能会产生不一样的结果。
举个例子,对于代码清单14-10所示的脚本:
redis.call("SET", "msg", "hello world")
redis.call("SET", "database", "redis")
redis.call("SET", "number", 10086)
如果我们使用同步模式调试这个脚本,并在执行脚本的第一行代码之 后按Ctrl+C键退出调试,那么调试器将在执行完整个脚本之后退出调 试会话:
$ redis-cli --ldb-sync-mode --eval set_strings.lua
* Stopped at 1, stop reason = step over
-> 1 redis.call("SET", "msg", "hello world")
lua debugger> next
<redis> SET msg hello world
<reply> "+OK"
* Stopped at 2, stop reason = step over
-> 2 redis.call("SET", "database", "redis")
lua debugger>
$
通过访问数据库可以看到,脚本设置的3个键都出现在了数据库中,这 说明脚本包含的3个SET命令都被执行了:
redis> KEYS *
1) "number"
2) "database"
3) "msg"
此外,如果我们使用相同的模式调试相同的脚本,但是在执行脚本的 第1行之后使用abort命令退出调试:
$ redis-cli --ldb-sync-mode --eval set_strings.lua
* Stopped at 1, stop reason = step over
-> 1 redis.call("SET", "msg", "hello world")
lua debugger> next
<redis> SET msg hello world
<reply> "+OK"
* Stopped at 2, stop reason = step over
-> 2 redis.call("SET", "database", "redis")
lua debugger> abort
(error) ERR Error running script (call to f_4a3b211335f38c87bc0465bb0b6b0c9780-
f4be41): @user_script:2: script aborted for user request
(Lua debugging session ended)
那么数据库将只会包含脚本第1行代码设置的msg键:
redis> KEYS *
1) "msg"
为了避免出现类似问题,我们在进行调试,特别是在同步模式下进行 调试时,如果要中途停止调试,最好还是使用abort命令退出调试会 话,从而尽可能地避免意料之外的情况发生。