Tự xây dựng WebAssembly Runtime trong 5 ngày để thoát khỏi gánh nặng chi phí đám mây
Mệt mỏi vì chi phí Cloud Run cao ngất ngưởng, tác giả đã tự tay xây dựng một nền tảng sandbox đa người thuê (multi-tenant) tên là Badwater dựa trên WebAssembly và Rust. Hệ thống này tận dụng bubblewrap để cô lập an toàn và vận hành hiệu quả trên một chiếc VPS giá rẻ chỉ 8 USD/tháng.
Là đồng sáng lập một startup phần cứng âm thanh, chúng tôi cần hạ tầng để ký firmware, kích hoạt thiết bị và phân phối cập nhật OTA. Tôi đã xem xét các lựa chọn từ các ông lớn đám mây như AWS KMS hay Azure, nhưng mức giá cả là không hợp lý đối với một startup tự túc vốn. Hàng trăm đô la mỗi tháng cho những khối lượng công việc tiêu tốn rất ít tài nguyên.
Với nền tảng bảo mật nhúng (embedded security) trước đây, tư duy của tôi là: mọi tầng đều có thể thất bại, vì vậy phải phòng thủ theo từng lớp và xác minh mọi thứ. WebAssembly (WASM) dường như là công cụ hoàn hảo: được thiết kế để chạy trong sandbox, biên dịch thành binary nhỏ gọn và WASI Preview 2 vừa hỗ trợ mạng thực tế. Mục tiêu của tôi là xây dựng một phiên bản tối giản của Cloudflare Workers, tự host trên phần cứng phổ thông. Tôi gọi dự án này là Badwater.
Ngày 1 — API không hoạt động như hướng dẫn
Tôi chưa từng sử dụng Wasmtime trước đây. Mặc dù biết Rust và Linux, nhưng mô hình thành phần WASM hoàn toàn mới. Vấn đề đầu tiên gặp phải là sự khác biệt giữa core modules và WASM components. Khi làm việc với components, Func::wrap không hoạt động; bạn cần wasmtime::component::Linker.
Sau khi vượt qua vấn đề này, tôi gặp lỗi panic từ Tokio: "Cannot start a runtime from within a runtime". Wasmtime gọi block_on nội bộ, gây xung đột khi chạy trong thread async của Axum. Giải pháp là sử dụng tokio::task::spawn_blocking để chuyển toàn bộ việc thực thi WASM sang một thread blocking, tách biệt khỏi async runtime bên ngoài.
Ngày 2 — Suy nghĩ về những gì có thể sai
Một runtime WASM hoạt động là một chuyện, nhưng một runtime đa người thuê (multi-tenant) là chuyện khác. Câu hỏi lớn là cô lập: nếu mã của người thuê A bị crash, nó có ảnh hưởng đến người thuê B không?
Điều này dẫn đến quyết định kiến trúc: hai binary riêng biệt.
- badwater-dispatcher: Máy chủ HTTP, xử lý request, tải WASM từ bộ nhớ.
- badwater-runner: Trình thực thi WASM, chạy dưới dạng một tiến trình riêng biệt cho mỗi request, giao tiếp qua Unix socket.
Nếu runner bị crash, dispatcher hoàn toàn không bị ảnh hưởng vì chúng không chia sẻ bộ nhớ. Tuy nhiên, sandbox của WASM thôi chưa đủ. Tôi cần lớp thứ hai và tìm thấy bubblewrap — công cụ sandbox mà Flatpak sử dụng. Nó thiết lập Linux namespaces, loại bỏ các quyền (capabilities) và cung cấp root tmpfs mới.
Ngày 3 — Bubblewrap không thích bị ra lệnh cấm đoán
Việc khiến bubblewrap hoạt động thực tế đã chiếm phần lớn ngày 3. Lần thử đầu tiên thất bại với lỗi "Permission denied" khi truy cập /dev/null. Lý do là --ro-bind / / không bind đệ quy thư mục /dev. Giải pháp là thêm cờ --dev /dev.
Tiếp theo, DNS bị vỡ vì sandbox không thấy /etc/resolv.conf. Tôi phải bind mount nó một cách rõ ràng. Sau đó, khi chuyển sang Docker, tôi gặp lỗi về namespace do profile seccomp mặc định của Docker chặn syscall CLONE_NEWUSER. Giải pháp là chạy container với --privileged.
Đến cuối ngày 3, toàn bộ pipeline đã chạy end-to-end thành công với runner nhìn thấy chính nó là PID 2 bên trong namespace mới.
Ngày 4 — Vấn đề JIT
Khi triển khai lên Cloud Run, thời gian cold start lên tới ~2500ms — quá chậm. Wasmtime đang JIT-compile lại thành phần WASM trên mọi request. Giải pháp là biên dịch trước (pre-compile) sang file .cwasm (native machine code) bằng wasmtime compile.
Việc này giúp giảm thời gian thực thi từ 2081ms xuống còn 117ms. Tuy nhiên, hệ thống sau đó gặp lỗi ngắt quãng: "compilation setting 'has_avx512bitalg' is enabled, but not available on the host". Tôi đã biên dịch trên máy bàn cá nhân hỗ trợ AVX-512, nhưng các instance Cloud Run cũ hơn thì không.
Giải pháp là cấu hình Cranelift nhắm đến mục tiêu x86_64-unknown-linux-musl chung thay vì CPU máy chủ cụ thể. Sau đó, thời gian thực thi server-side ổn định dưới 100ms.
Ngày 5 — 8 USD/tháng, tên miền trực tiếp
Cloud Run tốt cho phát triển nhưng giá thành không hợp lý khi mở rộng quy mô. Tôi chuyển sang một VPS 4-core, 8GB từ OVH với giá 7,60 USD/tháng. VPS không có Workload Identity của GCP, nên tôi chuyển backend lưu trữ sang Cloudflare R2 (tương thích S3).
Tôi đã tự viết giao thức ký AWS SigV4 bằng 242 dòng Rust mà không cần SDK nặng nề. Sau khi triển khai lên OVH và đặt Cloudflare Tunnel phía trước, tôi có tên miền badwater.app.
Các số liệu cuối cùng từ triển khai thực tế rất ấn tượng:
- Health-check: Tổng thời gian server-side chỉ 9ms.
- Trace.cwasm (4.9MB): Tổng thời gian server-side 182ms (bao gồm lấy từ R2 và TLS handshake).
Kết quả và Thách thức tiếp theo
Toàn bộ nền tảng chỉ gồm 2.270 dòng Rust, dung lượng ảnh 16MB và chạy trên VPS 8 USD. Tuy nhiên, để trở thành một nền tảng multi-tenant thực sự cho người lạ, vẫn còn những thách thức lớn:
- Warm Pool: Hiện tại mỗi request kích hoạt fork() và khởi tạo sandbox, tốn 9ms. Cần một nhóm runner đã khởi động sẵn để đạt độ trễ dưới mili-giây.
- Vấn đề SSRF Metadata: Bubblewrap cô lập tiến trình nhưng chia sẻ khả năng định tuyến mạng. Một người thuê ác ý có thể truy cập endpoint metadata (169.254.169.254) của nhà cung cấp đám mây để đánh cắp IAM credentials. Cần cô lập network namespace.
- Tiered Image Caching: Cần chiến lược cache đa tầng (RAM -> SSD -> Object Storage) để tránh tải lặp lại binary từ internet và tốn băng thông.
Mã nguồn của dự án Badwater đã được mở nguồn dưới giấy phép GPL-3.0-or-later.
Source: github.com/peterw22/badwater
Bài viết liên quan
Công nghệ
Microsoft công bố mã nguồn của phiên bản DOS sớm nhất từng được phát hiện
30 tháng 4, 2026

Phần mềm
Proxy-Pointer RAG: Cách tiếp cận mới cho câu trả lời đa phương thức không cần Multimodal Embeddings
30 tháng 4, 2026

Phần mềm
Light Phone mở cửa cho nhà phát triển: Biến điện thoại "ngu" trở nên hữu ích hơn với các công cụ tùy chỉnh
30 tháng 4, 2026
