Оглавление
- 1. Введение
- 2. Backend
- 2.1. Инфраструктура.
- 2.2. Доменное имя. SSL
- 2.3. Каркас серверного приложения (мы находимся здесь)
- ...
- 3. Web
- 3.1. Flutter web-страница
- ...
- 4. Mobile
- ...
Подготовка
В прошлый раз мы закончили на том, что разместили статическую веб страницу-заглушку, разработанную с использованием 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
Вместо заключения
Каркас для бэкенд приложений готов. В следующей статье на его основе мы напишем новое приложение для авторизации пользователей сервиса. Для этого мы воспользуемся спецификацией oAuth2 и интегрируемся с VK и Github.
Mitai
Спасибо, было интересно!
пысы: не осилил туториал aqueduct отказывается делать db generate, как оказалось ему нужна версия дарта не выше 2.8.3 и чет интерес к нему погас, ведь есть же Shelf!
20912 Автор
А версия aqueduct какая? Может на beta канале попробовать?
Mitai
4.0.0-b1
Brandan 11 hours ago
This is actually an issue with Aqueduct itself when using the beta version. Newer versions of Dart break it, so make sure you are on Dart SDK 2.8.3 as that’s the last working version of using the beta version of Aqueduct.