高质量编程及性能调优实战(学习笔记)| 青训营;

51 阅读19分钟

高质量编程

一.简介

编写的代码能够达到正确可靠,简洁清晰的目标可称为高质量代码

各种边界条件是否考虑完备

异常情况处理,稳定性保证

易读易维护

编程原则

  1. 简单性 消除”多余的复杂性“,以简单清晰的逻辑编写代码 不理解的代码无法修复改进
  2. 可读性 代码是写给人看的,而不是机器 编写可维护代码的第一步是确定代码可读
  3. 生产力 团队整体工作效率非常重要

二.规范

代码格式
  • gofmt:Go语言的开发团队制定了统一的官方代码风格,并且推出了gofmt工具来帮助开发者格式化他们的代码到统一的风格,gofmt是一个cil程序,会优先读取标准输入,根据传入路径的不同,对应的处理也不同,如果传入的路径下包含多个go文件,就会将该路径下的所以go文件都格式化,如果传入的路径就只是单一的文件路径,就只会对该文件进行格式化,但是无论是哪种,当我们使用时,一定是得加上路径,否则命令就会无效。

    一般简化代码我们所用的参数是 -s,也就是gofmt -s [path],优点是可以去除数组、切片、Map初始化时不必要的类型说明;可以去除数组切片操作时不必要的索引指定;去除迭代是非必要的变量赋值;

  • goimport工具是Go官方提供的一种工具,它能够为我们自动格式化Go语言代码并对所有引入的包进行管理,包括自动增删依赖的包引用,将依赖包按字母序排序并分类,如果你使用的是Goland IDE的时候,建议使用goimports工具,它具备依赖管理+gofmt的功能

注释
  1. 注释公共符号,函数的功能
  2. 注释实现的过程
  3. 适合解释代码的外部因素
  4. 解释代码的限制条件
  5. 提供代码未表达出的上下文信息
命名规范
  • 变量:简洁胜于冗长,缩略词全大写,但当其位于变量开头且不需要导出时,使用全小写 例如,ServeHTTP,而不是ServeHttp, 使用XMLHTTPRequst,或者xmlHTTPRequest, 变量距离其被使用的地方越远,则需要携带越多上下文信息 (理解:途中可能会被多次处理,相当于携带了多个信息,而我们为了区分各个变量就要很清晰的知道这些信息,其主要表现在对这个变量的命名上,就比如设置了两个变量,一个变量是会经过一个函数的处理而带着相应的值,而另一个变量就没有,为了区分这两个变量就会在变量名上进行处理)

    全局变量在其名字中需要更多的上下文信息,使得在不同地方可以轻易辨认出其含义

    有特定含义的变量,不要去简化,这样会降低变量名的信息量

  • 函数 函数名不携带包名的上下文信息,因为包名和函数名总是成对出现的 例如,当http包中要创建两个服务函数,服务的函数名分别为servel和servelHTTP,由于导入时http.servel和http.servelHTTP相比,前者更简洁,使用命名按照前面那种命名 函数名尽量简短 当名为foo的包某个函数返回类型FOO时,可以省略类型信息而不导致歧义 当名为foo的包某个函数返回类型T时(T并不是FOO),可以在函数名中加入类型信息

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

    1. 简短并包含一定的上下文信息,例如schema,task等

    2. 不要与标准库同名,例如不要使用sync或者strings

    3. 不要使用常量名作为包名,例如使用bufio而不是buf

    4. 使用单数而不是复数,例如使用encoding而不是encodings

    5. 谨慎使用缩写,例如使用fmt在不破坏上下文的情况下比format更加简短

  • 核心目标:降低阅读理解代码的成本 重点考虑上下文信息,设计简洁清晰的名称

控制流程
  • 避免嵌套,保持正常流程清晰 例如如果两个分支都包含return语句,则可以去除冗余的else。

  • 尽量保持正常代码路径为最小路径。

  • 优先处理错误情况/特殊情况,尽早返回或继续循环来减少嵌套 最常见的正常流程的路径被嵌套在两个if条件内,成功的退出条件是return nil,必须仔细匹配大括号来发现, 函数最后一行返回一个错误,需要追溯到匹配的左括号,才能了解何时会触发错误 ,如果后续正常流程需要增加一步操作,调用新的函数,则又会增加一层嵌套。

  • 线性原理,处理逻辑尽量走直线,避免复杂的嵌套分支,正常流程代码沿着屏幕向下移动,提供代码可维护性和可读性,故障问题大多出现在复杂的条件语句和循环语句中。

错误和异常处理
  • 简单错误: 简单的错误指的是仅出现一次的错误,且在其他地方不需要捕获该错误,优先使用errors.New来创建匿名变量来直接表示简单错误,如果格式化的需求,使用fmt.Errof

  • 错误的Wrap和Unwrap: 错误的Wrap实际上是提供了一个error嵌套另一个error的能力,从而生成一个error的跟踪链,在fmt.Errof中使用:%w关键字来将一个错误关联至错误链中, error尽可能提供简明的上下文信息链,方便定位问题。 (注意:go1.13在errors中新增了三个新API和一个新的format关键字,分别是errors.is errors,As,errors.Unwrap,fmt.Errorf的%w。如果项目运行在小于go1.13的版本中,导入golang.org/x/xerrors来使用)

  • 错误的判定: 判断一个错误是否为特定错误,使用errors.is, 不同于使用==,使用该方法可以判定错误连上的所有错误是否含有特定的错误, 在错误连中获取特定种类的错误,使用errors.As

  • panic: 用于真正异常的情况,不建议在业务代码中使用panic,调用函数不包含recover会造成程序崩溃,若问题可以被屏蔽或解决,建议使用error代替panic,当程序启动阶段发生不可逆转的错误时,可以在init或main函数中使用panic

  • recover: recover只能在被defer的函数中使用,嵌套无法生效,只在当前goroutine生效,defer的语句是后进先出 如果需要更多的上下文信息,可以recover后在log中记录当前的调用栈,发生错误之后可以用来查看

三.性能优化建议

Benchmark工具 基准测试: go test -bench = . -benchmem

  • slice 尽可能在使用make()初始化切片时提供容量信息,预先分配内存空间,原因是若没有提前提供的话,在存入比切片容量大的数据时,切片会提前有一个扩容的过程,这个过程会在一定程度上影响到这整个的一个性能,注意的是切片操作并不会复制切片指向的元素,而是创建一个新的切片去复用其他原来切片的底层数组,这个会导致当一个很大的切片中复制一小部分切片,就仍然会引用到原来大的切片,降低性能,为了提高这么一个性能,用从copy去代替re-slice操作,

  • map 预分配内存,提前分配好空间可以减少内存的拷贝和rehash的消耗

  • 字符串处理,字符串的拼接

    使用+号拼接性能最差,strings.Builder,bytes.Buffer相近,前者还要更快。

    分析:字符串在Go语言中是不可变类型,占用内存大小是固定的,使用+每次都会重新分配内存,会影响性能,strings.Builder,bytes.Buffer底层都是【】byte数组,内存扩容,不需要每次都像+号一样重新分配内存。

    为啥strings.Builder更快呢:bytes,Buffer 转化为字符串时重新申请了一块空间,而strings.Builder直接将底层的【】byte转换成了字符串类型返回

  • 空结构体 使用空结构体节省内存,空结构体struct{}实例不占据任何的内存空间,例如可以用到map上面去,当我们只需要用到map的键时而不需要用到值时,就可以使值为空结构体,不占任何内存

  • atomic包 当我们需要保护一个变量时,不仅仅可以用到lock,还能用到atomic包,并且性能更好;锁的实现是通过操作系统来实现的,属于系统调用,而atomic操作是通过硬件来实现,效率比锁会高;sync.Mutex应该用来保护一段逻辑,而不仅仅只是用来保护一个变量;对于非数值操作,可以使用atomic.Value,能承载一个interface{}

小结:

  1. 避免常见的性能陷阱可以保证大部分程序的性能
  2. 普通应用代码,不要一味地追求程序的性能
  3. 越高级的性能优化手段越容易出现问题
  4. 在满足正确可靠,简洁清晰的质量要求的前提下提高程序性能

性能调优实战

简介

原则

  1. 要依靠数据而不是猜测
  2. 要定位最大瓶颈而不是细致末节
  3. 不要过早优化
  4. 不要过度优化

pprof实战

功能简介:

pprof是golang内置的性能分析工具,在进行性能问题分析时使用,是我们调试应用性能的常用工具。pprof主要是有四个模块:

  • CPU profile:当前程序的CPU使用情况,pprof按照一定频率去采集应用程序在CPU和寄存器上面的数据
  • Memory Profile:当前程序内存使用情况
  • Block Profiling:程序当前goroutines不在运行状态的情况,可以用来分析和查找死锁等性能瓶颈
  • Goroutine Proifiling:程序当前goroutine的使用情况,查看所有goroutine,查看调用关系,可发现未释放的go协程
