背景最近負(fù)責(zé)的一個自研的 Dubbo 注冊中心經(jīng)常收到 CPU 使用率的告警,于是進(jìn)行了一波優(yōu)化,,效果還不錯,,于是打算分享下思考、優(yōu)化過程,,希望對大家有一些幫助,。 自研 Dubbo 注冊中心是個什么東西,我畫個簡圖大家稍微感受一下就好,,看不懂也沒關(guān)系,,不影響后續(xù)的理解。
回到今天的重點,這個注冊中心最近 CPU 使用率長期處于中高水位,,偶爾有應(yīng)用發(fā)布,,推送量大時,CPU 甚至?xí)淮驖M,。 以前沒感覺到,,是因為接入的應(yīng)用不多,最近幾個月應(yīng)用越接越多,,慢慢就達(dá)到了告警閾值,。 尋找優(yōu)化點由于這項目是 Go 寫的(不懂 Go 的朋友也沒關(guān)系,本文重點在算法的優(yōu)化,,不在工具的使用上),, 找到哪里耗 CPU 還是挺簡單的:打開 pprof 即可,去線上采集一段時間即可,。 具體怎么操作可以參考我之前的這篇文章,,今天文章中用到的知識和工具,這篇文章都能找到,。 CPU profile 截了部分圖,,其他的不太重要,,可以看到消耗 CPU 多的是
稍微解釋下,AssembleCategoryProviders 方法是構(gòu)造返回 Dubbo provider 的 url,,由于會在返回 url 時對其做一些處理(比如調(diào)整權(quán)重等),,會涉及到對這個 Dubbo url 的解析,。又由于推拉結(jié)合的模式,線上服務(wù)使用方越多,,這個處理的 QPS 就越大,,所以它占用了大部分 CPU 一點也不奇怪。 這兩個 redis 操作可能是序列化占用了 CPU,,更大頭在 assembleUrlWeight,,有點琢磨不透。 接下來我們就分析下 assembleUrlWeight 如何優(yōu)化,,因為他占用 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)行重新計算,,實現(xiàn)多機(jī)房流量按權(quán)重的分配,。 這個過程涉及到 url 參數(shù)的解析,,再進(jìn)行 weight 的計算,最后再還原為一個 url Dubbo 的 url 結(jié)構(gòu)和普通 url 結(jié)構(gòu)一致,,其特點是參數(shù)可能比較多,,沒有 CPU 主要就消耗在這兩次解析和最后的還原中,,我們看這兩次解析的目的就是為了拿到 url 中的 url.Parse 和 url.ParseQuery 都是 Go 官方提供的庫,各個語言也都有實現(xiàn),,其核心是解析 url 為一個對象,,方便地獲取 url 的各個部分。 如果了解信息熵這個概念,,其實你就大概知道這里面一定是可以優(yōu)化的,。 url.Parse 和 url.ParseQuery 在這個場景下解析肯定存在冗余,,冗余意味著 CPU 在做多余的事情。 因為一個 Dubbo url 參數(shù)通常是很多的,,我們只需要拿這兩個參數(shù),,而 url.Parse 解析了所有的參數(shù)。 舉個例子,,給定一個數(shù)組,,求其中的最大值,如果先對數(shù)組進(jìn)行排序,,再取最大值顯然是存在冗余操作的,。 排序后的數(shù)組不僅能取最大值,還能取第二大值,、第三大值...最小值,,信息存在冗余了,所以先排序肯定不是求最大值的最優(yōu)解,。 優(yōu)化優(yōu)化獲取 url 參數(shù)性能第一想法是,,不要解析全部 url,只拿相應(yīng)的參數(shù),,這就很像我們寫的算法題,,比如獲取 weight 參數(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) } 先對這兩個函數(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多倍 新寫的這個方法,,有兩個小細(xì)節(jié),,第一是返回值中區(qū)分了參數(shù)是否存在,這個后面會用到,;第二是字符串的操作用到了 優(yōu)化 url 寫入?yún)?shù)性能計算出 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) 全部優(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)化完,,對改動方法寫了相應(yīng)的單元測試,,確認(rèn)沒問題后,上線進(jìn)行觀察,,CPU Idle(空閑率) 提升了10%以上 最后其實本文展示的是一個 Go 程序非常常規(guī)的性能優(yōu)化,,也是相對來說比較簡單,看完后,,大家可能還有疑問:
針對第一個問題,其實這是個歷史問題,當(dāng)你接手系統(tǒng)時他就是這樣,,如果程序出問題,,你去改整個機(jī)制,可能周期比較長,,而且容易出問題 第二個問題,,其實剛也順帶回答了,這樣優(yōu)化,,改動最小,,收益最大,別的點沒這么好改,,短期來說,,拿收益最重要。當(dāng)然我們后續(xù)也打算對這個系統(tǒng)進(jìn)行重構(gòu),,但重構(gòu)之前,,這樣優(yōu)化,足以解決問題,。
文章來源:https://www.cnblogs.com/zhuochongdashi/archive/2021/11/21/15584270.html |
|
來自: 冒險的K > 《應(yīng)用文》