LINUX.ORG.RU

Веб страница с большим стейтом. Как оптимизировать обмен?

 , ,


0

2

Всем привет. Прошу совета

Сразу к примеру. Допустим есть система товароучета. За раз на странице показывается 100 товаров, информация о них и остатки. Суммарно допустим это 10 тысяч значений, размером около 200 КБ

Значения меняются относительно динамично, как локально, так и на сервере (а точнее другими пользователями системы). Товары добавляются, удаляются, изменяются их свойства и остатки.

Сделано всё в лоб. Раз в n секунд получение и перерисовка всего стейта (нужной страницы). Плюс к этому новый стейт запрашивается на каждое локальное изменение в учитываемых данных.

Получается эдакий PHP-стайл, где на каждое изменение прилетает новая страница, но теперь со вкусом JSON-а и реакта

Вопрос. Как это дело оптимизировать? В идеале на страницу должны прилетать только изменения относительно некоего предыдущего состояния. Не так идеально - получать полные даные, но обновлять в стейте тольк то, что изменилось (кажется это зовется модным словом reconciliation)

Есть ли устоявшиеся подходы, готовые решения?

еще можно с Rxjs повозиться, а че у тебя вообще за стек на фронтенде? чем html рисуется?

TDrive ★★★★★
()
Ответ на: комментарий от TDrive

вебсокеты + redux

Это транспорт и стейт менеджер. Вопрос про события, происходящие ДО популяции данных в стейт

Что-то типа API, отправляющее id снимка состояния, на которое сервер пришлет лог изменений, а клент аккуратно, с минимальными деструкциями, применит этот лог к имеющемуся стейту

По любому должно быть готовое решение )

kijllfatncdaplp
() автор топика
Ответ на: комментарий от kijllfatncdaplp

Что-то типа API, отправляющее id снимка состояния, на которое сервер пришлет лог изменений

Готового не знаю, я подобное сам всегда писал и там сильно по разному может быть от задачи.

Например, зачем отправлять id снимка состояния если можно просто получить последнюю актуальную версию данных об объекте? сделать объекты достаточно маленькими, или сами товары или даже товар разделить на более мелкие и подписываться на них отдельно по вебсокетам, наверное самый простой в реализации вариант.

С этими дифами… полюбому что то где то будет хериться или порядок будет меняться по пути до фронтенда, придется придумать какие нибудь механизмы проверки.

TDrive ★★★★★
()
Ответ на: комментарий от TDrive

Ну и там еще где то дебаунс должен быть)

TDrive ★★★★★
()
Ответ на: комментарий от TDrive

С этими дифами… полюбому что то где то будет хериться или порядок будет меняться по пути до фронтенда

Так у всех сущностей же есть id. Плюс предварительная сортировка. Так что не проблема )

Можно и самому, но мне кажется должно быть более-менее популярное решение готовое

kijllfatncdaplp
() автор топика
Ответ на: комментарий от kijllfatncdaplp

Так у всех сущностей же есть id. Плюс предварительная сортировка. Так что не проблема )

Нет, там могут быть более крутые проблемы. Представь что у тебя все дифы на отправку складываются в какой нибудь редис откуда их забирает сервер вебсокетов и вот этот редис начинает переполняться или просто по непонятным причинам что то херить. в результате ты на фронтенде получаешь 1, 2, 4, 5, 6 дифы а 3 нету, тебе придется придумать таймаут ожидания 3 дифа, после чего запросить снова все 6 (а скорее всего уже 8) дифов.

А теперь представь что будет происходить на пиковых нагрузках и представь, что твой сайт открыли на телефоне который находится в метро и инет пропадает между станциями метро, каждые 5 минут.))

TDrive ★★★★★
()
Ответ на: комментарий от kijllfatncdaplp

не уверен, просто единственное что вспомнил из готового

TDrive ★★★★★
()
Ответ на: комментарий от kijllfatncdaplp

А что будет в текущей схеме, если два клиента одновременно изменят одни и те же данные?

tfeartx
()
Ответ на: комментарий от kijllfatncdaplp

есть и готовые решения. но никому не всралось ими пользоваться ради 200кб и странички на реакте. кто борется за производительность – с реактом не связывается

anonymous
()
Ответ на: комментарий от anonymous

Я тож самое хотел написать, но ждал ответа на свой вопрос. На 10к записей и 200кб проще отдавать все данные целиком, чем пилить сомнительные решения-костыли из комментариев выше.

Но я почитал бы варианты решения этой задачи на больших объемах данных, это уже интереснее.

tfeartx
()
Ответ на: комментарий от anonymous

Тормозит, когда активно добавляешь/меняешь штуки. Особенно в телефоне. Реакт тут ни при чем. vue будет тупить не меньше, если не больше, т.к. на каждый приход 200kB, будет стоить дерево обзерверов с 10k реактивными значениями

kijllfatncdaplp
() автор топика
Ответ на: комментарий от tfeartx

И я бы с удовольствием почитал варианты и от анонимуса и твои варианты)

TDrive ★★★★★
()
Ответ на: комментарий от tfeartx

изобретем заново GraphQL

GraphQL вообще никаким боком не относится к проблеме ТСа. Ему нужно вместо снапшота получать события изменений, а не кровати переставлять.

no-such-file ★★★★★
()
Ответ на: комментарий от no-such-file

Относится, но не напрямую, разработчики GraphQL решали похожую проблему, они избавились от необходимости передавать весь стейт и предоставили фронтенду возможность самостоятельно решать какие данные нужны, а по поводу событий изменений, так ты сам на сервере решаешь что отправлять клиенту. ТСу в любом случае нужна отправка полного стейта для инициализации, а события можно реализовать только для части данных, в качестве дополнительной фичи и к этому можно плавно прийти с того что у него сейчас, постепенно переписывая.

В общем относится но это не готовое решение)

TDrive ★★★★★
()
Ответ на: комментарий от TDrive

самостоятельно решать какие данные нужны

Если б он знал какие данные нужны, он бы и не запрашивал все товары, к gql это никак не относится. Проблема ТСа именно в том, что нет возможности понять что надо, а что не надо.

по поводу событий изменений, так ты сам на сервере решаешь что отправлять клиенту

Опять же это ортогонально к gql, если б обновления были реализованы как события, то и говорить было б не о чем. Но оно не реализовано. gql сам по себе тут никак не поможет, будет такое же обновление стейта каждый раз, вид сбоку.

относится но это не готовое решение

Это вообще никакое не решение, с тем же успехом можно посоветовать любую другую рандомную технологию.

no-such-file ★★★★★
()
Ответ на: комментарий от no-such-file

GraphQL вообще никаким боком не относится к проблеме ТСа.

Ну почему же так категорично.

Subscriptions are long-lasting GraphQL read operations that can update their result whenever a particular server-side event occurs. 
vvn_black ★★★★★
()

Если не связываться с вебсокетами, которые ещё то веселье в условном метро, как говорилось выше.

В базе данных на сервере у объектов храни время последнего изменения (по мнению сервера), в ответах помимо объектов возвращай текущее время (по мнению сервера), в запросе на поллинг объектов добавь опциональное поле «возвращать только объекты, изменённые после такого-то времени по мнению сервера». На клиенте прилагай время полученное в предыдущем ответе к следующему запросу.

Что дальше делать на клиенте это уже надо спрашивать у программистов на жабаскрипт. Но по идее там сейчас всякие модные observable, при изменении конкретных объектов должны трогаться только конкретные DOM-ноды.

PolarFox ★★★★★
()
Ответ на: комментарий от no-such-file

Если б он знал какие данные нужны, он бы и не запрашивал все товары

Он запрашивает весь стейт потому что это самое простое решение, первое что приходит в голову, а не потому что не знает что ему нужно.

Это вообще никакое не решение, с тем же успехом можно посоветовать любую другую рандомную технологию.

Нет не любую) Но я уже написал, что gql тут совсем не ключевой элемент и топить за него не собираюсь.)

TDrive ★★★★★
()
Ответ на: комментарий от PolarFox

По поводу вебсокетов, единственное что смущает это вот эта фраза ТСа.

Плюс к этому новый стейт запрашивается на каждое локальное изменение в учитываемых данных.

Хрен знает что там за частота изменений и как все это выглядит на практике… может он запихав туда вебсокеты избавится от 90% трафика.

TDrive ★★★★★
()
Ответ на: комментарий от TDrive

Локальное изменение, которое мы туда отправили, к нам придёт обратно как один из обновлённых объектов. Главное что не 200кб на каждый чих.

PolarFox ★★★★★
()
Последнее исправление: PolarFox (всего исправлений: 1)

Ходить за данными надо не на сервер, на каждый чих, а в локальный персистентный стор. На клиенте есть целая IndexedDB для персистентного храненния объектов, не засирая память. Браузер сам закеширует все горячее.

На сервере должна быть очередь сообщений, куда падают все события, и на которую подписываются клиенты. От событий и происходит обновление данных в клиентском сторе. Слушать очереди можно хоть сокетами, хоть опросами, хоть через Server Sent Events. Зависит от задач и кейсов.

При изменении данных происходит коммит в мастер на сервер.

Разруливание конфликтов мерджей это уровень бизнес-логики. Можно ждать ответа мастера об удачности коммита, или сообщение о конфликте при изменении, если при попытке изменения данных на сервере уже была более новая версия, которую запушил кто-то параллельно. Можно просто продумать блокировку записи при входе в режим редактирования.

