Bối cảnh & Vấn đề thực tế
Khoảng 2 năm trước, mỗi đợt release tính năng mới lên production của team mình đều khá áp lực. Quy trình kinh điển: merge code vào main, kích hoạt CI/CD, build Docker image rồi cập nhật cụm Kubernetes. Nếu chẳng may dính lỗi memory leak hoặc câu query làm khóa bảng PostgreSQL, cả team lại vội vàng revert commit và build lại từ đầu. Quá trình rollback này ngốn mất 15 đến 20 phút. Trong ngần ấy thời gian, hàng trăm đơn hàng đã bị gián đoạn.
Vấn đề cốt lõi: chúng ta thường gộp chung hai khái niệm tách biệt.
- Deployment: Đưa mã nguồn mới lên server chạy an toàn.
- Release: Mở tính năng đó cho người dùng cuối trải nghiệm.
Tách rời hai bước này chính là lý do Feature Flags (hay Feature Toggles) ra đời. Thay vì gom nhánh code cả tháng trời, 5 anh em trong team chuyển sang merge liên tục vào nhánh chính mỗi ngày (Trunk-based Development). Mọi đoạn code mới đều bọc trong một Feature Flag ở trạng thái OFF. Khi muốn test nội bộ hoặc mở thử cho 5% khách hàng, tech lead chỉ cần gạt cờ trên UI. Thời gian xử lý sự cố giảm từ 20 phút xuống còn đúng 1 cú click chuột trong 1 giây.
Về công cụ, LaunchDarkly chạy rất mượt nhưng chi phí từ $8–$10/user/tháng khá tốn kém với startup. Flagsmith cũng ổn, nhưng mình chọn Unleash vì mã nguồn mở, cho phép self-host qua Docker để kiểm soát dữ liệu nội bộ. Điểm cộng lớn nhất là SDK Node.js của Unleash đánh giá cờ trực tiếp trên RAM, latency chỉ dưới 0.1ms cho mỗi lần gọi.
Cài đặt môi trường
Hệ thống gồm hai khối riêng biệt: Unleash Server (UI quản trị và lưu trữ cấu hình) và Unleash Client SDK nhúng trực tiếp trong code Node.js.
1. Chạy Unleash Server bằng Docker Compose
Dưới đây là file docker-compose.yml chuẩn để dựng Unleash kèm cơ sở dữ liệu PostgreSQL 15:
services:
db:
image: postgres:15-alpine
environment:
POSTGRES_DB: unleash
POSTGRES_USER: unleash_user
POSTGRES_PASSWORD: secret_db_password
volumes:
- unleash_db_data:/var/lib/postgresql/data
restart: unless-stopped
unleash:
image: unleashorg/unleash-server:latest
ports:
- "4242:4242"
environment:
DATABASE_URL: postgres://unleash_user:secret_db_password@db:5432/unleash
DATABASE_SSL: "false"
LOG_LEVEL: "warn"
INIT_FRONTEND_API_TOKENS: "default:development.unleash-insecure-frontend-api-token"
INIT_CLIENT_API_TOKENS: "default:development.unleash-insecure-client-api-token"
depends_on:
- db
restart: unless-stopped
volumes:
unleash_db_data:
Khởi động dịch vụ:
docker compose up -d
Sau khoảng 15 giây, mở trình duyệt vào http://localhost:4242. Thông tin đăng nhập mặc định:
- Username:
admin - Password:
unleash4all(hệ thống sẽ yêu cầu đổi mật khẩu mới ngay lần đầu)
2. Cài đặt SDK vào project Node.js
Chạy lệnh cài đặt bộ thư viện cần thiết trong thư mục backend Express:
npm install unleash-client express dotenv
Cấu hình và Tích hợp
1. Tạo API Token trên Unleash
Để server Node.js kéo được trạng thái cờ, bạn cần tạo một token phân quyền:
- Truy cập dashboard Unleash > Settings > API access.
- Chọn New API token.
- Chọn loại Server-side SDK (Client), gán project default và môi trường development.
- Lưu lại chuỗi token hiển thị trên màn hình.
Tiếp theo, vào mục Feature toggles, tạo cờ mới có tên new-checkout-flow.
2. Khởi tạo Unleash Client dạng Singleton
SDK của Unleash hoạt động theo cơ chế In-memory & Background polling. Cứ mỗi vài giây, client ngầm gửi một request kéo toàn bộ rules về lưu tại RAM. Khi API gọi hàm kiểm tra, logic chạy hoàn toàn trên bộ nhớ local. Ứng dụng không phải gửi thêm request HTTP nào ra ngoài, đảm bảo tốc độ phản hồi cực nhanh.
Tạo file src/unleash.js:
const { initialize } = require('unleash-client');
let unleashInstance = null;
function getUnleash() {
if (!unleashInstance) {
unleashInstance = initialize({
url: process.env.UNLEASH_URL || 'http://localhost:4242/api/',
appName: 'payment-service',
environment: process.env.NODE_ENV || 'development',
customHeaders: {
Authorization: process.env.UNLEASH_API_TOKEN || '*:development.unleash-insecure-client-api-token',
},
// Đồng bộ rule mới mỗi 5 giây khi dev (prod nên để 10000 - 15000ms)
refreshInterval: 5000,
// Gửi số liệu thống kê hit/miss về server mỗi 30 giây
metricsInterval: 30000,
});
unleashInstance.on('ready', () => {
console.log('[Unleash] SDK đã nạp rule vào RAM thành công.');
});
unleashInstance.on('error', (err) => {
console.error('[Unleash] Lỗi đồng bộ cấu hình:', err.message);
});
}
return unleashInstance;
}
module.exports = { getUnleash };
3. Áp dụng Flag vào Endpoint thanh toán
Dưới đây là file src/app.js thể hiện cách bọc logic nghiệp vụ bằng flag:
const express = require('express');
const { getUnleash } = require('./unleash');
const app = express();
app.use(express.json());
const unleash = getUnleash();
app.post('/api/checkout', (req, res) => {
const userId = req.headers['x-user-id'] || 'anonymous';
const userEmail = req.headers['x-user-email'] || '';
// Ngữ cảnh để Unleash đánh giá điều kiện phân nhóm
const context = {
userId: userId,
properties: {
email: userEmail,
plan: req.body.plan || 'free',
},
};
// Kiểm tra trạng thái cờ với fallback an toàn là false
const isNewFlowActive = unleash.isEnabled('new-checkout-flow', context, false);
if (isNewFlowActive) {
// Luồng thanh toán Stripe Elements mới
return res.json({
version: 'v2',
gateway: 'stripe-one-click',
orderId: 'ORD-2026-NEW',
});
}
// Luồng thanh toán cũ đã kiểm chứng ổn định
return res.json({
version: 'v1',
gateway: 'standard-redirect',
orderId: 'ORD-2026-LEGACY',
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Backend lắng nghe tại port ${PORT}`);
});
Hãy lưu ý đối số thứ ba trong hàm unleash.isEnabled('new-checkout-flow', context, false). Đây là giá trị fallback an toàn. Nếu Unleash Server gặp sự cố mạng hoặc container bị restart, SDK sẽ trả về ngay false. Ứng dụng tự động dùng luồng cũ mà không ném ra exception hay làm crash tiến trình.
4. Phân phối tính năng với Activation Strategies
Unleash không chỉ có bật/tắt toàn bộ. Giá trị thực tế nằm ở các chiến lược phân phối động cấu hình trực tiếp trên UI:
- Gradual Rollout (Canary testing): Cho phép mở cờ cho 10%, 25%, rồi nâng dần lên 100% người dùng. Thuật toán MurmurHash3 dựa trên
userIdgiữ kết quả nhất quán. User A khi đã thuộc nhóm 10% sẽ luôn trải nghiệm giao diện mới ở các lần đăng nhập sau. - Targeting theo User ID: Chỉ định danh sách tài khoản cụ thể (ví dụ
dev_quang,qa_lan) để đội ngũ nội bộ test trước trên production. - Custom Constraints: Lọc theo điều kiện mở rộng, ví dụ domain email kết thúc bằng
@company.comhoặc app version từ2.4.0trở lên.
Kiểm tra và Vận hành
1. Test chuyển đổi luồng tức thì
Khởi động Express server:
node src/app.js
Gửi request kiểm tra khi cờ new-checkout-flow đang TẮT:
curl -X POST http://localhost:3000/api/checkout \
-H "Content-Type: application/json" \
-H "x-user-id: user_123"
Phản hồi trả về bản v1:
{"version":"v1","gateway":"standard-redirect","orderId":"ORD-2026-LEGACY"}
Tiếp theo, vào dashboard Unleash và gạt cờ sang BẬT. Đợi 5 giây để SDK hoàn tất chu kỳ poll, sau đó chạy lại lệnh curl vừa rồi. Kết quả đổi sang bản v2 ngay lập tức:
{"version":"v2","gateway":"stripe-one-click","orderId":"ORD-2026-NEW"}
Toàn bộ quá trình chuyển luồng hoàn tất trong chớp mắt. Không có kết nối HTTP nào bị ngắt quãng. Không có container nào phải reload.
2. Đo lường hiệu quả qua Tab Metrics
Mỗi lần hàm isEnabled được gọi, SDK tự gom metric và gửi định kỳ về server. Mở chi tiết cờ và chuyển sang tab Metrics, bạn sẽ theo dõi được:
- Số lượt trúng nhánh
Yesso với nhánhNotrong 1 giờ qua. - Lưu lượng request thực tế phân bổ giữa hai phiên bản code.
- Phát hiện sớm các cờ rác đã bật 100% quá lâu nhưng chưa được dọn dẹp khỏi mã nguồn.
3. Ba nguyên tắc tránh biến Feature Flags thành bẫy kỹ thuật
Sau hơn 6 tháng áp dụng thực tế trên hệ thống xử lý khoảng 50.000 đơn/ngày, team mình rút ra 3 bài học đắt giá:
- Quy ước tiền tố tên cờ: Phân loại rõ mục đích ngay từ đầu. Dùng
release-cho tính năng mới cần rollout dần,killswitch-cho công tắc ngắt khẩn cấp khi tích hợp đối tác thứ ba bị lỗi, vàexperiment-cho các thử nghiệm A/B testing ngắn hạn. - Xóa flag triệt để sau khi launch: Flag sinh ra để giảm rủi ro, nhưng nếu để quá lâu sẽ làm phình to nợ kỹ thuật (technical debt). Khi một tính năng đã chạy ổn định 100% trong 2 tuần, hãy tạo ngay ticket trong sprint kế tiếp để xóa khối
if/elsevà gỡ flag khỏi Unleash. - Ưu tiên tính độc lập của flag: Hạn chế tối đa việc lồng các flag vào nhau (nested flags). Code lồng 2–3 tầng cờ tính năng sẽ khiến việc viết unit test và tái hiện bug trở thành cơn ác mộng.
Nhờ Unleash, việc đưa tính năng mới lên production không còn là canh bạc mỗi tối thứ Sáu. Khi mọi rủi ro đều có thể ngắt trong vòng 1 giây, cả đội phát triển có thể tự tin đẩy nhanh tốc độ chuyển giao sản phẩm.
