2017-Kleppmann-《Designing Data Intensive Applications》-中文概述

参考地址:https://ngte.cowtransfer.com/s/63c41532fba247

Designing Data-Intensive Applications

《Designing Data-Intensive Applications》着实是一本非常优秀的讲解数据库与分布式系统底层实现原理的书籍,特别是 Martin Kleppmann 从理论到实践深入浅出地介绍了这些系统的实现路径。

本书主要分为三个部分:1~4 章介绍 Foundations of Data Systems,5~9 章介绍了 Distributed Data,10~12 章介绍了 Derived Data。

1. Foundations of Data Systems

介绍性章节定义了可靠性,可伸缩性和可维护性。我特别喜欢 Twitter 向关注者传递推文的演变方式的例子。关于响应时间也有一个好处:当最终用户需要多个后端呼叫时,即使只有一小部分单独的请求很慢(尾部延迟放大),许多用户也可能会遇到延迟。

2. Data Models and Query Languages

本章讨论了众所周知的关系模型和文档模型(NoSQL)。有一个很好的示例说明了如何将 LinkedIn 配置文件表示为 JSON 文档–配置文件的树状结构非常适合用 JSON 表示。但是,随着数据在文档之间变得更加相互关联(在 LinkedIn 示例中:对公司和大学的引用,用户之间的建议),可能会出现问题。在文档模型中,联接从数据库转移到应用程序。

文档数据库有时称为无模式(schema-less)。这是不正确的,因为存在隐式架构。更好的方式是写模式(schema-on-write,数据库在存储值时强制执行模式)和读模式(schema-on-read,模式是隐式的,仅在读取数据时才解释)。这类似于编程语言中的静态类型和动态类型。

接下来,将讨论查询语言,并很好地说明了命令式查询(编程语言中的循环)与声明式查询(SQL 中的 SELECT 语句)之间的对比。最后,讨论了类似图形的数据模型(例如,由 Neo4J 使用)和该模型的各种查询语言。

3. Storage and Retrieval

本章首先说明 LSM 树和 B 树的工作方式。

LSM-trees

Kleppmann 首先用两行 bash 代码创建一个简单的数据库。通过将行(键后跟值)添加到文件中来存储键和值。要检索给定密钥的值,请遍历文件,并在找到密钥后获取该值。写入现有密钥仅意味着将新行附加到文件中。文件中保留了带有键和值的旧行。但是 get 函数仅返回为给定键找到的最后一个值,因此文件中保留旧行并不重要。

这里的关键思想是仅将其追加到文件中是快速的(与更改文件中的值相反)。但是,扫描整个文件以获取键的值很慢。一种提高速度的方法是保留一个单独的散列,该散列具有一个指针,该指针指向文件中每个键的起始位置。然后,读取操作首先查找要使用的偏移量,然后从文件中的该位置开始读取。

接下来,我们假设文件中的所有行均按键排序。这些文件称为字符串排序表,缩写为 SSTables。如果我们有许多在不同时间创建的文件,则给定密钥可能会出现在许多文件中。但是,密钥的最新值是唯一的有效值-所有其他值都已过时(由较新的值代替)。我们可以将这些文件合并为一个文件,同时摆脱所有过时的行。这称为压缩,并且由于所有文件均按键排序,因此可以有效地完成合并排序的方式。

首先如何创建排序的文件?您维护一个称为内存表的内存中树结构(例如红黑树或 AVL 树)。这样可以使数据保持排序,并且一旦树达到一定大小(几兆字节),它就会作为新的 SSTable 被写出。添加或更改键的值只是意味着将其添加到内存表中(如果已经存在,则可能会覆盖它)。要查找键的值,请先在内存表中搜索,然后在最新的 SSTable 中搜索,然后在下一个较旧的 SSTable 中搜索,依此类推。

这种存储结构称为日志结构合并树(LSM-tree),例如在 Cassandra 中使用。

B-trees

