

Как загружать большие объемы данных? Часть 1.
Привет, друзья! Наша команда более 10 лет занимается вопросами эффективной передачи данных на мобильные устройства. Мы исследовали разные варианты: одни оказались слишком медленными, другие приводили к переполнению памяти на мобильном устройстве.
Хотим рассказать, как мы в команде «Форсайт. Мобильная платформа» сделали синхронизацию больших объемов данных, чтобы это работало, в том числе, на ТСД (терминал сбора данных). Для экономии батареи ТСД специально снабжают слабыми процессорами. Весь подбор инструментов и алгоритмов мы уже апробировали в продукте «Форсайт. Мобильная платформа» (ФМП).
Специфичные условия для мобильного приложения начнем РАЗБИРАТЬ С КРЫШИ, так будет проще подобраться к существу вопроса. В среднем, отличие общедоступных приложений от бизнес-приложений – в объеме потребляемых данных. У бизнес-приложений объем данных значительно больше. Но, как водится, ожидание бизнес-пользователей от приложения точно такие же, как и у всех: приложение должно работать не просто быстро, а моментально. А это значит, что нужно найти особые техники по ускоренной передаче данных. При подборе технологий для транспорта данных нам хотелось получить:
А) Стабильный механизм передачи данных.
Б) Самый быстрый/производительный протокол из возможных.
Поскольку мы производим спецшину по транспорту данных, для нас это означает, что мобильный пользователь будет обращаться в наш инструмент за «чемоданом» данных, и мы должны как можно быстрее передать ему этот «чемодан» целиком. У разработчиков возникает вопрос — насколько большой может быть «чемодан»? Насколько будет нескромен кейс, который нужно будет тащить? За ориентир мы взяли ½ миллиона записей табличных данных.
У вас может возникнуть вопрос: зачем тащить ½ миллиона записей на мобильное устройство?
ВЫ АРХИТЕКТУРНО НЕ ТАК ДЕЛАЕТЕ! Ведь можно подгружать данные по мере необходимости.
Сразу ответим — мы с этого начинали. Подгрузка данных – требует стабильного подключения к сети, это online-приложение, у которого вскрылись свои проблемы.
70% пользователей сообщили: «нормально работает».
30% пользователей написали: «приложение работает плохо, зависает и не грузит».
В online время реакции приложения на действия пользователя не ровное, у многих пользователей есть выбросы в большую сторону. Увы, чистый online нам не помощник. Нужно обеспечить одинаковую работу у всех пользователей. Такова специфика бизнес-приложений.
Снова смотрим на архитектуру перегрузки ½ миллиона записей. Будем исследовать, какой протокол даёт приемлемую надёжность и скорость. Для этапа загрузки БОЛЬШИХ данных мы стали применять термин “Первичная синхронизация”. Процедура может быть сверхтяжёлая, и всё равно было нужно, чтобы она проходила гладко, не падала из-за переполнения памяти, какой бы объем ни тащили в мобильное приложение.
Проводя исследования и сравнения различных форматов и протоколов, мы выделили:
1) Большие объемы способен передать потоковый алгоритм. Он же показал низкую чувствительность к объемам данных.
2) Максимальную стабильность в разных условиях связи имеет потоковый алгоритм передачи. Так как есть механизм дозагрузки пакета.
3) За счёт объявления в заголовке структуры данных и далее передачи массива значений через разделитель (без сжатия GZip), мы увидели сопоставимый размер данных, передаваемых по сети.
Что получаем? Давайте сравним и подытожим:
|
Распространённый подход (простой в реализации JSon) |
Потоковый (делать сложнее) |
|
Цепочка действий |
|
|
● На сервере: вычитка данных с БД ● На сервере: сериализация в JSon для передачи данных по интернет-каналам. ● Передача по сети Internet ● На телефоне: десериализация (конвертация в команду SQL) ● На телефоне: запись в БД.
|
● На сервере: вычитка данных с БД ● Разделение объема на пакеты ● На сервере: сериализация пакетов для передачи данных по интернет-каналам (Приведение к формату). ● Как первый пакет готов Передача по сети Internet. ● На телефоне: десериализация пакета (конвертация в команду SQL) ● На телефоне: запись в БД. |
|
Вывод |
|
|
Все шаги идут последовательно, следующий шаг выполняется только после успешного завершения предыдущего. Легко делать, но можно отметить довольно быструю деградацию с ростом объёма. |
Потоковая передача данных — хороший инструмент в сфере технологий для создания масштабируемых приложений. Но будут сложности с реализацией и отладкой. |
Посмотрите на схему в точку «А» на мобильном устройстве. Изначально тут мы с благими намерениями сделали несколько лишних вычислений, которые потом пришлось исправлять.
Перечислим лишние, если вы решите повторять алгоритм:
-
При разрыве соединения возобновление дозагрузки начинается со следующего байта. Хотели максимально сэкономить трафик. Подсчёт байтов реализовали через запись в промежуточный файл.
-
Собирали полный пакет ответа в промежуточном кеше, чтобы запустить запись в БД в рамках отдельной операции вставки. Узнали, что запись на файловую систему — относительно медленная операция.
-
Проводили на устройстве конвертацию формата из JSon в SQL команду.
Лишние действия замедляют процесс синхронизации. Далее покажем, как мы всё это исправили и получили более органичный результат:
-
Внутри формата JSon на сервере ФМП мы стали формировать записи в нотации SQL языка, чтобы не делать этого на мобильном устройстве, а сразу брать пакет и делать вставку в БД.
-
Возобновление дозагрузки стали делать по ID номеру строки, внутри пакета, перестали считать номера байтов и стали использовать ID номера строки.
-
Отказались от промежуточного формирования файла.
Мы рекомендуем избегать записей в промежуточный файл. Если задача записать в БД, проектируйте вставку (insert) без промежуточных вычислений и действий. В этом случае вы, как и мы, приблизитесь к скорости, фактически равной ширине предоставляемого канала.
У ФМП сложилась схема из двух частей:
А) серверная часть умеет отправлять данные в потоке
Б) мобильный фреймворк умеет принимать поток и складывать его в БД на телефоне.
Посмотрите на схему ниже.

