这是我参与「第五届青训营 」伴学笔记创作活动的第 3 天~
1 高质量编程
1.1 简介
什么是高质量代码,实际是个偏主观的。
标准编写的代码能够达到正确可靠、简洁清晰的目标可称之为高质量代码
- 正确性:是否考虑各种边界条件,错误的调用是否能够处理
- 可靠性:异常情况或者错误的处理策略是否明确,依赖的服务出现异常是否能够处理
- 简洁:逻辑是否简单,后续调整功能或新增功能是否能够快速支持
- 清晰:其他人在阅读理解代码的时候是否能清楚明白,重构或者修改功能是否不会担心出现无法预料的问题
对于不同的语言,对于高质量有通用的原则。 简单性 消除“多余的复杂性",以简单清晰的逻辑编写代码,在实际工程项目中,复杂的程序逻辑会让人害怕重构和优化,因为无法明确预知调整造成的影响范围,难以理解的逻辑,排查问题时也难以定位,不知道如何修复
可读性 可读性很重要,因为代码是写给人看的,而不是机器。在项目不断迭代的过程中,大部分工作是对已有功能的完善或打展,很少会完全下线某个功能,对应的功能代码实际会生存很长时间。已上线的代码在其生命周期内会被不同的人阅读几十上百次
生产力 编程在当前更多是团队合作,因此团队整体的工作效率是非常重要的一方面,为了降低新成员上手项目代码的成本,Go语言甚至通过工具强制统一所有代码格式、编码在整个项目开发链路中的一个节点, 遵循规范,避免常见缺陷的代码能够降低后续联调、测试、验证、上线等各个节点的出现问题的概率,就算出现问题也能快速排查定位。
1.2编码规范
1.2.1 代码格式
首先是推荐使用gofmt自动格式化代码,保证所有的Go代码与官方推荐格式保持-致 而且可以很方便的进行配置,像Goland内置了相关功能,直接开启即可在保存文件的时候自动格式化 另外可以考虑goimports,会对依赖包进行管理,自动增删依赖的包引用,按字母序排序分类,具体可以根 据团队实际情况配置使用! 之所以将格式化放在第一条, 因为这是后续规范的基础,团队合作review其他人的代码时就能体会到这条规 范的作用了
1.2.2 注释
公共符号例如对外提供的接口、变量等等。
注释应该提供代码未表达出的上下文信息
1.2.3命名规范
1.变量
2.函数
Q:什么是包?
由于包为http,因此调用过程为http.Server,因此包中已经包含了函数所需要的上下文信息,所以使用server进行命名更加好。
3.包名
命名核心是降低阅读理解代码的成本,重点考虑上下文信息,设计简洁清晰的名称。
1.2.4控制流程
对于异常情况应该先返回。
1.2.5 错误和异常处理
错误链示例
使用error.ls来判定
error.as取特定错误
panic是更加严重的错误,一般会导致程序的崩溃。
对应panic,使用recover来进行处理
Q:什么是defer函数
复习以下!哪种方式更好?
1.3性能优化建议
1.3.1 slice 切片
在make时初始化切片提供的容量信息
1.3.2 Map
为什么Map的大小可以预知
1.3.3 string
对于字符串的拼接,有如下三种方式。 第一种,直接使用+进行拼接。
第二种,使用builder
第三种,使用stringbuffer
性能分析
builder优于buffer是因为最终返回是将数组转化为字符串类型而非重新申请空间
进一步地,可以通过预设buffer和builder的大小进一步提升性能
1.3.4 空结构体
空结构体的定义如下所示
对于如下代码
1.3.5 atomic包
atomic包具有较好性能的原因
2 性能调优实战
2.1简介
2.2性能分析工具 pprof
2.2.1 pprof使用
下载github.com/wolfogre/go… 代码进行测试。
该源码import了pprof
当服务运行后,服务起来之后,就会多多一条路由,如http://127.0.0.1:6060/debug/pprof,有以下输出
在启动完服务后,使用资源管理器观察内存占用的情况。
如下,访问该链接会得到一个profile文件,go tool pprof相当于在帮助解析难懂的profile文件。
Step 1:
由下面两个图可知,tiger.eat耗时较长。当函数中没有调用其他函数时,Flat等于Cum;当函数中只有其他函数调用时,Flat=0,例如main.main
Step2:
使用ui的形式打开解析的结果(需要提前安装好graphviz)
blog.csdn.net/myRealizati…
在VScode的终端键入如下所示的命令
此时会在浏览器上弹出如下所示的界面,可以使用图形化的方式查看性能分析的结果。
通过-http=:8080参数, 可以开启pprof自带的Web UI,性能指标会以网页的形式呈现。 再次启动pprof工具,注意这时的链接结尾是heap,等待采样完成后,浏览器会被自动打开,展示出熟悉的web视图,同时展示的资源使用从「CPU时间」 变为了「内存占用」 可以明显看到,这里出问题的是*Mouse.Steal0函数, 它占用了1GB内存。在页面顶端的View菜单中 ,我们可以切换不同的视图。
我们再切换到Source视图,可以看到页面上展示出了刚刚看到的四个调用和具体的源码视图。如果觉得内容太多,也可以在顶部的搜索框中输入Steal来使用正则表达式过滤需要的代码。
根据源码我们会发现,在*Mouse.Steal0这 个函数会向固定的Buffer中不断追加1MB内存,直到Buffer达到1GB大小为止, 和我们在Graph视图中发现的情况致。
我们将这里的问题代码注释掉,至此,[炸弹 」已被拔除了两个。
我们打开sample菜单,会发现堆内存实际上提供了四种指标:
在堆内存采样中,默认展示的是inuse_ space视图, 只展示当前持有的内存, 但如果有的内存已经释放,这时inuse采样就不会展示了 。我们切换到alloc_ space指标。 后续分析下alloc的内存问题。
马上就定位到了*Dog.Run0中还藏着-个炸弹,它会每次 申请16MB大小的内存,并且已经累计申请了超过3.5GB内存。在Top视图中还能看到这个函数被内联了。
看看定位到的函数,果然,这个函数在反复申请16MB的内存,但因为是无意义的申请,分配结束之后会马上被GC,所以在inuse采样中并不会体现出来。
我们将这一行问题代码注释掉,继续接下来的排查。至此,内存部分的「炸弹」已经被全部拆除。
step3: Golang是门自带垃圾回收的语言,一般情况 下内存泄露是没那么容易发生的 但是有种例外: goroutine是很容易泄露的,进而会导致内存泄露。所以接下来,我们来看看goroutine的使用情况。
到Source视图搜索Drink,发现函数每次会发起10条无意义的goroutine,每条等待30秒后才退出,导致了goroutine的泄露。
这里为了模拟泄漏场景,只等待了30秒就退出了试想下,如果发起的goroutine没有退出,同时不断有新的goroutine被启动, 对应的内存占用持续增长,CPU调度压力也不断增大,最终进程会被系统Kiil掉
Step4:
Step5:
2.2.2 pprof原理
首先来看CPU。CPU采样会记录所有的调用栈和它们的占用时间。 在采样时,进程会每秒暂停一百次, 每次会记录当前的调用栈信息。汇总之 后,根据调用栈在采样中出现的次数来推断函数的运行时间。 你需要手动地启动和停止采样。每秒100次的暂 停频率也不能更改。 这个定时暂停机制在unix或类unix系统上是依赖信号机制实现的。 每次暂停都会接收到一个信号,通过系统计时器来保证这个信号是固定频 率发送的。
一共有三个相关角色:进程本身、操作系统和写缓冲。
启动采样时,进程向OS注册一个定时器,OS会每隔10ms向进程发送一个SIGPROF信号 ,进程接收到信号后就 会对当前的调用栈进行记录。
与此同时,进程会启动一个写缓冲的goroutine,它会 每隔100ms从进程中读取已经记录的堆栈信息,并写入到输出流。
当采样停止时,进程向OS取消定时器,不再接收信号,写缓冲读取不到新的堆栈时,结束输出。
接下来看看堆内存采样。
提到内存指标的时候说的都是「堆内存」而不是「内存」, 这是因为pprof的内存采样是有局限性的。
内存采样在实现上依赖了内存分配器的记录,所以它只能记录在堆上分配,且会参与GC的内存,一些其他的内存分配,例如调用结束就 会回收的栈内存、- 些更底层使用cgo调用分配的内存,是不会被内存采样
它的采样率是一个大小, 默认每分配512KB内存会采样一 次,采样率是可以在运行开头调整的, 设为1则为每次分配都会记录。
与CPU和goroutine都不同的是,内存的采样是一个持续的过程, 它会记录从程序运行起的所有分 配或释放的内存大小和对象数量,并在采样时遍历这些结果进行汇总。
还记得刚才的例子中,堆内存采样的四种指标吗? llo的两项指标是从程序运行开始的累计指标,而inuse的两项指标是通过累计分配减去累计释放得到的程序当前持有的指标。你也可以通过比较两次lloc的差值
来得到某一段时间程序分配的内存大小和数量]
接下来我们来看看goroutine和系统线程的采样。这两个采祥指标在概念上和实现上都比较相似,所以在这里进行对比。 Goroutie采样会记录所有用户发起,也就是入口不是runtime开头的goroutine,以及main函数所在goroutine的信息和创建这些goroutine的调用栈; 他们在实现上非常的相似,都是会在STW之后,遍历所有goroutine/所有线程的列表(图中的m就是 GMP模型中的m,在golang中和线程 对应)并输出堆栈,最后Start The World继续运行。这个采样是立刻触发 的全量记录,你可以通过比较两个时间点的差值来得到某一时间段的指标。
最后是阻塞和锁竞争这两种采样 这两个指标在流程和原理.上也非常相似 这两个采样记录的都是对应操作发生的调用栈、次数和耗时,不过这两个指标的采样率含义并不相同。 阻塞操作的采样率是一个「阈值」,消耗超过阈值时间的阻塞操作才 会被记录,1为每次操作都会记录。记得炸弹程序的main代码吗?里面设置了rate=1 锁竞争的采样率是一个「比例」, 运行时会通过随机数来只记录固定比例的锁操作,1为每次操作都会记录。 它们在实现上也是基本相同的。都是一个主动上报的过程。 在阻塞操作或锁操作发生时,会计算出消耗的时间,连同调用栈-起主动上报给采样器,采样器会根据采样率可能会丢弃些记录。 在采样时,采样器会遍历已经记录的信息,统计出具体操作的次数、调用栈和总耗时。和堆内存一样, 你可以对比两个时间点的差 值计算出段时间内的操作指标。
2.3性能优化案例
在实际工作中,当服务规模比较小的时候,可能不会触发很多性能问题,同时性能优化带来的效果也不明显,很难体会到性能调优带来的收益 而当业务量逐渐增大,比如一一个服务使用了几千台机器的时候,性能优化一个百分点,就能节省数百台机器,成本降低是非常可观的 接下来我们来了解下工程中进行性能调优的实际案例 程序从不同的应用层次上看,可以分为业务服务、基础库和Go语言本身三类,对应优化的适用范围也是越来越广 业务服务-般指直 接提供功能的程序,比如专门处理用户评论操作的程序 基础库一般指提供通用功能的程序,主要是针对业务服务提供功能,比如监控组件,负责收集业务服务的运行指标 另外还有对Go语言本身进行的优化项
2.3.1 业务服务优化
不同服务依赖的工具包,比如监控等等。
那么接下来就来看一下业务服务优化的主要流程,主要分四步,这些流程也是性能调优相对通用的流程,可以适用其他场景。
和上面评估代码优化效果的benchmarkI具类似,对于服务的性能也需要一个评估手 段和标准
优化的核心是发现服务性能的瓶颈,这里主要也是用pprof采样性能数据,分析服务的表现
发现瓶颈后需要进行服务改造,重构代码,使用更高效的组件等。
最后一步是优化效果验证,通过压测对比和正确性验证之后,服务可以上线进行实际收益评估
整体的流程可以循环并行执行,每个优化点可能不同,可以分别评估验证
之所以不用benchmark是因为实际服务逻辑比较复杂,希望从更高的层面分析服务的性能问题,同时机器在不同负载下的性能表现也会不同,右图是负载和单核
qps的对应数据。
另外因为逻辑复杂,不同的请求参数会走不同的处理逻辑,对应的性能表现也不相同,需要尽量模拟线上真实情况,分析真正的性能瓶颈。
压测会录制线上的请求流量,通过控制回放速度来对服务 进行测试,测试范围可以是单个实例,也可以是整个集群,同样性能采集也会区分单机和集群。
有了服务优化前的性能报告和一些性能采样数据, 我们可以进行性能瓶颈分析了 业务服务常见的性能问题可能是使用基础组件不规范 比如这里通过火焰图看出json的解析部分占用了较多的CPU资源,那么我们就能定位到具体的逻辑代码,是在每次使用配置时都会进行json解析,拿到配置项,实 际组件内部提供了缓存机制,只有数据变更的时候才需要重新解析json。 还有是类似日志使用不规范,一部分 是调试日志发布到线上,- 部分是线上服务在不同的调用链路上数据有差别,测试场景日志量还好,但是到了真实线上全量 场景,会导致日志量增加,影响性能
定位到性能瓶颈后,我们也有了对应的修复手段,但是修改完后能直接发布上线吗? 性能优化的前提是保证正确性,所以在变动较大的性能优化上线之前,还需要进行正确性验证,因为线上的场景和流程太多,所以要借助自动化手段来保证优化 后程序的正确性 同样是线上请求的录制,不过这里不仅包含请求参数录制,还会录制线上的返回内容,重放时对比线上的返回内容和优化后服务的返回内容进行正确性验证 比如图中作者信息相关的字段值在优化有有变化,需要进一步排查原因。
改造完成后,可以进行优化效果验证了。 验证分两部分,首先依然是用同样的数据对优化后的服务进行压测,可以看到现在的数据比优化前好很多,能够支持更多的qps。 正式上线的时候会逐步放量,记录真正的优化效果。 同时压测并不能保证和线上表现完全一致, 有时还要通过线上的表现再进行分析改进,是个长期的过程。
以上的内容是针对单个服务的优化过程,从史向的视用有,性能是不是还有优化空间? 在熟悉服务的整体部署情况后,可以针对具体的接口链路进行分析调优。 比如Service A调用Service B是否存在重复调用的情况,调用Service B服务时,是否更小的结果数据集就能满足需求,接口是否一定要实时数据,能否在Service A 层进行缓存,减轻调用压力? 这种优化只使用与特定业务场景,适用范围窄,不过能更合理的利用资源。
2.3.2 基础库优化
适用范围更广的就是基础库的优化 比如在实际的业务服务中,为了评估某些功能上线后的效果,经常需要进行AB实验,看看不同策略对核心指标的影响,所以公司内部多数服务都会使用AB实验的 SDK,如果能优化AB组件库的性能,所有用到的服务都会有性能提升。 类似业务服务的优化流程,也会先统计下各个服务中AB组件的资源占用情况,看看AB组件的哪些逻辑更耗费资源,提取公共问题进行重点优化。 图中看到有部分性能耗费在序列化上,因为AB相关的数据量较大,所以在制定优化方案时会考虑优化数据序列化协议,同时进行按需加载,只处理服务需要的数。 完成改造和内部压测验证后,会逐步选择线上服务进行试点放量,发现潜在的正确性和使用上的问题,不断迭代后推广到更多服务。