dot Redis 8 来了——而且是开源的

了解更多

5 6 7 种跟踪和调试 Redis Lua 脚本的方法

或者:人非圣贤,孰能无过;Debug 乃纠错之母。

所有更新之母:Redis v3.2 具有其自己的 Lua 调试器

更新 1:我用一种更高级的调试方法跟进了这个话题,所以请查看第 2 部分 - redis-lua-debugger 及其附带文章 <- 小更新:rld 不再维护,因为它与 v3+ 不兼容。

如果你曾经写过哪怕一行代码,你就会知道本杰明·富兰克林的著名引言应该修改为

“在这个世界上,没有什么可以确定,除了死亡、税收……和 BUG。”

软件缺陷是生活中的一个事实,因为软件是由人编写的,而人会犯错。即使你是一个编写优秀代码的优秀程序员(或者是一个窃取它的伟大程序员),使用经过验证的方法和设计模式,只使用最佳工具,并提交给同行进行代码审查……尽管你尽了最大努力,你可能会一次又一次地发现自己因为一个难以捉摸的小妖精而撞墙。

追踪这些问题并非易事。它需要耐心、努力,在许多情况下,还需要一点灵感才能正确识别故障的根本原因。在为 Redis 开发 Lua 脚本时(从 2.6 版本开始提供的一个功能),这可能会变得更加棘手。这是因为你的代码在服务器本身内部运行,这使得你更难了解代码的内部结构。为了让这更容易一些(也许可以让你少撞几次墙),这里有五种方法可以让你获得像超人一样的 X 射线视觉来观察你的 Lua 脚本。

Reminder
--------
You can run a Lua script in a file with `redis-cli` in the following manner:

  redis-cli --eval script.lua key1 key2 ... , arg1 arg2 ...
                       ^          ^         ^     ^
    the script's file -+          |         |     |
       keys passed to the script -+         |     |
          a comma separates keys from args -+     |
                  arguments passed to the script -+

1. 使用 Redis 日志

Redis 的嵌入式 Lua 引擎提供了一个函数,可以打印到其日志文件(使用 loglevellogfile 指令在你的 redis.conf 中配置)。这个函数,方便地命名为 redis.log,使用起来非常简单——只需像这样从你的脚本中调用它

redis.log(redis.LOG_WARNING, "foo bar")

redis.log 函数接受两个参数:第一个是消息的日志级别(选择在 LOG_DEBUG, LOG_VERBOSE, LOG_NOTICELOG_WARNING 之间),第二个参数是要记录的值。有关更多信息,请参阅 EVAL 的文档。

优点:使用日志文件通常是跟踪你的工作流程的最简单方法。此外,你可以近乎实时地查看已记录的消息(即,通过尾部跟踪日志文件)。

缺点:在某些情况下,这种方法不是一个可行的选择(例如,当你无法访问 Redis 主机或当日志过于嘈杂而无法舒适地工作时)。但是,不要绝望,因为不止一种方法可以解决问题 (meeeeeow!?!)。

2. 使用 Lua 表

Lua 的表是关联数组,并且是该语言的唯一“容器”数据结构。你可以轻松地使用它们以类似日志的方式存储消息,并在你的代码完成运行后返回结果数组。这是一个例子

local logtable = {}

local function logit(msg)
  logtable[#logtable+1] = msg
end

logit("foo")
logit("bar")

return logtable

运行上面的例子会产生以下结果

foo@bar:~$ redis-cli --eval log-with-table.lua
1) "foo"
2) "bar"

优点:在任何地方都有效,并且只需要很少的设置。

缺点:将日志表作为你的代码的回复返回,会阻止它返回任何有意义的内容。这种方法消耗内存来存储中间日志表,并且你需要等待脚本完成才能获得日志消息。

3. 使用 Redis 列表

如果你要求你的 Lua 代码在完成执行后返回有意义的回复,并且仍然保留日志记录机制,你可以使用 Redis 的 List 数据结构来存储和检索你的消息。这是一个显示这种方法的代码片段

