VIP会员一折促销,仅需200元/年!
当前位置: 首页 > 资讯 > 社会热点 > 两大10亿级的储存挑战,微信究竟是怎么扛住的?

两大10亿级的储存挑战,微信究竟是怎么扛住的?

放大字体  缩小字体 发布日期:2019-09-18 浏览次数:0
8090奶茶加盟

原标题:两大10亿级的储存挑战,微信究竟是怎么扛住的?

本文转载自公众号:云加社区(ID:QcloudCommunity,腾讯云官方开发者社区),作者:林枫&张根兴

10亿级,是微信用户的数量级。这个庞大数字的背后,是“看一看”、“微信广告”、“微信支付”、“小程序”等业务对数据库10亿级的读写需求。那么,在此场景下诞生的FeatureKV,究竟是怎样强悍的一个存储系统呢?

背景:两个十亿级的挑战

PaxosStore是微信内广泛应用的强一致性的分布式存储系统,它广泛支撑了微信的在线应用,峰值过亿TPS,运行在数千台服务器上,在线服务场景下性能强悍。但在软件开发中没有银弹,在面对离线产出、在线只读的数据场景,PaxosStore面临了两个新的十亿挑战:

10亿/秒的挑战:

“看一看”团队需要一个存储系统来存放CTR过程需要用到的模型,实现存储和计算分离,使得推荐模型的大小不会受限于单机内存;每次对文章的排序打分,ctrsvr会从这个存储系统中拉取成千上万个特征,这些特征需要是相同版本的,PaxosStore的BatchGet不保证相同版本;业务方预估,这个存储系统需要支持10亿/秒的QPS,PaxosStore的副本数是固定的,无法增加只读副本;这个存储系统需要有版本管理和模型管理的功能,支持历史版本回退。

10亿/小时的挑战:

微信内部不少团队反馈,他们需要把10亿级(也就是微信用户的数量级)信息,每天定期写到PaxosStore中,但PaxosStore的写入速度无法满足要求,有时候甚至一天都写不完,写太快还会影响现网的其他业务;PaxosStore是一个保证强一致性的存储系统,为在线业务设计,其性能也能满足在线业务的需求。但面对这种离线灌库、在线只读、不要求强一致性保证的场景,就需要很高的成本才能满足业务的需求了;基于数据的应用越来越多,这类的数据存储需求也越来越多,我们需要解决这个问题,把10亿级key量的数据写入时间控制在1个小时左右。

上述场景具有定时批量写、在线只读的特点,为了解决这些场景的痛点问题,我们基于性能强大的WFS(微信自研分布式文件系统)和稳如磐石的Chubby(微信自研元数据存储),设计并实现了FeatureKV,它是一个高性能Key-Value存储系统,具有以下特点:

高性能且易于扩展

优秀的读性能:在B70机型上,全内存的表可以有千万级的QPS;在TS80A机型上,数据存放于SSD的表可以有百万级的QPS;优秀的写性能:在远程文件系统性能足够的情况下,可以在1小时内完成十亿个key、平均ValueSize是400Byte的数据的写入;易于扩展:水平扩容(读性能)和纵向扩容(容量)可以在数小时内完成,写性能扩容只是扩容一个无状态的模块(DataSvr),可以在分钟级完成。

对批量写支持友好

任务式的写接口:支持以WFS/HDFS上的文件作为输入,业务方无需编写、执行灌数据工具,支持失败重试、告警;支持增量更新/全量更新:增量更新是在上一个版本的基础上,对一批新输入的Key-Value进行覆盖写,输入中没有的key则保持不变。而全量更新则是丢弃上一个版本的数据,灌入一批新的Key-Value;支持TTL:支持过期自动删除功能。

具有版本管理功能

事务的BatchGet接口:保证一次BatchGet得到的数据都是同一个版本的;支持历史版本回退:一次更新会产生一个递增的版本,支持历史版本回退,包括增量更新生成的版本。

当然,在软件开发中没有银弹,FeatureKV在设计上它做了取舍:

不支持在线写入数据,当数据量较小时(GB级),FeatureKV可以做到十分钟级的更新频率;不保证强一致性,保证最终一致性,并且在大部分时间里可以保证顺序一致性。

