Profiling and Optimizing in Go

为何做profiling

使用profiling工具能帮你发现包括CPUIO和内存在内多种类型的热点。所谓热点,是指那些为了能显著提升性能而值得你去关注的地方。了解针对性能的不同边界因素也是比较重要的,比方说,如果一个程序使用100 Mbps带宽的网络进行通信,而目前已经占用了超过90 Mbps的带宽,为了提升它的性能,你拿这样的程序也没啥办法了。在磁盘IO、内存消耗和计算密集型任务方面,也有类似的边界因素。

go语言如何做profiling

    我日常主要使用”runtime/pprof”包

CPU Profiler

    Go 运行时包含了内建的CPU分析器,它用来展示某个函数耗费了多少CPU百分时间。这里有三种方式来使用它:

var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
 
 func main() {
     flag.Parse()
     if *cpuprofile != "" {
         f, err := os.Create(*cpuprofile)
         if err != nil {
             log.Fatal(err)
         }
         pprof.StartCPUProfile(f)
         defer pprof.StopCPUProfile()
     }
             ...

    执行go tool pprof progress filename

  最主要的命令是’topN’,查看profile里采样最多的n条记录。

(pprof) top10

Total: 2525 samples

     298  11.8%  11.8%      345  13.7% runtime.mapaccess1_fast64

     268  10.6%  22.4%     2124  84.1% main.FindLoops

     251   9.9%  32.4%      451  17.9% scanblock

     178   7.0%  39.4%      351  13.9% hash_insert

     131   5.2%  44.6%      158   6.3% sweepspan

     119   4.7%  49.3%      350  13.9% main.DFS

      96   3.8%  53.1%       98   3.9% flushptrbuf

      95   3.8%  56.9%       95   3.8% runtime.aeshash64

      95   3.8%  60.6%      101   4.0% runtime.settype_flush

      88   3.5%  64.1%      988  39.1% runtime.mallocgc

    每一列的含义: 采样数,采样比,累计采样比,出现数,出现比,函数名

    出现:(正在运行或等待调用函数返回)

 

    如果想以第四/第五列来排序:

    (pprof) top5 -cum

    Total: 2525 samples

           0   0.0%   0.0%     2144  84.9% gosched0

           0   0.0%   0.0%     2144  84.9% main.main

           0   0.0%   0.0%     2144  84.9% runtime.main

           0   0.0%   0.0%     2124  84.1% main.FindHavlakLoops

         268  10.6%  10.6%     2124  84.1% main.FindLoops

        注:In fact the total for `main.FindLoops` and `main.main` should have been 100%, but each stack sample only includes the bottom 100 stack frames; during about a quarter of the samples, the recursive `main.DFS` function was more than 100 frames deeper than `main.main` so the complete trace was truncated.

web命令  生成SVG格式状态分析图,如下:

箭头表示调用关系,箭头上的数字表示调用的次数

   Profiling and Optimizing in Go_第1张图片

    

查看具体某个函数的源码:

(pprof) list DFS

Total: 2525 samples

ROUTINE ====================== main.DFS in /home/rsc/g/benchgraffiti/havlak/havlak1.go

   119    697 Total samples (flat / cumulative)

     3      3  240: func DFS(currentNode *BasicBlock, nodes []*UnionFindNode, number map[*BasicBlock]int, last []int, current int) int {

     1      1  241:     nodes[current].Init(currentNode, current)

     1     37  242:     number[currentNode] = current

     .      .  243:

     1      1  244:     lastid := current

    89     89  245:     for _, target := range currentNode.OutEdges {

     9    152  246:             if number[target] == unvisited {

     7    354  247:                     lastid = DFS(target, nodes, number, last, lastid+1)

     .      .  248:             }

     .      .  249:     }

     7     59  250:     last[number[currentNode]] = lastid

     1      1  251:     return lastid

    可以看到采样情况精确到代码行。通过样本数,我们可以定位到热点行,然后考虑适合的优化策略; 除了递归占用较多的运行,还有242250246占用了采样占比也不少,当可以用slicearray的时候就不要用map

MEM profiling

var memprofile = flag.String("memprofile", "", "write memory profile to this file")
 ...
if *memprofile != "" {
   f, err := os.Create(*memprofile)
   if err != nil {
       log.Fatal(err)
   }
   pprof.WriteHeapProfile(f)
   f.Close()
   return
}

    查看命令同cpuprofile

    (pprof) top5

     Total: 82.4 MB

         56.3  68.4%  68.4%     56.3  68.4% main.FindLoops

         17.6  21.3%  89.7%     17.6  21.3% main.(*CFG).CreateNode

          8.0   9.7%  99.4%     25.6  31.0% main.NewBasicBlockEdge

          0.5   0.6% 100.0%      0.5   0.6% itab

          0.0   0.0% 100.0%      0.5   0.6% fmt.init

    这里显示了函数当前大致分配的内存。第一行main.FindLoops分配了56.3M/82.4MB的内存。

 

    类似 CPU profiling,通过 list 命令查看函数具体的内存分配情况:

       (pprof) list FindLoops

         Total: 82.4 MB

         ROUTINE ====================== main.FindLoops in /home/rsc/g/benchgraffiti/havlak/havlak3.go

           56.3   56.3 Total MB (flat / cumulative)

         ...

            1.9    1.9  268:     nonBackPreds := make([]map[int]bool, size)

            5.8    5.8  269:     backPreds := make([][]int, size)

              .      .  270:

            1.9    1.9  271:     number := make([]int, size)

            1.9    1.9  272:     header := make([]int, size, size)

            1.9    1.9  273:     types := make([]int, size, size)

            1.9    1.9  274:     last := make([]int, size, size)

            1.9    1.9  275:     nodes := make([]*UnionFindNode, size, size)

              .      .  276:

              .      .  277:     for i := 0; i < size; i++ {

            9.5    9.5  278:             nodes[i] = new(UnionFindNode)

              .      .  279:     }

         ...

              .      .  286:     for i, bb := range cfgraph.Blocks {

              .      .  287:             number[bb.Name] = unvisited

           29.5   29.5  288:             nonBackPreds[i] = make(map[int]bool)

              .      .  289:     }

         ...

          一眼就可以看出哪里可以优化

        (pprof) web mallocgc

 

     很容易看出从FindLoops产生大量的内存分配,虽然golang是自带垃圾回收的

 

Race detection

    当两个goroutine并发访问同一个变量,且至少一个goroutine对变量进行写操作时,就会发生数据竞争(data race)。为了协助诊断这种bugGo提供了一个内置的数据竞争检测工具。通过传入-race选项,go tool就可以启动竞争检测。

        $ go test -race mypkg // to test the package

        $ go run -race mysrc.go // to run the source file

        $ go build -race mycmd // to build the command

       $ go install -race mypkg // to install the package

//testrace.go
package main
 
import "fmt"
import "time"
 
func main() {
        var i int = 0 
        go func() {
                for {
                        i++ 
                        fmt.Println("subroutine: i = ", i)
                        time.Sleep(1 * time.Second)
                }   
        }() 
        for {
                i++ 
                fmt.Println("mainroutine: i = ", i)
                time.Sleep(1 * time.Second)
        }   
}

        执行可以看到如下输出

        $go run -race testrace.go

        mainroutine: i = 1

        WARNING: DATA RACE

        Read by goroutine 5:

        main.func·001()

        /Users/brant/Test/Go/testrace.go:10 +0×49

        Previous write by main goroutine:

        main.main()

        /Users/brant/Test/Go/testrace.go:17 +0xd5

        Goroutine 5 (running) created at:

        main.main()

        /Users/brant/Test/Go/testrace.go:14 +0xaf

        ==================

        subroutine: i = 2

        mainroutine: i = 3

        subroutine: i = 4

        mainroutine: i = 5

        subroutine: i = 6

        

 

GC trace

    GODEBUG=gctrace=1 ./myProgram (2 > ./gc.prof)   设置环境变量

   从统计情况来看,gc延时在20ms级,对于业务没什么影响。目前利用提前分配缓存的方式,做对比测试

    缓存前:

    gc24(4): 11+4+15320+1 us, 65 -> 131 MB, 997302 (7313677-6316375) objects, 10528/3680/0 sweeps, 11(701) handoff, 16(134) steal, 136/84/84 yields

    缓存后:

    gc9(4): 19+7+22589+2 us, 255 -> 510 MB, 3118992 (3119052-60) objects, 34311/0/0 sweeps, 21(925) handoff, 17(182) steal, 157/82/99 yields

    说明:

    第几次(参与gc的工作线程数);

    gc时间(stop the world+ sweeping + marking + waiting for worker threads to finish

    内存(heap)变化;

    对象数(共分配共释放);

  描述sweep阶段: total 10528 memory spans  /3620 were swept on demand or in background/0 were swept during stop-the-world phase ,剩下是未使用的spans

 

 

Block profiler

类似cpu profiler,我们用runtime/pprof.Lookup("block").WriteTo. 来记录程序运行中的block信息。

    并非所有的阻塞都是不利的。当一个goroutine阻塞时,底层的工作线程就会简单地转换到另一个goroutine。所以Go并行环境下的阻塞 与非并行环境下的mutex的阻塞是有很大不同的。

time.Ticker上发生的阻塞通常是可行的,如果一个goroutine阻塞Ticker超过十秒,你将会在profile中看到有十秒的阻塞,这 是很好的。发生在sync.WaitGroup上的阻塞经常也是可以的,例如,一个任务需要十秒,等待WaitGroup完成的goroutine会在 profile中生成十秒的阻塞。发生在sync.Cond上的阻塞可好可坏,取决于情况不同。消费者在通道阻塞表明生产者缓慢或不工作。生产者在通道阻塞,表明消费者缓慢,但这通常也是可以的。在基于通道的信号量发生阻塞,表明了限制在这个信号量上的goroutine的数量。发生在sync.Mutexsync.RWMutex上的阻塞通常是不利的。你可以在可视化过程中,在pprof中使用--ignore标志来从profile中排除已知的无关阻塞。

过多的goroutine阻塞/解除阻塞消耗了CPU时间,帮助减少goroutine阻塞。

1.在生产者--消费者情景中使用充足的缓冲通道。无缓冲的通道实际上限制了程序的并发可用性。

2. 针对于主要为读取的工作量,使用sync.RWMutex而不是sync.Mutex。因为读取操作在sync.RWMutex中从来不会阻塞其它的读取操作。甚至是在实施级别。

3.在某些情况下,可以通过使用copy-on-write技术来完全移除互斥。如果受保护的数据结构很少被修改,可以为它制作一份副本,然后就可以这样更新它:

type Config struct {
    Routes   map[string]net.Addr
    Backends []net.Addr
}
var gConfig Config
var config unsafe.Pointer  // actual type is *Config
config=unsafe.Pointer(&gConfig)
 
// Worker goroutines use this function to obtain the current config.
func CurrentConfig() *Config {
    return (*Config)(atomic.LoadPointer(&config))
}
 
// Background goroutine periodically creates a new Config object
// as sets it as current using this function.
func UpdateConfig(cfg *Config) {
    atomic.StorePointer(&config, unsafe.Pointer(cfg))
}

    这种模式防止在更新时写入阻塞读取。

    4.分割是另一种用于减少共享可变数据结构竞争和阻塞的通用技术。下面是一个展示如何分割哈希表(hashmap)的例子:

type Partition struct {
    sync.RWMutex
    m map[string]string
}
 
const partCount = 64
var m [partCount]Partition
 
func Find(k string) string {
    idx := hash(k) % partCount
    part := &m[idx]
    part.RLock()
    v := part.m[k]
    part.RUnlock()
    return v
}

    5.使用缓存池

    func (p *Pool) Get() interface{}

        func (p *Pool) Put(x interface{})

 

 Alloc Trace

    GODEBUG=allocfreetrace=1

    记录每次内存分配和释放情况

    tracealloc(0xc208062500, 0x100, array of parse.Node)

    goroutine 16 [running]:

    runtime.mallocgc(0x100, 0x3eb7c1, 0x0)

        runtime/malloc.goc:190 +0x145 fp=0xc2080b39f8

    runtime.growslice(0x31f840, 0xc208060700, 0x8, 0x8, 0x1, 0x0, 0x0, 0x0)

        runtime/slice.goc:76 +0xbb fp=0xc2080b3a90

    text/template/parse.(*Tree).parse(0xc2080820e0, 0xc208023620, 0x0, 0x0)

        text/template/parse/parse.go:289 +0x549 fp=0xc2080b3c50

    ...

     

    tracefree(0xc208002d80, 0x120)

    goroutine 16 [running]:

    runtime.MSpan_Sweep(0x73b080)

           runtime/mgc0.c:1880 +0x514 fp=0xc20804b8f0

    runtime.MCentral_CacheSpan(0x69c858)

           runtime/mcentral.c:48 +0x2b5 fp=0xc20804b920

    runtime.MCache_Refill(0x737000, 0xc200000012)

           runtime/mcache.c:78 +0x119 fp=0xc20804b950

    ...

    说明:内存块地址,内存大小,类型,goroutine id,堆栈

 

Scheduler trace

    goroutine是非常轻量级的,它就是一段代码,一个函数入口,以及在堆上为其分配的一个堆栈, Go运行库最多会启动$GOMAXPROCS个线程来运行goroutine

    GODEBUG=schedtrace=1000 ./myserver

    SCHED 1004ms: gomaxprocs=4 idleprocs=0 threads=11 idlethreads=4 runqueue=8 [0 1 0 3]

    SCHED 2005ms: gomaxprocs=4 idleprocs=0 threads=11 idlethreads=5 runqueue=6 [1 5 4 0]

    SCHED 3008ms: gomaxprocs=4 idleprocs=0 threads=11 idlethreads=4 runqueue=10 [2 2 2 1]

    时间跨度,max processidle process,工作线程数,空闲线程,runable_gorouting全局队列长度, 可执行的 goroutine 的预处理器队列的长度

你可能感兴趣的:(Profiling and Optimizing in Go)