如果您曾经编写过哪怕一行代码,您就会知道本杰明·富兰克林的著名名言应该修改为
“在这个世界上,没有什么是一定的,除了死亡、税收……以及 BUG。”
软件缺陷是生活中不可避免的一部分,因为软件是由肉体ware制作的,而人类会犯错。即使您是一位优秀的程序员,编写出优秀的代码(或一位优秀的程序员,偷窃代码),使用经过验证的方法和设计模式,仅使用一流的工具,并接受同行代码审查… 尽管您付出了最大的努力,您仍然可能一遍又一遍地撞墙,因为有一个难以捉摸的小妖精。
追踪这些问题并非易事。它需要耐心、努力,并且在许多情况下还需要一丝灵感来正确识别故障的根本原因。当为 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 -+
Redis 的嵌入式 Lua 引擎提供了一个函数,该函数可以将其打印到日志文件(在 redis.conf 中使用 loglevel 和 logfile 指令配置)。此函数名为 redis.log,使用起来非常简单——只需像这样从脚本中调用它即可
redis.log(redis.LOG_WARNING, "foo bar")
redis.log 函数接受两个参数:第一个是消息的日志级别(选择在 LOG_DEBUG、LOG_VERBOSE、LOG_NOTICE 和 LOG_WARNING 之间),第二个参数是要记录的值。有关更多信息,请参阅 EVAL 的文档。
优点:使用日志文件通常是跟踪工作流的最简单方法。此外,您还可以近乎实时地查看记录的消息(即通过尾随日志文件)。
缺点:在某些情况下,这种方法不可行(例如,当您无权访问 Redis 主机或日志过于嘈杂,无法舒适地使用时)。但是,不要绝望,因为剥猫皮的方法不止一种(喵喵!?!)。
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"
优点:在任何地方都能工作,并且需要很少的设置。
缺点:将日志表作为代码的回复返回将阻止它返回任何有意义的内容。这种方法会消耗内存来存储中间日志表,并且您需要等待脚本完成才能获取日志消息。
如果您需要 Lua 代码在执行完成后返回有意义的回复,并且仍然保留日志机制,您可以使用 Redis 的列表数据结构来存储和检索消息。以下代码片段展示了这种方法
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 执行写入操作,因此这不能与非确定性命令一起使用。
发布/订阅 有很多奇妙的用途,但您知道它也可以用于调试吗?让您的脚本将日志消息发布到一个频道,并订阅该频道,您可以跟踪正在发生的事情。以下是如何操作
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"
优点:开销小,实时显示消息。
缺点:发布/订阅的传递不能保证,因此您可能会错过一条有价值的日志消息(但这确实是一个小概率事件)。
当您试图理解一段代码时,向代码中添加跟踪只能帮您走那么远,因为有时候您真正需要/想要一个完整的调试器。来自 @Trikoder 的 Marijan Šuflaj 提供的这个巧妙技巧为您提供了 exactly that – 用于您的 Lua 脚本和 Redis 的调试器。
Marijan 的想法的实质是使用一个免费提供的 Lua 调试器来调试您的 Lua 代码,并通过精简的包装代码添加 Redis 特定的命令。他的博客文章将带您完成实现该壮举的步骤,甚至包括 Redis 特定命令包装器的示例代码。虽然这不能让您在 vivo 中调试代码,但这是您可以最接近的方法,并且就所有实际目的而言都一样好。
优点:为您的 Lua 代码使用完整的调试器。
缺点:需要一定程度的非平凡设置。
MONITOR
和 ECHO
我偶然发现了一种有用的跟踪方法,它与使用日志非常相似,但有一个额外的优势。这个想法是打开一个专门的连接到您的服务器,并运行 MONITOR
命令(请注意,这将返回 Redis 执行的所有命令的流,因此您不想在繁忙的服务器上执行此操作)。设置好监控后,您可以从脚本中调用 ECHO
来跟踪内容(例如,redis.call('ECHO', 'the value of foo is' .. foo))
。额外的优势是,您的监控流还显示在脚本执行的每个其他 redis.call
上,与您的跟踪并排——很酷!
Lua 的 print 命令在 Redis 的脚本中运行良好,您可以随意调用它,并尽情跟踪。但是,它唯一的缺陷是它会将所有内容直接输出到 stdout,因此除非您正在查看服务器的输出,否则您很可能会错过它。
您的代码有很多方法可以(并且会)失败,追踪原因可能是一项艰巨的任务。在为 Redis 开发 Lua 脚本时,我希望您会发现这些方法在消除使代码无法按预期运行的烦人的小动物方面很有用。有疑问?反馈?还有其他技巧、提示或您想看到的内容主题?发送电子邮件 或 发推文给我——我随时可用 🙂