↓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

Sau sự cố, biên bản thường chốt một nguyên nhân gốc rồi đóng hồ sơ. Bài này giải thích vì sao sự cố lớn hay đến từ những thành phần đều chạy đúng nhưng tương tác sai, và nên điều tra theo hướng nào thay thế.

I. Tóm tắt: hệ thống không còn hỏng kiểu đồng hồ #

Kỹ sư được dạy rằng hệ thống hỏng vì một bộ phận hỏng: tìm bộ phận đó, thay nó, xong. Charles Perrow chỉ ra rằng khi các phần tương tác chằng chịt và giữa chúng hết khoảng đệm, tai nạn lại trở thành chuyện không tránh được của chính cấu trúc. Bài này phân tích tách bạch hai tính chất trên. Nó cho thấy vì sao kết luận “lỗi con người” thường chỉ là chỗ tiện để khép lại hồ sơ sự cố. Từ đó, ta đổi câu hỏi: ràng buộc an toàn nào đã bị vi phạm, và cơ chế nào lẽ ra phải giữ vững nó?

Nhiều mẫu biên bản sự cố có một ô “Nguyên nhân gốc”, và ô đó hiếm khi bị bỏ trống, kể cả khi không nguyên nhân nào đủ sức một mình điền vào. Root Cause Analysis (RCA) là cách các đội điền ô đó: lần ngược từ sự cố, tìm một nguyên nhân gốc, sửa, đóng hồ sơ. Cách làm này coi hệ thống như một chiếc đồng hồ cơ: tháo từng bộ phận ra là hiểu cả chiếc, và với hệ đơn giản thì đúng vậy.

Nó sai với một complex adaptive system. Ở loại hệ này, các thành phần liên tục tác động qua lại và tự đổi hành vi. Ví dụ, hạ tầng tự thêm máy khi tải tăng, trong lúc app tự gửi lại yêu cầu chờ quá lâu. Bài này lập luận rằng ở loại hệ đó, cuộc săn nguyên nhân gốc thường về tay không, và tai nạn mới là chuyện bình thường. Muốn thấy vì sao, hãy tách hai biến cố trông giống nhau.

II. Hỏng hóc và tai nạn: hai thứ trông giống mà khác hẳn #

Tôi dùng một tình huống giả định cho cả bài: 12 giờ trưa, một sàn thương mại điện tử mở flash sale, hàng chục nghìn người cùng bấm “Mua”. Mỗi lần bấm gửi đi một request. Request là một lần app gửi yêu cầu tới server.

Hệ thống của sàn chia thành nhiều service. Service là phần mềm lo một việc, như giỏ hàng hay thanh toán. Mỗi service chạy nhiều bản sao trên nhiều máy; Kubernetes là phần mềm chạy các bản sao đó và khởi động lại bản nào bị tắt.

Hai biến cố dưới đây trông giống nhau nhưng diễn biến khác hẳn:

  • Hỏng hóc dự kiến được. Cục bộ, có giới hạn, nằm trong thiết kế. Nếu một bản sao của service giỏ hàng dùng quá bộ nhớ được cấp và bị tắt, Kubernetes khởi động lại nó trong lúc các bản sao khác nhận request. Chuyện dừng ở đó.
  • Tai nạn hệ thống. Hậu quả không tỉ lệ với nguyên nhân và vượt mọi ranh giới thiết kế, trong khi mọi thành phần đều chạy đúng như được lập trình. Khi hàng nghìn máy cùng lúc tự bảo vệ đúng cách, các cơ chế đó cộng hưởng thành một vòng tự khuếch đại. Tai nạn sinh ra từ tương tác giữa những hành vi an toàn, qua những chỗ nối quá chặt.

Ví dụ điển hình là retry storm ở Bài 02. Retry là việc app tự gửi lại request sau khi chờ quá lâu. Request chậm thì app retry, retry làm hệ chậm thêm, và app lại retry nhiều hơn.

Với loại thứ nhất, đi tìm bộ phận hỏng là đúng việc. Với loại thứ hai, tôi có thể đọc code tới sáng mà dòng nào cũng đúng. Không có bộ phận hỏng để tìm, thì phải nhìn vào cấu trúc.

III. Hai tính chất quyết định rủi ro, theo Perrow #

Charles Perrow, nhà xã hội học người Mỹ, từng phân tích sự cố nhà máy điện hạt nhân Three Mile Island năm 1979. Trong cuốn Normal Accidents (1984), ông xếp các hệ thống theo hai tính chất. Chữ “normal” ở đây nghĩa là không tránh được với cấu trúc đó, không phải là thường xuyên.

Interactive complexity: các phần ảnh hưởng nhau theo đường không ai vẽ ra #

Interactive complexity là mức độ các phần ảnh hưởng nhau theo những đường không ai lên kế hoạch hay dễ nhìn ra, bất kể hệ có bao nhiêu phần. Dấu hiệu: sửa A làm hỏng D, dù A và D không hề gọi nhau. Trong phần mềm, nó đến từ phụ thuộc bắc cầu, dữ liệu nhiều service cùng sửa, và các lớp trung gian chen giữa các cuộc gọi. Phụ thuộc bắc cầu nghĩa là A cần B, B cần C, nên C hỏng thì A cũng hỏng.

Coupling: giữa các phần còn bao nhiêu khoảng đệm #

Coupling đo khoảng đệm giữa hai phần: thời gian dư, chỗ chứa tạm, đường vòng. Tight coupling là khi gần như hết khoảng đệm; loose coupling là khi còn nhiều.

Ai thấy shipper đứng chờ trước quán cơm trưa đã thấy tight coupling: bếp trễ mười phút thì shipper và các đơn sau đều trễ theo. Một cái kệ để món xong chờ shipper tới lấy là loose coupling: bên nào chậm, bên kia vẫn làm việc khác. Phép so sánh có một giới hạn: cái kệ không làm món ra nhanh hơn, đơn nấu trễ vẫn giao trễ; nó chỉ giữ cho bên chậm không giữ chân bên kia.

Trong phần mềm, năm service gọi nhau đồng bộ là tight coupling. Gọi đồng bộ nghĩa là bên gọi đứng chờ trả lời rồi mới làm việc khác, như shipper ở quầy, nên một service chậm là cả chuỗi đứng. Hàng đợi giữa hai service là loose coupling. Hàng đợi giữ việc chờ xử lý như cái kệ giữ món: bên nhận tắt mười phút, việc vẫn nằm đó chờ.

Rủi ro tăng theo tích của hai tính chất #

Perrow không đưa ra phương trình cho chuyện này; mô hình dưới đây là bản phác thảo về hình dạng rủi ro, không phải công cụ dự báo. Trong mô hình, rủi ro tích lũy theo thời gian vận hành và theo tích của hai tính chất, không phải tổng. Hệ chằng chịt nhưng còn nhiều đệm vẫn sống tốt, và hệ đơn giản ít đệm cũng vậy. Chỉ khi cả hai cùng cao, xác suất tai nạn mới tiến sát 100%, và câu hỏi “có sập không” thành “khi nào sập”. Ví dụ, ở cùng mức phức tạp 1 và trong một đơn vị thời gian, loose coupling cho xác suất tai nạn khoảng 39%, tight coupling khoảng 95%.

Đào sâu: mô hình rủi ro viết thành công thức
$$P_{acc} = 1 - e^{-\int (\chi \cdot \tau)\, dt}$$

Đọc bằng lời: xác suất chưa gặp tai nạn nào giảm dần khi tích hai tính chất cộng dồn theo thời gian, và $P_{acc}$ là phần còn lại cho đủ 1.

  • $P_{acc}$: xác suất đã có ít nhất một tai nạn hệ thống, từ 0 đến 1.
  • $\chi$, $\tau$: interactive complexity và coupling, thang không đơn vị, ánh xạ sang bảng dưới; $\tau$ càng lớn càng chặt.
  • $t$: thời gian vận hành, đơn vị tùy chọn.

Trong một đơn vị thời gian, tích phân bằng $\chi \cdot \tau$. Với $\chi = 1$: $\tau = 0{,}5$ cho $1 - e^{-0{,}5} \approx 39\%$, còn $\tau = 3$ cho $1 - e^{-3} \approx 95\%$.

Quy đổi sang hệ thống thật #

Khái niệmNói bằng lờiĐo gì trong hệ thống thật
Interactive complexityẢnh hưởng theo đường không ai thấySố phụ thuộc bắc cầu; dữ liệu nhiều service cùng sửa; số lớp trung gian mỗi cuộc gọi
Tight couplingHết khoảng đệm, như shipper chờ ở quầyĐộ dài chuỗi gọi đồng bộ; giới hạn chờ cứng thấp hơn mức 1% request chậm nhất cần; khóa buộc nhiều máy xếp lượt
Hỏng hóc thành phầnMột bộ phận hỏng, hậu quả nằm yênBug lặp lại được, ảnh hưởng hẹp, có ranh giới chặn từ lúc thiết kế
Tai nạn hệ thốngPhần nào cũng đúng, cả hệ vẫn đứngDashboard toàn xanh trong khi hệ thống đã ngừng phản hồi

Còn một câu hỏi: chuyện nhỏ thành sự cố lớn bằng cách nào?

IV. Vì sao hệ thống kẹt lại mà không tự hồi phục #

Quay lại đợt flash sale. 12 giờ 01, service thanh toán chậm lại và nhiều request quá timeout. Timeout là thời gian tối đa bên gọi chịu chờ trước khi bỏ cuộc; sau đó app retry. Retry có mặt để app bền hơn, và ở đây nó làm việc đó bằng cách nhân tải lên đúng chỗ đang nghẽn: cho thử lại tối đa hai lần, mỗi request chậm có thể thành ba.

Chuyện cục bộ thường hóa toàn cục tại một tài nguyên dùng chung bị cạn, như luồng xử lý, vùng nhớ đệm của mạng hay kết nối tới database. Dễ hình dung nhất là thread pool. Thread pool là nhóm cố định các luồng xử lý trong server, mỗi luồng lo một request một lúc, như những người đứng bếp ở quán.

Pool còn chỗ thì một lần retry chỉ là một request thừa. Pool đã kín thì nó phải xếp hàng và đẩy một request khác quá timeout, rồi request đó lại sinh thêm retry. Hành vi an toàn trên một máy thành cơn bão trên nghìn máy, không phải vì nó đổi khác, mà vì khoảng đệm đã hết.

Hệ rơi vào vòng này khi lỗi lan nhanh hơn khả năng hấp thụ của nó. Bài 02 gọi trạng thái cuối là metastable failure. Đó là kiểu sập tự duy trì sau khi cú đẩy đã qua: người mua đã vãn, trang thanh toán vẫn quay vòng. Sơ đồ dưới đi theo con đường đó từ trên xuống.

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 phần?} B -- "Không: gọi đồng bộ, timeout cứng" --> C[Tai nạn hệ thống thành chuyện không tránh được] B -- "Còn: hàng đợi, xử lý sau" --> D[Lỗi cục bộ, phục hồi được] C --> E[Vòng tự khuếch đại: retry storm] E --> F[Tài nguyên dùng chung cạn: thread pool, hàng chờ] F --> G[Metastable failure: sập và không tự hồi] style C fill:#F97316,stroke:#171717,color:#FFFFFF style G fill:#171717,stroke:#F97316,color:#F97316

Mọi thành phần vẫn chạy đúng, nhưng ô nguyên nhân gốc cần được điền, và ứng viên gần tay nhất thường là con người.

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

Giả sử 12 giờ 05, người trực khởi động lại database đang quá tải, và hệ chậm thêm một nhịp trong lúc database nạp lại dữ liệu hay dùng vào bộ nhớ. Biên bản gần như tự viết: “Nguyên nhân: người trực thao tác sai.”

Kết luận đó thường là một stop rule1. Stop rule là điểm cuộc điều tra dừng lại. Ở đây, đó là chỗ tiện để đóng hồ sơ, hay được chọn vì rẻ hơn việc thừa nhận kiến trúc có vấn đề.

