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 кб в сжатом виде, то, ограничив коллекцию на каждом узле таким значением, мы будем получать все с приемлемой скоростью.

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


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