高质量编程与性能调优实战(性能分析工具pprof)很幸苦的 纯手打博客

本文是记录2022年5月字节跳动青训营的课程笔记!!!

要点:

如何编写更简洁清晰的代码

常用Go语言程序优化手段

熟悉Go程序性能分析工具

了解工程中性能优化的原则和流程

1.高质量编程

什么是高质量?编写的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码

正确性:是否考虑各种边界条件,错误的调用是否能够处理

可靠性:异常情况或者错误的处理是否明确,依赖的服务出现异常是否能够处理

简洁:逻辑是否简单,后续调整功能或者新增功能是否能够快速支持

清晰:其他人在阅读理解代码的时候是否能清楚明白,重构或者修改功能不会担心出现无法预料的问题

简介

编程原则:

简单性:消除“多余的复杂性”,以简单清晰的逻辑编写代码;

              不理解的代码无法修复改进;

可读性:代码是写给人看的,而不是机器;

编写可维护代码的第一步是确保代码可读;

生产力:团队整体工作效率非常重要;

编码规范:

推荐使用gofmt自动格式化代码

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

Good code has lots of comments, bad code requires lots of comments

                                                                                       ——   Dave Thomas and Andrew Hunt

注释应该解释代码的作用:

注释公共符号:

注释实现过程: 

适合解释代码的外部因素 提供额外的上下文:

解释代码的限制条件:

公共符号的注释:Google Style指南中有两条规则(1.任何既不明显也不简短的功能必须予以注释。2;无论长度或复杂度如何,对库中的任何函数都必须进行注释)

 注:不需要注释实现接口的方法,公共符号要始终注释

编码命名规范:

variable:

简洁胜于冗长

缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写

变量距离其实被使用的地方越远,则需要携带越多的上下文信息

function:

函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的

函数名尽量简短

package:

只由小写字母组成,不包含大写字母和下划线等字符

简短并包含一定的上下文信息

不要与标准库同名

以下规则尽量满足:

不适用常用变量名作为包名

是用单数而不是复数

谨慎的使用缩写

控制流程:

避免嵌套,保持正常流程清晰

保持正常代码路径为最小缩进

 

Go语言代码不是成功的路径越来越深地嵌套在右边,而是随着函数的执行,正常流程大的代码会沿着屏幕向下移动

一个功能如果可以通过多个功能的线性结合来实现,那么他的结构就会非常简单。反过来用条件分支控制代码、毫无章法地增加状态数等行为会让代码变得难以理解。

如果能让正常流程自上而下、简单清晰地进行处理,代码的可读性就会大幅提高,与此同时可维护性也将提高,添加功能等改良工作也会变得更加容易

故障问题大多出现在复杂的条件语句和循环语句中,在维护这种逻辑时,添加功能会变成高风险的操作,很容易遗漏部分条件导致问题

错误和异常处理:

简单的错误:

简单的错误指的是仅出现一次的错误,且在其他地方不需要捕获该错误

优先使用erroes.New来创建匿名变量来直接表示该错误

如果有格式化的需求,使用fmt.Errorf



错误的Wrap和Unwrap

错误的Wrap实际上时提供了一个error嵌套在另一个error的能力,从而生成一个error的跟踪链

在fmt.Errorf中使用:%w关键字来将一个错误关联至错误链中

错误判定:

判定一个错误是否为特定错误,使用errors.Is

不同于使用== ,该方法可以判定错误链上的所有错误是否含有特定的错误

在错误链上获取特定种类的错误,使用errors.As

panic:

不建议在业务中使用panic(因为panic发生后,会向上传播至调用栈顶,如果当前goroutine中所有defer函数都不包含recover就会造成整个程序崩溃)

当程序启动阶段发生不可逆转的错误时,可以在init或main函数中使用panic

recover:

recover只能被defer的函数中使用

嵌套无法生效

只有在当前goroutine生效

defer的语句是后进先出

如果需要更多的上下文信息,可以recover后在log中记录当前的调用栈

性能优化建议

性能优化的前提是满足正确可靠、简洁清晰等质量因素

性能优化综合评估,有时候时间效率和空间效率可能对立

Benchmark

Go语言提供了支持基准性能测试的benchmark工具

以计算斐波拉契数列的函数为例,分为两个文件,fib.go编写函数代码,fib_test.go编写benchmark的逻辑,通过命令benchmark可以得到预测结果

-benchmaem表示统计内存信息

slice:

slice预分配内存(尽可能在使用make()初始化切片时提供容量信息)

切片本质是一个数组片段的描述:包括数组指针,片段的长度,片段的容量(不改变内存分配下的最大长度)

切片操作并不复制切片指向的元素

创建一个新的切片会复用原来切片的底层数组(当append之后的长度小于等于cap,将会直接里永远底层数组的剩余空间,当append后长度大于cap则会分配一块更大的区域来容纳新的底层数组)

