🚩 Feature flag để tắt tính năng lỗi

By Better Wise

Bài trước bạn đã dựng được E2E test bằng Playwright, đó là lưới an toàn trước khi bấm deploy. Nhưng test chỉ bắt lỗi ở staging. Câu hỏi tôi luôn hỏi trước khi cho ai đó deploy: "2 giờ sáng, tính năng thanh toán mới hỏng trên production, bạn làm gì?". Deploy lại bản cũ mất 10 phút. Bấm một công tắc mất 30 giây. Bài này dạy bạn làm công tắc đó.

Sau bài này, bạn cần làm được:

  • Giải thích feature flag là gì và khi nào dùng nó thay vì rollback
  • Bọc một tính năng rủi ro trong flag, mặc định là tắt
  • Tắt flag trên production mà không deploy lại, và biết vì sao đôi khi "tắt mà không tắt"
  • Chỉ ra 3 cạm bẫy khiến flag thành nợ kỹ thuật
  • Trả lời được 3 câu hỏi trước khi bật flag cho người dùng thật

Feature flag: công tắc đọc lúc runtime

Feature flag là một giá trị cấu hình nằm ngoài code, được đọc lúc runtime, quyết định một nhánh code có chạy hay không. Deploy đưa code mới lên server. Flag quyết định người dùng có đi vào code mới hay không.

Vì giá trị được đọc lúc chạy, bạn đổi nó trên trang quản trị hoặc config service là code đổi hành vi ngay, không build lại, không deploy lại. Một flag chỉ có giá trị khi nó tắt được trong vòng một phút.

Ba loại flag bạn sẽ gặp:

Loại Dùng khi Vòng đời
Release flag che tính năng đang làm dở, chưa muốn lộ ngắn, xoá sau khi ổn định
Kill switch cần tắt ngay một tính năng rủi ro khi có sự cố dài, nằm trong runbook
Experiment flag so sánh hai phiên bản cho một nhóm người dùng ngắn, có ngày kết thúc

Bài này tập trung vào kill switch, vì đó là thứ cứu bạn lúc 2 giờ sáng.

Bài tập nhanh 1. Trong 3 tình huống sau, cái nào nên làm kill switch, cái nào không cần flag? (a) Trang thanh toán mới, chưa test kỹ với thẻ quốc tế. (b) Đổi màu nút bấm từ xanh sang xanh đậm. (c) Job gửi email nhắc nợ tự động, chạy một lần mỗi ngày.

Xem đáp án

(a) Có, đây là ứng viên rõ nhất: tiền của người dùng, rủi ro cao, tắt được ngay là bắt buộc. (b) Không. Rủi ro gần bằng 0, thêm flag chỉ tạo thêm thứ phải dọn. (c) Có, nhưng theo kiểu khác: kill switch dừng job, hoặc cho phép chạy ở chế độ dry-run. Job tự động chạy sai có thể gửi hàng nghìn email sai, không ai kịp sửa bằng tay.

Bọc một tính năng rủi ro trong flag

Nguyên tắc quan trọng nhất: mặc định phải là tắt. Nếu đọc flag bị lỗi, hệ thống phải rơi về nhánh cũ, không phải nhánh mới.

// config/flags.js
const DEFAULTS = { "checkout.newFlow": false }; // false = tắt, fail closed
const TTL_MS = 30_000;
let cache = { values: {}, readAt: 0 };

export async function isOn(name) {
  if (!(name in DEFAULTS)) return false;          // flag lạ = tắt
  if (Date.now() - cache.readAt > TTL_MS) {
    cache = { values: await fetchFlags(), readAt: Date.now() };
  }
  const v = cache.values[name];
  return v === undefined ? DEFAULTS[name] : v === true;
}

Dùng nó ở đúng một chỗ quyết định, và giữ nhánh cũ chạy được:

app.post("/checkout", async (req, res) => {
  if (await isOn("checkout.newFlow")) return newCheckout(req, res);
  return legacyCheckout(req, res); // đường lui, phải còn chạy tốt
});

