Metastocle — децентрализованное хранилище данных

    image


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


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


    Немного предыстории


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


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


    Сохраняя файлы, нам нужно куда-то записать хэши, чтобы потом иметь к ним доступ. Как раз для этого нам и пригодился metastocle. В нем мы храним все что нужно: названия песен, ссылки на файлы и.т.д.


    В итоге, все это было приведено к некому универсальному виду, и система состоит из трех основных сущностей:


    • Коллекции — аналогично популярным nosql базам данных, сущность для определения структуры данных, различных опций и.т.п.
    • Документы — непосредственно сами данные, в виде объектов.
    • Действия (инструкции) — набор правил для обработки требуемых данных: фильтрация, сортировка, лимитирование и.т.д.

    Давайте посмотрим пару примеров:


    Сервер:


    const Node = require('metastocle').Node;
    
    (async () => {
      try {
        const node = new Node({
          port: 4000,
          hostname: 'localhost'
        });
    
        // Создаем коллекцию
        await node.addCollection('test', { limit:  10000, pk:  'id' });
        await node.init();
      }
      catch(err) {
        console.error(err.stack);
        process.exit(1);
      }
    })();

    Клиент:


    const Client = require('metastocle').Client;
    
    (async () => {
      try {
        const client = new Client({
          address: 'localhost:4000'
        });
        await client.init();
    
        // Добавляем документ
        const doc = await client.addDocument('test', { text: 'hi' });
    
        // Обновляем этот документ
        await client.updateDocuments('test', { text: 'bye' }, {
          filter: { id: doc.id }
        });
    
        // Добавляем еще один документ
        await client.addDocument('test', { id: 2, text: 'new' });
    
        // Получаем второй документ
        const results = await client.getDocuments('test', {
          filter: { id: 2 }
        });
    
        // Получаем его иначе
        const doc2 = await client.getDocumentById('test', 2));
    
        // Добавляем еще документов
        for(let i = 10; i <= 20; i++) {
          await client.addDocument('test', { id: i, x: i });
        }
    
        // Получаем документы, соответствующие всем условиям
        const results2 = await client.getDocuments('test', {
          filter: { id: { $gt: 15 } },
          sort: [['x', 'desc']],
          limit:  2,
          offset:  1,
          fields: ['id']
        });
    
        // Удаляем документы, у которых id > 15
        await client.deleteDocuments('test', {
          filter: { id: { $gt: 15 } }
        });
      }
      catch(err) {
        console.error(err.stack);
        process.exit(1);
      }
    })();

    Клиенты не могут создавать коллекции, сеть сама устанавливает структуру, а пользователи лишь работают с документами. Коллекции можно описывать и декларативно, через опции узла:


    const node = new Node({
       port: 4000,
       hostname: 'localhost',
       collections: {
         test: { limit: 10000, pk: 'id' }
       }
     });

    Основные настройки коллекции:


    • pk — поле первичного ключа. Можно не указывать, если таковое не требуется. Если поле указано, то, по умолчанию, создается uuid хэш, но при желании можно прокинуть любую строку или число.
    • limit — максимальное количество документов на одном узле
    • queue — режим очереди: если включен, то при достижении лимита  определенные документы удаляются, чтобы записать новые
    • limitationOrder — если лимитирование и очередь включены, то можно указать правила сортировки для определения удаляемых документов. По умолчанию, удаляются те, с которыми уже давно не работали.
    • schema — структура полей документов
    • defaults — значения по умолчанию полей документов. 
    • hooks — хуки полей документов. 
    • preferredDuplicates — можно указать предпочтительное количество дубликатов документа в сети.

    Структура полей коллекции (schema) может быть описана в виде:


    { 
      type: 'object',
      props: {
        count: 'number',
        title: 'string',
        description: { type: 'string' },
        priority: {
          type: 'number',
          value: val => val >= -1 && val <= 1
        },
        goods: {
          type: 'array',
          items: {
            type: 'object',
            props: {
              title: 'string',
              isAble: 'boolean'
            }
          }
        }
      }
    }

    Полный набор всех правил можно найти в функции utils.validateSchema() в https://github.com/ortexx/spreadable/blob/master/src/utils.js


    Значения по умолчанию и хуки могут быть в виде:


    { 
      defaults: {
        date: Date.now
        priority: 0
        'nested.prop': (key, doc) => Date.now() - doc.date
      },
      hooks: {
        priority: (val, key, doc, prevDoc) => prevDoc? prevDoc.priority + 1: val
      }
    }

    Основные особенности библиотеки:


    • Работа по принципу CRUD
    • Хранение всех типов данных Javascript, которые можно сериализовать, в том числе вложенных.
    • Данные могут добавляться в хранилище через любой узел
    • Данные могут дублироваться для большей надежности
    • Запросы могут содержать вложенные фильтры

    Изоморфность


    Клиент написан на javascript и изоморфен, его можно использовать прямо из браузера. 


    Можно загрузить файл https://github.com/ortexx/metastocle/blob/master/dist/metastocle.client.jsкак скрипт и получить доступ к window.ClientMetastocle либо импортить через систему сборки и.т.п.


    Api клиента


    • async Client.prototype.addDocument() — добавление документа в коллекцию
    • async Client.prototype.getDocuments() — получение документов из коллекции по каким-либо инструкциям
    • async Client.prototype.getDocumentsСount() — получение количества документов в коллекции
    • async Client.prototype.getDocumentByPk() — получение документа из коллекции по первичному ключу
    • async Client.prototype.updateDocuments() — обновление документов в коллекции по каким-либо инструкциям
    • async Client.prototype.deleteDocuments() — удаление документов из коллекции по каким-либо инструкциям

    Основные действия (инструкции):


    .filter — фильтрация данных, пример:


    { 
      a: { $lt: 1 },
      $and: [
        { x: 1 },
        { y: { $gt: 2 } },
        { 
          $or: [
            { z: 1 },
            { "b.c": 2 }
          ] 
        }
      ]
    }

    .sort — сортировка данных, пример:


    { sort: [['x', 'asc'], ['y.z', 'desc']] }

    .limit — количество данных


    .offset — начальная позиция отбора данных


    .fields — необходимые поля в документах


    Более подробно все инструкции и возможные значения описаны в readme.


    Работа через командную строку


    Библиотеку можно использовать через командную строку. Для этого нужно установить ее глобально: npm i -g metastocle --unsafe-perm=true --allow-root. После этого можно запускать нужные экшены из директории с проектом, где узел.


    Например, metastocle -a getDocumentByPk -o test -p 1 -c ./config.js, чтобы получить документ с первичным ключом 1 из коллекции test. Все экшены можно найти в https://github.com/ortexx/metastocle/blob/master/bin/actions.js


    Ограничения


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

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


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


    • Нужно максимально сужать выборку получаемых данных, стараться организовывать все так, чтобы получать документы по ключам, либо по каким-то другим полям, но отфильтрованным до оптимальных размеров. 
    • Если же данных все-таки надо тянуть много, то придется лимитировать каждый сервер, исходя их оптимальных размеров, для передачи их по сети. Скажем, если 10000 документов в какой-то коллекции весят 100 кб в сжатом виде, то, ограничив коллекцию на каждом узле таким значением, мы будем получать все с приемлемой скоростью.

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


    По любым вопросам обращайтесь:


    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

      +1
      Вот опять — описали API своего проекта, а не вложенные в него идеи. Написали про децентрализованность, но в API-то про эту децентрализованность ничего нет. А как раз в ней все сложные и интересные моменты. Как ноды ищут друг друга? Как документы распределяются по нодам? И прочие стандартные вопросы торрентов-блокчейнов.
        0

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

        0

        В чем отличие от того же IPFS?

          0
          Если изнутри:
          Все, что построено на стандартном dht работает по принципу хэш -> данные. То есть поиск осуществляется по хэшу. В моей реализации нет привязки ни к чему, акцент на быстрый обход сети для получения данных, по любым условиям. Детали опишу в следующей статье.

          Если снаружи:
          Почти ничего общего нет.

          Если пройдетесь по предыдущим статьям, то ответ будет полнее.
          0

          почему p2p, если предполагаются роли сервера и клиента?

            0
            Где p2p? Метка что ли одна в конце? :) Так это просто близкие тематики, кто-то ищет p2p, ему может быть интересно.

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

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