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