FeatureKV现在在微信内部已经广泛应用,包括看一看、微信广告、微信支付、小程序等业务,接下来会阐述FeatureKV的设计,并具体说明如何解决上述两个十亿挑战。

总体设计

系统架构

FeatureKV涉及的外部依赖有三个:

Chubby:用来保存系统中的元数据。FeatureKV内很多地方是通过对Chubby内的元数据轮询来实现分布式协同、通信;USER_FS:业务侧的分布式文件系统,可以是WFS/HDFS,因为FeatureKV的写接口是任务式的,输入是一个分布式文件系统上的路径;FKV_WFS:FeatureKV使用的分布式文件系统,用来存放DataSvr产出的、可以被KVSvr使用的数据文件。可以保存多个历史版本,用于支持历史版本回退。

这三个外部依赖都可以和其他业务共用。FKV_WFS和USER_FS可以是同一个模块。FKV_WFS可以使用HDFS替代。Chubby可以使用etcd替代。

DataSvr:

主要负责写数据,把USER_FS的输入,经过数据格式重整、路由分片、建索引等流程,生成KVSvr可用的数据文件,写到FKV_WFS中;它是一个无状态的服务,写任务的状态信息保存在Chubby中,扩容DataSvr,可以增加系统的写性能;一般部署2台就好,部分场景写任务较多可以适当扩容。

KVSvr:

对外提供读服务,通过轮询Chubby来感知数据更新,再从WFS拉取数据到本地,加载数据并提供只读服务;它是一个有状态服务,一个KVSvr模块会由K个Sect和N个Role组成,共K*N台机器;每个Sect都有全量的数据,每次BatchGet只需要发往某一个Sect,增加Sect可以扩容读性能,而并不会增加BatchGet的rpc次数;相同的Role负责的数据切片都是一样的,单机故障时Batch请求直接换机重试就好;K最少是2,用以保证系统的容灾能力,包括在变更时候的可用性;N不能是任意一个数字,可以看下面第二部分。

写入流程:

FeatureKV只支持批量写入数据,每次写任务可以是增量更新/全量更新的,每次写入的数据量大小无限制。离线的批量写接口设计,我们踩过一些坑:

一开始我们打算封一些类/工具,打算让业务端直接用我们的类/工具,打包Key-Value数据,直接写到FKV_WFS的目录上。该方案最省带宽,但是这样做让我们后续的数据格式升级变得很麻烦,需要让所有业务方配合,所以这个方案就废弃了。

然后,我们起了一个新模块DataSvr,在DataSvr上面开了一个tcpsvr,业务侧输出Key-Value,写入工具会把Key-Value数据发过来这个tcpsvr完成打包,但是还是有下面这些问题:

写入的速度与业务方的代码质量、机器资源有关,曾经碰到过的情况是,业务方的代码里面用std::stringstreams解析浮点数输入,这个函数占用了90%+的CPU(用std::strtof会快很多),或者业务方跑写入工具的机器,被别的进程用了90%+的CPU,最后反馈FeatureKV写得很慢;DataSvr的日常变更或机器故障,会导致任务失败。前端工具发包的方法无法对任务进行重试,因为Key-Value的输入流无法重放。

最终,我们设计了一个任务式的接口,以USER_FS上的路径作为输入:

业务侧把数据按照约定好的格式,放在USER_FS中,向DataSvr提交一个写任务;DataSvr流式读取USER_FS中的数据,对数据进行格式重整、路由分片、建索引,然后把数据写入FKV_WFS中,并更新Chubby中的元数据。其中写任务的分布式执行、失败重试等,也需要通过Chubby来同步任务状态;KVSvr通过轮询Chubby感知数据更新,把数据拉取到本地,完成加载并提供服务。

数据路由

考虑扩缩容,FeatureKV会把一个版本的数据切分为N份,N现在是2400,通过哈希HashFun(key)%N来决定key属于那份文件。

KVSvr加载哪些文件是由一致性哈希决定的,角色相同的KVSvr会加载相同一批在扩缩容的时候,数据腾挪的单位是文件。

由于这个一致性哈希只有2400个节点,当2400不能被sect内机器数量整除时,会出现比较明显的负载不均衡的情况。所以FeatureKV的sect内机器数得能够整除2400。还好2400是一个幸运数,它30以内的因数包括1,2,3,4,5,6,8,10,12,15,16,20,24,25,30,已经可以满足大部分场景了。

上图是N=6时候的例子,Part_00[0-5]表示6份数据文件。从RoleNum=2扩容成RoleNum=3的时候,只需要对Part_003和Part_005这两份文件进行腾挪,Part_005从Role_0迁出至Role_2,Part_003从Role_1迁出至Role_2。

由于现网所用的N=2400,节点数较少,为了减少每次路由的耗时,我们枚举了RoleNum<100&&2400%RoleNum==0的所有情况,打了一个一致性哈希表。

系统扩展性

FeatureKV的FKV_WFS上存有当前可用版本的所有数据,所以扩容导致的文件腾挪,只需要新角色的机器从FKV_WFS拉取相应编号的文件,旧角色机器的丢弃相应编号的文件即可。

当BatchSize足够大的时候,一次BatchGet的rpc数量等价于Role数量,这些rpc都是并行的。当Role数量较大时,这些rpc出现最少一个长尾请求的概率就越高,而BatchGet的耗时是取决于最慢一个rpc的。上图展示了单次rpc是长尾请求的概率是0.01%的情况下,不同Role数量情况下的BatchGet长尾概率,通过公式1-(0.999^N)计算。

增加Sect(读性能扩容):

每个Sect都有全量的数据,增加一个Sect意味着增加一个只读副本,可以达到读性能扩容的效果;由于一个BatchGet只需要发往一个Sect,RPC数量是收敛的,不会因为底下的KVSvr有200台而发起200次RPC。这种设计可以降低BatchGet操作的平均耗时,减少长尾请求出现的概率。

增加Role(存储容量+读性能扩容):

假设每台机的存储能力是相等的,增加Role的数量便可以增加存储容量;由于整个模块的机器都多了,所以读性能也会增加,整个模块在读吞吐量上的扩容效果等价于增加Sect;但当Role数量较大时,一次BatchGet涉及的机器会变多,出现长尾请求概率会增大,所以一般建议Role的数量不要超过30。

增加DataSvr(写性能扩容):

DataSvr是一个无状态服务,可以做到分钟级的扩容速度;底下的写任务是分布式的跑,一次写会切分为多个并行的job,增加DataSvr的实例数,可以增加整个模块的写性能。

数据迁移都是以文件为级别,没有复杂的迁移逻辑,不考虑灰度流程的话,可以在小时级完成,考虑灰度流程一般是一天内。

系统容灾

KVSvr侧:

每个Sect的机器是部署在同一个园区的,只需要部署2个Sect就可以容忍一个园区的机器故障;具体案例:2019年3月23号,上海南汇园区光缆被挖断,某个featurekv有1/3的机器在上面,故障期间服务稳定;故障期间部分RPC超时,导致长尾请求增加。但是换机重试之后大部分请求都成功了,最终失败出现次数很低。后续全局屏蔽了南汇园区的机器之后,长尾请求和最终失败完全消失。

DataSvr/WFS侧:

即便这两部分整个挂掉,FeatureKV的KVSvr还是可以提供只读服务,对于大部分定时批量写、在线只读的场景,这样已经足够了;具体案例:2019年6月3号,某个分布式文件系统集群故障,不可用9小时。某个featurekv的USER_FS和FKV_WFS都是这个集群。故障期间业务方的输出产出流程也停止了,没有产生写任务。整个故障期间,featurekv的读服务稳定。

十亿每秒的挑战:在线读服务的具体设计

KVSvr读性能优化

为了提高KVSvr的性能,我们采取了下面一些优化手段:

1)高性能哈希表

针对部分数据量较少、读请求很高的数据,FeatureKV可以用MemTable这一个全内存的表结构来提供服务。Memtable底层实现是一个我们自己实现的只读哈希表,在16线程并发访问的时候可以达到2800w的QPS,已经超过了rpc框架的性能,不会成为整个系统瓶颈。

2)libcoaio

针对部分数据量较大、读请求要求较低的数据,FeatureKV可以用BlkTable或IdxTable这两种表结构来提供服务,这两表结构会把数据存放在SSD中。而SSD的读性能需要通过多路并发访问才能完全发挥。在线服务不可能开太多的线程,操作系统的调度是有开销的。这里我们利用了libco中对linuxaio的封装,实现了协程级的多路并发读盘,经过压测在value_size是100Byte的情况下,TS80A上4块SSD盘可以达到150w+/s的QPS。

3)数据包序列化

在perf调优的过程中,我们发现batch_size较大的情况下(ctrfeaturekv的平均batch_size是4k+),rpc数据包的序列化时耗时会较大,所以这里我们自己做了一层序列化/反序列化,rpc层的参数是一段二进制buffer。

4)数据压缩

不同业务对数据压缩的需求是不一样的,在存储模型的场景,value会是一段浮点数/浮点数数组,表示一些非0.特征。这时候如果用snappy这类明文压缩算法,效果就不太好了,压缩比不高而且浪费cpu。针对这类场景,我们引入了半精度浮点数(由kimmyzhang的sage库提供)来做传输阶段的数据压缩,降低带宽成本。

分布式事务BatchGet的实现

需求背景:

更新分为全量更新和增量更新两种,一次更新包括多条数据,每次更新都会让版本号递增,BatchGet也会返回多条数据。业务方希望这些更新都是事务的,BatchGet的时候如果一个更新没有全部执行完,那就返回上一个版本的数据,不能返回半新半旧的数据。

RoleNum=1的情况:

数据没有分片,都落在同一台机器上,我们调研后发现有这么两种做法:

MVCC:多版本并发控制,具体实现就是LevelDB这样的存储引擎,保存多版本的数据,可以通过snapshot控制数据的生命周期,以及访问指定版本的数据。这种方案的数据结构需要同时支持读写操作,后台也得有线程通过清理过期的数据,要支持全量更新也是比较复杂;COW:写时复制,具体的实现就是双Buffer切换,具体到FeatureKV的场景,增量更新还需要把上一个版本的数据拷贝一份,再加上增量的数据。这种方案的好处是可以设计一个生成后只读的数据结构,只读的数据结构可以有更高的性能,缺点是需要双倍的空间开销。

为了保证在线服务的性能,我们采用了COW的方式,设计了第一部分中提到了只读哈希表,来做到单机的事务BatchGet。

RoleNum>1的情况:

数据分布在不同机器,而不同机器完成数据加载的时间点不一样,从分布式的角度去看,可能没有一个统一的版本。

一个直观的想法,就是保存最近N份版本,然后选出每个Role都有的、最新的一份版本。

N的取值会影响存储资源(内存、磁盘)的开销,最少是2。为了达到这个目的,我们在DataSvr侧加入了这么两个限制:

单个表的更新是串行的;写任务开始结束之前,加多一步版本对齐的逻辑,即等待所有的kvsvr都加载完最新的版本。

这样我们就可以在只保留最近2个版本的情况下,保证分布式上拥有一个统一的版本。在COW的场景下,只要把另外一个Buffer的数据延期删除(直到下次更新才删),就可以了保留最近2个版本了,内存开销也不会变大。

拥有全局统一的版本之后,事务BatchGet应该怎么实现呢?

先发一轮rpc询问各role的版本情况?这样做会让QPS翻倍,并且下一时刻那台机可能就发生数据更新了。

数据更新、版本变动其实是很低频的,大部分时刻都是返回最新一个版本就行了,并且可以在回包的时候带上B-Version(即另外一个Buffer的版本),让client端在出现版本不一致的时候,可以选出一个全局统一的版本SyncVersion,再对不是SyncVersion的数据进行重试。

在数据更新的时候,数据不一致的持续时间可能是分钟级的,这种做法会带来一波波的重试请求,影响系统的稳定性。所以我们还做了一个优化就是缓存下这个SyncVersion,每次BatchGet的时候,如果有SyncVersion缓存,则直接拉取SyncVersion这个版本的数据。

版本回退

每个表的元数据中有一个回退版本字段,默认是0表示不处于回退状态,当这个字段非0,则表示回退至某个版本。

先考虑如何实现版本回退:

考虑简单的情况,一个表每次都是全量更新。那么每次让都是让KVSvr从FKV_WFS拉取指定版本的数据到本地,走正常的全量更新流程就好了。

然后,需要考虑增量的情况。如果一个表每次更新都是增量更新,那么回退某个版本Vi,就需要把V1到Vi这一段都拉到KVSvr本地,进行更新重放,类似于数据库的binlog,当累计了成千上万的增量版本之后,这是不可能完成的事。

我们需要有一个异步的worker,来把一段连续的增量,以及其前面的全量版本,合并为一个新的全量版本,类似checkpoint的概念,这样就可以保证一次回退不会涉及太多的增量版本。这个异步的worker的实现在DataSvr中。

更进一步,这里有一个优化就是如果回退的版本在本地双Buffer中,那么只是简单的切换一下双Buffer的指针就好,可以做到秒级回退效果。实际上很多回退操作都是回退到最后一个正常版本,很可能是上一个版本,在本地的双Buffer中。

处于回退状态的表禁止写入数据,防止再次写入错误的数据。

再考虑如何解除回退:

解除回退就是让某个表,以回退版本的数据继续提供服务,并且以回退版本的数据为基础执行后续的增量更新。

直接解除回退状态,现网会先更新为回退前的版本,如果还有流量的话则会读到回退前的异常数据,这里存在一个时间窗口。

数据的版本号要保证连续递增,这一点在数据更新的流程中会依赖,所以不能简单粗暴的删除最后一段数据。

为了避免这个问题,我们借用了COW的思想,先复制一遍。具体的实现就是把当前回退的版本,写出一个全量的版本,作为最新的数据版本。

这一步需要点时间,但在回退的场景下,我们对解除回退的耗时要求并不高。只要回退够快,解除回退是安全的,就可以了。

十亿每小时的挑战:离线写流程的具体设计

背景

DataSvr主要的工作是把数据从USER_FS写入FKV_WFS,在写入过程需要做路由切分、数据格式重建等工作,是一个流式处理的过程。

FeatureKV中目前有三种表结构,不同的表结构在写流程中有不一样的处理逻辑:

MemTable:数据全内存,索引是无序的哈希结构,容量受限于内存,离线写逻辑简单;IdxTable:索引全内存,索引是有序的数组,Key量受限于内存,离线写逻辑较为简单,需要写多一份索引;BlkTable:块索引全内存,索引是有序的数据,记录着磁盘中一个4KB数据块的begin_key和end_key,容量没限制,离线写流程复杂,需要对数据文件进行排序。

单机的DataSvr

一开始,我们只有MemTable,数据都是全内存的。MemTable的数据最大也就200+GB,这个数据量并不大,单机处理可以节省分布式协同、结果合并等步骤的开销,所以我们有了上面的架构:

一次写任务只由一个DataSvr执行。

Parser每次处理一个输入文件,解析出Key-Value数据,计算路由并把数据投递到对应的Que。

一个Sender负责处理一个Que的数据,底下会对应多份FKV_FS的文件。FKV_FS上的一个文件只能由一个Sender写入。

总的设计思想是,让可以并行跑的流程都并行起来,榨干硬件资源。

具体的实现,加入了很多批量化的优化,比如对FS的IO都是带buffer的,队列数据的入队/出队都是batch的等,尽量提高整个系统的吞吐能力。

最终,在台24核机器上的写入速度可以达到100MB/s,写入100GB的数据只需要20分钟左右。

分布式的DataSvr

再往后,FeatureKV需要处理十亿级Key量、TB级的数据写入,因此我们加入了IdxTable和BlkTable这两种表结构,这对于写流程的挑战有以下两点:

生成的数据需要有序,只有有序的数据才能做到范围索引的效果,让单机的key量不受内存限制;TB级的写速度,100MB/s是不够用的,写入1TB需要接近3小时的时间,并且这里是不可扩展的,即便有很多很多机器,也是3小时,这里需要变得可以扩展。

先考虑数据排序的问题:

我们得先把数据切片跑完,才能把一个Part的数据都拿出来,对数据进行排序,前面的数据切片类似于MapReduce的Map,后续的排序就是Reduce,Reduce中存在着较大的计算资源开销,需要做成分布式的。

Map阶段复用上述的单机DataSvr逻辑,数据切分后会得到一份临时的全量结果,然后实现一个分布式的Reduce逻辑,每个Reduce的输入是一份无序的数据,输出一份有序的数据及其索引。

