首页 > 应用开发 > > 高德Go生态的服务稳定性建设|性能优化的实战总结

阿里妹导读
目前go语言不仅在阿里集团内部,在整个互联网行业内也越来越流行,本文把高德过去go服务开发中的性能调优经验进行总结和沉淀,希望能为正在使用go语言的同学在性能优化方面带来一些参考价值。
前言
一、性能调优-理论篇
1.1 衡量指标
以上这些都是系统资源层面用于衡量性能的指标,除此之外还有应用本身的稳定性指标:
1.2 制定优化方案
1)提高复用性,将通用的代码抽象出来,减少重复开发。
2)池化,对象可以池化,减少内存分配;协程可以池化,避免无限制创建协程打满内存。
3)并行化,在合理创建协程数量的前提下,把互不依赖的部分并行处理,减少整体的耗时。
4)异步化,把不需要关心实时结果的请求,用异步的方式处理,不用一直等待结果返回。
5)算法优化,使用时间复杂度更低的算法。
从上面可以看出zap的性能比同类结构化日志包更好,也比标准库更快,那我们就可以选择更好的三方库。
二、性能调优-工具篇
2.1 benchmark
package main
import ( "fmt" "strconv" "testing")
func BenchmarkStrconv(b *testing.B) { for n := ; n < b.N; n++ { strconv.Itoa(n) }}
func BenchmarkFmtSprint(b *testing.B) { for n := ; n < b.N; n++ { fmt.Sprint(n) }}
我们可以用命令行go test -bench . 来运行基准测试,输出结果如下:
goos: darwingoarch: amd64pkg: maincpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz 41988014 27.41 ns/op 13738172 81.19 ns/opok main 7.039s
goos: darwingoarch: amd64pkg: maincpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz 211533207 31.60 ns/op 69481287 89.58 ns/opPASSok main 18.891s
goos: darwingoarch: amd64pkg: maincpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz 217894554 31.76 ns/op 217140132 31.45 ns/op 219136828 31.79 ns/op 70683580 89.53 ns/op 63881758 82.51 ns/op 64984329 82.04 ns/opPASSok main 54.296s结果变化也不大,看来strconv是真的比fmt.Sprint快很多。那快是快,会不会内存分配上情况就相反呢?
goos: darwingoarch: amd64pkg: maincpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz 43700922 27.46 ns/op 7 B/op 0 allocs/op 143412 80.88 ns/op 16 B/op 2 allocs/opPASSok main 7.031s可以看到strconv在内存分配上是0次,每次运行使用的内存是7字节,只是fmt.Sprint的43.8%,简直是全方面的优于fmt.Sprint啊。那究竟是为什么strconv比fmt.Sprint好这么多呢?
const digits = "0123456789abcdefghijklmnopqrstuvwxyz"const smallsString = "00010203040506070809" + "10111213141516171819" + "20212223242526272829" + "30313233343536373839" + "40414243444546474849" + "50515253545556575859" + "60616263646566676869" + "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 > ; j-- { is := us % 100 * 2 us /= 100 i -= 2 a[i+1] = smallsString[is+1] a[i+] = smallsString[is+] } ...}
而fmt.Sprint则是通过反射来实现这一目的的,fmt.Sprint得先判断入参的类型,在知道参数是int型后,再调用fmt.fmtInteger方法把int转换成string,这多出来的步骤肯定没有直接把int转成string来的高效。
// fmtInteger formats signed 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
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/http" _ "net/http/pprof")
func main() { err := http.ListenAndServe(":6060", nil) if err != nil { panic(err) }}
将程序run起来后,我们通过访问http://127.0.0.1:6060/debug/pprof/就可以看到如下页面:

go tool pprof -http :6060 test_cpu




2.3 trace









2.4 后记
三、性能调优-技巧篇
3.1 字符串拼接
// 推荐:用+进行字符串拼接func BenchmarkPlus(b *testing.B) { for i := ; i < b.N; i++ { s := "a" + "b" _ = s }}// 不推荐:用fmt.Sprintf进行字符串拼接func BenchmarkFmt(b *testing.B) { for i := ; i < b.N; i++ { s := fmt.Sprintf("%s%s", "a", "b") _ = s }}
goos: darwingoarch: amd64pkg: maincpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHzBenchmarkPlus-12 1000000000 0.2658 ns/op B/op allocs/opBenchmarkFmt-12 16559949 70.83 ns/op 2 B/op 1 allocs/opPASSok main 5.908s
// 推荐:初始化时指定容量func BenchmarkGenerateWithCap(b *testing.B) {
nums := make([]int, , 10000) for n := ; n < b.N; n++ { for i:=; i < 10000; i++ { nums = append(nums, i) } }}// 不推荐:初始化时不指定容量func BenchmarkGenerate(b *testing.B) { nums := make([]int, ) for n := ; n < b.N; n++ { for i:=; i < 10000; i++ { nums = append(nums, i) } }}
goos: darwingoarch: amd64pkg: maincpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHzBenchmarkGenerateWithCap-12 23508 336485 ns/op 476667 B/op allocs/opBenchmarkGenerate-12 22620 68747 ns/op 426141 B/op allocs/opPASSok main 16.628s
3.3 遍历 []struct{} 使用下标而不是 range
func getIntSlice() []int { nums := make([]int, 1024, 1024) for i := ; i < 1024; i++ { nums[i] = i } return nums}// 用下标遍历[]intfunc BenchmarkIndexIntSlice(b *testing.B) { nums := getIntSlice() b.ResetTimer() for i := ; i < b.N; i++ { var tmp int for k := ; k < len(nums); k++ { tmp = nums[k] } _ = tmp }}// 用range遍历[]int元素func BenchmarkRangeIntSlice(b *testing.B) { nums := getIntSlice() b.ResetTimer() for i := ; i < b.N; i++ { var tmp int for _, num := range nums { tmp = num } _ = tmp }}
goos: darwingoarch: amd64pkg: demo/testcpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHzBenchmarkIndexIntSlice-12 3923230 270.2 ns/op B/op allocs/opBenchmarkRangeIntSlice-12 4518495 287.8 ns/op B/op allocs/opPASSok demo/test 3.303s
可以看到,在遍历[]int时,两种方式并无差别。
type Item struct { id int val [1024]byte}// 推荐:用下标遍历[]struct{}func BenchmarkIndexStructSlice(b *testing.B) { var items [1024]Item for i := ; i < b.N; i++ { var tmp int for j := ; j < len(items); j++ { tmp = items[j].id } _ = tmp }}// 推荐:用range的下标遍历[]struct{}func BenchmarkRangeIndexStructSlice(b *testing.B) { var items [1024]Item for i := ; i < b.N; i++ { var tmp int for k := range items { tmp = items[k].id } _ = tmp }}// 不推荐:用range遍历[]struct{}的元素func BenchmarkRangeStructSlice(b *testing.B) { var items [1024]Item for i := ; i < b.N; i++ { var tmp int for _, item := range items { tmp = item.id } _ = tmp }}
goos: darwingoarch: amd64pkg: demo/testcpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHzBenchmarkIndexStructSlice-12 4413182 266.7 ns/op B/op allocs/opBenchmarkRangeIndexStructSlice-12 4545476 269.4 ns/op B/op allocs/opBenchmarkRangeStructSlice-12 33300 35444 ns/op B/op allocs/opPASSok demo/test 5.282s可以看到,用for循环下标的方式性能都差不多,但是用range遍历数组里的元素时,性能则相差很多,前面两种方法是第三种方法的130多倍。主要原因是通过for k, v := range获取到的元素v实际上是原始值的一个拷贝。所以在面对复杂的struct进行遍历的时候,推荐使用下标。但是当遍历对象是复杂结构体的指针([]*struct{})时,用下标还是用range迭代元素的性能就差不多了。
3.4 利用unsafe包避开内存copy

func Str2bytes(s string) []byte { x := (*[2]uintptr)(unsafe.Pointer(&s)) h := [3]uintptr{x[], x[1], x[1]} return *(*[]byte)(unsafe.Pointer(&h))}
func Bytes2str(b []byte) string { return *(*string)(unsafe.Pointer(&b))}
我们通过benchmark来验证一下是否性能更优:
// 推荐:用unsafe.Pointer实现string到bytesfunc BenchmarkStr2bytes(b *testing.B) { s := "testString" var bs []byte for n := ; n < b.N; n++ { bs = Str2bytes(s) } _ = bs}// 不推荐:用类型转换实现string到bytesfunc BenchmarkStr2bytes2(b *testing.B) { s := "testString" var bs []byte for n := ; n < b.N; n++ { bs = []byte(s) } _ = bs}// 推荐:用unsafe.Pointer实现bytes到stringfunc BenchmarkBytes2str(b *testing.B) { bs := Str2bytes("testString") var s string b.ResetTimer() for n := ; n < b.N; n++ { s = Bytes2str(bs) } _ = s}// 不推荐:用类型转换实现bytes到stringfunc BenchmarkBytes2str2(b *testing.B) { bs := Str2bytes("testString") var s string b.ResetTimer() for n := ; n < b.N; n++ { s = string(bs) } _ = s}
goos: darwingoarch: amd64pkg: demo/testcpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHzBenchmarkStr2bytes-12 1000000000 0.2938 ns/op B/op allocs/opBenchmarkStr2bytes2-12 38193139 28.39 ns/op 16 B/op 1 allocs/opBenchmarkBytes2str-12 1000000000 0.2552 ns/op B/op allocs/opBenchmarkBytes2str2-12 60836140 19.60 ns/op 16 B/op 1 allocs/opPASSok demo/test 3.301s
可以看到使用unsafe.Pointer比强制类型转换性能是要高不少的,从内存分配上也可以看到完全没有新的内存被分配。
3.5 协程池
var wg sync.WaitGroupch := make(chan struct{}, 3)for i := ; i < 10; i++ { ch <- struct{}{} wg.Add(1) go func(i int) { defer wg.Done() log.Println(i) time.Sleep(time.Second) <-ch }(i)}wg.Wait()
这里通过限制channel长度为3,可以实现多只有3个协程被创建的效果。
func Test_ErrGroupRun(t *testing.T) { errgroup := WithTimeout(nil, 10*time.Second) errgroup.SetMaxProcs(4) for index := ; index < 10; index++ { errgroup.Run(nil, index, "test", func(context *gin.Context, i interface{}) (interface{}, error) { t.Logf("[%s]input:%+v, time:%s", "test", i, time.Now().Format("2006-01-02 15:04:05")) time.Sleep(2*time.Second) return i, nil }) } errgroup.Wait()}
输出结果如下:
=== RUN Test_ErrGroupRun errgroup_test.go:23: [test]input:, time:2022-12-04 17:31:29 errgroup_test.go:23: [test]input:3, time:2022-12-04 17:31:29 errgroup_test.go:23: [test]input:1, time:2022-12-04 17:31:29 errgroup_test.go:23: [test]input:2, time:2022-12-04 17:31:29 errgroup_test.go:23: [test]input:4, time:2022-12-04 17:31:31 errgroup_test.go:23: [test]input:5, time:2022-12-04 17:31:31 errgroup_test.go:23: [test]input:6, time:2022-12-04 17:31:31 errgroup_test.go:23: [test]input:7, time:2022-12-04 17:31:31 errgroup_test.go:23: [test]input:8, time:2022-12-04 17:31:33 errgroup_test.go:23: [test]input:9, time:2022-12-04 17:31:33--- PASS: Test_ErrGroupRun (6.00s)PASS
errgroup可以通过SetMaxProcs设定协程池的大小,从上面的结果可以看到,多就4个协程在运行。
3.6 sync.Pool 对象复用
type ActTask struct { Id int64 `ddb:"id"` // 主键id Status common.Status `ddb:"status"` // 状态 0=初始 1=生效 2=失效 3=过期 BizProd common.BizProd `ddb:"biz_prod"` // 业务类型 Name string `ddb:"name"` // 活动名 Adcode string `ddb:"adcode"` // 城市 RealTimeRuleByte []byte `ddb:"realtime_rule"` // 实时规则json ...}
type RealTimeRuleStruct struct { Filter []*struct { PropertyId int64 `json:"property_id"` PropertyCode string `json:"property_code"` Operator string `json:"operator"` Value []string `json:"value"` } `json:"filter"` ExtData [1024]byte `json:"ext_data"`}
func (at *ActTask) RealTimeRule() *form.RealTimeRule { if err := json.Unmarshal(at.RealTimeRuleByte, &at.RealTimeRuleStruct); err != nil { return nil } return at.RealTimeRuleStruct}以这里的实时投放规则为例,我们会将过滤规则反序列化为字节数组。每次json.Unmarshal都会申请一个临时的结构体对象,而这些对象都是分配在堆上的,会给 GC 造成很大压力,严重影响程序的性能。
var realTimeRulePool = sync.Pool{ New: func() interface{} { return new(RealTimeRuleStruct) },}
然后调用 Pool 的 Get() 和 Put() 方法来获取和放回池子中。
rule := realTimeRulePool.Get().(*RealTimeRuleStruct)json.Unmarshal(buf, rule)realTimeRulePool.Put(rule)
var realTimeRule = []byte("{\\\"filter\\\":[{\\\"property_id\\\":2,\\\"property_code\\\":\\\"search_poiid_industry\\\",\\\"operator\\\":\\\"in\\\",\\\"value\\\":[\\\"yimei\\\"]},{\\\"property_id\\\":4,\\\"property_code\\\":\\\"request_page_id\\\",\\\"operator\\\":\\\"in\\\",\\\"value\\\":[\\\"all\\\"]}],\\\"white_list\\\":[{\\\"property_id\\\":1,\\\"property_code\\\":\\\"white_list_for_adiu\\\",\\\"operator\\\":\\\"in\\\",\\\"value\\\":[\\\"j838ef77bf227chcl89888f3fb0946\\\",\\\"lb89bea9af558589i55559764bc83e\\\"]}],\\\"ipc_user_tag\\\":[{\\\"property_id\\\":1,\\\"property_code\\\":\\\"ipc_crowd_tag\\\",\\\"operator\\\":\\\"in\\\",\\\"value\\\":[\\\"test_20227041152_mix_ipc_tag\\\"]}],\\\"relation_id\\\":0,\\\"is_copy\\\":true}")// 推荐:复用一个对象,不用每次都生成新的func BenchmarkUnmarshalWithPool(b *testing.B) { for n := ; n < b.N; n++ { task := realTimeRulePool.Get().(*RealTimeRuleStruct) json.Unmarshal(realTimeRule, task) realTimeRulePool.Put(task) }}// 不推荐:每次都会生成一个新的临时对象func BenchmarkUnmarshal(b *testing.B) { for n := ; n < b.N; n++ { task := &RealTimeRuleStruct{} json.Unmarshal(realTimeRule, task) }}
goos: darwingoarch: amd64pkg: demo/testcpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHzBenchmarkUnmarshalWithPool-12 3627546 319.4 ns/op 312 B/op 7 allocs/opBenchmarkUnmarshal-12 2342208 490.8 ns/op 1464 B/op 8 allocs/opPASSok demo/test 3.525s
可以看到,两种方法在时间消耗上差不太多,但是在内存分配上差距明显,使用sync.Pool后内存占用仅为不使用的1/5。
3.7 避免系统调用
// 推荐:不使用系统调用func BenchmarkNoSytemcall(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { if configs.PUBLIC_KEY != nil { } } })}// 不推荐:使用系统调用func BenchmarkSytemcall(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { if os.Getenv("PUBLIC_KEY") != "" { } } })}goos: darwingoarch: amd64pkg: demo/testcpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHzBenchmarkNoSytemcall-12 1000000000 0.1495 ns/op B/op allocs/opBenchmarkSytemcall-12 37224988 31.10 ns/op B/op allocs/opPASSok demo/test 1.877s
四、性能调优-实战篇
案例1: go协程创建数据库连接不释放导致内存暴涨


案例2: 优惠索引内存分配大,gc 耗时高




案例3:流量上涨导致cpu异常飙升

1、优化toEntity方法,简化为单独的ID()方法
2、优化数组、map初始化结构
3、优化adCode转换为string过程


案例4:内存对象未释放导致内存泄漏






结语
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/758/viewspace-3141542/ ,如需转载,请注明出处,否则将追究法律责任。