ddia的阅读笔记(一)

写在前面的话

原书来源
https://legacy.gitbook.com/book/vonng/ddia-cn/details

偶然从旁人推荐中得知了这本书,正好最近也算是陷入了一定的瓶颈,于是看看这本书来窥视一下大神的境界

这篇阅读笔记主要内容是ddia的数据系统的一到四节的内容,从这四节看下来的体验就是ddia这本书目前所讲的内容更像是九阴真经一样的内功心法大全,提供了各种场景和心法的思路,用于开拓眼界,这也是我目前所需要的,由于之前在游戏行业和区块链行业都做了一段时间,学到的都算是奇门心法,现在也需要一些普适性的心法。

言归正传,先说总结

第一章提出了一些数据密集型应用的基本思考方式,可以理解成总纲

核心在于提出了应用在非功能需求上的思考

第二章从数据模型出发,展示了各种不同的数据模型,可以理解成最基础的招数的心法

核心在于如何选择一些抽象的数据模型,更直白的说法是,SQL、NOSQL OR GRAPH

第三章是在第二章基础上,详细的展示了如何对数据模型进行存储,可以理解成最基础招数的具体招数

核心在于介绍了数据库从设计理念上的不同,OLTP以及OLAP,更细分层面是OLTP中的两种存储引擎,日记结构以及就地更新。

第四章主要研究了数据结构变为网络或者硬盘上字节的几种方法,另外就是这些方法对于演化性的影响,另外讨论了数据流的一些问题。

核心在于介绍了三种不同的编码模式:语言内置,文本格式以及二进制格式。另外介绍了数据流的三种模式:数据库,REST和RPC,异步消息机制。

可靠性,可扩展性,可维护性

数据密集型任务的标准组件

  • 数据库
  • 缓存
  • 搜索索引
  • 流处理
  • 批处理

一个常见的的数据系统架构

数据系统架构

上图可以看到应用程序的整体流程

  1. 请求缓存
  2. 如果缓存不命中请求db,搜索请求会用到全局索引
  3. 请求数据后会去执行应用程序代码进行改变,更新索引,更新缓存
  4. 应用程序的异步任务会到消息队列
  5. 消费消息队列的消息后的应用程序会触发另一些行为,影响到外部世界

接下来进入主旨,如何保证这个系统的可靠性,可扩展性和可维护性

可靠性

  • 应用程序表现出用户所期望的功能
  • 允许用户犯错,允许用户以出乎意料的方式使用软件
  • 在于其的负载和数据量下,性能满足要求
  • 系统能房子未经授权的访问和滥用

粗略的理解

即使出现问题,也能继续正常工作

硬件故障

硬件冗余

磁盘可以组建RAID,服务器可以有双路电源和热插拔CPU,数据中心可能有电池和柴油发电机作为后备电源,某个组件挂掉时冗余组件可以立刻接管

软件错误

难以预估的系统性错误,这种错误跨节点相关 例如

  • 接受特定的错误输入导致所有实例崩溃:2012年6月30日的闰秒
  • 失控进程占用共享资源
  • 系统依赖的服务变慢,没有响应或者开始返回错误的响应
  • 级联故障,小组件的小故障触发另一个组件的故障,进而触发更多故障

这种错误没有速效药,有一些小办法来解决

  • 考虑系统中的假设和交互
  • 彻底测试
  • 进程格力
  • 允许进程崩溃重启
  • 测量、监控并分析生产环境中的系统行为

人为错误

  • 以最小化犯错机会的方式设计系统
  • 将人们最容易犯错的地方与可能导致失效的地方解耦
  • 彻底测试,单元/集成/手动测试
  • 允许从人为错误中简单快速的恢复
  • 配置详细和明确的监控:比如性能指标和错误率
  • 良好的管理时间与重逢的培训

可靠性非常的重要,但是某些情况下也可能会选择牺牲可靠性来降低开发成本

可扩展性

用来描述系统应对负载增长能力的术语,需要考虑的是

  • 如果系统以特定方式增长,有什么选项可以应对增长
  • 如何增加计算资源来处理额外的负载

描述负载

使用负载参数的数字来描述,可能是

  • 每秒向Web服务器发出的请求
  • 数据库中的读写比率
  • 聊天室中同时活跃的用户数量
  • 缓存命中率或者其他等等

描述性能

