面對海量數(shù)據(jù)的計(jì)數(shù)器要如何做?
在地鐵上,你可能經(jīng)常使用微博瀏覽、點(diǎn)贊熱門話題,甚至參與抽獎活動并轉(zhuǎn)發(fā)相關(guān)內(nèi)容。這些行為涉及到微博數(shù)據(jù)統(tǒng)計(jì)中的各種指標(biāo),主要包括:
- 微博的互動數(shù)據(jù):評論數(shù)、點(diǎn)贊數(shù)、轉(zhuǎn)發(fā)數(shù)、瀏覽數(shù)、表態(tài)數(shù)等;
 - 用戶的社交數(shù)據(jù):粉絲數(shù)、關(guān)注數(shù)、發(fā)布微博數(shù)、私信數(shù)等。
 
微博維度的計(jì)數(shù)代表了一條微博在平臺上的受歡迎程度,而用戶維度的數(shù)據(jù),特別是粉絲數(shù),則反映了用戶在微博社交網(wǎng)絡(luò)中的影響力和受關(guān)注程度。這些計(jì)數(shù)信息對于用戶和平臺都具有重要意義
但在設(shè)計(jì)計(jì)數(shù)系統(tǒng)時,不少人會出現(xiàn)性能不高、存儲成本很大的問題,比如,把計(jì)數(shù)與微博數(shù)據(jù)存儲在一起,這樣每次更新計(jì)數(shù)的時候都需要鎖住這一行記錄,降低了寫入的并發(fā)。在我看來,之所以出現(xiàn)這些問題,還是因?yàn)槟銓τ?jì)數(shù)系統(tǒng)的設(shè)計(jì)和優(yōu)化不甚了解,所以要想解決痛點(diǎn),你有必要形成完備的設(shè)計(jì)方案。
計(jì)數(shù)在業(yè)務(wù)上的特點(diǎn)
微博系統(tǒng)中微博條目的數(shù)量已經(jīng)超過了千億級別。僅僅計(jì)算微博的轉(zhuǎn)發(fā)、評論、點(diǎn)贊、瀏覽等核心計(jì)數(shù),其數(shù)據(jù)量級已經(jīng)達(dá)到了幾千億的級別。而微博條目的數(shù)量還在不斷高速增長,隨著微博業(yè)務(wù)的不斷發(fā)展,微博維度的計(jì)數(shù)種類也可能會持續(xù)擴(kuò)展(比如增加了表態(tài)數(shù))。因此,僅僅是微博維度上的計(jì)數(shù)量級就已經(jīng)過了萬億級別。
此外,微博的用戶量級已經(jīng)超過了 10 億,用戶維度的計(jì)數(shù)量級相比微博維度來說雖然相差很大,但也達(dá)到了百億級別。面對如此龐大的數(shù)據(jù)量,如何存儲這些過萬億級別的數(shù)字,對我們來說確實(shí)是一大挑戰(zhàn)。
考慮到訪問量大和性能要求高的情況,對于微博這樣擁有數(shù)億活躍用戶的社交平臺來說,計(jì)數(shù)系統(tǒng)需要能夠應(yīng)對每秒數(shù)百萬次的訪問量,同時要求在毫秒級別內(nèi)返回結(jié)果。為了達(dá)到這樣的性能要求,我們可以采取一些簡單而有效的方法,比如選擇高性能的存儲和緩存技術(shù),優(yōu)化數(shù)據(jù)庫設(shè)計(jì)和查詢,采用分布式架構(gòu),以及設(shè)置負(fù)載均衡和故障恢復(fù)機(jī)制。這樣可以保證系統(tǒng)在高并發(fā)情況下仍然能夠快速、穩(wěn)定地處理大量請求,滿足用戶的需求
支撐高并發(fā)的計(jì)數(shù)系統(tǒng)要如何設(shè)計(jì)
在最初設(shè)計(jì)計(jì)數(shù)系統(tǒng)時,微博的流量還沒有現(xiàn)在這么龐大。我們遵循了KISS(Keep It Simple and Stupid)原則,選擇了使用MySQL來存儲計(jì)數(shù)數(shù)據(jù)。這是因?yàn)镸ySQL是我們團(tuán)隊(duì)最熟悉的數(shù)據(jù)庫,我們在運(yùn)維方面也有豐富的經(jīng)驗(yàn)。舉個具體的例子來說,我們將微博的計(jì)數(shù)數(shù)據(jù)存儲在MySQL數(shù)據(jù)庫中的單個表中,每個微博對應(yīng)一行記錄,包括評論數(shù)、點(diǎn)贊數(shù)、轉(zhuǎn)發(fā)數(shù)等計(jì)數(shù)數(shù)據(jù)列。這樣的設(shè)計(jì)簡單易于實(shí)現(xiàn)和維護(hù),符合我們當(dāng)時的需求和團(tuán)隊(duì)的技術(shù)水平。
以微博 ID 為主鍵,然后將轉(zhuǎn)發(fā)數(shù)、評論數(shù)、點(diǎn)贊數(shù)和瀏覽數(shù)等微博維度的計(jì)數(shù)數(shù)據(jù)分別存儲在單獨(dú)的列中,這樣可以方便地通過一條SQL語句來獲取特定微博的計(jì)數(shù)數(shù)據(jù)。例如:
select repost_count, comment_count, praise_count, view_count from t_weibo_count where weibo_id = ?在數(shù)據(jù)量級和訪問量級都不大的情況下,采用以微博ID為主鍵,將轉(zhuǎn)發(fā)數(shù)、評論數(shù)、點(diǎn)贊數(shù)和瀏覽數(shù)等計(jì)數(shù)數(shù)據(jù)存儲在單個MySQL表中的方式是最簡單的。但隨著微博的不斷壯大,之前的計(jì)數(shù)系統(tǒng)面臨了諸多問題和挑戰(zhàn)。
隨著微博用戶數(shù)量和發(fā)布的微博數(shù)量迅速增加,計(jì)數(shù)數(shù)據(jù)量級也隨之飛速增長。當(dāng)MySQL數(shù)據(jù)庫單表的存儲量級達(dá)到幾千萬時,性能會受到損耗。因此,為了解決這些問題,我們考慮采用分庫分表的方式,將數(shù)據(jù)量分散存儲,以提升讀取計(jì)數(shù)數(shù)據(jù)的性能。
我們用“weibo_id”作為分區(qū)鍵,在選擇分庫分表的方式時,考慮了下面兩種:
對于分庫分表的方式,有兩種常見的策略可以考慮。一種是根據(jù)微博ID進(jìn)行哈希分庫分表,另一種是根據(jù)微博ID生成的時間來進(jìn)行分庫分表。
首先,根據(jù)哈希算法對weibo_id計(jì)算哈希值,然后根據(jù)這個哈希值確定需要存儲到哪一個數(shù)據(jù)庫的哪一張表中。這種方法可以將數(shù)據(jù)均勻地分散到多個數(shù)據(jù)庫和表中,以實(shí)現(xiàn)負(fù)載均衡和提升讀取性能。
另一種方式是按照weibo_id生成的時間來進(jìn)行分庫分表。可以利用發(fā)號器生成的ID中的時間戳信息,將微博數(shù)據(jù)按照時間戳進(jìn)行分庫分表,比如每天一張表或者每月一張表等。這樣可以根據(jù)微博的發(fā)布時間快速定位到對應(yīng)的數(shù)據(jù)庫和表,便于數(shù)據(jù)的管理和查詢。
因?yàn)樵绞亲罱l(fā)布的微博,計(jì)數(shù)數(shù)據(jù)的訪問量就越大,所以雖然我考慮了兩種方案,但是按照時間來分庫分表會造成數(shù)據(jù)訪問的不均勻,最后用了哈希的方式來做分庫分表。
圖片
在微博最初的版本中,首頁信息流并不展示計(jì)數(shù)數(shù)據(jù),因此使用MySQL可以承受當(dāng)時的計(jì)數(shù)數(shù)據(jù)讀取訪問量。但隨著微博的發(fā)展,首頁信息流也開始展示轉(zhuǎn)發(fā)、評論和點(diǎn)贊等計(jì)數(shù)數(shù)據(jù),導(dǎo)致信息流的訪問量急劇增加。僅僅依靠數(shù)據(jù)庫已無法滿足如此高的并發(fā)讀取需求。
為了應(yīng)對這一挑戰(zhàn),我們考慮使用Redis來加速讀請求。通過部署多個Redis從節(jié)點(diǎn)來提升可用性和性能,并通過Hash的方式對數(shù)據(jù)進(jìn)行分片,以保證計(jì)數(shù)的讀取性能。然而,采用數(shù)據(jù)庫+緩存的方式存在一個嚴(yán)重的弊端:無法保證數(shù)據(jù)的一致性。例如,如果數(shù)據(jù)庫寫入成功而緩存更新失敗,就會導(dǎo)致數(shù)據(jù)不一致,從而影響計(jì)數(shù)的準(zhǔn)確性。
因此,為了解決數(shù)據(jù)一致性的問題,我們最終決定完全拋棄MySQL,全面采用Redis作為計(jì)數(shù)的存儲組件。Redis的高性能和內(nèi)存存儲特性使其能夠輕松應(yīng)對高并發(fā)的讀取請求,并且通過持久化機(jī)制和主從復(fù)制,可以保證數(shù)據(jù)的持久性和可用性,同時也降低了數(shù)據(jù)不一致的風(fēng)險。
圖片
針對熱門微博高頻寫入的情況,可以考慮以下簡單的方法來降低寫入壓力:
- 異步處理: 將計(jì)數(shù)寫入操作異步化,先將操作記錄在消息隊(duì)列中,再由后臺任務(wù)異步處理寫入計(jì)數(shù)數(shù)據(jù),減輕數(shù)據(jù)庫的寫入壓力。
 - 計(jì)數(shù)緩存: 使用緩存暫時存儲計(jì)數(shù)數(shù)據(jù),減少對數(shù)據(jù)庫的直接寫入請求,提高寫入性能。
 - 合并寫入: 將相同微博的計(jì)數(shù)操作合并,減少數(shù)據(jù)庫的寫入次數(shù),如多個用戶同時轉(zhuǎn)發(fā)同一條微博時,將轉(zhuǎn)發(fā)操作合并為一次寫入計(jì)數(shù)數(shù)據(jù)的操作。
 - 分片存儲: 根據(jù)微博ID進(jìn)行分片存儲,將數(shù)據(jù)分散到不同存儲節(jié)點(diǎn)上,分散寫入壓力。
 - 寫入限流: 實(shí)行寫入限流策略,限制每個用戶或微博的寫入頻率,防止寫入請求過載數(shù)據(jù)庫。
 
