Golang: http.Client의 Timeout
개요
2021년에도 관련글을 작성 하였었는데 최근에 이 부분을 한 번 더 읽게되어 정리해둔다.
Timeout은 강건한(robust) 애플리케이션을 만드는데 핵심적인 역할을 한다. 우리가 제어할 수 없는 외부의 시스템, 서버와 소통을 하는데 무한정 기다릴 수가 없으니 일정 시간내에 원하는 응답을 받지 못하면 더 이상 기다리지 않는것이다.
http.Client 에서는 다양한 Timeout을 설정할 수 있는데 http.Client의 Timeout 필드만 보자.
http.Client의 Timeout 필드
type Client struct {
Transport RoundTripper
CheckRedirect func(req *Request, via []*Request) error
Jar CookieJar
Timeout time.Duration
}
Timeout 필드의 코멘트 일부를 번역하면 다음과 같다.
이 코드 주석은 Go 언어의 http.Client 구조체에 있는 Timeout 필드에 대한 설명입니다. Timeout은 이 클라이언트에 의해 만들어진 요청에 대한 시간 제한을 지정합니다. 타임아웃은 연결 시간, 리디렉션, 응답 본문 읽기를 포함합니다. 타이머는 Get, Head, Post, 또는 **Do**가 반환된 후에도 계속 실행되며, Response.Body 읽기를 중단할 수 있습니다.
Timeout이 0인 경우 타임아웃이 없음을 의미합니다.
클라이언트는 Request의 컨텍스트가 끝난 것처럼 기본 Transport에 대한 요청을 취소합니다.
요청을 시작해서 응답의 본문을 읽기까지의 시간이 Timeout 설정시간을 넘어서면 Timeout 에러가 발생하는 것이다.
아래 그림에 잘 설명이 되어있다.
Client.Do 와 http.Client.Timeout
그런데 자세히 보면 Client.Do, 즉, 요청의 응답을 받는데까지의 시간은 Response의 헤더를 다 받는데까지의 시간이고, http.Client의 Timeout은 Response body를 다 읽는데 까지의 시간이다. 이 부분이 미묘해서 예제코드를 만들어보았다.
코드 설명
- 클라이언트 준비: 150ms Timeout을 설정한 클라이언트. 서버에 따라 네트워크에 따라 값을 조절해야 한다. 여기서는 네이버 서버로 요청을 보내보았다.
- 서버로 요청을 보내고 response를 error없이 받았는지 확인한다.
- Client.Get 함수가 에러없이 리턴되었는지 확인. 즉, response headers 까지 잘 받았는지 확인
- 에러가 발생하지 않았다면 받은 헤더를 출력해본다.
- 마지막으로 response body를 읽는다. io.ReadAll 함수로 읽는데 Timeout을 포함한 에러가 발생하는지를 보자.
package main
import (
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
start := time.Now()
// http.Client 준비
timeout := 150
client := &http.Client{
Timeout: time.Duration(timeout) * time.Millisecond,
}
// 서버로 요청 보내기.
fmt.Printf("client request time: %v ms\\n", time.Since(start).Milliseconds())
resp, err := client.Get("<https://naver.com>")
if err != nil {
fmt.Printf("error: request failed: %v", err)
os.Exit(1)
}
defer resp.Body.Close()
fmt.Printf("http.Client.Timeout: %v ms\\n", timeout)
fmt.Printf("client receive time: %v ms\\n", time.Since(start).Milliseconds())
fmt.Printf("-- Response status: %s\\n", resp.Status)
fmt.Printf("-- Headers received: %s\\n", resp.Header)
_, err = io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("error: cannot read response: %v", err)
os.Exit(1)
}
fmt.Println("Response received")
}
테스트 결과
컴퓨터마다, 서버, 네트워크마다 약간씩의 차이가 있겠지만 내 컴퓨터에서 https://naver.com 을 여러번 호출한 결과이다.
Header도 읽지 못한 경우
Client.Get 함수의 리턴에서 에러가 났다. 서버 회신의 header들을 미처 다 받지 못하고 Timeout이 걸린것이다.
client request time: 0 ms
error: request failed: Get "<https://www.naver.com/>": context deadline exceeded (Client.Timeout exceeded while awaiting headers)exit status 1
Header는 읽었으나 body를 읽다가 에러가 난 경우
io.ReadAll 함수에서 에러가 났다. 헤더까지 무사히 받은 것을 알 수 있다.
Timeout은 150ms으로 해두었는데, 헤더까지 무사히 받은 시간이 147ms 이다. 바디를 읽을 시간이 3ms 밖에 안되는데 시간이 부족했던 것이다.
client request time: 0 ms
http.Client.Timeout: 150 ms
client receive time: 147 ms
-- Response status: 200 OK
-- Headers received: map[Cache-Control:[no-cache, no-store, must-revalidate] Content-Type:[text/html; charset=UTF-8] Date:[Thu, 14 Dec 2023 15:38:57 GMT] Pragma:[no-cache] Referrer-Policy:[unsafe-url] Server:[NWS] Set-Cookie:[PM_CK_loc=5a0016f7d08f6547aebcc735dfcd69678832b5d8940b3658eb54c59823d822e7; Expires=Fri, 15 Dec 2023 15:38:57 GMT; Path=/; HttpOnly] Strict-Transport-Security:[max-age=63072000; includeSubdomains] Vary:[Accept-Encoding] X-Frame-Options:[DENY] X-Xss-Protection:[1; mode=block]]
error: cannot read response: context deadline exceeded (Client.Timeout or context cancellation while reading body)exit status 1
무사히 응답을 받은 경우
이번에는 헤더를 받은 시간이 137ms으로 바디를 읽을 시간은 13ms이면 충분했나 보다.
client request time: 0 ms
http.Client.Timeout: 150 ms
client receive time: 137 ms
-- Response status: 200 OK
-- Headers received: map[Cache-Control:[no-cache, no-store, must-revalidate] Content-Type:[text/html; charset=UTF-8] Date:[Thu, 14 Dec 2023 15:46:16 GMT] Pragma:[no-cache] Referrer-Policy:[unsafe-url] Server:[NWS] Set-Cookie:[PM_CK_loc=5a0016f7d08f6547aebcc735dfcd69678832b5d8940b3658eb54c59823d822e7; Expires=Fri, 15 Dec 2023 15:46:16 GMT; Path=/; HttpOnly] Strict-Transport-Security:[max-age=63072000; includeSubdomains] Vary:[Accept-Encoding] X-Frame-Options:[DENY] X-Xss-Protection:[1; mode=block]]
Response received
결론
여기서 http.Client.Timeout에 대한 이야기는 마무리한다. 엄밀해야 하는 경우가 아니라면 이 타임아웃만 설정해도 충분하다. 모니터링을 통해 시스템에 맞는 적절한 Timeout을 걸어두면 될 것이다.