【Redis】大数据量(百亿级)Key存储需求及解决方案
问题导读:
1. 需求背景是什么?
2. 存储何种数据?
3. 数据特点是什么?
4. 存在哪些技术挑战?
5. 解决方案有哪些?
6. md5散列桶的方法需要注意哪些问题?
7. 测试结果是什么
原文链接: 问题导读: 1. 需求背景是什么? 2. 存储何种数据? 3. 数据特点是什么? 4. 存在哪些技术挑战? 5. 解决方案有哪些? 6. md5散列桶的方法需要注意哪些问题? 7. 测试结果是什么? 解决方案: 1 需求背景 该应用场景为 DMP(Data Management Platform:数据管理平台,是把分散的多方数据进行整合纳入统一的技术平台,并对这些数据进行标准化和细分,让用户可以把这些细分结果推向现有的互动营销环境里的平台)缓存存储需求,DMP 需要管理非常多的第三方 id 数据,其中包括各媒体 cookie 与自身 cookie(以下统称supperid)的 mapping 关系,还包括了 supperid 的人口标签、移动端 id(主要是idfa(Identifierfor Advertising:广告标识符)和 imei(International Mobile Equipment Identity:国际移动设备识别码的缩写。俗称“手机串号”、“手机串码”、“手机序列号”,用于在GSM移动网络中识别每一部独立的手机,相当于手机的身份证号码))的人口标签,以及一些黑名单 id、ip 等数据。 在 hdfs(Hadoop 分布式文件系统)的帮助下离线存储千亿记录并不困难,然而 DMP 还需要提供毫秒级的实时查询。由于 cookie 这种 id 本身的不稳定性,所以很多真实用户的浏览行为会导致大量新的 cookie 生成,只有及时同步 mapping 的数据才能命中 DMP 的人口标签,无法通过预热来获取较高的命中,这就给缓存存储带来了极大的挑战。 经过实际测试,对于上述数据,常规存储超过五十亿的 kv 记录就需要 1T 多的内存,如果需要做高可用多副本大数据数据存储,那带来的消耗是巨大的,另外 kv 的长短不齐也会带来很多内存碎片,这就需要超大规模的存储方案来解决上述问题。 2 存储何种数据 人口标签主要是 cookie、imei、idfa 以及其对应的 gender(性别)、age(年龄段)、geo(地域)等;mapping 关系主要是媒体 cookie 对 supperid 的映射。以下是数据存储示例: 1) PC 端的ID: 媒体编号 - 媒体cookie => supperid supperid => { age => 年龄段编码, gender => 性别编码, geo => 地理位置编码 } 2) Device 端的ID: imei or idfa => { age => 年龄段编码, gender => 性别编码, geo => 地理位置编码 } 显然,PC 数据需要存储两种:key => value 和 key => hashmap,而 Device 数据需要存储一种 key => hashmap 即可。 3 数据特点 短key短value,其中supperid为21位数字:比如 160524201514168952222;imei为小写md5:比如2d131005dc0f37d362a5d97094103633;idfa 为大写带”- ”md5:比如:51DFFC83-9541-4411-FA4F-356927E39D04;媒体自身的 cookie 长短不一;需要为全量数据提供服务,supperid 是百亿级、媒体映射是千亿级、移动id是几十亿级;每天有十亿级别的 mapping 关系产生;对于较大时间窗口内可以预判热数据(有一些存留的稳定 cookie);对于当前 mapping 数据无法预判热数据,有很多是新生成的 cookie; 4 存在的技术挑战 1)长短不一容易造成内存碎片; 2)由于指针大量存在,内存膨胀率比较高,一般在7倍,纯内存存储通病; 3)虽然可以通过 cookie 的行为预判其热度,但每天新生成的 id 依然很多(百分比比较敏感,暂不透露); 4)由于服务要求在公网环境(国内公网延迟 60ms 以下)下 100ms 以内,所以原则上当天新更新的 mapping和人口标签需要全部 in memory,从而不会让请求落到后端的冷数据; 5)业务方面,所有数据原则上至少保留 35 天甚至更久; 6)内存至今也比较昂贵,百亿级Key乃至千亿级存储方案势在必行! 5 解决方案 5.1 淘汰策略 存储吃紧的一个重要原因在于每天会有很多新数据入库,所以及时清理数据尤为重要。主要方法就是发现和保留热数据并且淘汰冷数据。 网民的量级远远达不到几十亿的规模,id 有一定的生命周期,会不断的变化。所以很大程度上我们存储的 id 实际上是无效的。而查询其实(前端的逻辑)就是广告曝光,跟人的行为有关,所以一个 id 在某个时间窗口的(可能是一个 campaign(广告Campaign是指广告主在一段明确的期间里(如一年),推出一系列拥有共同主题或讯息的广告,以期建立广告讯息的累积效果,塑造品牌与企业一致的形象,并给予目标受众持续而深刻的刺激与冲击),半个月、几个月)访问行为上会有一定的重复性。 数据初始化之前,我们先利用 hbase(HBase是一个分布式的、面向列的开源数据库,HBase不同于一般的关系数据库,它是一个适合于非结构化数据存储的数据库。另一个不同的是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(key1) 值作为 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,所以我们需要有一种机制计算出合适位数的散列,而且为了节约内存,我们需要利用全部字符类型(ASCII码在0~127之间)来填充,而不用HexString,这样Key的长度可以缩短到一半。下面是具体的实现方式:
参数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后缀相同的概率*桶样本数=发生冲突的期望值 (编辑:威海站长网) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |