计算机组成原理:设计大型DMP系统

DMP:数据管理平台

DMP 系统的全称叫作数据管理平台(Data Management Platform),目前广泛应用在互联网的广告定向(Ad Targeting)、个性化推荐(Recommendation)这些领域。

通常来说,DMP系统会通过处理海量的互联网访问数据以及机器学习算法,给一个用户标注上各种各样的标签。然后,在我们做个性化推荐和广告投放的时候,再利用这些标签,去做实际的广告排序、推荐等工作。无论是google的搜索广告,淘宝里的千人前面的商品信息,还是抖音里面的信息流推荐,背后都会有一个DMP系统。
在这里插入图片描述

那么,一个DMP系统应该怎么搭建呢?对于外部使用DMP的系统或者用户来说,可以简单的把DMP看成是一个键-值对(Key-Value)数据库。我们的广告系统或者推荐系统,可以通过一个客户端输入用户的唯一标识(ID),然后拿到这个用户的各种信息。

基于此,对于这个KV数据库,我们的期望也很清楚,那就是:低响应、高可用性、高并发、海量数据,同时我们需要付得起对应的成本。如果用数字来衡量这些指标,我们的期望就会具体化成下面这样:

  • 低响应时间:一般的广告系统留给整个广告投放决策的时间也就是10ms左右,所以对于访问DMP获取用户数据,预期的响应时间都在1ms之内。
  • 高可用性:DMP尝尝用在广告系统里面。DMP系统出问题,往往就意味着我们整个的广告收入在不可用的时间就没了,所以我们对于可用性的追求是没有上限的
  • 高并发:还是以广告系统为例,如果每天我们需要响应 100 亿次的广告请求,那么我们每秒的并发请求数就在 100 亿 / (86400) ~= 12K 次左右,所以我们的 DMP 需要支持高并发
  • 数据量:如果我们的产品针对中国市场,那么我们需要有 10 亿个 Key,对应的假设每个用户有 500 个标签,标签有对应的分数。标签和分数都用一个 4 字节(Bytes)的整数来表示,那么一共我们需要 10 亿 x 500 x (4 + 4) Bytes = 400 TB 的数据了。
  • 低成本:我们还是从广告系统的角度来考虑。广告系统的收入通常用 CPM(Cost Per Mille),也就是千次曝光来统计。如果千次曝光的利润是 $0.10,那么每天 100 亿次的曝光就是 100 万美元的利润。这个利润听起来非常高了。但是反过来算一下,你会发现,DMP 每 1000 次的请求的成本不能超过 $0.10。最好只有 $0.01,甚至更低,我们才能尽可能多赚到一点广告利润。

虽然从外部看起来,DMP特别简单,就是一个KV数据库,但是生成这个数据库需要做的事情更多。

  • 为了能够生成这个KV数据库,我们需要有一个在客户端或者Web端的数据采集模块,不断采集用户的行为,向后端的服务器发送数据
  • 服务端接收到数据,就要把这份数据仿造一个数据管道(Data Pipeline)里面。
  • 数据管道的下游,需要实际将数据落地到数据仓库(Data Warehouse),把所有的这些数据结构化的存储起来
  • 后继,我们就可以通过程序去分析这部分日志,生成报表或者利用数据运行各种机器学习算法
  • 除了这个数据仓库之外,我们还会有一个实时数据处理模块,也放在数据管道的下游。它同样会读取数据管道里面的数,去进行各种实时计算,然后把需要的结果写入到DMP的KV数据库里面去

在这里插入图片描述

MongoDB 真的万能吗?

数据管道和数据仓库的性能取舍:

  • 对于数据管道来说,我们需要的是高吞吐量,它的并发量虽然和KV数据库差不多,但是在响应时间上,要求就没有那么严格了,1-2 秒甚至再多几秒的延时都是可以接受的。而且,和KV数据库不太一样,数据管道的数据读写都是顺序读写,没有大量的随机读写的需求
  • 对于数据仓库来说:
    • 数据仓库的数据读取的量要比管道大很多。管道的数据读取就是我们当时写入的数据,一天有10TB的日志数据,管道只会写入10TB。下游的数据仓库存放数据和实时数据模块读取的数据,再加上个 2 倍的 10TB,也就是 20TB 也就够了。
    • 但是,数据仓库的数据分析任务要读取的数据量就大多了。一方面,我们可能要分析一周、一个月乃至一个季度的数据。这一次分析要读取的数据可不是 10TB,而是 100TB 乃至1PB。我们一天在数据仓库上跑的分析任务也不是 1 个,而是成千上万个,所以数据的读
      取量是巨大的。另一方面,我们存储在数据仓库里面的数据,也不像数据管道一样,存放几个小时、最多一天的数据,而是往往要存上 3 个月甚至是 1 年的数据。所以,我们需要的是 1PB 乃至 5PB 这样的存储空间。