Báo cáo STELLA (2017) của SNAFUcatchers, một nhóm nhà nghiên cứu và công ty công nghệ, mô tả chỗ đứng của người trực bằng một đường vạch. Dưới vạch là hệ thống đang chạy, không ai nhìn thấy trực tiếp. Trên vạch là con người, với hình dung về hệ thống chỉ dựng từ các màn hình như dashboard và cảnh báo. Hình dung đó không bao giờ khớp hoàn toàn với hệ thống thật.

Đọc lại sự cố khi đã biết kết cục, tôi luôn thấy rõ lẽ ra phải làm gì; người trực lúc 12 giờ 05 chỉ có bức tranh trên màn hình. Hai khái niệm giải thích vì sao “lỗi con người” gần như luôn là nhãn sai:

  • Local rationality. Hai nhà nghiên cứu an toàn Sidney Dekker và Richard Cook lập luận rằng không ai đi làm để phá hệ thống. Local rationality nghĩa là từ chỗ người đó đứng, với những gì dashboard cho thấy, hành động của họ là hợp lý. Nó chỉ trông sai khi nhìn lại, với thông tin lúc đó chưa ai có. Trách người ta vì điều “lẽ ra phải biết” là phạt họ vì thiếu thứ hệ thống chưa từng đưa cho họ.
  • Graceful extensibility2. Nhà nghiên cứu an toàn David Woods dùng tên này cho khả năng của hệ thống nới rộng cách ứng phó ra ngoài thiết kế. Trong sự cố lớn, người vận hành thường là nguồn phục hồi chứ không phải nguồn lỗi. Có người bớt tải theo cách không runbook nào viết, có người tắt phần gợi ý sản phẩm để giữ trang thanh toán sống. Runbook là tài liệu hướng dẫn từng bước cho một loại sự cố. Tự động hóa khó làm việc này, vì nó chỉ xử lý kịch bản đã có người lường trước.

Vậy cuộc điều tra nên đi tiếp về đâu?

VI. Thôi tìm con domino đầu tiên: STAMP và CAST #

RCA hình dung một hàng domino: tìm con đổ đầu tiên, dựng lại, xong. Trong đợt flash sale, domino xếp thành mạng, và nhiều con đổ cùng lúc.

Nancy Leveson, giáo sư ở MIT, đề xuất STAMP, một mô hình tai nạn thay chuỗi sự kiện bằng cấu trúc kiểm soát. Cấu trúc kiểm soát gồm các ràng buộc an toàn của hệ, cùng những người và cơ chế có nhiệm vụ giữ chúng.

Ràng buộc an toàn là điều hệ thống không bao giờ được vi phạm, viết thành câu cụ thể. Theo STAMP, tai nạn xảy ra khi một ràng buộc không được giữ đủ chặt, dù vì một bộ phận hỏng hay vì các bộ phận đều đúng mà tương tác sai.

CAST là cách điều tra đi kèm STAMP. Thay vì dừng ở “cái gì hỏng” hay “ai làm sai”, nó soát cả cấu trúc kiểm soát. Phần công nghệ gồm Kubernetes, circuit breaker và cảnh báo; phần tổ chức gồm quy trình duyệt code mới, quy định trực và ngưỡng cảnh báo3. Circuit breaker là cơ chế ngắt cuộc gọi tới một service cứ lỗi liên tục, như cầu dao điện tự nhảy.

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% bản sao của một service trên cùng một máy”.
  2. Cơ chế nào lẽ ra phải giữ nó, 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 lẽ ra phải thấy gì mà đã không thấy? Vòng phản hồi là đường tín hiệu từ hệ thống quay về người hay cơ chế điều khiển nó.

RCA cho ra “sửa bug ở service X”, hay với đợt flash sale, “nhắc người trực cẩn thận hơn”. CAST cho ra “chưa có gì phát hiện hay chặn vi phạm ràng buộc này; hãy dựng nó”. Danh sách của CAST dài hơn và khó giao cho một người, và đó chính là điểm của nó.

VII. Biểu đồ: coupling chặt đẩy rủi ro lên sớm thế nào #

Biểu đồ vẽ mô hình ở Mục III với ba mức coupling, để nhìn chứ không để dự báo. Trục ngang là interactive complexity, trục dọc là xác suất tai nạn từ 0 đến 1. Đường xám là loose coupling (0,5), cam nhạt là mức vừa (1,5), cam đậm là tight coupling (3); hãy nhìn đường cam đậm đã sát trần khi đường xám mới lên khoảng một phần ba.

Metaphorical Risk of Systemic Collapse

Siết chặt một hệ phức tạp thì rủi ro dựng đứng từ sớm: với tight coupling, rủi ro vượt 90% khi độ phức tạp mới khoảng 0,8; với loose coupling, phải tới khoảng 4,6. Chỉ đổi cách các phần chờ nhau, dư địa độ phức tạp trước ngưỡng đó đã rộng gấp sáu lần. Con số sáu không phải phát hiện, chỉ là 3 chia 0,5 nằm sẵn trong phép nhân. Biểu đồ vẽ từ công thức thì không cãi lại được công thức.

Đào sâu: code vẽ biểu đồ

Code dưới đây tính mô hình trong một đơn vị thời gian cho 100 mức độ phức tạp và vẽ ba đường; ngưỡng 90% nằm ở $\chi \cdot \tau = \ln 10 \approx 2{,}30$.

 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):
    # Mô hình phác thảo: rủi ro tăng theo tích hai tính chất, tiến dần 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('Metaphorical Risk of Systemic Collapse')
plt.xlabel('Interactive Complexity (χ)')
plt.ylabel('P(Accident)')
plt.legend()
plt.show()

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

  1. Tạo khoảng đệm thời gian. Ở chỗ nghiệp vụ cho phép, thay cuộc gọi đồng bộ bằng việc gửi vào hàng đợi, như cái kệ ở quán. Theo mô hình ở Mục III, giảm coupling có tác dụng ngang giảm độ phức tạp cùng tỉ lệ, và theo tôi thường dễ làm hơn nhiều. Câu nên hỏi đội kỹ thuật: khi người dùng bấm “Thanh toán”, bao nhiêu service phải chờ nhau?

  2. Đầu tư vào khả năng quan sát, không chỉ thu thập số liệu. Khả năng quan sát là hiểu được hệ đang làm gì khi nhìn từ bên ngoài, thứ giúp thu hẹp khoảng cách mà STELLA mô tả. Dashboard 200 biểu đồ không giúp gì nếu thứ bạn cần là biểu đồ thứ 201. Câu nên hỏi không phải “đã thu số đo này chưa”, mà là “lúc 2 giờ sáng, người trực có thấy ràng buộc nào đang bị vi phạm không?”

  3. Chừa chỗ cho con người ra tay. Khi chạm giới hạn, kiến trúc phải có sẵn tay gạt thay vì khóa cứng vào trạng thái kẹt: circuit breaker bật bằng tay, công tắc tắt tính năng, van xả tải. Van xả tải là cơ chế chủ động từ chối bớt request để phần còn lại vẫn chạy.

  4. Bảo vệ ràng buộc, không chỉ bảo vệ thành phần. Viết các ràng buộc an toàn thành câu cụ thể và dựng cơ chế phát hiện khi chúng bị vi phạm. Điều này áp dụng cho cả ràng buộc tổ chức, như “không đưa code mới lên trong hai giờ trước flash sale”.

IX. Ta giữ được ràng buộc, không giữ được cả hệ #

Normal Accidents Theory và STAMP cho tôi một góc nhìn khiêm tốn mà thực dụng: không ai kiểm soát trọn một hệ phức tạp. Ta chỉ đặt ra và giữ được các ràng buộc chi phối nó. Công sức nên dồn vào việc làm hệ chịu được những thứ chưa ai lường trước, hơn là đuổi theo một nguyên nhân gốc. Lần tới gặp ô “Nguyên nhân gốc”, thử ghi vào đó ràng buộc nào đã không ai giữ.

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


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

  2. Graceful extensibility: Woods (2018) đặt khái niệm này đối lập với brittleness, kiểu hệ gãy đột ngột khi gặp tình huống ngoài thiết kế. Con người thường lấp vào những khoảng trống mà tự động hóa chưa được lập trình để xử lý. ↩︎

  3. Blunt end / sharp end: trong lý thuyết an toàn, “sharp end” là tiền tuyến, tức người trực tiếp thao tác trên hệ thống thật. “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. ↩︎