「许多应用程序都是数据密集型的,而非计算密集型的。因此 CPU 很少成为这类应用的瓶颈,更大的问题通常来自于数据量、数据复杂性、以及数据的变更速度。」
数据密集型应用通常由标准组件构成,包括但是不限于:
越来越多的应用程序有着各种严格而广泛的需求,单个组件不足以满足所有数据处理和存储的需求。整个需求被拆分为一系列能被单个组件高效完成的任务,并通过应用代码将它们缝合起来。
如下图所示为一个组合使用多个组件的数据系统架构:
注:当你将多个工具组合在一起提供服务时,服务的接口或应用程序编程接口(API),通常向客户端隐藏这些底层实现的细节。
设计数据系统或服务时可能会遇到很多棘手的问题,例如:
在某些情况下,企业可能会选择牺牲可靠性来降低开发成本或运营成本,但是在做这些的同时,希望企业能够清楚的意识到其影响的范围及其是否可控。
注:一项关于大型互联网服务的研究发现,运维配置错误是导致服务中断的首要原因,而硬件故障(服务器或网络)仅导致了 10% - 25% 的服务中断
可伸缩性用来描述系统应对负载增长能力的术语。讨论可伸缩性意味着考虑诸如「如果系统以特定方式增长,有什么选项可以应对增长?」和「如何增加计算资源来处理额外的负载?」
描述负载
负载可以用负载参数的数字来描述,参数的最佳选择取决于架构:
描述性能
系统的负载被描述好以后,研究负载增加会发生什么?
注:如何描述系统性能,比如批处理系统,通常关系的是吞吐量(每秒可以处理的记录量)比如 hadoop,对于在线系统,通常更重要的是服务的响应时间,即客户端发送到接收请求之间的时间。
软件的大部分开销并不在最初的开发阶段,而是在持续的维护阶段,包括
在设计之初就尽量考虑尽可能减少维护期间的痛苦,为此,我们将特别关注软件系统的三个设计原则:
多数应用使用层层叠加的数据模型构建。对于每层数据模型的关键问题是:它如何用更低一层数据模型来表示的?
注:一个复杂的应用程序可能会有更多的中间层,每个层都通过提供一个明确的数据模型来隐藏更低层次中的复杂性。这些抽象允许不同的人群有效地协作。
最著名的数据模型是 SQL ,数据被组织成关系(SQL 中称作表),其中每个关系是元组(SQL 中称为行)的无序集合。
NoSQL 被追溯性地重新解释为不仅是 SQL(Not Only SQL)
采用 NoSQL 数据库的背后有以下几个驱动因素:
如果数据存储在关系表中,那么需要一个笨拙的转换层,处于应用程序代码中的对象和表,行,列的数据库模型之间。像 Hibernate 这样的对象关系映射(ORM)框架可以减少这个转换层所需的样板代码数据量,但是它不能完全隐藏这两个模型之间的差异。
注:文档模型比关系模型具有更好的局部性。因为关联信息存储在同一条记录中,一个查询就足够了。
使用 ID 的好处是,ID 对人类没有任何意义,因而永远不需要改变; ID 可以保持不变,即使它标识的信息发生变化。
在关系数据库中,通过 ID 来引入其他表中的行是正常的,因为连接很容易。在文档数据库中,一对多树结构没有必要用连接,对连接的支持通常很弱。
注:任何对人类有意义的东西都可能需要在将来的某个时候改变——如果这些信息被复制,所有的冗余副本也需要更新。这会导致写入开销,也存在不一致风险。
在多对多的关系和连接已经常规用在关系数据库时,文档数据库和 NoSQL 重启了辩论:如何更好地在数据库中表示多对多关系?
无论是关系模型还是文档模型都没有重走「网络模型」来标识多对多关系的老路,所以并没有重蹈覆辙。(ps 网络模型很像字典树,详细请参考第二章
注:在表示多对一和多对多的关系时,关系数据库和文档数据库并没有根本的不同:在这两种情况下,相关项目都被一哥唯一的标识符引用,这个标识符在关系模型中称为外键,在文档模型中称为文档引用
支持文档数据模型的主要论据是架构灵活性,因局部性而拥有更好的性能,以及对于某些应用程序而言更接近于应用程序使用的数据结构。关系模型通过为连接提供更好的支持以及支持多对一和多对多的关系来反击。
哪种数据模型更有助于简化应用代码?
没有办法说那种数据模型更有助于简化应用代码,因为它取决于数据项之间的关系种类。对高度关联的数据而言,文档模型是极其糟糕的,关系模型是可以接受的,而选用图形模型是最自然的
文档模型中的架构灵活性?
读时模式:文档数据库在读取数据时通常假定某种结构——即隐式模式,但不由数据库强制执行。
写时模式:关系数据库模式明确,确保所有数据多符合其模式。
注:读时模式类似于编程语言中的动态类型检查,而写时模式类似于静态类型检查。
查询的数据局限性?
局部性仅仅适用于同时需要文档绝大部分内容的情况。数据库通常需要加载整个文档,即使只访问其中的一小部分,这对于大型文档来说是浪费的。更新文档时,通常需要重写,只有不改变文档大小的修改才可以容易地原地执行。因此,通常建议保持相对小的文档,并避免增加文档大小的写入。
文档和关系数据库的融合进?
随着时间的推移,关系数据库和文档数据库变得越来越相似,这是一件好事:数据模型互相补充,如果一个数据库能够处理类似文档的数据,并能够对其执行关系查询,那么应用程序就可以使用最符合其需要的功能组合。
在声明式查询语言(SQL或关系代数)中,只需要指定所需数据的模式 — 结果必须符合哪些条件,以及如何将数据转换(例如,排序,分组和集合)— 但不是如何实现这个目标。数据库系统的查询优化器决定使用哪些索引和哪些连接方法,以及何时顺序执行查询的各个部分。
Web 上的声明式查询
在 Web 浏览器中,使用声明式 CSS 样式比使用 JavaScript 命令式地操作样式要好得多。类似地,在数据库中,使用像 SQL 这样的声明式语言比使用命令式查询 API 要好得多。
MapReduce 查询
MapReduce 即不是一个声明式的查询语言,也不是一个完全命令式的查询 API ,而是处于两者之间,查询的逻辑用代码片段来表示,这些代码片段会被处理框架重复性调用。它基于 map 和 reduce 函数,两个函数存在于许多函数式编程语言中。
关系模型可以处理多对多关系的简单情况,但是随着数据之间的连接变得复杂,将数据建模为图形显得更加适合。典型的例子:
注:汽车导航系统搜索道路网络中两点之间的最短路径,PageRank 可以用在网络图上来确定网页的流行程度,从而确定该网页在搜索结果中的排名。
一个数据库系统在最基础的层次上需要完成两件事:
作为程序员,为什么要关系数据库内部存储和检索的机制?
你可能不会从头开始实现自己的存储引擎,但是确实要你从许多可用的存储引擎中选择一个合适的。而为了协调存储引擎以适配应用工作负载,也需要你大致了解存储引擎底层究竟在做什么?
注:本章研究两大类存储引擎,日志结构的存储引擎,以及面向页面的存储引擎(B 树)
世界上最简单的数据库可以用两个 bash 函数来实现:
#!/bin/bash
db_set () {
echo "$1,$2" >> database
}
db_get () {
grep "^$1," database | sed -e "s/^$1,//" | tail -n 1
}
db_set:执行 db_set key value,会将键(key)和值(value)存储在数据库,每次对 db_set 的调用都会向文件末尾追加记录,所以更新键的时候旧版本的值不会被覆盖——因而查找最新值的时候,需要找到文件中键最后一次出现的位置。(ps:注意 db_get 中使用了 tail -n 1
注:与 db_set 做的事情类似,许多数据库在内部使用了日志(log),也就是一个仅追加(append-only)的数据文件。真正的数据库有更多的问题需要处理,比如并发控制、回收磁盘空间以及避免日志无限增长、处理错误与部分写入的记录
db_get:必须从头到尾扫描整个数据库文件来查找键的出现,用算法语言来说,查找的开销是 O(n),
注:为了高效查找数据库中的特定键的值,需要一个数据结构——索引。索引是从主数据衍生的附加结构。许多数据库允许添加与删除索引,这不会影响数据的内容,它只影响查询的性能。维护额外的结构会产生开销,特别是在写入时。任何类型的索引通常都会减慢写入速度,因为每次写入数据时都需要更新索引
键值索引不是唯一的索引类型,但键值数据是很常见的。对于更复杂的索引来说,这是一个有用的构建模块。
注:键值存储与大多数编程语言中提供的字典(dictionary)类型非常相似,通常字典都是用散列映射实现的。
哈希索引需要考虑的问题:
注:哈希表索引存在以下局限性
- 散列表必须能放进内存
- 磁盘哈希映射很难表现优秀,它需要大量的随机访问 I/O ,当它变满时增长是很昂贵的,并且散列冲突需要很多的逻辑。
哈希索引中,每个日志结构的存储段都是一系列键值对。这些对按照它们的写入顺序出现,日志中稍后的值优先于日志中较早的相同键的值。除此之外,文件中键值对的顺序并不重要。
可以使用排序字符串表(Sorted String Table)对段文件的格式做简单的变化,要求键值对的序列按键排序,同时保证每个键只在每个合并的段文件中出现一次,使用排序字符串表的优势如下:
构建和维护 SSTables
如何让数据按照键排序?大致的工具流程如下:
用 SSTable 制作 LSM 树
LSM 树(日志结构合并树),基于合并和压缩排序文件原理的存储引擎通常被称为 LSM 存储引擎。在 LevelDB 和 RocksDB 中均有实现。
LSM 树的基本思想——保存一系列在后台合并的 SSTables —— 简单而高效。即使数据集比可用内存大得多,它仍能继续正常工作。由于数据按排序顺序存储,因此可以高效地执行范围查询(扫描所有高于某些最小值和最高值的的所有键),并且因为磁盘写入是连续的,所以 LSM 树可以支持非常高的写入吞吐量。
性能优化
当查找数据库中不存在的键时,LSM 树算法可能会很慢:必须先检查内存表,然后将这些段一直回到最老的(可能必须从磁盘上读取每一个)然后才能确定键不存在。
注:为了优化这种访问,存储引擎通常使用额外的 Bloom 过滤器。
像 SSTables 一样,B树保持按键排序的键值对,这允许高效的键值查询和范围查询。 前面的日志结构索引将数据库分解为可变大小的段,通常是几兆字节或更大的大小,并且总是按顺序编写段。相比之下,B 树将数据库分解成固定大小的块或页面,传统上大小为 4KB(或更大),并且一次只能读取或写入一个页面。
注:在 B 树的一个页面中对子页面的引用的数量成为分支因子,例如,上图的分支因子为 6,在实践中,分支因子取决于存储页面参考和范围边界所需要的空间量,通常是几百个。
让 B树更可靠
B 树的基本底层写操作是用新的数据覆盖磁盘上的页面。假定覆盖不改变页面的位置:即,当页面被覆盖时,对该页面的所有引用保持完整。这与日志结构索引(LSM 树)形成鲜明对比,后者只附加到文件(并最终删除过时的文件)但从不修改文件。
为了使数据库对崩溃具有韧性,B树实现通常会带有一个额外的磁盘数据结构:预写式日志(WAL)(也称作重做日志(redo log)。这是一个仅追加的文件,每个 B 树修改都可以应用到树本身的页面上。当数据库在崩溃恢复时,这个日志被用来使 B树恢复到一致的状态。
B树优化
通常 LSM树的写入速度更快,而 B树的读取速度更快。LSM树上读取通常比较慢,因为它们必须在压缩的不同阶段检查几个不同的数据结构和 SSTables。
LSM树的优点
LSM树通常比 B 树支持更高的写入吞吐量,部分原因是因为它们具有较低的写放大,部分是因为它们顺序地写入紧凑的 SSTable 文件而不是必须覆盖树中的几个页面。这种差异在磁盘硬盘驱动器上尤其重要,顺序写入比随机写入快得多。
LSM树可以被压缩得更好,因此经常比 B树在磁盘上产生更小的文件。B 树存储引擎会由于分割而留下一些未被使用的磁盘空间:当页面被拆分或某行不能放入现有页面时,页面中的某些空间仍未被使用。由于 LSM树不是面向页面的,并且定期重写 SSTables 以去除碎片,所以它们具有较低的存储开销,特别是当使用平坦压缩时。
注:在数据库的生命周期中写入数据库导致对磁盘的多次写入——被称为写放大。
LSM树的缺点
日志结构的存储的缺点是压缩过程有时会干扰正在进行的读写操作。尽管存储引擎尝试逐步执行压缩而不影响并发访问,但是磁盘资源有限,所以很容易发生因为压缩操作请求等待磁盘。对吞吐量和平均响应时间影响通常很小,但是在更高百分比的情况下,对日志结构化存储引擎的查询响应时间有时会相当长,而 B 树的行为则相对于更可测。
如果写入吞吐量很高,并且压缩没有仔细配置,压缩跟不上写入速率。这种情况下,磁盘上未合并段的数量不断增加,直到磁盘空间用完,读取速度也会减慢,因为它们需要检查更多段文件。
将值存储在索引中
索引中的关键字是查询索引的内容,但是该值可以有以下两种情况:
注:例如在 MySQL 的 InnoDB 存储引擎中,表的主键总是一个聚簇索引,二级索引引用主键
多列索引
多列索引又称为连接索引,它通过将一个列的值追加到另一个列的后面,简单地将多个字段组合成一个键。
多维索引
一种查询多个列的更一般的方法,这对于地理空间的数据尤为重要。例如,餐厅搜索网站可能有一个数据库,其中包含每个餐厅的经度和纬度。
注:一个标准的 B 树和 LSM 树索引不能高效地响应这种查询,一种选择是使用空间填充曲线将二维位置转换为单个数字,然后使用常规的 B树索引。更普遍的是,使用特殊化的空间索引,例如 R树。
全文搜索和模糊索引
在内存中存储一切
比较事务处理和分析的特点*
属性 | 事务处理 OLTP | 分析系统 OLAP |
---|---|---|
主要读取模式 | 查询少量记录,按键读取 | 在大批量记录上聚合 |
主要写入模式 | 随机访问,写入要求低延时 | 批量导入(ETL),事件流 |
主要用户 | 终端用户,通过Web应用 | 内部数据分析师,决策支持 |
处理的数据 | 数据的最新状态(当前时间点) | 随时间推移的历史事件 |
数据集尺寸 | GB ~ TB | TB ~ PB |
注:在二十世纪八十年代末和九十年代初期,公司有停止使用 OLTP 系统进行分析的趋势,而是在单独的数据库上进行分析。这个单独的数据库被称为数据仓库。
数据仓库
数据仓库包含公司各种 OLTP 系统中所有的只读数据副本。从 OLTP 数据库中抽取刷数据,转换成合适的分析模式,清理并加载到数据仓库中。将数据存入仓库的过程称为"抽取—转换—加载(ETL)"
OLTP数据库和数据仓库之间的分歧
表面上,一个数据仓库和一个关系 OLTP 数据库看起来很相似,因为它们都有一个 SQL 查询接口。然而,系统的内部看起来可能完全不同,因为他们针对非常不同的查询模式进行优化。现在许多数据库供应商都将重点放在支持事务处理货分析工作负载上,而不是两者都支持。
星型和雪花型:分析的模式
星型模型:当表关系可视化时,事实表在中间,由维表包围;与这些表的连接就像星星的光芒。
注:这个模式的变体称为雪花模式
在大多数 OLTP 数据库中,存储都是以面向行的方式进行布局的:表格的一行中的所有值都是相邻存储。文档数据库是相似的:整个文档通常存储为一个连续的字节序列。
面向列的存储背后的想法很简单:不是将所有来自一行的值存储在一起,而是将来自每一列的所有值存储在一起。如果每个列存储在一个单独的文件中,查询只需要读取和解析查询中使用的列,这可以节省大量的工作。
列压缩
除了仅从磁盘加载查询所需要的列以外,我们还可以通过压缩数据来进一步降低对磁盘吞吐量的需求。面向列的存储通常很适合压缩,在数据仓库中特别有效的一种技术是位图编码。
注:面向列的存储和列族
Cassandra 和 HBase 有一个列族的概念,他们从 Bigtable 继承。然而,把它们称为面向列是非常具有误导性的:在每个列族中,它们将一行中的所有列于行键一起存储,并且不使用列压缩。因此,Bigtable 模型仍然主要是面向行的。
内存带宽
对于需要扫描数百万行的数据仓库查询来说,一个巨大的瓶颈是从磁盘获取数据到内存的带宽。除了减少需要从磁盘加载的数据量以外,面向列的存储布局也可以有效利用 CPU 周期。
在列存储中,存储行的顺序并不一定很重要。按插入顺序存储它们是最简单的,因为插入一个新行就意味着附加到每个列文件。但是,我们可以选择强制执行一个命令,就像我们之前对 SSTables 所做的那样,并将其用作索引机制。
注:每列独自排序是没有意义的,也需一次对整行进行排序。数据库的管理员可以使用他们对常见查询的知识来选择表格应该被排序的列。
几个不同的排序顺序
在一个面向列的存储中有多个排序顺序有点类似于在一个面向行的存储中有多个二级索引。但最大的区别在于面向行的存储将每一行保存在一个地方(在堆文件或聚簇索引中),二级索引只包含指向匹配行的指针。在列存储中,通常在其他地方没有任何指向数据的指针,只有包含值的列。
以上的优化在数据仓库中是有意义的,因为大多数负载由分析人员运行的大型只读查询组成。面向列的储存,压缩和排序都有助于更快地读取这些查询。然而,他们有写更加困难的缺点。
数据仓库的另一个值得一提的是物化汇总。如前所述,数据仓库查询通常涉及一个聚合函数,如 SQL 中的 COUNT、SUM、AVG、MIN 或 MAX。如果相同的聚合被许多不同的查询使用,那么每次都可以通过原始数据来处理。为什么不缓存一些查询使用最频繁的计数或综合?
针对于上面创建这种缓存的方式是物化视图。在关系数据模型中,还有一种通常被定义为一个虚拟视图:一个类似于表的对象,其内容是一些查询的结果。
虚拟视图:只是写入查询的捷径。从虚拟视图读取时,SQL 引擎会将其展开到视图的底层查询中,然后处理展开的查询。
物化视图:当底层数据发生变化时,物化视图需要更新,因为它是数据的非规范化副本。数据库可以自动完成,但是这样的更新使得写入成本更高,这就是在 OLTP 数据库中不经常使用物化视图的原因。
在 事务处理(OLTP)中,我们能看到两派主流的存储引擎:
日志结构学派:
只允许附加到文件和删除过时的文件,但不会更新已经写入的文件。Bitcask,SSTables,LSM 树,LevelDB,Cassandra,HBase,Lucene 等都属于这个类别。
就地更新学派
将磁盘视为一组可以重写的固定大小的页面。B树是这种哲学的典范,用在所有主要的关系数据库和许多非关系数据库。
断断续续的摘抄了半个月,才写完了前三章,大概心情不好的时候不适合写技术文章吧
就这样吧,第四章的总结拖到下次吧,嗯,应该来的急。