记一次提升18倍的性能优化
背景
最近负责的一个自研的 Dubbo 注册中心经常收到 CPU 使用率的告警,于是进行了一波优化,效果还不错,于是打算分享下思考、优化过程,希望对大家有一些帮助。
自研 Dubbo 注册中心是个什么东西,我画个简图大家稍微感受一下就好,看不懂也没关系,不影响后续的理解。
Consumer 和 Provider 的服务发现请求(注册、注销、订阅)都发给 Agent,由它全权代理
Registry 和 Agent 保持 Grpc 长链接,长链接的目的主要是 Provider 方有变更时,能及时推送给相应的 Consumer。为了保证数据的正确性,做了推拉结合的机制,Agent 会每隔一段时间去 Registry 拉取订阅的服务列表
Agent 和业务服务部署在同一台机器上,类似 Service Mesh 的思路,尽量减少对业务的入侵,这样就能快速的迭代了
回到今天的重点,这个注册中心最近 CPU 使用率长期处于中高水位,偶尔有应用发布,推送量大时,CPU 甚至会被打满。
以前没感觉到,是因为接入的应用不多,最近几个月应用越接越多,慢慢就达到了告警阈值。
寻找优化点
由于这项目是 Go 写的(不懂 Go 的朋友也没关系,本文重点在算法的优化,不在工具的使用上), 找到哪里耗 CPU 还是挺简单的:打开 pprof 即可,去线上采集一段时间即可。
具体怎么操作可以参考我之前的这篇文章,今天文章中用到的知识和工具,这篇文章都能找到。
CPU profile 截了部分图,其他的不太重要,可以看到消耗 CPU 多的是 AssembleCategoryProviders
方法,与其直接关联的是
2个 redis 相关的方法
1个叫
assembleUrlWeight
的方法
稍微解释下,AssembleCategoryProviders 方法是构造返回 Dubbo provider 的 url,由于会在返回 url 时对其做一些处理(比如调整权重等),会涉及到对这个 Dubbo url 的解析。又由于推拉结合的模式,线上服务使用方越多,这个处理的 QPS 就越大,所以它占用了大部分 CPU 一点也不奇怪。
这两个 redis 操作可能是序列化占用了 CPU,更大头在 assembleUrlWeight,有点琢磨不透。
接下来我们就分析下 assembleUrlWeight 如何优化,因为他占用 CPU 最多,优化效果肯定最好。
下面是 assembleUrlWeight 的伪代码:
func AssembleUrlWeight(rawurl string, lidcWeight int) string {u, err := url.Parse(rawurl)if err != nil {return rawurl}values, err := url.ParseQuery(u.RawQuery)if err != nil {return rawurl}if values.Get("lidc_weight") != "" {return rawurl}endpointWeight := 100if values.Get("weight") != "" {endpointWeight, err = strconv.Atoi(values.Get("weight"))if err != nil {endpointWeight = 100}}values.Set("weight", strconv.Itoa(lidcWeight*endpointWeight))u.RawQuery = values.Encode()return u.String()
}
传参 rawurl 是 Dubbo provider 的url,lidcWeight 是机房权重。根据配置的机房权重,将 url 中的 weight 进行重新计算,实现多机房流量按权重的分配。
这个过程涉及到 url 参数的解析,再进行 weight 的计算,最后再还原为一个 url
Dubbo 的 url 结构和普通 url 结构一致,其特点是参数可能比较多,没有 #
后面的片段部分。
CPU 主要就消耗在这两次解析和最后的还原中,我们看这两次解析的目的就是为了拿到 url 中的 lidc_weight
和 weight
参数。
url.Parse 和 url.ParseQuery 都是 Go 官方提供的库,各个语言也都有实现,其核心是解析 url 为一个对象,方便地获取 url 的各个部分。
如果了解信息熵这个概念,其实你就大概知道这里面一定是可以优化的。Shannon(香农)
借鉴了热力学的概念,把信息中排除了冗余后的平均信息量称为信息熵
。
url.Parse 和 url.ParseQuery 在这个场景下解析肯定存在冗余,冗余意味着 CPU 在做多余的事情。
因为一个 Dubbo url 参数通常是很多的,我们只需要拿这两个参数,而 url.Parse 解析了所有的参数。
举个例子,给定一个数组,求其中的最大值,如果先对数组进行排序,再取最大值显然是存在冗余操作的。
排序后的数组不仅能取最大值,还能取第二大值、第三大值...最小值,信息存在冗余了,所以先排序肯定不是求最大值的最优解。
优化
优化获取 url 参数性能
第一想法是,不要解析全部 url,只拿相应的参数,这就很像我们写的算法题,比如获取 weight 参数,它只可能是这两种情况(不存在 #,所以简单很多):
dubbo://127.0.0.1:20880/org.newboo.basic.MyDemoService?weight=100&...
dubbo://127.0.0.1:20880/org.newboo.basic.MyDemoService?xx=yy&weight=100&...
要么是 &weight=
,要么是 ?weight=
,结束要么是&
,要么直接到字符串尾,代码就很好写了,先手写个解析参数的算法:
func GetUrlQueryParam(u string, key string) (string, error) {sb := strings.Builder{}sb.WriteString(key)sb.WriteString("=")index := strings.Index(u, sb.String())if (index == -1) || (index+len(key)+1 > len(u)) {return "", UrlParamNotExist}var value = strings.Builder{}for i := index + len(key) + 1; i < len(u); i++ {if i+1 > len(u) {break}if u[i:i+1] == "&" {break}value.WriteString(u[i : i+1])}return value.String(), nil
}
原先获取参数的方法可以摘出来:
func getParamByUrlParse(ur string, key string) string {u, err := url.Parse(ur)if err != nil {return ""}values, err := url.ParseQuery(u.RawQuery)if err != nil {return ""}return values.Get(key)
}
先对这两个函数进行 benchmark:
func BenchmarkGetQueryParam(b *testing.B) {for i := 0; i < b.N; i++ {getParamByUrlParse(u, "anyhost")getParamByUrlParse(u, "version")getParamByUrlParse(u, "not_exist")}
}func BenchmarkGetQueryParamNew(b *testing.B) {for i := 0; i < b.N; i++ {GetUrlQueryParam(u, "anyhost")GetUrlQueryParam(u, "version")GetUrlQueryParam(u, "not_exist")}
}
Benchmark 结果如下:
BenchmarkGetQueryParam-4 103412 9708 ns/op
BenchmarkGetQueryParam-4 111794 9685 ns/op
BenchmarkGetQueryParam-4 115699 9818 ns/op
BenchmarkGetQueryParamNew-4 2961254 409 ns/op
BenchmarkGetQueryParamNew-4 2944274 406 ns/op
BenchmarkGetQueryParamNew-4 2895690 405 ns/op
可以看到性能大概提升了20多倍
新写的这个方法,有两个小细节,第一是返回值中区分了参数是否存在,这个后面会用到;第二是字符串的操作用到了 strings.Builder
,这也是实际测试的结果,使用 +
或者 fmt.Springf
性能都没这个好,感兴趣可以测试下看看。
优化 url 写入参数性能
计算出 weight 后再把 weight 写入 url 中,这里直接给出优化后的代码:
func AssembleUrlWeightNew(rawurl string, lidcWeight int) string {if lidcWeight == 1 {return rawurl}lidcWeightStr, err1 := GetUrlQueryParam(rawurl, "lidc_weight")if err1 == nil && lidcWeightStr != "" {return rawurl}var err errorendpointWeight := 100weightStr, err2 := GetUrlQueryParam(rawurl, "weight")if weightStr != "" {endpointWeight, err = strconv.Atoi(weightStr)if err != nil {endpointWeight = 100}}if err2 != nil { // url中不存在weightfinUrl := strings.Builder{}finUrl.WriteString(rawurl)if strings.Contains(rawurl, "?") {finUrl.WriteString("&weight=")finUrl.WriteString(strconv.Itoa(lidcWeight * endpointWeight))return finUrl.String()} else {finUrl.WriteString("?weight=")finUrl.WriteString(strconv.Itoa(lidcWeight * endpointWeight))return finUrl.String()}} else { // url中存在weightoldWeightStr := strings.Builder{}oldWeightStr.WriteString("weight=")oldWeightStr.WriteString(weightStr)newWeightStr := strings.Builder{}newWeightStr.WriteString("weight=")newWeightStr.WriteString(strconv.Itoa(lidcWeight * endpointWeight))return strings.ReplaceAll(rawurl, oldWeightStr.String(), newWeightStr.String())}
}
主要就是分为 url 中是否存在 weight 两种情况来讨论:
url 本身不存在 weight 参数,则直接在 url 后拼接一个 weight 参数,当然要注意是否存在
?
url 本身存在 weight 参数,则直接进行字符串替换
细心的你肯定又发现了,当 lidcWeight = 1
时,直接返回,因为 lidcWeight = 1
时,后面的计算其实都不起作用(Dubbo 权重默认为100),索性别操作,省点 CPU。
全部优化完,总体做一下 benchmark:
func BenchmarkAssembleUrlWeight(b *testing.B) {for i := 0; i < b.N; i++ {for _, ut := range []string{u, u1, u2, u3} {AssembleUrlWeight(ut, 60)}}
}func BenchmarkAssembleUrlWeightNew(b *testing.B) {for i := 0; i < b.N; i++ {for _, ut := range []string{u, u1, u2, u3} {AssembleUrlWeightNew(ut, 60)}}
}
结果如下:
BenchmarkAssembleUrlWeight-4 34275 33289 ns/op
BenchmarkAssembleUrlWeight-4 36646 32432 ns/op
BenchmarkAssembleUrlWeight-4 36702 32740 ns/op
BenchmarkAssembleUrlWeightNew-4 573684 1851 ns/op
BenchmarkAssembleUrlWeightNew-4 646952 1832 ns/op
BenchmarkAssembleUrlWeightNew-4 563392 1896 ns/op
大概提升 18 倍性能,而且这可能还是比较差的情况,如果传入 lidcWeight = 1,效果更好。
效果
优化完,对改动方法写了相应的单元测试,确认没问题后,上线进行观察,CPU Idle(空闲率) 提升了10%以上
最后
其实本文展示的是一个 Go 程序非常常规的性能优化,也是相对来说比较简单,看完后,大家可能还有疑问:
为什么要在推送和拉取的时候去解析 url 呢?不能事先算好存起来吗?
为什么只优化了这点,其他的点是否也可以优化呢?
针对第一个问题,其实这是个历史问题,当你接手系统时他就是这样,如果程序出问题,你去改整个机制,可能周期比较长,而且容易出问题
第二个问题,其实刚也顺带回答了,这样优化,改动最小,收益最大,别的点没这么好改,短期来说,拿收益最重要。当然我们后续也打算对这个系统进行重构,但重构之前,这样优化,足以解决问题。
有道无术,术可成;有术无道,止于术
欢迎大家关注Java之道公众号
好文章,我在看❤️
记一次提升18倍的性能优化相关推荐
- 带你重走 TiDB TPS 提升 1000 倍的性能优化之旅
今天我们来聊一下数据库的性能优化,第一部分简单介绍一下性能优化的通用的方法,第二部分我们讲一个实际案例. 性能优化这个事情核心只有一句话,用户响应时间去哪儿了?性能优化很困难的原因在于,为了定位用户响 ...
- 【云原生AI】Fluid + JindoFS 助力微博海量小文件模型训练速度提升 18 倍
简介: 深度学习平台在微博社交业务扮演着重要的角色.计算存储分离架构下,微博深度学习平台在数据访问与调度方面存在性能低效的问题.本文将介绍微博内部设计实现的一套全新的基于 Fluid(内含 Jindo ...
- 5秒到1秒,记一次效果“非常”显著的性能优化
性能优化,有时候看起来是一个比较虚的技术需求.除非代码慢的已经让人无法忍受,否则,很少有公司会有觉悟投入资源去做这些工作.即使你有了性能指标数据,也很难说服领导做一个由耗时300ms降低到150ms的 ...
- Python爬虫多线程提升数据下载的性能优化
Pyhton爬虫多线程提升数据下载的性能优化 很幸运地上了两次Tony老师关于python爬虫的课(收获巨多),在这里我对第一次课做一下知识总结: 1.什么是爬虫? 自动从网络上进行数据采集的程序 一 ...
- python写spark的效率问题_“大数据架构”Spark 3.0发布,重大变化,性能提升18倍...
我们激动地宣布,作为Databricks运行时7.0的一部分,可以在Databricks上使用Apache SparkTM 3.0.0版本.3.0.0版本包含超过3400个补丁,是开源社区做出巨大贡献 ...
- 谷歌丰田联合成果ALBERT了解一下:新轻量版BERT,参数小18倍,性能依旧SOTA
作者 | Less Wright 编译 | ronghuaiyang 来源 | AI公园(ID:AI_Paradise) [导读]这是来自Google和Toyota的新NLP模型,超越Bert,参数小 ...
- 记一次息屏指纹解锁性能优化实现
需求背景 .客户反馈我司的指纹解锁机器冷屏(息屏)解锁下速度太慢,体验很差,而对比机却非常快 .对比发现,我司机器跟市面品牌机的冷屏解锁速度差了不是一个等级, 急待改善. 既然要优化功能,首先要做到理 ...
- 首个中文Stable Diffusion模型开源;TPU演进十年;18个PyTorch性能优化技巧 | AI系统前沿动态...
1. TPU演进十年:Google的十大经验教训 希腊神话中,特洛伊战争的起因是两方争夺世界上最美的女人--海伦,后世诗人将海伦的美貌"令成千战舰为之起航".TPU就像海伦,它的出 ...
- 记一次对 Laravel-permission 项目的性能优化
我最近研究分析了在 SWIS上面创建的项目的性能.令人惊讶的是,最耗费性能的方法之一是优秀的 spatie/laravel-permission 包造成的. 经过查阅更多资料和研究,发现一个可能明显 ...
最新文章
- IROS 2021 | 具有挑战性的Hilti SLAM数据集
- 复杂性系统面临的难题
- python中文软件-Python编程软件下载
- div 居中,浏览器兼容性
- 如何把导航条做成sitefactory政府版的样子实现动态读取子栏目显示
- GNU Wget 命令及其参数说明
- 接受map_[译] 图解 Map、Reduce 和 Filter 数组方法
- [渝粤教育] 西南科技大学 中国现代文学 在线考试复习资料
- dataframe 筛选_Spark.DataFrame与Spark.ML简介
- 比特币 源代码_GitHub遭黑客攻击:窃取数百源码并勒索比特币,数量惊人!
- RandomAccess接口
- 【学习笔记】【OC语言】继承
- Word标题:自动编号
- HDU2825-AC自动机+状压dp
- 正则匹配某字符前的内容
- Matlab中矩阵的右上角有一撇表示什么意思
- 在线教育平台建立付费会员体系
- 【linux】一文总结linux的环境变量
- ubuntu linux通过rclone 挂载onedrive 到本地磁盘
- undeclared here (not in a function)
热门文章
- centos8.4 nginx 问题
- java 缓冲区溢出_基于数组越界的缓冲区溢出
- dnf会修改跨区服务器吗,dnf新跨区系统上线之后,整个游戏只有9个大区
- linux脚本里使用sftp,如何在shell脚本里使用sftp批量传送文件
- ipython和jupyter_IPython与Jupyter Notebook以及Anaconda
- (王道408考研操作系统)第五章输入/输出(I/O)管理-第一节6:设备的分配和回收
- (王道408考研操作系统)第三章内存管理-第一节6-2:非连续分配管理方式之基本分页存储管理之基本地址变换机构
- 回溯算法之全排列问题
- 推动Windows的限制:物理内存
- 任意文件上传之绕过云waf+本地防火墙双重防护