余晖落尽暮晚霞,黄昏迟暮远山寻
本站
当前位置:网站首页 > 编程知识 > 正文

MongoDB在QQ小世界Feed云系统中的应用及业务架构优化实践

xiyangw 2023-05-13 15:47 12 浏览 0 评论

1

业务背景

MongoDB在QQ小世界Feed云系统中的应用及业务架构优化实践

QQ 小世界最主要的四个 Feed 场景有:基于推荐流的广场页、个人主页,被动消息列表以及基于关注流的关注页。

最新 Feed 云架构由腾讯老 Feeds 云重构而来,老 Feeds 云存在如下问题:

  • 性能问题

老系统读写性能差,通过调研测试确认 MongoDB 读写性能好,同时支持更多查询功能。老系统无法像 MongoDB 一样支持字段过滤( Feed 权限过滤等),字段排序(个人主页赞排序等),事务等。

  • 数据一致性问题

老系统采用了 ckv+tssd 为 tlist 做一层缓存,系统依赖多款存储服务,容易形成数据不一致的问题。

  • 同步组件维护性问题

老系统采用同步中心组件作为服务间的连接桥梁,同步中心组件缺失运维维护,因此采用kafka作为中间件作为异步处理。

  • 存储组件维护成本高

老系统 Feeds 底层 tlist 、 tssd 扩容、监控信息等服务能力相对不足。

  • 服务冗余问题

老系统设计不合理,评论、回复、赞、转等互动服务冗杂在 Feeds 服务中,缺乏功能拆分,存在服务过滤逻辑冗杂,协议设计不规范等问题。


MongoDB 的优势

除了读写性能,通过调研及测试确认 MongoDB 拥有高性能、低时延、分布式、高压缩比、天然高可用、多种读写分离访问策略、快速 DDL 操作等优势,可以方便 QQ 系统业务快速迭代开发。

新的 Feed 云架构,也就是 UFO(UGC Feed all in One)系统,通过一些列的业务侧架构优化,存储服务迁移 MongoDB 后,最终获得了极大收益,主要收益如下:

  • 维护成本降低
  • 业务性能提升
  • 用户体验更好
  • 存储成本更少
  • 业务迭代开发效率提升
  • Feed 命中率显著提升,几乎100%


2

小世界 Feed 云系统面临的问题

通过 Feed 云系统改造,研发全新的 UFO 系统替换掉之前老的 Feed 云系统,实现了小世界的性能提升、三地多活容灾;同时针对小世界特性,对新 Feed 云系统做了削峰策略优化,极大的提升了用户体验。


2.1. 老 Feed 系统主要问题


改造优化前面临的问题主要有三个方面:

  • 写性能差

QQ 小世界为开放关系链的社交,时有出现热 Key 写入性能不足的问题。比如被动落地慢,Feed 发表、写评论吞吐量低等。

  • 机房不稳定

之前小世界所有服务都是单地域部署,机房出现问题就会引起整个服务不可用,单点问题比较突出。

  • 业务增长快,系统负载高

小世界业务目前 DAU 涨的很快,有时候做会出现新用户蜂拥进入小世界的情况,对后台的负载造成压力。

2.2. 新场景下 Feed 云问题


Feed 云是从 QQ 空间系统里抽出来的一套通用 Feed 系统,支持 Feed 发表,评论,回复,点赞等基础的 UGC 操作。同时支持关系链、时间序拉取 Feed ,按 ID 拉取 Feed 等,小世界就是基于这套 Feed 云系统搭起来的。

但在小世界场景下,Feed 云还是有很多问题。我们分析 Feed 云主要存在三个问题。首先是之前提到的慢的问题,主要体现在热 Key 写入性能差,SSP 同步框架性能差。其次一个问题是维护成本高,因为他采用了多套存储,同时代码比较老旧,很难融入新的中台。另外还有使用不方便问题,主要体现在一个是 Feed 异步落地,也就是我发表一个 Feed,跟上层返回已经发表成功,但实际上还可能没有在 Feed 系统最终落地。在一个是大 Key 有时候写不进去,需要手动处理。


3

数据库存储选型

下面就是对存储进行选型,首先我们要细化对存储的要求,按照我们的目标 DAU,候选存储需要满足以下要求:

  • 高并发读写
  • 方便快捷的 DDL 操作
  • 分布式、支持实时快捷扩缩容
  • 读写分离支持
  • 海量表数据,新增字段业务无感知

目前腾讯内部大致符合我们需求的存储主要是 MongoDB 和 Redis,因此那我就对两者做了对比,下表里面列了一些详细的情况。4C8G低规格 MongoDB 实例性能数据对比结果如下:

