В сложных ИТ-системах важны не только основные модули, но и механизмы их взаимодействия — коннекторы (они же драйверы). Например, без них сложно выстроить связь приложения с базой данных. Но закрыть все потребности существующими драйверами не всегда возможно: они могут не удовлетворять требованиям по производительности, функциональности или даже совместимости. По этим причинам разработчикам приходится создавать свои драйверы к СУБД.

Меня зовут Иван Банников. Я ведущий разработчик команды Data Integration Team из экосистемы Tarantool. В этой статье я расскажу о разработке драйверов на примере создания коннектора к Tarantool.

Материал подготовлен по мотивам моего доклада на CodeFest. Вы можете посмотреть его здесь.

Что такое коннектор и почему может понадобиться его разработка

Коннектор (драйвер) к СУБД — библиотека, которая позволяет приложению взаимодействовать с базой данных так, как будто база данных является частью самого приложения.

Приложение передает коннектору данные, необходимые для работы с БД: например, адрес сервера базы данных, имя пользователя и пароль, сертификаты, чтобы установить безопасное и аутентифицированное соединение — а коннектор содержит только логику работы с БД и не затрагивает бизнес-логику.

Как правило, коннекторы для разных языков программирования предоставляются производителем конкретной СУБД. Но готовые решения не всегда могут обеспечить нужные показатели или закрыть текущие потребности. Поэтому иногда при разработке проектов разработчики вынуждены заниматься и созданием своих драйверов к СУБД. Можно выделить несколько предпосылок, указывающих на такую необходимость.

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

  • Низкое качество документации, API, продукта. Некоторые доступные коннекторы плохо проработаны: у них может не быть полной и понятной документации, их API может работать некорректно, а совместимость с нужной СУБД может быть неполной. В итоге использование таких драйверов ресурсозатратнее и тяжелее, чем разработка своего коннектора с нуля.

  • Подходящего драйвера нет. Бывает, что драйвера к СУБД на нужном языке просто нет. Например, к СУБД может быть доступен целый стек коннекторов, но в этом наборе нет драйвера для Python. Если БД критически важна для проекта, коннектор придется писать самостоятельно.

Перед началом разработки: нюансы и входные требования

Перед разработкой своего драйвера к СУБД важно разобраться с двумя аспектами.

Архитектура драйвера

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

  • Гиперобобщение. Не стоит пытаться создать универсальный коннектор «швейцарский нож». Зачастую это не оправдано: излишне сложно с точки зрения архитектуры, избыточно по функциональности и очень проблематично в поддержке и администрировании. Более того, это ограничивает развитие, поскольку требует соблюдения множества накопившихся контрактов.

  • Нацеленность на абстрактную бизнес-задачу, а не конкретные сценарии. Драйвер должен решать конкретные задачи и быть ориентированным на конкретную систему в соответствии с реальным запросом бизнеса. В противном случае библиотека будет компромиссным решением, которое не учитывает все «реалии на земле» и в целом не очень соответствует ожиданиям пользователей. 

  • Отсутствие четко выделенных слоев и размытие границ между модулями. Архитектуру следует проектировать поэтапно, выделяя важные уровни, каждый из которых решает свой набор задач. Если не соблюдать это правило, есть риск, что даже малейший сбой будет приводить к множественным ошибкам в разных частях драйвера. Так произойдет, если они будут слишком сильно связаны между собой, врастая друг в друга в процессе эволюции. Вот какая картина может получиться:

При разработке архитектуры драйвера важна четкая структура слоев с определенным порядком их построения.

  • Первый уровень архитектуры — работа с сетью. Сюда относятся модули, классы, функции, отвечающие за работу с соединением, общее описание пакетов (примитивов) протокола, их прием и отправку.

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

  • Третий уровень архитектуры — работа с данными и низкоуровневым API. На этом уровне уже прорабатываются компоненты и решения для балансировки, мониторинга и управления соединениями, сериализации и десериализации. 

  • Четвертый уровень архитектуры — клиент. Здесь идет разработка уже непосредственно высокоуровневого интерфейса. Причем клиент для драйвера может быть как одиночный, так и кластерный. На этом же уровне реализуются утилиты для коннектора, например, для расширения функциональности и совместимости.

  • Пятый уровень — экосистема. Верхний уровень развития коннектора — разработка экосистемы для него, а также интеграция с другими экосистемами. Фактически речь о расширении некого продуктового портфеля, в котором каждый коннектор закрывает отдельную потребность.

