Go 优化技巧

举报
Rolle 发表于 2024/08/18 08:58:12 2024/08/18
【摘要】 大概什么时候会想到优化某个函数会被频繁调用时接口或者是数据结构设计不算合理从而内存占用过高时接口响应耗时太多当代码太乱,问题频出那发现问题的方式有哪些 ?单接口压测(这里一般是新接口或者是接口有改动时),固定QPS压测极限QPS压测(这种一般是来查看接口的最大承压规模)全链路压测(这一块儿主要是针对于一些流量高峰前的准备)一些代码评测工具Go代码评估工具:goreporter – 生成Go代...

大概什么时候会想到优化

  • 某个函数会被频繁调用时
  • 接口或者是数据结构设计不算合理从而内存占用过高时
  • 接口响应耗时太多
  • 当代码太乱,问题频出

那发现问题的方式有哪些 ?

  • 单接口压测(这里一般是新接口或者是接口有改动时),固定QPS压测
  • 极限QPS压测(这种一般是来查看接口的最大承压规模)
  • 全链路压测(这一块儿主要是针对于一些流量高峰前的准备)

一些代码评测工具

Go代码评估工具:

  1. goreporter – 生成Go代码质量评估报告
  2. dingo-hunter – 用于在Go程序中找出deadlocks的静态分析器
  3. flen – 在Go程序包中获取函数长度信息
  4. go/ast – Package ast声明了关于Go程序包用于表示语法树的类型
  5. gocyclo – 在Go源代码中测算cyclomatic函数复杂性
  6. Go Meta Linter – 同时Go lint工具且工具的输出标准化
  7. go vet – 检测Go源代码并报告可疑的构造
  8. ineffassign – 在Go代码中检测无效赋值
  9. safesql – Golang静态分析工具,防止SQL注入

一些工具

  • benckmark
    1. 在对不同的工具、开源库等使用时,可以先对该方法进行benckmark。然后决定到底最后用哪一个
  • go pprof
    1. CPU Profile:用于诊断导致CPU占用较高的代码逻辑。
    2. Memory Profile:用于诊断导致内存占用较高的代码逻辑。
    3. Goroutine Profile:用于诊断导致Goroutine占用较高的代码逻辑,分析Goroutine执行的具体逻辑。
    4. Block Profile:用于诊断阻塞运行的代码逻辑,例如检测大锁(mutex锁定了一个运行时间较长的逻辑,并且有很多其他Goroutine在等待这个锁)

Go 内置的内存 profiler 可以让我们对线上系统进行内存使用采样,有四个相应的指标:

  1. inuse_objects:当我们认为内存中的驻留对象过多时,就会关注该指标
  2. inuse_space:当我们认为应用程序占据的 RSS 过大时,会关注该指标
  3. alloc_objects:当应用曾经发生过历史上的大量内存分配行为导致 CPU 或内存使用大幅上升时,可能关注该指标
  4. 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是不需要协程池的。当然,协程池还是有一些自己的优势:

  1. 可以限制goroutine数量,避免无限制的增长。
  2. 减少栈扩容的次数。
  3. 频繁创建goroutine的场景下,资源复用,节省内存。(需要一定规模。一般场景下,效果不太明显。)

go对goroutine有一定的复用能力。所以要根据场景选择是否使用协程池,不恰当的场景不仅得不到收益,反而增加系统复杂性。

减小锁消耗

并发场景下,对临界区加锁比较常见。带来的性能隐患也必须重视。常见的优化手段有:

  • 减小锁粒度:go标准库当中,math.rand就有这么一处隐患。当我们直接使用rand库生成随机数时,实际上由全局的globalRand对象负责生成。globalRand加锁后生成随机数,会导致我们在高频使用随机数的场景下效率低下。
  • atomic:适当场景下,用原子操作代替互斥锁也是一种经典的lock-free技巧。标准库中sync.map针对读操作的优化消除了rwlock,是一个标准的案例。对它的介绍文章也比较多,不在赘述。

将多个小对象合并成一个大的对象

减少不必要的指针间接引用,多使用copy引用

例如使用bytes.Buffer代替*bytes.Buffer,因为使用指针时,会分配2个对象来完成引用。

cpu耗时优化

  1. make时提前预估size
  2. 临时的map、slice采用sync.pool
  3. 大于32k也可以用sync.pool
  4. 不滥用goroutine,减少gc压力
  5. 不滥用mutex,减少上下文切换
  6. []byte与string临时变量转换用unsafe
  7. 减少reflect、defer使用
  8. atomic无锁使用
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。