高德 服务器(高德Go生态的服务稳定性建设|性能优化的实战总结)

目前go语言不仅在阿里集团内部,在整个互联网行业内也越来越流行,本文把高德过去go服务开发中的性能调优经验进行总结和沉淀,希望能为正在使用go语言的同学在性能优化方面带来一些参考价值。

作者 | 阳迪、联想、君清

来源 | 阿里开发者公众号

前言

go语言凭借着优秀的性能,简洁的编码风格,极易使用的协程等优点,逐渐在各大互联网公司中流行起来。而高德业务使用go语言已经有3年时间了,随着高德业务的发展,go语言生态也日趋完善,今后会有越来越多新的go服务出现。在任何时候,保障服务的稳定性都是首要的,go服务也不例外,而性能优化作为保障服务稳定性,降本增效的重要手段之一,在高德go服务日益普及的当下显得愈发移动大流量卡重要。此时此刻,我们将过去go服务开发中的性能调优经验进行总结和沉淀,为您呈上这篇精心准备的go性能调优指南。

通过本文您将收获以下内容:

从理论的角度,和你一起捋清性能优化的思路,制定最合适的优化方案。推荐几款go语言性能分析利器,与你一起在性能优化的路上披荆斩棘。总结归纳了众多go语言中常用的性能优化小技巧,总有一个你能用上。基于高德go服务百万级QPS实践,分享几个性能优化实战案例,让性能优化不再是纸上谈兵。

1. 性能调优-理论篇

1.1 衡量指标

优化的第一步是先衡量一个应用性能的好坏,性能良好的应用自然不必费心优化,性能较差的应用,则需要从多个方面来考察,找到木桶里的短板,才能对症下药。那么移动大流量卡如何衡量一个应用的性能好坏呢?最主要的还是通过观察应用对核心资源的占用情况以及应用的稳定性指标来衡量。所谓核心资源,就是相对稀缺的,并且可能会导致应用无法正常运行的资源,常见的核心资源如下:

cpu:对于偏计算型的应用,cpu往往是影响性能好坏的关键,如果代码中存在无限循环,或是频繁的线程上下文切换,亦或是糟糕的垃圾回收策略,都将导致cpu被大量占用,使得应用程序无法获取到足够的cpu资源,从而响应缓慢,性能变差。内存:内存的读写速度非常快,往往不是性能的瓶颈,但是内存相对来说容量有限且价格昂贵,如果应用大量分配内存而不及时回收,就会造成内存溢出或泄漏,应用无法分配新的内存,便无法正常运行,这将移动大流量卡导致很严重的事故。带宽:对于偏网络I/O型的应用,例如网关服务,带宽的大小也决定了应用的性能好坏,如果带宽太小,当系统遇到大量并发请求时,带宽不够用,网络延迟就会变高,这个虽然对服务端可能无感知,但是对客户端则是影响甚大。磁盘:相对内存来说,磁盘价格低廉,容量很大,但是读写速度较慢,如果应用频繁的进行磁盘I/O,那性能可想而知也不会太好。

以上这些都是系统资源层面用于衡量性能的指标,除此之外还有应用本身的稳定性指标:

异常率:也叫错误率,一般分两种,执行超时和应用panic。panic会导致应用不可用,虽然服务通常都会配置相应的重启机制,确保偶然的应用挂掉后能重启再次提供服务,但是经常性的pani移动大流量卡c,会导致应用频繁的重启,减少了应用正常提供服务的时间,整体性能也就变差了。异常率是非常重要的指标,服务的稳定和可用是一切的前提,如果服务都不可用了,还谈何性能优化。响应时间(RT):包括平均响应时间,百分位(top percentile)响应时间。响应时间是指应用从收到请求到返回结果后的耗时,反应的是应用处理请求的快慢。通常平均响应时间无法反应服务的整体响应情况,响应慢的请求会被响应快的请求平均掉,而响应慢的请求往往会给用户带来糟糕的体验,即所谓的长尾请求,所以我们需要百分位响应时间,例如tp99响应时间,即99%的请求都会在这个时间内返回。吞吐量:主要指应用在一定时间内处理请求/事务的数量移动大流量卡,反映的是应用的负载能力。我们当然希望在应用稳定的情况下,能承接的流量越大越好,主要指标包括QPS(每秒处理请求数)和QPM(每分钟处理请求数)。

1.2 制定优化方案

明确了性能指标以后,我们就可以评估一个应用的性能好坏,同时也能发现其中的短板并对其进行优化。但是做性能优化,有几个点需要提前注意:

第一,不要反向优化。比如我们的应用整体占用内存资源较少,但是rt偏高,那我们就针对rt做优化,优化完后,rt下降了30%,但是cpu使用率上升了50%,导致一台机器负载能力下降30%,这便是反向优化。性能优化要从整体考虑,尽量在优化一个方面时,不影响其他方面,或是其他方面略微下降。

第二,不要过度优化。如移动大流量卡果应用性能已经很好了,优化的空间很小,比如rt的tp99在2ms内,继续尝试优化可能投入产出比就很低了,不如将这些精力放在其他需要优化的地方上。

由此可见,在优化之前,明确想要优化的指标,并制定合理的优化方案是很重要的。

常见的优化方案有以下几种:

优化代码

有经验的程序员在编写代码时,会时刻注意减少代码中不必要的性能消耗,比如使用strconv而不是fmt.Sprint进行数字到字符串的转化,在初始化map或slice时指定合理的容量以减少内存分配等。良好的编程习惯不仅能使应用性能良好,同时也能减少故障发生的几率。总结下来,常用的代码优化方向有以下几种:

提高复用性,将通用的代码抽象出来,减少重复开发移动大流量卡池化,对象可以池化,减少内存分配;协程可以池化,避免无限制创建协程打满内存。并行化,在合理创建协程数量的前提下,把互不依赖的部分并行处理,减少整体的耗时。异步化,把不需要关心实时结果的请求,用异步的方式处理,不用一直等待结果返回。算法优化,使用时间复杂度更低的算法。使用设计模式

设计模式是对代码组织形式的抽象和总结,代码的结构对应用的性能有着重要的影响,结构清晰,层次分明的代码不仅可读性好,扩展性高,还能避免许多潜在的性能问题,帮助开发人员快速找到性能瓶颈,进行专项优化,为服务的稳定性提供保障。常见的对性能有所提升的设计模式例如单例模式,我们可以在应用启动时将需要的外部依赖服务用单例模式先初始移动大流量卡化,避免创建太多重复的连接。

空间换时间或时间换空间

在优化的前期,可能一个小的优化就能达到很好的效果。但是优化的尽头,往往要面临抉择,鱼和熊掌不可兼得。性能优秀的应用往往是多项资源的综合利用最优。为了达到综合平衡,在某些场景下,就需要做出一些调整和牺牲,常用的方法就是空间换时间或时间换空间。比如在响应时间优先的场景下,把需要耗费大量计算时间或是网络i/o时间的中间结果缓存起来,以提升后续相似请求的响应速度,便是空间换时间的一种体现。

使用更好的三方库

在我们的应用中往往会用到很多开源的第三方库,目前在github上的go开源项目就有173万+。有很多go官方库的性能表现并不佳,比如go官方的日志库性移动大流量卡能就一般,下面是zap发布的基准测试信息(记录一条消息和10个字段的性能表现)。

Package

Time

Time % to zap

Objects Allocated

⚡️ zap

862 ns/op

+0%

5 allocs/op

⚡️ zap (sugared)

1250 ns/op

+45%

11 allocs/op

zerolog

4021 ns/op

+366%

76 allocs/op

go-kit

4542 ns/op

+427%

105 allocs/op

apex/log

26785 ns/op

+3007%

115 allocs/op

logrus

29501 ns/op

+3322%

125 allocs/op

log15

2移动大流量卡9906 ns/op

+3369%

122 allocs/op

从上面可以看出zap的性能比同类结构化日志包更好,也比标准库更快,那我们就可以选择更好的三方库。

2. 性能调优-工具篇

当我们找到应用的性能短板,并针对短板制定相应优化方案,最后按照方案对代码进行优化之后,我们怎么知道优化是有效的呢?直接将代码上线,观察性能指标的变化,风险太大了。此时我们需要有好用的性能分析工具,帮助我们检验优化的效果,下面将为大家介绍几款go语言中性能分析的利器。

2.1 benchmark

Go语言标准库内置的 testing 测试框架提供了基准测试(benchmark)的能力,benchmark可以帮助我们评估代码的性移动大流量卡能表现,主要方式是通过在一定时间(默认1秒)内重复运行测试代码,然后输出执行次数和内存分配结果。下面我们用一个简单的例子来验证一下,strconv是否真的比fmt.Sprint快。首先我们来编写一段基准测试的代码,如下:

package main import ( “fmt” “strconv” “testing” ) func BenchmarkStrconv(b *testing.B) { for n := 0; n < b.N; n++ { strconv.Itoa(n) } } func BenchmarkFmtSprint(b *testing.B) { for n := 0; n < b.N; n移动大流量卡++ { fmt.Sprint(n) } }

我们可以用命令行go test -bench . 来运行基准测试,输出结果如下:

goos: darwin goarch: amd64 pkg: main cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkStrconv-12 41988014 27.41 ns/op BenchmarkFmtSprint-12 13738172 81.19 ns/op ok main 7.039s

可以看到strconv每次执行只用了27.41纳秒,而fmt.Sprint则是81.19纳秒,strconv的性能是fmt.Sprint的移动大流量卡三倍,那为什么strconv要更快呢?会不会是这次运行时间太短呢?为了公平起见,我们决定让他们再比赛一轮,这次我们延长比赛时间,看看结果如何。

通过go test -bench . -benchtime=5s 命令,我们可以把测试时间延长到5秒,结果如下:

goos: darwin goarch: amd64 pkg: main cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkStrconv-12 211533207 31.60 ns/op BenchmarkFmtSprint-12 69481287 89.58 ns/op PASS ok main 18.891s

结果有些变化,str移动大流量卡conv每次执行的时间上涨了4ns,但变化不大,差距仍有2.9倍。但是我们仍然不死心,我们决定让他们一次跑三轮,每轮5秒,三局两胜。

通过go test -bench . -benchtime=5s -count=3 命令,我们可以把测试进行3轮,结果如下:

goos: darwin goarch: amd64 pkg: main cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz BenchmarkStrconv-12 217894554 31.76 ns/op BenchmarkStrconv-12 217140132 31.45 ns/op BenchmarkStrconv-12 21913682移动大流量卡8 31.79 ns/op BenchmarkFmtSprint-12 70683580 89.53 ns/op BenchmarkFmtSprint-12 63881758 82.51 ns/op BenchmarkFmtSprint-12 64984329 82.04 ns/op PASS ok main 54.296s

结果变化也不大,看来strconv是真的比fmt.Sprint快很多。那快是快,会不会内存分配上情况就相反呢?

通过go test -bench . -benchmem 这个命令我们可以看到两个方法的内存分配情况,结果如下:

goos: darwin goarch: amd64 pkg: main cpu: Intel(R) Co移动大流量卡re(TM) i7-9750H CPU @ 2.60GHz BenchmarkStrconv-12 43700922 27.46 ns/op 7 B/op 0 allocs/op BenchmarkFmtSprint-12 143412 80.88 ns/op 16 B/op 2 allocs/op PASS ok main 7.031s

可以看到strconv在内存分配上是0次,每次运行使用的内存是7字节,只是fmt.Sprint的43.8%,简直是全方面的优于fmt.Sprint啊。那究竟是为什么strconv比fmt.Sprint好这么多呢?

通过查看strconv的代码,我们发现,对于小于100的数字,strconv是直接通过digit移动大流量卡ssmallsString这两个常量进行转换的,而大于等于100的数字,则是通过不断除以100取余,然后再找到余数对应的字符串,把这些余数的结果拼起来进行转换的。

const digits = “0123456789abcdefghijklmnopqrstuvwxyz” const smallsString = “00010203040506070809” + “10111213141516171819” + “20212223242526272829” + “30313233343536373839” + “40414243444546474849” + “50515253545556575859” + “606162636移动大流量卡46566676869″ + “70717273747576777879” + “80818283848586878889” + “90919293949596979899” // small returns the string for an i with 0 <= i < nSmalls. func small(i int) string { if i < 10 { return digits[i : i+1] } return smallsString[i*2 : i*2+2] } func formatBits(dst []byte, u uint64, base int, neg, append_ bool) (d []移动大流量卡byte, s string) { … for j := 4; j > 0; j– { is := us % 100 * 2 us /= 100 i -= 2 a[i+1] = smallsString[is+1] a[i+0] = smallsString[is+0] } … }

而fmt.Sprint则是通过反射来实现这一目的的,fmt.Sprint得先判断入参的类型,在知道参数是int型后,再调用fmt.fmtInteger方法把int转换成string,这多出来的步骤肯定没有直接把int转成string来的高效。

// fmtInteger formats s移动大流量卡igned and unsigned integers. func (f *fmt) fmtInteger(u uint64, base int, isSigned bool, verb rune, digits string) { … switch base { case 10: for u >= 10 { i– next := u / 10 buf[i] = byte(0 + u – next*10) u = next } … }

benchmark还有很多实用的函数,比如ResetTimer可以重置启动时耗费的准备时间,StopTimer和StartTimer则可以移动大流量卡暂停和启动计时,让测试结果更集中在核心逻辑上。

2.2 pprof

2.2.1 使用介绍

pprof是go语言官方提供的profile工具,支持可视化查看性能报告,功能十分强大。pprof基于定时器(10ms/次)对运行的go程序进行采样,搜集程序运行时的堆栈信息,包括CPU时间、内存分配等,最终生成性能报告。

pprof有两个标准库,使用的场景不同:

runtime/pprof 通过在代码中显式的增加触发和结束埋点来收集指定代码块运行时数据生成性能报告。net/http/pprof 是对runtime/pprof的二次封装,基于web服务运行,通过访问链接触发,采集服务运行时的数据生成性能报告。

run移动大流量卡time/pprof的使用方法如下:

package main import ( “os” “runtime/pprof” “time” ) func main() { w, _ := os.OpenFile(“test_cpu”, os.O_RDWR | os.O_CREATE | os.O_APPEND, 0644) pprof.StartCPUProfile(w) time.Sleep(time.Second) pprof.StopCPUProfile() }

我们也可以使用另外一种方法,net/http/pprof:

package main import ( “net/h移动大流量卡ttp” _ “net/http/pprof” ) func main() { err := http.ListenAndServe(“:6060”, nil) if err != nil { panic(err) } }

将程序run起来后,我们通过访问

http://127.0.0.1:6060/debug/pprof/就可以看到如下页面:

点击profile就可以下载cpu profile文件。那我们如何查看我们的性能报告呢?

pprof支持两种查看模式,终端和web界面, 注意: 想要查看可视化界面需要提前安装graphviz

这里我们以web界面为例,在终端内我们输入如下命令:

go tool pp移动大流量卡rof -http :6060 test_cpu

就会在浏览器里打开一个页面,内容如下:

从界面左上方VIEW栏下,我们可以看到,pprof支持Flame Graph,dot Graph和Top等多种视图,下面我们将一一介绍如何阅读这些视图。

2.2.1 火焰图 Flame Graph如何阅读

首先,推荐直接阅读火焰图,在查函数耗时场景,这个比较直观;

最简单的:横条越长,资源消耗、占用越多

注意:每一个function 的横条虽然很长,但可能是他的下层“子调用”耗时产生的,所以一定要关注“下一层子调用”各自的耗时分布;

每个横条支持点击下钻能力,可以更详细的分析子层的耗时占比。

2.2.2 dot Graph 移动大流量卡图如何阅读

英文原文在这里:https://github.com/google/pprof/blob/master/doc/README.md

节点颜色:红色表示耗时多的节点;绿色表示耗时少的节点;灰色表示耗时几乎可以忽略不计(接近零);节点字体大小 :字体越大,表示占“上层函数调用”比例越大;(其实上层函数自身也有耗时,没包含在此)字体越小,表示占“上层函数调用”比例越小;线条(边)粗细:线条越粗,表示消耗了更多的资源;反之,则越少;线条(边)颜色:颜色越红,表示性能消耗占比越高;颜色越绿,表示性能消耗占比越低;灰色,表示性能消耗几乎可以忽略不计;虚线:表示中间有一些节点被“移除”或者忽略了;(一移动大流量卡般是因为耗时较少所以忽略了)实线:表示节点之间直接调用

内联边标记:被调用函数已经被内联到调用函数中对于一些代码行比较少的函数,编译器倾向于将它们在编译期展开从而消除函数调用,这种行为就是内联。)

2.2.3 TOP 表如何阅读

flat:当前函数,运行耗时(不包含内部调用其他函数的耗时)flat%:当前函数,占用的 CPU 运行耗时总比例(不包含外部调用函数)sum%:当前行的flat%与上面所有行的flat%总和cum:当前函数加上它内部的调用的运行总耗时(包含内部调用其他函数的耗时)cum%:同上的 CPU 运行耗时总比例

2.3 trace

pprof已经有了对内存和CPU的分析能力,那tr移动大流量卡ace工具有什么不同呢?虽然pprof的CPU分析器,可以告诉你什么函数占用了最多的CPU时间,但它并不能帮助你定位到是什么阻止了goroutine运行,或者在可用的OS线程上如何调度goroutines。这正是trace真正起作用的地方。

我们需要更多关于Go应用中各个goroutine的执行情况的更为详细的信息,可以从P(goroutine调度器概念中的processor)和G(goroutine调度器概念中的goroutine)的视角完整的看到每个P和每个G在Tracer开启期间的全部“所作所为”,对Tracer输出数据中的每个P和G的行为分析并结合详细的event数据来辅助问题诊断的。