新的问题:大内存未释放

原切片由大量的元素构成,但是我们在原切片的基础上切片,虽然只使用了很小的一段,但底层数组在内存中仍然占据了大量空间,得不到释放

可以使用copy代替re-slice

map:

同slice,不断地向map中添加元素会触发map的扩容

提前分配好空间可以减少内存拷贝和Rehash的消耗

建议根据实际需求提前预估好需要的空间

字符串处理

常见的字符串拼接方式

 

 实际运行,“+”拼接性能最差,strings.Builder,bytes.Buffer相近,strings.Buffer最快

字符串在Go中是不可变的类型,占用内存空间是固定的

使用+每次都会重新分配内存

strings.Builder,bytes.Buffer底层都是[]byte 数组

内存扩容策略,不需要每次拼接都重新分配内存

空结构体

使用空结构体节省内存

空结构体struct{}实例不占用任何空间

可作为各种场景下的占位符使用

节省资源、空结构体本身具备很强的语义,不需要任何值。仅作为占位符

atomic包

在工作中迟早会遇见多线程编程的场景,比如实现一个多线程公用的的计数器,如何保证计数准确、线程安全有不同的方式

使用atomic包:

锁的实现是通过操作系统来实现的,属于系统调用

atomic操作通过硬件实现,效率比锁搞

sync.Mutex应该保护一段逻辑,不仅仅用于保护一个变量

对于非数值操作,可以使用atomic.Value能承载一个interface{}

性能优化总结:

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

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

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

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

2.性能分析工具 pprof

性能调优的前提是对应用程序性能表现有实际的数据指标,对于Go程序,有一个很方便的工具就是pprof

这里有一个开源项目,已经制造了一些问题代码,需要进行排查

wolfogre/go-pprof-practice: go pprof practice. (github.com)

实际分析排查过程

 pprof 的采样过程和原理

  • CPU 采样
  • 堆内存采样
  • 协程和系统线程采样
  • 阻塞操作和锁竞争采样

CPU采样

CPU采样会记录所有调用栈和他们所占用的时间

在采样时,进程会每秒暂停一百次,每次记录当前的调度信息。汇总后,根据调用栈在采样中出现的次数来推断函数的运行时间

堆内存采样

采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的内存大小和数量

采样率:每分配512KB记录一次

采样时间:从程序运行开始到采样时

采样指标:alloc_space,alloc_objects,inuse_spac,inuse_objects

计算方式:inuse = alloc - free

协程和系统线程采样

协程:记录所有用户发起且在运行中的协程(即入口非runtime开头的)runtime.main的调用栈信息

线程:记录程序创建所有系统线程的信息

阻塞操作和锁竞争采样

业务优化 

流程:

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

建立服务性能评估手段 

之所以不用benchmark是因为时间服务逻辑比较复杂,希望从更高的层面分析服务的性能问题,同时机器在不同负载下的性能也会不同

评估手段建立后,产出一个服务的性能指标分析报告

有了服务优化前的性能报告和一些性能采样数据,我们可以进行性能瓶颈分析

业务服务常见的性能问题可能是使用的基础组件不规范

类似日志使用不归发,一部分是调试日志发布到线上,一部分是线上服务在不同的调用链路上数据有差别,测试场景日志还好,但是到了真实线上全景场景,会导致日志数量增加影响性能

另外常见的性能问题就是高并发场景的优化不足

重点优化项改造:

正确性实际基础!

性能优化的前提是保证正确性,使用在变动较大的性能优化上线之前,需要进行正确性验证,借助自动化手段保证优化后程序的正确性

优化效果验证:

用同样的数据对优化后的服务进行压测,同时压测并不能保证和线上表现完全一致,有时还要通过线上的表现在进行分析改进,是个长期的过程

基础库优化

适应范围更广,覆盖更多服务

AB 实验 SDK 的优化:

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

Go语言优化

适应范围最广,Go 服务都有收益

优化方式

  • 优化内存分配策略
  • 优化代码编译流程,生成更高效的程序
  • 内部压测验证
  • 推广业务服务落地验证

总结

性能评估要依靠数据,用实际的结果做决策

对于pprof工具,可以通过分析实际的程序,熟悉相关功能,理解好基本原理,后续能够更好的解决性能问题

在真正的服务性能调优流程中,链路会很长,重点是要保证正确性,不影响功能,同时定位好问题

参考资料:

  • 注释应该解释代码什么情况会出错
  • 《编程的原则:改善代码质量的101个方法》,总结了很多编程原则,按照是什么 -> 为什么 -> 怎么做进行了说明,mp.weixin.qq.com/s/vXSZOl2Gt…
  • Go 官方博客,有关于 Go 的最新进展,go.dev/blog/

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

裁道友不裁贫道

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值