Dart Service: Server Application Framework

Table of contents




Training



The last time we ended on that placed a static Web page dummy, developed using Flutter for the web. The page displays the progress of the development of our service, but the data on the dates of the start of development and release had to be hardcoded in the application. Thus, we lost the ability to change the information on the page. It's time to develop a data server application. A diagram of all service applications is in the article "Service in Dart Language: Introduction, Backend Infrastructure" .



In this article, we will write an application using the Aqueduct framework, evaluate its performance and resource consumption in different modes, write a toolkit for compilation into a native application for Windows and Linux, deal with migrations of the database schema for domain application classes, and even publish our tool docker image to the public DockerHub register.







Usefulness




Installing Aqueduct



Let's start by installing dart-sdk, a Dart development kit. You can install it using your operating system's package manager as suggested here . However, in the case of Windows, no package manager is installed on your system by default. So just:



  • Download the archive and unpack it to the C drive:
  • , , , . . Β« Β»



  • Path . dart , , C:\dart-sdk\bin
  • , dart pub ( dart)



    dart --version






    pub -v




  • , ,
  • aqueduct CLI (command line interface)



    pub global activate aqueduct






    aqueduct




It is theoretically possible to install a PostgreSQL database server locally as well . However, Docker will allow us to avoid this need and make the development environment similar to the runtime on the server.



Application generation



So, let's open our server folder in VsCode



code c:/docs/dart_server


For those who have not seen the first and second articles, the source code can be cloned from the guthub repository :



git clone https://github.com/AndX2/dart_server.git
Let's create an application template:



aqueduct create data_app






Let's get acquainted with the contents of the project template:



  • README.md - a note describing how to work with an aqueduct project, run tests, generate API documentation, etc. Support file.
  • 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.


Configuration



The first task to be solved is setting up the application at startup. Aqueduct has a built-in mechanism for extracting parameters from configuration files, but this mechanism is not very convenient when running in a Docker container. We will act differently:



  • Let's pass the list of variables to the container operating system.
  • When launching the application inside the container, we read the operating system environment variables and use them for initial configuration.
  • Let's create a route for viewing all the environment variables of the running application over the network (this will be useful when viewing the application state from the admin panel).


In the / lib folder, create several folders and the first repository for accessing environment variables:







EnvironmentRepository in the constructor reads environment variables from the operating system as a Map <String, String> dictionary and saves them in the private variable _env . Let's add a method to get all parameters in the form of a dictionary: lib / service / EnvironmentService - the logical component for accessing EnvironmentRepository data:















Dependency injection



Here you need to stop and deal with component dependencies:



  • the network controller will need an instance of the variable service,
  • the service must be unique for the entire application,
  • to create a service, you must first create an instance of the variable repository.


We will solve these problems using the GetIt library . Connect the required package to pubspec.yaml :







Create an instance of the injector container lib / di / di_container.dart and write a method with registration of the repository and service: Call the DI container initialization method in the application preparation method:















Network layer



lib / controller / ActuatorController - http networking component. It contains methods for accessing the service data of the application: Let's declare route handlers for controllers in lib / controller / Routes :















First start



To run you need:



  • package the application into a Docker image,
  • add container to docker-compose script,
  • configure NGINX to proxy requests.


Create a Dockerfile in the application folder . This is the script for building and running the image for Docker: Add the application container to the docker-compose.yaml script : Create the data_app.env file with configuration variables for the application: Add a new location to the NGINX debug config conf.dev.d / default.conf : Run the debug script with the pre-build flag:



































docker-compose -f docker-compose.yaml -f docker-compose.dev.yaml up --build






The script ran successfully, but there are several alarming points:



  • the official dart image from google is 290MB archived . When unpacked, it will take up much more space - 754MB. View a list of images and their sizes:



    docker images
  • The build and JIT compilation time was 100+ seconds. Too much to run an app on sale
  • Memory consumption in docker dashboard 300 MB immediately after launch



  • In a load test (network GET / api / actuator / requests only) memory consumption is in the range of 350-390 MB for an application running in one isolate





