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