「《DDIA》读书笔记」数据系统基础

二、数据系统基础

0. 前置知识

0.1. 固态硬盘&机械硬盘

顺序读写与随机读写测试

固态硬盘的随机读写性能会比机械硬盘更加优秀,但价格也会更加昂贵,不过固态硬盘的价格在逐渐走低,取代掉机械硬盘也只是时间问题。

0.2. 磁盘的性能瓶颈

磁盘I/O那些事

磁盘调度中寻道时间和旋转延迟的区别

img

img

影响磁盘的关键因素是磁盘服务时间,即磁盘完成一个I/O请求所花费的时间,它由寻道时间、旋转延迟和数据传输时间三部分构成。

  1. 寻道时间:Tseek是指将读写磁头移动至正确的磁道上所需要的时间。寻道时间越短,I/O操作越快,目前磁盘的平均寻道时间一般在3-15ms。
  2. 旋转延迟:Trotation是指盘片旋转将请求数据所在的扇区移动到读写磁头下方所需要的时间。旋转延迟取决于磁盘转速,通常用磁盘旋转一周所需时间的1/2表示。比如:7200rpm的磁盘平均旋转延迟大约为60*1000/7200/2 = 4.17ms,而转速为15000rpm的磁盘其平均旋转延迟为2ms。
  3. 数据传输时间:Ttransfer是指完成传输所请求的数据所需要的时间,它取决于数据传输率,其值等于数据大小除以数据传输率。目前IDE/ATA能达到133MB/s,SATA II可达到300MB/s的接口数据传输率,数据传输时间通常远小于前两部分消耗时间。简单计算时可忽略。

衡量性能的指标:

机械硬盘的连续读写性能很好,但随机读写性能很差,这主要是因为磁头移动到正确的磁道上需要时间,随机读写时,磁头需要不停的移动,时间都浪费在了磁头寻址上,所以性能不高。衡量磁盘的重要主要指标是IOPS和吞吐量。

3. 存储与检索

3.1. 驱动数据库的数据结构

3.1.1. 在内存中存储一切

数据从磁盘读取到内存耗时的原因:内存数据结构编码为磁盘数据结构的开销大

所谓的 反缓存(anti-caching) 方法通过在内存不足的情况下将最近最少使用的数据从内存转移到磁盘,并在将来再次访问时将其重新加载到内存中。(思想看起来像LRU......)

3.2. 事务处理还是分析?

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

OLTP系统往往对业务运作至关重要,因而通常会要求 高可用 与 低延迟。所以DBA会密切关注他们的OLTP数据库,他们通常不愿意让业务分析人员在OLTP数据库上运行临时分析查询,因为这些查询通常开销巨大,会扫描大部分数据集,这会损害同时执行的事务的性能。

相比之下,数据仓库是一个独立的数据库,分析人员可以查询他们想要的内容而不影响OLTP操作。数据仓库包含公司各种OLTP系统中所有的只读数据副本。从OLTP数据库中提取数据(使用定期的数据转储或连续的更新流),转换成适合分析的模式,清理并加载到数据仓库中。将数据存入仓库的过程称为“抽取-转换-加载(ETL)“

image-20211217125020784

3.3. 列储存

面向列的存储背后的想法很简单:不要将所有来自一行的值存储在一起,而是将来自每一列的所有值存储在一起。

面向列的存储布局依赖于每个列文件包含相同顺序的行。 因此,如果您需要重新组装整行,您可以从每个单独的列文件中获取第23项,并将它们放在一起形成表的第23行。

面向列的存储很适合压缩,是需要在适当的时候进行排序

但是列存储也有写入困难的缺点,插入需要更新所有列,可以采用LSM树:

所有的写操作首先进入一个内存中的存储,在这里它们被添加到一个已排序的结构中,并准备写入磁盘。内存中的存储是面向行还是列的,这并不重要。当已经积累了足够的写入数据时,它们将与磁盘上的列文件合并,并批量写入新文件。(大概思路如此,细节再研究🧐)

数据仓库提效的一个方法:物化视图。在关系数据模型中,它通常被定义为一个标准(虚拟)视图:一个类似于表的对象,其内容是一些查询的结果。不同的是,物化视图是查询结果的实际副本,写入磁盘,而虚拟视图只是写入查询的捷径。从虚拟视图读取时,SQL引擎会将其展开到视图的底层查询中,然后处理展开的查询。

物化视图的常见特例是数据立方体,根据不同纬度的数据组合起来一个数据立方体用于加速查询,但数据立方体本身不具有查询原始数据的灵活性,所以数据仓库尽可能多的保存原始数据,只是将数据立方体作为某些查询的性能提升手段。

4. 编码与演化

双向兼容性:

  1. 向后兼容:新代码可以读旧数据
  2. 向前兼容:老代码可以读新数据

4.1. 数据编码格式

从内存到字节序列:序列化/编码

从字节序列到内存:反序列化/解码

编码的格式有下面几种:

  1. 语言特定的格式:java.io.Serializable等等
  2. JSON、XML、CSV属于文本格式,具有人类可读性,也属于纯纯的二进制编码格式
  3. Thrift、Protobuf,有自己的IDL(接口定义语言),也有自己的二进制编码格式,采用不同的压缩手段
  4. Avor通过字段名来标识,对动态生成的模式更友善,不需要像Thrift和Protobuf那样预先加序列号

Thrift和Protobuf依赖于代码生成:在定义了模式之后,可以使用您选择的编程语言生成实现此模式的代码。这在Java,C ++或C#等静态类型语言中很有用,因为它允许将高效的内存中结构用于解码的数据,并且在编写访问数据结构的程序时允许在IDE中进行类型检查和自动完成。

在动态类型编程语言(如JavaScript,Ruby或Python)中,生成代码没有太多意义,因为没有编译时类型检查器来满足。代码生成在这些语言中经常被忽视,因为它们避免了显式的编译步骤。而且,对于动态生成的模式(例如从数据库表生成的Avro模式),代码生成对获取数据是一个不必要的障碍。

Avro为静态类型编程语言提供了可选的代码生成功能,但是它也可以在不生成任何代码的情况下使用。

4.2. 数据流的类型

4.2.1. 数据流类型

数据流有如下几种类型:

  1. 数据库(Database)
  2. 远程过程调用(RPC&REST)
  3. 异步消息传递(MQ)

这里可能有同学不是很清楚REST(没错,就是我),感兴趣的话可以看一下这篇文章(看个文章都能吃到狗粮也是绝了)

如何给老婆解释什么是RESTful

怎样用通俗的语言解释REST,以及RESTful?

简单的解释一句:REST是用HTTP的动词(POST、GET、PUT、DELETE)描述操作

4.2.2. RPC的问题

  1. 本地函数调用是可预测的,并且成功或失败仅取决于受您控制的参数。网络请求是不可预知的。
  2. 本地函数调用要么返回结果,要么抛出异常,或者永远不返回。网络请求有另一个可能的结果:由于超时,它可能会返回没有结果。
  3. RPC如果重试,协议中引入幂等机制。本地函数调用没有这个问题。
  4. RPC的调用时间不可控,容易受大对象的影响。
  5. RPC的客户端和服务端可以是不同语言,序列化和反序列化的过程中可能会出问题,例如JS无法解析数字大于2^53的问题

4.2.3. RPC当前的方向

毫无疑问,RPC当前的方向就是在解决上面提出的那几个问题

针对上面的问题,现有的RPC框架都或多或少的给出了解决方案

有那么一个问题:为什么使用RPC而非REST?

使用二进制编码格式的自定义RPC协议可以实现比通用的JSON over REST更好的性能。但是,RESTful API还有其他一些显著的优点:方便实验和调试(只需使用Web浏览器或命令行工具curl,无需任何代码生成或软件安装即可向其请求),能被所有主流的编程语言和平台所支持,还有大量可用的工具(服务器,缓存,负载平衡器,代理,防火墙,监控,调试工具,测试工具等)的生态系统。

由于这些原因,REST似乎是公共API的主要风格。 RPC框架的主要重点在于同一组织拥有的服务之间的请求,通常在同一数据中心内。

4.2.4. 消息传递中的数据流

和RPC相比,使用消息代理有这几个优点:

  1. 如果收件人不可用或过载,可以充当缓冲区,从而提高系统的可靠性。(存储)
  2. 它可以自动将消息重新发送到已经崩溃的进程,从而防止消息丢失。(重试)
  3. 避免发件人需要知道收件人的IP地址和端口号(这在虚拟机经常出入的云部署中特别有用)。(注册中心)
  4. 它允许将一条消息发送给多个收件人(消费者组)
  5. 将发件人与收件人逻辑分离(发件人只是发布邮件,不关心使用者)。(解耦)

其实上面这几个优点也不一定都是RPC没有的,例如注册中心,Dubbo就可以使用Zookeeper作为注册中心,从而达到「避免发件人需要知道收件人的IP地址和端口号」的目的。

然而,与RPC相比,差异在于消息传递通信通常是单向的:发送者通常不期望收到其消息的回复。一个进程可能发送一个响应,但这通常是在一个单独的通道上完成的。这种通信模式是异步的:发送者不会等待消息被传递,而只是发送它,然后忘记它。

如上是书里的一段话,放在现在我认为不太准确,因为现在的MQ会对自己的功能进行个性化的封装,例如RocketMQ就有3种消息发送模式:同步、异步、oneway。书里讲的单向通信只是RocketMQ的oneway的发送模式。

4.2.5. 分布式Actor框架

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

针对这句话我的理解是:

每个对象都有一个属于自己的Actor,不同的Actor之间通过异步消息通信,每个Actor一次只能处理一条消息,因此不需要担心并发问题。反观正常操作,有对象A、B、C,对象A、B同时调用对象C的方法,对象C的数据可能由于并发出现问题。比较着来说,Actor框架更有利于处理并发消息(因为有个队列可以给请求排队......)

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×