Go 1.27 khắc phục vấn đề tái sử dụng kết nối HTTP/1 với các phần thân phản hồi chưa được đọc
Go 1.27 cải thiện hiệu suất của client HTTP/1 bằng cách tự động xử lý các phần thân phản hồi chưa được đọc, cho phép tái sử dụng kết nối mà không cần thay đổi mã thủ công.
Được dịch tự động từ bản gốc tiếng Anh.
Nhóm phát triển Go đã ra mắt phiên bản 1.27, mang lại một cải tiến hiệu suất đáng kể cho các client HTTP/1 không đọc hết phần thân của phản hồi. Trong các bài kiểm tra benchmark so với index.golang.org, runtime mới hoàn thành 100 yêu cầu GET tuần tự trong khoảng 150 mili giây, so với 710 mili giây ở Go 1.26. Tốc độ tăng gấp năm lần này giải quyết một điểm yếu tồn tại từ lâu, khi dữ liệu phản hồi không được sử dụng ngăn cản việc tái sử dụng kết nối TCP.
Vấn đề nằm ở đâu
Các nhà phát triển xây dựng dịch vụ backend bằng Go từ lâu đã gặp phải những hình phạt về hiệu suất âm thầm khi sử dụng gói chuẩn net/http với HTTP/1. Vấn đề xảy ra khi client thực hiện một yêu cầu nhưng không tiêu thụ toàn bộ phần thân phản hồi trước khi đóng nó. Ở Go 1.26 và các phiên bản trước đó, HTTP client sẽ phát hiện dữ liệu chưa được đọc và từ bỏ kết nối TCP cơ bản thay vì cố gắng tái sử dụng nó. Điều này buộc phải tạo một kết nối mới cho mọi yêu cầu tiếp theo, dẫn đến chi phí đầy đủ cho các bắt tay (handshake) TCP và TLS mỗi lần.
Cải tiến trong Go 1.27 thay đổi hành vi này. Runtime hiện nay tự động xả (drain) các phần thân phản hồi chưa được đọc khi resp.Body.Close() được gọi, cho phép kết nối vẫn hoạt động và sẵn sàng cho các yêu cầu trong tương lai. Thay đổi này nhắm cụ thể vào lưu lượng HTTP/1, vốn vẫn được sử dụng rộng bất chấp sự phổ biến của HTTP/2. Bằng cách loại bỏ nhu cầu các nhà phát triển phải tự tay xả phần thân, bản cập nhật giảm độ trễ và mức tiêu thụ tài nguyên cho các dịch vụ chỉ kiểm tra mã trạng thái hoặc phân tích một phần phản hồi JSON.
Cách thức hoạt động
Để hiểu rõ bản sửa lỗi, chúng ta cần xem xét cách HTTP/1 xử lý truyền tải dữ liệu. Khác với HTTP/2, vốn ghép kênh (multiplexes) nhiều luồng trên một kết nối duy nhất, HTTP/1 gửi các phản hồi theo thứ tự như một dòng byte liên tục. Client đọc dòng trạng thái và các header trước, sau đó là phần thân. Để kết nối có thể được tái sử dụng, client phải đọc từng byte của phần thân phản hồi hiện tại để biết chính xác nơi phản hồi tiếp theo bắt đầu. Nếu còn bất kỳ byte nào chưa được đọc, client không thể an toàn xác định điểm bắt đầu của tin nhắn tiếp theo, dẫn đến lỗi giao thức nếu kết nối được tái sử dụng.
Trong các phiên bản trước, nếu một nhà phát triển gọi resp.Body.Close() mà không đọc toàn bộ phần thân, gói net/http sẽ đánh dấu kết nối là không thể sử dụng. Sau đó, nó sẽ đóng socket và mở một cái mới cho yêu cầu tiếp theo. Quá trình này bao gồm một bắt tay TCP, mất một thời gian vòng truyền (RTT), và một bắt tay TLS, có thể mất hai RTT đối với TLS 1.2 hoặc một RTT đối với TLS 1.3. Trên các mạng có độ trễ cao, những lần bắt tay này chiếm ưu thế trong tổng thời gian yêu cầu.
Go 1.27 sửa đổi phương thức Close() để tự động đọc và loại bỏ bất kỳ dữ liệu còn lại nào trong bộ đệm phần thân phản hồi trước khi trả kết nối về pool. Điều này đảm bảo rằng luồng dữ liệu được định vị đúng chỗ cho yêu cầu tiếp theo. Runtime xử lý điều này một cách hiệu quả, tránh chặn không cần thiết trong khi vẫn đảm bảo tính chính xác của giao thức. Cơ chế này mô phỏng những gì nhiều nhà phát triển Go giàu kinh nghiệm trước đây đã thực hiện thủ công bằng cách sao chép phần thân sang io.Discard.
Chi tiết quan trọng
- Go 1.27 giảm thời gian cho 100 yêu cầu GET HTTP/1 tuần tự từ ~710ms xuống ~150ms trong các bài kiểm tra benchmark.
- Cải tiến áp dụng cụ thể cho các kết nối HTTP/1; HTTP/2 không bị ảnh hưởng bởi vấn đề này do kiến trúc ghép kênh của nó.
- Các biện pháp khắc phục trước đây đòi hỏi nhà phát triển phải tự tay xả phần thân phản hồi bằng cách sử dụng
io.Copy(io.Discard, resp.Body)trước khi đóng. - Bản sửa lỗi tự động tiêu thụ các byte chưa được đọc khi
resp.Body.Close()được gọi, giữ nguyên khả năng tái sử dụng kết nối. - Các trường hợp thất bại trong tái sử dụng kết nối thường xảy ra trong mã chỉ kiểm tra
resp.StatusCodehoặc phân tích JSON mà không đọc các ký tự xuống dòng cuối cùng. - Thay đổi này tương thích ngược và không yêu cầu sửa đổi mã cho các ứng dụng hiện có để tận hưởng lợi ích.
Tại sao điều này quan trọng
Đối với các kỹ sư phần mềm xây dựng microservices hoặc API client, bản cập nhật này loại bỏ một nguồn gây ra độ trễ ngoài ý muốn phổ biến. Nhiều ứng dụng chỉ quan tâm đến thành công hay thất bại của một cuộc gọi API, được chỉ báo bởi mã trạng thái HTTP, và bỏ qua phần thân phản hồi. Không có bản sửa lỗi này, các ứng dụng như vậy sẽ chịu hiệu suất kém do sự biến động liên tục của kết nối. Chi phí thiết lập các kết nối TCP và TLS mới là đáng kể, đặc biệt trong các hệ thống phân tán nơi các dịch vụ giao tiếp qua các khu vực có độ trễ mạng cao hơn.
Hơn nữa, thay đổi này giảm tải nhận thức cho các nhà phát triển. Trước đây, các nhóm phải tuân thủ nghiêm ngặt các tiêu chuẩn lập trình để đảm bảo tất cả phần thân phản hồi đều được xả, thường đòi hỏi các hàm wrapper tùy chỉnh hoặc quy tắc linter. Những trường hợp bị bỏ sót dẫn đến suy giảm hiệu suất tinh tế, khó chẩn đoán vì mã trông có vẻ đúng và chức năng hoạt động tốt. Với Go 1.27, thư viện chuẩn xử lý trường hợp biên này nội bộ, làm cho việc sử dụng HTTP client an toàn và hiệu quả trở thành hành vi mặc định.
Bạn có thể làm gì
- Nâng cấp toolchain Go của bạn lên phiên bản 1.27 để hưởng lợi từ các cải tiến tự động hóa tái sử dụng kết nối.
- Xem xét lại mã HTTP client hiện có để xác định những nơi phần thân phản hồi bị bỏ qua hoặc chỉ được đọc một phần.
- Loại bỏ logic tự tay xả phần thân như
io.Copy(io.Discard, resp.Body)nếu nó được thêm vào chỉ nhằm mục đích tái sử dụng kết nối. - Kiểm thử độ trễ của dịch vụ của bạn dưới tải để quan sát sự giảm thiểu chi phí thiết lập kết nối.
- Đảm bảo các công cụ giám sát của bạn theo dõi tỷ lệ tái sử dụng kết nối để xác minh cải tiến trong môi trường production.
- Tiếp tục sử dụng HTTP/2 khi có thể để hưởng lợi từ ghép kênh vốn có, nhưng dựa vào bản sửa lỗi này cho các endpoint HTTP/1 cũ.



