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

Mục lục bài viết(23 mục)
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.
Hệ thống Hiệu năng cao & Cơ sở dữ liệu Hiện đại
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"
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)
}
}
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ạnh | Rust/Axum/Tokio | Node.js/Express/Fastify | Go/Gin/Echo |
|---|---|---|---|
| Hiệu suất | Tuyệ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ấp | Trung bình đến cao (chi phí V8 engine) | Thấp đến trung bình |
| Mô hình đồng thời | Async/await (Tokio runtime) | Event Loop (đơn luồng, I/O không chặn) | Goroutine & Channels |
| An toàn kiểu | Mạ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ỗi | Enum Result, anyhow/thiserror | Callbacks, Promises, try/catch | Nhiều giá trị trả về, interface error |
| Độ trưởng thành hệ sinh thái | Phát triển nhanh chóng, sẵn sàng sản xuất | Rất trưởng thành, hệ sinh thái npm rộng lớn | Trưởng thành, thư viện chuẩn mạnh mẽ |
| Đường cong học tập | Dố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 binary | Rấ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ụng | API hiệu suất cao, dịch vụ độ trễ thấp, nhúng, lập trình hệ thống | Tạo mẫu nhanh, dịch vụ giới hạn I/O, ứng dụng web | Microservice, công cụ CLI, dịch vụ mạng |
| Đánh đổi | Chi 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_connectionstrongPgPoolOptionsvà 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 ANALYZEtrong 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
JOINhoặ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ý
DEBUGhoặcTRACEquá 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ườngRUST_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ạnnofilecho 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_connectionscho 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
drophoặcclosethủ 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ụngFROM scratchhoặ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
targethoặ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.tomlhoặcCargo.lockthay đổ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).
- Xem xét các khối
unsafe: Kiểm tra cẩn thận bất kỳ mãunsafenào để đảm bảo tính đúng đắn. - 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.
- 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
unsafebên trong. - 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.
- 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.
- Tạo bản di chuyển:
sqlx migrate add <migration_name> - Á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. - 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.");
// ...
}
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Axum vs Actix-web: Xây dựng REST API sản xuất bằng Rust
Hướng dẫn thực tế để xây dựng web API sản xuất bằng Rust: so sánh Axum và Actix-web, truy cập cơ sở dữ liệu SQLx bất đồng bộ, xử lý lỗi với thiserror, AppState được chia sẻ và các điểm chuẩn triển khai.
Read more
WebAssembly Ngoài Trình Duyệt: Xây Dựng Microservices Hiệu Năng Cao
Khám phá cách sử dụng WebAssembly phía máy chủ với Wasmtime, WasmEdge và Spin để xây dựng các microservices tốc độ gần như native, không phụ thuộc ngôn ngữ và được sandbox về khả năng — với các điểm chuẩn thực tế so với Docker containers.
Read more
ConnectRPC vs gRPC năm 2026: An toàn kiểu dữ liệu đầu cuối cho trình duyệt web & Go microservices
Hướng dẫn toàn diện về connectrpc vs grpc năm 2026: an toàn kiểu dữ liệu đầu cuối cho trình duyệt web & Go microservices với kiến trúc cấp độ sản phẩm và ví dụ code.
Read more