B 树是最广泛使用的索引结构(在传统 SQL 数据库中使用),并且与 LSM 树有很大不同。B 树还按键对键/值对进行排序,以便快速查找。但是,数据以固定大小的块(也称为页面)存储在磁盘上,传统上大小为 4 KB。要更改块中的值,需要写入整个块。通过使用指向子块的指针来创建树,在该子块中,键范围变得越来越具体,直到找到包含所需值的叶块为止。分支因子描述了父节点可以具有多少个子节点。

大多数数据库都可以容纳在三层或四层深的 B 树中–具有 4 KB 块且分支因子为 500 的四层树最多可以存储 256 TB。B 树结构是 1970 年由波音公司的 Rudolf Bayer 和 Edward McCreight 引入的。目前尚不清楚 B 的含义-根据维基百科,已经提出了波音,平衡,宽阔,浓密和拜耳的建议。

LSM 树通常用于写入,而 B 树则用于读取。但是,有几个因素会影响性能,这些因素将在本章中进行讨论。描述了不同类型的优化,以及用于处理崩溃的策略,例如使用预写日志(WAL,也称为重做日志)。

Transactions and Analytics

当数据库首次出现时,它们通常用于商业交易,例如进行销售或支付薪水。即使将数据库用于其他任务,术语 transaction 仍然存在。现在数据库用法分为两大类:在线事务处理(OLTP)和在线分析处理(OLAP)。OLTP 通常在面对最终用户时使用,并且交易通常很小。OLAP 在数据仓库环境中使用得更多,在该环境中查询可以聚合数百万条记录中的值。OLAP 数据库通常以星型模式进行组织。有时面向列的存储用于 OLAP 用例。当处理一列中的多个值时(例如求和或求平均值),这可能会更有效率。本章提供了一个很好的示例,说明如何使用位图和游程长度编码来压缩存储的值。

本章很长,但是非常好。我在工作中同时使用 MySQL 和 Cassandra,这有助于理解内部存储模型之间的差异。

4. Encoding and Evolution

当程序中的数据需要保存到文件或通过网络发送时,需要以某种格式进行编码。

  • 向后兼容性–新代码可以读取由旧代码编写的数据。

  • 前向兼容性–旧代码可以读取新代码编写的数据。

目前常见的有三种类型的数据编码:

  • 第一种类型是特定于语言的格式,例如 Java 的 java.io.Serializable 或 Python 的 pickle。使用此类编码方式存在许多问题:安全性,版本控制,性能以及它们与特定编程语言绑定的事实。

  • 接下来是标准化的文本编码,例如 XML,JSON 和 CSV。他们也有问题。例如,它们不支持二进制字符串(没有字符编码的字节序列)。JSON 不能区分整数和浮点数,也没有指定精度。CSV 是一种模糊的格式。例如,如何处理逗号和换行符?

  • 最后,还有几种著名的二进制编码格式:Thrift(最初来自 Facebook),Protocol Buffers(最初来自 Google)和 Avro(最初来自 Hadoop 项目)。它们都有模式,并使用一些巧妙的技巧使编码紧凑。

5. Replication

这是“分布式数据”部分的第一章。复制意味着相同的数据存储在多台计算机上。复制的一些原因是:即使系统的某些部分发生故障也要继续工作(可用性提高),使数据在地理位置上更接近用户(减少延迟),以及通过从许多计算机提供相同的数据来增加读取吞吐量。

涵盖了三种不同的模型:单 Leader(Single Leader),多 Leader(Multi Leader)和无 Leader(Leaderless)。复制可以是同步的也可以是异步的。如果是同步的,则 Leader 和所有关注者在确认写回用户之前会先对其进行确认。如果任何跟随者速度慢或失败,则这可能会阻止所有写入。因此,异步复制更为常见。

复制有很多棘手的方面。首先是在设置新的关注者时。由于 Leader 中的数据不断变化,因此通常您需要对 Leader 的数据库进行一致的快照并将其复制到新的关注者中。然后,您需要处理复制过程中发生的更改积压。

如果关注者失败,则恢复后需要追赶。这意味着它需要跟踪失败之前 Leader 已经处理过的交易。如果 Leader 失败,则需要选择一个新的 Leader。这称为故障转移。这里很多事情都会出错。如果使用异步复制,则新的 Leader 可能尚未收到所有写入。如果在选择新的 Leader 后,前 Leader 重新加入集群,那些未复制的写入应该怎么办?