И вот как все это в идеале должно выглядеть:

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

Асинхронность

Для многих коннекторов к СУБД рационально реализовывать асинхронный тип взаимодействия. Чтобы понять причину, стоит разобраться с тем, как работают синхронный и асинхронный драйверы.

Так, в случае синхронного драйвера алгоритм действий следующий:

  1. Клиентский код запрашивает операцию А.

  2. Синхронный драйвер принимает операцию и отправляет запрос в БД, после этого драйвер переходит в режим ожидания.

  3. База данных принимает запрос, обрабатывает его и отправляет ответ.

  4. Драйвер получает ответ и отдает результат клиентскому коду.

Таким образом, клиентский код вынужден ожидать результат, причем часто на это уходит немало времени, процессор используется неэффективно.

В случае с асинхронным драйвером алгоритм несколько иной:

  1. Клиентский код запрашивает операцию Ы.

  2. Асинхронный драйвер принимает операцию и дает обещание, что запрос будет передан в базу данных, при этом драйвер не переходит в режим ожидания.

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

  4. Как только запрос будет обработан базой данных и результат будет передан на коннектор, последний вернет его клиентскому коду.

  5. В момент получения результата клиентский код вынужден остановиться и немедленно обработать полученный результат.

Главное преимущество метода в том, что клиентский код может одновременно отправлять множество запросов и не останавливать работу на время ожидания. Соответственно, производительность и быстродействие системы с асинхронным коннектором обычно выше.

Первый шаг разработки: знакомство с СУБД

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

Поскольку в рамках статьи в качестве примера мы используем Tarantool, разбирать будем именно его особенности.

Tarantool — In-memory NoSQL СУБД. Причем здесь NoSQL — Not Only SQL, то есть решение поддерживает как SQL, так и другие способы доступа к данным. Tarantool поддерживает схемы, гарантии ACID, репликации, шардирование. От других решений его отличает встроенный интерпретатор Lua, что приближает Tarantool к серверу приложений.

Модель данных, на которой работает Tarantool, довольно простая.

Так, есть Space — аналог таблицы БД. В Space хранятся кортежи — массивы значений определенных типов. Например, кортеж может состоять из трех элементов: идентификатора, строк, даты.

Далее у каждого Space должен быть задан первичный ключ, по которому можно извлечь кортежи. То есть это key-value-модель. Помимо первичного ключа, также можно определять и вторичные индексы, что в дальнейшем дает возможность делать выборки по интересующим нас полям.

Также для разработки коннектора важно понимать, какие операции поддерживаются СУБД. В случае Tarantool это стандартный набор:

  • вставка (insert);

  • выборка по индексу (select);

  • изменение (update);

  • удаление (delete);

  • замена (replace);

  • вставка или изменение (upsert).

Таким образом, уже на этом этапе понятно, какие операции надо реализовать в драйвере.

Знакомство с протоколом

Для работы с Tarantool используется бинарный протокол IProto, в котором используется формат данных MessagePack.

Примечание: подробнее об IProto можно почитать здесь. О MessagePack — здесь.

Теперь перейдем к структуре пакетов IProto. 

Каждый пакет IProto состоит из трех секций:

  • размер пакета в байтах;

  • заголовки;

  • тело.

Причем каждая секция — это корректный MessagePack-терм. 

Заголовки IProto-пакета

