Microservices mit Rust
Jens Siebert (@jens_siebert)
betterCode(Rust), 13. Oktober 2021
Über mich
• Senior Software Developer bei
doks.innovation in Kassel
• Drohnen-Steuerung, Computer
Vision, Architektur
• Maker, 3D-Drucker, Nerd
Quo vadis, Backend-Entwicklung?
Interpretierte Sprachen:
• Java/JVM-basierte Sprachen: Spring, Micronaut, MicroProfile
• Javascript/Typescript: Node.js, Deno
• Python: Django, Flask
Kompilierte Sprachen:
• Go: Kite, go-kit, go-micro
• Rust: actix-web, rocket, tide, warp
Quo vadis, Backend-Entwicklung?
Interpretierte Sprachen:
• Einfach zu lernen
• Sicherheit durch automatische Speicherverwaltung
• Garbage Collection
• Langsamer Start-up/Just-in-Time Kompilierung
Go:
• Einfach zu lernen
• Sicherheit durch automatische Speicherverwaltung
• Garbage Collection
• Schneller Start-up/Ahead-of-Time Kompilierung
Und Rust?
Rust:
• Nicht ganz so einfach zu lernen
• Sicherheit durch automatische Speicherverwaltung
• Garbage Collection
• Schneller Start-up/Ahead-of-Time Kompilierung
Rust vs. Go? Rust AND Go!
For most companies and users, Go is the right default option. Its performance is strong, Go is
easy to adopt, and Go’s highly modular nature makes it particularly good for situations
where requirements are changing or evolving.
As your product matures, and requirements stabilize, there may be opportunities to have
large wins from marginal increases in performance. In these cases, using Rust to maximize
performance may well be worth the initial investment.
https://thenewstack.io/rust-vs-go-why-theyre-better-together
Discord „Read States“ Service
https://discord.com/blog/why-discord-is-switching-from-go-to-rust
Discord „Read States“ Service
https://discord.com/blog/why-discord-is-switching-from-go-to-rust
Discord „Read States“ Service
https://discord.com/blog/why-discord-is-switching-from-go-to-rust
Web Frameworks für Rust
• actix-web (https://actix.rs)
• rocket (https://rocket.rs)
• tide (https://github.com/http-rs/tide)
• warp (https://github.com/seanmonstar/warp)
Auswahlhilfe: https://www.lpalmieri.com/posts/2020-07-04-choosing-a-rust-web-framework-2020-edition/
actix-web Architektur
actix-web
Client
tokio
Operating System
High Performance Asynchronous IO
actix-web Features
• Vollständig asynchron
• HTTP/1.x und HTTP/2
• Request Routing
• Middlewares (Logger, Session, CORS, etc.)
• Transparente (De-)Kompression
• WebSockets
• Streams
• Unterstützung für SSL/TLS
• Unterstützung für Keep-Alive und Slow Requests
• Statische Assets
Projekt Setup
1. cargo new actix-hello-world
2.
Hello World!
use actix_web::{web, App, HttpRequest, HttpServer, Responder};
async fn greet(req: HttpRequest) -> impl Responder {
let name = req.match_info().get("name").unwrap_or("World");
format!("Hello {}!", &name)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/", web::get().to(greet))
.route("/{name}", web::get().to(greet))
})
.bind("127.0.0.1:8000")?
.run()
.await
}
Main-Funktion
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/", web::get().to(greet))
.route("/{name}", web::get().to(greet))
})
.bind("127.0.0.1:8000")?
.run()
.await
}
Server-Initialisierung
App-Initialisierung
Route-Mapping
Adress-Bindung
Entry-Point
Handler-Funktion
// Impliziter HttpResponse
async fn greet(req: HttpRequest) -> impl Responder {
let name = req.match_info().get("name").unwrap_or("World");
format!("Hello {}!", &name)
}
// Expliziter HttpResponse
async fn greet(req: HttpRequest) -> Result<HttpResponse, Error> {
let name = req.match_info().get("name").unwrap_or("World");
let body = format!("Hello {}!", &name);
Ok(HttpResponse::Ok().body(body))
}
Extractors -- Path
#[get("/{name}")]
async fn greet(web::Path(name): web::Path<String>) -> impl Responder {
let value = if name.is_empty() {
String::from("World!")
}
else {
name
};
format!("Hello {}!", &value)
}
Extractors -- Query
#[derive(Deserialize)]
struct Info {
name: String,
}
#[get("/")]
async fn greet(info: web::Query<Info>) -> impl Responder {
format!("Welcome {}!", info.name)
}
curl "http://localhost:8000?name=betterCode(Rust)"
Extractors -- JSON
#[derive(Deserialize)]
struct Info {
name: String,
}
#[get("/")]
async fn greet(info: web::Json<Info>) -> impl Responder {
format!("Welcome {}!", info.name)
}
curl -X GET
-H "Content-type: application/json“
-H "Accept: application/json"
-d ‘{"name":"betterCode(Rust)"}‘
http://localhost:8000/"
Extractors – Form Data
#[derive(Deserialize)]
struct Info {
name: String,
}
#[post("/")]
async fn greet(info: web::Form<Info>) -> impl Responder {
format!("Welcome {}!", info.name)
}
curl -X POST
-H "Content-type: application/x-www-form-urlencoded"
-d "name=betterCode(Rust)"
http://localhost:8000/"
Middleware
use actix_web::{web, middleware, App, HttpRequest, HttpServer, Responder};
[…]
#[actix_web::main]
async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "actix_web=info");
env_logger::init();
HttpServer::new(|| {
App::new()
.wrap(middleware::Logger::default())
.route("/", web::get().to(greet))
.route("/{name}", web::get().to(greet))
})
.bind("127.0.0.1:8000")?
.run()
.await
}
Authentifizierung
use actix_web::dev::ServiceRequest;
use actix_web_httpauth::middleware::HttpAuthentication;
use actix_web_httpauth::extractors::basic::BasicAuth;
use actix_web_httpauth::extractors::bearer::BearerAuth;
async fn basic_validation(req: ServiceRequest, _cred: BasicAuth) ->
Result<ServiceRequest, Error> {
// Validate credetials here...
Ok(req)
}
async fn bearer_validation(req: ServiceRequest, _cred: BearerAuth) ->
Result<ServiceRequest, Error> {
// Validate credetials here...
Ok(req)
}
Authentifizierung -- Middleware
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
let basic_auth = HttpAuthentication::basic(basic_validation);
let bearer_auth = HttpAuthentication::bearer(bearer_validation);
App::new()
.wrap(middleware::Logger::default())
.wrap(basic_auth) // oder .wrap(bearer_auth)
.route("/", web::get().to(greet))
.route("/{name}", web::get().to(greet))
})
.bind("127.0.0.1:8000")?
.run()
.await
}
Datenbankzugriff mit Diesel
• Object Relational Mapper und Query Builder für Rust
• Fokus auf möglichst schlanke Abstraktionen
• Hohe Performance
• Unterstützte Datenbanken:
• MySQL
• PostgreSQL
• SQLite
Datenbankzugriff mit Diesel -- Setup
cargo install diesel_cli --no-default-features --features sqlite
echo "DATABASE_URL=test.db" > .env
diesel setup
Datenbankzugriff mit Diesel -- Migrations
mkdir migrations
diesel migration generate users
diesel migration run
Datenbankzugriff mit Diesel -- Mapping
#[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable)]
pub struct User {
pub id: String,
pub name: String
}
Datenbankzugriff mit Diesel -- Verbindung
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let connspec = std::env::var("DATABASE_URL").expect("DATABASE_URL");
let manager = ConnectionManager::<SqliteConnection>::new(connspec);
let pool = r2d2::Pool::builder()
.build(manager)
.expect("Failed to create pool.");
HttpServer::new(move || {
App::new()
.data(pool.clone())
.service(get_user)
.service(add_user)
})
.bind("127.0.0.1:8000")?
.run()
.await
}
Datenbankzugriff mit Diesel -- Insert
pub fn insert_new_user(nm: &str, conn: &SqliteConnection,) ->
Result<User, diesel::result::Error> {
use crate::schema::users::dsl::*;
let new_user = User {
id: Uuid::new_v4().to_string(),
name: nm.to_owned()
};
diesel::insert_into(users).values(&new_user).execute(conn)?;
Ok(new_user)
}
Datenbankzugriff mit Diesel -- Insert-Handler
#[post("/user")]
async fn add_user(pool: web::Data<DbPool>, form: web::Json<NewUser>) ->
Result<HttpResponse, Error> {
let conn = pool.get().expect("couldn't get db connection from pool");
let user = web::block(move || insert_new_user(&form.name, &conn))
.await
.map_err(|e| {
eprintln!("{}", e);
HttpResponse::InternalServerError().finish()
})?;
Ok(HttpResponse::Ok().json(user))
}
Testen
#[cfg(test)]
mod tests {
use super::*;
use actix_web::test;
#[actix_rt::test]
async fn test_user_creation_ok() {
// DB connection and logging setup
let mut app = test::init_service(
// App initialization
)
.await;
let req = test::TestRequest::post()
.uri("/user")
.set_json(&NewUser {
name: "Test user".to_owned(),
}).to_request();
let resp: User = test::read_response_json(&mut app, req).await;
assert_eq!(resp.name, "Test user");
}
}
Continuous Integration
• Tests:
• cargo test
• Code Coverage:
• cargo install tarpaulin *
• cargo tarpaulin --ignore-tests
• Lint:
• rustup component add clippy
• cargo clippy -- -D warnings
• Formatting:
• rustup component add rustfmt
• cargo fmt -- --check
• Auditing:
• cargo install cargo-audit
• cargo audit
* = unterstützt zurzeit nur x86_64-linux
Fazit
Rust als Basis für Microservices bietet:
• Hohe Performance durch Ahead-of-Time Kompilierung, Zero Cost Abstractions
• Sicheres Speichermanagement bereits während der Kompilierung
• Sichere Nebenläufigkeit
• Keine Einbrüche bei der Performance durch Garbage Collection
• Komfortables Tooling, welches etablierten Sprachen und Frameworks in nichts nachsteht
• Freundliche und hilfsbereite Community
• Teilweise steile Lernkurve (aber es lohnt sich!)
Literatur
(01/2022) (12/2021)
Vielen Dank!
https://www.rust-lang.org/learn
https://actix.rs/docs
https://diesel.rs/guides
Twitter: @jens_siebert