执行复制也很棘手。如果使用 NOW()或 RAND(),则仅重复 SQL 语句就会引起问题。也有许多其他边缘情况,因此通常不使用此方法。而是使用数据库内部预写日志。这是一个仅附加字节序列,其中包含对数据库的所有写操作。

Replication Lag

使用异步复制时,您最终会保持一致。即使复制时滞通常很小,但各种因素(例如网络问题或高负载)也可能导致时滞变为几秒钟甚至几分钟。当您有复制滞后时,可能会发生一些问题:如果提交的数据是从与接收到的写入不同的服务器上完成的,则您提交数据然后重新加载页面时,可能看不到刚写入的数据。有几种可能的解决方案,可以保证您自己阅读内容。跨设备读取(从笔记本电脑更新,然后从手机读取)甚至更加棘手。

另一个异常是时间似乎在向后移动。第一次读取返回用户 X 最近所做的注释。刷新页面时,读取来自另一个(滞后)服务器,并且尚未由用户 X 进行注释。单调读取是避免这种情况的一种方法,例如,通过将所有读取路由到同一服务器。如果最终的一致性太弱,则可以使用分布式事务。

Multi-Leader Replication

在多数据中心操作中,使用多头复制很有意义。有几个优点:性能(写入不必全部都经过一个 Leader),数据中心中断的容忍度,网络问题的容忍度(临时性问题不会阻止写入过程)。但是,如果 Leader 多于一位,则存在写冲突的风险。有几种方法可以处理这些问题:上次写入获胜(基于时间戳或唯一 ID),自动将值合并在一起或保留冲突值并让用户选择要保留的值。

Leaderless Replication

Dynamo,Cassandra,Riak 和 Voldemort 都使用无 Leader 复制。为了确保没有写入丢失,并且没有读取返回过时的值,将使用仲裁读取和写入。如果有 n 个副本,则每次写入必须由 w 个节点确认是否成功,并且我们必须为每个读取至少查询 r 个节点。只要 w + r> n,我们期望在读取时获得最新的值。在最简单的情况下,n = 3,w = 2,r = 2。但是,仍然存在边缘情况–例如,如果写入与读取并发,则写入可能仅存在于某些副本中,并且不清楚是否应为读取返回新值或旧值。

如果节点已关闭且具有过时的值,则从该节点读取值时可以检测到该节点。这可以启动修复。还可能在后台运行反熵过程,以寻找不一致之处并启动修复。

本章以一个很好的例子作为结束,说明如何使用版本向量来跟踪并发事件之间的因果关系。

6. Partitioning

将数据分为多个分区(也称为分片)的原因是可伸缩性。请注意,您可以同时进行分区和复制,每个分区可以复制到多个节点。每条数据恰好属于一个分区。分区可以在键范围内进行,也可以通过键的散列来完成。如果某些分区的命中率高于其他分区(工作负载偏斜),则分区的优势可能会丢失。二级索引对于分区数据库可能会比较棘手。解决此问题的一种方法是分散/聚集。

随着数据的增长和更改,可能需要重新平衡,将负载从一个节点转移到另一个节点。您不应移动过多的数据。例如,仅使用 hash mod N 将导致太多更改。更好的方法是使用比节点更多的分区,并且仅在节点之间移动整个分区。如果自动进行重新平衡,则存在级联故障的风险。因此,最好仅手动执行此操作。请求到正确分区的路由通常由单独的协调服务(例如 Zookeeper)处理。

7. Transactions

本章介绍在一台计算机上的事务。一个事务组将一个读写单元写入一个逻辑单元。整个事务要么成功(提交),要么失败(中止,回滚)。这样一来,应用程序开发人员就不必处理许多潜在的问题:与数据库的连接断开,在写入所有数据之前数据库或应用程序崩溃,多个客户端可能会覆盖彼此的数据等。

