您现在的位置是:数据库 >>正文
RocksDB 在 vivo 消息推送系统中的实践
数据库749人已围观
简介一、背景在消息推送系统中,业务方服务器通过调用推送接口向 VPUSH 服务发送消息,请求中会包含一个 registerId,用于标识接收该消息的用户设备。当 VPUSH 服务接收 ...

一 、消息推背景
在消息推送系统中 ,送系业务方服务器通过调用推送接口向 VPUSH 服务发送消息,统中请求中会包含一个 registerId,消息推用于标识接收该消息的送系用户设备。当 VPUSH 服务接收到推送请求时,统中会使用 registerId 找到对应的消息推用户,并将消息推送给其手机 。送系然而,统中在 VPUSH 内部处理消息推送时 ,消息推需要使用一个内部标识符 ClientId 来标识每个用户的送系设备,可以通过 ClientId 查找到对应的统中设备信息。
因此,云计算消息推系统中引入了一个映射存储服务 MappingTranformServer(后文简称MT),送系用于处理 registerId 和 ClientId 之间的统中转换。MT 服务缓存了所有用户的设备标识符 ,使用 RocksDB 作为底层存储引擎,RocksDB 可以提供高并发读写能力,以磁盘作为存储介质,节省存储成本。当 VPUSH 服务需要将 registerId 转换为 ClientId 时,会向 MT 服务发起查询请求 ,MT 服务根据请求中的 registerId 查找对应的 ClientId 并返回,这样系统下游节点就能够通过 ClientId 找到对应的设备 ,免费模板并将消息推送到用户手机上了。
系统中除了 registerId 以外,还有许多其他的标识符 ,所以引入了 ClientId 来降低后期维护和开发的成本。由于regId 比较有代表性,下文中主要会以 regId 进行举例讲解 。

二、RocksDB 原理介绍
在介绍业务场景之前 ,先简单介绍一下RocksDB的基本原理。RocksDB的前身是LevelDB ,由于LevelDB不支持高并发写入,模板下载Facebook(Meta)针对于LevelDB的一些痛点进行了改造,便有了RocksDB。RocksDB相比于LevelDB ,其支持高并发读写,优化了SST文件布局 ,提供了多种压缩策略,总的来说,RocksDB 在继承了 LevelDB 的全部功能的基础之上,还针对内存和磁盘数据存储进行了优化,使得 RocksDB 具有更高的吞吐量和更低的延迟 ,香港云服务器更适合分布式 、高可靠性的存储场景。行业内也有许多数据库将RocksDB作为底层的存储引擎,比如 TiDB 。
2.1 LSM设计思想在介绍RocksDB的架构和原理之前 ,先来了解一下其设计思想:LSM 。
LSM全称为log-structured merge-tree。LSM并非一种数据结构 ,而是一种设计思想 ,最根本的目的就是服务器租用要规避对磁盘的随机写入问题,提升写的效率。其思路如下:
写入顺序 :从内存到磁盘
将数据先写入到内存中 。随着内存存储数据越来越多 ,达到内存阈值,则会将内存中的数据转移到磁盘中。磁盘中数据也分为多层 ,其中L0层的数据最热,而最冷的数据分布在Ln层,且会定期进行合并操作。LSM 在 RocksDB 设计中的建站模板体现:
在写入数据的时候,同时记录操作日志。因为内存具有易失性 ,当程序崩溃后,内存的数据就丢失了 ,记录日志用于在程序崩溃或者重启时,内存的数据不会丢失。磁盘中的数据并非使用了整体索引结构,而是使用了有序的文件集合结构 。每次将内存中的数据写入到磁盘中或者将磁盘中的数据进行合并时,都会生成新的文件,这一次生成的文件会作为一个层 ,磁盘上会划分多层 ,层与层之间相互隔离,并且有序 ,有序保证了查找数据时可以使用二分查找 。磁盘文件层级如下图所示。数据按照key进行字典序排序。由上述可知,数据从内存写入磁盘时,会不断生成新的文件,所以需要不断对磁盘中的文件进行合并,然而如果数据乱序 ,便无法做到高效合并且保持有序。
当然 ,LSM也存在一些读放大、写放大 、空间放大的问题:
【读放大】:读取的时候需要从内存一直寻找到磁盘中【写放大】:程序写入数据一次 ,系统要写多次(例如 :内存一次、磁盘一次)【空间放大】:一份数据在系统中多个地方存在 ,占用了更多空间2.2 内部结构了解完LSM后,可以仔细剖析一下 RocksbDB 的内部结构,下图是 RocksDB 的内部结构图。
RocksDB 中会分出 ColumnFamily(列族,一系列 kv 组成的数据集 ,可以理解为就是一个 namespace) ,所有的读写操作都需指定 ColumnFamily,每个 ColumnFamily 主要由三部分组成,分别是 memtable/sstfile/wal。
memtable 是内存文件数据 ,新写入的数据会先进入到 memtable 中,当 memtable 内存空间写满后,会有一部分老数据被转移到 sstfile 中 。sstfile 便是磁盘中的持久化文件。所有 ColumnFamily 都会共享 WAL(write-ahead-log) 日志文件 。
(1)内存部分
① memtable
也称为active memtable 。热点数据均存在这块内存中,用于快速返回用户的读写请求。一旦memtable被写满之后,就会被转为immutable memtable,并生成一个新的active memtable来提供服务。memtable支持多种结构:
skipList/vector/hashLinkList。写入数据时通过对key进行字典序排序 ,保持有序。跳跃表的查找速度可以简单理解近似二分查找log(n)。跳表结构如下图所示 :

