В рамках этой презентации будет обсуждаться концепция клиент-серверного приложения, которое единоразово решает задачу "получения-передачи данных". Приложения, которое работает с данными, но при этом не запрашивает эти данные при помощи выборок, чем существенно сокращает количество серверсайд кода, повышает надежность и производительность приложения, а также предлагает множество инновационных свойств, недоступных при использовании обычной клиент-серверной архитектуры.
34. Серверная часть
• Реализует один REST endpoint
• Реализует логику защиты от
высокочастотных запросов
• Реализует кеширование данных в
памяти
• Реализует логику обратного HTTP
• …
Привет, меня зовут Денис, я разработчик, долгое время работал с Upwork и ITEA
Программированием занимаюсь более 10 лет, работаю в основном в области web-разработки, хотя так-же работал в области обработки данных и искусственного интеллекта.
И программирование для меня не просто профессия: (хобби, то что я люблю, чему уделяю все свободное время)
И сегодня я расскажу вам о своих нарботках последних трех лет - об архитектурном шаблоне под названием рефлекс.
План моего выступления следующий:
Сначала мы поговорим о том Из чего состоит наша ежедневная работа
Далее поговорим о том Какие неэффективности она содержит
И в конце мы попытаемся разобраться как эти существующие проблемы разрешить
-Для вопросов у нас отведена
(Следующий слайд)
Вы не спроста видите сейчас перед собой это произведение искусства, это мотоцикл Ducati
Венец инженерной мысли и непосильных трудов огромного количества специалистов в области
Дизайна, Инженерии, Тестирования, Проектного менеджмента и Бизнеса в целом
Иными словами всего того к чему мы так-же имеем непосредственное отношение
Но почему-то вспоминая результаты нашей с вами работы скорее возникнет следующая ассоциация
(следующий слайд)
Подымите пожалуйста руки те кто понимает смысл фразы “запилить костылек” ☺
Сфера мотоциклов очень похожа на сферу разработки в том отношении что клиенты в обоих случаях хотят впринципе одного и того же. В случае мотоциклов – нечто на двух колесах выглядящее сногшибательно, способное разогнаться до сотни за две секунды. Так-же и в случае коммерческого программирования клиент хочет получить нечто называемое web-приложением, способное разогнать бизнес заказчика за кратчайшие сроки. Вот и получается что все заказчики хотят примерно одного и того же, предельно быстрого надежного безопасного изделия.
И если задаться вопросом, что же приводит к такого рода результату то ответ «проблемы» будет выглядить наиболее логично ☺
В данном случае проблемы с Дизайном / Инженерией / Планированием / и Проектным менеджментом.
И в этой связи давайте попытаемся перечислить наши c вами ошибки которые мы допускаем в нашей работе ежедневно…
Этот список можно продолжать еще долго…
Но можно ли сказать что мы допускаем эти проблемы умышленно? В большенстве случаев нет, тогда почему мы допускаем эти проблемы?
Да потому что для того что бы написать код изолированный от перечисленных проблем это все равно что…
(следующий слайд)
Проложить такой вот кабель через Атлантику ☺
Обратите внимание на все эти уровни изоляции, они чем-то напоминают изоляцию кода от озвученных ранее проблем.
Иными словами генерировать такой надежности код достаточно сложно, это во первых!
Ну а во вторых, давайте задумаемся почему понятие проводов вовсе ассоциируется с нашими приложениями?
(следующий слайд)
Почему такое безобидное изображение заставляет разработчиков краснеть? ☺
И что бы ответить на этот вопрос нам необходимо обратить внимание на то чем мы занимаемся на протяжении нашего рабочего дня.
(следующий слайд)
Мы пишем:
Код базы данных, Код серверной части, Код клиентской части
Но что делает этот код?
Если посмотреть на это с высоты птичьего полета то он лишь передает данные между базой и клиентом, производя разные трансформации над данными на этом пути.
То есть код который мы пишем решает лишь одну задачу – задачу передачи данных между БД и клиентом, но решения получаются каждый раз разные.
И в итоге получается что создавая такую связь между БД клиентом мы создаем создаем спагетти!!! Да, безусловно существует большое количество методологий (следующий слайд)
Которые в теории должны помочь нам из неупорядоченного клубка связей получить нечто подобное (указать рукой на слайд)
Кто например слышал про CQRS?
[[[Подымите пожалуйста руки]]]
А [[[Подымите пожалуйста руки]]] кто использовал CQRS на практике?
(поднять руку) (3 сек) (улыбнуться) (следующий слайд)
Это то место где практика побеждает теорию… ☺
Что же является ключевой проблемой которая приводит к образованию сложности в приложениях? Я с вашего позволения отвечу на этот вопрос.
Причиной увеличения сложности приложения есть выборки.
Это может прозвучать бессмысленно но я сейчас попытаюсь объяснить что имеется в виду.
(следующий слайд)
Выборки не повторно используемы
На этом слайде вы видите два пересекающихся множества данных (кстати выглядят они как спагетти в разрезе). Это множества данных которые возвращаются в двух различных случаях. Обратите внимание на то что данные в этих двух множествах пересекаются.
А теперь для того что бы написать одну повторно используемую выборку которая обрабатывала оба случая.
Необходимо что бы эта выборка возвращала оба множества данных. Но на практике это очень большой риск связанный с производительностью.
И соответственно на практике так, никто не делает и при возникновении такой ситуации пишут две различные выборки.
А теперь что это за собой тянет? Две выборки тянут за собой два разных типа которые эти данные описывают. Два типа тянут за собой два разных метода на стороне сервера. Два разных метода это два разных REST-endpoint’а.
Два разных REST-endpoint’а это два разных ajax запроса на стороне клиента,
Два ajax запроса это две разные кучи преобразований и сложных трансформаций данных которые происходят до того как данные отображаются пользователю…
Количество работы заложенное в реализацию этих двух выборок – колоссально! И это не вся необходимая работа, умножьте ее объем на изоляцию от упомянутых ранее проблем?
И все эти усилия необходимы ради того что бы решить лишь всего лишь одну задачу (и каждый раз по новому) – передать данные между базой и клиентом.
Второй причиной по которой выборки являются основной причиной сложности в наших приложениях есть факт того что производительность выборок зависит от структуры данных, чем более сложная структура данных тем более усилий необходимо для того что бы такую выборку реализовать и тем больше места для ошибки возникает при реализации такой выборки.
Таким образом возникает вопрос, так как же побороть выборки и весь тянущийся с ними copy-paste?
Reflex это действительно в некотором смысле беспроводный маршрутизатор для веб-приложений,
используя рефлекс задача передачи данных между базой и клиентом решается всего один раз в самом начале разработки приложения.
Данные которые можно видеть пользователю
Отправляются на клиент
И поддерживаются там свежими при помощи обратного HTTP (SignalR / Socket.io)
Данные которые разрешено видеть пользователю назовем гранью базы данных для некоторого пользователя
Но отправлять все данные не целесообразно их может быть очень много.
Таким образом приходим ко второму понятию. Слепок данных пользователя это подмножество грани которое имеет большую площадь покрытия системы но сравнительно небольшой объем.
На практике это может быть 10-20-50 возможно даже 100 мб данных которых достаточно для работы приложения в большинстве случаев.
А имея большинство необходимых данных на клиенте нет необходимости делать выборки
Имея большинство необходимых данных на клиенте мы получаем полноценный офлайн режиму в рамках которого можем просматривать искать изменять удалять данные без связи с сервером.
Позволяя пользователю редактировать данные без связи с сервером, мы попадаем в ситуацию когда изменения могут конфликтовать между собой.
И в этой связи я хотел бы поднять задать вопрос, а что же такое конфликт?
Какая из версий более правильная?
Кто считает что первая подымите руку
Кто считает что вторая подымите руку
Кто считает что обе версии имеют право на жизнь, подымите обе руки ☺
На самом деле в том что бы хранить все версии данных (только на стороне базы данных!!!) есть очень большой смысл:
Мы всегда можем например вернуться в любое предыдущее состояние системы и проанализировать кто и какие изменения внес в наши данные,
следовательно нет необходимости логировать изменения
И кроме этого мы можем перенести вопрос связанный с резолвингом конфликтов на клиент
Предоставить пользователю обе версии и дать возможность выбрать какая версия более правильная
Таким образом снимая ответственность за резолюшен конфликтов с системы и возлагая ее на пользователя мы делаем большое одолжение пользователю так как такая система будет более гибкой
И для того что бы реализовать всю озвученную магию достаточно всего-лишь следующей структуры базы данных.
Несколько комментариев этой структуры данных
Во первых здесь используется SQL Server
Но база может быть любой Sql / NoSql не важно
Эту схему можно реализовать используя практически любую базу данных, так как для реализации этого подхода используются лишь базовые свойства баз данных
Во вторых для этой схемы используется денормализированный подход к хранению данных
Все пользовательские данные хранятся в поле Json в табличке Items а при помощи поля TableName производится разграничение пользовательских данных по типу
В третьих возможно использовать обычный подход к хранению данных и разбить текущую таблицу на множество других, и хоть это возможно это усложнит вашу жизнь
В четвертых текущая схема данных позволяет реализовать схему при которой любое изменение данных независимо от того это Update или Delete реализуются по средством Insert’а в базу
Еще одним отличием от стандартных подходов есть табличка Permissions.
Она используется для того что бы регламентировать доступы пользователей к данным, на основании ссылок на данные в этой табличке из таблички Items у нас есть возможность довольно легко вычислить какие данные к каким пользователям относятся. Таблица RolePermissions при помощи которой возможно управлять разрешениями на просмотр данных без модификации таблицы ролей и таблицы Items что существенно повышает производительность операций по изменению разрешений.
Давайте более детально рассмотрим основные таблицы, таблица Operations
Operations используется для того что бы хранить в себе данные описывающие вносимое пользователем изменения (для этого используется поле CreatedById)
Так-же в этой табличке хранятся сервисные данные о том когда была начата и когда была закончена текущая операция по изменению данных
А так-же поле Finished которое говорит о том а была ли окончена операция по изменению данных вовсе, к этому полю мы вернемся по позже
Таблица Items
Поле Id есть ID версии данных таким образом Id версии всегда уникально
Соответственно PreviousID – предыдущая версия данных, на основании которой производилось редактирование
Поле UUID это обычное ID данного, это то ID с которым мы привыкли ежедневно работать, как вы видите поле UUID может содержать дубликаты
О поле Json мы говорили, в нем хранятся пользовательские данные
Kamikaze это флаг того что версия была удалена
Остальные поля из-за рамок моего выступления мы опустим но их названия говорят сами за себя
Таким образом предложенная схема данных позволяет хранить пользовательские данные в многоверсионном режиме
а на этом слайде мы видим диаграмму жизненного цикла некой гипотетической записи, давайте рассмотрим что она означает
…
Не правда ли эта схема напоминает схему комитов в системе контроля версий Git
И возникает вопрос как-же работать с такими денормализированными данными?
Ответ прост, так как нам необходимо отправить на фронтенд лишь самую последнюю версию данных то для этого достаточно лишь реализовать выборку GetLatest
GetLatest принимает два параметра начало и конец промежутка времени в рамках которого происходит подсчет «последних» версий
Так для промежутка X это версии 4 и 5, но если предположить что у клиента уже есть эти версии то делать повторную отправку этих данных смысла никакого нет
ровно как и отправлять версии промежуточных редактирований которые на промежутке Y отмечены красным цветом.
То есть имея в распоряжении промежуток X и запрашивая при этом промежуток Y клиенту необходимо
Удалить версии 4, 5
А так-же добавить версию 8
Это может показаться сложной операцией но:
Вот та выбока которая производит конкретное вычисление GetLatest. Из-за рамок отведенного мне времени мы не будем углубляться в детали того как эта выборка реализована, основное на что здесь стоит обратить внимание, это то что выборка крайне проста по своей природе и структуре. Такая выборка будет выполняться на максимально возможной скорости.
Единственное что может вас смутить это конструкция «From Computed»
Это вью, и вот его реализация, это вью лишь вычисляет время следующего редактирования
Которое используется для вычисления того попадает конкретная версия в заданный диапазон или нет. Тут так-же важно сказать что с точки зрения производительности эта выборка очень быстра и крайне производительна, за счет ее простой структуры а ее производительность зависит лишь от объема данных которые необходимо вернуть.
А вот вставка данных, она состоит из двух этапов это связанно с сложностью вычисления времени завершения транзакции.
Проблема в том что COMMIT TRANSACTION не атомарно по своей природе.
И для того что бы завершить транзакцию необходимо время
что бы проставить специальный флаг на всех вставляемых данных свидетельствующий о том что данные уже доступны для чтения.
По этому если выставлять время завершения транзакции внутри транзакции то для большого объема данных это время будет не верным
так как оно не будет учитывать времени необходимого на завершение транзакции
А вот вторая фаза вставки данных, все что она делает это обновляет значение в таблице Operations выставляя ей время завершения операции
и флаг того что операция завершена в принципе. Это делает данные вставленные в рамках первой фазы доступными для GetLatest.
Цикл WHILE EXISTS может совершить несколько итераций только в случае когда ранее произошла катастрофа и база успела сохранить лишь данные первой фазы в таком случае выставится время завершения операции не только для текущей операции но и для предыдущей. Это свойство само-лечения базы.
Ну а обновление таблицы Operations по одной записи используется для того что бы сделать вычисление времени завершения операции как можно более точно и атомарно.
Почему это важно?
- Если не учитывать время необходимое на COMMIT TRANSACTION то по факту данные станут доступными как бы в прошлом
- И если за это время какой либо из клиентов успеет выполнить GetLatest то он получит часть данных и это изменение будет для него навсегда потеряно.
На этом мы завершим рассказ о структуре базы данных и переключимся на обсуждение прочих элементов архитектурного шаблона рефлекс.
По факту мы не будем рассматривать код серверной части из-за того что он не содержит ни каких примечательностей а лишь поговорим о том какие задачи он решает:
Во первых он реализует один REST endpoint при помощи которого происходит обмен данными
Во вторых он реализует защиту от высокочастотных запросов, иными словами он накапливает все получаемые изменения и раз 50 мс одним большим запросом производит сохранение в базу
В третьих он реализует кеширование «последних» изменений и на основании их становится возможным вычислить инструкции для текущего клиента запрашивающего недавние изменения не прибегая к соединению с базой, это ускоряет работу сервера на несколько порядков.
Ну и фактически реализует логику работы обратного HTTP (SignalR)
…
На этом мы рассказ о сервере мы завершим и переключимся на клиентскую часть
Клиентская часть состоит из двух частей:
Поддерживающий код
Который занимается сохранением промежуточных данных в клиентскую базу IndexedDB или WebSQL
Настраивает SignalR
И делает большое количество других сервисных операций
Здесь важно сказать что этот код пишется раз в начале разработки
Клиентская библиотека реализующая паттерн репозиторий которая занимается тем что склеивает денормализированные данные полученные со стороны базы в одну полносвязную модель согласно схемы данных которая определяется на стороне клиента.
Давайте посмотрим короткий скринкаст реального приложения использующего этот архитектурный шаблон
В качестве заключительного слова я хотел бы обратить внимание каждого на то что мы с вами как разработчики за последние десятилетие окончательно превратились из художников и хакеров в поваров и сантехников
Наша работа стала шаблонной и скучной, Reflex это попытка посмотреть по новому на архитектуру веб приложений
И да безусловно, Reflex подымает большое количество вопросов и не пытаясь отвечать на эти вопросы мы делаем большой вклад в то чтобы все осталось как есть
На текущем этапе разработка находится на фазе готового прототипа, и к сожалению сейчас ни какой информации в интернете вы пока не найдете.
Так-же отсутствует поддержка в соцсетях, но если вам эта тема кажется интересной и вы бы хотели попытаться воспользоваться ей
Подойдите ко мне после презентации и мы попытаемся как-то наладить связь.