T移动大流量卡racer可以帮助我们记录的详细事件包含有:

与goroutine调度有关的事件信息:goroutine的创建、启动和结束;goroutine在同步原语(包括mutex、channel收发操作)上的阻塞与解锁。与网络有关的事件:goroutine在网络I/O上的阻塞和解锁;与系统调用有关的事件:goroutine进入系统调用与从系统调用返回;与垃圾回收器有关的事件:GC的开始/停止,并发标记、清扫的开始/停止。

Tracer主要也是用于辅助诊断这三个场景下的具体问题的:

并行执行程度不足的问题:比如没有充分利用多核资源等;因GC导致的延迟较大的问题;Goroutine执行情况分析,尝试发现gorou移动大流量卡tine因各种阻塞(锁竞争、系统调用、调度、辅助GC)而导致的有效运行时间较短或延迟的问题。

2.3.1 trace性能报告

打开trace性能报告,首页信息包含了多维度数据,如下图:

View trace:以图形页面的形式渲染和展示tracer的数据,这也是我们最为关注/最常用的功能Goroutine analysis:以表的形式记录执行同一个函数的多个goroutine的各项trace数据Network blocking profile:用pprof profile形式的调用关系图展示网络I/O阻塞的情况Synchronization blocking profile:用pprof profil移动大流量卡e形式的调用关系图展示同步阻塞耗时情况Syscall blocking profile:用pprof profile形式的调用关系图展示系统调用阻塞耗时情况Scheduler latency profile:用pprof profile形式的调用关系图展示调度器延迟情况User-defined tasks和User-defined regions:用户自定义trace的task和regionMinimum mutator utilization:分析GC对应用延迟和吞吐影响情况的曲线图

通常我们最为关注的是View trace和Goroutine analysis,下面将详细说说这两项的用法。

2移动大流量卡.3.2 view trace

如果Tracer跟踪时间较长,trace会将View trace按时间段进行划分,避免触碰到trace-viewer的限制:

View trace使用快捷键来缩放时间线标尺:w键用于放大(从秒向纳秒缩放),s键用于缩小标尺(从纳秒向秒缩放)。我们同样可以通过快捷键在时间线上左右移动:s键用于左移,d键用于右移。(游戏快捷键WASD)

采样状态

这个区内展示了三个指标:Goroutines、Heap和Threads,某个时间点上的这三个指标的数据是这个时间点上的状态快照采样:Goroutines:某一时间点上应用中启动的goroutine的数量,当我们点击某个时间点上的g移动大流量卡oroutines采样状态区域时(我们可以用快捷键m来准确标记出那个时间点),事件详情区会显示当前的goroutines指标采样状态:

Heap指标则显示了某个时间点上Go应用heap分配情况(包括已经分配的Allocated和下一次GC的目标值NextGC):

Threads指标显示了某个时间点上Go应用启动的线程数量情况,事件详情区将显示处于InSyscall(整阻塞在系统调用上)和Running两个状态的线程数量情况:

P视角区

这里将View trace视图中最大的一块区域称为“P视角区”。这是因为在这个区域,我们能看到Go应用中每个P(Goroutine调度概念中的P)上发生的所有事件,包括移动大流量卡:EventProcStart、EventProcStop、EventGoStart、EventGoStop、EventGoPreempt、Goroutine辅助GC的各种事件以及Goroutine的GC阻塞(STW)、系统调用阻塞、网络阻塞以及同步原语阻塞(mutex)等事件。除了每个P上发生的事件,我们还可以看到以单独行显示的GC过程中的所有事件。

剩余60%,完整内容请点击下方链接查看:

https://mp.weixin.qq.com/s?__biz=MzIzOTU0NTQ0MA==&mid=2247532274&idx=1&sn=b6d9cd9d71d78ee73118880bc1a54移动大流量卡6e6&chksm=e92a43fdde5dcaebe4145a71dc2a4f84e845627fb69da0ea16b028c54fd87371c9fb33c85274&token=1334496393&lang=zh_CN#rd

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。


友情提醒: 请添加客服微信进行免费领取流量卡!
QQ交流群:226333560 站长微信:qgzmt2

原创文章,作者:sunyaqun,如若转载,请注明出处:https://www.dallk.cn/56179.html

(0)
sunyaqunsunyaqun
上一篇 2024年7月7日
下一篇 2024年7月7日

相关推荐

发表回复

登录后才能评论