大量にhttp.Getするという書き捨てコードを書いていたのですが、途中までは順調に進むのですがあるところから急に通信を行わなくなる、という事態にハマりました。
一体何が?
なんじゃこりゃ、というわけでとりあえずコネクションの状況を確認してみたのですが、途中で止まるのも納得なTIME_WAITだらけの状況でした。
そう、TIME_WAITにポートが使い潰されて止まっていたのです。
その時のコードがこちら。
package main
import (
"fmt"
"net/http"
"sync"
)
func main() {
var wg = sync.WaitGroup{}
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
url := "http://www.google.co.jp"
resp, err := http.Get(url)
if err != nil {
fmt.Println("Error: " + err.Error())
return
}
defer resp.Body.Close()
fmt.Println("Success")
}()
}
wg.Wait()
}
これなんですが、実行するとTIME_WAITが大量に発生します。
Macで確認するときは netstat
ではなく、 sysctl net.inet.tcp.tw_pcbcount
で確認しましょう。
( Macだと netstat
ではTIME_WAITが表示されないのです・・・
原因
何が原因かというと、コネクションプールがほとんど利用できてないんですよね。これ。
MaxIdleConnsPerHost
のデフォルトが2なので、2コネクションしか再利用できてない状態で新規のコネクションをポンポン開けちゃう状態でした。
なので、こうしてみました。
package main
import (
"fmt"
"sync"
"net/http"
"golang.org/x/sync/semaphore"
"context"
)
var client *http.Client
const (
Limit = 100
Weight = 1
)
func main() {
var wg = sync.WaitGroup{}
s := semaphore.NewWeighted(Limit)
defaultRoundTripper := http.DefaultTransport
defaultTransportPointer, ok := defaultRoundTripper.(*http.Transport)
if !ok {
panic(fmt.Sprintf("defaultRoundTripper not an *http.Transport"))
}
defaultTransport := *defaultTransportPointer
defaultTransport.MaxIdleConns = 0
defaultTransport.MaxIdleConnsPerHost = 100
client = &http.Client{Transport: &defaultTransport}
for i := 0; i < 5000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
s.Acquire(context.Background(), Weight)
defer s.Release(Weight)
url := "http://www.google.co.jp"
resp, err := client.Get(url)
if err != nil {
fmt.Println("Error: " + err.Error())
return
}
defer resp.Body.Close()
fmt.Println("Success")
}()
}
wg.Wait()
}
ホスト毎に100件分のコネクションを再利用する感じのコードです。
間髪入れずにブン回すので、semaphore処理も入れてます。
これでTIME_WAITが発生しないと思うでしょ?ダメなんだなこれが。
$ sysctl net.inet.tcp.tw_pcbcount
net.inet.tcp.tw_pcbcount: 0
$ sysctl net.inet.tcp.tw_pcbcount
net.inet.tcp.tw_pcbcount: 27
$ sysctl net.inet.tcp.tw_pcbcount
net.inet.tcp.tw_pcbcount: 77
$ sysctl net.inet.tcp.tw_pcbcount
net.inet.tcp.tw_pcbcount: 146
$ sysctl net.inet.tcp.tw_pcbcount
net.inet.tcp.tw_pcbcount: 201
$ sysctl net.inet.tcp.tw_pcbcount
net.inet.tcp.tw_pcbcount: 288
$ sysctl net.inet.tcp.tw_pcbcount
net.inet.tcp.tw_pcbcount: 334
...
こんな感じで無限にTIME_WAITが増えていって、全然再利用されていません。
めちゃくちゃ悩んだんですが、どうやら resp
を読み込まないとコネクションが再利用されないようです。
package main
import (
"fmt"
"sync"
"net/http"
"golang.org/x/sync/semaphore"
"context"
"io/ioutil"
)
var client *http.Client
const (
Limit = 100
Weight = 1
)
func main() {
var wg = sync.WaitGroup{}
s := semaphore.NewWeighted(Limit)
defaultRoundTripper := http.DefaultTransport
defaultTransportPointer, ok := defaultRoundTripper.(*http.Transport)
if !ok {
panic(fmt.Sprintf("defaultRoundTripper not an *http.Transport"))
}
defaultTransport := *defaultTransportPointer
defaultTransport.MaxIdleConns = 0
defaultTransport.MaxIdleConnsPerHost = 100
client = &http.Client{Transport: &defaultTransport}
for i := 0; i < 5000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
s.Acquire(context.Background(), Weight)
defer s.Release(Weight)
url := "http://www.google.co.jp"
resp, err := client.Get(url)
if err != nil {
fmt.Println("Error: " + err.Error())
return
}
defer resp.Body.Close()
_, err = ioutil.ReadAll(resp.Body)
fmt.Println("Success")
}()
}
wg.Wait()
}
これでやってみると・・・
$ sysctl net.inet.tcp.tw_pcbcount
net.inet.tcp.tw_pcbcount: 0
$ sysctl net.inet.tcp.tw_pcbcount
net.inet.tcp.tw_pcbcount: 0
$ sysctl net.inet.tcp.tw_pcbcount
net.inet.tcp.tw_pcbcount: 0
$ sysctl net.inet.tcp.tw_pcbcount
net.inet.tcp.tw_pcbcount: 0
$ sysctl net.inet.tcp.tw_pcbcount
net.inet.tcp.tw_pcbcount: 0
$ sysctl net.inet.tcp.tw_pcbcount
net.inet.tcp.tw_pcbcount: 0
$ sysctl net.inet.tcp.tw_pcbcount
net.inet.tcp.tw_pcbcount: 0
...
やったーーーーー!!TIME_WAITが0にできたよーーーーーー!!!
という感じで、もし同じ現象で悩んでる方がいたら試してみてください。