Presumably, the resources of our budget VPS will not be enough to run such a resource-intensive application. Let's check:



  • Create a folder on the server for the new version of the application and copy the contents of the project



    ssh root@dartservice.ru "mkdir -p /opt/srv_2" && scp -r ./* root@91.230.60.120:/opt/srv_2/
  • Now you need to transfer to this folder the web page project from / opt / srv_1 / public / and the entire contents of the / opt / srv_1 / sertbot / folder (it contains SSL certificates for NGINX and Let's encrypt bot logs), and copy the key from / opt / srv_1 / dhparam /
  • Launch the server resource monitor in a separate console



    htop


  • Let's run the docker-compose script in the / opt / srv_2 / folder



    docker-compose up --build -d
  • This is how the application assembly looks before launch:





  • And so - in work:







    Of the available 1GB of RAM, our application consumes 1.5GB "occupying" the missing paging file. Yes, the application has started, but we are not talking about any load capacity.
  • Let's stop the script:



    docker-compose down


AOT



We have three tasks to solve:



  • reduce the consumption of RAM by dart application,
  • reduce startup time,
  • reduce the size of the docker container of the application.


The solution will be to abandon dart at runtime. Since version 2.6, dart applications support compilation into native executable code . Aqueduct supports compilation starting from version 4.0.0-b1.



Let's start by removing the aqueduct CLI locally:



pub global deactivate aqueduct


Install the new version:



pub global activate aqueduct 4.0.0-b1


Let's raise the dependencies in pubspec.yaml: Let's build the







native application:



aqueduct build


The result will be a single file data_app.aot assembly of about 6MB in size. You can immediately launch this application with parameters, for example:



data_app.aot --port 8080 --isolates 2


Memory consumption immediately after launch is less than 10 MB.



Let's see under load. Test parameters: network GET / actuator requests, 100 threads with maximum available speed, 10 minutes. Result:







Total: average speed - 13k requests per second for a 1.4kV JSON response body, average response time - 7 ms, memory consumption (for two instances) 42 MB. There are no errors.



When we repeat the test with six instances of the application, the average speed, of course, rises to 19k / s, but the utilization of the processor reaches 45% when the memory consumption is 64 MB.

This is an excellent result.



Container packing



Here we will face one more difficulty: we can only compile a dart application into a native for the current OS. In my case, this is Windows10 x64. In a docker container, I would of course prefer one of the Linux distributions - for example Ubuntu 20.10.



The solution here will be an intermediate docker-stand, used only for building native applications for Ubuntu. Let's write it / dart2native / Dockerfile : Now let's build it into a docker image named aqueduct_builder: 4.0.0-b1 , overwriting the old versions, if any:











docker build --pull --rm -f "dart2native\Dockerfile" -t aqueduct_builder:4.0.0-b1 "dart2native"


Let's check:



docker images






Let's write a build script for the native application docker-compose.dev.build.yaml : Run the build script:











docker-compose -f docker-compose.dev.build.yaml up






The file data_app.aot compiled for Ubuntu already takes 9 MB. At startup, utilizes 19 MB of RAM (for two instances). Let's conduct local load testing with the same conditions, but in a container with NGINX proxying (GET, 100 threads):







On average, 5.3k requests per second. At the same time, the consumption of RAM did not exceed 55 MB. The image size has decreased compared to the installed dart and aqueduct from 840 MB to 74 MB on disk.



Let's write a new script docker-compose.aot.yaml to launch the application. To do this, we will replace the data_app description block by installing the base image β€œempty” Ubuntu: 20.10. Let's mount the assembly file and change the launch command:







Let's solve one more service problem: in fact, the docker build image with dart and aqueduct installed is quite a reusable tool. It makes sense to upload it to a public register and connect it as a ready-made downloadable image. This requires:



  • register with a public register like DockerHub ,
  • log in locally with the same login



    docker login 
  • rename the uploaded image using the login / title: tag scheme



    docker image tag a365ac7f5bbb andx2/aqueduct:4.0.0-b1
  • unload image into register



    docker push andx2/aqueduct:4.0.0-b1


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



Now we can modify our build script to use the public image







Database connection



Aqueduct already has a built-in ORM for working with a PostgreSQL database. To use it you need:



  • Create domain objects that describe records in the database.
  • . : , , . Aqueduct , , ManagedObject ( ), , . .
  • . , , .
  • , aqueduct, , seed() β€” - .
  • aqueduct CLI.


Let's start by connecting a new docker container to the PostgreSQL database in the docker-compose.aot.yaml script. Ready image based on Linux Alpine ("compact" version of Linux for embedded applications): Here you need to pay attention to the environment variable file data_db.env . The fact is that the image is pre-configured to use these variables as username, host, port, and access password. Let's add these variables to the file: The values ​​are given conditionally. We will also mount the host folder ./data_db/ into a container for storing database data. Next, in the data_app application, add the / service / DbHelper class to connect to the database using environment variables:





























Let's create a domain object managed by ORM to get the settings of the client application: Add a repository and a service to add settings and get the current version: Network controller: Register new components in the DI container: Add a new controller and endpoint to the router: Now we will generate a database migration file. Let's execute:















































aqueduct db generate
The result will be the creation of migration files in the project folder: Now you need to solve the service problem: migrations must be applied from a system with aqueduct (and dart) installed to a database running in a container, and this must be done both during local development and on the server. For this case, we will use the previously built and published image for the AOT assembly. Let's write the corresponding docker-compose database migration script: An interesting detail is the database connection string. When running the script, you can pass a file with environment variables as an argument, and then use these variables for substitution in the script:























docker-compose -f docker-compose.migrations.yaml --env-file=./data_app.env --compatibility up --abort-on-container-exit


Let's also pay attention to the launch flags:



  • --compatibility - compatibility with docker-compose versions of 2.x scripts. This will allow deploy options to be used to restrict resource usage by the container, which are ignored in versions 3.x. We've limited RAM consumption to 200MB and CPU usage to 50%
  • --abort-on-container-exit - This flag sets the script execution mode so that when one of the script containers stops, all the others will be terminated. Therefore, when the command to migrate the database schema is executed and the container with aqueduct stops, docker-compose will also terminate the database container.


Publication



To prepare for publishing the application, you must:



  • data_app.env data_db.env. , POSTGRES_PASSWORD=postgres_password
  • docker-compose.aot.yaml docker-compose.yaml.
  • /api/actuator. .


Copy the application folder ./data_app/ to the server . An important point here will be the -p switch (copy while preserving file attributes) in the copy command. Let me remind you that when building the native application, we set the execution rights to the data_app.aot file :



scp -rp ./data_app root@dartservice.ru:/opt/srv_1


Let's also copy:



  • Changed NGINX configuration ./conf.d/default.conf
  • Startup and migration scripts docker-compose.yaml , docker-compose.migrations.yaml
  • Files with environment variables data_app.env and data_db.env


Add the / opt / srv_1 / data_db folder on the server . This is the host filesystem volume to be mounted in the database container. All PostgreSQL data will be saved here.



mkdir /opt/srv_2/data_db


Let's execute the script for migrating the database schema:



docker-compose -f docker-compose.migrations.yaml --env-file=./data_app.env up --abort-on-container-exit


Let's run the application script:



docker-compose up -d


-> Source code github



Instead of a conclusion



The framework for the backend applications is ready. In the next article, based on it, we will write a new application for authorizing service users. To do this, we will use the oAuth2 specification and integrate with VK and Github.



All Articles