Microservices mit Rust

  • 1.
    Microservices mit Rust JensSiebert (@jens_siebert) betterCode(Rust), 13. Oktober 2021
  • 2.
    Über mich • SeniorSoftware Developer bei doks.innovation in Kassel • Drohnen-Steuerung, Computer Vision, Architektur • Maker, 3D-Drucker, Nerd
  • 3.
    Quo vadis, Backend-Entwicklung? InterpretierteSprachen: • Java/JVM-basierte Sprachen: Spring, Micronaut, MicroProfile • Javascript/Typescript: Node.js, Deno • Python: Django, Flask Kompilierte Sprachen: • Go: Kite, go-kit, go-micro • Rust: actix-web, rocket, tide, warp
  • 4.
    Quo vadis, Backend-Entwicklung? InterpretierteSprachen: • Einfach zu lernen • Sicherheit durch automatische Speicherverwaltung • Garbage Collection • Langsamer Start-up/Just-in-Time Kompilierung Go: • Einfach zu lernen • Sicherheit durch automatische Speicherverwaltung • Garbage Collection • Schneller Start-up/Ahead-of-Time Kompilierung
  • 5.
    Und Rust? Rust: • Nichtganz so einfach zu lernen • Sicherheit durch automatische Speicherverwaltung • Garbage Collection • Schneller Start-up/Ahead-of-Time Kompilierung
  • 6.
    Rust vs. Go?Rust AND Go! For most companies and users, Go is the right default option. Its performance is strong, Go is easy to adopt, and Go’s highly modular nature makes it particularly good for situations where requirements are changing or evolving. As your product matures, and requirements stabilize, there may be opportunities to have large wins from marginal increases in performance. In these cases, using Rust to maximize performance may well be worth the initial investment. https://thenewstack.io/rust-vs-go-why-theyre-better-together
  • 7.
    Discord „Read States“Service https://discord.com/blog/why-discord-is-switching-from-go-to-rust
  • 8.
    Discord „Read States“Service https://discord.com/blog/why-discord-is-switching-from-go-to-rust
  • 9.
    Discord „Read States“Service https://discord.com/blog/why-discord-is-switching-from-go-to-rust
  • 10.
    Web Frameworks fürRust • actix-web (https://actix.rs) • rocket (https://rocket.rs) • tide (https://github.com/http-rs/tide) • warp (https://github.com/seanmonstar/warp) Auswahlhilfe: https://www.lpalmieri.com/posts/2020-07-04-choosing-a-rust-web-framework-2020-edition/
  • 11.
  • 12.
    actix-web Features • Vollständigasynchron • HTTP/1.x und HTTP/2 • Request Routing • Middlewares (Logger, Session, CORS, etc.) • Transparente (De-)Kompression • WebSockets • Streams • Unterstützung für SSL/TLS • Unterstützung für Keep-Alive und Slow Requests • Statische Assets
  • 13.
    Projekt Setup 1. cargonew actix-hello-world 2.
  • 14.
    Hello World! use actix_web::{web,App, HttpRequest, HttpServer, Responder}; async fn greet(req: HttpRequest) -> impl Responder { let name = req.match_info().get("name").unwrap_or("World"); format!("Hello {}!", &name) } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .route("/", web::get().to(greet)) .route("/{name}", web::get().to(greet)) }) .bind("127.0.0.1:8000")? .run() .await }
  • 15.
    Main-Funktion #[actix_web::main] async fn main()-> std::io::Result<()> { HttpServer::new(|| { App::new() .route("/", web::get().to(greet)) .route("/{name}", web::get().to(greet)) }) .bind("127.0.0.1:8000")? .run() .await } Server-Initialisierung App-Initialisierung Route-Mapping Adress-Bindung Entry-Point
  • 16.
    Handler-Funktion // Impliziter HttpResponse asyncfn greet(req: HttpRequest) -> impl Responder { let name = req.match_info().get("name").unwrap_or("World"); format!("Hello {}!", &name) } // Expliziter HttpResponse async fn greet(req: HttpRequest) -> Result<HttpResponse, Error> { let name = req.match_info().get("name").unwrap_or("World"); let body = format!("Hello {}!", &name); Ok(HttpResponse::Ok().body(body)) }
  • 17.
    Extractors -- Path #[get("/{name}")] asyncfn greet(web::Path(name): web::Path<String>) -> impl Responder { let value = if name.is_empty() { String::from("World!") } else { name }; format!("Hello {}!", &value) }
  • 18.
    Extractors -- Query #[derive(Deserialize)] structInfo { name: String, } #[get("/")] async fn greet(info: web::Query<Info>) -> impl Responder { format!("Welcome {}!", info.name) } curl "http://localhost:8000?name=betterCode(Rust)"
  • 19.
    Extractors -- JSON #[derive(Deserialize)] structInfo { name: String, } #[get("/")] async fn greet(info: web::Json<Info>) -> impl Responder { format!("Welcome {}!", info.name) } curl -X GET -H "Content-type: application/json“ -H "Accept: application/json" -d ‘{"name":"betterCode(Rust)"}‘ http://localhost:8000/"
  • 20.
    Extractors – FormData #[derive(Deserialize)] struct Info { name: String, } #[post("/")] async fn greet(info: web::Form<Info>) -> impl Responder { format!("Welcome {}!", info.name) } curl -X POST -H "Content-type: application/x-www-form-urlencoded" -d "name=betterCode(Rust)" http://localhost:8000/"
  • 21.
    Middleware use actix_web::{web, middleware,App, HttpRequest, HttpServer, Responder}; […] #[actix_web::main] async fn main() -> std::io::Result<()> { std::env::set_var("RUST_LOG", "actix_web=info"); env_logger::init(); HttpServer::new(|| { App::new() .wrap(middleware::Logger::default()) .route("/", web::get().to(greet)) .route("/{name}", web::get().to(greet)) }) .bind("127.0.0.1:8000")? .run() .await }
  • 22.
    Authentifizierung use actix_web::dev::ServiceRequest; use actix_web_httpauth::middleware::HttpAuthentication; useactix_web_httpauth::extractors::basic::BasicAuth; use actix_web_httpauth::extractors::bearer::BearerAuth; async fn basic_validation(req: ServiceRequest, _cred: BasicAuth) -> Result<ServiceRequest, Error> { // Validate credetials here... Ok(req) } async fn bearer_validation(req: ServiceRequest, _cred: BearerAuth) -> Result<ServiceRequest, Error> { // Validate credetials here... Ok(req) }
  • 23.
    Authentifizierung -- Middleware #[actix_web::main] asyncfn main() -> std::io::Result<()> { HttpServer::new(|| { let basic_auth = HttpAuthentication::basic(basic_validation); let bearer_auth = HttpAuthentication::bearer(bearer_validation); App::new() .wrap(middleware::Logger::default()) .wrap(basic_auth) // oder .wrap(bearer_auth) .route("/", web::get().to(greet)) .route("/{name}", web::get().to(greet)) }) .bind("127.0.0.1:8000")? .run() .await }
  • 24.
    Datenbankzugriff mit Diesel •Object Relational Mapper und Query Builder für Rust • Fokus auf möglichst schlanke Abstraktionen • Hohe Performance • Unterstützte Datenbanken: • MySQL • PostgreSQL • SQLite
  • 25.
    Datenbankzugriff mit Diesel-- Setup cargo install diesel_cli --no-default-features --features sqlite echo "DATABASE_URL=test.db" > .env diesel setup
  • 26.
    Datenbankzugriff mit Diesel-- Migrations mkdir migrations diesel migration generate users diesel migration run
  • 27.
    Datenbankzugriff mit Diesel-- Mapping #[derive(Debug, Clone, Serialize, Deserialize, Queryable, Insertable)] pub struct User { pub id: String, pub name: String }
  • 28.
    Datenbankzugriff mit Diesel-- Verbindung #[actix_web::main] async fn main() -> std::io::Result<()> { let connspec = std::env::var("DATABASE_URL").expect("DATABASE_URL"); let manager = ConnectionManager::<SqliteConnection>::new(connspec); let pool = r2d2::Pool::builder() .build(manager) .expect("Failed to create pool."); HttpServer::new(move || { App::new() .data(pool.clone()) .service(get_user) .service(add_user) }) .bind("127.0.0.1:8000")? .run() .await }
  • 29.
    Datenbankzugriff mit Diesel-- Insert pub fn insert_new_user(nm: &str, conn: &SqliteConnection,) -> Result<User, diesel::result::Error> { use crate::schema::users::dsl::*; let new_user = User { id: Uuid::new_v4().to_string(), name: nm.to_owned() }; diesel::insert_into(users).values(&new_user).execute(conn)?; Ok(new_user) }
  • 30.
    Datenbankzugriff mit Diesel-- Insert-Handler #[post("/user")] async fn add_user(pool: web::Data<DbPool>, form: web::Json<NewUser>) -> Result<HttpResponse, Error> { let conn = pool.get().expect("couldn't get db connection from pool"); let user = web::block(move || insert_new_user(&form.name, &conn)) .await .map_err(|e| { eprintln!("{}", e); HttpResponse::InternalServerError().finish() })?; Ok(HttpResponse::Ok().json(user)) }
  • 31.
    Testen #[cfg(test)] mod tests { usesuper::*; use actix_web::test; #[actix_rt::test] async fn test_user_creation_ok() { // DB connection and logging setup let mut app = test::init_service( // App initialization ) .await; let req = test::TestRequest::post() .uri("/user") .set_json(&NewUser { name: "Test user".to_owned(), }).to_request(); let resp: User = test::read_response_json(&mut app, req).await; assert_eq!(resp.name, "Test user"); } }
  • 32.
    Continuous Integration • Tests: •cargo test • Code Coverage: • cargo install tarpaulin * • cargo tarpaulin --ignore-tests • Lint: • rustup component add clippy • cargo clippy -- -D warnings • Formatting: • rustup component add rustfmt • cargo fmt -- --check • Auditing: • cargo install cargo-audit • cargo audit * = unterstützt zurzeit nur x86_64-linux
  • 33.
    Fazit Rust als Basisfür Microservices bietet: • Hohe Performance durch Ahead-of-Time Kompilierung, Zero Cost Abstractions • Sicheres Speichermanagement bereits während der Kompilierung • Sichere Nebenläufigkeit • Keine Einbrüche bei der Performance durch Garbage Collection • Komfortables Tooling, welches etablierten Sprachen und Frameworks in nichts nachsteht • Freundliche und hilfsbereite Community • Teilweise steile Lernkurve (aber es lohnt sich!)
  • 34.
  • 35.