Reddit是著名的社交新聞網站,光是在2012年,它的獨立訪客就達到了4000萬,頁面瀏覽量達到了370億次。幾年過去了,網站用戶有增無減,而隨著用戶的增多,網站的響應速度卻一直在改進。這要得益于Reddit使用了大量的緩存。而隨著網站規模不斷增長,緩存數量也隨著增加,那么Reddit是如何做到在增大緩存規模的同時又能保證它們的響應速度的?
我們知道,緩存的命中率越高,整體速度就越快,因為不需要重新從數據源加載數據。除此之外,如何管理緩存,比如緩存的過期時間,新舊緩存的交換,以及緩存的設計等等,它們對緩存的整體性能都有很大影響。來自Reddit的工程師Daniel Ellis在Reddit官方博客上分享了他們是如何使用Memcached集群來存儲網站的緩存數據的。
Reddit的緩存規模和基本策略
Reddit目前使用了54個規格為r3.2xlarge的AWS EC2實例,每個實例擁有61GB內存,也就是說總的緩存大小差不多是3.3TB,而且這些緩存并不包括應用程序的本地緩存。Reddit的緩存包含了多種類型的數據,包括數據庫對象、查詢結果集、函數調用,還有一些看起來不太像緩存的東西,比如限定速率、分布式鎖等等。如何管理這么大規模的緩存是一件很挑戰性的事情,Reddit采用的是“不要把所有雞蛋放在同一個籃子里”的基本策略。也就是說,他們并不是把3.3TB的內存看成一個總的大緩存池,而是按照負載類型對緩存進行分類,每種類型占用一定數量的緩存空間。這樣做有幾個好處:
首先,按照負載類型對緩存進行分區,每種類型的緩存可以獨立地伸縮。例如,對于數據庫緩存來說,如果它的命中率降低,交換率變高,同時數據庫變慢,那么就要考慮對數據庫緩存進行擴展,而它的擴展不會影響到其它類型的緩存。
其次,按照負載類型對緩存進行分區,可以有針對性地對某種類型的緩存進行負載測試,從而預測該類型緩存的使用規模,并作出權衡。
第三個好處跟Memcached的內存分配模型有關系。Memcached按照板塊(slab)來分配內存,例如,1至96字節的對象可能被放到板塊1,97至120字節的對象被放到板塊2,并依此類推。這樣做可以避免出現內存碎片。不過,Memcached的這種分配機制不能動態變化,也就是說一旦設定好了這種模式就不能對其進行修改。如果一開始設定了用來存儲1KB的對象,但后來想用它來存儲500KB的對象,那么交換率就會變得很高。而按照負載類型來區分緩存,那么就可以根據實際數據類型的大小類設定板塊大小。
新版本的Memcached可能支持slab_automove功能,不過這是后話了。
Reddit的緩存類型
接下來我們來看看Reddit的幾種緩存類型。
數據庫對象緩存(thing-cache)
Instances | 16 r3.2xlarge |
Memcached Version |
1.4.30 |
Total RAM | 976 GB |
Get Rate | ~800k/s |
Set Rate | ~13k/s |
Miss % | 1.2-2% |
Typical Object Size | 384-1184 bytes |
數據庫對象緩存是Reddit最大的緩存池。這些對象是無schema的,開發人員可以很容易地對這些對象添加新屬性,而無需對數據庫schema進行變更。這些對象包括用戶評論、鏈接和賬戶等等。該類型緩存是Reddit最繁忙也最有用的緩存,命中率高達99%。
主緩存(cache-main)
Instances | 11 r3.2xlarge |
Memcached Version |
1.4.30 |
Total RAM |
671 GB |
Get Rate |
~82k/s |
Set Rate | ~10k/s |
Miss % |
~75% |
Typical Object Size | <96 bytes |
主緩存是Reddit第二大緩存池。這個緩存是一般性的緩存,里面存放的所有用來展示/r/all的結果集。不過從表格中可以看到,這個緩存的命中率并不高,大概只有25%左右。
渲染緩存(cache-render)
Instances | 8 r3.2xlarge |
Memcached Version |
1.4.30 |
Total RAM |
488 GB |
Get Rate |
~224k/s |
Set Rate | ~103k/s |
Miss % |
~45-55% |
Typical Object Size | 240-2320 bytes |
第三大緩存用來存放渲染過的頁面模板或頁面片段。這個緩存相對安全,就算發生失效,也不會對系統造成太大影響。它的命中率只有大概50%左右,畢竟頁面信息需要不斷更新,所以渲染過的頁面模板或片段也需要更新。再則,就算這個緩存失效,也不會給數據庫負載帶來多大影響,因為用來渲染頁面的上下文內容已經在其它獨立的緩存中加載過了。不過,因為緩存的key是基于上下文內容生成的,如果key發生變化,模板就需要重新緩存,這個需要消耗額外的CPU,也會使頁面響應時間變長。
持久緩存(cache-perma)
Instances | 6 r3.2xlarge |
Memcached Version | 1.4.17 |
Total RAM |
366 GB |
Get Rate |
24k/s |
Set Rate | 4k/s |
Miss % | <1% |
Typical Object Size | 96-120 bytes |
最后一個要細說的緩存,也是命中率最高的緩存——持久緩存,它的命中率超過了99%。這個緩存用來存放數據庫的查詢結果,還有用戶評論和鏈接。為什么管這個緩存叫持久緩存,因為他們使用了讀-改-寫(read-modify-write)的模式。例如,在用戶新增一個評論時,他們會同時更新緩存和后端的數據庫(Cassandra),而不是簡單地讓緩存失效,這樣就避免了需要再次從數據庫加載數據。
非緩存對象池
之前提過,除了上述的幾種緩存,Reddit還使用了速率限定和分布式鎖。
對于一個并發量很大的網站來說,采取速率限定是很重要的一個措施,它可以避免用戶無限制地消耗網站的資源。他們按照時間段把不同的key存放在不同的bucket里,每個key的TTL會隨著每次調用逐步增加。通過檢查這些TTL就可以確保它們不會超出限定的范圍。因為一旦超過限定范圍,該用戶就無法再做任何操作。
另一方面,得益于Memcached的“add”原子操作命令,他們可以實現分布式鎖。因為“add”命令每次會產生一個新的key,只要這個key原先不存在,那么就相當于獲得了一把鎖。鎖用完了就會被移除,下一次調用“add”會生成新的鎖。這個操作保證同時只有一個進程可以獲得這個鎖。不過這也是他們的痛點之一。因為這里存在單點故障問題,一旦需要做遷移或維護,會讓整個網站不可用。Reddit團隊計劃在未來逐步減少甚至避免使用這種鎖。
其它緩存池
除了上述幾種緩存,Reddit還有一些小型的緩存池,比如對象關系的緩存、函數調用結果的緩存等等。因為這些緩存都不大,這里不一一贅述。
mcrouter
上面介紹了Reddit的緩存分區策略,以及各種緩存類型的特點。接下來,我們來看看Reddit是如何使用mcrouter來滿足各種復雜的使用場景的。
mcrouter是由Facebook開源的Memcached連接池。為什么要用連接池?就像訪問數據庫要使用數據庫連接池一樣,使用連接池可以對連接進行重用和管理,避免了重復創建和銷毀連接的開銷。Reddit有很多應用服務器,每個服務器上面運行著多個工作進程,如果這些進程獨自向緩存集群發起連接,那么連接數量會暴增。無法重用連接是一種資源浪費,同時會給緩存集群帶來更大壓力。通過在應用服務器上使用mcrouter,當前服務器上所有進程到緩存集群的連接可以形成一個連接池,并通過mcrouter這個唯一的出口連接到相應的緩存上。
除了作為連接池,mcrouter還能處理很多復雜的場景。mcrouter提供了多種路由類型,比如PrefixSelectorRoute,它通過匹配key的前綴來決定應該到哪個緩存上獲取數據。這樣就可以把特定功能的操作路由到特定的緩存上。
如果要往緩存集群里增加新的緩存實例,那么可以使用WarmUpRoute。新加入的緩存實例被稱為“冷”緩存,而原先的實例叫作“熱”緩存。WarmUpRoute的工作原理是說,把所有寫操作路由到“冷”緩存上,而把未命中的讀操作路由到“熱”緩存上,然后把在“熱”緩存上命中的緩存結果異步地更新到“冷”緩存上,那么下次同樣的讀操作就也可以在“冷”緩存上命中。通過拷貝“熱”緩存里的數據可以避免操作數據庫,保證性能不會受到影響。
mcrouter還提供了FailoverRoute,顧名思義,這個特性可以避免緩存的單點故障,因為它會為一種類型的緩存創建多個緩存池,如果其中一個失效了,請求會被路由到另一個備份的緩存實例上。
Reddit還使用了影子緩存。不同于WarmUpRoute,WarmUpRoute只是把未命中的讀操作拷貝到新實例上,而影子緩存會把讀操作和寫操作都拷貝一份到新的實例上,但前提是不改變數據源。通過影子緩存,他們可以對緩存的負載情況進行觀察,因為新加的實例作為舊實例的“影子”而存在,在不影響舊實例的前提下可以看到整個緩存的工作情況。
mcrouter還支持數據復制,這個功能不僅為緩存提供了高可用性,同時防止出現緩存熱點。
自定義監控
緩存有時候會變成一個黑盒,所以對它們進行監控是很有必要的。GitHub上有一個叫做Diamond的Python腳本可以收集Memcached的基本統計信息,比如對象的交換和命中率等等。不過這些信息還太簡單,Reddit團隊需要知道在發生對象交換時,緩存內部還發生了其它什么狀況。因為通過Memcached的“stats slabs”命令可以看到板塊的度量指標,于是他們基于這些命令自己寫了一個追蹤板塊度量指標的工具。他們還開發了一個簡陋的可視化儀表盤:
Reddit團隊還開發了另外一個工具,叫作mcsauna。這個工具被部署在每個緩存服務器上,它可以檢測網絡流量,并根據配置規則把不同的key保存在不同的bucket里,然后把結果輸出到文件上。FilesCollector會收集這些文件,分析里面的key,并以圖形化的方式呈現出來。從這些圖形上可以看出那些熱點的key。
展望
緩存為提升網站的響應速度做出了不可磨滅的貢獻。而在如何使用緩存方面,Reddit還有很長的路要走。接下來,他們可能要想著如何通過服務發現來對配置進行自動化,從而實現緩存的自動擴展,而不需要人工的介入。而隨著Memcached版本的不斷改進,他們也要針對現有系統進行調整,從而最大化緩存的性能。