Как загружать большие объемы данных? Часть 1.

Привет, друзья! Наша команда более 10 лет занимается вопросами эффективной передачи данных на мобильные устройства. Мы исследовали разные варианты: одни оказались слишком медленными, другие приводили к переполнению памяти на мобильном устройстве.

Хотим рассказать, как мы в команде «Форсайт. Мобильная платформа» сделали синхронизацию больших объемов данных, чтобы это работало, в том числе, на ТСД (терминал сбора данных). Для экономии батареи ТСД специально снабжают слабыми процессорами. Весь подбор инструментов и алгоритмов мы уже апробировали в продукте «Форсайт. Мобильная платформа» (ФМП).

Специфичные условия для мобильного приложения начнем РАЗБИРАТЬ С КРЫШИ, так будет проще подобраться к существу вопроса. В среднем, отличие общедоступных приложений от бизнес-приложений – в объеме потребляемых данных. У бизнес-приложений объем данных значительно больше. Но, как водится, ожидание бизнес-пользователей от приложения точно такие же, как и у всех: приложение должно работать не просто быстро, а моментально. А это значит, что нужно найти особые техники по ускоренной передаче данных. При подборе технологий для транспорта данных нам хотелось получить:

А) Стабильный механизм передачи данных.

Б) Самый быстрый/производительный протокол из возможных.

Поскольку мы производим спецшину по транспорту данных, для нас это означает, что мобильный пользователь будет обращаться в наш инструмент за «чемоданом» данных, и мы должны как можно быстрее передать ему этот «чемодан» целиком. У разработчиков возникает вопрос — насколько большой может быть «чемодан»? Насколько будет нескромен кейс, который нужно будет тащить? За ориентир мы взяли ½ миллиона записей табличных данных.

У вас может возникнуть вопрос: зачем тащить ½ миллиона записей на мобильное устройство?

ВЫ АРХИТЕКТУРНО НЕ ТАК ДЕЛАЕТЕ! Ведь можно подгружать данные по мере необходимости.

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

70% пользователей сообщили: «нормально работает».

30% пользователей написали: «приложение работает плохо, зависает и не грузит».

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

Снова смотрим на архитектуру перегрузки ½ миллиона записей. Будем исследовать, какой протокол даёт приемлемую надёжность и скорость. Для этапа загрузки БОЛЬШИХ данных мы стали применять термин «Первичная синхронизация». Процедура может быть сверхтяжёлая, и всё равно было нужно, чтобы она проходила гладко, не падала из‑за переполнения памяти, какой бы объем ни тащили в мобильное приложение.

Проводя исследования и сравнения различных форматов и протоколов, мы выделили:

  1. Большие объемы способен передать потоковый алгоритм. Он же показал низкую чувствительность к объемам данных.

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

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

Что получаем? Давайте сравним и подытожим:

Распространённый подход

(простой в реализации JSon)

Потоковый

(делать сложнее)

Цепочка действий 

●  На сервере: вычитка данных с БД

●  На сервере: сериализация в JSon для передачи данных по интернет-каналам.

●  Передача по сети Internet

●  На телефоне: десериализация (конвертация в команду SQL)

●  На телефоне: запись в БД.

 

 

●  На сервере: вычитка данных с БД

●  Разделение объема на пакеты

●  На сервере: сериализация пакетов для передачи данных по интернет-каналам (Приведение к формату).

●  Как первый пакет готов Передача по сети Internet.

●  На телефоне: десериализация пакета (конвертация в команду SQL)

●  На телефоне: запись в БД. 

Вывод

Все шаги идут последовательно, следующий шаг выполняется только после успешного завершения предыдущего. Легко делать, но можно отметить довольно быструю деградацию с ростом объёма.

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

Сравнение подходов
Сравнение подходов

Посмотрите на схему в точку «А» на мобильном устройстве. Изначально тут мы с благими намерениями сделали несколько лишних вычислений, которые потом пришлось исправлять.

