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

100 阅读7分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第 3 篇笔记

一、本堂课重点内容

1. 高质量编程

  • 高质量编程的概念
  • golang编码规范
  • golang程序性能优化

2. 性能调优实战

  • 性能调优简介
  • 性能分析工具pprof的使用案例
  • 性能调优实战案例

二、详细知识点介绍:

1. 高质量编程

简介

高质量的代码首先具备正确性,需要考虑完备各种边界条件,对代码中可能出现的异常情况需要有相应的处理,具备可靠性,最后代码需要简洁清晰,注释完备,易读易维护

编程原则
1.  简单性:消除代码中多余的复杂性,比如复杂的逻辑嵌套,以简单清晰的逻辑编写代码,不理解的代
    无法修复改进
2.  可读性:可维护的代码第一步是确保代码的清晰可读

编码规范

  1. 代码格式:使用gofmt工具格式化代码,gofmt是官方提供的工具,能够将代码自动格式化为官方统一风格,goimports能够自动增删依赖声明

  2. 代码注释:注释应当解释代码的作用、代码的实现逻辑、代码实现的原因、代码可能的异常情况

  3. 命名规范

    变量名:简洁优于冗长,缩略词全部大写(但是位于变量开头且不需要导出使用全小写),如果变量使用的地方与变量声明的地方越远,变量名就要带有越多的上下文信息

    包名:只由小写字母组成,不包含大写字母和下划线;简短并包含一定的上下文信息;不与标准库同名;不使用常用变量名作为包名;使用单数而不是复数;在不影响含义的情况下使用缩写

  4. 控制流程:减少嵌套的使用,在异常处理时对出现异常的地方提前return,是一个函数中每个部分也能呢更清晰 5.错误和异常处理:不建议在业务代码中使用panic,因为panic会直接中止程序的进行,业务异常应当通过error向上层抛出,在最上层进行recover的处理;不多在程序启动阶段发生不可逆转的错误,这样的错误完全影响了程序的正常运行,可以用panic方便更快的定位错误;revocer只能在defer函数中使用,嵌套无法生效且只在当前的协程中生效

性能优化建议

  1. 性能评估:使用go语言官方提供的benchmark工具:go test -bench=. -benchmem,执行之后可以得到结果:测试函数名-cpu核数 执行次数 每次执行花费的时间 每次执行申请多大内存 每次执行申请几次内存
  2. slice预分配内存:尽可能在使用make初始化切片时提供容量信息,这样申请内存次数就减少(和Java中ArrayList初始化指定容量一样)
  3. 使用copy代替re-slice:在大切片上面进行小切片会保留原有的大切片内存不释放,造成的过多的内存损耗,如果原切片不再使用,只用切片中的一部分则使用copy的方式代替切片上的切片
  4. map预分配内存:向map中添加元素会触发map的扩容,提前分配好空间可以减少内存拷贝的消耗
  5. 字符串处理:使用strings.Builder这个结构体代替+进行字符串拼接,使用bytes.Buffer相对strings.Builder更快,这两个底层都是byte数组,具有内存扩容策略,不需要每次拼接重新分配内存
  6. atomic包:锁的实现是通过操作系统来实现的,属于系统调用,而atomic是通过硬件实现,效率比锁高,sync.Mutex应该用来保护一段逻辑而不仅仅是一个变量

2. pprof采样过程和原理

  1. CPU:操作系统每10ms向进程发送一次SIGPROF信号,进程每次收到SIGPROF都会记录调用对战,每100ms读取已经记录的调用栈并写入输出流
  2. 堆内存:采样程序通过内存分配器在堆上分配和释放的内存,记录分配/释放的大小和数量,采样率:每分配512kb记录一次;采样时间:从程序运行开始到采样时间;采样指标:alloc_space,alloc_objects,inuse_space,inuse_object
  3. 协程:记录所有用户发起且在运行中的goroutine tuntime.main调用栈信息
  4. 线程:记录程序创建的所有系统线程的信息
  5. 阻塞:采样阻塞操作的次数和耗时,采样率:阻塞耗时超过阈值才会被记录
  6. 锁:采样争抢锁的次数和耗时,采样率:只记录固定比例的所操作

3. 业务服务优化

基本概念

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

流程

  1. 建立服务性能评估手段:单独benchmark无法满足复杂的逻辑分析,且不同请求参数覆盖逻辑不同,需要模拟线上真实流量情况,需要用到压测平台来对服务进行压力测试;采集单机性能数据和集性能数据;得到压测报告
  2. 分析性能数据,定位性能瓶颈:根据压测报告,定位到具体性能瓶颈,如基本库使用方式不当、对高低峰场景的火焰图进行对比查看是否是高并发优化不当
  3. 重点优化项改造:性能优化过程中正确性是基础,可以将线上的请求数据进行记录,在优化使用记录对新的逻辑进行测试
  4. 优化效果验证:在优化结束之后,再次进行压测;集合服务整体的链路进行进一步优化

三、实践练习例子:

在浏览器输入目标程序的地址后面接/debug/pprof查看各个性能指标情况

  1. 通过go tool pprof 项目的地址/profile来排查CPU占用过高的情况
  2. 输入top指令查看占用资源最多的函数,top后面接参数表示查看前几个函数 image.png
  3. 根据结果可知Eat函数占用时间最大,通过list Eat指令(list表示根据指定的正则表达式查找代码行),查找该函数的代码行具体占用情况 image.png
  4. 通过web指令可以生成图形化的流程图,需要下载graphviz并配置环境变量 image.png
  5. 重新启动,这次以heap结尾go tool pprof http://localhost:6060/debug/pprof/heap,排查内存占用情况;同样按照上面的流程可以找到对应的内存占用最高的代码

image.png 6. 再以goroutine结尾,查看协程:go tool pprof http://localhost:6060/debug/pprof/goroutine同样根据top、list、web定位到问题代码处

image.png

  1. 排查锁的争用:go tool pprof http://localhost:6060/debug/pprof/mutex,同样三段操作

image.png 8. 根据block后缀,排查阻塞操作

四、课后个人总结:

  1. 性能调优是必要的,但这也需要建立在系统成型的基础之上,不需要在搭建系统的时候就时刻考虑性能问题,从而投鼠忌器
  2. 在调优过程中要定位到性能瓶颈,而不是在无关紧要的细节上花费大量成本