Как мы видим на схеме, заголовок IProto-пакета — это обычный MessagePack MAP, то есть ассоциативный словарь, закодированный в формате MessagePack. Заголовок — это пара «ключ — значение» в данном словаре. Из обязательных заголовков должны присутствовать две пары: REQUEST_TIME и SYNC. Ключ REQUEST_TYPE указывает на тип пакета (операция, ответ на запрос, потоковое сообщение, ошибка и так далее), а SYNC содержит в себе идентификатор пакета. Идентификаторы пакетов нужны для сопоставления запросов и ответов, так как протокол IProto позволяет выполнять множество запросов одновременно.

Тело IProto-пакета

Тело IProto-пакета также является простым MessagePack-словарем и в большинстве случаев содержит ключ BODY, который указывает на произвольный объект MP_OBJECT.

Ключи, что в заголовках, что в теле, являются обычными числовыми константами.

Первый слой: работа с соединением, общее представление пакетов

Теперь остановимся на теме интерфейсов, которые нужны для описания работы пакетов и соединений. Для примера код будем писать на языке Java.

Общее представление пакетов

Исходя из общей структуры IProto-пакета, можно выразить обобщенные интерфейсы для представления пакетов:

interface IProtoMessage {

  int getType();

  void setSyncId(long id);

  long setSyncId();

  byte[] getBodyAttribute(int key);

  void setBodyAttribute(int key, byte[] mpObjectBytes);

  byte[] pack (MessageBufferPacker packer);

}

Здесь все просто. Мы указываем:

  • метод getType(), чтобы понимать, какой ответ прилетел;

  • метод getSyncId(), чтобы распознавать идентификатор ответа или запроса;

  • набор методов, с помощью которых можно добавлять и изменять атрибуты тела (getBodyAttribyte(int key) и setBodyAttribute(int key, byte[] value));

  • метод pack(), чтобы для каждого пакета можно было применять набор оптимизаций для быстрой упаковки в байтовую последовательность.

Как мы видим, работа идет на самом низком уровне, без сериализации и десериализации — работаем напрямую с байтами. Исключение делается для служебных методов, как, например, getSyncId() и getType(), которые отдают уже десериализованные значения.

Представление соединений

Пакеты должен кто-то читать и отправлять. За это отвечает абстракция соединений. Ее представление также можно свести к небольшому набору команд:

interface IProtoConnection {

  void listen(Consumer<IProtoMessage> handler);

  CompletableFuture<Void> send(IProtoMessage msg);

  CompletableFuture<Void> connect(InetSocketAddress address,

                                  long timeoutMs);

  void close();

}

Как мы видим из методов интерфейса, соединение должно уметь:

  • открываться;

  • закрываться;

  • посылать сформированные пакеты;

  • получать пакеты.

Но как эти пакеты получать? За это как раз отвечает метод listen(Consumer<IProtoMessage> handler), который на входе принимает функцию обратного вызова. Эта функция будет вызвана при приеме каждого нового пакета. Отметим, что данный интерфейс не раскрывает никаких деталей того, как это соединение реализовано. Могут быть использованы совершенно разные механизмы и библиотеки, например Java NIO или Netty. Важно другое: этот интерфейс отвечает за небольшой набор простых задач. Уже на этом этапе можно, хоть и с некоторым трудом, вручную создавая пакеты, выполнять запросы и получать ответы. Но сопоставление запросов и ответов, создание пакетов — это уже задача следующего уровня.

Второй слой: работа с протоколом, детальные описания пакетов

Работа с протоколом

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

interface IProtoClient {

  void init(IProtoConnection connection);

  CompletableFuture<IProtoMessage> select(byte[] key,

                                          int spaceId,

                                          int indexId);

  CompletableFuture<IProtoMessage> insert(byte[] tuple,

                                          int spaceId);

  CompletableFuture<IProtoMessage> delete(byte[] key,

                                          int spaceId);

  ...

}

Рассмотрим пример реализации операции SELECT. Для начала ознакомимся со структурой IProto-пакета, представляющего операцию SELECT, и опишем класс, который реализует данный пакет.