这种做法有一次全量写和一次全量读的额外开销。

具体的流程如下图所示,DATASVRSORTING阶段由多台DataSvr参与,每个浅蓝色的方框表示一个DataSvr实例。

再考虑大数据量情况下的扩展性:

参考上图,现在DataSvr的排序阶段其实已经是分布式的了,唯一一个单点的、无法扩容的是数据切片阶段。

实现分布式的数据切片,有两种做法:

一是每个DataSvr处理部分输入的User_Part文件,每个DataSvr都会输出2400个切片后的文件,那么当一次分布式切片有K个DataSvr实例参与,就会生成2400*K个切片后的文件,后续需要把相同编号的文件合并,或者直接作为排序阶段的输入。

二是每个DataSvr负责生成部分编号的FKV文件,每次都读入全量的用户输入,批处理生成一批编号的FKV文件。

第一种做法如果是处理MemTable或者IdxTable,就需要后接一个Merging过程,来把TMP_i_0,TMP_i_1,TMP_i_2...合并为一个FKV_i。而处理BlkTable的时候,由于其后续是有一个Sorting的逻辑的,只需要把Sorting的逻辑改为接受多个文件的输入即可。故这种做法的坏处是在数据量较少的时候,MemTable或者IdxTable采用分布式数据切片可能会更慢,Merging阶段的耗时会比分布式切片减少的耗时更多。

第二种做法生成的直接就是2400个文件,没有后续Merging流程。但它会带来读放大的问题,假设数据被切分成为T批,就会有T-1次额外的全量读开销。在数据量大的情况下,批数会越多,因为排序的数据需要全部都进内存,只能切得更小。

在小数据场景,单机的数据分片已经足够了,所以我们选用了第一种方案。

是否分布式切分,是一个可选项,在数据量较小的情况下,可以不走这条路径,回到单机DataSvr的处理流程。

最终,我们得到了一个可以线性扩展的离线处理流程,面对10亿、1TB数据的数据。

在实现BlkTable之前,这是一个不可能完成的任务。

在实现分布式数据切片之前,这份数据需要120min才能完成写入。

现在,我们只需要71min便可以完成这份数据的写入。

上面这一套流程,其实很像MapReduce,是多个Map,Reduce过程拼接在一起的结果。我们自己实现了一遍,主要是基于性能上的考虑,可以把系统优化到极致。

现网运营状况

FeatureKV在现在已经部署了10+个模块,共270+台机,业务涉及看一看、搜一搜、微信广告、小程序、微信支付、数据中心用户画像、附近的生活、好物圈等各类数业务,解决了离线生成的数据应用于在线服务的痛点问题,支撑着各类数据驱动业务的发展。

最大的一个模型存储模块有210台机:

11亿特征/s:日均峰值BatchGet次数是29w/s,平均BatchSize是3900,模块压测时达到过30亿特征/s;15ms:96.3%的BatchGet请求在15ms内完成,99.6%的BatchGet请求在30ms内完成;99.999999%:99.999999%的事务BatchGet执行成功。

微信广告基于FeatureKV实现个性化拉取+个性化广告位置,推荐策略能够及时更新。相比于旧的方案,拉取量和收入都取得了较大的增长,拉取+21.8%,收入+14.3%。

微信支付在面对面发券以及支付风控中都有用FeatureKV,存储了多份十亿级的特征,之前一天无法更新完的数据可以在数小时内完成更新。

总结

一开始,这类定时批量写、在线只读的需求不太普遍,一般业务会用PaxosStore或者文件分发来解决。

但随着越来越多的应用/需求都与数据有关,这些数据需要定期大规模输入到在线服务当中,并需要很强的版本管理能力,比如用户画像、机器学习的模型(DNN、LR、FM)、规则字典,甚至正排/倒排索引等,因此我们开发了FeatureKV来解决这类痛点问题,并取得了良好的效果。

作者丨林枫&张根兴

来源丨云加社区(ID:QcloudCommunity)

dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn

责任编辑:

“如果发现本网站发布的资讯影响到您的版权,可以联系本站!同时欢迎来本站投稿!

0条 [查看全部]  相关评论