You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

126 lines
16 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 53 | 设计大型DMP系统SSD拯救了所有的DBA
上一讲里根据DMP系统的各个应用场景我们从抽象的原理层面选择了AeroSpike作为KV数据库Kafka作为数据管道Hadoop/Hive来作为数据仓库。
不过呢肯定有不信邪的工程师会问为什么MongoDB甚至是MySQL这样的文档数据库或者传统的关系型数据库不适用呢为什么不能通过优化SQL、添加缓存这样的调优手段解决这个问题呢
今天DMP的下半场我们就从数据库实现的原理一起来看一看这背后的原因。如果你能弄明白今天的这些更深入、更细节的原理对于什么场景使用什么数据库就会更加胸有成竹而不是只有跑了大量的性能测试才知道。下次做数据库选型的时候你就可以“以理服人”了。
## 关系型数据库:不得不做的随机读写
我们先来想一想,如果现在让你自己写一个最简单的关系型数据库,你的数据要怎么存放在硬盘上?
最简单最直观的想法是用一个CSV文件格式。一个文件就是一个数据表。文件里面的每一行就是这个表里面的一条记录。如果要修改数据库里面的某一条记录那么我们要先找到这一行然后直接去修改这一行的数据。读取数据也是一样的。
要找到这样数据最笨的办法自然是一行一行读也就是遍历整个CSV文件。不过这样的话相当于随便读取任何一条数据都要扫描全表太浪费硬盘的吞吐量了。那怎么办呢我们可以试试给这个CSV文件加一个索引。比如给数据的行号加一个索引。如果你学过数据库原理或者算法和数据结构那你应该知道通过B+树多半是可以来建立这样一个索引的。
索引里面没有一整行的数据,只有一个映射关系,这个映射关系可以让行号直接从硬盘的某个位置去读。所以,索引比起数据小很多。我们可以把索引加载到内存里面。即使不在内存里面,要找数据的时候快速遍历一下整个索引,也不需要读太多的数据。
加了索引之后我们要读取特定的数据就不用去扫描整个数据表文件了。直接从特定的硬盘位置就可以读到想要的行。索引不仅可以索引行号还可以索引某个字段。我们可以创建很多个不同的独立的索引。写SQL的时候where子句后面的查询条件可以用到这些索引。
不过,这样的话,写入数据的时候就会麻烦一些。我们不仅要在数据表里面写入数据,对于所有的索引也都需要进行更新。这个时候,写入一条数据就要触发好几个随机写入的更新。
![](https://static001.geekbang.org/resource/image/f3/3c/f3c01bc2de99dbb83ad17cef1fb38a3c.jpeg)
在这样一个数据模型下查询操作很灵活。无论是根据哪个字段查询只要有索引我们就可以通过一次随机读很快地读到对应的数据。但是这个灵活性也带来了一个很大的问题那就是无论干点什么都有大量的随机读写请求。而随机读写请求如果请求最终是要落到硬盘上特别是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的写操作
![](https://static001.geekbang.org/resource/image/02/58/02d58b12403f7907975e00549a008c58.jpeg)
Cassandra只有顺序写入没有随机写入
Cassandra解决随机写入数据的解决方案简单来说就叫作“不随机写只顺序写”。对于Cassandra数据库的写操作通常包含两个动作。第一个是往磁盘上写入一条提交日志Commit Log。另一个操作则是直接在内存的数据结构上去更新数据。后面这个往内存的数据结构里面的数据更新只有在提交日志写成功之后才会进行。每台机器上都有一个可靠的硬盘可以让我们去写入提交日志。写入提交日志都是顺序写Sequential Write而不是随机写Random Write这使得我们最大化了写入的吞吐量。
如果你不明白这是为什么,可以回到[第47讲](https://time.geekbang.org/column/article/118191)看看硬盘的性能评测。无论是HDD硬盘还是SSD硬盘顺序写入都比随机写入要快得多。
内存的空间比较有限一旦内存里面的数据量或者条目超过一定的限额Cassandra就会把内存里面的数据结构dump到硬盘上。这个Dump的操作也是顺序写而不是随机写所以性能也不会是一个问题。除了Dump的数据结构文件Cassandra还会根据row key来生成一个索引文件方便后续基于索引来进行快速查询。
随着硬盘上的Dump出来的文件越来越多Cassandra会在后台进行文件的对比合并。在很多别的KV数据库系统里面也有类似这种的合并动作比如AeroSpike或者Google的BigTable。这些操作我们一般称之为Compaction。合并动作同样是顺序读取多个文件在内存里面合并完成再Dump出来一个新的文件。整个操作过程中在硬盘层面仍然是顺序读写。
### Cassandra的读操作
![](https://static001.geekbang.org/resource/image/68/b0/68855c2861f07417bbc2eb64672d36b0.jpeg)
Cassandra的读请求会通过缓存、BloomFilter进行两道过滤尽可能避免数据请求命中硬盘
当我们要从Cassandra读数据的时候会从内存里面找数据再从硬盘读数据然后把两部分的数据合并成最终结果。这些硬盘上的文件在内存里面会有对应的Cache只有在Cache里面找不到我们才会去请求硬盘里面的数据。
如果不得不访问硬盘因为硬盘里面可能Dump了很多个不同时间点的内存数据的快照。所以找数据的时候我们也是按照时间从新的往旧的里面找。
这也就带来另外一个问题我们可能要查询很多个Dump文件才能找到我们想要的数据。所以Cassandra在这一点上又做了一个优化。那就是它会为每一个Dump的文件里面所有Row Key生成一个BloomFilter然后把这个BloomFilter放在内存里面。这样如果想要查询的Row Key在数据文件里面不存在那么99%以上的情况下它会被BloomFilter过滤掉而不需要访问硬盘。
这样,只有当数据在内存里面没有,并且在硬盘的某个特定文件上的时候,才会触发一次对于硬盘的读请求。
## SSDDBA们的大救星
Cassandra是Facebook在2008年开源的。那个时候SSD硬盘还没有那么普及。可以看到它的读写设计充分考虑了硬件本身的特性。在写入数据进行持久化上Cassandra没有任何的随机写请求无论是Commit Log还是Dump全部都是顺序写。
在数据读的请求上最新写入的数据都会更新到内存。如果要读取这些数据会优先从内存读到。这相当于是一个使用了LRU的缓存机制。只有在万般无奈的情况下才会有对于硬盘的随机读请求。即使在这样的情况下Cassandra也在文件之前加了一层BloomFilter把本来因为Dump文件带来的需要多次读硬盘的问题简化成多次内存读和一次硬盘读。
这些设计使得Cassandra即使是在HDD硬盘上也能有不错的访问性能。因为所有的写入都是顺序写或者写入到内存所以写入可以做到高并发。HDD硬盘的吞吐率还是很不错的每秒可以写入100MB以上的数据如果一条数据只有1KB那么10万的WPSWrites per seconds也是能够做到的。这足够支撑我们DMP期望的写入压力了。
而对于数据的读就有一些挑战了。如果数据读请求有很强的局部性那我们的内存就能搞定DMP需要的访问量。
但是问题就出在这个局部性上。DMP的数据访问分布其实是缺少局部性的。你仔细想一想DMP的应用场景就明白了。DMP里面的Row Key都是用户的唯一标识符。普通用户的上网时长怎么会有局部性呢每个人上网的时间和访问网页的次数就那么多。上网多的人一天最多也就24小时。大部分用户一天也要上网23小时。我们没办法说把这些用户的数据放在内存里面那些用户不放。
![](https://static001.geekbang.org/resource/image/cf/ca/cf55146f8cf79029af6d1f86f3de86ca.jpeg)
DMP系统只有根据国家和时区不同有比较明显的局部性是局部性不强的系统
那么我们可不可能有一定的时间局部性呢如果是Facebook那样的全球社交网络那可能还有一定的时间局部性。毕竟不同国家的人的时区不一样。我们可以说在印度人民的白天把印度人民的数据加载到内存里面美国人民的数据就放在硬盘上。到了印度人民的晚上再把美国人民的数据换到内存里面来。
如果你的主要业务是在国内,那这个时间局部性就没有了。大家的上网高峰时段,都是在早上上班路上、中午休息的时候以及晚上下班之后的时间,没有什么区分度。
面临这个情况如果你们的CEO或者CTO问你是不是可以通过优化程序来解决这个问题如果你没有仔细从数据分布和原理的层面思考这个问题而直接一口答应下来那你可能之后要头疼了因为这个问题很有可能是搞不定的。
因为缺少了时间局部性我们内存的缓存能够起到的作用就很小了大部分请求最终还是要落到HDD硬盘的随机读上。但是HDD硬盘的随机读的性能太差了我们在[第45讲](https://time.geekbang.org/column/article/116104)看过也就是100QPS左右。而如果全都放内存那就太贵了成本在HDD硬盘100倍以上。
不过幸运的是从2010年开始SSD硬盘的大规模商用帮助我们解决了这个问题。它的价格在HDD硬盘的10倍但是随机读的访问能力在HDD硬盘的百倍以上。也就是说用上了SSD硬盘我们可以用1/10的成本获得和内存同样的QPS。同样的价格的SSD硬盘容量则是内存的几十倍也能够满足我们的需求用较低的成本存下整个互联网用户信息。
不夸张地说过去十年的“大数据”“高并发”“千人千面”有一半的功劳应该归在让SSD容量不断上升、价格不断下降的硬盘产业上。
回到我们看到的Cassandra的读写设计你会发现Cassandra的写入机制完美匹配了我们在第46和47讲所说的SSD硬盘的优缺点。
在数据写入层面Cassandra的数据写入都是Commit Log的顺序写入也就是不断地在硬盘上往后追加内容而不是去修改现有的文件内容。一旦内存里面的数据超过一定的阈值Cassandra又会完整地Dump一个新文件到文件系统上。这同样是一个追加写入。
数据的对比和紧凑化Compaction同样是读取现有的多个文件然后写一个新的文件出来。写入操作只追加不修改的特性正好天然地符合SSD硬盘只能按块进行擦除写入的操作。在这样的写入模式下Cassandra用到的SSD硬盘不需要频繁地进行后台的Compaction能够最大化SSD硬盘的使用寿命。这也是为什么Cassandra在SSD硬盘普及之后能够获得进一步快速发展。
## 总结延伸
好了关于DMP和存储器的内容讲到这里就差不多了。希望今天的这一讲能够让你从Cassandra的数据库实现的细节层面彻底理解怎么运用好存储器的性能特性和原理。
传统的关系型数据库我们把一条条数据存放在一个地方同时再把索引存放在另外一个地方。这样的存储方式其实很方便我们进行单次的随机读和随机写数据的存储也可以很紧凑。但是问题也在于此大部分的SQL请求都会带来大量的随机读写的请求。这使得传统的关系型数据库其实并不适合用在真的高并发的场景下。
我们的DMP需要的访问场景其实没有复杂的索引需求但是会有比较高的并发性。我带你一看了Facebook开源的Cassandra这个分布式KV数据库的读写设计。通过在追加写入Commit Log和更新内存Cassandra避开了随机写的问题。内存数据的Dump和后台的对比合并同样也都避开了随机写的问题使得Cassandra的并发写入性能极高。
在数据读取层面通过内存缓存和BloomFilterCassandra已经尽可能地减少了需要随机读取硬盘里面数据的情况。不过挑战在于DMP系统的局部性不强使得我们最终的随机读的请求还是要到硬盘上。幸运的是SSD硬盘在数据海量增长的那几年里价格不断下降使得我们最终通过SSD硬盘解决了这个问题。
而SSD硬盘本身的擦除后才能写入的机制正好非常适合Cassandra的数据读写模式最终使得Cassandra在SSD硬盘普及之后得到了更大的发展。
## 推荐阅读
今天的推荐阅读,是一篇相关的论文。我推荐你去读一读[Cassandra - A Decentralized Structured Storage System](https://www.cs.cornell.edu/projects/ladis2009/papers/lakshman-ladis2009.pdf)。读完这篇论文一方面你会对分布式KV数据库的设计原则有所了解了解怎么去做好数据分片、故障转移、数据复制这些机制另一方面你可以看到基于内存和硬盘的不同存储设备的特性Cassandra是怎么有针对性地设计数据读写和持久化的方式的。
## 课后思考
除了MySQL这样的关系型数据库还有Cassandra这样的分布式KV数据库。实际上在海量数据分析的过程中还有一种常见的数据库叫作列式存储的OLAP的数据库比如[Clickhouse](https://clickhouse.yandex/)。你可以研究一下Clickhouse这样的数据库里面的数据是怎么存储在硬盘上的。
欢迎把你研究的结果写在留言区,和大家一起分享、交流。如果觉得有帮助,你也可以把这篇文章分享给你的朋友,和他一起讨论、学习。