Подход сохранения данных для веб-приложений с Интернет-компьютером фонда DFINITY.
Мы разрабатываем доказательство концепции для переноса нашего веб-приложения DeckDeckGo на Интернет-компьютер DFINITY.
После проверки интеграции хостинга и аутентификации без пароля мы решаем последний вопрос нашего POC: сохранение пользовательских данных и презентаций в цепочке блоков.
Попутно мы опробовали две концепции:
- «консервативный»: сохранение данных в едином хранилище, подобном базе данных.
- «футуристический»: создание смарт-контракта, похожего на базу данных, на лету для каждой колоды, созданной пользователем.
В этой статье я представляю эти два подхода.
Введение
Объем этого сообщения в блоге ограничен простыми концепциями базы данных ключ-значение. На такую настойчивость мы рассчитываем в DeckDeckGo.
Я не знаю, реализовал ли кто-нибудь более тяжелые концепции с помощью IC (Интернет-компьютер), такие как полнофункциональная база данных SQL, работающая на блокчейне.
Вы, конечно, можете оспорить это, прокомментировав сообщение в блоге или обратившись к 😃.
«В наши дни»
Прежде чем копаться в Internet Computer, давайте сначала рассмотрим некоторые общие устаревшие концепции.
В настоящее время, независимо от того, без сервера или нет, сохранение данных часто решается с помощью одной базы данных для всей информации.
Пользователи используют (веб-приложение). Он вызывает конечные точки для сохранения и чтения данных и возвращает результаты пользователям.
В микросервисной архитектуре приложение (я) разделено как набор сервисов, но в некоторой степени результат основан на той же концепции.
Пользователи используют (веб-приложение). Он вызывает выделенную службу, которая, в свою очередь, вызывает конечные точки для сохранения и чтения данных и возвращает результаты пользователям.
Независимо от этих архитектур, моно- или микросервисов, данные хранятся в монолитной базе данных.
Фрагмент кода
Я не знаю точной архитектуры Google Firestore, но с точки зрения пользователя и со стороны это выглядит как монобаза, размещенная в облаке.
Если мы разработаем, например, приложение, в котором перечислены виды животных, которыми владеют наши пользователи, мы, скорее всего, определим db-коллекцию `pets` для сбора их записей.
Чтобы запросить и сохранить данные, мы должны реализовать в нашем веб-приложении функции получения (get
) и установщика (add
), которые будут вызывать облачную базу данных.
import firebase from 'firebase/app'; import 'firebase/firestore'; const firestore = firebase.firestore(); const get = async (entryId) => { const snapshot = await firestore.collection('pets').doc(entryId).get(); if (snapshot.exists) { console.log(snapshot.data()); } }; const add = async (userId, dog, cat) => { const {id: entryId} = await firestore.collection('pets').add({ userId, dog, cat }); console.log(entryId); };
Все пользователи будут использовать одну и ту же точку входа, и их данные также будут сохранены в одной коллекции (firestore.collection('pets')
).
«Консерватор»
Интернет-компьютер не предоставляет встроенное решение для базы данных, просто добавляющее воды, что не является целью фонда DFINITY (насколько я понимаю). Они обеспечивают футуристическую сеть блокчейнов, которая также может запускать веб-приложения. Это все".
Создатели должны создавать на его основе функции. Они могут реализовать функции и реализовать их на ИС с помощью смарт-контрактов.
Я представляю «канистру» как контейнер, который выполняет набор функций в облаке, за исключением того, что на самом деле это децентрализованная сеть блокчейнов 😉. По сути, он может делать все, что можно реализовать, от вычислений до сохранения данных.
После изучения документации SDK, примеров и связанной демонстрации, наш первый подход имел целью воспроизвести знакомый нам подход, то есть подход сегодняшнего дня или монолитная база данных.
Однако, учитывая блокчейн-характер Интернет-компьютера, было бы неплохо представить архитектуру децентрализованным образом с данными, совместно используемыми в форме блоков.
Пользователи используют (веб-приложение), оно вызывает конечные точки для сохранения и чтения данных и возвращает результаты пользователям.
Если мы опускаем аспект блокчейна (и децентрализацию), он в основном работает так же, как то, с чем я знаком, не так ли?
Фрагмент кода
То, как мы можем представить сохранение данных в контейнере, как мы это делали бы для нашего веб-приложения «домашних животных», снова будет очень похоже на предыдущую архитектуру.
Вместо коллекции базы данных «ключ-значение» - поскольку встроенных баз данных нет - у нашего актора просто будет атрибут, содержащий данные.
Этим атрибутом может быть, например, Trie, функциональная карта (и набор), представление которой является каноническим и не зависит от истории операций.
Чтобы запросить (get
) и сохранить (add
) данные, мы могли бы реализовать актор, контейнер, как показано ниже:
Канистры и функции, представленные в Интернете. Компьютер обычно может быть написан на Motoko или Rust. Motoko публикуется и поддерживается фондом DFINITY, и большинство примеров документации предоставлено на этом конкретном языке программирования. В основном поэтому мы разработали концепцию проверки вместе с Motoko.
import Trie "mo:base/Trie"; import Nat32 "mo:base/Nat32"; actor { type PetId = Nat32; type Data = { dog: Bool; cat: Bool; userId: Text; }; private stable var entryId: PetId = 0; private stable var pets : Trie.Trie<PetId, Data> = Trie.empty(); public func add(userId: Text, dog: Bool, cat: Bool): async () { let data = { userId = userId; dog = dog; cat = cat; }; pets := Trie.replace( pets, key(entryId), Nat32.equal, ?data, ).0; entryId += 1; }; public query func get(entryId: Nat32) : async ?Data { let result = Trie.find(pets, key(entryId), Nat32.equal); return result; }; private func key(x : PetId) : Trie.Key<PetId> { return { hash = x; key = x }; }; };
Данные будут сохранены в смарт-контракте накопителя, реплицированы и децентрализованы с использованием знакомого нам подхода.
Примечание. Ссылка на соответствующую площадку Motoko Playground, чтобы попробовать поиграть с приведенным выше примером.
«Футуристический 🤯»
Наш первый «консервативный» подход подтвердил гипотезу о возможности сохранения данных для нашего веб-приложения в Internet Compute. Но как насчет масштабируемости?
В нашем первом подходе мы не использовали Trie
, как в примере; вместо этого мы храним данные в HashMap
. Таким образом, это сделало бы систему немного более масштабируемой, поскольку данные потенциально все равно доставлялись бы быстрее, даже с большим объемом данных. Однако в какой-то момент мы все равно достигли определенных пределов.
Кроме того, накопитель может хранить только 4 ГБ страниц памяти из-за ограничений реализаций WebAssembly (источник).
Вот почему мы оспорили нашу первую идею и попытались найти более масштабируемую конструкцию.
Это привело к архитектуре, в которой главный субъект действует как менеджер, который - на лету, после создания объекта - генерирует децентрализованную безопасную простую базу данных типа ключ-значение для каждого отдельного сохранения данных каждого пользователя 🤯.
На приведенной выше диаграмме я отображал только двух пользователей и не отражал блокчейн-характер сети, но, надеюсь, хорошо отражает идею.
Пользователь через веб-приложение запрашивает у менеджера персональный децентрализованный безопасный простой контейнер для хранения данных типа "ключ-значение". После получения он / она использует это выделенное личное пространство для запроса и сохранения своих данных.
Фрагмент кода
Для реализации такого решения нам потребуются два участника. Один, который действует как «менеджер», контейнер, который генерирует другие накопители на лету, и реализация для накопителей, которые заботятся о сохранении данных, тех, которые генерируются на лету.
«Менеджеру», возможно, придется отслеживать или нет список контейнеров, которые он сгенерировал, если конечные пользователи не сохранят на своей стороне свои ссылки. Однако в следующем фрагменте мы предположим, что он действительно должен отслеживать, что он генерирует.
Базовая реализация менеджера близка к тому, что мы уже реализовали. Однако вместо Trie
я использовал HashMap для отслеживания идентификаторов сгенерированных контейнеров.
import HashMap "mo:base/HashMap"; import Principal "mo:base/Principal"; import Cycles "mo:base/ExperimentalCycles"; import Iter "mo:base/Iter"; import Pet "./Pet"; actor { type CanisterId = Principal; type UserId = Principal; private func isPrincipalEqual(x: Principal, y: Principal): Bool { x == y }; private var canisters: HashMap.HashMap<UserId, CanisterId> = HashMap.HashMap<UserId, CanisterId>(10, isPrincipalEqual, Principal.hash); private stable var upgradeCanisters: [(Principal, CanisterId)] = []; public shared({caller}) func create(): async (CanisterId) { Cycles.add(1_000_000_000_000); let canister = await Pet.Pet(); let id: CanisterId = await canister.id(); canisters.put(caller, id); return id; }; public shared query({caller}) func get() : async ?CanisterId { let id: ?CanisterId = canisters.get(caller); return id; }; system func preupgrade() { upgradeCanisters := Iter.toArray(canisters.entries()); }; system func postupgrade() { canisters := HashMap.fromIter<UserId, CanisterId> (upgradeCanisters.vals(), 10, isPrincipalEqual, Principal.hash); upgradeCanisters := []; }; };
Функция create
заботится о высвобождении накопителей новых пользователей в Интернет-компьютере, которые будут использоваться для сохранения данных. Сначала он выделяет минимальное количество циклов для новых участников.
Функция get
возвращает идентификатор контейнера пользователя, если он существует.
И preupgrade
, и postupgrade
используются для сохранения памяти между обновлениями (см. Документацию).
После разработки менеджера мы сможем реализовать actor
, предназначенный для сохранения личных данных пользователя.
import Principal "mo:base/Principal"; actor class Pet() = this { type Data = { dog: Bool; cat: Bool; }; private stable var myPet: ?Data = null; public func set(dog: Bool, cat: Bool): async () { myPet := ?{ dog = dog; cat = cat; }; }; public query func get() : async ?Data { return myPet; }; public query func id() : async Principal { return Principal.fromActor(this); }; };
Его реализация выглядит знакомой с тем, что мы видели раньше, за исключением небольшого (но важного) изменения: нет ни entryId
, ни Trie
тоже.
У каждого пользователя есть свое маленькое королевство. Вот почему ему не нужен идентификатор для поиска данных в большом массиве данных. Ему даже не нужно запоминать userId
.
Это царство пользователя, оно содержит только личные данные этого конкретного пользователя!
Примечание. Ссылка на соответствующую площадку Motoko Playground, чтобы проверить пример выше.
Плюсы и минусы
Конечная архитектура действительно масштабируема, так как каждый пользователь работает в своей собственной сфере. Он также разделяет интересы на четкую принадлежность и хорошо подходит для безопасного подхода.
Однако стоит отметить, что это связано с дополнительным администрированием, будь то во время обновления их кодов или обработки их затрат.
Тем не менее, мы большие поклонники этой архитектуры и считаем, что преимущества того стоят. Следовательно, это концепция, которую мы использовали бы, если бы наше доказательство концепции превратилось в реальный жизненный вариант продуктивного использования.
В конце концов, великая держава сопряжена с большой ответственностью!
Резюме
Это довольно непростая тема. Я надеюсь, что мне удалось передать основную идею этих архитектур и, что наиболее важно, ту, которую я назвал «футуристической».
Бесконечность не предел!
Дэйвид
Вы можете связаться со мной в Твиттере или на моем сайте.
Попробуйте DeckDeckGo для следующих слайдов.
Грантовая программа
Нам невероятно повезло, что нас выбрали для участия в Программе грантов для разработчиков DFINITY, чтобы поддержать экосистему разработчиков, наградить команды за создание децентрализованных приложений, инструментов и инфраструктуры на Интернет-компьютере.
Я не могу не подчеркнуть тот факт, что если у вас есть отличный проект, вам обязательно нужно подать заявку. До сих пор это было не что иное, как потрясающий опыт, и просто дайте почувствовать себя причастным к чему-то, что опережает свое время.
Все рисунки в этой статье были разработаны с помощью Excalidraw, какого изящного инструмента для рисования.