High Availability (HA): Thiết kế cho thất bại (Design for Failure) và cơ chế failover
7/19/2026 · 12p đọc
title: "High Availability (HA): Thiết kế cho thất bại (Design for Failure) và cơ chế failover"
series: "Software Architecture Masterclass"
part: "Phần 3 — Hệ thống Phân tán & Hạ tầng"
order: 26
audience: "Solutions Architect & Technical Lead"
reading_time: "13 phút"
tags: ["high-availability", "design-for-failure", "failover", "chaos-engineering", "multi-az", "disaster-recovery", "sla", "resilience"]
High Availability (HA): Thiết kế cho thất bại (Design for Failure) và cơ chế failover
3 giờ sáng, một AZ (Availability Zone) của nhà cung cấp cloud gặp sự cố mất điện cục bộ. Không phải toàn vùng, không phải toàn khu vực — chỉ một AZ. Nhưng hệ thống của bạn sập hoàn toàn, vì con RDS primary, hai trong ba node ứng dụng, và cụm Redis cache đều nằm chung một AZ đó. Đội vận hành thức dậy, chạy failover thủ công, mất 40 phút để khôi phục. Khách hàng B2B lớn nhất gọi điện hỏi tại sao downtime không có cảnh báo trước.
Điều trớ trêu: không ai trong team thiết kế hệ thống này là người tệ. Họ viết code sạch, test coverage tốt, CI/CD chuẩn chỉnh. Vấn đề nằm ở một giả định ngầm mà gần như mọi kỹ sư đều mắc phải khi áp lực deadline đè xuống: giả định rằng phần cứng, mạng, và các dependency bên ngoài sẽ hoạt động như tài liệu mô tả. Happy path được code kỹ lưỡng; đường thất bại (failure path) thì bị bỏ ngỏ, hoặc tệ hơn — được "xử lý" bằng một dòng log và một lời cầu nguyện.
High Availability không phải là một tính năng bạn bật lên bằng một config flag. Nó là một tư duy thiết kế xuyên suốt — và giống mọi quyết định kiến trúc khác trong series này, nó có giá của nó. Bài này nói về triết lý Design for Failure, cơ chế failover thực tế, và quan trọng nhất: khi nào đầu tư vào HA là hợp lý, khi nào nó là lãng phí ngân sách công ty vào một cơn ác mộng gần như không bao giờ xảy ra.
The Problem
Đa số hệ thống được thiết kế theo tư duy lạc quan mặc định (optimistic default): server sẽ luôn phản hồi, database connection sẽ luôn thành công, network call tới service khác sẽ luôn trả về trong 200ms. Code được viết cho kịch bản "mọi thứ đều ổn", còn nhánh lỗi chỉ là một try/catch bọc quanh, ném ra một exception generic, hoặc tệ hơn — không có gì cả.
Vấn đề là: trong một hệ thống phân tán, việc một thành phần nào đó hỏng không phải là "nếu" (if) mà là "khi nào" (when). Đĩa cứng có MTBF (Mean Time Between Failures) hữu hạn. Card mạng lỗi. Datacenter mất điện. Một bản deploy có bug. Một dependency third-party rate-limit bạn giữa giờ cao điểm. Với hệ thống đủ lớn và đủ nhiều node, xác suất có ít nhất một thành phần hỏng tại bất kỳ thời điểm nào tiến gần đến 100%. Đây là lý do Werner Vogels (CTO Amazon) từng nói một câu nổi tiếng: "Everything fails, all the time."
Vấn đề thực sự không phải là việc một node lỗi — mà là việc kiến trúc để lỗi của MỘT thành phần lan truyền và làm sập TOÀN BỘ hệ thống. Đây gọi là single point of failure (SPOF) — điểm lỗi đơn lẻ. Một load balancer không có standby. Một database chỉ có primary, không có replica. Một message queue chạy trên một node duy nhất. Mỗi SPOF là một quả bom hẹn giờ mà xác suất kích hoạt tăng dần theo thời gian vận hành của hệ thống.
Điều nguy hiểm hơn: SPOF thường vô hình cho tới khi nó nổ. Diagram kiến trúc trên Confluence trông rất đẹp, có đủ ký hiệu "HA", "Auto Scaling Group", "Multi-AZ" — nhưng không ai thực sự test xem khi node đó chết, hệ thống có failover đúng như thiết kế hay không. Đây là khoảng cách giữa "được thiết kế để chịu lỗi" và "đã được CHỨNG MINH là chịu được lỗi" — khoảng cách mà chaos engineering tồn tại để lấp đầy.
The Theory
Nguyên tắc cốt lõi là Design for Failure: thay vì hỏi "làm sao để thành phần này không bao giờ hỏng?", hãy hỏi "khi thành phần này hỏng — và nó SẼ hỏng — hệ thống sẽ phản ứng như thế nào?". Đây là sự đảo ngược tư duy hoàn toàn so với thiết kế truyền thống vốn tập trung vào việc tăng độ tin cậy của từng thành phần đơn lẻ (mua phần cứng đắt tiền hơn, RAID, UPS...). Design for Failure chấp nhận rằng từng thành phần riêng lẻ SẼ hỏng, và đặt gánh nặng độ tin cậy lên KIẾN TRÚC tổng thể, không lên từng node.
Ba khái niệm nền tảng cần phân biệt rõ:
Redundancy (dự phòng) — có nhiều hơn một bản sao của mỗi thành phần trọng yếu, đặt ở các vùng lỗi (failure domain) độc lập. Failure domain là ranh giới mà một sự cố không thể vượt qua để lan sang phần khác — ví dụ một AZ, một rack điện, một nhà cung cấp mạng. Redundancy vô nghĩa nếu tất cả bản sao nằm chung một failure domain — đó chính là sai lầm trong ví dụ mở bài: ba node "dự phòng" nhưng chung một AZ vẫn là một SPOF ở cấp AZ.
Failover — cơ chế TỰ ĐỘNG chuyển traffic/trách nhiệm từ thành phần lỗi sang bản dự phòng khi phát hiện sự cố. Có hai mô hình chính:
- Active-Passive: một node chính xử lý toàn bộ traffic, node phụ đứng chờ (standby) và chỉ nhận traffic khi node chính chết. Đơn giản hơn, tránh được vấn đề đồng bộ trạng thái hai chiều, nhưng lãng phí tài nguyên (node phụ không làm gì phần lớn thời gian) và có failover time (thời gian phát hiện lỗi + chuyển đổi) không bằng 0.
- Active-Active: nhiều node cùng xử lý traffic song song, chia tải qua load balancer. Khi một node chết, các node còn lại tự động gánh phần traffic dôi ra. Tận dụng tài nguyên tốt hơn, failover gần như tức thời (chỉ cần loại node chết khỏi pool), nhưng đòi hỏi các node phải stateless hoặc có cơ chế đồng bộ trạng thái phức tạp hơn nhiều (đặc biệt với database — xung đột ghi giữa các node active).
Circuit Breaker — cơ chế ngăn lỗi lan truyền (cascading failure) theo chiều ngang giữa các service. Khi service A gọi service B liên tục thất bại (timeout, 5xx), circuit breaker "mở mạch" — ngừng gọi B trong một khoảng thời gian, trả lỗi nhanh (fail fast) hoặc fallback, thay vì để mỗi request tới A đều phải chờ B timeout rồi mới fail. Nếu không có circuit breaker, một service downstream chậm có thể làm cạn kiệt connection pool và thread pool của toàn bộ service upstream — lỗi cục bộ biến thành lỗi toàn hệ thống. Đây chính là cơ chế "cầu chì" — ngắt trước khi cháy lan.
Phương pháp kiểm chứng những cơ chế trên hoạt động thật, chứ không chỉ tồn tại trên diagram, là Chaos Engineering — thực hành CHỦ ĐỘNG gây ra lỗi có kiểm soát trong môi trường (thường là production hoặc staging sát production) để quan sát hệ thống phản ứng ra sao. Nguyên lý nền tảng: nếu bạn không test failover, bạn không biết nó có hoạt động — bạn chỉ đang hy vọng. Game day (buổi diễn tập sự cố có kịch bản) là hình thức có tổ chức của chaos engineering: team chủ động rút phích một node, cắt mạng một AZ giả lập, hoặc tăng latency của một dependency, rồi đo thời gian phát hiện (detection time) và thời gian phục hồi (recovery time) thực tế.
The Trade-off
Đây là phần không được phép mơ hồ. HA không miễn phí — nó là một khoản đầu tư có ROI cụ thể, và như mọi khoản đầu tư, nó có thể lãi hoặc lỗ tùy vào bối cảnh.
Được gì khi đầu tư vào HA:
- Giảm downtime, do đó giảm thiệt hại doanh thu trực tiếp (với hệ thống e-commerce/thanh toán, mỗi phút downtime có thể quy đổi thành tiền cụ thể).
- Giảm thiệt hại uy tín — với B2B SaaS, một sự cố nghiêm trọng có thể là lý do khách hàng lớn không gia hạn hợp đồng hoặc kích hoạt điều khoản SLA credit.
- Team vận hành ngủ ngon hơn — ít alert lúc 3 giờ sáng hơn, ít burnout hơn.
- Khả năng đáp ứng SLA hợp đồng (uptime 99.9%, 99.95%...) — với một số khách hàng enterprise, đây là điều kiện bắt buộc để ký hợp đồng, không phải "nice to have".
Mất gì — và đây là phần thường bị lờ đi:
- Chi phí hạ tầng nhân đôi (hoặc hơn): multi-AZ nghĩa là chạy ít nhất 2x số node so với single-AZ cho cùng một service, cộng thêm chi phí truyền dữ liệu cross-AZ (data transfer fee thường không hề rẻ ở quy mô lớn). Active-active đa vùng địa lý (multi-region) còn tốn kém hơn nhiều lần.
- Độ phức tạp vận hành tăng theo cấp số nhân, không tuyến tính: mỗi lớp redundancy thêm vào là một lớp cần giám sát, một lớp có thể tự nó gây ra lỗi mới (ví dụ: cơ chế failover bị lỗi và tự kích hoạt sai — false positive failover gây downtime thay vì ngăn downtime). Đã có nhiều sự cố nổi tiếng trong ngành mà nguyên nhân gốc là chính cơ chế HA/failover tự động phản ứng sai.
- Data consistency phức tạp hơn hẳn: active-active với database đòi hỏi giải quyết xung đột ghi (conflict resolution), hoặc chấp nhận eventual consistency (đã bàn ở Bài 22) — đánh đổi giữa tính sẵn sàng và tính nhất quán không biến mất, nó chỉ chuyển từ tầng hạ tầng sang tầng ứng dụng.
- Thời gian phát triển kéo dài: viết code idempotent, retry-safe, và chịu được việc dependency chập chờn tốn effort đáng kể hơn viết code giả định happy path. Testing cũng nặng hơn — cần test cả các nhánh lỗi, không chỉ nhánh thành công.
- Chi phí tổ chức chaos engineering: game day cần thời gian của nhiều kỹ sư senior, và có RỦI RO THẬT — gây lỗi có kiểm soát trong production vẫn có thể vượt tầm kiểm soát nếu thực hiện ẩu.
Điểm mấu chốt của trade-off này: chi phí HA tăng gần như theo cấp số nhân khi bạn tiến gần đến "five nines" (99.999% uptime — tương đương chưa tới 5.5 phút downtime/năm), trong khi lợi ích tăng theo đường cong bão hòa. Từ 99% lên 99.9% uptime có thể chỉ cần thêm một replica; từ 99.9% lên 99.99% có thể cần multi-region active-active với toàn bộ bộ máy đồng bộ dữ liệu phức tạp; từ 99.99% lên 99.999% đòi hỏi mức đầu tư mà rất ít doanh nghiệp ngoài big tech biện minh nổi bằng số liệu tài chính thực tế.
The Architecture Diagram
Sơ đồ dưới đây so sánh hai mô hình topology: Active-Active và Active-Passive, cùng với vai trò của health check trong việc kích hoạt failover.
graph TB
subgraph "Active-Active (Multi-AZ)"
LB1["Load Balancer<br/>(health-check liên tục)"]
LB1 -->|traffic ~50%| A1["App Node A<br/>(AZ-1) — ACTIVE"]
LB1 -->|traffic ~50%| A2["App Node B<br/>(AZ-2) — ACTIVE"]
A1 --> DB1["DB Primary<br/>(AZ-1)"]
A2 --> DB1
DB1 -.sync replication.-> DB1R["DB Replica<br/>(AZ-2)"]
A1 -.lỗi khi A1 chết.-> LB1
note1["Node A chết → LB loại khỏi pool<br/>traffic dồn 100% qua Node B<br/>failover ~ tức thời, không mất request mới"]
end
subgraph "Active-Passive (Failover thủ công/tự động)"
LB2["Load Balancer / DNS Failover"]
LB2 -->|100% traffic| P1["App Node Primary<br/>(AZ-1) — ACTIVE"]
P2["App Node Standby<br/>(AZ-2) — PASSIVE<br/>(idle, chờ lệnh)"]
P1 -.heartbeat.-> HM["Health Monitor"]
HM -."phát hiện Primary chết<br/>(sau N lần check thất bại)".-> P2
HM -."kích hoạt promote".-> P2
P2 -."trở thành Primary mới<br/>(có độ trễ failover: giây → phút)".-> LB2
note2["Trong lúc failover: request bị drop/timeout<br/>cho tới khi Standby được promote"]
end
style A1 fill:#2d7d46,color:#fff
style A2 fill:#2d7d46,color:#fff
style P1 fill:#2d7d46,color:#fff
style P2 fill:#888,color:#fff
style DB1 fill:#1f6feb,color:#fff
style DB1R fill:#5a8fd6,color:#fff
Điểm quan sát quan trọng từ diagram: trong mô hình active-active, health check của load balancer là "trái tim" của toàn bộ cơ chế — nếu health check không đủ nhạy (interval quá dài, threshold quá lỏng), node chết vẫn tiếp tục nhận traffic trong một khoảng thời gian trước khi bị loại. Trong mô hình active-passive, độ trễ failover (thời gian từ lúc primary chết tới lúc standby được promote và nhận traffic) là con số CỤ THỂ cần đo và cam kết — đây chính là một phần của RTO (Recovery Time Objective) sẽ được bàn sâu ở Bài 29 về Disaster Recovery.
Implementation Guardrails
Mọi thành phần trên đường dẫn trọng yếu (critical path) phải có cơ chế failover được ĐỊNH NGHĨA rõ ràng bằng văn bản — không phải "chắc nó tự failover" mà phải ghi rõ: điều kiện kích hoạt (health check fail bao nhiêu lần, trong bao lâu), ai/cái gì thực hiện chuyển đổi, và thời gian kỳ vọng để hoàn tất. Nếu không trả lời được câu hỏi "component X chết thì điều gì xảy ra tiếp theo, mất bao lâu?" bằng con số cụ thể, đó không phải là HA — đó là một hy vọng.
Không có failover nào được coi là đúng cho tới khi nó được KIỂM THỬ bằng chaos engineering/game day thực tế — ít nhất định kỳ (theo quý hoặc sau mỗi thay đổi hạ tầng lớn). Diagram trên Confluence không chứng minh được gì; log của một lần failover thật (dù là do sự cố thật hay do chủ động gây ra) mới chứng minh. Bắt đầu ở môi trường staging trước khi tự tin làm ở production, và luôn có kế hoạch rollback/dừng thử nghiệm khẩn cấp.
Redundancy phải đặt ở các failure domain THỰC SỰ độc lập — kiểm tra kỹ rằng các bản sao "dự phòng" không chia sẻ ngầm cùng một điểm lỗi (cùng AZ, cùng rack điện, cùng nhà cung cấp DNS, cùng credential/secret store). Đây là lỗi phổ biến nhất: tưởng đã có redundancy nhưng thực ra tất cả bản sao vẫn phụ thuộc vào một SPOF ẩn ở tầng thấp hơn.
Circuit breaker và timeout PHẢI có ở mọi lời gọi qua network giữa các service — không có ngoại lệ "vì service đó ổn định lắm". Timeout không đặt = một request treo vô hạn = một thread bị giữ vô hạn = cạn kiệt tài nguyên = cascading failure. Giá trị timeout và ngưỡng circuit breaker phải dựa trên số liệu latency thực tế đo được, không phải số áng chừng.
Architect's Verdict
Đầu tư vào HA phải khớp với chi phí downtime THẬT của doanh nghiệp — được tính bằng tiền, bằng hợp đồng SLA, bằng uy tín — chứ không phải bằng nỗi sợ mơ hồ "lỡ nó sập thì sao". Trước khi thiết kế multi-AZ, multi-region, active-active phức tạp, hãy tự hỏi: một giờ downtime tốn công ty bao nhiêu tiền, và số đó có biện minh cho chi phí + độ phức tạp của giải pháp không? Five-nines cực kỳ đắt đỏ — cả về hạ tầng lẫn độ phức tạp vận hành — và phần lớn doanh nghiệp, kể cả nhiều công ty tự nhận là "mission-critical", không thực sự cần đến nó. Ba nines với failover được test kỹ còn giá trị hơn năm nines chỉ tồn tại trên giấy.
🔗 Bài viết liên quan
- CAP Theorem — nền tảng lý thuyết cho đánh đổi giữa tính sẵn sàng và tính nhất quán mà mọi thiết kế active-active phải đối mặt.
- Disaster Recovery: RTO và RPO — mở rộng khái niệm failover sang kịch bản mất toàn bộ vùng/trung tâm dữ liệu, với các chỉ số đo lường cụ thể.
Bài trước: Message Queues · Bài tiếp theo: Infrastructure as Code (IaC)