记一次go的http.client高并发踩坑记
记一次go的http.client高并发踩坑记
最早在写求索的时候就遇到了同样的问题,但是因为求索并发并不是特别高,所以不是每次都能复显,我也不想一直挂着dlv去等。
最近在写一些扫描、爆破的小工具,遇到相同的问题,折腾了2天终于解决记录一下。
大概会出现这样的错误
net/http.(*persistconn).writeloop(0xc00b3498c0)
其实是3个问题
1. 创建大量client之后导致的协程溢出
这个问题有俩个原因
1.1 重复创建transport
默认使用http.Default.Get/http.NewRequest,都会创建一个新的transport,导致进行了大量的不必要的开销。
其实transport是可以复用的,本身http.client就是一个请求池
var transport http.Transport
func init() {
transport = http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
}
func (h *Http) Execute() *http.Response {
defer func() {
if err := recover(); err != nil {
log.Println(err)
}
}()
var err error
//复用transport
h.HttpClient.Transport = &transport
h.HttpResponse, err = h.HttpClient.Do(h.HttpRequest)
if err != nil {
log.Println("[!] Http Execute Error : ", err)
h.HttpResponse = nil
return nil
}
return h.HttpResponse
}
1.2 HttpResponse.Body未完成完全读取
一般是开了大量的goroutine去执行http.client,但是最后导致client没有自动回收、关闭,导致goroutine的崩溃
goroutine 退出需要满足:
- body 读取完毕
- request 主动 cancel
- request context Done 状态 true
- 当前的 persistConn 关闭
比较常见的场景就是我们只去获取了header,并没有去读取body,然后就return了,据说不把body读完也会导致同样的问题
为了以防万一,我们把request也设置一个context进行手动Done
// 新建一个请求
func (h *Http) New(method, urls string) error {
var err error
//设置一个context
h.Ctx, h.CtxCancel = context.WithCancel(context.Background())
....
h.HttpRequest, err = http.NewRequest(h.HttpRequestType, h.HttpRequestUrl, h.HttpBody)
h.HttpRequest.WithContext(h.Ctx)
return err
}
// 关闭请求与body
func (h *Http) Close() {
defer func() {
if err := recover(); err != nil {
log.Println(err)
}
}()
if h.HttpResponse != nil {
//读取内容并关闭body
h.readAll()
}
if h.CtxCancel != nil {
//主动停止request
h.CtxCancel()
}
}
2. 内存溢出
内存溢出的原因是因为一般我们读body都是这样的
resp:=make([]byte,512)
ioutil.ReadAll(HttpResponse.Body,resp)
这样每次读取一个新的都会进行一次makeslice操作,导致内存使用率增大,最后被系统强制killer
解决方案就是使用sync.Pool,创建一个byte的pool,每次都从pool里面去取空闲的出来使用
var pool sync.Pool
func init(){
//初始化一个pool
pool = sync.Pool{
New: func() interface{} {
return bytes.NewBuffer(make([]byte, 4096))
},
}
}
.....
func (h *Http) readAll() ([]byte, error) {
//获取一个新的,如果不存在则会调用new创建
buffer := pool.Get().(*bytes.Buffer)
buffer.Reset()
defer func() {
if buffer != nil {
//重新放回去
pool.Put(buffer)
buffer = nil
}
}()
if h.HttpResponse == nil {
return nil, fmt.Errorf("HttpResponse is nil")
}
if h.HttpResponse.Body == nil {
return nil, fmt.Errorf("HttpResponse.Body is nil")
}
_, err := io.Copy(buffer, h.HttpResponse.Body)
if err != nil && err != io.EOF {
//log.Printf("readAll io.copy failure error:%v \n", err)
return nil, fmt.Errorf("readAll io.copy failure error:%v", err)
}
defer h.HttpResponse.Body.Close()
return buffer.Bytes(), nil
}
参考资料
https://barbery.me/post/2019-08-02-fix-goroutine-memory-leak/
https://sanyuesha.com/2019/09/10/go-http-request-goroutine-leak/
http://xiaorui.cc/archives/7172
https://riptutorial.com/go/example/16314/sync-pool
https://studygolang.com/articles/16282