- 第一部分
- 第1章 可靠、可扩展与可维护的应用系统
- 第2章 数据模型与查询语言
- 关系模型与文档模型
- 数据查询语言
- 图状数据模型
- 第3章 数据存储与检索
- 数据库核心:数据结构
- 事务处理与分析处理
- 列式存储
- 数据编码与演化
- 数据编码格式
- 数据流模式
- 第二部分 分布式数据系统
- 第5章 数据复制
- 主节点与从节点
- 复制滞后问题
- 多主节点复制
- 无主节点复制
- 第6章 数据分区
- 数据分区与数据复制
- 键-值数据的分区
- 分区与二级索引
- 分区再平衡
- 请求路由
- 第7章 事务
- 深入理解事务
- 弱隔离级别
可靠性意味着即使发生故障,系统也可以正常工作。故障包括硬件(通常是随机的,不相关的)、软件(缺陷通常是系统的,更加难以处理)以及入为(总是很难避免时不时会出错)方面。容错技术可以很好地隐藏某种类型故障,避免影响最终用户。
可扩展性是指负载增加时,有效保持系统性能的相关技术策略。为了讨论可扩展性,我们首先探讨了如何定釐描述负载和性能。简单地以Twitter浏览时间线为例描述负载,并将响应时间百分位数作为衡批性能的有效方式。对于可扩展的系统,增加处理能力的同时,还可以在高负载情况下持续保持系统的高可靠性。
可维护性则意味着许多方面,但究其本质是为了让工程和运营团队更为轻松。良好的抽象可以帮助降低复杂性,并使系统更易于修改和适配新场景。良好的可 *** 作性意味着对系统健康状况有良好的可观测性和有效的管理方法。
第2章 数据模型与查询语言 关系模型与文档模型文档数据模型:模式灵活、局部性、一对多
关系模型:联结、多对一和多对多语义
数据查询语言- 声明式查询语言:SQL、关系代数
- 命令式查询语言
- Mapreduce既不是声明式查询语言,也不是一个完全命令式的查询API
如果数据大多是一对多关系(树结构数据)或者记录之间没有关系,那么文档模型是最合适的。关系模型能够处理简单的多对多关系,但是随着数据之间的关联越来越复杂,将数据建模转化为图模型会更加自然
属性图、三元存储
第3章 数据存储与检索 数据库核心:数据结构Bitcask是一种日志结构的存储引擎,并采用哈希表作为索引
SSTable要求按照key-value对的顺序按键排序,相比哈希索引的日志段,具有以下优点:
- 合并段更加简单高效(归并排序)
- 在文件中查找特定的键时, 不再需要在内存中保存所有键的索引
- 由于读请求往往需要扫描请求范围内的多个key-value对,可以考虑将这些记录保存到一个块中并在写磁盘之前将其压缩
存储引擎的基本工作流程如下:
- 当写入时,将其添加到内存中的平衡树数据结构中(例如红黑树)。这个内存中的树有时被称为内存表。
- 当内存表大千某个阈值(通常为几兆字节)时,将其作为SSTable文件写入磁盘。由于树已经维护了按键排序的key-value对,写磁盘可以比较高效。新的SSTable文件成为数据库的最新部分。当SSTable写磁盘的同时,写入可以继续添加到一个新的内存表实例。
- 为了处理读请求,首先尝试在内存表中查找键,然后是最新的磁盘段文件,接下来是次新的磁盘段文件,以此类推,直到找到目标(或为空)。
- 后台进程周期性地执行段合并与压缩过程,以合并多个段文件,并丢弃那些已被覆盖或删除的值。
上述方案可以很好地工作。但它还存在一个问题:如果数据库崩溃,最近的写入(在内存表中但尚未写入磁盘)将会丢失。为了避免该问题,可以在磁盘上保留单独的日志,每个写入都会立即追加到该日志,就像上一节所述。那个日志文件不需要按键排序,这并不重要,因为它的唯一目的是在崩溃后恢复内存表。每当将内存表写入SSTable时,相应的日志可以被丢弃。
相关存储引擎:LevelDB、RocksDB、Bigtable论文、Cassandra、Hbase
基于合并和压缩排序文件原理的存储引擎通常都被称为LSM存储引擎
布隆过滤器是内存高效的数据结构,用于近似计算集合的内容。如果数据库中不存在某个键,它能够很快告诉你结果,从而节省了很多对于不存在的键的不必要的磁盘读取。
分层压缩:LevelDB、RocksDB、Cassandra
大小分级:Hbase、Cassandra
B-trees
之前看到的日志结构索引将数据库分解为可变大小的段,通常大小为儿兆字节或更大,并且始终按顺序写入段。相比之下,B-tree将数据库分解成固定大小的块或页,传统上大小为4KB(有时更大),页是内部读/写的最小单元。这种设计更接近底层硬件,因为磁盘也是以固定大小的块排列。
对比B-tree和LSM-tree
尽管B-tree的实现比LSM-tree的实现更为成熟,然而由于LSM-tree的性能特点,LSM tree目前很有吸引力。根据经验,LSM-tree通常对千写入更快,而B-tree被认为对于读取更快。读取通常在LSM-tree上较慢,因为它们必须在不同的压缩阶段检查多个不同的数据结构和SSTable。
聚集索引、覆盖索引、多列索引、多维索引、全文搜索和模糊索引
内存数据库:redis、memcached
事务处理与分析处理OLTP与OLAP的对比
数据仓库包含公司所有各种OLTP系统的只读副本。从OLTP数据库(使用周期性数据转储或连续更新流)中提取数据,转换为分析友好的模式,执行必要的清理,然后加载到数据仓库中。将数据导入数据仓库的过程称为提取-转换-加载(Extract-Transform-Load, ETL)。
列式存储面向列存储的想法很简单:不要将一行中的所有值存储在一起,而是将每列中的所有值存储在一起。如果每个列存储在一个单独的文件中,查询只需要读取和解析在该查询中使用的那些列,这可以节省大量的工作。
内存带宽和矢量化处理
对于需要扫描数百万行的数据仓库查询,将数据从磁盘加载到内存的带宽是一大瓶颈。然而,这还不是唯一的瓶颈。分析数据库的开发人员还要关心如何高效地将内存的带宽用于CPU缓存,避免分支错误预测和CPU指令处理流水线中的气泡,并利用现代CPU中的单指令多数据(SIMD)指令。
除了减少需要从磁盘加载的数据釐之外,面向列的存储布局也有利于高效利用CPU周期。例如,查询引擎可以将一大块压缩列数据放入CPU的Ll缓存中,并以紧凑循环(即没有函数调用)进行迭代。对于每个被处理的记录,CPU能够比基于很多函数调用和条件判断的代码更快地执行这种循环。列压缩使得列中更多的行可以加载到Ll缓存。诸如先前描述的按位AND和OR的运算符,可被设计成直接对这样的列压缩数据块进行 *** 作。这种技术被称为矢量化处理。
数据编码与演化 数据编码格式Json、XML、Thrift、Protocol Buffers
数据流模式基于数据库的数据流、基于服务的数据流: REST和RPC、基于消息传递的数据流
远程过程调用(RPC)的问题
- 本地函数调用是可预测的,并且成功或失败仅取决于控制的参数。网络请求是不可预测的。请求或响应可能由于网络问题而丢失,或者远程计算机可能速度慢或不可用,这些问题完全不在控制范围之内。网络问题很常见,因此必须有所准备,例如重试失败的请求。
- 本地函数调用要么返回一个结果,要么抛出一个异常,或者永远不会返回(因为进入无限循环或者进程崩溃)。网络请求有另一个可能的结果:由于超时,它返回时可能没有结果。在这种情况下,根本不知道发生了什么:如果没有收到来自远程服务的响应,无法知道请求是否成功。我们将在第8章更详细地讨论此类问题。
- 如果重试失败的网络请求,可能会发生请求实际上已经完成,只是响应丢失的情况。在这种情况下,重试将导致该 *** 作被执行多次,除非在协议中建立重复数据消除(幕等性)机制。本地函数调用则没有这样问题。在第11章更详细地讨论幕等性。
- 每次调用本地函数时,通常需要大致相同的时间来执行。网络请求比函数调用要慢得多,而且其延迟也有很大的变化:情况好时,它可能会在不到1ms的时间内完成,但是当网络拥塞或者远程服务过载时,可能需要儿秒钟的时间才能完成相同 *** 作。
- 调用本地函数时,可以高效地将引用(指针)传递给本地内存中的对象。当发出网络请求时,所有这些参数都需要被编码成可以通过网络发送的字节序列。如果参数是像数字或字符串这样的基本类型,这没关系,但是对于较大的对象很快就会出现问题。
- 客户端和服务可以用不同的编程语言来实现,所以RPC框架必须将数据类型从一种语言转换成另一种语言。 因为不是所有的语言都具有相同的类型,所以最终可能会丑陋。例如,回想一下Javascript的数字大千2^53的问题(参阅本章前面的"JSON、XML与二进制变体”部分)。用单一语言编写的单个进程中不存在此问题。
与直接RPC相比,使用消息代理有以下几个优点
- 如果接收方不可用或过载,它可以充当缓冲区,从而提高系统的可靠性。
- 它可以自动将消息重新发送到崩溃的进程,从而防止消息丢失。
- 它避免了发送方需要知道接收方的IP地址和端口号(这在虚拟机经常容易起起停停的云部署中特别有用)。
- 它支持将一条消息发送给多个接收方。
- 它在逻辑上将发送方与接收方分离(发送方只是发布消息,并不关心谁使用它们)。
分布式Actor框架
Actor模型是用于单个进程中并发的编程模型。逻辑被封装在Actor中,而不是直接处理线程(以及竞争条件、锁定和死锁的相关问题)。每个Actor通常代表一个客户端或实体,它可能具有某些本地状态(不与其他任何Actor共享),并且它通过发送和接收异步消息与其他Actor通信。不保证消息传送:在某些错误情况下,消息将丢失。由于每个Actor一次只处理一条消息,因此不需要担心线程,每个Actor都可以由框架独立调度。
第二部分 分布式数据系统 第5章 数据复制 主节点与从节点主从复制的工作原理
- 指定某一个副本为主副本(或称为主节点)。当客户写数据库时, 必须将写请求首先发送给主副本, 主副本首先将新数据写入本地存储。
- 其他副本则全部称为从副本(或称为从节点)注l 。主副本把新数据写入本地存储后, 然后将数据更改作为复制的日志或更改流发送给所有从副本。每个从副本获得更改日志之后将其应用到本地, 且严格保持与主副本相同的写入顺序。
- 客户端从数据库中读数据时, 可以在主副本或者从副本上执行查询。再次强调,只有主副本才可以接受写请求;从客户端的角度来看, 从副本都是只读的。
同步复制与异步复制
实践中, 如果数据库启用了同步复制,通常意味着其中某一个从节点是同步的, 而其他节点则是异步模式。
配置新的从节点
处理节点失效
从节点失效:追赶式恢复
主节点失效: 节点切换
复制日志的实现
基于语句的复制:主节点记录所执行的每个写请求( *** 作语旬)并将该 *** 作语旬作为日志发送给从节点。
基于预写日志(WAL)传输:所有对数据库写入的字节序列都被记入日志。因此可以使用完全相同的日志在另一个节点上构建副本:除了将日志写入磁盘之外, 主节点还可以通过网络将其发送给从节点。
基于行的逻辑日志复制:关系数据库的逻辑日志通常是指一系列记录来描述数据表行级别的写请求,binlog
基于触发器的复制:触发器支持注册自己的应用层代码, 使得当数据库系统发生数据更改(写事务)时自动执行上述自定义代码。通过触发器技术, 可以将数据更改记录到一个单独的表中,然后外部处理逻辑访问该表, 实施必要的自定义应用层逻辑, 例如将数据更改复制到另一个系统。
复制滞后问题读自己的写
采用异步复制会导致读写不一致
基于主从复制的系统实现读写一致性(写后读一致性)的方案:
- 如果用户访问可能会被修改的内容, 从主节点读取;否则, 在从节点读取。这背后就要求有一些方法在实际执行查询之前, 就已经知道内容是否可能会被修改。例如, 社交网络上的用户首页信息通常只能由所有者编辑, 而其他人无法编辑。因此, 这就形成一个简单的规则:总是从主节点读取用户自己的首页配置文件,而在从节点读取其他用户的配置文件。
- 如果应用的大部分内容都可能被所有用户修改, 那么上述方法将不太有效, 它会导致大部分内容都必须经由主节点, 这就丧失了读 *** 作的扩展性。此时需要其他方案来判断是否从主节点读取。例如, 跟踪最近更新的时间, 如果更新后一分钟之内, 则总是在主节点读取;并监控从节点的复制滞后程度, 避免从那些滞后时间超过一分钟的从节点读取。
- 客户端还可以记住最近更新时的时间戳, 并附带在读请求中, 据此信息, 系统可以确保对该用户提供读服务时都应该至少包含了该时间戳的更新。如果不够新,要么交由另一个副本来处理, 要么等待直到副本接收到了最近的更新。时间戳可以是逻辑时间戳(例如用来指示写入顺序的日志序列号)或实际系统时钟(在这种情况下, 时钟同步又称为一个关键点, 请参阅第8章”不可靠的时钟")。
- 如果副本分布在多数据中心(例如考虑与用户的地理接近, 以及高可用性), 情况会更复杂些。必须先把请求路由到主节点所在的数据中心(该数据中心可能离用户很远)。
单调读
单调读一致性可以确保不会发生这种异常。这是一个比强一致性弱, 但比最终一致性强的保证。当读取数据时, 单调读保证, 如果某个用户依次进行多次读取, 则他绝不会看到回滚现象, 即在读取较新值之后又发生读旧值的情况。
实现单调读的一种方式是, 确保每个用户总是从固定的同一副本执行读取(而不同的用户可以从不同的副本读取)。例如, 基千用户ID的啥希的方法而不是随机选择副本。但如果该副本发生失效, 则用户的查询必须重新路由到另一个副本。
前缀一致读
该保证是说, 对于一系列按照某个顺序发生的写请求, 那么读取这些内容时也会按照当时写入的顺序。
多主节点复制系统存在多个主节点, 每个都可以接收写请求,客户端将写请求发送到其中的一个主节点上,由该主节点负责将数据更改事件同步到其他主节点和自己的从节点。
在一个数据中心内部使用多主节点基本没有太大意义, 其复杂性已经超过所能带来的好处。但是, 在以下场景这种配置则是合理的。包括:多数据中心,离线客户端 *** 作、协作编辑
处理写冲突
同步与异步冲突检测、避免冲突、收敛于一致状态、自定义冲突解决逻辑
无主节点复制客户端将写请求发送到多个节点上,读取时从多个节点上并行读取, 以此检测和纠正某些过期数据。
读修复:当客户端并行读取多个副本时, 可以检测到过期的返回值。例如, 在图5-10中,用户2345从副本3获得的是版本6, 而从副本l和2得到的是版本7。客户端可以判断副本3一个过期值, 然后将新值写入到该副本。这种方法主要适合那些被频繁读取的场景。
反熵过程:此外, 一些数据存储有后台进程不断查找副本之间数据的差异, 将任何缺少的数据从一个副本复制到另一个副本。与基千主节点复制的复制日志不同, 此反熵过程并不保证以特定的顺序复制写入, 并且会引入明显的同步滞后。
quorum
确定前后关系
服务器判断 *** 作是否并发的依据主要依靠对比版本号, 而并不需要解释新旧值本身(值可以是任何数据结构)。算法的工作流程如下:
- 服务器为每个主键维护一个版本号, 每当主键新值写入时递增版本号, 并将新版本号与写入的值一起保存。
- 当客户端读取主键时, 服务器将返回所有(未被覆盖的)当前值以及最新的版本号。且要求写之前, 客户必须先发送读请求。
- 客户端写主键, 写请求必须包含之前读到的版本号、读到的值和新值合并后的集合。写请求的响应可以像读 *** 作一样, 会返回所有当前值, 这样就可以像购物车例子那样一步步链接起多个写入的值。
- 当服务器收到带有特定版本号的写入时, 覆盖该版本号或更低版本的所有值(因为知道这些值已经被合并到新传入的值集合中),但必须保存更高版本号的所有值(因为这些值与当前的写 *** 作属于并发)。
分区通常是这样定义的, 即每一条数据(或者每条记录, 每行或每个文档)只属于某个特定分区。采用数据分区的主要目的是提高可扩展性。不同的分区可以放在一个无共享集群的不同节点上。这样一个大数据集可以分散在更多的磁盘上, 查询负载也随之分布到更多的处理器上。
数据分区与数据复制分区通常与复制结合使用, 即每个分区在多个节点都存有副本。这意味着某条记录属千特定的分区, 而同样的内容会保存在不同的节点上以提高系统的容错性。
键-值数据的分区基于关键字区间分区、基于关键字哈希值分区
Cassandra则在两种分区策略之间做了一个折中。Cassandra中的表可以声明为由多个列组成的复合主键。复合主键只有第一部分可用千哈希分区, 而其他列则用作组合索引来对Cassandra SSTable中的数据进行排序。因此, 它不支持在第一列上进行区间查询, 但如果为第一列指定好了固定值, 可以对其他列执行高效的区间查询。
分区与二级索引基于文档分区的二级索引: 每个分区完全独立, 各自维护自己的二级索引, 且只负责自己分区内的文档而不关心其他分区中数据。每当需要写数据库时, 包括添加, 删除或更新文档等, 只需要处理包含目标文档ID的那一个分区。
基于词条的二级索引分区:可以对所有的数据构建全局索引, 而不是每个分区维护自己的本地索引。而且,为避免成为瓶颈, 不能将全局索引存储在一个节点上, 否则就破坏了设计分区均衡的目标。所以, 全局索引也必须进行分区, 且可以与数据关键字采用不同的分区策略。
分区再平衡动态再平衡的策略
采取取模的方法,如果节点数N发生变化后,迁移成本太大
固定数量的分区:首先, 创建远超实际节点数的分区数, 然后为每个节点分配多个分区。例如, 对干一个10节点的集群, 数据库可以从一开始就逻辑划分为1000个分区, 这样大约每个节点承担100个分区。接下来, 如果集群中添加了一个新节点, 该新节点可以从每个现有的节点上匀走几个分区, 直到分区再次达到全局平衡。
动态分区:当分区的数据增长超过一个可配的参数阈值(Hbase上默认值是10GB) , 它就拆分为两个分区,每个承担一半的数据。相反, 如果大量数据被删除, 并且分区缩小到某个阈值以下, 则将其与相邻分区进行合并。
按节点比例分区:每个节点具有固定数量的分区。此时,当节点数不变时, 每个分区的大小与数据集大小保持正比的增长关系; 当节点数增加时, 分区则会调整变得更小。较大的数据量通常需要大量的节点来存储, 因此这种方法也使每个分区大小保持稳定。
请求路由 第7章 事务 深入理解事务ACID、base唯一可以确定的是“ 它不是ACID " , 此外它几乎没有承诺任何东西。
弱隔离级别欢迎分享,转载请注明来源:内存溢出
评论列表(0条)