Помимо потоковой загрузки данных, фреймворк реализует: защиту данных, загрузку файлов, аутентификацию пользователя, дельта загрузку и многое другое, обещаем про это тоже написать.
С ФМП для подключения потоковой передачи данных — от бизнес источника до приложения — программировать ничего не нужно. На стороне бизнес-системы определите выходные интерфейсы, и далее ФМП сделает всё необходимое автоматически. ФМП создаст необходимые объекты на сервере, а аналогичную работу проделает сам фреймворк в мобильном приложении. Потоковая передача будет работать, даже если бизнес-система не поддерживает поток, эту работу на себя возьмёт ФМП.
Посмотрите листинг кода на Kotlin для Android Фреймворка. Под столь компактный код упакованы:
-
Аутентификация
-
Создание БД
-
Создание структур в БД под хранение объекта данных
-
Обработка входного потока данных
-
Чтение данных из БД.
val fmp: FMP = FMP.Builder() // Создать конструктор FMP.
.api(FMP.API.V2) // Указать версию API сервера.
.address(«https://HOST«) // Адрес сервера платформы.
.environment(«ENVIRONMENT») // Среда на сервере.
.project(«PROJECT») // Проект внутри среды.
.username(«USERNAME») // имя пользователя.
.deviceID(«device_id») // Указать ID устройства.
.storage(«/path/to/storage») // Указать рабочую директорию.
.build() // Создать FMP.
val auth_password: FMPResult = fmp.user.auth(«password») // Аутентификация по паролю.
val resource: FMPResource = fmp.resource
.name(«…») // Указать название ресурса на сервере.
.params(«…») // Указать параметры ресурса.
.cacheByParams(true) // Использовать кэширование по параметрам.
.delta(true) // Использовать дельту.
.filter(true) // Использовать фильтрацию FMPQuery.
.build() // Получить FMPResource.
val download: FMPResult = resource.download() // Потоковая загрузка данных ресурса и сохранение в БД на телефоне.
val tableData: FMPResult>> = resource.database.select(«SELECT * FROM mTable;») // Получение данных из БД на телефоне.
Потоковая загрузка решает проблемы с большими объёмами и стабильно загружает данные. Но вы хотите ещё быстрее. Мы создали решение, которое отлично подходит для условий, если предоставляется широкий канал и покрытие очень хорошее. Суть ускорения в многопоточности, это позволяет ускорить получение данных.
Взгляните на схему, на ней за единицу времени удвоили производительность, можно запустить ещё несколько параллельных потоков, и тогда будет в три раза быстрее и так далее x4, x5…

Что можем порекомендовать: первым на загрузку ставьте самый большой по объёму справочник. Параллельно запрашивайте цепочку из меньших по объёму.
В схеме с многопотоковой загрузкой часто бывает, что время на загрузку коррелирует с объёмом самого большого справочника.
Ниже приведём
package ru.fsight.fmp.test
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.junit.Assert
import ru.fsight.fmp.FMP
import ru.fsight.fmp.FMPQuery
import ru.fsight.fmp.FMPResource
import ru.fsight.fmp.FMPResult
class SandboxTest {
@Before fun setUp() { } @After fun cleanUp() { } @Test fun `Initialization FMP`() { val fmp: FMP = FMP.Builder() .address("http://mobilefmp.dev.fs.fsight.world") .environment("env_denis") .project("proj_test") .api(FMP.API.V2) .deviceID("denistest") .storage("./test") .username("test") .build() } @Test fun `User authentication by password`() { val fmp: FMP = FMP.Builder() .address("http://mobilefmp.dev.fs.fsight.world") .environment("env_denis") .project("proj_test") .api(FMP.API.V2) .deviceID("denistest") .storage("./test") .username("test") .build() val auth: FMPResult = fmp.user.auth("testtest") } @Test fun `Getting the resource data manually`() { val fmp: FMP = FMP.Builder() .address("http://mobilefmp.dev.fs.fsight.world") .environment("env_denis") .project("proj_test") .api(FMP.API.V2) .deviceID("denistest") .storage("./test") .username("test") .debug(false) .build() val auth: FMPResult = fmp.user.auth("testtest") val resource_1: FMPResource = fmp.resource .name("GetPlanningPeriodsCount") .params("{\"Count\":500000}") // 1C Basic .build() val resource_2: FMPResource = fmp.resource .name("GetPlanningPeriodsCount_2") .params("{\"Count\":500000}") .build() val resource_3: FMPResource = fmp.resource .name("GetPlanningPeriodsCount_3") .params("{\"Count\":500000}") .build() val start = System.currentTimeMillis() var total1 = 0L var total2 = 0L var total3 = 0L val t1 = Thread { val start1 = System.currentTimeMillis() val Download_1 = resource_1.download() total1 = System.currentTimeMillis() - start1 } val t2 = Thread { val start2 = System.currentTimeMillis() val Download_2 = resource_2.download() total2 = System.currentTimeMillis() - start2 } val t3 = Thread { val start3 = System.currentTimeMillis() val Download_3 = resource_3.download() total3 = System.currentTimeMillis() - start3 }
// Parallel //параллельная
t1.start()
t2.start()
t3.start()
t1.join()
t2.join()
t3.join()
// Seq //последовательная
//t1.start()
//t1.join()
//t2.start()
//t2.join()
//t3.start()
//t3.join()
val total = System.currentTimeMillis() - start println("Total = ${total}, 1 = ${total1}, 2 = ${total2}, 3 = ${total3}") }
}
Хотим сказать, что с таким простым транспортом можно сконцентрироваться на создании интерфейса бизнес-приложения.
▪️ Затрат времени почти нет, код компактный и понятный.
▪️ Не нужно тратить время на апробации подходящих решений для надёжной загрузки больших объёмов.
▪️ Стабилизация приложения также занимает меньше времени. А это, как вы знаете, наиболее эмоционально напряжённая часть проекта. Из-за возможных остановок сервиса и перевыпуска релизных сборок.
Как итог – бизнес-пользователь получает приложение быстрее, а значит дешевле.
Желаем всем добра и высокой скорости передачи данных.
ссылка на оригинал статьи https://habr.com/ru/articles/931940/
Добавить комментарий