对于在线系统,最重要的是服务的响应时间,即客户端发送请求到接受响应之间的时间

  • 平均响应时间

    • 算数平均值
    • 不能告诉有多少用户实际上经历了这个延迟
  • 百分位点

    • 将响应时间列表按最快到最慢排序,那么中位数就在正中间
    • 可以明确的反应出有多少用户比这个快,有多少用户比这个慢
    • 中位数简单被缩写为p50
  • 尾部延迟

    • 响应时间的高百分位点非常重要,直接影响用户的服务体验
    • p99.9
    • 由于排队延迟的原因,测试客户端响应时间非常重要

应对负载的方法

  • 纵向扩展(垂直扩展)

    • 转向更强大的机器
  • 横向扩展(水平扩展)

    • 将负载分布到多台小机器上
  • 跨多台机器部署无状态服务非常简单,但是将带状态的数据系统从单节点变为分布式配置会引入许多额外复杂度

  • 良好适配应用的可扩展架构是围绕假设建立的

可维护性

在设计之初就尽量考虑尽可能减少维护期间的痛苦,关注软件系统的三个设计原则

  • 可操作性
    • 便于运维团队保持系统平稳运行
  • 简单性
    • 从系统中消除尽可能多的复杂性,使新工程师也能轻松理解系统
  • 可演化性
    • 使工程师在未来可以轻松的对系统进行更改

可操作性

优良的运维可以弥补一定的软件缺陷,一个优良运维团队的职责如下:

  • 监控系统的运行状况
  • 跟踪问题的原因
  • 及时更新软件和平台
  • 了解系统间的相互作用
  • 预测未来的问题,并在问题出现之前加以解决
  • 简历部署、配置、管理方面的良好实践,编写相应工具
  • 执行复杂的维护任务
  • 配置变更时,维护系统的安全性
  • 定义工作流程,使运维的操作可预测
  • 铁打的营盘流水的兵,增加组织对系统的了解

数据系统可以通过各种方式降低日常任务的困扰度

  • 通过良好的监控,提供对系统内部状态和运行时行为的可见性
  • 为自动化提供良好支持,将系统与标准化工具相继承
  • 避免依赖单台机器
  • 提供良好的文档和易于理解的操作模型
  • 提供良好的默认行为,但需要时也允许管理员只有覆盖默认值
  • 有条件时进行自我修复,但需要时也允许管理员手动控制系统状态
  • 行为可预测,最大限度减少意外

简单性

额外复杂度:由具体实现中涌现,而非问题本身固有的复杂度

消除额外复杂度的最好工具就是抽象

可演化性

敏捷开发模式为适应变化提供了框架

数据模型与查询语言

基本思想:每一层都通过提供一个明确的数据模型来隐藏更低层次中的复杂性,这些抽象允许不同的人群有效的协作。

关系模型与文档模型

关系模型的著名代表就是SQL

  • 数据被组织成关系(表)
  • 其中每个关系是元组(行)的无序集合

NOSQL

驱动因素

  • 需要比关系型数据库更好的扩展性,包括非常大的数据集或者非常高的写入吞吐量
  • 免费和开源软件更受欢迎
  • 关系模型不能很好的支持一些特殊的查询操作
  • 受挫于关系模型的限制性,渴望一种更具多动态性与表现力的数据模型

对象关系不匹配

面向对象模型与SQL数据模型需要一个笨拙的转换层,像ActiveRecord和Hibernate这样的对象关系映射(ORM)框架可以减少这个转换成说需要的样板代码的数量,但是他们不能完全隐藏这两个模型之间的差异。

多对一和多对多的关系

简历中的区域和行业,有两种方式来存储

  1. 存储为纯文本的字符串
  2. 给出地理区域和行业的标准化列表,让用户从下拉中选择或者自动填充器中选择

其中2的好处在于

  • 简介之间样式和拼写统一
  • 避免歧义
  • 易于更新
  • 本地化支持
  • 更好的搜索

另外在于副本问题

  • 使用ID
    • 对人类有意义的信息只存储在一处
    • 所有引用的地方使用ID
  • 使用文本字符串
    • 所有引用的地方复制对人类有意义的信息

使用ID的好处是ID对人类没有意义,因此不需要改变,是数据库规范化的关键思想

可惜的是在文档模型中多对一的关系非常不合适。很多文档数据库并不支持链接

所以可以考虑在将某些特别的东西设置为实体,例如简历中的组织学校,这样就能方便的进行链接。

文档模型的优势

  • 架构灵活性
  • 因局部性而有更好的性能
  • 更接近应用程序使用的数据结构

