Lỗi ECONNRESET trong TCP: Khi nào kết nối bị reset và tại sao?

Phần mềm17 tháng 5, 2026·4 phút đọc

Bài viết phân tích nguyên nhân kỹ thuật đằng sau lỗi ECONNRESET phổ biến khi làm việc với các socket TCP. Tác giả giải thích việc đóng kết nối trong khi vẫn còn dữ liệu chờ xử lý sẽ kích hoạt gói tin RST, đồng thời chia sẻ giải pháp khắc phục cho vấn đề giữa Nginx và Gunicorn.

Lỗi ECONNRESET trong TCP: Khi nào kết nối bị reset và tại sao?

Hãy tưởng tượng bạn có hai dịch vụ đang chạy trên cùng một máy chủ. Một dịch vụ mở một socket lắng nghe TCP (server) và dịch vụ kia kết nối đến nó (client). Chúng trao đổi dữ liệu bình thường, nhưng thỉnh thoảng, phía client nhận được lỗi ECONNRESET khi đọc dữ liệu từ socket, mà không hề có crash hay log lỗi nào khác. Vấn đề này đến từ đâu?

minh họa mạngminh họa mạng

Thiết lập thử nghiệm

Để tái hiện vấn đề, chúng ta sẽ tạo một chương trình "server" đơn giản: mở một socket TCP mới, chờ kết nối và fork một tiến trình mới cho mỗi yêu cầu. Nhiệm vụ của server chỉ là gửi 600.000 byte dữ liệu về client ngay khi kết nối được thiết lập. Con số này đủ lớn để kích hoạt hành vi mà chúng ta đang tìm hiểu.

minh họa codeminh họa code

Ở phía client, nó kết nối đến cổng 8125 trên localhost và gọi recv() cho đến khi kết thúc (EOF) hoặc xảy ra lỗi. Client có một cờ --spam thú vị: khi được kích hoạt, client sẽ gửi một ít dữ liệu đến server trước khi cố gắng nhận dữ liệu về. Và chính hành động này dường như gây ra sự cố: recv() của client trả về -1 và errno được thiết lập là 104 (Connection reset by peer).

Rõ ràng, đây là một gói tin TCP RST. Vậy nó đến từ đâu?

Điều tra với Strace

Chúng ta gắn strace vào server để xem xét. Kết quả cho thấy server không bị crash. Nó fork tiến trình con, sử dụng sendto() để đổ toàn bộ 600.000 byte dữ liệu cho client và sau đó... thoát ngay lập tức.

Điều đáng chú ý là sendto() báo đã gửi thành công toàn bộ 600.000 byte. Tuy nhiên, như tài liệu hướng dẫn (manpage) lưu ý: "Hoàn thành thành công cuộc gọi sendto() không đảm bảo việc truyền tải thông tin". Đó chỉ là dữ liệu được đưa vào bộ đệm cục bộ.

Có vẻ như không có sự khác biệt trong hành vi của server dù client có dùng --spam hay không. Server chỉ việc gửi xong dữ liệu và đóng kết nối.

Nguyên nhân thực sự: Việc đóng kết nối sớm

Thử nghiệm cho thấy vấn đề nằm ở thời điểm đóng kết nối. Hãy thêm một độ trễ sleep(1) trước khi gọi close() trong hàm serve_client(), và kết quả thay đổi ngay lập tức.

Giả thuyết được đưa ra: Khi server đóng kết nối (gọi close()) trong khi vẫn còn dữ liệu chờ được đọc trong bộ đệm nhận của chính nó (dữ liệu do client gửi tới nhưng server chưa kịp recv()), thì stack TCP sẽ gửi gói tin RST thay vì trình tự đóng kết nối bình thường (FIN).

lady debuglady debug

RFC 793 (các đặc tả kỹ thuật của TCP) cũng đề cập đến điều này: Một máy chủ có thể thực hiện chuỗi đóng "bán song song" (half-duplex). Nếu gọi CLOSE trong khi dữ liệu nhận vẫn đang chờ xử lý trong TCP, hoặc dữ liệu mới đến sau khi CLOSE được gọi, TCP của máy chủ đó NÊN gửi RST để báo hiệu rằng dữ liệu đã bị mất.

Thực tế với Nginx và Gunicorn

Tác giả gặp vấn đề này trong một hệ thống thực tế: Nginx hoạt động như reverse proxy cho một ứng dụng Flask chạy trên Gunicorn. Thỉnh thoảng, Nginx nhận được lỗi ECONNRESET từ Gunicorn.

Quá trình xử lý như sau:

  1. Nginx chuyển tiếp yêu cầu HTTP tới Gunicorn thông qua hai cuộc gọi hệ thống writev(): một cho header và một cho body.
  2. Gunicorn đọc dữ liệu từ socket.
  3. Đôi khi, Gunicorn chỉ nhận được dữ liệu từ lệnh writev() đầu tiên (header). Có vẻ Gunicorn hoặc ứng dụng Flask bên trong khá "lười": nếu không có phần code nào truy cập vào body của request, nó sẽ không bother gọi recv() để đọc phần còn lại.

Vấn đề nảy sinh khi Gunicorn kết thúc giao dịch này: Nó gửi phản hồi xong và đóng socket ngay lập tức (close()). Nếu tại thời điểm đó, phần body của yêu cầu vẫn còn nằm trong bộ đệm socket của Gunicorn chưa được đọc, việc đóng kết nối này sẽ gây ra gói tin RST.

Giải pháp và lưu ý

Cách khắc phục (workaround) là đảm bảo ứng dụng Python thực hiện một thao tác giả định (dummy operation) lên phần body của HTTP để đảm bảo nó đã được đọc hoàn toàn từ socket. Kể từ đó, lỗi ECONNRESET không còn xuất hiện.

Tuy nhiên, cần lưu ý rủi ro bảo mật: Nếu ai đó gửi một yêu cầu POST với dữ liệu lên tới 10GB tới server của bạn mà bạn chỉ có 1GB RAM, việc cố đọc toàn bộ body có thể gây vấn đề. Cấu hình client_max_body_size trong Nginx có thể giúp ngăn chặn điều này.

Hiểu rõ cách thức hoạt động của TCP và hành vi của các hàm socket là rất quan trọng để xây dựng các hệ thống mạng ổn định và dễ debug.

Chia sẻ:FacebookX
Nội dung tổng hợp bằng AI, mang tính tham khảo. Xem bài gốc ↗