2

Here are some tests and results I have run against the redis-benchmark tool.

C02YLCE2LVCF:Downloads xxxxxx$ redis-benchmark -p 7000 -q -r 1000000 -n 2000000 JSON.SET fooz . [9999]
JSON.SET fooz . [9999]: 93049.23 requests per second

C02YLCE2LVCF:Downloads xxxxxx$ redis-benchmark -p 7000 -q -r 1000000 -n 2000000 evalsha 8d2d42f1e3a5ce869b50a2b65a8bfaafe8eff57a 1 fooz [5555]
evalsha 8d2d42f1e3a5ce869b50a2b65a8bfaafe8eff57a 1 fooz [5555]: 61132.17 requests per second

C02YLCE2LVCF:Downloads xxxxxx$ redis-benchmark -p 7000 -q -r 1000000 -n 2000000 eval "return redis.call('JSON.SET', KEYS[1], '.', ARGV[1])" 1 fooz [5555]
eval return redis.call('JSON.SET', KEYS[1], '.', ARGV[1]) 1 fooz [5555]: 57423.41 requests per second

That is a significant drop in performance for something that is supposed to have the advantage of performance for a script running server side verse the client running a script client side.

From client to EVALSHA = 34% performance loss

From EVALSHA to EVAL = 6% performance loss

The results are similar for a NON-JSON insert set command

C02YLCE2LVCF:Downloads xxxxxx$ redis-benchmark -p 7000 -q -r 1000000 -n 2000000 set fooz 3333
set fooz 3333: 116414.43 requests per second

C02YLCE2LVCF:Downloads xxxxxxx$ redis-benchmark -p 7000 -q -r 1000000 -n 2000000 evalsha e32aba8d03c97f4418a8593ed4166640651e18da 1 fooz [2222]
evalsha e32aba8d03c97f4418a8593ed4166640651e18da 1 fooz [2222]: 78520.67 requests per second

I first noticed this when I did an info commandstat and observed the poorer performance for the EVALSHA command

# Commandstats
cmdstat_ping:calls=331,usec=189,usec_per_call=0.57
cmdstat_eval:calls=65,usec=4868,usec_per_call=74.89
cmdstat_del:calls=2,usec=21,usec_per_call=10.50
cmdstat_ttl:calls=78,usec=131,usec_per_call=1.68
cmdstat_psync:calls=51,usec=2515,usec_per_call=49.31
cmdstat_command:calls=5,usec=3976,usec_per_call=795.20
cmdstat_scan:calls=172,usec=1280,usec_per_call=7.44
cmdstat_replconf:calls=185947,usec=217446,usec_per_call=1.17
****cmdstat_json.set:calls=1056,usec=26635,usec_per_call=25.22**
****cmdstat_evalsha:calls=1966,usec=68867,usec_per_call=35.03**
cmdstat_expire:calls=1073,usec=1118,usec_per_call=1.04
cmdstat_flushall:calls=9,usec=694,usec_per_call=77.11
cmdstat_monitor:calls=1,usec=1,usec_per_call=1.00
cmdstat_get:calls=17,usec=21,usec_per_call=1.24
cmdstat_cluster:calls=102761,usec=23379827,usec_per_call=227.52
cmdstat_client:calls=100551,usec=122382,usec_per_call=1.22
cmdstat_json.del:calls=247,usec=2487,usec_per_call=10.07
cmdstat_script:calls=207,usec=10834,usec_per_call=52.34
cmdstat_info:calls=4532,usec=229808,usec_per_call=50.71
cmdstat_json.get:calls=1615,usec=11923,usec_per_call=7.38
cmdstat_type:calls=78,usec=115,usec_per_call=1.47

From JSON.SET to EVALSHA there is ~30% performance reduction which is what I observed in the direct testing.

The question is, why? And, is this anything to be concerned with or is this observation within fair expectations?

