fix broken format

This commit is contained in:
Feng Ruohang 2025-08-11 09:36:05 +08:00
parent 59cab3178f
commit 18fcb5c205
14 changed files with 221 additions and 581 deletions

View File

@ -60,7 +60,7 @@ breadcrumbs: false
除了管理后端服务的团队外,通常还有两类人需要访问组织的数据:**业务分析师**,他们生成关于组织活动的报告,以帮助管理层做出更好的决策(**商业智能** 或 **BI**);以及 **数据科学家**,他们在数据中寻找新的见解,或创建由数据分析和机器学习/AI 支持的面向用户的产品功能(例如,电子商务网站上的"购买了 X 的人也购买了 Y"推荐、风险评分或垃圾邮件过滤等预测分析,以及搜索结果排名)。 除了管理后端服务的团队外,通常还有两类人需要访问组织的数据:**业务分析师**,他们生成关于组织活动的报告,以帮助管理层做出更好的决策(**商业智能** 或 **BI**);以及 **数据科学家**,他们在数据中寻找新的见解,或创建由数据分析和机器学习/AI 支持的面向用户的产品功能(例如,电子商务网站上的"购买了 X 的人也购买了 Y"推荐、风险评分或垃圾邮件过滤等预测分析,以及搜索结果排名)。
尽管业务分析师和数据科学家倾向于使用不同的工具并以不同的方式操作,但他们有一些共同点:两者都执行 **分析**,这意味着他们查看用户和后端服务生成的数据,但他们通常不修改这些数据(除了可能修复错误)。他们可能创建生数据集,其中原始数据已经以某种方式处理过。这导致了两种类型系统之间的分离——我们将在本书中使用这种区别: 尽管业务分析师和数据科学家倾向于使用不同的工具并以不同的方式操作,但他们有一些共同点:两者都执行 **分析**,这意味着他们查看用户和后端服务生成的数据,但他们通常不修改这些数据(除了可能修复错误)。他们可能创建生数据集,其中原始数据已经以某种方式处理过。这导致了两种类型系统之间的分离——我们将在本书中使用这种区别:
* **事务型系统** 由后端服务和数据基础设施组成,在这里创建数据,例如通过服务外部用户。在这里,应用程序代码基于用户执行的操作读取和修改其数据库中的数据。 * **事务型系统** 由后端服务和数据基础设施组成,在这里创建数据,例如通过服务外部用户。在这里,应用程序代码基于用户执行的操作读取和修改其数据库中的数据。
* **分析型系统** 服务于业务分析师和数据科学家的需求。它们包含来自事务型系统的只读数据副本,并针对分析所需的数据处理类型进行了优化。 * **分析型系统** 服务于业务分析师和数据科学家的需求。它们包含来自事务型系统的只读数据副本,并针对分析所需的数据处理类型进行了优化。
@ -145,7 +145,7 @@ breadcrumbs: false
因此,组织面临着以适合数据科学家使用的形式提供数据的需求。答案是 **数据湖**:一个集中的数据存储库,保存任何可能对分析有用的数据副本,通过 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]。
@ -153,7 +153,7 @@ Apache Hive、Spark SQL、Presto 和 Trino 是这种方法的例子。
#### 超越数据湖 {#beyond-the-data-lake} #### 超越数据湖 {#beyond-the-data-lake}
随着分析实践的成熟,组织越来越关注分析系统和数据管道的管理和运,例如 DataOps 宣言所捕获的 [^18]。其中一部分是治理、隐私和遵守 GDPR 和 CCPA 等法规的问题,我们将在["数据系统、法律与社会"](/ch1#sec_introduction_compliance)和[待补充链接]中讨论。 随着分析实践的成熟,组织越来越关注分析系统和数据管道的管理和运,例如 DataOps 宣言所捕获的 [^18]。其中一部分是治理、隐私和遵守 GDPR 和 CCPA 等法规的问题,我们将在["数据系统、法律与社会"](/ch1#sec_introduction_compliance)和[待补充链接]中讨论。
此外,分析数据越来越多地不仅作为文件和关系表提供,还作为事件流(参见[待补充链接])。使用基于文件的数据分析,你可以定期(例如,每天)重新运行分析以响应数据的变化,但流处理允许分析系统以秒级的速度响应事件。根据应用程序及其时间敏感性,流处理方法可能很有价值,例如识别和阻止潜在的欺诈或滥用活动。 此外,分析数据越来越多地不仅作为文件和关系表提供,还作为事件流(参见[待补充链接])。使用基于文件的数据分析,你可以定期(例如,每天)重新运行分析以响应数据的变化,但流处理允许分析系统以秒级的速度响应事件。根据应用程序及其时间敏感性,流处理方法可能很有价值,例如识别和阻止潜在的欺诈或滥用活动。
@ -161,21 +161,21 @@ Apache Hive、Spark SQL、Presto 和 Trino 是这种方法的例子。
### 权威数据源与派生数据 {#sec_introduction_derived} ### 权威数据源与派生数据 {#sec_introduction_derived}
与事务型系统和分析型系统之间的区别相关,本书还区分了 **权威记录系统****生数据系统**。这些术语很有用,因为它们可以帮助你澄清数据在系统中的流动: 与事务型系统和分析型系统之间的区别相关,本书还区分了 **权威记录系统****生数据系统**。这些术语很有用,因为它们可以帮助你澄清数据在系统中的流动:
权威记录系统 权威记录系统
: 权威记录系统,也称为 **权威数据源**,保存某些数据的权威或 **规范** 版本。当新数据进入时,例如作为用户输入,它首先写入这里。每个事实只表示一次(表示通常是 **规范化** 的;参见["规范化、反规范化和连接"](/ch3#sec_datamodels_normalization))。如果另一个系统与权威记录系统之间存在任何差异,那么权威记录系统中的值(根据定义)是正确的。 : 权威记录系统,也称为 **权威数据源**,保存某些数据的权威或 **规范** 版本。当新数据进入时,例如作为用户输入,它首先写入这里。每个事实只表示一次(表示通常是 **规范化** 的;参见["规范化、反规范化和连接"](/ch3#sec_datamodels_normalization))。如果另一个系统与权威记录系统之间存在任何差异,那么权威记录系统中的值(根据定义)是正确的。
生数据系统 生数据系统
: 生系统中的数据是从另一个系统获取一些现有数据并以某种方式转换或处理它的结果。如果你丢失了生数据,你可以从原始源重新创建它。一个经典的例子是缓存:如果存在,可以从缓存提供数据,但如果缓存不包含你需要的内容,你可以回退到底层数据库。反规范化值、索引、物化视图、转换的数据表示和在数据集上训练的模型也属于这一类别。 : 生系统中的数据是从另一个系统获取一些现有数据并以某种方式转换或处理它的结果。如果你丢失了生数据,你可以从原始源重新创建它。一个经典的例子是缓存:如果存在,可以从缓存提供数据,但如果缓存不包含你需要的内容,你可以回退到底层数据库。反规范化值、索引、物化视图、转换的数据表示和在数据集上训练的模型也属于这一类别。
从技术上讲,生数据是 **冗余** 的,因为它复制了现有信息。然而,它通常对于在读取查询上获得良好性能至关重要。你可以从单个源派生几个不同的数据集,使你能够从不同的"视角"查看数据。 从技术上讲,生数据是 **冗余** 的,因为它复制了现有信息。然而,它通常对于在读取查询上获得良好性能至关重要。你可以从单个源派生几个不同的数据集,使你能够从不同的"视角"查看数据。
分析系统通常是生数据系统,因为它们是在其他地方创建的数据的消费者。事务型服务可能包含权威记录系统和衍生数据系统的混合。权威记录系统是首先写入数据的主数据库,而衍生数据系统是加速常见读取操作的索引和缓存,特别是对于权威记录系统无法有效回答的查询。 分析系统通常是生数据系统,因为它们是在其他地方创建的数据的消费者。事务型服务可能包含权威记录系统和派生数据系统的混合。权威记录系统是首先写入数据的主数据库,而派生数据系统是加速常见读取操作的索引和缓存,特别是对于权威记录系统无法有效回答的查询。
大多数数据库、存储引擎和查询语言本质上不是权威记录系统或衍生系统。数据库只是一个工具:如何使用它取决于你。权威记录系统和衍生数据系统之间的区别不取决于工具,而取决于你如何在应用程序中使用它。通过明确哪些数据是从哪些其他数据派生的,你可以为原本混乱的系统架构带来清晰度。 大多数数据库、存储引擎和查询语言本质上不是权威记录系统或派生系统。数据库只是一个工具:如何使用它取决于你。权威记录系统和派生数据系统之间的区别不取决于工具,而取决于你如何在应用程序中使用它。通过明确哪些数据是从哪些其他数据派生的,你可以为原本混乱的系统架构带来清晰度。
当一个系统中的数据源自另一个系统中的数据时,你需要一个过程来在权威记录系统中的原始数据发生变化时更新生数据。不幸的是,许多数据库的设计基于这样的假设:你的应用程序只需要使用那一个数据库,它们不易于集成多个系统以传播此类更新。在[待补充链接]中,我们将讨论 **数据集成** 的方法,这允许我们组合多个数据系统来实现单个系统无法做到的事情。 当一个系统中的数据源自另一个系统中的数据时,你需要一个过程来在权威记录系统中的原始数据发生变化时更新生数据。不幸的是,许多数据库的设计基于这样的假设:你的应用程序只需要使用那一个数据库,它们不易于集成多个系统以传播此类更新。在[待补充链接]中,我们将讨论 **数据集成** 的方法,这允许我们组合多个数据系统来实现单个系统无法做到的事情。
这就结束了我们对分析和事务处理的比较。在下一节中,我们将研究另一个你可能已经看到多次争论的权衡。 这就结束了我们对分析和事务处理的比较。在下一节中,我们将研究另一个你可能已经看到多次争论的权衡。
@ -186,45 +186,66 @@ 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} ### 云服务的利弊 {#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} ### 云原生系统架构 {#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" >}}
@ -235,9 +256,11 @@ Apache Hive、Spark SQL、Presto 和 Trino 是这种方法的例子。
#### 云服务的分层 {#layering-of-cloud-services} #### 云服务的分层 {#layering-of-cloud-services}
许多自托管数据系统的系统要求非常简单:它们在传统操作系统(如 Linux 或 Windows上运行将数据存储为文件系统上的文件并通过 TCP/IP 等标准网络协议进行通信。少数系统依赖于特殊硬件,如 GPU用于机器学习或 RDMA 网络接口但总的来说自托管软件倾向于使用非常通用的计算资源CPU、RAM、文件系统和 IP 网络。 许多自托管数据系统的系统要求非常简单:它们在传统操作系统(如 Linux 或 Windows上运行将数据存储为文件系统上的文件并通过 TCP/IP 等标准网络协议进行通信。
少数系统依赖于特殊硬件,如 GPU用于机器学习或 RDMA 网络接口但总的来说自托管软件倾向于使用非常通用的计算资源CPU、RAM、文件系统和 IP 网络。
在云中,这种类型的软件可以在基础设施即服务环境中运行,使用一个或多个虚拟机(或 **实例**),分配一定的 CPU、内存、磁盘和网络带宽。与物理机器相比云实例可以更快地配置并且有更多种类的大小但除此之外它们与传统计算机类似你可以在上面运行任何你喜欢的软件但你负责自己管理它。 在云中,这种类型的软件可以在基础设施即服务环境中运行,使用一个或多个虚拟机(或 **实例**),分配一定的 CPU、内存、磁盘和网络带宽。
与物理机器相比,云实例可以更快地配置,并且有更多种类的大小,但除此之外,它们与传统计算机类似:你可以在上面运行任何你喜欢的软件,但你负责自己管理它。
相比之下,云原生服务的关键思想是不仅使用由操作系统管理的计算资源,还基于较低级别的云服务构建更高级别的服务。例如: 相比之下,云原生服务的关键思想是不仅使用由操作系统管理的计算资源,还基于较低级别的云服务构建更高级别的服务。例如:
@ -330,9 +353,7 @@ ETL参见["数据仓库"](/ch1#sec_introduction_dwh))只是故事的一部
分布式系统也有缺点。通过网络进行的每个请求和 API 调用都需要处理失败的可能性:网络可能中断,或者服务可能过载或崩溃,因此任何请求都可能超时而没有收到响应。在这种情况下,我们不知道服务是否收到了请求,简单地重试它可能不安全。我们将在[第 9 章](/ch9#ch_distributed)中详细讨论这些问题。 分布式系统也有缺点。通过网络进行的每个请求和 API 调用都需要处理失败的可能性:网络可能中断,或者服务可能过载或崩溃,因此任何请求都可能超时而没有收到响应。在这种情况下,我们不知道服务是否收到了请求,简单地重试它可能不安全。我们将在[第 9 章](/ch9#ch_distributed)中详细讨论这些问题。
尽管数据中心网络很快,但调用另一个服务仍然比在同一进程中调用函数慢得多 [^44]。 尽管数据中心网络很快,但调用另一个服务仍然比在同一进程中调用函数慢得多 [^44]。
在处理大量数据时,与其将数据从存储传输到处理它的单独机器,不如将计算带到已经拥有数据的机器上可能更快 [^45]。 在处理大量数据时,与其将数据从存储传输到处理它的单独机器,不如将计算带到已经拥有数据的机器上可能更快 [^45]。
更多的节点并不总是更快:在某些情况下,单台计算机上的简单单线程程序可以比具有 100 多个 CPU 核心的集群表现得更好 [^46]。 更多的节点并不总是更快:在某些情况下,单台计算机上的简单单线程程序可以比具有 100 多个 CPU 核心的集群表现得更好 [^46]。
故障排除分布式系统通常很困难:如果系统响应缓慢,你如何找出问题所在?在 **可观测性** [^47] [^48] 的标题下开发了诊断分布式系统问题的技术,这涉及收集有关系统执行的数据,并允许以允许分析高级指标和单个事件的方式进行查询。**追踪** 工具(如 OpenTelemetry、Zipkin 和 Jaeger允许你跟踪哪个客户端为哪个操作调用了哪个服务器以及每次调用花费了多长时间 [^49]。 故障排除分布式系统通常很困难:如果系统响应缓慢,你如何找出问题所在?在 **可观测性** [^47] [^48] 的标题下开发了诊断分布式系统问题的技术,这涉及收集有关系统执行的数据,并允许以允许分析高级指标和单个事件的方式进行查询。**追踪** 工具(如 OpenTelemetry、Zipkin 和 Jaeger允许你跟踪哪个客户端为哪个操作调用了哪个服务器以及每次调用花费了多长时间 [^49]。
@ -343,7 +364,7 @@ ETL参见["数据仓库"](/ch1#sec_introduction_dwh))只是故事的一部
### 微服务与 Serverless {#sec_introduction_microservices} ### 微服务与 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每个服务有一个负责其维护的团队。因此复杂的应用程序可以分解为多个交互服务每个服务由单独的团队管理。
@ -381,7 +402,7 @@ ETL参见["数据仓库"](/ch1#sec_introduction_dwh))只是故事的一部
每个从事此类系统工作的人都有责任考虑道德影响并确保他们遵守相关法律。没有必要让每个人都成为法律和道德专家,但对法律和道德原则的基本认识与分布式系统中的一些基础知识同样重要。 每个从事此类系统工作的人都有责任考虑道德影响并确保他们遵守相关法律。没有必要让每个人都成为法律和道德专家,但对法律和道德原则的基本认识与分布式系统中的一些基础知识同样重要。
法律考虑正在影响数据系统设计的基础 [^61]。例如GDPR 授予个人在请求时删除其数据的权利(有时称为 **被遗忘权**)。然而,正如我们将在本书中看到的,许多数据系统依赖不可变构造(如仅追加日志)作为其设计的一部分;我们如何确保删除应该不可变的文件中间的某些数据?我们如何处理已被纳入生数据集(参见["权威数据源与派生数据"](/ch1#sec_introduction_derived))的数据删除,例如机器学习模型的训练数据?回答这些问题会带来新的工程挑战。 法律考虑正在影响数据系统设计的基础 [^61]。例如GDPR 授予个人在请求时删除其数据的权利(有时称为 **被遗忘权**)。然而,正如我们将在本书中看到的,许多数据系统依赖不可变构造(如仅追加日志)作为其设计的一部分;我们如何确保删除应该不可变的文件中间的某些数据?我们如何处理已被纳入生数据集(参见["权威数据源与派生数据"](/ch1#sec_introduction_derived))的数据删除,例如机器学习模型的训练数据?回答这些问题会带来新的工程挑战。
目前,我们对于哪些特定技术或系统架构应被视为"符合 GDPR"没有明确的指导方针。法规故意不强制要求特定技术,因为随着技术的进步,这些技术可能会迅速变化。相反,法律文本规定了需要解释的高级原则。这意味着如何遵守隐私法规的问题没有简单的答案,但我们将通过这个视角来看待本书中的一些技术。 目前,我们对于哪些特定技术或系统架构应被视为"符合 GDPR"没有明确的指导方针。法规故意不强制要求特定技术,因为随着技术的进步,这些技术可能会迅速变化。相反,法律文本规定了需要解释的高级原则。这意味着如何遵守隐私法规的问题没有简单的答案,但我们将通过这个视角来看待本书中的一些技术。

View File

@ -415,9 +415,9 @@ Reduce 侧方法的优点是不需要对输入数据做任何假设:无论其
Google 最初使用 MapReduce 是为其搜索引擎建立索引,其实现为由 5 到 10 个 MapReduce 作业组成的工作流【1】。虽然 Google 后来也不仅仅是为这个目的而使用 MapReduce 【43】但如果从构建搜索索引的角度来看更能帮助理解 MapReduce。直至今日Hadoop MapReduce 仍然是为 Lucene/Solr 构建索引的好方法【44】 Google 最初使用 MapReduce 是为其搜索引擎建立索引,其实现为由 5 到 10 个 MapReduce 作业组成的工作流【1】。虽然 Google 后来也不仅仅是为这个目的而使用 MapReduce 【43】但如果从构建搜索索引的角度来看更能帮助理解 MapReduce。直至今日Hadoop MapReduce 仍然是为 Lucene/Solr 构建索引的好方法【44】
我们在 “[全文搜索和模糊索引](/ch3#全文搜索和模糊索引)” 中简要地了解了 Lucene 这样的全文搜索索引是如何工作的:它是一个文件(关键词字典),你可以在其中高效地查找特定关键字,并找到包含该关键字的所有文档 ID 列表(文章列表)。这是一种非常简化的看法 —— 实际上,搜索索引需要各种额外数据,以便根据相关性对搜索结果进行排名、纠正拼写错误、解析同义词等等 —— 但这个原则是成立的。 我们在 “[全文检索与模糊索引](/ch3#全文检索与模糊索引)” 中简要地了解了 Lucene 这样的全文检索索引是如何工作的:它是一个文件(关键词字典),你可以在其中高效地查找特定关键字,并找到包含该关键字的所有文档 ID 列表(文章列表)。这是一种非常简化的看法 —— 实际上,搜索索引需要各种额外数据,以便根据相关性对搜索结果进行排名、纠正拼写错误、解析同义词等等 —— 但这个原则是成立的。
如果需要对一组固定文档执行全文则批处理是一种构建索引的高效方法Mapper 根据需要对文档集合进行分区,每个 Reducer 构建该分区的索引,并将索引文件写入分布式文件系统。构建这样的文档分区索引(请参阅 “[分区与次级索引](/ch6#分区与次级索引)”)并行处理效果拔群。 如果需要对一组固定文档执行全文则批处理是一种构建索引的高效方法Mapper 根据需要对文档集合进行分区,每个 Reducer 构建该分区的索引,并将索引文件写入分布式文件系统。构建这样的文档分区索引(请参阅 “[分区与次级索引](/ch6#分区与次级索引)”)并行处理效果拔群。
由于按关键字查询搜索索引是只读操作,因而这些索引文件一旦创建就是不可变的。 由于按关键字查询搜索索引是只读操作,因而这些索引文件一旦创建就是不可变的。
@ -485,7 +485,7 @@ MapReduce 作业的输出处理遵循同样的原理。通过将输入视为不
MPP 数据库是单体的紧密集成的软件负责磁盘上的存储布局查询计划调度和执行。由于这些组件都可以针对数据库的特定需求进行调整和优化因此整个系统可以在其设计针对的查询类型上取得非常好的性能。而且SQL 查询语言允许以优雅的语法表达查询,而无需编写代码,可以在业务分析师使用的可视化工具(例如 Tableau中访问到。 MPP 数据库是单体的紧密集成的软件负责磁盘上的存储布局查询计划调度和执行。由于这些组件都可以针对数据库的特定需求进行调整和优化因此整个系统可以在其设计针对的查询类型上取得非常好的性能。而且SQL 查询语言允许以优雅的语法表达查询,而无需编写代码,可以在业务分析师使用的可视化工具(例如 Tableau中访问到。
另一方面,并非所有类型的处理都可以合理地表达为 SQL 查询。例如,如果要构建机器学习和推荐系统,或者使用相关性排名模型的全文索索引,或者执行图像分析,则很可能需要更一般的数据处理模型。这些类型的处理通常是特别针对特定应用的(例如机器学习的特征工程,机器翻译的自然语言模型,欺诈预测的风险评估函数),因此它们不可避免地需要编写代码,而不仅仅是查询。 另一方面,并非所有类型的处理都可以合理地表达为 SQL 查询。例如,如果要构建机器学习和推荐系统,或者使用相关性排名模型的全文索索引,或者执行图像分析,则很可能需要更一般的数据处理模型。这些类型的处理通常是特别针对特定应用的(例如机器学习的特征工程,机器翻译的自然语言模型,欺诈预测的风险评估函数),因此它们不可避免地需要编写代码,而不仅仅是查询。
MapReduce 使工程师能够轻松地在大型数据集上运行自己的代码。如果你有 HDFS 和 MapReduce那么你 **可以** 在它之上建立一个 SQL 查询执行引擎,事实上这正是 Hive 项目所做的【31】。但是你也可以编写许多其他形式的批处理这些批处理不必非要用 SQL 查询表示。 MapReduce 使工程师能够轻松地在大型数据集上运行自己的代码。如果你有 HDFS 和 MapReduce那么你 **可以** 在它之上建立一个 SQL 查询执行引擎,事实上这正是 Hive 项目所做的【31】。但是你也可以编写许多其他形式的批处理这些批处理不必非要用 SQL 查询表示。
@ -711,7 +711,7 @@ Spark、Flink 和 Tez 避免将中间状态写入 HDFS因此它们采取了
得益于这个框架,你在批处理作业中的代码无需操心实现容错机制:框架可以保证作业的最终输出与没有发生错误的情况相同,虽然实际上也许不得不重试各种任务。比起在线服务一边处理用户请求一边将写入数据库作为处理请求的副作用,批处理提供的这种可靠性语义要强得多。 得益于这个框架,你在批处理作业中的代码无需操心实现容错机制:框架可以保证作业的最终输出与没有发生错误的情况相同,虽然实际上也许不得不重试各种任务。比起在线服务一边处理用户请求一边将写入数据库作为处理请求的副作用,批处理提供的这种可靠性语义要强得多。
批处理作业的显著特点是,它读取一些输入数据并产生一些输出数据,但不修改输入 —— 换句话说,输出是从输入生出的。最关键的是,输入数据是 **有界的bounded**:它有一个已知的,固定的大小(例如,它包含一些时间点的日志文件或数据库内容的快照)。因为它是有界的,一个作业知道自己什么时候完成了整个输入的读取,所以一个工作在做完后,最终总是会完成的。 批处理作业的显著特点是,它读取一些输入数据并产生一些输出数据,但不修改输入 —— 换句话说,输出是从输入生出的。最关键的是,输入数据是 **有界的bounded**:它有一个已知的,固定的大小(例如,它包含一些时间点的日志文件或数据库内容的快照)。因为它是有界的,一个作业知道自己什么时候完成了整个输入的读取,所以一个工作在做完后,最终总是会完成的。
在下一章中,我们将转向流处理,其中的输入是 **无界的unbounded** —— 也就是说,你还有活儿要干,然而它的输入是永无止境的数据流。在这种情况下,作业永无完成之日。因为在任何时候都可能有更多的工作涌入。我们将看到,在某些方面上,流处理和批处理是相似的。但是关于无尽数据流的假设也对我们构建系统的方式产生了很多改变。 在下一章中,我们将转向流处理,其中的输入是 **无界的unbounded** —— 也就是说,你还有活儿要干,然而它的输入是永无止境的数据流。在这种情况下,作业永无完成之日。因为在任何时候都可能有更多的工作涌入。我们将看到,在某些方面上,流处理和批处理是相似的。但是关于无尽数据流的假设也对我们构建系统的方式产生了很多改变。

View File

@ -248,7 +248,7 @@ Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 Distri
最近,人们对 **变更数据捕获change data capture, CDC** 越来越感兴趣这是一种观察写入数据库的所有数据变更并将其提取并转换为可以复制到其他系统中的形式的过程。CDC 是非常有意思的,尤其是当变更能在被写入后立刻用于流时。 最近,人们对 **变更数据捕获change data capture, CDC** 越来越感兴趣这是一种观察写入数据库的所有数据变更并将其提取并转换为可以复制到其他系统中的形式的过程。CDC 是非常有意思的,尤其是当变更能在被写入后立刻用于流时。
例如,你可以捕获数据库中的变更,并不断将相同的变更应用至搜索索引。如果变更日志以相同的顺序应用,则可以预期搜索索引中的数据与数据库中的数据是匹配的。搜索索引和任何其他生数据系统只是变更流的消费者,如 [图 11-5](/v1/ddia_1105.png) 所示。 例如,你可以捕获数据库中的变更,并不断将相同的变更应用至搜索索引。如果变更日志以相同的顺序应用,则可以预期搜索索引中的数据与数据库中的数据是匹配的。搜索索引和任何其他生数据系统只是变更流的消费者,如 [图 11-5](/v1/ddia_1105.png) 所示。
![](/v1/ddia_1105.png) ![](/v1/ddia_1105.png)
@ -317,7 +317,7 @@ Kafka Connect【41】致力于将广泛的数据库系统的变更数据捕获
事件日志本身并不是很有用,因为用户通常期望看到的是系统的当前状态,而不是变更历史。例如,在购物网站上,用户期望能看到他们购物车里的当前内容,而不是他们购物车所有变更的一个仅追加列表。 事件日志本身并不是很有用,因为用户通常期望看到的是系统的当前状态,而不是变更历史。例如,在购物网站上,用户期望能看到他们购物车里的当前内容,而不是他们购物车所有变更的一个仅追加列表。
因此,使用事件溯源的应用需要拉取事件日志(表示 **写入** 系统的数据),并将其转换为适合向用户显示的应用状态(从系统 **读取** 数据的方式【47】。这种转换可以使用任意逻辑但它应当是确定性的以便能再次运行并从事件日志中生出相同的应用状态。 因此,使用事件溯源的应用需要拉取事件日志(表示 **写入** 系统的数据),并将其转换为适合向用户显示的应用状态(从系统 **读取** 数据的方式【47】。这种转换可以使用任意逻辑但它应当是确定性的以便能再次运行并从事件日志中生出相同的应用状态。
与变更数据捕获一样,重播事件日志允许让你重新构建系统的当前状态。不过,日志压缩需要采用不同的方式处理: 与变更数据捕获一样,重播事件日志允许让你重新构建系统的当前状态。不过,日志压缩需要采用不同的方式处理:
@ -358,7 +358,7 @@ $$
**图 11-6 应用当前状态与事件流之间的关系** **图 11-6 应用当前状态与事件流之间的关系**
如果你持久存储了变更日志,那么重现状态就非常简单。如果你认为事件日志是你的记录系统,而所有的生状态都从它派生而来那么系统中的数据流动就容易理解的多。正如帕特・赫兰Pat Helland所说的【52】 如果你持久存储了变更日志,那么重现状态就非常简单。如果你认为事件日志是你的记录系统,而所有的生状态都从它派生而来那么系统中的数据流动就容易理解的多。正如帕特・赫兰Pat Helland所说的【52】
> 事务日志记录了数据库的所有变更。高速追加是更改日志的唯一方法。从这个角度来看,数据库的内容其实是日志中记录最新值的缓存。日志才是真相,数据库是日志子集的缓存,这一缓存子集恰好来自日志中每条记录与索引值的最新值。 > 事务日志记录了数据库的所有变更。高速追加是更改日志的唯一方法。从这个角度来看,数据库的内容其实是日志中记录最新值的缓存。日志才是真相,数据库是日志子集的缓存,这一缓存子集恰好来自日志中每条记录与索引值的最新值。
@ -366,9 +366,9 @@ $$
#### 不可变事件的优点 #### 不可变事件的优点
数据库中的不变性是一个古老的概念。例如,会计在几个世纪以来一直在财务记账中应用不变性。一笔交易发生时,它被记录在一个仅追加写入的分类帐中,实质上是描述货币、商品或服务转手的事件日志。账目,比如利润、亏损、资产负债表,是从分类账中的交易求和生而来【53】。 数据库中的不变性是一个古老的概念。例如,会计在几个世纪以来一直在财务记账中应用不变性。一笔交易发生时,它被记录在一个仅追加写入的分类帐中,实质上是描述货币、商品或服务转手的事件日志。账目,比如利润、亏损、资产负债表,是从分类账中的交易求和生而来【53】。
如果发生错误,会计师不会删除或更改分类帐中的错误交易 —— 而是添加另一笔交易以补偿错误,例如退还一笔不正确的费用。不正确的交易将永远保留在分类帐中,对于审计而言可能非常重要。如果从不正确的分类账生出的错误数字已经公布那么下一个会计周期的数字就会包括一个更正。这个过程在会计事务中是很常见的【54】。 如果发生错误,会计师不会删除或更改分类帐中的错误交易 —— 而是添加另一笔交易以补偿错误,例如退还一笔不正确的费用。不正确的交易将永远保留在分类帐中,对于审计而言可能非常重要。如果从不正确的分类账生出的错误数字已经公布那么下一个会计周期的数字就会包括一个更正。这个过程在会计事务中是很常见的【54】。
尽管这种可审计性只在金融系统中尤其重要,但对于不受这种严格监管的许多其他系统,也是很有帮助的。如 “[批处理输出的哲学](/ch11#批处理输出的哲学)” 中所讨论的,如果你意外地部署了将错误数据写入数据库的错误代码,当代码会破坏性地覆写数据时,恢复要困难得多。使用不可变事件的仅追加日志,诊断问题与故障恢复就要容易的多。 尽管这种可审计性只在金融系统中尤其重要,但对于不受这种严格监管的许多其他系统,也是很有帮助的。如 “[批处理输出的哲学](/ch11#批处理输出的哲学)” 中所讨论的,如果你意外地部署了将错误数据写入数据库的错误代码,当代码会破坏性地覆写数据时,恢复要困难得多。使用不可变事件的仅追加日志,诊断问题与故障恢复就要容易的多。
@ -376,7 +376,7 @@ $$
#### 从同一事件日志中派生多个视图 #### 从同一事件日志中派生多个视图
此外,通过从不变的事件日志中分离出可变的状态,你可以针对不同的读取方式,从相同的事件日志中生出几种不同的表现形式。效果就像一个流的多个消费者一样([图 11-5](/v1/ddia_1105.png)):例如,分析型数据库 Druid 使用这种方式直接从 Kafka 摄取数据【55】Pistachio 是一个分布式的键值存储,使用 Kafka 作为提交日志【56】Kafka Connect 能将来自 Kafka 的数据导出到各种不同的数据库与索引【41】。这对于许多其他存储和索引系统如搜索服务器来说是很有意义的当系统要从分布式日志中获取输入时亦然请参阅 “[保持系统同步](#保持系统同步)”)。 此外,通过从不变的事件日志中分离出可变的状态,你可以针对不同的读取方式,从相同的事件日志中生出几种不同的表现形式。效果就像一个流的多个消费者一样([图 11-5](/v1/ddia_1105.png)):例如,分析型数据库 Druid 使用这种方式直接从 Kafka 摄取数据【55】Pistachio 是一个分布式的键值存储,使用 Kafka 作为提交日志【56】Kafka Connect 能将来自 Kafka 的数据导出到各种不同的数据库与索引【41】。这对于许多其他存储和索引系统如搜索服务器来说是很有意义的当系统要从分布式日志中获取输入时亦然请参阅 “[保持系统同步](#保持系统同步)”)。
添加从事件日志到数据库的显式转换能够使应用更容易地随时间演进如果你想要引入一个新功能以新的方式表示现有数据则可以使用事件日志来构建一个单独的、针对新功能的读取优化视图无需修改现有系统而与之共存。并行运行新旧系统通常比在现有系统中执行复杂的模式迁移更容易。一旦不再需要旧的系统你可以简单地关闭它并回收其资源【47,57】。 添加从事件日志到数据库的显式转换能够使应用更容易地随时间演进如果你想要引入一个新功能以新的方式表示现有数据则可以使用事件日志来构建一个单独的、针对新功能的读取优化视图无需修改现有系统而与之共存。并行运行新旧系统通常比在现有系统中执行复杂的模式迁移更容易。一旦不再需要旧的系统你可以简单地关闭它并回收其资源【47,57】。
@ -388,7 +388,7 @@ $$
#### 并发控制 #### 并发控制
事件溯源和变更数据捕获的最大缺点是,事件日志的消费者通常是异步的,所以可能会出现这样的情况:用户会写入日志,然后从日志生视图中读取,结果发现他的写入还没有反映在读取视图中。我们之前在 “[读己之写](/ch5#读己之写)” 中讨论了这个问题以及可能的解决方案。 事件溯源和变更数据捕获的最大缺点是,事件日志的消费者通常是异步的,所以可能会出现这样的情况:用户会写入日志,然后从日志生视图中读取,结果发现他的写入还没有反映在读取视图中。我们之前在 “[读己之写](/ch5#读己之写)” 中讨论了这个问题以及可能的解决方案。
一种解决方案是将事件追加到日志时同步执行读取视图的更新。而将这些写入操作合并为一个原子单元需要 **事务**,所以要么将事件日志和读取视图保存在同一个存储系统中,要么就需要跨不同系统进行分布式事务。或者,你也可以使用在 “[使用全序广播实现线性一致的存储](/ch9#使用全序广播实现线性一致的存储)” 中讨论的方法。 一种解决方案是将事件追加到日志时同步执行读取视图的更新。而将这些写入操作合并为一个原子单元需要 **事务**,所以要么将事件日志和读取视图保存在同一个存储系统中,要么就需要跨不同系统进行分布式事务。或者,你也可以使用在 “[使用全序广播实现线性一致的存储](/ch9#使用全序广播实现线性一致的存储)” 中讨论的方法。
@ -419,7 +419,7 @@ $$
2. 你能以某种方式将事件推送给用户,例如发送报警邮件或推送通知,或将事件流式传输到可实时显示的仪表板上。在这种情况下,人是流的最终消费者。 2. 你能以某种方式将事件推送给用户,例如发送报警邮件或推送通知,或将事件流式传输到可实时显示的仪表板上。在这种情况下,人是流的最终消费者。
3. 你可以处理一个或多个输入流,并产生一个或多个输出流。流可能会经过由几个这样的处理阶段组成的流水线,最后再输出(选项 1 或 2 3. 你可以处理一个或多个输入流,并产生一个或多个输出流。流可能会经过由几个这样的处理阶段组成的流水线,最后再输出(选项 1 或 2
在本章的剩余部分中,我们将讨论选项 3处理流以产生其他生流。处理这样的流的代码片段,被称为 **算子operator****作业job**。它与我们在 [第十一章](/ch11) 中讨论过的 Unix 进程和 MapReduce 作业密切相关,数据流的模式是相似的:一个流处理器以只读的方式使用输入流,并将其输出以仅追加的方式写入一个不同的位置。 在本章的剩余部分中,我们将讨论选项 3处理流以产生其他生流。处理这样的流的代码片段,被称为 **算子operator****作业job**。它与我们在 [第十一章](/ch11) 中讨论过的 Unix 进程和 MapReduce 作业密切相关,数据流的模式是相似的:一个流处理器以只读的方式使用输入流,并将其输出以仅追加的方式写入一个不同的位置。
流处理中的分区和并行化模式也非常类似于 [第十一章](/ch11) 中介绍的 MapReduce 和数据流引擎,因此我们不再重复这些主题。基本的 Map 操作(如转换和过滤记录)也是一样的。 流处理中的分区和并行化模式也非常类似于 [第十一章](/ch11) 中介绍的 MapReduce 和数据流引擎,因此我们不再重复这些主题。基本的 Map 操作(如转换和过滤记录)也是一样的。
@ -462,7 +462,7 @@ CEP 的实现包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
#### 维护物化视图 #### 维护物化视图
我们在 “[数据库与流](#数据库与流)” 中看到,数据库的变更流可以用于维护派生数据系统(如缓存、搜索索引和数据仓库),并使其与源数据库保持最新。我们可以将这些示例视作维护 **物化视图materialized view** 的一种具体场景(请参阅 “[聚合:数据立方体和物化视图](/ch3#聚合:数据立方体和物化视图)”):在某个数据集上生出一个替代视图以便高效查询并在底层数据变更时更新视图【50】。 我们在 “[数据库与流](#数据库与流)” 中看到,数据库的变更流可以用于维护派生数据系统(如缓存、搜索索引和数据仓库),并使其与源数据库保持最新。我们可以将这些示例视作维护 **物化视图materialized view** 的一种具体场景(请参阅 “[聚合:数据立方体和物化视图](/ch3#聚合:数据立方体和物化视图)”):在某个数据集上生出一个替代视图以便高效查询并在底层数据变更时更新视图【50】。
同样,在事件溯源中,应用程序的状态是通过应用事件日志来维护的;这里的应用程序状态也是一种物化视图。与流分析场景不同的是,仅考虑某个时间窗口内的事件通常是不够的:构建物化视图可能需要任意时间段内的 **所有** 事件,除了那些可能由日志压缩丢弃的过时事件(请参阅 “[日志压缩](#日志压缩)”)。实际上,你需要一个可以一直延伸到时间开端的窗口。 同样,在事件溯源中,应用程序的状态是通过应用事件日志来维护的;这里的应用程序状态也是一种物化视图。与流分析场景不同的是,仅考虑某个时间窗口内的事件通常是不够的:构建物化视图可能需要任意时间段内的 **所有** 事件,除了那些可能由日志压缩丢弃的过时事件(请参阅 “[日志压缩](#日志压缩)”)。实际上,你需要一个可以一直延伸到时间开端的窗口。
@ -470,7 +470,7 @@ CEP 的实现包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
#### 在流上搜索 #### 在流上搜索
除了允许搜索由多个事件构成模式的 CEP 外,有时也存在基于复杂标准(例如全文索查询)来搜索单个事件的需求。 除了允许搜索由多个事件构成模式的 CEP 外,有时也存在基于复杂标准(例如全文索查询)来搜索单个事件的需求。
例如,媒体监测服务可以订阅新闻文章 Feed 与来自媒体的播客搜索任何关于公司、产品或感兴趣的话题的新闻。这是通过预先构建一个搜索查询来完成的然后不断地将新闻项的流与该查询进行匹配。在一些网站上也有类似的功能例如当市场上出现符合其搜索条件的新房产时房地产网站的用户可以要求网站通知他们。Elasticsearch 的这种过滤器功能是实现这种流搜索的一种选择【76】。 例如,媒体监测服务可以订阅新闻文章 Feed 与来自媒体的播客搜索任何关于公司、产品或感兴趣的话题的新闻。这是通过预先构建一个搜索查询来完成的然后不断地将新闻项的流与该查询进行匹配。在一些网站上也有类似的功能例如当市场上出现符合其搜索条件的新房产时房地产网站的用户可以要求网站通知他们。Elasticsearch 的这种过滤器功能是实现这种流搜索的一种选择【76】。
@ -612,7 +612,7 @@ GROUP BY follows.follower_id
流连接直接对应于这个查询中的表连接。时间线实际上是这个查询结果的缓存,每当底层的表发生变化时都会更新 [^iii]。 流连接直接对应于这个查询中的表连接。时间线实际上是这个查询结果的缓存,每当底层的表发生变化时都会更新 [^iii]。
[^iii]: 如果你将流视作表的生物,如 [图 11-6](/v1/ddia_1106.png) 所示而把一个连接看作是两个表的乘法u·v那么会发生一些有趣的事情物化连接的变化流遵循乘积法则(u·v)'= u'v + uv'。换句话说任何推文的变化量都与当前的关注联系在一起任何关注的变化量都与当前的推文相连接【49,50】。 [^iii]: 如果你将流视作表的生物,如 [图 11-6](/v1/ddia_1106.png) 所示而把一个连接看作是两个表的乘法u·v那么会发生一些有趣的事情物化连接的变化流遵循乘积法则(u·v)'= u'v + uv'。换句话说任何推文的变化量都与当前的关注联系在一起任何关注的变化量都与当前的推文相连接【49,50】。
#### 连接的时间依赖性 #### 连接的时间依赖性
@ -691,13 +691,13 @@ AMQP/JMS 风格的消息代理
基于日志的消息代理 基于日志的消息代理
: 代理将一个分区中的所有消息分配给同一个消费者节点,并始终以相同的顺序传递消息。并行是通过分区实现的,消费者通过存档最近处理消息的偏移量来跟踪工作进度。消息代理将消息保留在磁盘上,因此如有必要的话,可以回跳并重新读取旧消息。 : 代理将一个分区中的所有消息分配给同一个消费者节点,并始终以相同的顺序传递消息。并行是通过分区实现的,消费者通过存档最近处理消息的偏移量来跟踪工作进度。消息代理将消息保留在磁盘上,因此如有必要的话,可以回跳并重新读取旧消息。
基于日志的方法与数据库中的复制日志(请参阅 [第五章](/v1/ch5))和日志结构存储引擎(请参阅 [第三章](/v1/ch3))有相似之处。我们看到,这种方法对于消费输入流,并产生衍生状态或衍生输出数据流的系统而言特别适用。 基于日志的方法与数据库中的复制日志(请参阅 [第五章](/v1/ch5))和日志结构存储引擎(请参阅 [第三章](/v1/ch3))有相似之处。我们看到,这种方法对于消费输入流,并产生派生状态或派生输出数据流的系统而言特别适用。
就流的来源而言,我们讨论了几种可能性:用户活动事件,定期读数的传感器,和 Feed 数据(例如,金融中的市场数据)能够自然地表示为流。我们发现将数据库写入视作流也是很有用的:我们可以捕获变更日志 —— 即对数据库所做的所有变更的历史记录 —— 隐式地通过变更数据捕获,或显式地通过事件溯源。日志压缩允许流也能保有数据库内容的完整副本。 就流的来源而言,我们讨论了几种可能性:用户活动事件,定期读数的传感器,和 Feed 数据(例如,金融中的市场数据)能够自然地表示为流。我们发现将数据库写入视作流也是很有用的:我们可以捕获变更日志 —— 即对数据库所做的所有变更的历史记录 —— 隐式地通过变更数据捕获,或显式地通过事件溯源。日志压缩允许流也能保有数据库内容的完整副本。
将数据库表示为流为系统集成带来了很多强大机遇。通过消费变更日志并将其应用至衍生系统,你能使诸如搜索索引、缓存以及分析系统这类衍生数据系统不断保持更新。你甚至能从头开始,通过读取从创世至今的所有变更日志,为现有数据创建全新的视图。 将数据库表示为流为系统集成带来了很多强大机遇。通过消费变更日志并将其应用至派生系统,你能使诸如搜索索引、缓存以及分析系统这类派生数据系统不断保持更新。你甚至能从头开始,通过读取从创世至今的所有变更日志,为现有数据创建全新的视图。
像流一样维护状态以及消息重播的基础设施,是在各种流处理框架中实现流连接和容错的基础。我们讨论了流处理的几种目的,包括搜索事件模式(复杂事件处理),计算分窗聚合(流分析),以及保证生数据系统处于最新状态(物化视图)。 像流一样维护状态以及消息重播的基础设施,是在各种流处理框架中实现流连接和容错的基础。我们讨论了流处理的几种目的,包括搜索事件模式(复杂事件处理),计算分窗聚合(流分析),以及保证生数据系统处于最新状态(物化视图)。
然后我们讨论了在流处理中对时间进行推理的困难,包括处理时间与事件时间戳之间的区别,以及当你认为窗口已经完事之后,如何处理到达的掉队事件的问题。 然后我们讨论了在流处理中对时间进行推理的困难,包括处理时间与事件时间戳之间的区别,以及当你认为窗口已经完事之后,如何处理到达的掉队事件的问题。

View File

@ -36,21 +36,21 @@ breadcrumbs: false
### 组合使用派生数据的工具 ### 组合使用派生数据的工具
例如,为了处理任意关键词的搜索查询,将 OLTP 数据库与全文索索引集成在一起是很常见的需求。尽管一些数据库(例如 PostgreSQL包含了全文索引功能对于简单的应用完全够了【1】但更复杂的搜索能力就需要专业的信息检索工具了。相反的是搜索索引通常不适合作为持久的记录系统因此许多应用需要组合这两种不同的工具以满足所有需求。 例如,为了处理任意关键词的搜索查询,将 OLTP 数据库与全文索索引集成在一起是很常见的需求。尽管一些数据库(例如 PostgreSQL包含了全文索引功能对于简单的应用完全够了【1】但更复杂的搜索能力就需要专业的信息检索工具了。相反的是搜索索引通常不适合作为持久的记录系统因此许多应用需要组合这两种不同的工具以满足所有需求。
我们在 “[保持系统同步](/ch12#保持系统同步)” 中接触过集成数据系统的问题。随着数据不同表示形式的增加,集成问题变得越来越困难。除了数据库和搜索索引之外,也许你需要在分析系统(数据仓库,或批处理和流处理系统)中维护数据副本;维护从原始数据中生的缓存,或反规范化的数据版本;将数据灌入机器学习、分类、排名或推荐系统中;或者基于数据变更发送通知。 我们在 “[保持系统同步](/ch12#保持系统同步)” 中接触过集成数据系统的问题。随着数据不同表示形式的增加,集成问题变得越来越困难。除了数据库和搜索索引之外,也许你需要在分析系统(数据仓库,或批处理和流处理系统)中维护数据副本;维护从原始数据中生的缓存,或反规范化的数据版本;将数据灌入机器学习、分类、排名或推荐系统中;或者基于数据变更发送通知。
令人惊讶的是我经常看到软件工程师做出这样的陈述“根据我的经验99% 的人只需要 X” 或者 “...... 不需要 X”对于各种各样的 X。我认为这种陈述更像是发言人自己的经验而不是技术实际上的实用性。可能对数据执行的操作其范围极其宽广。某人认为鸡肋而毫无意义的功能可能是别人的核心需求。当你拉高视角并考虑跨越整个组织范围的数据流时数据集成的需求往往就会变得明显起来。 令人惊讶的是我经常看到软件工程师做出这样的陈述“根据我的经验99% 的人只需要 X” 或者 “...... 不需要 X”对于各种各样的 X。我认为这种陈述更像是发言人自己的经验而不是技术实际上的实用性。可能对数据执行的操作其范围极其宽广。某人认为鸡肋而毫无意义的功能可能是别人的核心需求。当你拉高视角并考虑跨越整个组织范围的数据流时数据集成的需求往往就会变得明显起来。
#### 理解数据流 #### 理解数据流
当需要在多个存储系统中维护相同数据的副本以满足不同的访问模式时,你要对输入和输出了如指掌:哪些数据先写入,哪些数据表示生自哪些来源?如何以正确的格式,将所有数据导入正确的地方? 当需要在多个存储系统中维护相同数据的副本以满足不同的访问模式时,你要对输入和输出了如指掌:哪些数据先写入,哪些数据表示生自哪些来源?如何以正确的格式,将所有数据导入正确的地方?
例如,你可能会首先将数据写入 **记录系统** 数据库,捕获对该数据库所做的变更(请参阅 “[变更数据捕获](/ch12#变更数据捕获)”然后将变更以相同的顺序应用于搜索索引。如果变更数据捕获CDC是更新索引的唯一方式则可以确定该索引完全派生自记录系统因此与其保持一致除软件错误外。写入数据库是向该系统提供新输入的唯一方式。 例如,你可能会首先将数据写入 **记录系统** 数据库,捕获对该数据库所做的变更(请参阅 “[变更数据捕获](/ch12#变更数据捕获)”然后将变更以相同的顺序应用于搜索索引。如果变更数据捕获CDC是更新索引的唯一方式则可以确定该索引完全派生自记录系统因此与其保持一致除软件错误外。写入数据库是向该系统提供新输入的唯一方式。
允许应用程序直接写入搜索索引和数据库引入了如 [图 11-4](/fig/ddia_1204.png) 所示的问题,其中两个客户端同时发送冲突的写入,且两个存储系统按不同顺序处理它们。在这种情况下,既不是数据库说了算,也不是搜索索引说了算,所以它们做出了相反的决定,进入彼此间持久性的不一致状态。 允许应用程序直接写入搜索索引和数据库引入了如 [图 11-4](/fig/ddia_1204.png) 所示的问题,其中两个客户端同时发送冲突的写入,且两个存储系统按不同顺序处理它们。在这种情况下,既不是数据库说了算,也不是搜索索引说了算,所以它们做出了相反的决定,进入彼此间持久性的不一致状态。
如果你可以通过单个系统来提供所有用户输入,从而决定所有写入的排序,则通过按相同顺序处理写入,可以更容易地生出其他数据表示。这是状态机复制方法的一个应用,我们在 “[全序广播](/ch9#全序广播)” 中看到。无论你使用变更数据捕获还是事件溯源日志,都不如简单的基于全序的决策原则更重要。 如果你可以通过单个系统来提供所有用户输入,从而决定所有写入的排序,则通过按相同顺序处理写入,可以更容易地生出其他数据表示。这是状态机复制方法的一个应用,我们在 “[全序广播](/ch9#全序广播)” 中看到。无论你使用变更数据捕获还是事件溯源日志,都不如简单的基于全序的决策原则更重要。
基于事件日志来更新派生数据的系统,通常可以做到 **确定性****幂等性**(请参阅 “[幂等性](/ch12#幂等性)”),使得从故障中恢复相当容易。 基于事件日志来更新派生数据的系统,通常可以做到 **确定性****幂等性**(请参阅 “[幂等性](/ch12#幂等性)”),使得从故障中恢复相当容易。
@ -93,7 +93,7 @@ breadcrumbs: false
* 如果你可以记录一个事件来记录用户在做出决定之前所看到的系统状态并给该事件一个唯一的标识符那么后面的任何事件都可以引用该事件标识符来记录因果关系【4】。我们将在 “[读也是事件](#读也是事件)” 中回到这个想法。 * 如果你可以记录一个事件来记录用户在做出决定之前所看到的系统状态并给该事件一个唯一的标识符那么后面的任何事件都可以引用该事件标识符来记录因果关系【4】。我们将在 “[读也是事件](#读也是事件)” 中回到这个想法。
* 冲突解决算法(请参阅 “[自动冲突解决](/ch5#自动冲突解决)”)有助于处理以意外顺序传递的事件。它们对于维护状态很有用,但如果行为有外部副作用(例如,给用户发送通知),就没什么帮助了。 * 冲突解决算法(请参阅 “[自动冲突解决](/ch5#自动冲突解决)”)有助于处理以意外顺序传递的事件。它们对于维护状态很有用,但如果行为有外部副作用(例如,给用户发送通知),就没什么帮助了。
也许,随着时间的推移,应用开发模式将出现,使得能够有效地捕获因果依赖关系,并且保持正确的生状态,而不会迫使所有事件经历全序广播的瓶颈)。 也许,随着时间的推移,应用开发模式将出现,使得能够有效地捕获因果依赖关系,并且保持正确的生状态,而不会迫使所有事件经历全序广播的瓶颈)。
### 批处理与流处理 ### 批处理与流处理
@ -105,11 +105,11 @@ breadcrumbs: false
Spark 在批处理引擎上执行流处理,将流分解为 **微批次microbatches**,而 Apache Flink 则在流处理引擎上执行批处理【5】。原则上一种类型的处理可以用另一种类型来模拟但是性能特征会有所不同例如在跳跃或滑动窗口上微批次可能表现不佳【6】。 Spark 在批处理引擎上执行流处理,将流分解为 **微批次microbatches**,而 Apache Flink 则在流处理引擎上执行批处理【5】。原则上一种类型的处理可以用另一种类型来模拟但是性能特征会有所不同例如在跳跃或滑动窗口上微批次可能表现不佳【6】。
#### 维护生状态 #### 维护生状态
批处理有着很强的函数式风格(即使其代码不是用函数式语言编写的):它鼓励确定性的纯函数,其输出仅依赖于输入,除了显式输出外没有副作用,将输入视作不可变的,且输出是仅追加的。流处理与之类似,但它扩展了算子以允许受管理的、容错的状态(请参阅 “[失败后重建状态”](/ch12#失败后重建状态))。 批处理有着很强的函数式风格(即使其代码不是用函数式语言编写的):它鼓励确定性的纯函数,其输出仅依赖于输入,除了显式输出外没有副作用,将输入视作不可变的,且输出是仅追加的。流处理与之类似,但它扩展了算子以允许受管理的、容错的状态(请参阅 “[失败后重建状态”](/ch12#失败后重建状态))。
具有良好定义的输入和输出的确定性函数的原理不仅有利于容错(请参阅 “[幂等性](/ch12#幂等性)”也简化了有关组织中数据流的推理【7】。无论派生数据是搜索索引、统计模型还是缓存采用这种观点思考都是很有帮助的将其视为从一个东西生出另一个的数据管道,通过函数式应用代码推送一个系统的状态变更,并将其效果应用至派生系统中。 具有良好定义的输入和输出的确定性函数的原理不仅有利于容错(请参阅 “[幂等性](/ch12#幂等性)”也简化了有关组织中数据流的推理【7】。无论派生数据是搜索索引、统计模型还是缓存采用这种观点思考都是很有帮助的将其视为从一个东西生出另一个的数据管道,通过函数式应用代码推送一个系统的状态变更,并将其效果应用至派生系统中。
原则上,派生数据系统可以同步地维护,就像关系数据库在与索引表写入操作相同的事务中同步更新次级索引一样。然而,异步是使基于事件日志的系统稳健的原因:它允许系统的一部分故障被抑制在本地。而如果任何一个参与者失败,分布式事务将中止,因此它们倾向于通过将故障传播到系统的其余部分来放大故障(请参阅 “[分布式事务的限制](/ch9#分布式事务的限制)”)。 原则上,派生数据系统可以同步地维护,就像关系数据库在与索引表写入操作相同的事务中同步更新次级索引一样。然而,异步是使基于事件日志的系统稳健的原因:它允许系统的一部分故障被抑制在本地。而如果任何一个参与者失败,分布式事务将中止,因此它们倾向于通过将故障传播到系统的其余部分来放大故障(请参阅 “[分布式事务的限制](/ch9#分布式事务的限制)”)。
@ -117,7 +117,7 @@ Spark 在批处理引擎上执行流处理,将流分解为 **微批次micro
#### 应用演化后重新处理数据 #### 应用演化后重新处理数据
在维护派生数据时,批处理和流处理都是有用的。流处理允许将输入中的变化以低延迟反映在生视图中,而批处理允许重新处理大量累积的历史数据以便将新视图导出到现有数据集上。 在维护派生数据时,批处理和流处理都是有用的。流处理允许将输入中的变化以低延迟反映在生视图中,而批处理允许重新处理大量累积的历史数据以便将新视图导出到现有数据集上。
特别是,重新处理现有数据为维护系统、演化并支持新功能和需求变更提供了一个良好的机制(请参阅 [第四章](/ch4))。没有重新进行处理,模式演化将仅限于简单的变化,例如向记录中添加新的可选字段或添加新类型的记录。无论是在写时模式还是在读时模式中都是如此(请参阅 “[文档模型中的模式灵活性](/ch2#文档模型中的模式灵活性)”)。另一方面,通过重新处理,可以将数据集重组为一个完全不同的模型,以便更好地满足新的要求。 特别是,重新处理现有数据为维护系统、演化并支持新功能和需求变更提供了一个良好的机制(请参阅 [第四章](/ch4))。没有重新进行处理,模式演化将仅限于简单的变化,例如向记录中添加新的可选字段或添加新类型的记录。无论是在写时模式还是在读时模式中都是如此(请参阅 “[文档模型中的模式灵活性](/ch2#文档模型中的模式灵活性)”)。另一方面,通过重新处理,可以将数据集重组为一个完全不同的模型,以便更好地满足新的要求。
@ -129,7 +129,7 @@ Spark 在批处理引擎上执行流处理,将流分解为 **微批次micro
> >
> 以这种方式 “再加工” 现有的轨道,让新旧版本并存,可以在几年的时间内逐渐改变轨距。然而,这是一项昂贵的事业,这就是今天非标准轨距仍然存在的原因。例如,旧金山湾区的 BART 系统使用了与美国大部分地区不同的轨距。 > 以这种方式 “再加工” 现有的轨道,让新旧版本并存,可以在几年的时间内逐渐改变轨距。然而,这是一项昂贵的事业,这就是今天非标准轨距仍然存在的原因。例如,旧金山湾区的 BART 系统使用了与美国大部分地区不同的轨距。
生视图允许 **渐进演化gradual evolution**。如果你想重新构建数据集,不需要执行突然切换式的迁移。取而代之的是,你可以将旧架构和新架构并排维护为相同基础数据上的两个独立生视图。然后可以开始将少量用户转移到新视图以测试其性能并发现任何错误而大多数用户仍然会被路由到旧视图。你可以逐渐地增加访问新视图的用户比例最终可以删除旧视图【10】。 生视图允许 **渐进演化gradual evolution**。如果你想重新构建数据集,不需要执行突然切换式的迁移。取而代之的是,你可以将旧架构和新架构并排维护为相同基础数据上的两个独立生视图。然后可以开始将少量用户转移到新视图以测试其性能并发现任何错误而大多数用户仍然会被路由到旧视图。你可以逐渐地增加访问新视图的用户比例最终可以删除旧视图【10】。
这种逐渐迁移的美妙之处在于如果出现问题每个阶段的过程都很容易逆转你始终有一个可以回滚的可用系统。通过降低不可逆损害的风险你能对继续前进更有信心从而更快地改善系统【11】。 这种逐渐迁移的美妙之处在于如果出现问题每个阶段的过程都很容易逆转你始终有一个可以回滚的可用系统。通过降低不可逆损害的风险你能对继续前进更有信心从而更快地改善系统【11】。
@ -137,11 +137,11 @@ Spark 在批处理引擎上执行流处理,将流分解为 **微批次micro
如果批处理用于重新处理历史数据而流处理用于处理最近的更新那么如何将这两者结合起来Lambda 架构【12】是这方面的一个建议引起了很多关注。 如果批处理用于重新处理历史数据而流处理用于处理最近的更新那么如何将这两者结合起来Lambda 架构【12】是这方面的一个建议引起了很多关注。
Lambda 架构的核心思想是通过将不可变事件附加到不断增长的数据集来记录传入数据,这类似于事件溯源(请参阅 “[事件溯源](/ch12#事件溯源)”)。为了从这些事件中生出读取优化的视图Lambda 架构建议并行运行两个不同的系统:批处理系统(如 Hadoop MapReduce和独立的流处理系统如 Storm Lambda 架构的核心思想是通过将不可变事件附加到不断增长的数据集来记录传入数据,这类似于事件溯源(请参阅 “[事件溯源](/ch12#事件溯源)”)。为了从这些事件中生出读取优化的视图Lambda 架构建议并行运行两个不同的系统:批处理系统(如 Hadoop MapReduce和独立的流处理系统如 Storm
在 Lambda 方法中,流处理器消耗事件并快速生成对视图的近似更新;批处理器稍后将使用同一组事件并生成生视图的更正版本。这个设计背后的原因是批处理更简单,因此不易出错,而流处理器被认为是不太可靠和难以容错的(请参阅 “[容错](/ch12#容错)”)。而且,流处理可以使用快速近似算法,而批处理使用较慢的精确算法。 在 Lambda 方法中,流处理器消耗事件并快速生成对视图的近似更新;批处理器稍后将使用同一组事件并生成生视图的更正版本。这个设计背后的原因是批处理更简单,因此不易出错,而流处理器被认为是不太可靠和难以容错的(请参阅 “[容错](/ch12#容错)”)。而且,流处理可以使用快速近似算法,而批处理使用较慢的精确算法。
Lambda 架构是一种有影响力的想法,它将数据系统的设计变得更好,尤其是通过推广这样的原则:在不可变事件流上建立生视图,并在需要时重新处理事件。但是我也认为它有一些实际问题: Lambda 架构是一种有影响力的想法,它将数据系统的设计变得更好,尤其是通过推广这样的原则:在不可变事件流上建立生视图,并在需要时重新处理事件。但是我也认为它有一些实际问题:
* 在批处理和流处理框架中维护相同的逻辑是很显著的额外工作。虽然像 Summingbird【13】这样的库提供了一种可以在批处理和流处理的上下文中运行的计算抽象。调试、调整和维护两个不同系统的操作复杂性依然存在【14】。 * 在批处理和流处理框架中维护相同的逻辑是很显著的额外工作。虽然像 Summingbird【13】这样的库提供了一种可以在批处理和流处理的上下文中运行的计算抽象。调试、调整和维护两个不同系统的操作复杂性依然存在【14】。
* 由于流管道和批处理管道产生独立的输出,因此需要合并它们以响应用户请求。如果计算是基于滚动窗口的简单聚合,则合并相当容易,但如果视图基于更复杂的操作(例如连接和会话化)而导出,或者输出不是时间序列,则会变得非常困难。 * 由于流管道和批处理管道产生独立的输出,因此需要合并它们以响应用户请求。如果计算是基于滚动窗口的简单聚合,则合并相当容易,但如果视图基于更复杂的操作(例如连接和会话化)而导出,或者输出不是时间序列,则会变得非常困难。
@ -179,9 +179,9 @@ Unix 和关系数据库以非常不同的哲学来处理信息管理问题。Uni
* 次级索引,使你可以根据字段的值有效地搜索记录(请参阅 “[其他索引结构](/ch3#其他索引结构)”) * 次级索引,使你可以根据字段的值有效地搜索记录(请参阅 “[其他索引结构](/ch3#其他索引结构)”)
* 物化视图,这是一种预计算的查询结果缓存(请参阅 “[聚合:数据立方体和物化视图](/ch3#聚合:数据立方体和物化视图)”) * 物化视图,这是一种预计算的查询结果缓存(请参阅 “[聚合:数据立方体和物化视图](/ch3#聚合:数据立方体和物化视图)”)
* 复制日志,保持其他节点上数据的副本最新(请参阅 “[复制日志的实现](/ch5#复制日志的实现)”) * 复制日志,保持其他节点上数据的副本最新(请参阅 “[复制日志的实现](/ch5#复制日志的实现)”)
* 全文搜索索引,允许在文本中进行关键字搜索(请参阅 “[全文搜索和模糊索引](/ch3#全文搜索和模糊索引)”也内置于某些关系数据库【1】 * 全文检索索引,允许在文本中进行关键字搜索(请参阅 “[全文检索与模糊索引](/ch3#全文检索与模糊索引)”也内置于某些关系数据库【1】
在 [第十一章](/ch11) 和 [第十二章](/ch12) 中,出现了类似的主题。我们讨论了如何构建全文索索引(请参阅 “[批处理工作流的输出](/ch11#批处理工作流的输出)”),了解了如何维护物化视图(请参阅 “[维护物化视图](/ch12#维护物化视图)”)以及如何将变更从数据库复制到派生数据系统(请参阅 “[变更数据捕获](/ch12#变更数据捕获)”)。 在 [第十一章](/ch11) 和 [第十二章](/ch12) 中,出现了类似的主题。我们讨论了如何构建全文索索引(请参阅 “[批处理工作流的输出](/ch11#批处理工作流的输出)”),了解了如何维护物化视图(请参阅 “[维护物化视图](/ch12#维护物化视图)”)以及如何将变更从数据库复制到派生数据系统(请参阅 “[变更数据捕获](/ch12#变更数据捕获)”)。
数据库中内置的功能与人们用批处理和流处理器构建的派生数据系统似乎有相似之处。 数据库中内置的功能与人们用批处理和流处理器构建的派生数据系统似乎有相似之处。
@ -256,16 +256,16 @@ Unix 和关系数据库以非常不同的哲学来处理信息管理问题。Uni
在本节中,我将详细介绍这些想法,并探讨一些围绕分拆数据库和数据流的想法构建应用的方法。 在本节中,我将详细介绍这些想法,并探讨一些围绕分拆数据库和数据流的想法构建应用的方法。
#### 应用代码作为生函数 #### 应用代码作为生函数
当一个数据集生自另一个数据集时,它会经历某种转换函数。例如: 当一个数据集生自另一个数据集时,它会经历某种转换函数。例如:
* 次级索引是由一种直白的转换函数生成的派生数据集:对于基础表中的每行或每个文档,它挑选被索引的列或字段中的值,并按这些值排序(假设使用 B 树或 SSTable 索引,按键排序,如 [第三章](/ch3) 所述)。 * 次级索引是由一种直白的转换函数生成的派生数据集:对于基础表中的每行或每个文档,它挑选被索引的列或字段中的值,并按这些值排序(假设使用 B 树或 SSTable 索引,按键排序,如 [第三章](/ch3) 所述)。
* 全文索索引是通过应用各种自然语言处理函数而创建的,诸如语言检测、分词、词干或词汇化、拼写纠正和同义词识别,然后构建用于高效查找的数据结构(例如倒排索引)。 * 全文索索引是通过应用各种自然语言处理函数而创建的,诸如语言检测、分词、词干或词汇化、拼写纠正和同义词识别,然后构建用于高效查找的数据结构(例如倒排索引)。
* 在机器学习系统中,我们可以将模型视作从训练数据通过应用各种特征提取、统计分析函数生的数据,当模型应用于新的输入数据时,模型的输出是从输入和模型(因此间接地从训练数据)中生的。 * 在机器学习系统中,我们可以将模型视作从训练数据通过应用各种特征提取、统计分析函数生的数据,当模型应用于新的输入数据时,模型的输出是从输入和模型(因此间接地从训练数据)中生的。
* 缓存通常包含将以用户界面UI显示的形式的数据聚合。因此填充缓存需要知道 UI 中引用的字段UI 中的变更可能需要更新缓存填充方式的定义,并重建缓存。 * 缓存通常包含将以用户界面UI显示的形式的数据聚合。因此填充缓存需要知道 UI 中引用的字段UI 中的变更可能需要更新缓存填充方式的定义,并重建缓存。
用于次级索引的生函数是如此常用的需求,以致于它作为核心功能被内建至许多数据库中,你可以简单地通过 `CREATE INDEX` 来调用它。对于全文索引常见语言的基本语言特征可能内置到数据库中但更复杂的特征通常需要领域特定的调整。在机器学习中特征工程是众所周知的特定于应用的特征通常需要包含很多关于用户交互与应用部署的详细知识【35】。 用于次级索引的生函数是如此常用的需求,以致于它作为核心功能被内建至许多数据库中,你可以简单地通过 `CREATE INDEX` 来调用它。对于全文索引常见语言的基本语言特征可能内置到数据库中但更复杂的特征通常需要领域特定的调整。在机器学习中特征工程是众所周知的特定于应用的特征通常需要包含很多关于用户交互与应用部署的详细知识【35】。
当创建派生数据集的函数不是像创建次级索引那样的标准搬砖函数时,需要自定义代码来处理特定于应用的东西。而这个自定义代码是让许多数据库挣扎的地方,虽然关系数据库通常支持触发器、存储过程和用户定义的函数,可以用它们来在数据库中执行应用代码,但它们有点像数据库设计里的事后反思。(请参阅 “[传递事件流](/ch12#传递事件流)”)。 当创建派生数据集的函数不是像创建次级索引那样的标准搬砖函数时,需要自定义代码来处理特定于应用的东西。而这个自定义代码是让许多数据库挣扎的地方,虽然关系数据库通常支持触发器、存储过程和用户定义的函数,可以用它们来在数据库中执行应用代码,但它们有点像数据库设计里的事后反思。(请参阅 “[传递事件流](/ch12#传递事件流)”)。
@ -293,16 +293,16 @@ Unix 和关系数据库以非常不同的哲学来处理信息管理问题。Uni
我们在 “[数据库与流](/ch12#数据库与流)” 中看到了这一思路,我们讨论了将数据库的变更日志视为一种我们可以订阅的事件流。诸如 Actor 的消息传递系统(请参阅 “[消息传递中的数据流](/ch4#消息传递中的数据流)”)也具有响应事件的概念。早在 20 世纪 80 年代,**元组空间tuple space** 模型就已经探索了表达分布式计算的方式观察状态变更并作出反应的过程【38,39】。 我们在 “[数据库与流](/ch12#数据库与流)” 中看到了这一思路,我们讨论了将数据库的变更日志视为一种我们可以订阅的事件流。诸如 Actor 的消息传递系统(请参阅 “[消息传递中的数据流](/ch4#消息传递中的数据流)”)也具有响应事件的概念。早在 20 世纪 80 年代,**元组空间tuple space** 模型就已经探索了表达分布式计算的方式观察状态变更并作出反应的过程【38,39】。
如前所述,当触发器由于数据变更而被触发时,或次级索引更新以反映索引表中的变更时,数据库内部也发生着类似的情况。分拆数据库意味着将这个想法应用于在主数据库之外,用于创建派生数据集:缓存、全文索索引、机器学习或分析系统。我们可以为此使用流处理和消息传递系统。 如前所述,当触发器由于数据变更而被触发时,或次级索引更新以反映索引表中的变更时,数据库内部也发生着类似的情况。分拆数据库意味着将这个想法应用于在主数据库之外,用于创建派生数据集:缓存、全文索索引、机器学习或分析系统。我们可以为此使用流处理和消息传递系统。
需要记住的重要一点是,维护派生数据不同于执行异步任务。传统的消息传递系统通常是为执行异步任务设计的(请参阅 “[日志与传统的消息传递相比](/ch12#日志与传统的消息传递相比)”): 需要记住的重要一点是,维护派生数据不同于执行异步任务。传统的消息传递系统通常是为执行异步任务设计的(请参阅 “[日志与传统的消息传递相比](/ch12#日志与传统的消息传递相比)”):
* 在维护派生数据时,状态变更的顺序通常很重要(如果多个视图是从事件日志生的,则需要按照相同的顺序处理事件,以便它们之间保持一致)。如 “[确认与重新传递](/ch12#确认与重新传递)” 中所述,许多消息代理在重传未确认消息时没有此属性,双写也被排除在外(请参阅 “[保持系统同步](/ch12#保持系统同步)”)。 * 在维护派生数据时,状态变更的顺序通常很重要(如果多个视图是从事件日志生的,则需要按照相同的顺序处理事件,以便它们之间保持一致)。如 “[确认与重新传递](/ch12#确认与重新传递)” 中所述,许多消息代理在重传未确认消息时没有此属性,双写也被排除在外(请参阅 “[保持系统同步](/ch12#保持系统同步)”)。
* 容错是派生数据的关键:仅仅丢失单个消息就会导致派生数据集永远与其数据源失去同步。消息传递和生状态更新都必须可靠。例如,许多 Actor 系统默认在内存中维护 Actor 的状态和消息,所以如果运行 Actor 的机器崩溃,状态和消息就会丢失。 * 容错是派生数据的关键:仅仅丢失单个消息就会导致派生数据集永远与其数据源失去同步。消息传递和生状态更新都必须可靠。例如,许多 Actor 系统默认在内存中维护 Actor 的状态和消息,所以如果运行 Actor 的机器崩溃,状态和消息就会丢失。
稳定的消息排序和容错消息处理是相当严格的要求,但与分布式事务相比,它们开销更小,运行更稳定。现代流处理组件可以提供这些排序和可靠性保证,并允许应用代码以流算子的形式运行。 稳定的消息排序和容错消息处理是相当严格的要求,但与分布式事务相比,它们开销更小,运行更稳定。现代流处理组件可以提供这些排序和可靠性保证,并允许应用代码以流算子的形式运行。
这些应用代码可以执行任意处理,包括数据库内置生函数通常不提供的功能。就像通过管道链接的 Unix 工具一样,流算子可以围绕着数据流构建大型系统。每个算子接受状态变更的流作为输入,并产生其他状态变化的流作为输出。 这些应用代码可以执行任意处理,包括数据库内置生函数通常不提供的功能。就像通过管道链接的 Unix 工具一样,流算子可以围绕着数据流构建大型系统。每个算子接受状态变更的流作为输入,并产生其他状态变化的流作为输出。
#### 流处理器和服务 #### 流处理器和服务
@ -339,7 +339,7 @@ Unix 和关系数据库以非常不同的哲学来处理信息管理问题。Uni
#### 物化视图和缓存 #### 物化视图和缓存
全文索索引就是一个很好的例子写路径更新索引读路径在索引中搜索关键字。读写都需要做一些工作。写入需要更新文档中出现的所有关键词的索引条目。读取需要搜索查询中的每个单词并应用布尔逻辑来查找包含查询中所有单词AND 运算符的文档或者每个单词OR 运算符)的任何同义词。 全文索索引就是一个很好的例子写路径更新索引读路径在索引中搜索关键字。读写都需要做一些工作。写入需要更新文档中出现的所有关键词的索引条目。读取需要搜索查询中的每个单词并应用布尔逻辑来查找包含查询中所有单词AND 运算符的文档或者每个单词OR 运算符)的任何同义词。
如果没有索引,搜索查询将不得不扫描所有文档(如 grep如果有着大量文档这样做的开销巨大。没有索引意味着写入路径上的工作量较少没有要更新的索引但是在读取路径上需要更多工作。 如果没有索引,搜索查询将不得不扫描所有文档(如 grep如果有着大量文档这样做的开销巨大。没有索引意味着写入路径上的工作量较少没有要更新的索引但是在读取路径上需要更多工作。
@ -490,7 +490,7 @@ COMMIT;
[例 12-2]() 依赖于 `request_id` 列上的唯一约束。如果一个事务尝试插入一个已经存在的 ID那么 `INSERT` 失败,事务被中止,使其无法生效两次。即使在较弱的隔离级别下,关系数据库也能正确地维护唯一性约束(而在 “[写入偏差与幻读](/ch7#写入偏差与幻读)” 中讨论过,应用级别的 **检查 - 然后 - 插入** 可能会在不可串行化的隔离下失败)。 [例 12-2]() 依赖于 `request_id` 列上的唯一约束。如果一个事务尝试插入一个已经存在的 ID那么 `INSERT` 失败,事务被中止,使其无法生效两次。即使在较弱的隔离级别下,关系数据库也能正确地维护唯一性约束(而在 “[写入偏差与幻读](/ch7#写入偏差与幻读)” 中讨论过,应用级别的 **检查 - 然后 - 插入** 可能会在不可串行化的隔离下失败)。
除了抑制重复的请求之外,[例 12-2]() 中的请求表表现得就像一种事件日志,暗示着事件溯源的想法(请参阅 “[事件溯源](/ch12#事件溯源)”)。更新账户余额事实上不必与插入事件发生在同一个事务中,因为它们是冗余的,而能由下游消费者从请求事件中生出来 —— 只要该事件被恰好处理一次,这又一次可以使用请求 ID 来强制执行。 除了抑制重复的请求之外,[例 12-2]() 中的请求表表现得就像一种事件日志,暗示着事件溯源的想法(请参阅 “[事件溯源](/ch12#事件溯源)”)。更新账户余额事实上不必与插入事件发生在同一个事务中,因为它们是冗余的,而能由下游消费者从请求事件中生出来 —— 只要该事件被恰好处理一次,这又一次可以使用请求 ID 来强制执行。
#### 端到端原则 #### 端到端原则
@ -563,7 +563,7 @@ COMMIT;
2. 流处理器读取请求日志。对于每个请求消息,它向输出流发出两条消息:付款人账户 A 的借记指令(按 A 分区),收款人 B 的贷记指令(按 B 分区)。被发出的消息中会带有原始的请求 ID。 2. 流处理器读取请求日志。对于每个请求消息,它向输出流发出两条消息:付款人账户 A 的借记指令(按 A 分区),收款人 B 的贷记指令(按 B 分区)。被发出的消息中会带有原始的请求 ID。
3. 后续处理器消费借记 / 贷记指令流,按照请求 ID 除重,并将变更应用至账户余额。 3. 后续处理器消费借记 / 贷记指令流,按照请求 ID 除重,并将变更应用至账户余额。
步骤 1 和步骤 2 是必要的,因为如果客户直接发送贷记与借记指令,则需要在这两个分区之间进行原子提交,以确保两者要么都发生或都不发生。为了避免对分布式事务的需要,我们首先将请求持久化记录为单条消息,然后从这第一条消息中生出贷记指令与借记指令。几乎在所有数据系统中,单对象写入都是原子性的(请参阅 “[单对象写入](/ch7#单对象写入)),因此请求要么出现在日志中,要么就不出现,无需多分区原子提交。 步骤 1 和步骤 2 是必要的,因为如果客户直接发送贷记与借记指令,则需要在这两个分区之间进行原子提交,以确保两者要么都发生或都不发生。为了避免对分布式事务的需要,我们首先将请求持久化记录为单条消息,然后从这第一条消息中生出贷记指令与借记指令。几乎在所有数据系统中,单对象写入都是原子性的(请参阅 “[单对象写入](/ch7#单对象写入)),因此请求要么出现在日志中,要么就不出现,无需多分区原子提交。
如果流处理器在步骤 2 中崩溃,则它会从上一个存档点恢复处理。这样做时,它不会跳过任何请求消息,但可能会多次处理请求并产生重复的贷记与借记指令。但由于它是确定性的,因此它只是再次生成相同的指令,而步骤 3 中的处理器可以使用端到端请求 ID 轻松地对其除重。 如果流处理器在步骤 2 中崩溃,则它会从上一个存档点恢复处理。这样做时,它不会跳过任何请求消息,但可能会多次处理请求并产生重复的贷记与借记指令。但由于它是确定性的,因此它只是再次生成相同的指令,而步骤 3 中的处理器可以使用端到端请求 ID 轻松地对其除重。
@ -589,7 +589,7 @@ COMMIT;
* 完整性Integrity * 完整性Integrity
完整性意味着没有损坏;即没有数据丢失,并且没有矛盾或错误的数据。尤其是如果某些派生数据集是作为底层数据之上的视图而维护的(请参阅 “[从事件日志中派生出当前状态](/ch12#从事件日志中派生出当前状态)”),这种生必须是正确的。例如,数据库索引必须正确地反映数据库的内容 —— 缺失某些记录的索引并不是很有用。 完整性意味着没有损坏;即没有数据丢失,并且没有矛盾或错误的数据。尤其是如果某些派生数据集是作为底层数据之上的视图而维护的(请参阅 “[从事件日志中派生出当前状态](/ch12#从事件日志中派生出当前状态)”),这种生必须是正确的。例如,数据库索引必须正确地反映数据库的内容 —— 缺失某些记录的索引并不是很有用。
如果完整性被违背,这种不一致是永久的:在大多数情况下,等待与重试并不能修复数据库损坏。相反的是,需要显式地检查与修复。在 ACID 事务的上下文中(请参阅 “[ACID 的含义](/ch7#ACID的含义)”),一致性通常被理解为某种特定于应用的完整性概念。原子性和持久性是保持完整性的重要工具。 如果完整性被违背,这种不一致是永久的:在大多数情况下,等待与重试并不能修复数据库损坏。相反的是,需要显式地检查与修复。在 ACID 事务的上下文中(请参阅 “[ACID 的含义](/ch7#ACID的含义)”),一致性通常被理解为某种特定于应用的完整性概念。原子性和持久性是保持完整性的重要工具。
@ -611,7 +611,7 @@ ACID 事务通常既提供及时性(例如线性一致性)也提供完整性
正如我们在上一节看到的那样,可靠的流处理系统可以在无需分布式事务与原子提交协议的情况下保持完整性,这意味着它们有潜力达到与后者相当的正确性,同时还具备好得多的性能与运维稳健性。为了达成这种正确性,我们组合使用了多种机制: 正如我们在上一节看到的那样,可靠的流处理系统可以在无需分布式事务与原子提交协议的情况下保持完整性,这意味着它们有潜力达到与后者相当的正确性,同时还具备好得多的性能与运维稳健性。为了达成这种正确性,我们组合使用了多种机制:
* 将写入操作的内容表示为单条消息,从而可以轻松地被原子写入 —— 与事件溯源搭配效果拔群(请参阅 “[事件溯源](/ch12#事件溯源)”)。 * 将写入操作的内容表示为单条消息,从而可以轻松地被原子写入 —— 与事件溯源搭配效果拔群(请参阅 “[事件溯源](/ch12#事件溯源)”)。
* 使用与存储过程类似的确定性衍生函数,从这一消息中衍生出所有其他的状态变更(请参阅 “[真的串行执行](/ch7#真的串行执行)” 和 “[应用代码作为衍生函数](/ch13#应用代码作为衍生函数)”) * 使用与存储过程类似的确定性派生函数,从这一消息中派生出所有其他的状态变更(请参阅 “[真的串行执行](/ch7#真的串行执行)” 和 “[应用代码作为派生函数](/ch13#应用代码作为派生函数)”)
* 将客户端生成的请求 ID 传递通过所有的处理层次,从而允许端到端的除重,带来幂等性。 * 将客户端生成的请求 ID 传递通过所有的处理层次,从而允许端到端的除重,带来幂等性。
* 使消息不可变,并允许派生数据能随时被重新处理,这使从错误中恢复更加容易(请参阅 “[不可变事件的优点](/ch12#不可变事件的优点)”) * 使消息不可变,并允许派生数据能随时被重新处理,这使从错误中恢复更加容易(请参阅 “[不可变事件的优点](/ch12#不可变事件的优点)”)
@ -645,7 +645,7 @@ ACID 事务通常既提供及时性(例如线性一致性)也提供完整性
例如,这种系统可以使用多领导者配置运维,跨越多个数据中心,在区域间异步复制。任何一个数据中心都可以持续独立运行,因为不需要同步的跨区域协调。这样的系统的及时性保证会很弱 —— 如果不引入协调它是不可能是线性一致的 —— 但它仍然可以提供有力的完整性保证。 例如,这种系统可以使用多领导者配置运维,跨越多个数据中心,在区域间异步复制。任何一个数据中心都可以持续独立运行,因为不需要同步的跨区域协调。这样的系统的及时性保证会很弱 —— 如果不引入协调它是不可能是线性一致的 —— 但它仍然可以提供有力的完整性保证。
在这种情况下,可串行化事务作为维护生状态的一部分仍然是有用的但它们只能在小范围内运行在那里它们工作得很好【8】。异构分布式事务如 XA 事务,请参阅 “[实践中的分布式事务](/ch9#实践中的分布式事务)”不是必需的。同步协调仍然可以在需要的地方引入例如在无法恢复的操作之前强制执行严格的约束但是如果只是应用的一小部分地方需要它没必要让所有操作都付出协调的代价。【43】。 在这种情况下,可串行化事务作为维护生状态的一部分仍然是有用的但它们只能在小范围内运行在那里它们工作得很好【8】。异构分布式事务如 XA 事务,请参阅 “[实践中的分布式事务](/ch9#实践中的分布式事务)”不是必需的。同步协调仍然可以在需要的地方引入例如在无法恢复的操作之前强制执行严格的约束但是如果只是应用的一小部分地方需要它没必要让所有操作都付出协调的代价。【43】。
另一种审视协调与约束的角度是:它们减少了由于不一致而必须做出的道歉数量,但也可能会降低系统的性能和可用性,从而可能增加由于宕机中断而需要做出的道歉数量。你不可能将道歉数量减少到零,但可以根据自己的需求寻找最佳平衡点 —— 既不存在太多不一致性,又不存在太多可用性问题。 另一种审视协调与约束的角度是:它们减少了由于不一致而必须做出的道歉数量,但也可能会降低系统的性能和可用性,从而可能增加由于宕机中断而需要做出的道歉数量。你不可能将道歉数量减少到零,但可以根据自己的需求寻找最佳平衡点 —— 既不存在太多不一致性,又不存在太多可用性问题。
@ -693,9 +693,9 @@ ACID 意义下的一致性(请参阅 “[一致性](/ch7#一致性)”)基
如果一个事务在一个数据库中改变了多个对象,在这一事实发生后,很难说清这个事务到底意味着什么。即使你捕获了事务日志(请参阅 “[变更数据捕获](/ch12#变更数据捕获)”),各种表中的插入、更新和删除操作并不一定能清楚地表明 **为什么** 要执行这些变更。决定这些变更的是应用逻辑中的调用,而这一应用逻辑稍纵即逝,无法重现。 如果一个事务在一个数据库中改变了多个对象,在这一事实发生后,很难说清这个事务到底意味着什么。即使你捕获了事务日志(请参阅 “[变更数据捕获](/ch12#变更数据捕获)”),各种表中的插入、更新和删除操作并不一定能清楚地表明 **为什么** 要执行这些变更。决定这些变更的是应用逻辑中的调用,而这一应用逻辑稍纵即逝,无法重现。
相比之下,基于事件的系统可以提供更好的可审计性。在事件溯源方法中,系统的用户输入被表示为一个单一不可变事件,而任何其导致的状态变更都衍生自该事件。衍生可以实现为具有确定性与可重复性,因而相同的事件日志通过相同版本的衍生代码时,会导致相同的状态变更。 相比之下,基于事件的系统可以提供更好的可审计性。在事件溯源方法中,系统的用户输入被表示为一个单一不可变事件,而任何其导致的状态变更都派生自该事件。派生可以实现为具有确定性与可重复性,因而相同的事件日志通过相同版本的派生代码时,会导致相同的状态变更。
显式处理数据流(请参阅 “[批处理输出的哲学](/ch11#批处理输出的哲学)”)可以使数据的 **来龙去脉provenance** 更加清晰,从而使完整性检查更具可行性。对于事件日志,我们可以使用散列来检查事件存储没有被破坏。对于任何衍生状态,我们可以重新运行从事件日志中衍生它的批处理器与流处理器,以检查是否获得相同的结果,或者,甚至并行运行冗余的生流程。 显式处理数据流(请参阅 “[批处理输出的哲学](/ch11#批处理输出的哲学)”)可以使数据的 **来龙去脉provenance** 更加清晰,从而使完整性检查更具可行性。对于事件日志,我们可以使用散列来检查事件存储没有被破坏。对于任何派生状态,我们可以重新运行从事件日志中派生它的批处理器与流处理器,以检查是否获得相同的结果,或者,甚至并行运行冗余的生流程。
具有确定性且定义良好的数据流,也使调试与跟踪系统的执行变得容易,以便确定它 **为什么** 做了某些事情【4,69】。如果出现意想之外的事情那么重现导致意外事件的确切事故现场的诊断能力 —— 一种时间旅行调试功能是非常有价值的。 具有确定性且定义良好的数据流,也使调试与跟踪系统的执行变得容易,以便确定它 **为什么** 做了某些事情【4,69】。如果出现意想之外的事情那么重现导致意外事件的确切事故现场的诊断能力 —— 一种时间旅行调试功能是非常有价值的。
@ -875,13 +875,13 @@ ACID 意义下的一致性(请参阅 “[一致性](/ch7#一致性)”)基
在本章中,我们讨论了设计数据系统的新方式,而且也包括了我的个人观点,以及对未来的猜测。我们从这样一种观察开始:没有单种工具能高效服务所有可能的用例,因此应用必须组合使用几种不同的软件才能实现其目标。我们讨论了如何使用批处理与事件流来解决这一 **数据集成data integration** 问题,以便让数据变更在不同系统之间流动。 在本章中,我们讨论了设计数据系统的新方式,而且也包括了我的个人观点,以及对未来的猜测。我们从这样一种观察开始:没有单种工具能高效服务所有可能的用例,因此应用必须组合使用几种不同的软件才能实现其目标。我们讨论了如何使用批处理与事件流来解决这一 **数据集成data integration** 问题,以便让数据变更在不同系统之间流动。
在这种方法中,某些系统被指定为记录系统,而其他数据则通过转换生自记录系统。通过这种方式,我们可以维护索引、物化视图、机器学习模型、统计摘要等等。通过使这些生和转换操作异步且松散耦合,能够防止一个区域中的问题扩散到系统中不相关部分,从而增加整个系统的稳健性与容错性。 在这种方法中,某些系统被指定为记录系统,而其他数据则通过转换生自记录系统。通过这种方式,我们可以维护索引、物化视图、机器学习模型、统计摘要等等。通过使这些生和转换操作异步且松散耦合,能够防止一个区域中的问题扩散到系统中不相关部分,从而增加整个系统的稳健性与容错性。
将数据流表示为从一个数据集到另一个数据集的转换也有助于演化应用程序:如果你想变更其中一个处理步骤,例如变更索引或缓存的结构,则可以在整个输入数据集上重新运行新的转换代码,以便重新生输出。同样,出现问题时,你也可以修复代码并重新处理数据以便恢复。 将数据流表示为从一个数据集到另一个数据集的转换也有助于演化应用程序:如果你想变更其中一个处理步骤,例如变更索引或缓存的结构,则可以在整个输入数据集上重新运行新的转换代码,以便重新生输出。同样,出现问题时,你也可以修复代码并重新处理数据以便恢复。
这些过程与数据库内部已经完成的过程非常类似,因此我们将数据流应用的概念重新改写为,**分拆unbundling** 数据库组件,并通过组合这些松散耦合的组件来构建应用程序。 这些过程与数据库内部已经完成的过程非常类似,因此我们将数据流应用的概念重新改写为,**分拆unbundling** 数据库组件,并通过组合这些松散耦合的组件来构建应用程序。
衍生状态可以通过观察底层数据的变更来更新。此外,衍生状态本身可以进一步被下游消费者观察。我们甚至可以将这种数据流一路传送至显示数据的终端用户设备,从而构建可动态更新以反映数据变更,并在离线时能继续工作的用户界面。 派生状态可以通过观察底层数据的变更来更新。此外,派生状态本身可以进一步被下游消费者观察。我们甚至可以将这种数据流一路传送至显示数据的终端用户设备,从而构建可动态更新以反映数据变更,并在离线时能继续工作的用户界面。
接下来,我们讨论了如何确保所有这些处理在出现故障时保持正确。我们看到可伸缩的强完整性保证可以通过异步事件处理来实现,通过使用端到端操作标识符使操作幂等,以及通过异步检查约束。客户端可以等到检查通过,或者不等待继续前进,但是可能会冒有违反约束需要道歉的风险。这种方法比使用分布式事务的传统方法更具可伸缩性与可靠性,并且在实践中适用于很多业务流程。 接下来,我们讨论了如何确保所有这些处理在出现故障时保持正确。我们看到可伸缩的强完整性保证可以通过异步事件处理来实现,通过使用端到端操作标识符使操作幂等,以及通过异步检查约束。客户端可以等到检查通过,或者不等待继续前进,但是可能会冒有违反约束需要道歉的风险。这种方法比使用分布式事务的传统方法更具可伸缩性与可靠性,并且在实践中适用于很多业务流程。

View File

@ -11,18 +11,20 @@ breadcrumbs: false
> [艾伦・凯](https://www.drdobbs.com/architecture-and-design/interview-with-alan-kay/240003442) > [艾伦・凯](https://www.drdobbs.com/architecture-and-design/interview-with-alan-kay/240003442)
> 在接受 *Dr Dobb's Journal* 采访时2012 年) > 在接受 *Dr Dobb's Journal* 采访时2012 年)
如果你正在构建一个应用程序,你将会被一系列需求所驱动。在你的需求列表中,最重要的可能是应用程序必须提供的功能:需要哪些界面和按钮,以及每个操作应该做什么,以实现软件的目的。这些是你的 *功能性需求* 如果你正在构建一个应用程序,你将会被一系列需求所驱动。在你的需求列表中,最重要的可能是应用程序必须提供的功能:需要哪些界面和按钮,以及每个操作应该做什么,以实现软件的目的。这些是你的 ***功能性需求***
此外,你可能还有一些 *非功能性需求*:例如,应用程序应该快速、可靠、安全、合规,并且易于维护。这些需求可能没有明确写下来,因为它们看起来有些显而易见,但它们与应用程序的功能同样重要:一个慢得让人无法忍受或不可靠的应用程序还不如不存在。 此外,你可能还有一些 ***非功能性需求***:例如,应用程序应该快速、可靠、安全、合规,并且易于维护。
这些需求可能没有明确写下来,因为它们看起来有些显而易见,但它们与应用程序的功能同样重要:一个慢得让人无法忍受或不可靠的应用程序还不如不存在。
许多非功能性需求,比如安全性,超出了本书的范围。但我们将考虑一些非功能性需求,本章将帮助你为自己的系统阐明它们: 许多非功能性需求,比如安全性,超出了本书的范围。但我们将考虑一些非功能性需求,本章将帮助你为自己的系统阐明它们:
* 如何定义和衡量系统的 *性能*(参见 ["描述性能"](/ch2#sec_introduction_percentiles) * 如何定义和衡量系统的 **性能**(参见 ["描述性能"](/ch2#sec_introduction_percentiles)
* 服务 *可靠* 意味着什么——即即使出现问题也能继续正确工作(参见 ["可靠性与容错"](/ch2#sec_introduction_reliability) * 服务 **可靠** 意味着什么——即即使出现问题也能继续正确工作(参见 ["可靠性与容错"](/ch2#sec_introduction_reliability)
* 通过在系统负载增长时添加计算能力的有效方法,使系统具有 *可伸缩性*(参见 ["可伸缩性"](/ch2#sec_introduction_scalability));以及 * 通过在系统负载增长时添加计算能力的有效方法,使系统具有 **可伸缩性**(参见 ["可伸缩性"](/ch2#sec_introduction_scalability));以及
* 使系统长期更易于维护(参见 ["可运维性"](/ch2#sec_introduction_maintainability))。 * 使系统长期更 **易于维护**(参见 ["可运维性"](/ch2#sec_introduction_maintainability))。
本章介绍的术语在后续章节中也很有用,当我们深入研究数据密集型系统的实现细节时。然而,抽象定义可能相当枯燥;为了使这些想法更具体,我们将从一个案例研究开始本章,研究社交网络服务可能如何工作,这将提供性能和可伸缩性的实际案例。
本章介绍的术语在后续章节中也很有用,当我们深入研究数据密集型系统的实现细节时。然而,抽象定义可能相当枯燥;为了使这些想法更具体,我们将从一个案例研究开始本章,研究社交网络服务可能如何工作,这将提供性能和可伸缩性的实际示例。
## 案例研究:社交网络首页时间线 {#sec_introduction_twitter} ## 案例研究:社交网络首页时间线 {#sec_introduction_twitter}
@ -59,7 +61,7 @@ SELECT posts.*, users.* FROM posts
想象一下,我们为每个用户存储一个包含其首页时间线的数据结构,即他们关注的人的最近帖子。每次用户发布帖子时,我们查找他们的所有粉丝,并将该帖子插入到每个粉丝的首页时间线中——就像向邮箱投递消息一样。现在当用户登录时,我们可以简单地给他们这个预先计算的首页时间线。此外,要接收时间线上任何新帖子的通知,用户的客户端只需订阅添加到其首页时间线的帖子流。 想象一下,我们为每个用户存储一个包含其首页时间线的数据结构,即他们关注的人的最近帖子。每次用户发布帖子时,我们查找他们的所有粉丝,并将该帖子插入到每个粉丝的首页时间线中——就像向邮箱投递消息一样。现在当用户登录时,我们可以简单地给他们这个预先计算的首页时间线。此外,要接收时间线上任何新帖子的通知,用户的客户端只需订阅添加到其首页时间线的帖子流。
这种方法的缺点是,现在每次用户发布帖子时我们需要做更多的工作,因为首页时间线是需要更新的生数据。该过程如 [图 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" >}}
@ -245,7 +247,7 @@ Akamai 最近的一项研究 [^24] 声称响应时间增加 100 毫秒将电子
将这些问题标记为"人为错误"并希望通过更严格的程序和规则合规性来更好地控制人类行为来解决它们是很诱人的。然而,责怪人们的错误是适得其反的。我们所说的"人为错误"实际上不是事件的原因,而是人们在社会技术系统中尽力做好工作的问题的症状 [^71]。通常,复杂系统具有紧急行为,组件之间的意外交互也可能导致故障 [^72]。 将这些问题标记为"人为错误"并希望通过更严格的程序和规则合规性来更好地控制人类行为来解决它们是很诱人的。然而,责怪人们的错误是适得其反的。我们所说的"人为错误"实际上不是事件的原因,而是人们在社会技术系统中尽力做好工作的问题的症状 [^71]。通常,复杂系统具有紧急行为,组件之间的意外交互也可能导致故障 [^72]。
各种技术措施可以帮助最小化人为错误的影响,包括彻底测试(手写测试和对大量随机输入的 *属性测试*[^38]、快速回滚配置更改的回滚机制、新代码的逐步推出、详细和清晰的监控、用于诊断生产问题的可观性工具(参见 ["分布式系统的问题"](/ch1#sec_introduction_dist_sys_problems)),以及鼓励"正确的事情"并阻止"错误的事情"的精心设计的界面。 各种技术措施可以帮助最小化人为错误的影响,包括彻底测试(手写测试和对大量随机输入的 *属性测试*[^38]、快速回滚配置更改的回滚机制、新代码的逐步推出、详细和清晰的监控、用于诊断生产问题的可观性工具(参见 ["分布式系统的问题"](/ch1#sec_introduction_dist_sys_problems)),以及鼓励"正确的事情"并阻止"错误的事情"的精心设计的界面。
然而,这些事情需要时间和金钱的投资,在日常业务的务实现实中,组织通常优先考虑创收活动而不是增加其抵御错误的韧性的措施。如果在更多功能和更多测试之间有选择,许多组织可以理解地选择功能。鉴于这种选择,当可预防的错误不可避免地发生时,责怪犯错误的人是没有意义的——问题是组织的优先事项。 然而,这些事情需要时间和金钱的投资,在日常业务的务实现实中,组织通常优先考虑创收活动而不是增加其抵御错误的韧性的措施。如果在更多功能和更多测试之间有选择,许多组织可以理解地选择功能。鉴于这种选择,当可预防的错误不可避免地发生时,责怪犯错误的人是没有意义的——问题是组织的优先事项。
@ -332,26 +334,30 @@ Akamai 最近的一项研究 [^24] 声称响应时间增加 100 毫秒将电子
如果我们今天创建的每个系统都足够有价值以长期生存,它有一天将成为遗留系统。为了最小化需要维护我们软件的未来几代人的痛苦,我们应该在设计时考虑维护问题。尽管我们不能总是预测哪些决定可能会在未来造成维护难题,但在本书中,我们将注意几个广泛适用的原则: 如果我们今天创建的每个系统都足够有价值以长期生存,它有一天将成为遗留系统。为了最小化需要维护我们软件的未来几代人的痛苦,我们应该在设计时考虑维护问题。尽管我们不能总是预测哪些决定可能会在未来造成维护难题,但在本书中,我们将注意几个广泛适用的原则:
操作性 运维性Operability
: 使组织容易保持系统平稳运行。 : 使组织容易保持系统平稳运行。
简单性 简单性Simplicity
: 通过使用易于理解、一致的模式和结构来实施它,并避免不必要的复杂性,使新工程师容易理解系统。 : 通过使用易于理解、一致的模式和结构来实施它,并避免不必要的复杂性,使新工程师容易理解系统。
可演化性 可演化性Evolvability
: 使工程师将来容易对系统进行更改,随着需求变化而适应和扩展它以用于未预料的用例。 : 使工程师将来容易对系统进行更改,随着需求变化而适应和扩展它以用于未预料的用例。
### 可运维性:让运维更轻松 {#id37} ### 可运维性:让运维更轻松 {#id37}
我们之前在 ["云时代的运维"](/ch1#sec_introduction_operations) 中讨论了运维的角色,我们看到人类流程对于可靠运维至少与软件工具一样重要。事实上,有人建议"良好的运维通常可以解决糟糕(或不完整)软件的局限性,但良好的软件无法与糟糕的运维可靠地运行" [^60]。 我们之前在 ["云时代的运维"](/ch1#sec_introduction_operations) 中讨论了运维的角色,我们看到人类流程对于可靠运维至少与软件工具一样重要。
事实上,有人提出 “良好的运维通常可以解决糟糕(或不完整)软件的局限性,但再好的软件碰上糟糕的运维也难以可靠地运行” [^60]。
在由数千台机器组成的大规模系统中,手动维护将是不合理地昂贵的,自动化是必不可少的。然而,自动化可能是一把双刃剑:总会有边缘情况(如罕见的故障场景)需要运维团队的手动干预。由于无法自动处理的情况是最复杂的问题,更大的自动化需要一个 *更* 熟练的运维团队来解决这些问题 [^88]。 在由数千台机器组成的大规模系统中,手动维护将是不合理地昂贵的,自动化是必不可少的。然而,自动化可能是一把双刃剑:
总会有边缘情况(如罕见的故障场景)需要运维团队的手动干预。由于无法自动处理的情况是最复杂的问题,更大的自动化需要一个 **更** 熟练的运维团队来解决这些问题 [^88]。
此外,如果自动化系统出错,通常比依赖操作员手动执行某些操作的系统更难排除故障。出于这个原因,更多的自动化并不总是对可操作性更好。然而,一定程度的自动化很重要,最佳点将取决于你特定应用程序和组织的细节。 此外,如果自动化系统出错,通常比依赖操作员手动执行某些操作的系统更难排除故障。出于这个原因,更多的自动化并不总是对可操作性更好。
然而,一定程度的自动化很重要,最佳点将取决于你特定应用程序和组织的细节。
良好的可操作性意味着使常规任务变得容易,使运维团队能够将精力集中在高价值活动上。数据系统可以做各种事情来使常规任务变得容易,包括 [^89] 良好的可操作性意味着使常规任务变得容易,使运维团队能够将精力集中在高价值活动上。
数据系统可以做各种事情来使常规任务变得容易,包括 [^89]
* 允许监控工具检查系统的关键指标,并支持可观性工具(参见 ["分布式系统的问题"](/ch1#sec_introduction_dist_sys_problems))以深入了解系统的运行时行为。各种商业和开源工具可以在这里提供帮助 [^90]。 * 允许监控工具检查系统的关键指标,并支持可观性工具(参见 ["分布式系统的问题"](/ch1#sec_introduction_dist_sys_problems))以深入了解系统的运行时行为。各种商业和开源工具可以在这里提供帮助 [^90]。
* 避免对单个机器的依赖(允许在系统整体继续不间断运行的同时关闭机器进行维护) * 避免对单个机器的依赖(允许在系统整体继续不间断运行的同时关闭机器进行维护)
* 提供良好的文档和易于理解的操作模型("如果我做 XY 将会发生" * 提供良好的文档和易于理解的操作模型("如果我做 XY 将会发生"
* 提供良好的默认行为,但也给管理员在需要时覆盖默认值的自由 * 提供良好的默认行为,但也给管理员在需要时覆盖默认值的自由
@ -360,39 +366,59 @@ Akamai 最近的一项研究 [^24] 声称响应时间增加 100 毫秒将电子
### 简单性:管理复杂度 {#id38} ### 简单性:管理复杂度 {#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} ### 可演化性:让变化更容易 {#sec_introduction_evolvability}
你的系统需求将保持不变的可能性极小。它们更可能处于不断变化中:你学习新事实、以前未预料的用例出现、业务优先级发生变化、用户请求新功能、新平台取代旧平台、法律或监管要求发生变化、系统增长迫使架构变化等。 你的系统需求将保持不变的可能性极小。它们更可能处于不断变化中:
你学习新事实、以前未预料的用例出现、业务优先级发生变化、用户请求新功能、
新平台取代旧平台、法律或监管要求发生变化、系统增长迫使架构变化等。
在组织流程方面,*敏捷* 工作模式为适应变化提供了框架。敏捷社区还开发了在频繁变化的环境中开发软件时有用的技术工具和流程例如测试驱动开发TDD和重构。在本书中我们搜索在由具有不同特征的几个不同应用程序或服务组成的系统级别增加敏捷性的方法。 在组织流程方面,*敏捷* 工作模式为适应变化提供了框架。敏捷社区还开发了在频繁变化的环境中开发软件时有用的技术工具和流程,
例如测试驱动开发TDD和重构。在本书中我们搜索在由具有不同特征的几个不同应用程序或服务组成的系统级别增加敏捷性的方法。
你可以修改数据系统并使其适应不断变化的需求的容易程度与其简单性及其抽象密切相关:松散耦合、简单系统通常比紧密耦合、复杂系统更容易修改。由于这是一个如此重要的想法,我们将使用不同的词来指代数据系统级别的敏捷性:*可演化性* [^97]。 你可以修改数据系统并使其适应不断变化的需求的容易程度与其简单性及其抽象密切相关:松散耦合、简单系统通常比紧密耦合、复杂系统更容易修改。
由于这是一个如此重要的想法,我们将使用不同的词来指代数据系统级别的敏捷性:*可演化性* [^97]。
使大型系统中的变化困难的一个主要因素是某些操作不可逆,因此需要非常谨慎地采取该操作 [^98]。例如,假设你正在从一个数据库迁移到另一个数据库:如果在新数据库出现问题时无法切换回旧系统,风险就会高得多,而如果你可以轻松返回。最小化不可逆性提高了灵活性。 使大型系统中的变化困难的一个主要因素是某些操作不可逆,因此需要非常谨慎地采取该操作 [^98]。
例如,假设你正在从一个数据库迁移到另一个数据库:如果在新数据库出现问题时无法切换回旧系统,风险就会高得多,而如果你可以轻松返回。最小化不可逆性提高了灵活性。
## 总结 {#summary} ## 总结 {#summary}
在本章中,我们研究了几个非功能性需求的例子:性能、可靠性、可伸缩性和可维护性。通过这些主题,我们还遇到了我们在本书其余部分需要的原则和术语。我们从社交网络中首页时间线如何实现的案例研究开始,这说明了规模上出现的一些挑战。 在本章中,我们研究了几个非功能性需求的例子:性能、可靠性、可伸缩性和可维护性。
通过这些主题,我们还遇到了我们在本书其余部分需要的原则和术语。我们从社交网络中首页时间线如何实现的案例研究开始,这说明了规模上出现的一些挑战。
我们讨论了如何衡量性能(例如,使用响应时间百分位数)、系统上的负载(例如,使用吞吐量指标),以及它们如何在 SLA 中使用。可伸缩性是一个密切相关的概念:即,在负载增长时确保性能保持不变。我们看到了可伸缩性的一些一般原则,例如将任务分解为可以独立运行的较小部分,我们将在以下章节中深入研究可伸缩性技术的技术细节。 我们讨论了如何衡量性能(例如,使用响应时间百分位数)、系统上的负载(例如,使用吞吐量指标),以及它们如何在 SLA 中使用。
可伸缩性是一个密切相关的概念:即,在负载增长时确保性能保持不变。我们看到了可伸缩性的一些一般原则,例如将任务分解为可以独立运行的较小部分,我们将在以下章节中深入研究可伸缩性技术的技术细节。
为了实现可靠性,你可以使用容错技术,即使某个组件(例如,磁盘、机器或其他服务)出现故障,系统也可以继续提供其服务。我们看到了可能发生的硬件故障的例子,并将它们与软件故障区分开来,软件故障可能更难处理,因为它们通常是强相关的。实现可靠性的另一个方面是建立对人类犯错误的韧性,我们看到无责备事后分析作为从事件中学习的技术。 为了实现可靠性,你可以使用容错技术,即使某个组件(例如,磁盘、机器或其他服务)出现故障,系统也可以继续提供其服务。
我们看到了可能发生的硬件故障的例子,并将它们与软件故障区分开来,软件故障可能更难处理,因为它们通常是强相关的。
实现可靠性的另一个方面是建立对人类犯错误的韧性,我们看到无责备事后分析作为从事件中学习的技术。
最后,我们研究了可维护性的几个方面,包括支持运维团队的工作、管理复杂性以及随着时间的推移使应用程序功能易于演化。实现这些目标没有简单的答案,但有一件事可以帮助,那就是使用提供有用抽象的易于理解的构建块来构建应用程序。本书的其余部分将涵盖一系列在实践中被证明有价值的构建块。 最后,我们研究了可维护性的几个方面,包括支持运维团队的工作、管理复杂性以及随着时间的推移使应用程序功能易于演化。
实现这些目标没有简单的答案,但有一件事可以帮助,那就是使用提供有用抽象的易于理解的构建块来构建应用程序。本书的其余部分将涵盖一系列在实践中被证明有价值的构建块。
### 参考 {#参考} ### 参考 {#参考}

View File

@ -10,9 +10,9 @@ breadcrumbs: false
> >
> 路德维希・维特根斯坦《逻辑哲学论》1922 > 路德维希・维特根斯坦《逻辑哲学论》1922
数据模型或许是开发软件最重要的部分,因为它们有着深远的影响:不仅影响软件的编写方式,还影响我们 *思考问题* 的方式。 数据模型或许是开发软件最重要的部分,因为它们有着深远的影响:不仅影响软件的编写方式,还影响我们 **思考问题** 的方式。
大多数应用程序都是通过层层叠加的数据模型来构建的。对于每一层来说,关键问题是:如何用更低层次的数据模型来 *表示* 它?例如: 大多数应用程序都是通过层层叠加的数据模型来构建的。对于每一层来说的关键问题是:如何用更低层次的数据模型来 **表示** 它?例如:
1. 作为应用程序开发者,你观察现实世界(其中有人员、组织、货物、行为、资金流动、传感器等),并用对象或数据结构,以及操作这些数据结构的 API 来建模。这些结构通常是特定于应用程序的。 1. 作为应用程序开发者,你观察现实世界(其中有人员、组织、货物、行为、资金流动、传感器等),并用对象或数据结构,以及操作这些数据结构的 API 来建模。这些结构通常是特定于应用程序的。
2. 当你想要存储这些数据结构时,你用通用的数据模型来表达它们,例如 JSON 或 XML 文档、关系数据库中的表,或者图中的顶点和边。这些数据模型是本章的主题。 2. 当你想要存储这些数据结构时,你用通用的数据模型来表达它们,例如 JSON 或 XML 文档、关系数据库中的表,或者图中的顶点和边。这些数据模型是本章的主题。
@ -26,26 +26,42 @@ breadcrumbs: false
-------- --------
> [!TIP] 术语:声明式查询语言 > [!TIP] 术语:声明式查询语言
>
本章中的许多查询语言(如 SQL、Cypher、SPARQL 或 Datalog都是 *声明式* 的,这意味着你指定所需数据的模式 —— 结果必须满足什么条件,以及你希望如何转换数据(例如,排序、分组和聚合)—— 但不指定 *如何* 实现该目标。数据库系统的查询优化器可以决定使用哪些索引和哪些连接算法,以及以什么顺序执行查询的各个部分。 > 本章中的许多查询语言(如 SQL、Cypher、SPARQL 或 Datalog都是 **声明式** 的,这意味着你指定所需数据的模式 ——
> 结果必须满足什么条件,以及你希望如何转换数据(例如,排序、分组和聚合)—— 但不指定 **如何** 实现该目标。
相比之下,使用大多数编程语言,你必须编写一个 *算法* —— 即告诉计算机以什么顺序执行哪些操作。声明式查询语言很有吸引力,因为它通常更简洁,比显式算法更容易编写。但更重要的是,它还隐藏了查询引擎的实现细节,这使得数据库系统可以在不需要更改任何查询的情况下引入性能改进 [^1]。 > 数据库系统的查询优化器可以决定使用哪些索引和哪些连接算法,以及以什么顺序执行查询的各个部分。
>
例如,数据库可能能够跨多个 CPU 核心和机器并行执行声明式查询,而你无需担心如何实现该并行性 [^2]。在手写算法中,实现这种并行执行将需要大量的工作。 > 相比之下,使用大多数编程语言,你必须编写一个 **算法** —— 即告诉计算机以什么顺序执行哪些操作。
> 声明式查询语言很有吸引力,因为它通常更简洁,比显式算法更容易编写。
> 但更重要的是,它还隐藏了查询引擎的实现细节,这使得数据库系统可以在不需要更改任何查询的情况下引入性能改进 [^1]。
>
> 例如,数据库可能能够跨多个 CPU 核心和机器并行执行声明式查询,而你无需担心如何实现该并行性 [^2]。
> 在手写算法中,实现这种并行执行将需要大量的工作。
-------- --------
## 关系模型与文档模型 {#sec_datamodels_history} ## 关系模型与文档模型 {#sec_datamodels_history}
今天最广为人知的数据模型可能是 SQL它基于 Edgar Codd 在 1970 年提出的关系模型 [^3]:数据被组织成 *关系*(在 SQL 中称为 *表*),其中每个关系是 *元组*(在 SQL 中称为 *行*)的无序集合。 今天最广为人知的数据模型可能是 SQL它基于 Edgar Codd 在 1970 年提出的关系模型 [^3]
数据被组织成 **关系**(在 SQL 中称为 **表**),其中每个关系是 **元组**(在 SQL 中称为 **行**)的无序集合。
关系模型最初是一个理论提议,当时许多人怀疑它是否能够高效实现。然而,到 20 世纪 80 年代中期关系数据库管理系统RDBMS和 SQL 已成为大多数需要存储和查询具有某种规则结构的数据的人的首选工具。许多数据管理用例在几十年后仍然由关系数据主导 —— 例如,商业分析(参见 ["星型与雪花型:分析模式"](/ch3#sec_datamodels_analytics))。 关系模型最初是一个理论提议,当时许多人怀疑它是否能够高效实现。
然而,到 20 世纪 80 年代中期关系数据库管理系统RDBMS和 SQL 已成为大多数需要存储和查询具有某种规则结构的数据的人的首选工具。
许多数据管理用例在几十年后仍然由关系数据主导 —— 例如,商业分析(参见 ["星型与雪花型:分析模式"](/ch3#sec_datamodels_analytics))。
多年来,出现了许多与数据存储和查询相关的竞争方法。在 20 世纪 70 年代和 80 年代初,*网状模型* 和 *层次模型* 是主要的替代方案,但关系模型最终战胜了它们。对象数据库在 20 世纪 80 年代末和 90 年代初出现又消失。XML 数据库在 21 世纪初出现,但只获得了小众的采用。每个关系模型的竞争者在其时代都产生了大量的炒作,但都没有持续下去 [^4]。相反SQL 已经发展到在其关系核心之外纳入其他数据类型 —— 例如,增加了对 XML、JSON 和图数据的支持 [^5]。 多年来,出现了许多与数据存储和查询相关的竞争方法。在 20 世纪 70 年代和 80 年代初,**网状模型** 和 **层次模型** 是主要的替代方案,但关系模型最终战胜了它们。
对象数据库在 20 世纪 80 年代末和 90 年代初出现又消失。XML 数据库在 21 世纪初出现,但只获得了小众的采用。
每个关系模型的竞争者在其时代都产生了大量的炒作,但都没有持续下去 [^4]。
相反SQL 已经发展到在其关系核心之外纳入其他数据类型 —— 例如,增加了对 XML、JSON 和图数据的支持 [^5]。
在 2010 年代,*NoSQL* 是试图推翻关系数据库主导地位的最新流行词。NoSQL 指的不是单一技术,而是围绕新数据模型、模式灵活性、可扩展性以及向开源许可模式转变的一系列松散的想法。一些数据库将自己标榜为 *NewSQL*,因为它们旨在提供 NoSQL 系统的可扩展性以及传统关系数据库的数据模型和事务保证。NoSQL 和 NewSQL 的想法在数据系统设计中产生了很大的影响,但随着这些原则被广泛采用,这些术语的使用已经减少。 在 2010 年代,**NoSQL** 是试图推翻关系数据库主导地位的最新流行词。
NoSQL 指的不是单一技术,而是围绕新数据模型、模式灵活性、可伸缩性以及向开源许可模式转变的一系列松散的想法。
一些数据库将自己标榜为 *NewSQL*,因为它们旨在提供 NoSQL 系统的可伸缩性以及传统关系数据库的数据模型和事务保证。
NoSQL 和 NewSQL 的想法在数据系统设计中产生了很大的影响,但随着这些原则被广泛采用,这些术语的使用已经减少。
NoSQL 运动的一个持久影响是 *文档模型* 的流行,它通常将数据表示为 JSON。这个模型最初由专门的文档数据库如 MongoDB 和 Couchbase推广尽管大多数关系数据库现在也增加了 JSON 支持。与通常被视为具有严格和不灵活模式的关系表相比JSON 文档被认为更加灵活。 NoSQL 运动的一个持久影响是 **文档模型** 的流行,它通常将数据表示为 JSON。
这个模型最初由专门的文档数据库(如 MongoDB 和 Couchbase推广尽管大多数关系数据库现在也增加了 JSON 支持。
与通常被视为具有严格和不灵活模式的关系表相比JSON 文档被认为更加灵活。
文档和关系数据的优缺点已经被广泛讨论;让我们来看看该辩论的一些关键点。 文档和关系数据的优缺点已经被广泛讨论;让我们来看看该辩论的一些关键点。
@ -174,7 +190,7 @@ db.users.aggregate([
* 在反规范化表示中,我们会在每个人的个人资料中包含标志的图像 URL这使得 JSON 文档自包含,但如果我们需要更改标志,就会产生麻烦,因为我们现在需要找到旧 URL 的所有出现并更新它们 [^9]。 * 在反规范化表示中,我们会在每个人的个人资料中包含标志的图像 URL这使得 JSON 文档自包含,但如果我们需要更改标志,就会产生麻烦,因为我们现在需要找到旧 URL 的所有出现并更新它们 [^9]。
* 在规范化表示中,我们将创建一个代表组织或学校的实体,并在该实体上存储其名称、标志 URL 以及可能的其他属性(描述、新闻提要等)一次。然后,每个提到该组织的简历都会简单地引用其 ID更新标志很容易。 * 在规范化表示中,我们将创建一个代表组织或学校的实体,并在该实体上存储其名称、标志 URL 以及可能的其他属性(描述、新闻提要等)一次。然后,每个提到该组织的简历都会简单地引用其 ID更新标志很容易。
作为一般原则,规范化数据通常写入更快(因为只有一个副本),但查询更慢(因为它需要连接);反规范化数据通常读取更快(连接更少),但写入更昂贵(更多副本要更新,使用更多磁盘空间)。你可能会发现将反规范化视为衍生数据的一种形式很有帮助(["记录系统和衍生数据"](/ch1#sec_introduction_derived)),因为你需要设置一个过程来更新数据的冗余副本。 作为一般原则,规范化数据通常写入更快(因为只有一个副本),但查询更慢(因为它需要连接);反规范化数据通常读取更快(连接更少),但写入更昂贵(更多副本要更新,使用更多磁盘空间)。你可能会发现将反规范化视为派生数据的一种形式很有帮助(["记录系统和派生数据"](/ch1#sec_introduction_derived)),因为你需要设置一个过程来更新数据的冗余副本。
除了执行所有这些更新的成本之外,如果进程在进行更新的过程中崩溃,你还需要考虑数据库的一致性。提供原子事务的数据库(参见 ["原子性"](/ch8#sec_transactions_acid_atomicity))使保持一致性变得更容易,但并非所有数据库都在多个文档之间提供原子性。通过流处理确保一致性也是可能的,我们将在 [待补充链接] 中讨论。 除了执行所有这些更新的成本之外,如果进程在进行更新的过程中崩溃,你还需要考虑数据库的一致性。提供原子事务的数据库(参见 ["原子性"](/ch8#sec_transactions_acid_atomicity))使保持一致性变得更容易,但并非所有数据库都在多个文档之间提供原子性。通过流处理确保一致性也是可能的,我们将在 [待补充链接] 中讨论。
@ -469,7 +485,7 @@ CREATE INDEX edges_heads ON edges (head_vertex);
[示例 3-4](/ch3#fig_cypher_create) 显示了将 [图 3-6](/ch3#fig_datamodels_graph) 的左侧部分插入图数据库的 Cypher 查询。图的其余部分可以类似地添加。每个顶点都被赋予一个符号名称,如 `usa``idaho`。该名称不存储在数据库中,仅在查询内部使用以在顶点之间创建边,使用箭头符号:`(idaho) -[:WITHIN]-> (usa)` 创建一条标记为 `WITHIN` 的边,其中 `idaho` 作为尾节点,`usa` 作为头节点。 [示例 3-4](/ch3#fig_cypher_create) 显示了将 [图 3-6](/ch3#fig_datamodels_graph) 的左侧部分插入图数据库的 Cypher 查询。图的其余部分可以类似地添加。每个顶点都被赋予一个符号名称,如 `usa``idaho`。该名称不存储在数据库中,仅在查询内部使用以在顶点之间创建边,使用箭头符号:`(idaho) -[:WITHIN]-> (usa)` 创建一条标记为 `WITHIN` 的边,其中 `idaho` 作为尾节点,`usa` 作为头节点。
{{< figure id="fig_cypher_create" title="示例 3-4. [图 3-6](/ch3#fig_datamodels_graph) 中数据的子集,表示为 Cypher 查询" class="w-full my-4" >}} {{< figure link="#fig_datamodels_graph" id="fig_cypher_create" title="示例 3-4. 图 3-6 中数据的子集,表示为 Cypher 查询" class="w-full my-4" >}}
``` ```
CREATE CREATE
@ -519,7 +535,7 @@ RETURN person.name
自 SQL:1999 以来,查询中可变长度遍历路径的想法可以使用称为 *递归公用表表达式*`WITH RECURSIVE` 语法)的东西来表达。[示例 3-6](/ch3#fig_graph_sql_query) 显示了相同的查询 —— 查找从美国移民到欧洲的人的姓名 —— 使用此技术在 SQL 中表达。然而,与 Cypher 相比,语法非常笨拙。 自 SQL:1999 以来,查询中可变长度遍历路径的想法可以使用称为 *递归公用表表达式*`WITH RECURSIVE` 语法)的东西来表达。[示例 3-6](/ch3#fig_graph_sql_query) 显示了相同的查询 —— 查找从美国移民到欧洲的人的姓名 —— 使用此技术在 SQL 中表达。然而,与 Cypher 相比,语法非常笨拙。
{{< figure id="fig_graph_sql_query" title="示例 3-6. 与 [示例 3-5](/ch3#fig_cypher_query) 相同的查询,使用递归公用表表达式在 SQL 中编写" class="w-full my-4" >}} {{< figure link="#fig_cypher_query" id="fig_graph_sql_query" title="示例 3-6. 与 示例 3-5 相同的查询,使用递归公用表表达式在 SQL 中编写" class="w-full my-4" >}}
```sql ```sql
WITH RECURSIVE WITH RECURSIVE
@ -599,7 +615,7 @@ Oracle 对递归查询有不同的 SQL 扩展,它称之为 *层次* [^41]。
[示例 3-7](/ch3#fig_graph_n3_triples) 显示了与 [示例 3-4](/ch3#fig_cypher_create) 中相同的数据,以称为 *Turtle* 的格式编写为三元组,它是 *Notation3**N3*)的子集 [^48]。 [示例 3-7](/ch3#fig_graph_n3_triples) 显示了与 [示例 3-4](/ch3#fig_cypher_create) 中相同的数据,以称为 *Turtle* 的格式编写为三元组,它是 *Notation3**N3*)的子集 [^48]。
{{< figure id="fig_graph_n3_triples" title="示例 3-7. [图 3-6](/ch3#fig_datamodels_graph) 中数据的子集,表示为 Turtle 三元组" class="w-full my-4" >}} {{< figure link="#fig_datamodels_graph" id="fig_graph_n3_triples" title="示例 3-7. 图 3-6 中数据的子集,表示为 Turtle 三元组" class="w-full my-4" >}}
``` ```
@prefix : <urn:example:>. @prefix : <urn:example:>.
@ -623,7 +639,7 @@ _:namerica :type "continent".
一遍又一遍地重复相同的主语相当重复,但幸运的是,你可以使用分号来表达关于同一主语的多个内容。这使得 Turtle 格式非常易读:见 [示例 3-8](/ch3#fig_graph_n3_shorthand)。 一遍又一遍地重复相同的主语相当重复,但幸运的是,你可以使用分号来表达关于同一主语的多个内容。这使得 Turtle 格式非常易读:见 [示例 3-8](/ch3#fig_graph_n3_shorthand)。
{{< figure id="fig_graph_n3_shorthand" title="示例 3-8. 编写 [示例 3-7](/ch3#fig_graph_n3_triples) 中数据的更简洁方式" class="w-full my-4" >}} {{< figure link="#fig_graph_n3_triples" id="fig_graph_n3_shorthand" title="示例 3-8. 编写 示例 3-7 中数据的更简洁方式" class="w-full my-4" >}}
``` ```
@prefix : <urn:example:>. @prefix : <urn:example:>.
@ -647,7 +663,7 @@ _:namerica a :Location; :name "North America"; :type "continent".
我们在 [示例 3-8](/ch3#fig_graph_n3_shorthand) 中使用的 Turtle 语言实际上是在 *资源描述框架*RDF[^55] 中编码数据的一种方式这是为语义网设计的数据模型。RDF 数据也可以用其他方式编码,例如(更冗长地)用 XML如 [示例 3-9](/ch3#fig_graph_rdf_xml) 所示。像 Apache Jena 这样的工具可以在不同的 RDF 编码之间自动转换。 我们在 [示例 3-8](/ch3#fig_graph_n3_shorthand) 中使用的 Turtle 语言实际上是在 *资源描述框架*RDF[^55] 中编码数据的一种方式这是为语义网设计的数据模型。RDF 数据也可以用其他方式编码,例如(更冗长地)用 XML如 [示例 3-9](/ch3#fig_graph_rdf_xml) 所示。像 Apache Jena 这样的工具可以在不同的 RDF 编码之间自动转换。
{{< figure id="fig_graph_rdf_xml" title="示例 3-9. [示例 3-8](/ch3#fig_graph_n3_shorthand) 的数据,使用 RDF/XML 语法表示" class="w-full my-4" >}} {{< figure link="#fig_graph_n3_shorthand" id="fig_graph_rdf_xml" title="示例 3-9. 示例 3-8 的数据,使用 RDF/XML 语法表示" class="w-full my-4" >}}
```xml ```xml
<rdf:RDF xmlns="urn:example:" <rdf:RDF xmlns="urn:example:"
@ -817,7 +833,7 @@ query ChatApp {
[示例 3-14](/ch3#fig_graphql_response) 显示了对 [示例 3-13](/ch3#fig_graphql_query) 中查询的响应可能是什么样子。响应是一个反映查询结构的 JSON 文档:它正好包含请求的那些属性,不多也不少。这种方法的优点是服务器不需要知道客户端需要哪些属性来渲染用户界面;相反,客户端可以简单地请求它需要的内容。例如,此查询不会为 `replyTo` 消息的发送者请求个人资料图片 URL但如果用户界面更改为添加该个人资料图片客户端可以很容易地将所需的 `imageUrl` 属性添加到查询中,而无需更改服务器。 [示例 3-14](/ch3#fig_graphql_response) 显示了对 [示例 3-13](/ch3#fig_graphql_query) 中查询的响应可能是什么样子。响应是一个反映查询结构的 JSON 文档:它正好包含请求的那些属性,不多也不少。这种方法的优点是服务器不需要知道客户端需要哪些属性来渲染用户界面;相反,客户端可以简单地请求它需要的内容。例如,此查询不会为 `replyTo` 消息的发送者请求个人资料图片 URL但如果用户界面更改为添加该个人资料图片客户端可以很容易地将所需的 `imageUrl` 属性添加到查询中,而无需更改服务器。
{{< figure id="fig_graphql_response" title="示例 3-14. 对 [示例 3-13](/ch3#fig_graphql_query) 中查询的可能响应" class="w-full my-4" >}} {{< figure link="#fig_graphql_query" id="fig_graphql_response" title="示例 3-14. 对 示例 3-13 中查询的可能响应" class="w-full my-4" >}}
```json ```json
{ {
@ -857,7 +873,7 @@ query ChatApp {
在我们迄今为止讨论的所有数据模型中,数据以与写入相同的形式被查询 —— 无论是 JSON 文档、表中的行,还是图中的顶点和边。然而,在复杂的应用程序中,有时很难找到一种能够满足所有不同查询和呈现数据方式的单一数据表示。在这种情况下,以一种形式写入数据,然后从中派生出针对不同类型读取优化的多种表示形式可能是有益的。 在我们迄今为止讨论的所有数据模型中,数据以与写入相同的形式被查询 —— 无论是 JSON 文档、表中的行,还是图中的顶点和边。然而,在复杂的应用程序中,有时很难找到一种能够满足所有不同查询和呈现数据方式的单一数据表示。在这种情况下,以一种形式写入数据,然后从中派生出针对不同类型读取优化的多种表示形式可能是有益的。
我们之前在 ["记录系统和生数据"](/ch1#sec_introduction_derived) 中看到了这个想法ETL参见 ["数据仓库"](/ch1#sec_introduction_dwh))就是这种派生过程的一个例子。现在我们将进一步深入这个想法。如果我们无论如何都要从一种数据表示派生出另一种,我们可以选择分别针对写入和读取优化的不同表示。如果你只想为写入优化数据建模,而不关心高效查询,你会如何建模? 我们之前在 ["记录系统和生数据"](/ch1#sec_introduction_derived) 中看到了这个想法ETL参见 ["数据仓库"](/ch1#sec_introduction_dwh))就是这种派生过程的一个例子。现在我们将进一步深入这个想法。如果我们无论如何都要从一种数据表示派生出另一种,我们可以选择分别针对写入和读取优化的不同表示。如果你只想为写入优化数据建模,而不关心高效查询,你会如何建模?
也许写入数据的最简单、最快速和最具表现力的方式是 *事件日志*:每次你想写入一些数据时,你将其编码为自包含的字符串(可能是 JSON包括时间戳然后将其追加到事件序列中。此日志中的事件是 *不可变的*:你永远不会更改或删除它们,你只会向日志追加更多事件(这可能会取代早期事件)。事件可以包含任意属性。 也许写入数据的最简单、最快速和最具表现力的方式是 *事件日志*:每次你想写入一些数据时,你将其编码为自包含的字符串(可能是 JSON包括时间戳然后将其追加到事件序列中。此日志中的事件是 *不可变的*:你永远不会更改或删除它们,你只会向日志追加更多事件(这可能会取代早期事件)。事件可以包含任意属性。
@ -942,7 +958,7 @@ query ChatApp {
* 研究基因组数据的研究人员通常需要执行 *序列相似性搜索*,这意味着获取一个非常长的字符串(代表 DNA 分子)并将其与相似但不相同的大量字符串数据库进行匹配。这里描述的数据库都无法处理这种用法,这就是研究人员编写了像 GenBank [^69] 这样的专门基因组数据库软件的原因。 * 研究基因组数据的研究人员通常需要执行 *序列相似性搜索*,这意味着获取一个非常长的字符串(代表 DNA 分子)并将其与相似但不相同的大量字符串数据库进行匹配。这里描述的数据库都无法处理这种用法,这就是研究人员编写了像 GenBank [^69] 这样的专门基因组数据库软件的原因。
* 许多金融系统使用具有复式记账的 *账本* 作为其数据模型。这种类型的数据可以在关系数据库中表示,但也有像 TigerBeetle 这样专门研究这种数据模型的数据库。加密货币和区块链通常基于分布式账本,它们的数据模型中也内置了价值转移。 * 许多金融系统使用具有复式记账的 *账本* 作为其数据模型。这种类型的数据可以在关系数据库中表示,但也有像 TigerBeetle 这样专门研究这种数据模型的数据库。加密货币和区块链通常基于分布式账本,它们的数据模型中也内置了价值转移。
* *全文索* 可以说是一种经常与数据库一起使用的数据模型。信息检索是一个大型的专业主题,我们不会在本书中详细介绍,但我们将在 ["全文索"](/ch4#sec_storage_full_text) 中涉及搜索索引和向量搜索。 * *全文索* 可以说是一种经常与数据库一起使用的数据模型。信息检索是一个大型的专业主题,我们不会在本书中详细介绍,但我们将在 ["全文索"](/ch4#sec_storage_full_text) 中涉及搜索索引和向量搜索。
我们现在必须到此为止了。在下一章中,我们将讨论在 *实现* 本章中描述的数据模型时出现的一些权衡。 我们现在必须到此为止了。在下一章中,我们将讨论在 *实现* 本章中描述的数据模型时出现的一些权衡。

View File

@ -79,7 +79,7 @@ $ cat database
为了高效地找到数据库中特定键的值,我们需要一个不同的数据结构:*索引*。在本章中,我们将研究一系列索引结构并了解它们的比较;一般思想是以特定方式(例如,按某个键排序)构建数据,使定位所需数据更快。如果你想以几种不同的方式搜索相同的数据,你可能需要在数据的不同部分上建立几个不同的索引。 为了高效地找到数据库中特定键的值,我们需要一个不同的数据结构:*索引*。在本章中,我们将研究一系列索引结构并了解它们的比较;一般思想是以特定方式(例如,按某个键排序)构建数据,使定位所需数据更快。如果你想以几种不同的方式搜索相同的数据,你可能需要在数据的不同部分上建立几个不同的索引。
索引是从主数据生出的 *额外* 结构。许多数据库允许你添加和删除索引,这不会影响数据库的内容;它只影响查询的性能。维护额外的结构会产生开销,特别是在写入时。对于写入,很难超越简单地追加到文件的性能,因为这是最简单的写入操作。任何类型的索引通常都会减慢写入速度,因为每次写入数据时也需要更新索引。 索引是从主数据生出的 *额外* 结构。许多数据库允许你添加和删除索引,这不会影响数据库的内容;它只影响查询的性能。维护额外的结构会产生开销,特别是在写入时。对于写入,很难超越简单地追加到文件的性能,因为这是最简单的写入操作。任何类型的索引通常都会减慢写入速度,因为每次写入数据时也需要更新索引。
这是存储系统中的一个重要权衡:精心选择的索引加快了读查询速度,但每个索引都会消耗额外的磁盘空间并减慢写入速度,有时会大幅减慢 [^1]。因此,数据库通常不会默认为所有内容建立索引,而是要求你 —— 编写应用程序或管理数据库的人 —— 使用你对应用程序典型查询模式的了解来手动选择索引。然后你可以选择为你的应用程序带来最大收益的索引,而不会引入超过必要的写入开销。 这是存储系统中的一个重要权衡:精心选择的索引加快了读查询速度,但每个索引都会消耗额外的磁盘空间并减慢写入速度,有时会大幅减慢 [^1]。因此,数据库通常不会默认为所有内容建立索引,而是要求你 —— 编写应用程序或管理数据库的人 —— 使用你对应用程序典型查询模式的了解来手动选择索引。然后你可以选择为你的应用程序带来最大收益的索引,而不会引入超过必要的写入开销。
@ -521,15 +521,15 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
### 全文检索 {#sec_storage_full_text} ### 全文检索 {#sec_storage_full_text}
全文索允许你通过可能出现在文本中任何位置的关键字搜索文本文档集合(网页、产品描述等)[^88]。信息检索是一个大的专业主题,通常涉及特定于语言的处理:例如,几种亚洲语言在单词之间没有空格或标点符号,因此将文本分割成单词需要一个指示哪些字符序列构成单词的模型。全文索还经常涉及匹配相似但不相同的单词(例如拼写错误或单词的不同语法形式)和同义词。这些问题超出了本书的范围。 全文索允许你通过可能出现在文本中任何位置的关键字搜索文本文档集合(网页、产品描述等)[^88]。信息检索是一个大的专业主题,通常涉及特定于语言的处理:例如,几种亚洲语言在单词之间没有空格或标点符号,因此将文本分割成单词需要一个指示哪些字符序列构成单词的模型。全文索还经常涉及匹配相似但不相同的单词(例如拼写错误或单词的不同语法形式)和同义词。这些问题超出了本书的范围。
然而,在其核心,你可以将全文索视为另一种多维查询:在这种情况下,可能出现在文本中的每个单词(*词项*)是一个维度。包含词项 *x* 的文档在维度 *x* 中的值为 1不包含 *x* 的文档的值为 0。搜索提到"红苹果"的文档意味着查询在 *红* 维度中查找 1同时在 *苹果* 维度中查找 1。维度数量可能因此非常大。 然而,在其核心,你可以将全文索视为另一种多维查询:在这种情况下,可能出现在文本中的每个单词(*词项*)是一个维度。包含词项 *x* 的文档在维度 *x* 中的值为 1不包含 *x* 的文档的值为 0。搜索提到"红苹果"的文档意味着查询在 *红* 维度中查找 1同时在 *苹果* 维度中查找 1。维度数量可能因此非常大。
许多搜索引擎用来回答此类查询的数据结构称为 *倒排索引*。这是一个键值结构,其中键是词项,值是包含该词项的所有文档的 ID 列表(*倒排列表*)。如果文档 ID 是顺序数字,倒排列表也可以表示为稀疏位图,如 [图 4-8](#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](#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* 元语法。例如,字符串 `"hello"` 的三元语法(*n* = 3`"hel"`、`"ell"` 和 `"llo"`。如果我们为所有三元语法构建倒排索引,我们可以搜索至少三个字符长的任意子字符串的文档。三元语法索引甚至允许在搜索查询中使用正则表达式;缺点是它们相当大 [^94]。 除了将文本分解为单词,另一种选择是查找长度为 *n* 的所有子字符串,称为 *n* 元语法。例如,字符串 `"hello"` 的三元语法(*n* = 3`"hel"`、`"ell"` 和 `"llo"`。如果我们为所有三元语法构建倒排索引,我们可以搜索至少三个字符长的任意子字符串的文档。三元语法索引甚至允许在搜索查询中使用正则表达式;缺点是它们相当大 [^94]。
@ -565,7 +565,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
倒排文件IVF索引 倒排文件IVF索引
: 向量空间被聚类为向量的分区(称为 *质心*以减少必须比较的向量数量。IVF 索引比平面索引更快,但只能给出近似结果:即使查询和文档彼此接近,它们也可能落入不同的分区。对 IVF 索引的查询首先定义 *探针*,这只是要检查的分区数。使用更多探针的查询将更准确,但会更慢,因为必须比较更多向量。 : 向量空间被聚类为向量的分区(称为 *质心*以减少必须比较的向量数量。IVF 索引比平面索引更快,但只能给出近似结果:即使查询和文档彼此接近,它们也可能落入不同的分区。对 IVF 索引的查询首先定义 *探针*,这只是要检查的分区数。使用更多探针的查询将更准确,但会更慢,因为必须比较更多向量。
可导航小世界HNSW 层可导航小世界HNSW
: HNSW 索引维护向量空间的多个层,如 [图 4-11](#fig_vector_hnsw) 所示。每一层都表示为一个图,其中节点表示向量,边表示与附近向量的接近度。查询首先在最顶层定位最近的向量,该层具有少量节点。然后查询移动到下面一层的同一节点,并跟随该层中的边,该层连接更密集,寻找更接近查询向量的向量。该过程继续直到到达最后一层。与 IVF 索引一样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" >}} {{< figure src="/fig/ddia_0411.png" id="fig_vector_hnsw" caption="图 4-11. 在 HNSW 索引中搜索最接近给定查询向量的数据库条目。" class="w-full my-4" >}}
@ -587,7 +587,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
* 日志结构方法只允许追加到文件和删除过时文件但从不更新已写入的文件。SSTable、LSM 树、RocksDB、Cassandra、HBase、Scylla、Lucene 等属于这一组。一般来说,日志结构存储引擎往往提供高写入吞吐量。 * 日志结构方法只允许追加到文件和删除过时文件但从不更新已写入的文件。SSTable、LSM 树、RocksDB、Cassandra、HBase、Scylla、Lucene 等属于这一组。一般来说,日志结构存储引擎往往提供高写入吞吐量。
* 就地更新方法将磁盘视为一组可以覆盖的固定大小页。B 树是这种理念的最大例子,用于所有主要的关系型 OLTP 数据库以及许多非关系型数据库。作为经验法则B 树往往更适合读取,提供比日志结构存储更高的读取吞吐量和更低的响应时间。 * 就地更新方法将磁盘视为一组可以覆盖的固定大小页。B 树是这种理念的最大例子,用于所有主要的关系型 OLTP 数据库以及许多非关系型数据库。作为经验法则B 树往往更适合读取,提供比日志结构存储更高的读取吞吐量和更低的响应时间。
然后我们查看了可以同时搜索多个条件的索引:多维索引(如 R 树)可以同时按纬度和经度搜索地图上的点,全文索索引可以搜索出现在同一文本中的多个关键字。最后,向量数据库用于文本文档和其他媒体的语义搜索;它们使用具有大量维度的向量,并通过比较向量相似性来查找相似文档。 然后我们查看了可以同时搜索多个条件的索引:多维索引(如 R 树)可以同时按纬度和经度搜索地图上的点,全文索索引可以搜索出现在同一文本中的多个关键字。最后,向量数据库用于文本文档和其他媒体的语义搜索;它们使用具有大量维度的向量,并通过比较向量相似性来查找相似文档。
作为应用程序开发人员,如果你掌握了有关存储引擎内部的这些知识,你就能更好地知道哪种工具最适合你的特定应用程序。如果你需要调整数据库的调优参数,这种理解使你能够想象更高或更低的值可能产生什么影响。 作为应用程序开发人员,如果你掌握了有关存储引擎内部的这些知识,你就能更好地知道哪种工具最适合你的特定应用程序。如果你需要调整数据库的调优参数,这种理解使你能够想象更高或更低的值可能产生什么影响。

View File

@ -297,7 +297,7 @@ Cassandra、ScyllaDB 和 Riak 采用不同的方法:它们在节点之间使
但是,如果你想要所有结果并且事先不知道它们的分区键,你需要将查询发送到所有分片,并组合你收到的结果,因为匹配的记录可能分散在所有分片中。在 [图 7-9](/ch7#fig_sharding_local_secondary) 中,红色汽车出现在分片 0 和分片 1 中。 但是,如果你想要所有结果并且事先不知道它们的分区键,你需要将查询发送到所有分片,并组合你收到的结果,因为匹配的记录可能分散在所有分片中。在 [图 7-9](/ch7#fig_sharding_local_secondary) 中,红色汽车出现在分片 0 和分片 1 中。
这种查询分片数据库的方法有时称为 *分散/聚集*,它可能使二级索引上的读取查询相当昂贵。即使并行查询分片,分散/聚集也容易导致尾部延迟放大(见 ["响应时间指标的使用"](/ch2#sec_introduction_slo_sla))。它还限制了应用程序的可扩展性:添加更多分片让你存储更多数据,但如果每个分片无论如何都必须处理每个查询,它不会增加你的查询吞吐量。 这种查询分片数据库的方法有时称为 *分散/聚集*,它可能使二级索引上的读取查询相当昂贵。即使并行查询分片,分散/聚集也容易导致尾部延迟放大(见 ["响应时间指标的使用"](/ch2#sec_introduction_slo_sla))。它还限制了应用程序的可伸缩性:添加更多分片让你存储更多数据,但如果每个分片无论如何都必须处理每个查询,它不会增加你的查询吞吐量。
尽管如此,本地二级索引被广泛使用 [^31]例如MongoDB、Riak、Cassandra [^32]、Elasticsearch [^33]、SolrCloud 和 VoltDB [^34] 都使用本地二级索引。 尽管如此,本地二级索引被广泛使用 [^31]例如MongoDB、Riak、Cassandra [^32]、Elasticsearch [^33]、SolrCloud 和 VoltDB [^34] 都使用本地二级索引。
@ -309,7 +309,7 @@ Cassandra、ScyllaDB 和 Riak 采用不同的方法:它们在节点之间使
{{< figure src="/fig/ddia_0710.png" id="fig_sharding_global_secondary" caption="图 7-10. 全局二级索引反映来自所有分片的数据,并且本身按索引值进行分片。" class="w-full my-4" >}} {{< figure src="/fig/ddia_0710.png" id="fig_sharding_global_secondary" caption="图 7-10. 全局二级索引反映来自所有分片的数据,并且本身按索引值进行分片。" class="w-full my-4" >}}
这种索引也称为 *基于词项分区* [^30]:回忆一下 ["全文搜索"](/ch4#sec_storage_full_text),在全文搜索中,*词项* 是你可以搜索的文本中的关键字。这里我们将其推广为指二级索引中你可以搜索的任何值。 这种索引也称为 *基于词项分区* [^30]:回忆一下 ["全文检索"](/ch4#sec_storage_full_text),在全文检索中,*词项* 是你可以搜索的文本中的关键字。这里我们将其推广为指二级索引中你可以搜索的任何值。
全局索引使用词项作为分区键,因此当你查找特定词项或值时,你可以找出需要查询哪个分片。和以前一样,分片可以包含连续的词项范围(如 [图 7-10](/ch7#fig_sharding_global_secondary)),或者你可以基于词项的哈希将词项分配给分片。 全局索引使用词项作为分区键,因此当你查找特定词项或值时,你可以找出需要查询哪个分片。和以前一样,分片可以包含连续的词项范围(如 [图 7-10](/ch7#fig_sharding_global_secondary)),或者你可以基于词项的哈希将词项分配给分片。

View File

@ -643,7 +643,7 @@ VoltDB 还使用存储过程进行复制:它不是将事务的写入从一个
但是,对于需要访问多个分片的任何事务,数据库必须协调它所涉及的所有分片之间的事务。存储过程需要在所有分片上同步执行,以确保整个系统的可串行化。 但是,对于需要访问多个分片的任何事务,数据库必须协调它所涉及的所有分片之间的事务。存储过程需要在所有分片上同步执行,以确保整个系统的可串行化。
由于跨分片事务具有额外的协调开销因此它们比单分片事务慢得多。VoltDB 报告的跨分片写入吞吐量约为每秒 1,000 次,这比其单分片吞吐量低几个数量级,并且无法通过添加更多机器来增加[^61]。最近的研究探索了使多分片事务更具可扩展性的方法[^63]。 由于跨分片事务具有额外的协调开销因此它们比单分片事务慢得多。VoltDB 报告的跨分片写入吞吐量约为每秒 1,000 次,这比其单分片吞吐量低几个数量级,并且无法通过添加更多机器来增加[^61]。最近的研究探索了使多分片事务更具可伸缩性的方法[^63]。
事务是否可以是单分片的很大程度上取决于应用程序使用的数据结构。简单的键值数据通常可以很容易地分片,但具有多个二级索引的数据可能需要大量的跨分片协调(参见["分片和二级索引"](/ch7#sec_sharding_secondary_indexes))。 事务是否可以是单分片的很大程度上取决于应用程序使用的数据结构。简单的键值数据通常可以很容易地分片,但具有多个二级索引的数据可能需要大量的跨分片协调(参见["分片和二级索引"](/ch7#sec_sharding_secondary_indexes))。
@ -1006,7 +1006,7 @@ XA 的最大问题可以通过以下方式解决:
因此,实现精确一次处理只需要数据库中的事务——跨数据库和消息代理的原子性对于此用例不是必需的。在数据库中记录消息 ID 使消息处理*幂等*,因此可以安全地重试消息处理而不会重复其副作用。流处理框架(如 Kafka Streams中使用类似的方法来实现精确一次语义我们将在[待补充链接]中看到。 因此,实现精确一次处理只需要数据库中的事务——跨数据库和消息代理的原子性对于此用例不是必需的。在数据库中记录消息 ID 使消息处理*幂等*,因此可以安全地重试消息处理而不会重复其副作用。流处理框架(如 Kafka Streams中使用类似的方法来实现精确一次语义我们将在[待补充链接]中看到。
但是,数据库内的内部分布式事务对于此类模式的可扩展性仍然有用:例如,它们将允许消息 ID 存储在一个分片上,而消息处理更新的主数据存储在其他分片上,并确保跨这些分片的事务提交的原子性。 但是,数据库内的内部分布式事务对于此类模式的可伸缩性仍然有用:例如,它们将允许消息 ID 存储在一个分片上,而消息处理更新的主数据存储在其他分片上,并确保跨这些分片的事务提交的原子性。

View File

@ -677,7 +677,7 @@ DST 提供了超越可重放性的几个优势。Antithesis 等工具试图通
事实上,使事物确定性是一个简单但强大的想法,在分布式系统设计中一再出现。除了确定性模拟测试,我们在过去的章节中已经看到了几种使用确定性的方法: 事实上,使事物确定性是一个简单但强大的想法,在分布式系统设计中一再出现。除了确定性模拟测试,我们在过去的章节中已经看到了几种使用确定性的方法:
* 事件溯源的一个关键优势(见 ["事件溯源和 CQRS"](/ch3#sec_datamodels_events))是你可以确定性地重放事件日志以重建生的物化视图。 * 事件溯源的一个关键优势(见 ["事件溯源和 CQRS"](/ch3#sec_datamodels_events))是你可以确定性地重放事件日志以重建生的物化视图。
* 工作流引擎(见 ["持久执行和工作流"](/ch5#sec_encoding_dataflow_workflows))依赖于工作流定义是确定性的,以提供持久执行语义。 * 工作流引擎(见 ["持久执行和工作流"](/ch5#sec_encoding_dataflow_workflows))依赖于工作流定义是确定性的,以提供持久执行语义。
* *状态机复制*,我们将在 ["使用共享日志"](/ch10#sec_consistency_smr) 中讨论,通过在每个副本上独立执行相同的确定性事务序列来复制数据。我们已经看到了这个想法的两个变体:基于语句的复制(见 ["复制日志的实现"](/ch6#sec_replication_implementation))和使用存储过程的串行事务执行(见 ["存储过程的利弊"](/ch8#sec_transactions_stored_proc_tradeoffs))。 * *状态机复制*,我们将在 ["使用共享日志"](/ch10#sec_consistency_smr) 中讨论,通过在每个副本上独立执行相同的确定性事务序列来复制数据。我们已经看到了这个想法的两个变体:基于语句的复制(见 ["复制日志的实现"](/ch6#sec_replication_implementation))和使用存储过程的串行事务执行(见 ["存储过程的利弊"](/ch8#sec_transactions_stored_proc_tradeoffs))。
@ -699,7 +699,7 @@ DST 提供了超越可重放性的几个优势。Antithesis 等工具试图通
如果你习惯于在单台计算机的理想数学完美环境中编写软件,其中相同的操作总是确定性地返回相同的结果,那么转向分布式系统混乱的物理现实可能会有点震惊。相反,分布式系统工程师通常会认为如果一个问题可以在单台计算机上解决,那它就是微不足道的 [^4],而且单台计算机现在确实可以做很多事情。如果你可以避免打开潘多拉的盒子,只需将事情保持在单台机器上,例如使用嵌入式存储引擎(见 ["嵌入式存储引擎"](/ch4#sidebar_embedded)),通常值得这样做。 如果你习惯于在单台计算机的理想数学完美环境中编写软件,其中相同的操作总是确定性地返回相同的结果,那么转向分布式系统混乱的物理现实可能会有点震惊。相反,分布式系统工程师通常会认为如果一个问题可以在单台计算机上解决,那它就是微不足道的 [^4],而且单台计算机现在确实可以做很多事情。如果你可以避免打开潘多拉的盒子,只需将事情保持在单台机器上,例如使用嵌入式存储引擎(见 ["嵌入式存储引擎"](/ch4#sidebar_embedded)),通常值得这样做。
然而,正如在 ["分布式系统与单节点系统"](/ch1#sec_introduction_distributed) 中讨论的,可扩展性并不是使用分布式系统的唯一原因。容错和低延迟(通过将数据在地理上放置在靠近用户的位置)是同样重要的目标,而这些事情无法通过单个节点实现。分布式系统的力量在于,原则上它们可以在服务层面永远运行而不被中断,因为所有故障和维护都可以在节点层面处理。(实际上,如果错误的配置更改被推送到所有节点,仍然会让分布式系统崩溃。) 然而,正如在 ["分布式系统与单节点系统"](/ch1#sec_introduction_distributed) 中讨论的,可伸缩性并不是使用分布式系统的唯一原因。容错和低延迟(通过将数据在地理上放置在靠近用户的位置)是同样重要的目标,而这些事情无法通过单个节点实现。分布式系统的力量在于,原则上它们可以在服务层面永远运行而不被中断,因为所有故障和维护都可以在节点层面处理。(实际上,如果错误的配置更改被推送到所有节点,仍然会让分布式系统崩溃。)
在本章中,我们还探讨了网络、时钟和进程的不可靠性是否是不可避免的自然法则。我们看到它不是:可以在网络中提供硬实时响应保证和有界延迟,但这样做非常昂贵,并导致硬件资源利用率降低。大多数非安全关键系统选择便宜和不可靠而不是昂贵和可靠。 在本章中,我们还探讨了网络、时钟和进程的不可靠性是否是不可避免的自然法则。我们看到它不是:可以在网络中提供硬实时响应保证和有界延迟,但这样做非常昂贵,并导致硬件资源利用率降低。大多数非安全关键系统选择便宜和不可靠而不是昂贵和可靠。

View File

@ -103,7 +103,7 @@ breadcrumbs: false
## **全文检索full-text search** ## **全文检索full-text search**
通过任意关键字来搜索文本,通常具有附加特征,例如匹配类似的拼写词或同义词。全文索引是一种支持这种查询的次级索引。请参阅“[全文搜索和模糊索引](/ch3#全文搜索和模糊索引)”。 通过任意关键字来搜索文本,通常具有附加特征,例如匹配类似的拼写词或同义词。全文索引是一种支持这种查询的次级索引。请参阅“[全文检索与模糊索引](/ch3#全文检索与模糊索引)”。
## **图graph** ## **图graph**
@ -233,7 +233,7 @@ breadcrumbs: false
## **记录系统system of record** ## **记录系统system of record**
一个保存主要权威版本数据的系统,也被称为真相的来源。首先在这里写入数据变更,其他数据集可以从记录系统生。请参阅[第三部分](/part-iii)的介绍。 一个保存主要权威版本数据的系统,也被称为真相的来源。首先在这里写入数据变更,其他数据集可以从记录系统生。请参阅[第三部分](/part-iii)的介绍。
## **超时timeout** ## **超时timeout**

View File

@ -24,15 +24,15 @@ breadcrumbs: false
派生数据系统Derived data systems 派生数据系统Derived data systems
: **派生系统** 中的数据,通常是另一个系统中的现有数据以某种方式进行转换或处理的结果。如果丢失派生数据,可以从原始来源重新创建。 : **派生系统** 中的数据,通常是另一个系统中的现有数据以某种方式进行转换或处理的结果。如果丢失派生数据,可以从原始来源重新创建。
典型的例子是 **缓存cache**:如果数据在缓存中,就可以由缓存提供服务;如果缓存不包含所需数据,则降级由底层数据库提供。非规范化的值,索引和物化视图亦属此类。在推荐系统中,预测汇总数据通常生自用户日志。 典型的例子是 **缓存cache**:如果数据在缓存中,就可以由缓存提供服务;如果缓存不包含所需数据,则降级由底层数据库提供。非规范化的值,索引和物化视图亦属此类。在推荐系统中,预测汇总数据通常生自用户日志。
从技术上讲,派生数据是 **冗余的redundant**,因为它重复了已有的信息。但是派生数据对于获得良好的只读查询性能通常是至关重要的。它通常是非规范化的。可以从单个源头生出多个不同的数据集,使你能从不同的 “视角” 洞察数据。 从技术上讲,派生数据是 **冗余的redundant**,因为它重复了已有的信息。但是派生数据对于获得良好的只读查询性能通常是至关重要的。它通常是非规范化的。可以从单个源头生出多个不同的数据集,使你能从不同的 “视角” 洞察数据。
并不是所有的系统都在其架构中明确区分 **记录系统****派生数据系统**,但是这是一种有用的区分方式,因为它明确了系统中的数据流:系统的哪一部分具有哪些输入和哪些输出,以及它们如何相互依赖。 并不是所有的系统都在其架构中明确区分 **记录系统****派生数据系统**,但是这是一种有用的区分方式,因为它明确了系统中的数据流:系统的哪一部分具有哪些输入和哪些输出,以及它们如何相互依赖。
大多数数据库,存储引擎和查询语言,本质上既不是记录系统也不是派生系统。数据库只是一个工具:如何使用它取决于你自己。**记录系统和派生数据系统之间的区别不在于工具,而在于应用程序中的使用方式。** 大多数数据库,存储引擎和查询语言,本质上既不是记录系统也不是派生系统。数据库只是一个工具:如何使用它取决于你自己。**记录系统和派生数据系统之间的区别不在于工具,而在于应用程序中的使用方式。**
通过梳理数据的生关系,可以清楚地理解一个令人困惑的系统架构。这将贯穿本书的这一部分。 通过梳理数据的生关系,可以清楚地理解一个令人困惑的系统架构。这将贯穿本书的这一部分。
## 章节概述 ## 章节概述

View File

@ -72,7 +72,7 @@ breadcrumbs: false
2. 在 [第二部分](/part-ii) 中,我们从讨论存储在一台机器上的数据转向讨论分布在多台机器上的数据。这对于可伸缩性通常是必需的,但带来了各种独特的挑战。我们首先讨论复制([第五章](/ch5))、分区 / 分片([第六章](/ch6))和事务([第七章](/ch7))。然后我们将探索关于分布式系统问题的更多细节([第八章](/ch8)),以及在分布式系统中实现一致性与共识意味着什么([第九章](/ch9))。 2. 在 [第二部分](/part-ii) 中,我们从讨论存储在一台机器上的数据转向讨论分布在多台机器上的数据。这对于可伸缩性通常是必需的,但带来了各种独特的挑战。我们首先讨论复制([第五章](/ch5))、分区 / 分片([第六章](/ch6))和事务([第七章](/ch7))。然后我们将探索关于分布式系统问题的更多细节([第八章](/ch8)),以及在分布式系统中实现一致性与共识意味着什么([第九章](/ch9))。
3. 在 [第三部分](/part-iii) 中,我们讨论那些从其他数据集生出一些数据集的系统。派生数据经常出现在异构系统中:当没有单个数据库可以把所有事情都做的很好时,应用需要集成几种不同的数据库、缓存、索引等。在 [第十一章](/ch11) 中我们将从一种派生数据的批处理方法开始,然后在此基础上建立在 [第十二章](/ch12) 中讨论的流处理。最后,在 [第十三章](/ch13) 中,我们将所有内容汇总,讨论在将来构建可靠、可伸缩和可维护的应用程序的方法。 3. 在 [第三部分](/part-iii) 中,我们讨论那些从其他数据集生出一些数据集的系统。派生数据经常出现在异构系统中:当没有单个数据库可以把所有事情都做的很好时,应用需要集成几种不同的数据库、缓存、索引等。在 [第十一章](/ch11) 中我们将从一种派生数据的批处理方法开始,然后在此基础上建立在 [第十二章](/ch12) 中讨论的流处理。最后,在 [第十三章](/ch13) 中,我们将所有内容汇总,讨论在将来构建可靠、可伸缩和可维护的应用程序的方法。
## 参考文献与延伸阅读 ## 参考文献与延伸阅读

View File

@ -20,514 +20,91 @@ breadcrumbs: false
## [1. 数据系统架构中的权衡](/ch1) ## [1. 数据系统架构中的权衡](/ch1)
- [分析型与事务型系统](/ch1#sec_introduction_analytics) - [分析型与事务型系统](/ch1#sec_introduction_analytics)
- [事务处理与分析的特征](/ch1#sec_introduction_oltp)
- [数据仓库](/ch1#sec_introduction_dwh)
- [从数据仓库到数据湖](/ch1#from-data-warehouse-to-data-lake)
- [超越数据湖](/ch1#beyond-the-data-lake)
- [权威数据源与派生数据](/ch1#sec_introduction_derived)
- [云服务与自托管](/ch1#sec_introduction_cloud) - [云服务与自托管](/ch1#sec_introduction_cloud)
- [云服务的利弊](/ch1#sec_introduction_cloud_tradeoffs)
- [云原生系统架构](/ch1#sec_introduction_cloud_native)
- [云服务的分层](/ch1#layering-of-cloud-services)
- [存储与计算的分离](/ch1#sec_introduction_storage_compute)
- [云时代的运维](/ch1#sec_introduction_operations)
- [分布式与单节点系统](/ch1#sec_introduction_distributed) - [分布式与单节点系统](/ch1#sec_introduction_distributed)
- [分布式系统的问题](/ch1#sec_introduction_dist_sys_problems)
- [微服务与 Serverless](/ch1#sec_introduction_microservices)
- [云计算与超级计算](/ch1#id17)
- [数据系统、法律与社会](/ch1#sec_introduction_compliance) - [数据系统、法律与社会](/ch1#sec_introduction_compliance)
- [总结](/ch1#summary) - [总结](/ch1#summary)
- [参考](/ch1#参考)
## [2. 定义非功能性需求](/ch2) ## [2. 定义非功能性需求](/ch2)
- [案例研究:社交网络首页时间线](/ch2#sec_introduction_twitter) - [案例研究:社交网络首页时间线](/ch2#sec_introduction_twitter)
- [表示用户、帖子与关注关系](/ch2#id20)
- [时间线的物化与更新](/ch2#sec_introduction_materializing)
- [描述性能](/ch2#sec_introduction_percentiles) - [描述性能](/ch2#sec_introduction_percentiles)
- [延迟与响应时间](/ch2#id23)
- [平均值、中位数与百分位数](/ch2#id24)
- [响应时间指标的应用](/ch2#sec_introduction_slo_sla)
- [可靠性与容错](/ch2#sec_introduction_reliability) - [可靠性与容错](/ch2#sec_introduction_reliability)
- [容错](/ch2#id27)
- [硬件与软件故障](/ch2#sec_introduction_hardware_faults)
- [通过冗余容忍硬件故障](/ch2#tolerating-hardware-faults-through-redundancy)
- [软件故障](/ch2#software-faults)
- [人类与可靠性](/ch2#id31)
- [可伸缩性](/ch2#sec_introduction_scalability) - [可伸缩性](/ch2#sec_introduction_scalability)
- [描述负载](/ch2#id33)
- [共享内存、共享磁盘与无共享架构](/ch2#sec_introduction_shared_nothing)
- [可伸缩性原则](/ch2#id35)
- [可运维性](/ch2#sec_introduction_maintainability) - [可运维性](/ch2#sec_introduction_maintainability)
- [可运维性:让运维更轻松](/ch2#id37)
- [简单性:管理复杂度](/ch2#id38)
- [可演化性:让变化更容易](/ch2#sec_introduction_evolvability)
- [总结](/ch2#summary) - [总结](/ch2#summary)
- [参考](/ch2#参考)
## [3. 数据模型与查询语言](/ch3) ## [3. 数据模型与查询语言](/ch3)
- [关系模型与文档模型](/ch3#sec_datamodels_history) - [关系模型与文档模型](/ch3#sec_datamodels_history)
- [对象关系不匹配](/ch3#sec_datamodels_document)
- [对象关系映射ORM](/ch3#object-relational-mapping-orm)
- [用于一对多关系的文档数据模型](/ch3#the-document-data-model-for-one-to-many-relationships)
- [规范化、反规范化与连接](/ch3#sec_datamodels_normalization)
- [规范化的权衡](/ch3#trade-offs-of-normalization)
- [社交网络案例研究中的反规范化](/ch3#denormalization-in-the-social-networking-case-study)
- [多对一与多对多关系](/ch3#sec_datamodels_many_to_many)
- [星型与雪花型:分析模式](/ch3#sec_datamodels_analytics)
- [何时使用哪种模型](/ch3#sec_datamodels_document_summary)
- [文档模型中的模式灵活性](/ch3#sec_datamodels_schema_flexibility)
- [读写的数据局部性](/ch3#sec_datamodels_document_locality)
- [文档的查询语言](/ch3#query-languages-for-documents)
- [文档和关系数据库的融合](/ch3#convergence-of-document-and-relational-databases)
- [图数据模型](/ch3#sec_datamodels_graph) - [图数据模型](/ch3#sec_datamodels_graph)
- [属性图](/ch3#id56)
- [Cypher 查询语言](/ch3#id57)
- [SQL 中的图查询](/ch3#id58)
- [三元组存储与 SPARQL](/ch3#id59)
- [RDF 数据模型](/ch3#the-rdf-data-model)
- [SPARQL 查询语言](/ch3#the-sparql-query-language)
- [Datalog递归关系查询](/ch3#id62)
- [GraphQL](/ch3#id63)
- [事件溯源与 CQRS](/ch3#sec_datamodels_events) - [事件溯源与 CQRS](/ch3#sec_datamodels_events)
- [数据框、矩阵与数组](/ch3#sec_datamodels_dataframes) - [数据框、矩阵与数组](/ch3#sec_datamodels_dataframes)
- [总结](/ch3#summary) - [总结](/ch3#summary)
- [参考](/ch3#参考)
## [4. 存储与检索](/ch4) ## [4. 存储与检索](/ch4)
- [OLTP 系统的存储与索引](/ch4#sec_storage_oltp) - [OLTP 系统的存储与索引](/ch4#sec_storage_oltp)
- [日志结构存储](/ch4#sec_storage_log_structured)
- [SSTable 文件格式](/ch4#the-sstable-file-format)
- [构建和合并 SSTable](/ch4#constructing-and-merging-sstables)
- [布隆过滤器](/ch4#bloom-filters)
- [压实策略](/ch4#sec_storage_lsm_compaction)
- [B 树](/ch4#sec_storage_b_trees)
- [使 B 树可靠](/ch4#sec_storage_btree_wal)
- [B 树变体](/ch4#b-tree-variants)
- [比较 B 树与 LSM 树](/ch4#sec_storage_btree_lsm_comparison)
- [读取性能](/ch4#read-performance)
- [顺序与随机写入](/ch4#sidebar_sequential)
- [写放大](/ch4#write-amplification)
- [磁盘空间使用](/ch4#disk-space-usage)
- [多列索引与二级索引](/ch4#sec_storage_index_multicolumn)
- [在索引中存储值](/ch4#sec_storage_index_heap)
- [全内存存储](/ch4#sec_storage_inmemory)
- [分析型数据存储](/ch4#sec_storage_analytics) - [分析型数据存储](/ch4#sec_storage_analytics)
- [云数据仓库](/ch4#sec_cloud_data_warehouses)
- [列式存储](/ch4#sec_storage_column)
- [列压缩](/ch4#sec_storage_column_compression)
- [列存储中的排序顺序](/ch4#sort-order-in-column-storage)
- [写入列式存储](/ch4#writing-to-column-oriented-storage)
- [查询执行:编译与向量化](/ch4#sec_storage_vectorized)
- [物化视图与多维数据集](/ch4#sec_storage_materialized_views)
- [多维索引与全文索引](/ch4#sec_storage_multidimensional) - [多维索引与全文索引](/ch4#sec_storage_multidimensional)
- [全文检索](/ch4#sec_storage_full_text)
- [向量嵌入](/ch4#id92)
- [总结](/ch4#summary) - [总结](/ch4#summary)
- [参考](/ch4#参考)
## [5. 编码与演化](/ch5) ## [5. 编码与演化](/ch5)
- [编码数据的格式](/ch5#sec_encoding_formats) - [编码数据的格式](/ch5#sec_encoding_formats)
- [特定语言的格式](/ch5#id96)
- [JSON、XML 及其二进制变体](/ch5#sec_encoding_json)
- [JSON 模式](/ch5#json-schema)
- [二进制编码](/ch5#binary-encoding)
- [Protocol Buffers](/ch5#sec_encoding_protobuf)
- [字段标签与模式演化](/ch5#field-tags-and-schema-evolution)
- [Avro](/ch5#sec_encoding_avro)
- [写入者模式与读取者模式](/ch5#the-writers-schema-and-the-readers-schema)
- [模式演化规则](/ch5#schema-evolution-rules)
- [但什么是写入者模式?](/ch5#but-what-is-the-writers-schema)
- [动态生成的模式](/ch5#dynamically-generated-schemas)
- [模式的优点](/ch5#sec_encoding_schemas)
- [数据流的模式](/ch5#sec_encoding_dataflow) - [数据流的模式](/ch5#sec_encoding_dataflow)
- [流经数据库的数据流](/ch5#sec_encoding_dataflow_db)
- [不同时间写入的不同值](/ch5#different-values-written-at-different-times)
- [归档存储](/ch5#archival-storage)
- [流经服务的数据流REST 与 RPC](/ch5#sec_encoding_dataflow_rpc)
- [Web 服务](/ch5#sec_web_services)
- [远程过程调用RPC的问题](/ch5#sec_problems_with_rpc)
- [负载均衡器、服务发现和服务网格](/ch5#sec_encoding_service_discovery)
- [RPC 的数据编码与演化](/ch5#data-encoding-and-evolution-for-rpc)
- [持久化执行与工作流](/ch5#sec_encoding_dataflow_workflows)
- [持久化执行](/ch5#durable-execution)
- [事件驱动的架构](/ch5#sec_encoding_dataflow_msg)
- [消息代理](/ch5#message-brokers)
- [分布式 actor 框架](/ch5#distributed-actor-frameworks)
- [总结](/ch5#summary) - [总结](/ch5#summary)
- [参考](/ch5#参考)
## [6. 复制](/ch6) ## [6. 复制](/ch6)
- [单主复制](/ch6#sec_replication_leader) - [单主复制](/ch6#sec_replication_leader)
- [同步复制与异步复制](/ch6#sec_replication_sync_async)
- [设置新的副本](/ch6#sec_replication_new_replica)
- [处理节点故障](/ch6#sec_replication_failover)
- [从节点故障:追赶恢复](/ch6#follower-failure-catch-up-recovery)
- [领导者故障:故障转移](/ch6#leader-failure-failover)
- [复制日志的实现](/ch6#sec_replication_implementation)
- [基于语句的复制](/ch6#statement-based-replication)
- [预写日志WAL传输](/ch6#write-ahead-log-wal-shipping)
- [逻辑(基于行)日志复制](/ch6#logical-row-based-log-replication)
- [复制延迟的问题](/ch6#sec_replication_lag) - [复制延迟的问题](/ch6#sec_replication_lag)
- [读己之写](/ch6#sec_replication_ryw)
- [单调读](/ch6#sec_replication_monotonic_reads)
- [一致前缀读](/ch6#sec_replication_consistent_prefix)
- [复制延迟的解决方案](/ch6#id131)
- [多主复制](/ch6#sec_replication_multi_leader) - [多主复制](/ch6#sec_replication_multi_leader)
- [跨地域运行](/ch6#sec_replication_multi_dc)
- [多主复制拓扑](/ch6#sec_replication_topologies)
- [不同拓扑的问题](/ch6#problems-with-different-topologies)
- [同步引擎与本地优先软件](/ch6#sec_replication_offline_clients)
- [实时协作、离线优先和本地优先应用](/ch6#real-time-collaboration-offline-first-and-local-first-apps)
- [同步引擎的利弊](/ch6#pros-and-cons-of-sync-engines)
- [处理写入冲突](/ch6#sec_replication_write_conflicts)
- [冲突避免](/ch6#conflict-avoidance)
- [最后写入者胜(丢弃并发写入)](/ch6#sec_replication_lww)
- [手动冲突解决](/ch6#manual-conflict-resolution)
- [自动冲突解决](/ch6#automatic-conflict-resolution)
- [CRDT 与操作变换](/ch6#sec_replication_crdts)
- [什么是冲突?](/ch6#what-is-a-conflict)
- [无主复制](/ch6#sec_replication_leaderless) - [无主复制](/ch6#sec_replication_leaderless)
- [当节点故障时写入数据库](/ch6#id287)
- [追赶错过的写入](/ch6#sec_replication_read_repair)
- [读写仲裁](/ch6#sec_replication_quorum_condition)
- [仲裁一致性的局限](/ch6#sec_replication_quorum_limitations)
- [监控陈旧性](/ch6#monitoring-staleness)
- [单主与无主复制的性能](/ch6#sec_replication_leaderless_perf)
- [多地区操作](/ch6#multi-region-operation)
- [检测并发写入](/ch6#sec_replication_concurrent)
- ["先发生"关系与并发](/ch6#sec_replication_happens_before)
- [捕获先发生关系](/ch6#capturing-the-happens-before-relationship)
- [版本向量](/ch6#version-vectors)
- [总结](/ch6#summary) - [总结](/ch6#summary)
- [参考](/ch6#参考)
## [7. 分片](/ch7) ## [7. 分片](/ch7)
- [分片的利与弊](/ch7#sec_sharding_reasons) - [分片的利与弊](/ch7#sec_sharding_reasons)
- [面向多租户的分片](/ch7#sec_sharding_multitenancy)
- [键值数据的分片](/ch7#sec_sharding_key_value) - [键值数据的分片](/ch7#sec_sharding_key_value)
- [按键的范围分片](/ch7#sec_sharding_key_range)
- [重新平衡键范围分片数据](/ch7#rebalancing-key-range-sharded-data)
- [按键的哈希分片](/ch7#sec_sharding_hash)
- [哈希取模节点数](/ch7#hash-modulo-number-of-nodes)
- [固定数量的分片](/ch7#fixed-number-of-shards)
- [按哈希范围分片](/ch7#sharding-by-hash-range)
- [一致性哈希](/ch7#sec_sharding_consistent_hashing)
- [倾斜的工作负载与缓解热点](/ch7#sec_sharding_skew)
- [运维:自动/手动再均衡](/ch7#sec_sharding_operations)
- [请求路由](/ch7#sec_sharding_routing) - [请求路由](/ch7#sec_sharding_routing)
- [分片与二级索引](/ch7#sec_sharding_secondary_indexes) - [分片与二级索引](/ch7#sec_sharding_secondary_indexes)
- [本地二级索引](/ch7#id166)
- [全局二级索引](/ch7#id167)
- [总结](/ch7#summary) - [总结](/ch7#summary)
- [References](/ch7#references)
## [8. 事务](/ch8) ## [8. 事务](/ch8)
- [事务到底是什么?](/ch8#sec_transactions_overview) - [事务到底是什么?](/ch8#sec_transactions_overview)
- [ACID 的含义](/ch8#sec_transactions_acid)
- [原子性](/ch8#sec_transactions_acid_atomicity)
- [一致性](/ch8#sec_transactions_acid_consistency)
- [隔离性](/ch8#sec_transactions_acid_isolation)
- [持久性](/ch8#durability)
- [单对象与多对象操作](/ch8#sec_transactions_multi_object)
- [单对象写入](/ch8#sec_transactions_single_object)
- [多对象事务的需求](/ch8#sec_transactions_need)
- [处理错误和中止](/ch8#handling-errors-and-aborts)
- [弱隔离级别](/ch8#sec_transactions_isolation_levels) - [弱隔离级别](/ch8#sec_transactions_isolation_levels)
- [读已提交](/ch8#sec_transactions_read_committed)
- [没有脏读](/ch8#no-dirty-reads)
- [没有脏写](/ch8#sec_transactions_dirty_write)
- [实现读已提交](/ch8#sec_transactions_read_committed_impl)
- [快照隔离与可重复读](/ch8#sec_transactions_snapshot_isolation)
- [多版本并发控制MVCC](/ch8#sec_transactions_snapshot_impl)
- [观察一致快照的可见性规则](/ch8#sec_transactions_mvcc_visibility)
- [索引与快照隔离](/ch8#indexes-and-snapshot-isolation)
- [快照隔离、可重复读和命名混淆](/ch8#snapshot-isolation-repeatable-read-and-naming-confusion)
- [防止丢失更新](/ch8#sec_transactions_lost_update)
- [原子写操作](/ch8#atomic-write-operations)
- [显式锁定](/ch8#explicit-locking)
- [自动检测丢失的更新](/ch8#automatically-detecting-lost-updates)
- [条件写入(比较并设置)](/ch8#sec_transactions_compare_and_set)
- [冲突解决与复制](/ch8#conflict-resolution-and-replication)
- [写偏斜与幻读](/ch8#sec_transactions_write_skew)
- [描述写偏斜](/ch8#characterizing-write-skew)
- [更多写偏斜的例子](/ch8#more-examples-of-write-skew)
- [导致写偏斜的幻读](/ch8#sec_transactions_phantom)
- [物化冲突](/ch8#materializing-conflicts)
- [可串行化](/ch8#sec_transactions_serializability) - [可串行化](/ch8#sec_transactions_serializability)
- [实际串行执行](/ch8#sec_transactions_serial)
- [将事务封装在存储过程中](/ch8#encapsulating-transactions-in-stored-procedures)
- [存储过程的利弊](/ch8#sec_transactions_stored_proc_tradeoffs)
- [分片](/ch8#sharding)
- [串行执行总结](/ch8#summary-of-serial-execution)
- [两阶段锁定2PL](/ch8#sec_transactions_2pl)
- [两阶段锁定的实现](/ch8#implementation-of-two-phase-locking)
- [两阶段锁定的性能](/ch8#performance-of-two-phase-locking)
- [谓词锁](/ch8#predicate-locks)
- [索引范围锁](/ch8#sec_transactions_2pl_range)
- [可串行化快照隔离SSI](/ch8#sec_transactions_ssi)
- [悲观并发控制与乐观并发控制](/ch8#pessimistic-versus-optimistic-concurrency-control)
- [基于过时前提的决策](/ch8#decisions-based-on-an-outdated-premise)
- [检测陈旧的 MVCC 读取](/ch8#detecting-stale-mvcc-reads)
- [检测影响先前读取的写入](/ch8#sec_detecting_writes_affect_reads)
- [可串行化快照隔离的性能](/ch8#performance-of-serializable-snapshot-isolation)
- [分布式事务](/ch8#sec_transactions_distributed) - [分布式事务](/ch8#sec_transactions_distributed)
- [两阶段提交2PC](/ch8#sec_transactions_2pc)
- [系统性的承诺](/ch8#a-system-of-promises)
- [协调器故障](/ch8#coordinator-failure)
- [三阶段提交](/ch8#three-phase-commit)
- [跨不同系统的分布式事务](/ch8#sec_transactions_xa)
- [精确一次消息处理](/ch8#sec_transactions_exactly_once)
- [XA 事务](/ch8#xa-transactions)
- [存疑时持有锁](/ch8#holding-locks-while-in-doubt)
- [从协调器故障中恢复](/ch8#recovering-from-coordinator-failure)
- [XA 事务的问题](/ch8#problems-with-xa-transactions)
- [数据库内部的分布式事务](/ch8#sec_transactions_internal)
- [再谈精确一次消息处理](/ch8#exactly-once-message-processing-revisited)
- [总结](/ch8#summary) - [总结](/ch8#summary)
- [参考](/ch8#参考) - [参考](/ch8#参考)
## [9. 分布式系统的麻烦](/ch9) ## [9. 分布式系统的麻烦](/ch9)
- [故障与部分失效](/ch9#sec_distributed_partial_failure) - [故障与部分失效](/ch9#sec_distributed_partial_failure)
- [不可靠的网络](/ch9#sec_distributed_networks) - [不可靠的网络](/ch9#sec_distributed_networks)
- [TCP 的局限性](/ch9#sec_distributed_tcp)
- [网络故障的实践](/ch9#sec_distributed_network_faults)
- [检测故障](/ch9#id307)
- [超时和无界延迟](/ch9#sec_distributed_queueing)
- [网络拥塞和排队](/ch9#network-congestion-and-queueing)
- [同步与异步网络](/ch9#sec_distributed_sync_networks)
- [我们不能简单地使网络延迟可预测吗?](/ch9#can-we-not-simply-make-network-delays-predictable)
- [不可靠的时钟](/ch9#sec_distributed_clocks) - [不可靠的时钟](/ch9#sec_distributed_clocks)
- [单调时钟与日历时钟](/ch9#sec_distributed_monotonic_timeofday)
- [日历时钟](/ch9#time-of-day-clocks)
- [单调时钟](/ch9#monotonic-clocks)
- [时钟同步和准确性](/ch9#sec_distributed_clock_accuracy)
- [对同步时钟的依赖](/ch9#sec_distributed_clocks_relying)
- [用于事件排序的时间戳](/ch9#sec_distributed_lww)
- [带置信区间的时钟读数](/ch9#clock-readings-with-a-confidence-interval)
- [用于全局快照的同步时钟](/ch9#sec_distributed_spanner)
- [进程暂停](/ch9#sec_distributed_clocks_pauses)
- [响应时间保证](/ch9#sec_distributed_clocks_realtime)
- [限制垃圾回收的影响](/ch9#sec_distributed_gc_impact)
- [知识、真相和谎言](/ch9#sec_distributed_truth) - [知识、真相和谎言](/ch9#sec_distributed_truth)
- [多数派原则](/ch9#sec_distributed_majority)
- [分布式锁和租约](/ch9#sec_distributed_lock_fencing)
- [隔离僵尸进程和延迟请求](/ch9#sec_distributed_fencing_tokens)
- [多副本隔离](/ch9#fencing-with-multiple-replicas)
- [拜占庭故障](/ch9#sec_distributed_byzantine)
- [弱形式的谎言](/ch9#weak-forms-of-lying)
- [系统模型与现实](/ch9#sec_distributed_system_model)
- [定义算法的正确性](/ch9#defining-the-correctness-of-an-algorithm)
- [安全性与活性](/ch9#sec_distributed_safety_liveness)
- [将系统模型映射到现实世界](/ch9#mapping-system-models-to-the-real-world)
- [形式化方法和随机测试](/ch9#sec_distributed_formal)
- [模型检查与规范语言](/ch9#model-checking-and-specification-languages)
- [故障注入](/ch9#sec_fault_injection)
- [确定性模拟测试](/ch9#deterministic-simulation-testing)
- [总结](/ch9#summary) - [总结](/ch9#summary)
- [参考](/ch9#参考)
## [10. 一致性与共识](/ch10) ## [10. 一致性与共识](/ch10)
- [线性一致性](/ch10#sec_consistency_linearizability) - [线性一致性](/ch10#sec_consistency_linearizability)
- [什么使系统具有线性一致性?](/ch10#sec_consistency_lin_definition)
- [依赖线性一致性](/ch10#sec_consistency_linearizability_usage)
- [锁定与领导者选举](/ch10#locking-and-leader-election)
- [约束与唯一性保证](/ch10#sec_consistency_uniqueness)
- [跨通道时序依赖](/ch10#cross-channel-timing-dependencies)
- [实现线性一致性系统](/ch10#sec_consistency_implementing_linearizable)
- [线性一致性与仲裁](/ch10#sec_consistency_quorum_linearizable)
- [线性一致性的代价](/ch10#sec_linearizability_cost)
- [CAP 定理](/ch10#the-cap-theorem)
- [线性一致性与网络延迟](/ch10#linearizability-and-network-delays)
- [ID 生成器和逻辑时钟](/ch10#sec_consistency_logical) - [ID 生成器和逻辑时钟](/ch10#sec_consistency_logical)
- [逻辑时钟](/ch10#sec_consistency_timestamps)
- [Lamport 时间戳](/ch10#lamport-timestamps)
- [混合逻辑时钟](/ch10#hybrid-logical-clocks)
- [Lamport/混合逻辑时钟 vs. 向量时钟](/ch10#lamporthybrid-logical-clocks-vs-vector-clocks)
- [线性一致的 ID 生成器](/ch10#sec_consistency_linearizable_id)
- [实现线性一致的 ID 生成器](/ch10#implementing-a-linearizable-id-generator)
- [使用逻辑时钟强制约束](/ch10#enforcing-constraints-using-logical-clocks)
- [共识](/ch10#sec_consistency_consensus) - [共识](/ch10#sec_consistency_consensus)
- [共识的多面性](/ch10#sec_consistency_faces)
- [单值共识](/ch10#single-value-consensus)
- [比较并设置作为共识](/ch10#compare-and-set-as-consensus)
- [共享日志作为共识](/ch10#sec_consistency_shared_logs)
- [获取并增加作为共识](/ch10#fetch-and-add-as-consensus)
- [原子提交作为共识](/ch10#atomic-commitment-as-consensus)
- [共识的实践](/ch10#sec_consistency_total_order)
- [使用共享日志](/ch10#sec_consistency_smr)
- [从单主复制到共识](/ch10#from-single-leader-replication-to-consensus)
- [共识的微妙之处](/ch10#subtleties-of-consensus)
- [共识的利弊](/ch10#pros-and-cons-of-consensus)
- [总结](/ch10#summary) - [总结](/ch10#summary)
- [参考文献](/ch10#参考文献)
## [第十一章:批处理](/ch11) ## [第十一章:批处理](/ch11)
- [使用Unix工具的批处理](/ch11#使用unix工具的批处理) - [使用Unix工具的批处理](/ch11#使用unix工具的批处理)
- [简单日志分析](/ch11#简单日志分析)
- [命令链与自定义程序](/ch11#命令链与自定义程序)
- [排序 VS 内存中的聚合](/ch11#排序-vs-内存中的聚合)
- [Unix哲学](/ch11#unix哲学)
- [统一的接口](/ch11#统一的接口)
- [逻辑与布线相分离](/ch11#逻辑与布线相分离)
- [透明度和实验](/ch11#透明度和实验)
- [MapReduce和分布式文件系统](/ch11#mapreduce和分布式文件系统) - [MapReduce和分布式文件系统](/ch11#mapreduce和分布式文件系统)
- [MapReduce作业执行](/ch11#mapreduce作业执行)
- [分布式执行MapReduce](/ch11#分布式执行mapreduce)
- [MapReduce工作流](/ch11#mapreduce工作流)
- [Reduce侧连接与分组](/ch11#reduce侧连接与分组)
- [示例:用户活动事件分析](/ch11#示例用户活动事件分析)
- [排序合并连接](/ch11#排序合并连接)
- [把相关数据放在一起](/ch11#把相关数据放在一起)
- [分组](/ch11#分组)
- [处理偏斜](/ch11#处理偏斜)
- [Map侧连接](/ch11#map侧连接)
- [广播散列连接](/ch11#广播散列连接)
- [分区散列连接](/ch11#分区散列连接)
- [Map侧合并连接](/ch11#map侧合并连接)
- [MapReduce工作流与Map侧连接](/ch11#mapreduce工作流与map侧连接)
- [批处理工作流的输出](/ch11#批处理工作流的输出)
- [建立搜索索引](/ch11#建立搜索索引)
- [键值存储作为批处理输出](/ch11#键值存储作为批处理输出)
- [批处理输出的哲学](/ch11#批处理输出的哲学)
- [Hadoop与分布式数据库的对比](/ch11#hadoop与分布式数据库的对比)
- [存储多样性](/ch11#存储多样性)
- [处理模型的多样性](/ch11#处理模型的多样性)
- [针对频繁故障设计](/ch11#针对频繁故障设计)
- [MapReduce之后](/ch11#mapreduce之后) - [MapReduce之后](/ch11#mapreduce之后)
- [物化中间状态](/ch11#物化中间状态)
- [数据流引擎](/ch11#数据流引擎)
- [容错](/ch11#容错)
- [关于物化的讨论](/ch11#关于物化的讨论)
- [图与迭代处理](/ch11#图与迭代处理)
- [Pregel处理模型](/ch11#pregel处理模型)
- [容错](/ch11#容错)
- [并行执行](/ch11#并行执行)
- [高级API和语言](/ch11#高级api和语言)
- [向声明式查询语言的转变](/ch11#向声明式查询语言的转变)
- [专业化的不同领域](/ch11#专业化的不同领域)
- [本章小结](/ch11#本章小结) - [本章小结](/ch11#本章小结)
- [参考文献](/ch11#参考文献) - [参考文献](/ch11#参考文献)
## [第十二章:流处理](/ch12) ## [第十二章:流处理](/ch12)
- [传递事件流](/ch12#传递事件流) - [传递事件流](/ch12#传递事件流)
- [消息传递系统](/ch12#消息传递系统)
- [直接从生产者传递给消费者](/ch12#直接从生产者传递给消费者)
- [消息代理](/ch12#消息代理)
- [消息代理与数据库的对比](/ch12#消息代理与数据库的对比)
- [多个消费者](/ch12#多个消费者)
- [确认与重新传递](/ch12#确认与重新传递)
- [分区日志](/ch12#分区日志)
- [使用日志进行消息存储](/ch12#使用日志进行消息存储)
- [日志与传统的消息传递相比](/ch12#日志与传统的消息传递相比)
- [消费者偏移量](/ch12#消费者偏移量)
- [磁盘空间使用](/ch12#磁盘空间使用)
- [当消费者跟不上生产者时](/ch12#当消费者跟不上生产者时)
- [重播旧消息](/ch12#重播旧消息)
- [数据库与流](/ch12#数据库与流) - [数据库与流](/ch12#数据库与流)
- [保持系统同步](/ch12#保持系统同步)
- [变更数据捕获](/ch12#变更数据捕获)
- [变更数据捕获的实现](/ch12#变更数据捕获的实现)
- [初始快照](/ch12#初始快照)
- [日志压缩](/ch12#日志压缩)
- [变更流的API支持](/ch12#变更流的api支持)
- [事件溯源](/ch12#事件溯源)
- [从事件日志中派生出当前状态](/ch12#从事件日志中派生出当前状态)
- [命令和事件](/ch12#命令和事件)
- [状态、流和不变性](/ch12#状态流和不变性)
- [不可变事件的优点](/ch12#不可变事件的优点)
- [从同一事件日志中派生多个视图](/ch12#从同一事件日志中派生多个视图)
- [并发控制](/ch12#并发控制)
- [不变性的局限性](/ch12#不变性的局限性)
- [流处理](/ch12#流处理) - [流处理](/ch12#流处理)
- [流处理的应用](/ch12#流处理的应用)
- [复合事件处理](/ch12#复合事件处理)
- [流分析](/ch12#流分析)
- [维护物化视图](/ch12#维护物化视图)
- [在流上搜索](/ch12#在流上搜索)
- [消息传递和RPC](/ch12#消息传递和rpc)
- [时间推理](/ch12#时间推理)
- [事件时间与处理时间](/ch12#事件时间与处理时间)
- [知道什么时候准备好了](/ch12#知道什么时候准备好了)
- [你用的是谁的时钟?](/ch12#你用的是谁的时钟)
- [窗口的类型](/ch12#窗口的类型)
- [流连接](/ch12#流连接)
- [流流连接(窗口连接)](/ch12#流流连接窗口连接)
- [流表连接(流扩充)](/ch12#流表连接流扩充)
- [表表连接(维护物化视图)](/ch12#表表连接维护物化视图)
- [连接的时间依赖性](/ch12#连接的时间依赖性)
- [容错](/ch12#容错)
- [微批量与存档点](/ch12#微批量与存档点)
- [原子提交再现](/ch12#原子提交再现)
- [幂等性](/ch12#幂等性)
- [失败后重建状态](/ch12#失败后重建状态)
- [本章小结](/ch12#本章小结) - [本章小结](/ch12#本章小结)
- [参考文献](/ch12#参考文献) - [参考文献](/ch12#参考文献)
## [第十三章:数据系统的未来](/ch13) ## [第十三章:数据系统的未来](/ch13)
- [数据集成](/ch13#数据集成) - [数据集成](/ch13#数据集成)
- [组合使用派生数据的工具](/ch13#组合使用派生数据的工具)
- [理解数据流](/ch13#理解数据流)
- [派生数据与分布式事务](/ch13#派生数据与分布式事务)
- [全序的限制](/ch13#全序的限制)
- [排序事件以捕获因果关系](/ch13#排序事件以捕获因果关系)
- [批处理与流处理](/ch13#批处理与流处理)
- [维护衍生状态](/ch13#维护衍生状态)
- [应用演化后重新处理数据](/ch13#应用演化后重新处理数据)
- [Lambda架构](/ch13#lambda架构)
- [统一批处理和流处理](/ch13#统一批处理和流处理)
- [分拆数据库](/ch13#分拆数据库) - [分拆数据库](/ch13#分拆数据库)
- [组合使用数据存储技术](/ch13#组合使用数据存储技术)
- [创建索引](/ch13#创建索引)
- [一切的元数据库](/ch13#一切的元数据库)
- [开展分拆工作](/ch13#开展分拆工作)
- [分拆系统vs集成系统](/ch13#分拆系统vs集成系统)
- [少了什么?](/ch13#少了什么)
- [围绕数据流设计应用](/ch13#围绕数据流设计应用)
- [应用代码作为衍生函数](/ch13#应用代码作为衍生函数)
- [应用代码和状态的分离](/ch13#应用代码和状态的分离)
- [数据流:应用代码与状态变化的交互](/ch13#数据流应用代码与状态变化的交互)
- [流处理器和服务](/ch13#流处理器和服务)
- [观察派生数据状态](/ch13#观察派生数据状态)
- [物化视图和缓存](/ch13#物化视图和缓存)
- [有状态、可离线的客户端](/ch13#有状态可离线的客户端)
- [将状态变更推送给客户端](/ch13#将状态变更推送给客户端)
- [端到端的事件流](/ch13#端到端的事件流)
- [读也是事件](/ch13#读也是事件)
- [多分区数据处理](/ch13#多分区数据处理)
- [将事情做正确](/ch13#将事情做正确) - [将事情做正确](/ch13#将事情做正确)
- [数据库的端到端原则](/ch13#数据库的端到端原则)
- [正好执行一次操作](/ch13#正好执行一次操作)
- [抑制重复](/ch13#抑制重复)
- [操作标识符](/ch13#操作标识符)
- [端到端原则](/ch13#端到端原则)
- [在数据系统中应用端到端思考](/ch13#在数据系统中应用端到端思考)
- [强制约束](/ch13#强制约束)
- [唯一性约束需要达成共识](/ch13#唯一性约束需要达成共识)
- [基于日志消息传递中的唯一性](/ch13#基于日志消息传递中的唯一性)
- [多分区请求处理](/ch13#多分区请求处理)
- [及时性与完整性](/ch13#及时性与完整性)
- [数据流系统的正确性](/ch13#数据流系统的正确性)
- [宽松地解释约束](/ch13#宽松地解释约束)
- [无协调数据系统](/ch13#无协调数据系统)
- [信任但验证](/ch13#信任但验证)
- [维护完整性尽管软件有Bug](/ch13#维护完整性尽管软件有bug)
- [不要盲目信任承诺](/ch13#不要盲目信任承诺)
- [验证的文化](/ch13#验证的文化)
- [为可审计性而设计](/ch13#为可审计性而设计)
- [端到端原则重现](/ch13#端到端原则重现)
- [用于可审计数据系统的工具](/ch13#用于可审计数据系统的工具)
- [做正确的事情](/ch13#做正确的事情) - [做正确的事情](/ch13#做正确的事情)
- [预测性分析](/ch13#预测性分析)
- [偏见与歧视](/ch13#偏见与歧视)
- [责任与问责](/ch13#责任与问责)
- [反馈循环](/ch13#反馈循环)
- [隐私和追踪](/ch13#隐私和追踪)
- [监视](/ch13#监视)
- [同意与选择的自由](/ch13#同意与选择的自由)
- [隐私与数据使用](/ch13#隐私与数据使用)
- [数据资产与权力](/ch13#数据资产与权力)
- [回顾工业革命](/ch13#回顾工业革命)
- [立法与自律](/ch13#立法与自律)
- [本章小结](/ch13#本章小结) - [本章小结](/ch13#本章小结)
- [参考文献](/ch13#参考文献) - [参考文献](/ch13#参考文献)