Chuyển đến nội dung chính
  1. Bài viết/

Bài 03: Normal Accidents Theory và sự ngụy biện của Root Cause Analysis

Mọi buổi post-mortem đều kết thúc giống nhau: chỉ ra một nguyên nhân gốc, deploy bản vá, đi tiếp. Bài này lập luận rằng với hệ thống phân tán hiện đại, quy trình đó là tự lừa dối — sự cố lớn sinh ra từ tương tác giữa những thành phần đều đang chạy đúng, và cách điều tra thay thế là soát lại cấu trúc kiểm soát chứ không truy tìm con domino đầu tiên.

I. Tàn dư tư duy đồng hồ cơ #

Kỹ sư được dạy rằng hệ thống hỏng vì một bộ phận hỏng: tìm ra bộ phận đó, thay nó, xong việc. Với hệ thống phân tán hiện đại, cách nghĩ này thất bại theo một kiểu có tên gọi. Charles Perrow chỉ ra rằng khi hai tính chất cùng cao — các thành phần tương tác chằng chịt, và giữa chúng không còn khoảng đệm — thì tai nạn không còn là sự cố bất thường mà trở thành hành vi bình thường của chính cấu trúc đó. Bài này bóc tách hai tính chất ấy, chỉ ra vì sao nhãn “lỗi con người” thường là một cái cớ để đóng hồ sơ sớm, và thay quy trình truy tìm nguyên nhân gốc bằng một câu hỏi khác: ràng buộc an toàn nào đã bị vi phạm, và cấu trúc kiểm soát nào lẽ ra phải giữ được nó.

Kỹ sư lớn lên với mô hình đồng hồ cơ. Có gì hỏng à? Một bánh răng vỡ, hoặc ai đó quên lên dây. Tháo rời từng bộ phận là hiểu được toàn thể. Gọn gàng, thỏa mãn, và với những hệ thống đơn giản thì đúng.

Mô hình ấy vỡ ngay khi bạn bước vào một hệ thống phức hợp thích ứng — cách gọi cho những hệ mà các thành phần vừa liên tục tác động lẫn nhau, vừa tự thay đổi hành vi theo tình huống. Một kiến trúc cloud-native điển hình là như vậy: autoscaler phản ứng với tải, load balancer phản ứng với autoscaler, retry policy phản ứng với cả hai.

Báo cáo STELLA của nhóm SNAFUcatchers đặt tên cho khoảng cách quan trọng nhất trong loại hệ này: cái nằm dưới vạch và cái nằm trên vạch. Dưới vạch là hệ thống thật — codebase, phần cứng, đường mạng — thứ không ai nhìn thấy trực tiếp. Trên vạch là những gì con người thực sự chạm vào: dashboard, log, alert. Hai thứ đó không bao giờ trùng khớp. Người trực sự cố không debug hệ thống; họ debug một bức tranh về hệ thống, được vẽ bởi những metric mà ai đó đã chọn thu thập từ sáu tháng trước.

Nên khi outage ập tới, phản xạ đầu tiên là đi săn một nguyên nhân gốc. Bài này nói rằng cuộc săn đó thường về tay không — và bản thân tai nạn mới là chuyện bình thường.

II. Phân biệt hai thứ trông giống nhau: hỏng hóc và tai nạn #

Muốn nói chuyện được về sụp đổ hệ thống, phải tách bạch hai biến cố nhìn từ ngoài rất giống nhau nhưng cư xử hoàn toàn khác:

  • Hỏng hóc dự kiến được (expected failure). Cục bộ, có biên giới, nằm dưới ngưỡng. Một pod ăn hết memory và bị kill. Hệ thống có sẵn khả năng hấp thụ: orchestrator phát hiện, đẩy traffic sang node khác, cấp lại pod. Chuyện này nằm trong thiết kế, và nó kết thúc ở đúng chỗ nó bắt đầu.
  • Tai nạn hệ thống (systemic accident). Phi tuyến, không có biên giới, vượt ngưỡng. Điểm đáng sợ nằm ở chỗ: mọi thành phần đều đang chạy đúng như được lập trình. Không có bug nào. Nhưng ở quy mô hàng nghìn node, chính các cơ chế tự bảo vệ cộng hưởng với nhau thành một vòng lặp tự khuếch đại — retry storm ở bài trước là ví dụ chuẩn. Tai nạn sinh ra từ tương tác giữa những hành vi an toàn, đi qua những chỗ ghép nối quá chặt.

