日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

一个95分位延迟要求5ms的场景,如何做性能优化

發布時間:2024/4/11 编程问答 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 一个95分位延迟要求5ms的场景,如何做性能优化 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

組內的數據系統在承接一個業務需求時無法滿足性能需求,于是針對這個場景做了一些優化,在此寫篇文章做記錄。

業務場景是這樣:調用方一次獲取某個用戶的幾百個特征(可以把特征理解為屬性),特征以 redis hash 的形式存儲在持久化 KV 數據庫中,特征數據以天級別為更新粒度。要求 95 分位的延遲在 5ms 左右。

這個數據系統屬于無狀態的服務,為了增大吞吐量和降低延遲,從存儲和代碼兩方面進行優化。

存儲層面

存儲層面,一次調用一個用戶的三百個特征原方案是用 redis hash 做表,每個 field 為用戶的一個特征。由于用戶單個請求會獲取幾百個特征,即使用hmget做合并,存儲也需要去多個 slot 中獲取數據,效率較低,于是對數據進行歸一化,即:把 hash 表的所有 filed 打包成一個 json 格式的 string,舉個例子:

//?優化前的特征為?hash?格式 hash?key?:?user_2837947 127.0.0.1:6379>?hgetall?user_2837947 1)?"name"????//?特征1 2)?"薯條"?????//?特征1的值 3)?"age"????//?特征2 4)?"18"?????//?特征2的值 5)?"address"?//?特征3 6)?"China"???//?特征3的值//?優化后的特征為?string?json格式 string?key:?user_2837947 val: {"name":"薯條","age":18,"address":"China" }

特征進行打包后解決了一次請求去多個 slot 獲取數據時延較大的問題。但是這樣做可能帶來新的問題:若 hash filed 過多,string 的 value 值會很大。目前想到的解法有兩種,一種是按照類型將特征做細分,比如原來一個 string 里面有 300 的字段,拆分成 3 個有 100 個值的 string 類型。第二種是對 string val 進行壓縮,在數據存儲時壓縮存儲,讀取數據時在程序中解壓縮。這兩種方法也可以結合使用。

如果這樣仍不能滿足需求,可以在持久化 KV 存儲前再加一層緩存,緩存失效時間根據業務特點設置,這樣程序交互的流程會變成這樣:


代碼層面

接著來優化一下代碼。首先需要幾個工具去協助我們做性能優化。首先是壓測工具,壓測工具可以模擬真實流量,在預估的 QPS 下觀察系統的表現情況。發壓時注意漸進式加壓,不要一下次壓得太死。

然后還需要 profiler 工具。Golang 的生態中相關工具我們能用到的有 pprof 和 trace。pprof 可以看 CPU、內存、協程等信息在壓測流量進來時系統調用的各部分耗時情況。而 trace 可以查看 runtime 的情況,比如可以查看協程調度信息等。本次優化使用 壓測工具+pprof 的 CPU profiler。

下面來看一下 CPU 運行耗時情況:

右側主要是 runtime 部分,先忽略

火焰圖中圈出來的大平頂山都是可以優化的地方,

這里的三座平頂山的主要都是json.Marshal和json.Unmarshal操作引起的,對于 json 的優化,有兩種思路,一種是換個高性能的 json 解析包 ,另一種是根據業務需求看能否繞過解析。下面分別來介紹:

高性能解析包+一點黑科技

這里使用了陶師傅的包github.com/json-iterator/go。看了他的 benchmark 結果,比 golang 原生庫還是要快很多的。自己再寫個比較符合我們場景的Benchmark看陶師傅有沒有騙我們:

package?mainimport?("encoding/json"jsoniter?"github.com/json-iterator/go""testing" )var?s?=?`{....300多個filed..}`func?BenchmarkDefaultJSON(b?*testing.B)?{for?i?:=?0;?i?<?b.N;?i++?{param?:=?make(map[string]interface{})_?=?json.Unmarshal([]byte(s),?&param)} }func?BenchmarkIteratorJSON(b?*testing.B)?{for?i?:=?0;?i?<?b.N;?i++?{param?:=?make(map[string]interface{})var?json?=?jsoniter.ConfigCompatibleWithStandardLibrary_?=?json.Unmarshal([]byte(s),?&param)} }

運行結果:

這個包易用性也很強,在原來 json 代碼解析的上面加一行代碼就可以了:

var?json?=?jsoniter.ConfigCompatibleWithStandardLibrary err?=?json.Unmarshal(datautil.String2bytes(originData),?&fieldMap

還有一個可以優化的地方是string和[]byte之間的轉化,我們在代碼里用的參數類型是string,而 json 解析接受的參數是[]byte,所以一般在json解析時需要進行轉化:

err?=?json.Unmarshal([]byte(originData),?&fieldMap)

那么string轉化為[]byte發生了什么呢。

package?mainfunc?main(){a?:=?"string"b?:=?[]byte(a)println(b) }

我們用匯編把編譯器悄悄做的事抓出來:

來看一下這個函數做了啥:

這里底層會發生拷貝現象,我們可以拿到[]byte和string的底層結構后,用黑科技去掉拷貝過程:

func?String2bytes(s?string)?[]byte?{x?:=?(*[2]uintptr)(unsafe.Pointer(&s))h?:=?[3]uintptr{x[0],?x[1],?x[1]}return?*(*[]byte)(unsafe.Pointer(&h)) }func?Bytes2String(b?[]byte)?string?{return?*(*string)(unsafe.Pointer(&b)) }

下面寫 benchmark 看一下黑科技好不好用:

package?mainimport?("strings""testing" )var?s?=?strings.Repeat("hello",?1024)func?testDefault()?{a?:=?[]byte(s)_?=?string(a) }func?testUnsafe()?{a?:=?String2bytes(s)_?=?Bytes2String(a) }func?BenchmarkTestDefault(b?*testing.B)?{for?i?:=?0;?i?<?b.N;?i++?{testDefault()} }func?BenchmarkTestUnsafe(b?*testing.B)?{for?i?:=?0;?i?<?b.N;?i++?{testUnsafe()} }

運行速度,內存分配上效果都很明顯,黑科技果然黑:

加 cache,空間換時間

項目中有一塊代碼負責處理 N 個請求中的參數。代碼如下:

for?_,?item?:=?range?items?{var?params?map[string]stringerr?:=?json.Unmarshal([]byte(items[1]),?&params)if?err?!=?nil?{...} }

在這個需要優化的場景中,上游在單次請求獲取某個用戶300多個特征,如果用上面的代碼我們需要json.Unmarshal300多次,這是個無用且非常耗時的操作,可以加 cache 優化一下:

paramCache?:=?make(map[string]map[string]string)for?_,?item?:=?range?items?{var?params?map[string]stringtmpParams,?ok?:=?cacheDict[items[1]]//?沒有解析過,進行解析if?ok?==?false?{err?:=?json.Unmarshal([]byte(items[1]),?&params)if?err?!=?nil?{...}cacheDict[items[1]]?=?params}?else?{//?解析過,copy出一份//?這里的copy是為了預防并發問題params?=?DeepCopyMap(tmpParams)}}

這樣理論上不會存在任何的放大現象,讀者朋友如果有批處理的接口,代碼中又有類似這樣的操作,可以看下這里是否有優化的可能性。

for?{dosomething() }

替換耗時邏輯

火焰圖中的 TplToStr 模板函數同樣占到了比較大的 CPU 耗時,此函數的功能是把用戶傳來的參數和預制的模板拼出一個新的 string 字符串,比如:

入參:Tpl: shutiao_test_{{user_id}} user_id: 123478 返回:shutiao_test_123478

在我們的系統中,這個函數根據模板和用戶參數拼出一個 flag,根據這個 flag 是否相同作為某個操作的標記。這個拼模板是一個非常耗時的操作,這塊可以直接用字符串拼接去代替模板功能,比如:

入參:Tpl: shutiao_test_{{user_id}} user_id: 123478 返回:shutiao_test_user_id_123478

優化完之后,火焰圖中已經看不到這個函數的平頂山了,直接節省了 5%的 CPU 的調用百分比。

prealloc

還發現一些 growslice 占得微量 cpu 耗時,本以為預分配可以解決問題,但做 benchmark 測試發現 slice 容量較小時是否做預分配在性能上差異不大:

package?mainimport?"testing"func?test(m?*[]string)?{for?i?:=?0;?i?<?300;?i++?{*m?=?append(*m,?string(i))} }func?BenchmarkSlice(b?*testing.B)?{for?i?:=?0;?i?<?b.N;?i++?{b.StopTimer()m?:=?make([]string,?0)b.StartTimer()test(&m)} }func?BenchmarkCapSlice(b?*testing.B)?{for?i?:=?0;?i?<?b.N;?i++?{b.StopTimer()m?:=?make([]string,?300)b.StartTimer()test(&m)} }

對于代碼中用到的 map 也可以做一些預分配,寫 map 時如果能確認容量盡量用 make 函數對容量進行初始化。

package?mainimport?"testing"func?test(m?map[string]string)?{for?i?:=?0;?i?<?300;?i++?{m[string(i)]?=?string(i)} }func?BenchmarkMap(b?*testing.B)?{for?i?:=?0;?i?<?b.N;?i++?{b.StopTimer()m?:=?make(map[string]string)b.StartTimer()test(m)} }func?BenchmarkCapMap(b?*testing.B)?{for?i?:=?0;?i?<?b.N;?i++?{b.StopTimer()m?:=?make(map[string]string,?300)b.StartTimer()test(m)} }

這個優化還是比較有效的:

異步化

接口流程中有一些不影響主流程的操作完全可以異步化,比如:往外發送的統計工作。在 golang 中異步化就是起個協程。

總結一下套路:

代碼層面的優化,是 us 級別的,而針對業務對存儲進行優化,可以做到 ms 級別的,所以優化越靠近應用層效果越好。對于代碼層面,優化的步驟是:

  • 壓測工具模擬場景所需的真實流量

  • pprof 等工具查看服務的 CPU、mem 耗時

  • 鎖定平頂山邏輯,看優化可能性:異步化,改邏輯,加 cache 等

  • 局部優化完寫 benchmark 工具查看優化效果

  • 整體優化完回到步驟一,重新進行 壓測+pprof 看效果,看 95 分位耗時能否滿足要求(如果無法滿足需求,那就換存儲吧~。

  • 另外推薦一個不錯的庫,這是 Golang 布道師 Dave Cheney 搞的用來做性能調優的庫,使用起來非常方便:https://github.com/pkg/profile,可以看 pprof和 trace 信息。有興趣讀者可以了解一下。


    最后,給自己打個廣告

    歡迎加入?隨波逐流的薯條?微信群。

    薯條目前有草帽群、木葉群、琦玉群,群交流內容不限于技術、投資、趣聞分享等話題。歡迎感興趣的同學入群交流。

    入群請加薯條的個人微信:709834997。并備注:加入薯條微信群。

    總結

    以上是生活随笔為你收集整理的一个95分位延迟要求5ms的场景,如何做性能优化的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。