性能调优 | 青训营

92 阅读11分钟

高质量编程

高质量编程要求

要求

  1. 高质量代码——达到正确可靠、简洁清晰的目标
  2. 各种边界条件需要考虑完备
  3. 异常情况的处理设计到位,保证稳定性
  4. 代码易读易维护

编程原则

  1. 简单性
  • 消除”多余的复杂性“,以简单清晰的逻辑编写代码
  • 难理解的代码无法修复改进
  1. 可读性
  • 代码是写给人看的,而不是机器
  • 编写可维护代码的第一步是确保代码可读
  1. 生产力
  • 个人有较高的生产力,团队工作效率也会大幅提升

编码规范

代码格式

  1. gofmt

Go语言官方提供的工具,能自动格式化go语言代码为官方统一风格
常见IDE都支持方便的配置

  1. goimports

也是Go语言官方提供的工具,实际等于gofmt加上依赖包管理,自动增删依赖的包引用、将依赖包按字母序排序并分类

注释

  1. 解释代码作用:比如公共符号
  2. 解释代码如何做的:复杂代码的实现过程稍作解释
  3. 解释代码实现的原因:代码的外部因素,如外部输入;上下文,比如在前提xxx下执行该方法/代码
  4. 解释代码可能出错的情况:比如不正常输入会造成的异常,均写在注释中

命名规则

  1. 变量
    • 简洁胜于冗长,在局部范围中,局部变量的命名尽可能简洁
    • 缩略词全大写,如HTTP,但当其位于变量开头且不需要导出(不为外部引用)时,使用全小写
    • 变量距离其被使用的地方越远,则需要携带越多的上下文信息,全局变量需要在名字中包含尽可能多的上下文信息
    • 具有特定含义的变量名需要尽可能包含该包含的信息
  2. 函数
    • 函数名不包含包名的上下文信息
    • 函数名尽量短
    • 当包内的函数返回类型与包名不同时,需要在函数名包含该返回类型的信息
    • 小写字母组成,不包含大写/下划线
    • 简短但包含一定的上下文信息
    • 不能与标准库同名
    • 不使用常用变量名作为包名,使用单数而非复数,谨慎使用缩写

控制流程

  1. 避免嵌套,保持正常流程(直线)
  2. 优先处理错误/特殊情况,尽早返回或继续循环来减少嵌套
  3. 代码的可维护性和可读性优于高级复杂
  4. 注意:故障问题大多出现在复杂的条件语句和循环语句中

异常和错误处理

  1. 简单错误——仅出现一次的错误,且在其他地方不需要捕获该错误
    • 优先使用errors.New来创建匿名变量直接表示简单错误
    • 如果有格式化需求,使用fmt.Errorf
  2. 错误的Wrap和Unwrap——error之间存在嵌套关系,从而生成的error跟踪链
    • 在fmt.Errorf中使用%w关键字来将一个错误关联至错误链中
  3. 错误判定——判定一个错误是否为特定错误
    • 使用errors.Is(err,特定err)——返回bool,该方法可以判定错误链上的所有错误是否含有特定错误
    • 使用==,只能判断当前返回的错误是否为特定错误
    • 使用errors.As(err,特定err)——该方法可以获取错误链上的特定错误,通过特定错误对象显示错误信息
  4. panic和recover

image.pngimage.png

性能调优

性能优化建议

性能优化的前提是满足正确可靠、简洁清晰等质量因素
性能优化是综合评估,有时候时间效率和空间效率可能对立
针对Go语言特性,介绍Go相关的性能优化建议

Benchmark

功能:为性能测试提供执行代码的单次消耗内存/花费时间等实际数据
指令go test -bench=. -benchmen

Slice

  1. 创建切片时,尽可能在使用make()初始化切片时提供容量信息。

data := make([]int,0,size)优于data := make([]int,0)

  1. 理由:Slice预分配内存
    • 一个切片的创建,包含三个信息:数组指针,片段长度,片段容量。
    • 向一个没有指定容量信息的切片中填充数据会经过两个步骤:扩容+填充,而拥有初始容量的切片,只需要填充。以上两种在容量不足前都会进行扩容。
  2. 使用已有切片的部分切片信息
    • 在原有切片上切片——不会创建新的底层数组,将直接引用原底层数组,原底层数组的内存得不到释放,所需的内存较大
    • 创建一个新的切片,将所需要的切片信息copy下来——创建新的底层数组,所占内存较小
  • 测试内存占用命令:go test -run=. -v

Map

  1. 创建map时,尽可能在使用make()初始化map时提供容量信息。

data := make(map[int]int)优于data := make(map[int]int,size)

  1. 理由:Map预分配内存
    • 不断向map中添加元素的操作会触发map的扩容
    • 提前分配好空间可以减少内存拷贝和Rehash的消耗
    • 建议根据实际需求提前预估好需要的空间
  2. Map的其他使用场景

实现set,可以考虑用map实现,此时,仅需要用到map的键(key),而不需要值。因为即便值设为bool类型(所占内存较小),也会占用一字节内存

字符串处理

常见字符串拼接方式

  1. string“+”string
s := ""
for i := range "abcd"{
    s += i
}
  1. strings.Builder
var builder strings.Builder
var str = "hello"
for i := 0; i<5;i++{
    builder.WriteString(str)    
}
return builder.String()
  1. bytes.Buffer
buf := new(bytes.Buffer)
var str = "hello"
for i := 0; i<5;i++{
    buf.WriteString(str)    
}
return buf.String()

使用比较

  1. 使用“+”拼接性能最差,strings.Builder和bytes.Buffer差不多,strings.Builder更快
  2. 原因:
    1. 字符串在Go语言中是不可变类型,占用的内存大小是固定的,使用+每次都会重新分配内存
    2. 而strings.Builder和bytes.Buffer底层都是[]byte数组,其有内存扩容机制,无需每次拼接重新分配内存
    3. strings.Builder直接将底层的[]byte转换成了字符串类型返回,而bytes.Buffer转化为字符串时重新申请了一块内存,消耗更多内存和时间

空结构体

  1. 空结构体struct{}示例不占据任何的内存空间
  2. 可作为各种场景下的占位符使用
    1. 好处:节省资源
    2. 好处:空结构体本身具备很强寓意,不需要任何值,仅作为占位符
func Fun1(n int) {
	m := make(map[int]struct{})
	for i := 0; i < n; i++ {
		m[i] = struct{}{}
	}
}
func Fun2(n int) {
	m := make(map[int]bool)
	for i := 0; i < n; i++ {
		m[i] = false
	}
}

atomic包

  1. 如何使用atomic包——保证系统并发安全运行
//使用atomic包
func AtomicAddOne(c *atomicCounter) {
	atomic.AddInt32(&c.i, 1)
}
//加锁
func MutexAddOne(c *mutexCounter) {
	c.m.Lock()
	c.i++
	c.m.Unlock()
}
  1. 二者比较

锁的实现是通过操作系统来实现的,属于系统调用,而atomic 操作是通过硬件实现,效率比锁高
锁——sync.Mutex 应该用来保护一段逻辑,不仅仅用于保护一个变量
对于非数值操作,可以使用 atomic.Value,能承载一个interface{}

性能调优原则

  1. 要依靠数据而不是猜测,通过实际数据进行优化
  2. 优化定位于最大瓶颈而不是无足轻重的细枝末节
  3. 不要过早优化,不能根据预测未来可能的需求进行优化
  4. 不要过度优化,

性能调优工具

pprof使用实战

  1. pprof功能简介

image.png

  1. pprof实战——项目搭建

