找回密码
 会员注册
查看: 18|回复: 0

用户众测白名单控制原理及优化过程_UTF_8

[复制链接]

2万

主题

0

回帖

7万

积分

超级版主

积分
73159
发表于 2024-10-2 11:20:29 | 显示全部楼层 |阅读模式
本文主要讲述了当前用户众测服务中白名单控制相关的原理以及优化过程。同时借此分享一些在优化过程中使用的Golang小技巧以及遇到的一些问题,不一定具有普适性,或能给大家在日常搬砖中些许的灵感。0、用户众测是什么?简单来说,指的就是一种让外部用户参与非正式版本测试的行为。这在我们日常使用app中或多或少可能见过带有“参与内测”之类的弹窗字眼,这可能就是一种用户众测弹窗。那么本文涉及的白名单是个什么东西呢?其实就是我们的外部用户。既然是非正式版,那就是说并不是所有线上用户都可以参与,这里就涉及到一个白名单控制逻辑。白名单的内容,我们取用户手机的device_id在我们的用户众测服务中,通过device_id我们可以控制用户参与众测,控制的内容主要为:1.弹窗,通过众测平台的任务,可以对白名单下发类似下方的弹窗,用户可以选择下载测试版本进行体验,也可拒绝。2.H5页面:承接众测任务展示,奖励下发、积分获取等功能(如下所示),通常在app内“我的”tab处展示页面的入口。3.黄点提示:当有新任务时,透出黄点提示,用户点击后黄点消失。1、当前现状从上文可知描述,白名单说白了就是device_id集合,更直接的,device_id是一个int64的值,从而白名单不就是一个包含有若干in64数值的Set,很直观的一个想法就是存一个集合,自然而然, redis/abase等+内存缓存是非常显而易见的解决方案。从而,在客户端请求过来时,能够下发众测相关的配置,简单明了, 一个Get操作完事了。比如一个基于redis的方案可能如下:redis的HSET,通过key+value+filed的形式存储(SET也可以)能够快速的判断一个device_id是不是应该是展示sdk入口;更进一步,还可以将白名单缓存一份到内存中;从而,这个判断逻辑要么是一个纯内存操作,要么就是读一下redis,想想都觉得快,so easy!But too young and too naive!来看一下这样有什么问题。白名单的数量:当前头部app,如今日头条、抖音、火山小视频等,白名单用户数量已达到百万级别,并且呈不断增长的趋势;显然上述基于redis方案, 不管是使用HSET还是SET,都对应的是同一个key,这key势必是一个大key;热key问题,所谓热key : QPS特别高的一个key称为热key。从线上全量用户的角度来看,如抖音、头条这种亿级别的app来看,白名单数量只占极少数,上述方案来看,所有用户都需要访问这个唯一的key,显然,是一个热key。为了解决上述大key、热key问题,当前项目使用了一种基于分级key的解决方案。将上述的单个key拆分成多个key进行存储,原来key存储子key,以期解决大key、热key问题,实际事与愿违,白费力气,甚至不如不改(没有实测过)。一级的key存储二级key(key_1, key_2, ..., key_n)二级key_i使用redis set存储实际device_idkey_i限制value的数量上限(项目中使用4000)来看一下实际的存储过程。1.1 添加白名单用户去每个子key中“找空位”插入,超过子key数量限制时,新增一个子key; PS: 没有去重,造成存储空间的浪费,实际如果要去重的话,得遍历每个子key,而不是一味的找空位插入。1.2 查询用户挨个去每个子key下查询判断是不是存在;PS: 大部分用户都需要遍历所有子key,效率极其低下。1.3 删除用户挨个遍历每个子key,进行删除操作; PS: 同样地,效率极其低下。在众测场景下,QPS主要集中在查询和删除。上述的分级key显然很鸡肋。虽然解决了大key,将key拆分,但是却加重了热key,效率低下,时延高,同时由于未去重,造成服务内存占用高(实际来看,600万的名单,实际存了2个亿),占用机器实例多。一些截图如下,由于分级key导致:2、优化方案上述方案的主要问题在于:1. 数据重复;2. 查询效率低下;针对这两个问题,查询效率低的主要原因在于:对于一个device_id,无法直接定位到子key,所以只能挨个遍历。如果能够直接定位到子key,就可以解决效率的问题。因此有了如下改进的方案:还是采用分级key的策略;从横向的角度(而不是纵向)限制value的数量, 限定子key的数量;使用hash函数对device_id进行映射,直接定位到子key, 从而(如下),相同余数的用户在同一个子key中。同时解决了上述提到的两个问题;2.1 添加白名单用户直接通过hash函数取模的方式定位到子key;2.2 查询用户2.3 删除用户2.4 入口检测方案当前的检查方案由各业务服务端与众测服务端同步控制;存在的问题:名单之间传输的数据量大;双端服务内存占用大,造成服务内存告警;由于数据未做去重处理,导致数据量不断增长,接口时延高;优化方案:在业务服务端和众测服务端之间,不全量同步整个大白名单;名单仅存储在众测服务端中,提供接口(RPC)给业务服务端判断单个用户是否在白名单中;至此,整个入口检测流量整体迁移到众测服务端,但是事情还没了,经过分级key的改造,我们基本上将压力全部打到了redis上面。对于大部分请求来说,大部分应该都是不命中的,也就是说大部分请求访问redis都是不命中的,也就是说是无效访问,自然而然,Bloom Filter是一个很好的选择, 故优化后的访问方案如下:3、优化效果sdk页面接口时延由之前的秒级(高达10s)->ms级(平均低于200ms);平台接口时延由20s+ -> 600ms-,同时由于重构python ->go,机器实例240+减少至40;Bloom filter过滤效果很好, 能够过滤95%以上的数据请求;4、Golang使用小技巧4.1 Goroutine的使用在面对一些处理时间较长的接口而言,串行执行每个步骤的时间可能超出预期,这时候可以使用goroutine去做一些异步处理操作。在go中,这是非常容易做到。不需要等待响应结果的情况可以写出如下语句,异步执行操作;go func() {...}()但是在qps比较大的情况的, 无限制的开gorounte去执行可能导致goroutine数量飙高,影响服务性能。这时候我们可以使用线程池gopool("code.byted.org/gopkg/gopool"),控制work线程的数量,当然可能导致处理时延增长。其工作原理如下:pool:将task以单链表的形式组织起来, 并能够在满足条件的情况下新建goroutine(work)从链表头取task进行处理;task:包含需要执行的函数f 以及contextwork:本质上是pool新建的goroutine, 处理task中的f函数sync.pool的运用, 上述三种结构使用sync.pool管理, 减少GC时间如果需要等待处理接口,则可使用sync包中的WaitGroup或者使用channel进行等待执行结果如下:waiter := sync.WaitGroup{}for b := uint64(0); b
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-12 17:31 , Processed in 1.250811 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表