QQ资料存储平台UDC的设计揭秘

一、引言

承载了24亿有效QQ号码的资料存储平台对很多同学而言一直是个谜,虽然有不少相关的文章对QQ后台架构进行了分享,但一个是因为范围太广,很难覆盖细节,另一个是年久失修,QQ的资料平台做了很多重构,原来的相关分享都没有涉及。

这篇文章希望把我本人在QQ后台工作期间对资料平台进行的重构和各种优化,用尽可能简单易懂的方式展示给大家。

如果你不懂NoSQL,如果你不懂CAP,没关系,UDC远早于NoSQL提出之前就默默地实践着CAP理论,默默开垦着分布式存储的这片天地 -- Just follow me.

在2015年年底,资料平台承载的注册用户数是24.9亿

二、QQ资料平台介绍

有QQ就有资料后台,即通资料是腾讯为数不多的伴随着QQ一路走来的存储平台,也是迄今为止拥有独立存储架构的极少数存储系统之一。

db_server是目前仍在运行的资料逻辑层的一个服务,处理部分来自PC QQ的资料请求协议。资料的所有模块代码都位于“OICQ”目录下,OICQ的名称一直保留至今。

这里面至今保留着小马哥(ponyma)亲自操刀的代码:

言归正传,我们看看资料平台究竟是什么。。。废话,当然是存储QQ资料的后台 -- 可惜你只对了一半,这是资料平台存储的东西:

  • QQ基础资料(昵称、性别等)
  • 各类会员标志位(绿钻、黄钻、蓝钻等标志位和相关等级)
  • 各种后台控制信息(号码状态、冻结标志、注册信息等等)
  • 空间资料
  • 互联登录资料
  • QQ上显示的各类图标
  • 会员资料(头像挂件、气泡、背景等等)
  • 附近的人、QQ秀、QQ看点等等手Q业务托管的资料

恰切地说,资料平台就是存储以QQ号码为主key的通用Key-Value存储平台,专注于为QQ体系的业务提供快速存储接入服务,主要的功能包括:

  1. 快速海量存储接入,提供高性能海量存储服务,提供每秒亿级批量拉取服务。
  2. 使用现有通道快速对接PC-QQ、手机QQ、QQ空间、各类基于QQ统一登录组件的业务;
  3. 集成基于关系链的隐私控制能力;
  4. 快速接入安全打击、日志、统一推送、回滚、重做、数据提取、数据分析等等成熟功能。

2016年元旦前夜,受跨年红包推动,资料平台批量读取的UIN(QQ号码)请求量峰值达6.3G/分钟(1.05亿/秒)

上图中,因为监控平台Monitor最高支持32位整数,超过4.29G的视图部分溢出了,只显示了超过的部分。(在Monitor接入的所有业务中,只有QQ资料平台有出现溢出的情况)

三、存储层的改造

QQ资料平台的存储层(称为用户资料中心,User Data Center,简称UDC)是一套独立的存储系统,负责数据的分片、容灾、存储和负载均衡功能。

1. 改造前的架构

改造之前UDC的总体框架如下:

整体架构分为3层,接口层、缓存层(部分Cache、简单资料Cache、会员Cache等)和File层:

  1. 接口层:负责请求调度、负载均衡、部分Cache回填等逻辑。
  2. 缓存层:缓存数据到内存,提供高性能读请求,存储结构使用多阶哈希,包括两类模块:
    1. 通用部分Cache:缓存部分用户的全量数据,不命中时由接口层回填;
    2. 各类功能Cache:缓存全量用户的部分字段(不同cache缓存不同字段),提供批量读取功能;
  3. File层:负责数据的写操作和对其它拷贝的同步。

在这个存储架构设计之初,UDC的写量非常小,平均写请求每秒不到1000次,file直接写文件完全可以满足需求,另外,这种分层存储的结构有利于解决冷热号段不均的问题。

但随着移动互联网的兴起,UDC的读写请求量都程指数倍翻滚,部分Cache的回填、淘汰使得Cache性能急速下降,File也越来越扛不住海量的写请求。

这是同一个请求指标在2009到2015期间周视图请求量的对比图:

File的内存化成为越来越迫切的需求,在2011年到12年改造之前,UDC的事故发生率越来越频繁,主要的原因是Cache和File都已经达到性能瓶颈,扩容成本越来越大,甚至扩容也无法满足需求。

2. 改造方案

整体的改造思路是:

1)去掉file文件存储,改成全内存的存储;

2)通过多份数据的冗余存储确保数据不会丢失;

3)统一Cache的存储结构,简化数据同步逻辑;

4)存储的字段可配置化,方便新业务接入。

这是改造后的结构:

这个架构主最要的特点就是采用扁平化的设计思路,把File(改造后仍然保留File的叫法,但实际上是全内存存储)和Cache打平,去除接口层。

改造点包括:

  1. 批量Cache统一化:原来的很多业务定制的Cache(简单资料、会员Cache等)合并为统一的批量Cache,不同的批量Cache只有配置不同(主要是支持的字段集合不同),其它完全一样,包括代码和二进制程序。
  2. 存储全内存化:统一使用LinkTable(后面会介绍,这里开源了一个哈希表+LinkTable的实现 GitHub - shaneyuee/shmhash: A multi level hash based on linux shm, using shmhash, user can put any key-value data in shared memory.)作为底层的存储组件,极大提升读写性能。除了BinLog实时落到,读写都是全内存化操作,32核心32G内存的机器单机支持最多25万/秒的读和5千/秒的写。(写主要受限于BinLog写磁盘和同时对30多个备拷贝的同步。UDC最多可以支持96个拷贝,当前拷贝数是31,包括4个File拷贝和27个批量Cache拷贝。)
  3. 架构扁平化:废除接口层和部分Cache,使得File和Cache处于统一层级,都直接被逻辑层寻址;File主拷贝同步备拷贝时不再区分Cache和File,使用统一的同步方式进行同步;
  4. 收拢数据出口:日志、备份、转发统一收拢到File进行分发。因为逻辑层的命令字很多,参差不齐,转发逻辑变更时需要地毯式搜索所有入口进行修改,收拢到File后,所有字段都原样转发到转发中心/备份中心,集中一处配置分发到具体业务:
    1. 流水日志:日志回滚中心取代流水日志中心,增强回滚功能,增加日志重做功能;
    2. 切片备份:切片转发中心取代备份中心,增强加密备份功能、TDW对接功能,增加数据提取功能、统计功能;
    3. 变更通知:加强资料转发中心功能,支持可配置化转发(按字段、命令字、业务类型配置转发),支持手Q、PC-QQ在线push,支持push给好友;
  5. 虚拟字段支持:支持多个字段/多个字段的部分bit/多个字段的部分字节任意组成一个新的字段进行读写访问和Push通知。

新架构的优点非常明显,主要表现在:

  1. 架构扁平,没有复杂逻辑,运营方便
  2. 高性能,单机25w/s,99.9%时延小于1ms
  3. 冷热分离,热数据放批量Cache,合并拉取,减少网络和机器资源
  4. 数据同构化,全部为紧凑TLV格式存储,增强一致性保证
  5. 分类转发,线下切片转发 vs. 在线PUSH
  6. 聚合运算,基于切片导数据 vs. 在线导数据(批量/Filter)

四、存储层的主要特性

下面简单介绍下存储层的几个主要特性:

1. 内部存储结构

底层的存储组件是分级存储的LinkTable,使用全内存存储,每天定时dump到文件上进行备份。

这是LinkTable的存储结构:

QQ后台的小伙伴设计了一个基于共享内存的存储LinkTable,基本思路就是将共享内存分块,块与块之间用链表链接起来,这样可以将变长数据分到一个或多个块进行存储。UDC对LinkTable进行了大量的优化,包括预回收池、空闲链表、索引排序优化等等,后面有机会在单独进行分享

基于LinkTable的内部存储结构如下:

上图展现的是UDC的存储结构,其中,批量Cache和File的存储结构基本上一致,唯一的不同是,File上面运行着同步进程负责对其它拷贝进行数据同步,批量Cache上面没有同步进程 -- File也同支持批量拉取协议,后面专门讲述批量逻辑,可以把File理解为支持全量字段和接受写操作并对外同步的批量Cache

为了提高性能,内部实现全部使用TLP(Type-Length-Pointer,基于指针的TLV实现)以减少内存拷贝。

