2010-07-09

Новости Redis и cl-redis

Наконец, дошли руки обновить наш клиент для Redis и доработать его для поддержки новых режимов, которые добавились в последних версиях БД. Заодно и проверить утверждения из статьи о том, что клиент тривиально расширяемый и легко адаптируется к изменениям.

На самом деле, пришлось немного повозиться. Но не потому, что утверждения ошибочны, а из-за стандартной проблемы подавляющего большинства софтверных проектов — неадекватной документации. Впрочем, в случае с Redis'ом все не однозначно. С одной стороны, в общем, документация хорошая и правильная: во-первых, не перегруженная, во-вторых, описан протокол взаимодействия, и для каждой команды более-менее указано, как она использует протокол (а еще и другие полезные вещи: иногда примеры использования, а также, что мне понравилось, временные характеристики). Но, в том то и дело, что "более-менее" указано, и некоторые важные моменты упущены, так что в этот раз пришлось лезть в код родного клиента redis-cli и даже прослушивать его взаимодействие с сервером tcpdump'ом (жалко, что я не додумался с этого начать :)

Короче говоря, у Redis появилась поддержка хеш-таблиц, но обнаружился баг у всех команд, которые передают ключи (в терминах Redis — "поля") в таблице (самый простой пример: HGET table field). Почему этого не заметили разработчики? Видимо, потому, что родной клиент использует для общения с сервером не описанный протокол с разными вариантами передачи команд: inline, bulk и т.п.,— а простой унифицированный способ в форме multi-bulk, т.е. вместо строки "HGET table field", которую посылал наш клиент, вот что:
"*3
$4
HGET
$5
table
$5
field"

Соответственно, наверно, и не заметили особенность при обработке полей в случае передачи команды в inline-форме.

Справедливости ради, нужно сказать, что если порыться в документации, то можно найти замечание, что "A good client library may implement unknown commands using this command format in order to support new commands out of the box without modifications." (т.е. самый продуктивный путь был — снова перечитать спецификацию протокола :) Теперь вот, задумался, может стоит перевести все команды на эту multi-bulk форму. И API упростится, будет не:
(def-cmd HGET (key field)
"Retrieve the value of the specified hash FIELD."
:generic :bulk)
а
(def-cmd HGET (key field) :bulk
"Retrieve the value of the specified hash FIELD.")
С другой стороны, немного уменьшиться производительность.

Вообще, Redis все в большем количестве мест использует multi-bulk форму (поскольку она наиболее общая). Поддержка PubSub сделана также на ней, хотя и с небольшим отклонением.

PubSub — это сейчас такой горячий пирожок, который все хотят съесть. Как это реализовано здесь? Есть команды SUBSCRIBE и PSUBSCRIBE, позволяющие подписаться на каналы соответственно по имени и по шаблону (типа "news.*"). И есть PUBLISH, которая посылает сообщения. Сообщения доставляются условно мгновенно и приходят в рамках тех активных соединений, в которых произведена подписка. Таким образом, типичный подход к применению этого, насколько я понимаю — это когда у нас есть отдельная нить-слушатель, которая обрабатывает приходящие сообщения и иницирует какую-то реакцию в программе. Где-то так:
(with-connection ()
(red-subscribe "chan")
(loop
(let ((msg (expect :multi)))
(bt:make-thread (lambda ()
(process (elt msg 2))))
;; или же, например
(bt:with-lock-held (*lock*)
(push (elt msg 2) *internal-queue*)))))
Сообщения имеют вид '("message" <канал> <сообщение>) для простой подписки и '(<шаблон> <канал> <сообщение>) для PSUBSCRIBE. Вот, собственно, и всё.

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

Redis действительно быстро развивается и много чего меняется, добавляются радикально новые варианты использования: в данном случае транзакции и PubSub. Но для поддержки всего этого в клиенте хватает точечных изменений на уровне реализации протокола, никакого рефакторинга. В этот раз нужно было добавить еще несколько вариантов ожидания ответа: :queued (для транзакций), при котором считывается сразу несколько разнородных ответов подряд; :float; а также :pubsub,— поменять несколько определений команд, потому что поменялась сама их спецификация. Ну и добавилась обработка особого случая транзакций, когда любая команда возвращает "QUEUED" вместо своего стандартного ответа.

PS. Да, и еще про транзакции: теперь, как видите, Redis и их поддерживает. Что меня заинтересовало — это обсуждение гарантий целостности (на этой же странице внизу), которые, на первый взгляд, недостаточны: нет условия успешного завершения всех команд в рамках транзакции, чтобы транзакция была признана успешной. Т.е. ROLLBACK не предусмотрен. Но вот, что пишет на этот счет Сальваторе Санфилиппо:
I think you are missing the point about MULTI/EXEC, that is, a Redis command only fails on syntax error or type mismatch. That is, in code without errors the transaction will either be executed as whole or nothing. Still it is true that if there are programming errors like using a list command against a set, or a syntax error, only the sane commands will be performed and the others instead will fail, but it's very hard for this to happen in production.

Так что транзакции в Redis кислотные по-своему, и нужно хорошо уяснить для себя их семантику, прежде чем браться применять. (И опять, возвращаясь к тому, с чего я начинал: встает проблема адекватности документации. А идеальная документация — исполняемая... ;)

9 comments:

  1. Кстати, а не рассматривали возможность использовать iolib вместо usocket?

    ReplyDelete
  2. @archimag
    Я, честно говоря, только недавно с ним ознакомился (и не далее, как вчера, прочитал туториал). Я правильно понял, что там собственный интерфейс к сокетам, минуя SB-BSD-SOCKET? Потому что в процессе долгой эксплуатации cl-redis начали вылезать интересные race condition'ы в SBCL-части. В таком случае смысл переписать однозначно есть.

    ReplyDelete
  3. @Vsevolod
    Да, там идёт прямая работа с C API посредством CFFI.

    ReplyDelete
  4. @archimag
    Перешел на iolib в версии 1.6. Погоняю немного в моих экстремальных условиях.

    ReplyDelete
  5. А iolib какой версии? Просто стабильная и версия из git отличаются довольно существенно.

    ReplyDelete
  6. @archimag
    Я взял 0.6 (т.е. стабильную). А что, есть разница в API для git-версии? Там, на самом деле, совсем немного поменять пришлось. Единственный момент, который у меня вызвал вопрос — это семантика unread-char: почему-то после этого в моем случае следующий read-char возвращает #\Newline (http://github.com/vseloved/cl-redis/blob/master/redis.lisp#L234). В то же время для обычного теста (http://lisper.ru/apps/format/143) всё нормально...

    ReplyDelete
  7. @Vseloved
    Насчёт API не помню, там особо-то меняться нечему, а вот с поведением могу быть существенные изменения. Во-первых, сейчас везде в iolib в настройках дескрипторов используется неблокирующий ввод/вывод, во-вторых, было очень много переписано. Попадались досадные баги. В общем, надо тестировать.

    ReplyDelete
  8. @archimag
    А когда будет следующая стабильная версия?

    ReplyDelete
  9. > А когда будет следующая стабильная версия?

    Сам хотел бы знать :) Но это будет событие!

    ReplyDelete