ACID 代表原子性,一致性,隔离性和耐久性。原子性与并发无关(隔离)。相反,这意味着所有写入都成功,或者没有成功。没有部分写入的数据。一致性意味着在事务之前和之后,不变性必须始终为真。例如,所有帐户的贷方和借方始终保持平衡。数据库可以检查某些不变量,例如外键约束和唯一性约束。但是通常,只有应用程序才能定义有效或无效的数据-数据库仅存储数据。

隔离意味着并发执行的事务不会相互干扰。其余各章中的大多数都涉及到这一点。最后,持久性意味着成功写入的数据不会丢失。但是,即使将数据写入磁盘,也有许多方式可能会丢失数据:磁盘可能会损坏,固件错误可能会阻止您的读取,计算机可能死机并且即使磁盘上的数据很好也无法获取数据。

Isolation Levels

当多个客户端同时读取和更新数据时,会有很多陷阱。为避免这些情况,有几种隔离级别可以防止出现问题。最基本的级别是已提交读。这意味着在读取时,您只会看到已提交的数据,而不是正在写入但尚未提交的数据。写入数据库时,您只会覆盖已提交的数据。这也称为无脏读和无脏写。

快照隔离(Snapshot Isolation)处理不同部分之间的一致性。如果您有两个帐户,每个帐户中有 500,并且您从第一个帐户中读取了余额,然后从第二个帐户中读取了余额(两个都在同一笔交易中读取),那么它们的总和应为 1000。但是,在第一次读取和第二次读取之间,可能会有一笔并发交易将 100 从第二个帐户转移到第一个帐户。因此,第一次读取可能会得到 500,而第二次读取会返回 400(因为此处已减去 100)。如果我们再次重复读取同一帐户,则第一个帐户将获得 600,第二个帐户将获得 400,因此现在它们的总和达到了预期的 1000。如果避免了总和更改的问题,则称为快照隔离,也称为可重复读取。

问题是我们希望在给定的时间点在数据库中看到的内容保持一致。在上面的示例中,我们不希望看到总额仅为\900 美元的情况。例如,这在进行备份时很重要。进行备份可能需要几个小时,并且数据会不断变化,但是我们希望存储在备份中的内容保持一致。长时间运行的查询也是如此–我们希望它们在一致的快照上执行。常见的解决方案是使用多版本并发控制(MVCC)。数据库可以保留一个对象的多个不同的提交版本,因为各种进行中的事务可能需要在不同的时间点查看数据库的状态。

当两个事务都写入数据时,就有丢失更新的风险。例如,如果两个用户同时读取,递增和写入结果,则最终的计数器值可能仅被更新了一个,而不是两个。如果事务读取某些信息,基于该信息做出决定并写入结果,则可能存在写入偏斜。这意味着在写入结果时,其所基于的前提不再成立。例如,一个会议室预订系统试图避免重复预订。

Serializable transactions

避免出现问题的一种可靠方法是,如果所有事务都依次执行。但是,性能常常遭受太多损失。在某些情况下,您实际上可以串行执行所有事务。您需要将整个数据集存储在内存中,并使用存储过程来避免事务期间的网络往返,并且吞吐量必须足够低才能由单个 CPU 处理。

大约 30 年以来,唯一可广泛使用的可序列化算法是两阶段锁定(2PL)。广泛的锁定意味着吞吐量下降。您也很容易陷入僵局。一种称为可序列化快照隔离(SSI)的新算法也可以提供可序列化性,但是由于乐观并发控制,与 2PL 使用的悲观并发控制相反,它具有更好的性能。

8. The Trouble with Distributed Systems

这是本书中我最喜欢的另一章(以及第 3 章“存储和检索”)。即使我在分布式系统和并发问题上工作了很长时间,但本章对于我可能会出错的所有方式还是让我大开眼界。

  • 网络不可靠。节点之间的连接可能会以各种方式失败。除正常故障模式外,还列出了一些异常模式:交换机的软件升级导致所有网络数据包延迟一分钟以上,鲨鱼咬伤并损坏海底电缆,所有入站数据包均被丢弃,而出站数据包则被丢弃。发送成功。即使在一个数据中心等受控环境中,网络问题也很常见。一项研究表明每月有 12 个网络故障。检测网络问题的唯一解决方案是超时。

  • 不可靠的时钟。时钟根据某些日历返回当前日期和时间。它们通常与 NTP 同步。由于计算机上的本地时钟可能会漂移,因此它可能早于 NTP 时间,并且重置会使它看起来像是在时间上回跳。单调时钟更适合于测量经过的时间–保证它们始终向前移动。

