大概什么时候会想到优化
- 某个函数会被频繁调用时
- 接口或者是数据结构设计不算合理从而内存占用过高时
- 接口响应耗时太多
- 当代码太乱,问题频出
那发现问题的方式有哪些 ?
- 单接口压测(这里一般是新接口或者是接口有改动时),固定QPS压测
- 极限QPS压测(这种一般是来查看接口的最大承压规模)
- 全链路压测(这一块儿主要是针对于一些流量高峰前的准备)
一些代码评测工具
Go代码评估工具:
- goreporter – 生成Go代码质量评估报告
- dingo-hunter – 用于在Go程序中找出deadlocks的静态分析器
- flen – 在Go程序包中获取函数长度信息
- go/ast – Package ast声明了关于Go程序包用于表示语法树的类型
- gocyclo – 在Go源代码中测算cyclomatic函数复杂性
- Go Meta Linter – 同时Go lint工具且工具的输出标准化
- go vet – 检测Go源代码并报告可疑的构造
- ineffassign – 在Go代码中检测无效赋值
- safesql – Golang静态分析工具,防止SQL注入
一些工具
- benckmark
- 在对不同的工具、开源库等使用时,可以先对该方法进行benckmark。然后决定到底最后用哪一个
- go pprof
- CPU Profile:用于诊断导致CPU占用较高的代码逻辑。
- Memory Profile:用于诊断导致内存占用较高的代码逻辑。
- Goroutine Profile:用于诊断导致Goroutine占用较高的代码逻辑,分析Goroutine执行的具体逻辑。
- Block Profile:用于诊断阻塞运行的代码逻辑,例如检测大锁(mutex锁定了一个运行时间较长的逻辑,并且有很多其他Goroutine在等待这个锁)
Go 内置的内存 profiler 可以让我们对线上系统进行内存使用采样,有四个相应的指标:
- inuse_objects:当我们认为内存中的驻留对象过多时,就会关注该指标
- inuse_space:当我们认为应用程序占据的 RSS 过大时,会关注该指标
- alloc_objects:当应用曾经发生过历史上的大量内存分配行为导致 CPU 或内存使用大幅上升时,可能关注该指标
- alloc_space:当应用历史上发生过内存使用大量上升时,会关注该指标
接下来会从这几方面来展开讲一下
Slice
- 提前为slice分配内存
○ 在必要的时候,使用第三个参数: make([]T, 0, len)
○ 如果事先不知道确切的数量并且slice是临时的,可以设置得大一些,只要slice在运行时不会增长。
- 不要忘记使用“copy”
○ 这点是需要在复制时尽可能不要使用 append,例如,在合并两个或多个slice时。
- 不要留下未使用的slice
○ 如果需要从slice中切下一小块并仅使用它,其实主要部分也会保留下来。可以使用copy产生一个新的slice,而旧的对象让GC回收。
string
- strings.Builder,bytes.Buffer相近
- 优先使用 strings.Builder 而不是 =
struct
- 通过内存对齐来减小struct大小
- 可以对齐struct(根据字段的大小,以正确的顺序排列),从而可以减小struct本身的大小
- 遍历 []struct{} 使用下标而不是 range
- 有的时候我们在使用channel的时候,通常如果是来表示一个标识,而不是发送数据的情况下,用于通知子协程来完成一些任务的情况,这种情况就比较好实用 空结构体了
defer
- 尽量不要使用 defer,或者至少 不要在循环中使用defer 。
map
- 跟Slice一样,需要提前分配内存
- 初始化map时,指定它的大小
- 如果需要表示占位时,其value用空结构体表示
- struct{} 什么都不是
- 清空map
- map只能增长,不能缩小。需要控制这一点——完全而明确地重置map
- 指针使用上
- 如果 map 不包含指针,而且要知道字符串也是指针——使用[]byte而不是字符串作为键。
interface
- 计算内存分配 ○ 请记住,要给一个接口赋值,首先需要将其拷贝到某处,然后粘贴一个指针。关键字是拷贝。事实证明,装箱和拆箱的成本将近似于结构体的大小和一次分配。
sync.Pool
fasthttp。它几乎把所有的对象都用sync.Pool维护,所以它才自称是http的10倍,但其实没
协程池
绝大部分应用场景,go是不需要协程池的。当然,协程池还是有一些自己的优势:
- 可以限制goroutine数量,避免无限制的增长。
- 减少栈扩容的次数。
- 频繁创建goroutine的场景下,资源复用,节省内存。(需要一定规模。一般场景下,效果不太明显。)
go对goroutine有一定的复用能力。所以要根据场景选择是否使用协程池,不恰当的场景不仅得不到收益,反而增加系统复杂性。
减小锁消耗
并发场景下,对临界区加锁比较常见。带来的性能隐患也必须重视。常见的优化手段有:
- 减小锁粒度:go标准库当中,math.rand就有这么一处隐患。当我们直接使用rand库生成随机数时,实际上由全局的globalRand对象负责生成。globalRand加锁后生成随机数,会导致我们在高频使用随机数的场景下效率低下。
- atomic:适当场景下,用原子操作代替互斥锁也是一种经典的lock-free技巧。标准库中sync.map针对读操作的优化消除了rwlock,是一个标准的案例。对它的介绍文章也比较多,不在赘述。
将多个小对象合并成一个大的对象
减少不必要的指针间接引用,多使用copy引用
例如使用bytes.Buffer代替*bytes.Buffer,因为使用指针时,会分配2个对象来完成引用。
cpu耗时优化
- make时提前预估size
- 临时的map、slice采用sync.pool
- 大于32k也可以用sync.pool
- 不滥用goroutine,减少gc压力
- 不滥用mutex,减少上下文切换
- []byte与string临时变量转换用unsafe
- 减少reflect、defer使用
- atomic无锁使用