Go 高质量编程与性能调优

Entropy Tree Lv4

本文来源于第五届字节跳动青训营活动,已收录到golang高质量编程与性能调优 | 青训营笔记 - 掘金 (juejin.cn) ,主要记录了对golang高质量编程与性能调优的学习

高质量编程与性能调优实战

主要内容

  • 如何编写更简洁清晰的代码
  • 常用Go语言程序的优化手段
  • 熟悉Go程序性能分析工具
  • 了解工程中性能优化的原则和流程

1.高质量编程

1.1 简介

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

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

  • 可靠性:异常情况或者错误处理的策略明确,依赖的服务出现异常能够及时处理。

  • 简单:逻辑简单,后续调整功能或新增功能能够快速支持。

  • 清晰:其他人在阅读代码时容易理解,重构或者修改功能不会出现无法预料的问题。

编程的原则

简单性

消除“多余的复杂性”,以简单清晰的逻辑编写代码。复杂的逻辑难以重构和优化,无法明确预知其调整造成的影响范围,也难以在排查问题时定位。

可读性

从后期来看,大部分工作是对已有功能的完善和扩展,对应功能的代码会存在很长时间。维护是一个项目最漫长的周期,好的可读性可以降低维护的时间成本。

生产力

Go语言有特定的代码格式限制,甚至有专门的工具强制统一所有代码格式。遵循代码规范,能够避免常见的缺陷代码,降低后续联调、测试、验证、上线等各个节点出现问题的概率以及快速排查定位问题。

1.2 编码规范

1.2.1 代码格式

推荐使用gofmt自动格式化代码,gofmt是Go语言官方提供的工具,能够自动格式化GO语言代码为官方统一风格,常见的IDE都支持配置。

另外的,goimports会对依赖包进行管理,自动增删依赖包引用,按字母排序分类。

1.2.2 注释

注释有以下四种使用方式

  • 解释代码作用。

    适合说明公共符号,比如对外提供的函数用注释描述它的功能和用途,除非函数的功能简单而明显时,才可省略注释。

    对于显而易见的内容不需要注释。

  • 解释代码如何实现

    适合说明逻辑实现过程,解释复杂的不明显的逻辑。

    不要解释显而易见的流程,避免增加冗余信息和造成误解。

  • 解释代码实现的原因

    适合解释代码的外部因素,为什么需要实现这个代码,提供上下文信息,说明这段代码在上下文中的意义。

  • 解释代码出错的可能情况

    适合解释代码的限制条件,一些潜在的限制条件或者无法处理的情况,例如性能隐患,输入的限制条件,可能出现的错误情况等。使阅读者在不需要了解代码实现细节的情况下弄清限制条件。

公共符号始终要注释

  • 包中声明的每个公共符号:变量、常量函数以及结构都需要添加注释
  • 任何既不明显又不简短的公共功能必须予以注释
  • 无论长度或复杂程度如何,对库中的函数都必须进行注释
  • 例外情况,不需要注释实现接口的方法。

更多规范的注释可以参考golang的官方仓库源码golang/go

小结

  • 代码本身应该是最好的注释
  • 注释应该提供代码未表达出的上下文信息

1.2.3 命名规范

命名是代码编写中最常见的规范。

variable

  • 简洁胜于冗长

  • 缩略词全大写,但当其位于变量开头且不需要对外提供时,使用全小写

    例如,使用SeverHTTP而不是ServerHttp。使用XMLHTTPRequest或者xmlHTTPRequest。

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

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

  • 几乎不影响理解的变量名变动,选择最简介的名称即可。

  • 具有特定含义的变量名变动,选择意义最贴切的名称即可。

function

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

    例如,使用time.Now()而不是time.NowTime()。

  • 函数名尽量简短

  • 当名为foo的包某个函数返回类型为Foo时,可以省略类型信息避免歧义

  • 当名为foo的包某个函数返回类型为T时(T不是Foo),可以在函数名中加入类型信息

    例如,parseInt

package

  • 只由小写字母组成,不能包含大写字母和下划线等字符
  • 简短并包含一定的上下文信息。例如schema
  • 不要与标准库同名。例如不要使用sync或者strings

