ИДЕЯ


Идея проекта — попробовать реализовать базовые функции MongoDB API для поиска, сохранения, обновления и удаления документов так, чтобы можно было бы не меняя клиентский код вместо MongoDB использовать InterSystems Cache.

МОТИВАЦИЯ


Возможно, если взять интерфейс MongoDB и в качестве хранилища данных использовать InterSystems Cache, то можно получить некоторый выигрыш в производительности.

Ну, а почему бы и нет?! ?\_(?)_/?

ОГРАНИЧЕНИЯ


В рамках исследовательского проекта было сделано несколько упрощений:
   — используются только примитивные типы данных:
         — null, boolean, number, string, array, object, ObjectId;
   — клиентский код работает с MongoDB посредством MongoDB драйвера;
   — клиентский код использует MongoDB Node.js driver;
   — клиентский код использует только базовые функции MongoDB API:
         — find, findOne — поиск документов;
         — save, insert — сохранение документов;
         — update — обновление документов;
         — remove — удаление документов;
         — count — подсчет документов.

РЕАЛИЗАЦИЯ


В итоге задача разбилась на следующие подзадачи:
   — воспроизвести интерфейс MongoDB Node.js driver по выбранным базовым функциям;
   — реализовать этот интерфейс, используя в качестве хранилища данных — InterSystems Cache:
       — разработать схему представления базы данных в Cache;
       — разработать схему представления коллекций в Cache;
       — разработать схему представления документов в Cache;
       — разработать схему взаимодействия с Cache, используя Node.js;
       — реализовать разработанные схемы и немножко потестить. :)

ДЕТАЛИ РЕАЛИЗАЦИИ


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

MongoDB определяет базу данных как физический контейнер для коллекций. А коллекцию как набор документов. И, наконец, документ, как набор данных. Документ подобен JSON документу, но с большим количеством допустимых типов — BSON.

В InterSystems Cache все данные хранятся в глобалах. Упрощенно, можно думать о глобалах как о иерархических структурах данных.

В этом проекте все данные будут храниться в одном глобале — ^MonCache.

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

Схема представления базы данных в Cache

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

Я выбрал самый простой и очевидный способ решения поставленной задачи. Базы данных представляются узлом первого уровня в глобале ^MonCache. Кроме этого такому узлу приписывается значение "", для того, чтобы реализовать поддержку «пустых» баз данных. Всё дело в том, что если этого не делать и просто добавлять дочерние узлы, то как только все дочерние узлы будут удалены, родительский узел также будет удален (особенности глобалов).

Итого, каждая база данных представляется в Cache в следующем виде:

^MonCache(<db>) = ""

Например, представление базы данных «my_database» будет таким:

^MonCache("my_database") = ""

Схема представления коллекций в Cache

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

Итого, каждая коллекция представляется в Cache в следующем виде:

^MonCache(<db>) = ""
^MonCache(<db>, <collection>) = 0

Например, представление коллекции «my_collection» в базе данных «my_database» будет таким:

^MonCache("my_database") = ""
^MonCache("my_database", "my_collection") = 0

Схема представления документов в Cache

Документ, в этом проекте, это JSON документ, расширенный дополнительным типом — ObjectId. Нужно было разработать схему представления документов на иерархических структурах данных. Здесь меня ждало несколько сюрпризов. Во-первых, нет возможности использовать «родной» null в Cache, так как Cache не поддерживает null. Второй интересный момент в том, что boolean значения реализованы константами 0 и 1. Т.е., грубо говоря, true — 1, false — 0. Самым ожидаемым проблемным моментом стало то, что нужно придумать как хранить ObjectId. В общем, все эти проблемы были успешно решены в самой, как мне казалось, простой форме. Далее, я рассмотрю каждый тип данных и его представление.

Схемы представления
Для более лаконичной записи я буду использовать специальное обозначение — @.
Вместо ^MonCache(<db>,<collection>,<document id>, ...) я буду просто писать
@(...).

Пусть есть поле f типа «null».

f: null

Определим для него следующее представление:

@("f", "t") = "null"

Пусть есть поле f типа «boolean» (значение true).

f: true

Определим для него следующее представление:

@("f", "t") = "boolean"
@("f", "v") = 1

Пусть есть поле f типа «boolean» (значение false).

f: false

Определим для него следующее представление:

@("f", "t") = "boolean"
@("f", "v") = 0

Пусть есть поле f типа «number».

f: 3.14

Определим для него следующее представление:

@("f", "t") = "number"
@("f", "v") = 3.14

Пусть есть поле f типа «string».

f: 'Habrahabr.ru'

Определим для него следующее представление:

@("f", "t") = "string"
@("f", "v") = "Habrahabr.ru"

Пусть есть поле f типа «ObjectId».

f: ObjectId('56b43c20af9c4f3fe2cc2908')

Определим для него следующее представление:

@("f", "t") = "objectid"
@("f", "v") = "56b43c20af9c4f3fe2cc2908"

Осталось два типа: «object» и «array». По своей сути эти типы являются «контейнерами» для значений более «простых» типов. Поэтому можно просто рекурсивно применить уже описанные правила и получить представления для элементов этих контейнеров. Единственный тонкий момент — нужно придумать способ сохранения порядка элементов в контейнере типа «array». Это решается тривиально — все элементы нумеруются в порядке обхода, и в том же порядке производится представление.

Пусть есть поле f типа «object» (пустой).

f: {}

Определим для него следующее представление:

@("f", "t") = "object"

Пусть есть поле f типа «object».

f: { site: 'Habrahabr.ru', topic: 276391 }

Определим для него следующее представление:

@("f", "t") = "object"
@("f", "v", "site", "t") = "string"
@("f", "v", "site", "v") = "Habrahabr.ru"
@("f", "v", "topic", "t") = "number"
@("f", "v", "topic", "v") = 276391

Пусть есть поле f типа «array» (пустой).

f: []

Определим для него следующее представление:

@("f", "t") = "array"

Пусть есть поле f типа «array».

f: [ 'Habrahabr.ru', 276391 ]

Определим для него следующее представление:

@("f", "t") = "array"
@("f", "v", 0, "t") = "string"
@("f", "v", 0, "v") = "Habrahabr.ru"
@("f", "v", 1, "t") = "number"
@("f", "v", 1, "v") = 276391


Схема взаимодействия с Cache

Логичным и простым выбором драйвера для работы с InterSystems Cache стал выбор Node.js драйвера (на сайте документации можно увидеть и другие драйверы для взаимодействия с Cache). Однако, сразу стоит отметить, что возможностей драйвера было недостаточно. Хотелось делать несколько вставок и всё это в рамках одной транзакции. Поэтому было принято решение разработать набор Cache ObjectScript классов, которые использовались для имитации MongoDB API, но на стороне Cache.

Cache Node.js драйвер не умел обращаться к классам в Cache, но зато умел делать вызовы программ в Cache. Этот факт привел к написанию небольшой программки — своеобразного мостика между драйвером и классами в Cache.

В итоге схема выглядела следующим образом:


В рамках работы над проектом был разработан специальный формат NSNJSON (Not So Normal JSON), который позволял «протаскивать» ObjectId, null, true, false через драйвер в Cache. С данным форматом можно ознакомиться на соответствующей странице на GitHub — NSNJSON. На Хабрахабр я выкладывал три статьи, посвященные этому формату:

   — Усложнённый упрощённый JSON;
   — JSON для любителей скобочек;
   — NSNJSON. ? (Заключительная статья).

ВОЗМОЖНОСТИ MONCACHE


При выполнении операции поиска документов поддерживаются следующие критерии:

   — $eq — эквивалентность;
   — $ne — не эквивалентно;
   — $not — отрицание критерия;
   — $lt — менее чем;
   — $gt — более чем;
   — $exists — существование.

При выполнении операции обновления документов поддерживаются следующие операторы:

   — $set — установка значения;
   — $inc — инкрементирование значения на заданную величину;
   — $mul — умножение значения на заданную величину;
   — $unset — удаление значения;
   — $rename — переименование значения.

ПРИМЕР


Я взял этот код со страницы официального драйвера и немного переделал его.

var insertDocuments = function(db, callback) {
  var collection = db.collection('documents');
  collection.insertOne({ site: 'Habrahabr.ru', topic: 276391 }, function(err, result) {
    assert.equal(err, null);
    console.log("Inserted 1 document into the document collection");
    callback(result);
  });
}

var MongoClient = require('mongodb').MongoClient
  , assert = require('assert');

var url = 'mongodb://localhost:27017/myproject';

MongoClient.connect(url, function(err, db) {
  assert.equal(null, err);
  console.log("Connected correctly to server");

  insertDocument(db, function() {
    db.close();
  });
});

Этот код можно легко переделать чтобы он работал с MonCache!
Надо просто сменить драйвер!

// var MongoClient = require('mongodb').MongoClient
var MongoClient = require('moncache-driver').MongoClient

После выполнения этого кода глобал ^MonCache будет выглядеть следующим образом:
^MonCache("myproject","documents")=1
^MonCache("myproject","documents",1,"_id","t")="objectid"
^MonCache("myproject","documents",1,"_id","v")="b18cd934860c8b26be50ba34"
^MonCache("myproject","documents",1,"site","t")="string"
^MonCache("myproject","documents",1,"site","v")="Habrahabr.ru"
^MonCache("myproject","documents",1,"topic","t")="number"
^MonCache("myproject","documents",1,"topic","v")=267391

ДЕМО


Кроме всего прочего было запущено небольшое демо приложение (исходники), также реализованное на Node.js для демонстрации смены драйвера с MongoDB Node.js на MonCache Node.js без перезапуска сервера и изменения исходного кода. Приложение представляет собой крошечную демонстрационную площадку для выполнения CRUD операций над продуктами и офисами, а также интерфейс для смены конфигурации (смены драйвера).

Сервер позволяет создавать продукты и офисы, которые сохраняются в выбранное в конфигурации хранилище (Cache или MongoDB).

Вкладка «Заказы» выводит список заказов. Записи я создал, но форму не допилил, вы можете помочь проекту (исходники).

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

ЗАКЛЮЧЕНИЕ


В заключении отвечу на главный вопрос. Да! Действительно удалось получить некоторое увеличение производительности выполнения базовых операций.

Проект MonCache опубликован на GitHub и доступен под лицензией MIT.

КРАТКАЯ ИНСТРУКЦИЯ


  1. Установите Cache
  2. Загрузите все необходимые компоненты MonCache в Cache
  3. Создайте в Cache область MONCACHE
  4. Создайте в Cache пользователя moncache с паролем ehcacnom
  5. Создайте переменную окружения MONCACHE_USERNAME = moncache
  6. Создайте переменную окружения MONCACHE_PASSWORD = ehcacnom
  7. Создайте переменную окружения MONCACHE_NAMESPACE = MONCACHE
  8. Измените в вашем проекте зависимость от 'mongodb' на 'moncache-driver'
  9. Запускайте ваш проект! :-)

АКАДЕМИЧЕСКАЯ ПРОГРАММА INTERSYSTEMS


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

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


  1. Scratch
    08.02.2016 12:51
    +2

    Некоторое увеличение? На сколько?


    1. jxcoder
      08.02.2016 13:41
      +1

      Удавалось достичь прироста в 20%.


      1. wickedweasel
        08.02.2016 14:05

        По сравнению с какой версией MongoDB?


        1. jxcoder
          08.02.2016 14:08

          MongoDB shell version: 3.0.4


    1. intersystems
      08.02.2016 13:45
      +4

      Прежде чем автор ответит на вопрос, скажу что нас, как заказчиков грантового исследования, интересовала прежде всего проверка реализуемости такого сценария: приложение работающее с Mongo переключается на Cache и продолжает прекрасно работать, не зная о том, что оно уже работает не с Mongo. Интересно нам это было как еще один факт подтверждения мультимодельности СУБД InterSystems Cache.
      Автор может сказать и показать о производительности что-то конкретное с цифрами, но это некоторая субъективная история именно этого проекта и по большому счету ничего не значит и не было официальной задачей проекта. Очевидно, что в другом подобном проекте с производительностью может быть все иначе.
      Т.е. это официальный дисклеймер, что все упомянутые цифры не носят никакого официального характера.


  1. tsafin
    08.02.2016 13:34
    +4

    А немного связанный с предыдущим, но отдельный вопрос — базы данных это не только про соединение и сохранение объектов, но и про поиск. Очень важная часть Mongo API это работа с aggregation pipeline. Насколько хорошо это спроецировалось в вашей реализации? (Ну, т.е. я вижу упоминание операций, и делаю вывод, что скорее всего, это так или иначе работает, но есть вопросы) Подхватываются ли индексы при вычислении агрегатов на стороне Cache, как делается сортировка, вот это вот все? Есть замеры?


  1. jxcoder
    08.02.2016 13:48
    +1

    Очень важная часть Mongo API это работа с aggregation pipeline. Насколько хорошо это спроецировалось в вашей реализации?


    Если вы имеете ввиду Aggregation
    Pipeline
    , то в данной реализации это не поддерживается.

    Подхватываются ли индексы при вычислении агрегатов на стороне Cache, как делается сортировка, вот это вот все?


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

    Есть замеры?


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


    1. zag2art
      08.02.2016 14:18

      Для реализации aggregation pipeline за основу можно взять mingo — github.com/kofrasa/mingo. Отличная штука.


  1. doublefint
    08.02.2016 14:18
    +4

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


    1. tsafin
      09.02.2016 02:29
      +2

      Да, недостатки такой схемы понятны с самого начала, т.к. аналогичных механизмов хранения документов в иерархических массивах уже было много. На предыдущей итерации похожей дискуссии был приведен интересный XML документ, который бы хотелось попытаться сохранить в предлагаемой схеме трансляции JSON (мы ведь понимаем, что XML и JSON часто эквивалентные способы сериализации иерархии объектов?)
      https://habrahabr.ru/company/intersystems/blog/264173/#comment_8533249

      Подробно не расписывал и не подсчитывал, и думаю в предел subscript-ов не упремся, но было бы интересно посмотреть.


      1. tsafin
        09.02.2016 02:36
        +1

        И именно по этой причине в проекте DocumentDB на базе движка Cache' (первый видимый результат этого проекта вы можете увидеть уже сейчас в 2016.1 — нативная поддержка JSON в языке, о чем недавно рассказал Штефан Витман на developer community портале), именно по этому схема хранения вложенных коллекций документов будет не простая проекция на иерархический массив, а специализированная структура данных «packed vectors array». О чем мы расскажем при случае.


  1. r4nd0m
    09.02.2016 11:41
    +3

    А вы не думали оформить проект в виде storage engine? Тогда и интерфейс не нужно будет воспроизводить и сравнивать с остальными движками будет проще.


    1. intersystems
      09.02.2016 15:45

      Отличная идея. Хорошая заявка на грант InterSystems.