Перечислим лишние, если вы решите повторять алгоритм:

  1. При разрыве соединения возобновление дозагрузки начинается со следующего байта. Хотели максимально сэкономить трафик. Подсчёт байтов реализовали через запись в промежуточный файл.

  2. Собирали полный пакет ответа в промежуточном кеше, чтобы запустить запись в БД в рамках отдельной операции вставки. Узнали, что запись на файловую систему — относительно медленная операция.

  3. Проводили на устройстве конвертацию формата из JSon в SQL команду.

Лишние действия замедляют процесс синхронизации. Далее покажем, как мы всё это исправили и получили более органичный результат:

  1. Внутри формата JSon на сервере ФМП мы стали формировать записи в нотации SQL языка, чтобы не делать этого на мобильном устройстве, а сразу брать пакет и делать вставку в БД.

  2. Возобновление дозагрузки стали делать по ID номеру строки, внутри пакета, перестали считать номера байтов и стали использовать ID номера строки.

  3. Отказались от промежуточного формирования файла.

Мы рекомендуем избегать записей в промежуточный файл. Если задача записать в БД, проектируйте вставку (insert) без промежуточных вычислений и действий. В этом случае вы, как и мы, приблизитесь к скорости, фактически равной ширине предоставляемого канала.

У ФМП сложилась схема из двух частей:

А) серверная часть умеет отправлять данные в потоке
Б) мобильный фреймворк умеет принимать поток и складывать его в БД на телефоне.

Посмотрите на схему ниже.

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

С ФМП для подключения потоковой передачи данных - от бизнес источника до приложения - программировать ничего не нужно. На стороне бизнес-системы определите выходные интерфейсы, и далее ФМП сделает всё необходимое автоматически. ФМП создаст необходимые объекты на сервере, а аналогичную работу проделает сам фреймворк в мобильном приложении. Потоковая передача будет работать, даже если бизнес-система не поддерживает поток, эту работу на себя возьмёт ФМП.

Посмотрите листинг кода на Kotlin для Android Фреймворка. Под столь компактный код упакованы:

  1. Аутентификация

  2. Создание БД

  3. Создание структур в БД под хранение объекта данных

  4. Обработка входного потока данных

  5. Чтение данных из БД.

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}")

}

}

Хотим сказать, что с таким простым транспортом можно сконцентрироваться на создании интерфейса бизнес-приложения.

  • Затрат времени почти нет, код компактный и понятный.

  • Не нужно тратить время на апробации подходящих решений для надёжной загрузки больших объёмов.

  • Стабилизация приложения также занимает меньше времени. А это, как вы знаете, наиболее эмоционально напряжённая часть проекта. Из-за возможных остановок сервиса и перевыпуска релизных сборок.

Как итог — бизнес‑пользователь получает приложение быстрее, а значит дешевле.

Желаем всем добра и высокой скорости передачи данных.

Комментарии (6)


  1. Chapaev2023
    29.07.2025 07:57

    все равно непонятно зачем вашему пользователю 500 000 записей - он с ними что делает?


  1. Oleg-Bachurin Автор
    29.07.2025 07:57

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


    1. Chapaev2023
      29.07.2025 07:57

      это все в мобильнике? или пользователь тоже мобильный?
      даже если так - посредине склада пользователю срочно требуется посмотреть номекулатуру в 100 000 записей о товаре?
      и какие выводы можно сделать по такой объмной выборке ?


  1. Habr4687544
    29.07.2025 07:57

    Sql > csv > http Accept gzip> csv > bulk insert


  1. Oleg-Bachurin Автор
    29.07.2025 07:57

    Здравствуйте, спасибо за цепочку, для некоторых задач подходящая. Если в первой точке, подготовите файл БД сразу пригодный для работы на телефоне. То лишние операции из цепочки можете оптимизировать. Будет быстро.


  1. Oleg-Bachurin Автор
    29.07.2025 07:57

    Спасибо за уточняющие вопросы. В нашем опыте, мобильным пользователям, в рабочую смену, работать со справочными позициями требуется поштучно. В конкретный момент времени при выполнении операции нужна одна запись из объёма. Выводы можно сделать такие, если требуется построить систему где у всех пользователей должно работать мгновенно (время реакции на действия до 1 сек) то данные должны быть с высокой доступностью. Будем рады если пригодиться наш опыт.