Современный клиент к NoSQL-базе данных
Интеграция через базу данных (БД) — один из распространенных видов интеграции. Но БД — тоже сервис, к которому также требуется подключение. Для пользователей эта процедура сводится к подключению коннекторов и изучению их API, но «под капотом» подобных клиентов может скрываться большая архитектура со сложной логикой взаимодействия.
Меня зовут Артем Дубинин. Я старший программист в команде интеграции компании Tarantool. В этой статье я хочу пролить свет на Black-box-устройства современных Java-клиентов для интеграции с NoSQL-базами данных на примере Tarantool.
Материал подготовлен по мотивам моего доклада «Современный клиент к NoSQL-базе данных». Вы можете посмотреть его здесь.
Немного про Tarantool
Tarantool — мультипротокольная Middleware-платформа для работы с данными. В архитектуре приложений Tarantool может выполнять роль промежуточного слоя, который способен одновременно решать несколько задач:
- управлять движением данных;
- ускорять цифровые сервисы;
- снижать нагрузку на Core-cистемы.
Многозадачность Tarantool во многом достигается благодаря тому, что инструмент сочетает в себе функционал сервера приложений и NoSQL-хранилища с гибкой схемой данных.
Как и любая другая платформа для хранения, Tarantool может интегрироваться с приложениями, написанными на разных языках.
Помимо этого, Tarantool может быть представлен в виде шардированного кластера для горизонтального масштабирования данных при работе с несколькими инстансами приложений. Такой сценарий использования особенно важен, когда нужно распределить данные между несколькими экземплярами Tarantool.
В нашем случае это также важно учитывать при разработке клиентов для интеграции.
Для интеграции Tarantool с пользовательскими приложениями есть (и было) несколько библиотек:
- 2012–2019: Tarantool — Java;
- 2020 — now: Cartridge — Java;
- 2020 — now: Сartridge — SpringData;
- 2023 — now: Tarantool — Java EE.
Разнообразие библиотек обусловлено разными сценариями использования — мы стараемся закрыть все потребности пользователей. Сейчас мы работаем над созданием SDK (Software Development Kit), в который планируем упаковать все необходимые модули и решения.
Структура клиента
Принципы взаимодействия приложений с базами данных постоянно видоизменялись и эволюционировали. Это четко прослеживается в разрезе работы, например, с Java-приложениями. Так, раньше пытались обобщить функционал в простой для использования интерфейс, но с ограничениями: структурно в самом интерфейсе не было кода — весь код для работы с БД был в драйвере, то есть коннекторе. В целом эта структура остается и сейчас, только теперь больше функционального кода ушло в обобщение. Это обусловлено тем, что Spring-Data уже не просто интерфейс, а целый комбайн, который позволяет просто интегрировать те самые драйверы/коннекторы.
Структурно современный клиент для интеграции с NoSQL БД, как правило, состоит из нескольких основных компонентов, которые отвечают за:
- сеть (Network);
- маппинг (Mapping);
- пользовательский интерфейс.
Помимо этого, в структуру также могут входить вспомогательные компоненты, такие как:
- Connection pool;
- Ecosystem;
- Balancing.
Теперь обо всем подробнее и по порядку.
Network
Взаимодействие любого клиента и БД начинается именно с сети — логично, ведь без этого просто невозможен обмен данными. Причем от режимов работы сети зависит, какие типы обмена данными можно будет организовывать (синхронный или асинхронный).
Есть несколько основных компонентов сетевого взаимодействия клиента с сервером.
- Подключение. Например, TCP/IP-соединения с помощью интерфейсов языка программирования или готовых библиотек.
- Передача данных. Для этого, например, может использоваться неблокирующий ввод/вывод, что для пользователя можно представить как CompletableFuture. По завершении неблокирующего чтения ответа на запрос эта CompletableFuture сможет завершиться обработчиком чтения.
- Безопасность. Как правило, безопасность передачи данных обеспечивается с помощью шифрования трафика и паролей с применением алгоритмов SSL/TLS, SCRAM, PAP (используется для шифрования паролей в Tarantool).
- Протокол — текстовый или бинарный, с помощью которого БД может понимать, что именно она получает на вход, с какой схемой взаимодействия. В Redis это протокол RESP, в Mongo — MongoDB Wire Protocol, в Tarantool — IPROTO.
Виды протоколов
Протокол фактически является компонентом, который описывает передаваемый поток данных и предоставляет инструкции для корректной работы с ним.
Большинство крупных БД имеют свой протокол, который наиболее полно описывает принципы работы с базой данных.
- В Redis — RESP (Redis Serialization Protocol). Это текстовый протокол. Основан на принципах клиент-серверной архитектуры и поддерживает несколько стилей взаимодействия, включая «запрос — ответ» и Push-сообщения. Протокол RESP использует формат JSON для сериализации данных, что делает его легко читаемым и удобным для обработки ПО. Кроме того, он оптимизирован для работы с большими объемами данных и обеспечивает высокую скорость передачи информации.
- В MongoDB — The MongoDB Wire Protocol. Это бинарный протокол. Основан на формате BSON (Binary JSON), который представляет собой компактный JSON, предназначенный для сериализации и передачи структурированных данных. Протокол имеет стиль «запрос — ответ», что означает, что каждый запрос от клиента сопровождается соответствующим ответом от сервера. Это позволяет клиентам и серверам взаимодействовать в предсказуемом и надежном режиме, обеспечивая точность и целостность передаваемых данных.
- В Tarantool — IPROTO. Это бинарный протокол. Основан на формате кодирования MsgPack, который представляет собой компактный и эффективный формат сериализации данных. Протокол IPROTO поддерживает несколько стилей взаимодействия, включая «запрос — ответ» и событийный стиль. Это позволяет клиентам и серверам взаимодействовать в различных режимах в зависимости от потребностей приложения.
Структура протоколов
Структурно каждый протокол состоит из нескольких основных блоков, которые описывают разные аспекты взаимодействия клиента и БД.
- Формат передачи пользовательских данных. Таким образом устанавливается «стандарт» формата кодировки передаваемых данных. Блок обязательный, поскольку именно он гарантирует совместимость отправляемых и получаемых данных. Примеры форматов: BSON (Mongodb), MsgPack (Tarantool)
- Метаданные. Здесь для БД описывается вся дополнительная информация о потоках данных и работе с ними. К такой могут относиться: тип запроса (SELECT), ID запроса, ID транзакции, версия схемы. При этом, например, в Header протокола Tarantool IPROTO нередко указываются всего два поля: тип запроса (Request Type) и формат запроса (Sync).
- Алгоритм взаимодействия. В этом блоке описывается, как отправляются запросы, как поступают ответы и как их синхронизировать.
Алгоритмы взаимодействия — один из ключевых элементов протокола, поэтому о них стоит поговорить подробнее.
Алгоритм взаимодействия
Алгоритм взаимодействия определяется самой базой данных. При этом вариантов отправки запросов и получения ответа может быть несколько.
- По порядку. В этом случае ответы возвращаются в том же порядке, в каком приходили запросы.
- С использованием Sync ID. В данном случае все запросы помечаются персональными синхронизирующими идентификаторами (ID), что дает возможность обрабатывать запросы в любом порядке и даже в нескольких тредах одновременно — синхронизация по порядку не нужна. Соответственно, и порядок поступления ответов на запросы также не важен.
- По подписке (Subscription). Подписочный алгоритм взаимодействия схож с принципом использования мапы. Но только в данном случае запрос отправляется один раз, а ответы приходят каждый раз при изменении значения ключа синхронизации. При этом конкретный Callback вызывается с помощью метаданных.
Таким образом, компоненты сети (в том числе упомянутые протоколы) уже позволяют выстраивать полноценное взаимодействие пользователя с базой данных, указывая только формат кодирования, метаданные и алгоритмы.
Но подобные клиенты не покрывают все сценарии, поскольку во многих кейсах требуется не только передача данных или объектов, но и их переформатирование.
Именно для этих задач в структуре клиента интеграции нужен маппинг.
Mapping
Маппинг — процесс определения соответствия между разными моделями данных и их преобразование для достижения полного соответствия.
Если упрощенно, задача маппинга — преобразование пользовательских данных в сущность для протокола. Например, такая цепочка преобразований может иметь следующий вид:
Java Object → Jackson → JSON String + Metadata → DB
С другими форматами алгоритм преобразования схож — зачастую будет меняться только библиотека формата.
Вместе с тем структура формата, требующаяся для протокола, может быть специфичной. Например, может потребоваться передавать данные в определенной структуре, чтобы верхнеуровневый контейнер был всегда массивом (array) или ассоциативным массивом (map). В таком случае алгоритм будет меняться с учетом особенностей запросов, базы данных и требований к обработке.
Проблема парсинга структур
Маппинг можно рассматривать как парсер из пользовательских данных в структуру для БД. Но подобный парсинг может быть осложнен несколькими факторами.
1. Сложные деревья вложенности структур. Например, может прийти «сложный» объект с несколькими уровнями вложенности.
Но для NoSQL-решений это не совсем подходящий сценарий, поэтому работа с подобными деревьями может быть сопряжена с определенными ограничениями. С точки зрения коннектора здесь важно понимать, что придется работать с разветвленной структурой, то есть подключать готовые или самописные обработчики деревьев.
2. Невозможность простой передачи типа с дженериками. Например, методу недостаточно описания на уровне List
Необходимость парсить из одной системы в разные типы. При парсинге нередко возникает необходимость различного представления структур с учетом требований системы. Такое случается, когда система добавляет в БД одни типы данных, а второй системе нужно работать с данными других типов. При этом добавлять сложные обработчики не всегда эффективно и просто.
То есть, например, мы можем Integer из Messagepack представить как Double. Это можно сделать на уровне Java: мы можем просто взять Java Integer, а потом перевести его в Double. Но это лишнее взаимодействие и дополнительная нагрузка на специалистов. Намного проще сразу выполнять преобразование в нужный формат. Это тоже является одной из проблем парсинга структур.
Application Programming Interface
Теперь о третьем базовом компоненте современного клиента к NoSQL-базам данных — пользовательском интерфейсе.
Программный интерфейс работает с тремя основными блоками.
1. Пользовательские данные, передаваемые в БД. В данном случае для разных БД различия на уровне кода минимальны.
2. Информация о формате записи или получения ответа. Причем такая информация дается только клиенту — в БД она не используется. Здесь важно, что ответ можно получить в формате и виде, удобном для пользователя. Например, MongoDB по умолчанию выдает ответы в виде документов. Поэтому в API надо закладывать возможность предоставлять ответ в разных видах.
3. Client vs Cluster Client. Интерфейсы взаимодействия с единичным сервером и кластером различаются. Соответственно, различаются и сценарии работы пользовательского интерфейса на уровне кода и компонентов.
В таком случае есть два подхода: давать пользователю разные интерфейсы для каждого сценария (Client и Cluster Client) или предоставлять обобщенный интерфейс, в котором вся логика обработки скрыта «под капотом». Но вариант с обобщенным интерфейсом сложнее как для пользователя, так и с точки зрения реализации и администрирования. Поэтому «простой путь» с двумя разными интерфейсами выглядит рациональнее.
Опциональные компоненты структуры клиента к NoSQL БД
Как я упоминал в начале статьи, помимо основных (обязательных) компонентов, в структуре клиента могут быть и дополнительные, интеграция которых помогает расширить возможности классической стоковой реализации.
Вариантов опциональных компонентов может быть много. Рассмотрим наиболее востребованные.
Connection Pool
Connection Pool — механизм, который позволяет управлять пулом подключений к базе данных. То есть когда новый пользователь запрашивает доступ к БД, ему выдается уже открытое соединение из этого пула. Если все открытые соединения уже заняты, создается новое. Это уменьшает время обработки запросов и повышает производительность приложения.
Connection Pool может использоваться в разных сценариях работы с кластером, в том числе для:
- интерактивных транзакций;
- подписки по разным инстансам хранения;
- балансировки нагрузки.
Balancing
Для балансировки нагрузки есть много сторонних решений, поэтому целесообразность реализации встроенного механизма распределения запросов не всегда очевидна.
Но на самом деле обусловлена она тем, что сторонние инструменты подходят не для всех сценариев. Например, если есть метаданные, которые нужны по нескольким запросам. Для таких задач часто пишется кастомная логика обработки, и балансировщик на уровне клиента оказывается очень полезным.
Виды разных балансировок на примере Tarantool — Java EE
Причем роль внутреннего балансировщика может выполнять не только полнофункциональное решение, но и условный буфер, который на уровне клиента обеспечивает возможность работы со сторонним балансировщиком.
Помимо прочего, наличие встроенного балансировщика удобно и с точки зрения «холодного старта»: с инструментом проще начать работать, если все нужное есть «из коробки» и не надо думать о поиске стороннего решения и вариантах его интеграции.
Ecosystem
Структура современного клиента к NoSQL БД также может включать слой экосистемных решений и реализаций. Упор на экосистемность в рамках клиента интеграции решает сразу комплекс задач:
- обеспечивает унификацию API (легко перейти из одной БД в другую);
- помогает ускорить и упростить разработку, развивать экспертизу;
- дает доступ к фичам экосистемного инструмента и расширяет совместимость со сторонними решениями;
- обеспечивает стандартизацию, уход от «зоопарка технологий».
Что в итоге
Большинство разработчиков привыкли видеть коннекторы в виде готовой «коробки», с помощью которой можно просто интегрировать приложение и БД. Но «под капотом» этой коробки на самом деле скрывается «слоеный пирог», в котором каждый «ингредиент» незаменим и решает определенные задачи.
Во многом такая сложная внутренняя структура обусловлена динамичным развитием баз данных, а также скоростью и сложностью передаваемых потоков данных — с момента появления требования к драйверам интеграции постоянно росли, поэтому клиенты интеграции начали эволюционно получать больше функций и внутренних компонентов, несмотря на все сложности и нетривиальные моменты разработки.
Важно при этом, что структурно современные клиенты к NoSQL БД довольно гибкие. Поэтому их можно расширять, обновлять и адаптировать под разные требования, варианты и сценарии использования, главное — понимать принципы работы коннекторов и нюансы их разработки.