为什么Redis中Lua执行速度缓慢?有解决方法吗?

5

我正在评估在redis中使用lua脚本,它们似乎有点慢。我进行了如下基准测试:

  1. 对于非lua版本,我简单地执行了1M次SET key_i val_i
  2. 对于lua版本,我做了同样的事情,但是在脚本中:EVAL "SET KEYS[1] ARGV[1]" 1 key_i val_i

在我的笔记本电脑上测试,lua版本比非lua版本慢3倍左右。我知道lua是一种脚本语言,没有编译等等,但这似乎是很多性能开销吗?这是正常现象吗?

假设这确实是正常现象,是否有任何解决方法?是否可以以更快速度实现脚本语言,例如C(redis是用C编写的),以实现更好的性能?

编辑:我正在使用此处的go代码进行测试:https://gist.github.com/ortutay/6c4a02dee0325a608941


你是怎么做到这两个的?在非Lua版本中,你是如何执行那个循环的?而在Lua版本中,你又是如何执行它的? - Nicol Bolas
我正在使用Go库,只是循环执行多次。这是我的完整测试脚本:https://gist.github.com/ortutay/6c4a02dee0325a608941 - Sam Lee
3个回答

10

问题不在于Lua或Redis,而是在于你的期望值。你正在将一个脚本编译1百万次。没有理由认为这会很快。

Redis中EVAL的目的不是执行单个命令; 您可以自己执行。目的是在Redis本身内部执行复杂逻辑,而不是在本地客户端上执行逻辑。也就是说,您实际上需要在单个EVAL脚本中执行整个一百万次操作集,这将由Redis服务器本身执行。

我不太了解Go,所以无法编写调用它的语法。但我知道Lua脚本的样子是这样的:

for i = 1, ARGV[1] do
  local key = "key:" .. tostring(i)
  redis.call('SET', key, i)
end

把它放进一个Go字符串中,然后将其传递给适当的调用函数,不带关键字参数,并且只有一个非关键字参数,表示循环的次数。


有趣。所以你的意思是说很多开销只是每次编译脚本。我想我期望它在第一次运行后保留一个预编译版本,或者类似的东西。 - Sam Lee
1
Redis只编译一次脚本,因为编译版本被缓存,但如果您想运行一个大型脚本,使用EVALSHA来节省带宽可能是明智的选择。 - maffews
1
EVALSHA 不仅可以节省带宽!它还可以节省计算脚本体上 SHA 的时间。即使是对于小型脚本,SHA 也是相当昂贵的函数,因此最好始终使用 EVALSHA。 - funny_falcon
那么结果是什么?我很感兴趣 :) 我认为 Lua 版本应该更快? - ch271828n

1
我偶然发现了这个讨论串,对基准测试结果也感到好奇。我写了一个简单的Ruby脚本来进行比较。该脚本对同一键使用不同选项进行简单的“SET/GET”操作。
require "redis"

def elapsed_time(name, &block)
  start = Time.now
  block.call
  puts "#{name} - elapsed time: #{(Time.now-start).round(3)}s"
end

iterations = 100000
redis_key = "test"

redis = Redis.new

elapsed_time "Scenario 1: From client" do
  iterations.times { |i|
    redis.set(redis_key, i.to_s)
    redis.get(redis_key)
  }
end

eval_script1 = <<-LUA
redis.call("SET", "#{redis_key}", ARGV[1])
return redis.call("GET", "#{redis_key}")
LUA

elapsed_time "Scenario 2: Using EVAL" do
  iterations.times { |i|
    redis.eval(eval_script1, [redis_key], [i.to_s])
  }
end

elapsed_time "Scenario 3: Using EVALSHA" do
  sha1 = redis.script "LOAD", eval_script1
  iterations.times { |i|
    redis.evalsha(sha1, [redis_key], [i.to_s])
  }
end

eval_script2 = <<-LUA
for i = 1,#{iterations} do
  redis.call("SET", "#{redis_key}", tostring(i))
  redis.call("GET", "#{redis_key}")
end
LUA

elapsed_time "Scenario 4: Inside EVALSHA" do
  sha1 = redis.script "LOAD", eval_script2
  redis.evalsha(sha1, [redis_key], [])
end

eval_script3 = <<-LUA
for i = 1,2*#{iterations} do
  redis.call("SET", "#{redis_key}", tostring(i))
  redis.call("GET", "#{redis_key}")
end
LUA

elapsed_time "Scenario 5: Inside EVALSHA with 2x the operations" do
  sha1 = redis.script "LOAD", eval_script3
  redis.evalsha(sha1, [redis_key], [])
en

我在我的Macbook Pro上运行得到了以下结果

Scenario 1: From client - elapsed time: 11.498s
Scenario 2: Using EVAL - elapsed time: 6.616s
Scenario 3: Using EVALSHA - elapsed time: 6.518s
Scenario 4: Inside EVALSHA - elapsed time: 0.241s
Scenario 5: Inside EVALSHA with 2x the operations - elapsed time: 0.5s

总之:

  • 场景1与场景2表明,往返时间是主要贡献因素,因为场景1向Redis发出了2个请求,而场景2只发出了1个请求,且场景1的执行时间约为场景2的2倍。
  • 场景2与场景3表明EVALSHA确实提供了一些好处,我相信这种好处会随着脚本变得更加复杂而增加。
  • 场景4与场景5表明调用脚本的开销几乎是最小的,因为我们将操作数量翻倍,看到了约2倍的执行时间增加。

那么,您的结论是什么?从客户端执行“EvalSha”吗?这个问题似乎缺少一个结论。 - Just a coder

0

现在有一个解决方法,使用John Sully创建的模块。它适用于Redis和KeyDB,并允许您使用V8 JIT引擎,比Lua脚本运行复杂脚本快得多。https://github.com/JohnSully/ModJS


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接