Feeds 系统简析 ---- 手Q游戏中心游戏圈

2023-03-14 14:24:18 浏览数 (2)

项目背景介绍

游戏圈,是手Q游戏中心在社交化场景的一个探索和实践,将用户在游戏内的战绩、高光等事件作为动态展示在好友的 feeds 流列表中,产品形态上类似微信朋友圈、QQ 空间、推特等。

整体交互上就是用户产生游戏高光(发动态),好友进入动态 feeds 列表按照时间线查看。

先分享下我们业务的一些数据:feeds 发表峰值(写)大约是在 x k/s 左右,日发表量约在 xkw,活跃用户大约 x 亿,单条 feeds 约 900B,每个用户存最近 x 条,大约需要 5T 存储;好友 feeds 流时间线拉取(读)峰值大约在 xk/s。

技术方案

社交 feeds 型的产品,绕不开的挑战是:

  1. 读写方案选型。
  2. 海量 feeds 存储选型。

读写方案选型

业界几款明星产品的选型:

QQ 空间

读扩散

微信朋友圈

写扩散

新浪微博

读写扩散

twitter

写扩散

facebook

读扩散

读扩散:只有发件箱。优点是写简单、省存储、feeds 策略更灵活(例如支持多种关系链);缺点是读逻辑实现复杂且存储读扩散压力大。

写扩散:有发件箱和收件箱。优点是读简单,缺点是费存储、写复杂(合并写)、大V关系链下扩散慢,延迟高。

读写扩散:本质还是写扩散。大V用读扩散加速拉取。

架构选择上没有银弹,具体选型还是要结合业务的场景选择。以我们业务为例,选择读扩散,主要考虑:

  1. 产品和游戏侧强关联,未来有接入多种关系链的可能(游戏好友等)。
  2. 公司大背景,降本增效,写扩散,沉默用户和低活用户太费存储和计算资源了。
  3. 关系链权限风险。很多数据走游戏流水接入,无登录态或互联登录态,权限层级上无法获取手Q侧关系链数据(低权限换高权限,违反规则)。

存储选型

存储诉求:1是要支持海量存储,10T级别,最好是分布式的,可动态扩容;2是要支持高并发的访问,业务侧估算,xk/s 的 feeds 流拉取,平均每个活跃用户有 xxx 好友,对存储层压力在 64w/s 左右;3是要上云。

结合如上诉求,可选的就这2个:Mongo 分片集群、Tendis/keewidb。

Mongo 的特点是:

  1. 分布式分表,扩容、迁移业务无感。
  2. 性能较好,云上6核16G规格,大约单片支持 5w/s(参见腾讯云产品文档)。
  3. 数据 schema 可动态扩展,业务友好。
  4. bson 存储,略费存储。10T存储每月成本 xx 万左右。

Tendis/Keewidb 简而言之就是支持大容量的降冷版的 redis,特点如下:

  1. 分布式,扩容、迁移业务无感。
  2. 数据读写性能高,热门数据单分片大约支持 10w/s 左右的读写。
  3. 不支持部分字段读取(例如某个用户近10条 feeds 等),kv 长度最大不能超过 16MB。
  4. 数据 key 无法降冷,占用内存。也就是说如果采用 feedid 作为 key,key 数量会极为庞大,内存中基本全是 key,热门内容的缓存效果会大打折扣。
  5. 成本较高,10T存储每月 xx 万左右。

综上来说,我们选择了 mongo 集群作为存储方案,单用户单条 feeds 为一条记录。

整体架构

实现细节

TimeFeeds 流

用户进入动态页,按照时间顺序展示好友的 feeds 列表。采用读扩散的方案,先拉取好友列表,再拉取好友的 feeds 记录统一排序返回页面,每页展示10条动态。

参考QQ空间的实现方案,为了减轻读扩散对存储层的压力,游戏圈主要采用如下两种策略:

1. Redis 时间索引