包括大 Key 的支持,高并发读的性能,单热 Key 写入性能,局部读能力等等。发现在大 Key 支持方面,Tendis 不能满足我们业务需求,,主要是大 Value 和 Redis 的 Key 是不降冷的,永久占用内存。

所以最终我们选择了 MongoDB 作为最终存储。


4

MongoDB 业务用法及内核性能优化

4.1. MongoDB 表设计

4.1.1. Feed 表及索引设计

  • InnerFeed 表

InnerFeed 为整个主动被动Feed结构,主要设计Feed核心信息,设计 Feed 主人、唯一ID、Feed 权限:

 message InnerFeed  
 {  
     string        feedID = 1; //id,存储层使用,唯一标识一条feed  
     string        feedOwner = 2; //Feeds主人  
     trpc.feedcloud.ufobase.SingleFeed    feedData = 3;  //feed详情数据  
     uint32        feedMask = 4;      //信息中心内部使用的
     //feed 权限flag标志,参考 ENUM_UGCFLAG  
     trpc.feedcloud.ufougcright.ENUM_UGCFLAG   feedRightFlag = 5;      
 };  
  • SingleFeed 表

SingleFeed 为 Feed 基本信息,Feed 类型,主动、评论被动、回复被动、Feed 生成时间以及 Feed 详情:

 message SingleFeed {  
     int32         feedType = 4;   //Feed类型,主动、评论被动、回复被动。。。
     uint32        feedTime = 5;  
     FeedsSummary       summary = 7;   //FeedsSummary  
     map<string, string> ext = 14;      //拓展信息  
     ...  
 };
  • FeedsSummary 表

FeedsSummary 为 Feed 详情,其中 UgcData 为原贴主贴数据,UgcData.content 负责存储业务自定义的二进制数据,OpratorInfo 为 Feed 操作详情,携带对应操作的操作人、时间、修改数据等信息:

 //FeedsSummary  
 message FeedsSummary  
 {  
     UgcData            ugcData = 1;         //内容详情  
     OpratorInfo        opInfo  = 2;         //操作信息  
 };  
   
 // UgcData 详情  
 message UgcData  
 {  
     string              userID = 1 [(validate.rules).string.tsecstr = true]
     uint32              cTime = 2;  
     bytes               content = 5;   //透传数据,二进制buffer  
 ...  
 };  
   
 message OpratorInfo  
 {  
     uint32        action = 1;  //操作类型,如评论、回复等,见FC_API_ACTION  
      //操作人uin  
     string        userID = 2 [(validate.rules).string.tsecstr = true];      
     uint32                  cTime = 3;           //操作时间
      //如果是评论或者回复,当前评论或者回复详情放这里,其它回复内容是全部。    
     T2Body                  t2body = 4;        
     uint32                  modifyFlag = 11;      //ENUM_FEEDS_MODIFY_DEFINE
      ...  
 };  
  • Feed索引设计

Feed 主要涉及个人主页 Feed 拉取、关注页个人 Feed 聚合:

"key" : {"feedOwner" : -1,"feedData.feedKey" : -1}

根据 FeedID 拉取指定的 Feed 详情:

"key" : {"feedOwner" : -1,"feedData.feedTime" : -1}


4.1.2. 评论回复表及所有设计

  • InnerT2Body 表

InnerT2Body 为整个评论结构,回复作为内嵌数组内嵌评论中,结构如下:

 message InnerT2Body  
 {  
     string   feedID = 1;  
     //如果是评论或者回复,当前评论或者回复详情放这里,其它回复内容是全部。
     trpc.feedcloud.ufobase.T2Body t2body = 2;        
 };  
  • T2Body 表

T2Body 为评论信息,涉及评论 ID、时间、内容等基本信息:

 message T2Body                   //comment(评论)  
 {  
     string              userID = 1;      //评论uin  
     uint32              cTime = 2;       //评论时间  
     string              ID = 3;          //ugc中的seq  
     //评论内容,二进制结构,可包含文字、图片等,业务自定义  
     string              content = 5;    
     uint32              respNum = 6;     //回复数  
     repeated T3Body     vt3Body = 7;        //回复列表  
     ...  
 };  
  • T3Body 表

T3Body 为回复信息,涉及回复 ID、时间、内容、被回复人的 ID 等基本信息:

 message T3Body                     //reply(回复)  
 {  
     string              userID = 1;              //回复人  
     uint32              cTime = 2;               //回复时间  
     int32               modifyFlag = 3;      //见COMM_REPLY_MODIFYFLAG  
     string              ID = 4;                  //ugc中的seq  
     string              targetUID = 5;           //被回复人  
     //回复内容,二进制结构,可包含文字、图片等,业务自定义 
     string              content = 6;            
 };
  • 评论索引设计

(1)评论主要涉及评论时间序排序:"key" : {"feedID" : -1,"t2body.cTime" : -1}

(2)根据评论 ID 拉取指定的评论详情:"key" : {"feedID" : -1,"t2body.ID" : -1}


4.2. 片建选择及分片方式

以 Feed 表为例,QQ 小世界主要查询都带有 feedowner ,并且该字段唯一,因此选择码 ID 作为片建,这样可以最大化提升查询性能,索引查询都可以通过同一个分片获取数据。此外,为了避免分片间数据不均衡引起的 moveChunk 操作,因此选择 hashed 分片方式,同时提前进行预分片,MongoDB 默认支持 hashed 预分片,预分片方式如下:

 use feed  
 sh.enableSharding("feed")  
 //n为实际分片数  
 sh.shardCollection("feed.feed", {"feedowner": "hashed"}, false,{numInitialChunks:8192*n})  


4.3. 低峰期滑动窗口设置


当分片间 chunks 数据不均衡的情况下,会触发自动 balance 均衡,对于低规格实例,balance 过程存在如下问题:

  • CPU 消耗过高,迁移过程甚至消耗90%左右 CPU
  • 业务访问抖动,耗时增加
  • 慢日志增加
  • 异常告警增多

以上问题都是由于 balance 过程进行 moveChunk 数据搬迁过程引起,为了快速实现数据从一个分片迁移到另一个分片,MongoDB 内部会不停的把数据从一个分片挪动到另一个分片,这时候就会消耗大量 CPU,从而引起业务抖动。

MongoDB 内核也考虑到了 balance 过程对业务有一定影响,因此默认支持了 balance 窗口设置,这样就可以把 balance 过程和业务高峰期进行错峰,这样来最大化规避数据迁移引起的业务抖动。例如设置凌晨0-6点低峰期进行balance窗口设置,对应命令如下:

 use config  
 db.settings.update({"_id":"balancer"},{"$set":{"activeWindow":{"start":"00:00","stop":"06:00"}}},true)


4.4. MongoDB 内核优化

4.4.1内核认证随机数生成优化

MongoDB 在认证过程中会读取 /dev/urandom 用来生成随机字符串来返回给客户端,目的是为了保证每次认证都有个不同的 Auth 变量,以防止被重放攻击。当同时有大量连接进来时,会导致多个线程同时读取该文件,而出于安全性考虑,避免多并发读返回相同的字符串(虽然概率极小),在该文件上加一把 spinlock 锁(很早期的时候并没有这把锁,所以也没有性能问题),导致 CPU 大部分消耗在 spinlock ,这导致在多并发情况下随机数的读取性能较差,而设计者的初衷也不是为了速度。

腾讯 MongoDB 内核随机数优化方法:新版本内核已做相关优化:mongos 启动的时候读 /dev/urandom 获取随机字符串作为种子,传给伪随机数算法,后续的随机字符串由算法实现,不去内核态获取。

优化前后测试对比验证方法:通过 Python 脚本模拟不断建链断链场景,1000个子进程并发写入,连接池参数设置 socketTimeoutMS=100,maxPoolSize=100 ,其中 socketTimeoutMS 超时时间设置较短,模拟超时后不断重试直到成功写入数据的场景(最多100次)。测试主要代码如下:

 def insert(num,retry):  
     print("insert:",num)  
     if retry <= 0:  
         print("unable to write to database")  
         return  
     db_client = pymongo.MongoClient(MONGO_URI,maxPoolSize=100,socketTimeoutMS=100)  
     db = db_client['test']  
     posts = db['tb3']  
     try:  
         saveData = []  
         for i in range(0, num):  
             saveData.append({  
             'task_id':i,  
             })  
             posts.insert({'task_id':i})  
     except Exception as e:  
         retry -= 1  
         insert(num,retry)  
         print("Exception:",e)  
   
 def main(process_num,num,retry):  
     pool = multiprocessing.Pool(processes=process_num)  
     for i in xrange(num):  
         pool.apply_async(insert, (100,retry, ))  
     pool.close()  
     pool.join()  
     print "Sub-processes done."  
   
 if __name__ == "__main__":  
     main(1000,1000,100)


优化结果如下:

优化前:CPU 峰值消耗60核左右,重试次数 1710,而且整体测试耗时要更长,差不多增加2 倍。优化后:CPU 峰值: 7核 左右,重试次数 1272,整体性能更好。

mongos 连接池优化:

