While quietly rolling out the public beta of Adafruit IO, we noticed some CPU spikes in our Node.js processes that handle MQTT. We currently use a slightly modified version of Mosca by @mcollina as our MQTT broker. It is a very solid package, but we seemed to be pushing the limits of Mosca’s Redis persistence module.
Debugging
After examining the output of node --perf
in chrome://tracing, I seemed to have found the culprit. It looked as if Mosca was waiting on Redis to return the result of HKEYS
for retained topics.
v8 QuickSort native array.js:710:26
v8 QuickSort native array.js:710:26
v8 QuickSort native array.js:710:26
v8 QuickSort native array.js:710:26
v8 QuickSort native array.js:710:26
v8 QuickSort native array.js:710:26
v8 QuickSort native array.js:710:26
v8 InnerArraySort native array.js:670:24 {1}
v8 sort native array.js:885:19 {2}
v8 /home/deploy/io/releases/20151202164744/node/node_modules/mosca/lib/persistence/redis.js:220:44
v8 RedisClient.return_reply /home/deploy/io/releases/20151202164744/node/node_modules/redis/index.js:608:47 {2}
v8 /home/deploy/io/releases/20151202164744/node/node_modules/redis/index.js:306:34
v8 doNTCallback0 node.js:416:27
v8 _tickCallback node.js:335:27 {1}
We retain all current values for topics, so they are immediately returned when a user connects to Adafruit IO and subscribes to a topic. This enables the user to return to a previous state on startup without making any calls to the REST API.
Improving Performance with Redis using Lua
In order to improve performance, I decided to do some preprocessing in Lua on the Redis server before returning hash keys to Node.js. MQTT has two standard wildcard characters: +
is a wildcard for matching one topic level, and #
can be used for matching multiple. Given Lua’s limited string pattern matching abilities, I decided to narrow down the results using Lua and do the final matching in Node.js.
Here’s the Lua script:
local cursor = 0
local response = {}
local match
local hscan
repeat
hscan = redis.call('HSCAN', KEYS[1], cursor, 'MATCH', ARGV[1])
cursor = tonumber(hscan[1])
match = hscan[2]
if match then
for idx = 1, #match, 2 do
response[match[idx]] = match[idx + 1]
end
end
until cursor == 0
return cjson.encode(response)
You can call the script from Node.js using Redis EVAL
like this:
'use strict';
// this example is es6, so it will only work in node >=4.0.0.
// it assumes you have a redis hash created with at least topic/1/1
// and topic/2/1 as keys.
const redis = require('redis'),
async = require('async');
const client = redis.createClient(6379, '127.0.0.1');
// syntax highlighting is currently broken for es6 template strings :\
const script = "\
local cursor = 0 \
local response = {} \
local match \
local hscan \
repeat \
hscan = redis.call('HSCAN', KEYS[1], cursor, 'MATCH', ARGV[1]) \
cursor = tonumber(hscan[1]) \
match = hscan[2] \
if match then \
for idx = 1, #match, 2 do \
response[match[idx]] = match[idx + 1] \
end \
end \
until cursor == 0 \
return cjson.encode(response)";
client.eval(script, 1, 'test', 'topic/*/999997', function(err, topics){
if (err) throw err;
console.log('found', topics);
client.end();
});
The HSCAN
wildcard character is *
, so I just replaced the MQTT wildcard characters with *
, and did the final pattern matching in Node.js.
Benchmarks
I was benchmarking the change with 1 million retained hash keys, and it looks like HKEYS
method is about 258% slower than preprocessing with Lua. The results are listed below so you can compare the results of the current HKEYS
method vs the lua script:
created 1000000 hash keys
real 0m23.453s
user 0m11.763s
sys 0m11.694s
-------------------------------------- HKEYS --------------------------------------
retrieved 1000000 hash keys via HKEYS
JS matched topic: 'topic/0.09308264800347388/999997'
got value via HGET: 0.3918832363560796
result {"topic\/0.09308264800347388\/999997":"0.3918832363560796"}
real 0m3.460s
user 0m2.729s
sys 0m0.194s
---------------------------------------- LUA ---------------------------------------
retrieved 1 hash keys via Lua
JS matched topic: 'topic/0.09308264800347388/999997'
result {"topic\/0.09308264800347388\/999997":"0.3918832363560796"}
real 0m1.337s
user 0m0.116s
sys 0m0.024s
The pull request to mosca can be found here: mcollina/mosca#379