圖片
如何降低計(jì)數(shù)系統(tǒng)的存儲成本
在微博這樣的場景下,我們面臨著處理萬億級別計(jì)數(shù)數(shù)據(jù)的挑戰(zhàn)。對于這種規(guī)模的數(shù)據(jù)存儲,我們需要在有限的成本下實(shí)現(xiàn)全量計(jì)數(shù)數(shù)據(jù)的存取。Redis作為內(nèi)存存儲系統(tǒng),相較于使用磁盤存儲的MySQL,存儲成本差異巨大。舉例來說,一臺服務(wù)器可以掛載2TB的磁盤,但內(nèi)存可能只有128GB,這意味著磁盤存儲空間是內(nèi)存的16倍。
Redis因其通用性而對內(nèi)存的使用較為粗放,存在大量指針和額外數(shù)據(jù)結(jié)構(gòu)開銷。比如,若要存儲一個KV類型的計(jì)數(shù)信息,鍵(Key)是8字節(jié)的長整型weibo_id,值(Value)是4字節(jié)整型的轉(zhuǎn)發(fā)數(shù),在Redis中將會占用超過70個字節(jié)的空間,這造成了空間的巨大浪費(fèi)。
在面對這一問題時,如何優(yōu)化存儲空間呢?
我建議對原生Redis進(jìn)行改造,采用新的數(shù)據(jù)結(jié)構(gòu)和數(shù)據(jù)類型來存儲計(jì)數(shù)數(shù)據(jù)。我的改造主要涉及兩點(diǎn):
首先,原生Redis在存儲Key時是按照字符串類型來存儲的。比如,一個8字節(jié)的Long類型的數(shù)據(jù),需要28個字節(jié)的存儲空間(8字節(jié)的字符串頭部信息 + 19字節(jié)的數(shù)字長度 + 1字節(jié)的字符串結(jié)尾標(biāo)志)。如果我們直接使用Long類型來存儲,只需要8個字節(jié),節(jié)省了20個字節(jié)的空間。
其次,我去除了原生Redis中多余的指針?,F(xiàn)在,如果要存儲一個鍵值對(KV)信息,只需要12個字節(jié)(8字節(jié)的weibo_id + 4字節(jié)的轉(zhuǎn)發(fā)數(shù)),相比之前有很大的改進(jìn)。
同時,我們也會使用一個大的數(shù)組來存儲計(jì)數(shù)信息,存儲的位置是基于 weibo_id 的哈希值來計(jì)算出來的,具體的算法像下面展示的這樣:
同時,我們也會使用一個大的數(shù)組來存儲計(jì)數(shù)信息,存儲的位置是基于 weibo_id 的哈希值來計(jì)算出來的,具體的算法像下面展示的這樣:在對原生Redis進(jìn)行改造后,我們還需要進(jìn)一步考慮如何節(jié)省內(nèi)存的使用。舉例來說,微博的計(jì)數(shù)數(shù)據(jù)包括轉(zhuǎn)發(fā)數(shù)、評論數(shù)、瀏覽數(shù)、點(diǎn)贊數(shù)等等。如果每個計(jì)數(shù)都需要存儲weibo_id,那么總共需要的存儲空間是48字節(jié)(8字節(jié)的weibo_id * 4個微博ID + 每個計(jì)數(shù)4字節(jié))。
然而,我們可以將相同微博ID的計(jì)數(shù)數(shù)據(jù)存儲在一起,這樣就只需要記錄一個微博ID,省去了多余的三個微博ID的存儲開銷。這樣一來,存儲空間就進(jìn)一步減少了。
不過,即使經(jīng)過上面的優(yōu)化,由于計(jì)數(shù)的量級實(shí)在是太過巨大,并且還在以極快的速度增長,所以如果我們以全內(nèi)存的方式來存儲計(jì)數(shù)信息,就需要使用非常多的機(jī)器來支撐。
針對微博計(jì)數(shù)數(shù)據(jù)具有明顯的熱點(diǎn)屬性的情況,我們考慮優(yōu)化計(jì)數(shù)服務(wù),增加SSD磁盤,將時間上較久遠(yuǎn)的數(shù)據(jù)存儲在磁盤上,內(nèi)存中只保留最近的數(shù)據(jù),以盡量減少服務(wù)器的使用。
具體做法是,將較久遠(yuǎn)的計(jì)數(shù)數(shù)據(jù)dump到SSD磁盤上,而內(nèi)存中僅保留最近的數(shù)據(jù)。當(dāng)需要讀取冷數(shù)據(jù)時,使用單獨(dú)的I/O線程異步地從SSD磁盤加載冷數(shù)據(jù)到一個單獨(dú)的Cold Cache中。

