Goのnet/httpとKeep-Alive

golang

Keep-AliveするとTCPコネクションを使い回し、名前解決やコネクション(3 way handshake)を毎回行わなくてよくなる。 Goのnet/httpではデフォルトでKeep-Aliveが 有効になっているが、 全体と同一ホストでそれぞれKeep-Aliveするコネクション数が制限される。 これらの値はClientのTransportが持っていて、 これがnullだとDefaultTransportが参照されるので、意識しなければこの値が使われる。

同一のホストに同時にたくさんリクエストする場合、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

参考

Re:golang の http.Client を速くする - Shogo’s Blog