UDC同时支持gzip/lz4/lz4hc/snappy的压缩格式。默认使用snappy格式,压缩率达75%以上,压缩的性能开销几乎可以忽略,压缩后CPU涨幅不到5%,单机仍然可以处理25w/s的请求。

2. 寻址逻辑

比较当前流行的分布式存储系统,大多数采用基于哈希的寻址方式,比如一致性哈希,UDC仍然采用相对传统的寻址方式,配置化寻址:由配置中心分段配置QQ号码的路由策略。具体实现方式如下:

  • UDC的配置中心把QQ号码按10万为单位进行配置,当前支持到37.5亿(UDC最大可以支持到128亿的号码,如果需要重新编译后可以支持任意64位整型的号码),分成37500个unit,再分成10个set,每个set 3750的unit,并按号段冷热的情况,每两个set(一个热号段set加一个冷号段set)的unit打散分布到一组机器上(File是12台B6)这样做的目的是:
    1. 均摊冷热号段的请求到不同的机器,实现负载均衡。
    2. 按set部署,方便扩容及容量监控。
    3. 同个号段的号码集中几台机器处理,减少批量拉取时需要分发的机器数量(经验表明大多数用户的好友都跟自己的号码处于同一个号段内)。

  • UDC分多个拷贝进行存储,一个拷贝可以认为就是一份全量用户的数据,有一组机器来承担,一般部署在同一个IDC内。比如File目前有4个拷贝,深圳两个拷贝,上海和天津各一个拷贝,主写在深圳。寻址的时候,如果是写或者读主拷贝,直接寻址主拷贝负责该号码对应Unit的机器;如果是普通的读,按读取方的IDC编码按同IDC->同城->同国->全球的优先级寻址对应Unit的机器。
  • 每台物理机器被划分为多台虚拟机,用端口进行标识,以提高并发处理性能。
    1. 每个端口对应一个具体的写进程和一片共享内存
    2. 写进程负责对分配到本端口的一组Unit进行写操作,以此保证任何时刻只有一个写进程操作LinkTable,保证不会有写写冲突的产生。
    3. 读进程attach本机所有的端口对应的共享内存,不区分收包端口,请求包发到任何读进程都可以被正确处理。这样做的目的是为了保证处理批量请求的时候,没必要把请求包分发到同一台机器的不同进程上。

3. 同步逻辑

UDC的同步逻辑复用mysql的binlog+seq机制,简单说:

  • 每个Unit维护一个Sequence变量
  • Unit数据变更时,Sequence自增,保存对应Seq的完整修改日志(Bin Log)
  • 同步数据时,顺序按照Seq取出Bin Log在另一个拷贝上重做

上图描述的是增量同步的方式,在实现细节上,UDC考虑到写BinLog开销和数据一致性的问题,采用全量同步的方式:

  1. BinLog中只保留了Seq和对应写入的QQ号码(UIN)。
  2. 维护一份所有Unit的最新Sequence和其它拷贝的对端Sequence表。
  3. 同步进程以Unit为单位对比两边的Seq,发现不一致时发起同步操作:
      1. 读取对方Seq+1对应的UIN和最新数据,发送给对方的写进程进行同步。
      2. 对方写进程接收到同步包后,对比请求包的Seq和本地的Seq。
      3. 如果请求包的Seq=本地Seq+1,则更新本地数据并返回成功,同步进程收到回包后将对端Seq加1。
      4. 否则回包告知主写server本机该Unit当前的Seq,主写进程更新对端Seq为新的Seq。

另外,因为UDC的拷贝数很多(目前是31个),一次写请求会触发31次同步,为了缓解同步压力,UDC对同步进行了优化,包括:

  1. 合并同步包:对于同一台对端机器,合并多个同步包进行同步,合并受限于两个条件:
  • 数据包大小,默认配置上限20K
  • 合并同步包个数,默认配置上限100个

默认情况下,一旦有写请求,马上就会触发同步,不会有合并的情况,当写量比较大时,上一次同步还没结束,就已经发生了很多次新的写入,此时同步进程就会主动合并同步包进行同步。

  1. 按需同步:为了减少同步包的个数,同步进程主动检查要同步的对端机器有没有缓存要写入的字段,如果没有,则不进行同步,只是发一个更新Sequence的包让对端Sequence加1。

