问题表现
重现代码:
package main
import (
"fmt"
"io"
"net/http"
"time"
)
func main() {
client := &http.Client{
Timeout: time.Duration(3) * time.Second,
}
for i := 0; i < 100; i++ {
go func() {
for {
req, _ := http.NewRequest(http.MethodGet, "https://baidu.com", nil)
rsp, err := client.Do(req)
if err != nil {
fmt.Println("request failed", err)
continue
}
rsp.Body.Close()
body, err := io.ReadAll(rsp.Body)
if err != nil {
fmt.Println("read body failed", err)
continue
}
fmt.Println(string(body))
}
}()
}
select {}
}
启动后,随着请求越来越多,很快就出现了"cannot assign requested address"错误,服务器出现大量TIME_WAIT连接。
问题原因
type Client struct {
// Transport specifies the mechanism by which individual
// HTTP requests are made.
// If nil, DefaultTransport is used.
Transport RoundTripper
未配置Transport时,使用默认的DefaultTransport。
var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: defaultTransportDialContext(&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}),
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
这里指定了最大空闲连接未100,未指定单个host的最大空闲连接。
func (t *Transport) maxIdleConnsPerHost() int {
if v := t.MaxIdleConnsPerHost; v != 0 {
return v
}
return DefaultMaxIdleConnsPerHost
}
如果未配置MaxIdleConnsPerHost,则使用默认的DefaultMaxIdleConnsPerHost配置。
// DefaultMaxIdleConnsPerHost is the default value of [Transport]'s
// MaxIdleConnsPerHost.
const DefaultMaxIdleConnsPerHost = 2
这下清楚了:
100个协程,请求同一个地址,只能保留2个空闲连接,超出的请求完就会退出,产生一个TIME_WAIT;
然后再创建一个连接,请求完关闭,又产生一个TIME_WAIT,直至耗尽端口。
解决方案
创建Http.Client时,配置MaxIdleConnsPerHost即可。