local loglist = KEYS[1]
redis.pcall("DEL", loglist)

local function logit(msg)
  redis.pcall("RPUSH", loglist, msg)
end

logit("foo")
logit("bar")

return 42

请注意,脚本的第 2 行删除了日志的键以进行清理。运行此脚本,然后运行 LRANGE 以获取“日志”,结果如下

foo@bar:~$ redis-cli --eval log-with-list.lua log
(integer) 42
foo@bar:~$ redis-cli LRANGE log 0 -1
1) "foo"
2) "bar"

优点:与 Lua 表一样,这很直接并且有效。

缺点:类似于表的方法,并且列表的长度存在任意大小限制 (2^32)。你还需要传递列表的键名以确保集群兼容性,最重要的是,由于你将对 Redis 执行写入操作,因此这不能用于非确定性命令。

4. 使用 Redis 的发布/订阅

发布/订阅 有很多很棒的用途,但是你知道它也可以用于调试吗?通过让你的脚本将日志消息发布到通道并订阅该通道,你可以跟踪正在发生的事情。这是如何做到的

local logchannel = "log"

local function logit(msg)
  redis.pcall("PUBLISH", logchannel, msg)
end

logit("foo")
logit("bar")

return 42

在运行此脚本之前,打开另一个终端窗口并订阅你的 log 通道。当脚本运行时,你应该在你的订阅者终端中获得以下输出

foo@bar:~$ redis-cli SUBSCRIBE log
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "log"
3) (integer) 1
1) "message"
2) "log"
3) "foo"
1) "message"
2) "log"
3) "bar"

优点:开销小,实时显示消息。

缺点:发布/订阅的传递没有保证,所以你可能会错过一条具有启发性的日志消息(但这确实是一个很小的可能性)。

5. 使用 Lua 调试器

当你试图理解一段代码时,向你的代码添加跟踪只能让你走到这一步,因为有时你真的需要/想要一个功能齐全的调试器供你使用。来自 @Trikoder 的 Marijan Šuflaj 提供的这个巧妙的技巧正好为你提供了这个——一个用于你的带有 Redis 的 Lua 脚本的调试器。

Marijan 的想法的要点是使用一个免费提供的 Lua 调试器来调试你的 Lua 代码,并使用精简的包装代码添加 Redis 特定的命令。他的博客文章将带你完成完成这一壮举的步骤,甚至包括 Redis 特定的命令包装器的示例代码。虽然这不能让你完全在 vivo 中调试代码,但它与你可以获得的最近距离一样好,并且在所有实际目的上都一样好。

优点:为你的 Lua 代码使用一个功能齐全的调试器。

缺点:需要一定程度的非平凡设置。

更新 2: 奖励方法 – 使用

MONITORECHO 我偶然发现另一种有用的跟踪方法,它与使用日志非常相似,但有一个额外的优点。这个想法是打开一个到你的服务器的专用连接并运行 MONITOR 命令(请注意,这将返回由 Redis 执行的所有命令的流,所以你不想在一个繁忙的服务器上这样做)。一旦你设置好监控器,你可以从脚本中调用 ECHO 来跟踪内容(例如 redis.call('ECHO', 'foo 的值是' .. foo))。额外的优点是你的监控器流也会显示你的脚本与你的跟踪内联执行的每一个其他的 redis.call – 太棒了!

更新 3: 另一个奖励方法 – 使用 Lua 的 print

Lua 的 print 命令在 Redis 的脚本中运行良好,你可以随意调用它并尽情跟踪。然而,它唯一的问题是它直接将所有内容输出到 stdout,所以除非你正在监视服务器的输出,否则你很容易错过它。

结论

你的代码可能会(并且将会)以多种方式失败,而追踪原因可能是一项艰巨的任务。在为 Redis 开发 Lua 脚本时,我希望你发现这些方法对于消除那些阻止你的代码按你期望的方式运行的讨厌的生物很有用。有问题吗?反馈?你希望看到涵盖的任何其他技巧、提示或主题?电子邮件tweet 我 – 我很容易联系到 🙂