•20 min read

Xây dựng Microservice hiệu suất cao với Rust và Axum: Hướng dẫn sản xuất hoàn chỉnh

Xây dựng Microservice hiệu suất cao với Rust và Axum: Hướng dẫn sản xuất hoàn chỉnh

Hướng dẫn này trình bày chi tiết việc xây dựng các microservice thông lượng cao bằng Rust, Axum và Tokio. Chúng ta sẽ đề cập đến cấu trúc dự án, middleware Tower nâng cao cho các vấn đề vận hành quan trọng, tương tác cơ sở dữ liệu mạnh mẽ với SQLx, cơ chế tắt máy an toàn và các bản dựng Docker multi-stage được tối ưu hóa cho các image sản xuất tối thiểu.

Audio Briefing
0:00 / 0:00

Cấu trúc dự án và các Dependency

Một cấu trúc dự án được tổ chức tốt là tối quan trọng cho khả năng bảo trì và khả năng mở rộng. Chúng tôi ủng hộ một cách tiếp cận mô-đun, tách biệt các mối quan tâm thành các crate hoặc mô-đun riêng biệt.

.
├── Cargo.toml
├── src
│   ├── main.rs
│   ├── config.rs
│   ├── handlers.rs
│   ├── models.rs
│   ├── middleware
│   │   ├── mod.rs
│   │   ├── rate_limit.rs
│   │   └── tracing.rs
│   └── db.rs
└── Dockerfile

Cargo.toml của chúng ta sẽ bao gồm các dependency thiết yếu:

# Cargo.toml
[package]
name = "high-throughput-service"
version = "0.1.0"
edition = "2021"

[dependencies]
# Web framework
axum = { version = "0.7", features = ["macros"] }
tokio = { version = "1", features = ["full"] }
tower = { version = "0.4", features = ["full"] }
tower-http = { version = "0.5", features = ["full"] }

# Database
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono"] }
deadpool-redis = { version = "0.13", features = ["rt-tokio-1"] } # For rate limiting

# Serialization/Deserialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"

# Configuration
config = { version = "0.13", features = ["toml", "yaml"] } # Or `envy` for env vars

# Logging and Tracing
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
opentelemetry = { version = "0.21", features = ["rt-tokio"] }
opentelemetry-sdk = { version = "0.21", features = ["rt-tokio"] }
opentelemetry-stdout = { version = "0.16" } # For local dev
opentelemetry-otlp = { version = "0.14", features = ["grpc-tonic", "reqwest-client", "tokio"] }
tracing-opentelemetry = "0.22"