搜索[https://github.com/wolforge/go-pprof-practice](https://github.com/wolforge/go-pprof-practice)下载项目
该项目中存在一些影响性能的问题代码,运行

  1. pprof实战——浏览器查看指标

浏览输入[http://localhost:6060/debug/pprof](http://localhost:6060/debug/pprof)
点击即可查看相关指标信息

pprof实战——查看CPU使用情况

打开任务管理器查看**go-pprof-practice **这个程序占用的CPU运行情况
在终端cmd,输入go tool pprof "http://localhost:6060/debug/pprof/profile?second=10"

  1. pprof实战——pprof工具的使用——top
    1. 从终端进入pprof工具后,可以输入top命令,查看占用资源最多的函数

image.png

  1. 分析五个参数指标

image.png

  • 通常情况,当函数仅自运行,不调用其他函数时,其对应指标flat==cum会等同于flat%==cum%
  • 当函数(func1)运行时会调用其他函数如(func2),则其cum==flat(func1)+flat(func2),cum%==flat%(func1)+flat%(func2)
  • flat==0说明函数中只有其他函数的调用,即该函数没有自己特有的代码部分,仅调用其他函数
  1. 分析:通常占用资源最多的函数可能就是性能瓶颈
  2. pprof实战——pprof工具的使用——其他命令
    1. list func_name 功能:根据指定正则表达式查找代码行
    2. web 功能:调用关系可视化,显示函数调用关系图

pprof实战——查看内存使用情况

打开任务管理器查看**go-pprof-practice **这个程序占用的内存使用情况
在终端cmd,输入go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"
将打开如下页面,显示每个函数占用内存

  1. 点击VIEW即可切换视图

image.png

  1. top视图——按函数分析内存占用情况

image.png

  1. source视图——逐行分析占用内存

image.png

  1. 点击SAMPLE可以显示inuse的采样信息

image.png

  1. alloc_objects:程序累计申请的对象数
  2. alloc_space:程序累计申请的内存大小
  3. inuse_objects:程序当前持有的对象数
  4. inuse_space:程序当前占用的内存大小

pprof实战——查看goroutine情况

在终端cmd,输入go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"

  1. 点击VIEW,选择Flame Graph (火焰图)
    1. 火焰图是动态的,支持点击块进行分析
    2. 由上到下表示调用顺序,上调用下
    3. 每一个块代表一个函数,越长代表占用CPU的时间更长
  2. 点击View,选择Source

通过搜索wolf,注释掉开启100个协程的代码

pprof实战——查看锁mutex的使用情况

在终端cmd,输入go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"
点击View,选择Source,查看哪里耗时较多,是否存在锁

pprof实战——查看阻塞block的使用情况

  1. 操作

在终端cmd,输入go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"
即可显示阻塞信息

  1. 问题
    1. 有的时候阻塞有很多,但并不会一一展示出来,为什么?

原因在于:在复杂的调用关系中,把所有的函数调用节点展示出来其实不利于定位问题,所以,系统设置了一些过滤措施,把占比较小的函数调用节点给省去,不显示。

  1. 如何显示被系统drop的阻塞节点呢?

浏览输入[http://localhost:6060/debug/pprof](http://localhost:6060/debug/pprof),在首页点击block查看所有阻塞的信息

pprof采样过程和原理

  1. CPU

image.png

  1. Heap堆

image.png

  1. goroutine

image.png

  1. block&mutex

image.png

性能调优案例

基本概念介绍

屏幕截图 2023-08-05 194426.png

  • 服务:能单独部署,承载一定功能的程序
  • 依赖:如上图ServiceA的功能实现依赖ServiceB的响应结果
  • 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系
  • 基础库:公共的工具包、中间件

业务服务优化流程

建立服务性能评估手段

  1. 服务性能评估方式
    • 单独benchmark 无法满足复杂逻辑分析
    • 不同负载情况下性能表现差异
  2. 请求流量构造
    • 不同请求参数覆盖逻辑不同
    • 线上真实流量情况
  3. 压测范围
    • 单机器压测
    • 集群压测
  4. 性能数据采集
    • 单机性能数据
    • 集群性能数据

分析性能数据,定位性能瓶颈

  1. 使用库不规范
  2. 高并发场景优化不足

重点优化项改造

  • 正确性是基础
  • 响应数据diff
    • 线上请求数据录制回放
    • 新旧逻辑接口数据diff

优化效果验证

  1. 重复压测验证
  2. 上线评估优化效果
    1. 关注服务监控
    2. 逐步放量
    3. 收集性能数据

进一步优化,服务整体链路分析

  • 规范上游服务调用接口,明确场景需求
  • 分析链路,通过业务流程优化提升服务性能

基础库调优与Go语言优化

AB实验SDK的优化

  1. 分析基础库核心逻辑和性能瓶颈
  • 设计完善改造方案
  • 数据按需获取
  • 数据序列化协议优化
  1. 内部压测验证
  2. 推广业务服务落地验证

编译器&运行时优化

  • 优化内存分配策略
  • 优化代码编译流程
  • 内部测压验证
  • 推广业务服务落地验证

优点

  • 接入简单,只需要调整编译配置
  • 通用性强

总结

性能调优涉及到的方面有很多,有时优化了一方面,但另一方面却受到了限制,因此需要综合考虑,权衡利弊,一个完整的优化方案应考虑到更多的问题,不能为了解决一个难题创造更多难题。