数据系统

基石

可靠性

造成错误的原因叫做故障(fault),预料并应对故障的系统特性可称为容错(fault-tolerant)或者弹性(resilient)。

故障(fault)不同于失效(failure),故障定义的是系统偏离正常状态,失效则是整个系统不能对用户提供服务

硬件的故障、软件错误、人为失误都会导致故障或者失效

可伸缩性

可伸缩性(Scalability) 是用来描述系统应对负载增长能力的术语

负载可以用一些称为 负载参数(load parameters) 的数字来描述,如每秒请求数、读写请求比率等等。

为了描述性能,单个请求的响应参数是没有太多参考参考价值的,需要通过一个可测量的数值分布来描述:

中位数、平均数、95%、99%

为了明确大多数用户的指标,中位数是最有价值的,而99%,95%则是用来描述异常的刺突。

为了应对负载,可以进行垂直扩展或者水平扩展。没有一种银弹可以解决所有的应对负载需求,一个良好适配应用的可扩展架构,是围绕着假设(assumption)建立的,这就是所谓负载参数。

可维护性

数据模型

不同的数据模型是为不同的应用场景而设计的。选择适合应用程序的数据模型非常重要。

应用层有各种领域概念,存储层则有JSON、XML或关系模型,数据库层则有以内存或字节来表示JSON、XML等,最底层使用电流或者脉冲来表示字节。

关系模型已经持续称霸了大约25~30年,2010年新起的NoSQL文档模型在最近一段时间内也慢慢与关系模型配合使用,随着时间的推移,关系数据库和文档数据库似乎变得越来越相似。

由于应用的数据最终都被转换成关系模型,应用代码跟数据库之间就需要一个转换中间层,由于现在大部分使用了OO编程语言,二者之间转换还是挺吃力的,即出现阻抗不匹配。

当前的文档模型和上世纪70年代的层次模型很像,一对多很容易支持,但多对多就抓瞎了,文档模型会不会重蹈层次模型的覆辙。

文档模型拥有较关系模型强的架构灵活性,因为我们对数据的解释是发生在我们的代码之中,这意味着一旦数据模型发生改变,修改代码就好了。

同时文档模型拥有数据局部性优势,但这只对同时需要同一文档里的大部分数据时下的场景有效。

查询语言

相较于命令式查询,声明式查询更适合并行。

对于声明式查询语言来说,在编写查询语句时,不需要指定执行细节:查询优化程序会自动选择预测效率最高的策略

介于命令式与声明式之间的有如MapReduce的map及reduce操作,函数式编程其实也是介于二者之间,其通过较高层级的抽象实现,底层运行时可以有效地利用并行性来优化

图状数据模型查询语言

存储与查询

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

为了解决对一个值多次写入造成的空间浪费,日志被分成多个固定大小段,当新启动一个段写入时,可以对段进行合并压缩减少磁盘空间占用

段合并与压缩

日志结构需要考虑的问题:

这种方式对于写入的性能非常好,但读取性能很差,于是引入了索引。

索引

事务处理与分析处理

为了避免进行事务分析对在线事务的影响,通过采用同步数据到数据仓库的方式来解决这个需求,将数据存入仓库的过程称为“抽取-转换-加载(ETL)”

数据仓库与ETL

将一张事实表转换为多张维度表方便进行数据分析

对于部分的查询只需要用到少部分列,但一个事实表可能用来成百上千个列,这个时候引入列存储来提升性能,相对来说一个列的重复数据会较多,可以执行列压缩,一些取值比较连续分布的数据,在使用列存储以后,可以利用 runlength encoding 类似的压缩方法大大提高压缩率

Dremel 通过对列里的每一个值,定义了 Repetition Level 和 Definition Level,区分清楚了每一个值是隶属于哪一层嵌套中的,以及是否需要填充对应的 Optional 的字段或者结构体,使得能够转化成列存储之前的每一行的数据。Dremel 的数据是以行列混合的形式存储下来的。在单个服务器上,数据是列存储的,但是在全局,数据又根据行进行分区,分配到了不同的服务器节点上

Dremel架构

Dremel通过使用中间服务器来并行合并执行结果来降低延迟,对于某些掉队的节点:跑起来特别慢,拖慢了整个系统返回一个查询结果的时间。一旦监测到掉队者的出现,它就会把任务再发给另外一个节点,由于Dremel是给数据分析所舒勇的,也可以设置扫描了 98% 或者 99% 的数据就返回一个近似的结果

物化视图加快查询

物化视图的特殊情况:数据立方体

编码与演化

新旧版本的代码,以及新旧数据格式可能会在系统中同时共处。系统想要继续顺利运行,就需要保持双向兼容性:

数据的使用形式:

相较于XML和JSON,二进制编码Thirft/Protocol Buffers/Avro更加节省空间。

为了保证软件演化过程的向前向后兼容,对每个字段,通过一个唯一标签来标志,只要字段不被删除,新的代码肯定可以处理老数据,但反过来,只有添加的每个字段是可选的或者有默认值,才能向前兼容。

数据类型也对演化过程有影响,如果老数据是32位的,新数据是64位的,升级可以无缝迁移,但向前兼容时就会有问题。

使用读模式跟写模式解决差异:

20213315519

同时如果数据流经过旧代码,那么部分数据很有可能丢失:

20213315109

数据流模式

数据集成

首先要有可靠、完整的数据流,后续才能谈得上在上层对这些数据进行存储、可视化、预测

批处理系统

UNIX的管道

MapReduce

分布式批处理需要解决的问题

  1. 如何将输入数据分区
  2. 容错:任务可能随时会失败

流处理

消息系统

早期使用消息队列来实现流处理:

stateDiagram
  direction LR
  数据源 --> 消息队列1
  消息队列1 --> 处理逻辑1
  处理逻辑1 --> 消息队列2
  消息队列2 --> 处理逻辑3
  消息队列2 --> 处理逻辑4
  处理逻辑3 --> 消息队列3
  处理逻辑4 --> 消息队列3

消息系统与传统的数据库有着本质的区别:数据临时与永久之分

分区日志消息系统:结合了传统消息系统与数据库:既是流,又能存

每个逻辑数据源都可以建模为它自己的日志

以日志为中心的基础设施栈

流与数据库

本质上就是状态复制机的实现

本质上数据库的日志就是流,数据库里的数据就是当前流重放的快照

流处理

时间问题

流处理依赖于本地时间戳,时钟是不可靠的,同时考虑消息堆积、软件错误等问题,基于时间戳的流分析可能不准

窗口类型
  1. 轮转窗口:固定长度,相互之间没有重叠且紧邻 [1,3] [4,6]
  2. 跳跃窗口:固定长度,允许之间重叠以进行平滑过度 [1,3] [2,4]
  3. 滑动窗口
  4. 会话窗口:没有固定时间,将同一用户的事件组合在一起

容错

这需要消费端保证幂等性,否则为了容错会输出不止一次导致副作用