即使使用 NTP 同步不同的计算机,也存在许多可能的问题。由于 NTP 服务器存在网络延迟,因此时钟的精确度受到限制。seconds 秒也造成了许多大故障。这些和其他问题意味着一天可能没有精确的 86,400 秒,时钟可能向后移动,并且一个节点上的时间可能与另一节点上的时间完全不同。此外,不正确的时钟很容易被忽视。

依靠时间戳来订购事件(例如 Cassandra 中的 Last Write Wins)可能会导致意外结果。Google Spanner 使用的一种解决方案是确定时间戳的置信区间,并确保在订购事件时不存在重叠。

  • 处理暂停。与时间有关的另一个问题是过程暂停。在执行操作之前,检查当前时间然后执行某些操作的代码可能已暂停了几秒钟。发生这种情况的方式有很多:垃圾回收,从代码中看不出来的同步磁盘访问(Java 类加载器延迟加载类文件),操作系统交换到磁盘(分页)等。

为了处理分布式系统中的这些各种问题,多数人定义了事实(在下一章中对此进行了更多讨论)。这里描述的可能的问题可能会导致一些非常棘手的情况。更糟糕的是,如果参与的节点故意尝试引起问题。那就是所谓的拜占庭式断层,但这在本书中没有涉及。通过控制所有涉及的服务器,可以避免此类问题。

本章以定义安全性(Safety)和活力(Liveness)为结尾。安全意味着没有不好的事情发生(例如,错误的数据未写入数据库),活跃意味着最终发生了好事(例如,在 Leader 节点发生故障之后,最终选举了新的 Leader)。

9. Consistency and Consensus

分布式一致性主要是在面对延迟和故障时协调副本的状态。

Linearizability

线性化意味着复制的数据看起来好像只有一个数据副本,并且所有操作看起来都像是对数据进行原子操作(您不会看到值在新值和旧值之间转换)。它使数据库的行为类似于单线程程序中的变量。问题是它运行缓慢,尤其是在网络延迟较大的情况下。

CAP 定理有时表示为一致性,可用性,分区容限:从 3 中选择 2。但是网络分区是一个错误,因此您别无选择,它会发生。克莱普曼(Kleppmann)认为该定理最好表示为:划分时一致或可用。他还指出,CAP 定理仅涉及分区,没有提及其他故障,例如网络延迟或死节点。因此,CAP 定理不是那么有用。

Ordering Guarantees

总顺序允许比较任何两个元素,并且您始终可以说哪个更大。线性化系统的总阶数。如果同时发生两个事件,则无法确定哪个事件先发生。这导致因果关系的一致性模型较弱。它定义了部分顺序:某些操作是相对于彼此进行排序的,但是有些操作是不可比较的(并发)。

Lamport 时间戳提供与因果关系一致的总排序。但是,这还不足以解决诸如确保所选用户名唯一的问题(如果两个用户同时尝试选择相同的用户名,那么我们将在此之后才知道两个人中有谁)。这导致总顺序广播。它要求没有消息丢失;如果消息传递到一个节点,则将消息传递到所有节点。它还要求消息以相同顺序传递到每个节点。具有全部顺序广播可以实现正确的数据库复制。

Distributed Transactions and Consensus

对于分布式事务,可以使用两阶段提交(2PC)。它需要一个协调员。首先,它向每个节点发送准备消息。节点检查它们将能够执行写入,如果是,则回答“是”。如果所有节点都回答是,则下一条消息是提交。然后,每个节点都必须执行写操作。