經(jīng)過以上優(yōu)化措施,我們的計(jì)數(shù)服務(wù)現(xiàn)在已經(jīng)能夠支撐高并發(fā)大數(shù)據(jù)量的考驗(yàn),無論是在性能、成本還是可用性方面都能夠滿足業(yè)務(wù)需求。通過微博設(shè)計(jì)計(jì)數(shù)系統(tǒng)的例子,我想強(qiáng)調(diào)的是,在系統(tǒng)設(shè)計(jì)過程中需要了解當(dāng)前系統(tǒng)面臨的痛點(diǎn),并針對這些痛點(diǎn)進(jìn)行細(xì)致的優(yōu)化。
舉例來說,微博計(jì)數(shù)系統(tǒng)的痛點(diǎn)是存儲成本。因此,我們在后期的優(yōu)化中主要圍繞如何使用有限的服務(wù)器存儲全量的計(jì)數(shù)數(shù)據(jù)展開。即使對開源組件(如Redis)進(jìn)行深度定制可能會增加運(yùn)維成本,但這些優(yōu)化都被視為實(shí)現(xiàn)計(jì)數(shù)系統(tǒng)的必要權(quán)衡。通過深入了解系統(tǒng)痛點(diǎn)并針對性地進(jìn)行優(yōu)化,我們能夠更好地提高系統(tǒng)的性能、降低成本,并確保系統(tǒng)的可用性。
總結(jié)
數(shù)據(jù)庫 + 緩存的方案是計(jì)數(shù)系統(tǒng)的初級階段,完全可以支撐中小訪問量和存儲量的存儲服務(wù)。如果你的項(xiàng)目還處在初級階段,量級還不是很大,那么你一開始可以考慮使用這種方案。
通過對原生 Redis 組件的改造,我們可以極大地減小存儲數(shù)據(jù)的內(nèi)存開銷。
使用 SSD+ 內(nèi)存的方案可以最終解決存儲計(jì)數(shù)數(shù)據(jù)的成本問題。這個方式適用于冷熱數(shù)據(jù)明顯的場景,你在使用時需要考慮如何將內(nèi)存中的數(shù)據(jù)做換入換出。
隨著互聯(lián)網(wǎng)技術(shù)的發(fā)展,越來越多的業(yè)務(wù)場景需要大量的內(nèi)存資源來存儲業(yè)務(wù)數(shù)據(jù),但對性能或延遲要求不高。全內(nèi)存存儲會帶來極大的成本浪費(fèi),因此一些開源組件開始支持使用SSD替代內(nèi)存存儲冷數(shù)據(jù),比如Pika和SSDB。我建議您了解它們的實(shí)現(xiàn)原理,以便在需要時在項(xiàng)目中使用。
在微博的計(jì)數(shù)服務(wù)中也采用了類似的思路,將熱點(diǎn)數(shù)據(jù)存儲在內(nèi)存中,而將冷數(shù)據(jù)存儲在SSD上,這樣既保證了性能,又降低了成本。如果您的業(yè)務(wù)需要大量內(nèi)存存儲熱點(diǎn)數(shù)據(jù),不妨考慮采用類似的思路來優(yōu)化您的系統(tǒng)。















 
 
 








 
 
 
 