第五届字节跳动青训营Class3笔记 | 青训营笔记

161 阅读6分钟

这是我参与「第五届青训营 」笔记创作活动的第3天

本节课程主要介绍了Go语言的高质量编程和性能调优。

1.高质量编程

编程原则

简单性 可读性 生产力

编码规范

注释

公共符号始终要注释,包中每个公共符号、并不简短或简单的功能都需要注释......

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

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

代码格式

使用gofmt自动格式化代码

image.png

或使用goimports格式化代码(同时格式化依赖包的引用)

变量命名

缩略词全大写,其他使用驼峰式命名规则进行区分,但是缩略词在变量名非开头时则不需要:

eg. ServeHttp XMLHTTPRequest xmlHTTPRequest

函数命名

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

包命名

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

控制流程

尽量保持正常的代码路径为最小缩进,优先处理错误情况、特殊情况,尽早返回或持续循环来减少嵌套。

错误和异常处理

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

2.高性能编程

Benchmark

go test -bench=. -benchmem

image.png

Slice预分配内存

尽可能在使用make()初始化切片时提供容量信息

切片的本质是一个数组片段的描述

包括数组指针、片段的长度、片段的容量(在不改变内存分配下的最大长度)

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

创建一个新的切片会复用原来切片的底层数组

type slice struct {
    array unsafe.Pointer
    len int
    cap int
}

image.png

陷阱:大内存未释放

在已有切片的基础上创建切片,不会创建新的底层数组

// 只使用了origin数组后两个元素的空间,直接根据原数组创建切片会导致新的底层数组产生

func GetLastBySlice(origin []int) []int{
    return origin[len(origin)-2:]
}

func GetLastBySlice(origin []int) []int{
    result := make([]int,2)
    copy(result, origin[len(origin)-2:]
    return result
}

同理,使用map也需要预分配内存

字符串处理

使用strings.Builder来进行字符串处理

与java相同,Go语言中字符串类型所占空间都是不变的,使用'+'运算符都会新建一份空间,使用strings.Builder可以达到动态分配空间的目的。

strings.Builder也可以预分配空间,使用Grow()方法可以为其预分配一片空间

使用空结构体节省内存

空结构体实例struct{}不占用任何空间,只是作为占位符号

例:实现Set数据类型,也即只使用Map的键,不使用值,则可以声明为myset := make(map[stirng]sturct{}) 对应的值只需要使用struct{}{}即可(前半部分为空结构体实例,后半部分为其实现)

atomic包

使用atomic包可以用于多线程维护一个变量(代替加锁去锁过程)

本质上因为锁是通过操作系统来实现吗,而atomic是通过硬件实现,性能更高。

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

3.性能调优实战

性能优化原则

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

性能分析工具pprof

image.png 为代码增加pprof工具如下:

log.SetFlags(log.Lshortfile | log.LstdFlags)
log.SetOutput(os.Stdout)

runtime.GOMAXPROCS(1)
runtime.SetMutexProfileFraction(1)
runtime.SetBlockProfileRate(1)

go func() {
        if err := http.ListenAndServe(":6060", nil); err != nil {
                log.Fatal(err)
        }
        os.Exit(0)
}()

浏览器查看指标:pprof会将采样结果输出到localhost:6060/debug/pprof上,也可以使用go tool pprof <url>来直接获取目标目录的信息

cpu排查

使用go tool pprof http://localhost:6060/debug/pprof/profile?seconds=10指令进入pprof cpu排查模式

使用top命令显示cpu当前运行的全部进程

image.png

指标解释:

flat:当前函数本身执行耗时

flat%: 当前函数执行占用cpu总时间的比例

sum%:自上到当前行所有flat%的总和

cum:当前函数本身的消耗+其他函数调用耗时

cum%:当前函数本身耗时+其他函数调用耗时占cpu总时间的比例

当flat==cum时,flat没有调用其他函数 flat==0时,函数中只有其他函数的调用

使用list命令根据指定的正则表达式查找代码行

web命令可以调用关系可视化

image.png

排查对应函数问题

堆内存排查

执行go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"即可进行堆内存的排查

image.png

也可以在菜单栏下拉框中选择其他方式进行盘查(如源码盘查等)

指标说明

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

协程问题排查

执行go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"即可进行协程内存的排查

阻塞问题排查

执行go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"即可进行阻塞问题的排查

pprof的采样过程和采样原理

image.png

cpu

操作系统每10ms向进程发送一次SIGPROF信号,进程每次接收到SIGPROF会记录调用堆栈,每100ms读取已经记录的调用栈并写入输出流。

堆内存

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

协程采样

记录所有用户发起且在运行中的goroutine,用runtime.main的调用栈信息

4.性能调优案例

image.png

基本概念:

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

建立服务性能评估手段:

根据线上真实流量情况,进行单机器压测和集群压测,获取单机性能数据和集群性能数据。

业务服务优化

线上请求数据录制回放,新旧逻辑接口数据diff

基础库优化

分析基础库核心逻辑和性能瓶颈,内部压测验证,推广业务服务落地验证

总结

本节课从质量和性能两个角度介绍了Go的优化,对Go语言的代码规范进行了进一步的强调,同时对于基本内存分配、代码逻辑、协程管理等多个角度优化了代码选择。之后介绍了pprof库在调优场景的应用和原理,最后结合实际服务情景介绍了系统应对策略。

引用

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