在这里插入图片描述
那我们该选择什么样的解决方案呢?

  • 对于KV数据库,最佳的选择方式自然是使用SSD硬盘,选择择 AeroSpike 这样的 KV数据库。高并发的随机访问并不适合HDD的机械硬盘,而400TB的数据,如果没有内存的话,成本又会显得太高
  • 对于数据管道,最佳选择当前是Kafka。因为我们追求的是吞吐率,采用了Zero-Copy和DMA机制的Kafka最大化了作为数据管道的吞吐率。而且,数据管道的读写都是顺序读写,所以我们也就不需要对随机读写提供支持,用上HDD硬盘就好了
  • 到了数据仓库,存储的数据流更大了。在硬盘层面使用HDD硬盘成了一个必选项。否则,我们的存储成本就会差上10倍。这么大量的数据,在存储上我们需要定义清除Schema,使得每个字段都不需要额外存储元数据,能够通过 Avro/Thrift/ProtoBuffer这样的二进制序列化的方式存储下来,或者干脆直接使用Hive这样明确了字段定义的数据仓库产品。

也就是说,DMA系统可以选择 AeroSpike作为 KV 数据库,Kafka 作为数据管道,Hadoop/Hive 来作为数据仓库。

问题是:为什么MongoDB、以及MySQL这样的文档数据库或者关系型数据库不适用呢?为什么不能通过优化SQL、添加缓存这样的调优手段,解决这个问题呢?

关系型数据库:不得不做的随机读写

我们先来想一想,如果现在让你自己写一个最简单的关系型数据库,你的数据要怎么存放在硬盘上?

最直观的一个想法是:

  • 用一个CSV文件格式。一个文件就是一个数据表。文件里面的每一行就是这个表里面的一条记录。如果要修改数据库里面的某一条记录,那么我们要先找到这一行,然后直接去修改这一行的数据,读取数据也是一样的。

  • 要找到这一行数据,最笨的方法自然是一行一行读,也就是遍历整个CSV文件。不过这样的话,相当于随便读任何一条数据都要扫描全表,太浪费硬盘的吞吐量了。那怎么办呢?我们可以实时给CSV文件夹一个索引。比如,给数据的行号加一个索引,这个索引可以通过B+树来创建。

  • 索引里面没有一整行的数据,只有一个映射关系,这个映射关系可以让行号直接从硬盘的某个位置去读。所以,索引比起数据要小很多。我们可以把索引加载到内存里面,即使不在内存里面,要找数据的时候快速遍历一下整个索引,也不需要读取太多的数据

  • 加了索引之后,我们要读取特定的数据,就不用去扫描整个数据表文件了。直接从特定的硬盘位置,就可以读到想要的行。索引不仅可以索引行号,还可以索引某个字段。我们可以创建很多个不同的独立索引。写SQL的时候,where子句后面的查询条件可以用到这些索引。

  • 不过,这样的话,写入数据的时候就会麻烦一点。我们不仅要在数据表里面写入数据,对于所有的索引也都需要更新。这个时候,写入一条数据就要触发好几个随机写入的更新。
    在这里插入图片描述

  • 在这样一个数据模型下,查询操作很灵活。无论是根据哪个字段查询,只要有索引,我们就可以通过一次随机读,很多的读到对应的数据。但是,这个灵活性也带来了一个很大的问题,那就是无论干点什么,都有大量的随机读写请求。而随机读写请求,如果请求最终是要落在硬盘上,特别是HDD硬盘的话,我们就很难做到高并发了。毕竟HDD硬盘只有100左右的QPS。

  • 这个随时添加索引,可以根据任意字段进行查询,这样表现出的灵活性,又是我们的DMP系统里面不太需要的

    • DMP的KV数据库主要的应用场景,是根据主键的随机查询,不需要根据其他字段进行筛选查询。
    • 数据管道的需求,则只需要不断追加写入和顺序读取就好了。
    • 即使进行数据分析的数据仓库,通常也不是根据字段进行数据筛选,而是全量扫描数据进行分析汇总。
  • 后面的两个场景还好说,大不了我们让程序去扫描全表或者追加写入。但是,在 KV 数据库这个需求上,刚才这个最简单的关系型数据库的设计,就会面临大量的随机写入和随机读取的挑战。

