[Программирование, Разработка мобильных приложений, Dart, Flutter] Сервис на языке Dart: каркас серверного приложения

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
18-Авг-2020 14:37

Оглавление

SPL


Подготовка
В прошлый раз мы закончили на том, что разместили статическую веб страницу-заглушку, разработанную с использованием Flutter для web. Страница отображает прогресс разработки нашего сервиса, однако данные о датах начала разработки и релиза пришлось захардкодить в приложении. Таким образом мы лишились возможности изменить сведения на странице. Пришло время разработать приложение — сервер данных. Схема всех приложений сервиса — в статье «Сервис на языке Dart: введение, инфраструктура бэкэнд».
В этой статье мы напишем приложение с использованием фреймворка Aqueduct, оценим его производительность и потребление ресурсов в разных режимах, напишем инструментарий для компиляции в нативное приложение для Windows и Linux, разберемся с миграциями схемы базы данных для доменных классов приложения и даже опубликуем наш инструментальный docker образ в публичный регистр DockerHub.

полезности

SPL
  • все скриншоты с кодом «кликабельные»
  • полный код здесь
  • консольные команды собраны в README.md проекта
  • комментарии, вопросы, советы — приветствуются
  • пообщаться с автором можно в Телеграмм канале


Установка Aqueduct
Начнем с установки dart-sdk — набора средств разработки на языке Dart. Установить его можно с использованием пакетного менеджера вашей операционной системы как предложено здесь. Однако, в случае Windows никакого пакетного менеджера в вашей системе по умолчанию не установлено. Поэтому просто:
  • Скачаем архив и распакуем его на диск C:
  • Теперь, чтобы наша операционная система знала, где искать исполняемые файлы, добавим необходимые пути. Откроем переменные ОС. Для этого начнем вводить «изменение переменных среды текущего пользователя» в строке поиска

  • В открывшемся окне выберем переменную Path и нажмем Изменить. В открывшемся списке создадим новую строку с адресом до исполняемых файлов dart в файловой системе, например, C:\dart-sdk\bin
  • Проверим, что dart и pub (пакетный менеджер dart) доступны
    dart --version


    pub -v


  • Возможно, чтобы новые пути стали доступны, придется перезагрузиться
  • Установим утилиту командной строки aqueduct CLI (command line interface)
    pub global activate aqueduct
    Проверим доступность
    aqueduct


Теоретически можно установить локально также сервер баз данных PostgreSQL. Однако Docker позволит нам избежать этой необходимости и сделает среду разработки подобной среде выполнения на сервере.
Генерация приложения
Итак, откроем папку нашего сервера в VsCode
code c:/docs/dart_server

Для тех, кто не видел первуюи вторуюстатьи, исходный код можно склонировать из guthub репозитория:
git clone https://github.com/AndX2/dart_server.git
Создадим шаблон приложения:
aqueduct create data_app


Ознакомимся с содержимым шаблона проекта:
  • README.md — заметка с описанием, как работать с aqueduct проектом, запускать тесты, генерировать API документацию и пр. Вспомогательный файл.
  • pubspec.yaml — спецификция для пакетного менеджера pub. Здесь находятся сведения об используемых пакетах, названии, описании, версии проекта и пр.
  • config.yaml и config.src.yaml — конфигурация для отладки и тестирования проекта соответственно. Мы не будем использовать этот способ конфигурирования.
  • analysis_options.yaml — правила для линтера (утилиты подсветки ошибок в исходном коде). Вспомогательный файл.
  • .travis.yml — конфигурация для системы автоматической сборки и тестирования (continuous Integration). Вспомогательный файл.
  • pubspec.lock и .packages — автоматически сгенерированные файлы пакетного менеджера pub. Первый — список всех зависимостей проекта, включая транзитивные и их конкретные версии, второй — расположение скачанных пакетов зависимостей в файловой системе (кэше).
  • .dart_tool/package_config.json — файл отчета о генерации кода нашего проекта, созданный aqueduct CLI. Вспомогательный файл.
  • bin/main.dart — точка входа в приложение при локальном запуске (например, для отладки). Мы не будем использовать такой способ запуска (исключая тесты).
    оригинал
  • lib/channel.dart — Фактически ApplicationChannel — это и есть экземпляр нашего приложения. Aqueduct умеет запускать несколько таких экземпляров для более эффективной утилизации ресурсов CPU и RAM. Такие экземпляры работают в изолированных потоках (в Dart их называют isolate) и никак (почти) не могут взаимодействовать друг с другом.
    оригинал
  • lib/data_app.dart — файл инкапсуляции импортов зависимостей. Позволяет объединить необходимые пакеты в условную (library) библиотеку dart_app
    оригинал
  • test/ — автотесты. Здесь можно разместить юнит-тесты, поскольку механизм тестирования сетевого слоя рассчитан на локальный запуск приложения и не будет использоваться при разработке. Для тестов будем использовать Postman.

