2026/4/11 1:44:24
网站建设
项目流程
合肥seo网站排名优化公司,怎么做网站推广图片,网站发的文章如何优化,北京做网站的外包公司前言
写Go两年多#xff0c;pprof一直是知道但没深入用过的状态。直到三个月前#xff0c;一个服务内存从2G慢慢涨到8G#xff0c;然后OOM被杀#xff0c;周而复始。常规手段都试了#xff0c;就是找不到泄漏点。
被逼无奈#xff0c;花了两天时间把pprof彻底…前言写Go两年多pprof一直是知道但没深入用过的状态。直到三个月前一个服务内存从2G慢慢涨到8G然后OOM被杀周而复始。常规手段都试了就是找不到泄漏点。被逼无奈花了两天时间把pprof彻底搞明白。那次排查下来我才发现这工具的威力——不只是找内存泄漏CPU热点、阻塞分析、协程泄漏全都能搞定。这篇文章把我踩的坑和总结的经验分享出来希望能帮到同样在用Go的朋友。pprof是什么pprof是Go自带的性能分析工具可以采集运行中程序的各种profile数据CPU ProfileCPU时间花在哪些函数上Heap Profile内存分配情况谁在消耗内存Goroutine Profile协程状态有没有泄漏Block Profile阻塞分析哪里在等锁Mutex Profile锁竞争分析两种使用方式runtime/pprof程序内部调用适合工具类程序net/http/pprofHTTP接口暴露适合长期运行的服务生产环境基本都用HTTP方式随时可以拉取profile数据。快速接入往服务里加几行代码就行packagemainimport(net/http_net/http/pprof// 匿名导入自动注册路由)funcmain(){// 单独起一个端口给pprof别和业务混在一起gofunc(){http.ListenAndServe(:6060,nil)}()// 业务代码...}如果用了gin或者echo这类框架需要手动注册// Gin框架importgithub.com/gin-contrib/pproffuncmain(){r:gin.Default()pprof.Register(r)// 注册pprof路由r.Run(:8080)}启动后访问http://localhost:6060/debug/pprof/就能看到profile列表。实战一CPU热点分析前段时间接手一个服务QPS上不去CPU先到100%了。用pprof定位# 采集30秒CPU profilego tool pprof http://localhost:6060/debug/pprof/profile?seconds30进入交互式界面后(pprof) top 20 Showing nodes accounting for 28.5s, 89.2% of 31.95s total flat flat% sum% cum cum% 12.30s 38.50% 38.50% 12.30s 38.50% encoding/json.(*decodeState).scanWhile 5.20s 16.27% 54.77% 5.20s 16.27% runtime.memmove 3.10s 9.70% 64.47% 15.40s 48.20% encoding/json.(*decodeState).object 2.80s 8.76% 73.23% 2.80s 8.76% runtime.mallocgc38.5%的时间在json.scanWhile这个函数是标准库JSON解析的核心函数。继续看调用链(pprof) list scanWhile发现是一个接口每次请求都在解析一个2MB的配置文件。这配置明明可以启动时加载一次的。改完之后CPU直接降了40%QPS翻倍。火焰图更直观命令行看不直观生成火焰图# 采集profile并用web打开go tool pprof -http:8080 http://localhost:6060/debug/pprof/profile?seconds30浏览器自动打开点VIEW - Flame Graph宽度就是时间占比一眼就能看出哪个函数是大头。实战二内存泄漏排查这是我被坑最惨的一次。服务内存持续增长几个小时就OOM。先看当前内存分配情况go tool pprof http://localhost:6060/debug/pprof/heap(pprof) top Showing nodes accounting for 2.1GB, 95.2% of 2.2GB total flat flat% sum% cum cum% 1.50GB 68.18% 68.18% 1.50GB 68.18% bytes.makeSlice 0.35GB 15.91% 84.09% 0.35GB 15.91% github.com/xxx/cache.(*LRU).Add 0.25GB 11.36% 95.45% 0.25GB 11.36% bufio.NewReaderSize1.5G在bytes.makeSlice但这只是分配的位置不是根因。用-alloc_space看累计分配go tool pprof -alloc_space http://localhost:6060/debug/pprof/heap(pprof) top flat flat% sum% cum cum% 150GB 45.2% 45.2% 150GB 45.2% bytes.makeSlice 89GB 26.8% 72.0% 89GB 26.8% github.com/xxx/cache.(*LRU).Add150GB累计分配程序才跑了几小时。看调用链(pprof) tree bytes.makeSlice顺着调用链往上找发现是一个HTTP客户端没有关闭Response Body// 问题代码resp,err:http.Get(url)iferr!nil{returnerr}// 没有 resp.Body.Close()连接不会被复用buffer一直累积data,_:ioutil.ReadAll(resp.Body)经典错误。加上defer close之后内存稳定在500MB。对比两次heap快照更精准的方法是对比两个时间点的heap# 第一次采集curlhttp://localhost:6060/debug/pprof/heapheap1.out# 等一段时间第二次采集curlhttp://localhost:6060/debug/pprof/heapheap2.out# 对比差异go tool pprof -base heap1.out heap2.out这样能看到这段时间内净增加的内存分配排除启动时的正常分配干扰。实战三协程泄漏有一次服务响应越来越慢CPU内存都正常但协程数从几百涨到了几万。go tool pprof http://localhost:6060/debug/pprof/goroutine(pprof) top Showing nodes accounting for 32156, 99.8% of 32210 total flat flat% sum% cum cum% 32100 99.66% 99.66% 32100 99.66% runtime.gopark 56 0.17% 99.83% 56 0.17% runtime.netpollblock3万多协程在gopark等待状态看具体在等什么(pprof) traces runtime.gopark找到了32100 runtime.gopark runtime.chanrecv1 github.com/xxx/worker.(*Pool).dispatch原来是worker pool里的协程在等channel但生产者已经退出了没人发消息这些协程就永远等下去。问题代码func(p*Pool)Start(){fori:0;ip.size;i{gofunc(){fortask:rangep.taskChan{// 如果channel没close这里永远阻塞task.Run()}}()}}Pool退出时没close channel修复func(p*Pool)Stop(){close(p.taskChan)// 关闭channel所有worker协程会退出}实战四锁竞争分析有个服务QPS死活上不去CPU只用了30%。怀疑是锁竞争。需要先开启mutex profiling默认关闭runtime.SetMutexProfileFraction(1)// 采样率1表示全采集然后采集go tool pprof http://localhost:6060/debug/pprof/mutex(pprof) top flat flat% sum% cum cum% 5.2s 78.5% 78.5% 5.2s 78.5% sync.(*RWMutex).Lock 1.1s 16.6% 95.1% 1.1s 16.6% sync.(*Mutex).Lock78%的时间在等RWMutex定位到代码varcache sync.Map// 本来用的sync.Mapvarmu sync.RWMutexvardatamap[string]interface{}// 被改成了这样读也要抢锁funcGet(keystring)interface{}{mu.RLock()defermu.RUnlock()returndata[key]}之前用sync.Map没问题有人重构的时候改成了普通mapRWMutex高并发下读锁也成了瓶颈。改回sync.MapQPS立马上去了。生产环境注意事项1. 安全隔离pprof端口别暴露到公网内网也要加认证importnet/http/pproffuncmain(){mux:http.NewServeMux()// 加个简单的认证mux.HandleFunc(/debug/pprof/,func(w http.ResponseWriter,r*http.Request){ifr.Header.Get(X-Pprof-Token)!your-secret-token{http.Error(w,Forbidden,403)return}pprof.Index(w,r)})gohttp.ListenAndServe(:6060,mux)}2. 采集开销Heap profile几乎无开销随便采CPU profile有开销别采太久30秒足够Mutex/Block profile需要设置采样率别设成1用100或10003. 批量排查多个实例排查时我一般用星空组网把测试环境和线上机器串起来本地直接访问任意实例的pprof端口不用一台台跳。4. 常用命令速查# CPU分析go tool pprof http://host:port/debug/pprof/profile?seconds30# 内存分析当前在用go tool pprof http://host:port/debug/pprof/heap# 内存分析累计分配go tool pprof -alloc_space http://host:port/debug/pprof/heap# 协程分析go tool pprof http://host:port/debug/pprof/goroutine# 阻塞分析go tool pprof http://host:port/debug/pprof/block# 锁分析go tool pprof http://host:port/debug/pprof/mutex# 直接打开web界面go tool pprof -http:8080[profile文件或URL]交互命令top显示资源占用最高的函数list 函数名显示函数源码和每行的资源占用tree显示调用树web生成SVG在浏览器打开traces显示调用栈搭配其他工具配合trace分析延迟pprof是采样的看不到具体的请求延迟分布。这时候用traceimportruntime/tracef,_:os.Create(trace.out)trace.Start(f)defertrace.Stop()go tool trace trace.out能看到每个协程的时间线GC暂停时间调度延迟等。配合benchmark写benchmark时自动采集profilegotest-bench. -cpuprofilecpu.out -memprofilemem.out go tool pprof cpu.out总结pprof用熟了Go程序的性能问题基本都能定位到CPU高采CPU profile找热点函数内存涨采heap profile对比两次快照找增量响应慢先看协程数再看锁竞争协程泄漏看goroutine profile找阻塞点这工具是Go的标配开销小、信息全没理由不用。每个Go服务都应该把pprof端口开着出问题时能第一时间采集现场。建议新项目直接把pprof加进去别等出了问题再临时加——那时候可能已经OOM重启了现场都没了。