1. 
          

          1. 新聞動(dòng)態(tài)

            提升18倍的性能優(yōu)化

            網(wǎng)站優(yōu)化 發(fā)布者:ou3377 2021-12-11 09:14 訪(fǎng)問(wèn)量:138

            背景

            最近負責的一個(gè)自研的 Dubbo 注冊中心經(jīng)常收到 CPU 使用率的告警,于是進(jìn)行了一波優(yōu)化,效果還不錯,于是打算分享下思考、優(yōu)化過(guò)程,希望對大家有一些幫助。

            自研 Dubbo 注冊中心是個(gè)什么東西,我畫(huà)個(gè)簡(jiǎn)圖大家稍微感受一下就好,看不懂也沒(méi)關(guān)系,不影響后續的理解。

            圖片

            • Consumer 和 Provider 的服務(wù)發(fā)現請求(注冊、注銷(xiāo)、訂閱)都發(fā)給 Agent,由它全權代理
            • Registry 和 Agent 保持 Grpc 長(cháng)鏈接,長(cháng)鏈接的目的主要是 Provider 方有變更時(shí),能及時(shí)推送給相應的 Consumer。為了保證數據的正確性,做了推拉結合的機制,Agent 會(huì )每隔一段時(shí)間去 Registry 拉取訂閱的服務(wù)列表
            • Agent 和業(yè)務(wù)服務(wù)部署在同一臺機器上,類(lèi)似 Service Mesh 的思路,盡量減少對業(yè)務(wù)的入侵,這樣就能快速的迭代了

            回到今天的重點(diǎn),這個(gè)注冊中心最近 CPU 使用率長(cháng)期處于中高水位,偶爾有應用發(fā)布,推送量大時(shí),CPU 甚至會(huì )被打滿(mǎn)。

            以前沒(méi)感覺(jué)到,是因為接入的應用不多,最近幾個(gè)月應用越接越多,慢慢就達到了告警閾值。

            尋找優(yōu)化點(diǎn)

            由于這項目是 Go 寫(xiě)的(不懂 Go 的朋友也沒(méi)關(guān)系,本文重點(diǎn)在算法的優(yōu)化,不在工具的使用上), 找到哪里耗 CPU 還是挺簡(jiǎn)單的:打開(kāi) pprof 即可,去線(xiàn)上采集一段時(shí)間即可。

            具體怎么操作可以參考我之前的這篇文章,今天文章中用到的知識和工具,這篇文章都能找到。

            圖片

            CPU profile 截了部分圖,其他的不太重要,可以看到消耗 CPU 多的是 AssembleCategoryProviders方法,與其直接關(guān)聯(lián)的是

            • 2個(gè) redis 相關(guān)的方法
            • 1個(gè)叫assembleUrlWeight的方法

            稍微解釋下,AssembleCategoryProviders 方法是構造返回 Dubbo provider 的 url,由于會(huì )在返回 url 時(shí)對其做一些處理(比如調整權重等),會(huì )涉及到對這個(gè) Dubbo url 的解析。又由于推拉結合的模式,線(xiàn)上服務(wù)使用方越多,這個(gè)處理的 QPS 就越大,所以它占用了大部分 CPU 一點(diǎn)也不奇怪。

            這兩個(gè) redis 操作可能是序列化占用了 CPU,更大頭在 assembleUrlWeight,有點(diǎn)琢磨不透。

            接下來(lái)我們就分析下 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 是機房權重。根據配置的機房權重,將 url 中的 weight 進(jìn)行重新計算,實(shí)現多機房流量按權重的分配。

            這個(gè)過(guò)程涉及到 url 參數的解析,再進(jìn)行 weight 的計算,最后再還原為一個(gè) url

            Dubbo 的 url 結構和普通 url 結構一致,其特點(diǎn)是參數可能比較多,沒(méi)有 #后面的片段部分。

            圖片

            CPU 主要就消耗在這兩次解析和最后的還原中,我們看這兩次解析的目的就是為了拿到 url 中的 lidc_weight 和 weight 參數。

            url.Parse 和 url.ParseQuery 都是 Go 官方提供的庫,各個(gè)語(yǔ)言也都有實(shí)現,其核心是解析 url 為一個(gè)對象,方便地獲取 url 的各個(gè)部分。

            如果了解信息熵這個(gè)概念,其實(shí)你就大概知道這里面一定是可以?xún)?yōu)化的。Shannon(香農) 借鑒了熱力學(xué)的概念,把信息中排除了冗余后的平均信息量稱(chēng)為信息熵。

            圖片

            url.Parse 和 url.ParseQuery 在這個(gè)場(chǎng)景下解析肯定存在冗余,冗余意味著(zhù) CPU 在做多余的事情。

            因為一個(gè) Dubbo url 參數通常是很多的,我們只需要拿這兩個(gè)參數,而 url.Parse 解析了所有的參數。

            舉個(gè)例子,給定一個(gè)數組,求其中的最大值,如果先對數組進(jìn)行排序,再取最大值顯然是存在冗余操作的。

            排序后的數組不僅能取最大值,還能取第二大值、第三大值...最小值,信息存在冗余了,所以先排序肯定不是求最大值的最優(yōu)解。

            優(yōu)化

            優(yōu)化獲取 url 參數性能

            第一想法是,不要解析全部 url,只拿相應的參數,這就很像我們寫(xiě)的算法題,比如獲取 weight 參數,它只可能是這兩種情況(不存在 #,所以簡(jiǎn)單很多):

            • 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=,結束要么是&,要么直接到字符串尾,代碼就很好寫(xiě)了,先手寫(xiě)個(gè)解析參數的算法:

            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
            }

            原先獲取參數的方法可以摘出來(lái):

            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è)函數進(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 結果如下:

            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多倍

            新寫(xiě)的這個(gè)方法,有兩個(gè)小細節,第一是返回值中區分了參數是否存在,這個(gè)后面會(huì )用到;第二是字符串的操作用到了 strings.Builder,這也是實(shí)際測試的結果,使用 +或者 fmt.Springf 性能都沒(méi)這個(gè)好,感興趣可以測試下看看。

            優(yōu)化 url 寫(xiě)入參數性能

            計算出 weight 后再把 weight 寫(xiě)入 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 兩種情況來(lái)討論:

            • url 本身不存在 weight 參數,則直接在 url 后拼接一個(gè) weight 參數,當然要注意是否存在 ?
            • url 本身存在 weight 參數,則直接進(jìn)行字符串替換

            細心的你肯定又發(fā)現了,當 lidcWeight = 1 時(shí),直接返回,因為 lidcWeight = 1 時(shí),后面的計算其實(shí)都不起作用(Dubbo 權重默認為100),索性別操作,省點(diǎn) CPU。

            全部?jì)?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)
              }
             }
            }

            結果如下:

            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)方法寫(xiě)了相應的單元測試,確認沒(méi)問(wèn)題后,上線(xiàn)進(jìn)行觀(guān)察,CPU Idle(空閑率) 提升了10%以上

            圖片

            最后

            其實(shí)本文展示的是一個(gè) Go 程序非常常規的性能優(yōu)化,也是相對來(lái)說(shuō)比較簡(jiǎn)單,看完后,大家可能還有疑問(wèn):

            • 為什么要在推送和拉取的時(shí)候去解析 url 呢?不能事先算好存起來(lái)嗎?
            • 為什么只優(yōu)化了這點(diǎn),其他的點(diǎn)是否也可以?xún)?yōu)化呢?

            針對第一個(gè)問(wèn)題,其實(shí)這是個(gè)歷史問(wèn)題,當你接手系統時(shí)他就是這樣,如果程序出問(wèn)題,你去改整個(gè)機制,可能周期比較長(cháng),而且容易出問(wèn)題

            圖片

            第二個(gè)問(wèn)題,其實(shí)剛也順帶回答了,這樣優(yōu)化,改動(dòng)最小,收益最大,別的點(diǎn)沒(méi)這么好改,短期來(lái)說(shuō),拿收益最重要。當然我們后續也打算對這個(gè)系統進(jìn)行重構,但重構之前,這樣優(yōu)化,足以解決問(wèn)題。



            關(guān)鍵字: 性能優(yōu)化 CPU使用率

            文章連接: http://www.gostscript.com/wzyh/803.html

            版權聲明:文章由 晨展科技 整理收集,來(lái)源于互聯(lián)網(wǎng)或者用戶(hù)投稿,如有侵權,請聯(lián)系我們,我們會(huì )立即刪除。如轉載請保留

            双腿国产亚洲精品无码不卡|国产91精品无码麻豆|97久久久久久久极品|无码人妻少妇久久中文字幕
                1.