前戏:
go net/http包,必须要手动关闭嘛?非也。线上程序为啥协程数量直线上升,因为使用的姿势不对,请换个姿势。
干货:
- 手动关闭,释放资源 defer res.Body.Close() (这是一个好习惯,要保持)
- 不想手动关闭,请读取完Body: io.ReadAll(res.Body)
以上两点,请任意选择一项即可。但是,如果一个都不喜欢,不想要,不想实现。那对不起,让你见识一下内存的飙升,协程数量的直线递增。
客观请留步。。。以下为长篇踩雷故事,可忽略(摸鱼党忽略这一句,请继续往下看)
背景:惊心动魄,血压飙升,上图
很清晰,一眼就能看出来。这量起来了,要赚钱了。呵呵~~
赶紧查问题吧,TCP连接这么高,没有释放,必然是没有关闭啊,pprof分析一下吧
协程泄漏了。。。。。
问题:TCP连接过高+协程泄漏
最小复现代码:
func IsUploadSuccess(url string) bool {
if len(url) == 0 {
return false
}
res, err := http.Get(url)
if err != nil {
return false
}
if res.StatusCode != 0 && res.StatusCode != http.StatusOK {
return false
}
if res.ContentLength <= 512 {
return false
}
return true
}
判断录音文件是否上传成功。ChatGPT 亲口告诉我:http.Get 不显示执行 defer res.Body.Close() 也行。我信了,程序也能跑起来,数据验证也正确,没问题,AI就是强大。
但是,线上异常了OOM。。。。。
严查到底,深究原因:
一、/net/http/transport.go:1750
这两个小可爱,长的真Loop
二、继续查0
/net/http/transport.go:2091
这不就是死循环嘛,什么时候跳出呢?
三、继续查1
pprof分析之后,发现协程泄漏就是 readLoop(),什么时候才跳出死循环,就是上边的三处。
找到这里,就真香大白了。和上边的干货总结一样:
- 手动关闭 defer res.Body.Close()
- 三处:bodyEOF。读取完毕之后,也跳出循环了。(沾花惹草,不负责,这还能行,分分钟让你下不了台)
/net/http/transport.go:2183
回归业务,如何解决
解决:
- 主动关闭
- 读取完body (要负责,取了就要用,不要浪费)
func IsUploadSuccess(url string) bool {
if len(url) == 0 {
return false
}
res, err := http.Get(url)
if err != nil {
return false
}
/*
// 方案一:手动关闭
defer func() {
if err := res.Body.Close(); err != nil {
fmt.Printf("close err:%+v \r\n", err.Error())
}
}()
*/
// 方案二:读取body
//_, _ = io.ReadAll(res.Body)
if res.StatusCode != 0 && res.StatusCode != http.StatusOK {
return false
}
if res.ContentLength <= 512 {
return false
}
return true
}
其他优化:
取了还不用,不如不取。
http.Get(url) ==> http.Head(url)
复现代码:
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
"runtime"
"sync"
"time"
)
func main() {
fmt.Printf("start\r\n")
GetMem()
// 模拟请求10次
for i := 0; i < 10; i++ {
DoLogic(int32(i))
//time.Sleep(time.Minute)
}
GetMem()
fmt.Printf("end\r\n")
}
func DoLogic(s int32) {
url := "https://www.baidu.com/"
var i int32
var wg sync.WaitGroup
for {
if i > 1000 {
break
}
wg.Add(1)
go func() {
defer wg.Done()
IsUploadSuccess(url)
}()
time.Sleep(time.Millisecond)
i++
}
nums := runtime.NumGoroutine()
fmt.Printf("进程:%d,NumGoroutine:%d\r\n", s, nums)
GetMem()
wg.Wait()
return
}
func GetMem() {
var mem runtime.MemStats
runtime.ReadMemStats(&mem)
allocatedMib := bytesToMib(mem.Alloc)
totalAllocatedMib := bytesToMib(mem.TotalAlloc)
heapInUseMib := bytesToMib(mem.HeapAlloc)
heapIdleMib := bytesToMib(mem.HeapIdle)
heapReleasedMib := bytesToMib(mem.HeapReleased)
fmt.Printf("Allocated memory: %.2f MiB\r\n", allocatedMib)
fmt.Printf("Total allocated memory: %.2f MiB\r\n", totalAllocatedMib)
fmt.Printf("Heap memory in use: %.2f MiB\r\n", heapInUseMib)
fmt.Printf("Heap memory idle: %.2f MiB\r\n", heapIdleMib)
fmt.Printf("Heap memory released to OS: %.2f MiB\r\n", heapReleasedMib)
}
func bytesToMib(bytes uint64) float64 {
return float64(bytes) / (1024 * 1024)
}
func IsUploadSuccess(url string) bool {
if len(url) == 0 {
return false
}
res, err := http.Get(url)
if err != nil {
return false
}
/*
// 方案一:手动关闭
defer func() {
if err := res.Body.Close(); err != nil {
fmt.Printf("close err:%+v \r\n", err.Error())
}
}()
*/
// 方案二:读取body
//_, _ = io.ReadAll(res.Body)
if res.StatusCode != 0 && res.StatusCode != http.StatusOK {
return false
}
if res.ContentLength <= 512 {
return false
}
return true
}
【end】
我为人人,人人为我,美美与共,天下大同。