Khác biệt này quyết định cách điều tra. Với loại thứ nhất, tìm bộ phận hỏng là đúng việc. Với loại thứ hai, không có bộ phận nào hỏng để mà tìm.

III. Khung của Perrow: hai biến số quyết định #

Charles Perrow, sau khi phân tích sự cố Three Mile Island, rút gọn rủi ro cấu trúc của mọi hệ thống về hai biến số.

Interactive complexity ( $\chi$) — độ chằng chịt của tương tác. Không phải “hệ thống có bao nhiêu thành phần”, mà “có bao nhiêu đường ảnh hưởng lẫn nhau mà không ai vẽ ra được”. Dấu hiệu nhận biết: khi sửa A làm hỏng D, mà giữa A và D không có đường gọi trực tiếp nào. Trong kiến trúc phần mềm, nó đến từ dependency bắc cầu, state dùng chung, và các lớp sidecar hay service mesh nằm giữa mọi cuộc gọi.

Tight coupling ($\tau$) — độ chặt của ghép nối. Là nghịch đảo của khoảng đệm: giữa hai thành phần còn bao nhiêu thời gian, bao nhiêu chỗ chứa tạm, bao nhiêu đường vòng. Một chuỗi gọi gRPC đồng bộ năm tầng là ghép nối chặt: tầng cuối chậm thì cả năm tầng cùng chờ. Một hàng đợi giữa hai service là ghép nối lỏng: bên nhận chết mười phút, tin nhắn vẫn nằm đó.

1. Rủi ro là tích của hai biến số #

Quan hệ giữa chúng viết được thành một phương trình — với một cảnh báo đặt ngay đây: đây là heuristic để suy nghĩ, không phải công cụ để đo. Nó cho thấy hình thù của rủi ro, không cho ra con số đem đi dự báo.

$$P_{acc} = 1 - e^{-\int (\chi \cdot \tau)\, dt}$$

Đọc bằng lời: rủi ro tích lũy theo thời gian vận hành, và nó tích lũy theo tích của hai biến số chứ không phải tổng. Nghĩa là hệ thống chằng chịt nhưng ghép nối lỏng thì vẫn sống được; hệ thống ghép nối chặt nhưng đơn giản cũng vậy. Chỉ khi cả hai cùng cao thì $P_{acc}$ mới lao về 1 — và một khi nó đã lao về 1, câu hỏi không còn là “có xảy ra không” mà là “khi nào”.

Thay số cho cụ thể: với $\chi = 1$, một hệ ghép nối lỏng ($\tau = 0{,}5$) cho $P_{acc} \approx 39\%$, còn cùng hệ đó ghép nối chặt ($\tau = 3$) cho $P_{acc} \approx 95\%$. Cùng một mức phức tạp, chỉ đổi cách ghép nối, rủi ro nhảy từ “thỉnh thoảng” sang “gần như chắc chắn”.

2. Quy đổi sang chỉ số kiến trúc #

Biến số lý thuyếtĐo bằng gì trong hệ thống thật
Interactive complexity ($\chi$)Số dependency bắc cầu; số chỗ dùng chung state; số lớp sidecar/mesh chen giữa các cuộc gọi
Tight coupling ($\tau$)Độ dài chuỗi gọi đồng bộ; timeout cố định đặt thấp hơn p99 thực tế; số distributed lock
Hỏng hóc thành phầnBug tất định, phạm vi hẹp, đã được cô lập từ lúc thiết kế
Tai nạn hệ thốngMọi dashboard đều xanh nhưng hệ thống đã ngừng phản hồi

IV. Con đường dẫn tới trạng thái kẹt cứng #

Hệ thống rơi vào trạng thái kẹt cứng (metastability) khi tốc độ lan lỗi vượt qua khả năng hấp thụ còn lại của nó. Điểm kích hoạt gần như luôn giống nhau: một tài nguyên dùng chung bão hòa — thread pool, network buffer, hàng đợi kết nối database.

Cơ chế đáng chú ý nằm ở chỗ tài nguyên dùng chung biến chuyện cục bộ thành chuyện toàn cục. Khi còn khoảng trống, một lần retry chỉ là một request thừa. Khi thread pool đã đầy, cũng lần retry đó chiếm mất chỗ của một request đang chờ, đẩy request kia vào timeout, và tạo ra một lần retry nữa. Một hành vi hoàn toàn an toàn trên một node trở thành cơn bão trên một nghìn node — không phải vì nó đổi bản chất, mà vì khoảng đệm đã hết.

graph TD A[Tương tác chằng chịt: sửa A hỏng D] --> B{Còn khoảng đệm giữa các thành phần?} B -- "Không: gọi đồng bộ, timeout cứng" --> C[Tai nạn hệ thống là chuyện bình thường] B -- "Còn: hàng đợi, bất đồng bộ" --> D[Lỗi cục bộ, phục hồi được] C --> E[Vòng lặp tự khuếch đại: retry storm] E --> F[Tài nguyên dùng chung bão hòa: thread pool, hàng đợi] F --> G[Kẹt cứng: sập và không tự bò dậy] style C fill:#F97316,stroke:#171717,color:#FFFFFF style G fill:#171717,stroke:#F97316,color:#F97316

V. “Lỗi con người” thường là chỗ dừng, không phải câu trả lời #

Kết luận sự cố do “lỗi con người” thực chất là một stop rule1 — một điểm dừng thuận tiện để đóng hồ sơ, thường được chọn vì nó rẻ hơn việc thừa nhận một khiếm khuyết kiến trúc.

Hai khái niệm giải thích vì sao nhãn đó gần như luôn sai:

  • Local rationality — tính hợp lý tại chỗ. Sidney Dekker và Richard Cook lập luận: không kỹ sư nào đến công ty để phá hệ thống. Nhìn từ chỗ họ đứng, với những gì họ thấy trên dashboard ngay lúc đó, hành động của họ là hợp lý. Nó chỉ trông ngu ngốc khi nhìn lại từ tương lai, với toàn bộ thông tin mà lúc ấy không ai có. Điều tra kiểu “lẽ ra phải biết” thực chất đang phạt người ta vì không có một thứ mà hệ thống chưa bao giờ cung cấp cho họ.
  • Graceful extensibility2 — khả năng nới ra khi chạm giới hạn. Khái niệm của David Woods, chỉ năng lực của hệ thống xoay xở với những tình huống nằm ngoài mọi kịch bản đã thiết kế. Trong các sự cố lớn, con người vận hành thường không phải nguyên nhân gây lỗi. Họ là nguồn phục hồi: người nghĩ ra cách xả tải mà không có runbook nào viết, người quyết định tắt một tính năng để cứu phần còn lại. Automation không làm được việc đó, vì automation chỉ xử lý những kịch bản đã được nghĩ tới từ trước.

VI. Thay RCA bằng STAMP và CAST #

Root Cause Analysis truyền thống dựa trên hình ảnh chuỗi domino: tìm con đổ đầu tiên, dựng nó lên, xong. Trong hệ phi tuyến, hình ảnh đó sai ngay từ tiền đề — những con domino không xếp thành hàng, chúng xếp thành mạng, và nhiều con đổ cùng lúc.

STAMP của Nancy Leveson thay chuỗi sự kiện bằng một cấu trúc kiểm soát: hệ thống được nhìn như một tập các ràng buộc an toàn cùng những cơ chế có nhiệm vụ giữ các ràng buộc đó. Tai nạn xảy ra không phải vì một thành phần hỏng, mà vì một ràng buộc an toàn không còn ai giữ.

CAST là quy trình điều tra đi kèm. Điểm khác biệt so với RCA: nó không hỏi “cái gì hỏng” hay “ai làm sai”, mà soát lại toàn bộ cấu trúc kiểm soát — bao gồm cả phần công nghệ (Kubernetes, circuit breaker, alert) lẫn phần tổ chức (quy trình duyệt CI/CD, chính sách trực, ngưỡng cảnh báo)3.

Ba câu hỏi thay cho câu hỏi nguyên nhân gốc:

  1. Ràng buộc an toàn nào đã bị vi phạm? (Ví dụ: “không bao giờ để quá 30% pod của một service nằm trên cùng một node”.)
  2. Cơ chế nào lẽ ra phải giữ ràng buộc đó, và vì sao nó không giữ được?
  3. Vòng phản hồi nào bị đứt — người vận hành đáng lẽ phải thấy tín hiệu gì mà đã không thấy?

Ba câu hỏi này cho ra hạng mục sửa chữa khác hẳn. RCA cho ra “sửa bug ở service X”. CAST cho ra “chúng ta không có cơ chế nào phát hiện ràng buộc này bị vi phạm, và đó là hạng mục cần làm”.

VII. Mô phỏng hình thù của rủi ro #

Mô phỏng dưới đây không dùng để dự báo — nó dùng để nhìn. Nhưng nó cho thấy đúng một điều: siết chặt một hệ thống phức tạp thì đường cong rủi ro không bò lên từ từ, nó bật dựng.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import numpy as np
import matplotlib.pyplot as plt

def model_system_risk(complexity, coupling):
    # Heuristic: rủi ro tích lũy theo tích của hai biến số, hội tụ về 1
    return 1 - np.exp(-(complexity * coupling))

complexity_range = np.linspace(0.1, 5, 100)
coupling_levels = [0.5, 1.5, 3.0]  # lỏng (hàng đợi), vừa, chặt (gọi đồng bộ)
colors = ['#D4D4D4', '#FB923C', '#F97316']

plt.figure(figsize=(10, 5))
for level, color in zip(coupling_levels, colors):
    risk = [model_system_risk(c, level) for c in complexity_range]
    plt.plot(complexity_range, risk, label=f'Coupling τ = {level}', color=color)

plt.title('Hình thù rủi ro sụp đổ hệ thống')
plt.xlabel('Interactive Complexity (χ)')
plt.ylabel('P(Accident)')
plt.legend()
plt.show()

Đọc biểu đồ: ở mức ghép nối chặt ($\tau = 3$), rủi ro đã vượt 90% khi độ phức tạp mới ở mức $\chi = 0{,}8$. Ở mức ghép nối lỏng ($\tau = 0{,}5$), phải tới $\chi = 4{,}6$ mới chạm cùng ngưỡng đó. Cùng một hệ thống, chỉ đổi cách các thành phần chờ nhau, dư địa phức tạp an toàn rộng gấp gần sáu lần.

Metaphorical Risk of Systemic Collapse

VIII. Bốn việc làm được ngay #

  1. Tạo khoảng đệm thời gian. Đổi các phụ thuộc đồng bộ thành luồng sự kiện bất đồng bộ ở những chỗ nghiệp vụ cho phép. Đây là cách trực tiếp nhất để kéo $\tau$ xuống, và theo phương trình ở Mục III, nó cũng là cách rẻ nhất.
  2. Đầu tư vào khả năng quan sát, không chỉ thu thập số liệu. Câu hỏi đúng không phải “ta có metric này chưa” mà “người trực lúc 2 giờ sáng có thấy được ràng buộc nào đang bị vi phạm không”. Đây chính là việc thu hẹp khoảng cách trên vạch và dưới vạch ở Mục I.
  3. Thiết kế để hệ thống nới ra được. Khi chạm giới hạn, kiến trúc phải chừa đường cho con người can thiệp — cầu dao bật tay, cờ tắt tính năng, van xả tải — thay vì khóa cứng vào trạng thái kẹt.
  4. Bảo vệ ràng buộc, không chỉ bảo vệ thành phần. Viết ra các ràng buộc an toàn của hệ thống thành câu cụ thể, rồi dựng cơ chế phát hiện khi chúng bị vi phạm. Danh sách đó nên gồm cả ràng buộc tổ chức, không chỉ ràng buộc kỹ thuật.

IX. Lời kết #

Normal Accidents Theory và STAMP đưa ra một góc nhìn khiêm tốn nhưng thực dụng: ta không kiểm soát trọn vẹn một hệ thống phức hợp: ta chỉ đặt ra và giữ được những ràng buộc chi phối nó. Chấp nhận rằng rủi ro hệ thống là thuộc tính của chính cấu trúc, ta ngừng đuổi theo ảo giác nguyên nhân gốc và dồn nguồn lực vào việc thật sự có ích: làm cho hệ thống chịu được những thứ chưa ai nghĩ tới.

X. Tài liệu tham khảo và đọc thêm #


  1. Stop rule: điểm dừng trong một cuộc điều tra, được chọn theo thỏa hiệp của tổ chức chứ không theo logic. Gán nhãn “lỗi con người” tạo cảm giác vấn đề đã được giải quyết, và chính cảm giác đó chặn việc đào tiếp xuống phần kiến trúc. ↩︎

  2. Graceful extensibility: năng lực của một hệ thống xoay xở với những tình huống nằm ngoài mọi kịch bản đã thiết kế, nơi con người là phần lấp vào những vùng mà tự động hóa chưa từng được dạy. ↩︎

  3. Blunt end / sharp end: trong lý thuyết an toàn, “sharp end” là tiền tuyến — người trực tiếp thao tác trên production. “Blunt end” là quản lý, chính sách, quy trình: ở xa hiện trường nhưng chính là nơi đặt ra các điều kiện dẫn tới lỗi ở tiền tuyến. ↩︎