关系模型的优势

  • 为链接提供更好的支持
  • 支持多对一及多对多的关系

写代码的选择

  • 文档模型

    • 应用程序的数据具有类似于文档的结构(一对多关系树)
    • 文档嵌套不能太深
    • 读时模式的文档结构
    • 文档数据局部性(数据库会加载整个文档,即使只访问其中一小部分)
  • 关系模型

    • 多对多的关系的需求
    • 强类型检查

数据查询语言

  • 命令式代码查询
  • 声明式查询
  • web上的声明式查询
  • MapReduce
    • 处于申明式查询和命令式查询之间
    • 查询逻辑用代码片段来表示,代码片段会被框架重复性调用
    • 基于map和reduce函数

一个MapReduce的例子

海洋生物学家这个例子还没太看明白

图数据模型

  • 顶点

例如

  • 社交图谱

    • 顶点是人
    • 边表示哪些人彼此认识
  • 网络图谱

    • 顶点是网页
    • 边表示指向其它页面的HTML链接
  • 公路或铁路网络

    • 顶点是交叉路口
    • 边表示他们之间的道路或者铁路线

属性图

顶点

  • 唯一的标识符
  • 一组出边
  • 一组入边
  • 一组属性(键值对)

  • 唯一标识符
  • 边的起点/尾部顶点
  • 边的终点/头部顶点
  • 描述两个顶点之间关系类型的标签
  • 一组属性(键值对)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 使用SQL来表示属性图

CREATE TABLE vertices (
vertex_id INTEGER PRIMARY KEY,
properties JSON
);
CREATE TABLE edges (
edge_id INTEGER PRIMARY KEY,
tail_vertex INTEGER REFERENCES vertices (vertex_id),
head_vertex INTEGER REFERENCES vertices (vertex_id),
label TEXT,
properties JSON
);
CREATE INDEX edges_tails ON edges (tail_vertex);
CREATE INDEX edges_heads ON edges (head_vertex);

Cypher 查询语言

属性图的声明式查询语言,为Neo4j图形数据库而发明

SQL中的图查询

使用递归公用表表达式(WITH RECURSIVE语法)

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
WITH RECURSIVE
-- in_usa 包含所有的美国境内的位置ID
in_usa(vertex_id) AS (
SELECT vertex_id FROM vertices WHERE properties ->> 'name' = 'United States'
UNION
SELECT edges.tail_vertex FROM edges
JOIN in_usa ON edges.head_vertex = in_usa.vertex_id
WHERE edges.label = 'within'
),
-- in_europe 包含所有的欧洲境内的位置ID
in_europe(vertex_id) AS (
SELECT vertex_id FROM vertices WHERE properties ->> 'name' = 'Europe'
UNION
SELECT edges.tail_vertex FROM edges
JOIN in_europe ON edges.head_vertex = in_europe.vertex_id
WHERE edges.label = 'within' ),
-- born_in_usa 包含了所有类型为Person,且出生在美国的顶点
born_in_usa(vertex_id) AS (
SELECT edges.tail_vertex FROM edges
JOIN in_usa ON edges.head_vertex = in_usa.vertex_id
WHERE edges.label = 'born_in' ),
-- lives_in_europe 包含了所有类型为Person,且居住在欧洲的顶点。
lives_in_europe(vertex_id) AS (
SELECT edges.tail_vertex FROM edges
JOIN in_europe ON edges.head_vertex = in_europe.vertex_id
WHERE edges.label = 'lives_in')
SELECT vertices.properties ->> 'name'
FROM vertices
JOIN born_in_usa ON vertices.vertex_id = born_in_usa.vertex_id
JOIN lives_in_europe ON vertices.vertex_id = lives_in_europe.vertex_id;

P.S.
mysql中要进行图查询是非常复杂的。

三元组存储和SPARQL

在三元存储中,所有的信息都是简单的三部分表示形式存储(主语、谓语、宾语)

1
例如(吉姆,喜欢,香蕉)

三元组的主语是图中的一个顶点,而宾语是下面两者之一

  • 原始数据中类型的值,例如
1
(lucy, age, 33)
  • 图中的另外的顶点,例如
1
(lucy, marriedTo, alain)

语义网络

RDF数据模型

SPARQL查询语言

基础:Datalog

存储与检索

驱动数据库的数据结构

最简单的db可以用以下实现

1
2
3
4
5
6
7
#!/bin/bash
db_set () {
echo "$1,$2" >> database
}
db_get () {
grep "^$1," database | sed -e "s/^$1,//" | tail -n 1
}

实现了基础的键值存储功能。并且在set时使用了尾部追加写入的方式

很多数据库在内部使用了日志(log),这也是一个仅追加(append-only)的数据文件。

日志:一个仅追加的记录序列。它可能压根就不是给人类看 的,使用二进制格式,并仅能由其他程序读取。

为了高效的查找数据库中特定键的值,需要一个数据结构:索引(index)

索引的大致思路:保存一些额外的元数据作为路 标,帮助你找到想要的数据

这是存储系统中一个重要的权衡:精心选择的索引加快了读查询的速度,但是每个索引都会 拖慢写入速度。

哈希索引

假设我们的数据存储只是一个追加写入的文件,就像前面的例子一样。那么最简单的索引策 略就是:保留一个内存中的哈希映射,其中每个键都映射到一个数据文件中的字节偏移量, 指明了可以找到对应值的位置

如何在追加索引中利用磁盘空间

将日志分为特定大小的段,当日志增长到特定尺寸时关闭当前段文件,并开始写入 一个新的段文件。然后,我们就可以对这些段进行压缩(compaction),压缩意味着在日志中丢弃重复的键,只保留每个键的最近更新。在进行时,我们仍然可以继续使用旧的段文件来正常提供读写请求。合并过程完成后,我们将读取请求 转换为使用新的合并段而不是旧段 —— 然后可以简单地删除旧的段文件。

接下来就是一些细节问题

文件格式

二进制

删除记录

使用逻辑删除,合并时再进行放弃

崩溃恢复

数据库重启时,内存散列映射将丢失,可以通过存储加速恢复磁盘上每个段的哈希映射的快照,可以更快地加载到内存中。

部分写入记录

数据库可能随时崩溃,包括将记录附加到日志中途,所以需要包含校验和,允许监测和忽略日志的损坏部分。

并发控制

常见的实现选择是只有一个写入器线程

局限性

  • 散列表必须能放进内存:磁盘哈希映射需要大量随机访问I/O
  • 范围查询效率不高:必须在散列映射中单独查找每个键

SSTables和LSM树

排序字符串表(Sorted String Table) 简称 SSTable

要求键值对的序列按键排序

优势:

  • 合并段是简单而高效的,即使文件大于可用内存
    • 归并排序有序文本
  • 为了在文件中找到一个特定的键,你不再需要保存内存中所有键的索引
    • 因为主键是有序的,因此可以通过临近位置偏移搜到想要的地方

构建和维护SSTables

使用内存中的红黑树或AVL树,可以按任何顺序插入键并按排序顺序读取它们。

  • 写入时,添加到内存中的平衡树数据结构,内存树又是被称为内存表(memtable)
  • 内存表大于某个阈值,将其作为SSTable文件写入磁盘。
  • 读取时,先尝试在内存表中寻找关键字,再到最近的磁盘段中寻找关键字
  • 有时会在后台运行合并和压缩过程以组合段文件并丢弃覆盖或删除的值

缺陷:

如果数据库崩溃,则最近的写入(在内存表中, 但尚未写入磁盘)将丢失

解决方案:

可以在磁盘上保存一个单独的日志,每个写入都会立即被附加到磁盘上,就像在前一节中一样。该日志不是按排序顺序,但这并不重要,因为它的唯一目的是在崩溃后恢复内存表。每当内存表写出到SSTable时,相应的日志都可以被丢弃。

用SSTables制作LSM树

此处的本质是LevelDB和RocksDB中使用的关键字存储引擎库

LSM树:日志结构合并树(Log-Structured Merge Tree)

性能优化

查找数据库中不存在的键:

  • 检查内存表
  • 从磁盘中读取,回到最老
  • 最后确定键不存在

解决方案:

  • 使用Bloom过滤器

布隆过滤器是用于近似集合内容的内存高效数据结构,它可以告诉您数据库中是否出现键,从而为不存在的键节省许多不必要的磁盘读取操作

  • 其它策略
    • 大小分层压实
    • 平坦压缩:LevelDB和RocksDB
    • 大小分层:HBase

水平压实:关键范围被拆分成更小的SSTables,较旧的数据被移动到单独的“水平”,这使得压缩能够更加递增地进行,并且使用更少的磁盘空间。

LSM树的小结

基本思想:保存一系列在后台合并的SSTables,简单而有效。

  • 数据集大于可用内存仍然能正常工作
  • 高效地执行范围查询:数据按排序顺序存储
  • 支持非常高的写入吞吐量:磁盘写入是连续的

