Mitigation Engineering: Rollback, Feature Flag, Circuit Breaker
Tại sao chủ đề này quan trọng
Khi incident đã bước sang trạng thái Mitigation (xem file 02), câu hỏi thực sự không phải là "chúng ta có công cụ gì" — hầu hết tổ chức đều đã có rollback pipeline, feature flag system, hoặc service mesh. Câu hỏi thật là: công cụ nào phù hợp với đúng loại lỗi đang xảy ra, và nó hoạt động ở tầng nào trong hệ thống. Chọn sai công cụ mitigation không chỉ lãng phí thời gian — nó có thể làm incident tệ hơn. Rollback một service khi nguyên nhân thực sự nằm ở một dependency bên ngoài không giải quyết được gì. Retry một request tới một backend đã quá tải sẽ đẩy backend đó sập nhanh hơn. Hiểu đúng cơ chế của từng công cụ là điều kiện để chọn đúng công cụ dưới áp lực thời gian.
Ba cơ chế mitigation phổ biến nhất — rollback, feature flag, circuit breaker — hoạt động ở ba tầng hoàn toàn khác nhau của hệ thống: tầng deployment artifact, tầng runtime configuration, và tầng network call giữa các service. Sự khác biệt về tầng này quyết định tốc độ kích hoạt, phạm vi ảnh hưởng, và quan trọng nhất là loại lỗi mà mỗi cơ chế thực sự có thể chữa được.
Internal model: Rollback — tầng deployment artifact
Vì sao rollback nhanh khi artifact là immutable
Cơ chế nền tảng khiến rollback trở thành một thao tác an toàn và (tương đối) nhanh là tính bất biến của artifact đã triển khai. Trên Cloud Run, mỗi lần deploy tạo ra một revision mới hoàn toàn độc lập, và revision cũ không bị xóa hay ghi đè — nó vẫn chạy được nếu traffic được trỏ lại vào đó. Vì vậy, rollback trên Cloud Run về bản chất không phải là "build lại phiên bản cũ" mà chỉ đơn giản là đổi tỷ lệ định tuyến traffic giữa các revision đã tồn tại sẵn:
gcloud run services update-traffic myservice --to-revisions=REVISION_CU=100Vì revision cũ đã chạy ổn định trong production trước đó (đã qua thử lửa thật với traffic thật), rollback bằng cách chuyển traffic gần như tức thời và có xác suất thành công rất cao — nó không phải là một deploy mới với rủi ro chưa biết, mà là quay lại một trạng thái đã được validated.
Cloud Deploy: rollback không phải là "tua ngược thời gian" mà là một rollout mới
Đây là một chi tiết cơ chế dễ bị hiểu sai: khi bạn rollback một target trong Cloud Deploy, hệ thống không phục hồi trạng thái cũ tức thời — nó "tạo ra một rollout resource mới dựa trên một release trước đó". Nói cách khác, rollback trong Cloud Deploy chạy lại toàn bộ pipeline deploy cho phiên bản cũ, bao gồm cả các bước verification job và approval gate như một lần deploy bình thường. Mặc định, nó dùng "release cuối cùng có rollout thành công" tới target đó, nhưng có thể chỉ định release khác:
gcloud deploy targets rollback TARGET_NAME \
--delivery-pipeline=PIPELINE_NAME \
--release=RELEASE_NAME \
--rollout-id=ROLLOUT_IDHệ quả thực tế cần lưu ý: nếu pipeline có approval gate thủ công, rollback qua Cloud Deploy sẽ đứng chờ approval giống như một deploy thông thường — nó không phải một nút "khẩn cấp bỏ qua toàn bộ quy trình". Nếu tổ chức muốn rollback tức thời trong tình huống SEV1 mà không chờ approval gate, cần thiết kế riêng một pipeline hoặc một cơ chế phê duyệt nhanh cho tình huống incident — đây là quyết định kiến trúc phải làm trước khi có incident, không phải lúc đang cháy.
Với GKE thuần (không qua Cloud Deploy), kubectl rollout undo hoạt động tương tự về mặt khái niệm — chuyển Deployment về ReplicaSet trước đó — nhưng nếu cluster đang được quản lý qua Cloud Deploy, nên ưu tiên rollback qua Cloud Deploy để giữ trạng thái pipeline nhất quán, tránh tình trạng "trạng thái thật của cluster" và "trạng thái mà Cloud Deploy nghĩ nó đang quản lý" bị lệch nhau (drift) — một nguồn gốc gây confusion nghiêm trọng trong incident kế tiếp khi ai đó nhìn vào Cloud Deploy console và thấy thông tin không khớp với thực tế cluster.
Giới hạn thật của rollback: nó không sửa được state đã bị hỏng
Đây là ràng buộc quan trọng nhất cần khắc sâu: rollback chỉ hoàn tác code, không hoàn tác dữ liệu. Nếu nguyên nhân của incident là một database migration không tương thích ngược (backward-incompatible schema change), hoặc dữ liệu đã bị ghi sai bởi phiên bản lỗi trước khi rollback được thực hiện, việc rollback code có thể khiến tình huống tệ hơn — code cũ giờ đọc phải dữ liệu ở định dạng mới mà nó không hiểu, hoặc dữ liệu đã sai vẫn tiếp tục sai bất kể version code nào đang chạy. Rollback là công cụ đúng cho lỗi logic trong code hoặc lỗi cấu hình đi kèm deploy — nó không phải công cụ vạn năng cho mọi loại incident.
Internal model: Feature Flag — tầng runtime configuration
Cơ chế: tách rời deploy khỏi release
Feature flag giải quyết một vấn đề mà rollback không giải quyết được: tốc độ. Rollback qua Cloud Deploy vẫn phải chạy lại pipeline; ngay cả rollback tức thời trên Cloud Run vẫn cần một lệnh deploy-adjacent. Feature flag hoạt động ở một tầng hoàn toàn khác — runtime configuration nằm ngoài binary đã deploy — nghĩa là việc bật/tắt một flag không đụng chạm gì đến pipeline deployment, không cần build lại, không cần chờ approval gate của hệ thống CI/CD.
Đây chính là nguyên lý "decouple deploy khỏi release": code chứa tính năng mới có thể đã được deploy vào production từ lâu nhưng bị flag tắt, và việc bật/tắt tính năng đó chỉ là thay đổi một giá trị cấu hình được binary đọc tại runtime. Trong bối cảnh mitigation, điều này có nghĩa là một "kill switch" được chuẩn bị trước có thể tắt một luồng code cụ thể (ví dụ một tính năng mới đang gây lỗi) trong vài giây, mà không ảnh hưởng đến bất kỳ phần nào khác của hệ thống.
Firebase Remote Config làm ví dụ cơ chế rollout
Firebase Remote Config minh hoạ rõ cơ chế rollout có kiểm soát: khi tạo một rollout với tỷ lệ phần trăm, hệ thống tự động chia một phần bằng nhau của audience vào control group để so sánh hiệu năng giữa nhóm bật tính năng và nhóm không bật — nếu rollout ở mức 2%, thêm 2% khác được đưa vào control group, còn 96% còn lại không bị đụng đến. Có một ràng buộc kỹ thuật đáng chú ý: tỷ lệ rollout phải ≤ 50% trừ khi rollout thẳng lên 100% — nghĩa là không có mức trung gian giữa 50% và 100%, một giới hạn thiết kế buộc đội ngũ phải đưa ra quyết định rõ ràng "đã đủ tin tưởng để full rollout" thay vì trườn dần một cách mơ hồ.
Trong ngữ cảnh mitigation, cơ chế control-group này ít quan trọng hơn khả năng đảo ngược tức thời: khi một flag được set về 0%, toàn bộ traffic ngay lập tức quay lại hành vi cũ mà không cần bất kỳ redeploy nào.
Giới hạn thật: kill switch phải được chuẩn bị trước khi incident xảy ra
Đây là ràng buộc quan trọng nhất của feature flag như công cụ mitigation: bạn không thể thêm một kill switch trong lúc đang xảy ra incident cho một đoạn code chưa từng được bọc flag từ trước. Feature flag chỉ có tác dụng như công cụ mitigation nếu nó đã được thiết kế và triển khai trước — nghĩa là quyết định "tính năng nào cần một escape hatch" là một quyết định kiến trúc phải làm ở thời điểm phát triển tính năng, không phải một phản xạ có thể tạo ra tại chỗ. Đây cũng là lý do các tổ chức trưởng thành về resilience coi việc bọc flag cho mọi thay đổi rủi ro cao là một phần bắt buộc của quy trình release, không phải một tùy chọn.
Internal model: Circuit Breaker — tầng network call giữa các service
Cơ chế outlier detection, không phải cơ chế retry
Circuit breaker trong Cloud Service Mesh (dựa trên nền Istio/Envoy) được triển khai qua khái niệm outlier detection — phát hiện hành vi bất thường trong lưu lượng mạng. Trong một DestinationRule, bạn định nghĩa thế nào là hành vi bất thường (ví dụ: tỷ lệ lỗi 5xx vượt ngưỡng, hoặc số lỗi liên tiếp vượt ngưỡng trong một cửa sổ thời gian), và khi điều kiện đó xảy ra, cơ chế sẽ ejects (loại bỏ tạm thời) pod hoặc endpoint đó khỏi pool load balancing.
Điểm cơ chế cần phân biệt rõ ràng: circuit breaker không phải retry, và về bản chất còn đối lập với retry. Retry thêm tải vào một backend đã đang gặp khó khăn — gửi lại request hy vọng lần sau sẽ thành công; nếu backend đang quá tải (chứ không phải lỗi ngẫu nhiên), retry chỉ khiến nó quá tải nặng hơn, tạo ra một vòng lặp dương (retry storm) đẩy nhanh sự sụp đổ. Circuit breaker làm điều ngược lại: nó rút tải ra khỏi một backend đang có dấu hiệu bất ổn, cho nó không gian để phục hồi thay vì tiếp tục dội request vào.
Tự động, không cần con người quyết định
Đây là khác biệt cơ chế lớn nhất so với rollback và feature flag: cả hai cơ chế đó đều cần một con người (hoặc một pipeline được con người kích hoạt) ra quyết định mitigate. Circuit breaker hoạt động hoàn toàn tự động ở tầng data plane, phản ứng trong vài giây dựa trên các ngưỡng đã được cấu hình từ trước — nó là cơ chế mitigation duy nhất trong ba loại có thể phản ứng nhanh hơn cả thời gian con người kịp nhận ra alert. Sau một khoảng "base ejection time", endpoint bị loại sẽ được đưa trở lại pool để thử lại (cơ chế tương tự half-open trong circuit breaker pattern cổ điển) — nếu nó vẫn lỗi, nó bị eject lại với thời gian dài hơn.
Giới hạn thật: che giấu vấn đề chứ không sửa nó
Circuit breaker bảo vệ caller khỏi một dependency đang lỗi, nhưng nó không sửa dependency đó. Đây là con dao hai lưỡi: nó ngăn một lỗi cục bộ lan thành cascading failure toàn hệ thống (đúng mục tiêu resilience), nhưng đồng thời có thể khiến một phần chức năng âm thầm suy giảm (degrade) mà không ai để ý ngay — vì phần lớn traffic vẫn "thành công" theo nghĩa không throw lỗi tràn lan, chỉ có một phần nhỏ endpoint bị loại khỏi vòng quay. Nếu không có observability đi kèm để theo dõi chính xác bao nhiêu endpoint đang bị eject và tại sao, circuit breaker có thể biến một sự cố rõ ràng thành một sự suy giảm âm thầm kéo dài — khó phát hiện hơn, và dễ bị bỏ quên trong giai đoạn Resolution.
So sánh trực tiếp: tầng, tốc độ, và loại lỗi phù hợp
| Cơ chế | Tầng hoạt động | Tốc độ kích hoạt | Ai quyết định | Loại lỗi phù hợp | Không phù hợp với |
|---|---|---|---|---|---|
| Rollback | Deployment artifact | Giây (Cloud Run) đến phút (Cloud Deploy qua pipeline) | Con người | Lỗi logic/cấu hình đi kèm bản deploy mới nhất | Data đã bị hỏng, schema không tương thích ngược |
| Feature flag | Runtime configuration | Giây, không cần pipeline | Con người | Một luồng tính năng cụ thể đã được bọc flag từ trước | Lỗi ở tầng hạ tầng, hoặc code chưa từng được bọc flag |
| Circuit breaker | Network call layer | Giây, tự động | Không cần con người | Một dependency cụ thể đang lỗi/chậm, cần cô lập | Lỗi lan toả toàn hệ thống, không có "endpoint xấu" cụ thể để cô lập |
Ba cơ chế thường được dùng kết hợp theo lớp, không loại trừ lẫn nhau
Một hiểu lầm phổ biến là coi ba cơ chế này như ba lựa chọn cạnh tranh nhau — phải chọn một. Trong thực tế vận hành, chúng thường hoạt động theo lớp phòng thủ chồng lên nhau, vì mỗi lớp phản ứng ở một tốc độ và một phạm vi khác nhau. Hãy hình dung một luồng sự kiện thực tế: một bản deploy mới của service thanh toán chứa một lỗi khiến nó phản hồi chậm bất thường cho một loại request cụ thể.
Đầu tiên, circuit breaker phản ứng trong vài giây mà không cần con người — outlier detection phát hiện tỷ lệ lỗi/timeout tăng ở endpoint đó và bắt đầu eject nó khỏi pool, giảm bớt tải dồn vào backend đang gặp khó khăn và ngăn cascading failure lan sang các service gọi đến nó. Đây là lớp phòng thủ nhanh nhất nhưng chỉ mua thời gian, không giải quyết gốc rễ.
Trong lúc đó, on-call engineer đang ở giai đoạn Triage (xem file 02) xác định rằng nguyên nhân nằm ở chính bản deploy mới nhất. Nếu tính năng gây lỗi đã được bọc trong một feature flag từ trước, bước mitigation nhanh thứ hai là tắt flag đó ngay lập tức — tức thời, không cần chạm vào pipeline deploy. Đây là lớp phòng thủ thứ hai, nhanh gần bằng circuit breaker nhưng có chủ đích của con người và giải quyết đúng nguyên nhân bề mặt thay vì chỉ cô lập triệu chứng.
Nếu tính năng gây lỗi không được bọc flag (giới hạn đã nêu ở trên), hoặc nếu vấn đề nằm sâu hơn trong toàn bộ binary chứ không tách được thành một luồng cụ thể, lớp phòng thủ thứ ba là rollback qua Cloud Deploy hoặc chuyển traffic trên Cloud Run — chậm hơn hai lớp trên (vài phút thay vì vài giây) nhưng triệt để hơn vì loại bỏ hoàn toàn artifact có vấn đề khỏi đường đi của traffic.
Việc hiểu ba cơ chế này như các lớp chồng lên nhau, không phải các lựa chọn loại trừ, giải thích vì sao các tổ chức trưởng thành về resilience đầu tư vào cả ba đồng thời thay vì chỉ chọn một: circuit breaker xử lý tốc độ, feature flag xử lý độ chính xác của phạm vi tắt, rollback xử lý trường hợp hai lớp trên không đủ.
Constraints & failure mode: mitigation che khuất tín hiệu root cause
Một hệ quả thường bị bỏ qua của cả ba cơ chế trên: mitigation càng hiệu quả, tín hiệu để tìm root cause càng nhanh biến mất. Ngay khi rollback thành công, metric trở lại bình thường — nghĩa là bất kỳ ai bắt đầu điều tra sau thời điểm đó sẽ không còn thấy triệu chứng sống để quan sát trực tiếp, chỉ còn dữ liệu lịch sử. Đây là lý do live incident document (đã bàn ở file 01) phải ghi lại chính xác thời điểm và nội dung của từng hành động mitigation — action nào được thực hiện, lúc mấy giờ, bởi ai — vì đây sẽ là mốc thời gian tham chiếu quan trọng nhất khi dựng lại timeline cho root cause analysis ở giai đoạn hậu-incident.
Anti-pattern: dùng retry như một hình thức "mitigation" cho backend quá tải
Một sai lầm phổ biến dưới áp lực thời gian là tăng retry count hoặc giảm timeout ở tầng client với suy nghĩ "cho nó thêm cơ hội thử lại". Nếu nguyên nhân gốc là backend đang quá tải (không phải lỗi ngẫu nhiên thoáng qua), hành động này về bản chất đi ngược lại cơ chế mà circuit breaker được thiết kế để làm — nó bơm thêm tải vào đúng nơi đang cần được giảm tải, có thể biến một sự cố cục bộ thành cascading failure toàn hệ thống. Hiểu lầm gốc rễ dẫn đến sai lầm này là coi "request thất bại" đơn thuần là vấn đề độ tin cậy cần retry nhiều hơn, thay vì đặt câu hỏi trước: thất bại này là do lỗi ngẫu nhiên, hay là tín hiệu rằng backend đang ở trạng thái cần được giảm tải, không phải được thử lại nhiều hơn.
GCP-native implementation guidance
Ví dụ cấu hình outlier detection (circuit breaker) trong Cloud Service Mesh qua DestinationRule:
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
name: checkout-backend
spec:
host: checkout-backend.default.svc.cluster.local
trafficPolicy:
outlierDetection:
consecutive5xxErrors: 5
interval: 10s
baseEjectionTime: 30s
maxEjectionPercent: 50Cấu hình trên loại bỏ một endpoint khỏi pool trong 30 giây sau khi ghi nhận 5 lỗi 5xx liên tiếp trong cửa sổ 10 giây, giới hạn tối đa 50% pool bị eject cùng lúc để tránh tự loại bỏ toàn bộ backend nếu vấn đề là toàn cục chứ không phải cục bộ ở một vài instance.