
Hành trình 6 tuần truy vết bug 'ma' trong thư viện hyper: Khi hiệu năng vô tình che giấu lỗi hệ thống
Cloudflare chia sẻ quá trình gỡ lỗi kéo dài 6 tuần đối với một lỗi race condition ẩn mình trong thư viện HTTP hyper của Rust. Sự cố này khiến dữ liệu hình ảnh bị cắt ngắn không rõ nguyên nhân sau khi kiến trúc hệ thống được tối ưu hóa. Bài viết phân tích sâu về cách họ sử dụng strace, cơ chế socket buffer và cách một thay đổi nhỏ trong mã nguồn Rust đã giải quyết vấn đề triệt để.
Hành trình truy vết bug "ma" trong thư viện hyper
Tại Cloudflare, dịch vụ Images — được xây dựng bằng Rust trên nền tảng Workers — đóng vai trò quan trọng trên mạng lưới biên của chúng tôi. Để xử lý các kết nối HTTP, chúng tôi tin dùng hyper, một thư viện HTTP nguồn mở phổ biến trong hệ sinh thái Rust. Tuy nhiên, một thay đổi nhỏ trong kiến trúc vào cuối năm 2025 đã vô tình làm bộc lộ một lỗi race condition tiềm ẩn từ lâu.

Bối cảnh sự cố
Binding Images cho phép các Worker thực hiện các quy trình tùy chỉnh để xử lý hình ảnh từ xa. Sau khi kiến trúc lại binding để kết nối trực tiếp hơn giữa Workers runtime và dịch vụ Images, người dùng bắt đầu báo cáo về việc yêu cầu xử lý hình ảnh bị lỗi, đặc biệt là với các tệp có kích thước lớn. Điều kỳ lạ là phản hồi trả về vẫn là HTTP 200 OK mà không có bất kỳ thông báo lỗi nào trong log; dữ liệu hình ảnh đơn giản là bị cắt ngắn.
Nguyên lý vận hành và điểm nghẽn
Binding giao tiếp với dịch vụ Images thông qua các kết nối socket. Dữ liệu được đẩy vào các bộ đệm (buffer) của nhân hệ điều hành. Khi hyper xử lý phản hồi:
hypernhận dữ liệu đã mã hóa từ dịch vụ Images.hyperghi dữ liệu vào bộ đệm nội bộ.hyperxả (flush) dữ liệu từ bộ đệm nội bộ vào bộ đệm socket outbound.
Nếu phía người đọc (reader) xử lý dữ liệu đủ nhanh, hyper sẽ hoàn tất quá trình và đóng kết nối. Tuy nhiên, nếu phía đọc chậm, bộ đệm outbound sẽ đầy và hyper phải chờ. Vấn đề nảy sinh khi thời điểm hyper quyết định đóng kết nối không đồng bộ với quá trình xả dữ liệu còn tồn đọng trong bộ đệm.
Quá trình điều tra: Đối mặt với lỗi "ma"
Chúng tôi đã mất 6 tuần để truy vết sự cố này với hàng loạt giả thuyết:
- Kiểm tra thời gian chờ (Timeouts): Không liên quan vì lỗi không phụ thuộc vào độ dài yêu cầu.
- Cập nhật phiên bản hyper: Từ 0.14.x lên 1.8.x nhưng lỗi vẫn tồn tại, chứng tỏ đây là một vấn đề gốc rễ.
- Phân tích Workers runtime: Loại trừ khả năng lỗi từ phía client của runtime.
Bước ngoặt với strace
Sử dụng strace để ghi lại các syscall cho thấy sự khác biệt rõ rệt giữa yêu cầu thành công và yêu cầu thất bại. Khi lỗi xảy ra, chỉ một phần nhỏ dữ liệu được ghi vào socket, sau đó shutdown(42, SHUT_WR) được gọi ngay lập tức:
sendto(42, "HTTP/1.1 200 OK\r\nContent-Length: 14991808\r\n...", ...) = 219264
shutdown(42, SHUT_WR) = 0
Dịch vụ Images tin rằng nó đã gửi xong toàn bộ dữ liệu, trong khi thực tế dữ liệu vẫn còn kẹt trong bộ đệm của hyper.
Nguyên nhân gốc rễ: Lỗi trong code
Vấn đề nằm trong tệp dispatch.rs của hyper, cụ thể là vòng lặp poll_loop:
fn poll_loop(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
loop {
let _ = self.poll_read(cx)?;
let _ = self.poll_write(cx)?;
let _ = self.poll_flush(cx)?; // Vị trí gây lỗi
if !self.conn.wants_read_again() {
return Poll::Ready(Ok(()));
}
}
}
Việc sử dụng let _ đã vô tình loại bỏ kết quả trả về của poll_flush(cx). Nếu flush trả về Poll::Pending (cho biết bộ đệm chưa trống), vòng lặp vẫn tiếp tục thực hiện wants_read_again() và có khả năng thoát sớm, đóng kết nối khi dữ liệu vẫn còn đang nằm trong hàng đợi.
Kết luận
Sự cố này nhắc nhở chúng ta rằng ngay cả những thay đổi nhỏ trong kiến trúc hệ thống (như việc thay đổi cơ chế kết nối từ network sang Unix socket) cũng có thể làm bộc lộ những lỗi logic ẩn sâu trong các thư viện nền tảng vốn đã hoạt động ổn định trong nhiều năm. Chỉ với 4 dòng code sửa đổi để xử lý đúng tín hiệu Poll::Pending, chúng tôi đã giải quyết hoàn toàn vấn đề "cắt ngắn dữ liệu" mà không cần bất kỳ sự can thiệp lớn nào khác.
Do you like this post?
Upvote to push this post higher on the community feed
