新聞動(dòng)態(tài)
提升18倍的性能優(yōu)化
網(wǎng)站優(yōu)化 發(fā)布者:ou3377 2021-12-11 09:14 訪問量:173
最近負(fù)責(zé)的一個(gè)自研的 Dubbo 注冊中心經(jīng)常收到 CPU 使用率的告警,于是進(jìn)行了一波優(yōu)化,效果還不錯(cuò),于是打算分享下思考、優(yōu)化過程,希望對大家有一些幫助。
自研 Dubbo 注冊中心是個(gè)什么東西,我畫個(gè)簡圖大家稍微感受一下就好,看不懂也沒關(guān)系,不影響后續(xù)的理解。
回到今天的重點(diǎn),這個(gè)注冊中心最近 CPU 使用率長期處于中高水位,偶爾有應(yīng)用發(fā)布,推送量大時(shí),CPU 甚至?xí)淮驖M。
以前沒感覺到,是因?yàn)榻尤氲膽?yīng)用不多,最近幾個(gè)月應(yīng)用越接越多,慢慢就達(dá)到了告警閾值。
由于這項(xiàng)目是 Go 寫的(不懂 Go 的朋友也沒關(guān)系,本文重點(diǎn)在算法的優(yōu)化,不在工具的使用上), 找到哪里耗 CPU 還是挺簡單的:打開 pprof 即可,去線上采集一段時(shí)間即可。
具體怎么操作可以參考我之前的這篇文章,今天文章中用到的知識和工具,這篇文章都能找到。
CPU profile 截了部分圖,其他的不太重要,可以看到消耗 CPU 多的是 AssembleCategoryProviders
方法,與其直接關(guān)聯(lián)的是
assembleUrlWeight
的方法稍微解釋下,AssembleCategoryProviders 方法是構(gòu)造返回 Dubbo provider 的 url,由于會(huì)在返回 url 時(shí)對其做一些處理(比如調(diào)整權(quán)重等),會(huì)涉及到對這個(gè) Dubbo url 的解析。又由于推拉結(jié)合的模式,線上服務(wù)使用方越多,這個(gè)處理的 QPS 就越大,所以它占用了大部分 CPU 一點(diǎn)也不奇怪。
這兩個(gè) redis 操作可能是序列化占用了 CPU,更大頭在 assembleUrlWeight,有點(diǎn)琢磨不透。
接下來我們就分析下 assembleUrlWeight 如何優(yōu)化,因?yàn)樗加?CPU 最多,優(yōu)化效果肯定最好。
下面是 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 := 100
if 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 是機(jī)房權(quán)重。根據(jù)配置的機(jī)房權(quán)重,將 url 中的 weight 進(jìn)行重新計(jì)算,實(shí)現(xiàn)多機(jī)房流量按權(quán)重的分配。
這個(gè)過程涉及到 url 參數(shù)的解析,再進(jìn)行 weight 的計(jì)算,最后再還原為一個(gè) url
Dubbo 的 url 結(jié)構(gòu)和普通 url 結(jié)構(gòu)一致,其特點(diǎn)是參數(shù)可能比較多,沒有 #
后面的片段部分。
CPU 主要就消耗在這兩次解析和最后的還原中,我們看這兩次解析的目的就是為了拿到 url 中的 lidc_weight
和 weight
參數(shù)。
url.Parse 和 url.ParseQuery 都是 Go 官方提供的庫,各個(gè)語言也都有實(shí)現(xiàn),其核心是解析 url 為一個(gè)對象,方便地獲取 url 的各個(gè)部分。
如果了解信息熵這個(gè)概念,其實(shí)你就大概知道這里面一定是可以優(yōu)化的。Shannon(香農(nóng))
借鑒了熱力學(xué)的概念,把信息中排除了冗余后的平均信息量稱為信息熵
。
url.Parse 和 url.ParseQuery 在這個(gè)場景下解析肯定存在冗余,冗余意味著 CPU 在做多余的事情。
因?yàn)橐粋€(gè) Dubbo url 參數(shù)通常是很多的,我們只需要拿這兩個(gè)參數(shù),而 url.Parse 解析了所有的參數(shù)。
舉個(gè)例子,給定一個(gè)數(shù)組,求其中的最大值,如果先對數(shù)組進(jìn)行排序,再取最大值顯然是存在冗余操作的。
排序后的數(shù)組不僅能取最大值,還能取第二大值、第三大值...最小值,信息存在冗余了,所以先排序肯定不是求最大值的最優(yōu)解。
第一想法是,不要解析全部 url,只拿相應(yīng)的參數(shù),這就很像我們寫的算法題,比如獲取 weight 參數(shù),它只可能是這兩種情況(不存在 #,所以簡單很多):
要么是 &weight=
,要么是 ?weight=
,結(jié)束要么是&
,要么直接到字符串尾,代碼就很好寫了,先手寫個(gè)解析參數(shù)的算法:
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
}
原先獲取參數(shù)的方法可以摘出來:
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)
}
先對這兩個(gè)函數(shù)進(jìn)行 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 結(jié)果如下:
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多倍
新寫的這個(gè)方法,有兩個(gè)小細(xì)節(jié),第一是返回值中區(qū)分了參數(shù)是否存在,這個(gè)后面會(huì)用到;第二是字符串的操作用到了 strings.Builder
,這也是實(shí)際測試的結(jié)果,使用 +
或者 fmt.Springf
性能都沒這個(gè)好,感興趣可以測試下看看。
計(jì)算出 weight 后再把 weight 寫入 url 中,這里直接給出優(yōu)化后的代碼:
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 error
endpointWeight := 100
weightStr, err2 := GetUrlQueryParam(rawurl, "weight")
if weightStr != "" {
endpointWeight, err = strconv.Atoi(weightStr)
if err != nil {
endpointWeight = 100
}
}
if err2 != nil { // url中不存在weight
finUrl := 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中存在weight
oldWeightStr := 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 兩種情況來討論:
?
細(xì)心的你肯定又發(fā)現(xiàn)了,當(dāng) lidcWeight = 1
時(shí),直接返回,因?yàn)?nbsp;lidcWeight = 1
時(shí),后面的計(jì)算其實(shí)都不起作用(Dubbo 權(quán)重默認(rèn)為100),索性別操作,省點(diǎn) CPU。
全部優(yōu)化完,總體做一下 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)
}
}
}
結(jié)果如下:
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,效果更好。
優(yōu)化完,對改動(dòng)方法寫了相應(yīng)的單元測試,確認(rèn)沒問題后,上線進(jìn)行觀察,CPU Idle(空閑率) 提升了10%以上
其實(shí)本文展示的是一個(gè) Go 程序非常常規(guī)的性能優(yōu)化,也是相對來說比較簡單,看完后,大家可能還有疑問:
針對第一個(gè)問題,其實(shí)這是個(gè)歷史問題,當(dāng)你接手系統(tǒng)時(shí)他就是這樣,如果程序出問題,你去改整個(gè)機(jī)制,可能周期比較長,而且容易出問題
第二個(gè)問題,其實(shí)剛也順帶回答了,這樣優(yōu)化,改動(dòng)最小,收益最大,別的點(diǎn)沒這么好改,短期來說,拿收益最重要。當(dāng)然我們后續(xù)也打算對這個(gè)系統(tǒng)進(jìn)行重構(gòu),但重構(gòu)之前,這樣優(yōu)化,足以解決問題。
【推薦閱讀】
文章連接: http://www.hsjyfc.com.cn/wzyh/803.html
版權(quán)聲明:文章由 晨展科技 整理收集,來源于互聯(lián)網(wǎng)或者用戶投稿,如有侵權(quán),請聯(lián)系我們,我們會(huì)立即刪除。如轉(zhuǎn)載請保留