《数据密集型应用系统设计》:本书旨在帮助大家更好地驾驭处理数据和存储数据相关技术。
在计算机领域,人们往往会被新的东西所吸引,但我认为前人有很多优秀的工作和积累依然值得深入学习。
经典很难过时,快速过了一些章节,功力不够,后面需要再读一遍。本书过于概念化还是要回归到工程中。
数据密集与计算密集是当今两大典型负载类型,前者以大数据为代表,后者以深度学习和HPC等为主要代表。其中大数据是开展深度学习的重要前提,也是当今云计算和Web服务的核心支撑技术。本书属于大数据范畴,主要采用分布式系统来处理和存储数据(涉及数据库、Hadoop、NoSQL、流处理等),探讨了三个主题:系统的可靠性、可扩展性与可维护性。
对于一个应用系统,如果“数据”是其成败决定性因素,包括数据的规模、数据的复杂度或者数据产生与变化的速率等,我们就可以称为“数据密集型应用系统”,与之对应的是计算密集型(Compute-Intensive),CPU主频往往是后者最大的制约瓶颈。
第一部分:数据系统基础
第1章:可靠、可扩展与可维护的应用系统
通常将数据库、队列、高速缓存等视为不同类型的系统。
影响数据系统设计的因素有很多,其中包括相关人员技能和经验水平、遗留系统依赖性、交付周期、对不同风险因素的容忍度、监管合规等。
本书将专注于对大多数软件系统都极为重要的三个问题:
可靠性(Reliability)
当出现意外情况如硬件、软件故障、人为失误等,系统应可以继续正常运转:虽然性能可能有所降低,但确保功能正确。
可能出错的事情称为错误(faults)或故障,系统可应对错误则称为容错(fault-tolerant)或者弹性(resilient)。故障通常被定义为组件偏离其正常规格,而失效意味系统作为一个整体停止,无法向用户提供所需的服务。
设计容错机制来避免从故障引发系统失效。虽然我们通常倾向于容忍故障而不是预防故障,但是也存在“预防胜于治疗”的情况,安全问题就是一例,例如:如果攻击者破坏了系统并窃取了敏感数据,则该事件造成的影响显然无法被撤销。
- 硬件故障
对于硬件故障总是很容易想到:硬盘崩溃,内存故障,电网停电,甚至有人误拔掉了网线。为硬件添加冗余来减少系统故障率。通过软件容错的方式来容忍多机失效成为新的手段,或者至少成为硬件容错的有力补充。 - 软件错误
通常认为硬件故障之间多是相互独立的:一台机器的磁盘出现故障并不意味着另一台机器的磁盘也要失效。除非存在某种弱相关(例如一些共性原因,如服务器机架中的温度过高),否则通常不太可能出现大量硬件组件同时失效的情况。另一类故障则是系统内的软件问题。这些故障事先更加难以预料,而且因为节点之间是由软件关联的,因而往往会导致更多的系统故障。 - 人为失误
如果我们假定人是不可靠的,那么该如何保证系统的可靠性呢?
1). 以最小出错的方式来设计系统。例如,精心设计的抽象层、API以及管理界面,使“做正确的事情”很轻松,但搞坏很复杂。但是,如果限制过多,人们就会想法来绕过它,这会抵消其正面作用。因此解决之道在于很好的平衡。
2). 想办法分离最容易出错的地方、容易引发故障的接口。特别是,提供一个功能齐全但非生产用的沙箱环境,使人们可以放心的尝试、体验,包括导入真实的数据,万一出现问题,不会影响真实用户。
3). 充分的测试:从各单元测试到全系统集成测试以及手动测试 。自动化测试已被 广泛使用,对于覆盖正常操作中很少出现的边界条件等尤为重要。
4). 当出现人为失误时,提供快速的恢复机制以尽量减少故障影响。例如,快速回滚 配置改动,滚动发布新代码 (这样任何意外的错误仅会影响一小部分用户),并提供校验数据的工具(防止旧的计算方式不正确 )。
5). 设置详细而清晰的监控子系统,包括性能指标和错误率。
6). 推行管理流程并加以培训。可扩展性(Scalability)
随着规模的增长, 例如数据量 、 流量或复杂性, 系统应以合理的方式来匹配这种增长。
可扩展性是用来描述系统应对负载增加能力的术语。负载可以用称为负载参数的若干数字来描述 。 参数的最佳选择取决于系统的体系结构,它可能是 Web 服务器的毎秒请求处理次数 ,数据库中写入的比例 , 聊天室的同时活动用户数量 , 缓存命中率等 。
在批处理系统如Hadoop中,我们通常关心吞吐量(throughput),即每秒可处理的记录条数,或者在某指定数据集上运行作业所需的总时间,而在线系统通常更看重服务的响应时间(responsetime),即客户端从发送请求到接收响应之间的间隔。
最好不要将响应时间视为一个固定的数字,而是可度量的一种数值分布。中位数指标非常适合描述多少用户需要等待多长时间:一半的用户请求的服务时间少于中位数响应时间,另一半则多于中位数的时间。因此中位数也称为50百分位数,有时缩写为p50。为了弄清楚异常值有多糟糕,需要关注更大的百分位数如常见的第95、99和99.9(缩写为p95、p99和p999)值。作为典型的响应时间阈值,它们分别表示有95%、99%或99.9%的请求响应时间快于阈值。
采用较高的响应时间百分位数(tail latencies,尾部延迟或长尾效应)很重要,因为它们直接影响用户的总体服务体验。
排队延迟往往在高百分数响应时间中影响很大。由于服务器并行处理的请求有限(例如,CPU内核数的限制),正在处理的少数请求可能会阻挡后续请求,这种情况有时被称为队头阻塞。即使后续请求可能处理很简单,但它阻塞在等待先前请求的完成,客户端将会观察到极慢的响应时间。因此,很重要的一点是要在客户端来测量响应时间。
现在谈论更多的是如何在垂直扩展(即升级到更强大的机器)和水平扩展(即将负载分布到多个更小的机器)之间做取舍。某些系统具有弹性特征,它可以自动检测负载增加,然后自动添加更多计算资源,而其他系统则是手动扩展(人工分析性能表现,之后决定添加更多计算)。把无状态服务分布然后扩展至多台机器相对比较容易,而有状态服务从单个节点扩展到分布式多机环境的复杂性会大大增加。可维护性(Maintainability)
随着时间的推移,许多新的人员参与到系统开发和运维,以维护现有功能或适配新场景等,系统都应高效运转。
软件的大部分成本并不在最初的开发阶段,而是在于整个生命周期内持续的投入,这包括维护与缺陷修复,监控系统来保持正常运行、故障排查、适配新平台、搭配新场景、技术缺陷的完善以及增加新功能等。
从软件设计时开始考虑,尽可能较少维护期间的麻烦,甚至避免造出容易过期的系统。为此,我们将特别关注软件系统的三个设计原则: - 可运维性:运维更轻松
运营团队对于保持软件系统顺利运行至关重要。 - 简单性:简化复杂度
简化系统设计并不意味着减少系统功能,而主要意味着消除意外方面的复杂性,正如Moseley和Marks把复杂性定义为一种“意外”,即它并非软件固有,被用户所见或感知,而是实现本身所衍生出来的问题。消除意外复杂性最好手段之一是抽象。一个好的设计抽象可以隐藏大量的实现细节,并对外提供干净、易懂的接口。 - 可演化性:易于改变
目标是可以轻松地修改数据系统,使其适应不断变化的需求,这和简单性与抽象性密切相关:简单易懂的系统往往比复杂的系统更容易修改。第2章:数据模型与查询语言
大多数应用程序是通过一层一层叠加数据模型来构建的。每层都通过提供一个简洁的数据模型来隐藏下层的复杂性。
历史上,数据最初被表示为一棵大树(层次模型),但是这不利于表示多对多关系,所以发明了关系模型来解决这个问题。
新的非关系“NoSQL”数据存储在两个主要方向上存在分歧:
1.文档数据库的目标用例是数据来自于自包含文档,且一个文档与其他文档之间的关联很少。
2.图数据库则针对相反的场景,目标用例是所有数据都可能会互相关联。
文档数据库和图数据库有一个共同点,那就是它们通常不会对存储的数据强加某个模式,这可以使应用程序更容易适应不断变化的需求。但是,应用程序很可能仍然假定数据具有一定的结构,只不过是模式是显式(写时强制)还是隐式(读时处理)的问题。关系模型与文档模型
现在最著名的数据模型可能是SQL,它基于EdgarCodd于1970年提出的关系模型:数据被组织成关系(relations),在SQL中称为表(table),其中每个关系都是元组(tuples)的无序集合(在SQL中称为行)。
采用NoSQL数据库有这样几个驱动因素,包括:比关系数据库更好的扩展性需求,包括支持超大数据集或超高写入吞吐量。普遍偏爱免费和开源软件而不是商业数据库产品。关系模型不能很好地支持一些特定的查询操作。对关系模式一些限制性感到沮丧,渴望更具动态和表达力的数据模型。
关系数据库可能仍将继续与各种非关系数据存储一起使用,这种思路有时也被称为混合持久化。
如果数据存储在关系表中,那么应用层代码中的对象与表、行和列的数据库模型之间需要一个笨拙的转换层。ActiveRecord和Hibernate这样的对象-关系映射(ORM)框架则减少了此转换层所需的样板代码量,但是他们并不能完全隐藏两个模型之间的差异。
对于关系数据库,由于支持联结操作,可以很方便地通过ID来引用其他表中的行。而在文档数据库中,一对多的树状结构不需要联结,支持联结通常也很弱。如果数据库本身不支持联结,则必须在应用程序代码中,通过对数据库进行多次查询来模拟联结。
在关系数据库中,查询优化器自动决定以何种顺序执行查询,以及使用哪些索引。
文档数据库是某种方式的层次模型:即在其父记录中保存了嵌套记录(一对多关系),而不是存储在单独的表中。但是,在表示多对一和多对多的关系时,关系数据库和文档数据库并没有根本的不同:在这两种情况下,相关项都由唯一的标识符引用,该标识符在关系模型中被称为外键,在文档模型中被称为文档引用”。标识符可以查询时通过联结操作或相关后续查询来解析。
支持文档数据模型的主要论点是模式灵活性,由于局部性而带来较好的性能,对于某些应用来说,它更接近于应用程序所使用的数据结构。关系模型则强在联结操作、多对一和多对多关系更简洁的表达上,与文档模型抗衡。
如果应用数据具有类似文档的结构(即一对多关系树,通常一次加载整个树),那么使用文档模型更为合适。
文档模型也有一定的局限性:例如,不能直接引用文档中的嵌套项,然而,只要文档嵌套不太深,这通常不是问题。但是,如果应用程序确实使用了多对多关系,那么文档模型就变得不太吸引人。 - 文档模型中的模式灵活性
大多数文档数据库,以及关系数据库中的JSON支持,都不会对文档中的数据强制执行任何模式。文档数据库有时被称为无模式,但这具有误导性,更准确的术语应该是读时模式(数据的结构是隐式的,只有在读取时才解释),与写时模式(关系数据库的一种传统方法,模式是显式的,并且数据库确保数据写入时都必须遵循)相对应。读时模式类似编程语言中的动态(运行时)类型检查,而写时模式类似于静态(编译时)类型检查。 - 查询的数据局部性
文档通常存储为编码为JSON、XML或其二进制变体(如MongoDB的BSON)的连续字符串。
局部性优势仅适用需要同时访问文档大部分内容的场景。由于数据库通常会加载整个文档,如果应用只是访问其中的一小部分,则对于大型文档数据来讲就有些浪费。通常建议文档应该尽量小且避免写入时增加文档大小。这些性能方面的不利因素大大限制了文档数据库的适用场景。数据查询语言
SQL是一种声明式查询语言。命令式语言告诉计算机以特定顺序执行某些操作。而对于声明式查询语言(如SQL或关系代数),则只需指定所需的数据模式,结果需满足什么条件,以及如何转换数据(例如,排序、分组和聚合),而不需指明如何实现这一目标。
声明式查询语言很有吸引力,它比命令式API更加简洁和容易使用。但更重要的是,它对外隐藏了数据库引擎的很多实现细节,这样数据库系统能够在不改变查询语句的情况下提高性能。命令式代码由于指定了特定的执行顺序,很难在多核和多台机器上并行化。声明式语言则对于并行执行更为友好,它们仅指定了结果所满足的模式,而不指定如何得到结果的具体算法。 - MapReduce查询
MapReduce是一种编程模型,用于在许多机器上批量处理海量数据,MapReduce既不是声明式查询语言,也不是一个完全命令式的查询API,而是介于两者之间:查询的逻辑用代码片段来表示,这些代码片段可以被处理框架重复地调用。它主要基于许多函数式编程语言中的map(也称为collect)和reduce(也称为fold或inject)函数。map和reduce函数对于可执行的操作有所限制。它们必须是纯函数,这意味着只能使用传递进去的数据作为输入,而不能执行额外的数据库查询,也不能有任何副作用。图数据模型
- 图状数据模型
关系模型能够处理简单的多对多关系,但是随着数据之间的关联越来越复杂,将数据建模转化为图模型会更加自然。图由两种对象组成:顶点(也称为结点或实体)和边(也称为关系或弧)。很多数据可以建模为图。
关于图模型一些值得注意的地方:
1.任何顶点都可以连接到其他任何顶点。没有模式限制哪种事物可以或不可以关联。
2.给定某个顶点,可以高效地得到它的所有入边和出边,从而遍历图,即沿着这些顶点链条一直向前或向后。
3.通过对不同类型的关系使用不同的标签,可以在单个图中存储多种不同类型的信息,同时仍然保持整洁的数据模型。 - Cypher查询语言
Cypher是一种用于属性图的声明式查询语言,最早为Neo4j图形数据库而创建”。 - SQL中的图查询
在关系数据库中,通常会预先知道查询中需要哪些 join操作。而对于图查询,在找到要查找的顶点之前,可能需要遍历数量未知的边,也就是说,join操作数量并不是预先确定的。SQL:1999标准以后,查询过程中这种可变的遍历路径可以使用称为递归公用表表达式(即WITH RECURSIVE语法)来表示。 - 三元存储与SPARQL
在三元存储中,所有信息都以非常简单的三部分形式存储(主体,谓语,客体)。
三元组的主体相当于图中的顶点。而客体则是以下两种之一:1.原始数据类型中的值,如字符串或数字。2.图中的另一个顶点。
可以使用分号来说明同一主体的多个对象信息。
语义网目标是帮助机器在一定程度上理解Web信息的含义,使得高效的信息共享和机器智能协同成为可能。不幸的是,语义网在21世纪初被严重夸大了,时至今日也没有在实践中见到任何靠谱的实现。
三元存储数据模型其实完全独立于语义网,它与语义网并没有任何关系。语义网技术在实际应用中的局限性主要包括非严格性、处理复杂性、语境理解局限性、知识共享困难、资源需求难以满足、模型协同和隐私安全之间的矛盾等。
RDF(Resource Description Framework)是W3C组织推荐使用的用来描述资源及其之间关系的语言规范,具有简单、易扩展、开放性、易交换和易综合等特点。RDF在形式上以三元组表示实体及实体之间的关系,反映了物理世界中具体的事物及关系。
RDF数据模型因为旨在为全网数据交换而设计,RDF存在一些特殊的约定。三元组的主体、谓语和客体通常是URI。
SPARQL是一种采用RDF数据模型的三元存储查询语言,名字是SPARQL Protocol和RDF Query Language的缩写。第3章:数据存储与检索
数据库核心: 数据结构
许多数据库内部都使用日志(log),日志是一个仅支持追加式更新的数据文件。为了高效地查找数据库中特定键的值,需要新的数据结构:索引。索引是基于原始数据派生而来的额外数据结构。很多数据库允许单独添加和删除索引,而不影响数据库的内容,它只会影响查询性能。维护额外的结构势必会引入开销,特别是在新数据写入时。
存储系统中重要的权衡设计:适当的索引可以加速读取查询,但每个索引都会减慢写速度。为此,默认情况下,数据库通常不会对所有内容进行索引,它需要应用开发人员或数据库管理员,基于对应用程序典型查询模式的了解,来手动选择索引。目的是为应用程序提供最有利加速的同时,避免引入过多不必要的开销。 - 哈希索引
哈希索引是一种以键-值存储数据、基于哈希表实现的索引结构,只有精确匹配索引所有列的查询才能生效。哈希索引在等值查询中明显有绝对优势,因为只需要经过一次运算即可找到对应的键值。
局限性: 哈希表必须全部放入内存,所以如果有大量的键,就没那么幸运了。区间查询效率不高。 - SSTables和LSM-Tree
排序字符串表,或简称为SSTable,是一种数据存储格式。字符串排序时,优先参考从左到右逐一比较的方式来确定两个字符串的优先级;忽略大小写字母的差异。
SSTable相比哈希索引的日志段,具有以下优点:
1.合并段更加简单高效,即使文件大于可用内存。2.在文件中查找特定的键时,不再需要在内存中保存所有键的索引。3.由于读请求往往需要扫描请求范围内的多个key-value对,可以考虑将这些记录保存到一个块中并在写磁盘之前将其压缩。然后稀疏内存索引的每个条目指向压缩块的开头。除了节省磁盘空间,压缩还减少了I/O带宽的占用。
构建和维护SSTables:
内存排序有很多广为人知的树状数据结构,例如红黑树或AVL树。使用这些数据结构,可以按任意顺序插入键并以排序后的顺序读取它们。
如果数据库崩溃,最近的写入(在内存表中但尚未写入磁盘)将会丢失。为了避免该问题,可以在磁盘上保留单独的日志,每个写入都会立即追加到该日志。日志文件不需要按键排序,这并不重要,因为它的唯一目的是在崩溃后恢复内存表。每当将内存表写入SSTable时,相应的日志可以被丢弃。
从SSTables到LSM-Tree:
Log-Structured Merge-Tree(LSM-Tree)是一种分层有序面向磁盘的数据结构,由Patrick O’Neil等人于1996年提出。LSM-Tree的基本思想十分简单,就是将对数据的增量更新暂时保存在内存中,达到指定的存储阈值后将该批更新批量写入磁盘,在批量写入的过程中与已经存在的数据做合并操作。在数据读取时,同样需要合并磁盘中的数据和内存中最近的修改操作。 LSM-Tree的性能优势来源于其数据的读写控制方式防止了大量的数据更新造成的磁盘随机写入,但读取数据时需要多次磁盘I/O来访问较多的文件。与传统的B+树相比,LSM-Tree牺牲了读取性能来大幅提升写入性能。
Lucene是Elasticsearch和Solr等全文搜索系统所使用的索引引擎。
性能优化
存储引擎通常使用额外的布隆过滤器。布隆过滤器是内存高效的数据结构,用于近似计算集合的内容。如果数据库中不存在某个键,它能够很快告诉你结果,从而节省了很多对于不存在的键的不必要的磁盘读取)。
不同的策略会影响甚至决定SSTables压缩和合并时的具体顺序和时机。最常见的方式是大小分级和分层压缩。
LSM-tree可以支持非常高的写入吞吐量。 - B-trees
B-tree索引的物理文件通常以平衡树(Balance Tree)的结构存储,所有实际需要的数据都存放在树的叶子节点中,且到任何一个叶子节点的最短路径长度都完全相同,因此得名B-tree索引。B-tree索引主要由叶子节点、分支节点和根节点组成。最广泛使用的索引结构是另一种不同的:B-tree。
B-tree将数据库分解成固定大小的块或页,传统上大小为4KB(有时更大),页是内部读/写的最小单元。B-tree中一个页所包含的子页引用数量称为分支因子。在实际中,分支因素取决于存储页面引用和范围边界所需的空间总量,通常为几百个。大多数数据库可以适合3~4层的B-tree,因此不需要遍历非常深的页面层次即可找到所需的页。
使B-tree可靠:B-tree底层的基本写操作是使用新数据覆盖磁盘上的旧页。常见B-tree的实现需要支持磁盘上的额外的数据结构:预写日志(write-ahead log,WAL),也称为重做日志。这是一个仅支持追加修改的文件,每个B-tree的修改必须先更新WAL然后再修改树本身的页。当数据库在崩溃后需要恢复时,该日志用于将B-tree恢复到最近一致的状态。如果多个线程要同时访问B-tree,则需要注意并发控制,否则线程可能会看到树处于不一致的状态。通常使用锁存器(轻量级的锁)保护树的数据结构来完成。
优化B-tree:一些数据库(如LMDB)不使用覆盖页和维护WAL来进行崩溃恢复,而是使用写时复制方案。保存键的缩略信息,而不是完整的键,这样可以节省页空间。一 般来说, 页可以放在磁盘上的任何位置;没有要求相邻的页需要放在磁盘的相邻位置。添加额外的指针到树中。B-tree的变体如分形树, 借鉴了一些日志结构的想法来减少磁盘寻道(与分形无关)。 - 对比B-tree和LSM-tree
根据经验,LSM-tree通常对于写入更快,而B-tree被认为对于读取更快。读取通常在LSM-tree上较慢,因为它们必须在不同的压缩阶段检查多个不同的数据结构和SSTable。
LSM-tree的优点:
B-tree索引必须至少写两次数据:一次写入预写日志,一次写入树的页本身(还可能发生页分裂)。 即使该页中只有几个字节更改,也必须承受写整个页的开销。
对于大量写密集的应用程序,性能瓶颈很可能在于数据库写入磁盘的速率。 在这种情况下,写放大具有直接的性能成本:存储引擎写入磁盘的次数越多,可用磁盘带宽中每秒可以处理的写入越少。此外,LSM-tree通常能够承受比B-tree更高的写入吞吐最,部分是因为它们有时具有较低的写放大(尽管这取决于存储引擎的配置和工作负载),部分原因是它们以顺序方式写入紧凑的SSTable文件,而不必重写树中的多个页。 这种差异对于磁盘驱动器尤为重要,原因是磁盘的顺序写比随机写要快得多。LSM-tree可以支持更好地压缩, 因此通常磁盘上的文件比B-tree小很多。 由于碎片,B-tree存储引擎使某些磁盘空间无法使用: 当页被分裂或当一行的内容不能适合现有页时, 页中的某些空间无法使用。 由于LSM-tree不是面向页的, 并且定期重写SSTables以消除碎片化, 所以它们具有较低的存储开销, 特别是在使用分层压缩时。
LSM-tree的缺点:
日志结构存储的缺点是压缩过程有时会干扰正在进行的读写操作。 如果写入吞吐量很高并且压缩没有仔细配置, 那么就会发生压缩无法匹配新数据写入速率的情况。通常, 即使压缩不能跟上, 基于SSTable的存储引擎也不会限制到来的写入速率, 因此需要额外的监控措施来及时发现这种情况。
B-tree的优点则是每个键都恰好唯一对应于索引中的某个位置, 而日志结构的存储引擎可能在不同的段中具有相同键的多个副本。 - 其他索引结构
聚集索引(在索引中直接保存行数据)和非聚集索引(仅存储索引中的数据的引用)之间有一种折中设计称为覆盖索引或包含列的索引, 它在索引中保存一些表的列值。它可以支持只通过索引即可回答某些简单查询(在这种情况下,称索引覆盖了查询)与任何类型的数据冗余一样,聚集和覆盖索引可以加快读取速度,但是它们需要额外的存储,并且会增加写入的开销。此外,数据库还需要更多的工作来保证事务性,这样应用程序不会因为数据冗余而得到不一致的结果。
多列索引类型称为级联索引, 它通过将一列追加到另一列,将几个字段简单地组合成一个键(索引的定义指定字段连接的顺序)。
多维索引是更普遍的一次查询多列的方法, 这对地理空间数据尤为重要。
全文搜索和模糊索引。事务处理与分析处理
事务不一定具有ACID(原子性、一致性、隔离性和持久性)属性。事务处理只是意味着允许客户端进行低延迟读取和写入,相比于只能周期性地运行(如每天一次)的批处理作业。
OLTP(On-Line Transaction Processing,联机事务处理)和OLAP(On-Line Analytical Processing,联机分析处理)是两种不同的数据处理方式。OLTP主要处理大量用户下的大量事务,如插入、更新或删除数据,适用于在线交易系统,如电子商务系统、银行、证券等。而OLAP则专注于满足分析人员大量数据的分析和统计,适用于报表分析场景,对准确性、事务性和实时性要求较低。 - 数据仓库
数据仓库包含公司所有各种OLTP系统的只读副本。从OLTP数据库(使用周期性数据转储或连续更新流)中提取数据,转换为分析友好的模式,执行必要的清理,然后加载到数据仓库中。将数据导入数据仓库的过程称为提取-转换-加载(Extract-Transform-Load,ETL)。数据仓库的数据模型最常见的是关系型,因为SQL通常适合分析查询。
事实表中的列是属性,其他列可能会引用其他表的外键,称为维度表。由于事实表中的每一行都代表一个事件,维度通常代表事件的对象(who)、什么(what)、地点(where)、时间(when)、方法(how)以及原因(why)。
“星型模式”来源于当表关系可视化时,事实表位于中间,被一系列维度表包围;这些表的连接就像星星的光芒。
该模板的一个变体称为雪花模式,其中维度进一步细分为子空间。
雪花模式比星型模式更规范化,但是星型模式通常是首选,主要是因为对于分析人员,星型模式使用起来更简单。
星型与雪花型分析模式是数据仓库中常用的两种数据结构,它们的主要区别在于维度表与事实表的关系。星型模型中,所有维度直接与事实表相关联,维度之间没有关联,维度表围绕事实表呈星状分布。而雪花模型则是在星型模型的基础上,允许维度表之间产生关联关系,事实表可以通过维度和维度的关联获得间接维度。
一、星型模型
1) 星型模型以事实表为中心,所有的维度直接和事实表相关联,维度和维度之间没有关联,维度表围绕在事实表周围呈星状分布。
2) 星型模型的优点是数据模型简单易理解,容易定义层级结构,可以减少join次数。缺点是从事实表查询时,由于维度太多导致执行效率太低,可能导致信息不一致,数据冗余较多,模型的灵活性较差。
二、雪花模型
1) 雪花模型是星型模型的扩展,允许维度表之间产生关联关系,事实表可以通过维度和维度的关联获得间接维度。
2) 雪花模型的优点是数据完整性较好,冗余小,可以提高应用系统的灵活性。缺点是结构复杂,构建难度大,表数量较多,增加了管理的复杂性,如果维表间发生join会导致执行效率低下。
三、两种模型的应用
1) 星型模型适合处理“预先定义的,需求相对确定的”数据分析课题,因为星型模型的好处是维度数据和事实表永远直接关联,因此,数据库访问的效率相对较高。
2) 雪花模型适合解决“探索性的,随时有可能变化的,临时的”数据分析课题,因为雪花模型的维度部分用实体关系建模处理,在关联查询,尤其是多层次的维度关联查询时,效率略低,但是换来的好处是数据模型相对稳定,维度的更新、增加等操作代价较小,对分析需求的变更有一定的自适应性列式存储
面向列存储的想法很简单:不要将一行中的所有值存储在一起,而是将每列中的所有值存储在一起。如果每个列存储在一个单独的文件中,查询只需要读取和解析在该查询中使用的那些列,这可以节省大量的工作。面向列的存储布局依赖一组列文件,每个文件以相同顺序保存着数据行。
列压缩:除了仅从磁盘中加载查询所需的列之外,还可以通过压缩数据来进一步降低对磁盘吞吐量的要求。取决于列中具体数据模式,可以采用不同的压缩技术。矢量化处理是一种数据压缩技术,主要用于对列数据进行压缩和解压。其主要过程包括码表设计、编码和解码。
在数据仓库中特别有效的一种技术是位图编码。除了减少需要从磁盘加载的数据量之外,面向列的存储布局也有利于高效利用CPU周期。
列存储中的排序:即使数据是按列存储的,它也需要一次排序整行。排序的另一个优点是它可以帮助进一步压缩列。基于第一个排序键的压缩效果通常最好。第二个和第三个排序键会使情况更加复杂,也通常不会有太多相邻的重复值。排序优先级进一步下降的列基本上会呈现接近随机的顺序,因此通常无法压缩。
列存储通常采用“仅追加”模式,更新操作在新版本执行,这种模式比“原地更新”模式更简单,不需要重新排序和对属性值的重新编码,从而减少磁盘读写次数。
列存储的写操作:面向列的存储、压缩和排序都非常有助于加速读取查询。但是,它们的缺点是让写入更加困难。
聚合:数据立方体与物化视图。
物化视图是包含一个查询结果的数据库对象,它预先计算并保存表连接或聚集等耗时较多的操作。当执行相关查询时,可以直接从物化视图中获得这些数据,而无须从多个表中再依次获取。合理使用物化视图可大大降低数据库中SQL的逻辑读,提高查询效率。
一、物化视图的特点
1) 物化视图是物理化视图的简称,顾名思义,该视图存储实际数据,因此,会占用一定的数据库空间。
2) 物化视图中的数据可以随基表的变化而变化。
3) 物化视图可以加快某些查询操作的速度,但它减慢了DML的速度。
二、物化视图的应用
1) 物化视图对于大数据表的处理显得尤为重要。为了统计一个拥有百万级记录的数据表的总和及平均值问题,将耗费大量数据库资源和时间。可以通过物化视图改善这一状况。
2) 在交互式处理请求可以通过物化视图回答时,可以直接使用物化视图中的聚合数据,而不必使用原始数据表格,从而大幅提升处理速度。
三、物化视图的注意事项
1) 物化视图并不适合统计更新频繁的数据,因为每次的更新都连带更新物化视图,所付出的代价是相当大的。
2) 当底层表的数据发生变化时,物化视图也应当更新到最新数据。
物化视图常见的一种特殊情况称为数据立方体或OLAP立方体。数据立方体或OLAP立方体是数据仓库中多维模型的核心,它由方体的格组成,每个方体都对应于给定多维数据在不同程度上的汇总。数据立方体的计算和探查在数据仓库构建中扮演着至关重要的角色,并且对于多维空间的灵活挖掘是重要的。数据立方体的应用: 数据立方体在电力系统高层分析查询中应用广泛; 数据立方体在大数据分析领域中也有广泛应用,如基于Spark的OLAP计算技术。
在OLTP方面,由两个主要流派的存储引擎:
日志结构流派,它只允许追加式更新文件和删除过时的文件,但不会修改已写入的文件。BitCask、SSTables、LSM-tree、LevelDB、Cassandra、HBase, Lucene等属于此类。
原地更新流派,将磁盘视为可以覆盖的一组固定大小的页。B-tree是这一哲学的最典型代表,它已用于所有主要的关系数据库,以及大量的非关系数据库。
日志结构的存储引擎是一个相对较新的方案。其关键思想是系统地将磁盘上随机访问写入转为顺序写入,由于硬盘驱动器和SSD的性能特性,可以实现更高的写入吞吐量。
第4章:数据编码与演化
当数据格式或模式发生变化时,经常需要对应用程序代码进行相应的调整。为了使系统继续顺利运行,需要保持双向的兼容性:
向后兼容 — 较新的代码可以读取由旧代码编写的数据。
向前兼容 — 较旧的代码可以读取由新代码编写的数据。
数据编码格式
程序通常使用(至少)两种不同的数据表示形式:
1.在内存中,数据保存在对象、结构体、列表、数组、哈希表和树等结构中。
2.将数据写入文件或通过网络发送时,必须将其编码为某种自包含的字节序列(例如JSON文档)。
因此,在这两种表示之间需要进行类型的转化。从内存中的表示到字节序列的转化称为编码(或序列化等),相反的过程称为解码(或解析,反序列化)
- JSON、XML与二进制变体
JSON、XML和CSV都是文本格式,因此具有不错的可读性(尽管语法容易引发争论)。JSON不像XML那么冗长,但与二进制格式相比,两者仍然占用大量空间。这种观察导致开发了大量的二进制编码,用以支持JSON(举几个例子,如MessagePack、BSON、BJSON,UBJSON,BISON和Smile)和XML(如WBXML和Fast Infoset)。 - Thrift与Protocol Buffers
Protocol Buffers和Thrift都是用于数据交换和远程过程调用的框架,它们最初分别由Google和Facebook开发,并且都在2007-2008年开源。这两种框架都需要模式来编码任意的数据,并且都使用接口描述语言来描述模式。
一、Protocol Buffers
1) Protocol Buffers最初是在Google开发的,现在由Google托管在http://code)google)com/p/protobuf。
2) Protocol Buffers是一种基于二进制编码的跨语言、跨平台、易扩展的数据交换格式,广泛应用于服务端通信等场景。
3) Protocol Buffers为常见的服务端开发语言提供了运行时的支持,可以通过它的官方代码仓库进行了解。
二、Thrift
1) Thrift最初是在Facebook开发的,现在是一个Apache项目,托管在http://thrift)apache)org。
2) Thrift是一种接口定义语言,用来为众多语言定义和创建服务。
3) Thrift包含一个完整的软件库,该库可用于创建客户端和服务器,以及创建便于两者之间通信的RPC机制。
字段标签和模式演化
模式不可避免地需要随着时间而不断变化,称之为模式演化。字段标签(field tag)对编码数据的含义至关重要。可以轻松更改模式中字段的名称,而编码永远不直接引用字段名称。但不能随便更改字段的标签,它会导致所有现有编码数据无效。
可以添加新的字段到模式,只要给每个字段一个新的标记号码。为了保持向后兼容性,在模式的初始部署之后添加的每个字段都必须是可选的或具有默认值。只能删除可选的字段(必填字段永远不能被删除),而且不能再次使用相同的标签号码(因为可能仍然有写入的数据包含旧的标签号码,而该字段必须被新代码忽略)。
数据类型和模式演化
如果改变字段的数据类型存在值会丢失精度或被截断的风险。
Protocol Buffers的一个奇怪的细节是,它没有列表或数组数据类型,而是有字段的重复标记(repeated,这是必需和可选之外的第三个选项)。Thrift有专用的列表数据类型,它使用列表元素的数据类型进行参数化。它不支持Protocol Buffers那样从单值到多值的改变,但是它具有支持嵌套列表的优点。 - Avro
Avro也使用模式来指定编码的数据结构。它有两种模式语言:一种(AvroIDL)用于人工编辑,另一种(基于JSON)更易于机器读取。
Apache Avro是一个开源项目,主要用于数据序列化,最初是Hadoop的子项目,后来成为Apache的一个独立项目。Avro提供了丰富的数据结构类型、快速可压缩的二进制数据格式、存储持久性数据的文件集、远程调用(Remote Procedure Call,RPC)的功能和简单的动态语言集成功能。
Avro的关键思想是,写模式和读模式不必是完全一模一样,它们只需保持兼容。使用Avro,向前兼容意味着可以将新版本的模式作为writer,并将旧版本的模式作为reader。相反,向后兼容意味着可以用新版本的模式作为reader,并用旧版本的模式作为writer。为了保持兼容性,只能添加或删除具有默认值的字段。Avro不像Protocol Buffers和Thrift那样具有可选和必需的标签(而是有联合类型和默认值)。只要Avro可以转换类型,就可以改变字段的数据类型。在任何情况下,提供一个模式版本信息的数据库都非常有用,它可以充当一个说明文档来检查模式兼容性情况。与Protocol Buffers和Thrift相比,Avro方法的一个优点是不包含任何标签号。关键之处在于Avro对动态生成的模式更友好。Thrift和Protocol Buffers依赖于代码生成:在定义了模式之后,可以使用选择的编程语言生成实现此模式的代码。对于动态生成的模式(例如从数据库表生成的Avro模式)的情况,代码生成对获取数据反而是不必要的障碍。Avro为静态类型编程语言提供了可选的代码生成,但是它也可以在不生成代码的情况下直接使用。
Protocol Buffers、Thrift和Avro都使用了模式来描述二进制编码格式。它们的模式语言比XML模式或JSON模式简单得多,它支持更详细的验证规则。
一、Avro的主要特性
1) 动态类型:Avro无须生成代码,数据总是伴以模式定义,这样就可以在不生成代码、静态数据类型的情况下对数据进行所有处理,有利于构建通用的数据处理系统和语言。
2) 无标记数据:由于在读取数据时有模式定义,这就大大减少了数据编辑所需的类型信息,从而减少序列化空间开销。
3) 不用手动分配的字段ID:当数据模式发生变化,处理数据时总是同时提供新旧模式,差异就可以用字段名来做符号化的分析。
二、Avro的应用
1) Avro和动态语言结合后,读写数据文件和使用RPC协议都不需要生成代码,而代码生成作为一种可选的优化,只需要在静态类型语言中实现。
2) 很多知名开源项目都应用了Avro,包括Hadoop、Cassandra等。
三、Avro与Protocol Buffers的区别
1) Avro采用了预先定义的Schema(模式)来描述对象的序列化结构,从而无须编译。
2) 与Protocol Buffers相比,在数据之间不存在其他任何标识,有利于提高数据处理效率。数据流模式
- 基于数据库的数据流
在数据库中,写入数据库的进程对数据进行编码,而读取数据库的进程对数据进行解码。数据库通常支持在任何时候更新任何值。模式演化支持整个数据库看起来像是采用单个模式编码,即使底层存储可能包含各个版本模式所编码的记录。数据转储通常使用最新的模式进行编码,即使源数据库中的原始编码包含了不同时代的各种模式版本。由于无论如何都要复制数据,所以此时最好对数据副本进行统一的编码。 - 基于服务的数据流:REST和RPC
当HTTP被用作与服务通信的底层协议时,它被称为Web服务。有两种流行的Web服务方法:REST和SOAP。
REST不是一种协议,而是一个基于HTTP原则的设计理念。它强调简单的数据格式,使用URL来标识资源,并使用HTTP功能进行缓存控制、身份验证和内容类型协商。
根据REST原则所设计的API称为RESTful。SOAP是一种基于XML的协议,用于发出网络API请求注。
SOAP Web服务的API使用被称为WSDL(Web Services Description Language,一种基于XML的语言)来描述。 - 远程过程调用(Remote Procedure Call,RPC)的问题
RPC模型试图使向远程网络服务发出请求看起来与在同一进程中调用编程语言中的函数或方法相同(这种抽象称为位置透明)。网络请求与本地函数调用非常不同:本地函数调用是可预测的,并且成功或失败仅取决于控制的参数。网络请求是不可预测的:请求或响应可能由于网络问题而丢失,或者远程计算机可能速度慢或不可用,这些问题完全不在控制范围之内。本地函数调用要么返回一个结果,要么抛出一个异常,或者永远不会返回(因为进入无限循环或者进程崩溃)。网络请求有另一个可能的结果:由于超时,它返回时可能没有结果。
如果重试失败的网络请求,可能会发生请求实际上已经完成,只是响应丢失的情况。在这种情况下,重试将导致该操作被执行多次,除非在协议中建立重复数据消除(幂等性)机制。每次调用本地函数时,通常需要大致相同的时间来执行。网络请求比函数调用要慢得多,而且其延迟也有很大的变化。调用本地函数时,可以高效地将引用(指针)传递给本地内存中的对象。当发出网络请求时,所有这些参数都需要被编码成可以通过网络发送的字节序列。客户端和服务可以用不同的编程语言来实现,所以RPC框架必须将数据类型从一种语言转换成另一种语言。RPC的发展方向:由于这些原因,REST似乎是公共API的主流风格。RPC框架主要侧重于同一组织内多项服务之间的请求,通常发生在同一数据中心内。
RPC的数据编码和演化:
RPC方案的向后和向前兼容性属性取决于它所使用的具体编码技术:Thrift、gRPC(Protocol Buffers)和Avro RPC可以根据各自编码格式的兼容性规则进行演化。如果不得不进行一些破坏兼容性的更改,则服务提供者往往会同时维护多个版本的服务API。 - 基于消息传递的数据流
与直接RPC相比,使用消息代理有以下几个优点:
·如果接收方不可用或过载,它可以充当缓冲区,从而提高系统的可靠性。
·它可以自动将消息重新发送到崩溃的进程,从而防止消息丢失。
·它避免了发送方需要知道接收方的IP地址和端口号(这在虚拟机经常容易起起停停的云部署中特别有用)。
.它支持将一条消息发送给多个接收方。
·它在逻辑上将发送方与接收方分离(发送方只是发布消息,并不关心谁使用它们)。
RPC的差异在于,消息传递通信通常是单向的:发送方通常不期望收到对其消息的回复。 - 消息代理
详细的传递语义因实现和配置而异,但通常情况下,消息代理的使用方式如下:一个进程向指定的队列或主题发送消息,并且代理确保消息被传递给队列或主题的一个或多个消费者或订阅者。在同一主题上可以有许多生产者和许多消费者。主题只提供单向数据流。但是,消费者本身可能会将消息发布到另一个主题,也可以发送到一个回复队列,该队列由原始消息发送者来消费(这样支持类似RPC的请求/响应数据流)。
消息代理通常不会强制任何特定的数据模型,消息只是包含一些元数据的字节序列, 因此可以使用任何编码格式。 - 分布式Actor框架
Actor模型是用于单个进程中并发的编程模型。第二部分:分布式数据系统
第5章:数据复制
复制主要指通过互联网络在多台机器上保存相同数据的副本。
复制或者多副本技术主要服务于以下目的:
·连接断开与容错:允许应用程序在出现网络中断时继续工作。
.低延迟:将数据放置在距离用户较近的地方,从而实现更快地交互。
.可扩展性:采用多副本读取,大幅提高系统读操作的吞吐量。
主要讨论了三种多副本方案: - 主从复制
所有的客户端写入操作都发送到某一个节点(主节点),由该节点负责将数据更改事件发送到其他副本(从节点)。每个副本都可以接收读请求,但内容可能是过期值。 - 多主节点复制
系统存在多个主节点,每个都可以接收写请求,客户端将写请求发送到其中的一个主节点上,由该主节点负责将数据更改事件同步到其他主节点和自己的从节点。 - 无主节点复制
客户端将写请求发送到多个节点上,读取时从多个节点上并行读取,以此检测和纠正某些过期数据。
主从复制非常流行,主要是因为它很容易理解,也不需要担心冲突问题。而万一出现节点失效、网络中断或者延迟抖动等情况,多主节点和无主节点复制方案会更加可靠,不过背后的代价则是系统的复杂性和弱一致性保证。在系统稳定状态下异步复制性能优秀,但仍须认真考虑一旦出现复制滞后和节点失效两种场景会导致何种影响。
以下一致性模型,来帮助应用程序处理复制滞后:
·写后读一致性 – 保证用户总能看到自己所提交的最新数据。
·单调读 – 用户在某个时间点读到数据之后,保证此后不会出现比该时间点更早的数据。
·前缀一致读 – 保证数据之间的因果关系,例如,总是以正确的顺序先读取问题,然后看到回答。
多主节点和无主节点复制方案所引入的并发问题,即由于多个写可能同时发生,继而可能产生冲突。
第6章:数据分区
数据量如果太大,单台机器进行存储和处理就会成为瓶颈,因此需要引入数据分区机制。分区的目地是通过多台机器均匀分布数据和查询负载,避免出现热点。两种主要的分区方法:
- 基于关键字区间的分区。
先对关键字进行排序,每个分区只负责一段包含最小到最大关键字范围的一段关键字。对关键字排序的优点是可以支持高效的区间查询,但是如果应用程序经常访问与排序一致的某段关键字,就会存在热点的风险。采用这种方法,当分区太大时,通常将其分裂为两个子区间,从而动态地再平衡分区。 - 哈希分区。
将哈希函数作用于每个关键字,每个分区负责一定范围的哈希值。区间查询效率比较低,但可以更均匀地分配负载。采用哈希分区时,通常事先创建好足够多(但固定数量)的分区,让每个节点承担多个分区,当添加或删除节点时将某些分区从一个节点迁移到另一个节点,也可以支持动态分区。
二级索引也需要进行分区,有两种方法:基于文档来分区二级索引(本地索引)。基于词条来分区二级索引(全局索引)。
第7章:事务
事务将应用程序的多个读、写操作捆绑在一起成为一个逻辑操作单元。即事务中的所有读写是一个执行的整体,整个事务要么成功(提交)、要么失败(中止或回滚)。如果失败,应用程序可以安全地重试。这样,由于不需要担心部分失败的情况(无论出于何种原因),应用层的错误处理就变得简单很多。有了事务,应用程序可以不用考虑某些内部潜在的错误以及复杂的并发性问题,这些都可以交给数据库来负责处理(我们称之为安全性保证)。
事务作为一个抽象层,使得应用程序可以忽略数据库内部一些复杂的并发问题,以及某些硬件、软件故障,从而简化应用层的处理逻辑,大量的错误可以转化为简单的事务中止和应用层重试。
读-提交,快照隔离(或可重复读取)与可串行化是数据库事务处理中的三种隔离级别。它们都是为了解决并发事务对数据的影响,保证数据的一致性和完整性。
一、读-提交
读-提交是数据库事务处理的一种隔离级别,它要求一个事务只能看到已经提交的其他事务的结果。这种隔离级别可以防止脏读,即一个事务读取到另一个事务未提交的数据。但是,它不能防止不可重复读和幻读。
二、快照隔离
快照隔离是一种数据库事务处理的技术,它可以让每个事务都从数据库的一致性快照中读取数据。事务最开始能看到的数据只能是已经完成提交的数据,并且是最近一次提交的事务。这种技术可以防止脏读和不可重复读,但不能防止幻读。
三、可串行化
可串行化是数据库事务处理的最高隔离级别,它要求并发的事务执行的效果跟按某种顺序串行执行效果一样。通常的实现方法就是事务期间访问的数据全程加锁,以防止事务期间访问的数据被其他事务修改了。这种隔离级别可以防止所有的数据异常现象,包括脏读、不可重复读和幻读。
·脏读
客户端读到了其他客户端尚未提交的写入。读-提交以及更强的隔离级别可以防止脏读。
·脏写
客户端覆盖了另一个客户端尚未提交的写入。几乎所有的数据库实现都可以防止脏写。
·读倾斜(不可重复读)
客户在不同的时间点看到了不同值。快照隔离是最用的防范手段,即事务总是在某个时间点的一致性快照中读取数据。通常采用多版本并发控制(MVCC)来实现快照隔离。
·更新丢失
两个客户端同时执行读-修改-写入操作序列,出现了其中一个覆盖了另一个的写入,但又没有包含对方最新值的情况,最终导致了部分修改数据发生了丢失。
·写倾斜
事务首先查询数据,根据返回的结果而作出某些决定,然后修改数据库。当事务提交时,支持决定的前提条件已不再成立。只有可串行化的隔离才能防止这种异常。
·幻读
事务读取了某些符合查询条件的对象,同时另一个客户端执行写入,改变了先前的查询结果。快照隔离可以防止简单的幻读,但写倾斜情况则需要特殊处理,例如采用区间范围锁。
实现可串行化隔离的三种不同方法:严格串行执行事务;两阶段加锁;可串行化的快照隔离(SSI)。
第8章:分布式系统的挑战
分布式系统中可能发生的各种典型问题,包括:当通过网络发送数据包时,数据包可能会丢失或者延迟;同样,回复也可能会丢失或延迟。
节点的时钟可能会与其他节点存在明显的不同步(尽管尽最大努力设置了NTP服务器),时钟还可能会突然向前跳跃或者倒退,依靠精确的时钟存在一些风险,没有特别简单的办法来精确测量时钟的偏差范围。进程可能在执行过程中的任意时候遭遇长度未知的暂停(一个重要的原因是垃圾回收),结果它被其他节点宣告为失效,尽管后来又恢复执行,却对中间的暂停毫无所知。只要软件试图跨节点做任何事情,就有可能出现失败,或者随机变慢,或者根本无应答(最终超时)。对于分布式环境,我们的目标是建立容忍部分失效的软件系统,这样即使某些部件发生失效,系统整体还可以继续运行。
可扩展性并不是使用分布式系统的唯一原因。容错与低延迟(将数据放置在距离用户较近的地方)也是同样重要的目标,而后两者无法靠单节点来实现。
高性能计算多采用更加可靠的组件,发生故障时完全停止系统,之后重新启动。相比之下,分布式系统会长时间不间断运行,以避免影响服务级别。
第9章:一致性与共识
线性化(一种流行的一致性模型):其目标是使多副本对外看起来好像是单一副本,然后所有操作以原子方式运行,就像一个单线程程序操作变量一样。线性化的概念简单,容易理解,看起来很有吸引力,但它的主要问题在于性能,特别是在网络延迟较大的环境中。
因果关系对事件进行了某种排序(根据事件发生的原因-结果依赖关系)。因果一致性避免了线性化昂贵的协调开销,且对网络延迟的敏感性要低很多。共识意味着就某一项提议,所有节点做出一致的决定,而且决定不可撤销。
ZooKeeper等工具以一种类似外包方式为应用提供了重要的共识服务、故障检测和成员服务等。并不是每个系统都需要共识。例如无主复制和多主复制复制系统通常并不支持全局共识。
第三部分:派生数据
第10章:批处理系统
区分三种不同类型的系统:
·在线服务(或称在线系统)
服务等待客户请求或指令的到达。当收到请求或指令时,服务试图尽可能快地处理它,并发回一个响应。响应时间通常是服务性能的主要衡量指标,而可用性同样非常重要(如果客户端无法访问服务,用户可能会收到一个报错消息)。
·批处理系统(或称离线系统)
批处理系统接收大量的输入数据,运行一个作业来处理数据,并产生输出数据。作业往往需要执行一段时间(从几分钟到几天),所以用户通常不会等待作业完成。相反,批量作业通常会定期运行(例如,每天一次)。批处理作业的主要性能衡量标准通常是吞吐量(处理一定大小的输入数据集所需的时间)。
· 流处理系统(或称近实时系统)
流处理介于在线与离线/批处理之间(所以有时称为近实时或近线处理)。与批处理系统类似,流处理系统处理输入并产生输出(而不是响应请求)。但是,流式作业在事件发生后不久即可对事件进行处理,而批处理作业则使用固定的一组输入数据进行操作。这种差异使得流处理系统比批处理系统具有更低的延迟。
分布式批处理框架需要解决的两个主要问题是:分区;容错 。
分布式批处理引擎有一个有意限制的编程模型:回调函数(如mapper和reducer)被设定为无状态,并且除了指定输出之外没有外部可见的任何副作用。这个限制使得框架隐藏了抽象背后的一些困难的分布式系统问题,从而在面对崩溃和网络问题时,可以安全地重试任务,并丢弃任何失败任务的输出。如果针对某个分区的多个任务都成功了,则实际上只会有其中一个任务使其输出可见。
批处理作业的显著特点是它读取一些输入数据并产生一些输出数据,而不修改输入。换句话说,输出是从输入派生而来。至关重要的是,输入数据是有界的:数据大小固定已知(例如,它包含一些时间点的日志文件或数据库内容的快照)。因为它是有界的,所以一个作业总是可以知道何时完成了对整个输入的读取,何时作业最终完成。
第11章: 流处理系统
流处理长期以来一直被用于监控目的, 即希望在发生某些特定事件时收到警报。
与批处理一样,我们需要丢弃所有失败任务的部分输出。然而,由于流处理长时间运行并持续产生输出,所以不能把所有的输出都简单地丢弃掉。相反,基于微批处理、检查点、事务或幂等性写入等,可以实现更细粒度的恢复机制。
第12章:数据系统的未来
强的完整性保证可以通过异步事件处理,通过使用端到端操作标识使操作最终满足幂等性,并通过异步检查约束来实现。客户可以等到检查通过或者不用等,但是如果万一违反约束则需要事后道歉。这种方法比使用分布式事务的传统方法更具可扩展性和可靠性,并且适合于实践中有多个业务流程同时工作的场景。
虽然数据可以用来帮助人们,但是也可能造成重大的伤害:做出严重影响人们生活的貌似公平的决定,这种算法决定难以对其提起诉讼;导致歧视和剥削;使监视泛滥;暴露私密信息等。我们也面临着数据泄露的风险,而且即使善意的数据使用也可能会产生某些意想不到的后果。
软件工程师如果只专注于技术而忽视其后果是不够的,道德责任也是我们要担起的责任。评判道德总是困难的,但它太重要了以至无论如何不能被忽视。
- 算法囚笼
算法囚笼是指人们在享受算法带来的便利的同时,可能会在认知与决策、消费、社会位置、劳动等多方面受到算法的限制和操控。这种限制和操控可能源于算法的不透明性,使得人们无法了解其运作机制,从而在算法出现问题后,连制造者自身也难以解释或者溯源。
一、算法囚笼的表现
1) 算法可能带来认知与决策的囚禁,如人们可能因为自身的身份和原有的社会位置等,被决策算法打下某种标记,从而获得更多的资源和向上流动的可能性,而原来处于不利地位的人,则因标记处于更加不利的地位,从而失去就业、获得投资等相应机会。
2) 算法可能带来消费的囚禁,如人们可能因为算法的个性化推送,陷入“信息茧房”,闭目塞听,加深“信息窄化”的程度。
3) 算法可能带来社会位置的囚禁,如人们可能因为算法的影响,被系统地排除在包括工作、航空旅行、保险、物业租赁、金融服务以及其他社会关键方面之外。
4) 算法可能带来劳动的囚禁,如平台或商家可以对劳动者进行严密控制,这种控制可能演变为劳动者的自我约束与自我激励,个别劳动者甚至变成“永动机”或“恒点工”。
二、算法囚笼的成因
1) 算法的不透明性,使得人们无法了解其运作机制,从而在算法出现问题后,连制造者自身也难以解释或者溯源。
2) 算法的设计和应用可能过于追求效率和利润,忽视了事件处理中的公平,以及公平对于劳动者和社会的重要意义。
三、算法囚笼的应对
1) 提高算法素养,帮助人们认识算法风险,提高反囚禁能力。
2) 平台和监管者都应该在服务评价方面适当引入更多对骑手和司机的保护机制,并避免恶意差评对劳动者的伤害。
3) 反垄断机构和劳动保护机构应要求平台企业不得以“算法优化”为名频繁调整单位时间或绩效的工资回报率,特别是对工资率的下调和对劳动时间与强度要求的上调。
如果在算法的输入中存在系统性偏见,那么系统很可能吸收塔并在最终输出中放大这种偏见!预测分析系统只是基于过去而推断,如果过去是有偏见的,它们就会把这种偏见编码下来。数据和模型只应该是我们的工具,而不是我们的主人。
自动决策引发了责任与问责方面的问题。预测系统的输出是概率性的,在个别情况下也可能是错误的。
盲目地相信数据至高无上不仅是误解的,而且是非常危险的。随着数据驱动的决策变得越来越普遍,我们需要弄清楚如何使算法更负责任和透明,如何避免强化现有的偏见,以及如何在错误不可避免时加以修复。还需要弄清楚如何防止数据被滥用,并努力发挥数据的正面作用。
通过思考整个系统(不仅是计算机化的部分,还有与之互动的人)可以预测许多后果,这是一种被称为系统思维的方法。
我们可以尝试理解一个数据分析系统是如何响应不同的行为、结构和特征。系统是否强化和扩大了人们之间存在的差异(例如,使富者变得更富或穷人变得更穷)? 即使有最好的意图,我们也必须小心意外的后果。 - 数据隐私与追踪
除了预测分析方面的问题(即使用数据来做出关于人的自动化决策)之外,数据收集本身也存在道德问题。用户得到免费的服务,并尽可能地被引诱参与到服务中。对用户的追踪不再是服务与个人,而是服务于资助广告客户的需求。我认为这种关系可以用一个更阴暗的词来描述:监视。在我们试图依靠软件“掌控世界”过程中,我们建立了世界上迄今为止最大规模的监视基础设施。并非所有的数据收集都必须满足监控的要求,但是检查这些数据可以帮助理解我们与数据收集者的关系。 - 赞成与选择的自由
用户几乎不知道什么样的个人数据会进入到数据库,或者数据是如何保留和处理的,大多数隐私政策的条款也极尽所能地搞得含混不清。
而且,数据是通过单向过程从用户提取而来,而不是通过真正的互惠关系,也不是公平的价值交换。对于不同意被监视的用户,唯一真正的选择就是不使用服务。当一个服务具有网络效应时,人们选择不使用它是有社会成本的。对于处境较差的人来说,选择自由没有意义:对他们来说,被监视变得不可避免。 - 数据隐私和使用
拥有隐私并不意味着一切事情都要保密;它意味着你可以自由选择向谁展示,并展示哪些东西,要公开什么,以及要保密什么。隐私权是一个决定权:每个人都能够决定在各种情况下如何在保密和透明之间取舍。现在不是用户根据自己的喜好决定向谁显示,显示哪些隐私内容;做决定的是公司,公司通常会以最大化利润为目标来处置隐私权。所谓隐私设置,即允许在线服务的用户控制哪些数据对他人可见,只是将一些控制权还给用户的起点。但是,无论设置如何,服务本身仍然可以不受限制地访问数据,并且可以以隐私策略所允许的任何方式自由地使用它。即使服务承诺不会将数据出售给第三方,它通常也授予自身无限制的权利,在内部处理和分析数据时往往远远超过用户公开可见的权利。
数据作为资产和权力,即使我们认为有能力防止数据滥用,但是每当我们收集数据时,都需要平衡带来的好处和落入坏人手中的风险:计算机系统可能会被犯罪分子篡改,数据可能会被内部人员泄露,公司可能会落入不当价值观的无良管理层手中等。
数据是信息时代的关键性特征。数据问题是信息时代的污染问题。数据保护法可能有助于维护个人的权利。我们应该停止过度以用户为衡量指标,牢记用户值得尊重。我们应该主动调整数据收集和处理流程,建立和维持与那些依赖我们软件的人们之间的信任关系。我们应该允许每个人维护自己的隐私,即控制自己的数据而不是通过监视来窃取他们的控制权。我们不应该永远保留数据,一旦不再需要,就尽快清除它们。我所看到的一个很有前途的方法是通过加密协议来实施访问控制,而不仅仅是通过策略。