Holy Grail на стероидах: тотальная синхронизация и изоморфный JavaScript на Swarm.js

    Сегодня на Хабре мы представляем технологию реплицированной модели, которая позволяет создавать коллаборативные и реалтаймовые веб приложения так же легко, как локальные десктопные. Мы считаем, что при разработке приложений, синхронизация данных в реальном времени должна быть доступна так же, как TCP поток, HTTP запрос или ток из розетки — сразу и без вопросов. HTML5 приложения, написанные на Swarm, в части автономности, локальности и скорости загрузки не уступают нативным.
    Используя библиотеку Swarm, за выходные мы делаем больше, чем за месяц делали без Swarm. Что важней — можем делать то, что без неё делать вообще не могли. Эту библиотеку синхронизации мы предлагаем совершенно бесплатно.



    Сегодня мы выкладываем TodoMVC++, реактивное HolyGrail-на-стероидах приложение, написанное на Swarm+React. Приведу список демонстрируемых в приложении возможностей:

    • Мгновенная загрузка — страница отрисовывается на сервере и приходит на клиента, как сжатый HTML; затем, подтягиваются код и данные, и страница оживляется. Isomorphic JavaScript в действии.
    • Кэширование данных в WebStorage — позволяет как убыстрить загрузку, так и работать в оффлайне, не теряя результатов работы.
    • Оффлайновая работа — с данными уже понятно, а если добавляем cache manifest, то HTML5 приложение может загружаться и работать без интернета!
    • Синхронизация в реальном времени — откройте несколько закладок (синхронизация через WebStorage) или откройте ту же страницу на телефоне/айпэде/другом браузере (WebSocket).
    • Сложносинхронизируемые структуры данных (да-да, именно сло-жно-син-хро-ни-зи-ру-е-мы-е).

    В целом, приложение написано без каких-либо отсылок к сети, как простое (локальное) MVC приложение. Синхронизация и кеширование полностью происходит на уровне библиотеки Swarm, а приложение работает с локальными Backbone-подобными объектами модели.

    Итак, вот само приложение: ppyr.us.
    Вот код: github.com/gritzko/todomvc-swarm

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

    Сегодня, синхронизация весьма востребована и на клиенте и на сервере. В серверном хозяйстве интернет-гигантов появляется всё больше различных хранилищ и средств обработки данных, которые требуют синхронизации. Принцип одной большой БД, как источника истины – трудномасштабируем. Но мы говорим, в первую очередь, о клиенте. Сегодня, рядовой пользователь обзавёлся множеством устройств. Когда данные, которые он видит на экране ноутбука, не соответствуют данным на экране айфона – пользователь огорчается. Синхронизация реплик нужна везде, и мы считаем, что система синхронизации данных (СД) скоро будет таким же айтишным ширпотребом, как БД.

    Специфика текущего момента в том, что даже лидеры индустрии допинали свои решения по синхронизации лишь до стадии «типа работает». GDocs не вполне работает в оффлайне, GTalk и Skype систематически теряют реплики из истории чатов, Evernote славится самыми разнообразными багами. И это — лидеры. Вообще, проблема синхронизации на удивление сложна и многогранна. Возьмём Evernote. Если бы Evernote был локальным приложением, его 80/20 подмножество мог бы написать студент. Так же, как располагая MySQL и PHP, Цукерберг написал facebook.

    В чём же фундаментальная сложность синхронизации? Давайте поймём, как работают классические технологии реплицирования и синхронизации, как они поддерживают идентичность реплик. Самый простой подход — это делать всё через центр. Все операции записи стекаются в центральную БД, обратно из центра идут новые результаты операций чтения. Вроде бы, надёжно и просто, но скоро сложности начинают надвигаться с трёх сторон.
    1. concurrency — пока шёл ответ на прежнюю операцию, клиент успел сделать что-то ещё, и как это теперь совместить — уже не вполне понятно,
    2. масштабирование схемы, где все операции проходят через одну точку,
    3. функционирование при плохом интернете, когда центр не отвечает клиентам или отвечает медленно.


    Первый типичный шаг масштабирования этой схемы — это репликация по принципу master-slave — так, как она реализована в типичной БД. Мастер укладывает операции в линейный лог и раздаёт этот лог slave'ам, которые прикладывают его к своим репликам данных, в том же линейном порядке, и получают тот же результат. Это помогает масштабировать чтение, но добавляет элемент eventual consistency, т.к. slave'ы обновляются с некоторым лагом. Проблема записи остаётся — все записи идут через тот же центр. Линеаризацию можно растянуть на распределённые реплики с помощью consensus algorithm, типа Paxos или Raft, но и там линеаризацию делает «лидер», т.е. всё тот же центр. Когда центр всё-таки затыкается, приступают к горизонтальному масштабированию — распиливают базу на «шарды». Тут уже линеаризация, а с ней и весь ACID, рвётся на тысячу маленьких ACID'иков.

    Ну и с оффлайновой работой центр и линеаризация трудносовместимы. Можно, конечно, сказать, что оффлайна «скоро не будет», но факт в том, что оффлайн случается и случается регулярно. Если мы что-то твитим или лайкаем, это можно потерпеть, а если что-то посерьёзней, то вряд ли. Если, например, официант ногой задел провод и интернет пропал, то мы не можем выгнать клиентов из ресторана, пока админ не приедет на машине с мигалками, и обслуживать бесплатно тоже не можем (пример от Макса Нальского, сооснователя IIKO).

    Причём, все эти приключения на стороне сервера ещё никак не затрагивают сторону клиента. Клиент просто ждёт, пока сервера всё согласуют между собой и сообщат результат. В небезызвестном проекте Meteor пытались сделать синхронизацию клиентов в реальном времени, фактически кэшируя MongoDB на клиента. Чтобы всё работало живенько, ожидание ответа сервера замаскировали трюком «latency compensation». Клиент накатывает операцию на свой кэш, отправляет её серверу, сервер отвечает, успешно ли приложилось, а если нет — высылает исправление. Подход более чем сомнительный. «-Люся, ты машину в гараж поставила? -Да, частично!»

    Вот такая сложная история с линеаризацией. Что ж, тем интересней посмотреть на популярные решения, которые на линеаризацию забили. Есть два хороших примера — Git и CouchDB. Git написал Линус Торвальдс, который среди разработчиков Linux был тем самым «центром». Вероятно, поэтому он хорошо прочувствовал, что центр медленный, центр не масштабируется. В git, синхронизация происходит по принципу master-master. Данные представляются, как орграф версий, все параллельные версии нужно когда-то смёржить. Масштабирование — идеальное, оффлайн — без проблем. Примерно то же и в CouchDB. Есть попытки вынесения CouchDB-подобной логики на клиента — pouchdb и hood.ie.

    Что-то совсем новенькое в этой области — это CRDT, и о нём сегодня и речь, извините за долгое вступление. CRDT – это Convergent/Commutative/Cloud Replicated Data Types. Общий замысел CRDT — в использовании частичного порядка вместо линеаризации. Операции могут происходить параллельно на многих репликах, и некоторые операции конкурентны — т.е. произошли на разных репликах, не зная друг о друге, ни одна из них не «первая», и на разных репликах они прикладываются в разном порядке. Если используемые структуры данных выдерживают такое лёгкое переупорядочение операций, не нарушающее причинно-следственных связей, то все связанные с центром проблемы просто испаряются.

    Другой вопрос — а много ли таких CRDT структур данных? Как оказалось, весь вычислительный ширпотреб — переменные, массивы, ассоциативные массивы — вполне себе реализуются в виде CRDT. А если мы деньги считаем? Тогда-то точно нужна линеаризация и гарантии ACID? Увы и ах, тут оказалось, что новое — это хорошо забытое старое. Обнаружилось, что используемые в бухгалтерии структуры данных — счета, балансы — вполне себе CRDT. Ведь в период Возрождения, когда сформировались традиции бухгалтерии, интернета не было, вот они и выкрутились без линеаризации.

    Большая сияющая возможность CRDT — это живые реплики, полностью работоспособные даже в отсутствие соединения с центром. Ну и немедленное приложение всех операций, без пробежки до центра. Такая автономность и быстрота особенно актуальна в двух случаях. Во-первых, для мобильных устройств — их используют на ходу, при ненадёжном интернете. CRDT позволяет запасать данные впрок, и спокойно работать локально, с фоновой синхронизацией. Во-вторых, для приложений с функцией совместной работы, особенно — в реальном времени (думаем про Google Docs, Apple iCloud). В таких приложениях «состояние» большое и быстро меняется, и каждая пробежка до сервера и обратно — гвоздь в гроб.

    Есть и не-CRDT технологии, позволяющие работать с данными в оффлайне. Своё API синхронизации предлагает Dropbox, есть StrongLoop, Firebase итд итп, их море. Все эти решения работают по принципу Last-Write-Wins (LWW) — каждой записи присваивается временная метка, запись с большей меткой перетирает прежние. На том же принципе построена Cassandra. И в нашей библиотеке Swarm, самый ходовой примитив — это LWW-объект. Преимущество Swarm — это те структуры данных, которые через LWW не решаются. Например, текст при одновременном редактировании.

    Вообще, в Зазеркалье распределённых систем всё наоборот. В обычных языках программирования, самая простая операция — это инкремент переменной, ++. Чуть сложнее работа с массивами, ещё сложней — объекты и ассоциативные коллекции. В распределённых системах, всё с точностью до наоборот! LWW объекты и ассоциативные контейнеры не представляют особой сложности. Линейные структуры (массивы, текст) весьма сложны, а счётчики сложны чрезвычайно. Это можно видеть на примере Cassandra, где LWW объекты сделали в первую очередь, а счётчики, вроде бы, всё ещё допиливают.



    Ближе к делу. Мы решили написать TodoMVC на Swarm+React, чтобы показать библиотеку в действии. Собственно, первый TodoMVC на Swarm+React был написан в июле Андреем Поппом меньше, чем за день, но тот код не был «идиоматическим». В этот раз, мы добавили линейные коллекции (Vector), серверный рендер и кучу всяких вкусностей. Более того, обычный TodoMVC показался нам скушноват и бесполезен. Например, глядя на React+flux TodoMVC, очень сложно понять, зачем авторы всё эти хитрости намутили в простейшем приложении. Поэтому, мы добавили одну фичу — рекурсивность. По нажатию Tab, пользователь переходит во вложенный «дочерний» список. Также, мы адаптировали интерфейс под реалтаймовую синхронизацию. Такое приложение уже представляет хоть какую-то практическую пользу. Также, стали показывать состояние приложения в URL — для лёгкого шаринга между пользователями. Вообще, нам было трудно остановиться. По сравнению с разработкой реалтаймовых проектов в прошлом, в лице Swarm мы имели этакий меч-кладенец, и всё время руки чесались укокошить кого-нибудь ещё.

    Подробный разбор приложения и библиотеки — в следующем материале.

    Следите за апдейтами в твиттере проекта @swarm_js.
    Swarm
    0.00
    Company
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 49

      +2
      Очень интересный проект. Был на вашем докладе на MoscowJS в июле. Мы там с вами в лифте ехали :-)

      Сам использую DerbyJS/ShareJS, а в CRDT всё никак не доходят руки покопаться. Если можно как-то сравнить на пальцах OT и CRDT, ShareJS и SwarmJS. Не помешала бы статья по CRDT для чайников.

      Чего мне не хватает в ShareJS — так это кэша на клиенте. В принципе это можно добавить, но изначально не было заложенно и нужно немного менять и ShareJS, и RacerJS, и DerbyJS. Тут есть противоречие, потому что Derby в данный момент при первом запросе считывает данные из бд на сервере, рисует html, сериализует эти данные, отправляет на клиент вместе с html, там десериализует и восстанавливает такое же состояние, как было на сервере. Как мне представляется, если мы хотим кэшировать данные на клиенте и каждый раз их не тягать с сервера (а только разницу), то приложение должно сначала загрузиться, потом посмотреть в кэш, какие данные есть и уже потом отправить на сервер запрос, чтобы тот прислал разницу данных. Я полагаю, что у вас так и сделано.

      Вот еще такой вопрос: есть ли поддержка подписки на запросы (и на отсортированные запросы)?
        0
        Спасибо!
        Про кэш. В LWW-решениях появление кэша на клиенте достаточно просто, он там и есть обычно. В нашем (CRDT) тоже. В OT решении — крайне маловероятно. OT крайне уязвимо к асинхронности, там комбинаторный взрыв получается и всякие каки. А оффлайновая работа «с кэша» — это максимально асинхронный сценарий.
        В Google Docs приковыряли кэш как-то, но там, насколько понимаю, использовано другое решение, чем при онлайновой работе, плюс какие-то экстеншены нужно устанавливать в браузер. Внутрь не смотрел, но стойкий химический запах ощущается.
          +1
          Как я понял в таком случае разрешение конфликтов с помощью timestamp (CRDT) имеет преимущество над версией документа (OT). Ну а в LWW-решениях — кто последний вышел в онлайн, тот и затер всё что было до него. Правильно?

          Вот еще вопрос. В ShareJS атомарность на уровне документа. То есть (по крайней мере для JSON-типа) можно подписаться на документ, коллекцию или запрос к коллекции (в том числе отсортированный). Нельзя подписаться на часть документа. Как это в SwarmJS?
            0
            1. В случае Causal trees конкурентные правки не перетираются. Там формально дерево символов, обход (depth first traversal) которого является текстом. Символы в дерево только добавляются. Timestamp используется для адресации и для упорядочения сиблингов (конкурентно вставленных в то же место символов). В Causal trees конфликтов, как таковых, нет.

            2. Подписка идёт на объект, у него есть состояние, id и версия. Подписка на поле объекта возможна в API, но это фактически подписка на объект + фильтр.
              +1
              Я правильно понимаю, что нету такого понятия как подписка на коллекцию или запрос к коллекции? Например, могу ли я подписаться на документы коллекции users, где user.age > 20, отсортированные по user.name, при этом skip = 20, limit = 10?
                0
                Коллекции есть, Set и Vector, и можно подписываться на события в объектах коллекции. Вручную отсортировать / профильтровать тоже можно.

                Но обернуть запрос к БД в виде коллекции и слушать его — пока нельзя, но такой функционал в работе. Собственно, он неизбежно бекендо-зависимый, отдельная реализация для SQL, отдельная для Mongo, итд.
                  0
                  Да, в ShareJS это сделано в виде адаптеров к разным бд, например, Mongo.

                  Спасибо за ответы. Появилось некоторое понимание. Желаю вам и проекту успехов!
      • UFO just landed and posted this here
          0
          Зависит от типа. Для простых объектов и ассоциативных массивов last write wins, где last определяется по timestamp. В Кассандре похожая схема (Лампортовская).
          Для линейных структур (Vector, Text) используется merge по алгоритму Causal Trees. Результаты похожи на посимвольный 3-way merge (это чем патчи мержатся обычно в source control, только там всё построчно).
          • UFO just landed and posted this here
              0
              Можете попробовать разобрать заметку про Causal Trees или посмотреть в блоге проекта пост про Лампортовы метки.
              Что-то более подробное, с картинками, думаю, появится со временем тоже в блоге проекта.
              Следите за @swarm_js.
              • UFO just landed and posted this here
              0
              При добавлении 2х разных ключей в хеш один потеряется? Это не ок!
                0
                Библиотека Swarm — двуслойная.

                Первый уровень — op-based CRDT основа, там реализованы «лог», «операция», «объект».

                Второй уровень — собственно типы данных, написанные поверх основы.

                Ничто не мешает добавить класс ManualMap, который будет сохранять и явно указывать на конфликты. Сейчас реализован только самый простой ширпотреб.
                  +1
                  Очень напоминает sharejs с их 2-мя уровнями — ядро с OT, и типы данных поверх. И тоже ничто не мешать любой тип данных туда вворотить, например, для поддержки reach-text редакторов. Как у вас, кстати, с таким типом?
                    +1
                    Рекомендую статью Виктора: Operational transformation: the newest and coolest tech from the 80s
                      0
                      Совместный редактор нам писать приходилось. Учитывая, что таких web-based real-time collab editor codebases в мире 4-5, в смысле — которые использовались в продакшене, то мы, конечно, очень этим горды.

                      В Swarm такого типа пока нет, есть только plain text. Можем реализовать и RichText однажды. Если найдём заказчика, например, или как-то время выкроим. Там довольно много работы.
              +1
              страница отрисовывается на сервере и приходит на клиента, как сжатый HTML
              Расскажите о том, почему бы её и не отрисовывать на клиенте, тем разгружая сервер.
              • UFO just landed and posted this here
                  +3
                  Так долго.
                  * скачал html
                  * пропарсил
                  * запустил js
                  * вытянул данные
                  * отрисовал
                  * засунул в DOM
                  Браузеры хорошо оптимизированы по линии скачал-показал, плюс пробежки туда-сюда это RTT.
                    +4
                    В DerbyJS тоже изоморфный шаблонизатор. Это хорошо по трём причинам:
                    1) SEO
                    2) Скорость загрузки приложения. Клиент сразу видит html, а не ждёт пока загрузится весь js и сгенерирует html.
                    3) Удобно использовать тот же шаблонизатор на сервере для рендеринга html для писем, документов и тп.
                      –4
                      > Расскажите о том, почему бы её и не отрисовывать на клиенте, тем разгружая сервер.

                      Сразу вспомнилась книжка «Психбольница в руках пациентов» Алана Купера. В ней аргументируется, почему программистов нельзя допускать к принятию концептуальных решений, иначе они сделают всё, как удобно им самим, а не пользователям.
                        +2
                        Грубовато. Разгруженный и быстрый продакшн сервер скорее нужен пользователям, а не разработчикам.
                          –2
                          Выше уже ответили, что подход с рендерингом на сервере для пользователя оказывается быстрее. Можно потреблять контент, не дожидаясь, пока загрузятся скрипты и оживет интерактив.
                      +2
                      А документация есть?
                        +1
                        С этим пока слабовато. Есть примеры. Комментарии в коде swarm — скорее для разработчиков.
                        Обещаем скоро исправиться.
                        +3
                        Жду нормальный вменяемый ликбез по CRDT — в магию не верю.
                          +1
                          Планируем делать по материалу в неделю, плюс-минус. Следите за твиттером @swarm_js.
                            0
                            Заказываю следующую статью по CRDT :) Иначе абсолютно не понятны ограничения технологии.
                              +1
                              Дайте две!
                            +2
                            Я даю вам $100 и мы не открываем черный ящик.
                            0
                            Пока читал, подумалось, что очень перспективным направлением могла бы быть разработка виртуального серверного браузера.
                            Т.е. со стороны разработчика это выглядело бы как разработка для обобщенного клиентского браузера, но со всеми серверными фичами. А синхронизацией с «настоящим» (клиентским) браузером занимался бы сам виртуальный сервер.
                              +1
                              Извиняюсь, промахнулся.

                              Правильно ли я понимаю, что вы предлагаете хранить у клиента всю историю изменений, как это делается в dvcs? У такого подхода есть несколько проблем:
                              1. большие требования ко клиенту по памяти (оперативной и/или дисковой)
                              2. необходимость шифрации данных (или хотябы их очистка при логауте)

                              Вариант с центральным хранилищем последней истины выглядит лучше. Клиент в этом случае может удалять старые версии по факту синхронизации.
                              0
                              Простите, что-то не до конца понял.
                              На фронтенде мы подключаем библиотеку, а что выступает в роли backend, куда идус запросы на синхронизацию, и где хранится информация?
                                0
                                На сервер github.com/gritzko/todomvc-swarm/blob/master/TodoAppServer.js
                                На сервере такой же код Swarm.
                                Ну, и локально оно кэширует всё в WebStorage. IndexedDB на подходе.
                                  0
                                  Ага, так у вас на сервере тоже JS…
                                    0
                                    Ещё есть первый вариант java-версии (сервер и андроид). И на go ведётся разработка.
                                0
                                Я тут поразмышлял и пришёл к выводу, что Святой Грааль проблемы синхронизации находится в другой плоскости бытия — UX. В зависимости от самых разных факторов (не все из которых могут быть доступны в приложении) могут потребоваться различные стратегии слияния, так что любая автоматика неизбежно будет ошибаться. Поэтому разумнее положиться на то, что пользователь лучше знает как должны сливаться его изменения — достаточно в ленту его новостей постить информацию об автомерже его изменений с чужими (речь об «одновременных» изменениях одной сущности) и увидев её пользователь по необходимости правильно актуализирует значение. В этом свете сложная интеллектуальная система слияния версий становится лишней, уступая простым и предсказуемым (для пользователя) стратегиям.
                                  0
                                  Ну-ну, представьте это в Google Docs.
                                    0
                                    А в чём проблема с Google Docs?
                                      0
                                      Представьте UX при ручном мерже правок одного параграфа в Google Docs, я это имел в виду.
                                        0
                                        Я ничего не говорил про ручной мёрж.
                                          0
                                          А, ну тогда почти согласен.
                                          Вы так абстрактно написали.
                                          Вот 3-way merge — это сложно или просто?
                                          По-моему, просто и предсказуемо.
                                            0
                                            В случае крупных конфликтов это очень сложно. Ну и интерфейсно он не очень органично вписывается. Хотя, 3-way merge для произвольных структур данных (не только текста) — это была бы крутая фича.
                                            0
                                            «увидев её пользователь по необходимости правильно актуализирует значение» — это, разве, не про ручной мёрж?
                                              0
                                              Это скорее ручное исправление последствий автоматического мёржа.
                                    –2
                                    … и никто не спросил, что за девушка на фото. ладно, я спрошу — это кто такая ухоженная?
                                      0
                                      Не совсем въехал в этот CRDT, расширяющий сознание туториал жизненно необходим. Интересно, возможно ли натянуть модели из Swarm на иммутабельные структуры данных, загнать состояние в одну точку и затем шарить его части по компонентам? Эту технику привнес Om, она удобна и идет в массы. Ключевой фактор — состояние не размазывается по приложению.
                                        0
                                        Вопрос крайне интересный, и ответ на него сложный.
                                        В Swarm, объекты имеют метку версии _version, которая позволяет установить факт наличия/отсутствия изменений, но иммутабельности нет.

                                      Only users with full accounts can post comments. Log in, please.