-- 因为写量大时自动合并多个同步包,对于批量Cache来说,很多写入的字段都不需要同步,此时同步性能可以得到极大提升。

  1. 异步化同步:同步进程有两个独立模块实现,一个是发包模块,一个是收包模块,两个模块异步独立运行,不相互等待,即:

-- 同步操作不设置超时时间

-- 发包模块只管当前哪些拷贝需要发同步包进行同步

-- 收包模块只管当前收到哪些对端机器的同步确认包,以便更新对端Sequence

写进程操作过程:

  1. 写数据和本地Sequence,每个Unit对应一个Sequence,跟数据一起存储在LinkTable中。
  2. 写BinLog缓存和BinLog文件。

-- UDC的BinLog缓存是一个共享内存哈希表,目的是为了加速同步,使得正常情况下通过共享内存交互就可以完成数据同步,只有当突然有大量写入或对端死机恢复的时候,才需要读磁盘的BinLog进行同步。

同步进程操作过程:

  1. 遍历Unit,遍历对应的备Copy列表
  2. 读取Unit的本地Sequence
  3. 读取Unit和对应Copy的对端Sequence
  4. 判断是否需求同步(对端Sequence < 本地Sequence),如果不需要则跳过
  5. 读取Unit+对端Sequence对应的BinLog(先读缓存,缓存不命中则读文件)
  6. 根据BinLog的Uin取出最新数据,同步给对端拷贝

备机接收同步过程:

  1. 写数据
  2. 写BinLog文件
  3. 更新本地Sequence

如果备拷贝是File,也需要维护BinLog文件,目的是为了主机死机的时候,备机可以随时接替主机,成为新的主写机器,对其它拷贝进行同步。

4. 自动切换高速写模式

因为写进程写BinLog时需要实时落地,以确保数据万无一失,使得UDC单机能处理的最大写请求在每秒5000次左右。

为了全力支持今年的跨年红包和除夕红包活动(红包活动发放的奖品中包含头像挂件和气泡,这两个字段都存储在UDC中),按产品的要求,UDC需要支持20万/秒的写入量(大约是平时写入量的20倍,平时写入量不到1万/秒)。

为此,UDC首次采用缓存写BinLog文件的方式,写BinLog时只需写入缓存,然后马上返回,等空闲时再检查是否需要把BinLog合并落地到文件中。落地文件需要满足两个条件:

  1. BinLog的个数达到一定阀值,比如100,一个Unit已经连续写了100次
  2. 上次落地的时间超过一定期限,比如1s,超过1s没落地就要强制落地。

BinLog缓存落地作为一个开关,开启的时候UDC进入极限写操作模式,在B5机器4个进程压测时可以达到2万/秒,运营环境是B6机器8个进程,保守估计可以达到5万/秒写入以上。

在元旦前夕进行多次演练,发现开关的打开关闭很麻烦,一不小心很容易造成运营事故,为此,我们考虑到这个开关是否可以跟过载保护机制结合起来,在机器接近过载的时候自动打开这个开关,并在判断完全恢复后自动关闭开关,进入实时落地BinLog模式。

这里大概介绍下UDC的过载保护机制,UDC收包的时候,会同时检查该数据包从进入网卡队列到应用层收取时的时延,如果这个时延大于一定阀值(写进程配置的是500毫秒),则认为自己过载了,尽快将数据包从socket缓冲区中取出来丢掉,以确保新来的请求能被处理,详细的过载保护机制请看这篇文章:一种高效防止滚雪球并快速从滚雪球状态恢复的方法

经过多次演习,我们最终确定两个数字,3和200:

-- 如果收包时延大于200ms,证明系统的负载已经很大,需要快速进行高速写模式,自动把开关打开。

-- 如果收包时延小于3ms,证明系统已经恢复,自动把开关关闭。

5. 批量Cache

UDC区别于其它分布式存储系统,最主要的特色体现在高性能批量Cache上。

批量Cache的前身是SimpleInfo,一套为了PC客户端批量拉取好友简单资料搭建的全量号码部分字段的Cache,直接使用数组方式管理共享内存,每个UIN占用固定大小。SimpleInfo的最大缺点是无法增加字段,业务逻辑和存储绑定在一起,导致访问性能底下(当时的16核心机器单机处理性能是8000/秒),而扩容又异常复杂。