Конфигурация
Первая задача, которую предстоит решить, — настройка приложения при запуске. Aqueduct имеет встроенный механизм извлечения параметров из конфигурационных файлов, но этот механизм не очень удобен при запуске в Docker контейнере. Мы поступим иначе:
  • Передадим список переменных в операционную систему контейнера.
  • При запуске приложения внутри контейнера прочитаем переменные окружения операционной системы и используем их для первоначальной настройки.
  • Создадим маршрут для просмотра по сети всех переменных окружения запущенного приложения (это будет полезно при просмотре состояния приложения из панели администратора).

В папке /lib создадим несколько папок и первый репозиторий для доступа к переменным окружения:

EnvironmentRepository в конструкторе считывает переменные окружения из операционной системы в виде словаря Map<String, String> и сохраняет в приватной переменной _env. Добавим метод для получения всех параметров в виде словаря:

оригинал
lib/service/EnvironmentService — логический компонент доступа к данным EnvironmentRepository:

оригинал
Инъекция зависимостей
Здесь необходимо остановиться и разобраться с зависимостями компонентов:
  • сетевому контроллеру потребуется экземпляр сервиса переменных,
  • сервис должен быть единственным для всего приложения,
  • для создания сервиса необходимо предварительно создать экземпляр репозитория переменных.

Эти задачи решим с помощью библиотеки GetIt. Подключим необходимый пакет в pubspec.yaml:

Создадим экземпляр контейнера инжектора lib/di/di_container.dart и напишем метод с регистрацией репозитория и сервиса:

оригинал
Метод инициализации контейнера DI вызовем в методе подготовки приложения:

оригинал
Cетевой слой
lib/controller/ActuatorController — сетевой http компонент. Он содержит методы доступа к служебным данным приложения:

оригинал
Задекларируем обработчики маршрутов для контроллеров в lib/controller/Routes:

оригинал
Первый запуск
Для запуска необходимо:
  • приложение упаковать в Docker образ,
  • добавить контейнер в сценарий docker-compose,
  • настроить NGINX для проксирования запросов.

В папке приложения создадим Dockerfile. Это скрипт сборки и запуска образа для Docker:

оригинал
Добавим контейнер приложения в сценарий docker-compose.yaml:

оригинал
Создадим файл data_app.env с переменными конфигурации для приложения:

оригинал
Добавим новый location в отладочный конфиг NGINX conf.dev.d/default.conf:

оригинал
Запускаем отладочный сценарий с флагом предварительной сборки образов:
docker-compose -f docker-compose.yaml -f docker-compose.dev.yaml up --build


Сценарий успешно запустился, но настораживают несколько моментов:
  • официальный образ со средой dart от google занимает 290MБ в виде архива. В распакованном виде он займет кратно больше места — 754МБ. Посмотреть список образов и их размер:
    docker images

  • Время сборки и JIT-компиляции составило 100+ сек. Многовато для запуска приложения на проде
  • Потребление памяти в docker dashboard 300 МБ сразу после запуска
  • В нагрузочном тесте (только сетевые запросы GET /api/actuator/) потребление памяти находится в диапазоне 350—390 МБ для приложения, запущенного в одном изоляте

Предположительно ресурсов нашего бюджетного VPS не хватит для работы такого ресурсоемкого приложения. Давайте проверим:
  • Создадим на сервере папку для новой версии приложения и скопируем содержимое проекта
    ssh root@dartservice.ru "mkdir -p /opt/srv_2" && scp -r ./* root@91.230.60.120:/opt/srv_2/
  • Теперь необходимо перенести в эту папку проект web-страницы из /opt/srv_1/public/ и все содержимое папки /opt/srv_1/sertbot/ (в ней находятся SSL сертификаты для NGINX и логи Let’s encrypt бота), также скопируем ключ из /opt/srv_1/dhparam/
  • Запустим в отдельной консоли монитор ресурсов сервера
    htop

  • Выполним docker-compose сценарий в папке /opt/srv_2/
    docker-compose up --build -d

  • Так выглядит сборка приложения перед запуском:
  • А так — в работе:
    Из доступного 1ГБ оперативной памяти наше приложение потребляет 1,5ГБ «заняв» недостающее в файле подкачки. Да, приложение запустилось, но ни о какой нагрузочной способности речь не идет.
  • Остановим сценарий
    docker-compose down

AOT
Нам предстоит решить три задачи:
  • снизить потребление оперативной памяти dart приложением,
  • уменьшить время запуска,
  • снизить размер docker-контейнера приложения.

Решением станет отказ от dart в рантайме. Начиная с версии 2.6, dart-приложения поддерживают компиляцию в нативный исполняемый код. Aqueduct поддерживает компиляцию начиная с версии 4.0.0-b1.
Начнем с локального удаления aqueduct CLI
pub global deactivate aqueduct

Установим новую версию
pub global activate aqueduct 4.0.0-b1

Поднимем зависимости в pubspec.yaml:

Соберем нативное приложение
aqueduct build

Результатом будет однофайловая сборка data_app.aot размером около 6 МБ. Можно сразу запустить это приложение с параметрами, например,
data_app.aot --port 8080 --isolates 2
Потребление памяти сразу после запуска — менее 10 МБ.
Посмотрим под нагрузкой. Параметры теста: сетевые запросы GET /actuator, 100 потоков с максимальной доступной скоростью, 10 минут. Результат:

Итого: средняя скорость — 13к запросов в сек для тела ответа JSON 1,4кВ, среднее время ответа — 7 мсек, потребление оперативной памяти (на два инстанса) 42 MB. Ошибок нет.
При повторном тесте с шестью инстансами приложения средняя скорость, конечно, повышается до 19к/сек, но и утилизация процессора достигает 45% при потреблении памяти 64 МБ.
Это превосходный результат.
Упаковка в контейнер
Здесь мы столкнемся еще с одной сложностью: скомпилировать dart-приложение в натив мы можем только под текущую ОС. В моем случае это Windows10 x64. В docker-контейнере я, конечно, предпочел бы один из дистрибутивов Linux — например, Ubuntu20_10.
Решением здесь станет промежуточный docker-стенд, используемый только для сборки нативных приложений под Ubuntu. Напишем его /dart2native/Dockerfile:

оригинал
Теперь соберем его в docker-образ с именем aqueduct_builder:4.0.0-b1, перезаписав, если есть, старые версии:
docker build --pull --rm -f "dart2native\Dockerfile" -t aqueduct_builder:4.0.0-b1 "dart2native"

Проверим
docker images


Напишем сценарий сборки нативного приложения docker-compose.dev.build.yaml:

оригинал
Запустим сценарий сборки
docker-compose -f docker-compose.dev.build.yaml up


Файл скомпилированного под Ubuntu приложения data_app.aot занимает уже 9 МБ. При запуске утилизирует 19 МБ оперативной памяти (для двух инстансов). Проведем локальное нагрузочное тестирование с теми же условиями, но в контейнере с проксированием NGINX (GET, 100 потоков):

В среднем 5,3к запросов в секунду. При этом потребление оперативной памяти не превысило 55 МБ. Размер образа уменьшился по сравнению с установленным dart и aqueduct c 840 МБ до 74 МБ на диске.
Напишем новый сценарий docker-compose.aot.yaml запуска приложения. Для этого заменим блок описания data_app, установив базовым образ “пустой” Ubuntu:20_10. Смонтируем файл сборки и изменим команду запуска:

оригинал
Решим еще одну сервисную задачу: фактически сборочный docker-образ c установленными dart и aqueduct — вполне себе переиспользуемый инструмент. Имеет смысл выгрузить его в общедоступный регистр и подключать как готовый скачиваемый образ. Для этого необходимо:
  • зарегистрироваться в публичном регистре, например, DockerHub,
  • авторизоваться локально с тем же логином
    docker login
  • переименовать выгружаемый образ по схеме login/title:tag
    docker image tag a365ac7f5bbb andx2/aqueduct:4.0.0-b1
  • выгрузить образ в регистр
    docker push andx2/aqueduct:4.0.0-b1

    https://hub.docker.com/repository/docker/andx2/aqueduct/general

Теперь можно изменить наш сценарий сборки для использования публичного образа
Подключение базы данных
В aqueduct уже встроен ORM для работы с базой данных PostgreSQL. Для его использования необходимо:
  • Создать доменные объекты, описывающие записи в базе данных.
  • На основе доменных объектов и связей между ними сгенерировать файл миграции. Заметка: для записи в базу данных необходимо, чтобы в БД были подготовлены таблицы, чьи схемы подходят для хранения доменных объектов. Aqueduct предоставляет инструмент миграции, который фактически обходит все классы проекта, являющиеся расширением ManagedObject (управляемые объекты), прочитывает типы их полей и связей с другими управляемыми объектами и создает специальный файл, в котором описано изменение схемы таблиц и связей в базе данных по сравнению со схемой предыдущего файла миграции. Файлы миграции добавляются при каждой перегенерации схемы.
  • Применить файлы миграции к базе данных. Применение происходит последовательно, начиная с версии, которая записана в БД текущей.
  • В файлы миграции, сгенерированные aqueduct, можно вносить свои изменения, например определить реализацию метода seed() — для добавления в БД каких-либо начальных данных.
  • Генерация и применение миграций производится aqueduct CLI.

Начнем с подключения нового docker-контейнера с БД PostgreSQL в сценарии docker-compose.aot.yaml. Готовый образ на основе Linux Alpine («компактная» версия Linux для встраиваемых применений):

оригинал
Здесь необходимо обратить внимание на файл переменных окружения data_db.env. Дело в том, что образ заранее настроен на использование этих переменных в качестве имени пользователя, хоста, порта и пароля доступа. Добавим эти переменные в файл:

оригинал
Значения приведены условно.
Также смонтируем папку хоста ./data_db/ в контейнер для хранения данных БД.
Далее в приложении data_app добавим класс /service/DbHelper для подключения к базе данных, используя переменные окружения:

оригинал
Создадим доменный объект, управляемый ORM, для получения настроек клиентского приложения:

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

оригинал

оригинал
Сетевой контроллер:

оригинал
Зарегистрируем новые компоненты в DI контейнере:

оригинал
Добавим новый контроллер и эндпойнт в маршрутизатор:

оригинал
Теперь сгенерируем файл миграции базы данных. Выполним
aqueduct db generate
Результатом будет создание файлов миграции в папке проекта:


оригинал
Теперь нужно решить сервисную задачу: миграции нужно применять из системы с установленным aqueduct (и dart) к базе данных, работающей в контейнере, и это нужно выполнять как при локальной разработке, так и на сервере. Используем для этого кейса ранее собранный и опубликованный образ для AOT-сборки. Напишем соответствующий docker-compose сценарий миграции БД:

оригинал
Интересная деталь — строка подключения к БД. При запуске сценария можно передать в качестве аргумента файл с переменными окружения, а затем использовать эти переменные для подстановки в сценарии:
docker-compose -f docker-compose.migrations.yaml --env-file=./data_app.env --compatibility up --abort-on-container-exit

Также обратим внимание на флаги запуска:
  • --compatibility — совместимость с версиями docker-compose сценариев 2.х. Это позволит использовать параметры deploy для ограничения использования ресурсов контейнером, которые игнорируются в версиях 3.х. Мы ограничили потребление оперативной памяти до 200МБ и использование процессорного времени до 50%
  • --abort-on-container-exit — этот флаг устанавливает режим выполнения сценария таким образом, что при остановке одного из контейнеров сценария будут завершены все остальные. Поэтому, когда выполнится команда миграции схемы базы данных и контейнер с aqueduct остановится, docker-compose завершит также и работу контейнера базы данных

Публикация
Для подготовки к публикации приложения необходимо:
  • Изменить переменные окружения в data_app.env и data_db.env. Напомню, что сейчас у нас POSTGRES_PASSWORD=postgres_password
  • Переименовать сценарий запуска нативного приложения docker-compose.aot.yaml в docker-compose.yaml. Команда запуска приложения на сервере не должна иметь аргументов
  • Временно заблокировать маршрут просмотра переменных окружения запущенного приложения /api/actuator. В следующей статье мы реализуем механизм авторизации по ролям и откроем доступ к этому маршруту только для администратора.

Скопируем на сервер папку приложения ./data_app/. Важным моментом здесь будет ключ -p (копировать с сохранением атрибутов файлов) в команде копирования. Напомню, что при сборке нативного приложения мы установили права на исполнение файлу data_app.aot:
scp -rp ./data_app root@dartservice.ru:/opt/srv_1

Скопируем также:
  • Измененную конфигурацию NGINX ./conf.d/default.conf
  • Сценарии запуска и миграции docker-compose.yaml, docker-compose.migrations.yaml
  • Файлы с переменными окружения data_app.env и data_db.env

Добавим на сервере папку /opt/srv_1/data_db. Это том файловой системы хоста для монтирования в контейнер базы данных. Здесь будут сохраняться все данные PostgreSQL.
mkdir /opt/srv_2/data_db

Выполним сценарий миграции схемы базы данных:
docker-compose -f docker-compose.migrations.yaml --env-file=./data_app.env up --abort-on-container-exit

Запустим сценарий приложения
docker-compose up -d

Исходный код github.
Вместо заключения
Каркас для бэкенд приложений готов. В следующей статье на его основе мы напишем новое приложение для авторизации пользователей сервиса. Для этого мы воспользуемся спецификацией oAuth2 и интегрируемся с VK и Github.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_programmirovanie (Программирование), #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_dart, #_flutter, #_dart, #_flutter, #_docker, #_nginx, #_surf, #_fullstack_development, #_blog_kompanii_surf (
Блог компании Surf
)
, #_programmirovanie (
Программирование
)
, #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
)
, #_dart, #_flutter
Профиль  ЛС 
Показать сообщения:     

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

Текущее время: 22-Ноя 15:00
Часовой пояс: UTC + 5