# Utilities
uuid = { version = "1", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
anyhow = "1"
Advertisement

Quản lý cấu hình

Cấu hình bên ngoài là rất quan trọng. Chúng ta sẽ sử dụng crate config để tải cài đặt từ các biến môi trường và một tệp cấu hình.

// src/config.rs
use serde::Deserialize;
use std::net::SocketAddr;

#[derive(Debug, Deserialize, Clone)]
pub struct AppConfig {
    pub server_address: SocketAddr,
    pub database_url: String,
    pub redis_url: String,
    pub service_name: String,
    pub otel_exporter_otlp_endpoint: Option<String>,
}

impl AppConfig {
    pub fn load() -> Result<Self, config::ConfigError> {
        let s = config::Config::builder()
            .add_source(config::File::with_name("config.toml").required(false))
            .add_source(config::Environment::with_prefix("APP"))
            .build()?;
        s.try_deserialize()
    }
}

Kết nối cơ sở dữ liệu với SQLx

SQLx cung cấp các truy vấn được kiểm tra tại thời điểm biên dịch và kết nối pool mạnh mẽ. Chúng ta sẽ sử dụng deadpool-redis cho Redis.

// src/db.rs
use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;
use deadpool_redis::{Pool as RedisPool, Config as RedisConfig, Runtime};
use std::time::Duration;
use anyhow::Result;

pub async fn setup_pg_pool(database_url: &str) -> Result<PgPool> {
    let pool = PgPoolOptions::new()
        .max_connections(50)
        .min_connections(10)
        .acquire_timeout(Duration::from_secs(5))
        .connect_timeout(Duration::from_secs(5))
        .idle_timeout(Duration::from_secs(30 * 60)) // 30 minutes
        .test_before_acquire(true)
        .connect(database_url)
        .await?;
    Ok(pool)
}

pub fn setup_redis_pool(redis_url: &str) -> Result<RedisPool> {
    let cfg = RedisConfig::from_url(redis_url);
    let pool = cfg.create_pool(Some(Runtime::Tokio1))?;
    Ok(pool)
}

Tower Middleware cho các vấn đề vận hành

Kiến trúc hướng dịch vụ của Tower rất mạnh mẽ. Chúng ta sẽ triển khai giới hạn tốc độ, truy vết phân tán và xác thực yêu cầu.

Truy vết phân tán

Tích hợp OpenTelemetry là rất quan trọng cho khả năng quan sát.

// src/middleware/tracing.rs
use axum::{
    extract::Request,
    middleware::Next,
    response::Response,
};
use opentelemetry::{
    global,
    trace::{SpanKind, Tracer},
    Context, KeyValue,
};
use opentelemetry_sdk::{
    trace::{Config, TracerProvider},
    Resource,
};
use opentelemetry_otlp::WithExportConfig;
use tracing::{info_span, Instrument};
use tracing_opentelemetry::OpenTelemetrySpanExt;
use anyhow::Result;

pub fn init_tracer(service_name: &str, otlp_endpoint: Option<&str>) -> Result<()> {
    let resource = Resource::new(vec![
        KeyValue::new("service.name", service_name.to_string()),
        KeyValue::new("service.version", env!("CARGO_PKG_VERSION").to_string()),
    ]);

    let tracer_provider = if let Some(endpoint) = otlp_endpoint {
        opentelemetry_otlp::new_exporter()
            .tonic()
            .with_endpoint(endpoint)
            .build_tracer_provider_with_config(
                Config::default().with_resource(resource)
            )
    } else {
        // Fallback to stdout for local development if no OTLP endpoint is configured
        opentelemetry_stdout::new_pipeline()
            .with_trace_config(Config::default().with_resource(resource))
            .install_simple()
            .expect("Failed to install stdout tracer provider")
    };

    global::set_tracer_provider(tracer_provider);
    Ok(())
}

pub async fn trace_layer(req: Request, next: Next) -> Response {
    let path = req.uri().path().to_string();
    let method = req.method().to_string();

    let tracer = global::tracer("axum-server");
    let parent_cx = global::get_text_map_propagator(|propagator| {
        propagator.extract(&opentelemetry_http::HeaderExtractor(req.headers()))
    });

    let span = tracer
        .span_builder(format!("HTTP {} {}", method, path))
        .with_kind(SpanKind::Server)
        .with_parent_context(parent_cx)
        .start(&tracer);

    let cx = Context::current_with_span(span);
    let _guard = cx.attach();

    let response = next.run(req).instrument(info_span!("request_processing")).await;

    let span = cx.span();
    span.set_attribute(KeyValue::new("http.method", method));
    span.set_attribute(KeyValue::new("http.target", path));
    span.set_attribute(KeyValue::new("http.status_code", response.status().as_u16() as i64));
    span.end();

    response
}

Giới hạn tốc độ

Một bộ giới hạn tốc độ phân tán sử dụng Redis là cần thiết để bảo vệ tài nguyên.

// src/middleware/rate_limit.rs
use axum::{
    extract::{Request, State},
    http::StatusCode,
    middleware::Next,
    response::Response,
};
use deadpool_redis::redis::AsyncCommands;
use deadpool_redis::Pool as RedisPool;
use std::time::Duration;
use tracing::{error, info};

#[derive(Clone)]
pub struct RateLimiter {
    pub redis_pool: RedisPool,
    pub limit: u64,
    pub window_seconds: u64,
}

impl RateLimiter {
    pub fn new(redis_pool: RedisPool, limit: u64, window_seconds: u64) -> Self {
        Self {
            redis_pool,
            limit,
            window_seconds,
        }
    }

    pub async fn layer(State(rate_limiter): State<RateLimiter>, request: Request, next: Next) -> Result<Response, StatusCode> {
        let ip_address = request
            .headers()
            .get("X-Forwarded-For")
            .and_then(|h| h.to_str().ok())
            .and_then(|s| s.split(',').next()) // Take the first IP in case of multiple proxies
            .unwrap_or("unknown")
            .to_string();

        let key = format!("rate_limit:{}", ip_address);

        let mut conn = rate_limiter.redis_pool.get().await.map_err(|e| {
            error!("Failed to get Redis connection: {:?}", e);
            StatusCode::INTERNAL_SERVER_ERROR
        })?;

        let (count, _): (u64, u64) = deadpool_redis::redis::pipe()
            .atomic()
            .incr(&key, 1)
            .expire(&key, rate_limiter.window_seconds as usize)
            .query_async(&mut *conn)
            .await
            .map_err(|e| {
                error!("Failed to execute Redis rate limit command: {:?}", e);
                StatusCode::INTERNAL_SERVER_ERROR
            })?;

        if count > rate_limiter.limit {
            info!("Rate limit exceeded for IP: {}", ip_address);
            return Err(StatusCode::TOO_MANY_REQUESTS);
        }

        Ok(next.run(request).await)
    }
}
Advertisement

Handler và Routing

Các hàm handler của Axum rất đơn giản. Chúng ta sẽ định nghĩa một kiểm tra sức khỏe đơn giản và một endpoint dữ liệu.

// src/handlers.rs
use axum::{
    extract::{Path, State},
    http::StatusCode,
    response::{IntoResponse, Json},
};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use uuid::Uuid;
use chrono::{DateTime, Utc};
use tracing::{info, error};

#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Item {
    pub id: Uuid,
    pub name: String,
    pub description: Option<String>,
    pub created_at: DateTime<Utc>,
}

