Go 1.27, HTTP/1 응답 본문을 안 읽어도 연결 재사용 가능해짐
- Go 1.26까지는 응답 본문을 끝까지 읽지 않고 Close하면 기존 HTTP/1 연결이 버려지고 다음 요청마다 새 연결을 맺음
- Go 1.27에서는 본문을 안 읽은 채 Close해도 연결을 재사용하게 바뀌어 index.golang.org 대상 100회 요청 시간이 약 710밀리초에서 약 150밀리초로 줄어듦
- HTTP/1은 응답이 순차로 이어지는 구조라서 앞 응답 본문을 끝까지 읽어야 다음 응답의 시작점을 찾을 수 있음
- 상태 코드만 필요한 클라이언트는 본문을 버리는 과정에서 조용한 성능 저하를 겪는데, 성공 여부 같은 짧은 JSON 응답도 이 문제에 해당됨
- HTTP/2는 스트림 다중화 덕분에 한 응답 본문이 남아 있어도 같은 연결의 다른 요청을 막지 않음
Hacker News 의견들
xhttp.DrainResponseBody 이제 드디어 지울 수 있겠다. 그거 생각보다 잘 안 되던 헬퍼였는데, r60 때부터 회사랑 프로젝트 옮길 때마다 복붙해서 쓰던 거임.
본문 다운로드를 건너뛰던 동작이 바뀌어서 아쉽긴 한데, 다운로드 시간을 50ms로 묶어주니까 네트워크가 느려도 취소된 본문에 막히진 않더라. 트레이드오프로는 괜찮음.
리버스 프록시 뒤에서도 이게 되는지 궁금함. nginx가 본문 안 읽고 끊은 서버에서 IO 에러 뜨고 5xx로 올라와서 고생했음. Chrome이랑 curl은 괜찮은데 Firefox는 XHR 상태 코드가 0으로 나오더라.
net/http 문서에 이 내용이 없는 것 같음. Close가 직관이랑 다르게 동작하는데 어디에도 안 적혀 있음. 소켓을 진짜로 바로 끊고 싶으면 Connection: close 헤더를 가짜로 넣어야 하냐?
Close는 HTTP 요청 기준으로 동작하는 거라 HTTP/2에선 소켓이랑 아무 상관이 없음. 소켓을 강제로 닫으려면 아래 계층 객체를 직접 꺼내야 해서 좀 지저분함. CloseNow() 같은 메서드가 있으면 말이 됨.
HTTP 라이브러리가 어려운 이유는 DNS, TLS, L4까지 알아야 해서임. 앱 개발자용으로 추상화하면 HTTP 버전도 모르는 상태에서 드레인 문제가 새어 나옴. Rust의 hyper랑 reqwest가 그 분리를 시도한 예시임.
json Decoder로 읽어도 응답 끝에 개행이 남으면 연결 재사용이 안 됨. 그래서 나는 디코딩 후에 Decoder.More 호출해서 공백을 비우는 습관 들였음.
드레인 최대치는 있는 것 같은데, chunked 스트리밍 응답에는 그 최적화가 안 먹힌다고 봄. json.NewEncoder로 흘려 보내는 서버라면 효과 없음.
Decoder가 원래 그런 엣지 케이스 때문에 권장되지 않는 거임. encoding/json/v2의 UnmarshalRead가 그 용도로 나온 거라고 알고 있음.
HTTP/2 쓰면 이 문제 자체가 없다고 글에 나와 있음. 결국 이건 HTTP/1 최적화라는 뜻이고, 백엔드 서버가 H2를 안 쓰면 어쩔 수 없이 HTTP/1을 써야 함.
CloudFront도 오리진 쪽은 HTTP/1만 지원함. 외부 서비스 말고 내부 통신이나 CDN 쪽에선 아직 H1이 많이 쓰임.
drainAndCloseBody가 세 번 바뀐 끝에 이 방식이 나온 게 웃김. 구현을 단순하게 가져가려다 보니 이런 엣지 케이스를 뒤늦게 잡는 셈임.