суббота, сентября 13, 2014

Используем Lua чтобы сделать multi-get для хешей в Redis

На нашем текущем проекте в Tradier мы активно используем хеши в Redis. Нам очень нравиться универсальность Redis - разные типы данных создают широкий диапазон возможностей. И нас впечатляет поведение Redis, когда приходится быстро писать много данных. При этом впечатление от быстрой записи несколько меркнет как только приходится делать мулти чтение .

Используя Redis Ruby Gem мы начали с конвейерных (pipilined) запросов. Pipiline запросы в Redis возвращают массив с результатами всех операций в конвейере:
data = {}

$redis.pipelined do
  keys.each do |key|
    data[key] = $redis.hgetall(key)
  end
end

data.each do |key,value|
  data[k] = v.value
end
Такой подход оказался весьма медленным, как только мы стали работать с большими наборами ключей. Очень хотелось чего-то похожего на multi-get в Memcached. И использование Lua скриптов в Redis выглядело самой интересной альтернативой любым других решениям, чтобы сделать такую фичу. Не особо работая с Lua мы очень удивились тому, как много можно сделать с использованием Lua скриптов. Используя Lua мы можем за один запрос к Redis получить все ключи, которые нам нужны:
local collate = function (key)
  local raw_data = redis.call('HGETALL', key)
  local data = {}

  for idx = 1, #raw_data, 2 do
    data[raw_data[idx]] = raw_data[idx + 1]
  end

  return data;
end

local data = {}

for _, key in ipairs(KEYS) do
  data[key] = collate(key)
end
Код реально простой. Мы можем пройтись по переданным в функцию ключам и собрать данные из разных хешей в одну кучу. Проблемы начались, когда мы стали отправлять данные обратно в ruby. Оказалось, что объекты в Lua не всегда просто сериализуются в объекты Ruby. Реализация Lua в Redis позволяет использовать сериализацию в cjson и cmsgpack. Нам надо просто вернуть всё назад:
-- return json
return cjson.encode(response)

-- return messagepack
return cmsgpack.pack(data)
Выбирая между pipelined запросами, lua + json и lua + messagepack, последний вариант оказался самым быстрым. Наша финальная реализация:
require 'redis'
require 'redis'
require 'msgpack'

keys = %w(FOO BAR BAZ)

lua_msgpack_loader = <<LUA
local collate = function (key)
  local raw_data = redis.call('HGETALL', key)
  local hash_data = {}

  for idx = 1, #raw_data, 2 do
    hash_data[raw_data[idx]] = raw_data[idx + 1]
  end

  return hash_data;
end

local data = {}

for _, key in ipairs(KEYS) do
  data[key] = collate(key)
end

return cmsgpack.pack(data)
LUA

redis = ::Redis.new(:driver => :hiredis)
data = MessagePack.unpack(redis.eval(lua_msgpack_loader, :keys => keys))
И конечно же результаты тестов для 10к ключей:
                     user     system      total        real
lua + json        0.350000   0.010000   0.360000 (  1.242315)
lua + msgpack     0.260000   0.020000   0.280000 (  1.146377)
redis pipelined   1.070000   0.020000   1.090000 (  1.759858)
В целом, мы были приятно удивлены как Redis и Lua позволяют делать классные вещи!

Оригинал на английском языке: http://stdout.tradier.com/development/2014/07/10/using-lua-to-implement-multi-get-on-redis-hashes.html?utm_source=redisweekly&utm_medium=email#.VBLQO8J_sud
Отправить комментарий