所以,在实际的大型系统中,大家都会使用专门的分布式 KV 数据库,来足这个需求。那么下面,我们就一起来看一看,Facebook 开源的 Cassandra 的数据存储和读写是怎么做的,这些设计是怎么解决高并发的随机读写问题的。

Cassandra:顺序写和随机读

Cassandra的数据模型

作为一个分布式KV数据库,Cassandra的键一般被称为Row Key。其实就是一个16到36个字节的字符串。每一个Row Key对应的值其实是一个哈希表,里面可以用键值对,再存入很多你需要的数据。

Cassandra本身不想关系型数据库那样,有严格的Schema,在数据库创建的一开始就定义好了有哪些列(column)。但是,它设计了一个叫做列族(Column Family)的概念,我们需要把经常放在一起使用的字段。比如,DMP里面的人口属性,可以把它当做一个列族;用户的兴趣信息,是另外一个列族。这样,即保持了不需要严格的Schema这样的灵活性,也保留了可以把常常一起使用的数据存放在一起的空间局部性。

往 Cassandra 的里面读写数据,其实特别简单,就好像是在一个巨大的分布式的哈希表里面写数据。我们指定一个 Row Key,然后插入或者更新这个 Row Key 的数据就好了。

Cassandra的写操作

Cassandra解决随机写入数据的解决方案,简单来说,就是“不随机写,只顺序写”。对于Cassandra数据库的写操作,通常包含两个动作:

  • 第一个,是往磁盘写入一条提交日志(commit log)
  • 另一个,是直接在内存的数据结构上去更新数据。

第二个往内存的数据结构里面的数据更新,只有在提交日志写成功之后才会进行。每台机器上,都有一个可靠的硬盘可以让我们去写入提交日志。写入提交日志都是顺序写,而不是随机写。这使得我们最大化了写入的吞吐量
在这里插入图片描述
内存的空间比较有限,一旦内存里面的数据量或者条目超过一定的限额,Cassandra就会把内存里面的数据结构dump到硬盘上。这个dump的操作,也是顺序写而不是随机写,所以性能也不会是一个问题。除了Cassandra的数据结构问题,Cassandra还会根据row key来生成一个索引文件,方便后继基于索引来进行快速查询。

随着硬盘上Dump出来的文件越来越多,Cassandra会在后台进行文件的对比合并。合并动作同样是顺序读取多个文件,在内存里面合并完成,在Dump出来一个新的文件。整个操作过程中,在硬盘层面仍然是顺序读写

Cassandra的读操作

  • 当我们要从Cassandra读数据的时候,会从内存里面找数据,再从硬盘读数据,然后把两部分的数据合并成最终结果。这些硬盘上的文件,在内存里面会有对应的Cache,只有在Cache里面找不到,我们才会去请求硬盘里面的数据
  • 如果不得不访问硬盘,因为硬盘里面可能Dump了很多个不同时间点的内存数据快照。所以,找数据的时候,我们也是按照时间从新往旧里找

这也就带来了另外一个问题,我们可能要查询很多个Dump文件,才能找到我们想要的数据。所以,Cassandra又做了一个优化。那就是,它会为每一个Dump的文件里面所有Row Key生成一个BloomFilter,然后把这个BloomFilter放在内存里面。这样,如果想要查询的Row Key不存在,那么99%以上的情况下,它会被BloomFilter过滤掉,而不需要访问硬盘。

这样,只有当数据的内存里面没有,并且在硬盘上某个特定文件上时,才会触发一次对硬盘的读请求

在这里插入图片描述

SSD