B树

在几乎所有的关系数据库中,仍然是标准的索引实现,许多非关系数据库也使用它们

设计理念

  • 保持按键排序的键值对
  • 将数据库分解成固定大小的块或者页面,并且一次只能读取或者写入一个页面。
  • 每个页面都可以使用地址来标识,允许一个页面引用另一个页面,从而构建出一个页面树

B树

一个页面会被指定为B树的根;在索引中查找一个键时,就从这里开始。该页面包含几个键和 对子页面的引用。每个子页面负责一段连续范围的键,引用之间的键,指明了引用子页面的键范围。

在B树的一个页面中对子页面的引用的数量称为分支因子,例如上图中,分支因子是6。

B树的更新

搜索包含该键的叶页,更改该页中的值,并将该页写回到磁盘

B树的新增

找到其范围内包含新键的页面,并且添加到页面

如果没有足够可用空间容纳新键盘,则将其分成两个半满页面,更新父页面以解释新分区,如图所示

B树添加

增加B树的可靠性

不可靠的原因:

B树的基本底层写操作是用新数据覆盖磁盘上的页面

再加之 一些操作需要覆盖几个不同的页面,例如插入导致页面过度而拆分页面,因为如果数据库在仅有一些页面被写入后崩溃,那么最终将导致一个损坏的索引

解决方案:

预写式日志 (WAL, write-ahead-log) (也称为重做日志(redo log))。这是一个仅追加的文件,每个B树修改都可以应用到树本身的页面上。当数据库在崩溃后恢复时,这个日志被用来使B树 恢复到一致的状态

B树的优化

  • 使用写时复制方案,二部是覆盖页面并维护WAL进行崩溃恢复。(LMDB)
  • 不存储整个键来节省页面空间,特别是在树内部的页面上,键只需要提供足够的信息来充当键范围之间的边界
  • 额外的指针已添加到树中
  • B树的变体如分形树借用一些日志结构的思想来减少磁盘寻

B树与LSM树的比较

通常,LSM树写入速度更快,B树的读取速度更快。

LSM树的优点

写放大(write amplification)

在数据库的生命 周期中写入数据库导致对磁盘的多次写入

  • LSM树比B树支持更高的写入吞吐量
  • LSM树可以被压缩得更好,并且具有较低的存储开销(因为会定期重写SSTables去除碎片)

LSM树的缺点

  • 压缩过程有时会干扰正在进行的读写操作
  • 高写入吞吐量,数据库越大,压缩所需的磁盘带宽就越多

B树的优点

  • 每个键只存在于索引中的一个位置,这样可以提供更强大的事物语义的操作。

其它索引结构

例如关系数据库中使用CREATE INDEX命令在同一个表中创建多个二级索引。

B树/日志结构索引都可以用作辅助索引。

值存储在索引中

索引的关键字是搜索的内容,可以是以下两种情况:

  • 是所讨论的实际行
  • 也可以是对存储在别处的行的引用

堆文件(heap file): 行被存储的地方,并且存储的数据没有特定的顺序。

优势

  • 避免了在存在多个二级索引时复制数据
  • 不更改键的情况下更新值时非常高效:只要新值不大于旧值,就可以覆盖

劣势

  • 更改键时更新值可能会更新所有索引,或者在旧堆位置留下一个转发指针
  • 索引到堆文件的额外跳跃对读取来说性能损失很大。

索引的分类

  • 聚集索引(clustered index):将索引行直接存储在索引中。
1
例如,在MySQL的InnoDB存储引擎中,表的主键 总是一个聚簇索引,二级索引用主键(而不是堆文件中的位置)【31】。在SQL Server中, 可以为每个表指定一个聚簇索引【32】。
  • 非聚集索引(nonclustered index):仅在索引中存储对数据的引用
  • 覆盖索引(covering index)/包含列的索引(index with included columns):其存储表的一部分在索引内

多列索引

连接索引(concatenated index):它通过将一列的值追加到另一列后面,简单地将多个字段组合成一个键(索引定义中指定了字段的连接顺序)

多维索引(multi-dimensional index):是一种查询多个列的更一般的方法,这对于地理空间数据尤为重要。

全文搜索和模糊索引

Lucene:内存中的索引 是键中字符的有限状态自动机,类似于trie。这个自动机可以转换成Levenshtein自动机,它支持在给定的编辑距离内有效地搜索单词。

在内存中存储一切

