数据密集型应用设计

数据密集型应用设计

December 10, 2021
Book
Ddia

第一章 可靠性、可扩展性、可维护性 #

数据密集型应用和计算密集型应用

现今很多应用程序都是 数据密集型(data-intensive) 的,而非 计算密集型(compute-intensive) 的。因此CPU很少成为这类应用的瓶颈,更大的问题通常来自数据量、数据复杂性、以及数据的变更速度

数据密集型应用通常由标准组件构建而成,标准组件提供了很多通用的功能;例如,许多应用程序都需要:

存储数据,以便自己或其他应用程序之后能再次找到 (数据库(database)

记住开销昂贵操作的结果,加快读取速度(缓存(cache)

允许用户按关键字搜索数据,或以各种方式对数据进行过滤(搜索索引(search indexes)

向其他进程发送消息,进行异步处理(流处理(stream processing)

定期处理累积的大批量数据(批处理(batch processing)

对应可选的组件在我映像中可以有:

数据库:mysql, postgresql

缓存: redis, memcached

搜索索引: elastic search, sonic, redis search

流处理: kafka, redis stream

批处理: linux cron, golang timer

使用较小的通用组件创建了一个全新的、专用的数据系统。

组件图

如何衡量一个系统的好坏 #

设计数据系统或服务时可能会遇到很多棘手的问题,例如:当系统出问题时,如何确保数据的正确性和完整性?当部分系统退化降级时,如何为客户提供始终如一的良好性能?当负载增加时,如何扩容应对?什么样的 API 才是好的 API?

  1. 可靠性(Reliability)

    系统在困境(adversity)(硬件故障、软件故障、人为错误)中仍可正常工作(正确完成功能,并能达到期望的性能水准)。

    故障通常定义为系统的一部分状态偏离其标准,而失效则是系统作为一个整体停止向用户提供服务。故障的概率不可能降到零,因此最好设计容错机制以防因故障而导致失效

    硬件错误的解决:为了减少系统的故障率,第一反应通常都是增加单个硬件的冗余度,例如:磁盘可以组建 RAID,服务器可能有双路电源和热插拔 CPU,数据中心可能有电池和柴油发电机作为后备电源,某个组件挂掉时冗余组件可以立刻接管。

    软件错误的解决:仔细考虑系统中的假设和交互;彻底的测试;进程隔离;允许进程崩溃并重启;测量、监控并分析生产环境中的系统行为。

    人为错误的解决:

  2. 可扩展性(Scalability)

    有合理的办法应对系统的增长(数据量、流量、复杂性)

  3. 可维护性(Maintainability)

    许多不同的人(工程师、运维)在不同的生命周期,都能高效地在系统上工作(使系统保持现有行为,并适应新的应用场景)。

从人的角度看:可靠就是能共困苦,同富贵;可扩展就是学习能力强,心胸广阔;可维护就是对人对己无偏见、无特例。

2PC(两阶段提交) #

2PC,two-phase commit,两阶段提交。

一种用于实现跨多个节点的原子事务提交的算法,即确保所有节点提交或所有节点中止。

2PC

2PC 使用一个通常不会出现在单节点事务中的新组件:协调者(coordinator)(也称为事务管理器(transaction manager))。

正常情况下,2PC 事务以应用在多个数据库节点上读写数据开始。我们称这些数据库节点为参与者(participants)。

当应用准备提交时,协调者开始阶段 1 :它发送一个准备(prepare)请求到每个节点,询问它们是否能够提交。然后协调者会跟踪参与者的响应:

如果所有参与者都回答“是”,表示它们已经准备好提交,那么协调者在阶段 2 发出提交(commit)请求,然后提交真正发生。

如果任意一个参与者回复了“否”,则协调者在阶段 2 中向所有节点发送中止(abort)请求。

那如果在阶段 2 有事务提交失败了呢?

在两阶段提交的情况下,准备(prepare 阶段)请求和提交(commit 阶段)请求当然也可以轻易丢失。 2PC 又有什么不同呢?

2PC

第十一章 流处理 #

先回忆了批处理的特点:即输入是有界的,即已知和有限的大小,所以批处理知道它何时完成输入的读取。

实际上,很多数据是无界限的,因为它随着时间的推移而逐渐到达:你的用户在昨天和今天产生了数据,明天他们将继续产生更多的数据。除非你停业,否则这个过程永远都不会结束,所以数据集从来就不会以任何有意义的方式“完成”。

因此,批处理程序必须将数据人为地分成固定时间段的数据块,例如,在每天结束时处理一天的数据,或者在每小时结束时处理一小时的数据。

日常批处理中的问题是,输入的变更只会在一天之后的输出中反映出来,这对于许多急躁的用户来说太慢了。为了减少延迟,我们可以更频繁地运行处理 —— 比如说,在每秒钟的末尾 —— 或者甚至更连续一些,完全抛开固定的时间切片,当事件发生时就立即进行处理,这就是流处理(stream processing)背后的想法

在本章中,我们将把事件流(event stream)视为一种数据管理机制:无界限,增量处理,与上一章中批量数据相对应。

原则上讲,文件或数据库就足以连接生产者和消费者:生产者将其生成的每个事件写入数据存储,且每个消费者定期轮询数据存储,检查自上次运行以来新出现的事件。这实际上正是批处理在每天结束时处理当天数据时所做的事情。

但当我们想要进行低延迟的连续处理时,如果数据存储不是为这种用途专门设计的,那么轮询开销就会很大。轮询的越频繁,能返回新事件的请求比例就越低,而额外开销也就越高。相比之下,最好能在新事件出现时直接通知消费者。

数据库在传统上对这种通知机制支持的并不好,关系型数据库通常有 触发器(trigger) ,它们可以对变化作出反应(如,插入表中的一行),但是它们的功能非常有限,并且在数据库设计中有些后顾之忧。相应的是,已经开发了专门的工具来提供事件通知。

两个问题:

  1. 如果生产者发送消息的速度比消费者能够处理的速度快会发生什么?

  2. 如果节点崩溃或暂时脱机,会发生什么情况? —— 是否会有消息丢失?

命令和事件 #

事件溯源的哲学是仔细区分事件(event)和命令(command)。

当来自用户的请求刚到达时,它一开始是一个命令:在这个时间点上它仍然可能可能失败,比如,因为违反了一些完整性条件。应用必须首先验证它是否可以执行该命令。

如果验证成功并且命令被接受,则它变为一个持久化且不可变的事件

在事件生成的时刻,它就成为了事实(fact)。即使客户稍后决定更改或取消预订,他们之前曾预定了某个特定座位的事实仍然成立,而更改或取消是之后添加的单独的事件。