RaftKeeper 是一款高新能分布式共識(shí)服務(wù),完全兼容 Zookeeper 但性能更出色,更多關(guān)于 RaftKeeer 參考Github,我們將 RaftKeeper 大規(guī)模應(yīng)用到 ClickHouse 場(chǎng)景中,用于解決 ZooKeeper 的性能瓶頸問題,同時(shí) RaftKeeper 也可以用于其它大數(shù)據(jù)組件比如 HBase。
v2.1.0 作為 v2.0.0 后的重要版本,引入了一系列新特性,包括異步創(chuàng)建 snapshot。該版本的最大亮點(diǎn)在于性能優(yōu)化:寫請(qǐng)求性能提升 11%,讀寫混合場(chǎng)景更是大幅提升了 118% 。本文將從工程細(xì)節(jié)的角度深入解析新版本的改進(jìn)與優(yōu)化。
一、性能優(yōu)化效果
在性能測(cè)試中,我們使用了raftkeeper-bench工具,測(cè)試環(huán)境為三個(gè)節(jié)點(diǎn)組成的集群,每個(gè)節(jié)點(diǎn)配置為 16 核 CPU、32GB 內(nèi)存和 100GB 存儲(chǔ)空間。測(cè)試對(duì)象包括 RaftKeeper v2.1.0、RaftKeeper v2.0.4 和 ZooKeeper 3.7.1,均采用默認(rèn)配置。
測(cè)試分為兩組:
第一組測(cè)試純 create 操作的性能,create 操作的 value 大小為 100 字節(jié)。結(jié)果顯示,RaftKeeper v2.1.0 相較于 v2.0.4 性能提升了 11%,相較于 ZooKeeper 性能提升了 143%。
第二組請(qǐng)求比例為 create-1%、set-8%、get-45%、list-45%、delete-1%。其中,list 請(qǐng)求結(jié)果包含 100 個(gè)子節(jié)點(diǎn),每個(gè)子節(jié)點(diǎn)大小為 50 字節(jié);get、set、create 請(qǐng)求的節(jié)點(diǎn) value 大小為 100 字節(jié)。結(jié)果顯示,RaftKeeper v2.1.0 相較于 v2.0.4 性能提升了 118%,相較于 ZooKeeper 性能提升了 198%。
rk2.1.0 版本在測(cè)試中 avgRT 和 TP99 指標(biāo)均優(yōu)于 rk2.0.4,具體可以參考測(cè)試報(bào)告。
二、性能優(yōu)化
接下來從工程細(xì)節(jié)的角度,介紹一些 v2.1.0 的優(yōu)化點(diǎn)。
1. 響應(yīng)并行序列化
RaftKeeper 被我們廣泛應(yīng)用到 ClickHouse 中,下圖是一個(gè)規(guī)模較大的 RaftKeeper 集群的火焰圖,通過火焰圖發(fā)現(xiàn) ResponseThread 線程消耗不少 CPU 時(shí)間片,其中大概三分之一時(shí)間片用于序列化響應(yīng)。
ResponseThread 負(fù)責(zé)序列化響應(yīng)并且轉(zhuǎn)發(fā)給 IO 線程,它是一個(gè)單線程,串行執(zhí)行序列化會(huì)增大延遲。我們可以把響應(yīng)的序列化交給 IO 線程來做,以并發(fā)的方式提高吞吐。
同時(shí)可以看到sdallocx_default函數(shù)占用了不少時(shí)間片,該函數(shù)是 jemelloc 釋放內(nèi)存的函數(shù),函數(shù)對(duì)于時(shí)間片的消耗沒有問題,但是該操作在基于 mutex 的同步隊(duì)列中執(zhí)行會(huì)增加鎖的時(shí)間。
/// responses_queue是一個(gè)基于mutex的同步隊(duì)列,在tryPop方法中釋放response_for_session會(huì)增加lock的時(shí)間
復(fù)制代碼
解決的方式是在 tryPop 方法前先釋放 response_for_session 的內(nèi)存空間。
下面的表格展示了優(yōu)化前后的性能指標(biāo),測(cè)試共有四組每組使用不同的并發(fā)度,其中響應(yīng)大小為 50bytes,當(dāng)并發(fā)度為 10 的時(shí)候,TPS 增加 31%,AvgRT 降低 32%。
2. 優(yōu)化 List 請(qǐng)求
依然是同一個(gè) RaftKeeper 集群,通過火焰圖發(fā)現(xiàn),List 請(qǐng)求處理幾乎消耗了 request-processor 線程所有的 CPU 時(shí)間片。在 RaftKeeper 的執(zhí)行鏈路中 request-processor 負(fù)責(zé)處理用戶的請(qǐng)求,它是一個(gè)單線程,所以比較容易成為瓶頸點(diǎn)。
通過火焰圖可以發(fā)現(xiàn)兩個(gè)瓶頸點(diǎn):1.為字符串分配內(nèi)存空間;2.插入 vector。
List 請(qǐng)求返回的結(jié)果是一個(gè) std::vector動(dòng)態(tài)數(shù)組,其內(nèi)存 layout 如下圖所示,每個(gè)成員是一個(gè)字符串,每個(gè)字符串需要分配一塊動(dòng)態(tài)內(nèi)存用于保存數(shù)據(jù),所以當(dāng)字符串多的時(shí)候需要大量的動(dòng)態(tài)內(nèi)存分配。
一個(gè)很直觀的優(yōu)化思路,可以設(shè)計(jì)一個(gè) compact strings,數(shù)據(jù)采用緊湊的方式存儲(chǔ),在以下的設(shè)計(jì)中,采用兩個(gè)連續(xù)內(nèi)存空間,一個(gè)用于存儲(chǔ)數(shù)據(jù),一個(gè)用于存儲(chǔ) offset,具體參考:CompactStrings實(shí)現(xiàn)。
優(yōu)化后從火焰圖方面看 List 請(qǐng)求處理在 CPU 的占比從 5.46%下降到 3.37%,進(jìn)行 List 請(qǐng)求的 benchmark 測(cè)試,TPS 從 45.8w/s 增長(zhǎng)到 61.9w/s,同時(shí) TP99 更低。
優(yōu)化前:
復(fù)制代碼
3. 優(yōu)化無用的系統(tǒng)調(diào)用
系統(tǒng)調(diào)用會(huì)引起用戶態(tài)和內(nèi)核態(tài)的上下文切換,往往系統(tǒng)調(diào)用函數(shù)會(huì)有比較大的開銷,我們通過 bpftrace 對(duì) RaftKeeper 進(jìn)行了 profile
BPFTRACE_MAX_PROBES=1024 bpftrace -p 4179376 -e '
復(fù)制代碼
發(fā)現(xiàn)大量的getsockname和getsockopt系統(tǒng)調(diào)用占用了不少開銷。
Execution count:
復(fù)制代碼
這些系統(tǒng)調(diào)用本不該存在,經(jīng)過排查發(fā)現(xiàn)是在打印日志的時(shí)候錯(cuò)誤的進(jìn)行了調(diào)用。
const auto socket_name = sock.isStream() ? sock.address().toString() : sock.peerAddress().toString();
復(fù)制代碼
4. 線程池優(yōu)化
下圖是一次 benchmark(讀寫 4:6 的比例)RaftKeeper 的火焰圖,進(jìn)行性能瓶頸分析發(fā)現(xiàn),發(fā)現(xiàn) request-processor 線程的 CPU 時(shí)間片大部分時(shí)間(超過 60%)消耗在條件變量等待的調(diào)用。
在 RaftKeeper 的主執(zhí)行鏈路中 request-processor 線程負(fù)責(zé)處理用戶請(qǐng)求,它的主要流程可以簡(jiǎn)單抽象為:1. 對(duì)于寫請(qǐng)求,單線程處理;2. 對(duì)于讀請(qǐng)求,通過線程池并發(fā)處理,然后調(diào)用 request_thread->wait()阻塞等待所有讀取請(qǐng)求完成。
/// 1. process read-request by a thread pool
復(fù)制代碼
增加監(jiān)控指標(biāo)分別統(tǒng)計(jì)讀和寫請(qǐng)求的執(zhí)行時(shí)間發(fā)現(xiàn),在讀請(qǐng)求和寫請(qǐng)求數(shù)量幾乎相同的情況下,讀請(qǐng)求的處理延時(shí)是寫請(qǐng)求的 3 倍。
因?yàn)槊總€(gè)請(qǐng)求的處理時(shí)間很短,到這里可以推測(cè)出,線程池任務(wù)調(diào)度的時(shí)間不可忽視,所以出現(xiàn)了性能下降。解決方式是去掉線程池,單線程處理讀請(qǐng)求,以下 benchmark 是優(yōu)化前后 benchmark 結(jié)果,TPS 提升 13%。
優(yōu)化前:
復(fù)制代碼
三、Snapshot 優(yōu)化
1. 異步 snapshot
在 RaftKeeper 整個(gè)請(qǐng)求處理鏈路中,創(chuàng)建 snapshot 是在主鏈路中進(jìn)行處理的,當(dāng)數(shù)據(jù)量大的時(shí)候會(huì)長(zhǎng)時(shí)間阻塞用戶請(qǐng)求,造成請(qǐng)求超時(shí)、leader 切換等引起服務(wù)不可用的問題,在我們線上場(chǎng)景中對(duì)于 6000w 的數(shù)據(jù)做 snapshot 需要 180s。
為了解決以上問題,新版本中支持了異步 snapshot,當(dāng)需要?jiǎng)?chuàng)建 snapshot 的時(shí)候首先將整個(gè) DataTree 拷貝一份,這一步在主線程中處理,然后在后臺(tái)將拷貝的 DataTree 序列化到磁盤中。
采用這用方式 6000w 的數(shù)據(jù)做 snaphot 對(duì)用戶的阻塞時(shí)間從 180s 降低到了 4.5s,但是這種方案也有一些負(fù)面效果,需要額外消耗大于 50%的內(nèi)存。
為了進(jìn)一步降低對(duì)用戶的阻塞時(shí)間,對(duì) DataTree 拷貝進(jìn)行了進(jìn)一步優(yōu)化。DataTree 拷貝其實(shí)是一個(gè)計(jì)算密集型的任務(wù),所以可以采用向量化的方式,同時(shí)會(huì)遍歷 hashmap 可以適當(dāng)進(jìn)行 prefetch。
inline void memcopy(char * __restrict dst, const char * __restrict src, size_t n)
復(fù)制代碼
上面的拷貝函數(shù)基于 SSE 指令集,優(yōu)化后 DataTree 拷貝時(shí)間從 4.5s 降低到 3.5s。
2. Snapshot 加載速度優(yōu)化
RaftKeeper 老版本中,啟動(dòng)服務(wù)之后 snapshot 加載速度比較慢,線上一個(gè)作為 ClickHouse metadata 存儲(chǔ)的 Raftkeeper 有 6kw 的數(shù)據(jù),在 NVMe 磁盤的服務(wù)器上加載 snapshot 需要 180s,導(dǎo)致服務(wù)啟動(dòng)速度很慢。
加載 snapshot 主要分兩步,第一步讀取磁盤上的數(shù)據(jù),反序列化成節(jié)點(diǎn);第二步遍歷 DataTree 并構(gòu)建父子關(guān)系,其中第一步是并行的,第二步是單線程的。
由于第二步是單線程執(zhí)行,可以改成并行的方式,并行化改造的基礎(chǔ)是 DataTree 是一個(gè)二層 HashMap 結(jié)構(gòu),改造后每個(gè)線程負(fù)責(zé)固定的 bucket,這樣避免了并發(fā)問題。具體流程為首先從磁盤讀取數(shù)據(jù)并按照 bucket 的粒度存儲(chǔ)節(jié)點(diǎn)和父子關(guān)系,然后填充 DataTree 并構(gòu)建父子關(guān)系。
優(yōu)化后加載 snapshot 時(shí)間從 180s 降低到 99s,之后又通過鎖優(yōu)化、snapshot 格式優(yōu)化、減少數(shù)據(jù)拷貝等手段將時(shí)間降低到 22s。
四、上線效果
我們選取線上一個(gè)對(duì) ZooKeeper 請(qǐng)求量大的 ClickHouse 集群,在 ClickHouse 測(cè)的監(jiān)控指標(biāo)看 QPS 大概為 17w/s,其中絕大部分為 List 請(qǐng)求。依次將其從 ZooKeeper 升級(jí)到 RaftKeeper v2.0.4 和 v2.1.0,觀察監(jiān)控指標(biāo)
可以看到 RaftKeeper v2.0.4 的表現(xiàn)不及 ZooKeeper(主要原因是該場(chǎng)景下絕大部分請(qǐng)求是 list,v2.0.4 對(duì)于 list 請(qǐng)求性能較差),但是 v2.1.0 有比較大幅的優(yōu)勢(shì)。
-
內(nèi)存
+關(guān)注
關(guān)注
8文章
3055瀏覽量
74331 -
性能
+關(guān)注
關(guān)注
0文章
271瀏覽量
19040 -
zookeeper
+關(guān)注
關(guān)注
0文章
34瀏覽量
3712
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論