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