При должном усердии дополнительно пишется сервис воркер, который и в фоне переодически слушает очередь и подгружает данные, даже если вкладка закрыта (и даже при закрытом браузере, если пользователь разрешит), чтобы не было плохо при загрузке всех изменений разом при каждом открытии.

Искать под это все готовые решения как-то странно. Ибо в этом обычно и заключается написание многопользовательского приложения. Пара другая клиентских и серверных сервисов и очередей. Если готовое что и есть, то скорее всего это кровавый энтерпрайз заточеный для решения максимально широкого спектра задач и нацеленный на высокий хайлоад, при этом так или иначе это скорее всего будет чисто серверная сторона, а не фуллстек.

object
()
Ответ на: комментарий от PolarFox

в запросе на поллинг объектов добавь опциональное поле «возвращать только объекты, изменённые после такого-то времени по мнению сервера»

А это мысль! Спасибо. Попробую.В sql запрос добавить фильтр на «новее этого timestamp».

Не понятно только как красиво провернуть доставку на клиент информации об удаленных объектах

kijllfatncdaplp
() автор топика
Ответ на: комментарий от PolarFox

Не всегда работает, кстати. На клиенте оптимистично изменил стейт, отправил запрос. В этот момент прилетает новый апдейт, а наш запрос на изменение еще в пути. Значение моргает несколько раз в течении секунд )

kijllfatncdaplp
() автор топика
Ответ на: комментарий от kijllfatncdaplp

Поллить по послелнему времени синхронизации надо не базу с данными, а очередь с событиями об изменении этих данных. Туда же и падают события об удалении объектов.

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

object
()
Ответ на: комментарий от object

Серьезный подход. Это получается отдельный API для получения данных (первоначального) и еще один API (с отдельными DTO-шками под каждую операцию для каждой сущности) для получения изменений с очереди и прокручивания их в клиентском сторе?

kijllfatncdaplp
() автор топика
Ответ на: комментарий от object

А есть название у этого дела? Очереди операций всмысле. Где почитать, чтобы преисполниться и овладеть терминологией?

kijllfatncdaplp
() автор топика
Ответ на: комментарий от object

И еще вопрос. В очереди только информация об изменениях или новые значения сущностей из нее же брать остальным клентам?

kijllfatncdaplp
() автор топика
Ответ на: комментарий от kijllfatncdaplp

Есть паттерны EventSourcing и CQRS, но они в целом о том, чтобы в таком виде хранить стейт всего придожения (то есть вся база данных это большой таймлайн изменений). Тебе это в целом не нужно, у теья все прилодение это распоеделенная бд, где мастер-ноды на сервере - ты в них пишешь, а слейв-нодв на клиентах, из которых ты читаешь и которые синхронизируешь с мастером.

Очередь нужна для того, чтобы реализовать push-модель обновления данных в клиентских стораджах.

Фактически это вмего-лишь еще одна ручка твоего апи. REST (или что там у тебя) остается для точечной подгрузки первоначальных данных и коммитов. А очередь (которую ты можешь илт слушать илиполлить, как сам решишь) ты читаешь от определеннлго таймстемпа от которого последний раз синхронизировался.

Очереди сообщений это тоде общепринятый паттерн, они иак и называбтся очереди. Для очередей отдельно есть готовые решения, хоть тот же rabbitmq. Но для симпл проджекта написать сервис очереди тривиально в целом.

(В очередь пишет только мастер, остальные только читают. В осереди только один тип сущности - событие, которое хранит вмю осиальную информацию. Фактически упрощенно очередь эьо одна таблица с индексом по таймстемпу).

object
()
Ответ на: комментарий от object

у теья все прилодение это распоеделенная бд, где мастер-ноды на сервере - ты в них пишешь, а слейв-нодв на клиентах, из которых ты читаешь и которые синхронизируешь с мастером.

Только нужно не забывать про права доступа к данным которые у разных клиентов могут сильно отличаться)

TDrive ★★★★★
()
Ответ на: комментарий от kijllfatncdaplp

It depends.

Зависит от размера твоих объектов и диффов изменений. Модно храни и в очереди, просто чаще ее схлопывать (создавать контрольную точку), можно просто хранить ид обновленноц сущности, и список полей которые были изменены. При синхронизации, клиенты читают очередь от настоязего в прошлое (до точкии последней синхронизации), поэтому данные дважды грузить не придется. Если в 10 часов у товара изменили цену, а в 11 изменили цену и название, то ты сходишь за обновлением только раз, подгрузив сразу последние значения всех но только изменнвх полей. А если сущгость в 12 уже удалили, то ты вообще никуда за ней пойдешь.

object
()
Ответ на: комментарий от object

Спасибо за развернутый ответ.

В очередь пишет только мастер, остальные только читают

Т.е. запрос с клиента сразу в очередь не попадает; сначала сервер обрабатывает запрос, затем сообщает об этом событии в очередь?