内存数据库

  • 仅用于缓存(如Memcached),重启时允许数据丢失
  • 持久性存储:
    • 通过特殊的硬件
    • 更改日志写入磁盘
    • 定时快照写入磁盘或者通过复制内存实现

一些有趣的例子

  • VoltDB、MemSQL、Oracle TimesTen:具有关系模型的内存数据库
  • RAM Cloud:开源的内存键值存储器,具有持久性(对存储器中的数据以及 磁盘上的数据使用日志结构化方法)
  • Redis和Couchbase:异步写入磁盘,提供了较弱的持久性

内存数据库更快的原因在于省去了将内存数据结构编码为磁盘数据结构的开销

事务处理和分析

属性 事务处理 OLTP 分析系统 OLAP
主要读取模式 查询少量记录,按键读取 在大批量记录上聚合
主要写入模式 随机访问,写入要求低延时 批量导入(ETL),事件流
主要用户 终端用户,通过Web应用 内部数据分析师,决策支持
处理的数据 数据的最新状态(当前时间点) 随时间推移的历史事件
数据集尺寸 GB ~ TB TB ~ PB

专门进行分析的数据库被称为数据仓库(data warehouse)

数据仓库

数据仓库建立

OLTP数据库与数据仓库之间的分歧

数据仓库通常是关系型的,然而由于针对不同的查询模式进行了优化,因此不是两种都支持。

一些有趣的数据仓库的例子

  • Microsoft SQL Server、SAP HANA支持在同一产品中进行事务处理和数据仓库
  • Teradata、Vertica、SAP HANA、ParAccel:昂贵的付费仓库
  • Apache Hive、Spark SQL、Cloudera Impala、Facebook Presto、Apache Tajo、Apache Drill:开源的数据仓库竞争者

分析模式

  • 星型模式(维度建模)

由一个中心组成,例如如下模式

实际销售表

日期 产品 仓库表 促销 用户 数量 原始价格 折扣价格
日期表key 产品表key 仓储表key 促销表key 用户表key 数量 价格 价格

可以看到中心表是由其他表的主键所构成的

  • 雪花模式

星型表的一种变体,其中的尺寸再次被分解,例如产品表中继续被细分等等。

列存储

场景

事实表虽然超过100列,但是每次数据仓库的分析查询只会访问4、5个查询

然而在大多数OLTP数据库中,存储都是面向行的方式进行布局的:表格的一行中的所有值都相邻存储。

即是构建了常见列的索引,然而面向行的存储引擎仍然会将所有这些行从磁盘中加载到内存中,解析,过滤,需要很长时间。

方法

不要将所有来自一行的值存储在一起,而是将来自每一列的所有值存储在一起

简而言之就是讲每个列存储在一个单独的文件中,查询只需要读取和解析查询中使用的那些列。

列压缩

位图编码

位图编码

面向列的存储和列族

Cassandra和HBase有一个列族的概念,他们从Bigtable继承【9】。然而,把它们称为 面向列是非常具有误导性的:在每个列族中,它们将一行中的所有列与行键一起存储, 并且不使用列压缩。因此,Bigtable模型仍然主要是面向行的。

内存带宽和向量处理

矢量化处理

列存储中的排序顺序

每次排序都会对所有列进行影响

举例:

如果查询通常以日期范围为目标,例如上个月,则可以将 date_key 作为第一个排序键。然后,查询优化器只能扫描上个月的行,这 比扫描所有行要快得多。

第二列可以确定第一列中具有相同值的任何行的排序顺序。

排序的另一个好处是可以压缩列(参见位图中的压缩相同值)

几个不同的排序顺序

Vertica中被采用:使用不同的方式存储相同的数据。在查询时可以使用最适合查询模式的版本

写入列存储

使用LSM树,所有的写操作先进入一个内存中的存储,在这里它们被添加到一个已排序的结构中,并准备写入磁盘。内存中的存储是面向行还是列的,这并不重要。当已经积累了足够的写入数据时,它们将与磁盘上的列文件合并,并批量写入新文件。这基本上是Vertica所做的。

聚合

缓存一些常见的聚合函数,例如SQL中的COUNT,SUM,AVG,MIN,MAX

物化视图:

在关系数据库中通常会被定义为一个虚拟视图,不同的是,物化视图是查询结果的实际副本,写入磁盘,而虚拟视图只是写入查询的捷径。从虚拟视图读取时,SQL引擎会将其展开到视图的底层查询中,然后处理展开的查询。

虚拟视图

一个类似于表的对象,其内容是一些查询的结果

虚拟视图

这里只是用了两个维度的视图,更多维度需要自己去想象了。

物化视图
优点:查询变得非常快
缺点:不具有查询原始数据的灵活性

编码与演化

代码变更的基本方案

  • 服务端:滚动升级(阶段发布),将新版本部署到少数几个节点,检查新版本是否正常运行,然后逐渐部完所有的节点
  • 客户端:是否升级看用户的心情,用户可能长期都不会升级。

新旧代码会可能同时共处,因此要保持双向兼容性

  • 向后兼容:新代码可以读旧数据
  • 向前兼容:旧代码可以读新数据

向前兼容的核心在于旧版的程序需要忽略新版数据格式中新增的部分。

编码数据的格式

程序至少要使用两种形式的数据

  • 内存中的数据格式
  • 要写入文件或者通过网络发送,必须通过其编码为某种自包含的字节序列(例如JSON文档)
  • 编码:内存到字节序列的转换(序列化,编组)
  • 解码:反之(解析,反序列化,反编组)

语言特定格式

编程语言的内建支持

  • JAVA:java.io.Serializable
  • Ruby:Marshal
  • Python:pickle
  • 其它的第三方库: kryo for Java

优势:

  • 方便
  • 很少的额外代码实现内存对象的保存与恢复

劣势:

  • 编码与语言深度板顶,其它语言很难读取
  • 为了恢复相同对象的数据,解码过程需要实例化任意类的能力,可能会导致安全问题
  • 向前向后兼容可能会有问题
  • 效率很低(Java的内置序列化点名批评)

JSON,XML和二进制变体

JSON,XML和CSV是文本格式,具有人类可读性,被广泛支持,可惜也有些内部问题

  • 数字编码多有歧义:XML和CSV不能区分数字和字符串,JSON能区分,但是不能区分整数和浮点数,而且不能指定精度
  • 处理大量数据时,问题会更加严重,Twitter上有一个大于$2^{53}$的数字的例子,它使用一个64位的数 字来标识每条推文。Twitter API返回的JSON包含了两种推特ID,一个JSON数字,另一个是十进制字符串,以此避免JavaScript程序无法正确解析数字的问题
  • JSON和XML对Unicode字符串有很好的支持,但是不支持二进制数据,因此使用Base64编码来解决这个问题。
  • CSV没有任何模式,需要应用程序定义每行和每列的含义。

二进制编码

为了节约空间,导致大量的二进制编码版本的JSON & XML出现,例如

  • JSON:MessagePack,BSON,BJSON,UBJSON,BISON和Smile等
  • XML:WBXML和Fast Infoset等

Thrift与Protocol Buffer

Thrift:Facebook开发,需要接口定义语言(IDL)来描述模式,如下

1
2
3
4
5
struct Person {
1: required string userName,
2: optional i64 favoriteNumber,
3: optional list<string> interests
}

Protocol Buffers: Google开发,与Thrift类似

1
2
3
4
5
message Person {
required string user_name = 1;
optional int64 favorite_number = 2;
repeated string interests = 3;
}

字段标签的演变

以Thrift为例,字段由标签号码(1,2,3)标识,使用数据类型(i64,string)为注释。

  • 向前兼容性
    • 旧代码不知道新的字段,可以简单的忽略,数据类型注释保证了忽略跳过的字节数
  • 向后兼容性
    • 在模式部署后新添加的字段都必须是可选或者具有默认值的

数据类型的演变

允许,但是值会失去精度或者被扼杀

Avro

Apache Avro: Hadoop的子项目,一种Avro IDL用于人工编辑,一种基于JSON用于机器读取

1
2
3
4
5
record Person {
string userName;
union { null, long } favoriteNumber = null;
array<string> interests;
}

对应的等价JSON表示为