使用单个领导者数据库,所有决策都由领导者做出,因此您可以具有线性化的操作,唯一性约束,完全有序的复制日志等等(这些属性都可以简化为共识)。如果领导者失败,或者网络问题使您无法到达它,则有以下三种选择:等待其恢复,通过人工选择新领导者进行手动故障转移,或使用算法自动选择新领导者。ZooKeeper 和 etcd 之类的工具可以帮助您解决这一问题。

但是,并非所有系统都需要共识。无领导者和多领导者复制系统通常没有这样做。相反,他们必须处理发生的冲突。这对我来说是最难理解的一章(我什至不确定我是否完全理解),尤其是关于如何将共识化为其他属性的解释。

10. Batch Processing

这是本书中处理派生数据的第一章。记录系统(保存数据的权威版本)与派生数据系统之间存在区别。派生数据系统中的数据是以某种方式转换或处理的现有数据,例如缓存或搜索索引。本章从使用 Unix 工具进行批处理的示例开始。为了从访问日志中找到五个最受欢迎的 URL,将命令 sort,awk,uniq 和 head 管道在一起。排序实际上比我想象的要强大得多。如果数据集不适合内存,它将自动存储到磁盘,并且将自动并行化多个 CPU 内核之间的排序(如果有)。

MapReduce 的工作方式与管道式 Unix 工具的工作方式之间有相似之处。它们不修改输入,除了产生输出外没有其他副作用,并且文件以顺序方式写入一次。对于 Unix 工具,stdin 和 stdout 是输入和输出。MapReduce 作业在分布式文件系统上读写文件。由于输入是不可变的,并且没有副作用,因此失败的 MapReduce 作业可以再次运行。根据我们希望从 MapReduce 作业中获得什么结果,可以执行几种不同类型的联接:排序合并联接,广播哈希联接和分区哈希联接。

使用管道化 Unix 命令的类比,MapReduce 就像将每个命令的输出写入一个临时文件。有一些新的数据流引擎,例如 Flink,可以比传统的 MapReduce 改善性能。例如,与每个阶段相比,不经常存储到文件中(实现),并且仅在需要时才进行排序。如果某些步骤避免了排序,那么您也不需要整个数据集,并且可以流水线化各个阶段。

11. Stream Processing

事件是一个小的,独立的,不可变的对象,其中包含某个时间点发生的事情的详细信息。例如,事件可以由用户在网页上执行操作,传感器的温度测量,服务器指标(例如 CPU 利用率)或股票价格生成。流处理与批处理类似,但是对无限制的流而不是固定大小的输入连续进行。用这种类比,消息代理是文件系统的流等效项。消息代理有两大类,具体取决于它们是在处理完消息后丢弃还是保留消息。基于日志的消息代理(例如 Kafka)保留消息,因此可以返回并重读旧消息。这类似于数据库和日志结构化存储引擎中的复制日志。

将写入数据库视为流也可能很有用。日志压缩可以减少所需的存储空间,同时仍然允许流保留数据库的完整副本。将数据库表示为流允许诸如搜索索引,缓存和分析系统之类的派生数据系统不断更新。这是通过使用更改日志并将其应用于派生系统来完成的。也可以从头开始创建新视图,并使用所有事件直到现在。这也与事件源非常相似。

通常,每个事件都有一个时间戳。此时间戳与服务器处理事件的时间不同,这可能导致某些奇怪的情况。例如,用户发出一个由 Web 服务器 A 处理的 Web 请求。然后,用户发出另一个由 Web 服务器 B 处理的请求。两个 Web 服务器都发出事件,但是 B 的事件首先到达消息代理(可能是由于排队或网络故障)。因此,消息代理从 B 看到事件,然后从 A 看到事件,即使它们以相反的顺序发生。

您还可以对流执行分析。例如,测量某物的速率,计算某个时间段内的滚动平均值或将当前统计信息与以前的时间间隔进行比较以检测趋势。可以使用各种类型的窗口:翻滚,跳动,滑动或会话。同样,就像批处理作业一样,您可以将流数据与数据库表连接起来以丰富事件数据。

12. The Future of Data Systems

在本章中,Kleppmann 描述了他对如何设计数据系统的看法。它基于第 11 章的思想,使用记录系统中的事件流创建数据的各种派生视图。由于派生是异步的并且是松散耦合的,因此一个区域中的问题不会像紧密集成的系统中那样传播到其他不相关的领域。此外,这些类型的系统可以更好地处理错误。如果处理数据的代码存在错误,则可以修复该错误,然后可以重新处理数据。

还讨论了诸如交易之类的内部措施不足以防止两次错误执行操作的问题。检查需要从应用程序端到端进行。例如,可以通过为操作分配唯一标识符来确保操作是幂等的,并检查该 ID 仅对操作执行一次。有时,在出现问题时能够进行补偿也要好得多,而不是花费大量精力来防止它发生。例如,如果帐户已透支,则为补偿性交易;如果航班已超额预订,则为道歉和补偿。如果这种情况不太经常发生,那么大多数企业都可以接受。通过异步检查约束,您可以避免大多数协调并保持完整性,同时还能保持良好的性能。

与数据流相关的讨论是从请求/响应系统转移到发布/订阅数据流。如果您收到所有更改的通知,则可以使视图保持最新(与电子表格的工作方式比较(更改在单元格中波动))。但是,这样做很难,因为请求/响应的假设已在数据库,库和框架中根深蒂固。

本章的最后部分讨论了在开发数据处理系统时的道德考虑。一个有趣的思想实验是用监视替换单词数据。在我们的监控驱动组织中,我们收集实时监控流并将其存储在我们的监控仓库中。我们的监视科学家使用高级分析和监视处理程序来获得新见解。

Nuggets

在整本书中,我发现很多有趣的信息。这是我的一些最爱。

  • 内存数据库的速度并不比基于磁盘的数据库快,因为它们可以从内存中读取,而传统的数据库可以从磁盘读取。操作系统仍然将最近使用的磁盘块缓存在内存中。相反,速度优势来自于不必将内存中的数据结构编码为适合写入磁盘的格式(第 89 页)。

  • 某些语言中的内置哈希函数不适用于获取分区键,因为同一键在不同进程中可能具有不同的哈希值。例如,Java 中的 Object.hashCode() 和 Ruby 中的 Object#hash(第 203 页)。

  • 在 Google 中,运行一个小时的 MapReduce 任务被终止的风险为 5%。该速率比由于硬件问题,计算机重新启动等导致的故障率高出一个数量级。MapReduce 旨在承受频繁的意外任务终止的原因不是因为硬件特别不可靠,而是因为任意选择的自由终止过程可以在计算群集中更好地利用资源(第 418 页)。

每章均以引号开头。我特别喜欢其中的两个。第 5 章中的第一个是我在软件开发方面一直以来最喜欢的报价之一:总是可以找到一个有效的复杂系统,它是从一个简单的有效系统演变而来的。相反的命题也似乎是正确的:从头开始设计的复杂系统永远行不通,也无法运转。约翰·加尔(John Gall),Systemantics,1975 年

这是第 11 章的第二个引用:可能出错的事物和不可能出错的事物之间的主要区别在于,当不可能出错的事物出错时,通常发现根本无法修理–道格拉斯·亚当斯(Douglas Adams),《几乎无害》(1992 年)

Conclusion

如今,感觉大多数系统都是以一种或另一种方式分布的系统。对于所有软件开发人员来说,设计数据密集型应用程序几乎都是必读的。因此,了解其中的许多概念确实很有用。

本书中描述和解决的许多问题都归结为并发问题。通常,有很好的图片和图表说明了这些要点。在每一章的开头,都有一张幻想样式的地图,其中列出了下一章的关键概念。我很喜欢那些。

设计数据密集型应用程序很厚-超过 550 页。这让我开始犹豫,几乎觉得太过气了。幸运的是,我们今年春天在工作中为读书俱乐部选择了它。这给了我足够的微动,可以开始并继续前进。我真的很高兴开始,因为其中包含了很多很好的信息。我特别喜欢它同时具有理论和实践意义。

如果您喜欢此摘要,则一定要阅读整本书。还有更多的细节和示例,它们都很有趣。强烈推荐!

Links

上一页