可靠性: 系统在困境(adversity)(硬件故障、软件故障、人为错误)中仍可正常工作(正确完成功能,并能达到期望的性能水准。
可靠性(Reliability) 意味着即使发生故障,系统也能正常工作。故障可能发生在硬件(通常是随机的和不相关的),软件(通常是系统性的Bug,很难处理),和人类(不可避免地时不时出错)。 容错技术 可以对终端用户隐藏某些类型的故障。
可扩展性: 有合理的办法应对系统的增长(数据量、流量、复杂性)
可扩展性(Scalability) 意味着即使在负载增加的情况下也有保持性能的策略。为了讨论可扩展性,我们首先需要定量描述负载和性能的方法。我们简要了解了推特主页时间线的例子,介绍描述负载的方法,并将响应时间百分位点作为衡量性能的一种方式。在可扩展的系统中可以添加 处理容量(processing capacity) 以在高负载下保持可靠。
可维护性:许多不同的人(工程师、运维)在不同的生命周期,都能高效地在系统上工作(使系统保持现有行为,并适应新的应用场景)
可维护性(Maintainability) 有许多方面,但实质上是关于工程师和运维团队的生活质量的。良好的抽象可以帮助降低复杂度,并使系统易于修改和适应新的应用场景。良好的可操作性意味着对系统的健康状态具有良好的可见性,并拥有有效的管理手段。
文档数据库的应用场景是:数据通常是自我包含的,而且文档之间的关系非常稀少
图形数据库用于相反的场景:任意事物都可能与任何事物相关联
在高层次上,我们看到存储引擎分为两大类:优化 事务处理(OLTP) 或 在线分析(OLAP) 。这些用例的访问模式之间有很大的区别:
OLTP系统通常面向用户,这意味着系统可能会收到大量的请求。为了处理负载,应用程序通常只访问每个查询中的少部分记录。应用程序使用某种键来请求记录,存储引擎使用索引来查找所请求的键的数据。磁盘寻道时间往往是这里的瓶颈。
数据仓库和类似的分析系统会低调一些,因为它们主要由业务分析人员使用,而不是由最终用户使用。它们的查询量要比OLTP系统少得多,但通常每个查询开销高昂,需要在短时间内扫描数百万条记录。磁盘带宽(而不是查找时间)往往是瓶颈,列式存储是这种工作负载越来越流行的解决方案。
Json XML 编码 --> 二进制编码的发展
二进制编码技术:Apache Thrift / Protocol Buffers(protobuf)/
服务中的数据流:REST & RPC
REST不是一个协议,而是一个基于HTTP原则的设计哲学。它强调简单的数据格式,使用URL来标识资源,并使用HTTP功能进行缓存控制,身份验证和内容类型协商。与SOAP相比,REST已经越来越受欢迎,至少在跨组织服务集成的背景下,并经常与微服务相关。根据REST原则设计的API称为restful, 通常涉及较少的代码生成和自动化工具。
RPC调用和本地函数调用的不同:
复制意味着在通过网络连接的多台机器上保留相同数据的副本,需要复制的原因:
复制算法:单领导者(single leader),多领导者(multi leader)和无领导者(leaderless)
基于领导者的复制原理:
同步复制和异步复制:
同步复制的优点是,从库保证有与主库一致的最新数据副本。如果主库突然失效,我们可以确信这些数据仍然能在从库上上找到。缺点是,如果同步从库没有响应(比如它已经崩溃,或者出现网络故障,或其它任何原因),主库就无法处理写入操作。主库必须阻止所有写入,并等待同步副本再次可用。
因此,将所有从库都设置为同步的是不切实际的:任何一个节点的中断都会导致整个系统停滞不前。实际上,如果在数据库上启用同步复制,通常意味着其中一个跟随者是同步的,而其他的则是异步的。如果同步从库变得不可用或缓慢,则使一个异步从库同步。这保证你至少在两个节点上拥有最新的数据副本:主库和同步从库。 这种配置有时也被称为 半同步。
通常情况下,基于领导者的复制都配置为完全异步。 在这种情况下,如果主库失效且不可恢复,则任何尚未复制给从库的写入都会丢失。 这意味着即使已经向客户端确认成功,写入也不能保证 持久(Durable)。 然而,一个完全异步的配置也有优点:即使所有的从库都落后了,主库也可以继续处理写入。
如何确保新的从库拥有主库数据的精确副本?
节点宕机:
从库失效:追赶恢复
在其本地磁盘上,每个从库记录从主库收到的数据变更。如果从库崩溃并重新启动,或者,如果主库和从库之间的网络暂时中断,则比较容易恢复:从库可以从日志中知道,在发生故障之前处理的最后一个事务。因此,从库可以连接到主库,并请求在从库断开连接时发生的所有数据变更。当应用完所有这些变化后,它就赶上了主库,并可以像以前一样继续接收数据变更流。
主库失效:故障切换
故障切换的麻烦:
如果使用异步复制,则新主库可能没有收到老主库宕机前最后的写入操作
如果数据库需要和其他外部存储相协调,那么丢弃写入内容是极其危险的操作。
发生某些故障时可能会出现两个节点都以为自己是主库的情况,这种情况称为 脑裂(split brain)。一些系统采取了安全防范措施:当检测到两个主库节点同时存在时会关闭其中一个节点ii,但设计粗糙的机制可能最后会导致两个节点都被关闭。
复制日志的实现:
1、基于语句的复制
2、传输预写式日志(WAL)
3、逻辑日志复制(基于行)
另一种方法是,复制和存储引擎使用不同的日志格式,这样可以使复制日志从存储引擎内部分离出来。这种复制日志被称为逻辑日志,以将其与存储引擎的(物理)数据表示区分开来。
4、基于触发器的复制
复制延迟问题
1、读己之写
图 用户写入后从旧副本中读取数据。需要写后读(read-after-write)的一致性来防止这种异常
2、单调读
图 用户首先从新副本读取,然后从旧副本读取。时光倒流。为了防止这种异常,我们需要单调的读取。
3、一致前缀读
图 如果某些分区的复制速度慢于其他分区,那么观察者在看到问题之前可能会看到答案。
复制延迟问题的解决方案:事务(性能和可用性代价过高)和其他替代机制
多主复制
应用场景
1、运维多个数据中心
图 跨多个数据中心的多主复制
2、需要离线的客户端
考虑手机,笔记本电脑和其他设备上的日历应用。无论设备目前是否有互联网连接,你需要能随时查看你的会议(发出读取请求),输入新的会议(发出写入请求)。如果在离线状态下进行任何更改,则设备下次上线时,需要与服务器和其他设备同步。
在这种情况下,每个设备都有一个充当领导者的本地数据库(它接受写请求),并且在所有设备上的日历副本之间同步时,存在异步的多主复制过程。复制延迟可能是几小时甚至几天,具体取决于何时可以访问互联网。
从架构的角度来看,这种设置实际上与数据中心之间的多领导者复制类似,每个设备都是一个“数据中心”,而它们之间的网络连接是极度不可靠的。从历史上各类日历同步功能的破烂实现可以看出,想把多活配好是多么困难的一件事。
3、协同编辑
我们通常不会将协作式编辑视为数据库复制问题,但与前面提到的离线编辑用例有许多相似之处。当一个用户编辑文档时,所做的更改将立即应用到其本地副本(Web浏览器或客户端应用程序中的文档状态),并异步复制到服务器和编辑同一文档的任何其他用户。
如果要保证不会发生编辑冲突,则应用程序必须先取得文档的锁定,然后用户才能对其进行编辑。如果另一个用户想要编辑同一个文档,他们首先必须等到第一个用户提交修改并释放锁定。这种协作模式相当于在领导者上进行交易的单领导者复制。
但是,为了加速协作,您可能希望将更改的单位设置得非常小(例如,一个按键),并避免锁定。这种方法允许多个用户同时进行编辑,但同时也带来了多领导者复制的所有挑战,包括需要解决冲突。
处理写入冲突(多领导者复制的最大问题)
图 两个主库同时更新同一记录引起的写入冲突
无主复制
当节点故障时写入数据库
图5-10 仲裁写入,法定读取,并在节点中断后读修复。
检测并发写入
图 并发写入Dynamo风格的数据存储:没有明确定义的顺序。
捕获"此前发生"关系
最初,购物车是空的。在它们之间,客户端向数据库发出五次写入:
分区与复制
分区通常与复制结合使用,使得每个分区的副本存储在多个节点上。
图6-1 组合使用复制和分区:每个节点充当某些分区的领导者,其他分区充当追随者。
键值数据的分区
1、根据键的范围分区(字典)
2、根据键的散列分区
分区与次级索引
次级索引是关系型数据库的基础,并且在文档数据库中也很普遍。许多键值存储(如HBase和Volde-mort)为了减少实现的复杂度而放弃了次级索引,但是一些(如Riak)已经开始添加它们,因为它们对于数据模型实在是太有用了。并且次级索引也是Solr和Elasticsearch等搜索服务器的基石。
次级索引的问题是它们不能整齐地映射到分区。有两种用二级索引对数据库进行分区的方法:基于文档的分区(document-based)和基于关键词(term-based)的分区
按文档的二级索引
根据关键词(Term)的二级索引
分区再平衡
随着时间的推移,数据库会有各种变化。
所有这些更改都需要数据和请求从一个节点移动到另一个节点。 将负载从集群中的一个节点向另一个节点移动的过程称为再平衡(reblancing)。
无论使用哪种分区方案,再平衡通常都要满足一些最低要求:
平衡策略:
* 反面教材:hash mod N * 固定数量的分区
* 动态分区 * 按节点比例分区
请求路由(客户端发送请求时连接数据库的那个节点?)
许多分布式数据系统都依赖于一个独立的协调服务,比如ZooKeeper来跟踪集群元数据。 下图每个节点在ZooKeeper中注册自己,ZooKeeper维护分区到节点的可靠映射。 其他参与者(如路由层或分区感知客户端)可以在ZooKeeper中订阅此信息。 只要分区分配发生的改变,或者集群中添加或删除了一个节点,ZooKeeper就会通知路由层使路由信息保持最新状态。
ACID
原子性(Atomicity)
在多线程编程中,如果一个线程执行一个原子操作,这意味着另一个线程无法看到该操作的一半结果。系统只能处于操作之前或操作之后的状态,而不是介于两者之间的状态。
ACID原子性的定义特征是:能够在错误时中止事务,丢弃该事务进行的所有写入变更的能力。如果这些写操作被分组到一个原子事务中,并且该事务由于错误而不能完成(提交),则该事务将被中止,并且数据库必须丢弃或撤消该事务中迄今为止所做的任何写入。
一致性(Consistency)
对数据的一组特定陈述必须始终成立。即不变量(invariants)。例如,在会计系统中,所有账户整体上必须借贷相抵。如果一个事务开始于一个满足这些不变量的有效数据库,且在事务处理期间的任何写入操作都保持这种有效性,那么可以确定,不变量总是满足的。
原子性,隔离性和持久性是数据库的属性,而一致性(在ACID意义上)是应用程序的属性。应用可能依赖数据库的原子性和隔离属性来实现一致性,但这并不仅取决于数据库。
隔离性(Isolation)
同时执行的事务是相互隔离的:它们不能相互冒犯。大多数数据库都会同时被多个客户端访问。如果它们各自读写数据库的不同部分,这是没有问题的,但是如果它们访问相同的数据库记录,则可能会遇到并发问题。
持久性(Durability)
持久性 是一个承诺,即一旦事务成功完成,即使发生硬件故障或数据库崩溃,写入的任何数据也不会丢失。
单对象和多对象操作
图 违反隔离性:一个事务读取另一个事务的未被执行的写入(“脏读”)
没有原子性,错误处理就要复杂得多,缺乏隔离性,就会导致并发问题。
事务隔离级别
读已提交(Read Committed)
图 没有脏读:用户2只有在用户1的事务已经提交后才能看到x的新值。
图 如果存在脏写,来自不同事务的冲突写入可能会混淆在一起
读取偏差(不可重复读)
在同一个事务中,客户端在不同的时间点会看见数据库的不同状态。快照隔离经常用于解决这个问题。快照隔离的实现通常使用写锁来防止脏写,从性能的角度来看,快照隔离的一个关键原则是:读不阻塞写,写不阻塞读。为了实现快照隔离,数据库必须可能保留一个对象的几个不同的提交版本,这种技术被称为多版本并发控制。
如果一个数据库只需要提供读已提交的隔离级别,而不提供快照隔离,那么保留一个对象的两个版本就足够了:提交的版本和被覆盖但尚未提交的版本。支持快照隔离的存储引擎通常也使用MVCC来实现读已提交隔离级别。一种典型的方法是读已提交为每个查询使用单独的快照,而快照隔离对整个事务使用相同的快照。
图中,当事务12 从账户2 读取时,它会看到 $500 的余额,因为 $500 余额的删除是由事务13 完成的(根据规则3,事务12 看不到事务13 执行的删除),且400美元记录的创建也是不可见的(按照相同的规则)
图 使用多版本对象实现快照隔离
更新丢失
两个客户端同时执行**读取-修改-写入序列**。其中一个写操作,在没有合并另一个写入变更情况下,直接覆盖了另一个写操作的结果。所以导致数据丢失。快照隔离的一些实现可以自动防止这种异常,而另一些实现则需要手动锁定(`SELECT FOR UPDATE`)。
幻读
事务读取符合某些搜索条件的对象。另一个客户端进行写入,影响搜索结果。快照隔离可以防止直接的幻像读取,但是写入歪斜环境中的幻影需要特殊处理,例如索引范围锁定。
在存储过程中封装事务
即使人类已经找到了关键路径,事务仍然以交互式的客户端/服务器风格执行,一次一个语句。应用程序进行查询,读取结果,可能根据第一个查询的结果进行另一个查询,依此类推。查询和结果在应用程序代码(在一台机器上运行)和数据库服务器(在另一台机器上)之间来回发送。
在这种交互式的事务方式中,应用程序和数据库之间的网络通信耗费了大量的时间。如果不允许在数据库中进行并发处理,且一次只处理一个事务,则吞吐量将会非常糟糕,因为数据库大部分的时间都花费在等待应用程序发出当前事务的下一个查询。在这种数据库中,为了获得合理的性能,需要同时处理多个事务。
出于这个原因,具有单线程串行事务处理的系统不允许交互式的多语句事务。取而代之,应用程序必须提前将整个事务代码作为存储过程提交给数据库。这些方法之间的差异如图所示。如果事务所需的所有数据都在内存中,则存储过程可以非常快地执行,而不用等待任何网络或磁盘I/O。
图 交互式事务和存储过程之间的区别
存储过程与内存存储,使得在单个线程上执行所有事务变得可行。由于不需要等待I/O,且避免了并发控制机制的开销,它们可以在单个线程上实现相当好的吞吐量。
可序列化快照隔离(SSI, serializable snapshot isolation)
检测旧MVCC读取(读之前存在未提交的写入)
当一个事务从MVCC数据库中的一致快照读时,它将忽略取快照时尚未提交的任何其他事务所做的写入。上图中,事务43 认为Alice的 on_call = true
,因为事务42(修改Alice的待命状态)未被提交。然而,在事务43想要提交时,事务42 已经提交。这意味着在读一致性快照时被忽略的写入已经生效,事务43 的前提不再为真。
为了防止这种异常,数据库需要跟踪一个事务由于MVCC可见性规则而忽略另一个事务的写入。当事务想要提交时,数据库检查是否有任何被忽略的写入现在已经被提交。如果是这样,事务必须中止。
为什么要等到提交?当检测到陈旧的读取时,为什么不立即中止事务43 ?因为如果事务43 是只读事务,则不需要中止,因为没有写入偏差的风险。当事务43 进行读取时,数据库还不知道事务是否要稍后执行写操作。此外,事务42 可能在事务43 被提交的时候中止或者可能仍然未被提交,因此读取可能终究不是陈旧的。通过避免不必要的中止,SSI 保留快照隔离对从一致快照中长时间运行的读取的支持。
检测影响之前读取的写入(读之后写入)
上图中,事务42 和43 都在班次1234 查找值班医生。如果在shift_id
上有索引,则数据库可以使用索引项1234 来记录事务42 和43 读取这个数据的事实。 (如果没有索引,这个信息可以在表级别进行跟踪)。这个信息只需要保留一段时间:在一个事务完成(提交或中止)之后,所有的并发事务完成之后,数据库就可以忘记它读取的数据了。
当事务写入数据库时,它必须在索引中查找最近曾读取受影响数据的其他事务。这个过程类似于在受影响的键范围上获取写锁,但锁并不会阻塞事务到其他事务完成,而是像一个引线一样只是简单通知其他事务:你们读过的数据可能不是最新的啦。
上图中,事务43 通知事务42 其先前读已过时,反之亦然。事务42首先提交并成功,尽管事务43 的写影响了42 ,但因为事务43 尚未提交,所以写入尚未生效。然而当事务43 想要提交时,来自事务42 的冲突写入已经被提交,所以事务43 必须中止。
部分失效是分布式系统的决定性特征。为了容忍错误,第一步是检测它们,但即使这样也很难。大多数系统没有检测节点是否发生故障的准确机制,所以大多数分布式算法依靠超时来确定远程节点是否仍然可用。 一旦检测到故障,使系统容忍它也并不容易:没有全局变量,没有共享内存,没有共同的知识,或机器之间任何其他种类的共享状态。
大多数非安全关键系统会选择便宜而不可靠,而不是昂贵和可靠。分布式系统可以永久运行而不会在服务层面中断,因为所有的错误和维护都可以在节点级别进行处理——至少在理论上是如此。 (实际上,如果一个错误的配置变更被应用到所有的节点,仍然会使分布式系统瘫痪)。
一致性保证
最终一致性:非常弱的保证
线性一致性:最强一致性模型之一
因果一致性
线性一致性
图 这个系统是非线性一致的,导致了球迷的困惑
线性一致性背后的基本思想很简单:使系统看起来好像只有一个数据副本。
图 可视化读取和写入看起来已经生效的时间点。 B的最后读取不是线性一致性的
上图中有一些有趣的细节需要指出:
第一个客户端B发送一个读取 x
的请求,然后客户端D发送一个请求将 x
设置为 0
,然后客户端A发送请求将 x
设置为 1
。尽管如此,返回到B的读取值为 1
(由A写入的值)。这是可以的:这意味着数据库首先处理D的写入,然后是A的写入,最后是B的读取。虽然这不是请求发送的顺序,但这是一个可以接受的顺序,因为这三个请求是并发的。也许B的读请求在网络上略有延迟,所以它在两次写入之后才到达数据库。
在客户端A从数据库收到响应之前,客户端B的读取返回 1
,表示写入值 1
已成功。这也是可以的:这并不意味着在写之前读到了值,这只是意味着从数据库到客户端A的正确响应在网络中略有延迟。
此模型不假设有任何事务隔离:另一个客户端可能随时更改值。例如,C首先读取 1
,然后读取 2
,因为两次读取之间的值由B更改。可以使用原子比较并设置(cas)操作来检查该值是否未被另一客户端同时更改:B和C的cas请求成功,但是D的cas请求失败(在数据库处理它时,x
的值不再是 0
)。
客户B的最后一次读取(阴影条柱中)不是线性一致性的。 该操作与C的cas写操作并发(它将 x
从 2
更新为 4
)。在没有其他请求的情况下,B的读取返回 2
是可以的。然而,在B的读取开始之前,客户端A已经读取了新的值 4
,因此不允许B读取比A更旧的值。再次,与图9-1中的Alice和Bob的情况相同。
这就是线性一致性背后的直觉。 正式的定义【6】更准确地描述了它。 通过记录所有请求和响应的时序,并检查它们是否可以排列成有效的顺序,测试一个系统的行为是否线性一致性是可能的(尽管在计算上是昂贵的)【11】。
线性一致性的有效场景
锁定和领导选举 (zookeeper etcd 使用一致性算法以容错方式保证)
约束和唯一性保证 (用户名或电子邮件地址必须唯一标识一个用户)
跨信道的时序依赖
图 Web服务器和图像调整器通过文件存储和消息队列进行通信,打开竞争条件的可能性
出现这个问题是因为Web服务器和缩放器之间存在两个不同的信道:文件存储与消息队列。没有线性一致性的新鲜性保证,这两个信道之间的竞争条件是可能的。
因果一致性
因果性对系统中的事件施加了顺序(什么发生在什么之前,基于因与果)。与线性一致不同,线性一致性将所有操作放在单一的全序时间线中,因果一致性为我们提供了一个较弱的一致性模型:某些事件可以是并发的,所以版本历史就像是一条不断分叉与合并的时间线。因果一致性没有线性一致性的协调开销,而且对网络问题的敏感性要低得多。
分布式事务与共识
共识:所有节点一致同意所做决定,且这一决定不可撤销。
共识问题:
线性一致性的CAS寄存器
寄存器需要基于当前值是否等于操作给出的参数,原子地**决定**是否设置新值。
原子事务提交
数据库必须**决定**是否提交或中止分布式事务。
全序广播
消息系统必须**决定**传递消息的顺序。
锁和租约
当几个客户端争抢锁或租约时,由锁来**决定**哪个客户端成功获得锁。
成员/协调服务
给定某种故障检测器(例如超时),系统必须**决定**哪些节点活着,哪些节点因为会话超时需 要被宣告死亡。
唯一性约束
当多个事务同时尝试使用相同的键创建冲突记录时,约束必须**决定**哪一个被允许,哪些因为 违反约束而失败。
三种系统类型
*服务(在线系统)*:每收到一个,服务会试图尽快处理它,并发回一个响应。响应时间通常 是服务性能的主要衡量指标,可用性通常非常重要
批处理系统(离线系统)*:大量的输入数据,跑一个作业(job)*来处理它,并生成一些输出 数据,这往往需要一段时间(从几分钟到几天),所以通常不会有用户等待作业完成。相反,批 量作业通常会定期运行(例如,每天一次)。批处理作业的主要性能衡量标准通常是吞吐量(处 理特定大小的输入所需的时间).
*流处理系统(准实时系统)*: 介于两者之间。流处理消费输入并产生输出,在事件发生后不久就会对事件进行操作,不会等待一组固定的输入数据(批处理的特点),因此具有低延迟的特点。
UNIX 分析简单日志
cat /var/log/nginx/access.log | #1 awk '{print $7}' | #2 sort | #3 uniq -c | #4 sort -r -n | #5 head -n 5 #6 1. 读取日志文件 2. 将每一行按空格分割成不同的字段,每行只输出第七个字段,恰好是请求的URL。在我们的例子中是`/css/typography.css`。 3. 按字母顺序排列请求的URL列表。如果某个URL被请求过n次,那么排序后,文件将包含连续重复出现n次的该URL。 4. `uniq`命令通过检查两个相邻的行是否相同来过滤掉输入中的重复行。 `-c`则表示还要输出一个计数器:对于每个不同的URL,它会报告输入中出现该URL的次数。 5. 第二种排序按每行起始处的数字(`-n`)排序,这是URL的请求次数。然后逆序(`-r`)返回结果,大的数字在前。 6. 最后,只输出前五行(`-n 5`),并丢弃其余的 output: 4189 /favicon.ico 3631 /2013/05/24/improving-security-of-ssh-private-keys.html 2124 /2012/12/05/schema-evolution-in-avro-protocol-buffers-thrift.html 1369 / 915 /css/typography.css
优点:接受任意形式输入,符合万物皆文件的概念 (进程的输出可以是下一个进程的输入)
各命令之间的性能高。
缺点:只能在一台机器上运行(进程到进程) ---> 引出Hadoop
MapReduce
MapReduce是一个编程框架,你可以使用它编写代码来处理HDFS等分布式文件系统中的大型数据集。支持多台机器上进行并行计算。Mapper和Reducer一次只能处理一条记录;它们不需要知道它们的输入来自哪里,或者输出去往什么地方,所以框架可以处理在机器之间移动数据的复杂性。
要创建MapReduce作业,你需要实现两个回调函数,Mapper和Reducer。
Mapper
Mapper会在每条输入记录上调用一次,其工作是从输入记录中提取键值。对于每个输入,它可以生成任意数量的键值对(包括None)。它不会保留从一个输入记录到下一个记录的任何状态,因此每个记录都是独立处理的。
Reducer
MapReduce框架拉取由Mapper生成的键值对,收集属于同一个键的所有值,并使用在这组值列表上迭代调用Reducer。 Reducer可以产生输出记录(例如相同URL的出现次数)。
分布式执行MapReduce
每个输入文件的大小通常是数百兆字节。 MapReduce调度器(图中未显示)试图在其中一台存储输入文件副本的机器上运行每个Mapper,只要该机器有足够的备用RAM和CPU资源来运行Mapper任务【26】。这个原则被称为将计算放在数据附近【27】:它节省了通过网络复制输入文件的开销,减少网络负载并增加局部性。
计算的Reduce端也被分区。虽然Map任务的数量由输入文件块的数量决定,但Reducer的任务的数量是由作业作者配置的(它可以不同于Map任务的数量)。为了确保具有相同键的所有键值对最终落在相同的Reducer处,框架使用键的散列值来确定哪个Reduce任务应该接收到特定的键值对。
只要当Mapper读取完输入文件,并写完排序后的输出文件,MapReduce调度器就会通知Reducer可以从该Mapper开始获取输出文件。Reducer连接到每个Mapper,并下载自己相应分区的有序键值对文件。按Reducer分区,排序,从Mapper向Reducer复制分区数据,这一整个过程被称为混洗(shuffle)
Reduce任务从Mapper获取文件,并将它们合并在一起,并保留有序特性。因此,如果不同的Mapper生成了键相同的记录,则在Reducer的输入中,这些记录将会相邻。Reducer调用时会收到一个键,和一个迭代器作为参数,迭代器会顺序地扫过所有具有该键的记录(因为在某些情况可能无法完全放入内存中)。Reducer可以使用任意逻辑来处理这些记录,并且可以生成任意数量的输出记录。这些输出记录会写入分布式文件系统上的文件中
排序合并连接
为了在批处理过程中实现良好的吞吐量,计算必须(尽可能)限于单台机器上进行。为待处理的每条记录发起随机访问的网络请求实在是太慢了。更好的方法是获取用户数据库的副本,并将它和用户行为日志放入同一个分布式文件系统中。
当MapReduce框架通过键对Mapper输出进行分区,然后对键值对进行排序时,效果是具有相同ID的所有活动事件和用户记录在Reducer输入中彼此相邻。 Map-Reduce作业甚至可以也让这些记录排序,使Reducer总能先看到来自用户数据库的记录,紧接着是按时间戳顺序排序的活动事件 —— 这种技术被称为二次排序(secondary sort)
由于Reducer一次处理一个特定用户ID的所有记录,因此一次只需要将一条用户记录保存在内存中,而不需要通过网络发出任何请求。这个算法被称为排序合并连接(sort-merge join),因为Mapper的输出是按键排序的,然后Reducer将来自连接两侧的有序记录列表合并在一起。
处理倾斜
在单个Reducer中收集与某个名流相关的所有活动(例如他们发布内容的回复)可能导致严重的倾斜(也称为热点(hot spot))—— 也就是说,一个Reducer必须比其他Reducer处理更多的记录(参见“负载倾斜与消除热点“)。由于MapReduce作业只有在所有Mapper和Reducer都完成时才完成,所有后续作业必须等待最慢的Reducer才能启动。
Pig中的倾斜连接(skewed join)方法首先运行一个抽样作业来确定哪些键是热键。连接实际执行时,Mapper会将热键的关联记录随机(相对于传统MapReduce基于键散列的确定性方法)发送到几个Reducer之一。对于另外一侧的连接输入,与热键相关的记录需要被复制到所有处理该键的Reducer上
广播散列连接
两个连接输入之一很小,所以它并没有分区,而且能被完全加载进一个哈希表中。因此,你可以为连接输入大端的每个分区启动一个Mapper,将输入小端的散列表加载到每个Mapper中,然后扫描大端,一次一条记录,并为每条记录查询散列表。
分区散列连接
如果两个连接输入以相同的方式分区(使用相同的键,相同的散列函数和相同数量的分区),则可以独立地对每个分区应用散列表方法。
回调函数
分布式批处理引擎有一个刻意限制的编程模型:回调函数(比如Mapper和Reducer)被假定是无状态的,而且除了指定的输出外,必须没有任何外部可见的副作用。这一限制允许框架在其抽象下隐藏一些困难的分布式系统问题:当遇到崩溃和网络问题时,任务可以安全地重试,任何失败任务的输出都被丢弃。如果某个分区的多个任务成功,则其中只有一个能使其输出实际可见。
得益于这个框架,你在批处理作业中的代码无需操心实现容错机制:框架可以保证作业的最终输出与没有发生错误的情况相同,也许不得不重试各种任务。在线服务处理用户请求,并将写入数据库作为处理请求的副作用,比起在线服务,批处理提供的这种可靠性语义要强得多。
批处理的特点总结
批处理作业的显著特点是,它读取一些输入数据并产生一些输出数据,但不修改输入—— 换句话说,输出是从输入衍生出的。最关键的是,输入数据是有界的(bounded):它有一个已知的,固定的大小(例如,它包含一些时间点的日志文件或数据库内容的快照)。因为它是有界的,一个作业知道自己什么时候完成了整个输入的读取,所以一个工作在做完后,最终总是会完成的。
消息代理和事件日志可以视作文件系统的流式等价物。
消息代理(消息队列)
与数据库的差异
多个消费者
负载均衡与扇出
图(a)负载平衡:在消费者间共享消费主题;(b)扇出:将每条消息传递给多个消费者。
两种模式可以组合使用:例如,两个独立的消费者组可以每组各订阅一个主题,每一组都共同收到所有消息,但在每一组内部,每条消息仅由单个节点处理。
分区日志(基于日志的消息代理)
图 生产者通过将消息追加写入主题分区文件来发送消息,消费者依次读取这些文件
变更数据捕获(CDC)
图 将数据按顺序写入一个数据库,然后按照相同的顺序将这些更改应用到其他系统
流处理的三种类型
流流连接
两个输入流都由活动事件组成,而连接算子在某个时间窗口内搜索相关的事件。例如,它可能会将同一个用户30分钟内进行的两个活动联系在一起。如果你想要找出一个流内的相关事件,连接的两侧输入可能实际上都是同一个流(自连接(self-join))。
流表连接
一个输入流由活动事件组成,另一个输入流是数据库变更日志。变更日志保证了数据库的本地副本是最新的。对于每个活动事件,连接算子将查询数据库,并输出一个扩展的活动事件。
表表连接
两个输入流都是数据库变更日志。在这种情况下,一侧的每一个变化都与另一侧的最新状态相连接。结果是两表连接所得物化视图的变更流。
某些系统被指定为记录系统,而其他数据则通过转换衍生自记录系统。通过这种方式,我们可以维护索引,物化视图,机器学习模型,统计摘要等等。通过使这些衍生和转换操作异步且松散耦合,能够防止一个区域中的问题扩散到系统中不相关部分,从而增加整个系统的稳健性与容错性。
将数据流表示为从一个数据集到另一个数据集的转换也有助于演化应用程序:如果你想变更其中一个处理步骤,例如变更索引或缓存的结构,则可以在整个输入数据集上重新运行新的转换代码,以便重新衍生输出。同样,出现问题时,你也可以修复代码并重新处理数据以便恢复。
这些过程与数据库内部已经完成的过程非常类似,因此我们将数据流应用的概念重新改写为,分拆(unbundling) 数据库组件,并通过组合这些松散耦合的组件来构建应用程序。
衍生状态可以通过观察底层数据的变更来更新。此外,衍生状态本身可以进一步被下游消费者观察。我们甚至可以将这种数据流一路传送至显示数据的终端用户设备,从而构建可动态更新以反映数据变更,并在离线时能继续工作的用户界面。
接下来,我们讨论了如何确保所有这些处理在出现故障时保持正确。我们看到可扩展的强完整性保证可以通过异步事件处理来实现,通过使用端到端操作标识符使操作幂等,以及通过异步检查约束。客户端可以等到检查通过,或者不等待继续前进,但是可能会冒有违反约束需要道歉的风险。这种方法比使用分布式事务的传统方法更具可扩展性与可靠性,并且在实践中适用于很多业务流程。
通过围绕数据流构建应用,并异步检查约束,我们可以避免绝大多数的协调工作,创建保证完整性且性能仍然表现良好的系统,即使在地理散布的情况下与出现故障时亦然。然后,我们对使用审计来验证数据完整性,以及损坏检测进行了一些讨论。
最后,我们退后一步,审视了构建数据密集型应用的一些道德问题。我们看到,虽然数据可以用来做好事,但它也可能造成很大伤害:作出严重影响人们生活的决定却难以申诉,导致歧视与剥削,监视常态化,曝光私密信息。我们也冒着数据被泄露的风险,并且可能会发现,即使是善意地使用数据也可能会导致意想不到的后果。
由于软件和数据对世界产生了如此巨大的影响,我们工程师们必须牢记,我们有责任为我们想要的那种世界而努力:一个尊重人们,尊重人性的世界。我希望我们能够一起为实现这一目标而努力。