Tại sao mình lại ‘all-in’ vào Rust và Axum?
Sau 6 tháng thực chiến đưa các service Rust lên production, mình rút ra một điều: Rust không hề ‘khó nhằn’ như lời đồn. Cái khó nhất thực tế chỉ là việc làm quen với tư duy quản lý bộ nhớ (Ownership). Trước đây, mình thường chọn Node.js hoặc Go để làm API vì tốc độ code nhanh. Tuy nhiên, khi đối mặt với các bài toán xử lý hàng triệu bản ghi hoặc yêu cầu độ trễ (latency) dưới 10ms, Rust mới thực sự là ‘quái vật’.
Trong hệ sinh thái Rust, Axum nổi lên như một lựa chọn cân bằng nhất. Được phát triển bởi đội ngũ đứng sau Tokio, Axum tận dụng tối đa hệ thống Type System của Rust. Điều này giúp API của bạn cực kỳ an toàn ngay từ lúc biên dịch. Bạn sẽ hiếm khi gặp lỗi Runtime nếu code đã ‘pass’ qua compiler.
Điều khiến mình ‘kết’ Axum nhất chính là khả năng tương thích hoàn hảo với hệ sinh thái tower. Nếu bạn đã quen với cách viết middleware của Express hay Gin, Axum sẽ mang lại cảm giác rất gần gũi. Tuy nhiên, nó ở một đẳng cấp an toàn và hiệu năng hoàn toàn khác biệt.
Khởi tạo dự án: Đừng quên bộ khung chuẩn
Để bắt đầu, hãy đảm bảo máy bạn đã cài Rust qua rustup. Chúng ta khởi tạo dự án chỉ với một dòng lệnh:
cargo new rust-axum-api && cd rust-axum-api
Dưới đây là bộ dependency ‘xương sống’ mà mình luôn tin dùng cho các dự án thực tế. Bạn hãy cập nhật vào file Cargo.toml:
[dependencies]
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
sqlx = { version = "0.7", features = ["runtime-tokio-native-tls", "postgres", "macros"] }
dotenvy = "0.15"
tower-http = { version = "0.5", features = ["trace", "cors"] }
tracing = "0.1"
tracing-subscriber = "0.3"
Mình chọn sqlx thay vì các ORM nặng nề. Lý do rất đơn giản: nó cho phép kiểm tra lỗi cú pháp SQL ngay khi build code. Nếu bạn viết sai tên cột, compiler sẽ báo lỗi ngay lập tức, giúp tiết kiệm hàng giờ debug trên môi trường staging.
Cấu hình thực tế: Từ Database đến Routing
1. Kết nối Database không lo ‘nghẽn’
Thay vì dùng connection string cứng trong code, hãy dùng file .env. Việc này giúp tách biệt môi trường dev và production dễ dàng hơn.
DATABASE_URL=postgres://user:password@localhost:5432/my_db
Trong hàm main, chúng ta thiết lập Connection Pool. Kinh nghiệm của mình là luôn set max_connections dựa trên cấu hình server. Ví dụ, với một con VPS 2 vCPU, con số 5-10 là hợp lý để tránh quá tải cho Postgres.
#[tokio::main]
async fn main() {
dotenvy::dotenv().ok();
let db_url = std::env::var("DATABASE_URL").expect("Thiếu DATABASE_URL");
let pool = PgPoolOptions::new()
.max_connections(10)
.connect(&db_url)
.await
.expect("Kết nối DB thất bại");
}
2. Xử lý Handler với Extractors thông minh
Axum có cơ chế “Extractors” rất mạnh mẽ. Bạn muốn lấy dữ liệu từ JSON body hay Query string? Chỉ cần khai báo kiểu dữ liệu trong tham số hàm. Để chuẩn bị cấu hình JSON chuẩn xác, mình thường dùng JSON Formatter để kiểm tra cấu trúc trước khi map vào Struct trong Rust.
#[derive(Serialize, Deserialize)]
struct User {
id: i32,
username: String,
}
async fn get_users(State(pool): State<sqlx::PgPool>) -> Json<Vec<User>> {
let users = sqlx::query_as!(User, "SELECT id, username FROM users")
.fetch_all(&pool)
.await
.unwrap_or_default();
Json(users)
}
Triển khai lên Production: Sức mạnh của Binary duy nhất
Một sai lầm phổ biến là copy toàn bộ source code lên server rồi chạy cargo run. Đừng làm vậy! Lợi thế lớn nhất của Rust là biên dịch ra một file binary duy nhất nặng khoảng 15-20MB. Bạn chỉ cần build bản release:
cargo build --release
Sau đó, copy file tại target/release/rust-axum-api lên server Linux. Để service chạy ổn định 24/7, hãy dùng systemd. Dưới đây là file config mẫu giúp app tự khởi động lại nếu chẳng may bị crash:
[Service]
ExecStart=/var/www/app/rust-axum-api
Restart=always
Environment=DATABASE_URL=postgres://user:password@localhost/db
User=www-data
Con số ấn tượng: Hiệu năng và RAM
Khi chạy thực tế, bạn sẽ phải kinh ngạc. Một API Axum cơ bản chỉ tiêu thụ khoảng 12-15MB RAM ở trạng thái nghỉ. Trong khi đó, các ứng dụng Java hay Spring Boot tương đương thường ngốn ít nhất 200-300MB. Về tốc độ xử lý, mình từng test tải với wrk và đạt mức 45,000 requests/giây trên một con VPS cấu hình thấp mà CPU vẫn rất ổn định.
Để theo dõi log, bạn chỉ cần dùng lệnh journalctl -u myapp -f. Mọi thông tin từ crate tracing sẽ hiển thị rõ ràng, giúp việc tìm lỗi trên production không còn là ác mộng.
Chuyển sang Rust có thể khiến bạn mất thêm thời gian ở giai đoạn đầu. Tuy nhiên, sự yên tâm về hệ thống và chi phí server được tối ưu triệt để là phần thưởng cực kỳ xứng đáng. Đừng ngần ngại, hãy thử bắt đầu với một microservice nhỏ ngay hôm nay!