#[derive(Debug, Deserialize)]
pub struct CreateItem {
    pub name: String,
    pub description: Option<String>,
}

#[derive(Clone)]
pub struct AppState {
    pub pg_pool: PgPool,
    // Other shared state like RedisPool, etc.
}

pub async fn health_check() -> impl IntoResponse {
    (StatusCode::OK, "OK")
}

pub async fn create_item(
    State(state): State<AppState>,
    Json(payload): Json<CreateItem>,
) -> Result<Json<Item>, StatusCode> {
    info!("Attempting to create item: {}", payload.name);
    let new_item = sqlx::query_as!(
        Item,
        r#"
        INSERT INTO items (id, name, description, created_at)
        VALUES ($1, $2, $3, $4)
        RETURNING id, name, description, created_at
        "#,
        Uuid::new_v4(),
        payload.name,
        payload.description,
        Utc::now()
    )
    .fetch_one(&state.pg_pool)
    .await
    .map_err(|e| {
        error!("Failed to insert item: {:?}", e);
        StatusCode::INTERNAL_SERVER_ERROR
    })?;

    info!("Successfully created item with ID: {}", new_item.id);
    Ok(Json(new_item))
}

pub async fn get_item(
    State(state): State<AppState>,
    Path(item_id): Path<Uuid>,
) -> Result<Json<Item>, StatusCode> {
    info!("Attempting to retrieve item with ID: {}", item_id);
    let item = sqlx::query_as!(
        Item,
        r#"
        SELECT id, name, description, created_at
        FROM items
        WHERE id = $1
        "#,
        item_id
    )
    .fetch_optional(&state.pg_pool)
    .await
    .map_err(|e| {
        error!("Failed to query item: {:?}", e);
        StatusCode::INTERNAL_SERVER_ERROR
    })?
    .ok_or(StatusCode::NOT_FOUND)?;

    info!("Successfully retrieved item with ID: {}", item_id);
    Ok(Json(item))
}

Tắt máy an toàn

Việc tắt máy đúng cách đảm bảo không có yêu cầu đang thực hiện nào bị chấm dứt đột ngột và các tài nguyên được giải phóng.

// src/main.rs (excerpt)
// ... imports ...
use tokio::signal;
use tracing::info;

