Goのnet/httpとKeep-Alive
golangKeep-AliveするとTCPコネクションを使い回し、名前解決やコネクション(3 way handshake)を毎回行わなくてよくなる。
Goのnet/http
ではデフォルトでKeep-Aliveが
有効になっているが、
全体と同一ホストでそれぞれKeep-Aliveするコネクション数が制限される。
これらの値はClientのTransportが持っていて、
これがnullだとDefaultTransportが参照されるので、意識しなければこの値が使われる。
- MaxIdleConns: DefaultTransportでは100になっている。0にすると無制限。
- MaxIdleConnsPerHost: デフォルト値は2。0にするとデフォルト値が使われる。
同一のホストに同時にたくさんリクエストする場合、2だとほとんど意味を持たない。これを増やして比較してみた。
package main
import (
"fmt"
"io/ioutil"
"net/http"
"time"
)
var client = http.Client{
Timeout: time.Millisecond * 100,
}
const TOTAL_REQUEST_NUM = 3000
const TARGET_URL = "*****"
func main() {
http.DefaultTransport.(*http.Transport).MaxIdleConns = 0
http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = 3000
okChan := make(chan int, TOTAL_REQUEST_NUM)
ngChan := make(chan int, TOTAL_REQUEST_NUM)
var okCount = 0
var ngCount = 0
// connect and keep-alive
for i := 0; i < TOTAL_REQUEST_NUM; i++ {
go request(okChan, ngChan)
}
for {
select {
case <-okChan:
okCount++
case <-ngChan:
ngCount++
}
if okCount+ngCount == TOTAL_REQUEST_NUM {
break
}
}
okCount = 0
ngCount = 0
startNs := time.Now().UnixNano()
for i := 0; i < TOTAL_REQUEST_NUM; i++ {
go request(okChan, ngChan)
}
for {
select {
case <-okChan:
okCount++
case <-ngChan:
ngCount++
}
if okCount+ngCount == TOTAL_REQUEST_NUM {
break
}
}
endNs := time.Now().UnixNano()
fmt.Printf("[RESULT] request: %d, ok: %d, ng: %d, time(ms) %d\n",
TOTAL_REQUEST_NUM, okCount, ngCount, (endNs-startNs)/(1000*1000))
}
func request(okch chan int, ngch chan int) {
req, err := http.NewRequest("GET", TARGET_URL, nil)
if err != nil {
panic(err.Error())
}
resp, err := client.Do(req)
if err != nil {
fmt.Println(err.Error())
ngch <- 1
return
}
defer resp.Body.Close()
_, err = ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err.Error())
ngch <- 1
return
}
okch <- 1
}
-
デフォルト設定:
[RESULT] request: 3000, ok: 530, ng: 2470, time(ms) 173
-
MaxIdleConnsPerHostを3000に:
[RESULT] request: 3000, ok: 3000, ng: 0, time(ms) 88
全てのリクエストが時間内に捌けるようになったので効果があったようだ。
ただ、このコードのようにgoroutineを無尽蔵に生成すると、限界を超えたときにタイムアウトが頻発してしまうので注意。
Goroutineの数をworkerで抑制する - sambaiz.net
ちなみに、tcnksm/go-httpstatでnet/http/httptraceすると各処理にかかっている時間が分かる。
req, err := http.NewRequest("GET", TARGET_URL, nil)
if err != nil {
panic(err.Error())
}
result := new(httpstat.Result)
ctx := httpstat.WithHTTPStat(req.Context(), result)
req = req.WithContext(ctx)
resp, err := client.Do(req)
if err != nil {
fmt.Println(err.Error())
ngch <- 1
return
}
defer resp.Body.Close()
_, err = ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println(err.Error())
ngch <- 1
return
}
result.End(time.Now())
fmt.Printf("%+v\n", result)
Start Transfer: 85 ms
Total: 86 ms
DNS lookup: 0 ms
TCP connection: 0 ms
TLS handshake: 0 ms
Server processing: 64 ms
Content transfer: 1 ms
Name Lookup: 0 ms
Connect: 0 ms
Pre Transfer: 0 ms
Start Transfer: 64 ms
Total: 65 ms