Статьи

Седьмой принцип Redis: мы оптимизируемся для радости


Седьмой принцип, изложенный в  Манифесте Redis , действительно совпадает с моими убеждениями и взглядами, и, вероятно, убеждениями всех инженеров (независимо от опыта). В этом мире не так много удовлетворения, как этот порыв волнения, который вы получаете, изобретая умный способ сделать что-то новое и / или лучшее. Эта радостная спешка, в свою очередь, может и часто приводит к развитию серьезной зависимости, как признают Дональд Кнут («преждевременная оптимизация — корень всего зла») и Рэндалл Манро (например,  http://xkcd.com/1205/http://xkcd.com/1445/  и  http://xkcd.com/1319/ ) давно.

Однако я обнаружил, что, используя здравый смысл и холодную логику каждый раз, когда у меня возникает желание начать оптимистический квест, я могу поддерживать свою зависимость удовлетворенной и контролируемой. Этот пост о трех таких побуждениях из прошлой недели.

оптимизация путем обмена знаниями

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

Так что именно во время моего ежедневного патрулирования интернетов я наткнулся на этот  ТАК вопрос  и начал на него отвечать. В своем ответе я сознательно пропустил подробности о том, как можно было бы сканировать большой список — ОП выглядел как новичок, и я не думал, что он это ухитрится, — но я все еще получал эту радость, потому что я помог кому-то сделать что-то лучше. Надеюсь, я также посоветовал ему изучить и понять команды, которые он использовал, прежде чем погрузиться в кодирование (и, возможно, я также сэкономил миру некоторые ресурсы процессора и сети на этом пути).

оптимизация путем разведки

Хотя этот ТАК вопрос сам по себе не является чем-то особенным — или блогом (хотя я только что сделал) — он заставил меня задуматься об этой недостающей команде LSCAN. Есть  SCAN  для сканирования пространства ключейHSCAN  для хэшейSSCAN  для наборов и  ZSCAN  для отсортированных наборов, но ничего для списков. Так как списки Redis’ (и  списки быстрого ) являются регулярными списки (или связанные списки ziplists), случайным образом доступ к их элементам делается в O (N) сложности. Наличие эффективной команды LSCAN означает реализацию списков Redis по-разному, и поскольку бесплатных обедов не существует, нужно платить. Списки хороши (то есть O (1)) для всплывающих окон и толчков, и даны принципы # 3 ( «Фундаментальные структуры данных для фундаментального API»)) и № 5 ( «Мы против сложности» ) Манифеста, если вы используете Список и хотите сканировать его с одного конца на другой, то, вероятно, вам не следовало использовать Список в первую очередь.

Но, ради аргумента и просто для хихиканья, можете ли вы как-то сделать то, что сделал бы LSCAN (если бы он был там в первую очередь)? Интересный вопрос … ну, конечно, вы можете. Для начала, если вас не беспокоит согласованность из-за параллелизма, вы можете легко использовать цикл с RPOPLPUSH  и вести подсчет для более эффективного обхода списка. Конечно, если параллелизм является проблемой, вам, вероятно, нужно найти способ дублировать список и безопасно пройти копию.

Так как же дублировать список с 10 миллионами элементов? Давайте сначала создадим такой список (все тесты выполняются в ВМ на ноутбуке, поэтому YMMV):

foo@bar:~$ redis-benchmark -r 10000000 -n 10000000 -P 1000 lpush L __rand_int__
====== lpush L __rand_int__ ======
10000000 requests completed in 1.30 seconds
…
814929.50 requests per second




foo@bar:~$ redis-cli llen L
(integer) 10030000

Быстрое копирование необходимо будет выполнить на сервере, поэтому наивным способом копирования списка будет написание сценария Lua, который будет извлекать элементы один за другим и возвращать их обратно в оригинал и в место назначения. Я написал такой сценарий и проверил его в этом длинном списке …

-- @desc: copies a list with POP and PUSH
-- @usage: redis-cli --eval copy_list_with_popnpush.lua <source> <dest>




local s = KEYS[1]
local d = KEYS[2]
local l = redis.call("LLEN", s)
local i = tonumber(l)




while i > 0 do
local v = redis.call("RPOPLPUSH", s, s)
redis.call("LPUSH", d, v)
i = i - 1
end




return l

Это заняло целую вечность, и  в журнале появилось ожидаемое сообщение  «Lua медленный скрипт обнаружен» :

foo@bar:~$ time redis-cli --eval copy_list_with_popnpush.lua L T
(integer) 10030000




real	0m23.579s
user	0m0.000s
sys	0m0.006s

23 секунды — это слишком долго, и хотя вы можете выполнять некоторые вызовы LPUSH, возможно, LRANGE  — лучший подход:

-- @desc: copies a list with LRANGE
-- @usage: redis-cli --eval copy_list_with_lrange.lua <source> <dest>




local s = KEYS[1]
local d = KEYS[2]
local i = tonumber(redis.call("LLEN", s))
local j = 0




while j < i do
local l = redis.call("LRANGE", s, j, j+99)
redis.call("LPUSH", d, unpack(l))
j = j + 100
end
foo@bar:~$ redis-cli del T
(integer) 1
foo@bar:~$ time redis-cli --eval copy_list_with_lrange.lua L T
(integer) 10030000




real	0m11.148s
user	0m0.000s
sys	0m0.004s

11 секунд намного лучше, чем 23, но как насчет  сортировки — это будет быстрее? Возможно, давайте проверим это:

foo@bar:~$ redis-cli del T
(integer) 1
foo@bar:~$ time redis-cli sort L by nosort store T
(integer) 10030000




real	0m2.390s
user	0m0.000s
sys	0m0.003s

Впечатляющее улучшение — всего 2,248 секунды и примерно в 10 раз быстрее, чем хлопать и толкать, но можем ли мы сделать еще лучше? Позвольте мне проверить, есть ли в Redis какие-либо команды COPY, DUPLICATE или CLONE… Нет, самое близкое, что есть, —  MIGRATE , но он принимает только одно имя ключа. Но ждать! [лампочка] А как насчет  DUMP  и его дополнительного  ВОССТАНОВЛЕНИЯ … это может быть?

-- @desc: The fastest, type-agnostic way to copy a Redis key
-- @usage: redis-cli --eval copy_key.lua <source> <dest> , [NX]




local s = KEYS[1]
local d = KEYS[2]




if redis.call("EXISTS", d) == 1 then
if type(ARGV[1]) == "string" and ARGV[1]:upper() == "NX" then
return nil
else
redis.call("DEL", d)
end
end




redis.call("RESTORE", d, 0, redis.call("DUMP", s))
return "OK"
foo@bar:~$ redis-cli del T
(integer) 1
foo@bar:~$ time redis-cli --eval copy_key.lua L T
"OK"




real	0m1.661s
user	0m0.000s
sys	0m0.007s

Это действительно хороший трюк — копирование с помощью дампирования и восстановления происходит намного быстрее, потому что вы обходите всю логику управления структурой данных (вроде как хороший  старый POKE  в BASIC, и самое далекое, что вы можете пройти, пока в Redis не введены указатели на значения: П). Это почти в 20 раз лучше, чем наивный подход, и теоретически он должен работать для любого типа данных — списков, хэшей, наборов и отсортированных наборов. Мне нужно запомнить этот небольшой взлом, когда мне нужно быстро скопировать значения.

оптимизация путем взлома

Но держись! Я могу даже подумать о чем-то более неприятном — как насчет того, чтобы Redis работал быстрее, чем Redis? Что если кто-то напишет фрагмент кода, который может выпустить Redis-совместимые сериализации значений, как это делает DUMP? С этой функциональностью вы можете действительно быстро загружать данные в Redis. Если не пытаться записывать данные непосредственно в пространство памяти процесса Redis, этот подход потенциально может превзойти любой обычный клиент на основе RESP, перенеся часть нагрузки Redis на самого клиента. Захватывающе!

Я знаю, по крайней мере, один такой код, так что теперь я действительно испытываю желание посмотреть на реализацию DUMP [вставить ссылку] и посмотреть, смогу ли я разорвать его и перенести, скажем, для Hashmap. Тогда я мог бы проверить загрузку JSON, предварительно обработав и введя их окончательную сериализацию напрямую вместо использования HMSET. Возможности действительно бесконечны, но столько же нужно усилий, чтобы это произошло. И даже это работает и относительно без ошибок (да, верно), я буду полагаться на внутренний частный API Redis, так что вероятность поломки огромна. И эти проблемы в стороне, это просто  кажется  неправильным. Возможно, лучшим решением будет встроить Redis в клиентское приложение и реализовать репликацию Master <-> Master для синхронизации между локальным и удаленным экземплярами …

Так что я останавливаюсь здесь, оставляя мой ход мыслей все еще на своем пути, и большая часть моих диких оптимизационных фантазий остается невыполненной. Я сказал вам, что могу контролировать свою склонность к радости, здравый смысл наконец-то потянул аварийный тормоз, и это доказательство. Но, тем не менее, на днях Сальваторе сказал мне (совсем по другому вопросу), что:  «ломать вещи — это хорошо, квинтэссенция взлома :-)» , так что, возможно, я должен…

Вопросов? Обратная связь? Не стесняйтесь, чтобы  написать  или  написать  мне — я очень доступен 🙂