async fn shutdown_signal() {
    let ctrl_c = async {
        signal::ctrl_c()
            .await
            .expect("failed to install Ctrl+C handler");
    };

    #[cfg(unix)]
    let terminate = async {
        signal::unix::signal(signal::unix::SignalKind::terminate())
            .expect("failed to install SIGTERM handler")
            .recv()
            .await;
    };

    #[cfg(not(unix))]
    let terminate = std::future::pending::<()>();

    tokio::select! {
        _ = ctrl_c => {},
        _ = terminate => {},
    }

    info!("Shutdown signal received, initiating graceful shutdown...");
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // ... config loading, tracing setup, db setup ...

    let app = Router::new()
        .route("/health", get(health_check))
        .route("/items", post(create_item))
        .route("/items/:id", get(get_item))
        .with_state(app_state.clone())
        .layer(middleware::from_fn_with_state(rate_limiter.clone(), RateLimiter::layer))
        .layer(middleware::from_fn(trace_layer))
        .layer(TraceLayer::new_for_http()) // Axum's built-in tracing for request/response logging
        .layer(SetRequestIdLayer::new(
            Make<Uuid>::new(),
            PropagateRequestIdLayer::new(HeaderName::from_static("x-request-id")),
        ))
        .layer(TimeoutLayer::new(Duration::from_secs(30))); // Request timeout

    let listener = tokio::net::TcpListener::bind(&config.server_address).await?;
    info!("Listening on {}", config.server_address);

    axum::serve(listener, app.into_make_service())
        .with_graceful_shutdown(shutdown_signal())
        .await?;

    info!("Server gracefully shut down.");
    Ok(())
}

Docker Multi-Stage Builds

Các image Docker được tối ưu hóa là rất quan trọng để triển khai nhanh chóng và giảm bề mặt tấn công. Chúng ta sẽ sử dụng bản dựng multi-stage để tạo ra một image scratch tối thiểu.

# Dockerfile

# Stage 1: Builder
FROM rust:1.78-slim-bookworm AS builder

