MonCaché — реализация MongoDB API на основе InterSystems Caché


    ИДЕЯ


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

    МОТИВАЦИЯ


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

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

    ОГРАНИЧЕНИЯ


    В рамках исследовательского проекта было сделано несколько упрощений:
       — используются только примитивные типы данных:
             — 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 Caché:
           — разработать схему представления базы данных в Caché;
           — разработать схему представления коллекций в Caché;
           — разработать схему представления документов в Caché;
           — разработать схему взаимодействия с Caché, используя Node.js;
           — реализовать разработанные схемы и немножко потестить. :)

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


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

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

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

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

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

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

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

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

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

    ^MonCache(<db>) = ""
    

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

    ^MonCache("my_database") = ""
    

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

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

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

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

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

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

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

    Документ, в этом проекте, это JSON документ, расширенный дополнительным типом — ObjectId. Нужно было разработать схему представления документов на иерархических структурах данных. Здесь меня ждало несколько сюрпризов. Во-первых, нет возможности использовать «родной» null в Caché, так как Caché не поддерживает 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
    


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

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

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

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


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

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

    ВОЗМОЖНОСТИ MONCACHÉ


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

       — $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();
      });
    });

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

    // 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 на MonCaché Node.js без перезапуска сервера и изменения исходного кода. Приложение представляет собой крошечную демонстрационную площадку для выполнения CRUD операций над продуктами и офисами, а также интерфейс для смены конфигурации (смены драйвера).

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

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

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

    ЗАКЛЮЧЕНИЕ


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

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

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


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

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


    Если вам интересно реализовать собственный исследовательский проект на технологиях InterSystems, то вы можете посетить специализированный сайт, посвященный академическим программам InterSystems.
    InterSystems
    InterSystems IRIS: СУБД, ESB, BI, Healthcare

    Похожие публикации

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

      +2
      Некоторое увеличение? На сколько?
        +1
        Удавалось достичь прироста в 20%.
          0
          По сравнению с какой версией MongoDB?
            0
            MongoDB shell version: 3.0.4
          +4
          Прежде чем автор ответит на вопрос, скажу что нас, как заказчиков грантового исследования, интересовала прежде всего проверка реализуемости такого сценария: приложение работающее с Mongo переключается на Caché и продолжает прекрасно работать, не зная о том, что оно уже работает не с Mongo. Интересно нам это было как еще один факт подтверждения мультимодельности СУБД InterSystems Caché.
          Автор может сказать и показать о производительности что-то конкретное с цифрами, но это некоторая субъективная история именно этого проекта и по большому счету ничего не значит и не было официальной задачей проекта. Очевидно, что в другом подобном проекте с производительностью может быть все иначе.
          Т.е. это официальный дисклеймер, что все упомянутые цифры не носят никакого официального характера.
          +4
          А немного связанный с предыдущим, но отдельный вопрос — базы данных это не только про соединение и сохранение объектов, но и про поиск. Очень важная часть Mongo API это работа с aggregation pipeline. Насколько хорошо это спроецировалось в вашей реализации? (Ну, т.е. я вижу упоминание операций, и делаю вывод, что скорее всего, это так или иначе работает, но есть вопросы) Подхватываются ли индексы при вычислении агрегатов на стороне Caché, как делается сортировка, вот это вот все? Есть замеры?
            +1
            Очень важная часть Mongo API это работа с aggregation pipeline. Насколько хорошо это спроецировалось в вашей реализации?


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

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


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

            Есть замеры?


            Замеры проводились но, как уже было сказано выше intersystems эти цифры носят частный характер. Могу провести отдельные замеры по интересующим вас сценариям.
              0
              Для реализации aggregation pipeline за основу можно взять mingo — github.com/kofrasa/mingo. Отличная штука.
              +4
              Возможно, стоит подумать над возможными ограничениями выбранной схемы хранения. Тем более, что похожая задача, только для XML, уже решалась
                +2
                Да, недостатки такой схемы понятны с самого начала, т.к. аналогичных механизмов хранения документов в иерархических массивах уже было много. На предыдущей итерации похожей дискуссии был приведен интересный XML документ, который бы хотелось попытаться сохранить в предлагаемой схеме трансляции JSON (мы ведь понимаем, что XML и JSON часто эквивалентные способы сериализации иерархии объектов?)
                https://habrahabr.ru/company/intersystems/blog/264173/#comment_8533249

                Подробно не расписывал и не подсчитывал, и думаю в предел subscript-ов не упремся, но было бы интересно посмотреть.
                  +1
                  И именно по этой причине в проекте DocumentDB на базе движка Cache' (первый видимый результат этого проекта вы можете увидеть уже сейчас в 2016.1 — нативная поддержка JSON в языке, о чем недавно рассказал Штефан Витман на developer community портале), именно по этому схема хранения вложенных коллекций документов будет не простая проекция на иерархический массив, а специализированная структура данных «packed vectors array». О чем мы расскажем при случае.
                +3
                А вы не думали оформить проект в виде storage engine? Тогда и интерфейс не нужно будет воспроизводить и сравнивать с остальными движками будет проще.

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

              Самое читаемое