如果你翻閱過一些關(guān)于大型網(wǎng)站擴(kuò)展(Scaling)的資料,那么你可能聽說過一個叫memcached的東西。memcached是一個高性能、分布式的內(nèi)存對象緩存系統(tǒng)。我們Facebook可能是世界上最大的memcached用戶了。我們利用memcached來減輕數(shù)據(jù)庫的負(fù)擔(dān)。memcached確實(shí)很快,但是我們還要讓他更快、更高效。我們使用了超過800臺服務(wù)器,提供超過28TB的內(nèi)存來服務(wù)于用戶。在過去的一年里,隨著Facebook的用戶量直線上升,我們遇到了一系列的擴(kuò)展問題。日益增長的需求使得我們必須對操作系統(tǒng)和memcached進(jìn)行一些修改,以獲得足夠的性能來為我們的用戶提供最好的體驗(yàn)。
因?yàn)槲覀冇泻脦浊_機(jī)器,每個都運(yùn)行了幾百個Apache進(jìn)程甚至更多,最終導(dǎo)致到memcached進(jìn)程的TCP鏈接有幾十萬個。這些鏈接本身并不是什么大問題,但是memcached為每個TCP鏈接分配內(nèi)存的方法卻很成問題。memcached為每個鏈接使用單獨(dú)的緩存進(jìn)行數(shù)據(jù)的讀寫。當(dāng)達(dá)到幾十萬鏈接的時候,這些累計起來達(dá)好幾個G——這些內(nèi)存其實(shí)可以更好地用于存儲用戶數(shù)據(jù)。為了收復(fù)這些內(nèi)存,我們實(shí)現(xiàn)了一個針對TCP和UDP套接字的每線程共享的鏈接緩存池。這個改變使每個服務(wù)器可以收回幾個G的內(nèi)存。
雖然TCP上我們改進(jìn)了內(nèi)存的使用效率,但我們還是轉(zhuǎn)向了UDP,目的是讓get(獲取)操作能降低網(wǎng)絡(luò)流量、讓multi-get(同時并行地獲取幾百個鍵值)能實(shí)現(xiàn)應(yīng)用程序級別的流量控制。我們發(fā)現(xiàn)Linux上到了一定負(fù)載之后,UDP的性能下降地很厲害。這是由于,當(dāng)從多個線程通過單個套接字傳遞數(shù)據(jù)時,在UDP套接字鎖上產(chǎn)生的大量鎖競爭導(dǎo)致的。要通過分離鎖來修復(fù)內(nèi)核恐怕不太容易。所以,我們使用了分離的UDP套接字來傳遞回復(fù)(每個線程用一個答復(fù)套接字)。這樣改動之后,我們就可以部署UDP同時后端性能不打折。
另一個Linux中的問題是到了一定負(fù)載后,某個核心可能因進(jìn)行網(wǎng)絡(luò)軟終端處理會飽和而限制了網(wǎng)絡(luò)IO。在Linux中,網(wǎng)絡(luò)中斷只會總是傳遞給某個核心,因此所有的接受軟終端的網(wǎng)絡(luò)處理都發(fā)生在該內(nèi)核上。另外,我們還發(fā)現(xiàn)某些網(wǎng)卡有過高的中斷頻率。我們通過引入網(wǎng)絡(luò)接口的“投機(jī)”輪詢解決了這兩個問題。在該模型中,我們組合了中斷驅(qū)動和輪詢驅(qū)動的網(wǎng)絡(luò)IO。一旦進(jìn)入網(wǎng)絡(luò)驅(qū)動(通常是傳輸一個數(shù)據(jù)包時)以及在進(jìn)程調(diào)度器的空閑循環(huán)的時候,對網(wǎng)絡(luò)接口進(jìn)行輪詢。另外,我們也用到了中斷(來控制延遲),不過網(wǎng)絡(luò)中斷用到的數(shù)量大大減少(一般通過大幅度提升中斷聯(lián)結(jié)閾值interrupt coalescing thresholds)。由于我們在每個核心上進(jìn)行網(wǎng)絡(luò)傳輸,同時由于在調(diào)度器的空閑循環(huán)中對網(wǎng)絡(luò)IO進(jìn)行輪詢,我們將網(wǎng)絡(luò)處理均勻地分散到每個核心上。
最后,當(dāng)開始部署8核機(jī)器的時候,我們在測試中發(fā)現(xiàn)了新的瓶頸。首先,memcached的stat工具集依賴于一個全局鎖。這在4核上已經(jīng)很令人討厭了,在8核上,這個鎖可以占用20-30%的CPU使用率。我們通過將stats工具集移入每個線程,并且需要的時候?qū)⒔Y(jié)果聚合起來。其次,我們發(fā)現(xiàn)隨著傳遞UDP數(shù)據(jù)包的線程數(shù)量的增加,性能卻在降低。最后在保護(hù)每個網(wǎng)絡(luò)設(shè)備的傳送隊(duì)列的鎖上發(fā)現(xiàn)了嚴(yán)重的爭用。數(shù)據(jù)包是由設(shè)備驅(qū)動進(jìn)行入隊(duì)傳輸和出隊(duì)。該隊(duì)列由Linux的“netdevice”層來管理,它位于IP和設(shè)備驅(qū)動之間。每次只能有一個數(shù)據(jù)包加入或移出隊(duì)列,這造成了嚴(yán)重的爭用。我們當(dāng)中的一位工程師修改了出隊(duì)算法,實(shí)現(xiàn)了傳輸?shù)呐砍鲫?duì),去掉了隊(duì)列鎖,然后批量傳送數(shù)據(jù)包。這個更正將請求鎖的開銷平攤到了多個數(shù)據(jù)包,顯著地減少了鎖爭用,這樣我們就能在8核系統(tǒng)上將memcached伸展至8線程。
做了這些修改之后,我們可以將memcached提升到每秒處理20萬個UDP請求,平均延遲降低為173微秒。可以達(dá)到的總吞吐量為30萬UDP請求/s,不過在這個請求速度上的延遲太高,因此在我們的系統(tǒng)中用處不大。對于普通版本的Linux和memcached上的50,000 UDP請求/s而言,這是個了不起的提升。
我們希望盡快將我們的修改集成到官方的memcached倉庫中去,我們決定在這之前,先將我們對memcached的修改發(fā)布到github上。