vlambda博客
学习文章列表

Redis 如何调试Lua 脚本​

概述

Redis 3.2开始,内置了 Lua debugger(简称LDB),使用Lua debugger可以很方便的对我们编写的Lua脚本进行调试

快速开始

可以使用下面的步骤创建一个新的debug会话:

  • 在本地创建一个 Lua脚本
  • 使用 redis-cli,通过 --ldb参数进入到 debug模式,使用 --eval参数指定需要 debugLua脚本

比如我本地创建了一个/tmp/script.lua脚本文件,脚本内容如下:

local foo = redis.call('ping')
return foo

接下来可以使用redis-cli/tmp/script.lua脚本进行调试:

$ redis-cli --ldb --eval /tmp/script.lua
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 foo = redis.call('ping')
lua debugger>

当我们执行完redis-cli --ldb --eval /tmp/script.lua后,从输出信息可以看出,我们开启了一个debugging会话,并且代码停在了第一行的位置,可以使用step命令让脚本执行到下一行

lua debugger> step
<redis> ping
<reply> "+PONG"
* Stopped at 2, stop reason = step over
-> 2   return foo

输出信息中<redis> ping表示脚本执行了ping命令,<reply> "+PONG"则是redis-server的返回信息。执行完setp命令后,代码不会继续执行,而是停在了第二行,再次执行step命令,执行return foo代码,此时脚本代码已经执行完,debugging会话结束,并输出脚本返回结果

lua debugger> step

PONG

(Lua debugging session ended -- dataset changes rolled back)

完整的流程如下:

LDB 工作模式

默认情况下,当某个客户端向服务端发起debugging会话的时候,并不会阻塞服务端,即服务端仍能正常的处理请求,而且也能同时处理多个debugging会话,因为会话是并行的,并不会互相干扰

另外一点是,当debugging会话结束的时候,Lua脚本中对redis数据的所有修改都会回滚,这样做的好处是当多个会话同时debug的时候不会互相干扰,并且可以在不用清理/还原数据的情况下,使用restart重新开启debug会话时,使每次的执行效果都相同

首先执行下面命令初始化数据:

$ rpush list_a a b c d
(integer) 4

然后编写script.lua脚本:

ocal src = KEYS[1]
local dst = KEYS[2]
local count = redis.call('llen',src)

while count > 0 do
    local item = redis.call('rpop',src)
    redis.call('lpush',dst,item)
    count = count - 1
end

return redis.call('llen',dst)

该脚本的作用是将src列表中的所有元素移动到dst中,接下对该脚本进行调试

$ redis-cli --ldb --eval /tmp/script.lua list_a list_b
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 src = KEYS[1]
lua debugger> continue

(integer) 4

(Lua debugging session ended -- dataset changes rolled back)

在这里我们使用continue命令(后面会介绍该命令的使用)让脚本直接执行完毕,然后在redis中看看list_alist_b的值

$ lrange list_a 0 -1
1) "a"
2) "b"
3) "c"
4) "d"
$ lrange list_b 0 -1
(empty array)

可以看到,redis中的数据并没有发生改变,因为默认工作模式下,当debugging会话关闭的时候,会将会话中的所有修改回滚

我们还可以通过--ldb-sync-mode参数,指定LDB同步模式,在该工作模式下调试Lua脚本会阻塞服务端,debug的过程中服务端不会处理其他请求,并且不会对数据做回滚操作

使用同步模式执行之前的脚本

$ redis-cli --ldb-sync-mode --eval /tmp/script.lua list_a list_b
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 src = KEYS[1]

此时我们我们在另外一个客户端执行任何命令都会被阻塞,直到脚本执行完毕,接下来我们使用continue命令让脚本执行完。然后查看list_alist_b的数据

$ lrange list_a 0 -1
(empty array)
$ lrange list_b 0 -1
1) "a"
2) "b"
3) "c"
4) "d"

可以看到,list_a中的元素已经移动至list_b中了,说明在同步模式下不会对脚本操作进行回滚

断点

添加断点

Lua脚本中添加和删除端点都非常简单,可以在LDB中,通过break <line>添加端点,比如我们想在脚本的第五行添加断点,只需要在LDB执行break 5即可

$ redis-cli --ldb --eval /tmp/script.lua list_a list_b
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 src = KEYS[1]
lua debugger> break 5
   4
  #5   while count > 0 do
   6       local item = redis.call('rpop',src)

添加完断点后,可以使用continue命令,让脚本执行到断点处

