mirror of
https://github.com/Vonng/ddia.git
synced 2025-09-26 23:09:18 +08:00
polish ch1 - ch4
This commit is contained in:
parent
d6eb35ba92
commit
59cab3178f
5
.gitignore
vendored
5
.gitignore
vendored
@ -7,4 +7,7 @@ output/
|
||||
public/
|
||||
.hugo_build.lock
|
||||
.claude
|
||||
CLAUDE.md
|
||||
CLAUDE.md
|
||||
content/cn/
|
||||
zh.md
|
||||
en.md
|
15
README.md
15
README.md
@ -1,7 +1,7 @@
|
||||
# 设计数据密集型应用(第二版) - 中文翻译版
|
||||
|
||||
[](https://ddia.vonng.com)
|
||||
[](https://ddia.vonng.com/v1)
|
||||
[](https://ddia.vonng.com)
|
||||
[](https://ddia.vonng.com/v1)
|
||||
[](https://star-history.com/#Vonng/ddia&Date)
|
||||
|
||||
**作者**: [Martin Kleppmann](https://martin.kleppmann.com),[《Designing Data-Intensive Applications 2nd Edition》](https://learning.oreilly.com/library/view/designing-data-intensive-applications/9781098119058/ch01.html): 英国剑桥大学分布式系统研究员,演讲者,博主和开源贡献者,软件工程师和企业家,曾在 LinkedIn 和 Rapportive 负责数据基础架构。
|
||||
@ -13,7 +13,7 @@
|
||||
**阅读**:访问 [https://ddia.vonng.com](https://ddia.vonng.com) 阅读本书在线版本,或使用 [hugo](https://gohugo.io/documentation/) / [hextra](https://imfing.github.io/hextra/zh-cn/) 主题自行构建。
|
||||
|
||||
> [!NOTE]
|
||||
> **通知**:DDIA [**第二版**](https://ddia.vonng.com) 预览版正在翻译中 ([`main`](https://github.com/Vonng/ddia/tree/main) 分支),欢迎加入并提出您的宝贵意见。
|
||||
> [**DDIA 第二版**](https://ddia.vonng.com) 正在翻译中(翻译至至第十章),欢迎阅览并提出您的宝贵意见。
|
||||
|
||||
|
||||
---------
|
||||
@ -66,13 +66,14 @@
|
||||
- [8. 事务](https://ddia.vonng.com/ch8)
|
||||
- [9. 分布式系统的麻烦](https://ddia.vonng.com/ch9)
|
||||
- [10.一致性与共识](https://ddia.vonng.com/ch10)
|
||||
* [第三部分:派生数据](https://ddia.vonng.com/part-iii
|
||||
* [第三部分:派生数据](https://ddia.vonng.com/part-iii)
|
||||
- [11. 批处理](https://ddia.vonng.com/ch11) (尚未发布)
|
||||
- [12. 流处理](https://ddia.vonng.com/ch12) (尚未发布)
|
||||
- [13. 做正确的事](https://ddia.vonng.com/ch13) (尚未发布)
|
||||
- [13. 做正确的事](https://ddia.vonng.com/ch13)(尚未发布)
|
||||
* [术语表](https://ddia.vonng.com/glossary)
|
||||
* [后记](https://ddia.vonng.com/colophon)
|
||||
|
||||

|
||||
|
||||
|
||||
---------
|
||||
@ -85,7 +86,9 @@
|
||||
|
||||
译者保留对此版本译文的署名权,其他权利以原作者和出版社的主张为准。
|
||||
|
||||
本译文只供学习研究参考之用,不得公开传播发行或用于商业用途。有能力阅读英文书籍者请购买正版支持。
|
||||
本译文只供学习研究参考之用,不得公开发行或用于商业用途,有能力阅读英文书籍者请购买正版支持,本书英文原版在 [O'REILLY](https://learning.oreilly.com/api/v1/continue/9781098119058/) 平台上提供在线免费试预览。
|
||||
|
||||
|
||||
|
||||
|
||||
---------
|
||||
|
@ -1748,4 +1748,4 @@ come into play when *implementing* the data models described in this chapter.
|
||||
[^66]: Stavros Papadopoulos, Kushal Datta, Samuel Madden, and Timothy Mattson. [The TileDB Array Data Storage Manager](https://www.vldb.org/pvldb/vol10/p349-papadopoulos.pdf). *Proceedings of the VLDB Endowment*, volume 10, issue 4, pages 349–360, November 2016. [doi:10.14778/3025111.3025117](https://doi.org/10.14778/3025111.3025117)
|
||||
[^67]: Florin Rusu. [Multidimensional Array Data Management](https://faculty.ucmerced.edu/frusu/Papers/Report/2022-09-fntdb-arrays.pdf). *Foundations and Trends in Databases*, volume 12, numbers 2–3, pages 69–220, February 2023. [doi:10.1561/1900000069](https://doi.org/10.1561/1900000069)
|
||||
[^68]: Ed Targett. [Bloomberg, Man Group team up to develop open source “ArcticDB” database](https://www.thestack.technology/bloomberg-man-group-arcticdb-database-dataframe/). *thestack.technology*, March 2023. Archived at [perma.cc/M5YD-QQYV](https://perma.cc/M5YD-QQYV)
|
||||
[^69]: Dennis A. Benson, Ilene Karsch-Mizrachi, David J. Lipman, James Ostell, and David L. Wheeler. [GenBank](https://academic.oup.com/nar/article/36/suppl_1/D25/2507746). *Nucleic Acids Research*, volume 36, database issue, pages D25–D30, December 2007. [doi:10.1093/nar/gkm929](https://doi.org/10.1093/nar/gkm929)
|
||||
[^69]: Dennis A. Benson, Ilene Karsch-Mizrachi, David J. Lipman, James Ostell, and David L. Wheeler. [GenBank](https://academic.oup.com/nar/article/36/suppl_1/D25/2507746). *Nucleic Acids Research*, volume 36, database issue, pages D25–D30, December 2007. [doi:10.1093/nar/gkm929](https://doi.org/10.1093/nar/gkm929)
|
||||
|
@ -4,36 +4,41 @@ weight: 101
|
||||
breadcrumbs: false
|
||||
---
|
||||
|
||||
> *没有解决方案,只有利弊权衡。[…] 但你要努力获得最好的权衡,这就是你所能期望的全部。*
|
||||
> *没有完美的解决方案,只有权衡取舍。[…] 你能做的就是努力获得最佳的权衡,这就是你所能期望的一切。*
|
||||
>
|
||||
> [Thomas Sowell](https://www.youtube.com/watch?v=2YUtKr8-_Fg),接受 Fred Barnes 采访(2005)
|
||||
|
||||
数据是当今许多应用程序开发的核心。随着 Web 和移动应用、软件即服务(SaaS)以及云服务的出现,将来自许多不同用户的数据存储在共享的基于服务器的数据基础设施中已经变得很正常。来自用户活动、业务交易、设备和传感器的数据需要被存储并可供分析使用。当用户与应用程序交互时,他们既读取存储的数据,也生成更多的数据。
|
||||
> [!TIP] 早期读者注意事项
|
||||
> 通过早期发布电子书,您可以在书籍最早期的形式中获得内容——作者在撰写时的原始和未经编辑的内容——以便您可以在这些技术正式发布之前就充分利用它们。
|
||||
>
|
||||
> 这将是最终书籍的第 1 章。本书的 GitHub 仓库是 https://github.com/ept/ddia2-feedback。
|
||||
> 如果您想积极参与审阅和评论本草稿,请在 GitHub 上联系我们。
|
||||
|
||||
少量数据可以在单台机器上存储和处理,通常相当容易处理。然而,随着数据量或查询速率的增长,需要将其分布在多台机器上,这带来了许多挑战。随着应用程序需求变得更加复杂,仅将所有内容存储在一个系统中已经不够,可能需要组合多个提供不同功能的存储或处理系统。
|
||||
数据是当今许多应用程序开发的核心。随着 Web 和移动应用、软件即服务(SaaS)以及云服务的兴起,将来自不同用户的数据存储在共享的基于服务器的数据基础设施中已成为常态。来自用户活动、业务交易、设备和传感器的数据需要被存储并可供分析使用。当用户与应用程序交互时,他们既读取已存储的数据,也生成更多的数据。
|
||||
|
||||
如果数据管理是开发应用程序的主要挑战之一,我们称该应用程序为 **数据密集型**(data-intensive)[^1]。
|
||||
虽然在 **计算密集型**(compute-intensive)系统中的挑战是并行化某些非常大的计算,但在数据密集型应用中,我们通常更担心诸如存储和处理大数据量、管理数据变更、确保在故障和并发情况下的一致性以及确保服务高可用等问题。
|
||||
少量的数据可以存储和处理在单台机器上,通常相当容易处理。然而,随着数据量或查询速率的增长,数据需要分布在多台机器上,这带来了许多挑战。随着应用程序需求变得更加复杂,将所有内容存储在一个系统中已经不够,可能需要组合多个提供不同能力的存储或处理系统。
|
||||
|
||||
如果数据管理是开发应用程序的主要挑战之一,我们就称应用程序为 **数据密集型(data-intensive)** 的 [^1]。虽然在 **计算密集型(compute-intensive)** 系统中,挑战是并行化某些非常大规模的计算,但在数据密集型应用中,我们通常更关心诸如存储和处理大量数据、管理数据变更、在面对故障和并发时确保一致性,以及确保服务高可用等问题。
|
||||
|
||||
这些应用程序通常由提供常用功能的标准构建块构建而成。例如,许多应用程序需要:
|
||||
|
||||
* 存储数据,以便它们或另一个应用程序以后可以再次找到它(**数据库**)
|
||||
* 存储数据,以便它们或其他应用程序以后能再次找到(**数据库**)
|
||||
* 记住昂贵操作的结果,以加快读取速度(**缓存**)
|
||||
* 允许用户按关键字搜索数据或以各种方式过滤数据(**搜索索引**)
|
||||
* 处理事件和数据变更(**流处理**)
|
||||
* 定期处理大量累积的数据(**批处理**)
|
||||
* 一旦事件和数据变更发生就立即处理(**流处理**)
|
||||
* 定期处理累积的大量数据(**批处理**)
|
||||
|
||||
在构建应用程序时,我们通常会采用几个软件系统或服务,例如数据库或 API,并用一些应用程序代码将它们粘合在一起。如果你正在做这些数据系统设计的目标,那么这个过程可能会很容易。
|
||||
在构建应用程序时,我们通常会采用几个软件系统或服务,例如数据库或 API,并用一些应用程序代码将它们粘合在一起。如果你正在做数据系统设计的工作,那么这个过程可能会相当容易。
|
||||
|
||||
然而,随着你的应用程序变得更加雄心勃勃,挑战就会出现。有许多具有不同特性的数据库系统,适合不同的目的——你如何选择使用哪一个?有各种缓存方法、构建搜索索引的几种方法等等——你如何权衡它们的利弊?你需要找出哪些工具和哪些方法最适合手头的任务,当需要做单个工具无法单独完成的事情时,组合工具可能会很困难。
|
||||
然而,随着你的应用程序变得更加雄心勃勃,挑战就会出现。有许多具有不同特性的数据库系统,适合不同的目的——你如何选择使用哪一个?有各种缓存方法、构建搜索索引的几种方式等等——你如何推理它们的权衡?你需要找出哪些工具和哪些方法最适合手头的任务,当你需要做单个工具无法单独完成的事情时,组合工具可能会很困难。
|
||||
|
||||
本书是一本指南,帮助你决定使用哪些技术以及如何组合它们。正如你将看到的,没有一种方法从根本上优于其他方法;每种方法都有利弊。通过本书,你将学会提出正确的问题来评估和比较数据系统,这样你就能找出哪种方法最能满足你特定应用程序的需求。
|
||||
本书是一个指南,帮助你决定使用哪些技术以及如何组合它们。正如你将看到的,没有一种方法从根本上优于其他方法;一切都有利弊。通过本书,你将学会提出正确的问题来评估和比较数据系统,以便你能找出哪种方法最能满足你特定应用程序的需求。
|
||||
|
||||
我们将通过查看当今组织中数据的一些典型使用方式来开始我们的旅程。这里的许多想法起源于 **企业软件**(即大型组织(如大公司和政府)的软件需求和工程实践),因为历史上只有大型组织拥有需要复杂技术解决方案的大数据量。如果你的数据量足够小,你可以简单地将其保存在电子表格中!然而,最近小公司和初创企业管理大数据量并构建数据密集型系统也变得很常见。
|
||||
我们将通过观察当今组织中数据的一些典型使用方式来开始我们的旅程。这里的许多想法起源于 **企业软件**(即大型组织(如大公司和政府)的软件需求和工程实践),因为历史上只有大型组织拥有需要复杂技术解决方案的大数据量。如果你的数据量足够小,你可以简单地将其保存在电子表格中!然而,最近小公司和初创公司管理大数据量并构建数据密集型系统也变得很常见。
|
||||
|
||||
数据系统的关键挑战之一是不同的人需要用数据做非常不同的事情。如果你在一家公司工作,你和你的团队会有一套优先事项,而另一个团队可能有完全不同的目标,即使你们可能使用相同的数据集!此外,这些目标可能没有明确表达,这可能导致关于正确方法的误解和分歧。
|
||||
数据系统的关键挑战之一是不同的人需要用数据做非常不同的事情。如果你在一家公司工作,你和你的团队将有一套优先事项,而另一个团队可能有完全不同的目标,即使你们可能在处理相同的数据集!此外,这些目标可能没有明确阐述,这可能导致对正确方法的误解和分歧。
|
||||
|
||||
为了帮助你理解可以做出哪些选择,本章比较了几个对比的概念,并探讨了它们的权衡:
|
||||
为了帮助你了解可以做出哪些选择,本章比较了几个对比概念,并探讨了它们的权衡:
|
||||
|
||||
* 事务型系统和分析型系统之间的区别(["分析型与事务型系统"](/ch1#sec_introduction_analytics));
|
||||
* 云服务和自托管系统的利弊(["云服务与自托管"](/ch1#sec_introduction_cloud));
|
||||
@ -44,143 +49,136 @@ breadcrumbs: false
|
||||
|
||||
> [!TIP] 术语:前端和后端
|
||||
|
||||
本书中我们将讨论的大部分内容都与 **后端开发** 有关。为了解释这个术语:对于 Web 应用程序,在 Web 浏览器中运行的客户端代码称为 **前端**,而处理用户请求的服务器端代码称为 **后端**。移动应用类似于前端,因为它们提供用户界面,通常通过互联网与服务器端后端通信。前端有时会在用户设备上本地管理数据 [^2],但最大的数据基础设施挑战通常在于后端:前端只需要处理一个用户的数据,而后端代表 **所有** 用户管理数据。
|
||||
本书中我们将讨论的大部分内容都与 **后端开发** 有关。为了解释这个术语:对于 Web 应用程序,在 Web 浏览器中运行的客户端代码称为 **前端**,处理用户请求的服务器端代码称为 **后端**。移动应用类似于前端,它们提供用户界面,通常通过互联网与服务器端后端通信。前端有时在用户设备上本地管理数据 [^2],但最大的数据基础设施挑战通常在于后端:前端只需要处理一个用户的数据,而后端代表 **所有** 用户管理数据。
|
||||
|
||||
后端服务通常可通过 HTTP(有时是 WebSocket)访问;它通常由一些应用程序代码组成,这些代码在一个或多个数据库中读写数据,有时还与其他数据系统(如缓存或消息队列)接口(我们可能统称为 **数据基础设施**)。应用程序代码通常是 **无状态的**(即,当它完成处理一个 HTTP 请求时,它会忘记关于该请求的所有内容),任何需要从一个请求持续到另一个请求的信息都需要存储在客户端或服务器端数据基础设施中。
|
||||
后端服务通常可通过 HTTP(有时是 WebSocket)访问;它通常由一些应用程序代码组成,这些代码在一个或多个数据库中读取和写入数据,有时还与其他数据系统(如缓存或消息队列)接口(我们可能将其统称为 **数据基础设施**)。应用程序代码通常是 **无状态的**(即,当它完成处理一个 HTTP 请求时,它会忘记关于该请求的所有内容),任何需要从一个请求持续到另一个请求的信息都需要存储在客户端或服务器端数据基础设施中。
|
||||
|
||||
|
||||
## 分析型与事务型系统 {#sec_introduction_analytics}
|
||||
|
||||
如果你在企业中从事数据系统工作,你可能会遇到几种不同类型的数据工作人员。第一类是构建处理读取和更新数据请求的服务的 **后端工程师**;这些服务通常直接或通过其他服务间接地为外部用户提供服务(参见["微服务和无服务器"](/ch1#sec_introduction_microservices))。有时服务是供组织其他部分内部使用的。
|
||||
如果你在企业中从事数据系统工作,你可能会遇到几种不同类型的数据工作者。第一类是 **后端工程师**,他们构建处理读取和更新数据请求的服务;这些服务通常直接或间接地通过其他服务为外部用户提供服务(参见["微服务与 Serverless"](/ch1#sec_introduction_microservices))。有时服务是供组织其他部分内部使用的。
|
||||
|
||||
除了管理后端服务的团队外,通常还有另外两组人需要访问组织的数据:**业务分析师**,他们生成有关组织活动的报告,以帮助管理层做出更好的决策(**商业智能** 或 **BI**),以及 **数据科学家**,他们在数据中寻找新颖的见解或创建由数据分析和机器学习/AI 支持的面向用户的产品功能(例如,电子商务网站上的"购买了 X 的人也购买了 Y"推荐、预测分析如风险评分或垃圾邮件过滤,以及搜索结果排名)。
|
||||
除了管理后端服务的团队外,通常还有两类人需要访问组织的数据:**业务分析师**,他们生成关于组织活动的报告,以帮助管理层做出更好的决策(**商业智能** 或 **BI**);以及 **数据科学家**,他们在数据中寻找新的见解,或创建由数据分析和机器学习/AI 支持的面向用户的产品功能(例如,电子商务网站上的"购买了 X 的人也购买了 Y"推荐、风险评分或垃圾邮件过滤等预测分析,以及搜索结果排名)。
|
||||
|
||||
虽然业务分析师和数据科学家倾向于使用不同的工具并以不同的方式操作,但他们有一些共同点:两者都执行 **分析**,这意味着他们查看用户和后端服务生成的数据,但他们通常不修改这些数据(除了可能修复错误)。他们可能会创建派生数据集,其中原始数据已经以某种方式处理过。这导致了两种系统类型之间的分离——这种区别将贯穿本书:
|
||||
尽管业务分析师和数据科学家倾向于使用不同的工具并以不同的方式操作,但他们有一些共同点:两者都执行 **分析**,这意味着他们查看用户和后端服务生成的数据,但他们通常不修改这些数据(除了可能修复错误)。他们可能创建衍生数据集,其中原始数据已经以某种方式处理过。这导致了两种类型系统之间的分离——我们将在本书中使用这种区别:
|
||||
|
||||
* **事务型系统** 由后端服务和数据基础设施组成,在这里创建数据,例如通过为外部用户提供服务。在这里,应用程序代码基于用户执行的操作读取和修改其数据库中的数据。
|
||||
* **事务型系统** 由后端服务和数据基础设施组成,在这里创建数据,例如通过服务外部用户。在这里,应用程序代码基于用户执行的操作读取和修改其数据库中的数据。
|
||||
* **分析型系统** 服务于业务分析师和数据科学家的需求。它们包含来自事务型系统的只读数据副本,并针对分析所需的数据处理类型进行了优化。
|
||||
|
||||
正如我们将在下一节看到的,事务型和分析型系统通常由于充分的理由而保持分离。随着这些系统的成熟,出现了两个新的专门角色:**数据工程师** 和 **分析工程师**。数据工程师是知道如何集成事务型和分析型系统的人,并且更广泛地负责组织的数据基础设施 [^3]。
|
||||
分析工程师对数据进行建模和转换,使其对组织中的业务分析师和数据科学家更有用 [^4]。
|
||||
正如我们将在下一节中看到的,事务型系统和分析型系统通常出于充分的理由而保持分离。随着这些系统的成熟,出现了两个新的专业角色:**数据工程师** 和 **分析工程师**。数据工程师是知道如何集成事务型系统和分析型系统的人,并更广泛地负责组织的数据基础设施 [^3]。分析工程师对数据进行建模和转换,使其对组织中的业务分析师和数据科学家更有用 [^4]。
|
||||
|
||||
许多工程师专门从事事务型或分析型方面。然而,本书涵盖了事务型和分析型数据系统,因为两者在组织内数据的生命周期中都发挥着重要作用。我们将深入探讨用于向内部和外部用户提供服务的数据基础设施,以便你能够更好地与分界线另一边的同事合作。
|
||||
许多工程师专注于事务型或分析型方面。然而,本书涵盖了事务型和分析型数据系统,因为两者在组织内数据生命周期中都扮演着重要角色。我们将深入探讨用于向内部和外部用户提供服务的数据基础设施,以便你能更好地与这一分界线另一边的同事合作。
|
||||
|
||||
### 事务处理与分析的特征 {#sec_introduction_oltp}
|
||||
|
||||
在商业数据处理的早期,对数据库的写入通常对应于正在发生的 **商业交易**:进行销售、向供应商下订单、支付员工工资等。随着数据库扩展到不涉及资金交换的领域,术语 **事务** 仍然保留了下来,指的是形成逻辑单元的一组读写操作。
|
||||
在商业数据处理的早期,对数据库的写入通常对应于发生的 **商业交易**:进行销售、向供应商下订单、支付员工工资等。随着数据库扩展到不涉及金钱交换的领域,**事务** 这个术语仍然保留了下来,指的是形成逻辑单元的一组读取和写入。
|
||||
|
||||
> [!NOTE]
|
||||
> [第 8 章](/ch8#ch_transactions) 详细探讨了我们所说的事务的含义。本章宽泛地使用该术语来指代低延迟的读写操作。
|
||||
> [第 8 章](/ch8#ch_transactions) 详细探讨了我们所说的事务的含义。本章松散地使用该术语来指代低延迟的读取和写入。
|
||||
|
||||
尽管数据库开始用于许多不同类型的数据——社交媒体上的帖子、游戏中的动作、地址簿中的联系人等等——基本访问模式仍然类似于处理商业交易。事务型系统通常通过某个键查找少量记录(这称为 **点查询**)。基于用户的输入插入、更新或删除记录。因为这些应用程序是交互式的,这种访问模式被称为 **联机事务处理**(OLTP)。
|
||||
尽管数据库开始用于许多不同类型的数据——社交媒体上的帖子、游戏中的移动、地址簿中的联系人等等——基本的访问模式仍然类似于处理商业交易。事务型系统通常通过某个键查找少量记录(这称为 **点查询**)。基于用户的输入插入、更新或删除记录。因为这些应用程序是交互式的,这种访问模式被称为 **联机事务处理**(OLTP)。
|
||||
|
||||
然而,数据库也越来越多地用于分析,与 OLTP 相比具有非常不同的访问模式。通常,分析查询会扫描大量记录,并计算聚合统计信息(如计数、总和或平均值),而不是将单个记录返回给用户。例如,超市连锁店的业务分析师可能想要回答以下分析查询:
|
||||
然而,数据库也越来越多地用于分析,与 OLTP 相比,分析具有非常不同的访问模式。通常,分析查询会扫描大量记录,并计算聚合统计信息(如计数、求和或平均值),而不是将单个记录返回给用户。例如,超市连锁店的业务分析师可能想要回答以下分析查询:
|
||||
|
||||
* 我们每家商店在一月份的总收入是多少?
|
||||
* 在我们最近的促销期间,我们卖出的香蕉比平时多了多少?
|
||||
* 哪个品牌的婴儿食品最常与品牌 X 纸尿裤一起购买?
|
||||
* 在我们最近的促销期间,我们卖出了比平时多多少香蕉?
|
||||
* 哪个品牌的婴儿食品最常与 X 品牌尿布一起购买?
|
||||
|
||||
这些查询产生的报告对商业智能很重要,帮助管理层决定下一步做什么。为了将这种使用数据库的模式与事务处理区分开来,它被称为 **联机分析处理**(OLAP)[^5]。
|
||||
OLTP 和分析之间的区别并不总是明确的,但一些典型特征列在[表 1-1](/ch1#tab_oltp_vs_olap) 中。
|
||||
这些类型查询产生的报告对商业智能很重要,帮助管理层决定下一步做什么。为了将这种使用数据库的模式与事务处理区分开来,它被称为 **联机分析处理**(OLAP)[^5]。OLTP 和分析之间的区别并不总是很明确,但[表 1-1](/ch1#tab_oltp_vs_olap) 列出了一些典型特征。
|
||||
|
||||
{{< figure id="tab_oltp_vs_olap" title="表 1-1. 比较事务型和分析型系统的特征" class="w-full my-4" >}}
|
||||
{{< figure id="tab_oltp_vs_olap" title="表 1-1. 事务型系统和分析型系统特征比较" class="w-full my-4" >}}
|
||||
|
||||
| 属性 | 事务型系统(OLTP) | 分析型系统(OLAP) |
|
||||
|--------|-----------------|---------------|
|
||||
| 主要读取模式 | 点查询(按键获取单个记录) | 对大量记录进行聚合 |
|
||||
| 主要写入模式 | 创建、更新和删除单个记录 | 批量导入(ETL)或事件流 |
|
||||
| 人类用户示例 | Web/移动应用程序的最终用户 | 内部分析师,用于决策支持 |
|
||||
| 机器使用示例 | 检查操作是否被授权 | 检测欺诈/滥用模式 |
|
||||
| 查询类型 | 固定的查询集,由应用程序预定义 | 分析师可以进行任意查询 |
|
||||
| 数据代表 | 数据的最新状态(当前时间点) | 随时间发生的事件历史 |
|
||||
| 数据集大小 | GB 到 TB | TB 到 PB |
|
||||
| 属性 | 事务型系统(OLTP) | 分析型系统(OLAP) |
|
||||
|-----------------|----------------------------------------|-----------------------------------|
|
||||
| 主要读取模式 | 点查询(通过键获取单个记录) | 对大量记录进行聚合 |
|
||||
| 主要写入模式 | 创建、更新和删除单个记录 | 批量导入(ETL)或事件流 |
|
||||
| 人类用户示例 | Web/移动应用程序的最终用户 | 内部分析师,用于决策支持 |
|
||||
| 机器使用示例 | 检查操作是否被授权 | 检测欺诈/滥用模式 |
|
||||
| 查询类型 | 固定的查询集,由应用程序预定义 | 分析师可以进行任意查询 |
|
||||
| 数据代表 | 数据的最新状态(当前时间点) | 随时间发生的事件历史 |
|
||||
| 数据集大小 | GB 到 TB | TB 到 PB |
|
||||
|
||||
> [!NOTE]
|
||||
> **OLAP** 中 **联机** 的含义不清楚;它可能指的是查询不仅用于预定义的报告,而且分析师使用 OLAP 系统交互式地进行探索性查询。
|
||||
> OLAP 中 **online** 的含义不明确;它可能指的是查询不仅用于预定义的报告,而且分析师可以交互式地使用 OLAP 系统进行探索性查询。
|
||||
|
||||
对于事务型系统,通常不允许用户构建自定义 SQL 查询并在数据库上运行它们,因为这可能会允许他们读取或修改他们无权访问的数据。此外,他们可能会编写执行成本昂贵的查询,从而影响其他用户的数据库性能。出于这些原因,OLTP 系统主要运行嵌入到应用程序代码中的固定查询集,仅偶尔使用一次性自定义查询进行维护或故障排除。另一方面,分析数据库通常赋予用户手动编写任意 SQL 查询的自由,或使用数据可视化或仪表板工具(如 Tableau、Looker 或 Microsoft Power BI)自动生成查询。
|
||||
在事务型系统中,通常不允许用户构建自定义 SQL 查询并在数据库上运行它们,因为这可能会允许他们读取或修改他们没有权限访问的数据。此外,他们可能编写执行成本高昂的查询,从而影响其他用户的数据库性能。出于这些原因,OLTP 系统主要运行嵌入到应用程序代码中的固定查询集,只偶尔使用一次性自定义查询进行维护或故障排除。另一方面,分析数据库通常让用户可以自由地手动编写任意 SQL 查询,或使用 Tableau、Looker 或 Microsoft Power BI 等数据可视化或仪表板工具自动生成查询。
|
||||
|
||||
还有一种专为分析工作负载(聚合多条记录的查询)设计但嵌入到面向用户产品中的系统类型。这一类别被称为 **产品分析** 或 **实时分析**,专为此类用途设计的系统包括 Pinot、Druid 和 ClickHouse [^6]。
|
||||
还有一种类型的系统是为分析工作负载(对许多记录进行聚合的查询)设计的,但嵌入到面向用户的产品中。这一类别被称为 **产品分析** 或 **实时分析**,为这种用途设计的系统包括 Pinot、Druid 和 ClickHouse [^6]。
|
||||
|
||||
### 数据仓库 {#sec_introduction_dwh}
|
||||
|
||||
起初,相同的数据库用于事务处理和分析查询。SQL 在这方面相当灵活:它对两种类型的查询都很有效。然而,在 20 世纪 80 年代末和 90 年代初,公司停止将其 OLTP 系统用于分析目的,转而在单独的数据库系统上运行分析的趋势出现了。这个单独的数据库被称为 **数据仓库**。
|
||||
起初,相同的数据库既用于事务处理,也用于分析查询。SQL 在这方面相当灵活:它对两种类型的查询都很有效。然而,在 20 世纪 80 年代末和 90 年代初,公司倾向于停止使用其 OLTP 系统进行分析目的,而是在单独的数据库系统上运行分析。这个单独的数据库被称为 **数据仓库**。
|
||||
|
||||
一家大型企业可能有几十个甚至几百个联机事务处理系统:为面向客户的网站提供支持的系统、控制实体店中的销售点(收银台)系统、跟踪仓库库存、规划车辆路线、管理供应商、管理员工以及执行许多其他任务的系统。这些系统中的每一个都很复杂,需要一个团队来维护它,因此这些系统最终大多独立于彼此运行。
|
||||
一家大型企业可能有几十个,甚至上百个联机事务处理系统:为面向客户的网站提供动力的系统、控制实体店中的销售点(收银台)系统、跟踪仓库中的库存、规划车辆路线、管理供应商、管理员工以及执行许多其他任务。这些系统中的每一个都很复杂,需要一个团队来维护它,因此这些系统最终主要是相互独立地运行。
|
||||
|
||||
由于几个原因,业务分析师和数据科学家直接查询这些 OLTP 系统通常是不可取的:
|
||||
出于几个原因,业务分析师和数据科学家直接查询这些 OLTP 系统通常是不可取的:
|
||||
|
||||
* 感兴趣的数据可能分散在多个事务型系统中,使得在单个查询中组合这些数据集变得困难(这个问题被称为 **数据孤岛**);
|
||||
* 适合 OLTP 的模式和数据布局类型不太适合分析(参见["用于分析的星型和雪花型模式"](/ch3#sec_datamodels_analytics));
|
||||
* 感兴趣的数据可能分布在多个事务型系统中,使得在单个查询中组合这些数据集变得困难(称为 **数据孤岛** 的问题);
|
||||
* 适合 OLTP 的模式和数据布局不太适合分析(参见["星型和雪花型:分析模式"](/ch3#sec_datamodels_analytics));
|
||||
* 分析查询可能相当昂贵,在 OLTP 数据库上运行它们会影响其他用户的性能;以及
|
||||
* OLTP 系统可能位于用户出于安全或合规原因不允许直接访问的单独网络中。
|
||||
|
||||
相比之下,**数据仓库** 是一个单独的数据库,分析师可以随心所欲地查询,而不会影响 OLTP 操作 [^7]。
|
||||
正如我们将在[第 4 章](/ch4#ch_storage)中看到的,数据仓库通常以与 OLTP 数据库非常不同的方式存储数据,以优化分析中常见的查询类型。
|
||||
相比之下,**数据仓库** 是一个单独的数据库,分析师可以随心所欲地查询,而不会影响 OLTP 操作 [^7]。正如我们将在[第 4 章](/ch4#ch_storage)中看到的,数据仓库通常以与 OLTP 数据库非常不同的方式存储数据,以优化分析中常见的查询类型。
|
||||
|
||||
数据仓库包含公司中所有各种 OLTP 系统中数据的只读副本。数据从 OLTP 数据库中提取(使用定期数据转储或连续更新流),转换为分析友好的模式,清理,然后加载到数据仓库中。将数据导入数据仓库的过程称为 **提取-转换-加载**(ETL),如[图 1-1](/ch1#fig_dwh_etl) 所示。有时 **转换** 和 **加载** 步骤的顺序会交换(即,转换在数据仓库中完成,在加载之后),导致 **ELT**。
|
||||
数据仓库包含公司中所有各种 OLTP 系统中数据的只读副本。数据从 OLTP 数据库中提取(使用定期数据转储或连续更新流),转换为分析友好的模式,清理,然后加载到数据仓库中。这种将数据导入数据仓库的过程称为 **提取-转换-加载**(ETL),如[图 1-1](/ch1#fig_dwh_etl) 所示。有时 **转换** 和 **加载** 步骤的顺序会互换(即,在加载后在数据仓库中进行转换),从而产生 **ELT**。
|
||||
|
||||
{{< figure src="/fig/ddia_0101.png" id="fig_dwh_etl" caption="图 1-1. ETL 到数据仓库的简化概述。" class="w-full my-4" >}}
|
||||
|
||||
在某些情况下,ETL 过程的数据源是外部 SaaS 产品,如客户关系管理(CRM)、电子邮件营销或信用卡处理系统。在这些情况下,你无法直接访问原始数据库,因为它只能通过软件供应商的 API 访问。将这些外部系统的数据带入你自己的数据仓库可以实现通过 SaaS API 无法进行的分析。用于 SaaS API 的 ETL 通常由专门的数据连接器服务(如 Fivetran、Singer 或 AirByte)实现。
|
||||
在某些情况下,ETL 过程的数据源是外部 SaaS 产品,如客户关系管理(CRM)、电子邮件营销或信用卡处理系统。在这些情况下,你无法直接访问原始数据库,因为它只能通过软件供应商的 API 访问。将这些外部系统的数据导入你自己的数据仓库可以实现通过 SaaS API 无法实现的分析。SaaS API 的 ETL 通常由专门的数据连接器服务(如 Fivetran、Singer 或 AirByte)实现。
|
||||
|
||||
一些数据库系统提供 **混合事务/分析处理**(HTAP),旨在在单个系统中启用 OLTP 和分析,而无需从一个系统到另一个系统的 ETL [^8] [^9]。
|
||||
然而,许多 HTAP 系统内部由一个 OLTP 系统和一个单独的分析系统组成,隐藏在一个公共接口后面——因此两者之间的区别对于理解这些系统的工作原理仍然很重要。
|
||||
一些数据库系统提供 **混合事务/分析处理**(HTAP),旨在在单个系统中启用 OLTP 和分析,而无需从一个系统 ETL 到另一个系统 [^8] [^9]。然而,许多 HTAP 系统内部由一个 OLTP 系统与一个单独的分析系统耦合组成,隐藏在公共接口后面——因此两者之间的区别对于理解这些系统如何工作仍然很重要。
|
||||
|
||||
此外,即使 HTAP 存在,由于不同的目标和要求,事务型和分析型系统之间的分离也很常见。特别是,每个事务型系统都有自己的数据库被认为是良好的实践(参见["微服务和无服务器"](/ch1#sec_introduction_microservices)),导致数百个独立的事务型数据库;另一方面,企业通常只有一个数据仓库,以便业务分析师可以在单个查询中组合来自多个事务型系统的数据。
|
||||
此外,尽管 HTAP 存在,但由于目标和要求不同,事务型系统和分析型系统之间的分离是常见的。特别是,每个事务型系统拥有自己的数据库被认为是良好的做法(参见["微服务与 Serverless"](/ch1#sec_introduction_microservices)),导致数百个单独的事务型数据库;另一方面,企业通常有一个单一的数据仓库,以便业务分析师可以在单个查询中组合来自多个事务型系统的数据。
|
||||
|
||||
因此,HTAP 并不能取代数据仓库。相反,它在同一个应用程序既需要执行扫描大量行的分析查询,又需要以低延迟读取和更新单个记录的场景中很有用。例如,欺诈检测可能涉及此类工作负载 [^10]。
|
||||
因此,HTAP 不会取代数据仓库。相反,它在同一应用程序需要既执行扫描大量行的分析查询,又以低延迟读取和更新单个记录的场景中很有用。例如,欺诈检测可能涉及此类工作负载 [^10]。
|
||||
|
||||
事务型和分析型系统之间的分离是更广泛趋势的一部分:随着工作负载变得更加苛刻,系统变得更加专业化并针对特定工作负载进行优化。通用系统可以轻松处理小数据量,但规模越大,系统往往变得越专业化 [^11]。
|
||||
事务型系统和分析型系统之间的分离是更广泛趋势的一部分:随着工作负载变得更加苛刻,系统变得更加专业化并针对特定工作负载进行优化。通用系统可以舒适地处理小数据量,但规模越大,系统往往变得越专业化 [^11]。
|
||||
|
||||
#### 从数据仓库到数据湖 {#from-data-warehouse-to-data-lake}
|
||||
|
||||
数据仓库通常使用通过 SQL 查询的 **关系** 数据模型(参见[第 3 章](/ch3#ch_datamodels)),可能使用专门的商业智能软件。这个模型非常适合业务分析师需要进行的查询类型,但不太适合数据科学家的需求,他们可能需要执行以下任务:
|
||||
数据仓库通常使用通过 SQL 查询的 **关系** 数据模型(参见[第 3 章](/ch3#ch_datamodels)),可能使用专门的商业智能软件。这个模型很适合业务分析师需要进行的查询类型,但不太适合数据科学家的需求,他们可能需要执行以下任务:
|
||||
|
||||
* 将数据转换为适合训练机器学习模型的形式;通常这需要将数据库表的行和列转换为称为 **特征** 的数值向量或矩阵。以最大化训练模型性能的方式执行此转换的过程称为 **特征工程**,它通常需要难以使用 SQL 表达的自定义代码。
|
||||
* 将数据转换为适合训练机器学习模型的形式;这通常需要将数据库表的行和列转换为称为 **特征** 的数值向量或矩阵。以最大化训练模型性能的方式执行这种转换的过程称为 **特征工程**,它通常需要难以用 SQL 表达的自定义代码。
|
||||
* 获取文本数据(例如,产品评论)并使用自然语言处理技术尝试从中提取结构化信息(例如,作者的情感或他们提到的主题)。同样,他们可能需要使用计算机视觉技术从照片中提取结构化信息。
|
||||
|
||||
虽然已经努力将机器学习运算符添加到 SQL 数据模型 [^12] 并在关系基础上构建高效的机器学习系统 [^13],但许多数据科学家更喜欢不在关系数据库(如数据仓库)中工作。相反,许多人更喜欢使用 Python 数据分析库(如 pandas 和 scikit-learn)、统计分析语言(如 R)以及分布式分析框架(如 Spark)[^14]。
|
||||
我们将在["数据框、矩阵和数组"](/ch3#sec_datamodels_dataframes)中进一步讨论这些。
|
||||
尽管已经努力将机器学习算子添加到 SQL 数据模型 [^12] 并在关系基础上构建高效的机器学习系统 [^13],但许多数据科学家不喜欢在数据仓库等关系数据库中工作。相反,许多人更喜欢使用 Python 数据分析库(如 pandas 和 scikit-learn)、统计分析语言(如 R)和分布式分析框架(如 Spark)[^14]。我们将在["数据框、矩阵和数组"](/ch3#sec_datamodels_dataframes)中进一步讨论这些。
|
||||
|
||||
因此,组织面临着以适合数据科学家使用的形式提供数据的需求。答案是 **数据湖**:一个中央数据存储库,保存可能对分析有用的任何数据的副本,通过 ETL 过程从事务型系统获得。与数据仓库的区别在于,数据湖只包含文件,而不强加任何特定的文件格式或数据模型。数据湖中的文件可能是数据库记录的集合,使用文件格式(如 Avro 或 Parquet)编码(参见[第 5 章](/ch5#ch_encoding)),但它们同样可以包含文本、图像、视频、传感器读数、稀疏矩阵、特征向量、基因组序列或任何其他类型的数据 [^15]。
|
||||
除了更加灵活之外,这通常也比关系数据存储更便宜,因为数据湖可以使用商品化的文件存储,如对象存储(参见["云原生系统架构"](/ch1#sec_introduction_cloud_native))。
|
||||
因此,组织面临着以适合数据科学家使用的形式提供数据的需求。答案是 **数据湖**:一个集中的数据存储库,保存任何可能对分析有用的数据副本,通过 ETL 过程从事务型系统获得。与数据仓库的区别在于,数据湖只是包含文件,而不强制任何特定的文件格式或数据模型。数据湖中的文件可能是数据库记录的集合,使用 Avro 或 Parquet 等文件格式编码(参见[第 5 章](/ch5#ch_encoding)),但它们同样可以包含文本、图像、视频、传感器读数、稀疏矩阵、特征向量、基因组序列或任何其他类型的数据 [^15]。除了更灵活之外,这通常也比关系数据存储更便宜,因为数据湖可以使用商品化的文件存储,如对象存储(参见["云原生系统架构"](/ch1#sec_introduction_cloud_native))。
|
||||
|
||||
ETL 过程已经被推广为 **数据管道**,在某些情况下,数据湖已成为从事务型系统到数据仓库路径上的中间站。数据湖包含由事务型系统产生的"原始"形式的数据,没有转换为关系数据仓库模式。这种方法的优点是数据的每个消费者都可以将原始数据转换为最适合其需求的形式。它被称为 **寿司原则**:"原始数据更好"[^16]。
|
||||
ETL 过程已经泛化为 **数据管道**,在某些情况下,数据湖已成为从事务型系统到数据仓库路径上的中间站。数据湖包含事务型系统产生的"原始"形式的数据,没有转换为关系数据仓库模式。这种方法的优势在于,每个数据消费者都可以将原始数据转换为最适合其需求的形式。它被称为 **寿司原则**:"生数据更好"[^16]。
|
||||
|
||||
除了将数据从数据湖加载到单独的数据仓库之外,还可以直接在数据湖中的文件上运行典型的数据仓库工作负载(SQL 查询和业务分析),以及数据科学/机器学习工作负载。这种架构被称为 **数据湖仓**,它需要一个查询执行引擎和一个元数据(例如,模式管理)层来扩展数据湖的文件存储 [^17]。
|
||||
除了从数据湖加载数据到单独的数据仓库之外,还可以直接在数据湖中的文件上运行典型的数据仓库工作负载(SQL 查询和业务分析),以及数据科学/机器学习工作负载。这种架构被称为 **数据湖仓**,它需要一个查询执行引擎和一个元数据(例如,模式管理)层来扩展数据湖的文件存储 [^17]。
|
||||
|
||||
Apache Hive、Spark SQL、Presto 和 Trino 是这种方法的示例。
|
||||
Apache Hive、Spark SQL、Presto 和 Trino 是这种方法的例子。
|
||||
|
||||
#### 超越数据湖 {#beyond-the-data-lake}
|
||||
|
||||
随着分析实践的成熟,组织越来越关注分析系统和数据管道的管理和操作,例如 DataOps 宣言中所捕获的 [^18]。
|
||||
其中一部分是治理、隐私以及符合 GDPR 和 CCPA 等法规的问题,我们将在["数据系统、法律与社会"](/ch1#sec_introduction_compliance)和[待补充链接]中讨论。
|
||||
随着分析实践的成熟,组织越来越关注分析系统和数据管道的管理和运营,例如 DataOps 宣言所捕获的 [^18]。其中一部分是治理、隐私和遵守 GDPR 和 CCPA 等法规的问题,我们将在["数据系统、法律与社会"](/ch1#sec_introduction_compliance)和[待补充链接]中讨论。
|
||||
|
||||
此外,分析数据越来越多地不仅作为文件和关系表提供,还作为事件流提供(参见[待补充链接])。使用基于文件的数据分析,你可以定期(例如,每天)重新运行分析以响应数据的变化,但流处理允许分析系统以秒级的速度响应事件。根据应用程序及其时间敏感性,流处理方法可能很有价值,例如识别和阻止潜在的欺诈或滥用活动。
|
||||
此外,分析数据越来越多地不仅作为文件和关系表提供,还作为事件流(参见[待补充链接])。使用基于文件的数据分析,你可以定期(例如,每天)重新运行分析以响应数据的变化,但流处理允许分析系统以秒级的速度响应事件。根据应用程序及其时间敏感性,流处理方法可能很有价值,例如识别和阻止潜在的欺诈或滥用活动。
|
||||
|
||||
在某些情况下,分析系统的输出被提供给事务型系统(这个过程有时被称为 **反向 ETL** [^19])。例如,在分析系统上训练的机器学习模型可能会部署到生产环境中,以便它可以为最终用户生成推荐,例如"购买了 X 的人也购买了 Y"。这种分析系统的部署输出也被称为 **数据产品** [^20]。
|
||||
机器学习模型可以使用专门的工具(如 TFX、Kubeflow 或 MLflow)部署到事务型系统。
|
||||
在某些情况下,分析系统的输出被提供给事务型系统(这个过程有时被称为 **反向 ETL** [^19])。例如,在分析系统中训练的机器学习模型可能会部署到生产环境中,以便为最终用户生成推荐,例如"购买了 X 的人也购买了 Y"。这种分析系统的部署输出也被称为 **数据产品** [^20]。机器学习模型可以使用 TFX、Kubeflow 或 MLflow 等专门工具部署到事务型系统。
|
||||
|
||||
### 权威数据源与派生数据 {#sec_introduction_derived}
|
||||
|
||||
与事务型和分析型系统的区别相关,本书还区分了 **权威记录系统** 和 **派生数据系统**。这些术语很有用,因为它们可以帮助你理清数据在系统中的流动:
|
||||
与事务型系统和分析型系统之间的区别相关,本书还区分了 **权威记录系统** 和 **衍生数据系统**。这些术语很有用,因为它们可以帮助你澄清数据在系统中的流动:
|
||||
|
||||
权威记录系统
|
||||
: 权威记录系统(SoR, System of Record),也称为 **权威数据源**,保存某些数据的权威或 **规范** 版本。当新数据进入时,例如作为用户输入,它首先被写入这里。每个事实只表示一次(表示通常是 **规范化的**;参见["规范化、反规范化和连接"](/ch3#sec_datamodels_normalization))。如果另一个系统和权威记录系统之间存在任何差异,那么权威记录系统中的值(按定义)是正确的。
|
||||
: 权威记录系统,也称为 **权威数据源**,保存某些数据的权威或 **规范** 版本。当新数据进入时,例如作为用户输入,它首先写入这里。每个事实只表示一次(表示通常是 **规范化** 的;参见["规范化、反规范化和连接"](/ch3#sec_datamodels_normalization))。如果另一个系统与权威记录系统之间存在任何差异,那么权威记录系统中的值(根据定义)是正确的。
|
||||
|
||||
派生数据系统
|
||||
: 派生系统中的数据是通过获取另一个系统中的一些现有数据并以某种方式转换或处理它的结果。如果你丢失了派生数据,你可以从原始源重新创建它。一个经典的例子是缓存:如果存在,可以从缓存中提供数据,但如果缓存不包含你需要的内容,你可以回退到底层数据库。反规范化的值、索引、物化视图、转换的数据表示以及在数据集上训练的模型也属于这一类别。
|
||||
衍生数据系统
|
||||
: 衍生系统中的数据是从另一个系统获取一些现有数据并以某种方式转换或处理它的结果。如果你丢失了衍生数据,你可以从原始源重新创建它。一个经典的例子是缓存:如果存在,可以从缓存提供数据,但如果缓存不包含你需要的内容,你可以回退到底层数据库。反规范化值、索引、物化视图、转换的数据表示和在数据集上训练的模型也属于这一类别。
|
||||
|
||||
从技术上讲,派生数据是 **冗余的**,因为它复制了现有信息。然而,它对于在读查询上获得良好性能通常是必不可少的。你可以从单个源派生几个不同的数据集,使你能够从不同的"视角"查看数据。
|
||||
从技术上讲,衍生数据是 **冗余** 的,因为它复制了现有信息。然而,它通常对于在读取查询上获得良好性能至关重要。你可以从单个源派生几个不同的数据集,使你能够从不同的"视角"查看数据。
|
||||
|
||||
分析系统通常是派生数据系统,因为它们是在其他地方创建的数据的消费者。事务型服务可能包含权威记录系统和派生数据系统的混合。权威记录系统是数据首次写入的主要数据库,而派生数据系统是加速常见读取操作的索引和缓存,特别是对于权威记录系统无法有效回答的查询。
|
||||
分析系统通常是衍生数据系统,因为它们是在其他地方创建的数据的消费者。事务型服务可能包含权威记录系统和衍生数据系统的混合。权威记录系统是首先写入数据的主数据库,而衍生数据系统是加速常见读取操作的索引和缓存,特别是对于权威记录系统无法有效回答的查询。
|
||||
|
||||
大多数数据库、存储引擎和查询语言本质上不是权威记录系统或派生系统。数据库只是一个工具:如何使用它取决于你。权威记录系统和派生数据系统之间的区别不取决于工具,而是取决于你在应用程序中如何使用它。通过明确哪些数据是从哪些其他数据派生的,你可以为原本令人困惑的系统架构带来清晰度。
|
||||
大多数数据库、存储引擎和查询语言本质上不是权威记录系统或衍生系统。数据库只是一个工具:如何使用它取决于你。权威记录系统和衍生数据系统之间的区别不取决于工具,而取决于你如何在应用程序中使用它。通过明确哪些数据是从哪些其他数据派生的,你可以为原本混乱的系统架构带来清晰度。
|
||||
|
||||
当一个系统中的数据是从另一个系统中的数据派生的时,你需要一个过程来在权威记录系统中的原始数据发生变化时更新派生数据。不幸的是,许多数据库的设计基于你的应用程序只需要使用那一个数据库的假设,它们不容易集成多个系统以传播此类更新。在[待补充链接]中,我们将讨论 **数据集成** 的方法,这允许我们组合多个数据系统来实现单个系统无法完成的事情。
|
||||
当一个系统中的数据源自另一个系统中的数据时,你需要一个过程来在权威记录系统中的原始数据发生变化时更新衍生数据。不幸的是,许多数据库的设计基于这样的假设:你的应用程序只需要使用那一个数据库,它们不易于集成多个系统以传播此类更新。在[待补充链接]中,我们将讨论 **数据集成** 的方法,这允许我们组合多个数据系统来实现单个系统无法做到的事情。
|
||||
|
||||
这就结束了我们对分析和事务处理的比较。在下一节中,我们将研究另一个你可能已经看到多次争论的权衡。
|
||||
|
||||
这就是我们对分析和事务处理比较的结束。在下一节中,我们将研究另一个你可能已经看到多次辩论的权衡。
|
||||
|
||||
|
||||
|
||||
@ -188,243 +186,226 @@ Apache Hive、Spark SQL、Presto 和 Trino 是这种方法的示例。
|
||||
|
||||
对于组织需要做的任何事情,首要问题之一是:应该在内部完成,还是应该外包?应该自建还是购买?
|
||||
|
||||
最终,这是一个关于业务优先级的问题。管理智慧认为,作为组织核心竞争力或竞争优势的事情应该在内部完成,而非核心、常规或普通的事情应该留给供应商 [^21]。
|
||||
举一个极端的例子,大多数公司不自己发电(除非他们是能源公司,撇开紧急备用电源不谈),因为从电网购买电力更便宜。
|
||||
归根结底,这是一个关于业务优先级的问题。公认的管理智慧是,作为组织核心竞争力或竞争优势的事物应该在内部完成,而非核心、例行或常见的事物应该留给供应商 [^21]。举一个极端的例子,大多数公司不会自己发电(除非他们是能源公司,而且不考虑紧急备用电源),因为从电网购买电力更便宜。
|
||||
|
||||
对于软件,需要做出两个重要决定:谁构建软件以及谁部署它。外包每个决定的程度有多种可能性,如[图 1-2](/ch1#fig_cloud_spectrum) 所示。一个极端是你自己编写和运行的定制软件;另一个极端是广泛使用的云服务或软件即服务(SaaS)产品,由外部供应商实施和运营,你只能通过 Web 界面或 API 访问。
|
||||
对于软件,需要做出的两个重要决定是谁构建软件和谁部署它。有一系列可能性,每个决定都在不同程度上外包,如[图 1-2](/ch1#fig_cloud_spectrum) 所示。一个极端是你自己编写并在内部运行的定制软件;另一个极端是广泛使用的云服务或软件即服务(SaaS)产品,由外部供应商实施和运营,你只能通过 Web 界面或 API 访问。
|
||||
|
||||
{{< figure src="/fig/ddia_0102.png" id="fig_cloud_spectrum" caption="图 1-2. 软件类型及其操作的范围。" class="w-full my-4" >}}
|
||||
{{< figure src="/fig/ddia_0102.png" id="fig_cloud_spectrum" caption="图 1-2. 软件类型及其运营的范围。" class="w-full my-4" >}}
|
||||
|
||||
中间地带是你 **自托管** 的现成软件(开源或商业),即自己部署——例如,如果你下载 MySQL 并将其安装在你控制的服务器上。这可能在你自己的硬件上(通常称为 **本地部署**,即使服务器实际上在租用的数据中心机架中,而不是字面上在你自己的场所),或者在云中的虚拟机上(**基础设施即服务** 或 IaaS)。沿着这个范围还有更多的点,例如,采用开源软件并运行它的修改版本。
|
||||
中间地带是你 **自托管** 的现成软件(开源或商业),即自己部署——例如,如果你下载 MySQL 并将其安装在你控制的服务器上。这可能在你自己的硬件上(通常称为 **本地部署**,即使服务器实际上在租用的数据中心机架中而不是字面上在你自己的场所),或者在云中的虚拟机上(**基础设施即服务** 或 IaaS)。沿着这个范围还有更多的点,例如,采用开源软件并运行其修改版本。
|
||||
|
||||
与这个范围分开的还有 **如何** 部署服务的问题,无论是在云中还是本地——例如,你是否使用像 Kubernetes 这样的编排框架。然而,部署工具的选择超出了本书的范围,因为其他因素对数据系统的架构有更大的影响。
|
||||
与这个范围分开的还有 **如何** 部署服务的问题,无论是在云中还是在本地——例如,是否使用 Kubernetes 等编排框架。然而,部署工具的选择超出了本书的范围,因为其他因素对数据系统的架构有更大的影响。
|
||||
|
||||
### 云服务的利弊 {#sec_introduction_cloud_tradeoffs}
|
||||
|
||||
使用云服务而不是自己运行类似的软件,本质上是将该软件的操作外包给云提供商。有支持和反对云服务的充分论据。云提供商声称使用他们的服务可以节省你的时间和金钱,并且与设置自己的基础设施相比,可以让你更快地行动。
|
||||
使用云服务而不是自己运行可比较的软件,本质上是将该软件的运营外包给云提供商。使用云服务有充分的支持和反对理由。云提供商声称,使用他们的服务可以节省你的时间和金钱,并允许你比设置自己的基础设施更快地移动。
|
||||
|
||||
云服务是否实际上比自托管更便宜、更容易,很大程度上取决于你的技能和系统的工作负载。如果你已经有设置和操作所需系统的经验,并且你的负载相当可预测(即,你需要的机器数量不会剧烈波动),那么购买自己的机器并自己运行软件通常更便宜 [^22] [^23]。
|
||||
云服务实际上是否比自托管更便宜、更容易,很大程度上取决于你的技能和系统的工作负载。如果你已经有设置和运营所需系统的经验,并且你的负载相当可预测(即,你需要的机器数量不会剧烈波动),那么购买自己的机器并自己在上面运行软件通常更便宜 [^22] [^23]。
|
||||
|
||||
另一方面,如果你需要一个你还不知道如何部署和操作的系统,那么采用云服务通常比自己学习管理系统更容易、更快。如果你必须专门雇用和培训员工来维护和操作系统,那可能会变得非常昂贵。当你使用云时,你仍然需要一个运维团队(参见["云时代的运维"](/ch1#sec_introduction_operations)),但外包基本系统管理可以让你的团队专注于更高层次的问题。
|
||||
另一方面,如果你需要一个你还不知道如何部署和运营的系统,那么采用云服务通常比学习自己管理系统更容易、更快。如果你必须专门雇用和培训员工来维护和运营系统,那可能会变得非常昂贵。使用云时你仍然需要一个运维团队(参见["云时代的运维"](/ch1#sec_introduction_operations)),但外包基本的系统管理可以让你的团队专注于更高层次的问题。
|
||||
|
||||
当你将系统的操作外包给专门运行该服务的公司时,这可能会带来更好的服务,因为提供商从为许多客户提供服务中获得了运营专业知识。另一方面,如果你自己运行服务,你可以配置和调整它以在你的特定工作负载上表现良好;云服务不太可能愿意代表你进行此类自定义。
|
||||
当你将系统的运营外包给专门运行该服务的公司时,这可能会带来更好的服务,因为提供商从向许多客户提供服务中获得了运营专业知识。另一方面,如果你自己运行服务,你可以配置和调整它以在你的特定工作负载上表现良好;云服务不太可能愿意代表你进行此类定制。
|
||||
|
||||
如果你的系统负载随时间变化很大,云服务特别有价值。如果你配置机器以能够处理峰值负载,但这些计算资源大部分时间都处于闲置状态,系统就变得不太划算。在这种情况下,云服务的优势在于它们可以更容易地根据需求变化扩大或缩小你的计算资源。
|
||||
如果你的系统负载随时间变化很大,云服务特别有价值。如果你配置机器以能够处理峰值负载,但这些计算资源大部分时间都处于空闲状态,系统就变得不太具有成本效益。在这种情况下,云服务的优势在于它们可以更容易地根据需求变化向上或向下扩展你的计算资源。
|
||||
|
||||
例如,分析系统通常具有极其可变的负载:快速运行大型分析查询需要并行的大量计算资源,但一旦查询完成,这些资源就会闲置,直到用户进行下一个查询。预定义的查询(例如,用于每日报告)可以排队和调度以平滑负载,但对于交互式查询,你希望它们完成得越快,工作负载就越可变。如果你的数据集如此之大,以至于快速查询它需要大量的计算资源,使用云可以节省资金,因为你可以将未使用的资源返回给提供商,而不是让它们闲置。对于较小的数据集,这种差异不太显著。
|
||||
例如,分析系统通常具有极其可变的负载:快速运行大型分析查询需要并行使用大量计算资源,但一旦查询完成,这些资源就会处于空闲状态,直到用户进行下一个查询。预定义的查询(例如,每日报告)可以排队和调度以平滑负载,但对于交互式查询,你希望它们完成得越快,工作负载就变得越可变。如果你的数据集如此之大,以至于快速查询需要大量的计算资源,使用云可以节省资金,因为你可以将未使用的资源返回给提供商,而不是让它们闲置。对于较小的数据集,这种差异不太显著。
|
||||
|
||||
云服务的最大缺点是你无法控制它:
|
||||
|
||||
* 如果它缺少你需要的功能,你所能做的就是礼貌地询问供应商是否会添加它;你通常不能自己实现它。
|
||||
* 如果服务宕机,你所能做的就是等待它恢复。
|
||||
* 如果你使用服务的方式触发了错误或导致性能问题,你将很难诊断问题。使用你自己运行的软件,你可以从操作系统获取性能指标和调试信息,以帮助你理解其行为,并且你可以查看服务器日志,但使用供应商托管的服务,你通常无法访问这些内部信息。
|
||||
* 此外,如果服务关闭或变得不可接受地昂贵,或者如果供应商决定以你不喜欢的方式更改他们的产品,你将任由他们摆布——继续运行软件的旧版本通常不是一个选项,因此你将被迫迁移到替代服务 [^24]。
|
||||
如果有其他服务公开兼容的 API,这种风险会得到缓解,但对于许多云服务,没有标准 API,这提高了切换成本,使供应商锁定成为一个问题。
|
||||
* 如果你以触发错误或导致性能问题的方式使用服务,你将很难诊断问题。对于你自己运行的软件,你可以从操作系统获取性能指标和调试信息来帮助你理解其行为,你可以查看服务器日志,但对于供应商托管的服务,你通常无法访问这些内部信息。
|
||||
* 此外,如果服务关闭或变得无法接受地昂贵,或者如果供应商决定以你不喜欢的方式更改他们的产品,你就受制于他们——继续运行软件的旧版本通常不是一个选项,所以你将被迫迁移到替代服务 [^24]。如果有暴露兼容 API 的替代服务,这种风险会得到缓解,但对于许多云服务,没有标准 API,这增加了切换成本,使供应商锁定成为一个问题。
|
||||
* 云提供商需要被信任以保持数据安全,这可能会使遵守隐私和安全法规的过程复杂化。
|
||||
|
||||
尽管存在所有这些风险,组织在云服务之上构建新应用程序或采用混合方法(云服务用于系统的某些方面)变得越来越流行。然而,云服务不会取代所有内部数据系统:许多较旧的系统早于云,对于任何具有现有云服务无法满足的专业要求的服务,内部系统仍然是必要的。例如,对延迟非常敏感的应用程序(如高频交易)需要对硬件的完全控制。
|
||||
尽管有所有这些风险,组织在云服务之上构建新应用程序或采用混合方法(其中云服务用于系统的某些方面)变得越来越流行。然而,云服务不会取代所有内部数据系统:许多较旧的系统早于云,对于任何具有现有云服务无法满足的专业要求的服务,内部系统仍然是必要的。例如,对延迟非常敏感的应用程序(如高频交易)需要完全控制硬件。
|
||||
|
||||
### 云原生系统架构 {#sec_introduction_cloud_native}
|
||||
|
||||
除了具有不同的经济模型(订阅服务而不是购买硬件和许可软件在其上运行),云的兴起还对数据系统在技术层面上的实现产生了深远的影响。术语 **云原生** 用于描述旨在利用云服务的架构。
|
||||
除了具有不同的经济模型(订阅服务而不是购买硬件和许可软件在其上运行)之外,云的兴起也对数据系统在技术层面的实现产生了深远的影响。术语 **云原生** 用于描述旨在利用云服务的架构。
|
||||
|
||||
原则上,几乎任何你可以自托管的软件也可以作为云服务提供,实际上,许多流行的数据系统现在都有托管服务。然而,从头开始设计为云原生的系统已被证明具有几个优势:在相同硬件上具有更好的性能、更快地从故障中恢复、能够快速扩展计算资源以匹配负载,以及支持更大的数据集 [^25] [^26] [^27]。
|
||||
[表 1-2](/ch1#tab_cloud_native_dbs) 列出了两种类型系统的一些示例。
|
||||
原则上,几乎任何你可以自托管的软件也可以作为云服务提供,实际上,许多流行的数据系统现在都有托管服务。然而,从头开始设计为云原生的系统已被证明具有几个优势:在相同硬件上具有更好的性能、从故障中更快恢复、能够快速扩展计算资源以匹配负载,以及支持更大的数据集 [^25] [^26] [^27]。[表 1-2](/ch1#tab_cloud_native_dbs) 列出了两种类型系统的一些示例。
|
||||
|
||||
{{< figure id="#tab_cloud_native_dbs" title="表 1-2. 自托管和云原生数据库系统的示例" class="w-full my-4" >}}
|
||||
{{< figure id="#tab_cloud_native_dbs" title="表 1-2. 自托管和云原生数据库系统示例" class="w-full my-4" >}}
|
||||
|
||||
| 类别 | 自托管系统 | 云原生系统 |
|
||||
|----------|---------------------------|---------------------------------------------------------------------|
|
||||
| 事务型/OLTP | MySQL、PostgreSQL、MongoDB | AWS Aurora [^25]、Azure SQL DB Hyperscale [^26]、Google Cloud Spanner |
|
||||
| 分析型/OLAP | Teradata、ClickHouse、Spark | Snowflake [^27]、Google BigQuery、Azure Synapse Analytics |
|
||||
| 类别 | 自托管系统 | 云原生系统 |
|
||||
|------------------|----------------------------|----------------------------------------------------------------------|
|
||||
| 事务型/OLTP | MySQL、PostgreSQL、MongoDB | AWS Aurora [^25]、Azure SQL DB Hyperscale [^26]、Google Cloud Spanner |
|
||||
| 分析型/OLAP | Teradata、ClickHouse、Spark | Snowflake [^27]、Google BigQuery、Azure Synapse Analytics |
|
||||
|
||||
#### 云服务的分层 {#layering-of-cloud-services}
|
||||
|
||||
许多自托管数据系统具有非常简单的系统要求:它们在传统操作系统(如 Linux 或 Windows)上运行,将数据作为文件存储在文件系统上,并通过标准网络协议(如 TCP/IP)进行通信。少数系统依赖于特殊硬件,如 GPU(用于机器学习)或 RDMA 网络接口,但总的来说,自托管软件倾向于使用非常通用的计算资源:CPU、RAM、文件系统和 IP 网络。
|
||||
许多自托管数据系统的系统要求非常简单:它们在传统操作系统(如 Linux 或 Windows)上运行,将数据存储为文件系统上的文件,并通过 TCP/IP 等标准网络协议进行通信。少数系统依赖于特殊硬件,如 GPU(用于机器学习)或 RDMA 网络接口,但总的来说,自托管软件倾向于使用非常通用的计算资源:CPU、RAM、文件系统和 IP 网络。
|
||||
|
||||
在云中,这种类型的软件可以在基础设施即服务环境中运行,使用一个或多个具有一定 CPU、内存、磁盘和网络带宽分配的虚拟机(或 **实例**)。与物理机器相比,云实例可以更快地配置,并且有更多种类的大小,但在其他方面它们类似于传统计算机:你可以在其上运行任何你喜欢的软件,但你负责自己管理它。
|
||||
在云中,这种类型的软件可以在基础设施即服务环境中运行,使用一个或多个虚拟机(或 **实例**),分配一定的 CPU、内存、磁盘和网络带宽。与物理机器相比,云实例可以更快地配置,并且有更多种类的大小,但除此之外,它们与传统计算机类似:你可以在上面运行任何你喜欢的软件,但你负责自己管理它。
|
||||
|
||||
相比之下,云原生服务的关键思想不仅是使用由你的操作系统管理的计算资源,还要基于较低级别的云服务构建更高级别的服务。例如:
|
||||
相比之下,云原生服务的关键思想是不仅使用由操作系统管理的计算资源,还基于较低级别的云服务构建更高级别的服务。例如:
|
||||
|
||||
* **对象存储** 服务(如 Amazon S3、Azure Blob Storage 和 Cloudflare R2)存储大文件。它们提供比典型文件系统更有限的 API(基本文件读写),但它们的优势在于隐藏了底层物理机器:服务自动将数据分布在许多机器上,因此你不必担心任何一台机器上的磁盘空间用完。即使某些机器或其磁盘完全故障,也不会丢失数据。
|
||||
* 许多其他服务又建立在对象存储和其他云服务之上:例如,Snowflake 是一个基于云的分析数据库(数据仓库),它依赖于 S3 进行数据存储 [^27],而其他一些服务又建立在 Snowflake 之上。
|
||||
* 许多其他服务反过来建立在对象存储和其他云服务之上:例如,Snowflake 是一个基于云的分析数据库(数据仓库),依赖于 S3 进行数据存储 [^27],而一些其他服务反过来建立在 Snowflake 之上。
|
||||
|
||||
与计算中的抽象一样,没有一个你应该使用的正确答案。作为一般规则,更高级别的抽象往往更面向特定用例。如果你的需求与为其设计更高级别系统的情况相匹配,使用现有的更高级别系统可能会以比从较低级别系统自己构建它少得多的麻烦提供你需要的东西。另一方面,如果没有满足你需求的高级系统,那么从较低级别的组件自己构建它是唯一的选择。
|
||||
与计算中的抽象一样,没有一个正确的答案告诉你应该使用什么。作为一般规则,更高级别的抽象往往更面向特定的用例。如果你的需求与为其设计更高级别系统的情况相匹配,使用现有的更高级别系统可能会以比从较低级别系统自己构建它少得多的麻烦提供你需要的东西。另一方面,如果没有满足你需求的高级系统,那么从较低级别的组件自己构建它是唯一的选择。
|
||||
|
||||
#### 存储与计算的分离 {#sec_introduction_storage_compute}
|
||||
|
||||
在传统计算中,磁盘存储被认为是持久的(我们假设一旦某些东西被写入磁盘,它就不会丢失)。为了容忍单个硬盘的故障,RAID(独立磁盘冗余阵列)通常用于在连接到同一台机器的多个磁盘上维护数据副本。RAID 可以在硬件中或由操作系统在软件中执行,并且它对访问文件系统的应用程序是透明的。
|
||||
在传统计算中,磁盘存储被认为是持久的(我们假设一旦某些东西被写入磁盘,它就不会丢失)。为了容忍单个硬盘的故障,通常使用 RAID(独立磁盘冗余阵列)在连接到同一台机器的几个磁盘上维护数据副本。RAID 可以在硬件中执行,也可以由操作系统在软件中执行,它对访问文件系统的应用程序是透明的。
|
||||
|
||||
在云中,计算实例(虚拟机)也可能有本地磁盘连接,但云原生系统通常将这些磁盘更像临时缓存,而不像长期存储。这是因为如果关联的实例发生故障,或者如果实例被更大或更小的实例(在不同的物理机器上)替换以适应负载变化,本地磁盘将变得不可访问。
|
||||
在云中,计算实例(虚拟机)也可能有本地磁盘连接,但云原生系统通常将这些磁盘更多地视为临时缓存,而不是长期存储。这是因为如果相关实例失败,或者如果实例被更大或更小的实例(在不同的物理机器上)替换以适应负载变化,本地磁盘就会变得不可访问。
|
||||
|
||||
作为本地磁盘的替代方案,云服务还提供虚拟磁盘存储,可以从一个实例分离并附加到另一个实例(Amazon EBS、Azure 托管磁盘和 Google Cloud 中的持久磁盘)。这样的虚拟磁盘实际上不是物理磁盘,而是由一组单独的机器提供的云服务,它模拟磁盘(**块设备**,其中每个块通常为 4 KiB 大小)的行为。这项技术使得在云中运行传统的基于磁盘的软件成为可能,但块设备仿真引入了可以在从头开始为云设计的系统中避免的开销 [^25]。它还使应用程序对网络故障非常敏感,因为虚拟块设备上的每个 I/O 实际上都是网络调用 [^28]。
|
||||
作为本地磁盘的替代方案,云服务还提供可以从一个实例分离并附加到另一个实例的虚拟磁盘存储(Amazon EBS、Azure 托管磁盘和 Google Cloud 中的持久磁盘)。这种虚拟磁盘实际上不是物理磁盘,而是由一组单独的机器提供的云服务,它模拟磁盘的行为(**块设备**,其中每个块通常为 4 KiB 大小)。这项技术使得在云中运行传统的基于磁盘的软件成为可能,但块设备仿真引入了在从头为云设计的系统中可以避免的开销 [^25]。它还使应用程序对网络故障非常敏感,因为虚拟块设备上的每个 I/O 实际上都是网络调用 [^28]。
|
||||
|
||||
为了解决这个问题,云原生服务通常避免使用虚拟磁盘,而是建立在针对特定工作负载优化的专用存储服务上。对象存储服务(如 S3)专为长期存储相当大的文件而设计,大小从数百千字节到几千兆字节不等。存储在数据库中的单个行或值通常比这小得多;因此,云数据库通常在单独的服务中管理较小的值,并在对象存储中存储较大的数据块(包含许多单个值)[^26] [^29]。我们将在[第 4 章](/ch4#ch_storage)中看到这样做的方法。
|
||||
为了解决这个问题,云原生服务通常避免使用虚拟磁盘,而是建立在针对特定工作负载优化的专用存储服务之上。对象存储服务(如 S3)设计用于长期存储相当大的文件,大小从数百千字节到几千兆字节不等。数据库中存储的单个行或值通常比这小得多;因此,云数据库通常在单独的服务中管理较小的值,并将较大的数据块(包含许多单个值)存储在对象存储中 [^26] [^29]。我们将在[第 4 章](/ch4#ch_storage)中看到这样做的方法。
|
||||
|
||||
在传统系统架构中,同一台计算机负责存储(磁盘)和计算(CPU 和 RAM),但在云原生系统中,这两个职责已经在某种程度上分离或 **解耦** [^9] [^27] [^30] [^31]:
|
||||
例如,S3 只存储文件,如果你想分析该数据,你必须在 S3 之外的某个地方运行分析代码。这意味着通过网络传输数据,我们将在["分布式与单节点系统"](/ch1#sec_introduction_distributed)中进一步讨论。
|
||||
在传统的系统架构中,同一台计算机负责存储(磁盘)和计算(CPU 和 RAM),但在云原生系统中,这两个职责已经在某种程度上分离或 **解耦** [^9] [^27] [^30] [^31]:例如,S3 只存储文件,如果你想分析该数据,你必须在 S3 之外的某个地方运行分析代码。这意味着通过网络传输数据,我们将在["分布式与单节点系统"](/ch1#sec_introduction_distributed)中进一步讨论。
|
||||
|
||||
此外,云原生系统通常是 **多租户** 的,这意味着不是每个客户都有一个单独的机器,而是来自多个不同客户的数据和计算由同一服务在同一共享硬件上处理 [^32]。
|
||||
此外,云原生系统通常是 **多租户** 的,这意味着不是每个客户都有一台单独的机器,而是来自几个不同客户的数据和计算由同一服务在同一共享硬件上处理 [^32]。
|
||||
|
||||
多租户可以实现更好的硬件利用率、更容易的可扩展性以及云提供商更容易的管理,但它还需要仔细的工程设计,以确保一个客户的活动不会影响其他客户的系统性能或安全性 [^33]。
|
||||
多租户可以实现更好的硬件利用率、更容易的可伸缩性和云提供商更容易的管理,但它也需要仔细的工程设计,以确保一个客户的活动不会影响其他客户的系统性能或安全性 [^33]。
|
||||
|
||||
### 云时代的运维 {#sec_introduction_operations}
|
||||
|
||||
传统上,管理组织服务器端数据基础设施的人被称为 **数据库管理员**(DBA)或 **系统管理员**(sysadmins)。最近,许多组织试图将软件开发和运维的角色整合到具有后端服务和数据基础设施共同责任的团队中;**DevOps** 理念指导了这一趋势。**站点可靠性工程师**(SRE)是 Google 对这一想法的实现 [^34]。
|
||||
传统上,管理组织服务器端数据基础设施的人员被称为 **数据库管理员**(DBA)或 **系统管理员**(sysadmins)。最近,许多组织已经尝试将软件开发和运维的角色整合到团队中,共同负责后端服务和数据基础设施;**DevOps** 理念引导了这一趋势。**站点可靠性工程师**(SRE)是 Google 对这个想法的实现 [^34]。
|
||||
|
||||
运维的作用是确保服务可靠地交付给用户(包括配置基础设施和部署应用程序),并确保稳定的生产环境(包括监控和诊断可能影响可靠性的任何问题)。对于自托管系统,运维传统上涉及在单个机器级别上的大量工作,例如容量规划(例如,监控可用磁盘空间并在空间用完之前添加更多磁盘)、配置新机器、将服务从一台机器移动到另一台机器以及安装操作系统补丁。
|
||||
运维的作用是确保服务可靠地交付给用户(包括配置基础设施和部署应用程序),并确保稳定的生产环境(包括监控和诊断可能影响可靠性的任何问题)。对于自托管系统,运维传统上涉及大量在单个机器级别的工作,例如容量规划(例如,监控可用磁盘空间并在空间用完之前添加更多磁盘)、配置新机器、将服务从一台机器移动到另一台机器,以及安装操作系统补丁。
|
||||
|
||||
许多云服务提供了一个隐藏实际实现服务的单个机器的 API。例如,云存储用 **计量计费** 取代了固定大小的磁盘,你可以存储数据而无需提前规划容量需求,然后根据实际使用的空间收费。此外,许多云服务保持高可用,即使单个机器发生故障(参见["可靠性和容错"](/ch2#sec_introduction_reliability))。
|
||||
许多云服务提供了一个 API,隐藏了实际实现服务的单个机器。例如,云存储用 **计量计费** 替换固定大小的磁盘,你可以存储数据而无需提前规划容量需求,然后根据实际使用的空间收费。此外,许多云服务保持高可用性,即使单个机器发生故障(参见["可靠性与容错"](/ch2#sec_introduction_reliability))。
|
||||
|
||||
从单个机器到服务的重点转移伴随着运维角色的变化。提供可靠服务的高级目标保持不变,但流程和工具已经发展。DevOps/SRE 理念更加强调:
|
||||
|
||||
* 自动化——偏好可重复的流程而不是手动一次性作业,
|
||||
* 偏好短暂的虚拟机和服务而不是长期运行的服务器,
|
||||
* 实现频繁的应用程序更新,
|
||||
* 自动化——优先考虑可重复的流程而不是手动的一次性工作,
|
||||
* 优先考虑短暂的虚拟机和服务而不是长期运行的服务器,
|
||||
* 启用频繁的应用程序更新,
|
||||
* 从事件中学习,以及
|
||||
* 保留组织关于系统的知识,即使个人来来去去 [^35]。
|
||||
|
||||
随着云服务的兴起,角色出现了分歧:基础设施公司的运维团队专门为大量客户提供可靠服务的细节,而服务的客户在基础设施上花费尽可能少的时间和精力 [^36]。
|
||||
随着云服务的兴起,角色出现了分叉:基础设施公司的运维团队专门研究向大量客户提供可靠服务的细节,而服务的客户在基础设施上花费尽可能少的时间和精力 [^36]。
|
||||
|
||||
云服务的客户仍然需要运维,但他们专注于不同的方面,例如为给定任务选择最合适的服务、将不同的服务相互集成以及从一个服务迁移到另一个服务。即使计量计费消除了传统意义上的容量规划需求,了解你为哪个目的使用哪些资源仍然很重要,这样你就不会在不需要的云资源上浪费金钱:容量规划变成了财务规划,性能优化变成了成本优化 [^37]。
|
||||
云服务的客户仍然需要运维,但他们专注于不同的方面,例如为给定任务选择最合适的服务、将不同服务相互集成,以及从一个服务迁移到另一个服务。即使计量计费消除了传统意义上的容量规划需求,了解你为哪个目的使用哪些资源仍然很重要,这样你就不会在不需要的云资源上浪费金钱:容量规划变成了财务规划,性能优化变成了成本优化 [^37]。
|
||||
|
||||
此外,云服务确实有资源限制或 **配额**(例如你可以同时运行的最大进程数),你需要在遇到它们之前了解并规划它们 [^38]。
|
||||
此外,云服务确实有资源限制或 **配额**(例如你可以同时运行的最大进程数),你需要在遇到它们之前了解并规划这些 [^38]。
|
||||
|
||||
采用云服务可能比运行自己的基础设施更容易、更快,尽管即使在这里,学习如何使用它也有成本,也许还要解决其限制。随着越来越多的供应商提供针对不同用例的越来越广泛的云服务,不同服务之间的集成成为一个特别的挑战 [^39] [^40]。
|
||||
采用云服务可能比运行自己的基础设施更容易、更快,尽管即使在这里,学习如何使用它也有成本,也许还要解决其限制。随着越来越多的供应商提供针对不同用例的更广泛的云服务,不同服务之间的集成成为一个特别的挑战 [^39] [^40]。
|
||||
|
||||
ETL(参见["数据仓库"](/ch1#sec_introduction_dwh))只是故事的一部分;事务型云服务也需要相互集成。目前,缺乏促进这种集成的标准,因此它通常涉及大量的手动工作。
|
||||
|
||||
其他不能完全外包给云服务的运维方面包括维护应用程序及其使用的库的安全性、管理你自己的服务之间的交互、监控服务的负载以及追踪问题的原因,例如性能下降或中断。虽然云正在改变运维的角色,但对运维的需求仍然很大。
|
||||
无法完全外包给云服务的其他运维方面包括维护应用程序及其使用的库的安全性、管理你自己的服务之间的交互、监控服务的负载,以及追踪问题的原因,例如性能下降或中断。虽然云正在改变运维的角色,但对运维的需求比以往任何时候都大。
|
||||
|
||||
|
||||
|
||||
## 分布式与单节点系统 {#sec_introduction_distributed}
|
||||
|
||||
涉及多台通过网络通信的机器的系统称为 **分布式系统**。参与分布式系统的每个进程称为一个 **节点**。你可能希望系统分布式的原因有很多:
|
||||
涉及多台机器通过网络通信的系统称为 **分布式系统**。参与分布式系统的每个进程称为 **节点**。你可能希望系统分布式的原因有多种:
|
||||
|
||||
固有分布式系统
|
||||
固有的分布式系统
|
||||
: 如果应用程序涉及两个或多个交互用户,每个用户使用自己的设备,那么系统不可避免地是分布式的:设备之间的通信必须通过网络进行。
|
||||
|
||||
云服务之间的请求
|
||||
: 如果数据存储在一个服务中但在另一个服务中处理,则必须通过网络从一个服务传输到另一个服务。
|
||||
|
||||
容错/高可用
|
||||
: 如果你的应用程序需要继续工作,即使一台机器(或几台机器、网络或整个数据中心)宕机,你可以使用多台机器来提供冗余。当一个失败时,另一个可以接管。参见["可靠性和容错"](/ch2#sec_introduction_reliability)和[第 6 章](/ch6#ch_replication)关于复制。
|
||||
容错/高可用性
|
||||
: 如果你的应用程序需要在一台机器(或几台机器、网络或整个数据中心)发生故障时继续工作,你可以使用多台机器为你提供冗余。当一台故障时,另一台可以接管。参见["可靠性与容错"](/ch2#sec_introduction_reliability)和[第 6 章](/ch6#ch_replication)关于复制的内容。
|
||||
|
||||
可扩展性
|
||||
: 如果你的数据量或计算需求增长超过单台机器可以处理的范围,你可以潜在地将负载分散到多台机器上。参见["可扩展性"](/ch2#sec_introduction_scalability)。
|
||||
可伸缩性
|
||||
: 如果你的数据量或计算需求增长超过单台机器的处理能力,你可以潜在地将负载分散到多台机器上。参见["可伸缩性"](/ch2#sec_introduction_scalability)。
|
||||
|
||||
延迟
|
||||
: 如果你在世界各地都有用户,你可能希望在全球各个地区都有服务器,以便每个用户都可以从地理上靠近他们的服务器获得服务。这避免了用户必须等待网络数据包在世界各地传输才能回答他们的请求。参见["描述性能"](/ch2#sec_introduction_percentiles)。
|
||||
: 如果你在世界各地都有用户,你可能希望在全球各个地区都有服务器,以便每个用户都可以从地理位置接近他们的服务器获得服务。这避免了用户必须等待网络数据包绕地球半圈才能回答他们的请求。参见["描述性能"](/ch2#sec_introduction_percentiles)。
|
||||
|
||||
弹性
|
||||
: 如果你的应用程序在某些时候繁忙而在其他时候空闲,云部署可以根据需求扩大或缩小,因此你只需为正在积极使用的资源付费。这在单台机器上更困难,它需要配置以处理最大负载,即使在几乎不使用时也是如此。
|
||||
: 如果你的应用程序在某些时候很忙,在其他时候很空闲,云部署可以根据需求向上或向下扩展,因此你只需为实际使用的资源付费。这在单台机器上更困难,它需要配置以处理最大负载,即使在几乎不使用的时候也是如此。
|
||||
|
||||
使用专用硬件
|
||||
: 系统的不同部分可以利用不同类型的硬件来匹配其工作负载。例如,对象存储可能使用具有许多磁盘但很少 CPU 的机器,而数据分析系统可能使用具有大量 CPU 和内存但没有磁盘的机器,机器学习系统可能使用具有 GPU 的机器(对于训练深度神经网络和其他机器学习任务,GPU 比 CPU 高效得多)。
|
||||
: 系统的不同部分可以利用不同类型的硬件来匹配其工作负载。例如,对象存储可能使用具有许多磁盘但很少 CPU 的机器,而数据分析系统可能使用具有大量 CPU 和内存但没有磁盘的机器,机器学习系统可能使用具有 GPU 的机器(GPU 在训练深度神经网络和其他机器学习任务方面比 CPU 效率高得多)。
|
||||
|
||||
法律合规
|
||||
: 一些国家有数据驻留法,要求关于其管辖范围内人员的数据必须在该国家地理范围内存储和处理 [^41]。
|
||||
这些规则的范围各不相同——例如,在某些情况下,它仅适用于医疗或金融数据,而其他情况则更广泛。因此,在多个此类管辖区拥有用户的服务必须将其数据分布在多个位置的服务器上。
|
||||
: 一些国家有数据驻留法律,要求其管辖范围内的人员数据必须在该国地理范围内存储和处理 [^41]。这些规则的范围各不相同——例如,在某些情况下,它仅适用于医疗或金融数据,而其他情况则更广泛。因此,在几个这样的管辖区域中拥有用户的服务将不得不将他们的数据分布在几个位置的服务器上。
|
||||
|
||||
可持续性
|
||||
: 如果你在何时何地运行作业方面有灵活性,你可能能够在可再生电力充足的时间和地点运行它们,并避免在电网承受压力时运行它们。这可以减少你的碳排放,并让你在电力便宜时利用便宜的电力 [^42] [^43]。
|
||||
: 如果你对运行作业的地点和时间有灵活性,你可能能够在可再生电力充足的时间和地点运行它们,并避免在电网紧张时运行它们。这可以减少你的碳排放,并允许你在电力可用时利用廉价的电力 [^42] [^43]。
|
||||
|
||||
这些原因既适用于你自己编写的服务(应用程序代码),也适用于由现成软件(如数据库)组成的服务。
|
||||
|
||||
### 分布式系统的问题 {#sec_introduction_dist_sys_problems}
|
||||
|
||||
分布式系统也有缺点。通过网络进行的每个请求和 API 调用都需要处理失败的可能性:网络可能中断,或者服务可能过载或崩溃,因此任何请求都可能超时而不收到响应。在这种情况下,我们不知道服务是否收到了请求,简单地重试它可能不安全。我们将在[第 9 章](/ch9#ch_distributed)中详细讨论这些问题。
|
||||
分布式系统也有缺点。通过网络进行的每个请求和 API 调用都需要处理失败的可能性:网络可能中断,或者服务可能过载或崩溃,因此任何请求都可能超时而没有收到响应。在这种情况下,我们不知道服务是否收到了请求,简单地重试它可能不安全。我们将在[第 9 章](/ch9#ch_distributed)中详细讨论这些问题。
|
||||
|
||||
虽然数据中心网络很快,但调用另一个服务仍然比在同一进程中调用函数慢得多 [^44]。
|
||||
尽管数据中心网络很快,但调用另一个服务仍然比在同一进程中调用函数慢得多 [^44]。
|
||||
|
||||
在处理大量数据时,与其将数据从存储传输到处理它的单独机器,不如将计算带到已经拥有数据的机器上可能更快 [^45]。
|
||||
|
||||
更多的节点并不总是更快:在某些情况下,一台计算机上的简单单线程程序可以比拥有超过 100 个 CPU 核心的集群表现得明显更好 [^46]。
|
||||
更多的节点并不总是更快:在某些情况下,单台计算机上的简单单线程程序可以比具有 100 多个 CPU 核心的集群表现得更好 [^46]。
|
||||
|
||||
故障排除分布式系统通常很困难:如果系统响应缓慢,你如何找出问题所在?在 **可观测性** 的标题下开发了诊断分布式系统问题的技术 [^47] [^48],
|
||||
这涉及收集有关系统执行的数据,并允许以允许分析高级指标和单个事件的方式对其进行查询。**追踪** 工具(如 OpenTelemetry、Zipkin 和 Jaeger)允许你跟踪哪个客户端为哪个操作调用了哪个服务器,以及每个调用花费了多长时间 [^49]。
|
||||
故障排除分布式系统通常很困难:如果系统响应缓慢,你如何找出问题所在?在 **可观测性** [^47] [^48] 的标题下开发了诊断分布式系统问题的技术,这涉及收集有关系统执行的数据,并允许以允许分析高级指标和单个事件的方式进行查询。**追踪** 工具(如 OpenTelemetry、Zipkin 和 Jaeger)允许你跟踪哪个客户端为哪个操作调用了哪个服务器,以及每次调用花费了多长时间 [^49]。
|
||||
|
||||
数据库提供了各种机制来确保数据一致性,正如我们将在[第 6 章](/ch6#ch_replication)和[第 8 章](/ch8#ch_transactions)中看到的。然而,当每个服务都有自己的数据库时,维护这些不同服务之间数据的一致性就成了应用程序的问题。
|
||||
分布式事务(我们在[第 8 章](/ch8#ch_transactions)中探讨)是确保一致性的一种可能技术,但它们很少在微服务环境中使用,因为它们与使服务彼此独立的目标背道而驰,并且许多数据库不支持它们 [^50]。
|
||||
数据库提供了各种机制来确保数据一致性,正如我们将在[第 6 章](/ch6#ch_replication)和[第 8 章](/ch8#ch_transactions)中看到的。然而,当每个服务都有自己的数据库时,维护这些不同服务之间的数据一致性就成了应用程序的问题。分布式事务(我们在[第 8 章](/ch8#ch_transactions)中探讨)是确保一致性的一种可能技术,但它们在微服务上下文中很少使用,因为它们违背了使服务彼此独立的目标,而且许多数据库不支持它们 [^50]。
|
||||
|
||||
由于所有这些原因,如果你可以在单台机器上做某事,与设置分布式系统相比,这通常要简单得多,成本也低得多 [^23] [^46] [^51]。
|
||||
CPU、内存和磁盘已经变得更大、更快、更可靠。与单节点数据库(如 DuckDB、SQLite 和 KùzuDB)结合使用时,许多工作负载现在可以在单个节点上运行。我们将在[第 4 章](/ch4#ch_storage)中更多地探讨这个主题。
|
||||
出于所有这些原因,如果你可以在单台机器上做某事,与设置分布式系统相比,这通常要简单得多,成本也更低 [^23] [^46] [^51]。CPU、内存和磁盘已经变得更大、更快、更可靠。当与 DuckDB、SQLite 和 KùzuDB 等单节点数据库结合使用时,许多工作负载现在可以在单个节点上运行。我们将在[第 4 章](/ch4#ch_storage)中进一步探讨这个主题。
|
||||
|
||||
### 微服务与 Serverless {#sec_introduction_microservices}
|
||||
|
||||
将系统分布在多台机器上的最常见方法是将它们分为客户端和服务器,并让客户端向服务器发出请求。最常见的是,HTTP 用于此通信,正如我们将在["通过服务的数据流:REST 和 RPC"](/ch5#sec_encoding_dataflow_rpc)中讨论的。同一进程可能既是服务器(处理传入请求)又是客户端(向其他服务发出出站请求)。
|
||||
在多台机器上分布系统的最常见方式是将它们分为客户端和服务器,并让客户端向服务器发出请求。最常见的是使用 HTTP 进行此通信,正如我们将在["通过服务的数据流:REST 和 RPC"](/ch5#sec_encoding_dataflow_rpc)中讨论的。同一进程可能既是服务器(处理传入请求)又是客户端(向其他服务发出出站请求)。
|
||||
|
||||
这种构建应用程序的方式传统上被称为 **面向服务的架构**(SOA);最近,这个想法已经被细化为 **微服务** 架构 [^52] [^53]。
|
||||
在这种架构中,服务有一个明确定义的目的(例如,在 S3 的情况下,这将是文件存储);每个服务公开一个可以由客户端通过网络调用的 API,每个服务有一个负责其维护的团队。因此,复杂的应用程序可以分解为多个交互服务,每个服务由单独的团队管理。
|
||||
这种构建应用程序的方式传统上被称为 **面向服务架构**(SOA);最近,这个想法已经被细化为 **微服务** 架构 [^52] [^53]。在这种架构中,服务有一个明确定义的目的(例如,在 S3 的情况下,这将是文件存储);每个服务公开一个可以由客户端通过网络调用的 API,每个服务有一个负责其维护的团队。因此,复杂的应用程序可以分解为多个交互服务,每个服务由单独的团队管理。
|
||||
|
||||
将复杂的软件分解为多个服务有几个优点:每个服务可以独立更新,减少团队之间的协调工作;每个服务可以分配它需要的硬件资源;通过将实现细节隐藏在 API 后面,服务所有者可以自由更改实现而不影响客户端。在数据存储方面,每个服务拥有自己的数据库并且不在服务之间共享数据库是很常见的:共享数据库将有效地使整个数据库结构成为服务 API 的一部分,然后该结构将难以更改。共享数据库还可能导致一个服务的查询对其他服务的性能产生负面影响。
|
||||
将复杂的软件分解为多个服务有几个优点:每个服务可以独立更新,减少团队之间的协调工作;每个服务可以分配它需要的硬件资源;通过将实现细节隐藏在 API 后面,服务所有者可以自由地更改实现而不影响客户端。在数据存储方面,每个服务通常有自己的数据库,而不在服务之间共享数据库:共享数据库实际上会使整个数据库结构成为服务 API 的一部分,然后该结构将很难更改。共享数据库还可能导致一个服务的查询对其他服务的性能产生负面影响。
|
||||
|
||||
另一方面,拥有许多服务本身可能会滋生复杂性:每个服务都需要基础设施来部署新版本、调整分配的硬件资源以匹配负载、收集日志、监控服务健康状况,并在出现问题时向值班工程师发出警报。**编排** 框架(如 Kubernetes)已成为部署服务的流行方式,因为它们为这种基础设施提供了基础。在开发期间测试服务可能会很复杂,因为你还需要运行它所依赖的所有其他服务。
|
||||
另一方面,拥有许多服务本身可能会带来复杂性:每个服务都需要用于部署新版本、调整分配的硬件资源以匹配负载、收集日志、监控服务健康状况以及在出现问题时向值班工程师发出警报的基础设施。**编排** 框架(如 Kubernetes)已成为部署服务的流行方式,因为它们为这种基础设施提供了基础。在开发期间测试服务可能很复杂,因为你还需要运行它所依赖的所有其他服务。
|
||||
|
||||
微服务 API 的演进可能具有挑战性。调用 API 的客户端期望 API 具有某些字段。开发人员可能希望随着业务需求的变化向 API 添加或删除字段,但这样做可能会导致客户端失败。更糟糕的是,这种失败通常直到开发周期的后期才被发现,当更新的服务 API 部署到暂存或生产环境时。API 描述标准(如 OpenAPI 和 gRPC)有助于管理客户端和服务器 API 之间的关系;我们将在[第 5 章](/ch5#ch_encoding)中进一步讨论这些。
|
||||
微服务 API 的演进可能具有挑战性。调用 API 的客户端期望 API 具有某些字段。开发人员可能希望根据业务需求的变化向 API 添加或删除字段,但这样做可能会导致客户端失败。更糟糕的是,这种失败通常直到开发周期的后期才被发现,当更新的服务 API 部署到暂存或生产环境时。API 描述标准(如 OpenAPI 和 gRPC)有助于管理客户端和服务器 API 之间的关系;我们将在[第 5 章](/ch5#ch_encoding)中进一步讨论这些。
|
||||
|
||||
微服务主要是人员问题的技术解决方案:允许不同的团队独立取得进展,而无需相互协调。这在大公司中很有价值,但在没有很多团队的小公司中,使用微服务可能是不必要的开销,最好以最简单的方式实现应用程序 [^52]。
|
||||
|
||||
**无服务器** 或 **函数即服务**(FaaS)是部署服务的另一种方法,其中基础设施的管理外包给云供应商 [^33]。
|
||||
使用虚拟机时,你必须明确选择何时启动或关闭实例;相比之下,使用无服务器模型,云提供商根据对你的服务的传入请求自动分配和释放硬件资源 [^54]。
|
||||
无服务器部署将更多的运营负担转移到云提供商,并通过使用而不是机器实例实现灵活计费。为了提供这些好处,许多无服务器基础设施提供商对函数执行施加时间限制、限制运行时环境,并且在首次调用函数时可能会遭受缓慢的启动时间。术语"无服务器"也可能具有误导性:每个无服务器函数执行仍在服务器上运行,但后续执行可能在不同的服务器上运行。此外,BigQuery 和各种 Kafka 产品等基础设施已采用"无服务器"术语来表示其服务自动扩展,并且它们按使用而不是机器实例计费。
|
||||
**Serverless** 或 **函数即服务**(FaaS)是部署服务的另一种方法,其中基础设施的管理外包给云供应商 [^33]。使用虚拟机时,你必须明确选择何时启动或关闭实例;相比之下,使用 serverless 模型,云提供商根据对你服务的传入请求自动分配和释放硬件资源 [^54]。Serverless 部署将更多的运营负担转移到云提供商,并通过使用量而不是机器实例实现灵活的计费。为了提供这些好处,许多 serverless 基础设施提供商对函数执行施加时间限制,限制运行时环境,并且在首次调用函数时可能会遭受缓慢的启动时间。术语"serverless"也可能具有误导性:每个 serverless 函数执行仍然在服务器上运行,但后续执行可能在不同的服务器上运行。此外,BigQuery 和各种 Kafka 产品等基础设施已经采用"serverless"术语来表示他们的服务自动扩展,并且他们按使用量而不是机器实例计费。
|
||||
|
||||
就像云存储用计量计费模型取代了容量规划(提前决定购买多少磁盘)一样,无服务器方法正在为代码执行带来计量计费:你只需为应用程序代码实际运行的时间付费,而不必提前配置资源。
|
||||
就像云存储用计量计费模型取代了容量规划(提前决定购买多少磁盘)一样,serverless 方法正在为代码执行带来计量计费:你只为应用程序代码实际运行的时间付费,而不必提前配置资源。
|
||||
|
||||
### 云计算与超级计算 {#id17}
|
||||
|
||||
云计算不是构建大规模计算系统的唯一方式;另一种选择是 **高性能计算**(HPC),也称为 **超级计算**。虽然有重叠,但与云计算和企业数据中心系统相比,HPC 通常有不同的优先级并使用不同的技术。其中一些差异是:
|
||||
云计算不是构建大规模计算系统的唯一方式;另一种选择是 **高性能计算**(HPC),也称为 **超级计算**。尽管有重叠,但与云计算和企业数据中心系统相比,HPC 通常有不同的优先级并使用不同的技术。其中一些差异是:
|
||||
|
||||
* 超级计算机通常用于计算密集型科学计算任务,如天气预报、气候建模、分子动力学(模拟原子和分子的运动)、复杂优化问题和求解偏微分方程。另一方面,云计算往往用于在线服务、业务数据系统以及需要以高可用性为用户请求提供服务的类似系统。
|
||||
* 超级计算机通常运行大型批处理作业,这些作业会定期将其计算状态检查点到磁盘。如果节点发生故障,常见的解决方案是简单地停止整个集群工作负载,修复故障节点,然后从最后一个检查点重新启动计算 [^55] [^56]。
|
||||
对于云服务,通常不希望停止整个集群,因为服务需要以最小的中断持续为用户提供服务。
|
||||
* 超级计算机节点通常通过共享内存和远程直接内存访问(RDMA)进行通信,这支持高带宽和低延迟,但假设系统用户之间具有高度信任 [^57]。
|
||||
在云计算中,网络和机器通常由相互不信任的组织共享,需要更强的安全机制,如资源隔离(例如,虚拟机)、加密和身份验证。
|
||||
* 云数据中心网络通常基于 IP 和以太网,以 Clos 拓扑排列以提供高二分带宽——网络整体性能的常用度量 [^55] [^58]。
|
||||
超级计算机通常使用专门的网络拓扑,如多维网格和环面 [^59],
|
||||
这些拓扑为具有已知通信模式的 HPC 工作负载产生更好的性能。
|
||||
* 云计算允许节点分布在多个地理区域,而超级计算机通常假设其所有节点都紧密相邻。
|
||||
* 超级计算机通常用于计算密集型科学计算任务,例如天气预报、气候建模、分子动力学(模拟原子和分子的运动)、复杂优化问题和求解偏微分方程。另一方面,云计算往往用于在线服务、业务数据系统和需要以高可用性为用户请求提供服务的类似系统。
|
||||
* 超级计算机通常运行大型批处理作业,定期将其计算状态检查点到磁盘。如果节点发生故障,常见的解决方案是简单地停止整个集群工作负载,修复故障节点,然后从最后一个检查点重新启动计算 [^55] [^56]。对于云服务,通常不希望停止整个集群,因为服务需要以最小的中断持续为用户提供服务。
|
||||
* 超级计算机节点通常通过共享内存和远程直接内存访问(RDMA)进行通信,这支持高带宽和低延迟,但假设系统用户之间有高度的信任 [^57]。在云计算中,网络和机器通常由相互不信任的组织共享,需要更强的安全机制,如资源隔离(例如,虚拟机)、加密和身份验证。
|
||||
* 云数据中心网络通常基于 IP 和以太网,以 Clos 拓扑排列以提供高对分带宽——这是网络整体性能的常用度量 [^55] [^58]。超级计算机通常使用专门的网络拓扑,例如多维网格和环面 [^59],这为具有已知通信模式的 HPC 工作负载产生更好的性能。
|
||||
* 云计算允许节点分布在多个地理区域,而超级计算机通常假设它们的所有节点都靠近在一起。
|
||||
|
||||
大规模分析系统有时与超级计算共享一些特征,如果你在这个领域工作,了解这些技术可能是值得的。然而,本书主要关注需要持续可用的服务,如["可靠性和容错"](/ch2#sec_introduction_reliability)中所讨论的。
|
||||
大规模分析系统有时与超级计算共享一些特征,如果你在这个领域工作,了解这些技术可能是值得的。然而,本书主要关注需要持续可用的服务,如["可靠性与容错"](/ch2#sec_introduction_reliability)中所讨论的。
|
||||
|
||||
## 数据系统、法律与社会 {#sec_introduction_compliance}
|
||||
|
||||
到目前为止,你已经在本章中看到,数据系统的架构不仅受技术目标和要求的影响,还受它们支持的组织的人类需求的影响。越来越多的数据系统工程师意识到,仅为自己的业务需求服务是不够的:我们还对整个社会负有责任。
|
||||
到目前为止,你已经在本章中看到,数据系统的架构不仅受到技术目标和要求的影响,还受到它们所支持的组织的人力需求的影响。越来越多的数据系统工程师认识到,仅服务于自己企业的需求是不够的:我们还对整个社会负有责任。
|
||||
|
||||
一个特别关注的问题是存储关于人及其行为的数据的系统。自 2018 年以来,**通用数据保护条例**(GDPR)赋予许多欧洲国家的居民对其个人数据的更大控制和法律权利,类似的隐私法规已在世界各地的各个国家和州采用,例如加州消费者隐私法案(CCPA)。关于 AI 的法规,如 **欧盟 AI 法案**,对个人数据的使用方式施加了进一步的限制。
|
||||
一个特别的关注点是存储有关人员及其行为数据的系统。自 2018 年以来,**通用数据保护条例**(GDPR)赋予了许多欧洲国家居民对其个人数据更大的控制权和法律权利,类似的隐私法规已在世界各地的各个国家和州采用,包括例如加州消费者隐私法(CCPA)。关于 AI 的法规,例如 **欧盟 AI 法案**,对个人数据的使用方式施加了进一步的限制。
|
||||
|
||||
此外,即使在不直接受监管的领域,人们也越来越认识到计算机系统对人和社会的影响。社交媒体改变了个人消费新闻的方式,这影响了他们的政治观点,因此可能影响选举结果。自动化系统越来越多地做出对个人产生深远影响的决定,例如决定谁应该获得贷款或保险、谁应该被邀请参加工作面试,或者谁应该被怀疑犯罪 [^60]。
|
||||
此外,即使在不直接受法规约束的领域,人们也越来越认识到计算机系统对人和社会的影响。社交媒体改变了个人消费新闻的方式,这影响了他们的政治观点,因此可能影响选举结果。自动化系统越来越多地做出对个人产生深远影响的决策,例如决定谁应该获得贷款或保险覆盖,谁应该被邀请参加工作面试,或者谁应该被怀疑犯罪 [^60]。
|
||||
|
||||
从事此类系统工作的每个人都有责任考虑道德影响并确保它们符合相关法律。没有必要让每个人都成为法律和道德专家,但对法律和道德原则的基本认识与对分布式系统的一些基础知识一样重要。
|
||||
每个从事此类系统工作的人都有责任考虑道德影响并确保他们遵守相关法律。没有必要让每个人都成为法律和道德专家,但对法律和道德原则的基本认识与分布式系统中的一些基础知识同样重要。
|
||||
|
||||
法律考虑正在影响数据系统设计的基础 [^61]。
|
||||
例如,GDPR 授予个人根据要求删除其数据的权利(有时称为 **被遗忘权**)。然而,正如我们将在本书中看到的,许多数据系统依赖于不可变构造(如仅追加日志)作为其设计的一部分;我们如何确保删除应该是不可变的文件中间的某些数据?我们如何处理已纳入派生数据集(参见["权威记录系统与派生数据"](/ch1#sec_introduction_derived))的数据的删除,例如机器学习模型的训练数据?回答这些问题会带来新的工程挑战。
|
||||
法律考虑正在影响数据系统设计的基础 [^61]。例如,GDPR 授予个人在请求时删除其数据的权利(有时称为 **被遗忘权**)。然而,正如我们将在本书中看到的,许多数据系统依赖不可变构造(如仅追加日志)作为其设计的一部分;我们如何确保删除应该不可变的文件中间的某些数据?我们如何处理已被纳入衍生数据集(参见["权威数据源与派生数据"](/ch1#sec_introduction_derived))的数据删除,例如机器学习模型的训练数据?回答这些问题会带来新的工程挑战。
|
||||
|
||||
目前,我们没有关于哪些特定技术或系统架构应被视为"符合 GDPR"的明确指导方针。该法规故意不强制要求特定技术,因为随着技术的进步,这些技术可能会迅速变化。相反,法律文本阐述了需要解释的高级原则。这意味着如何遵守隐私法规的问题没有简单的答案,但我们将通过这个视角来看待本书中的一些技术。
|
||||
目前,我们对于哪些特定技术或系统架构应被视为"符合 GDPR"没有明确的指导方针。法规故意不强制要求特定技术,因为随着技术的进步,这些技术可能会迅速变化。相反,法律文本规定了需要解释的高级原则。这意味着如何遵守隐私法规的问题没有简单的答案,但我们将通过这个视角来看待本书中的一些技术。
|
||||
|
||||
一般来说,我们存储数据是因为我们认为其价值大于存储它的成本。然而,值得记住的是,存储成本不仅仅是你为 Amazon S3 或其他服务支付的账单:成本效益计算还应考虑到如果数据被泄露或被对手破坏的责任和声誉损害风险,以及如果数据的存储和处理被发现不符合法律的法律成本和罚款风险 [^51]。
|
||||
一般来说,我们存储数据是因为我们认为其价值大于存储它的成本。然而,值得记住的是,存储成本不仅仅是你为 Amazon S3 或其他服务支付的账单:成本效益计算还应该考虑到如果数据被泄露或被对手入侵的责任和声誉损害风险,以及如果数据的存储和处理被发现不符合法律的法律成本和罚款风险 [^51]。
|
||||
|
||||
政府或警察部队也可能迫使公司交出数据。当存在数据可能揭示犯罪行为的风险时(例如,在几个中东和非洲国家的同性恋,或在几个美国州寻求堕胎),存储该数据会给用户带来真正的安全风险。例如,前往堕胎诊所的旅行很容易通过位置数据揭示,甚至可能通过用户 IP 地址随时间的日志(表示大致位置)。
|
||||
政府或警察部队也可能迫使公司交出数据。当存在数据可能暴露犯罪行为的风险时(例如,在几个中东和非洲国家的同性恋,或在几个美国州寻求堕胎),存储该数据会为用户创造真正的安全风险。例如,去堕胎诊所的旅行很容易被位置数据揭示,甚至可能通过用户 IP 地址随时间的日志(表示大致位置)。
|
||||
|
||||
一旦考虑到所有风险,可能合理地决定某些数据根本不值得存储,因此应该删除。这种 **数据最小化** 原则(有时用德语术语 **Datensparsamkeit** 称呼)与"大数据"理念相反,后者是推测性地存储大量数据,以防将来有用 [^62]。
|
||||
但它符合 GDPR,该法规要求个人数据只能为指定的明确目的收集,该数据以后不得用于任何其他目的,并且数据不得保留超过收集目的所需的时间 [^63]。
|
||||
一旦考虑到所有风险,可能合理地决定某些数据根本不值得存储,因此应该删除。这个 **数据最小化** 原则(有时以德语术语 **Datensparsamkeit** 为人所知)与"大数据"哲学相反,后者是投机性地存储大量数据,以防将来有用 [^62]。但它符合 GDPR,该法规要求个人数据只能为指定的、明确的目的收集,这些数据以后不得用于任何其他目的,并且数据不得保留超过收集目的所需的时间 [^63]。
|
||||
|
||||
企业也注意到了隐私和安全问题。信用卡公司要求支付处理企业遵守严格的支付卡行业(PCI)标准。处理器经常接受独立审计师的评估,以验证持续合规性。软件供应商也受到了更多的审查。许多买家现在要求他们的供应商遵守服务组织控制(SOC)第 2 类标准。与 PCI 合规性一样,供应商接受第三方审计以验证遵守情况。
|
||||
企业也注意到了隐私和安全问题。信用卡公司要求支付处理企业遵守严格的支付卡行业(PCI)标准。处理商经常接受独立审计师的评估,以验证持续的合规性。软件供应商也受到了更多的审查。现在许多买家要求他们的供应商遵守服务组织控制(SOC)类型 2 标准。与 PCI 合规性一样,供应商接受第三方审计以验证遵守情况。
|
||||
|
||||
一般来说,重要的是要平衡你的业务需求与你正在收集和处理其数据的人的需求。这个话题还有很多内容;在[待补充链接]中,我们将更深入地探讨道德和法律合规主题,包括偏见和歧视问题。
|
||||
一般来说,重要的是平衡你的业务需求与你收集和处理其数据的人的需求。这个话题还有很多内容;在[待补充链接]中,我们将更深入地探讨道德和法律合规性的主题,包括偏见和歧视的问题。
|
||||
|
||||
## 总结 {#summary}
|
||||
|
||||
本章的主题是理解权衡:也就是说,认识到对于许多问题没有一个正确的答案,而是有几种不同的方法,每种方法都有各种利弊。我们探讨了影响数据系统架构的一些最重要的选择,并介绍了本书其余部分所需的术语。
|
||||
本章的主题是理解权衡:也就是说,认识到对于许多问题,没有一个正确的答案,而是有几种不同的方法,每种方法都有各种利弊。我们探讨了影响数据系统架构的一些最重要的选择,并介绍了本书其余部分所需的术语。
|
||||
|
||||
我们首先区分了事务型(事务处理,OLTP)和分析型(OLAP)系统,并看到了它们的不同特征:不仅管理具有不同访问模式的不同类型的数据,而且还服务于不同的受众。我们遇到了数据仓库和数据湖的概念,它们通过 ETL 从事务型系统接收数据馈送。在[第 4 章](/ch4#ch_storage)中,我们将看到事务型和分析型系统由于需要服务的不同类型的查询,通常使用非常不同的内部数据布局。
|
||||
我们首先区分了事务型(事务处理,OLTP)和分析型(OLAP)系统,并看到了它们的不同特征:不仅管理不同类型的数据和不同的访问模式,而且服务于不同的受众。我们遇到了数据仓库和数据湖的概念,它们通过 ETL 从事务型系统接收数据馈送。在[第 4 章](/ch4#ch_storage)中,我们将看到事务型和分析型系统通常使用非常不同的内部数据布局,因为它们需要服务的查询类型不同。
|
||||
|
||||
然后,我们将云服务(一个相对较新的发展)与传统的自托管软件范式进行了比较,后者以前主导着数据系统架构。这些方法中哪一个更具成本效益在很大程度上取决于你的特定情况,但不可否认的是,云原生方法正在为数据系统的架构带来重大变化,例如它们分离存储和计算的方式。
|
||||
然后,我们将云服务(一个相对较新的发展)与传统的自托管软件范式进行了比较,后者以前主导了数据系统架构。这些方法中哪一种更具成本效益在很大程度上取决于你的特定情况,但不可否认的是,云原生方法正在为数据系统的架构带来重大变化,例如它们分离存储和计算的方式。
|
||||
|
||||
云系统本质上是分布式的,我们简要地研究了分布式系统与使用单台机器相比的一些权衡。在某些情况下,你无法避免分布式,但如果可能将其保留在单台机器上,建议不要急于使系统分布式。在[第 9 章](/ch9#ch_distributed)中,我们将更详细地介绍分布式系统的挑战。
|
||||
云系统本质上是分布式的,我们简要地研究了分布式系统与使用单台机器相比的一些权衡。有些情况下你无法避免分布式,但如果可能在单台机器上保持系统,建议不要急于使系统分布式。在[第 9 章](/ch9#ch_distributed)中,我们将更详细地介绍分布式系统的挑战。
|
||||
|
||||
最后,我们看到数据系统架构不仅由部署系统的业务需求决定,还由保护其数据被处理的人的权利的隐私法规决定——这是许多工程师倾向于忽视的一个方面。我们如何将法律要求转化为技术实现还不太清楚,但在我们浏览本书的其余部分时,记住这个问题很重要。
|
||||
最后,我们看到数据系统架构不仅由部署系统的企业的需求决定,还由保护数据被处理的人的权利的隐私法规决定——这是许多工程师容易忽视的一个方面。我们如何将法律要求转化为技术实现还不太了解,但在我们阅读本书的其余部分时,记住这个问题很重要。
|
||||
|
||||
### 参考
|
||||
|
||||
@ -492,3 +473,4 @@ CPU、内存和磁盘已经变得更大、更快、更可靠。与单节点数
|
||||
[^61]: Supreeth Shastri, Vinay Banakar, Melissa Wasserman, Arun Kumar, and Vijay Chidambaram. [Understanding and Benchmarking the Impact of GDPR on Database Systems](https://www.vldb.org/pvldb/vol13/p1064-shastri.pdf). *Proceedings of the VLDB Endowment*, volume 13, issue 7, pages 1064–1077, March 2020. [doi:10.14778/3384345.3384354](https://doi.org/10.14778/3384345.3384354)
|
||||
[^62]: Martin Fowler. [Datensparsamkeit](https://www.martinfowler.com/bliki/Datensparsamkeit.html). *martinfowler.com*, December 2013. Archived at [perma.cc/R9QX-CME6](https://perma.cc/R9QX-CME6)
|
||||
[^63]: [Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016 (General Data Protection Regulation)](https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32016R0679&from=EN). *Official Journal of the European Union* L 119/1, May 2016.
|
||||
|
||||
|
@ -1,43 +1,42 @@
|
||||
---
|
||||
title: "2. 定义非功能性需求"
|
||||
weight: 102
|
||||
math: true
|
||||
breadcrumbs: false
|
||||
---
|
||||
|
||||

|
||||
|
||||
> *互联网做得如此之好,以至于大多数人认为它是一种自然资源,就像太平洋一样,而不是人造的东西。上一次有这种规模的技术如此没有错误是什么时候?*
|
||||
> *互联网做得如此之好,以至于大多数人都把它想象成像太平洋一样的自然资源,而不是人造的东西。上一次出现这种规模且无差错的技术是什么时候?*
|
||||
>
|
||||
> [Alan Kay](https://www.drdobbs.com/architecture-and-design/interview-with-alan-kay/240003442),
|
||||
> 在接受 *Dr Dobb's Journal* 采访时(2012)
|
||||
> [艾伦・凯](https://www.drdobbs.com/architecture-and-design/interview-with-alan-kay/240003442),
|
||||
> 在接受 *Dr Dobb's Journal* 采访时(2012 年)
|
||||
|
||||
如果你正在构建一个应用程序,你将受到一系列需求的驱动。在你的列表顶部,最可能的是应用程序必须提供的功能:你需要什么屏幕和什么按钮,以及每个操作应该做什么以实现你的软件目的。这些是你的 **功能性需求**。
|
||||
如果你正在构建一个应用程序,你将会被一系列需求所驱动。在你的需求列表中,最重要的可能是应用程序必须提供的功能:需要哪些界面和按钮,以及每个操作应该做什么,以实现软件的目的。这些是你的 *功能性需求*。
|
||||
|
||||
此外,你可能还有一些 **非功能性需求**:例如,应用程序应该快速、可靠、安全、合规,并且易于维护。这些需求可能没有明确写下来,因为它们可能看起来有些显而易见,但它们和应用程序的功能一样重要:一个慢得难以忍受或不可靠的应用程序可能等同于不存在。
|
||||
此外,你可能还有一些 *非功能性需求*:例如,应用程序应该快速、可靠、安全、合规,并且易于维护。这些需求可能没有明确写下来,因为它们看起来有些显而易见,但它们与应用程序的功能同样重要:一个慢得让人无法忍受或不可靠的应用程序还不如不存在。
|
||||
|
||||
许多非功能性需求,如安全性,超出了本书的范围。但是有一些非功能性需求我们将考虑,本章将帮助你为自己的系统阐明它们:
|
||||
许多非功能性需求,比如安全性,超出了本书的范围。但我们将考虑一些非功能性需求,本章将帮助你为自己的系统阐明它们:
|
||||
|
||||
* 如何定义和衡量系统的 **性能**(参见 ["描述性能"](/ch2#sec_introduction_percentiles));
|
||||
* 服务 **可靠** 意味着什么——即使在出现问题时也能继续正确工作(参见 ["可靠性和容错"](/ch2#sec_introduction_reliability));
|
||||
* 通过在系统负载增长时有效添加计算能力的方法,使系统具有 **可扩展性**(参见 ["可扩展性"](/ch2#sec_introduction_scalability));以及
|
||||
* 使系统在长期内更容易维护(参见 ["可维护性"](/ch2#sec_introduction_maintainability))。
|
||||
* 如何定义和衡量系统的 *性能*(参见 ["描述性能"](/ch2#sec_introduction_percentiles));
|
||||
* 服务 *可靠* 意味着什么——即即使出现问题也能继续正确工作(参见 ["可靠性与容错"](/ch2#sec_introduction_reliability));
|
||||
* 通过在系统负载增长时添加计算能力的有效方法,使系统具有 *可伸缩性*(参见 ["可伸缩性"](/ch2#sec_introduction_scalability));以及
|
||||
* 使系统长期更易于维护(参见 ["可运维性"](/ch2#sec_introduction_maintainability))。
|
||||
|
||||
本章介绍的术语在后续章节中也很有用,当我们深入了解数据密集型系统的实现细节时。然而,抽象定义可能相当枯燥;为了让这些想法更具体,我们将以一个社交网络服务如何工作的案例研究开始本章,这将提供性能和可扩展性的实际示例。
|
||||
本章介绍的术语在后续章节中也很有用,当我们深入研究数据密集型系统的实现细节时。然而,抽象定义可能相当枯燥;为了使这些想法更具体,我们将从一个案例研究开始本章,研究社交网络服务可能如何工作,这将提供性能和可伸缩性的实际示例。
|
||||
|
||||
## 案例研究:社交网络首页时间线 {#sec_introduction_twitter}
|
||||
|
||||
想象一下,你被赋予实现一个 X(前 Twitter)风格的社交网络的任务,用户可以发布消息并关注其他用户。这将是对这种服务实际工作方式的巨大简化 [^1] [^2] [^3],但它将有助于说明大规模系统中出现的一些问题。
|
||||
想象一下,你被赋予了实现一个类似 X(前身为 Twitter)风格的社交网络的任务,用户可以发布消息并关注其他用户。这将是对这种服务实际工作方式的巨大简化 [^1] [^2] [^3],但它将有助于说明大规模系统中出现的一些问题。
|
||||
|
||||
假设用户每天发布 5 亿条消息,平均每秒 5,700 条帖子。偶尔,速率可能飙升至每秒 150,000 条帖子 [^4]。我们还假设平均用户关注 200 人,并有 200 个粉丝(尽管范围很广:大多数人只有少数粉丝,而少数名人如巴拉克·奥巴马有超过 1 亿粉丝)。
|
||||
假设用户每天发布 5 亿条帖子,或平均每秒 5,700 条帖子。偶尔,速率可能飙升至每秒 150,000 条帖子 [^4]。我们还假设平均每个用户关注 200 人并有 200 个粉丝(尽管范围非常广泛:大多数人只有少数粉丝,而少数名人如巴拉克・奥巴马有超过 1 亿粉丝)。
|
||||
|
||||
### 表示用户、帖子与关注关系 {#id20}
|
||||
|
||||
想象我们将所有数据保存在关系数据库中,如 [图 2-1](/ch2#fig_twitter_relational) 所示。我们有一个用户表,一个帖子表,和一个关注关系表。
|
||||
假设我们将所有数据保存在关系数据库中,如 [图 2-1](/ch2#fig_twitter_relational) 所示。我们有一个用户表、一个帖子表和一个关注关系表。
|
||||
|
||||
{{< figure src="/fig/ddia_0201.png" id="fig_twitter_relational" caption="图 2-1. 用户可以相互关注的社交网络的简单关系模式。" class="w-full my-4" >}}
|
||||
{{< figure src="/fig/ddia_0201.png" id="fig_twitter_relational" caption="图 2-1. 社交网络的简单关系模式,用户可以相互关注。" class="w-full my-4" >}}
|
||||
|
||||
假设我们的社交网络必须支持的主要读取操作是 **主页时间线**,它显示你关注的人最近的帖子(为简单起见,我们将忽略广告、来自你未关注的人的建议帖子和其他扩展)。我们可以编写以下 SQL 查询来获取特定用户的主页时间线:
|
||||
假设我们的社交网络必须支持的主要读取操作是 *首页时间线*,它显示你关注的人最近发布的帖子(为简单起见,我们将忽略广告、来自你未关注的人的推荐帖子和其他扩展)。我们可以编写以下 SQL 查询来获取特定用户的首页时间线:
|
||||
|
||||
```sql
|
||||
SELECT posts.*, users.* FROM posts
|
||||
@ -48,103 +47,103 @@ SELECT posts.*, users.* FROM posts
|
||||
LIMIT 1000
|
||||
```
|
||||
|
||||
要执行此查询,数据库将使用 `follows` 表查找 `current_user` 关注的所有人,查找这些用户的最近帖子,并按时间戳排序以获取任何被关注用户的最新 1,000 条帖子。
|
||||
要执行此查询,数据库将使用 `follows` 表找到 `current_user` 关注的所有人,查找这些用户最近的帖子,并按时间戳排序以获取被关注用户的最新 1,000 条帖子。
|
||||
|
||||
帖子应该是及时的,所以假设在某人发布帖子后,我们希望他们的粉丝能够在 5 秒内看到它。一种方法是用户的客户端在用户在线时每 5 秒重复上述查询(这称为 **轮询**)。如果我们假设有 1000 万用户同时在线并登录,那意味着每秒运行查询 200 万次。即使你增加轮询间隔,这也是很多的。
|
||||
帖子应该是及时的,所以假设在某人发布帖子后,我们希望他们的粉丝能够在 5 秒内看到它。一种方法是让用户的客户端每 5 秒重复上述查询(这称为 *轮询*)。如果我们假设有 1000 万用户同时在线登录,这意味着每秒运行 200 万次查询。即使增加轮询间隔,这也是很大的负载。
|
||||
|
||||
此外,上述查询相当昂贵:如果你关注 200 人,它需要获取这 200 人中每个人的最近帖子列表,并合并这些列表。每秒 200 万次时间线查询意味着数据库需要每秒从某个发送者查找最近的帖子 4 亿次——一个巨大的数字。这是平均情况。一些用户关注数万个帐户;对他们来说,这个查询执行起来非常昂贵,并且很难快速执行。
|
||||
此外,上述查询相当昂贵:如果你关注 200 人,它需要获取这 200 人中每个人的最近帖子列表,并合并这些列表。每秒 200 万次时间线查询意味着数据库需要每秒查找某个发送者的最近帖子 4 亿次——这是一个巨大的数字。这是平均情况。一些用户关注数万个账户;对他们来说,这个查询执行起来非常昂贵,而且很难快速完成。
|
||||
|
||||
### 时间线的物化与更新 {#sec_introduction_materializing}
|
||||
|
||||
我们如何做得更好?首先,服务器主动将新帖子推送给当前在线的任何粉丝,而不是轮询,会更好。其次,我们应该预先计算上述查询的结果,以便可以从缓存中提供用户对其主页时间线的请求。
|
||||
我们如何做得更好?首先,与其轮询,不如服务器主动向当前在线的任何粉丝推送新帖子。其次,我们应该预先计算上述查询的结果,以便可以从缓存中提供用户的首页时间线请求。
|
||||
|
||||
想象一下,对于每个用户,我们存储一个包含其主页时间线的数据结构,即他们关注的人的最近帖子。每次用户发布帖子时,我们查找他们的所有粉丝,并将该帖子插入每个粉丝的主页时间线——就像将消息投递到邮箱一样。现在当用户登录时,我们可以简单地给他们我们预先计算的主页时间线。此外,要接收有关其时间线上任何新帖子的通知,用户的客户端只需订阅添加到其主页时间线的帖子流。
|
||||
想象一下,我们为每个用户存储一个包含其首页时间线的数据结构,即他们关注的人的最近帖子。每次用户发布帖子时,我们查找他们的所有粉丝,并将该帖子插入到每个粉丝的首页时间线中——就像向邮箱投递消息一样。现在当用户登录时,我们可以简单地给他们这个预先计算的首页时间线。此外,要接收时间线上任何新帖子的通知,用户的客户端只需订阅添加到其首页时间线的帖子流。
|
||||
|
||||
这种方法的缺点是,每次用户发布帖子时,我们现在需要做更多的工作,因为主页时间线是需要更新的派生数据。该过程如 [图 2-2](/ch2#fig_twitter_timelines) 所示。当一个初始请求导致执行几个下游请求时,我们使用术语 **扇出** 来描述请求数量增加的因子。
|
||||
这种方法的缺点是,现在每次用户发布帖子时我们需要做更多的工作,因为首页时间线是需要更新的衍生数据。该过程如 [图 2-2](/ch2#fig_twitter_timelines) 所示。当一个初始请求导致几个下游请求被执行时,我们使用术语 *扇出* 来描述请求数量增加的因子。
|
||||
|
||||
{{< figure src="/fig/ddia_0202.png" id="fig_twitter_timelines" caption="图 2-2. 扇出:将新帖子投递给发布帖子的用户的每个粉丝。" class="w-full my-4" >}}
|
||||
{{< figure src="/fig/ddia_0202.png" id="fig_twitter_timelines" caption="图 2-2. 扇出:将新帖子传递给发布帖子的用户的每个粉丝。" class="w-full my-4" >}}
|
||||
|
||||
以每秒 5,700 条帖子的速率,如果平均帖子到达 200 个粉丝(即扇出因子为 200),我们将需要每秒执行超过 100 万次主页时间线写入。这很多,但与我们否则必须执行的每秒 4 亿次按发送者的帖子查找相比,这仍然是一个显著的节省。
|
||||
以每秒 5,700 条帖子的速率,如果平均帖子到达 200 个粉丝(即扇出因子为 200),我们将需要每秒执行超过 100 万次首页时间线写入。这很多,但与我们本来需要的每秒 4 亿次每个发送者的帖子查找相比,这仍然是一个显著的节省。
|
||||
|
||||
如果由于某些特殊事件导致帖子速率激增,我们不必立即进行时间线投递——我们可以将它们排队,并接受帖子暂时需要更长时间才能显示在粉丝的时间线中。即使在这种负载峰值期间,时间线仍然可以快速加载,因为我们只是从缓存中提供它们。
|
||||
如果由于某些特殊事件导致帖子速率激增,我们不必立即进行时间线交付——我们可以将它们排队,并接受帖子在粉丝的时间线中显示会暂时花费更长时间。即使在这种负载峰值期间,时间线仍然可以快速加载,因为我们只是从缓存中提供它们。
|
||||
|
||||
这种预先计算和更新查询结果的过程称为 **物化**,时间线缓存是 **物化视图** 的一个例子(我们将在 [Link to Come] 中进一步讨论这个概念)。物化视图加速了读取,但作为回报,我们必须在写入时做更多的工作。对于大多数用户来说,写入的成本是适度的,但社交网络还必须考虑一些极端情况:
|
||||
这种预先计算和更新查询结果的过程称为 *物化*,时间线缓存是 *物化视图* 的一个例子(我们将在 [后续章节] 中进一步讨论这个概念)。物化视图加速了读取,但作为回报,我们必须在写入时做更多的工作。对于大多数用户来说,写入成本是适度的,但社交网络还必须考虑一些极端情况:
|
||||
|
||||
* 如果用户关注了大量帐户,并且这些帐户发布了很多内容,那么该用户的物化时间线将有很高的写入率。然而,在这种情况下,用户实际上不太可能阅读其时间线中的所有帖子,因此可以简单地丢弃其时间线写入的一部分,只向用户显示他们关注的帐户的帖子样本 [^5]。
|
||||
* 当拥有大量粉丝的名人帐户发布帖子时,我们必须做大量工作将该帖子插入其数百万粉丝的主页时间线。在这种情况下,不能丢弃其中一些写入。解决此问题的一种方法是将名人帖子与其他人的帖子分开处理:我们可以通过单独存储名人帖子并在读取物化时间线时将它们合并,从而节省将它们添加到数百万时间线的工作。尽管有这些优化,在社交网络上处理名人可能需要大量基础设施 [^6]。
|
||||
* 如果用户关注非常多的账户,并且这些账户发布很多内容,该用户的物化时间线将有很高的写入率。然而,在这种情况下,用户实际上不太可能阅读其时间线中的所有帖子,因此可以简单地丢弃其时间线的一些写入,只向用户显示他们关注的账户的帖子样本 [^5]。
|
||||
* 当拥有大量粉丝的名人账户发布帖子时,我们必须做大量工作将该帖子插入到他们数百万粉丝的每个首页时间线中。在这种情况下,丢弃一些写入是不可接受的。解决这个问题的一种方法是将名人帖子与其他人的帖子分开处理:我们可以通过将名人帖子单独存储并在读取时与物化时间线合并,来节省将它们添加到数百万时间线的工作。尽管有这些优化,在社交网络上处理名人仍然需要大量基础设施 [^6]。
|
||||
|
||||
## 描述性能 {#sec_introduction_percentiles}
|
||||
|
||||
大多数关于软件性能的讨论考虑两种主要类型的指标:
|
||||
大多数关于软件性能的讨论都考虑两种主要的度量类型:
|
||||
|
||||
响应时间
|
||||
: 从用户发出请求到收到请求答案的经过时间。测量单位是秒(或毫秒,或微秒)。
|
||||
: 从用户发出请求到收到所请求答案的经过时间。测量单位是秒(或毫秒,或微秒)。
|
||||
|
||||
吞吐量
|
||||
: 系统正在处理的每秒请求数,或每秒数据量。对于给定的硬件资源分配,存在可以处理的 **最大吞吐量**。测量单位是"每秒的某物"。
|
||||
: 系统正在处理的每秒请求数,或每秒数据量。对于给定的硬件资源分配,存在可以处理的 *最大吞吐量*。测量单位是"每秒某物"。
|
||||
|
||||
在社交网络案例研究中,"每秒帖子数"和"每秒时间线写入数"是吞吐量指标,而"加载主页时间线所需的时间"或"帖子投递给粉丝所需的时间"是响应时间指标。
|
||||
在社交网络案例研究中,"每秒帖子数"和"每秒时间线写入数"是吞吐量指标,而"加载首页时间线所需的时间"或"帖子传递给粉丝的时间"是响应时间指标。
|
||||
|
||||
吞吐量和响应时间之间通常存在联系;[图 2-3](/ch2#fig_throughput) 勾画了在线服务的这种关系示例。当请求吞吐量低时,服务的响应时间低,但随着负载增加,响应时间增加。这是因为 **排队**:当请求到达高负载系统时,CPU 很可能已经在处理较早的请求,因此传入的请求需要等待较早的请求完成。随着吞吐量接近硬件可以处理的最大值,排队延迟急剧增加。
|
||||
吞吐量和响应时间之间通常存在联系;在线服务的这种关系示例如 [图 2-3](/ch2#fig_throughput) 所示。当请求吞吐量较低时,服务具有较低的响应时间,但随着负载增加,响应时间也会增加。这是因为 *排队*:当请求到达高负载系统时,CPU 很可能已经在处理先前的请求,因此传入请求需要等待先前请求完成。随着吞吐量接近硬件可以处理的最大值,排队延迟急剧增加。
|
||||
|
||||
{{< figure src="/fig/ddia_0203.png" id="fig_throughput" caption="图 2-3. 当服务的吞吐量接近其容量时,由于排队,响应时间急剧增加。" class="w-full my-4" >}}
|
||||
{{< figure src="/fig/ddia_0203.png" id="fig_throughput" caption="图 2-3. 随着服务的吞吐量接近其容量,由于排队,响应时间急剧增加。" class="w-full my-4" >}}
|
||||
|
||||
--------
|
||||
|
||||
> [!TIP] 当过载系统无法恢复时
|
||||
|
||||
如果系统接近过载,吞吐量被推到接近极限,它有时会进入恶性循环,变得效率更低,因此更加过载。例如,如果有一长串请求等待处理,响应时间可能会增加到客户端超时并重新发送其请求的程度。这导致请求率进一步增加,使问题变得更糟——**重试风暴**。即使负载再次降低,这样的系统也可能保持过载状态,直到重新启动或以其他方式重置。这种现象被称为 **亚稳态故障**,它可能导致生产系统中的严重中断 [^7] [^8]。
|
||||
如果系统接近过载,吞吐量被推到极限附近,它有时会进入恶性循环,变得效率更低,从而更加过载。例如,如果有很长的请求队列等待处理,响应时间可能会增加到客户端超时并重新发送请求的程度。这导致请求率进一步增加,使问题变得更糟——*重试风暴*。即使负载再次降低,这样的系统也可能保持过载状态,直到重新启动或以其他方式重置。这种现象称为 *亚稳态故障*,它可能导致生产系统的严重中断 [^7] [^8]。
|
||||
|
||||
为了避免重试使服务过载,你可以在客户端增加并随机化连续重试之间的时间(**指数退避** [^9] [^10]),并暂时停止向最近返回错误或超时的服务发送请求(使用 **熔断器** [^11] [^12] 或 **令牌桶** 算法 [^13])。服务器还可以检测何时接近过载并开始主动拒绝请求(**负载卸除** [^14]),并发送要求客户端减速的响应(**背压** [^1] [^15])。排队和负载均衡算法的选择也可能产生影响 [^16]。
|
||||
为了避免重试使服务过载,你可以在客户端增加并随机化连续重试之间的时间(*指数退避* [^9] [^10]),并暂时停止向最近返回错误或超时的服务发送请求(使用 *熔断器* [^11] [^12] 或 *令牌桶* 算法 [^13])。服务器还可以检测何时接近过载并开始主动拒绝请求(*负载卸除* [^14]),并发送响应要求客户端减速(*背压* [^1] [^15])。排队和负载均衡算法的选择也可能产生影响 [^16]。
|
||||
|
||||
--------
|
||||
|
||||
就性能指标而言,响应时间通常是用户最关心的,而吞吐量决定了所需的计算资源(例如,你需要多少服务器),因此决定了服务特定工作负载的成本。如果吞吐量可能增长超过当前硬件可以处理的范围,则需要扩展容量;如果通过添加计算资源可以显著增加其最大吞吐量,则称系统是 **可扩展的**。
|
||||
就性能指标而言,响应时间通常是用户最关心的,而吞吐量决定了所需的计算资源(例如,你需要多少服务器),因此决定了服务特定工作负载的成本。如果吞吐量可能会增长超出当前硬件可以处理的范围,则需要扩展容量;如果系统的最大吞吐量可以通过添加计算资源显著增加,则称系统为 *可伸缩的*。
|
||||
|
||||
在本节中,我们将主要关注响应时间,我们将在 ["可扩展性"](/ch2#sec_introduction_scalability) 中回到吞吐量和可扩展性。
|
||||
在本节中,我们将主要关注响应时间,我们将在 ["可伸缩性"](/ch2#sec_introduction_scalability) 中回到吞吐量和可伸缩性。
|
||||
|
||||
### 延迟与响应时间 {#id23}
|
||||
|
||||
"延迟"和"响应时间"有时可以互换使用,但在本书中,我们将以特定方式使用这些术语(如 [图 2-4](/ch2#fig_response_time) 所示):
|
||||
"延迟"和"响应时间"有时可互换使用,但在本书中我们将以特定方式使用这些术语(如 [图 2-4](/ch2#fig_response_time) 所示):
|
||||
|
||||
* **响应时间** 是客户端看到的;它包括系统中任何地方产生的所有延迟。
|
||||
* **服务时间** 是服务主动处理用户请求的持续时间。
|
||||
* **排队延迟** 可能发生在流程的几个点:例如,在收到请求后,它可能需要等到 CPU 可用才能处理;如果同一台机器上的其他任务通过出站网络接口发送大量数据,响应数据包可能需要在发送到网络之前进行缓冲。
|
||||
* **延迟** 是请求未被主动处理的时间的总称,即它是 **潜在的** 时间。特别是,**网络延迟** 或 **网络延迟** 是指请求和响应在网络中传输所花费的时间。
|
||||
* *响应时间* 是客户端看到的;它包括系统中任何地方产生的所有延迟。
|
||||
* *服务时间* 是服务主动处理用户请求的持续时间。
|
||||
* *排队延迟* 可能发生在流程中的几个点:例如,在收到请求后,它可能需要等待直到 CPU 可用才能被处理;如果同一台机器上的其他任务通过出站网络接口发送大量数据,响应数据包可能需要在发送之前进行缓冲。
|
||||
* *延迟* 是一个涵盖请求未被主动处理时间的总称,即在此期间它是 *潜在的*。特别是,*网络延迟* 或 *网络延迟* 指的是请求和响应在网络中传输所花费的时间。
|
||||
|
||||
{{< figure src="/fig/ddia_0204.png" id="fig_response_time" caption="图 2-4. 响应时间、服务时间、网络延迟和排队延迟。" class="w-full my-4" >}}
|
||||
|
||||
在 [图 2-4](/ch2#fig_response_time) 中,时间从左到右流动,每个通信节点显示为水平线,请求或响应消息显示为从一个节点到另一个节点的粗对角箭头。你将在本书的过程中经常遇到这种风格的图表。
|
||||
在 [图 2-4](/ch2#fig_response_time) 中,时间从左到右流动,每个通信节点显示为水平线,请求或响应消息显示为从一个节点到另一个节点的粗对角箭头。你将在本书中经常遇到这种风格的图表。
|
||||
|
||||
响应时间可能从一个请求到下一个请求显著变化,即使你一遍又一遍地发出相同的请求。许多因素可能会增加随机延迟:例如,上下文切换到后台进程、网络数据包丢失和 TCP 重传、垃圾回收暂停、强制从磁盘读取的页面错误、服务器机架中的机械振动 [^17],或许多其他原因。我们将在 ["超时和无界延迟"](/ch9#sec_distributed_queueing) 中更详细地讨论这个主题。
|
||||
响应时间可能会因请求而异,即使你一遍又一遍地发出相同的请求。许多因素可能会增加随机延迟:例如,上下文切换到后台进程、网络数据包丢失和 TCP 重传、垃圾回收暂停、强制从磁盘读取的缺页错误、服务器机架中的机械振动 [^17],或许多其他原因。我们将在 ["超时与无界延迟"](/ch9#sec_distributed_queueing) 中更详细地讨论这个主题。
|
||||
|
||||
排队延迟通常占响应时间变化的很大一部分。由于服务器只能并行处理少量事物(例如,受其 CPU 核心数量的限制),只需要少量慢速请求就可以阻止后续请求的处理——这种效应称为 **队头阻塞**。即使那些后续请求具有快速的服务时间,由于等待先前请求完成的时间,客户端也会看到缓慢的整体响应时间。排队延迟不是服务时间的一部分,因此在客户端测量响应时间很重要。
|
||||
排队延迟通常占响应时间变化的很大一部分。由于服务器只能并行处理少量事务(例如,受其 CPU 核心数的限制),只需要少量慢请求就可以阻塞后续请求的处理——这种效应称为 *队头阻塞*。即使那些后续请求的服务时间很快,由于等待先前请求完成的时间,客户端仍会看到缓慢的整体响应时间。排队延迟不是服务时间的一部分,因此在客户端测量响应时间很重要。
|
||||
|
||||
### 平均值、中位数与百分位数 {#id24}
|
||||
|
||||
因为响应时间从一个请求到下一个请求各不相同,我们需要将其视为不是单个数字,而是可以测量的值的 **分布**。在 [图 2-5](/ch2#fig_lognormal) 中,每个灰色条代表对服务的请求,其高度显示该请求花费了多长时间。大多数请求相当快,但偶尔有 **异常值** 需要更长时间。网络延迟的变化也称为 **抖动**。
|
||||
因为响应时间因请求而异,我们需要将其视为值的 *分布*,而不是单个数字。在 [图 2-5](/ch2#fig_lognormal) 中,每个灰色条表示对服务的请求,其高度显示该请求花费的时间。大多数请求相当快,但偶尔会有 *异常值* 需要更长时间。网络延迟的变化也称为 *抖动*。
|
||||
|
||||
{{< figure src="/fig/ddia_0205.png" id="fig_lognormal" caption="图 2-5. 说明平均值和百分位数:对服务的 100 个请求样本的响应时间。" class="w-full my-4" >}}
|
||||
{{< figure src="/fig/ddia_0205.png" id="fig_lognormal" caption="图 2-5. 说明平均值和百分位数:100 个服务请求的响应时间样本。" class="w-full my-4" >}}
|
||||
|
||||
报告服务的 **平均** 响应时间是很常见的(技术上,**算术平均值**:即,将所有响应时间相加,然后除以请求数)。平均响应时间对于估计吞吐量限制很有用 [^18]。然而,如果你想知道你的"典型"响应时间,平均值不是一个很好的指标,因为它不会告诉你有多少用户实际经历了那种延迟。
|
||||
报告服务的 *平均* 响应时间是常见的(技术上是 *算术平均值*:即,将所有响应时间相加,然后除以请求数)。平均响应时间对于估计吞吐量限制很有用 [^18]。然而,如果你想知道你的"典型"响应时间,平均值不是一个很好的指标,因为它不能告诉你有多少用户实际经历了那种延迟。
|
||||
|
||||
通常使用 **百分位数** 会更好。如果你将响应时间列表从最快到最慢排序,那么 **中位数** 是中间点:例如,如果你的中位数响应时间是 200 毫秒,这意味着你的一半请求在不到 200 毫秒内返回,一半请求需要更长时间。这使中位数成为一个很好的指标,如果你想知道用户通常需要等待多长时间。中位数也称为 **第 50 百分位数**,有时缩写为 **p50**。
|
||||
通常使用 *百分位数* 更好。如果你将响应时间列表从最快到最慢排序,那么 *中位数* 就在中间:例如,如果你的中位响应时间是 200 毫秒,这意味着一半的请求在不到 200 毫秒内返回,一半的请求花费的时间更长。这使得中位数成为了解用户通常需要等待多长时间的良好指标。中位数也称为 *第 50 百分位*,有时缩写为 *p50*。
|
||||
|
||||
为了弄清楚你的异常值有多糟糕,你可以查看更高的百分位数:**第 95**、**第 99** 和 **第 99.9** 百分位数是常见的(缩写为 **p95**、**p99** 和 **p999**)。它们是响应时间阈值,95%、99% 或 99.9% 的请求比该特定阈值快。例如,如果第 95 百分位响应时间是 1.5 秒,这意味着 100 个请求中有 95 个需要不到 1.5 秒,100 个请求中有 5 个需要 1.5 秒或更长时间。这在 [图 2-5](/ch2#fig_lognormal) 中有所说明。
|
||||
为了弄清异常值有多糟糕,你可以查看更高的百分位数:*第 95*、*99* 和 *99.9* 百分位数很常见(缩写为 *p95*、*p99* 和 *p999*)。它们是 95%、99% 或 99.9% 的请求比该特定阈值快的响应时间阈值。例如,如果第 95 百分位响应时间是 1.5 秒,这意味着 100 个请求中的 95 个花费不到 1.5 秒,100 个请求中的 5 个花费 1.5 秒或更长时间。这在 [图 2-5](/ch2#fig_lognormal) 中有所说明。
|
||||
|
||||
响应时间的高百分位数,也称为 **尾延迟**,很重要,因为它们直接影响用户对服务的体验。例如,亚马逊根据第 99.9 百分位数描述内部服务的响应时间要求,即使它只影响 1,000 个请求中的 1 个。这是因为请求最慢的客户通常是那些在其帐户上拥有最多数据的客户,因为他们进行了许多购买——也就是说,他们是最有价值的客户 [^19]。通过确保网站对他们来说速度快,保持这些客户满意很重要。
|
||||
响应时间的高百分位数,也称为 *尾部延迟*,很重要,因为它们直接影响用户的服务体验。例如,亚马逊在描述内部服务的响应时间要求时使用第 99.9 百分位,即使它只影响 1,000 个请求中的 1 个。这是因为请求最慢的客户通常是那些账户上数据最多的客户,因为他们进行了许多购买——也就是说,他们是最有价值的客户 [^19]。确保网站对他们来说速度快对于保持这些客户的满意度很重要。
|
||||
|
||||
另一方面,优化第 99.99 百分位数(10,000 个请求中最慢的 1 个)被认为对亚马逊的目的来说太昂贵且收益不足。在非常高的百分位数下减少响应时间很困难,因为它们容易受到你无法控制的随机事件的影响,而且收益递减。
|
||||
另一方面,优化第 99.99 百分位(10,000 个请求中最慢的 1 个)被认为太昂贵,对亚马逊的目的没有足够的好处。在非常高的百分位数上减少响应时间很困难,因为它们很容易受到你无法控制的随机事件的影响,而且收益递减。
|
||||
|
||||
--------
|
||||
|
||||
> [!TIP] 响应时间对用户的影响
|
||||
|
||||
直觉上似乎很明显,快速服务比慢速服务对用户更好 [^20]。然而,令人惊讶的是,很难获得可靠的数据来量化延迟对用户行为的影响。
|
||||
直觉上似乎很明显,快速服务比慢速服务对用户更好 [^20]。然而,要获得可靠的数据来量化延迟对用户行为的影响是令人惊讶地困难的。
|
||||
|
||||
一些经常被引用的统计数据是不可靠的。2006 年,谷歌报告说,搜索结果从 400 毫秒减慢到 900 毫秒与流量和收入下降 20% 相关 [^21]。然而,2009 年谷歌的另一项研究报告说,延迟增加 400 毫秒仅导致每天搜索减少 0.6% [^22],同年必应发现加载时间增加两秒使广告收入减少 4.3% [^23]。来自这些公司的较新数据似乎没有公开。
|
||||
一些经常被引用的统计数据是不可靠的。2006 年,谷歌报告说,搜索结果从 400 毫秒减慢到 900 毫秒与流量和收入下降 20% 相关 [^21]。然而,2009 年谷歌的另一项研究报告说,延迟增加 400 毫秒导致每天搜索减少仅 0.6% [^22],同年必应发现加载时间增加两秒将广告收入减少 4.3% [^23]。这些公司的较新数据似乎没有公开。
|
||||
|
||||
Akamai 最近的一项研究 [^24] 声称响应时间增加 100 毫秒使电子商务网站的转化率降低多达 7%;然而,仔细检查后,同一研究显示,非常 **快** 的页面加载时间也与较低的转化率相关!这个看似矛盾的结果可以解释为加载最快的页面通常是那些没有有用内容的页面(例如,404 错误页面)。然而,由于该研究没有努力将页面内容的影响与加载时间的影响分开,其结果可能没有意义。
|
||||
Akamai 最近的一项研究 [^24] 声称响应时间增加 100 毫秒将电子商务网站的转化率降低多达 7%;然而,仔细检查后,同一研究显示,非常 *快* 的页面加载时间也与较低的转化率相关!这个看似矛盾的结果是因为加载最快的页面通常是那些没有有用内容的页面(例如,404 错误页面)。然而,由于该研究没有努力将页面内容的影响与加载时间的影响分开,其结果可能没有意义。
|
||||
|
||||
雅虎的一项研究 [^25] 比较了快速加载与慢速加载搜索结果的点击率,控制了搜索结果的质量。它发现当快速和慢速响应之间的差异为 1.25 秒或更多时,快速搜索的点击次数增加 20-30%。
|
||||
|
||||
@ -152,271 +151,270 @@ Akamai 最近的一项研究 [^24] 声称响应时间增加 100 毫秒使电子
|
||||
|
||||
### 响应时间指标的应用 {#sec_introduction_slo_sla}
|
||||
|
||||
高百分位数在后端服务中尤其重要,这些服务在服务单个最终用户请求时被多次调用。即使你并行进行调用,最终用户请求仍然需要等待最慢的并行调用完成。只需要一个慢速调用就能使整个最终用户请求变慢,如 [图 2-6](/ch2#fig_tail_amplification) 所示。即使只有一小部分后端调用很慢,如果最终用户请求需要多个后端调用,获得慢速调用的机会也会增加,因此更高比例的最终用户请求最终会变慢(一种称为 **尾延迟放大** 的效应 [^26])。
|
||||
高百分位数在被多次调用作为服务单个最终用户请求的一部分的后端服务中尤其重要。即使你并行进行调用,最终用户请求仍然需要等待最慢的并行调用完成。只需要一个慢调用就可以使整个最终用户请求变慢,如 [图 2-6](/ch2#fig_tail_amplification) 所示。即使只有一小部分后端调用很慢,如果最终用户请求需要多个后端调用,获得慢调用的机会就会增加,因此更高比例的最终用户请求最终会变慢(这种效应称为 *尾部延迟放大* [^26])。
|
||||
|
||||
{{< figure src="/fig/ddia_0206.png" id="fig_tail_amplification" caption="图 2-6. 当需要几个后端调用来服务请求时,只需要一个慢速后端请求就能减慢整个最终用户请求。" class="w-full my-4" >}}
|
||||
{{< figure src="/fig/ddia_0206.png" id="fig_tail_amplification" caption="图 2-6. 当需要几个后端调用来服务请求时,只需要一个慢的后端请求就可以减慢整个最终用户请求。" class="w-full my-4" >}}
|
||||
|
||||
百分位数通常用于 **服务级别目标**(SLO)和 **服务级别协议**(SLA)中,作为定义服务预期性能和可用性的方式 [^27]。例如,SLO 可能为服务设定目标,使中位数响应时间小于 200 毫秒,第 99 百分位数低于 1 秒,并且至少 99.9% 的有效请求导致非错误响应。SLA 是一份合同,规定如果未满足 SLO 会发生什么(例如,客户可能有权获得退款)。至少这是基本思想;在实践中,为 SLO 和 SLA 定义良好的可用性指标并不简单 [^28] [^29]。
|
||||
百分位数通常用于 *服务级别目标*(SLO)和 *服务级别协议*(SLA),作为定义服务预期性能和可用性的方式 [^27]。例如,SLO 可能设定服务的中位响应时间小于 200 毫秒且第 99 百分位低于 1 秒的目标,以及至少 99.9% 的有效请求导致非错误响应的目标。SLA 是一份合同,规定如果不满足 SLO 会发生什么(例如,客户可能有权获得退款)。这至少是基本想法;实际上,为 SLO 和 SLA 定义良好的可用性指标并不简单 [^28] [^29]。
|
||||
|
||||
--------
|
||||
|
||||
> [!TIP] 计算百分位数
|
||||
|
||||
如果你想将响应时间百分位数添加到服务的监控仪表板中,你需要持续高效地计算它们。例如,你可能希望保留过去 10 分钟内请求响应时间的滚动窗口。每分钟,你计算该窗口中值的中位数和各种百分位数,并将这些指标绘制在图表上。
|
||||
如果你想将响应时间百分位数添加到服务的监控仪表板中,你需要持续有效地计算它们。例如,你可能希望保留过去 10 分钟内请求的响应时间的滚动窗口。每分钟,你计算该窗口中值的中位数和各种百分位数,并在图表上绘制这些指标。
|
||||
|
||||
最简单的实现是保留时间窗口内所有请求的响应时间列表,并每分钟对该列表进行排序。如果这对你来说效率太低,有一些算法可以以最小的 CPU 和内存成本计算百分位数的良好近似值。开源百分位数估计库包括 HdrHistogram、t-digest [^30] [^31]、OpenHistogram [^32] 和 DDSketch [^33]。
|
||||
最简单的实现是在时间窗口内保留所有请求的响应时间列表,并每分钟对该列表进行排序。如果这对你来说效率太低,有一些算法可以以最小的 CPU 和内存成本计算百分位数的良好近似值。开源百分位数估计库包括 HdrHistogram、t-digest [^30] [^31]、OpenHistogram [^32] 和 DDSketch [^33]。
|
||||
|
||||
请注意,对百分位数求平均值,例如,减少时间分辨率或组合来自多台机器的数据,在数学上是没有意义的——聚合响应时间数据的正确方法是添加直方图 [^34]。
|
||||
请注意,平均百分位数,例如,减少时间分辨率或组合来自多台机器的数据,在数学上是没有意义的——聚合响应时间数据的正确方法是添加直方图 [^34]。
|
||||
|
||||
--------
|
||||
|
||||
## 可靠性与容错 {#sec_introduction_reliability}
|
||||
|
||||
每个人都对什么是可靠或不可靠有直观的想法。对于软件,典型的期望包括:
|
||||
每个人都对某物是否可靠或不可靠有直观的想法。对于软件,典型的期望包括:
|
||||
|
||||
* 应用程序执行用户期望的功能。
|
||||
* 它可以容忍用户犯错误或以意外方式使用软件。
|
||||
* 它的性能对于所需的用例、预期的负载和数据量来说足够好。
|
||||
* 它可以容忍用户犯错误或以意想不到的方式使用软件。
|
||||
* 在预期的负载和数据量下,其性能足以满足所需的用例。
|
||||
* 系统防止任何未经授权的访问和滥用。
|
||||
|
||||
如果所有这些事情加在一起意味着"正确工作",那么我们可以将 **可靠性** 理解为大致意思是"即使出现问题也能继续正确工作"。为了更准确地说明出现问题,我们将区分 **故障** 和 **失效** [^35] [^36] [^37]:
|
||||
如果所有这些加在一起意味着"正确工作",那么我们可以将 *可靠性* 大致理解为"即使出现问题也能继续正确工作"。为了更准确地说明出现问题,我们将区分 *故障* 和 *失效* [^35] [^36] [^37]:
|
||||
|
||||
故障
|
||||
: 故障是系统的特定 **部分** 停止正确工作:例如,如果单个硬盘驱动器出现故障,或单台机器崩溃,或系统依赖的外部服务出现中断。
|
||||
: 故障是指系统的某个特定 *部分* 停止正确工作:例如,如果单个硬盘驱动器发生故障,或单台机器崩溃,或外部服务(系统所依赖的)发生中断。
|
||||
|
||||
失效
|
||||
: 失效是系统 **作为一个整体** 停止向用户提供所需的服务;换句话说,当它不满足服务级别目标(SLO)时。
|
||||
: 失效是指 *整个* 系统停止向用户提供所需的服务;换句话说,当它不满足服务级别目标(SLO)时。
|
||||
|
||||
故障和失效之间的区别可能令人困惑,因为它们是同一件事,只是在不同的级别。例如,如果硬盘驱动器停止工作,我们说硬盘驱动器已经失效:如果系统仅包含那一个硬盘驱动器,它已经停止提供所需的服务。然而,如果你谈论的系统包含许多硬盘驱动器,那么从更大系统的角度来看,单个硬盘驱动器的失效只是一个故障,更大的系统可能能够通过在另一个硬盘驱动器上拥有数据副本来容忍该故障。
|
||||
故障和失效之间的区别可能会令人困惑,因为它们在不同层面上是同一件事。例如,如果硬盘驱动器停止工作,我们说硬盘驱动器已失效:如果系统仅由该一个硬盘驱动器组成,它已停止提供所需的服务。然而,如果你正在谈论的系统包含许多硬盘驱动器,那么从更大系统的角度来看,单个硬盘驱动器的失效只是一个故障,并且更大的系统可能能够通过在另一个硬盘驱动器上拥有数据副本来容忍该故障。
|
||||
|
||||
### 容错 {#id27}
|
||||
|
||||
如果系统尽管发生某些故障仍继续向用户提供所需的服务,我们称该系统为 **容错的**。如果系统无法容忍某个部分出现故障,我们称该部分为 **单点故障**(SPOF),因为该部分的故障会升级导致整个系统的失效。
|
||||
如果系统在发生某些故障时仍继续向用户提供所需的服务,我们称系统为 *容错的*。如果系统不能容忍某个部分变得有故障,我们称该部分为 *单点故障*(SPOF),因为该部分的故障会升级导致整个系统的失效。
|
||||
|
||||
例如,在社交网络案例研究中,可能发生的故障是在扇出过程中,参与更新物化时间线的机器崩溃或变得不可用。为了使此过程容错,我们需要确保另一台机器可以接管此任务,而不会错过任何应该投递的帖子,也不会复制任何帖子。(这个想法被称为 **精确一次语义**,我们将在 [Link to Come] 中详细研究它。)
|
||||
例如,在社交网络案例研究中,可能发生的故障是在扇出过程中,参与更新物化时间线的机器崩溃或变得不可用。为了使这个过程容错,我们需要确保另一台机器可以接管这项任务,而不会错过任何应该交付的帖子,也不会复制任何帖子。(这个想法被称为 *精确一次语义*,我们将在 [后续章节] 中详细研究它。)
|
||||
|
||||
容错总是限于一定数量的某些类型的故障。例如,系统可能能够容忍最多两个硬盘驱动器同时故障,或最多三个节点中的一个崩溃。容忍任何数量的故障是没有意义的:如果所有节点都崩溃,就无能为力了。如果整个地球(及其上的所有服务器)被黑洞吞噬,容忍该故障将需要太空网络托管——祝你获得该预算项目的批准好运。
|
||||
容错总是限于某些类型的某些数量的故障。例如,系统可能能够容忍最多两个硬盘驱动器同时故障,或最多三个节点中的一个崩溃。如果所有节点都崩溃,没有什么可以做的,这没有意义容忍任何数量的故障。如果整个地球(及其上的所有服务器)被黑洞吞噬,容忍该故障将需要在太空中进行网络托管——祝你获得批准该预算项目的好运。
|
||||
|
||||
违反直觉的是,在这种容错系统中,通过故意触发故障来 **增加** 故障率可能是有意义的——例如,通过在没有警告的情况下随机杀死单个进程。这称为 **故障注入**。许多关键错误实际上是由于错误处理不当造成的 [^38];通过故意引发故障,你确保容错机制得到持续锻炼和测试,这可以增加你对故障在自然发生时将被正确处理的信心。**混沌工程** 是一门学科,旨在通过故意注入故障等实验来提高对容错机制的信心 [^39]。
|
||||
反直觉地,在这种容错系统中,通过故意触发故障来 *增加* 故障率是有意义的——例如,在没有警告的情况下随机杀死单个进程。这称为 *故障注入*。许多关键错误实际上是由于错误处理不当造成的 [^38];通过故意引发故障,你确保容错机制不断得到锻炼和测试,这可以增加你对故障自然发生时将被正确处理的信心。*混沌工程* 是一门旨在通过故意注入故障等实验来提高对容错机制的信心的学科 [^39]。
|
||||
|
||||
尽管我们通常更喜欢容忍故障而不是预防故障,但有些情况下预防比治疗更好(例如,因为不存在治疗方法)。例如,安全事项就是这种情况:如果攻击者已经入侵了系统并获得了对敏感数据的访问,该事件无法撤消。然而,本书主要处理可以治愈的故障类型,如以下各节所述。
|
||||
尽管我们通常更喜欢容忍故障而不是预防故障,但在预防比治疗更好的情况下(例如,因为不存在治疗方法)。安全问题就是这种情况:如果攻击者已经破坏了系统并获得了对敏感数据的访问,该事件无法撤消。然而,本书主要涉及可以治愈的故障类型,如以下部分所述。
|
||||
|
||||
### 硬件与软件故障 {#sec_introduction_hardware_faults}
|
||||
|
||||
当我们考虑系统故障的原因时,硬件故障很快就会浮现在脑海中:
|
||||
当我们想到系统失效的原因时,硬件故障很快就会浮现在脑海中:
|
||||
|
||||
* 大约 2-5% 的磁性硬盘驱动器每年故障 [^40] [^41];在拥有 10,000 个磁盘的存储集群中,我们因此应该预期平均每天有一个磁盘故障。最近的数据表明磁盘变得更可靠,但故障率仍然很高 [^42]。
|
||||
* 大约 0.5-1% 的固态驱动器(SSD)每年故障 [^43]。少量的位错误会自动纠正 [^44],但不可纠正的错误大约每年每个驱动器发生一次,即使在相当新的驱动器中(即,经历过很少磨损);这个错误率高于磁性硬盘驱动器 [^45]、[^46]。
|
||||
* 其他硬件组件,如电源、RAID 控制器和内存模块也会故障,尽管频率低于硬盘驱动器 [^47] [^48]。
|
||||
* 大约千分之一的机器有一个 CPU 核心偶尔会计算错误的结果,可能是由于制造缺陷 [^49] [^50] [^51]。在某些情况下,错误的计算会导致崩溃,但在其他情况下,它会导致程序简单地返回错误的结果。
|
||||
* RAM 中的数据也可能被损坏,要么是由于宇宙射线等随机事件,要么是由于永久性物理缺陷。即使使用带有纠错码(ECC)的内存,超过 1% 的机器在给定年份中遇到不可纠正的错误,这通常会导致机器崩溃和受影响的内存模块需要更换 [^52]。此外,某些病理内存访问模式可以高概率翻转位 [^53]。
|
||||
* 整个数据中心可能变得不可用(例如,由于停电或网络配置错误)甚至被永久破坏(例如,因火灾、洪水或地震 [^54])。太阳风暴在太阳喷射大量带电粒子时在长距离电线中感应大电流,可能会损坏电网和海底网络电缆 [^55]。尽管这种大规模故障很少见,但如果服务无法容忍数据中心的丢失,它们的影响可能是灾难性的 [^56]。
|
||||
* 大约 2-5% 的磁性硬盘驱动器每年发生故障 [^40] [^41];在拥有 10,000 个磁盘的存储集群中,我们因此应该期望平均每天有一个磁盘故障。最近的数据表明磁盘变得更可靠,但故障率仍然很显著 [^42]。
|
||||
* 大约 0.5-1% 的固态硬盘(SSD)每年发生故障 [^43]。少量位错误会自动纠正 [^44],但不可纠正的错误大约每年每个驱动器发生一次,即使在相当新的驱动器中(即,经历很少磨损);这个错误率高于磁性硬盘驱动器 [^45]、[^46]。
|
||||
* 其他硬件组件,如电源、RAID 控制器和内存模块也会发生故障,尽管频率低于硬盘驱动器 [^47] [^48]。
|
||||
* 大约千分之一的机器有一个 CPU 核心偶尔计算错误的结果,可能是由于制造缺陷 [^49] [^50] [^51]。在某些情况下,错误的计算会导致崩溃,但在其他情况下,它会导致程序简单地返回错误的结果。
|
||||
* RAM 中的数据也可能被损坏,要么是由于宇宙射线等随机事件,要么是由于永久性物理缺陷。即使使用纠错码(ECC)的内存,超过 1% 的机器在给定年份遇到不可纠正的错误,这通常会导致机器崩溃和受影响的内存模块需要更换 [^52]。此外,某些病理内存访问模式可以以高概率翻转位 [^53]。
|
||||
* 整个数据中心可能变得不可用(例如,由于停电或网络配置错误)甚至被永久摧毁(例如,由火灾、洪水或地震 [^54])。太阳风暴,当太阳喷射大量带电粒子时,会在长距离电线中感应出大电流,可能会损坏电网和海底网络电缆 [^55]。尽管这种大规模故障很少见,但如果服务不能容忍数据中心的丢失,它们的影响可能是灾难性的 [^56]。
|
||||
|
||||
这些事件足够罕见,以至于在处理小型系统时你通常不需要担心它们,只要你可以轻松更换有故障的硬件。然而,在大规模系统中,硬件故障发生得足够频繁,以至于它们成为正常系统操作的一部分。
|
||||
这些事件足够罕见,你在处理小型系统时通常不需要担心它们,只要你可以轻松更换变得有故障的硬件。然而,在大规模系统中,硬件故障发生得足够频繁,以至于它们成为正常系统运行的一部分。
|
||||
|
||||
#### 通过冗余容忍硬件故障 {#tolerating-hardware-faults-through-redundancy}
|
||||
|
||||
我们对不可靠硬件的第一反应通常是向单个硬件组件添加冗余,以降低系统的故障率。磁盘可以设置为 RAID 配置(将数据分布在同一台机器中的多个磁盘上,以便故障磁盘不会导致数据丢失),服务器可能有双电源和可热插拔的 CPU,数据中心可能有电池和柴油发电机作为备用电源。这种冗余通常可以使机器无中断运行多年。
|
||||
我们对不可靠硬件的第一反应通常是向各个硬件组件添加冗余,以降低系统的故障率。磁盘可以设置为 RAID 配置(将数据分布在同一台机器的多个磁盘上,以便故障磁盘不会导致数据丢失),服务器可能有双电源和可热插拔的 CPU,数据中心可能有电池和柴油发电机作为备用电源。这种冗余通常可以使机器不间断运行多年。
|
||||
|
||||
当组件故障是独立的时,冗余最有效,即一个故障的发生不会改变另一个故障发生的可能性。然而,经验表明,组件故障之间通常存在显著相关性 [^41] [^57] [^58];整个服务器机架或整个数据中心的不可用仍然比我们希望的更频繁。
|
||||
当组件故障独立时,冗余最有效,即一个故障的发生不会改变另一个故障发生的可能性。然而,经验表明,组件故障之间通常存在显著的相关性 [^41] [^57] [^58];整个服务器机架或整个数据中心的不可用仍然比我们希望的更频繁地发生。
|
||||
|
||||
硬件冗余增加了单台机器的正常运行时间;然而,如 ["分布式与单节点系统"](/ch1#sec_introduction_distributed) 中所讨论的,使用分布式系统有一些优势,例如能够容忍一个数据中心的完全中断。出于这个原因,云系统倾向于较少关注单个机器的可靠性,而是旨在通过在软件级别容忍故障节点来使服务高度可用。云提供商使用 **可用区** 来识别哪些资源在物理上位于同一位置;同一地方的资源比地理上分离的资源更有可能同时故障。
|
||||
硬件冗余增加了单台机器的正常运行时间;然而,如 ["分布式与单节点系统"](/ch1#sec_introduction_distributed) 中所讨论的,使用分布式系统有一些优势,例如能够容忍一个数据中心的完全中断。出于这个原因,云系统倾向于较少关注单个机器的可靠性,而是旨在通过在软件级别容忍故障节点来使服务高度可用。云提供商使用 *可用区* 来识别哪些资源在物理上位于同一位置;同一地方的资源比地理上分离的资源更可能同时发生故障。
|
||||
|
||||
我们在本书中讨论的容错技术旨在容忍整个机器、机架或可用区的丢失。它们通常通过允许一个数据中心的机器在另一个数据中心的机器故障或变得无法访问时接管来工作。我们将在 [第 6 章](/ch6#ch_replication)、[第 10 章](/ch10#ch_consistency) 以及本书的其他各个点讨论这种容错技术。
|
||||
我们在本书中讨论的容错技术旨在容忍整个机器、机架或可用区的丢失。它们通常通过允许一个数据中心的机器在另一个数据中心的机器发生故障或变得不可达时接管来工作。我们将在 [第 6 章](/ch6)、[第 10 章](/ch10) 以及本书的其他各个地方讨论这种容错技术。
|
||||
|
||||
可以容忍整个机器丢失的系统也具有操作优势:如果你需要重新启动机器(例如,应用操作系统安全补丁),单服务器系统需要计划停机时间,而多节点容错系统可以通过一次重新启动一个节点来打补丁,而不会影响用户的服务。这称为 **滚动升级**,我们将在 [第 5 章](/ch5#ch_encoding) 中进一步讨论它。
|
||||
能够容忍整个机器丢失的系统也具有运营优势:如果你需要重新启动机器(例如,应用操作系统安全补丁),单服务器系统需要计划停机时间,而多节点容错系统可以一次修补一个节点,而不影响用户的服务。这称为 *滚动升级*,我们将在 [第 5 章](/ch5) 中进一步讨论它。
|
||||
|
||||
#### 软件故障 {#software-faults}
|
||||
|
||||
尽管硬件故障可能是弱相关的,但它们仍然大多是独立的:例如,如果一个磁盘故障,同一台机器中的其他磁盘很可能在一段时间内仍然正常。另一方面,软件故障通常高度相关,因为许多节点运行相同的软件并因此具有相同的错误是很常见的 [^59] [^60]。这种故障更难预测,它们往往比不相关的硬件故障导致更多的系统故障 [^47]。例如:
|
||||
尽管硬件故障可能是弱相关的,但它们大多仍然是独立的:例如,如果一个磁盘发生故障,同一台机器中的其他磁盘很可能在一段时间内还能正常工作。另一方面,软件故障通常高度相关,因为许多节点运行相同的软件并因此具有相同的错误是常见的 [^59] [^60]。这种故障比不相关的硬件故障更难预料,并且它们往往导致比硬件故障更多的系统失效 [^47]。例如:
|
||||
|
||||
* 导致每个节点在特定情况下同时故障的软件错误。例如,2012 年 6 月 30 日,闰秒导致许多 Java 应用程序由于 Linux 内核中的错误而同时挂起,导致许多互联网服务瘫痪 [^61]。由于固件错误,某些型号的所有 SSD 在精确运行 32,768 小时(不到 4 年)后突然故障,使其上的数据无法恢复 [^62]。
|
||||
* 使用某些共享、有限资源的失控进程,例如 CPU 时间、内存、磁盘空间、网络带宽或线程 [^63]。例如,在处理大型请求时消耗过多内存的进程可能会被操作系统杀死。客户端库中的错误可能导致比预期更高的请求量 [^64]。
|
||||
* 系统依赖的服务变慢、无响应或开始返回损坏的响应。
|
||||
* 不同系统之间的交互导致在每个系统单独测试时不会发生的紧急行为 [^65]。
|
||||
* 级联故障,其中一个组件中的问题导致另一个组件过载并减速,这反过来又导致另一个组件瘫痪 [^66] [^67]。
|
||||
* 在特定情况下导致每个节点同时失效的软件错误。例如,2012 年 6 月 30 日,闰秒导致许多 Java 应用程序由于 Linux 内核中的错误而同时挂起 [^61]。由于固件错误,某些型号的所有 SSD 在精确运行 32,768 小时(不到 4 年)后突然失效,使其上的数据无法恢复 [^62]。
|
||||
* 使用某些共享、有限资源(如 CPU 时间、内存、磁盘空间、网络带宽或线程)的失控进程 [^63]。例如,处理大请求时消耗过多内存的进程可能会被操作系统杀死。客户端库中的错误可能导致比预期更高的请求量 [^64]。
|
||||
* 系统所依赖的服务变慢、无响应或开始返回损坏的响应。
|
||||
* 不同系统之间的交互导致在隔离测试每个系统时不会发生的紧急行为 [^65]。
|
||||
* 级联故障,其中一个组件中的问题导致另一个组件过载和减速,这反过来又导致另一个组件崩溃 [^66] [^67]。
|
||||
|
||||
导致这些软件故障的错误通常会潜伏很长时间,直到它们被一组不寻常的情况触发。在这些情况下,会发现软件对其环境做出了某种假设——虽然该假设通常是正确的,但由于某种原因它最终不再正确 [^68] [^69]。
|
||||
导致这些类型软件故障的错误通常会潜伏很长时间,直到它们被一组不寻常的环境触发。在这些情况下,软件对其环境做出了某种假设——虽然该假设通常是正确的,但它最终由于某种原因不再成立 [^68] [^69]。
|
||||
|
||||
软件中系统性故障的问题没有快速解决方案。许多小事情可以帮助:仔细考虑系统中的假设和交互;彻底测试;进程隔离;允许进程崩溃并重新启动;避免反馈循环,如重试风暴(参见 ["当过载系统无法恢复时"](/ch2#sidebar_metastable));测量、监控和分析生产中的系统行为。
|
||||
软件中的系统故障没有快速解决方案。许多小事情可以帮助:仔细考虑系统中的假设和交互;彻底测试;进程隔离;允许进程崩溃和重新启动;避免反馈循环,如重试风暴(参见 ["当过载系统无法恢复时"](/ch2#sidebar_metastable));测量、监控和分析生产中的系统行为。
|
||||
|
||||
### 人类与可靠性 {#id31}
|
||||
|
||||
人类设计和构建软件系统,保持系统运行的操作员也是人类。与机器不同,人类不只是遵循规则;他们的力量在于在完成工作时具有创造性和适应性。然而,这一特性也导致了不可预测性,有时尽管有最好的意图,但会犯错误,导致故障。例如,一项对大型互联网服务的研究发现,操作员的配置更改是中断的主要原因,而硬件故障(服务器或网络)仅在 10-25% 的中断中发挥作用 [^70]。
|
||||
人类设计和构建软件系统,保持系统运行的操作员也是人类。与机器不同,人类不只是遵循规则;他们的力量是创造性和适应性地完成工作。然而,这一特征也导致不可预测性,有时会导致失效的错误,尽管有最好的意图。例如,一项对大型互联网服务的研究发现,操作员的配置更改是中断的主要原因,而硬件故障(服务器或网络)仅在 10-25% 的中断中发挥作用 [^70]。
|
||||
|
||||
将这些问题标记为"人为错误"并希望通过更严格的程序和规则合规来更好地控制人类行为来解决它们是很诱人的。然而,为错误责怪人们是适得其反的。我们所说的"人为错误"实际上不是事件的原因,而是人们尽力完成工作的社会技术系统中问题的症状 [^71]。通常,复杂系统具有紧急行为,其中组件之间的意外交互也可能导致故障 [^72]。
|
||||
将这些问题标记为"人为错误"并希望通过更严格的程序和规则合规性来更好地控制人类行为来解决它们是很诱人的。然而,责怪人们的错误是适得其反的。我们所说的"人为错误"实际上不是事件的原因,而是人们在社会技术系统中尽力做好工作的问题的症状 [^71]。通常,复杂系统具有紧急行为,组件之间的意外交互也可能导致故障 [^72]。
|
||||
|
||||
各种技术措施可以帮助最小化人为错误的影响,包括彻底测试(手写测试和对大量随机输入的 **属性测试**)[^38]、用于快速恢复配置更改的回滚机制、新代码的逐步推出、详细而清晰的监控、用于诊断生产问题的可观察性工具(参见 ["分布式系统的问题"](/ch1#sec_introduction_dist_sys_problems)),以及设计良好的接口,鼓励"正确的事情"并阻止"错误的事情"。
|
||||
各种技术措施可以帮助最小化人为错误的影响,包括彻底测试(手写测试和对大量随机输入的 *属性测试*)[^38]、快速回滚配置更改的回滚机制、新代码的逐步推出、详细和清晰的监控、用于诊断生产问题的可观察性工具(参见 ["分布式系统的问题"](/ch1#sec_introduction_dist_sys_problems)),以及鼓励"正确的事情"并阻止"错误的事情"的精心设计的界面。
|
||||
|
||||
然而,这些事情需要时间和金钱的投资,在日常业务的实用现实中,组织通常优先考虑创收活动,而不是增加其对错误的弹性的措施。如果在更多功能和更多测试之间有选择,许多组织可以理解地选择功能。鉴于这种选择,当不可避免地发生可预防的错误时,责怪犯错误的人是没有意义的——问题是组织的优先事项。
|
||||
然而,这些事情需要时间和金钱的投资,在日常业务的务实现实中,组织通常优先考虑创收活动而不是增加其抵御错误的韧性的措施。如果在更多功能和更多测试之间有选择,许多组织可以理解地选择功能。鉴于这种选择,当可预防的错误不可避免地发生时,责怪犯错误的人是没有意义的——问题是组织的优先事项。
|
||||
|
||||
越来越多的组织正在采用 **无责备事后分析** 的文化:在事件发生后,鼓励参与的人员分享关于发生的事情的全部细节,而不必担心受到惩罚,因为这使组织中的其他人能够学习如何在未来防止类似的问题 [^73]。这个过程可能会发现需要改变业务优先级、需要投资被忽视的领域、需要改变相关人员的激励措施,或者需要引起管理层注意的其他系统性问题。
|
||||
越来越多的组织正在采用 *无责备事后分析* 的文化:事件发生后,鼓励相关人员充分分享发生的事情的细节,而不用担心惩罚,因为这允许组织中的其他人学习如何在未来防止类似的问题 [^73]。这个过程可能会发现需要改变业务优先级、需要投资于被忽视的领域、需要改变相关人员的激励措施,或者需要引起管理层注意的其他一些系统性问题。
|
||||
|
||||
作为一般原则,在调查事件时,你应该对简单的答案持怀疑态度。"鲍勃在部署该更改时应该更加小心"是没有成效的,但"我们必须用 Haskell 重写后端"也不是。相反,管理层应该借此机会从每天使用它的人的角度了解社会技术系统如何工作的细节,并根据这些反馈采取措施改进它 [^71]。
|
||||
作为一般原则,在调查事件时,你应该对简单化的答案持怀疑态度。"鲍勃在部署该更改时应该更加小心"是没有成效的,但"我们必须用 Haskell 重写后端"也不是。相反,管理层应该借此机会从每天与之合作的人的角度了解社会技术系统如何工作的细节,并根据这些反馈采取措施改进它 [^71]。
|
||||
|
||||
--------
|
||||
|
||||
> [!TIP] 可靠性有多重要?
|
||||
|
||||
可靠性不仅适用于核电站和空中交通管制——更平凡的应用程序也应该可靠地工作。业务应用程序中的错误会导致生产力损失(如果数字报告不正确,还会带来法律风险),电子商务网站的中断可能会在收入损失和声誉损害方面造成巨大成本。
|
||||
可靠性不仅仅适用于核电站和空中交通管制——更平凡的应用程序也应该可靠地工作。业务应用程序中的错误会导致生产力损失(如果数字报告不正确,还会有法律风险),电子商务网站的中断可能会在收入和声誉损害方面造成巨大成本。
|
||||
|
||||
在许多应用程序中,几分钟甚至几小时的临时中断是可以容忍的 [^74],但永久的数据丢失或损坏将是灾难性的。考虑一位父母,他将所有孩子的照片和视频存储在你的照片应用程序中 [^75]。如果该数据库突然损坏,他们会有什么感觉?他们会知道如何从备份中恢复吗?
|
||||
在许多应用程序中,几分钟甚至几小时的临时中断是可以容忍的 [^74],但永久数据丢失或损坏将是灾难性的。考虑一位家长在你的照片应用程序中存储他们孩子的所有照片和视频 [^75]。如果该数据库突然损坏,他们会有什么感觉?他们会知道如何从备份中恢复吗?
|
||||
|
||||
作为不可靠软件如何伤害人们的另一个例子,考虑邮局地平线丑闻。1999 年至 2019 年间,数百名管理英国邮局分支机构的人因盗窃或欺诈而被定罪,因为会计软件显示他们的账户有短缺。最终,很明显,这些短缺中的许多是由于软件中的错误造成的,许多定罪此后已被推翻 [^76]。导致这一可能是英国历史上最大的司法不公的是,英国法律假定计算机正确运行(因此,计算机产生的证据是可靠的),除非有相反的证据 [^77]。软件工程师可能会嘲笑软件可能永远没有错误的想法,但这对那些因不可靠的计算机系统而被错误监禁、宣布破产,甚至因错误定罪而自杀的人来说是很少的安慰。
|
||||
作为不可靠软件如何伤害人们的另一个例子,考虑邮局地平线丑闻。在 1999 年至 2019 年期间,管理英国邮局分支机构的数百人因会计软件显示其账户短缺而被判盗窃或欺诈罪。最终变得清楚,许多这些短缺是由于软件中的错误,许多定罪已被推翻 [^76]。导致这一可能是英国历史上最大的司法不公的是,英国法律假设计算机正确运行(因此,计算机产生的证据是可靠的),除非有相反的证据 [^77]。软件工程师可能会嘲笑软件可能无错误的想法,但这对那些因不可靠的计算机系统而被错误监禁、宣布破产甚至自杀的人来说,这是很少的安慰。
|
||||
|
||||
在某些情况下,我们可能会选择牺牲可靠性以降低开发成本(例如,在为未经证实的市场开发原型产品时)——但我们应该非常清楚何时在偷工减料,并牢记潜在的后果。
|
||||
在某些情况下,我们可能选择牺牲可靠性以降低开发成本(例如,在为未经证实的市场开发原型产品时)——但我们应该非常清楚何时走捷径并牢记潜在的后果。
|
||||
|
||||
--------
|
||||
|
||||
## 可伸缩性 {#sec_introduction_scalability}
|
||||
|
||||
即使系统今天可靠地工作,这并不意味着它将来一定会可靠地工作。降级的一个常见原因是负载增加:也许系统已经从 10,000 个并发用户增长到 100,000 个并发用户,或者从 100 万增长到 1000 万。也许它正在处理比以前大得多的数据量。
|
||||
即使系统今天可靠地工作,这并不意味着它将来必然会可靠地工作。降级的一个常见原因是负载增加:也许系统已经从 10,000 个并发用户增长到 100,000 个并发用户,或者从 100 万增长到 1000 万。也许它正在处理比以前大得多的数据量。
|
||||
|
||||
**可扩展性** 是我们用来描述系统处理负载增加的能力的术语。有时,在讨论可扩展性时,人们会发表类似"你不是谷歌或亚马逊。停止担心规模,只使用关系数据库"的评论。这个格言是否适用于你取决于你正在构建的应用程序类型。
|
||||
*可伸缩性* 是我们用来描述系统应对负载增加能力的术语。有时,在讨论可伸缩性时,人们会发表评论,如"你不是谷歌或亚马逊。停止担心规模,只使用关系数据库。"这个格言是否适用于你取决于你正在构建的应用程序类型。
|
||||
|
||||
如果你正在构建一个目前只有少数用户的新产品,也许在一家初创公司,那么首要的工程目标通常是保持系统尽可能简单和灵活,以便你可以轻松修改和调整产品的功能,因为你了解更多关于客户需求的信息 [^78]。在这样的环境中,担心未来可能需要的假设规模是适得其反的:在最好的情况下,对可扩展性的投资是浪费的努力和过早的优化;在最坏的情况下,它们会将你锁定在不灵活的设计中,并使你更难发展你的应用程序。
|
||||
如果你正在构建一个目前只有少数用户的新产品,也许是在初创公司,首要的工程目标通常是保持系统尽可能简单和灵活,以便你可以在了解更多关于客户需求时轻松修改和调整产品的功能 [^78]。在这种环境中,担心未来可能需要的假设规模是适得其反的:在最好的情况下,对可伸缩性的投资是浪费的努力和过早的优化;在最坏的情况下,它们会将你锁定在不灵活的设计中,并使你的应用程序更难发展。
|
||||
|
||||
原因是可扩展性不是一维标签:说"X 是可扩展的"或"Y 不扩展"是没有意义的。相反,讨论可扩展性意味着考虑以下问题:
|
||||
原因是可伸缩性不是一维标签:说"X 是可伸缩的"或"Y 不伸缩"是没有意义的。相反,讨论可伸缩性意味着考虑诸如以下问题:
|
||||
|
||||
* "如果系统以特定方式增长,我们有哪些选择来应对增长?"
|
||||
* "如果系统以特定方式增长,我们有什么选择来应对增长?"
|
||||
* "我们如何添加计算资源来处理额外的负载?"
|
||||
* "根据当前的增长预测,我们何时会达到当前架构的极限?"
|
||||
* "基于当前的增长预测,我们何时会达到当前架构的极限?"
|
||||
|
||||
如果你成功地使你的应用程序流行,因此处理越来越多的负载,你将了解你的性能瓶颈在哪里,因此你将知道你需要在哪些维度上扩展。那时是开始担心可扩展性技术的时候了。
|
||||
如果你成功地使你的应用程序受欢迎,因此处理越来越多的负载,你将了解你的性能瓶颈在哪里,因此你将知道需要沿着哪些维度进行伸缩。那时是开始担心可伸缩性技术的时候。
|
||||
|
||||
### 描述负载 {#id33}
|
||||
|
||||
首先,我们需要简洁地描述系统上的当前负载;只有这样我们才能讨论增长问题(如果我们的负载翻倍会发生什么?)。通常这将是吞吐量的度量:例如,服务的每秒请求数、每天到达的新数据的千兆字节数,或每小时购物车结账的数量。有时你关心某些可变数量的峰值,例如 ["案例研究:社交网络主页时间线"](/ch2#sec_introduction_twitter) 中同时在线用户的数量。
|
||||
首先,我们需要简洁地描述系统上的当前负载;只有这样我们才能讨论增长问题(如果我们的负载翻倍会发生什么?)。通常这将是吞吐量的度量:例如,对服务的每秒请求数、每天到达多少千兆字节的新数据,或每小时购物车结账的数量。有时你关心某个变量数量的峰值,例如 ["案例研究:社交网络首页时间线"](/ch2#sec_introduction_twitter) 中同时在线用户的数量。
|
||||
|
||||
通常,负载的其他统计特征也会影响访问模式,从而影响可扩展性要求。例如,你可能需要知道数据库中读取与写入的比率、缓存的命中率或每个用户的数据项数(例如,社交网络案例研究中的粉丝数)。也许平均情况对你很重要,或者你的瓶颈可能由少数极端情况主导。这一切都取决于你特定应用程序的细节。
|
||||
通常还有其他影响访问模式并因此影响可伸缩性要求的负载统计特征。例如,你可能需要知道数据库中的读写比率、缓存的命中率或每个用户的数据项数量(例如,社交网络案例研究中的粉丝数量)。也许平均情况对你很重要,或者也许你的瓶颈由少数极端情况主导。这一切都取决于你特定应用程序的细节。
|
||||
|
||||
一旦你描述了系统上的负载,你就可以研究负载增加时会发生什么。你可以从两个方面来看:
|
||||
一旦你描述了系统上的负载,你就可以调查当负载增加时会发生什么。你可以从两个方面来看待它:
|
||||
|
||||
* 当你以某种方式增加负载并保持系统资源(CPU、内存、网络带宽等)不变时,你的系统性能如何受到影响?
|
||||
* 当你以某种方式增加负载并保持系统资源(CPU、内存、网络带宽等)不变时,系统的性能如何受到影响?
|
||||
* 当你以某种方式增加负载时,如果你想保持性能不变,你需要增加多少资源?
|
||||
|
||||
通常我们的目标是在满足 SLA 要求的同时保持系统的性能(参见 ["响应时间指标的使用"](/ch2#sec_introduction_slo_sla)),同时最小化运行系统的成本。所需的计算资源越多,成本就越高。可能某些类型的硬件比其他类型更具成本效益,随着新型硬件的出现,这些因素可能会随着时间而改变。
|
||||
通常我们的目标是在最小化运行系统成本的同时保持系统性能在 SLA 的要求范围内(参见 ["响应时间指标的应用"](/ch2#sec_introduction_slo_sla))。所需的计算资源越多,成本就越高。可能某些类型的硬件比其他类型更具成本效益,这些因素可能会随着新类型硬件的出现而随时间变化。
|
||||
|
||||
如果你可以加倍资源以处理两倍的负载,同时保持性能相同,我们说你有 **线性可扩展性**,这被认为是一件好事。偶尔可以用少于两倍的资源处理两倍的负载,这是由于规模经济或峰值负载的更好分布 [^79] [^80]。更有可能的是,成本增长速度快于线性,效率低下可能有很多原因。例如,如果你有大量数据,那么处理单个写入请求可能涉及比你有少量数据时更多的工作,即使请求的大小相同。
|
||||
如果你可以将资源翻倍以处理两倍的负载,同时保持性能不变,我们说你有 *线性可伸缩性*,这被认为是好事。偶尔,由于规模经济或峰值负载的更好分布,可以用不到两倍的资源处理两倍的负载 [^79] [^80]。更可能的是,成本增长速度快于线性,并且效率低下可能有许多原因。例如,如果你有大量数据,那么处理单个写请求可能涉及比你有少量数据时更多的工作,即使请求的大小相同。
|
||||
|
||||
### 共享内存、共享磁盘与无共享架构 {#sec_introduction_shared_nothing}
|
||||
|
||||
增加服务硬件资源的最简单方法是将其移动到更强大的机器。单个 CPU 核心不再变得明显更快,但你可以购买(或租用云实例)具有更多 CPU 核心、更多 RAM 和更多磁盘空间的机器。这种方法称为 **垂直扩展** 或 **纵向扩展**。
|
||||
增加服务硬件资源的最简单方法是将其移动到更强大的机器。单个 CPU 核心不再变得显著更快,但你可以购买一台机器(或租用云实例)具有更多 CPU 核心、更多 RAM 和更多磁盘空间。这种方法称为 *纵向伸缩* 或 *向上扩展*。
|
||||
|
||||
你可以通过使用多个进程或线程在单台机器上获得并行性。属于同一进程的所有线程都可以访问相同的 RAM,因此这种方法也称为 **共享内存架构**。共享内存方法的问题是成本增长速度快于线性:具有两倍硬件资源的高端机器通常成本远远超过两倍。由于瓶颈,两倍大小的机器通常可以处理不到两倍的负载。
|
||||
你可以通过使用多个进程或线程在单台机器上获得并行性。属于同一进程的所有线程都可以访问相同的 RAM,因此这种方法也称为 *共享内存架构*。共享内存方法的问题是成本增长速度快于线性:具有两倍硬件资源的高端机器通常成本远远超过两倍。由于瓶颈,两倍大小的机器通常可以处理不到两倍的负载。
|
||||
|
||||
另一种方法是 **共享磁盘架构**,它使用具有独立 CPU 和 RAM 的几台机器,但将数据存储在机器之间共享的磁盘阵列上,它们通过快速网络连接:**网络附加存储**(NAS)或 **存储区域网络**(SAN)。这种架构传统上用于本地数据仓库工作负载,但争用和锁定的开销限制了共享磁盘方法的可扩展性 [^81]。
|
||||
另一种方法是 *共享磁盘架构*,它使用几台具有独立 CPU 和 RAM 的机器,但将数据存储在机器之间共享的磁盘阵列上,这些机器通过快速网络连接:*网络附加存储*(NAS)或 *存储区域网络*(SAN)。这种架构传统上用于本地数据仓库工作负载,但争用和锁定的开销限制了共享磁盘方法的可伸缩性 [^81]。
|
||||
|
||||
相比之下,**无共享架构** [^82](也称为 **水平扩展** 或 **横向扩展**)已经获得了很大的普及。在这种方法中,我们使用具有多个节点的分布式系统,每个节点都有自己的 CPU、RAM 和磁盘。节点之间的任何协调都在软件级别通过传统网络完成。
|
||||
相比之下,*无共享架构* [^82](也称为 *横向伸缩* 或 *向外扩展*)已经获得了很大的流行。在这种方法中,我们使用具有多个节点的分布式系统,每个节点都有自己的 CPU、RAM 和磁盘。节点之间的任何协调都在软件级别通过传统网络完成。
|
||||
|
||||
无共享的优点是它具有线性扩展的潜力,它可以使用提供最佳性价比的任何硬件(特别是在云中),它可以在负载增加或减少时更轻松地调整其硬件资源,并且它可以通过在多个数据中心和区域分布系统来实现更高的容错性。缺点是它需要显式分片(参见 [第 7 章](/ch7#ch_sharding)),并且会产生分布式系统的所有复杂性([第 9 章](/ch9#ch_distributed))。
|
||||
无共享的优点是它有线性伸缩的潜力,它可以使用提供最佳性价比的任何硬件(特别是在云中),它可以随着负载的增加或减少更容易地调整其硬件资源,并且它可以通过在多个数据中心和地区分布系统来实现更大的容错。缺点是它需要显式分片(参见 [第 7 章](/ch7)),并且它会产生分布式系统的所有复杂性([第 9 章](/ch9))。
|
||||
|
||||
一些云原生数据库系统使用单独的服务进行存储和事务执行(参见 ["存储和计算分离"](/ch1#sec_introduction_storage_compute)),多个计算节点共享对同一存储服务的访问。该模型与共享磁盘架构有一些相似之处,但它避免了旧系统的可扩展性问题:它不提供文件系统(NAS)或块设备(SAN)抽象,而是提供专门为数据库的特定需求设计的专用 API [^83]。
|
||||
一些云原生数据库系统为存储和事务执行使用单独的服务(参见 ["存储与计算分离"](/ch1#sec_introduction_storage_compute)),多个计算节点共享对同一存储服务的访问。这个模型与共享磁盘架构有一些相似之处,但它避免了旧系统的可伸缩性问题:它不是提供文件系统(NAS)或块设备(SAN)抽象,而是存储服务提供专门为数据库特定需求设计的 API [^83]。
|
||||
|
||||
### 可伸缩性原则 {#id35}
|
||||
|
||||
大规模运行的系统架构通常高度特定于应用程序——没有通用的、一刀切的可扩展架构(非正式地称为 **万金油**)。例如,设计用于处理每秒 100,000 个请求(每个 1 kB 大小)的系统与设计用于每分钟 3 个请求(每个 2 GB 大小)的系统看起来非常不同——即使两个系统具有相同的数据吞吐量(100 MB/秒)。
|
||||
在大规模运行的系统架构通常对应用程序高度特定——没有通用的、一刀切的可伸缩架构(非正式地称为 *万金油*)。例如,设计用于处理每秒 100,000 个请求(每个 1 kB 大小)的系统与设计用于每分钟 3 个请求(每个 2 GB 大小)的系统看起来非常不同——即使两个系统具有相同的数据吞吐量(100 MB/秒)。
|
||||
|
||||
此外,适合一个负载级别的架构不太可能应对 10 倍的负载。如果你正在处理快速增长的服务,因此你可能需要在每个数量级负载增加时重新考虑你的架构。由于应用程序的需求可能会发展,通常不值得提前规划未来的扩展需求超过一个数量级。
|
||||
此外,适合一个负载级别的架构不太可能应对 10 倍的负载。如果你正在开发快速增长的服务,因此很可能你需要在每个数量级的负载增加时重新考虑你的架构。由于应用程序的需求可能会演变,通常不值得提前规划超过一个数量级的未来伸缩需求。
|
||||
|
||||
可扩展性的一个良好通用原则是将系统分解为可以在很大程度上相互独立运行的较小组件。这是微服务背后的基本原则(参见 ["微服务和无服务器"](/ch1#sec_introduction_microservices))、分片([第 7 章](/ch7#ch_sharding))、流处理([Link to Come])和无共享架构。然而,挑战在于知道在哪里划定应该在一起的事物和应该分开的事物之间的界限。微服务的设计指南可以在其他书籍中找到 [^84],我们在 [第 7 章](/ch7#ch_sharding) 中讨论无共享系统的分片。
|
||||
可伸缩性的一个良好通用原则是将系统分解为可以在很大程度上相互独立运行的较小组件。这是微服务背后的基本原则(参见 ["微服务与无服务器"](/ch1#sec_introduction_microservices))、分片([第 7 章](/ch7))、流处理([后续章节])和无共享架构。然而,挑战在于知道在哪里划分应该在一起的事物和应该分开的事物之间的界限。微服务的设计指南可以在其他书籍中找到 [^84],我们在 [第 7 章](/ch7) 中讨论无共享系统的分片。
|
||||
|
||||
另一个好原则是不要让事情变得比必要的更复杂。如果单机数据库可以完成工作,它可能比复杂的分布式设置更可取。自动扩展系统(根据需求自动添加或删除资源)很酷,但如果你的负载相当可预测,手动扩展的系统可能会有更少的操作意外(参见 ["操作:自动或手动再平衡"](/ch7#sec_sharding_operations))。具有五个服务的系统比具有五十个服务的系统更简单。良好的架构通常涉及方法的实用混合。
|
||||
另一个好原则是不要让事情变得比必要的更复杂。如果单机数据库可以完成工作,它可能比复杂的分布式设置更可取。自动伸缩系统(根据需求自动添加或删除资源)很酷,但如果你的负载相当可预测,手动伸缩的系统可能会有更少的操作意外(参见 ["操作:自动或手动再平衡"](/ch7#sec_sharding_operations))。具有五个服务的系统比具有五十个服务的系统更简单。良好的架构通常涉及方法的务实混合。
|
||||
|
||||
## 可运维性 {#sec_introduction_maintainability}
|
||||
|
||||
软件不会磨损或遭受材料疲劳,因此它不会像机械物体那样破坏。但是应用程序的需求经常变化,软件运行的环境发生变化(例如其依赖项和底层平台),并且它有需要修复的错误。
|
||||
软件不会磨损或遭受材料疲劳,因此它不会像机械物体那样以同样的方式损坏。但应用程序的要求经常变化,软件运行的环境发生变化(例如其依赖项和底层平台),并且它有需要修复的错误。
|
||||
|
||||
人们普遍认识到,软件的大部分成本不在于其初始开发,而在于其持续维护——修复错误、保持系统运行、调查故障、使其适应新平台、为新用例修改它、偿还技术债务以及添加新功能 [^85] [^86]。
|
||||
人们普遍认为,软件的大部分成本不在其初始开发中,而在其持续维护中——修复错误、保持其系统运行、调查故障、将其适应新平台、为新用例修改它、偿还技术债务和添加新功能 [^85] [^86]。
|
||||
|
||||
然而,维护也很困难。如果系统已成功运行很长时间,它可能会使用今天很少有工程师理解的过时技术(例如大型机和 COBOL 代码);关于系统如何以及为什么以某种方式设计的机构知识可能会随着人们离开组织而丢失;可能有必要修复其他人的错误。此外,计算机系统通常与它支持的人类组织交织在一起,这意味着这种 **遗留** 系统的维护既是人的问题,也是技术问题 [^87]。
|
||||
然而,维护也很困难。如果系统已成功运行很长时间,它可能使用今天不多工程师理解的过时技术(如大型机和 COBOL 代码);关于系统如何以及为何以某种方式设计的机构知识可能已经随着人们离开组织而丢失;可能需要修复其他人的错误。此外,计算机系统通常与它支持的人类组织交织在一起,这意味着此类 *遗留* 系统的维护既是人的问题,也是技术问题 [^87]。
|
||||
|
||||
我们今天创建的每个系统如果有足够的价值能够存活很长时间,有一天会成为遗留系统。为了最大限度地减少未来需要维护我们软件的几代人的痛苦,我们应该在设计时考虑维护问题。尽管我们不能总是预测哪些决定可能在未来造成维护麻烦,但在本书中,我们将关注几个广泛适用的原则:
|
||||
如果我们今天创建的每个系统都足够有价值以长期生存,它有一天将成为遗留系统。为了最小化需要维护我们软件的未来几代人的痛苦,我们应该在设计时考虑维护问题。尽管我们不能总是预测哪些决定可能会在未来造成维护难题,但在本书中,我们将注意几个广泛适用的原则:
|
||||
|
||||
可运维性
|
||||
: 使组织能够轻松地保持系统平稳运行。
|
||||
可操作性
|
||||
: 使组织容易保持系统平稳运行。
|
||||
|
||||
简单性
|
||||
: 通过使用易于理解、一致的模式和结构来实现系统,并避免不必要的复杂性,使新工程师能够轻松理解系统。
|
||||
: 通过使用易于理解、一致的模式和结构来实施它,并避免不必要的复杂性,使新工程师容易理解系统。
|
||||
|
||||
可演化性
|
||||
: 使工程师能够轻松地在未来对系统进行更改,随着需求的变化而调整和扩展它以适应意外的用例。
|
||||
: 使工程师将来容易对系统进行更改,随着需求变化而适应和扩展它以用于未预料的用例。
|
||||
|
||||
### 可运维性:让运维更轻松 {#id37}
|
||||
|
||||
我们之前在 ["云时代的运维"](/ch1#sec_introduction_operations) 中讨论了运维的作用,我们看到人类流程对于可靠运维至少与软件工具一样重要。事实上,有人建议"良好的运维通常可以解决糟糕(或不完整)软件的限制,但良好的软件无法通过糟糕的运维可靠地运行" [^60]。
|
||||
我们之前在 ["云时代的运维"](/ch1#sec_introduction_operations) 中讨论了运维的角色,我们看到人类流程对于可靠运维至少与软件工具一样重要。事实上,有人建议"良好的运维通常可以解决糟糕(或不完整)软件的局限性,但良好的软件无法与糟糕的运维可靠地运行" [^60]。
|
||||
|
||||
在由数千台机器组成的大规模系统中,手动维护将是不合理的昂贵,自动化是必不可少的。然而,自动化可能是一把双刃剑:总会有边缘情况(例如罕见的故障场景)需要运维团队的手动干预。由于无法自动处理的情况是最复杂的问题,更大的自动化需要一个 **更** 熟练的运维团队来解决这些问题 [^88]。
|
||||
在由数千台机器组成的大规模系统中,手动维护将是不合理地昂贵的,自动化是必不可少的。然而,自动化可能是一把双刃剑:总会有边缘情况(如罕见的故障场景)需要运维团队的手动干预。由于无法自动处理的情况是最复杂的问题,更大的自动化需要一个 *更* 熟练的运维团队来解决这些问题 [^88]。
|
||||
|
||||
此外,如果自动化系统出错,它通常比依赖操作员手动执行某些操作的系统更难排除故障。出于这个原因,更多的自动化并不总是对可操作性更好。然而,一定量的自动化很重要,最佳点将取决于你特定应用程序和组织的具体情况。
|
||||
此外,如果自动化系统出错,通常比依赖操作员手动执行某些操作的系统更难排除故障。出于这个原因,更多的自动化并不总是对可操作性更好。然而,一定程度的自动化很重要,最佳点将取决于你特定应用程序和组织的细节。
|
||||
|
||||
良好的可操作性意味着使例行任务变得容易,允许运维团队将精力集中在高价值活动上。数据系统可以做各种事情来使例行任务变得容易,包括 [^89]:
|
||||
良好的可操作性意味着使常规任务变得容易,使运维团队能够将精力集中在高价值活动上。数据系统可以做各种事情来使常规任务变得容易,包括 [^89]:
|
||||
|
||||
* 允许监控工具检查系统的关键指标,并支持可观察性工具(参见 ["分布式系统的问题"](/ch1#sec_introduction_dist_sys_problems))以深入了解系统的运行时行为。各种商业和开源工具可以在这里提供帮助 [^90]。
|
||||
* 避免对单个机器的依赖(允许在系统作为一个整体继续不间断运行的同时关闭机器进行维护)
|
||||
* 提供良好的文档和易于理解的操作模型("如果我做 X,Y 将发生")
|
||||
* 避免对单个机器的依赖(允许在系统整体继续不间断运行的同时关闭机器进行维护)
|
||||
* 提供良好的文档和易于理解的操作模型("如果我做 X,Y 将会发生")
|
||||
* 提供良好的默认行为,但也给管理员在需要时覆盖默认值的自由
|
||||
* 在适当的情况下自我修复,但也在需要时为管理员提供对系统状态的手动控制
|
||||
* 表现出可预测的行为,最大限度地减少意外
|
||||
* 在适当的地方自我修复,但也在需要时给管理员手动控制系统状态
|
||||
* 表现出可预测的行为,最小化意外
|
||||
|
||||
### 简单性:管理复杂度 {#id38}
|
||||
|
||||
小型软件项目可以拥有令人愉快的简单和富有表现力的代码,但随着项目变大,它们通常变得非常复杂且难以理解。这种复杂性减慢了每个需要在系统上工作的人的速度,进一步增加了维护成本。陷入复杂性的软件项目有时被描述为 **大泥球** [^91]。
|
||||
小型软件项目可以有令人愉快地简单和富有表现力的代码,但随着项目变大,它们通常变得非常复杂且难以理解。这种复杂性减慢了需要在系统上工作的每个人,进一步增加了维护成本。陷入复杂性的软件项目有时被描述为 *大泥球* [^91]。
|
||||
|
||||
当复杂性使维护变得困难时,预算和时间表经常被超支。在复杂的软件中,进行更改时引入错误的风险也更大:当系统对开发人员来说更难理解和推理时,隐藏的假设、意外的后果和意外的交互更容易被忽视 [^69]。相反,降低复杂性极大地提高了软件的可维护性,因此简单性应该是我们构建的系统的关键目标。
|
||||
当复杂性使维护困难时,预算和时间表经常超支。在复杂软件中,进行更改时引入错误的风险也更大:当系统对开发人员来说更难理解和推理时,隐藏的假设、意外的后果和意外的交互更容易被忽视 [^69]。相反,降低复杂性极大地提高了软件的可维护性,因此简单性应该是我们构建的系统的关键目标。
|
||||
|
||||
简单的系统更容易理解,因此我们应该尝试以最简单的方式解决给定的问题。不幸的是,说起来容易做起来难。某事是否简单通常是主观品味问题,因为没有客观的简单性标准 [^92]。例如,一个系统可能会在简单接口后面隐藏复杂的实现,而另一个系统可能有一个向其用户公开更多内部细节的简单实现——哪个更简单?
|
||||
简单系统更容易理解,因此我们应该尝试以尽可能简单的方式解决给定问题。不幸的是,这说起来容易做起来难。某物是否简单通常是主观的品味问题,因为没有客观的简单性标准 [^92]。例如,一个系统可能在简单界面后面隐藏复杂的实现,而另一个系统可能有一个向用户公开更多内部细节的简单实现——哪一个更简单?
|
||||
|
||||
推理复杂性的一种尝试是将其分为两类,**基本** 和 **偶然** 复杂性 [^93]。想法是基本复杂性是应用程序问题域中固有的,而偶然复杂性仅由于我们工具的限制而产生。不幸的是,这种区别也是有缺陷的,因为随着我们的工具发展,基本和偶然之间的界限会发生变化 [^94]。
|
||||
推理复杂性的一种尝试是将其分为两类,*本质* 和 *偶然* 复杂性 [^93]。这个想法是,本质复杂性是应用程序问题域中固有的,而偶然复杂性仅由于我们工具的限制而产生。不幸的是,这种区别也有缺陷,因为本质和偶然之间的边界随着我们工具的发展而变化 [^94]。
|
||||
|
||||
我们管理复杂性的最佳工具之一是 **抽象**。良好的抽象可以在干净、易于理解的外观后面隐藏大量的实现细节。良好的抽象也可以用于各种不同的应用程序。这种重用不仅比多次重新实现类似的东西更有效,而且还导致更高质量的软件,因为抽象组件中的质量改进使所有使用它的应用程序受益。
|
||||
我们管理复杂性的最佳工具之一是 *抽象*。良好的抽象可以在干净、易于理解的外观后面隐藏大量实现细节。良好的抽象也可以用于各种不同的应用程序。这种重用不仅比多次重新实现类似的东西更有效,而且还导致更高质量的软件,因为抽象组件中的质量改进使所有使用它的应用程序受益。
|
||||
|
||||
例如,高级编程语言是隐藏机器代码、CPU 寄存器和系统调用的抽象。SQL 是隐藏复杂的磁盘和内存数据结构、来自其他客户端的并发请求以及崩溃后不一致的抽象。当然,在高级语言中编程时,我们仍在使用机器代码;我们只是不 **直接** 使用它,因为编程语言抽象使我们免于思考它。
|
||||
例如,高级编程语言是隐藏机器代码、CPU 寄存器和系统调用的抽象。SQL 是一种隐藏复杂的磁盘上和内存中数据结构、来自其他客户端的并发请求以及崩溃后不一致性的抽象。当然,在用高级语言编程时,我们仍在使用机器代码;我们只是不 *直接* 使用它,因为编程语言抽象使我们免于考虑它。
|
||||
|
||||
应用程序代码的抽象,旨在降低其复杂性,可以使用 **设计模式** [^95] 和 **领域驱动设计**(DDD)[^96] 等方法创建。本书不是关于这种特定于应用程序的抽象,而是关于你可以在其上构建应用程序的通用抽象,例如数据库事务、索引和事件日志。如果你想使用 DDD 等技术,你可以在本书中描述的基础之上实现它们。
|
||||
应用程序代码的抽象,旨在降低其复杂性,可以使用诸如 *设计模式* [^95] 和 *领域驱动设计*(DDD)[^96] 等方法创建。本书不是关于此类特定于应用程序的抽象,而是关于你可以在其上构建应用程序的通用抽象,例如数据库事务、索引和事件日志。如果你想使用像 DDD 这样的技术,你可以在本书中描述的基础之上实现它们。
|
||||
|
||||
### 可演化性:让变化更容易 {#sec_introduction_evolvability}
|
||||
|
||||
你的系统需求极不可能永远保持不变。它们更有可能处于不断变化中:你学习新事实,以前意料之外的用例出现,业务优先级发生变化,用户请求新功能,新平台取代旧平台,法律或监管要求发生变化,系统的增长迫使架构变化等。
|
||||
你的系统需求将保持不变的可能性极小。它们更可能处于不断变化中:你学习新事实、以前未预料的用例出现、业务优先级发生变化、用户请求新功能、新平台取代旧平台、法律或监管要求发生变化、系统增长迫使架构变化等。
|
||||
|
||||
就组织流程而言,**敏捷** 工作模式为适应变化提供了一个框架。敏捷社区还开发了在频繁变化的环境中开发软件时有用的技术工具和流程,例如测试驱动开发(TDD)和重构。在本书中,我们寻找在由几个具有不同特征的不同应用程序或服务组成的系统级别提高敏捷性的方法。
|
||||
在组织流程方面,*敏捷* 工作模式为适应变化提供了框架。敏捷社区还开发了在频繁变化的环境中开发软件时有用的技术工具和流程,例如测试驱动开发(TDD)和重构。在本书中,我们搜索在由具有不同特征的几个不同应用程序或服务组成的系统级别增加敏捷性的方法。
|
||||
|
||||
你可以修改数据系统并使其适应不断变化的需求的难易程度与其简单性及其抽象密切相关:松散耦合、简单的系统通常比紧密耦合、复杂的系统更容易修改。由于这是一个如此重要的想法,我们将使用不同的词来指代数据系统级别的敏捷性:**可演化性** [^97]。
|
||||
你可以修改数据系统并使其适应不断变化的需求的容易程度与其简单性及其抽象密切相关:松散耦合、简单系统通常比紧密耦合、复杂系统更容易修改。由于这是一个如此重要的想法,我们将使用不同的词来指代数据系统级别的敏捷性:*可演化性* [^97]。
|
||||
|
||||
使变化变得困难的一个主要因素是当某些行动是不可逆的,因此需要非常小心地采取该行动 [^98]。例如,假设你正在从一个数据库迁移到另一个数据库:如果你无法在新数据库出现问题时切换回旧系统,那么风险比你可以轻松返回时要高得多。最小化不可逆性可以提高灵活性。
|
||||
使大型系统中的变化困难的一个主要因素是某些操作不可逆,因此需要非常谨慎地采取该操作 [^98]。例如,假设你正在从一个数据库迁移到另一个数据库:如果在新数据库出现问题时无法切换回旧系统,风险就会高得多,而如果你可以轻松返回。最小化不可逆性提高了灵活性。
|
||||
|
||||
## 总结 {#summary}
|
||||
|
||||
在本章中,我们研究了几个非功能性需求的示例:性能、可靠性、可扩展性和可维护性。通过这些主题,我们还遇到了我们在本书其余部分所需的原则和术语。我们从一个如何在社交网络中实现主页时间线的案例研究开始,这说明了大规模出现的一些挑战。
|
||||
在本章中,我们研究了几个非功能性需求的例子:性能、可靠性、可伸缩性和可维护性。通过这些主题,我们还遇到了我们在本书其余部分需要的原则和术语。我们从社交网络中首页时间线如何实现的案例研究开始,这说明了规模上出现的一些挑战。
|
||||
|
||||
我们讨论了如何衡量性能(例如,使用响应时间百分位数)、系统上的负载(例如,使用吞吐量指标)以及它们如何在 SLA 中使用。可扩展性是一个密切相关的概念:即,确保负载增长时性能保持不变。我们看到了一些可扩展性的一般原则,例如将任务分解为可以独立运行的较小部分,我们将在以下章节中深入探讨可扩展性技术的技术细节。
|
||||
我们讨论了如何衡量性能(例如,使用响应时间百分位数)、系统上的负载(例如,使用吞吐量指标),以及它们如何在 SLA 中使用。可伸缩性是一个密切相关的概念:即,在负载增长时确保性能保持不变。我们看到了可伸缩性的一些一般原则,例如将任务分解为可以独立运行的较小部分,我们将在以下章节中深入研究可伸缩性技术的技术细节。
|
||||
|
||||
为了实现可靠性,你可以使用容错技术,即使某些组件(例如,磁盘、机器或其他服务)出现故障,系统也可以继续提供其服务。我们看到了可能发生的硬件故障的示例,并将它们与软件故障区分开来,软件故障可能更难处理,因为它们通常高度相关。实现可靠性的另一个方面是建立对人类犯错误的韧性,我们看到无责备事后分析作为从事件中学习的技术。
|
||||
为了实现可靠性,你可以使用容错技术,即使某个组件(例如,磁盘、机器或其他服务)出现故障,系统也可以继续提供其服务。我们看到了可能发生的硬件故障的例子,并将它们与软件故障区分开来,软件故障可能更难处理,因为它们通常是强相关的。实现可靠性的另一个方面是建立对人类犯错误的韧性,我们看到无责备事后分析作为从事件中学习的技术。
|
||||
|
||||
最后,我们研究了可维护性的几个方面,包括支持运维团队的工作、管理复杂性以及使应用程序的功能随着时间的推移易于发展。关于如何实现这些事情没有简单的答案,但有一件事可以帮助,那就是使用经过验证在实践中有价值的易于理解的构建块来构建应用程序。本书的其余部分将涵盖一系列已被证明在实践中有价值的构建块。
|
||||
最后,我们研究了可维护性的几个方面,包括支持运维团队的工作、管理复杂性以及随着时间的推移使应用程序功能易于演化。实现这些目标没有简单的答案,但有一件事可以帮助,那就是使用提供有用抽象的易于理解的构建块来构建应用程序。本书的其余部分将涵盖一系列在实践中被证明有价值的构建块。
|
||||
|
||||
|
||||
### 参考
|
||||
### 参考 {#参考}
|
||||
|
||||
[^1]: Mike Cvet. [How We Learned to Stop Worrying and Love Fan-In at Twitter](https://www.youtube.com/watch?v=WEgCjwyXvwc). At *QCon San Francisco*, December 2016.
|
||||
[^2]: Raffi Krikorian. [Timelines at Scale](https://www.infoq.com/presentations/Twitter-Timeline-Scalability/). At *QCon San Francisco*, November 2012. Archived at [perma.cc/V9G5-KLYK](https://perma.cc/V9G5-KLYK)
|
||||
[^3]: Twitter. [Twitter’s Recommendation Algorithm](https://blog.twitter.com/engineering/en_us/topics/open-source/2023/twitter-recommendation-algorithm). *blog.twitter.com*, March 2023. Archived at [perma.cc/L5GT-229T](https://perma.cc/L5GT-229T)
|
||||
[^3]: Twitter. [Twitter's Recommendation Algorithm](https://blog.twitter.com/engineering/en_us/topics/open-source/2023/twitter-recommendation-algorithm). *blog.twitter.com*, March 2023. Archived at [perma.cc/L5GT-229T](https://perma.cc/L5GT-229T)
|
||||
[^4]: Raffi Krikorian. [New Tweets per second record, and how!](https://blog.twitter.com/engineering/en_us/a/2013/new-tweets-per-second-record-and-how) *blog.twitter.com*, August 2013. Archived at [perma.cc/6JZN-XJYN](https://perma.cc/6JZN-XJYN)
|
||||
[^5]: Jaz Volpert. [When Imperfect Systems are Good, Actually: Bluesky’s Lossy Timelines](https://jazco.dev/2025/02/19/imperfection/). *jazco.dev*, February 2025. Archived at [perma.cc/2PVE-L2MX](https://perma.cc/2PVE-L2MX)
|
||||
[^6]: Samuel Axon. [3% of Twitter’s Servers Dedicated to Justin Bieber](https://mashable.com/archive/justin-bieber-twitter). *mashable.com*, September 2010. Archived at [perma.cc/F35N-CGVX](https://perma.cc/F35N-CGVX)
|
||||
[^5]: Jaz Volpert. [When Imperfect Systems are Good, Actually: Bluesky's Lossy Timelines](https://jazco.dev/2025/02/19/imperfection/). *jazco.dev*, February 2025. Archived at [perma.cc/2PVE-L2MX](https://perma.cc/2PVE-L2MX)
|
||||
[^6]: Samuel Axon. [3% of Twitter's Servers Dedicated to Justin Bieber](https://mashable.com/archive/justin-bieber-twitter). *mashable.com*, September 2010. Archived at [perma.cc/F35N-CGVX](https://perma.cc/F35N-CGVX)
|
||||
[^7]: Nathan Bronson, Abutalib Aghayev, Aleksey Charapko, and Timothy Zhu. [Metastable Failures in Distributed Systems](https://sigops.org/s/conferences/hotos/2021/papers/hotos21-s11-bronson.pdf). At *Workshop on Hot Topics in Operating Systems* (HotOS), May 2021. [doi:10.1145/3458336.3465286](https://doi.org/10.1145/3458336.3465286)
|
||||
[^8]: Marc Brooker. [Metastability and Distributed Systems](https://brooker.co.za/blog/2021/05/24/metastable.html). *brooker.co.za*, May 2021. Archived at [perma.cc/7FGJ-7XRK](https://perma.cc/7FGJ-7XRK)
|
||||
[^9]: Marc Brooker. [Exponential Backoff And Jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/). *aws.amazon.com*, March 2015. Archived at [perma.cc/R6MS-AZKH](https://perma.cc/R6MS-AZKH)
|
||||
[^10]: Marc Brooker. [What is Backoff For?](https://brooker.co.za/blog/2022/08/11/backoff.html) *brooker.co.za*, August 2022. Archived at [perma.cc/PW9N-55Q5](https://perma.cc/PW9N-55Q5)
|
||||
[^11]: Michael T. Nygard. [*Release It!*](https://learning.oreilly.com/library/view/release-it-2nd/9781680504552/), 2nd Edition. Pragmatic Bookshelf, January 2018. ISBN: 9781680502398
|
||||
[^12]: Frank Chen. [Slowing Down to Speed Up – Circuit Breakers for Slack’s CI/CD](https://slack.engineering/circuit-breakers/). *slack.engineering*, August 2022. Archived at [perma.cc/5FGS-ZPH3](https://perma.cc/5FGS-ZPH3)
|
||||
[^12]: Frank Chen. [Slowing Down to Speed Up – Circuit Breakers for Slack's CI/CD](https://slack.engineering/circuit-breakers/). *slack.engineering*, August 2022. Archived at [perma.cc/5FGS-ZPH3](https://perma.cc/5FGS-ZPH3)
|
||||
[^13]: Marc Brooker. [Fixing retries with token buckets and circuit breakers](https://brooker.co.za/blog/2022/02/28/retries.html). *brooker.co.za*, February 2022. Archived at [perma.cc/MD6N-GW26](https://perma.cc/MD6N-GW26)
|
||||
[^14]: David Yanacek. [Using load shedding to avoid overload](https://aws.amazon.com/builders-library/using-load-shedding-to-avoid-overload/). Amazon Builders’ Library, *aws.amazon.com*. Archived at [perma.cc/9SAW-68MP](https://perma.cc/9SAW-68MP)
|
||||
[^14]: David Yanacek. [Using load shedding to avoid overload](https://aws.amazon.com/builders-library/using-load-shedding-to-avoid-overload/). Amazon Builders' Library, *aws.amazon.com*. Archived at [perma.cc/9SAW-68MP](https://perma.cc/9SAW-68MP)
|
||||
[^15]: Matthew Sackman. [Pushing Back](https://wellquite.org/posts/lshift/pushing_back/). *wellquite.org*, May 2016. Archived at [perma.cc/3KCZ-RUFY](https://perma.cc/3KCZ-RUFY)
|
||||
[^16]: Dmitry Kopytkov and Patrick Lee. [Meet Bandaid, the Dropbox service proxy](https://dropbox.tech/infrastructure/meet-bandaid-the-dropbox-service-proxy). *dropbox.tech*, March 2018. Archived at [perma.cc/KUU6-YG4S](https://perma.cc/KUU6-YG4S)
|
||||
[^17]: Haryadi S. Gunawi, Riza O. Suminto, Russell Sears, Casey Golliher, Swaminathan Sundararaman, Xing Lin, Tim Emami, Weiguang Sheng, Nematollah Bidokhti, Caitie McCaffrey, Gary Grider, Parks M. Fields, Kevin Harms, Robert B. Ross, Andree Jacobson, Robert Ricci, Kirk Webb, Peter Alvaro, H. Birali Runesha, Mingzhe Hao, and Huaicheng Li. [Fail-Slow at Scale: Evidence of Hardware Performance Faults in Large Production Systems](https://www.usenix.org/system/files/conference/fast18/fast18-gunawi.pdf). At *16th USENIX Conference on File and Storage Technologies*, February 2018.
|
||||
[^18]: Marc Brooker. [Is the Mean Really Useless?](https://brooker.co.za/blog/2017/12/28/mean.html) *brooker.co.za*, December 2017. Archived at [perma.cc/U5AE-CVEM](https://perma.cc/U5AE-CVEM)
|
||||
[^19]: Giuseppe DeCandia, Deniz Hastorun, Madan Jampani, Gunavardhan Kakulapati, Avinash Lakshman, Alex Pilchin, Swaminathan Sivasubramanian, Peter Vosshall, and Werner Vogels. [Dynamo: Amazon’s Highly Available Key-Value Store](https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf). At *21st ACM Symposium on Operating Systems Principles* (SOSP), October 2007. [doi:10.1145/1294261.1294281](https://doi.org/10.1145/1294261.1294281)
|
||||
[^19]: Giuseppe DeCandia, Deniz Hastorun, Madan Jampani, Gunavardhan Kakulapati, Avinash Lakshman, Alex Pilchin, Swaminathan Sivasubramanian, Peter Vosshall, and Werner Vogels. [Dynamo: Amazon's Highly Available Key-Value Store](https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf). At *21st ACM Symposium on Operating Systems Principles* (SOSP), October 2007. [doi:10.1145/1294261.1294281](https://doi.org/10.1145/1294261.1294281)
|
||||
[^20]: Kathryn Whitenton. [The Need for Speed, 23 Years Later](https://www.nngroup.com/articles/the-need-for-speed/). *nngroup.com*, May 2020. Archived at [perma.cc/C4ER-LZYA](https://perma.cc/C4ER-LZYA)
|
||||
[^21]: Greg Linden. [Marissa Mayer at Web 2.0](https://glinden.blogspot.com/2006/11/marissa-mayer-at-web-20.html). *glinden.blogspot.com*, November 2005. Archived at [perma.cc/V7EA-3VXB](https://perma.cc/V7EA-3VXB)
|
||||
[^22]: Jake Brutlag. [Speed Matters for Google Web Search](https://services.google.com/fh/files/blogs/google_delayexp.pdf). *services.google.com*, June 2009. Archived at [perma.cc/BK7R-X7M2](https://perma.cc/BK7R-X7M2)
|
||||
@ -424,19 +422,19 @@ Akamai 最近的一项研究 [^24] 声称响应时间增加 100 毫秒使电子
|
||||
[^24]: Akamai Technologies, Inc. [The State of Online Retail Performance](https://web.archive.org/web/20210729180749/https%3A//www.akamai.com/us/en/multimedia/documents/report/akamai-state-of-online-retail-performance-spring-2017.pdf). *akamai.com*, April 2017. Archived at [perma.cc/UEK2-HYCS](https://perma.cc/UEK2-HYCS)
|
||||
[^25]: Xiao Bai, Ioannis Arapakis, B. Barla Cambazoglu, and Ana Freire. [Understanding and Leveraging the Impact of Response Latency on User Behaviour in Web Search](https://iarapakis.github.io/papers/TOIS17.pdf). *ACM Transactions on Information Systems*, volume 36, issue 2, article 21, April 2018. [doi:10.1145/3106372](https://doi.org/10.1145/3106372)
|
||||
[^26]: Jeffrey Dean and Luiz André Barroso. [The Tail at Scale](https://cacm.acm.org/research/the-tail-at-scale/). *Communications of the ACM*, volume 56, issue 2, pages 74–80, February 2013. [doi:10.1145/2408776.2408794](https://doi.org/10.1145/2408776.2408794)
|
||||
[^27]: Alex Hidalgo. [*Implementing Service Level Objectives: A Practical Guide to SLIs, SLOs, and Error Budgets*](https://www.oreilly.com/library/view/implementing-service-level/9781492076803/). O’Reilly Media, September 2020. ISBN: 1492076813
|
||||
[^27]: Alex Hidalgo. [*Implementing Service Level Objectives: A Practical Guide to SLIs, SLOs, and Error Budgets*](https://www.oreilly.com/library/view/implementing-service-level/9781492076803/). O'Reilly Media, September 2020. ISBN: 1492076813
|
||||
[^28]: Jeffrey C. Mogul and John Wilkes. [Nines are Not Enough: Meaningful Metrics for Clouds](https://research.google/pubs/pub48033/). At *17th Workshop on Hot Topics in Operating Systems* (HotOS), May 2019. [doi:10.1145/3317550.3321432](https://doi.org/10.1145/3317550.3321432)
|
||||
[^29]: Tamás Hauer, Philipp Hoffmann, John Lunney, Dan Ardelean, and Amer Diwan. [Meaningful Availability](https://www.usenix.org/conference/nsdi20/presentation/hauer). At *17th USENIX Symposium on Networked Systems Design and Implementation* (NSDI), February 2020.
|
||||
[^30]: Ted Dunning. [The t-digest: Efficient estimates of distributions](https://www.sciencedirect.com/science/article/pii/S2665963820300403). *Software Impacts*, volume 7, article 100049, February 2021. [doi:10.1016/j.simpa.2020.100049](https://doi.org/10.1016/j.simpa.2020.100049)
|
||||
[^31]: David Kohn. [How percentile approximation works (and why it’s more useful than averages)](https://www.timescale.com/blog/how-percentile-approximation-works-and-why-its-more-useful-than-averages/). *timescale.com*, September 2021. Archived at [perma.cc/3PDP-NR8B](https://perma.cc/3PDP-NR8B)
|
||||
[^31]: David Kohn. [How percentile approximation works (and why it's more useful than averages)](https://www.timescale.com/blog/how-percentile-approximation-works-and-why-its-more-useful-than-averages/). *timescale.com*, September 2021. Archived at [perma.cc/3PDP-NR8B](https://perma.cc/3PDP-NR8B)
|
||||
[^32]: Heinrich Hartmann and Theo Schlossnagle. [Circllhist — A Log-Linear Histogram Data Structure for IT Infrastructure Monitoring](https://arxiv.org/pdf/2001.06561.pdf). *arxiv.org*, January 2020.
|
||||
[^33]: Charles Masson, Jee E. Rim, and Homin K. Lee. [DDSketch: A Fast and Fully-Mergeable Quantile Sketch with Relative-Error Guarantees](https://www.vldb.org/pvldb/vol12/p2195-masson.pdf). *Proceedings of the VLDB Endowment*, volume 12, issue 12, pages 2195–2205, August 2019. [doi:10.14778/3352063.3352135](https://doi.org/10.14778/3352063.3352135)
|
||||
[^34]: Baron Schwartz. [Why Percentiles Don’t Work the Way You Think](https://orangematter.solarwinds.com/2016/11/18/why-percentiles-dont-work-the-way-you-think/). *solarwinds.com*, November 2016. Archived at [perma.cc/469T-6UGB](https://perma.cc/469T-6UGB)
|
||||
[^34]: Baron Schwartz. [Why Percentiles Don't Work the Way You Think](https://orangematter.solarwinds.com/2016/11/18/why-percentiles-dont-work-the-way-you-think/). *solarwinds.com*, November 2016. Archived at [perma.cc/469T-6UGB](https://perma.cc/469T-6UGB)
|
||||
[^35]: Walter L. Heimerdinger and Charles B. Weinstock. [A Conceptual Framework for System Fault Tolerance](https://resources.sei.cmu.edu/asset_files/TechnicalReport/1992_005_001_16112.pdf). Technical Report CMU/SEI-92-TR-033, Software Engineering Institute, Carnegie Mellon University, October 1992. Archived at [perma.cc/GD2V-DMJW](https://perma.cc/GD2V-DMJW)
|
||||
[^36]: Felix C. Gärtner. [Fundamentals of fault-tolerant distributed computing in asynchronous environments](https://dl.acm.org/doi/pdf/10.1145/311531.311532). *ACM Computing Surveys*, volume 31, issue 1, pages 1–26, March 1999. [doi:10.1145/311531.311532](https://doi.org/10.1145/311531.311532)
|
||||
[^37]: Algirdas Avižienis, Jean-Claude Laprie, Brian Randell, and Carl Landwehr. [Basic Concepts and Taxonomy of Dependable and Secure Computing](https://hdl.handle.net/1903/6459). *IEEE Transactions on Dependable and Secure Computing*, volume 1, issue 1, January 2004. [doi:10.1109/TDSC.2004.2](https://doi.org/10.1109/TDSC.2004.2)
|
||||
[^38]: Ding Yuan, Yu Luo, Xin Zhuang, Guilherme Renna Rodrigues, Xu Zhao, Yongle Zhang, Pranay U. Jain, and Michael Stumm. [Simple Testing Can Prevent Most Critical Failures: An Analysis of Production Failures in Distributed Data-Intensive Systems](https://www.usenix.org/system/files/conference/osdi14/osdi14-paper-yuan.pdf). At *11th USENIX Symposium on Operating Systems Design and Implementation* (OSDI), October 2014.
|
||||
[^39]: Casey Rosenthal and Nora Jones. [*Chaos Engineering*](https://learning.oreilly.com/library/view/chaos-engineering/9781492043850/). O’Reilly Media, April 2020. ISBN: 9781492043867
|
||||
[^39]: Casey Rosenthal and Nora Jones. [*Chaos Engineering*](https://learning.oreilly.com/library/view/chaos-engineering/9781492043850/). O'Reilly Media, April 2020. ISBN: 9781492043867
|
||||
[^40]: Eduardo Pinheiro, Wolf-Dietrich Weber, and Luiz Andre Barroso. [Failure Trends in a Large Disk Drive Population](https://www.usenix.org/legacy/events/fast07/tech/full_papers/pinheiro/pinheiro_old.pdf). At *5th USENIX Conference on File and Storage Technologies* (FAST), February 2007.
|
||||
[^41]: Bianca Schroeder and Garth A. Gibson. [Disk failures in the real world: What does an MTTF of 1,000,000 hours mean to you?](https://www.usenix.org/legacy/events/fast07/tech/schroeder/schroeder.pdf) At *5th USENIX Conference on File and Storage Technologies* (FAST), February 2007.
|
||||
[^42]: Andy Klein. [Backblaze Drive Stats for Q2 2021](https://www.backblaze.com/blog/backblaze-drive-stats-for-q2-2021/). *backblaze.com*, August 2021. Archived at [perma.cc/2943-UD5E](https://perma.cc/2943-UD5E)
|
||||
@ -446,7 +444,7 @@ Akamai 最近的一项研究 [^24] 声称响应时间增加 100 毫秒使电子
|
||||
[^46]: Jacob Alter, Ji Xue, Alma Dimnaku, and Evgenia Smirni. [SSD failures in the field: symptoms, causes, and prediction models](https://dl.acm.org/doi/pdf/10.1145/3295500.3356172). At *International Conference for High Performance Computing, Networking, Storage and Analysis* (SC), November 2019. [doi:10.1145/3295500.3356172](https://doi.org/10.1145/3295500.3356172)
|
||||
[^47]: Daniel Ford, François Labelle, Florentina I. Popovici, Murray Stokely, Van-Anh Truong, Luiz Barroso, Carrie Grimes, and Sean Quinlan. [Availability in Globally Distributed Storage Systems](https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Ford.pdf). At *9th USENIX Symposium on Operating Systems Design and Implementation* (OSDI), October 2010.
|
||||
[^48]: Kashi Venkatesh Vishwanath and Nachiappan Nagappan. [Characterizing Cloud Computing Hardware Reliability](https://www.microsoft.com/en-us/research/wp-content/uploads/2010/06/socc088-vishwanath.pdf). At *1st ACM Symposium on Cloud Computing* (SoCC), June 2010. [doi:10.1145/1807128.1807161](https://doi.org/10.1145/1807128.1807161)
|
||||
[^49]: Peter H. Hochschild, Paul Turner, Jeffrey C. Mogul, Rama Govindaraju, Parthasarathy Ranganathan, David E. Culler, and Amin Vahdat. [Cores that don’t count](https://sigops.org/s/conferences/hotos/2021/papers/hotos21-s01-hochschild.pdf). At *Workshop on Hot Topics in Operating Systems* (HotOS), June 2021. [doi:10.1145/3458336.3465297](https://doi.org/10.1145/3458336.3465297)
|
||||
[^49]: Peter H. Hochschild, Paul Turner, Jeffrey C. Mogul, Rama Govindaraju, Parthasarathy Ranganathan, David E. Culler, and Amin Vahdat. [Cores that don't count](https://sigops.org/s/conferences/hotos/2021/papers/hotos21-s01-hochschild.pdf). At *Workshop on Hot Topics in Operating Systems* (HotOS), June 2021. [doi:10.1145/3458336.3465297](https://doi.org/10.1145/3458336.3465297)
|
||||
[^50]: Harish Dattatraya Dixit, Sneha Pendharkar, Matt Beadon, Chris Mason, Tejasvi Chakravarthy, Bharath Muthiah, and Sriram Sankar. [Silent Data Corruptions at Scale](https://arxiv.org/abs/2102.11245). *arXiv:2102.11245*, February 2021.
|
||||
[^51]: Diogo Behrens, Marco Serafini, Sergei Arnautov, Flavio P. Junqueira, and Christof Fetzer. [Scalable Error Isolation for Distributed Systems](https://www.usenix.org/conference/nsdi15/technical-sessions/presentation/behrens). At *12th USENIX Symposium on Networked Systems Design and Implementation* (NSDI), May 2015.
|
||||
[^52]: Bianca Schroeder, Eduardo Pinheiro, and Wolf-Dietrich Weber. [DRAM Errors in the Wild: A Large-Scale Field Study](https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/35162.pdf). At *11th International Joint Conference on Measurement and Modeling of Computer Systems* (SIGMETRICS), June 2009. [doi:10.1145/1555349.1555372](https://doi.org/10.1145/1555349.1555372)
|
||||
@ -463,17 +461,17 @@ Akamai 最近的一项研究 [^24] 声称响应时间增加 100 毫秒使电子
|
||||
[^63]: Lorin Hochstein. [awesome limits](https://github.com/lorin/awesome-limits). *github.com*, November 2020. Archived at [perma.cc/3R5M-E5Q4](https://perma.cc/3R5M-E5Q4)
|
||||
[^64]: Caitie McCaffrey. [Clients Are Jerks: AKA How Halo 4 DoSed the Services at Launch & How We Survived](https://www.caitiem.com/2015/06/23/clients-are-jerks-aka-how-halo-4-dosed-the-services-at-launch-how-we-survived/). *caitiem.com*, June 2015. Archived at [perma.cc/MXX4-W373](https://perma.cc/MXX4-W373)
|
||||
[^65]: Lilia Tang, Chaitanya Bhandari, Yongle Zhang, Anna Karanika, Shuyang Ji, Indranil Gupta, and Tianyin Xu. [Fail through the Cracks: Cross-System Interaction Failures in Modern Cloud Systems](https://tianyin.github.io/pub/csi-failures.pdf). At *18th European Conference on Computer Systems* (EuroSys), May 2023. [doi:10.1145/3552326.3587448](https://doi.org/10.1145/3552326.3587448)
|
||||
[^66]: Mike Ulrich. [Addressing Cascading Failures](https://sre.google/sre-book/addressing-cascading-failures/). In Betsy Beyer, Jennifer Petoff, Chris Jones, and Niall Richard Murphy (ed). [*Site Reliability Engineering: How Google Runs Production Systems*](https://www.oreilly.com/library/view/site-reliability-engineering/9781491929117/). O’Reilly Media, 2016. ISBN: 9781491929124
|
||||
[^66]: Mike Ulrich. [Addressing Cascading Failures](https://sre.google/sre-book/addressing-cascading-failures/). In Betsy Beyer, Jennifer Petoff, Chris Jones, and Niall Richard Murphy (ed). [*Site Reliability Engineering: How Google Runs Production Systems*](https://www.oreilly.com/library/view/site-reliability-engineering/9781491929117/). O'Reilly Media, 2016. ISBN: 9781491929124
|
||||
[^67]: Harri Faßbender. [Cascading failures in large-scale distributed systems](https://blog.mi.hdm-stuttgart.de/index.php/2022/03/03/cascading-failures-in-large-scale-distributed-systems/). *blog.mi.hdm-stuttgart.de*, March 2022. Archived at [perma.cc/K7VY-YJRX](https://perma.cc/K7VY-YJRX)
|
||||
[^68]: Richard I. Cook. [How Complex Systems Fail](https://www.adaptivecapacitylabs.com/HowComplexSystemsFail.pdf). Cognitive Technologies Laboratory, April 2000. Archived at [perma.cc/RDS6-2YVA](https://perma.cc/RDS6-2YVA)
|
||||
[^69]: David D. Woods. [STELLA: Report from the SNAFUcatchers Workshop on Coping With Complexity](https://snafucatchers.github.io/). *snafucatchers.github.io*, March 2017. Archived at [archive.org](https://web.archive.org/web/20230306130131/https%3A//snafucatchers.github.io/)
|
||||
[^70]: David Oppenheimer, Archana Ganapathi, and David A. Patterson. [Why Do Internet Services Fail, and What Can Be Done About It?](https://static.usenix.org/events/usits03/tech/full_papers/oppenheimer/oppenheimer.pdf) At *4th USENIX Symposium on Internet Technologies and Systems* (USITS), March 2003.
|
||||
[^71]: Sidney Dekker. [*The Field Guide to Understanding ‘Human Error’, 3rd Edition*](https://learning.oreilly.com/library/view/the-field-guide/9781317031833/). CRC Press, November 2017. ISBN: 9781472439055
|
||||
[^71]: Sidney Dekker. [*The Field Guide to Understanding 'Human Error', 3rd Edition*](https://learning.oreilly.com/library/view/the-field-guide/9781317031833/). CRC Press, November 2017. ISBN: 9781472439055
|
||||
[^72]: Sidney Dekker. [*Drift into Failure: From Hunting Broken Components to Understanding Complex Systems*](https://www.taylorfrancis.com/books/mono/10.1201/9781315257396/drift-failure-sidney-dekker). CRC Press, 2011. ISBN: 9781315257396
|
||||
[^73]: John Allspaw. [Blameless PostMortems and a Just Culture](https://www.etsy.com/codeascraft/blameless-postmortems/). *etsy.com*, May 2012. Archived at [perma.cc/YMJ7-NTAP](https://perma.cc/YMJ7-NTAP)
|
||||
[^74]: Itzy Sabo. [Uptime Guarantees — A Pragmatic Perspective](https://world.hey.com/itzy/uptime-guarantees-a-pragmatic-perspective-736d7ea4). *world.hey.com*, March 2023. Archived at [perma.cc/F7TU-78JB](https://perma.cc/F7TU-78JB)
|
||||
[^75]: Michael Jurewitz. [The Human Impact of Bugs](http://jury.me/blog/2013/3/14/the-human-impact-of-bugs). *jury.me*, March 2013. Archived at [perma.cc/5KQ4-VDYL](https://perma.cc/5KQ4-VDYL)
|
||||
[^76]: Mark Halper. [How Software Bugs led to ‘One of the Greatest Miscarriages of Justice’ in British History](https://cacm.acm.org/news/how-software-bugs-led-to-one-of-the-greatest-miscarriages-of-justice-in-british-history/). *Communications of the ACM*, January 2025. [doi:10.1145/3703779](https://doi.org/10.1145/3703779)
|
||||
[^76]: Mark Halper. [How Software Bugs led to 'One of the Greatest Miscarriages of Justice' in British History](https://cacm.acm.org/news/how-software-bugs-led-to-one-of-the-greatest-miscarriages-of-justice-in-british-history/). *Communications of the ACM*, January 2025. [doi:10.1145/3703779](https://doi.org/10.1145/3703779)
|
||||
[^77]: Nicholas Bohm, James Christie, Peter Bernard Ladkin, Bev Littlewood, Paul Marshall, Stephen Mason, Martin Newby, Steven J. Murdoch, Harold Thimbleby, and Martyn Thomas. [The legal rule that computers are presumed to be operating correctly – unforeseen and unjust consequences](https://www.benthamsgaze.org/wp-content/uploads/2022/06/briefing-presumption-that-computers-are-reliable.pdf). Briefing note, *benthamsgaze.org*, June 2022. Archived at [perma.cc/WQ6X-TMW4](https://perma.cc/WQ6X-TMW4)
|
||||
[^78]: Dan McKinley. [Choose Boring Technology](https://mcfunley.com/choose-boring-technology). *mcfunley.com*, March 2015. Archived at [perma.cc/7QW7-J4YP](https://perma.cc/7QW7-J4YP)
|
||||
[^79]: Andy Warfield. [Building and operating a pretty big storage system called S3](https://www.allthingsdistributed.com/2023/07/building-and-operating-a-pretty-big-storage-system.html). *allthingsdistributed.com*, July 2023. Archived at [perma.cc/7LPK-TP7V](https://perma.cc/7LPK-TP7V)
|
||||
@ -481,7 +479,7 @@ Akamai 最近的一项研究 [^24] 声称响应时间增加 100 毫秒使电子
|
||||
[^81]: Ben Stopford. [Shared Nothing vs. Shared Disk Architectures: An Independent View](http://www.benstopford.com/2009/11/24/understanding-the-shared-nothing-architecture/). *benstopford.com*, November 2009. Archived at [perma.cc/7BXH-EDUR](https://perma.cc/7BXH-EDUR)
|
||||
[^82]: Michael Stonebraker. [The Case for Shared Nothing](https://dsf.berkeley.edu/papers/hpts85-nothing.pdf). *IEEE Database Engineering Bulletin*, volume 9, issue 1, pages 4–9, March 1986.
|
||||
[^83]: Panagiotis Antonopoulos, Alex Budovski, Cristian Diaconu, Alejandro Hernandez Saenz, Jack Hu, Hanuma Kodavalla, Donald Kossmann, Sandeep Lingam, Umar Farooq Minhas, Naveen Prakash, Vijendra Purohit, Hugh Qu, Chaitanya Sreenivas Ravella, Krystyna Reisteter, Sheetal Shrotri, Dixin Tang, and Vikram Wakade. [Socrates: The New SQL Server in the Cloud](https://www.microsoft.com/en-us/research/uploads/prod/2019/05/socrates.pdf). At *ACM International Conference on Management of Data* (SIGMOD), pages 1743–1756, June 2019. [doi:10.1145/3299869.3314047](https://doi.org/10.1145/3299869.3314047)
|
||||
[^84]: Sam Newman. [*Building Microservices*, second edition](https://www.oreilly.com/library/view/building-microservices-2nd/9781492034018/). O’Reilly Media, 2021. ISBN: 9781492034025
|
||||
[^84]: Sam Newman. [*Building Microservices*, second edition](https://www.oreilly.com/library/view/building-microservices-2nd/9781492034018/). O'Reilly Media, 2021. ISBN: 9781492034025
|
||||
[^85]: Nathan Ensmenger. [When Good Software Goes Bad: The Surprising Durability of an Ephemeral Technology](https://themaintainers.wpengine.com/wp-content/uploads/2021/04/ensmenger-maintainers-v2.pdf). At *The Maintainers Conference*, April 2016. Archived at [perma.cc/ZXT4-HGZB](https://perma.cc/ZXT4-HGZB)
|
||||
[^86]: Robert L. Glass. [*Facts and Fallacies of Software Engineering*](https://learning.oreilly.com/library/view/facts-and-fallacies/0321117425/). Addison-Wesley Professional, October 2002. ISBN: 9780321117427
|
||||
[^87]: Marianne Bellotti. [*Kill It with Fire*](https://learning.oreilly.com/library/view/kill-it-with/9781098128883/). No Starch Press, April 2021. ISBN: 9781718501188
|
||||
@ -495,4 +493,4 @@ Akamai 最近的一项研究 [^24] 声称响应时间增加 100 毫秒使电子
|
||||
[^95]: Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. [*Design Patterns: Elements of Reusable Object-Oriented Software*](https://learning.oreilly.com/library/view/design-patterns-elements/0201633612/). Addison-Wesley Professional, October 1994. ISBN: 9780201633610
|
||||
[^96]: Eric Evans. [*Domain-Driven Design: Tackling Complexity in the Heart of Software*](https://learning.oreilly.com/library/view/domain-driven-design-tackling/0321125215/). Addison-Wesley Professional, August 2003. ISBN: 9780321125217
|
||||
[^97]: Hongyu Pei Breivold, Ivica Crnkovic, and Peter J. Eriksson. [Analyzing Software Evolvability](https://www.es.mdh.se/pdf_publications/1251.pdf). at *32nd Annual IEEE International Computer Software and Applications Conference* (COMPSAC), July 2008. [doi:10.1109/COMPSAC.2008.50](https://doi.org/10.1109/COMPSAC.2008.50)
|
||||
[^98]: Enrico Zaninotto. [From X programming to the X organisation](https://martinfowler.com/articles/zaninotto.pdf). At *XP Conference*, May 2002. Archived at [perma.cc/R9AR-QCKZ](https://perma.cc/R9AR-QCKZ)
|
||||
[^98]: Enrico Zaninotto. [From X programming to the X organisation](https://martinfowler.com/articles/zaninotto.pdf). At *XP Conference*, May 2002. Archived at [perma.cc/R9AR-QCKZ](https://perma.cc/R9AR-QCKZ)
|
File diff suppressed because it is too large
Load Diff
@ -6,20 +6,20 @@ breadcrumbs: false
|
||||
|
||||

|
||||
|
||||
> *生活中的一大痛苦是,每个人给事物起的名字都有一点点不对。这让世界上的一切都比换个名字后更难理解。计算机主要并不是在算术意义上进行计算。[...] 它们主要是归档系统。*
|
||||
> *生活的苦恼之一是,每个人对事物的命名都有些偏差。这让我们理解世界变得比本该有的样子困难一些,要是命名方式不同就好了。计算机的主要功能并不是传统意义上的计算,比如算术运算。[……] 它们主要是归档系统。*
|
||||
>
|
||||
> [Richard Feynman](https://www.youtube.com/watch?v=EKWGGDXe5MA&t=296s),
|
||||
> *特立独行的思维* 研讨会(1985)
|
||||
> [理查德·费曼](https://www.youtube.com/watch?v=EKWGGDXe5MA&t=296s),
|
||||
> *特立独行的思考* 研讨会(1985)
|
||||
|
||||
在最基础的层面上,数据库需要做两件事:当你给它一些数据时,它应该存储这些数据,当你稍后再次询问时,它应该把数据返回给你。
|
||||
在最基础的层面上,数据库需要做两件事:当你给它一些数据时,它应该存储这些数据;当你之后再询问时,它应该把数据返回给你。
|
||||
|
||||
在 [第三章](/ch3#ch_datamodels) 中,我们讨论了数据模型和查询语言——即你向数据库提供数据的格式,以及稍后再次请求数据的接口。在本章中,我们从数据库的角度讨论同样的问题:数据库如何存储你提供给它的数据,以及当你请求时如何再次找到数据。
|
||||
在 [第 3 章](/ch3#ch_datamodels) 中,我们讨论了数据模型和查询语言 —— 即你向数据库提供数据的格式,以及之后再次请求数据的接口。在本章中,我们从数据库的角度讨论同样的问题:数据库如何存储你提供的数据,以及当你请求时如何再次找到这些数据。
|
||||
|
||||
作为应用程序开发人员,为什么要关心数据库内部如何处理存储和检索?你可能不会从头开始实现自己的存储引擎,但你 *确实* 需要从众多可用的存储引擎中选择一个适合你应用程序的。为了配置存储引擎在你的工作负载类型上表现良好,你需要对存储引擎在底层做什么有个大致的了解。
|
||||
作为应用开发者,你为什么要关心数据库内部如何处理存储和检索?你可能不会从头开始实现自己的存储引擎,但你 *确实* 需要从众多可用的存储引擎中选择一个适合你应用的。为了让存储引擎在你的工作负载类型上表现良好,你需要对存储引擎在底层做了什么有个大致的了解。
|
||||
|
||||
特别是,针对事务性工作负载(OLTP)优化的存储引擎与针对分析优化的存储引擎之间存在很大差异(我们在 ["分析与操作系统"](/ch1#sec_introduction_analytics) 中介绍了这种区别)。本章首先研究两种 OLTP 存储引擎系列:写出不可变数据文件的 *日志结构* 存储引擎,以及像 *B 树* 这样就地更新数据的存储引擎。这些结构既用于键值存储,也用于二级索引。
|
||||
特别是,针对事务型工作负载(OLTP)优化的存储引擎和针对分析型工作负载优化的存储引擎之间存在巨大差异(我们在 ["分析型与事务型系统"](/ch1#sec_introduction_analytics) 中介绍了这种区别)。本章首先研究两种用于 OLTP 的存储引擎家族:写入不可变数据文件的 *日志结构* 存储引擎,以及像 *B 树* 这样就地更新数据的存储引擎。这些结构既用于键值存储,也用于二级索引。
|
||||
|
||||
稍后在 ["分析型数据存储"](/ch4#sec_storage_analytics) 中,我们将讨论一系列针对分析优化的存储引擎,在 ["多维和全文索引"](/ch4#sec_storage_multidimensional) 中,我们将简要介绍用于更高级查询(如文本检索)的索引。
|
||||
随后在 ["分析型数据存储"](#sec_storage_analytics) 中,我们将讨论一系列针对分析优化的存储引擎;在 ["多维索引与全文索引"](#sec_storage_multidimensional) 中,我们将简要介绍用于更高级查询(如文本检索)的索引。
|
||||
|
||||
## OLTP 系统的存储与索引 {#sec_storage_oltp}
|
||||
|
||||
@ -37,9 +37,9 @@ db_get () {
|
||||
}
|
||||
```
|
||||
|
||||
这两个函数实现了一个键值存储。你可以调用 `db_set key value`,它会在数据库中存储 `key` 和 `value`。键和值可以是(几乎)任何你喜欢的东西——例如,值可以是 JSON 文档。然后你可以调用 `db_get key`,它会查找与该特定键关联的最新值并返回它。
|
||||
这两个函数实现了一个键值存储。你可以调用 `db_set key value`,它将在数据库中存储 `key` 和 `value`。键和值可以是(几乎)任何你喜欢的内容 —— 例如,值可以是一个 JSON 文档。然后你可以调用 `db_get key`,它会查找与该特定键关联的最新值并返回它。
|
||||
|
||||
它确实有效:
|
||||
它确实能工作:
|
||||
|
||||
```bash
|
||||
$ db_set 12 '{"name":"London","attractions":["Big Ben","London Eye"]}'
|
||||
@ -50,7 +50,7 @@ $ db_get 42
|
||||
{"name":"San Francisco","attractions":["Golden Gate Bridge"]}
|
||||
```
|
||||
|
||||
存储格式非常简单:一个文本文件,每行包含一个键值对,用逗号分隔(大致类似于 CSV 文件,忽略转义问题)。每次调用 `db_set` 都会追加到文件末尾。如果你多次更新一个键,值的旧版本不会被覆盖——你需要查看文件中键的最后一次出现来找到最新值(因此 `db_get` 中有 `tail -n 1`):
|
||||
存储格式非常简单:一个文本文件,每行包含一个键值对,用逗号分隔(大致类似 CSV 文件,忽略转义问题)。每次调用 `db_set` 都会追加到文件末尾。如果你多次更新一个键,旧版本的值不会被覆盖 —— 你需要查看文件中键的最后一次出现来找到最新值(因此 `db_get` 中使用了 `tail -n 1`):
|
||||
|
||||
```bash
|
||||
$ db_set 42 '{"name":"San Francisco","attractions":["Exploratorium"]}'
|
||||
@ -65,303 +65,303 @@ $ cat database
|
||||
|
||||
```
|
||||
|
||||
对于如此简单的东西,`db_set` 函数实际上具有相当好的性能,因为追加到文件通常非常高效。与 `db_set` 所做的类似,许多数据库内部使用 *日志*,这是一个仅追加的数据文件。真正的数据库需要处理更多问题(如处理并发写入、回收磁盘空间以免日志无限增长,以及在崩溃恢复时处理部分写入的记录),但基本原理是相同的。日志非常有用,我们将在本书中多次遇到它们。
|
||||
对于如此简单的实现,`db_set` 函数实际上有相当好的性能,因为追加到文件通常非常高效。与 `db_set` 所做的类似,许多数据库内部使用 *日志*,这是一个仅追加的数据文件。真正的数据库有更多问题要处理(如处理并发写入、回收磁盘空间以防日志无限增长,以及从崩溃中恢复时处理部分写入的记录),但基本原理是相同的。日志非常有用,我们将在本书中多次遇到它们。
|
||||
|
||||
---------
|
||||
|
||||
> [!NOTE]
|
||||
> *日志* 这个词通常用于指应用程序日志,其中应用程序输出描述正在发生的事情的文本。在本书中,*日志* 用于更一般的意义:磁盘上仅追加的记录序列。它不必是人类可读的;它可能是二进制的,仅供数据库系统内部使用。
|
||||
> *日志* 这个词通常用于指应用程序日志,应用程序输出描述正在发生什么的文本。在本书中,*日志* 用于更一般的含义:磁盘上仅追加的记录序列。它不一定是人类可读的;它可能是二进制的,仅供数据库系统内部使用。
|
||||
|
||||
--------
|
||||
|
||||
|
||||
另一方面,如果你的数据库中有大量记录,`db_get` 函数的性能会很糟糕。每次你想查找一个键时,`db_get` 必须从头到尾扫描整个数据库文件,寻找键的出现。用算法术语来说,查找的成本是 *O*(*n*):如果你将数据库中的记录数 *n* 翻倍,查找需要两倍的时间。这不好。
|
||||
另一方面,如果你的数据库中有大量记录,`db_get` 函数的性能会很糟糕。每次你想查找一个键时,`db_get` 必须从头到尾扫描整个数据库文件,寻找该键的出现。用算法术语来说,查找的成本是 *O*(*n*):如果你的数据库中的记录数 *n* 翻倍,查找时间也会翻倍。这并不好。
|
||||
|
||||
为了高效地找到数据库中特定键的值,我们需要一种不同的数据结构:*索引*。在本章中,我们将研究一系列索引结构并比较它们;总体思路是以特定方式(例如,按某个键排序)构建数据,使定位所需数据更快。如果你想以几种不同的方式搜索相同的数据,你可能需要在数据的不同部分上建立几个不同的索引。
|
||||
为了高效地找到数据库中特定键的值,我们需要一个不同的数据结构:*索引*。在本章中,我们将研究一系列索引结构并了解它们的比较;一般思想是以特定方式(例如,按某个键排序)构建数据,使定位所需数据更快。如果你想以几种不同的方式搜索相同的数据,你可能需要在数据的不同部分上建立几个不同的索引。
|
||||
|
||||
索引是从主数据派生的 *额外* 结构。许多数据库允许你添加和删除索引,这不会影响数据库的内容;它只影响查询的性能。维护额外的结构会产生开销,特别是在写入时。对于写入,很难超越简单追加到文件的性能,因为这是最简单的写入操作。任何类型的索引通常都会减慢写入速度,因为每次写入数据时也需要更新索引。
|
||||
索引是从主数据衍生出的 *额外* 结构。许多数据库允许你添加和删除索引,这不会影响数据库的内容;它只影响查询的性能。维护额外的结构会产生开销,特别是在写入时。对于写入,很难超越简单地追加到文件的性能,因为这是最简单的写入操作。任何类型的索引通常都会减慢写入速度,因为每次写入数据时也需要更新索引。
|
||||
|
||||
这是存储系统中的一个重要权衡:精心选择的索引可以加速读查询,但每个索引都会消耗额外的磁盘空间并减慢写入速度,有时会大幅减慢 [^1]。因此,数据库通常不会默认为所有内容建立索引,而是需要你——编写应用程序或管理数据库的人——使用你对应用程序典型查询模式的了解来手动选择索引。然后你可以选择为你的应用程序带来最大收益的索引,而不会在写入时引入超过必要的开销。
|
||||
这是存储系统中的一个重要权衡:精心选择的索引加快了读查询速度,但每个索引都会消耗额外的磁盘空间并减慢写入速度,有时会大幅减慢 [^1]。因此,数据库通常不会默认为所有内容建立索引,而是要求你 —— 编写应用程序或管理数据库的人 —— 使用你对应用程序典型查询模式的了解来手动选择索引。然后你可以选择为你的应用程序带来最大收益的索引,而不会引入超过必要的写入开销。
|
||||
|
||||
### 日志结构存储 {#sec_storage_log_structured}
|
||||
|
||||
首先,让我们假设你想继续将数据存储在 `db_set` 写入的仅追加文件中,只是想加速读取。你可以通过在内存中保留一个哈希映射来做到这一点,其中每个键都映射到文件中可以找到该键最新值的字节偏移量,如 [图 4-1](/ch4#fig_storage_csv_hash_index) 所示。
|
||||
首先,让我们假设你想继续将数据存储在 `db_set` 写入的仅追加文件中,你只是想加快读取速度。一种方法是在内存中保留一个哈希映射,其中每个键都映射到文件中可以找到该键最新值的字节偏移量,如 [图 4-1](#fig_storage_csv_hash_index) 所示。
|
||||
|
||||
{{< figure src="/fig/ddia_0401.png" id="fig_storage_csv_hash_index" caption="图 4-1. 以类似 CSV 格式存储键值对日志,使用内存中的哈希映射进行索引。" class="w-full my-4" >}}
|
||||
{{< figure src="/fig/ddia_0401.png" id="fig_storage_csv_hash_index" caption="图 4-1. 以类似 CSV 格式存储键值对日志,使用内存哈希映射建立索引。" class="w-full my-4" >}}
|
||||
|
||||
每当你向文件追加新的键值对时,你也会更新哈希映射以反映刚刚写入的数据的偏移量。当你想查找一个值时,你使用哈希映射找到日志文件中的偏移量,寻址到该位置,然后读取值。如果数据文件的该部分已经在文件系统缓存中,读取根本不需要任何磁盘 I/O。
|
||||
每当你向文件追加新的键值对时,你也会更新哈希映射以反映刚刚写入数据的偏移量。当你想查找一个值时,你使用哈希映射找到日志文件中的偏移量,寻找到该位置,然后读取值。如果数据文件的那部分已经在文件系统缓存中,读取根本不需要任何磁盘 I/O。
|
||||
|
||||
这种方法要快得多,但仍然存在几个问题:
|
||||
这种方法速度更快,但仍然存在几个问题:
|
||||
|
||||
* 你永远不会释放被覆盖的旧日志条目占用的磁盘空间;如果你持续写入数据库,可能会耗尽磁盘空间。
|
||||
* 哈希映射不是持久化的,所以当你重启数据库时必须重建它——例如,通过扫描整个日志文件来找到每个键的最新字节偏移量。如果你有大量数据,这会使重启变慢。
|
||||
* 哈希表必须适合内存。原则上,你可以在磁盘上维护哈希表,但不幸的是很难使磁盘上的哈希映射表现良好。它需要大量的随机访问 I/O,当它变满时扩展成本很高,哈希冲突需要繁琐的逻辑 [^2]。
|
||||
* 范围查询效率不高。例如,你不能轻松扫描 `10000` 和 `19999` 之间的所有键——你必须在哈希映射中单独查找每个键。
|
||||
* 你永远不会释放被覆盖的旧日志条目占用的磁盘空间;如果你不断写入数据库,可能会耗尽磁盘空间。
|
||||
* 哈希映射不是持久化的,所以当你重启数据库时必须重建它 —— 例如,通过扫描整个日志文件来找到每个键的最新字节偏移量。如果你有大量数据,这会使重启变慢。
|
||||
* 哈希表必须适合内存。原则上,你可以在磁盘上维护哈希表,但不幸的是,很难让磁盘上的哈希映射表现良好。它需要大量的随机访问 I/O,当它变满时扩展成本高昂,哈希冲突需要复杂的逻辑 [^2]。
|
||||
* 范围查询效率不高。例如,你不能轻松扫描 `10000` 和 `19999` 之间的所有键 —— 你必须在哈希映射中单独查找每个键。
|
||||
|
||||
#### SSTable 文件格式 {#the-sstable-file-format}
|
||||
|
||||
实际上,哈希表很少用于数据库索引,相反,保持数据按 *键排序* 的结构更为常见 [^3]。这种结构的一个例子是 *排序字符串表*,简称 *SSTable*,如 [图 4-2](/ch4#fig_storage_sstable_index) 所示。这种文件格式也存储键值对,但它确保它们按键排序,每个键在文件中只出现一次。
|
||||
实际上,哈希表很少用于数据库索引,相反,保持数据 *按键排序* 的结构更为常见 [^3]。这种结构的一个例子是 *排序字符串表*(*Sorted String Table*),简称 *SSTable*,如 [图 4-2](#fig_storage_sstable_index) 所示。这种文件格式也存储键值对,但它确保它们按键排序,每个键在文件中只出现一次。
|
||||
|
||||
{{< figure src="/fig/ddia_0402.png" id="fig_storage_sstable_index" caption="图 4-2. 带有稀疏索引的 SSTable,允许查询跳转到正确的块。" class="w-full my-4" >}}
|
||||
|
||||
现在你不需要在内存中保留所有键:你可以将 SSTable 中的键值对分组到几千字节的 *块* 中,然后在索引中存储每个块的第一个键。这种只存储部分键的索引称为 *稀疏* 索引。该索引存储在 SSTable 的单独部分中,例如使用不可变 B 树、trie 或其他允许查询快速查找特定键的数据结构 [^4]。
|
||||
现在你不需要在内存中保留所有键:你可以将 SSTable 中的键值对分组为几千字节的 *块*,然后在索引中存储每个块的第一个键。这种只存储部分键的索引称为 *稀疏* 索引。这个索引存储在 SSTable 的单独部分,例如使用不可变 B 树、字典树或其他允许查询快速查找特定键的数据结构 [^4]。
|
||||
|
||||
例如,在 [图 4-2](/ch4#fig_storage_sstable_index) 中,一个块的第一个键是 `handbag`,下一个块的第一个键是 `handsome`。现在假设你要查找键 `handiwork`,它不出现在稀疏索引中。由于排序,你知道 `handiwork` 必须出现在 `handbag` 和 `handsome` 之间。这意味着你可以寻址到 `handbag` 的偏移量,并从那里扫描文件,直到找到 `handiwork`(或没有,如果键不在文件中)。几千字节的块可以非常快速地扫描。
|
||||
例如,在 [图 4-2](#fig_storage_sstable_index) 中,一个块的第一个键是 `handbag`,下一个块的第一个键是 `handsome`。现在假设你要查找键 `handiwork`,它没有出现在稀疏索引中。由于排序,你知道 `handiwork` 必须出现在 `handbag` 和 `handsome` 之间。这意味着你可以寻找到 `handbag` 的偏移量,然后从那里扫描文件,直到找到 `handiwork`(或没有,如果该键不在文件中)。几千字节的块可以非常快速地扫描。
|
||||
|
||||
此外,每个记录块可以被压缩(如 [图 4-2](/ch4#fig_storage_sstable_index) 中的阴影区域所示)。除了节省磁盘空间外,压缩还减少了 I/O 带宽的使用,代价是使用更多的 CPU 时间。
|
||||
此外,每个记录块都可以压缩(在 [图 4-2](#fig_storage_sstable_index) 中用阴影区域表示)。除了节省磁盘空间外,压缩还减少了 I/O 带宽使用,代价是使用更多一点的 CPU 时间。
|
||||
|
||||
#### 构建和合并 SSTable {#constructing-and-merging-sstables}
|
||||
|
||||
SSTable 文件格式比仅追加日志更适合读取,但它使写入更加困难。我们不能简单地追加到末尾,因为那样文件将不再排序(除非键恰好按升序写入)。如果每次在中间某处插入键时都必须重写整个 SSTable,写入将变得太昂贵。
|
||||
SSTable 文件格式在读取方面比仅追加日志更好,但它使写入更加困难。我们不能简单地追加到末尾,因为那样文件就不再排序了(除非键恰好按升序写入)。如果我们每次在中间某处插入键时都必须重写整个 SSTable,写入将变得太昂贵。
|
||||
|
||||
我们可以用 *日志结构* 方法解决这个问题,这是仅追加日志和排序文件之间的混合:
|
||||
|
||||
1. 当写入到来时,将其添加到内存中的有序映射数据结构,如红黑树、跳表 [^5] 或 trie [^6]。使用这些数据结构,你可以按任何顺序插入键,高效地查找它们,并按排序顺序读回它们。这个内存中的数据结构称为 *内存表*。
|
||||
2. 当内存表变得大于某个阈值——通常是几兆字节——将其按排序顺序作为 SSTable 文件写入磁盘。我们将这个新的 SSTable 文件称为数据库的最新 *段*,它与旧段一起作为单独的文件存储。每个段都有其内容的单独索引。当新段被写入磁盘时,数据库可以继续写入新的内存表实例,当 SSTable 写入完成时,旧内存表的内存被释放。
|
||||
3. 为了读取某个键的值,首先尝试在内存表和最新的磁盘段中查找键。如果不在那里,查看下一个较旧的段,等等,直到找到键或到达最旧的段。如果键不出现在任何段中,它就不存在于数据库中。
|
||||
4. 不时地,在后台运行合并和压缩过程来合并段文件并丢弃被覆盖或删除的值。
|
||||
1. 当写入操作到来时,将其添加到内存中的有序映射数据结构中,例如红黑树、跳表 [^5] 或字典树 [^6]。使用这些数据结构,你可以按任意顺序插入键,高效地查找它们,并按排序顺序读回它们。这个内存数据结构称为 *内存表*(*memtable*)。
|
||||
2. 当内存表变得大于某个阈值(通常是几兆字节)时,将其按排序顺序作为 SSTable 文件写入磁盘。我们将这个新的 SSTable 文件称为数据库的最新 *段*,它与旧段一起作为单独的文件存储。每个段都有自己内容的单独索引。当新段被写入磁盘时,数据库可以继续写入新的内存表实例,当 SSTable 写入完成时,旧内存表的内存被释放。
|
||||
3. 为了读取某个键的值,首先尝试在内存表和最新的磁盘段中找到该键。如果没有找到,就在下一个较旧的段中查找,依此类推,直到找到键或到达最旧的段。如果键没有出现在任何段中,则它不存在于数据库中。
|
||||
4. 不时地在后台运行合并和压实过程,以合并段文件并丢弃被覆盖或删除的值。
|
||||
|
||||
合并段的工作类似于 *归并排序* 算法 [^5]。该过程如 [图 4-3](/ch4#fig_storage_sstable_merging) 所示:并排开始读取输入文件,查看每个文件中的第一个键,将最低键(根据排序顺序)复制到输出文件,然后重复。如果同一个键出现在多个输入文件中,只保留较新的值。这产生了一个新的合并段文件,也按键排序,每个键一个值,并且使用最少的内存,因为我们可以一次迭代一个键的 SSTable。
|
||||
合并段的工作方式类似于 *归并排序* 算法 [^5]。该过程如 [图 4-3](#fig_storage_sstable_merging) 所示:并排开始读取输入文件,查看每个文件中的第一个键,将最低的键(根据排序顺序)复制到输出文件,然后重复。如果同一个键出现在多个输入文件中,只保留较新的值。这会产生一个新的合并段文件,也按键排序,每个键只有一个值,并且它使用最少的内存,因为我们可以一次遍历一个键的 SSTable。
|
||||
|
||||
{{< figure src="/fig/ddia_0403.png" id="fig_storage_sstable_merging" caption="图 4-3. 合并几个 SSTable 段,只保留每个键的最新值。" class="w-full my-4" >}}
|
||||
{{< figure src="/fig/ddia_0403.png" id="fig_storage_sstable_merging" caption="图 4-3. 合并多个 SSTable 段,仅保留每个键的最新值。" class="w-full my-4" >}}
|
||||
|
||||
为了确保内存表中的数据在数据库崩溃时不会丢失,存储引擎在磁盘上保留一个单独的日志,每次写入都立即追加到该日志。此日志不按键排序,但这并不重要,因为它的唯一目的是在崩溃后恢复内存表。每次内存表被写出到 SSTable 后,日志的相应部分可以被丢弃。
|
||||
为了确保数据库崩溃时内存表中的数据不会丢失,存储引擎在磁盘上保留一个单独的日志,每次写入都会立即追加到该日志中。此日志不按键排序,但这无关紧要,因为它的唯一目的是在崩溃后恢复内存表。每次内存表被写出到 SSTable 后,日志的相应部分就可以丢弃。
|
||||
|
||||
如果你想删除一个键及其关联的值,你必须向数据文件追加一个称为 *墓碑* 的特殊删除记录。当日志段合并时,墓碑告诉合并过程丢弃已删除键的任何先前值。一旦墓碑被合并到最旧的段中,它就可以被丢弃。
|
||||
如果你想删除一个键及其关联的值,你必须向数据文件追加一个称为 *墓碑*(*tombstone*)的特殊删除记录。当日志段合并时,墓碑告诉合并过程丢弃已删除键的任何先前值。一旦墓碑合并到最旧的段中,它就可以被丢弃。
|
||||
|
||||
这里描述的算法基本上就是 RocksDB [^7]、Cassandra、Scylla 和 HBase [^8] 中使用的算法,所有这些都受到 Google 的 Bigtable 论文 [^9] 的启发(该论文引入了术语 *SSTable* 和 *memtable*)。
|
||||
这里描述的算法本质上就是 RocksDB [^7]、Cassandra、Scylla 和 HBase [^8] 中使用的算法,它们都受到 Google 的 Bigtable 论文 [^9] 的启发(该论文引入了 *SSTable* 和 *memtable* 这两个术语)。
|
||||
|
||||
该算法最初于 1996 年以 *日志结构合并树* 或 *LSM 树* 的名称发表 [^10],建立在早期日志结构文件系统工作的基础上 [^11]。因此,基于合并和压缩排序文件原理的存储引擎通常称为 *LSM 存储引擎*。
|
||||
该算法最初于 1996 年以 *日志结构合并树*(*Log-Structured Merge-Tree*)或 *LSM 树*(*LSM-Tree*)[^10] 的名称发布,建立在早期日志结构文件系统工作的基础上 [^11]。因此,基于合并和压实排序文件原理的存储引擎通常被称为 *LSM 存储引擎*。
|
||||
|
||||
在 LSM 存储引擎中,段文件是一次写入的(通过写出内存表或通过合并一些现有段),此后它是不可变的。段的合并和压缩可以在后台线程中完成,当它进行时,我们仍然可以使用旧段文件继续提供读取服务。当合并过程完成时,我们将读取请求切换到使用新的合并段而不是旧段,然后可以删除旧段文件。
|
||||
在 LSM 存储引擎中,段文件是一次性写入的(通过写出内存表或合并一些现有段),此后它是不可变的。段的合并和压实可以在后台线程中完成,当它进行时,我们仍然可以使用旧的段文件继续提供读取服务。当合并过程完成时,我们将读取请求切换到使用新的合并段而不是旧段,然后可以删除旧的段文件。
|
||||
|
||||
段文件不一定必须存储在本地磁盘上:它们也非常适合写入对象存储。例如,SlateDB 和 Delta Lake [^12] 采用了这种方法。
|
||||
|
||||
拥有不可变段文件也简化了崩溃恢复:如果在写出内存表或合并段时发生崩溃,数据库可以删除未完成的 SSTable 并重新开始。持久化写入到内存表的日志如果在写入记录的中途发生崩溃,或者磁盘已满,可能包含不完整的记录;这些通常通过在日志中包含校验和来检测,并丢弃损坏或不完整的日志条目。我们将在 [第 8 章](/ch8#ch_transactions) 中更多地讨论持久性和崩溃恢复。
|
||||
具有不可变段文件也简化了崩溃恢复:如果在写出内存表或合并段时发生崩溃,数据库可以删除未完成的 SSTable 并重新开始。将写入持久化到内存表的日志如果在写入记录的过程中发生崩溃,或者磁盘已满,可能包含不完整的记录;这些通常通过在日志中包含校验和来检测,并丢弃损坏或不完整的日志条目。我们将在 [第 8 章](/ch8#ch_transactions) 中更多地讨论持久性和崩溃恢复。
|
||||
|
||||
#### 布隆过滤器 {#bloom-filters}
|
||||
|
||||
使用 LSM 存储,读取很久以前最后更新或不存在的键可能会很慢,因为存储引擎需要检查几个段文件。为了加速此类读取,LSM 存储引擎通常在每个段中包含一个 *布隆过滤器* [^13],它提供了一种快速但近似的方法来检查特定键是否出现在特定 SSTable 中。
|
||||
使用 LSM 存储,读取很久以前更新的键或不存在的键可能会很慢,因为存储引擎需要检查多个段文件。为了加快此类读取,LSM 存储引擎通常在每个段中包含一个 *布隆过滤器*(*Bloom filter*)[^13],它提供了一种快速但近似的方法来检查特定键是否出现在特定 SSTable 中。
|
||||
|
||||
[图 4-4](/ch4#fig_storage_bloom) 显示了一个包含两个键和 16 位的布隆过滤器示例(实际上,它会包含更多键和更多位)。对于 SSTable 中的每个键,我们计算一个哈希函数,生成一组数字,然后将其解释为位数组的索引 [^14]。我们将对应于这些索引的位设置为 1,其余保留为 0。例如,键 `handbag` 哈希到数字(2、9、4),所以我们将第 2、9 和 4 位设置为 1。然后将位图作为 SSTable 的一部分存储,与键的稀疏索引一起。这需要一些额外的空间,但与 SSTable 的其余部分相比,布隆过滤器通常很小。
|
||||
[图 4-4](#fig_storage_bloom) 显示了一个包含两个键和 16 位的布隆过滤器示例(实际上,它会包含更多的键和更多的位)。对于 SSTable 中的每个键,我们计算一个哈希函数,产生一组数字,然后将其解释为位数组的索引 [^14]。我们将对应于这些索引的位设置为 1,其余保持为 0。例如,键 `handbag` 哈希为数字 (2, 9, 4),所以我们将第 2、9 和 4 位设置为 1。然后将位图与键的稀疏索引一起存储为 SSTable 的一部分。这需要一点额外的空间,但与 SSTable 的其余部分相比,布隆过滤器通常很小。
|
||||
|
||||
{{< figure src="/fig/ddia_0404.png" id="fig_storage_bloom" caption="图 4-4. 布隆过滤器提供了一种快速的概率检查,以确定特定键是否存在于特定 SSTable 中。" class="w-full my-4" >}}
|
||||
{{< figure src="/fig/ddia_0404.png" id="fig_storage_bloom" caption="图 4-4. 布隆过滤器提供了一种快速的概率检查,用于判断特定键是否存在于特定 SSTable 中。" class="w-full my-4" >}}
|
||||
|
||||
当我们想知道一个键是否出现在 SSTable 中时,我们像以前一样计算该键的相同哈希,并检查这些索引处的位。例如,在 [图 4-4](/ch4#fig_storage_bloom) 中,我们正在查询键 `handheld`,它哈希到(6、11、2)。其中一个位是 1(即第 2 位),而另外两个是 0。这些检查可以使用所有 CPU 支持的位运算极其快速地进行。
|
||||
当我们想知道一个键是否出现在 SSTable 中时,我们像以前一样计算该键的相同哈希,并检查这些索引处的位。例如,在 [图 4-4](#fig_storage_bloom) 中,我们查询键 `handheld`,它哈希为 (6, 11, 2)。其中一个位是 1(即第 2 位),而另外两个是 0。这些检查可以使用所有 CPU 都支持的位运算非常快速地进行。
|
||||
|
||||
如果至少有一个位是 0,我们知道键肯定不出现在 SSTable 中。如果查询中的位都是 1,键很可能在 SSTable 中,但也可能巧合地所有这些位都被其他键设置为 1。这种看起来好像键存在,即使它不存在的情况,称为 *误报*。
|
||||
如果至少有一个位是 0,我们知道该键肯定不在 SSTable 中。如果查询中的位都是 1,那么该键很可能在 SSTable 中,但也有可能是巧合,所有这些位都被其他键设置为 1。这种看起来键存在但实际上不存在的情况称为 *假阳性*(*false positive*)。
|
||||
|
||||
误报的概率取决于键的数量、每个键设置的位数和布隆过滤器中的总位数。你可以使用在线计算器工具来计算适合你应用程序的正确参数 [^15]。作为经验法则,你需要为 SSTable 中的每个键分配 10 位布隆过滤器空间,以获得 1% 的误报概率,每为每个键分配额外的 5 位,概率降低十倍。
|
||||
假阳性的概率取决于键的数量、每个键设置的位数和布隆过滤器中的总位数。你可以使用在线计算器工具为你的应用计算出正确的参数 [^15]。作为经验法则,你需要为 SSTable 中的每个键分配 10 位布隆过滤器空间以获得 1% 的假阳性概率,每为每个键分配额外的 5 位,概率就会降低十倍。
|
||||
|
||||
在 LSM 存储引擎的上下文中,误报没有问题:
|
||||
在 LSM 存储引擎的上下文中,假阳性没有问题:
|
||||
|
||||
* 如果布隆过滤器说键 *不* 存在,我们可以安全地跳过该 SSTable,因为我们可以确定它不包含该键。
|
||||
* 如果布隆过滤器说键 *存在*,我们必须查阅稀疏索引并解码键值对块以检查键是否真的在那里。如果是误报,我们做了一些不必要的工作,但除此之外没有造成伤害——我们只是继续搜索下一个最旧的段。
|
||||
* 如果布隆过滤器说键 *存在*,我们必须查询稀疏索引并解码键值对块以检查键是否真的在那里。如果是假阳性,我们做了一些不必要的工作,但除此之外没有害处 —— 我们只是继续使用下一个最旧的段进行搜索。
|
||||
|
||||
#### 压实策略 {#sec_storage_lsm_compaction}
|
||||
|
||||
一个重要的细节是 LSM 存储如何选择何时执行压缩,以及在压缩中包含哪些 SSTable。许多基于 LSM 的存储系统允许你配置使用哪种压缩策略,一些常见的选择是 [^16] [^17]:
|
||||
一个重要的细节是 LSM 存储如何选择何时执行压实,以及在压实中包括哪些 SSTable。许多基于 LSM 的存储系统允许你配置使用哪种压实策略,一些常见的选择是 [^16] [^17]:
|
||||
|
||||
大小分层压实
|
||||
: 较新和较小的 SSTable 依次合并成较旧和较大的 SSTable。包含较旧数据的 SSTable 可能变得非常大,合并它们需要大量临时磁盘空间。这种策略的优点是它可以处理非常高的写入吞吐量。
|
||||
分层压实(Size-tiered compaction)
|
||||
: 较新和较小的 SSTable 依次合并到较旧和较大的 SSTable 中。包含较旧数据的 SSTable 可能变得非常大,合并它们需要大量的临时磁盘空间。这种策略的优点是它可以处理非常高的写入吞吐量。
|
||||
|
||||
分层压实
|
||||
: 键范围被分成更小的 SSTable,较旧的数据被移动到单独的"级别"中,这允许压缩更渐进地进行,并使用比大小分层策略更少的磁盘空间。这种策略对于读取比大小分层压缩更有效,因为存储引擎需要读取更少的 SSTable 来检查它们是否包含键。
|
||||
分级压实(Leveled compaction)
|
||||
: 键范围被分成较小的 SSTable,较旧的数据被移动到单独的"级别"中,这允许压实更增量地进行,并且比分层策略使用更少的磁盘空间。这种策略对于读取比分层压实更有效,因为存储引擎需要读取更少的 SSTable 来检查它们是否包含该键。
|
||||
|
||||
作为经验法则,如果你主要有写入和很少读取,大小分层压缩性能更好,而如果你的工作负载以读取为主,分层压缩性能更好。如果你频繁写入少量键,很少写入大量键,那么分层压缩也可能有优势 [^18]。
|
||||
作为经验法则,如果你主要有写入而读取很少,分层压实表现更好,而如果你的工作负载以读取为主,分级压实表现更好。如果你频繁写入少量键,而很少写入大量键,那么分级压实也可能有优势 [^18]。
|
||||
|
||||
尽管有许多细微之处,LSM 树的基本思想——保持在后台合并的 SSTable 级联——简单而有效。我们在 ["比较 B 树和 LSM 树"](/ch4#sec_storage_btree_lsm_comparison) 中更详细地讨论它们的性能特征。
|
||||
尽管有许多细微之处,但 LSM 树的基本思想 —— 保持在后台合并的 SSTable 级联 —— 简单而有效。我们将在 ["比较 B 树与 LSM 树"](#sec_storage_btree_lsm_comparison) 中更详细地讨论它们的性能特征。
|
||||
|
||||
--------
|
||||
|
||||
> [!TIP] 嵌入式存储引擎
|
||||
>
|
||||
> 许多数据库作为接受网络查询的服务运行,但也有 *嵌入式* 数据库不公开网络 API。相反,它们是在与你的应用程序代码相同的进程中运行的库,通常读取和写入本地磁盘上的文件,你通过正常的函数调用与它们交互。嵌入式存储引擎的例子包括 RocksDB、SQLite、LMDB、DuckDB 和 KùzuDB [^19]。
|
||||
>
|
||||
> 嵌入式数据库在移动应用程序中非常常用,用于存储本地用户的数据。在后端,如果数据足够小以适合单台机器,并且没有很多并发事务,它们可能是合适的选择。例如,在多租户系统中,如果每个租户足够小且彼此完全分离(即,你不需要运行组合来自多个租户的数据的查询),你可能可以为每个租户使用单独的嵌入式数据库实例 [^20]。
|
||||
>
|
||||
> 我们在本章中讨论的存储和检索方法既用于嵌入式数据库,也用于客户端-服务器数据库。在 [第 6 章](/ch6#ch_replication) 和 [第 7 章](/ch7#ch_sharding) 中,我们将讨论跨多台机器扩展数据库的技术。
|
||||
|
||||
许多数据库作为接受网络查询的服务运行,但也有 *嵌入式* 数据库不公开网络 API。相反,它们是在与应用程序代码相同的进程中运行的库,通常读取和写入本地磁盘上的文件,你通过正常的函数调用与它们交互。嵌入式存储引擎的例子包括 RocksDB、SQLite、LMDB、DuckDB 和 KùzuDB [^19]。
|
||||
|
||||
嵌入式数据库在移动应用中非常常用,用于存储本地用户的数据。在后端,如果数据足够小以适合单台机器,并且没有太多并发事务,它们可能是一个合适的选择。例如,在多租户系统中,如果每个租户足够小且完全与其他租户分离(即,你不需要运行合并多个租户数据的查询),你可能可以为每个租户使用单独的嵌入式数据库实例 [^20]。
|
||||
|
||||
我们在本章讨论的存储和检索方法既用于嵌入式数据库,也用于客户端-服务器数据库。在 [第 6 章](/ch6#ch_replication) 和 [第 7 章](/ch7#ch_sharding) 中,我们将讨论跨多台机器扩展数据库的技术。
|
||||
|
||||
--------
|
||||
|
||||
### B 树 {#sec_storage_b_trees}
|
||||
|
||||
日志结构方法很流行,但它不是键值存储的唯一形式。按键读取和写入数据库记录的最广泛使用的结构是 *B 树*。
|
||||
日志结构方法很流行,但它不是键值存储的唯一形式。按键读取和写入数据库记录最广泛使用的结构是 *B 树*。
|
||||
|
||||
B 树于 1970 年引入 [^21],不到 10 年后被称为"无处不在" [^22],它们经受住了时间的考验。它们仍然是几乎所有关系数据库中的标准索引实现,许多非关系数据库也使用它们。
|
||||
B 树于 1970 年引入 [^21],不到 10 年后就被称为"无处不在"[^22],它们经受住了时间的考验。它们仍然是几乎所有关系数据库中的标准索引实现,许多非关系数据库也使用它们。
|
||||
|
||||
像 SSTable 一样,B 树按键排序键值对,这允许高效的键值查找和范围查询。但相似之处到此为止:B 树具有非常不同的设计理念。
|
||||
像 SSTable 一样,B 树按键保持键值对排序,这允许高效的键值查找和范围查询。但相似之处到此为止:B 树有着非常不同的设计理念。
|
||||
|
||||
我们之前看到的日志结构索引将数据库分解为可变大小的 *段*,通常为几兆字节或更大,一次写入然后不可变。相比之下,B 树将数据库分解为固定大小的 *块* 或 *页*,并可能就地覆盖页。页传统上大小为 4 KiB,但 PostgreSQL 现在默认使用 8 KiB,MySQL 默认使用 16 KiB。
|
||||
我们之前看到的日志结构索引将数据库分解为可变大小的 *段*,通常为几兆字节或更大,写入一次后就不可变。相比之下,B 树将数据库分解为固定大小的 *块* 或 *页*,并可能就地覆盖页。页传统上大小为 4 KiB,但 PostgreSQL 现在默认使用 8 KiB,MySQL 默认使用 16 KiB。
|
||||
|
||||
每个页可以使用页号标识,这允许一个页引用另一个页——类似于指针,但在磁盘上而不是在内存中。如果所有页都存储在同一个文件中,将页号乘以页大小会给我们页所在文件中的字节偏移量。我们可以使用这些页引用来构建页树,如 [图 4-5](/ch4#fig_storage_b_tree) 所示。
|
||||
每个页都可以使用页号来标识,这允许一个页引用另一个页 —— 类似于指针,但在磁盘上而不是在内存中。如果所有页都存储在同一个文件中,将页号乘以页大小就给我们文件中页所在位置的字节偏移量。我们可以使用这些页引用来构建页树,如 [图 4-5](#fig_storage_b_tree) 所示。
|
||||
|
||||
{{< figure src="/fig/ddia_0405.png" id="fig_storage_b_tree" caption="图 4-5. 使用 B 树索引查找键 251。从根页开始,我们首先跟随引用到键 200-300 的页,然后是键 250-270 的页。" class="w-full my-4" >}}
|
||||
{{< figure src="/fig/ddia_0405.png" id="fig_storage_b_tree" caption="图 4-5. 使用 B 树索引查找键 251。从根页开始,我们首先跟随引用到键 200–300 的页,然后是键 250–270 的页。" class="w-full my-4" >}}
|
||||
|
||||
一个页被指定为 B 树的 *根*;每当你想在索引中查找一个键时,你从这里开始。该页包含几个键和对子页的引用。每个子负责一个连续的键范围,引用之间的键指示这些范围之间的边界在哪里。(这种结构有时称为 B+ 树,但我们不需要将其与其他 B 树变体区分开来。)
|
||||
一个页被指定为 B 树的 *根*;每当你想在索引中查找一个键时,你就从这里开始。该页包含几个键和对子页的引用。每个子负责一个连续的键范围,引用之间的键指示这些范围之间的边界在哪里。(这种结构有时称为 B+ 树,但我们不需要将其与其他 B 树变体区分开来。)
|
||||
|
||||
在 [图 4-5](/ch4#fig_storage_b_tree) 的示例中,我们正在查找键 251,所以我们知道需要跟随 200 和 300 之间的页引用。这将我们带到一个外观相似的页,进一步将 200-300 范围分解为子范围。最终我们到达包含单个键的页(*叶页*),它要么内联包含每个键的值,要么包含对可以找到值的页的引用。
|
||||
在 [图 4-5](#fig_storage_b_tree) 的例子中,我们正在查找键 251,所以我们知道我们需要跟随边界 200 和 300 之间的页引用。这将我们带到一个看起来相似的页,该页进一步将 200–300 范围分解为子范围。最终我们到达包含单个键的页(*叶页*),该页要么内联包含每个键的值,要么包含对可以找到值的页的引用。
|
||||
|
||||
B 树的一页中对子页的引用数称为 *分支因子*。例如,在 [图 4-5](/ch4#fig_storage_b_tree) 中,分支因子为六。实际上,分支因子取决于存储页引用和范围边界所需的空间量,但通常是几百。
|
||||
B 树的一个页中对子页的引用数称为 *分支因子*。例如,在 [图 4-5](#fig_storage_b_tree) 中,分支因子为六。实际上,分支因子取决于存储页引用和范围边界所需的空间量,但通常为几百。
|
||||
|
||||
如果你想更新 B 树中现有键的值,你搜索包含该键的叶页,并用包含新值的版本覆盖磁盘上的该页。如果你想添加新键,你需要找到其范围包含新键的页并将其添加到该页。如果页中没有足够的空闲空间来容纳新键,则页分成两个半满页,并更新父页以考虑键范围的新细分。
|
||||
如果你想更新 B 树中现有键的值,你搜索包含该键的叶页,并用包含新值的版本覆盖磁盘上的该页。如果你想添加一个新键,你需要找到其范围包含新键的页并将其添加到该页。如果页中没有足够的空闲空间来容纳新键,则页被分成两个半满的页,并更新父页以说明键范围的新细分。
|
||||
|
||||
{{< figure src="/fig/ddia_0406.png" id="fig_storage_b_tree_split" caption="图 4-6. 通过在边界键 337 上分割页来增长 B 树。父页被更新以引用两个子页。" class="w-full my-4" >}}
|
||||
|
||||
在 [图 4-6](/ch4#fig_storage_b_tree_split) 的示例中,我们想插入键 334,但范围 333-345 的页已满。因此,我们将其分成范围 333-337(包括新键)的页和 337-344 的页。我们还必须更新父页以引用两个子页,它们之间的边界值为 337。如果父页没有足够的空间用于新引用,它也可能需要分割,分割可以一直持续到树的根。当根分割时,我们在其上方创建一个新根。删除键(可能需要合并节点)更复杂 [^5]。
|
||||
在 [图 4-6](#fig_storage_b_tree_split) 的例子中,我们想插入键 334,但范围 333–345 的页已经满了。因此,我们将其分成范围 333–337(包括新键)的页和 337–344 的页。我们还必须更新父页以引用两个子页,它们之间的边界值为 337。如果父页没有足够的空间容纳新引用,它也可能需要被分割,分割可以一直持续到树的根。当根被分割时,我们在它上面创建一个新根。删除键(可能需要合并节点)更复杂 [^5]。
|
||||
|
||||
该算法确保树保持 *平衡*:具有 *n* 个键的 B 树始终具有 *O*(log *n*) 的深度。大多数数据库可以适合三或四层深的 B 树,因此你不需要跟随很多页引用来找到你要查找的页。(具有 500 的分支因子的 4 KiB 页的四层树可以存储多达 250 TB。)
|
||||
这个算法确保树保持 *平衡*:具有 *n* 个键的 B 树始终具有 *O*(log *n*) 的深度。大多数数据库可以适合三或四层深的 B 树,所以你不需要跟随许多页引用来找到你要查找的页。(具有 500 分支因子的 4 KiB 页的四层树可以存储多达 250 TB。)
|
||||
|
||||
#### 使 B 树可靠 {#sec_storage_btree_wal}
|
||||
|
||||
B 树的基本底层写入操作是用新数据覆盖磁盘上的页。假设覆盖不会改变页的位置;即,当页被覆盖时,对该页的所有引用保持不变。这与日志结构索引(如 LSM 树)形成鲜明对比,后者只追加到文件(并最终删除过时的文件)但从不就地修改文件。
|
||||
B 树的基本底层写操作是用新数据覆盖磁盘上的页。假设覆盖不会改变页的位置;即,当页被覆盖时,对该页的所有引用保持不变。这与日志结构索引(如 LSM 树)形成鲜明对比,后者只追加到文件(并最终删除过时的文件),但从不就地修改文件。
|
||||
|
||||
一次覆盖几个页,如在页分割中,是一个危险的操作:如果数据库在只写入了一些页后崩溃,你最终会得到一个损坏的树(例如,可能有一个 *孤立* 页不是任何父页的子页)。如果硬件不能原子地写入整个页,你也可能最终得到部分写入的页(这称为 *撕裂页* [^23])。
|
||||
一次覆盖多个页,如在页分割中,是一个危险的操作:如果数据库在只写入了部分页后崩溃,你最终会得到一个损坏的树(例如,可能有一个 *孤立* 页,它不是任何父页的子页)。如果硬件不能原子地写入整个页,你也可能最终得到部分写入的页(这称为 *撕裂页*(*torn page*)[^23])。
|
||||
|
||||
为了使数据库对崩溃具有弹性,B 树实现通常包括磁盘上的额外数据结构:*预写日志*(WAL)。这是一个仅追加文件,在将每个 B 树修改应用于树本身的页之前,必须将其写入该文件。当数据库在崩溃后恢复时,此日志用于将 B 树恢复到一致状态 [^2] [^24]。在文件系统中,等效机制称为 *日志记录*。
|
||||
为了使数据库对崩溃具有弹性,B 树实现通常包括磁盘上的额外数据结构:*预写日志*(*write-ahead log*,WAL)。这是一个仅追加文件,每个 B 树修改必须在应用于树本身的页之前写入其中。当数据库在崩溃后恢复时,此日志用于将 B 树恢复到一致状态 [^2] [^24]。在文件系统中,等效机制称为 *日志记录*(*journaling*)。
|
||||
|
||||
为了提高性能,B 树实现通常不会立即将每个修改的页写入磁盘,而是首先在内存中缓冲 B 树页一段时间。预写日志然后还确保在崩溃的情况下数据不会丢失:只要数据已写入 WAL,并使用 `fsync()` 系统调用刷新到磁盘,数据将是持久的,因为数据库将能够在崩溃后恢复它 [^25]。
|
||||
为了提高性能,B 树实现通常不会立即将每个修改的页写入磁盘,而是首先将 B 树页缓冲在内存中一段时间。预写日志还确保在崩溃的情况下数据不会丢失:只要数据已写入 WAL,并使用 `fsync()` 系统调用刷新到磁盘,数据就是持久的,因为数据库将能够在崩溃后恢复它 [^25]。
|
||||
|
||||
#### B 树变体 {#b-tree-variants}
|
||||
|
||||
由于 B 树已经存在了很长时间,多年来已经开发了许多变体。仅举几例:
|
||||
由于 B 树已经存在了很长时间,多年来已经开发了许多变体。仅举几个例子:
|
||||
|
||||
* 一些数据库(如 LMDB)使用写时复制方案 [^26],而不是覆盖页并维护 WAL 进行崩溃恢复。修改的页被写入不同的位置,并创建树中父页的新版本,指向新位置。这种方法对于并发控制也很有用,正如我们将在 ["快照隔离和可重复读"](/ch8#sec_transactions_snapshot_isolation) 中看到的。
|
||||
* 我们可以通过不存储整个键,而是缩写它来节省页中的空间。特别是在树内部的页上,键只需要提供足够的信息来充当键范围之间的边界。将更多键打包到页中允许树具有更高的分支因子,从而减少层级。
|
||||
* 为了加速按排序顺序扫描键范围,一些 B 树实现尝试布局树,使叶页在磁盘上按顺序出现,减少磁盘寻道次数。然而,随着树的增长,很难维持该顺序。
|
||||
* 已向树添加了额外的指针。例如,每个叶页可能具有对其左右兄弟页的引用,这允许按顺序扫描键而无需跳回父页。
|
||||
* 一些数据库(如 LMDB)使用写时复制方案 [^26],而不是覆盖页并维护 WAL 以进行崩溃恢复。修改的页被写入不同的位置,并创建树中父页的新版本,指向新位置。这种方法对于并发控制也很有用,我们将在 ["快照隔离和可重复读"](/ch8#sec_transactions_snapshot_isolation) 中看到。
|
||||
* 我们可以通过不存储整个键而是缩写它来节省页中的空间。特别是在树内部的页中,键只需要提供足够的信息来充当键范围之间的边界。在页中打包更多键允许树具有更高的分支因子,从而减少层数。
|
||||
* 为了加快按排序顺序扫描键范围,一些 B 树实现尝试布局树,使叶页按顺序出现在磁盘上,减少磁盘寻道次数。然而,随着树的增长,很难维持这种顺序。
|
||||
* 已向树添加了其他指针。例如,每个叶页可能有对其左右兄弟页的引用,这允许按顺序扫描键而无需跳回父页。
|
||||
|
||||
### 比较 B 树与 LSM 树 {#sec_storage_btree_lsm_comparison}
|
||||
|
||||
作为经验法则,LSM 树更适合写入密集型应用程序,而 B 树对于读取更快 [^27] [^28]。然而,基准测试通常对工作负载的细节很敏感。你需要使用你的特定工作负载测试系统才能进行有效比较。此外,这不是 LSM 和 B 树之间的严格二选一选择:存储引擎有时融合了两种方法的特征,例如通过拥有多个 B 树并以 LSM 风格合并它们。在本节中,我们将简要讨论在衡量存储引擎性能时值得考虑的几件事。
|
||||
作为经验法则,LSM 树更适合写入密集型应用,而 B 树对读取更快 [^27] [^28]。然而,基准测试通常对工作负载的细节很敏感。你需要使用特定的工作负载测试系统,以便进行有效的比较。此外,这不是 LSM 和 B 树之间的严格二选一选择:存储引擎有时会混合两种方法的特征,例如具有多个 B 树并以 LSM 风格合并它们。在本节中,我们将简要讨论在衡量存储引擎性能时值得考虑的几件事。
|
||||
|
||||
#### 读取性能 {#read-performance}
|
||||
|
||||
在 B 树中,查找键涉及在 B 树的每个层级读取一页。由于层级数通常很小,这意味着从 B 树读取通常很快且具有可预测的性能。在 LSM 存储引擎中,读取通常必须检查处于不同压缩阶段的几个不同 SSTable,但布隆过滤器有助于减少所需的实际磁盘 I/O 操作数。两种方法都可以表现良好,哪个更快取决于存储引擎和工作负载的细节。
|
||||
在 B 树中,查找键涉及在 B 树的每个级别读取一个页。由于级别数通常很小,这意味着从 B 树读取通常很快并且具有可预测的性能。在 LSM 存储引擎中,读取通常必须检查处于不同压实阶段的几个不同 SSTable,但布隆过滤器有助于减少所需的实际磁盘 I/O 操作数。两种方法都可以表现良好,哪个更快取决于存储引擎的细节和工作负载。
|
||||
|
||||
范围查询在 B 树上简单而快速,因为它们可以使用树的排序结构。在 LSM 存储上,范围查询也可以利用 SSTable 排序,但它们需要并行扫描所有段并组合结果。布隆过滤器对范围查询没有帮助(因为你需要计算范围内每个可能键的哈希,这是不切实际的),使得范围查询在 LSM 方法中比点查询更昂贵 [^29]。
|
||||
|
||||
如果内存表填满,高写入吞吐量可能会在日志结构存储引擎中导致延迟峰值。如果数据无法足够快地写入磁盘,可能是因为压缩过程无法跟上传入写入,就会发生这种情况。许多存储引擎,包括 RocksDB,在这种情况下执行 *背压*:它们暂停所有读取和写入,直到内存表被写入磁盘 [^30] [^31]。
|
||||
如果内存表填满,高写入吞吐量可能会导致日志结构存储引擎中的延迟峰值。如果数据无法足够快地写入磁盘,可能是因为压实过程无法跟上传入的写入,就会发生这种情况。许多存储引擎,包括 RocksDB,在这种情况下执行 *背压*:它们暂停所有读取和写入,直到内存表被写入磁盘 [^30] [^31]。
|
||||
|
||||
关于读取吞吐量,现代 SSD(特别是 NVMe)可以并行执行许多独立的读取请求。LSM 树和 B 树都能够提供高读取吞吐量,但存储引擎需要精心设计以利用这种并行性 [^32]。
|
||||
关于读取吞吐量,现代 SSD(特别是 NVMe)可以并行执行许多独立的读请求。LSM 树和 B 树都能够提供高读取吞吐量,但存储引擎需要仔细设计以利用这种并行性 [^32]。
|
||||
|
||||
#### 顺序与随机写入 {#sidebar_sequential}
|
||||
|
||||
使用 B 树,如果应用程序写入分散在整个键空间中的键,则产生的磁盘操作也是随机分散的,因为存储引擎需要覆盖的页可能位于磁盘上的任何位置。另一方面,日志结构存储引擎一次写入整个段文件(要么写出内存表,要么压缩现有段),这比 B 树中的页大得多。
|
||||
使用 B 树时,如果应用程序写入的键分散在整个键空间中,生成的磁盘操作也会随机分散,因为存储引擎需要覆盖的页可能位于磁盘的任何位置。另一方面,日志结构存储引擎一次写入整个段文件(无论是写出内存表还是压实现有段),这比 B 树中的页大得多。
|
||||
|
||||
许多小的、分散的写入模式(如在 B 树中发现的)称为 *随机写入*,而较少的大写入模式(如在 LSM 树中发现的)称为 *顺序写入*。磁盘通常具有比随机写入吞吐量更高的顺序写入吞吐量,这意味着日志结构存储引擎通常可以在相同硬件上处理比 B 树更高的写入吞吐量。这种差异在旋转磁盘硬盘(HDD)上特别大;在今天大多数数据库使用的固态硬盘(SSD)上,差异较小,但仍然很明显(参见 ["SSD 上的顺序与随机写入"](/ch4#sidebar_sequential))。
|
||||
许多小的、分散的写入模式(如 B 树中的)称为 *随机写入*,而较少的大写入模式(如 LSM 树中的)称为 *顺序写入*。磁盘通常具有比随机写入更高的顺序写入吞吐量,这意味着日志结构存储引擎通常可以在相同硬件上处理比 B 树更高的写入吞吐量。这种差异在旋转磁盘硬盘(HDD)上特别大;在今天大多数数据库使用的固态硬盘(SSD)上,差异较小,但仍然明显(参见 ["SSD 上的顺序与随机写入"](#sidebar_sequential))。
|
||||
|
||||
--------
|
||||
|
||||
> [!TIP] SSD 上的顺序与随机写入
|
||||
>
|
||||
> 在旋转磁盘硬盘(HDD)上,顺序写入比随机写入快得多:随机写入必须机械地将磁头移动到新位置,并等待盘片的正确部分通过磁头下方,这需要几毫秒——在计算时间尺度上是永恒的。然而,SSD(固态硬盘)包括 NVMe(非易失性内存快速,即连接到 PCI Express 总线的闪存)现在已经在许多用例中取代了 HDD,它们不受此类机械限制。
|
||||
>
|
||||
> 尽管如此,SSD 的顺序写入吞吐量也高于随机写入。原因是闪存可以一次读取或写入一页(通常为 4 KiB),但只能一次擦除一个块(通常为 512 KiB)。块中的某些页可能包含有效数据,而其他页可能包含不再需要的数据。在擦除块之前,控制器必须首先将包含有效数据的页移动到其他块中;这个过程称为 *垃圾收集*(GC)[^33]。
|
||||
>
|
||||
> 顺序写入工作负载一次写入更大的数据块,因此整个 512 KiB 块很可能属于单个文件;当该文件稍后再次删除时,可以擦除整个块而无需执行任何 GC。另一方面,使用随机写入工作负载,块更可能包含有效和无效数据页的混合,因此 GC 必须在擦除块之前执行更多工作 [^34] [^35] [^36]。
|
||||
>
|
||||
> GC 消耗的写入带宽随后不可用于应用程序。此外,GC 执行的额外写入会导致闪存磨损;因此,随机写入比顺序写入更快地磨损驱动器。
|
||||
|
||||
在旋转磁盘硬盘(HDD)上,顺序写入比随机写入快得多:随机写入必须机械地将磁头移动到新位置,并等待盘片的正确部分经过磁头下方,这需要几毫秒 —— 在计算时间尺度上是永恒的。然而,SSD(固态硬盘)包括 NVMe(非易失性内存快速,即连接到 PCI Express 总线的闪存)现在已经在许多用例中超越了 HDD,它们不受这种机械限制。
|
||||
|
||||
尽管如此,SSD 对顺序写入的吞吐量也高于随机写入。原因是闪存可以一次读取或写入一页(通常为 4 KiB),但只能一次擦除一个块(通常为 512 KiB)。块中的某些页可能包含有效数据,而其他页可能包含不再需要的数据。在擦除块之前,控制器必须首先将包含有效数据的页移动到其他块中;这个过程称为 *垃圾回收*(GC)[^33]。
|
||||
|
||||
顺序写入工作负载一次写入更大的数据块,因此整个 512 KiB 块很可能属于单个文件;当该文件稍后再次被删除时,整个块可以被擦除而无需执行任何 GC。另一方面,对于随机写入工作负载,块更可能包含有效和无效数据页的混合,因此 GC 必须在块可以擦除之前执行更多工作 [^34] [^35] [^36]。
|
||||
|
||||
GC 消耗的写入带宽就不能用于应用程序。此外,GC 执行的额外写入会导致闪存磨损;因此,随机写入比顺序写入更快地磨损驱动器。
|
||||
|
||||
--------
|
||||
|
||||
#### 写放大 {#write-amplification}
|
||||
|
||||
使用任何类型的存储引擎,来自应用程序的一个写入请求都会在底层磁盘上变成多个 I/O 操作。使用 LSM 树,首先将值写入日志以获得持久性,然后在内存表写入磁盘时再次写入,并且每次键值对成为压缩的一部分时再次写入。(如果值明显大于键,可以通过将值与键分开存储,并仅对包含键和值引用的 SSTable 执行压缩来减少此开销 [^37]。)
|
||||
对于任何类型的存储引擎,来自应用程序的一次写请求都会转换为底层磁盘上的多个 I/O 操作。对于 LSM 树,一个值首先被写入日志以保证持久性,然后在内存表写入磁盘时再次写入,并且每次键值对参与压实时再次写入。(如果值明显大于键,可以通过将值与键分开存储,并仅对包含键和值引用的 SSTable 执行压实来减少这种开销 [^37]。)
|
||||
|
||||
B 树索引必须至少写入每条数据两次:一次到预写日志,一次到树页本身。此外,它们有时需要写出整个页,即使该页中只有几个字节发生了变化,以确保 B 树在崩溃或断电后可以正确恢复 [^38] [^39]。
|
||||
B 树索引必须至少写入每条数据两次:一次写入预写日志,一次写入树页本身。此外,它们有时需要写出整个页,即使该页中只有几个字节发生了变化,以确保 B 树在崩溃或断电后可以正确恢复 [^38] [^39]。
|
||||
|
||||
如果你将某些工作负载中写入磁盘的总字节数除以你仅写入没有索引的仅追加日志时必须写入的字节数,你就得到 *写放大*。(有时写放大是根据 I/O 操作而不是字节定义的。)在写入密集型应用程序中,瓶颈可能是数据库可以写入磁盘的速率。在这种情况下,写放大越高,它可以在可用磁盘带宽内处理的每秒写入越少。
|
||||
如果你获取在某个工作负载中写入磁盘的总字节数,然后除以如果你只是写入没有索引的仅追加日志需要写入的字节数,你就得到了 *写放大*。(有时写放大是根据 I/O 操作而不是字节来定义的。)在写入密集型应用程序中,瓶颈可能是数据库可以写入磁盘的速率。在这种情况下,写放大越高,它在可用磁盘带宽内可以处理的每秒写入次数就越少。
|
||||
|
||||
写放大是 LSM 树和 B 树中的问题。哪个更好取决于各种因素,例如键和值的长度,以及你覆盖现有键与插入新键的频率。对于典型工作负载,LSM 树往往具有较低的写放大,因为它们不必写入整个页,并且可以压缩 SSTable 的块 [^40]。这是使 LSM 存储引擎非常适合写入密集型工作负载的另一个因素。
|
||||
写放大是 LSM 树和 B 树中的问题。哪个更好取决于各种因素,例如键和值的长度,以及你覆盖现有键与插入新键的频率。对于典型的工作负载,LSM 树往往具有较低的写放大,因为它们不必写入整个页,并且可以压缩 SSTable 的块 [^40]。这是使 LSM 存储引擎非常适合写入密集型工作负载的另一个因素。
|
||||
|
||||
除了影响吞吐量外,写放大还与 SSD 的磨损有关:具有较低写放大的存储引擎将更慢地磨损 SSD。
|
||||
除了影响吞吐量,写放大也与 SSD 的磨损有关:写放大较低的存储引擎将更慢地磨损 SSD。
|
||||
|
||||
在测量存储引擎的写入吞吐量时,重要的是运行实验足够长的时间,以使写放大的影响变得明显。写入空 LSM 树时,还没有压缩在进行,因此所有磁盘带宽都可用于新写入。随着数据库的增长,新写入需要与压缩共享磁盘带宽。
|
||||
在测量存储引擎的写入吞吐量时,重要的是要运行足够长的实验,以便写放大的影响变得清晰。当写入空的 LSM 树时,还没有进行压实,因此所有磁盘带宽都可用于新写入。随着数据库的增长,新写入需要与压实共享磁盘带宽。
|
||||
|
||||
#### 磁盘空间使用 {#disk-space-usage}
|
||||
|
||||
B 树会随着时间的推移变得 *碎片化*:例如,如果删除了大量键,数据库文件可能包含许多不再被 B 树使用的页。对 B 树的后续添加可以使用那些空闲页,但它们不能轻易地返回给操作系统,因为它们在文件中间,因此它们仍然占用文件系统上的空间。因此,数据库需要一个后台进程来移动页以更好地放置它们,例如 PostgreSQL 中的 vacuum 进程 [^25]。
|
||||
B 树可能会随着时间的推移变得 *碎片化*:例如,如果删除了大量键,数据库文件可能包含许多 B 树不再使用的页。对 B 树的后续添加可以使用这些空闲页,但它们不能轻易地返回给操作系统,因为它们在文件的中间,所以它们仍然占用文件系统上的空间。因此,数据库需要一个后台过程来移动页以更好地放置它们,例如 PostgreSQL 中的真空过程 [^25]。
|
||||
|
||||
碎片化在 LSM 树中不是问题,因为压缩过程无论如何都会定期重写数据文件,并且 SSTable 没有未使用空间的页。此外,键值对块可以在 SSTable 中更好地压缩,因此通常在磁盘上产生比 B 树更小的文件。被覆盖的键和值继续消耗空间,直到它们被压缩删除,但使用分层压缩时此开销相当低 [^40] [^41]。大小分层压缩(参见 ["压缩策略"](/ch4#sec_storage_lsm_compaction))使用更多磁盘空间,特别是在压缩期间暂时使用。
|
||||
碎片化在 LSM 树中不太成问题,因为压实过程无论如何都会定期重写数据文件,而且 SSTable 没有未使用空间的页。此外,SSTable 中的键值对块可以更好地压缩,因此通常比 B 树在磁盘上产生更小的文件。被覆盖的键和值继续消耗空间,直到它们被压实删除,但使用分级压实时,这种开销相当低 [^40] [^41]。分层压实(参见 ["压实策略"](#sec_storage_lsm_compaction))使用更多的磁盘空间,特别是在压实期间临时使用。
|
||||
|
||||
在磁盘上有某些数据的多个副本也可能是一个问题,当你需要删除某些数据,并确信它真的已被删除(可能是为了遵守数据保护法规)。例如,在大多数 LSM 存储引擎中,已删除的记录可能仍然存在于较高级别,直到代表删除的墓碑通过所有压缩级别传播,这可能需要很长时间。专门的存储引擎设计可以更快地传播删除 [^42]。
|
||||
在磁盘上有一些数据的多个副本也可能是一个问题,当你需要删除一些数据,并确信它真的已被删除(也许是为了遵守数据保护法规)。例如,在大多数 LSM 存储引擎中,已删除的记录可能仍然存在于较高级别中,直到代表删除的墓碑通过所有压实级别传播,这可能需要很长时间。专门的存储引擎设计可以更快地传播删除 [^42]。
|
||||
|
||||
另一方面,SSTable 段文件的不可变性质很有用,如果你想在某个时间点拍摄数据库快照(例如,用于备份或创建用于测试的数据库副本):你可以写出内存表并记录该时间点存在的段文件。只要你不删除快照的一部分文件,就不需要实际复制它们。在页被覆盖的 B 树中,有效地拍摄这样的快照更加困难。
|
||||
另一方面,SSTable 段文件的不可变性质在你想在某个时间点对数据库进行快照时很有用(例如,用于备份或创建数据库副本以进行测试):你可以写出内存表并记录该时间点存在的段文件。只要你不删除快照的一部分的文件,你就不需要实际复制它们。在其页被覆盖的 B 树中,有效地进行这样的快照更困难。
|
||||
|
||||
|
||||
### 多列索引与二级索引 {#sec_storage_index_multicolumn}
|
||||
|
||||
到目前为止,我们只讨论了键值索引,它们类似于关系模型中的 *主键* 索引。主键唯一标识关系表中的一行、文档数据库中的一个文档或图数据库中的一个顶点。数据库中的其他记录可以通过其主键(或 ID)引用该行/文档/顶点,索引用于解析此类引用。
|
||||
到目前为止,我们只讨论了键值索引,它们就像关系模型中的 *主键* 索引。主键唯一标识关系表中的一行,或文档数据库中的一个文档,或图数据库中的一个顶点。数据库中的其他记录可以通过其主键(或 ID)引用该行/文档/顶点,索引用于解析此类引用。
|
||||
|
||||
拥有 *二级索引* 也很常见。在关系数据库中,你可以使用 `CREATE INDEX` 命令在同一个表上创建几个二级索引,允许你按主键以外的列进行搜索。例如,在 [第 3 章](/ch3#ch_datamodels) 的 [图 3-1](/ch3#fig_obama_relational) 中,你很可能在 `user_id` 列上有一个二级索引,以便你可以在每个表中找到属于同一用户的所有行。
|
||||
拥有 *二级索引* 也非常常见。在关系数据库中,你可以使用 `CREATE INDEX` 命令在同一个表上创建多个二级索引,允许你按主键以外的列进行搜索。例如,在 [第 3 章](/ch3#ch_datamodels) 的 [图 3-1](/ch3#fig_obama_relational) 中,你很可能在 `user_id` 列上有一个二级索引,以便你可以在每个表中找到属于同一用户的所有行。
|
||||
|
||||
二级索引可以很容易地从键值索引构建。主要区别在于,在二级索引中,索引值不一定是唯一的;也就是说,同一个索引条目下可能有许多行(文档、顶点)。这可以通过两种方式解决:要么使索引中的每个值成为匹配行标识符列表(如全文索引中的发布列表),要么通过向其附加行标识符使每个条目唯一。具有就地更新的存储引擎(如 B 树)和日志结构存储都可以用于实现索引。
|
||||
二级索引可以很容易地从键值索引构建。主要区别在于,在二级索引中,索引值不一定是唯一的;也就是说,同一索引条目下可能有许多行(文档、顶点)。这可以通过两种方式解决:要么使索引中的每个值成为匹配行标识符的列表(如全文索引中的倒排列表),要么通过向其追加行标识符使每个条目唯一。具有就地更新的存储引擎(如 B 树)和日志结构存储都可用于实现索引。
|
||||
|
||||
#### 在索引中存储值 {#sec_storage_index_heap}
|
||||
|
||||
索引中的键是查询搜索的内容,但值可以是几种东西之一:
|
||||
|
||||
* 如果实际数据(行、文档、顶点)直接存储在索引结构中,则称为 *聚簇索引*。例如,在 MySQL 的 InnoDB 存储引擎中,表的主键始终是聚簇索引,在 SQL Server 中,你可以为每个表指定一个聚簇索引 [^43]。
|
||||
* 或者,值可以是对实际数据的引用:要么是相关行的主键(InnoDB 对二级索引执行此操作),要么是对磁盘上位置的直接引用。在后一种情况下,存储行的地方称为 *堆文件*,它以无特定顺序存储数据(它可能是仅追加的,或者它可能跟踪已删除的行以便稍后用新数据覆盖它们)。例如,Postgres 使用堆文件方法 [^44]。
|
||||
* 两者之间的中间地带是 *覆盖索引* 或 *包含列的索引*,除了在堆或主键聚簇索引上存储完整行外,还在索引中存储表的 *某些* 列 [^45]。这允许仅使用索引回答某些查询,而无需解析主键或查看堆文件(在这种情况下,索引称为 *覆盖* 查询)。这可以使某些查询更快,但数据的重复意味着索引使用更多磁盘空间并减慢写入速度。
|
||||
* 或者,值可以是对实际数据的引用:要么是相关行的主键(InnoDB 对二级索引这样做),要么是对磁盘上位置的直接引用。在后一种情况下,存储行的地方称为 *堆文件*,它以无特定顺序存储数据(它可能是仅追加的,或者它可能跟踪已删除的行以便稍后用新数据覆盖它们)。例如,Postgres 使用堆文件方法 [^44]。
|
||||
* 两者之间的折中是 *覆盖索引* 或 *包含列的索引*,它在索引中存储表的 *某些* 列,除了在堆上或主键聚簇索引中存储完整行 [^45]。这允许仅使用索引来回答某些查询,而无需解析主键或查看堆文件(在这种情况下,索引被称为 *覆盖* 查询)。这可以使某些查询更快,但数据的重复意味着索引使用更多的磁盘空间并减慢写入速度。
|
||||
|
||||
到目前为止讨论的索引只将单个键映射到值。如果你需要同时查询表的多个列(或文档中的多个字段),请参阅 ["多维和全文索引"](/ch4#sec_storage_multidimensional)。
|
||||
到目前为止讨论的索引只将单个键映射到值。如果你需要同时查询表的多个列(或文档中的多个字段),请参见 ["多维索引与全文索引"](#sec_storage_multidimensional)。
|
||||
|
||||
在不更改键的情况下更新值时,堆文件方法可以允许记录就地覆盖,前提是新值不大于旧值。如果新值更大,情况会更复杂,因为它可能需要移动到堆中有足够空间的新位置。在这种情况下,要么所有索引都需要更新以指向记录的新堆位置,要么在旧堆位置留下转发指针 [^2]。
|
||||
当更新值而不更改键时,堆文件方法可以允许记录就地覆盖,前提是新值不大于旧值。如果新值更大,情况会更复杂,因为它可能需要移动到堆中有足够空间的新位置。在这种情况下,要么所有索引都需要更新以指向记录的新堆位置,要么在旧堆位置留下转发指针 [^2]。
|
||||
|
||||
### 全内存存储 {#sec_storage_inmemory}
|
||||
|
||||
本章到目前为止讨论的数据结构都是对磁盘限制的回答。与主内存相比,磁盘很难处理。使用磁盘和 SSD,如果你想在读取和写入方面获得良好的性能,需要仔细布局磁盘上的数据。然而,我们容忍这种尴尬,因为磁盘有两个显著的优势:它们是持久的(如果电源关闭,它们的内容不会丢失),并且它们每千兆字节的成本低于 RAM。
|
||||
本章到目前为止讨论的数据结构都是对磁盘限制的回应。与主内存相比,磁盘很难处理。对于磁盘和 SSD,如果你想在读取和写入上获得良好的性能,磁盘上的数据需要仔细布局。然而,我们容忍这种尴尬,因为磁盘有两个显著的优势:它们是持久的(如果断电,其内容不会丢失),并且它们每千兆字节的成本比 RAM 低。
|
||||
|
||||
随着 RAM 变得更便宜,每千兆字节成本的论点被削弱了。许多数据集根本不是那么大,因此将它们完全保存在内存中是完全可行的,可能分布在几台机器上。这导致了 *内存数据库* 的发展。
|
||||
随着 RAM 变得更便宜,每千兆字节成本的论点被侵蚀。许多数据集根本不是那么大,因此将它们完全保留在内存中是完全可行的,可能分布在几台机器上。这导致了 *内存数据库* 的发展。
|
||||
|
||||
一些内存键值存储,如 Memcached,仅用于缓存使用,如果机器重新启动,数据丢失是可以接受的。但其他内存数据库旨在持久性,这可以通过特殊硬件(如电池供电的 RAM)、将更改日志写入磁盘、将定期快照写入磁盘或将内存状态复制到其他机器来实现。
|
||||
一些内存键值存储,例如 Memcached,仅用于缓存,如果机器重新启动,数据丢失是可以接受的。但其他内存数据库旨在实现持久性,这可以通过特殊硬件(例如电池供电的 RAM)、将更改日志写入磁盘、将定期快照写入磁盘或将内存状态复制到其他机器来实现。
|
||||
|
||||
当内存数据库重新启动时,它需要重新加载其状态,要么从磁盘,要么通过网络从副本(除非使用特殊硬件)。尽管写入磁盘,它仍然是内存数据库,因为磁盘仅用作持久性的仅追加日志,读取完全从内存提供服务。写入磁盘还具有操作优势:磁盘上的文件可以轻松备份、检查和由外部实用程序分析。
|
||||
当内存数据库重新启动时,它需要重新加载其状态,要么从磁盘,要么通过网络从副本(除非使用特殊硬件)。尽管写入磁盘,它仍然是一个内存数据库,因为磁盘仅用作持久性的仅追加日志,读取完全从内存提供。写入磁盘还具有操作优势:磁盘上的文件可以轻松备份、检查和由外部实用程序分析。
|
||||
|
||||
VoltDB、SingleStore 和 Oracle TimesTen 等产品是具有关系模型的内存数据库,供应商声称通过消除管理磁盘数据结构相关的所有开销,它们可以提供巨大的性能改进 [^46] [^47]。RAMCloud 是一个开源的、具有持久性的内存键值存储(对内存中的数据以及磁盘上的数据使用日志结构方法)[^48]。
|
||||
VoltDB、SingleStore 和 Oracle TimesTen 等产品是具有关系模型的内存数据库,供应商声称,通过消除管理磁盘数据结构相关的所有开销,它们可以提供巨大的性能改进 [^46] [^47]。RAMCloud 是一个开源的内存键值存储,具有持久性(对内存中的数据以及磁盘上的数据使用日志结构方法)[^48]。
|
||||
|
||||
Redis 和 Couchbase 通过异步写入磁盘提供弱持久性。
|
||||
|
||||
反直觉的是,内存数据库的性能优势不是因为它们不需要从磁盘读取。即使基于磁盘的存储引擎,如果你有足够的内存,也可能永远不需要从磁盘读取,因为操作系统无论如何都会在内存中缓存最近使用的磁盘块。相反,它们可以更快,因为它们可以避免以可以写入磁盘的形式编码内存数据结构的开销 [^49]。
|
||||
反直觉的是,内存数据库的性能优势不是因为它们不需要从磁盘读取。即使是基于磁盘的存储引擎,如果你有足够的内存,也可能永远不需要从磁盘读取,因为操作系统无论如何都会在内存中缓存最近使用的磁盘块。相反,它们可以更快,因为它们可以避免将内存数据结构编码为可以写入磁盘的形式的开销 [^49]。
|
||||
|
||||
除了性能,内存数据库的另一个有趣领域是提供难以用基于磁盘的索引实现的数据模型。例如,Redis 为各种数据结构(如优先级队列和集合)提供类似数据库的接口。因为它将所有数据保存在内存中,所以它的实现相对简单。
|
||||
除了性能,内存数据库的另一个有趣领域是提供难以使用基于磁盘的索引实现的数据模型。例如,Redis 为各种数据结构(例如优先队列和集合)提供类似数据库的接口。因为它将所有数据保留在内存中,其实现相对简单。
|
||||
|
||||
|
||||
## 分析型数据存储 {#sec_storage_analytics}
|
||||
|
||||
数据仓库的数据模型最常见的是关系型,因为 SQL 通常非常适合分析查询。有许多图形化数据分析工具可以生成 SQL 查询、可视化结果,并允许分析师探索数据(通过 *下钻* 和 *切片切块* 等操作)。
|
||||
数据仓库的数据模型最常见的是关系型,因为 SQL 通常非常适合分析查询。有许多图形数据分析工具可以生成 SQL 查询、可视化结果,并允许分析师探索数据(通过 *下钻* 和 *切片切块* 等操作)。
|
||||
|
||||
表面上,数据仓库和关系型 OLTP 数据库看起来很相似,因为它们都有 SQL 查询接口。然而,系统的内部可能看起来完全不同,因为它们针对非常不同的查询模式进行了优化。许多数据库供应商现在专注于支持事务处理或分析工作负载,但不是两者都支持。
|
||||
表面上,数据仓库和关系型 OLTP 数据库看起来很相似,因为它们都有 SQL 查询接口。然而,系统的内部可能看起来完全不同,因为它们针对非常不同的查询模式进行了优化。许多数据库供应商现在专注于支持事务处理或分析工作负载,但不是两者兼而有之。
|
||||
|
||||
一些数据库,如 Microsoft SQL Server、SAP HANA 和 SingleStore,在同一产品中支持事务处理和数据仓库。然而,这些混合事务和分析处理(HTAP)数据库(在 ["数据仓库"](/ch1#sec_introduction_dwh) 中介绍)越来越多地变成两个独立的存储和查询引擎,它们恰好可以通过通用的 SQL 接口访问 [^50] [^51] [^52] [^53]。
|
||||
一些数据库,如 Microsoft SQL Server、SAP HANA 和 SingleStore,在同一产品中支持事务处理和数据仓库。然而,这些混合事务和分析处理(HTAP)数据库(在 ["数据仓库"](/ch1#sec_introduction_dwh) 中介绍)越来越多地成为两个独立的存储和查询引擎,它们恰好可以通过通用的 SQL 接口访问 [^50] [^51] [^52] [^53]。
|
||||
|
||||
### 云数据仓库 {#sec_cloud_data_warehouses}
|
||||
|
||||
Teradata、Vertica 和 SAP HANA 等数据仓库供应商既销售商业许可的本地仓库,也销售基于云的解决方案。但随着他们的许多客户迁移到云,新的云数据仓库如 Google Cloud BigQuery、Amazon Redshift 和 Snowflake 也已被广泛采用。与传统数据仓库不同,云数据仓库利用可扩展的云基础设施,如对象存储和无服务器计算平台。
|
||||
Teradata、Vertica 和 SAP HANA 等数据仓库供应商既销售商业许可下的本地仓库,也销售基于云的解决方案。但随着他们的许多客户转向云,新的云数据仓库(如 Google Cloud BigQuery、Amazon Redshift 和 Snowflake)也变得广泛采用。与传统数据仓库不同,云数据仓库利用可扩展的云基础设施,如对象存储和无服务器计算平台。
|
||||
|
||||
云数据仓库往往与其他云服务集成得更好,并且更具弹性。例如,许多云仓库支持自动日志摄取,并提供与数据处理框架(如 Google Cloud 的 Dataflow 或 Amazon Web Services 的 Kinesis)的轻松集成。这些仓库也更具弹性,因为它们将查询计算与存储层解耦 [^54]。数据持久化在对象存储而不是本地磁盘上,这使得独立调整存储容量和查询计算资源变得容易,正如我们之前在 ["云原生系统架构"](/ch1#sec_introduction_cloud_native) 中看到的。
|
||||
云数据仓库往往与其他云服务更好地集成,并且更具弹性。例如,许多云仓库支持自动日志摄取,并提供与数据处理框架(如 Google Cloud 的 Dataflow 或 Amazon Web Services 的 Kinesis)的轻松集成。这些仓库也更具弹性,因为它们将查询计算与存储层解耦 [^54]。数据持久存储在对象存储而不是本地磁盘上,这使得可以独立调整存储容量和查询的计算资源,正如我们之前在 ["云原生系统架构"](/ch1#sec_introduction_cloud_native) 中看到的。
|
||||
|
||||
开源数据仓库如 Apache Hive、Trino 和 Apache Spark 也随着云而发展。随着分析数据存储转移到对象存储上的数据湖,开源仓库已经开始分解 [^55]。以下组件以前集成在单个系统(如 Apache Hive)中,现在通常作为单独的组件实现:
|
||||
Apache Hive、Trino 和 Apache Spark 等开源数据仓库也随着云的发展而发展。随着分析数据存储转移到对象存储上的数据湖,开源仓库已经开始分解 [^55]。以下组件以前集成在单个系统(如 Apache Hive)中,现在通常作为单独的组件实现:
|
||||
|
||||
查询引擎
|
||||
: 查询引擎如 Trino、Apache DataFusion 和 Presto 解析 SQL 查询,将它们优化为执行计划,并针对数据执行它们。执行通常需要并行的分布式数据处理任务。一些查询引擎提供内置任务执行,而其他查询引擎选择使用第三方执行框架,如 Apache Spark 或 Apache Flink。
|
||||
: Trino、Apache DataFusion 和 Presto 等查询引擎解析 SQL 查询,将其优化为执行计划,并针对数据执行它们。执行通常需要并行、分布式数据处理任务。一些查询引擎提供内置任务执行,而其他选择使用第三方执行框架,如 Apache Spark 或 Apache Flink。
|
||||
|
||||
存储格式
|
||||
: 存储格式决定了表的行如何编码为文件中的字节,然后通常存储在对象存储或分布式文件系统中 [^12]。然后,查询引擎可以访问此数据,但使用数据湖的其他应用程序也可以访问。此类存储格式的示例包括 Parquet、ORC、Lance 或 Nimble,我们将在下一节中看到更多相关内容。
|
||||
: 存储格式确定表的行如何编码为文件中的字节,然后通常存储在对象存储或分布式文件系统中 [^12]。然后查询引擎可以访问这些数据,但使用数据湖的其他应用程序也可以访问。此类存储格式的示例包括 Parquet、ORC、Lance 或 Nimble,我们将在下一节中看到更多关于它们的内容。
|
||||
|
||||
表格式
|
||||
: 以 Apache Parquet 和类似存储格式编写的文件一旦写入通常是不可变的。为了支持行插入和删除,使用表格式,如 Apache Iceberg 或 Databricks 的 Delta 格式。表格式指定一种文件格式,该格式定义哪些文件构成表以及表的模式。此类格式还提供高级功能,如时间旅行(查询表在先前时间点的状态的能力)、垃圾收集,甚至事务。
|
||||
: 以 Apache Parquet 和类似存储格式编写的文件一旦编写通常是不可变的。为了支持行插入和删除,使用 Apache Iceberg 或 Databricks 的 Delta 格式等表格式。表格式指定定义哪些文件构成表以及表模式的文件格式。此类格式还提供高级功能,例如时间旅行(查询表在以前时间点的能力)、垃圾回收,甚至事务。
|
||||
|
||||
数据目录
|
||||
: 就像表格式定义哪些文件组成表一样,数据目录定义哪些表组成数据库。目录用于创建、重命名和删除表。与存储和表格式不同,数据目录如 Snowflake 的 Polaris 和 Databricks 的 Unity Catalog 通常作为独立服务运行,可以使用 REST 接口查询。Apache Iceberg 也提供目录,可以在客户端内运行或作为单独的进程运行。查询引擎在读取和写入表时使用目录信息。传统上,目录和查询引擎是集成的,但将它们解耦使数据发现和数据治理系统(在 ["数据系统、法律和社会"](/ch1#sec_introduction_compliance) 中讨论)也能访问目录的元数据。
|
||||
: 就像表格式定义哪些文件构成表一样,数据目录定义哪些表组成数据库。目录用于创建、重命名和删除表。与存储和表格式不同,Snowflake 的 Polaris 和 Databricks 的 Unity Catalog 等数据目录通常作为可以使用 REST 接口查询的独立服务运行。Apache Iceberg 也提供目录,可以在客户端内运行或作为单独的进程运行。查询引擎在读取和写入表时使用目录信息。传统上,目录和查询引擎已经集成,但将它们解耦使数据发现和数据治理系统(在 ["数据系统、法律和社会"](/ch1#sec_introduction_compliance) 中讨论)也能够访问目录的元数据。
|
||||
|
||||
### 列式存储 {#sec_storage_column}
|
||||
|
||||
如 ["星型和雪花型:分析模式"](/ch3#sec_datamodels_analytics) 中所讨论的,数据仓库按照惯例通常使用关系模式,其中有一个包含对维度表的外键引用的大型事实表。如果你的事实表中有数万亿行和 PB 级数据,有效地存储和查询它们就成为一个具有挑战性的问题。维度表通常要小得多(数百万行),因此在本节中我们将重点关注事实的存储。
|
||||
如 ["星型和雪花型:分析模式"](/ch3#sec_datamodels_analytics) 中所讨论的,数据仓库按照惯例通常使用带有大型事实表的关系模式,该表包含对维度表的外键引用。如果你的事实表中有数万亿行和数 PB 的数据,有效地存储和查询它们就成为一个具有挑战性的问题。维度表通常要小得多(数百万行),因此在本节中我们将重点关注事实的存储。
|
||||
|
||||
尽管事实表通常超过 100 列宽,但典型的数据仓库查询一次只访问其中的 4 或 5 列(分析很少需要 `"SELECT *"` 查询)[^52]。以 [示例 4-1](/ch4#fig_storage_analytics_query) 中的查询为例:它访问大量行(2024 日历年中每次有人购买水果或糖果的情况),但它只需要访问 `fact_sales` 表的三列:`date_key`、`product_sk` 和 `quantity`。查询忽略所有其他列。
|
||||
尽管事实表通常有超过 100 列,但典型的数据仓库查询一次只访问其中的 4 或 5 列(分析很少需要 `"SELECT *"` 查询)[^52]。以 [示例 4-1](#fig_storage_analytics_query) 中的查询为例:它访问大量行(2024 日历年期间每次有人购买水果或糖果的情况),但它只需要访问 `fact_sales` 表的三列:`date_key`、`product_sk` 和 `quantity`。查询忽略所有其他列。
|
||||
|
||||
{{< figure id="fig_storage_analytics_query" title="示例 4-1. 分析人们是否更倾向于购买新鲜水果或糖果,取决于星期几" class="w-full my-4" >}}
|
||||
|
||||
@ -379,40 +379,40 @@ GROUP BY
|
||||
dim_date.weekday, dim_product.category;
|
||||
```
|
||||
|
||||
我们如何有效地执行这个查询?
|
||||
我们如何高效地执行这个查询?
|
||||
|
||||
在大多数 OLTP 数据库中,存储以 *行式* 方式布局:表的一行中的所有值彼此相邻存储。文档数据库类似:整个文档通常作为一个连续的字节序列存储。你可以在 [图 4-1](/ch4#fig_storage_csv_hash_index) 的 CSV 示例中看到这一点。
|
||||
在大多数 OLTP 数据库中,存储是以 *面向行* 的方式布局的:表中一行的所有值彼此相邻存储。文档数据库类似:整个文档通常作为一个连续的字节序列存储。你可以在 [图 4-1](#fig_storage_csv_hash_index) 的 CSV 示例中看到这一点。
|
||||
|
||||
为了处理像 [示例 4-1](/ch4#fig_storage_analytics_query) 这样的查询,你可能在 `fact_sales.date_key` 和/或 `fact_sales.product_sk` 上有索引,它们告诉存储引擎在哪里找到特定日期或特定产品的所有销售。但是,行式存储引擎仍然需要将所有这些行(每行由 100 多个属性组成)从磁盘加载到内存中,解析它们,并过滤掉不符合所需条件的行。这可能需要很长时间。
|
||||
为了处理像 [示例 4-1](#fig_storage_analytics_query) 这样的查询,你可能在 `fact_sales.date_key` 和/或 `fact_sales.product_sk` 上有索引,告诉存储引擎在哪里找到特定日期或特定产品的所有销售。但是,面向行的存储引擎仍然需要将所有这些行(每行包含超过 100 个属性)从磁盘加载到内存中,解析它们,并过滤掉不符合所需条件的行。这可能需要很长时间。
|
||||
|
||||
*列式*(或 *列式*)存储背后的想法很简单:不要将一行中的所有值存储在一起,而是将每个 *列* 中的所有值存储在一起 [^56]。如果每列单独存储,查询只需要读取和解析该查询中使用的那些列,这可以节省大量工作。[图 4-7](/ch4#fig_column_store) 使用 [图 3-5](/ch3#fig_dwh_schema) 中事实表的扩展版本显示了这一原理。
|
||||
*面向列*(或 *列式*)存储背后的想法很简单:不要将一行中的所有值存储在一起,而是将每 *列* 中的所有值存储在一起 [^56]。如果每列单独存储,查询只需要读取和解析该查询中使用的那些列,这可以节省大量工作。[图 4-7](#fig_column_store) 使用 [图 3-5](/ch3#fig_dwh_schema) 中事实表的扩展版本展示了这一原理。
|
||||
|
||||
--------
|
||||
|
||||
> [!NOTE]
|
||||
> 列存储在关系数据模型中最容易理解,但它同样适用于非关系数据。例如,Parquet [^57] 是一种支持文档数据模型的列式存储格式,基于 Google 的 Dremel [^58],使用一种称为 *切碎* 或 *条带化* 的技术 [^59]。
|
||||
> 列存储在关系数据模型中最容易理解,但它同样适用于非关系数据。例如,Parquet [^57] 是一种列式存储格式,它支持基于 Google 的 Dremel [^58] 的文档数据模型,使用一种称为 *分解*(*shredding*)或 *条带化*(*striping*)的技术 [^59]。
|
||||
|
||||
--------
|
||||
|
||||
{{< figure src="/fig/ddia_0407.png" id="fig_column_store" caption="图 4-7. 按列而不是按行存储关系数据。" class="w-full my-4" >}}
|
||||
|
||||
列式存储布局依赖于每列以相同顺序存储行。因此,如果你需要重新组装整行,你可以从每个单独列中取出第 23 个条目,并将它们组合在一起形成表的第 23 行。
|
||||
面向列的存储布局依赖于每列以相同顺序存储行。因此,如果你需要重新组装整行,你可以从每个单独的列中取出第 23 个条目,并将它们组合在一起形成表的第 23 行。
|
||||
|
||||
实际上,列式存储引擎实际上并不会一次存储整个列(可能包含数万亿行)。相反,它们将表分成数千或数百万行的块,并在每个块内分别存储每列的值 [^60]。由于许多查询限制在特定日期范围内,因此通常使每个块包含特定时间戳范围的行。然后查询只需要在与所需日期范围重叠的那些块中加载它需要的列。
|
||||
实际上,列式存储引擎并不真的一次存储整个列(可能包含数万亿行)。相反,它们将表分解为数千或数百万行的块,并且在每个块内,它们分别存储每列的值 [^60]。由于许多查询都限制在特定的日期范围内,因此通常使每个块包含特定时间戳范围的行。然后查询只需要在与所需日期范围重叠的那些块中加载它需要的列。
|
||||
|
||||
列式存储现在几乎用于所有分析数据库 [^60],从大规模云数据仓库(如 Snowflake [^61])到单节点嵌入式数据库(如 DuckDB [^62]),以及产品分析系统(如 Pinot [^63] 和 Druid [^64])。它用于存储格式,如 Parquet、ORC [^65] [^66]、Lance [^67] 和 Nimble [^68],以及内存分析格式,如 Apache Arrow [^65] [^69] 和 Pandas/NumPy [^70]。一些时间序列数据库,如 InfluxDB IOx [^71] 和 TimescaleDB [^72],也基于列式存储。
|
||||
列式存储如今几乎用于所有分析数据库 [^60],从大规模云数据仓库(如 Snowflake [^61])到单节点嵌入式数据库(如 DuckDB [^62]),以及产品分析系统(如 Pinot [^63] 和 Druid [^64])。它用于存储格式,如 Parquet、ORC [^65] [^66]、Lance [^67] 和 Nimble [^68],以及内存分析格式,如 Apache Arrow [^65] [^69] 和 Pandas/NumPy [^70]。一些时间序列数据库,如 InfluxDB IOx [^71] 和 TimescaleDB [^72],也基于面向列的存储。
|
||||
|
||||
#### 列压缩 {#sec_storage_column_compression}
|
||||
|
||||
除了只从磁盘加载查询所需的那些列外,我们还可以通过压缩数据进一步减少对磁盘吞吐量和网络带宽的需求。幸运的是,列式存储通常非常适合压缩。
|
||||
除了只从磁盘加载查询所需的那些列之外,我们还可以通过压缩数据进一步减少对磁盘吞吐量和网络带宽的需求。幸运的是,面向列的存储通常非常适合压缩。
|
||||
|
||||
看看 [图 4-7](/ch4#fig_column_store) 中每列的值序列:它们通常看起来相当重复,这是压缩的好兆头。根据列中的数据,可以使用不同的压缩技术。在数据仓库中特别有效的一种技术是 *位图编码*,如 [图 4-8](/ch4#fig_bitmap_index) 所示。
|
||||
看看 [图 4-7](#fig_column_store) 中每列的值序列:它们看起来经常重复,这是压缩的良好迹象。根据列中的数据,可以使用不同的压缩技术。在数据仓库中特别有效的一种技术是 *位图编码*,如 [图 4-8](#fig_bitmap_index) 所示。
|
||||
|
||||
{{< figure src="/fig/ddia_0408.png" id="fig_bitmap_index" caption="图 4-8. 单列的压缩、位图索引存储。" class="w-full my-4" >}}
|
||||
|
||||
通常,列中不同值的数量与行数相比很小(例如,零售商可能有数十亿笔销售交易,但只有 100,000 种不同的产品)。我们现在可以将具有 *n* 个不同值的列转换为 *n* 个单独的位图:每个不同值一个位图,每行一位。如果行具有该值,则位为 1,否则为 0。
|
||||
通常,列中不同值的数量与行数相比很小(例如,零售商可能有数十亿条销售交易,但只有 100,000 种不同的产品)。我们现在可以将具有 *n* 个不同值的列转换为 *n* 个单独的位图:每个不同值一个位图,每行一位。如果该行具有该值,则该位为 1,否则为 0。
|
||||
|
||||
一种选择是使用每行一位存储这些位图。然而,这些位图通常包含大量零(我们说它们是 *稀疏* 的)。在这种情况下,位图可以额外进行游程编码:计算连续零或一的数量并存储该数字,如 [图 4-8](/ch4#fig_bitmap_index) 底部所示。*咆哮位图* 等技术在两种位图表示之间切换,使用更紧凑的表示 [^73]。这可以使列的编码非常高效。
|
||||
一种选择是使用每行一位来存储这些位图。然而,这些位图通常包含大量零(我们说它们是 *稀疏* 的)。在这种情况下,位图可以另外进行游程编码:计算连续零或一的数量并存储该数字,如 [图 4-8](#fig_bitmap_index) 底部所示。诸如 *咆哮位图*(*roaring bitmaps*)之类的技术在两种位图表示之间切换,使用最紧凑的表示 [^73]。这可以使列的编码非常高效。
|
||||
|
||||
像这样的位图索引非常适合数据仓库中常见的查询类型。例如:
|
||||
|
||||
@ -420,179 +420,178 @@ GROUP BY
|
||||
: 加载 `product_sk = 31`、`product_sk = 68` 和 `product_sk = 69` 的三个位图,并计算三个位图的按位 *OR*,这可以非常高效地完成。
|
||||
|
||||
`WHERE product_sk = 30 AND store_sk = 3:`
|
||||
: 加载 `product_sk = 30` 和 `store_sk = 3` 的位图,并计算按位 *AND*。这有效是因为列以相同顺序包含行,因此一列位图中的第 *k* 位对应于另一列位图中第 *k* 位的同一行。
|
||||
: 加载 `product_sk = 30` 和 `store_sk = 3` 的位图,并计算按位 *AND*。这有效是因为列以相同的顺序包含行,所以一列位图中的第 *k* 位对应于另一列位图中第 *k* 位的同一行。
|
||||
|
||||
位图也可用于回答图查询,例如查找社交网络中被用户 *X* 关注且也关注用户 *Y* 的所有用户 [^74]。列式数据库还有各种其他压缩方案,你可以在参考文献中找到 [^75]。
|
||||
位图也可用于回答图查询,例如查找社交网络中被用户 *X* 关注并且也关注用户 *Y* 的所有用户 [^74]。列式数据库还有各种其他压缩方案,你可以在参考文献中找到 [^75]。
|
||||
|
||||
--------
|
||||
|
||||
> [!NOTE]
|
||||
> 不要将列式数据库与 *宽列*(也称为 *列族*)数据模型混淆,在该模型中,一行可以有数千列,并且不需要所有行都具有相同的列 [^9]。尽管名称相似,宽列数据库是行式的,因为它们将一行中的所有值存储在一起。Google 的 Bigtable、Apache Accumulo 和 HBase 是宽列模型的示例。
|
||||
> 不要将面向列的数据库与 *宽列*(也称为 *列族*)数据模型混淆,在该模型中,一行可以有数千列,并且不需要所有行都有相同的列 [^9]。尽管名称相似,宽列数据库是面向行的,因为它们将一行中的所有值存储在一起。Google 的 Bigtable、Apache Accumulo 和 HBase 是宽列模型的例子。
|
||||
|
||||
--------
|
||||
|
||||
#### 列存储中的排序顺序 {#sort-order-in-column-storage}
|
||||
|
||||
在列存储中,行存储的顺序不一定重要。最简单的是按插入顺序存储它们,因为插入新行只意味着追加到每列。然而,我们可以选择施加顺序,就像我们之前对 SSTable 所做的那样,并将其用作索引机制。
|
||||
在列存储中,行的存储顺序并不一定重要。最简单的是按插入顺序存储它们,因为这样插入新行只需追加到每列。但是,我们可以选择强制执行顺序,就像我们之前对 SSTable 所做的那样,并将其用作索引机制。
|
||||
|
||||
请注意,独立排序每列是没有意义的,因为那样我们就不再知道列中的哪些项属于同一行。我们只能重建一行,因为我们知道一列中的第 *k* 项与另一列中的第 *k* 项属于同一行。
|
||||
请注意,独立排序每列是没有意义的,因为那样我们就不再知道列中的哪些项属于同一行。我们只能重建一行,因为我们知道一列中的第 *k* 个项与另一列中的第 *k* 个项属于同一行。
|
||||
|
||||
相反,数据需要一次排序整行,即使它是按列存储的。数据库管理员可以使用他们对常见查询的了解来选择表应该按哪些列排序。例如,如果查询经常针对日期范围(如上个月),那么将 `date_key` 作为第一个排序键可能是有意义的。然后查询只能扫描上个月的行,这将比扫描所有行快得多。
|
||||
相反,数据需要一次排序整行,即使它是按列存储的。数据库管理员可以使用他们对常见查询的了解来选择表应按哪些列排序。例如,如果查询经常针对日期范围(例如上个月),则将 `date_key` 作为第一个排序键可能是有意义的。然后查询可以只扫描上个月的行,这将比扫描所有行快得多。
|
||||
|
||||
第二列可以确定在第一列中具有相同值的任何行的排序顺序。例如,如果 `date_key` 是 [图 4-7](/ch4#fig_column_store) 中的第一个排序键,那么 `product_sk` 作为第二个排序键可能是有意义的,这样同一天同一产品的所有销售在存储中组合在一起。这将有助于需要在特定日期范围内按产品分组或过滤销售的查询。
|
||||
第二列可以确定在第一列中具有相同值的任何行的排序顺序。例如,如果 `date_key` 是 [图 4-7](#fig_column_store) 中的第一个排序键,那么 `product_sk` 作为第二个排序键可能是有意义的,这样同一天同一产品的所有销售都在存储中分组在一起。这将有助于需要在某个日期范围内按产品分组或过滤销售的查询。
|
||||
|
||||
排序顺序的另一个优点是它可以帮助列的压缩。如果主排序列没有很多不同的值,那么排序后,它将有很长的序列,其中相同的值连续重复多次。简单的游程编码(就像我们在 [图 4-8](/ch4#fig_bitmap_index) 中用于位图的那样)可以将该列压缩到几千字节——即使表有数十亿行。
|
||||
排序顺序的另一个优点是它可以帮助压缩列。如果主排序列没有许多不同的值,那么排序后,它将有很长的序列,其中相同的值在一行中重复多次。简单的游程编码,就像我们在 [图 4-8](#fig_bitmap_index) 中用于位图的那样,可以将该列压缩到几千字节 —— 即使表有数十亿行。
|
||||
|
||||
该压缩效果在第一个排序键上最强。第二个和第三个排序键将更加混乱,因此不会有如此长的重复值运行。排序优先级更低的列基本上以随机顺序出现,因此它们可能不会压缩得那么好。但是,对前几列进行排序总体上仍然是一种胜利。
|
||||
该压缩效果在第一个排序键上最强。第二和第三个排序键将更加混乱,因此不会有如此长的重复值运行。排序优先级较低的列基本上以随机顺序出现,因此它们可能不会压缩得那么好。但是,让前几列排序仍然是整体上的胜利。
|
||||
|
||||
#### 写入列式存储 {#writing-to-column-oriented-storage}
|
||||
|
||||
我们在 ["表征事务处理和分析"](/ch1#sec_introduction_oltp) 中看到,数据仓库中的读取往往包括对大量行的聚合;列式存储、压缩和排序都有助于使这些读取查询更快。数据仓库中的写入往往是数据的批量导入,通常通过 ETL 过程。
|
||||
我们在 ["事务处理和分析的特征"](/ch1#sec_introduction_oltp) 中看到,数据仓库中的读取往往包括大量行的聚合;列式存储、压缩和排序都有助于使这些读取查询更快。数据仓库中的写入往往是数据的批量导入,通常通过 ETL 过程。
|
||||
|
||||
使用列式存储,在排序表中间某处写入单个行将非常低效,因为你必须从插入位置开始重写所有压缩列。然而,一次批量写入许多行可以分摊重写这些列的成本,使其高效。
|
||||
使用列式存储,在排序表的中间某处写入单个行将非常低效,因为你必须从插入位置开始重写所有压缩列。但是,一次批量写入许多行会分摊重写这些列的成本,使其高效。
|
||||
|
||||
通常使用日志结构方法批量执行写入。所有写入首先进入行式、排序的内存存储。当积累了足够的写入时,它们与磁盘上的列编码文件合并,并批量写入新文件。由于旧文件保持不可变,新文件一次写入,对象存储非常适合存储这些文件。
|
||||
通常使用日志结构方法以批次执行写入。所有写入首先进入面向行的、排序的内存存储。当积累了足够的写入时,它们将与磁盘上的列编码文件合并,并批量写入新文件。由于旧文件保持不可变,新文件一次写入,对象存储非常适合存储这些文件。
|
||||
|
||||
查询需要检查磁盘上的列数据和内存中的最近写入,并将两者结合起来。查询执行引擎向用户隐藏了这种区别。从分析师的角度来看,通过插入、更新或删除修改的数据会立即反映在后续查询中。Snowflake、Vertica、Apache Pinot、Apache Druid 和许多其他系统都这样做 [^61] [^63] [^64] [^76]。
|
||||
查询需要检查磁盘上的列数据和内存中的最近写入,并将两者结合起来。查询执行引擎对用户隐藏了这种区别。从分析师的角度来看,已通过插入、更新或删除修改的数据会立即反映在后续查询中。Snowflake、Vertica、Apache Pinot、Apache Druid 和许多其他系统都这样做 [^61] [^63] [^64] [^76]。
|
||||
|
||||
|
||||
### 查询执行:编译与向量化 {#sec_storage_vectorized}
|
||||
|
||||
复杂的分析 SQL 查询被分解为由多个阶段组成的 *查询计划*,称为 *算子*,这些算子可能分布在多台机器上以进行并行执行。查询规划器可以通过选择使用哪些算子、以什么顺序执行它们以及在哪里运行每个算子来执行大量优化。
|
||||
用于分析的复杂 SQL 查询被分解为由多个阶段组成的 *查询计划*,称为 *算子*,这些算子可能分布在多台机器上以并行执行。查询规划器可以通过选择使用哪些算子、以何种顺序执行它们以及在哪里运行每个算子来执行大量优化。
|
||||
|
||||
在每个算子中,查询引擎需要对列中的值执行各种操作,例如查找值在特定值集中的所有行(可能作为连接的一部分),或检查值是否大于 15。它还需要查看同一行的几列,例如查找所有产品是香蕉且商店是特定感兴趣商店的销售交易。
|
||||
在每个算子内,查询引擎需要对列中的值执行各种操作,例如查找值在特定值集中的所有行(可能作为连接的一部分),或检查值是否大于 15。它还需要查看同一行的几列,例如查找产品是香蕉且商店是特定感兴趣商店的所有销售交易。
|
||||
|
||||
对于需要扫描数百万行的数据仓库查询,我们不仅需要担心它们需要从磁盘读取的数据量,还需要担心执行复杂算子所需的 CPU 时间。最简单的算子类型就像编程语言的解释器:在迭代每行时,它检查表示查询的数据结构,以找出需要对哪些列执行哪些比较或计算。不幸的是,这对于许多分析目的来说太慢了。出现了两种高效查询执行的替代方法 [^77]:
|
||||
对于需要扫描数百万行的数据仓库查询,我们不仅需要担心它们需要从磁盘读取的数据量,还需要担心执行复杂算子所需的 CPU 时间。最简单的算子类型就像编程语言的解释器:在遍历每一行时,它检查表示查询的数据结构,以找出需要对哪些列执行哪些比较或计算。不幸的是,这对许多分析目的来说太慢了。高效查询执行的两种替代方法已经出现 [^77]:
|
||||
|
||||
查询编译
|
||||
: 查询引擎获取 SQL 查询并生成用于执行它的代码。代码逐行迭代,查看感兴趣列中的值,执行所需的任何比较或计算,并在满足所需条件时将必要的值复制到输出缓冲区。查询引擎将生成的代码编译为机器代码(通常使用现有编译器,如 LLVM),然后在已加载到内存中的列编码数据上运行它。这种代码生成方法类似于 Java 虚拟机(JVM)和类似运行时中使用的即时(JIT)编译方法。
|
||||
: 查询引擎获取 SQL 查询并生成用于执行它的代码。代码逐行迭代,查看感兴趣列中的值,执行所需的任何比较或计算,如果满足所需条件,则将必要的值复制到输出缓冲区。查询引擎将生成的代码编译为机器代码(通常使用现有编译器,如 LLVM),然后在已加载到内存中的列编码数据上运行它。这种代码生成方法类似于 Java 虚拟机(JVM)和类似运行时中使用的即时(JIT)编译方法。
|
||||
|
||||
向量化处理
|
||||
: 查询被解释,而不是编译,但通过批量处理列中的许多值而不是逐行迭代来加快速度。一组固定的预定义算子内置于数据库中;我们可以向它们传递参数并获得一批结果 [^50] [^75]。
|
||||
: 查询被解释,而不是编译,但通过批量处理列中的许多值而不是逐行迭代来提高速度。一组固定的预定义算子内置在数据库中;我们可以向它们传递参数并获得一批结果 [^50] [^75]。
|
||||
|
||||
例如,我们可以将 `product_sk` 列和"香蕉"的 ID 传递给相等算子,并获得一个位图(输入列中每个值一位,如果是香蕉则为 1);然后我们可以将 `store_sk` 列和感兴趣商店的 ID 传递给同一个相等算子,并获得另一个位图;然后我们可以将两个位图传递给"按位 AND"算子,如 [图 4-9](/ch4#fig_bitmap_and) 所示。结果将是一个位图,其中包含特定商店中所有香蕉销售的 1。
|
||||
例如,我们可以将 `product_sk` 列和"香蕉"的 ID 传递给相等算子,并获得一个位图(输入列中每个值一位,如果是香蕉则为 1);然后我们可以将 `store_sk` 列和感兴趣商店的 ID 传递给相同的相等算子,并获得另一个位图;然后我们可以将两个位图传递给"按位 AND"算子,如 [图 4-9](#fig_bitmap_and) 所示。结果将是一个位图,包含特定商店中所有香蕉销售的 1。
|
||||
|
||||
{{< figure src="/fig/ddia_0409.png" id="fig_bitmap_and" caption="图 4-9. 两个位图之间的按位 AND 适合向量化。" class="w-full my-4" >}}
|
||||
|
||||
这两种方法在实现方面非常不同,但在实践中都在使用 [^77]。两者都可以通过利用现代 CPU 的特性来实现非常好的性能:
|
||||
这两种方法在实现方面非常不同,但两者都在实践中使用 [^77]。两者都可以通过利用现代 CPU 的特性来实现非常好的性能:
|
||||
|
||||
* 优先顺序内存访问而不是随机访问以减少缓存未命中 [^78],
|
||||
* 在紧密的内部循环中完成大部分工作(即,使用少量指令且没有函数调用)以保持 CPU 指令处理管道繁忙并避免分支错误预测,
|
||||
* 利用并行性,如多线程和单指令多数据(SIMD)指令 [^79] [^80],以及
|
||||
* 直接对压缩数据进行操作,而无需将其解码为单独的内存表示,这节省了内存分配和复制成本。
|
||||
* 优先选择顺序内存访问而不是随机访问以减少缓存未命中 [^78],
|
||||
* 在紧密的内部循环中完成大部分工作(即,具有少量指令且没有函数调用)以保持 CPU 指令处理管道繁忙并避免分支预测错误,
|
||||
* 利用并行性,例如多线程和单指令多数据(SIMD)指令 [^79] [^80],以及
|
||||
* 直接对压缩数据进行操作,而无需将其解码为单独的内存表示,这可以节省内存分配和复制成本。
|
||||
|
||||
### 物化视图与多维数据集 {#sec_storage_materialized_views}
|
||||
|
||||
我们之前在 ["物化和更新时间线"](/ch2#sec_introduction_materializing) 中遇到了 *物化视图*:在关系数据模型中,它们是类似表的对象,其内容是某些查询的结果。区别在于物化视图是查询结果的实际副本,写入磁盘,而虚拟视图只是编写查询的快捷方式。当你从虚拟视图读取时,SQL 引擎会即时将其扩展为视图的底层查询,然后处理扩展的查询。
|
||||
我们之前在 ["物化和更新时间线"](/ch2#sec_introduction_materializing) 中遇到了 *物化视图*:在关系数据模型中,它们是表状对象,其内容是某些查询的结果。区别在于物化视图是查询结果的实际副本,写入磁盘,而虚拟视图只是编写查询的快捷方式。当你从虚拟视图读取时,SQL 引擎会即时将其扩展为视图的基础查询,然后处理扩展的查询。
|
||||
|
||||
当底层数据发生变化时,物化视图需要相应地更新。一些数据库可以自动执行此操作,还有像 Materialize 这样专门从事物化视图维护的系统 [^81]。执行此类更新意味着写入时需要更多工作,但物化视图可以提高重复需要执行相同查询的工作负载中的读取性能。
|
||||
当基础数据更改时,物化视图需要相应更新。一些数据库可以自动执行此操作,还有像 Materialize 这样专门从事物化视图维护的系统 [^81]。执行此类更新意味着写入时需要更多工作,但物化视图可以改善在重复需要执行相同查询的工作负载中的读取性能。
|
||||
|
||||
*物化聚合* 是一种可以在数据仓库中有用的物化视图类型。如前所述,数据仓库查询通常涉及聚合函数,如 SQL 中的 `COUNT`、`SUM`、`AVG`、`MIN` 或 `MAX`。如果许多不同的查询使用相同的聚合,每次都处理原始数据可能是浪费的。为什么不缓存查询最常使用的一些计数或总和呢?*数据立方体* 或 *OLAP 立方体* 通过创建按不同维度分组的聚合网格来做到这一点 [^82]。[图 4-10](/ch4#fig_data_cube) 显示了一个示例。
|
||||
*物化聚合* 是一种可以在数据仓库中有用的物化视图类型。如前所述,数据仓库查询通常涉及聚合函数,例如 SQL 中的 `COUNT`、`SUM`、`AVG`、`MIN` 或 `MAX`。如果许多不同的查询使用相同的聚合,每次都处理原始数据可能会很浪费。为什么不缓存查询最常使用的一些计数或总和?*多维数据集* 或 *OLAP 立方体* 通过创建按不同维度分组的聚合网格来做到这一点 [^82]。[图 4-10](#fig_data_cube) 显示了一个示例。
|
||||
|
||||
{{< figure src="/fig/ddia_0410.png" id="fig_data_cube" caption="图 4-10. 数据立方体的两个维度,通过求和聚合数据。" class="w-full my-4" >}}
|
||||
{{< figure src="/fig/ddia_0410.png" id="fig_data_cube" caption="图 4-10. 多维数据集的两个维度,通过求和聚合数据。" class="w-full my-4" >}}
|
||||
|
||||
现在想象每个事实只有两个维度表的外键——在 [图 4-10](/ch4#fig_data_cube) 中,这些是 `date_key` 和 `product_sk`。你现在可以绘制一个二维表,日期沿一个轴,产品沿另一个轴。每个单元格包含具有该日期-产品组合的所有事实的属性(例如,`net_price`)的聚合(例如,`SUM`)。然后,你可以沿着每行或列应用相同的聚合,并获得已减少一个维度的摘要(无论日期如何的产品销售,或无论产品如何的日期销售)。
|
||||
现在假设每个事实只有两个维度表的外键 —— 在 [图 4-10](#fig_data_cube) 中,这些是 `date_key` 和 `product_sk`。你现在可以绘制一个二维表,日期沿着一个轴,产品沿着另一个轴。每个单元格包含具有该日期-产品组合的所有事实的属性(例如 `net_price`)的聚合(例如 `SUM`)。然后,你可以沿着每行或列应用相同的聚合,并获得已减少一个维度的摘要(不管日期的产品销售,或不管产品的日期销售)。
|
||||
|
||||
一般来说,事实通常有两个以上的维度。在 [图 3-5](/ch3#fig_dwh_schema) 中有五个维度:日期、产品、商店、促销和客户。很难想象五维超立方体会是什么样子,但原理保持不变:每个单元格包含特定日期-产品-商店-促销-客户组合的销售。然后可以沿着每个维度重复汇总这些值。
|
||||
|
||||
物化数据立方体的优点是某些查询变得非常快,因为它们已经有效地被预先计算。例如,如果你想知道昨天每家商店的总销售额,你只需要查看适当维度的总计——无需扫描数百万行。
|
||||
物化多维数据集的优点是某些查询变得非常快,因为它们已经有效地预先计算了。例如,如果你想知道昨天每个商店的总销售额,你只需要查看适当维度的总计 —— 不需要扫描数百万行。
|
||||
|
||||
缺点是数据立方体没有查询原始数据的灵活性。例如,无法计算成本超过 100 美元的商品所占销售额的比例,因为价格不是维度之一。因此,大多数数据仓库尝试保留尽可能多的原始数据,并仅将聚合(如数据立方体)用作某些查询的性能提升。
|
||||
缺点是多维数据集没有与查询原始数据相同的灵活性。例如,没有办法计算成本超过 100 美元的商品的销售比例,因为价格不是维度之一。因此,大多数数据仓库尽可能多地保留原始数据,并仅将聚合(如多维数据集)用作某些查询的性能提升。
|
||||
|
||||
|
||||
## 多维索引与全文索引 {#sec_storage_multidimensional}
|
||||
|
||||
我们在本章前半部分看到的 B 树和 LSM 树允许对单个属性进行范围查询:例如,如果键是用户名,你可以使用它们作为索引来高效地查找所有以 L 开头的名字。但有时,按单个属性搜索是不够的。
|
||||
我们在本章前半部分看到的 B 树和 LSM 树允许对单个属性进行范围查询:例如,如果键是用户名,你可以使用它们作为索引来高效查找所有以 L 开头的名称。但有时,按单个属性搜索是不够的。
|
||||
|
||||
最常见的多列索引类型称为 *联合索引*,它通过将一列追加到另一列来简单地将几个字段组合成一个键(索引定义指定字段连接的顺序)。这就像老式的纸质电话簿,它提供了从(*姓*,*名*)到电话号码的索引。由于排序顺序,索引可用于查找所有具有特定姓氏的人,或所有具有特定 *姓-名* 组合的人。但是,如果你想查找所有具有特定名字的人,索引就没用了。
|
||||
最常见的多列索引类型称为 *联合索引*,它通过将一列追加到另一列来将几个字段组合成一个键(索引定义指定字段以何种顺序连接)。这就像老式的纸质电话簿,它提供从(*姓氏*、*名字*)到电话号码的索引。由于排序顺序,索引可用于查找具有特定姓氏的所有人,或具有特定 *姓氏-名字* 组合的所有人。但是,如果你想查找具有特定名字的所有人,索引是无用的。
|
||||
|
||||
另一方面,*多维索引* 允许你一次查询多个列。这在地理空间数据中特别重要。例如,餐厅搜索网站可能有一个包含每家餐厅纬度和经度的数据库。当用户在地图上查看餐厅时,网站需要搜索用户当前查看的矩形地图区域内的所有餐厅。这需要像下面这样的二维范围查询:
|
||||
另一方面,*多维索引* 允许你一次查询多个列。在地理空间数据中这尤其重要。例如,餐厅搜索网站可能有一个包含每个餐厅的纬度和经度的数据库。当用户在地图上查看餐厅时,网站需要搜索用户当前查看的矩形地图区域内的所有餐厅。这需要像以下这样的二维范围查询:
|
||||
|
||||
```sql
|
||||
SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
|
||||
AND longitude > -0.1162 AND longitude < -0.1004;
|
||||
```
|
||||
|
||||
纬度和经度列上的联合索引无法有效地回答这种查询:它可以给你纬度范围内的所有餐厅(但在任何经度),或经度范围内的所有餐厅(但在南北极之间的任何地方),但不能同时满足两者。
|
||||
纬度和经度列上的联合索引无法有效地回答这种查询:它可以为你提供纬度范围内的所有餐厅(但在任何经度),或经度范围内的所有餐厅(但在北极和南极之间的任何地方),但不能同时提供两者。
|
||||
|
||||
一种选择是使用空间填充曲线将二维位置转换为单个数字,然后使用常规 B 树索引 [^83]。更常见的是,使用专门的空间索引,如 R 树或 Bkd 树 [^84];它们划分空间,使附近的数据点往往分组在同一子树中。例如,PostGIS 使用 PostgreSQL 的通用搜索树索引设施将地理空间索引实现为 R 树 [^85]。也可以使用规则间隔的三角形、正方形或六边形网格 [^86]。
|
||||
一种选择是使用空间填充曲线将二维位置转换为单个数字,然后使用常规 B 树索引 [^83]。更常见的是,使用专门的空间索引,如 R 树或 Bkd 树 [^84];它们划分空间,使附近的数据点倾向于分组在同一子树中。例如,PostGIS 使用 PostgreSQL 的通用搜索树索引设施将地理空间索引实现为 R 树 [^85]。也可以使用规则间隔的三角形、正方形或六边形网格 [^86]。
|
||||
|
||||
多维索引不仅仅用于地理位置。例如,在电子商务网站上,你可以在(*红*、*绿*、*蓝*)维度上使用三维索引来搜索特定颜色范围内的产品,或者在天气观测数据库中,你可以在(*日期*、*温度*)上有一个二维索引,以便有效地搜索 2013 年期间温度在 25 到 30℃ 之间的所有观测。使用一维索引,你必须扫描 2013 年的所有记录(无论温度如何),然后按温度过滤它们,或者反之亦然。二维索引可以同时按时间戳和温度缩小范围 [^87]。
|
||||
多维索引不仅用于地理位置。例如,在电子商务网站上,你可以在维度(*红色*、*绿色*、*蓝色*)上使用三维索引来搜索某个颜色范围内的产品,或者在天气观测数据库中,你可以在(*日期*、*温度*)上有一个二维索引,以便有效地搜索 2013 年期间温度在 25 到 30°C 之间的所有观测。使用一维索引,你必须扫描 2013 年的所有记录(不管温度),然后按温度过滤它们,反之亦然。二维索引可以同时按时间戳和温度缩小范围 [^87]。
|
||||
|
||||
### 全文检索 {#sec_storage_full_text}
|
||||
|
||||
全文搜索允许你通过可能出现在文本中任何位置的关键字搜索文本文档集合(网页、产品描述等)[^88]。信息检索是一个大的专业主题,通常涉及特定于语言的处理:例如,几种亚洲语言在单词之间没有空格或标点符号,因此将文本分割成单词需要一个指示哪些字符序列构成单词的模型。全文搜索还经常涉及匹配相似但不相同的单词(如拼写错误或单词的不同语法形式)和同义词。这些问题超出了本书的范围。
|
||||
全文搜索允许你通过可能出现在文本中任何位置的关键字搜索文本文档集合(网页、产品描述等)[^88]。信息检索是一个大的专业主题,通常涉及特定于语言的处理:例如,几种亚洲语言在单词之间没有空格或标点符号,因此将文本分割成单词需要一个指示哪些字符序列构成单词的模型。全文搜索还经常涉及匹配相似但不相同的单词(例如拼写错误或单词的不同语法形式)和同义词。这些问题超出了本书的范围。
|
||||
|
||||
然而,在其核心,你可以将全文搜索视为另一种多维查询:在这种情况下,可能出现在文本中的每个单词(*词项*)是一个维度。包含词项 *x* 的文档在维度 *x* 中的值为 1,不包含 *x* 的文档的值为 0。搜索提到"红苹果"的文档意味着查询在 *红* 维度中查找 1,同时在 *苹果* 维度中查找 1。因此,维度数可能非常大。
|
||||
然而,在其核心,你可以将全文搜索视为另一种多维查询:在这种情况下,可能出现在文本中的每个单词(*词项*)是一个维度。包含词项 *x* 的文档在维度 *x* 中的值为 1,不包含 *x* 的文档的值为 0。搜索提到"红苹果"的文档意味着查询在 *红* 维度中查找 1,同时在 *苹果* 维度中查找 1。维度数量可能因此非常大。
|
||||
|
||||
许多搜索引擎用来回答此类查询的数据结构称为 *倒排索引*。这是一个键值结构,其中键是词项,值是包含该词项的所有文档的 ID 列表(*发布列表*)。如果文档 ID 是连续数字,发布列表也可以表示为稀疏位图,如 [图 4-8](/ch4#fig_bitmap_index) 所示:词项 *x* 的位图中的第 *n* 位是 1,如果 ID 为 *n* 的文档包含词项 *x* [^89]。
|
||||
许多搜索引擎用来回答此类查询的数据结构称为 *倒排索引*。这是一个键值结构,其中键是词项,值是包含该词项的所有文档的 ID 列表(*倒排列表*)。如果文档 ID 是顺序数字,倒排列表也可以表示为稀疏位图,如 [图 4-8](#fig_bitmap_index):词项 *x* 的位图中的第 *n* 位是 1,如果 ID 为 *n* 的文档包含词项 *x* [^89]。
|
||||
|
||||
现在,查找包含词项 *x* 和 *y* 的所有文档类似于搜索匹配两个条件的行的向量化数据仓库查询([图 4-9](/ch4#fig_bitmap_and)):加载词项 *x* 和 *y* 的两个位图并计算它们的按位 AND。即使位图是游程编码的,这也可以非常高效地完成。
|
||||
查找包含词项 *x* 和 *y* 的所有文档现在类似于搜索匹配两个条件的行的向量化数据仓库查询([图 4-9](#fig_bitmap_and)):加载词项 *x* 和 *y* 的两个位图并计算它们的按位 AND。即使位图是游程编码的,这也可以非常高效地完成。
|
||||
|
||||
例如,Elasticsearch 和 Solr 使用的全文索引引擎 Lucene 就是这样工作的 [^90]。它将词项到发布列表的映射存储在类似 SSTable 的排序文件中,这些文件使用我们在本章前面看到的相同日志结构方法在后台合并 [^91]。PostgreSQL 的 GIN 索引类型也使用发布列表来支持全文搜索和 JSON 文档内的索引 [^92] [^93]。
|
||||
例如,Elasticsearch 和 Solr 使用的全文索引引擎 Lucene 就是这样工作的 [^90]。它将词项到倒排列表的映射存储在类似 SSTable 的排序文件中,这些文件使用我们在本章前面看到的相同日志结构方法在后台合并 [^91]。PostgreSQL 的 GIN 索引类型也使用倒排列表来支持全文搜索和 JSON 文档内的索引 [^92] [^93]。
|
||||
|
||||
不是将文本分解为单词,另一种方法是查找长度为 *n* 的所有子字符串,称为 *n*-gram。例如,字符串 `"hello"` 的三元组(*n* = 3)是 `"hel"`、`"ell"` 和 `"llo"`。如果我们为所有三元组构建倒排索引,我们可以搜索至少三个字符长的任意子字符串的文档。三元组索引甚至允许在搜索查询中使用正则表达式;缺点是它们相当大 [^94]。
|
||||
除了将文本分解为单词,另一种选择是查找长度为 *n* 的所有子字符串,称为 *n* 元语法。例如,字符串 `"hello"` 的三元语法(*n* = 3)是 `"hel"`、`"ell"` 和 `"llo"`。如果我们为所有三元语法构建倒排索引,我们可以搜索至少三个字符长的任意子字符串的文档。三元语法索引甚至允许在搜索查询中使用正则表达式;缺点是它们相当大 [^94]。
|
||||
|
||||
为了处理文档或查询中的拼写错误,Lucene 能够在一定编辑距离内搜索单词的文本(编辑距离为 1 意味着已添加、删除或替换了一个字母)[^95]。它通过将词项集存储为键上字符的有限状态自动机(类似于 *trie* [^96])来实现这一点,并将其转换为 *Levenshtein 自动机*,该自动机支持在给定编辑距离内高效搜索单词 [^97]。
|
||||
为了处理文档或查询中的拼写错误,Lucene 能够在一定编辑距离内搜索文本中的单词(编辑距离为 1 意味着已添加、删除或替换了一个字母)[^95]。它通过将词项集存储为字符上的有限状态自动机(类似于 *字典树* [^96])并将其转换为 *莱文斯坦自动机* 来实现,该自动机支持在给定编辑距离内高效搜索单词 [^97]。
|
||||
|
||||
|
||||
### 向量嵌入 {#id92}
|
||||
|
||||
语义搜索超越了同义词和拼写错误,试图理解文档概念和用户意图。例如,如果你的帮助页面包含标题为"取消订阅"的页面,用户在搜索"如何关闭我的账户"或"终止合同"时仍应能够找到该页面,即使它们使用完全不同的词,但在含义上很接近。
|
||||
语义搜索超越了同义词和拼写错误,试图理解文档概念和用户意图。例如,如果你的帮助页面包含标题为"取消订阅"的页面,用户在搜索"如何关闭我的帐户"或"终止合同"时仍应能够找到该页面,即使它们使用完全不同的单词,但在含义上很接近。
|
||||
|
||||
为了理解文档的语义——它的含义——语义搜索索引使用嵌入模型将文档转换为浮点值的向量,称为 *向量嵌入*。向量表示多维空间中的一个点,每个浮点值表示文档沿一个维度轴的位置。当嵌入的输入文档在语义上相似时,嵌入模型生成的向量嵌入彼此接近(在这个多维空间中)。
|
||||
为了理解文档的语义 —— 它的含义 —— 语义搜索索引使用嵌入模型将文档转换为浮点值向量,称为 *向量嵌入*。向量表示多维空间中的一个点,每个浮点值表示文档沿着一个维度轴的位置。嵌入模型生成的向量嵌入在(这个多维空间中)彼此接近,当嵌入的输入文档在语义上相似时。
|
||||
|
||||
--------
|
||||
|
||||
> [!NOTE]
|
||||
> 我们在 ["查询执行:编译和向量化"](/ch4#sec_storage_vectorized) 中看到了术语 *向量化处理*。语义搜索中的向量具有不同的含义。在向量化处理中,向量指的是可以使用专门优化的代码处理的一批位。在嵌入模型中,向量是表示多维空间中位置的浮点数列表。
|
||||
> 我们在 ["查询执行:编译与向量化"](#sec_storage_vectorized) 中看到了术语 *向量化处理*。语义搜索中的向量有不同的含义。在向量化处理中,向量指的是可以用特别优化的代码处理的一批位。在嵌入模型中,向量是表示多维空间中位置的浮点数列表。
|
||||
|
||||
--------
|
||||
|
||||
例如,关于农业的维基百科页面的三维向量嵌入可能是 `[0.1, 0.22, 0.11]`。关于蔬菜的维基百科页面会非常接近,可能嵌入为 `[0.13, 0.19, 0.24]`。关于星型模式的页面可能有 `[0.82, 0.39, -0.74]` 的嵌入,相对较远。我们可以通过观察看出前两个向量比第三个更接近。
|
||||
|
||||
嵌入模型使用更大的向量(通常超过 1,000 个数字),但原理是相同的。我们不试图理解各个数字的含义;它们只是嵌入模型指向抽象多维空间中位置的方式。搜索引擎使用距离函数(如余弦相似度或欧几里得距离)来测量向量之间的距离。余弦相似度测量两个向量角度的余弦以确定它们有多接近,而欧几里得距离测量空间中两点之间的直线距离。
|
||||
嵌入模型使用更大的向量(通常超过 1,000 个数字),但原理是相同的。我们不试图理解各个数字的含义;它们只是嵌入模型指向抽象多维空间中位置的一种方式。搜索引擎使用距离函数(如余弦相似度或欧几里得距离)来测量向量之间的距离。余弦相似度测量两个向量角度的余弦以确定它们的接近程度,而欧几里得距离测量空间中两点之间的直线距离。
|
||||
|
||||
许多早期的嵌入模型,如 Word2Vec [^98]、BERT [^99] 和 GPT [^100],处理文本数据。此类模型通常实现为神经网络。研究人员继续为视频、音频和图像创建嵌入模型。最近,模型架构已变得 *多模态*:单个模型可以为多种模态(如文本和图像)生成向量嵌入。
|
||||
许多早期的嵌入模型,如 Word2Vec [^98]、BERT [^99] 和 GPT [^100] 都处理文本数据。这些模型通常实现为神经网络。研究人员继续为视频、音频和图像创建嵌入模型。最近,模型架构已经变成 *多模态* 的:单个模型可以为多种模态(如文本和图像)生成向量嵌入。
|
||||
|
||||
当用户输入查询时,语义搜索引擎使用嵌入模型生成向量嵌入。用户的查询和相关上下文(如用户的位置)被输入嵌入模型。在嵌入模型生成查询的向量嵌入后,搜索引擎必须使用向量索引找到具有相似向量嵌入的文档。
|
||||
语义搜索引擎在用户输入查询时使用嵌入模型生成向量嵌入。用户的查询和相关上下文(例如用户的位置)被输入到嵌入模型中。嵌入模型生成查询的向量嵌入后,搜索引擎必须使用向量索引找到具有相似向量嵌入的文档。
|
||||
|
||||
向量索引存储文档集合的向量嵌入。要查询索引,你传入查询的向量嵌入,索引返回其向量最接近查询向量的文档。由于我们之前看到的 R 树对于具有许多维度的向量效果不佳,因此使用专门的向量索引,例如:
|
||||
向量索引存储文档集合的向量嵌入。要查询索引,你传入查询的向量嵌入,索引返回其向量最接近查询向量的文档。由于我们之前看到的 R 树不适用于多维向量,因此使用专门的向量索引,例如:
|
||||
|
||||
平面索引
|
||||
: 向量按原样存储在索引中。查询必须读取每个向量并测量其与查询向量的距离。平面索引是准确的,但测量查询和每个向量之间的距离很慢。
|
||||
平面索引(Flat indexes)
|
||||
: 向量按原样存储在索引中。查询必须读取每个向量并测量其与查询向量的距离。平面索引是准确的,但测量查询与每个向量之间的距离很慢。
|
||||
|
||||
倒排文件(IVF)索引
|
||||
: 向量空间被聚类成向量的分区(称为 *质心*),以减少必须比较的向量数量。IVF 索引比平面索引更快,但只能给出近似结果:查询和文档可能落入不同的分区,即使它们彼此接近。IVF 索引上的查询首先定义 *探针*,这只是要检查的分区数。使用更多探针的查询将更准确,但会更慢,因为必须比较更多向量。
|
||||
: 向量空间被聚类为向量的分区(称为 *质心*),以减少必须比较的向量数量。IVF 索引比平面索引更快,但只能给出近似结果:即使查询和文档彼此接近,它们也可能落入不同的分区。对 IVF 索引的查询首先定义 *探针*,这只是要检查的分区数。使用更多探针的查询将更准确,但会更慢,因为必须比较更多向量。
|
||||
|
||||
分层可导航小世界(HNSW)
|
||||
: HNSW 索引维护向量空间的多个层,如 [图 4-11](/ch4#fig_vector_hnsw) 所示。每一层都表示为一个图,其中节点表示向量,边表示与附近向量的接近度。查询首先在最顶层定位最近的向量,该层具有少量节点。然后查询移动到下面层中的同一节点,并跟随该层中的边,该层连接更密集,寻找更接近查询向量的向量。该过程一直持续到到达最后一层。与 IVF 索引一样,HNSW 索引是近似的。
|
||||
层次可导航小世界(HNSW)
|
||||
: HNSW 索引维护向量空间的多个层,如 [图 4-11](#fig_vector_hnsw) 所示。每一层都表示为一个图,其中节点表示向量,边表示与附近向量的接近度。查询首先在最顶层定位最近的向量,该层具有少量节点。然后查询移动到下面一层的同一节点,并跟随该层中的边,该层连接更密集,寻找更接近查询向量的向量。该过程继续直到到达最后一层。与 IVF 索引一样,HNSW 索引是近似的。
|
||||
|
||||
{{< figure src="/fig/ddia_0411.png" id="fig_vector_hnsw" caption="图 4-11. 在 HNSW 索引中搜索最接近给定查询向量的数据库条目。" class="w-full my-4" >}}
|
||||
|
||||
|
||||
许多流行的向量数据库实现 IVF 和 HNSW 索引。Facebook 的 Faiss 库有每种的许多变体 [^101],PostgreSQL 的 pgvector 也支持两者 [^102]。IVF 和 HNSW 算法的完整细节超出了本书的范围,但它们的论文是很好的资源 [^103] [^104]。
|
||||
|
||||
许多流行的向量数据库实现了 IVF 和 HNSW 索引。Facebook 的 Faiss 库有每种的许多变体 [^101],PostgreSQL 的 pgvector 也支持两者 [^102]。IVF 和 HNSW 算法的完整细节超出了本书的范围,但它们的论文是极好的资源 [^103] [^104]。
|
||||
|
||||
## 总结 {#summary}
|
||||
|
||||
在本章中,我们试图深入了解数据库如何执行存储和检索。当你将数据存储在数据库中时会发生什么,以及当你稍后再次查询数据时数据库会做什么?
|
||||
在本章中,我们试图深入了解数据库如何执行存储和检索。当你在数据库中存储数据时会发生什么,当你稍后再次查询数据时数据库会做什么?
|
||||
|
||||
["分析与操作系统"](/ch1#sec_introduction_analytics) 介绍了事务处理(OLTP)和分析(OLAP)之间的区别。在本章中,我们看到为 OLTP 优化的存储引擎与为分析优化的存储引擎看起来非常不同:
|
||||
["分析型与事务型系统"](/ch1#sec_introduction_analytics) 介绍了事务处理(OLTP)和分析(OLAP)之间的区别。在本章中,我们看到为 OLTP 优化的存储引擎与为分析优化的存储引擎看起来非常不同:
|
||||
|
||||
* OLTP 系统针对大量请求进行了优化,每个请求读取和写入少量记录,并且需要快速响应。记录通常通过主键或二级索引访问,这些索引通常是从键到记录的有序映射,也支持范围查询。
|
||||
* 数据仓库和类似的分析系统针对扫描大量记录的复杂读取查询进行了优化。它们通常使用列式存储布局和压缩,以最大限度地减少此类查询需要从磁盘读取的数据量,并使用查询的即时编译或向量化来最大限度地减少处理数据所花费的 CPU 时间。
|
||||
* 数据仓库和类似的分析系统针对扫描大量记录的复杂读取查询进行了优化。它们通常使用带有压缩的列式存储布局,以最小化此类查询需要从磁盘读取的数据量,并使用查询的即时编译或向量化来最小化处理数据所花费的 CPU 时间。
|
||||
|
||||
在 OLTP 方面,我们看到了两种主要思想流派的存储引擎:
|
||||
在 OLTP 方面,我们看到了两个主要思想流派的存储引擎:
|
||||
|
||||
* 日志结构方法,只允许追加到文件和删除过时的文件,但从不更新已写入的文件。SSTable、LSM 树、RocksDB、Cassandra、HBase、Scylla、Lucene 等属于这一组。一般来说,日志结构存储引擎往往提供高写入吞吐量。
|
||||
* 就地更新方法,将磁盘视为一组可以覆盖的固定大小页面。B 树是这种理念的最大例子,用于所有主要的关系 OLTP 数据库以及许多非关系数据库。作为经验法则,B 树往往更适合读取,提供比日志结构存储更高的读取吞吐量和更低的响应时间。
|
||||
* 日志结构方法,只允许追加到文件和删除过时文件,但从不更新已写入的文件。SSTable、LSM 树、RocksDB、Cassandra、HBase、Scylla、Lucene 等属于这一组。一般来说,日志结构存储引擎往往提供高写入吞吐量。
|
||||
* 就地更新方法,将磁盘视为一组可以覆盖的固定大小页。B 树是这种理念的最大例子,用于所有主要的关系型 OLTP 数据库以及许多非关系型数据库。作为经验法则,B 树往往更适合读取,提供比日志结构存储更高的读取吞吐量和更低的响应时间。
|
||||
|
||||
然后,我们研究了可以同时搜索多个条件的索引:多维索引(如 R 树)可以同时按纬度和经度搜索地图上的点,全文搜索索引可以搜索同一文本中出现的多个关键字。最后,向量数据库用于文本文档和其他媒体的语义搜索;它们使用具有更多维度的向量,并通过比较向量相似性来查找相似文档。
|
||||
然后我们查看了可以同时搜索多个条件的索引:多维索引(如 R 树)可以同时按纬度和经度搜索地图上的点,全文搜索索引可以搜索出现在同一文本中的多个关键字。最后,向量数据库用于文本文档和其他媒体的语义搜索;它们使用具有大量维度的向量,并通过比较向量相似性来查找相似文档。
|
||||
|
||||
作为应用程序开发人员,如果你掌握了有关存储引擎内部的这些知识,你就能更好地了解哪种工具最适合你的特定应用程序。如果你需要调整数据库的调优参数,这种理解使你能够想象更高或更低的值可能产生什么效果。
|
||||
作为应用程序开发人员,如果你掌握了有关存储引擎内部的这些知识,你就能更好地知道哪种工具最适合你的特定应用程序。如果你需要调整数据库的调优参数,这种理解使你能够想象更高或更低的值可能产生什么影响。
|
||||
|
||||
尽管本章不能让你成为调整任何特定存储引擎的专家,但希望它为你提供了足够的词汇和想法,让你能够理解所选数据库的文档。
|
||||
尽管本章不能让你成为调优任何特定存储引擎的专家,但它希望为你提供了足够的词汇和想法,使你能够理解你选择的数据库的文档。
|
||||
|
||||
|
||||
|
||||
|
@ -34,7 +34,7 @@ breadcrumbs: false
|
||||
|
||||
向前兼容性的另一个挑战如 [图 5-1](/ch5#fig_encoding_preserve_field) 所示。假设你向记录模式添加了一个字段,新代码创建了包含该新字段的记录并将其存储在数据库中。随后,旧版本的代码(尚不知道新字段)读取记录,更新它,然后写回。在这种情况下,理想的行为通常是旧代码保持新字段不变,即使它无法解释。但是,如果记录被解码为不显式保留未知字段的模型对象,数据可能会丢失,如 [图 5-1](/ch5#fig_encoding_preserve_field) 所示。
|
||||
|
||||
{{< figure src="/fig/ddia_0501.png" id="fig_encoding_preserve_field" caption="当旧版本的应用程序更新之前由新版本应用程序写入的数据时,如果不小心,数据可能会丢失。" class="w-full my-4" >}}
|
||||
{{< figure src="/fig/ddia_0501.png" id="fig_encoding_preserve_field" caption="图 5-1. 当旧版本的应用程序更新之前由新版本应用程序写入的数据时,如果不小心,数据可能会丢失。" class="w-full my-4" >}}
|
||||
|
||||
在本章中,我们将研究几种编码数据的格式,包括 JSON、XML、Protocol Buffers 和 Avro。特别是,我们将研究它们如何处理模式变化,以及它们如何支持新旧数据和代码需要共存的系统。然后我们将讨论这些格式如何用于数据存储和通信:在数据库、Web 服务、REST API、远程过程调用(RPC)、工作流引擎以及事件驱动系统(如 actor 和消息队列)中。
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user