性能优化和 pprof 使用 | 青训营笔记

163 阅读2分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 4 天

一、本堂课重点内容

  • bench 使用
  • 常用性能优化策略
  • pprof 使用

二、详细知识点介绍:

bench 使用

  • 查看参数说明

    go help testflag
    
  • 给出内存分配, 大小, 次数等统计

    go test -bench=. -benchmem
    

预分配内存

slice, map 注意预分配内存, 能有效减少内存分配次数

陷阱: 大内存未释放

  • 在已有切片基础上创建切片,不会创建新的底层数组

  • 场景

    • 原切片较大,代码在原切片基础上新建小切片
    • 原底层数组在内存中有引用,得不到释放
  • 可使用 copy 替代re-slice

    func GetLastBySlice(origin [lint) [lint {
        return origin[len(origin)-2:]
    }
    
    func GetLastByCopy(origin [lint) [lint {
        result := make( [lint, 2)
        copy(result, origin[len(origin)-2:])
        return result
    }
    

字符串处理

  • +
  • strings.Builder
  • bytes.Buffer

使用 + 拼接性能最差,strings.Builder, bytes.Buffer 相近,strings.Buffer 更快

  • 宇符串在 Go 语言中是不可变类型,占用内存大小是固定的
  • 使用 + 每次都会重新分配内存
  • strings.Builderbytes.Buffer 底层都是 byte 数组
  • 内存扩容策略,不需要每次拼接重新分配内存

为什么 strings.Buffer 更快?

bytes.Buffer 转化为字符串时重新申请了一块空间

// To build strings more efficiently, see the strings. Builder type.
func (b *Buffer) String() string {
    if b == nil {
        // Special case, useful in debugging.
        return "<nil>"
    }
    return string(b.buf[b.off:])
}

strings.Builder 直接将底层的 Byte 转换成了字符串类型返回

// String returns the accumulated string.
func (b *Builder) String() string {
    return *(*string)(unsafe.Pointer(&b.buf))
}

巧用空结构体

节省内存

  • 空结构体实例不占据任何的内存空间
  • 可作为各种场景下的占位符使用
  • 节省资源
  • 空结构体本身具备很强的语义,即这里不需要任何值,仅作为占位符
func EmptyStructMap(n int) {
    m := make(map[int]struct{})
    for i := 0; i < n; it+ {
        m[i] = struct{k}
    }
}

func BoolMap(n int) {
    m := make(map[int]bool)
    for i := 0; i < n; it+ {
        m[i] = false
    }
}

实现 Set

  • 实现 Set,可以考虑用 map 来代替
  • 对于这个场景,只需要用到 map 的键,而不需要值
  • 即使是将 map 的值设置为 bool 类型,也会多占据 1 个字节空间

使用 atomic 代替加锁

type atomicCounter struct {
    i int32
}

func AtomicAddOne(c *atomicCounter) {
    atomic.AddInt32(Sc.I, 1)
}
  • 锁的实现是通过操作系统来实现,属于系统调用
  • atomic 操作是通过硬件实现,效率比锁高
  • sync.Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量
  • 对于非数值操作,可以使用 atomic.Value,能承载一个 interface{}

三、实践练习例子:

是用 pprof 进行性能问题排查

启动 http 服务

以下面的代码为例

package main

import (
	"log"
	"net/http"
        
        // 会自动注册 handler 到 http server,方便通过 http 接口获取程序运行采样
	_ "net/http/pprof" 
	"os"
	"runtime"
)

func main() {
	log.SetFlags(log.Lshortfile | log.LstdFlags)
	log.SetOutput(os.Stdout)

	runtime.GOMAXPROCS(1) // 限制 CPU 使用数,避免过载 
        runtime.SetMutexProfileFraction(1) // 开启对锁调用的跟踪 
        runtime.SetBlockProfileRate(1) // 开启对阻塞操作的跟踪

	// 启动一个 http server,注意 pprof 相关的 handler 已经自动注册过了
	if err := http.ListenAndServe(":9091", nil); err != nil {
		log.Fatal(err)
	}
	os.Exit(0)

}

项目启动后就可以测试了, 访问 http://localhost:9091/debug/pprof/

image.png

指标内容
allocs内存分配情况的采样信息
blocks阻塞操作情况的采样信息
cmdline显示程序启动命令及参数
goroutine当前所有协程的堆栈信息
heap堆上内存使用情况的采样信息
mutex锁争用情况的采样信息
profileCPU 占用情况的采样信息
threadcreate系统线程创建情况的采样信息
trace程序运行跟踪信息

trace 可以行参阅《深入浅出 Go trace》

使用 pprof 命令行

注意, 对于不同的指标, 连接不一样

svg 或者 top 并不一定能完全找到问题, 可能 http 被阻塞, 注意查看网页里的原始数据

CPU

go tool pprof http://localhost:6060/debug/pprof/profile

image.png

内存

go tool pprof http://localhost:6060/debug/pprof/heap

GC

排查频繁内存回收

频繁的 GC 对 golang 程序性能的影响也是非常严重的。内存使用量并不高,可能 GC 过于频繁.

为了获取程序运行过程中 GC 日志,我们需要先退出程序,再在重新启动前赋予一个环境变量,同时为了避免其他日志的干扰,使用 grep 筛选出 GC 日志查看:

GODEBUG=gctrace=1 ./go-pprof-practice | grep gc

注意 GC 的间隔时间

接下来使用 pprof 排查时,关注的不是什么地方在占用大量内存,而是什么地方在不停地申请内存。

go tool pprof http://localhost:6060/debug/pprof/allocs

由于内存的申请与释放频度是需要一段时间来统计的,需要保证程序已经运行了几分钟之后,再运行命令

在 golang 里,对象是使用堆内存还是栈内存,由编译器进行逃逸分析并决定,如果对象不会逃逸,便可在使用栈内存,但总有意外,就是对象的尺寸过大时,便不得不使用堆内存。所以这里设置申请 16 MiB 的内存就是为了避免编译器直接在栈上分配,如果那样得话就不会涉及到 GC 了。

Goroutine

在 golang 中,协程本身是可能泄露的,或者叫协程失控,进而导致内存泄露。

go tool pprof http://localhost:6060/debug/pprof/goroutine

go tool pprof http://localhost:6060/debug/pprof/mutex

阻塞 (block)

go tool pprof http://localhost:6060/debug/pprof/block

常用内置命令

top

image.png

list

image.png

web

功能是生成 svg 图片, 需要提前安装 graphviz

image.png

四、课后个人总结:

pprof 大法好

五、引用参考: