Redis如何輕松支撐萬(wàn)億級(jí)日訪問量?
Redis 在微博內(nèi)部分布在各個(gè)應(yīng)用場(chǎng)景,比如像現(xiàn)在春晚必爭(zhēng)的“紅包飛”活動(dòng),還有像粉絲數(shù)、用戶數(shù)、閱讀數(shù)、轉(zhuǎn)評(píng)贊、評(píng)論蓋樓、廣告推薦、負(fù)反饋、音樂榜單等等都有用到 Redis。
圖片來自 Pexels
本人將分為如下三個(gè)方面分享:
- Redis 在微博的應(yīng)用場(chǎng)景
- Redis 在微博的優(yōu)化
- 未來展望
Redis 在微博的應(yīng)用場(chǎng)景
業(yè)務(wù)&規(guī)模&挑戰(zhàn)
線上的業(yè)務(wù)有前面提到的信息流、廣告、用戶關(guān)系等等,還有現(xiàn)在大家可能比較感興趣的熱搜,用戶一般會(huì)去看發(fā)生了什么事情,還有引爆閱讀量的話題,以及現(xiàn)在兵家必爭(zhēng)之地的視頻,微博大大小小的業(yè)務(wù)都有用到 Redis。
線上規(guī)模方面,微博有 100T+ 存儲(chǔ),1000+ 臺(tái)物理機(jī),10000+Redis 實(shí)例。
關(guān)于面臨的挑戰(zhàn),我們每天有萬(wàn)億級(jí)的讀寫,線上的響應(yīng)時(shí)間要求也比較高。
舉一個(gè)簡(jiǎn)單的例子,我們部署資源是跨機(jī)房部署,但是有一些業(yè)務(wù)部門連跨機(jī)房部署存在的多余 2 毫秒的延遲都要投訴反饋(真的是臣妾做不到啊,如果單機(jī)房故障了呢?有些業(yè)務(wù)方真是異想天開)。響應(yīng)時(shí)間基本上四個(gè) 9 是 20 毫秒。
成本的話因?yàn)槲覀兙€上有大量需求是上 T 的,所以成本壓力其實(shí)也特別大。
技術(shù)選型
上圖是微博數(shù)據(jù)庫(kù)的技術(shù)選型,可以看到這里面不僅僅包含 Redis 等 NoSQL,還有隊(duì)列、存儲(chǔ)。
優(yōu)化
從 2010 年開始,我們就基于官方的 2.0 版本引進(jìn) Redis,到現(xiàn)在已經(jīng)有 9 個(gè)年頭了。
我們主要做了以下這些方面的改進(jìn):
- Redis 編碼格式,在特殊場(chǎng)景下可以節(jié)省 30% 的空間。
- 主從庫(kù)方面有獨(dú)立的復(fù)制線程。
- 我們定制化一些數(shù)據(jù)結(jié)構(gòu),比如:LongSet 數(shù)據(jù)結(jié)構(gòu),它是一個(gè)“固定長(zhǎng)度開放尋址的 Hash 數(shù)組”,減少 Redis dict 很多額外的指針開銷。
- 在主從復(fù)制方面,獨(dú)立復(fù)制線程+完全增量復(fù)制,這樣的話,如果網(wǎng)絡(luò)主從臨時(shí)斷了,只要從當(dāng)前的 Pos 點(diǎn)同步數(shù)據(jù)就行。
- 在持久化方面,我們是全量的 RDB 加增量的 AOF 復(fù)制。
- AOF 寫入/ 刷盤,主線程—>BIO,避免了因?yàn)閷懭雽?dǎo)致的阻塞。
- 落地時(shí)間,不可控—>cronsave 可控。
- 增加 aofnumber,設(shè)置 AOF 數(shù)量,避免因?yàn)閷懭脒^快,磁盤寫滿。
- 高可用,Redis 的 HA 我們并沒有用官方的或者社區(qū)開源的,用的是我們自己開發(fā)的一套 Redis HA,保障在故障的情況下,能快速進(jìn)行切換。
微博有大量的技術(shù)場(chǎng)景,比如轉(zhuǎn)評(píng)贊、閱讀數(shù)等,對(duì)于一些用戶來說,他們是很關(guān)心這些指標(biāo)的。
如果我們用原生的 Redis,會(huì)浪費(fèi)大量的存儲(chǔ)空間,因?yàn)樗漠a(chǎn)品特別特殊,它的 Key 是一個(gè)用戶的 ID,Value 是數(shù)字。
我們自己內(nèi)部最早改了一版叫 Redis Counter,它相當(dāng)于只維持了一個(gè)哈希表,節(jié)省了大量的 Redis 內(nèi)存空間。
當(dāng)然它有一個(gè)缺點(diǎn)就是當(dāng)初是短平快地上線了,所以它只支持單個(gè)列和單個(gè)表,如果你要存轉(zhuǎn)發(fā),評(píng)論,贊 3 個(gè)計(jì)數(shù)的話需要部署 3 套資源,這樣一來大家訪問微博取這 3 個(gè)數(shù)的速度會(huì)變慢。
而且需要維護(hù) 3 套資源,為了應(yīng)對(duì)這種場(chǎng)景,我們支持了多列和多表的方式,如果一個(gè)表寫滿了,可以繼續(xù)寫下一個(gè)表,寫到最后一個(gè)表時(shí),我們可以把前面的表滾到盤里面,但是這個(gè)時(shí)候是不可讀的。
為了解決不可讀的問題,我們想了一個(gè)辦法,把表都放在磁盤里面,維護(hù) ddb 的數(shù)據(jù)結(jié)構(gòu),在這樣的落地方式下,就可以把最近的熱數(shù)據(jù)放在內(nèi)存里面,把冷數(shù)據(jù)或者歷史數(shù)據(jù)放在磁盤里面。
之前統(tǒng)計(jì)了一下,在線上 90% 多的情況下,用戶只訪問幾個(gè)月的數(shù)據(jù),所以一些長(zhǎng)尾數(shù)據(jù)可以靠從磁盤中讀取數(shù)據(jù)來解決,也不影響用戶體驗(yàn)。
微博還有一些存在性判斷的行為,比如是否贊過、是否閱讀過,這些全量的數(shù)據(jù)特別大,如果用 Redis 的話對(duì)內(nèi)存成本花費(fèi)特別大。
所以我們改造了一版服務(wù),它是一個(gè)兼容 Redis 協(xié)議,基于 BloomFilter,開發(fā)了一版 Phantom,高性能,單線程網(wǎng)絡(luò)處理機(jī)制,與 Redis 性能相當(dāng),低存儲(chǔ)空間,每條記錄占用 1.2*N 字節(jié)(1% 的誤判率,每增加 0.6*N 字節(jié)誤判率下降為原來的 1/10,N 為單個(gè)槽位占用的 bit 數(shù))。
當(dāng)然還有其他像我們最近用的隊(duì)列、MySQL 等等其他類型的數(shù)據(jù)庫(kù),這邊就不展開了。
簡(jiǎn)單做一下 Redis 第一階段優(yōu)化的小結(jié):
- 無阻塞落地。
- 增量復(fù)制->RDB+AOF。
- 在線熱升級(jí)。
- 關(guān)系 Graph 定制,內(nèi)存降為 1/10,性能相當(dāng)。
- 計(jì)數(shù)定制化,內(nèi)存降為1/4,性能提升 3-5 倍。
- BloomFilter。
但是我們做了這么多優(yōu)化還是跟不上業(yè)務(wù)的需求。
Redis 在微博的優(yōu)化
首先需要明白為什么要優(yōu)化,我們一般從三個(gè)方面進(jìn)行考慮:
首先是業(yè)務(wù)方。目前線上的業(yè)務(wù)方需要關(guān)心資源的分布、容量規(guī)劃等多方面,比如內(nèi)存是否滿了、磁盤是否滿了、如果用 MySQL 的話是否要提前分庫(kù)分表、QPS 是否能扛住。
我們希望把這些問題對(duì)業(yè)務(wù)方屏蔽,他們只管用,而不用關(guān)心太多涉及到資源細(xì)節(jié)的方面。
第二是 DBA。雖然現(xiàn)在微博已經(jīng)不是處于高速增長(zhǎng)的狀態(tài)了,但實(shí)際上它也還是以一定的速度在增長(zhǎng),所以對(duì) DBA 來說,需求還是特別多的。
加上我們部門是承接微博所有的數(shù)據(jù)庫(kù)的服務(wù),有微博最多的服務(wù)器,因此對(duì)于我們來說,需求多,變更多,挑戰(zhàn)大。
從設(shè)計(jì)的角度,我們要考慮如何設(shè)計(jì) Redis 更合理。
總結(jié)了一下有三個(gè)方面:
- 高性能,讀寫快、訪問快、響應(yīng)時(shí)間快。
- 能夠支持大容量的需求。
- 可擴(kuò)展,因?yàn)榻佑|的業(yè)務(wù)方比較多,就會(huì)發(fā)現(xiàn)一個(gè)問題,基本上沒有幾個(gè)業(yè)務(wù)方能把自己的需求描述得特別清楚,經(jīng)常上線之后才發(fā)現(xiàn)內(nèi)存不夠了,或者寫入扛不住了,所以這個(gè)時(shí)候我們需要在可擴(kuò)展性方面提供一個(gè)強(qiáng)有力的支持。
我們可以把這三個(gè)方面解釋為三座大山。
Cache Service 服務(wù)化
為了解決三座大山,首先要把 Cache 服務(wù)化,它是一個(gè)多級(jí)緩存的服務(wù),能夠解決高訪問、高并發(fā)的問題以及實(shí)現(xiàn)高可用。
基于這套系統(tǒng),也設(shè)計(jì)了一套后臺(tái)程序,根據(jù)微博的流量進(jìn)行自動(dòng)監(jiān)測(cè)、能夠支持自動(dòng)擴(kuò)縮容,這樣能快速扛過峰值,峰值過去之后又回收機(jī)器,實(shí)現(xiàn)了對(duì)資源的充分利用。
當(dāng)然這套系統(tǒng)還在持續(xù)完善中,希望未來能做到更智能。Config Service 就是我們把配置放在配置中心里面,Client 再?gòu)呐渲弥行睦锩胬∨渲谩?/p>
一共有兩種訪問方式,第一種是 SDK,第二種是支持多語(yǔ)言的,通過 Proxy 把請(qǐng)求路由到后端的 Cache 里面。DBA 只要通過管理平臺(tái)就可以對(duì)資源進(jìn)行快速擴(kuò)縮容。
現(xiàn)在講一下多級(jí)的 Cache,實(shí)際上這里面有四個(gè)角色:
- Master
- Maste-l1
- Slave
- Slave-l1
Master 跟 Slave 沒有同步關(guān)系,只是按角色作用的方式命名的,Master-l1 有多組數(shù)據(jù)來扛熱點(diǎn),Master 是基準(zhǔn)數(shù)據(jù)保存全量數(shù)據(jù)。
Slave 一般是做多機(jī)房的容災(zāi),Slave-l1 做多機(jī)房的數(shù)據(jù)同步,這個(gè)同步只保證最終數(shù)據(jù)的一致性。
以讀取作為例子來說一下流程,讀取是先訪問 Master-l1,如果沒有命中會(huì)訪問 Master,如果又沒有命中會(huì)訪問到 Slave,通過這 3 層,大部分情況下能把 99% 的熱點(diǎn)給扛住,然后還有 1% 的流量才會(huì)落到 MySQL 里面。
假如是 100 萬(wàn)的讀,穿透到 MySQL 只有一萬(wàn) QPS,如果 100 萬(wàn)的讀全部都打到 MySQL 的話,對(duì)于 MySQL 而言成本特別高。
而且大家知道,MySQL 在高并發(fā)讀寫情況下,很容易被打死,且在短時(shí)間內(nèi)是恢復(fù)不了。
Cache Service 目前支持 MC 和 Redis 協(xié)議 2 種協(xié)議。
上圖是我們 DBA 操作的擴(kuò)縮容的界面,這個(gè)業(yè)務(wù)總共有 20 組,每組有 5 個(gè) IP,5×20=100 個(gè)實(shí)例。
實(shí)際上就是一百個(gè)實(shí)例在里面提供服務(wù),線上有好多個(gè)單個(gè)集群服務(wù),可以支撐百萬(wàn)甚至千萬(wàn) QPS 的高并發(fā)訪問,而且可以支持快速的擴(kuò)縮容。
分享一下我們之前的成功案例,我們已經(jīng)實(shí)現(xiàn)好幾年的春晚 1000+ 臺(tái)阿里云 ECS 彈性擴(kuò)縮容,多次實(shí)現(xiàn)無降級(jí)平滑過渡,高峰期支持微博 50% 的春晚核心流量。
上圖是我們內(nèi)部為了支持系統(tǒng)而進(jìn)行的系統(tǒng)整合,在這邊就不展開了。
MCQ 服務(wù)化
基于前面的 Cache 服務(wù)化,我們?cè)?2018 上半年跟業(yè)務(wù)方一起合作,把隊(duì)列也給服務(wù)化了。
為什么要把隊(duì)列單獨(dú)提出來呢?是因?yàn)榻?jīng)常有內(nèi)部或外部的人問,你們發(fā)微博是什么樣的流程?你們發(fā)評(píng)論是什么樣的流程?數(shù)據(jù)怎么解決?
這些問題很關(guān)鍵的一環(huán)就是在隊(duì)列里面,發(fā)微博的時(shí)候?qū)嶋H上是先寫到隊(duì)列,然后隊(duì)列再寫到后端的 MySQL 里面。
如果這個(gè)時(shí)候 MySQL 宕機(jī)了,我們會(huì)有一個(gè)修復(fù)隊(duì)列,專門有一個(gè) Key 來存這部分的數(shù)據(jù),等 MySQL 恢復(fù)以后再把這部分?jǐn)?shù)據(jù)寫入到 MySQL 里面。
上面還有一個(gè) BCP,是因?yàn)楫?dāng)初我們?cè)谧鲞@一套的時(shí)候,實(shí)際上是想在整個(gè)微博推廣。
去年比特幣特別火,我們也想通過購(gòu)買比特幣的方式,在內(nèi)部通過機(jī)器的資源或者內(nèi)部開源的一些東西來做等價(jià)物質(zhì)的轉(zhuǎn)換,然后來應(yīng)用這個(gè)服務(wù),但是最終這個(gè)計(jì)劃沒有具體落地。
上圖是一鍵告警以及操作的監(jiān)控圖。前面提到我們把 Cache 服務(wù)化了,但是實(shí)際上并沒有很好地解決容量過大的問題,雖然現(xiàn)在內(nèi)存的價(jià)格一直在下降,但相對(duì)硬盤來說價(jià)格還是太高。
如果我們經(jīng)常有像 5T 或者 10T 的業(yè)務(wù),并且全放內(nèi)存里面的話,對(duì)于我們成本的壓力實(shí)際上是特別大的。
而且我們需要向?qū)iT的成本委員會(huì)申領(lǐng)資源,只有成本委員會(huì)同意了我們才能拿到這些機(jī)器,整個(gè)周期時(shí)間長(zhǎng)。
如何解決 Redis 容量過大?
為了解決容量過大的問題,我們想把容量從內(nèi)存放到磁盤里面。
我們當(dāng)時(shí)考慮了一些特性,比如支持冷熱數(shù)據(jù)的分離,比如把歷史的數(shù)據(jù)或者全量的數(shù)據(jù)全部存在磁盤,然后支持持久化、支持?jǐn)?shù)據(jù)主從復(fù)制、支持在線熱升級(jí),需要兼容 Redis 數(shù)據(jù)類型,還要兼容與 Redis 的復(fù)制。
基于前面的場(chǎng)景,像微博這種屬性特別適合用這種方法,就算冷熱數(shù)據(jù)不明顯,比如上 T,每秒幾 K 訪問的情況,用這個(gè)方法也特別合適。
下面講一下處理模塊,里面有主線程和后臺(tái)線程。
主線程主要處理連接的請(qǐng)求、協(xié)議的解析以及命令的請(qǐng)求,后臺(tái)線程主要是復(fù)制線程,還有像 BIO 線程,我們把像刷盤操作是寫 AOF 都是放在這個(gè)線程,這樣可以盡可能減少寫入所造成的對(duì) Redis 的阻塞。
還有一個(gè) BloomFilter,是基于布谷鳥算法來優(yōu)化,初始化的時(shí)候指定 Filter 的容量,新增雙向鏈表管理 Hash 沖突。
從這個(gè)名字大家可以猜到,是 Redis+RocksDB 的結(jié)合,為什么這個(gè)時(shí)候我們不像前面提到的類似設(shè)計(jì) CounterserviceSSD 那樣自己設(shè)計(jì),其實(shí)主要原因是當(dāng)初我們?cè)谠O(shè)計(jì)時(shí) RocksDB 還沒有非常多大規(guī)模的應(yīng)用。
現(xiàn)在 RocksDB 已經(jīng)特別成熟,而且有非常多成功的案例。
我們還有一個(gè)不自己開發(fā)的原因,就是如果自己開發(fā)的話,可能適用性或者性能,以及代碼健壯性反而沒有那么好,所以為了節(jié)省時(shí)間我們采用了 RocksDB 來做存儲(chǔ),避免重復(fù)造輪子。
LRU 是為了加快訪問速度的,如果第一次訪問的時(shí)候沒有在內(nèi)存里面讀取到,就從磁盤里面讀取,它實(shí)際上會(huì)放在內(nèi)存,下次你再讀取的時(shí)候會(huì)從 LRU 里面讀取出來。
這邊還涉及到數(shù)據(jù)從內(nèi)存到磁盤換入換出,如果 Key 或者 Value 特別大的話,性能會(huì)有影響。
這就是前面提到的為什么我們不推薦那種特別大的 Key 或者 Value 用 RedRocks。
把前面的處理模塊和后端整合一下就形成了以下這樣的架構(gòu)圖:
對(duì)其做一下小結(jié):
簡(jiǎn)單易用:完全兼容 Redis,業(yè)務(wù)方不用做任何改動(dòng)就可以遷移上。
成本優(yōu)勢(shì):把熱點(diǎn)數(shù)據(jù)或者頻繁訪問數(shù)據(jù)放在內(nèi)存,全量的數(shù)據(jù)全部放磁盤,這是一個(gè)特別大的優(yōu)勢(shì),可以突破內(nèi)存容量限制。
高性能:熱點(diǎn)數(shù)據(jù)在內(nèi)存,熱點(diǎn)數(shù)據(jù)訪問性能和 Redis 相當(dāng)。
下圖是性能壓測(cè)報(bào)告,我們對(duì)比了 Set 的隨機(jī)對(duì)寫:
仍滿足不了新需求?
我們前面已經(jīng)解決了大容量的問題,但還是有很多困難并沒有得到很好的解決。
因此,我們借鑒了開源經(jīng)驗(yàn),也調(diào)研了 Twemproxy、Codis、Corvus、Redis-Cluser 這些功能:
實(shí)際上我們?cè)?2015 年就已經(jīng)存在基于 Twemproxy 的業(yè)務(wù),在線上的話像微博音樂、微博健康、通行證這 3 個(gè)業(yè)務(wù)已經(jīng)上線。
但是我們沒有在內(nèi)部大范圍推廣開來,其中涉及到 2 個(gè)主要的原因:
- 第一就是遷移還是比較費(fèi)時(shí)間。
- 第二是無法比較完美的動(dòng)態(tài)增加節(jié)點(diǎn),還有內(nèi)部一些其他原因等等的約束。
以上是我們的設(shè)計(jì)思路:
- 一是能支持在線擴(kuò)縮容。
- 二是支持多語(yǔ)言的訪問,因?yàn)槲覀兪且獙?duì)整個(gè)公司進(jìn)行推廣的,而不是說只對(duì)一個(gè)部門,所以為了推廣方便我們必須有這種功能。
- 三是對(duì)服務(wù)化特性的需求。
下面簡(jiǎn)單講一下 Proxy 里面各模塊的功能:
Port 自動(dòng)增刪和監(jiān)聽:根據(jù) Vintage 對(duì)本 Proxy 節(jié)點(diǎn)的配置,自動(dòng)增加監(jiān)聽的端口或者刪除移除的端口,監(jiān)聽客戶端的連接。
Redis 協(xié)議解析:解析 Redis 協(xié)議,確定需要路由的請(qǐng)求,非法和不支持的請(qǐng)求直接返回錯(cuò)誤。
路由:需要獲取和監(jiān)聽端口對(duì)應(yīng)的 Backend 以及它們的 Slot, 根據(jù)端口、Key 和 Redis 命令選擇一個(gè) Backend, 將請(qǐng)求路由到對(duì)應(yīng)的 Backend,并將結(jié)果返回給客戶端。
配置監(jiān)控:監(jiān)控 Vintage 中本 Proxy 的配置,包括端口的變動(dòng)、端口和 Backend 的變動(dòng)以及 Slot 的變化等,通知端口監(jiān)聽模塊和路由模塊。
指標(biāo)監(jiān)控:需要將 Metrics 發(fā)送到 Graphite 中進(jìn)行監(jiān)控。
日志記錄:生成日志文件以便跟蹤。
Redis 存儲(chǔ)方面:還是沿用我們內(nèi)部改造的 Redis 版本,相對(duì)之前線上的版本,這次我們新增了官方比如 Mememory,內(nèi)存編碼的優(yōu)化,以及內(nèi)部新增的一些新的功能。
關(guān)于集群管理方面,無論是 Redis 也好,MySQL 也好,對(duì)資源的任何管理都可以用這個(gè)來總結(jié),包括五個(gè)部分:
- 資源申請(qǐng)
- 資源分配
- 業(yè)務(wù)上線
- 資源查詢
- 資源變更
對(duì)于業(yè)務(wù)申請(qǐng)這一方面需要有一個(gè)業(yè)務(wù)唯一的標(biāo)識(shí),QPS、數(shù)據(jù)類型是怎樣的,基于這些考察我們?cè)賹?duì)它進(jìn)行分配、配置、部署。
基于前面我們做了那么多的優(yōu)化以及平臺(tái)服務(wù)化,用下圖作為總結(jié)比較合適,就相當(dāng)于服務(wù)的高可用、高性能以及可擴(kuò)展這些方面,我們基本上都用前面這一套方案解決了。
未來展望
無論是最開始的 MySQL 也好還是到后面的 Oracle 也好,這些都離不開 SQL。如果我們能把數(shù)據(jù)一致性解決好的話,Redis 的應(yīng)用場(chǎng)景會(huì)更廣。
現(xiàn)在有很多公司對(duì) Raft 做了二次開發(fā),后續(xù)我們也會(huì)投入到在這方面中。
借用兩句話結(jié)束今天的演講:“數(shù)據(jù)庫(kù)實(shí)際上是需要你用最快的速度把數(shù)據(jù)存儲(chǔ)下來,然后以最方便的方式把數(shù)據(jù)給回憶起來。”
作者:蘭將州
簡(jiǎn)介:新浪微博核心 Feed 流、廣告數(shù)據(jù)庫(kù)業(yè)務(wù)線負(fù)責(zé)人,主要負(fù)責(zé) MySQL、NoSQL、TiDB 相關(guān)的自動(dòng)化開發(fā)和運(yùn)維,參與 Redis、counteservice_ssd、Memcacheq 相關(guān)代碼的開發(fā),目前關(guān)注分布式系統(tǒng)。