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

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

计算密集型服务 性能优化实战始末

發(fā)布時(shí)間:2024/4/11 编程问答 39 豆豆
生活随笔 收集整理的這篇文章主要介紹了 计算密集型服务 性能优化实战始末 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

背景

項(xiàng)目背景

worker 服務(wù)數(shù)據(jù)鏈路圖

worker 服務(wù)消費(fèi)上游數(shù)據(jù)(工作日高峰期產(chǎn)出速度達(dá)近 200 MB/s,節(jié)假日高峰期可達(dá) 300MB/s 以上),進(jìn)行中間處理后,寫入多個(gè)下游。在實(shí)踐中結(jié)合業(yè)務(wù)場(chǎng)景,基于快慢隔離的思想,以三個(gè)不同的 consumer group 消費(fèi)同一 Topic,隔離三種數(shù)據(jù)處理鏈路。

面對(duì)問題

  • worker 服務(wù)在高峰期時(shí) CPU Idle 會(huì)降至 60%,因其屬于數(shù)據(jù)處理類計(jì)算密集型服務(wù),CPU Idle 過低會(huì)使服務(wù)吞吐降低,在數(shù)據(jù)處理上產(chǎn)生較大延時(shí),且受限于 Kafka 分區(qū)數(shù),無法進(jìn)行橫向擴(kuò)容;

  • 對(duì)上游數(shù)據(jù)的采樣率達(dá) **30%**,業(yè)務(wù)方對(duì)數(shù)據(jù)的完整性有較大訴求,但系統(tǒng) CPU 存在瓶頸,無法滿足;

性能優(yōu)化

針對(duì)以上問題,開始著手對(duì)服務(wù) CPU Idle 進(jìn)行優(yōu)化;抓取服務(wù) pprof profile 圖如下:go tool pprof -http=:6061 http://「ip:port」/debug/pprof/profile

優(yōu)化前的 pprof profile 圖

服務(wù)與存儲(chǔ)之間置換壓力

背景

worker 服務(wù)消費(fèi)到上游數(shù)據(jù)后,會(huì)先寫全部寫入 Apollo 的數(shù)據(jù)庫(kù),之后每分鐘定時(shí)撈取處理,但消息體大小 P99 分位達(dá)近 1.5M,對(duì)于 Apollo 有較嚴(yán)重的大 Key 問題,再結(jié)合 RocksDB 特有的寫放大問題,會(huì)進(jìn)一步加劇存儲(chǔ)壓力。在這個(gè)背景下,我們采用 zlib 壓縮算法,對(duì)消息體先進(jìn)行壓縮后再寫入 Apollo,減緩讀寫大 Key 對(duì) Apollo 的壓力。

優(yōu)化

在 CPU 的優(yōu)化過程中,我們發(fā)現(xiàn)服務(wù)在壓縮操作上占用了較多的 CPU,于是對(duì)壓縮等級(jí)進(jìn)行調(diào)整,以減小壓縮率、增大下游存儲(chǔ)壓力為代價(jià),減少壓縮操作對(duì)服務(wù) CPU 的占用,提升服務(wù) CPU 。這一操作本質(zhì)上是在服務(wù)與存儲(chǔ)之間進(jìn)行壓力置換,是一種空間換時(shí)間的做法。

關(guān)于壓縮等級(jí)

這里需要特別注意的是,在壓縮等級(jí)的設(shè)置上可能存在較為嚴(yán)重的邊際效用遞減問題。在進(jìn)行基準(zhǔn)測(cè)試時(shí)發(fā)現(xiàn),將壓縮等級(jí)由 BestCompression 調(diào)整為 DefaultCompression 后,壓縮率只有近 1? 的下降,但壓縮方面的 CPU 占用卻相對(duì)提高近 **50%**。此結(jié)論不能適用于所有場(chǎng)景,需從實(shí)際情況出發(fā),但在使用時(shí)應(yīng)注意這個(gè)問題,選擇相對(duì)最優(yōu)的壓縮方式。

zlib 可設(shè)置的壓縮等級(jí)

使用更高效的序列化庫(kù)

背景

worker 服務(wù)在設(shè)計(jì)之初基于快慢隔離的思想,使用三個(gè)不同的 consumer group 進(jìn)行分開消費(fèi),導(dǎo)致對(duì)同一份數(shù)據(jù)會(huì)重復(fù)消費(fèi)三次,而上游產(chǎn)出的數(shù)據(jù)是在 PB 序列化之后寫入 Kafka,消費(fèi)側(cè)亦需要進(jìn)行 PB 反序列化方能使用,因此導(dǎo)致了 PB 反序列化操作在 CPU 上的較大開銷。

優(yōu)化

經(jīng)過探討和調(diào)研后發(fā)現(xiàn),gogo/protobuf 三方庫(kù)相較于原生的 golang/protobuf 庫(kù)性能更好,在 CPU 上占用更低,速度更快,因此采用 gogo/protobuf 庫(kù)替換掉原生的 golang/protobuf 庫(kù)。

gogo/protobuf 為什么快

  • 通過對(duì)每一個(gè)字段都生成代碼的方式,取消了對(duì)反射的使用

  • 采用預(yù)計(jì)算方式,在序列化時(shí)能夠減少內(nèi)存分配次數(shù),進(jìn)而減少了內(nèi)存分配帶來的系統(tǒng)調(diào)用、鎖和 GC 等代價(jià)。

用過去或未來?yè)Q現(xiàn)在的時(shí)間:頁(yè)面靜態(tài)化、池化技術(shù)、預(yù)編譯、代碼生成等都是提前做一些事情,用過去的時(shí)間,來降低用戶在線服務(wù)的響應(yīng)時(shí)間;另外對(duì)于一些在線服務(wù)非必須的計(jì)算、存儲(chǔ)的耗時(shí)操作,也可以異步化延后進(jìn)行處理,這就是用未來的時(shí)間換現(xiàn)在的時(shí)間。出處:https://mp.weixin.qq.com/s/S8KVnG0NZDrylenIwSCq8g

關(guān)于序列化庫(kù)

這里只列舉的了 PB 序列化庫(kù)的優(yōu)化 Case,但在 JSON 序列化方面也存在一樣的優(yōu)化手段,如 json-iterator、sonic、gjson 等等,我們?cè)?Feature 服務(wù)中先后采用了 json-iterator 與 gjson 庫(kù)替換原有的標(biāo)準(zhǔn)庫(kù) JSON 序列化方式,均取得了顯著效果。JSON 庫(kù)調(diào)研報(bào)告:https://segmentfault.com/a/1190000041591284

調(diào)整壓縮等級(jí)與更換 PB 序列化庫(kù)之后

數(shù)據(jù)攢批 減少調(diào)用

背景

在觀察 pprof 圖后發(fā)現(xiàn)寫 hbase 占用了近 50% 的相對(duì) CPU,經(jīng)過進(jìn)一步分析后,發(fā)現(xiàn)每次在序列化一個(gè)字段時(shí) Thrift 都會(huì)調(diào)用一次 socket->syscall,帶來頻繁的上下文切換開銷。

優(yōu)化

閱讀代碼后發(fā)現(xiàn), 原代碼中使用了 Thrift 的 TTransport 實(shí)現(xiàn),其功能是包裝 TSocket,裸調(diào) Syscall,每次 Write 時(shí)都會(huì)調(diào)用 socket 寫入進(jìn)而調(diào)用 Syscall。這與通常我們的編碼習(xí)慣不符,認(rèn)為應(yīng)該有一個(gè) buffer 充當(dāng)中間層進(jìn)行數(shù)據(jù)攢批,當(dāng) buffer 寫完或者寫滿后再向下層寫入。于是進(jìn)一步閱讀 Thrift 源碼,發(fā)現(xiàn)其中有多種 Transport 實(shí)現(xiàn),而 TTBufferedTransport 是符合我們編碼習(xí)慣的。

對(duì) Thrift 調(diào)用進(jìn)行優(yōu)化,使用帶 buffer 的 transport,大大減少對(duì) Syscall的調(diào)用