引入pprof:
  • 方法一:针对未使用http服务的普通单机程序上使用,项目中导入runtime/pprof,主要用来产生dump文件,然后再使用Go Tool PProf 来分析这个运行日志
  • 方法二:就是我们比较常用的一种方式,针对就是开启了http,就可以在项目中导入net/http/pprof,这个是对方法一导入的runtime/pprof的封装,可以做到直接在web上看到当前当前web服务的状态
  • 方法二具体实现:直接就是在main包里导入import _ "net/http/pprof",当然你还要开启http服务,也就是还需要导入import "net/http,导入之后需要设置一个端口去监听,而且还要单独设置,利用一个协程,具体如下:
go func() {
		if err := http.ListenAndServe(":6060", nil); err != nil {
			log.Fatal(err)
		}
		os.Exit(0)
	}()

这里设置一个goroutine去监听,这里的端口是自己电脑上空闲的端口,不唯一,我这里设置的是6060,这样设置好了就可以去网页上查看相关信息了

pprof进行分析
  • 介绍 在网页中输入自己程序中设置的端口,然后再加上/debug/pprof就可以打开pprof的网页,里面有多个参数,各个参数的含义如下: 8379a7f632eec72e7a00cd4d19c6b6f4.png

  • profile 首先我们得得到profile,proflie可以给出当前程序的运行状态,我们点进网页,然后点击profile,会加载一段时间然后让你下载一个profile文件,然后根据网页下面的描述在终端或者DOS命令窗口执行 go tool pprof命令 加上你的那个文件绝对地址.

    或者可以直接加网址,网址是那个网页后加上/profile也可以在后面加时间,意思是多少时间的数据,就比如: go tool pprof "http://localhost:6060/debug/pprof/profile?second=10s" 然后出现(pprof)就说明成功了,

    输入top命令可查找占用资源最多的函数,里面的几个参数的意思

  1. flat 当前函数本身的执行耗时

  2. flat% flat占cpu总时间的比例

  3. sum%上面每一行的flat%总和

  4. cum 指当前函数本身加上其调用函数的总耗时

  5. cum% cum占cpu总时间的比例

    若flat==cum 函数中没有调用其他函数,flat == 0 函数中只有其他函数调用,也可以用list 查看指定的函数的时间,指定的正则表达式查看代码行,list Eat web,可以让这些调用关系可视化,前提是得下载可视化文件Graphviz,配置对应的环境变量

  • 然后查看heap,堆内存,在doc命令窗口或者终端输入 go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap" 注意的是8080端口不能被占用,如果被占用可以换其他端口,8081,8082等都行 打开网页就可以看到是哪些占了很大的内存,将对应的程序注释掉即可(实际项目中则是要做优化,可以通过view上的top来看最占内存的是哪个;可以通过view中的source来看哪个语句最占内存,这样将对应最大内存的优化一下, 如果还发现占有很大内存,还可以再次打开这个网页,点击上面的SAMPLE,里面有四个参数,分别是
  1. alloc_objects 程序累计申请的对象数

  2. alloc_space 程序累计申请的内存大小

  3. inuse_objects 程序当前持有的对象数

  4. inuse_space 程序当前占用的内存大小

    可以点击alloc_space来查看当前程序累计申请的内存大小,找到最大的哪个程序,进行优化

  • 接着就是查看goroutine-协程数,这里的协程数过多,应该存在问题,与heap类似,在doc命令窗口输入 go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine", 端口也得注意不能被占用,网页会出现应该流程图,如果没有的话,点击VIEW 然后点击里面的Graph,可以看到整个一个过程,滑到最下面就可以发现问题,有一个地方多了太多协程,将对应地方进行优化即可,也可以看Flame Graph,也在VIEW中,由上到下表示调用顺序,长度就代表占用cpu的时间,这个图是动态的,可以点击进行分析,也可以在source下对应搜索函数,然后可以得到对应函数中哪条语句导致的,最后进行优化

  • 再然后就是看mutex,锁的情况,与上面一样 go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex" 进网页也同样可以看到,有一个程序花了大量的时间,出了问题,对应进行优化即可,与上面的步骤基本一致

  • 最后就是看block 同样的步骤 go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block" 这里需要注意,我们需要关注的是项目内出现的阻塞,而对于本身启动项目所需要的外部的文件,我们就可以不用管,这里处理完应该是还会剩一个外部的,这个是启动这个项目做的,不用去修改

pprof 采样过程和原理

CPU

采样对象:函数调用和它们占用的时间 采样率:100次/秒,固定值 采样时间:从手动启动到手动结束 流程: 开始采样->设定信号处理函数->开启定时器 停止采样->取消信号处理函数->关闭定时器

具体采样过程 操作系统 每10ms向进程发送一次SIGPROF信号 进程 每次接收到SIGPROF会记录调用堆栈的信息 写缓冲 每100ms读取已经记录的调用栈并写入输出流

过程:go程序进程启动后注册一个定时器,然后启动定时器,操作系统以10ms为一个循环(loop)发送SIGPROF信号,进程接受到这个信号,就会以记录一次堆栈信息,并且每100ms写入缓冲中一次,缓冲再写入输出流,进程停止后就会关闭定时器,最后形成一个profile文件,整个过程操作系统每发一次信号,进程记录一次堆栈信息然后输出十次堆栈信息

Heap-堆内存

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

Goroutine-协程&ThreadCreate-线程创建

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

Goroutine :stop the world->遍历allg切片->输出创建g的堆栈->Start The World ThreadCreate: stop the world->遍历allm链表->输出创建m的堆栈->Start The World

Block-阻塞 & Mutex-锁

阻塞操作 采样阻塞操作的次数和耗时 采样率:阻塞耗时超过阈值的才会被记录,1为每次阻塞均 记录 过程: 阻塞操作上报调用栈和消耗时间给Profiler,Profiler做出判断,时间未达阈值则丢弃,若到达则采样遍历阻塞记录,最后统计阻塞次数和耗时

锁竞争 采样争抢锁的次数和耗时 采样率:只记录固定比例的锁操作,1为每次加锁均记录 过程: 锁竞争操作上报调用栈和消耗时间给Profiler,Profiler做出判断,比例未命中则丢弃,若到达则采样遍历锁记录,最后统计锁竞争次数和耗时

案例

案例一

业务服务优化 基本概念:

  • 服务:单独部署,承载一定功能的程序
  • 依赖:ServiceA的功能实现依赖ServiceB的响应结果,称为ServiceA依赖Serivice B
  • 调用链路:能支持一个接口请求的相关服务集合及其相互之间的依赖关系 例如:客户端到网关,然后网关将对应的请求转换成对应的服务,服务再依赖与其他服务,其他服务下可能有连接到存储器或者其他层获取数据,这整个一条链计算调用链路 基础库:公共的工具包,中间件

优化流程

  1. 建立服务性能评估手段,服务性能评估方式,单独benchmark无法满足复杂逻辑分析,不同负载情况下性能表现差异
  2. 请求流量构造,不同请求参数覆盖逻辑不同,线上真实流量情况
  3. 压测范围,单机器压测,集群压测
  4. 性能数据采集,单机性能数据,集群性能数据
  5. 分析性能数据,定位性能瓶颈,使用库不规范,高并发场景优化不足
  6. 重点优化项改造,正确性是基础,响应数据diff,线上请求数据录制回放,将第一次测试的数据录制下来,新旧逻辑接口数据diff,优化改造后与第一次进行对比
  7. 优化效果验证,重复压测验证,上线评估优化效果,关注服务监控,逐步放量,收集性能数据
  8. 进一步优化,服务整体链路分析,规范上游服务调用接口,明确场景需求,分析链路,通过业务流程优化提升服务性能
案例二 基础库优化

AB实验SDK的优化

  1. 分析基础库核心逻辑和性能瓶颈,可以用pprof看具体的占比
  2. 设计完善改造方案
  3. 数据按需获取
  4. 数据序列化协议优化
  5. 内部压测验证
  6. 推广业务服务落地验证
案例三 Go语言优化

编译器&运行时优化

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

优点

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

总结

  • 性能调优原则,要依靠数据而不是猜测
  • 性能分析工具 pprof,熟练使用pprof工具排查性能问题并了解其基本原理
  • 性能调优,保证正确性,定位主要瓶颈

总结及建议

  • 总结:这章主要就是讲优化的一些方法,最主要就是要学会使用pprof工具去排查性能问题,进而去解决性能问题,然后就是编程时注释一定要写,变量名一定得规范,这个在项目中是非常重要的
  • 学习建议:这个工具就要多去实操,其中的原理也得弄清楚,这样才能更好的去排查性能问题,进而解决性能问题,可以先将一些开源的项目进行练习,多多练习