Заголовки у пакета стандартные, а вот на тело стоит взглянуть повнимательнее. Рассмотрим наиболее часто используемые ключи:

  • IPROTO_SPACE_ID (MP_UINT)

  • IPROTO_INDEX_ID (MP_UINT)

  • IPROTO_KEY (MP_ARRAY)

Для всех запросов SELECT обязательно указывать такие ключи, как IPROTO_SPACE_ID, IPROTO_INDEX_ID, которые говорят, из какого спейса выбрать данные и по какому индексу. Для первичного ключа всегда нужно указывать 0. Если не указывать IPROTO_KEY, то можно получить выборку по всему спейсу. Если указать IPROTO_KEY, можно получить отдельную запись из спейса.

Все это можно выполнить в теле метода select(). Однако гораздо лучше описать класс, реализующий интерфейс IProtoMessage, который подставит все сам внутри себя.

public class IProtoSelect extends IProtoPacket

                          implements IProtoMessage {

  public IProtoSelect(int spaceId, int indexId, byte[] key) {

      this.setBodyAttribute(IPROTO_SPACE_ID,

                            mp_serialize(spaceId));

      this.setBodyAttribute(IPROTO_INDEX_ID,

                            mp_serialize(indexId));

      this.setBodyAttribute(IPROTO_KEY, key);

  }

  public byte[] pack() {

      return bytes;

  }

}

Таким образом, реализация метода IProtoClient.select() в первом приближении выглядит следующим образом:

public CompletableFuture<IProtoMessage> select(byte[] key,

                                               int spaceId,

                                               int indexId) {

  long requestId = getNextRequestId();

  IProtoMessage selectRequest = new IProtoSelect(

      key,

      spaceId,

      indexId

  );

  selectRequest.setSyncId(requestId);

  connection.send(selectRequest);

  CompletableFuture<IProtoMessage> future =

      new CompletableFuture<>();

  registerFuture(future, requestId);

  return future;

}

В этом методе мы:

  1. Обращаемся к полю connection, который содержит в себе экземпляр IProtoConnection.

  2. Вызываем у объекта соединения метод send, тем самым отправляя пакет.

Метод registerFuture здесь играет роль механизма регистрации обещания (future) —  чтобы потом через эту future вернуть результат коду, который ожидает его. Как происходит получение результатов, рассмотрим на примере метода инициализации IProtoClient:

public void init(IProtoConnection connection) {

    this.connection = connection;

    this.connection.listen(this::acceptPacket);

}

private void acceptPacket(IProtoMessage msg) {

  CompletableFuture<IProtoMessage> future  getRegisteredFuture(msg.getSyncId());

  future.complete(msg);

}

Как хранятся и обрабатываются «обещания», зависит от реализации. Например, можно выполнить отдельную реализацию интерфейса IProtoClient, которая гарантирует корректную работу в многопоточном окружении, и отдельную реализацию для работы не в многопоточном окружении. 

Мы подошли к самому важному моменту: на примере мы показали, что по SYNC_ID сопоставляются запросы и ответы и происходит диспетчеризация ответов. Но мы показали лишь схему «запрос — ответ», что в действительности сильно ограничивает возможности нашего клиента с точки зрения расширения протоколов. Об этом и пойдет речь далее.

Конечные автоматы

Протокол IProto не исчерпывается схемой «запрос — ответ», так как в нем есть и другие виды взаимодействий, которые в эту схему не укладываются. Как сделать наиболее универсальный механизм, который можно расширять относительно безболезненно?

Для этих задач используется конечный автомат — абстрактная модель, которая описывает поведение системы с фиксированным количеством состояний и правилами перехода между ними.

Конечные автоматы (КА) могут быть нескольких видов. Помимо простой схемы «запрос — ответ» по синхронному принципу, с помощью КА можно описать взаимодействие в виде подписки с получением всех сообщений до команды «конец потока».

В рамках протокола IProto таких семейств протоколов несколько. Например, который реализует схему «запрос — ответ» с тайм-аутом.

Или стриминговый протокол, при котором все сводится к алгоритму: ожидаем сообщение → получаем сообщение → обрабатываем сообщение → снова ждем.

 

В рамках протокола IProto все взаимодействия, которые по смыслу относятся к одной и той же операции (поток сообщений в ответ на некоторый запрос или просто одно сообщение на один запрос), объединяются по SYNC_ID — соответственно, надо просто регистрировать нужный конечный автомат по SYNC_ID и все входящие сообщения обрабатывать именно зарегистрированным конечным автоматом, находя его по SYNC_ID из прилетающих сообщений. Интерфейс КА, необходимый и достаточный, очень простой:

interface IProtoStateMachine {

	void start(IProtoConnection wire);

	boolean process(IProtoMessage pkt);

	void kill(Throwable e);

}

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

Вот пример простейшего конечного автомата. 

class RequestResponseFSM implements iProtoStateMachine {

	private CompletableFuture<IProtoMessage> future;

	private Timer timer;

	private IProtoMessage request;

@0verride

	boolean process(IProtoMessage pkt) {

		this.future.complete(pkt);

		return true;

	}

}

Код неполный, но даже его достаточно для понимания сути представления автомата. Мы сознательно опускаем такие моменты, как out-of-band-сообщения, реализацию тайм-аутов, нам важно указать на суть: с помощью конечного автомата можно реализовать любые схемы обмена сообщениями, единственным условием для этого нужно только наличие идентификатора SYNC_ID, по которому все сообщения можно отнести к той или иной выполняемой операции.

Высокоуровневый клиент

Теперь, когда с первым и вторым уровнем архитектуры мы разобрались, можно переходить к высокоуровневому клиенту, который будет «лицом» коннектора. Важно, что конкретных требований к способу разработки и выбору паттернов нет — это во многом определяется типом драйвера, особенностями подключаемой СУБД и характеристиками целевой аудитории.

Вместе с тем есть несколько общих рекомендаций, которым желательно следовать при разработке клиента.

Удобство API

API должен быть удобным, работа с ним должна быть интуитивно понятной. Помимо этого, он должен скрывать детали протокола — это важно для безопасности. Это один из самых сложных аспектов разработки высокоуровневого клиента, так как «удобство» и «интуитивная понятность» — вещи достаточно субъективные и их приходится прорабатывать с учетом всех уже известных проблем других коннекторов.

Организационно разработка часто может выглядеть следующим образом: разработчики могут независимо друг от друга выработать свои предложения по API, а дальше они оцениваются и по ним проводятся голосование, review и так далее. Поэтому, если коннектор должен быть доступен другим командам как внешний артефакт, лучше не спешить с выпуском мажорного релиза, который зафиксирует все контракты и их придется поддерживать долгое время. Лучше насобирать граблей и вдумчиво их поизучать.

Mapping

В верхнеуровневом клиенте важно использовать модули mapping для упаковки и распаковки данных. Это нужно, чтобы получать данные в виде нативных объектов. Очень хорошим архитектурным упражнением будет выделение отдельного слоя для сериализации и десериализации, чтобы можно было менять реализации. Например, если появляется библиотека, которая реализует десериализацию в нативные объекты быстрее и лучше, чем Jackson, то это требует работы только в этом слое.

Балансировка, пулы соединений

Балансировку и пулы соединений важно прорабатывать с учетом особенностей протокола. От корректности реализации этих модулей во многом будут зависеть отказоустойчивость и производительность коннектора.

Важно обратить внимание на возможности протокола по мультиплексированию запросов. Канал в рамках протокола может быть:

  • разделяемым и асинхронным, как в IProto, где все сообщения связаны между собой по SYNC_ID и нет никаких ограничений на блокировку доступа к соединению;

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

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

Но с балансировкой возникают некоторые сложности. Обычный подход к балансировке, как это принято в случае с HTTP1.x, не работает, так как соединения имеют состояние. Поэтому балансировку не сделать внешней и требуется выполнить внутреннюю балансировку. В данном случае балансировщик для IProto — это надстройка над пулом соединений, которая выбирает соединения из пула в определенном порядке. Поэтому пул соединений для IProto должен еще уметь обозначать соединения, чтобы по их идентификаторам их можно было вытащить. Приведем простейший интерфейс пула соединений:

interface IProtoClientPool {

  CompletableFuture<IProtoClient> get(IProtoClientId clientId);

}

Отметим, что под соединением на этом уровне мы уже понимаем экземпляры не IProtoConnection, а именно IProtoClient. Класс IProtoClientId отвечает за представление идентификаторов подключений, по которым можно получить нужное соединение из пула.

Также покажем простейший интерфейс балансировщика:

interface IProtoClientBalancer {

  void init(IProtoClientPool pool);

  CompletableFuture<IProtoClient> getNext();

}

Задача балансировщика очень простая: сформировать нужный IProtoClientId и по нему получить соединение из пула.

Отдельные интерфейсы и реализации высокоуровневых клиентов

Высокоуровневых клиентов может быть реализовано несколько разных видов в зависимости от разных задач. Надо учитывать формат использования коннектора и потенциальные сценарии работы. Выбирать режим работы (кластер или одиночный клиент) надо именно исходя из этого. Не рекомендуется выделять с самого начала некоторый «абстрактный интерфейс». Лучше для каждой задачи реализовать отдельный клиент и только потом уже отыскивать общие места.

Грабли и принципы: основные рекомендации

В процессе развития экосистемы Tarantool мы неоднократно сталкивались с разработкой драйверов на практике, а не в теории. Поэтому имеем не только определенный положительный опыт, но и немалое количество набитых шишек. Исходя из этого, мы выработали для себя несколько правил и рекомендаций.

  • Любая операция должна быть конечной во времени. Никаких вечных тайм-аутов (0, -1, null и любых других обозначений вечности). Бесконечный тайм-аут — это потенциальная проблема с зависанием вашего сервиса в будущем.

  • Никаких вечных циклов: while (true) {…}, for (;;) {…}. Это делает продукт нестабильным, непредсказуемым и может сказаться на работоспособности всей ИТ-системы. Любой цикл в коде должен иметь конечное число итераций.

  • Осторожно с рекурсией, особенно в случае с CompletableFuture. Асинхронный код и рекурсия — довольно взрывоопасная смесь.

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

  • Супергибкий, универсальный API — не всегда хорошо. Обусловлено это тем, что универсальное решение сложно не только создавать, но и поддерживать. Зачастую — избыточно трудно и ресурсозатратно. Лучше четко ограничивать контракт.

  • Драйвер сетевой, поэтому база данных — хороший помощник для тестирования. Особенно это актуально в случае Tarantool, потому что на Tarantool можно написать сетевые прокси, которые имитируют задержки. При этом надо помнить, что пирамида тестирования — хорошо, но самые надежные тесты — интеграционные, где есть реальный запущенный экземпляр БД и реальная сеть.

Вместо выводов

Разработка драйвера к СУБД — задача, с которой сталкиваются многие разработчики. Но зачастую создание такого коннектора не легкая прогулка, а сложный процесс, который требует глубокого погружения в архитектуру и особенности СУБД, поиска лучших вариантов и заблаговременного определения совместимости, нагрузок и сценариев использования. Поэтому описанные в статье моменты — не «серебряная пуля» и не подробная пошаговая инструкция How to do step by step для всех вариантов и случаев, а лишь рекомендации, правила и подсказки, которые мы выработали по результатам неоднократной работы с коннекторами. 

Примечание: рекомендую также почитать статью моего коллеги Артема Дубинина «Современный клиент к NoSQL-базе данных». Это большая обзорная статья, где сравниваются несколько баз данных и клиентов. В том числе Tarantool и наш новый драйвер.

Узнавайте о новых релизах, вебинарах и выходящих статьях в Telegram-канале Tarantool News.

О принципах и примерах работы продуктов Tarantool читайте в блоге на сайте.

Комментарии (0)