更換 transport 之后,對(duì) HBase 的調(diào)用消耗只剩最右側(cè)的一條了

對(duì) HBase 的訪問耗時(shí)大幅下降

Thrift Client 部分源碼分析

Transport 使用了裝飾器模式

Transport 實(shí)現(xiàn)作用
TTransport包裝 TSocket,裸調(diào) Syscall,每次 Write 都會(huì)調(diào)用 syscall;
TTBufferedTransport需要提前聲明 buffer 的大小,在調(diào)用 Socket 之前加了一層 buffer,寫滿或者寫完之后再調(diào)用 Syscall;
TFramedTransport與 TTBufferedTransport 類似,但只會(huì)在全部寫入 buffer 后,再調(diào)用 Syscall。數(shù)據(jù)格式為:size+content,客戶端與服務(wù)端必須都使用該實(shí)現(xiàn),否則會(huì)因?yàn)楦袷讲患嫒輬?bào)錯(cuò);
streamTransport傳入自己實(shí)現(xiàn)的 IO 接口;
TMemoryBufferTransport純內(nèi)存交換,不與網(wǎng)絡(luò)交互;
ProtocolProtocol 實(shí)現(xiàn)作用
TBinaryProtocol直接的二進(jìn)制格式;
TCompactProtocol緊湊型、高效和壓縮的二進(jìn)制格式;
TJSONProtocolJSON 格式;
TSimpleJSONProtocolSimpleJSON 產(chǎn)生的輸出適用于 AJAX 或腳本語(yǔ)言,它不保留Thrift的字段標(biāo)簽,不能被 Thrift 讀回,它不應(yīng)該與全功能的 TJSONProtocol 相混淆;https://cwiki.apache.org/confluence/display/THRIFT/ThriftUsageJava

關(guān)于數(shù)據(jù)攢批

數(shù)據(jù)攢批:將數(shù)據(jù)先寫入用戶態(tài)內(nèi)存中,而后統(tǒng)一調(diào)用 syscall 進(jìn)行寫入,常用在數(shù)據(jù)落盤、網(wǎng)絡(luò)傳輸中,可降低系統(tǒng)調(diào)用次數(shù)、利用磁盤順序?qū)懱匦缘?#xff0c;是一種空間換時(shí)間的做法。有時(shí)也會(huì)犧牲一定的數(shù)據(jù)實(shí)時(shí)性,如 kafka producer 側(cè)。相似優(yōu)化可見:https://mp.weixin.qq.com/s/ntNGz6mjlWE7gb_ZBc5YeA

語(yǔ)法調(diào)整

除在對(duì)庫(kù)的使用上進(jìn)行優(yōu)化外,在 GO 語(yǔ)言本身的使用上也存在一些優(yōu)化方式;

  • slice、map 預(yù)初始化,減少頻繁擴(kuò)容導(dǎo)致的內(nèi)存拷貝與分配開銷;

  • 字符串連接使用 strings.builder(預(yù)初始化) 代替 fmt.Sprintf();

func?ConcatString(sl?...string)?string?{n?:=?0for?i?:=?0;?i?<?len(sl);?i++?{n?+=?len(sl[i])}var?b?strings.Builderb.Grow(n)for?_,?v?:=?range?sl?{b.WriteString(v)}return?b.String() }
  • buffer 修改返回 string([]byte) 操作為 []byte,減少內(nèi)存 []byte -> string 的內(nèi)存拷貝開銷;


  • string <-> []byte 的另一種優(yōu)化,需確保 []byte 內(nèi)容后續(xù)不會(huì)被修改,否則會(huì)發(fā)生 panic;

func?String2Bytes(s?string)?[]byte?{sh?:=?(*reflect.StringHeader)(unsafe.Pointer(&s))bh?:=?reflect.SliceHeader{Data:?sh.Data,Len:??sh.Len,Cap:??sh.Len,}return?*(*[]byte)(unsafe.Pointer(&bh)) }func?Bytes2String(b?[]byte)?string?{return?*(*string)(unsafe.Pointer(&b)) }

關(guān)于語(yǔ)法調(diào)整

更多語(yǔ)法調(diào)整,見以下文章

  • https://www.bacancytechnology.com/blog/golang-performance

  • https://mp.weixin.qq.com/s/Lv2XTD-SPnxT2vnPNeREbg

GC 調(diào)優(yōu)

背景

在上次優(yōu)化完成之后,系統(tǒng)已經(jīng)基本穩(wěn)定,CPU Idle 高峰期也可以維持在 80% 左右,但后續(xù)因業(yè)務(wù)訴求對(duì)上游數(shù)據(jù)采樣率調(diào)整至 100%,CPU.Idle 高峰期指標(biāo)再次下降至近 70%,且由于定時(shí)任務(wù)的問題,存在 CPU.Idle 掉 0 風(fēng)險(xiǎn);

優(yōu)化

經(jīng)過對(duì) pprof 的再次分析,發(fā)現(xiàn) runtime.gcMarkWorker 占用不合常理,達(dá)到近 30%,于是開始著手對(duì) GC 進(jìn)行優(yōu)化;

GC 優(yōu)化前 pprof 圖

方法一:使用 sync.pool()

通常來說使用 sync.pool() 緩存對(duì)象,減少對(duì)象分配數(shù),是優(yōu)化 GC 的最佳方式,因此我們?cè)陧?xiàng)目中使用其對(duì) bytes.buffer 對(duì)象進(jìn)行緩存復(fù)用,意圖減少 GC 開銷,但實(shí)際上線后 CPU Idle 卻略微下降,且 GC 問題并無緩解。原因有二:

  • sync.pool 是全局對(duì)象,讀寫存在競(jìng)爭(zhēng)問題,因此在這方面會(huì)消耗一定的 CPU,但之所以通常用它優(yōu)化后 CPU 會(huì)有提升,是因?yàn)樗膶?duì)象復(fù)用功能對(duì) GC 帶來的優(yōu)化,因此 sync.pool 的優(yōu)化效果取決于鎖競(jìng)爭(zhēng)增加的 CPU 消耗與優(yōu)化 GC 減少的 CPU 消耗這兩者的差值;

  • GC 壓力的大小通常取決于 inuse_objects,與 inuse_heap 無關(guān),也就是說與正在使用的對(duì)象數(shù)有關(guān),與正在使用的堆大小無關(guān);

  • 本次優(yōu)化時(shí)選擇對(duì) bytes.buffer 進(jìn)行復(fù)用,是想做到減少堆大小的分配,出發(fā)點(diǎn)錯(cuò)了,對(duì) GC 問題的理解有誤,對(duì) GC 的優(yōu)化因從 pprof heap 圖 inuse_objects 與 alloc_objects 兩個(gè)指標(biāo)出發(fā)。

    甚至沒有依賴經(jīng)驗(yàn), 只是單純的想當(dāng)然了🤦?♂?

    方法二:設(shè)置 GOGC

    原理:GOGC 默認(rèn)值是 100,也就是下次 GC 觸發(fā)的 heap 的大小是這次 GC 之后的 heap 的一倍,通過調(diào)大 GOGC 值(gcpercent)的方式,達(dá)到減少 GC 次數(shù)的目的;

    公式:gc_trigger = heap_marked * (1+gcpercent/100) gcpercent:通過 GOGC 來設(shè)置,默認(rèn)是 100,也就是當(dāng)前內(nèi)存分配到達(dá)上次存活堆內(nèi)存 2 倍時(shí),觸發(fā) GC;heap_marked:上一個(gè) GC 中被標(biāo)記的(存活的)字節(jié)數(shù);

    問題:GOGC 參數(shù)不易控制,設(shè)置較小提升有限,設(shè)置較大容易有 OOM 風(fēng)險(xiǎn),因?yàn)槎汛笮”旧硎窃趯?shí)時(shí)變化的,在任何流量下都設(shè)置一個(gè)固定值,是一件有風(fēng)險(xiǎn)的事情。這個(gè)問題目前已經(jīng)有解決方案,Uber 發(fā)表的文章中提到了一種自動(dòng)調(diào)整 GOGC 參數(shù)的方案,用于在這種方式下優(yōu)化 GO 的 GC CPU 占用,不過業(yè)界還沒有開源相關(guān)實(shí)現(xiàn)

    設(shè)置 GOGC 至 1000% 后,GC 占用大幅縮小

    內(nèi)存利用率接近 100%

    方法三:GO ballast 內(nèi)存控制

    ballast 壓艙物--航海。為提供所需的吃水和穩(wěn)定性而臨時(shí)或永久攜帶在船上的重型材料。來源:Dictionary.com

    原理:仍然是從利用了下次 GC 觸發(fā)的 heap 的大小是這次 GC 之后的 heap 的一倍這一原理,初始化一個(gè)生命周期貫穿整個(gè) Go 應(yīng)用生命周期的超大 slice,用于內(nèi)存占位,增大 heap_marked 值降低 GC 頻率;實(shí)際操作有以下兩種方式

    公式:gc_trigger = heap_marked * (1+gcpercent/100) gcpercent:通過 GOGC 來設(shè)置,默認(rèn)是 100,也就是當(dāng)前內(nèi)存分配到達(dá)上次存活堆內(nèi)存 2 倍時(shí),觸發(fā) GC;heap_marked:上一個(gè) GC 中被標(biāo)記的(存活的)字節(jié)數(shù);

    方式一

    方式二

    兩種方式都可以達(dá)到同樣的效果,但是方式一會(huì)實(shí)際占用物理內(nèi)存,在可觀測(cè)性上會(huì)更舒服一點(diǎn),方式二并不會(huì)實(shí)際占用物理內(nèi)存。

    原因:Memory in ‘nix (and even Windows) systems is virtually addressed and mapped through page tables by the OS. When the above code runs, the array the ballast slice points to will be allocated in the program’s virtual address space. Only if we attempt to read or write to the slice, will the page fault occur that causes the physical RAM backing the virtual addresses to be allocated. 引用自:https://blog.twitch.tv/en/2019/04/10/go-memory-ballast-how-i-learnt-to-stop-worrying-and-love-the-heap/

    優(yōu)化后 GC 頻率由每秒 5 次降低到了每秒 0.1 次

    使用 ballast 內(nèi)存控制后,GC 占用縮小至紅框大小

    使用方式一后,內(nèi)存始終穩(wěn)定在 25% -30%,即 3G 大小

    相比于設(shè)置 GOGC 的優(yōu)勢(shì)

    • 安全性更高,OOM 風(fēng)險(xiǎn)小;

    • 效果更好,可以從 pprof 圖看出,后者的優(yōu)化效果更大;

    負(fù)面考量問:雖然通過大切片占位的方式可以有效降低 GC 頻率,但是每次 GC 需要掃描和回收的對(duì)象數(shù)量變多了,是否會(huì)導(dǎo)致進(jìn)行 GC 的那一段時(shí)間產(chǎn)生耗時(shí)毛刺?答:不會(huì),GC 有兩個(gè)階段 mark 與 sweep,unused_objects 只與 sweep 階段有關(guān),但這個(gè)過程是非??焖俚?#xff1b;mark 階段是 GC 時(shí)間占用最主要的部分,但其只與當(dāng)前的 inuse_objects 有關(guān),與 unused_objects 無太大關(guān)系;因此,綜上所述,降低頻率確實(shí)會(huì)讓每次 GC 時(shí)的 unused_objects 有所增長(zhǎng),但并不會(huì)對(duì) GC 增加太多負(fù)擔(dān);

    關(guān)于 ballast 內(nèi)存控制更詳細(xì)的內(nèi)容請(qǐng)看:https://blog.twitch.tv/en/2019/04/10/go-memory-ballast-how-i-learnt-to-stop-worrying-and-love-the-heap/

    關(guān)于 GC 調(diào)優(yōu)

    • GC 優(yōu)化手段的優(yōu)先級(jí):設(shè)置 GOGC、GO ballast 內(nèi)存控制等操作是一種治標(biāo)不治本略顯 trick 的方式,在做 GC 優(yōu)化時(shí)還應(yīng)先從對(duì)象復(fù)用、減少對(duì)象分配角度著手,在確無優(yōu)化空間或優(yōu)化成本較大時(shí),再選擇此種方式;

    • 設(shè)置 GOGC、GO ballast 內(nèi)存控制等操作本質(zhì)上也是一種空間換時(shí)間的做法,在內(nèi)存與 CPU 之間進(jìn)行壓力置換;

    • 在 GC 調(diào)優(yōu)方面,還有很多其他優(yōu)化方式,如 bigcache 在堆內(nèi)定義大數(shù)組切片自行管理、fastcache 直接調(diào)用 syscall.mmap 申請(qǐng)堆外內(nèi)存使用、offheap 使用 cgo 管理堆外內(nèi)存等等。

    優(yōu)化效果

    黃色線:調(diào)整壓縮等級(jí)與更換 PB 序列化庫(kù);綠色線:Thrift 序列化更換帶 buffer 的 transport;

    藍(lán)色曲線抖動(dòng)是因?yàn)樯嫌螛I(yè)務(wù)放量,后又做垂直伸縮將 CPU 由 8 核提至 16 核 GC 優(yōu)化(紅框部分)

    • 先后將 CPU 提升 **25%、10%**(假設(shè)不做伸縮);

    • 支持上游數(shù)據(jù) 100% 放量;

    • 通過對(duì) CPU 瓶頸的解決,順利合并服務(wù),下掉 70 臺(tái)容器。

    總結(jié)

    經(jīng)驗(yàn)分享

    • 做性能優(yōu)化經(jīng)驗(yàn)很重要,其次在優(yōu)化之前掌握一部分前置知識(shí)更好;

    • 平時(shí)多看一些資料學(xué)習(xí),有優(yōu)化機(jī)會(huì)就抓住實(shí)踐,避免書到用時(shí)方恨少;

    • 仔細(xì)觀察 pprof 圖,分析大塊部分;

    • 觀察問題點(diǎn)的 api 使用,可能具有更高效的使用方式

    • 記錄優(yōu)化過程和優(yōu)化效果,以后分享、吹逼用的上;

    • 最好可以構(gòu)建穩(wěn)定的基準(zhǔn)環(huán)境,驗(yàn)證效果;

    • 空間換時(shí)間是萬(wàn)能鑰匙,工程問題不要只 case by case 的看,很多解決方案都是同一種思想的不同落地,嘗試去總結(jié)和掌握這種思想,最后達(dá)到遷移復(fù)用的效果;

    • 多和大佬討論,非常重要,以上多項(xiàng)優(yōu)化都出自與大佬(特別鳴謝 @李小宇@曹春暉)討論后的實(shí)踐;

    參考

    • gogo/protobuf:https://jishuin.proginn.com/p/763bfbd4f993

    • 改善 Go 語(yǔ)言編程質(zhì)量的 50 個(gè)有效實(shí)踐:第 15 章 注意Go 字符串是原生類型

    • 字節(jié)跳動(dòng) Go RPC 框架 KiteX 性能優(yōu)化實(shí)踐:https://mp.weixin.qq.com/s/Xoaoiotl7ZQoG2iXo9_DWg

    • 某高并發(fā)服務(wù) GOGC 及 UDP Pool 優(yōu)化:https://mp.weixin.qq.com/s/EuJ3Pw0s24Nr1h2edn5Sgg

    • 性能優(yōu)化 | Go Ballast 讓內(nèi)存控制更加絲滑:https://mp.weixin.qq.com/s/gc34RYqmzeMndEJ1-7sOwg

    • Go 自底向上的性能優(yōu)化實(shí)踐:https://www.bilibili.com/video/BV1GT4y127SC?spm_id_from=333.999.0.0

    • Go memory ballast: How I learnt to stop worrying and love the heap:https://blog.twitch.tv/en/2019/04/10/go-memory-ballast-how-i-learnt-to-stop-worrying-and-love-the-heap/

    總結(jié)

    以上是生活随笔為你收集整理的计算密集型服务 性能优化实战始末的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。