通过调整 MinSize 和 MaxSize ,将连接数固定,避免非必要的连接过期断开重建,防止请求波动期间造成大量连接的新建和断开,能够很好的缓解毛刺。优化方法如下:

 ShardingTaskExecutorPoolMaxSize: 70  
 ShardingTaskExecutorPoolMinSize: 35

如下图所示,17:30调整的,慢查询少了 2 个数量级:


4.5. MongoDB 集群监控信息统计

如下图所示,整个 QQ 小世界数据库存储迁移 MongoDB 后,平均响应时延控制在5ms以内,整体性能良好。


5

关于作者

腾讯 PCG 功能开发一组团队, 腾讯 MongoDB 团队。

相关推荐

辞旧迎新,新手使用Containerd时的几点须知

相信大家在2020年岁末都被Kubernetes即将抛弃Docker的消息刷屏了。事实上作为接替Docker运行时的Containerd在早在Kubernetes1.7时就能直接与Kubelet集成使...

分布式日志系统ELK+skywalking分布式链路完整搭建流程

开头在分布式系统中,日志跟踪是一件很令程序员头疼的问题,在遇到生产问题时,如果是多节点需要打开多节点服务器去跟踪问题,如果下游也是多节点且调用多个服务,那就更麻烦,再者,如果没有分布式链路,在生产日志...

Linux用户和用户组管理

1、用户账户概述-AAA介绍AAA指的是Authentication、Authorization、Accounting,即认证、授权和审计。?认证:验证用户是否可以获得权限,是3A的第一步,即验证身份...

linux查看最后N条日志

其实很简单,只需要用到tail这个命令tail-100catalina.out输入以上命令,就能列出catalina.out的最后100行。...

解决linux系统日志时间错误的问题

今天发现一台虚拟机下的系统日志:/var/log/messages,文件时间戳不对,跟正常时间差了12个小时。按网上说的执行了servicersyslogrestart重启syslog服务,还是不...

全程软件测试(六十二):软件测试工作如何运用Linux—读书笔记

从事过软件测试的小伙们就会明白会使用Linux是多么重要的一件事,工作时需要用到,面试时会被问到,简历中需要写到。对于软件测试人员来说,不需要你多么熟练使用Linux所有命令,也不需要你对Linux...

Linux运维之为Nginx添加错误日志(error_log)配置

Nginx错误日志信息介绍配置记录Nginx的错误信息是调试Nginx服务的重要手段,属于核心功能模块(nginx_core_module)的参数,该参数名字为error_log,可以放在不同的虚机主...

Linux使用swatchdog实时监控日志文件的变化

1.前言本教程主要讲解在Linux系统中如何使用swatchdog实时监控日志文件的变化。swatchdog(SimpleWATCHDOG)是一个简单的Perl脚本,用于监视类Unix系统(比如...

syslog服务详解

背景:需求来自于一个客户想将服务器的日志转发到自己的日志服务器上,所以希望我们能提供这个转发的功能,同时还要满足syslog协议。1什么是syslog服务1.1syslog标准协议如下图这里的fa...

linux日志文件的管理、备份及日志服务器的搭建

日志文件存放目录:/var/log[root@xinglog]#cd/var/log[root@xinglog]#lsmessages:系统日志secure:登录日志———————————...

运维之日志管理简介

日志简介在运维过程中,日志是必不可少的东西,通过日志可以快速发现问题所在。日志分类日志分类,对不同的日志进行不同维度的分析。操作系统日志操作系统是基础,应用都是在其之上;操作系统日志的分析,可以反馈出...

Apache Log4j 爆核弹级漏洞,Spring Boot 默认日志框架就能完美躲过

这两天沸沸扬扬的Log4j2漏洞门事件炒得热火朝天:突发!ApacheLog4j2报核弹级漏洞。。赶紧修复!!|Java技术栈|Java|SpringBoot|Spring...

Linux服务器存在大量log日志,如何快速定位错误?

来源:blog.csdn.net/nan1996jiang/articlep/details/109550303针对大量log日志快速定位错误地方tail/head简单命令使用:附加针对大量log日志...

Linux中查看日志文件的正确姿势,求你别tail走天下了!

作为一个后端开发工程师,在Linux中查看查看文件内容是基本操作了。尤其是通常要分析日志文件排查问题,那么我们应该如何正确打开日志文件呢?对于我这种小菜鸡来说,第一反应就是cat,tail,vi(或...

分享几款常用的付费日志系统,献给迷茫的你!

概述在前一篇文章中,我们分享了几款免费的日志服务器。他们各有各的特点,但是大家有不同的需求,有时免费的服务器不能满足大家的需要,下面推荐几款付费的日志服务器。1.Nagios日志服务器Nagio...

取消回复欢迎 发表评论: