微博技術解密(下)

微博技術解密(下)| 微博存儲的那些事兒

今天是微博技術解密系列的第二期,我們來聊聊微博存儲的使用經驗。上一期“微博技術解密”我講到微博主要使用了兩大類存儲:一類是數據庫,主要以 MySQL 為主;一類是緩存,主要以 Memcached 和 Redis 為主。

今天我來分享一下微博在使用數據庫和緩存方面的經驗,也歡迎你給我留言一起切磋討論。

MySQL

上一期我講到微博 Feed 的存儲使用了兩層的結構,為了減少對 MySQL 數據庫的訪問壓力,在前面部署了 Memcached 緩存,擋住了 99% 的訪問壓力,只有 1% 的請求會訪問數據庫。然而對於微博業務來說,這 1% 的請求也有幾萬 QPS,對於單機只能扛幾千 QPS 的 MySQL 數據庫來說還是太大了。為此我們又對數據庫端口進行了拆分,你可以看下面的示意圖,每個用戶的 UID 是唯一的,不同 UID 的用戶按照一定的 Hash 規則訪問不同的端口,這樣的話單個數據庫端口的訪問量就會變成原來的 1/8。除此之外,考慮到微博的讀請求量要遠大於寫請求量,所以有必要對數據庫的讀寫請求進行分離,寫請求訪問 Master,讀請求訪問 Slave,這樣的話 Master 只需要一套,Slave 根據訪問量的需要可以有多套,也就是“一主多從”的架構。最後考慮到災備的需要,還會在異地部署一套冷備的災備數據庫,平時不對外提供線上服務,每天對所有最新的數據進行備份,以防線上數據庫發生同時宕機的情況。

微博技術解密(下)| 微博存儲的那些事兒

Memcached

在 MySQL 數據庫前面,還使用了 Memcached 作為緩存來承擔幾百萬 QPS 的數據請求,產生的帶寬問題是最大挑戰。為此微博採用了下圖所示的多層緩存結構,即 L1-Master-Slave,它們的作用各不相同。

L1 主要起到分擔緩存帶寬壓力的作用,並且如果有需要可以無限進行橫向擴展,任何一次數據請求,都隨機請求其中一組 L1 緩存,這樣的話,假如一共 10 組 L1,數據請求量是 200 萬 QPS,那麼每一組 L1 緩存的請求量就是 1/10,也就是 20 萬 QPS;同時每一組緩存又包含了 4 臺機器,按照用戶 UID 進行 Hash,每一臺機器只存儲其中一部分數據,這樣的話每一臺機器的訪問量就只有 1/4 了。

Master 主要起到防止訪問穿透到數據庫的作用,所以一般內存大小要比 L1 大得多,以存儲儘可能多的數據。當 L1 緩存沒有命中時,不能直接穿透到數據庫,而是先訪問 Master。

Slave 主要起到高可用的目的,以防止 Master 的緩存宕機時,從 L1 穿透訪問的數據直接請求數據庫,起到“兜底”的作用。

微博技術解密(下)| 微博存儲的那些事兒

Redis

微博的存儲除了大量使用 MySQL 和 Memcached 以外,還有一種存儲也被廣泛使用,那就是 Redis。並且基於微博自身的業務特點,我們對原生的 Redis 進行了改造,因此誕生了兩類主要的 Redis 存儲組件:CounterService 和 Phantom。

1. CounterService

CounterService 的主要應用場景就是計數器,比如微博的轉發、評論、讚的計數。早期微博曾採用了 Redis 來存儲微博的轉發、評論、贊計數,但隨著微博的數據量越來越大,發現 Redis 內存的有效負荷還是比較低的,它一條 KV 大概需要至少 65 個字節,但實際上一條微博的計數 Key 需要 8 個字節,Value 大概 4 個字節,實際上有效的只有 12 個字節,其餘四十多個字節都是被浪費的。這還只是單個 KV,如果一條微博有多個計數的情況下,它的浪費就更多了,比如轉評贊三個計數,一個 Key 是 long 結構,佔用 8 個字節,每個計數是 int 結構,佔用 4 個字節,三個計數大概需要 20 個字節就夠了;而使用 Redis 的話,需要將近 200 個字節。正因為如此,我們研發了 CounterService,相比 Redis 來說它的內存使用量減少到原來的 1/15~1/5。而且還進行了冷熱數據分離,熱數據放到內存裡,冷數據放到磁盤上,並使用 LRU,如果冷數據重新變熱,就重新放到內存中。

你可以看下面的示意圖,CounterService 的存儲結構上面是內存下面是 SSD,預先把內存分成 N 個 Table,每個 Table 根據微博 ID 的指針序列,劃出一定範圍。任何一個微博 ID 過來先找到它所在的 Table,如果有的話,直接對它進行增減;如果沒有,就新增加一個 Key。有新的微博 ID 過來,發現內存不夠的時候,就會把最小的 Table dump 到 SSD 裡面去,留著新的位置放在最上面供新的微博 ID 來使用。如果某一條微博特別熱,轉發、評論或者贊計數超過了 4 個字節,計數變得很大該怎麼處理呢?對於超過限制的,我們把它放在 Aux Dict 進行存放,對於落在 SSD 裡面的 Table,我們有專門的 Index 進行訪問,通過 RDB 方式進行復制。

微博技術解密(下)| 微博存儲的那些事兒

2. Phantom

微博還有一種場景是“存在性判斷”,比如某一條微博某個用戶是否贊過、某一條微博某個用戶是否看過之類的。這種場景有個很大的特點,它檢查是否存在,因此每條記錄非常小,比如 Value 用 1 個位存儲就夠了,但總數據量又非常巨大。比如每天新發布的微博數量在 1 億條左右,是否被用戶讀過的總數據量可能有上千億,怎麼存儲是個非常大的挑戰。而且還有一個特點是,大多數微博是否被用戶讀過的存在性都是 0,如果存儲 0 的話,每天就得存上千億的記錄;如果不存的話,就會有大量的請求最終會穿透 Cache 層到 DB 層,任何 DB 都沒有辦法抗住那麼大的流量。

假設每天要存儲上千億條記錄,用原生的 Redis 存儲顯然是不可行的,因為原生的 Redis,單個 KV 就佔了 65 個字節,這樣每天存儲上千億條記錄,需要增加將近 6TB 存儲,顯然是不可接受的。而用上面提到的微博自研的 CounterService 來存儲的話,一個 Key 佔 8 個字節,Value 用 1 個位存儲就夠了,一個 KV 就佔大約 8 個字節,這樣每天存儲上千億條記錄,需要增加將近 800GB 存儲。雖然相比於原生的 Redis 存儲方案,已經節省了很多,但存儲成本依然很高,每天將近 1TB。

所以就迫切需要一種更加精密的存儲方案,針對存在性判斷的場景能夠最大限度優化存儲空間,後來我們就自研了 Phantom。

就像下圖所描述的那樣,Phantom 跟 CounterService 一樣,採取了分 Table 的存儲方案,不同的是 CounterService 中每個 Table 存儲的是 KV,而 Phantom 的每個 Table 是一個完整的 BloomFilter,每個 BloomFilter 存儲的某個 ID 範圍段的 Key,所有 Table 形成一個列表並按照 Key 範圍有序遞增。當所有 Table 都存滿的時候,就把最小的 Table 數據清除,存儲最新的 Key,這樣的話最小的 Table 就滾動成為最大的 Table 了。

微博技術解密(下)| 微博存儲的那些事兒

下圖描述了 Phantom 的請求處理過程,當一個 Key 的讀寫請求過來時,先根據 Key 的範圍確定這個 Key 屬於哪個 Table,然後再根據 BloomFilter 的算法判斷這個 Key 是否存在。

微博技術解密(下)| 微博存儲的那些事兒

這裡我簡單介紹一下 BloomFilter 是如何判斷一個 Key 是否存在的,感興趣的同學可以自己搜索一下 BloomFilter 算法的詳細說明。為了判斷某個 Key 是否存在,BloomFilter 通過三次 Hash 函數到 Table 的不同位置,然後判斷這三個位置的值是否為 1,如果都是 1 則證明 Key 存在。

來看下面這張圖,假設 x1 和 x2 存在,就把 x1 和 x2 通過 Hash 後找到的三個位置都設置成 1。

微博技術解密(下)| 微博存儲的那些事兒

再看下面這張圖,判斷 y1 和 y2 是否存在,就看 y1 和 y2 通過 Hash 後找到的三個位置是否都是 1。比如圖中 y1 第二個位置是 0,說明 y1 不存在;而 y2 的三個位置都是 1,說明 y2 存在。

微博技術解密(下)| 微博存儲的那些事兒

Phantom 正是通過把內存分成 N 個 Table,每一個 Table 內使用 BloomFilter 判斷是否存在,最終每天使用的內存只有 120GB。而存在性判斷的業務場景最高需要滿足一週的需求,所以最多使用的內存也就是 840GB。

總結

今天我給你講解了微博業務中使用範圍最廣的三個存儲組件:一個是 MySQL,主要用作持久化存儲數據,由於微博數據訪問量大,所以進行了數據庫端口的拆分來降低單個數據庫端口的請求壓力,並且進行了讀寫分離和異地災備,採用了 Master-Slave-Backup 的架構;一個是 Memcached,主要用作數據庫前的緩存,減少對數據庫訪問的穿透並提高訪問性能,採用了 L1-Master-Slave 的架構;一個是 Redis,基於微博自身業務需要,我們對 Redis 進行了改造,自研了 CounterService 和 Phantom,分別用於存儲微博計數和存在性判斷,大大減少了對內存的使用,節省了大量機器成本。


分享到:


相關文章: