Einleitung: Warum Rust für Microservices?
Die Microservice-Architektur ist seit langem der Standard für den Aufbau skalierbarer verteilter Systeme. Üblicherweise werden dafür Go, Java oder Node.js gewählt. In den letzten Jahren zieht jedoch Rust immer mehr Aufmerksamkeit auf sich – eine systemnahe Programmiersprache, die die Leistung von C++ mit Speichersicherheit kombiniert. In diesem Artikel auf pythonlib.ru werden wir untersuchen, wie man Microservices in Rust erstellt, welche Bibliotheken man verwendet und wie man die Architektur organisiert.
Rust eignet sich ideal für Microservices aufgrund von:
- Keinem Garbage Collector (geringe Latenz und vorhersehbarer Speicherverbrauch);
- Sicherer Speicherverwaltung zur Kompilierzeit;
- Hervorragender Unterstützung für Asynchronität durch das
Tokio-Ökosystem; - Kleiner Größe der resultierenden Binärdatei (ideal für Containerisierung).
Architektur eines Microservices in Rust
Ein typischer Microservice in Rust besteht aus mehreren Schichten:
- HTTP-Server (normalerweise basierend auf
actix-weboderaxum); - Geschäftslogik (reine Rust-Funktionen);
- Datenschicht (Datenbankzugriff über
sqlxoderdiesel); - Asynchroner Runtime (
Tokio); - Serialisierung (meistens
serdefür JSON).
Betrachten wir die grundlegende Architektur am Beispiel eines einfachen Benutzerverwaltungsdienstes.
1. Projekteinrichtung und Abhängigkeiten
Erstellen Sie ein neues Cargo-Projekt und fügen Sie die Abhängigkeiten in Cargo.toml hinzu:
[package]name = "user-service"version = "0.1.0"edition = "2021"
[dependencies]actix-web = "4"serde = { version = "1", features = ["derive"] }serde_json = "1"tokio = { version = "1", features = ["full"] }sqlx = { version = "0.7", features = ["runtime-tokio", "postgres"] }dotenv = "0.15"2. Erstellung der grundlegenden Serverstruktur
Definieren wir die Datenmodelle und starten den HTTP-Server:
use actix_web::{web, App, HttpServer, Responder};use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone)]struct User { id: u32, name: String, email: String,}
// In-Memory-Speicher für das Beispielasync fn get_users() -> impl Responder { let users = vec![ User { id: 1, name: "Alice".into(), email: "alice@example.com".into() }, User { id: 2, name: "Bob".into(), email: "bob@example.com".into() }, ]; web::Json(users)}
#[actix_web::main]async fn main() -> std::io::Result<()> { println!("Starting user-service on http://127.0.0.1:8080");
HttpServer::new(|| { App::new() .route("/users", web::get().to(get_users)) }) .bind("127.0.0.1:8080")? .run() .await}Dies ist ein minimaler Microservice. Beachten Sie: #[actix_web::main] ist ein Makro, das die asynchrone Runtime Tokio startet.
3. Asynchrone Datenbankarbeit
Ein echter Microservice sollte Daten speichern. Wir verwenden sqlx für PostgreSQL. Erstellen Sie eine .env-Datei mit der Verbindungs-URL:
DATABASE_URL=postgres://user:password@localhost/users_dbJetzt schreiben wir eine Funktion zum Abrufen von Benutzern aus der Datenbank:
use sqlx::PgPool;
async fn get_users_from_db(pool: web::Data<PgPool>) -> impl Responder { // SQL-Abfrage ausführen und Ergebnis in die User-Struktur mappen let users: Vec<User> = sqlx::query_as::<_, User>("SELECT id, name, email FROM users") .fetch_all(pool.get_ref()) .await .unwrap_or_default();
web::Json(users)}
#[actix_web::main]async fn main() -> std::io::Result<()> { dotenv::dotenv().ok(); let database_url = std::env::va