<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          記一次提升18倍的Dubbo性能優(yōu)化

          共 3321字,需瀏覽 7分鐘

           ·

          2021-11-30 14:42

          背景

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

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

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

          回到今天的重點,這個注冊中心最近 CPU 使用率長期處于中高水位,偶爾有應(yīng)用發(fā)布,推送量大時,CPU 甚至?xí)淮驖M。

          以前沒感覺到,是因為接入的應(yīng)用不多,最近幾個月應(yīng)用越接越多,慢慢就達到了告警閾值。

          尋找優(yōu)化點

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

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

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

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

          稍微解釋下,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 是機房權(quán)重。根據(jù)配置的機房權(quán)重,將 url 中的 weight 進行重新計算,實現(xiàn)多機房流量按權(quán)重的分配。

          這個過程涉及到 url 參數(shù)的解析,再進行 weight 的計算,最后再還原為一個 url

          Dubbo 的 url 結(jié)構(gòu)和普通 url 結(jié)構(gòu)一致,其特點是參數(shù)可能比較多,沒有 #后面的片段部分。

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

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

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

          url.Parse 和 url.ParseQuery 在這個場景下解析肯定存在冗余,冗余意味著 CPU 在做多余的事情。

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

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

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

          優(yōu)化

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

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

          • 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=,結(jié)束要么是&,要么直接到字符串尾,代碼就很好寫了,先手寫個解析參數(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ù)進行 benchmark:

          func?BenchmarkGetQueryParam(b?*testing.B)?{
          ?for?i?:=?0;?i???getParamByUrlParse(u,?"anyhost")
          ??getParamByUrlParse(u,?"version")
          ??getParamByUrlParse(u,?"not_exist")
          ?}
          }

          func?BenchmarkGetQueryParamNew(b?*testing.B)?{
          ?for?i?:=?0;?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多倍

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

          優(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 兩種情況來討論:

          • url 本身不存在 weight 參數(shù),則直接在 url 后拼接一個 weight 參數(shù),當(dāng)然要注意是否存在 ?
          • url 本身存在 weight 參數(shù),則直接進行字符串替換

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

          全部優(yōu)化完,總體做一下 benchmark:

          func?BenchmarkAssembleUrlWeight(b?*testing.B)?{
          ?for?i?:=?0;?i???for?_,?ut?:=?range?[]string{u,?u1,?u2,?u3}?{
          ???AssembleUrlWeight(ut,?60)
          ??}
          ?}
          }

          func?BenchmarkAssembleUrlWeightNew(b?*testing.B)?{
          ?for?i?:=?0;?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)的單元測試,確認沒問題后,上線進行觀察,CPU Idle(空閑率) 提升了10%以上

          最后

          其實本文展示的是一個 Go 程序非常常規(guī)的性能優(yōu)化,也是相對來說比較簡單,看完后,大家可能還有疑問:

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

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

          第二個問題,其實剛也順帶回答了,這樣優(yōu)化,改動最小,收益最大,別的點沒這么好改,短期來說,拿收益最重要。當(dāng)然我們后續(xù)也打算對這個系統(tǒng)進行重構(gòu),但重構(gòu)之前,這樣優(yōu)化,足以解決問題。

          瀏覽 41
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  91av影视 | 厂里宿舍少妇愉情视频 | 在线播放 亚洲 | 亚洲一本色| 久久免费少妇视频 |