# Install build dependencies for SQLx and OpenSSL
RUN apt-get update && apt-get install -y \
    pkg-config \
    libssl-dev \
    postgresql-client \
    musl-tools \
    && rm -rf /var/lib/apt/lists/*

# Set the target for musl (static linking)
RUN rustup target add x86_64-unknown-linux-musl

WORKDIR /app

# Copy Cargo.toml and Cargo.lock first to leverage Docker cache
COPY Cargo.toml Cargo.lock ./

# Create a dummy src directory and main.rs to cache dependencies
RUN mkdir src && echo "fn main() {}" > src/main.rs
# Build dependencies only
RUN cargo build --release --target x86_64-unknown-linux-musl
# Remove dummy files
RUN rm -rf src target/x86_64-unknown-linux-musl/release/high-throughput-service

# Copy the actual source code
COPY src ./src

# Build the application
RUN cargo build --release --target x86_64-unknown-linux-musl

# Stage 2: Runtime
FROM scratch

# Set timezone data (important for many applications)
# FROM debian:bookworm-slim # Alternative if scratch is too restrictive for your needs
# RUN apt-get update && apt-get install -y tzdata && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Copy the compiled binary from the builder stage
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/high-throughput-service ./high-throughput-service

# Copy configuration file if present
COPY config.toml ./config.toml

# Expose the port the application listens on
EXPOSE 8080

# Set environment variables for configuration
ENV APP_SERVER_ADDRESS="0.0.0.0:8080"
ENV APP_DATABASE_URL="postgres://user:password@host:port/database"
ENV APP_REDIS_URL="redis://:password@host:port/"
ENV APP_SERVICE_NAME="high-throughput-service"
# ENV APP_OTEL_EXPORTER_OTLP_ENDPOINT="http://otel-collector:4317"

# Run the application
CMD ["./high-throughput-service"]

Dockerfile này tạo ra một image thường dưới 20MB, chỉ chứa binary được liên kết tĩnh và cấu hình cần thiết.

So sánh kiến trúc & đánh đổi

Tính năng/Khía cạnhRust/Axum/TokioNode.js/Express/FastifyGo/Gin/Echo
Hiệu suấtTuyệt vời (gần như bare-metal)Tốt (hướng sự kiện, nút thắt cổ chai đơn luồng)Tuyệt vời (goroutine, biên dịch)
Mức sử dụng bộ nhớRất thấpTrung bình đến cao (chi phí V8 engine)Thấp đến trung bình
Mô hình đồng thờiAsync/await (Tokio runtime)Event Loop (đơn luồng, I/O không chặn)Goroutine & Channels
An toàn kiểuMạnh (thời gian biên dịch)Yếu (thời gian chạy, TypeScript cải thiện điều này)Mạnh (thời gian biên dịch)
Xử lý lỗiEnum Result, anyhow/thiserrorCallbacks, Promises, try/catchNhiều giá trị trả về, interface error
Độ trưởng thành hệ sinh tháiPhát triển nhanh chóng, sẵn sàng sản xuấtRất trưởng thành, hệ sinh thái npm rộng lớnTrưởng thành, thư viện chuẩn mạnh mẽ
Đường cong học tậpDốc (ownership, borrow checker, async)Trung bình (sắc thái JavaScript, mẫu async)Trung bình (nguyên thủy đồng thời, interface)
Kích thước binaryRất nhỏ (liên kết tĩnh)Lớn (bao gồm Node.js runtime)Nhỏ (liên kết tĩnh)
Trường hợp sử dụngAPI hiệu suất cao, dịch vụ độ trễ thấp, nhúng, lập trình hệ thốngTạo mẫu nhanh, dịch vụ giới hạn I/O, ứng dụng webMicroservice, công cụ CLI, dịch vụ mạng
Đánh đổiChi phí phát triển ban đầu cao hơn, đường cong học tập dốc, an toàn và hiệu suất thời gian chạy tuyệt vời.Phát triển nhanh hơn, runtime lớn hơn, tiềm ẩn lỗi thời gian chạy, hiệu suất ít bị giới hạn bởi CPU.Cân bằng tốt giữa hiệu suất và tốc độ phát triển, đồng thời đơn giản hơn Rust, hệ thống kiểu ít biểu cảm hơn.

Những vấn đề và cách khắc phục trong sản xuất

1. database connection timed out hoặc connection refused

Chế độ lỗi: Ứng dụng không thể kết nối với cơ sở dữ liệu PostgreSQL khi khởi động hoặc trong quá trình hoạt động. Cách khắc phục:

  • Kết nối mạng: Xác minh máy chủ cơ sở dữ liệu có thể truy cập được từ môi trường của microservice (ví dụ: ping, telnet <db_host> <db_port>).
  • Quy tắc tường lửa: Đảm bảo các cổng cần thiết (mặc định 5432 cho PostgreSQL) được mở.
  • Thông tin đăng nhập cơ sở dữ liệu: Kiểm tra lại APP_DATABASE_URL để đảm bảo tên người dùng, mật khẩu, máy chủ, cổng và tên cơ sở dữ liệu chính xác.
  • Tính khả dụng của cơ sở dữ liệu: Xác nhận máy chủ PostgreSQL đang chạy và chấp nhận kết nối.
  • Cạn kiệt Connection Pool: Nếu lỗi xảy ra trong quá trình hoạt động, hãy tăng max_connections trong PgPoolOptions và theo dõi giới hạn kết nối cơ sở dữ liệu.

2. Redis connection timed out hoặc connection refused

Chế độ lỗi: Giới hạn tốc độ hoặc các tính năng phụ thuộc Redis khác bị lỗi. Cách khắc phục:

  • Kết nối mạng: Xác minh máy chủ Redis có thể truy cập được (ví dụ: ping, telnet <redis_host> <redis_port>).
  • Quy tắc tường lửa: Đảm bảo cổng Redis (mặc định 6379) được mở.
  • Thông tin đăng nhập Redis: Kiểm tra APP_REDIS_URL để đảm bảo mật khẩu và máy chủ/cổng chính xác.
  • Tính khả dụng của Redis: Xác nhận máy chủ Redis đang chạy.

3. Sử dụng CPU cao / Đỉnh độ trễ

Chế độ lỗi: Dịch vụ gặp phải tải CPU cao bất ngờ hoặc thời gian phản hồi chậm dưới tải trung bình. Cách khắc phục:

  • Tối ưu hóa truy vấn cơ sở dữ liệu: Phân tích các truy vấn chậm bằng cách sử dụng EXPLAIN ANALYZE trong PostgreSQL. Đảm bảo lập chỉ mục phù hợp.
  • Vấn đề truy vấn N+1: Xác định và tái cấu trúc các handler thực hiện nhiều lệnh gọi cơ sở dữ liệu trong một vòng lặp. Sử dụng JOIN hoặc batching.
  • Các hoạt động giới hạn CPU: Lập hồ sơ mã Rust để xác định các điểm nóng. Cân nhắc chuyển các tính toán nặng sang các worker nền hoặc tối ưu hóa thuật toán.
  • Mức độ chi tiết ghi nhật ký: Ghi nhật ký DEBUG hoặc TRACE quá mức trong sản xuất có thể làm tăng đáng kể chi phí. Điều chỉnh biến môi trường RUST_LOG.

4. Lỗi Too Many Open Files

Chế độ lỗi: Dịch vụ gặp sự cố với lỗi cấp hệ điều hành cho biết không thể mở thêm tệp (socket là tệp). Cách khắc phục:

  • ulimit: Tăng giới hạn nofile cho người dùng đang chạy dịch vụ. Trong Docker, điều này có thể được đặt bằng --ulimit nofile=65536:65536.
  • Cài đặt Connection Pool: Đảm bảo max_connections cho các pool cơ sở dữ liệu và Redis là hợp lý và không quá cao, tiêu thụ quá nhiều bộ mô tả tệp.
  • Rò rỉ tài nguyên: Điều tra xem các kết nối hoặc bộ xử lý tệp có đang được đóng đúng cách hay không. RAII của Rust giúp ích, nhưng drop hoặc close thủ công có thể cần thiết cho các tài nguyên bên ngoài.

5. Dữ liệu truy vết không xuất hiện trong Collector

Chế độ lỗi: Các truy vết OpenTelemetry không hiển thị trong hệ thống APM của bạn (ví dụ: Jaeger, Datadog). Cách khắc phục:

  • APP_OTEL_EXPORTER_OTLP_ENDPOINT: Xác minh endpoint được cấu hình đúng và có thể truy cập được từ microservice.
  • Tính khả dụng của Collector: Đảm bảo OpenTelemetry collector hoặc APM agent đang chạy và lắng nghe trên cổng được chỉ định.
  • Tường lửa: Kiểm tra các quy tắc tường lửa giữa dịch vụ và collector.
  • Lấy mẫu: Nếu sử dụng bộ lấy mẫu, hãy đảm bảo các truy vết không bị lấy mẫu quá mức.
  • Tên dịch vụ: Xác nhận APP_SERVICE_NAME được đặt, vì nó rất quan trọng để xác định các truy vết.

6. Vấn đề kích thước Docker Image

Chế độ lỗi: Docker image cuối cùng lớn bất ngờ, mặc dù sử dụng các bản dựng multi-stage. Cách khắc phục:

  • FROM scratch: Đảm bảo giai đoạn cuối cùng thực sự sử dụng FROM scratch hoặc một base image tối thiểu như distroless/static.
  • Các tệp không cần thiết: Kiểm tra kỹ để đảm bảo chỉ có binary đã biên dịch và cấu hình thiết yếu được sao chép trong giai đoạn cuối cùng. Tránh sao chép các thư mục target hoặc mã nguồn.
  • Liên kết tĩnh: Đảm bảo mục tiêu x86_64-unknown-linux-musl được sử dụng để liên kết tĩnh nhằm tránh cần glibc trong image cuối cùng.
  • Hủy bỏ bộ nhớ cache bản dựng: Nếu Cargo.toml hoặc Cargo.lock thay đổi thường xuyên, bộ nhớ cache bản dựng dependency có thể bị hủy bỏ. Cấu trúc Dockerfile để sao chép những thứ này trước.

Các câu hỏi thường gặp

Q1: Làm cách nào để xử lý xác thực yêu cầu ngoài việc giải mã JSON cơ bản?

A1: Đối với xác thực phức tạp, hãy tích hợp một thư viện xác thực như validator. Bạn có thể tạo một extractor Axum tùy chỉnh sử dụng validator để kiểm tra dữ liệu đến.

// Example: Custom validator extractor
use axum::{
    async_trait,
    extract::{FromRequest, Request},
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use serde::de::DeserializeOwned;
use validator::Validate;

pub struct ValidatedJson<T>(pub T);

#[async_trait]
impl<T, S> FromRequest<S> for ValidatedJson<T>
where
    T: DeserializeOwned + Validate,
    S: Send + Sync,
{
    type Rejection = Response;

    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
        let Json(value) = Json::<T>::from_request(req, state)
            .await
            .map_err(|err| err.into_response())?;

        value.validate().map_err(|err| {
            (StatusCode::BAD_REQUEST, Json(err)).into_response()
        })?;

        Ok(ValidatedJson(value))
    }
}

// Usage in handler:
// pub async fn create_item(ValidatedJson(payload): ValidatedJson<CreateItem>) -> ...

Q2: Cách tốt nhất để quản lý trạng thái chia sẻ (như database pools) giữa các handler là gì?

A2: Extractor State của Axum là cách làm thông thường. Định nghĩa một struct (ví dụ: AppState) chứa tất cả các tài nguyên chia sẻ của bạn, clone nó và truyền nó vào phương thức .with_state() của router. Mỗi handler sau đó có thể trích xuất State<AppState>. Đảm bảo struct trạng thái của bạn triển khai Clone.

Q3: Làm cách nào để triển khai xác thực và ủy quyền?

A3: Triển khai xác thực và ủy quyền dưới dạng Tower middleware. Đối với xác thực, một middleware có thể trích xuất một token (ví dụ: JWT) từ các header, xác thực nó, sau đó chèn thông tin người dùng vào các phần mở rộng yêu cầu. Middleware ủy quyền hoặc logic handler tiếp theo sau đó có thể truy xuất dữ liệu người dùng này. Các thư viện như jsonwebtoken rất hữu ích cho việc xử lý JWT.

Q4: Dịch vụ của tôi gặp sự cố với SIGSEGV hoặc các lỗi cấp thấp khác. Tôi nên làm gì?

A4: SIGSEGV (lỗi phân đoạn) trong Rust rất hiếm nhưng cho thấy lỗi bộ nhớ, thường do mã unsafe hoặc tương tác FFI (Foreign Function Interface).

  1. Xem xét các khối unsafe: Kiểm tra cẩn thận bất kỳ mã unsafe nào để đảm bảo tính đúng đắn.
  2. Ranh giới FFI: Nếu tương tác với các thư viện C/C++, hãy đảm bảo quản lý bộ nhớ và chuyển đổi kiểu chính xác qua ranh giới FFI.
  3. Vấn đề Dependency: Kiểm tra các vấn đề đã biết trong các dependency của bạn, đặc biệt là những dependency sử dụng unsafe bên trong.
  4. Memory Sanitizers: Mặc dù khó tích hợp với Rust hơn, các công cụ như AddressSanitizer (ASan) đôi khi có thể giúp gỡ lỗi các vấn đề bộ nhớ gốc.
  5. Khả năng tái tạo: Cố gắng tạo một ví dụ có thể tái tạo tối thiểu để cô lập vấn đề.

Q5: Làm cách nào để xử lý các bản di chuyển cơ sở dữ liệu trong môi trường sản xuất?

A5: Sử dụng sqlx-cli để quản lý các bản di chuyển.

  1. Tạo bản di chuyển: sqlx migrate add <migration_name>
  2. Áp dụng bản di chuyển: Trong mã khởi động ứng dụng của bạn, sử dụng sqlx::migrate!().run(&pool).await; để tự động áp dụng các bản di chuyển đang chờ xử lý. Điều này đảm bảo lược đồ cơ sở dữ liệu của bạn luôn được cập nhật với mã ứng dụng của bạn.
  3. Dịch vụ di chuyển riêng biệt: Đối với các triển khai phức tạp hơn, hãy cân nhắc chạy các bản di chuyển như một bước riêng biệt, trước khi triển khai hoặc như một vùng chứa khởi tạo Kubernetes chuyên dụng.
// src/main.rs (đoạn trích cho các bản di chuyển)
// ...
#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // ...
    let pg_pool = db::setup_pg_pool(&config.database_url).await?;

    // Áp dụng các bản di chuyển cơ sở dữ liệu
    info!("Đang chạy các bản di chuyển cơ sở dữ liệu...");
    sqlx::migrate!("./migrations") // Đường dẫn đến thư mục bản di chuyển của bạn
        .run(&pg_pool)
        .await?;
    info!("Các bản di chuyển cơ sở dữ liệu đã được áp dụng thành công.");
    // ...
}
Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement