用最少的機(jī)器支撐萬(wàn)億級(jí)訪問(wèn),微博6年Redis優(yōu)化歷程
微博是從 2010 年開(kāi)始引入 Redis ,現(xiàn)在 Redis 已經(jīng)廣泛應(yīng)用于微博的多個(gè)業(yè)務(wù)場(chǎng)景,如關(guān)系、計(jì)數(shù)、通知提醒等,目前 Redis 集群存儲(chǔ)超過(guò)百億記錄,每天上萬(wàn)億的讀取訪問(wèn)。隨著業(yè)務(wù)的快速發(fā)展,我們?cè)谑褂眠^(guò)程中碰到的問(wèn)題及解決方法給大家做一個(gè)分享。主要包括以下方面: 實(shí)現(xiàn)機(jī)制高可用、業(yè)務(wù)極致定制以及服務(wù)化。
Redis 2.0 時(shí)代(2010 - 2011)
實(shí)現(xiàn)機(jī)制高可用優(yōu)化
微博最早使用的是 Redis 2.0 版本,在初期業(yè)務(wù)規(guī)模不大的時(shí)候, Redis 服務(wù)運(yùn)行比較穩(wěn)定。但是隨著業(yè)務(wù)數(shù)據(jù)量和訪問(wèn)量的增加,一些問(wèn)題逐漸暴露出來(lái):
持久化問(wèn)題
在我們大多數(shù)業(yè)務(wù)場(chǎng)景中 Redis 是當(dāng)做存儲(chǔ)來(lái)使用,會(huì)開(kāi)啟持久化機(jī)制。線上采用單機(jī)多實(shí)例的部署結(jié)構(gòu),服務(wù)器的內(nèi)存使用率也會(huì)比較高。由于官方版本觸發(fā) bgsave和 bgrewriteaof 操作的時(shí)間點(diǎn)是不可控的,依賴于相關(guān)的配置項(xiàng)和業(yè)務(wù)的寫(xiě)入模型,因此可能會(huì)出現(xiàn)單機(jī)部署的多個(gè) Redis 實(shí)例同時(shí)觸發(fā) bgsave 或 bgrewriteaof 操作,這兩個(gè)操作都是通過(guò) fork 出一個(gè)子進(jìn)程來(lái)完成的,由于 copy-on-write 機(jī)制,可能會(huì)導(dǎo)致服務(wù)器內(nèi)存很快耗盡, Redis 服務(wù)崩潰。
此外在磁盤(pán)壓力較大時(shí)(生成 rdb、aof 重寫(xiě)),對(duì) aof 的寫(xiě)入及 fsync 操作可能會(huì)出現(xiàn)阻塞,雖然從 2.4 版本開(kāi)始 fsync 操作調(diào)整到 bio 線程來(lái)做,主線程 aof 的寫(xiě)入阻塞仍會(huì)導(dǎo)致服務(wù)阻塞。
主從同步問(wèn)題
為了提高服務(wù)可用性,避免單點(diǎn)問(wèn)題,我們線上業(yè)務(wù) Redis 大多采用主從結(jié)構(gòu)部署。官方版本的主從同步機(jī)制,在網(wǎng)絡(luò)出現(xiàn)問(wèn)題時(shí)(如瞬斷),會(huì)導(dǎo)致主從重新進(jìn)行一次全量復(fù)制。對(duì)單個(gè)端口來(lái)說(shuō),如果數(shù)據(jù)量小,那么這個(gè)影響不大,而如果數(shù)據(jù)量比較大的話,就會(huì)導(dǎo)致網(wǎng)絡(luò)流量暴增,同時(shí) slave 在加載 rdb 時(shí)無(wú)法響應(yīng)任何請(qǐng)求。當(dāng)然官方 2.8 版本支持了 psync 增量復(fù)制的機(jī)制,一定程度上解決了主從連接斷開(kāi)會(huì)引發(fā)全量復(fù)制的問(wèn)題,但是這種機(jī)制受限于復(fù)制積壓緩沖區(qū)大小,同時(shí)在主庫(kù)故障需要執(zhí)行切主操作場(chǎng)景下,主從仍然需要進(jìn)行全量復(fù)制。
版本升級(jí)及管理問(wèn)題

早期 Redis 版本運(yùn)行不夠穩(wěn)定,經(jīng)常需要修復(fù) bug、支持新的運(yùn)維需求及版本優(yōu)化,導(dǎo)致版本迭代很頻繁。官方版本在執(zhí)行升級(jí)操作時(shí),需要服務(wù)重啟,我們大多數(shù)線上業(yè)務(wù)都開(kāi)啟了持久化機(jī)制,重啟操作耗時(shí)較長(zhǎng),加上使用 Redis 業(yè)務(wù)線比較多,版本升級(jí)操作的復(fù)雜度很高。由于統(tǒng)一版本帶來(lái)的運(yùn)維工作量實(shí)在太高,線上 Redis 版本曾經(jīng)一度增加到十幾個(gè),給版本管理也帶來(lái)很大的困難。
為了解決以上問(wèn)題我們對(duì) Redis 原生實(shí)現(xiàn)機(jī)制做了以下優(yōu)化:
1. 對(duì)于持久化機(jī)制,采用 rdb + aof 的持久化方式。
aof 文件按固定大小滾動(dòng),生成 rdb 文件時(shí)記錄當(dāng)前 aof 的 position,全量的數(shù)據(jù)包含在 rdb 和所記錄位置點(diǎn)之后的 aof 文件,廢棄 aof 重寫(xiě)機(jī)制,生成 rdb 后刪除無(wú)效的 aof 文件;增加了定時(shí)持久化操作的配置項(xiàng) cronsave,將單機(jī)部署的多個(gè) Redis 實(shí)例的持久化操作分散在不同的時(shí)間點(diǎn)進(jìn)行,并且錯(cuò)開(kāi)業(yè)務(wù)高峰;將對(duì) aof 的寫(xiě)入操作也放到 bio 線程來(lái)做,解決磁盤(pán)壓力較大時(shí) Redis 阻塞的問(wèn)題。
2. 對(duì)于主從同步機(jī)制,借鑒 MySQL 的復(fù)制機(jī)制并做了簡(jiǎn)化。
使用 rdb + aof 的方式,支持基于 aofpositon 的增量復(fù)制。從庫(kù)只需與主庫(kù)進(jìn)行一次全量同步同步,后續(xù)主從連接斷開(kāi)或切主操作,從庫(kù)都是與主庫(kù)進(jìn)行增量復(fù)制。

對(duì)于版本升和管理級(jí)的問(wèn)題, Redis 的核心處理邏輯封裝到動(dòng)態(tài)庫(kù),內(nèi)存中的數(shù)據(jù)保存在全局變量里,通過(guò)外部程序來(lái)調(diào)用動(dòng)態(tài)庫(kù)里的相應(yīng)函數(shù)來(lái)讀寫(xiě)數(shù)據(jù)。版本升級(jí)時(shí)只需要替換成新的動(dòng)態(tài)庫(kù)文件即可,無(wú)須重新載入數(shù)據(jù)。通過(guò)這樣的方式,版本升級(jí)只需執(zhí)行一條指令,即可在毫秒級(jí)別完成代碼的升級(jí),同時(shí)對(duì)客戶端請(qǐng)求無(wú)任何影響。

除了以上幾點(diǎn),也做了很多其它的優(yōu)化,如主從延遲時(shí)間檢測(cè),危險(xiǎn)命令認(rèn)證等。通過(guò)逐步的優(yōu)化,內(nèi)部的 Redis 版本也開(kāi)始進(jìn)入穩(wěn)定期,應(yīng)用規(guī)模也在持續(xù)的增加。
業(yè)務(wù)極致定制化時(shí)代(2012 - 2013)
RedisCounter / LongSet
在某些特定的業(yè)務(wù)場(chǎng)景下,隨著業(yè)務(wù)規(guī)模的持續(xù)增加, Redis 的使用又暴露出來(lái)一些問(wèn)題,尤其是服務(wù)成本問(wèn)題(小編:是省服務(wù)器的意思?)。為此結(jié)合特定的業(yè)務(wù)場(chǎng)景我們對(duì) Redis 做了一些定制的優(yōu)化。這里主要介紹一下在關(guān)系和計(jì)數(shù)兩個(gè)業(yè)務(wù)場(chǎng)景下做的定制優(yōu)化。
- 關(guān)系
微博關(guān)系業(yè)務(wù)包含添加、取消關(guān)注,判斷關(guān)注關(guān)系等相關(guān)的業(yè)務(wù)邏輯,引入 Redis 后使用的是 hash 數(shù)據(jù)結(jié)構(gòu),并且當(dāng)作存儲(chǔ)使用。但是隨著用戶規(guī)模的快速增長(zhǎng),關(guān)系服務(wù) Redis 容量達(dá)到十幾 TB,并且還在快速的增長(zhǎng),如何應(yīng)對(duì)成本壓力?
為了解決服務(wù)成本問(wèn)題,我們把 Redis 的角色由 storage 調(diào)整為 cache。
這是因?yàn)殡S著用戶數(shù)量的增長(zhǎng),業(yè)務(wù)模型由初期的熱點(diǎn)數(shù)據(jù)不集中已經(jīng)轉(zhuǎn)變?yōu)橛忻黠@的冷熱之分。對(duì)于關(guān)注關(guān)系變更、判斷關(guān)注關(guān)系,hash 數(shù)據(jù)結(jié)構(gòu)是最佳的數(shù)據(jù)結(jié)構(gòu),但是存在以下問(wèn)題:
- cache miss 后回寫(xiě)關(guān)注列表性能差,對(duì)于關(guān)注數(shù)較多的微博會(huì)員,回寫(xiě)操作耗時(shí)可達(dá)到 10ms,這對(duì)于單線程的 Redis 來(lái)說(shuō)是致命的;
- Redis hash 結(jié)構(gòu)的內(nèi)存使用率不高,要保證 cahce 的命中率所需的 cache 容量仍然是很大的。
于是,我們定制了 longset 數(shù)據(jù)結(jié)構(gòu),它是一個(gè)“固定長(zhǎng)度開(kāi)放尋址的 hash 數(shù)組”,通過(guò)選擇合適的 hash 算法及數(shù)組填充率,可實(shí)現(xiàn)關(guān)注關(guān)系變更及判斷的性能與原生 Redis hash 相當(dāng),同時(shí) cache miss 后通過(guò) client 重建 longset 結(jié)構(gòu),實(shí)現(xiàn) O(1) 復(fù)雜度回寫(xiě)。
通過(guò)定制 longset 數(shù)據(jù)結(jié)構(gòu),將關(guān)系 Redis 內(nèi)存占用降低了一個(gè)數(shù)量級(jí)(小編:這該節(jié)約了多少服務(wù)器……發(fā)獎(jiǎng)金了嗎?),同時(shí)保證了服務(wù)性能。

計(jì)數(shù)
微博有很多計(jì)數(shù)場(chǎng)景,如用戶緯度的關(guān)注數(shù)、粉絲數(shù),微博緯度的轉(zhuǎn)發(fā)數(shù)、評(píng)論數(shù)等。計(jì)數(shù)作為微博中一項(xiàng)很重要的數(shù)據(jù),在微博業(yè)務(wù)中承擔(dān)了很重要的角色。為更好的滿足計(jì)數(shù)業(yè)務(wù)需求,我們基于 Redis 定制了內(nèi)部的計(jì)數(shù)服務(wù)。
原生 Redis 為了支持多數(shù)據(jù)類型,需要維護(hù)很多指針信息,存儲(chǔ)一份業(yè)務(wù)計(jì)數(shù)要占到約 80 個(gè)字節(jié),內(nèi)存利用率很低。為此我們定制了第一版計(jì)數(shù)器 Redis counter,通過(guò)預(yù)先分配內(nèi)存數(shù)組存儲(chǔ)計(jì)數(shù),并且采用 doublehash 解決沖突,減少了原生 Redis 大量的指針開(kāi)銷。通過(guò)以上優(yōu)化將內(nèi)存成本降低到原來(lái)的 1/4 以下。(小編:又節(jié)約了 3 / 4 服務(wù)器……)
隨著微博的發(fā)展,微博緯度的計(jì)數(shù)不斷增加,在原來(lái)的轉(zhuǎn)發(fā)數(shù)、評(píng)論數(shù)基礎(chǔ)上,又增加了表態(tài)數(shù),2013 年還上線了閱讀數(shù)。 Redis counter 已不能很好的解決這類擴(kuò)展問(wèn)題:
- 存儲(chǔ)單條微博相關(guān)的計(jì)數(shù),需要重復(fù)存儲(chǔ)微博 mid 信息,并且數(shù)據(jù)全部存儲(chǔ)在內(nèi)存,服務(wù)成本較高;
- 獲取單條微博全部的計(jì)數(shù),需要調(diào)用多次計(jì)數(shù)接口,對(duì)服務(wù)端壓力很大。
為此我們又設(shè)計(jì)了改進(jìn)版的計(jì)數(shù)器 CounterService,增加如下特性:
- Schema 支持多列:支持動(dòng)態(tài)加列,內(nèi)存使用精簡(jiǎn)到 bit
- 冷熱數(shù)據(jù)分離:頻繁訪問(wèn)的熱數(shù)據(jù)存儲(chǔ)在 memory,訪問(wèn)較少的冷數(shù)據(jù)存儲(chǔ)在磁盤(pán),降低服務(wù)成本
- LRU 緩存冷數(shù)據(jù):增加 LRU 模塊,緩存訪問(wèn)到的冷數(shù)據(jù),保證冷數(shù)據(jù)的訪問(wèn)性能。
- 異步 IO 線程訪問(wèn)冷數(shù)據(jù):避免冷數(shù)據(jù)的訪問(wèn)影響服務(wù)的整體性能

通過(guò)以上的定制優(yōu)化,我們從根本上解決了計(jì)數(shù)業(yè)務(wù)的成本及性能問(wèn)題。
除了以上關(guān)系、計(jì)數(shù)業(yè)務(wù)場(chǎng)景的定制優(yōu)化,為了滿足判斷類業(yè)務(wù)場(chǎng)景需求,定制了 BloomFilter 服務(wù);為了滿足 feed 聚合業(yè)務(wù)場(chǎng)景需求,定制了 VerctorService 服務(wù);為了降低服務(wù)成本,定制了 SSDCache 服務(wù)等。(小編:老板感動(dòng)得流淚了)
服務(wù)化時(shí)代(2014 -)
Cache Service、SSD Cache
隨著微博業(yè)務(wù)的快速增長(zhǎng),Redis 集群規(guī)模也在持續(xù)增加,目前微博 Redis 集群內(nèi)存占用數(shù)十 TB,服務(wù)于數(shù)百個(gè)業(yè)務(wù)線,Redis 集群的管理依然面臨很多的問(wèn)題。
數(shù)據(jù)遷移問(wèn)題
隨著時(shí)間推移,越來(lái)越多的業(yè)務(wù)由于數(shù)據(jù)量的增加,單端口到內(nèi)存占用已經(jīng)達(dá)到上限,微博內(nèi)部建議單端口內(nèi)存不超過(guò) 20GB,因此需要重新拆分端口,這就涉及到數(shù)據(jù)遷移,目前遷移操作是通過(guò)內(nèi)部開(kāi)發(fā)的一個(gè)遷移工具來(lái)完成的,遷移操作的成本相對(duì)較高。
數(shù)據(jù)路由問(wèn)題
目前的使用方式,需要在業(yè)務(wù)代碼中實(shí)現(xiàn)數(shù)據(jù)路由規(guī)則,路由規(guī)則的變更需要重新上線代碼,業(yè)務(wù)變更復(fù)雜度較高。同時(shí)節(jié)點(diǎn)配置采用 DNS 的方式,存在實(shí)時(shí)性和負(fù)載不均的問(wèn)題,雖然使用過(guò)程中有對(duì)應(yīng)的解決策略,但是需要一定的運(yùn)維干預(yù),運(yùn)維復(fù)雜度較高。
HA 系統(tǒng)不成熟
當(dāng)前的 HA 系統(tǒng)更多的是采用自動(dòng)發(fā)現(xiàn)問(wèn)題,手動(dòng)確認(rèn)處理的策略,沒(méi)有實(shí)現(xiàn)真正意義的自動(dòng)化,運(yùn)維成本依然很高。
為了解決以上問(wèn)題,我們?cè)?Redis 基礎(chǔ)上實(shí)現(xiàn)服務(wù)化框架 CacheService。
CacheService 最早是為了解決內(nèi)部使用 memcached 遇到的問(wèn)題而開(kāi)發(fā)的服務(wù)化框架,主要包含以下幾個(gè)模塊:
配置中心 ConfigServer
微博內(nèi)部的配置服務(wù)中心,主要是管理靜態(tài)配置和動(dòng)態(tài)命名服務(wù)的一個(gè)遠(yuǎn)程服務(wù),并能夠在配置發(fā)生變更的時(shí)候?qū)崟r(shí)通知監(jiān)聽(tīng)的 ConfigClient。
資源層
實(shí)際的數(shù)據(jù)存儲(chǔ)引擎,初期支持 memcached,后續(xù)又?jǐn)U展了 Redis、SSDCache 組件,其中 SSDCache 是為了降低服務(wù)成本,內(nèi)部開(kāi)發(fā)的基于 SSD 的存儲(chǔ)組件,用于緩存介于 memory 和 DB 之間的 warm 數(shù)據(jù)。
代理層
代理業(yè)務(wù)端的請(qǐng)求,并基于設(shè)定的路由規(guī)則轉(zhuǎn)發(fā)到后端的 cache 資源,它本身是無(wú)狀態(tài)的。proxy 啟動(dòng)后會(huì)去從 ConfigServer 加載后端 cache 資源的配置列表進(jìn)行初始化,并接收 ConfigServer 的配置變更的實(shí)時(shí)通知。
客戶端
提供給業(yè)務(wù)方使用的 SDK 包,通過(guò)它不需要在業(yè)務(wù)代碼中實(shí)現(xiàn)數(shù)據(jù)路由規(guī)則,業(yè)務(wù)方也無(wú)需關(guān)心后端 cache 的資源。只需要簡(jiǎn)單配置所使用的服務(wù)池名 group 和業(yè)務(wù)標(biāo)識(shí) namespace 即可使用 cache 資源,client 從 ConfigServer 獲取 proxy 的節(jié)點(diǎn)列表,選擇合適的 proxy 節(jié)點(diǎn)發(fā)送請(qǐng)求,支持多種負(fù)載均衡策略,同時(shí)會(huì)自動(dòng)探測(cè) proxy 節(jié)點(diǎn)變更。
集群管理系統(tǒng) ClusterManager
管理集群中各個(gè)組件的運(yùn)行狀態(tài)以保證業(yè)務(wù)的 SLA 指標(biāo),當(dāng)出現(xiàn)異常時(shí)會(huì)自動(dòng)執(zhí)行運(yùn)維處理。同時(shí)配置變更、數(shù)據(jù)遷移等集群操作也都是由它來(lái)負(fù)責(zé)。

為支持 Redis 服務(wù)化,在服務(wù)化框架擴(kuò)展支持了 Redis proxy,同時(shí)為了實(shí)
現(xiàn)在線數(shù)據(jù)遷移,參照 Redis cluster 的設(shè)計(jì)思想,對(duì)內(nèi)部 Redis 存儲(chǔ)做了改造,支持 slot 數(shù)據(jù)分片,數(shù)據(jù)遷移操作由 ClusterManager 組件執(zhí)行,完成 slot 的重新規(guī)劃及數(shù)據(jù)遷移。此外還支持 Redis 的 failover 機(jī)制,在master 或 slave 節(jié)點(diǎn)故障時(shí)會(huì)自動(dòng)執(zhí)行容錯(cuò)處理。我們 Redis 服務(wù)化項(xiàng)目 tribe 是從 2015 年底開(kāi)始上線,處于逐步完善過(guò)程中。
總結(jié)
從對(duì) Redis 的優(yōu)化歷程可以看出,技術(shù)的進(jìn)步是由業(yè)務(wù)的需求推動(dòng)的,我們需要擁抱需求。同時(shí)對(duì)于一個(gè)服務(wù)我們需要持續(xù)優(yōu)化并保證服務(wù)的運(yùn)維友好性才能保證服務(wù)的生命力。后續(xù)的一些計(jì)劃,完善服務(wù)化體系中冷熱數(shù)據(jù)分級(jí)存儲(chǔ)機(jī)制以降低服務(wù)成本;引入新的組件以更好的滿足業(yè)務(wù)需求、進(jìn)一步完善集群管理組件降低運(yùn)維復(fù)雜度。
作者:劉東輝
劉東輝,新浪微博基礎(chǔ)架構(gòu)組研發(fā)工程師。2013 年加入微博,先后參與微博 Redis、CounterService、SSDCache、CacheService 等基礎(chǔ)組件的設(shè)計(jì)與開(kāi)發(fā)工作,目前專注于分布式緩存、存儲(chǔ)方向。