以标准库包名为例

  • 不使用常用的变量名作为包名。例如使用bufio而不是buf
  • 使用单数而不是复数。例如使用encoding而不是encodings
  • 谨慎地使用缩写。例如使用fmt在不破坏上下文的情况下比format更加简短

小结

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

1.2.4 控制流程

流程控制经常用到的就是if else这种条件控制语句。

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

一个简单的if else,如果两个分支都包含return语句,则可以去除冗余的else,方便后续维护。else一般就是表示正常流程,如果需要在正常流程中新增判断逻辑,则去除else可以避免分支嵌套。

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

  • 优先处理错误情况或特殊情况,尽早返回或继续循环来减少嵌套。

    例如,使用err != nil而不是err == nil就是为了优先处理错误情况。

小结

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

1.2.5 错误和异常处理

简单错误

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

错误的Wrap和Unwarp

  • 错误的Wrap实际上是提供了一个error嵌套另一个error的能力,从而形成一个error的跟踪链
  • 在fmt.Errorf中使用%w占位符来将一个错误关联至错误跟踪链中

错误判定

  • 判断一个错误是否为特定错误,使用errors.Is
  • 不同于使用==,使用该方法可以判断错误链上的所有错误中是否含有特定的错误
  • 在错误链上获取特定种类的错误,使用errors.As 。与errors.Is不同的是As会取出调用链中指定类型的错误,并将错误赋值给定义好的变量,方便后续处理

panic

panic比错误更加严重,它的出现表示程序无法正常工作。

  • 不建议在业务代码中使用panic
  • panic发生后会向上传播至调用栈顶,如果当前goroutine中所有deferred函数都不包含recover就会造成整个程序崩溃
  • 若问题可以被屏蔽或解决,建议使用error代替panic
  • 特殊地,当程序启动阶段发生不可逆的错误时,可以在init或main函数中使用panic

recover

panic的产生并不只在程序运行阶段。如果是引入其他库时产生的bug而导致的panic就需要使用recover。

注意recover的生效条件

  • recover只能在被defer的函数中使用
  • 在嵌套中无法生效
  • 只在当前goroutine生效
  • defer的语句是先进后出

如果需要更多的上下文信息,可以在recover后用log记录当前调用的栈,方便分析定位。

小结

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

1.3 性能优化建议

简介

  • 性能优化的前提是满足正确可靠、简洁清晰等质量因素
  • 性能优化是综合评估,时间效率和空间效率可能在某些情况下不能兼得
  • 针对Go语言特性来介绍Go相关的性能优化建议

1.3.1 Benchmark

如何使用

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

通过以下命令进行性能测试

1
go test -bench=. -benchmem

结果说明

  • 第一个参数如BenchmarkFib10-8对应被测试函数的名称,-8表示GOMAXPROCS的值为8
  • 第二个参数如1855870表示一共执行1855870次,即b.N的值
  • 第三个参数如602.5ns/op表示每次执行花费602.5ns
  • 第四个参数如0B/op表示每次执行时申请的内存空间大小
  • 第五个参数如0allocs/op表示每次执行时申请内存的次数

1.3.2 Slice

slice预分配内存

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

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

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

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

    以切片的append为例,append时有两种场景:

    当append之后的长度小于等于容量,将会直接利用原底层数组剩余的空间。

    当append之后的长度大于容量,则会分配一块更大的区域来容纳新的底层数组。

因此,为了避免内存发生拷贝,如果能够明确最终的切片的大小,预先设置容量的值能够避免额外的内存分配,获得更好的性能。

大内存未释放问题

原切片由大量的元素构成,在原切片的基础上创建切片只使用了很小一段,但底层数组在内存中仍占据着大量空间,无法释放。

  • 在已有切片的基础上创建切片,不会创建新的底层数组
  • 可以使用copy代替re-slice,因为通过copy会指向一个新的底层数组,原来的底层数组不再被引用之后,内存就会被回收。

1.3.3 Map

map预分配内存

原理

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

1.3.4 字符串处理

使用strings.Builder

  • 常见的字符串拼接方式就是使用+号直接拼接或者使用strings.Builder的方法拼接以及bytes.Buffer的方法拼接

  • 使用+号拼接性能最差,strings.Builder和bytes.Buffer性能相近,strings.Builder更快。

    当使用+拼接两个字符串时,生成一个新的字符串,那么就需要开辟一段新的空间,新空间的大小是原来两个字符串的大小之和。拼接第三个字符串时,再开辟一段新空间,新空间大小是三个字符串大小之和,以此类推。

  • 分析

    • 字符串在Go语言中是不可变类型,占用内存大小是固定的
    • 使用+每次都会重新分配内存
    • strings.Builder和bytes.Buffer底层都是[]byte数组
    • Builder和Buffer的内存扩容策略,不需要每次拼接重新分配内存
  • Builder比Buffer快的原因

    • bytes.Buffer转化为字符串时重新申请了一块空间
    • strings.Builder直接将底层的[]byte转换成了字符串类型返回
  • 在已知字符串长度的情况下,进一步提升Builder的拼接性能,使用预分配减少分配次数。优化后的Builder只需要一次内存分配,而优化后的Buffer有两次。

1.3.5 空结构体

使用空结构体节省内存

  • 空结构体struct{}实例不占据任何内存空间

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

    • 节省资源
    • 空结构体本身具备很强的语义,不需要任何值就能作为占位符
  • Set方法的实现,可以考虑用map来代替

  • 对于这个场景,只需要用到map的键,不需要值

  • 即使是将map的值设置为bool类型,也会多占据1个字节空

    一个开源实现golang-set/threadunsafe.go

1.3.6 atomic包

多线程编程场景下性能优化,保证线程安全。

如何使用atomic包

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

小结

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

2.性能调优实战

2.1 简介

性能调优原则

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

2.2 性能分析工具 pprof

说明

如果想知道应用在什么地方耗费了多少CPU、Memory。对于go程序有一个很方便的工具——pprof

2.2.1 功能简介

pprof是用于可视化和分析性能分析数据的工具

  • 分析-Profile:网页、可视化终端
  • 工具-Tool:runtime/pprof、net/http/pprof
  • 采样-Sample:CPU、堆内存-Heap、协程-Goroutine、锁-Mutex、阻塞-Block、线程创建-ThreadCreate
  • 展示-View:Top、调用图-Graph、火焰图-FlameGraph、Peek、源码-Source、反汇编-Disassemble

2.2.2 排查实战

搭建pprof实践项目

  • 项目地址wolfogre/go-pprof-practice: go pprof practice.

  • 该项目提前埋入了炸弹代码,能产生可观测的性能问题。windows可用任务管理器查看内存占用情况,启动项目前确保内存足够。

  • 项目中引入了net/http/pprof包,在启动项目后可以通过浏览器访问pprof工具

    main函数部分代码说明

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    runtime.GOMAXPROCS(1)              //限制CPU使用数
    runtime.SetMutexProfileFraction(1) //开启锁调用跟踪
    runtime.SetBlockProfileRate(1) //开启阻塞调用跟踪

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

浏览器查看指标

在项目启动成功后,访问http://localhost:6060/debug/pprof,可以看到pprof的计数页面。

页面上展示了可用的程序运行采样数据,分别是

  • allocs:内存分配情况
  • blocks:阻塞操作情况
  • cmdline:程序启动命令及其参数
  • goroutine:当前所有协程的堆栈信息
  • heap:堆上内存使用情况
  • mutex:锁竞争操作情况
  • profile:CPU占用情况
  • threadcreate:当前所有常见的系统线程的堆栈信息
  • trace:程序运行跟踪情况

cmdline可以显式运行进程的命令,找到问题来源的程序。threadcreate比较复杂,不透明。trace需要配合另外的工具解析,这里暂不深入。

炸弹主要在CPU、堆内存、goroutine、锁竞争和阻塞操作上,可以通过pprof工具分析

CPU

使用操作系统自带的工具查看CPU占用,发现该项目程序的异常占用。

pprof的采样结果是将一段时间内的信息汇总输出到文件中,所以需要获取这个profile文件。可以直接使用暴露的接口链接下载文件后使用,也可以直接用pprof工具连接这个接口下载需要的数据。

使用go tool pprof + 采样链接的命令来启动采样,例如

1
go tool pprof "http://localhost:6060/debug/pprof/profile?seconds=10"

链接就是刚刚炸弹程序暴露出来的接口,链接结尾的profile代表采样的对象是CPU使用。如果在浏览器里直接打开链接会启动一个60秒的采样并在采样结束后下载文件到本地。这里加上seconds=10的参数,指定采样时间为10秒。文件下载保存的路径会在pprof终端展示。

pprof终端命令

  • top命令用于查看占用资源最多的函数。top有以下参数指标

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

    • flat%:flat占CPU总时间的比例

    • sum%:上面每一行flat%的总和

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

    • cum%:cum占CPU总时间的比例

    默认展示资源占用最高的10个函数,如果只需要查看占用最高的N个函数,在top命令后加上一个数字N即可。

    flat=cum的函数说明该函数没有调用其他函数

    flat=0的函数说明该函数只有其他函数的调用


  • list命令会根据给出的正则表达式查找代码,并按行展示出每一行的占用

    例如,在pprof终端输入list Eat,就能查看Eat函数每行的占用情况。这样就能定位到具体的代码。


  • web命令可以生成一张调用关系图,默认通过浏览器打开。该命令需要提前安装Graphviz并配置环境变量才能使用。官网下载Download | Graphviz

    图中除了每个节点的资源占用以外,还会将它们的调用关系描述出来。资源占用最高的函数会比较明显地展示出来。

退出pprof终端,输入q回车即可。将炸弹程序直接注释。

Heap-堆内存

注释问题代码后,重新启动项目。用系统工具(windows是任务管理器)查看CPU占用,可以发现进程的占用已经降下来了。但还有内存占用问题,接下来排查内存问题。

上面排查CPU问题时使用的是pprof终端,这里使用另一种展示方式,通过-http=:8080参数,开启pprof自带的web UI,以网页的形式来呈现性能指标。注意链接结尾是heap

1
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/heap"

通过上面的命令启动pprof工具后,在采样完成后会自动打开浏览器,展示出web视图,同时展示的资源使用从CPU时间变成了内存占用。

通过页面顶端的View菜单可以切换不同的视图,初始默认是Graph视图。Top视图就类似于终端top命令的展示效果,Source视图就类似于终端list命令的展示效果。通过这些视图可以快速定位问题,注释掉问题代码,拆除第二个炸弹。

重新运行项目,查看内存占用,发现内存占用也大幅下降。但实际上前面解决的内存问题只是堆内存采样的四种指标之一的inuse_space。四种指标如下

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

默认展示视图就是inuse_space视图,只展示当前持有的内存,但对于已经释放的内存,inuse采样不会进行展示。通过页面顶端的Sample菜单切换到alloc_space指标,分析alloc的内存问题。定位到问题代码,注释掉。至此,内存部分炸弹已经被全部清理。

goroutine协程

Golang是一门自带垃圾回收的语言,一般情况下内存泄露是没那么容易发生的。但是有一种例外,goroutine是很容易泄露的,进而导致内存泄露。接下来运行项目,分析goroutine使用情况。

1
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/goroutine"

goroutine使用默认视图不方便分析,可以切换到Flame Graph火焰图更加直观。

火焰图的展示

  • 由上到下表示调用顺序,展示了各个函数调用之间的层级关系
  • 每一行代表一个函数,条形越长代表占用的资源越多
  • 火焰图是动态的,支持点击块进行分析

火焰图是非常常用的性能分析工具,在程序逻辑复杂的情况下很有用。

切换到Source视图,在该视图下搜索火焰图中占用最高的函数名称。定位到问题代码,注释掉。重新启动访问pprof可以发现goroutine已下降到正常水平。

mutex-锁

启动pprof工具

1
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/mutex"

观察web视图,分析锁操作,切换到Source视图定位到具体代码,注释掉。

block-阻塞

在程序中,除了锁的竞争会导致阻塞以外,还有很多逻辑(例如读取一个channel)也会导致阻塞。

1
go tool pprof -http=:8080 "http://localhost:6060/debug/pprof/block"

同理在Graph视图中定位到问题函数后,切换到Source视图定位到问题代码,注释掉。

但实际上,该示例项目在之前的计数页面上存在两个阻塞,上面只解决了其中一个阻塞。只展示了一个阻塞,是因为另一个阻塞操作的节点总用时小于总时长的千分之五而被省略掉。这是pprof的过滤策略,能够更加有效地突出重点问题信息,而省略相对没有问题的信息。如果没有过滤策略的话,对于一个复杂的程序,大量无关紧要的内容都会展示出来,不利于定位问题。

第二个阻塞操作,尽管不会在pprof工具中展示出来,但也会被pprof记录下来,可以通过暴露出来的接口地址http://localhost:6060/debug/pprof中的block链接直接访问。通过访问可以得知这个阻塞操作是符合预期的正常操作。

小结

  • 五种使用pprof采集的常用性能指标: CPU、堆内存、Goroutine、 锁竞争和阻塞;
  • 两种展示方式:交互式终端和网页;
  • 四种视图: Top、 Graph、 源码和火焰图。
  • pprof除了http的获取方式之外,也可以直接在运行时调用runtime/pprof包将指标数据输出到本地文件。
  • 视图中还有一个更底层的「反汇编」视图。感兴趣的话可自行尝试。

2.2.3 pprof的采样过程和原理

CPU

  • 采样对象:所有函数调用栈和它们的占用时间
  • 采样率:100次/秒,固定值
  • 采样时间:从手动启动到手动结束

这个定时机制在unix或类unix系统上是依赖信号机制实现的。每次暂停都会接收到一个信号,通过系统计时器来保证这个信息是固定频率发送的。

开始采样——>设定信号处理函数——>开启定时器

停止采样——>取消信号处理函数——>关闭定时器

一共有三个相关角色:进程本身、操作系统和写缓冲。启动采样时,进程向OS注册一个定时器

  • 操作系统每10ms向进程发送一次SIGPROF信号

  • 进程每次接收到SIGPROF信号后就会对当前的调用堆栈进行记录

  • 与此同时,进程会启动一个写缓冲的goroutine,每隔100ms从进程中读取已经记录的堆栈信息,并写入到输出流。

当采样停止时,进程向OS取消定时器,不再接收信号,写缓冲读取不到新的堆栈时,结束输出。

Heap-堆内存

pprof的内存采样是有局限性的,内存采样在实现上依赖了内存分配器的记录,所以只能记录在堆上分配且参与GC的内存,一些其他的内存分配,例如调用结束就会回收的栈内存、一些更底层使用cgo调用分配的内存,是不会被内存采样记录的。

  • 采样程序通过内存分配器记录在堆上分配和释放的内存,记录分配和释放的大小和数量
  • 采样率:每分配512KB记录一次,可在运行开头修改,设置为1则表示每次分配都会记录
  • 采样时间:从程序运行开始到获取采样结果时
  • 采样指标:alloc_space, alloc_objects, inuse_space, inuse_objects。
  • 计算方式:inuse = alloc - free

Goroutine-协程和ThreadCreate-线程创建

goroutine和threadcreate这两个采样指标在概念上和实现上都比较相似。

  • Goroutine

    记录所有用户发起且在运行中的goroutine(即入口非runtime开头的)以及runtime.main函数中的调用栈信息

    Stop The World——>遍历allg切片——>输出创建g的堆栈——>Start The World

  • ThreadCreate

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

    Stop The World——>遍历allm切片——>输出创建m的堆栈——>Start The World

  • 它们在实现上非常相似,都是在Stop The World之后遍历所有goroutine/所有线程的列表(上面的m就是 GMP模型中的m,在golang中和线程一一对应)并输出堆栈,最后Start The World继续运行。这个采样是立刻触发的全量记录,可以通过比较两个时间点的差值来得到某一时间段的指标。

Block-阻塞和Mutex-锁

阻塞和锁竞争这两种采样指标在流程和原理上也非常相似,这两个采样记录的都是对应操作发生的调用栈、次数和耗时,不过这两个指标的采样率含义并不相同。

  • 阻塞操作
    • 采样阻塞操作的次数和耗时
    • 采样率:阻塞耗时超过阈值时间的阻塞操作才会被记录,设置运行参数为1表示每次操作都会记录。
  • 锁竞争
    • 采样争抢锁的次数和耗时
    • 采样率:只记录固定比例的锁操作,设置运行参数为1表示每次加锁都会记录。
  • 它们在实现上也是基本相同的,都是一个「主动上报」的过程
    • 在阻塞操作或锁操作发生时,会计算出消耗的时间,连同调用栈一起主动上报给采样器,采样器会根据采样率可能会丢弃一些记录。 例如时间未达阈值或者比例未命中。
    • 在采样时,采样器会遍历已经记录的信息,统计出具体操作的次数、调用栈和总耗时。和堆内存一样, 可以对比两个时间点的差值计算出这段时间内的操作指标。

小结

  • 掌握常用pprof工具功能
  • 灵活运用pprof工具分析解决性能问题
  • 了解pprof的采样过程和工作原理

2.3 性能调优案例

简介

介绍实际业务服务性能优化的案例。对逻辑相对复杂的程序进行性能调优。

  • 业务服务优化

    业务服务一般指直接提供功能的服务,比如用户评论系统

  • 基础库优化

    基础库一般指提供通用功能的程序,主要是针对业务服务提供功能,比如监控组件,用于收集业务服务的运行指标

  • Go语言优化

    对Go语言本身进行的优化项

2.3.1 业务服务优化

基本概念

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

流程

  • 建立服务性能评估手段。
  • 分析性能数据,定位性能瓶颈。用pprof采样性能数据、分析服务。
  • 重点优化项改造。重构代码、使用更高效的组件等。
  • 优化效果验证。通过压测对比和正确性验证之后,上线服务进行实际收益评估。

建立服务性能评估手段

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

最后表现为一个服务的性能指标分析报告

分析性能数据,定位性能瓶颈

常见性能问题

  • 使用库不规范

    • 基础组件不规范,一般是代码编写逻辑问题,比如提供了缓存机制,但是代码中没有开启。
    • 日志使用不规范,一般是线上服务环境导致某一调用链路数据量剧增,日志量随之剧增,影响性能。
  • 高并发场景优化不足

    例如同步请求造成的性能瓶颈,影响到了业务逻辑处理,后续可改造成异步请求提升性能。

重点优化项改造

  • 性能优化的前提是保证正确性,在变动较大的性能优化上线之前,还需要进行正确性验证。由于线上的场景和流程太多,所以一般会借助自动化手段来保证优化后程序的正确性。

  • 响应数据diff

    • 线上请求数据录制回放

      不仅包含请求参数录制,还有线上的返回内容录制

    • 新旧逻辑接口数据diff

      重放时对比线上的返回内容和优化后服务的返回内容进行正确性验证

优化效果验证

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

压测并不能保证和线上表现完全一致,有时还要通过线上的表现再进行分析改进,是个长期的过程。

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

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

2.3.2 基础库优化

AB实验SDK的优化

A/B测试(也称为分割测试桶测试)是一种将网页或应用程序的两个版本相互比较以确定哪个版本的性能更好的方法。AB测试本质上是一个实验,其中页面的两个或多个变体随机显示给用户,统计分析确定哪个变体对于给定的转换目标(指标如CTR)效果更好。

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

2.3.3 Go语言优化

编译器和运行时优化

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

优点

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

2.4 总结

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

参考资料

Add a test - The Go Programming Language

testing package - testing - Go Packages

切片(slice)性能及陷阱 | Go 语言高性能编程 | 极客兔兔

什么是A/B test?有哪些流程?有什么用? - 腾讯云开发者社区-腾讯云

  • 标题: Go 高质量编程与性能调优
  • 作者: Entropy Tree
  • 创建于 : 2023-01-28 23:23:13
  • 更新于 : 2023-04-01 07:55:52
  • 链接: https://www.entropy-tree.top/2023/01/28/golang-day3/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论