Что делать, если нужно хранить разнообразные данные децентрализованно? Объекты, массивы, даты, числа, строки, да что угодно. Обязательно ли придумывать мощную СУБД для этого? Ведь часто нам просто нужно хранить и получать данные распределенно, открыто, но максимально просто и без особых притязаний.
В этой статье хотел бы немного раскрыть библиотеку 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 кб в сжатом виде, то, ограничив коллекцию на каждом узле таким значением, мы будем получать все с приемлемой скоростью.
Если создавать децентрализованный проект, в котором нужно хранить разнообразные данные, то вариантов, на данный момент, очень мало. А таких, чтобы все было удобно и доступно для среднестатистического программиста, еще меньше.
По любым вопросам обращайтесь:
zzzzzzzzzzzz
Вот опять — описали API своего проекта, а не вложенные в него идеи. Написали про децентрализованность, но в API-то про эту децентрализованность ничего нет. А как раз в ней все сложные и интересные моменты. Как ноды ищут друг друга? Как документы распределяются по нодам? И прочие стандартные вопросы торрентов-блокчейнов.
IsOrtex Автор
В следующей статье уже как раз все это распишу. Просто механизм распределения и получения данных один и тот же во всех слоях, не хотелось дублировать все в каждой статье.