Redis 大数据量(百亿级)Key存储需求及解决方案
點(diǎn)擊下方公眾號(hào)「關(guān)注」和「星標(biāo)」
回復(fù)“1024”獲取獨(dú)家整理的學(xué)習(xí)資料!
最近我在思考實(shí)時(shí)數(shù)倉(cāng)問(wèn)題的時(shí)候,想到了巨量的redis的存儲(chǔ)的問(wèn)題,然后翻閱到這篇文章,與各位分享。
需求背景
該應(yīng)用場(chǎng)景為DMP緩存存儲(chǔ)需求,DMP需要管理非常多的第三方id數(shù)據(jù),其中包括各媒體cookie與自身cookie(以下統(tǒng)稱supperid)的mapping關(guān)系,還包括了supperid的人口標(biāo)簽、移動(dòng)端id(主要是idfa和imei)的人口標(biāo)簽,以及一些黑名單id、ip等數(shù)據(jù)。
在hdfs的幫助下離線存儲(chǔ)千億記錄并不困難,然而DMP還需要提供毫秒級(jí)的實(shí)時(shí)查詢。由于cookie這種id本身具有不穩(wěn)定性,所以很多的真實(shí)用戶的瀏覽行為會(huì)導(dǎo)致大量的新cookie生成,只有及時(shí)同步mapping的數(shù)據(jù)才能命中DMP的人口標(biāo)簽,無(wú)法通過(guò)預(yù)熱來(lái)獲取較高的命中,這就跟緩存存儲(chǔ)帶來(lái)了極大的挑戰(zhàn)。
經(jīng)過(guò)實(shí)際測(cè)試,對(duì)于上述數(shù)據(jù),常規(guī)存儲(chǔ)超過(guò)五十億的kv記錄就需要1T多的內(nèi)存,如果需要做高可用多副本那帶來(lái)的消耗是巨大的,另外kv的長(zhǎng)短不齊也會(huì)帶來(lái)很多內(nèi)存碎片,這就需要超大規(guī)模的存儲(chǔ)方案來(lái)解決上述問(wèn)題。
存儲(chǔ)何種數(shù)據(jù)
人?標(biāo)簽主要是cookie、imei、idfa以及其對(duì)應(yīng)的gender(性別)、age(年齡段)、geo(地域)等;mapping關(guān)系主要是媒體cookie對(duì)supperid的映射。以下是數(shù)據(jù)存儲(chǔ)?示例:
PC端的ID:媒體編號(hào)-媒體cookie=>supperid
Device端的ID:
顯然PC數(shù)據(jù)需要存儲(chǔ)兩種key=>value還有key=>hashmap,?而Device數(shù)據(jù)需要存儲(chǔ)?一種key=>hashmap即可。
數(shù)據(jù)特點(diǎn)
短key短value:
媒體自身的cookie長(zhǎng)短不一;
需要為全量數(shù)據(jù)提供服務(wù),supperid是百億級(jí)、媒體映射是千億級(jí)、移動(dòng)id是幾十億級(jí);
每天有十億級(jí)別的mapping關(guān)系產(chǎn)生;
對(duì)于較大時(shí)間窗口內(nèi)可以預(yù)判熱數(shù)據(jù)(有一些存留的穩(wěn)定cookie);
對(duì)于當(dāng)前mapping數(shù)據(jù)無(wú)法預(yù)判熱數(shù)據(jù),有很多是新生成的cookie;
存在的技術(shù)挑戰(zhàn)
1)長(zhǎng)短不一容易造成內(nèi)存碎片;
2)由于指針大量存在,內(nèi)存膨脹率比較高,一般在7倍,純內(nèi)存存儲(chǔ)通病;
3)雖然可以通過(guò)cookie的行為預(yù)判其熱度,但每天新生成的id依然很多(百分比比較敏感,暫不透露);
4)由于服務(wù)要求在公網(wǎng)環(huán)境(國(guó)內(nèi)公網(wǎng)延遲60ms以下)下100ms以內(nèi),所以原則上當(dāng)天新更新的mapping和人口標(biāo)簽需要全部in memory,而不會(huì)讓請(qǐng)求落到后端的冷數(shù)據(jù);
5)業(yè)務(wù)方面,所有數(shù)據(jù)原則上至少保留35天甚至更久;
6)內(nèi)存至今也比較昂貴,百億級(jí)Key乃至千億級(jí)存儲(chǔ)方案勢(shì)在必行!
解決方案
5.1 淘汰策略
存儲(chǔ)吃緊的一個(gè)重要原因在于每天會(huì)有很多新數(shù)據(jù)入庫(kù),所以及時(shí)清理數(shù)據(jù)尤為重要。主要方法就是發(fā)現(xiàn)和保留熱數(shù)據(jù)淘汰冷數(shù)據(jù)。網(wǎng)民的量級(jí)遠(yuǎn)遠(yuǎn)達(dá)不到幾十億的規(guī)模,id有一定的生命周期,會(huì)不斷的變化。所以很大程度上我們存儲(chǔ)的id實(shí)際上是無(wú)效的。而查詢其實(shí)前端的邏輯就是廣告曝光,跟人的行為有關(guān),所以一個(gè)id在某個(gè)時(shí)間窗口的(可能是一個(gè)campaign,半個(gè)月、幾個(gè)月)訪問(wèn)行為上會(huì)有一定的重復(fù)性。數(shù)據(jù)初始化之前,我們先利用hbase將日志的id聚合去重,劃定TTL的范圍,一般是35天,這樣可以砍掉近35天未出現(xiàn)的id。另外在Redis中設(shè)置過(guò)期時(shí)間是35天,當(dāng)有訪問(wèn)并命中時(shí),對(duì)key進(jìn)行續(xù)命,延長(zhǎng)過(guò)期時(shí)間,未在35天出現(xiàn)的自然淘汰。這樣可以針對(duì)穩(wěn)定cookie或id有效,實(shí)際證明,續(xù)命的方法對(duì)idfa和imei比較實(shí)用,長(zhǎng)期積累可達(dá)到非常理想的命中。
5.2 減少膨脹
Hash表空間大小和Key的個(gè)數(shù)決定了沖突率(或者用負(fù)載因子衡量),再合理的范圍內(nèi),key越多自然hash表空間越大,消耗的內(nèi)存自然也會(huì)很大。再加上大量指針本身是長(zhǎng)整型,所以內(nèi)存存儲(chǔ)的膨脹十分可觀。先來(lái)談?wù)勅绾伟裬ey的個(gè)數(shù)減少。
大家先來(lái)了解一種存儲(chǔ)結(jié)構(gòu)。我們期望將key1=>value1存儲(chǔ)在redis中,那么可以按照如下過(guò)程去存儲(chǔ)。先用固定長(zhǎng)度的隨機(jī)散列md5(key)值作為redis的key,我們稱之為BucketId,而將key1=>value1存儲(chǔ)在hashmap結(jié)構(gòu)中,這樣在查詢的時(shí)候就可以讓client按照上面的過(guò)程計(jì)算出散列,從而查詢到value1。
過(guò)程變化簡(jiǎn)單描述為:get(key1) -> hget(md5(key1), key1) 從而得到value1。如果我們通過(guò)預(yù)先計(jì)算,讓很多key可以在BucketId空間里碰撞,那么可以認(rèn)為一個(gè)BucketId下面掛了多個(gè)key。比如平均每個(gè)BucketId下面掛10個(gè)key,那么理論上我們將會(huì)減少超過(guò)90%的redis key的個(gè)數(shù)。
具體實(shí)現(xiàn)起來(lái)有一些麻煩,而且用這個(gè)方法之前你要想好容量規(guī)模。我們通常使用的md5是32位的hexString(16進(jìn)制字符),它的空間是128bit,這個(gè)量級(jí)太大了,我們需要存儲(chǔ)的是百億級(jí),大約是33bit(2的33次方),所以我們需要有一種機(jī)制計(jì)算出合適位數(shù)的散列,而且為了節(jié)約內(nèi)存,我們需要利用全部字符類型(ASCII碼在0~127之間)來(lái)填充,而不用HexString,這樣Key的長(zhǎng)度可以縮短到一半。
下面是具體的實(shí)現(xiàn)方式
public?static?byte?[]?getBucketId(byte?[]?key,?Integer?bit)?{MessageDigest?mdInst?=?MessageDigest.getInstance("MD5");mdInst.update(key);byte?[]?md?=?mdInst.digest();byte?[]?r?=?new?byte[(bit-1)/7?+?1];//?因?yàn)橐粋€(gè)字節(jié)中只有7位能夠表示成單字符,ascii碼是7位int?a?=?(int)?Math.pow(2,?bit%7)-2;md[r.length-1]?=?(byte)?(md[r.length-1]?&?a);System.arraycopy(md,?0,?r,?0,?r.length);for(int?i=0;i<r.length;i++)?{if(r[i]<0)?r[i]?&=?127;}return?r; }參數(shù)bit決定了最終BucketId空間的大小,空間大小集合是2的整數(shù)冪次的離散值。這里解釋一下為何一個(gè)字節(jié)中只有7位可用,是因?yàn)閞edis存儲(chǔ)key時(shí)需要是ASCII(0~127),而不是byte array。如果規(guī)劃百億級(jí)存儲(chǔ),計(jì)劃每個(gè)桶分擔(dān)10個(gè)kv,那么我們只需2^30=1073741824的桶個(gè)數(shù)即可,也就是最終key的個(gè)數(shù)。
5.3 減少碎片
碎片主要原因在于內(nèi)存無(wú)法對(duì)齊、過(guò)期刪除后,內(nèi)存無(wú)法重新分配。通過(guò)上文描述的方式,我們可以將人口標(biāo)簽和mapping數(shù)據(jù)按照上面的方式去存儲(chǔ),這樣的好處就是redis key是等長(zhǎng)的。
另外對(duì)于hashmap中的key我們也做了相關(guān)優(yōu)化,截取cookie或者deviceid的后六位作為key,這樣也可以保證內(nèi)存對(duì)齊,理論上會(huì)有沖突的可能性,但在同一個(gè)桶內(nèi)后綴相同的概率極低(試想id幾乎是隨機(jī)的字符串,隨意10個(gè)由較長(zhǎng)字符組成的id后綴相同的概率*桶樣本數(shù)=發(fā)生沖突的期望值<<0.05,也就是說(shuō)出現(xiàn)一個(gè)沖突樣本則是極小概率事件,而且這個(gè)概率可以通過(guò)調(diào)整后綴保留長(zhǎng)度控制期望值)。而value只存儲(chǔ)age、gender、geo的編碼,用三個(gè)字節(jié)去存儲(chǔ)。
另外提一下,減少碎片還有個(gè)很low但是有效的方法,將slave重啟,然后強(qiáng)制的failover切換主從,這樣相當(dāng)于給master整理的內(nèi)存的碎片。
推薦Google-tcmalloc, facebook-jemalloc內(nèi)存分配,可以在value不大時(shí)減少內(nèi)存碎片和內(nèi)存消耗。有人測(cè)過(guò)大value情況下反而libc更節(jié)約。
作者:小熱愛?
來(lái)源:juejin.cn/post/6956147115286822948
推薦閱讀?點(diǎn)擊標(biāo)題可跳轉(zhuǎn)
這篇 ElasticSearch 詳細(xì)使用教程,內(nèi)部分享時(shí)被老大表?yè)P(yáng)了
放棄 Maven 之后,我用它!!!速度賊快
最牛逼的故障診斷工具!秒級(jí)定位線上問(wèn)題
我賣掉北京 500 萬(wàn)的房子,在老家生活的這兩年…
最牛逼的性能監(jiān)控系統(tǒng)!集強(qiáng)大功能于一身
挺帶勁!通過(guò) Nginx 來(lái)實(shí)現(xiàn)封殺惡意訪問(wèn)
Zabbix 通過(guò) API 監(jiān)控 Kubernetes
面試官:為什么 delete 表數(shù)據(jù),磁盤空間卻還是被占用
如何快速定位當(dāng)前數(shù)據(jù)庫(kù)消耗 CPU 最高的 sql 語(yǔ)句?
總結(jié)
以上是生活随笔為你收集整理的Redis 大数据量(百亿级)Key存储需求及解决方案的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: verilog设计简易正弦波信号发生器_
- 下一篇: java将中文转换为pinyin/繁简互