[Series Die-To-Redeliver] Phần 1: Tư duy "Thà chết chứ không giấu lỗi" (Crash-Only Software)
Chào anh em,
Trong suốt những năm tháng làm nghề và review code, mình nhận thấy có một thói quen rất phổ biến mà hầu như anh em dev nào (kể cả mình ngày xưa) cũng mắc phải: Sợ lỗi và tìm mọi cách để giấu lỗi. Chúng ta thường có xu hướng bọc try...catch ở mọi ngóc ngách. Có lỗi à? console.log(error) hoặc ghi vào file log nào đó, return false, rồi cầu mong cho mọi chuyện êm đẹp và tiến trình (Worker) vẫn tiếp tục chạy rầm rầm. Nhưng anh em ạ, trên môi trường Production, việc cố gắng cho hệ thống "sống ngoi ngóp" khi đang mang mầm bệnh lại chính là nguyên nhân dẫn đến những thảm họa data đau đầu nhất. Hôm nay, mình sẽ mở bát series "Die-To-Redeliver" bằng một khái niệm nghe rất cực đoan nhưng lại là kim chỉ nam cho các hệ thống phân tán: Tư duy Crash-Only Software (Thà chết chứ không giấu lỗi).
1. Bệnh lạm dụng try...catch và những "Zombie Worker"
Hãy tưởng tượng bạn đang viết một Background Worker để xử lý đơn hàng từ RabbitMQ. Luồng xử lý cơ bản là:
- Lấy message từ Queue.
- Trừ tiền user qua API của cổng thanh toán.
- Cập nhật trạng thái đơn hàng vào Database.
Đùng một cái, Database bị nghẽn (Connection Timeout). Nếu code theo tư duy thông thường, anh em sẽ bọc try...catch đoạn update DB, ghi log “Lỗi kết nối DB”, rồi... cho Worker chạy tiếp sang message khác.
Chuyện gì xảy ra? Tiến trình Worker đó vẫn sống. Nó tiếp tục cắn hàng ngàn message khác từ Queue. API cổng thanh toán vẫn bị gọi, tiền user vẫn bị trừ, nhưng Database thì không lưu được trạng thái! Bạn vừa tạo ra một Zombie Worker — một cái xác không hồn, chỉ biết đi phá hoại data chứ không mang lại giá trị gì. Đến khi khách hàng gọi điện lên chửi vì bị trừ tiền mà không thấy đơn đâu, anh em lôi file log ra đọc thì ôi thôi, hàng vạn dòng log báo lỗi DB trôi tuột đi rồi. Vớt vát làm sao được nữa?
2. Crash-Only Software: Chết là một tính năng!
Để giải quyết bài toán trên, các kỹ sư hệ thống trên thế giới áp dụng một tư duy có tên là Crash-Only Software.
Thay vì cố gắng dự đoán mọi rủi ro và xử lý lỗi một cách cồng kềnh, nếu hệ thống gặp phải những sự cố cốt lõi không thể tự phục hồi ngay (mất mạng, rớt DB, hết Memory,...), hãy chủ động cho tiến trình đó chết ngay lập tức (Fail Fast).
Tại sao "chết" lại an toàn hơn?
- Ngăn chặn phá hoại: Worker chết đồng nghĩa với việc nó ngừng kéo message mới. Không còn rủi ro hỏng data hàng loạt.
- Dễ debug: Một service báo "Offline" hoặc "Restarting 10 times" trên Dashboard sẽ đập ngay vào mắt team Ops/DevOps để kịp thời xử lý, thay vì một service báo "Healthy" nhưng lại âm thầm nhả lỗi vào file log.
- Ủy quyền phục hồi cho hạ tầng: Đây chính là điểm ăn tiền. Chúng ta không bắt code phải gánh việc phục hồi, mà đẩy việc đó cho hệ sinh thái hạ tầng.
3. "Die-To-Redeliver" bước ra ánh sáng
Từ tư duy Crash-Only, chúng ta đi đến hạt nhân của series này: Pattern Die-To-Redeliver trong việc xử lý Message Queue.
Sự kết hợp này hoàn hảo đến mức đáng kinh ngạc. Quy trình giờ đây đơn giản và gọn gàng hơn rất nhiều:
- Worker nhận Job.
- Có lỗi ngoại cảnh (Ví dụ: Call API bên thứ 3 bị 500, DB rớt mạng).
- Code của bạn thay vì
try...catchlằng nhằng, chỉ cần ném ra một Exception chưa được catch (Unhandled Exception) hoặc gọi lệnhexit(1). Worker chính thức đột tử. - Kết nối TCP giữa Worker và Message Broker (như RabbitMQ/Kafka) bị đứt cái phựt.
- Broker nhận ra: "Chết dở, thằng Worker chết mà chưa kịp báo cáo hoàn thành (chưa gửi cờ ACK)".
- Broker ngay lập tức đưa message đó trở lại Queue (Redeliver) để chờ một Worker khác khỏe mạnh hơn xử lý, hoặc chờ chính thằng Worker vừa chết được (Supervisor/Kubernetes) hồi sinh lại rồi xử lý tiếp.
Kết quả: Bạn không cần phải thiết kế bảng failed_jobs, không cần cronjob để chạy lại những job lỗi, không cần viết logic retry phức tạp trong code. Hạ tầng (Infrastructure) tự động lo liệu tất cả thông qua "Cái chết" của tiến trình.
Tổng kết
Tư duy "Thà chết chứ không giấu lỗi" không khuyến khích việc code ẩu để hệ thống hở tí là sập. Nó yêu cầu chúng ta phân định rạch ròi: Lỗi nào là lỗi nghiệp vụ (Validate sai, user không tồn tại) thì phải xử lý êm đẹp; Lỗi nào là lỗi hạ tầng, môi trường thì cứ dũng cảm mà "chết" để bảo toàn tính toàn vẹn của dữ liệu.
Tuy nhiên, "chết" thì dễ, nhưng làm sao để hệ thống dọn dẹp bãi chiến trường sau cái chết, và làm sao để những message bị Redeliver không gây ra lỗi "trừ tiền 2 lần"?
Tất cả những bí mật đó nằm ở cơ chế ACK/NACK và những cạm bẫy phía sau vòng đời của một Message. Anh em hãy đón chờ Phần 2: Sau cái chết là gì? (Cơ chế ACK/NACK & Redeliver) nhé. Cảm ơn anh em đã đọc bài!
All rights reserved