3

I am trying to learn rust atm hence I've created simple todo web app with actix and mongodb and deployed to linux server (ubuntu 18.04) via docker. But I realized, even while no connection/request (ie. right after container start), cpu usage stays at %5-6.

Why this is happening? Do you have any idea?

cpu usage htop

cpu usage portainer

src/main.rs


#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let logger = slog::Logger::root(
        Mutex::new(slog_json::Json::default(std::io::stderr())).map(slog::Fuse),
        o!("version" => env!("CARGO_PKG_VERSION"))
    );

    let settings = Settings::new(&logger).unwrap();
    let server_url = format!("0.0.0.0:{}",settings.server.port);
    info!(logger, "Listening on: {}", server_url);

    let http_context = HttpContext{
        logger,
        repository : Repository::new(&settings).await.unwrap()
    };
    HttpServer::new(move || {
        App::new()
            .wrap(
                StructuredLogger::new(http_context.logger.new(o!("log_type" => "access")))
            )
            .data(web::JsonConfig::default().limit(4096))
            .data(http_context.clone())// <- limit size of the payload (global configuration)
            .service(web::resource("/").route(web::get().to(index)))
            .service(web::resource("/todo/{id}").route(web::get().to(get)))
            .service(web::resource("/todo").route(web::post().to(save)))
    }).bind(server_url)?.run().await
}

src/repository.rs

use mongodb::{Client, options::ClientOptions, error::Error, Collection};
use bson::{doc};
use crate::model::Todo;
use crate::settings::Settings;

#[derive(Clone)]
pub struct Repository {
   collection : Collection,
}


impl Repository {
    pub async fn new(settings : &Settings) ->  Result<Self, Error> {
        let mut client_options = ClientOptions::parse(&settings.database.url).await?;
        client_options.app_name = Some("todo-rs".to_string());
        let client = Client::with_options(client_options)?;
        let db = client.database(&settings.database.db_name);
        let collection = db.collection(&settings.database.collection_name);

        Ok(Repository {
            collection
        })
    }

    pub async fn insert(&self, todo: Todo) ->  Result<bool, Error> {
        let serialized_todo = bson::to_bson(&todo)?;
        let document = serialized_todo.as_document().unwrap();


        let insert_result = self.collection.insert_one(document.to_owned(), None).await?;
        let inserted_id = insert_result
            .inserted_id
            .as_object_id().expect("Retrieved _id should have been of type ObjectId");

        dbg!(inserted_id);
        Ok(true)
    }

    pub async fn get(&self, id : &str) ->  Result<Option<Todo>, Error> {
        let result = self.collection.find_one(doc! {"id": id}, None).await?;
        match result {
            Some(doc) => {
                Ok(Some(bson::from_bson(bson::Bson::Document(doc)).unwrap()))
            }
            None => {
                Ok(None)
            }
        }
    }

}

src/handler.rs

use actix_web::{web,  HttpResponse};
use chrono::prelude::*;
use crate::dto::{SaveTodoRequest, IndexResponse, SaveTodoResponse};
use crate::model::Todo;
use uuid::Uuid;
use crate::repository::Repository;

#[derive(Clone)]
pub struct HttpContext {
    pub logger : slog::Logger,
    pub repository: Repository,
}


pub async fn index() -> HttpResponse {
    let my_obj = IndexResponse {
        message: "Hello, World!".to_owned(),
    };
    HttpResponse::Ok().json(my_obj) // <- send response
}

pub async fn save(context: web::Data<HttpContext>, resource: web::Json<SaveTodoRequest>) -> HttpResponse {
    let todo_request = resource.into_inner();
    let todo = Todo {
        added_at: Utc::now(),
        name: todo_request.name,
        done: false,
        tags: todo_request.tags,
        id: Uuid::new_v4().to_string(),
    };

    let logger = context.logger.clone();
    let result = context.repository.insert(todo).await;
    match result {
        Ok(result) => {
            info!(logger, "Saved");
            HttpResponse::Ok().json(SaveTodoResponse{success:result})
        },
        Err(e) => {
            error!(logger,"Error while saving, {:?}", e);
            HttpResponse::InternalServerError().finish()
        }
    }
}

pub async fn get(context: web::Data<HttpContext>, id: web::Path<String>) -> HttpResponse {
    let logger = context.logger.clone();
    let result = context.repository.get(&id.into_inner()).await;
    match result {
        Ok(result) => {
            match result {
                Some(r) => HttpResponse::Ok().json(r),
                None => HttpResponse::NoContent().finish(),
            }
        },
        Err(e) => {
            error!(logger,"Error while saving, {:?}", e);
            HttpResponse::InternalServerError().finish()
        }
    }
}

dockerfile

FROM rust:latest as builder
WORKDIR /usr/src/app
COPY . .
RUN cargo build --release


FROM debian:buster-slim
RUN apt-get update && apt-get install -y ca-certificates tzdata
ENV TZ=Etc/UTC
WORKDIR /usr/src/app
COPY --from=builder /usr/src/app/config config
COPY --from=builder /usr/src/app/target/release/todo-rs .

EXPOSE 80
CMD ["./todo-rs"]
Blithe
  • 105
  • 1
  • 9
  • 1
    Can you try making a reproducable, self-contained testcase with as little code as possible? The most simple actix web server setup (from the example [here](https://github.com/actix/actix-web)) does not exhibit this behavior. – HHK Jan 26 '21 at 19:58
  • 1
    Btw, I think its not related with actix. I've deleted mongodb related stuff and deployed. Cpu usage back to normal. – Blithe Jan 27 '21 at 05:44
  • I have previously made a simple "hello world" actixwweb servers and put it into container like you have, I did not see this kind of behavior at all, so I don't think it is related to actix web or having actix web in a container. Have you tried a different container? Maybe alpine? – PhilipK Jan 27 '21 at 20:00
  • @Blithe Did you find a way to solve this? Im facing the same problem. – Fer Nando Feb 02 '21 at 00:31
  • @FerNando no actually. I've removed collection from repository struct and create collection reference for every request in repository methods. I don't think its is best practice. But at least, I fixed cpu usage problem. https://pastecode.io/s/SYuUL1G9U9 – Blithe Feb 02 '21 at 10:05

1 Answers1

0

I was having this same issue using actix, later I switched to warp but I think the problem is the same.

NEW

You can wrap your mongo connection with Arc<Database> so with that when you copy the db to get other, you copy the reference and not the entire connector, that improves performance a lot.

At this moment (2021/2/6), mongodb crate is using an old version of reqwest and you might have tokio version problems(between 0.2 and 1.0) . You can fix that using the last version of the main branch, putting this on your dependencies:

mongodb = { git = "https://github.com/mongodb/mongo-rust-driver/" }

OLD VERSION

I supposed that every clone of mongo-driver consumes cpu. So what you need to do is to have a single point in your program that communicates with the db. You can achieve that using tokio channels.

This is what I have:

use tokio::sync::mpsc;
...
let (tx, rx) = mpsc::channel::<DBCommand>(32);
tokio::spawn( async move{
    db_channel(rx).await;        
});
let routes = filters::filters(tx);
warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;

So in warp you should pass as the AppState your transmitter, so you can send messages to the db channel like this:

tx.send(/* channel struct /*).await.unwrap();

This guide was enough for me https://tokio.rs/tokio/tutorial/channels. Also, you will need to pass a oneshot transmitter to the db channel to recover the response from the db.

Fer Nando
  • 81
  • 1
  • 5