Golang: HTTP 클라이언트의 연결 관리(3)
개요
요청이 빈번하게 발생한다면 연결을 끊지않고 유지해두는 것이 낫다. 하지만 요청이 없는데도 연결을 유지하는 것은 또 다른 비용이다. 이번에는 연결을 언제까지 유지하여야 하는지에 대한 설정을 알아보자.
IdleConnTimeout 이다.
MaxIdleConns: 유지 가능한 최대 유휴 커넥션 수, default: 100
MaxIdleConnsPerHost: 호스트마다 유지 가능한 최대 유휴 커넥션 수, default: 2
IdleConnTimeout: 유휴 커넥션 타임아웃, default: 90초
MaxConnsPerHost: 호스트마다 사용 가능한 최대 활성/유휴 커넥션 수, default: 0 (무제한)
커넥션 풀에 무한정 연결을 유지할 수 없다.
클라이언트 코드
서버는 이전 포스팅에서와 같이 바로 회신을 하는 코드이기에 생략한다.
클라이언트 전송 계층 설정
기본값은 90초인 IdleConnTimeout 값을 10초로 변경하였다.
tr := &http.Transport{
MaxConnsPerHost: 5,
MaxIdleConnsPerHost: 3,
**IdleConnTimeout: 10 * time.Second,**
}
client := http.Client{
Transport: tr,
}
1차 전화 통화(비유) - 커넥션 풀에 3개의 연결 넣어두기
동시다발로 5번의 요청이 발생하고, 5초간 기다린다.
커넥션 풀에 3개의 연결이 유지될 것이다.
wg.Add(5)
for i := 0; i < 5; i++ {
go func(i int) {
defer wg.Done()
resp, err := client.Get("<http://localhost:8080>")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
fmt.Printf("g-%d: %s\\n", i, string(body))
}(i)
}
wg.Wait()
time.Sleep(5 * time.Second)
2차 전화 통화(비유) - 커넥션 풀의 연결을 사용한 다음 닫히게 하기
5개의 고루틴이 각각 1초 간격으로 두 번의 요청을 한다. 결과적으로 5개의 요청이 동시다발로 이루어지고 1초뒤에 한 번더 5개의 요청이 동시다발로 이루어진다. 그리고 나서 15초간 대기한다.
5번 + 5번의 요청은 커넥션 풀의 연결로 이루어짐을 확인하고, 15초가 지나면 타임아웃으로 커넥션 풀의 연결이 닫히게 하려는 것이다.
wg.Add(5)
for i := 0; i < 5; i++ {
go func(i int) {
defer wg.Done()
for i := 0; i < 2; i++ {
resp, err := client.Get("<http://localhost:8080>")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
fmt.Printf("g-%d: %s\\n", i+10, string(body))
time.Sleep(time.Second)
}
}(i)
}
time.Sleep(15 * time.Second)
3차 전화 통화(비유)
이제 커넥션 풀에 유지되는 연결이 없는 상태이다.
여기서 5개의 고루틴이 각각 1초 간격으로 100번의 요청이 이루어지게 한다. 결과적으로 5개의 동시다발 요청이 1초 간격으로 100번이나 이루어지게 된다.
첫 5번의 요청은 5개의 연결을 생성하고, 이후는 커넥션 풀에 3개의 연결이 유지되면서 이를 계속 사용할 것이 예상된다.
wg.Add(5)
for i := 0; i < 5; i++ {
go func(i int) {
defer wg.Done()
for i := 0; i < 100; i++ {
resp, err := client.Get("<http://localhost:8080>")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
fmt.Printf("g-%d: %s\\n", i+20, string(body))
time.Sleep(time.Second)
}
}(i)
}
wg.Wait()
서버 로그 분석
1단계에서는 5개의 연결이 생성되고
2단계에서는 커넥션 풀의 3개의 연결을 계속 사용하는 것을 보여주며
3단계에서는 2단계의 연결이 닫힌 다음 다시 5개의 연결이 발생, 커넥션 풀에 3개의 연결이 유지되며 이후 계속 사용된다.
$go run server.go
// 1단계 - 5개의 연결(= 5개의 다른 포트 사용. 52484~52488)
receive a request from: [::1]:52484 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52488 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52486 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52485 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52487 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
// 2단계 - 3개의 연결이 커넥션 풀에 들어갔다가 재사용된다. 52484, 52487, 52488
receive a request from: [::1]:52487 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52488 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52484 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52484 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52487 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52487 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52488 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52484 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52487 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52484 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
// 3단계 - 커넥션 풀의 연결이 모두 닫히고 새로운 5개의 연결이 생긴다. 52542~52546
receive a request from: [::1]:52542 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52544 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52545 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52543 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52546 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
// 다시금 3개가 커넥션 풀로 들어와 재사용된다. 52542, 52544, 52545
receive a request from: [::1]:52542 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52544 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52545 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52542 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
receive a request from: [::1]:52544 map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
... ...
정리
전송 계층의 연결은 비용이 든다. 한 번 연결한 다음에 이를 계속 사용하면 연결의 비용, 시간을 아낄 수 있다. 한 편 연결을 사용하지도 않으면서 계속 유지하는 것 역시 낭비이다. 사용하는 도메인의 특성과 상황을 이해하고 그에 맞게 설정을 하여야 하는 이유이다.