Redis實戰:如何構建類微博的億級社交平台


微博及 Twitter 這兩大社交平台都重度依賴 Redis 來承載海量用戶訪問。本文介紹如何使用 Redis 來設計一個社交系統,以及如何擴展 Redis 讓其能夠承載上億用戶的訪問規模。

雖然單台 Redis 具備極佳的性能,但隨着系統規模增大,單台服務器不能存儲所有數據、以及沒辦法處理所有讀寫請求的問題遲早都會出現,這時我們就需要對 Redis 進行擴展,讓它能夠滿足需求。

在介紹如何擴展之前,我們先看下如何用 Redis 來搭建一個社交平台。

使用 Redis 搭建社交平台

用 Redis 來搭建一個社交平台,需要首先考慮以下幾個核心功能。

1. 已發表微博

 

可以使用 Redis 的 hash 來保存已發表微博。

 
 

一條微博通常包括多個字段,比如發表時間、發表用戶、正文內容等,通常使用微博 id 作為 key 將多個鍵值對作為 hash 保存在 Redis 中。

2. 信息流

 
 

當一個用戶訪問它的首頁信息流時候,他可以看到他所有關注用戶最新的信息。key 是當前用戶的 uid, 信息流的內容以 id / timestamp 的形式保存在 zset 中,timestamp 用於排序,以便返回的列表是按照時間順序排列。微博的 id 用於業務下一步獲取微博的相關信息。

3. 關注與粉絲

 

 
 
 
 

我們可以把關注及粉絲庫也存在 zset 中,依舊使用 timestamp 來排序。key 是當前用戶 uid。

了解上述結構之后,我們繼續來看如何使用 Redis 來擴展整個系統,具備處理億級用戶的能力。

我們首先要做的,就是在 Redis 能夠存儲所有數據並且能夠正常地處理寫查詢的情況下,讓 Redis 的讀查詢處理能力超過單台 Redis 服務器所能提供的讀查詢處理能力。

擴展讀性能

假定我們用 Redis 構建一個與微博或 Twitter 具有相同特性和功能的社交網站,網站的其中一個特性就是允許用戶查看他們自己的 profile 頁和個人首頁信息流,每當用戶訪問時,程序就會從信息流里面獲取大約 30 條內容。

因為一台專門負責獲取信息流的 Redis 服務器每秒至少可以同時為 3,000 ~ 10,000 個用戶獲取信息流消息,所以這一操作對於規模較小的社交網站來說並不會造成什么問題。

但是對於規模更大的社交網站來說,程序每秒需要獲取的信息流消息數量將遠遠超過單台 Redis 服務器所能處理的上限,因此我們必須想辦法提升 Redis 每秒能夠獲取的信息流消息數量。

下面我們將會討論如何使用只讀的從服務器提升系統處理讀查詢的性能,使得系統的整體讀性能能夠超過單台 Redis 服務器所能提供的讀查詢性能上限。

在對讀查詢的性能進行擴展,並將額外的服務器用作從服務器以提高系統處理讀查詢的性能之前,讓我們先來回顧一下 Redis 提高性能的幾個途徑。

在使用短結構時,請確保壓縮列表的最大長度不會太大以至於影響性能。

根據程序需要執行的查詢的類型,選擇能夠為這種查詢提供最好性能的結構。比如說,不要把 LIST 當作 SET 使用;也不要獲取整個 HASH 然后在客戶端里面對其進行排序,而是應該直接使用 ZSET;諸如此類。

在將大體積的對象緩存到 Redis 之前,考慮對它進行壓縮以減少讀取和寫入對象時所需的網絡帶寬。對比壓縮算法 lz4、gzip 和 bzip2,看看哪個算法能夠對被存儲的數據提供最好的壓縮效果和最好的性能。

使用 pipeline(pipeline 是否啟用事務性質由具體的程序決定)以及連接池。

在做好了能確保讀查詢和寫查詢能夠快速執行的一切准備之后,接下來要考慮的就是如何實際解決“怎樣才能處理更多讀請求”這個正題。

提升 Redis 讀取能力的最簡單方法,就是添加提供讀能力的從服務器

用戶可以運行一些額外的服務器,讓它們與主服務器進行連接,然后接受主服務器發送的數據副本並通過網絡進行准實時的更新(具體的更新速度取決於網絡帶寬)。通過將讀請求分散到不同的從服務器上面進行處理,用戶可以從新添加的從服務器上獲得額外的讀查詢處理能力。

記住:只對主服務器進行寫入

在使用只讀從服務器的時候,請務必記得只對 Redis 主服務器進行寫入。在默認情況下,嘗試對一個被配置為從服務器的 Redis 服務器進行寫入將引發一個錯誤(就算這個從服務器是其他從服務器的主服務器,也是如此)。

簡單來說,要將一個 Redis 服務器變為從服務器,我們只需要在 Redis 的配置文件里面,加上一條slaveof host port語句,並將 host 和 port 兩個參數的值分別替換為主服務器的 IP 地址和端口號就可以了。除此之外,我們還可以通過對一個正在運行的 Redis 服務器發送SLAVEOF host port命令來把它配置為從服務器。需要注意的一點是,當一個從服務器連接至主服務器的時候,從服務器原本存儲的所有數據將被清空。最后,通過向從服務器發送SLAVEOF no one命令,我們可以讓這個從服務器斷開與主服務器的連接。

使用多個 Redis 從服務器處理讀查詢時可能會遇到的最棘手的問題,就是主服務器臨時下線或者永久下線。每當有從服務器嘗試與主服務器建立連接的時候,主服務器就會為從服務器創建一個快照,如果在快照創建完畢之前,有多個從服務器都嘗試與主服務器進行連接,那么這些從服務器將接收到同一個快照。從效率的角度來看,這種做法非常好,因為它可以避免創建多個快照。

但是,同時向多個從服務器發送快照的多個副本,可能會將主服務器可用的大部分帶寬消耗殆盡。使主服務器的延遲變高,甚至導致主服務器已經建立了連接的從服務器斷開。

解決從服務器重同步(resync)問題的其中一個方法,就是減少主服務器需要傳送給從服務器的數據數量,這可以通過構建樹狀復制中間層來完成

 
 

(圖:一個 Redis 主從復制樹示例,樹的最底層由 9 個從服務器組成,而中間層則由 3 個復制輔助服務器組成)

從服務器樹非常有用,在對不同數據中心(data center)進行復制的時候,這種從服務器樹甚至是必需的:通過緩慢的廣域網(WAN)連接進行重同步是一件相當耗費資源的工作,這種工作應該交給位於中間層的從服務器去做,而不必勞煩最頂層的主服務器。但是另一方面,構建從服務器樹也會帶來復雜的網絡拓撲結構(topology),這增加了手動和自動處理故障轉移的難度。

除了構建樹狀的從服務器群組之外,解決從服務器重同步問題的另一個方法就是對網絡連接進行壓縮,從而減少需要傳送的數據量。一些 Redis 用戶就發現使用帶壓縮的 SSH 隧道(tunnel)進行連接可以明顯地降低帶寬占用,比如某個公司就曾經使用這種方法,將復制單個從服務器所需的帶寬從原來的 21Mbit 降低為 1.8Mbit(http://mng.bz/2ivv)。如果讀者也打算使用這個方法的話,那么請記得使用 SSH 提供的選項來讓 SSH 連接在斷線后自動重連。

加密和壓縮開銷

一般來說,使用 SSH 隧道帶來的加密開銷並不會給服務器造成大的負擔,因為2.6 GHz 主頻的英特爾酷睿 2 單核處理器在只使用單個處理核心的情況下,每秒能夠使用 AES-128 算法加密 180MB 數據,而在使用 RC4 算法的情況下,每秒則可以加密大約 350MB 數據。在處理器足夠強勁並且擁有千兆網絡連接的情況下,程序即使在加密的情況下也能夠充分地使用整個網絡連接。

唯一可能會出問題的地方是壓縮—因為 SSH 默認使用的是 gzip 壓縮算法。SSH 提供了配置選項,可以讓用戶選擇指定的壓縮級別(具體信息可以參考SSH的文檔),它的 1 級壓縮在使用之前提到的 2.6GHz 處理器的情況下,可以在復制的初始時候,以每秒 24~52MB 的速度對 Redis 的 RDB 文件進行壓縮;並在復制進入持續更新階段之后,以每秒 60~80MB 的速度對 Redis 的 AOF 文件進行壓縮。

使用 Redis Sentinel

Redis Sentinel 可以配合 Redis 的復制功能使用,並對下線的主服務器進行故障轉移。Redis Sentinel 是運行在特殊模式下的 Redis 服務器,但它的行為和一般的 Redis 服務器並不相同。

 
 

Sentinel 會監視一系列主服務器以及這些主服務器的從服務器,通過向主服務器發送PUBLISH命令和SUBSCRIBE命令,並向主服務器和從服務器發送PING命令,各個 Sentinel 進程可以自主識別可用的從服務器和其他 Sentinel。

當主服務器失效的時候,監視這個主服務器的所有 Sentinel 就會基於彼此共有的信息選出一個 Sentinel,並從現有的從服務器當中選出一個新的主服務器。當被選中的從服務器轉換成主服務器之后,那個被選中的 Sentinel 就會讓剩余的其他從服務器去復制這個新的主服務器(在默認設置下,Sentinel 會一個接一個地遷移從服務器,但這個數量可以通過配置選項進行修改)。

一般來說,使用 Redis Sentinel 的目的就是為了向主服務器屬下的從服務器提供自動故障轉移服務。此外,Redis Sentinel 還提供了可選的故障轉移通知功能,這個功能可以通過調用用戶提供的腳本來執行配置更新等操作。

更深入了解 Redis Sentinel 可以閱讀http://redis.io/topics/sentinel

在了解如何擴展讀性能的方法之后,接下來我們該考慮如何擴展寫性能了。

擴展寫性能和內存容量

隨着被緩存的數據越來越多,當數據沒辦法被存儲到單台機器上面的時候,我們就需要想辦法把數據分割存儲到由多台機器組成的集群里面。

擴展寫容量

盡管這一節中討論的是如何使用分片來增加可用內存的總數量,但是這些方法同樣可以在一台 Redis 服務器的寫性能到達極限的時候,提升 Redis 的寫吞吐量。

在對寫性能進行擴展之前,首先需要確認我們是否已經用盡了一切辦法去降低內存占用,並且是否已經盡可能地減少了需要寫入的數據量。

對自己編寫的所有方法進行了檢查,盡可能地減少程序需要讀取的數據量。

將無關的功能遷移至其他服務器。

在對 Redis 進行寫入之前,嘗試在本地內存中對將要寫入的數據進行聚合計算,這一做法可以應用於所有分析方法和統計計算方法。

使用鎖去替換可能會給速度帶來限制的 WATCH/MULTI/EXEC 事務,或者使用 Lua 腳本。

在使用 AOF 持久化的情況下,機器的硬盤必須將程序寫入的所有數據都存儲起來,這需要花費一定的時間。對於 400,000 個短命令來說,硬盤每秒可能只需要寫入幾 MB 的數據;但是對於 100,000 個長度為  1KB 的命令來說,硬盤每秒將需要寫入100MB 的數據。

如果用盡了一切方法降低內存占用並且盡可能地提高性能之后,問題仍然未解決,那么說明我們已經遇到了只使用單台機器帶來的瓶頸,是時候將數據分片到多台機器上面了。

本文介紹的數據分片方法要求用戶使用固定數量的 Redis 服務器。舉個例子,如果寫入量預計每 6 個月就會增加 4 倍,那么我們可以將數據預先分片(preshard)到 256 個分片里面,從而擁有一個在接下來的 2 年時間里面都能夠滿足預期寫入量增長的分片方案(具體要規划多長遠的方案要由你自己決定)。

為了應對增長而進行預先分片

在為了應對未來可能出現的流量增長而對系統進行預先分片的時候,我們可能會陷入這樣一種處境:目前擁有的數據實在太少,按照預先分片方法計算出的機器數量去存儲這些數據只會得不償失。為了能夠如常地對數據進行分割,我們可以在單台機器上面運行多個 Redis 服務器,並將每個服務器用作一個分片。

注意,在同一台機器上面運行多個 Redis 服務器的時候,請記得讓每個服務器都監聽不同的端口,並確保所有服務器寫入的都是不同的快照文件或 AOF 文件。

在單台機器上面運行多個 Redis 服務器

上面介紹了如何將寫入命令分片到多台服務器上面執行,從而增加系統的可用內存總量並提高系統處理寫入操作的能力。但是,如果你在執行諸如搜索和排序這樣的復雜查詢時,感覺系統的性能受到了 Redis 單線程設計的限制,而你的機器又有更多的計算核心、更多的通信網絡資源,以及更多用於存儲快照文件和 AOF 文件的硬盤 I/O,那么你可以考慮在單台機器上面運行多個 Redis 服務器。你需要做的就是對位於同一台機器上面的所有服務器進行配置,讓它們分別監聽不同的端口,並確保它們擁有不同的快照配置或 AOF 配置。

擴展復雜的業務場景

在對各式各樣的 Redis 服務進行擴展的時候,常常會遇到這樣一種情況:因為服務執行的查詢並不只是讀寫那么簡單,所以只對數據進行簡單分片並不足以滿足復雜業務場景的需求。

對社交網站進行擴展

下面介紹如何對類似微博或者 Twitter 這樣的社交網站進行擴展,介紹的目的是為了讓我們更好的理解使用什么樣的數據結構及方法來構建一個大型社交網絡,這些方法幾乎可以無限制地進行——只要資金允許,我們可以將一個社交網站擴展至任意規模。

對社交網站進行擴展的第一步,就是找出經常被讀取的數據以及經常被寫入的數據,並思考是否有可能將常用數據和不常用數據分開

首先,假設我們已經把用戶已發表的微博放在一個獨立的 Redis 服務器,並使用只讀的從服務器處理針對這些數據進行大量讀取操作。那么一個社交網站上需要進行擴展的主要是兩個類型的數據:信息流、關注及粉絲列表。

擴展已發表微博的數據庫

當你的社交網站獲得一定的訪問量之后,我們需要對存儲已發表微博的數據庫做進一步的擴展,而不僅僅只添加從服務器。

因為每條微博都完整地存儲在一個單獨的 HASH 里面,所以程序可以很容易地基於散列所在的鍵,把各條微博 hash 分片到由多個 Redis 服務器組成的集群里面。

因為對每條微博 hash 進行分片並不困難,所以分片的工作應該並不難完成。擴展微博數據庫的另一種方法,就是將 Redis 用作緩存,並把最新發布的消息存儲到 Redis 里,而較舊(也就是較少讀取)的消息則存儲到以硬盤存儲為主的服務器里面,像 PostgreSQL、MySQL、Riak、MongoDB 等。

在一個社交網站上,主要的信息流有 3 種:用戶首頁的信息流、profile 信息流以及分組信息流。各個信息流本身都是相似的,所以我們將使用相同的處理方式。

下面我們來看社交系統中最核心的兩種系統如何通過不同的分片策略對其進行擴展。

1.對信息流列表進行分片

標題所說的“對信息流進行分片”實際上有些詞不達意,因為首頁信息流和分組列表信息流通常都比較短(最大通常只有 1,000 條,實際的數量由zset-max-ziplist-size選項的值決定),因此實際上並不需要對信息流的內容進行分片;我們真正要做的是根據鍵名,把不同的信息流分別存儲到不同的分片上面。

另一方面,社交網站每個用戶 profile 信息流通常無限增長的。盡管絕大多數用戶每天最多只會發布幾條微博,但也有話癆用戶以明顯高於這一頻率的速度發布大量信息。以 Twitter 為例,該網站上發布信息最多的 1,000 個用戶,每人都發布了超過 150,000 條推文,而其中發布最多的 15 個用戶,每人都發布了上百萬條推文。

從實用性的角度來看,一個合乎情理的做法是限制每個用戶的已發表微博最多只能存儲大約 20,000 條信息,並將最舊的信息刪除或者隱藏——這種做法足以處理 99.999% 的 Twitter 用戶,而我們也會使用這一方案來對社交網站的個人信息流進行擴展。擴展個人信息流的另一種方法,就是使用本節稍后介紹的關注庫進行擴展的技術。

2.通過分片對關注及粉絲列表擴展

雖然對信息流進行擴展的方法相當直觀易懂,但是對關注和粉絲列表這些由有序集合構成的“列表”進行擴展卻並不容易。這些有序集合絕大多數都很短(如 Twitter 上 99.99% 的用戶的關注者都少於 1,000 人),但是也存在少量用戶的列表非常大,他們關注了非常多的人或者擁有數量龐大的粉絲。

從實用性的角度來考慮,一個合理的做法是給用戶以及分組可以關注的人數設置一個上限(比如新浪微博普通用戶最大允許關注 2,000 用戶)。不過這個方法雖然可以控制用戶的關注人數,但是仍然解決不了單個用戶的粉絲數人數過多的問題。

為了處理關注和粉絲列表變得非常巨大的情況,我們需要將實現這些列表的有序集合划分到多個分片上面,說得更具體一樣,也就是根據分片的數量把用戶的粉絲划分為多個部分,存在多個 zset 中。為此,我們需要為ZADD命令、ZREM命令和ZRANGEBYSCORE命令實現特定的分片版本。

和信息流分片的區別是,這次分片的對象是數據而不是鍵。此外,為了減少程序創建和調用連接的數量,把關注和粉絲的數據放置在同一個分片里面將是一種非常有意義的做法。因此這次我們將使用新的方法對數據進行分片。

為了能夠在關注及粉絲數據進行分片的時候,把兩者數據都存儲到同一個分片里面,程序將會把關注者和被關注者雙方的 ID 用作查找分片鍵的其中一個參數。

總結

本章對各式各樣的程序進行了回顧,介紹了一些對它們進行擴展以處理更多讀寫流量並獲得更多可用內存的方法,其中包括使用只讀從服務器、使用可以執行寫查詢的從服務器、使用分片以及使用支持分片功能的類和函數。盡管這些方法可能沒有完全覆蓋讀者在擴展特定程序時可能會遇到的所有問題,但是這些例子中展示的每項技術都可以廣泛地應用到其他情景里面。

本文希望向讀者傳達這樣一個概念:對任何系統進行擴展都是一項頗具挑戰性的任務。但是通過 Redis,我們可以使用多種不同的方法來對平台進行擴展,從而把平台擴展成我們想要的規模。

 
 

本文節選自人民郵電出版社《Redis 實戰》第 8、10 章,由聚焦 Redis 領域的黃健宏翻譯,感興趣的讀者可以在各大書店購買。以下是翻譯過程中的一張照片,看出譯者對質量非常用心。

 
 

圖片來源:http://blog.huangz.me/diary/2015/memories-of-redis-in-action-translation.html

人郵也新開了公眾號「人郵 IT 書坊」長期提供最新 IT 圖書資訊,歡迎關注。

更多精彩,關注人郵IT書坊

一個為程序員提供干貨、書訊、贈書活動的地方

 
 

參考閱讀

移動直播技術秒開優化經驗(含PPT)

保證分布式系統數據一致性的6種方案

校長:技術成長四個階段需要的架構知識

唯品會RPC服務框架與容器化演進

本文由人民郵電出版社信息技術分社授權「高可用架構」發表。轉載請注明來自高可用架構「ArchNotes」微信公眾號及包含以下二維碼。

 

高可用架構

改變互聯網的構建方式

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM