Скучное вступление
Не так давно, мне довелось участвовать в разработке некоего программно-аппаратного комплекса для одной американской компании. Разрабатывал я бэкенд, немного фронтенд, сращивал устройства с облаком (IoT то бишь). Стек технологий был обозначен четко. Ни в право, ни в лево — enterprise, одним словом. В определенный момент меня перекинули в помощь на фронтенд POS (Point of Sale) веб приложения.
Проблема. Становится интересней
Всё бы ничего, но веб приложение разрабатывалось для работы в 6 тыс. офисах по всей территории Америки (для начала). Где, как оказалось, с интернетом могут быть проблемы. Да да, в той самой, продвинутой Америке! Проблемы с покрытием не только проводного интернета, но и мобильной связью! Т.е. плохой интернет канал (часто, мобильный) — вполне себе обычная история для небольших американских городов.
А это же POS… Тут, понимаешь, клиенты стоят, надо инвойс быстро распечатать… Тормозов быть не должно! И livesearch… Были обсуждения, прикидки, в итоге — не стали грузить бэкенд запросами (трафик, опять же). Сошлись на том, что веб приложение должно по-максимуму подгружать данные и делать, тот же поиск, локально. Речь идет, конечно, о данных, размер которых позволяет это сделать.
Данных фронтенд тянул много, из разных сервисов. Как следствие — большой трафик и долгая загрузка страниц. В общем — беда.
Часть проблем решается бэкендом (сжатие, гео-кластеринг и тп), но это — отдельная история, сейчас только о фронтенде.
Кэш
Первое, что было сразу сделано (и это логично) — кэширование и хранение получаемых данных локально. По возможности — долго, насколько позволяет секьюрити. Для каких-то данных лучше использовать localStorage, для других — sessionStorage, а что не помещается — можно просто хранить в памяти. Мы использовали ангуляр, поэтому нам вполне подошел для этих целей angular-cache.
Отчасти, это решило проблему — при первом обращении к какой-либо странице, подгружались требуемые ей ресурсы. Далее, ресурсы уже брались из кэша.
Сделали, конечно, инвалидацию кэша и тп. Трафик сократился, но отклик при первоначальном обращении к страницам оставался непозволительно большим.
Фоновая подгрузка ресурсов
Следующим логичным шагом стала фоновая подгрузка ресурсов. Пока мы находимся на главной странице, смотрим сообщения, алерты и тп — происходит фоновая загрузка ресурсов для скрытых разделов. Идея в том, что когда пользователь переключится на нужный раздел, данные (или хотя бы их часть) будут уже в кэше, загрузка не потребуется, отклик страницы уменьшится. Первый вариант самый простой:
$q.all([
Service1.preLoad(),
Service2.preLoad(),
…
ServiceN.preLoad()
]);
Где Service.preLoad() — функция, возвращающая promise ресурса страницы.
Но есть проблема — в этом случае все промисы выполняются одновременно, т.е. одновременно тянутся все 100500 ресурсов. А канал-то — не резиновый! Плюс у нас мега-запрос к стороннему сервису, который долго и медленно качал много данных. В итоге все параллельные запросы выполнялись по времени почти столько же, сколько и этот мега-запрос.
Окей, загрузим по-порядку:
Service1.preLoad()
.then().Service2.preLoad()
…
.then().ServiceN.preLoad();
Оно, воде как, стало лучше, но не очень — если пользователь сразу шел в “неправильный” раздел — будем ждать прогрузки очереди, пока не доберемся до ресурсов этого раздела. В общем, тут бесконечная эвристика. Что-то лучше пачкой загружать, что-то отдельно в параллели, и тд и тп… Хотелось, однако, какого-то более системного подхода.
Очередь
И опять логичный шаг — положить загрузку всех ресурсов в динамическую очередь. Приоритетную. Если заходим в раздел, а его данные (или их часть) еще в очереди — мы повышаем их приоритет и они загружаются в первую очередь. В итоге отклик <= чем было. Мелочь, а приятно.
Так то оно так, только есть еще одно, не резиновое место — размер кэша.
Система ресурсов
Чем глубже погружался в эту проблему, тем сильнее возникало дежавю — я это уже проходил! Ограниченные ресурсы памяти, фоновая загрузка, выгрузка, приоритеты… это же… это же — типичная система ресурсов игрового движка! Едешь на машинке — локации подгружаются, что далеко позади — выгружаются. Еще термин специальный был для игровых движков — поддержка стримминга… Лет 5 жизни я провел в геймдеве, это было круто…
Так вот. Получается, веб приложение, по сути — аналог ресурсоемкой игры. Только здесь у нас не локации — а страницы/разделы. Страницы тянут ресурсы и складывают в кэш, а он — ограниченный в размере, надо что-то выгружать. Т.е. аналогия уместна.
Проблема веб приложения, что и в геймдеве — предсказать где будет пользователь, чтобы загрузить ресурсы заранее. Решение №1 — дизайн, конечно. Направить пользователя по предсказуемому (а иногда, и единственному) маршруту. Что не удается решить дизайном — статистика + эвристика.
Выгрузка — то же самое. Только уже надо понять, что выгрузить в первую очередь. Задаем приоритеты самим ресурсам, выгружаем ресурсы с наиболее низким приоритетом. Или грохаем целыми пачками.
Вариантов реализации — масса. Самый предсказуемый — предпроцессинг, ручной либо автоматический. Мы должны указать, какие ресурсы в локации/разделе нам будут нужны, а каким-то снизить приоритет.
LOD
И тут Остапа понесло… В смысле, в голову полезли геймдев-трюки, применимые в веб разработке. Один из них — LOD (Level of Detail), уровень детализации. В геймдеве он применяется для текстур и моделей. Можно сразу прогрузить мир с минимальным уровнем детализации, а стриммить уже детализированные модели текстуры. И игрок всегда что-то видит, и даже, может играть.
Т.е. нужна система LOD для загружаемых данных! Для веб подходит самый примитивный вариант — два уровня детализации. Сначала грузим начальные данные, которые видит пользователь (первые страницы таблиц, например).
Данных получается мало, грузятся быстро. А бэкграундом… бэкграундом грузятся уже LOD’ы “по-тяжелее”.
Компрессия
Впихнуть невпихуемое — почти стандартная задача игродела. Ну так давайте раздвинем границы localStorage! Берем какой-нибудь LZ-компрессор и вперед! Да, но localStorage может хранить только строки… Ну, тогда сгодится, например, lz-string.js. Компрессия уже не та, но даже -20%, когда в распоряжении всего 5Мб — это совсем не плохо! И как бонус — секьюрные дела, в localStorage будет не открытый текст, а китайские знаки.
А дальше-то, что?
Дальше… дальше мысль несется к неизведанным глубинам. В памяти всплывает VFS (Virtual File System) — прослойка между ресурсной системой игры и файловой системой операционной системы. Обычно, всё крутится вокруг data-файла, к которому можно обратится как к файловой системе. Прочитать файл, записать… А что если сделать VRC (Virtual REST Call)!? Тогда ведь можно поддержать работу веб приложения вообще при отсутствии интернет соединения! В какай-то степени, конечно, но всё же.
Контроллеры общаются с менеджером ресурсов. Он что может отдать — отдает сразу, все остальные запросы отдаются VRC. А он, в свою очередь, уже самостоятельно синхронизирует своё состояние с бэкендом и, по мере загрузки, информирует об этом.
Когда говорят про оффлайновую работу веб приложения, обязательно проскальзывает Meteor. Круто, конечно, но мы находились в жестких рамках стека разработки. Предлагаемый же вариант, можно реализовать практически на любом фреймворке. С оговорками, конечно, но можно.
Но статья конкретно не про это. А про то, как порой неожиданно, всплывает опыт давно минувших дел…
Приятного кодирования, друзья!
Комментарии (13)
Zav
07.06.2016 05:41+1Оригинальное управление ресурсами — возьму на вооружение, спасибо.
Касательно размеров хранилища — можно ведь запросить ещё.
Ну и в зависимости от продукта это можно ещё решить, например, плагином для браузера.
В том же хроме сайт можно вообще вынести, что бы открывался отдельным ярлыком без интерфейса самого браузера.
Касательно метеора — это не ультимативное решение, мне приходилось ранее решать проблемы работы приложений оффлайн. Текущий мой проект реализован на метеоре, и в ближайший месяц-два обязательно напишу цикл статей по разработке. Но что хочу сказать так это то, что везде надо подходить с умом :-)
Один пример приведу. Метеор круто копит внешние запросы, когда нет подключения, и отсылает их пачкой когда оное появляется.
Пол года назад, на этапе закрытого бета теста, мы словили очень крутой само-ддос, когда в датацентре решили перезагрузить наш сервер и он сам подняться не смог, а с утра, те кто не выключал игру, начали слать тысячи запросов, которое накопились за это время.
Очевидным решением был контроль исходящей очереди.
Это я к тому, что с какого-то момента всё равно подобные детали приходится контролировать в полу-автоматическом режиме и любой отдельно взятый инструмент необходимо уметь грамотно применять.
Уважаемый модератор — если приведенные ссылки «не приемлимы» — просто удалите их, не надо мне в очередной раз блокировать аккаунт, пожалуйста.ilya_radinsky
07.06.2016 07:07Согласен. Собственно, вся эта возня с кэшами возникла для исключения потенциального ддос'а — livesearch.
amakhrov
07.06.2016 06:21> А что если сделать VRC (Virtual REST Call)!? Тогда ведь можно поддержать работу веб приложения вообще при отсутствии интернет соединения!…
Вы только что описали работу ServiceWorker :) developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
Proteck
07.06.2016 12:46Было бы интересно увидеть результаты, какое ускорение получилось по каждому из улучшений
ilya_radinsky
07.06.2016 13:43Честно говоря, таких цифр нет — просто боролись за приемлемую скорость загрузки и уменьшение трафика.
atc
07.06.2016 12:56Очень похоже на клиентские data layer пакеты, вроде ember-data, orbit.js, fortune.js, особенно в части построения графа зависимостей, оптимизации запросов, кешировании.
Автор, вы их не рассматривали?ilya_radinsky
07.06.2016 13:45Нет, не рассматривали. У нас были жесткие рамки: бэкенд — MS (ASP.Net, MS SQL, Azure и тп), фронтенд — ангуляр (с поддержкой планшетов и мобильников).
atc
07.06.2016 13:49С ангуляром эти библиотеки вполне совместимы, к бекенду так же требований практически не предъявляют за исключением протокола (чаще всего используется jsonapi.org), но и это вполне преодолимо, так как в большинстве data layer возможно написание адаптеров под свое api.
ilya_radinsky
07.06.2016 14:12Деталей особо не помню, но мы в свое время ничего подходящего для себя не нашли. Те же веб сокеты мы смогли использовать только в рамках SignalR и Azure IoT Hub.
yokotoka
07.06.2016 13:46Кстати, насчёт оффлайновой работы POS-терминалов есть ещё вот такая интересная штука:
https://www.youtube.com/watch?v=uyZKWyciSXY
https://github.com/gritzko/swarm
Ashur_451
Немного напомнило мне как в Learning How To Learn на курсере рассказывали про свойства нейронных паттернов. Один нейронный паттерн созданный при обучении одной вещи может быть использован частично или полностью в выполнении другой. Я на пример, всегда видел много аналогий в музыкальной теории и математике.
PS: Ссылка на курс, очень советую, ru.coursera.org/learn/learning-how-to-learn