轉(zhuǎn)自戈神 ?brpc的研發(fā)經(jīng)歷

RPC是個老概念,五花八門的實現(xiàn)非常多。在14年我剛轉(zhuǎn)到基礎(chǔ)架構(gòu)部時,其實是不想做RPC框架的。我的想法可能和很多工程師一樣:之前做了那么多系統(tǒng),現(xiàn)在就讓我來搞個編程框架?而且這能做出什么花頭?但事實很快證明我錯了,編程上的事真的需要實踐,否則看問題就很淺。像搞深度學習,vgg rcnn gan嘴上可以說得不停,但只要沒在真正嚴肅的項目中調(diào)過參數(shù),你就是門外漢。

RPC的深度在于現(xiàn)代的互聯(lián)網(wǎng)公司中幾乎所有服務(wù)都是使用RPC的,大部分工程師和它打交道。如果你能看到其中的痛點,提高了效率,那么整個公司的開發(fā)效率都會有明顯的提升。大家都是從學生時代過來的,心里清楚一個東西在正確的條件下正確運行很容易,但要在所有情況下能正確運行就非常困難。前兩天我修了個問題:brpc在fedora 26下一個weak function莫名其妙地沒有被tcmalloc中的對應(yīng)版本覆蓋,導致heap profiler啟用不了,ubuntu,centos下都是好的。這種問題往往和系統(tǒng)或ld有關(guān),要精確定位很麻煩,最后我找到了一個workaround。但這個事情耗了我?guī)讉€小時,因為需要在很多系統(tǒng)上驗證沒有regression。RPC里大量此類東西,雖然麻煩但能提高用戶體驗。那個問題其實和brpc對tcmalloc的支持方式有關(guān),brpc默認不鏈接tcmalloc,但用戶在程序中鏈接tcmalloc后,我們希望cpu和heap profiler要自動開啟(這兩個功能依賴tcmalloc的API),同時用戶不用重編brpc。所以我們得在brpc中動態(tài)判定是否鏈接了tcmalloc,這就沒那么容易了。對我們很麻煩,但用戶的體驗更好了,甚至用戶會覺得理所當然。

知識是需要大量實踐的,你也許可以在正確的條件下用dlsym有效地覆蓋一個glibc中的函數(shù),但你可能不知道dlsym在有多版本符號存在時可能無效,或dlsym和一些庫合用時(比如用于展開棧的libunwind)會死鎖,或dlsym對靜態(tài)鏈接是無效的除非編譯加了-rdynamic。你也許可以基于一些上下文切換庫三下五除二搞出個libcoroutine,但你可能不知道的是JNI會檢查stack layout而不能使用自定義棧,或程序運行在valgrind中需要注冊棧地址才不會報錯,或一個棧跑到另一個LWP上展開時會觸發(fā)gcc4以上版本的thread-local誤優(yōu)化。這些知識,成千上萬條這種知識,通過實踐才會深深地刻畫在腦中,構(gòu)成一個工程師真正的競爭力。

我一直堅信所有的用戶體驗都是端到端的,只有站在用戶的角度,把整個流程以既高效又不失擴展性的方式走通,才是最好的選擇。良好的文檔正是這種理念的體現(xiàn):給新用戶鋪好路能快速上手,讓老用戶知其所以然更上一層樓。這種想法也體現(xiàn)在代碼中的方方面面:每個選項都有合理的默認值,用戶不設(shè)也能用;在注釋中提示best practice,避免用戶走彎路;用戶界面、日志內(nèi)容不啰嗦,讓用戶一眼看清楚問題的全貌。不做并不意味著我們沒能力做,而是早已被事實證明可能出現(xiàn)非常subtle的bug而被淘汰掉的選擇。知道的越多,你就越會有一種責任感,需要幫助用戶修一條好路,避免陷到你已經(jīng)踩過的成百上千個坑中。

說到性能,RPC的性能評估其實很像VC投資初創(chuàng)公司:每家都在說自己的東西好,并能拿出數(shù)據(jù),可真的好不好天曉得。所以VC只能看團隊,查背景,憑感覺,這錢花出去了能不能拿回來心里都慌的很。RPC其實也這樣,每個實現(xiàn)都有大量獨特的設(shè)計和接口,用戶不太可能輕易地從一個RPC切換到另一個RPC,并在完全相同的環(huán)境下進行對比。每個RPC實現(xiàn)都在說自己高性能,輕量級。這是個自賣自夸的游戲,用戶只能看臉。但就像我們奇怪古人連那么簡單的東西都不知道一樣,人的認知就是這樣,內(nèi)行的常識可能對外行非常困難,甚至這個常識非常簡單。在很多年以前,我們對“高性能”的認識還停留在“極限QPS”和“延時”兩個維度的時候,被一個復雜系統(tǒng)中的擁塞問題搞的焦頭爛額,大家就覺得莫名其妙啊,每個環(huán)節(jié)都很快,這延時怎么就嘩嘩嘩地漲上去了。最后在反反復復的思考和分析后才發(fā)現(xiàn),QPS和延時的乘積與程序的最大服務(wù)能力緊密相連。我們搞了個概念叫volume,發(fā)現(xiàn)串行系統(tǒng)的volume可以相加,并行系統(tǒng)的volume可以求min,然后一層層地迭代上去從而計算出復雜系統(tǒng)同時能處理的最大請求數(shù),并解決了擁塞問題。

不過就是個乘法。

今天我們知道這個原理是little's law,tcp中的BDP也是類似的道理。我們在文檔中描述了相關(guān)的知識。但即使是這樣,根據(jù)我們在百度內(nèi)的支持經(jīng)驗(沒人會否認百度研發(fā)的整體素質(zhì)吧),大部分RPC的用戶對這個乘法理解還是有困難的,更別提理解串行相加,并行求min,在系統(tǒng)設(shè)計中活學活用了。一個乘法尚且同此,更深入的可想而知。普通用戶是很難看明白性能測試的道道的。我們團隊里有個老梗:“處處是熱點,處處不是瓶頸”。這說的是如果整個程序?qū)懙亩己艽直?,不考慮性能,最后用profiler一跑,發(fā)現(xiàn)每個點都只有1%,2%,然后得出結(jié)論,“性能非常好,優(yōu)化空間已經(jīng)不大”。但實際上你去分析下hot path,會發(fā)現(xiàn)有太多可以大幅提高的點了。性能就是這樣,設(shè)計確保了流程是最優(yōu)化的,但實現(xiàn)也非常重要,細節(jié)全靠摳。brpc上關(guān)鍵路徑上的代碼多一次new都需要討論,最熱的路徑上甚至不允許出現(xiàn)申明一個可能無用的空std::string,因為老版本glibc中的空string是要加引用計數(shù)的,對cache有影響。

摳細節(jié)的背后需要工程師對性能的深入理解。一個函數(shù)的性能是可以估算出來的,測試只是驗證。如果不符合預期,你就要深入地去看,最終理解背后的原因。為什么一次激烈的cacheline同步大約是700ns?或是一次調(diào)度延時至少是3us,99%以內(nèi)是20us?或是linux下的timed condition有60us的延時?或是一次上下文切換可以在200ns內(nèi)做完?或是無競爭的mutex可以實現(xiàn)為兩條20ns左右wait-free的原子指令?掌握了這些知識,你才能抓大放小,把精力放在最關(guān)鍵的事情上,并把它做到世界上最好的水平。

但即使到現(xiàn)在,brpc中仍然有一些極具挑戰(zhàn)性的問題,比如bthread的調(diào)度如何能更好地保持cache locality,如何在NUMA機器上跑得更好,如何盡量消除內(nèi)核調(diào)度的延時,如何更高效率地重用棧...如此種種。我們把brpc開源出來,正是為了讓感興趣的伙伴一起加入進來,做出一個更上一層樓的RPC框架。與大家共勉。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容