② immutable memtable
是由于memtable写满后,转换而来 ,只提供读,不能做修改 。当系统中触发flush时,就会将同一个ColumnFamily中的immutable memtable进行合并,生成一个sst file放入磁盘中,位于磁盘的L0层。
(2)磁盘部分
① sst,全称为sorted sequence table
是存储在磁盘中的持久化数据 。sst中也有多种格式,默认设置为BlockBasedTable。其是根据data block来进行归类存储的。block中还分为data block数据块,meta block元数据块 ,footer块尾。每块的k-v都是有序的 。data block也有缓存,名为block cache。顾名思义用于缓存SST文件中的热点数据到内存中,提供高速的读服务,所有ColumnFamily中都共用一块block cache 。block cache可以设置两种数据结构:LRU cache和Clock cache 。
② WAL,全称为write ahead log。
WAL会把所有写操作保存到磁盘中,当程序发生崩溃时,可以利用WAL重新构建memtable 。如果容忍一定数量数据丢失,也可以关闭WAL来提升写入的性能。
③ Manifest
该文件主要用于持久化整个LSM的信息。RocksDB需要将LSM树的信息保存于内存中,以便快速进行查找或者对sst进行compaction压缩合并 。而RocksDB也会将这些信息持久化到磁盘中 ,这个就是Manifest文件。其主要内容便是事务性相关日志以及RocksDB状态的变化 。当RocksDB崩溃后重启时,就会先读取Manifest文件对LSM进行重建 ,再根据WAL对内存memtable进行恢复。
2.3 写入数据流程了解完RocksDB的内部结构,我们来分析一下 RocksDB 的写入流程如下:

写入流程 :
将数据写入 memtable 的同时也会写 WAL(write-ahead-log)当 memtable 达到一定阈值后,会将数据迁移到 immutable memtable,其中 ,immutable 中的数据只能读不能写之后 flush 线程会负责将 immutable 中的数据持久化到磁盘中,即 SST file(L0层)compaction 线程会触发 compaction 操作将 L0 的 sst file 合并到 L1-Ln 层中。所有的 sst file 都是只读不写2.4 读取数据流程同样地 ,还有读取流程 ,RocksDB 的读取流程如下:

简而言之 ,读流程基于内存到磁盘的顺序 ,逐层进行查找。
下图为读取过程中所经历的一些数据对象 :
列族指针ColumnFamilyHandle指向了列族对象ColumnFamily ,列族对象中存放有列族相关的数据 :ColumnFamilyData 。ColumnFamilyData 中关键的数据为 SuperVersion ,SuperVersion 为当前最新版本的数据集 ,内部维护了内存的memtable和immutable memtable的指针以及磁盘数据的指针 。
读取细节如下图所示:
数据读取的入口为DBImpl的Get方法,通过该入口 ,先在内存中的MemTable进行遍历。图中的MemTableResp为MemTable的具体实现 。当在MemTable中没有读取到数据时,便会到MemTableListVersion中进行读取 ,MemTableListVersion 内部存放着多个 immutable memtable。当内存中读取不到数据时,便会到磁盘中读取,也就是Version类 。Version中FilePicker逐层读取文件,每次读取到文件时,先查看TableCache ,TableCache维护了SST读取器的信息,方便快速查找 。如果在TableCache中没找到相关的信息,便会执行FindTable,并将读取到读取器放入到TableCache中 ,方便下次查找 。最后,通过读取器对SST进行遍历查找。
RocksDB 通过在写入数据时先存入内存来保证写入高性能 ,内存写满后便会将内存的数据转移到磁盘 ,写入磁盘时保持 key 有序来提升磁盘查询的效率(类似于二分查找) ,并且对磁盘中的数据进行分层 ,热点数据所在的层级越低,冷数据存储的层级越高。
三、业务场景介绍
简单了解了 RocksDB 后 ,来看下具体的一些业务实践场景 。
目前 ,registerId 与 clientId 的映射数量约为数百亿,每个应用为每个用户分配一个 registerId,但每个用户只有一个 clientId ,因此 ,registerId 到 clientId 的映射是多对一的关系。这些数据都存储在RocksDB中。

为了做到服务的高并发 、高可用,每个应用的缓存以多副本的形式分散在多台 MT 服务器中,形成多对多的关系。例如 ,MT1、MT2 和 MT3 中均缓存了app1的全量数据 ,app2 的全量数据则存放于 MT2 和 MT4 中,如下图所示:
图片
消息推送时 ,MT的上游服务会根据推送请求内的appId寻址到MT服务器完成映射的转换。
此时 ,读者可能会想到,不少系统使用 Redis 作为缓存服务,它似乎也可以完成这样的任务 ,为什么还需要开发一个专门的映射服务 ?
实际上,主要有以下几个原因 :
成本问题