Ghi Chú Nhỏ Về Lỗi Liên Quan Đến Xử Lý Song Song - nói dối e blog

Ghi Chú Nhỏ Về Lỗi Liên Quan Đến Xử Lý Song Song

Hôm nay mình đã giải quyết được một lỗi tồn đọng từ lâu, qua đó hiểu rõ bản chất của hàng loạt hiện tượng kỳ lạ đã từng xảy ra trước đây. Tâm trạng vô cùng hào hứng!

Câu chuyện bắt đầu khi một yêu cầu mới hôm nay khiến mình phải đọc lại mã nguồn cũ, phát hiện ra một thiết kế khiếm khuyết. Lỗi này có thể tạo áp lực cực lớn cho một module hệ thống trong những điều kiện đặc biệt, vô tình tạo thành một bài kiểm tra hiệu năng cường độ cao. Nhờ đó mình đã hoàn thiện lại phần xử lý song song trước đây chưa tối ưu.

Vì quá phức tạp để diễn giải ngắn gọn, mình quyết định viết bài blog này để ghi lại chi tiết.

Bối cảnh ban đầu (tuần trước):
Bạn Anh Nam phát hiện ra hiện tượng lạ khi dùng robot test tải hệ thống: Khi tất cả robot đều hoạt động, CPU không quá tải, nhưng khi tắt hết robot, hệ thống chạy không lại khiến CPU tăng vọt. Dù cả ngày debug không tìm thấy deadlock nào, hiện tượng này vẫn gây ra tình trạng người dùng mới không thể đăng nhập được - một vấn đề nghiêm trọng không thể bỏ qua.

Giải pháp tạm thời:
Chúng mình phát hiện ra chỉ cần giảm số luồng làm việc của Skynet xuống bằng số nhân CPU là hiện tượng biến mất. Tuy nhiên, mình luôn băn khoăn về nguyên nhân thực sự.

Phân tích sâu hơn:
Khi CPU tăng cao, phần lớn tài nguyên bị tiêu hao cho thao tác vào/ra hàng đợi tin nhắn toàn cục. Nguyên nhân là do luồng làm việc quay vòng (spin) vô ích khi có hàng trăm luồng làm việc nhưng hàng đợi trống rỗng. Điều này khiến xung đột khóa quay (spinlock) trở nên nghiêm trọng, làm các luồng có công việc thực sự khó tiếp cận tài nguyên.

Cải tiến thiết kế:
Hàng đợi toàn cục của mình đơn giản chỉ chứa các hàng đợi tin nhắn cấp 2 của các dịch vụ. Mỗi luồng làm việc đều bình đẳng, lấy một hàng đợi cấp 2 ra xử lý tin nhắn rồi đẩy lại vào. Mình nhận ra có thể thiết kế hàng đợi này không dùng khóa (lock-free) để loại bỏ xung đột.

Thách thức khi triển khai:
Ban đầu mình nghĩ chỉ cần vài dòng mã cho cấu trúc vòng lặp không khóa, nhưng thực tế phức tạp hơn nhiều. Khi có quá nhiều luồng, một luồng có thể bị “đói” (starvation). Dù dùng atomic để di chuyển con trỏ vòng lặp, vẫn không đảm bảo đọc được dữ liệu ngay lập tức. Ngay cả khi hàng đợi 64K phần tử chỉ chứa vài tin nhắn, hiện tượng vòng lặp quấn ngược (wrap-around) vẫn xảy ra.

Giải pháp then chốt:
Sau nhiều lần thất bại với kỹ thuật busy-wait (chờ tích cực), mình thay đổi ngữ nghĩa API hàng đợi: Thay vì trả về NULL chỉ khi hàng đợi trống, giờ đây NULL có thể do xung đột cạnh tranh. Trong ngữ cảnh sử dụng duy nhất của mình, thay đổi này hợp lý và đơn giản hóa mã nguồn đáng kể.

Bài học từ yêu cầu mới:
Hôm nay bạn Mike yêu cầu thêm giao diện gửi tin nhắn đa điểm đến nhiều đích. Mình phát hiện ra mã nguồn cũ quá phức tạp do cố gắng tối ưu hóa. Trong hệ thống Skynet, các đối tượng dịch vụ được tham chiếu qua handle số học thay vì con trỏ C trực tiếp. Cách này giúp quản lý vòng đời đối tượng dễ dàng hơn nhưng lại gây ra chi phí tra cứu hash.

Vấn đề gốc rễ:
Việc cache con trỏ C để tối ưu đa điểm đã dẫn đến lỗi đếm tham chiếu (reference count) không chính xác. Khi một đối tượng thoát nhóm, nếu không xóa ngay khỏi cấu trúc nhóm (dùng hash set kém hiệu quả), đếm tham chiếu sẽ không về 0. Điều này khiến hệ thống không xóa được hàng đợi tin nhắn tương ứng, dẫn đến luồng làm việc liên tục thử/xóa hàng đợi, gây nghẽn CPU và xung đột khóa nghiêm trọng.

Kết luận:
Qua quá trình này, mình hiểu rõ hơn về các bẫy tiềm ẩn trong thiết kế hệ thống phân tán, đặc biệt là trong việc cân bằng giữa tối ưu hiệu năng và độ phức tạp mã nguồn. Bài học lớn nhất: Đôi khi giải pháp đơn giản nhất lại hiệu quả nhất!

0%