lua debugger> continue
* Stopped at 5, stop reason = break point
->#5   while count > 0 do

break后面可以跟多个行号,一次性添加多个断点,行号之间用空格隔开

查看断点

使用break命令可以查看脚本中设置的所有断点

lua debugger> break
1 breakpoints set:
->#5   while count > 0 do

通过LDB返回的信息,可以看到脚本中共设置了一个断点,且行号为 5

删除端点

断点可以使用break -<line>进行删除,其中<line>为要删除的断点的行号,如删除第五行的断点可以使用break -5

lua debugger> b -5
Breakpoint removed.

同样的,可以使用break -1 -2的方式批量删除多个断点

如果想一次性删除脚本中的所有断点,可以使用break 0

lua debugger> b 0
All breakpoints removed.

动态添加断点

除了使用break命令设置断点外,还可以通过在脚本中调用redis.breakpoint()函数的方式设置断点,当脚本执行到redis.breakpoint()方法时,会在redis.breakpoint()的下一行模拟一个断点,跟使用break设置断点一样的效果,比如想在/tmp/script.lua的 while 循环里面设置断点,且当count==3时才执行进行debug,可以将脚本修改为

local src = KEYS[1]
local dst = KEYS[2]
local count = redis.call('llen',src)

while count > 0 do
    if (count == 3) then redis.breakpoint() end
    local item = redis.call('rpop',src)
    redis.call('lpush',dst,item)
    count = count - 1
end

return redis.call('llen',dst)

使用LDB对脚本进行 debug

$ redis-cli --ldb --eval /tmp/script.lua list_a list_b
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 src = KEYS[1]
lua debugger> continue
* Stopped at 7, stop reason = redis.breakpoint() called
-> 7       local item = redis.call('rpop',src)

可以看到,当执行contiue时,脚本停在了第 7 行,在这里,我们可以使用print命令打印count的值

lua debugger> print count
<value> 3

发现count的值为 3,证明redis.breakpoint()命令生效了,且通过该方法,可以是我们动态的设置断点,当满足一定条件的情况下才进入断点

控制台输出

在平常开发中,我们习惯使用控制台输出的方式来打印一些提示信息(如变量值等),比如Java中,我们可以使用System.out.print()函数将信息输出到控制台。在Redis Lua中,也提供了向控制台输出信息的方法redis.debug()redis.debug()可以接收多个参数,参数之间用逗号隔开

比如下面脚本

local a = {1,2,3}
local b = false
redis.debug(a,b)

使用LDB执行该脚本

$ redis-cli --ldb --eval /tmp/script.lua
 Stopped at 1, stop reason = step over
-> 1   local a = {1,2,3}
lua debugger> continue
<debug> line 3: {1; 2; 3}, false

可以看到,当执行到redis.debug(a,b)的时候,控制台输出了变量a跟变量b的值

常用命令介绍

LDB中,输入help命令,可以查看LDB支持的所有命令

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]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 like if there was a breakpoint in the
                     next line of code.

这里命令的说明写的很清楚,其中有一些命令在上面也有用过,下面对其他的指令进行介绍

step | next

step命令在快速开始有演示过,next命令是step的别名,使用stepnext命令,可以执行当前行的代码,并且停在下一行

list [line]

查看line行周围的脚本代码,特别地,当line=0或不指定时,表示查看当前位置的行附近的代码

lua debugger> list
-> 1   local a = {1,2,3}
   2   local b = false
   3   redis.debug(a,b)

eval <code>

执行Lua脚本

lua debugger> eval redis.call('ping')
<retval> {["ok"]="PONG"}

redis <cmd>

执行redis命令

lua debugger> redis ping
<redis> ping
<reply> "+PONG"

abort

结束脚本执行,在同步模式下,执行过的脚本对数据的修改会被保留下来,即不会回滚

print

print命令可以打印出脚本中的变量的值

lua debugger> list
   1   local a = {1,2,3}
   2   local b = false
-> 3   redis.debug(a,b)
lua debugger> print a
<value> {1; 2; 3}

print后面不指定变量名的时候,会打印所有变量及其值

lua debugger> list
   1   local a = {1,2,3}
   2   local b = false
-> 3   redis.debug(a,b)
lua debugger> print
<value> a = {1; 2; 3}
<value> b = false

命令缩写

LDB在设计命令的时候,每个不同的命令的首字母都不一样,因此可以使用首字母缩写的方式代替命令全拼,如print命令,可以使用其首字母p代替