高质量编程与性能调优 | 青训营笔记

77 阅读7分钟

这是我参与[第五届青训营]伴学笔记创作活动的第9天

一、高质量编程

1、简介

高质量的定义:编写的代码能够达到正确可靠、简介清晰的目标

 即:
 各种边界条件考虑完备
 异常情况处理,稳定性保证
 易读易维护

编程原则

简单性:

 消除多余的复杂性,以简单清晰的逻辑编写代码
 不理解的代码无法修复改进

可读性:

 代码是写给人看的,而不是机器
 编写可维护可维护代码的第一步是确保代码可读

生产力:

 团队整体工作效率非常重要

2、编码规范

如何编写高质量的Go代码

代码格式

 使用gofmt自动格式化代码
 使用goimprots自动增删依赖的包引用、将代码包按字母序排序并分类

注释

 注释应该解释代码作用
 注释应该解释代码如何做的
 注释应该解释代码实现的原因
 注释应该解释代码什么情况会出错

命令规范

 简介胜于冗长
 缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写
 变量距离其被使用的地方越远,则需要携带越多的上下文信息

控制流程

 避免嵌套,保持正常流程清晰
 尽量保持正常代码路劲为最小缩进

错误和异常处理

 error尽可能提供简明的上下文信息链,方便定位问题
 panic用于真正异常的情况
 recover生效范围,在当前goroutine的被defer的函数中生效

3、性能优化建议

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

如何使用

Benchmark工具

 性能表现需要实际数据衡量
 go语言提供了支持基准性能测试的benchmark工具

slice

 尽可能在使用make()初始化切片时提供容量信息
 切片本质时一个数组片段的描述
     包括数组指针
     片段的长度
     片段的容量(不改变内存分配情况下的最大长度)
 切片操作并不复制切片指向的元素
 创建一个新的切片会复用原来切片的底层数组
 ​
 关于切片的另一个陷阱:大内存未释放
 在已有切片基础上创建切片,不会创建新的底层数组
 场景
     原切片较大,代码在原切片基础上新建小切片
     底层数组在内存中有引用,得不到释放
 可使用copy代替re-slice

map预分配内存

 不断向map中添加元素的操作会触发map的扩容
 提前分配好空间可以减少内存拷贝和Rehash的消耗
 建议根据实际需求提前预估好需要的空间

字符串处理

 使用strings.Builder进行字符串拼接
 ​
 关于字符串在go语言中的分析:
     字符串在go语言中时不可变类型,占用内存大小时固定的
     使用+每次都会重新分配内存
     strings.Builder,bytes.Buffer底层都是[]byte数组
     内存扩容策略,不需要每次拼接重新分配内存
 注:使用 + 拼接字符串性能最差,strings.Builder,bytes.Buffer相近,strings.Buffer更快

空结构体

 空结构体struct{}实例不占据任何内存空间
 可作为各场景下的占位符使用
     节省资源
     空结构体本书具备很强的语义,即这里不需要任何值,仅作为占位符

atomic包

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

小结:

避免常见的性能陷阱可以保证大部分程序的性能

普通应用代码,不要一味地追求程序地性能

越高级的性能优化手段越容易出问题

在满足正确可靠、简洁清晰的质量要求的前提下提高程序性能

二、性能调优实战

1、简介

性能调优的原则

 要依靠数据而不是猜测
 要定位最大瓶颈而不是细枝末节
 不要过早优化
 不要过度优化

2、性能分析工具pprof

pprof是一个用于可视化、分析性能和分析数据的工具,它可以计算出在什么地方耗费了多少cpu、memory

pprof功能简介

image-20230221195835206

pprof排查实战

前置准备:先下载项目代码,能够编译运行;会占用1cpu核心和超过1GB的内存

pprof的采样过程和原理

CPU

 采样对象:函数调用和它们占用的时间
 采样率:100次/秒,固定值
 采样时间:从手动启动到手动结束
 ​
 操作系统:
     每10ms向进程发送一次SIGPROF信号
 进程:
     每次接收的SIGPROF会记录调用堆栈
 写缓冲:
     每100ms读取已经记录的调用栈并写入输出流

Heap-堆内存

 采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量
 采样率:每分配512KB记录一次,可在运行开头修改,1为每次分配均记录
 采样时间:从程序运行开始到采样时
 采样指标:alloc_space,alloc_objects,inuse_space,inuse_objects
 计算方式:inuse=alloc—free

Goroutine-协程&ThreadCreate-线程创建

 Goroutine
     记录所有用户发起且在运行中的goroutine(即入口非runtime开头的)runtime.main的调用栈信息
 ThreadCreate
     记录程序创建的所有系统线程信息

Bolck-阻塞&Mutex-锁

 阻塞操作
     采样阻塞操作的次数和耗时
     采样率:阻塞耗时超过阙值的才会被记录,1为每次阻塞均记录
 锁竞争
     采样争抢锁的次数和耗时
     采样率:只记录固定比例的锁操作,1为每次加锁均记录

3、性能调优案例

简介

介绍实际业务服务性能优化的案例

对逻辑相对复杂的程序如何进行性能调优

业务服务优化

基本概念

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

流程

 建立服务性能评估手段
 分析性能数据,定位性能瓶颈
 重点优化项改造
 优化效果验证

建立服务性能评估手段

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

优化效果验证

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

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

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

基础库优化

AB实验SDK优化

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

Go语言优化

编译器&运行时优化

 优化内存分配策略
 优化代码编译流程,生成更高效的程序
 内部压测验证
 推广业务服务落地验证
 ​
 优点
     接入简单,只需要调整编译配置
     通用性强