这是阅读该书后简单总结的大纲,自用。
第一部分 数据系统基础
第1章 可靠、可扩展与可维护的应用系统
-
认识数据系统
-
通常将数据库、队列、高速缓存等视为不同类型的系统,因为有着不同的访问模式。但本书将它们归类为数据系统。
-
原因
-
- 近年出现了多种数据库存和处理的功能可以适用于不同的场景
-
- 越来越多的应用系统需要把各个组件组合在一起
-
-
-
对于大多数软件系统都极为重要的三个问题
-
可靠性
- 出现意外情况时系统应该能正常运转
-
可扩展性
- 随着规模增长,系统以合理方式来匹配这种增长
-
可维护性
- 新的人员参与系统开发和运维时,系统都应该高效运转
-
-
-
可靠性
- 硬件故障
- 软件错误
- 人为失误
- 可靠性的重要性
-
可扩展性
-
描述负载
- 可以是称为负载参数的若干数字来描述
-
描述性能
- 负载增加会发生什么
- 负责增加,若要保持性能不变,需要增加多少资源
-
应对负载增加的方法
-
考虑每增加一个数量级的负载,架构应如何设计
- 如何在水平扩展和垂直扩展(升级更强大的机器)之间做取舍
- 某些系统具有弹性特性
- 无状态服务的扩展比较容易
-
-
-
可维护性
-
可运维性:运维更轻松
-
简单性:简化复杂性
- 通过抽象消除复杂性
-
可演化性:易于改变
-
第2章 数据模型与查询语言
-
关系模型与文档模型
-
最著名的数据模型是SQL,关系数据库的核心是商业数据处理
-
NoSQL的诞生
-
现在的含义被逆向重新解释为不仅仅是SQL
-
使用NoSQL的原因
- 比关系数据库更好的扩展性需求,支持超大数据集和写入吞吐量
- 偏爱免费开源产品
- 支持一些特定的查询操作
- 具有动态和表达力
-
-
对象-关系不匹配
-
编程语言中的对象与DB行列需要笨拙的转换层
-
一对多的关系如何表达 (例如对于简历这种自包含的结构)
- 传统SQL需要将联系信息放在单独的表中
- 之后SQL标准提供了对结构化数据类型的支持(😯)
- 将工作、教育和联系信息编码为JSON或XML文档 (一个具有层次关系的JSON)
-
-
多对一与多对多的关系
-
对于城市和公司使用ID而非字符串有一些好处
-
对于需要表达多对一关系不适合文档模型,因为树状结构几乎不支持联结
-
如果应用程序的初始版本适合采用无联结的文档模型,随着应用支持更多的功能,数据页变得更加互联一体化
- 比如可能要把简历和学校链接到实体的引用
- 比如用户可以推荐其它其他人,推荐者的资料需要更新到多个被推荐者上面
-
-
文档数据库是否在重演历史?
-
随着对联结的需求增加,文档和NoSQL如何表示数据关系开始被争论
-
历史上的层次模型与之类似
-
网络模型
- CODASYL模型支持一个节点引用多个父节点,用类似指针的方法依次遍历(手动访问路径)
- 缺点是查询和更新数据库变得异常复杂
-
关系模型
-
定义了所有数据的格式:关系(表)只是元组(行)的集合
-
核心要点是:只需构建一次查询优化器,然后后面都可从中受益
- 如果没有查询优化器,那么特定查询手动编写访问路径更容易
-
-
文档数据库比较
- 文档数据库是1对多的层次模块,在表示多对1或多对多时与关系数据库没有根本的区别
-
-
关系数据库与文档数据库现状
-
哪种数据模型的应用代码更简单?
- 文档数据库不能引用文档中的嵌套项,且不能使用多对多关系
- 取决于数据项之间的关系类型
-
文档模型中的模式灵活性
-
文档数据库并非无模式,而是读时模式(数据的结构是隐式的,只在读取时解释)
- 在更改模式时由应用层处理
-
关系数据库是写时模式(模式是显式的,在写入时需要遵循)
- 在更改模式时需要停机
- 也可以将新列默认值设置为NULL,在读取时填充,类似与于文档数据库
-
-
查询的数据局部性
- 如果经常访问整个文档,则文档数据库的存储局部性由优势。否则数据被划分在多个表中,需要更多磁盘I/O。
- 有些数据库也提供了相关数据归为一组的功能
-
文档数据库与关系数据库的融合
- 大多数关系数据库都支持Json和XML索引
- 文档数据库也支持在客户端执行高效联结
-
-
-
数据查询语言
-
命令式查询语言告诉计算机以特定的顺序执行某些操作,而声明式查询语言只需要指定所需的数据模式
-
Web上的声明式查询
- 用CSS类比声明式查询语言,用JS类比命令式查询语言
-
MapReduce查询
-
MongoDB中对MapReduce的使用:map和reduce都是纯函数,只能使用传递进去的数据作为数据,不能查询数据库,不能有副作用
- 看起来像Redis中的lua脚本,在声明式查询中添加了命令式查询的小组件
-
-
-
图状数据模型
-
属性图
- 每个顶点都包含出边、入边、属性的集合、标识符
- 每个边都包含起点、终点、描述顶点间关系的标签、属性的集合、标识符
- 可以将图存储看做有两个关系表组成,一个存储顶点,一个存储边
-
Cypher查询语言
- 属性图的查询语言。是声明式查询语言。
-
SQL中的图查询
- 把图数据放在关系结构中有些困难
- 对于可变路径长度,可以用递归公用表达式模拟
-
三元存储于SPARQL
-
所有信息都以三部分存储(主体,谓语,客体)
-
语义网
- 将信息发布为机器可读的格式给计算机阅读,RDF就是这样,让不同网站以一致的格式发布数据
-
RDF数据模型
- 以XML格式,把对象ID、对象的属性、对象的关系表示出来
-
SPARQL查询语言
- 采用RDF数据的三元存储查询语言,是Cypher语言的祖先
-
-
Datalog基础
- 比SPARQL或CYPHER更古老的语言
-
第3章 数据存储与检索
-
数据库核心:数据结构
-
最简单的数据结构:一个日志文件,set 时增加一行 key-value,读取时读取最后一个要查找的 key
- 日志是仅支持追加式更新的数据文件
-
哈希索引
-
最简单的索引是保存内存的哈希表,将每个key一一映射到数据文件的offset
-
如何避免用尽磁盘空间
- 把文件分成一定大小的段,当文件达到大小时就关闭它,并写入后续新的段文件中。然后在这些段中执行压缩(丢弃重复的key)
-
优势
- 追加和分段是顺序写,速度快
- 并发和崩溃恢复更容易
-
缺点
- 哈希表必须全部放入内存,磁盘上维护的hash map会产生大量随机访问I/O,不可采用。
- 难以实现区间查询
-
-
SSTables和LSM-Tree
-
相比于上面,要求key按顺序排列,并且对于日志中出现的同一个键,后出现的值优先级高
-
查找key时,不需要哈希表,由于是有序的,可以采用稀疏索引
-
合并多个段时,也可以线性扫描
-
构建和维护SSTables
- 写入时:添加到内存中的平衡树数据结构中
- 当内存表大于某个阈值时,将SSTable写入磁盘
- 读请求:先在内存表中查找键,然后是最新的段文件
- 后台进程周期性地执行段合并与压缩过程
-
从SSTable到LSM-Tree
- 基于合并和压缩排序文件原理的存储引擎通常都被称为LSM存储引擎
-
性能优化
- 使用布隆过滤器,避免不存在的key消耗时间过长
-
-
B-trees
-
B-tree将数据库分解成固定大小的页(4KB),是内部读写最小单元
-
某一页是B-tree的根(root), 在索引中查找某一键时,总是从这里开始,该页面包含若干个页和对子页的引用
-
使B-tree可靠
- B树需要对页进行覆盖,保持在磁盘中的物理位置
- 通过预写日志(重做日志),用于异常时恢复
- 多个线程访问B树要进行并发控制,通常使用锁存器保护树的数据结构
-
优化B-tree
-
一些db不使用覆盖页和WAL,而是使用写时复制方案
- 🔗第7章快照隔离与可重复读
-
保存键的缩略信息,而非完整的键,以便节省页空间
-
使相邻子页按顺序保存在磁盘上
-
添加额外的指针可以向左或向右引用同级的兄弟页
-
-
-
对比B-tree和LSM-tree
-
LSM-tree的优点
-
写放大
- B-tree索引至少写两次数据,预写日志和树页本身,还可能发生页分裂,及时只有几个字节更改也要写入整个页
- SSTable也会有反复压缩和合并产生写放大
-
吞吐量
- LSM-tree的吞吐量更高
-
碎片
- LSM-tree定期重写以消除碎片化,所以具有较低的存储开销
-
-
LSM-tree的缺点
- 压缩过程干扰读写操作,影响性能
- B-tree的每个键在索引中有唯一位置,事务语义支持更强
-
-
其它索引结构
-
二级索引的键不是唯一的,但也可以用B-tree或日志结构索引
-
在索引中存储值
- 索引的value可以存储其它行的引用,也可以存储整行
-
多列索引
- 最常见的索引类型称为级联索引
- 还有地理位置索引
-
全文搜索和模糊索引
- 需要特殊技术
-
在内存中保存所有索引内容
- 一些内存中的KV存储,如memcached,主要用于缓存,机器重启造成的丢失是可恢复的
- 内存数据为库的优势是避免使用写磁盘的格式对内存数据结构编码的开销,而不是不需要从磁盘中读取
- 提供了一些基于磁盘索引很难实现的数据模型,例如Redis
-
-
-
事务处理与分析处理
-
两种事务处理系统的分别是OLTP和OLAP,前者用于"事务",后者是"分析"
-
数据仓库
-
数据仓库中单独的数据库,分析者可以在不影响OLTP操作的情况下尽情使用
-
OLTP数据库和数据仓库之间的差异
- 它们针对迥然不同的查询模式进行了各自优化
-
-
星型与雪花型分析模式
- 分析型业务的数据模型少得多,大部分使用星型模式,也称为维度建模
- 模式的核心是事实表,表示特定时间发生的事件,其它列可以会引用其它表的外键
-
-
列式存储
-
按列存储在查找部分列时,列被保存在磁盘上相邻的位置
-
列压缩
-
当列中不同值的数量小于行数,可以用bitmap映射,1 bit 对应 1 行
-
如果位图中有很多0,可以对位图进行游程编码
-
内存带宽与矢量化处理
- 如何高效地将内存带宽应用于缓存:
- 面向列的存储布局有利于高效利用CPU周期
-
-
列存储中的排序
-
排序需要对一整行进行排序。可以帮助进一步压缩列
-
几种不同的排序
- 以多种不同的排序方式存储数据,便于查询
-
-
列存储的写操作
- 类似LSM-tree,所有的写入先进行内存存储区,将其添加到已排序的结构中
-
聚合:数据立方体与物化视图
- 创建缓存的一种方式是物化视图
- 物化视图的另一种特殊情况称为数据立方体或OLAP立方体(类似数据透视表)
-
第4章 数据编码与演化
-
数据编码格式
-
程序通常使用两种不同的数据表示形式
- 在内存中通过指针提高访问效率
- 将数据写入文件时,必须编码为某种字包含的字节序列。
-
语言特定的格式
-
许多编程语言内置的序列化工具有一些问题
- 编码通常与特定的语言绑定在一起
- 经常忽略向前和向后兼容性等问题
-
-
JSON XML与二进制变体
-
除了表面的语法问题之外,还有一些微妙的问题
- 数字编码(整数还是浮点)
- 字节流编码
- 模式支持
-
二进制编码
- 就是用紧凑的编码表示变量个数和类型,比如MessagePack
-
-
Thrift和Protocol Buffers
-
Thrift分为BinaryProtocol和CompactProtocol
- Binary的示例是type+field tag+length+data
-
PB与compatct protocol类型
-
字段标签和模式演化
- 模式演化时不能更换字段标签,也不能标记required
-
数据类型和模式演化
- 如果将i64改成i32会遭遇截断
- PB的字段变成数组则只能看到最后一个元素
-
-
Avro
-
是Hadoop的子项目
-
没有标签和数据类型
-
写模式与读模式
- 用所知道的模式来编码数据,称为写模式
- 当应用程序读取藉此数据时,期望某些数据符合某个模式称为读模式
- Avro可以自动将数据从写模式转化为读模式
-
模式演化规则
- 保持兼容性,只能添加或删除具有默认值的字段
-
那么writer模式是什么?
- 在hadoop中有大量相同格式的文件,所以只需要在首个文件声明文件类型,就能知道写入时的模式
- 或者在编码记录的开头写下版本号
-
动态生成的模式
- Avro是字段是通过名子而非序号来标识的
-
代码生成和动态类型语言
- 动态类型语言中通常不需要代码生成
- Avro和Pb Thrift一样都可以代码生成
-
-
模式的优点
-
-
数据流模式
-
进程间数据流动的方式
- 通过数据库、服务调用和异步消息传递
-
基于数据库的数据流
-
单个进程访问时,写入数据时要保证向后兼容性
-
多个进程访问时需要保证向前和向后兼容
-
添加新字段时保留未知字段不变
-
避免在编解码时丢失未知字段
-
不同的时间写入不同的值
- 大多数关系数据库允许进行简单的数据变更,例如添加具有默认值为空的新列
- 将数据重写或迁移为新模式的代价很高
-
归档存储
- 转储通常使用最新的模式复制数据
-
-
基于服务的数据流:REST和RPC
-
应该期望新旧版本的服务器和客户端能同时运行,而不必与其它团队协调
-
网络服务
-
Web服务使用的不同上下文
- 运行在用户设备上的客户端应用程序通过HTTP向服务发出请求
- 组织内的服务调用
- 调用其它在线服务
-
REST和SOAP是设计理念完全相反的Web服务方法
- REST强调简单的数据格式,使用URL来标识资源
- SOAP基于XML的目的是独立于HTTP,避免使用HTTP的大多数功能,带有强大而复杂的多种相关标准
-
-
远程过程调用(RPC)的问题
- RPC请求不可预测(请求可以丢失),如果请求丢失,根本不知道发生了什么(请求是否真的执行了)
- 重试可能导致请求被执行多次
- 不同语言拥有的基本类型不同(比如Js的数字大于2^53)
-
RPC的发展方向
- 新一代RPC更加明确了远程请求和本地请求不同的事实,比如使用Future封装可以失败的异步操作,gRPC支持流
- 一些框架还提供了服务发现
-
RPC的数据编码和演化
-
假定所有的服务器都先被更新,客户端后被更新,则需要
- 请求上具有身后兼容性,响应上具有向前兼容性
-
-
-
基于消息传递的数据流
-
就是使用MQ
-
消息代理
- Topic只提供单向数据流,但消费者本身可能会将消息发布到另一个主题
- 消息代理通常不会强制任何特定的数据模型
-
分布式Actor框架
- Actor框架:用于单个进程的并发编程模型,逻辑被封装在Actor中,通过发送和接收异步消息与其它Actor通信
- 分布式Actor框架可以跨多个节点,与RPC相比位置透明性更高
- 在执行滚动升级时需要考虑前后兼容性问题
-
-
第二部分 分布式数据系统
第5章 数据复制
-
在多台机器上保存相同数据的副本
-
主节点与从节点
-
当有了多副本时如何保证所有副本之间的数据一致
-
主从复制的原理
- 客户写入数据库时,必须将写请求首先发给主副本
- 将更改发送给所有从副本,要保证写入顺序一致
- 客户端可以在主副本或从副本上执行查询请求
-
同步复制与异步复制
-
同步复制的速度可能非常慢,但可以明确保证从节点完成了与主节点的更新同步
-
全异步方式无法保证数据的持久化
- 虽然异步模式听起来是不靠谱的折中设计,但异步复制还是被广泛使用
-
-
配置新的从节点
-
主要操作步骤如下
- 在某个时间点对主节点的数据副本产生一个一致性快照
- 将此快照拷贝到新的从节点
- 主节点发送请求快照点之后的数据更改日志
- 从节点应用数据变更
-
-
处理节点失败
-
如何通过主从复制实现系统的高可用
-
从节点失效:追赶式恢复
- 从节点请求某笔事务之后的所有数据变更
-
主节点失效:节点切换
-
将某个节点切换为主节点
-
自动切换的步骤
- 通过超时机制确认主节点失效
- 选举新的主节点
- 重新配置系统使新主节点生效
-
上述步骤的问题
-
原主节点上未完成复制的写请求被丢弃,可能违背数据更新持久化的承诺
-
如果数据库之外有其它系统依赖数据库的内容并在一起协同使用,丢弃数据的方案就非常危险
- 比如GitHub的事故中,新的主节点的自增ID小于原主节点,导致主键被重新分配,而这些主键已被外部Redis所引用,导致私有数据泄漏
-
可能发生脑裂,系统可能会强制关闭其中一个节点,但如果设计不周,可能出现两个主节点被同时关闭
-
如何设置合适的timeout来检测主节点失效
-
-
-
-
复制日志的实现
-
基于语句的复制
-
主节点将每个操作语句作为日志发送给从节点
-
不适用的场景
-
任何调用非确定性的语句,如NOW()获取当前时间,可能会在不同副本上产生不同的值
-
如果语句使用了自增列,或者依赖于数据库的现有数据,则必须按照相同的顺序执行
- 影响多个同时并发执行的事务
-
有副使用的语句(如触发器)可能会在每个副本上产生不同的副使用
-
-
-
基于预写日志(WAL)传输
-
日志通过网络发送给从节点,建立与主节点数据完全相同的数据副本
-
主要缺点
-
日志描述的数据结果非常底层,比如哪些磁盘块的哪些字节发生改变,导致主从节点不能运行不同版本的软件
- 导致数据库升级时必须停机
-
-
-
基于行的逻辑日志复制
- 复制与存储逻辑剥离,即复制和存储引擎采用不同的日志格式
- 按照行插入、删除和更新设置不同的字段
-
基于触发器的复制
- 当数据库系统发生数据更改时,运行自己的应用层代码
-
-
-
复制滞后问题
-
主从复制模式适用于读操作密集的负载,但这种扩展方式主要用于异步复制,异步复制可能导致读到过期数据,但最终一致性。复制滞后问题需要关注
-
读自己的写(自己的写→读)
-
为了避免用户看到刚刚提交数据丢失,我们需要写后读一致性
-
可行的方案
-
如果用户访问可能会被修改的内容,则从主节点读取,例如社交网络的用户首页,从主节点读自己的配置文件,在从节点读取其它用户的配置文件
-
如果大部分内容都可能被所有用户修改,则上述方法不太有效,此时需要其它方案
- 跟踪最近更新的时间,在更新后一分钟内,问题从主节点读取
- 监控从节点的复制滞后程度,避免从滞后时间一分钟的从节点读取
-
客户端可以记住最近更新的时间戳(逻辑时间戳或实际系统时钟),附带在读请求中,系统就确保至少包含了该时间戳的更新(如果不够新要更换从节点)
-
如果副本分布在多数据中心,则必须先把请求路由到主节点所在的数据中心
-
-
跨设备写后读要考虑的问题
- 不能用时间戳的方法
- 就确保不同请求到达同一个数据中心
-
-
单调读
-
保证用户依次进行多次读取,不能看到回滚现象
-
实现方式
- 每个用户固定问题从固定的同一副本执行读取
-
-
前缀一致读
-
保证对于一系列按照某个顺序发生的写请求,读取这些内容时也按照当时写入的顺序
- 在许多分布式数据库中,不同的分区独立运行,因此不存在全局写入事件
-
解决方案
- 一个方法是确保任何具有因果顺序的写入都交给一个分区来完成,但此方案真正的实现效率大打折扣
- 🔗 在本章稍后的 Happend-before 关系与并发 会继续该问题的探讨
-
-
复制滞后的解决方案
- 如果延迟多大影响用户体验,则最终一致系统应该提供更强的保证
- 应用层处理这些问题复杂且容易出错
- 分布式数据库的事务不太成熟
-
-
多主节点复制
-
可以配置多个主节点,每个节点都可以接受写操作
- 每个主节点都需要将数据更改转发到所有其他节点
-
使用场景
-
多数据中心
-
在数据中心之间,主节点负责同其他数据中心的主节点进行数据的交换更新
-
对比与单主节点
-
性能:每个写操作都可以在本地数据中信息快速响应
-
容忍数据中心失效
-
容忍网络问题
-
最大的缺点
- 不同数据中心可能会同时修改相同的数据,因此必须解决潜在的写冲突
-
-
-
离线客户端操作
- 允许设备内有一个充当主节点的本地数据库,用来接收写请求,然后设备之间采用异步方式同步主节点的副本
-
协作编辑
- 为了确保不会发生编辑冲突,应用程序必须先将文档锁定
- 为了加快协作编辑的效率,可编辑的粒度需要非常小
-
-
处理写冲突
-
同步与异步冲突检测
-
如果采用异步检测冲突,则出现冲突再要求用户层来解决为时已晚
-
如果采用同步检测冲突,则会丧失多主节点的主要优势
-
避免冲突
- 在应用层保证对特定记录的写请求总是通过同一个主节点
- 但这种冲突避免方式可能在数据中心发生故障时失效,必须有措施处理写入冲突
-
收敛于一致状态
-
对于主从复制模型写入是有顺序的,多主节点不行
-
可能的方式
- 给每个写入分配唯一的ID,挑选ID最大者获胜
- 为每个副本设置不同的优先级,序号高的副本始终优先写入
- 以某种方式将值合并在一起
- 利用预定义好的格式来记录和保留冲突相关的所有信息
-
-
自定义冲突解决逻辑
-
在写入时执行
- 在复制变更日志时检测到冲突,调用应用层的冲突处理程序
-
在读取时执行
- 在检测到冲突时保留所有值,在下一次读取数据时把数据的多个版本返回给应用层,提示用户解决或自动解决
-
-
什么是冲突?
-
-
-
拓扑结构
-
三种拓扑结构
- ring, 星型拓扑, all-to-all
-
环形和星型拓扑的问题,如果某一节点发生故障会影响到其它节点之间复制日志的转发
-
全连接拓扑的问题由于某些网络链路比其他链路更快可能导致复制日志之间的覆盖
-
-
-
无主节点复制
-
允许任何副本直接接受来自客户端的写请求
-
节点失效时写入数据库
-
假设有3个副本,有两个可用于接收写请求,用户只要收到两个确认的回复之后,即可认为写入成功
-
读修复与反熵
-
当失效节点重新上线是如何修复
- 读修复:在客户端并行读取时可以检测到过期的返回值,客户端将新值写入到旧副本
- 反熵过程:有些数据存储有后台进程不断查找副本之间的差异,将任何缺少的数据从一个副本复制到另一个副本
-
-
读写quorum
-
有n个副本,写入需要w个节点确认,读取必须至少查询r个节点,则只要w+r>n
- 在Dynamo风格的数据库中,最常见的选择是n=3或5,w=r=(n+1)/2上取整
- max(n-w,n-r)确定了容忍失效的节点数
-
-
-
Quorum一致性的局限性
-
局限性
-
如果采用了sloppy quorum,写操作的w节点和读取的r节点可以不会出现重叠的节点
- 🔗宽松的quorum与数据回传
-
如果两个写操作同时发生,无法明确先后顺序
- 🔗处理写冲突
-
如果读写操作同时发生,写操作可能仅在一部分副本上完成,则读取返回新值还是旧值存在不确定性
-
如果发生部分写入失败,则已成功的副本不会回滚
-
如果具有新值的节点后来发生失效,但恢复数据来自某个旧值,则总的新值副本会低于w,打破了之前的判定条件
-
-
监控旧值
- 无主节点的系统没有固定的写入顺序,无法比较偏移量的差值,监控非常困难
-
宽松的quorum与数据回传
- 在大规模集群中,如果网络中断切断客户端到多数数据库节点的连接,则可以返回错误,
- 也可以使用放松的仲裁(sloppy quorum)
-
多数据中心操作
- 跨数据中心时,客户端通常只等待本地数据中心内的 quorum 节点数的确认
- 也可以将数据库节点之间的通信限制在一个数据中心内,跨数据中心的复制在后台异步执行,类似于多主节点复制分割
-
-
检测并发写
-
如果客户端A和B同时向3个节点上发起请求
- 如果节点1只收到A的写请求,节点2和3分别收到AB的请求但顺序相反
- 节点2认为X的最终值是B而其它节点则认为是A
-
最后写入者获胜(丢弃并发写入),LWW
- 我们无法知道哪个先发生,但可以用一个明确的方法来确定哪一个写入是最新的,如果每个请求都附带时间戳
-
Happens-before关系和并发
- 如果A、B之间存在因果关系则后面的操作可以覆盖较早的操作,如果属于并发,就需要解决冲突
-
确定先后关系
-
服务器判断操作是否并发的依据主要依靠对比版本号,而并不需要解释新旧值本身
-
工作流程如下类似CAS操作,要求写之前必须先读,获取最新的版本号
-
当服务器收到某个请求时,可以覆盖更低版本号的所有值
-
与CAS的差异是必须保存更高版本号的所有值,因为属于并发
- 保存版本更高的所有值仅适用于无主节点复制
-
-
-
合并同时写入的值
- 在应用程序中合并并发值需要额外工作,例如购物车中的商品应该合并
- 删除操作在合并时可能导致错误,因此需要墓碑机制
-
版本矢量
-
单个版本号不足以捕获操作之间的依赖关系,需要为每个副本和主键定义版本号,形成版本矢量
-
具体来说,如果存在两个版本矢量V1和V2,系统会逐个比较它们中对应节点的逻辑修改时间。如果V1中所有节点的逻辑修改时间都不小于V2中对应节点的逻辑修改时间,并且至少有一个节点的逻辑修改时间大于V2中对应节点的逻辑修改时间,那么就可以确定V1比V2新。
- 如果V1在所有节点的逻辑修改时间都>=V2,则V1比V2新
-
-
-
第6章 数据分区
-
每一条数据只属于某个特定分区,每个分区都视为一个完整的小型数据库,目的是提高扩展性
-
数据分区与数据复制
- 第5章的所有原理均适用于这里
-
键值数据的分区
-
如果分区均匀,那么理论上10个节点能处理10倍的数据量和10被的读写吞吐量
-
但如果分区不均匀,向某些节点倾斜,则会导致分区效率严重下降
- 避免倾斜的最简单方法是随机分配key,但缺点是找不到key在哪个节点上
-
基于关键字区间分区
-
就像字典一样,A-B分配在第1页,H-K分配在第4页
-
未必需要均匀分布:
- 分区边界可以由管理员手动确定,或者由数据库自动选择
-
每个分区内按照关键字排序,便于区间查询
-
缺点是某些访问模式会导致热点,比如按照时间分区则所有写入操作都集中到同一个分区(当前的分区)
- 为了避免问题可以在时间戳前面加上writer名称作为前缀
-
-
基于关键字哈希值分区
-
可以将关键字均匀分配到多个分区中,分区边界可以均匀的,也可以伪随机选择
-
丧失了区间查询特性,如果进行区间查询则会把区间查询发送到所有分区上
-
cassandra的表可以声明复合主键,第一列用来分区,其它列用来排序
- 例如(user_id, update_timestamp),可以实现对于某一用户高效查询
-
-
负载倾斜与热点
-
哈希分区可以减轻热点,但无法做到完全避免,比如所有读写操作都针对同一个key
- 这种高度倾斜的负载很难自动消除
-
一个简单方法是在key中添加一个随机数,将写操作分配到100个不同关键字上
- 缺点是任何读取操作都需要从100个关键字中读取然后合并
-
-
-
分区与二级索引
-
二级索引通常只用来加速特定值的查询
-
基于文档分区的二级索引
-
一种方式是在每个文档内维护二级索引,成为本地索引
- 在查询时需要合并所有分区的结果
- 缺点是容易导致度延迟显著放大(延迟取决于最慢的一个)
-
-
基于词条的二级索引分区
-
创建全局索引但对全局索引进行分区,可以与数据关键字采用不同的分区策略
- 读取时速度快,不需要查询每个节点
- 写入时有写放大,因为可能会涉及多个二级索引
-
-
-
分区再平衡
-
随着查询压力,数据规模和节点故障的变化,要求数据和请求转移到其它节点
-
分区再平衡的要求
- 平衡之后,负载、存储、请求更均匀地分布
- 再平衡执行过程中可以正常提供读写服务
- 避免不必要的负载迁移
-
动态再平衡的策略
-
为什么不用取模?
- 如果节点数N发生变化则很多关键字都要迁移节点,频繁地迁移操作大大增加了再平衡的成本
-
固定数量的分区
-
首先创建远超实际节点数的分区数,然后为每个节点分配多个分区,比如每个节点承担100个分区
- 在新增节点时仅需要调整分区与节点的对应关系
-
-
动态分区
- 有些数据库可以配置参数阈值,如果超过10GB就拆分成两个分区
- 预分裂可以缓解在到达第一个分裂点之间,请求集中在一个节点的问题:如果是关键字区间分区,预分裂要求已经知道一些关键字的分布情况
-
按节点比例分区
-
使分区数和集群节点数成正比关系
- 每个节点具有固定数量的分区
-
每次分裂时从随机节点上拿走一半的分区
-
-
-
自动与手动再平衡操作
- 全自动式的再平衡
- 纯手动方式(管理员手动配置)
- 过渡阶段(自动生成分区建议方案,然后要管理员手动确认)
-
-
请求路由
-
如何将请求路由到节点上
- 允许客户端连接任意的节点,例如采用循环式的负载均衡器
- 将所有客户端请求发送到一个路由层
- 客户端感知分区和节点分配关系
-
确保参与者的共识
- 许多系统使用独立的协调服务(zookeeper)来跟踪集群元数据
- 少数系统使用节点间的gossip协议同步集群状态变化,允许请求发送到任意节点并转发至目标分区节点
-
-
并行查询执行
-
对于主要用于数据分析的关系型数据库,查询类型要复杂的多
- 🔗参见第10章批处理系统
-
第7章 事务
-
深入理解事务
-
ACID的含义
-
原子性
- 原子性与多个操作的并发性不相关
- 在出错时中止事务,并将部分完成的写入全部丢弃。或许可中止性比原子性更为准确
-
一致性
-
在不同的场景中有不同含义
-
在副本一致性和异步复制模型中,关注最终一致性,数据是否最终会一致
- 🔗第5章复制滞后问题
-
在一致性哈希中是用于动态分区再平衡的方法
- 🔗第6章一致性哈希
-
在CAP理论中,一致性用来表示线性化
- 🔗第9章可线性化
-
在本章(ACID)中是指数据库处于应用程序的预期状态
-
-
要求应用程序正确地定义事务来保证一致性
-
-
隔离性
-
多个并发执行的事务相互隔离,不能相互交叉
-
错误解释
- 经典的数据库教材把隔离定义为可串行化,然而实践中很少使用串行化隔离
-
-
持久性
-
提供一个安全可靠的地方来存储数据,不用担心数据丢失
-
其实不存在完美的持久性
- 🔗第1章可靠性
-
-
-
单对象与多对象的事务操作
-
多对象事务是在一个事务中会修改多个对象,目的通常是为了在多个数据对象之间保持同步
-
单对象写入
- 原子性和隔离性也适用于单个对象的更新,如向数据库写入20KB,但发送10KB后网络中断,或者更新的过程中发生电源故障
-
多对象事务的必要性
- 对于关系数据模型,处理外键时;在图模型时,顶点有多个边连接到其他顶点
- 文档数据库缺乏join支持而导致反规范化时,就需要一次性更新多个文档
- 对于带有二级索引的数据库,每次更改值时都需要同步更新索引
-
处理错误与中止
-
支持安全的重试机制是处理错误的关键
-
如果因为网络问题导致事务重复执行,需要应用级的去重机制
- 系统过载时,重试可能加剧问题,应设置重试次数上限
- 永久性故障无需重试
- 重试产生的数据库外的副作用应该避免,比如发送电子邮件
-
如果涉及多个系统的事务提交,可能需要两阶段提交来确保一致性
-
-
-
-
弱隔离级别
-
多个事务同时访问或修改相同数据时可能引发竞争条件,这个并发错误难以通过测试发现,难以稳定复现,只在特定时刻触发
- 数据库通过事务隔离来隐藏并发问题,但实现强隔离会牺牲性能。
- 弱隔离级别只能防止一部分并发问题,并且实际上已经导致了大量并发错误。
-
读-提交
-
read-commited只提供以下两个保证:读写数据库时只读取/覆盖已成功提交的数据
-
防止脏读
-
事务的任何写入只有在成功提交了之后才能被其他人观察到
-
使用场景
- 事务需要更新多个对象
- 事务发生中止,所有的写入操作都要回滚
-
-
防止脏写
-
脏写是覆盖先前未提交事务一部分的写入,读-提交防止脏写通常的方式是推迟第二个写请求,直到前面的事务完成提交
-
使用场景
- 事务需要更新多个对象
-
缺点:仍然会导致更新丢失
- 🔗防止更新丢失
-
-
实现读-提交
-
防止脏写:数据库通常使用行级锁来防止脏写
-
防止脏读:
- 一种方式是使用相同的锁,但影响性能
- 目前大多数数据库采用的方法是,对于待更新对象,维护旧值和当前持锁事务要设置的新值
-
-
-
快照级别隔离与可重复读
-
一个事务内两次相同的查询得到了不同的结果,成为不可重复读或者读倾斜
-
主要用于的场景:需要知道数据库在某个时刻点所冻结的一致性快照
-
实现快照级别隔离
- 防止脏写:与前面一样通过写锁
- 为了实现快照隔离,数据库采用了MVCC
-
一致性快照的可见性原则
- 每笔事务开始时,数据库列出所有尚在进行中的其它事务,然后忽略这些事务完成的部分写入,即不可见
- 较晚事务ID所做的任何修改不可见,不管这些事务是否完成了提交
- 除此之外,其它所有的写入都对应用查询可见
-
索引与快照级别隔离
- MVCC如何支持索引?
- 一种方式:让索引指向所有版本,然后把不同版本放在同一个内存页面上
- 另一种方式:让每个写入事务都创建一棵新的B-tree root,有修改的页从叶子到root都要创建新副本,利于读
-
可重复读与命名混淆
- SQL标准对隔离级别的定义存在缺陷,现在可重复读代表什么已经不清楚了
-
-
防止更新丢失
-
避免写事务并发导致更新丢失
- 场景:递增计数器、更新复杂对象的一部分
-
原子写操作
- 一些简单操作是原子更新的,比如 update t set count = count + 1 where key='foo'
- 原子操作通常采用独占加锁的方式实现
-
显式加锁
- select * from figures where game_id=222 for update;
- 这种方法是可行的。但很多代码会忘记在必要的地方加锁
-
自动检测更新丢失
- postgreSQL和oracle和SQL server都支持检测更新丢失,mysql不支持
-
原子比较和设置(CAS)
- update table set content='new' where conent='old'
- 只有在内容没有发生变化时才设置
-
冲突解决和复制
- 多副本时要采取措施防止并发更新
- 多版本冲突通常支持多个并发写,并保留版本冲突,由应用层或特定数据结构解决
-
-
写倾斜和幻读
-
定义写倾斜
-
可以将写倾斜定义成一种更广义的更新丢失问题
- 即如果两个事务读取相同的一组对象,然后更新其中一部分
- 不同的事务可能更新不同的对象,则可能发生写倾斜
- 更新相同的对象,则是更新丢失
-
-
更多写倾斜的例子
-
会议预定系统
- 在预定时首先检查时间段是否有冲突
-
多人游戏
-
声明一个用户检测冲突
-
-
为了产生写倾斜
-
所有写倾斜都遵循一个模式
- 首先输入一些查询条件,即采用select查询所有满足条件的行
- 根据查询的结果,应用层代码决定下一步操作
- 如果应用程序继续执行,将发起数据库写入
-
在一个事务中的写入改变了另一个事务查询结果的现象,称为幻读
-
-
实体化冲突
- 如果问题的关键是查询结果中没有对象可以加锁,可以认为引入一些可加锁的对象
- 这种方法称为实体化冲突,把幻读问题转变为针对数据库中一组具体行的锁冲突问题
-
-
-
串行化
-
可串行化采用了一下三种技术之一
- 严格按照串行顺序执行
- 两阶段锁定
- 乐观并发控制技术
-
实际串行执行
-
单线程执行有时可能会比支持并发的系统效率更高,尤其是可以避免锁开销
-
采用存储过程封装事务
- 数据库早期将用户操作序列作为单一事务处理(要等待用户交互)
- 后来OLTP应用避免在事务中等待用户交互
-
存储过程的优缺点
- 存储过程语言不通用;在db中运行代码难以管理,调试困难;
-
分区
- 串行执行所有事务使得并发控制更加简单,但db的吞吐量被限制在单核
- 为了扩展到多核和多节点,可以对数据进行分区
- 存储过程需要跨越所有分区加锁执行,以保证可串行化,跨区事务吞吐量比单机降低了好几个数量级
-
串行执行小结
- 事务必须简短高效
- 仅限活动数据集能完全加载到内存的场景
- 写入吞吐量必须足够低
-
-
两阶段加锁
-
两阶段加锁与两个阶段提交是完全不同的东西
-
两阶段加锁的强制性更高,多个事务可以同时读取同一个对象,但只要出现任何写操作都必须加锁独占访问。
-
若事务A读取了某对象,事务B想写入对象,则要等A结束;如果A修改了对象,B想要读取对象,则要等A结束。
-
两阶段的含义似乎是锁的两阶段:共享锁和独占锁
-
实现两阶段加锁
-
mysql的可串行化隔离使用了两阶段加锁
-
基本原理
- 要读取对象必须获得共享锁
- 要修改对象必须获取独占锁
- 如果事务首先读取对象,然后尝试写入对象,则需要将共享锁升级独占锁
- 事务持有锁直到事务结束
-
-
两阶段加锁的性能
- 事务吞吐量和响应时间下降很多,原因主要是降低了事务并发性
-
谓词锁
-
可串行化隔离必须防止幻读问题
-
谓词锁会限制如下访问
- 查询时需要获取共享锁
- 增删改时需要检查是否与任何谓词锁冲突,并等待冲突事务完成后继续
-
-
索引区间锁
- 谓词锁性能不佳,如果事务中存在很多锁,检查匹配这些锁非常耗时,大多数2PL数据库采用索引区间锁(next-key locking)
- 索引区间锁会锁定更大范围的对象。
-
-
可串行化的快照隔离
-
SSI算法,serializable snapshot isolation
-
悲观与乐观的并发控制
- SSI是一种乐观并发控制,如果可能发生潜在冲突,事务会继续执行而非中止,除非检查后确实发生了冲突,就中止并重试
-
基于过期的条件做决定
-
参考前面的写倾斜,事务首先查询数据再根据查询结果做后续操作,但事务可能已被修改
- 🔗写倾斜和幻读
-
db如何判断查询结果发生变化性?
- 读取是否作用于一个(即将)过期的MVCC对象
- 检测写入是否影响即将完成的读取
-
-
检测是否读取了过期的MVCC对象
- 在事务中,数据库需要追踪那些由于MVCC可见性规则而被忽略的写操作,如果有则中止事务
-
检测是否影响了之前的读
- 这里使用索引区间锁,但SSI锁不会阻塞其它事务
-
可串行化快照隔离的性能
- SSI相比两阶段加锁,优势在于无需等待锁
- 此外还能突破CPU核限制,将冲突检测部署在多台机器上
-
-
第8章 分布式系统的挑战
-
故障与部分失效
-
对于分布式系统,理想化的标准正确模型不再适用,各种各样的事情都可能出错
-
云计算和超算
-
构建大规模计算系统时,高性能计算和云计算是两个极端
- HPC适用于计算密集型任务
- 云计算满足在线服务和弹性资源需求
-
在错误处理上
- HPC常采用快照保存任务状态,故障时停止整个集群任务
- 互联网服务需持续在线,不能停机修复
-
-
-
不可靠的网络
-
当通过网络发送数据包时,数据包可能丢失或者延迟;同样回复也可能会丢失或延迟。所以如果未收到回复,并不能确定消息是否发送成功
-
现实中的网络故障
- 网络问题出人意料地普遍
-
检测故障
-
超时与无限期的延迟
- 较长的延迟需要更长的等待,较短的延迟可能会出现误判
- 网络拥塞与排队
-
同步与异步网络
- 网络延迟是否可预测?
-
-
不可靠的时钟
-
节点的时钟可能会与其它节点存在明显的不同步,时钟还可能会突然向前跳跃或者倒退,依靠精确的时钟存在一些风险,没有特别简单的方法来精确测量时钟的偏差范围
-
单调时钟与墙上时钟
-
墙上时钟
- 根据某个日历返回当前的日期时间
-
单调时钟
-
-
时钟同步与准确性
-
依赖同步的时钟
- 时间戳与事件排序
- 时钟的置信区间
- 全局快照的同步时钟
-
进程暂停
- 进程可能在执行过程中的任意时候遭遇长度未知的暂停(一个重要的原因是垃圾回收),结果它被其它节点宣告为失效,尽管后来又恢复执行,却对中间的暂停毫无所知
- 响应时间保证
- 调整垃圾回收的影响
-
-
知识、真相与谎言
-
真相由多数决定
- 主节点与锁
- Fencing令牌
-
拜占庭故障
- 弱的谎言形式
-
理论系统模型与现实
- 算法的正确性
- 安全性与活性
- 将系统模型映射到现实世界
-
第9章 一致性与共识
-
一致性保证
- 大多数多副本数据库都提供了最终一致性,但它无法告诉我们系统何时会收敛,当系统出现故障时最终一致性的临界条件或错误才会对外暴露出来
-
可线性化
-
基本思想是让系统看起来好像只有一个数据副本
-
如何达到可线性化?
-
可线性化是什么
- 在写入操作之前,读到旧值;在写入操作之后,读到新值
- 最重要的约束:一旦某个读操作返回了新值,之后所有的读都必须返回新值
-
可线性化就是可以把不同客户端上的事件连成一条线并且不违背约束
-
-
线性化的依赖条件
-
什么场景应该用线性化
- 子主题 1
-
加锁与主节点选举
- 不管锁如何实现,都必须满足可线性化:所有节点必须同意哪个节点持有锁
-
约束与唯一性保证
- 用户名或电子邮件必须唯一标识一个用户
-
跨时间通道的依赖
-
-
实现线性化系统
-
主从复制(部分支持可线性化)
-
共识算法(可线性化)
-
多主复制(不可线性化)
-
线性化与quorum(可能不可线性化)
-
默认情况下不支持,如果愿意降低性能
-
方案1:在读取返回结果给应用之前必须同步执行读修复;在写入之前,必须读取quorum节点获取最新值
- 🔗第5章 读修复与反熵
-
Cassandra确实会等待读修复完成,但是采用最后写入获胜的解决方案,因此会丧失线性化
-
-
最安全的假定是类似Dynamo风格的无主系统无法保证线性化
-
-
-
线性化的代价
-
如果两个数据中心之间发生网络中断,会发生什么情况
- 对于多主复制的数据库,每个数据中心内部都可以正常运行
- 对于主从复制系统,无法完成写入和线性化读取
-
CAP理论
-
即使在一个数据中心内部,只要有不可靠的网络,都会发生违背线性化的风险,可以做以下权衡
- 如果应用要求线性化,当出现网络问题时必须等待网络修复
- 如果应用不要求线性化,那么断开连接之后服务可用,但行为结果不符合线性化
-
CAP最初一个经验法则,没有准确定义。
- CAP在事实上鼓励大家探索无共享系统(倾向于可用而非一致)
- CAP只考虑了一种一致性模型(线性化)和一种故障模型(网络分区)
-
-
可线性化与网络延迟
-
现代多核CPU上的内存是非线性化,除非使用内存屏障,是为了提高性能。原因是每个CPU核都有独立的cache和寄存器,所有修改都异步刷新到主存。
- CAP理论不适用于当今的多核-内存一致性模型,放弃线性化的原因是为了性能而不是容错
- P是分区容错,
-
分布式数据库也一样,不支持线性化是为了提高性能,而不是为了保持容错性能
-
-
-
-
顺序保证
-
线性化寄存器对外呈现好像只有一份数据拷贝
-
🔗第五章 处理写冲突
- 确定复制日志的写入顺序
-
第七章的可串行化则是确保事务的执行结果按照某种顺序执行一样
-
🔗第八章 依赖于同步的时钟
- 时间戳与时钟试图将顺序引入无序的操作世界
-
-
顺序与因果关系
-
之前反复提到顺序的引用是有助于保持因果关系
-
如果系统服从因果关系所规定的顺序,称之为因果一致性
- 例如快照隔离提供了因果一致性。当从db中读数据时如果查到了某些数据,也一定能看到触发该数据的前序事件
-
因果顺序并非全序
- 并不是任意两个事件都能比较
-
可线性化强于因果一致性
- 任何可线性化的系统都将正确地保证因果关系
- 可线性化的任意两个事件都能比较
- 因果一致性可以认为是,不会由于网络延迟而显著影响性能,又能对网络故障提供容错的最强的一致性模型
-
捕获因果依赖关系
-
实现方法与检测并发写的方法类似,检测对同一个主键的并发写请求
- 🔗第五章 检测并发写
-
因果一致性要更进一步,追踪整个数据库请求的因果关系
-
-
-
序列号排序
-
采用算法产生一个数字序列用以识别操作,通常是递增的计数器
-
非因果序列发生器
- 每个节点独立产生一组序列号,缺点是无法捕获跨节点的操作顺序
-
Lamport时间戳
-
每次只跟踪见到的最大计数器值
-
有时会与版本向量发生混淆
- 🔗第五章 检测并发写
- 版本矢量是每个节点都持有其它节点的版本号(大部分是旧的,用来检测冲突)
- Lamport每个节点都只有自己的版本号
-
-
时间戳排序依然不够
- 比如对于创建用户名的请求,由于可能无法获取全部节点的时间戳所以找不到最早的请求。
- 因为无法确定这些操作是否发生,何时确定等
-
-
全序关系广播
-
全序关系广播需要满足两个基本安全属性
- 没有消息丢失(一定要发送到所有节点上)
- 消息以相同的顺序发送到每个节点上
-
如果网络发生故障,算法要不断重试
-
使用全序关系广播
- 全序关系广播是数据库所需要的
- 理解全序关系广播的另一种方式是将其视为日志
- 全序关系广播对于提供fencing令牌的锁服务也很有用,每个获取锁的请求都附加到日志中,所有消息按照按照日志中的顺序存放
-
采用全序关系广播实现线性化存储
-
可线性化与全序关系广播是否相同?
- 不完全是,全序关系广播相当于共识,共识与线性化有密切的相关性
-
全序关系广播基于异步模型
- 保证消息以固定的顺序可靠地发送,但不保证消息何时发送成功
-
可线性化强调就近性
- 可线性化是:读取时保证能够看到最新的写入值
-
如果有了全序关系广播,就可以在上面构建线性化的存储系统,步骤:
-
- 在日志中追加一条消息,并指明想要的用户名
-
-
读取日志,将其广播给所有节点,等待回复
- 读取日志是重新申明我需要那个用户名吗
-
-
- 检查是否有消息声称用户名已占用,如果第一条回复来自当前节点,成功;来自其它节点,失败
-
-
虽然此过程可以保证线性化写入但不能保证线性化读取
- 其它节点读到的可能是旧值
-
-
采用线性化存储实现全序关系广播
- 最简单的方法是在寄存器上存储一个计数,然后使其支持原子自增读取操作或原子CAS操作
-
-
-
分布式事务与共识
-
共识是让多个节点就某个值达成一致,应用场景
- 主节点选举
- 原子事务提交:对于支持跨节点事务的数据库,需要每个节点对事务是否提交达成一致
-
原子提交与两阶段提交
-
从单节点到分布式的原子提交
- 在单节点上如果日志记录写之前发生了崩溃,则事务需要中止。
- 在分布式事务中,仅仅向其它节点发送提交请求让它们独立执行是不够的,因为网络、节点崩溃或违反约束等其他情况可能导致节点提交状态的不一致
- 已提交的事务不可撤销,但可以通过新的事务抵消已提交事务的效果
-
两阶段提交
- 2PC引入了新组件——协调者来管理事务。
- 当准备提交事务时,协调者开始阶段1:向所有参与者发出准备请求,若参与者回复是,则会在阶段2:发出提交请求
-
系统的承诺
- 两阶段保证跨节点的原子性的关键在于其独特的两个阶段和承诺机制
- 在准备阶段:如果做出了提交的承诺,则无法返回;在提交阶段,一旦提交不可撤销
-
协调者发生故障
- 在准备阶段,如果协调者收到任何准备请求失败或超时都会中止事务;在提交阶段,如果提交或中止请求失败,协调者会不断重试
- 一旦参与者收到准备请求并投票“是”,便不能单方面放弃,若协调者在作出决定后崩溃但参与者未收到提交请求,就会陷入不确定状态。
- 协调者必须在发送提交请求前将决定写入磁盘日志。
-
三阶段提交
- 目前3PC假定一个有界的网络延迟和节点在规定时间内响应
-
-
实践中的分布式事务
-
分布式事务由于操作缺陷、性能问题以及承诺的可靠性问题而备受批评。许多云服务提供商因运维难题而选择不支持分布式事务。
-
区分分布式事务的定义
- 数据库内部是指跨数据库节点的内部事务通常可行且工作良好
- 异构分布式事务涉及不同技术实现的参与者,其原子提交充满挑战。
-
Exactly-once消息处理
- 例子:当且仅当数据库中处理消息的事务重新提交,消息队列才会标记该消息已经处理完毕。
- 只有在所有受影响系统都是用相同的原子提交协议的前提下,异构的分布式事务才是可行的,例如邮件服务器不支持两阶段提交,因为不能重试。
-
XA交易
- XA是异构环境下实施两阶段提交的工业标准
-
停顿时仍持有锁
- 陷入停顿的参与者节点仍持有锁,这会影响其它事务的执行。
- 在两阶段提交的过程中,如果协调者崩溃或日志丢失,参与者可能永久保持在锁定状态。
-
从协调者故障中恢复
- 在实践中可能出现协调者恢复失败、悬而未决等问题,这是需要管理员手动介入。
- XA事务实现启发式决策,允许参与者单方面决定事务进程,可能破坏原子性,仅适用于应急
-
分布式事务的限制
- 如果协调者不支持数据复制则它就是系统的单点故障
- 协调者集成到应用服务器后服务器不再无状态
- 无法深入检测不同系统之间的思索条件
- 分布式事务有扩大系统失败的风险,违反了构建容错系统的目标
-
-
支持容错的共识
-
共识算法通常形式化描述如下
- 一个或多个节点可以提议某些值,由共识算法来决定最终值
-
共识算法必须满足以下性质
- 协商一致性:所有节点都接受相同的协议
- 诚实性:所有节点不能反悔
- 合法性:如果决定了值v,v一定是由某个节点所提议的
- 可中止性:节点如果不崩溃最终一定可以达成决议
-
共识算法与全序广播
- 现在的容错式共识算法通常采用全序关系广播来决定一系列值的顺序
-
主从复制与共识
- 共识算法用于选举主节点,但看似陷入要选举主节点需要先有主节点的循环。
- 实际上可以在在现有节点正常工作时进行复制,主节点失效时选举新主节点
-
Epoch和Quorum
- 主节点通过epoch确保每个epoch内主节点唯一
- 通过收集quorum节点投票来决策
- 存在两轮投票:选主节点和对主节点进行提议投票
-
共识的局限性
- 节点投票是同步复制过程,影响性能
- 多数算法假定固定节点集,不支持动态成员资格
- 共识系统依赖网络有较低延迟
-
-
成员与协调服务
- zookeeper和etcd这类项目与数据库相比实现了共识算法,不仅提供全需广播,还具备线性化的原子操作、操作全序、故障检测和更改通知等。
- 节点任务分配
- 服务发现
- 成员服务
-
第三部分 派生数据
第10章 批处理系统
-
使用UNIX工具进行批处理
-
简单日志分析
-
命令链与自定义程序
-
排序与内存中聚合
- Linux中的sort可以自动外部排序,可以自动并行化
-
-
UNIX设计哲学
-
统一接口
- 需要程序使用相同的数据格式才可以将程序连接到一起
-
逻辑与布线分离
- 将输入/输出的布线连接与程序逻辑分开
-
透明与测试
-
-
-
MapReduce与分布式文件系统
-
MapReduce以简单直接的方式处理大量数据,与UNIX进程类型,不会修改原始输入。在HDFS上操作。
-
MapReduce作业执行
-
MapReduce的分布式执行
-
调度器会尝试在输入文件副本的某台机器上运行mapper任务,称为计算靠近数据
- map任务的数量由输入文件块的数量决定
-
框架根据关键字的哈希值确定哪个reduce任务接受特定的键值对
- reducer数量由人工配置
-
键值对必须排序
-
首先在map阶段使用类似第3章的技术进行排序,并按reducer对输出进行分块
- 🔗 第3章 SSTables和LSM-Trees
-
reducer从mapper中获取文件并合并在一起
-
-
-
MapReduce工作流
- 将MapReduce作业连接起来
- 通过将每个作业输出到中间文件,称为中间状态实体化
-
-
Reduce端的join与分组
-
在许多数据集中,通常一条记录会与另一条记录存在关联;在批处理的背景下讨论join,主要解决数据集内存在关联的所有事件
-
示例:分析用户活动事件
- 示例:将用户活动事件与用户数据库相关联
- 最简单的实现方式是在(远程db中的)用户数据库中查询每个遇到的用户ID,但性能会非常差
- 因此更有效的方式是获取用户数据库放在HDFS的一组文件中,用户活动记录放到另一组文件中
-
排序-合并join
- 两组mapper扫描分别扫描两种数据,并且都用用户id为key,然后相同id的数据都进入相同reducer分区中
- 也可以使用次级排序:首先看到用户数据库中的记录再按照时间戳顺序查询活动事件
-
把相关数据放在一起
- 排序-合并join的作用是把相关数据放在一起
-
分组
- 通过某个关键字如sql中的group by字句对记录进行分组,相同关键字称为一组
-
处理数据倾斜
-
如果单个关键字的数据量特别大,会导致严重的数据倾斜
-
补偿算法
- 首先通过抽样作业确定哪些属于热键
- 热键有关的记录随机选择reducer,其它key要基于哈希
-
另一种方法是使用map端join
-
-
-
map端join操作
-
reducer端join不需要对数据做任何假设,缺点是复制到reducer和合并reduce输入的代价非常昂贵
-
广播哈希join
- 如果大数据集与小数据集join,当小数据集可以完全放入内存时,可以把小数据集广播给每个mapper
- 另一种方法是把小数据集保存在本地磁盘上的只读索引中
-
分区哈希join
- 参与join的两个数据集都根据某个键进行分区
-
map端合并join
- 如果不仅根据某个键分区还按照关键字排序,则数据集是否能载入内存并不重要
-
具有map端join的MapReduce工作流
- 选择map或reduce端join会影响输出结构
-
-
批处理工作流的输出
-
批处理与分析更接近
-
生成搜索索引
- 索引一旦创建是不可变的
-
批处理输出键值
- 比如用于构建机器学习系统或者推荐系统
-
批处理输出的哲学
- 如果代码中引入了漏洞,可以简单地回滚到先前版本,然后重新运行job
- 如果map或reduce任务失败,框架会自动安排重试
- 相同的文件可以用作不同job的输入
-
-
对比Hadoop与分布式数据库
-
大规模并行处理MPP数据库更专注于在一个集群上并行执行SQL查询分析,而MapReduce更像一个可以运行任意程序的通用操作系统
-
存储多样性
- 数据库要求遵循特定的模型,而DFS是字节序列,Hadoop允许无差别存储再决定后续处理
- 数据转储方式减轻了解释数据的负担,将责任转给消费者
-
处理模型的多样性
- MPP只允许SQL表达式,MapReduce则可以自定义代码更灵活
-
针对频繁故障的设计
- MPP对故障敏感,遇到故障会中止查询,让用户重新提交查询
- MapReduce能容忍单个任务的失败
-
-
-
超越MapReduce
-
MapReduce模型本身存在一些问题,比如性能不佳
-
中间状态实体化
-
很多情况下,一个job的输出只能用于另一个job的输入,此时,DFS上的文件只是中间状态
-
完全实体化中间状态与UNIX管道相比有一些不利因素
- 最慢的job会拖慢工作流
- mapper通常是冗余的,因为mapper通常是为分区和排序阶段做准备
- 对于临时数据,把中间文件复制到多个节点大材小用
-
数据流引擎
- 数据流引擎将整个工作流视为一个作业处理,不分解成独立子作业
- 这些引擎提供了更灵活的功能组合方式
-
容错
- 流处理框架避免将中间状态写入HDFS,而是采取重新计算的方法应对错误
-
关于实体化的讨论
- 数据流引擎将更类似于UNIX管道,不需要将中间状态自行写入文件系统
-
-
图与迭代处理
-
MapReduce不适合迭代遍历边的法
-
Pregel处理模型
- 计算的批量同步并行(BSP)模型
- 一个顶点可以发送消息到另一个节点
-
容错
-
并行执行
-
-
高级API和语言
-
数据流API通常使用关系式构建块来表示计算
-
转向声明式查询语言
- 自动选择join算法
- 子主题 2
-
不同领域的专业化
- Spark和Flink之上实现了用于机器学习的各种算法
-
-
第11章 流处理系统
-
发送事件流
-
在流处理中,记录通常称为事件:一个小的、独立的、不可变的对象
- 事件可以被多个消费者读取
- 有专门的工具用来提供事件通知
-
消息系统
-
在发布/订阅模式中,不同的系统采取了不同的方法,下面两个问题可以作为区分方法
-
如果生产者发送消息的速度比消费者处理的快,会发生什么,一般有3种选择
- 丢弃消息,缓存在队列中,激活背压(back-pressure,禁止生产消息,例如TCP)
-
如果节点崩溃或暂时离线,是否会有消息丢失
-
-
生产者和消费者之间的消息传递
- 有的消息系统,生产者直连消费者
-
消息代理(消息队列)
- 系统可以适应不断变化的客户端(连接、断开和崩溃)
-
消息代理和数据库对比
- 消息传递成功后自动删除消息
- 消息系统假设队列很短
- 不支持二级索引和各种搜索数据的方式
-
多个消费者
-
多个消费者读取一个主题中的消息时,有两种主要的消息传递模式
-
负载均衡式
- 多个消费者共同分担一个主题
-
扇出式
- 消息传递给多个消费者
-
-
-
确认和重新传递
-
客户端必须在处理完消息后显示地告诉代理
-
当MQ没有收到确认,比如与客户端断开连接,MQ将消息重新传递给另一个消费者
-
重传行为与负载均衡结合时会对消息排序产生影响
- 为了避免问题,可以为每个消费者使用单独的queue
-
-
-
分区日志
-
人们对数据库的预期是永久保存,对MQ的预期是瞬间消息,将两者结合起来是日志消息代理
-
基于日志的消息存储
- MQ生产者将消息追加到日志末尾,消费者读取接收,日志可以在多台机器上进行分区,实现每秒数百万条消息的吞吐量
-
对比日志和传统消息系统
-
便于实现扇出式消息传递
-
为实现负载均衡,可以将分区分配给消费者组中的节点,缺陷
- 同一分区消息传递到同一节点,节点数受限于分区数
- 单消息处理缓慢阻塞该分区后续消息
-
高代价且需并行处理、消息排序不重要的场合,JMS/AMQP(消费完就删消息)更合适
-
消息顺序重要的情况下,基于日志的方法更好
-
-
消费者偏移量
- MQ通过偏移量了解那些消息已确认
-
磁盘空间使用
- 消费者落后太多会丢失消息,日志是一个有限大小的磁盘缓冲区
-
重新处理信息
- 日志方式的MQ便于重试
-
-
-
数据库与流
-
将数据流思想引入数据库有助于解决异构数据系统中的问题
-
保持系统同步
-
由于相同或相关的数据出现在多个不同的地方,因此他们需要保持相互同步
-
一种方式是进行完整数据库转储,就是批处理
-
另一种方式是双重写入
- 问题之一是竞争条件:两个进程都要写入A库和写入B库的同一个key,则会有顺序问题
- 问题之二是其中一个写入可能会失败
-
-
变更数据捕获
-
变更数据捕获CDC是将变化的数据复制到其它系统
-
实现变更数据捕获
- 有解析预写日志,有的通过解析binlog
-
初始快照
- 有了更改日志可以通过replay重建数据库的状态
-
日志压缩
- 日志可以仅保留最后一次写入值
-
对变更流的API支持
- 越来越多的数据库支持将变更流作为标准结构,而不是依赖反向工程
-
-
事件溯源
-
应用逻辑基于不可变事件构建,事件存储仅支持增加,记录用户行为,使应用演化和调试更容易
-
从事件日志导出当前状态
- 事件日志对用户并不直观,需要将日志转化为适合用户查看的状态,比如导出当前状态的快照
-
命令和事件
- 用户请求到达时是命令,命令可能会失败,命令被接受后才变成事件
- 不允许事件流消费者拒绝事件,所有验证都要在成为事件之前发生
-
-
状态,流与不可变性
-
状态是随着时间推移和变化的事件的结果
-
不可变事件的优势
- 追加日志有助于更轻松地诊断和恢复问题,捕获更多信息,对于后续分析和业务决策有重要价值
-
相同的事件日志派生多个视图
- 通过事件日志派生出多个面向读取的表示方式,类似于多个消费者从流中读取数据
- 将数据的写入形式和读取形式分开,并允许多个读取试图提供了很大的灵活性,这被称为命令查询责任分离(CQRS)
-
并发控制
- CDC的最大缺点是事件日志的消费者通常是异步的,所以用户发现写操作没有反映在读取视图中
- 一种方案是同步执行read视图的更新
-
不变性的限制
- 许多系统都依赖于不变性,比如MVCC,git
- 对于主要添加数据的负载,支持不变形相对容易;对于高更新和删除率的工作负载,不变历史数据可能导致数据庞大和碎片化问题
-
-
-
流处理
-
有了流之后可以做什么:将数据写入存储系统然后被查询;将事件推送给用户;可以合并或分裂成多个数据流
-
流处理的适用场景
-
复杂事件处理(CEP)
- 使用SQL这样的高级声明式查询语言或图形界面来描述应该检测到的事件模式。将查询提交给处理引擎,该引擎在流内部维护匹配所需的状态机
-
流分析
- CEP和流分析的界限有些模糊。流分析更关系面向大量事件的累计效果和统计指标
-
维护物化视图
- 对某个数据集导出一个特定的视图以便高效查询
-
在流上搜索
- 比如在流上搜索关于公司、产品或感兴趣主题的相关新闻,这是通过预先制定一个搜索查询来完成的
-
消息传递和RPC
-
第4章讨论了基于消息系统作为RPC的替代方式,例如actor模型
- 🔗第4章 基于消息传递的数据流
-
-
-
流的时间问题
-
流处理系统经常和时间打交道,例如“最后5分钟内的平均值”,但这种定义处理起来非常棘手
-
事件时间与处理时间
- 事件处理可能会因为多种原因而滞后,如排队、网络故障、性能问题等,流处理算法需要特殊处理
-
了解什么时候准备就绪
-
无法确定什么时候能收到特定窗口内的所有事件,或者是否还有一些事件尚未到来。
-
在一段时间没有看到任何新的事件之后,可以认为超时并宣布关闭该窗口
-
如果遇到了滞后事件
-
一种方法是忽略滞后的事件,因为正常情况下只是一小部分时间
- 可以将丢弃事件的数量作为metrics
-
发布一个更正:针对滞后事件的一个更新值
-
-
-
你用谁的时钟?
-
考虑一个场景:移动应用向服务器报告使用率事件
-
为了调整不正确的设备时钟,一种方法是记录3个时间戳
- 根据设备的时钟,记录事件发生的时间
- 根据设备的时钟,记录事件发送到服务器的时间
- 根据服务器时钟,记录服务器收到事件的时间
-
-
窗口类型
-
如何定义时间段即窗口,有几种常见类型
- 轮转窗口:每一分钟都属于一个窗口
- 跳跃窗口:相邻5分钟为一个窗口,窗口会重叠。可以通过先计算1分钟轮转窗口再聚合几个相邻的窗口,一次跳跃1分钟
- 滑动窗口:包含在某个间隔内发生的所有事件,比跳跃窗口平滑
- 会话窗口:将时间上紧密相关的事件分组在一起
-
-
-
流式join
-
流和流join
-
场景是将搜索结果和点击事件组合在一起
-
流处理操作需要维护状态:例如在最后一小时内发生的所有事件,按会话ID建索引
- 如果有匹配的事件,则发出一个派生事件来表明哪个搜索结果发生了单击
- 如果没有匹配到,则发出一个派生事件表明没有单击
-
-
流和表join
-
有几种方法
- 每次都根据用户ID从数据库中查询
- 将数据库方法加载到流处理器中
-
流处理器可以订阅数据表的更新日志和活动事件流,于是变成了流和流join
- 区别是表的join可以回溯到开始时间
-
-
表和表join(物化视图维护)
- 当用户发生不同事件时,维护收件箱
-
join的时间依赖性
- 每当税率变更时,就赋予一个新的标识符,发表页包括销售时的税率标识符。这种方式使join操作具有确定性,但无法进行日志压缩
-
-
流处理的容错
-
流处理不能像批处理一下重启任务
-
微批处理和校验点
- 将流分解成多个小块,当流重启时从检查点开始
-
重新审视原子提交
- 在更受限制的环境中,有可能实现有效的XA协议,是通过在流处理框架中管理状态更改和消息传递
-
幂等性
- 可以通过幂等实现容错和保留流处理操作的唯一性
- 即使某些操作不是天生具备幂等性,往往也可以使用一些额外的元数据使其变得幂等
- 当从一个处理节点切换至另一个节点时,可能需要必要的fencing措施
-
故障后重建状态
-
任何需要状态的流处理,例如基于窗口的聚合,必须确保故障发生后状态可以恢复
-
方法
- 一种是将状态保存在远程存储中并采取复制,但查询远程db可能很慢
- 另一种是将状态保存在本地,并定期复制
- 如果状态的窗口相当短,也可以从输入流开始重建
-
-
-
第12章 数据系统的未来
- 数据集成
- 分拆数据库
- 端到端的正确性