Cassandra是Facebook在2008年开源的。那个时候,SSD硬盘还没有那么普及。可以看到,它的读写设计充分考虑了硬件本身的特性。在这些数据进行持久化上,Cassandra没有任何的随机写请求,无论是commit log还是dump,全部都是顺序写。

在数据读的请求上,最新写入的数据都会更新到内存。如果要读取这些数据,会优先从内存读到。这相当于是一个使用了LRU的缓存机制。只有在万般无奈的情况下,才会有对于硬盘的随机读请求。即使在这样的情况下,Cassandra也在文件之前加了一层BloomFilter,把本来因为Dump文件带来的需要多次读硬盘的问题,简化成了多次内存读和一次硬盘读。

这些实际,使得Cassandra即使是在HDD硬盘上,也能有不错的访问性能。因为所有的写入都是循序写或者写入到内存。所以,写入可以做到高并发。HDD硬盘的吞吐率还是很不错的,每秒可以写入100MB 以上的数据,如果一条数据只有 1KB,那么 10 万的WPS(Writes per seconds)也是能够做到的。这足够支撑我们 DMP 期望的写入压力了。

对于数据的读,就有一些挑战了。如果数据读请求具有很强的局部性,那我们的内存就能搞定DMP需要的访问量。

但是,问题就出在这个局部性上。DMP的数据访问分布,其实是缺少局部性的。因为DMP里面的Row Key但是用户的唯一标识符,普通用户的上网时长怎么会有局部性呢?每个人上网的时间和访问页面的次数就那么多。上网多的人,一天最多也就24小时。大部分用户一天也要上网2~3小时。我们没办法说,把这些用户的数据放在内存里面,那些用户不放。

在这里插入图片描述
DMP 系统,是局部性不强的系统

因为缺少了时间局部性,我们内存的缓存能够起到的数据就很小了,大部分请求最终还是要落在HDD硬盘的随机读上。但是HTTP硬盘的随机读的性能太差了,也就100QPS左右。如果全部放在内存,那就太贵了,成本在HDD硬盘100倍以上,

不过,幸运的是,从 2010 年开始,SSD 硬盘的大规模商用帮助我们解决了这个问题。而Cassandra 的写入机制完美匹配了 SSD 硬盘的优缺点:

  • 在数据写入层面,Cassandra 的数据写入都是Commit Log的顺序写入,也就是不断的在硬盘后追加内容,而不是去修改现有的文件内容。一旦内存里面的数据超过一定的阈值,Cassandra 又会完整的Dump一个新文件到文件系统上。这同样是一个追加写入
  • 数据的对比和紧凑化:同样是读取现有的多个文件,然后写一个新的文件出来。写入操作只追加不修改的特性,正好天然的符合SSD硬盘只能按块进行擦除写入的操作。在这样的写入模式下,Cassandra 用到的SSD硬盘,不需要频繁的进行后台的Compaction,能够最大化 SSD 硬盘的使用寿命。这也是为什么,Cassandra 在 SSD 硬盘普及之后,能够获得进一步快速发展。

总结

  • 传统的关系型数据库,我们把一条条数据存放在一个地方,同时把索引存放在另一个地方。这样的存储方式,其实很方便我们进行单次的随机读和随机写,数据的存储也可以很紧凑。但是问题也在于此,大部分的SQL请求,都会带来大量的随机读写的请求。这使得传统的关系型数据库,其实并不适合用在真正的高并发场景下
  • 我们的DMP需要的访问场景,其实没有复杂的索引需求,但是会有比较高的并发性。Cassandra 这个分布式 KV 数据库通过在追加写入 Commit Log 和更新内存,Cassandra 避开了随机写的问题。内存数据的 Dump 和后台的对比合并,同样也都避开了随机写的问题,使得 Cassandra 的并发写入性能极高。
  • 在数据读取层面,通过内存缓存和BloomFilter,Cassandra 已经尽可能的减少了需要随机读取硬盘里面数据的情况,不过挑战在于,DMP系统的局部性不强,使得我们最终的随机读的请求还是要到硬盘上。幸运的是,SSD硬盘在数据海量增长的那几年里价格不断下降,使得我们通过SSD硬盘解决了这个问题
  • 而SSD硬盘本身的擦除后才能写入的机制,正好非常适合 Cassandra 的数据读写模式,最终使得 Cassandra 在 SSD 硬盘普及之后得到了更大的发展。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值