https://zhuanlan.zhihu.com/p/89883126
开场: 如何判断一个大集合中是否含有某个元素?
背景:
为了最大化提升广告转化效果,业务方决定对接巨量引擎,广点通以及快手RTA服务。并针对自身情况决定是否出广告(新用户出,旧用户不出)。
大致示意图
要求: 1. QPS至少要能撑住30W。2. 接口响应不能超过60ms
面临的问题:
1.高并发——> 负载均衡这块交由中台完成(部署到k8s, 由40个pod分摊掉流量)。
2.低延迟 ——> 响应要快,计算不能太耗时。
调研:
对于低延迟,快速判断请求侧用户信息是不是老用户,需要对旧用户池进行检索。存储方面自然想到redis。
检索示意图
数据量级: 目前旧用户池约有6亿条数据, 考虑到设备标识多样性(IDFA, IMEI, Android_id..), 以及大小写问题,这个数据量可以估算为15亿左右(需要多预估一些空间)。
挑选数据结构: String。 每一个设备都是一个key, 请求方只需要看这个key在不在即可
实现:
key: key前缀+md5(device)。
例: app:old_users:202cb962ac59075b964b07152d234b70
值: 1
写命令: SET app:old_users:202cb962ac59075b964b07152d234b70 1
读命令: EXISTS app:old_users:202cb962ac59075b964b07152d234b70
空间预估: size = key.length * 16亿 = 46B * 16 * 10^8 = 68GB
这种方案成本较高,且会随着用户增加线性提升,直接pass。需要找到一种查找快,且占用空间小的方案。
抽象:
判断一个设备是否在旧用户池,如果把用户池看成集合,把设备看做元素,那么就可以回到开场的问题——如何判断一个大集合中是否含有某个元素? 布隆过滤器似乎是最佳的解决方案。
使用布隆过滤器的好处:
省空间: 不需要存储完整的元素,只需要对元素进行hash然后将bit向量表中的某个位设为1即可(先不考虑碰撞问题)
查找快: 只需要对查找的元素进行hash然后看bit向量表中对应的位是否为1。
缺点:
1.因为碰撞,会有一定的误报率( 不在集合的一定不在, 在的不一定在 )。这个可以通过使用多个hash函数减少误报,但无法完全消除。
2.不支持删除操作(还是因为碰撞,会出现误删的问题)。
有关布隆过滤器的详解可以参考
Bloom Filters by Examplellimllib.github.io/bloomfilter-tutorial/
结合REDIS:
redis原生并不自带布隆过滤器, 需要专门下载并自行编译和加载,使用方法见
RedisBloom Documentationoss.redislabs.com/redisbloom/
这里用到布隆过滤器两个API
1.读方: BF.EXISTS KEY element
2.写方: BF.ADD KEY element
两个API即可完成布隆过滤器元素的增加和查找操作(注: 这里的KEY就是布隆过滤器)。
拆KEY:
阿里云的redis单机只能经受10WQPS,再往上就不行了;所以需要选择集群版redis, 用集群来承担读方的查询压力。那么需要将key打散并分摊到不同的节点上。
拆key的规则: 取md5(device)头四位拼接,这样头四位相同的设备会落到相同的key里(布隆过滤器)
例: deviceArray = [
"202cb962ac59075b964b07152d234b70",
"202cb35dac59075b964b07152d234b95",
"202cb35dac09875b964b07152d234b88",
....
]
对应的写命令:
BF.ADD app:old_users:202c 202cb962ac59075b964b07152d234b70
BF.ADD app:old_users:202c 202cb35dac59075b964b07152d234b95
BF.ADD app:old_users:202c 202cb35dac09875b964b07152d234b88
....
空间预估: key的数量最多为 16^4(十六进制) 。
size = key.length 16^4 = 18B * 65536 = 1.125MB
这样key所占的空间可以忽略。
上线效果:
线上接口QPS: 峰值26W, 谷底4W,平响9ms。
redis使用情况:key数: 13w+(因为一些原因要增加其它的key), 占用内存:3.19GB(布隆过滤器本身以及bit向量都需要占用空间)。
总结:
项目做完后,有一些值得思考的地方,整理一下
1.提前预估(要为项目的以后做打算)
QPS预估: 最开始是按最小峰值预估的(10W左右),没有考虑业务发展的情况(后期接近30W)。如果不是领导提醒,自己就傻乎乎的按10W的量去做了,那redis的选型就会有问题(主从版redis只能支持10w,后来改为集群版),以后要估高一些,若资源使用率不高,再降配也不晚。
2.布隆过滤器使用(技术方案要尽可能通用)
开发时使用redis布隆过滤器的时候,用的是本地加载官方组件并启动redis,并没有考虑到阿里云是否支持;结果默认是不支持的... 好在后来跟对方协商,对方可以提供定制化版的redis,可以支持布隆过滤器的API。 这个事后还是有点后怕的, 如果它不支持呢? 如果要迁移到别的云呢?这个其实是留下了隐患的。 技术方案要有备选(用bitmap+偏移量实现, 但是hash算法要找轮子或自己实现)
3.抽象能力
这个非常重要,最终的技术方案并不是自己想到的,而是经过领导指点的。在特定的业务场景下,用合适的数据结构解决业务上的问题,为什么别人能抽象到具体的方案,而自己没想到? 归根结底还是对数据结构和算法理解的不够透彻, 这块是需要深耕的地方。