Lobsters

Better HTTP/1 connection reuse

HTTP/1 연결 재사용 개선

Go 1.27의 net/http.Transport는 닫을 때 읽지 않은 HTTP/1 응답 본문을 최대 256KB, 50ms 동안 비동기로 읽어 연결 재사용을 돕습니다. 본문을 읽지 않고 상태 코드만 확인하는 예제는 Go 1.26보다 약 5배 빨라졌습니다.

에디터 노트

이 변경은 본문을 읽지 않는 호출에서 연결 설정 비용을 줄이면서, 본문을 무제한으로 내려받지 않도록 한 설계입니다. Go 블로그의 예제에서는 100회 요청 시간이 710ms에서 150ms로 줄었고, 새 Transport는 최대 256KB와 50ms라는 경계를 둡니다. 그래서 작은 잔여 본문은 연결 재사용으로 이어질 수 있지만 큰 본문이나 느린 응답까지 기다리지는 않습니다. HTTP/2에는 같은 재사용 제약이 없다는 설명도 적용 범위를 분명히 합니다. 댓글이 제공되지 않아 별도의 사용자 경험이나 반론은 확인되지 않습니다.

AI 요약

Go 1.26에서 HTTP/1로 index.golang.org에 100번 요청하는 예제는 약 710ms가 걸립니다. 같은 프로그램이 Go 1.27에서는 약 150ms로 줄어듭니다. 예제는 응답 상태 코드만 확인하고 본문을 읽지 않은 채 닫습니다. 그동안 net/http는 읽지 않은 본문이 남은 연결을 재사용하지 않고 새 연결을 맺었습니다.

새 연결에는 왕복 시간이 듭니다

HTTP 요청은 TCP와 보통 TLS 연결 위에서 오갑니다. 새 연결을 만들면 TCP 핸드셰이크에 1 RTT가 필요합니다. TLS 1.2는 키 교환과 서버 인증에 2 RTT가 더 걸려, HTTP 요청을 보내기 전까지 총 3 RTT가 듭니다. 네트워크 지연이 큰 환경에서는 연결 설정에 드는 시간만으로도 실제 요청 처리 시간보다 길어질 수 있습니다. TLS 1.3을 지원하는 index.golang.org에서는 핸드셰이크가 1 RTT 안에 끝날 수 있지만, 새 연결을 반복해서 만드는 비용은 벤치마크에서 여전히 드러납니다.

HTTP/1에서 본문을 남기면 연결을 재사용하기 어렵습니다

HTTP/1에서는 여러 응답이 같은 연결의 바이트 흐름에 순서대로 실립니다. net/http는 응답 상태 줄과 헤더를 읽어 Response를 만든 뒤 호출자에게 반환하고, 본문은 Response.Body를 읽을 때 소비합니다. 다음 응답의 시작을 찾으려면 현재 본문을 끝까지 읽어야 합니다. 따라서 본문이 남은 채 Close를 호출하면 Go 1.26까지는 기존 연결을 버리고 다음 요청에서 새 연결을 맺었습니다.

상태 코드만 필요한 코드도 서버가 JSON 메시지 같은 본문을 보낸다면 이 문제를 겪습니다. 본문을 처리하는 코드에서도 끝까지 읽지 않으면 연결을 놓칠 수 있습니다. 예를 들어 encoding/json.Decoder는 JSON 객체의 닫는 중괄호를 읽으면 디코딩을 마칩니다. 응답 끝에 개행 문자가 붙었다면 그 한 바이트가 남아 연결 재사용을 막을 수 있습니다.

직접 비우는 방식과 그 한계

기존 우회책은 본문을 io.Discard로 복사해 끝까지 비운 뒤 닫는 것입니다. 작은 본문에는 통하지만 큰 본문을 모두 내려받으면 새 연결을 만드는 편보다 느릴 수 있습니다. 그래서 글은 읽는 양을 256KB로 제한하고, 본문이 천천히 도착하는 경우를 고려해 대기 시간도 50ms로 제한하는 방식을 설명합니다. 호출을 지연하지 않으려면 비우기 작업을 백그라운드에서 실행해야 합니다. 이처럼 직접 처리하면 읽을 양과 시간, 고루틴 종료까지 관리해야 합니다.

Go 1.27의 자동 처리

Go 1.27부터 net/http.Transport는 닫을 때 남은 HTTP/1 본문을 비동기로 읽습니다. 읽는 양은 최대 256KB, 대기 시간은 최대 50ms입니다. 본문을 그 안에 다 읽으면 기존 연결을 재사용하고, 제한 시간이나 용량 안에 끝나지 않으면 연결을 포기합니다. 응답에 Connection: close가 있으면 본문을 읽어도 연결을 재사용할 수 없으므로 비우기를 건너뜁니다. 이 동작은 이후 Go 버전에서 달라질 수 있습니다.

HTTP/2는 여러 스트림을 한 연결에서 다중화하므로 한 응답의 미완료 본문이 다른 요청의 연결 공유를 막지 않습니다. 따라서 글은 HTTP/2 응답까지 재사용을 위해 억지로 비울 필요는 없다고 설명합니다. Go 1.27에서는 더 이상 본문을 직접 비우는 우회 코드를 작성하지 않고, 필요가 끝난 응답 본문을 닫는 방식을 권장합니다.

원문: Go Blog / 번역·요약: Trawling