在并發(fā)編程里面玩祟,鎖是一個(gè)逃不開的東西腹缩,對(duì)于很多多線程共享的資源,使用鎖是一個(gè)非常的好的辦法空扎,來(lái)支持對(duì)這些資源安全的訪問(wèn)藏鹊。但鎖雖然方便,用不好就容易出現(xiàn)問(wèn)題转锈。
首先大家需要知道盘寡,對(duì)鎖單純的進(jìn)行 Lock 和 Unlock 操作其實(shí)是很快的,但如果涉及到了 Lock Contention撮慨,那么性能就會(huì)有較大的影響了竿痰。
Rust Prometheus Metric Vector
最開始注意到 Lock Contention 問(wèn)題應(yīng)該是在我們使用 Rust Prometheus 庫(kù)的時(shí)候,因?yàn)楫?dāng)時(shí)是按照 Go 的 official library 來(lái)設(shè)計(jì)的砌溺,所以對(duì) Metric Vector 類型的我們也是直接一個(gè)讀寫鎖來(lái)保證里面共享的 HashMap 的安全性影涉。
因?yàn)閹?kù)的細(xì)節(jié)很多同學(xué)不會(huì)關(guān)心,他們會(huì)自然的認(rèn)為 Prometheus 的 API 設(shè)計(jì)的比較高效规伐,所以在一些頻繁需要 metrics 的地方蟹倾,他們直接調(diào)用了 Prometheus 的 API,如果只是單線程還好猖闪,但悲催的是鲜棠,很多邏輯是會(huì)涉及到多線程操作的,然后我們就發(fā)現(xiàn)性能下降非常的嚴(yán)重培慌。譬如這個(gè) PR 就是發(fā)現(xiàn)了這樣的問(wèn)題才臨時(shí)修改的豁陆。
為了解決 Prometheus Metric Vector 的 Lock Contention 問(wèn)題,我們現(xiàn)在臨時(shí)的解決辦法就是使用 Local Metrics吵护,也就是先線程自己去記錄 metrics盒音,然后定期的 flush 到全局的 Prometheus metrics 里面竖配,這樣整體性能就好了很多,但其實(shí)這種做法很丑陋里逆。后面我們準(zhǔn)備參考 RocksDB 的做法进胯,使用 Per CPU 的 metrics 方案,也就是每個(gè) CPU 自己去統(tǒng)計(jì) metrics原押。無(wú)論是不是多線程胁镐,同時(shí)一個(gè)線程的時(shí)間片就只可能在一個(gè) CPU 上面,所以相關(guān)的 metrics 自然可以跟這個(gè) CPU 關(guān)聯(lián)诸衔,詳細(xì)可以參考 Core-local Statistics 這篇文章盯漂。
雖然我之前說(shuō)單純的進(jìn)行 Lock 和 Unlock 很快,但在一些特別高頻的地方笨农,還是會(huì)有影響就缆,所以對(duì)于一些非常快速的操作谒亦,我們后面開始使用 SpinLock竭宰。
Worker Thread Pool
上面說(shuō)了 Prometheus Metrics 遇到的坑,再來(lái)說(shuō)說(shuō)最近遇到的第二個(gè)問(wèn)題份招。大家知道切揭,我們通常會(huì)寫一個(gè) worker thread pool 來(lái)并發(fā)的處理我們的任務(wù),而通常的一個(gè) thread pool 的實(shí)現(xiàn)方式锁摔,就是使用 mutex + condition var 了廓旬,這個(gè)實(shí)現(xiàn)的代碼實(shí)在的太多了,大家直接 Google 吧谐腰,這里就不詳細(xì)說(shuō)明了孕豹。
在 TiKV 里面,我們也使用 mutex + condition var 來(lái)做了一個(gè) thread pool十气,然后這周我在跟另一個(gè)線程調(diào)度問(wèn)題励背,跟蔥頭討論的時(shí)候,突然想 benchmark 下我們的 thread pool 的極限性能桦踊,先寫了一個(gè)簡(jiǎn)單的測(cè)試椅野,測(cè)試很簡(jiǎn)單终畅,就是往一個(gè) channel 里面發(fā)數(shù)據(jù)籍胯,然后還有一個(gè)獨(dú)立的線程不停的收,每一秒統(tǒng)計(jì)收到的數(shù)據(jù)個(gè)數(shù)离福。
然后我開始 benchmark杖狼,悲催的發(fā)現(xiàn),QPS 不到 200 K妖爷,也就是 TiKV 現(xiàn)在極限情況下面單個(gè)節(jié)點(diǎn)最多能處理 200 K 的請(qǐng)求蝶涩,雖然還不錯(cuò)理朋,但我總覺(jué)得可以在高一點(diǎn)。因?yàn)槲覀兪?8 個(gè) worker thread绿聘。如果調(diào)整成 4 個(gè)嗽上,性能會(huì)更好,這個(gè)是正常的熄攘,因?yàn)閷?duì)于這種幾乎沒(méi)啥消耗的 task兽愤,CPU 執(zhí)行起來(lái)非常的快,這時(shí)候反倒是多個(gè)線程的 context switch 會(huì)影響到整體性能挪圾,關(guān)于線程調(diào)度這塊浅萧,以后我詳細(xì)研究了在整理出來(lái),這里先不討論了哲思。
然后我就想洼畅,仍然是 8 個(gè) worker thread,如何還能性能提升棚赔?而且在測(cè)試的使用帝簇,使用 top -H
觀察,會(huì)明顯發(fā)現(xiàn) CPU 打不滿靠益,而我的測(cè)試機(jī)器是 40 Core己儒,不存在性能問(wèn)題。我只能將我的目光鎖定到了 mutex 的 Lock Contention 上面捆毫,因?yàn)樵谖覀兊膶?shí)現(xiàn)中闪湾,給 task queue 添加任務(wù)以及從 worker thread 獲取任務(wù),都是在 mutex 里面進(jìn)行的绩卤,也就是每次操作都會(huì)極大的概率遇到 Lock Contention途样。
于是我先做了第一個(gè)優(yōu)化,將 task queue 從 mutex 里面移出來(lái)濒憋,使用 SpinLock何暇,測(cè)試發(fā)現(xiàn) QPS 上升到 230 K,然后用 perf record 兩次測(cè)試凛驮,diff 對(duì)比:
# Event 'cycles:ppp'
#
# Baseline Delta Shared Object Symbol
# ........ ....... ................... ...................................................................................
#
8.40% +7.58% [kernel.vmlinux] [k] _raw_spin_lock
3.29% -0.17% [kernel.vmlinux] [k] __schedule
1.02% +0.09% [kernel.vmlinux] [k] futex_wait_setup
0.96% -0.06% [kernel.vmlinux] [k] finish_task_switch
發(fā)現(xiàn)之前的測(cè)試比 SpinLock 的多了 7.5% 的 _raw_spin_lock
開銷裆站,這個(gè)當(dāng)然不是我們自己實(shí)現(xiàn)的 SpinLock,而是內(nèi)核 Mutex 里面的黔夭。
但即使這樣宏胯,我仍然發(fā)現(xiàn),worker thread 的 CPU 跑不滿本姥,懷疑是 worker 線程取任務(wù)太快肩袍,而 Producer 線程沒(méi)法那么快的把 task 加到隊(duì)列里面,于是 worker 線程因?yàn)闆](méi)有獲取到任務(wù)婚惫,只能重新進(jìn)入 condition wait 狀態(tài)氛赐。于是我暴力的改了一下獲取 task 的代碼魂爪,循環(huán) 10 次,如果沒(méi)取到任務(wù)艰管,就調(diào)用 thread 的 yield 函數(shù)讓出時(shí)間片滓侍,下一輪繼續(xù)嘗試。然后這次 QPS 漲了不少牲芋,直接到了 300 K+ 了粗井,當(dāng)然 CPU 也高了不少了。
但這個(gè)測(cè)試只是能評(píng)估我們極限性能街图,好指導(dǎo)我們后續(xù)的優(yōu)化浇衬。譬如現(xiàn)在我就知道我們最多不到 200 K 的 QPS,如果還是這個(gè)模型餐济,其它地方再怎么優(yōu)化耘擂,也還是這么多。
說(shuō)到這里絮姆,在聊一件性能測(cè)試的時(shí)候遇到的奇怪的事情醉冤,我在使用 perf stat 分析性能的時(shí)候,發(fā)現(xiàn)使用 SpinLock 的版本 QPS 能到 400 K篙悯,但之前的版本仍然不到 200 K蚁阳。這個(gè)結(jié)果實(shí)話當(dāng)時(shí)第一眼看到,是非常的奇怪的鸽照,后來(lái)思考了一下螺捐,因?yàn)?perf stat 其實(shí)是會(huì)在一些內(nèi)核的地方進(jìn)行 probe 的,這個(gè)會(huì)影響性能矮燎,可能因?yàn)檫@樣定血,導(dǎo)致 worker thread 那邊的 condition wait 以及調(diào)度變慢了,結(jié)果 Producer 那邊扔進(jìn)去更多的任務(wù)诞外,使得 worker 后面一直能取到任務(wù)澜沟,不會(huì)進(jìn)入 mutex 和 condition wait 了。而對(duì)于我們之前的版本來(lái)說(shuō)峡谊,無(wú)論怎樣茫虽,都要通過(guò) mutex 來(lái)進(jìn)行 task 操作,自然就不會(huì)出現(xiàn) SpinLock 這樣的情況既们。對(duì)于這個(gè)問(wèn)題濒析,這只是我的猜測(cè),我也跟其他對(duì) Linux 系統(tǒng)很精通的同學(xué)討論過(guò)這個(gè)問(wèn)題贤壁,大家都覺(jué)得這個(gè)解釋比較合理悼枢,只是后面我也懶得去追下去了,等后面有空了脾拆,重新折騰調(diào)度的時(shí)候再看看吧馒索。
Perf script
在測(cè)試的時(shí)候,我使用 perf trace 或者 perf record 大概知道了不同方案 mutex 的調(diào)用情況名船,但其實(shí)我最想知道的是到底出現(xiàn)了多少次 Lock Contention绰上,因?yàn)槲覒岩删褪且驗(yàn)檫@個(gè)導(dǎo)致的性能差異。
雖然能通過(guò) systemtap 來(lái)搞定這個(gè)問(wèn)題渠驼,但我不知道為啥頭腦抽風(fēng)了蜈块,想用 perf 來(lái)搞定。開始想的是直接 perf stat 的時(shí)候傳入相應(yīng)的 event 來(lái)做迷扇,但我在 perf list 里面沒(méi)找到對(duì)應(yīng)的 event百揭,Google 了下也沒(méi)啥好的結(jié)果,這個(gè)其實(shí)還是我對(duì)底層了解的不夠蜓席,真熟練了直接源碼一翻就知道了器一。但幸運(yùn)的是,我突然發(fā)現(xiàn)了 perf script厨内,之前這個(gè)玩意我只是在生成火焰圖的時(shí)候見(jiàn)過(guò)祈秕,但自己并沒(méi)有用過(guò)。昨天才知道雏胃,它其實(shí)是一個(gè)非常強(qiáng)大的東西请毛,也就是說(shuō),我們可以寫自己的 script 來(lái)分析 perf 采樣的數(shù)據(jù)瞭亮,而使用的 script language 并不需要像 systemtap 那樣重新學(xué)一門新的方仿,就是 perl 或者 python,當(dāng)然 perl 就算了统翩,python 雖然我不喜歡兼丰,但至少之前還寫過(guò) 2 年,完全夠用了唆缴。具體的 perf script 的東西鳍征,后面有時(shí)間可以在詳細(xì)的說(shuō)明。
我使用 perf script -l
看現(xiàn)在有啥可用的腳本面徽,幸運(yùn)的發(fā)現(xiàn)了一個(gè) futex-contention
艳丛,直接使用 perf script report futex-contention
得到了如下的輸出:
test[132777] lock 7faaf8a220c0 contended 199 times, 132761 avg ns
test[132782] lock 7faaf8a22090 contended 1171 times, 519948 avg ns
test[132778] lock 7faaf8a22090 contended 1145 times, 2004888 avg ns
test[132778] lock 7faaf8a220c0 contended 226 times, 1694905 avg ns
test[132779] lock 7faaf8a22090 contended 1199 times, 2745749 avg ns
然后再把這個(gè)結(jié)果貼到 Excel 里面,對(duì) times 匯總趟紊,確定了 SpinLock 的版本比原來(lái)的版本少了將近 15% 的 Lock Contention氮双。
總結(jié)
其實(shí)上面折騰了這么多,看起來(lái)沒(méi)折騰出啥東西霎匈,無(wú)非就是知道了 Lock Contention 對(duì)性能會(huì)有影響戴差,但對(duì)我來(lái)說(shuō),更大的收獲在于在排查問(wèn)題的時(shí)候铛嘱,對(duì)系統(tǒng)理解更加深入暖释,對(duì) perf 這些工具用起來(lái)更加熟練了袭厂。
TiKV 開發(fā)到現(xiàn)在,很多調(diào)優(yōu)工作其實(shí)已經(jīng)涉及到了跟系統(tǒng)和硬件的結(jié)合上面球匕,之前我們還能假設(shè)更底層對(duì)我們是一個(gè)黑盒纹磺,但現(xiàn)在為了更好的性能,需要我們對(duì)更下面抽絲剝繭亮曹,開始深入理解和掌握了橄杨。如果你對(duì) Linux 非常感興趣,想折騰下 DPDK 等跟硬件更緊密的優(yōu)化開發(fā)照卦,歡迎給我發(fā)郵件式矫,我的郵箱 tl@pingcap.com。