后来因为业务的发展,空间资料、会员标志位、Open资料相继接入UDC,这些业务同样也需要批量拉取的功能,于是开发了类似SimpleInfo的定制化批量Cache系统(后面我们称为“旧批量Cache”),所不同的是,考虑到字段扩容的问题,不再使用数组方式管理内存,而是使用当时比较流行的LinkTable,处理性能有所提升,而且扩容、加字段等运维操作变得更为简单。

批量Cache的批量拉取协议采用接力赛的方式在多个分布式的服务器间进行转包,并由最后处理完请求的服务进程回包给请求方,如下图所示:

其中,batch_cache_svr是指具体处理用户请求的服务进程,而每台物理机器一般会运行数个到数十个不等的服务进程,进程直接也会相互转包,服务进程处理完本进程服务的UIN后,会优先转给本机的其它进程,当本机所有进程都处理完成后,才转给其它机器进行处理。

批量Cache协议的设计决定了其固有的缺陷,就是内部转包的问题。 根据Monitor上的统计,几乎每一个外部读请求,都需要被转发10次才能完成读取操作,从而使得服务器90%的CPU都用来处理内部转包的逻辑,大大限制了服务器的处理能力。

下面的表格统计了当时情况下(2012年1月9日峰值),几个批量Cache系统的转包情况:

批量Cache名称

全部请求(pkg/min)

进程间转包

机器间转包

外部请求

空间资料Cache

18.8M

70%

20%

10%

会员Cache

4.7M

27%

63%

10%*

互联Cache

1.4M

70%

12%

18%

批量Cache改造主要做了几个事情:

  1. 统一为一套代码、一套二进制
  2. 支持字段可配置化
  3. 底层改成优化的LinkTable存储
  4. 串包协议改成分包协议

分包的协议如下图所示:

改造后单机处理能力得到极大提升,单机处理请求UIN数峰值可以达到300万/秒,处理请求量可以达到25万/秒。

这是改造完成后运营系统的一台批量Cache机器处理的Uin数上报视图:

单机处理130万Uin/秒时占用的CPU只有32%。

这里大概总结下批量Cache高性能的必备因素:

  1. “0”拷贝,批量Cache从收包到回包,总共做了两次拷贝,一次是从LinkTable取出数据,一次是组回包,其它的处理全部是基于指针进行。
  2. “0”系统调用,UDC收包后第一件事是取系统时间,使用的是优化过的时间函数(敬请期待后续分享文章:Linux下的gettimeofday()函数及其优化),几乎没有开销,其它系统调用除了send/recvd都被优化调了。
  3. 实时打解包,UDC的请求包和回包被设计成可以只处理部分内容的格式,在处理某个UIN时实时解开该UIN请求,处理完请求后实时打包该UIN结果,使得内存拷贝降至最少。

-- 基于同一思想,shane(作者)打造了第一个支持实时打解部分包的KV协议处理引擎--KnvProtoEngine,该引擎已经作为腾讯第一批开源软件对外开源并成功申请国家专利,欢迎大家尝先使用~~

6. 虚拟字段

UDC的另外一个特性就是对虚拟字段的支持,恰切地说,是为资料平台支持虚拟字段提供了条件--虚拟字段的映射逻辑是在逻辑层实现的。

UDC的协议支持按bit/byte修改和读取字段的部分值,因而,如果用户只想拉取所有好友的会员标志位,逻辑层只需要批量拉取一个bit就可以实现,大大节省网络资源。

虚拟字段其实是一个或多个字段的全部或部分组成一个新的字段,对业务方来说,使用虚拟字段跟真实字段,完全没有区别。

下图是部分虚拟字段的映射表:

资料平台目前配置了超过3000个虚拟字段

五、结束语

UDC作为一个腾讯最古老的分布式存储系统,同时作为公司级海量服务课程的核心载体,在即通几代新老程序员(tony、ls、ppchen、bisonliao等等)的共同努力下,时刻保持与时俱进,从PC时代到互联网时代,到移动互联网时代,再到物联网时代,时时刻刻接受着挑战,不断超越--不断超越同行,不断自我超越,不断引领分布式存储的大潮流。

  • 24
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值