• golang 内存和cpu优化


    golang 内存和cpu优化

    背景介绍

    在压力测试的过程中程序会发生内存和CPU飙升的情况,并且持续一段时间后,虽有所回落,但是内存还是没有及时回收,分析可能存在内存泄露的情况。

    问题分析

    (1.)在代码中加入性能分析的监控,具体如下:

    import 	(
      _ "net/http/pprof" // 引入 pprof 模块
      _ "github.com/mkevac/debugcharts"  // 可选,图形化插件
    )
    
    func main(){
        // ...
        // 内存分析
    	go func() {
    		http.ListenAndServe("0.0.0.0:8090", nil)
    	}()
        // ...
    }
    

    (2.) 运行程序,由于程序运行在远端linux服务器,如需在本地查看还需要进行端口映射。当然也可以直接在远端linux服务器上通过命令行方式进行查看,但是追踪代码路径时可能找不到,需要指定代码源路径。

    go tool pprof -http 172.0.0.88:8070 http://172.0.0.88:8090/debug/pprof/heap
    // 浏览器访问
    http://172.0.0.88:8070
    

    (3.)通过jemter进行压力测试

    (4.)查看top10的内存占用,分析top10的函数占用,这里可以看到addMap()函数占比较高,可着重分析。

    参数说明:

    列名 含义
    flat 本函数的执行耗时
    flat% flat 占 CPU 总时间的比例。
    sum% 前面每一行的 flat 占比总和
    cum 累计量。指该函数加上该函数调用的函数总耗时
    cum% cum 占 CPU 总时间的比例

    (5.)停掉jemter的压力测试,等待两分钟后(便于GC进行垃圾回收)查看仍然在占用中的内存。这里可以查询inuse_space和inuse_obj这两个参数。这里也可以通过peek查看具体代码的哪一行占用内存较高。

    (6.)既然没有了用户操作,内存还被占用,没有释放,那必然存在问题,进一步查看这一块代码进行分析。

    这里分析代码发现,addMap有一个递归操作,在调用该函数结束后,map仍然没有释放,这里需要说明的是go1.14一直存在map内存的问题,go1.17该问题已修复。这里我做了对该函数的性能测试,并打印了内存信息。

    // 打印堆栈信息
    func printMemStats() {
      var m runtime.MemStats
      runtime.ReadMemStats(&m)
      fmt.Printf("Alloc = %v TotalAlloc = %v  Just Freed = %v Sys = %v NumGC = %v
    ",
        m.Alloc/1024, m.TotalAlloc/1024, ((m.TotalAlloc-m.Alloc)-lastTotalFreed)/1024, m.Sys/1024, m.NumGC)
    
      lastTotalFreed = m.TotalAlloc - m.Alloc
    }
    -------------------------------------------------------------------
    参数说明:
    Alloc:当前堆上对象占用的内存大小。
    TotalAlloc:堆上总共分配出的内存大小。
    Sys:程序从操作系统总共申请的内存大小。
    NumGC:垃圾回收运行的次数。
    
    // 基准测试
    go test -bench=. -benchmem // 进行时间、内存的基准测试
    
    go test -bench=. -run=none -benchmem -memprofile=mem.pprof
    go test -bench=. -run=none -blockprofile=block.pprof
    go test -bench=. -run=none -benchmem -memprofile=mem.pprof -cpuprofile=cpu.pprof
    

    测试代码

    import (
    	"testing"
    )
    
    func BenchmarAddMap(b *testing.B) {
    	// 运行 addMap 函数 b.N 次
    	for n := 0; n < b.N; n++ {
    		addMap()
    		printMemStats()  // 打印内存信息
    	}
    }
    
    // 输出内存和CPU的信息
    go test -bench=. -run=none 
    -benchmem -memprofile=mem.pprof 
    -cpuprofile=cpu.pprof 
    -blockprofile=block.pprof
    
    // 使用go tool进行分析
    go tool pprof cpu.pprof
    top10 -cum // 查看top10占用情况
    list xxx // 查看具体某个函数的内存
    
    go tool pprof -http=":8080" cpu.pprof  // 使用web界面进行分析
    

    经过对addMap()函数进行性能测试发现,申请的内存一直在增长,总的内存占比也在增长。

    (7.)map内存释放

    • 如果删除的元素是值类型,如int,float,bool,string以及数组和struct,map的内存不会自动释放

    • 如果删除的元素是引用类型,如指针,slice,map,chan等,map的内存会自动释放,但释放的内存是子元素应用类型的内存占用

    • 将map设置为nil后,内存被回收,map 不会收缩 “不再使用” 的空间。就算把所有键值删除,它依然保留内存空间以待后用。

      综合以上三点结论,我们需要对所有频繁使用map的地方,进行手动释放map内存,即将map=nil

      slice在用完后,最好也能手动置空 slice= slice[0:0],理由是:golang中slice是对数组的引用,底层实现实际上还是数组。对slice一定要谨慎使用append操作。如果cap未变化时,slice是对数组的引用,并且append会修改被引用数组的值。append操作导致cap变化后,会复制被引用的数组,然后切断引用关系。

    (8.)修改完map后,继续分析,发现goroutine中wg使用也存在部分问题。

    WaitGroup 对象内部有一个计数器,最初从0开始,它有三个方法:Add(), Done(), Wait() 用来控制计数器的数量。Add(n) 把计数器设置为nDone() 每次把计数器-1wait() 会阻塞代码的运行,直到计数器地值减为0。 使用wg时计数器不能为负值,另外WaitGroup对象不是一个引用类型,在通过函数传值的时候需要使用地址。

    // 错误示例:
    func testGoroutine() {
    	wg := sync.WaitGroup{}
    	for i := 0; i < 10; i++ {
            // wg.Add(1)  // 正确用法
    		go func() {
    		    wg.Add(1)   // 注意:wg.Add需要放到goroutine外部,才能起到计数的作用
    			defer wg.Done()
    			fmt.Println("hello world")
    		}()
    	}
    	wg.Wait()
    }
    

    另外这里建议使用goroutine池来实现,防止因为启动过多的goutine而导致内存占用过多,需要控制goroutine数量, 可以使用sync waitGroup+ 非阻塞channel实现 代码如下:

    package gopool
    
    import "sync"
    
    // goroutine pool
    type GoroutinePool struct {
    	c  chan struct{}
    	wg *sync.WaitGroup
    }
    
    // 采用有缓冲channel实现,当channel满的时候阻塞
    func NewGoroutinePool(maxSize int) *GoroutinePool {
    	if maxSize <= 0 {
    		panic("max size too small")
    	}
    	return &GoroutinePool{
    		c:  make(chan struct{}, maxSize),
    		wg: new(sync.WaitGroup),
    	}
    }
    
    // add
    func (g *GoroutinePool) Add(delta int) {
    	g.wg.Add(delta)
    	for i := 0; i < delta; i++ {
    		g.c <- struct{}{}
    	}
    
    }
    
    // done
    func (g *GoroutinePool) Done() {
    	<-g.c
    	g.wg.Done()
    }
    
    // wait
    func (g *GoroutinePool) Wait() {
    	g.wg.Wait()
    }
    

    (9.)goroutine修改完后,再次测试效果又好了很多,再分析一下timer和ticker,毕竟这两个也很容易产生内存泄露,进一步完善一下代码。

    sendTimer := time.NewTimer(time.Second)
    	for {
    		if !sendTimer.Stop() {
    			select {
    			case <-sendTimer.C:
    			default:
    			}
    		}
    		select {
    		case <-this.exit:
    			sendTimer.Stop()
    			return
    		case <-sendTimer.C:
    			// 发送
    			// doSomething()
    			sendTimer.Reset(time.Second)
    		}
    	}
    

    (10.)尽可能的少用全局变量,因为全局变量只有在程序结束后,内存才能得到释放。尽量使用局部变量(栈上分配),多个局部变量合并一个大的结构体或数组,减少扫描对象的次数,一次回尽可能多的内存。

    (11)defer虽好,但是也要适当使用。

    当前代码中有许多地方为了打印日志方便,直接使用defer log.Printf("xxx"),建议直接在函数结尾处打印,或者发生错误的地方打印。defer设计之初,主要用于资源释放,锁的释放等场景。

    defer的实现机制:编译器通过 runtime.deferproc “注册” 延迟调用,除目标函数地址外,还会复制相关参数(包括 receiver)。在函数返回前,执行 runtime.deferreturn 提取相关信息执行延迟调用。这其中的代价自然不是普通函数调用一条 CALL 指令所能比拟的。

    (12)查看某程序内存占用,可以通过pidstat -r -p 13084 1来查看。

    minflt/s: 每秒次缺页错误次数(minor page faults),次缺页错误次数意即虚拟内存地址映射成物理内存地址产生的page fault次数
    majflt/s: 每秒主缺页错误次数(major page faults),当虚拟内存地址映射成物理内存地址时,相应的page在swap中,这样的page fault为major page fault,一般在内存使用紧张时产生
    VSZ:      该进程使用的虚拟内存(以kB为单位)
    RSS:      该进程使用的物理内存(以kB为单位)
    %MEM:     该进程使用内存的百分比
    Command:  拉起进程对应的命令
    

    参考链接

    map内存释放

    timer的正确使用

    defer的性能分析
    go-test
    其他

    【励志篇】: 古之成大事掌大学问者,不惟有超世之才,亦必有坚韧不拔之志。
  • 相关阅读:
    优雅高效的MyBatis-Plus工具快速入门使用
    mybatis中#{}和${}的区别
    异常处理com.sun.image.codec.jpeg.JPEGImageEncoder
    图片压缩之-JPEGCodec失效替换方案
    Bugly实现app全量更新
    MyBatis下MySqL用户口令不能为空
    java.lang.OutOfMemoryError: PermGen space及其解决方法
    Hibernate or 的用法
    如何理解<base href="<%=basePath%>"
    小程序 wx.request ajax示例
  • 原文地址:https://www.cnblogs.com/tomtellyou/p/15147022.html
Copyright © 2020-2023  润新知