Bốn quy tắc tôi kiểm tra trong review:

  • Tên flag theo hành vi (checkout.newFlow), không theo người (checkout.tuan)
  • Không có flag nào thì trả về false, không bao giờ ném lỗi
  • Nhánh cũ không bị xoá, không bị sửa hỏng
  • Mỗi lần đọc flag, ghi log trạng thái để lúc debug biết user đi nhánh nào

Bài tập nhanh 2. Đoạn code này sai ở đâu trên production?

if (process.env.NEW_CHECKOUT !== "false") {
  return newCheckout(req, res);
}
return legacyCheckout(req, res);

Xem đáp án

Biến môi trường chưa được set trên server mới thì giá trị là undefined, và undefined !== "false" là true. Kết quả: tính năng mới chạy dù bạn chưa hề bật nó. Đây là lỗi default ngược, nguy hiểm hơn cả việc không có flag.

Sửa: so sánh dương bản rõ ràng, chỉ bật khi giá trị đúng bằng "true", còn lại đều là tắt.

const newFlowOn = process.env.NEW_CHECKOUT === "true";

Tắt flag trên production trong một phút

Khi có sự cố, đừng sửa code. Làm theo thứ tự này và ghi lại từng bước:

  1. Xác nhận sự cố đến từ tính năng nào (log, Sentry, báo cáo người dùng).
  2. Tắt flag của tính năng đó trên trang quản trị production.
  3. Chờ hết TTL cache (ví dụ 30 giây), rồi chạy lại smoke test.
  4. Ghi vào kênh sự cố: giờ tắt, người tắt, lý do, kết quả smoke test.
  5. Mở ticket dọn flag sau khi bug được sửa và tính năng được bật lại.
flowchart TD
    A["🚨 Sự cố trên production"] --> B{"Tính năng mới nằm sau flag?"}
    B -- Có --> C["🔴 Tắt flag trong config"]
    B -- Không --> D["↩️ Rollback bản deploy trước"]
    C --> E["🧪 Chạy smoke test"]
    D --> E
    E --> F{"Hết lỗi?"}
    F -- Chưa --> D
    F -- Rồi --> G["📝 Ghi log và mở ticket"]

Ba lý do khiến "tắt mà không tắt", hãy kiểm tra trước khi bật flag:

Warning: Nếu app đọc flag từ biến môi trường lúc khởi động, tắt flag phải restart service. Như vậy không còn là tắt tức thì. Muốn tắt trong 30 giây, flag phải được đọc từ config service hoặc database với TTL ngắn, không phải hằng số trong bundle.

Cache phía client và CDN cũng tính vào thời gian đó. Nếu trang HTML được cache 5 phút ở CDN, người dùng vẫn thấy tính năng lỗi trong 5 phút.

Bài tập nhanh 3. Bạn tắt flag trên dashboard, đợi 2 phút, lỗi vẫn còn. Nêu 3 chỗ cần kiểm tra theo thứ tự.

Xem đáp án

  1. App có đọc lại config không, hay chỉ đọc lúc boot? Xem log xem có dòng đọc flag gần nhất lúc nào.
  2. Cache: CDN hoặc trình duyệt vẫn giữ HTML/JS bản cũ, thử hard reload hoặc kiểm tra header cache.
  3. Nhiều instance: chỉ một pod nhận config mới, các pod còn lại vẫn chạy config cũ.

Nếu cả ba đều ổn mà lỗi vẫn còn, giả định cuối cùng là tính năng đó không thật sự nằm sau flag. Lúc đó mới dùng rollback.

Những chỗ release engineer hay bị flag phản tác dụng

  • Flag đặt sai chỗ. Bọc ở UI mà quên bọc ở API thì người dùng không thấy nút, nhưng job nền vẫn gọi endpoint đó. Bọc cả hai phía, hoặc bọc ở tầng service dùng chung.
  • Flag mặc định bật. Config service sập thì tính năng rủi ro tự bật lên. Với tính năng đụng tiền hoặc dữ liệu người dùng, mặc định phải là tắt.
  • Không có ngày hết hạn. Mỗi flag là một nhánh if vĩnh viễn trong code. Đặt ngày hết hạn ngay khi tạo flag và đưa việc dọn flag vào checklist go-live.
  • Ai cũng bật tắt được. Chỉ nhóm trực release có quyền, và mọi lần đổi phải có log audit: ai, lúc nào, flag nào, giá trị gì.
  • Không kiểm tra lại sau khi tắt. Tắt flag không kèm smoke test thì bạn chỉ đang tin vào dashboard.
  • Quy tắc của tôi: nếu bạn không trả lời được "tắt flag này bằng cách nào, mất bao lâu" thì tính năng đó chưa được phép deploy.

Chốt lại và bước tiếp theo

  • Feature flag là công tắc cấu hình, cho phép tắt tính năng lỗi trên production trong vài chục giây, không cần deploy lại.
  • Muốn tắt tức thì, flag phải được đọc từ config service hoặc database với TTL ngắn, không phải hằng số nằm trong bundle.
  • Tính năng rủi ro cần kill switch, mặc định tắt, có ngày hết hạn và có log audit.
  • Tắt flag là bước đầu của xử lý sự cố, luôn đi kèm smoke test và ghi log.

Feature flag cứu bạn khi lỗi nằm gọn trong một tính năng. Nhưng khi lỗi đến từ migration database hoặc cấu hình hạ tầng, công tắc đó không giúp được gì. Bài tiếp theo, Sao lưu và rollback trước khi deploy, sẽ trả lời câu hỏi mà tôi luôn hỏi trước khi cho bấm deploy: nếu bước này hỏng thì dữ liệu có lấy lại được không.

Front: Feature flag là gì? Back: Công tắc cấu hình cho phép bật hoặc tắt một tính năng trên production mà không cần deploy lại.

Front: Vì sao feature flag nhanh hơn rollback khi có sự cố? Back: Tắt flag mất vài chục giây, không mất dữ liệu và không ảnh hưởng các tính năng khác trong cùng bản deploy.

Front: Điều kiện để flag tắt gần như tức thì là gì? Back: App đọc flag từ config service hoặc database với TTL ngắn, không phải hằng số nằm trong bundle.

Front: TTL của config nghĩa là gì? Back: Thời gian app cache giá trị config trước khi đọc lại.

Front: Vì sao tính năng rủi ro nên mặc định tắt? Back: Nếu config service lỗi, tính năng đó không tự bật lên ngoài ý muốn.

Front: Kill switch dùng cho trường hợp nào? Back: Tính năng rủi ro cao như thanh toán, gửi email, gọi API bên thứ ba, cần tắt khẩn cấp.

Front: Release flag khác kill switch ở điểm nào? Back: Release flag chỉ để phát hành dần tính năng mới rồi xoá đi, kill switch giữ lâu để tắt khẩn cấp.

Front: Vì sao mỗi flag cần ngày hết hạn? Back: Flag tồn tại lâu tạo thêm nhánh code vĩnh viễn, khó test và dễ gây nợ kỹ thuật.

Front: Ai nên có quyền đổi flag trên production? Back: Chỉ nhóm trực release, và mọi lần đổi phải được ghi log audit.

Front: Sau khi tắt flag phải làm gì ngay? Back: Chờ hết TTL, chạy lại smoke test và ghi log sự cố.

Front: Ba nguyên nhân khiến tắt flag mà lỗi vẫn còn? Back: App chỉ đọc config lúc khởi động, cache CDN hoặc trình duyệt, và nhiều instance chưa nhận config mới.

Front: Bọc flag ở UI đã đủ chưa? Back: Chưa, phải bọc cả API hoặc job nền vì các luồng đó vẫn gọi thẳng endpoint.

Feature flag cho phép tắt tính năng lỗi mà không cần _____. Muốn tắt gần như tức thì, app phải đọc flag từ config service với _____ ngắn. Với tính năng đụng tiền hoặc dữ liệu người dùng, giá trị mặc định của flag phải là _____. Mỗi flag cần có _____ để tránh nợ kỹ thuật. Ngay sau khi tắt flag, phải chạy lại _____ để xác nhận production đã ổn.