亿级⼯具类APP头条数据聚合优化实践
亿级⼯具类APP头条数据聚合优化实践
业务介绍
中华万年历的头条数据是根据推荐算法聚合⽽成的数据,包括ALS算法数据、⽤户画像数据、时效数据、⾮时效数据、定投数据、惊喜数据、频道数据、热榜数据、⽤户相关阅读推荐数据等。启动⽅式分为冷启动和⽤户画像启动。
冷启动:⽆⽤户画像或⽤户画像得分<8分。
⽤户画像:根据⽤户浏览头条数据给⽤户打的⼀系列标签,标签采⽤Long型的数字进⾏标记,譬如娱乐285L、旅游1127L。
时效数据:和时间相关的数据,会随着时间的推移⾃动消失,譬如新闻、娱乐。
⾮时效数据:和时间不相关的数据,会长期存在,譬如养⽣。
定投数据:通过管理后台⼿动投放的数据,⼀般为固定位置数据,如⼴告、帖⼦。
惊喜数据:排除画像之外的数据。
频道数据:多个标签下的数据组合⽽成的数据。频道是标签的⽗类,⼀个频道对应包涵多个标签,标签是⽤户画像组成的基本单位。
热榜数据:根据⽤户点击实时上传的⽇志计算得分较⾼的数据。
⽤户相关阅读推荐数据:根据⽤户点击实时上传的⽇志计算相关联的数据。
数据存储
头条的数据都是从合作⽅抓取的,通过定时调⽤第三⽅API进⾏抓取。抓取的数据经过频道标签分类后存储到MySQL数据库。头条服务会每隔⼀段时间把数据库⾥⾯的数据reload到Redis中,然后再从Redis中reload到本地内存中。数据的聚合就是把内存中的数据按照算法进⾏组装。
为什么要经过两次的数据reload,因为我们的接⼝服务是⽀持⽔平扩展的,如果单⼀的从数据库reload的话,数据库的连接压⼒会随着服务节点的增加⽽增⼤,数据加载不⼀致的机率会也会增加。使⽤redis进⾏中间过渡可以把数据库的压⼒分担到Redis,毕竟Redis的并发能⼒⾼于MySQL,访问速度也⾼于MySQL。
数据reload到本地内存会经过筛选分类,即每种数据在内存中都会有对应的⼀个数据池,这些数据池是通过reload循环迭代分进去的。
数据池分为:
新池⼦:存放新抓取的⾮实效数据,数据结构为
Set<Long>
⽼池⼦:存放有点击率、pv的数据,数据结构为
List<Long>
视频池⼦:存放所有的视频数据,数据池结构为
List<WnlLifeCardItemBean>
⾮实效标签池⼦:存放标签对应的⾮实效条⽬id,数据结构为
Multimap<Long, Long>
实效标签池⼦:存放标签对应的实效条⽬id,数据结构为
Multimap<Long, Long>
黄历池池⼦:存放黄历标签下的数据,数据结构为
redis八种数据结构List<WnlLifeCardItemBean>
星座池⼦:存放星座标签下的数据,数据结构为
List<WnlLifeCardItemBean>
未来天提醒池⼦:存放投放的电影、体育等节⽬提醒数据,数据结构为
List<WnlLifeCardItemBean>
TotalMap:加载所有数据的id->bean集合。
除了本地内存的数据池外还有⼤数据平台推荐的其它数据,该数据存放在Redis中,数据结构为:
Set<Long>
备注: WnlLifeCardItemBean为返回的头条对象bean,Long类型的是bean对象id或标签id。
数据存储架构图:
早期数据更新⽅式
数据更新主要存在两个地⽅的更新:Redis和Local。对新抓取的数据在API服务接⼝中采⽤spring quartz每隔⼀段时间从Redis中读取⼀次然后同步到Local。Redis中的数据则是通过⼀个单独的bg模块,同样采⽤spring quartz定时任务每隔⼀段时间从MySQL中读取,然后同步到Redis中。除了新抓取的数据外,在每个API服务中还有每秒更新pv、click的定时任务。
由于我们抓取的数据分为⾃动上架和⼿动上架,⼿动上架需要运营⼈员审核通过后才能在客户端展⽰,对⾃动上架不符合要求的数据也需要做下架处理,按照上⾯的更新⽅式显然不能⽴即⽣效。
值得思考的问题:
API节点较多怎么保证每个本地内存中的数据是否⼀致
能否有针对性的更新,不⽤每次都reload所有数据
能否分离API中的定时任务到bg模块
能否及时响应数据变化⾃动更新
遇到的问题
数据更新丢失。bg在更新Redis数据时是先添加原数据然后再建⽴索引,原数据采⽤String数据结构,bean的ID作为key,序列化的对象作为value。索引采⽤Set数据结构,value存放的是bean的id。每次更新数据时会删除索引然后重新创建。如果头条接⼝服务正在reload数据的时候发⽣bg更新任务则会导致reload 到Local中的数据丢失。
reload时间过长。头条服务在启动的时候不会⽴即初始化数据,⽽是通过⽤户触发,异步的完成加载。为了避免⼤量⽤户并发reload操作采⽤Cache对操作进⾏缓存,设置缓存时间的⼤⼩。值得注意的是如果缓存设置的时间⼩于加载的时间则同样会造成并发的reload。
占⽤内存较⼤,耗费CPU。随着抓取的数据越来越多,⾮实效的数据也越积越多,最终导致内存中的数据越来越⼤,从redis中读取数据进⾏反序列化需要耗费⼤量的CPU。虽然限制读取数据的条数可以避免这个问题,但是数据是糅合在⼀起的,被限制的⼀部分数据可能是对⽤户最有价值的数据。
业务数据分离
由于数据种类较多,数据量较⼤,每次变更⼀条数据重新reload全部数据会导致内存和CPU迅速上升,尤其是全局的临时变量替换、json的反序列化。这样做不仅重复加载,⽽且还会因为其它数据加载的失败⽽影响到所需要的数据,没有做到有针对性的更新。尤其在定投⼴告数据时,⼴告需要很长时间才能出现,或是因为没有加载进来不出现,这样就直接影响到了收⼊,肯定是不允许的。为了减
少更新的数据量,把数据按照业务进⾏分离,每次更新⼀条数据只reload对应的数据种类。
更细粒度的数据更新可以针对到某⼀条。由于本系统数据已经按业务进⾏了拆分,数据量在合理的范围内,做整体替换实现更加简单。
业务数据分离是为了保证最⼩的数据变动,我们按照业务需求把⼀条SQL拆分了多条SQL,每条SQL完成对应的数据加载,同样内存中的数据也做了细化。内存中的数据和Redis中的数据同步是通过Redis的订阅发布实现的。
整体结构如图:
推荐数据迁移到Redis
虽然数据分离解决了⼀部分reload的问题,但推荐数据是个⼤的数据块,需要把所有⾮实效的数据都加载进来,内存压⼒很⼤。当初把数据缓存在本地是为了提⾼客户端的访问效率,但当数据增加到⼀定程度时,每次进⾏数据替换都会产⽣占⽤内存较⼤的临时变量,⽼的变量会被Java虚拟机⾃动回收,所以在数据reload的过程中gc会变得更加频繁。分析解决办法:1. 增加机器内存⽆疑需要增加成本;2. 使⽤增量更新做简单的数据版本控制,针对变化的数据直接在内存中进⾏修改,不做整体的reload替换,但这样做⼜引出了新的问题,怎样保证每个节点的数据是否⼀致,更新失败怎么处理,怎么做到数据有效的监控;3. 把数据迁移到Redis。
如果按照⽅法2去实现的话等于是⼜要造⼀个Redis,所以最终采⽤了把数据迁移到Redis的⽅式。数据存储主要分为基础数据和索引数据,索引数据是有序的id Set集合,索引按照推荐策略进⾏了分类,如新闻、频道、曝光、ctr等,通过调⽤⼤数据平台来更新索引的分类和Set集合元素的score值,⽤以保证推荐数据的准确性。
为了减少Redis的连接次数,每次推荐都会计算出⾜够多的数据存放到⽤户的阅读缓存中,如果⽤户阅读缓存中的数据不够了会重新触发聚合计算。
数据抓取
头条的数据来源是API接⼝抓取(经过授权),之前的⽅式都是针对每⼀种数据源在bg模块中进⾏单独开发,然后在xml中配置quartz定时运⾏任务,没有做到数据监控和可视化管理。如果要停⽌或修改某⼀个数数据源的抓取任务必须停⽌整个bg服务然后再修改代码或quartz配置⽂件。
修改后的数据抓取框架:
独⽴出⼀个专门的数据抓项⽬ulike,通过后台管理,实现任务的可配置化。
Mgr 后台管理,管理数据源配置和任务配置,查看抓取的数据以及监控信息。
MySQL 存储Mgr的管理信息
Scheduler 负责任务调度
Redis 调度通过Redis发布订阅传给Processor命令,对任务进⾏操作。
Processor 负责处理抓取命令,业务处理。
Engine 对源数据进⾏解析获取系统所需要的数据。
推荐数据查询优化
1. 多个redis命令操作改为pipeline管道模式操作
2. ⼀次计算多页推荐数据进⾏缓存
3. 迭代器模式访问标签索引数据,控制游标的位置,在⽤户连续访问超过⼀定的时间后进⾏回位,保证查询最新的推荐数据。
4. 多线程异步计算
查询设计图:
后记
头条信息对⽤户的价值关键在于推荐的算法,我们会根据⼤数据分析持续调整推荐算法、优化聚合算法,使⽤户的体验达到最佳。
版权申明:内容来源⽹络,版权归原创者所有。除⾮⽆法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会⽴即删除并表⽰歉意。谢谢。
-END-