Бизнес требует стабильной работы мобильного приложения у всех пользователей. Что делать?

Блог Форсайт

Олег Бачурин

Как загружать большие объемы данных на мобильные устройства бизнес-пользователей?

Часть 1.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  • При разрыве соединения возобновление дозагрузки начинается со следующего байта. Хотели максимально сэкономить трафик. Подсчёт байтов реализовали через запись в промежуточный файл.
  • Собирали полный пакет ответа в промежуточном кеше, чтобы запустить запись в БД в рамках отдельной операции вставки. Узнали, что запись на файловую систему — относительно медленная операция.
  • Проводили на устройстве конвертацию формата из 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}")

}
}

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

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

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

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

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

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

Комментарии

Email не будет опубликован.
Подробнее о политике использования персональных данных