Как стать автором
Обновить
70.14
Surf
Создаём веб- и мобильные приложения

Сервис на языке Dart: каркас серверного приложения

Время на прочтение 11 мин
Количество просмотров 9.5K
Оглавление


Подготовка


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

В этой статье мы напишем приложение с использованием фреймворка Aqueduct, оценим его производительность и потребление ресурсов в разных режимах, напишем инструментарий для компиляции в нативное приложение для Windows и Linux, разберемся с миграциями схемы базы данных для доменных классов приложения и даже опубликуем наш инструментальный docker образ в публичный регистр DockerHub.



Полезности
  • все скриншоты с кодом «кликабельные»
  • полный код здесь
  • консольные команды собраны в 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 — например, Ubuntu 20.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

Вместо заключения


Каркас для бэкенд приложений готов. В следующей статье на его основе мы разработаем приложение на Flutter для авторизации пользователей сервиса. Для этого мы воспользуемся спецификацией oAuth2 и интегрируемся с VK и Github.

Больше полезного про Flutter — в телеграм-канале Surf Flutter Team. Публикуем кейсы, лучшие практики, новости и вакансии Surf, а также проводим прямые эфиры. Присоединяйтесь!
Теги:
Хабы:
+7
Комментарии 3
Комментарии Комментарии 3

Публикации

Информация

Сайт
surf.ru
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия