Product Management
Đăng nhập
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Bài 46 — Feature Flags — Trunk-based Enabler

Đã đọc 0/15 mục · còn khoảng 12 phút
Cách chúng tôi ghi nhận

Một mục được tính là đã đọc khi nó nằm giữa màn hình đủ lâu so với độ dài của nó (bảng và sơ đồ tính một nửa), bạn vẫn đang cuộn hoặc bấm phím, và bạn đã thấy tới cuối mục. Lướt nhanh hoặc để tab mở không được tính. Chúng tôi chỉ lưu thời gian đọc từng mục — không lưu gì khác.

Mở đầu — vì sao bài này quan trọng

Hãy tưởng tượng bạn là DevOps engineer tại một công ty fintech ở TP.HCM. Nhóm dev quyết định theo trunk-based development: mọi người merge code vào nhánh main mỗi ngày, thậm chí nhiều lần trong ngày, thay vì ôm một nhánh feature dài cả tuần rồi mới merge. Nghe rất hợp lý — merge nhỏ, ít conflict, CI luôn xanh.

Nhưng ngay lập tức bạn vấp phải một câu hỏi khó chịu: nếu code của tính năng "chuyển tiền quốc tế" mới merge vào main mà tính năng đó còn dang dở — mới xong phần backend, chưa xong phần kiểm tra hạn mức, chưa test kỹ — thì khi deploy lên production, chẳng lẽ người dùng thật sẽ thấy một tính năng nửa vời? Họ bấm vào nút "Chuyển tiền quốc tế" và app crash? Đó là kịch bản ác mộng.

Đây chính là mâu thuẫn cốt lõi mà mọi đội theo trunk-based đều gặp: muốn merge liên tục nhưng không muốn release liên tục những thứ chưa sẵn sàng. Feature Flags (còn gọi là feature toggles) là kỹ thuật giải quyết mâu thuẫn này. Nó tách rời hai khái niệm mà lâu nay ta hay nhầm là một: deploy (đưa code lên máy chủ) và release (bật tính năng cho người dùng thấy).

Nắm vững feature flags, bạn mở khóa được cả một loạt năng lực: deploy an toàn giữa ban ngày, thử nghiệm A/B, rollout dần dần theo phần trăm người dùng, và tắt tính năng lỗi trong vài giây mà không cần deploy lại. Đây là một trong những kỹ năng phân biệt giữa một đội "biết dùng CI/CD" và một đội thực sự làm chủ dòng chảy phát hành phần mềm.

Khái niệm cốt lõi

Feature Flag là gì

Feature flag về bản chất là một câu lệnh điều kiện (if/else) bao quanh một đoạn code, và điều kiện đó được điều khiển từ bên ngoài — thường là từ một cấu hình, một biến môi trường, hoặc một dịch vụ quản lý flag tập trung.

if (featureFlags.isEnabled("international_transfer", user)) {
  showInternationalTransferButton();
} else {
  // giữ nguyên hành vi cũ, người dùng không thấy gì bất thường
}

Điểm mấu chốt: đoạn code của tính năng "chuyển tiền quốc tế" đã nằm sẵn trên production, đã được deploy, nhưng vì flag đang tắt nên không ai thấy. Khi tính năng hoàn thiện và test xong, bạn chỉ cần bật flag từ dashboard — không deploy, không restart, tính năng xuất hiện tức thì.

Deploy khác Release

Đây là tư tưởng quan trọng nhất của bài. Trước đây, deploy và release là một: bạn đẩy code lên là người dùng thấy ngay. Feature flag tách đôi:

  • Deploy: mã nguồn đã chạy trên máy chủ production, nhưng có thể đang "ngủ" sau flag tắt.
  • Release: quyết định kinh doanh — khi nào, cho ai, bao nhiêu phần trăm người dùng được thấy tính năng.
Nhờ tách đôi, đội kỹ thuật deploy theo nhịp của mình (mỗi ngày, mỗi giờ), còn đội sản phẩm release theo nhịp của thị trường (đúng ngày ra mắt chiến dịch, đúng khung giờ ít tải).

Các loại feature flag

Không phải flag nào cũng giống nhau. Martin Fowler phân loại thành bốn nhóm mà bạn nên phân biệt rõ vì vòng đời của chúng rất khác nhau:

  • Release toggles — flag để ẩn tính năng chưa xong trong trunk-based development. Sống ngắn, phải xóa ngay sau khi tính năng ra mắt ổn định. Đây là loại liên quan trực tiếp đến bài học này.
  • Experiment toggles — dùng cho A/B testing, chia người dùng thành nhóm để đo lường phiên bản nào tốt hơn. Sống theo thời gian thí nghiệm.
  • Ops toggles — công tắc vận hành, ví dụ tắt tính năng gợi ý AI khi hệ thống quá tải để giảm tải. Có thể sống rất lâu như một "cầu dao" (kill switch).
  • Permission toggles — bật tính năng cho nhóm người dùng cụ thể, ví dụ chỉ khách VIP mới thấy. Có thể sống vĩnh viễn vì nó là logic nghiệp vụ.

Trunk-based development và mối liên hệ

Trunk-based development khuyến khích merge nhỏ và thường xuyên vào main. Nhưng tính năng lớn không thể xong trong một lần commit. Feature flag chính là "chất xúc tác" (enabler) cho phép bạn merge từng mảnh nhỏ của một tính năng lớn vào main — mỗi mảnh đều nằm sau flag tắt — cho tới khi ghép đủ mảnh thì bật flag. Không có feature flag, trunk-based development gần như bất khả thi với các tính năng lớn.

Rollout theo phần trăm (Progressive Delivery)

Flag không chỉ là bật/tắt nhị phân. Một hệ thống flag tốt cho phép bật cho 1% người dùng trước, quan sát metrics và error rate, rồi tăng dần lên 5%, 25%, 50%, 100%. Đây gọi là progressive rollout hay canary release ở tầng ứng dụng. Nếu 1% đầu tiên phát sinh lỗi, bạn tắt flag ngay, chỉ 1% người dùng bị ảnh hưởng trong vài phút.

Tình huống thực tế

Ví dụ 1 — Tiki và tính năng thanh toán mới mùa sale

Bối cảnh: Một sàn thương mại điện tử lớn như Tiki chuẩn bị ra mắt phương thức thanh toán trả góp mới đúng dịp sale 11/11. Đội backend đã viết xong logic tích hợp với đối tác tài chính từ ba tuần trước, nhưng không thể để tính năng "rò rỉ" cho người dùng trước ngày công bố chính thức vì lý do hợp đồng marketing.

Diễn giải: Đội bọc toàn bộ luồng trả góp sau một release toggle tên installment_payment_v2. Suốt ba tuần, code này được deploy lên production hàng ngày cùng các bản build khác, chạy song song, thậm chí đội QA bật flag riêng cho tài khoản test của họ để kiểm thử trên chính môi trường production. Đúng 0h ngày 11/11, một người trong đội vận hành bật flag từ dashboard cho 10% người dùng, theo dõi 15 phút thấy tỉ lệ giao dịch thành công ổn định, rồi tăng lên 100%. Không có một lần deploy nào diễn ra vào giờ cao điểm nhạy cảm đó.

Bài học: Tách deploy khỏi release cho phép đội kỹ thuật hoàn tất mọi việc rủi ro (deploy, test trên production) trước, và biến khoảnh khắc "ra mắt" thành một thao tác bấm nút an toàn, có thể đảo ngược. Nếu 10% đầu tiên gặp sự cố với đối tác tài chính, họ tắt flag và không ai ngoài 10% đó bị ảnh hưởng trong ít phút.

Ví dụ 2 — Startup SaaS ở Singapore và kill switch cứu một đêm on-call

Bối cảnh: Một startup SaaS B2B tại Singapore triển khai tính năng export báo cáo PDF nặng, dùng nhiều CPU. Tính năng đã ra mắt ổn cả tháng. Một đêm, có khách hàng lớn export đồng loạt hàng nghìn báo cáo, đẩy CPU của cụm server lên 95%, các request khác bắt đầu chậm và timeout.

Diễn giải: Rất may, ngay từ đầu đội đã đặt một ops toggle pdf_export_enabled bọc quanh tính năng này như một kill switch. Kỹ sư on-call không cần hiểu code, không cần rollback deploy (vốn mất 10-15 phút và có rủi ro riêng). Anh ta chỉ mở dashboard flag, tắt pdf_export_enabled trong 5 giây. Tính năng export tạm hiển thị thông báo "đang bảo trì", CPU hạ nhiệt ngay, các khách hàng khác không còn bị ảnh hưởng. Sáng hôm sau đội bình tĩnh thêm cơ chế hàng đợi (queue) giới hạn số lượng export đồng thời rồi mới bật lại flag.

Bài học: Ops toggle đóng vai trò cầu dao khẩn cấp, giúp giảm MTTR (thời gian trung bình khôi phục sự cố) từ hàng chục phút xuống vài giây. Với tính năng có rủi ro về tài nguyên hoặc phụ thuộc bên thứ ba, hãy chủ động cài kill switch ngay từ đầu, đừng đợi đến khi cháy nhà.

Ví dụ 3 — Đội nội bộ và bài học "flag zombie"

Bối cảnh: Một đội sản phẩm tại một công ty công nghệ ở Hà Nội dùng feature flag rất tích cực. Sau hai năm, codebase tích tụ hơn 400 flag, trong đó khoảng 250 flag là release toggle đã bật 100% từ lâu nhưng chưa ai xóa.

Diễn giải: Một hôm, một dev vô tình sửa cấu hình, tắt nhầm một flag zombie tưởng đã chết nhưng thực ra code cũ vẫn còn nhánh else. Kết quả là một tính năng đang chạy bỗng biến mất trên production, gây hoang mang. Ngoài ra, mỗi hàm phải đọc qua 5-6 tầng if/else lồng nhau vì flag chồng flag, khiến việc đọc code và viết test trở nên cực khổ. Test phải cover đủ tổ hợp bật/tắt của nhiều flag, số lượng ca test bùng nổ theo cấp số nhân.

Bài học: Feature flag là món nợ kỹ thuật (technical debt) nếu không dọn dẹp. Release toggle phải có "ngày hết hạn". Nhiều đội đặt quy tắc: mỗi release flag khi tạo ra phải gắn một ticket dọn dẹp và một hạn chót; CI thậm chí cảnh báo khi phát hiện flag quá cũ. "Tạo flag thì dễ, xóa flag mới khó" — hãy kỷ luật ngay từ đầu.

Hướng dẫn từng bước

Giả sử bạn cần đưa một tính năng lớn vào production theo trunk-based với feature flag. Đây là quy trình từng bước.

Bước 1 — Chọn công cụ quản lý flag. Ở quy mô nhỏ, một biến môi trường hoặc file config đơn giản là đủ. Khi cần rollout theo phần trăm, targeting theo người dùng, bật/tắt runtime không cần deploy, hãy dùng dịch vụ chuyên dụng: LaunchDarkly (thương mại, mạnh nhất), Unleash (open-source, tự host được — rất hợp với công ty VN muốn kiểm soát dữ liệu), Flagsmith, hoặc Flipt. Với đội mới bắt đầu, tôi khuyên Unleash self-hosted vì miễn phí và đủ tính năng.

Bước 2 — Đặt tên flag rõ ràng và khai báo tập trung. Dùng quy ước nhất quán, ví dụ int_transfer_v2, kèm mô tả, chủ sở hữu (owner), và loại flag (release/ops/experiment). Đừng rải chuỗi tên flag rác khắp code.

Bước 3 — Bọc code sau flag, mặc định TẮT. Viết code tính năng mới trong nhánh if (flag on), còn nhánh else giữ nguyên hành vi cũ. Điểm quan trọng: mặc định phải là tắt, để khi deploy production không có gì thay đổi với người dùng.

if flags.is_enabled("int_transfer_v2", context=user):
    return new_transfer_flow(request)
else:
    return legacy_transfer_flow(request)

Bước 4 — Merge vào main và deploy bình thường. Vì flag tắt, bạn cứ merge từng mảnh nhỏ mỗi ngày, CI xanh, deploy production đều đặn mà chẳng ai thấy tính năng dở dang.

Bước 5 — Test trên production bằng cách bật flag cho nội bộ. Bật flag chỉ cho nhóm tài khoản QA/nhân viên (targeting theo user ID hoặc email domain). Đây là "test in production" an toàn.

Bước 6 — Rollout dần theo phần trăm. Bật 1% → theo dõi error rate, latency, business metrics → 5% → 25% → 100%. Mỗi nấc dừng lại quan sát. Có sự cố thì tắt ngay.

Bước 7 — Dọn dẹp flag. Khi flag đã 100% và ổn định vài tuần, xóa flag, xóa nhánh else code cũ, biến logic mới thành đường đi mặc định. Đóng ticket. Flag chết đúng lúc.

Lỗi thường gặp & mẹo

Lỗi 1 — Không bao giờ dọn flag. Đây là lỗi phổ biến nhất, dẫn tới "flag zombie" như ví dụ 3. Mẹo: mỗi release flag tạo ra kèm một ticket xóa và hạn chót; định kỳ rà soát flag cũ.

Lỗi 2 — Lồng flag quá sâu. Flag trong flag trong flag khiến code không thể đọc và test bùng nổ tổ hợp. Mẹo: giữ flag ở tầng cao (entry point của tính năng), tránh rải rác vào từng hàm nhỏ.

Lỗi 3 — Mặc định bật thay vì tắt. Nếu flag mới mà mặc định bật, khi hệ thống quản lý flag lỗi hoặc chưa kịp cấu hình, tính năng dở dang lộ ra production. Mẹo: luôn fail-safe về hành vi cũ khi không đọc được flag.

Lỗi 4 — Quên xử lý khi dịch vụ flag chết. Nếu dùng LaunchDarkly/Unleash mà dịch vụ đó down, code của bạn phải có giá trị mặc định (fallback) hợp lý, không được crash. Mẹo: SDK các dịch vụ này đều hỗ trợ default value và cache local — hãy cấu hình đúng.

Lỗi 5 — Đưa logic nghiệp vụ dài hạn vào release toggle. Release toggle phải chết sớm; nếu bạn dùng nó để phân quyền vĩnh viễn thì nên đổi thành permission toggle và tách bạch rõ. Mẹo: phân loại flag ngay khi tạo.

Mẹo bổ sung — Đo lường qua flag. Gắn flag với hệ thống metrics: khi rollout 5%, bạn phải so sánh được nhóm bật flag và nhóm không bật về tỉ lệ lỗi, thời gian phản hồi, tỉ lệ chuyển đổi. Đây là cầu nối giữa feature flag và văn hóa đo lường của DevOps.

Bài tập thực hành

  • Cài đặt cơ bản: Dựng một Unleash server bằng Docker Compose ở máy local, tạo một flag tên new_checkout mặc định tắt, và tích hợp SDK vào một ứng dụng web nhỏ (Node.js hoặc Python) để nút "Thanh toán mới" chỉ hiện khi flag bật.
  • Rollout theo phần trăm: Cấu hình flag new_checkout rollout 20% người dùng dựa trên user ID (dùng cơ chế gradual rollout của Unleash). Viết một script mô phỏng 1000 user và xác nhận xấp xỉ 200 user thấy tính năng mới.
  • Kill switch: Thêm một ops toggle heavy_report_enabled bọc quanh một hàm giả lập tốn tài nguyên. Viết tài liệu runbook ngắn mô tả on-call sẽ tắt flag này thế nào khi hệ thống quá tải.
  • Phân tích flag zombie: Cho một đoạn code có 3 flag lồng nhau, hãy chỉ ra tất cả tổ hợp trạng thái cần test, và đề xuất cách refactor để giảm độ phức tạp sau khi 2 trong 3 flag đã ra mắt ổn định.
  • Viết chính sách: Soạn một chính sách quản lý feature flag cho đội của bạn: quy ước đặt tên, phân loại, quy tắc gắn owner, và quy trình dọn dẹp có hạn chót.

Tóm tắt

Feature flag là kỹ thuật bọc code sau một công tắc điều khiển từ bên ngoài, cho phép tách deploy khỏi release — đưa code lên production mà chưa cần cho người dùng thấy. Đây chính là chất xúc tác (enabler) làm cho trunk-based development khả thi: bạn merge từng mảnh nhỏ của tính năng lớn vào main mỗi ngày, mỗi mảnh nằm sau flag tắt, rồi bật khi hoàn thiện.

Có bốn loại flag với vòng đời khác nhau: release toggle (ngắn, phải dọn), experiment toggle (A/B testing), ops toggle (kill switch vận hành) và permission toggle (phân quyền). Flag mở ra progressive rollout theo phần trăm, test an toàn trên production, và khả năng tắt sự cố trong vài giây — giảm mạnh thời gian khôi phục.

Cạm bẫy lớn nhất là không dọn flag, biến chúng thành nợ kỹ thuật gây rối code và bùng nổ ca test. Nguyên tắc vàng: mặc định tắt để fail-safe, phân loại flag khi tạo, gắn owner và hạn chót dọn dẹp, và luôn có fallback khi dịch vụ flag gặp sự cố. Nắm chắc feature flag, bạn có trong tay một trong những công cụ mạnh nhất để phát hành phần mềm vừa nhanh vừa an toàn.

Học xong bài này rồi? Tạo tài khoản miễn phí để lưu lại — lần sau vào là biết ngay đang dở ở đâu, và học hết khóa thì có chứng chỉ. Lưu tiến độ của tôi