1
2
3
4
5
6
7
8
9
{
"type": "record",
"name": "Person",
"fields": [
{"name": "userName", "type": "string"},
{"name": "favoriteNumber", "type": ["null", "long"], "default": null},
{"name": "interests", "type": {"type": "array", "items": "string"}
]
}

Avro的作者模式和读者模式

  • 作者模式:应用程序想要编码一些数据,可以使用它知道的任何版本的模式编码数据
  • 读者模式:应用程序想要解码一些数据,它会希望数据在某个模式中,这就是读者模式

Avro的关键思想在于作者模式和读者模式不必相同,它们只需要兼容。

例如:

Avro的作者模式与读者模式

如果读取数据的代码遇到:

  • 在作者模式中但不在读者模式中:则忽略
  • 在读者模式中但不在作者模式中:则使用读者模式中的默认填充值

Avro的模式演变规则

只能添加或者删除具有默认值的字段

对于更改类型的情况

  • Avro可以转换类型,就恶意改变字段的数据类型
  • 更改字段名称是向后兼容的,但不能向前兼容
  • 向联合类型添加分支也是向后兼容的,不能向前兼容

动态生成的模式

架构不包含任何标签号码

数据流的类型

数据库中的数据流

需要担心的是旧版本代码在更新数据库中的值时会导致领域不完整(未写回新的字段)

数据与代码时间的不匹配

大部分关系数据库都允许简单的模式更改,例如添加一个默认值为空的列

归档存储

  • 快照
  • 备份
  • 数据仓库等

服务中的数据流

服务器公开的API被称为服务

  • REST

  • RPC

Web服务

服务使用HTTP作为底层通信协议,可称WEB服务

  • 运行在用户设备上的客户端应用
  • 一种服务向同一组织拥有的另一项服务提出请求(中间件)
  • 一种服务通过互联网向不同组织所拥有的服务提出请求(在线服务提供的公共API)

目前有两种web服务方法

  • REST:强调简单的数据格式,使用URL来标识资源,并使用HTTP功能进行缓存控制,身份验证和内容类型协商

根据REST原则设计的API称为RESTful

  • SOAP:用于制作网络API请求的基于XML的协议

远程过程调用(RPC)的问题

RPC本质在于向远程网络服务发出请求,但是根本上是具有缺陷的

  • RPC请求是不可预知的,必须去预测,例如重试失败的请求
  • RPC可能因为超时,返回没有任何结果
  • RPC的重试可能会导致操作被执行多次,需要在协议中引入除重机制(幂等)

幂等:某个函数或者某个接口使用相同参数调用一次或者无限次,其造成的后果是一样的

  • RPC的延迟是很不稳定的,可能1秒返回结果,也可能需要更长的时间
  • RPC的请求在发送较大的对象是可能会变成问题
  • RPC的跨语言数据类型可能会有问题

RPC的方向

  • Thrift,Avro带有RPC支持
  • gRPC使用了Protocol Buffer的RPC实现
  • Finagle使用Thrfit
  • Rest.li使用JSON over HTTP

REST是公共API的主要锋哥
RPC主要是同一组织之间拥有的服务请求

数据编码与RPC的演化

RPC方案的前后向兼容性属性从它使用的编码方式中继承

  • RPC可能需要提供多个版本的服务API
  • RESTful API,常用的方法是在URL或HTTP Accept头中使用版本号
  • API密钥来表示特定客户端的服务
  • 将客户端请求的api版本存储在服务器上并允许通过单独的管理页面进行管理和更新

消息传递中的数据流

异步消息传递系统(消息代理)的优点

  • 收件人不可用或过载的情况下可以充当缓冲区,提高系统可靠性
  • 可以自动将消息重发到已经崩溃的进程,防止消息丢失
  • 避免发件人知道收件人的IP和端口
  • 允许将一条消息发给多个收件人
  • 将发件人与收件人逻辑分离

消息传递通信是单向的:发送者不期望收到消息的回复,只是发送它,忘记它

消息掮客

一些开源的中间件消息队列,例如

  • RabbitMQ,ActiveMQ,HornetQ,NATS和Apache Kafka等开源实现
  • TIBCO,IBM WebSphere和webMethods等商业软件

通常情况下:一个进程将消息发送到指定的队列或主题,代理确保将消息传递给一个或多个消费者或订阅者到那个队列或主题。在同一主题上可以有许多生产者和许多消费者。

分布式的Actor框架

Actor模型

逻辑被封装在角色中,而不是直接处理线程(以及竞争条件,锁定和死锁的相关问题)。每个角色通常代表一个客户或实体,它可能有一些本地状态(不与其他任何角色共享),它通过发送和接收异步消息与其他角色通信。消息传送不保证:在某些错误情况下,消息将丢失。由于每个角色一次只能处理一条消息,因此不需要担心线程,每个角色可以由框架独立调度。

在分布式的框架中,可以用这个模型跨越多个节点扩展应用程序。

流行的分布式actor框架处理消息

  • Akka使用Java的内置序列化
  • Orleans默认使用不支持滚动升级部署的自定义数据编码格式
  • Erlang OTP对记录模式的更改是非常困难的