Нередко оказывается, что даже в большом «зоопарке» общедоступных решений нет инструмента, отвечающего всем требованиям. В таком случае команды вынуждены двигаться в сторону разработки своего продукта.
Меня зовут Александр Кленов. Я тимлид разработки Tarantool DB в команде Tarantool. В этой статье я расскажу, почему мы решили добавить в свой продуктовый портфель Tarantool DB и что реализовали в инструменте, а также покажу на примере словарей, почему строить свою СУБД сложно.
Предпосылки создания Tarantool DB
Современные ИТ-системы нередко предполагают не только работу с большими потоками данных, но и изменение характера нагрузки. Среди прочего это касается и OLTP-систем, в том числе Tarantool, которым все больше приходится обрабатывать в реальном времени не обычные транзакции, а высокочастотные микротранзакции.
Нюанс в том, что существующие СУБД разных типов «из коробки» не совсем подходят для таких задач.
Реляционные БД (например, MySQL, PostgreSQL, Oracle, MS SQL, Amazon Aurora и т. д.) не являются кластером из коробки, а настройка низкого Latency для них требует определённой экспертизы и дополнительных усилий.
Документоориентированные БД (например, MongoDB, CouchDB) не предполагают работу с таблицами и индексами, не поддерживают SQL и не позволяют гибко настраивать права доступа.
Key-value-хранилища (например, Redis, Memcached, Couchbase, Amazon S3) не имеют поддержки транзакций, итераторов и вторичных индексов, то есть скорость их работы в отдельных сценариях ограничена.
Колоночные базы данных (например, ClickHouse) больше ориентированы на OLAP-нагрузку, то есть на анализ данных, а не на обработку высокочастотных микротранзакций.
Графовые БД тоже рассчитаны на другой профиль нагрузки и другие задачи.
Нам хотелось иметь полезные фичи из разных видов СУБД, поэтому когда-то мои коллеги сделали СУБД Tarantool. Однако Tarantool не является коробкой. С целью создания коробочного решения и появился Tarantool DB. Основной фичей первого релиза Tarantool DB является сокращение Time-to-market (TTM). Миссия второй версии — переход на более новый и технологичный вариант платформы.
Tarantool DB и его особенности
Tarantool DB — NoSQL СУБД, которая работает с протоколами Redis и IPROTO.
В инструменте скомбинированы наиболее востребованные свойства и параметры БД разных типов.
Решение имеет гарантированно низкую Latency, «из коробки» поддерживает шардинг и балансировку, имеет высокую отказоустойчивость.
От реляционных БД Tarantool DB получил таблицы, транзакции (ACID), индексы, поддержку SQL, возможность гибкой настройки прав доступа и использования итераторов.
От документоориентированных в Tarantool DB унаследован принцип работы с файлами, структурами, вложенностями. В Tarantool DB данные хранятся в MessagePack — формате сериализации данных, который является эффективным бинарным аналогом JSON. Благодаря компактности и скорости MessagePack часто выбирают для обмена данными в системах, где важна производительность. Подробнее о MessagePack можно почитать здесь и здесь.
От Key-value взял основные принципы, благодаря которым может применяться в качестве Drop-in-замены Memcached и Redis.
При этом Tarantool DB не ориентирован на задачи аналитики и не подходит для проектов с большим количеством бизнес-логики, поскольку работает в однопоточном режиме.
Примечание: Подробнее о Tarantool DB можно почитать на официальной странице продукта.
Профит от разработки собственной СУБД
Разработка своего решения помогла нам получить инструмент, который в большей степени закрывает наши потребности и боли клиентов. При этом ресурсы, затраченные на построение инструмента, вполне оправданы, поскольку его функционал позволяет отказаться от компромиссов и построения «костылей».
Более того, в нашем случае разработка Tarantool DB — долгосрочная инвестиция в «продуктовый портфель», который мы не только предоставляем клиентам, но и активно используем сами для внутренних проектов.
Сложности построения своей СУБД
Создание своей СУБД сопряжено не только с выгодами, но и с трудностями. В первую очередь это обусловлено сложностью подобных систем.
Так, создание Tarantool DB предполагало разработку:
инсталлятора;
коннекторов;
документации и примеров;
тестовых кейсов и сред;
вспомогательных и тестовых инструментов;
модулей для расширения функциональности.
Таким образом, за созданием СУБД скрывается целый массив задач разработки. Причем в каждой задаче подразумевается много сложных подзадач.
Например, разработка новых функций включает такие шаги и мероприятия:
проработка идеи;
валидация идеи;
проектирование;
архитектурное ревью;
кодирование;
ревью кода;
тестирование (QA);
документирование;
ревью документации;
релиз.
Разбор возможных сложностей на примере проектирования словарей
Для полного понимания возможных проблем и задач при создании и развитии своей СУБД рассмотрим всего один этап из описанных ранее шагов разработки, а именно — проектирование на примере словарей.
Словари — это датасеты небольшого размера, которые, как правило, содержат не более 100 000 записей, имеют низкий RPS на запись и нужны на всех узлах для предоставления данных в пользовательские методы. Обычно в словарях содержатся нормализованные данные — например, название улиц, города, валюты, пол пользователя и другие.
Важно, что словари содержат сразу все свои данные на каждом узле. Соответственно, при размещении словарей на узлах кластера нужно выстроить механизм доставки вновь поступающих данных до всех узлов, гарантируя их консистентность.
Обычно кластер разносят на два, три и даже больше дата-центров, а в каждом дата-центре может быть задействовано несколько серверов. Для примера возьмём, что таких три (по одному на каждый дата-центр). Соответственно, получается следующая топология:
в каждом дата-центре по одному серверу;
на каждом сервере по узлу кластера, выполняющему перенаправление запросов (роутеру), и узлы хранения данных, каждый из которых является частью репликационной группы (шарда).
Разложить данные по кластеру можно тремя способами.
Запросы наружу. Самый простой и надежный метод, который подразумевает, что каждый узел по установленному интервалу времени обращается за новыми данными (например, за файлами) к другим источникам. Данный вариант предполагает усложнение инфраструктуры, так как, если источником являются файлы, их нужно как-то раскладывать по серверам, а если источником выступает БД, это другие сложности, о которых ниже.
Генерация внутри. Метод подразумевает, что для работы с поступающими внешними данными и формирования из них таблиц можно использовать некую функцию. Удобно, но в таком случае невозможно получить полный контроль над происходящим.
Внешнее управление. Способ подразумевает прокидывание наружу ручек, через которые можно делать импорт и экспорт данных.
Все варианты хороши в своих ситуациях. Но у нас реализовано третьим способом, так как:
по сравнению с первым вариантом это дает более компактное решение и более высокую скорость реакции на новые данные;
по сравнению со вторым вариантом дает возможность управлять данными извне.
Реализовать такой подход теоретически можно тоже несколькими способами.
Использование репликации
В этом случае ничего не нужно придумывать с нуля, а упор делается на использование того, что уже есть.
То есть можно взять роутеры, объединить их в одну репликационную группу и хранить данные словарей в них. Соответственно, на роутерах можно выполнять добавление или удаление данных из словарей.
Но способ явно не лишен недостатков, особенно в контексте работы с Tarantool DB:
репликационная группа Tarantool поддерживает только 32 узла;
большой репликасет неизбежно будет оказывать влияние на файловер и кластер;
порог входа в работу с методом высокий и требует детальной проработки документации;
усложняется запись и поиск данных, поскольку все операции надо выполнять через роутер;
повышается значимость в системе роутеров и отпадает возможность реализации безроутерного коннектора;
появляются лишние запросы со Storage.
В качестве альтернативы этому варианту можно подключить другой кластер и интегрироваться с существующими решениями (Tarantool, etcd, Consul, ZooKeeper), чтобы вынести на них логику работы со словарями.
Но фактически решение ничем не отличается от размещения на роутерах, поэтому проблемы имеет те же самые.
Синхронная запись
В данном случае для распространения словарей подразумевается использовать синхронную репликацию, обычный двухфазный коммит. Соответственно, доставка осуществляется в два захода: на первой фазе данные раскладываются на каждый узел, на второй происходит коммит.
Но у этой схемы есть существенный недостаток. Так, в случае сбоя на любом из узлов коммит не будет выполнен.
Безусловно, на случай отказов можно реализовать схему с кворумами, но тогда все сильно усложняется и надо предусматривать механику работы с узлами, которые изначально были недоступны, но после включились.
Более того, в такой реализации может произойти и отказ менеджера транзакций двухфазного коммита. То есть может быть ситуация, при которой на часть узлов обновленные данные уже доставлены, а на часть еще нет.
С масштабированием такой схемы тоже всё непросто: при добавлении нового узла надо предусматривать механизмы доставки данных и на него.
Итого: в схеме больше рисков и сложностей, чем преимуществ.
Схема с лидером
Здесь всё просто. Метод заключается в назначении лидера, на который приходят данные. При этом сам лидер дальше раскладывает новые данные по остальным узлам.
В случае отказа одного из Storage лидер его просто игнорирует и заполняет данными остальные узлы.
При этом лидер будет периодически пытаться доставить данные до выключенного Storage.
Механизм в целом не новый и активно используется для репликации в разных базах данных. Но в контексте работы со словарями он не позволяет закрыть все узкие места.
Так, управлять лидером и контролировать, чтобы одновременно был только один лидер, сложно. Более того, реализация такого варианта требует выстраивания сложной механики, что для существующего механизма и текущей задачи просто избыточно.
Pulling and Gossip
В данном случае всё сводится к тому, что мы «стягиваем» данные и раздаем их по Gossip-принципу. То есть все узлы условно размещаются по кругу и им сообщается, что:
при поступлении новых данных каждый узел оповещает своего левого соседа;
при получении оповещения каждый узел идет за новыми данными к правому соседу.
На практике это работает следующим образом:
на Storage B-0 поступают новые данные;
Storage B-0 оповещает об этом левого соседа, то есть Storage C-0;
после получения оповещения Storage C-0 идет к своему правому соседу, то есть к Storage B-0, за новыми данными;
после получения новых данных Storage C-0 оповещает своего левого соседа, то есть Storage D-0, и цикл повторяется.
В итоге даже если один из узлов в цепочке временно недоступен, после включения он сможет автоматически получить актуальные данные и включиться в выстроенный пайплайн. То есть данные в любом случае будут доставлены на все узлы.
Одновременно с этим, поскольку эта схема фактически реализует мультимастерный подход, где можно писать во все узлы, есть риск, что по одному и тому же ключу будет одновременно сделано две записи, которые противоречат друг другу. Например, что x=1 и x=2.
Причем обычный подход к решению таких конфликтов, где одно значение сразу затирается другим, в чистом виде здесь не совсем подходит. Но подход вполне можно модернизировать, поскольку запись происходит редко. Так, одновременно с данными можно записывать метку времени и при распространении новых данных сравнивать метку и сохранять только те данные, которые пришли позже (то есть более актуальные).
Таким образом, в варианте с Pulling and Gossip для раскатки словарей есть и мультимастер, и решение конфликтов по метке времени, и перезапуск по таймеру для автоматического подключения временно не работающих узлов. Плюс у метода очень простой API, который сводится к базовому набору команд: get, set, del.
При этом метод гарантирует:
распространение новых данных;
согласованность и консистентность данных;
сохранение четкого порядка записи.
Но даже у этого метода при всех его достоинствах есть и несколько нюансов:
Порядок записи гарантируется, пока запись производится редко, то есть выполняются условия словарей. Если запись становится более частой, чем максимальная разница во времени между серверами кластера, порядок уже не гарантируется — данные могут быть записаны в случайном порядке.
Важно внимательно относиться к манипуляциям со временем. Так, при переводе времени вперед ничего не происходит. Но если перевести время назад, весь механизм сломается, поскольку на запись будут приходить данные, созданные по меткам времени раньше, чем последняя запись, которая уже есть в БД. Для исключения таких рисков у нас реализована защита, которая остановит запись до тех пор, пока время не догонит последнюю метку.
Надо помнить, что кластерных транзакций в СУБД может не быть (например, в Tarantool их нет). Поэтому важно понимать, что транзакции выполняются в пределах узла, и учитывать это при проектировании. Это подразумевает, что при сохранении данных по разным ключам в транзакции на каждом из узлов результат может не соответствовать ожиданиям. Например: на одном узле мы пишем в транзакции, что А = 1 и В = 2. А на другом узле пишем в транзакции, что А = 2 и В = 3. Из-за меток времени после фиксации транзакций на узлах и синхронизации данных между узлами результат будет: А = 2, В = 2.
Таким образом, даже разработка небольшой функции внутри СУБД требует много ресурсов и глубокого погружения во все нюансы. А таких функций и разработок в рамках создания и поддержки собственной СУБД не один десяток.
Вместо выводов
Построение своей СУБД — это всегда больно, дорого, сложно и трудоемко. Поэтому инициировать такую разработку целесообразно, если:
существующие решения не закрывают потребности;
у команды есть нужная экспертиза и ресурсы;
разработка с нуля оправдана экономически и отвечает бизнес-целям.
Вместе с тем надо понимать, что схемы создания идеальной СУБД не существует — в любом случае придется методом проб и ошибок «строить велосипед», который наиболее полно удовлетворяет требованиям компании. И полезной практикой в этом будет комбинирование существующих вариантов: беря за основу идеи и принципы существующих вариантов, можно, как из кубиков лего, построить наиболее эффективный и надежный инструмент, готовый к работе даже в высоконагруженных системах.
Комментарии (8)
ViacheslavNk
23.09.2024 16:03Реляционные БД (например, MySQL, PostgreSQL, Oracle, MS SQL, Amazon Aurora и т. д.) не позволяют строить кластерные реализации,
У MSSQL есть Failover Cluster, есть Always On availability groups, у Oracle есть Data Guard, RAC
а настройка низкого Latency для них требует глубокой экспертизы и дополнительных усилий.
Все так, но делать собственное решение сравнимое с выше перечисленными, потребует еще большей экспертизы и времени.
Parmenides
"Oracle, MS SQL ... не позволяют строить кластерные реализации, а настройка низкого Latency для них требует глубокой экспертизы и дополнительных усилий."
Мягко говоря, спорное утверждение.
Kahelman
Ага, меня тоже порадовало- нет экспертизы настроить low latency для существующих Бах данных, но есть экспертиза написать свою с нуля ….
Хотелось бы чтобы автор привёл оценку во сколько VK обошлась бы лицензия на Oracle под их нужды ….
Думаю что после этого Oracle можно не упоминать в суе. То же касается и MS SQL.
Далее пошло откровенное передёргивание и натягивание совы на глобус:
Наша база данных поддерживает ACID транзакции, … но транзакции поддерживаются только в рамках узла …
Транзакция она как свежеть - бывает первая она же и последняя, поддержка транзакций либо есть либо нет.
Тем более если вы ACID поддержку заявили.
Другой вопрос что при определённом размере базы данных и ее географическом распределении классические транзакции вам уже могут и не потребоваться.
1div0 Автор
Сложно сказать, ведь наш продукт использует далеко не только VK
1div0 Автор
На самом деле в кластерном решении достаточно часто хватает транзакции в рамках узла. Это связано с тем, что при проектировании стараются избежать map-reduce-ов и стараются размещать связанные данные рядом, на одном узле. Конечно при этом нужно много чего учитывать и это не удобно. Было бы хорошо иметь кластерную транзакцию. Но пока её нет.
1div0 Автор
Вы правы, конечно же стоило написать иначе. Я имел ввиду, что из коробки перечисленные реляционки - не кластер. Но настроить из них кластер вполне можно, только это требует дополнительной экспертизы. Я вычитывал текст много раз и как-то это пропустил. Извините.