Введение
Следует сказать, что этот пост изначально был опубликован на хабре с помощью товарищей из Mail.Ru. За что им большое спасибо.
Выбор СУБД
В жизни каждого проекта рано или поздно возникает переломный момент, когда нужно выбрать СУБД для хранения всех данных. Наш проект с этой точки зрения простой: пользователи, голосования, ответы, какая-то попутно собираемая информация — всё это прекрасно можно держать в key-value хранилище. Поэтому на старте мы рассматривали три варианта: Redis, Tarantool и MySQL с handlersocket. Фаворитом с самого начала был Redis. Он быстро работает, у него замечательный коннектор для .NET, созданный командой Stack Overflow. К слову, сам Stack Overflow написан на .NET, работает на Windows, у них SQL Server от Microsoft, Redis и ещё много интересного. У Redis прекрасная документация. Если мы нанимаем нового программиста, который никогда не работал с Redis, то мы отправляем его туда — и через три дня он знает примерно всё, что ему нужно знать для использования Redis.
Под вторым номером шёл Tarantool. К сожалению, у него не было такого удобного сайта, как у Redis. Как и коннектора для .NET. По скорости он нас полностью устраивал, так как не сильно отличался от Redis. Если вы включаете write ahead logging, то любая запись окажется на диске. И если у вас не глючит контроллер, диск или прочее железо, то получается вполне надёжно. Также в Tarantool есть вторичные индексы. В некоторых случаях это очень важная фича. В Redis её нет, приходится делать вручную.
Handlersocket оказался аутсайдером. Он медленнее, чем Redis и Tarantool. Зато доступна ACID-модель, если ваш движок в MySQL её поддерживает. Доступна вся инфраструктура от MySQL: репликация, мониторинг, эксплейн, бэкапы. Можно строить сложные отчёты на обычном SQL. Коннектора под .NET тоже не было.
В результате мы выбрали Redis и запустили его в свою закрытую альфу. Но через какое-то время выяснилось, что отсутствие вторичных индексов — более серьёзная проблема, чем мы думали. Если нужно выбрать все голосования, созданные каким-либо автором, то приходится заводить дополнительные списки для хранения (можно, конечно, делать полный перебор, но это не наш случай). Есть риск, что в результате разных сбоев данные станут неконсистентными. Попытки исправить это через Lua-скрипты или транзакции привели к тому, что работа Redis замедлилась примерно в три-четыре раза, что перестало нас устраивать. Это не проблема самого Redis, а следствие его нецелевого использования, на мой взгляд.
Tarantool и Windows
Тогда мы решили перейти на Tarantool. И перед нами встали сразу две проблемы. У Tarantool до сих пор нет бинарника под Windows (и неизвестно, когда появится) — раз. Не было коннектора для .NET — два. Вообще говоря, это спорный вопрос, можно ли назвать недостатком отсутствие версии нашего хранилища под Windows. Я считаю это преимуществом. Ведь в production будет, скорее всего, Linux, а отсутствие версии под Windows заставит программиста разбираться в том, как всё работает на самом деле. Программист для отладки и мониторинга будет пользоваться теми же инструментами, что и в production. По моим наблюдениям, это повышает вероятность написания качественного кода без ошибок и уменьшает время простоя в случае катастроф.
Первую проблему мы решили использованием Docker for Windows для разработки. А вот вторая проблема была посложнее.
Коннектор
Поскольку готового коннектора для Tarantool не существовало, мы написали свой. Для этого пришлось решить две задачи. Первая: реализовать сериализацию и десериализацию в msgpack, так как это формат обмена данных в Tarantool. У нас она была решена в рамках проекта MsgPack.Light, так как мы тоже храним данные, упакованные в msgpack. Вторая задача сводилась к буквальной реализации протокола обмена данных с Tarantool iProto. Она решена в рамках проекта progaudi.tarantool.
Мы поддерживаем
- .NET 4.6 и выше,
- новый opensource-фреймворк .NET Core: netstandard 1.4 и выше.
Раньше коннектор назывался tarantool.csharp, но после обратной связи от комьюнити мы его переименовали в progaudi.tarantool. Теперь название не должно вызывать никакой путаницы — можно ли использовать коннектор из F# или нет. Можно было с самого начала.
Благодаря недавним улучшениям в MsgPack.Light, коннектор научился работать с тарантуловским типом данных scalar. В будущем планируется ещё больше упростить работу с коннектором и уйти от явной конвертации объектов пользователя в TarantoolTuple-структуры.
Мы не поддерживаем DDL, потому что он выходит за рамки iProto и должен быть реализован обёртками над EVAL-командой. Лично я считаю, что если программист использует Tarantool, то он должен писать схему в Lua, потому что в таком случае можно шарить её с админами и распространять сразу в работающее окружение. Может быть, я неправ, и мы реализуем возможность делать это из .NET.
На текущий момент у нас есть соединение только с одной нодой Tarantool. Мы не полностью поддерживаем CALL_16, в определённых случаях на стороне Tarantool происходит странная упаковка, и это приводит к ошибкам при десериализации. Новый CALL поддерживается полностью, рекомендуем пользоваться им.
Особенности разработки коннекторов к Tarantool
К сожалению, в Tarantool невозможно попросить сервер выводить все запросы в лог. В некоторых ситуациях это очень мешает при отладке, при этом нам обычно не важна производительность сервера Tarantool. Хотелось бы получить какую-то ручку, которая включает запись всех запросов в лог.
При логировании запросов в лог хочется их как-то отделять друг от друга. Для этого у нас есть connection id (box.session.id) и request id (box.session.sync). К сожалению, box.session.sync расшарен на все запросы в рамках одного соединения, как следствие, он может меняться, если выполнение запроса прерывается из-за достижения yield point (запись в базу, ручная передача управления и так далее). В принципе, это важно только там, где может встретиться несколько точек логирования, например, в хранимых процедурах. В таких случаях следует box.session.sync сохранять до первого yield point в локальную переменную.
Сервер приложений
Важная часть Tarantool — это сервер приложений. К нему существует множество уже готовых модулей, начиная с простых, таких как автоматическая перезагрузка других модулей, заканчивая шардированием, очередями и драйверами к другим СУБД (1, 2 и многое другое). Все модули, которые мы пробовали, прекрасно работают, неплохо документированы и поддерживаются.
Из всего этого разнообразия мы используем tarantool/queue для очередей и tarantool/prometheus для сбора метрик. Также у нас есть немного своей логики на Lua. Немного — потому что наша команда из мира .NET. Мы привыкли, что есть статический анализ кода, есть пошаговая отладка, удобные профайлеры. У Lua с этим проблемы, особенно со статическим анализом кода.
Репликация
Очень хотелось бы получить синхронную репликацию. Мы уже умеем жить без неё, но очень сильно ждём. Пока что мы пользуемся имеющейся master-master асинхронной репликацией. Один из наших сервисов работает с картинками. Там фигурируют случайные ключи, и мы можем писать в любую ноду, потому что вероятность совпадения ключей крайне мала. Нам нужно, чтобы совпали сгенерированный guid для картинок и SHA256. Так как к этому мы пришли не сразу, то коннектор до сих пор не умеет соединяться с несколькими нодами. В нынешнем году мы это обязательно исправим.
Сборка кластера в старых версиях
В очень старом билде 1.7.3-0 может не собраться кластер. Допустим, у вас три мастер-ноды. Указываете им одинаковые конфигурации, запускаете. И пока любая из нод не увидит свои источники, откуда она забирает данные, она не будет принимать запросы от других клиентов. К сожалению, эти источники для них — клиенты. В результате все три ноды не отвечают на запросы и ждут 30 секунд. В это время они ищут свои источники, не находят, пишут в лог: «Источников нет, я выключилась». И кластер не собирается. Приходится собирать вручную. Запускаем одну ноду без общей конфигурации. Она поднимается, подключаем к ней остальные ноды, а потом меняем конфигурацию на единую. В новой версии этот баг уже исправлен.
Версионирование образа для докера tarantool/tarantool
Допустим, версия образа 1.7.3. А какая версия Tarantool внутри? Мы знаем, что 1.7.3, но номер сборки неизвестен. Единственный вариант — посмотреть исходники. Допустим, нужна версия Tarantool 1.7.3-115, потому что в 114 ещё была проблема, которая нас больно бьет, а в 116 — мы не знаем, есть она или нет. Какую версию образа взять?
Мы решили задачу просто: сами собираем свой образ, указываем конкретный номер сборки — и всё прекрасно работает. Но в целом это небольшая проблема. Образ по умолчанию прекрасно работает и покроет бо́льшую часть запросов, туда включены все основные модули, которые нужны для работы с Tarantool: мониторинг, очереди и т. д. Можно брать и пользоваться.
Интерактивные запросы
Чтобы написать интерактивный запрос в Tarantool, нужно знать Lua и уметь пользоваться командной строкой. Наши тестеры и админы умеют обращаться с командной строкой, но они не горят желанием разбираться с Lua. Они хотят быстренько написать SQL-скрипт, проверить какой-нибудь счётчик или timestamp. Поэтому мы очень ждём анонсированную поддержку SQLite-диалекта.
Мониторинг и логирование
С ними всё прекрасно. Есть модуль tarantool/prometheus, который опубликован на сайте Prometheus. Так как мы используем в production docker, то логи мы с него собираем с помощью Fluentd (1 и 2) и складываем их в ElasticSearch.
Выводы
.NET — это далеко не только Windows: .NET Core работает на всех платформах. У нас в production есть .NET-приложение, которое прекрасно функционирует на Linux. Наши opensource-проекты без проблем собираются и работают на Windows, Mac OS и Linux (тестируются только Windows 10, Max OS X, Ubuntu 16.04). При этом вполне возможно для разработки и отладки использовать весь богатый инструментарий, доступный в мире .NET и зачастую бесплатный даже для коммерческой разработки.
Благодаря кроссплатформенности новых .NET-решений, появляется возможность использовать недоступные ранее инструменты, например, Tarantool. Если вам важна скорость работы, вторичные индексы, возможность использования СУБД в качестве сервера приложений или очереди задач, то Tarantool — очень хороший выбор на сегодняшний день.