Redis Lua 脚本调试器用法说明

本文是 Redis 官方 Lua 脚本调试器文档的中文翻译, 原文为: https://redis.io/topics/ldb

概述

从 Redis 3.2 版本开始, Redis 将内置一个完整的 Lua 调试器, 它的存将会让编写复杂的 Lua 脚本变得容易。

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

  • LDB 使用服务器-客户端模型, 它是一个远程调试器。 Redis 服务器将被用作调试服务器, 而默认的调试客户端则是 redis-cli 。 另一方面, 其他客户端也可以通过实现服务器提供的简单协议来让自己成为调试客户端。

  • 在默认情况下, 每个调试回话都是一个子进程回话。 这意味着在调试 Lua 脚本的时候, 服务器不会被阻塞并且可以继续进行开发, 又或者同时执行多个调试回话。 这也意味着在调试结束之后, 被调试脚本造成的修改都会被回滚, 因此用户可以随时重启一个新的调试回话, 并在与原来一模一样的数据集上进行调试。

  • 如果有需要的话, 用户也可以选择同步调试模式, 这个模式将不会创建子进程, 因此调试过程中对数据库进行的所有修改都会被保留, 并且服务器在整个调试过程中都会被阻塞。

  • 支持单步调试。

  • 支持静态和动态断点。

  • 支持将被调试的脚本载入至调试终端。

  • 支持对 Lua 变量进行视察。

  • 支持追踪脚本执行的 Redis 命令。

  • 支持以美观样式打印 Redis 值以及 Lua 值。

  • 能够在无限循环以及长时间执行的步骤中模拟出一个断点。

使用教程

学习如何使用 Lua 调试器的其中一个简单的方法, 就是观看以下视频教程: http://www.bilibili.com/video/av9437433/

Note

请使用开发服务器而不是生成服务器来进行调试。 另外别忘了, 使用非默认的同步调试模式将导致服务器在整个调试过程中都会被阻塞。

用户可以通过以下步骤来开启一个新的调试会话:

  1. 使用编辑器创建你的脚本。让我们假设你的脚本位于 /tmp/script.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 服务器。

进入了调试模式的 redis-cli 将提示用户使用以下三个命令:

  • quit —— 结束调试回话。 调试器将移除所有断点, 跳过所有未执行语句, 并最终退出 redis-cli 。

  • restart —— 重新载入脚本文件, 并重头开始一个新的调试会话。 用户在调试的过程中, 通常会在调试之后对脚本进行修改, 然后通过执行 restart 来对修改后的脚本继续进行调试, 这个步骤一般会迭代发生多次。

  • help —— 打印出可用的调试命令。

lua debugger> help
Redis Lua debugger help:
[h]elp               打印这个帮助
[s]tep               运行当前行然后再次停止
[n]ext               step 的别名,作用相同
[c]continue          运行直到遇到下个断点为止
[l]list              列出当前行附近的代码
[l]list [line]       列出指定行 line 附近的代码
                     line = 0 代表列出当前行附近的代码
[l]list [line] [ctx] 列出位于行 line 附近的 ctx 行代码
[w]hole              列出整个脚本源码,相当于执行 'list 1 1000000'
[p]rint              打印出所有局部变量
[p]rint <var>        打印出指定的局部变量,也可以用于打印全局变量 KEYS 以及 ARGV
[b]reak              列出所有断点
[b]reak <line>       将断点添加至指定行
[b]reak -<line>      移除指定行的断点
[b]reak 0            移除所有断点
[t]race              打印回溯链条(Show a backtrace
[e]eval <code>       在不同的调用幁中执行指定的 Lua 代码
[r]edis <cmd>        执行给定的 Redis 命令
[m]axlen [len]        Redis 的回复以及 Lua 变量转储(dump)截断至指定的长度。
                     将参数 len 的值设置为 0 表示不对长度进行限制。
[a]abort             停止执行脚本。
                     在同步模式下,对数据库的修改将被保留。

以下是两个可以在 Lua 脚本中进行调用的调试函数:
redis.debug()        在调试终端中输出日志。
redis.breakpoint()   暂停脚本的执行,就像遇到了一个断点一样。

需要注意的是, 在默认情况下, 调试器在启动之后将处于单步调试模式。 调试器会停在脚本第一行具有实际作用的代码之前, 然后等待用户的指令。

这时, 用户可以通过执行 step 命令来让调试器执行当前行的代码, 并移动到下一行具有实际作用的代码之前。 在执行 step 命令时, 服务器将会展示出服务器执行的所有命令, 就像这样:

* 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 服务器因为任何原因而断开连接时, 调试会话也会终止。

当服务器关闭时, 所有子进程调试会话都会被终止。

调试命令的缩写

因为调试通常是一个繁重的重复性任务, 所以每个 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 在默认情况下将使用子进程来创建调试会话, 并且在调试完成之后, 脚本对数据库进行的任何修改都将会被回滚。 因为后续的调试会话不需要重置数据库就可以直接启动, 所以这种做法通常来说都是合理的。

但是在一些特殊情况下, 为了追踪特定的 bug , 用户可以会想要保留每个调试会话对数据库所做的修改。 想要这么做的用户可以在启动调试器时, 向 redis-cli 客户端给定 ldb-sync-mode 选项:

redis-cli --ldb-sync-mode --eval /tmp/script.lua

注意, 运行在这一调试模式下的服务器在进行调试的过程中将不可用, 所以请小心使用这一选项。 当处于这一模式时, abort 命令可以在中途停止那些已经对数据库进行过修改的脚本。 使用 abort 命令来终止调试会话与正常地终止调试会话是不同的: 如果用户只是简单地用 CTRL + C 来停止 redis-cli , 那么调试会话将在整个脚本都执行完毕之后终止; 而 abort 则会中途停止脚本并在有需要时启动一个新的调试会话。

在脚本中进行日志记录

redis.debug() 函数是一个非常强力的调试手段, 它可以在 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() 函数将不会引起任何效果。 另外需要注意的是, redis.debug() 可以接受多个参数, 这些参数在输出中将由逗号以及空格进行分隔。

为了让值可以更为直观地展示给正在调试脚本的程序员, 表格以及嵌套表格将以正确的方式(displayed correctly)进行展示。

调试客户端

LDB 使用客户端-服务器模型, 作为调试服务器的 Redis 服务器使用 RESP 进行通讯, 而 redis-cli 则作为默认的调试客户端。 与此同时, 所有客户端只要满足以下条件的任意一个, 就可以用于调试:

  1. 客户端提供了原生的接口, 用于设置调试模式以及控制调试会话。

  2. 客户端提供了通过 RESP 发送任意命令的接口。

  3. 客户端允许向 Redis 服务器发送原始消失(raw message)。

比如说, ZeroBrane StudioRedis plugin 就通过 redis-lua 集成了 LDB 。 以下这个简单的示例演示了这个插件是如何完成这一工作的:

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()
黄健宏
2017.3.28