记一次go中httpClient优化

因为业务需要发送大量的http的请求,会有很多的302跳转,使用的是go,对go中的httpClient做了一些重新配置,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var client *http.Client

func init() {
client = &http.Client{
Transport: &http.Transport{
ReadBufferSize: 2048, //tcp 读buffer
WriteBufferSize: 2048, //tcp 写buff
MaxResponseHeaderBytes: 4096, //响应最大header头部,默认是10M,减少头部大小可以堆内存
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
ForceAttemptHTTP2: true, //启用http2 性能提升2倍,tcp 连接数减少2/3
MaxIdleConns: 1000, // 最大空闲连接数
IdleConnTimeout: 10 * time.Second, // 空闲连接超时时间

TLSHandshakeTimeout: 10 * time.Second, // TLS握手超时
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
SessionTicketsDisabled: false,
ClientSessionCache: &sessionCache{cache: sync.Map{}}}, //InsecureSkipVerify用来控制客户端是否证书和服务器主机名。如果设置为true, 则不会校验证书以及证书中的主机名和服务器主机名是否一致。
MaxIdleConnsPerHost: 1000, // 使用长连接,需要调高该值 每个主机的最大空闲连接数
MaxConnsPerHost: 5000,
DisableKeepAlives: false,
},
Timeout: 10 * time.Second, // 请求超时时间
}
}

一般情况下,这样的配置应该会有不错的效果,但是部署到线上之后出现了,cpu稍微有点高(与优化后的对比),峰值30M的带宽全部吃满,导致很多请求其实发不出去了。

着重观察10点13之前的cpu与带宽情况,cpu不太高,但是带宽使用很大


重点是发送http请求使用时长,有一些已经超过20秒了,即便每次是500个请求,开启了500个协程,使用了连接池,依然花费巨大的时间。

前前后后几个月一直在分析具体原因,一次正常工作的下午,说再细细看一下这块的情况,仔细分析了当前场景与http发送的关系,结合机器的流量观察,发现有很多的与第三方建立的链接没有释放, 这不符合我们的逻辑,理论建立连接发送数据后就会断开,但是结果不是这样,优化了一下,禁止重定向,到达我们预定义的地址后就不再请求下次链接,修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
client = &http.Client{
Transport: &http.Transport{
ReadBufferSize: 2048, //tcp 读buffer
WriteBufferSize: 2048, //tcp 写buff
MaxResponseHeaderBytes: 4096, //响应最大header头部,默认是10M,减少头部大小可以堆内存
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
ForceAttemptHTTP2: true, //启用http2 性能提升2倍,tcp 连接数减少2/3
MaxIdleConns: 1000, // 最大空闲连接数
IdleConnTimeout: 10 * time.Second, // 空闲连接超时时间

TLSHandshakeTimeout: 10 * time.Second, // TLS握手超时
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
SessionTicketsDisabled: false,
ClientSessionCache: &sessionCache{cache: sync.Map{}}}, //InsecureSkipVerify用来控制客户端是否证书和服务器主机名。如果设置为true, 则不会校验证书以及证书中的主机名和服务器主机名是否一致。
MaxIdleConnsPerHost: 1000, // 使用长连接,需要调高该值 每个主机的最大空闲连接数
MaxConnsPerHost: 5000,
DisableKeepAlives: false,
},
Timeout: 10 * time.Second, // 请求超时时间
CheckRedirect: func(req *http.Request, via []*http.Request) error {
//这里面添加业务自定的逻辑比如
//超过最大跳转次数 我们就返回错误 或者返回 return http.ErrUseLastResponse
if check.MaxRedirect(len(via)) {
return errors.New("max redirect") // return http.ErrUseLastResponse
}
return http.ErrUseLastResponse
},
}
  1. 让禁止重定向,让CheckRedirect返回一个http.ErrUseLastResponse,http.ErrUseLastResponse的结果会返回上一次跳转成功的结果,根据自己的业务去判断需要返回什么
  2. 与此同时,修改入参的请求为类似mq方式,接收请求,做了一个1500大小的chan作为延迟队列,启动协程去消费chan中的数据,再去发送
    简单的代码逻辑如下:

接收请求

1
2
3
4
5
6
7
for _, item := range reqList {
if err := RequestAsyncProducer(item); err != nil {
msg = err.Error()
break
}
}
return msg

请求异步发送到一个chan

1
2
3
4
5
6
7
8
func RequestAsyncProducer(data Data) error {
select {
case topicRequests <- &data:
return nil
default:
return errors.New("too busy")
}
}

消费chan中的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func consumer(parallel int) {
ch := make(chan struct{}, parallel)
for i := 0; i < parallel; i++ {
ch <- struct{}{}
}
for {
<-ch
go func() {
defer func() {
if err := recover(); err != nil {
ch <- struct{}{}
fmt.Println("error", err)
}
}()
var request *Data
ticker := time.Tick(time.Duration(3) * time.Second)
for {
select {
case request = <-topicRequests:
toSend(*request)
case <-ticker:
// wait to close
}
}
}()
}
}

经过优化后的效果入最开的图,响应时间如下,异步消费了当然会很快了,带宽,cpu如最开始的10点13之后的,下降很大,现在带宽已不是问题了。


记一次go中httpClient优化
https://vaughnn.github.io/posts/e9488b72/
作者
vaughnn
发布于
2024年3月12日
更新于
2024年3月13日