kijllfatncdaplp
() автор топика
Ответ на: комментарий от object

Смотря как сделаны сообщения, если там просто новые версии полей измененной записи из базы, а права запрещают доступ к отдельным полям то их придется вырезать перед отправкой.

Если ролей мало то может быть вариант когда проще сделать несколько очередей под каждую роль.

А еще может быть ситуация когда у человека нет доступа к сырым данным но должен быть доступ к результату от этих недоступных данных, так что там еще и в самих эвентах придется вычислять какую нибудь инфу зарание.

TDrive ★★★★★
()
Ответ на: комментарий от object

Если в 10 часов у товара изменили цену, а в 11 изменили цену и название, то ты сходишь за обновлением только раз

Занятная штука. Обязательно поиграюсь

подгрузив сразу последние значения всех но только изменнвх полей

Кажется это звучит как ад для языков со статической типизацией ) Запросы по разным полям. (Кроме того, это же будет n запросов для n изменившихся сущностей. Но это всё детали конечно)

kijllfatncdaplp
() автор топика
Ответ на: комментарий от kijllfatncdaplp

Да. Клиент пишет в мастер. А мастер по итогу уже заполняет очередь.

Если у тебя при коммите возникает конфликт (пользователь пытается обновить данные которые уже обновили параллельно), то тут ты сам решаещь кпк разруливать. Почитай про то как устроены базы дпнных в целом (лни де тоже парадлельно обрабатывают транзакции на изменнния, там есть разные виды блокировок) и как усироены распределеннык базы данных, какиони поддерживают консисиентность.

Но в целом тут всего несколько путей, зависит. От бизнес логики, ты либо принудительно обновляешь данные в любом случае (какая разница что пока ты менял цену ее уже изменили. Ты же ее не от балжы менял, а по какой-то причине. В худшем млучае будет просто дублирование события). Либо при конфликте отменяешь коммтт возврашая его пользователю с вопросом че делать и показывая уже новые данные, либо ты ппока сущность кто-то редактирует блокируешь от изменений (событие о блокировке пишется в ту же очередь) - но ткт надо уже предусмотреть макмимальное время блокировки и прочие нюансы (я могу открвть релактировпние и пойти спать). Без блокировок пока пользователь редактирует сущность, события все раанг могут приходить, так чтоиизвестит о том, что данные уже обновились можно и раньше).

В общем зависит от потребностей.

object
()
Ответ на: комментарий от kijllfatncdaplp

Кроме того, это же будет n запросов для n изменившихся сущностей. Но это всё детали конечно

Вот это уже решает graphql, например. Он для того и был создан. С ним можно сбатчить все в один запрос.

Но в целом тебе ни что не мешает и на ресте сделать ручку для гоуппового read. Там же просто массив необходимых сущностей и полей каждой.

object
()
Ответ на: комментарий от kijllfatncdaplp

Отпишись потом в этом треде по результатам) Получится у тебя запилить все это до состояния прода или нет)

TDrive ★★★★★
()
Ответ на: комментарий от TDrive

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

В целом таких нюансов млжно гору накидать, они все индивидуальны и зависят от бизнес процессов.

Потому и нет смысла искать какие универсальные готовыеирешения. Они если и будут, то окажутся очень громоздкими, что ты времени на конфигурацию потратишь больше, чем если бы писал все под задачу. И все равно не покроют фуллстек.

object
()
Ответ на: комментарий от object

Согласен, вот мне и интересно, что из всего этого в результате у ТСа получится, учитывая что он пришел за готовым решением что бы не разбираться во всем этом)

TDrive ★★★★★
()
Ответ на: комментарий от TDrive

Мы, програмисты, народ небыстрый. Дедлайн умножь на 2 и еще месяц прибавь )

kijllfatncdaplp
() автор топика
Ответ на: комментарий от kijllfatncdaplp

Не забывай еще про ленивую подгрузку данных в целом. То есть, тебе не обязательно при первом запросе синхронихировать всю базу. Или при долгом отсутствии клиента. На клиенте у тебя просто сервис который не найдет сущность у себя в базе при запросе, должен будет сходить за ней в мастер.

Синхронизировать надо только очередь первоочередно. Из очереди можно часто понять что сущности уже например и не существует больше.

object
()
Ответ на: комментарий от object

Ну, хотя, если у тебя там вся база в 200кб убирается, то тут ленивость ни к чему.

object
()
Ответ на: комментарий от TDrive

а не потому что не знает что ему нужно

Конечно он не знает, он и спрашивает, какие есть варианты заиметь diff вместо снапшота. Технически абсолютно насрать как он эту дельту будет получать, хоть голубиной почтой. Сокеты-херокеты - это всё вторично и не имеет отношения к вопросу.

no-such-file ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.