For context, the reason why I am using EVALSHA and not the direct JSON.SET command is for 2 reasons.

  1. The IORedis client library doesn't have direct support using RedisJson.

  2. Because of the previous fact, I would have had to use send_command() which then would have sent the direct command over to the server but doesn't work with pipelining while using TypeScript. So I would have had to do every other command separately and forgo pipelining.

  3. I thought this was supposed to be better performance?

****** Update:

So in the end, based on the below answer I refactored my code to only include 1 EVALSHA for the write because it uses 2 commands which are a set and expire command. Again, I can't single this into RedisJson so that is the reason why.

Here is the code for someones reference: Shows evalsha and fallback

await this.client.evalsha(this.luaWriteCommand, '1', documentChange.id, JSON.stringify(documentChange), expirationSeconds)
   .catch((error) => {
        console.error(error);
        evalSHAFail = true;
    });
if (evalSHAFail) {
   console.error('EVALSHA for write not processed, using EVAL');
   await this.client.eval("return redis.pcall('JSON.SET', KEYS[1], '.', ARGV[1]), redis.pcall('expire', KEYS[1], ARGV[2]);", '1', documentChange.id, JSON.stringify(documentChange), expirationSeconds);
   console.log('SRANS FRUNDER');
   this.luaWriteCommand = undefined;                 
Guy Korland
  • 9,139
  • 14
  • 59
  • 106
Christian Matthew
  • 4,014
  • 4
  • 33
  • 43

1 Answers1

4

Why Lua script is slower in your case?

Because EVALSHA needs to do more work than a single JSON.SET or SET command. When running EVALSHA, Redis needs to push arguments to Lua stack, run Lua script, and pop return values from Lua stack. It should be slower than a c function call for JSON.SET or SET.

So When does server side script has a performance advantage?

First of all, you must run more than one command in script, otherwise, there won't be any performance advantage as I mentioned above.

Secondly, server side script runs faster than sending serval commands to Redis one-by-one, get the results form Redis, and do the computation work on the client side. Because, Lua script saves lots of Round Trip Time.

Thirdly, if you need to do really complex computation work in Lua script. It might not be a good idea. Because Redis runs the script in a single thread, if the script takes too much time, it will block other clients. Instead, on the client side, you can take the advantage of multi-core to do the complex computation.

for_stack
  • 21,012
  • 4
  • 35
  • 48
  • Would I be able to pipeline in a Lua script? Or is it just the same to run the additional command in the script separately? Problem with additional command is that I need to load the script from code. How can I line item commands to script load? – Christian Matthew Jan 06 '20 at 05:15
  • pipeline has nothing to do with Lua script. Redis runs all commands in Lua script in a transaction. What do you mean by *How can I line item commands to script load?* – for_stack Jan 06 '20 at 07:55
  • let me rephrase, what I want to do is run the lua script to do 2 things in sequence. 1. do the JSON.SET command and 2. set the expire (JSON.SET doesn't have an attached ex command to it) So would I write 2 local's (redis.call...) and return them? – Christian Matthew Jan 06 '20 at 14:44
  • Here is what I am referring to. which works. `script load "local action1 = redis.call('set','tester', 'rester'); local action2 = redis.call('expire', 'tester', '35'); return action1, action2;"` – Christian Matthew Jan 06 '20 at 18:56
  • 2
    The script lgtm @christia :) – Itamar Haber Jan 06 '20 at 19:08
  • thanks, just wanted to make sure the result is synchronous correct and should be guaranteed to run. – Christian Matthew Jan 06 '20 at 21:04
  • in the end, lol I don't know the amount of commands to "you should run a lua script" is a good ratio but I can say the performance of the evalsha seems weird to me compared to the straight commands. Now that I am doing 2 commands for lua I am noticing the lua command is now taking twice as long. However, perhaps this is better because I am not making the second round trip so I am keeping it. Unless you say something different – Christian Matthew Jan 07 '20 at 20:21
  • 1
    @ChristianMatthew In your case, wrap `JSON.SET` and `EXPIRE` in a Lua script is a good choice. Compared to two straight commands, Lua script should run faster (it saves round trip time), and is atomic. – for_stack Jan 08 '20 at 00:05