談推ter的百TB級Redis緩存實踐
摘要:在推ter工作的數年時間,Yao見證了緩存服務的擴展之路——從1個項目到上百項目的使用。為了支撐如此龐大的緩存體系,推ter使用了成千上萬臺服務器,多個集群,以及過百TB內存。
【編者按】文章內容是HighScalability創始人Todd Hoff基于推ter工程師Yao Yu “Scaling Redis at 推ter”演講的總結。在演講中,Yao從高等級概括了推ter為什么會選擇Redis,及如此規模緩存服務打造的挑戰和途徑。
以下為譯文:
自2010年,Yao Yu已經效力于推ter的緩存團隊。而本文主要基于她近日發表的“Scaling Redis at 推ter”演講,主要談推ter的Redis擴展,同時也不局限于Redis。從演講中不難發現,推ter在緩存服務打造上積累了相當豐富的經驗,就如你所想,推ter使用了大量的緩存。
Timeline服務(一個數據中心)Hybrid List使用情況:
- 分配40TB左右的內存堆棧
- 3000萬QPS(query per second)
- 超過6000個實例
BTtree(一個數據中心)使用狀態:
- 分配65TB的內存堆棧
- 900萬QPS
- 超過4000個實例
下文將會帶你詳細的學習BTree和Hybrid。
幾個值得關注的點:
- Redis的表現非常不錯,因為它將大量服務器中未使用的資源整合成有價值的服務。
- 推ter通過兩個專為其業務設計的新數據模型來定制化Redis,因此他們獲得了理想的性能,但是也因此受限于舊的代碼,無法迅速添加新特性。因此我(Todd)一直在想,為什么他們會使用Redis來做這樣的事情。只是想基于自己數據結構建立一個Timeline服務?Redis 真的適合干這樣的事情?
- 在網絡飽和之前,分析大量節點上的日志數據,使用本地的CPU。
- 如果想獲得非常高的性能,那么必須做快慢分離,數據永遠都是快的代言,而命令和控制則代表了慢。
- 當下,推ter正在使用Mesos作為作業調度程序以遷移到一個容器環境,這個做法很新穎,因此如何實現是一大看點。當然這個途徑也存在弊端,比如在復雜的運行時環境指定硬件資源的使用限制。
- 中央集群管理器來監控集群。
- 緩慢的JVM,快速的C,因此他們使用C/C++重寫緩存代理。
重點關注以上技術點,下面一起來看推ter的Redis使用之道:
為什么使用Redis?
- 使用Redis驅動Timeline,也是推ter系統內最重要的服務。Timeline是Tweet基于id的一個索引,Home Timeline則是所有Tweet連接起來形成的一個列表。User Timeline則由用戶生成的Tweet組成,它們形成了另外一個列表。
- 為什么使用Redis代替Memcache?主要因為網絡帶寬問題(Network Bandwidth Problem)和長通用前綴問題(Long Common Prefix Problem)。
- 網絡帶寬問題
- Memcache在Timeline上的表現并沒有Redis好,最大問題發生在fanout(推送)上。
- 推ter的讀和寫往往以增量方式進行,雖然每次的更新很少,但是Timeline本身的體積很大。
- 當一個Tweet產生時,它會被寫入對應的Timeline中。對于某些數據結構來說,Tweet是組成它的一小部分數據。在讀的時候,小批量Tweet被加載,而在滾輪向下滾動時,則會加載另外的一小批量。
- Home Timeline可能會非常大,但是用戶仍然希望在同一個集合中讀取,它可能會包含3000個實體。因此,在性能的優化上,避免數據庫讀取將非常重要。
- 增量讀寫使用了一個“讀-改-寫”的過程,因此對Timeline這樣的大型對象進行small reads開銷是非常昂貴的,通常會造成網絡瓶頸。
- 在每秒10萬+讀和寫的gigalink上,如果對象的平均大小超過1K,網絡將成為瓶頸。
- 長通用前綴問題(其實是兩個問題)
- 在數據格式上使用了一個靈活的模式,每個對象都有不同的屬性組成。對每個屬性都建獨立的鍵,這需求給每個屬性單獨發送請求,而不是對所有的屬性都進行緩存。
- 使用不同的時間戳跟蹤度量。如果獨立緩存每個度量樣本,那么長通用前綴會被不停的重復緩存。
- 平衡度量和靈活的數據模式,使用分層的key space更令人滿意。
- 通過CPU使用情況配置專門的集群。舉個例子,內存鍵值存儲對CPU的利用很低。服務器上1%的CPU時間也許能支撐1K+的RPS,基于不同數據模式會得出不同的結果。
- Reids的表現非常不錯。它能看到服務器可以做什么,而不是正在做什么。對于簡單的鍵值存儲,像Redis這樣的服務在服務器上可能會存在很多的CPU動態余量。
- 推ter首次為Timeline服務配置Redis是在2010年,同時搭載Redis的還有Ads服務。
- 推ter并沒有使用Redis的磁盤特性。這很大程度因為在推ter的系統中,緩存和存儲都在不同的團隊完成,他們會根據自己的使用來定制。也就是,對比Redis,存儲團隊有更好的服務。
- 熱鍵是一個必須解決的問題,因此推ter建立一個分層式緩存,客戶緩存會自動的緩存熱鍵。
Hybrid List
- 為Redis添加Hybrid List以獲得更可預期的內存性能。
- Timeline是Tweet ID組成的列表,因此是一堆的整型,每個ID都很小。
- Redis支持兩個列表類型:ziplist和linklist。Ziplist更具備空間效率,linklist則更加靈活,在雙向鏈表下每個建會占用兩個指針,對比ID的體積來說,這個開銷非常大。
- 如果從內存的使用效率上看,ziplists是唯一之選。
- Ziplist的最大閾值被設置為Timeline的大小。因此在調整Ziplist的閾值之前,永遠都不會儲存比它更大的Timeline。這同樣意味著一個產品決策,在Timeline中儲存多少個Tweet。
- 對ziplist做添加和刪除操作效率是非常低的,特別在列表非常大的情況下。從ziplist中使用memmove將數據刪除,這里必須保證列表仍然是連續的。向ziplist中添加則需要一個內存realloc調用,以保證新實體有足夠的空間。
- 鑒于Timeline的體積,寫入操作存在潛在的高延時。Timeline在體積上的變化很大,大部分的用戶不會頻繁發送Tweet,因此他們的User Timeline都很小。Home Timeline,特別涉及到那些名流人物則可能很大。當修改一個巨大的Timeline,而內存又被緩存占滿,通常會進行這個過程:大量小體積 timeline將會被驅逐,直到有足夠的連續內存來儲存這個巨大的ziplist。這些內存管理操作將花費很多的時間,因此寫入操作可能存在非常高的潛在延時。
- 因為寫入會造成很多Timeline的修改操作,基于需要在內存中擴展Timeline,這里有很大的可能會產生寫延時陷阱。
- 基于寫延時帶來的高可變性,很難為寫入建立SLA。
- Hybrid List是ziplists組成的linklist,因此存在一個基于所有ziplist最大體積的閾值(以字節為單位)。以字節為單位最大程度上是為了內存效率,它可以幫助分配和解除分配相同體積的內存。當一個列表結束后,空間就被分配給下一個Ziplist。Ziplist并不可回收,直到這個列表為空。這就意味,通過刪除,你可以讓每個ziplist只包含一個實體。通常情況下,Tweet并不會被全部刪除。
- 在Hybrid List之前,解決方案是盡快的讓一個大的Timeline過期,這會給其他Timeline節約出內存,但是如果用戶再查看這個Timeline時,開銷是非常大的。
BTree
- 將BTree添加到Redis是為了支持分層鍵上的范圍查詢,從而得到一個結果列表。
- 在Redis中,增加次關鍵字或字段一般通過Hash Map處理,排序數據以執行一個范圍查詢時,sorted set被使用。因為sorted set只能使用一個double類型的score來排序,所以這里不能任意指定次級鍵或者名稱來排序。同時,鑒于Hash Map使用的線性搜索,因此它并不適用于存在太多次關鍵字或字段的情況。
- 通過BSD實現BTree,并將之添加到Redis中。支持鍵查詢和范圍查詢,同時還具備了良好的查詢性能及簡單的代碼。唯一的缺點是BTree并不具備一個良好的內存利用效率,因為指針將造成大量的元數據開銷。
集群管理
- 為每個目的建立單獨的集群,每個集群部署了1個以上的Redis實例。如果一個數據集的大小大于單Redis實例可以支撐的極限,或者單Redis實例并不能提供足夠的吞吐量,key space需要被分割,數據則會橫跨一組實例在多個分片上保存,路由器將會為key選擇應該保存的數據分片。
- 集群管理是Redis不會被撐爆的首要原因。既然可以使用集群,那么沒理由不將所有的用例轉移到Redis上。
- Redis集群運營起來并不輕松。基于頻繁的更新操作,人們使用Redis,但是許多Redis運營并不是冪等的。比如,網絡故障可能會引起重試,從而在一定程度上破壞數據的完整性。
- Redis集群支持集中管理者操控全局。使用memcache,許多集群使用了一個基于一致性哈希的客戶端解決方案。如果出現非一致性數據,只能順其自然。為了提供更好的服務,集群需要一個功能負責檢測哪個分片發生問題,并且重新進行操作來保證同步。在足夠長的時間后,緩存應該被清理。在 Redis中破壞數據是不容易被發現的,當有一個列表,它缺失了一個部分,這個問題很難被描述清楚。
- 在建立Redis集群上,推ter做過了很多嘗試。Twemproxy,最初并不是在推ter內部使用,它被建立用于服務 Twemcache,隨后添加了對Redis的支持。同時,還針對代理類型路由建立了兩個額外的解決方案,其中一個與Timeline服務有關,但不是通用的;另一個則是專為Timeline設計的通用解決方案,提供了集群管理、復制和分片修復。
- 集群中存在3個選擇,服務器相互間通信以達成一個協議:集群狀態;使用一個代理;或者是當客戶端數量達到閾值時做一個客戶端方面的集群管理。
- 沒有做服務器方面的優化,因為一直以保持服務器簡單、透明和快速為理念。
- 并沒有通過客戶端,因為改變不容易被推廣。在推ter,1個緩存集群大約為100個項目使用。如果在一個客戶端中做改變必須推進到100個客戶端,這花費的時間可能以年計算。快速迭代意味著客戶端不能放任何代碼。
- 使用一個代理模式路由途徑以及分片主要基于兩個原因。首先,緩存服務必須是個高性能服務。如果你想獲得高性能,就必須分離快快慢路徑,快對應著數據路徑,而慢則對應了命令行和控制路徑。其次,如果將集群管理混合到服務器中,將增加Redis編碼的復雜度,從而造成一個有狀態的服務,如果你想給管理代碼打補丁或者升級,有狀態的服務你必須要重啟,從而可能會導致丟失一部分數據,滾動重啟集群是非常痛苦的。
- 使用代理途徑的另一個原因是在客戶端和服務器之間插入一個新的網絡躍點。關于在添加額外的網絡躍點上,Profiling 揭露了業內普遍存在的一個謠言。最起碼在推ter的系統中,Redis服務器產生的延時不會超過0.5毫秒。在Twiiter,大部分的后端系統都基于Java,并使用Finagle做相互間的交互。在通過Finagle后,延時增加到10毫秒。因此附加的網絡躍點并不是問題,問題的本身在于 JVM。除了JVM之外,你可以做任何的事情,當然除了添加又一個JVM。
- 代理的失敗并不會增加困擾。在數據路徑上,引入一個代理層并不意味著帶來額外開銷。客戶端不用去關心需要連接的代理。如果因為代理故障而造成的超時,客戶端只需要隨便選擇一個其他的代理。在代理等級不會發生任何分片,它們同樣是無狀態的。為了擴展吞吐量可以添加更多的代理,代價是額外的成本。代理層只會因為轉發被分配資源。集群管理、分片以及集群狀態監視都不是代理負責的范圍。代理之間并不需要保持一致。
- 推ter中存在實例同時打開10萬個連接的情況,服務器也并不會因此發生故障。在推ter,服務器沒理由去關閉一個連接,讓它一直打開有助于改善延時。
- 緩存集群被用作后備緩存(look-aside cache)。緩存本身不會負責數據的補充,客戶端負責取得一個丟失的鍵并進行緩存。如果一個節點發生故障,分片會被轉移到另一個節點。對故障恢復后的服務器進行同步以保證數據的一致性,所有這些都通過集群管理者完成。在讓一個集群更容易理解上,中央觀點確實非常重要。
- 測試使用C++來編寫代理。C++代理帶來了一個顯著的性能提升,隨后代理層都使用了C和C++。
數據洞察
- 當有調用顯示緩存系統失效,而大多數緩存都是正常時,這通常因為客戶端的配置錯誤,或者它們請求的鍵過多從而導致濫用緩存,當然也可能因為對同一個鍵多次請求造成服務器或鏈接飽和。
- 如果通知某個人正在濫用系統,他們很可能會要求出示證據。哪個鍵?哪個分片的問題?什么樣的流量導致了這個問題?因此你需要做足夠的度量和分析,從而將證據展示給客戶。
- 面向服務的架構并不會給帶來問題隔離或者是自動debug,必須對組成系統的每個組件保持足夠的能見度。
- 決定在緩存上獲得洞察力。緩存使用C編寫所以足夠快速,因此它可以能其他組件所不能,提供足夠的數據,而其他服務不能為每個請求都提供數據。
- 可以實現為每條命令單獨建立日志。在10萬QPS時,緩存可以記錄下所有發生的事情。
- 避免鎖和阻塞,特別不能因為磁盤寫入而造成阻塞。
- 在每秒100請求和每條日志消息100字節的情況下,每臺服務器每秒會記錄10MB的數據。當問題發生時,這些數據傳輸將造成很大的網絡開銷,大約占10%的帶寬,這種開銷完全不允許。
- 預計算服務器上的日志以減少開銷。設想是已經清楚需要被計算的內容,一個進程負責讀取日志,計算并生成一個摘要信息,然后只定期發送主機的摘要信息。對比原始數據,這個體積將微不足道。
- 這些云計算數據通過Storm聚合和存儲,并在上面建立可視化系統,你可以基于這些建立容量規劃。因為每條日志都可以被捕獲,所以你可以做許多事情。
- 對于運營來說,洞察力非常重要。如果出現丟包現象,通常情況下是熱鍵或者是流量峰值導致。
對Redis的希望清單
- 顯式的內存管理。
- Deployable(Lua)Scripts。
- 多線程,可以簡化集群管理。推ter有很多高性能服務器,每個主機都擁有100GB以上的內存以及大量的CPU。為了使用一個服務器的所有能力,需要在實體主機商開啟許多Redis實例。通過多線程減少需要啟動實例數量,從而更容易的進行管理。
學到的知識
- 可預見的規模需求。集群越大、客戶越多,你越希望你的服務更可預知和確定。如果只有一個用戶和一個問題,你可以迅速的找到并解決這個問題。但是如果你有70個客戶,你完全忙不過來。
- 如果你需要在許多分片上推廣某個更新,一個分片的速度問題就可能導致全局問題。
- 推ter正在向container環境發展,使用了Mesos作為作業調度器,調度器用來給請求分配CPU、內存等資源數量。當作業占用的資源高于請求時,監視器會直接將它終止。在容器的環境下,Redis會產生一個問題。Redis引入了外部存儲碎片,這意味著你要使用更多內存來存儲同樣的數據。如果不想作業被終止,必須設計一個緩沖的區間。你可能會認為內存碎片率設定在5%就足矣,但是我更愿意多分配10%,甚至是20%的空間作為緩沖。或者在我認為每臺主機連接數可能會達到5000時,我將給系統分配支撐1萬個連接數的內存,結果會造成很大的浪費。對于當下大多數低延時服務來說,Mesos都不太適合,因此這些作業會與其他作業隔離。
- 在運行時清楚資源使用是非常重要的。在一個大型集群中,總會有問題產生。雖然你一直認為系統很安全,但是事情還是以意料之外的方式發生了。當下很少出現某臺機器完全崩潰的情況,比如,在達到10GB的內存上限后,在有空閑內存之前,請求都會被拒絕,造成的后果僅是一小部分請求不能獲得自己所需的內存資源,無傷大雅。而垃圾回收問題則是非常麻煩的,它可能降低系統的流量,當下這個問題已經困擾了很多機構。
- 用數據說話。在計算到磁盤和計算到網絡之前,查看相對網絡速度、CPU速度計磁盤速度是非常有意義的,比如,節點被推送到中央監視服務之前查看的日志綜述。除此之外,Redis中的LUA也是給數據提供計算的途徑。
- LUA當下還沒有在Redis生產環境中實現。響應式腳本意味著服務提供商不能保證他們的SLA,一個被加載的腳本可以做任何事情,因此沒有服務提供商會因為添加一些代碼鋌而走險去破壞SLA。但是對于部署模型來說,意義重大,它將允許代碼評審和基準測試,以清晰的計算資源使用和性能。
- Redis作為下一個高性能流處理平臺。當下已經擁有pub-sub和scripting,沒什么不可以的。
原文鏈接: How 推ter Uses Redis to Scale - 105TB RAM, 39MM QPS,10,000+ Instances(編譯/童陽 責編/仲浩)
來自:http://www.csdn.net/article/2014-09-10/2821615-how-推ter-uses-redis-to-scale-105tb-ram-39mm-qps-10000-ins#0-tsina-1-37582-397232819ff9a47a7b7e80a40613cfe1