记录一下本人在公司当中参与的一次服务的性能优化的过程以及思路。
背景
在双11来临之前的1个月,我们做了一次服务的自检,发现服务的cpu负载一直都很高,和相应的线上流量不太匹配,按照双11当天的流量,应该是要扩容到飞起,成本无法承受。
所以要在双11到来之前做一次老服务的整体性能优化
性能分析 & 工具
针对cpu负载过高的基本也就是老套路,直接review对应时间的代码即可。
如果不知道开始结束时间的话,还是通过万能的pprof来找问题。
pprof
开启方式
开启方式很简单,网上有很多详细的教程,这里就提供一下相关的代码
代码初始化的时候做插入
import (
_ "net/http/pprof"
"net/http"
)
func init() {
go func() {
http.ListenAndServe("0.0.0.0:6060", nil)
}()
}
然后就可以开启采集
# 抓 30s 的 CPU Profile
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
分析结果
通过上面pprof的捉取,你就能够得到
下面就是采集的结果
从图上可以看到,smartRecommend和queryBannerInfo占了服务40%的cpu了。
知道问题出现的根源之后我们就可以去找具体的问题点了,
下面我们单独看这两个方法
优化点一:smartRecommend
sort包排序
先来看smartRecommend的问题,从图中可以看出,sort.SliceStable占用了大量的cpu,sort.SliceStable下面的json.Unmarshal又占用了大量的cpu。
很明显是在sort.SliceStable里面做了unamrshal导致的问题。
上代码
func calcProductPrice(product *interface{}) float64 {
// 这里藏了太多的东西了
if !isProductDiscountValid(product) {
return product.Price
}
return product.Price - product.DiscountOff
}
func sortProductsByPriceAfterDiscountAsc(products []*interface{}) {
sort.SliceStable(products, func(p, q int) bool {
return calcProductPrice(products[p]) < calcProductPrice(products[q])
})
}
func isProductDiscountValid(product *interface{}) bool {
if product.Discount == "" {
return false
}
var productDiscount ProductDiscount
// 这里涉及到了json解析
if err := json.Unmarshal([]byte(product.Discount), &productDiscount); err != nil {
fmt.Println("isProductDiscountValid Unmarshal err:%v,product:%+v", err, product)
return false
}
// 时间转换
startAt, err := ParseTimeFromMultipleFormats(productDiscount.StartAt)
if err != nil {
fmt.Println("isProductDiscountValid start err:%v,product:%+v", err, product)
return false
}
if time.Now().Before(startAt) {
return false
}
if productDiscount.EndAt == "" {
return true
}
// 又来一个时间转换
endAt, err := ParseTimeFromMultipleFormats(productDiscount.EndAt)
if err != nil {
fmt.Println("isProductDiscountValid end err:%v,product:%+v", err, product)
return false
}
if time.Now().After(endAt) {
return false
}
return true
}
这里的排序涉及到大量商品折后价的比较,并且都是写在sort.SliceStable里面的。这里就涉及到sort.SliceStable的底层原理了,后面章节有详解
我们现在就要知道一个点,stable都是稳定排序,底层是使用插入排序以及归并排序,
解决方案
最好的方式还是将sort.Slice的折后价的计算逻辑都放到上一层去计算,sort方法里面只比较,不做复杂的逻辑计算。
具体方式:我们写一个v2版本的排序方法,提前计算折后价即可。
fmt.Println日志输出
fmt.Print的输出都是带了反射的。这里没什么好说的,线上代码就不要出现这一类的调试代码了。
这里在commit和code review的时候就需要过滤掉的内容(老代码已无从考究了)。
效果
通过本机的Benchmark就能够看出差距有多大了(macbook m2 pro,16G)
// 优化前
BenchmarkSortSkuLableByPriceAfterDiscountAsc-10 6 196320556 ns/op 7600428 B/op 151704 allocs/op
// 优化后
BenchmarkSortSkuLableByPriceAfterDiscountAsc-10 160485 7263 ns/op 952 B/op 3 allocs/op
效果直接起飞
| 指标 | 优化前 | 优化后 | 提升倍数(约) |
|---|---|---|---|
| 时间(ns/op) | 196 320 556 ns/op | 7 263 ns/op | ~27 000× 更快 |
| 内存分配(B/op) | 7 600 428 B/op (≈7.2 MB) | 952 B/op (≈0.93 KB) | ~8 000× 更少 |
| 分配次数(allocs/op) | 151 704 allocs/op | 3 allocs/op | ~50 000× 更少 |
知识点
sort
go的sort包里面主要是两个入口
func Sort(data Interface)
func Stable(data Interface)
Sort:对data进行 不稳定排序(元素相等时不保证原有相对顺序),在大多数场景下性能最快Stable:对data进行 稳定排序(相等元素保持原有顺序),如果你需要多关键字排序或保持先前顺序,就用它
两个都是按照sort.Interface来实现的
type Interface interface {
Len() int // 元素数量
Less(i, j int) bool // i 元素是否小于 j 元素
Swap(i, j int) // 交换 i 和 j 元素
}
内部实现
Go 1.19 之前,Sort 主要基于三种算法的混合
- 快速排序(Quicksort)
- 堆排序(Heapsort) :当递归深度过大时,为了避免最坏 O(n²),切换到堆排序
- 插入排序(Insertion sort) :当待排序区间长度很小时(通常 ≤ 20),直接用插入排序以减少常数开销
pdqsort
在1.19之后,就换成的pdqsort(字节大佬写的),这是一种混合排序,参考了rust和c++里面的实现。
核心思想:不断地判断当前的排序情况,然后选用不同的方式来排序。并且对常见的情况做了特殊优化。
这一块可以直接看字节的文章,mp.weixin.qq.com/s/5HqfRGqPy…。
fmt.Println
fmt.Prinltn的参数都是通过反射来获取的,所以性能可想而知。这一块本地调试即可,commit之间一定要记得干掉(话说为啥这么多人不喜欢用debug模式,所有参数都一目了然了)。
最终效果
目前来看高峰期减少了50%的pod的数量,从原来的20个pod下降到了10个。
总结
虽然改动不大,但是整体的效果还是很有效的。
当然,不只是上面的操作降低了成本,我们还做了一些其他的优化来优化服务的整体性能的。(打算分开两篇来写)