目录
- 第一部分
- 可靠性、可扩展性、可维护性
- 硬件故障
- 描述负载
- 吞吐与延迟
- 可维护性
- 第二章 数据模型与查询语言
- 第三章
- 索引
- 哈希索引
- B-tree
- 事务
- 第三章 编码
- 第二部分、分布式数据系统
- 第五章 数据复制
- 单主从复制
- 节点失效
- 日志实现
- 复制滞后问题
- 多主节点复制
- 第六章、数据分区
- 3
第一部分
可靠性、可扩展性、可维护性
什么算是"数据密集型" (data-intensive )应用?
对于一个应用系统,如果“数据”是其成败决定性因素,包括数据的规模 、 数据的复杂度或者数据产
生与变化的速率等,我们就可以称为“数据密集型应用系统” ;
知其然,知其所以然
可靠性 (Reliability)
当出现意外情况如硬件、软件故障、人为失误等,系统应可以继续正常运转:虽然性能可能有所降低,但确保功能正确。
可扩展性 (Scalability)
随着规模的增长 ,例如数据量 、流量或复杂性,系统应以合理的方式来匹配这种增长。
可维护性 (Maintainability)
随着时间的推移,许多新的人员参与到系统开发和运维, 以维护现有功能或适配新场景等,系统都应高效运转。
硬件故障
硬件故障
当我们考虑系统故障时,对于硬件故障总是很容易想到 : 硬盘崩愤,内存故障,电网停电,甚至有人误拔掉了网线。任何与大型数据中心合作过的人都可以告诉你,当有很多机器时,这类事情迟早会发生。
有研究证明硬盘的平均无故障时间( MTTF )约为 10 ~ 50年。因此,在一个包括10,000个磁盘的存储集群中,我们应该预期平均每天有一个磁盘发生故障。我们的第 一个反应通常是为硬件添加冗余来减少系统故障率。 例如对磁盘配置RAID ,服务器配备双电源,甚至热插拔C PU , 数据中心添加备用电源、发电机等。当 一个组件发生故障,备用组件可以快速接管,之后再更换失效的组件。这种方法可能并不能完全防止硬件故障所引发的失效,但还是被普遍采用,且在实际中也确实可以让系统不间断运行长达数年。
由 于软件错误,导致当输入特定值时应用服务器总是崩愤。 例如 , 2012年6月 3 0
日发生闰秒,由于Linux 内核中的一个bug ,导致了很多应用程序在该时刻发生挂
起[9]。
描述负载
我们以Twitter为例,使用其2012年 l l 月 发布的数据。 Twitter的两个典型业务操作
是:
1、发推:用户可以快速推送新消息到所有的关注者,平均请求大约 4.6k/s,峰值约 12k/s。
2、主页时间线浏览:平均请求 300k/s 来查看关注对象的最新推。
Twitter扩展性的挑战重点不在于消息大小,而在于巨大的扇出结构:每个用户会关注很多人,也有许多粉丝。此时大概有两种处理方案:
1、将发送的新推插入到全局的推集合中,当用户查看时间线时,首先查找所有的关注对象,列出这些人的所有推 ,最后以时间为序来排序合井。
2、对每个用户的时间线维护一个缓存,当用户推送新推时,查询其关注者,将新推插入到每个关注者的时间线缓存中。因为已经预先将结果取出,之后访问时间线性能非常快。
Twitter在其第一个版本使用了方住 l ,但发现主页时间线的读负载压力与日俱增,系统优化颇费周折,因此转而采用第二种方法并加以改造 。普通人第二种,粉丝数多的第一种。
批处理系统如Hadoop 中 ,我们通常关心吞吐量,即每秒可处理的记录条数。
在线系统通常更看重服务的响应时间,即客户端从发送请求到接收响应之间的间隔 。
吞吐与延迟
响应时间和延迟容易说淆使用,但它们并不完全一样。
通常响应时间是客户端看到的 :除了处理请求时间外,还包括来回网络延迟和各种排队延迟 。
延迟则是请求花费在处理上的时间。
采用较高的响应时间百分位数很重要,因为它们直接影响用户的总体服务体验。亚马逊采用 99.9百分位数来定义其内部服务的响应时间标准,或许它仅影响 1000个请求中的 1个。但是考虑到请求最慢的客户往往是购买了更多的商品,因此数据量更大。换言之, 他们是最有价值的客户。让这些客户始终保持愉悦的购物体验显然非常重要 : 亚马逊还注意到,响应时间每增加100ms ,销售额就会下降了约 1 %,其他研究则表明,1s的延迟增加等价于客户满意度下降 16%。
可维护性
软件的大部分成本并不在最初的开发阶段,而是在于整个生命周期内持续的投入,这包括维护与缺陷修复,监控系统来保持正常运行、故障排查、适配新平台、搭配新场景、技术缺陷的完善以及增加新功能等。
可以从软件设计时开始考虑,尽可能较少维护期间的麻烦预测未来可能的问题,并在问题发生之前即使解决(例如容量规划)。制定流程来规范操作行为,并保持生产环境稳定 。
提供良好的文档和易于理解的操作模式,诸如“如果我做了X ,会发生Y”。
第二章 数据模型与查询语言
大多数应用程序是通过一层一层叠加数据模型来构建的 。
1.程序开发人员,观测现实世界,把人员、货物、行为等,通过对象或数据结构,以及操作这些数据结构的API来建模 。
2.当需要存储这些数据结构时,采用关系型数据库中的表、图模型、JSON或XML文档来表示。
3.数据库工程师接着决定用何种内存、磁盘或网络的字节格式来表示上述JSON/XML/关系/图形数据 。数据表示需要支持多种方式的查询、搜索、操作和处理数据。
4.在更下一层,硬件工程师则需要考虑用电流、光脉冲、磁场等来表示字节。
基本思想相同 : 每层都通过提供一个简洁的数据模型来隐藏下层的复杂性。
对象-关系不匹配
现在大多数应用开发都采用面向对象的编程语言 ,由于兼容性问题,普遍对SQL数据模型存在抱怨: 如果数据存储在关系表中 , 那么应用层代码中的对象与表、行和列的数据库模型之间需要一个笨拙的转换层。
面向对象编程语言中的数据结构是对象,每个对象可以包含属性和方法。
关系型数据库的数据结构是表,表由行(记录)和列(字段)组成。
数据类型映射问题:面向对象语言和关系型数据库使用不同的数据类型,比如Java的整数类型和MySQL的INT类型,或者Java的String类型和MySQL的VARCHAR类型。这就需要你在存储或者检索数据时进行类型转换,以确保数据能够正确地被处理。
Hibernate、MyBatis这些对象-关系映射( ORM )框架一定程度上可以简化这样的问题处理,但是他们并不能完全隐藏两个模型之间的差异。
关系模型所做的则是定义了所有数据的格式:关系(表)只是元组(行)的集合 ,仅此而已。没有复杂的嵌套结构, 也没有复杂的访问路径。可以读取表中的任何一行或者所有行,支持任意条件查询。
第三章
一个最基本的数据库只需做两件事情:向它插入数据时,它就保存数据;查询时,它应该返回那些数据。
比如:一个纯文本文件,其中每行包含一个key-value对 ,用逗号分隔。每次调用 db set 追加新内容到文件末尾,这样如果多次更新某个键,旧版本的值不会被覆盖。查询时去看文件中最后一次出现的键来找到最新的值。
索引
问题1:
如果日志文件保存了大量的记录,那么 db_get 函数的性能会非常差。每次想查找一个键, db_get必须从头到尾扫描整个数据库文件来查找键的出现位置,查找的开销是 O(n ),如果数据库的记录条数加倍,则查找需要两倍的时间。
解决1:为了高效地查找数据库中特定键的值 , 需要新的数据结构:索引它们背后的基本想法都是保留一些额外的元数据,这些元数据作为路标,帮助定位想要的数据。
哈希索引
一个最简单的索引实现就是哈希K-V,保存内存中的 hashmap ,把每个键一一映射到数据文件中特定的字节偏移量 ,这样就可以找到每个值的位置。每当在文件中追加新的key-value对,时,还要更新hashmap来反映刚刚写入数据的偏移量 (包括插入新的键和更新已有的键)。当查找某个值时,使用 hashmap来找到文件中的偏移量,即存储位置,然后读取其内容 。只需一次磁盘寻址,就可以将value从磁盘加载到内存。如果那部分数据文件已经在文件系统的缓存中,则读取根本不需要任何的磁盘I/O 。
问题:只追加到一个文件,那么如何避免最终用尽磁盘空 间 ?
解决:一个好的解决方案是将日志分解成一定大小的段,当文件达到一定大小时就关闭它,并将后续写入到新的段文件中。然后可以在这些段上执行压缩,并在执行压缩的同时将多个段合并在一起。现在每个段现在都有自己的内存哈希表 ,将键映射到文件的偏移量。 为了找到键的值,首先检查最新的段的 hashmap ;如果键不存在,检查第二最新的段,以此类推。由于合并过程可以维持较少的段数量 ,因此查找通常不需要检查很多 hashmap 。
问题:为什么不设计成新值直接覆盖旧值?
解决:追加和分段合并主要是顺序写,它通常比随机写入快得多。
哈希索引局限:1哈希表必须全部放入内存,2范围查询效率不高。
B-tree
B-tree将数据库分解成固定大小的块或页,传统上大小为4 KB ,页是内部读写的最小单元。这种设计更接近底层硬件,因为磁盘也是以固定大小的块排列。
用这些页面引用来构造一个树状页面 ,指定一页为 B-t ree的根:每当查找索引中的一个键时,总是从这里开始。该页面包含若干个键和对子页的引用。每个孩子都负责一个连续范围内的键,相邻引用之间的键可以指示这些范围之间的边界。
B-tree底层的基本写操作是使用新数据覆盖磁盘上的旧页。它假设覆盖不会改变页的磁盘存储位置, 也就是说,当页被覆盖时,对该页的所有引用保持不变。可以认为磁盘上的页覆盖写对应确定的硬件操作。在磁性硬盘驱动器上,这意味着将磁头首先移动到正确的位置,然后旋转盘面,最后用新的数据覆盖相应的扇区。对于SSD ,由于SSD必须一次擦除并重写非常大的存储芯片块,情况会更为复杂。
非易失性存储(NVM)在数据库中的作用主要体现在以下几个方面:
提高数据安全性:NVM具有非易失性,即在断电后数据不会丢失。这使得数据库在遇到意外断电或其他故障时,能够更好地保护数据的完整性和一致性。
提升读写性能:NVM的读写性能接近DRAM,特别是读取速度远快于写入速度。这意味着在数据库操作中,数据的读取可以更加迅速,从而提高整体的数据库性能。
减少数据寻道时间:NVM没有数据寻道时间,类似于SSD。这使得数据库在进行数据访问时,可以更快速地定位到所需的数据,减少等待时间
事务
数据库管理员通常不愿意让业务分析人员在OLTP数据库上直接运行临时分析查询,这些查询通常代价很高,要扫描大量数据集,这可能会损害并发执行事务的性能。
相比之下,数据仓库则是单独的数据库,分析人员可以在不影响OLTP操作的情况下尽情地使用。数据仓库包含公司所有各种OLTP系统的只读副本。
事实表:销售的产品ID、产品名、购买的客户ID、客户名、产品的分类、购买的日期…
维度表:产品表、客户表、产品分类表…
在大多数OLTP数据库中,存储以面向行的方式布局:来自表的一行的所有值彼此相邻存储。可以在表上使用索引,告诉存储引擎在哪里查找特定日期或特定产品的所有销售。
但是,面向行的存储引擎仍然需要将所有行从磁盘加载到内存中、解析它们, 井过滤出不符合所需条件的行。这可能需要很长时间 。
面向列存储的想怯很简单:不要将一行中的所有值存储在一起,而是将每列中的所有值存储在一起。如果每个列存储在一个单独的文件中,查询只需要读取和解析在该查询中使用的那些列,这可以节省大量的工作。但请注意,单独排序每列是没有意义的,如果这样的话就无法知道列中的某一项属于哪一行。因为知道某列中 的第k项和 另一列的第k项一定属于同一行,基于这种约定我们可以重建一行。
概括来讲,存储引 擎分为两大类:针对事务处理(OLTP)优化的架构,以及针对分析型(OLAP)的优化架构。
- OLTP系统通常面向用 户,这意味着它们可能收到大量 的请求。为了处理负载,应用程序通常在每个查询 中只涉及少量的记录。应用程序基于某种键来请求记录,而存储引 擎使用索引来查找所请求键的数据。磁盘寻道时间往往是瓶颈 。
- 由于不是直接面对最终用户 ,数据仓库和类似的分析型系统相对并不太广为人知,它们主要由业务分析师使用。处理的查询请求数目远低于OLTP系统,但每个查询通常要求非常苛刻,需要在短时间 内扫描数百万条记录。 磁盘带宽(不是寻道时间)通常是瓶颈,而面向列的存储对于这种工作负载成为日益流行的解决方案。
第三章 编码
程序通常使用( 至少)两种不同的数据表示形式 :
1.在内存中,数据保存在对象、结构体、列表、数组、哈希表和树等结构中。这些数据结构针对CPU的高效访问和操作进行了优化(通常使用指针)。
2.将数据写入文件或通过网络发送时,必须将其编码为某种自包含的字节序列(例如JSON文档)。由 于指针对其他进程没有意义,所以这个字节序列表示看起来与内存中使用的数据结构大不一样 。
因此,在这两种表示之间需要进行类型的转化。从内存中的表示到字节序列的转化称为编码(序列化等),相反的过程称为解码(反序列化)。
第二部分、分布式数据系统
扩展能力
当负载增加需要更强的处理能力时,最简单的办法就是购买更强大的机器(有时称为垂直扩展)。由一个操作系统管理更多的CPU ,内存和磁盘,通过高速内部总线使每个CPU都可以访问所有的存储器或磁盘。在这样一个共享内存架构中,所有这些组件的集合可看作一台大机器。共享内存架构的问题在于,成本增长过快甚至超过了线性 :即如果把一台机器内的CPU数量增加一倍,内存扩容一倍,磁盘容量加大一倍,则最终总成本增加不止一倍。并且由于性能瓶颈因素,这样一台机器尽管拥有了两倍的硬件指标但却不一定能处理两倍的负载。
第五章 数据复制
如何确保所有副本之间 的数据是一致的?
单主从复制
只有主节点才可以接受写请求,主节点把新数据写入本地存储后,然后将数据更改作为复制的日志发送给所有从节点。读数据时 ,可以在主节点或者从节点上执行查询。
又分为同步与异步
同步复制的优点, 一旦向用户确认,从节点可以明确保证完成了与主节点的更新同步,数据已经处于最新版本 。万一主节点发生故障,总是可以在从节点继续访问最新数据。缺点,如果同步的从节点无法完成确认(例如由于从节点发生崩愤,或者网络故障,或任何其他原因),写入就不能视为成功。 主节点会阻塞其后所有的写操作,直到同步副本确认完成 。任何一个同步节点的中断都会导致整个系统更新停滞不前 。
把所有从节点都配置为同步复制有些不切实际,因为任何一个同步节点的中断都会导致整个系统更新停滞不前 。
异步复制: 如果主节点发生失败且不可恢复 ,则所有尚未复制到从节点的写请求都会丢失。这意味着即使向客户端确认了 写操作, 却无法保证数据的持久化。
从节点全同步/全异步都不可取 ,一般是一个同步剩余异步。这样可以保证至少有两个节点(即主节点和一个同步从节点)拥有最新的数据副本。这种配置有时也称为半同步。
问题:新增了节点怎么保证新节点和主节点数据一致?
- 简单地将数据文件从一个节点复制到另二个节点通常是不够的。主要是因为客户端仍在不断向数据库写入新数据,数据始终处于不断变化之中,因此常规的文件拷贝方式将会导致不同节点上呈现出不同时间点的数据。
- 锁定数据库(不可写)来使磁盘上的文件保持一致,但这会违反高可用的设计目标。
- 在某个时间点对主节点的数据副本产生一个快照,将此快照拷贝到新的从节点 。从节点先更新快照,再请求这期间主节点发生的数据更改日志。(也就是MySQL里的binlog)。
节点失效
从节点失效 : 追赶式恢复
从节点的本地磁盘上都保存了副本收到的数据变更日志。如果从节点发生崩溃,然后顺利重启,根据
副本的复制日志,从节点可以知道在发生故障之前所处理的最后一笔事务,然后连接到主节点,并请求自那笔事务之后中断期间内所有的数据变更。在收到这些数据变更日志之后,将其应用到本地来追赶主节点。之后就和正常情况一样持续接收来自主节点数据流的变化。
主节点失效:主从切换
日志实现
- 基于语句的实现INSERT 、 UPDATE或DELETE语句,类似于AOF。
问题:非确定性的语句,如NOW() 获取当前时间,或RAND()获取一个随机数等,可能会在不同的副本上产生不同的值。(如果语句存在不确定性操作,MySQL会切换到基于行的复制)
如果语句中使用了自增列,则所有副本必须按照完全相同的顺序执行,否则可能会带来不同的结果。 - 基于行的逻辑日志某行数据发生更新或删除操作时,不是记录整条SQL语句,而是记录哪些行发生了变化(比如哪些行被更新或删除),然后将这些变化信息发送给其他节点,接收节点根据这些信息来执行相应的更新或删除操作。(由于逻辑日志与存储引擎逻辑解锢,因此可以更容易地保持向后兼容,从而使主从节点能够运行不同版本的软件甚至是不同的存储引擎。)
复制滞后问题
写读一致性
主从节点,主节点进行改,主从进行查,如果改了之后,查询刚好是从节点,并且修改的数据还没同步过来,就发生了不一致(比如个人资料修改)。
解决:用户首页信息只能由自己编辑,那就总是从主节点读取用户自己的首页配置文件,而在从节点读取其他用户的配置文件 。
或者是:跟踪最近更新的时间 ,如果更新后xx分钟之内,就在主节点读取;并监控从节点的复制滞后程度 ,避免从那些滞后时间超过xx分钟的从节点读取 。
前缀一致读
你好然后才是吃饭了没,它们之间存在因果关系,由于复制滞后,变成了吃饭了没,你好。
多主节点复制
第六章、数据分区
每一条数据(或者每条记录,每行或每个文档)只属于某个特定分区。采用数据分区的主要目的是提高可扩展性。
分区通常与复制结合使用,即每个分区在多个节点都存有副本。这意味着某条记录属于特定的分区 ,而同样的内容会保存在不同的节点上以提高系统的容错性。一个节点上可能存储了多个分区。一个节点可能即是某些分区的主副本,同时又是其他分区的从副本。
分区的主要目标是将数据和查询负载均匀分布在所有节点上 。 如果节点平均分担负载 ,那么理论上 10个节点应该能够处理 10倍的数据量和 10倍于单个节点的读写吞吐量
如果分区不均匀,则会出现某些分区节点比其他分区承担更多 的数据量或查询负载,称之为倾斜。倾斜会导致分区效率严重下降,在极端情况下,所有的负载可能会集中在一个分区节点上,这就意味着 10个节点9个空闲,系统的瓶颈在最繁忙的那个节点上。
基于关键字区间分区
一种分区方式是为每个分区分配一段连续的关键字或者关键宇区间范围(以最小值和最大值来指示)
然而,基于关键字的区间分区的缺点是某些访问模式会导致热点。如果关键字是时间戳,则分区对应于一个时间范围,例如每天一个分区。然而,当测量数据从传感器写入数据库时,所有的写入操作都集中在同一个分区(即当天的分区),这会导致该分区在写入时负载过高,而其他分区始终处于空闲状态。
基于关键字晗希值分区
对于上述数据倾斜与热点问题,许多分布式系统采用了基于关键字哈希函数的方式来分区。
一个好的哈希函数可以处理数据倾斜并使其均匀分布 。 哈希分区。将哈希函数作用于每个关键字,每个分区负责一定范围 的哈希值。这种方法打破了原关键字的顺序关系,它的区间查询效率比较低,但可以更均匀地分配负载。
3
可以说,在分布式系统中,怀疑,悲观和偏执狂才能生存。
系统的可靠性应该取决于最不可靠的组件。
网络是不可靠的,(a )请求丢失; ( b )远程节点关闭; (c )响应丢失
处理这个问题通常采用超时机制:在等待一段时间之后,如果仍然没有收到回复则选择放弃,并且认为响应不会到达。