Redis 记录所有用户最近发表 feeds 的时间戳。首页拉取时,以当前时间为基准,根据 redis 时间索引只拉取最近的10个好友的10条 feeds;翻页时,以上一页的最后一条 feeds 时间为基准,找到基准时间最近的10个好友,加上前一页展示的所有好友,拉取这些好友的基准时间前发表的10条 feeds,排序后返回。

缺陷很明显:越往后翻页,查好友数量越多,拉取速度越慢。但是按照统计数据来说,超过 90% 的用户都是只看第一页,因此问题不是很严重。

2. 缓存

那针对这 10% 的活跃用户,我们有办法进行优化吗?作为开发人员,我们要有精益求精,极致优化的追求。

方案就是叠加缓存,我们知道时间是单向流逝的。因此,用户 a 在时间 t0~t1 内看到的 feeds 应该说是固定的,不管用户是在 t2、t3、t4 进来查看,只要查看的时间区间是 t0~t1 那就是同一份数据。基于这个原因,我们可以额外做缓存优化。

具体来说,就是用户每次进入页面,我们将当前计算好的 feeds 列表缓存在 mongo 中(只缓存最近10页),下次用户进入页面时,计算增量的 feeds 列表,然后合并到 feeds 列表缓存中去。这样,用户特定时间区间内的 feeds 列表只会计算1次。

当然,这种策略更复杂,也会有额外的存储消耗,我们可以做进一步优化。

  1. 针对缓存列表增加过期时间,例如5天,保证缓存不会单调增长。
  2. 针对用户进行分级,例如产品高活用户,走这种策略缓存计算和加载;低活用户,一般只看首页内容,则不进入缓存机制。

Feeds 超长清理

手Q游戏中心,毕竟不是一个 feeds 流的产品形态,只是希望展示用户最近的好友动态。因此产品策略上,对 feeds 的保留时间做了限制,每个用户只保留最近 N 条。

我们采用的方案是:在业务谷期,对 feeds 存储离线扫描;同时为了减少离线扫描的数量,用咆哮位图记录每天发表过 feeds 的用户;同时做了读写分离和合并查询,提升扫描速度。

性能优化

上线后,发现服务器 CPU 消耗较多,Perf 后发现大多数在 gc 上。

我们用 Sync.Pool 复用了大多数网络 IO 过程中的临时对象,优化后CPU 降低了 18%,gc 耗时也降低了很多。

线上 Mongo 高负载优化

现象

线上 Mongo 集群经常出现尖峰,峰值期间业务访问质量大受影响。

原因

定位后发现,毛刺是由于业务服务和 Mongos 实例链接断掉后,短时间内大批量新建链接引起的。上游调用下来超时时间800ms。db 高负载时,由于800ms内无法响应,driver 认为出错,直接关闭链接,重新新起链接去查,新链接在 800ms 内无法建立,导致driver 不停地关闭、重建新链接;导致雪崩的出现。

业务侧设置每台实例 maxPoolSize=minPoolSize=15。

代码语言:txt复制
1. 线上 Mongod 集群高负载,偶尔出现一些超时慢查询,超过 800ms。
2. 业务使用连接池访问,高负载时链接访问1s超时了,导致链接被释放。
3. 连接池监控发现链接不够了,新起链接去 mongos,同样 1s 超时释放超时连接。
4. 后面反复这个流程,导致1分钟内不停的创建新链接,达到13w/min,引起雪崩。

解决方案

  1. Mongo 集群设置 relance.window 时间窗口为 3:00~8:00,避免平时 rebalance 影响。
  2. Mongo driver 断掉上游的 ctx,新起新的 ctx 超时3s去访问 db。避免上游 ctx 结束导致建立中的链接不停失败。上游可以失败,但是链接要保持住不要重连。

效果

调整完之后业务侧已经不受影响,慢查询数量从 3.5k/min 降低到 300/min 即时偶尔有超时,也不会产生雪崩效应。

0 人点赞