9 Commits

Author SHA1 Message Date
Feng Ruohang
6ec7c3452c adjust sidebar links 2025-07-31 08:08:57 +08:00
Feng Ruohang
c197b92a65 add hugo publish ci 2025-07-31 07:58:24 +08:00
Feng Ruohang
e6fc42862c add v2 stub 2025-07-31 07:57:16 +08:00
Feng Ruohang
83d41128f2 add hugo resource 2025-07-31 07:38:42 +08:00
Feng Ruohang
8f5f50977d adjust content format for hugo hextra 2025-07-31 07:28:15 +08:00
Feng Ruohang
0b9033753d adjust en-us format and move to content/ 2025-07-31 07:27:31 +08:00
Feng Ruohang
a57a51e1d9 move img to static/img 2025-07-31 07:25:02 +08:00
Feng Ruohang
b17fe0547f also fix zh-tw markdown link in hugo mode 2025-07-31 07:24:29 +08:00
Feng Ruohang
a8c07bb5e6 move zh-tw to content/tw 2025-07-31 07:24:12 +08:00
195 changed files with 7260 additions and 2637 deletions

76
.github/workflows/pages.yaml vendored Normal file
View File

@@ -0,0 +1,76 @@
# Sample workflow for building and deploying a Hugo site to GitHub Pages
name: Deploy Hugo site to Pages
on:
# Runs on pushes targeting the default branch
push:
branches: ["hugo"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
# Default to bash
defaults:
run:
shell: bash
jobs:
# Build job
build:
runs-on: ubuntu-latest
env:
HUGO_VERSION: 0.147.7
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # fetch all history for .GitInfo and .Lastmod
submodules: recursive
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Setup Pages
id: pages
uses: actions/configure-pages@v4
- name: Setup Hugo
run: |
wget -O ${{ runner.temp }}/hugo.deb https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_linux-amd64.deb \
&& sudo dpkg -i ${{ runner.temp }}/hugo.deb
- name: Build with Hugo
env:
# For maximum backward compatibility with Hugo modules
HUGO_ENVIRONMENT: production
HUGO_ENV: production
run: |
hugo \
--gc --minify \
--baseURL "${{ steps.pages.outputs.base_url }}/"
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./public
# Deployment job
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

4
.gitignore vendored
View File

@@ -3,4 +3,6 @@
__pycache__/
.DS_Store
tmp/
output/
output/
public/
.hugo_build.lock

View File

@@ -1,9 +1,14 @@
default: dev
default: doc
d:dev
dev:
hugo serve
# serve document with docsify (or python)
doc:
bin/doc
b:build
build:
hugo build
.PHONY: default d dev b build
# generate zh-tw version
translate:

178
README.md
View File

@@ -1,7 +1,6 @@
# 设计数据密集型应用 - 中文翻译版
[![Webite: ddia](https://img.shields.io/badge/v1-ddia.pigsty.io-slategray?style=flat)](https://ddia.pigsty.io)
[![Webite: ddia2](https://img.shields.io/badge/v2-ddia2.pigsty.io-slategray?style=flat)](https://ddia2.pigsty.io)
[![Webite: ddia](https://img.shields.io/badge/v1-ddia.pigsty.io-slategray?style=flat)](https://ddia.vonng.com)
[![GitHub Stars](https://img.shields.io/github/stars/Vonng/ddia?style=flat&logo=github&logoColor=black&color=slategray)](https://star-history.com/#Vonng/ddia&Date)
**作者** [Martin Kleppmann](https://martin.kleppmann.com)[《Designing Data-Intensive Applications 2nd Edition》](https://learning.oreilly.com/library/view/designing-data-intensive-applications/9781098119058/ch01.html) 英国剑桥大学分布式系统研究员,演讲者,博主和开源贡献者,软件工程师和企业家,曾在 LinkedIn 和 Rapportive 负责数据基础架构。
@@ -10,9 +9,7 @@
**校订** [@yingang](https://github.com/yingang) [繁體中文](zh-tw/README.md) **版本维护** by [@afunTW](https://github.com/afunTW)
**阅览**在本地使用 [Docsify](https://docsify.js.org/) (根目录中执行 `make` 或 [Typora](https://www.typora.io)、[Gitbook](https://vonng.gitbook.io/vonng/) 以获取最佳阅读体验。
**通知**DDIA [**第二版**](https://github.com/Vonng/ddia/tree/v2) 正在翻译中 ([`v2`](https://github.com/Vonng/ddia/tree/v2)分支),欢迎加入并提出您的宝贵意见!
**通知**DDIA [**第二版**](https://ddia.vonng.com/v2) 正在翻译中 ([`content/v2`](https://github.com/Vonng/ddia/tree/hugo/content/v2) 目录),欢迎加入并提出您的宝贵意见!
@@ -20,9 +17,7 @@
## 译序
> 不懂数据库的全栈工程师不是好架构师
>
> —— 冯若航 / Vonng
> 不懂数据库的全栈工程师不是好架构师 —— 冯若航 / [@Vonng](https://github.com/Vonng)
现今尤其是在互联网领域大多数应用都属于数据密集型应用。本书从底层数据结构到顶层架构设计将数据系统设计中的精髓娓娓道来。其中的宝贵经验无论是对架构师、DBA、还是后端工程师、甚至产品经理都会有帮助。
@@ -52,86 +47,86 @@
## 目录
### [序言](preface.md)
### [序言](content/zh/preface.md)
### [第一部分:数据系统基础](part-i.md)
### [第一部分:数据系统基础](content/zh/part-i.md)
* [第一章:可靠性、可伸缩性和可维护性](ch1.md)
* [关于数据系统的思考](ch1.md#关于数据系统的思考)
* [可靠性](ch1.md#可靠性)
* [可伸缩性](ch1.md#可伸缩性)
* [可维护性](ch1.md#可维护性)
* [本章小结](ch1.md#本章小结)
* [第二章:数据模型与查询语言](ch2.md)
* [关系模型与文档模型](ch2.md#关系模型与文档模型)
* [数据查询语言](ch2.md#数据查询语言)
* [图数据模型](ch2.md#图数据模型)
* [本章小结](ch2.md#本章小结)
* [第三章:存储与检索](ch3.md)
* [驱动数据库的数据结构](ch3.md#驱动数据库的数据结构)
* [事务处理还是分析?](ch3.md#事务处理还是分析?)
* [列式存储](ch3.md#列式存储)
* [本章小结](ch3.md#本章小结)
* [第四章:编码与演化](ch4.md)
* [编码数据的格式](ch4.md#编码数据的格式)
* [数据流的类型](ch4.md#数据流的类型)
* [本章小结](ch4.md#本章小结)
* [第一章:可靠性、可伸缩性和可维护性](content/zh/ch1.md)
* [关于数据系统的思考](content/zh/ch1.md#关于数据系统的思考)
* [可靠性](content/zh/ch1.md#可靠性)
* [可伸缩性](content/zh/ch1.md#可伸缩性)
* [可维护性](content/zh/ch1.md#可维护性)
* [本章小结](content/zh/ch1.md#本章小结)
* [第二章:数据模型与查询语言](content/zh/ch2.md)
* [关系模型与文档模型](content/zh/ch2.md#关系模型与文档模型)
* [数据查询语言](content/zh/ch2.md#数据查询语言)
* [图数据模型](content/zh/ch2.md#图数据模型)
* [本章小结](content/zh/ch2.md#本章小结)
* [第三章:存储与检索](content/zh/ch3.md)
* [驱动数据库的数据结构](content/zh/ch3.md#驱动数据库的数据结构)
* [事务处理还是分析?](content/zh/ch3.md#事务处理还是分析?)
* [列式存储](content/zh/ch3.md#列式存储)
* [本章小结](content/zh/ch3.md#本章小结)
* [第四章:编码与演化](content/zh/ch4.md)
* [编码数据的格式](content/zh/ch4.md#编码数据的格式)
* [数据流的类型](content/zh/ch4.md#数据流的类型)
* [本章小结](content/zh/ch4.md#本章小结)
### [第二部分:分布式数据](part-ii.md)
### [第二部分:分布式数据](content/zh/part-ii.md)
* [第五章:复制](ch5.md)
* [领导者与追随者](ch5.md#领导者与追随者)
* [复制延迟问题](ch5.md#复制延迟问题)
* [多主复制](ch5.md#多主复制)
* [无主复制](ch5.md#无主复制)
* [本章小结](ch5.md#本章小结)
* [第六章:分区](ch6.md)
* [分区与复制](ch6.md#分区与复制)
* [键值数据的分区](ch6.md#键值数据的分区)
* [分区与次级索引](ch6.md#分区与次级索引)
* [分区再平衡](ch6.md#分区再平衡)
* [请求路由](ch6.md#请求路由)
* [本章小结](ch6.md#本章小结)
* [第七章:事务](ch7.md)
* [事务的棘手概念](ch7.md#事务的棘手概念)
* [弱隔离级别](ch7.md#弱隔离级别)
* [可串行化](ch7.md#可串行化)
* [本章小结](ch7.md#本章小结)
* [第八章:分布式系统的麻烦](ch8.md)
* [故障与部分失效](ch8.md#故障与部分失效)
* [不可靠的网络](ch8.md#不可靠的网络)
* [不可靠的时钟](ch8.md#不可靠的时钟)
* [知识、真相与谎言](ch8.md#知识、真相与谎言)
* [本章小结](ch8.md#本章小结)
* [第九章:一致性与共识](ch9.md)
* [一致性保证](ch9.md#一致性保证)
* [线性一致性](ch9.md#线性一致性)
* [顺序保证](ch9.md#顺序保证)
* [分布式事务与共识](ch9.md#分布式事务与共识)
* [本章小结](ch9.md#本章小结)
* [第五章:复制](content/zh/ch5.md)
* [领导者与追随者](content/zh/ch5.md#领导者与追随者)
* [复制延迟问题](content/zh/ch5.md#复制延迟问题)
* [多主复制](content/zh/ch5.md#多主复制)
* [无主复制](content/zh/ch5.md#无主复制)
* [本章小结](content/zh/ch5.md#本章小结)
* [第六章:分区](content/zh/ch6.md)
* [分区与复制](content/zh/ch6.md#分区与复制)
* [键值数据的分区](content/zh/ch6.md#键值数据的分区)
* [分区与次级索引](content/zh/ch6.md#分区与次级索引)
* [分区再平衡](content/zh/ch6.md#分区再平衡)
* [请求路由](content/zh/ch6.md#请求路由)
* [本章小结](content/zh/ch6.md#本章小结)
* [第七章:事务](content/zh/ch7.md)
* [事务的棘手概念](content/zh/ch7.md#事务的棘手概念)
* [弱隔离级别](content/zh/ch7.md#弱隔离级别)
* [可串行化](content/zh/ch7.md#可串行化)
* [本章小结](content/zh/ch7.md#本章小结)
* [第八章:分布式系统的麻烦](content/zh/ch8.md)
* [故障与部分失效](content/zh/ch8.md#故障与部分失效)
* [不可靠的网络](content/zh/ch8.md#不可靠的网络)
* [不可靠的时钟](content/zh/ch8.md#不可靠的时钟)
* [知识、真相与谎言](content/zh/ch8.md#知识、真相与谎言)
* [本章小结](content/zh/ch8.md#本章小结)
* [第九章:一致性与共识](content/zh/ch9.md)
* [一致性保证](content/zh/ch9.md#一致性保证)
* [线性一致性](content/zh/ch9.md#线性一致性)
* [顺序保证](content/zh/ch9.md#顺序保证)
* [分布式事务与共识](content/zh/ch9.md#分布式事务与共识)
* [本章小结](content/zh/ch9.md#本章小结)
### [第三部分:衍生数据](part-iii.md)
### [第三部分:衍生数据](content/zh/part-iii.md)
* [第十章:批处理](ch10.md)
* [使用Unix工具的批处理](ch10.md#使用Unix工具的批处理)
* [MapReduce和分布式文件系统](ch10.md#MapReduce和分布式文件系统)
* [MapReduce之后](ch10.md#MapReduce之后)
* [本章小结](ch10.md#本章小结)
* [第十一章:流处理](ch11.md)
* [传递事件流](ch11.md#传递事件流)
* [数据库与流](ch11.md#数据库与流)
* [流处理](ch11.md#流处理)
* [本章小结](ch11.md#本章小结)
* [第十二章:数据系统的未来](ch12.md)
* [数据集成](ch12.md#数据集成)
* [分拆数据库](ch12.md#分拆数据库)
* [将事情做正确](ch12.md#将事情做正确)
* [做正确的事情](ch12.md#做正确的事情)
* [本章小结](ch12.md#本章小结)
* [第十章:批处理](content/zh/ch10.md)
* [使用Unix工具的批处理](content/zh/ch10.md#使用Unix工具的批处理)
* [MapReduce和分布式文件系统](content/zh/ch10.md#MapReduce和分布式文件系统)
* [MapReduce之后](content/zh/ch10.md#MapReduce之后)
* [本章小结](content/zh/ch10.md#本章小结)
* [第十一章:流处理](content/zh/ch11.md)
* [传递事件流](content/zh/ch11.md#传递事件流)
* [数据库与流](content/zh/ch11.md#数据库与流)
* [流处理](content/zh/ch11.md#流处理)
* [本章小结](content/zh/ch11.md#本章小结)
* [第十二章:数据系统的未来](content/zh/ch12.md)
* [数据集成](content/zh/ch12.md#数据集成)
* [分拆数据库](content/zh/ch12.md#分拆数据库)
* [将事情做正确](content/zh/ch12.md#将事情做正确)
* [做正确的事情](content/zh/ch12.md#做正确的事情)
* [本章小结](content/zh/ch12.md#本章小结)
### [术语表](glossary.md)
### [术语表](content/zh/glossary.md)
### [后记](colophon.md)
### [后记](content/zh/colophon.md)
@@ -167,16 +162,16 @@
| ISSUE & Pull Requests | USER | Title |
|-------------------------------------------------|------------------------------------------------------------|----------------------------------------------------------------|
| [359](https://github.com/Vonng/ddia/pull/359) | [@c25423](https://github.com/c25423) | ch10: 修正一处拼写错误 |
| [358](https://github.com/Vonng/ddia/pull/358) | [@lewiszlw](https://github.com/lewiszlw) | ch4: 修正一处拼写错误 |
| [356](https://github.com/Vonng/ddia/pull/356) | [@lewiszlw](https://github.com/lewiszlw) | ch2: 修正一处标点错误 |
| [355](https://github.com/Vonng/ddia/pull/355) | [@DuroyGeorge](https://github.com/DuroyGeorge) | ch12: 修正一处格式错误 |
| [354](https://github.com/Vonng/ddia/pull/354) | [@justlorain](https://github.com/justlorain) | ch7: 修正一处参考链接 |
| [353](https://github.com/Vonng/ddia/pull/353) | [@fantasyczl](https://github.com/fantasyczl) | ch3&9: 修正两处引用错误 |
| [352](https://github.com/Vonng/ddia/pull/352) | [@fantasyczl](https://github.com/fantasyczl) | 支持输出为 EPUB 格式 |
| [349](https://github.com/Vonng/ddia/pull/349) | [@xiyihan0](https://github.com/xiyihan0) | ch1: 修正一处格式错误 |
| [348](https://github.com/Vonng/ddia/pull/348) | [@omegaatt36](https://github.com/omegaatt36) | ch3: 修正一处图像链接 |
| [346](https://github.com/Vonng/ddia/issues/346) | [@Vermouth1995](https://github.com/Vermouth1995) | ch1: 优化一处翻译 |
| [359](https://github.com/Vonng/ddia/pull/359) | [@c25423](https://github.com/c25423) | ch10: 修正一处拼写错误 |
| [358](https://github.com/Vonng/ddia/pull/358) | [@lewiszlw](https://github.com/lewiszlw) | ch4: 修正一处拼写错误 |
| [356](https://github.com/Vonng/ddia/pull/356) | [@lewiszlw](https://github.com/lewiszlw) | ch2: 修正一处标点错误 |
| [355](https://github.com/Vonng/ddia/pull/355) | [@DuroyGeorge](https://github.com/DuroyGeorge) | ch12: 修正一处格式错误 |
| [354](https://github.com/Vonng/ddia/pull/354) | [@justlorain](https://github.com/justlorain) | ch7: 修正一处参考链接 |
| [353](https://github.com/Vonng/ddia/pull/353) | [@fantasyczl](https://github.com/fantasyczl) | ch3&9: 修正两处引用错误 |
| [352](https://github.com/Vonng/ddia/pull/352) | [@fantasyczl](https://github.com/fantasyczl) | 支持输出为 EPUB 格式 |
| [349](https://github.com/Vonng/ddia/pull/349) | [@xiyihan0](https://github.com/xiyihan0) | ch1: 修正一处格式错误 |
| [348](https://github.com/Vonng/ddia/pull/348) | [@omegaatt36](https://github.com/omegaatt36) | ch3: 修正一处图像链接 |
| [346](https://github.com/Vonng/ddia/issues/346) | [@Vermouth1995](https://github.com/Vermouth1995) | ch1: 优化一处翻译 |
| [343](https://github.com/Vonng/ddia/pull/343) | [@kehao-chen](https://github.com/kehao-chen) | ch10: 优化一处翻译 |
| [341](https://github.com/Vonng/ddia/pull/341) | [@YKIsTheBest](https://github.com/YKIsTheBest) | ch3: 优化两处翻译 |
| [340](https://github.com/Vonng/ddia/pull/340) | [@YKIsTheBest](https://github.com/YKIsTheBest) | ch2: 优化多处翻译 |
@@ -357,7 +352,8 @@
| [6 ](https://github.com/Vonng/ddia/pull/6) | [@MuAlex](https://github.com/MuAlex) | Ch6 change version1 |
| [5 ](https://github.com/Vonng/ddia/pull/5) | [@nevertiree](https://github.com/nevertiree) | Chapter 01语法微调 |
| [2 ](https://github.com/Vonng/ddia/pull/2) | [@seagullbird](https://github.com/seagullbird) | 序言初翻 |
</details>
</details><br>

View File

@@ -1,77 +0,0 @@
# Summary
* [简介](README.md)
* [序言](preface.md)
* [第一部分:数据系统基础](part-i.md)
* [第一章:可靠性、可伸缩性和可维护性](ch1.md)
* [关于数据系统的思考](ch1.md#关于数据系统的思考)
* [可靠性](ch1.md#可靠性)
* [可伸缩性](ch1.md#可伸缩性)
* [可维护性](ch1.md#可维护性)
* [本章小结](ch1.md#本章小结)
* [第二章:数据模型与查询语言](ch2.md)
* [关系模型与文档模型](ch2.md#关系模型与文档模型)
* [数据查询语言](ch2.md#数据查询语言)
* [图数据模型](ch2.md#图数据模型)
* [本章小结](ch2.md#本章小结)
* [第三章:存储与检索](ch3.md)
* [驱动数据库的数据结构](ch3.md#驱动数据库的数据结构)
* [事务处理还是分析?](ch3.md#事务处理还是分析?)
* [列式存储](ch3.md#列式存储)
* [本章小结](ch3.md#本章小结)
* [第四章:编码与演化](ch4.md)
* [编码数据的格式](ch4.md#编码数据的格式)
* [数据流的类型](ch4.md#数据流的类型)
* [本章小结](ch4.md#本章小结)
* [第二部分:分布式数据](part-ii.md)
* [第五章:复制](ch5.md)
* [领导者与追随者](ch5.md#领导者与追随者)
* [复制延迟问题](ch5.md#复制延迟问题)
* [多主复制](ch5.md#多主复制)
* [无主复制](ch5.md#无主复制)
* [本章小结](ch5.md#本章小结)
* [第六章:分区](ch6.md)
* [分区与复制](ch6.md#分区与复制)
* [键值数据的分区](ch6.md#键值数据的分区)
* [分区与次级索引](ch6.md#分区与次级索引)
* [分区再平衡](ch6.md#分区再平衡)
* [请求路由](ch6.md#请求路由)
* [本章小结](ch6.md#本章小结)
* [第七章:事务](ch7.md)
* [事务的棘手概念](ch7.md#事务的棘手概念)
* [弱隔离级别](ch7.md#弱隔离级别)
* [可串行化](ch7.md#可串行化)
* [本章小结](ch7.md#本章小结)
* [第八章:分布式系统的麻烦](ch8.md)
* [故障与部分失效](ch8.md#故障与部分失效)
* [不可靠的网络](ch8.md#不可靠的网络)
* [不可靠的时钟](ch8.md#不可靠的时钟)
* [知识、真相与谎言](ch8.md#知识、真相与谎言)
* [本章小结](ch8.md#本章小结)
* [第九章:一致性与共识](ch9.md)
* [一致性保证](ch9.md#一致性保证)
* [线性一致性](ch9.md#线性一致性)
* [顺序保证](ch9.md#顺序保证)
* [分布式事务与共识](ch9.md#分布式事务与共识)
* [本章小结](ch9.md#本章小结)
* [第三部分:衍生数据](part-iii.md)
* [第十章:批处理](ch10.md)
* [使用Unix工具的批处理](ch10.md#使用Unix工具的批处理)
* [MapReduce和分布式文件系统](ch10.md#MapReduce和分布式文件系统)
* [MapReduce之后](ch10.md#MapReduce之后)
* [本章小结](ch10.md#本章小结)
* [第十一章:流处理](ch11.md)
* [传递事件流](ch11.md#传递事件流)
* [数据库与流](ch11.md#数据库与流)
* [流处理](ch11.md#流处理)
* [本章小结](ch11.md#本章小结)
* [第十二章:数据系统的未来](ch12.md)
* [数据集成](ch12.md#数据集成)
* [分拆数据库](ch12.md#分拆数据库)
* [将事情做正确](ch12.md#将事情做正确)
* [做正确的事情](ch12.md#做正确的事情)
* [本章小结](ch12.md#本章小结)
* [术语表](glossary.md)
* [后记](colophon.md)

View File

@@ -1,5 +0,0 @@
- 语言版本
- [简体中文](/)
- [繁体中文](/zh-tw/)
- [英文原文](/en-us/)

View File

@@ -1,18 +0,0 @@
- [序言](preface.md)
- [第一部分:数据系统基础](part-i.md)
- [第一章:可靠性、可伸缩性和可维护性](ch1.md)
- [第二章:数据模型与查询语言](ch2.md)
- [第三章:存储与检索](ch3.md)
- [第四章:编码与演化](ch4.md)
- [第二部分:分布式数据](part-ii.md)
- [第五章:复制](ch5.md)
- [第六章:分区](ch6.md)
- [第七章:事务](ch7.md)
- [第八章:分布式系统的麻烦](ch8.md)
- [第九章:一致性与共识](ch9.md)
- [第三部分:衍生数据](part-iii.md)
- [第十章:批处理](ch10.md)
- [第十一章:流处理](ch11.md)
- [第十二章:数据系统的未来](ch12.md)
- [术语表](glossary.md)
- [后记](colophon.md)

View File

@@ -1,32 +1,62 @@
#!/usr/bin/env python3
import os, sys, opencc
import re
def process_urls(text):
"""处理 Markdown 中的相对 URL在前面添加 /tw 前缀"""
# 定义需要处理的页面路径(不带.md后缀
page_paths = [
'/ch1', '/ch2', '/ch3', '/ch4', '/ch5', '/ch6',
'/ch7', '/ch8', '/ch9', '/ch10', '/ch11', '/ch12',
'/part-i', '/part-ii', '/part-iii',
'/preface', '/glossary', '/colophon'
]
# 对每个页面路径进行替换
for page_path in page_paths:
# 匹配 Markdown 链接格式 [text](page_path) 或 [text](page_path#anchor)
pattern = rf'\[([^\]]*)\]\(({re.escape(page_path)})(#[^)]*)?\)'
# 替换为添加 /tw 前缀的版本
def replace_func(match):
text_part = match.group(1)
path_part = match.group(2)
anchor_part = match.group(3) or ''
return f'[{text_part}](/tw{path_part}{anchor_part})'
text = re.sub(pattern, replace_func, text)
return text
def convert(src_path, dst_path, cfg='s2twp.json'):
converter = opencc.OpenCC(cfg)
with open(src_path, "r", encoding='utf-8') as src, open(dst_path, "w+", encoding='utf-8') as dst:
dst.write("\n".join(
converter.convert(line.rstrip()).replace('(img/', '(../img/')
.replace('髮送', '傳送')
.replace('', '釋出')
.replace('', '發生')
.replace('', '')
.replace('嚐試', '嘗試')
.replace('線上性一致', '在線性一致') # 优先按“在线”解析了?
.replace('復雜', '複雜')
.replace('討論瞭', '討論了')
.replace('倒黴', '倒楣')
.replace('區域性性', '區域性')
.replace('下麵條件', '下面條件') # 优先按“面条”解析了?
.replace('當日志', '當日誌') # 优先按“当日”解析了,没有考虑后面的“日志”
.replace('真即時間', '真實時間') # 优先按“实时”解析了,没有考虑面的“真实”
.replace('面向物件', '物件導向')
for line in src))
process_urls(
converter.convert(line.rstrip()).replace('(img/', '(../img/')
.replace('', '傳送')
.replace('', '釋出')
.replace('', '')
.replace('髮出', '發出')
.replace('嚐試', '嘗試')
.replace('線上性一致', '在線性一致') # 优先按"在线"解析了?
.replace('復雜', '複雜')
.replace('討論瞭', '討論了')
.replace('倒黴', '倒楣')
.replace('區域性性', '區域性')
.replace('下麵條件', '下面條件') # 优先按"面条"解析了
.replace('當日志', '當日誌') # 优先按"当日"解析了,没有考虑面的"日志"
.replace('真即時間', '真實時間') # 优先按"实时"解析了,没有考虑前面的"真实"
.replace('面向物件', '物件導向')
)
for line in src))
print("convert %s to %s" % (src_path, dst_path))
if __name__ == '__main__':
print(sys.argv)
home = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(sys.argv[0])), '..'))
os.chdir(home)
for f in os.listdir():
zh_dir = os.path.join(home, 'content', 'zh')
tw_dir = os.path.join(home, 'content', 'tw')
os.chdir(zh_dir)
for f in os.listdir(zh_dir):
if f.endswith('.md'):
convert(f, "zh-tw/" + f)
dst = os.path.join(tw_dir, f)
convert(f, dst)

View File

@@ -1,4 +1,11 @@
# Designing Data-Intensive Applications
---
title: "Designing Data-Intensive Applications"
linkTitle: DDIA
cascade:
type: docs
breadcrumbs: false
---
—— **The Big Ideas Behind Reliable, Scalable, and Maintainable Systems**
@@ -24,27 +31,27 @@
## Table of Contents
### [Preface](preface.md)
### [Preface](/en/preface)
### [Part I: Foundations of Data Systems](part-i.md)
- [1. Reliable, Scalable, and Maintainable Applications](ch1.md)
- [2. Data Models and Query Languages](ch2.md)
- [3. Storage and Retrieval](ch3.md)
- [4. Encoding and Evolution](ch4.md)
### [Part I: Foundations of Data Systems](/en/part-i)
- [1. Reliable, Scalable, and Maintainable Applications](/en/ch1)
- [2. Data Models and Query Languages](/en/ch2)
- [3. Storage and Retrieval](/en/ch3)
- [4. Encoding and Evolution](/en/ch4)
### [Part II: Distributed Data](part-ii.md)
- [5. Replication](ch5.md)
- [6. Partitioning](ch6.md)
- [7. Transactions](ch7.md)
- [8. The Trouble with Distributed Systems](ch8.md)
- [9. Consistency and Consensus](ch9.md)
### [Part II: Distributed Data](/en/part-ii)
- [5. Replication](/en/ch5)
- [6. Partitioning](/en/ch6)
- [7. Transactions](/en/ch7)
- [8. The Trouble with Distributed Systems](/en/ch8)
- [9. Consistency and Consensus](/en/ch9)
### [Part III: Derived Data](part-iii.md)
- [10. Batch Processing](ch10.md)
- [11. Stream Processing](ch11.md)
- [12. The Future of Data Systems](ch12.md)
### [Part III: Derived Data](/en/part-iii)
- [10. Batch Processing](/en/ch10)
- [11. Stream Processing](/en/ch11)
- [12. The Future of Data Systems](/en/ch12)
### [Glossary](glossary.md)
### [Glossary](/en/glossary)
### [Colophon](colophon.md)
### [Colophon](/en/colophon)

View File

@@ -1,12 +1,17 @@
# 1. Reliable, Scalable, and Maintainable Applications
---
title: "1. Reliable, Scalable, and Maintainable Applications"
linkTitle: "1. Reliable, Scalable, and Maintainable Applications"
weight: 101
breadcrumbs: false
---
![](../img/ch1.png)
![](/img/ch1.png)
> *The Internet was done so well that most people think of it as a natural resource like the Pacific Ocean, rather than something that was man-made. When was the last time a tech nology with a scale like that was so error-free?*
>
> — [Alan Kay](http://www.drdobbs.com/architecture-and-design/interview-with-alan-kay/240003442), in interview with *Dr Dobbs Journal* (2012)
-----------------------
Many applications today are *data-intensive*, as opposed to *compute-intensive*. Raw CPU power is rarely a limiting factor for these applications—bigger problems are usually the amount of data, the complexity of data, and the speed at which it is changing.
@@ -46,7 +51,7 @@ An application has to meet various requirements in order to be useful. There are
There is unfortunately no easy fix for making applications reliable, scalable, or main tainable. However, there are certain patterns and techniques that keep reappearing in different kinds of applications. In the next few chapters we will take a look at some examples of data systems and analyze how they work toward those goals.
Later in the book, in [Part III](part-iii.md), we will look at patterns for systems that consist of sev eral components working together, such as the one in [Figure 1-1](../img/fig1-1.png).
Later in the book, in [Part III](/en/part-iii), we will look at patterns for systems that consist of sev eral components working together, such as the one in [Figure 1-1](/img/fig1-1.png).
@@ -85,4 +90,4 @@ Later in the book, in [Part III](part-iii.md), we will look at patterns for syst
1. Frederick P Brooks: “No Silver Bullet Essence and Accident in Software Engineering,” in *The Mythical Man-Month*, Anniversary edition, Addison-Wesley, 1995. ISBN: 978-0-201-83595-3
1. Ben Moseley and Peter Marks: “[Out of the Tar Pit](https://curtclifton.net/papers/MoseleyMarks06a.pdf),” at *BCS Software Practice Advancement* (SPA), 2006.
1. Rich Hickey: “[Simple Made Easy](http://www.infoq.com/presentations/Simple-Made-Easy),” at *Strange Loop*, September 2011.
1. Hongyu Pei Breivold, Ivica Crnkovic, and Peter J. Eriksson: “[Analyzing Software Evolvability](http://www.es.mdh.se/pdf_publications/1251.pdf),” at *32nd Annual IEEE International Computer Software and Applications Conference* (COMPSAC), July 2008. [doi:10.1109/COMPSAC.2008.50](http://dx.doi.org/10.1109/COMPSAC.2008.50)
1. Hongyu Pei Breivold, Ivica Crnkovic, and Peter J. Eriksson: “[Analyzing Software Evolvability](http://www.es.mdh.se/pdf_publications/1251.pdf),” at *32nd Annual IEEE International Computer Software and Applications Conference* (COMPSAC), July 2008. [doi:10.1109/COMPSAC.2008.50](http://dx.doi.org/10.1109/COMPSAC.2008.50)

View File

@@ -1,6 +1,11 @@
# 10. Batch Processing
---
title: "10. Batch Processing"
linkTitle: "10. Batch Processing"
weight: 310
breadcrumbs: false
---
![](../img/ch10.png)
![](/img/ch10.png)
> *A system cannot be successful if it is too strongly influenced by a single person. Once the initial design is complete and fairly robust, the real test begins as people with many different viewpoints undertake their own experiments.*
>
@@ -10,7 +15,7 @@
In the first two parts of this book we talked a lot about *requests* and *queries*, and the corresponding *responses* or *results*. This style of data processing is assumed in many modern data systems: you ask for something, or you send an instruction, and some time later the system (hopefully) gives you an answer. Databases, caches, search indexes, web servers, and many other systems work this way.
In such *online* systems, whether its a web browser requesting a page or a service call ing a remote API, we generally assume that the request is triggered by a human user, and that the user is waiting for the response. They shouldnt have to wait too long, so we pay a lot of attention to the *response time* of these systems (see “[Describing Performance](ch1.md#describing-performance)”).
In such *online* systems, whether its a web browser requesting a page or a service call ing a remote API, we generally assume that the request is triggered by a human user, and that the user is waiting for the response. They shouldnt have to wait too long, so we pay a lot of attention to the *response time* of these systems (see “[Describing Performance](/en/ch1#describing-performance)”).
The web, and increasing numbers of HTTP/REST-based APIs, has made the request/ response style of interaction so common that its easy to take it for granted. But we should remember that its not the only way of building systems, and that other approaches have their merits too. Lets distinguish three different types of systems:
@@ -24,7 +29,7 @@ A batch processing system takes a large amount of input data, runs a *job* to pr
***Stream processing systems (near-real-time systems)***
Stream processing is somewhere between online and offline/batch processing (so it is sometimes called *near-real-time* or *nearline* processing). Like a batch pro cessing system, a stream processor consumes inputs and produces outputs (rather than responding to requests). However, a stream job operates on events shortly after they happen, whereas a batch job operates on a fixed set of input data. This difference allows stream processing systems to have lower latency than the equivalent batch systems. As stream processing builds upon batch process ing, we discuss it in [Chapter 11](ch11.md).
Stream processing is somewhere between online and offline/batch processing (so it is sometimes called *near-real-time* or *nearline* processing). Like a batch pro cessing system, a stream processor consumes inputs and produces outputs (rather than responding to requests). However, a stream job operates on events shortly after they happen, whereas a batch job operates on a fixed set of input data. This difference allows stream processing systems to have lower latency than the equivalent batch systems. As stream processing builds upon batch process ing, we discuss it in [Chapter 11](/en/ch11).
As we shall see in this chapter, batch processing is an important building block in our quest to build reliable, scalable, and maintainable applications. For example, Map Reduce, a batch processing algorithm published in 2004 [1], was (perhaps over- enthusiastically) called “the algorithm that makes Google so massively scalable” [2]. It was subsequently implemented in various open source data systems, including Hadoop, CouchDB, and MongoDB.

View File

@@ -1,6 +1,12 @@
# 11. Stream Processing
---
title: "Stream Processing"
linkTitle: "11. Stream Processing"
weight: 311
breadcrumbs: false
---
![](../img/ch11.png)
![](/img/ch11.png)
> *A complex system that works is invariably found to have evolved from a simple system that works. The inverse proposition also appears to be true: A complex system designed from scratch never works and cannot be made to work.*
>
@@ -8,9 +14,9 @@
---------------
In [Chapter 10](ch10.md) we discussed batch processing—techniques that read a set of files as input and produce a new set of output files. The output is a form of *derived data*; that is, a dataset that can be recreated by running the batch process again if necessary. We saw how this simple but powerful idea can be used to create search indexes, recom mendation systems, analytics, and more.
In [Chapter 10](/en/ch10) we discussed batch processing—techniques that read a set of files as input and produce a new set of output files. The output is a form of *derived data*; that is, a dataset that can be recreated by running the batch process again if necessary. We saw how this simple but powerful idea can be used to create search indexes, recom mendation systems, analytics, and more.
However, one big assumption remained throughout [Chapter 10](ch10.md): namely, that the input is bounded—i.e., of a known and finite size—so the batch process knows when it has finished reading its input. For example, the sorting operation that is central to MapReduce must read its entire input before it can start producing output: it could happen that the very last input record is the one with the lowest key, and thus needs to be the very first output record, so starting the output early is not an option.
However, one big assumption remained throughout [Chapter 10](/en/ch10): namely, that the input is bounded—i.e., of a known and finite size—so the batch process knows when it has finished reading its input. For example, the sorting operation that is central to MapReduce must read its entire input before it can start producing output: it could happen that the very last input record is the one with the lowest key, and thus needs to be the very first output record, so starting the output early is not an option.
In reality, a lot of data is unbounded because it arrives gradually over time: your users produced data yesterday and today, and they will continue to produce more data tomorrow. Unless you go out of business, this process never ends, and so the dataset is never “complete” in any meaningful way [1]. Thus, batch processors must artifi cially divide the data into chunks of fixed duration: for example, processing a days worth of data at the end of every day, or processing an hours worth of data at the end of every hour.
@@ -27,7 +33,7 @@ In this chapter we will look at *event streams* as a data management mechanism:
## Summary
In this chapter we have discussed event streams, what purposes they serve, and how to process them. In some ways, stream processing is very much like the batch pro cessing we discussed in [Chapter 10](ch10.md), but done continuously on unbounded (neverending) streams rather than on a fixed-size input. From this perspective, message brokers and event logs serve as the streaming equivalent of a filesystem.
In this chapter we have discussed event streams, what purposes they serve, and how to process them. In some ways, stream processing is very much like the batch pro cessing we discussed in [Chapter 10](/en/ch10), but done continuously on unbounded (neverending) streams rather than on a fixed-size input. From this perspective, message brokers and event logs serve as the streaming equivalent of a filesystem.
We spent some time comparing two types of message brokers:
@@ -39,7 +45,7 @@ The broker assigns individual messages to consumers, and consumers acknowl ed
The broker assigns all messages in a partition to the same consumer node, and always delivers messages in the same order. Parallelism is achieved through par titioning, and consumers track their progress by checkpointing the offset of the last message they have processed. The broker retains messages on disk, so it is possible to jump back and reread old messages if necessary.
The log-based approach has similarities to the replication logs found in databases (see [Chapter 5](ch5.md)) and log-structured storage engines (see [Chapter 3](ch3.md)). We saw that this approach is especially appropriate for stream processing systems that consume input streams and generate derived state or derived output streams.
The log-based approach has similarities to the replication logs found in databases (see [Chapter 5](/en/ch5)) and log-structured storage engines (see [Chapter 3](/en/ch3)). We saw that this approach is especially appropriate for stream processing systems that consume input streams and generate derived state or derived output streams.
In terms of where streams come from, we discussed several possibilities: user activity events, sensors providing periodic readings, and data feeds (e.g., market data in finance) are naturally represented as streams. We saw that it can also be useful to think of the writes to a database as a stream: we can capture the changelog—i.e., the history of all changes made to a database—either implicitly through change data cap ture or explicitly through event sourcing. Log compaction allows the stream to retain a full copy of the contents of a database.

View File

@@ -1,6 +1,12 @@
# 12. The Future of Data Systems
---
title: "12. The Future of Data Systems"
linkTitle: "12. The Future of Data Systems"
weight: 312
breadcrumbs: false
---
![](../img/ch12.png)
![](/img/ch12.png)
> *If a thing be ordained to another as to its end, its last end cannot consist in the preservation of its being. Hence a captain does not intend as a last end, the preservation of the ship entrusted to him, since a ship is ordained to something else as its end, viz. to navigation.*
>
@@ -14,7 +20,7 @@ So far, this book has been mostly about describing things as they *are* at prese
Opinions and speculation about the future are of course subjective, and so I will use the first person in this chapter when writing about my personal opinions. You are welcome to disagree with them and form your own opinions, but I hope that the ideas in this chapter will at least be a starting point for a productive discussion and bring some clarity to concepts that are often confused.
The goal of this book was outlined in [Chapter 1](ch1.md): to explore how to create applications and systems that are *reliable*, *scalable*, and *maintainable*. These themes have run through all of the chapters: for example, we discussed many fault-tolerance algo rithms that help improve reliability, partitioning to improve scalability, and mecha nisms for evolution and abstraction that improve maintainability. In this chapter we will bring all of these ideas together, and build on them to envisage the future. Our goal is to discover how to design applications that are better than the ones of today— robust, correct, evolvable, and ultimately beneficial to humanity.
The goal of this book was outlined in [Chapter 1](/en/ch1): to explore how to create applications and systems that are *reliable*, *scalable*, and *maintainable*. These themes have run through all of the chapters: for example, we discussed many fault-tolerance algo rithms that help improve reliability, partitioning to improve scalability, and mecha nisms for evolution and abstraction that improve maintainability. In this chapter we will bring all of these ideas together, and build on them to envisage the future. Our goal is to discover how to design applications that are better than the ones of today— robust, correct, evolvable, and ultimately beneficial to humanity.
## ……

View File

@@ -1,6 +1,12 @@
# 2. Data Models and Query Languages
---
title: "2. Data Models and Query Languages"
linkTitle: "2. Data Models and Query Languages"
weight: 102
breadcrumbs: false
---
![](../img/ch2.png)
![](/img/ch2.png)
> *The limits of my language mean the limits of my world.*
>
@@ -23,7 +29,7 @@ There are many different kinds of data models, and every data model embodies ass
It can take a lot of effort to master just one data model (think how many books there are on relational data modeling). Building software is hard enough, even when work ing with just one data model and without worrying about its inner workings. But since the data model has such a profound effect on what the software above it can and cant do, its important to choose one that is appropriate to the application.
In this chapter we will look at a range of general-purpose data models for data stor age and querying (point 2 in the preceding list). In particular, we will compare the relational model, the document model, and a few graph-based data models. We will also look at various query languages and compare their use cases. In [Chapter 3](ch3.md) we will discuss how storage engines work; that is, how these data models are actually implemented (point 3 in the list).
In this chapter we will look at a range of general-purpose data models for data stor age and querying (point 2 in the preceding list). In particular, we will compare the relational model, the document model, and a few graph-based data models. We will also look at various query languages and compare their use cases. In [Chapter 3](/en/ch3) we will discuss how storage engines work; that is, how these data models are actually implemented (point 3 in the list).
@@ -53,7 +59,7 @@ Although we have covered a lot of ground, there are still many data models left
* Researchers working with genome data often need to perform *sequence- similarity searches*, which means taking one very long string (representing a DNA molecule) and matching it against a large database of strings that are simi lar, but not identical. None of the databases described here can handle this kind of usage, which is why researchers have written specialized genome database software like GenBank [48].
- Particle physicists have been doing Big Datastyle large-scale data analysis for decades, and projects like the Large Hadron Collider (LHC) now work with hun dreds of petabytes! At such a scale custom solutions are required to stop the hardware cost from spiraling out of control [49].
- *Full-text search* is arguably a kind of data model that is frequently used alongside databases. Information retrieval is a large specialist subject that we wont cover in great detail in this book, but well touch on search indexes in [Chapter 3](ch3.md) and [Part III](part-iii.md).
- *Full-text search* is arguably a kind of data model that is frequently used alongside databases. Information retrieval is a large specialist subject that we wont cover in great detail in this book, but well touch on search indexes in [Chapter 3](/en/ch3) and [Part III](/en/part-iii).
We have to leave it there for now. In the next chapter we will discuss some of the trade-offs that come into play when *implementing* the data models described in this chapter.

View File

@@ -1,6 +1,11 @@
# 3. Storage and Retrieval
---
title: "3. Storage and Retrieval"
linkTitle: "3. Storage and Retrieval"
weight: 103
breadcrumbs: false
---
![](../img/ch3.png)
![](/img/ch3.png)
> *Wer Ordnung hält, ist nur zu faul zum Suchen.
> (If you keep things tidily ordered, youre just too lazy to go searching.)*
@@ -11,7 +16,7 @@
On the most fundamental level, a database needs to do two things: when you give it some data, it should store the data, and when you ask it again later, it should give the data back to you.
In [Chapter 2](ch2.md) we discussed data models and query languages—i.e., the format in which you (the application developer) give the database your data, and the mecha nism by which you can ask for it again later. In this chapter we discuss the same from the databases point of view: how we can store the data that were given, and how we can find it again when were asked for it.
In [Chapter 2](/en/ch2) we discussed data models and query languages—i.e., the format in which you (the application developer) give the database your data, and the mecha nism by which you can ask for it again later. In this chapter we discuss the same from the databases point of view: how we can store the data that were given, and how we can find it again when were asked for it.
Why should you, as an application developer, care how the database handles storage and retrieval internally? Youre probably not going to implement your own storage engine from scratch, but you *do* need to select a storage engine that is appropriate for your application, from the many that are available. In order to tune a storage engine to perform well on your kind of workload, you need to have a rough idea of what the storage engine is doing under the hood.

View File

@@ -1,6 +1,11 @@
# 4. Encoding and Evolution
---
title: "4. Encoding and Evolution"
linkTitle: "4. Encoding and Evolution"
weight: 104
breadcrumbs: false
---
![](../img/ch4.png)
![](/img/ch4.png)
> *Everything changes and nothing stands still.*
>
@@ -8,11 +13,11 @@
-------------------
Applications inevitably change over time. Features are added or modified as new products are launched, user requirements become better understood, or business cir cumstances change. In [Chapter 1](ch1.mdj) we introduced the idea of *evolvability*: we should aim to build systems that make it easy to adapt to change (see “[Evolvability: Making Change Easy](ch1.md#evolvability-making-change-easy)”).
Applications inevitably change over time. Features are added or modified as new products are launched, user requirements become better understood, or business cir cumstances change. In [Chapter 1](/en/ch1j) we introduced the idea of *evolvability*: we should aim to build systems that make it easy to adapt to change (see “[Evolvability: Making Change Easy](/en/ch1#evolvability-making-change-easy)”).
In most cases, a change to an applications features also requires a change to data that it stores: perhaps a new field or record type needs to be captured, or perhaps existing data needs to be presented in a new way.
The data models we discussed in [Chapter 2](ch2.md) have different ways of coping with such change. Relational databases generally assume that all data in the database conforms to one schema: although that schema can be changed (through schema migrations; i.e., ALTER statements), there is exactly one schema in force at any one point in time. By contrast, schema-on-read (“schemaless”) databases dont enforce a schema, so the database can contain a mixture of older and newer data formats written at different times (see “[Schema flexibility in the document model](ch3.md#schema-flexibility-in-the-document-model)”).
The data models we discussed in [Chapter 2](/en/ch2) have different ways of coping with such change. Relational databases generally assume that all data in the database conforms to one schema: although that schema can be changed (through schema migrations; i.e., ALTER statements), there is exactly one schema in force at any one point in time. By contrast, schema-on-read (“schemaless”) databases dont enforce a schema, so the database can contain a mixture of older and newer data formats written at different times (see “[Schema flexibility in the document model](/en/ch3#schema-flexibility-in-the-document-model)”).
When a data format or schema changes, a corresponding change to application code often needs to happen (for example, you add a new field to a record, and the applica tion code starts reading and writing that field). However, in a large application, code changes often cannot happen instantaneously:

View File

@@ -1,6 +1,11 @@
# 5. Replication
---
title: "5. Replication"
linkTitle: "5. Replication"
weight: 205
breadcrumbs: false
---
![](../img/ch5.png)
![](/img/ch5.png)
> *The major difference between a thing that might go wrong and a thing that cannot possibly go wrong is that when a thing that cannot possibly go wrong goes wrong it usually turns out to be impossible to get at or repair.*
>
@@ -8,7 +13,7 @@
------
In [Part I](part-i.md) of this book, we discussed aspects of data systems that apply when data is stored on a single machine. Now, in [Part II](part-ii.md), we move up a level and ask: what happens if multiple machines are involved in storage and retrieval of data?
In [Part I](/en/part-i) of this book, we discussed aspects of data systems that apply when data is stored on a single machine. Now, in [Part II](/en/part-ii), we move up a level and ask: what happens if multiple machines are involved in storage and retrieval of data?
There are various reasons why you might want to distribute a database across multi ple machines:

View File

@@ -1,6 +1,12 @@
# 6. Partitioning
---
linktitle: "6. Partitioning"
linkTitle: "6. Partitioning"
weight: 206
breadcrumbs: false
---
![](../img/ch6.png)
![](/img/ch6.png)
> *Clearly, we must break away from the sequential and not limit the computers. We must state definitions and provide for priorities and descriptions of data. We must state relation ships, not procedures.*
>
@@ -10,9 +16,9 @@
In [Chapter 5](ch5.md) we discussed replication—that is, having multiple copies of the same data on different nodes. For very large datasets, or very high query throughput, that is not sufficient: we need to break the data up into *partitions*, also known as *sharding*.[^i]
In [Chapter 5](/en/ch5) we discussed replication—that is, having multiple copies of the same data on different nodes. For very large datasets, or very high query throughput, that is not sufficient: we need to break the data up into *partitions*, also known as *sharding*.[^i]
[^i]: Partitioning, as discussed in this chapter, is a way of intentionally breaking a large database down into smaller ones. It has nothing to do with *network partitions* (netsplits), a type of fault in the network between nodes. We will discuss such faults in [Chapter 8](ch8.md).
[^i]: Partitioning, as discussed in this chapter, is a way of intentionally breaking a large database down into smaller ones. It has nothing to do with *network partitions* (netsplits), a type of fault in the network between nodes. We will discuss such faults in [Chapter 8](/en/ch8).
> #### Terminological confusion
>
@@ -21,11 +27,11 @@ In [Chapter 5](ch5.md) we discussed replication—that is, having multiple copie
Normally, partitions are defined in such a way that each piece of data (each record, row, or document) belongs to exactly one partition. There are various ways of achiev ing this, which we discuss in depth in this chapter. In effect, each partition is a small database of its own, although the database may support operations that touch multi ple partitions at the same time.
The main reason for wanting to partition data is *scalability*. Different partitions can be placed on different nodes in a shared-nothing cluster (see the introduction to [Part II](part-ii.md) for a definition of *shared nothing*). Thus, a large dataset can be distributed across many disks, and the query load can be distributed across many processors.
The main reason for wanting to partition data is *scalability*. Different partitions can be placed on different nodes in a shared-nothing cluster (see the introduction to [Part II](/en/part-ii) for a definition of *shared nothing*). Thus, a large dataset can be distributed across many disks, and the query load can be distributed across many processors.
For queries that operate on a single partition, each node can independently execute the queries for its own partition, so query throughput can be scaled by adding more nodes. Large, complex queries can potentially be parallelized across many nodes, although this gets significantly harder.
Partitioned databases were pioneered in the 1980s by products such as Teradata and Tandem NonStop SQL [1], and more recently rediscovered by NoSQL databases and Hadoop-based data warehouses. Some systems are designed for transactional work loads, and others for analytics (see “[Transaction Processing or Analytics?](ch3.md#transaction-processing-or-analytics?)”): this difference affects how the system is tuned, but the fundamentals of partitioning apply to both kinds of workloads.
Partitioned databases were pioneered in the 1980s by products such as Teradata and Tandem NonStop SQL [1], and more recently rediscovered by NoSQL databases and Hadoop-based data warehouses. Some systems are designed for transactional work loads, and others for analytics (see “[Transaction Processing or Analytics?](/en/ch3#transaction-processing-or-analytics?)”): this difference affects how the system is tuned, but the fundamentals of partitioning apply to both kinds of workloads.
In this chapter we will first look at different approaches for partitioning large datasets and observe how the indexing of data interacts with partitioning. Well then talk about rebalancing, which is necessary if you want to add or remove nodes in your cluster. Finally, well get an overview of how databases route requests to the right partitions and execute queries.

View File

@@ -1,6 +1,11 @@
# 7. Transactions
---
title: "7. Transactions"
linkTitle: "7. Transactions"
weight: 207
breadcrumbs: false
---
![](../img/ch7.png)
![](/img/ch7.png)
> *Some authors have claimed that general two-phase commit is too expensive to support, because of the performance or availability problems that it brings. We believe it is better to have application programmers deal with performance problems due to overuse of transac tions as bottlenecks arise, rather than always coding around the lack of transactions.*
>
@@ -91,7 +96,7 @@ For decades this has been the standard way of implementing serializability, but
A fairly new algorithm that avoids most of the downsides of the previous approaches. It uses an optimistic approach, allowing transactions to proceed without blocking. When a transaction wants to commit, it is checked, and it is aborted if the execution was not serializable.
The examples in this chapter used a relational data model. However, as discussed in “[The need for multi-object transactions](ch7.md#the-need-for-multi-object-transactions)”, transactions are a valuable database feature, no matter which data model is used.
The examples in this chapter used a relational data model. However, as discussed in “[The need for multi-object transactions](/en/ch7#the-need-for-multi-object-transactions)”, transactions are a valuable database feature, no matter which data model is used.
In this chapter, we explored ideas and algorithms mostly in the context of a database running on a single machine. Transactions in distributed databases open a new set of difficult challenges, which well discuss in the next two chapters.

View File

@@ -1,6 +1,12 @@
# 8. The Trouble with Distributed Systems
---
title: "8. The Trouble with Distributed Systems"
linkTitle: "8. The Trouble with Distributed Systems"
weight: 208
breadcrumbs: false
---
![](../img/ch8.png)
![](/img/ch8.png)
> *Hey I just met you*
> *The networks laggy*
@@ -11,15 +17,15 @@
---------
A recurring theme in the last few chapters has been how systems handle things going wrong. For example, we discussed replica failover (“[Handling Node Outages](ch5.md#handing-node-outages)”), replication lag (“[Problems with Replication Lag](ch5.md#problems-with-replication-lag)”), and con currency control for transactions (“[Weak Isolation Levels](ch7.md#weak-isolation-levels)”). As we come to understand various edge cases that can occur in real systems, we get better at handling them.
A recurring theme in the last few chapters has been how systems handle things going wrong. For example, we discussed replica failover (“[Handling Node Outages](/en/ch5#handing-node-outages)”), replication lag (“[Problems with Replication Lag](/en/ch5#problems-with-replication-lag)”), and con currency control for transactions (“[Weak Isolation Levels](/en/ch7#weak-isolation-levels)”). As we come to understand various edge cases that can occur in real systems, we get better at handling them.
However, even though we have talked a lot about faults, the last few chapters have still been too optimistic. The reality is even darker. We will now turn our pessimism to the maximum and assume that anything that *can* go wrong *will* go wrong.[^i] (Experienced systems operators will tell you that is a reasonable assumption. If you ask nicely, they might tell you some frightening stories while nursing their scars of past battles.)
[^i]: With one exception: we will assume that faults are *non-Byzantine* (see “[Byzantine Faults](ch8.md#byzantine-faults)”).
[^i]: With one exception: we will assume that faults are *non-Byzantine* (see “[Byzantine Faults](/en/ch8#byzantine-faults)”).
Working with distributed systems is fundamentally different from writing software on a single computer—and the main difference is that there are lots of new and excit ing ways for things to go wrong [1, 2]. In this chapter, we will get a taste of the prob lems that arise in practice, and an understanding of the things we can and cannot rely on.
In the end, our task as engineers is to build systems that do their job (i.e., meet the guarantees that users are expecting), in spite of everything going wrong. In [Chapter 9](ch9.md), we will look at some examples of algorithms that can provide such guarantees in a distributed system. But first, in this chapter, we must understand what challenges we are up against.
In the end, our task as engineers is to build systems that do their job (i.e., meet the guarantees that users are expecting), in spite of everything going wrong. In [Chapter 9](/en/ch9), we will look at some examples of algorithms that can provide such guarantees in a distributed system. But first, in this chapter, we must understand what challenges we are up against.
This chapter is a thoroughly pessimistic and depressing overview of things that may go wrong in a distributed system. We will look into problems with networks (“[Unreliable Networks](#unreliable-networks)”); clocks and timing issues (“[Unreliable Clocks](#unreliable-clocks)”); and well discuss to what degree they are avoidable. The consequences of all these issues are disorienting, so well explore how to think about the state of a dis tributed system and how to reason about things that have happened (“[Knowledge, Truth, and Lies](#knowledge-truth-and-lies)”).
@@ -45,7 +51,7 @@ Once a fault is detected, making a system tolerate it is not easy either: there
If youre used to writing software in the idealized mathematical perfection of a single computer, where the same operation always deterministically returns the same result, then moving to the messy physical reality of distributed systems can be a bit of a shock. Conversely, distributed systems engineers will often regard a problem as triv ial if it can be solved on a single computer [5], and indeed a single computer can do a lot nowadays [95]. If you can avoid opening Pandoras box and simply keep things on a single machine, it is generally worth doing so.
However, as discussed in the introduction to [Part II](part-ii.md), scalability is not the only reason for wanting to use a distributed system. Fault tolerance and low latency (by placing data geographically close to users) are equally important goals, and those things can not be achieved with a single node.
However, as discussed in the introduction to [Part II](/en/part-ii), scalability is not the only reason for wanting to use a distributed system. Fault tolerance and low latency (by placing data geographically close to users) are equally important goals, and those things can not be achieved with a single node.
In this chapter we also went on some tangents to explore whether the unreliability of networks, clocks, and processes is an inevitable law of nature. We saw that it isnt: it is possible to give hard real-time response guarantees and bounded delays in net works, but doing so is very expensive and results in lower utilization of hardware resources. Most non-safety-critical systems choose cheap and unreliable over expen sive and reliable.

View File

@@ -1,6 +1,12 @@
# 9. Consistency and Consensus
---
title: "9. Consistency and Consensus"
linkTitle: "9. Consistency and Consensus"
weight: 209
breadcrumbs: false
---
![](../img/ch9.png)
![](/img/ch9.png)
> *Is it better to be alive and wrong or right and dead?*
>
@@ -8,15 +14,15 @@
---------------
Lots of things can go wrong in distributed systems, as discussed in [Chapter 8](ch8.md). The simplest way of handling such faults is to simply let the entire service fail, and show the user an error message. If that solution is unacceptable, we need to find ways of *tolerating* faults—that is, of keeping the service functioning correctly, even if some internal component is faulty.
Lots of things can go wrong in distributed systems, as discussed in [Chapter 8](/en/ch8). The simplest way of handling such faults is to simply let the entire service fail, and show the user an error message. If that solution is unacceptable, we need to find ways of *tolerating* faults—that is, of keeping the service functioning correctly, even if some internal component is faulty.
In this chapter, we will talk about some examples of algorithms and protocols for building fault-tolerant distributed systems. We will assume that all the problems from [Chapter 8](ch8.md) can occur: packets can be lost, reordered, duplicated, or arbitrarily delayed in the network; clocks are approximate at best; and nodes can pause (e.g., due to garbage collection) or crash at any time.
In this chapter, we will talk about some examples of algorithms and protocols for building fault-tolerant distributed systems. We will assume that all the problems from [Chapter 8](/en/ch8) can occur: packets can be lost, reordered, duplicated, or arbitrarily delayed in the network; clocks are approximate at best; and nodes can pause (e.g., due to garbage collection) or crash at any time.
The best way of building fault-tolerant systems is to find some general-purpose abstractions with useful guarantees, implement them once, and then let applications rely on those guarantees. This is the same approach as we used with transactions in [Chapter 7](ch7.md): by using a transaction, the application can pretend that there are no crashes (atomicity), that nobody else is concurrently accessing the database (isola tion), and that storage devices are perfectly reliable (durability). Even though crashes, race conditions, and disk failures do occur, the transaction abstraction hides those problems so that the application doesnt need to worry about them.
The best way of building fault-tolerant systems is to find some general-purpose abstractions with useful guarantees, implement them once, and then let applications rely on those guarantees. This is the same approach as we used with transactions in [Chapter 7](/en/ch7): by using a transaction, the application can pretend that there are no crashes (atomicity), that nobody else is concurrently accessing the database (isola tion), and that storage devices are perfectly reliable (durability). Even though crashes, race conditions, and disk failures do occur, the transaction abstraction hides those problems so that the application doesnt need to worry about them.
We will now continue along the same lines, and seek abstractions that can allow an application to ignore some of the problems with distributed systems. For example, one of the most important abstractions for distributed systems is *consensus*: that is, getting all of the nodes to agree on something. As we shall see in this chapter, reliably reaching consensus in spite of network faults and process failures is a surprisingly tricky problem.
Once you have an implementation of consensus, applications can use it for various purposes. For example, say you have a database with single-leader replication. If the leader dies and you need to fail over to another node, the remaining database nodes can use consensus to elect a new leader. As discussed in “[Handling Node Outages](ch5.md#handling-onde-outages)” on page 156, its important that there is only one leader, and that all nodes agree who the leader is. If two nodes both believe that they are the leader, that situation is called *split brain*, and it often leads to data loss. Correct implementations of consensus help avoid such problems.
Once you have an implementation of consensus, applications can use it for various purposes. For example, say you have a database with single-leader replication. If the leader dies and you need to fail over to another node, the remaining database nodes can use consensus to elect a new leader. As discussed in “[Handling Node Outages](/en/ch5#handling-onde-outages)” on page 156, its important that there is only one leader, and that all nodes agree who the leader is. If two nodes both believe that they are the leader, that situation is called *split brain*, and it often leads to data loss. Correct implementations of consensus help avoid such problems.
Later in this chapter, in “[Distributed Transactions and Consensus](#distributed-transactions-and-consensus)”, we will look into algorithms to solve consensus and related problems. But first we first need to explore the range of guarantees and abstractions that can be provided in a distributed system.
@@ -77,13 +83,13 @@ However, if that single leader fails, or if a network interruption makes the lea
Although a single-leader database can provide linearizability without executing a consensus algorithm on every write, it still requires consensus to maintain its leader ship and for leadership changes. Thus, in some sense, having a leader only “kicks the can down the road”: consensus is still required, only in a different place, and less fre quently. The good news is that fault-tolerant algorithms and systems for consensus exist, and we briefly discussed them in this chapter.
Tools like ZooKeeper play an important role in providing an “outsourced” consen sus, failure detection, and membership service that applications can use. Its not easy to use, but it is much better than trying to develop your own algorithms that can withstand all the problems discussed in [Chapter 8](ch8.md). If you find yourself wanting to do one of those things that is reducible to consensus, and you want it to be fault-tolerant, then it is advisable to use something like ZooKeeper.
Tools like ZooKeeper play an important role in providing an “outsourced” consen sus, failure detection, and membership service that applications can use. Its not easy to use, but it is much better than trying to develop your own algorithms that can withstand all the problems discussed in [Chapter 8](/en/ch8). If you find yourself wanting to do one of those things that is reducible to consensus, and you want it to be fault-tolerant, then it is advisable to use something like ZooKeeper.
Nevertheless, not every system necessarily requires consensus: for example, leaderless and multi-leader replication systems typically do not use global consensus. The con flicts that occur in these systems (see “[Handling Write Conflicts](ch5.md#handling-write-conflicts)”) are a consequence of not having consensus across different leaders, but maybe thats okay: maybe we simply need to cope without linearizability and learn to work better with data that has branching and merging version histories.
Nevertheless, not every system necessarily requires consensus: for example, leaderless and multi-leader replication systems typically do not use global consensus. The con flicts that occur in these systems (see “[Handling Write Conflicts](/en/ch5#handling-write-conflicts)”) are a consequence of not having consensus across different leaders, but maybe thats okay: maybe we simply need to cope without linearizability and learn to work better with data that has branching and merging version histories.
This chapter referenced a large body of research on the theory of distributed systems. Although the theoretical papers and proofs are not always easy to understand, and sometimes make unrealistic assumptions, they are incredibly valuable for informing practical work in this field: they help us reason about what can and cannot be done, and help us find the counterintuitive ways in which distributed systems are often flawed. If you have the time, the references are well worth exploring.
This brings us to the end of [Part II](part-ii.md) of this book, in which we covered replication ([Chapter 5](ch5.md)), partitioning ([Chapter 6](ch6.md)), transactions ([Chapter 7](ch7.md)), distributed system failure models ([Chapter 8](ch8.md)), and finally consistency and consensus ([Chapter 9](ch9.md)). Now that we have laid a firm foundation of theory, in [Part III](part-iii.md) we will turn once again to more practical systems, and discuss how to build powerful applications from heterogeneous building blocks.
This brings us to the end of [Part II](/en/part-ii) of this book, in which we covered replication ([Chapter 5](/en/ch5)), partitioning ([Chapter 6](/en/ch6)), transactions ([Chapter 7](/en/ch7)), distributed system failure models ([Chapter 8](/en/ch8)), and finally consistency and consensus ([Chapter 9](/en/ch9)). Now that we have laid a firm foundation of theory, in [Part III](/en/part-iii) we will turn once again to more practical systems, and discuss how to build powerful applications from heterogeneous building blocks.
## References

View File

@@ -1,4 +1,8 @@
# Colophon
---
title: Colophon
weight: 600
breadcrumbs: false
---
## About the Author

View File

@@ -1,9 +1,10 @@
# Glossary
> Please note that the definitions in this glossary are short and sim ple, intended to convey the core idea but not the full subtleties of a term. For more detail, please follow the references into the main text.
[TOC]
---
title: Glossary
weight: 500
breadcrumbs: false
---
> Please note that the definitions in this glossary are short and simple, intended to convey the core idea but not the full subtleties of a term. For more detail, please follow the references into the main text.
### asynchronous

25
content/en/part-i.md Normal file
View File

@@ -0,0 +1,25 @@
---
title: "PART I: Foundations of Data Systems"
weight: 100
breadcrumbs: false
---
The first four chapters go through the fundamental ideas that apply to all data sys tems, whether running on a single machine or distributed across a cluster of machines:
1. [Chapter 1](/en/ch1) introduces the terminology and approach that were going to use throughout this book. It examines what we actually mean by words like *reliabil ity*, *scalability*, and *maintainability*, and how we can try to achieve these goals.
2. [Chapter 2](/en/ch2) compares several different data models and query languages—the most visible distinguishing factor between databases from a developers point of view. We will see how different models are appropriate to different situations.
3. [Chapter 3](/en/ch4) turns to the internals of storage engines and looks at how databases lay out data on disk. Different storage engines are optimized for different workloads, and choosing the right one can have a huge effect on performance.
4. [Chapter 4](/en/ch4) compares various formats for data encoding (serialization) and espe cially examines how they fare in an environment where application requirements change and schemas need to adapt over time.
Later, [Part II](/en/part-ii) will turn to the particular issues of distributed data systems.
## Index
- [1. Reliable, Scalable, and Maintainable Applications](/en/ch1)
- [2. Data Models and Query Languages](/en/ch2)
- [3. Storage and Retrieval](/en/ch3)
- [4. Encoding and Evolution](/en/ch4)

View File

@@ -1,4 +1,8 @@
# PART II: Distributed Data
---
title: "PART II: Distributed Data"
weight: 200
breadcrumbs: false
---
> *For a successful technology, reality must take precedence over public relations, for nature cannot be fooled.*
>
@@ -6,7 +10,7 @@
-------
In [Part I](part-i.md) of this book, we discussed aspects of data systems that apply when data is stored on a single machine. Now, in [Part II](part-ii.md), we move up a level and ask: what hap pens if multiple machines are involved in storage and retrieval of data?
In [Part I](/en/part-i) of this book, we discussed aspects of data systems that apply when data is stored on a single machine. Now, in [Part II](/en/part-ii), we move up a level and ask: what hap pens if multiple machines are involved in storage and retrieval of data?
There are various reasons why you might want to distribute a database across multi ple machines:
@@ -56,15 +60,15 @@ There are two common ways data is distributed across multiple nodes:
***Replication***
Keeping a copy of the same data on several different nodes, potentially in differ ent locations. Replication provides redundancy: if some nodes are unavailable, the data can still be served from the remaining nodes. Replication can also help improve performance. We discuss replication in [Chapter 5](ch5.md).
Keeping a copy of the same data on several different nodes, potentially in differ ent locations. Replication provides redundancy: if some nodes are unavailable, the data can still be served from the remaining nodes. Replication can also help improve performance. We discuss replication in [Chapter 5](/en/ch5).
***Partitioning***
Splitting a big database into smaller subsets called *partitions* so that different par titions can be assigned to different nodes (also known as *sharding*). We discuss partitioning in [Chapter 6](ch6.md).
Splitting a big database into smaller subsets called *partitions* so that different par titions can be assigned to different nodes (also known as *sharding*). We discuss partitioning in [Chapter 6](/en/ch6).
These are separate mechanisms, but they often go hand in hand, as illustrated in Figure II-1.
![](../img/figii-1.png)
![](/img/figii-1.png)
> *Figure II-1. A database split into two partitions, with two replicas per partition.*
@@ -73,6 +77,13 @@ With an understanding of those concepts, we can discuss the difficult trade-offs
Later, in Part III of this book, we will discuss how you can take several (potentially distributed) datastores and integrate them into a larger system, satisfying the needs of a complex application. But first, lets talk about distributed data.
## Index
- [5. Replication](/en/ch5)
- [6. Partitioning](/en/ch6)
- [7. Transactions](/en/ch7)
- [8. The Trouble with Distributed Systems](/en/ch8)
- [9. Consistency and Consensus](/en/ch9)
## References

View File

@@ -1,6 +1,10 @@
# PART III: Derived Data
---
title: "PART III: Derived Data"
weight: 300
breadcrumbs: false
---
In Parts [I](part-i.md) and [II](part-ii.md) of this book, we assembled from the ground up all the major consid erations that go into a distributed database, from the layout of data on disk all the way to the limits of distributed consistency in the presence of faults. However, this discussion assumed that there was only one database in the application.
In Parts [I](/en/part-i) and [II](/en/part-ii) of this book, we assembled from the ground up all the major consid erations that go into a distributed database, from the layout of data on disk all the way to the limits of distributed consistency in the presence of faults. However, this discussion assumed that there was only one database in the application.
In reality, data systems are often more complex. In a large application you often need to be able to access and process data in many different ways, and there is no one data base that can satisfy all those different needs simultaneously. Applications thus com monly use a combination of several different datastores, indexes, caches, analytics systems, etc. and implement mechanisms for moving data from one store to another.
@@ -34,5 +38,11 @@ By being clear about which data is derived from which other data, you can bring
## Overview of Chapters
We will start in [Chapter 10](ch10.md) by examining batch-oriented dataflow systems such as MapReduce, and see how they give us good tools and principles for building large- scale data systems. In [Chapter 11](ch11.md) we will take those ideas and apply them to data streams, which allow us to do the same kinds of things with lower delays. [Chapter 12](ch12.md) concludes the book by exploring ideas about how we might use these tools to build reliable, scalable, and maintainable applications in the future.
We will start in [Chapter 10](/en/ch10) by examining batch-oriented dataflow systems such as MapReduce, and see how they give us good tools and principles for building large- scale data systems. In [Chapter 11](/en/ch11) we will take those ideas and apply them to data streams, which allow us to do the same kinds of things with lower delays. [Chapter 12](/en/ch12) concludes the book by exploring ideas about how we might use these tools to build reliable, scalable, and maintainable applications in the future.
## Index
- [10. Batch Processing](/en/ch10)
- [11. Stream Processing](/en/ch11)
- [12. The Future of Data Systems](/en/ch12)

View File

@@ -1,4 +1,8 @@
# Preface
---
title: Preface
weight: 50
breadcrumbs: false
---
If you have worked in software engineering in recent years, especially in server-side and backend systems, you have probably been bombarded with a plethora of buzz words relating to storage and processing of data. NoSQL! Big Data! Web-scale! Sharding! Eventual consistency! ACID! CAP theorem! Cloud services! MapReduce! Real-time!
@@ -63,10 +67,10 @@ This book has a bias toward free and open source software (FOSS), because readin
This book is arranged into three parts:
1. In [Part I](part-i.md), we discuss the fundamental ideas that underpin the design of data- intensive applications. We start in [Chapter 1](ch1.md) by discussing what were actually trying to achieve: reliability, scalability, and maintainability; how we need to think about them; and how we can achieve them. In [Chapter 2](ch2.md) we compare sev eral different data models and query languages, and see how they are appropriate to different situations. In [Chapter 3](ch3.md) we talk about storage engines: how databases arrange data on disk so that we can find it again efficiently. [Chapter 4](ch4.md) turns to formats for data encoding (serialization) and evolution of schemas over time.
2. [In Part II](part-ii.md), we move from data stored on one machine to data that is distributed across multiple machines. This is often necessary for scalability, but brings with it a variety of unique challenges. We first discuss replication ([Chapter 5](ch5.md)), parti tioning/sharding ([Chapter 6](ch6.md)), and transactions ([Chapter 7](ch7.md)). We then go into more detail on the problems with distributed systems ([Chapter 8](ch8.md)) and what it means to achieve consistency and consensus in a distributed system ([Chapter 9](ch9.md)).
1. In [Part I](/en/part-i), we discuss the fundamental ideas that underpin the design of data- intensive applications. We start in [Chapter 1](/en/ch1) by discussing what were actually trying to achieve: reliability, scalability, and maintainability; how we need to think about them; and how we can achieve them. In [Chapter 2](/en/ch2) we compare sev eral different data models and query languages, and see how they are appropriate to different situations. In [Chapter 3](/en/ch3) we talk about storage engines: how databases arrange data on disk so that we can find it again efficiently. [Chapter 4](/en/ch4) turns to formats for data encoding (serialization) and evolution of schemas over time.
2. [In Part II](/en/part-ii), we move from data stored on one machine to data that is distributed across multiple machines. This is often necessary for scalability, but brings with it a variety of unique challenges. We first discuss replication ([Chapter 5](/en/ch5)), parti tioning/sharding ([Chapter 6](/en/ch6)), and transactions ([Chapter 7](/en/ch7)). We then go into more detail on the problems with distributed systems ([Chapter 8](/en/ch8)) and what it means to achieve consistency and consensus in a distributed system ([Chapter 9](/en/ch9)).
3. In [Part III](part-iii.md), we discuss systems that derive some datasets from other datasets. Derived data often occurs in heterogeneous systems: when there is no one data base that can do everything well, applications need to integrate several different databases, caches, indexes, and so on. In [Chapter 10](ch10.md) we start with a batch pro cessing approach to derived data, and we build upon it with stream processing in [Chapter 11](ch11.md). Finally, in [Chapter 12](ch12.md) we put everything together and discuss approaches for building reliable, scalable, and maintainable applications in the future.
3. In [Part III](/en/part-iii), we discuss systems that derive some datasets from other datasets. Derived data often occurs in heterogeneous systems: when there is no one data base that can do everything well, applications need to integrate several different databases, caches, indexes, and so on. In [Chapter 10](/en/ch10) we start with a batch pro cessing approach to derived data, and we build upon it with stream processing in [Chapter 11](/en/ch11). Finally, in [Chapter 12](/en/ch12) we put everything together and discuss approaches for building reliable, scalable, and maintainable applications in the future.

28
content/en/toc.md Normal file
View File

@@ -0,0 +1,28 @@
---
title: "Table of Content"
linkTitle: "Table of Content"
weight: 10
breadcrumbs: false
---
![](/img/title.png)
* [Preface](/en/preface)
* [Part I: Foundations of Data Systems](/en/part-i)
- [1. Reliable, Scalable, and Maintainable Applications](/en/ch1)
- [2. Data Models and Query Languages](/en/ch2)
- [3. Storage and Retrieval](/en/ch3)
- [4. Encoding and Evolution](/en/ch4)
* [Part II: Distributed Data](/en/part-ii)
- [5. Replication](/en/ch5)
- [6. Partitioning](/en/ch6)
- [7. Transactions](/en/ch7)
- [8. The Trouble with Distributed Systems](/en/ch8)
- [9. Consistency and Consensus](/en/ch9)
* [Part III: Derived Data](/en/part-iii)
- [10. Batch Processing](/en/ch10)
- [11. Stream Processing](/en/ch11)
- [12. The Future of Data Systems](/en/ch12)
* [Glossary](/en/glossary)
* [Colophon](/en/colophon)

View File

@@ -1,28 +1,29 @@
# 設計資料密集型應用 - 中文翻譯版
---
title: 設計資料密集型應用
linkTitle: DDIA
cascade:
type: docs
breadcrumbs: false
---
[![Webite: ddia](https://img.shields.io/badge/v1-ddia.pigsty.io-slategray?style=flat)](https://ddia.pigsty.io)
[![Webite: ddia2](https://img.shields.io/badge/v2-ddia2.pigsty.io-slategray?style=flat)](https://ddia2.pigsty.io)
[![GitHub Stars](https://img.shields.io/github/stars/Vonng/ddia?style=flat&logo=github&logoColor=black&color=slategray)](https://star-history.com/#Vonng/ddia&Date)
**作者** [Martin Kleppmann](https://martin.kleppmann.com)[《Designing Data-Intensive Applications 2nd Edition》](https://learning.oreilly.com/library/view/designing-data-intensive-applications/9781098119058/ch01.html) 英國劍橋大學分散式系統研究員,演講者,博主和開源貢獻者,軟體工程師和企業家,曾在 LinkedIn 和 Rapportive 負責資料基礎架構。
**譯者**[馮若航](https://vonng.com) / [Vonng](https://github.com/Vonng) (rh@vonng.com) 創業者,[開源貢獻者](https://gitstar-ranking.com/Vonng)PostgreSQL Hacker。開源 RDS PG [Pigsty](https://pigsty.cc/zh/) 與公眾號《[非法加馮](https://mp.weixin.qq.com/s/p4Ys10ZdEDAuqNAiRmcnIQ)》作者,[資料庫老司機](https://pigsty.cc/zh/blog/db)[雲計算泥石流](https://pigsty.cc/zh/blog/cloud)曾於阿里蘋果探探擔任架構師與DBA
**譯者**[**馮若航**](https://vonng.com),網名 [@Vonng](https://github.com/Vonng)。
PostgreSQL 專家,資料庫老司機,雲計算泥石流。
[**Pigsty**](https://pgsty.com) 作者與創始人。
架構師DBA全棧工程師 @ TanTanAlibabaApple。
獨立開源貢獻者,[GitStar Ranking 585](https://gitstar-ranking.com/Vonng)[國區活躍 Top20](https://committers.top/china)。
[DDIA](https://ddia.pigsty.io) / [PG Internal](https://pgint.vonng.com) 中文版譯者,公眾號:《老馮雲數》,資料庫 KOL。
**校訂** [@yingang](https://github.com/yingang) [繁體中文](zh-tw/README.md) **版本維護** by [@afunTW](https://github.com/afunTW)
**校訂** [@yingang](https://github.com/yingang) [繁體中文](/tw) **版本維護** by [@afunTW](https://github.com/afunTW) [完整貢獻者列表](/contrib)
**閱覽**:在本地使用 [Docsify](https://docsify.js.org/) (根目錄中執行 `make` 或 [Typora](https://www.typora.io)、[Gitbook](https://vonng.gitbook.io/vonng/) 以獲取最佳閱讀體驗。
**通知**DDIA [**第二版**](https://github.com/Vonng/ddia/tree/v2) 正在翻譯中 ([`v2`](https://github.com/Vonng/ddia/tree/v2)分支),歡迎加入並提出您的寶貴意見!
---------
> [!NOTE]
> DDIA [**第二版**](https://github.com/Vonng/ddia/tree/v2) 正在翻譯中 ([`v2`](https://github.com/Vonng/ddia/tree/v2)分支),歡迎加入並提出您的寶貴意見!
## 譯序
> 不懂資料庫的全棧工程師不是好架構師
>
> —— 馮若航 / Vonng
> 不懂資料庫的全棧工程師不是好架構師 —— 馮若航 / Vonng
現今尤其是在網際網路領域大多數應用都屬於資料密集型應用。本書從底層資料結構到頂層架構設計將資料系統設計中的精髓娓娓道來。其中的寶貴經驗無論是對架構師、DBA、還是後端工程師、甚至產品經理都會有幫助。
@@ -30,110 +31,53 @@
這也是一本深入淺出的書,講述概念的來龍去脈而不是賣弄定義,介紹事物發展演化歷程而不是事實堆砌,將複雜的概念講述的淺顯易懂,但又直擊本質不失深度。每章最後的引用質量非常好,是深入學習各個主題的絕佳索引。
本書為資料系統的設計、實現、與評價提供了很好的概念框架。讀完並理解本書內容後,讀者可以輕鬆看破大多數的技術忽悠,與技術磚家撕起來虎虎生風🤣
本書為資料系統的設計、實現、與評價提供了很好的概念框架。讀完並理解本書內容後,讀者可以輕鬆看破大多數的技術忽悠,與技術磚家撕起來虎虎生風。
這是 2017 年譯者讀過最好的一本技術類書籍,這麼好的書沒有中文翻譯,實在是遺憾。某不才,願為先進技術文化的傳播貢獻一份力量。既可以深入學習有趣的技術主題,又可以鍛鍊中英文語言文字功底,何樂而不為?
---------
## 前言
> 在我們的社會中,技術是一種強大的力量。資料、軟體、通訊可以用於壞的方面:不公平的階級固化,損害公民權利,保護既得利益集團。但也可以用於好的方面:讓底層人民發出自己的聲音,讓每個人都擁有機會,避免災難。本書獻給所有將技術用於善途的人們。
---------
> 計算是一種流行文化,流行文化鄙視歷史。流行文化關乎個體身份和參與感,但與合作無關。流行文化活在當下,也與過去和未來無關。我認為大部分(為了錢)編寫程式碼的人就是這樣的,他們不知道自己的文化來自哪裡。
>
> —— 阿蘭・凱接受 Dobb 博士的雜誌採訪時2012 年)
---------
## 目錄
### [序言](preface.md)
### [序言](/tw/preface)
### [第一部分:資料系統基礎](part-i.md)
### [第一部分:資料系統基礎](/tw/part-i)
* [第一章:可靠性、可伸縮性和可維護性](ch1.md)
* [關於資料系統的思考](ch1.md#關於資料系統的思考)
* [可靠性](ch1.md#可靠性)
* [可伸縮性](ch1.md#可伸縮性)
* [可維護性](ch1.md#可維護性)
* [本章小結](ch1.md#本章小結)
* [第二章:資料模型與查詢語言](ch2.md)
* [關係模型與文件模型](ch2.md#關係模型與文件模型)
* [資料查詢語言](ch2.md#資料查詢語言)
* [圖資料模型](ch2.md#圖資料模型)
* [本章小結](ch2.md#本章小結)
* [第三章:儲存與檢索](ch3.md)
* [驅動資料庫的資料結構](ch3.md#驅動資料庫的資料結構)
* [事務處理還是分析?](ch3.md#事務處理還是分析?)
* [列式儲存](ch3.md#列式儲存)
* [本章小結](ch3.md#本章小結)
* [第四章:編碼與演化](ch4.md)
* [編碼資料的格式](ch4.md#編碼資料的格式)
* [資料流的型別](ch4.md#資料流的型別)
* [本章小結](ch4.md#本章小結)
* [第一章:可靠性、可伸縮性和可維護性](/tw/ch1)
* [第二章:資料模型與查詢語言](/tw/ch2)
* [第三章:儲存與檢索](/tw/ch3)
* [第四章:編碼與演化](/tw/ch4)
### [第二部分:分散式資料](part-ii.md)
### [第二部分:分散式資料](/tw/part-ii)
* [第五章:複製](ch5.md)
* [領導者與追隨者](ch5.md#領導者與追隨者)
* [複製延遲問題](ch5.md#複製延遲問題)
* [多主複製](ch5.md#多主複製)
* [無主複製](ch5.md#無主複製)
* [本章小結](ch5.md#本章小結)
* [第六章:分割槽](ch6.md)
* [分割槽與複製](ch6.md#分割槽與複製)
* [鍵值資料的分割槽](ch6.md#鍵值資料的分割槽)
* [分割槽與次級索引](ch6.md#分割槽與次級索引)
* [分割槽再平衡](ch6.md#分割槽再平衡)
* [請求路由](ch6.md#請求路由)
* [本章小結](ch6.md#本章小結)
* [第七章:事務](ch7.md)
* [事務的棘手概念](ch7.md#事務的棘手概念)
* [弱隔離級別](ch7.md#弱隔離級別)
* [可序列化](ch7.md#可序列化)
* [本章小結](ch7.md#本章小結)
* [第八章:分散式系統的麻煩](ch8.md)
* [故障與部分失效](ch8.md#故障與部分失效)
* [不可靠的網路](ch8.md#不可靠的網路)
* [不可靠的時鐘](ch8.md#不可靠的時鐘)
* [知識、真相與謊言](ch8.md#知識、真相與謊言)
* [本章小結](ch8.md#本章小結)
* [第九章:一致性與共識](ch9.md)
* [一致性保證](ch9.md#一致性保證)
* [線性一致性](ch9.md#線性一致性)
* [順序保證](ch9.md#順序保證)
* [分散式事務與共識](ch9.md#分散式事務與共識)
* [本章小結](ch9.md#本章小結)
* [第五章:複製](/tw/ch5)
* [第六章:分割槽](/tw/ch6)
* [第七章:事務](/tw/ch7)
* [第八章:分散式系統的麻煩](/tw/ch8)
* [第九章:一致性與共識](/tw/ch9)
### [第三部分:衍生資料](part-iii.md)
### [第三部分:衍生資料](/tw/part-iii)
* [第十章:批處理](ch10.md)
* [使用Unix工具的批處理](ch10.md#使用Unix工具的批處理)
* [MapReduce和分散式檔案系統](ch10.md#MapReduce和分散式檔案系統)
* [MapReduce之後](ch10.md#MapReduce之後)
* [本章小結](ch10.md#本章小結)
* [第十一章:流處理](ch11.md)
* [傳遞事件流](ch11.md#傳遞事件流)
* [資料庫與流](ch11.md#資料庫與流)
* [流處理](ch11.md#流處理)
* [本章小結](ch11.md#本章小結)
* [第十二章:資料系統的未來](ch12.md)
* [資料整合](ch12.md#資料整合)
* [分拆資料庫](ch12.md#分拆資料庫)
* [將事情做正確](ch12.md#將事情做正確)
* [做正確的事情](ch12.md#做正確的事情)
* [本章小結](ch12.md#本章小結)
* [第十章:批處理](/tw/ch10)
* [第十一章:流處理](/tw/ch11)
* [第十二章:資料系統的未來](/tw/ch12)
### [術語表](glossary.md)
### [後記](colophon.md)
### [術語表](/tw/glossary)
### [後記](/tw/colophon)
<br>
---------
@@ -156,27 +100,27 @@
1. [序言初翻修正](https://github.com/Vonng/ddia/commit/afb5edab55c62ed23474149f229677e3b42dfc2c) by [@seagullbird](https://github.com/Vonng/ddia/commits?author=seagullbird)
2. [第一章語法標點校正](https://github.com/Vonng/ddia/commit/973b12cd8f8fcdf4852f1eb1649ddd9d187e3644) by [@nevertiree](https://github.com/Vonng/ddia/commits?author=nevertiree)
3. [第六章部分校正](https://github.com/Vonng/ddia/commit/d4eb0852c0ec1e93c8aacc496c80b915bb1e6d48) 與[第十章的初翻](https://github.com/Vonng/ddia/commit/9de8dbd1bfe6fbb03b3bf6c1a1aa2291aed2490e) by [@MuAlex](https://github.com/Vonng/ddia/commits?author=MuAlex)
4. [第一部分](part-i.md)前言,[ch2](ch2.md)校正 by [@jiajiadebug](https://github.com/Vonng/ddia/commits?author=jiajiadebug)
5. [詞彙表](glossary.md)、[後記](colophon.md)關於野豬的部分 by [@Chowss](https://github.com/Vonng/ddia/commits?author=Chowss)
4. [第一部分](/tw/part-i)前言,[ch2](/tw/ch2)校正 by [@jiajiadebug](https://github.com/Vonng/ddia/commits?author=jiajiadebug)
5. [詞彙表](/tw/glossary)、[後記](/tw/colophon)關於野豬的部分 by [@Chowss](https://github.com/Vonng/ddia/commits?author=Chowss)
6. [繁體中文](https://github.com/Vonng/ddia/pulls)版本與轉換指令碼 by [@afunTW](https://github.com/afunTW)
7. 多處翻譯修正 by [@songzhibin97](https://github.com/Vonng/ddia/commits?author=songzhibin97) [@MamaShip](https://github.com/Vonng/ddia/commits?author=MamaShip) [@FangYuan33](https://github.com/Vonng/ddia/commits?author=FangYuan33)
8. 感謝所有作出貢獻,提出意見的朋友們:
8. [感謝所有作出貢獻,提出意見的朋友們](/contrib)
<details>
<summary><a href="https://github.com/Vonng/ddia/pulls">Pull Requests</a> & <a href="https://github.com/Vonng/ddia/issues">Issues</a></summary>
| ISSUE & Pull Requests | USER | Title |
|-------------------------------------------------|------------------------------------------------------------|----------------------------------------------------------------|
| [359](https://github.com/Vonng/ddia/pull/359) | [@c25423](https://github.com/c25423) | ch10: 修正一處拼寫錯誤 |
| [358](https://github.com/Vonng/ddia/pull/358) | [@lewiszlw](https://github.com/lewiszlw) | ch4: 修正一處拼寫錯誤 |
| [356](https://github.com/Vonng/ddia/pull/356) | [@lewiszlw](https://github.com/lewiszlw) | ch2: 修正一處標點錯誤 |
| [355](https://github.com/Vonng/ddia/pull/355) | [@DuroyGeorge](https://github.com/DuroyGeorge) | ch12: 修正一處格式錯誤 |
| [354](https://github.com/Vonng/ddia/pull/354) | [@justlorain](https://github.com/justlorain) | ch7: 修正一處參考連結 |
| [353](https://github.com/Vonng/ddia/pull/353) | [@fantasyczl](https://github.com/fantasyczl) | ch3&9: 修正兩處引用錯誤 |
| [352](https://github.com/Vonng/ddia/pull/352) | [@fantasyczl](https://github.com/fantasyczl) | 支援輸出為 EPUB 格式 |
| [349](https://github.com/Vonng/ddia/pull/349) | [@xiyihan0](https://github.com/xiyihan0) | ch1: 修正一處格式錯誤 |
| [348](https://github.com/Vonng/ddia/pull/348) | [@omegaatt36](https://github.com/omegaatt36) | ch3: 修正一處影像連結 |
| [346](https://github.com/Vonng/ddia/issues/346) | [@Vermouth1995](https://github.com/Vermouth1995) | ch1: 最佳化一處翻譯 |
| [359](https://github.com/Vonng/ddia/pull/359) | [@c25423](https://github.com/c25423) | ch10: 修正一處拼寫錯誤 |
| [358](https://github.com/Vonng/ddia/pull/358) | [@lewiszlw](https://github.com/lewiszlw) | ch4: 修正一處拼寫錯誤 |
| [356](https://github.com/Vonng/ddia/pull/356) | [@lewiszlw](https://github.com/lewiszlw) | ch2: 修正一處標點錯誤 |
| [355](https://github.com/Vonng/ddia/pull/355) | [@DuroyGeorge](https://github.com/DuroyGeorge) | ch12: 修正一處格式錯誤 |
| [354](https://github.com/Vonng/ddia/pull/354) | [@justlorain](https://github.com/justlorain) | ch7: 修正一處參考連結 |
| [353](https://github.com/Vonng/ddia/pull/353) | [@fantasyczl](https://github.com/fantasyczl) | ch3&9: 修正兩處引用錯誤 |
| [352](https://github.com/Vonng/ddia/pull/352) | [@fantasyczl](https://github.com/fantasyczl) | 支援輸出為 EPUB 格式 |
| [349](https://github.com/Vonng/ddia/pull/349) | [@xiyihan0](https://github.com/xiyihan0) | ch1: 修正一處格式錯誤 |
| [348](https://github.com/Vonng/ddia/pull/348) | [@omegaatt36](https://github.com/omegaatt36) | ch3: 修正一處影像連結 |
| [346](https://github.com/Vonng/ddia/issues/346) | [@Vermouth1995](https://github.com/Vermouth1995) | ch1: 最佳化一處翻譯 |
| [343](https://github.com/Vonng/ddia/pull/343) | [@kehao-chen](https://github.com/kehao-chen) | ch10: 最佳化一處翻譯 |
| [341](https://github.com/Vonng/ddia/pull/341) | [@YKIsTheBest](https://github.com/YKIsTheBest) | ch3: 最佳化兩處翻譯 |
| [340](https://github.com/Vonng/ddia/pull/340) | [@YKIsTheBest](https://github.com/YKIsTheBest) | ch2: 最佳化多處翻譯 |
@@ -357,12 +301,15 @@
| [6 ](https://github.com/Vonng/ddia/pull/6) | [@MuAlex](https://github.com/MuAlex) | Ch6 change version1 |
| [5 ](https://github.com/Vonng/ddia/pull/5) | [@nevertiree](https://github.com/nevertiree) | Chapter 01語法微調 |
| [2 ](https://github.com/Vonng/ddia/pull/2) | [@seagullbird](https://github.com/seagullbird) | 序言初翻 |
</details>
</details><br />
---------
## 協議
## 許可證
[![License: CC-BY 4.0](https://img.shields.io/github/license/Vonng/ddia?logo=opensourceinitiative&logoColor=green&color=slategray)](https://github.com/Vonng/ddia/blob/master/LICENSE)
本專案採用 [CC-BY 4.0](https://github.com/Vonng/ddia/blob/master/LICENSE) 許可證,您可以在這裡找到完整說明:
- [署名 4.0 協議國際版 CC BY 4.0 Deed](https://creativecommons.org/licenses/by/4.0/deed.zh-hans)
- [Attribution 4.0 International CC BY 4.0](https://creativecommons.org/licenses/by/4.0/deed.en)

View File

@@ -1,15 +1,17 @@
# 第一章:可靠性、可伸縮性和可維護性
---
title: "第一章:可靠性、可伸縮性和可維護性"
linkTitle: "1. 可靠性、可伸縮性和可維護性"
weight: 101
breadcrumbs: false
---
![](../img/ch1.png)
![](/img/ch1.png)
> 網際網路做得太棒了,以至於大多數人將它看作像太平洋這樣的自然資源,而不是什麼人工產物。上一次出現這種大規模且無差錯的技術,你還記得是什麼時候嗎?
>
> —— [艾倫・凱](http://www.drdobbs.com/architecture-and-design/interview-with-alan-kay/240003442) 在接受 Dobb 博士雜誌採訪時說2012 年)
-----------------------
[TOC]
現今很多應用程式都是 **資料密集型data-intensive** 的,而非 **計算密集型compute-intensive** 的。因此 CPU 很少成為這類應用的瓶頸,更大的問題通常來自資料量、資料複雜性、以及資料的變更速度。
資料密集型應用通常由標準組件構建而成,標準組件提供了很多通用的功能;例如,許多應用程式都需要:
@@ -39,9 +41,9 @@
其次,越來越多的應用程式有著各種嚴格而廣泛的要求,單個工具不足以滿足所有的資料處理和儲存需求。取而代之的是,總體工作被拆分成一系列能被單個工具高效完成的任務,並透過應用程式碼將它們縫合起來。
例如如果將快取應用管理的快取層Memcached 或同類產品)和全文搜尋(全文搜尋伺服器,例如 Elasticsearch 或 Solr功能從主資料庫剝離出來那麼使快取 / 索引與主資料庫保持同步通常是應用程式碼的責任。[圖 1-1](../img/fig1-1.png) 給出了這種架構可能的樣子(細節將在後面的章節中詳細介紹)。
例如如果將快取應用管理的快取層Memcached 或同類產品)和全文搜尋(全文搜尋伺服器,例如 Elasticsearch 或 Solr功能從主資料庫剝離出來那麼使快取 / 索引與主資料庫保持同步通常是應用程式碼的責任。[圖 1-1](/img/fig1-1.png) 給出了這種架構可能的樣子(細節將在後面的章節中詳細介紹)。
![](../img/fig1-1.png)
![](/img/fig1-1.png)
**圖 1-1 一個可能的組合使用多個元件的資料系統架構**
@@ -166,7 +168,7 @@
大體上講,這一對操作有兩種實現方式。
1. 釋出推文時,只需將新推文插入全域性推文集合即可。當一個使用者請求自己的主頁時間線時,首先查詢他關注的所有人,查詢這些被關注使用者釋出的推文並按時間順序合併。在如 [圖 1-2](../img/fig1-2.png) 所示的關係型資料庫中,可以編寫這樣的查詢:
1. 釋出推文時,只需將新推文插入全域性推文集合即可。當一個使用者請求自己的主頁時間線時,首先查詢他關注的所有人,查詢這些被關注使用者釋出的推文並按時間順序合併。在如 [圖 1-2](/img/fig1-2.png) 所示的關係型資料庫中,可以編寫這樣的查詢:
```sql
SELECT tweets.*, users.*
@@ -176,13 +178,13 @@
WHERE follows.follower_id = current_user
```
![](../img/fig1-2.png)
![](/img/fig1-2.png)
**圖 1-2 推特主頁時間線的關係型模式簡單實現**
2. 為每個使用者的主頁時間線維護一個快取,就像每個使用者的推文收件箱([圖 1-3](../img/fig1-3.png))。當一個使用者釋出推文時,查詢所有關注該使用者的人,並將新的推文插入到每個主頁時間線快取中。因此讀取主頁時間線的請求開銷很小,因為結果已經提前計算好了。
2. 為每個使用者的主頁時間線維護一個快取,就像每個使用者的推文收件箱([圖 1-3](/img/fig1-3.png))。當一個使用者釋出推文時,查詢所有關注該使用者的人,並將新的推文插入到每個主頁時間線快取中。因此讀取主頁時間線的請求開銷很小,因為結果已經提前計算好了。
![](../img/fig1-3.png)
![](/img/fig1-3.png)
**圖 1-3 用於分發推特至關注者的資料流水線2012 年 11 月的負載引數【16】**
@@ -192,7 +194,7 @@
在推特的例子中,每個使用者粉絲數的分佈(可能按這些使用者的發推頻率來加權)是探討可伸縮性的一個關鍵負載引數,因為它決定了扇出負載。你的應用程式可能具有非常不同的特徵,但可以採用相似的原則來考慮它的負載。
推特軼事的最終轉折:現在已經穩健地實現了方法 2推特逐步轉向了兩種方法的混合。大多數使用者發的推文會被扇出寫入其粉絲主頁時間線快取中。但是少數擁有海量粉絲的使用者即名流會被排除在外。當用戶讀取主頁時間線時分別地獲取出該使用者所關注的每位名流的推文再與使用者的主頁時間線快取合併如方法 1 所示。這種混合方法能始終如一地提供良好效能。在 [第十二章](ch12.md) 中我們將重新討論這個例子,這在覆蓋更多技術層面之後。
推特軼事的最終轉折:現在已經穩健地實現了方法 2推特逐步轉向了兩種方法的混合。大多數使用者發的推文會被扇出寫入其粉絲主頁時間線快取中。但是少數擁有海量粉絲的使用者即名流會被排除在外。當用戶讀取主頁時間線時分別地獲取出該使用者所關注的每位名流的推文再與使用者的主頁時間線快取合併如方法 1 所示。這種混合方法能始終如一地提供良好效能。在 [第十二章](/tw/ch12) 中我們將重新討論這個例子,這在覆蓋更多技術層面之後。
### 描述效能
@@ -213,9 +215,9 @@
即使不斷重複傳送同樣的請求,每次得到的響應時間也都會略有不同。現實世界的系統會處理各式各樣的請求,響應時間可能會有很大差異。因此我們需要將響應時間視為一個可以測量的數值 **分佈distribution**,而不是單個數值。
在 [圖 1-4](../img/fig1-4.png) 中,每個灰條代表一次對服務的請求,其高度表示請求花費了多長時間。大多數請求是相當快的,但偶爾會出現需要更長的時間的異常值。這也許是因為緩慢的請求實質上開銷更大,例如它們可能會處理更多的資料。但即使(你認為)所有請求都花費相同時間的情況下,隨機的附加延遲也會導致結果變化,例如:上下文切換到後臺程序,網路資料包丟失與 TCP 重傳垃圾收集暫停強制從磁碟讀取的頁面錯誤伺服器機架中的震動【18】還有很多其他原因。
在 [圖 1-4](/img/fig1-4.png) 中,每個灰條代表一次對服務的請求,其高度表示請求花費了多長時間。大多數請求是相當快的,但偶爾會出現需要更長的時間的異常值。這也許是因為緩慢的請求實質上開銷更大,例如它們可能會處理更多的資料。但即使(你認為)所有請求都花費相同時間的情況下,隨機的附加延遲也會導致結果變化,例如:上下文切換到後臺程序,網路資料包丟失與 TCP 重傳垃圾收集暫停強制從磁碟讀取的頁面錯誤伺服器機架中的震動【18】還有很多其他原因。
![](../img/fig1-4.png)
![](/img/fig1-4.png)
**圖 1-4 展示了一個服務 100 次請求響應時間的均值與百分位數**
@@ -225,7 +227,7 @@
如果想知道典型場景下使用者需要等待多長時間,那麼中位數是一個好的度量標準:一半使用者請求的響應時間少於響應時間的中位數,另一半服務時間比中位數長。中位數也被稱為第 50 百分位點,有時縮寫為 p50。注意中位數是關於單個請求的如果使用者同時發出幾個請求在一個會話過程中或者由於一個頁面中包含了多個資源則至少一個請求比中位數慢的機率遠大於 50%。
為了弄清異常值有多糟糕,可以看看更高的百分位點,例如第 95、99 和 99.9 百分位點(縮寫為 p95p99 和 p999。它們意味著 95%、99% 或 99.9% 的請求響應時間要比該閾值快,例如:如果第 95 百分位點響應時間是 1.5 秒,則意味著 100 個請求中的 95 個響應時間快於 1.5 秒,而 100 個請求中的 5 個響應時間超過 1.5 秒。如 [圖 1-4](../img/fig1-4.png) 所示。
為了弄清異常值有多糟糕,可以看看更高的百分位點,例如第 95、99 和 99.9 百分位點(縮寫為 p95p99 和 p999。它們意味著 95%、99% 或 99.9% 的請求響應時間要比該閾值快,例如:如果第 95 百分位點響應時間是 1.5 秒,則意味著 100 個請求中的 95 個響應時間快於 1.5 秒,而 100 個請求中的 5 個響應時間超過 1.5 秒。如 [圖 1-4](/img/fig1-4.png) 所示。
響應時間的高百分位點(也稱為 **尾部延遲**,即 **tail latencies**)非常重要,因為它們直接影響使用者的服務體驗。例如亞馬遜在描述內部服務的響應時間要求時是以 99.9 百分位點為準,即使它隻影響一千個請求中的一個。這是因為請求響應最慢的客戶往往也是資料最多的客戶,也可以說是最有價值的客戶 —— 因為他們掏錢了【19】。保證網站響應迅速對於保持客戶的滿意度非常重要亞馬遜觀察到響應時間增加 100 毫秒,銷售量就減少 1%【20】而另一些報告說慢 1 秒鐘會讓客戶滿意度指標減少 16%【2122】。
@@ -239,13 +241,13 @@
> #### 實踐中的百分位點
>
> 在多重呼叫的後端服務裡,高百分位數變得特別重要。即使並行呼叫,終端使用者請求仍然需要等待最慢的並行呼叫完成。如 [圖 1-5](../img/fig1-5.png) 所示,只需要一個緩慢的呼叫就可以使整個終端使用者請求變慢。即使只有一小部分後端呼叫速度較慢,如果終端使用者請求需要多個後端呼叫,則獲得較慢呼叫的機會也會增加,因此較高比例的終端使用者請求速度會變慢(該效果稱為尾部延遲放大,即 tail latency amplification【24】
> 在多重呼叫的後端服務裡,高百分位數變得特別重要。即使並行呼叫,終端使用者請求仍然需要等待最慢的並行呼叫完成。如 [圖 1-5](/img/fig1-5.png) 所示,只需要一個緩慢的呼叫就可以使整個終端使用者請求變慢。即使只有一小部分後端呼叫速度較慢,如果終端使用者請求需要多個後端呼叫,則獲得較慢呼叫的機會也會增加,因此較高比例的終端使用者請求速度會變慢(該效果稱為尾部延遲放大,即 tail latency amplification【24】
>
> 如果你想將響應時間百分點新增到你的服務的監視儀表板則需要持續有效地計算它們。例如你可以使用滑動視窗來跟蹤連續10分鐘內的請求響應時間。每一分鐘你都會計算出該視窗中的響應時間中值和各種百分數並將這些度量值繪製在圖上。
>
> 簡單的實現是在時間視窗內儲存所有請求的響應時間列表,並且每分鐘對列表進行排序。如果對你來說效率太低,那麼有一些演算法能夠以最小的 CPU 和記憶體成本如前向衰減【25】、t-digest【26】或 HdrHistogram 【27】來計算百分位數的近似值。請注意平均百分比例如減少時間解析度或合併來自多臺機器的資料在數學上沒有意義 - 聚合響應時間資料的正確方法是新增直方圖【28】。
![](../img/fig1-5.png)
![](/img/fig1-5.png)
**圖 1-5 當一個請求需要多個後端請求時,單個後端慢請求就會拖慢整個終端使用者的請求**
@@ -257,7 +259,7 @@
人們經常討論 **縱向伸縮**scaling up也稱為垂直伸縮即 vertical scaling轉向更強大的機器**橫向伸縮**scaling out也稱為水平伸縮即 horizontal scaling將負載分佈到多臺小機器上之間的對立。跨多臺機器分配負載也稱為 “**無共享shared-nothing**” 架構。可以在單臺機器上執行的系統通常更簡單,但高階機器可能非常貴,所以非常密集的負載通常無法避免地需要橫向伸縮。現實世界中的優秀架構需要將這兩種方法務實地結合,因為使用幾臺足夠強大的機器可能比使用大量的小型虛擬機器更簡單也更便宜。
有些系統是 **彈性elastic** 的,這意味著可以在檢測到負載增加時自動增加計算資源,而其他系統則是手動伸縮(人工分析容量並決定向系統新增更多的機器)。如果負載 **極難預測highly unpredictable**,則彈性系統可能很有用,但手動伸縮系統更簡單,並且意外操作可能會更少(請參閱 “[分割槽再平衡](ch6.md#分割槽再平衡)”)。
有些系統是 **彈性elastic** 的,這意味著可以在檢測到負載增加時自動增加計算資源,而其他系統則是手動伸縮(人工分析容量並決定向系統新增更多的機器)。如果負載 **極難預測highly unpredictable**,則彈性系統可能很有用,但手動伸縮系統更簡單,並且意外操作可能會更少(請參閱 “[分割槽再平衡](/tw/ch6#分割槽再平衡)”)。
跨多臺機器部署 **無狀態服務stateless services** 非常簡單,但將帶狀態的資料系統從單節點變為分散式配置則可能引入許多額外複雜度。出於這個原因,常識告訴我們應該將資料庫放在單個節點上(縱向伸縮),直到伸縮成本或可用性需求迫使其改為分散式。
@@ -366,7 +368,7 @@
不幸的是,使應用可靠、可伸縮或可維護並不容易。但是某些模式和技術會不斷重新出現在不同的應用中。在接下來的幾章中,我們將看到一些資料系統的例子,並分析它們如何實現這些目標。
在本書後面的 [第三部分](part-iii.md) 中,我們將看到一種模式:幾個元件協同工作以構成一個完整的系統(如 [圖 1-1](../img/fig1-1.png) 中的例子)
在本書後面的 [第三部分](/tw/part-iii) 中,我們將看到一種模式:幾個元件協同工作以構成一個完整的系統(如 [圖 1-1](/img/fig1-1.png) 中的例子)
## 參考文獻
@@ -404,11 +406,4 @@
1. Frederick P Brooks: “No Silver Bullet Essence and Accident in Software Engineering,” in *The Mythical Man-Month*, Anniversary edition, Addison-Wesley, 1995. ISBN: 978-0-201-83595-3
1. Ben Moseley and Peter Marks: “[Out of the Tar Pit](https://curtclifton.net/papers/MoseleyMarks06a.pdf),” at *BCS Software Practice Advancement* (SPA), 2006.
1. Rich Hickey: “[Simple Made Easy](http://www.infoq.com/presentations/Simple-Made-Easy),” at *Strange Loop*, September 2011.
1. Hongyu Pei Breivold, Ivica Crnkovic, and Peter J. Eriksson: “[Analyzing Software Evolvability](http://www.es.mdh.se/pdf_publications/1251.pdf),” at *32nd Annual IEEE International Computer Software and Applications Conference* (COMPSAC), July 2008. [doi:10.1109/COMPSAC.2008.50](http://dx.doi.org/10.1109/COMPSAC.2008.50)
------
| 上一章 | 目錄 | 下一章 |
| ----------------------------------- | ------------------------------- | ------------------------------------ |
| [第一部分:資料系統基礎](part-i.md) | [設計資料密集型應用](README.md) | [第二章:資料模型與查詢語言](ch2.md) |
1. Hongyu Pei Breivold, Ivica Crnkovic, and Peter J. Eriksson: “[Analyzing Software Evolvability](http://www.es.mdh.se/pdf_publications/1251.pdf),” at *32nd Annual IEEE International Computer Software and Applications Conference* (COMPSAC), July 2008. [doi:10.1109/COMPSAC.2008.50](http://dx.doi.org/10.1109/COMPSAC.2008.50)

View File

@@ -1,18 +1,19 @@
# 第十章:批處理
---
title: "第十章:批處理"
linkTitle: "10. 批處理"
weight: 310
breadcrumbs: false
---
![](../img/ch10.png)
![](/img/ch10.png)
> 帶有太強個人色彩的系統無法成功。當最初的設計完成並且相對穩定時,不同的人們以自己的方式進行測試,真正的考驗才開始。
>
> —— 高德納
---------------
[TOC]
在本書的前兩部分中,我們討論了很多關於 **請求****查詢** 以及相應的 **響應****結果**。許多現有資料系統中都採用這種資料處理方式你傳送請求指令一段時間後我們期望系統會給出一個結果。資料庫、快取、搜尋索引、Web 伺服器以及其他一些系統都以這種方式工作。
像這樣的 **線上online** 系統,無論是瀏覽器請求頁面還是呼叫遠端 API 的服務,我們通常認為請求是由人類使用者觸發的,並且正在等待響應。他們不應該等太久,所以我們非常關注系統的響應時間(請參閱 “[描述效能](ch1.md#描述效能)”)。
像這樣的 **線上online** 系統,無論是瀏覽器請求頁面還是呼叫遠端 API 的服務,我們通常認為請求是由人類使用者觸發的,並且正在等待響應。他們不應該等太久,所以我們非常關注系統的響應時間(請參閱 “[描述效能](/tw/ch1#描述效能)”)。
Web 和越來越多的基於 HTTP/REST 的 API 使互動的請求 / 響應風格變得如此普遍,以至於很容易將其視為理所當然。但我們應該記住,這不是構建系統的唯一方式,其他方法也有其優點。我們來看看三種不同型別的系統:
@@ -26,7 +27,7 @@ Web 和越來越多的基於 HTTP/REST 的 API 使互動的請求 / 響應風格
* 流處理系統(準即時系統)
流處理介於線上和離線(批處理)之間,所以有時候被稱為 **準即時near-real-time****準線上nearline** 處理。像批處理系統一樣,流處理消費輸入併產生輸出(並不需要響應請求)。但是,流式作業在事件發生後不久就會對事件進行操作,而批處理作業則需等待固定的一組輸入資料。這種差異使流處理系統比起批處理系統具有更低的延遲。由於流處理基於批處理,我們將在 [第十一章](ch11.md) 討論它。
流處理介於線上和離線(批處理)之間,所以有時候被稱為 **準即時near-real-time****準線上nearline** 處理。像批處理系統一樣,流處理消費輸入併產生輸出(並不需要響應請求)。但是,流式作業在事件發生後不久就會對事件進行操作,而批處理作業則需等待固定的一組輸入資料。這種差異使流處理系統比起批處理系統具有更低的延遲。由於流處理基於批處理,我們將在 [第十一章](/tw/ch11) 討論它。
正如我們將在本章中看到的那樣批處理是構建可靠、可伸縮和可維護應用程式的重要組成部分。例如2004 年釋出的批處理演算法 Map-Reduce可能被過分熱情地被稱為 “造就 Google 大規模可伸縮性的演算法”【2】。隨後在各種開源資料系統中得到應用包括 Hadoop、CouchDB 和 MongoDB。
@@ -122,7 +123,7 @@ Ruby 指令碼在記憶體中儲存了一個 URL 的雜湊表,將每個 URL
哪種方法更好?這取決於你有多少個不同的 URL。對於大多數中小型網站你可能可以為所有不同網址提供一個計數器假設我們使用 1GB 記憶體)。在此例中,作業的 **工作集**working set即作業需要隨機訪問的記憶體大小僅取決於不同 URL 的數量:如果日誌中只有單個 URL重複出現一百萬次則散列表所需的空間表就只有一個 URL 加上一個計數器的大小。當工作集足夠小時,記憶體散列表表現良好,甚至在效能較差的筆記型電腦上也可以正常工作。
另一方面,如果作業的工作集大於可用記憶體,則排序方法的優點是可以高效地使用磁碟。這與我們在 “[SSTables 和 LSM 樹](ch3.md#SSTables和LSM樹)” 中討論過的原理是一樣的:資料塊可以在記憶體中排序並作為段檔案寫入磁碟,然後多個排序好的段可以合併為一個更大的排序檔案。歸併排序具有在磁碟上執行良好的順序訪問模式。(請記住,針對順序 I/O 進行最佳化是 [第三章](ch3.md) 中反覆出現的主題,相同的模式在此重現)
另一方面,如果作業的工作集大於可用記憶體,則排序方法的優點是可以高效地使用磁碟。這與我們在 “[SSTables 和 LSM 樹](/tw/ch3#SSTables和LSM樹)” 中討論過的原理是一樣的:資料塊可以在記憶體中排序並作為段檔案寫入磁碟,然後多個排序好的段可以合併為一個更大的排序檔案。歸併排序具有在磁碟上執行良好的順序訪問模式。(請記住,針對順序 I/O 進行最佳化是 [第三章](/tw/ch3) 中反覆出現的主題,相同的模式在此重現)
GNU CoreutilsLinux中的 `sort` 程式透過溢位至磁碟的方式來自動應對大於記憶體的資料集,並能同時使用多個 CPU 核進行並行排序【9】。這意味著我們之前看到的簡單的 Unix 命令鏈很容易伸縮至大資料集,且不會耗盡記憶體。瓶頸可能是從磁碟讀取輸入檔案的速度。
@@ -203,11 +204,11 @@ MapReduce 有點像 Unix 工具,但分佈在數千臺機器上。像 Unix 工
[^iv]: 一個不同之處在於,對於 HDFS可以將計算任務安排在儲存特定檔案副本的計算機上執行而物件儲存通常將儲存和計算分開。如果網路頻寬是一個瓶頸從本地磁碟讀取有效能優勢。但是請注意如果使用糾刪碼Erasure Coding則會丟失區域性因為來自多臺機器的資料必須進行合併以重建原始檔案【20】。
與網路連線儲存NAS和儲存區域網路SAN架構的共享磁碟方法相比HDFS 基於 **無共享** 原則(請參閱 [第二部分](part-ii.md) 的介紹)。共享磁碟儲存由集中式儲存裝置實現,通常使用定製硬體和專用網路基礎設施(如光纖通道)。而另一方面,無共享方法不需要特殊的硬體,只需要透過傳統資料中心網路連線的計算機。
與網路連線儲存NAS和儲存區域網路SAN架構的共享磁碟方法相比HDFS 基於 **無共享** 原則(請參閱 [第二部分](/tw/part-ii) 的介紹)。共享磁碟儲存由集中式儲存裝置實現,通常使用定製硬體和專用網路基礎設施(如光纖通道)。而另一方面,無共享方法不需要特殊的硬體,只需要透過傳統資料中心網路連線的計算機。
HDFS 在每臺機器上運行了一個守護程序,它對外暴露網路服務,允許其他節點訪問儲存在該機器上的檔案(假設資料中心中的每臺通用計算機都掛載著一些磁碟)。名為 **NameNode** 的中央伺服器會跟蹤哪個檔案塊儲存在哪臺機器上。因此HDFS 在概念上建立了一個大型檔案系統,可以使用所有執行有守護程序的機器的磁碟。
為了容忍機器和磁碟故障,檔案塊被複制到多臺機器上。複製可能意味著多個機器上的相同資料的多個副本,如 [第五章](ch5.md) 中所述,或者諸如 Reed-Solomon 碼這樣的糾刪碼方案它能以比完全複製更低的儲存開銷來支援恢復丟失的資料【20,22】。這些技術與 RAID 相似,後者可以在連線到同一臺機器的多個磁碟上提供冗餘;區別在於在分散式檔案系統中,檔案訪問和複製是在傳統的資料中心網路上完成的,沒有特殊的硬體。
為了容忍機器和磁碟故障,檔案塊被複制到多臺機器上。複製可能意味著多個機器上的相同資料的多個副本,如 [第五章](/tw/ch5) 中所述,或者諸如 Reed-Solomon 碼這樣的糾刪碼方案它能以比完全複製更低的儲存開銷來支援恢復丟失的資料【20,22】。這些技術與 RAID 相似,後者可以在連線到同一臺機器的多個磁碟上提供冗餘;區別在於在分散式檔案系統中,檔案訪問和複製是在傳統的資料中心網路上完成的,沒有特殊的硬體。
HDFS 的可伸縮性已經很不錯了:在撰寫本書時,最大的 HDFS 部署執行在上萬臺機器上,總儲存容量達數百 PB【23】。如此大的規模已經變得可行因為使用商品硬體和開源軟體的 HDFS 上的資料儲存和訪問成本遠低於在專用儲存裝置上支援同等容量的成本【24】。
@@ -222,7 +223,7 @@ MapReduce 是一個程式設計框架,你可以使用它編寫程式碼來處
這四個步驟可以作為一個 MapReduce 作業執行。步驟 2Map和 4Reduce是你編寫自定義資料處理程式碼的地方。步驟 1將檔案分解成記錄由輸入格式解析器處理。步驟 3 中的排序步驟隱含在 MapReduce 中 —— 你不必編寫它,因為 Mapper 的輸出始終在送往 Reducer 之前進行排序。
要建立 MapReduce 作業你需要實現兩個回撥函式Mapper 和 Reducer其行為如下請參閱 “[MapReduce 查詢](ch2.md#MapReduce查詢)”):
要建立 MapReduce 作業你需要實現兩個回撥函式Mapper 和 Reducer其行為如下請參閱 “[MapReduce 查詢](/tw/ch2#MapReduce查詢)”):
* Mapper
@@ -238,21 +239,21 @@ MapReduce 是一個程式設計框架,你可以使用它編寫程式碼來處
MapReduce 與 Unix 命令管道的主要區別在於MapReduce 可以在多臺機器上並行執行計算而無需編寫程式碼來顯式處理並行問題。Mapper 和 Reducer 一次只能處理一條記錄;它們不需要知道它們的輸入來自哪裡,或者輸出去往什麼地方,所以框架可以處理在機器之間移動資料的複雜性。
在分散式計算中可以使用標準的 Unix 工具作為 Mapper 和 Reducer【25】但更常見的是它們被實現為傳統程式語言的函式。在 Hadoop MapReduce 中Mapper 和 Reducer 都是實現特定介面的 Java 類。在 MongoDB 和 CouchDB 中Mapper 和 Reducer 都是 JavaScript 函式(請參閱 “[MapReduce 查詢](ch2.md#MapReduce查詢)”)。
在分散式計算中可以使用標準的 Unix 工具作為 Mapper 和 Reducer【25】但更常見的是它們被實現為傳統程式語言的函式。在 Hadoop MapReduce 中Mapper 和 Reducer 都是實現特定介面的 Java 類。在 MongoDB 和 CouchDB 中Mapper 和 Reducer 都是 JavaScript 函式(請參閱 “[MapReduce 查詢](/tw/ch2#MapReduce查詢)”)。
[圖 10-1](../img/fig10-1.png) 顯示了 Hadoop MapReduce 作業中的資料流。其並行化基於分割槽(請參閱 [第六章](ch6.md)):作業的輸入通常是 HDFS 中的一個目錄,輸入目錄中的每個檔案或檔案塊都被認為是一個單獨的分割槽,可以單獨處理 map 任務([圖 10-1](../img/fig10-1.png) 中的 m1m2 和 m3 標記)。
[圖 10-1](/img/fig10-1.png) 顯示了 Hadoop MapReduce 作業中的資料流。其並行化基於分割槽(請參閱 [第六章](/tw/ch6)):作業的輸入通常是 HDFS 中的一個目錄,輸入目錄中的每個檔案或檔案塊都被認為是一個單獨的分割槽,可以單獨處理 map 任務([圖 10-1](/img/fig10-1.png) 中的 m1m2 和 m3 標記)。
每個輸入檔案的大小通常是數百兆位元組。MapReduce 排程器(圖中未顯示)試圖在其中一臺儲存輸入檔案副本的機器上執行每個 Mapper只要該機器有足夠的備用 RAM 和 CPU 資源來執行 Mapper 任務【26】。這個原則被稱為 **將計算放在資料附近**【27】它節省了透過網路複製輸入檔案的開銷減少網路負載並增加區域性。
![](../img/fig10-1.png)
![](/img/fig10-1.png)
**圖 10-1 具有三個 Mapper 和三個 Reducer 的 MapReduce 任務**
在大多數情況下,應該在 Mapper 任務中執行的應用程式碼在將要執行它的機器上還不存在,所以 MapReduce 框架首先將程式碼(例如 Java 程式中的 JAR 檔案)複製到適當的機器。然後啟動 Map 任務並開始讀取輸入檔案,一次將一條記錄傳入 Mapper 回撥函式。Mapper 的輸出由鍵值對組成。
計算的 Reduce 端也被分割槽。雖然 Map 任務的數量由輸入檔案塊的數量決定,但 Reducer 的任務的數量是由作業作者配置的(它可以不同於 Map 任務的數量)。為了確保具有相同鍵的所有鍵值對最終落在相同的 Reducer 處,框架使用鍵的雜湊值來確定哪個 Reduce 任務應該接收到特定的鍵值對(請參閱 “[根據鍵的雜湊分割槽](ch6.md#根據鍵的雜湊分割槽)”)。
計算的 Reduce 端也被分割槽。雖然 Map 任務的數量由輸入檔案塊的數量決定,但 Reducer 的任務的數量是由作業作者配置的(它可以不同於 Map 任務的數量)。為了確保具有相同鍵的所有鍵值對最終落在相同的 Reducer 處,框架使用鍵的雜湊值來確定哪個 Reduce 任務應該接收到特定的鍵值對(請參閱 “[根據鍵的雜湊分割槽](/tw/ch6#根據鍵的雜湊分割槽)”)。
鍵值對必須進行排序,但資料集可能太大,無法在單臺機器上使用常規排序演算法進行排序。相反,分類是分階段進行的。首先每個 Map 任務都按照 Reducer 對輸出進行分割槽。每個分割槽都被寫入 Mapper 程式的本地磁碟,使用的技術與我們在 “[SSTables 與 LSM 樹](ch3.md#SSTables和LSM樹)” 中討論的類似。
鍵值對必須進行排序,但資料集可能太大,無法在單臺機器上使用常規排序演算法進行排序。相反,分類是分階段進行的。首先每個 Map 任務都按照 Reducer 對輸出進行分割槽。每個分割槽都被寫入 Mapper 程式的本地磁碟,使用的技術與我們在 “[SSTables 與 LSM 樹](/tw/ch3#SSTables和LSM樹)” 中討論的類似。
只要當 Mapper 讀取完輸入檔案並寫完排序後的輸出檔案MapReduce 排程器就會通知 Reducer 可以從該 Mapper 開始獲取輸出檔案。Reducer 連線到每個 Mapper並下載自己相應分割槽的有序鍵值對檔案。按 Reducer 分割槽,排序,從 Mapper 向 Reducer 複製分割槽資料,這一整個過程被稱為 **混洗shuffle**【26】一個容易混淆的術語 —— 不像洗牌,在 MapReduce 中的混洗沒有隨機性)。
@@ -276,23 +277,23 @@ Hadoop 的各種高階工具(如 Pig 【30】、Hive 【31】、Cascading 【3
### Reduce側連線與分組
我們在 [第二章](ch2.md) 中討論了資料模型和查詢語言的連線,但是我們還沒有深入探討連線是如何實現的。現在是我們再次撿起這條線索的時候了。
我們在 [第二章](/tw/ch2) 中討論了資料模型和查詢語言的連線,但是我們還沒有深入探討連線是如何實現的。現在是我們再次撿起這條線索的時候了。
在許多資料集中,一條記錄與另一條記錄存在關聯是很常見的:關係模型中的 **外部索引鍵**,文件模型中的 **文件引用** 或圖模型中的 **邊**。當你需要同時訪問這一關聯的兩側(持有引用的記錄與被引用的記錄)時,連線就是必須的。正如 [第二章](ch2.md) 所討論的,非規範化可以減少對連線的需求,但通常無法將其完全移除 [^v]。
在許多資料集中,一條記錄與另一條記錄存在關聯是很常見的:關係模型中的 **外部索引鍵**,文件模型中的 **文件引用** 或圖模型中的 **邊**。當你需要同時訪問這一關聯的兩側(持有引用的記錄與被引用的記錄)時,連線就是必須的。正如 [第二章](/tw/ch2) 所討論的,非規範化可以減少對連線的需求,但通常無法將其完全移除 [^v]。
[^v]: 我們在本書中討論的連線通常是等值連線,即最常見的連線型別,其中記錄透過與其他記錄在特定欄位(例如 ID中具有 **相同值** 相關聯。有些資料庫支援更通用的連線型別,例如使用小於運算子而不是等號運算子,但是我們沒有地方來講這些東西。
在資料庫中,如果執行只涉及少量記錄的查詢,資料庫通常會使用 **索引** 來快速定位感興趣的記錄(請參閱 [第三章](ch3.md))。如果查詢涉及到連線,則可能涉及到查詢多個索引。然而 MapReduce 沒有索引的概念 —— 至少在通常意義上沒有。
在資料庫中,如果執行只涉及少量記錄的查詢,資料庫通常會使用 **索引** 來快速定位感興趣的記錄(請參閱 [第三章](/tw/ch3))。如果查詢涉及到連線,則可能涉及到查詢多個索引。然而 MapReduce 沒有索引的概念 —— 至少在通常意義上沒有。
當 MapReduce 作業被賦予一組檔案作為輸入時,它讀取所有這些檔案的全部內容;資料庫會將這種操作稱為 **全表掃描**。如果你只想讀取少量的記錄,則全表掃描與索引查詢相比,代價非常高昂。但是在分析查詢中(請參閱 “[事務處理還是分析?](ch3.md#事務處理還是分析?)”),通常需要計算大量記錄的聚合。在這種情況下,特別是如果能在多臺機器上並行處理時,掃描整個輸入可能是相當合理的事情。
當 MapReduce 作業被賦予一組檔案作為輸入時,它讀取所有這些檔案的全部內容;資料庫會將這種操作稱為 **全表掃描**。如果你只想讀取少量的記錄,則全表掃描與索引查詢相比,代價非常高昂。但是在分析查詢中(請參閱 “[事務處理還是分析?](/tw/ch3#事務處理還是分析?)”),通常需要計算大量記錄的聚合。在這種情況下,特別是如果能在多臺機器上並行處理時,掃描整個輸入可能是相當合理的事情。
當我們在批處理的語境中討論連線時,我們指的是在資料集中解析某種關聯的全量存在。例如我們假設一個作業是同時處理所有使用者的資料,而非僅僅是為某個特定使用者查詢資料(而這能透過索引更高效地完成)。
#### 示例:使用者活動事件分析
[圖 10-2](../img/fig10-2.png) 給出了一個批處理作業中連線的典型例子。左側是事件日誌,描述登入使用者在網站上做的事情(稱為 **活動事件**,即 activity events**點選流資料**,即 clickstream data右側是使用者資料庫。你可以將此示例看作是星型模式的一部分請參閱 “[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”):事件日誌是事實表,使用者資料庫是其中的一個維度。
[圖 10-2](/img/fig10-2.png) 給出了一個批處理作業中連線的典型例子。左側是事件日誌,描述登入使用者在網站上做的事情(稱為 **活動事件**,即 activity events**點選流資料**,即 clickstream data右側是使用者資料庫。你可以將此示例看作是星型模式的一部分請參閱 “[星型和雪花型:分析的模式](/tw/ch3#星型和雪花型:分析的模式)”):事件日誌是事實表,使用者資料庫是其中的一個維度。
![](../img/fig10-2.png)
![](/img/fig10-2.png)
**圖 10-2 使用者行為日誌與使用者檔案的連線**
@@ -302,13 +303,13 @@ Hadoop 的各種高階工具(如 Pig 【30】、Hive 【31】、Cascading 【3
為了在批處理過程中實現良好的吞吐量,計算必須(儘可能)限於單臺機器上進行。為待處理的每條記錄發起隨機訪問的網路請求實在是太慢了。而且,查詢遠端資料庫意味著批處理作業變為 **非確定的nondeterministic**,因為遠端資料庫中的資料可能會改變。
因此,更好的方法是獲取使用者資料庫的副本(例如,使用 ETL 程序從資料庫備份中提取資料,請參閱 “[資料倉庫](ch3.md#資料倉庫)”),並將它和使用者行為日誌放入同一個分散式檔案系統中。然後你可以將使用者資料庫儲存在 HDFS 中的一組檔案中,而使用者活動記錄儲存在另一組檔案中,並能用 MapReduce 將所有相關記錄集中到同一個地方進行高效處理。
因此,更好的方法是獲取使用者資料庫的副本(例如,使用 ETL 程序從資料庫備份中提取資料,請參閱 “[資料倉庫](/tw/ch3#資料倉庫)”),並將它和使用者行為日誌放入同一個分散式檔案系統中。然後你可以將使用者資料庫儲存在 HDFS 中的一組檔案中,而使用者活動記錄儲存在另一組檔案中,並能用 MapReduce 將所有相關記錄集中到同一個地方進行高效處理。
#### 排序合併連線
回想一下Mapper 的目的是從每個輸入記錄中提取一對鍵值。在 [圖 10-2](../img/fig10-2.png) 的情況下,這個鍵就是使用者 ID一組 Mapper 會掃過活動事件(提取使用者 ID 作為鍵,活動事件作為值),而另一組 Mapper 將會掃過使用者資料庫(提取使用者 ID 作為鍵,使用者的出生日期作為值)。這個過程如 [圖 10-3](../img/fig10-3.png) 所示。
回想一下Mapper 的目的是從每個輸入記錄中提取一對鍵值。在 [圖 10-2](/img/fig10-2.png) 的情況下,這個鍵就是使用者 ID一組 Mapper 會掃過活動事件(提取使用者 ID 作為鍵,活動事件作為值),而另一組 Mapper 將會掃過使用者資料庫(提取使用者 ID 作為鍵,使用者的出生日期作為值)。這個過程如 [圖 10-3](/img/fig10-3.png) 所示。
![](../img/fig10-3.png)
![](/img/fig10-3.png)
**圖 10-3 在使用者 ID 上進行的 Reduce 端連線。如果輸入資料集分割槽為多個檔案,則每個分割槽都會被多個 Mapper 並行處理**
@@ -344,11 +345,11 @@ Hadoop 的各種高階工具(如 Pig 【30】、Hive 【31】、Cascading 【3
如果存在與單個鍵關聯的大量資料,則 “將具有相同鍵的所有記錄放到相同的位置” 這種模式就被破壞了。例如在社交網路中,大多數使用者可能會與幾百人有連線,但少數名人可能有數百萬的追隨者。這種不成比例的活動資料庫記錄被稱為 **關鍵物件linchpin object**【38】或 **熱鍵hot key**
在單個 Reducer 中收集與某個名人相關的所有活動(例如他們釋出內容的回覆)可能導致嚴重的 **偏斜**(也稱為 **熱點**,即 hot spot—— 也就是說,一個 Reducer 必須比其他 Reducer 處理更多的記錄(請參閱 “[負載偏斜與熱點消除](ch6.md#負載偏斜與熱點消除)”)。由於 MapReduce 作業只有在所有 Mapper 和 Reducer 都完成時才完成,所有後續作業必須等待最慢的 Reducer 才能啟動。
在單個 Reducer 中收集與某個名人相關的所有活動(例如他們釋出內容的回覆)可能導致嚴重的 **偏斜**(也稱為 **熱點**,即 hot spot—— 也就是說,一個 Reducer 必須比其他 Reducer 處理更多的記錄(請參閱 “[負載偏斜與熱點消除](/tw/ch6#負載偏斜與熱點消除)”)。由於 MapReduce 作業只有在所有 Mapper 和 Reducer 都完成時才完成,所有後續作業必須等待最慢的 Reducer 才能啟動。
如果連線的輸入存在熱鍵可以使用一些演算法進行補償。例如Pig 中的 **偏斜連線skewed join** 方法首先執行一個抽樣作業Sampling Job來確定哪些鍵是熱鍵【39】。連線實際執行時Mapper 會將熱鍵的關聯記錄 **隨機**(相對於傳統 MapReduce 基於鍵雜湊的確定性方法)傳送到幾個 Reducer 之一。對於另外一側的連線輸入,與熱鍵相關的記錄需要被複制到 **所有** 處理該鍵的 Reducer 上【40】。
這種技術將處理熱鍵的工作分散到多個 Reducer 上,這樣可以使其更好地並行化,代價是需要將連線另一側的輸入記錄複製到多個 Reducer 上。Crunch 中的 **分片連線sharded join** 方法與之類似,但需要顯式指定熱鍵而不是使用抽樣作業。這種技術也非常類似於我們在 “[負載偏斜與熱點消除](ch6.md#負載偏斜與熱點消除)” 中討論的技術,使用隨機化來緩解分割槽資料庫中的熱點。
這種技術將處理熱鍵的工作分散到多個 Reducer 上,這樣可以使其更好地並行化,代價是需要將連線另一側的輸入記錄複製到多個 Reducer 上。Crunch 中的 **分片連線sharded join** 方法與之類似,但需要顯式指定熱鍵而不是使用抽樣作業。這種技術也非常類似於我們在 “[負載偏斜與熱點消除](/tw/ch6#負載偏斜與熱點消除)” 中討論的技術,使用隨機化來緩解分割槽資料庫中的熱點。
Hive 的偏斜連線最佳化採取了另一種方法。它需要在表格元資料中顯式指定熱鍵,並將與這些鍵相關的記錄單獨存放,與其它檔案分開。當在該表上執行連線時,對於熱鍵,它會使用 Map 端連線(請參閱下一節)。
@@ -367,11 +368,11 @@ Reduce 側方法的優點是不需要對輸入資料做任何假設:無論其
適用於執行 Map 端連線的最簡單場景是大資料集與小資料集連線的情況。要點在於小資料集需要足夠小,以便可以將其全部載入到每個 Mapper 的記憶體中。
例如,假設在 [圖 10-2](../img/fig10-2.png) 的情況下,使用者資料庫小到足以放進記憶體中。在這種情況下,當 Mapper 啟動時它可以首先將使用者資料庫從分散式檔案系統讀取到記憶體中的散列表中。完成此操作後Mapper 可以掃描使用者活動事件,並簡單地在散列表中查詢每個事件的使用者 ID [^vi]。
例如,假設在 [圖 10-2](/img/fig10-2.png) 的情況下,使用者資料庫小到足以放進記憶體中。在這種情況下,當 Mapper 啟動時它可以首先將使用者資料庫從分散式檔案系統讀取到記憶體中的散列表中。完成此操作後Mapper 可以掃描使用者活動事件,並簡單地在散列表中查詢每個事件的使用者 ID [^vi]。
[^vi]: 這個例子假定散列表中的每個鍵只有一個條目,這對使用者資料庫(使用者 ID 唯一標識一個使用者)可能是正確的。通常,雜湊表可能需要包含具有相同鍵的多個條目,而連線運算子將對每個鍵輸出所有的匹配。
參與連線的較大輸入的每個檔案塊各有一個 Mapper在 [圖 10-2](../img/fig10-2.png) 的例子中活動事件是較大的輸入)。每個 Mapper 都會將較小輸入整個載入到記憶體中。
參與連線的較大輸入的每個檔案塊各有一個 Mapper在 [圖 10-2](/img/fig10-2.png) 的例子中活動事件是較大的輸入)。每個 Mapper 都會將較小輸入整個載入到記憶體中。
這種簡單有效的演算法被稱為 **廣播雜湊連線broadcast hash join****廣播** 一詞反映了這樣一個事實,每個連線較大輸入端分割槽的 Mapper 都會將較小輸入端資料集整個讀入記憶體中(所以較小輸入實際上 “廣播” 到較大資料的所有分割槽上),**雜湊** 一詞反映了它使用一個散列表。Pig名為 “**複製連結replicated join**”Hive“**MapJoin**”Cascading 和 Crunch 支援這種連線。它也被諸如 Impala 的資料倉庫查詢引擎使用【41】。
@@ -379,7 +380,7 @@ Reduce 側方法的優點是不需要對輸入資料做任何假設:無論其
#### 分割槽雜湊連線
如果 Map 側連線的輸入以相同的方式進行分割槽,則雜湊連線方法可以獨立應用於每個分割槽。在 [圖 10-2](../img/fig10-2.png) 的情況中,你可以根據使用者 ID 的最後一位十進位制數字來對活動事件和使用者資料庫進行分割槽(因此連線兩側各有 10 個分割槽。例如Mapper3 首先將所有具有以 3 結尾的 ID 的使用者載入到散列表中,然後掃描 ID 為 3 的每個使用者的所有活動事件。
如果 Map 側連線的輸入以相同的方式進行分割槽,則雜湊連線方法可以獨立應用於每個分割槽。在 [圖 10-2](/img/fig10-2.png) 的情況中,你可以根據使用者 ID 的最後一位十進位制數字來對活動事件和使用者資料庫進行分割槽(因此連線兩側各有 10 個分割槽。例如Mapper3 首先將所有具有以 3 結尾的 ID 的使用者載入到散列表中,然後掃描 ID 為 3 的每個使用者的所有活動事件。
如果分割槽正確無誤,可以確定的是,所有你可能需要連線的記錄都落在同一個編號的分割槽中。因此每個 Mapper 只需要從輸入兩端各讀取一個分割槽就足夠了。好處是每個 Mapper 都可以在記憶體散列表中少放點資料。
@@ -406,7 +407,7 @@ Reduce 側方法的優點是不需要對輸入資料做任何假設:無論其
我們已經說了很多用於實現 MapReduce 工作流的演算法,但卻忽略了一個重要的問題:這些處理完成之後的最終結果是什麼?我們最開始為什麼要跑這些作業?
在資料庫查詢的場景中我們將事務處理OLTP與分析兩種目的區分開來請參閱 “[事務處理還是分析?](ch3.md#事務處理還是分析?)”。我們看到OLTP 查詢通常根據鍵查詢少量記錄,使用索引,並將其呈現給使用者(比如在網頁上)。另一方面,分析查詢通常會掃描大量記錄,執行分組與聚合,輸出通常有著報告的形式:顯示某個指標隨時間變化的圖表,或按照某種排位取前 10 項,或將一些數字細化為子類。這種報告的消費者通常是需要做出商業決策的分析師或經理。
在資料庫查詢的場景中我們將事務處理OLTP與分析兩種目的區分開來請參閱 “[事務處理還是分析?](/tw/ch3#事務處理還是分析?)”。我們看到OLTP 查詢通常根據鍵查詢少量記錄,使用索引,並將其呈現給使用者(比如在網頁上)。另一方面,分析查詢通常會掃描大量記錄,執行分組與聚合,輸出通常有著報告的形式:顯示某個指標隨時間變化的圖表,或按照某種排位取前 10 項,或將一些數字細化為子類。這種報告的消費者通常是需要做出商業決策的分析師或經理。
批處理放哪裡合適?它不屬於事務處理,也不是分析。它和分析比較接近,因為批處理通常會掃過輸入資料集的絕大部分。然而 MapReduce 作業工作流與用於分析目的的 SQL 查詢是不同的(請參閱 “[Hadoop 與分散式資料庫的對比](#Hadoop與分散式資料庫的對比)”)。批處理過程的輸出通常不是報表,而是一些其他型別的結構。
@@ -414,15 +415,15 @@ Reduce 側方法的優點是不需要對輸入資料做任何假設:無論其
Google 最初使用 MapReduce 是為其搜尋引擎建立索引,其實現為由 5 到 10 個 MapReduce 作業組成的工作流【1】。雖然 Google 後來也不僅僅是為這個目的而使用 MapReduce 【43】但如果從構建搜尋索引的角度來看更能幫助理解 MapReduce。直至今日Hadoop MapReduce 仍然是為 Lucene/Solr 構建索引的好方法【44】
我們在 “[全文搜尋和模糊索引](ch3.md#全文搜尋和模糊索引)” 中簡要地瞭解了 Lucene 這樣的全文搜尋索引是如何工作的:它是一個檔案(關鍵詞字典),你可以在其中高效地查詢特定關鍵字,並找到包含該關鍵字的所有文件 ID 列表(文章列表)。這是一種非常簡化的看法 —— 實際上,搜尋索引需要各種額外資料,以便根據相關性對搜尋結果進行排名、糾正拼寫錯誤、解析同義詞等等 —— 但這個原則是成立的。
我們在 “[全文搜尋和模糊索引](/tw/ch3#全文搜尋和模糊索引)” 中簡要地瞭解了 Lucene 這樣的全文搜尋索引是如何工作的:它是一個檔案(關鍵詞字典),你可以在其中高效地查詢特定關鍵字,並找到包含該關鍵字的所有文件 ID 列表(文章列表)。這是一種非常簡化的看法 —— 實際上,搜尋索引需要各種額外資料,以便根據相關性對搜尋結果進行排名、糾正拼寫錯誤、解析同義詞等等 —— 但這個原則是成立的。
如果需要對一組固定文件執行全文搜尋則批處理是一種構建索引的高效方法Mapper 根據需要對文件集合進行分割槽,每個 Reducer 構建該分割槽的索引,並將索引檔案寫入分散式檔案系統。構建這樣的文件分割槽索引(請參閱 “[分割槽與次級索引](ch6.md#分割槽與次級索引)”)並行處理效果拔群。
如果需要對一組固定文件執行全文搜尋則批處理是一種構建索引的高效方法Mapper 根據需要對文件集合進行分割槽,每個 Reducer 構建該分割槽的索引,並將索引檔案寫入分散式檔案系統。構建這樣的文件分割槽索引(請參閱 “[分割槽與次級索引](/tw/ch6#分割槽與次級索引)”)並行處理效果拔群。
由於按關鍵字查詢搜尋索引是隻讀操作,因而這些索引檔案一旦建立就是不可變的。
如果索引的文件集合發生更改,一種選擇是定期重跑整個索引工作流,並在完成後用新的索引檔案批次替換以前的索引檔案。如果只有少量的文件發生了變化,這種方法的計算成本可能會很高。但它的優點是索引過程很容易理解:文件進,索引出。
另一個選擇是,可以增量建立索引。如 [第三章](ch3.md) 中討論的如果要在索引中新增刪除或更新文件Lucene 會寫新的段檔案,並在後臺非同步合併壓縮段檔案。我們將在 [第十一章](ch11.md) 中看到更多這種增量處理。
另一個選擇是,可以增量建立索引。如 [第三章](/tw/ch3) 中討論的如果要在索引中新增刪除或更新文件Lucene 會寫新的段檔案,並在後臺非同步合併壓縮段檔案。我們將在 [第十一章](/tw/ch11) 中看到更多這種增量處理。
#### 鍵值儲存作為批處理輸出
@@ -440,7 +441,7 @@ Google 最初使用 MapReduce 是為其搜尋引擎建立索引,其實現為
更好的解決方案是在批處理作業 **內** 建立一個全新的資料庫,並將其作為檔案寫入分散式檔案系統中作業的輸出目錄,就像上節中的搜尋索引一樣。這些資料檔案一旦寫入就是不可變的,可以批次載入到處理只讀查詢的伺服器中。不少鍵值儲存都支援在 MapReduce 作業中構建資料庫檔案,包括 Voldemort 【46】、Terrapin 【47】、ElephantDB 【48】和 HBase 批次載入【49】。
構建這些資料庫檔案是 MapReduce 的一種好用法:使用 Mapper 提取出鍵並按該鍵排序已經完成了構建索引所必需的大量工作。由於這些鍵值儲存大多都是隻讀的檔案只能由批處理作業一次性寫入然後就不可變所以資料結構非常簡單。比如它們就不需要預寫式日誌WAL請參閱 “[讓 B 樹更可靠](ch3.md#讓B樹更可靠)”)。
構建這些資料庫檔案是 MapReduce 的一種好用法:使用 Mapper 提取出鍵並按該鍵排序已經完成了構建索引所必需的大量工作。由於這些鍵值儲存大多都是隻讀的檔案只能由批處理作業一次性寫入然後就不可變所以資料結構非常簡單。比如它們就不需要預寫式日誌WAL請參閱 “[讓 B 樹更可靠](/tw/ch3#讓B樹更可靠)”)。
將資料載入到 Voldemort 時伺服器將繼續用舊資料檔案服務請求同時將新資料檔案從分散式檔案系統複製到伺服器的本地磁碟。一旦複製完成伺服器會自動將查詢切換到新檔案。如果在這個過程中出現任何問題它可以輕易回滾至舊檔案因為它們仍然存在而且不可變【46】。
@@ -456,7 +457,7 @@ MapReduce 作業的輸出處理遵循同樣的原理。透過將輸入視為不
- 同一組檔案可用作各種不同作業的輸入,包括計算指標的監控作業並且評估作業的輸出是否具有預期的性質(例如,將其與前一次執行的輸出進行比較並測量差異) 。
- 與 Unix 工具類似MapReduce 作業將邏輯與佈線(配置輸入和輸出目錄)分離,這使得關注點分離,可以重用程式碼:一個團隊可以專注實現一個做好一件事的作業;而其他團隊可以決定何時何地執行這項作業。
在這些領域,在 Unix 上表現良好的設計原則似乎也適用於 Hadoop但 Unix 和 Hadoop 在某些方面也有所不同。例如,因為大多數 Unix 工具都假設輸入輸出是無型別文字檔案,所以它們必須做大量的輸入解析工作(本章開頭的日誌分析示例使用 `{print $7}` 來提取 URL。在 Hadoop 上可以透過使用更結構化的檔案格式消除一些低價值的語法轉換:比如 Avro請參閱 “[Avro](ch4.md#Avro)”)和 Parquet請參閱 “[列式儲存](ch3.md#列式儲存)”)經常使用,因為它們提供了基於模式的高效編碼,並允許模式隨時間推移而演進(見 [第四章](ch4.md))。
在這些領域,在 Unix 上表現良好的設計原則似乎也適用於 Hadoop但 Unix 和 Hadoop 在某些方面也有所不同。例如,因為大多數 Unix 工具都假設輸入輸出是無型別文字檔案,所以它們必須做大量的輸入解析工作(本章開頭的日誌分析示例使用 `{print $7}` 來提取 URL。在 Hadoop 上可以透過使用更結構化的檔案格式消除一些低價值的語法轉換:比如 Avro請參閱 “[Avro](/tw/ch4#Avro)”)和 Parquet請參閱 “[列式儲存](/tw/ch3#列式儲存)”)經常使用,因為它們提供了基於模式的高效編碼,並允許模式隨時間推移而演進(見 [第四章](/tw/ch4))。
### Hadoop與分散式資料庫的對比
@@ -474,11 +475,11 @@ MapReduce 作業的輸出處理遵循同樣的原理。透過將輸入視為不
在純粹主義者看來,這種仔細的建模和匯入似乎是可取的,因為這意味著資料庫的使用者有更高質量的資料來處理。然而實踐經驗表明,簡單地使資料快速可用 —— 即使它很古怪,難以使用,使用原始格式 —— 也通常要比事先決定理想資料模型要更有價值【54】。
這個想法與資料倉庫類似(請參閱 “[資料倉庫](ch3.md#資料倉庫)”將大型組織的各個部分的資料集中在一起是很有價值的因為它可以跨越以前相互分離的資料集進行連線。MPP 資料庫所要求的謹慎模式設計拖慢了集中式資料收集速度;以原始形式收集資料,稍後再操心模式的設計,能使資料收集速度加快(有時被稱為 “**資料湖data lake**” 或 “**企業資料中心enterprise data hub**”【55】
這個想法與資料倉庫類似(請參閱 “[資料倉庫](/tw/ch3#資料倉庫)”將大型組織的各個部分的資料集中在一起是很有價值的因為它可以跨越以前相互分離的資料集進行連線。MPP 資料庫所要求的謹慎模式設計拖慢了集中式資料收集速度;以原始形式收集資料,稍後再操心模式的設計,能使資料收集速度加快(有時被稱為 “**資料湖data lake**” 或 “**企業資料中心enterprise data hub**”【55】
不加區分的資料轉儲轉移瞭解釋資料的負擔:資料集的生產者不再需要強制將其轉化為標準格式,資料的解釋成為消費者的問題(**讀時模式** 方法【56】請參閱 “[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)”)。如果生產者和消費者是不同優先順序的不同團隊,這可能是一種優勢。甚至可能不存在一個理想的資料模型,對於不同目的有不同的合適視角。以原始形式簡單地轉儲資料,可以允許多種這樣的轉換。這種方法被稱為 **壽司原則sushi principle**“原始資料更好”【57】。
不加區分的資料轉儲轉移瞭解釋資料的負擔:資料集的生產者不再需要強制將其轉化為標準格式,資料的解釋成為消費者的問題(**讀時模式** 方法【56】請參閱 “[文件模型中的模式靈活性](/tw/ch2#文件模型中的模式靈活性)”)。如果生產者和消費者是不同優先順序的不同團隊,這可能是一種優勢。甚至可能不存在一個理想的資料模型,對於不同目的有不同的合適視角。以原始形式簡單地轉儲資料,可以允許多種這樣的轉換。這種方法被稱為 **壽司原則sushi principle**“原始資料更好”【57】。
因此Hadoop 經常被用於實現 ETL 過程(請參閱 “[資料倉庫](ch3.md#資料倉庫)”):事務處理系統中的資料以某種原始形式轉儲到分散式檔案系統中,然後編寫 MapReduce 作業來清理資料,將其轉換為關係形式,並將其匯入 MPP 資料倉庫以進行分析。資料建模仍然在進行,但它在一個單獨的步驟中進行,與資料收集相解耦。這種解耦是可行的,因為分散式檔案系統支援以任何格式編碼的資料。
因此Hadoop 經常被用於實現 ETL 過程(請參閱 “[資料倉庫](/tw/ch3#資料倉庫)”):事務處理系統中的資料以某種原始形式轉儲到分散式檔案系統中,然後編寫 MapReduce 作業來清理資料,將其轉換為關係形式,並將其匯入 MPP 資料倉庫以進行分析。資料建模仍然在進行,但它在一個單獨的步驟中進行,與資料收集相解耦。這種解耦是可行的,因為分散式檔案系統支援以任何格式編碼的資料。
#### 處理模型的多樣性
@@ -492,7 +493,7 @@ MapReduce 使工程師能夠輕鬆地在大型資料集上執行自己的程式
至關重要的是,這些不同的處理模型都可以在共享的單個機器叢集上執行,所有這些機器都可以訪問分散式檔案系統上的相同檔案。在 Hadoop 方式中,不需要將資料匯入到幾個不同的專用系統中進行不同型別的處理:系統足夠靈活,可以支援同一個叢集內不同的工作負載。不需要移動資料,使得從資料中挖掘價值變得容易得多,也使採用新的處理模型容易的多。
Hadoop 生態系統包括隨機訪問的 OLTP 資料庫,如 HBase請參閱 “[SSTables 和 LSM 樹](ch3.md#SSTables和LSM樹)”)和 MPP 風格的分析型資料庫,如 Impala 【41】。HBase 與 Impala 都不使用 MapReduce但都使用 HDFS 進行儲存。它們是迥異的資料訪問與處理方法,但是它們可以共存,並被整合到同一個系統中。
Hadoop 生態系統包括隨機訪問的 OLTP 資料庫,如 HBase請參閱 “[SSTables 和 LSM 樹](/tw/ch3#SSTables和LSM樹)”)和 MPP 風格的分析型資料庫,如 Impala 【41】。HBase 與 Impala 都不使用 MapReduce但都使用 HDFS 進行儲存。它們是迥異的資料訪問與處理方法,但是它們可以共存,並被整合到同一個系統中。
#### 針對頻繁故障設計
@@ -528,7 +529,7 @@ MapReduce 方式更適用於較大的作業:要處理如此之多的資料並
但是MapReduce 執行模型本身也存在一些問題這些問題並沒有透過增加另一個抽象層次而解決而對於某些型別的處理它表現得非常差勁。一方面MapReduce 非常穩健:你可以使用它在任務會頻繁終止的多租戶系統上處理幾乎任意大量級的資料,並且仍然可以完成工作(雖然速度很慢)。另一方面,對於某些型別的處理而言,其他工具有時會快上幾個數量級。
在本章的其餘部分中,我們將介紹一些批處理方法。在 [第十一章](ch11.md) 我們將轉向流處理,它可以看作是加速批處理的另一種方法。
在本章的其餘部分中,我們將介紹一些批處理方法。在 [第十一章](/tw/ch11) 我們將轉向流處理,它可以看作是加速批處理的另一種方法。
### 物化中間狀態
@@ -538,7 +539,7 @@ MapReduce 方式更適用於較大的作業:要處理如此之多的資料並
但在很多情況下,你知道一個作業的輸出只能用作另一個作業的輸入,這些作業由同一個團隊維護。在這種情況下,分散式檔案系統上的檔案只是簡單的 **中間狀態intermediate state**:一種將資料從一個作業傳遞到下一個作業的方式。在一個用於構建推薦系統的,由 50 或 100 個 MapReduce 作業組成的複雜工作流中存在著很多這樣的中間狀態【29】。
將這個中間狀態寫入檔案的過程稱為 **物化materialization**。(在 “[聚合:資料立方體和物化檢視](ch3.md#聚合:資料立方體和物化檢視)” 中已經在物化檢視的背景中遇到過這個術語。它意味著對某個操作的結果立即求值並寫出來,而不是在請求時按需計算)
將這個中間狀態寫入檔案的過程稱為 **物化materialization**。(在 “[聚合:資料立方體和物化檢視](/tw/ch3#聚合:資料立方體和物化檢視)” 中已經在物化檢視的背景中遇到過這個術語。它意味著對某個操作的結果立即求值並寫出來,而不是在請求時按需計算)
作為對照,本章開頭的日誌分析示例使用 Unix 管道將一個命令的輸出與另一個命令的輸入連線起來。管道並沒有完全物化中間狀態,而是隻使用一個小的記憶體緩衝區,將輸出增量地 **流stream** 向輸入。
@@ -597,13 +598,13 @@ Spark、Flink 和 Tez 避免將中間狀態寫入 HDFS因此它們採取了
### 圖與迭代處理
在 “[圖資料模型](ch2.md#圖資料模型)” 中,我們討論了使用圖來建模資料,並使用圖查詢語言來遍歷圖中的邊與點。[第二章](ch2.md) 的討論集中在 OLTP 風格的應用場景:快速執行查詢來查詢少量符合特定條件的頂點。
在 “[圖資料模型](/tw/ch2#圖資料模型)” 中,我們討論了使用圖來建模資料,並使用圖查詢語言來遍歷圖中的邊與點。[第二章](/tw/ch2) 的討論集中在 OLTP 風格的應用場景:快速執行查詢來查詢少量符合特定條件的頂點。
批處理上下文中的圖也很有趣,其目標是在整個圖上執行某種離線處理或分析。這種需求經常出現在機器學習應用(如推薦引擎)或排序系統中。例如,最著名的圖形分析演算法之一是 PageRank 【69】它試圖根據連結到某個網頁的其他網頁來估計該網頁的流行度。它作為配方的一部分用於確定網路搜尋引擎呈現結果的順序。
> 像 Spark、Flink 和 Tez 這樣的資料流引擎(請參閱 “[物化中間狀態](#物化中間狀態)”)通常將運算元作為 **有向無環圖DAG** 的一部分安排在作業中。這與圖處理不一樣:在資料流引擎中,**從一個運算元到另一個運算元的資料流** 被構造成一個圖,而資料本身通常由關係型元組構成。在圖處理中,資料本身具有圖的形式。又一個不幸的命名混亂!
許多圖演算法是透過一次遍歷一條邊來表示的,將一個頂點與近鄰的頂點連線起來,以傳播一些資訊,並不斷重複,直到滿足一些條件為止 —— 例如,直到沒有更多的邊要跟進,或直到一些指標收斂。我們在 [圖 2-6](../img/fig2-6.png) 中看到一個例子,它透過重複跟進標明地點歸屬關係的邊,生成了資料庫中北美包含的所有地點列表(這種演算法被稱為 **傳遞閉包**,即 transitive closure
許多圖演算法是透過一次遍歷一條邊來表示的,將一個頂點與近鄰的頂點連線起來,以傳播一些資訊,並不斷重複,直到滿足一些條件為止 —— 例如,直到沒有更多的邊要跟進,或直到一些指標收斂。我們在 [圖 2-6](/img/fig2-6.png) 中看到一個例子,它透過重複跟進標明地點歸屬關係的邊,生成了資料庫中北美包含的所有地點列表(這種演算法被稱為 **傳遞閉包**,即 transitive closure
可以在分散式檔案系統中儲存圖(包含頂點和邊的列表的檔案),但是這種 “重複至完成” 的想法不能用普通的 MapReduce 來表示,因為它只掃過一趟資料。這種演算法因此經常以 **迭代** 的風格實現:
@@ -621,13 +622,13 @@ Spark、Flink 和 Tez 避免將中間狀態寫入 HDFS因此它們採取了
在每次迭代中,為每個頂點呼叫一個函式,將所有傳送給它的訊息傳遞給它 —— 就像呼叫 Reducer 一樣。與 MapReduce 的不同之處在於,在 Pregel 模型中,頂點在一次迭代到下一次迭代的過程中會記住它的狀態,所以這個函式只需要處理新的傳入訊息。如果圖的某個部分沒有被傳送訊息,那裡就不需要做任何工作。
這與 Actor 模型有些相似(請參閱 “[分散式的 Actor 框架](ch4.md#分散式的Actor框架)”除了頂點狀態和頂點之間的訊息具有容錯性和永續性且通訊以固定的回合進行在每次迭代中框架遞送上次迭代中傳送的所有訊息。Actor 通常沒有這樣的時序保證。
這與 Actor 模型有些相似(請參閱 “[分散式的 Actor 框架](/tw/ch4#分散式的Actor框架)”除了頂點狀態和頂點之間的訊息具有容錯性和永續性且通訊以固定的回合進行在每次迭代中框架遞送上次迭代中傳送的所有訊息。Actor 通常沒有這樣的時序保證。
#### 容錯
頂點只能透過訊息傳遞進行通訊(而不是直接相互查詢)的事實有助於提高 Pregel 作業的效能,因為訊息可以成批處理,且等待通訊的次數也減少了。唯一的等待是在迭代之間:由於 Pregel 模型保證所有在一輪迭代中傳送的訊息都在下輪迭代中送達,所以在下一輪迭代開始前,先前的迭代必須完全完成,而所有的訊息必須在網路上完成複製。
即使底層網路可能丟失、重複或任意延遲訊息(請參閱 “[不可靠的網路](ch8.md#不可靠的網路)”Pregel 的實現能保證在後續迭代中訊息在其目標頂點恰好處理一次。像 MapReduce 一樣,框架能從故障中透明地恢復,以簡化在 Pregel 上實現演算法的程式設計模型。
即使底層網路可能丟失、重複或任意延遲訊息(請參閱 “[不可靠的網路](/tw/ch8#不可靠的網路)”Pregel 的實現能保證在後續迭代中訊息在其目標頂點恰好處理一次。像 MapReduce 一樣,框架能從故障中透明地恢復,以簡化在 Pregel 上實現演算法的程式設計模型。
這種容錯是透過在迭代結束時定期存檔所有頂點的狀態來實現的即將其全部狀態寫入持久化儲存。如果某個節點發生故障並且其記憶體中的狀態丟失則最簡單的解決方法是將整個圖計算回滾到上一個存檔點然後重啟計算。如果演算法是確定性的且訊息記錄在日誌中那麼也可以選擇性地只恢復丟失的分割槽就像之前討論過的資料流引擎【72】。
@@ -658,13 +659,13 @@ Spark、Flink 和 Tez 避免將中間狀態寫入 HDFS因此它們採取了
與硬寫執行連線的程式碼相比指定連線關係運算元的優點是框架可以分析連線輸入的屬性並自動決定哪種上述連線演算法最適合當前任務。Hive、Spark 和 Flink 都有基於代價的查詢最佳化器可以做到這一點甚至可以改變連線順序最小化中間狀態的數量【66,77,78,79】。
連線演算法的選擇可以對批處理作業的效能產生巨大影響,而無需理解和記住本章中討論的各種連線演算法。如果連線是以 **宣告式declarative** 的方式指定的,那這就這是可行的:應用只是簡單地說明哪些連線是必需的,查詢最佳化器決定如何最好地執行連線。我們以前在 “[資料查詢語言](ch2.md#資料查詢語言)” 中見過這個想法。
連線演算法的選擇可以對批處理作業的效能產生巨大影響,而無需理解和記住本章中討論的各種連線演算法。如果連線是以 **宣告式declarative** 的方式指定的,那這就這是可行的:應用只是簡單地說明哪些連線是必需的,查詢最佳化器決定如何最好地執行連線。我們以前在 “[資料查詢語言](/tw/ch2#資料查詢語言)” 中見過這個想法。
但 MapReduce 及其資料流後繼者在其他方面,與 SQL 的完全宣告式查詢模型有很大區別。MapReduce 是圍繞著回撥函式的概念建立的對於每條記錄或者一組記錄呼叫一個使用者定義的函式Mapper 或 Reducer並且該函式可以自由地呼叫任意程式碼來決定輸出什麼。這種方法的優點是可以基於大量已有庫的生態系統創作解析、自然語言分析、影像分析以及執行數值或統計算法等。
自由執行任意程式碼,長期以來都是傳統 MapReduce 批處理系統與 MPP 資料庫的區別所在(請參閱 “[Hadoop 與分散式資料庫的對比](#Hadoop與分散式資料庫的對比)” 一節)。雖然資料庫具有編寫使用者定義函式的功能,但是它們通常使用起來很麻煩,而且與大多數程式語言中廣泛使用的程式包管理器和依賴管理系統相容不佳(例如 Java 的 Maven、Javascript 的 npm 以及 Ruby 的 gems
然而資料流引擎已經發現,支援除連線之外的更多 **宣告式特性** 還有其他的優勢。例如,如果一個回撥函式只包含一個簡單的過濾條件,或者只是從一條記錄中選擇了一些欄位,那麼在為每條記錄呼叫函式時會有相當大的額外 CPU 開銷。如果以宣告方式表示這些簡單的過濾和對映操作,那麼查詢最佳化器可以利用列式儲存佈局(請參閱 “[列式儲存](ch3.md#列式儲存)”只從磁碟讀取所需的列。Hive、Spark DataFrames 和 Impala 還使用了向量化執行(請參閱 “[記憶體頻寬和向量化處理](ch3.md#記憶體頻寬和向量化處理)”):在對 CPU 快取友好的內部迴圈中迭代資料避免函式呼叫。Spark 生成 JVM 位元組碼【79】Impala 使用 LLVM 為這些內部迴圈生成本機程式碼【41】。
然而資料流引擎已經發現,支援除連線之外的更多 **宣告式特性** 還有其他的優勢。例如,如果一個回撥函式只包含一個簡單的過濾條件,或者只是從一條記錄中選擇了一些欄位,那麼在為每條記錄呼叫函式時會有相當大的額外 CPU 開銷。如果以宣告方式表示這些簡單的過濾和對映操作,那麼查詢最佳化器可以利用列式儲存佈局(請參閱 “[列式儲存](/tw/ch3#列式儲存)”只從磁碟讀取所需的列。Hive、Spark DataFrames 和 Impala 還使用了向量化執行(請參閱 “[記憶體頻寬和向量化處理](/tw/ch3#記憶體頻寬和向量化處理)”):在對 CPU 快取友好的內部迴圈中迭代資料避免函式呼叫。Spark 生成 JVM 位元組碼【79】Impala 使用 LLVM 為這些內部迴圈生成本機程式碼【41】。
透過在高階 API 中引入宣告式的部分,並使查詢最佳化器可以在執行期間利用這些來做最佳化,批處理框架看起來越來越像 MPP 資料庫了(並且能實現可與之媲美的效能)。同時,透過擁有執行任意程式碼和以任意格式讀取資料的可擴充套件性,它們保持了靈活性的優勢。
@@ -803,11 +804,4 @@ Spark、Flink 和 Tez 避免將中間狀態寫入 HDFS因此它們採取了
1. Mostafa Mokhtar: “[Hive 0.14 Cost Based Optimizer (CBO) Technical Overview](https://web.archive.org/web/20170607112708/http://hortonworks.com/blog/hive-0-14-cost-based-optimizer-cbo-technical-overview/),” *hortonworks.com*, March 2, 2015.
1. Michael Armbrust, Reynold S Xin, Cheng Lian, et al.: “[Spark SQL: Relational Data Processing in Spark](http://people.csail.mit.edu/matei/papers/2015/sigmod_spark_sql.pdf),” at *ACM International Conference on Management of Data* (SIGMOD), June 2015. [doi:10.1145/2723372.2742797](http://dx.doi.org/10.1145/2723372.2742797)
1. Daniel Blazevski: “[Planting Quadtrees for Apache Flink](https://blog.insightdatascience.com/planting-quadtrees-for-apache-flink-b396ebc80d35),” *insightdataengineering.com*, March 25, 2016.
1. Tom White: “[Genome Analysis Toolkit: Now Using Apache Spark for Data Processing](https://web.archive.org/web/20190215132904/http://blog.cloudera.com/blog/2016/04/genome-analysis-toolkit-now-using-apache-spark-for-data-processing/),” *blog.cloudera.com*, April 6, 2016.
------
| 上一章 | 目錄 | 下一章 |
| --------------------------------- | ------------------------------- | ------------------------ |
| [第三部分:衍生資料](part-iii.md) | [設計資料密集型應用](README.md) | [第十一章:流處理](ch11.md) |
1. Tom White: “[Genome Analysis Toolkit: Now Using Apache Spark for Data Processing](https://web.archive.org/web/20190215132904/http://blog.cloudera.com/blog/2016/04/genome-analysis-toolkit-now-using-apache-spark-for-data-processing/),” *blog.cloudera.com*, April 6, 2016.

View File

@@ -1,18 +1,21 @@
# 第十一章:流處理
---
title: "第十一章:流處理"
linkTitle: "11. 流處理"
weight: 311
breadcrumbs: false
---
![](../img/ch11.png)
![](/img/ch11.png)
> 有效的複雜系統總是從簡單的系統演化而來。反之亦然:從零設計的複雜系統沒一個能有效工作的。
>
> —— 約翰・加爾Systemantics1975
---------------
[TOC]
在 [第十章](/tw/ch10) 中,我們討論了批處理技術,它讀取一組檔案作為輸入,並生成一組新的檔案作為輸出。輸出是 **衍生資料derived data** 的一種形式;也就是說,如果需要,可以透過再次執行批處理過程來重新建立資料集。我們看到了如何使用這個簡單而強大的想法來建立搜尋索引、推薦系統、做分析等等。
在 [第十章](ch10.md) 中,我們討論了批處理技術,它讀取一組檔案作為輸入,並生成一組新的檔案作為輸出。輸出是 **衍生資料derived data** 的一種形式;也就是說,如果需要,可以透過再次執行批處理過程來重新建立資料集。我們看到了如何使用這個簡單而強大的想法來建立搜尋索引、推薦系統、做分析等等
然而,在 [第十章](ch10.md) 中仍然有一個很大的假設即輸入是有界的即已知和有限的大小所以批處理知道它何時完成輸入的讀取。例如MapReduce 核心的排序操作必須讀取其全部輸入,然後才能開始生成輸出:可能發生這種情況:最後一條輸入記錄具有最小的鍵,因此需要第一個被輸出,所以提早開始輸出是不可行的。
然而,在 [第十章](/tw/ch10) 中仍然有一個很大的假設即輸入是有界的即已知和有限的大小所以批處理知道它何時完成輸入的讀取。例如MapReduce 核心的排序操作必須讀取其全部輸入,然後才能開始生成輸出:可能發生這種情況:最後一條輸入記錄具有最小的鍵,因此需要第一個被輸出,所以提早開始輸出是不可行的
實際上,很多資料是 **無界限** 的,因為它隨著時間的推移而逐漸到達:你的使用者在昨天和今天產生了資料,明天他們將繼續產生更多的資料。除非你停業,否則這個過程永遠都不會結束,所以資料集從來就不會以任何有意義的方式 “完成”【1】。因此批處理程式必須將資料人為地分成固定時間段的資料塊例如在每天結束時處理一天的資料或者在每小時結束時處理一小時的資料。
@@ -27,11 +30,11 @@
在批處理領域,作業的輸入和輸出是檔案(也許在分散式檔案系統上)。流處理領域中的等價物看上去是什麼樣子的?
當輸入是一個檔案(一個位元組序列),第一個處理步驟通常是將其解析為一系列記錄。在流處理的上下文中,記錄通常被叫做 **事件event** ,但它本質上是一樣的:一個小的、自包含的、不可變的物件,包含某個時間點發生的某件事情的細節。一個事件通常包含一個來自日曆時鐘的時間戳,以指明事件發生的時間(請參閱 “[單調鍾與日曆時鐘](ch8.md#單調鍾與日曆時鐘)”)。
當輸入是一個檔案(一個位元組序列),第一個處理步驟通常是將其解析為一系列記錄。在流處理的上下文中,記錄通常被叫做 **事件event** ,但它本質上是一樣的:一個小的、自包含的、不可變的物件,包含某個時間點發生的某件事情的細節。一個事件通常包含一個來自日曆時鐘的時間戳,以指明事件發生的時間(請參閱 “[單調鍾與日曆時鐘](/tw/ch8#單調鍾與日曆時鐘)”)。
例如,發生的事件可能是使用者採取的行動,例如檢視頁面或進行購買。它也可能來源於機器,例如對溫度感測器或 CPU 利用率的週期性測量。在 “[使用 Unix 工具的批處理](ch10.md#使用Unix工具的批處理)” 的示例中Web 伺服器日誌的每一行都是一個事件。
例如,發生的事件可能是使用者採取的行動,例如檢視頁面或進行購買。它也可能來源於機器,例如對溫度感測器或 CPU 利用率的週期性測量。在 “[使用 Unix 工具的批處理](/tw/ch10#使用Unix工具的批處理)” 的示例中Web 伺服器日誌的每一行都是一個事件。
事件可能被編碼為文字字串或 JSON或者某種二進位制編碼如 [第四章](ch4.md) 所述。這種編碼允許你儲存一個事件,例如將其追加到一個檔案,將其插入關係表,或將其寫入文件資料庫。它還允許你透過網路將事件傳送到另一個節點以進行處理。
事件可能被編碼為文字字串或 JSON或者某種二進位制編碼如 [第四章](/tw/ch4) 所述。這種編碼允許你儲存一個事件,例如將其追加到一個檔案,將其插入關係表,或將其寫入文件資料庫。它還允許你透過網路將事件傳送到另一個節點以進行處理。
在批處理中,檔案被寫入一次,然後可能被多個作業讀取。類似地,在流處理術語中,一個事件由 **生產者producer** (也稱為 **釋出者publisher****傳送者sender** )生成一次,然後可能由多個 **消費者consumer** **訂閱者subscribers****接收者recipients** 進行處理【3】。在檔案系統中檔名標識一組相關記錄在流式系統中相關的事件通常被聚合為一個 **主題topic****流stream**
@@ -44,21 +47,21 @@
### 訊息傳遞系統
向消費者通知新事件的常用方式是使用 **訊息傳遞系統messaging system**:生產者傳送包含事件的訊息,然後將訊息推送給消費者。我們之前在 “[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)” 中談到了這些系統,但現在我們將詳細介紹這些系統。
向消費者通知新事件的常用方式是使用 **訊息傳遞系統messaging system**:生產者傳送包含事件的訊息,然後將訊息推送給消費者。我們之前在 “[訊息傳遞中的資料流](/tw/ch4#訊息傳遞中的資料流)” 中談到了這些系統,但現在我們將詳細介紹這些系統。
像生產者和消費者之間的 Unix 管道或 TCP 連線這樣的直接通道是實現訊息傳遞系統的簡單方法。但是大多數訊息傳遞系統都在這一基本模型上進行了擴充套件。特別的是Unix 管道和 TCP 將恰好一個傳送者與恰好一個接收者連線,而一個訊息傳遞系統允許多個生產者節點將訊息傳送到同一個主題,並允許多個消費者節點接收主題中的訊息。
在這個 **釋出 / 訂閱** 模式中,不同的系統採取各種各樣的方法,並沒有針對所有目的的通用答案。為了區分這些系統,問一下這兩個問題會特別有幫助:
1. **如果生產者傳送訊息的速度比消費者能夠處理的速度快會發生什麼?** 一般來說,有三種選擇:系統可以丟掉訊息,將訊息放入緩衝佇列,或使用 **背壓**backpressure也稱為 **流量控制**,即 flow control阻塞生產者以免其傳送更多的訊息。例如 Unix 管道和 TCP 就使用了背壓:它們有一個固定大小的小緩衝區,如果填滿,傳送者會被阻塞,直到接收者從緩衝區中取出資料(請參閱 “[網路擁塞和排隊](ch8.md#網路擁塞和排隊)”)。
1. **如果生產者傳送訊息的速度比消費者能夠處理的速度快會發生什麼?** 一般來說,有三種選擇:系統可以丟掉訊息,將訊息放入緩衝佇列,或使用 **背壓**backpressure也稱為 **流量控制**,即 flow control阻塞生產者以免其傳送更多的訊息。例如 Unix 管道和 TCP 就使用了背壓:它們有一個固定大小的小緩衝區,如果填滿,傳送者會被阻塞,直到接收者從緩衝區中取出資料(請參閱 “[網路擁塞和排隊](/tw/ch8#網路擁塞和排隊)”)。
如果訊息被快取在佇列中那麼理解佇列增長會發生什麼是很重要的。當佇列裝不進記憶體時系統會崩潰嗎還是將訊息寫入磁碟如果是這樣磁碟訪問又會如何影響訊息傳遞系統的效能【6】
2. **如果節點崩潰或暫時離線,會發生什麼情況? —— 是否會有訊息丟失?** 與資料庫一樣,永續性可能需要寫入磁碟和 / 或複製的某種組合(請參閱 “[複製與永續性](ch7.md#複製與永續性)”),這是有代價的。如果你能接受有時訊息會丟失,則可能在同一硬體上獲得更高的吞吐量和更低的延遲。
2. **如果節點崩潰或暫時離線,會發生什麼情況? —— 是否會有訊息丟失?** 與資料庫一樣,永續性可能需要寫入磁碟和 / 或複製的某種組合(請參閱 “[複製與永續性](/tw/ch7#複製與永續性)”),這是有代價的。如果你能接受有時訊息會丟失,則可能在同一硬體上獲得更高的吞吐量和更低的延遲。
是否可以接受訊息丟失取決於應用。例如對於週期傳輸的感測器讀數和指標偶爾丟失的資料點可能並不重要因為更新的值會在短時間內發出。但要注意如果大量的訊息被丟棄可能無法立刻意識到指標已經不正確了【7】。如果你正在對事件計數那麼它們能夠可靠送達是更重要的因為每個丟失的訊息都意味著使計數器的錯誤擴大。
我們在 [第十章](ch10.md) 中探討的批處理系統的一個很好的特性是,它們提供了強大的可靠性保證:失敗的任務會自動重試,失敗任務的部分輸出會自動丟棄。這意味著輸出與沒有發生故障一樣,這有助於簡化程式設計模型。在本章的後面,我們將研究如何在流處理的上下文中提供類似的保證。
我們在 [第十章](/tw/ch10) 中探討的批處理系統的一個很好的特性是,它們提供了強大的可靠性保證:失敗的任務會自動重試,失敗任務的部分輸出會自動丟棄。這意味著輸出與沒有發生故障一樣,這有助於簡化程式設計模型。在本章的後面,我們將研究如何在流處理的上下文中提供類似的保證。
#### 直接從生產者傳遞給消費者
@@ -66,8 +69,8 @@
* UDP 組播廣泛應用於金融行業例如股票市場其中低時延非常重要【8】。雖然 UDP 本身是不可靠的,但應用層的協議可以恢復丟失的資料包(生產者必須記住它傳送的資料包,以便能按需重新發送資料包)。
* 無代理的訊息庫,如 ZeroMQ 【9】和 nanomsg 採取類似的方法,透過 TCP 或 IP 多播實現釋出 / 訂閱訊息傳遞。
* StatsD 【10】和 Brubeck 【7】使用不可靠的 UDP 訊息傳遞來收集網路中所有機器的指標並對其進行監控。(在 StatsD 協議中,只有接收到所有訊息,才認為計數器指標是正確的;使用 UDP 將使得指標處在一種最佳近似狀態【11】。另請參閱 “[TCP 與 UDP](ch8.md#TCP與UDP)”
* 如果消費者在網路上公開了服務,生產者可以直接傳送 HTTP 或 RPC 請求(請參閱 “[服務中的資料流REST 與 RPC](ch4.md#服務中的資料流REST與RPC)”)將訊息推送給使用者。這就是 webhooks 背後的想法【12】一種服務的回撥 URL 被註冊到另一個服務中,並且每當事件發生時都會向該 URL 發出請求。
* StatsD 【10】和 Brubeck 【7】使用不可靠的 UDP 訊息傳遞來收集網路中所有機器的指標並對其進行監控。(在 StatsD 協議中,只有接收到所有訊息,才認為計數器指標是正確的;使用 UDP 將使得指標處在一種最佳近似狀態【11】。另請參閱 “[TCP 與 UDP](/tw/ch8#TCP與UDP)”
* 如果消費者在網路上公開了服務,生產者可以直接傳送 HTTP 或 RPC 請求(請參閱 “[服務中的資料流REST 與 RPC](/tw/ch4#服務中的資料流REST與RPC)”)將訊息推送給使用者。這就是 webhooks 背後的想法【12】一種服務的回撥 URL 被註冊到另一個服務中,並且每當事件發生時都會向該 URL 發出請求。
儘管這些直接訊息傳遞系統在設計它們的環境中執行良好,但是它們通常要求應用程式碼意識到訊息丟失的可能性。它們的容錯程度極為有限:即使協議檢測到並重傳在網路中丟失的資料包,它們通常也只是假設生產者和消費者始終線上。
@@ -83,7 +86,7 @@
#### 訊息代理與資料庫的對比
有些訊息代理甚至可以使用 XA 或 JTA 參與兩階段提交協議(請參閱 “[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”)。這個功能與資料庫在本質上非常相似,儘管訊息代理和資料庫之間仍存在實踐上很重要的差異:
有些訊息代理甚至可以使用 XA 或 JTA 參與兩階段提交協議(請參閱 “[實踐中的分散式事務](/tw/ch9#實踐中的分散式事務)”)。這個功能與資料庫在本質上非常相似,儘管訊息代理和資料庫之間仍存在實踐上很重要的差異:
* 資料庫通常保留資料直至顯式刪除,而大多數訊息代理在訊息成功遞送給消費者時會自動刪除訊息。這樣的訊息代理不適合長期的資料儲存。
* 由於它們很快就能刪除訊息,大多數訊息代理都認為它們的工作集相當小 —— 即佇列很短。如果代理需要緩衝很多訊息比如因為消費者速度較慢如果記憶體裝不下訊息可能會溢位到磁碟每個訊息需要更長的處理時間整體吞吐量可能會惡化【6】。
@@ -94,7 +97,7 @@
#### 多個消費者
當多個消費者從同一主題中讀取訊息時,有兩種主要的訊息傳遞模式,如 [圖 11-1](../img/fig11-1.png) 所示:
當多個消費者從同一主題中讀取訊息時,有兩種主要的訊息傳遞模式,如 [圖 11-1](/img/fig11-1.png) 所示:
* 負載均衡load balancing
@@ -104,7 +107,7 @@
每條訊息都被傳遞給 **所有** 消費者。扇出允許幾個獨立的消費者各自 “收聽” 相同的訊息廣播,而不會相互影響 —— 這個流處理中的概念對應批處理中多個不同批處理作業讀取同一份輸入檔案 JMS 中的主題訂閱與 AMQP 中的交叉繫結提供了這一功能)。
![](../img/fig11-1.png)
![](/img/fig11-1.png)
**圖 11-1 a負載平衡在消費者間共享消費主題b扇出將每條訊息傳遞給多個消費者。**
@@ -114,11 +117,11 @@
消費者隨時可能會崩潰,所以有一種可能的情況是:代理向消費者遞送訊息,但消費者沒有處理,或者在消費者崩潰之前只進行了部分處理。為了確保訊息不會丟失,訊息代理使用 **確認acknowledgments**:客戶端必須顯式告知代理訊息處理完畢的時間,以便代理能將訊息從佇列中移除。
如果與客戶端的連線關閉,或者代理超出一段時間未收到確認,代理則認為訊息沒有被處理,因此它將訊息再遞送給另一個消費者。(請注意可能發生這樣的情況,訊息 **實際上是** 處理完畢的,但 **確認** 在網路中丟失了。需要一種原子提交協議才能處理這種情況,正如在 “[實踐中的分散式事務](ch9.md#實踐中的分散式事務)” 中所討論的那樣)
如果與客戶端的連線關閉,或者代理超出一段時間未收到確認,代理則認為訊息沒有被處理,因此它將訊息再遞送給另一個消費者。(請注意可能發生這樣的情況,訊息 **實際上是** 處理完畢的,但 **確認** 在網路中丟失了。需要一種原子提交協議才能處理這種情況,正如在 “[實踐中的分散式事務](/tw/ch9#實踐中的分散式事務)” 中所討論的那樣)
當與負載均衡相結合時,這種重傳行為對訊息的順序有種有趣的影響。在 [圖 11-2](../img/fig11-2.png) 中,消費者通常按照生產者傳送的順序處理訊息。然而消費者 2 在處理訊息 m3 時崩潰,與此同時消費者 1 正在處理訊息 m4。未確認的訊息 m3 隨後被重新發送給消費者 1結果消費者 1 按照 m4m3m5 的順序處理訊息。因此 m3 和 m4 的交付順序與生產者 1 的傳送順序不同。
當與負載均衡相結合時,這種重傳行為對訊息的順序有種有趣的影響。在 [圖 11-2](/img/fig11-2.png) 中,消費者通常按照生產者傳送的順序處理訊息。然而消費者 2 在處理訊息 m3 時崩潰,與此同時消費者 1 正在處理訊息 m4。未確認的訊息 m3 隨後被重新發送給消費者 1結果消費者 1 按照 m4m3m5 的順序處理訊息。因此 m3 和 m4 的交付順序與生產者 1 的傳送順序不同。
![](../img/fig11-2.png)
![](/img/fig11-2.png)
**圖 11-2 在處理 m3 時消費者 2 崩潰,因此稍後重傳至消費者 1**
@@ -130,7 +133,7 @@
資料庫和檔案系統採用截然相反的方法論:至少在某人顯式刪除前,通常寫入資料庫或檔案的所有內容都要被永久記錄下來。
這種思維方式上的差異對建立衍生資料的方式有巨大影響。如 [第十章](ch10.md) 所述,批處理過程的一個關鍵特性是,你可以反覆執行它們,試驗處理步驟,不用擔心損壞輸入(因為輸入是隻讀的)。而 AMQP/JMS 風格的訊息傳遞並非如此:收到訊息是具有破壞性的,因為確認可能導致訊息從代理中被刪除,因此你不能期望再次運行同一個消費者能得到相同的結果。
這種思維方式上的差異對建立衍生資料的方式有巨大影響。如 [第十章](/tw/ch10) 所述,批處理過程的一個關鍵特性是,你可以反覆執行它們,試驗處理步驟,不用擔心損壞輸入(因為輸入是隻讀的)。而 AMQP/JMS 風格的訊息傳遞並非如此:收到訊息是具有破壞性的,因為確認可能導致訊息從代理中被刪除,因此你不能期望再次運行同一個消費者能得到相同的結果。
如果你將新的消費者新增到訊息傳遞系統,通常只能接收到消費者註冊之後開始傳送的訊息。先前的任何訊息都隨風而逝,一去不復返。作為對比,你可以隨時為檔案和資料庫新增新的客戶端,且能讀取任意久遠的資料(只要應用沒有顯式覆蓋或刪除這些資料)。
@@ -138,15 +141,15 @@
#### 使用日誌進行訊息儲存
日誌只是磁碟上簡單的僅追加記錄序列。我們先前在 [第三章](ch3.md) 中日誌結構儲存引擎和預寫式日誌的上下文中討論了日誌,在 [第五章](ch5.md) 複製的上下文裡也討論了它。
日誌只是磁碟上簡單的僅追加記錄序列。我們先前在 [第三章](/tw/ch3) 中日誌結構儲存引擎和預寫式日誌的上下文中討論了日誌,在 [第五章](/tw/ch5) 複製的上下文裡也討論了它。
同樣的結構可以用於實現訊息代理生產者透過將訊息追加到日誌末尾來發送訊息而消費者透過依次讀取日誌來接收訊息。如果消費者讀到日誌末尾則會等待新訊息追加的通知。Unix 工具 `tail -f` 能監視檔案被追加寫入的資料,基本上就是這樣工作的。
為了伸縮超出單個磁碟所能提供的更高吞吐量,可以對日誌進行 **分割槽**(按 [第六章](ch6.md) 的定義)。不同的分割槽可以託管在不同的機器上,使得每個分割槽都有一份能獨立於其他分割槽進行讀寫的日誌。一個主題可以定義為一組攜帶相同型別訊息的分割槽。這種方法如 [圖 11-3](../img/fig11-3.png) 所示。
為了伸縮超出單個磁碟所能提供的更高吞吐量,可以對日誌進行 **分割槽**(按 [第六章](/tw/ch6) 的定義)。不同的分割槽可以託管在不同的機器上,使得每個分割槽都有一份能獨立於其他分割槽進行讀寫的日誌。一個主題可以定義為一組攜帶相同型別訊息的分割槽。這種方法如 [圖 11-3](/img/fig11-3.png) 所示。
在每個分割槽內,代理為每個訊息分配一個單調遞增的序列號或 **偏移量**offset在 [圖 11-3](../img/fig11-3.png) 中,框中的數字是訊息偏移量)。這種序列號是有意義的,因為分割槽是僅追加寫入的,所以分割槽內的訊息是完全有序的。沒有跨不同分割槽的順序保證。
在每個分割槽內,代理為每個訊息分配一個單調遞增的序列號或 **偏移量**offset在 [圖 11-3](/img/fig11-3.png) 中,框中的數字是訊息偏移量)。這種序列號是有意義的,因為分割槽是僅追加寫入的,所以分割槽內的訊息是完全有序的。沒有跨不同分割槽的順序保證。
![](../img/fig11-3.png)
![](/img/fig11-3.png)
**圖 11-3 生產者透過將訊息追加寫入主題分割槽檔案來發送訊息,消費者依次讀取這些檔案**
@@ -159,7 +162,7 @@ Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 Distri
然後每個客戶端將消費被指派分割槽中的 **所有** 訊息。通常情況下,當一個使用者被指派了一個日誌分割槽時,它會以簡單的單執行緒方式順序地讀取分割槽中的訊息。這種粗粒度的負載均衡方法有一些缺點:
* 共享消費主題工作的節點數,最多為該主題中的日誌分割槽數,因為同一個分割槽內的所有訊息被遞送到同一個節點 [^i]。
* 如果某條訊息處理緩慢,則它會阻塞該分割槽中後續訊息的處理(一種行首阻塞的形式;請參閱 “[描述效能](ch1.md#描述效能)”)。
* 如果某條訊息處理緩慢,則它會阻塞該分割槽中後續訊息的處理(一種行首阻塞的形式;請參閱 “[描述效能](/tw/ch1#描述效能)”)。
因此在訊息處理代價高昂希望逐條並行處理以及訊息的順序並沒有那麼重要的情況下JMS/AMQP 風格的訊息代理是可取的。另一方面,在訊息吞吐量很高,處理迅速,順序很重要的情況下,基於日誌的方法表現得非常好。
@@ -169,7 +172,7 @@ Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 Distri
順序消費一個分割槽使得判斷訊息是否已經被處理變得相當容易:所有偏移量小於消費者的當前偏移量的訊息已經被處理,而具有更大偏移量的訊息還沒有被看到。因此,代理不需要跟蹤確認每條訊息,只需要定期記錄消費者的偏移即可。這種方法減少了額外簿記開銷,而且在批處理和流處理中採用這種方法有助於提高基於日誌的系統的吞吐量。
實際上,這種偏移量與單領導者資料庫複製中常見的日誌序列號非常相似,我們在 “[設定新從庫](ch5.md#設定新從庫)” 中討論了這種情況。在資料庫複製中,日誌序列號允許跟隨者斷開連線後,重新連線到領導者,並在不跳過任何寫入的情況下恢復複製。這裡原理完全相同:訊息代理表現得像一個主庫,而消費者就像一個從庫。
實際上,這種偏移量與單領導者資料庫複製中常見的日誌序列號非常相似,我們在 “[設定新從庫](/tw/ch5#設定新從庫)” 中討論了這種情況。在資料庫複製中,日誌序列號允許跟隨者斷開連線後,重新連線到領導者,並在不跳過任何寫入的情況下恢復複製。這裡原理完全相同:訊息代理表現得像一個主庫,而消費者就像一個從庫。
如果消費者節點失效,則失效消費者的分割槽將指派給其他節點,並從最後記錄的偏移量開始消費訊息。如果消費者已經處理了後續的訊息,但還沒有記錄它們的偏移量,那麼重啟後這些訊息將被處理兩次。我們將在本章後面討論這個問題的處理方法。
@@ -208,9 +211,9 @@ Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 Distri
我們之前曾經說過,事件是某個時刻發生的事情的記錄。發生的事情可能是使用者操作(例如鍵入搜尋查詢)或讀取感測器,但也可能是 **寫入資料庫**。某些東西被寫入資料庫的事實是可以被捕獲、儲存和處理的事件。這一觀察結果表明,資料庫和資料流之間的聯絡不僅僅是磁碟日誌的物理儲存 —— 而是更深層的聯絡。
事實上,複製日誌(請參閱 “[複製日誌的實現](ch5.md#複製日誌的實現)”)是一個由資料庫寫入事件組成的流,由主庫在處理事務時生成。從庫將寫入流應用到它們自己的資料庫副本,從而最終得到相同資料的精確副本。複製日誌中的事件描述發生的資料更改。
事實上,複製日誌(請參閱 “[複製日誌的實現](/tw/ch5#複製日誌的實現)”)是一個由資料庫寫入事件組成的流,由主庫在處理事務時生成。從庫將寫入流應用到它們自己的資料庫副本,從而最終得到相同資料的精確副本。複製日誌中的事件描述發生的資料更改。
我們還在 “[全序廣播](ch9.md#全序廣播)” 中遇到了狀態機複製原理,其中指出:如果每個事件代表對資料庫的寫入,並且每個副本按相同的順序處理相同的事件,則副本將達到相同的最終狀態 (假設事件處理是一個確定性的操作)。這是事件流的又一種場景!
我們還在 “[全序廣播](/tw/ch9#全序廣播)” 中遇到了狀態機複製原理,其中指出:如果每個事件代表對資料庫的寫入,並且每個副本按相同的順序處理相同的事件,則副本將達到相同的最終狀態 (假設事件處理是一個確定性的操作)。這是事件流的又一種場景!
在本節中,我們將首先看看異構資料系統中出現的一個問題,然後探討如何透過將事件流的想法帶入資料庫來解決這個問題。
@@ -218,21 +221,21 @@ Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 Distri
正如我們在本書中所看到的,沒有一個系統能夠滿足所有的資料儲存、查詢和處理需求。在實踐中,大多數重要應用都需要組合使用幾種不同的技術來滿足所有的需求:例如,使用 OLTP 資料庫來為使用者請求提供服務,使用快取來加速常見請求,使用全文索引來處理搜尋查詢,使用資料倉庫用於分析。每一種技術都有自己的資料副本,並根據自己的目的進行儲存方式的最佳化。
由於相同或相關的資料出現在了不同的地方,因此相互間需要保持同步:如果某個專案在資料庫中被更新,它也應當在快取、搜尋索引和資料倉庫中被更新。對於資料倉庫,這種同步通常由 ETL 程序執行(請參閱 “[資料倉庫](ch3.md#資料倉庫)”),通常是先取得資料庫的完整副本,然後執行轉換,並批次載入到資料倉庫中 —— 換句話說,批處理。我們在 “[批處理工作流的輸出](ch10.md#批處理工作流的輸出)” 中同樣看到了如何使用批處理建立搜尋索引、推薦系統和其他衍生資料系統。
由於相同或相關的資料出現在了不同的地方,因此相互間需要保持同步:如果某個專案在資料庫中被更新,它也應當在快取、搜尋索引和資料倉庫中被更新。對於資料倉庫,這種同步通常由 ETL 程序執行(請參閱 “[資料倉庫](/tw/ch3#資料倉庫)”),通常是先取得資料庫的完整副本,然後執行轉換,並批次載入到資料倉庫中 —— 換句話說,批處理。我們在 “[批處理工作流的輸出](/tw/ch10#批處理工作流的輸出)” 中同樣看到了如何使用批處理建立搜尋索引、推薦系統和其他衍生資料系統。
如果週期性的完整資料庫轉儲過於緩慢,有時會使用的替代方法是 **雙寫dual write**,其中應用程式碼在資料變更時明確寫入每個系統:例如,首先寫入資料庫,然後更新搜尋索引,然後使快取項失效(甚至同時執行這些寫入)。
但是,雙寫有一些嚴重的問題,其中一個是競爭條件,如 [圖 11-4](../img/fig11-4.png) 所示。在這個例子中,兩個客戶端同時想要更新一個專案 X客戶端 1 想要將值設定為 A客戶端 2 想要將其設定為 B。兩個客戶端首先將新值寫入資料庫然後將其寫入到搜尋索引。因為運氣不好這些請求的時序是交錯的資料庫首先看到來自客戶端 1 的寫入將值設定為 A然後來自客戶端 2 的寫入將值設定為 B因此資料庫中的最終值為 B。搜尋索引首先看到來自客戶端 2 的寫入,然後是客戶端 1 的寫入,所以搜尋索引中的最終值是 A。即使沒發生錯誤這兩個系統現在也永久地不一致了。
但是,雙寫有一些嚴重的問題,其中一個是競爭條件,如 [圖 11-4](/img/fig11-4.png) 所示。在這個例子中,兩個客戶端同時想要更新一個專案 X客戶端 1 想要將值設定為 A客戶端 2 想要將其設定為 B。兩個客戶端首先將新值寫入資料庫然後將其寫入到搜尋索引。因為運氣不好這些請求的時序是交錯的資料庫首先看到來自客戶端 1 的寫入將值設定為 A然後來自客戶端 2 的寫入將值設定為 B因此資料庫中的最終值為 B。搜尋索引首先看到來自客戶端 2 的寫入,然後是客戶端 1 的寫入,所以搜尋索引中的最終值是 A。即使沒發生錯誤這兩個系統現在也永久地不一致了。
![](../img/fig11-4.png)
![](/img/fig11-4.png)
**圖 11-4 在資料庫中 X 首先被設定為 A然後被設定為 B而在搜尋索引處寫入以相反的順序到達**
除非有一些額外的併發檢測機制,例如我們在 “[檢測併發寫入](ch5.md#檢測併發寫入)” 中討論的版本向量,否則你甚至不會意識到發生了併發寫入 —— 一個值將簡單地以無提示方式覆蓋另一個值。
除非有一些額外的併發檢測機制,例如我們在 “[檢測併發寫入](/tw/ch5#檢測併發寫入)” 中討論的版本向量,否則你甚至不會意識到發生了併發寫入 —— 一個值將簡單地以無提示方式覆蓋另一個值。
雙重寫入的另一個問題是,其中一個寫入可能會失敗,而另一個成功。這是一個容錯問題,而不是一個併發問題,但也會造成兩個系統互相不一致的結果。確保它們要麼都成功要麼都失敗,是原子提交問題的一個例子,解決這個問題的代價是昂貴的(請參閱 “[原子提交與兩階段提交](ch9.md#原子提交與兩階段提交)”)。
雙重寫入的另一個問題是,其中一個寫入可能會失敗,而另一個成功。這是一個容錯問題,而不是一個併發問題,但也會造成兩個系統互相不一致的結果。確保它們要麼都成功要麼都失敗,是原子提交問題的一個例子,解決這個問題的代價是昂貴的(請參閱 “[原子提交與兩階段提交](/tw/ch9#原子提交與兩階段提交)”)。
如果你只有一個單領導者複製的資料庫,那麼這個領導者決定了寫入順序,而狀態機複製方法可以在資料庫副本上工作。然而,在 [圖 11-4](../img/fig11-4.png) 中,沒有單個主庫:資料庫可能有一個領導者,搜尋索引也可能有一個領導者,但是兩者都不追隨對方,所以可能會發生衝突(請參閱 “[多主複製](ch5.md#多主複製)”)。
如果你只有一個單領導者複製的資料庫,那麼這個領導者決定了寫入順序,而狀態機複製方法可以在資料庫副本上工作。然而,在 [圖 11-4](/img/fig11-4.png) 中,沒有單個主庫:資料庫可能有一個領導者,搜尋索引也可能有一個領導者,但是兩者都不追隨對方,所以可能會發生衝突(請參閱 “[多主複製](/tw/ch5#多主複製)”)。
如果實際上只有一個領導者 —— 例如,資料庫 —— 而且我們能讓搜尋索引成為資料庫的追隨者,情況要好得多。但這在實踐中可能嗎?
@@ -244,29 +247,29 @@ Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 Distri
最近,人們對 **變更資料捕獲change data capture, CDC** 越來越感興趣這是一種觀察寫入資料庫的所有資料變更並將其提取並轉換為可以複製到其他系統中的形式的過程。CDC 是非常有意思的,尤其是當變更能在被寫入後立刻用於流時。
例如,你可以捕獲資料庫中的變更,並不斷將相同的變更應用至搜尋索引。如果變更日誌以相同的順序應用,則可以預期搜尋索引中的資料與資料庫中的資料是匹配的。搜尋索引和任何其他衍生資料系統只是變更流的消費者,如 [圖 11-5](../img/fig11-5.png) 所示。
例如,你可以捕獲資料庫中的變更,並不斷將相同的變更應用至搜尋索引。如果變更日誌以相同的順序應用,則可以預期搜尋索引中的資料與資料庫中的資料是匹配的。搜尋索引和任何其他衍生資料系統只是變更流的消費者,如 [圖 11-5](/img/fig11-5.png) 所示。
![](../img/fig11-5.png)
![](/img/fig11-5.png)
**圖 11-5 將資料按順序寫入一個數據庫,然後按照相同的順序將這些更改應用到其他系統**
#### 變更資料捕獲的實現
我們可以將日誌消費者叫做 **衍生資料系統**,正如在 [第三部分](part-iii.md) 的介紹中所討論的:儲存在搜尋索引和資料倉庫中的資料,只是 **記錄系統** 資料的額外檢視。變更資料捕獲是一種機制,可確保對記錄系統所做的所有更改都反映在衍生資料系統中,以便衍生系統具有資料的準確副本。
我們可以將日誌消費者叫做 **衍生資料系統**,正如在 [第三部分](/tw/part-iii) 的介紹中所討論的:儲存在搜尋索引和資料倉庫中的資料,只是 **記錄系統** 資料的額外檢視。變更資料捕獲是一種機制,可確保對記錄系統所做的所有更改都反映在衍生資料系統中,以便衍生系統具有資料的準確副本。
從本質上說,變更資料捕獲使得一個數據庫成為領導者(被捕獲變化的資料庫),並將其他元件變為追隨者。基於日誌的訊息代理非常適合從源資料庫傳輸變更事件,因為它保留了訊息的順序(避免了 [圖 11-2](../img/fig11-2.png) 的重新排序問題)。
從本質上說,變更資料捕獲使得一個數據庫成為領導者(被捕獲變化的資料庫),並將其他元件變為追隨者。基於日誌的訊息代理非常適合從源資料庫傳輸變更事件,因為它保留了訊息的順序(避免了 [圖 11-2](/img/fig11-2.png) 的重新排序問題)。
資料庫觸發器可用來實現變更資料捕獲(請參閱 “[基於觸發器的複製](ch5.md#基於觸發器的複製)”),透過註冊觀察所有變更的觸發器,並將相應的變更項寫入變更日誌表中。但是它們往往是脆弱的,而且有顯著的效能開銷。解析複製日誌可能是一種更穩健的方法,但它也很有挑戰,例如如何應對模式變更。
資料庫觸發器可用來實現變更資料捕獲(請參閱 “[基於觸發器的複製](/tw/ch5#基於觸發器的複製)”),透過註冊觀察所有變更的觸發器,並將相應的變更項寫入變更日誌表中。但是它們往往是脆弱的,而且有顯著的效能開銷。解析複製日誌可能是一種更穩健的方法,但它也很有挑戰,例如如何應對模式變更。
LinkedIn 的 Databus【25】Facebook 的 Wormhole【26】和 Yahoo! 的 Sherpa【27】大規模地應用這個思路。Bottled Water 使用解碼 WAL 的 API 實現了 PostgreSQL 的 CDC【28】Maxwell 和 Debezium 透過解析 binlog 對 MySQL 做了類似的事情【29,30,31】Mongoriver 讀取 MongoDB oplog【32,33】而 GoldenGate 為 Oracle 提供類似的功能【34,35】。
類似於訊息代理,變更資料捕獲通常是非同步的:記錄資料庫系統在提交變更之前不會等待消費者應用變更。這種設計具有的運維優勢是,新增緩慢的消費者不會過度影響記錄系統。不過,所有複製延遲可能有的問題在這裡都可能出現(請參閱 “[複製延遲問題](ch5.md#複製延遲問題)”)。
類似於訊息代理,變更資料捕獲通常是非同步的:記錄資料庫系統在提交變更之前不會等待消費者應用變更。這種設計具有的運維優勢是,新增緩慢的消費者不會過度影響記錄系統。不過,所有複製延遲可能有的問題在這裡都可能出現(請參閱 “[複製延遲問題](/tw/ch5#複製延遲問題)”)。
#### 初始快照
如果你擁有 **所有** 對資料庫進行變更的日誌,則可以透過重播該日誌,來重建資料庫的完整狀態。但是在許多情況下,永遠保留所有更改會耗費太多磁碟空間,且重播過於費時,因此日誌需要被截斷。
例如,構建新的全文索引需要整個資料庫的完整副本 —— 僅僅應用最近變更的日誌是不夠的,因為這樣會丟失最近未曾更新的專案。因此,如果你沒有完整的歷史日誌,則需要從一個一致的快照開始,如先前的 “[設定新從庫](ch5.md#設定新從庫)” 中所述。
例如,構建新的全文索引需要整個資料庫的完整副本 —— 僅僅應用最近變更的日誌是不夠的,因為這樣會丟失最近未曾更新的專案。因此,如果你沒有完整的歷史日誌,則需要從一個一致的快照開始,如先前的 “[設定新從庫](/tw/ch5#設定新從庫)” 中所述。
資料庫的快照必須與變更日誌中的已知位置或偏移量相對應,以便在處理完快照後知道從哪裡開始應用變更。一些 CDC 工具集成了這種快照功能,而其他工具則把它留給你手動執行。
@@ -274,7 +277,7 @@ LinkedIn 的 Databus【25】Facebook 的 Wormhole【26】和 Yahoo! 的 Sherp
如果你只能保留有限的歷史日誌,則每次要新增新的衍生資料系統時,都需要做一次快照。但 **日誌壓縮log compaction** 提供了一個很好的備選方案。
我們之前在 “[雜湊索引](ch3.md#雜湊索引)” 中關於日誌結構儲存引擎的上下文中討論了日誌壓縮(請參閱 [圖 3-2](../img/fig3-2.png) 的示例)。原理很簡單:儲存引擎定期在日誌中查詢具有相同鍵的記錄,丟掉所有重複的內容,並只保留每個鍵的最新更新。這個壓縮與合併過程在後臺執行。
我們之前在 “[雜湊索引](/tw/ch3#雜湊索引)” 中關於日誌結構儲存引擎的上下文中討論了日誌壓縮(請參閱 [圖 3-2](/img/fig3-2.png) 的示例)。原理很簡單:儲存引擎定期在日誌中查詢具有相同鍵的記錄,丟掉所有重複的內容,並只保留每個鍵的最新更新。這個壓縮與合併過程在後臺執行。
在日誌結構儲存引擎中,具有特殊值 NULL**墓碑**,即 tombstone的更新表示該鍵被刪除並會在日誌壓縮過程中被移除。但只要鍵不被覆蓋或刪除它就會永遠留在日誌中。這種壓縮日誌所需的磁碟空間僅取決於資料庫的當前內容而不取決於資料庫中曾經發生的寫入次數。如果相同的鍵經常被覆蓋寫入則先前的值將最終將被垃圾回收只有最新的值會保留下來。
@@ -298,14 +301,14 @@ Kafka Connect【41】致力於將廣泛的資料庫系統的變更資料捕獲
與變更資料捕獲類似,事件溯源涉及到 **將所有對應用狀態的變更** 儲存為變更事件日誌。最大的區別是事件溯源將這一想法應用到了一個不同的抽象層次上:
* 在變更資料捕獲中,應用以 **可變方式mutable way** 使用資料庫,可以任意更新和刪除記錄。變更日誌是從資料庫的底層提取的(例如,透過解析複製日誌),從而確保從資料庫中提取的寫入順序與實際寫入的順序相匹配,從而避免 [圖 11-4](../img/fig11-4.png) 中的競態條件。寫入資料庫的應用不需要知道 CDC 的存在。
* 在變更資料捕獲中,應用以 **可變方式mutable way** 使用資料庫,可以任意更新和刪除記錄。變更日誌是從資料庫的底層提取的(例如,透過解析複製日誌),從而確保從資料庫中提取的寫入順序與實際寫入的順序相匹配,從而避免 [圖 11-4](/img/fig11-4.png) 中的競態條件。寫入資料庫的應用不需要知道 CDC 的存在。
* 在事件溯源中,應用邏輯顯式構建在寫入事件日誌的不可變事件之上。在這種情況下,事件儲存是僅追加寫入的,更新與刪除是不鼓勵的或禁止的。事件被設計為旨在反映應用層面發生的事情,而不是底層的狀態變更。
事件溯源是一種強大的資料建模技術:從應用的角度來看,將使用者的行為記錄為不可變的事件更有意義,而不是在可變資料庫中記錄這些行為的影響。事件溯源使得應用隨時間演化更為容易,透過更容易理解事情發生的原因來幫助除錯的進行,並有利於防止應用 Bug請參閱 “[不可變事件的優點](#不可變事件的優點)”)。
例如,儲存 “學生取消選課” 事件以中性的方式清楚地表達了單個行為的意圖,而其副作用 “從登記表中刪除了一個條目,而一條取消原因的記錄被新增到學生反饋表” 則嵌入了很多有關稍後對資料的使用方式的假設。如果引入一個新的應用功能,例如 “將位置留給等待列表中的下一個人” —— 事件溯源方法允許將新的副作用輕鬆地從現有事件中脫開。
事件溯源類似於 **編年史chronicle** 資料模型【45】事件日誌與星型模式中的事實表之間也存在相似之處請參閱 “[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”) 。
事件溯源類似於 **編年史chronicle** 資料模型【45】事件日誌與星型模式中的事實表之間也存在相似之處請參閱 “[星型和雪花型:分析的模式](/tw/ch3#星型和雪花型:分析的模式)”) 。
諸如 Event Store【46】這樣的專業資料庫已經被開發出來供使用事件溯源的應用使用但總的來說這種方法獨立於任何特定的工具。傳統的資料庫或基於日誌的訊息代理也可以用來構建這種風格的應用。
@@ -326,17 +329,17 @@ Kafka Connect【41】致力於將廣泛的資料庫系統的變更資料捕獲
事件溯源的哲學是仔細區分 **事件event****命令command**【48】。當來自使用者的請求剛到達時它一開始是一個命令在這個時間點上它仍然可能失敗比如因為違反了一些完整性條件。應用必須首先驗證它是否可以執行該命令。如果驗證成功並且命令被接受則它變為一個持久化且不可變的事件。
例如,如果使用者試圖註冊特定使用者名稱,或預定飛機或劇院的座位,則應用需要檢查使用者名稱或座位是否已被佔用。(先前在 “[容錯共識](ch9.md#容錯共識)” 中討論過這個例子)當檢查成功時,應用可以生成一個事件,指示特定的使用者名稱是由特定的使用者 ID 註冊的,或者座位已經預留給特定的顧客。
例如,如果使用者試圖註冊特定使用者名稱,或預定飛機或劇院的座位,則應用需要檢查使用者名稱或座位是否已被佔用。(先前在 “[容錯共識](/tw/ch9#容錯共識)” 中討論過這個例子)當檢查成功時,應用可以生成一個事件,指示特定的使用者名稱是由特定的使用者 ID 註冊的,或者座位已經預留給特定的顧客。
在事件生成的時刻,它就成為了 **事實fact**。即使客戶稍後決定更改或取消預訂,他們之前曾預定了某個特定座位的事實仍然成立,而更改或取消是之後新增的單獨的事件。
事件流的消費者不允許拒絕事件:當消費者看到事件時,它已經成為日誌中不可變的一部分,並且可能已經被其他消費者看到了。因此任何對命令的驗證,都需要在它成為事件之前同步完成。例如,透過使用一個可以原子性地自動驗證命令併發布事件的可序列事務。
或者,預訂座位的使用者請求可以拆分為兩個事件:第一個是暫時預約,第二個是驗證預約後的獨立的確認事件(如 “[使用全序廣播實現線性一致的儲存](ch9.md#使用全序廣播實現線性一致的儲存)” 中所述) 。這種分割方式允許驗證發生在一個非同步的過程中。
或者,預訂座位的使用者請求可以拆分為兩個事件:第一個是暫時預約,第二個是驗證預約後的獨立的確認事件(如 “[使用全序廣播實現線性一致的儲存](/tw/ch9#使用全序廣播實現線性一致的儲存)” 中所述) 。這種分割方式允許驗證發生在一個非同步的過程中。
### 狀態、流和不變性
我們在 [第十章](ch10.md) 中看到,批處理因其輸入檔案不變性而受益良多,你可以在現有輸入檔案上執行實驗性處理作業,而不用擔心損壞它們。這種不變性原則也是使得事件溯源與變更資料捕獲如此強大的原因。
我們在 [第十章](/tw/ch10) 中看到,批處理因其輸入檔案不變性而受益良多,你可以在現有輸入檔案上執行實驗性處理作業,而不用擔心損壞它們。這種不變性原則也是使得事件溯源與變更資料捕獲如此強大的原因。
我們通常將資料庫視為應用程式當前狀態的儲存 —— 這種表示針對讀取進行了最佳化,而且通常對於服務查詢而言是最為方便的表示。狀態的本質是,它會變化,所以資料庫才會支援資料的增刪改。這又該如何匹配不變性呢?
@@ -344,13 +347,13 @@ Kafka Connect【41】致力於將廣泛的資料庫系統的變更資料捕獲
無論狀態如何變化,總是有一系列事件導致了這些變化。即使事情已經執行與回滾,這些事件出現是始終成立的。關鍵的想法是:可變的狀態與不可變事件的僅追加日誌相互之間並不矛盾:它們是一體兩面,互為陰陽的。所有變化的日誌 —— **變化日誌changelog**,表示了隨時間演變的狀態。
如果你傾向於數學表示,那麼你可能會說,應用狀態是事件流對時間求積分得到的結果,而變更流是狀態對時間求微分的結果,如 [圖 11-6](../img/fig11-6.png) 所示【49,50,51】。這個比喻有一些侷限性例如狀態的二階導似乎沒有意義但這是考慮資料的一個實用出發點。
如果你傾向於數學表示,那麼你可能會說,應用狀態是事件流對時間求積分得到的結果,而變更流是狀態對時間求微分的結果,如 [圖 11-6](/img/fig11-6.png) 所示【49,50,51】。這個比喻有一些侷限性例如狀態的二階導似乎沒有意義但這是考慮資料的一個實用出發點。
$$
state(now) = \int_{t=0}^{now}{stream(t) \ dt} \\
stream(t) = \frac{d\ state(t)}{dt}
$$
![](../img/fig11-6.png)
![](/img/fig11-6.png)
**圖 11-6 應用當前狀態與事件流之間的關係**
@@ -366,35 +369,35 @@ $$
如果發生錯誤,會計師不會刪除或更改分類帳中的錯誤交易 —— 而是新增另一筆交易以補償錯誤例如退還一筆不正確的費用。不正確的交易將永遠保留在分類帳中對於審計而言可能非常重要。如果從不正確的分類賬衍生出的錯誤數字已經公佈那麼下一個會計週期的數字就會包括一個更正。這個過程在會計事務中是很常見的【54】。
儘管這種可審計性只在金融系統中尤其重要,但對於不受這種嚴格監管的許多其他系統,也是很有幫助的。如 “[批處理輸出的哲學](ch10.md#批處理輸出的哲學)” 中所討論的,如果你意外地部署了將錯誤資料寫入資料庫的錯誤程式碼,當代碼會破壞性地覆寫資料時,恢復要困難得多。使用不可變事件的僅追加日誌,診斷問題與故障恢復就要容易的多。
儘管這種可審計性只在金融系統中尤其重要,但對於不受這種嚴格監管的許多其他系統,也是很有幫助的。如 “[批處理輸出的哲學](/tw/ch10#批處理輸出的哲學)” 中所討論的,如果你意外地部署了將錯誤資料寫入資料庫的錯誤程式碼,當代碼會破壞性地覆寫資料時,恢復要困難得多。使用不可變事件的僅追加日誌,診斷問題與故障恢復就要容易的多。
不可變的事件也包含了比當前狀態更多的資訊。例如在購物網站上顧客可以將物品新增到他們的購物車然後再將其移除。雖然從履行訂單的角度第二個事件取消了第一個事件但對分析目的而言知道客戶考慮過某個特定項而之後又反悔可能是很有用的。也許他們會選擇在未來購買或者他們已經找到了替代品。這個資訊被記錄在事件日誌中但對於移出購物車就刪除記錄的資料庫而言這個資訊在移出購物車時可能就丟失了【42】。
#### 從同一事件日誌中派生多個檢視
此外,透過從不變的事件日誌中分離出可變的狀態,你可以針對不同的讀取方式,從相同的事件日誌中衍生出幾種不同的表現形式。效果就像一個流的多個消費者一樣([圖 11-5](../img/fig11-5.png)):例如,分析型資料庫 Druid 使用這種方式直接從 Kafka 攝取資料【55】Pistachio 是一個分散式的鍵值儲存,使用 Kafka 作為提交日誌【56】Kafka Connect 能將來自 Kafka 的資料匯出到各種不同的資料庫與索引【41】。這對於許多其他儲存和索引系統如搜尋伺服器來說是很有意義的當系統要從分散式日誌中獲取輸入時亦然請參閱 “[保持系統同步](#保持系統同步)”)。
此外,透過從不變的事件日誌中分離出可變的狀態,你可以針對不同的讀取方式,從相同的事件日誌中衍生出幾種不同的表現形式。效果就像一個流的多個消費者一樣([圖 11-5](/img/fig11-5.png)):例如,分析型資料庫 Druid 使用這種方式直接從 Kafka 攝取資料【55】Pistachio 是一個分散式的鍵值儲存,使用 Kafka 作為提交日誌【56】Kafka Connect 能將來自 Kafka 的資料匯出到各種不同的資料庫與索引【41】。這對於許多其他儲存和索引系統如搜尋伺服器來說是很有意義的當系統要從分散式日誌中獲取輸入時亦然請參閱 “[保持系統同步](#保持系統同步)”)。
新增從事件日誌到資料庫的顯式轉換能夠使應用更容易地隨時間演進如果你想要引入一個新功能以新的方式表示現有資料則可以使用事件日誌來構建一個單獨的、針對新功能的讀取最佳化檢視無需修改現有系統而與之共存。並行執行新舊系統通常比在現有系統中執行複雜的模式遷移更容易。一旦不再需要舊的系統你可以簡單地關閉它並回收其資源【47,57】。
如果你不需要擔心如何查詢與訪問資料,那麼儲存資料通常是非常簡單的。模式設計、索引和儲存引擎的許多複雜性,都是希望支援某些特定查詢和訪問模式的結果(請參閱 [第三章](ch3.md))。出於這個原因,透過將資料寫入的形式與讀取形式相分離,並允許幾個不同的讀取檢視,你能獲得很大的靈活性。這個想法有時被稱為 **命令查詢責任分離command query responsibility segregation, CQRS**【42,58,59】。
如果你不需要擔心如何查詢與訪問資料,那麼儲存資料通常是非常簡單的。模式設計、索引和儲存引擎的許多複雜性,都是希望支援某些特定查詢和訪問模式的結果(請參閱 [第三章](/tw/ch3))。出於這個原因,透過將資料寫入的形式與讀取形式相分離,並允許幾個不同的讀取檢視,你能獲得很大的靈活性。這個想法有時被稱為 **命令查詢責任分離command query responsibility segregation, CQRS**【42,58,59】。
資料庫和模式設計的傳統方法是基於這樣一種謬論,資料必須以與查詢相同的形式寫入。如果可以將資料從針對寫入最佳化的事件日誌轉換為針對讀取最佳化的應用狀態,那麼有關規範化和非規範化的爭論就變得無關緊要了(請參閱 “[多對一和多對多的關係](ch2.md#多對一和多對多的關係)”):在針對讀取最佳化的檢視中對資料進行非規範化是完全合理的,因為翻譯過程提供了使其與事件日誌保持一致的機制。
資料庫和模式設計的傳統方法是基於這樣一種謬論,資料必須以與查詢相同的形式寫入。如果可以將資料從針對寫入最佳化的事件日誌轉換為針對讀取最佳化的應用狀態,那麼有關規範化和非規範化的爭論就變得無關緊要了(請參閱 “[多對一和多對多的關係](/tw/ch2#多對一和多對多的關係)”):在針對讀取最佳化的檢視中對資料進行非規範化是完全合理的,因為翻譯過程提供了使其與事件日誌保持一致的機制。
在 “[描述負載](ch1.md#描述負載)” 中,我們討論了推特主頁時間線,它是特定使用者關注的人群所發推特的快取(類似郵箱)。這是 **針對讀取最佳化的狀態** 的又一個例子:主頁時間線是高度非規範化的,因為你的推文與你所有粉絲的時間線都構成了重複。然而,扇出服務保持了這種重複狀態與新推特以及新關注關係的同步,從而保證了重複的可管理性。
在 “[描述負載](/tw/ch1#描述負載)” 中,我們討論了推特主頁時間線,它是特定使用者關注的人群所發推特的快取(類似郵箱)。這是 **針對讀取最佳化的狀態** 的又一個例子:主頁時間線是高度非規範化的,因為你的推文與你所有粉絲的時間線都構成了重複。然而,扇出服務保持了這種重複狀態與新推特以及新關注關係的同步,從而保證了重複的可管理性。
#### 併發控制
事件溯源和變更資料捕獲的最大缺點是,事件日誌的消費者通常是非同步的,所以可能會出現這樣的情況:使用者會寫入日誌,然後從日誌衍生檢視中讀取,結果發現他的寫入還沒有反映在讀取檢視中。我們之前在 “[讀己之寫](ch5.md#讀己之寫)” 中討論了這個問題以及可能的解決方案。
事件溯源和變更資料捕獲的最大缺點是,事件日誌的消費者通常是非同步的,所以可能會出現這樣的情況:使用者會寫入日誌,然後從日誌衍生檢視中讀取,結果發現他的寫入還沒有反映在讀取檢視中。我們之前在 “[讀己之寫](/tw/ch5#讀己之寫)” 中討論了這個問題以及可能的解決方案。
一種解決方案是將事件追加到日誌時同步執行讀取檢視的更新。而將這些寫入操作合併為一個原子單元需要 **事務**,所以要麼將事件日誌和讀取檢視儲存在同一個儲存系統中,要麼就需要跨不同系統進行分散式事務。或者,你也可以使用在 “[使用全序廣播實現線性一致的儲存](ch9.md#使用全序廣播實現線性一致的儲存)” 中討論的方法。
一種解決方案是將事件追加到日誌時同步執行讀取檢視的更新。而將這些寫入操作合併為一個原子單元需要 **事務**,所以要麼將事件日誌和讀取檢視儲存在同一個儲存系統中,要麼就需要跨不同系統進行分散式事務。或者,你也可以使用在 “[使用全序廣播實現線性一致的儲存](/tw/ch9#使用全序廣播實現線性一致的儲存)” 中討論的方法。
另一方面,從事件日誌匯出當前狀態也簡化了併發控制的某些部分。許多對於多物件事務的需求(請參閱 “[單物件和多物件操作](ch7.md#單物件和多物件操作)”)源於單個使用者操作需要在多個不同的位置更改資料。透過事件溯源,你可以設計一個自包含的事件以表示一個使用者操作。然後使用者操作就只需要在一個地方進行單次寫入操作 —— 即將事件附加到日誌中 —— 這個還是很容易使原子化的。
另一方面,從事件日誌匯出當前狀態也簡化了併發控制的某些部分。許多對於多物件事務的需求(請參閱 “[單物件和多物件操作](/tw/ch7#單物件和多物件操作)”)源於單個使用者操作需要在多個不同的位置更改資料。透過事件溯源,你可以設計一個自包含的事件以表示一個使用者操作。然後使用者操作就只需要在一個地方進行單次寫入操作 —— 即將事件附加到日誌中 —— 這個還是很容易使原子化的。
如果事件日誌與應用狀態以相同的方式分割槽(例如,處理分割槽 3 中的客戶事件只需要更新分割槽 3 中的應用狀態),那麼直接使用單執行緒日誌消費者就不需要寫入併發控制了。它從設計上一次只處理一個事件(請參閱 “[真的序列執行](ch7.md#真的序列執行)”。日誌透過在分割槽中定義事件的序列順序消除了併發性的不確定性【24】。如果一個事件觸及多個狀態分割槽那麼需要做更多的工作我們將在 [第十二章](ch12.md) 討論。
如果事件日誌與應用狀態以相同的方式分割槽(例如,處理分割槽 3 中的客戶事件只需要更新分割槽 3 中的應用狀態),那麼直接使用單執行緒日誌消費者就不需要寫入併發控制了。它從設計上一次只處理一個事件(請參閱 “[真的序列執行](/tw/ch7#真的序列執行)”。日誌透過在分割槽中定義事件的序列順序消除了併發性的不確定性【24】。如果一個事件觸及多個狀態分割槽那麼需要做更多的工作我們將在 [第十二章](/tw/ch12) 討論。
#### 不變性的侷限性
許多不使用事件溯源模型的系統也還是依賴不可變性:各種資料庫在內部使用不可變的資料結構或多版本資料來支援時間點快照(請參閱 “[索引和快照隔離](ch7.md#索引和快照隔離)” 。Git、Mercurial 和 Fossil 等版本控制系統也依靠不可變的資料來儲存檔案的版本歷史記錄。
許多不使用事件溯源模型的系統也還是依賴不可變性:各種資料庫在內部使用不可變的資料結構或多版本資料來支援時間點快照(請參閱 “[索引和快照隔離](/tw/ch7#索引和快照隔離)” 。Git、Mercurial 和 Fossil 等版本控制系統也依靠不可變的資料來儲存檔案的版本歷史記錄。
永遠保持所有變更的不變歷史,在多大程度上是可行的?答案取決於資料集的流失率。一些工作負載主要是新增資料,很少更新或刪除;它們很容易保持不變。其他工作負載在相對較小的資料集上有較高的更新 / 刪除率在這些情況下不可變的歷史可能增至難以接受的巨大碎片化可能成為一個問題壓縮與垃圾收集的表現對於運維的穩健性變得至關重要【60,61】。
@@ -402,7 +405,7 @@ $$
在這種情況下,僅僅在日誌中新增另一個事件來指明先前的資料應該被視為刪除是不夠的 —— 你實際上是想改寫歷史並假裝資料從一開始就沒有寫入。例如Datomic 管這個特性叫 **切除excision** 【62】而 Fossil 版本控制系統有一個類似的概念叫 **避免shunning** 【63】。
真正刪除資料是非常非常困難的【64】因為副本可能存在於很多地方例如儲存引擎檔案系統和 SSD 通常會向一個新位置寫入而不是原地覆蓋舊資料【52】而備份通常是特意做成不可變的防止意外刪除或損壞。刪除操作更多的是指 “使取回資料更困難”,而不是指 “使取回資料不可能”。無論如何,有時你必須得嘗試,正如我們在 “[立法與自律](ch12.md#立法與自律)” 中所看到的。
真正刪除資料是非常非常困難的【64】因為副本可能存在於很多地方例如儲存引擎檔案系統和 SSD 通常會向一個新位置寫入而不是原地覆蓋舊資料【52】而備份通常是特意做成不可變的防止意外刪除或損壞。刪除操作更多的是指 “使取回資料更困難”,而不是指 “使取回資料不可能”。無論如何,有時你必須得嘗試,正如我們在 “[立法與自律](/tw/ch12#立法與自律)” 中所看到的。
## 流處理
@@ -411,15 +414,15 @@ $$
剩下的就是討論一下你可以用流做什麼 —— 也就是說,你可以處理它。一般來說,有三種選項:
1. 你可以將事件中的資料寫入資料庫、快取、搜尋索引或類似的儲存系統,然後能被其他客戶端查詢。如 [圖 11-5](../img/fig11-5.png) 所示,這是資料庫與系統其他部分所發生的變更保持同步的好方法 —— 特別是當流消費者是寫入資料庫的唯一客戶端時。如 “[批處理工作流的輸出](ch10.md#批處理工作流的輸出)” 中所討論的,它是寫入儲存系統的流等價物。
1. 你可以將事件中的資料寫入資料庫、快取、搜尋索引或類似的儲存系統,然後能被其他客戶端查詢。如 [圖 11-5](/img/fig11-5.png) 所示,這是資料庫與系統其他部分所發生的變更保持同步的好方法 —— 特別是當流消費者是寫入資料庫的唯一客戶端時。如 “[批處理工作流的輸出](/tw/ch10#批處理工作流的輸出)” 中所討論的,它是寫入儲存系統的流等價物。
2. 你能以某種方式將事件推送給使用者,例如傳送報警郵件或推送通知,或將事件流式傳輸到可即時顯示的儀表板上。在這種情況下,人是流的最終消費者。
3. 你可以處理一個或多個輸入流,併產生一個或多個輸出流。流可能會經過由幾個這樣的處理階段組成的流水線,最後再輸出(選項 1 或 2
在本章的剩餘部分中,我們將討論選項 3處理流以產生其他衍生流。處理這樣的流的程式碼片段被稱為 **運算元operator****作業job**。它與我們在 [第十章](ch10.md) 中討論過的 Unix 程序和 MapReduce 作業密切相關,資料流的模式是相似的:一個流處理器以只讀的方式使用輸入流,並將其輸出以僅追加的方式寫入一個不同的位置。
在本章的剩餘部分中,我們將討論選項 3處理流以產生其他衍生流。處理這樣的流的程式碼片段被稱為 **運算元operator****作業job**。它與我們在 [第十章](/tw/ch10) 中討論過的 Unix 程序和 MapReduce 作業密切相關,資料流的模式是相似的:一個流處理器以只讀的方式使用輸入流,並將其輸出以僅追加的方式寫入一個不同的位置。
流處理中的分割槽和並行化模式也非常類似於 [第十章](ch10.md) 中介紹的 MapReduce 和資料流引擎,因此我們不再重複這些主題。基本的 Map 操作(如轉換和過濾記錄)也是一樣的。
流處理中的分割槽和並行化模式也非常類似於 [第十章](/tw/ch10) 中介紹的 MapReduce 和資料流引擎,因此我們不再重複這些主題。基本的 Map 操作(如轉換和過濾記錄)也是一樣的。
與批次作業相比的一個關鍵區別是,流不會結束。這種差異會帶來很多隱含的結果。正如本章開始部分所討論的,排序對無界資料集沒有意義,因此無法使用 **排序合併連線**(請參閱 “[Reduce 側連線與分組](ch10.md#Reduce側連線與分組)”)。容錯機制也必須改變:對於已經運行了幾分鐘的批處理作業,可以簡單地從頭開始重啟失敗任務,但是對於已經執行數年的流作業,重啟後從頭開始跑可能並不是一個可行的選項。
與批次作業相比的一個關鍵區別是,流不會結束。這種差異會帶來很多隱含的結果。正如本章開始部分所討論的,排序對無界資料集沒有意義,因此無法使用 **排序合併連線**(請參閱 “[Reduce 側連線與分組](/tw/ch10#Reduce側連線與分組)”)。容錯機制也必須改變:對於已經運行了幾分鐘的批處理作業,可以簡單地從頭開始重啟失敗任務,但是對於已經執行數年的流作業,重啟後從頭開始跑可能並不是一個可行的選項。
### 流處理的應用
@@ -452,13 +455,13 @@ CEP 的實現包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
這些統計值通常是在固定時間區間內進行計算的,例如,你可能想知道在過去 5 分鐘內服務每秒查詢次數的均值,以及此時間段內響應時間的第 99 百分位點。在幾分鐘內取平均,能抹平秒和秒之間的無關波動,且仍然能向你展示流量模式的時間圖景。聚合的時間間隔稱為 **視窗window**,我們將在 “[時間推理](#時間推理)” 中更詳細地討論視窗。
流分析系統有時會使用機率演算法,例如 Bloom filter我們在 “[效能最佳化](ch3.md#效能最佳化)” 中遇到過來管理成員資格HyperLogLog【72】用於基數估計以及各種百分比估計算法請參閱 “[實踐中的百分位點](ch1.md#實踐中的百分位點)”。機率演算法產出近似的結果但比起精確演算法的優點是記憶體使用要少得多。使用近似演算法有時讓人們覺得流處理系統總是有損的和不精確的但這是錯誤看法流處理並沒有任何內在的近似性而機率演算法只是一種最佳化【73】。
流分析系統有時會使用機率演算法,例如 Bloom filter我們在 “[效能最佳化](/tw/ch3#效能最佳化)” 中遇到過來管理成員資格HyperLogLog【72】用於基數估計以及各種百分比估計算法請參閱 “[實踐中的百分位點](/tw/ch1#實踐中的百分位點)”。機率演算法產出近似的結果但比起精確演算法的優點是記憶體使用要少得多。使用近似演算法有時讓人們覺得流處理系統總是有損的和不精確的但這是錯誤看法流處理並沒有任何內在的近似性而機率演算法只是一種最佳化【73】。
許多開源分散式流處理框架的設計都是針對分析設計的:例如 Apache Storm、Spark Streaming、Flink、Concord、Samza 和 Kafka Streams 【74】。託管服務包括 Google Cloud Dataflow 和 Azure Stream Analytics。
#### 維護物化檢視
我們在 “[資料庫與流](#資料庫與流)” 中看到,資料庫的變更流可以用於維護衍生資料系統(如快取、搜尋索引和資料倉庫),並使其與源資料庫保持最新。我們可以將這些示例視作維護 **物化檢視materialized view** 的一種具體場景(請參閱 “[聚合:資料立方體和物化檢視](ch3.md#聚合:資料立方體和物化檢視)”在某個資料集上衍生出一個替代檢視以便高效查詢並在底層資料變更時更新檢視【50】。
我們在 “[資料庫與流](#資料庫與流)” 中看到,資料庫的變更流可以用於維護衍生資料系統(如快取、搜尋索引和資料倉庫),並使其與源資料庫保持最新。我們可以將這些示例視作維護 **物化檢視materialized view** 的一種具體場景(請參閱 “[聚合:資料立方體和物化檢視](/tw/ch3#聚合:資料立方體和物化檢視)”在某個資料集上衍生出一個替代檢視以便高效查詢並在底層資料變更時更新檢視【50】。
同樣,在事件溯源中,應用程式的狀態是透過應用事件日誌來維護的;這裡的應用程式狀態也是一種物化檢視。與流分析場景不同的是,僅考慮某個時間視窗內的事件通常是不夠的:構建物化檢視可能需要任意時間段內的 **所有** 事件,除了那些可能由日誌壓縮丟棄的過時事件(請參閱 “[日誌壓縮](#日誌壓縮)”)。實際上,你需要一個可以一直延伸到時間開端的視窗。
@@ -474,13 +477,13 @@ CEP 的實現包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
#### 訊息傳遞和RPC
在 “[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)” 中我們討論過,訊息傳遞系統可以作為 RPC 的替代方案,即作為一種服務間通訊的機制,比如在 Actor 模型中所使用的那樣。儘管這些系統也是基於訊息和事件,但我們通常不會將其視作流處理元件:
在 “[訊息傳遞中的資料流](/tw/ch4#訊息傳遞中的資料流)” 中我們討論過,訊息傳遞系統可以作為 RPC 的替代方案,即作為一種服務間通訊的機制,比如在 Actor 模型中所使用的那樣。儘管這些系統也是基於訊息和事件,但我們通常不會將其視作流處理元件:
* Actor 框架主要是管理模組通訊的併發和分散式執行的一種機制,而流處理主要是一種資料管理技術。
* Actor 之間的交流往往是短暫的、一對一的;而事件日誌則是持久的、多訂閱者的。
* Actor 可以以任意方式進行通訊(包括迴圈的請求 / 響應模式),但流處理通常配置在無環流水線中,其中每個流都是一個特定作業的輸出,由良好定義的輸入流中派生而來。
也就是說RPC 類系統與流處理之間有一些交叉領域。例如Apache Storm 有一個稱為 **分散式 RPC** 的功能它允許將使用者查詢分散到一系列也處理事件流的節點上然後這些查詢與來自輸入流的事件交織而結果可以被彙總併發回給使用者【78】另請參閱 “[多分割槽資料處理](ch12.md#多分割槽資料處理)”)。
也就是說RPC 類系統與流處理之間有一些交叉領域。例如Apache Storm 有一個稱為 **分散式 RPC** 的功能它允許將使用者查詢分散到一系列也處理事件流的節點上然後這些查詢與來自輸入流的事件交織而結果可以被彙總併發回給使用者【78】另請參閱 “[多分割槽資料處理](/tw/ch12#多分割槽資料處理)”)。
也可以使用 Actor 框架來處理流。但是,很多這樣的框架在崩潰時不能保證訊息的傳遞,除非你實現了額外的重試邏輯,否則這種處理不是容錯的。
@@ -490,13 +493,13 @@ CEP 的實現包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
在批處理中過程中,大量的歷史事件被快速地處理。如果需要按時間來分析,批處理器需要檢查每個事件中嵌入的時間戳。讀取執行批處理機器的系統時鐘沒有任何意義,因為處理執行的時間與事件實際發生的時間無關。
批處理可以在幾分鐘內讀取一年的歷史事件;在大多數情況下,感興趣的時間線是歷史中的一年,而不是處理中的幾分鐘。而且使用事件中的時間戳,使得處理是 **確定性** 的:在相同的輸入上再次執行相同的處理過程會得到相同的結果(請參閱 “[容錯](ch10.md#容錯)”)。
批處理可以在幾分鐘內讀取一年的歷史事件;在大多數情況下,感興趣的時間線是歷史中的一年,而不是處理中的幾分鐘。而且使用事件中的時間戳,使得處理是 **確定性** 的:在相同的輸入上再次執行相同的處理過程會得到相同的結果(請參閱 “[容錯](/tw/ch10#容錯)”)。
另一方面,許多流處理框架使用處理機器上的本地系統時鐘(**處理時間**,即 processing time來確定 **視窗windowing**【79】。這種方法的優點是簡單如果事件建立與事件處理之間的延遲可以忽略不計那也是合理的。然而如果存在任何顯著的處理延遲 —— 即,事件處理顯著地晚於事件實際發生的時間,這種處理方式就失效了。
#### 事件時間與處理時間
很多原因都可能導致處理延遲:排隊,網路故障(請參閱 “[不可靠的網路](ch8.md#不可靠的網路)”),效能問題導致訊息代理 / 訊息處理器出現爭用,流消費者重啟,從故障中恢復時重新處理過去的事件(請參閱 “[重播舊訊息](#重播舊訊息)”),或者在修復程式碼 BUG 之後。
很多原因都可能導致處理延遲:排隊,網路故障(請參閱 “[不可靠的網路](/tw/ch8#不可靠的網路)”),效能問題導致訊息代理 / 訊息處理器出現爭用,流消費者重啟,從故障中恢復時重新處理過去的事件(請參閱 “[重播舊訊息](#重播舊訊息)”),或者在修復程式碼 BUG 之後。
而且,訊息延遲還可能導致無法預測訊息順序。例如,假設使用者首先發出一個 Web 請求(由 Web 伺服器 A 處理),然後發出第二個請求(由伺服器 B 處理。A 和 B 發出描述它們所處理請求的事件,但是 B 的事件在 A 的事件發生之前到達訊息代理。現在,流處理器將首先看到 B 事件,然後看到 A 事件,即使它們實際上是以相反的順序發生的。
@@ -504,9 +507,9 @@ CEP 的實現包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
[^ii]: 感謝 Flink 社群的 Kostas Kloudas 提出這個比喻。
將事件時間和處理時間搞混會導致錯誤的資料。例如,假設你有一個流處理器用於測量請求速率(計算每秒請求數)。如果你重新部署流處理器,它可能會停止一分鐘,並在恢復之後處理積壓的事件。如果你按處理時間來衡量速率,那麼在處理積壓日誌時,請求速率看上去就像有一個異常的突發尖峰,而實際上請求速率是穩定的([圖 11-7](../img/fig11-7.png))。
將事件時間和處理時間搞混會導致錯誤的資料。例如,假設你有一個流處理器用於測量請求速率(計算每秒請求數)。如果你重新部署流處理器,它可能會停止一分鐘,並在恢復之後處理積壓的事件。如果你按處理時間來衡量速率,那麼在處理積壓日誌時,請求速率看上去就像有一個異常的突發尖峰,而實際上請求速率是穩定的([圖 11-7](/img/fig11-7.png))。
![](../img/fig11-7.png)
![](/img/fig11-7.png)
**圖 11-7 按處理時間分窗,會因為處理速率的變動引入人為因素**
@@ -527,7 +530,7 @@ CEP 的實現包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
當事件可能在系統內多個地方進行緩衝時,為事件分配時間戳更加困難了。例如,考慮一個移動應用向伺服器上報關於用量的事件。該應用可能會在裝置處於離線狀態時被使用,在這種情況下,它將在裝置本地緩衝事件,並在下一次網際網路連線可用時向伺服器上報這些事件(可能是幾小時甚至幾天)。對於這個流的任意消費者而言,它們就如延遲極大的滯留事件一樣。
在這種情況下,事件上的事件戳實際上應當是使用者交互發生的時間,取決於移動裝置的本地時鐘。然而使用者控制的裝置上的時鐘通常是不可信的,因為它可能會被無意或故意設定成錯誤的時間(請參閱 “[時鐘同步與準確性](ch8.md#時鐘同步與準確性)”)。伺服器收到事件的時間(取決於伺服器的時鐘)可能是更準確的,因為伺服器在你的控制之下,但在描述使用者互動方面意義不大。
在這種情況下,事件上的事件戳實際上應當是使用者交互發生的時間,取決於移動裝置的本地時鐘。然而使用者控制的裝置上的時鐘通常是不可信的,因為它可能會被無意或故意設定成錯誤的時間(請參閱 “[時鐘同步與準確性](/tw/ch8#時鐘同步與準確性)”)。伺服器收到事件的時間(取決於伺服器的時鐘)可能是更準確的,因為伺服器在你的控制之下,但在描述使用者互動方面意義不大。
要校正不正確的裝置時鐘一種方法是記錄三個時間戳【82】
@@ -557,11 +560,11 @@ CEP 的實現包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
* 會話視窗Session window
與其他視窗型別不同,會話視窗沒有固定的持續時間,而定義為:將同一使用者出現時間相近的所有事件分組在一起,而當用戶一段時間沒有活動時(例如,如果 30 分鐘內沒有事件)視窗結束。會話切分是網站分析的常見需求(請參閱 “[分組](ch10.md#分組)”)。
與其他視窗型別不同,會話視窗沒有固定的持續時間,而定義為:將同一使用者出現時間相近的所有事件分組在一起,而當用戶一段時間沒有活動時(例如,如果 30 分鐘內沒有事件)視窗結束。會話切分是網站分析的常見需求(請參閱 “[分組](/tw/ch10#分組)”)。
### 流連線
在 [第十章](ch10.md) 中,我們討論了批處理作業如何透過鍵來連線資料集,以及這種連線是如何成為資料管道的重要組成部分的。由於流處理將資料管道泛化為對無限資料集進行增量處理,因此對流進行連線的需求也是完全相同的。
在 [第十章](/tw/ch10) 中,我們討論了批處理作業如何透過鍵來連線資料集,以及這種連線是如何成為資料管道的重要組成部分的。由於流處理將資料管道泛化為對無限資料集進行增量處理,因此對流進行連線的需求也是完全相同的。
然而,新事件隨時可能出現在一個流中,這使得流連線要比批處理連線更具挑戰性。為了更好地理解情況,讓我們先來區分三種不同型別的連線:**流 - 流** 連線,**流 - 表** 連線,與 **表 - 表** 連線【84】。我們將在下面的章節中透過例子來說明。
@@ -577,11 +580,11 @@ CEP 的實現包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
#### 流表連線(流擴充)
在 “[示例:使用者活動事件分析](ch10.md#示例:使用者活動事件分析)”([圖 10-2](../img/fig10-2.png))中,我們看到了連線兩個資料集的批處理作業示例:一組使用者活動事件和一個使用者檔案資料庫。將使用者活動事件視為流,並在流處理器中連續執行相同的連線是很自然的想法:輸入是包含使用者 ID 的活動事件流,而輸出還是活動事件流,但其中使用者 ID 已經被擴充套件為使用者的檔案資訊。這個過程有時被稱為使用資料庫的資訊來 **擴充enriching** 活動事件。
在 “[示例:使用者活動事件分析](/tw/ch10#示例:使用者活動事件分析)”([圖 10-2](/img/fig10-2.png))中,我們看到了連線兩個資料集的批處理作業示例:一組使用者活動事件和一個使用者檔案資料庫。將使用者活動事件視為流,並在流處理器中連續執行相同的連線是很自然的想法:輸入是包含使用者 ID 的活動事件流,而輸出還是活動事件流,但其中使用者 ID 已經被擴充套件為使用者的檔案資訊。這個過程有時被稱為使用資料庫的資訊來 **擴充enriching** 活動事件。
要執行此連線,流處理器需要一次處理一個活動事件,在資料庫中查詢事件的使用者 ID並將檔案資訊新增到活動事件中。資料庫查詢可以透過查詢遠端資料庫來實現。但正如在 “[示例:使用者活動事件分析](ch10.md#示例:使用者活動事件分析)” 一節中討論的此類遠端查詢可能會很慢並且有可能導致資料庫過載【75】。
要執行此連線,流處理器需要一次處理一個活動事件,在資料庫中查詢事件的使用者 ID並將檔案資訊新增到活動事件中。資料庫查詢可以透過查詢遠端資料庫來實現。但正如在 “[示例:使用者活動事件分析](/tw/ch10#示例:使用者活動事件分析)” 一節中討論的此類遠端查詢可能會很慢並且有可能導致資料庫過載【75】。
另一種方法是將資料庫副本載入到流處理器中,以便在本地進行查詢而無需網路往返。這種技術與我們在 “[Map 側連線](ch10.md#Map側連線)” 中討論的雜湊連線非常相似:如果資料庫的本地副本足夠小,則可以是記憶體中的散列表,比較大的話也可以是本地磁碟上的索引。
另一種方法是將資料庫副本載入到流處理器中,以便在本地進行查詢而無需網路往返。這種技術與我們在 “[Map 側連線](/tw/ch10#Map側連線)” 中討論的雜湊連線非常相似:如果資料庫的本地副本足夠小,則可以是記憶體中的散列表,比較大的話也可以是本地磁碟上的索引。
與批處理作業的區別在於,批處理作業使用資料庫的時間點快照作為輸入,而流處理器是長時間執行的,且資料庫的內容可能隨時間而改變,所以流處理器資料庫的本地副本需要保持更新。這個問題可以透過變更資料捕獲來解決:流處理器可以訂閱使用者檔案資料庫的更新日誌,如同活動事件流一樣。當增添或修改檔案時,流處理器會更新其本地副本。因此,我們有了兩個流之間的連線:活動事件和檔案更新。
@@ -589,7 +592,7 @@ CEP 的實現包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
#### 表表連線(維護物化檢視)
我們在 “[描述負載](ch1.md#描述負載)” 中討論的推特時間線例子時說過,當用戶想要檢視他們的主頁時間線時,迭代使用者所關注人群的推文併合並它們是一個開銷巨大的操作。
我們在 “[描述負載](/tw/ch1#描述負載)” 中討論的推特時間線例子時說過,當用戶想要檢視他們的主頁時間線時,迭代使用者所關注人群的推文併合並它們是一個開銷巨大的操作。
相反,我們需要一個時間線快取:一種每個使用者的 “收件箱”,在傳送推文的時候寫入這些資訊,因而讀取時間線時只需要簡單地查詢即可。物化與維護這個快取需要處理以下事件:
@@ -612,7 +615,7 @@ GROUP BY follows.follower_id
流連線直接對應於這個查詢中的表連線。時間線實際上是這個查詢結果的快取,每當底層的表發生變化時都會更新 [^iii]。
[^iii]: 如果你將流視作表的衍生物,如 [圖 11-6](../img/fig11-6.png) 所示而把一個連線看作是兩個表的乘法u·v那麼會發生一些有趣的事情物化連線的變化流遵循乘積法則(u·v)'= u'v + uv'。換句話說任何推文的變化量都與當前的關注聯絡在一起任何關注的變化量都與當前的推文相連線【49,50】。
[^iii]: 如果你將流視作表的衍生物,如 [圖 11-6](/img/fig11-6.png) 所示而把一個連線看作是兩個表的乘法u·v那麼會發生一些有趣的事情物化連線的變化流遵循乘積法則(u·v)'= u'v + uv'。換句話說任何推文的變化量都與當前的關注聯絡在一起任何關注的變化量都與當前的推文相連線【49,50】。
#### 連線的時間依賴性
@@ -630,7 +633,7 @@ GROUP BY follows.follower_id
### 容錯
在本章的最後一節中,讓我們看一看流處理是如何容錯的。我們在 [第十章](ch10.md) 中看到,批處理框架可以很容易地容錯:如果 MapReduce 作業中的任務失敗,可以簡單地在另一臺機器上再次啟動,並且丟棄失敗任務的輸出。這種透明的重試是可能的,因為輸入檔案是不可變的,每個任務都將其輸出寫入到 HDFS 上的獨立檔案中,而輸出僅當任務成功完成後可見。
在本章的最後一節中,讓我們看一看流處理是如何容錯的。我們在 [第十章](/tw/ch10) 中看到,批處理框架可以很容易地容錯:如果 MapReduce 作業中的任務失敗,可以簡單地在另一臺機器上再次啟動,並且丟棄失敗任務的輸出。這種透明的重試是可能的,因為輸入檔案是不可變的,每個任務都將其輸出寫入到 HDFS 上的獨立檔案中,而輸出僅當任務成功完成後可見。
特別是,批處理容錯方法可確保批處理作業的輸出與沒有出錯的情況相同,即使實際上某些任務失敗了。看起來好像每條輸入記錄都被處理了恰好一次 —— 沒有記錄被跳過,而且沒有記錄被處理兩次。儘管重啟任務意味著實際上可能會多次處理記錄,但輸出中的可見效果看上去就像只處理過一次。這個原則被稱為 **恰好一次語義exactly-once semantics**,儘管 **等效一次effectively-once** 可能會是一個更寫實的術語【90】。
@@ -650,9 +653,9 @@ Apache Flink 則使用不同的方法,它會定期生成狀態的滾動存檔
為了在出現故障時表現出恰好處理一次的樣子,我們需要確保事件處理的所有輸出和副作用 **當且僅當** 處理成功時才會生效。這些影響包括傳送給下游運算元或外部訊息傳遞系統(包括電子郵件或推送通知)的任何訊息,任何資料庫寫入,對運算元狀態的任何變更,以及對輸入訊息的任何確認(包括在基於日誌的訊息代理中將消費者偏移量前移)。
這些事情要麼都原子地發生,要麼都不發生,但是它們不應當失去同步。如果這種方法聽起來很熟悉,那是因為我們在分散式事務和兩階段提交的上下文中討論過它(請參閱 “[恰好一次的訊息處理](ch9.md#恰好一次的訊息處理)”)。
這些事情要麼都原子地發生,要麼都不發生,但是它們不應當失去同步。如果這種方法聽起來很熟悉,那是因為我們在分散式事務和兩階段提交的上下文中討論過它(請參閱 “[恰好一次的訊息處理](/tw/ch9#恰好一次的訊息處理)”)。
在 [第九章](ch9.md) 中,我們討論了分散式事務傳統實現中的問題(如 XA。然而在限制更為嚴苛的環境中也是有可能高效實現這種原子提交機制的。Google Cloud Dataflow【81,92】和 VoltDB 【94】中使用了這種方法Apache Kafka 有計劃加入類似的功能【95,96】。與 XA 不同,這些實現不會嘗試跨異構技術提供事務,而是透過在流處理框架中同時管理狀態變更與訊息傳遞來內化事務。事務協議的開銷可以透過在單個事務中處理多個輸入訊息來分攤。
在 [第九章](/tw/ch9) 中,我們討論了分散式事務傳統實現中的問題(如 XA。然而在限制更為嚴苛的環境中也是有可能高效實現這種原子提交機制的。Google Cloud Dataflow【81,92】和 VoltDB 【94】中使用了這種方法Apache Kafka 有計劃加入類似的功能【95,96】。與 XA 不同,這些實現不會嘗試跨異構技術提供事務,而是透過在流處理框架中同時管理狀態變更與訊息傳遞來內化事務。事務協議的開銷可以透過在單個事務中處理多個輸入訊息來分攤。
#### 冪等性
@@ -664,7 +667,7 @@ Apache Flink 則使用不同的方法,它會定期生成狀態的滾動存檔
Storm 的 Trident 基於類似的想法來處理狀態【78】。依賴冪等性意味著隱含了一些假設重啟一個失敗的任務必須以相同的順序重播相同的訊息基於日誌的訊息代理能做這些事處理必須是確定性的沒有其他節點能同時更新相同的值【98,99】。
當從一個處理節點故障切換到另一個節點時,可能需要進行 **防護**fencing請參閱 “[領導者和鎖](ch8.md#領導者和鎖)”),以防止被假死節點干擾。儘管有這麼多注意事項,冪等操作是一種實現 **恰好一次語義** 的有效方式,僅需很小的額外開銷。
當從一個處理節點故障切換到另一個節點時,可能需要進行 **防護**fencing請參閱 “[領導者和鎖](/tw/ch8#領導者和鎖)”),以防止被假死節點干擾。儘管有這麼多注意事項,冪等操作是一種實現 **恰好一次語義** 的有效方式,僅需很小的額外開銷。
#### 失敗後重建狀態
@@ -672,7 +675,7 @@ Storm 的 Trident 基於類似的想法來處理狀態【78】。依賴冪等性
一種選擇是將狀態儲存在遠端資料儲存中,並進行復制,然而正如在 “[流表連線(流擴充)](#流表連線(流擴充))” 中所述,每個訊息都要查詢遠端資料庫可能會很慢。另一種方法是在流處理器本地儲存狀態,並定期複製。然後當流處理器從故障中恢復時,新任務可以讀取狀態副本,恢復處理而不丟失資料。
例如Flink 定期捕獲運算元狀態的快照,並將它們寫入 HDFS 等持久儲存中【92,93】。Samza 和 Kafka Streams 透過將狀態變更傳送到具有日誌壓縮功能的專用 Kafka 主題來複制狀態變更這與變更資料捕獲類似【84,100】。VoltDB 透過在多個節點上對每個輸入訊息進行冗餘處理來複制狀態(請參閱 “[真的序列執行](ch7.md#真的序列執行)”)。
例如Flink 定期捕獲運算元狀態的快照,並將它們寫入 HDFS 等持久儲存中【92,93】。Samza 和 Kafka Streams 透過將狀態變更傳送到具有日誌壓縮功能的專用 Kafka 主題來複制狀態變更這與變更資料捕獲類似【84,100】。VoltDB 透過在多個節點上對每個輸入訊息進行冗餘處理來複制狀態(請參閱 “[真的序列執行](/tw/ch7#真的序列執行)”)。
在某些情況下,甚至可能都不需要複製狀態,因為它可以從輸入流重建。例如,如果狀態是從相當短的視窗中聚合而成,則簡單地重播該視窗中的輸入事件可能是足夠快的。如果狀態是透過變更資料捕獲來維護的資料庫的本地副本,那麼也可以從日誌壓縮的變更流中重建資料庫(請參閱 “[日誌壓縮](#日誌壓縮)”)。
@@ -681,19 +684,19 @@ Storm 的 Trident 基於類似的想法來處理狀態【78】。依賴冪等性
## 本章小結
在本章中,我們討論了事件流,它們所服務的目的,以及如何處理它們。在某些方面,流處理非常類似於在 [第十章](ch10.md) 中討論的批處理,不過是在無限的(永無止境的)流而不是固定大小的輸入上持續進行。從這個角度來看,訊息代理和事件日誌可以視作檔案系統的流式等價物。
在本章中,我們討論了事件流,它們所服務的目的,以及如何處理它們。在某些方面,流處理非常類似於在 [第十章](/tw/ch10) 中討論的批處理,不過是在無限的(永無止境的)流而不是固定大小的輸入上持續進行。從這個角度來看,訊息代理和事件日誌可以視作檔案系統的流式等價物。
我們花了一些時間比較兩種訊息代理:
* AMQP/JMS 風格的訊息代理
代理將單條訊息分配給消費者,消費者在成功處理單條訊息後確認訊息。訊息被確認後從代理中刪除。這種方法適合作為一種非同步形式的 RPC另請參閱 “[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)”),例如在任務佇列中,訊息處理的確切順序並不重要,而且訊息在處理完之後,不需要回頭重新讀取舊訊息。
代理將單條訊息分配給消費者,消費者在成功處理單條訊息後確認訊息。訊息被確認後從代理中刪除。這種方法適合作為一種非同步形式的 RPC另請參閱 “[訊息傳遞中的資料流](/tw/ch4#訊息傳遞中的資料流)”),例如在任務佇列中,訊息處理的確切順序並不重要,而且訊息在處理完之後,不需要回頭重新讀取舊訊息。
* 基於日誌的訊息代理
代理將一個分割槽中的所有訊息分配給同一個消費者節點,並始終以相同的順序傳遞訊息。並行是透過分割槽實現的,消費者透過存檔最近處理訊息的偏移量來跟蹤工作進度。訊息代理將訊息保留在磁碟上,因此如有必要的話,可以回跳並重新讀取舊訊息。
基於日誌的方法與資料庫中的複製日誌(請參閱 [第五章](ch5.md))和日誌結構儲存引擎(請參閱 [第三章](ch3.md))有相似之處。我們看到,這種方法對於消費輸入流,併產生衍生狀態或衍生輸出資料流的系統而言特別適用。
基於日誌的方法與資料庫中的複製日誌(請參閱 [第五章](/tw/ch5))和日誌結構儲存引擎(請參閱 [第三章](/tw/ch3))有相似之處。我們看到,這種方法對於消費輸入流,併產生衍生狀態或衍生輸出資料流的系統而言特別適用。
就流的來源而言,我們討論了幾種可能性:使用者活動事件,定期讀數的感測器,和 Feed 資料(例如,金融中的市場資料)能夠自然地表示為流。我們發現將資料庫寫入視作流也是很有用的:我們可以捕獲變更日誌 —— 即對資料庫所做的所有變更的歷史記錄 —— 隱式地透過變更資料捕獲,或顯式地透過事件溯源。日誌壓縮允許流也能保有資料庫內容的完整副本。
@@ -821,10 +824,3 @@ Storm 的 Trident 基於類似的想法來處理狀態【78】。依賴冪等性
1. Jay Kreps: “[Re: Trying to Achieve Deterministic Behavior on Recovery/Rewind](http://mail-archives.apache.org/mod_mbox/samza-dev/201409.mbox/%3CCAOeJiJg%2Bc7Ei%3DgzCuOz30DD3G5Hm9yFY%3DUJ6SafdNUFbvRgorg%40mail.gmail.com%3E),” email to *samza-dev* mailing list, September 9, 2014.
1. E. N. (Mootaz) Elnozahy, Lorenzo Alvisi, Yi-Min Wang, and David B. Johnson: “[A Survey of Rollback-Recovery Protocols in Message-Passing Systems](http://www.cs.utexas.edu/~lorenzo/papers/SurveyFinal.pdf),” *ACM Computing Surveys*, volume 34, number 3, pages 375408, September 2002. [doi:10.1145/568522.568525](http://dx.doi.org/10.1145/568522.568525)
1. Adam Warski: “[Kafka Streams How Does It Fit the Stream Processing Landscape?](https://softwaremill.com/kafka-streams-how-does-it-fit-stream-landscape/),” *softwaremill.com*, June 1, 2016.
------
| 上一章 | 目錄 | 下一章 |
| ------------------------- | ------------------------------- | ---------------------------------- |
| [第十章:批處理](ch10.md) | [設計資料密集型應用](README.md) | [第十二章:資料系統的未來](ch12.md) |

View File

@@ -1,24 +1,26 @@
# 第十二章:資料系統的未來
---
title: "第十二章:資料系統的未來"
linkTitle: "12. 資料系統的未來"
weight: 312
breadcrumbs: false
---
![](../img/ch12.png)
![](/img/ch12.png)
> 如果船長的終極目標是保護船隻,他應該永遠待在港口。
>
> —— 聖托馬斯・阿奎那《神學大全》1265-1274
---------------
[TOC]
到目前為止,本書主要描述的是 **現狀**。在這最後一章中,我們將放眼 **未來**,討論應該是怎麼樣的:我將提出一些想法與方法,我相信它們能從根本上改進我們設計與構建應用的方式。
對未來的看法與推測當然具有很大的主觀性。所以在撰寫本章時,當提及我個人的觀點時會使用第一人稱。你完全可以不同意這些觀點並提出自己的看法,但我希望本章中的概念,至少能成為富有成效的討論出發點,並澄清一些經常被混淆的概念。
[第一章](ch1.md) 概述了本書的目標:探索如何建立 **可靠**、**可伸縮** 和 **可維護** 的應用與系統。這一主題貫穿了所有的章節:例如,我們討論了許多有助於提高可靠性的容錯演算法,有助於提高可伸縮性的分割槽,以及有助於提高可維護性的演化與抽象機制。在本章中,我們將把所有這些想法結合在一起,並在它們的基礎上展望未來。我們的目標是,發現如何設計出比現有應用更好的應用 —— 健壯、正確、可演化、且最終對人類有益。
[第一章](/tw/ch1) 概述了本書的目標:探索如何建立 **可靠**、**可伸縮** 和 **可維護** 的應用與系統。這一主題貫穿了所有的章節:例如,我們討論了許多有助於提高可靠性的容錯演算法,有助於提高可伸縮性的分割槽,以及有助於提高可維護性的演化與抽象機制。在本章中,我們將把所有這些想法結合在一起,並在它們的基礎上展望未來。我們的目標是,發現如何設計出比現有應用更好的應用 —— 健壯、正確、可演化、且最終對人類有益。
## 資料整合
本書中反覆出現的主題是,對於任何給定的問題都會有好幾種解決方案,所有這些解決方案都有不同的優缺點與利弊權衡。例如在 [第三章](ch3.md) 討論儲存引擎時我們看到了日誌結構儲存、B 樹以及列式儲存。在 [第五章](ch5.md) 討論複製時,我們看到了單領導者、多領導者和無領導者的方法。
本書中反覆出現的主題是,對於任何給定的問題都會有好幾種解決方案,所有這些解決方案都有不同的優缺點與利弊權衡。例如在 [第三章](/tw/ch3) 討論儲存引擎時我們看到了日誌結構儲存、B 樹以及列式儲存。在 [第五章](/tw/ch5) 討論複製時,我們看到了單領導者、多領導者和無領導者的方法。
如果你有一個類似於 “我想儲存一些資料並稍後再查詢” 的問題,那麼並沒有一種正確的解決方案。但對於不同的具體環境,總會有不同的合適方法。軟體實現通常必須選擇一種特定的方法。使單條程式碼路徑能做到穩定健壯且表現良好已經是一件非常困難的事情了 —— 嘗試在單個軟體中完成所有事情,幾乎可以保證,實現效果會很差。
@@ -32,7 +34,7 @@
例如,為了處理任意關鍵詞的搜尋查詢,將 OLTP 資料庫與全文搜尋索引整合在一起是很常見的需求。儘管一些資料庫(例如 PostgreSQL包含了全文索引功能對於簡單的應用完全夠了【1】但更複雜的搜尋能力就需要專業的資訊檢索工具了。相反的是搜尋索引通常不適合作為持久的記錄系統因此許多應用需要組合這兩種不同的工具以滿足所有需求。
我們在 “[保持系統同步](ch11.md#保持系統同步)” 中接觸過整合資料系統的問題。隨著資料不同表示形式的增加,整合問題變得越來越困難。除了資料庫和搜尋索引之外,也許你需要在分析系統(資料倉庫,或批處理和流處理系統)中維護資料副本;維護從原始資料中衍生的快取,或反規範化的資料版本;將資料灌入機器學習、分類、排名或推薦系統中;或者基於資料變更傳送通知。
我們在 “[保持系統同步](/tw/ch11#保持系統同步)” 中接觸過整合資料系統的問題。隨著資料不同表示形式的增加,整合問題變得越來越困難。除了資料庫和搜尋索引之外,也許你需要在分析系統(資料倉庫,或批處理和流處理系統)中維護資料副本;維護從原始資料中衍生的快取,或反規範化的資料版本;將資料灌入機器學習、分類、排名或推薦系統中;或者基於資料變更傳送通知。
令人驚訝的是我經常看到軟體工程師做出這樣的陳述“根據我的經驗99% 的人只需要 X” 或者 “...... 不需要 X”對於各種各樣的 X。我認為這種陳述更像是發言人自己的經驗而不是技術實際上的實用性。可能對資料執行的操作其範圍極其寬廣。某人認為雞肋而毫無意義的功能可能是別人的核心需求。當你拉高視角並考慮跨越整個組織範圍的資料流時資料整合的需求往往就會變得明顯起來。
@@ -40,23 +42,23 @@
當需要在多個儲存系統中維護相同資料的副本以滿足不同的訪問模式時,你要對輸入和輸出瞭如指掌:哪些資料先寫入,哪些資料表示衍生自哪些來源?如何以正確的格式,將所有資料匯入正確的地方?
例如,你可能會首先將資料寫入 **記錄系統** 資料庫,捕獲對該資料庫所做的變更(請參閱 “[變更資料捕獲](ch11.md#變更資料捕獲)”然後將變更以相同的順序應用於搜尋索引。如果變更資料捕獲CDC是更新索引的唯一方式則可以確定該索引完全派生自記錄系統因此與其保持一致除軟體錯誤外。寫入資料庫是向該系統提供新輸入的唯一方式。
例如,你可能會首先將資料寫入 **記錄系統** 資料庫,捕獲對該資料庫所做的變更(請參閱 “[變更資料捕獲](/tw/ch11#變更資料捕獲)”然後將變更以相同的順序應用於搜尋索引。如果變更資料捕獲CDC是更新索引的唯一方式則可以確定該索引完全派生自記錄系統因此與其保持一致除軟體錯誤外。寫入資料庫是向該系統提供新輸入的唯一方式。
允許應用程式直接寫入搜尋索引和資料庫引入瞭如 [圖 11-4](../img/fig11-4.png) 所示的問題,其中兩個客戶端同時傳送衝突的寫入,且兩個儲存系統按不同順序處理它們。在這種情況下,既不是資料庫說了算,也不是搜尋索引說了算,所以它們做出了相反的決定,進入彼此間永續性的不一致狀態。
允許應用程式直接寫入搜尋索引和資料庫引入瞭如 [圖 11-4](/img/fig11-4.png) 所示的問題,其中兩個客戶端同時傳送衝突的寫入,且兩個儲存系統按不同順序處理它們。在這種情況下,既不是資料庫說了算,也不是搜尋索引說了算,所以它們做出了相反的決定,進入彼此間永續性的不一致狀態。
如果你可以透過單個系統來提供所有使用者輸入,從而決定所有寫入的排序,則透過按相同順序處理寫入,可以更容易地衍生出其他資料表示。這是狀態機複製方法的一個應用,我們在 “[全序廣播](ch9.md#全序廣播)” 中看到。無論你使用變更資料捕獲還是事件溯源日誌,都不如簡單的基於全序的決策原則更重要。
如果你可以透過單個系統來提供所有使用者輸入,從而決定所有寫入的排序,則透過按相同順序處理寫入,可以更容易地衍生出其他資料表示。這是狀態機複製方法的一個應用,我們在 “[全序廣播](/tw/ch9#全序廣播)” 中看到。無論你使用變更資料捕獲還是事件溯源日誌,都不如簡單的基於全序的決策原則更重要。
基於事件日誌來更新衍生資料的系統,通常可以做到 **確定性****冪等性**(請參閱 “[冪等性](ch11.md#冪等性)”),使得從故障中恢復相當容易。
基於事件日誌來更新衍生資料的系統,通常可以做到 **確定性****冪等性**(請參閱 “[冪等性](/tw/ch11#冪等性)”),使得從故障中恢復相當容易。
#### 衍生資料與分散式事務
保持不同資料系統彼此一致的經典方法涉及分散式事務,如 “[原子提交與兩階段提交](ch9.md#原子提交與兩階段提交)” 中所述。與分散式事務相比,使用衍生資料系統的方法如何?
保持不同資料系統彼此一致的經典方法涉及分散式事務,如 “[原子提交與兩階段提交](/tw/ch9#原子提交與兩階段提交)” 中所述。與分散式事務相比,使用衍生資料系統的方法如何?
在抽象層面,它們透過不同的方式達到類似的目標。分散式事務透過 **鎖** 進行互斥來決定寫入的順序(請參閱 “[兩階段鎖定](ch7.md#兩階段鎖定)”),而 CDC 和事件溯源使用日誌進行排序。分散式事務使用原子提交來確保變更只生效一次,而基於日誌的系統通常基於 **確定性重試****冪等性**
在抽象層面,它們透過不同的方式達到類似的目標。分散式事務透過 **鎖** 進行互斥來決定寫入的順序(請參閱 “[兩階段鎖定](/tw/ch7#兩階段鎖定)”),而 CDC 和事件溯源使用日誌進行排序。分散式事務使用原子提交來確保變更只生效一次,而基於日誌的系統通常基於 **確定性重試****冪等性**
最大的不同之處在於事務系統通常提供 [線性一致性](ch9.md#線性一致性),這包含著有用的保證,例如 [讀己之寫](ch5.md#讀己之寫)。另一方面,衍生資料系統通常是非同步更新的,因此它們預設不會提供相同的時序保證。
最大的不同之處在於事務系統通常提供 [線性一致性](/tw/ch9#線性一致性),這包含著有用的保證,例如 [讀己之寫](/tw/ch5#讀己之寫)。另一方面,衍生資料系統通常是非同步更新的,因此它們預設不會提供相同的時序保證。
在願意為分散式事務付出代價的有限場景中,它們已被成功應用。但是,我認為 XA 的容錯能力和效能很差勁(請參閱 “[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”),這嚴重限制了它的實用性。我相信為分散式事務設計一種更好的協議是可行的。但使這樣一種協議被現有工具廣泛接受是很有挑戰的,且不是立竿見影的事。
在願意為分散式事務付出代價的有限場景中,它們已被成功應用。但是,我認為 XA 的容錯能力和效能很差勁(請參閱 “[實踐中的分散式事務](/tw/ch9#實踐中的分散式事務)”),這嚴重限制了它的實用性。我相信為分散式事務設計一種更好的協議是可行的。但使這樣一種協議被現有工具廣泛接受是很有挑戰的,且不是立竿見影的事。
在沒有廣泛支援的良好分散式事務協議的情況下,我認為基於日誌的衍生資料是整合不同資料系統的最有前途的方法。然而,諸如讀己之寫的保證是有用的,我認為告訴所有人 “最終一致性是不可避免的 —— 忍一忍並學會和它打交道” 是沒有什麼建設性的(至少在缺乏 **如何** 應對的良好指導時)。
@@ -66,26 +68,26 @@
對於足夠小的系統,構建一個完全有序的事件日誌是完全可行的(正如單主複製資料庫的流行所證明的那樣,它正好建立了這樣一種日誌)。但是,隨著系統向更大更複雜的工作負載伸縮,限制開始出現:
* 在大多數情況下,構建完全有序的日誌,需要所有事件彙集於決定順序的 **單個領導者節點**。如果事件吞吐量大於單臺計算機的處理能力,則需要將其分割槽到多臺計算機上(請參閱 “[分割槽日誌](ch11.md#分割槽日誌)”)。然後兩個不同分割槽中的事件順序關係就不明確了。
* 如果伺服器分佈在多個 **地理位置分散** 的資料中心上,例如為了容忍整個資料中心掉線,你通常在每個資料中心都有單獨的主庫,因為網路延遲會導致同步的跨資料中心協調效率低下(請參閱 “[多主複製](ch5.md#多主複製)”)。這意味著源自兩個不同資料中心的事件順序未定義。
* 將應用程式部署為微服務時(請參閱 “[服務中的資料流REST 與 RPC](ch4.md#服務中的資料流REST與RPC)”),常見的設計選擇是將每個服務及其持久狀態作為獨立單元進行部署,服務之間不共享持久狀態。當兩個事件來自不同的服務時,這些事件間的順序未定義。
* 某些應用程式在客戶端儲存狀態,該狀態在使用者輸入時立即更新(無需等待伺服器確認),甚至可以繼續離線工作(請參閱 “[需要離線操作的客戶端](ch5.md#需要離線操作的客戶端)”)。對於這樣的應用程式,客戶端和伺服器很可能以不同的順序看到事件。
* 在大多數情況下,構建完全有序的日誌,需要所有事件彙集於決定順序的 **單個領導者節點**。如果事件吞吐量大於單臺計算機的處理能力,則需要將其分割槽到多臺計算機上(請參閱 “[分割槽日誌](/tw/ch11#分割槽日誌)”)。然後兩個不同分割槽中的事件順序關係就不明確了。
* 如果伺服器分佈在多個 **地理位置分散** 的資料中心上,例如為了容忍整個資料中心掉線,你通常在每個資料中心都有單獨的主庫,因為網路延遲會導致同步的跨資料中心協調效率低下(請參閱 “[多主複製](/tw/ch5#多主複製)”)。這意味著源自兩個不同資料中心的事件順序未定義。
* 將應用程式部署為微服務時(請參閱 “[服務中的資料流REST 與 RPC](/tw/ch4#服務中的資料流REST與RPC)”),常見的設計選擇是將每個服務及其持久狀態作為獨立單元進行部署,服務之間不共享持久狀態。當兩個事件來自不同的服務時,這些事件間的順序未定義。
* 某些應用程式在客戶端儲存狀態,該狀態在使用者輸入時立即更新(無需等待伺服器確認),甚至可以繼續離線工作(請參閱 “[需要離線操作的客戶端](/tw/ch5#需要離線操作的客戶端)”)。對於這樣的應用程式,客戶端和伺服器很可能以不同的順序看到事件。
在形式上,決定事件的全域性順序稱為 **全序廣播**,相當於 **共識**(請參閱 “[共識演算法和全序廣播](ch9.md#共識演算法和全序廣播)”)。大多數共識演算法都是針對單個節點的吞吐量足以處理整個事件流的情況而設計的,並且這些演算法不提供多個節點共享事件排序工作的機制。設計可以伸縮至單個節點的吞吐量之上,且在地理位置分散的環境中仍然工作良好的的共識演算法仍然是一個開放的研究問題。
在形式上,決定事件的全域性順序稱為 **全序廣播**,相當於 **共識**(請參閱 “[共識演算法和全序廣播](/tw/ch9#共識演算法和全序廣播)”)。大多數共識演算法都是針對單個節點的吞吐量足以處理整個事件流的情況而設計的,並且這些演算法不提供多個節點共享事件排序工作的機制。設計可以伸縮至單個節點的吞吐量之上,且在地理位置分散的環境中仍然工作良好的的共識演算法仍然是一個開放的研究問題。
#### 排序事件以捕獲因果關係
在事件之間不存在因果關係的情況下,全序的缺乏並不是一個大問題,因為併發事件可以任意排序。其他一些情況很容易處理:例如,當同一物件有多個更新時,它們可以透過將特定物件 ID 的所有更新路由到相同的日誌分割槽來完全排序。然而,因果關係有時會以更微妙的方式出現(請參閱 “[順序與因果關係](ch9.md#順序與因果關係)”)。
在事件之間不存在因果關係的情況下,全序的缺乏並不是一個大問題,因為併發事件可以任意排序。其他一些情況很容易處理:例如,當同一物件有多個更新時,它們可以透過將特定物件 ID 的所有更新路由到相同的日誌分割槽來完全排序。然而,因果關係有時會以更微妙的方式出現(請參閱 “[順序與因果關係](/tw/ch9#順序與因果關係)”)。
例如,考慮一個社交網路服務,以及一對曾處於戀愛關係但剛分手的使用者。其中一個使用者將另一個使用者從好友中移除,然後向剩餘的好友傳送訊息,抱怨他們的前任。使用者的心思是他們的前任不應該看到這些粗魯的訊息,因為訊息是在好友狀態解除後傳送的。
但是如果好友關係狀態與訊息儲存在不同的地方,在這樣一個系統中,可能會出現 **解除好友** 事件與 **傳送訊息** 事件之間的因果依賴丟失的情況。如果因果依賴關係沒有被捕捉到,則傳送有關新訊息的通知的服務可能會在 **解除好友** 事件之前處理 **傳送訊息** 事件,從而錯誤地向前任傳送通知。
在本例中,通知實際上是訊息和好友列表之間的連線,使得它與我們先前討論的連線的時序問題有關(請參閱 “[連線的時間依賴性](ch11.md#連線的時間依賴性)”。不幸的是這個問題似乎並沒有一個簡單的答案【2,3】。起點包括
在本例中,通知實際上是訊息和好友列表之間的連線,使得它與我們先前討論的連線的時序問題有關(請參閱 “[連線的時間依賴性](/tw/ch11#連線的時間依賴性)”。不幸的是這個問題似乎並沒有一個簡單的答案【2,3】。起點包括
* 邏輯時間戳可以提供無需協調的全域性順序(請參閱 “[序列號順序](ch9.md#序列號順序)”),因此它們可能有助於全序廣播不可行的情況。但是,他們仍然要求收件人處理不按順序傳送的事件,並且需要傳遞其他元資料。
* 邏輯時間戳可以提供無需協調的全域性順序(請參閱 “[序列號順序](/tw/ch9#序列號順序)”),因此它們可能有助於全序廣播不可行的情況。但是,他們仍然要求收件人處理不按順序傳送的事件,並且需要傳遞其他元資料。
* 如果你可以記錄一個事件來記錄使用者在做出決定之前所看到的系統狀態並給該事件一個唯一的識別符號那麼後面的任何事件都可以引用該事件識別符號來記錄因果關係【4】。我們將在 “[讀也是事件](#讀也是事件)” 中回到這個想法。
* 衝突解決演算法(請參閱 “[自動衝突解決](ch5.md#自動衝突解決)”)有助於處理以意外順序傳遞的事件。它們對於維護狀態很有用,但如果行為有外部副作用(例如,給使用者傳送通知),就沒什麼幫助了。
* 衝突解決演算法(請參閱 “[自動衝突解決](/tw/ch5#自動衝突解決)”)有助於處理以意外順序傳遞的事件。它們對於維護狀態很有用,但如果行為有外部副作用(例如,給使用者傳送通知),就沒什麼幫助了。
也許,隨著時間的推移,應用開發模式將出現,使得能夠有效地捕獲因果依賴關係,並且保持正確的衍生狀態,而不會迫使所有事件經歷全序廣播的瓶頸)。
@@ -93,27 +95,27 @@
我會說資料整合的目標是,確保資料最終能在所有正確的地方表現出正確的形式。這樣做需要消費輸入、轉換、連線、過濾、聚合、訓練模型、評估、以及最終寫出適當的輸出。批處理和流處理是實現這一目標的工具。
批處理和流處理的輸出是衍生資料集,例如搜尋索引、物化檢視、向用戶顯示的建議、聚合指標等(請參閱 “[批處理工作流的輸出](ch10.md#批處理工作流的輸出)” 和 “[流處理的應用](ch11.md#流處理的應用)”)。
批處理和流處理的輸出是衍生資料集,例如搜尋索引、物化檢視、向用戶顯示的建議、聚合指標等(請參閱 “[批處理工作流的輸出](/tw/ch10#批處理工作流的輸出)” 和 “[流處理的應用](/tw/ch11#流處理的應用)”)。
正如我們在 [第十章](ch10.md) 和 [第十一章](ch11.md) 中看到的,批處理和流處理有許多共同的原則,主要的根本區別在於流處理器在無限資料集上執行,而批處理輸入是已知的有限大小。處理引擎的實現方式也有很多細節上的差異,但是這些區別已經開始模糊。
正如我們在 [第十章](/tw/ch10) 和 [第十一章](/tw/ch11) 中看到的,批處理和流處理有許多共同的原則,主要的根本區別在於流處理器在無限資料集上執行,而批處理輸入是已知的有限大小。處理引擎的實現方式也有很多細節上的差異,但是這些區別已經開始模糊。
Spark 在批處理引擎上執行流處理,將流分解為 **微批次microbatches**,而 Apache Flink 則在流處理引擎上執行批處理【5】。原則上一種型別的處理可以用另一種型別來模擬但是效能特徵會有所不同例如在跳躍或滑動視窗上微批次可能表現不佳【6】。
#### 維護衍生狀態
批處理有著很強的函式式風格(即使其程式碼不是用函式式語言編寫的):它鼓勵確定性的純函式,其輸出僅依賴於輸入,除了顯式輸出外沒有副作用,將輸入視作不可變的,且輸出是僅追加的。流處理與之類似,但它擴充套件了運算元以允許受管理的、容錯的狀態(請參閱 “[失敗後重建狀態”](ch11.md#失敗後重建狀態))。
批處理有著很強的函式式風格(即使其程式碼不是用函式式語言編寫的):它鼓勵確定性的純函式,其輸出僅依賴於輸入,除了顯式輸出外沒有副作用,將輸入視作不可變的,且輸出是僅追加的。流處理與之類似,但它擴充套件了運算元以允許受管理的、容錯的狀態(請參閱 “[失敗後重建狀態”](/tw/ch11#失敗後重建狀態))。
具有良好定義的輸入和輸出的確定性函式的原理不僅有利於容錯(請參閱 “[冪等性](ch11.md#冪等性)”也簡化了有關組織中資料流的推理【7】。無論衍生資料是搜尋索引、統計模型還是快取採用這種觀點思考都是很有幫助的將其視為從一個東西衍生出另一個的資料管道透過函式式應用程式碼推送一個系統的狀態變更並將其效果應用至衍生系統中。
具有良好定義的輸入和輸出的確定性函式的原理不僅有利於容錯(請參閱 “[冪等性](/tw/ch11#冪等性)”也簡化了有關組織中資料流的推理【7】。無論衍生資料是搜尋索引、統計模型還是快取採用這種觀點思考都是很有幫助的將其視為從一個東西衍生出另一個的資料管道透過函式式應用程式碼推送一個系統的狀態變更並將其效果應用至衍生系統中。
原則上,衍生資料系統可以同步地維護,就像關係資料庫在與索引表寫入操作相同的事務中同步更新次級索引一樣。然而,非同步是使基於事件日誌的系統穩健的原因:它允許系統的一部分故障被抑制在本地。而如果任何一個參與者失敗,分散式事務將中止,因此它們傾向於透過將故障傳播到系統的其餘部分來放大故障(請參閱 “[分散式事務的限制](ch9.md#分散式事務的限制)”)。
原則上,衍生資料系統可以同步地維護,就像關係資料庫在與索引表寫入操作相同的事務中同步更新次級索引一樣。然而,非同步是使基於事件日誌的系統穩健的原因:它允許系統的一部分故障被抑制在本地。而如果任何一個參與者失敗,分散式事務將中止,因此它們傾向於透過將故障傳播到系統的其餘部分來放大故障(請參閱 “[分散式事務的限制](/tw/ch9#分散式事務的限制)”)。
我們在 “[分割槽與次級索引](ch6.md#分割槽與次級索引)” 中看到次級索引經常跨越分割槽邊界。具有次級索引的分割槽系統需要將寫入傳送到多個分割槽如果索引按關鍵詞分割槽的話或將讀取傳送到所有分割槽如果索引是按文件分割槽的話。如果索引是非同步維護的這種跨分割槽通訊也是最可靠和最可伸縮的【8】另請參閱 “[多分割槽資料處理](#多分割槽資料處理)”)。
我們在 “[分割槽與次級索引](/tw/ch6#分割槽與次級索引)” 中看到次級索引經常跨越分割槽邊界。具有次級索引的分割槽系統需要將寫入傳送到多個分割槽如果索引按關鍵詞分割槽的話或將讀取傳送到所有分割槽如果索引是按文件分割槽的話。如果索引是非同步維護的這種跨分割槽通訊也是最可靠和最可伸縮的【8】另請參閱 “[多分割槽資料處理](#多分割槽資料處理)”)。
#### 應用演化後重新處理資料
在維護衍生資料時,批處理和流處理都是有用的。流處理允許將輸入中的變化以低延遲反映在衍生檢視中,而批處理允許重新處理大量累積的歷史資料以便將新檢視匯出到現有資料集上。
特別是,重新處理現有資料為維護系統、演化並支援新功能和需求變更提供了一個良好的機制(請參閱 [第四章](ch4.md))。沒有重新進行處理,模式演化將僅限於簡單的變化,例如向記錄中新增新的可選欄位或新增新型別的記錄。無論是在寫時模式還是在讀時模式中都是如此(請參閱 “[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)”)。另一方面,透過重新處理,可以將資料集重組為一個完全不同的模型,以便更好地滿足新的要求。
特別是,重新處理現有資料為維護系統、演化並支援新功能和需求變更提供了一個良好的機制(請參閱 [第四章](/tw/ch4))。沒有重新進行處理,模式演化將僅限於簡單的變化,例如向記錄中新增新的可選欄位或新增新型別的記錄。無論是在寫時模式還是在讀時模式中都是如此(請參閱 “[文件模型中的模式靈活性](/tw/ch2#文件模型中的模式靈活性)”)。另一方面,透過重新處理,可以將資料集重組為一個完全不同的模型,以便更好地滿足新的要求。
> ### 鐵路上的模式遷移
>
@@ -131,15 +133,15 @@ Spark 在批處理引擎上執行流處理,將流分解為 **微批次micro
如果批處理用於重新處理歷史資料而流處理用於處理最近的更新那麼如何將這兩者結合起來Lambda 架構【12】是這方面的一個建議引起了很多關注。
Lambda 架構的核心思想是透過將不可變事件附加到不斷增長的資料集來記錄傳入資料,這類似於事件溯源(請參閱 “[事件溯源](ch11.md#事件溯源)”。為了從這些事件中衍生出讀取最佳化的檢視Lambda 架構建議並行執行兩個不同的系統:批處理系統(如 Hadoop MapReduce和獨立的流處理系統如 Storm
Lambda 架構的核心思想是透過將不可變事件附加到不斷增長的資料集來記錄傳入資料,這類似於事件溯源(請參閱 “[事件溯源](/tw/ch11#事件溯源)”。為了從這些事件中衍生出讀取最佳化的檢視Lambda 架構建議並行執行兩個不同的系統:批處理系統(如 Hadoop MapReduce和獨立的流處理系統如 Storm
在 Lambda 方法中,流處理器消耗事件並快速生成對檢視的近似更新;批處理器稍後將使用同一組事件並生成衍生檢視的更正版本。這個設計背後的原因是批處理更簡單,因此不易出錯,而流處理器被認為是不太可靠和難以容錯的(請參閱 “[容錯](ch11.md#容錯)”)。而且,流處理可以使用快速近似演算法,而批處理使用較慢的精確演算法。
在 Lambda 方法中,流處理器消耗事件並快速生成對檢視的近似更新;批處理器稍後將使用同一組事件並生成衍生檢視的更正版本。這個設計背後的原因是批處理更簡單,因此不易出錯,而流處理器被認為是不太可靠和難以容錯的(請參閱 “[容錯](/tw/ch11#容錯)”)。而且,流處理可以使用快速近似演算法,而批處理使用較慢的精確演算法。
Lambda 架構是一種有影響力的想法,它將資料系統的設計變得更好,尤其是透過推廣這樣的原則:在不可變事件流上建立衍生檢視,並在需要時重新處理事件。但是我也認為它有一些實際問題:
* 在批處理和流處理框架中維護相同的邏輯是很顯著的額外工作。雖然像 Summingbird【13】這樣的庫提供了一種可以在批處理和流處理的上下文中執行的計算抽象。除錯、調整和維護兩個不同系統的操作複雜性依然存在【14】。
* 由於流管道和批處理管道產生獨立的輸出,因此需要合併它們以響應使用者請求。如果計算是基於滾動視窗的簡單聚合,則合併相當容易,但如果檢視基於更複雜的操作(例如連線和會話化)而匯出,或者輸出不是時間序列,則會變得非常困難。
* 儘管有能力重新處理整個歷史資料集是很好的,但在大型資料集上這樣做經常會開銷巨大。因此,批處理流水線通常需要設定為處理增量批處理(例如,在每小時結束時處理一小時的資料),而不是重新處理所有內容。這引發了 “[時間推理](ch11.md#時間推理)” 中討論的問題,例如處理滯留事件和處理跨批次邊界的視窗。增量化批處理計算會增加複雜性,使其更類似於流式傳輸層,這與保持批處理層儘可能簡單的目標背道而馳。
* 儘管有能力重新處理整個歷史資料集是很好的,但在大型資料集上這樣做經常會開銷巨大。因此,批處理流水線通常需要設定為處理增量批處理(例如,在每小時結束時處理一小時的資料),而不是重新處理所有內容。這引發了 “[時間推理](/tw/ch11#時間推理)” 中討論的問題,例如處理滯留事件和處理跨批次邊界的視窗。增量化批處理計算會增加複雜性,使其更類似於流式傳輸層,這與保持批處理層儘可能簡單的目標背道而馳。
#### 統一批處理和流處理
@@ -147,14 +149,14 @@ Lambda 架構是一種有影響力的想法,它將資料系統的設計變得
在一個系統中統一批處理和流處理需要以下功能,這些功能也正在越來越廣泛地被提供:
* 透過處理最近事件流的相同處理引擎來重播歷史事件的能力。例如,基於日誌的訊息代理可以重播訊息(請參閱 “[重播舊訊息](ch11.md#重播舊訊息)”),某些流處理器可以從 HDFS 等分散式檔案系統讀取輸入。
* 對於流處理器來說,恰好一次語義 —— 即確保輸出與未發生故障的輸出相同,即使事實上發生故障(請參閱 “[容錯](ch11.md#容錯)”)。與批處理一樣,這需要丟棄任何失敗任務的部分輸出。
* 按事件時間進行視窗化的工具,而不是按處理時間進行視窗化,因為處理歷史事件時,處理時間毫無意義(請參閱 “[時間推理](ch11.md#時間推理)”。例如Apache Beam 提供了用於表達這種計算的 API可以在 Apache Flink 或 Google Cloud Dataflow 使用。
* 透過處理最近事件流的相同處理引擎來重播歷史事件的能力。例如,基於日誌的訊息代理可以重播訊息(請參閱 “[重播舊訊息](/tw/ch11#重播舊訊息)”),某些流處理器可以從 HDFS 等分散式檔案系統讀取輸入。
* 對於流處理器來說,恰好一次語義 —— 即確保輸出與未發生故障的輸出相同,即使事實上發生故障(請參閱 “[容錯](/tw/ch11#容錯)”)。與批處理一樣,這需要丟棄任何失敗任務的部分輸出。
* 按事件時間進行視窗化的工具,而不是按處理時間進行視窗化,因為處理歷史事件時,處理時間毫無意義(請參閱 “[時間推理](/tw/ch11#時間推理)”。例如Apache Beam 提供了用於表達這種計算的 API可以在 Apache Flink 或 Google Cloud Dataflow 使用。
## 分拆資料庫
在最抽象的層面上資料庫Hadoop 和作業系統都發揮相同的功能它們儲存一些資料並允許你處理和查詢這些資料【16】。資料庫將資料儲存為特定資料模型的記錄表中的行、文件、圖中的頂點等而作業系統的檔案系統則將資料儲存在檔案中 —— 但其核心都是 “資訊管理” 系統【17】。正如我們在 [第十章](ch10.md) 中看到的Hadoop 生態系統有點像 Unix 的分散式版本。
在最抽象的層面上資料庫Hadoop 和作業系統都發揮相同的功能它們儲存一些資料並允許你處理和查詢這些資料【16】。資料庫將資料儲存為特定資料模型的記錄表中的行、文件、圖中的頂點等而作業系統的檔案系統則將資料儲存在檔案中 —— 但其核心都是 “資訊管理” 系統【17】。正如我們在 [第十章](/tw/ch10) 中看到的Hadoop 生態系統有點像 Unix 的分散式版本。
當然,有很多實際的差異。例如,許多檔案系統都不能很好地處理包含 1000 萬個小檔案的目錄,而包含 1000 萬個小記錄的資料庫完全是尋常而不起眼的。無論如何,作業系統和資料庫之間的相似之處和差異值得探討。
@@ -170,12 +172,12 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
在本書的過程中,我們討論了資料庫提供的各種功能及其工作原理,其中包括:
* 次級索引,使你可以根據欄位的值有效地搜尋記錄(請參閱 “[其他索引結構](ch3.md#其他索引結構)”)
* 物化檢視,這是一種預計算的查詢結果快取(請參閱 “[聚合:資料立方體和物化檢視](ch3.md#聚合:資料立方體和物化檢視)”)
* 複製日誌,保持其他節點上資料的副本最新(請參閱 “[複製日誌的實現](ch5.md#複製日誌的實現)”)
* 全文搜尋索引,允許在文字中進行關鍵字搜尋(請參閱 “[全文搜尋和模糊索引](ch3.md#全文搜尋和模糊索引)”也內置於某些關係資料庫【1】
* 次級索引,使你可以根據欄位的值有效地搜尋記錄(請參閱 “[其他索引結構](/tw/ch3#其他索引結構)”)
* 物化檢視,這是一種預計算的查詢結果快取(請參閱 “[聚合:資料立方體和物化檢視](/tw/ch3#聚合:資料立方體和物化檢視)”)
* 複製日誌,保持其他節點上資料的副本最新(請參閱 “[複製日誌的實現](/tw/ch5#複製日誌的實現)”)
* 全文搜尋索引,允許在文字中進行關鍵字搜尋(請參閱 “[全文搜尋和模糊索引](/tw/ch3#全文搜尋和模糊索引)”也內置於某些關係資料庫【1】
在 [第十章](ch10.md) 和 [第十一章](ch11.md) 中,出現了類似的主題。我們討論了如何構建全文搜尋索引(請參閱 “[批處理工作流的輸出](ch10.md#批處理工作流的輸出)”),瞭解瞭如何維護物化檢視(請參閱 “[維護物化檢視](ch11.md#維護物化檢視)”)以及如何將變更從資料庫複製到衍生資料系統(請參閱 “[變更資料捕獲](ch11.md#變更資料捕獲)”)。
在 [第十章](/tw/ch10) 和 [第十一章](/tw/ch11) 中,出現了類似的主題。我們討論了如何構建全文搜尋索引(請參閱 “[批處理工作流的輸出](/tw/ch10#批處理工作流的輸出)”),瞭解瞭如何維護物化檢視(請參閱 “[維護物化檢視](/tw/ch11#維護物化檢視)”)以及如何將變更從資料庫複製到衍生資料系統(請參閱 “[變更資料捕獲](/tw/ch11#變更資料捕獲)”)。
資料庫中內建的功能與人們用批處理和流處理器構建的衍生資料系統似乎有相似之處。
@@ -183,15 +185,15 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
想想當你執行 `CREATE INDEX` 在關係資料庫中建立一個新的索引時會發生什麼。資料庫必須掃描表的一致性快照,挑選出所有被索引的欄位值,對它們進行排序,然後寫出索引。然後它必須處理自一致快照以來所做的寫入操作(假設表在建立索引時未被鎖定,所以寫操作可能會繼續)。一旦完成,只要事務寫入表中,資料庫就必須繼續保持索引最新。
此過程非常類似於設定新的從庫副本(請參閱 “[設定新從庫](ch5.md#設定新從庫)”),也非常類似於流處理系統中的 **引導bootstrap** 變更資料捕獲(請參閱 “[初始快照](ch11.md#初始快照)”)。
此過程非常類似於設定新的從庫副本(請參閱 “[設定新從庫](/tw/ch5#設定新從庫)”),也非常類似於流處理系統中的 **引導bootstrap** 變更資料捕獲(請參閱 “[初始快照](/tw/ch11#初始快照)”)。
無論何時執行 `CREATE INDEX`,資料庫都會重新處理現有資料集(如 “[應用演化後重新處理資料](#應用演化後重新處理資料)” 中所述),並將該索引作為新檢視匯出到現有資料上。現有資料可能是狀態的快照,而不是所有發生變化的日誌,但兩者密切相關(請參閱 “[狀態、流和不變性](ch11.md#狀態、流和不變性)”)。
無論何時執行 `CREATE INDEX`,資料庫都會重新處理現有資料集(如 “[應用演化後重新處理資料](#應用演化後重新處理資料)” 中所述),並將該索引作為新檢視匯出到現有資料上。現有資料可能是狀態的快照,而不是所有發生變化的日誌,但兩者密切相關(請參閱 “[狀態、流和不變性](/tw/ch11#狀態、流和不變性)”)。
#### 一切的元資料庫
有鑑於此我認為整個組織的資料流開始像一個巨大的資料庫【7】。每當批處理、流或 ETL 過程將資料從一個地方傳輸到另一個地方並組裝時,它表現地就像資料庫子系統一樣,使索引或物化檢視保持最新。
從這種角度來看,批處理和流處理器就像精心實現的觸發器、儲存過程和物化檢視維護例程。它們維護的衍生資料系統就像不同的索引型別。例如,關係資料庫可能支援 B 樹索引、雜湊索引、空間索引(請參閱 “[多列索引](ch3.md#多列索引)”)以及其他型別的索引。在新興的衍生資料系統架構中,不是將這些設施作為單個整合資料庫產品的功能實現,而是由各種不同的軟體提供,執行在不同的機器上,由不同的團隊管理。
從這種角度來看,批處理和流處理器就像精心實現的觸發器、儲存過程和物化檢視維護例程。它們維護的衍生資料系統就像不同的索引型別。例如,關係資料庫可能支援 B 樹索引、雜湊索引、空間索引(請參閱 “[多列索引](/tw/ch3#多列索引)”)以及其他型別的索引。在新興的衍生資料系統架構中,不是將這些設施作為單個整合資料庫產品的功能實現,而是由各種不同的軟體提供,執行在不同的機器上,由不同的團隊管理。
這些發展在未來將會把我們帶到哪裡?如果我們從沒有適合所有訪問模式的單一資料模型或儲存格式的前提出發,我推測有兩種途徑可以將不同的儲存和處理工具組合成一個有凝聚力的系統:
@@ -213,20 +215,20 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
傳統的同步寫入方法需要跨異構儲存系統的分散式事務【18】我認為這是錯誤的解決方案請參閱 “[衍生資料與分散式事務](#衍生資料與分散式事務)”)。單個儲存或流處理系統內的事務是可行的,但是當資料跨越不同技術之間的邊界時,我認為具有冪等寫入的非同步事件日誌是一種更加健壯和實用的方法。
例如,分散式事務在某些流處理元件內部使用,以匹配 **恰好一次exactly-once** 語義(請參閱 “[原子提交再現](ch11.md#原子提交再現)”),這可以很好地工作。然而,當事務需要涉及由不同人群編寫的系統時(例如,當資料從流處理元件寫入分散式鍵值儲存或搜尋索引時),缺乏標準化的事務協議會使整合更難。有冪等消費者的有序事件日誌(請參閱 “[冪等性](ch11.md#冪等性)”是一種更簡單的抽象因此在異構系統中實現更加可行【7】。
例如,分散式事務在某些流處理元件內部使用,以匹配 **恰好一次exactly-once** 語義(請參閱 “[原子提交再現](/tw/ch11#原子提交再現)”),這可以很好地工作。然而,當事務需要涉及由不同人群編寫的系統時(例如,當資料從流處理元件寫入分散式鍵值儲存或搜尋索引時),缺乏標準化的事務協議會使整合更難。有冪等消費者的有序事件日誌(請參閱 “[冪等性](/tw/ch11#冪等性)”是一種更簡單的抽象因此在異構系統中實現更加可行【7】。
基於日誌的整合的一大優勢是各個元件之間的 **鬆散耦合loose coupling**,這體現在兩個方面:
1. 在系統級別,非同步事件流使整個系統在個別元件的中斷或效能下降時更加穩健。如果消費者執行緩慢或失敗,那麼事件日誌可以緩衝訊息(請參閱 “[磁碟空間使用](ch11.md#磁碟空間使用)”),以便生產者和任何其他消費者可以繼續不受影響地執行。有問題的消費者可以在問題修復後趕上,因此不會錯過任何資料,並且包含故障。相比之下,分散式事務的同步互動往往會將本地故障升級為大規模故障(請參閱 “[分散式事務的限制](ch9.md#分散式事務的限制)”)。
1. 在系統級別,非同步事件流使整個系統在個別元件的中斷或效能下降時更加穩健。如果消費者執行緩慢或失敗,那麼事件日誌可以緩衝訊息(請參閱 “[磁碟空間使用](/tw/ch11#磁碟空間使用)”),以便生產者和任何其他消費者可以繼續不受影響地執行。有問題的消費者可以在問題修復後趕上,因此不會錯過任何資料,並且包含故障。相比之下,分散式事務的同步互動往往會將本地故障升級為大規模故障(請參閱 “[分散式事務的限制](/tw/ch9#分散式事務的限制)”)。
2. 在人力方面,分拆資料系統允許不同的團隊獨立開發,改進和維護不同的軟體元件和服務。專業化使得每個團隊都可以專注於做好一件事,並與其他團隊的系統以明確的介面互動。事件日誌提供了一個足夠強大的介面,以捕獲相當強的一致性屬性(由於永續性和事件的順序),但也足夠普適於幾乎任何型別的資料。
#### 分拆系統vs整合系統
如果分拆確實成為未來的方式,它也不會取代目前形式的資料庫 —— 它們仍然會像以往一樣被需要。為了維護流處理元件中的狀態,資料庫仍然是需要的,並且為批處理和流處理器的輸出提供查詢服務(請參閱 “[批處理工作流的輸出](ch10.md#批處理工作流的輸出)” 與 “[流處理](ch11.md#流處理)”。專用查詢引擎對於特定的工作負載仍然非常重要例如MPP 資料倉庫中的查詢引擎針對探索性分析查詢進行了最佳化,並且能夠很好地處理這種型別的工作負載(請參閱 “[Hadoop 與分散式資料庫的對比](ch10.md#Hadoop與分散式資料庫的對比)”)。
如果分拆確實成為未來的方式,它也不會取代目前形式的資料庫 —— 它們仍然會像以往一樣被需要。為了維護流處理元件中的狀態,資料庫仍然是需要的,並且為批處理和流處理器的輸出提供查詢服務(請參閱 “[批處理工作流的輸出](/tw/ch10#批處理工作流的輸出)” 與 “[流處理](/tw/ch11#流處理)”。專用查詢引擎對於特定的工作負載仍然非常重要例如MPP 資料倉庫中的查詢引擎針對探索性分析查詢進行了最佳化,並且能夠很好地處理這種型別的工作負載(請參閱 “[Hadoop 與分散式資料庫的對比](/tw/ch10#Hadoop與分散式資料庫的對比)”)。
執行幾種不同基礎設施的複雜性可能是一個問題每種軟體都有一個學習曲線配置問題和操作怪癖因此部署儘可能少的移動部件是很有必要的。比起使用應用程式碼拼接多個工具而成的系統單一整合軟體產品也可以在其設計應對的工作負載型別上實現更好、更可預測的效能【23】。正如在前言中所說的那樣為了不需要的規模而構建系統是白費精力而且可能會將你鎖死在一個不靈活的設計中。實際上這是一種過早最佳化的形式。
分拆的目標不是要針對個別資料庫與特定工作負載的效能進行競爭;我們的目標是允許你結合多個不同的資料庫,以便在比單個軟體可能實現的更廣泛的工作負載範圍內實現更好的效能。這是關於廣度,而不是深度 —— 與我們在 “[Hadoop 與分散式資料庫的對比](ch10.md#Hadoop與分散式資料庫的對比)” 中討論的儲存和處理模型的多樣性一樣。
分拆的目標不是要針對個別資料庫與特定工作負載的效能進行競爭;我們的目標是允許你結合多個不同的資料庫,以便在比單個軟體可能實現的更廣泛的工作負載範圍內實現更好的效能。這是關於廣度,而不是深度 —— 與我們在 “[Hadoop 與分散式資料庫的對比](/tw/ch10#Hadoop與分散式資料庫的對比)” 中討論的儲存和處理模型的多樣性一樣。
因此,如果有一項技術可以滿足你的所有需求,那麼最好使用該產品,而不是試圖用更低層級的元件重新實現它。只有當沒有單一軟體滿足你的所有需求時,才會出現拆分和聯合的優勢。
@@ -236,7 +238,7 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
例如,如果我們可以簡單地宣告 `mysql | elasticsearch`,類似於 Unix 管道【22】成為 `CREATE INDEX` 的分拆等價物:它將讀取 MySQL 資料庫中的所有文件並將其索引到 Elasticsearch 叢集中。然後它會不斷捕獲對資料庫所做的所有變更,並自動將它們應用於搜尋索引,而無需編寫自定義應用程式碼。這種整合應當支援幾乎任何型別的儲存或索引系統。
同樣,能夠更容易地預先計算和更新快取將是一件好事。回想一下,物化檢視本質上是一個預先計算的快取,所以你可以透過為複雜查詢宣告指定物化檢視來建立快取,包括圖上的遞迴查詢(請參閱 “[圖資料模型](ch2.md#圖資料模型)”)和應用邏輯。在這方面有一些有趣的早期研究,如 **差分資料流differential dataflow**【24,25】我希望這些想法能夠在生產系統中找到自己的方法。
同樣,能夠更容易地預先計算和更新快取將是一件好事。回想一下,物化檢視本質上是一個預先計算的快取,所以你可以透過為複雜查詢宣告指定物化檢視來建立快取,包括圖上的遞迴查詢(請參閱 “[圖資料模型](/tw/ch2#圖資料模型)”)和應用邏輯。在這方面有一些有趣的早期研究,如 **差分資料流differential dataflow**【24,25】我希望這些想法能夠在生產系統中找到自己的方法。
### 圍繞資料流設計應用
@@ -254,14 +256,14 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
當一個數據集衍生自另一個數據集時,它會經歷某種轉換函式。例如:
* 次級索引是由一種直白的轉換函式生成的衍生資料集:對於基礎表中的每行或每個文件,它挑選被索引的列或欄位中的值,並按這些值排序(假設使用 B 樹或 SSTable 索引,按鍵排序,如 [第三章](ch3.md) 所述)。
* 次級索引是由一種直白的轉換函式生成的衍生資料集:對於基礎表中的每行或每個文件,它挑選被索引的列或欄位中的值,並按這些值排序(假設使用 B 樹或 SSTable 索引,按鍵排序,如 [第三章](/tw/ch3) 所述)。
* 全文搜尋索引是透過應用各種自然語言處理函式而建立的,諸如語言檢測、分詞、詞幹或詞彙化、拼寫糾正和同義詞識別,然後構建用於高效查詢的資料結構(例如倒排索引)。
* 在機器學習系統中,我們可以將模型視作從訓練資料透過應用各種特徵提取、統計分析函式衍生的資料,當模型應用於新的輸入資料時,模型的輸出是從輸入和模型(因此間接地從訓練資料)中衍生的。
* 快取通常包含將以使用者介面UI顯示的形式的資料聚合。因此填充快取需要知道 UI 中引用的欄位UI 中的變更可能需要更新快取填充方式的定義,並重建快取。
用於次級索引的衍生函式是如此常用的需求,以致於它作為核心功能被內建至許多資料庫中,你可以簡單地透過 `CREATE INDEX` 來呼叫它。對於全文索引常見語言的基本語言特徵可能內建到資料庫中但更複雜的特徵通常需要領域特定的調整。在機器學習中特徵工程是眾所周知的特定於應用的特徵通常需要包含很多關於使用者互動與應用部署的詳細知識【35】。
當建立衍生資料集的函式不是像建立次級索引那樣的標準搬磚函式時,需要自定義程式碼來處理特定於應用的東西。而這個自定義程式碼是讓許多資料庫掙扎的地方,雖然關係資料庫通常支援觸發器、儲存過程和使用者定義的函式,可以用它們來在資料庫中執行應用程式碼,但它們有點像資料庫設計裡的事後反思。(請參閱 “[傳遞事件流](ch11.md#傳遞事件流)”)。
當建立衍生資料集的函式不是像建立次級索引那樣的標準搬磚函式時,需要自定義程式碼來處理特定於應用的東西。而這個自定義程式碼是讓許多資料庫掙扎的地方,雖然關係資料庫通常支援觸發器、儲存過程和使用者定義的函式,可以用它們來在資料庫中執行應用程式碼,但它們有點像資料庫設計裡的事後反思。(請參閱 “[傳遞事件流](/tw/ch11#傳遞事件流)”)。
#### 應用程式碼和狀態的分離
@@ -279,19 +281,19 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
但是,在大多數程式語言中,你無法訂閱可變變數中的變更 —— 你只能定期讀取它。與電子表格不同,如果變數的值發生變化,變數的讀者不會收到通知(你可以在自己的程式碼中實現這樣的通知 —— 這被稱為 **觀察者模式** —— 但大多數語言沒有將這種模式作為內建功能)。
資料庫繼承了這種可變資料的被動方法:如果你想知道資料庫的內容是否發生了變化,通常你唯一的選擇就是輪詢(即定期重複你的查詢)。訂閱變更只是剛剛開始出現的功能(請參閱 “[變更流的 API 支援](ch11.md#變更流的API支援)”)。
資料庫繼承了這種可變資料的被動方法:如果你想知道資料庫的內容是否發生了變化,通常你唯一的選擇就是輪詢(即定期重複你的查詢)。訂閱變更只是剛剛開始出現的功能(請參閱 “[變更流的 API 支援](/tw/ch11#變更流的API支援)”)。
#### 資料流:應用程式碼與狀態變化的互動
從資料流的角度思考應用程式,意味著重新協調應用程式碼和狀態管理之間的關係。我們不再將資料庫視作被應用操縱的被動變數,取而代之的是更多地考慮狀態,狀態變更和處理它們的程式碼之間的相互作用與協同關係。應用程式碼透過在另一個地方觸發狀態變更來響應狀態變更。
我們在 “[資料庫與流](ch11.md#資料庫與流)” 中看到了這一思路,我們討論了將資料庫的變更日誌視為一種我們可以訂閱的事件流。諸如 Actor 的訊息傳遞系統(請參閱 “[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)”)也具有響應事件的概念。早在 20 世紀 80 年代,**元組空間tuple space** 模型就已經探索了表達分散式計算的方式觀察狀態變更並作出反應的過程【38,39】。
我們在 “[資料庫與流](/tw/ch11#資料庫與流)” 中看到了這一思路,我們討論了將資料庫的變更日誌視為一種我們可以訂閱的事件流。諸如 Actor 的訊息傳遞系統(請參閱 “[訊息傳遞中的資料流](/tw/ch4#訊息傳遞中的資料流)”)也具有響應事件的概念。早在 20 世紀 80 年代,**元組空間tuple space** 模型就已經探索了表達分散式計算的方式觀察狀態變更並作出反應的過程【38,39】。
如前所述,當觸發器由於資料變更而被觸發時,或次級索引更新以反映索引表中的變更時,資料庫內部也發生著類似的情況。分拆資料庫意味著將這個想法應用於在主資料庫之外,用於建立衍生資料集:快取、全文搜尋索引、機器學習或分析系統。我們可以為此使用流處理和訊息傳遞系統。
需要記住的重要一點是,維護衍生資料不同於執行非同步任務。傳統的訊息傳遞系統通常是為執行非同步任務設計的(請參閱 “[日誌與傳統的訊息傳遞相比](ch11.md#日誌與傳統的訊息傳遞相比)”):
需要記住的重要一點是,維護衍生資料不同於執行非同步任務。傳統的訊息傳遞系統通常是為執行非同步任務設計的(請參閱 “[日誌與傳統的訊息傳遞相比](/tw/ch11#日誌與傳統的訊息傳遞相比)”):
* 在維護衍生資料時,狀態變更的順序通常很重要(如果多個檢視是從事件日誌衍生的,則需要按照相同的順序處理事件,以便它們之間保持一致)。如 “[確認與重新傳遞](ch11.md#確認與重新傳遞)” 中所述,許多訊息代理在重傳未確認訊息時沒有此屬性,雙寫也被排除在外(請參閱 “[保持系統同步](ch11.md#保持系統同步)”)。
* 在維護衍生資料時,狀態變更的順序通常很重要(如果多個檢視是從事件日誌衍生的,則需要按照相同的順序處理事件,以便它們之間保持一致)。如 “[確認與重新傳遞](/tw/ch11#確認與重新傳遞)” 中所述,許多訊息代理在重傳未確認訊息時沒有此屬性,雙寫也被排除在外(請參閱 “[保持系統同步](/tw/ch11#保持系統同步)”)。
* 容錯是衍生資料的關鍵:僅僅丟失單個訊息就會導致衍生資料集永遠與其資料來源失去同步。訊息傳遞和衍生狀態更新都必須可靠。例如,許多 Actor 系統預設在記憶體中維護 Actor 的狀態和訊息,所以如果執行 Actor 的機器崩潰,狀態和訊息就會丟失。
穩定的訊息排序和容錯訊息處理是相當嚴格的要求,但與分散式事務相比,它們開銷更小,執行更穩定。現代流處理元件可以提供這些排序和可靠性保證,並允許應用程式碼以流運算元的形式執行。
@@ -300,28 +302,28 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
#### 流處理器和服務
當今流行的應用開發風格涉及將功能分解為一組透過同步網路請求(如 REST API進行通訊的 **服務**service請參閱 “[服務中的資料流REST 與 RPC](ch4.md#服務中的資料流REST與RPC)”)。這種面向服務的架構優於單一龐大應用的優勢主要在於:通過鬆散耦合來提供組織上的可伸縮性:不同的團隊可以專職於不同的服務上,從而減少團隊之間的協調工作(因為服務可以獨立部署和更新)。
當今流行的應用開發風格涉及將功能分解為一組透過同步網路請求(如 REST API進行通訊的 **服務**service請參閱 “[服務中的資料流REST 與 RPC](/tw/ch4#服務中的資料流REST與RPC)”)。這種面向服務的架構優於單一龐大應用的優勢主要在於:通過鬆散耦合來提供組織上的可伸縮性:不同的團隊可以專職於不同的服務上,從而減少團隊之間的協調工作(因為服務可以獨立部署和更新)。
在資料流中組裝流運算元與微服務方法有很多相似之處【40】。但底層通訊機制是有很大區別資料流採用單向非同步訊息流而不是同步的請求 / 響應式互動。
除了在 “[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)” 中列出的優點如更好的容錯性資料流系統還能實現更好的效能。例如假設客戶正在購買以一種貨幣定價但以另一種貨幣支付的商品。為了執行貨幣換算你需要知道當前的匯率。這個操作可以透過兩種方式實現【40,41】
除了在 “[訊息傳遞中的資料流](/tw/ch4#訊息傳遞中的資料流)” 中列出的優點如更好的容錯性資料流系統還能實現更好的效能。例如假設客戶正在購買以一種貨幣定價但以另一種貨幣支付的商品。為了執行貨幣換算你需要知道當前的匯率。這個操作可以透過兩種方式實現【40,41】
1. 在微服務方法中,處理購買的程式碼可能會查詢匯率服務或資料庫,以獲取特定貨幣的當前匯率。
2. 在資料流方法中,處理訂單的程式碼會提前訂閱匯率變更流,並在匯率發生變動時將當前匯率儲存在本地資料庫中。處理訂單時只需查詢本地資料庫即可。
第二種方法能將對另一服務的同步網路請求替換為對本地資料庫的查詢(可能在同一臺機器甚至同一個程序中)[^ii]。資料流方法不僅更快,而且當其他服務失效時也更穩健。最快且最可靠的網路請求就是壓根沒有網路請求!我們現在不再使用 RPC而是在購買事件和匯率更新事件之間建立流聯接請參閱 “[流表連線(流擴充)](ch11.md#流表連線(流擴充))”)。
第二種方法能將對另一服務的同步網路請求替換為對本地資料庫的查詢(可能在同一臺機器甚至同一個程序中)[^ii]。資料流方法不僅更快,而且當其他服務失效時也更穩健。最快且最可靠的網路請求就是壓根沒有網路請求!我們現在不再使用 RPC而是在購買事件和匯率更新事件之間建立流聯接請參閱 “[流表連線(流擴充)](/tw/ch11#流表連線(流擴充))”)。
[^ii]: 在微服務方法中,你也可以透過在處理購買的服務中本地快取匯率來避免同步網路請求。但是為了保證快取的新鮮度,你需要定期輪詢匯率以獲取其更新,或訂閱變更流 —— 這恰好是資料流方法中發生的事情。
連線是時間相關的:如果購買事件在稍後的時間點被重新處理,匯率可能已經改變。如果要重建原始輸出,則需要獲取原始購買時的歷史匯率。無論是查詢服務還是訂閱匯率更新流,你都需要處理這種時間相關性(請參閱 “[連線的時間依賴性](ch11.md#連線的時間依賴性)”)。
連線是時間相關的:如果購買事件在稍後的時間點被重新處理,匯率可能已經改變。如果要重建原始輸出,則需要獲取原始購買時的歷史匯率。無論是查詢服務還是訂閱匯率更新流,你都需要處理這種時間相關性(請參閱 “[連線的時間依賴性](/tw/ch11#連線的時間依賴性)”)。
訂閱變更流,而不是在需要時查詢當前狀態,使我們更接近類似電子表格的計算模型:當某些資料發生變更時,依賴於此的所有衍生資料都可以快速更新。還有很多未解決的問題,例如關於時間相關連線等問題,但我認為圍繞資料流構建應用的想法是一個非常有希望的方向。
### 觀察衍生資料狀態
在抽象層面,上一節討論的資料流系統提供了建立衍生資料集(例如搜尋索引、物化檢視和預測模型)並使其保持更新的過程。我們將這個過程稱為 **寫路徑write path**:只要某些資訊被寫入系統,它可能會經歷批處理與流處理的多個階段,而最終每個衍生資料集都會被更新,以適配寫入的資料。[圖 12-1](../img/fig12-1.png) 顯示了一個更新搜尋索引的例子。
在抽象層面,上一節討論的資料流系統提供了建立衍生資料集(例如搜尋索引、物化檢視和預測模型)並使其保持更新的過程。我們將這個過程稱為 **寫路徑write path**:只要某些資訊被寫入系統,它可能會經歷批處理與流處理的多個階段,而最終每個衍生資料集都會被更新,以適配寫入的資料。[圖 12-1](/img/fig12-1.png) 顯示了一個更新搜尋索引的例子。
![](../img/fig12-1.png)
![](/img/fig12-1.png)
**圖 12-1 在搜尋索引中,寫(文件更新)遇上讀(查詢)**
@@ -329,7 +331,7 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
總而言之,寫路徑和讀路徑涵蓋了資料的整個旅程,從收集資料開始,到使用資料結束(可能是由另一個人)。寫路徑是預計算過程的一部分 —— 即,一旦資料進入,即刻完成,無論是否有人需要看它。讀路徑是這個過程中只有當有人請求時才會發生的部分。如果你熟悉函數語言程式設計語言,則可能會注意到寫路徑類似於立即求值,讀路徑類似於惰性求值。
如 [圖 12-1](../img/fig12-1.png) 所示,衍生資料集是寫路徑和讀路徑相遇的地方。它代表了在寫入時需要完成的工作量與在讀取時需要完成的工作量之間的權衡。
如 [圖 12-1](/img/fig12-1.png) 所示,衍生資料集是寫路徑和讀路徑相遇的地方。它代表了在寫入時需要完成的工作量與在讀取時需要完成的工作量之間的權衡。
#### 物化檢視和快取
@@ -345,7 +347,7 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
從這個例子中我們可以看到,索引不是寫路徑和讀路徑之間唯一可能的邊界;快取常見搜尋結果也是可行的;而在少量文件上使用沒有索引的類 grep 掃描也是可行的。由此來看,快取,索引和物化檢視的作用很簡單:它們改變了讀路徑與寫路徑之間的邊界。透過預先計算結果,從而允許我們在寫路徑上做更多的工作,以節省讀路徑上的工作量。
在寫路徑上完成的工作和讀路徑之間的界限,實際上是本書開始處在 “[描述負載](ch1.md#描述負載)” 中推特例子裡談到的主題。在該例中,我們還看到了與普通使用者相比,名人的寫路徑和讀路徑可能有所不同。在 500 頁之後,我們已經繞回了起點!
在寫路徑上完成的工作和讀路徑之間的界限,實際上是本書開始處在 “[描述負載](/tw/ch1#描述負載)” 中推特例子裡談到的主題。在該例中,我們還看到了與普通使用者相比,名人的寫路徑和讀路徑可能有所不同。在 500 頁之後,我們已經繞回了起點!
#### 有狀態、可離線的客戶端
@@ -355,7 +357,7 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
傳統上,網路瀏覽器是無狀態的客戶端,只有當連線到網際網路時才能做一些有用的事情(能離線執行的唯一事情基本上就是上下滾動之前線上時載入好的頁面)。然而,最近的 “單頁面” JavaScript Web 應用已經獲得了很多有狀態的功能,包括客戶端使用者介面互動,以及 Web 瀏覽器中的持久化本地儲存。移動應用可以類似地在裝置上儲存大量狀態,而且大多數使用者互動都不需要與伺服器往返互動。
這些不斷變化的功能重新引發了對 **離線優先offline-first** 應用的興趣這些應用盡可能地在同一裝置上使用本地資料庫無需連線網際網路並在後臺網路連線可用時與遠端伺服器同步【42】。由於移動裝置通常具有緩慢且不可靠的蜂窩網路連線因此如果使用者的使用者介面不必等待同步網路請求且應用主要是離線工作的則這是一個巨大優勢請參閱 “[需要離線操作的客戶端](ch5.md#需要離線操作的客戶端)”)。
這些不斷變化的功能重新引發了對 **離線優先offline-first** 應用的興趣這些應用盡可能地在同一裝置上使用本地資料庫無需連線網際網路並在後臺網路連線可用時與遠端伺服器同步【42】。由於移動裝置通常具有緩慢且不可靠的蜂窩網路連線因此如果使用者的使用者介面不必等待同步網路請求且應用主要是離線工作的則這是一個巨大優勢請參閱 “[需要離線操作的客戶端](/tw/ch5#需要離線操作的客戶端)”)。
當我們擺脫無狀態客戶端與中央資料庫互動的假設,並轉向在終端使用者裝置上維護狀態時,這就開啟了新世界的大門。特別是,我們可以將裝置上的狀態視為 **伺服器狀態的快取**。螢幕上的畫素是客戶端應用中模型物件的物化檢視模型物件是遠端資料中心的本地狀態副本【27】。
@@ -367,17 +369,17 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
用我們的寫路徑與讀路徑模型來講主動將狀態變更推至到客戶端裝置意味著將寫路徑一直延伸到終端使用者。當客戶端首次初始化時它仍然需要使用讀路徑來獲取其初始狀態但此後它就能夠依賴伺服器傳送的狀態變更流了。我們在流處理和訊息傳遞部分討論的想法並不侷限於資料中心中我們可以進一步採納這些想法並將它們一直延伸到終端使用者裝置【43】。
這些裝置有時會離線,並在此期間無法收到伺服器狀態變更的任何通知。但是我們已經解決了這個問題:在 “[消費者偏移量](ch11.md#消費者偏移量)” 中,我們討論了基於日誌的訊息代理的消費者能在失敗或斷開連線後重連,並確保它不會錯過掉線期間任何到達的訊息。同樣的技術適用於單個使用者,每個裝置都是一個小事件流的小小訂閱者。
這些裝置有時會離線,並在此期間無法收到伺服器狀態變更的任何通知。但是我們已經解決了這個問題:在 “[消費者偏移量](/tw/ch11#消費者偏移量)” 中,我們討論了基於日誌的訊息代理的消費者能在失敗或斷開連線後重連,並確保它不會錯過掉線期間任何到達的訊息。同樣的技術適用於單個使用者,每個裝置都是一個小事件流的小小訂閱者。
#### 端到端的事件流
最近用於開發有狀態的客戶端與使用者介面的工具,例如如 Elm 語言【30】和 Facebook 的 React、Flux 和 Redux 工具鏈,已經透過訂閱表示使用者輸入或伺服器響應的事件流來管理客戶端的內部狀態,其結構與事件溯源相似(請參閱 “[事件溯源](ch11.md#事件溯源)”)。
最近用於開發有狀態的客戶端與使用者介面的工具,例如如 Elm 語言【30】和 Facebook 的 React、Flux 和 Redux 工具鏈,已經透過訂閱表示使用者輸入或伺服器響應的事件流來管理客戶端的內部狀態,其結構與事件溯源相似(請參閱 “[事件溯源](/tw/ch11#事件溯源)”)。
將這種程式設計模型擴充套件為:允許伺服器將狀態變更事件推送到客戶端的事件管道中,是非常自然的。因此,狀態變化可以透過 **端到端end-to-end** 的寫路徑流動:從一個裝置上的互動觸發狀態變更開始,經由事件日誌,並穿過幾個衍生資料系統與流處理器,一直到另一臺裝置上的使用者介面,而有人正在觀察使用者介面上的狀態變化。這些狀態變化能以相當低的延遲傳播 —— 比如說,在一秒內從一端到另一端。
一些應用(如即時訊息傳遞與線上遊戲)已經具有這種 “即時” 架構(在低延遲互動的意義上,不是在 “[響應時間保證](ch8.md#響應時間保證)” 中的意義上)。但我們為什麼不用這種方式構建所有的應用?
一些應用(如即時訊息傳遞與線上遊戲)已經具有這種 “即時” 架構(在低延遲互動的意義上,不是在 “[響應時間保證](/tw/ch8#響應時間保證)” 中的意義上)。但我們為什麼不用這種方式構建所有的應用?
挑戰在於,關於無狀態客戶端和請求 / 響應互動的假設已經根深蒂固地植入在我們的資料庫、庫、框架以及協議之中。許多資料儲存支援讀取與寫入操作,為請求返回一個響應,但只有極少數提供訂閱變更的能力 —— 請求返回一個隨時間推移的響應流(請參閱 “[變更流的 API 支援](ch11.md#變更流的API支援)” )。
挑戰在於,關於無狀態客戶端和請求 / 響應互動的假設已經根深蒂固地植入在我們的資料庫、庫、框架以及協議之中。許多資料儲存支援讀取與寫入操作,為請求返回一個響應,但只有極少數提供訂閱變更的能力 —— 請求返回一個隨時間推移的響應流(請參閱 “[變更流的 API 支援](/tw/ch11#變更流的API支援)” )。
為了將寫路徑延伸至終端使用者,我們需要從根本上重新思考我們構建這些系統的方式:從請求 / 響應互動轉向釋出 / 訂閱資料流【27】。更具響應性的使用者介面與更好的離線支援我認為這些優勢值得我們付出努力。如果你正在設計資料系統我希望你對訂閱變更的選項留有印象而不只是查詢當前狀態。
@@ -385,11 +387,11 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
我們討論過,當流處理器將衍生資料寫入儲存(資料庫,快取或索引)時,以及當用戶請求查詢該儲存時,儲存將充當寫路徑和讀路徑之間的邊界。該儲存應當允許對資料進行隨機訪問的讀取查詢,否則這些查詢將需要掃描整個事件日誌。
在很多情況下,資料儲存與流處理系統是分開的。但回想一下,流處理器還是需要維護狀態以執行聚合和連線的(請參閱 “[流連線](ch11.md#流連線)”。這種狀態通常隱藏在流處理器內部但一些框架也允許這些狀態被外部客戶端查詢【45】將流處理器本身變成一種簡單的資料庫。
在很多情況下,資料儲存與流處理系統是分開的。但回想一下,流處理器還是需要維護狀態以執行聚合和連線的(請參閱 “[流連線](/tw/ch11#流連線)”。這種狀態通常隱藏在流處理器內部但一些框架也允許這些狀態被外部客戶端查詢【45】將流處理器本身變成一種簡單的資料庫。
我願意進一步思考這個想法。正如到目前為止所討論的那樣對儲存的寫入是透過事件日誌進行的而讀取是臨時的網路請求直接流向儲存著待查資料的節點。這是一個合理的設計但不是唯一可行的設計。也可以將讀取請求表示為事件流並同時將讀事件與寫事件送往流處理器流處理器透過將讀取結果傳送到輸出流來響應讀取事件【46】。
當寫入和讀取都被表示為事件,並且被路由到同一個流運算元以便處理時,我們實際上是在讀取查詢流和資料庫之間執行流表連線。讀取事件需要被送往儲存資料的資料庫分割槽(請參閱 “[請求路由](ch6.md#請求路由)”),就像批處理和流處理器在連線時需要在同一個鍵上對輸入分割槽一樣(請參閱 “[Reduce 側連線與分組](ch10.md#Reduce側連線與分組)”)。
當寫入和讀取都被表示為事件,並且被路由到同一個流運算元以便處理時,我們實際上是在讀取查詢流和資料庫之間執行流表連線。讀取事件需要被送往儲存資料的資料庫分割槽(請參閱 “[請求路由](/tw/ch6#請求路由)”),就像批處理和流處理器在連線時需要在同一個鍵上對輸入分割槽一樣(請參閱 “[Reduce 側連線與分組](/tw/ch10#Reduce側連線與分組)”)。
服務請求與執行連線之間的這種相似之處是非常關鍵的【47】。一次性讀取請求只是將請求傳過連線運算元然後請求馬上就被忘掉了而一個訂閱請求則是與連線另一側過去與未來事件的持久化連線。
@@ -401,20 +403,20 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
對於只涉及單個分割槽的查詢,透過流來發送查詢與收集響應可能是殺雞用牛刀了。然而,這個想法開啟了分散式執行複雜查詢的可能性,這需要合併來自多個分割槽的資料,利用了流處理器已經提供的訊息路由、分割槽和連線的基礎設施。
Storm 的分散式 RPC 功能支援這種使用模式(請參閱 “[訊息傳遞和 RPC](ch11.md#訊息傳遞和RPC)”)。例如,它已經被用來計算瀏覽過某個推特 URL 的人數 —— 即,發推包含該 URL 的所有人的粉絲集合的並集【48】。由於推特的使用者是分割槽的因此這種計算需要合併來自多個分割槽的結果。
Storm 的分散式 RPC 功能支援這種使用模式(請參閱 “[訊息傳遞和 RPC](/tw/ch11#訊息傳遞和RPC)”)。例如,它已經被用來計算瀏覽過某個推特 URL 的人數 —— 即,發推包含該 URL 的所有人的粉絲集合的並集【48】。由於推特的使用者是分割槽的因此這種計算需要合併來自多個分割槽的結果。
這種模式的另一個例子是欺詐預防:為了評估特定購買事件是否具有欺詐風險,你可以檢查該使用者 IP 地址電子郵件地址帳單地址送貨地址的信用分。這些信用資料庫中的每一個都是有分割槽的因此為特定購買事件採集分數需要連線一系列不同的分割槽資料集【49】。
MPP 資料庫的內部查詢執行圖有著類似的特徵(請參閱 “[Hadoop 與分散式資料庫的對比](ch10.md#Hadoop與分散式資料庫的對比)”)。如果需要執行這種多分割槽連線,則直接使用提供此功能的資料庫,可能要比使用流處理器實現它要更簡單。然而將查詢視為流提供了一種選項,可以用於實現超出傳統現成解決方案的大規模應用。
MPP 資料庫的內部查詢執行圖有著類似的特徵(請參閱 “[Hadoop 與分散式資料庫的對比](/tw/ch10#Hadoop與分散式資料庫的對比)”)。如果需要執行這種多分割槽連線,則直接使用提供此功能的資料庫,可能要比使用流處理器實現它要更簡單。然而將查詢視為流提供了一種選項,可以用於實現超出傳統現成解決方案的大規模應用。
## 將事情做正確
對於只讀取資料的無狀態服務出問題也沒什麼大不了的你可以修復該錯誤並重啟服務而一切都恢復正常。像資料庫這樣的有狀態系統就沒那麼簡單了它們被設計為永遠記住事物或多或少所以如果出現問題這種錯誤的效果也將潛在地永遠持續下去這意味著它們需要更仔細的思考【50】。
我們希望構建可靠且 **正確** 的應用(即使面對各種故障,程式的語義也能被很好地定義與理解)。約四十年來,原子性、隔離性和永續性([第七章](ch7.md))等事務特性一直是構建正確應用的首選工具。然而這些地基沒有看上去那麼牢固:例如弱隔離級別帶來的困惑可以佐證(請參閱 “[弱隔離級別](ch7.md#弱隔離級別)”)。
我們希望構建可靠且 **正確** 的應用(即使面對各種故障,程式的語義也能被很好地定義與理解)。約四十年來,原子性、隔離性和永續性([第七章](/tw/ch7))等事務特性一直是構建正確應用的首選工具。然而這些地基沒有看上去那麼牢固:例如弱隔離級別帶來的困惑可以佐證(請參閱 “[弱隔離級別](/tw/ch7#弱隔離級別)”)。
事務在某些領域被完全拋棄,並被提供更好效能與可伸縮性的模型取代,但後者有更複雜的語義(例如,請參閱 “[無主複製](ch5.md#無主複製)”)。**一致性Consistency** 經常被談起,但其定義並不明確(請參閱 “[一致性](ch7.md#一致性)” 和 [第九章](ch9.md))。有些人斷言我們應當為了高可用而 “擁抱弱一致性”,但卻對這些概念實際上意味著什麼缺乏清晰的認識。
事務在某些領域被完全拋棄,並被提供更好效能與可伸縮性的模型取代,但後者有更複雜的語義(例如,請參閱 “[無主複製](/tw/ch5#無主複製)”)。**一致性Consistency** 經常被談起,但其定義並不明確(請參閱 “[一致性](/tw/ch7#一致性)” 和 [第九章](/tw/ch9))。有些人斷言我們應當為了高可用而 “擁抱弱一致性”,但卻對這些概念實際上意味著什麼缺乏清晰的認識。
對於如此重要的話題我們的理解以及我們的工程方法卻是驚人地薄弱。例如確定在特定事務隔離等級或複製配置下執行特定應用是否安全是非常困難的【51,52】。通常簡單的解決方案似乎在低併發性的情況下工作正常並且沒有錯誤但在要求更高的情況下卻會出現許多微妙的錯誤。
@@ -428,23 +430,23 @@ MPP 資料庫的內部查詢執行圖有著類似的特徵(請參閱 “[Hadoo
僅僅因為一個應用程式使用了具有相對較強安全屬性的資料系統(例如可序列化的事務),並不意味著就可以保證沒有資料丟失或損壞。例如,如果某個應用有個 Bug導致它寫入不正確的資料或者從資料庫中刪除資料那麼可序列化的事務也救不了你。
這個例子可能看起來很無聊,但值得認真對待:應用會出 Bug而人也會犯錯誤。我在 “[狀態、流和不變性](ch11.md#狀態、流和不變性)” 中使用了這個例子來支援不可變和僅追加的資料,閹割掉錯誤程式碼摧毀良好資料的能力,能讓從錯誤中恢復更為容易。
這個例子可能看起來很無聊,但值得認真對待:應用會出 Bug而人也會犯錯誤。我在 “[狀態、流和不變性](/tw/ch11#狀態、流和不變性)” 中使用了這個例子來支援不可變和僅追加的資料,閹割掉錯誤程式碼摧毀良好資料的能力,能讓從錯誤中恢復更為容易。
雖然不變性很有用,但它本身並非萬靈藥。讓我們來看一個可能發生的、非常微妙的資料損壞案例。
#### 正好執行一次操作
在 “[容錯](ch11.md#容錯)” 中,我們見到了 **恰好一次**(或 **等效一次**)語義的概念。如果在處理訊息時出現問題,你可以選擇放棄(丟棄訊息 —— 導致資料丟失)或重試。如果重試,就會有這種風險:第一次實際上成功了,只不過你沒有發現。結果這個訊息就被處理了兩次。
在 “[容錯](/tw/ch11#容錯)” 中,我們見到了 **恰好一次**(或 **等效一次**)語義的概念。如果在處理訊息時出現問題,你可以選擇放棄(丟棄訊息 —— 導致資料丟失)或重試。如果重試,就會有這種風險:第一次實際上成功了,只不過你沒有發現。結果這個訊息就被處理了兩次。
處理兩次是資料損壞的一種形式:為同樣的服務向客戶收費兩次(收費太多)或增長計數器兩次(誇大指標)都不是我們想要的。在這種情況下,恰好一次意味著安排計算,使得最終效果與沒有發生錯誤的情況一樣,即使操作實際上因為某種錯誤而重試。我們先前討論過實現這一目標的幾種方法。
最有效的方法之一是使操作 **冪等**idempotent請參閱 “[冪等性](ch11.md#冪等性)”):即確保它無論是執行一次還是執行多次都具有相同的效果。但是,將不是天生冪等的操作變為冪等的操作需要一些額外的努力與關注:你可能需要維護一些額外的元資料(例如更新了值的操作 ID 集合),並在從一個節點故障切換至另一個節點時做好防護(請參閱 “[領導者和鎖](ch8.md#領導者和鎖)”)。
最有效的方法之一是使操作 **冪等**idempotent請參閱 “[冪等性](/tw/ch11#冪等性)”):即確保它無論是執行一次還是執行多次都具有相同的效果。但是,將不是天生冪等的操作變為冪等的操作需要一些額外的努力與關注:你可能需要維護一些額外的元資料(例如更新了值的操作 ID 集合),並在從一個節點故障切換至另一個節點時做好防護(請參閱 “[領導者和鎖](/tw/ch8#領導者和鎖)”)。
#### 抑制重複
除了流處理之外其他許多地方也需要抑制重複的模式。例如TCP 使用了資料包上的序列號以便接收方可以將它們正確排序並確定網路上是否有資料包丟失或重複。在將資料交付應用前TCP 協議棧會重新傳輸任何丟失的資料包,也會移除任何重複的資料包。
但是,這種重複抑制僅適用於單條 TCP 連線的場景中。假設 TCP 連線是一個客戶端與資料庫的連線,並且它正在執行 [例 12-1]() 中的事務。在許多資料庫中,事務是繫結在客戶端連線上的(如果客戶端傳送了多個查詢,資料庫就知道它們屬於同一個事務,因為它們是在同一個 TCP 連線上傳送的)。如果客戶端在傳送 `COMMIT` 之後並在從資料庫伺服器收到響應之前遇到網路中斷與連線超時,客戶端是不知道事務是否已經被提交的([圖 8-1](../img/fig8-1.png))。
但是,這種重複抑制僅適用於單條 TCP 連線的場景中。假設 TCP 連線是一個客戶端與資料庫的連線,並且它正在執行 [例 12-1]() 中的事務。在許多資料庫中,事務是繫結在客戶端連線上的(如果客戶端傳送了多個查詢,資料庫就知道它們屬於同一個事務,因為它們是在同一個 TCP 連線上傳送的)。如果客戶端在傳送 `COMMIT` 之後並在從資料庫伺服器收到響應之前遇到網路中斷與連線超時,客戶端是不知道事務是否已經被提交的([圖 8-1](/img/fig8-1.png))。
**例 12-1 資金從一個賬戶到另一個賬戶的非冪等轉移**
@@ -457,7 +459,7 @@ COMMIT;
客戶端可以重連到資料庫並重試事務,但現在已經處於 TCP 重複抑制的範圍之外了。因為 [例 12-1]() 中的事務不是冪等的,可能會發生轉了 \$22 而不是期望的 \$11。因此儘管 [例 12-1]() 是一個事務原子性的標準樣例但它實際上並不正確而真正的銀行並不會這樣辦事【3】。
兩階段提交(請參閱 “[原子提交與兩階段提交](ch9.md#原子提交與兩階段提交)”)協議會破壞 TCP 連線與事務之間的 1:1 對映,因為它們必須在故障後允許事務協調器重連到資料庫,告訴資料庫將存疑事務提交還是中止。這足以確保事務只被恰好執行一次嗎?不幸的是,並不能。
兩階段提交(請參閱 “[原子提交與兩階段提交](/tw/ch9#原子提交與兩階段提交)”)協議會破壞 TCP 連線與事務之間的 1:1 對映,因為它們必須在故障後允許事務協調器重連到資料庫,告訴資料庫將存疑事務提交還是中止。這足以確保事務只被恰好執行一次嗎?不幸的是,並不能。
即使我們可以抑制資料庫客戶端與伺服器之間的重複事務,我們仍然需要擔心終端使用者裝置與應用伺服器之間的網路。例如,如果終端使用者的客戶端是 Web 瀏覽器,則它可能會使用 HTTP POST 請求向伺服器提交指令。也許使用者正處於一個訊號微弱的蜂窩資料網路連線中,它們成功地傳送了 POST但卻在能夠從伺服器接收響應之前沒了訊號。
@@ -482,9 +484,9 @@ BEGIN TRANSACTION;
COMMIT;
```
[例 12-2]() 依賴於 `request_id` 列上的唯一約束。如果一個事務嘗試插入一個已經存在的 ID那麼 `INSERT` 失敗,事務被中止,使其無法生效兩次。即使在較弱的隔離級別下,關係資料庫也能正確地維護唯一性約束(而在 “[寫入偏差與幻讀](ch7.md#寫入偏差與幻讀)” 中討論過,應用級別的 **檢查 - 然後 - 插入** 可能會在不可序列化的隔離下失敗)。
[例 12-2]() 依賴於 `request_id` 列上的唯一約束。如果一個事務嘗試插入一個已經存在的 ID那麼 `INSERT` 失敗,事務被中止,使其無法生效兩次。即使在較弱的隔離級別下,關係資料庫也能正確地維護唯一性約束(而在 “[寫入偏差與幻讀](/tw/ch7#寫入偏差與幻讀)” 中討論過,應用級別的 **檢查 - 然後 - 插入** 可能會在不可序列化的隔離下失敗)。
除了抑制重複的請求之外,[例 12-2]() 中的請求表表現得就像一種事件日誌,暗示著事件溯源的想法(請參閱 “[事件溯源](ch11.md#事件溯源)”)。更新賬戶餘額事實上不必與插入事件發生在同一個事務中,因為它們是冗餘的,而能由下游消費者從請求事件中衍生出來 —— 只要該事件被恰好處理一次,這又一次可以使用請求 ID 來強制執行。
除了抑制重複的請求之外,[例 12-2]() 中的請求表表現得就像一種事件日誌,暗示著事件溯源的想法(請參閱 “[事件溯源](/tw/ch11#事件溯源)”)。更新賬戶餘額事實上不必與插入事件發生在同一個事務中,因為它們是冗餘的,而能由下游消費者從請求事件中衍生出來 —— 只要該事件被恰好處理一次,這又一次可以使用請求 ID 來強制執行。
#### 端到端原則
@@ -507,9 +509,9 @@ COMMIT;
這實在是一個遺憾,因為容錯機制很難弄好。低層級的可靠機制(比如 TCP 中的那些)執行的相當好,因而剩下的高層級錯誤基本很少出現。如果能將這些剩下的高層級容錯機制打包成抽象,而應用不需要再去操心,那該多好呀 —— 但恐怕我們還沒有找到這一正確的抽象。
長期以來,事務被認為是一個很好的抽象,我相信它們確實是很有用的。正如 [第七章](ch7.md) 導言中所討論的,它們將各種可能的問題(併發寫入、違背約束、崩潰、網路中斷、磁碟故障)合併為兩種可能結果:提交或中止。這是對程式設計模型而言是一種巨大的簡化,但恐怕這還不夠。
長期以來,事務被認為是一個很好的抽象,我相信它們確實是很有用的。正如 [第七章](/tw/ch7) 導言中所討論的,它們將各種可能的問題(併發寫入、違背約束、崩潰、網路中斷、磁碟故障)合併為兩種可能結果:提交或中止。這是對程式設計模型而言是一種巨大的簡化,但恐怕這還不夠。
事務是代價高昂的,當涉及異構儲存技術時尤為甚(請參閱 “[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”)。我們拒絕使用分散式事務是因為它開銷太大,結果我們最後不得不在應用程式碼中重新實現容錯機制。正如本書中大量的例子所示,對併發性與部分失敗的推理是困難且違反直覺的,所以我懷疑大多數應用級別的機制都不能正確工作,最終結果是資料丟失或損壞。
事務是代價高昂的,當涉及異構儲存技術時尤為甚(請參閱 “[實踐中的分散式事務](/tw/ch9#實踐中的分散式事務)”)。我們拒絕使用分散式事務是因為它開銷太大,結果我們最後不得不在應用程式碼中重新實現容錯機制。正如本書中大量的例子所示,對併發性與部分失敗的推理是困難且違反直覺的,所以我懷疑大多數應用級別的機制都不能正確工作,最終結果是資料丟失或損壞。
出於這些原因,我認為探索對容錯的抽象是很有價值的。它使提供應用特定的端到端的正確性屬性變得更簡單,而且還能在大規模分散式環境中提供良好的效能與運維特性。
@@ -517,33 +519,33 @@ COMMIT;
讓我們思考一下在 [分拆資料庫](#分拆資料庫) 上下文中的 **正確性correctness**。我們看到端到端的除重可以透過從客戶端一路透傳到資料庫的請求 ID 實現。那麼其他型別的約束呢?
我們先來特別關注一下 **唯一性約束** —— 例如我們在 [例 12-2]() 中所依賴的約束。在 “[約束和唯一性保證](ch9.md#約束和唯一性保證)” 中,我們看到了幾個其他需要強制實施唯一性的應用功能例子:使用者名稱或電子郵件地址必須唯一標識使用者,檔案儲存服務不能包含多個重名檔案,兩個人不能在航班或劇院預訂同一個座位。
我們先來特別關注一下 **唯一性約束** —— 例如我們在 [例 12-2]() 中所依賴的約束。在 “[約束和唯一性保證](/tw/ch9#約束和唯一性保證)” 中,我們看到了幾個其他需要強制實施唯一性的應用功能例子:使用者名稱或電子郵件地址必須唯一標識使用者,檔案儲存服務不能包含多個重名檔案,兩個人不能在航班或劇院預訂同一個座位。
其他型別的約束也非常類似:例如,確保帳戶餘額永遠不會變為負數,確保不會超賣庫存,或者會議室沒有重複的預訂。執行唯一性約束的技術通常也可以用於這些約束。
#### 唯一性約束需要達成共識
在 [第九章](ch9.md) 中我們看到,在分散式環境中,強制執行唯一性約束需要共識:如果存在多個具有相同值的併發請求,則系統需要決定衝突操作中的哪一個被接受,並拒絕其他違背約束的操作。
在 [第九章](/tw/ch9) 中我們看到,在分散式環境中,強制執行唯一性約束需要共識:如果存在多個具有相同值的併發請求,則系統需要決定衝突操作中的哪一個被接受,並拒絕其他違背約束的操作。
達成這一共識的最常見方式是使單個節點作為領導,並使其負責所有決策。只要你不介意所有請求都擠過單個節點(即使客戶端位於世界的另一端),只要該節點沒有失效,系統就能正常工作。如果你需要容忍領導者失效,那麼就又回到了共識問題(請參閱 “[單主複製與共識](ch9.md#單主複製與共識)”)。
達成這一共識的最常見方式是使單個節點作為領導,並使其負責所有決策。只要你不介意所有請求都擠過單個節點(即使客戶端位於世界的另一端),只要該節點沒有失效,系統就能正常工作。如果你需要容忍領導者失效,那麼就又回到了共識問題(請參閱 “[單主複製與共識](/tw/ch9#單主複製與共識)”)。
唯一性檢查可以透過對唯一性欄位分割槽做橫向伸縮。例如,如果需要透過請求 ID 確保唯一性(如 [例 12-2]() 所示),你可以確保所有具有相同請求 ID 的請求都被路由到同一分割槽(請參閱 [第六章](ch6.md))。如果你需要讓使用者名稱是唯一的,則可以按使用者名稱的雜湊值做分割槽。
唯一性檢查可以透過對唯一性欄位分割槽做橫向伸縮。例如,如果需要透過請求 ID 確保唯一性(如 [例 12-2]() 所示),你可以確保所有具有相同請求 ID 的請求都被路由到同一分割槽(請參閱 [第六章](/tw/ch6))。如果你需要讓使用者名稱是唯一的,則可以按使用者名稱的雜湊值做分割槽。
但非同步多主複製排除在外,因為可能會發生不同主庫同時接受衝突寫操作的情況,因而這些值不再是唯一的(請參閱 “[實現線性一致的系統](ch9.md#實現線性一致的系統)”。如果你想立刻拒絕任何違背約束的寫入同步協調是無法避免的【56】。
但非同步多主複製排除在外,因為可能會發生不同主庫同時接受衝突寫操作的情況,因而這些值不再是唯一的(請參閱 “[實現線性一致的系統](/tw/ch9#實現線性一致的系統)”。如果你想立刻拒絕任何違背約束的寫入同步協調是無法避免的【56】。
#### 基於日誌訊息傳遞中的唯一性
日誌確保所有消費者以相同的順序看見訊息 —— 這種保證在形式上被稱為 **全序廣播total order boardcast** 並且等價於共識(請參閱 “[全序廣播](ch9.md#全序廣播)”)。在使用基於日誌的訊息傳遞的分拆資料庫方法中,我們可以使用非常類似的方法來執行唯一性約束。
日誌確保所有消費者以相同的順序看見訊息 —— 這種保證在形式上被稱為 **全序廣播total order boardcast** 並且等價於共識(請參閱 “[全序廣播](/tw/ch9#全序廣播)”)。在使用基於日誌的訊息傳遞的分拆資料庫方法中,我們可以使用非常類似的方法來執行唯一性約束。
流處理器在單個執行緒上依次消費單個日誌分割槽中的所有訊息(請參閱 “[日誌與傳統的訊息傳遞相比](ch11.md#日誌與傳統的訊息傳遞相比)”。因此如果日誌是按需要確保唯一的值做的分割槽則流處理器可以無歧義地、確定性地決定幾個衝突操作中的哪一個先到達。例如在多個使用者嘗試宣告相同使用者名稱的情況下【57】
流處理器在單個執行緒上依次消費單個日誌分割槽中的所有訊息(請參閱 “[日誌與傳統的訊息傳遞相比](/tw/ch11#日誌與傳統的訊息傳遞相比)”。因此如果日誌是按需要確保唯一的值做的分割槽則流處理器可以無歧義地、確定性地決定幾個衝突操作中的哪一個先到達。例如在多個使用者嘗試宣告相同使用者名稱的情況下【57】
1. 每個對使用者名稱的請求都被編碼為一條訊息,並追加到按使用者名稱雜湊值確定的分割槽。
2. 流處理器依序讀取日誌中的請求,並使用本地資料庫來追蹤哪些使用者名稱已經被佔用了。對於所有申請可用使用者名稱的請求,它都會記錄該使用者名稱,並向輸出流傳送一條成功訊息。對於所有申請已佔用使用者名稱的請求,它都會向輸出流傳送一條拒絕訊息。
3. 請求使用者名稱的客戶端監視輸出流,等待與其請求相對應的成功或拒絕訊息。
該演算法基本上與 “[使用全序廣播實現線性一致的儲存](ch9.md#使用全序廣播實現線性一致的儲存)” 中的演算法相同。它可以簡單地透過增加分割槽數伸縮至較大的請求吞吐量,因為每個分割槽都可以被獨立處理。
該演算法基本上與 “[使用全序廣播實現線性一致的儲存](/tw/ch9#使用全序廣播實現線性一致的儲存)” 中的演算法相同。它可以簡單地透過增加分割槽數伸縮至較大的請求吞吐量,因為每個分割槽都可以被獨立處理。
該方法不僅適用於唯一性約束,而且適用於許多其他型別的約束。其基本原理是,任何可能衝突的寫入都會路由到相同的分割槽並按順序處理。正如 “[什麼是衝突?](ch5.md#什麼是衝突?)” 與 “[寫入偏差與幻讀](ch7.md#寫入偏差與幻讀)” 中所述,衝突的定義可能取決於應用,但流處理器可以使用任意邏輯來驗證請求。這個想法與 Bayou 在 90 年代開創的方法類似【58】。
該方法不僅適用於唯一性約束,而且適用於許多其他型別的約束。其基本原理是,任何可能衝突的寫入都會路由到相同的分割槽並按順序處理。正如 “[什麼是衝突?](/tw/ch5#什麼是衝突?)” 與 “[寫入偏差與幻讀](/tw/ch7#寫入偏差與幻讀)” 中所述,衝突的定義可能取決於應用,但流處理器可以使用任意邏輯來驗證請求。這個想法與 Bayou 在 90 年代開創的方法類似【58】。
#### 多分割槽請求處理
@@ -557,17 +559,17 @@ COMMIT;
2. 流處理器讀取請求日誌。對於每個請求訊息,它向輸出流發出兩條訊息:付款人賬戶 A 的借記指令(按 A 分割槽),收款人 B 的貸記指令(按 B 分割槽)。被發出的訊息中會帶有原始的請求 ID。
3. 後續處理器消費借記 / 貸記指令流,按照請求 ID 除重,並將變更應用至賬戶餘額。
步驟 1 和步驟 2 是必要的,因為如果客戶直接傳送貸記與借記指令,則需要在這兩個分割槽之間進行原子提交,以確保兩者要麼都發生或都不發生。為了避免對分散式事務的需要,我們首先將請求持久化記錄為單條訊息,然後從這第一條訊息中衍生出貸記指令與借記指令。幾乎在所有資料系統中,單物件寫入都是原子性的(請參閱 “[單物件寫入](ch7.md#單物件寫入)),因此請求要麼出現在日誌中,要麼就不出現,無需多分割槽原子提交。
步驟 1 和步驟 2 是必要的,因為如果客戶直接傳送貸記與借記指令,則需要在這兩個分割槽之間進行原子提交,以確保兩者要麼都發生或都不發生。為了避免對分散式事務的需要,我們首先將請求持久化記錄為單條訊息,然後從這第一條訊息中衍生出貸記指令與借記指令。幾乎在所有資料系統中,單物件寫入都是原子性的(請參閱 “[單物件寫入](/tw/ch7#單物件寫入)),因此請求要麼出現在日誌中,要麼就不出現,無需多分割槽原子提交。
如果流處理器在步驟 2 中崩潰,則它會從上一個存檔點恢復處理。這樣做時,它不會跳過任何請求訊息,但可能會多次處理請求併產生重複的貸記與借記指令。但由於它是確定性的,因此它只是再次生成相同的指令,而步驟 3 中的處理器可以使用端到端請求 ID 輕鬆地對其除重。
如果你想確保付款人的帳戶不會因此次轉賬而透支,則可以使用一個額外的流處理器來維護賬戶餘額並校驗事務(按付款人賬戶分割槽),只有有效的事務會被記錄在步驟 1 中的請求日誌中。
透過將多分割槽事務分解為兩個不同分割槽方式的階段,並使用端到端的請求 ID我們實現了同樣的正確性屬性每個請求對付款人與收款人都恰好生效一次即使在出現故障且沒有使用原子提交協議的情況下依然如此。使用多個不同分割槽階段的想法與我們在 “[多分割槽資料處理](#多分割槽資料處理)” 中討論的想法類似(也請參閱 “[併發控制](ch11.md#併發控制)”)。
透過將多分割槽事務分解為兩個不同分割槽方式的階段,並使用端到端的請求 ID我們實現了同樣的正確性屬性每個請求對付款人與收款人都恰好生效一次即使在出現故障且沒有使用原子提交協議的情況下依然如此。使用多個不同分割槽階段的想法與我們在 “[多分割槽資料處理](#多分割槽資料處理)” 中討論的想法類似(也請參閱 “[併發控制](/tw/ch11#併發控制)”)。
### 及時性與完整性
事務的一個便利屬性是,它們通常是線性一致的(請參閱 “[線性一致性](ch9.md#線性一致性)”),也就是說,寫入者會等到事務提交,而之後其寫入立刻對所有讀取者可見。
事務的一個便利屬性是,它們通常是線性一致的(請參閱 “[線性一致性](/tw/ch9#線性一致性)”),也就是說,寫入者會等到事務提交,而之後其寫入立刻對所有讀取者可見。
當我們把一個操作拆分為跨越多個階段的流處理器時,卻並非如此:日誌的消費者在設計上就是非同步的,因此傳送者不會等其訊息被消費者處理完。但是,客戶端等待輸出流中的特定訊息是可能的。這正是我們在 “[基於日誌訊息傳遞中的唯一性](#基於日誌訊息傳遞中的唯一性)” 一節中檢查唯一性約束時所做的事情。
@@ -577,15 +579,15 @@ COMMIT;
* 及時性Timeliness
及時性意味著確保使用者觀察到系統的最新狀態。我們之前看到,如果使用者從陳舊的資料副本中讀取資料,它們可能會觀察到系統處於不一致的狀態(請參閱 “[複製延遲問題](ch5.md#複製延遲問題)”)。但這種不一致是暫時的,而最終會透過等待與重試簡單地得到解決。
及時性意味著確保使用者觀察到系統的最新狀態。我們之前看到,如果使用者從陳舊的資料副本中讀取資料,它們可能會觀察到系統處於不一致的狀態(請參閱 “[複製延遲問題](/tw/ch5#複製延遲問題)”)。但這種不一致是暫時的,而最終會透過等待與重試簡單地得到解決。
CAP 定理(請參閱 “[線性一致性的代價](ch9.md#線性一致性的代價)”)使用 **線性一致性linearizability** 意義上的一致性,這是實現及時性的強有力方法。像 **寫後讀** 這樣及時性更弱的一致性也很有用(請參閱 “[讀己之寫](ch5.md#讀己之寫)”)。
CAP 定理(請參閱 “[線性一致性的代價](/tw/ch9#線性一致性的代價)”)使用 **線性一致性linearizability** 意義上的一致性,這是實現及時性的強有力方法。像 **寫後讀** 這樣及時性更弱的一致性也很有用(請參閱 “[讀己之寫](/tw/ch5#讀己之寫)”)。
* 完整性Integrity
完整性意味著沒有損壞;即沒有資料丟失,並且沒有矛盾或錯誤的資料。尤其是如果某些衍生資料集是作為底層資料之上的檢視而維護的(請參閱 “[從事件日誌中派生出當前狀態](ch11.md#從事件日誌中派生出當前狀態)”),這種衍生必須是正確的。例如,資料庫索引必須正確地反映資料庫的內容 —— 缺失某些記錄的索引並不是很有用。
完整性意味著沒有損壞;即沒有資料丟失,並且沒有矛盾或錯誤的資料。尤其是如果某些衍生資料集是作為底層資料之上的檢視而維護的(請參閱 “[從事件日誌中派生出當前狀態](/tw/ch11#從事件日誌中派生出當前狀態)”),這種衍生必須是正確的。例如,資料庫索引必須正確地反映資料庫的內容 —— 缺失某些記錄的索引並不是很有用。
如果完整性被違背,這種不一致是永久的:在大多數情況下,等待與重試並不能修復資料庫損壞。相反的是,需要顯式地檢查與修復。在 ACID 事務的上下文中(請參閱 “[ACID 的含義](ch7.md#ACID的含義)”),一致性通常被理解為某種特定於應用的完整性概念。原子性和永續性是保持完整性的重要工具。
如果完整性被違背,這種不一致是永久的:在大多數情況下,等待與重試並不能修復資料庫損壞。相反的是,需要顯式地檢查與修復。在 ACID 事務的上下文中(請參閱 “[ACID 的含義](/tw/ch7#ACID的含義)”),一致性通常被理解為某種特定於應用的完整性概念。原子性和永續性是保持完整性的重要工具。
口號形式:違反及時性,“最終一致性”;違反完整性,“永無一致性”。
@@ -600,14 +602,14 @@ ACID 事務通常既提供及時性(例如線性一致性)也提供完整性
另一方面,對於在本章中討論的基於事件的資料流系統而言,它們的一個有趣特性就是將及時性與完整性分開。在非同步處理事件流時不能保證及時性,除非你顯式構建一個在返回之前明確等待特定訊息到達的消費者。但完整性實際上才是流處理系統的核心。
**恰好一次** 或 **等效一次** 語義(請參閱 “[容錯](ch11.md#容錯)”)是一種保持完整性的機制。如果事件丟失或者生效兩次,就有可能違背資料系統的完整性。因此在出現故障時,容錯訊息傳遞與重複抑制(例如,冪等操作)對於維護資料系統的完整性是很重要的。
**恰好一次** 或 **等效一次** 語義(請參閱 “[容錯](/tw/ch11#容錯)”)是一種保持完整性的機制。如果事件丟失或者生效兩次,就有可能違背資料系統的完整性。因此在出現故障時,容錯訊息傳遞與重複抑制(例如,冪等操作)對於維護資料系統的完整性是很重要的。
正如我們在上一節看到的那樣,可靠的流處理系統可以在無需分散式事務與原子提交協議的情況下保持完整性,這意味著它們有潛力達到與後者相當的正確性,同時還具備好得多的效能與運維穩健性。為了達成這種正確性,我們組合使用了多種機制:
* 將寫入操作的內容表示為單條訊息,從而可以輕鬆地被原子寫入 —— 與事件溯源搭配效果拔群(請參閱 “[事件溯源](ch11.md#事件溯源)”)。
* 使用與儲存過程類似的確定性衍生函式,從這一訊息中衍生出所有其他的狀態變更(請參閱 “[真的序列執行](ch7.md#真的序列執行)” 和 “[應用程式碼作為衍生函式](ch12.md#應用程式碼作為衍生函式)”)
* 將寫入操作的內容表示為單條訊息,從而可以輕鬆地被原子寫入 —— 與事件溯源搭配效果拔群(請參閱 “[事件溯源](/tw/ch11#事件溯源)”)。
* 使用與儲存過程類似的確定性衍生函式,從這一訊息中衍生出所有其他的狀態變更(請參閱 “[真的序列執行](/tw/ch7#真的序列執行)” 和 “[應用程式碼作為衍生函式](/tw/ch12#應用程式碼作為衍生函式)”)
* 將客戶端生成的請求 ID 傳遞透過所有的處理層次,從而允許端到端的除重,帶來冪等性。
* 使訊息不可變,並允許衍生資料能隨時被重新處理,這使從錯誤中恢復更加容易(請參閱 “[不可變事件的優點](ch11.md#不可變事件的優點)”)
* 使訊息不可變,並允許衍生資料能隨時被重新處理,這使從錯誤中恢復更加容易(請參閱 “[不可變事件的優點](/tw/ch11#不可變事件的優點)”)
這種機制組合在我看來,是未來構建容錯應用的一個非常有前景的方向。
@@ -626,7 +628,7 @@ ACID 事務通常既提供及時性(例如線性一致性)也提供完整性
道歉的成本是否能接受是一個商業決策。如果可以接受的話,在寫入資料之前檢查所有約束的傳統模型反而會帶來不必要的限制,而線性一致性的約束也不是必須的。樂觀寫入,事後檢查可能是一種合理的選擇。你仍然可以在做一些挽回成本高昂的事情前確保有相關的驗證,但這並不意味著寫入資料之前必須先進行驗證。
這些應用 **確實** 需要完整性:你不會希望丟失預訂資訊,或者由於借方貸方不匹配導致資金消失。但是它們在執行約束時 **並不需要** 及時性:如果你銷售的貨物多於倉庫中的庫存,可以在事後道歉後並彌補問題。這種做法與我們在 “[處理寫入衝突](ch5.md#處理寫入衝突)” 中討論的衝突解決方法類似。
這些應用 **確實** 需要完整性:你不會希望丟失預訂資訊,或者由於借方貸方不匹配導致資金消失。但是它們在執行約束時 **並不需要** 及時性:如果你銷售的貨物多於倉庫中的庫存,可以在事後道歉後並彌補問題。這種做法與我們在 “[處理寫入衝突](/tw/ch5#處理寫入衝突)” 中討論的衝突解決方法類似。
#### 無協調資料系統
@@ -639,17 +641,17 @@ ACID 事務通常既提供及時性(例如線性一致性)也提供完整性
例如,這種系統可以使用多領導者配置運維,跨越多個數據中心,在區域間非同步複製。任何一個數據中心都可以持續獨立執行,因為不需要同步的跨區域協調。這樣的系統的及時性保證會很弱 —— 如果不引入協調它是不可能是線性一致的 —— 但它仍然可以提供有力的完整性保證。
在這種情況下可序列化事務作為維護衍生狀態的一部分仍然是有用的但它們只能在小範圍內執行在那裡它們工作得很好【8】。異構分散式事務如 XA 事務,請參閱 “[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”不是必需的。同步協調仍然可以在需要的地方引入例如在無法恢復的操作之前強制執行嚴格的約束但是如果只是應用的一小部分地方需要它沒必要讓所有操作都付出協調的代價。【43】。
在這種情況下可序列化事務作為維護衍生狀態的一部分仍然是有用的但它們只能在小範圍內執行在那裡它們工作得很好【8】。異構分散式事務如 XA 事務,請參閱 “[實踐中的分散式事務](/tw/ch9#實踐中的分散式事務)”不是必需的。同步協調仍然可以在需要的地方引入例如在無法恢復的操作之前強制執行嚴格的約束但是如果只是應用的一小部分地方需要它沒必要讓所有操作都付出協調的代價。【43】。
另一種審視協調與約束的角度是:它們減少了由於不一致而必須做出的道歉數量,但也可能會降低系統的效能和可用性,從而可能增加由於宕機中斷而需要做出的道歉數量。你不可能將道歉數量減少到零,但可以根據自己的需求尋找最佳平衡點 —— 既不存在太多不一致性,又不存在太多可用性問題。
### 信任但驗證
我們所有關於正確性,完整性和容錯的討論都基於一些假設,假設某些事情可能會出錯,但其他事情不會。我們將這些假設稱為我們的 **系統模型**system model請參閱 “[將系統模型對映到現實世界](ch8.md#將系統模型對映到現實世界)”):例如,我們應該假設程序可能會崩潰,機器可能突然斷電,網路可能會任意延遲或丟棄訊息。但是我們也可能假設寫入磁碟的資料在執行 `fsync` 後不會丟失,記憶體中的資料沒有損壞,而 CPU 的乘法指令總是能返回正確的結果。
我們所有關於正確性,完整性和容錯的討論都基於一些假設,假設某些事情可能會出錯,但其他事情不會。我們將這些假設稱為我們的 **系統模型**system model請參閱 “[將系統模型對映到現實世界](/tw/ch8#將系統模型對映到現實世界)”):例如,我們應該假設程序可能會崩潰,機器可能突然斷電,網路可能會任意延遲或丟棄訊息。但是我們也可能假設寫入磁碟的資料在執行 `fsync` 後不會丟失,記憶體中的資料沒有損壞,而 CPU 的乘法指令總是能返回正確的結果。
這些假設是相當合理的,因為大多數時候它們都是成立的,如果我們不得不經常擔心計算機出錯,那麼基本上寸步難行。在傳統上,系統模型採用二元方法處理故障:我們假設有些事情可能會發生,而其他事情 **永遠** 不會發生。實際上,這更像是一個機率問題:有些事情更有可能,其他事情不太可能。問題在於違反我們假設的情況是否經常發生,以至於我們可能在實踐中遇到它們。
我們已經看到,資料可能會在尚未落盤時損壞(請參閱 “[複製與永續性](ch7.md#複製與永續性)”),而網路上的資料損壞有時可能規避了 TCP 校驗和(請參閱 “[弱謊言形式](ch8.md#弱謊言形式)” )。也許我們應當更關注這些事情?
我們已經看到,資料可能會在尚未落盤時損壞(請參閱 “[複製與永續性](/tw/ch7#複製與永續性)”),而網路上的資料損壞有時可能規避了 TCP 校驗和(請參閱 “[弱謊言形式](/tw/ch8#弱謊言形式)” )。也許我們應當更關注這些事情?
我過去所從事的一個應用收集了來自客戶端的崩潰報告我們收到的一些報告只有在這些裝置記憶體中出現了隨機位翻轉才解釋的通。這看起來不太可能但是如果有足夠多的裝置執行你的軟體那麼即使再不可能發生的事也確實會發生。除了由於硬體故障或輻射導致的隨機儲存器損壞之外一些病態的儲存器訪問模式甚至可以在沒有故障的儲存器中翻轉位【62】 —— 一種可用於破壞作業系統安全機制的效應【63】這種技術被稱為 **Rowhammer**)。一旦你仔細觀察,硬體並不是看上去那樣完美的抽象。
@@ -663,13 +665,13 @@ ACID 事務通常既提供及時性(例如線性一致性)也提供完整性
而對於應用程式碼我們不得不假設會有更多的錯誤因為絕大多數應用的程式碼經受的評審與測試遠遠無法與資料庫的程式碼相比。許多應用甚至沒有正確使用資料庫提供的用於維持完整性的功能例如外部索引鍵或唯一性約束【36】。
ACID 意義下的一致性(請參閱 “[一致性](ch7.md#一致性)”)基於這樣一種想法:資料庫以一致的狀態啟動,而事務將其從一個一致狀態轉換至另一個一致的狀態。因此,我們期望資料庫始終處於一致狀態。然而,只有當你假設事務沒有 Bug 時,這種想法才有意義。如果應用以某種錯誤的方式使用資料庫,例如,不安全地使用弱隔離等級,資料庫的完整性就無法得到保證。
ACID 意義下的一致性(請參閱 “[一致性](/tw/ch7#一致性)”)基於這樣一種想法:資料庫以一致的狀態啟動,而事務將其從一個一致狀態轉換至另一個一致的狀態。因此,我們期望資料庫始終處於一致狀態。然而,只有當你假設事務沒有 Bug 時,這種想法才有意義。如果應用以某種錯誤的方式使用資料庫,例如,不安全地使用弱隔離等級,資料庫的完整性就無法得到保證。
#### 不要盲目信任承諾
由於硬體和軟體並不總是符合我們的理想,所以資料損壞似乎早晚不可避免。因此,我們至少應該有辦法查明資料是否已經損壞,以便我們能夠修復它,並嘗試追查錯誤的來源。檢查資料完整性稱為 **審計auditing**
如 “[不可變事件的優點](ch11.md#不可變事件的優點)” 一節中所述,審計不僅僅適用於財務應用程式。不過,可審計性在財務中是非常非常重要的,因為每個人都知道錯誤總會發生,我們也都認為能夠檢測和解決問題是合理的需求。
如 “[不可變事件的優點](/tw/ch11#不可變事件的優點)” 一節中所述,審計不僅僅適用於財務應用程式。不過,可審計性在財務中是非常非常重要的,因為每個人都知道錯誤總會發生,我們也都認為能夠檢測和解決問題是合理的需求。
成熟的系統同樣傾向於考慮不太可能的事情出錯的可能性並管理這種風險。例如HDFS 和 Amazon S3 等大規模儲存系統並不完全信任磁碟它們執行後臺程序持續回讀檔案並將其與其他副本進行比較並將檔案從一個磁碟移動到另一個以便降低靜默損壞的風險【67】。
@@ -685,11 +687,11 @@ ACID 意義下的一致性(請參閱 “[一致性](ch7.md#一致性)”)基
#### 為可審計性而設計
如果一個事務在一個數據庫中改變了多個物件,在這一事實發生後,很難說清這個事務到底意味著什麼。即使你捕獲了事務日誌(請參閱 “[變更資料捕獲](ch11.md#變更資料捕獲)”),各種表中的插入、更新和刪除操作並不一定能清楚地表明 **為什麼** 要執行這些變更。決定這些變更的是應用邏輯中的呼叫,而這一應用邏輯稍縱即逝,無法重現。
如果一個事務在一個數據庫中改變了多個物件,在這一事實發生後,很難說清這個事務到底意味著什麼。即使你捕獲了事務日誌(請參閱 “[變更資料捕獲](/tw/ch11#變更資料捕獲)”),各種表中的插入、更新和刪除操作並不一定能清楚地表明 **為什麼** 要執行這些變更。決定這些變更的是應用邏輯中的呼叫,而這一應用邏輯稍縱即逝,無法重現。
相比之下,基於事件的系統可以提供更好的可審計性。在事件溯源方法中,系統的使用者輸入被表示為一個單一不可變事件,而任何其導致的狀態變更都衍生自該事件。衍生可以實現為具有確定性與可重複性,因而相同的事件日誌透過相同版本的衍生程式碼時,會導致相同的狀態變更。
顯式處理資料流(請參閱 “[批處理輸出的哲學](ch10.md#批處理輸出的哲學)”)可以使資料的 **來龍去脈provenance** 更加清晰,從而使完整性檢查更具可行性。對於事件日誌,我們可以使用雜湊來檢查事件儲存沒有被破壞。對於任何衍生狀態,我們可以重新執行從事件日誌中衍生它的批處理器與流處理器,以檢查是否獲得相同的結果,或者,甚至並行執行冗餘的衍生流程。
顯式處理資料流(請參閱 “[批處理輸出的哲學](/tw/ch10#批處理輸出的哲學)”)可以使資料的 **來龍去脈provenance** 更加清晰,從而使完整性檢查更具可行性。對於事件日誌,我們可以使用雜湊來檢查事件儲存沒有被破壞。對於任何衍生狀態,我們可以重新執行從事件日誌中衍生它的批處理器與流處理器,以檢查是否獲得相同的結果,或者,甚至並行執行冗餘的衍生流程。
具有確定性且定義良好的資料流,也使除錯與跟蹤系統的執行變得容易,以便確定它 **為什麼** 做了某些事情【4,69】。如果出現意想之外的事情那麼重現導致意外事件的確切事故現場的診斷能力 —— 一種時間旅行除錯功能是非常有價值的。
@@ -709,7 +711,7 @@ ACID 意義下的一致性(請參閱 “[一致性](ch7.md#一致性)”)基
我沒有資格評論這些技術用於貨幣,或者合同商定機制的價值。但從資料系統的角度來看,它們包含了一些有趣的想法。實質上,它們是分散式資料庫,具有資料模型與事務機制,而不同副本可以由互不信任的組織託管。副本不斷檢查其他副本的完整性,並使用共識協議對應當執行的事務達成一致。
我對這些技術的拜占庭容錯方面有些懷疑(請參閱 “[拜占庭故障](ch8.md#拜占庭故障)”),而且我發現 **工作證明proof of work** 技術非常浪費(比如,比特幣挖礦)。比特幣的交易吞吐量相當低,儘管更多是出於政治與經濟原因而非技術上的原因。不過,完整性檢查的方面是很有趣的。
我對這些技術的拜占庭容錯方面有些懷疑(請參閱 “[拜占庭故障](/tw/ch8#拜占庭故障)”),而且我發現 **工作證明proof of work** 技術非常浪費(比如,比特幣挖礦)。比特幣的交易吞吐量相當低,儘管更多是出於政治與經濟原因而非技術上的原因。不過,完整性檢查的方面是很有趣的。
密碼學審計與完整性檢查通常依賴 **默克爾樹Merkle tree**【74】這是一顆雜湊值的樹能夠用於高效地證明一條記錄出現在一個數據集中以及其他一些特性。除了炒作的沸沸揚揚的加密貨幣之外**證書透明性certificate transparency** 也是一種依賴 Merkle 樹的安全技術,用來檢查 TLS/SSL 證書的有效性【75,76】。
@@ -862,7 +864,7 @@ ACID 意義下的一致性(請參閱 “[一致性](ch7.md#一致性)”)基
我們應該允許每個人保留自己的隱私 —— 即,對自己資料的控制 —— 而不是透過監視來竊取這種控制權。我們控制自己資料的個體權利就像是國家公園的自然環境:如果我們不去明確地保護它、關心它,它就會被破壞。這將是公地的悲劇,我們都會因此而變得更糟。無所不在的監視並非不可避免的 —— 我們現在仍然能阻止它。
我們究竟能做到哪一步是一個開放的問題。首先我們不應該永久保留資料而是一旦不再需要就立即清除資料【111,112】。清除資料與不變性的想法背道而馳請參閱 “[不變性的侷限性](ch11.md#不變性的侷限性)”但這是可以解決的問題。我所看到的一種很有前景的方法是透過加密協議來實施訪問控制而不僅僅是透過策略【113,114】。總的來說文化與態度的改變是必要的。
我們究竟能做到哪一步是一個開放的問題。首先我們不應該永久保留資料而是一旦不再需要就立即清除資料【111,112】。清除資料與不變性的想法背道而馳請參閱 “[不變性的侷限性](/tw/ch11#不變性的侷限性)”但這是可以解決的問題。我所看到的一種很有前景的方法是透過加密協議來實施訪問控制而不僅僅是透過策略【113,114】。總的來說文化與態度的改變是必要的。
## 本章小結
@@ -1001,11 +1003,4 @@ ACID 意義下的一致性(請參閱 “[一致性](ch7.md#一致性)”)基
1. Maciej Cegłowski: “[Haunted by Data](http://idlewords.com/talks/haunted_by_data.htm),” *idlewords.com*, October 2015.
1. Sam Thielman: “[You Are Not What You Read: Librarians Purge User Data to Protect Privacy](https://www.theguardian.com/us-news/2016/jan/13/us-library-records-purged-data-privacy),” *theguardian.com*, January 13, 2016.
1. Conor Friedersdorf: “[Edward Snowdens Other Motive for Leaking](http://www.theatlantic.com/politics/archive/2014/05/edward-snowdens-other-motive-for-leaking/370068/),” *theatlantic.com*, May 13, 2014.
1. Phillip Rogaway: “[The Moral Character of Cryptographic Work](http://web.cs.ucdavis.edu/~rogaway/papers/moral-fn.pdf),” Cryptology ePrint 2015/1162, December 2015.
------
| 上一章 | 目錄 | 下一章 |
| --------------------------- | ------------------------------- | ------------------- |
| [第十一章:流處理](ch11.md) | [設計資料密集型應用](README.md) | [後記](colophon.md) |
1. Phillip Rogaway: “[The Moral Character of Cryptographic Work](http://web.cs.ucdavis.edu/~rogaway/papers/moral-fn.pdf),” Cryptology ePrint 2015/1162, December 2015.

View File

@@ -1,15 +1,17 @@
# 第二章:資料模型與查詢語言
---
title: "第二章:資料模型與查詢語言"
linkTitle: "2. 資料模型與查詢語言"
weight: 102
breadcrumbs: false
---
![](../img/ch2.png)
![](/img/ch2.png)
> 語言的邊界就是思想的邊界。
>
> —— 路德維奇・維特根斯坦《邏輯哲學》1922
>
-------------------
[TOC]
資料模型可能是軟體開發中最重要的部分了,因為它們的影響如此深遠:不僅僅影響著軟體的編寫方式,而且影響著我們的 **解題思路**
@@ -26,7 +28,7 @@
掌握一個數據模型需要花費很多精力(想想關係資料建模有多少本書)。即便只使用一個數據模型,不用操心其內部工作機制,構建軟體也是非常困難的。然而,因為資料模型對上層軟體的功能(能做什麼,不能做什麼)有著至深的影響,所以選擇一個適合的資料模型是非常重要的。
在本章中,我們將研究一系列用於資料儲存和查詢的通用資料模型(前面列表中的第 2 點)。特別地,我們將比較關係模型,文件模型和少量基於圖形的資料模型。我們還將檢視各種查詢語言並比較它們的用例。在 [第三章](ch3.md) 中,我們將討論儲存引擎是如何工作的。也就是說,這些資料模型實際上是如何實現的(列表中的第 3 點)。
在本章中,我們將研究一系列用於資料儲存和查詢的通用資料模型(前面列表中的第 2 點)。特別地,我們將比較關係模型,文件模型和少量基於圖形的資料模型。我們還將檢視各種查詢語言並比較它們的用例。在 [第三章](/tw/ch3) 中,我們將討論儲存引擎是如何工作的。也就是說,這些資料模型實際上是如何實現的(列表中的第 3 點)。
## 關係模型與文件模型
@@ -64,13 +66,13 @@
像 ActiveRecord 和 Hibernate 這樣的 **物件關係對映ORM object-relational mapping** 框架可以減少這個轉換層所需的樣板程式碼的數量,但是它們不能完全隱藏這兩個模型之間的差異。
![](../img/fig2-1.png)
![](/img/fig2-1.png)
**圖 2-1 使用關係型模式來表示領英簡介**
例如,[圖 2-1](../img/fig2-1.png) 展示瞭如何在關係模式中表示簡歷(一個 LinkedIn 簡介)。整個簡介可以透過一個唯一的識別符號 `user_id` 來標識。像 `first_name``last_name` 這樣的欄位每個使用者只出現一次,所以可以在 User 表上將其建模為列。但是,大多數人在職業生涯中擁有多於一份的工作,人們可能有不同樣的教育階段和任意數量的聯絡資訊。從使用者到這些專案之間存在一對多的關係,可以用多種方式來表示:
例如,[圖 2-1](/img/fig2-1.png) 展示瞭如何在關係模式中表示簡歷(一個 LinkedIn 簡介)。整個簡介可以透過一個唯一的識別符號 `user_id` 來標識。像 `first_name``last_name` 這樣的欄位每個使用者只出現一次,所以可以在 User 表上將其建模為列。但是,大多數人在職業生涯中擁有多於一份的工作,人們可能有不同樣的教育階段和任意數量的聯絡資訊。從使用者到這些專案之間存在一對多的關係,可以用多種方式來表示:
* 傳統 SQL 模型SQL1999 之前)中,最常見的規範化表示形式是將職位,教育和聯絡資訊放在單獨的表中,對 User 表提供外部索引鍵引用,如 [圖 2-1](../img/fig2-1.png) 所示。
* 傳統 SQL 模型SQL1999 之前)中,最常見的規範化表示形式是將職位,教育和聯絡資訊放在單獨的表中,對 User 表提供外部索引鍵引用,如 [圖 2-1](/img/fig2-1.png) 所示。
* 後續的 SQL 標準增加了對結構化資料型別和 XML 資料的支援;這允許將多值資料儲存在單行內,並支援在這些文件內查詢和索引。這些功能在 OracleIBM DB2MS SQL Server 和 PostgreSQL 中都有不同程度的支援【6,7】。JSON 資料型別也得到多個數據庫的支援,包括 IBM DB2MySQL 和 PostgreSQL 【8】。
* 第三種選擇是將職業,教育和聯絡資訊編碼為 JSON 或 XML 文件,將其儲存在資料庫的文字列中,並讓應用程式解析其結構和內容。這種配置下,通常不能使用資料庫來查詢該編碼列中的值。
@@ -116,13 +118,13 @@
}
```
有一些開發人員認為 JSON 模型減少了應用程式程式碼和儲存層之間的阻抗不匹配。不過,正如我們將在 [第四章](ch4.md) 中看到的那樣JSON 作為資料編碼格式也存在問題。無模式對 JSON 模型來說往往被認為是一個優勢;我們將在 “[文件模型中的模式靈活性](#文件模型中的模式靈活性)” 中討論這個問題。
有一些開發人員認為 JSON 模型減少了應用程式程式碼和儲存層之間的阻抗不匹配。不過,正如我們將在 [第四章](/tw/ch4) 中看到的那樣JSON 作為資料編碼格式也存在問題。無模式對 JSON 模型來說往往被認為是一個優勢;我們將在 “[文件模型中的模式靈活性](#文件模型中的模式靈活性)” 中討論這個問題。
JSON 表示比 [圖 2-1](../img/fig2-1.png) 中的多表模式具有更好的 **區域性locality**。如果在前面的關係型示例中獲取簡介,那需要執行多個查詢(透過 `user_id` 查詢每個表),或者在 User 表與其下屬表之間混亂地執行多路連線。而在 JSON 表示中,所有相關資訊都在同一個地方,一個查詢就足夠了。
JSON 表示比 [圖 2-1](/img/fig2-1.png) 中的多表模式具有更好的 **區域性locality**。如果在前面的關係型示例中獲取簡介,那需要執行多個查詢(透過 `user_id` 查詢每個表),或者在 User 表與其下屬表之間混亂地執行多路連線。而在 JSON 表示中,所有相關資訊都在同一個地方,一個查詢就足夠了。
從使用者簡介檔案到使用者職位,教育歷史和聯絡資訊,這種一對多關係隱含了資料中的一個樹狀結構,而 JSON 表示使得這個樹狀結構變得明確(見 [圖 2-2](../img/fig2-2.png))。
從使用者簡介檔案到使用者職位,教育歷史和聯絡資訊,這種一對多關係隱含了資料中的一個樹狀結構,而 JSON 表示使得這個樹狀結構變得明確(見 [圖 2-2](/img/fig2-2.png))。
![](../img/fig2-2.png)
![](/img/fig2-2.png)
**圖 2-2 一對多關係構建了一個樹結構**
@@ -144,7 +146,7 @@ JSON 表示比 [圖 2-1](../img/fig2-1.png) 中的多表模式具有更好的 **
[^ii]: 關於關係模型的文獻區分了幾種不同的規範形式,但這些區別幾乎沒有實際意義。一個經驗法則是,如果重複儲存了可以儲存在一個地方的值,則模式就不是 **規範化normalized** 的。
> 資料庫管理員和開發人員喜歡爭論規範化和非規範化,讓我們暫時保留判斷吧。在本書的 [第三部分](part-iii.md),我們將回到這個話題,探討系統的方法用以處理快取,非規範化和衍生資料。
> 資料庫管理員和開發人員喜歡爭論規範化和非規範化,讓我們暫時保留判斷吧。在本書的 [第三部分](/tw/part-iii),我們將回到這個話題,探討系統的方法用以處理快取,非規範化和衍生資料。
不幸的是,對這些資料進行規範化需要多對一的關係(許多人生活在一個特定的地區,許多人在一個特定的行業工作),這與文件模型不太吻合。在關係資料庫中,透過 ID 來引用其他表中的行是正常的,因為連線很容易。在文件資料庫中,一對多樹結構沒有必要用連線,對連線的支援通常很弱 [^iii]。
@@ -156,19 +158,19 @@ JSON 表示比 [圖 2-1](../img/fig2-1.png) 中的多表模式具有更好的 **
* 組織和學校作為實體
在前面的描述中,`organization`(使用者工作的公司)和 `school_name`(他們學習的地方)只是字串。也許他們應該是對實體的引用呢?然後,每個組織、學校或大學都可以擁有自己的網頁(標識、新聞提要等)。每個簡歷可以連結到它所提到的組織和學校,並且包括他們的圖示和其他資訊(請參閱 [圖 2-3](../img/fig2-3.png),來自 LinkedIn 的一個例子)。
在前面的描述中,`organization`(使用者工作的公司)和 `school_name`(他們學習的地方)只是字串。也許他們應該是對實體的引用呢?然後,每個組織、學校或大學都可以擁有自己的網頁(標識、新聞提要等)。每個簡歷可以連結到它所提到的組織和學校,並且包括他們的圖示和其他資訊(請參閱 [圖 2-3](/img/fig2-3.png),來自 LinkedIn 的一個例子)。
* 推薦
假設你想新增一個新的功能:一個使用者可以為另一個使用者寫一個推薦。在使用者的簡歷上顯示推薦,並附上推薦使用者的姓名和照片。如果推薦人更新他們的照片,那他們寫的任何推薦都需要顯示新的照片。因此,推薦應該擁有作者個人簡介的引用。
![](../img/fig2-3.png)
![](/img/fig2-3.png)
**圖 2-3 公司名不僅是字串還是一個指向公司實體的連結LinkedIn 截圖)**
[圖 2-4](../img/fig2-4.png) 闡明瞭這些新功能需要如何使用多對多關係。每個虛線矩形內的資料可以分組成一個文件,但是對單位,學校和其他使用者的引用需要表示成引用,並且在查詢時需要連線。
[圖 2-4](/img/fig2-4.png) 闡明瞭這些新功能需要如何使用多對多關係。每個虛線矩形內的資料可以分組成一個文件,但是對單位,學校和其他使用者的引用需要表示成引用,並且在查詢時需要連線。
![](../img/fig2-4.png)
![](/img/fig2-4.png)
**圖 2-4 使用多對多關係擴充套件簡歷**
@@ -178,7 +180,7 @@ JSON 表示比 [圖 2-1](../img/fig2-1.png) 中的多表模式具有更好的 **
20 世紀 70 年代最受歡迎的業務資料處理資料庫是 IBM 的資訊管理系統IMS最初是為了阿波羅太空計劃的庫存管理而開發的並於 1968 年有了首次商業釋出【13】。目前它仍在使用和維護執行在 IBM 大型機的 OS/390 上【14】。
IMS 的設計中使用了一個相當簡單的資料模型,稱為 **層次模型hierarchical model**,它與文件資料庫使用的 JSON 模型有一些驚人的相似之處【2】。它將所有資料表示為巢狀在記錄中的記錄樹這很像 [圖 2-2](../img/fig2-2.png) 的 JSON 結構。
IMS 的設計中使用了一個相當簡單的資料模型,稱為 **層次模型hierarchical model**,它與文件資料庫使用的 JSON 模型有一些驚人的相似之處【2】。它將所有資料表示為巢狀在記錄中的記錄樹這很像 [圖 2-2](/img/fig2-2.png) 的 JSON 結構。
同文檔資料庫一樣IMS 能良好處理一對多的關係但是很難應對多對多的關係並且不支援連線。開發人員必須決定是否複製非規範化資料或手動解決從一個記錄到另一個記錄的引用。這些二十世紀六七十年代的問題與現在開發人員遇到的文件資料庫問題非常相似【15】。
@@ -214,19 +216,19 @@ CODASYL 中的查詢是透過利用遍歷記錄列和跟隨訪問路徑表在資
#### 與文件資料庫相比
在一個方面,文件資料庫還原為層次模型:在其父記錄中儲存巢狀記錄([圖 2-1](../img/fig2-1.png) 中的一對多關係,如 `positions``education``contact_info`),而不是在單獨的表中。
在一個方面,文件資料庫還原為層次模型:在其父記錄中儲存巢狀記錄([圖 2-1](/img/fig2-1.png) 中的一對多關係,如 `positions``education``contact_info`),而不是在單獨的表中。
但是,在表示多對一和多對多的關係時,關係資料庫和文件資料庫並沒有根本的不同:在這兩種情況下,相關專案都被一個唯一的識別符號引用,這個識別符號在關係模型中被稱為 **外部索引鍵**,在文件模型中稱為 **文件引用**【9】。該識別符號在讀取時透過連線或後續查詢來解析。迄今為止文件資料庫沒有走 CODASYL 的老路。
### 關係型資料庫與文件資料庫在今日的對比
將關係資料庫與文件資料庫進行比較時,可以考慮許多方面的差異,包括它們的容錯屬性(請參閱 [第五章](ch5.md))和處理併發性(請參閱 [第七章](ch7.md))。本章將只關注資料模型中的差異。
將關係資料庫與文件資料庫進行比較時,可以考慮許多方面的差異,包括它們的容錯屬性(請參閱 [第五章](/tw/ch5))和處理併發性(請參閱 [第七章](/tw/ch7))。本章將只關注資料模型中的差異。
支援文件資料模型的主要論據是架構靈活性,因區域性而擁有更好的效能,以及對於某些應用程式而言更接近於應用程式使用的資料結構。關係模型透過為連線提供更好的支援以及支援多對一和多對多的關係來反擊。
#### 哪種資料模型更有助於簡化應用程式碼?
如果應用程式中的資料具有類似文件的結構(即,一對多關係樹,通常一次性載入整個樹),那麼使用文件模型可能是一個好主意。將類似文件的結構分解成多個表(如 [圖 2-1](../img/fig2-1.png) 中的 `positions``education``contact_info`)的關係技術可能導致繁瑣的模式和不必要的複雜的應用程式程式碼。
如果應用程式中的資料具有類似文件的結構(即,一對多關係樹,通常一次性載入整個樹),那麼使用文件模型可能是一個好主意。將類似文件的結構分解成多個表(如 [圖 2-1](/img/fig2-1.png) 中的 `positions``education``contact_info`)的關係技術可能導致繁瑣的模式和不必要的複雜的應用程式程式碼。
文件模型有一定的侷限性:例如,不能直接引用文件中的巢狀的專案,而是需要說 “使用者 251 的位置列表中的第二項”(很像層次模型中的訪問路徑)。但是,只要檔案巢狀不太深,這通常不是問題。
@@ -274,13 +276,13 @@ UPDATE users SET first_name = substring_index(name, ' ', 1); -- MySQL
#### 查詢的資料區域性
文件通常以單個連續字串形式進行儲存,編碼為 JSON、XML 或其二進位制變體(如 MongoDB 的 BSON。如果應用程式經常需要訪問整個文件例如將其渲染至網頁那麼儲存區域性會帶來效能優勢。如果將資料分割到多個表中如 [圖 2-1](../img/fig2-1.png) 所示),則需要進行多次索引查詢才能將其全部檢索出來,這可能需要更多的磁碟查詢並花費更多的時間。
文件通常以單個連續字串形式進行儲存,編碼為 JSON、XML 或其二進位制變體(如 MongoDB 的 BSON。如果應用程式經常需要訪問整個文件例如將其渲染至網頁那麼儲存區域性會帶來效能優勢。如果將資料分割到多個表中如 [圖 2-1](/img/fig2-1.png) 所示),則需要進行多次索引查詢才能將其全部檢索出來,這可能需要更多的磁碟查詢並花費更多的時間。
區域性僅僅適用於同時需要文件絕大部分內容的情況。即使只訪問文件其中的一小部分資料庫通常需要載入整個文件對於大型文件來說這種載入行為是很浪費的。更新文件時通常需要整個重寫。只有不改變文件大小的修改才可以容易地原地執行。因此通常建議保持相對小的文件並避免增加文件大小的寫入【9】。這些效能限制大大減少了文件資料庫的實用場景。
值得指出的是為了區域性而分組集合相關資料的想法並不侷限於文件模型。例如Google 的 Spanner 資料庫在關係資料模型中提供了同樣的區域性屬性允許模式宣告一個表的行應該交錯巢狀在父表內【27】。Oracle 類似地允許使用一個稱為 **多表索引叢集表multi-table index cluster tables** 的類似特性【28】。Bigtable 資料模型(用於 Cassandra 和 HBase中的 **列族column-family** 概念與管理區域性的目的類似【29】。
在 [第三章](ch3.md) 將還會看到更多關於區域性的內容。
在 [第三章](/tw/ch3) 將還會看到更多關於區域性的內容。
#### 文件和關係資料庫的融合
@@ -420,7 +422,7 @@ for (var i = 0; i < liElements.length; i++) {
MapReduce 是一個由 Google 推廣的程式設計模型用於在多臺機器上批次處理大規模的資料【33】。一些 NoSQL 資料儲存(包括 MongoDB 和 CouchDB支援有限形式的 MapReduce作為在多個文件中執行只讀查詢的機制。
關於 MapReduce 更詳細的介紹在 [第十章](ch10.md)。現在我們只簡要討論一下 MongoDB 使用的模型。
關於 MapReduce 更詳細的介紹在 [第十章](/tw/ch10)。現在我們只簡要討論一下 MongoDB 使用的模型。
MapReduce 既不是一個宣告式的查詢語言,也不是一個完全命令式的查詢 API而是處於兩者之間查詢的邏輯用程式碼片段來表示這些程式碼片段會被處理框架重複性呼叫。它基於 `map`(也稱為 `collect`)和 `reduce`(也稱為 `fold``inject`)函式,兩個函式存在於許多函數語言程式設計語言中。
@@ -488,7 +490,7 @@ db.observations.mapReduce(function map() {
map 和 reduce 函式在功能上有所限制:它們必須是 **純** 函式這意味著它們只使用傳遞給它們的資料作為輸入它們不能執行額外的資料庫查詢也不能有任何副作用。這些限制允許資料庫以任何順序執行任何功能並在失敗時重新執行它們。然而map 和 reduce 函式仍然是強大的:它們可以解析字串、呼叫庫函式、執行計算等等。
MapReduce 是一個相當底層的程式設計模型,用於計算機叢集上的分散式執行。像 SQL 這樣的更高階的查詢語言可以用一系列的 MapReduce 操作來實現(見 [第十章](ch10.md)),但是也有很多不使用 MapReduce 的分散式 SQL 實現。須注意SQL 並沒有限制它只能在單一機器上執行,而 MapReduce 也並沒有壟斷所有的分散式查詢執行。
MapReduce 是一個相當底層的程式設計模型,用於計算機叢集上的分散式執行。像 SQL 這樣的更高階的查詢語言可以用一系列的 MapReduce 操作來實現(見 [第十章](/tw/ch10)),但是也有很多不使用 MapReduce 的分散式 SQL 實現。須注意SQL 並沒有限制它只能在單一機器上執行,而 MapReduce 也並沒有壟斷所有的分散式查詢執行。
能夠在查詢中使用 JavaScript 程式碼是高階查詢的一個重要特性,但這不限於 MapReduce一些 SQL 資料庫也可以用 JavaScript 函式進行擴充套件【34】。
@@ -533,13 +535,13 @@ db.observations.aggregate([
在剛剛給出的例子中圖中的所有頂點代表了相同型別的事物人、網頁或交叉路口。不過圖並不侷限於這樣的同類資料同樣強大地是圖提供了一種一致的方式用來在單個數據儲存中儲存完全不同型別的物件。例如Facebook 維護一個包含許多不同型別的頂點和邊的單個圖頂點表示人、地點、事件、簽到和使用者的評論邊表示哪些人是好友、簽到發生在哪裡、誰評論了什麼帖子、誰參與了什麼事件等等【35】。
在本節中,我們將使用 [圖 2-5](../img/fig2-5.png) 所示的示例。它可以從社交網路或系譜資料庫中獲得:它顯示了兩個人,來自愛達荷州的 Lucy 和來自法國 Beaune 的 Alain。他們已婚住在倫敦。
在本節中,我們將使用 [圖 2-5](/img/fig2-5.png) 所示的示例。它可以從社交網路或系譜資料庫中獲得:它顯示了兩個人,來自愛達荷州的 Lucy 和來自法國 Beaune 的 Alain。他們已婚住在倫敦。
![](../img/fig2-5.png)
![](/img/fig2-5.png)
**圖 2-5 圖資料結構示例(框代表頂點,箭頭代表邊)**
有幾種不同但相關的方法用來構建和查詢圖表中的資料。在本節中,我們將討論屬性圖模型(由 Neo4jTitan 和 InfiniteGraph 實現和三元組儲存triple-store模型由 Datomic、AllegroGraph 等實現。我們將檢視圖的三種宣告式查詢語言CypherSPARQL 和 Datalog。除此之外還有像 Gremlin 【36】這樣的圖形查詢語言和像 Pregel 這樣的圖形處理框架(見 [第十章](ch10.md))。
有幾種不同但相關的方法用來構建和查詢圖表中的資料。在本節中,我們將討論屬性圖模型(由 Neo4jTitan 和 InfiniteGraph 實現和三元組儲存triple-store模型由 Datomic、AllegroGraph 等實現。我們將檢視圖的三種宣告式查詢語言CypherSPARQL 和 Datalog。除此之外還有像 Gremlin 【36】這樣的圖形查詢語言和像 Pregel 這樣的圖形處理框架(見 [第十章](/tw/ch10))。
### 屬性圖
@@ -586,7 +588,7 @@ CREATE INDEX edges_heads ON edges (head_vertex);
2. 給定任何頂點,可以高效地找到它的入邊和出邊,從而遍歷圖,即沿著一系列頂點的路徑前後移動(這就是為什麼 [例 2-2]() 在 `tail_vertex``head_vertex` 列上都有索引的原因)。
3. 透過對不同型別的關係使用不同的標籤,可以在一個圖中儲存幾種不同的資訊,同時仍然保持一個清晰的資料模型。
這些特性為資料建模提供了很大的靈活性,如 [圖 2-5](../img/fig2-5.png) 所示。圖中顯示了一些傳統關係模式難以表達的事情例如不同國家的不同地區結構法國有省和大區美國有縣和州國中國的怪事先忽略主權國家和民族錯綜複雜的爛攤子不同的資料粒度Lucy 現在的住所記錄具體到城市,而她的出生地點只是在一個州的級別)。
這些特性為資料建模提供了很大的靈活性,如 [圖 2-5](/img/fig2-5.png) 所示。圖中顯示了一些傳統關係模式難以表達的事情例如不同國家的不同地區結構法國有省和大區美國有縣和州國中國的怪事先忽略主權國家和民族錯綜複雜的爛攤子不同的資料粒度Lucy 現在的住所記錄具體到城市,而她的出生地點只是在一個州的級別)。
你可以想象該圖還能延伸出許多關於 Lucy 和 Alain 的事實,或其他人的其他更多的事實。例如,你可以用它來表示食物過敏(為每個過敏源增加一個頂點,並增加人與過敏源之間的一條邊來指示一種過敏情況),並連結到過敏源,每個過敏源具有一組頂點用來顯示哪些食物含有哪些物質。然後,你可以寫一個查詢,找出每個人吃什麼是安全的。圖在可演化性方面是富有優勢的:當你嚮應用程式新增功能時,可以輕鬆擴充套件圖以適應程式資料結構的變化。
@@ -594,7 +596,7 @@ CREATE INDEX edges_heads ON edges (head_vertex);
Cypher 是屬性圖的宣告式查詢語言,為 Neo4j 圖形資料庫而發明【37】它是以電影 “駭客帝國” 中的一個角色來命名的而與密碼學中的加密演算法無關【38】
[例 2-3]() 顯示了將 [圖 2-5](../img/fig2-5.png) 的左邊部分插入圖形資料庫的 Cypher 查詢。你可以以類似的方式把圖的剩餘部分新增進去,但這裡為了文章可閱讀性而省略這部分的示例。每個頂點都有一個像 `USA``Idaho` 這樣的符號名稱,查詢的其他部分可以使用這些名稱在頂點之間建立邊,使用箭頭符號:`Idaho - [WITHIN] ->USA` 建立一條標記為 `WITHIN` 的邊,`Idaho` 為尾節點,`USA` 為頭節點。
[例 2-3]() 顯示了將 [圖 2-5](/img/fig2-5.png) 的左邊部分插入圖形資料庫的 Cypher 查詢。你可以以類似的方式把圖的剩餘部分新增進去,但這裡為了文章可閱讀性而省略這部分的示例。每個頂點都有一個像 `USA``Idaho` 這樣的符號名稱,查詢的其他部分可以使用這些名稱在頂點之間建立邊,使用箭頭符號:`Idaho - [WITHIN] ->USA` 建立一條標記為 `WITHIN` 的邊,`Idaho` 為尾節點,`USA` 為頭節點。
**例 2-3 將圖 2-5 中的資料子集表示為 Cypher 查詢**
@@ -608,7 +610,7 @@ CREATE
(Lucy) -[:BORN_IN]-> (Idaho)
```
當 [圖 2-5](../img/fig2-5.png) 的所有頂點和邊被新增到資料庫後,讓我們提些有趣的問題:例如,找到所有從美國移民到歐洲的人的名字。更確切地說,這裡我們想要找到符合下面條件的所有頂點,並且返回這些頂點的 `name` 屬性:該頂點擁有一條連到美國任一位置的 `BORN_IN` 邊,和一條連到歐洲的任一位置的 `LIVING_IN` 邊。
當 [圖 2-5](/img/fig2-5.png) 的所有頂點和邊被新增到資料庫後,讓我們提些有趣的問題:例如,找到所有從美國移民到歐洲的人的名字。更確切地說,這裡我們想要找到符合下面條件的所有頂點,並且返回這些頂點的 `name` 屬性:該頂點擁有一條連到美國任一位置的 `BORN_IN` 邊,和一條連到歐洲的任一位置的 `LIVING_IN` 邊。
[例 2-4]() 展示瞭如何在 Cypher 中表達這個查詢。在 MATCH 子句中使用相同的箭頭符號來查詢圖中的模式:`(person) -[:BORN_IN]-> ()` 可以匹配 `BORN_IN` 邊的任意兩個頂點。該邊的尾節點被綁定了變數 `person`,頭節點則未被繫結。
@@ -895,9 +897,9 @@ Cypher 和 SPARQL 使用 SELECT 立即跳轉,但是 Datalog 一次只進行一
2. 資料庫存在 `within (usa, namerica)`,在上一步驟中生成 `within_recursive (namerica, 'North America')`,故運用規則 2。它會產生 `within_recursive (usa, 'North America')`
3. 資料庫存在 `within (idaho, usa)`,在上一步生成 `within_recursive (usa, 'North America')`,故運用規則 2。它產生 `within_recursive (idaho, 'North America')`
透過重複應用規則 1 和 2`within_recursive` 謂語可以告訴我們在資料庫中包含北美(或任何其他位置名稱)的所有位置。這個過程如 [圖 2-6](../img/fig2-6.png) 所示。
透過重複應用規則 1 和 2`within_recursive` 謂語可以告訴我們在資料庫中包含北美(或任何其他位置名稱)的所有位置。這個過程如 [圖 2-6](/img/fig2-6.png) 所示。
![](../img/fig2-6.png)
![](/img/fig2-6.png)
**圖 2-6 使用示例 2-11 中的 Datalog 規則來確定愛達荷州在北美。**
@@ -927,7 +929,7 @@ Cypher 和 SPARQL 使用 SELECT 立即跳轉,但是 Datalog 一次只進行一
* 粒子物理學家數十年來一直在進行大資料型別的大規模資料分析像大型強子對撞機LHC這樣的專案現在會處理數百 PB 的資料在這樣的規模下需要定製解決方案來阻止硬體成本的失控【49】。
* **全文搜尋** 可以說是一種經常與資料庫一起使用的資料模型。資訊檢索是一個很大的專業課題,我們不會在本書中詳細介紹,但是我們將在第三章和第三部分中介紹搜尋索引。
讓我們暫時將其放在一邊。在 [下一章](ch3.md) 中,我們將討論在 **實現** 本章描述的資料模型時會遇到的一些權衡。
讓我們暫時將其放在一邊。在 [下一章](/tw/ch3) 中,我們將討論在 **實現** 本章描述的資料模型時會遇到的一些權衡。
## 參考文獻
@@ -980,10 +982,4 @@ Cypher 和 SPARQL 使用 SELECT 立即跳轉,但是 Datalog 一次只進行一
1. Serge Abiteboul, Richard Hull, and Victor Vianu: [*Foundations of Databases*](http://webdam.inria.fr/Alice/). Addison-Wesley, 1995. ISBN: 978-0-201-53771-0, available online at *webdam.inria.fr/Alice*
1. Nathan Marz: “[Cascalog](https://github.com/nathanmarz/cascalog)," *github.com*.
1. Dennis A. Benson, Ilene Karsch-Mizrachi, David J. Lipman, et al.: “[GenBank](https://academic.oup.com/nar/article/36/suppl_1/D25/2507746),” *Nucleic Acids Research*, volume 36, Database issue, pages D25D30, December 2007. [doi:10.1093/nar/gkm929](http://dx.doi.org/10.1093/nar/gkm929)
1. Fons Rademakers: “[ROOT for Big Data Analysis](https://indico.cern.ch/event/246453/contributions/1566610/attachments/423154/587535/ROOT-BigData-Analysis-London-2013.pdf),” at *Workshop on the Future of Big Data Management*, London, UK, June 2013.
------
| 上一章 | 目錄 | 下一章 |
| -------------------------------------------- | ------------------------------- | ---------------------------- |
| [第一章:可靠性、可伸縮性和可維護性](ch1.md) | [設計資料密集型應用](README.md) | [第三章:儲存與檢索](ch3.md) |
1. Fons Rademakers: “[ROOT for Big Data Analysis](https://indico.cern.ch/event/246453/contributions/1566610/attachments/423154/587535/ROOT-BigData-Analysis-London-2013.pdf),” at *Workshop on the Future of Big Data Management*, London, UK, June 2013.

View File

@@ -1,19 +1,20 @@
# 第三章:儲存與檢索
---
title: "第三章:儲存與檢索"
linkTitle: "3. 儲存與檢索"
weight: 103
breadcrumbs: false
---
![](../img/ch3.png)
![](/img/ch3.png)
> 建立秩序,省卻搜尋
>
> —— 德國諺語
>
-------------------
[TOC]
一個數據庫在最基礎的層次上需要完成兩件事情:當你把資料交給資料庫時,它應當把資料儲存起來;而後當你向資料庫要資料時,它應當把資料返回給你。
在 [第二章](ch2.md) 中,我們討論了資料模型和查詢語言,即程式設計師將資料錄入資料庫的格式,以及再次要回資料的機制。在本章中我們會從資料庫的視角來討論同樣的問題:資料庫如何儲存我們提供的資料,以及如何在我們需要時重新找到資料。
在 [第二章](/tw/ch2) 中,我們討論了資料模型和查詢語言,即程式設計師將資料錄入資料庫的格式,以及再次要回資料的機制。在本章中我們會從資料庫的視角來討論同樣的問題:資料庫如何儲存我們提供的資料,以及如何在我們需要時重新找到資料。
作為程式設計師,為什麼要關心資料庫內部儲存與檢索的機理?你可能不會去從頭開始實現自己的儲存引擎,但是你 **確實** 需要從許多可用的儲存引擎中選擇一個合適的。而且為了讓儲存引擎能在你的工作負載型別上執行良好,你也需要大致瞭解儲存引擎在底層究竟做了什麼。
@@ -82,9 +83,9 @@ $ cat database
鍵值儲存與在大多數程式語言中可以找到的 **字典dictionary** 型別非常相似,通常字典都是用 **雜湊對映hash map****散列表hash table** 實現的。雜湊對映在許多演算法教科書中都有描述【1,2】所以這裡我們不會討論它的工作細節。既然我們已經可以用雜湊對映來表示 **記憶體中** 的資料結構,為什麼不使用它來索引 **硬碟上** 的資料呢?
假設我們的資料儲存只是一個追加寫入的檔案,就像前面的例子一樣,那麼最簡單的索引策略就是:保留一個記憶體中的雜湊對映,其中每個鍵都對映到資料檔案中的一個位元組偏移量,指明瞭可以找到對應值的位置,如 [圖 3-1](../img/fig3-1.png) 所示。當你將新的鍵值對追加寫入檔案中時,還要更新雜湊對映,以反映剛剛寫入的資料的偏移量(這同時適用於插入新鍵與更新現有鍵)。當你想查詢一個值時,使用雜湊對映來查詢資料檔案中的偏移量,**尋找seek** 該位置並讀取該值即可。
假設我們的資料儲存只是一個追加寫入的檔案,就像前面的例子一樣,那麼最簡單的索引策略就是:保留一個記憶體中的雜湊對映,其中每個鍵都對映到資料檔案中的一個位元組偏移量,指明瞭可以找到對應值的位置,如 [圖 3-1](/img/fig3-1.png) 所示。當你將新的鍵值對追加寫入檔案中時,還要更新雜湊對映,以反映剛剛寫入的資料的偏移量(這同時適用於插入新鍵與更新現有鍵)。當你想查詢一個值時,使用雜湊對映來查詢資料檔案中的偏移量,**尋找seek** 該位置並讀取該值即可。
![](../img/fig3-1.png)
![](/img/fig3-1.png)
**圖 3-1 以類 CSV 格式儲存鍵值對的日誌,並使用記憶體雜湊對映進行索引。**
@@ -92,15 +93,15 @@ $ cat database
像 Bitcask 這樣的儲存引擎非常適合每個鍵的值經常更新的情況。例如鍵可能是某個貓咪影片的網址URL而值可能是該影片被播放的次數每次有人點選播放按鈕時遞增。在這種型別的工作負載中有很多寫操作但是沒有太多不同的鍵 —— 每個鍵有很多的寫操作,但是將所有鍵儲存在記憶體中是可行的。
到目前為止,我們只是在追加寫入一個檔案 —— 所以如何避免最終用完硬碟空間?一種好的解決方案是,將日誌分為特定大小的 **段segment**,當日誌增長到特定尺寸時關閉當前段檔案,並開始寫入一個新的段檔案。然後,我們就可以對這些段進行 **壓縮compaction**,如 [圖 3-2](../img/fig3-2.png) 所示。這裡的壓縮意味著在日誌中丟棄重複的鍵,只保留每個鍵的最近更新。
到目前為止,我們只是在追加寫入一個檔案 —— 所以如何避免最終用完硬碟空間?一種好的解決方案是,將日誌分為特定大小的 **段segment**,當日誌增長到特定尺寸時關閉當前段檔案,並開始寫入一個新的段檔案。然後,我們就可以對這些段進行 **壓縮compaction**,如 [圖 3-2](/img/fig3-2.png) 所示。這裡的壓縮意味著在日誌中丟棄重複的鍵,只保留每個鍵的最近更新。
![](../img/fig3-2.png)
![](/img/fig3-2.png)
**圖 3-2 鍵值更新日誌(統計貓咪影片的播放次數)的壓縮,只保留每個鍵的最近值**
而且,由於壓縮經常會使得段變得很小(假設在一個段內鍵被平均重寫了好幾次),我們也可以在執行壓縮的同時將多個段合併在一起,如 [圖 3-3](../img/fig3-3.png) 所示。段被寫入後永遠不會被修改,所以合併的段被寫入一個新的檔案。凍結段的合併和壓縮可以在後臺執行緒中完成,這個過程進行的同時,我們仍然可以繼續使用舊的段檔案來正常提供讀寫請求。合併過程完成後,我們將讀取請求轉換為使用新合併的段而不是舊的段 —— 然後舊的段檔案就可以簡單地刪除掉了。
而且,由於壓縮經常會使得段變得很小(假設在一個段內鍵被平均重寫了好幾次),我們也可以在執行壓縮的同時將多個段合併在一起,如 [圖 3-3](/img/fig3-3.png) 所示。段被寫入後永遠不會被修改,所以合併的段被寫入一個新的檔案。凍結段的合併和壓縮可以在後臺執行緒中完成,這個過程進行的同時,我們仍然可以繼續使用舊的段檔案來正常提供讀寫請求。合併過程完成後,我們將讀取請求轉換為使用新合併的段而不是舊的段 —— 然後舊的段檔案就可以簡單地刪除掉了。
![](../img/fig3-3.png)
![](/img/fig3-3.png)
**圖 3-3 同時執行壓縮和分段合併**
@@ -130,7 +131,7 @@ $ cat database
乍一看,僅追加日誌似乎很浪費:為什麼不直接在檔案裡更新,用新值覆蓋舊值?僅追加的設計之所以是個好的設計,有如下幾個原因:
* 追加和分段合併都是順序寫入操作,通常比隨機寫入快得多,尤其是在磁性機械硬碟上。在某種程度上,順序寫入在基於快閃記憶體的 **固態硬碟SSD** 上也是好的選擇【4】。我們將在“[比較 B 樹和 LSM 樹](#比較B樹和LSM)”中進一步討論這個問題。
* 追加和分段合併都是順序寫入操作,通常比隨機寫入快得多,尤其是在磁性機械硬碟上。在某種程度上,順序寫入在基於快閃記憶體的 **固態硬碟SSD** 上也是好的選擇【4】。我們將在“[比較 B 樹和 LSM 樹](#比較b樹和lsm)”中進一步討論這個問題。
* 如果段檔案是僅追加的或不可變的,併發和崩潰恢復就簡單多了。例如,當一個數據值被更新的時候發生崩潰,你不用擔心檔案裡將會同時包含舊值和新值各自的一部分。
* 合併舊段的處理也可以避免資料檔案隨著時間的推移而碎片化的問題。
@@ -144,23 +145,23 @@ $ cat database
### SSTables和LSM樹
在 [圖 3-3](../img/fig3-3.png) 中,每個日誌結構儲存段都是一系列鍵值對。這些鍵值對按照它們寫入的順序排列,日誌中稍後的值優先於日誌中較早的相同鍵的值。除此之外,檔案中鍵值對的順序並不重要。
在 [圖 3-3](/img/fig3-3.png) 中,每個日誌結構儲存段都是一系列鍵值對。這些鍵值對按照它們寫入的順序排列,日誌中稍後的值優先於日誌中較早的相同鍵的值。除此之外,檔案中鍵值對的順序並不重要。
現在我們可以對段檔案的格式做一個簡單的改變:要求鍵值對的序列按鍵排序。乍一看,這個要求似乎打破了我們使用順序寫入的能力,我們將稍後再回到這個問題。
我們把這個格式稱為 **排序字串表Sorted String Table**,簡稱 SSTable。我們還要求每個鍵只在每個合併的段檔案中出現一次壓縮過程已經保證。與使用雜湊索引的日誌段相比SSTable 有幾個大的優勢:
1. 即使檔案大於可用記憶體,合併段的操作仍然是簡單而高效的。這種方法就像歸併排序演算法中使用的方法一樣,如 [圖 3-4](../img/fig3-4.png) 所示:你開始並排讀取多個輸入檔案,檢視每個檔案中的第一個鍵,複製最低的鍵(根據排序順序)到輸出檔案,不斷重複此步驟,將產生一個新的合併段檔案,而且它也是也按鍵排序的。
1. 即使檔案大於可用記憶體,合併段的操作仍然是簡單而高效的。這種方法就像歸併排序演算法中使用的方法一樣,如 [圖 3-4](/img/fig3-4.png) 所示:你開始並排讀取多個輸入檔案,檢視每個檔案中的第一個鍵,複製最低的鍵(根據排序順序)到輸出檔案,不斷重複此步驟,將產生一個新的合併段檔案,而且它也是也按鍵排序的。
![](../img/fig3-4.png)
![](/img/fig3-4.png)
**圖 3-4 合併幾個 SSTable 段,只保留每個鍵的最新值**
如果在幾個輸入段中出現相同的鍵,該怎麼辦?請記住,每個段都包含在一段時間內寫入資料庫的所有值。這意味著一個輸入段中的所有值一定比另一個段中的所有值都更近(假設我們總是合併相鄰的段)。當多個段包含相同的鍵時,我們可以保留最近段的值,並丟棄舊段中的值。
2. 為了在檔案中找到一個特定的鍵,你不再需要在記憶體中儲存所有鍵的索引。以 [圖 3-5](../img/fig3-5.png) 為例:假設你正在記憶體中尋找鍵 `handiwork`,但是你不知道這個鍵在段檔案中的確切偏移量。然而,你知道 `handbag``handsome` 的偏移,而且由於排序特性,你知道 `handiwork` 必須出現在這兩者之間。這意味著你可以跳到 `handbag` 的偏移位置並從那裡掃描,直到你找到 `handiwork`(或沒找到,如果該檔案中沒有該鍵)。
2. 為了在檔案中找到一個特定的鍵,你不再需要在記憶體中儲存所有鍵的索引。以 [圖 3-5](/img/fig3-5.png) 為例:假設你正在記憶體中尋找鍵 `handiwork`,但是你不知道這個鍵在段檔案中的確切偏移量。然而,你知道 `handbag``handsome` 的偏移,而且由於排序特性,你知道 `handiwork` 必須出現在這兩者之間。這意味著你可以跳到 `handbag` 的偏移位置並從那裡掃描,直到你找到 `handiwork`(或沒找到,如果該檔案中沒有該鍵)。
![](../img/fig3-5.png)
![](/img/fig3-5.png)
**圖 3-5 具有記憶體索引的 SSTable**
@@ -168,7 +169,7 @@ $ cat database
[^i]: 如果所有的鍵與值都是定長的,你可以使用段檔案上的二分查詢並完全避免使用記憶體索引。然而實踐中的鍵和值通常都是變長的,因此如果沒有索引,就很難知道記錄的分界點(前一條記錄結束以及後一條記錄開始的地方)。
3. 由於讀取請求無論如何都需要掃描所請求範圍內的多個鍵值對因此可以將這些記錄分組為塊block並在將其寫入硬碟之前對其進行壓縮如 [圖 3-5](../img/fig3-5.png) 中的陰影區域所示)[^譯註i] 。稀疏記憶體索引中的每個條目都指向壓縮塊的開始處。除了節省硬碟空間之外,壓縮還可以減少對 I/O 頻寬的使用。
3. 由於讀取請求無論如何都需要掃描所請求範圍內的多個鍵值對因此可以將這些記錄分組為塊block並在將其寫入硬碟之前對其進行壓縮如 [圖 3-5](/img/fig3-5.png) 中的陰影區域所示)[^譯註i] 。稀疏記憶體索引中的每個條目都指向壓縮塊的開始處。除了節省硬碟空間之外,壓縮還可以減少對 I/O 頻寬的使用。
[^譯註i]: 這裡的壓縮是 compression不是前文的 compaction請注意區分。
@@ -214,23 +215,23 @@ Lucene是一種全文搜尋的索引引擎在 Elasticsearch 和 Solr 被
我們前面看到的日誌結構索引將資料庫分解為可變大小的段通常是幾兆位元組或更大的大小並且總是按順序寫入段。相比之下B 樹將資料庫分解成固定大小的 **塊block****分頁page**,傳統上大小為 4KB有時會更大並且一次只能讀取或寫入一個頁面。這種設計更接近於底層硬體因為硬碟空間也是按固定大小的塊來組織的。
每個頁面都可以使用地址或位置來標識,這允許一個頁面引用另一個頁面 —— 類似於指標,但在硬碟而不是在記憶體中。我們可以使用這些頁面引用來構建一個頁面樹,如 [圖 3-6](../img/fig3-6.png) 所示。
每個頁面都可以使用地址或位置來標識,這允許一個頁面引用另一個頁面 —— 類似於指標,但在硬碟而不是在記憶體中。我們可以使用這些頁面引用來構建一個頁面樹,如 [圖 3-6](/img/fig3-6.png) 所示。
![](../img/fig3-6.png)
![](/img/fig3-6.png)
**圖 3-6 使用 B 樹索引查詢一個鍵**
一個頁面會被指定為 B 樹的根;在索引中查詢一個鍵時,就從這裡開始。該頁面包含幾個鍵和對子頁面的引用。每個子頁面負責一段連續範圍的鍵,根頁面上每兩個引用之間的鍵,表示相鄰子頁面管理的鍵的範圍(邊界)。
在 [圖 3-6](../img/fig3-6.png) 的例子中,我們正在尋找鍵 251 ,所以我們知道我們需要跟蹤邊界 200 和 300 之間的頁面引用。這將我們帶到一個類似的頁面,進一步將 200 到 300 的範圍拆分到子範圍。
在 [圖 3-6](/img/fig3-6.png) 的例子中,我們正在尋找鍵 251 ,所以我們知道我們需要跟蹤邊界 200 和 300 之間的頁面引用。這將我們帶到一個類似的頁面,進一步將 200 到 300 的範圍拆分到子範圍。
最終我們將到達某個包含單個鍵的頁面葉子頁面leaf page該頁面或者直接包含每個鍵的值或者包含了對可以找到值的頁面的引用。
在 B 樹的一個頁面中對子頁面的引用的數量稱為 **分支因子branching factor**。例如,在 [圖 3-6](../img/fig3-6.png) 中,分支因子是 6。在實踐中分支因子的大小取決於儲存頁面引用和範圍邊界所需的空間但這個值通常是幾百。
在 B 樹的一個頁面中對子頁面的引用的數量稱為 **分支因子branching factor**。例如,在 [圖 3-6](/img/fig3-6.png) 中,分支因子是 6。在實踐中分支因子的大小取決於儲存頁面引用和範圍邊界所需的空間但這個值通常是幾百。
如果要更新 B 樹中現有鍵的值,需要搜尋包含該鍵的葉子頁面,更改該頁面中的值,並將該頁面寫回到硬碟(對該頁面的任何引用都將保持有效)。如果你想新增一個新的鍵,你需要找到其範圍能包含新鍵的頁面,並將其新增到該頁面。如果頁面中沒有足夠的可用空間容納新鍵,則將其分成兩個半滿頁面,並更新父頁面以反映新的鍵範圍分割槽,如 [圖 3-7](../img/fig3-7.png) 所示 [^ii]。
如果要更新 B 樹中現有鍵的值,需要搜尋包含該鍵的葉子頁面,更改該頁面中的值,並將該頁面寫回到硬碟(對該頁面的任何引用都將保持有效)。如果你想新增一個新的鍵,你需要找到其範圍能包含新鍵的頁面,並將其新增到該頁面。如果頁面中沒有足夠的可用空間容納新鍵,則將其分成兩個半滿頁面,並更新父頁面以反映新的鍵範圍分割槽,如 [圖 3-7](/img/fig3-7.png) 所示 [^ii]。
![](../img/fig3-7.png)
![](/img/fig3-7.png)
**圖 3-7 透過分割頁面來生長 B 樹**
@@ -254,7 +255,7 @@ B 樹的基本底層寫操作是用新資料覆寫硬碟上的頁面,並假定
由於 B 樹已經存在了很久,所以並不奇怪這麼多年下來有很多最佳化的設計被開發出來,僅舉幾例:
* 不同於覆寫頁面並維護 WAL 以支援崩潰恢復,一些資料庫(如 LMDB使用寫時複製方案【21】。經過修改的頁面被寫入到不同的位置並且還在樹中建立了父頁面的新版本以指向新的位置。這種方法對於併發控制也很有用我們將在 “[快照隔離和可重複讀](ch7.md#快照隔離和可重複讀)” 中看到。
* 不同於覆寫頁面並維護 WAL 以支援崩潰恢復,一些資料庫(如 LMDB使用寫時複製方案【21】。經過修改的頁面被寫入到不同的位置並且還在樹中建立了父頁面的新版本以指向新的位置。這種方法對於併發控制也很有用我們將在 “[快照隔離和可重複讀](/tw/ch7#快照隔離和可重複讀)” 中看到。
* 我們可以透過不儲存整個鍵,而是縮短其大小,來節省頁面空間。特別是在樹內部的頁面上,鍵只需要提供足夠的資訊來充當鍵範圍之間的邊界。在頁面中包含更多的鍵允許樹具有更高的分支因子,因此也就允許更少的層級 [^iii]。
* 通常,頁面可以放置在硬碟上的任何位置;沒有什麼要求相鄰鍵範圍的頁面也放在硬碟上相鄰的區域。如果某個查詢需要按照排序順序掃描大部分的鍵範圍,那麼這種按頁面儲存的佈局可能會效率低下,因為每個頁面的讀取都需要執行一次硬碟查詢。因此,許多 B 樹的實現在佈局樹時會盡量使葉子頁面按順序出現在硬碟上。但是,隨著樹的增長,要維持這個順序是很困難的。相比之下,由於 LSM 樹在合併過程中一次性重寫一大段儲存,所以它們更容易使順序鍵在硬碟上連續儲存。
* 額外的指標被新增到樹中。例如,每個葉子頁面可以引用其左邊和右邊的兄弟頁面,使得不用跳回父頁面就能按順序對鍵進行掃描。
@@ -284,13 +285,13 @@ LSM 樹可以被壓縮得更好,因此通常能比 B 樹在硬碟上產生更
#### LSM樹的缺點
日誌結構儲存的缺點是壓縮過程有時會干擾正在進行的讀寫操作。儘管儲存引擎嘗試增量地執行壓縮以儘量不影響併發訪問,但是硬碟資源有限,所以很容易發生某個請求需要等待硬碟先完成昂貴的壓縮操作。對吞吐量和平均響應時間的影響通常很小,但是日誌結構化儲存引擎在更高百分位的響應時間(請參閱 “[描述效能](ch1.md#描述效能)”)有時會相當長,而 B 樹的行為則相對更具有可預測性【28】。
日誌結構儲存的缺點是壓縮過程有時會干擾正在進行的讀寫操作。儘管儲存引擎嘗試增量地執行壓縮以儘量不影響併發訪問,但是硬碟資源有限,所以很容易發生某個請求需要等待硬碟先完成昂貴的壓縮操作。對吞吐量和平均響應時間的影響通常很小,但是日誌結構化儲存引擎在更高百分位的響應時間(請參閱 “[描述效能](/tw/ch1#描述效能)”)有時會相當長,而 B 樹的行為則相對更具有可預測性【28】。
壓縮的另一個問題出現在高寫入吞吐量時:硬碟的有限寫入頻寬需要在初始寫入(記錄日誌和重新整理記憶體表到硬碟)和在後臺執行的壓縮執行緒之間共享。寫入空資料庫時,可以使用全硬碟頻寬進行初始寫入,但資料庫越大,壓縮所需的硬碟頻寬就越多。
如果寫入吞吐量很高,並且壓縮沒有仔細配置好,有可能導致壓縮跟不上寫入速率。在這種情況下,硬碟上未合併段的數量不斷增加,直到硬碟空間用完,讀取速度也會減慢,因為它們需要檢查更多的段檔案。通常情況下,即使壓縮無法跟上,基於 SSTable 的儲存引擎也不會限制傳入寫入的速率所以你需要進行明確的監控來檢測這種情況【29,30】。
B 樹的一個優點是每個鍵只存在於索引中的一個位置,而日誌結構化的儲存引擎可能在不同的段中有相同鍵的多個副本。這個方面使得 B 樹在想要提供強大的事務語義的資料庫中很有吸引力:在許多關係資料庫中,事務隔離是透過在鍵範圍上使用鎖來實現的,在 B 樹索引中這些鎖可以直接附加到樹上【5】。在 [第七章](ch7.md) 中,我們將更詳細地討論這一點。
B 樹的一個優點是每個鍵只存在於索引中的一個位置,而日誌結構化的儲存引擎可能在不同的段中有相同鍵的多個副本。這個方面使得 B 樹在想要提供強大的事務語義的資料庫中很有吸引力:在許多關係資料庫中,事務隔離是透過在鍵範圍上使用鎖來實現的,在 B 樹索引中這些鎖可以直接附加到樹上【5】。在 [第七章](/tw/ch7) 中,我們將更詳細地討論這一點。
B 樹在資料庫架構中是非常根深蒂固的,為許多工作負載都提供了始終如一的良好效能,所以它們不可能在短期內消失。在新的資料庫中,日誌結構化索引變得越來越流行。沒有簡單易行的辦法來判斷哪種型別的儲存引擎對你的使用場景更好,所以需要透過一些測試來得到相關經驗。
@@ -298,7 +299,7 @@ B 樹在資料庫架構中是非常根深蒂固的,為許多工作負載都提
到目前為止,我們只討論了鍵值索引,它們就像關係模型中的 **主鍵primary key** 索引。主鍵唯一標識關係表中的一行,或文件資料庫中的一個文件或圖形資料庫中的一個頂點。資料庫中的其他記錄可以透過其主鍵(或 ID引用該行 / 文件 / 頂點,索引就被用於解析這樣的引用。
次級索引secondary indexes也很常見。在關係資料庫中你可以使用 `CREATE INDEX` 命令在同一個表上建立多個次級索引而且這些索引通常對於有效地執行聯接join而言至關重要。例如在 [第二章](ch2.md) 中的 [圖 2-1](../img/fig2-1.png) 中,很可能在 `user_id` 列上有一個次級索引,以便你可以在每個表中找到屬於同一使用者的所有行。
次級索引secondary indexes也很常見。在關係資料庫中你可以使用 `CREATE INDEX` 命令在同一個表上建立多個次級索引而且這些索引通常對於有效地執行聯接join而言至關重要。例如在 [第二章](/tw/ch2) 中的 [圖 2-1](/img/fig2-1.png) 中,很可能在 `user_id` 列上有一個次級索引,以便你可以在每個表中找到屬於同一使用者的所有行。
次級索引可以很容易地從鍵值索引構建。次級索引主要的不同是鍵不是唯一的即可能有許多行文件頂點具有相同的鍵。這可以透過兩種方式來解決將匹配行識別符號的列表作為索引裡的值就像全文索引中的記錄列表或者透過向每個鍵新增行識別符號來使鍵唯一。無論哪種方式B 樹和日誌結構索引都可以用作次級索引。
@@ -368,7 +369,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
在早期的業務資料處理過程中,一次典型的資料庫寫入通常與一筆 *商業交易commercial transaction* 相對應:賣個貨、向供應商下訂單、支付員工工資等等。但隨著資料庫開始應用到那些不涉及到錢的領域,術語 **交易 / 事務transaction** 仍留了下來,用於指代一組讀寫操作構成的邏輯單元。
> 事務不一定具有 ACID原子性一致性隔離性和永續性屬性。事務處理只是意味著允許客戶端進行低延遲的讀取和寫入 —— 而不是隻能定期執行(例如每天一次)的批處理作業。我們在 [第七章](ch7.md) 中討論 ACID 屬性,在 [第十章](ch10.md) 中討論批處理。
> 事務不一定具有 ACID原子性一致性隔離性和永續性屬性。事務處理只是意味著允許客戶端進行低延遲的讀取和寫入 —— 而不是隻能定期執行(例如每天一次)的批處理作業。我們在 [第七章](/tw/ch7) 中討論 ACID 屬性,在 [第十章](/tw/ch10) 中討論批處理。
即使資料庫開始被用於許多不同型別的資料,比如部落格文章的評論、遊戲中的動作、地址簿中的聯絡人等等,基本的訪問模式仍然類似於處理商業交易。應用程式通常使用索引透過某個鍵找少量記錄。根據使用者的輸入來插入或更新記錄。由於這些應用程式是互動式的,這種訪問模式被稱為 **線上事務處理OLTP, OnLine Transaction Processing**
@@ -400,9 +401,9 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
這些 OLTP 系統往往對業務運作至關重要,因而通常會要求 **高可用****低延遲**。所以 DBA 會密切關注他們的 OLTP 資料庫,他們通常不願意讓業務分析人員在 OLTP 資料庫上執行臨時的分析查詢,因為這些查詢通常開銷巨大,會掃描大部分資料集,這會損害同時在執行的事務的效能。
相比之下,資料倉庫是一個獨立的資料庫,分析人員可以查詢他們想要的內容而不影響 OLTP 操作【48】。資料倉庫包含公司各種 OLTP 系統中所有的只讀資料副本。從 OLTP 資料庫中提取資料(使用定期的資料轉儲或連續的更新流),轉換成適合分析的模式,清理並載入到資料倉庫中。將資料存入倉庫的過程稱為 “**抽取 - 轉換 - 載入ETL**”,如 [圖 3-8](../img/fig3-8.png) 所示。
相比之下,資料倉庫是一個獨立的資料庫,分析人員可以查詢他們想要的內容而不影響 OLTP 操作【48】。資料倉庫包含公司各種 OLTP 系統中所有的只讀資料副本。從 OLTP 資料庫中提取資料(使用定期的資料轉儲或連續的更新流),轉換成適合分析的模式,清理並載入到資料倉庫中。將資料存入倉庫的過程稱為 “**抽取 - 轉換 - 載入ETL**”,如 [圖 3-8](/img/fig3-8.png) 所示。
![](../img/fig3-8.png)
![](/img/fig3-8.png)
**圖 3-8 ETL 至資料倉庫的簡化提綱**
@@ -422,11 +423,11 @@ Teradata、Vertica、SAP HANA 和 ParAccel 等資料倉庫供應商通常使用
### 星型和雪花型:分析的模式
正如 [第二章](ch2.md) 所探討的根據應用程式的需要在事務處理領域中使用了大量不同的資料模型。另一方面在分析型業務中資料模型的多樣性則少得多。許多資料倉庫都以相當公式化的方式使用被稱為星型模式也稱為維度建模【55】
正如 [第二章](/tw/ch2) 所探討的根據應用程式的需要在事務處理領域中使用了大量不同的資料模型。另一方面在分析型業務中資料模型的多樣性則少得多。許多資料倉庫都以相當公式化的方式使用被稱為星型模式也稱為維度建模【55】
[圖 3-9](../img/fig3-9.png) 中的示例模式顯示了可能在食品零售商處找到的資料倉庫。在模式的中心是一個所謂的事實表(在這個例子中,它被稱為 `fact_sales`)。事實表的每一行代表在特定時間發生的事件(這裡,每一行代表客戶購買的產品)。如果我們分析的是網站流量而不是零售量,則每行可能代表一個使用者的頁面瀏覽或點選。
[圖 3-9](/img/fig3-9.png) 中的示例模式顯示了可能在食品零售商處找到的資料倉庫。在模式的中心是一個所謂的事實表(在這個例子中,它被稱為 `fact_sales`)。事實表的每一行代表在特定時間發生的事件(這裡,每一行代表客戶購買的產品)。如果我們分析的是網站流量而不是零售量,則每行可能代表一個使用者的頁面瀏覽或點選。
![](../img/fig3-9.png)
![](/img/fig3-9.png)
**圖 3-9 用於資料倉庫的星型模式的示例**
@@ -434,7 +435,7 @@ Teradata、Vertica、SAP HANA 和 ParAccel 等資料倉庫供應商通常使用
事實表中的一些列是屬性,例如產品銷售的價格和從供應商那裡購買的成本(可以用來計算利潤率)。事實表中的其他列是對其他表(稱為維度表)的外部索引鍵引用。由於事實表中的每一行都表示一個事件,因此這些維度代表事件發生的物件、內容、地點、時間、方式和原因。
例如,在 [圖 3-9](../img/fig3-9.png) 中,其中一個維度是已售出的產品。`dim_product` 表中的每一行代表一種待售產品包括庫存單位SKU、產品描述、品牌名稱、類別、脂肪含量、包裝尺寸等。`fact_sales` 表中的每一行都使用外部索引鍵表明在特定交易中銷售了什麼產品。(簡單起見,如果客戶一次購買了幾種不同的產品,則它們在事實表中被表示為單獨的行)。
例如,在 [圖 3-9](/img/fig3-9.png) 中,其中一個維度是已售出的產品。`dim_product` 表中的每一行代表一種待售產品包括庫存單位SKU、產品描述、品牌名稱、類別、脂肪含量、包裝尺寸等。`fact_sales` 表中的每一行都使用外部索引鍵表明在特定交易中銷售了什麼產品。(簡單起見,如果客戶一次購買了幾種不同的產品,則它們在事實表中被表示為單獨的行)。
甚至日期和時間也通常使用維度表來表示,因為這允許對日期的附加資訊(諸如公共假期)進行編碼,從而允許區分假期和非假期的銷售查詢。
@@ -470,13 +471,13 @@ GROUP BY
我們如何有效地執行這個查詢?
在大多數 OLTP 資料庫中,儲存都是以面向行的方式進行佈局的:表格的一行中的所有值都相鄰儲存。文件資料庫也是相似的:整個文件通常儲存為一個連續的位元組序列。你可以在 [圖 3-1](../img/fig3-1.png) 的 CSV 例子中看到這個。
在大多數 OLTP 資料庫中,儲存都是以面向行的方式進行佈局的:表格的一行中的所有值都相鄰儲存。文件資料庫也是相似的:整個文件通常儲存為一個連續的位元組序列。你可以在 [圖 3-1](/img/fig3-1.png) 的 CSV 例子中看到這個。
為了處理像 [例 3-1]() 這樣的查詢,你可能在 `fact_sales.date_key``fact_sales.product_sk` 上有索引,它們告訴儲存引擎在哪裡查詢特定日期或特定產品的所有銷售情況。但是,面向行的儲存引擎仍然需要將所有這些行(每個包含超過 100 個屬性)從硬碟載入到記憶體中,解析它們,並過濾掉那些不符合要求的屬性。這可能需要很長時間。
列式儲存背後的想法很簡單:不要將所有來自一行的值儲存在一起,而是將來自每一列的所有值儲存在一起。如果每個列式儲存在一個單獨的檔案中,查詢只需要讀取和解析查詢中使用的那些列,這可以節省大量的工作。這個原理如 [圖 3-10](../img/fig3-10.png) 所示。
列式儲存背後的想法很簡單:不要將所有來自一行的值儲存在一起,而是將來自每一列的所有值儲存在一起。如果每個列式儲存在一個單獨的檔案中,查詢只需要讀取和解析查詢中使用的那些列,這可以節省大量的工作。這個原理如 [圖 3-10](/img/fig3-10.png) 所示。
![](../img/fig3-10.png)
![](/img/fig3-10.png)
**圖 3-10 按列儲存關係型資料,而不是行**
@@ -489,9 +490,9 @@ GROUP BY
除了僅從硬碟載入查詢所需的列以外,我們還可以透過壓縮資料來進一步降低對硬碟吞吐量的需求。幸運的是,列式儲存通常很適合壓縮。
看看 [圖 3-10](../img/fig3-10.png) 中每一列的值序列:它們通常看起來是相當重複的,這是壓縮的好兆頭。根據列中的資料,可以使用不同的壓縮技術。在資料倉庫中特別有效的一種技術是點陣圖編碼,如 [圖 3-11](../img/fig3-11.png) 所示。
看看 [圖 3-10](/img/fig3-10.png) 中每一列的值序列:它們通常看起來是相當重複的,這是壓縮的好兆頭。根據列中的資料,可以使用不同的壓縮技術。在資料倉庫中特別有效的一種技術是點陣圖編碼,如 [圖 3-11](/img/fig3-11.png) 所示。
![](../img/fig3-11.png)
![](/img/fig3-11.png)
**圖 3-11 壓縮的點陣圖索引儲存佈局**
@@ -535,9 +536,9 @@ WHERE product_sk = 31 AND store_sk = 3
相反,資料的排序需要對一整行統一操作,即使它們的儲存方式是按列的。資料庫管理員可以根據他們對常用查詢的瞭解,來選擇表格中用來排序的列。例如,如果查詢通常以日期範圍為目標,例如“上個月”,則可以將 `date_key` 作為第一個排序鍵。這樣查詢最佳化器就可以只掃描近1個月範圍的行了這比掃描所有行要快得多。
對於第一排序列中具有相同值的行,可以用第二排序列來進一步排序。例如,如果 `date_key` 是 [圖 3-10](../img/fig3-10.png) 中的第一個排序關鍵字,那麼 `product_sk` 可能是第二個排序關鍵字,以便同一天的同一產品的所有銷售資料都被儲存在相鄰位置。這將有助於需要在特定日期範圍內按產品對銷售進行分組或過濾的查詢。
對於第一排序列中具有相同值的行,可以用第二排序列來進一步排序。例如,如果 `date_key` 是 [圖 3-10](/img/fig3-10.png) 中的第一個排序關鍵字,那麼 `product_sk` 可能是第二個排序關鍵字,以便同一天的同一產品的所有銷售資料都被儲存在相鄰位置。這將有助於需要在特定日期範圍內按產品對銷售進行分組或過濾的查詢。
按順序排序的另一個好處是它可以幫助壓縮列。如果主要排序列沒有太多個不同的值,那麼在排序之後,將會得到一個相同的值連續重複多次的序列。一個簡單的遊程編碼(就像我們用於 [圖 3-11](../img/fig3-11.png) 中的點陣圖一樣)可以將該列壓縮到幾 KB —— 即使表中有數十億行。
按順序排序的另一個好處是它可以幫助壓縮列。如果主要排序列沒有太多個不同的值,那麼在排序之後,將會得到一個相同的值連續重複多次的序列。一個簡單的遊程編碼(就像我們用於 [圖 3-11](/img/fig3-11.png) 中的點陣圖一樣)可以將該列壓縮到幾 KB —— 即使表中有數十億行。
第一個排序鍵的壓縮效果最強。第二和第三個排序鍵會更混亂,因此不會有這麼長的連續的重複值。排序優先順序更低的列以幾乎隨機的順序出現,所以可能不會被壓縮。但對前幾列做排序在整體上仍然是有好處的。
@@ -567,13 +568,13 @@ WHERE product_sk = 31 AND store_sk = 3
當底層資料發生變化時,物化檢視需要更新,因為它是資料的非規範化副本。資料庫可以自動完成該操作,但是這樣的更新使得寫入成本更高,這就是在 OLTP 資料庫中不經常使用物化檢視的原因。在讀取繁重的資料倉庫中,它們可能更有意義(它們是否實際上改善了讀取效能取決於使用場景)。
物化檢視的常見特例稱為資料立方體或 OLAP 立方【64】。它是按不同維度分組的聚合網格。[圖 3-12](../img/fig3-12.png) 顯示了一個例子。
物化檢視的常見特例稱為資料立方體或 OLAP 立方【64】。它是按不同維度分組的聚合網格。[圖 3-12](/img/fig3-12.png) 顯示了一個例子。
![](../img/fig3-12.png)
![](/img/fig3-12.png)
**圖 3-12 資料立方的兩個維度,透過求和聚合**
想象一下,現在每個事實都只有兩個維度表的外部索引鍵 —— 在 [圖 3-12](../img/fig3-12.png) 中分別是日期和產品。你現在可以繪製一個二維表格,一個軸線上是日期,另一個軸線上是產品。每個單元格包含具有該日期 - 產品組合的所有事實的屬性(例如 `net_price`)的聚合(例如 `SUM`)。然後,你可以沿著每行或每列應用相同的彙總,並獲得減少了一個維度的彙總(按產品的銷售額,無論日期,或者按日期的銷售額,無論產品)。
想象一下,現在每個事實都只有兩個維度表的外部索引鍵 —— 在 [圖 3-12](/img/fig3-12.png) 中分別是日期和產品。你現在可以繪製一個二維表格,一個軸線上是日期,另一個軸線上是產品。每個單元格包含具有該日期 - 產品組合的所有事實的屬性(例如 `net_price`)的聚合(例如 `SUM`)。然後,你可以沿著每行或每列應用相同的彙總,並獲得減少了一個維度的彙總(按產品的銷售額,無論日期,或者按日期的銷售額,無論產品)。
一般來說,事實往往有兩個以上的維度。在圖 3-9 中有五個維度:日期、產品、商店、促銷和客戶。要想象一個五維超立方體是什麼樣子是很困難的,但是原理是一樣的:每個單元格都包含特定日期 - 產品 - 商店 - 促銷 - 客戶組合的銷售額。這些值可以在每個維度上求和彙總。
@@ -672,11 +673,4 @@ WHERE product_sk = 31 AND store_sk = 3
1. Michael Stonebraker, Daniel J. Abadi, Adam Batkin, et al.: “[C-Store: A Column-oriented DBMS](http://www.cs.umd.edu/~abadi/vldb.pdf),” at *31st International Conference on Very Large Data Bases* (VLDB), pages 553564, September 2005.
1. Andrew Lamb, Matt Fuller, Ramakrishna Varadarajan, et al.: “[The Vertica Analytic Database: C-Store 7 Years Later](http://vldb.org/pvldb/vol5/p1790_andrewlamb_vldb2012.pdf),” *Proceedings of the VLDB Endowment*, volume 5, number 12, pages 17901801, August 2012.
1. Julien Le Dem and Nong Li: “[Efficient Data Storage for Analytics with Apache Parquet 2.0](http://www.slideshare.net/julienledem/th-210pledem),” at *Hadoop Summit*, San Jose, June 2014.
1. Jim Gray, Surajit Chaudhuri, Adam Bosworth, et al.: “[Data Cube: A Relational Aggregation Operator Generalizing Group-By, Cross-Tab, and Sub-Totals](http://arxiv.org/pdf/cs/0701155.pdf),” *Data Mining and Knowledge Discovery*, volume 1, number 1, pages 2953, March 2007. [doi:10.1023/A:1009726021843](http://dx.doi.org/10.1023/A:1009726021843)
------
| 上一章 | 目錄 | 下一章 |
| ------------------------------------ | ------------------------------- | ---------------------------- |
| [第二章:資料模型與查詢語言](ch2.md) | [設計資料密集型應用](README.md) | [第四章:編碼與演化](ch4.md) |
1. Jim Gray, Surajit Chaudhuri, Adam Bosworth, et al.: “[Data Cube: A Relational Aggregation Operator Generalizing Group-By, Cross-Tab, and Sub-Totals](http://arxiv.org/pdf/cs/0701155.pdf),” *Data Mining and Knowledge Discovery*, volume 1, number 1, pages 2953, March 2007. [doi:10.1023/A:1009726021843](http://dx.doi.org/10.1023/A:1009726021843)

View File

@@ -1,21 +1,22 @@
# 第四章:編碼與演化
---
title: "第四章:編碼與演化"
linkTitle: "4. 編碼與演化"
weight: 104
breadcrumbs: false
---
![](../img/ch4.png)
![](/img/ch4.png)
> 唯變所適
>
> —— 以弗所的赫拉克利特,為柏拉圖所引(公元前 360 年)
>
-------------------
[TOC]
應用程式不可避免地隨時間而變化。新產品的推出,對需求的深入理解,或者商業環境的變化,總會伴隨著 **功能feature** 的增增改改。[第一章](ch1.md) 介紹了 **可演化性evolvability** 的概念:應該盡力構建能靈活適應變化的系統(請參閱 “[可演化性:擁抱變化](ch1.md#可演化性:擁抱變化)”)。
應用程式不可避免地隨時間而變化。新產品的推出,對需求的深入理解,或者商業環境的變化,總會伴隨著 **功能feature** 的增增改改。[第一章](/tw/ch1) 介紹了 **可演化性evolvability** 的概念:應該盡力構建能靈活適應變化的系統(請參閱 “[可演化性:擁抱變化](/tw/ch1#可演化性:擁抱變化)”)。
在大多數情況下,修改應用程式的功能也意味著需要更改其儲存的資料:可能需要使用新的欄位或記錄型別,或者以新方式展示現有資料。
我們在 [第二章](ch2.md) 討論的資料模型有不同的方法來應對這種變化。關係資料庫通常假定資料庫中的所有資料都遵循一個模式:儘管可以更改該模式(透過模式遷移,即 `ALTER` 語句),但是在任何時間點都有且僅有一個正確的模式。相比之下,**讀時模式**schema-on-read**無模式**,即 schemaless資料庫不會強制一個模式因此資料庫可以包含在不同時間寫入的新老資料格式的混合請參閱 “[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)” )。
我們在 [第二章](/tw/ch2) 討論的資料模型有不同的方法來應對這種變化。關係資料庫通常假定資料庫中的所有資料都遵循一個模式:儘管可以更改該模式(透過模式遷移,即 `ALTER` 語句),但是在任何時間點都有且僅有一個正確的模式。相比之下,**讀時模式**schema-on-read**無模式**,即 schemaless資料庫不會強制一個模式因此資料庫可以包含在不同時間寫入的新老資料格式的混合請參閱 “[文件模型中的模式靈活性](/tw/ch2#文件模型中的模式靈活性)” )。
當資料 **格式format****模式schema** 發生變化時,通常需要對應用程式程式碼進行相應的更改(例如,為記錄新增新欄位,然後修改程式開始讀寫該欄位)。但在大型應用程式中,程式碼變更通常不會立即完成:
@@ -45,7 +46,7 @@
1. 在記憶體中,資料儲存在物件、結構體、列表、陣列、散列表、樹等中。這些資料結構針對 CPU 的高效訪問和操作進行了最佳化(通常使用指標)。
2. 如果要將資料寫入檔案,或透過網路傳送,則必須將其 **編碼encode** 為某種自包含的位元組序列例如JSON 文件)。由於每個程序都有自己獨立的地址空間,一個程序中的指標對任何其他程序都沒有意義,所以這個位元組序列表示會與通常在記憶體中使用的資料結構完全不同 [^i]。
[^i]: 除一些特殊情況外,例如某些記憶體對映檔案或直接在壓縮資料上操作(如 “[列壓縮](ch3.md#列壓縮)” 中所述)。
[^i]: 除一些特殊情況外,例如某些記憶體對映檔案或直接在壓縮資料上操作(如 “[列壓縮](/tw/ch3#列壓縮)” 中所述)。
所以,需要在兩種表示之間進行某種型別的翻譯。從記憶體中表示到位元組序列的轉換稱為 **編碼Encoding** (也稱為 **序列化serialization****編組marshalling**),反過來稱為 **解碼Decoding**[^ii]**解析Parsing****反序列化deserialization****反編組unmarshalling**[^譯i]。
@@ -53,7 +54,7 @@
[^譯i]: Marshal 與 Serialization 的區別Marshal 不僅傳輸物件的狀態,而且會一起傳輸物件的方法(相關程式碼)。
> #### 術語衝突
> 不幸的是,在 [第七章](ch7.md) **事務Transaction** 的上下文裡,**序列化Serialization** 這個術語也出現了,而且具有完全不同的含義。儘管序列化可能是更常見的術語,為了避免術語過載,本書中堅持使用 **編碼Encoding** 表達此含義。
> 不幸的是,在 [第七章](/tw/ch7) **事務Transaction** 的上下文裡,**序列化Serialization** 這個術語也出現了,而且具有完全不同的含義。儘管序列化可能是更常見的術語,為了避免術語過載,本書中堅持使用 **編碼Encoding** 表達此含義。
這是一個常見的問題,因而有許多庫和編碼格式可供選擇。首先讓我們概覽一下。
@@ -113,7 +114,7 @@ JSON 比 XML 簡潔,但與二進位制格式相比還是太佔空間。這一
在下面的章節中,能達到比這好得多的結果,只用 32 個位元組對相同的記錄進行編碼。
![](../img/fig4-1.png)
![](/img/fig4-1.png)
**圖 4-1 使用 MessagePack 編碼的記錄(例 4-1**
@@ -141,27 +142,27 @@ message Person {
```
Thrift 和 Protocol Buffers 每一個都帶有一個程式碼生成工具它採用了類似於這裡所示的模式定義並且生成了以各種程式語言實現模式的類【18】。你的應用程式程式碼可以呼叫此生成的程式碼來對模式的記錄進行編碼或解碼。
用這個模式編碼的資料是什麼樣的令人困惑的是Thrift 有兩種不同的二進位制編碼格式 [^iii],分別稱為 BinaryProtocol 和 CompactProtocol。先來看看 BinaryProtocol。使用這種格式的編碼來編碼 [例 4-1]() 中的訊息只需要 59 個位元組,如 [圖 4-2](../img/fig4-2.png) 所示【19】。
用這個模式編碼的資料是什麼樣的令人困惑的是Thrift 有兩種不同的二進位制編碼格式 [^iii],分別稱為 BinaryProtocol 和 CompactProtocol。先來看看 BinaryProtocol。使用這種格式的編碼來編碼 [例 4-1]() 中的訊息只需要 59 個位元組,如 [圖 4-2](/img/fig4-2.png) 所示【19】。
![](../img/fig4-2.png)
![](/img/fig4-2.png)
**圖 4-2 使用 Thrift 二進位制協議編碼的記錄**
[^iii]: 實際上Thrift 有三種二進位制協議BinaryProtocol、CompactProtocol 和 DenseProtocol儘管 DenseProtocol 只支援 C ++ 實現所以不算作跨語言【18】。除此之外它還有兩種不同的基於 JSON 的編碼格式【19】。真逗
與 [圖 4-1](Img/fig4-1.png) 類似,每個欄位都有一個型別註釋(用於指示它是一個字串、整數、列表等),還可以根據需要指定長度(字串的長度,列表中的專案數) 。出現在資料中的字串 `(“Martin”, “daydreaming”, “hacking”)` 也被編碼為 ASCII或者說UTF-8與之前類似。
與 [圖 4-1](/img/fig4-1.png) 類似,每個欄位都有一個型別註釋(用於指示它是一個字串、整數、列表等),還可以根據需要指定長度(字串的長度,列表中的專案數) 。出現在資料中的字串 `(“Martin”, “daydreaming”, “hacking”)` 也被編碼為 ASCII或者說UTF-8與之前類似。
與 [圖 4-1](../img/fig4-1.png) 相比,最大的區別是沒有欄位名 `(userName, favoriteNumber, interests)`。相反,編碼資料包含欄位標籤,它們是數字 `(1, 2 和 3)`。這些是模式定義中出現的數字。欄位標記就像欄位的別名 - 它們是說我們正在談論的欄位的一種緊湊的方式,而不必拼出欄位名稱。
與 [圖 4-1](/img/fig4-1.png) 相比,最大的區別是沒有欄位名 `(userName, favoriteNumber, interests)`。相反,編碼資料包含欄位標籤,它們是數字 `(1, 2 和 3)`。這些是模式定義中出現的數字。欄位標記就像欄位的別名 - 它們是說我們正在談論的欄位的一種緊湊的方式,而不必拼出欄位名稱。
Thrift CompactProtocol 編碼在語義上等同於 BinaryProtocol但是如 [圖 4-3](../img/fig4-3.png) 所示,它只將相同的資訊打包成只有 34 個位元組。它透過將欄位型別和標籤號打包到單個位元組中,並使用可變長度整數來實現。數字 1337 不是使用全部八個位元組,而是用兩個位元組編碼,每個位元組的最高位用來指示是否還有更多的位元組。這意味著 - 64 到 63 之間的數字被編碼為一個位元組,-8192 和 8191 之間的數字以兩個位元組編碼,等等。較大的數字使用更多的位元組。
Thrift CompactProtocol 編碼在語義上等同於 BinaryProtocol但是如 [圖 4-3](/img/fig4-3.png) 所示,它只將相同的資訊打包成只有 34 個位元組。它透過將欄位型別和標籤號打包到單個位元組中,並使用可變長度整數來實現。數字 1337 不是使用全部八個位元組,而是用兩個位元組編碼,每個位元組的最高位用來指示是否還有更多的位元組。這意味著 - 64 到 63 之間的數字被編碼為一個位元組,-8192 和 8191 之間的數字以兩個位元組編碼,等等。較大的數字使用更多的位元組。
![](../img/fig4-3.png)
![](/img/fig4-3.png)
**圖 4-3 使用 Thrift 壓縮協議編碼的記錄**
最後Protocol Buffers只有一種二進位制編碼格式對相同的資料進行編碼如 [圖 4-4](../img/fig4-4.png) 所示。它的打包方式稍有不同,但與 Thrift 的 CompactProtocol 非常相似。Protobuf 將同樣的記錄塞進了 33 個位元組中。
最後Protocol Buffers只有一種二進位制編碼格式對相同的資料進行編碼如 [圖 4-4](/img/fig4-4.png) 所示。它的打包方式稍有不同,但與 Thrift 的 CompactProtocol 非常相似。Protobuf 將同樣的記錄塞進了 33 個位元組中。
![](../img/fig4-4.png)
![](/img/fig4-4.png)
**圖 4-4 使用 Protobuf 編碼的記錄**
@@ -183,7 +184,7 @@ Thrift CompactProtocol 編碼在語義上等同於 BinaryProtocol但是如 [
如何改變欄位的資料型別?這也許是可能的 —— 詳細資訊請查閱相關的文件 —— 但是有一個風險,值將失去精度或被截斷。例如,假設你將一個 32 位的整數變成一個 64 位的整數。新程式碼可以輕鬆讀取舊程式碼寫入的資料,因為解析器可以用零填充任何缺失的位。但是,如果舊程式碼讀取由新程式碼寫入的資料,則舊程式碼仍使用 32 位變數來儲存該值。如果解碼的 64 位值不適合 32 位,則它將被截斷。
Protobuf 的一個奇怪的細節是,它沒有列表或陣列資料型別,而是有一個欄位的重複標記(`repeated`,這是除必需和可選之外的第三個選項)。如 [圖 4-4](../img/fig4-4.png) 所示,重複欄位的編碼正如它所說的那樣:同一個欄位標記只是簡單地出現在記錄中。這具有很好的效果,可以將可選(單值)欄位更改為重複(多值)欄位。讀取舊資料的新程式碼會看到一個包含零個或一個元素的列表(取決於該欄位是否存在)。讀取新資料的舊程式碼只能看到列表的最後一個元素。
Protobuf 的一個奇怪的細節是,它沒有列表或陣列資料型別,而是有一個欄位的重複標記(`repeated`,這是除必需和可選之外的第三個選項)。如 [圖 4-4](/img/fig4-4.png) 所示,重複欄位的編碼正如它所說的那樣:同一個欄位標記只是簡單地出現在記錄中。這具有很好的效果,可以將可選(單值)欄位更改為重複(多值)欄位。讀取舊資料的新程式碼會看到一個包含零個或一個元素的列表(取決於該欄位是否存在)。讀取新資料的舊程式碼只能看到列表的最後一個元素。
Thrift 有一個專用的列表資料型別,它使用列表元素的資料型別進行引數化。這不允許 Protocol Buffers 所做的從單值到多值的演變,但是它具有支援巢狀列表的優點。
@@ -217,11 +218,11 @@ record Person {
}
```
首先,請注意模式中沒有標籤號碼。如果我們使用這個模式編碼我們的例子記錄([例 4-1]()Avro 二進位制編碼只有 32 個位元組長,這是我們所見過的所有編碼中最緊湊的。編碼位元組序列的分解如 [圖 4-5](../img/fig4-5.png) 所示。
首先,請注意模式中沒有標籤號碼。如果我們使用這個模式編碼我們的例子記錄([例 4-1]()Avro 二進位制編碼只有 32 個位元組長,這是我們所見過的所有編碼中最緊湊的。編碼位元組序列的分解如 [圖 4-5](/img/fig4-5.png) 所示。
如果你檢查位元組序列,你可以看到沒有什麼可以識別字段或其資料型別。編碼只是由連在一起的值組成。一個字串只是一個長度字首,後跟 UTF-8 位元組,但是在被包含的資料中沒有任何內容告訴你它是一個字串。它可以是一個整數,也可以是其他的整數。整數使用可變長度編碼(與 Thrift 的 CompactProtocol 相同)進行編碼。
![](../img/fig4-5.png)
![](/img/fig4-5.png)
**圖 4-5 使用 Avro 編碼的記錄**
@@ -235,11 +236,11 @@ record Person {
當一個應用程式想要解碼一些資料(從一個檔案或資料庫讀取資料、從網路接收資料等)時,它希望資料在某個模式中,這就是 Reader 模式。這是應用程式程式碼所依賴的模式,在應用程式的構建過程中,程式碼可能已經從該模式生成。
Avro 的關鍵思想是 Writer 模式和 Reader 模式不必是相同的 - 他們只需要相容。當資料解碼讀取Avro 庫透過並排檢視 Writer 模式和 Reader 模式並將資料從 Writer 模式轉換到 Reader 模式來解決差異。Avro 規範【20】確切地定義了這種解析的工作原理如 [圖 4-6](../img/fig4-6.png) 所示。
Avro 的關鍵思想是 Writer 模式和 Reader 模式不必是相同的 - 他們只需要相容。當資料解碼讀取Avro 庫透過並排檢視 Writer 模式和 Reader 模式並將資料從 Writer 模式轉換到 Reader 模式來解決差異。Avro 規範【20】確切地定義了這種解析的工作原理如 [圖 4-6](/img/fig4-6.png) 所示。
例如,如果 Writer 模式和 Reader 模式的欄位順序不同,這是沒有問題的,因為模式解析透過欄位名匹配欄位。如果讀取資料的程式碼遇到出現在 Writer 模式中但不在 Reader 模式中的欄位,則忽略它。如果讀取資料的程式碼需要某個欄位,但是 Writer 模式不包含該名稱的欄位,則使用在 Reader 模式中宣告的預設值填充。
![](../img/fig4-6.png)
![](/img/fig4-6.png)
**圖 4-6 一個 Avro Reader 解決讀寫模式的差異**
@@ -265,7 +266,7 @@ Avro 的關鍵思想是 Writer 模式和 Reader 模式不必是相同的 - 他
* 有很多記錄的大檔案
Avro 的一個常見用途 - 尤其是在 Hadoop 環境中 - 用於儲存包含數百萬條記錄的大檔案,所有記錄都使用相同的模式進行編碼(我們將在 [第十章](ch10.md) 討論這種情況)。在這種情況下,該檔案的作者可以在檔案的開頭只包含一次 Writer 模式。Avro 指定了一個檔案格式(物件容器檔案)來做到這一點。
Avro 的一個常見用途 - 尤其是在 Hadoop 環境中 - 用於儲存包含數百萬條記錄的大檔案,所有記錄都使用相同的模式進行編碼(我們將在 [第十章](/tw/ch10) 討論這種情況)。在這種情況下,該檔案的作者可以在檔案的開頭只包含一次 Writer 模式。Avro 指定了一個檔案格式(物件容器檔案)來做到這一點。
* 支援獨立寫入的記錄的資料庫
@@ -312,7 +313,7 @@ Avro 為靜態型別程式語言提供了可選的程式碼生成功能,但是
* 維護一個模式的資料庫允許你在部署任何內容之前檢查模式更改的向前和向後相容性。
* 對於靜態型別程式語言的使用者來說,從模式生成程式碼的能力是有用的,因為它可以在編譯時進行型別檢查。
總而言之,模式演化保持了與 JSON 資料庫提供的無模式 / 讀時模式相同的靈活性(請參閱 “[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)”),同時還可以更好地保證你的資料並提供更好的工具。
總而言之,模式演化保持了與 JSON 資料庫提供的無模式 / 讀時模式相同的靈活性(請參閱 “[文件模型中的模式靈活性](/tw/ch2#文件模型中的模式靈活性)”),同時還可以更好地保證你的資料並提供更好的工具。
## 資料流的型別
@@ -342,7 +343,7 @@ Avro 為靜態型別程式語言提供了可選的程式碼生成功能,但是
前面討論的編碼格式支援未知欄位的儲存,但是有時候需要在應用程式層面保持謹慎,如圖 4-7 所示。例如,如果將資料庫值解碼為應用程式中的模型物件,稍後重新編碼這些模型物件,那麼未知欄位可能會在該翻譯過程中丟失。解決這個問題不是一個難題,你只需要意識到它。
![](../img/fig4-7.png)
![](/img/fig4-7.png)
**圖 4-7 當較舊版本的應用程式更新以前由較新版本的應用程式編寫的資料時,如果不小心,資料可能會丟失。**
@@ -356,16 +357,16 @@ Avro 為靜態型別程式語言提供了可選的程式碼生成功能,但是
因此,模式演變允許整個資料庫看起來好像是用單個模式編碼的,即使底層儲存可能包含用各種歷史版本的模式編碼的記錄。
[^v]: 除了 MySQL即使並非真的必要它也經常會重寫整個表正如 “[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)” 中所提到的。
[^v]: 除了 MySQL即使並非真的必要它也經常會重寫整個表正如 “[文件模型中的模式靈活性](/tw/ch2#文件模型中的模式靈活性)” 中所提到的。
#### 歸檔儲存
也許你不時為資料庫建立一個快照,例如備份或載入到資料倉庫(請參閱 “[資料倉庫](ch3.md#資料倉庫)”)。在這種情況下,即使源資料庫中的原始編碼包含來自不同時代的模式版本的混合,資料轉儲通常也將使用最新模式進行編碼。既然你不管怎樣都要複製資料,那麼你可以對這個資料複製進行一致的編碼。
也許你不時為資料庫建立一個快照,例如備份或載入到資料倉庫(請參閱 “[資料倉庫](/tw/ch3#資料倉庫)”)。在這種情況下,即使源資料庫中的原始編碼包含來自不同時代的模式版本的混合,資料轉儲通常也將使用最新模式進行編碼。既然你不管怎樣都要複製資料,那麼你可以對這個資料複製進行一致的編碼。
由於資料轉儲是一次寫入的,而且以後是不可變的,所以 Avro 物件容器檔案等格式非常適合。這也是一個很好的機會,可以將資料編碼為面向分析的列式格式,例如 Parquet請參閱 “[列壓縮](ch3.md#列壓縮)”)。
由於資料轉儲是一次寫入的,而且以後是不可變的,所以 Avro 物件容器檔案等格式非常適合。這也是一個很好的機會,可以將資料編碼為面向分析的列式格式,例如 Parquet請參閱 “[列壓縮](/tw/ch3#列壓縮)”)。
在 [第十章](ch10.md) 中,我們將詳細討論使用檔案儲存中的資料。
在 [第十章](/tw/ch10) 中,我們將詳細討論使用檔案儲存中的資料。
### 服務中的資料流REST與RPC
@@ -378,7 +379,7 @@ Web 瀏覽器不是唯一的客戶端型別。例如,在移動裝置或桌面
此外,伺服器本身可以是另一個服務的客戶端(例如,典型的 Web 應用伺服器充當資料庫的客戶端)。這種方法通常用於將大型應用程式按照功能區域分解為較小的服務,這樣當一個服務需要來自另一個服務的某些功能或資料時,就會向另一個服務發出請求。這種構建應用程式的方式傳統上被稱為 **面向服務的體系結構service-oriented architectureSOA**,最近被改進和更名為 **微服務架構**【31,32】。
在某些方面,服務類似於資料庫:它們通常允許客戶端提交和查詢資料。但是,雖然資料庫允許使用我們在 [第二章](ch2.md) 中討論的查詢語言進行任意查詢,但是服務公開了一個特定於應用程式的 API它只允許由服務的業務邏輯應用程式程式碼預定的輸入和輸出【33】。這種限制提供了一定程度的封裝服務能夠對客戶可以做什麼和不可以做什麼施加細粒度的限制。
在某些方面,服務類似於資料庫:它們通常允許客戶端提交和查詢資料。但是,雖然資料庫允許使用我們在 [第二章](/tw/ch2) 中討論的查詢語言進行任意查詢,但是服務公開了一個特定於應用程式的 API它只允許由服務的業務邏輯應用程式程式碼預定的輸入和輸出【33】。這種限制提供了一定程度的封裝服務能夠對客戶可以做什麼和不可以做什麼施加細粒度的限制。
面向服務 / 微服務架構的一個關鍵設計目標是透過使服務獨立部署和演化來使應用程式更易於更改和維護。例如,每個服務應該由一個團隊擁有,並且該團隊應該能夠經常釋出新版本的服務,而不必與其他團隊協調。換句話說,我們應該期望伺服器和客戶端的舊版本和新版本同時執行,因此伺服器和客戶端使用的資料編碼必須在不同版本的服務 API 之間相容 —— 這正是我們在本章所一直在談論的。
@@ -415,8 +416,8 @@ Web 服務僅僅是透過網路進行 API 請求的一系列技術的最新版
所有這些都是基於 **遠端過程呼叫RPC** 的思想,該過程呼叫自 20 世紀 70 年代以來一直存在【42】。RPC 模型試圖向遠端網路服務發出請求,看起來與在同一程序中呼叫程式語言中的函式或方法相同(這種抽象稱為位置透明)。儘管 RPC 起初看起來很方便但這種方法根本上是有缺陷的【43,44】。網路請求與本地函式呼叫非常不同
* 本地函式呼叫是可預測的,並且成功或失敗僅取決於受你控制的引數。網路請求是不可預測的:請求或響應可能由於網路問題會丟失,或者遠端計算機可能很慢或不可用,這些問題完全不在你的控制範圍之內。網路問題很常見,因此必須有所準備,例如重試失敗的請求。
* 本地函式呼叫要麼返回結果,要麼丟擲異常,或者永遠不返回(因為進入無限迴圈或程序崩潰)。網路請求有另一個可能的結果:由於超時,它返回時可能沒有結果。在這種情況下,你根本不知道發生了什麼:如果你沒有得到來自遠端服務的響應,你無法知道請求是否透過(我們將在 [第八章](ch8.md) 更詳細地討論這個問題)。
* 如果你重試失敗的網路請求,可能會發生請求實際上已經完成,只是響應丟失的情況。在這種情況下,重試將導致該操作被執行多次,除非你在協議中建立資料去重機制(**冪等性**,即 idempotence。本地函式呼叫時沒有這樣的問題。在 [第十一章](ch11.md) 更詳細地討論冪等性)
* 本地函式呼叫要麼返回結果,要麼丟擲異常,或者永遠不返回(因為進入無限迴圈或程序崩潰)。網路請求有另一個可能的結果:由於超時,它返回時可能沒有結果。在這種情況下,你根本不知道發生了什麼:如果你沒有得到來自遠端服務的響應,你無法知道請求是否透過(我們將在 [第八章](/tw/ch8) 更詳細地討論這個問題)。
* 如果你重試失敗的網路請求,可能會發生請求實際上已經完成,只是響應丟失的情況。在這種情況下,重試將導致該操作被執行多次,除非你在協議中建立資料去重機制(**冪等性**,即 idempotence。本地函式呼叫時沒有這樣的問題。在 [第十一章](/tw/ch11) 更詳細地討論冪等性)
* 每次呼叫本地函式時,通常需要大致相同的時間來執行。網路請求比函式呼叫要慢得多,而且其延遲也是非常可變的:好的時候它可能會在不到一毫秒的時間內完成,但是當網路擁塞或者遠端服務超載時,可能需要幾秒鐘的時間才能完成相同的操作。
* 呼叫本地函式時,可以高效地將引用(指標)傳遞給本地記憶體中的物件。當你發出一個網路請求時,所有這些引數都需要被編碼成可以透過網路傳送的一系列位元組。如果引數是像數字或字串這樣的基本型別倒是沒關係,但是對於較大的物件很快就會出現問題。
* 客戶端和服務可以用不同的程式語言實現,所以 RPC 框架必須將資料型別從一種語言翻譯成另一種語言。這可能會變得很醜陋,因為不是所有的語言都具有相同的型別 —— 例如回想一下 JavaScript 的數字大於 $2^{53}$ 的問題(請參閱 “[JSON、XML 和二進位制變體](#JSON、XML和二進位制變體)”)。用單一語言編寫的單個程序中不存在此問題。
@@ -429,7 +430,7 @@ Web 服務僅僅是透過網路進行 API 請求的一系列技術的最新版
這種新一代的 RPC 框架更加明確的是遠端請求與本地函式呼叫不同。例如Finagle 和 Rest.li 使用 futurespromises來封裝可能失敗的非同步操作。`Futures` 還可以簡化需要並行發出多項服務並將其結果合併的情況【45】。gRPC 支援流其中一個呼叫不僅包括一個請求和一個響應還可以是隨時間的一系列請求和響應【46】。
其中一些框架還提供服務發現,即允許客戶端找出在哪個 IP 地址和埠號上可以找到特定的服務。我們將在 “[請求路由](ch6.md#請求路由)” 中回到這個主題。
其中一些框架還提供服務發現,即允許客戶端找出在哪個 IP 地址和埠號上可以找到特定的服務。我們將在 “[請求路由](/tw/ch6#請求路由)” 中回到這個主題。
使用二進位制編碼格式的自定義 RPC 協議可以實現比通用的 JSON over REST 更好的效能。但是RESTful API 還有其他一些顯著的優點:方便實驗和除錯(只需使用 Web 瀏覽器或命令列工具 curl無需任何程式碼生成或軟體安裝即可向其請求能被所有主流的程式語言和平臺所支援還有大量可用的工具伺服器、快取、負載平衡器、代理、防火牆、監控、除錯工具、測試工具等的生態系統。
@@ -467,15 +468,15 @@ RPC 方案的向後和向前相容性屬性是從它使用的編碼方式中繼
#### 訊息代理
過去,**訊息代理Message Broker** 主要是 TIBCO、IBM WebSphere 和 webMethods 等公司的商業軟體的秀場。最近像 RabbitMQ、ActiveMQ、HornetQ、NATS 和 Apache Kafka 這樣的開源實現已經流行起來。我們將在 [第十一章](ch11.md) 中對它們進行更詳細的比較。
過去,**訊息代理Message Broker** 主要是 TIBCO、IBM WebSphere 和 webMethods 等公司的商業軟體的秀場。最近像 RabbitMQ、ActiveMQ、HornetQ、NATS 和 Apache Kafka 這樣的開源實現已經流行起來。我們將在 [第十一章](/tw/ch11) 中對它們進行更詳細的比較。
詳細的交付語義因實現和配置而異,但通常情況下,訊息代理的使用方式如下:一個程序將訊息傳送到指定的佇列或主題,代理確保將訊息傳遞給那個佇列或主題的一個或多個消費者或訂閱者。在同一主題上可以有許多生產者和許多消費者。
一個主題只提供單向資料流。但是,消費者本身可能會將訊息釋出到另一個主題上(因此,可以將它們連結在一起,就像我們將在 [第十一章](ch11.md) 中看到的那樣),或者傳送給原始訊息的傳送者使用的回覆佇列(允許請求 / 響應資料流,類似於 RPC
一個主題只提供單向資料流。但是,消費者本身可能會將訊息釋出到另一個主題上(因此,可以將它們連結在一起,就像我們將在 [第十一章](/tw/ch11) 中看到的那樣),或者傳送給原始訊息的傳送者使用的回覆佇列(允許請求 / 響應資料流,類似於 RPC
訊息代理通常不會執行任何特定的資料模型 —— 訊息只是包含一些元資料的位元組序列,因此你可以使用任何編碼格式。如果編碼是向後和向前相容的,你可以靈活地對釋出者和消費者的編碼進行獨立的修改,並以任意順序進行部署。
如果消費者重新發布訊息到另一個主題,則可能需要小心保留未知欄位,以防止前面在資料庫環境中描述的問題([圖 4-7](../img/fig4-7.png))。
如果消費者重新發布訊息到另一個主題,則可能需要小心保留未知欄位,以防止前面在資料庫環境中描述的問題([圖 4-7](/img/fig4-7.png))。
#### 分散式的Actor框架
@@ -572,10 +573,4 @@ Actor 模型是單個程序中併發的程式設計模型。邏輯被封裝在 a
1. Philip A. Bernstein, Sergey Bykov, Alan Geller, et al.: “[Orleans: Distributed Virtual Actors for Programmability and Scalability](https://www.microsoft.com/en-us/research/publication/orleans-distributed-virtual-actors-for-programmability-and-scalability/),” Microsoft Research Technical Report MSR-TR-2014-41, March 2014.
1. “[Microsoft Project Orleans Documentation](http://dotnet.github.io/orleans/),” Microsoft Research, *dotnet.github.io*, 2015.
1. David Mercer, Sean Hinde, Yinso Chen, and Richard A O'Keefe: “[beginner: Updating Data Structures](http://erlang.org/pipermail/erlang-questions/2007-October/030318.html),” email thread on *erlang-questions* mailing list, *erlang.com*, October 29, 2007.
1. Fred Hebert: “[Postscript: Maps](http://learnyousomeerlang.com/maps),” *learnyousomeerlang.com*, April 9, 2014.
------
| 上一章 | 目錄 | 下一章 |
| ---------------------------- | ------------------------------- | --------------------------------- |
| [第三章:儲存與檢索](ch3.md) | [設計資料密集型應用](README.md) | [第二部分:分散式資料](part-ii.md) |
1. Fred Hebert: “[Postscript: Maps](http://learnyousomeerlang.com/maps),” *learnyousomeerlang.com*, April 9, 2014.

View File

@@ -1,22 +1,24 @@
# 第五章:複製
---
title: "第五章:複製"
linkTitle: "5. 複製"
weight: 205
breadcrumbs: false
---
![](../img/ch5.png)
![](/img/ch5.png)
> 與可能出錯的東西比,“不可能”出錯的東西最顯著的特點就是:一旦真的出錯,通常就徹底玩完了。
>
> —— 道格拉斯・亞當斯1992
------
[TOC]
複製意味著在透過網路連線的多臺機器上保留相同資料的副本。正如在 [第二部分](part-ii.md) 的介紹中所討論的那樣,我們希望能複製資料,可能出於各種各樣的原因:
複製意味著在透過網路連線的多臺機器上保留相同資料的副本。正如在 [第二部分](/tw/part-ii) 的介紹中所討論的那樣,我們希望能複製資料,可能出於各種各樣的原因:
* 使得資料與使用者在地理上接近(從而減少延遲)
* 即使系統的一部分出現故障,系統也能繼續工作(從而提高可用性)
* 伸縮可以接受讀請求的機器數量(從而提高讀取吞吐量)
本章將假設你的資料集非常小,每臺機器都可以儲存整個資料集的副本。在 [第六章](ch6.md) 中將放寬這個假設,討論對單個機器來說太大的資料集的分割(分片)。在後面的章節中,我們將討論複製資料系統中可能發生的各種故障,以及如何處理這些故障。
本章將假設你的資料集非常小,每臺機器都可以儲存整個資料集的副本。在 [第六章](/tw/ch6) 中將放寬這個假設,討論對單個機器來說太大的資料集的分割(分片)。在後面的章節中,我們將討論複製資料系統中可能發生的各種故障,以及如何處理這些故障。
如果複製中的資料不會隨時間而改變,那複製就很簡單:將資料複製到每個節點一次就萬事大吉。複製的困難之處在於處理複製資料的 **變更change**,這就是本章所要講的。我們將討論三種流行的變更復制演算法:**單領導者single leader單主****多領導者multi leader多主** 和 **無領導者leaderless無主**。幾乎所有分散式資料庫都使用這三種方法之一。
@@ -36,7 +38,7 @@
[^i]: 不同的人對 **熱hot**、**溫warm** 和 **冷cold** 備份伺服器有不同的定義。例如在 PostgreSQL 中,**熱備hot standby** 指的是能接受客戶端讀請求的副本。而 **溫備warm standby** 只是追隨領導者,但不處理客戶端的任何查詢。就本書而言,這些差異並不重要。
![](../img/fig5-1.png)
![](/img/fig5-1.png)
**圖 5-1 基於領導者的(主/從)複製**
@@ -48,13 +50,13 @@
想象一下 [圖 5-1](fig5-1.png) 中發生的場景,即網站的使用者更新他們的個人頭像。在某個時間點,客戶向主庫傳送更新請求;不久之後主庫就收到了請求。在某個時間點,主庫又會將資料變更轉發給自己的從庫。最終,主庫通知客戶更新成功。
[圖 5-2](../img/fig5-2.png) 顯示了系統各個元件之間的通訊:使用者客戶端、主庫和兩個從庫。時間從左向右流動。請求或響應訊息用粗箭頭表示。
[圖 5-2](/img/fig5-2.png) 顯示了系統各個元件之間的通訊:使用者客戶端、主庫和兩個從庫。時間從左向右流動。請求或響應訊息用粗箭頭表示。
![](../img/fig5-2.png)
![](/img/fig5-2.png)
**圖 5-2 基於領導者的複製:一個同步從庫和一個非同步從庫**
在 [圖 5-2](../img/fig5-2.png) 的示例中,從庫 1 的複製是同步的:在向用戶報告寫入成功並使結果對其他使用者可見之前,主庫需要等待從庫 1 的確認,確保從庫 1 已經收到寫入操作。而從庫 2 的複製是非同步的:主庫傳送訊息,但不等待該從庫的響應。
在 [圖 5-2](/img/fig5-2.png) 的示例中,從庫 1 的複製是同步的:在向用戶報告寫入成功並使結果對其他使用者可見之前,主庫需要等待從庫 1 的確認,確保從庫 1 已經收到寫入操作。而從庫 2 的複製是非同步的:主庫傳送訊息,但不等待該從庫的響應。
在這幅圖中,從庫 2 處理訊息前存在一個顯著的延遲。通常情況下,複製的速度相當快:大多數資料庫系統能在不到一秒內完成從庫的同步,但它們不能提供複製用時的保證。有些情況下,從庫可能落後主庫幾分鐘或更久,例如:從庫正在從故障中恢復,系統正在最大容量附近執行,或者當節點間存在網路問題時。
@@ -70,7 +72,7 @@
>
> 對於非同步複製系統而言,主庫故障時會丟失資料可能是一個嚴重的問題,因此研究人員仍在研究不丟資料但仍能提供良好效能和可用性的複製方法。例如,**鏈式複製chain replication**【8,9】是同步複製的一種變體已經在一些系統如 Microsoft Azure Storage【10,11】中成功實現。
>
> 複製的一致性與 **共識**consensus使幾個節點就某個值達成一致之間有著密切的聯絡[第九章](ch9.md) 將詳細地探討這一領域的理論。本章主要討論實踐中的資料庫常用的簡單複製形式。
> 複製的一致性與 **共識**consensus使幾個節點就某個值達成一致之間有著密切的聯絡[第九章](/tw/ch9) 將詳細地探討這一領域的理論。本章主要討論實踐中的資料庫常用的簡單複製形式。
>
### 設定新從庫
@@ -105,8 +107,8 @@
故障切換可以手動進行(通知管理員主庫掛了,並採取必要的步驟來建立新的主庫)或自動進行。自動的故障切換過程通常由以下步驟組成:
1. 確認主庫失效。有很多事情可能會出錯:崩潰、停電、網路問題等等。沒有萬無一失的方法來檢測出現了什麼問題,所以大多數系統只是簡單使用 **超時Timeout** :節點頻繁地相互來回傳遞訊息,如果一個節點在一段時間內(例如 30 秒)沒有響應,就認為它掛了(因為計劃內維護而故意關閉主庫不算)。
2. 選擇一個新的主庫。這可以透過選舉過程(主庫由剩餘副本以多數選舉產生)來完成,或者可以由之前選定的 **控制器節點controller node** 來指定新的主庫。主庫的最佳人選通常是擁有舊主庫最新資料副本的從庫(以最小化資料損失)。讓所有的節點同意一個新的領導者,是一個 **共識** 問題,將在 [第九章](ch9.md) 詳細討論。
3. 重新配置系統以啟用新的主庫。客戶端現在需要將它們的寫請求傳送給新主庫(將在 “[請求路由](ch6.md#請求路由)” 中討論這個問題)。如果舊主庫恢復,可能仍然認為自己是主庫,而沒有意識到其他副本已經讓它失去領導權了。系統需要確保舊主庫意識到新主庫的存在,併成為一個從庫。
2. 選擇一個新的主庫。這可以透過選舉過程(主庫由剩餘副本以多數選舉產生)來完成,或者可以由之前選定的 **控制器節點controller node** 來指定新的主庫。主庫的最佳人選通常是擁有舊主庫最新資料副本的從庫(以最小化資料損失)。讓所有的節點同意一個新的領導者,是一個 **共識** 問題,將在 [第九章](/tw/ch9) 詳細討論。
3. 重新配置系統以啟用新的主庫。客戶端現在需要將它們的寫請求傳送給新主庫(將在 “[請求路由](/tw/ch6#請求路由)” 中討論這個問題)。如果舊主庫恢復,可能仍然認為自己是主庫,而沒有意識到其他副本已經讓它失去領導權了。系統需要確保舊主庫意識到新主庫的存在,併成為一個從庫。
故障切換的過程中有很多地方可能出錯:
@@ -114,15 +116,15 @@
* 如果資料庫需要和其他外部儲存相協調,那麼丟棄寫入內容是極其危險的操作。例如在 GitHub 【13】的一場事故中一個過時的 MySQL 從庫被提升為主庫。資料庫使用自增 ID 作為主鍵,因為新主庫的計數器落後於老主庫的計數器,所以新主庫重新分配了一些已經被老主庫分配掉的 ID 作為主鍵。這些主鍵也在 Redis 中使用,主鍵重用使得 MySQL 和 Redis 中的資料產生不一致,最後導致一些私有資料洩漏到錯誤的使用者手中。
* 發生某些故障時(見 [第八章](ch8.md))可能會出現兩個節點都以為自己是主庫的情況。這種情況稱為 **腦裂split brain**,非常危險:如果兩個主庫都可以接受寫操作,卻沒有衝突解決機制(請參閱 “[多主複製](#多主複製)”),那麼資料就可能丟失或損壞。一些系統採取了安全防範措施:當檢測到兩個主庫節點同時存在時會關閉其中一個節點 [^ii]但設計粗糙的機制可能最後會導致兩個節點都被關閉【14】。
* 發生某些故障時(見 [第八章](/tw/ch8))可能會出現兩個節點都以為自己是主庫的情況。這種情況稱為 **腦裂split brain**,非常危險:如果兩個主庫都可以接受寫操作,卻沒有衝突解決機制(請參閱 “[多主複製](#多主複製)”),那麼資料就可能丟失或損壞。一些系統採取了安全防範措施:當檢測到兩個主庫節點同時存在時會關閉其中一個節點 [^ii]但設計粗糙的機制可能最後會導致兩個節點都被關閉【14】。
[^ii]: 這種機制稱為 **屏障fencing**,或者更充滿感情的術語是:**爆彼之頭Shoot The Other Node In The Head, STONITH**。我們將在 “[領導者和鎖](ch8.md#領導者和鎖)” 中對屏障進行詳細討論。
[^ii]: 這種機制稱為 **屏障fencing**,或者更充滿感情的術語是:**爆彼之頭Shoot The Other Node In The Head, STONITH**。我們將在 “[領導者和鎖](/tw/ch8#領導者和鎖)” 中對屏障進行詳細討論。
* 主庫被宣告死亡之前的正確超時應該怎麼配置?在主庫失效的情況下,超時時間越長意味著恢復時間也越長。但是如果超時設定太短,又可能會出現不必要的故障切換。例如,臨時的負載峰值可能導致節點的響應時間增加到超出超時時間,或者網路故障也可能導致資料包延遲。如果系統已經處於高負載或網路問題的困擾之中,那麼不必要的故障切換可能會讓情況變得更糟糕。
這些問題沒有簡單的解決方案。因此,即使軟體支援自動故障切換,不少運維團隊還是更願意手動執行故障切換。
節點故障、不可靠的網路、對副本一致性、永續性、可用性和延遲的權衡,這些問題實際上是分散式系統中的基本問題。[第八章](ch8.md) 和 [第九章](ch9.md) 將更深入地討論它們。
節點故障、不可靠的網路、對副本一致性、永續性、可用性和延遲的權衡,這些問題實際上是分散式系統中的基本問題。[第八章](/tw/ch8) 和 [第九章](/tw/ch9) 將更深入地討論它們。
### 複製日誌的實現
@@ -144,10 +146,10 @@
#### 傳輸預寫式日誌WAL
在 [第三章](ch3.md) 中,我們討論了儲存引擎如何在磁碟上表示資料,我們也發現了通常會將寫操作追加到日誌中:
在 [第三章](/tw/ch3) 中,我們討論了儲存引擎如何在磁碟上表示資料,我們也發現了通常會將寫操作追加到日誌中:
* 對於日誌結構儲存引擎(請參閱 “[SSTables 和 LSM 樹](ch3.md#SSTables和LSM樹)”),日誌是主要的儲存位置。日誌段在後臺壓縮,並進行垃圾回收。
* 對於覆寫單個磁碟塊的 [B 樹](ch3.md#B樹),每次修改都會先寫入 **預寫式日誌Write Ahead Log, WAL**,以便崩潰後索引可以恢復到一個一致的狀態。
* 對於日誌結構儲存引擎(請參閱 “[SSTables 和 LSM 樹](/tw/ch3#SSTables和LSM樹)”),日誌是主要的儲存位置。日誌段在後臺壓縮,並進行垃圾回收。
* 對於覆寫單個磁碟塊的 [B 樹](/tw/ch3#B樹),每次修改都會先寫入 **預寫式日誌Write Ahead Log, WAL**,以便崩潰後索引可以恢復到一個一致的狀態。
在任何一種情況下,該日誌都是包含了所有資料庫寫入的僅追加位元組序列。可以使用完全相同的日誌在另一個節點上構建副本:除了將日誌寫入磁碟之外,主庫還可以透過網路將其傳送給從庫。
@@ -171,7 +173,7 @@
由於邏輯日誌與儲存引擎的內部實現是解耦的,系統可以更容易地做到向後相容,從而使主庫和從庫能夠執行不同版本的資料庫軟體,或者甚至不同的儲存引擎。
對於外部應用程式來說邏輯日誌格式也更容易解析。如果要將資料庫的內容傳送到外部系統例如複製到資料倉庫進行離線分析或建立自定義索引和快取【18】這一點會很有用。這種技術被稱為 **資料變更捕獲change data capture**[第十一章](ch11.md) 將重新講到它。
對於外部應用程式來說邏輯日誌格式也更容易解析。如果要將資料庫的內容傳送到外部系統例如複製到資料倉庫進行離線分析或建立自定義索引和快取【18】這一點會很有用。這種技術被稱為 **資料變更捕獲change data capture**[第十一章](/tw/ch11) 將重新講到它。
#### 基於觸發器的複製
@@ -186,7 +188,7 @@
## 複製延遲問題
容忍節點故障只是需要複製的一個原因。正如在 [第二部分](part-ii.md) 的介紹中提到的,其它原因還包括可伸縮性(處理比單個機器更多的請求)和延遲(讓副本在地理位置上更接近使用者)。
容忍節點故障只是需要複製的一個原因。正如在 [第二部分](/tw/part-ii) 的介紹中提到的,其它原因還包括可伸縮性(處理比單個機器更多的請求)和延遲(讓副本在地理位置上更接近使用者)。
基於領導者的複製要求所有寫入都由單個節點處理但只讀查詢可以由任何一個副本來處理。所以對於讀多寫少的場景Web 上的常見模式),一個有吸引力的選擇是建立很多從庫,並將讀請求分散到所有的從庫上去。這樣能減小主庫的負載,並允許由附近的副本來處理讀請求。
@@ -206,7 +208,7 @@
但對於非同步複製,問題就來了。如 [圖 5-3](fig5-3.png) 所示:如果使用者在寫入後馬上就檢視資料,則新資料可能尚未到達副本。對使用者而言,看起來好像是剛提交的資料丟失了,所以他們不高興是可以理解的。
![](../img/fig5-3.png)
![](/img/fig5-3.png)
**圖 5-3 使用者寫入後從舊副本中讀取資料。需要寫後讀 (read-after-write) 的一致性來防止這種異常**
@@ -218,7 +220,7 @@
* 如果應用中的大部分內容都可能被使用者編輯,那這種方法就沒用了,因為大部分內容都必須從主庫讀取(讀伸縮就沒效果了)。在這種情況下可以使用其他標準來決定是否從主庫讀取。例如可以跟蹤上次更新的時間,在上次更新後的一分鐘內,從主庫讀。還可以監控從庫的複製延遲,防止向任何滯後主庫超過一分鐘的從庫發出查詢。
* 客戶端可以記住最近一次寫入的時間戳,系統需要確保從庫在處理該使用者的讀取請求時,該時間戳前的變更都已經傳播到了本從庫中。如果當前從庫不夠新,則可以從另一個從庫讀取,或者等待從庫追趕上來。這裡的時間戳可以是邏輯時間戳(表示寫入順序的東西,例如日誌序列號)或實際的系統時鐘(在這種情況下,時鐘同步變得至關重要,請參閱 “[不可靠的時鐘](ch8.md#不可靠的時鐘)”)。
* 客戶端可以記住最近一次寫入的時間戳,系統需要確保從庫在處理該使用者的讀取請求時,該時間戳前的變更都已經傳播到了本從庫中。如果當前從庫不夠新,則可以從另一個從庫讀取,或者等待從庫追趕上來。這裡的時間戳可以是邏輯時間戳(表示寫入順序的東西,例如日誌序列號)或實際的系統時鐘(在這種情況下,時鐘同步變得至關重要,請參閱 “[不可靠的時鐘](/tw/ch8#不可靠的時鐘)”)。
* 如果你的副本分佈在多個數據中心(為了在地理上接近使用者或者出於可用性目的),還會有額外的複雜性。任何需要由主庫提供服務的請求都必須路由到包含該主庫的資料中心。
@@ -234,9 +236,9 @@
在從非同步從庫讀取時可能發生的異常的第二個例子是使用者可能會遇到 **時光倒流moving backward in time**
如果使用者從不同從庫進行多次讀取,就可能發生這種情況。例如,[圖 5-4](../img/fig5-4.png) 顯示了使用者 2345 兩次進行相同的查詢,首先查詢了一個延遲很小的從庫,然後是一個延遲較大的從庫(如果使用者重新整理網頁時每個請求都被路由到一個隨機的伺服器,這種情況就很有可能發生)。第一個查詢返回了最近由使用者 1234 新增的評論,但是第二個查詢不返回任何東西,因為滯後的從庫還沒有拉取到該寫入內容。實際上可以認為第二個查詢是在比第一個查詢更早的時間點上觀察系統。如果第一個查詢沒有返回任何內容,那問題並不大,因為使用者 2345 可能不知道使用者 1234 最近添加了評論。但如果使用者 2345 先看見使用者 1234 的評論,然後又看到它消失,這就會讓人覺得非常困惑了。
如果使用者從不同從庫進行多次讀取,就可能發生這種情況。例如,[圖 5-4](/img/fig5-4.png) 顯示了使用者 2345 兩次進行相同的查詢,首先查詢了一個延遲很小的從庫,然後是一個延遲較大的從庫(如果使用者重新整理網頁時每個請求都被路由到一個隨機的伺服器,這種情況就很有可能發生)。第一個查詢返回了最近由使用者 1234 新增的評論,但是第二個查詢不返回任何東西,因為滯後的從庫還沒有拉取到該寫入內容。實際上可以認為第二個查詢是在比第一個查詢更早的時間點上觀察系統。如果第一個查詢沒有返回任何內容,那問題並不大,因為使用者 2345 可能不知道使用者 1234 最近添加了評論。但如果使用者 2345 先看見使用者 1234 的評論,然後又看到它消失,這就會讓人覺得非常困惑了。
![](../img/fig5-4.png)
![](/img/fig5-4.png)
**圖 5-4 使用者首先從新副本讀取,然後從舊副本讀取。時間看上去回退了。為了防止這種異常,我們需要單調的讀取。**
@@ -257,7 +259,7 @@
這兩句話之間有因果關係Cake 夫人聽到了 Poons 先生的問題並回答了這個問題。
現在想象第三個人正在透過從庫來聽這個對話。Cake 夫人說的內容是從一個延遲很低的從庫讀取的,但 Poons 先生所說的內容,從庫的延遲要大的多(見 [圖 5-5](../img/fig5-5.png))。於是,這個觀察者會聽到以下內容:
現在想象第三個人正在透過從庫來聽這個對話。Cake 夫人說的內容是從一個延遲很低的從庫讀取的,但 Poons 先生所說的內容,從庫的延遲要大的多(見 [圖 5-5](/img/fig5-5.png))。於是,這個觀察者會聽到以下內容:
*Mrs. Cake*
> 通常約十秒鐘Mr. Poons.
@@ -267,13 +269,13 @@
對於觀察者來說,看起來好像 Cake 夫人在 Poons 先生提問前就回答了這個問題。這種超能力讓人印象深刻但也會把人搞糊塗。【25】。
![](../img/fig5-5.png)
![](/img/fig5-5.png)
**圖 5-5 如果某些分割槽的複製速度慢於其他分割槽,那麼觀察者可能會在看到問題之前先看到答案。**
要防止這種異常,需要另一種型別的保證:**一致字首讀consistent prefix reads**【23】。這個保證的意思是說如果一系列寫入按某個順序發生那麼任何人讀取這些寫入時也會看見它們以同樣的順序出現。
這是 **分割槽partitioned****分片sharded** 資料庫中的一個特殊問題,我們將在 [第六章](ch6.md) 中討論分割槽資料庫。如果資料庫總是以相同的順序應用寫入,而讀取總是看到一致的字首,那麼這種異常不會發生。但是在許多分散式資料庫中,不同的分割槽獨立執行,因此不存在 **全域性的寫入順序**:當用戶從資料庫中讀取資料時,可能會看到資料庫的某些部分處於較舊的狀態,而某些則處於較新的狀態。
這是 **分割槽partitioned****分片sharded** 資料庫中的一個特殊問題,我們將在 [第六章](/tw/ch6) 中討論分割槽資料庫。如果資料庫總是以相同的順序應用寫入,而讀取總是看到一致的字首,那麼這種異常不會發生。但是在許多分散式資料庫中,不同的分割槽獨立執行,因此不存在 **全域性的寫入順序**:當用戶從資料庫中讀取資料時,可能會看到資料庫的某些部分處於較舊的狀態,而某些則處於較新的狀態。
一種解決方案是,確保任何因果相關的寫入都寫入相同的分割槽,但在一些應用中可能無法高效地完成這種操作。還有一些顯式跟蹤因果依賴關係的演算法,我們將在 “[“此前發生” 的關係和併發](#“此前發生”的關係和併發)” 一節中回到這個話題。
@@ -285,7 +287,7 @@
如果應用程式開發人員不必擔心微妙的複製問題,並可以信賴他們的資料庫 “做了正確的事情”,那該多好呀。這就是 **事務transaction** 存在的原因:**資料庫透過事務提供強大的保證**,所以應用程式可以更加簡單。
單節點事務已經存在了很長時間。然而在走向分散式(複製和分割槽)資料庫時,許多系統放棄了事務,聲稱事務在效能和可用性上的代價太高,並斷言在可伸縮系統中最終一致性是不可避免的。這個敘述有一些道理,但過於簡單了,本書其餘部分將提出更為細緻的觀點。我們將在 [第七章](ch7.md) 和 [第九章](ch9.md) 回到事務的話題,並將在 [第三部分](part-iii.md) 討論一些替代機制。
單節點事務已經存在了很長時間。然而在走向分散式(複製和分割槽)資料庫時,許多系統放棄了事務,聲稱事務在效能和可用性上的代價太高,並斷言在可伸縮系統中最終一致性是不可避免的。這個敘述有一些道理,但過於簡單了,本書其餘部分將提出更為細緻的觀點。我們將在 [第七章](/tw/ch7) 和 [第九章](/tw/ch9) 回到事務的話題,並將在 [第三部分](/tw/part-iii) 討論一些替代機制。
## 多主複製
@@ -294,7 +296,7 @@
基於領導者的複製有一個主要的缺點:只有一個主庫,而且所有的寫入都必須透過它 [^iv]。如果出於任何原因(例如和主庫之間的網路連線中斷)無法連線到主庫,就無法向資料庫寫入。
[^iv]: 如果資料庫被分割槽(見 [第六章](ch6.md)),每個分割槽都有一個主庫。不同的分割槽的主庫可能在不同的節點上,但是每個分割槽都必須有一個主庫。
[^iv]: 如果資料庫被分割槽(見 [第六章](/tw/ch6)),每個分割槽都有一個主庫。不同的分割槽的主庫可能在不同的節點上,但是每個分割槽都必須有一個主庫。
基於領導者的複製模型的自然延伸是允許多個節點接受寫入。複製仍然以同樣的方式發生:處理寫入的每個節點都必須將該資料變更轉發給所有其他節點。我們將其稱之為 **多領導者配置**multi-leader configuration也稱多主、多活複製即 master-master replication 或 active/active replication。在這種情況下每個主庫同時是其他主庫的從庫。
@@ -306,9 +308,9 @@
假如你有一個數據庫,副本分散在好幾個不同的資料中心(可能會用來容忍單個數據中心的故障,或者為了在地理上更接近使用者)。如果使用常規的基於領導者的複製設定,主庫必須位於其中一個數據中心,且所有寫入都必須經過該資料中心。
多主配置中可以在每個資料中心都有主庫。[圖 5-6](../img/fig5-6.png) 展示了這個架構。在每個資料中心內使用常規的主從複製;在資料中心之間,每個資料中心的主庫都會將其更改複製到其他資料中心的主庫中。
多主配置中可以在每個資料中心都有主庫。[圖 5-6](/img/fig5-6.png) 展示了這個架構。在每個資料中心內使用常規的主從複製;在資料中心之間,每個資料中心的主庫都會將其更改複製到其他資料中心的主庫中。
![](../img/fig5-6.png)
![](/img/fig5-6.png)
**圖 5-6 跨多個數據中心的多主複製**
@@ -328,7 +330,7 @@
有些資料庫預設情況下支援多主配置,但使用外部工具實現也很常見,例如用於 MySQL 的 Tungsten Replicator 【26】用於 PostgreSQL 的 BDR【27】以及用於 Oracle 的 GoldenGate 【19】。
儘管多主複製有這些優勢,但也有一個很大的缺點:兩個不同的資料中心可能會同時修改相同的資料,寫衝突是必須解決的(如 [圖 5-6](../img/fig5-6.png) 中的 “衝突解決conflict resolution。本書將在 “[處理寫入衝突](#處理寫入衝突)” 中詳細討論這個問題。
儘管多主複製有這些優勢,但也有一個很大的缺點:兩個不同的資料中心可能會同時修改相同的資料,寫衝突是必須解決的(如 [圖 5-6](/img/fig5-6.png) 中的 “衝突解決conflict resolution。本書將在 “[處理寫入衝突](#處理寫入衝突)” 中詳細討論這個問題。
由於多主複製在許多資料庫中都屬於改裝的功能所以常常存在微妙的配置缺陷且經常與其他資料庫功能之間出現意外的反應。比如自增主鍵、觸發器、完整性約束等都可能會有麻煩。因此多主複製往往被認為是危險的領域應儘可能避免【28】。
@@ -356,9 +358,9 @@
多主複製的最大問題是可能發生寫衝突,這意味著需要解決衝突。
例如,考慮一個由兩個使用者同時編輯的維基頁面,如 [圖 5-7](../img/fig5-7.png) 所示。使用者 1 將頁面的標題從 A 更改為 B並且使用者 2 同時將標題從 A 更改為 C。每個使用者的更改已成功應用到其本地主庫。但當非同步複製時會發現衝突【33】。單主資料庫中不會出現此問題。
例如,考慮一個由兩個使用者同時編輯的維基頁面,如 [圖 5-7](/img/fig5-7.png) 所示。使用者 1 將頁面的標題從 A 更改為 B並且使用者 2 同時將標題從 A 更改為 C。每個使用者的更改已成功應用到其本地主庫。但當非同步複製時會發現衝突【33】。單主資料庫中不會出現此問題。
![](../img/fig5-7.png)
![](/img/fig5-7.png)
**圖 5-7 兩個主庫同時更新同一記錄引起的寫入衝突**
@@ -380,7 +382,7 @@
單主資料庫按順序進行寫操作:如果同一個欄位有多個更新,則最後一個寫操作將決定該欄位的最終值。
在多主配置中,沒有明確的寫入順序,所以最終值應該是什麼並不清楚。在 [圖 5-7](../img/fig5-7.png) 中,在主庫 1 中標題首先更新為 B 而後更新為 C在主庫 2 中,首先更新為 C然後更新為 B。兩種順序都不比另一種“更正確”。
在多主配置中,沒有明確的寫入順序,所以最終值應該是什麼並不清楚。在 [圖 5-7](/img/fig5-7.png) 中,在主庫 1 中標題首先更新為 B 而後更新為 C在主庫 2 中,首先更新為 C然後更新為 B。兩種順序都不比另一種“更正確”。
如果每個副本只是按照它看到寫入的順序寫入,那麼資料庫最終將處於不一致的狀態:最終值將是在主庫 1 的 C 和主庫 2 的 B。這是不可接受的每個複製方案都必須確保資料最終在所有副本中都是相同的。因此資料庫必須以一種 **收斂convergent** 的方式解決衝突,這意味著所有副本必須在所有變更復制完成時收斂至一個相同的最終值。
@@ -388,7 +390,7 @@
* 給每個寫入一個唯一的 ID例如時間戳、長隨機數、UUID 或者鍵和值的雜湊),挑選最高 ID 的寫入作為勝利者,並丟棄其他寫入。如果使用時間戳,這種技術被稱為 **最後寫入勝利LWW, last write wins**。雖然這種方法很流行但是很容易造成資料丟失【35】。我們將在本章末尾的 [檢測併發寫入](#檢測併發寫入) 一節更詳細地討論 LWW。
* 為每個副本分配一個唯一的 IDID 編號更高的寫入具有更高的優先順序。這種方法也意味著資料丟失。
* 以某種方式將這些值合併在一起 - 例如,按字母順序排序,然後連線它們(在 [圖 5-7](../img/fig5-7.png) 中,合併的標題可能類似於 “B/C”
* 以某種方式將這些值合併在一起 - 例如,按字母順序排序,然後連線它們(在 [圖 5-7](/img/fig5-7.png) 中,合併的標題可能類似於 “B/C”
* 用一種可保留所有資訊的顯式資料結構來記錄衝突,並編寫解決衝突的應用程式程式碼(也許透過提示使用者的方式)。
@@ -404,7 +406,7 @@
當檢測到衝突時,所有衝突寫入被儲存。下一次讀取資料時,會將這些多個版本的資料返回給應用程式。應用程式可以提示使用者或自動解決衝突,並將結果寫回資料庫。例如 CouchDB 就以這種方式工作。
請注意衝突解決通常適用於單行記錄或單個文件的層面而不是整個事務【36】。因此如果你有一個事務會原子性地進行幾次不同的寫入請參閱 [第七章](ch7.md)),對於衝突解決而言,每個寫入仍需分開單獨考慮。
請注意衝突解決通常適用於單行記錄或單個文件的層面而不是整個事務【36】。因此如果你有一個事務會原子性地進行幾次不同的寫入請參閱 [第七章](/tw/ch7)),對於衝突解決而言,每個寫入仍需分開單獨考慮。
> #### 自動衝突解決
@@ -422,38 +424,38 @@
#### 什麼是衝突?
有些衝突是顯而易見的。在 [圖 5-7](../img/fig5-7.png) 的例子中,兩個寫操作併發地修改了同一條記錄中的同一個欄位,並將其設定為兩個不同的值。毫無疑問這是一個衝突。
有些衝突是顯而易見的。在 [圖 5-7](/img/fig5-7.png) 的例子中,兩個寫操作併發地修改了同一條記錄中的同一個欄位,並將其設定為兩個不同的值。毫無疑問這是一個衝突。
其他型別的衝突可能更為微妙而難以發現。例如,考慮一個會議室預訂系統:它記錄誰訂了哪個時間段的哪個房間。應用程式需要確保每個房間在任意時刻都只能被一組人進行預定(即不得有相同房間的重疊預訂)。在這種情況下,如果為同一個房間同時建立兩個不同的預訂,則可能會發生衝突。即使應用程式在允許使用者進行預訂之前先檢查會議室的可用性,如果兩次預訂是由兩個不同的主庫進行的,則仍然可能會有衝突。
雖然現在還沒有一個現成的答案,但在接下來的章節中,我們將更好地瞭解這個問題。我們將在 [第七章](ch7.md) 中看到更多的衝突示例,在 [第十二章](ch12.md) 中我們將討論用於檢測和解決複製系統中衝突的可伸縮方法。
雖然現在還沒有一個現成的答案,但在接下來的章節中,我們將更好地瞭解這個問題。我們將在 [第七章](/tw/ch7) 中看到更多的衝突示例,在 [第十二章](/tw/ch12) 中我們將討論用於檢測和解決複製系統中衝突的可伸縮方法。
### 多主複製拓撲
**複製拓撲**replication topology用來描述寫入操作從一個節點傳播到另一個節點的通訊路徑。如果你有兩個主庫如 [圖 5-7](../img/fig5-7.png) 所示,只有一個合理的拓撲結構:主庫 1 必須把它所有的寫入都發送到主庫 2反之亦然。當有兩個以上的主庫多種不同的拓撲都是可能的。[圖 5-8](../img/fig5-8.png) 舉例說明了一些例子。
**複製拓撲**replication topology用來描述寫入操作從一個節點傳播到另一個節點的通訊路徑。如果你有兩個主庫如 [圖 5-7](/img/fig5-7.png) 所示,只有一個合理的拓撲結構:主庫 1 必須把它所有的寫入都發送到主庫 2反之亦然。當有兩個以上的主庫多種不同的拓撲都是可能的。[圖 5-8](/img/fig5-8.png) 舉例說明了一些例子。
![](../img/fig5-8.png)
![](/img/fig5-8.png)
**圖 5-8 三種可以在多主複製中使用的拓撲示例。**
最常見的拓撲是全部到全部all-to-all如 [圖 5-8 (c)](../img/fig5-8.png)),其中每個主庫都將其寫入傳送給其他所有的主庫。然而,一些更受限的拓撲也會被使用到:例如,預設情況下 MySQL 僅支援 **環形拓撲circular topology**【34】其中每個節點都從一個節點接收寫入並將這些寫入加上自己的寫入轉發給另一個節點。另一種流行的拓撲結構具有星形的形狀 [^v]:一個指定的根節點將寫入轉發給所有其他節點。星形拓撲可以推廣到樹。
最常見的拓撲是全部到全部all-to-all如 [圖 5-8 (c)](/img/fig5-8.png)),其中每個主庫都將其寫入傳送給其他所有的主庫。然而,一些更受限的拓撲也會被使用到:例如,預設情況下 MySQL 僅支援 **環形拓撲circular topology**【34】其中每個節點都從一個節點接收寫入並將這些寫入加上自己的寫入轉發給另一個節點。另一種流行的拓撲結構具有星形的形狀 [^v]:一個指定的根節點將寫入轉發給所有其他節點。星形拓撲可以推廣到樹。
[^v]: 不要與星型模式混淆(請參閱 “[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”),其中描述了資料模型的結構,而不是節點之間的通訊拓撲。
[^v]: 不要與星型模式混淆(請參閱 “[星型和雪花型:分析的模式](/tw/ch3#星型和雪花型:分析的模式)”),其中描述了資料模型的結構,而不是節點之間的通訊拓撲。
在環形和星形拓撲中寫入可能需要在到達所有副本之前透過多個節點。因此節點需要轉發從其他節點收到的資料更改。為了防止無限複製迴圈每個節點被賦予一個唯一的識別符號並且在複製日誌中每次寫入都會使用其經過的所有節點的識別符號進行標記【43】。當一個節點收到用自己的識別符號標記的資料更改時該資料更改將被忽略因為節點知道它已經被處理過。
環形和星形拓撲的問題是,如果只有一個節點發生故障,則可能會中斷其他節點之間的複製訊息流,導致它們無法通訊,除非節點被修復。拓撲結構可以重新配置為跳過發生故障的節點,但在大多數部署中,這種重新配置必須手動完成。更密集連線的拓撲結構(例如全部到全部)的容錯性更好,因為它允許訊息沿著不同的路徑傳播,可以避免單點故障。
另一方面,全部到全部的拓撲也可能有問題。特別是,一些網路連結可能比其他網路連結更快(例如由於網路擁塞),結果是一些複製訊息可能 “超越” 其他複製訊息,如 [圖 5-9](../img/fig5-9.png) 所示。
另一方面,全部到全部的拓撲也可能有問題。特別是,一些網路連結可能比其他網路連結更快(例如由於網路擁塞),結果是一些複製訊息可能 “超越” 其他複製訊息,如 [圖 5-9](/img/fig5-9.png) 所示。
![](../img/fig5-9.png)
![](/img/fig5-9.png)
**圖 5-9 使用多主複製時,寫入可能會以錯誤的順序到達某些副本。**
在 [圖 5-9](../img/fig5-9.png) 中,客戶端 A 向主庫 1 的表中插入一行,客戶端 B 在主庫 3 上更新該行。然而,主庫 2 可以以不同的順序接收寫入:它可能先接收到更新(從它的角度來看,是對資料庫中不存在的行的更新),稍後才接收到相應的插入(其應該在更新之前)。
在 [圖 5-9](/img/fig5-9.png) 中,客戶端 A 向主庫 1 的表中插入一行,客戶端 B 在主庫 3 上更新該行。然而,主庫 2 可以以不同的順序接收寫入:它可能先接收到更新(從它的角度來看,是對資料庫中不存在的行的更新),稍後才接收到相應的插入(其應該在更新之前)。
這是一個因果關係的問題,類似於我們在 “[一致字首讀](#一致字首讀)” 中看到的:更新取決於先前的插入,所以我們需要確保所有節點先處理插入,然後再處理更新。僅僅在每一次寫入時新增一個時間戳是不夠的,因為時鐘不可能被充分地同步,所以主庫 2 就無法正確地對這些事件進行排序(見 [第八章](ch8.md))。
這是一個因果關係的問題,類似於我們在 “[一致字首讀](#一致字首讀)” 中看到的:更新取決於先前的插入,所以我們需要確保所有節點先處理插入,然後再處理更新。僅僅在每一次寫入時新增一個時間戳是不夠的,因為時鐘不可能被充分地同步,所以主庫 2 就無法正確地對這些事件進行排序(見 [第八章](/tw/ch8))。
要正確排序這些事件,可以使用一種稱為 **版本向量version vectors** 的技術,本章稍後將討論這種技術(請參閱 “[檢測併發寫入](#檢測併發寫入)”。然而許多多主複製系統中的衝突檢測技術實現得並不好。例如在撰寫本文時PostgreSQL BDR 不提供寫入的因果排序【27】而 Tungsten Replicator for MySQL 甚至都不做檢測衝突【34】。
@@ -474,9 +476,9 @@
假設你有一個帶有三個副本的資料庫,而其中一個副本目前不可用,或許正在重新啟動以安裝系統更新。在基於領導者的配置中,如果要繼續處理寫入,則可能需要執行故障切換(請參閱「[處理節點宕機](#處理節點宕機)」)。
另一方面,在無主配置中,不存在故障轉移。[圖 5-10](../img/fig5-10.png) 演示了會發生了什麼事情:客戶端(使用者 1234並行傳送寫入到所有三個副本並且兩個可用副本接受寫入但是不可用副本錯過了它。假設三個副本中的兩個承認寫入是足夠的在使用者 1234 已經收到兩個確定的響應之後,我們認為寫入成功。客戶簡單地忽略了其中一個副本錯過了寫入的事實。
另一方面,在無主配置中,不存在故障轉移。[圖 5-10](/img/fig5-10.png) 演示了會發生了什麼事情:客戶端(使用者 1234並行傳送寫入到所有三個副本並且兩個可用副本接受寫入但是不可用副本錯過了它。假設三個副本中的兩個承認寫入是足夠的在使用者 1234 已經收到兩個確定的響應之後,我們認為寫入成功。客戶簡單地忽略了其中一個副本錯過了寫入的事實。
![](../img/fig5-10.png)
![](/img/fig5-10.png)
**圖 5-10 法定寫入,法定讀取,並在節點中斷後讀修復。**
@@ -492,7 +494,7 @@
* 讀修復Read repair
當客戶端並行讀取多個節點時,它可以檢測到任何陳舊的響應。例如,在 [圖 5-10](../img/fig5-10.png) 中,使用者 2345 獲得了來自副本 3 的版本 6 值和來自副本 1 和 2 的版本 7 值。客戶端發現副本 3 具有陳舊值,並將新值寫回到該副本。這種方法適用於讀頻繁的值。
當客戶端並行讀取多個節點時,它可以檢測到任何陳舊的響應。例如,在 [圖 5-10](/img/fig5-10.png) 中,使用者 2345 獲得了來自副本 3 的版本 6 值和來自副本 1 和 2 的版本 7 值。客戶端發現副本 3 具有陳舊值,並將新值寫回到該副本。這種方法適用於讀頻繁的值。
* 反熵過程Anti-entropy process
@@ -502,7 +504,7 @@
#### 讀寫的法定人數
在 [圖 5-10](../img/fig5-10.png) 的示例中,我們認為即使僅在三個副本中的兩個上進行處理,寫入仍然是成功的。如果三個副本中只有一個接受了寫入,會怎樣?以此類推,究竟多少個副本完成才可以認為寫入成功?
在 [圖 5-10](/img/fig5-10.png) 的示例中,我們認為即使僅在三個副本中的兩個上進行處理,寫入仍然是成功的。如果三個副本中只有一個接受了寫入,會怎樣?以此類推,究竟多少個副本完成才可以認為寫入成功?
如果我們知道,每個成功的寫操作意味著在三個副本中至少有兩個出現,這意味著至多有一個副本可能是陳舊的。因此,如果我們從至少兩個副本讀取,我們可以確定至少有一個是最新的。如果第三個副本停機或響應速度緩慢,則讀取仍可以繼續返回最新值。
@@ -512,17 +514,17 @@
在 Dynamo 風格的資料庫中,引數 n、w 和 r 通常是可配置的。一個常見的選擇是使 n 為奇數(通常為 3 或 5並設定 $w = r = (n + 1) / 2$(向上取整)。但是你可以根據需要更改數字。例如,寫入次數較少且讀取次數較多的工作負載可以從設定 $w = n$ 和 $r = 1$中受益。這會使得讀取速度更快,但缺點是隻要有一個不可用的節點就會導致所有的資料庫寫入都失敗。
> 叢集中可能有多於 n 個的節點(叢集的機器數可能多於副本數目)。但是任何給定的值只能儲存在 n 個節點上。這允許對資料集進行分割槽,從而可以支援比單個節點的儲存能力更大的資料集。我們將在 [第六章](ch6.md) 繼續討論分割槽。
> 叢集中可能有多於 n 個的節點(叢集的機器數可能多於副本數目)。但是任何給定的值只能儲存在 n 個節點上。這允許對資料集進行分割槽,從而可以支援比單個節點的儲存能力更大的資料集。我們將在 [第六章](/tw/ch6) 繼續討論分割槽。
法定人數條件 $w + r > n$ 允許系統容忍不可用的節點,如下所示:
* 如果 $w < n$,當節點不可用時,我們仍然可以處理寫入。
* 如果 $r < n$,當節點不可用時,我們仍然可以處理讀取。
* 對於 $n = 3w = 2r = 2$,我們可以容忍一個不可用的節點。
* 對於 $n = 5w = 3r = 3$,我們可以容忍兩個不可用的節點。這個案例如 [圖 5-11](../img/fig5-11.png) 所示。
* 對於 $n = 5w = 3r = 3$,我們可以容忍兩個不可用的節點。這個案例如 [圖 5-11](/img/fig5-11.png) 所示。
* 通常,讀取和寫入操作始終並行傳送到所有 n 個副本。引數 w 和 r 決定我們等待多少個節點,即在我們認為讀或寫成功之前,有多少個節點需要報告成功。
![](../img/fig5-11.png)
![](/img/fig5-11.png)
**圖 5-11 如果 $w + r > n$,讀取 r 個副本,至少有一個副本必然包含了最近的成功寫入。**
@@ -531,7 +533,7 @@
### 法定人數一致性的侷限性
如果你有 n 個副本,並且你選擇了滿足 $w + r > n$ 的 w 和 r你通常可以期望每次讀取都能返回最近寫入的值。情況就是這樣因為你寫入的節點集合和你讀取的節點集合必然有重疊。也就是說你讀取的節點中必然至少有一個節點具有最新值如 [圖 5-11](../img/fig5-11.png) 所示)。
如果你有 n 個副本,並且你選擇了滿足 $w + r > n$ 的 w 和 r你通常可以期望每次讀取都能返回最近寫入的值。情況就是這樣因為你寫入的節點集合和你讀取的節點集合必然有重疊。也就是說你讀取的節點中必然至少有一個節點具有最新值如 [圖 5-11](/img/fig5-11.png) 所示)。
通常r 和 w 被選為多數(超過 $n/2$ )節點,因為這確保了 $w + r > n$,同時仍然容忍多達 $n/2$ 個節點故障。但是法定人數不一定必須是大多數重要的是讀寫使用的節點至少有一個節點的交集。其他法定人數的配置是可能的這使得分散式演算法的設計有一定的靈活性【45】。
@@ -546,11 +548,11 @@
* 如果寫操作與讀操作同時發生,寫操作可能僅反映在某些副本上。在這種情況下,不確定讀取返回的是舊值還是新值。
* 如果寫操作在某些副本上成功,而在其他節點上失敗(例如,因為某些節點上的磁碟已滿),在小於 w 個副本上寫入成功。所以整體判定寫入失敗但整體寫入失敗並沒有在寫入成功的副本上回滾。這意味著一個寫入雖然報告失敗後續的讀取仍然可能會讀取這次失敗寫入的值【47】。
* 如果攜帶新值的節點發生故障,需要從其他帶有舊值的副本進行恢復,則儲存新值的副本數可能會低於 w從而打破法定人數條件。
* 即使一切工作正常,有時也會不幸地出現關於 **時序timing** 的邊緣情況,我們將在 “[線性一致性和法定人數](ch9.md#線性一致性和法定人數)” 中看到這點。
* 即使一切工作正常,有時也會不幸地出現關於 **時序timing** 的邊緣情況,我們將在 “[線性一致性和法定人數](/tw/ch9#線性一致性和法定人數)” 中看到這點。
因此儘管法定人數似乎保證讀取返回最新的寫入值但在實踐中並不那麼簡單。Dynamo 風格的資料庫通常針對可以忍受最終一致性的用例進行最佳化。你可以透過引數 w 和 r 來調整讀取到陳舊值的機率,但把它們當成絕對的保證是不明智的。
尤其是,因為通常得不到 “[複製延遲問題](#複製延遲問題)” 中討論的那些保證(讀己之寫,單調讀,一致字首讀),前面提到的異常可能會發生在應用程式中。更強有力的保證通常需要 **事務****共識**。我們將在 [第七章](ch7.md) 和 [第九章](ch9.md) 回到這些話題。
尤其是,因為通常得不到 “[複製延遲問題](#複製延遲問題)” 中討論的那些保證(讀己之寫,單調讀,一致字首讀),前面提到的異常可能會發生在應用程式中。更強有力的保證通常需要 **事務****共識**。我們將在 [第七章](/tw/ch7) 和 [第九章](/tw/ch9) 回到這些話題。
#### 監控陳舊度
@@ -595,17 +597,17 @@ Riak 將客戶端和資料庫節點之間的所有通訊保持在一個本地的
Dynamo 風格的資料庫允許多個客戶端同時寫入相同的鍵Key這意味著即使使用嚴格的法定人數也會發生衝突。這種情況與多主複製相似請參閱 “[處理寫入衝突](#處理寫入衝突)”),但在 Dynamo 風格的資料庫中,在 **讀修復****提示移交** 期間也可能會產生衝突。
其問題在於,由於可變的網路延遲和部分節點的故障,事件可能以不同的順序到達不同的節點。例如,[圖 5-12](../img/fig5-12.png) 顯示了兩個客戶機 A 和 B 同時寫入三節點資料儲存中的鍵 X
其問題在於,由於可變的網路延遲和部分節點的故障,事件可能以不同的順序到達不同的節點。例如,[圖 5-12](/img/fig5-12.png) 顯示了兩個客戶機 A 和 B 同時寫入三節點資料儲存中的鍵 X
* 節點 1 接收來自 A 的寫入,但由於暫時中斷,未接收到來自 B 的寫入。
* 節點 2 首先接收來自 A 的寫入,然後接收來自 B 的寫入。
* 節點 3 首先接收來自 B 的寫入,然後從 A 寫入。
![](../img/fig5-12.png)
![](/img/fig5-12.png)
**圖 5-12 併發寫入 Dynamo 風格的資料儲存:沒有明確定義的順序。**
如果每個節點只要接收到來自客戶端的寫入請求就簡單地覆寫某個鍵值,那麼節點就會永久地不一致,如 [圖 5-12](../img/fig5-12.png) 中的最終獲取請求所示:節點 2 認為 X 的最終值是 B而其他節點認為值是 A 。
如果每個節點只要接收到來自客戶端的寫入請求就簡單地覆寫某個鍵值,那麼節點就會永久地不一致,如 [圖 5-12](/img/fig5-12.png) 中的最終獲取請求所示:節點 2 認為 X 的最終值是 B而其他節點認為值是 A 。
為了最終達成一致,副本應該趨於相同的值。如何做到這一點?有人可能希望複製的資料庫能夠自動處理,但不幸的是,大多數的實現都很糟糕:如果你想避免丟失資料,你(應用程式開發人員)需要知道很多有關資料庫衝突處理的內部資訊。
@@ -615,11 +617,11 @@ Dynamo 風格的資料庫允許多個客戶端同時寫入相同的鍵Key
實現最終收斂的一種方法是宣告每個副本只需要儲存 **“最近”** 的值,並允許 **“更舊”** 的值被覆蓋和拋棄。然後,只要我們有一種明確的方式來確定哪個寫是 “最近的”,並且每個寫入最終都被複制到每個副本,那麼複製最終會收斂到相同的值。
正如 **“最近”** 的引號所表明的,這個想法其實頗具誤導性。在 [圖 5-12](../img/fig5-12.png) 的例子中,當客戶端向資料庫節點發送寫入請求時,兩個客戶端都不知道另一個客戶端,因此不清楚哪一個先發送請求。事實上,說這兩種情況誰先發送請求是沒有意義的:既然我們說寫入是 **併發concurrent** 的,那麼它們的順序就是不確定的。
正如 **“最近”** 的引號所表明的,這個想法其實頗具誤導性。在 [圖 5-12](/img/fig5-12.png) 的例子中,當客戶端向資料庫節點發送寫入請求時,兩個客戶端都不知道另一個客戶端,因此不清楚哪一個先發送請求。事實上,說這兩種情況誰先發送請求是沒有意義的:既然我們說寫入是 **併發concurrent** 的,那麼它們的順序就是不確定的。
即使寫入沒有自然的排序,我們也可以強制進行排序。例如,可以為每個寫入附加一個時間戳,然後挑選最大的時間戳作為 **“最近的”**,並丟棄具有較早時間戳的任何寫入。這種衝突解決演算法被稱為 **最後寫入勝利LWW, last write wins**,是 Cassandra 唯一支援的衝突解決方法【53】也是 Riak 中的一個可選特徵【35】。
LWW 實現了最終收斂的目標,但以 **永續性** 為代價:如果同一個鍵有多個併發寫入,即使它們反饋給客戶端的結果都是成功的(因為它們被寫入 w 個副本也只有一個寫入將被保留而其他寫入將被默默地丟棄。此外LWW 甚至可能會丟棄不是併發的寫入,我們將在 “[有序事件的時間戳](ch8.md#有序事件的時間戳)” 中進行討論。
LWW 實現了最終收斂的目標,但以 **永續性** 為代價:如果同一個鍵有多個併發寫入,即使它們反饋給客戶端的結果都是成功的(因為它們被寫入 w 個副本也只有一個寫入將被保留而其他寫入將被默默地丟棄。此外LWW 甚至可能會丟棄不是併發的寫入,我們將在 “[有序事件的時間戳](/tw/ch8#有序事件的時間戳)” 中進行討論。
在類似快取的一些情況下寫入丟失可能是可以接受的。但如果資料丟失不可接受LWW 是解決衝突的一個很爛的選擇。
@@ -639,7 +641,7 @@ LWW 實現了最終收斂的目標,但以 **永續性** 為代價:如果同
> #### 併發性、時間和相對性
>
> 如果兩個操作 **“同時”** 發生,似乎應該稱為併發 —— 但事實上,它們在字面時間上重疊與否並不重要。由於分散式系統中的時鐘問題,現實中是很難判斷兩個事件是否是 **同時** 發生的,這個問題我們將在 [第八章](ch8.md) 中詳細討論。
> 如果兩個操作 **“同時”** 發生,似乎應該稱為併發 —— 但事實上,它們在字面時間上重疊與否並不重要。由於分散式系統中的時鐘問題,現實中是很難判斷兩個事件是否是 **同時** 發生的,這個問題我們將在 [第八章](/tw/ch8) 中詳細討論。
>
> 為了定義併發性,確切的時間並不重要:如果兩個操作都意識不到對方的存在,就稱這兩個操作 **併發**而不管它們實際發生的物理時間。人們有時把這個原理和物理學中的狹義相對論聯絡起來【54】該理論引入了資訊不能比光速更快的思想。因此如果兩個事件發生的時間差小於光透過它們之間的距離所需要的時間那麼這兩個事件不可能相互影響。
>
@@ -650,7 +652,7 @@ LWW 實現了最終收斂的目標,但以 **永續性** 為代價:如果同
我們來看一個演算法,它可以確定兩個操作是否為併發的,還是一個在另一個之前。簡單起見,我們從一個只有一個副本的資料庫開始。一旦我們知道了如何在單個副本上完成這項工作,我們可以將該方法推廣到具有多個副本的無主資料庫。
[圖 5-13](../img/fig5-13.png) 顯示了兩個客戶端同時向同一購物車新增專案。(如果這樣的例子讓你覺得無趣,那麼可以想象一下兩個空中交通管制員同時把飛機新增到他們正在跟蹤的區域。)最初,購物車是空的。然後客戶端向資料庫發出五次寫入:
[圖 5-13](/img/fig5-13.png) 顯示了兩個客戶端同時向同一購物車新增專案。(如果這樣的例子讓你覺得無趣,那麼可以想象一下兩個空中交通管制員同時把飛機新增到他們正在跟蹤的區域。)最初,購物車是空的。然後客戶端向資料庫發出五次寫入:
1. 客戶端 1 將牛奶加入購物車。這是該鍵的第一次寫入,伺服器成功儲存了它併為其分配版本號 1最後將值與版本號一起回送給客戶端。
2. 客戶端 2 將雞蛋加入購物車,不知道客戶端 1 同時添加了牛奶(客戶端 2 認為它的雞蛋是購物車中的唯一物品)。伺服器為此寫入分配版本號 2並將雞蛋和牛奶儲存為兩個單獨的值。然後它將這兩個值 **都** 返回給客戶端 2 ,並附上版本號 2。
@@ -658,13 +660,13 @@ LWW 實現了最終收斂的目標,但以 **永續性** 為代價:如果同
4. 同時,客戶端 2 想要加入火腿,不知道客戶端 1 剛剛加了麵粉。客戶端 2 在最近一次響應中從伺服器收到了兩個值 [牛奶] 和 [雞蛋],所以客戶端 2 現在合併這些值,並新增火腿形成一個新的值 [雞蛋,牛奶,火腿]。它將這個值傳送到伺服器,帶著之前的版本號 2 。伺服器檢測到新值會覆蓋版本 2 的值 [雞蛋],但新值也會與版本 3 的值 [牛奶,麵粉] **併發**,所以剩下的兩個值是版本 3 的 [牛奶,麵粉],和版本 4 的 [雞蛋,牛奶,火腿]。
5. 最後,客戶端 1 想要加培根。它之前從伺服器接收到了版本 3 的 [牛奶,麵粉] 和 [雞蛋],所以它合併這些,新增培根,並將最終值 [牛奶,麵粉,雞蛋,培根] 連同版本號 3 發往伺服器。這會覆蓋版本 3 的值 [牛奶,麵粉](請注意 [雞蛋] 已經在上一步被覆蓋),但與版本 4 的值 [雞蛋,牛奶,火腿] 併發,所以伺服器將保留這兩個併發值。
![](../img/fig5-13.png)
![](/img/fig5-13.png)
**圖 5-13 在同時編輯購物車時捕獲兩個客戶端之間的因果關係。**
[圖 5-13](../img/fig5-13.png) 中的操作之間的資料流如 [圖 5-14](../img/fig5-14.png) 所示。箭頭表示哪個操作發生在其他操作之前,意味著後面的操作知道或依賴於較早的操作。在這個例子中,客戶端永遠不會完全拿到伺服器上的最新資料,因為總是有另一個操作同時進行。但是舊版本的值最終會被覆蓋,並且不會丟失任何寫入。
[圖 5-13](/img/fig5-13.png) 中的操作之間的資料流如 [圖 5-14](/img/fig5-14.png) 所示。箭頭表示哪個操作發生在其他操作之前,意味著後面的操作知道或依賴於較早的操作。在這個例子中,客戶端永遠不會完全拿到伺服器上的最新資料,因為總是有另一個操作同時進行。但是舊版本的值最終會被覆蓋,並且不會丟失任何寫入。
![](../img/fig5-14.png)
![](/img/fig5-14.png)
**圖 5-14 圖 5-13 中的因果依賴關係圖。**
@@ -683,21 +685,21 @@ LWW 實現了最終收斂的目標,但以 **永續性** 為代價:如果同
合併併發值,本質上是與多主複製中的衝突解決問題相同,我們先前討論過(請參閱 “[處理寫入衝突](#處理寫入衝突)”)。一個簡單的方法是根據版本號或時間戳(最後寫入勝利)來選擇一個值,但這意味著丟失資料。所以,你可能需要在應用程式程式碼中額外做些更聰明的事情。
以購物車為例,一種合理的合併值的方法就是做並集。在 [圖 5-14](../img/fig5-14.png) 中,最後的兩個兄弟是 [牛奶,麵粉,雞蛋,培根] 和 [雞蛋,牛奶,火腿]。注意牛奶和雞蛋雖然同時出現在兩個併發值裡,但他們每個只被寫過一次。合併的值可以是 [牛奶,麵粉,雞蛋,培根,火腿],不再有重複了。
以購物車為例,一種合理的合併值的方法就是做並集。在 [圖 5-14](/img/fig5-14.png) 中,最後的兩個兄弟是 [牛奶,麵粉,雞蛋,培根] 和 [雞蛋,牛奶,火腿]。注意牛奶和雞蛋雖然同時出現在兩個併發值裡,但他們每個只被寫過一次。合併的值可以是 [牛奶,麵粉,雞蛋,培根,火腿],不再有重複了。
然而,如果你想讓人們也可以從他們的購物車中 **移除** 東西而不是僅僅新增東西那麼把併發值做並集可能不會產生正確的結果如果你合併了兩個客戶端的購物車並且只在其中一個客戶端裡面移除了一個專案那麼被移除的專案將會重新出現在這兩個客戶端的交集結果中【37】。為了防止這個問題要移除一個專案時不能簡單地直接從資料庫中刪除相反系統必須留下一個具有適當版本號的標記以在兄弟合併時表明該專案已被移除。這種刪除標記被稱為 **墓碑tombstone**(我們上一次看到墓碑是在 “[雜湊索引”](ch3.md#雜湊索引) 章節的日誌壓縮部分)。
然而,如果你想讓人們也可以從他們的購物車中 **移除** 東西而不是僅僅新增東西那麼把併發值做並集可能不會產生正確的結果如果你合併了兩個客戶端的購物車並且只在其中一個客戶端裡面移除了一個專案那麼被移除的專案將會重新出現在這兩個客戶端的交集結果中【37】。為了防止這個問題要移除一個專案時不能簡單地直接從資料庫中刪除相反系統必須留下一個具有適當版本號的標記以在兄弟合併時表明該專案已被移除。這種刪除標記被稱為 **墓碑tombstone**(我們上一次看到墓碑是在 “[雜湊索引”](/tw/ch3#雜湊索引) 章節的日誌壓縮部分)。
因為在應用程式程式碼中做兄弟合併是複雜且容易出錯的,所以有一些資料結構被設計出來用於自動執行這種合併,比如在 “[自動衝突解決](#自動衝突解決)” 中討論過的那些。舉例來說Riak 的資料型別就支援使用稱為 CRDT 【38,39,55】的能以合理方式自動進行兄弟合併的資料結構家族包括對保留刪除的支援。
#### 版本向量
[圖 5-13](../img/fig5-13.png) 中的示例只使用了一個副本。當有多個副本但又沒有主庫時,演算法該如何修改?
[圖 5-13](/img/fig5-13.png) 中的示例只使用了一個副本。當有多個副本但又沒有主庫時,演算法該如何修改?
[圖 5-13](../img/fig5-13.png) 使用單個版本號來捕獲操作之間的依賴關係,但是當多個副本併發接受寫入時,這是不夠的。相反,除了對每個鍵,我們還需要對 **每個副本** 使用版本號。每個副本在處理寫入時增加自己的版本號,並且跟蹤從其他副本中看到的版本號。這個資訊指出了要覆蓋哪些併發值,以及要保留哪些併發值或兄弟值。
[圖 5-13](/img/fig5-13.png) 使用單個版本號來捕獲操作之間的依賴關係,但是當多個副本併發接受寫入時,這是不夠的。相反,除了對每個鍵,我們還需要對 **每個副本** 使用版本號。每個副本在處理寫入時增加自己的版本號,並且跟蹤從其他副本中看到的版本號。這個資訊指出了要覆蓋哪些併發值,以及要保留哪些併發值或兄弟值。
所有副本的版本號集合稱為 **版本向量version vector**【56】。這個想法的一些變體正在被使用但最有趣的可能是在 Riak 2.0 【58,59】中使用的 **虛線版本向量dotted version vector**【57】。我們不會深入細節但是它的工作方式與我們在購物車示例中看到的非常相似。
與 [圖 5-13](../img/fig5-13.png) 中的版本號一樣當讀取值時版本向量會從資料庫副本傳送到客戶端並且隨後寫入值時需要將其傳送回資料庫。Riak 將版本向量編碼為一個字串,並稱其為 **因果上下文**,即 causal context。版本向量允許資料庫區分覆蓋寫入和併發寫入。
與 [圖 5-13](/img/fig5-13.png) 中的版本號一樣當讀取值時版本向量會從資料庫副本傳送到客戶端並且隨後寫入值時需要將其傳送回資料庫。Riak 將版本向量編碼為一個字串,並稱其為 **因果上下文**,即 causal context。版本向量允許資料庫區分覆蓋寫入和併發寫入。
另外,就像在單個副本中的情況一樣,應用程式可能需要合併併發值。版本向量結構能夠確保從一個副本讀取並隨後寫回到另一個副本是安全的。這樣做雖然可能會在其他副本上面建立資料,但只要能正確合併就不會丟失資料。
@@ -829,9 +831,3 @@ LWW 實現了最終收斂的目標,但以 **永續性** 為代價:如果同
1. Russell Brown: “[Vector Clocks Revisited Part 2: Dotted Version Vectors](https://riak.com/posts/technical/vector-clocks-revisited-part-2-dotted-version-vectors/),” *basho.com*, November 10, 2015.
1. Carlos Baquero: “[Version Vectors Are Not Vector Clocks](https://haslab.wordpress.com/2011/07/08/version-vectors-are-not-vector-clocks/),” *haslab.wordpress.com*, July 8, 2011.
1. Reinhard Schwarz and Friedemann Mattern: “[Detecting Causal Relationships in Distributed Computations: In Search of the Holy Grail](http://dcg.ethz.ch/lectures/hs08/seminar/papers/mattern4.pdf),” *Distributed Computing*, volume 7, number 3, pages 149174, March 1994. [doi:10.1007/BF02277859](http://dx.doi.org/10.1007/BF02277859)
--------
| 上一章 | 目錄 | 下一章 |
| :--------------------------------: | :-----------------------------: | :--------------------: |
| [第二部分:分散式資料](part-ii.md) | [設計資料密集型應用](README.md) | [第六章:分割槽](ch6.md) |

View File

@@ -1,19 +1,21 @@
# 第六章:分割槽
---
linktitle: "第六章:分割槽"
linkTitle: "6. 分割槽"
weight: 206
breadcrumbs: false
---
![](../img/ch6.png)
![](/img/ch6.png)
> 我們必須跳出電腦指令序列的窠臼。敘述定義、描述元資料、梳理關係,而不是編寫過程。
>
> —— Grace Murray Hopper未來的計算機及其管理1962
>
-------------
[TOC]
在 [第五章](/tw/ch5) 中,我們討論了複製 —— 即資料在不同節點上的副本,對於非常大的資料集,或非常高的吞吐量,僅僅進行復制是不夠的:我們需要將資料進行 **分割槽partitions**,也稱為 **分片sharding**[^i]。
在 [第五章](ch5.md) 中,我們討論了複製 —— 即資料在不同節點上的副本,對於非常大的資料集,或非常高的吞吐量,僅僅進行復制是不夠的:我們需要將資料進行 **分割槽partitions**,也稱為 **分片sharding**[^i]
[^i]: 正如本章所討論的,分割槽是一種有意將大型資料庫分解成小型資料庫的方式。它與 **網路分割槽network partitions, netsplits** 無關,這是節點之間網路故障的一種。我們將在 [第八章](ch8.md) 討論這些錯誤。
[^i]: 正如本章所討論的,分割槽是一種有意將大型資料庫分解成小型資料庫的方式。它與 **網路分割槽(network partitions, netsplits** 無關,這是節點之間網路故障的一種。我們將在 [第八章](/tw/ch8) 討論這些錯誤
> #### 術語澄清
>
@@ -22,11 +24,11 @@
通常情況下,每條資料(每條記錄,每行或每個文件)屬於且僅屬於一個分割槽。有很多方法可以實現這一點,本章將進行深入討論。實際上,每個分割槽都是自己的小型資料庫,儘管資料庫可能支援同時進行多個分割槽的操作。
分割槽主要是為了 **可伸縮性**。不同的分割槽可以放在不共享叢集中的不同節點上(請參閱 [第二部分](part-ii.md) 關於 [無共享架構](part-ii.md#無共享架構) 的定義)。因此,大資料集可以分佈在多個磁碟上,並且查詢負載可以分佈在多個處理器上。
分割槽主要是為了 **可伸縮性**。不同的分割槽可以放在不共享叢集中的不同節點上(請參閱 [第二部分](/tw/part-ii) 關於 [無共享架構](/tw/part-ii#無共享架構) 的定義)。因此,大資料集可以分佈在多個磁碟上,並且查詢負載可以分佈在多個處理器上。
對於在單個分割槽上執行的查詢,每個節點可以獨立執行對自己的查詢,因此可以透過新增更多的節點來擴大查詢吞吐量。大型,複雜的查詢可能會跨越多個節點並行處理,儘管這也帶來了新的困難。
分割槽資料庫在 20 世紀 80 年代由 Teradata 和 NonStop SQL【1】等產品率先推出最近因為 NoSQL 資料庫和基於 Hadoop 的資料倉庫重新被關注。有些系統是為事務性工作設計的,有些系統則用於分析(請參閱 “[事務處理還是分析](ch3.md#事務處理還是分析?)”):這種差異會影響系統的運作方式,但是分割槽的基本原理均適用於這兩種工作方式。
分割槽資料庫在 20 世紀 80 年代由 Teradata 和 NonStop SQL【1】等產品率先推出最近因為 NoSQL 資料庫和基於 Hadoop 的資料倉庫重新被關注。有些系統是為事務性工作設計的,有些系統則用於分析(請參閱 “[事務處理還是分析](/tw/ch3#事務處理還是分析?)”):這種差異會影響系統的運作方式,但是分割槽的基本原理均適用於這兩種工作方式。
在本章中,我們將首先介紹分割大型資料集的不同方法,並觀察索引如何與分割槽配合。然後我們將討論 [分割槽再平衡rebalancing](#分割槽再平衡),如果想要新增或刪除叢集中的節點,則必須進行再平衡。最後,我們將概述資料庫如何將請求路由到正確的分割槽並執行查詢。
@@ -34,11 +36,11 @@
分割槽通常與複製結合使用,使得每個分割槽的副本儲存在多個節點上。這意味著,即使每條記錄屬於一個分割槽,它仍然可以儲存在多個不同的節點上以獲得容錯能力。
一個節點可能儲存多個分割槽。如果使用主從複製模型,則分割槽和複製的組合如 [圖 6-1](../img/fig6-1.png) 所示。每個分割槽領導者(主庫)被分配給一個節點,追隨者(從庫)被分配給其他節點。每個節點可能是某些分割槽的主庫,同時是其他分割槽的從庫。
一個節點可能儲存多個分割槽。如果使用主從複製模型,則分割槽和複製的組合如 [圖 6-1](/img/fig6-1.png) 所示。每個分割槽領導者(主庫)被分配給一個節點,追隨者(從庫)被分配給其他節點。每個節點可能是某些分割槽的主庫,同時是其他分割槽的從庫。
我們在 [第五章](ch5.md) 討論的關於資料庫複製的所有內容同樣適用於分割槽的複製。大多數情況下,分割槽方案的選擇與複製方案的選擇是獨立的,為簡單起見,本章中將忽略複製。
我們在 [第五章](/tw/ch5) 討論的關於資料庫複製的所有內容同樣適用於分割槽的複製。大多數情況下,分割槽方案的選擇與複製方案的選擇是獨立的,為簡單起見,本章中將忽略複製。
![](../img/fig6-1.png)
![](/img/fig6-1.png)
**圖 6-1 組合使用複製和分割槽:每個節點充當某些分割槽的主庫,其他分割槽充當從庫。**
@@ -56,17 +58,17 @@
### 根據鍵的範圍分割槽
一種分割槽的方法是為每個分割槽指定一塊連續的鍵範圍(從最小值到最大值),如紙質百科全書的卷([圖 6-2](../img/fig6-2.png))。如果知道範圍之間的邊界,則可以輕鬆確定哪個分割槽包含某個值。如果你還知道分割槽所在的節點,那麼可以直接向相應的節點發出請求(對於百科全書而言,就像從書架上選取正確的書籍)。
一種分割槽的方法是為每個分割槽指定一塊連續的鍵範圍(從最小值到最大值),如紙質百科全書的卷([圖 6-2](/img/fig6-2.png))。如果知道範圍之間的邊界,則可以輕鬆確定哪個分割槽包含某個值。如果你還知道分割槽所在的節點,那麼可以直接向相應的節點發出請求(對於百科全書而言,就像從書架上選取正確的書籍)。
![](../img/fig6-2.png)
![](/img/fig6-2.png)
**圖 6-2 印刷版百科全書按照關鍵字範圍進行分割槽**
鍵的範圍不一定均勻分佈,因為資料也很可能不均勻分佈。例如在 [圖 6-2](../img/fig6-2.png) 中,第 1 捲包含以 A 和 B 開頭的單詞,但第 12 卷則包含以 T、U、V、X、Y 和 Z 開頭的單詞。只是簡單的規定每個捲包含兩個字母會導致一些卷比其他卷大。為了均勻分配資料,分割槽邊界需要依據資料調整。
鍵的範圍不一定均勻分佈,因為資料也很可能不均勻分佈。例如在 [圖 6-2](/img/fig6-2.png) 中,第 1 捲包含以 A 和 B 開頭的單詞,但第 12 卷則包含以 T、U、V、X、Y 和 Z 開頭的單詞。只是簡單的規定每個捲包含兩個字母會導致一些卷比其他卷大。為了均勻分配資料,分割槽邊界需要依據資料調整。
分割槽邊界可以由管理員手動選擇,也可以由資料庫自動選擇(我們會在 “[分割槽再平衡](#分割槽再平衡)” 中更詳細地討論分割槽邊界的選擇。Bigtable 使用了這種分割槽策略,以及其開源等價物 HBase 【2, 3】、RethinkDB 和 2.4 版本之前的 MongoDB 【4】。
在每個分割槽中,我們可以按照一定的順序儲存鍵(請參閱 “[SSTables 和 LSM 樹](ch3.md#SSTables和LSM樹)”)。好處是進行範圍掃描非常簡單,你可以將鍵作為聯合索引來處理,以便在一次查詢中獲取多個相關記錄(請參閱 “[多列索引](ch3.md#多列索引)”)。例如,假設我們有一個程式來儲存感測器網路的資料,其中主鍵是測量的時間戳(年月日時分秒)。範圍掃描在這種情況下非常有用,因為我們可以輕鬆獲取某個月份的所有資料。
在每個分割槽中,我們可以按照一定的順序儲存鍵(請參閱 “[SSTables 和 LSM 樹](/tw/ch3#SSTables和LSM樹)”)。好處是進行範圍掃描非常簡單,你可以將鍵作為聯合索引來處理,以便在一次查詢中獲取多個相關記錄(請參閱 “[多列索引](/tw/ch3#多列索引)”)。例如,假設我們有一個程式來儲存感測器網路的資料,其中主鍵是測量的時間戳(年月日時分秒)。範圍掃描在這種情況下非常有用,因為我們可以輕鬆獲取某個月份的所有資料。
然而Key Range 分割槽的缺點是某些特定的訪問模式會導致熱點。如果主鍵是時間戳則分割槽對應於時間範圍例如給每天分配一個分割槽。不幸的是由於我們在測量發生時將資料從感測器寫入資料庫因此所有寫入操作都會轉到同一個分割槽即今天的分割槽這樣分割槽可能會因寫入而過載而其他分割槽則處於空閒狀態【5】。
@@ -80,9 +82,9 @@
出於分割槽的目的雜湊函式不需要多麼強壯的加密演算法例如Cassandra 和 MongoDB 使用 MD5Voldemort 使用 Fowler-Noll-Vo 函式。許多程式語言都有內建的簡單雜湊函式(它們用於散列表),但是它們可能不適合分割槽:例如,在 Java 的 `Object.hashCode()` 和 Ruby 的 `Object#hash`同一個鍵可能在不同的程序中有不同的雜湊值【6】。
一旦你有一個合適的鍵雜湊函式,你可以為每個分割槽分配一個雜湊範圍(而不是鍵的範圍),每個透過雜湊雜湊落在分割槽範圍內的鍵將被儲存在該分割槽中。如 [圖 6-3](../img/fig6-3.png) 所示。
一旦你有一個合適的鍵雜湊函式,你可以為每個分割槽分配一個雜湊範圍(而不是鍵的範圍),每個透過雜湊雜湊落在分割槽範圍內的鍵將被儲存在該分割槽中。如 [圖 6-3](/img/fig6-3.png) 所示。
![](../img/fig6-3.png)
![](/img/fig6-3.png)
**圖 6-3 按雜湊鍵分割槽**
@@ -90,7 +92,7 @@
> #### 一致性雜湊
>
> 一致性雜湊由 Karger 等人定義。【7】 用於跨網際網路級別的快取系統,例如 CDN 中,是一種能均勻分配負載的方法。它使用隨機選擇的 **分割槽邊界partition boundaries** 來避免中央控制或分散式共識的需要。請注意,這裡的一致性與複製一致性(請參閱 [第五章](ch5.md))或 ACID 一致性(請參閱 [第七章](ch7.md)無關而只是描述了一種再平衡rebalancing的特定方法。
> 一致性雜湊由 Karger 等人定義。【7】 用於跨網際網路級別的快取系統,例如 CDN 中,是一種能均勻分配負載的方法。它使用隨機選擇的 **分割槽邊界partition boundaries** 來避免中央控制或分散式共識的需要。請注意,這裡的一致性與複製一致性(請參閱 [第五章](/tw/ch5))或 ACID 一致性(請參閱 [第七章](/tw/ch7)無關而只是描述了一種再平衡rebalancing的特定方法。
>
> 正如我們將在 “[分割槽再平衡](#分割槽再平衡)” 中所看到的,這種特殊的方法對於資料庫實際上並不是很好,所以在實際中很少使用(某些資料庫的文件仍然會使用一致性雜湊的說法,但是它往往是不準確的)。因為有可能產生混淆,所以最好避免使用一致性雜湊這個術語,而只是把它稱為 **雜湊分割槽hash partitioning**
@@ -118,7 +120,7 @@ Cassandra 採取了折衷的策略【11, 12, 13】。Cassandra 中的表可以
到目前為止,我們討論的分割槽方案依賴於鍵值資料模型。如果只通過主鍵訪問記錄,我們可以從該鍵確定分割槽,並使用它來將讀寫請求路由到負責該鍵的分割槽。
如果涉及次級索引,情況會變得更加複雜(參考 “[其他索引結構](ch3.md#其他索引結構)”)。次級索引通常並不能唯一地標識記錄,而是一種搜尋記錄中出現特定值的方式:查詢使用者 123 的所有操作、查詢包含詞語 `hogwash` 的所有文章、查詢所有顏色為紅色的車輛等等。
如果涉及次級索引,情況會變得更加複雜(參考 “[其他索引結構](/tw/ch3#其他索引結構)”)。次級索引通常並不能唯一地標識記錄,而是一種搜尋記錄中出現特定值的方式:查詢使用者 123 的所有操作、查詢包含詞語 `hogwash` 的所有文章、查詢所有顏色為紅色的車輛等等。
次級索引是關係型資料庫的基礎,並且在文件資料庫中也很普遍。許多鍵值儲存(如 HBase 和 Volde-mort為了減少實現的複雜度而放棄了次級索引但是一些如 Riak已經開始新增它們因為它們對於資料模型實在是太有用了。並且次級索引也是 Solr 和 Elasticsearch 等搜尋伺服器的基石。
@@ -126,31 +128,31 @@ Cassandra 採取了折衷的策略【11, 12, 13】。Cassandra 中的表可以
### 基於文件的次級索引進行分割槽
假設你正在經營一個銷售二手車的網站(如 [圖 6-4](../img/fig6-4.png) 所示)。每個列表都有一個唯一的 ID—— 稱之為文件 ID—— 並且用文件 ID 對資料庫進行分割槽(例如,分割槽 0 中的 ID 0 到 499分割槽 1 中的 ID 500 到 999 等)。
假設你正在經營一個銷售二手車的網站(如 [圖 6-4](/img/fig6-4.png) 所示)。每個列表都有一個唯一的 ID—— 稱之為文件 ID—— 並且用文件 ID 對資料庫進行分割槽(例如,分割槽 0 中的 ID 0 到 499分割槽 1 中的 ID 500 到 999 等)。
你想讓使用者搜尋汽車,允許他們透過顏色和廠商過濾,所以需要一個在顏色和廠商上的次級索引(文件資料庫中這些是 **欄位field**,關係資料庫中這些是 **列column** )。如果你聲明瞭索引,則資料庫可以自動執行索引 [^ii]。例如,無論何時將紅色汽車新增到資料庫,資料庫分割槽都會自動將其新增到索引條目 `color:red` 的文件 ID 列表中。
[^ii]: 如果資料庫僅支援鍵值模型,則你可能會嘗試在應用程式程式碼中建立從值到文件 ID 的對映來實現次級索引。如果沿著這條路線走下去,請萬分小心,確保你的索引與底層資料保持一致。競爭條件和間歇性寫入失敗(其中一些更改已儲存,但其他更改未儲存)很容易導致資料不同步 - 請參閱 “[多物件事務的需求](ch7.md#多物件事務的需求)”。
[^ii]: 如果資料庫僅支援鍵值模型,則你可能會嘗試在應用程式程式碼中建立從值到文件 ID 的對映來實現次級索引。如果沿著這條路線走下去,請萬分小心,確保你的索引與底層資料保持一致。競爭條件和間歇性寫入失敗(其中一些更改已儲存,但其他更改未儲存)很容易導致資料不同步 - 請參閱 “[多物件事務的需求](/tw/ch7#多物件事務的需求)”。
![](../img/fig6-4.png)
![](/img/fig6-4.png)
**圖 6-4 基於文件的次級索引進行分割槽**
在這種索引方法中,每個分割槽是完全獨立的:每個分割槽維護自己的次級索引,僅覆蓋該分割槽中的文件。它不關心儲存在其他分割槽的資料。無論何時你需要寫入資料庫(新增,刪除或更新文件),只需處理包含你正在編寫的文件 ID 的分割槽即可。出於這個原因,**文件分割槽索引** 也被稱為 **本地索引**(而不是將在下一節中描述的 **全域性索引**)。
但是,從文件分割槽索引中讀取需要注意:除非你對文件 ID 做了特別的處理,否則沒有理由將所有具有特定顏色或特定品牌的汽車放在同一個分割槽中。在 [圖 6-4](../img/fig6-4.png) 中,紅色汽車出現在分割槽 0 和分割槽 1 中。因此,如果要搜尋紅色汽車,則需要將查詢傳送到所有分割槽,併合並所有返回的結果。
但是,從文件分割槽索引中讀取需要注意:除非你對文件 ID 做了特別的處理,否則沒有理由將所有具有特定顏色或特定品牌的汽車放在同一個分割槽中。在 [圖 6-4](/img/fig6-4.png) 中,紅色汽車出現在分割槽 0 和分割槽 1 中。因此,如果要搜尋紅色汽車,則需要將查詢傳送到所有分割槽,併合並所有返回的結果。
這種查詢分割槽資料庫的方法有時被稱為 **分散 / 聚集scatter/gather**,並且可能會使次級索引上的讀取查詢相當昂貴。即使並行查詢分割槽,分散 / 聚集也容易導致尾部延遲放大(請參閱 “[實踐中的百分位點](ch1.md#實踐中的百分位點)”。然而它被廣泛使用MongoDBRiak 【15】Cassandra 【16】Elasticsearch 【17】SolrCloud 【18】和 VoltDB 【19】都使用文件分割槽次級索引。大多數資料庫供應商建議你構建一個能從單個分割槽提供次級索引查詢的分割槽方案但這並不總是可行尤其是當在單個查詢中使用多個次級索引時例如同時需要按顏色和製造商查詢
這種查詢分割槽資料庫的方法有時被稱為 **分散 / 聚集scatter/gather**,並且可能會使次級索引上的讀取查詢相當昂貴。即使並行查詢分割槽,分散 / 聚集也容易導致尾部延遲放大(請參閱 “[實踐中的百分位點](/tw/ch1#實踐中的百分位點)”。然而它被廣泛使用MongoDBRiak 【15】Cassandra 【16】Elasticsearch 【17】SolrCloud 【18】和 VoltDB 【19】都使用文件分割槽次級索引。大多數資料庫供應商建議你構建一個能從單個分割槽提供次級索引查詢的分割槽方案但這並不總是可行尤其是當在單個查詢中使用多個次級索引時例如同時需要按顏色和製造商查詢
### 基於關鍵詞(Term)的次級索引進行分割槽
我們可以構建一個覆蓋所有分割槽資料的 **全域性索引**,而不是給每個分割槽建立自己的次級索引(本地索引)。但是,我們不能只把這個索引儲存在一個節點上,因為它可能會成為瓶頸,違背了分割槽的目的。全域性索引也必須進行分割槽,但可以採用與主鍵不同的分割槽方式。
[圖 6-5](../img/fig6-5.png) 描述了這可能是什麼樣子:來自所有分割槽的紅色汽車在紅色索引中,並且索引是分割槽的,首字母從 `a``r` 的顏色在分割槽 0 中,`s``z` 的在分割槽 1。汽車製造商的索引也與之類似分割槽邊界在 `f``h` 之間)。
[圖 6-5](/img/fig6-5.png) 描述了這可能是什麼樣子:來自所有分割槽的紅色汽車在紅色索引中,並且索引是分割槽的,首字母從 `a``r` 的顏色在分割槽 0 中,`s``z` 的在分割槽 1。汽車製造商的索引也與之類似分割槽邊界在 `f``h` 之間)。
![](../img/fig6-5.png)
![](/img/fig6-5.png)
**圖 6-5 基於關鍵詞對次級索引進行分割槽**
@@ -160,11 +162,11 @@ Cassandra 採取了折衷的策略【11, 12, 13】。Cassandra 中的表可以
關鍵詞分割槽的全域性索引優於文件分割槽索引的地方點是它可以使讀取更有效率:不需要 **分散 / 收集** 所有分割槽,客戶端只需要向包含關鍵詞的分割槽發出請求。全域性索引的缺點在於寫入速度較慢且較為複雜,因為寫入單個文件現在可能會影響索引的多個分割槽(文件中的每個關鍵詞可能位於不同的分割槽或者不同的節點上) 。
理想情況下,索引總是最新的,寫入資料庫的每個文件都會立即反映在索引中。但是,在關鍵詞分割槽索引中,這需要跨分割槽的分散式事務,並不是所有資料庫都支援(請參閱 [第七章](ch7.md) 和 [第九章](ch9.md))。
理想情況下,索引總是最新的,寫入資料庫的每個文件都會立即反映在索引中。但是,在關鍵詞分割槽索引中,這需要跨分割槽的分散式事務,並不是所有資料庫都支援(請參閱 [第七章](/tw/ch7) 和 [第九章](/tw/ch9))。
在實踐中,對全域性次級索引的更新通常是 **非同步**也就是說如果在寫入之後不久讀取索引剛才所做的更改可能尚未反映在索引中。例如Amazon DynamoDB 聲稱在正常情況下其全域性次級索引會在不到一秒的時間內更新但在基礎架構出現故障的情況下可能會有延遲【20】。
全域性關鍵詞分割槽索引的其他用途包括 Riak 的搜尋功能【21】和 Oracle 資料倉庫它允許你在本地和全域性索引之間進行選擇【22】。我們將在 [第十二章](ch12.md) 中繼續關鍵詞分割槽次級索引實現的話題。
全域性關鍵詞分割槽索引的其他用途包括 Riak 的搜尋功能【21】和 Oracle 資料倉庫它允許你在本地和全域性索引之間進行選擇【22】。我們將在 [第十二章](/tw/ch12) 中繼續關鍵詞分割槽次級索引實現的話題。
## 分割槽再平衡
@@ -189,7 +191,7 @@ Cassandra 採取了折衷的策略【11, 12, 13】。Cassandra 中的表可以
#### 反面教材hash mod N
我們在前面說過([圖 6-3](../img/fig6-3.png)),最好將可能的雜湊分成不同的範圍,並將每個範圍分配給一個分割槽(例如,如果 $0 ≤ hash(key)< b_0$,則將鍵分配給分割槽 0如果 $b_0 ≤ hash(key) < b_1$,則分配給分割槽 1
我們在前面說過([圖 6-3](/img/fig6-3.png)),最好將可能的雜湊分成不同的範圍,並將每個範圍分配給一個分割槽(例如,如果 $0 ≤ hash(key)< b_0$,則將鍵分配給分割槽 0如果 $b_0 ≤ hash(key) < b_1$,則分配給分割槽 1
也許你想知道為什麼我們不使用 ***取模mod***(許多程式語言中的 % 運算子)。例如,`hash(key) mod 10` 會返回一個介於 0 和 9 之間的數字(如果我們將雜湊寫為十進位制數,雜湊模 10 將是最後一個數字)。如果我們有 10 個節點,編號為 0 到 9這似乎是將每個鍵分配給一個節點的簡單方法。
@@ -201,11 +203,11 @@ Cassandra 採取了折衷的策略【11, 12, 13】。Cassandra 中的表可以
幸運的是,有一個相當簡單的解決方案:建立比節點更多的分割槽,併為每個節點分配多個分割槽。例如,執行在 10 個節點的叢集上的資料庫可能會從一開始就被拆分為 1,000 個分割槽,因此大約有 100 個分割槽被分配給每個節點。
現在,如果一個節點被新增到叢集中,新節點可以從當前每個節點中 **竊取** 一些分割槽,直到分割槽再次公平分配。這個過程如 [圖 6-6](../img/fig6-6.png) 所示。如果從叢集中刪除一個節點,則會發生相反的情況。
現在,如果一個節點被新增到叢集中,新節點可以從當前每個節點中 **竊取** 一些分割槽,直到分割槽再次公平分配。這個過程如 [圖 6-6](/img/fig6-6.png) 所示。如果從叢集中刪除一個節點,則會發生相反的情況。
只有分割槽在節點之間的移動。分割槽的數量不會改變,鍵所指定的分割槽也不會改變。唯一改變的是分割槽所在的節點。這種變更並不是即時的 — 在網路上傳輸大量的資料需要一些時間 — 所以在傳輸過程中,原有分割槽仍然會接受讀寫操作。
![](../img/fig6-6.png)
![](/img/fig6-6.png)
**圖 6-6 將新節點新增到每個節點具有多個分割槽的資料庫叢集。**
@@ -219,7 +221,7 @@ Cassandra 採取了折衷的策略【11, 12, 13】。Cassandra 中的表可以
對於使用鍵範圍分割槽的資料庫(請參閱 “[根據鍵的範圍分割槽](#根據鍵的範圍分割槽)”),具有固定邊界的固定數量的分割槽將非常不便:如果邊界設定錯誤,可能會導致所有資料都在一個分割槽中,而其他分割槽則為空。手動重新配置分割槽邊界將非常繁瑣。
出於這個原因,按鍵的範圍進行分割槽的資料庫(如 HBase 和 RethinkDB會動態建立分割槽。當分割槽增長到超過配置的大小時在 HBase 上,預設值是 10GB會被分成兩個分割槽每個分割槽約佔一半的資料【26】。與之相反如果大量資料被刪除並且分割槽縮小到某個閾值以下則可以將其與相鄰分割槽合併。此過程與 B 樹頂層發生的過程類似(請參閱 “[B 樹](ch3.md#B樹)”)。
出於這個原因,按鍵的範圍進行分割槽的資料庫(如 HBase 和 RethinkDB會動態建立分割槽。當分割槽增長到超過配置的大小時在 HBase 上,預設值是 10GB會被分成兩個分割槽每個分割槽約佔一半的資料【26】。與之相反如果大量資料被刪除並且分割槽縮小到某個閾值以下則可以將其與相鄰分割槽合併。此過程與 B 樹頂層發生的過程類似(請參閱 “[B 樹](/tw/ch3#B樹)”)。
每個分割槽分配給一個節點,每個節點可以處理多個分割槽,就像固定數量的分割槽一樣。大型分割槽拆分後,可以將其中的一半轉移到另一個節點,以平衡負載。在 HBase 中,分割槽檔案的傳輸透過 HDFS底層使用的分散式檔案系統來實現【3】。
@@ -265,21 +267,21 @@ Cassandra 和 Ketama 使用的第三種方法是使分割槽數與節點數成
以上所有情況中的關鍵問題是:作出路由決策的元件(可能是節點之一,還是路由層或客戶端)如何瞭解分割槽 - 節點之間的分配關係變化?
![](../img/fig6-7.png)
![](/img/fig6-7.png)
**圖 6-7 將請求路由到正確節點的三種不同方式。**
這是一個具有挑戰性的問題,因為重要的是所有參與者都達成共識 - 否則請求將被傳送到錯誤的節點,得不到正確的處理。在分散式系統中有達成共識的協議,但很難正確地實現(見 [第九章](ch9.md))。
這是一個具有挑戰性的問題,因為重要的是所有參與者都達成共識 - 否則請求將被傳送到錯誤的節點,得不到正確的處理。在分散式系統中有達成共識的協議,但很難正確地實現(見 [第九章](/tw/ch9))。
許多分散式資料系統都依賴於一個獨立的協調服務,比如 ZooKeeper 來跟蹤叢集元資料,如 [圖 6-8](../img/fig6-8.png) 所示。每個節點在 ZooKeeper 中註冊自己ZooKeeper 維護分割槽到節點的可靠對映。其他參與者(如路由層或分割槽感知客戶端)可以在 ZooKeeper 中訂閱此資訊。只要分割槽分配發生了改變或者叢集中新增或刪除了一個節點ZooKeeper 就會通知路由層使路由資訊保持最新狀態。
許多分散式資料系統都依賴於一個獨立的協調服務,比如 ZooKeeper 來跟蹤叢集元資料,如 [圖 6-8](/img/fig6-8.png) 所示。每個節點在 ZooKeeper 中註冊自己ZooKeeper 維護分割槽到節點的可靠對映。其他參與者(如路由層或分割槽感知客戶端)可以在 ZooKeeper 中訂閱此資訊。只要分割槽分配發生了改變或者叢集中新增或刪除了一個節點ZooKeeper 就會通知路由層使路由資訊保持最新狀態。
![](../img/fig6-8.png)
![](/img/fig6-8.png)
**圖 6-8 使用 ZooKeeper 跟蹤分割槽分配給節點。**
例如LinkedIn的Espresso使用Helix 【31】進行叢集管理依靠ZooKeeper實現瞭如[圖6-8](../img/fig6-8.png)所示的路由層。HBase、SolrCloud和Kafka也使用ZooKeeper來跟蹤分割槽分配。MongoDB具有類似的體系結構但它依賴於自己的**配置伺服器config server** 實現和mongos守護程序作為路由層。
例如LinkedIn的Espresso使用Helix 【31】進行叢集管理依靠ZooKeeper實現瞭如[圖6-8](/img/fig6-8.png)所示的路由層。HBase、SolrCloud和Kafka也使用ZooKeeper來跟蹤分割槽分配。MongoDB具有類似的體系結構但它依賴於自己的**配置伺服器config server** 實現和mongos守護程序作為路由層。
Cassandra 和 Riak 採取不同的方法:他們在節點之間使用 **流言協議gossip protocol** 來傳播叢集狀態的變化。請求可以傳送到任意節點,該節點會轉發到包含所請求的分割槽的適當節點([圖 6-7](../img/fig6-7.png) 中的方法 1。這個模型在資料庫節點中增加了更多的複雜性但是避免了對像 ZooKeeper 這樣的外部協調服務的依賴。
Cassandra 和 Riak 採取不同的方法:他們在節點之間使用 **流言協議gossip protocol** 來傳播叢集狀態的變化。請求可以傳送到任意節點,該節點會轉發到包含所請求的分割槽的適當節點([圖 6-7](/img/fig6-7.png) 中的方法 1。這個模型在資料庫節點中增加了更多的複雜性但是避免了對像 ZooKeeper 這樣的外部協調服務的依賴。
Couchbase 不會自動進行再平衡,這簡化了設計。通常情況下,它配置了一個名為 moxi 的路由層它會從叢集節點了解路由變化【32】。
@@ -291,7 +293,7 @@ Couchbase 不會自動進行再平衡,這簡化了設計。通常情況下,
然而,通常用於分析的 **大規模並行處理MPP, Massively parallel processing** 關係型資料庫產品在其支援的查詢型別方面要複雜得多。一個典型的資料倉庫查詢包含多個連線過濾分組和聚合操作。MPP 查詢最佳化器將這個複雜的查詢分解成許多執行階段和分割槽,其中許多可以在資料庫叢集的不同節點上並行執行。涉及掃描大規模資料集的查詢特別受益於這種並行執行。
資料倉庫查詢的快速並行執行是一個專門的話題,由於分析有很重要的商業意義,可以帶來很多利益。我們將在 [第十章](ch10.md) 討論並行查詢執行的一些技巧。有關並行資料庫中使用的技術的更詳細的概述請參閱參考文獻【1,33】。
資料倉庫查詢的快速並行執行是一個專門的話題,由於分析有很重要的商業意義,可以帶來很多利益。我們將在 [第十章](/tw/ch10) 討論並行查詢執行的一些技巧。有關並行資料庫中使用的技術的更詳細的概述請參閱參考文獻【1,33】。
## 本章小結
@@ -357,11 +359,4 @@ Couchbase 不會自動進行再平衡,這簡化了設計。通常情況下,
1. Jason Wilder: “[Open-Source Service Discovery](http://jasonwilder.com/blog/2014/02/04/service-discovery-in-the-cloud/),” *jasonwilder.com*, February 2014.
1. Kishore Gopalakrishna, Shi Lu, Zhen Zhang, et al.: “[Untangling Cluster Management with Helix](http://www.socc2012.org/helix_onecol.pdf?attredirects=0),” at *ACM Symposium on Cloud Computing* (SoCC), October 2012. [doi:10.1145/2391229.2391248](http://dx.doi.org/10.1145/2391229.2391248)
1. “[Moxi 1.8 Manual](http://docs.couchbase.com/moxi-manual-1.8/),” Couchbase, Inc., 2014.
1. Shivnath Babu and Herodotos Herodotou: “[Massively Parallel Databases and MapReduce Systems](https://www.microsoft.com/en-us/research/wp-content/uploads/2013/11/db-mr-survey-final.pdf),” *Foundations and Trends in Databases*, volume 5, number 1, pages 1104, November 2013. [doi:10.1561/1900000036](http://dx.doi.org/10.1561/1900000036)
------
| 上一章 | 目錄 | 下一章 |
| :--------------------: | :-----------------------------: | :--------------------: |
| [第五章:複製](ch5.md) | [設計資料密集型應用](README.md) | [第七章:事務](ch7.md) |
1. Shivnath Babu and Herodotos Herodotou: “[Massively Parallel Databases and MapReduce Systems](https://www.microsoft.com/en-us/research/wp-content/uploads/2013/11/db-mr-survey-final.pdf),” *Foundations and Trends in Databases*, volume 5, number 1, pages 1104, November 2013. [doi:10.1561/1900000036](http://dx.doi.org/10.1561/1900000036)

View File

@@ -1,15 +1,16 @@
# 第七章:事務
---
title: "第七章:事務"
linkTitle: "7. 事務"
weight: 207
breadcrumbs: false
---
![](../img/ch7.png)
![](/img/ch7.png)
> 一些作者聲稱,支援通用的兩階段提交代價太大,會帶來效能與可用性的問題。讓程式設計師來處理過度使用事務導致的效能問題,總比缺少事務程式設計好得多。
>
> —— James Corbett 等人SpannerGoogle 的全球分散式資料庫2012
------
[TOC]
在資料系統的殘酷現實中,很多事情都可能出錯:
- 資料庫軟體、硬體可能在任意時刻發生故障(包括寫操作進行到一半時)。
@@ -31,14 +32,14 @@
本章將研究許多出錯案例,並探索資料庫用於防範這些問題的演算法。尤其會深入 **併發控制** 的領域,討論各種可能發生的競爭條件,以及資料庫如何實現 **讀已提交read committed****快照隔離snapshot isolation** 和 **可序列化serializability** 等隔離級別。
本章同時適用於單機資料庫與分散式資料庫;在 [第八章](ch8.md) 中將重點討論僅出現在分散式系統中的特殊挑戰。
本章同時適用於單機資料庫與分散式資料庫;在 [第八章](/tw/ch8) 中將重點討論僅出現在分散式系統中的特殊挑戰。
## 事務的棘手概念
現今,幾乎所有的關係型資料庫和一些非關係資料庫都支援 **事務**。其中大多數遵循 IBM System R第一個 SQL 資料庫)在 1975 年引入的風格【1,2,3】。40 年裡儘管一些實現細節發生了變化但總體思路大同小異MySQL、PostgreSQL、Oracle 和 SQL Server 等資料庫中的事務支援與 System R 異乎尋常地相似。
2000 年以後非關係NoSQL資料庫開始普及。它們的目標是在關係資料庫的現狀基礎上透過提供新的資料模型選擇請參閱 [第二章](ch2.md)並預設包含複製第五章和分割槽第六章來進一步提升。事務是這次運動的主要犧牲品這些新一代資料庫中的許多資料庫完全放棄了事務或者重新定義了這個詞描述比以前所理解的更弱得多的一套保證【4】。
2000 年以後非關係NoSQL資料庫開始普及。它們的目標是在關係資料庫的現狀基礎上透過提供新的資料模型選擇請參閱 [第二章](/tw/ch2)並預設包含複製第五章和分割槽第六章來進一步提升。事務是這次運動的主要犧牲品這些新一代資料庫中的許多資料庫完全放棄了事務或者重新定義了這個詞描述比以前所理解的更弱得多的一套保證【4】。
隨著這種新型分散式資料庫的炒作人們普遍認為事務是可伸縮性的對立面任何大型系統都必須放棄事務以保持良好的效能和高可用性【5,6】。另一方面資料庫廠商有時將事務保證作為 “重要應用” 和 “有價值資料” 的基本要求。這兩種觀點都是 **純粹的誇張**
@@ -70,9 +71,9 @@ ACID 原子性的定義特徵是:**能夠在錯誤時中止事務,丟棄該
一致性這個詞被賦予太多含義:
* 在 [第五章](ch5.md) 中,我們討論了副本一致性,以及非同步複製系統中的最終一致性問題(請參閱 “[複製延遲問題](ch5.md#複製延遲問題)”)。
* [一致性雜湊](ch6.md#一致性雜湊) 是某些系統用於重新分割槽的一種分割槽方法。
* 在 [CAP 定理](ch9.md#CAP定理) 中,一致性一詞用於表示 [線性一致性](ch9.md#線性一致性)。
* 在 [第五章](/tw/ch5) 中,我們討論了副本一致性,以及非同步複製系統中的最終一致性問題(請參閱 “[複製延遲問題](/tw/ch5#複製延遲問題)”)。
* [一致性雜湊](/tw/ch6#一致性雜湊) 是某些系統用於重新分割槽的一種分割槽方法。
* 在 [CAP 定理](/tw/ch9#CAP定理) 中,一致性一詞用於表示 [線性一致性](/tw/ch9#線性一致性)。
* 在 ACID 的上下文中,**一致性** 是指資料庫在應用程式的特定概念中處於 “良好狀態”。
很不幸,這一個詞就至少有四種不同的含義。
@@ -89,11 +90,11 @@ ACID 一致性的概念是,**對資料的一組特定約束必須始終成立*
大多數資料庫都會同時被多個客戶端訪問。如果它們各自讀寫資料庫的不同部分,這是沒有問題的,但是如果它們訪問相同的資料庫記錄,則可能會遇到 **併發** 問題(**競爭條件**,即 race conditions
[圖 7-1](../img/fig7-1.png) 是這類問題的一個簡單例子。假設你有兩個客戶端同時在資料庫中增長一個計數器。(假設資料庫沒有內建的自增操作)每個客戶端需要讀取計數器的當前值,加 1 ,再回寫新值。[圖 7-1](../img/fig7-1.png) 中,因為發生了兩次增長,計數器應該從 42 增至 44但由於競態條件實際上只增至 43 。
[圖 7-1](/img/fig7-1.png) 是這類問題的一個簡單例子。假設你有兩個客戶端同時在資料庫中增長一個計數器。(假設資料庫沒有內建的自增操作)每個客戶端需要讀取計數器的當前值,加 1 ,再回寫新值。[圖 7-1](/img/fig7-1.png) 中,因為發生了兩次增長,計數器應該從 42 增至 44但由於競態條件實際上只增至 43 。
ACID 意義上的隔離性意味著,**同時執行的事務是相互隔離的**:它們不能相互冒犯。傳統的資料庫教科書將隔離性形式化為 **可序列化Serializability**這意味著每個事務可以假裝它是唯一在整個資料庫上執行的事務。資料庫確保當多個事務被提交時結果與它們序列執行一個接一個是一樣的儘管實際上它們可能是併發執行的【10】。
![](../img/fig7-1.png)
![](/img/fig7-1.png)
**圖 7-1 兩個客戶之間的競爭狀態同時遞增計數器**
@@ -103,9 +104,9 @@ ACID 意義上的隔離性意味著,**同時執行的事務是相互隔離的*
資料庫系統的目的是,提供一個安全的地方儲存資料,而不用擔心丟失。**永續性** 是一個承諾,即一旦事務成功完成,即使發生硬體故障或資料庫崩潰,寫入的任何資料也不會丟失。
在單節點資料庫中,永續性通常意味著資料已被寫入非易失性儲存裝置,如硬碟或 SSD。它通常還包括預寫日誌或類似的檔案請參閱 “[讓 B 樹更可靠](ch3.md#讓B樹更可靠)”),以便在磁碟上的資料結構損壞時進行恢復。在帶複製的資料庫中,永續性可能意味著資料已成功複製到一些節點。為了提供永續性保證,資料庫必須等到這些寫入或複製完成後,才能報告事務成功提交。
在單節點資料庫中,永續性通常意味著資料已被寫入非易失性儲存裝置,如硬碟或 SSD。它通常還包括預寫日誌或類似的檔案請參閱 “[讓 B 樹更可靠](/tw/ch3#讓B樹更可靠)”),以便在磁碟上的資料結構損壞時進行恢復。在帶複製的資料庫中,永續性可能意味著資料已成功複製到一些節點。為了提供永續性保證,資料庫必須等到這些寫入或複製完成後,才能報告事務成功提交。
如 “[可靠性](ch1.md#可靠性)” 一節所述,**完美的永續性是不存在的** :如果所有硬碟和所有備份同時被銷燬,那顯然沒有任何資料庫能救得了你。
如 “[可靠性](/tw/ch1#可靠性)” 一節所述,**完美的永續性是不存在的** :如果所有硬碟和所有備份同時被銷燬,那顯然沒有任何資料庫能救得了你。
> #### 複製與永續性
>
@@ -114,8 +115,8 @@ ACID 意義上的隔離性意味著,**同時執行的事務是相互隔離的*
> 真相是,沒有什麼是完美的:
>
> * 如果你寫入磁碟然後機器宕機,即使資料沒有丟失,在修復機器或將磁碟轉移到其他機器之前,也是無法訪問的。這種情況下,複製系統可以保持可用性。
> * 一個相關性故障(停電,或一個特定輸入導致所有節點崩潰的 Bug可能會一次性摧毀所有副本請參閱「[可靠性](ch1.md#可靠性)」),任何僅儲存在記憶體中的資料都會丟失,故記憶體資料庫仍然要和磁碟寫入打交道。
> * 在非同步複製系統中,當主庫不可用時,最近的寫入操作可能會丟失(請參閱「[處理節點宕機](ch5.md#處理節點宕機)」)。
> * 一個相關性故障(停電,或一個特定輸入導致所有節點崩潰的 Bug可能會一次性摧毀所有副本請參閱「[可靠性](/tw/ch1#可靠性)」),任何僅儲存在記憶體中的資料都會丟失,故記憶體資料庫仍然要和磁碟寫入打交道。
> * 在非同步複製系統中,當主庫不可用時,最近的寫入操作可能會丟失(請參閱「[處理節點宕機](/tw/ch5#處理節點宕機)」)。
> * 當電源突然斷電時,特別是固態硬碟,有證據顯示有時會違反應有的保證:甚至 fsync 也不能保證正常工作【12】。硬碟韌體可能有錯誤就像任何其他型別的軟體一樣【13,14】。
> * 儲存引擎和檔案系統之間的微妙互動可能會導致難以追蹤的錯誤並可能導致磁碟上的檔案在崩潰後被損壞【15,16】。
> * 磁碟上的資料可能會在沒有檢測到的情況下逐漸損壞【17】。如果資料已損壞一段時間副本和最近的備份也可能損壞。這種情況下需要嘗試從歷史備份中恢復資料。
@@ -136,7 +137,7 @@ ACID 意義上的隔離性意味著,**同時執行的事務是相互隔離的*
同時執行的事務不應該互相干擾。例如,如果一個事務進行多次寫入,則另一個事務要麼看到全部寫入結果,要麼什麼都看不到,但不應該是一些子集。
這些定義假設你想同時修改多個物件(行,文件,記錄)。通常需要 **多物件事務multi-object transaction** 來保持多塊資料同步。[圖 7-2](../img/fig7-2.png) 展示了一個來自電郵應用的例子。執行以下查詢來顯示使用者未讀郵件數量:
這些定義假設你想同時修改多個物件(行,文件,記錄)。通常需要 **多物件事務multi-object transaction** 來保持多塊資料同步。[圖 7-2](/img/fig7-2.png) 展示了一個來自電郵應用的例子。執行以下查詢來顯示使用者未讀郵件數量:
```sql
SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
@@ -144,23 +145,23 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
但如果郵件太多,你可能會覺得這個查詢太慢,並決定用單獨的欄位儲存未讀郵件的數量(一種反規範化)。現在每當一個新訊息寫入時,必須也增長未讀計數器,每當一個訊息被標記為已讀時,也必須減少未讀計數器。
在 [圖 7-2](../img/fig7-2.png) 中,使用者 2 遇到異常情況:郵件列表裡顯示有未讀訊息,但計數器顯示為零未讀訊息,因為計數器增長還沒有發生 [^ii]。隔離性可以避免這個問題:透過確保使用者 2 要麼同時看到新郵件和增長後的計數器,要麼都看不到,而不是一個前後矛盾的中間結果。
在 [圖 7-2](/img/fig7-2.png) 中,使用者 2 遇到異常情況:郵件列表裡顯示有未讀訊息,但計數器顯示為零未讀訊息,因為計數器增長還沒有發生 [^ii]。隔離性可以避免這個問題:透過確保使用者 2 要麼同時看到新郵件和增長後的計數器,要麼都看不到,而不是一個前後矛盾的中間結果。
[^ii]: 可以說郵件應用中的錯誤計數器並不是什麼特別重要的問題。但換種方式來看,你可以把未讀計數器換成客戶賬戶餘額,把郵件收發看成支付交易。
![](../img/fig7-2.png)
![](/img/fig7-2.png)
**圖 7-2 違反隔離性:一個事務讀取另一個事務的未被執行的寫入(“髒讀”)。**
[圖 7-3](../img/fig7-3.png) 說明了對原子性的需求:如果在事務過程中發生錯誤,郵箱和未讀計數器的內容可能會失去同步。在原子事務中,如果對計數器的更新失敗,事務將被中止,並且插入的電子郵件將被回滾。
[圖 7-3](/img/fig7-3.png) 說明了對原子性的需求:如果在事務過程中發生錯誤,郵箱和未讀計數器的內容可能會失去同步。在原子事務中,如果對計數器的更新失敗,事務將被中止,並且插入的電子郵件將被回滾。
![](../img/fig7-3.png)
![](/img/fig7-3.png)
**圖 7-3 原子性確保發生錯誤時,事務先前的任何寫入都會被撤消,以避免狀態不一致**
多物件事務需要某種方式來確定哪些讀寫操作屬於同一個事務。在關係型資料庫中,通常基於客戶端與資料庫伺服器的 TCP 連線:在任何特定連線上,`BEGIN TRANSACTION``COMMIT` 語句之間的所有內容,被認為是同一事務的一部分.[^iii]
[^iii]: 這並不完美。如果 TCP 連線中斷,則事務必須中止。如果中斷發生在客戶端請求提交之後,但在伺服器確認提交發生之前,客戶端並不知道事務是否已提交。為了解決這個問題,事務管理器可以透過一個唯一事務識別符號來對操作進行分組,這個識別符號並未繫結到特定 TCP 連線。後續再 “[資料庫的端到端原則](ch12.md#資料庫的端到端原則)” 一節將回到這個主題。
[^iii]: 這並不完美。如果 TCP 連線中斷,則事務必須中止。如果中斷發生在客戶端請求提交之後,但在伺服器確認提交發生之前,客戶端並不知道事務是否已提交。為了解決這個問題,事務管理器可以透過一個唯一事務識別符號來對操作進行分組,這個識別符號並未繫結到特定 TCP 連線。後續再 “[資料庫的端到端原則](/tw/ch12#資料庫的端到端原則)” 一節將回到這個主題。
另一方面,許多非關係資料庫並沒有將這些操作組合在一起的方法。即使存在多物件 API例如某鍵值儲存可能具有在一個操作中更新幾個鍵的 multi-put 操作),但這並不一定意味著它具有事務語義:該命令可能在一些鍵上成功,在其他的鍵上失敗,使資料庫處於部分更新的狀態。
@@ -172,9 +173,9 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
- 如果在資料庫正在覆蓋磁碟上的前一個值的過程中電源發生故障,是否最終將新舊值拼接在一起?
- 如果另一個客戶端在寫入過程中讀取該文件,是否會看到部分更新的值?
這些問題非常讓人頭大,故儲存引擎一個幾乎普遍的目標是:對單節點上的單個物件(例如鍵值對)上提供原子性和隔離性。原子性可以透過使用日誌來實現崩潰恢復(請參閱 “[讓 B 樹更可靠](ch3.md#讓B樹更可靠)”),並且可以使用每個物件上的鎖來實現隔離(每次只允許一個執行緒訪問物件) 。
這些問題非常讓人頭大,故儲存引擎一個幾乎普遍的目標是:對單節點上的單個物件(例如鍵值對)上提供原子性和隔離性。原子性可以透過使用日誌來實現崩潰恢復(請參閱 “[讓 B 樹更可靠](/tw/ch3#讓B樹更可靠)”),並且可以使用每個物件上的鎖來實現隔離(每次只允許一個執行緒訪問物件) 。
一些資料庫也提供更複雜的原子操作 [^iv],例如自增操作,這樣就不再需要像 [圖 7-1](../img/fig7-1.png) 那樣的讀取 - 修改 - 寫入序列了。同樣流行的是 **[比較和設定CAS, compare-and-set](#比較並設定CAS)** 操作,僅當值沒有被其他併發修改過時,才允許執行寫操作。
一些資料庫也提供更複雜的原子操作 [^iv],例如自增操作,這樣就不再需要像 [圖 7-1](/img/fig7-1.png) 那樣的讀取 - 修改 - 寫入序列了。同樣流行的是 **[比較和設定CAS, compare-and-set](#比較並設定CAS)** 操作,僅當值沒有被其他併發修改過時,才允許執行寫操作。
[^iv]: 嚴格地說,**原子自增atomic increment** 這個術語在多執行緒程式設計的意義上使用了原子這個詞。在 ACID 的情況下,它實際上應該被稱為 **隔離的isolated** 的或 **可序列的serializable** 的增量。但這就太吹毛求疵了。
@@ -182,23 +183,23 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
#### 多物件事務的需求
許多分散式資料儲存已經放棄了多物件事務,因為多物件事務很難跨分割槽實現,而且在需要高可用性或高效能的情況下,它們可能會礙事。但說到底,在分散式資料庫中實現事務,並沒有什麼根本性的障礙。[第九章](ch9.md) 將討論分散式事務的實現。
許多分散式資料儲存已經放棄了多物件事務,因為多物件事務很難跨分割槽實現,而且在需要高可用性或高效能的情況下,它們可能會礙事。但說到底,在分散式資料庫中實現事務,並沒有什麼根本性的障礙。[第九章](/tw/ch9) 將討論分散式事務的實現。
但是我們是否需要多物件事務?**是否有可能只用鍵值資料模型和單物件操作來實現任何應用程式?**
有一些場景中,單物件插入,更新和刪除是足夠的。但是許多其他場景需要協調寫入幾個不同的物件:
* 在關係資料模型中,一個表中的行通常具有對另一個表中的行的外部索引鍵引用。(類似的是,在一個圖資料模型中,一個頂點有著到其他頂點的邊)。多物件事務使你確保這些引用始終有效:當插入幾個相互引用的記錄時,外部索引鍵必須是正確的和最新的,不然資料就沒有意義。
* 在文件資料模型中,需要一起更新的欄位通常在同一個文件中,這被視為單個物件 —— 更新單個文件時不需要多物件事務。但是,缺乏連線功能的文件資料庫會鼓勵非規範化(請參閱 “[關係型資料庫與文件資料庫在今日的對比](ch2.md#關係型資料庫與文件資料庫在今日的對比)”)。當需要更新非規範化的資訊時,如 [圖 7-2](../img/fig7-2.png) 所示,需要一次更新多個文件。事務在這種情況下非常有用,可以防止非規範化的資料不同步。
* 在文件資料模型中,需要一起更新的欄位通常在同一個文件中,這被視為單個物件 —— 更新單個文件時不需要多物件事務。但是,缺乏連線功能的文件資料庫會鼓勵非規範化(請參閱 “[關係型資料庫與文件資料庫在今日的對比](/tw/ch2#關係型資料庫與文件資料庫在今日的對比)”)。當需要更新非規範化的資訊時,如 [圖 7-2](/img/fig7-2.png) 所示,需要一次更新多個文件。事務在這種情況下非常有用,可以防止非規範化的資料不同步。
* 在具有次級索引的資料庫中(除了純粹的鍵值儲存以外幾乎都有),每次更改值時都需要更新索引。從事務角度來看,這些索引是不同的資料庫物件:例如,如果沒有事務隔離性,記錄可能出現在一個索引中,但沒有出現在另一個索引中,因為第二個索引的更新還沒有發生。
這些應用仍然可以在沒有事務的情況下實現。然而,**沒有原子性,錯誤處理就要複雜得多,缺乏隔離性,就會導致併發問題**。我們將在 “[弱隔離級別](#弱隔離級別)” 中討論這些問題,並在 [第十二章](ch12.md) 中探討其他方法。
這些應用仍然可以在沒有事務的情況下實現。然而,**沒有原子性,錯誤處理就要複雜得多,缺乏隔離性,就會導致併發問題**。我們將在 “[弱隔離級別](#弱隔離級別)” 中討論這些問題,並在 [第十二章](/tw/ch12) 中探討其他方法。
#### 處理錯誤和中止
事務的一個關鍵特性是如果發生錯誤它可以中止並安全地重試。ACID 資料庫基於這樣的哲學:如果資料庫有違反其原子性,隔離性或永續性的危險,則寧願完全放棄事務,而不是留下半成品。
然而並不是所有的系統都遵循這個哲學。特別是具有 [無主複製](ch5.md#無主複製) 的資料儲存,主要是在 “盡力而為” 的基礎上進行工作。可以概括為 “資料庫將做盡可能多的事,執行遇到錯誤時,它不會撤消它已經完成的事情” —— 所以,從錯誤中恢復是應用程式的責任。
然而並不是所有的系統都遵循這個哲學。特別是具有 [無主複製](/tw/ch5#無主複製) 的資料儲存,主要是在 “盡力而為” 的基礎上進行工作。可以概括為 “資料庫將做盡可能多的事,執行遇到錯誤時,它不會撤消它已經完成的事情” —— 所以,從錯誤中恢復是應用程式的責任。
錯誤發生不可避免,但許多軟體開發人員傾向於只考慮樂觀情況,而不是錯誤處理的複雜性。例如,像 Rails 的 ActiveRecord 和 Django 這樣的 **物件關係對映ORM, object-relation Mapping** 框架不會重試中斷的事務 —— 這個錯誤通常會導致一個從堆疊向上傳播的異常,所以任何使用者輸入都會被丟棄,使用者拿到一個錯誤資訊。這實在是太恥辱了,因為中止的重點就是允許安全的重試。
@@ -207,7 +208,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
- 如果事務實際上成功了,但是在伺服器試圖向客戶端確認提交成功時網路發生故障(所以客戶端認為提交失敗了),那麼重試事務會導致事務被執行兩次 —— 除非你有一個額外的應用級去重機制。
- 如果錯誤是由於負載過大造成的,則重試事務將使問題變得更糟,而不是更好。為了避免這種正反饋迴圈,可以限制重試次數,使用指數退避演算法,並單獨處理與過載相關的錯誤(如果允許)。
- 僅在臨時性錯誤(例如,由於死鎖,異常情況,臨時性網路中斷和故障切換)後才值得重試。在發生永久性錯誤(例如,違反約束)之後重試是毫無意義的。
- 如果事務在資料庫之外也有副作用,即使事務被中止,也可能發生這些副作用。例如,如果你正在傳送電子郵件,那你肯定不希望每次重試事務時都重新發送電子郵件。如果你想確保幾個不同的系統一起提交或放棄,**兩階段提交2PC, two-phase commit** 可以提供幫助(“[原子提交與兩階段提交](ch9.md#原子提交與兩階段提交)” 中將討論這個問題)。
- 如果事務在資料庫之外也有副作用,即使事務被中止,也可能發生這些副作用。例如,如果你正在傳送電子郵件,那你肯定不希望每次重試事務時都重新發送電子郵件。如果你想確保幾個不同的系統一起提交或放棄,**兩階段提交2PC, two-phase commit** 可以提供幫助(“[原子提交與兩階段提交](/tw/ch9#原子提交與兩階段提交)” 中將討論這個問題)。
- 如果客戶端程序在重試中失效,任何試圖寫入資料庫的資料都將丟失。
## 弱隔離級別
@@ -243,16 +244,16 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
設想一個事務已經將一些資料寫入資料庫,但事務還沒有提交或中止。另一個事務可以看到未提交的資料嗎?如果是的話,那就叫做 **髒讀dirty reads**【2】。
**讀已提交** 隔離級別執行的事務必須防止髒讀。這意味著事務的任何寫入操作只有在該事務提交時才能被其他人看到(然後所有的寫入操作都會立即變得可見)。如 [圖 7-4](../img/fig7-4.png) 所示,使用者 1 設定了 `x = 3`,但使用者 2 的 `get x` 仍舊返回舊值 2 (當用戶 1 尚未提交時)。
**讀已提交** 隔離級別執行的事務必須防止髒讀。這意味著事務的任何寫入操作只有在該事務提交時才能被其他人看到(然後所有的寫入操作都會立即變得可見)。如 [圖 7-4](/img/fig7-4.png) 所示,使用者 1 設定了 `x = 3`,但使用者 2 的 `get x` 仍舊返回舊值 2 (當用戶 1 尚未提交時)。
![](../img/fig7-4.png)
![](/img/fig7-4.png)
**圖 7-4 沒有髒讀:使用者 2 只有在使用者 1 的事務已經提交後才能看到 x 的新值。**
為什麼要防止髒讀,有幾個原因:
- 如果事務需要更新多個物件,髒讀取意味著另一個事務可能會只看到一部分更新。例如,在 [圖 7-2](../img/fig7-2.png) 中,使用者看到新的未讀電子郵件,但看不到更新的計數器。這就是電子郵件的髒讀。看到處於部分更新狀態的資料庫會讓使用者感到困惑,並可能導致其他事務做出錯誤的決定。
- 如果事務中止,則所有寫入操作都需要回滾(如 [圖 7-3](../img/fig7-3.png) 所示)。如果資料庫允許髒讀,那就意味著一個事務可能會看到稍後需要回滾的資料,即從未實際提交給資料庫的資料。想想後果就讓人頭大。
- 如果事務需要更新多個物件,髒讀取意味著另一個事務可能會只看到一部分更新。例如,在 [圖 7-2](/img/fig7-2.png) 中,使用者看到新的未讀電子郵件,但看不到更新的計數器。這就是電子郵件的髒讀。看到處於部分更新狀態的資料庫會讓使用者感到困惑,並可能導致其他事務做出錯誤的決定。
- 如果事務中止,則所有寫入操作都需要回滾(如 [圖 7-3](/img/fig7-3.png) 所示)。如果資料庫允許髒讀,那就意味著一個事務可能會看到稍後需要回滾的資料,即從未實際提交給資料庫的資料。想想後果就讓人頭大。
#### 沒有髒寫
@@ -262,10 +263,10 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
透過防止髒寫,這個隔離級別避免了一些併發問題:
- 如果事務更新多個物件,髒寫會導致不好的結果。例如,考慮 [圖 7-5](../img/fig7-5.png)以一個二手車銷售網站為例Alice 和 Bob 兩個人同時試圖購買同一輛車。購買汽車需要兩次資料庫寫入:網站上的商品列表需要更新,以反映買家的購買,銷售發票需要傳送給買家。在 [圖 7-5](../img/fig7-5.png) 的情況下,銷售是屬於 Bob 的(因為他成功更新了商品列表),但發票卻寄送給了 Alice因為她成功更新了發票表。讀已提交會防止這樣的事故。
- 但是,讀已提交併不能防止 [圖 7-1](../img/fig7-1.png) 中兩個計數器增量之間的競爭狀態。在這種情況下,第二次寫入發生在第一個事務提交後,所以它不是一個髒寫。這仍然是不正確的,但是出於不同的原因,在 “[防止丟失更新](#防止丟失更新)” 中將討論如何使這種計數器增量安全。
- 如果事務更新多個物件,髒寫會導致不好的結果。例如,考慮 [圖 7-5](/img/fig7-5.png)以一個二手車銷售網站為例Alice 和 Bob 兩個人同時試圖購買同一輛車。購買汽車需要兩次資料庫寫入:網站上的商品列表需要更新,以反映買家的購買,銷售發票需要傳送給買家。在 [圖 7-5](/img/fig7-5.png) 的情況下,銷售是屬於 Bob 的(因為他成功更新了商品列表),但發票卻寄送給了 Alice因為她成功更新了發票表。讀已提交會防止這樣的事故。
- 但是,讀已提交併不能防止 [圖 7-1](/img/fig7-1.png) 中兩個計數器增量之間的競爭狀態。在這種情況下,第二次寫入發生在第一個事務提交後,所以它不是一個髒寫。這仍然是不正確的,但是出於不同的原因,在 “[防止丟失更新](#防止丟失更新)” 中將討論如何使這種計數器增量安全。
![](../img/fig7-5.png)
![](/img/fig7-5.png)
**圖 7-5 如果存在髒寫,來自不同事務的衝突寫入可能會混淆在一起**
@@ -279,7 +280,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
但是要求讀鎖的辦法在實踐中效果並不好。因為一個長時間執行的寫入事務會迫使許多隻讀事務等到這個慢寫入事務完成。這會影響只讀事務的響應時間,並且不利於可操作性:因為等待鎖,應用某個部分的遲緩可能由於連鎖效應,導致其他部分出現問題。
出於這個原因,大多數資料庫 [^vi] 使用 [圖 7-4](../img/fig7-4.png) 的方式防止髒讀:對於寫入的每個物件,資料庫都會記住舊的已提交值,和由當前持有寫入鎖的事務設定的新值。當事務正在進行時,任何其他讀取物件的事務都會拿到舊值。只有當新值提交後,事務才會切換到讀取新值。
出於這個原因,大多數資料庫 [^vi] 使用 [圖 7-4](/img/fig7-4.png) 的方式防止髒讀:對於寫入的每個物件,資料庫都會記住舊的已提交值,和由當前持有寫入鎖的事務設定的新值。當事務正在進行時,任何其他讀取物件的事務都會拿到舊值。只有當新值提交後,事務才會切換到讀取新值。
[^vi]: 在撰寫本文時,唯一在讀已提交隔離級別使用讀鎖的主流資料庫是 IBM DB2 和使用 `read_committed_snapshot = off` 配置的 Microsoft SQL Server【23,36】。
@@ -287,9 +288,9 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
如果只從表面上看讀已提交隔離級別,你可能就認為它完成了事務所需的一切,這是情有可原的。它允許 **中止**(原子性的要求);它防止讀取不完整的事務結果,並且防止併發寫入造成的混亂。事實上這些功能非常有用,比起沒有事務的系統來,可以提供更多的保證。
但是在使用此隔離級別時,仍然有很多地方可能會產生併發錯誤。例如 [圖 7-6](../img/fig7-6.png) 說明了讀已提交時可能發生的問題。
但是在使用此隔離級別時,仍然有很多地方可能會產生併發錯誤。例如 [圖 7-6](/img/fig7-6.png) 說明了讀已提交時可能發生的問題。
![](../img/fig7-6.png)
![](/img/fig7-6.png)
**圖 7-6 讀取偏差Alice 觀察資料庫處於不一致的狀態**
@@ -297,7 +298,7 @@ Alice 在銀行有 1000 美元的儲蓄,分為兩個賬戶,每個 500 美元
這種異常被稱為 **不可重複讀nonrepeatable read****讀取偏差read skew**:如果 Alice 在事務結束時再次讀取賬戶 1 的餘額她將看到與她之前的查詢中看到的不同的值600 美元)。在讀已提交的隔離條件下,**不可重複讀** 被認為是可接受的Alice 看到的帳戶餘額確實在閱讀時已經提交了。
> 不幸的是,術語 **偏差skew** 這個詞是過載的:以前使用它是因為熱點的不平衡工作量(請參閱 “[負載偏斜與熱點消除](ch6.md#負載偏斜與熱點消除)”),而這裡偏差意味著異常的時序。
> 不幸的是,術語 **偏差skew** 這個詞是過載的:以前使用它是因為熱點的不平衡工作量(請參閱 “[負載偏斜與熱點消除](/tw/ch6#負載偏斜與熱點消除)”),而這裡偏差意味著異常的時序。
對於 Alice 的情況,這不是一個長期持續的問題。因為如果她幾秒鐘後重新整理銀行網站的頁面,她很可能會看到一致的帳戶餘額。但是有些情況下,不能容忍這種暫時的不一致:
@@ -307,7 +308,7 @@ Alice 在銀行有 1000 美元的儲蓄,分為兩個賬戶,每個 500 美元
* 分析查詢和完整性檢查
有時,你可能需要執行一個查詢,掃描大部分的資料庫。這樣的查詢在分析中很常見(請參閱 “[事務處理還是分析?](ch3.md#事務處理還是分析?)”),也可能是定期完整性檢查(即監視資料損壞)的一部分。如果這些查詢在不同時間點觀察資料庫的不同部分,則可能會返回毫無意義的結果。
有時,你可能需要執行一個查詢,掃描大部分的資料庫。這樣的查詢在分析中很常見(請參閱 “[事務處理還是分析?](/tw/ch3#事務處理還是分析?)”),也可能是定期完整性檢查(即監視資料損壞)的一部分。如果這些查詢在不同時間點觀察資料庫的不同部分,則可能會返回毫無意義的結果。
**快照隔離snapshot isolation**【28】是這個問題最常見的解決方案。想法是每個事務都從資料庫的 **一致快照consistent snapshot** 中讀取 —— 也就是說,事務可以看到事務開始時在資料庫中提交的所有資料。即使這些資料隨後被另一個事務更改,每個事務也只能看到該特定時間點的舊資料。
@@ -319,15 +320,15 @@ Alice 在銀行有 1000 美元的儲蓄,分為兩個賬戶,每個 500 美元
與讀取提交的隔離類似,快照隔離的實現通常使用寫鎖來防止髒寫(請參閱 “[讀已提交](#讀已提交)”),這意味著進行寫入的事務會阻止另一個事務修改同一個物件。但是讀取則不需要加鎖。從效能的角度來看,快照隔離的一個關鍵原則是:**讀不阻塞寫,寫不阻塞讀**。這允許資料庫在處理一致性快照上的長時間查詢時,可以正常地同時處理寫入操作,且兩者間沒有任何鎖爭用。
為了實現快照隔離,資料庫使用了我們看到的用於防止 [圖 7-4](../img/fig7-4.png) 中的髒讀的機制的一般化。資料庫必須可能保留一個物件的幾個不同的提交版本,因為各種正在進行的事務可能需要看到資料庫在不同的時間點的狀態。因為它同時維護著單個物件的多個版本,所以這種技術被稱為 **多版本併發控制MVCC, multi-version concurrency control**
為了實現快照隔離,資料庫使用了我們看到的用於防止 [圖 7-4](/img/fig7-4.png) 中的髒讀的機制的一般化。資料庫必須可能保留一個物件的幾個不同的提交版本,因為各種正在進行的事務可能需要看到資料庫在不同的時間點的狀態。因為它同時維護著單個物件的多個版本,所以這種技術被稱為 **多版本併發控制MVCC, multi-version concurrency control**
如果一個數據庫只需要提供 **讀已提交** 的隔離級別,而不提供 **快照隔離**,那麼保留一個物件的兩個版本就足夠了:已提交的版本和被覆蓋但尚未提交的版本。不過支援快照隔離的儲存引擎通常也使用 MVCC 來實現 **讀已提交** 隔離級別。一種典型的方法是 **讀已提交** 為每個查詢使用單獨的快照,而 **快照隔離** 對整個事務使用相同的快照。
[圖 7-7](../img/fig7-7.png) 說明了 PostgreSQL 如何實現基於 MVCC 的快照隔離【31】其他實現類似。當一個事務開始時它被賦予一個唯一的永遠增長 [^vii] 的事務 ID`txid`)。每當事務向資料庫寫入任何內容時,它所寫入的資料都會被標記上寫入者的事務 ID。
[圖 7-7](/img/fig7-7.png) 說明了 PostgreSQL 如何實現基於 MVCC 的快照隔離【31】其他實現類似。當一個事務開始時它被賦予一個唯一的永遠增長 [^vii] 的事務 ID`txid`)。每當事務向資料庫寫入任何內容時,它所寫入的資料都會被標記上寫入者的事務 ID。
[^vii]: 事實上,事務 ID 是 32 位整數,所以大約會在 40 億次事務之後溢位。PostgreSQL 的 Vacuum 過程會清理老舊的事務 ID確保事務 ID 溢位(回捲)不會影響到資料。
![](../img/fig7-7.png)
![](/img/fig7-7.png)
**圖 7-7 使用多版本物件實現快照隔離**
@@ -335,7 +336,7 @@ Alice 在銀行有 1000 美元的儲蓄,分為兩個賬戶,每個 500 美元
[^譯註ii]: 在 PostgreSQL 中,`created_by` 的實際名稱為 `xmin``deleted_by` 的實際名稱為 `xmax`
`UPDATE` 操作在內部翻譯為 `DELETE``INSERT` 。例如,在 [圖 7-7](../img/fig7-7.png) 中,事務 13 從賬戶 2 中扣除 100 美元,將餘額從 500 美元改為 400 美元。實際上包含兩條賬戶 2 的記錄:餘額為 \$500 的行被標記為 **被事務 13 刪除**,餘額為 \$400 的行 **由事務 13 建立**
`UPDATE` 操作在內部翻譯為 `DELETE``INSERT` 。例如,在 [圖 7-7](/img/fig7-7.png) 中,事務 13 從賬戶 2 中扣除 100 美元,將餘額從 500 美元改為 400 美元。實際上包含兩條賬戶 2 的記錄:餘額為 \$500 的行被標記為 **被事務 13 刪除**,餘額為 \$400 的行 **由事務 13 建立**
#### 觀察一致性快照的可見性規則
@@ -346,7 +347,7 @@ Alice 在銀行有 1000 美元的儲蓄,分為兩個賬戶,每個 500 美元
3. 由具有較晚事務 ID在當前事務開始之後開始的的事務所做的任何寫入都被忽略而不管這些事務是否已經提交。
4. 所有其他寫入,對應用都是可見的。
這些規則適用於建立和刪除物件。在 [圖 7-7](../img/fig7-7.png) 中,當事務 12 從賬戶 2 讀取時,它會看到 \$500 的餘額,因為 \$500 餘額的刪除是由事務 13 完成的(根據規則 3事務 12 看不到事務 13 執行的刪除),且 400 美元記錄的建立也是不可見的(按照相同的規則)。
這些規則適用於建立和刪除物件。在 [圖 7-7](/img/fig7-7.png) 中,當事務 12 從賬戶 2 讀取時,它會看到 \$500 的餘額,因為 \$500 餘額的刪除是由事務 13 完成的(根據規則 3事務 12 看不到事務 13 執行的刪除),且 400 美元記錄的建立也是不可見的(按照相同的規則)。
換句話說,如果以下兩個條件都成立,則可見一個物件:
@@ -361,7 +362,7 @@ Alice 在銀行有 1000 美元的儲蓄,分為兩個賬戶,每個 500 美元
在實踐中許多實現細節決定了多版本併發控制的效能。例如如果同一物件的不同版本可以放入同一個頁面中PostgreSQL 的最佳化可以避免更新索引【31】。
在 CouchDB、Datomic 和 LMDB 中使用另一種方法。雖然它們也使用 [B 樹](ch3.md#B樹),但它們使用的是一種 **僅追加 / 寫時複製append-only/copy-on-write** 的變體它們在更新時不覆蓋樹的頁面而為每個修改頁面建立一份副本。從父頁面直到樹根都會級聯更新以指向它們子頁面的新版本。任何不受寫入影響的頁面都不需要被複制並且保持不變【33,34,35】。
在 CouchDB、Datomic 和 LMDB 中使用另一種方法。雖然它們也使用 [B 樹](/tw/ch3#B樹),但它們使用的是一種 **僅追加 / 寫時複製append-only/copy-on-write** 的變體它們在更新時不覆蓋樹的頁面而為每個修改頁面建立一份副本。從父頁面直到樹根都會級聯更新以指向它們子頁面的新版本。任何不受寫入影響的頁面都不需要被複制並且保持不變【33,34,35】。
使用僅追加的 B 樹,每個寫入事務(或一批事務)都會建立一棵新的 B 樹,當建立時,從該特定樹根生長的樹就是資料庫的一個一致性快照。沒必要根據事務 ID 過濾掉物件,因為後續寫入不能修改現有的 B 樹;它們只能建立新的樹根。但這種方法也需要一個負責壓縮和垃圾收集的後臺程序。
@@ -379,7 +380,7 @@ Alice 在銀行有 1000 美元的儲蓄,分為兩個賬戶,每個 500 美元
到目前為止已經討論的 **讀已提交****快照隔離** 級別,主要保證了 **只讀事務在併發寫入時** 可以看到什麼。卻忽略了兩個事務併發寫入的問題 —— 我們只討論了髒寫(請參閱 “[沒有髒寫](#沒有髒寫)”),一種特定型別的寫 - 寫衝突是可能出現的。
併發的寫入事務之間還有其他幾種有趣的衝突。其中最著名的是 **丟失更新lost update** 問題,如 [圖 7-1](../img/fig7-1.png) 所示,以兩個併發計數器增量為例。
併發的寫入事務之間還有其他幾種有趣的衝突。其中最著名的是 **丟失更新lost update** 問題,如 [圖 7-1](/img/fig7-1.png) 所示,以兩個併發計數器增量為例。
如果應用從資料庫中讀取一些值,修改它並寫回修改的值(讀取 - 修改 - 寫入序列),則可能會發生丟失更新的問題。如果兩個事務同時執行,則其中一個的修改可能會丟失,因為第二個寫入的內容並沒有包括第一個事務的修改(有時會說後面寫入 **狠揍clobber** 了前面的寫入)這種模式發生在各種不同的情況下:
@@ -399,7 +400,7 @@ UPDATE counters SET value = value + 1 WHERE key = 'foo';
類似地,像 MongoDB 這樣的文件資料庫提供了對 JSON 文件的一部分進行本地修改的原子操作Redis 提供了修改資料結構(如優先順序佇列)的原子操作。並不是所有的寫操作都可以用原子操作的方式來表達,例如 wiki 頁面的更新涉及到任意文字編輯 [^viii],但是在可以使用原子操作的情況下,它們通常是最好的選擇。
[^viii]: 將文字文件的編輯表示為原子的變化流是可能的,儘管相當複雜。請參閱 “[自動衝突解決](ch5.md#自動衝突解決)”。
[^viii]: 將文字文件的編輯表示為原子的變化流是可能的,儘管相當複雜。請參閱 “[自動衝突解決](/tw/ch5#自動衝突解決)”。
原子操作通常透過在讀取物件時,獲取其上的排它鎖來實現。以便更新完成之前沒有其他事務可以讀取它。這種技術有時被稱為 **遊標穩定性cursor stability**【36,37】。另一個選擇是簡單地強制所有的原子操作在單一執行緒上執行。
@@ -452,15 +453,15 @@ UPDATE wiki_pages SET content = '新內容'
#### 衝突解決和複製
在複製資料庫中(請參閱 [第五章](ch5.md)),防止丟失的更新需要考慮另一個維度:由於在多個節點上存在資料副本,並且在不同節點上的資料可能被併發地修改,因此需要採取一些額外的步驟來防止丟失更新。
在複製資料庫中(請參閱 [第五章](/tw/ch5)),防止丟失的更新需要考慮另一個維度:由於在多個節點上存在資料副本,並且在不同節點上的資料可能被併發地修改,因此需要採取一些額外的步驟來防止丟失更新。
鎖和 CAS 操作假定只有一個最新的資料副本。但是多主或無主複製的資料庫通常允許多個寫入併發執行,並非同步複製到副本上,因此無法保證只有一個最新資料的副本。所以基於鎖或 CAS 操作的技術不適用於這種情況(我們將在 “[線性一致性](ch9.md#線性一致性)” 中更詳細地討論這個問題)。
鎖和 CAS 操作假定只有一個最新的資料副本。但是多主或無主複製的資料庫通常允許多個寫入併發執行,並非同步複製到副本上,因此無法保證只有一個最新資料的副本。所以基於鎖或 CAS 操作的技術不適用於這種情況(我們將在 “[線性一致性](/tw/ch9#線性一致性)” 中更詳細地討論這個問題)。
相反,如 “[檢測併發寫入](ch5.md#檢測併發寫入)” 一節所述,這種複製資料庫中的一種常見方法是允許併發寫入建立多個衝突版本的值(也稱為兄弟),並使用應用程式碼或特殊資料結構在事實發生之後解決和合並這些版本。
相反,如 “[檢測併發寫入](/tw/ch5#檢測併發寫入)” 一節所述,這種複製資料庫中的一種常見方法是允許併發寫入建立多個衝突版本的值(也稱為兄弟),並使用應用程式碼或特殊資料結構在事實發生之後解決和合並這些版本。
原子操作可以在複製的上下文中很好地工作,尤其當它們具有可交換性時(即,可以在不同的副本上以不同的順序應用它們,且仍然可以得到相同的結果)。例如,遞增計數器或向集合新增元素是可交換的操作。這是 Riak 2.0 資料型別背後的思想它可以防止複製副本丟失更新。當不同的客戶端同時更新一個值時Riak 自動將更新合併在一起以免丟失更新【39】。
另一方面最後寫入勝利LWW的衝突解決方法很容易丟失更新如 “[最後寫入勝利(丟棄併發寫入)](ch5.md#最後寫入勝利(丟棄併發寫入))” 中所述。不幸的是LWW 是許多複製資料庫中的預設方案。
另一方面最後寫入勝利LWW的衝突解決方法很容易丟失更新如 “[最後寫入勝利(丟棄併發寫入)](/tw/ch5#最後寫入勝利(丟棄併發寫入))” 中所述。不幸的是LWW 是許多複製資料庫中的預設方案。
### 寫入偏差與幻讀
@@ -470,9 +471,9 @@ UPDATE wiki_pages SET content = '新內容'
首先想象一下這個例子你正在為醫院寫一個醫生輪班管理程式。醫院通常會同時要求幾位醫生待命但底線是至少有一位醫生在待命。醫生可以放棄他們的班次例如如果他們自己生病了只要至少有一個同事在這一班中繼續工作【40,41】。
現在想象一下Alice 和 Bob 是兩位值班醫生。兩人都感到不適,所以他們都決定請假。不幸的是,他們恰好在同一時間點選按鈕下班。[圖 7-8](../img/fig7-8.png) 說明了接下來的事情。
現在想象一下Alice 和 Bob 是兩位值班醫生。兩人都感到不適,所以他們都決定請假。不幸的是,他們恰好在同一時間點選按鈕下班。[圖 7-8](/img/fig7-8.png) 說明了接下來的事情。
![](../img/fig7-8.png)
![](/img/fig7-8.png)
**圖 7-8 寫入偏差導致應用程式錯誤的示例**
@@ -595,7 +596,7 @@ COMMIT;
- **兩階段鎖定2PL, two-phase locking**,幾十年來唯一可行的選擇(請參閱 “[兩階段鎖定](#兩階段鎖定)”)
- 樂觀併發控制技術,例如 **可序列化快照隔離**serializable snapshot isolation請參閱 “[可序列化快照隔離](#可序列化快照隔離)”)
現在將主要在單節點資料庫的背景下討論這些技術;在 [第九章](ch9.md) 中,我們將研究如何將它們推廣到涉及分散式系統中多個節點的事務。
現在將主要在單節點資料庫的背景下討論這些技術;在 [第九章](/tw/ch9) 中,我們將研究如何將它們推廣到涉及分散式系統中多個節點的事務。
### 真的序列執行
@@ -605,8 +606,8 @@ COMMIT;
兩個進展引發了這個反思:
- RAM 足夠便宜了,許多場景現在都可以將完整的活躍資料集儲存在記憶體中(請參閱 “[在記憶體中儲存一切](ch3.md#在記憶體中儲存一切)”)。當事務需要訪問的所有資料都在記憶體中時,事務處理的執行速度要比等待資料從磁碟載入時快得多。
- 資料庫設計人員意識到 OLTP 事務通常很短,而且只進行少量的讀寫操作(請參閱 “[事務處理還是分析?](ch3.md#事務處理還是分析?)”)。相比之下,長時間執行的分析查詢通常是隻讀的,因此它們可以在序列執行迴圈之外的一致快照(使用快照隔離)上執行。
- RAM 足夠便宜了,許多場景現在都可以將完整的活躍資料集儲存在記憶體中(請參閱 “[在記憶體中儲存一切](/tw/ch3#在記憶體中儲存一切)”)。當事務需要訪問的所有資料都在記憶體中時,事務處理的執行速度要比等待資料從磁碟載入時快得多。
- 資料庫設計人員意識到 OLTP 事務通常很短,而且只進行少量的讀寫操作(請參閱 “[事務處理還是分析?](/tw/ch3#事務處理還是分析?)”)。相比之下,長時間執行的分析查詢通常是隻讀的,因此它們可以在序列執行迴圈之外的一致快照(使用快照隔離)上執行。
序列執行事務的方法在 VoltDB/H-Store、Redis 和 Datomic 中實現【46,47,48】。設計用於單執行緒執行的系統有時可以比支援併發的系統性能更好因為它可以避免鎖的協調開銷。但是其吞吐量僅限於單個 CPU 核的吞吐量。為了充分利用單一執行緒,需要有與傳統形式的事務不同的結構。
@@ -620,9 +621,9 @@ COMMIT;
在這種互動式的事務方式中,應用程式和資料庫之間的網路通訊耗費了大量的時間。如果不允許在資料庫中進行併發處理,且一次只處理一個事務,則吞吐量將會非常糟糕,因為資料庫大部分的時間都花費在等待應用程式發出當前事務的下一個查詢。在這種資料庫中,為了獲得合理的效能,需要同時處理多個事務。
出於這個原因,具有單執行緒序列事務處理的系統不允許互動式的多語句事務。取而代之,應用程式必須提前將整個事務程式碼作為儲存過程提交給資料庫。這些方法之間的差異如 [圖 7-9](../img/fig7-9.png) 所示。如果事務所需的所有資料都在記憶體中,則儲存過程可以非常快地執行,而不用等待任何網路或磁碟 I/O。
出於這個原因,具有單執行緒序列事務處理的系統不允許互動式的多語句事務。取而代之,應用程式必須提前將整個事務程式碼作為儲存過程提交給資料庫。這些方法之間的差異如 [圖 7-9](/img/fig7-9.png) 所示。如果事務所需的所有資料都在記憶體中,則儲存過程可以非常快地執行,而不用等待任何網路或磁碟 I/O。
![](../img/fig7-9.png)
![](/img/fig7-9.png)
**圖 7-9 互動式事務和儲存過程之間的區別(使用圖 7-8 的示例事務)**
@@ -644,13 +645,13 @@ VoltDB 還使用儲存過程進行復制:但不是將事務的寫入結果從
順序執行所有事務使併發控制簡單多了,但資料庫的事務吞吐量被限制為單機單核的速度。只讀事務可以使用快照隔離在其它地方執行,但對於寫入吞吐量較高的應用,單執行緒事務處理器可能成為一個嚴重的瓶頸。
為了伸縮至多個 CPU 核心和多個節點,可以對資料進行分割槽(請參閱 [第六章](ch6.md)),在 VoltDB 中支援這樣做。如果你可以找到一種對資料集進行分割槽的方法,以便每個事務只需要在單個分割槽中讀寫資料,那麼每個分割槽就可以擁有自己獨立執行的事務處理執行緒。在這種情況下可以為每個分割槽指派一個獨立的 CPU 核,事務吞吐量就可以與 CPU 核數保持線性伸縮【47】。
為了伸縮至多個 CPU 核心和多個節點,可以對資料進行分割槽(請參閱 [第六章](/tw/ch6)),在 VoltDB 中支援這樣做。如果你可以找到一種對資料集進行分割槽的方法,以便每個事務只需要在單個分割槽中讀寫資料,那麼每個分割槽就可以擁有自己獨立執行的事務處理執行緒。在這種情況下可以為每個分割槽指派一個獨立的 CPU 核,事務吞吐量就可以與 CPU 核數保持線性伸縮【47】。
但是,對於需要訪問多個分割槽的任何事務,資料庫必須在觸及的所有分割槽之間協調事務。儲存過程需要跨越所有分割槽鎖定執行,以確保整個系統的可序列性。
由於跨分割槽事務具有額外的協調開銷所以它們比單分割槽事務慢得多。VoltDB 報告的吞吐量大約是每秒 1000 個跨分割槽寫入比單分割槽吞吐量低幾個數量級並且不能透過增加更多的機器來增加吞吐量【49】。
事務是否可以是劃分至單個分割槽很大程度上取決於應用資料的結構。簡單的鍵值資料通常可以非常容易地進行分割槽,但是具有多個次級索引的資料可能需要大量的跨分割槽協調(請參閱 “[分割槽與次級索引](ch6.md#分割槽與次級索引)”)。
事務是否可以是劃分至單個分割槽很大程度上取決於應用資料的結構。簡單的鍵值資料通常可以非常容易地進行分割槽,但是具有多個次級索引的資料可能需要大量的跨分割槽協調(請參閱 “[分割槽與次級索引](/tw/ch6#分割槽與次級索引)”)。
#### 序列執行小結
@@ -661,7 +662,7 @@ VoltDB 還使用儲存過程進行復制:但不是將事務的寫入結果從
- 寫入吞吐量必須低到能在單個 CPU 核上處理,如若不然,事務需要能劃分至單個分割槽,且不需要跨分割槽協調。
- 跨分割槽事務是可能的,但是它們能被使用的程度有很大的限制。
[^x]: 如果事務需要訪問不在記憶體中的資料,最好的解決方案可能是中止事務,非同步地將資料提取到記憶體中,同時繼續處理其他事務,然後在資料載入完畢時重新啟動事務。這種方法被稱為 **反快取anti-caching**,正如前面在 “[在記憶體中儲存一切](ch3.md#在記憶體中儲存一切)” 中所述。
[^x]: 如果事務需要訪問不在記憶體中的資料,最好的解決方案可能是中止事務,非同步地將資料提取到記憶體中,同時繼續處理其他事務,然後在資料載入完畢時重新啟動事務。這種方法被稱為 **反快取anti-caching**,正如前面在 “[在記憶體中儲存一切](/tw/ch3#在記憶體中儲存一切)” 中所述。
### 兩階段鎖定
@@ -671,14 +672,14 @@ VoltDB 還使用儲存過程進行復制:但不是將事務的寫入結果從
> #### 2PL不是2PC
>
> 請注意雖然兩階段鎖定2PL聽起來非常類似於兩階段提交2PC但它們是完全不同的東西。我們將在 [第九章](ch9.md) 討論 2PC。
> 請注意雖然兩階段鎖定2PL聽起來非常類似於兩階段提交2PC但它們是完全不同的東西。我們將在 [第九章](/tw/ch9) 討論 2PC。
之前我們看到鎖通常用於防止髒寫(請參閱 “[沒有髒寫](#沒有髒寫)” 一節):如果兩個事務同時嘗試寫入同一個物件,則鎖可確保第二個寫入必須等到第一個寫入完成事務(中止或提交),然後才能繼續。
兩階段鎖定類似,但是鎖的要求更強得多。只要沒有寫入,就允許多個事務同時讀取同一個物件。但物件只要有寫入(修改或刪除),就需要 **獨佔訪問exclusive access** 許可權:
- 如果事務 A 讀取了一個物件,並且事務 B 想要寫入該物件,那麼 B 必須等到 A 提交或中止才能繼續(這確保 B 不能在 A 底下意外地改變物件)。
- 如果事務 A 寫入了一個物件,並且事務 B 想要讀取該物件,則 B 必須等到 A 提交或中止才能繼續(像 [圖 7-1](../img/fig7-1.png) 那樣讀取舊版本的物件在 2PL 下是不可接受的)。
- 如果事務 A 寫入了一個物件,並且事務 B 想要讀取該物件,則 B 必須等到 A 提交或中止才能繼續(像 [圖 7-1](/img/fig7-1.png) 那樣讀取舊版本的物件在 2PL 下是不可接受的)。
在 2PL 中,寫入不僅會阻塞其他寫入,也會阻塞讀,反之亦然。快照隔離使得 **讀不阻塞寫,寫也不阻塞讀**(請參閱 “[實現快照隔離](#實現快照隔離)”),這是 2PL 和快照隔離之間的關鍵區別。另一方面,因為 2PL 提供了可序列化的性質,它可以防止早先討論的所有競爭條件,包括丟失更新和寫入偏差。
@@ -703,7 +704,7 @@ VoltDB 還使用儲存過程進行復制:但不是將事務的寫入結果從
傳統的關係資料庫不限制事務的持續時間,因為它們是為等待人類輸入的互動式應用而設計的。因此,當一個事務需要等待另一個事務時,等待的時長並沒有限制。即使你保證所有的事務都很短,如果有多個事務想要訪問同一個物件,那麼可能會形成一個佇列,所以事務可能需要等待幾個其他事務才能完成。
因此,執行 2PL 的資料庫可能具有相當不穩定的延遲,如果在工作負載中存在爭用,那麼可能高百分位點處的響應會非常的慢(請參閱 “[描述效能](ch1.md#描述效能)”)。可能只需要一個緩慢的事務,或者一個訪問大量資料並獲取許多鎖的事務,就能把系統的其他部分拖慢,甚至迫使系統停機。當需要穩健的操作時,這種不穩定性是有問題的。
因此,執行 2PL 的資料庫可能具有相當不穩定的延遲,如果在工作負載中存在爭用,那麼可能高百分位點處的響應會非常的慢(請參閱 “[描述效能](/tw/ch1#描述效能)”)。可能只需要一個緩慢的事務,或者一個訪問大量資料並獲取許多鎖的事務,就能把系統的其他部分拖慢,甚至迫使系統停機。當需要穩健的操作時,這種不穩定性是有問題的。
基於鎖實現的讀已提交隔離級別可能發生死鎖,但在基於 2PL 實現的可序列化隔離級別中,它們會出現的頻繁的多(取決於事務的訪問模式)。這可能是一個額外的效能問題:當事務由於死鎖而被中止並被重試時,它需要從頭重做它的工作。如果死鎖很頻繁,這可能意味著巨大的浪費。
@@ -784,9 +785,9 @@ WHERE room_id = 123 AND
#### 檢測舊MVCC讀取
回想一下快照隔離通常是透過多版本併發控制MVCC見 [圖 7-10](../img/fig7-10.png))來實現的。當一個事務從 MVCC 資料庫中的一致快照讀時,它將忽略取快照時尚未提交的任何其他事務所做的寫入。在 [圖 7-10](../img/fig7-10.png) 中,事務 43 認為 Alice 的 `on_call = true` ,因為事務 42修改 Alice 的待命狀態)未被提交。然而,在事務 43 想要提交時,事務 42 已經提交。這意味著在讀一致性快照時被忽略的寫入已經生效,事務 43 的前提不再為真。
回想一下快照隔離通常是透過多版本併發控制MVCC見 [圖 7-10](/img/fig7-10.png))來實現的。當一個事務從 MVCC 資料庫中的一致快照讀時,它將忽略取快照時尚未提交的任何其他事務所做的寫入。在 [圖 7-10](/img/fig7-10.png) 中,事務 43 認為 Alice 的 `on_call = true` ,因為事務 42修改 Alice 的待命狀態)未被提交。然而,在事務 43 想要提交時,事務 42 已經提交。這意味著在讀一致性快照時被忽略的寫入已經生效,事務 43 的前提不再為真。
![](../img/fig7-10.png)
![](/img/fig7-10.png)
**圖 7-10 檢測事務何時從 MVCC 快照讀取過時的值**
@@ -796,19 +797,19 @@ WHERE room_id = 123 AND
#### 檢測影響之前讀取的寫入
第二種情況要考慮的是另一個事務在讀取資料之後修改資料。這種情況如 [圖 7-11](../img/fig7-11.png) 所示。
第二種情況要考慮的是另一個事務在讀取資料之後修改資料。這種情況如 [圖 7-11](/img/fig7-11.png) 所示。
![](../img/fig7-11.png)
![](/img/fig7-11.png)
**圖 7-11 在可序列化快照隔離中,檢測一個事務何時修改另一個事務的讀取。**
在兩階段鎖定的上下文中,我們討論了索引範圍鎖(請參閱 “[索引範圍鎖](#索引範圍鎖)”),它允許資料庫鎖定與某個搜尋查詢匹配的所有行的訪問權,例如 `WHERE shift_id = 1234`。可以在這裡使用類似的技術,除了 SSI 鎖不會阻塞其他事務。
在 [圖 7-11](../img/fig7-11.png) 中,事務 42 和 43 都在班次 1234 查詢值班醫生。如果在 `shift_id` 上有索引,則資料庫可以使用索引項 1234 來記錄事務 42 和 43 讀取這個資料的事實。(如果沒有索引,這個資訊可以在表級別進行跟蹤)。這個資訊只需要保留一段時間:在一個事務完成(提交或中止),並且所有的併發事務完成之後,資料庫就可以忘記它讀取的資料了。
在 [圖 7-11](/img/fig7-11.png) 中,事務 42 和 43 都在班次 1234 查詢值班醫生。如果在 `shift_id` 上有索引,則資料庫可以使用索引項 1234 來記錄事務 42 和 43 讀取這個資料的事實。(如果沒有索引,這個資訊可以在表級別進行跟蹤)。這個資訊只需要保留一段時間:在一個事務完成(提交或中止),並且所有的併發事務完成之後,資料庫就可以忘記它讀取的資料了。
當事務寫入資料庫時,它必須在索引中查詢最近曾讀取受影響資料的其他事務。這個過程類似於在受影響的鍵範圍上獲取寫鎖,但鎖並不會阻塞事務直到其他讀事務完成,而是像警戒線一樣只是簡單通知其他事務:你們讀過的資料可能不是最新的啦。
在 [圖 7-11](../img/fig7-11.png) 中,事務 43 通知事務 42 其先前讀已過時,反之亦然。事務 42 首先提交併成功,儘管事務 43 的寫影響了 42 ,但因為事務 43 尚未提交,所以寫入尚未生效。然而當事務 43 想要提交時,來自事務 42 的衝突寫入已經被提交,所以事務 43 必須中止。
在 [圖 7-11](/img/fig7-11.png) 中,事務 43 通知事務 42 其先前讀已過時,反之亦然。事務 42 首先提交併成功,儘管事務 43 的寫影響了 42 ,但因為事務 43 尚未提交,所以寫入尚未生效。然而當事務 43 想要提交時,來自事務 42 的衝突寫入已經被提交,所以事務 43 必須中止。
#### 可序列化快照隔離的效能
@@ -931,11 +932,4 @@ WHERE room_id = 123 AND
1. Michael J. Cahill: “[Serializable Isolation for Snapshot Databases](https://ses.library.usyd.edu.au/bitstream/handle/2123/5353/michael-cahill-2009-thesis.pdf),” PhD Thesis, University of Sydney, July 2009.
1. D. Z. Badal: “[Correctness of Concurrency Control and Implications in Distributed Databases](http://ieeexplore.ieee.org/abstract/document/762563/),” at *3rd International IEEE Computer Software and Applications Conference* (COMPSAC), November 1979.
1. Rakesh Agrawal, Michael J. Carey, and Miron Livny: “[Concurrency Control Performance Modeling: Alternatives and Implications](http://www.eecs.berkeley.edu/~brewer/cs262/ConcControl.pdf),” *ACM Transactions on Database Systems* (TODS), volume 12, number 4, pages 609654, December 1987. [doi:10.1145/32204.32220](http://dx.doi.org/10.1145/32204.32220)
1. Dave Rosenthal: “[Databases at 14.4MHz](http://web.archive.org/web/20150427041746/http://blog.foundationdb.com/databases-at-14.4mhz),” *blog.foundationdb.com*, December 10, 2014.
------
| 上一章 | 目錄 | 下一章 |
| ---------------------- | ------------------------------- | ---------------------------------- |
| [第六章:分割槽](ch6.md) | [設計資料密集型應用](README.md) | [第八章:分散式系統的麻煩](ch8.md) |
1. Dave Rosenthal: “[Databases at 14.4MHz](http://web.archive.org/web/20150427041746/http://blog.foundationdb.com/databases-at-14.4mhz),” *blog.foundationdb.com*, December 10, 2014.

View File

@@ -1,6 +1,12 @@
# 第八章:分散式系統的麻煩
---
title: "第八章:分散式系統的麻煩"
linkTitle: "8. 分散式系統的麻煩"
weight: 208
breadcrumbs: false
---
![](../img/ch8.png)
![](/img/ch8.png)
> 邂逅相遇
>
@@ -12,11 +18,8 @@
>
> —— Kyle Kingsbury, Carly Rae Jepsen 《網路分割槽的危害》2013 年)[^譯著1]
---------
[TOC]
最近幾章中反覆出現的主題是,系統如何處理錯誤的事情。例如,我們討論了 **副本故障切換**(“[處理節點中斷](ch5.md#處理節點宕機)”),**複製延遲**(“[複製延遲問題](ch5.md#複製延遲問題)”)和事務控制(“[弱隔離級別](ch7.md#弱隔離級別)”)。當我們瞭解可能在實際系統中出現的各種邊緣情況時,我們會更好地處理它們。
最近幾章中反覆出現的主題是,系統如何處理錯誤的事情。例如,我們討論了 **副本故障切換**(“[處理節點中斷](/tw/ch5#處理節點宕機)”),**複製延遲**(“[複製延遲問題](/tw/ch5#複製延遲問題)”)和事務控制(“[弱隔離級別](/tw/ch7#弱隔離級別)”)。當我們瞭解可能在實際系統中出現的各種邊緣情況時,我們會更好地處理它們。
但是,儘管我們已經談了很多錯誤,但之前幾章仍然過於樂觀。現實更加黑暗。我們現在將悲觀主義最大化,假設任何可能出錯的東西 **都會** 出錯 [^i]。(經驗豐富的系統運維會告訴你,這是一個合理的假設。如果你問得好,他們可能會一邊治療心理創傷一邊告訴你一些可怕的故事)
@@ -24,7 +27,7 @@
使用分散式系統與在一臺計算機上編寫軟體有著根本的區別主要的區別在於有許多新穎和刺激的方法可以使事情出錯【1,2】。在這一章中我們將瞭解實踐中出現的問題理解我們能夠依賴和不可以依賴的東西。
最後,作為工程師,我們的任務是構建能夠完成工作的系統(即滿足使用者期望的保證),儘管一切都出錯了。在 [第九章](ch9.md) 中,我們將看看一些可以在分散式系統中提供這種保證的演算法的例子。但首先,在本章中,我們必須瞭解我們面臨的挑戰。
最後,作為工程師,我們的任務是構建能夠完成工作的系統(即滿足使用者期望的保證),儘管一切都出錯了。在 [第九章](/tw/ch9) 中,我們將看看一些可以在分散式系統中提供這種保證的演算法的例子。但首先,在本章中,我們必須瞭解我們面臨的挑戰。
本章對分散式系統中可能出現的問題進行徹底的悲觀和沮喪的總結。我們將研究網路的問題(“[不可靠的網路](#不可靠的網路)”); 時鐘和時序問題(“[不可靠的時鐘](#不可靠的時鐘)”); 我們將討論他們可以避免的程度。所有這些問題的後果都是困惑的,所以我們將探索如何思考一個分散式系統的狀態,以及如何推理發生的事情(“[知識、真相與謊言](#知識、真相與謊言)”)。
@@ -67,11 +70,11 @@
* 系統越大其元件之一就越有可能壞掉。隨著時間的推移壞掉的東西得到修復新的東西又壞掉但是在一個有成千上萬個節點的系統中有理由認為總是有一些東西是壞掉的【7】。當錯誤處理的策略只由簡單放棄組成時一個大的系統最終會花費大量時間從錯誤中恢復而不是做有用的工作【8】。
* 如果系統可以容忍發生故障的節點,並繼續保持整體工作狀態,那麼這對於運營和維護非常有用:例如,可以執行滾動升級(請參閱 [第四章](ch4.md)),一次重新啟動一個節點,同時繼續給使用者提供不中斷的服務。在雲環境中,如果一臺虛擬機器執行不佳,可以殺死它並請求一臺新的虛擬機器(希望新的虛擬機器速度更快)。
* 如果系統可以容忍發生故障的節點,並繼續保持整體工作狀態,那麼這對於運營和維護非常有用:例如,可以執行滾動升級(請參閱 [第四章](/tw/ch4)),一次重新啟動一個節點,同時繼續給使用者提供不中斷的服務。在雲環境中,如果一臺虛擬機器執行不佳,可以殺死它並請求一臺新的虛擬機器(希望新的虛擬機器速度更快)。
* 在地理位置分散的部署中(保持資料在地理位置上接近使用者以減少訪問延遲),通訊很可能透過網際網路進行,與本地網路相比,通訊速度緩慢且不可靠。超級計算機通常假設它們的所有節點都靠近在一起。
如果要使分散式系統工作,就必須接受部分故障的可能性,並在軟體中建立容錯機制。換句話說,我們需要從不可靠的元件構建一個可靠的系統(正如 “[可靠性](ch1.md#可靠性)” 中所討論的那樣,沒有完美的可靠性,所以我們需要理解我們可以實際承諾的極限)。
如果要使分散式系統工作,就必須接受部分故障的可能性,並在軟體中建立容錯機制。換句話說,我們需要從不可靠的元件構建一個可靠的系統(正如 “[可靠性](/tw/ch1#可靠性)” 中所討論的那樣,沒有完美的可靠性,所以我們需要理解我們可以實際承諾的極限)。
即使在只有少數節點的小型系統中,考慮部分故障也是很重要的。在一個小系統中,很可能大部分元件在大部分時間都正常工作。然而,遲早會有一部分系統出現故障,軟體必須以某種方式處理。故障處理必須是軟體設計的一部分,並且作為軟體的運維,你需要知道在發生故障的情況下,軟體可能會表現出怎樣的行為。
@@ -86,16 +89,16 @@
>
> 雖然這個系統可以比它的底層部分更可靠但它的可靠性總是有限的。例如糾錯碼可以處理少量的單位元錯誤但是如果你的訊號被幹擾所淹沒那麼透過通道可以得到多少資料是有根本性的限制的【13】。TCP 可以隱藏資料包的丟失,重複和重新排序,但是它不能神奇地消除網路中的延遲。
>
> 雖然更可靠的高階系統並不完美,但它仍然有用,因為它處理了一些棘手的低階錯誤,所以其餘的錯誤通常更容易推理和處理。我們將在 “[資料庫的端到端原則](ch12.md#資料庫的端到端原則)” 中進一步探討這個問題。
> 雖然更可靠的高階系統並不完美,但它仍然有用,因為它處理了一些棘手的低階錯誤,所以其餘的錯誤通常更容易推理和處理。我們將在 “[資料庫的端到端原則](/tw/ch12#資料庫的端到端原則)” 中進一步探討這個問題。
## 不可靠的網路
正如在 [第二部分](part-ii.md) 的介紹中所討論的那樣,我們在本書中關注的分散式系統是無共享的系統,即透過網路連線的一堆機器。網路是這些機器可以通訊的唯一途徑 —— 我們假設每臺機器都有自己的記憶體和磁碟,一臺機器不能訪問另一臺機器的記憶體或磁碟(除了透過網路向伺服器發出請求)。
正如在 [第二部分](/tw/part-ii) 的介紹中所討論的那樣,我們在本書中關注的分散式系統是無共享的系統,即透過網路連線的一堆機器。網路是這些機器可以通訊的唯一途徑 —— 我們假設每臺機器都有自己的記憶體和磁碟,一臺機器不能訪問另一臺機器的記憶體或磁碟(除了透過網路向伺服器發出請求)。
**無共享** 並不是構建系統的唯一方式,但它已經成為構建網際網路服務的主要方式,其原因如下:相對便宜,因為它不需要特殊的硬體,可以利用商品化的雲計算服務,透過跨多個地理分佈的資料中心進行冗餘可以實現高可靠性。
網際網路和資料中心(通常是乙太網)中的大多數內部網路都是 **非同步分組網路asynchronous packet networks**。在這種網路中,一個節點可以向另一個節點發送一個訊息(一個數據包),但是網路不能保證它什麼時候到達,或者是否到達。如果你傳送請求並期待響應,則很多事情可能會出錯(其中一些如 [圖 8-1](../img/fig8-1.png) 所示):
網際網路和資料中心(通常是乙太網)中的大多數內部網路都是 **非同步分組網路asynchronous packet networks**。在這種網路中,一個節點可以向另一個節點發送一個訊息(一個數據包),但是網路不能保證它什麼時候到達,或者是否到達。如果你傳送請求並期待響應,則很多事情可能會出錯(其中一些如 [圖 8-1](/img/fig8-1.png) 所示):
1. 請求可能已經丟失(可能有人拔掉了網線)。
2. 請求可能正在排隊,稍後將交付(也許網路或接收方過載)。
@@ -104,7 +107,7 @@
5. 遠端節點可能已經處理了請求,但是網路上的響應已經丟失(可能是網路交換機配置錯誤)。
6. 遠端節點可能已經處理了請求,但是響應已經被延遲,並且稍後將被傳遞(可能是網路或者你自己的機器過載)。
![](../img/fig8-1.png)
![](/img/fig8-1.png)
**圖 8-1 如果傳送請求並沒有得到響應則無法區分a請求是否丟失b遠端節點是否關閉c響應是否丟失。**
@@ -122,20 +125,20 @@
> #### 網路分割槽
>
> 當網路的一部分由於網路故障而被切斷時,有時稱為 **網路分割槽network partition****網路斷裂netsplit**。在本書中,我們通常會堅持使用更一般的術語 **網路故障network fault**,以避免與 [第六章](ch6.md) 討論的儲存系統的分割槽(分片)相混淆。
> 當網路的一部分由於網路故障而被切斷時,有時稱為 **網路分割槽network partition****網路斷裂netsplit**。在本書中,我們通常會堅持使用更一般的術語 **網路故障network fault**,以避免與 [第六章](/tw/ch6) 討論的儲存系統的分割槽(分片)相混淆。
即使網路故障在你的環境中非常罕見,故障可能發生的事實,意味著你的軟體需要能夠處理它們。無論何時透過網路進行通訊,都可能會失敗,這是無法避免的。
如果網路故障的錯誤處理沒有定義與測試武斷地講各種錯誤可能都會發生例如即使網路恢復【20】叢集可能會發生 **死鎖**永久無法為請求提供服務甚至可能會刪除所有的資料【21】。如果軟體被置於意料之外的情況下它可能會做出出乎意料的事情。
處理網路故障並不意味著容忍它們:如果你的網路通常是相當可靠的,一個有效的方法可能是當你的網路遇到問題時,簡單地向用戶顯示一條錯誤資訊。但是,你確實需要知道你的軟體如何應對網路問題,並確保系統能夠從中恢復。有意識地觸發網路問題並測試系統響應(這是 Chaos Monkey 背後的想法;請參閱 “[可靠性](ch1.md#可靠性)”)。
處理網路故障並不意味著容忍它們:如果你的網路通常是相當可靠的,一個有效的方法可能是當你的網路遇到問題時,簡單地向用戶顯示一條錯誤資訊。但是,你確實需要知道你的軟體如何應對網路問題,並確保系統能夠從中恢復。有意識地觸發網路問題並測試系統響應(這是 Chaos Monkey 背後的想法;請參閱 “[可靠性](/tw/ch1#可靠性)”)。
### 檢測故障
許多系統需要自動檢測故障節點。例如:
* 負載平衡器需要停止向已死亡的節點轉發請求(從輪詢列表移出,即 out of rotation
* 在單主複製功能的分散式資料庫中,如果主庫失效,則需要將從庫之一升級為新主庫(請參閱 “[處理節點宕機](ch5.md#處理節點宕機)”)。
* 在單主複製功能的分散式資料庫中,如果主庫失效,則需要將從庫之一升級為新主庫(請參閱 “[處理節點宕機](/tw/ch5#處理節點宕機)”)。
不幸的是,網路的不確定性使得很難判斷一個節點是否工作。在某些特定的情況下,你可能會收到一些反饋資訊,明確告訴你某些事情沒有成功:
@@ -154,7 +157,7 @@
長時間的超時意味著長時間等待,直到一個節點被宣告死亡(在這段時間內,使用者可能不得不等待,或者看到錯誤資訊)。短的超時可以更快地檢測到故障,但有更高地風險誤將一個節點宣佈為失效,而該節點實際上只是暫時地變慢了(例如由於節點或網路上的負載峰值)。
過早地宣告一個節點已經死了是有問題的:如果這個節點實際上是活著的,並且正在執行一些動作(例如,傳送一封電子郵件),而另一個節點接管,那麼這個動作可能會最終執行兩次。我們將在 “[知識、真相與謊言](#知識、真相與謊言)” 以及 [第九章](ch9.md) 和 [第十一章](ch11.md) 中更詳細地討論這個問題。
過早地宣告一個節點已經死了是有問題的:如果這個節點實際上是活著的,並且正在執行一些動作(例如,傳送一封電子郵件),而另一個節點接管,那麼這個動作可能會最終執行兩次。我們將在 “[知識、真相與謊言](#知識、真相與謊言)” 以及 [第九章](/tw/ch9) 和 [第十一章](/tw/ch11) 中更詳細地討論這個問題。
當一個節點被宣告死亡時,它的職責需要轉移到其他節點,這會給其他節點和網路帶來額外的負擔。如果系統已經處於高負荷狀態,則過早宣告節點死亡會使問題更嚴重。特別是如果節點實際上沒有死亡,只是由於過載導致其響應緩慢;這時將其負載轉移到其他節點可能會導致 **級聯失效**(即 cascading failure表示在極端情況下所有節點都宣告對方死亡所有節點都將停止工作
@@ -166,12 +169,12 @@
在駕駛汽車時由於交通擁堵道路交通網路的通行時間往往不盡相同。同樣計算機網路上資料包延遲的可變性通常是由於排隊【25】
* 如果多個不同的節點同時嘗試將資料包傳送到同一目的地,則網路交換機必須將它們排隊並將它們逐個送入目標網路鏈路(如 [圖 8-2](../img/fig8-2.png) 所示)。在繁忙的網路鏈路上,資料包可能需要等待一段時間才能獲得一個插槽(這稱為網路擁塞)。如果傳入的資料太多,交換機佇列填滿,資料包將被丟棄,因此需要重新發送資料包 - 即使網路執行良好。
* 如果多個不同的節點同時嘗試將資料包傳送到同一目的地,則網路交換機必須將它們排隊並將它們逐個送入目標網路鏈路(如 [圖 8-2](/img/fig8-2.png) 所示)。在繁忙的網路鏈路上,資料包可能需要等待一段時間才能獲得一個插槽(這稱為網路擁塞)。如果傳入的資料太多,交換機佇列填滿,資料包將被丟棄,因此需要重新發送資料包 - 即使網路執行良好。
* 當資料包到達目標機器時,如果所有 CPU 核心當前都處於繁忙狀態,則來自網路的傳入請求將被作業系統排隊,直到應用程式準備好處理它為止。根據機器上的負載,這可能需要一段任意的時間。
* 在虛擬化環境中,正在執行的作業系統經常暫停幾十毫秒,因為另一個虛擬機器正在使用 CPU 核心。在這段時間內,虛擬機器不能從網路中消耗任何資料,所以傳入的資料被虛擬機器監視器 【26】排隊緩衝進一步增加了網路延遲的可變性。
* TCP 執行 **流量控制**flow control也稱為 **擁塞避免**,即 congestion avoidance**背壓**,即 backpressure其中節點會限制自己的傳送速率以避免網路鏈路或接收節點過載【27】。這意味著甚至在資料進入網路之前在傳送者處就需要進行額外的排隊。
![](../img/fig8-2.png)
![](/img/fig8-2.png)
**圖 8-2 如果有多臺機器將網路流量傳送到同一目的地,則其交換機佇列可能會被填滿。在這裡,埠 1,2 和 4 都試圖傳送資料包到埠 3**
@@ -186,7 +189,7 @@
所有這些因素都會造成網路延遲的變化。當系統接近其最大容量時,排隊延遲的變化範圍特別大:擁有足夠備用容量的系統可以輕鬆排空佇列,而在高利用率的系統中,很快就能積累很長的佇列。
在公共雲和多租戶資料中心中,資源被許多客戶共享:網路連結和交換機,甚至每個機器的網絡卡和 CPU在虛擬機器上執行時。批處理工作負載如 MapReduce請參閱 [第十章](ch10.md)能夠很容易使網路連結飽和。由於無法控制或瞭解其他客戶對共享資源的使用情況如果附近的某個人嘈雜的鄰居正在使用大量資源則網路延遲可能會發生劇烈變化【28,29】。
在公共雲和多租戶資料中心中,資源被許多客戶共享:網路連結和交換機,甚至每個機器的網絡卡和 CPU在虛擬機器上執行時。批處理工作負載如 MapReduce請參閱 [第十章](/tw/ch10)能夠很容易使網路連結飽和。由於無法控制或瞭解其他客戶對共享資源的使用情況如果附近的某個人嘈雜的鄰居正在使用大量資源則網路延遲可能會發生劇烈變化【28,29】。
在這種環境下,你只能透過實驗方式選擇超時:在一段較長的時期內、在多臺機器上測量網路往返時間的分佈,以確定延遲的預期變化。然後,考慮到應用程式的特性,可以確定 **故障檢測延遲****過早超時風險** 之間的適當折衷。
@@ -250,7 +253,7 @@
7. 這個快取條目何時到期?
8. 日誌檔案中此錯誤訊息的時間戳是什麼?
[例 1-4](ch1.md) 測量了 **持續時間**durations例如請求傳送與響應接收之間的時間間隔而 [例 5-8](ch5.md) 描述了 **時間點**point in time在特定日期和和特定時間發生的事件
[例 1-4](/tw/ch1) 測量了 **持續時間**durations例如請求傳送與響應接收之間的時間間隔而 [例 5-8](/tw/ch5) 描述了 **時間點**point in time在特定日期和和特定時間發生的事件
在分散式系統中,時間是一件棘手的事情,因為通訊不是即時的:訊息透過網路從一臺機器傳送到另一臺機器需要時間。收到訊息的時間總是晚於傳送的時間,但是由於網路中的可變延遲,我們不知道晚了多少時間。這個事實導致有時很難確定在涉及多臺機器時發生事情的順序。
@@ -313,27 +316,27 @@
讓我們考慮一個特別的情況,一件很有誘惑但也很危險的事情:依賴時鐘,在多個節點上對事件進行排序。例如,如果兩個客戶端寫入分散式資料庫,誰先到達? 哪一個更近?
[圖 8-3](../img/fig8-3.png) 顯示了在具有多主複製的資料庫中對時鐘的危險使用(該例子類似於 [圖 5-9](../img/fig5-9.png))。客戶端 A 在節點 1 上寫入 `x = 1`;寫入被複制到節點 3客戶端 B 在節點 3 上增加 x我們現在有 `x = 2`);最後這兩個寫入都被複制到節點 2。
[圖 8-3](/img/fig8-3.png) 顯示了在具有多主複製的資料庫中對時鐘的危險使用(該例子類似於 [圖 5-9](/img/fig5-9.png))。客戶端 A 在節點 1 上寫入 `x = 1`;寫入被複制到節點 3客戶端 B 在節點 3 上增加 x我們現在有 `x = 2`);最後這兩個寫入都被複制到節點 2。
![](../img/fig8-3.png)
![](/img/fig8-3.png)
**圖 8-3 客戶端 B 的寫入比客戶端 A 的寫入要晚,但是 B 的寫入具有較早的時間戳。**
在 [圖 8-3](../img/fig8-3.png) 中,當一個寫入被複制到其他節點時,它會根據發生寫入的節點上的日曆時鐘標記一個時間戳。在這個例子中,時鐘同步是非常好的:節點 1 和節點 3 之間的偏差小於 3ms這可能比你在實踐中能預期的更好。
在 [圖 8-3](/img/fig8-3.png) 中,當一個寫入被複制到其他節點時,它會根據發生寫入的節點上的日曆時鐘標記一個時間戳。在這個例子中,時鐘同步是非常好的:節點 1 和節點 3 之間的偏差小於 3ms這可能比你在實踐中能預期的更好。
儘管如此,[圖 8-3](../img/fig8-3.png) 中的時間戳卻無法正確排列事件:寫入 `x = 1` 的時間戳為 42.004 秒,但寫入 `x = 2` 的時間戳為 42.003 秒,即使 `x = 2` 在稍後出現。當節點 2 接收到這兩個事件時,會錯誤地推斷出 `x = 1` 是最近的值,而丟棄寫入 `x = 2`。效果上表現為,客戶端 B 的增量操作會丟失。
儘管如此,[圖 8-3](/img/fig8-3.png) 中的時間戳卻無法正確排列事件:寫入 `x = 1` 的時間戳為 42.004 秒,但寫入 `x = 2` 的時間戳為 42.003 秒,即使 `x = 2` 在稍後出現。當節點 2 接收到這兩個事件時,會錯誤地推斷出 `x = 1` 是最近的值,而丟棄寫入 `x = 2`。效果上表現為,客戶端 B 的增量操作會丟失。
這種衝突解決策略被稱為 **最後寫入勝利LWW**,它在多主複製和無主資料庫(如 Cassandra 【53】和 Riak 【54】中被廣泛使用請參閱 “[最後寫入勝利(丟棄併發寫入)](ch5.md#最後寫入勝利(丟棄併發寫入))” 一節)。有些實現會在客戶端而不是伺服器上生成時間戳,但這並不能改變 LWW 的基本問題:
這種衝突解決策略被稱為 **最後寫入勝利LWW**,它在多主複製和無主資料庫(如 Cassandra 【53】和 Riak 【54】中被廣泛使用請參閱 “[最後寫入勝利(丟棄併發寫入)](/tw/ch5#最後寫入勝利(丟棄併發寫入))” 一節)。有些實現會在客戶端而不是伺服器上生成時間戳,但這並不能改變 LWW 的基本問題:
* 資料庫寫入可能會神秘地消失具有滯後時鐘的節點無法覆蓋之前具有快速時鐘的節點寫入的值直到節點之間的時鐘偏差消逝【54,55】。此方案可能導致一定數量的資料被悄悄丟棄而未嚮應用報告任何錯誤。
* LWW 無法區分 **高頻順序寫入**(在 [圖 8-3](../img/fig8-3.png) 中,客戶端 B 的增量操作 **一定** 發生在客戶端 A 的寫入之後)和 **真正併發寫入**(寫入者意識不到其他寫入者)。需要額外的因果關係跟蹤機制(例如版本向量),以防止違背因果關係(請參閱 “[檢測併發寫入](ch5.md#檢測併發寫入)”)。
* LWW 無法區分 **高頻順序寫入**(在 [圖 8-3](/img/fig8-3.png) 中,客戶端 B 的增量操作 **一定** 發生在客戶端 A 的寫入之後)和 **真正併發寫入**(寫入者意識不到其他寫入者)。需要額外的因果關係跟蹤機制(例如版本向量),以防止違背因果關係(請參閱 “[檢測併發寫入](/tw/ch5#檢測併發寫入)”)。
* 兩個節點很可能獨立地生成具有相同時間戳的寫入,特別是在時鐘僅具有毫秒解析度的情況下。為了解決這樣的衝突,還需要一個額外的 **決勝值**tiebreaker可以簡單地是一個大隨機數但這種方法也可能會導致違背因果關係【53】。
因此,儘管透過保留 “最近” 的值並放棄其他值來解決衝突是很誘惑人的,但是要注意,“最近” 的定義取決於本地的 **日曆時鐘**,這很可能是不正確的。即使用嚴格同步的 NTP 時鐘,一個數據包也可能在時間戳 100 毫秒(根據傳送者的時鐘)時傳送,並在時間戳 99 毫秒(根據接收者的時鐘)處到達 —— 看起來好像資料包在傳送之前已經到達,這是不可能的。
NTP 同步是否能足夠準確,以至於這種不正確的排序不會發生?也許不能,因為 NTP 的同步精度本身,除了石英鐘漂移這類誤差源之外,還受到網路往返時間的限制。為了進行正確的排序,你需要一個比測量物件(即網路延遲)要精確得多的時鐘。
所謂的 **邏輯時鐘logic clock**【56,57】是基於遞增計數器而不是振盪石英晶體對於排序事件來說是更安全的選擇請參閱 “[檢測併發寫入](ch5.md#檢測併發寫入)”)。邏輯時鐘不測量一天中的時間或經過的秒數,而僅測量事件的相對順序(無論一個事件發生在另一個事件之前還是之後)。相反,用來測量實際經過時間的 **日曆時鐘****單調鍾** 也被稱為 **物理時鐘physical clock**。我們將在 “[順序保證](ch9.md#順序保證)” 中來看順序問題。
所謂的 **邏輯時鐘logic clock**【56,57】是基於遞增計數器而不是振盪石英晶體對於排序事件來說是更安全的選擇請參閱 “[檢測併發寫入](/tw/ch5#檢測併發寫入)”)。邏輯時鐘不測量一天中的時間或經過的秒數,而僅測量事件的相對順序(無論一個事件發生在另一個事件之前還是之後)。相反,用來測量實際經過時間的 **日曆時鐘****單調鍾** 也被稱為 **物理時鐘physical clock**。我們將在 “[順序保證](/tw/ch9#順序保證)” 中來看順序問題。
#### 時鐘讀數存在置信區間
@@ -349,13 +352,13 @@ NTP 同步是否能足夠準確,以至於這種不正確的排序不會發生
#### 全域性快照的同步時鐘
在 “[快照隔離和可重複讀](ch7.md#快照隔離和可重複讀)” 中,我們討論了快照隔離,這是資料庫中非常有用的功能,需要支援小型快速讀寫事務和大型長時間執行的只讀事務(用於備份或分析)。它允許只讀事務看到特定時間點的處於一致狀態的資料庫,且不會鎖定和干擾讀寫事務。
在 “[快照隔離和可重複讀](/tw/ch7#快照隔離和可重複讀)” 中,我們討論了快照隔離,這是資料庫中非常有用的功能,需要支援小型快速讀寫事務和大型長時間執行的只讀事務(用於備份或分析)。它允許只讀事務看到特定時間點的處於一致狀態的資料庫,且不會鎖定和干擾讀寫事務。
快照隔離最常見的實現需要單調遞增的事務 ID。如果寫入比快照晚寫入具有比快照更大的事務 ID則該寫入對於快照事務是不可見的。在單節點資料庫上一個簡單的計數器就足以生成事務 ID。
但是當資料庫分佈在許多機器上,也許可能在多個數據中心中時,由於需要協調,(跨所有分割槽)全域性單調遞增的事務 ID 會很難生成。事務 ID 必須反映因果關係:如果事務 B 讀取由事務 A 寫入的值,則 B 必須具有比 A 更大的事務 ID否則快照就無法保持一致。在有大量的小規模、高頻率的事務情景下在分散式系統中建立事務 ID 成為一個難以處理的瓶頸 [^vi]。
[^vi]: 存在分散式序列號生成器,例如 Twitter 的雪花Snowflake其以可伸縮的方式例如透過將 ID 空間的塊分配給不同節點)近似單調地增加唯一 ID。但是它們通常無法保證與因果關係一致的排序因為分配的 ID 塊的時間範圍比資料庫讀取和寫入的時間範圍要長。另請參閱 “[順序保證](ch9.md#順序保證)”。
[^vi]: 存在分散式序列號生成器,例如 Twitter 的雪花Snowflake其以可伸縮的方式例如透過將 ID 空間的塊分配給不同節點)近似單調地增加唯一 ID。但是它們通常無法保證與因果關係一致的排序因為分配的 ID 塊的時間範圍比資料庫讀取和寫入的時間範圍要長。另請參閱 “[順序保證](/tw/ch9#順序保證)”。
我們可以使用同步時鐘的時間戳作為事務 ID 嗎?如果我們能夠獲得足夠好的同步性,那麼這種方法將具有很合適的屬性:更晚的事務會有更大的時間戳。當然,問題在於時鐘精度的不確定性。
@@ -419,7 +422,7 @@ while (true) {
> #### 即時是真的嗎?
>
> 在嵌入式系統中,即時是指系統經過精心設計和測試,以滿足所有情況下的特定時間保證。這個含義與 Web 上對即時術語的模糊使用相反,後者描述了伺服器將資料推送到客戶端以及沒有嚴格的響應時間限制的流處理(見 [第十一章](ch11.md))。
> 在嵌入式系統中,即時是指系統經過精心設計和測試,以滿足所有情況下的特定時間保證。這個含義與 Web 上對即時術語的模糊使用相反,後者描述了伺服器將資料推送到客戶端以及沒有嚴格的響應時間限制的流處理(見 [第十一章](/tw/ch11))。
例如,如果車載感測器檢測到當前正在經歷碰撞,你肯定不希望安全氣囊釋放系統因為 GC 暫停而延遲彈出。
@@ -435,7 +438,7 @@ while (true) {
一個新興的想法是將 GC 暫停視為一個節點的短暫計劃中斷,並在這個節點收集其垃圾的同時,讓其他節點處理來自客戶端的請求。如果執行時可以警告應用程式一個節點很快需要 GC 暫停,那麼應用程式可以停止向該節點發送新的請求,等待它完成處理未完成的請求,然後在沒有請求正在進行時執行 GC。這個技巧向客戶端隱藏了 GC 暫停並降低了響應時間的高百分比【70,71】。一些對延遲敏感的金融交易系統【72】使用這種方法。
這個想法的一個變種是隻用垃圾收集器來處理短命物件(這些物件可以快速收集),並定期在積累大量長壽物件(因此需要完整 GC之前重新啟動程序【65,73】。一次可以重新啟動一個節點在計劃重新啟動之前流量可以從該節點移開就像 [第四章](ch4.md) 裡描述的滾動升級一樣。
這個想法的一個變種是隻用垃圾收集器來處理短命物件(這些物件可以快速收集),並定期在積累大量長壽物件(因此需要完整 GC之前重新啟動程序【65,73】。一次可以重新啟動一個節點在計劃重新啟動之前流量可以從該節點移開就像 [第四章](/tw/ch4) 裡描述的滾動升級一樣。
這些措施不能完全阻止垃圾回收暫停,但可以有效地減少它們對應用的影響。
@@ -450,7 +453,7 @@ while (true) {
幸運的是,我們不需要去搞清楚生命的意義。在分散式系統中,我們可以陳述關於行為(系統模型)的假設,並以滿足這些假設的方式設計實際系統。演算法可以被證明在某個系統模型中正確執行。這意味著即使底層系統模型提供了很少的保證,也可以實現可靠的行為。
但是,儘管可以使軟體在不可靠的系統模型中表現良好,但這並不是可以直截了當實現的。在本章的其餘部分中,我們將進一步探討分散式系統中的知識和真相的概念,這將有助於我們思考我們可以做出的各種假設以及我們可能希望提供的保證。在 [第九章](ch9.md) 中,我們將著眼於分散式系統的一些例子,這些演算法在特定的假設條件下提供了特定的保證。
但是,儘管可以使軟體在不可靠的系統模型中表現良好,但這並不是可以直截了當實現的。在本章的其餘部分中,我們將進一步探討分散式系統中的知識和真相的概念,這將有助於我們思考我們可以做出的各種假設以及我們可能希望提供的保證。在 [第九章](/tw/ch9) 中,我們將著眼於分散式系統的一些例子,這些演算法在特定的假設條件下提供了特定的保證。
### 真相由多數所定義
@@ -460,17 +463,17 @@ while (true) {
第三種情況,想象一個正在經歷長時間 **垃圾收集暫停stop-the-world GC Pause** 的節點,節點的所有執行緒被 GC 搶佔並暫停一分鐘因此沒有請求被處理也沒有響應被傳送。其他節點等待重試不耐煩並最終宣佈節點死亡並將其丟到靈車上。最後GC 完成節點的執行緒繼續好像什麼也沒有發生。其他節點感到驚訝因為所謂的死亡節點突然從棺材中抬起頭來身體健康開始和旁觀者高興地聊天。GC 後的節點最初甚至沒有意識到已經經過了整整一分鐘,而且自己已被宣告死亡。從它自己的角度來看,從最後一次與其他節點交談以來,幾乎沒有經過任何時間。
這些故事的寓意是,節點不一定能相信自己對於情況的判斷。分散式系統不能完全依賴單個節點,因為節點可能隨時失效,可能會使系統卡死,無法恢復。相反,許多分散式演算法都依賴於法定人數,即在節點之間進行投票(請參閱 “[讀寫的法定人數](ch5.md#讀寫的法定人數)”):決策需要來自多個節點的最小投票數,以減少對於某個特定節點的依賴。
這些故事的寓意是,節點不一定能相信自己對於情況的判斷。分散式系統不能完全依賴單個節點,因為節點可能隨時失效,可能會使系統卡死,無法恢復。相反,許多分散式演算法都依賴於法定人數,即在節點之間進行投票(請參閱 “[讀寫的法定人數](/tw/ch5#讀寫的法定人數)”):決策需要來自多個節點的最小投票數,以減少對於某個特定節點的依賴。
這也包括關於宣告節點死亡的決定。如果法定數量的節點宣告另一個節點已經死亡,那麼即使該節點仍感覺自己活著,它也必須被認為是死的。個體節點必須遵守法定決定並下臺。
最常見的法定人數是超過一半的絕對多數(儘管其他型別的法定人數也是可能的)。多數法定人數允許系統繼續工作,如果單個節點發生故障(三個節點可以容忍單節點故障;五個節點可以容忍雙節點故障)。系統仍然是安全的,因為在這個制度中只能有一個多數 —— 不能同時存在兩個相互衝突的多數決定。當我們在 [第九章](ch9.md) 中討論 **共識演算法consensus algorithms** 時,我們將更詳細地討論法定人數的應用。
最常見的法定人數是超過一半的絕對多數(儘管其他型別的法定人數也是可能的)。多數法定人數允許系統繼續工作,如果單個節點發生故障(三個節點可以容忍單節點故障;五個節點可以容忍雙節點故障)。系統仍然是安全的,因為在這個制度中只能有一個多數 —— 不能同時存在兩個相互衝突的多數決定。當我們在 [第九章](/tw/ch9) 中討論 **共識演算法consensus algorithms** 時,我們將更詳細地討論法定人數的應用。
#### 領導者和鎖
通常情況下,一些東西在一個系統中只能有一個。例如:
* 資料庫分割槽的領導者只能有一個節點,以避免 **腦裂**(即 split brain請參閱 “[處理節點宕機](ch5.md#處理節點宕機)”)。
* 資料庫分割槽的領導者只能有一個節點,以避免 **腦裂**(即 split brain請參閱 “[處理節點宕機](/tw/ch5#處理節點宕機)”)。
* 特定資源的鎖或物件只允許一個事務 / 客戶端持有,以防同時寫入和損壞。
* 一個特定的使用者名稱只能被一個使用者所註冊,因為使用者名稱必須唯一標識一個使用者。
@@ -478,9 +481,9 @@ while (true) {
如果一個節點繼續表現為 **天選者**,即使大多數節點已經宣告它已經死了,則在考慮不周的系統中可能會導致問題。這樣的節點能以自己賦予的權能向其他節點發送訊息,如果其他節點相信,整個系統可能會做一些不正確的事情。
例如,[圖 8-4](../img/fig8-4.png) 顯示了由於不正確的鎖實現導致的資料損壞錯誤。這個錯誤不僅僅是理論上的HBase 曾經有這個問題【74,75】假設你要確保一個儲存服務中的檔案一次只能被一個客戶訪問因為如果多個客戶試圖對此寫入該檔案將被損壞。你嘗試透過在訪問檔案之前要求客戶端從鎖定服務獲取租約來實現此目的。
例如,[圖 8-4](/img/fig8-4.png) 顯示了由於不正確的鎖實現導致的資料損壞錯誤。這個錯誤不僅僅是理論上的HBase 曾經有這個問題【74,75】假設你要確保一個儲存服務中的檔案一次只能被一個客戶訪問因為如果多個客戶試圖對此寫入該檔案將被損壞。你嘗試透過在訪問檔案之前要求客戶端從鎖定服務獲取租約來實現此目的。
![](../img/fig8-4.png)
![](/img/fig8-4.png)
**圖 8-4 分散式鎖的實現不正確:客戶端 1 認為它仍然具有有效的租約,即使它已經過期,從而破壞了儲存中的檔案**
@@ -488,15 +491,15 @@ while (true) {
#### 防護令牌
當使用鎖或租約來保護對某些資源(如 [圖 8-4](../img/fig8-4.png) 中的檔案儲存)的訪問時,需要確保一個被誤認為自己是 “天選者” 的節點不能擾亂系統的其它部分。實現這一目標的一個相當簡單的技術就是 **防護fencing**,如 [圖 8-5](../img/fig8-5.png) 所示
當使用鎖或租約來保護對某些資源(如 [圖 8-4](/img/fig8-4.png) 中的檔案儲存)的訪問時,需要確保一個被誤認為自己是 “天選者” 的節點不能擾亂系統的其它部分。實現這一目標的一個相當簡單的技術就是 **防護fencing**,如 [圖 8-5](/img/fig8-5.png) 所示
![](../img/fig8-5.png)
![](/img/fig8-5.png)
**圖 8-5 只允許以增加防護令牌的順序進行寫操作,從而保證儲存安全**
我們假設每次鎖定伺服器授予鎖或租約時,它還會返回一個 **防護令牌fencing token**,這個數字在每次授予鎖定時都會增加(例如,由鎖定服務增加)。然後,我們可以要求客戶端每次向儲存服務傳送寫入請求時,都必須包含當前的防護令牌。
在 [圖 8-5](../img/fig8-5.png) 中,客戶端 1 以 33 的令牌獲得租約,但隨後進入一個長時間的停頓並且租約到期。客戶端 2 以 34 的令牌(該數字總是增加)獲取租約,然後將其寫入請求傳送到儲存服務,包括 34 的令牌。稍後,客戶端 1 恢復生機並將其寫入儲存服務,包括其令牌值 33。但是儲存伺服器會記住它已經處理了一個具有更高令牌編號34的寫入因此它會拒絕帶有令牌 33 的請求。
在 [圖 8-5](/img/fig8-5.png) 中,客戶端 1 以 33 的令牌獲得租約,但隨後進入一個長時間的停頓並且租約到期。客戶端 2 以 34 的令牌(該數字總是增加)獲取租約,然後將其寫入請求傳送到儲存服務,包括 34 的令牌。稍後,客戶端 1 恢復生機並將其寫入儲存服務,包括其令牌值 33。但是儲存伺服器會記住它已經處理了一個具有更高令牌編號34的寫入因此它會拒絕帶有令牌 33 的請求。
如果將 ZooKeeper 用作鎖定服務,則可將事務標識 `zxid` 或節點版本 `cversion` 用作防護令牌。由於它們保證單調遞增因此它們具有所需的屬性【74】。
@@ -514,7 +517,7 @@ while (true) {
> ### 拜占庭將軍問題
>
> 拜占庭將軍問題是對所謂 “兩將軍問題” 的泛化【78】它想象兩個將軍需要就戰鬥計劃達成一致的情況。由於他們在兩個不同的地點建立了營地他們只能透過信使進行溝通信使有時會被延遲或丟失就像網路中的資訊包一樣。我們將在 [第九章](ch9.md) 討論這個共識問題。
> 拜占庭將軍問題是對所謂 “兩將軍問題” 的泛化【78】它想象兩個將軍需要就戰鬥計劃達成一致的情況。由於他們在兩個不同的地點建立了營地他們只能透過信使進行溝通信使有時會被延遲或丟失就像網路中的資訊包一樣。我們將在 [第九章](/tw/ch9) 討論這個共識問題。
>
> 在這個問題的拜占庭版本里,有 n 位將軍需要同意,他們的努力因為有一些叛徒在他們中間而受到阻礙。大多數的將軍都是忠誠的,因而發出了真實的資訊,但是叛徒可能會試圖透過傳送虛假或不真實的資訊來欺騙和混淆他人(在試圖保持未被發現的同時)。事先並不知道叛徒是誰。
>
@@ -543,7 +546,7 @@ Web 應用程式確實需要預期受終端使用者控制的客戶端(如 Web
### 系統模型與現實
已經有很多演算法被設計以解決分散式系統問題 —— 例如,我們將在 [第九章](ch9.md) 討論共識問題的解決方案。為了有用,這些演算法需要容忍我們在本章中討論的分散式系統的各種故障。
已經有很多演算法被設計以解決分散式系統問題 —— 例如,我們將在 [第九章](/tw/ch9) 討論共識問題的解決方案。為了有用,這些演算法需要容忍我們在本章中討論的分散式系統的各種故障。
演算法的編寫方式不應該過分依賴於執行的硬體和軟體配置的細節。這就要求我們以某種方式將我們期望在系統中發生的錯誤形式化。我們透過定義一個系統模型來做到這一點,這個模型是一個抽象,描述一個演算法可以假設的事情。
@@ -619,7 +622,7 @@ Web 應用程式確實需要預期受終端使用者控制的客戶端(如 Web
例如,在崩潰 - 恢復crash-recovery模型中的演算法通常假設穩定儲存器中的資料在崩潰後可以倖存。但是如果磁碟上的資料被破壞或者由於硬體錯誤或錯誤配置導致資料被清除會發生什麼情況【91】如果伺服器存在韌體錯誤並且在重新啟動時無法識別其硬碟驅動器即使驅動器已正確連線到伺服器那又會發生什麼情況【92】
法定人數演算法(請參閱 “[讀寫的法定人數](ch5.md#讀寫的法定人數)”)依賴節點來記住它聲稱儲存的資料。如果一個節點可能患有健忘症,忘記了以前儲存的資料,這會打破法定條件,從而破壞演算法的正確性。也許需要一個新的系統模型,在這個模型中,我們假設穩定的儲存大多能在崩潰後倖存,但有時也可能會丟失。但是那個模型就變得更難以推理了。
法定人數演算法(請參閱 “[讀寫的法定人數](/tw/ch5#讀寫的法定人數)”)依賴節點來記住它聲稱儲存的資料。如果一個節點可能患有健忘症,忘記了以前儲存的資料,這會打破法定條件,從而破壞演算法的正確性。也許需要一個新的系統模型,在這個模型中,我們假設穩定的儲存大多能在崩潰後倖存,但有時也可能會丟失。但是那個模型就變得更難以推理了。
演算法的理論描述可以簡單宣稱一些事是不會發生的 —— 在非拜占庭式系統中,我們確實需要對可能發生和不可能發生的故障做出假設。然而,真實世界的實現,仍然會包括處理 “假設上不可能” 情況的程式碼,即使程式碼可能就是 `printf("Sucks to be you")``exit(666)`實際上也就是留給運維來擦屁股【93】。這可以說是計算機科學和軟體工程間的一個差異
@@ -644,13 +647,13 @@ Web 應用程式確實需要預期受終端使用者控制的客戶端(如 Web
如果你習慣於在理想化的數學完美的單機環境同一個操作總能確定地返回相同的結果中編寫軟體那麼轉向分散式系統的凌亂的物理現實可能會有些令人震驚。相反如果能夠在單臺計算機上解決一個問題那麼分散式系統工程師通常會認為這個問題是平凡的【5】現在單個計算機確實可以做很多事情【95】。如果你可以避免開啟潘多拉的盒子把東西放在一臺機器上那麼通常是值得的。
但是,正如在 [第二部分](part-ii.md) 的介紹中所討論的那樣,可伸縮性並不是使用分散式系統的唯一原因。容錯和低延遲(透過將資料放置在距離使用者較近的地方)是同等重要的目標,而這些不能用單個節點實現。
但是,正如在 [第二部分](/tw/part-ii) 的介紹中所討論的那樣,可伸縮性並不是使用分散式系統的唯一原因。容錯和低延遲(透過將資料放置在距離使用者較近的地方)是同等重要的目標,而這些不能用單個節點實現。
在本章中,我們也轉換了幾次話題,探討了網路、時鐘和程序的不可靠性是否是不可避免的自然規律。我們看到這並不是:有可能給網路提供硬即時的響應保證和有限的延遲,但是這樣做非常昂貴,且導致硬體資源的利用率降低。大多數非安全關鍵系統會選擇 **便宜而不可靠**,而不是 **昂貴和可靠**
我們還談到了超級計算機,它們採用可靠的元件,因此當元件發生故障時必須完全停止並重新啟動。相比之下,分散式系統可以永久執行而不會在服務層面中斷,因為所有的錯誤和維護都可以在節點級別進行處理 —— 至少在理論上是如此。(實際上,如果一個錯誤的配置變更被應用到所有的節點,仍然會使分散式系統癱瘓)。
本章一直在講存在的問題,給我們展現了一幅黯淡的前景。在 [下一章](ch9.md) 中,我們將繼續討論解決方案,並討論一些旨在解決分散式系統中所有問題的演算法。
本章一直在講存在的問題,給我們展現了一幅黯淡的前景。在 [下一章](/tw/ch9) 中,我們將繼續討論解決方案,並討論一些旨在解決分散式系統中所有問題的演算法。
## 參考文獻
@@ -751,11 +754,4 @@ Web 應用程式確實需要預期受終端使用者控制的客戶端(如 Web
1. Thanh Do, Mingzhe Hao, Tanakorn Leesatapornwongsa, et al.: “[Limplock: Understanding the Impact of Limpware on Scale-out Cloud Systems](http://ucare.cs.uchicago.edu/pdf/socc13-limplock.pdf),” at *4th ACM Symposium on Cloud Computing* (SoCC), October 2013. [doi:10.1145/2523616.2523627](http://dx.doi.org/10.1145/2523616.2523627)
1. Frank McSherry, Michael Isard, and Derek G. Murray: “[Scalability! But at What COST?](http://www.frankmcsherry.org/assets/COST.pdf),” at *15th USENIX Workshop on Hot Topics in Operating Systems* (HotOS), May 2015.
[^譯著1]: 原詩為Hey I just met you. The networks laggy. But heres my data. So store it maybe.Hey, 應改編自《Call Me Maybe》歌詞I just met you, And this is crazy, But here's my number, So call me, maybe?
------
| 上一章 | 目錄 | 下一章 |
| ---------------------- | ------------------------------- | ------------------------------ |
| [第七章:事務](ch7.md) | [設計資料密集型應用](README.md) | [第九章:一致性與共識](ch9.md) |
[^譯著1]: 原詩為Hey I just met you. The networks laggy. But heres my data. So store it maybe.Hey, 應改編自《Call Me Maybe》歌詞I just met you, And this is crazy, But here's my number, So call me, maybe?

View File

@@ -1,23 +1,26 @@
# 第九章:一致性與共識
---
title: "第九章:一致性與共識"
linkTitle: "9. 一致性與共識"
weight: 209
breadcrumbs: false
---
![](../img/ch9.png)
![](/img/ch9.png)
> 好死還是賴活著?
> —— Jay Kreps, 關於 Kafka 與 Jepsen 的若干筆記 (2013)
---------------
[TOC]
正如 [第八章](/tw/ch8) 所討論的,分散式系統中的許多事情可能會出錯。處理這種故障的最簡單方法是簡單地讓整個服務失效,並向用戶顯示錯誤訊息。如果無法接受這個解決方案,我們就需要找到容錯的方法 —— 即使某些內部元件出現故障,服務也能正常執行。
正如 [第八章](ch8.md) 所討論的,分散式系統中的許多事情可能會出錯。處理這種故障的最簡單方法是簡單地讓整個服務失效,並向用戶顯示錯誤訊息。如果無法接受這個解決方案,我們就需要找到容錯的方法 —— 即使某些內部元件出現故障,服務也能正常執行
在本章中,我們將討論構建容錯分散式系統的演算法和協議的一些例子。我們將假設 [第八章](/tw/ch8) 的所有問題都可能發生:網路中的資料包可能會丟失、重新排序、重複推送或任意延遲;時鐘只是盡其所能地近似;且節點可以暫停(例如,由於垃圾收集)或隨時崩潰
在本章中,我們將討論構建容錯分散式系統的演算法和協議的一些例子。我們將假設 [](ch8.md) 的所有問題都可能發生:網路中的資料包可能會丟失、重新排序、重複推送或任意延遲;時鐘只是盡其所能地近似;且節點可以暫停(例如,由於垃圾收集)或隨時崩潰
構建容錯系統的最好方法,是找到一些帶有實用保證的通用抽象,實現一次,然後讓應用依賴這些保證。這與 [第七章](ch7.md) 中的事務處理方法相同:透過使用事務,應用可以假裝沒有崩潰(原子性),沒有其他人同時訪問資料庫(隔離),儲存裝置是完全可靠的(永續性)。即使發生崩潰,競態條件和磁碟故障,事務抽象隱藏了這些問題,因此應用不必擔心它們。
構建容錯系統的最好方法,是找到一些帶有實用保證的通用抽象,實現一次,然後讓應用依賴這些保證。這與 [](/tw/ch7) 中的事務處理方法相同:透過使用事務,應用可以假裝沒有崩潰(原子性),沒有其他人同時訪問資料庫(隔離),儲存裝置是完全可靠的(永續性)。即使發生崩潰,競態條件和磁碟故障,事務抽象隱藏了這些問題,因此應用不必擔心它們
現在我們將繼續沿著同樣的路線前進,尋求可以讓應用忽略分散式系統部分問題的抽象概念。例如,分散式系統最重要的抽象之一就是 **共識consensus****就是讓所有的節點對某件事達成一致**。正如我們在本章中將會看到的那樣,要可靠地達成共識,且不被網路故障和程序故障所影響,是一個令人驚訝的棘手問題。
一旦達成共識,應用可以將其用於各種目的。例如,假設你有一個單主複製的資料庫。如果主庫掛掉,並且需要故障切換到另一個節點,剩餘的資料庫節點可以使用共識來選舉新的領導者。正如在 “[處理節點宕機](ch5.md#處理節點宕機)” 中所討論的那樣,重要的是隻有一個領導者,且所有的節點都認同其領導。如果兩個節點都認為自己是領導者,這種情況被稱為 **腦裂split brain**,它經常會導致資料丟失。正確實現共識有助於避免這種問題。
一旦達成共識,應用可以將其用於各種目的。例如,假設你有一個單主複製的資料庫。如果主庫掛掉,並且需要故障切換到另一個節點,剩餘的資料庫節點可以使用共識來選舉新的領導者。正如在 “[處理節點宕機](/tw/ch5#處理節點宕機)” 中所討論的那樣,重要的是隻有一個領導者,且所有的節點都認同其領導。如果兩個節點都認為自己是領導者,這種情況被稱為 **腦裂split brain**,它經常會導致資料丟失。正確實現共識有助於避免這種問題。
在本章後面的 “[分散式事務與共識](#分散式事務與共識)” 中,我們將研究解決共識和相關問題的演算法。但首先,我們首先需要探索可以在分散式系統中提供的保證和抽象的範圍。
@@ -28,11 +31,11 @@
## 一致性保證
在 “[複製延遲問題](ch5.md#複製延遲問題)” 中,我們看到了資料庫複製中發生的一些時序問題。如果你在同一時刻檢視兩個資料庫節點,則可能在兩個節點上看到不同的資料,因為寫請求在不同的時間到達不同的節點。無論資料庫使用何種複製方法(單主複製、多主複製或無主複製),都會出現這些不一致情況。
在 “[複製延遲問題](/tw/ch5#複製延遲問題)” 中,我們看到了資料庫複製中發生的一些時序問題。如果你在同一時刻檢視兩個資料庫節點,則可能在兩個節點上看到不同的資料,因為寫請求在不同的時間到達不同的節點。無論資料庫使用何種複製方法(單主複製、多主複製或無主複製),都會出現這些不一致情況。
大多數複製的資料庫至少提供了 **最終一致性**這意味著如果你停止向資料庫寫入資料並等待一段不確定的時間那麼最終所有的讀取請求都會返回相同的值【1】。換句話說不一致性是暫時的最終會自行解決假設網路中的任何故障最終都會被修復。最終一致性的一個更好的名字可能是 **收斂convergence**因為我們預計所有的副本最終會收斂到相同的值【2】。
然而,這是一個非常弱的保證 —— 它並沒有說什麼時候副本會收斂。在收斂之前讀操作可能會返回任何東西或什麼都沒有【1】。例如如果你寫入了一個值然後立即再次讀取這並不能保證你能看到剛才寫入的值因為讀請求可能會被路由到另外的副本上。請參閱 “[讀己之寫](ch5.md#讀己之寫)” )。
然而,這是一個非常弱的保證 —— 它並沒有說什麼時候副本會收斂。在收斂之前讀操作可能會返回任何東西或什麼都沒有【1】。例如如果你寫入了一個值然後立即再次讀取這並不能保證你能看到剛才寫入的值因為讀請求可能會被路由到另外的副本上。請參閱 “[讀己之寫](/tw/ch5#讀己之寫)” )。
對於應用開發人員而言最終一致性是很困難的因為它與普通單執行緒程式中變數的行為有很大區別。對於後者如果將一個值賦給一個變數然後很快地再次讀取不可能讀到舊的值或者讀取失敗。資料庫表面上看起來像一個你可以讀寫的變數但實際上它有更複雜的語義【3】。
@@ -40,7 +43,7 @@
本章將探索資料系統可能選擇提供的更強一致性模型。它不是免費的:具有較強保證的系統可能會比保證較差的系統具有更差的效能或更少的容錯性。儘管如此,更強的保證能夠吸引人,因為它們更容易用對。只有見過不同的一致性模型後,才能更好地決定哪一個最適合自己的需求。
**分散式一致性模型** 和我們之前討論的事務隔離級別的層次結構有一些相似之處【4,5】請參閱 “[弱隔離級別](ch7.md#弱隔離級別)”)。儘管兩者有一部分內容重疊,但它們大多是無關的問題:事務隔離主要是為了 **避免由於同時執行事務而導致的競爭狀態**,而分散式一致性主要關於 **在面對延遲和故障時如何協調副本間的狀態**
**分散式一致性模型** 和我們之前討論的事務隔離級別的層次結構有一些相似之處【4,5】請參閱 “[弱隔離級別](/tw/ch7#弱隔離級別)”)。儘管兩者有一部分內容重疊,但它們大多是無關的問題:事務隔離主要是為了 **避免由於同時執行事務而導致的競爭狀態**,而分散式一致性主要關於 **在面對延遲和故障時如何協調副本間的狀態**
本章涵蓋了廣泛的話題,但我們將會看到這些領域實際上是緊密聯絡在一起的:
@@ -57,11 +60,11 @@
在一個線性一致的系統中,只要一個客戶端成功完成寫操作,所有客戶端從資料庫中讀取資料必須能夠看到剛剛寫入的值。要維護資料的單個副本的假象,系統應保障讀到的值是最近的、最新的,而不是來自陳舊的快取或副本。換句話說,線性一致性是一個 **新鮮度保證recency guarantee**。為了闡明這個想法,我們來看看一個非線性一致系統的例子。
![](../img/fig9-1.png)
![](/img/fig9-1.png)
**圖 9-1 這個系統是非線性一致的,導致了球迷的困惑**
[圖 9-1](../img/fig9-1.png) 展示了一個關於體育網站的非線性一致例子【9】。Alice 和 Bob 正坐在同一個房間裡,都盯著各自的手機,關注著 2014 年 FIFA 世界盃決賽的結果。在最後得分公佈後Alice 重新整理頁面,看到宣佈了獲勝者,並興奮地告訴 Bob。Bob 難以置信地重新整理了自己的手機,但他的請求路由到了一個落後的資料庫副本上,手機顯示比賽仍在進行。
[圖 9-1](/img/fig9-1.png) 展示了一個關於體育網站的非線性一致例子【9】。Alice 和 Bob 正坐在同一個房間裡,都盯著各自的手機,關注著 2014 年 FIFA 世界盃決賽的結果。在最後得分公佈後Alice 重新整理頁面,看到宣佈了獲勝者,並興奮地告訴 Bob。Bob 難以置信地重新整理了自己的手機,但他的請求路由到了一個落後的資料庫副本上,手機顯示比賽仍在進行。
如果 Alice 和 Bob 在同一時間重新整理並獲得了兩個不同的查詢結果,也許就沒有那麼令人驚訝了。因為他們不知道伺服器處理他們請求的精確時刻。然而 Bob 是在聽到 Alice 驚呼最後得分 **之後**,點選了重新整理按鈕(啟動了他的查詢),因此他希望查詢結果至少與愛麗絲一樣新鮮。但他的查詢返回了陳舊結果,這一事實違背了線性一致性的要求。
@@ -69,22 +72,22 @@
線性一致性背後的基本思想很簡單:使系統看起來好像只有一個數據副本。然而確切來講,實際上有更多要操心的地方。為了更好地理解線性一致性,讓我們再看幾個例子。
[圖 9-2](../img/fig9-2.png) 顯示了三個客戶端在線性一致資料庫中同時讀寫相同的鍵 `x`。在分散式系統文獻中,`x` 被稱為 **暫存器register**,例如,它可以是鍵值儲存中的一個 **鍵**,關係資料庫中的一 **行**,或文件資料庫中的一個 **文件**
[圖 9-2](/img/fig9-2.png) 顯示了三個客戶端在線性一致資料庫中同時讀寫相同的鍵 `x`。在分散式系統文獻中,`x` 被稱為 **暫存器register**,例如,它可以是鍵值儲存中的一個 **鍵**,關係資料庫中的一 **行**,或文件資料庫中的一個 **文件**
![](../img/fig9-2.png)
![](/img/fig9-2.png)
**圖 9-2 如果讀取請求與寫入請求併發,則可能會返回舊值或新值**
為了簡單起見,[圖 9-2](../img/fig9-2.png) 採用了使用者請求的視角,而不是資料庫內部的視角。每個橫柱都是由客戶端發出的請求,其中柱頭是請求傳送的時刻,柱尾是客戶端收到響應的時刻。因為網路延遲變化無常,客戶端不知道資料庫處理其請求的精確時間 —— 只知道它發生在傳送請求和接收響應之間的某個時刻。[^i]
為了簡單起見,[圖 9-2](/img/fig9-2.png) 採用了使用者請求的視角,而不是資料庫內部的視角。每個橫柱都是由客戶端發出的請求,其中柱頭是請求傳送的時刻,柱尾是客戶端收到響應的時刻。因為網路延遲變化無常,客戶端不知道資料庫處理其請求的精確時間 —— 只知道它發生在傳送請求和接收響應之間的某個時刻。[^i]
[^i]: 這個圖的一個微妙的細節是它假定存在一個全域性時鐘,由水平軸表示。雖然真實的系統通常沒有準確的時鐘(請參閱 “[不可靠的時鐘](ch8.md#不可靠的時鐘)”但這種假設是允許的為了分析分散式演算法我們可以假設存在一個精確的全域性時鐘不過演算法無法訪問它【47】。演算法只能看到由石英振盪器和 NTP 產生的對真實時間的逼近。
[^i]: 這個圖的一個微妙的細節是它假定存在一個全域性時鐘,由水平軸表示。雖然真實的系統通常沒有準確的時鐘(請參閱 “[不可靠的時鐘](/tw/ch8#不可靠的時鐘)”但這種假設是允許的為了分析分散式演算法我們可以假設存在一個精確的全域性時鐘不過演算法無法訪問它【47】。演算法只能看到由石英振盪器和 NTP 產生的對真實時間的逼近。
在這個例子中,暫存器有兩種型別的操作:
* $read(x)⇒v$表示客戶端請求讀取暫存器 `x` 的值,資料庫返回值 `v`
* $write(x,v)⇒r$ 表示客戶端請求將暫存器 `x` 設定為值 `v` ,資料庫返回響應 `r` (可能正確,可能錯誤)。
在 [圖 9-2](../img/fig9-2.png) 中,`x` 的值最初為 `0`,客戶端 C 執行寫請求將其設定為 `1`。發生這種情況時,客戶端 A 和 B 反覆輪詢資料庫以讀取最新值。A 和 B 的請求可能會收到怎樣的響應?
在 [圖 9-2](/img/fig9-2.png) 中,`x` 的值最初為 `0`,客戶端 C 執行寫請求將其設定為 `1`。發生這種情況時,客戶端 A 和 B 反覆輪詢資料庫以讀取最新值。A 和 B 的請求可能會收到怎樣的響應?
* 客戶端 A 的第一個讀操作,完成於寫操作開始之前,因此必須返回舊值 `0`
* 客戶端 A 的最後一個讀操作,開始於寫操作完成之後。如果資料庫是線性一致性的,它必然返回新值 `1`:因為讀操作和寫操作一定是在其各自的起止區間內的某個時刻被處理。如果在寫入結束後開始讀取,則讀取處理一定發生在寫入完成之後,因此它必須看到寫入的新值。
@@ -94,31 +97,31 @@
[^ii]: 如果讀取(與寫入同時發生時)可能返回舊值或新值,則稱該暫存器為 **常規暫存器regular register**【7,25】
為了使系統線性一致,我們需要新增另一個約束,如 [圖 9-3](../img/fig9-3.png) 所示
為了使系統線性一致,我們需要新增另一個約束,如 [圖 9-3](/img/fig9-3.png) 所示
![](../img/fig9-3.png)
![](/img/fig9-3.png)
**圖 9-3 任何一個讀取返回新值後,所有後續讀取(在相同或其他客戶端上)也必須返回新值。**
在一個線性一致的系統中,我們可以想象,在 `x` 的值從 `0` 自動翻轉到 `1` 的時候(在寫操作的開始和結束之間)必定有一個時間點。因此,如果一個客戶端的讀取返回新的值 `1`,即使寫操作尚未完成,所有後續讀取也必須返回新值。
[圖 9-3](../img/fig9-3.png) 中的箭頭說明了這個時序依賴關係。客戶端 A 是第一個讀取新的值 `1` 的位置。在 A 的讀取返回之後B 開始新的讀取。由於 B 的讀取嚴格發生於 A 的讀取之後,因此即使 C 的寫入仍在進行中,也必須返回 `1`(與 [圖 9-1](../img/fig9-1.png) 中的 Alice 和 Bob 的情況相同:在 Alice 讀取新值之後Bob 也希望讀取新的值)。
[圖 9-3](/img/fig9-3.png) 中的箭頭說明了這個時序依賴關係。客戶端 A 是第一個讀取新的值 `1` 的位置。在 A 的讀取返回之後B 開始新的讀取。由於 B 的讀取嚴格發生於 A 的讀取之後,因此即使 C 的寫入仍在進行中,也必須返回 `1`(與 [圖 9-1](/img/fig9-1.png) 中的 Alice 和 Bob 的情況相同:在 Alice 讀取新值之後Bob 也希望讀取新的值)。
我們可以進一步細化這個時序圖,展示每個操作是如何在特定時刻原子性生效的。[圖 9-4](../img/fig9-4.png) 顯示了一個更複雜的例子【10】。
我們可以進一步細化這個時序圖,展示每個操作是如何在特定時刻原子性生效的。[圖 9-4](/img/fig9-4.png) 顯示了一個更複雜的例子【10】。
在 [圖 9-4](../img/fig9-4.png) 中,除了讀寫之外,還增加了第三種類型的操作:
在 [圖 9-4](/img/fig9-4.png) 中,除了讀寫之外,還增加了第三種類型的操作:
* $cas(x, v_{old}, v_{new})⇒r$ 表示客戶端請求進行原子性的 [**比較與設定**](ch7.md#比較並設定CAS) 操作。如果暫存器 $x$ 的當前值等於 $v_{old}$ ,則應該原子地設定為 $v_{new}$ 。如果 $x$ 不等於 $v_{old}$ ,則操作應該保持暫存器不變並返回一個錯誤。$r$ 是資料庫的響應(正確或錯誤)。
* $cas(x, v_{old}, v_{new})⇒r$ 表示客戶端請求進行原子性的 [**比較與設定**](/tw/ch7#比較並設定CAS) 操作。如果暫存器 $x$ 的當前值等於 $v_{old}$ ,則應該原子地設定為 $v_{new}$ 。如果 $x$ 不等於 $v_{old}$ ,則操作應該保持暫存器不變並返回一個錯誤。$r$ 是資料庫的響應(正確或錯誤)。
[圖 9-4](../img/fig9-4.png) 中的每個操作都在我們認為操作被執行的時候用豎線標出(在每個操作的橫柱之內)。這些標記按順序連在一起,其結果必須是一個有效的暫存器讀寫序列(**每次讀取都必須返回最近一次寫入設定的值**)。
[圖 9-4](/img/fig9-4.png) 中的每個操作都在我們認為操作被執行的時候用豎線標出(在每個操作的橫柱之內)。這些標記按順序連在一起,其結果必須是一個有效的暫存器讀寫序列(**每次讀取都必須返回最近一次寫入設定的值**)。
線性一致性的要求是,操作標記的連線總是按時間(從左到右)向前移動,而不是向後移動。這個要求確保了我們之前討論的新鮮度保證:一旦新的值被寫入或讀取,所有後續的讀都會看到寫入的值,直到它被再次覆蓋。
![](../img/fig9-4.png)
![](/img/fig9-4.png)
**圖 9-4 將讀取和寫入看起來已經生效的時間點進行視覺化。客戶端 B 的最後一次讀取不是線性一致的**
[圖 9-4](../img/fig9-4.png) 中有一些有趣的細節需要指出:
[圖 9-4](/img/fig9-4.png) 中有一些有趣的細節需要指出:
* 第一個客戶端 B 傳送一個讀取 `x` 的請求,然後客戶端 D 傳送一個請求將 `x` 設定為 `0`,然後客戶端 A 傳送請求將 `x` 設定為 `1`。然而,返回給 B 的讀取值為 `1`(由 A 寫入的值)。這是可以的:這意味著資料庫首先處理 D 的寫入,然後是 A 的寫入,最後是 B 的讀取。雖然這不是請求傳送的順序,但這是一個可以接受的順序,因為這三個請求是併發的。也許 B 的讀請求在網路上略有延遲,所以它在兩次寫入之後才到達資料庫。
@@ -126,26 +129,26 @@
* 此模型不假設有任何事務隔離另一個客戶端可能隨時更改值。例如C 首先讀取到 `1` ,然後讀取到 `2` ,因為兩次讀取之間的值被 B 所更改。可以使用原子 **比較並設定cas** 操作來檢查該值是否未被另一客戶端同時更改B 和 C 的 **cas** 請求成功,但是 D 的 **cas** 請求失敗(在資料庫處理它時,`x` 的值不再是 `0` )。
* 客戶 B 的最後一次讀取(陰影條柱中)不是線性一致的。該操作與 C 的 **cas** 寫操作併發(它將 `x``2` 更新為 `4` 。在沒有其他請求的情況下B 的讀取返回 `2` 是可以的。然而,在 B 的讀取開始之前,客戶端 A 已經讀取了新的值 `4` ,因此不允許 B 讀取比 A 更舊的值。再次,與 [圖 9-1](../img/fig9-1.png) 中的 Alice 和 Bob 的情況相同。
* 客戶 B 的最後一次讀取(陰影條柱中)不是線性一致的。該操作與 C 的 **cas** 寫操作併發(它將 `x``2` 更新為 `4` 。在沒有其他請求的情況下B 的讀取返回 `2` 是可以的。然而,在 B 的讀取開始之前,客戶端 A 已經讀取了新的值 `4` ,因此不允許 B 讀取比 A 更舊的值。再次,與 [圖 9-1](/img/fig9-1.png) 中的 Alice 和 Bob 的情況相同。
這就是線性一致性背後的直覺。正式的定義【6】更準確地描述了它。透過記錄所有請求和響應的時序並檢查它們是否可以排列成有效的順序以測試一個系統的行為是否線性一致性是可能的儘管在計算上是昂貴的【11】。
> ### 線性一致性與可序列化
>
> **線性一致性** 容易和 [**可序列化**](ch7.md#可序列化) 相混淆,因為兩個詞似乎都是類似 “可以按順序排列” 的東西。但它們是兩種完全不同的保證,區分兩者非常重要:
> **線性一致性** 容易和 [**可序列化**](/tw/ch7#可序列化) 相混淆,因為兩個詞似乎都是類似 “可以按順序排列” 的東西。但它們是兩種完全不同的保證,區分兩者非常重要:
>
> ***可序列化***
>
> **可序列化Serializability** 是事務的隔離屬性,每個事務可以讀寫多個物件(行,文件,記錄)—— 請參閱 “[單物件和多物件操作](ch7.md#單物件和多物件操作)”。它確保事務的行為,與它們按照 **某種** 順序依次執行的結果相同每個事務在下一個事務開始之前執行完成。這種執行順序可以與事務實際執行的順序不同。【12】。
> **可序列化Serializability** 是事務的隔離屬性,每個事務可以讀寫多個物件(行,文件,記錄)—— 請參閱 “[單物件和多物件操作](/tw/ch7#單物件和多物件操作)”。它確保事務的行為,與它們按照 **某種** 順序依次執行的結果相同每個事務在下一個事務開始之前執行完成。這種執行順序可以與事務實際執行的順序不同。【12】。
>
> ***線性一致性***
>
> **線性一致性Linearizability** 是讀取和寫入暫存器(單個物件)的 **新鮮度保證**。它不會將操作組合為事務,因此它也不會阻止寫入偏差等問題(請參閱 “[寫入偏差和幻讀](ch7.md#寫入偏差與幻讀)”),除非採取其他措施(例如 [物化衝突](ch7.md#物化衝突))。
> **線性一致性Linearizability** 是讀取和寫入暫存器(單個物件)的 **新鮮度保證**。它不會將操作組合為事務,因此它也不會阻止寫入偏差等問題(請參閱 “[寫入偏差和幻讀](/tw/ch7#寫入偏差與幻讀)”),除非採取其他措施(例如 [物化衝突](/tw/ch7#物化衝突))。
>
> 一個數據庫可以提供可序列化和線性一致性,這種組合被稱為嚴格的可序列化或 **強的單副本可序列化strong-1SR**【4,13】。基於兩階段鎖定的可序列化實現請參閱 “[兩階段鎖定](ch7.md#兩階段鎖定)” 一節)或 **真的序列執行**(請參閱 “[真的序列執行](ch7.md#真的序列執行)”一節)通常是線性一致性的。
> 一個數據庫可以提供可序列化和線性一致性,這種組合被稱為嚴格的可序列化或 **強的單副本可序列化strong-1SR**【4,13】。基於兩階段鎖定的可序列化實現請參閱 “[兩階段鎖定](/tw/ch7#兩階段鎖定)” 一節)或 **真的序列執行**(請參閱 “[真的序列執行](/tw/ch7#真的序列執行)”一節)通常是線性一致性的。
>
> 但是,可序列化的快照隔離(請參閱 “[可序列化快照隔離](ch7.md#可序列化快照隔離)”)不是線性一致性的:按照設計,它從一致的快照中進行讀取,以避免讀者和寫者之間的鎖競爭。一致性快照的要點就在於 **它不會包括該快照之後的寫入**,因此從快照讀取不是線性一致性的。
> 但是,可序列化的快照隔離(請參閱 “[可序列化快照隔離](/tw/ch7#可序列化快照隔離)”)不是線性一致性的:按照設計,它從一致的快照中進行讀取,以避免讀者和寫者之間的鎖競爭。一致性快照的要點就在於 **它不會包括該快照之後的寫入**,因此從快照讀取不是線性一致性的。
### 依賴線性一致性
@@ -156,7 +159,7 @@
一個使用單主複製的系統需要確保領導者真的只有一個而不是幾個腦裂。一種選擇領導者的方法是使用鎖每個節點在啟動時嘗試獲取鎖成功者成為領導者【14】。不管這個鎖是如何實現的它必須是線性一致的所有節點必須就哪個節點擁有鎖達成一致否則就沒用了。
諸如 Apache ZooKeeper 【15】和 etcd 【16】之類的協調服務通常用於實現分散式鎖和領導者選舉。它們使用一致性演算法以容錯的方式實現線性一致的操作在本章後面的 “[容錯共識](#容錯共識)” 中討論此類演算法)[^iii]。還有許多微妙的細節來正確地實現鎖和領導者選舉(例如,請參閱 “[領導者和鎖](ch8.md#領導者和鎖)” 中的防護問題),而像 Apache Curator 【17】這樣的庫則透過在 ZooKeeper 之上提供更高級別的配方來提供幫助。但是,線性一致性儲存服務是這些協調任務的基礎。
諸如 Apache ZooKeeper 【15】和 etcd 【16】之類的協調服務通常用於實現分散式鎖和領導者選舉。它們使用一致性演算法以容錯的方式實現線性一致的操作在本章後面的 “[容錯共識](#容錯共識)” 中討論此類演算法)[^iii]。還有許多微妙的細節來正確地實現鎖和領導者選舉(例如,請參閱 “[領導者和鎖](/tw/ch8#領導者和鎖)” 中的防護問題),而像 Apache Curator 【17】這樣的庫則透過在 ZooKeeper 之上提供更高級別的配方來提供幫助。但是,線性一致性儲存服務是這些協調任務的基礎。
[^iii]: 嚴格地說ZooKeeper 和 etcd 提供線性一致性的寫操作但讀取可能是陳舊的因為預設情況下它們可以由任何一個副本提供服務。你可以選擇請求線性一致性讀取etcd 稱之為 **法定人數讀取quorum read**【16】而在 ZooKeeper 中,你需要在讀取之前呼叫 `sync()`【15】。請參閱 “[使用全序廣播實現線性一致的儲存](#使用全序廣播實現線性一致的儲存)”。
@@ -170,27 +173,27 @@
如果想要確保銀行賬戶餘額永遠不會為負數,或者不會出售比倉庫裡的庫存更多的物品,或者兩個人不會都預定了航班或劇院裡同一時間的同一個位置。這些約束條件都要求所有節點都同意一個最新的值(賬戶餘額,庫存水平,座位佔用率)。
在實際應用中,寬鬆地處理這些限制有時是可以接受的(例如,如果航班超額預訂,你可以將客戶轉移到不同的航班併為其提供補償)。在這種情況下,可能不需要線性一致性,我們將在 “[及時性與完整性](ch12.md#及時性與完整性)” 中討論這種寬鬆的約束。
在實際應用中,寬鬆地處理這些限制有時是可以接受的(例如,如果航班超額預訂,你可以將客戶轉移到不同的航班併為其提供補償)。在這種情況下,可能不需要線性一致性,我們將在 “[及時性與完整性](/tw/ch12#及時性與完整性)” 中討論這種寬鬆的約束。
然而一個硬性的唯一性約束關係型資料庫中常見的那種需要線性一致性。其他型別的約束如外部索引鍵或屬性約束可以不需要線性一致性【19】。
#### 跨通道的時序依賴
注意 [圖 9-1](../img/fig9-1.png) 中的一個細節:如果 Alice 沒有驚呼得分Bob 就不會知道他的查詢結果是陳舊的。他會在幾秒鐘之後再次重新整理頁面並最終看到最後的分數。由於系統中存在額外的通道Alice 的聲音傳到了 Bob 的耳朵中),線性一致性的違背才被注意到。
注意 [圖 9-1](/img/fig9-1.png) 中的一個細節:如果 Alice 沒有驚呼得分Bob 就不會知道他的查詢結果是陳舊的。他會在幾秒鐘之後再次重新整理頁面並最終看到最後的分數。由於系統中存在額外的通道Alice 的聲音傳到了 Bob 的耳朵中),線性一致性的違背才被注意到。
計算機系統也會出現類似的情況。例如,假設有一個網站,使用者可以上傳照片,一個後臺程序會調整照片大小,降低解析度以加快下載速度(縮圖)。該系統的架構和資料流如 [圖 9-5](../img/fig9-5.png) 所示。
計算機系統也會出現類似的情況。例如,假設有一個網站,使用者可以上傳照片,一個後臺程序會調整照片大小,降低解析度以加快下載速度(縮圖)。該系統的架構和資料流如 [圖 9-5](/img/fig9-5.png) 所示。
影像縮放器需要明確的指令來執行尺寸縮放作業,指令是 Web 伺服器透過訊息佇列傳送的(請參閱 [第十一章](ch11.md)。Web 伺服器不會將整個照片放在佇列中,因為大多數訊息代理都是針對較短的訊息而設計的,而一張照片的空間佔用可能達到幾兆位元組。取而代之的是,首先將照片寫入檔案儲存服務,寫入完成後再將給縮放器的指令放入訊息佇列。
影像縮放器需要明確的指令來執行尺寸縮放作業,指令是 Web 伺服器透過訊息佇列傳送的(請參閱 [第十一章](/tw/ch11)。Web 伺服器不會將整個照片放在佇列中,因為大多數訊息代理都是針對較短的訊息而設計的,而一張照片的空間佔用可能達到幾兆位元組。取而代之的是,首先將照片寫入檔案儲存服務,寫入完成後再將給縮放器的指令放入訊息佇列。
![](../img/fig9-5.png)
![](/img/fig9-5.png)
**圖 9-5 Web 伺服器和影像縮放器透過檔案儲存和訊息佇列進行通訊,開啟競爭條件的可能性。**
如果檔案儲存服務是線性一致的,那麼這個系統應該可以正常工作。如果它不是線性一致的,則存在競爭條件的風險:訊息佇列([圖 9-5](../img/fig9-5.png) 中的步驟 3 和 4可能比儲存服務內部的複製replication更快。在這種情況下當縮放器讀取影像步驟 5可能會看到影像的舊版本或者什麼都沒有。如果它處理的是舊版本的影像則檔案儲存中的全尺寸圖和縮圖就產生了永久性的不一致。
如果檔案儲存服務是線性一致的,那麼這個系統應該可以正常工作。如果它不是線性一致的,則存在競爭條件的風險:訊息佇列([圖 9-5](/img/fig9-5.png) 中的步驟 3 和 4可能比儲存服務內部的複製replication更快。在這種情況下當縮放器讀取影像步驟 5可能會看到影像的舊版本或者什麼都沒有。如果它處理的是舊版本的影像則檔案儲存中的全尺寸圖和縮圖就產生了永久性的不一致。
出現這個問題是因為 Web 伺服器和縮放器之間存在兩個不同的通道:檔案儲存與訊息佇列。沒有線性一致性的新鮮性保證,這兩個通道之間的競爭條件是可能的。這種情況類似於 [圖 9-1](../img/fig9-1.png),資料庫複製與 Alice 的嘴到 Bob 耳朵之間的真人音訊通道之間也存在競爭條件。
出現這個問題是因為 Web 伺服器和縮放器之間存在兩個不同的通道:檔案儲存與訊息佇列。沒有線性一致性的新鮮性保證,這兩個通道之間的競爭條件是可能的。這種情況類似於 [圖 9-1](/img/fig9-1.png),資料庫複製與 Alice 的嘴到 Bob 耳朵之間的真人音訊通道之間也存在競爭條件。
線性一致性並不是避免這種競爭條件的唯一方法,但它是最容易理解的。如果你可以控制額外通道(例如訊息佇列的例子,而不是在 Alice 和 Bob 的例子),則可以使用在 “[讀己之寫](ch5.md#讀己之寫)” 討論過的類似方法,不過會有額外的複雜度代價。
線性一致性並不是避免這種競爭條件的唯一方法,但它是最容易理解的。如果你可以控制額外通道(例如訊息佇列的例子,而不是在 Alice 和 Bob 的例子),則可以使用在 “[讀己之寫](/tw/ch5#讀己之寫)” 討論過的類似方法,不過會有額外的複雜度代價。
### 實現線性一致的系統
@@ -198,15 +201,15 @@
由於線性一致性本質上意味著 “表現得好像只有一個數據副本,而且所有的操作都是原子的”,所以最簡單的答案就是,真的只用一個數據副本。但是這種方法無法容錯:如果持有該副本的節點失效,資料將會丟失,或者至少無法訪問,直到節點重新啟動。
使系統容錯最常用的方法是使用複製。我們再來回顧 [第五章](ch5.md) 中的複製方法,並比較它們是否可以滿足線性一致性:
使系統容錯最常用的方法是使用複製。我們再來回顧 [第五章](/tw/ch5) 中的複製方法,並比較它們是否可以滿足線性一致性:
* 單主複製(可能線性一致)
在具有單主複製功能的系統中(請參閱 “[領導者與追隨者](ch5.md#領導者與追隨者)”),主庫具有用於寫入的資料的主副本,而追隨者在其他節點上保留資料的備份副本。如果從主庫或同步更新的從庫讀取資料,它們 **可能potential** 是線性一致性的 [^iv]。然而實際上並不是每個單主資料庫都是線性一致性的無論是因為設計的原因例如因為使用了快照隔離還是因為在併發處理上存在錯誤【10】。
在具有單主複製功能的系統中(請參閱 “[領導者與追隨者](/tw/ch5#領導者與追隨者)”),主庫具有用於寫入的資料的主副本,而追隨者在其他節點上保留資料的備份副本。如果從主庫或同步更新的從庫讀取資料,它們 **可能potential** 是線性一致性的 [^iv]。然而實際上並不是每個單主資料庫都是線性一致性的無論是因為設計的原因例如因為使用了快照隔離還是因為在併發處理上存在錯誤【10】。
[^iv]: 對單主資料庫進行分割槽(分片),使得每個分割槽有一個單獨的領導者,不會影響線性一致性,因為線性一致性只是對單一物件的保證。交叉分割槽事務是一個不同的問題(請參閱 “[分散式事務與共識](#分散式事務與共識)”)。
從主庫讀取依賴一個假設,你確切地知道領導者是誰。正如在 “[真相由多數所定義](ch8.md#真相由多數所定義)” 中所討論的那樣,一個節點很可能會認為它是領導者,而事實上並非如此 —— 如果具有錯覺的領導者繼續為請求提供服務可能違反線性一致性【20】。使用非同步複製故障切換時甚至可能會丟失已提交的寫入請參閱 “[處理節點宕機](ch5.md#處理節點宕機)”),這同時違反了永續性和線性一致性。
從主庫讀取依賴一個假設,你確切地知道領導者是誰。正如在 “[真相由多數所定義](/tw/ch8#真相由多數所定義)” 中所討論的那樣,一個節點很可能會認為它是領導者,而事實上並非如此 —— 如果具有錯覺的領導者繼續為請求提供服務可能違反線性一致性【20】。使用非同步複製故障切換時甚至可能會丟失已提交的寫入請參閱 “[處理節點宕機](/tw/ch5#處理節點宕機)”),這同時違反了永續性和線性一致性。
* 共識演算法(線性一致)
@@ -214,27 +217,27 @@
* 多主複製(非線性一致)
具有多主程式複製的系統通常不是線性一致的,因為它們同時在多個節點上處理寫入,並將其非同步複製到其他節點。因此,它們可能會產生需要被解決的寫入衝突(請參閱 “[處理寫入衝突](ch5.md#處理寫入衝突)”)。這種衝突是因為缺少單一資料副本所導致的。
具有多主程式複製的系統通常不是線性一致的,因為它們同時在多個節點上處理寫入,並將其非同步複製到其他節點。因此,它們可能會產生需要被解決的寫入衝突(請參閱 “[處理寫入衝突](/tw/ch5#處理寫入衝突)”)。這種衝突是因為缺少單一資料副本所導致的。
* 無主複製(也許不是線性一致的)
對於無主複製的系統Dynamo 風格;請參閱 “[無主複製](ch5.md#無主複製)”),有時候人們會聲稱透過要求法定人數讀寫( $w + r > n$ )可以獲得 “強一致性”。這取決於法定人數的具體配置,以及強一致性如何定義(通常不完全正確)。
對於無主複製的系統Dynamo 風格;請參閱 “[無主複製](/tw/ch5#無主複製)”),有時候人們會聲稱透過要求法定人數讀寫( $w + r > n$ )可以獲得 “強一致性”。這取決於法定人數的具體配置,以及強一致性如何定義(通常不完全正確)。
基於日曆時鐘(例如,在 Cassandra 中;請參閱 “[依賴同步時鐘](ch8.md#依賴同步時鐘)”)的 “最後寫入勝利” 衝突解決方法幾乎可以確定是非線性一致的,由於時鐘偏差,不能保證時鐘的時間戳與實際事件順序一致。寬鬆的法定人數(請參閱 “[寬鬆的法定人數與提示移交](ch5.md#寬鬆的法定人數與提示移交)”)也破壞了線性一致的可能性。即使使用嚴格的法定人數,非線性一致的行為也是可能的,如下節所示。
基於日曆時鐘(例如,在 Cassandra 中;請參閱 “[依賴同步時鐘](/tw/ch8#依賴同步時鐘)”)的 “最後寫入勝利” 衝突解決方法幾乎可以確定是非線性一致的,由於時鐘偏差,不能保證時鐘的時間戳與實際事件順序一致。寬鬆的法定人數(請參閱 “[寬鬆的法定人數與提示移交](/tw/ch5#寬鬆的法定人數與提示移交)”)也破壞了線性一致的可能性。即使使用嚴格的法定人數,非線性一致的行為也是可能的,如下節所示。
#### 線性一致性和法定人數
直覺上在 Dynamo 風格的模型中,嚴格的法定人數讀寫應該是線性一致性的。但是當我們有可變的網路延遲時,就可能存在競爭條件,如 [圖 9-6](../img/fig9-6.png) 所示。
直覺上在 Dynamo 風格的模型中,嚴格的法定人數讀寫應該是線性一致性的。但是當我們有可變的網路延遲時,就可能存在競爭條件,如 [圖 9-6](/img/fig9-6.png) 所示。
![](../img/fig9-6.png)
![](/img/fig9-6.png)
**圖 9-6 非線性一致的執行,儘管使用了嚴格的法定人數**
在 [圖 9-6](../img/fig9-6.png) 中,$x$ 的初始值為 0寫入客戶端透過向所有三個副本 $n = 3, w = 3$ )傳送寫入將 $x$ 更新為 `1`。客戶端 A 併發地從兩個節點組成的法定人群( $r = 2$ )中讀取資料,並在其中一個節點上看到新值 `1` 。客戶端 B 也併發地從兩個不同的節點組成的法定人數中讀取,並從兩個節點中取回了舊值 `0`
在 [圖 9-6](/img/fig9-6.png) 中,$x$ 的初始值為 0寫入客戶端透過向所有三個副本 $n = 3, w = 3$ )傳送寫入將 $x$ 更新為 `1`。客戶端 A 併發地從兩個節點組成的法定人群( $r = 2$ )中讀取資料,並在其中一個節點上看到新值 `1` 。客戶端 B 也併發地從兩個不同的節點組成的法定人數中讀取,並從兩個節點中取回了舊值 `0`
法定人數條件滿足( $w + r> n$ 但是這個執行是非線性一致的B 的請求在 A 的請求完成後開始,但是 B 返回舊值,而 A 返回新值。(又一次,如同 Alice 和 Bob 的例子 [圖 9-1](../img/fig9-1.png)
法定人數條件滿足( $w + r> n$ 但是這個執行是非線性一致的B 的請求在 A 的請求完成後開始,但是 B 返回舊值,而 A 返回新值。(又一次,如同 Alice 和 Bob 的例子 [圖 9-1](/img/fig9-1.png)
有趣的是,透過犧牲效能,可以使 Dynamo 風格的法定人數線性化:讀取者必須在將結果返回給應用之前,同步執行讀修復(請參閱 “[讀修復和反熵](ch5.md#讀修復和反熵)”) 並且寫入者必須在傳送寫入之前讀取法定數量節點的最新狀態【24,25】。然而由於效能損失Riak 不執行同步讀修復【26】。Cassandra 在進行法定人數讀取時,**確實** 在等待讀修復完成【27】但是由於使用了最後寫入勝利的衝突解決方案當同一個鍵有多個併發寫入時將不能保證線性一致性。
有趣的是,透過犧牲效能,可以使 Dynamo 風格的法定人數線性化:讀取者必須在將結果返回給應用之前,同步執行讀修復(請參閱 “[讀修復和反熵](/tw/ch5#讀修復和反熵)”) 並且寫入者必須在傳送寫入之前讀取法定數量節點的最新狀態【24,25】。然而由於效能損失Riak 不執行同步讀修復【26】。Cassandra 在進行法定人數讀取時,**確實** 在等待讀修復完成【27】但是由於使用了最後寫入勝利的衝突解決方案當同一個鍵有多個併發寫入時將不能保證線性一致性。
而且這種方式只能實現線性一致的讀寫不能實現線性一致的比較和設定CAS操作因為它需要一個共識演算法【28】。
@@ -245,9 +248,9 @@
一些複製方法可以提供線性一致性,另一些複製方法則不能,因此深入地探討線性一致性的優缺點是很有趣的。
我們已經在 [第五章](ch5.md) 中討論了不同複製方法的一些用例。例如對多資料中心的複製而言,多主複製通常是理想的選擇(請參閱 “[運維多個數據中心](ch5.md#運維多個數據中心)”)。[圖 9-7](../img/fig9-7.png) 說明了這種部署的一個例子。
我們已經在 [第五章](/tw/ch5) 中討論了不同複製方法的一些用例。例如對多資料中心的複製而言,多主複製通常是理想的選擇(請參閱 “[運維多個數據中心](/tw/ch5#運維多個數據中心)”)。[圖 9-7](/img/fig9-7.png) 說明了這種部署的一個例子。
![](../img/fig9-7.png)
![](/img/fig9-7.png)
**圖 9-7 網路中斷迫使在線性一致性和可用性之間做出選擇。**
@@ -286,7 +289,7 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
在分散式系統中有更多有趣的 “不可能” 的結果【41】且 CAP 定理現在已經被更精確的結果取代【2,42】所以它現在基本上成了歷史古蹟了。
[^vi]: 正如 “[真實世界的網路故障](ch8.md#真實世界的網路故障)” 中所討論的,本書使用 **分割槽partition** 指代將大資料集細分為小資料集的操作(分片;請參閱 [第六章](ch6.md))。與之對應的是,**網路分割槽network partition** 是一種特定型別的網路故障,我們通常不會將其與其他型別的故障分開考慮。但是,由於它是 CAP 的 P所以這種情況下我們無法避免混亂。
[^vi]: 正如 “[真實世界的網路故障](/tw/ch8#真實世界的網路故障)” 中所討論的,本書使用 **分割槽partition** 指代將大資料集細分為小資料集的操作(分片;請參閱 [第六章](/tw/ch6))。與之對應的是,**網路分割槽network partition** 是一種特定型別的網路故障,我們通常不會將其與其他型別的故障分開考慮。但是,由於它是 CAP 的 P所以這種情況下我們無法避免混亂。
#### 線性一致性和網路延遲
@@ -298,18 +301,18 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
許多分散式資料庫也是如此:它們是 **為了提高效能** 而選擇了犧牲線性一致性而不是為了容錯【46】。線性一致的速度很慢 —— 這始終是事實,而不僅僅是網路故障期間。
能找到一個更高效的線性一致儲存實現嗎看起來答案是否定的Attiya 和 Welch 【47】證明如果你想要線性一致性讀寫請求的響應時間至少與網路延遲的不確定性成正比。在像大多數計算機網路一樣具有高度可變延遲的網路中請參閱 “[超時與無窮的延遲](ch8.md#超時與無窮的延遲)”),線性讀寫的響應時間不可避免地會很高。更快地線性一致演算法不存在,但更弱的一致性模型可以快得多,所以對延遲敏感的系統而言,這類權衡非常重要。在 [第十二章](ch12.md) 中將討論一些在不犧牲正確性的前提下,繞開線性一致性的方法。
能找到一個更高效的線性一致儲存實現嗎看起來答案是否定的Attiya 和 Welch 【47】證明如果你想要線性一致性讀寫請求的響應時間至少與網路延遲的不確定性成正比。在像大多數計算機網路一樣具有高度可變延遲的網路中請參閱 “[超時與無窮的延遲](/tw/ch8#超時與無窮的延遲)”),線性讀寫的響應時間不可避免地會很高。更快地線性一致演算法不存在,但更弱的一致性模型可以快得多,所以對延遲敏感的系統而言,這類權衡非常重要。在 [第十二章](/tw/ch12) 中將討論一些在不犧牲正確性的前提下,繞開線性一致性的方法。
## 順序保證
之前說過,線性一致暫存器的行為就好像只有單個數據副本一樣,且每個操作似乎都是在某個時間點以原子性的方式生效的。這個定義意味著操作是按照某種良好定義的順序執行的。我們將操作以看上去被執行的順序連線起來,以此說明了 [圖 9-4](../img/fig9-4.png) 中的順序。
之前說過,線性一致暫存器的行為就好像只有單個數據副本一樣,且每個操作似乎都是在某個時間點以原子性的方式生效的。這個定義意味著操作是按照某種良好定義的順序執行的。我們將操作以看上去被執行的順序連線起來,以此說明了 [圖 9-4](/img/fig9-4.png) 中的順序。
**順序ordering** 這一主題在本書中反覆出現,這表明它可能是一個重要的基礎性概念。讓我們簡要回顧一下其它曾經出現過 **順序** 的上下文:
* 在 [第五章](ch5.md) 中我們看到,領導者在單主複製中的主要目的就是,在複製日誌中確定 **寫入順序order of write**—— 也就是從庫應用這些寫入的順序。如果不存在一個領導者,則併發操作可能導致衝突(請參閱 “[處理寫入衝突](ch5.md#處理寫入衝突)”)。
* 在 [第七章](ch7.md) 中討論的 **可序列化**,是關於事務表現的像按 **某種先後順序some sequential order** 執行的保證。它可以字面意義上地以 **序列順序serial order** 執行事務來實現,或者允許並行執行,但同時防止序列化衝突來實現(透過鎖或中止事務)。
* 在 [第八章](ch8.md) 討論過的在分散式系統中使用時間戳和時鐘(請參閱 “[依賴同步時鐘](ch8.md#依賴同步時鐘)”)是另一種將順序引入無序世界的嘗試,例如,確定兩個寫入操作哪一個更晚發生。
* 在 [第五章](/tw/ch5) 中我們看到,領導者在單主複製中的主要目的就是,在複製日誌中確定 **寫入順序order of write**—— 也就是從庫應用這些寫入的順序。如果不存在一個領導者,則併發操作可能導致衝突(請參閱 “[處理寫入衝突](/tw/ch5#處理寫入衝突)”)。
* 在 [第七章](/tw/ch7) 中討論的 **可序列化**,是關於事務表現的像按 **某種先後順序some sequential order** 執行的保證。它可以字面意義上地以 **序列順序serial order** 執行事務來實現,或者允許並行執行,但同時防止序列化衝突來實現(透過鎖或中止事務)。
* 在 [第八章](/tw/ch8) 討論過的在分散式系統中使用時間戳和時鐘(請參閱 “[依賴同步時鐘](/tw/ch8#依賴同步時鐘)”)是另一種將順序引入無序世界的嘗試,例如,確定兩個寫入操作哪一個更晚發生。
事實證明,順序、線性一致性和共識之間有著深刻的聯絡。儘管這個概念比本書其他部分更加理論化和抽象,但對於明確系統的能力範圍(可以做什麼和不可以做什麼)而言是非常有幫助的。我們將在接下來的幾節中探討這個話題。
@@ -317,12 +320,12 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
**順序** 反覆出現有幾個原因,其中一個原因是,它有助於保持 **因果關係causality**。在本書中我們已經看到了幾個例子,其中因果關係是很重要的:
* 在 “[一致字首讀](ch5.md#一致字首讀)”([圖 5-5](../img/fig5-5.png))中,我們看到一個例子:一個對話的觀察者首先看到問題的答案,然後才看到被回答的問題。這是令人困惑的,因為它違背了我們對 **因cause****果effect** 的直覺:如果一個問題被回答,顯然問題本身得先在那裡,因為給出答案的人必須先看到這個問題(假如他們並沒有預見未來的超能力)。我們認為在問題和答案之間存在 **因果依賴causal dependency**
* [圖 5-9](../img/fig5-9.png) 中出現了類似的模式,我們看到三位領導者之間的複製,並注意到由於網路延遲,一些寫入可能會 “壓倒” 其他寫入。從其中一個副本的角度來看,好像有一個對尚不存在的記錄的更新操作。這裡的因果意味著,一條記錄必須先被建立,然後才能被更新。
* 在 “[檢測併發寫入](ch5.md#檢測併發寫入)” 中我們觀察到,如果有兩個操作 A 和 B則存在三種可能性A 發生在 B 之前,或 B 發生在 A 之前,或者 A 和 B**併發**。這種 **此前發生happened before** 關係是因果關係的另一種表述:如果 A 在 B 前發生,那麼意味著 B 可能已經知道了 A或者建立在 A 的基礎上,或者依賴於 A。如果 A 和 B 是 **併發** 的,那麼它們之間並沒有因果聯絡;換句話說,我們確信 A 和 B 不知道彼此。
* 在事務快照隔離的上下文中(“[快照隔離和可重複讀](ch7.md#快照隔離和可重複讀)”),我們說事務是從一致性快照中讀取的。但此語境中 “一致” 到底又是什麼意思?這意味著 **與因果關係保持一致consistent with causality**如果快照包含答案它也必須包含被回答的問題【48】。在某個時間點觀察整個資料庫與因果關係保持一致意味著因果上在該時間點之前發生的所有操作其影響都是可見的但因果上在該時間點之後發生的操作其影響對觀察者不可見。**讀偏差read skew** 意味著讀取的資料處於違反因果關係的狀態(不可重複讀,如 [圖 7-6](../img/fig7-6.png) 所示)。
* 事務之間 **寫偏差write skew** 的例子(請參閱 “[寫入偏差與幻讀](ch7.md#寫入偏差與幻讀)”)也說明了因果依賴:在 [圖 7-8](../img/fig7-8.png) 中,愛麗絲被允許離班,因為事務認為鮑勃仍在值班,反之亦然。在這種情況下,離班的動作因果依賴於對當前值班情況的觀察。[可序列化快照隔離](ch7.md#可序列化快照隔離) 透過跟蹤事務之間的因果依賴來檢測寫偏差。
* 在愛麗絲和鮑勃看球的例子中([圖 9-1](../img/fig9-1.png)),在聽到愛麗絲驚呼比賽結果後,鮑勃從伺服器得到陳舊結果的事實違背了因果關係:愛麗絲的驚呼因果依賴於得分宣告,所以鮑勃應該也能在聽到愛麗斯驚呼後查詢到比分。相同的模式在 “[跨通道的時序依賴](#跨通道的時序依賴)” 一節中,以 “影像大小調整服務” 的偽裝再次出現。
* 在 “[一致字首讀](/tw/ch5#一致字首讀)”([圖 5-5](/img/fig5-5.png))中,我們看到一個例子:一個對話的觀察者首先看到問題的答案,然後才看到被回答的問題。這是令人困惑的,因為它違背了我們對 **因cause****果effect** 的直覺:如果一個問題被回答,顯然問題本身得先在那裡,因為給出答案的人必須先看到這個問題(假如他們並沒有預見未來的超能力)。我們認為在問題和答案之間存在 **因果依賴causal dependency**
* [圖 5-9](/img/fig5-9.png) 中出現了類似的模式,我們看到三位領導者之間的複製,並注意到由於網路延遲,一些寫入可能會 “壓倒” 其他寫入。從其中一個副本的角度來看,好像有一個對尚不存在的記錄的更新操作。這裡的因果意味著,一條記錄必須先被建立,然後才能被更新。
* 在 “[檢測併發寫入](/tw/ch5#檢測併發寫入)” 中我們觀察到,如果有兩個操作 A 和 B則存在三種可能性A 發生在 B 之前,或 B 發生在 A 之前,或者 A 和 B**併發**。這種 **此前發生happened before** 關係是因果關係的另一種表述:如果 A 在 B 前發生,那麼意味著 B 可能已經知道了 A或者建立在 A 的基礎上,或者依賴於 A。如果 A 和 B 是 **併發** 的,那麼它們之間並沒有因果聯絡;換句話說,我們確信 A 和 B 不知道彼此。
* 在事務快照隔離的上下文中(“[快照隔離和可重複讀](/tw/ch7#快照隔離和可重複讀)”),我們說事務是從一致性快照中讀取的。但此語境中 “一致” 到底又是什麼意思?這意味著 **與因果關係保持一致consistent with causality**如果快照包含答案它也必須包含被回答的問題【48】。在某個時間點觀察整個資料庫與因果關係保持一致意味著因果上在該時間點之前發生的所有操作其影響都是可見的但因果上在該時間點之後發生的操作其影響對觀察者不可見。**讀偏差read skew** 意味著讀取的資料處於違反因果關係的狀態(不可重複讀,如 [圖 7-6](/img/fig7-6.png) 所示)。
* 事務之間 **寫偏差write skew** 的例子(請參閱 “[寫入偏差與幻讀](/tw/ch7#寫入偏差與幻讀)”)也說明了因果依賴:在 [圖 7-8](/img/fig7-8.png) 中,愛麗絲被允許離班,因為事務認為鮑勃仍在值班,反之亦然。在這種情況下,離班的動作因果依賴於對當前值班情況的觀察。[可序列化快照隔離](/tw/ch7#可序列化快照隔離) 透過跟蹤事務之間的因果依賴來檢測寫偏差。
* 在愛麗絲和鮑勃看球的例子中([圖 9-1](/img/fig9-1.png)),在聽到愛麗絲驚呼比賽結果後,鮑勃從伺服器得到陳舊結果的事實違背了因果關係:愛麗絲的驚呼因果依賴於得分宣告,所以鮑勃應該也能在聽到愛麗斯驚呼後查詢到比分。相同的模式在 “[跨通道的時序依賴](#跨通道的時序依賴)” 一節中,以 “影像大小調整服務” 的偽裝再次出現。
因果關係對事件施加了一種 **順序**:因在果之前;訊息傳送在訊息收取之前。而且就像現實生活中一樣,一件事會導致另一件事:某個節點讀取了一些資料然後寫入一些結果,另一個節點讀取其寫入的內容,並依次寫入一些其他內容,等等。這些因果依賴的操作鏈定義了系統中的因果順序,即,什麼在什麼之前發生。
@@ -341,21 +344,21 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
* 線性一致性
在線性一致的系統中,操作是全序的:如果系統表現的就好像只有一個數據副本,並且所有操作都是原子性的,這意味著對任何兩個操作,我們總是能判定哪個操作先發生。這個全序在 [圖 9-4](../img/fig9-4.png) 中以時間線表示。
在線性一致的系統中,操作是全序的:如果系統表現的就好像只有一個數據副本,並且所有操作都是原子性的,這意味著對任何兩個操作,我們總是能判定哪個操作先發生。這個全序在 [圖 9-4](/img/fig9-4.png) 中以時間線表示。
* 因果性
我們說過,如果兩個操作都沒有在彼此 **之前發生**,那麼這兩個操作是併發的(請參閱 [“此前發生” 的關係和併發](ch5.md#“此前發生”的關係和併發))。換句話說,如果兩個事件是因果相關的(一個發生在另一個事件之前),則它們之間是有序的,但如果它們是併發的,則它們之間的順序是無法比較的。這意味著因果關係定義了一個偏序,而不是一個全序:一些操作相互之間是有順序的,但有些則是無法比較的。
我們說過,如果兩個操作都沒有在彼此 **之前發生**,那麼這兩個操作是併發的(請參閱 [“此前發生” 的關係和併發](/tw/ch5#“此前發生”的關係和併發))。換句話說,如果兩個事件是因果相關的(一個發生在另一個事件之前),則它們之間是有序的,但如果它們是併發的,則它們之間的順序是無法比較的。這意味著因果關係定義了一個偏序,而不是一個全序:一些操作相互之間是有順序的,但有些則是無法比較的。
因此,根據這個定義,在線性一致的資料儲存中是不存在併發操作的:必須有且僅有一條時間線,所有的操作都在這條時間線上,構成一個全序關係。可能有幾個請求在等待處理,但是資料儲存確保了每個請求都是在唯一時間線上的某個時間點自動處理的,不存在任何併發。
併發意味著時間線會分岔然後合併 —— 在這種情況下,不同分支上的操作是無法比較的(即併發操作)。在 [第五章](ch5.md) 中我們看到了這種現象:例如,[圖 5-14](../img/fig5-14.png) 並不是一條直線的全序關係,而是一堆不同的操作併發進行。圖中的箭頭指明瞭因果依賴 —— 操作的偏序。
併發意味著時間線會分岔然後合併 —— 在這種情況下,不同分支上的操作是無法比較的(即併發操作)。在 [第五章](/tw/ch5) 中我們看到了這種現象:例如,[圖 5-14](/img/fig5-14.png) 並不是一條直線的全序關係,而是一堆不同的操作併發進行。圖中的箭頭指明瞭因果依賴 —— 操作的偏序。
如果你熟悉像 Git 這樣的分散式版本控制系統,那麼其版本歷史與因果關係圖極其相似。通常,一個 **提交Commit** 發生在另一個提交之後,在一條直線上。但是有時你會遇到分支(當多個人同時在一個專案上工作時),**合併Merge** 會在這些併發建立的提交相融合時建立。
#### 線性一致性強於因果一致性
那麼因果順序和線性一致性之間的關係是什麼?答案是線性一致性 **隱含著implies** 因果關係任何線性一致的系統都能正確保持因果性【7】。特別是如果系統中有多個通訊通道如 [圖 9-5](../img/fig9-5.png) 中的訊息佇列和檔案儲存服務),線性一致性可以自動保證因果性,系統無需任何特殊操作(如在不同元件間傳遞時間戳)。
那麼因果順序和線性一致性之間的關係是什麼?答案是線性一致性 **隱含著implies** 因果關係任何線性一致的系統都能正確保持因果性【7】。特別是如果系統中有多個通訊通道如 [圖 9-5](/img/fig9-5.png) 中的訊息佇列和檔案儲存服務),線性一致性可以自動保證因果性,系統無需任何特殊操作(如在不同元件間傳遞時間戳)。
線性一致性確保因果性的事實使線性一致系統變得簡單易懂,更有吸引力。然而,正如 “[線性一致性的代價](#線性一致性的代價)” 中所討論的,使系統線性一致可能會損害其效能和可用性,尤其是在系統具有嚴重的網路延遲的情況下(例如,如果系統在地理上散佈)。出於這個原因,一些分散式資料系統已經放棄了線性一致性,從而獲得更好的效能,但它們用起來也更為困難。
@@ -373,16 +376,16 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
為了確定因果依賴,我們需要一些方法來描述系統中節點的 “知識”。如果節點在發出寫入 Y 的請求時已經看到了 X 的值,則 X 和 Y 可能存在因果關係。這個分析使用了那些在欺詐指控刑事調查中常見的問題CEO 在做出決定 Y 時是否 **知道** X
用於確定 *哪些操作發生在其他操作之前* 的技術,與我們在 “[檢測併發寫入](ch5.md#檢測併發寫入)” 中所討論的內容類似。那一節討論了無領導者資料儲存中的因果性為了防止丟失更新我們需要檢測到對同一個鍵的併發寫入。因果一致性則更進一步它需要跟蹤整個資料庫中的因果依賴而不僅僅是一個鍵。可以推廣版本向量以解決此類問題【54】。
用於確定 *哪些操作發生在其他操作之前* 的技術,與我們在 “[檢測併發寫入](/tw/ch5#檢測併發寫入)” 中所討論的內容類似。那一節討論了無領導者資料儲存中的因果性為了防止丟失更新我們需要檢測到對同一個鍵的併發寫入。因果一致性則更進一步它需要跟蹤整個資料庫中的因果依賴而不僅僅是一個鍵。可以推廣版本向量以解決此類問題【54】。
為了確定因果順序,資料庫需要知道應用讀取了哪個版本的資料。這就是為什麼在 [圖 5-13](../img/fig5-13.png) 中,來自先前操作的版本號在寫入時被傳回到資料庫的原因。在 SSI 的衝突檢測中會出現類似的想法,如 “[可序列化快照隔離](ch7.md#可序列化快照隔離)” 中所述:當事務要提交時,資料庫將檢查它所讀取的資料版本是否仍然是最新的。為此,資料庫跟蹤哪些資料被哪些事務所讀取。
為了確定因果順序,資料庫需要知道應用讀取了哪個版本的資料。這就是為什麼在 [圖 5-13](/img/fig5-13.png) 中,來自先前操作的版本號在寫入時被傳回到資料庫的原因。在 SSI 的衝突檢測中會出現類似的想法,如 “[可序列化快照隔離](/tw/ch7#可序列化快照隔離)” 中所述:當事務要提交時,資料庫將檢查它所讀取的資料版本是否仍然是最新的。為此,資料庫跟蹤哪些資料被哪些事務所讀取。
### 序列號順序
雖然因果是一個重要的理論概念,但實際上跟蹤所有的因果關係是不切實際的。在許多應用中,客戶端在寫入內容之前會先讀取大量資料,我們無法弄清寫入因果依賴於先前全部的讀取內容,還是僅包括其中一部分。顯式跟蹤所有已讀資料意味著巨大的額外開銷。
但還有一個更好的方法:我們可以使用 **序列號sequence number****時間戳timestamp** 來排序事件。時間戳不一定來自日曆時鐘(或物理時鐘,它們存在許多問題,如 “[不可靠的時鐘](ch8.md#不可靠的時鐘)” 中所述)。它可以來自一個 **邏輯時鐘logical clock**,這是一個用來生成標識操作的數字序列的演算法,典型實現是使用一個每次操作自增的計數器。
但還有一個更好的方法:我們可以使用 **序列號sequence number****時間戳timestamp** 來排序事件。時間戳不一定來自日曆時鐘(或物理時鐘,它們存在許多問題,如 “[不可靠的時鐘](/tw/ch8#不可靠的時鐘)” 中所述)。它可以來自一個 **邏輯時鐘logical clock**,這是一個用來生成標識操作的數字序列的演算法,典型實現是使用一個每次操作自增的計數器。
這樣的序列號或時間戳是緊湊的(只有幾個位元組大小),它提供了一個全序關係:也就是說每個操作都有一個唯一的序列號,而且總是可以比較兩個序列號,確定哪一個更大(即哪些操作後發生)。
@@ -390,14 +393,14 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
[^vii]: 與因果關係不一致的全序很容易建立,但沒啥用。例如你可以為每個操作生成隨機的 UUID並按照字典序比較 UUID以定義操作的全序。這是一個有效的全序但是隨機的 UUID 並不能告訴你哪個操作先發生,或者操作是否為併發的。
在單主複製的資料庫中(請參閱 “[領導者與追隨者](ch5.md#領導者與追隨者)”),複製日誌定義了與因果一致的寫操作。主庫可以簡單地為每個操作自增一個計數器,從而為複製日誌中的每個操作分配一個單調遞增的序列號。如果一個從庫按照它們在複製日誌中出現的順序來應用寫操作,那麼從庫的狀態始終是因果一致的(即使它落後於領導者)。
在單主複製的資料庫中(請參閱 “[領導者與追隨者](/tw/ch5#領導者與追隨者)”),複製日誌定義了與因果一致的寫操作。主庫可以簡單地為每個操作自增一個計數器,從而為複製日誌中的每個操作分配一個單調遞增的序列號。如果一個從庫按照它們在複製日誌中出現的順序來應用寫操作,那麼從庫的狀態始終是因果一致的(即使它落後於領導者)。
#### 非因果序列號生成器
如果主庫不存在(可能因為使用了多主資料庫或無主資料庫,或者因為使用了分割槽的資料庫),如何為操作生成序列號就沒有那麼明顯了。在實踐中有各種各樣的方法:
* 每個節點都可以生成自己獨立的一組序列號。例如有兩個節點,一個節點只能生成奇數,而另一個節點只能生成偶數。通常,可以在序列號的二進位制表示中預留一些位,用於唯一的節點識別符號,這樣可以確保兩個不同的節點永遠不會生成相同的序列號。
*可以將日曆時鐘物理時鐘的時間戳附加到每個操作上【55】。這種時間戳並不連續但是如果它具有足夠高的解析度那也許足以提供一個操作的全序關係。這一事實應用於* 最後寫入勝利 * 的衝突解決方法中(請參閱 “[有序事件的時間戳](ch8.md#有序事件的時間戳)”)。
*可以將日曆時鐘物理時鐘的時間戳附加到每個操作上【55】。這種時間戳並不連續但是如果它具有足夠高的解析度那也許足以提供一個操作的全序關係。這一事實應用於* 最後寫入勝利 * 的衝突解決方法中(請參閱 “[有序事件的時間戳](/tw/ch8#有序事件的時間戳)”)。
* 可以預先分配序列號區塊。例如,節點 A 可能要求從序列號 1 到 1,000 區塊的所有權,而節點 B 可能要求序列號 1,001 到 2,000 區塊的所有權。然後每個節點可以獨立分配所屬區塊中的序列號,並在序列號告急時請求分配一個新的區塊。
這三個選項都比單一主庫的自增計數器表現要好,並且更具可伸縮性。它們為每個操作生成一個唯一的,近似自增的序列號。然而它們都有同一個問題:生成的序列號與因果不一致。
@@ -406,9 +409,9 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
* 每個節點每秒可以處理不同數量的操作。因此,如果一個節點產生偶數序列號而另一個產生奇數序列號,則偶數計數器可能落後於奇數計數器,反之亦然。如果你有一個奇數編號的操作和一個偶數編號的操作,你無法準確地說出哪一個操作在因果上先發生。
* 來自物理時鐘的時間戳會受到時鐘偏移的影響,這可能會使其與因果不一致。例如 [圖 8-3](../img/fig8-3.png) 展示了一個例子,其中因果上晚發生的操作,卻被分配了一個更早的時間戳。[^viii]
* 來自物理時鐘的時間戳會受到時鐘偏移的影響,這可能會使其與因果不一致。例如 [圖 8-3](/img/fig8-3.png) 展示了一個例子,其中因果上晚發生的操作,卻被分配了一個更早的時間戳。[^viii]
[^viii]: 可以使物理時鐘時間戳與因果關係保持一致:在 “[全域性快照的同步時鐘](ch8.md#全域性快照的同步時鐘)” 中,我們討論了 Google 的 Spanner它可以估計預期的時鐘偏差並在提交寫入之前等待不確定性間隔。這種方法確保了實際上靠後的事務會有更大的時間戳。但是大多數時鐘不能提供這種所需的不確定性度量。
[^viii]: 可以使物理時鐘時間戳與因果關係保持一致:在 “[全域性快照的同步時鐘](/tw/ch8#全域性快照的同步時鐘)” 中,我們討論了 Google 的 Spanner它可以估計預期的時鐘偏差並在提交寫入之前等待不確定性間隔。這種方法確保了實際上靠後的事務會有更大的時間戳。但是大多數時鐘不能提供這種所需的不確定性度量。
* 在分配區塊的情況下,某個操作可能會被賦予一個範圍在 1,001 到 2,000 內的序列號,然而一個因果上更晚的操作可能被賦予一個範圍在 1 到 1,000 之間的數字。這裡序列號與因果關係也是不一致的。
@@ -417,9 +420,9 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
儘管剛才描述的三個序列號生成器與因果不一致但實際上有一個簡單的方法來產生與因果關係一致的序列號。它被稱為蘭伯特時間戳萊斯利・蘭伯特Leslie Lamport於 1978 年提出【56】現在是分散式系統領域中被引用最多的論文之一。
[圖 9-8](../img/fig9-8.png) 說明了蘭伯特時間戳的應用。每個節點都有一個唯一識別符號,和一個儲存自己執行運算元量的計數器。蘭伯特時間戳就是兩者的簡單組合:(計數器,節點 ID$(counter, node ID)$。兩個節點有時可能具有相同的計數器值,但透過在時間戳中包含節點 ID每個時間戳都是唯一的。
[圖 9-8](/img/fig9-8.png) 說明了蘭伯特時間戳的應用。每個節點都有一個唯一識別符號,和一個儲存自己執行運算元量的計數器。蘭伯特時間戳就是兩者的簡單組合:(計數器,節點 ID$(counter, node ID)$。兩個節點有時可能具有相同的計數器值,但透過在時間戳中包含節點 ID每個時間戳都是唯一的。
![](../img/fig9-8.png)
![](/img/fig9-8.png)
**圖 9-8 Lamport 時間戳提供了與因果關係一致的全序。**
@@ -428,17 +431,17 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
迄今,這個描述與上節所述的奇偶計數器基本類似。使蘭伯特時間戳因果一致的關鍵思想如下所示:每個節點和每個客戶端跟蹤迄今為止所見到的最大 **計數器** 值,並在每個請求中包含這個最大計數器值。當一個節點收到最大計數器值大於自身計數器值的請求或響應時,它立即將自己的計數器設定為這個最大值。
這如 [圖 9-8](../img/fig9-8.png) 所示,其中客戶端 A 從節點 2 接收計數器值 `5` ,然後將最大值 `5` 傳送到節點 1 。此時,節點 1 的計數器僅為 `1` ,但是它立即前移至 `5` ,所以下一個操作的計數器的值為 `6`
這如 [圖 9-8](/img/fig9-8.png) 所示,其中客戶端 A 從節點 2 接收計數器值 `5` ,然後將最大值 `5` 傳送到節點 1 。此時,節點 1 的計數器僅為 `1` ,但是它立即前移至 `5` ,所以下一個操作的計數器的值為 `6`
只要每一個操作都攜帶著最大計數器值,這個方案確保蘭伯特時間戳的排序與因果一致,因為每個因果依賴都會導致時間戳增長。
蘭伯特時間戳有時會與我們在 “[檢測併發寫入](ch5.md#檢測併發寫入)” 中看到的版本向量相混淆。雖然兩者有一些相似之處,但它們有著不同的目的:版本向量可以區分兩個操作是併發的,還是一個因果依賴另一個;而蘭伯特時間戳總是施行一個全序。從蘭伯特時間戳的全序中,你無法分辨兩個操作是併發的還是因果依賴的。蘭伯特時間戳優於版本向量的地方是,它更加緊湊。
蘭伯特時間戳有時會與我們在 “[檢測併發寫入](/tw/ch5#檢測併發寫入)” 中看到的版本向量相混淆。雖然兩者有一些相似之處,但它們有著不同的目的:版本向量可以區分兩個操作是併發的,還是一個因果依賴另一個;而蘭伯特時間戳總是施行一個全序。從蘭伯特時間戳的全序中,你無法分辨兩個操作是併發的還是因果依賴的。蘭伯特時間戳優於版本向量的地方是,它更加緊湊。
#### 光有時間戳排序還不夠
雖然蘭伯特時間戳定義了一個與因果一致的全序,但它還不足以解決分散式系統中的許多常見問題。
例如,考慮一個需要確保使用者名稱能唯一標識使用者帳戶的系統。如果兩個使用者同時嘗試使用相同的使用者名稱建立帳戶,則其中一個應該成功,另一個應該失敗(我們之前在 “[領導者和鎖](ch8.md#領導者和鎖)” 中提到過這個問題)。
例如,考慮一個需要確保使用者名稱能唯一標識使用者帳戶的系統。如果兩個使用者同時嘗試使用相同的使用者名稱建立帳戶,則其中一個應該成功,另一個應該失敗(我們之前在 “[領導者和鎖](/tw/ch8#領導者和鎖)” 中提到過這個問題)。
乍看之下,似乎操作的全序關係足以解決這一問題(例如使用蘭伯特時間戳):如果建立了兩個具有相同使用者名稱的帳戶,選擇時間戳較小的那個作為勝者(第一個抓到使用者名稱的人),並讓帶有更大時間戳者失敗。由於時間戳上有全序關係,所以這個比較總是可行的。
@@ -456,7 +459,7 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
如果你的程式只執行在單個 CPU 核上,那麼定義一個操作全序是很容易的:可以簡單認為就是 CPU 執行這些操作的順序。但是在分散式系統中,讓所有節點對同一個全域性操作順序達成一致可能相當棘手。在上一節中,我們討論了按時間戳或序列號進行排序,但發現它還不如單主複製給力(如果你使用時間戳排序來實現唯一性約束,就不能容忍任何錯誤,因為你必須要從每個節點都獲取到最新的序列號)。
如前所述,單主複製透過選擇一個節點作為主庫來確定操作的全序,並在主庫的單個 CPU 核上對所有操作進行排序。接下來的挑戰是,如果吞吐量超出單個主庫的處理能力,這種情況下如何擴充套件系統;以及,如果主庫失效(“[處理節點宕機](ch5.md#處理節點宕機)”),如何處理故障切換。在分散式系統文獻中,這個問題被稱為 **全序廣播total order broadcast****原子廣播atomic broadcast**[^ix]【25,57,58】。
如前所述,單主複製透過選擇一個節點作為主庫來確定操作的全序,並在主庫的單個 CPU 核上對所有操作進行排序。接下來的挑戰是,如果吞吐量超出單個主庫的處理能力,這種情況下如何擴充套件系統;以及,如果主庫失效(“[處理節點宕機](/tw/ch5#處理節點宕機)”),如何處理故障切換。在分散式系統文獻中,這個問題被稱為 **全序廣播total order broadcast****原子廣播atomic broadcast**[^ix]【25,57,58】。
[^ix]: “原子廣播” 是一個傳統的術語,非常混亂,而且與 “原子” 一詞的其他用法不一致:它與 ACID 事務中的原子性沒有任何關係,只是與原子操作(在多執行緒程式設計的意義上 或原子暫存器線性一致儲存有間接的聯絡。全序組播total order multicast是另一個同義詞。
@@ -480,19 +483,19 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
像 ZooKeeper 和 etcd 這樣的共識服務實際上實現了全序廣播。這一事實暗示了全序廣播與共識之間有著緊密聯絡,我們將在本章稍後進行探討。
全序廣播正是資料庫複製所需的:如果每個訊息都代表一次資料庫的寫入,且每個副本都按相同的順序處理相同的寫入,那麼副本間將相互保持一致(除了臨時的複製延遲)。這個原理被稱為 **狀態機複製state machine replication**【60】我們將在 [第十一章](ch11.md) 中重新回到這個概念。
全序廣播正是資料庫複製所需的:如果每個訊息都代表一次資料庫的寫入,且每個副本都按相同的順序處理相同的寫入,那麼副本間將相互保持一致(除了臨時的複製延遲)。這個原理被稱為 **狀態機複製state machine replication**【60】我們將在 [第十一章](/tw/ch11) 中重新回到這個概念。
與之類似,可以使用全序廣播來實現可序列化的事務:如 “[真的序列執行](ch7.md#真的序列執行)” 中所述如果每個訊息都表示一個確定性事務以儲存過程的形式來執行且每個節點都以相同的順序處理這些訊息那麼資料庫的分割槽和副本就可以相互保持一致【61】。
與之類似,可以使用全序廣播來實現可序列化的事務:如 “[真的序列執行](/tw/ch7#真的序列執行)” 中所述如果每個訊息都表示一個確定性事務以儲存過程的形式來執行且每個節點都以相同的順序處理這些訊息那麼資料庫的分割槽和副本就可以相互保持一致【61】。
全序廣播的一個重要表現是,順序在訊息送達時被固化:如果後續的訊息已經送達,節點就不允許追溯地將(先前)訊息插入順序中的較早位置。這個事實使得全序廣播比時間戳排序更強。
考量全序廣播的另一種方式是,這是一種建立日誌的方式(如在複製日誌、事務日誌或預寫式日誌中):傳遞訊息就像追加寫入日誌。由於所有節點必須以相同的順序傳遞相同的訊息,因此所有節點都可以讀取日誌,並看到相同的訊息序列。
全序廣播對於實現提供防護令牌的鎖服務也很有用(請參閱 “[防護令牌](ch8.md#防護令牌)”)。每個獲取鎖的請求都作為一條訊息追加到日誌末尾,並且所有的訊息都按它們在日誌中出現的順序依次編號。序列號可以當成防護令牌用,因為它是單調遞增的。在 ZooKeeper 中,這個序列號被稱為 `zxid` 【15】。
全序廣播對於實現提供防護令牌的鎖服務也很有用(請參閱 “[防護令牌](/tw/ch8#防護令牌)”)。每個獲取鎖的請求都作為一條訊息追加到日誌末尾,並且所有的訊息都按它們在日誌中出現的順序依次編號。序列號可以當成防護令牌用,因為它是單調遞增的。在 ZooKeeper 中,這個序列號被稱為 `zxid` 【15】。
#### 使用全序廣播實現線性一致的儲存
如 [圖 9-4](../img/fig9-4.png) 所示,在線性一致的系統中,存在操作的全序。這是否意味著線性一致與全序廣播一樣?不盡然,但兩者之間有著密切的聯絡 [^x]。
如 [圖 9-4](/img/fig9-4.png) 所示,在線性一致的系統中,存在操作的全序。這是否意味著線性一致與全序廣播一樣?不盡然,但兩者之間有著密切的聯絡 [^x]。
[^x]: 從形式上講,線性一致讀寫暫存器是一個 “更容易” 的問題。全序廣播等價於共識【67】而共識問題在非同步的崩潰 - 停止模型【68】中沒有確定性的解決方案而線性一致的讀寫暫存器 **可以** 在這種模型中實現【23,24,25】。然而支援諸如 **比較並設定CAS, compare-and-set**,或 **自增並返回increment-and-get** 的原子操作使它等價於共識問題【28】。因此共識問題與線性一致暫存器問題密切相關。
@@ -516,7 +519,7 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
* 你可以透過在日誌中追加一條訊息然後讀取日誌直到該訊息被讀回才執行實際的讀取操作。訊息在日誌中的位置因此定義了讀取發生的時間點etcd 的法定人數讀取有些類似這種情況【16】
* 如果日誌允許以線性一致的方式獲取最新日誌訊息的位置,則可以查詢該位置,等待該位置前的所有訊息都傳達到你,然後執行讀取。(這是 Zookeeper `sync()` 操作背後的思想【15】
* 你可以從同步更新的副本中進行讀取因此可以確保結果是最新的這種技術用於鏈式複製chain replication【63】請參閱 “[關於複製的研究](ch5.md#關於複製的研究)”)。
* 你可以從同步更新的副本中進行讀取因此可以確保結果是最新的這種技術用於鏈式複製chain replication【63】請參閱 “[關於複製的研究](/tw/ch5#關於複製的研究)”)。
#### 使用線性一致性儲存實現全序廣播
@@ -539,17 +542,17 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
**共識** 是分散式計算中最重要也是最基本的問題之一。從表面上看似乎很簡單:非正式地講,目標只是 **讓幾個節點達成一致get serveral nodes to agree on something**。你也許會認為這不會太難。不幸的是,許多出故障的系統都是因為錯誤地輕信這個問題很容易解決。
儘管共識非常重要,但關於它的內容出現在本書的後半部分,因為這個主題非常微妙,欣賞細微之處需要一些必要的知識。即使在學術界,對共識的理解也是在幾十年的過程中逐漸沉澱而來,一路上也有著許多誤解。現在我們已經討論了複製([第五章](ch5.md)),事務([第七章](ch7.md)),系統模型([第八章](ch8.md)),線性一致以及全序廣播(本章),我們終於準備好解決共識問題了。
儘管共識非常重要,但關於它的內容出現在本書的後半部分,因為這個主題非常微妙,欣賞細微之處需要一些必要的知識。即使在學術界,對共識的理解也是在幾十年的過程中逐漸沉澱而來,一路上也有著許多誤解。現在我們已經討論了複製([第五章](/tw/ch5)),事務([第七章](/tw/ch7)),系統模型([第八章](/tw/ch8)),線性一致以及全序廣播(本章),我們終於準備好解決共識問題了。
節點能達成一致,在很多場景下都非常重要,例如:
* 領導選舉
在單主複製的資料庫中,所有節點需要就哪個節點是領導者達成一致。如果一些節點由於網路故障而無法與其他節點通訊,則可能會對領導權的歸屬引起爭議。在這種情況下,共識對於避免錯誤的故障切換非常重要。錯誤的故障切換會導致兩個節點都認為自己是領導者(**腦裂**,請參閱 “[處理節點宕機](ch5.md#處理節點宕機)”)。如果有兩個領導者,它們都會接受寫入,它們的資料會發生分歧,從而導致不一致和資料丟失。
在單主複製的資料庫中,所有節點需要就哪個節點是領導者達成一致。如果一些節點由於網路故障而無法與其他節點通訊,則可能會對領導權的歸屬引起爭議。在這種情況下,共識對於避免錯誤的故障切換非常重要。錯誤的故障切換會導致兩個節點都認為自己是領導者(**腦裂**,請參閱 “[處理節點宕機](/tw/ch5#處理節點宕機)”)。如果有兩個領導者,它們都會接受寫入,它們的資料會發生分歧,從而導致不一致和資料丟失。
* 原子提交
在支援跨多節點或跨多分割槽事務的資料庫中,一個事務可能在某些節點上失敗,但在其他節點上成功。如果我們想要維護事務的原子性(就 ACID 而言,請參閱 “[原子性](ch7.md#原子性)”),我們必須讓所有節點對事務的結果達成一致:要麼全部中止 / 回滾(如果出現任何錯誤),要麼它們全部提交(如果沒有出錯)。這個共識的例子被稱為 **原子提交atomic commit** 問題 [^xii]。
在支援跨多節點或跨多分割槽事務的資料庫中,一個事務可能在某些節點上失敗,但在其他節點上成功。如果我們想要維護事務的原子性(就 ACID 而言,請參閱 “[原子性](/tw/ch7#原子性)”),我們必須讓所有節點對事務的結果達成一致:要麼全部中止 / 回滾(如果出現任何錯誤),要麼它們全部提交(如果沒有出錯)。這個共識的例子被稱為 **原子提交atomic commit** 問題 [^xii]。
[^xii]: 原子提交的形式化與共識稍有不同:原子事務只有在 **所有** 參與者投票提交的情況下才能提交,如果有任何參與者需要中止,則必須中止。共識則允許就 **任意一個** 被參與者提出的候選值達成一致。然而原子提交和共識可以相互簡化為對方【70,71】。**非阻塞** 原子提交則要比共識更為困難 —— 請參閱 “[三階段提交](#三階段提交)”。
@@ -557,7 +560,7 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
>
> 你可能已經聽說過以作者 FischerLynch 和 Paterson 命名的 FLP 結果【68】它證明如果存在節點可能崩潰的風險則不存在 **總是** 能夠達成共識的演算法。在分散式系統中,我們必須假設節點可能會崩潰,所以可靠的共識是不可能的。然而這裡我們正在討論達成共識的演算法,到底是怎麼回事?
>
> 答案是 FLP 結果是在 **非同步系統模型** 中被證明的(請參閱 “[系統模型與現實](ch8.md#系統模型與現實)”),而這是一種限制性很強的模型,它假定確定性演算法不能使用任何時鐘或超時。如果允許演算法使用 **超時** 或其他方法來識別可疑的崩潰節點即使懷疑有時是錯誤的則共識變為一個可解的問題【67】。即使僅僅允許演算法使用隨機數也足以繞過這個不可能的結果【69】。
> 答案是 FLP 結果是在 **非同步系統模型** 中被證明的(請參閱 “[系統模型與現實](/tw/ch8#系統模型與現實)”),而這是一種限制性很強的模型,它假定確定性演算法不能使用任何時鐘或超時。如果允許演算法使用 **超時** 或其他方法來識別可疑的崩潰節點即使懷疑有時是錯誤的則共識變為一個可解的問題【67】。即使僅僅允許演算法使用隨機數也足以繞過這個不可能的結果【69】。
>
> 因此,雖然 FLP 是關於共識不可能性的重要理論結果,但現實中的分散式系統通常是可以達成共識的。
@@ -568,17 +571,17 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
### 原子提交與兩階段提交
在 [第七章](ch7.md) 中我們瞭解到,事務原子性的目的是在多次寫操作中途出錯的情況下,提供一種簡單的語義。事務的結果要麼是成功提交,在這種情況下,事務的所有寫入都是持久化的;要麼是中止,在這種情況下,事務的所有寫入都被回滾(即撤消或丟棄)。
在 [第七章](/tw/ch7) 中我們瞭解到,事務原子性的目的是在多次寫操作中途出錯的情況下,提供一種簡單的語義。事務的結果要麼是成功提交,在這種情況下,事務的所有寫入都是持久化的;要麼是中止,在這種情況下,事務的所有寫入都被回滾(即撤消或丟棄)。
原子性可以防止失敗的事務攪亂資料庫,避免資料庫陷入半成品結果和半更新狀態。這對於多物件事務(請參閱 “[單物件和多物件操作](ch7.md#單物件和多物件操作)”)和維護次級索引的資料庫尤其重要。每個次級索引都是與主資料相分離的資料結構 —— 因此,如果你修改了一些資料,則還需要在次級索引中進行相應的更改。原子性確保次級索引與主資料保持一致(如果索引與主資料不一致,就沒什麼用了)。
原子性可以防止失敗的事務攪亂資料庫,避免資料庫陷入半成品結果和半更新狀態。這對於多物件事務(請參閱 “[單物件和多物件操作](/tw/ch7#單物件和多物件操作)”)和維護次級索引的資料庫尤其重要。每個次級索引都是與主資料相分離的資料結構 —— 因此,如果你修改了一些資料,則還需要在次級索引中進行相應的更改。原子性確保次級索引與主資料保持一致(如果索引與主資料不一致,就沒什麼用了)。
#### 從單節點到分散式原子提交
對於在單個數據庫節點執行的事務,原子性通常由儲存引擎實現。當客戶端請求資料庫節點提交事務時,資料庫將使事務的寫入持久化(通常在預寫式日誌中,請參閱 “[讓 B 樹更可靠](ch3.md#讓B樹更可靠)”),然後將提交記錄追加到磁碟中的日誌裡。如果資料庫在這個過程中間崩潰,當節點重啟時,事務會從日誌中恢復:如果提交記錄在崩潰之前成功地寫入磁碟,則認為事務被提交;否則來自該事務的任何寫入都被回滾。
對於在單個數據庫節點執行的事務,原子性通常由儲存引擎實現。當客戶端請求資料庫節點提交事務時,資料庫將使事務的寫入持久化(通常在預寫式日誌中,請參閱 “[讓 B 樹更可靠](/tw/ch3#讓B樹更可靠)”),然後將提交記錄追加到磁碟中的日誌裡。如果資料庫在這個過程中間崩潰,當節點重啟時,事務會從日誌中恢復:如果提交記錄在崩潰之前成功地寫入磁碟,則認為事務被提交;否則來自該事務的任何寫入都被回滾。
因此,在單個節點上,事務的提交主要取決於資料持久化落盤的 **順序**首先是資料然後是提交記錄【72】。事務提交或終止的關鍵決定時刻是磁碟完成寫入提交記錄的時刻在此之前仍有可能中止由於崩潰但在此之後事務已經提交即使資料庫崩潰。因此是單一的裝置連線到單個磁碟的控制器且掛載在單臺機器上使得提交具有原子性。
但是,如果一個事務中涉及多個節點呢?例如,你也許在分割槽資料庫中會有一個多物件事務,或者是一個按關鍵詞分割槽的次級索引(其中索引條目可能位於與主資料不同的節點上;請參閱 “[分割槽與次級索引](ch6.md#分割槽與次級索引)”)。大多數 “NoSQL” 分散式資料儲存不支援這種分散式事務,但是很多關係型資料庫叢集支援(請參閱 “[實踐中的分散式事務](#實踐中的分散式事務)”)。
但是,如果一個事務中涉及多個節點呢?例如,你也許在分割槽資料庫中會有一個多物件事務,或者是一個按關鍵詞分割槽的次級索引(其中索引條目可能位於與主資料不同的節點上;請參閱 “[分割槽與次級索引](/tw/ch6#分割槽與次級索引)”)。大多數 “NoSQL” 分散式資料儲存不支援這種分散式事務,但是很多關係型資料庫叢集支援(請參閱 “[實踐中的分散式事務](#實踐中的分散式事務)”)。
在這些情況下,僅向所有節點發送提交請求並獨立提交每個節點的事務是不夠的。這樣很容易發生違反原子性的情況:提交在某些節點上成功,而在其他節點上失敗:
@@ -586,9 +589,9 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
* 某些提交請求可能在網路中丟失,最終由於超時而中止,而其他提交請求則透過。
* 在提交記錄完全寫入之前,某些節點可能會崩潰,並在恢復時回滾,而其他節點則成功提交。
如果某些節點提交了事務,但其他節點卻放棄了這些事務,那麼這些節點就會彼此不一致(如 [圖 7-3](../img/fig7-3.png) 所示)。而且一旦在某個節點上提交了一個事務,如果事後發現它在其它節點上被中止了,它是無法撤回的。出於這個原因,一旦確定事務中的所有其他節點也將提交,節點就必須進行提交。
如果某些節點提交了事務,但其他節點卻放棄了這些事務,那麼這些節點就會彼此不一致(如 [圖 7-3](/img/fig7-3.png) 所示)。而且一旦在某個節點上提交了一個事務,如果事後發現它在其它節點上被中止了,它是無法撤回的。出於這個原因,一旦確定事務中的所有其他節點也將提交,節點就必須進行提交。
事務提交必須是不可撤銷的 —— 事務提交之後,你不能改變主意,並追溯性地中止事務。這個規則的原因是,一旦資料被提交,其結果就對其他事務可見,因此其他客戶端可能會開始依賴這些資料。這個原則構成了 **讀已提交** 隔離等級的基礎,在 “[讀已提交](ch7.md#讀已提交)” 一節中討論了這個問題。如果一個事務在提交後被允許中止,所有那些讀取了 **已提交卻又被追溯宣告不存在資料** 的事務也必須回滾。
事務提交必須是不可撤銷的 —— 事務提交之後,你不能改變主意,並追溯性地中止事務。這個規則的原因是,一旦資料被提交,其結果就對其他事務可見,因此其他客戶端可能會開始依賴這些資料。這個原則構成了 **讀已提交** 隔離等級的基礎,在 “[讀已提交](/tw/ch7#讀已提交)” 一節中討論了這個問題。如果一個事務在提交後被允許中止,所有那些讀取了 **已提交卻又被追溯宣告不存在資料** 的事務也必須回滾。
提交事務的結果有可能透過事後執行另一個補償事務compensating transaction來取消【73,74】但從資料庫的角度來看這是一個單獨的事務因此任何關於跨事務正確性的保證都是應用自己的問題。
@@ -596,15 +599,15 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
**兩階段提交two-phase commit** 是一種用於實現跨多個節點的原子事務提交的演算法即確保所有節點提交或所有節點中止。它是分散式資料庫中的經典演算法【13,35,75】。2PC 在某些資料庫內部使用,也以 **XA 事務** 的形式對應用可用【76,77】例如 Java Transaction API 支援)或以 SOAP Web 服務的 `WS-AtomicTransaction` 形式提供給應用【78,79】。
[圖 9-9](../img/fig9-9.png) 說明了 2PC 的基本流程。2PC 中的提交 / 中止過程分為兩個階段(因此而得名),而不是單節點事務中的單個提交請求。
[圖 9-9](/img/fig9-9.png) 說明了 2PC 的基本流程。2PC 中的提交 / 中止過程分為兩個階段(因此而得名),而不是單節點事務中的單個提交請求。
![](../img/fig9-9.png)
![](/img/fig9-9.png)
**圖 9-9 兩階段提交2PC的成功執行**
> #### 不要把2PC和2PL搞混了
>
> 兩階段提交2PC和兩階段鎖定請參閱 “[兩階段鎖定](ch7.md#兩階段鎖定)”是兩個完全不同的東西。2PC 在分散式資料庫中提供原子提交,而 2PL 提供可序列化的隔離等級。為了避免混淆,最好把它們看作完全獨立的概念,並忽略名稱中不幸的相似性。
> 兩階段提交2PC和兩階段鎖定請參閱 “[兩階段鎖定](/tw/ch7#兩階段鎖定)”是兩個完全不同的東西。2PC 在分散式資料庫中提供原子提交,而 2PL 提供可序列化的隔離等級。為了避免混淆,最好把它們看作完全獨立的概念,並忽略名稱中不幸的相似性。
2PC 使用一個通常不會出現在單節點事務中的新元件:**協調者**coordinator也稱為 **事務管理器**,即 transaction manager。協調者通常在請求事務的相同應用程序中以庫的形式實現例如嵌入在 Java EE 容器中),但也可以是單獨的程序或服務。這種協調者的例子包括 Narayana、JOTM、BTM 或 MSDTC。
@@ -638,9 +641,9 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
如果協調者在傳送 **準備** 請求之前失敗,參與者可以安全地中止事務。但是,一旦參與者收到了準備請求並投了 “是”,就不能再單方面放棄 —— 必須等待協調者回答事務是否已經提交或中止。如果此時協調者崩潰或網路出現故障,參與者什麼也做不了只能等待。參與者的這種事務狀態稱為 **存疑in doubt** 的或 **不確定uncertain** 的。
情況如 [圖 9-10](../img/fig9-10.png) 所示。在這個特定的例子中,協調者實際上決定提交,資料庫 2 收到提交請求。但是,協調者在將提交請求傳送到資料庫 1 之前發生崩潰,因此資料庫 1 不知道是否提交或中止。即使 **超時** 在這裡也沒有幫助:如果資料庫 1 在超時後單方面中止,它將最終與執行提交的資料庫 2 不一致。同樣,單方面提交也是不安全的,因為另一個參與者可能已經中止了。
情況如 [圖 9-10](/img/fig9-10.png) 所示。在這個特定的例子中,協調者實際上決定提交,資料庫 2 收到提交請求。但是,協調者在將提交請求傳送到資料庫 1 之前發生崩潰,因此資料庫 1 不知道是否提交或中止。即使 **超時** 在這裡也沒有幫助:如果資料庫 1 在超時後單方面中止,它將最終與執行提交的資料庫 2 不一致。同樣,單方面提交也是不安全的,因為另一個參與者可能已經中止了。
![](../img/fig9-10.png)
![](/img/fig9-10.png)
**圖 9-10 參與者投贊成票後,協調者崩潰。資料庫 1 不知道是否提交或中止**
@@ -652,7 +655,7 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
兩階段提交被稱為 **阻塞blocking**- 原子提交協議,因為存在 2PC 可能卡住並等待協調者恢復的情況。理論上,可以使一個原子提交協議變為 **非阻塞nonblocking** 的,以便在節點失敗時不會卡住。但是讓這個協議能在實踐中工作並沒有那麼簡單。
作為 2PC 的替代方案,已經提出了一種稱為 **三階段提交3PC** 的演算法【13,80】。然而3PC 假定網路延遲有界,節點響應時間有限;在大多數具有無限網路延遲和程序暫停的實際系統中(見 [第八章](ch8.md)),它並不能保證原子性。
作為 2PC 的替代方案,已經提出了一種稱為 **三階段提交3PC** 的演算法【13,80】。然而3PC 假定網路延遲有界,節點響應時間有限;在大多數具有無限網路延遲和程序暫停的實際系統中(見 [第八章](/tw/ch8)),它並不能保證原子性。
通常,非阻塞原子提交需要一個 **完美的故障檢測器perfect failure detector**【67,71】—— 即一個可靠的機制來判斷一個節點是否已經崩潰。在具有無限延遲的網路中超時並不是一種可靠的故障檢測機制因為即使沒有節點崩潰請求也可能由於網路問題而超時。出於這個原因2PC 仍然被使用,儘管大家都清楚可能存在協調者故障的問題。
@@ -683,7 +686,7 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
然而,只有當所有受事務影響的系統都使用同樣的 **原子提交協議atomic commit protocol** 時,這樣的分散式事務才是可能的。例如,假設處理訊息的副作用是傳送一封郵件,而郵件伺服器並不支援兩階段提交:如果訊息處理失敗並重試,則可能會發送兩次或更多次的郵件。但如果處理訊息的所有副作用都可以在事務中止時回滾,那麼這樣的處理流程就可以安全地重試,就好像什麼都沒有發生過一樣。
在 [第十一章](ch11.md) 中將再次回到 “恰好一次” 訊息處理的主題。讓我們先來看看允許這種異構分散式事務的原子提交協議。
在 [第十一章](/tw/ch11) 中將再次回到 “恰好一次” 訊息處理的主題。讓我們先來看看允許這種異構分散式事務的原子提交協議。
#### XA事務
@@ -701,9 +704,9 @@ XA 假定你的應用使用網路驅動或客戶端庫來與 **參與者**(資
為什麼我們這麼關心存疑事務?系統的其他部分就不能繼續正常工作,無視那些終將被清理的存疑事務嗎?
問題在於 **鎖locking**。正如在 “[讀已提交](ch7.md#讀已提交)” 中所討論的那樣,資料庫事務通常獲取待修改的行上的 **行級排他鎖**,以防止髒寫。此外,如果要使用可序列化的隔離等級,則使用兩階段鎖定的資料庫也必須為事務所讀取的行加上共享鎖(請參閱 “[兩階段鎖定](ch7.md#兩階段鎖定)”)。
問題在於 **鎖locking**。正如在 “[讀已提交](/tw/ch7#讀已提交)” 中所討論的那樣,資料庫事務通常獲取待修改的行上的 **行級排他鎖**,以防止髒寫。此外,如果要使用可序列化的隔離等級,則使用兩階段鎖定的資料庫也必須為事務所讀取的行加上共享鎖(請參閱 “[兩階段鎖定](/tw/ch7#兩階段鎖定)”)。
在事務提交或中止之前,資料庫不能釋放這些鎖(如 [圖 9-9](../img/fig9-9.png) 中的陰影區域所示)。因此,在使用兩階段提交時,事務必須在整個存疑期間持有這些鎖。如果協調者已經崩潰,需要 20 分鐘才能重啟,那麼這些鎖將會被持有 20 分鐘。如果協調者的日誌由於某種原因徹底丟失,這些鎖將被永久持有 —— 或至少在管理員手動解決該情況之前。
在事務提交或中止之前,資料庫不能釋放這些鎖(如 [圖 9-9](/img/fig9-9.png) 中的陰影區域所示)。因此,在使用兩階段提交時,事務必須在整個存疑期間持有這些鎖。如果協調者已經崩潰,需要 20 分鐘才能重啟,那麼這些鎖將會被持有 20 分鐘。如果協調者的日誌由於某種原因徹底丟失,這些鎖將被永久持有 —— 或至少在管理員手動解決該情況之前。
當這些鎖被持有時,其他事務不能修改這些行。根據資料庫的不同,其他事務甚至可能因為讀取這些行而被阻塞。因此,其他事務沒法兒簡單地繼續它們的業務了 —— 如果它們要訪問同樣的資料,就會被阻塞。這可能會導致應用大面積進入不可用狀態,直到存疑事務被解決。
@@ -723,10 +726,10 @@ XA 事務解決了保持多個參與者(資料系統)相互一致的現實
* 如果協調者沒有複製,而是隻在單臺機器上執行,那麼它是整個系統的失效單點(因為它的失效會導致其他應用伺服器阻塞在存疑事務持有的鎖上)。令人驚訝的是,許多協調者實現預設情況下並不是高可用的,或者只有基本的複製支援。
* 許多伺服器端應用都是使用無狀態模式開發的(受 HTTP 的青睞),所有持久狀態都儲存在資料庫中,因此具有應用伺服器可隨意按需新增刪除的優點。但是,當協調者成為應用伺服器的一部分時,它會改變部署的性質。突然間,協調者的日誌成為持久系統狀態的關鍵部分 —— 與資料庫本身一樣重要,因為協調者日誌是為了在崩潰後恢復存疑事務所必需的。這樣的應用伺服器不再是無狀態的了。
* 由於 XA 需要相容各種資料系統,因此它必須是所有系統的最小公分母。例如,它不能檢測不同系統間的死鎖(因為這將需要一個標準協議來讓系統交換每個事務正在等待的鎖的資訊),而且它無法與 SSI請參閱 [可序列化快照隔離](ch7.md#可序列化快照隔離))協同工作,因為這需要一個跨系統定位衝突的協議。
* 由於 XA 需要相容各種資料系統,因此它必須是所有系統的最小公分母。例如,它不能檢測不同系統間的死鎖(因為這將需要一個標準協議來讓系統交換每個事務正在等待的鎖的資訊),而且它無法與 SSI請參閱 [可序列化快照隔離](/tw/ch7#可序列化快照隔離))協同工作,因為這需要一個跨系統定位衝突的協議。
* 對於資料庫內部的分散式事務(不是 XA限制沒有這麼大 —— 例如,分散式版本的 SSI 是可能的。然而仍然存在問題2PC 成功提交一個事務需要所有參與者的響應。因此,如果系統的 **任何** 部分損壞,事務也會失敗。因此,分散式事務又有 **擴大失效amplifying failures** 的趨勢,這又與我們構建容錯系統的目標背道而馳。
這些事實是否意味著我們應該放棄保持幾個系統相互一致的所有希望?不完全是 —— 還有其他的辦法,可以讓我們在沒有異構分散式事務的痛苦的情況下實現同樣的事情。我們將在 [第十一章](ch11.md) 和 [第十二章](ch12.md) 回到這些話題。但首先,我們應該概括一下關於 **共識** 的話題。
這些事實是否意味著我們應該放棄保持幾個系統相互一致的所有希望?不完全是 —— 還有其他的辦法,可以讓我們在沒有異構分散式事務的痛苦的情況下實現同樣的事情。我們將在 [第十一章](/tw/ch11) 和 [第十二章](/tw/ch12) 回到這些話題。但首先,我們應該概括一下關於 **共識** 的話題。
### 容錯共識
@@ -759,15 +762,15 @@ XA 事務解決了保持多個參與者(資料系統)相互一致的現實
如果你不關心容錯,那麼滿足前三個屬性很容易:你可以將一個節點硬編碼為 “獨裁者”,並讓該節點做出所有的決定。但如果該節點失效,那麼系統就無法再做出任何決定。事實上,這就是我們在兩階段提交的情況中所看到的:如果協調者失效,那麼存疑的參與者就無法決定提交還是中止。
**終止** 屬性形式化了容錯的思想。它實質上說的是,一個共識演算法不能簡單地永遠閒坐著等死 —— 換句話說,它必須取得進展。即使部分節點出現故障,其他節點也必須達成一項決定(**終止** 是一種 **活性屬性**,而另外三種是 **安全屬性** —— 請參閱 “[安全性和活性](ch8.md#安全性和活性)”)。
**終止** 屬性形式化了容錯的思想。它實質上說的是,一個共識演算法不能簡單地永遠閒坐著等死 —— 換句話說,它必須取得進展。即使部分節點出現故障,其他節點也必須達成一項決定(**終止** 是一種 **活性屬性**,而另外三種是 **安全屬性** —— 請參閱 “[安全性和活性](/tw/ch8#安全性和活性)”)。
共識的系統模型假設,當一個節點 “崩潰” 時,它會突然消失而且永遠不會回來。(不像軟體崩潰,想象一下地震,包含你的節點的資料中心被山體滑坡所摧毀,你必須假設節點被埋在 30 英尺以下的泥土中,並且永遠不會重新上線)在這個系統模型中,任何需要等待節點恢復的演算法都不能滿足 **終止** 屬性。特別是2PC 不符合終止屬性的要求。
當然如果 **所有** 的節點都崩潰了,沒有一個在執行,那麼所有演算法都不可能決定任何事情。演算法可以容忍的失效數量是有限的:事實上可以證明,任何共識演算法都需要至少佔總體 **多數majority** 的節點正確工作以確保終止屬性【67】。多數可以安全地組成法定人數請參閱 “[讀寫的法定人數](ch5.md#讀寫的法定人數)”)。
當然如果 **所有** 的節點都崩潰了,沒有一個在執行,那麼所有演算法都不可能決定任何事情。演算法可以容忍的失效數量是有限的:事實上可以證明,任何共識演算法都需要至少佔總體 **多數majority** 的節點正確工作以確保終止屬性【67】。多數可以安全地組成法定人數請參閱 “[讀寫的法定人數](/tw/ch5#讀寫的法定人數)”)。
因此 **終止** 屬性取決於一個假設,**不超過一半的節點崩潰或不可達**。然而即使多數節點出現故障或存在嚴重的網路問題,絕大多數共識的實現都能始終確保安全屬性得到滿足 —— 一致同意完整性和有效性【92】。因此大規模的中斷可能會阻止系統處理請求但是它不能透過使系統做出無效的決定來破壞共識系統。
大多數共識演算法假設不存在 **拜占庭式錯誤**,正如在 “[拜占庭故障](ch8.md#拜占庭故障)” 一節中所討論的那樣。也就是說如果一個節點沒有正確地遵循協議例如如果它向不同節點發送矛盾的訊息它就可能會破壞協議的安全屬性。克服拜占庭故障穩健地達成共識是可能的只要少於三分之一的節點存在拜占庭故障【25,93】。但我們沒有地方在本書中詳細討論這些演算法了。
大多數共識演算法假設不存在 **拜占庭式錯誤**,正如在 “[拜占庭故障](/tw/ch8#拜占庭故障)” 一節中所討論的那樣。也就是說如果一個節點沒有正確地遵循協議例如如果它向不同節點發送矛盾的訊息它就可能會破壞協議的安全屬性。克服拜占庭故障穩健地達成共識是可能的只要少於三分之一的節點存在拜占庭故障【25,93】。但我們沒有地方在本書中詳細討論這些演算法了。
#### 共識演算法和全序廣播
@@ -788,11 +791,11 @@ XA 事務解決了保持多個參與者(資料系統)相互一致的現實
#### 單主複製與共識
在 [第五章](ch5.md) 中,我們討論了單主複製(請參閱 “[領導者與追隨者](ch5.md#領導者與追隨者)”),它將所有的寫入操作都交給主庫,並以相同的順序將它們應用到從庫,從而使副本保持在最新狀態。這實際上不就是一個全序廣播嗎?為什麼我們在 [第五章](ch5.md) 裡一點都沒擔心過共識問題呢?
在 [第五章](/tw/ch5) 中,我們討論了單主複製(請參閱 “[領導者與追隨者](/tw/ch5#領導者與追隨者)”),它將所有的寫入操作都交給主庫,並以相同的順序將它們應用到從庫,從而使副本保持在最新狀態。這實際上不就是一個全序廣播嗎?為什麼我們在 [第五章](/tw/ch5) 裡一點都沒擔心過共識問題呢?
答案取決於如何選擇領導者。如果主庫是由運維人員手動選擇和配置的,那麼你實際上擁有一種 **獨裁型別** 的 “共識演算法”:只有一個節點被允許接受寫入(即決定寫入複製日誌的順序),如果該節點發生故障,則系統將無法寫入,直到運維手動配置其他節點作為主庫。這樣的系統在實踐中可以表現良好,但它無法滿足共識的 **終止** 屬性,因為它需要人為干預才能取得 **進展**
一些資料庫會自動執行領導者選舉和故障切換,如果舊主庫失效,會提拔一個從庫為新主庫(請參閱 “[處理節點宕機](ch5.md#處理節點宕機)”)。這使我們向容錯的全序廣播更進一步,從而達成共識。
一些資料庫會自動執行領導者選舉和故障切換,如果舊主庫失效,會提拔一個從庫為新主庫(請參閱 “[處理節點宕機](/tw/ch5#處理節點宕機)”)。這使我們向容錯的全序廣播更進一步,從而達成共識。
但是還有一個問題。我們之前曾經討論過腦裂的問題,並且說過所有的節點都需要同意是誰領導,否則兩個不同的節點都會認為自己是領導者,從而導致資料庫進入不一致的狀態。因此,選出一位領導者需要共識。但如果這裡描述的共識演算法實際上是全序廣播演算法,並且全序廣播就像單主複製,而單主複製需要一個領導者,那麼...
@@ -804,9 +807,9 @@ XA 事務解決了保持多個參與者(資料系統)相互一致的現實
每次當現任領導被認為掛掉的時候,節點間就會開始一場投票,以選出一個新領導。這次選舉被賦予一個遞增的紀元編號,因此紀元編號是全序且單調遞增的。如果兩個不同的時代的領導者之間出現衝突(也許是因為前任領導者實際上並未死亡),那麼帶有更高紀元編號的領導說了算。
在任何領導者被允許決定任何事情之前,必須先檢查是否存在其他帶有更高紀元編號的領導者,它們可能會做出相互衝突的決定。領導者如何知道自己沒有被另一個節點趕下臺?回想一下在 “[真相由多數所定義](ch8.md#真相由多數所定義)” 中提到的:一個節點不一定能相信自己的判斷 —— 因為只有節點自己認為自己是領導者,並不一定意味著其他節點接受它作為它們的領導者。
在任何領導者被允許決定任何事情之前,必須先檢查是否存在其他帶有更高紀元編號的領導者,它們可能會做出相互衝突的決定。領導者如何知道自己沒有被另一個節點趕下臺?回想一下在 “[真相由多數所定義](/tw/ch8#真相由多數所定義)” 中提到的:一個節點不一定能相信自己的判斷 —— 因為只有節點自己認為自己是領導者,並不一定意味著其他節點接受它作為它們的領導者。
相反,它必須從 **法定人數quorum** 的節點中獲取選票(請參閱 “[讀寫的法定人數](ch5.md#讀寫的法定人數)”。對領導者想要做出的每一個決定都必須將提議值傳送給其他節點並等待法定人數的節點響應並贊成提案。法定人數通常但不總是由多數節點組成【105】。只有在沒有意識到任何帶有更高紀元編號的領導者的情況下一個節點才會投票贊成提議。
相反,它必須從 **法定人數quorum** 的節點中獲取選票(請參閱 “[讀寫的法定人數](/tw/ch5#讀寫的法定人數)”。對領導者想要做出的每一個決定都必須將提議值傳送給其他節點並等待法定人數的節點響應並贊成提案。法定人數通常但不總是由多數節點組成【105】。只有在沒有意識到任何帶有更高紀元編號的領導者的情況下一個節點才會投票贊成提議。
因此,我們有兩輪投票:第一次是為了選出一位領導者,第二次是對領導者的提議進行表決。關鍵的洞察在於,這兩次投票的 **法定人群** 必須相互 **重疊overlap**如果一個提案的表決透過則至少得有一個參與投票的節點也必須參加過最近的領導者選舉【105】。因此如果在一個提案的表決過程中沒有出現更高的紀元編號。那麼現任領導者就可以得出這樣的結論沒有發生過更高時代的領導選舉因此可以確定自己仍然在領導。然後它就可以安全地對提議值做出決定。
@@ -818,7 +821,7 @@ XA 事務解決了保持多個參與者(資料系統)相互一致的現實
儘管如此,它們並不是在所有地方都用上了,因為好處總是有代價的。
節點在做出決定之前對提議進行投票的過程是一種同步複製。如 “[同步複製與非同步複製](ch5.md#同步複製與非同步複製)” 中所述,通常資料庫會配置為非同步複製模式。在這種配置中發生故障切換時,一些已經提交的資料可能會丟失 —— 但是為了獲得更好的效能,許多人選擇接受這種風險。
節點在做出決定之前對提議進行投票的過程是一種同步複製。如 “[同步複製與非同步複製](/tw/ch5#同步複製與非同步複製)” 中所述,通常資料庫會配置為非同步複製模式。在這種配置中發生故障切換時,一些已經提交的資料可能會丟失 —— 但是為了獲得更好的效能,許多人選擇接受這種風險。
共識系統總是需要嚴格多數來運轉。這意味著你至少需要三個節點才能容忍單節點故障(其餘兩個構成多數),或者至少有五個節點來容忍兩個節點發生故障(其餘三個構成多數)。如果網路故障切斷了某些節點同其他節點的連線,則只有多數節點所在的網路可以繼續工作,其餘部分將被阻塞(請參閱 “[線性一致性的代價](#線性一致性的代價)”)。
@@ -840,11 +843,11 @@ ZooKeeper 模仿了 Google 的 Chubby 鎖服務【14,98】不僅實現了全
* 線性一致性的原子操作
使用原子 CAS 操作可以實現鎖:如果多個節點同時嘗試執行相同的操作,只有一個節點會成功。共識協議保證了操作的原子性和線性一致性,即使節點發生故障或網路在任意時刻中斷。分散式鎖通常以 **租約lease** 的形式實現,租約有一個到期時間,以便在客戶端失效的情況下最終能被釋放(請參閱 “[程序暫停](ch8.md#程序暫停)”)。
使用原子 CAS 操作可以實現鎖:如果多個節點同時嘗試執行相同的操作,只有一個節點會成功。共識協議保證了操作的原子性和線性一致性,即使節點發生故障或網路在任意時刻中斷。分散式鎖通常以 **租約lease** 的形式實現,租約有一個到期時間,以便在客戶端失效的情況下最終能被釋放(請參閱 “[程序暫停](/tw/ch8#程序暫停)”)。
* 操作的全序排序
如 “[領導者和鎖](ch8.md#領導者和鎖)” 中所述當某個資源受到鎖或租約的保護時你需要一個防護令牌來防止客戶端在程序暫停的情況下彼此衝突。防護令牌是每次鎖被獲取時單調增加的數字。ZooKeeper 透過全序化所有操作來提供這個功能,它為每個操作提供一個單調遞增的事務 ID`zxid`)和版本號(`cversion`【15】。
如 “[領導者和鎖](/tw/ch8#領導者和鎖)” 中所述當某個資源受到鎖或租約的保護時你需要一個防護令牌來防止客戶端在程序暫停的情況下彼此衝突。防護令牌是每次鎖被獲取時單調增加的數字。ZooKeeper 透過全序化所有操作來提供這個功能,它為每個操作提供一個單調遞增的事務 ID`zxid`)和版本號(`cversion`【15】。
* 失效檢測
@@ -860,7 +863,7 @@ ZooKeeper 模仿了 Google 的 Chubby 鎖服務【14,98】不僅實現了全
ZooKeeper/Chubby 模型執行良好的一個例子是,如果你有幾個程序例項或服務,需要選擇其中一個例項作為主庫或首選服務。如果領導者失敗,其他節點之一應該接管。這對單主資料庫當然非常實用,但對作業排程程式和類似的有狀態系統也很好用。
另一個例子是,當你有一些分割槽資源(資料庫、訊息流、檔案儲存、分散式 Actor 系統等),並需要決定將哪個分割槽分配給哪個節點時。當新節點加入叢集時,需要將某些分割槽從現有節點移動到新節點,以便重新平衡負載(請參閱 “[分割槽再平衡](ch6.md#分割槽再平衡)”)。當節點被移除或失效時,其他節點需要接管失效節點的工作。
另一個例子是,當你有一些分割槽資源(資料庫、訊息流、檔案儲存、分散式 Actor 系統等),並需要決定將哪個分割槽分配給哪個節點時。當新節點加入叢集時,需要將某些分割槽從現有節點移動到新節點,以便重新平衡負載(請參閱 “[分割槽再平衡](/tw/ch6#分割槽再平衡)”)。當節點被移除或失效時,其他節點需要接管失效節點的工作。
這類任務可以透過在 ZooKeeper 中明智地使用原子操作,臨時節點與通知來實現。如果設計得當,這種方法允許應用自動從故障中恢復而無需人工干預。不過這並不容易,儘管已經有不少在 ZooKeeper 客戶端 API 基礎之上提供更高層工具的庫,例如 Apache Curator 【17】。但它仍然要比嘗試從頭實現必要的共識演算法要好得多這樣的嘗試鮮有成功記錄【107】。
@@ -880,7 +883,7 @@ ZooKeeper、etcd 和 Consul 也經常用於服務發現 —— 也就是找出
ZooKeeper 和它的小夥伴們可以看作是成員資格服務membership services研究的悠久歷史的一部分這個歷史可以追溯到 20 世紀 80 年代並且對建立高度可靠的系統例如空中交通管制非常重要【110】。
成員資格服務確定哪些節點當前處於活動狀態並且是叢集的活動成員。正如我們在 [第八章](ch8.md) 中看到的那樣,由於無限的網路延遲,無法可靠地檢測到另一個節點是否發生故障。但是,如果你透過共識來進行故障檢測,那麼節點可以就哪些節點應該被認為是存在或不存在達成一致。
成員資格服務確定哪些節點當前處於活動狀態並且是叢集的活動成員。正如我們在 [第八章](/tw/ch8) 中看到的那樣,由於無限的網路延遲,無法可靠地檢測到另一個節點是否發生故障。但是,如果你透過共識來進行故障檢測,那麼節點可以就哪些節點應該被認為是存在或不存在達成一致。
即使它確實存在,仍然可能發生一個節點被共識錯誤地宣告死亡。但是對於一個系統來說,知道哪些節點構成了當前的成員關係是非常有用的。例如,選擇領導者可能意味著簡單地選擇當前成員中編號最小的成員,但如果不同的節點對現有的成員都有誰有不同意見,則這種方法將不起作用。
@@ -929,13 +932,13 @@ ZooKeeper 和它的小夥伴們可以看作是成員資格服務membership se
儘管單領導者資料庫可以提供線性一致性,且無需對每個寫操作都執行共識演算法,但共識對於保持及變更領導權仍然是必須的。因此從某種意義上說,使用單個領導者不過是 “緩兵之計”:共識仍然是需要的,只是在另一個地方,而且沒那麼頻繁。好訊息是,容錯的共識演算法與容錯的共識系統是存在的,我們在本章中簡要地討論了它們。
像 ZooKeeper 這樣的工具為應用提供了 “外包” 的共識、故障檢測和成員服務。它們扮演了重要的角色,雖說使用不易,但總比自己去開發一個能經受 [第八章](ch8.md) 中所有問題考驗的演算法要好得多。如果你發現自己想要解決的問題可以歸結為共識,並且希望它能容錯,使用一個類似 ZooKeeper 的東西是明智之舉。
像 ZooKeeper 這樣的工具為應用提供了 “外包” 的共識、故障檢測和成員服務。它們扮演了重要的角色,雖說使用不易,但總比自己去開發一個能經受 [第八章](/tw/ch8) 中所有問題考驗的演算法要好得多。如果你發現自己想要解決的問題可以歸結為共識,並且希望它能容錯,使用一個類似 ZooKeeper 的東西是明智之舉。
儘管如此,並不是所有系統都需要共識:例如,無領導者複製和多領導者複製系統通常不會使用全域性的共識。這些系統中出現的衝突(請參閱 “[處理寫入衝突](ch5.md#處理寫入衝突)”)正是不同領導者之間沒有達成共識的結果,但這也許並沒有關係:也許我們只是需要接受沒有線性一致性的事實,並學會更好地與具有分支與合併版本歷史的資料打交道。
儘管如此,並不是所有系統都需要共識:例如,無領導者複製和多領導者複製系統通常不會使用全域性的共識。這些系統中出現的衝突(請參閱 “[處理寫入衝突](/tw/ch5#處理寫入衝突)”)正是不同領導者之間沒有達成共識的結果,但這也許並沒有關係:也許我們只是需要接受沒有線性一致性的事實,並學會更好地與具有分支與合併版本歷史的資料打交道。
本章引用了大量關於分散式系統理論的研究。雖然理論論文和證明並不總是容易理解,有時也會做出不切實際的假設,但它們對於指導這一領域的實踐有著極其重要的價值:它們幫助我們推理什麼可以做,什麼不可以做,幫助我們找到反直覺的分散式系統缺陷。如果你有時間,這些參考資料值得探索。
這裡已經到了本書 [第二部分](part-ii.md) 的末尾,第二部介紹了複製([第五章](ch5.md))、分割槽([第六章](ch6.md))、事務([第七章](ch7.md))、分散式系統的故障模型([第八章](ch8.md))以及最後的一致性與共識([第九章](ch9.md))。現在我們已經奠定了紮實的理論基礎,我們將在 [第三部分](part-iii.md) 再次轉向更實際的系統,並討論如何使用異構的元件積木塊構建強大的應用。
這裡已經到了本書 [第二部分](/tw/part-ii) 的末尾,第二部介紹了複製([第五章](/tw/ch5))、分割槽([第六章](/tw/ch6))、事務([第七章](/tw/ch7))、分散式系統的故障模型([第八章](/tw/ch8))以及最後的一致性與共識([第九章](/tw/ch9))。現在我們已經奠定了紮實的理論基礎,我們將在 [第三部分](/tw/part-iii) 再次轉向更實際的系統,並討論如何使用異構的元件積木塊構建強大的應用。
## 參考文獻
@@ -1049,11 +1052,4 @@ ZooKeeper 和它的小夥伴們可以看作是成員資格服務membership se
1. Kyle Kingsbury: “[Call Me Maybe: Elasticsearch 1.5.0](https://aphyr.com/posts/323-call-me-maybe-elasticsearch-1-5-0),” *aphyr.com*, April 27, 2015.
1. Ivan Kelly: “[BookKeeper Tutorial](https://github.com/ivankelly/bookkeeper-tutorial),” *github.com*, October 2014.
1. Camille Fournier: “[Consensus Systems for the Skeptical Architect](https://vimeo.com/102667163),” at *Philly ETE*, Philadelphia, PA, USA, April 2014.
1. Kenneth P. Birman: “[A History of the Virtual Synchrony Replication Model](https://ptolemy.berkeley.edu/projects/truststc/pubs/713/History%20of%20the%20Virtual%20Synchrony%20Replication%20Model%202010.pdf),” in *Replication: Theory and Practice*, Springer LNCS volume 5959, chapter 6, pages 91120, 2010. ISBN: 978-3-642-11293-5, [doi:10.1007/978-3-642-11294-2_6](http://dx.doi.org/10.1007/978-3-642-11294-2_6)
------
| 上一章 | 目錄 | 下一章 |
| ---------------------------------- | ------------------------------- | --------------------------------- |
| [第八章:分散式系統的麻煩](ch8.md) | [設計資料密集型應用](README.md) | [第三部分:衍生資料](part-iii.md) |
1. Kenneth P. Birman: “[A History of the Virtual Synchrony Replication Model](https://ptolemy.berkeley.edu/projects/truststc/pubs/713/History%20of%20the%20Virtual%20Synchrony%20Replication%20Model%202010.pdf),” in *Replication: Theory and Practice*, Springer LNCS volume 5959, chapter 6, pages 91120, 2010. ISBN: 978-3-642-11293-5, [doi:10.1007/978-3-642-11294-2_6](http://dx.doi.org/10.1007/978-3-642-11294-2_6)

View File

@@ -1,4 +1,8 @@
# 後記
---
title: 後記
weight: 600
breadcrumbs: false
---
## 關於作者
@@ -11,11 +15,13 @@ Martin 是一位常規會議演講者,博主和開源貢獻者。他認為,
## 關於譯者
[馮若航](https://vonng.com/about)
[**馮若航**](https://vonng.com),網名 [@Vonng](https://github.com/Vonng)
PostgreSQL 專家,資料庫老司機,雲計算泥石流。
PostgreSQL 發行版 [**Pigsty**](https://pgsty.com) 作者與創始人。
架構師DBA全棧工程師 @ TanTanAlibabaApple。
獨立開源貢獻者,[GitStar Ranking 585](https://gitstar-ranking.com/Vonng)[國區活躍 Top20](https://committers.top/china)。
[DDIA](https://ddia.pigsty.io) / [PG Internal](https://pgint.vonng.com) 中文版譯者,資料庫/雲計算 KOL。
PostgreSQL DBA @ TanTan
Alibaba+-Finplus 架構師/全棧工程師 (2015 ~ 2017)
## 後記

231
content/tw/contrib.md Normal file
View File

@@ -0,0 +1,231 @@
---
title: 貢獻者
weight: 800
breadcrumbs: false
---
## 譯者
[**馮若航**](https://vonng.com),網名 [@Vonng](https://github.com/Vonng)。
PostgreSQL 專家,資料庫老司機,雲計算泥石流。
[**Pigsty**](https://pgsty.com) 作者與創始人。
架構師DBA全棧工程師 @ TanTanAlibabaApple。
獨立開源貢獻者,[GitStar Ranking 585](https://gitstar-ranking.com/Vonng)[國區活躍 Top20](https://committers.top/china)。
[DDIA](https://ddia.pigsty.io) / [PG Internal](https://pgint.vonng.com) 中文版譯者,公眾號:《老馮雲數》,資料庫 KOL。
## 校訂與維護
YinGang [@yingang](https://github.com/yingang) 對本書進行了全文校訂,並持續維護。
## 繁體中文版本
[繁體中文](/tw) **版本維護** by [@afunTW](https://github.com/afunTW)
## 貢獻列表
[GitHub 貢獻者列表](https://github.com/Vonng/ddia/graphs/contributors)
0. 全文校訂 by [@yingang](https://github.com/Vonng/ddia/commits?author=yingang)
1. [序言初翻修正](https://github.com/Vonng/ddia/commit/afb5edab55c62ed23474149f229677e3b42dfc2c) by [@seagullbird](https://github.com/Vonng/ddia/commits?author=seagullbird)
2. [第一章語法標點校正](https://github.com/Vonng/ddia/commit/973b12cd8f8fcdf4852f1eb1649ddd9d187e3644) by [@nevertiree](https://github.com/Vonng/ddia/commits?author=nevertiree)
3. [第六章部分校正](https://github.com/Vonng/ddia/commit/d4eb0852c0ec1e93c8aacc496c80b915bb1e6d48) 與[第十章的初翻](https://github.com/Vonng/ddia/commit/9de8dbd1bfe6fbb03b3bf6c1a1aa2291aed2490e) by [@MuAlex](https://github.com/Vonng/ddia/commits?author=MuAlex)
4. [第一部分](/tw/part-i)前言,[ch2](/tw/ch2)校正 by [@jiajiadebug](https://github.com/Vonng/ddia/commits?author=jiajiadebug)
5. [詞彙表](/tw/glossary)、[後記](/tw/colophon)關於野豬的部分 by [@Chowss](https://github.com/Vonng/ddia/commits?author=Chowss)
6. [繁體中文](https://github.com/Vonng/ddia/pulls)版本與轉換指令碼 by [@afunTW](https://github.com/afunTW)
7. 多處翻譯修正 by [@songzhibin97](https://github.com/Vonng/ddia/commits?author=songzhibin97) [@MamaShip](https://github.com/Vonng/ddia/commits?author=MamaShip) [@FangYuan33](https://github.com/Vonng/ddia/commits?author=FangYuan33)
感謝所有提出意見,作出貢獻的朋友們,您可以在這裡找到所有貢獻的 [Issue 列表](https://github.com/Vonng/ddia/issues) 與 [PR 列表](https://github.com/Vonng/ddia/pulls)
| ISSUE & Pull Requests | USER | Title |
|-------------------------------------------------|------------------------------------------------------------|----------------------------------------------------------------|
| [359](https://github.com/Vonng/ddia/pull/359) | [@c25423](https://github.com/c25423) | ch10: 修正一處拼寫錯誤 |
| [358](https://github.com/Vonng/ddia/pull/358) | [@lewiszlw](https://github.com/lewiszlw) | ch4: 修正一處拼寫錯誤 |
| [356](https://github.com/Vonng/ddia/pull/356) | [@lewiszlw](https://github.com/lewiszlw) | ch2: 修正一處標點錯誤 |
| [355](https://github.com/Vonng/ddia/pull/355) | [@DuroyGeorge](https://github.com/DuroyGeorge) | ch12: 修正一處格式錯誤 |
| [354](https://github.com/Vonng/ddia/pull/354) | [@justlorain](https://github.com/justlorain) | ch7: 修正一處參考連結 |
| [353](https://github.com/Vonng/ddia/pull/353) | [@fantasyczl](https://github.com/fantasyczl) | ch3&9: 修正兩處引用錯誤 |
| [352](https://github.com/Vonng/ddia/pull/352) | [@fantasyczl](https://github.com/fantasyczl) | 支援輸出為 EPUB 格式 |
| [349](https://github.com/Vonng/ddia/pull/349) | [@xiyihan0](https://github.com/xiyihan0) | ch1: 修正一處格式錯誤 |
| [348](https://github.com/Vonng/ddia/pull/348) | [@omegaatt36](https://github.com/omegaatt36) | ch3: 修正一處影像連結 |
| [346](https://github.com/Vonng/ddia/issues/346) | [@Vermouth1995](https://github.com/Vermouth1995) | ch1: 最佳化一處翻譯 |
| [343](https://github.com/Vonng/ddia/pull/343) | [@kehao-chen](https://github.com/kehao-chen) | ch10: 最佳化一處翻譯 |
| [341](https://github.com/Vonng/ddia/pull/341) | [@YKIsTheBest](https://github.com/YKIsTheBest) | ch3: 最佳化兩處翻譯 |
| [340](https://github.com/Vonng/ddia/pull/340) | [@YKIsTheBest](https://github.com/YKIsTheBest) | ch2: 最佳化多處翻譯 |
| [338](https://github.com/Vonng/ddia/pull/338) | [@YKIsTheBest](https://github.com/YKIsTheBest) | ch1: 最佳化一處翻譯 |
| [335](https://github.com/Vonng/ddia/pull/335) | [@kimi0230](https://github.com/kimi0230) | 修正一處繁體中文錯誤 |
| [334](https://github.com/Vonng/ddia/pull/334) | [@soulrrrrr](https://github.com/soulrrrrr) | ch2: 修正一處繁體中文錯誤 |
| [332](https://github.com/Vonng/ddia/pull/332) | [@justlorain](https://github.com/justlorain) | ch5: 修正一處翻譯錯誤 |
| [331](https://github.com/Vonng/ddia/pull/331) | [@Lyianu](https://github.com/Lyianu) | ch9: 更正幾處拼寫錯誤 |
| [330](https://github.com/Vonng/ddia/pull/330) | [@Lyianu](https://github.com/Lyianu) | ch7: 最佳化一處翻譯 |
| [329](https://github.com/Vonng/ddia/issues/329) | [@Lyianu](https://github.com/Lyianu) | ch6: 指出一處翻譯錯誤 |
| [328](https://github.com/Vonng/ddia/pull/328) | [@justlorain](https://github.com/justlorain) | ch4: 更正一處翻譯遺漏 |
| [326](https://github.com/Vonng/ddia/pull/326) | [@liangGTY](https://github.com/liangGTY) | ch1: 最佳化一處翻譯 |
| [323](https://github.com/Vonng/ddia/pull/323) | [@marvin263](https://github.com/marvin263) | ch5: 最佳化一處翻譯 |
| [322](https://github.com/Vonng/ddia/pull/322) | [@marvin263](https://github.com/marvin263) | ch8: 最佳化一處翻譯 |
| [304](https://github.com/Vonng/ddia/pull/304) | [@spike014](https://github.com/spike014) | ch11: 最佳化一處翻譯 |
| [298](https://github.com/Vonng/ddia/pull/298) | [@Makonike](https://github.com/Makonike) | ch11&12: 修正兩處錯誤 |
| [284](https://github.com/Vonng/ddia/pull/284) | [@WAangzE](https://github.com/WAangzE) | ch4: 更正一處列表錯誤 |
| [283](https://github.com/Vonng/ddia/pull/283) | [@WAangzE](https://github.com/WAangzE) | ch3: 更正一處錯別字 |
| [282](https://github.com/Vonng/ddia/pull/282) | [@WAangzE](https://github.com/WAangzE) | ch2: 更正一處公式問題 |
| [281](https://github.com/Vonng/ddia/pull/281) | [@lyuxi99](https://github.com/lyuxi99) | 更正多處內部連結錯誤 |
| [280](https://github.com/Vonng/ddia/pull/280) | [@lyuxi99](https://github.com/lyuxi99) | ch9: 更正內部連結錯誤 |
| [279](https://github.com/Vonng/ddia/issues/279) | [@codexvn](https://github.com/codexvn) | ch9: 指出公式在 GitHub Pages 顯示的問題 |
| [278](https://github.com/Vonng/ddia/pull/278) | [@LJlkdskdjflsa](https://github.com/LJlkdskdjflsa) | 發現了繁體中文版本中的錯誤翻譯 |
| [275](https://github.com/Vonng/ddia/pull/275) | [@117503445](https://github.com/117503445) | 更正 LICENSE 連結 |
| [274](https://github.com/Vonng/ddia/pull/274) | [@uncle-lv](https://github.com/uncle-lv) | ch7: 修正錯別字 |
| [273](https://github.com/Vonng/ddia/pull/273) | [@Sdot-Python](https://github.com/Sdot-Python) | ch7: 統一了 write skew 的翻譯 |
| [271](https://github.com/Vonng/ddia/pull/271) | [@Makonike](https://github.com/Makonike) | ch6: 統一了 rebalancing 的翻譯 |
| [270](https://github.com/Vonng/ddia/pull/270) | [@Ynjxsjmh](https://github.com/Ynjxsjmh) | ch7: 修正不一致的翻譯 |
| [263](https://github.com/Vonng/ddia/pull/263) | [@zydmayday](https://github.com/zydmayday) | ch5: 修正譯文中的重複單詞 |
| [260](https://github.com/Vonng/ddia/pull/260) | [@haifeiWu](https://github.com/haifeiWu) | ch4: 修正部分不準確的翻譯 |
| [258](https://github.com/Vonng/ddia/pull/258) | [@bestgrc](https://github.com/bestgrc) | ch3: 修正一處翻譯錯誤 |
| [257](https://github.com/Vonng/ddia/pull/257) | [@UnderSam](https://github.com/UnderSam) | ch8: 修正一處拼寫錯誤 |
| [256](https://github.com/Vonng/ddia/pull/256) | [@AlphaWang](https://github.com/AlphaWang) | ch7: 修正“可序列化”相關內容的多處翻譯不當 |
| [255](https://github.com/Vonng/ddia/pull/255) | [@AlphaWang](https://github.com/AlphaWang) | ch7: 修正“可重複讀”相關內容的多處翻譯不當 |
| [253](https://github.com/Vonng/ddia/pull/253) | [@AlphaWang](https://github.com/AlphaWang) | ch7: 修正“讀已提交”相關內容的多處翻譯不當 |
| [246](https://github.com/Vonng/ddia/pull/246) | [@derekwu0101](https://github.com/derekwu0101) | ch3: 修正繁體中文的轉譯錯誤 |
| [245](https://github.com/Vonng/ddia/pull/245) | [@skyran1278](https://github.com/skyran1278) | ch12: 修正繁體中文的轉譯錯誤 |
| [244](https://github.com/Vonng/ddia/pull/244) | [@Axlgrep](https://github.com/Axlgrep) | ch9: 修正不通順的翻譯 |
| [242](https://github.com/Vonng/ddia/pull/242) | [@lynkeib](https://github.com/lynkeib) | ch9: 修正不通順的翻譯 |
| [241](https://github.com/Vonng/ddia/pull/241) | [@lynkeib](https://github.com/lynkeib) | ch8: 修正不正確的公式格式 |
| [240](https://github.com/Vonng/ddia/pull/240) | [@8da2k](https://github.com/8da2k) | ch9: 修正不通順的翻譯 |
| [239](https://github.com/Vonng/ddia/pull/239) | [@BeBraveBeCurious](https://github.com/BeBraveBeCurious) | ch7: 修正不一致的翻譯 |
| [237](https://github.com/Vonng/ddia/pull/237) | [@zhangnew](https://github.com/zhangnew) | ch3: 修正錯誤的圖片連結 |
| [229](https://github.com/Vonng/ddia/pull/229) | [@lis186](https://github.com/lis186) | 指出繁體中文的轉譯錯誤:複雜 |
| [226](https://github.com/Vonng/ddia/pull/226) | [@chroming](https://github.com/chroming) | ch1: 修正導航欄中的章節名稱 |
| [220](https://github.com/Vonng/ddia/pull/220) | [@skyran1278](https://github.com/skyran1278) | ch9: 修正線性一致的繁體中文翻譯 |
| [194](https://github.com/Vonng/ddia/pull/194) | [@BeBraveBeCurious](https://github.com/BeBraveBeCurious) | ch4: 修正錯誤的翻譯 |
| [193](https://github.com/Vonng/ddia/pull/193) | [@BeBraveBeCurious](https://github.com/BeBraveBeCurious) | ch4: 最佳化譯文 |
| [192](https://github.com/Vonng/ddia/pull/192) | [@BeBraveBeCurious](https://github.com/BeBraveBeCurious) | ch4: 修正不一致和不通順的翻譯 |
| [190](https://github.com/Vonng/ddia/pull/190) | [@Pcrab](https://github.com/Pcrab) | ch1: 修正不準確的翻譯 |
| [187](https://github.com/Vonng/ddia/pull/187) | [@narojay](https://github.com/narojay) | ch9: 修正生硬的翻譯 |
| [186](https://github.com/Vonng/ddia/pull/186) | [@narojay](https://github.com/narojay) | ch8: 修正錯別字 |
| [185](https://github.com/Vonng/ddia/issues/185) | [@8da2k](https://github.com/8da2k) | 指出小標題跳轉的問題 |
| [184](https://github.com/Vonng/ddia/pull/184) | [@DavidZhiXing](https://github.com/DavidZhiXing) | ch10: 修正失效的網址 |
| [183](https://github.com/Vonng/ddia/pull/183) | [@OneSizeFitsQuorum](https://github.com/OneSizeFitsQuorum) | ch8: 修正錯別字 |
| [182](https://github.com/Vonng/ddia/issues/182) | [@lroolle](https://github.com/lroolle) | 建議docsify的主題風格 |
| [181](https://github.com/Vonng/ddia/pull/181) | [@YunfengGao](https://github.com/YunfengGao) | ch2: 修正翻譯錯誤 |
| [180](https://github.com/Vonng/ddia/pull/180) | [@skyran1278](https://github.com/skyran1278) | ch3: 指出繁體中文的轉譯錯誤 |
| [177](https://github.com/Vonng/ddia/pull/177) | [@exzhawk](https://github.com/exzhawk) | 支援 Github Pages 裡的公式顯示 |
| [176](https://github.com/Vonng/ddia/pull/176) | [@haifeiWu](https://github.com/haifeiWu) | ch2: 語義網相關翻譯更正 |
| [175](https://github.com/Vonng/ddia/pull/175) | [@cwr31](https://github.com/cwr31) | ch7: 不變式相關翻譯更正 |
| [174](https://github.com/Vonng/ddia/pull/174) | [@BeBraveBeCurious](https://github.com/BeBraveBeCurious) | README & preface: 更正不正確的中文用詞和標點符號 |
| [173](https://github.com/Vonng/ddia/pull/173) | [@ZvanYang](https://github.com/ZvanYang) | ch12: 修正不完整的翻譯 |
| [171](https://github.com/Vonng/ddia/pull/171) | [@ZvanYang](https://github.com/ZvanYang) | ch12: 修正重複的譯文 |
| [169](https://github.com/Vonng/ddia/pull/169) | [@ZvanYang](https://github.com/ZvanYang) | ch12: 更正不太通順的翻譯 |
| [166](https://github.com/Vonng/ddia/pull/166) | [@bp4m4h94](https://github.com/bp4m4h94) | ch1: 發現錯誤的文獻索引 |
| [164](https://github.com/Vonng/ddia/pull/164) | [@DragonDriver](https://github.com/DragonDriver) | preface: 更正錯誤的標點符號 |
| [163](https://github.com/Vonng/ddia/pull/163) | [@llmmddCoder](https://github.com/llmmddCoder) | ch1: 更正錯誤字 |
| [160](https://github.com/Vonng/ddia/pull/160) | [@Zhayhp](https://github.com/Zhayhp) | ch2: 建議將 network model 翻譯為網狀模型 |
| [159](https://github.com/Vonng/ddia/pull/159) | [@1ess](https://github.com/1ess) | ch4: 更正錯誤字 |
| [157](https://github.com/Vonng/ddia/pull/157) | [@ZvanYang](https://github.com/ZvanYang) | ch7: 更正不太通順的翻譯 |
| [155](https://github.com/Vonng/ddia/pull/155) | [@ZvanYang](https://github.com/ZvanYang) | ch7: 更正不太通順的翻譯 |
| [153](https://github.com/Vonng/ddia/pull/153) | [@DavidZhiXing](https://github.com/DavidZhiXing) | ch9: 修正縮圖的錯別字 |
| [152](https://github.com/Vonng/ddia/pull/152) | [@ZvanYang](https://github.com/ZvanYang) | ch7: 除重->去重 |
| [151](https://github.com/Vonng/ddia/pull/151) | [@ZvanYang](https://github.com/ZvanYang) | ch5: 修訂sibling相關的翻譯 |
| [147](https://github.com/Vonng/ddia/pull/147) | [@ZvanYang](https://github.com/ZvanYang) | ch5: 更正一處不準確的翻譯 |
| [145](https://github.com/Vonng/ddia/pull/145) | [@Hookey](https://github.com/Hookey) | 識別了當前簡繁轉譯過程中處理不當的地方,暫透過轉換指令碼規避 |
| [144](https://github.com/Vonng/ddia/issues/144) | [@secret4233](https://github.com/secret4233) | ch7: 不翻譯`next-key locking` |
| [143](https://github.com/Vonng/ddia/issues/143) | [@imcheney](https://github.com/imcheney) | ch3: 更新殘留的機翻段落 |
| [142](https://github.com/Vonng/ddia/issues/142) | [@XIJINIAN](https://github.com/XIJINIAN) | 建議去除段首的製表符 |
| [141](https://github.com/Vonng/ddia/issues/141) | [@Flyraty](https://github.com/Flyraty) | ch5: 發現一處錯誤格式的章節引用 |
| [140](https://github.com/Vonng/ddia/pull/140) | [@Bowser1704](https://github.com/Bowser1704) | ch5: 修正章節Summary中多處不通順的翻譯 |
| [139](https://github.com/Vonng/ddia/pull/139) | [@Bowser1704](https://github.com/Bowser1704) | ch2&ch3: 修正多處不通順的或錯誤的翻譯 |
| [137](https://github.com/Vonng/ddia/pull/137) | [@fuxuemingzhu](https://github.com/fuxuemingzhu) | ch5&ch6: 最佳化多處不通順的或錯誤的翻譯 |
| [134](https://github.com/Vonng/ddia/pull/134) | [@fuxuemingzhu](https://github.com/fuxuemingzhu) | ch4: 最佳化多處不通順的或錯誤的翻譯 |
| [133](https://github.com/Vonng/ddia/pull/133) | [@fuxuemingzhu](https://github.com/fuxuemingzhu) | ch3: 最佳化多處錯誤的或不通順的翻譯 |
| [132](https://github.com/Vonng/ddia/pull/132) | [@fuxuemingzhu](https://github.com/fuxuemingzhu) | ch3: 最佳化一處容易產生歧義的翻譯 |
| [131](https://github.com/Vonng/ddia/pull/131) | [@rwwg4](https://github.com/rwwg4) | ch6: 修正兩處錯誤的翻譯 |
| [129](https://github.com/Vonng/ddia/pull/129) | [@anaer](https://github.com/anaer) | ch4: 修正兩處強調文字和四處程式碼變數名稱 |
| [128](https://github.com/Vonng/ddia/pull/128) | [@meilin96](https://github.com/meilin96) | ch5: 修正一處錯誤的引用 |
| [126](https://github.com/Vonng/ddia/pull/126) | [@cwr31](https://github.com/cwr31) | ch10: 修正一處錯誤的翻譯(功能 -> 函式) |
| [125](https://github.com/Vonng/ddia/pull/125) | [@dch1228](https://github.com/dch1228) | ch2: 最佳化 how best 的翻譯(如何以最佳方式) |
| [123](https://github.com/Vonng/ddia/pull/123) | [@yingang](https://github.com/yingang) | translation updates (chapter 9, TOC in readme, glossary, etc.) |
| [121](https://github.com/Vonng/ddia/pull/121) | [@yingang](https://github.com/yingang) | translation updates (chapter 5 to chapter 8) |
| [120](https://github.com/Vonng/ddia/pull/120) | [@jiong-han](https://github.com/jiong-han) | Typo fix: 呲之以鼻 -> 嗤之以鼻 |
| [119](https://github.com/Vonng/ddia/pull/119) | [@cclauss](https://github.com/cclauss) | Streamline file operations in convert() |
| [118](https://github.com/Vonng/ddia/pull/118) | [@yingang](https://github.com/yingang) | translation updates (chapter 2 to chapter 4) |
| [117](https://github.com/Vonng/ddia/pull/117) | [@feeeei](https://github.com/feeeei) | 統一每章的標題格式 |
| [115](https://github.com/Vonng/ddia/pull/115) | [@NageNalock](https://github.com/NageNalock) | 第七章病句修改: 重複詞語 |
| [114](https://github.com/Vonng/ddia/pull/114) | [@Sunt-ing](https://github.com/Sunt-ing) | Update README.md: correct the book name |
| [113](https://github.com/Vonng/ddia/pull/113) | [@lpxxn](https://github.com/lpxxn) | 修改語句 |
| [112](https://github.com/Vonng/ddia/pull/112) | [@ibyte2011](https://github.com/ibyte2011) | Update ch9.md |
| [110](https://github.com/Vonng/ddia/pull/110) | [@lpxxn](https://github.com/lpxxn) | 讀已寫入資料 |
| [107](https://github.com/Vonng/ddia/pull/107) | [@abbychau](https://github.com/abbychau) | 單調鐘和好死還是賴活著 |
| [106](https://github.com/Vonng/ddia/pull/106) | [@enochii](https://github.com/enochii) | typo in ch2: fix braces typo |
| [105](https://github.com/Vonng/ddia/pull/105) | [@LiminCode](https://github.com/LiminCode) | Chronicle translation error |
| [104](https://github.com/Vonng/ddia/pull/104) | [@Sunt-ing](https://github.com/Sunt-ing) | several advice for better translation |
| [103](https://github.com/Vonng/ddia/pull/103) | [@Sunt-ing](https://github.com/Sunt-ing) | typo in ch4: should be 完成 rather than 完全 |
| [102](https://github.com/Vonng/ddia/pull/102) | [@Sunt-ing](https://github.com/Sunt-ing) | ch4: better-translation: 扼殺 → 破壞 |
| [101](https://github.com/Vonng/ddia/pull/101) | [@Sunt-ing](https://github.com/Sunt-ing) | typo in Ch4: should be "改變" rathr than "蓋面" |
| [100](https://github.com/Vonng/ddia/pull/100) | [@LiminCode](https://github.com/LiminCode) | fix missing translation |
| [99 ](https://github.com/Vonng/ddia/pull/99) | [@mrdrivingduck](https://github.com/mrdrivingduck) | ch6: fix the word rebalancing |
| [98 ](https://github.com/Vonng/ddia/pull/98) | [@jacklightChen](https://github.com/jacklightChen) | fix ch7.md: fix wrong references |
| [97 ](https://github.com/Vonng/ddia/pull/97) | [@jenac](https://github.com/jenac) | 96 |
| [96 ](https://github.com/Vonng/ddia/pull/96) | [@PragmaTwice](https://github.com/PragmaTwice) | ch2: fix typo about 'may or may not be' |
| [95 ](https://github.com/Vonng/ddia/pull/95) | [@EvanMu96](https://github.com/EvanMu96) | fix translation of "the battle cry" in ch5 |
| [94 ](https://github.com/Vonng/ddia/pull/94) | [@kemingy](https://github.com/kemingy) | ch6: fix markdown and punctuations |
| [93 ](https://github.com/Vonng/ddia/pull/93) | [@kemingy](https://github.com/kemingy) | ch5: fix markdown and some typos |
| [92 ](https://github.com/Vonng/ddia/pull/92) | [@Gilbert1024](https://github.com/Gilbert1024) | Merge pull request #1 from Vonng/master |
| [88 ](https://github.com/Vonng/ddia/pull/88) | [@kemingy](https://github.com/kemingy) | fix typo for ch1, ch2, ch3, ch4 |
| [87 ](https://github.com/Vonng/ddia/pull/87) | [@wynn5a](https://github.com/wynn5a) | Update ch3.md |
| [86 ](https://github.com/Vonng/ddia/pull/86) | [@northmorn](https://github.com/northmorn) | Update ch1.md |
| [85 ](https://github.com/Vonng/ddia/pull/85) | [@sunbuhui](https://github.com/sunbuhui) | fix ch2.md: fix ch2 ambiguous translation |
| [84 ](https://github.com/Vonng/ddia/pull/84) | [@ganler](https://github.com/ganler) | Fix translation: use up |
| [83 ](https://github.com/Vonng/ddia/pull/83) | [@afunTW](https://github.com/afunTW) | Using OpenCC to convert from zh-cn to zh-tw |
| [82 ](https://github.com/Vonng/ddia/pull/82) | [@kangni](https://github.com/kangni) | fix gitbook url |
| [78 ](https://github.com/Vonng/ddia/pull/78) | [@hanyu2](https://github.com/hanyu2) | Fix unappropriated translation |
| [77 ](https://github.com/Vonng/ddia/pull/77) | [@Ozarklake](https://github.com/Ozarklake) | fix typo |
| [75 ](https://github.com/Vonng/ddia/pull/75) | [@2997ms](https://github.com/2997ms) | Fix typo |
| [74 ](https://github.com/Vonng/ddia/pull/74) | [@2997ms](https://github.com/2997ms) | Update ch9.md |
| [70 ](https://github.com/Vonng/ddia/pull/70) | [@2997ms](https://github.com/2997ms) | Update ch7.md |
| [67 ](https://github.com/Vonng/ddia/pull/67) | [@jiajiadebug](https://github.com/jiajiadebug) | fix issues in ch2 - ch9 and glossary |
| [66 ](https://github.com/Vonng/ddia/pull/66) | [@blindpirate](https://github.com/blindpirate) | Fix typo |
| [63 ](https://github.com/Vonng/ddia/pull/63) | [@haifeiWu](https://github.com/haifeiWu) | Update ch10.md |
| [62 ](https://github.com/Vonng/ddia/pull/62) | [@ych](https://github.com/ych) | fix ch1.md typesetting problem |
| [61 ](https://github.com/Vonng/ddia/pull/61) | [@xianlaioy](https://github.com/xianlaioy) | docs:鍾-->種去掉ou |
| [60 ](https://github.com/Vonng/ddia/pull/60) | [@Zombo1296](https://github.com/Zombo1296) | 否則 -> 或者 |
| [59 ](https://github.com/Vonng/ddia/pull/59) | [@AlexanderMisel](https://github.com/AlexanderMisel) | 呼叫->呼叫,顯著->顯著 |
| [58 ](https://github.com/Vonng/ddia/pull/58) | [@ibyte2011](https://github.com/ibyte2011) | Update ch8.md |
| [55 ](https://github.com/Vonng/ddia/pull/55) | [@saintube](https://github.com/saintube) | ch8: 修改連結錯誤 |
| [54 ](https://github.com/Vonng/ddia/pull/54) | [@Panmax](https://github.com/Panmax) | Update ch2.md |
| [53 ](https://github.com/Vonng/ddia/pull/53) | [@ibyte2011](https://github.com/ibyte2011) | Update ch9.md |
| [52 ](https://github.com/Vonng/ddia/pull/52) | [@hecenjie](https://github.com/hecenjie) | Update ch1.md |
| [51 ](https://github.com/Vonng/ddia/pull/51) | [@latavin243](https://github.com/latavin243) | fix 修正ch3 ch4幾處翻譯 |
| [50 ](https://github.com/Vonng/ddia/pull/50) | [@AlexZFX](https://github.com/AlexZFX) | 幾個疏漏和格式錯誤 |
| [49 ](https://github.com/Vonng/ddia/pull/49) | [@haifeiWu](https://github.com/haifeiWu) | Update ch1.md |
| [48 ](https://github.com/Vonng/ddia/pull/48) | [@scaugrated](https://github.com/scaugrated) | fix typo |
| [47 ](https://github.com/Vonng/ddia/pull/47) | [@lzwill](https://github.com/lzwill) | Fixed typos in ch2 |
| [45 ](https://github.com/Vonng/ddia/pull/45) | [@zenuo](https://github.com/zenuo) | 刪除一個多餘的右括號 |
| [44 ](https://github.com/Vonng/ddia/pull/44) | [@akxxsb](https://github.com/akxxsb) | 修正第七章底部連結錯誤 |
| [43 ](https://github.com/Vonng/ddia/pull/43) | [@baijinping](https://github.com/baijinping) | "更假簡單"->"更加簡單" |
| [42 ](https://github.com/Vonng/ddia/pull/42) | [@tisonkun](https://github.com/tisonkun) | 修復 ch1 中的無序列表格式 |
| [38 ](https://github.com/Vonng/ddia/pull/38) | [@renjie-c](https://github.com/renjie-c) | 糾正多處的翻譯小錯誤 |
| [37 ](https://github.com/Vonng/ddia/pull/37) | [@tankilo](https://github.com/tankilo) | fix translation mistakes in ch4.md |
| [36 ](https://github.com/Vonng/ddia/pull/36) | [@wwek](https://github.com/wwek) | 1.修復多個連結錯誤 2.名詞最佳化修訂 3.錯誤修訂 |
| [35 ](https://github.com/Vonng/ddia/pull/35) | [@wwek](https://github.com/wwek) | fix ch7.md to ch8.md link error |
| [34 ](https://github.com/Vonng/ddia/pull/34) | [@wwek](https://github.com/wwek) | Merge pull request #1 from Vonng/master |
| [33 ](https://github.com/Vonng/ddia/pull/33) | [@wwek](https://github.com/wwek) | fix part-ii.md link error |
| [32 ](https://github.com/Vonng/ddia/pull/32) | [@JCYoky](https://github.com/JCYoky) | Update ch2.md |
| [31 ](https://github.com/Vonng/ddia/pull/31) | [@elsonLee](https://github.com/elsonLee) | Update ch7.md |
| [26 ](https://github.com/Vonng/ddia/pull/26) | [@yjhmelody](https://github.com/yjhmelody) | 修復一些明顯錯誤 |
| [25 ](https://github.com/Vonng/ddia/pull/25) | [@lqbilbo](https://github.com/lqbilbo) | 修復連結錯誤 |
| [24 ](https://github.com/Vonng/ddia/pull/24) | [@artiship](https://github.com/artiship) | 修改詞語順序 |
| [23 ](https://github.com/Vonng/ddia/pull/23) | [@artiship](https://github.com/artiship) | 修正錯別字 |
| [22 ](https://github.com/Vonng/ddia/pull/22) | [@artiship](https://github.com/artiship) | 糾正翻譯錯誤 |
| [21 ](https://github.com/Vonng/ddia/pull/21) | [@zhtisi](https://github.com/zhtisi) | 修正目錄和本章標題不符的情況 |
| [20 ](https://github.com/Vonng/ddia/pull/20) | [@rentiansheng](https://github.com/rentiansheng) | Update ch7.md |
| [19 ](https://github.com/Vonng/ddia/pull/19) | [@LHRchina](https://github.com/LHRchina) | 修復語句小bug |
| [16 ](https://github.com/Vonng/ddia/pull/16) | [@MuAlex](https://github.com/MuAlex) | Master |
| [15 ](https://github.com/Vonng/ddia/pull/15) | [@cg-zhou](https://github.com/cg-zhou) | Update translation progress |
| [14 ](https://github.com/Vonng/ddia/pull/14) | [@cg-zhou](https://github.com/cg-zhou) | Translate glossary |
| [13 ](https://github.com/Vonng/ddia/pull/13) | [@cg-zhou](https://github.com/cg-zhou) | 詳細修改了後記中和印度野豬相關的描述 |
| [12 ](https://github.com/Vonng/ddia/pull/12) | [@ibyte2011](https://github.com/ibyte2011) | 修改了部分翻譯 |
| [11 ](https://github.com/Vonng/ddia/pull/11) | [@jiajiadebug](https://github.com/jiajiadebug) | ch2 100% |
| [10 ](https://github.com/Vonng/ddia/pull/10) | [@jiajiadebug](https://github.com/jiajiadebug) | ch2 20% |
| [9 ](https://github.com/Vonng/ddia/pull/9) | [@jiajiadebug](https://github.com/jiajiadebug) | Preface, ch1, part-i translation minor fixes |
| [7 ](https://github.com/Vonng/ddia/pull/7) | [@MuAlex](https://github.com/MuAlex) | Ch6 translation pull request |
| [6 ](https://github.com/Vonng/ddia/pull/6) | [@MuAlex](https://github.com/MuAlex) | Ch6 change version1 |
| [5 ](https://github.com/Vonng/ddia/pull/5) | [@nevertiree](https://github.com/nevertiree) | Chapter 01語法微調 |
| [2 ](https://github.com/Vonng/ddia/pull/2) | [@seagullbird](https://github.com/seagullbird) | 序言初翻 |

View File

@@ -1,252 +1,256 @@
# 術語表
---
title: 術語表
weight: 500
breadcrumbs: false
---
> 請注意,本術語表中的定義簡短而簡單,旨在傳達核心思想,而不是術語的完整細微之處。有關更多詳細資訊,請參閱正文中的參考資料。
> 請注意,本術語表中的定義簡短而簡單,旨在傳達核心思想,而非死扣完整細節。有關更多詳細資訊,請參閱正文中的參考資料。
* **非同步asynchronous**
## **非同步asynchronous**
不等待某些事情完成(例如,將資料傳送到網路中的另一個節點),並且不會假設要花多長時間。請參閱“[同步複製與非同步複製](ch5.md#同步複製與非同步複製)”、“[同步網路與非同步網路](ch8.md#同步網路與非同步網路)”以及“[系統模型與現實](ch8.md#系統模型與現實)”。
不等待某些事情完成(例如,將資料傳送到網路中的另一個節點),並且不會假設要花多長時間。請參閱“[同步複製與非同步複製](/tw/ch5#同步複製與非同步複製)”、“[同步網路與非同步網路](/tw/ch8#同步網路與非同步網路)”以及“[系統模型與現實](/tw/ch8#系統模型與現實)”。
* **原子atomic**
## **原子atomic**
在併發操作的上下文中:描述一個在單個時間點看起來生效的操作,所以另一個併發程序永遠不會遇到處於“半完成”狀態的操作。另見隔離。
在事務的上下文中:將一些寫入操作分為一組,這組寫入要麼全部提交成功,要麼遇到錯誤時全部回滾。請參閱“[原子性](ch7.md#原子性)”和“[原子提交與兩階段提交](ch9.md#原子提交與兩階段提交)”。
在事務的上下文中:將一些寫入操作分為一組,這組寫入要麼全部提交成功,要麼遇到錯誤時全部回滾。請參閱“[原子性](/tw/ch7#原子性)”和“[原子提交與兩階段提交](/tw/ch9#原子提交與兩階段提交)”。
* **背壓backpressure**
## **背壓backpressure**
接收方接收資料速度較慢時,強制降低傳送方的資料傳送速度。也稱為流量控制。請參閱“[訊息傳遞系統](ch11.md#訊息傳遞系統)”。
接收方接收資料速度較慢時,強制降低傳送方的資料傳送速度。也稱為流量控制。請參閱“[訊息傳遞系統](/tw/ch11#訊息傳遞系統)”。
* **批處理batch process**
## **批處理batch process**
一種計算,它將一些固定的(通常是大的)資料集作為輸入,並將其他一些資料作為輸出,而不修改輸入。見[第十章](ch10.md)。
一種計算,它將一些固定的(通常是大的)資料集作為輸入,並將其他一些資料作為輸出,而不修改輸入。見[第十章](/tw/ch10)。
* **邊界bounded**
## **邊界bounded**
有一些已知的上限或大小。例如,網路延遲情況(請參閱“[超時與無窮的延遲](ch8.md#超時與無窮的延遲)”)和資料集(請參閱[第十一章](ch11.md)的介紹)。
有一些已知的上限或大小。例如,網路延遲情況(請參閱“[超時與無窮的延遲](/tw/ch8#超時與無窮的延遲)”)和資料集(請參閱[第十一章](/tw/ch11)的介紹)。
* **拜占庭故障Byzantine fault**
## **拜占庭故障Byzantine fault**
表現異常的節點,這種異常可能以任意方式出現,例如向其他節點發送矛盾或惡意訊息。請參閱“[拜占庭故障](ch8.md#拜占庭故障)”。
表現異常的節點,這種異常可能以任意方式出現,例如向其他節點發送矛盾或惡意訊息。請參閱“[拜占庭故障](/tw/ch8#拜占庭故障)”。
* **快取cache**
## **快取cache**
一種元件,透過儲存最近使用過的資料,加快未來對相同資料的讀取速度。快取中通常存放部分資料:因此,如果快取中缺少某些資料,則必須從某些底層較慢的資料儲存系統中,獲取完整的資料副本。
* **CAP定理CAP theorem**
## **CAP定理CAP theorem**
一個被廣泛誤解的理論結果,在實踐中是沒有用的。請參閱“[CAP定理](ch9.md#CAP定理)”。
一個被廣泛誤解的理論結果,在實踐中是沒有用的。請參閱“[CAP定理](/tw/ch9#CAP定理)”。
* **因果關係causality**
## **因果關係causality**
事件之間的依賴關係,當一件事發生在另一件事情之前。例如,後面的事件是對早期事件的回應,或者依賴於更早的事件,或者應該根據先前的事件來理解。請參閱“[“此前發生”的關係和併發](ch5.md#“此前發生”的關係和併發)”和“[順序與因果關係](ch9.md#順序與因果關係)”。
事件之間的依賴關係,當一件事發生在另一件事情之前。例如,後面的事件是對早期事件的回應,或者依賴於更早的事件,或者應該根據先前的事件來理解。請參閱“[“此前發生”的關係和併發](/tw/ch5#“此前發生”的關係和併發)”和“[順序與因果關係](/tw/ch9#順序與因果關係)”。
* **共識consensus**
## **共識consensus**
分散式計算的一個基本問題,就是讓幾個節點同意某些事情(例如,哪個節點應該是資料庫叢集的領導者)。問題比乍看起來要困難得多。請參閱“[容錯共識](ch9.md#容錯共識)”。
分散式計算的一個基本問題,就是讓幾個節點同意某些事情(例如,哪個節點應該是資料庫叢集的領導者)。問題比乍看起來要困難得多。請參閱“[容錯共識](/tw/ch9#容錯共識)”。
* **資料倉庫data warehouse**
## **資料倉庫data warehouse**
一個數據庫其中來自幾個不同的OLTP系統的資料已經被合併和準備用於分析目的。請參閱“[資料倉庫](ch3.md#資料倉庫)”。
一個數據庫其中來自幾個不同的OLTP系統的資料已經被合併和準備用於分析目的。請參閱“[資料倉庫](/tw/ch3#資料倉庫)”。
* **宣告式declarative**
## **宣告式declarative**
描述某些東西應有的屬性,但不知道如何實現它的確切步驟。在查詢的上下文中,查詢最佳化器採用宣告性查詢並決定如何最好地執行它。請參閱“[資料查詢語言](ch2.md#資料查詢語言)”。
描述某些東西應有的屬性,但不知道如何實現它的確切步驟。在查詢的上下文中,查詢最佳化器採用宣告性查詢並決定如何最好地執行它。請參閱“[資料查詢語言](/tw/ch2#資料查詢語言)”。
* **非規範化denormalize**
## **非規範化denormalize**
為了加速讀取,在標準資料集中引入一些冗餘或重複資料,通常採用快取或索引的形式。非規範化的值是一種預先計算的查詢結果,像物化檢視。請參閱“[單物件和多物件操作](ch7.md#單物件和多物件操作)”和“[從同一事件日誌中派生多個檢視](ch11.md#從同一事件日誌中派生多個檢視)”。
為了加速讀取,在標準資料集中引入一些冗餘或重複資料,通常採用快取或索引的形式。非規範化的值是一種預先計算的查詢結果,像物化檢視。請參閱“[單物件和多物件操作](/tw/ch7#單物件和多物件操作)”和“[從同一事件日誌中派生多個檢視](/tw/ch11#從同一事件日誌中派生多個檢視)”。
* **衍生資料derived data**
## **衍生資料derived data**
一種資料集,根據其他資料透過可重複執行的流程建立。必要時,你可以執行該流程再次建立衍生資料。衍生資料通常用於提高特定資料的讀取速度。常見的衍生資料有索引、快取和物化檢視。請參閱[第三部分](part-iii.md)的介紹。
一種資料集,根據其他資料透過可重複執行的流程建立。必要時,你可以執行該流程再次建立衍生資料。衍生資料通常用於提高特定資料的讀取速度。常見的衍生資料有索引、快取和物化檢視。請參閱[第三部分](/tw/part-iii)的介紹。
* **確定性deterministic**
## **確定性deterministic**
描述一個函式,如果給它相同的輸入,則總是產生相同的輸出。這意味著它不能依賴於隨機數字、時間、網路通訊或其他不可預測的事情。
* **分散式distributed**
## **分散式distributed**
在由網路連線的多個節點上執行。對於部分節點故障,具有容錯性:系統的一部分發生故障時,其他部分仍可以正常工作,通常情況下,軟體無需瞭解故障相關的確切情況。請參閱“[故障與部分失效](ch8.md#故障與部分失效)”。
在由網路連線的多個節點上執行。對於部分節點故障,具有容錯性:系統的一部分發生故障時,其他部分仍可以正常工作,通常情況下,軟體無需瞭解故障相關的確切情況。請參閱“[故障與部分失效](/tw/ch8#故障與部分失效)”。
* **持久durable**
## **持久durable**
以某種方式儲存資料,即使發生各種故障,也不會丟失資料。請參閱“[永續性](ch7.md#永續性)”。
以某種方式儲存資料,即使發生各種故障,也不會丟失資料。請參閱“[永續性](/tw/ch7#永續性)”。
* **ETLExtract-Transform-Load**
## **ETLExtract-Transform-Load**
提取-轉換-載入Extract-Transform-Load。從源資料庫中提取資料將其轉換為更適合分析查詢的形式並將其載入到資料倉庫或批處理系統中的過程。請參閱“[資料倉庫](ch3.md#資料倉庫)”。
提取-轉換-載入Extract-Transform-Load。從源資料庫中提取資料將其轉換為更適合分析查詢的形式並將其載入到資料倉庫或批處理系統中的過程。請參閱“[資料倉庫](/tw/ch3#資料倉庫)”。
* **故障切換failover**
## **故障切換failover**
在具有單一領導者的系統中,故障切換是將領導角色從一個節點轉移到另一個節點的過程。請參閱“[處理節點宕機](ch5.md#處理節點宕機)”。
在具有單一領導者的系統中,故障切換是將領導角色從一個節點轉移到另一個節點的過程。請參閱“[處理節點宕機](/tw/ch5#處理節點宕機)”。
* **容錯fault-tolerant**
## **容錯fault-tolerant**
如果出現問題(例如,機器崩潰或網路連線失敗),可以自動恢復。請參閱“[可靠性](ch1.md#可靠性)”。
如果出現問題(例如,機器崩潰或網路連線失敗),可以自動恢復。請參閱“[可靠性](/tw/ch1#可靠性)”。
* **流量控制flow control**
## **流量控制flow control**
見背壓backpressure
* **追隨者follower**
## **追隨者follower**
一種資料副本,僅處理領導者或主庫發出的資料變更,不直接接受來自客戶端的任何寫入。也稱為備庫、從庫、只讀副本或熱備份。請參閱“[領導者與追隨者](ch5.md#領導者與追隨者)”。
一種資料副本,僅處理領導者或主庫發出的資料變更,不直接接受來自客戶端的任何寫入。也稱為備庫、從庫、只讀副本或熱備份。請參閱“[領導者與追隨者](/tw/ch5#領導者與追隨者)”。
* **全文檢索full-text search**
## **全文檢索full-text search**
透過任意關鍵字來搜尋文字,通常具有附加特徵,例如匹配類似的拼寫詞或同義詞。全文索引是一種支援這種查詢的次級索引。請參閱“[全文搜尋和模糊索引](ch3.md#全文搜尋和模糊索引)”。
透過任意關鍵字來搜尋文字,通常具有附加特徵,例如匹配類似的拼寫詞或同義詞。全文索引是一種支援這種查詢的次級索引。請參閱“[全文搜尋和模糊索引](/tw/ch3#全文搜尋和模糊索引)”。
* **圖graph**
## **圖graph**
一種資料結構,由頂點(可以指向的東西,也稱為節點或實體)和邊(從一個頂點到另一個頂點的連線,也稱為關係或弧)組成。請參閱“[圖資料模型](ch2.md#圖資料模型)”。
一種資料結構,由頂點(可以指向的東西,也稱為節點或實體)和邊(從一個頂點到另一個頂點的連線,也稱為關係或弧)組成。請參閱“[圖資料模型](/tw/ch2#圖資料模型)”。
* **雜湊hash**
## **雜湊hash**
將輸入轉換為看起來像隨機數值的函式。相同的輸入會轉換為相同的數值,不同的輸入一般會轉換為不同的數值,也可能轉換為相同數值(也被稱為衝突)。請參閱“[根據鍵的雜湊分割槽](ch6.md#根據鍵的雜湊分割槽)”。
將輸入轉換為看起來像隨機數值的函式。相同的輸入會轉換為相同的數值,不同的輸入一般會轉換為不同的數值,也可能轉換為相同數值(也被稱為衝突)。請參閱“[根據鍵的雜湊分割槽](/tw/ch6#根據鍵的雜湊分割槽)”。
* **冪等idempotent**
## **冪等idempotent**
用於描述一種操作可以安全地重試執行,即執行多次的效果和執行一次的效果相同。請參閱“[冪等性](ch11.md#冪等性)”。
用於描述一種操作可以安全地重試執行,即執行多次的效果和執行一次的效果相同。請參閱“[冪等性](/tw/ch11#冪等性)”。
* **索引index**
## **索引index**
一種資料結構。透過索引,你可以根據特定欄位的值,在所有資料記錄中進行高效檢索。請參閱“[驅動資料庫的資料結構](ch3.md#驅動資料庫的資料結構)”。
一種資料結構。透過索引,你可以根據特定欄位的值,在所有資料記錄中進行高效檢索。請參閱“[驅動資料庫的資料結構](/tw/ch3#驅動資料庫的資料結構)”。
* **隔離性isolation**
## **隔離性isolation**
在事務上下文中,用於描述併發執行事務的互相干擾程度。序列執行具有最強的隔離性,不過其它程度的隔離也通常被使用。請參閱“[隔離性](ch7.md#隔離性)”。
在事務上下文中,用於描述併發執行事務的互相干擾程度。序列執行具有最強的隔離性,不過其它程度的隔離也通常被使用。請參閱“[隔離性](/tw/ch7#隔離性)”。
* **連線join**
## **連線join**
彙集有共同點的記錄。在一個記錄與另一個記錄有關(外部索引鍵,文件參考,圖中的邊)的情況下最常用,查詢需要獲取參考所指向的記錄。請參閱“[多對一和多對多的關係](ch2.md#多對一和多對多的關係)”和“[Reduce側連線與分組](ch10.md#Reduce側連線與分組)”。
彙集有共同點的記錄。在一個記錄與另一個記錄有關(外部索引鍵,文件參考,圖中的邊)的情況下最常用,查詢需要獲取參考所指向的記錄。請參閱“[多對一和多對多的關係](/tw/ch2#多對一和多對多的關係)”和“[Reduce側連線與分組](/tw/ch10#Reduce側連線與分組)”。
* **領導者leader**
## **領導者leader**
當資料或服務被複制到多個節點時,領導者是被指定為可以接受資料變更的副本。領導者可以透過某些協議選舉產生,也可以由管理員手動選擇。領導者也被稱為主庫。請參閱“[領導者與追隨者](ch5.md#領導者與追隨者)”。
當資料或服務被複制到多個節點時,領導者是被指定為可以接受資料變更的副本。領導者可以透過某些協議選舉產生,也可以由管理員手動選擇。領導者也被稱為主庫。請參閱“[領導者與追隨者](/tw/ch5#領導者與追隨者)”。
* **線性化linearizable**
## **線性化linearizable**
表現為系統中只有一份透過原子操作更新的資料副本。請參閱“[線性一致性](ch9.md#線性一致性)”。
表現為系統中只有一份透過原子操作更新的資料副本。請參閱“[線性一致性](/tw/ch9#線性一致性)”。
* **區域性locality**
## **區域性locality**
一種效能最佳化方式,如果經常在相同的時間請求一些離散資料,把這些資料放到一個位置。請參閱“[查詢的資料區域性](ch2.md#查詢的資料區域性)”。
一種效能最佳化方式,如果經常在相同的時間請求一些離散資料,把這些資料放到一個位置。請參閱“[查詢的資料區域性](/tw/ch2#查詢的資料區域性)”。
* **鎖lock**
## **鎖lock**
一種保證只有一個執行緒、節點或事務可以訪問的機制,如果其它執行緒、節點或事務想訪問相同元素,則必須等待鎖被釋放。請參閱“[兩階段鎖定](ch7.md#兩階段鎖定)”和“[領導者和鎖](ch8.md#領導者和鎖)”。
一種保證只有一個執行緒、節點或事務可以訪問的機制,如果其它執行緒、節點或事務想訪問相同元素,則必須等待鎖被釋放。請參閱“[兩階段鎖定](/tw/ch7#兩階段鎖定)”和“[領導者和鎖](/tw/ch8#領導者和鎖)”。
* **日誌log**
## **日誌log**
日誌是一個只能以追加方式寫入的檔案,用於存放資料。預寫式日誌用於在儲存引擎崩潰時恢復資料(請參閱“[讓B樹更可靠](ch3.md#讓B樹更可靠)”);結構化日誌儲存引擎使用日誌作為它的主要儲存格式(請參閱“[SSTables和LSM樹](ch3.md#SSTables和LSM樹)”);複製型日誌用於把寫入從領導者複製到追隨者(請參閱“[領導者與追隨者](ch5.md#領導者與追隨者)”);事件性日誌可以表現為資料流(請參閱“[分割槽日誌](ch11.md#分割槽日誌)”)。
日誌是一個只能以追加方式寫入的檔案,用於存放資料。預寫式日誌用於在儲存引擎崩潰時恢復資料(請參閱“[讓B樹更可靠](/tw/ch3#讓B樹更可靠)”);結構化日誌儲存引擎使用日誌作為它的主要儲存格式(請參閱“[SSTables和LSM樹](/tw/ch3#SSTables和LSM樹)”);複製型日誌用於把寫入從領導者複製到追隨者(請參閱“[領導者與追隨者](/tw/ch5#領導者與追隨者)”);事件性日誌可以表現為資料流(請參閱“[分割槽日誌](/tw/ch11#分割槽日誌)”)。
* **物化materialize**
## **物化materialize**
急切地計算並寫出結果,而不是在請求時計算。請參閱“[聚合:資料立方體和物化檢視](ch3.md#聚合:資料立方體和物化檢視)”和“[物化中間狀態](ch10.md#物化中間狀態)”。
急切地計算並寫出結果,而不是在請求時計算。請參閱“[聚合:資料立方體和物化檢視](/tw/ch3#聚合:資料立方體和物化檢視)”和“[物化中間狀態](/tw/ch10#物化中間狀態)”。
* **節點node**
## **節點node**
計算機上執行的一些軟體的例項,透過網路與其他節點通訊以完成某項任務。
* **規範化normalized**
## **規範化normalized**
以沒有冗餘或重複的方式進行結構化。在規範化資料庫中,當某些資料發生變化時,你只需要在一個地方進行更改,而不是在許多不同的地方複製很多次。請參閱“[多對一和多對多的關係](ch2.md#多對一和多對多的關係)”。
以沒有冗餘或重複的方式進行結構化。在規範化資料庫中,當某些資料發生變化時,你只需要在一個地方進行更改,而不是在許多不同的地方複製很多次。請參閱“[多對一和多對多的關係](/tw/ch2#多對一和多對多的關係)”。
* **OLAPOnline Analytic Processing**
## **OLAPOnline Analytic Processing**
線上分析處理。透過對大量記錄進行聚合(例如,計數,總和,平均)來表徵的訪問模式。請參閱“[事務處理還是分析?](ch3.md#事務處理還是分析?)”。
線上分析處理。透過對大量記錄進行聚合(例如,計數,總和,平均)來表徵的訪問模式。請參閱“[事務處理還是分析?](/tw/ch3#事務處理還是分析?)”。
* **OLTPOnline Transaction Processing**
## **OLTPOnline Transaction Processing**
線上事務處理。訪問模式的特點是快速查詢,讀取或寫入少量記錄,這些記錄通常透過鍵索引。請參閱“[事務處理還是分析?](ch3.md#事務處理還是分析?)”。
線上事務處理。訪問模式的特點是快速查詢,讀取或寫入少量記錄,這些記錄通常透過鍵索引。請參閱“[事務處理還是分析?](/tw/ch3#事務處理還是分析?)”。
* **分割槽partitioning**
## **分割槽partitioning**
將單機上的大型資料集或計算結果拆分為較小部分,並將其分佈到多臺機器上。也稱為分片。見[第六章](ch6.md)。
將單機上的大型資料集或計算結果拆分為較小部分,並將其分佈到多臺機器上。也稱為分片。見[第六章](/tw/ch6)。
* **百分位點percentile**
## **百分位點percentile**
透過計算有多少值高於或低於某個閾值來衡量值分佈的方法。例如某個時間段的第95個百分位響應時間是時間t則該時間段中95%的請求完成時間小於t5%的請求完成時間要比t長。請參閱“[描述效能](ch1.md#描述效能)”。
透過計算有多少值高於或低於某個閾值來衡量值分佈的方法。例如某個時間段的第95個百分位響應時間是時間t則該時間段中95%的請求完成時間小於t5%的請求完成時間要比t長。請參閱“[描述效能](/tw/ch1#描述效能)”。
* **主鍵primary key**
## **主鍵primary key**
唯一標識記錄的值(通常是數字或字串)。在許多應用程式中,主鍵由系統在建立記錄時生成(例如,按順序或隨機); 它們通常不由使用者設定。另請參閱次級索引。
* **法定人數quorum**
## **法定人數quorum**
在操作完成之前,需要對操作進行投票的最少節點數量。請參閱“[讀寫的法定人數](ch5.md#讀寫的法定人數)”。
在操作完成之前,需要對操作進行投票的最少節點數量。請參閱“[讀寫的法定人數](/tw/ch5#讀寫的法定人數)”。
* **再平衡rebalance**
## **再平衡rebalance**
將資料或服務從一個節點移動到另一個節點以實現負載均衡。請參閱“[分割槽再平衡](ch6.md#分割槽再平衡)”。
將資料或服務從一個節點移動到另一個節點以實現負載均衡。請參閱“[分割槽再平衡](/tw/ch6#分割槽再平衡)”。
* **複製replication**
## **複製replication**
在幾個節點(副本)上保留相同資料的副本,以便在某些節點無法訪問時,資料仍可訪問。請參閱[第五章](ch5.md)。
在幾個節點(副本)上保留相同資料的副本,以便在某些節點無法訪問時,資料仍可訪問。請參閱[第五章](/tw/ch5)。
* **模式schema**
## **模式schema**
一些資料結構的描述,包括其欄位和資料型別。可以在資料生命週期的不同點檢查某些資料是否符合模式(請參閱“[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)”),模式可以隨時間變化(請參閱[第四章](ch4.md))。
一些資料結構的描述,包括其欄位和資料型別。可以在資料生命週期的不同點檢查某些資料是否符合模式(請參閱“[文件模型中的模式靈活性](/tw/ch2#文件模型中的模式靈活性)”),模式可以隨時間變化(請參閱[第四章](/tw/ch4))。
* **次級索引secondary index**
## **次級索引secondary index**
與主要資料儲存器一起維護的附加資料結構,使你可以高效地搜尋與某種條件相匹配的記錄。請參閱“[其他索引結構](ch3.md#其他索引結構)”和“[分割槽與次級索引](ch6.md#分割槽與次級索引)”。
與主要資料儲存器一起維護的附加資料結構,使你可以高效地搜尋與某種條件相匹配的記錄。請參閱“[其他索引結構](/tw/ch3#其他索引結構)”和“[分割槽與次級索引](/tw/ch6#分割槽與次級索引)”。
* **可序列化serializable**
## **可序列化serializable**
保證多個併發事務同時執行時,它們的行為與按順序逐個執行事務相同。請參閱第七章的“[可序列化](ch7.md#可序列化)”。
保證多個併發事務同時執行時,它們的行為與按順序逐個執行事務相同。請參閱第七章的“[可序列化](/tw/ch7#可序列化)”。
* **無共享shared-nothing**
## **無共享shared-nothing**
與共享記憶體或共享磁碟架構相比獨立節點每個節點都有自己的CPU記憶體和磁碟透過傳統網路連線。見[第二部分](part-ii.md)的介紹。
與共享記憶體或共享磁碟架構相比獨立節點每個節點都有自己的CPU記憶體和磁碟透過傳統網路連線。見[第二部分](/tw/part-ii)的介紹。
* **偏斜skew**
## **偏斜skew**
各分割槽負載不平衡,例如某些分割槽有大量請求或資料,而其他分割槽則少得多。也被稱為熱點。請參閱“[負載偏斜與熱點消除](ch6.md#負載偏斜與熱點消除)”和“[處理偏斜](ch10.md#處理偏斜)”。
各分割槽負載不平衡,例如某些分割槽有大量請求或資料,而其他分割槽則少得多。也被稱為熱點。請參閱“[負載偏斜與熱點消除](/tw/ch6#負載偏斜與熱點消除)”和“[處理偏斜](/tw/ch10#處理偏斜)”。
時間線異常導致事件以不期望的順序出現。請參閱“[快照隔離和可重複讀](ch7.md#快照隔離和可重複讀)”中的關於讀取偏差的討論,“[寫入偏差與幻讀](ch7.md#寫入偏差與幻讀)”中的寫入偏差以及“[有序事件的時間戳](ch8.md#有序事件的時間戳)”中的時鐘偏斜。
時間線異常導致事件以不期望的順序出現。請參閱“[快照隔離和可重複讀](/tw/ch7#快照隔離和可重複讀)”中的關於讀取偏差的討論,“[寫入偏差與幻讀](/tw/ch7#寫入偏差與幻讀)”中的寫入偏差以及“[有序事件的時間戳](/tw/ch8#有序事件的時間戳)”中的時鐘偏斜。
* **腦裂split brain**
## **腦裂split brain**
兩個節點同時認為自己是領導者的情況,這種情況可能違反系統擔保。請參閱“[處理節點宕機](ch5.md#處理節點宕機)”和“[真相由多數所定義](ch8.md#真相由多數所定義)”。
兩個節點同時認為自己是領導者的情況,這種情況可能違反系統擔保。請參閱“[處理節點宕機](/tw/ch5#處理節點宕機)”和“[真相由多數所定義](/tw/ch8#真相由多數所定義)”。
* **儲存過程stored procedure**
## **儲存過程stored procedure**
一種對事務邏輯進行編碼的方式,它可以完全在資料庫伺服器上執行,事務執行期間無需與客戶端通訊。請參閱“[真的序列執行](ch7.md#真的序列執行)”。
一種對事務邏輯進行編碼的方式,它可以完全在資料庫伺服器上執行,事務執行期間無需與客戶端通訊。請參閱“[真的序列執行](/tw/ch7#真的序列執行)”。
* **流處理stream process**
## **流處理stream process**
持續執行的計算。可以持續接收事件流作為輸入,並得出一些輸出。見[第十一章](ch11.md)。
持續執行的計算。可以持續接收事件流作為輸入,並得出一些輸出。見[第十一章](/tw/ch11)。
* **同步synchronous**
## **同步synchronous**
非同步的反義詞。
* **記錄系統system of record**
## **記錄系統system of record**
一個儲存主要權威版本資料的系統,也被稱為真相的來源。首先在這裡寫入資料變更,其他資料集可以從記錄系統衍生。請參閱[第三部分](part-iii.md)的介紹。
一個儲存主要權威版本資料的系統,也被稱為真相的來源。首先在這裡寫入資料變更,其他資料集可以從記錄系統衍生。請參閱[第三部分](/tw/part-iii)的介紹。
* **超時timeout**
## **超時timeout**
檢測故障的最簡單方法之一,即在一段時間內觀察是否缺乏響應。但是,不可能知道超時是由於遠端節點的問題還是網路中的問題造成的。請參閱“[超時與無窮的延遲](ch8.md#超時與無窮的延遲)”。
檢測故障的最簡單方法之一,即在一段時間內觀察是否缺乏響應。但是,不可能知道超時是由於遠端節點的問題還是網路中的問題造成的。請參閱“[超時與無窮的延遲](/tw/ch8#超時與無窮的延遲)”。
* **全序total order**
## **全序total order**
一種比較事物的方法(例如時間戳),可以讓你總是說出兩件事中哪一件更大,哪件更小。總的來說,有些東西是無法比擬的(不能說哪個更大或更小)的順序稱為偏序。請參閱“[因果順序不是全序的](ch9.md#因果順序不是全序的)”。
一種比較事物的方法(例如時間戳),可以讓你總是說出兩件事中哪一件更大,哪件更小。總的來說,有些東西是無法比擬的(不能說哪個更大或更小)的順序稱為偏序。請參閱“[因果順序不是全序的](/tw/ch9#因果順序不是全序的)”。
* **事務transaction**
## **事務transaction**
為了簡化錯誤處理和併發問題,將幾個讀寫操作分組到一個邏輯單元中。見[第七章](ch7.md)。
為了簡化錯誤處理和併發問題,將幾個讀寫操作分組到一個邏輯單元中。見[第七章](/tw/ch7)。
* **兩階段提交2PC, two-phase commit**
## **兩階段提交2PC, two-phase commit**
一種確保多個數據庫節點全部提交或全部中止事務的演算法。請參閱“[原子提交與兩階段提交](ch9.md#原子提交與兩階段提交)”。
一種確保多個數據庫節點全部提交或全部中止事務的演算法。請參閱“[原子提交與兩階段提交](/tw/ch9#原子提交與兩階段提交)”。
* **兩階段鎖定2PL, two-phase locking**
## **兩階段鎖定2PL, two-phase locking**
一種用於實現可序列化隔離的演算法,該演算法透過事務獲取對其讀取或寫入的所有資料的鎖,直到事務結束。請參閱“[兩階段鎖定](ch7.md#兩階段鎖定)”。
一種用於實現可序列化隔離的演算法,該演算法透過事務獲取對其讀取或寫入的所有資料的鎖,直到事務結束。請參閱“[兩階段鎖定](/tw/ch7#兩階段鎖定)”。
* **無邊界unbounded**
## **無邊界unbounded**
沒有任何已知的上限或大小。反義詞是邊界bounded

38
content/tw/part-i.md Normal file
View File

@@ -0,0 +1,38 @@
---
title: 第一部分:資料系統基礎
weight: 100
breadcrumbs: false
---
本書前四章介紹了資料系統底層的基礎概念,無論是在單臺機器上執行的單點資料系統,還是分佈在多臺機器上的分散式資料系統都適用。
1. [第一章](/tw/ch1) 將介紹本書使用的術語和方法。**可靠性,可伸縮性和可維護性** ,這些詞彙到底意味著什麼?如何實現這些目標?
2. [第二章](/tw/ch2) 將對幾種不同的 **資料模型和查詢語言** 進行比較。從程式設計師的角度看,這是資料庫之間最明顯的區別。不同的資料模型適用於不同的應用場景。
3. [第三章](/tw/ch3) 將深入 **儲存引擎** 內部,研究資料庫如何在磁碟上擺放資料。不同的儲存引擎針對不同的負載進行最佳化,選擇合適的儲存引擎對系統性能有巨大影響。
4. [第四章](/tw/ch4) 將對幾種不同的 **資料編碼** 進行比較。特別研究了這些格式在應用需求經常變化、模式需要隨時間演變的環境中表現如何。
第二部分將專門討論在 **分散式資料系統** 中特有的問題。
## 索引
* [第一章:可靠性、可伸縮性和可維護性](/tw/ch1)
* [關於資料系統的思考](/tw/ch1#關於資料系統的思考)
* [可靠性](/tw/ch1#可靠性)
* [可伸縮性](/tw/ch1#可伸縮性)
* [可維護性](/tw/ch1#可維護性)
* [本章小結](/tw/ch1#本章小結)
* [第二章:資料模型與查詢語言](/tw/ch2)
* [關係模型與文件模型](/tw/ch2#關係模型與文件模型)
* [資料查詢語言](/tw/ch2#資料查詢語言)
* [圖資料模型](/tw/ch2#圖資料模型)
* [本章小結](/tw/ch2#本章小結)
* [第三章:儲存與檢索](/tw/ch3)
* [驅動資料庫的資料結構](/tw/ch3#驅動資料庫的資料結構)
* [事務處理還是分析?](/tw/ch3#事務處理還是分析)
* [列式儲存](/tw/ch3#列式儲存)
* [本章小結](/tw/ch3#本章小結)
* [第四章:編碼與演化](/tw/ch4)
* [編碼資料的格式](/tw/ch4#編碼資料的格式)
* [資料流的型別](/tw/ch4#資料流的型別)
* [本章小結](/tw/ch4#本章小結)

View File

@@ -1,4 +1,8 @@
# 第二部分:分散式資料
---
title: 第二部分:分散式資料
weight: 200
breadcrumbs: false
---
> 一個成功的技術,現實的優先順序必須高於公關,你可以糊弄別人,但糊弄不了自然規律。
>
@@ -7,7 +11,7 @@
-------
在本書的 [第一部分](part-i.md) 中,我們討論了資料系統的各個方面,但僅限於資料儲存在單臺機器上的情況。現在我們到了 [第二部分](part-ii.md),進入更高的層次,並提出一個問題:如果 **多臺機器** 參與資料的儲存和檢索,會發生什麼?
在本書的 [第一部分](/tw/part-i) 中,我們討論了資料系統的各個方面,但僅限於資料儲存在單臺機器上的情況。現在我們到了 [第二部分](/tw/part-ii),進入更高的層次,並提出一個問題:如果 **多臺機器** 參與資料的儲存和檢索,會發生什麼?
你可能會出於各種各樣的原因,希望將資料庫分佈到多臺機器上:
@@ -53,30 +57,55 @@
* 複製Replication
在幾個不同的節點上儲存資料的相同副本,可能放在不同的位置。複製提供了冗餘:如果一些節點不可用,剩餘的節點仍然可以提供資料服務。複製也有助於改善效能。[第五章](ch5.md) 將討論複製。
在幾個不同的節點上儲存資料的相同副本,可能放在不同的位置。複製提供了冗餘:如果一些節點不可用,剩餘的節點仍然可以提供資料服務。複製也有助於改善效能。[第五章](/tw/ch5) 將討論複製。
* 分割槽 (Partitioning)
將一個大型資料庫拆分成較小的子集(稱為 **分割槽**,即 partitions從而不同的分割槽可以指派給不同的 **節點**nodes亦稱 **分片**,即 sharding。[第六章](ch6.md) 將討論分割槽。
將一個大型資料庫拆分成較小的子集(稱為 **分割槽**,即 partitions從而不同的分割槽可以指派給不同的 **節點**nodes亦稱 **分片**,即 sharding。[第六章](/tw/ch6) 將討論分割槽。
複製和分割槽是不同的機制,但它們經常同時使用。如 [圖 II-1](../img/figii-1.png) 所示。
複製和分割槽是不同的機制,但它們經常同時使用。如 [圖 II-1](/img/figii-1.png) 所示。
![](../img/figii-1.png)
![](/img/figii-1.png)
**圖 II-1 一個數據庫切分為兩個分割槽,每個分割槽都有兩個副本**
理解了這些概念,就可以開始討論在分散式系統中需要做出的困難抉擇。[第七章](ch7.md) 將討論 **事務Transaction**,這對於瞭解資料系統中可能出現的各種問題,以及我們可以做些什麼很有幫助。[第八章](ch8.md) 和 [第九章](ch9.md) 將討論分散式系統的根本侷限性。
理解了這些概念,就可以開始討論在分散式系統中需要做出的困難抉擇。[第七章](/tw/ch7) 將討論 **事務Transaction**,這對於瞭解資料系統中可能出現的各種問題,以及我們可以做些什麼很有幫助。[第八章](/tw/ch8) 和 [第九章](/tw/ch9) 將討論分散式系統的根本侷限性。
在本書的 [第三部分](part-iii.md) 中,將討論如何將多個(可能是分散式的)資料儲存整合為一個更大的系統,以滿足複雜的應用需求。但首先,我們來聊聊分散式的資料。
在本書的 [第三部分](/tw/part-iii) 中,將討論如何將多個(可能是分散式的)資料儲存整合為一個更大的系統,以滿足複雜的應用需求。但首先,我們來聊聊分散式的資料。
## 索引
5. [複製](ch5.md)
6. [分割槽](ch6.md)
7. [事務](ch7.md)
8. [分散式系統的麻煩](ch8.md)
9. [一致性與共識](ch9.md)
* [第五章:複製](/tw/ch5)
* [領導者與追隨者](/tw/ch5#領導者與追隨者)
* [複製延遲問題](/tw/ch5#複製延遲問題)
* [多主複製](/tw/ch5#多主複製)
* [無主複製](/tw/ch5#無主複製)
* [本章小結](/tw/ch5#本章小結)
* [第六章:分割槽](/tw/ch6)
* [分割槽與複製](/tw/ch6#分割槽與複製)
* [鍵值資料的分割槽](/tw/ch6#鍵值資料的分割槽)
* [分割槽與次級索引](/tw/ch6#分割槽與次級索引)
* [分割槽再平衡](/tw/ch6#分割槽再平衡)
* [請求路由](/tw/ch6#請求路由)
* [本章小結](/tw/ch6#本章小結)
* [第七章:事務](/tw/ch7)
* [事務的棘手概念](/tw/ch7#事務的棘手概念)
* [弱隔離級別](/tw/ch7#弱隔離級別)
* [可序列化](/tw/ch7#可序列化)
* [本章小結](/tw/ch7#本章小結)
* [第八章:分散式系統的麻煩](/tw/ch8)
* [故障與部分失效](/tw/ch8#故障與部分失效)
* [不可靠的網路](/tw/ch8#不可靠的網路)
* [不可靠的時鐘](/tw/ch8#不可靠的時鐘)
* [知識、真相與謊言](/tw/ch8#知識真相與謊言)
* [本章小結](/tw/ch8#本章小結)
* [第九章:一致性與共識](/tw/ch9)
* [一致性保證](/tw/ch9#一致性保證)
* [線性一致性](/tw/ch9#線性一致性)
* [順序保證](/tw/ch9#順序保證)
* [分散式事務與共識](/tw/ch9#分散式事務與共識)
* [本章小結](/tw/ch9#本章小結)
## 參考文獻
@@ -84,10 +113,4 @@
1. Ulrich Drepper: “[What Every Programmer Should Know About Memory](https://people.freebsd.org/~lstewart/articles/cpumemory.pdf),” akkadia.org, November 21, 2007.
1. Ben Stopford: “[Shared Nothing vs. Shared Disk Architectures: An Independent View](http://www.benstopford.com/2009/11/24/understanding-the-shared-nothing-architecture/),” benstopford.com, November 24, 2009.
1. Michael Stonebraker: “[The Case for Shared Nothing](http://db.cs.berkeley.edu/papers/hpts85-nothing.pdf),” IEEE Database EngineeringBulletin, volume 9, number 1, pages 49, March 1986.
1. Frank McSherry, Michael Isard, and Derek G. Murray: “[Scalability! But at What COST?](http://www.frankmcsherry.org/assets/COST.pdf),” at 15th USENIX Workshop on Hot Topics in Operating Systems (HotOS),May 2015.
------
| 上一章 | 目錄 | 下一章 |
| ---------------------------- | ------------------------------- | ---------------------- |
| [第四章:編碼與演化](ch4.md) | [設計資料密集型應用](README.md) | [第五章:複製](ch5.md) |
1. Frank McSherry, Michael Isard, and Derek G. Murray: “[Scalability! But at What COST?](http://www.frankmcsherry.org/assets/COST.pdf),” at 15th USENIX Workshop on Hot Topics in Operating Systems (HotOS),May 2015.

View File

@@ -1,6 +1,11 @@
# 第三部分:衍生資料
---
title: 第三部分:衍生資料
weight: 300
breadcrumbs: false
---
在本書的 [第一部分](part-i.md) 和 [第二部分](part-ii.md) 中,我們自底向上地把所有關於分散式資料庫的主要考量都過了一遍。從資料在磁碟上的佈局,一直到出現故障時分散式系統一致性的侷限。但所有的討論都假定了應用中只用了一種資料庫。
在本書的 [第一部分](/tw/part-i) 和 [第二部分](/tw/part-ii) 中,我們自底向上地把所有關於分散式資料庫的主要考量都過了一遍。從資料在磁碟上的佈局,一直到出現故障時分散式系統一致性的侷限。但所有的討論都假定了應用中只用了一種資料庫。
現實世界中的資料系統往往更為複雜。大型應用程式經常需要以多種方式訪問和處理資料,沒有一個數據庫可以同時滿足所有這些不同的需求。因此應用程式通常組合使用多種元件:資料儲存、索引、快取、分析系統等等,並實現在這些元件中移動資料的機制。
@@ -28,17 +33,23 @@
## 章節概述
我們將從 [第十章](ch10.md) 開始,研究例如 MapReduce 這樣 **面向批處理batch-oriented** 的資料流系統。對於建設大規模資料系統,我們將看到,它們提供了優秀的工具和思想。[第十一章](ch11.md) 將把這些思想應用到 **流式資料data streams** 中,使我們能用更低的延遲完成同樣的任務。[第十二章](ch12.md) 將對本書進行總結,探討如何使用這些工具來構建可靠,可伸縮和可維護的應用。
我們將從 [第十章](/tw/ch10) 開始,研究例如 MapReduce 這樣 **面向批處理batch-oriented** 的資料流系統。對於建設大規模資料系統,我們將看到,它們提供了優秀的工具和思想。[第十一章](/tw/ch11) 將把這些思想應用到 **流式資料data streams** 中,使我們能用更低的延遲完成同樣的任務。[第十二章](/tw/ch12) 將對本書進行總結,探討如何使用這些工具來構建可靠,可伸縮和可維護的應用。
## 索引
10. [批處理](ch10.md)
11. [流處理](ch11.md)
12. [資料系統的未來](ch12.md)
------
| 上一章 | 目錄 | 下一章 |
| ------------------------------ | ------------------------------- | ------------------------- |
| [第九章:一致性與共識](ch9.md) | [設計資料密集型應用](README.md) | [第十章:批處理](ch10.md) |
* [第十章:批處理](/tw/ch10)
* [使用Unix工具的批處理](/tw/ch10#使用Unix工具的批處理)
* [MapReduce和分散式檔案系統](/tw/ch10#MapReduce和分散式檔案系統)
* [MapReduce之後](/tw/ch10#MapReduce之後)
* [本章小結](/tw/ch10#本章小結)
* [第十一章:流處理](/tw/ch11)
* [傳遞事件流](/tw/ch11#傳遞事件流)
* [資料庫與流](/tw/ch11#資料庫與流)
* [流處理](/tw/ch11#流處理)
* [本章小結](/tw/ch11#本章小結)
* [第十二章:資料系統的未來](/tw/ch12)
* [資料整合](/tw/ch12#資料整合)
* [分拆資料庫](/tw/ch12#分拆資料庫)
* [將事情做正確](/tw/ch12#將事情做正確)
* [做正確的事情](/tw/ch12#做正確的事情)
* [本章小結](/tw/ch12#本章小結)

View File

@@ -1,4 +1,9 @@
# 序言
---
title: 序言
weight: 50
breadcrumbs: false
---
如果近幾年從業於軟體工程,特別是伺服器端和後端系統開發,那麼你很有可能已經被大量關於資料儲存和處理的時髦詞彙轟炸過了: NoSQL大資料Web-Scale分片最終一致性ACIDCAP 定理雲服務MapReduce即時
@@ -60,11 +65,11 @@
本書分為三部分:
1. 在 [第一部分](part-i.md) 中,我們會討論設計資料密集型應用所賴的基本思想。我們從 [第一章](ch1.md) 開始,討論我們實際要達到的目標:可靠性、可伸縮性和可維護性;我們該如何思考這些概念;以及如何實現它們。在 [第二章](ch2.md) 中,我們比較了幾種不同的資料模型和查詢語言,看看它們如何適用於不同的場景。在 [第三章](ch3.md) 中將討論儲存引擎:資料庫如何在磁碟上擺放資料,以便能高效地再次找到它。[第四章](ch4.md) 轉向資料編碼(序列化),以及隨時間演化的模式。
1. 在 [第一部分](/tw/part-i) 中,我們會討論設計資料密集型應用所賴的基本思想。我們從 [第一章](/tw/ch1) 開始,討論我們實際要達到的目標:可靠性、可伸縮性和可維護性;我們該如何思考這些概念;以及如何實現它們。在 [第二章](/tw/ch2) 中,我們比較了幾種不同的資料模型和查詢語言,看看它們如何適用於不同的場景。在 [第三章](/tw/ch3) 中將討論儲存引擎:資料庫如何在磁碟上擺放資料,以便能高效地再次找到它。[第四章](/tw/ch4) 轉向資料編碼(序列化),以及隨時間演化的模式。
2. 在 [第二部分](part-ii.md) 中,我們從討論儲存在一臺機器上的資料轉向討論分佈在多臺機器上的資料。這對於可伸縮性通常是必需的,但帶來了各種獨特的挑戰。我們首先討論複製([第五章](ch5.md))、分割槽 / 分片([第六章](ch6.md))和事務([第七章](ch7.md))。然後我們將探索關於分散式系統問題的更多細節([第八章](ch8.md)),以及在分散式系統中實現一致性與共識意味著什麼([第九章](ch9.md))。
2. 在 [第二部分](/tw/part-ii) 中,我們從討論儲存在一臺機器上的資料轉向討論分佈在多臺機器上的資料。這對於可伸縮性通常是必需的,但帶來了各種獨特的挑戰。我們首先討論複製([第五章](/tw/ch5))、分割槽 / 分片([第六章](/tw/ch6))和事務([第七章](/tw/ch7))。然後我們將探索關於分散式系統問題的更多細節([第八章](/tw/ch8)),以及在分散式系統中實現一致性與共識意味著什麼([第九章](/tw/ch9))。
3. 在 [第三部分](part-iii.md) 中,我們討論那些從其他資料集衍生出一些資料集的系統。衍生資料經常出現在異構系統中:當沒有單個數據庫可以把所有事情都做的很好時,應用需要整合幾種不同的資料庫、快取、索引等。在 [第十章](ch10.md) 中我們將從一種衍生資料的批處理方法開始,然後在此基礎上建立在 [第十一章](ch11.md) 中討論的流處理。最後,在 [第十二章](ch12.md) 中,我們將所有內容彙總,討論在將來構建可靠、可伸縮和可維護的應用程式的方法。
3. 在 [第三部分](/tw/part-iii) 中,我們討論那些從其他資料集衍生出一些資料集的系統。衍生資料經常出現在異構系統中:當沒有單個數據庫可以把所有事情都做的很好時,應用需要整合幾種不同的資料庫、快取、索引等。在 [第十章](/tw/ch10) 中我們將從一種衍生資料的批處理方法開始,然後在此基礎上建立在 [第十一章](/tw/ch11) 中討論的流處理。最後,在 [第十二章](/tw/ch12) 中,我們將所有內容彙總,討論在將來構建可靠、可伸縮和可維護的應用程式的方法。
## 參考文獻與延伸閱讀

91
content/tw/toc.md Normal file
View File

@@ -0,0 +1,91 @@
---
title: "目錄"
linkTitle: "目錄"
weight: 10
breadcrumbs: false
---
![](/img/title.png)
## [序言](/tw/preface)
## [第一部分:資料系統基礎](/tw/part-i)
### [第一章:可靠性、可伸縮性和可維護性](/tw/ch1)
* [關於資料系統的思考](/tw/ch1#關於資料系統的思考)
* [可靠性](/tw/ch1#可靠性)
* [可伸縮性](/tw/ch1#可伸縮性)
* [可維護性](/tw/ch1#可維護性)
* [本章小結](/tw/ch1#本章小結)
### [第二章:資料模型與查詢語言](/tw/ch2)
* [關係模型與文件模型](/tw/ch2#關係模型與文件模型)
* [資料查詢語言](/tw/ch2#資料查詢語言)
* [圖資料模型](/tw/ch2#圖資料模型)
* [本章小結](/tw/ch2#本章小結)
### [第三章:儲存與檢索](/tw/ch3)
* [驅動資料庫的資料結構](/tw/ch3#驅動資料庫的資料結構)
* [事務處理還是分析?](/tw/ch3#事務處理還是分析)
* [列式儲存](/tw/ch3#列式儲存)
* [本章小結](/tw/ch3#本章小結)
### [第四章:編碼與演化](/tw/ch4)
* [編碼資料的格式](/tw/ch4#編碼資料的格式)
* [資料流的型別](/tw/ch4#資料流的型別)
* [本章小結](/tw/ch4#本章小結)
## [第二部分:分散式資料](/tw/part-ii)
### [第五章:複製](/tw/ch5)
* [領導者與追隨者](/tw/ch5#領導者與追隨者)
* [複製延遲問題](/tw/ch5#複製延遲問題)
* [多主複製](/tw/ch5#多主複製)
* [無主複製](/tw/ch5#無主複製)
* [本章小結](/tw/ch5#本章小結)
### [第六章:分割槽](/tw/ch6)
* [分割槽與複製](/tw/ch6#分割槽與複製)
* [鍵值資料的分割槽](/tw/ch6#鍵值資料的分割槽)
* [分割槽與次級索引](/tw/ch6#分割槽與次級索引)
* [分割槽再平衡](/tw/ch6#分割槽再平衡)
* [請求路由](/tw/ch6#請求路由)
* [本章小結](/tw/ch6#本章小結)
### [第七章:事務](/tw/ch7)
* [事務的棘手概念](/tw/ch7#事務的棘手概念)
* [弱隔離級別](/tw/ch7#弱隔離級別)
* [可序列化](/tw/ch7#可序列化)
* [本章小結](/tw/ch7#本章小結)
### [第八章:分散式系統的麻煩](/tw/ch8)
* [故障與部分失效](/tw/ch8#故障與部分失效)
* [不可靠的網路](/tw/ch8#不可靠的網路)
* [不可靠的時鐘](/tw/ch8#不可靠的時鐘)
* [知識、真相與謊言](/tw/ch8#知識真相與謊言)
* [本章小結](/tw/ch8#本章小結)
### [第九章:一致性與共識](/tw/ch9)
* [一致性保證](/tw/ch9#一致性保證)
* [線性一致性](/tw/ch9#線性一致性)
* [順序保證](/tw/ch9#順序保證)
* [分散式事務與共識](/tw/ch9#分散式事務與共識)
* [本章小結](/tw/ch9#本章小結)
## [第三部分:衍生資料](/tw/part-iii)
### [第十章:批處理](/tw/ch10)
* [使用Unix工具的批處理](/tw/ch10#使用Unix工具的批處理)
* [MapReduce和分散式檔案系統](/tw/ch10#MapReduce和分散式檔案系統)
* [MapReduce之後](/tw/ch10#MapReduce之後)
* [本章小結](/tw/ch10#本章小結)
### [第十一章:流處理](/tw/ch11)
* [傳遞事件流](/tw/ch11#傳遞事件流)
* [資料庫與流](/tw/ch11#資料庫與流)
* [流處理](/tw/ch11#流處理)
* [本章小結](/tw/ch11#本章小結)
### [第十二章:資料系統的未來](/tw/ch12)
* [資料整合](/tw/ch12#資料整合)
* [分拆資料庫](/tw/ch12#分拆資料庫)
* [將事情做正確](/tw/ch12#將事情做正確)
* [做正確的事情](/tw/ch12#做正確的事情)
* [本章小結](/tw/ch12#本章小結)
### [術語表](/tw/glossary)
### [後記](/tw/colophon)

316
content/v2/_index.md Normal file
View File

@@ -0,0 +1,316 @@
---
title: 设计数据密集型应用(第二版)
linkTitle: DDIA
cascade:
type: docs
breadcrumbs: false
---
**作者** [Martin Kleppmann](https://martin.kleppmann.com)[《Designing Data-Intensive Applications 2nd Edition》](https://learning.oreilly.com/library/view/designing-data-intensive-applications/9781098119058/ch01.html) 英国剑桥大学分布式系统研究员,演讲者,博主和开源贡献者,软件工程师和企业家,曾在 LinkedIn 和 Rapportive 负责数据基础架构。
**译者**[**冯若航**](https://vonng.com),网名 [@Vonng](https://github.com/Vonng)。
PostgreSQL 专家,数据库老司机,云计算泥石流。
[**Pigsty**](https://pgsty.com) 作者与创始人。
架构师DBA全栈工程师 @ TanTanAlibabaApple。
独立开源贡献者,[GitStar Ranking 585](https://gitstar-ranking.com/Vonng)[国区活跃 Top20](https://committers.top/china)。
[DDIA](https://ddia.pigsty.io) / [PG Internal](https://pgint.vonng.com) 中文版译者,公众号:《老冯云数》,数据库 KOL。
**校订** [@yingang](https://github.com/yingang) [繁體中文](/tw) **版本维护** by [@afunTW](https://github.com/afunTW) [完整贡献者列表](/contrib)
> [!NOTE]
> DDIA [**第二版**](https://github.com/Vonng/ddia/tree/v2) 正在翻译中 ([`hugo`](https://github.com/Vonng/ddia/tree/v2) 分支 `content/v2` 目录),欢迎加入并提出您的宝贵意见!
## 译序
> 不懂数据库的全栈工程师不是好架构师 —— 冯若航 / Vonng
现今尤其是在互联网领域大多数应用都属于数据密集型应用。本书从底层数据结构到顶层架构设计将数据系统设计中的精髓娓娓道来。其中的宝贵经验无论是对架构师、DBA、还是后端工程师、甚至产品经理都会有帮助。
这是一本理论结合实践的书,书中很多问题,译者在实际场景中都曾遇到过,读来让人击节扼腕。如果能早点读到这本书,该少走多少弯路啊!
这也是一本深入浅出的书,讲述概念的来龙去脉而不是卖弄定义,介绍事物发展演化历程而不是事实堆砌,将复杂的概念讲述的浅显易懂,但又直击本质不失深度。每章最后的引用质量非常好,是深入学习各个主题的绝佳索引。
本书为数据系统的设计、实现、与评价提供了很好的概念框架。读完并理解本书内容后,读者可以轻松看破大多数的技术忽悠,与技术砖家撕起来虎虎生风。
这是 2017 年译者读过最好的一本技术类书籍,这么好的书没有中文翻译,实在是遗憾。某不才,愿为先进技术文化的传播贡献一份力量。既可以深入学习有趣的技术主题,又可以锻炼中英文语言文字功底,何乐而不为?
## 前言
> 在我们的社会中,技术是一种强大的力量。数据、软件、通信可以用于坏的方面:不公平的阶级固化,损害公民权利,保护既得利益集团。但也可以用于好的方面:让底层人民发出自己的声音,让每个人都拥有机会,避免灾难。本书献给所有将技术用于善途的人们。
> 计算是一种流行文化,流行文化鄙视历史。流行文化关乎个体身份和参与感,但与合作无关。流行文化活在当下,也与过去和未来无关。我认为大部分(为了钱)编写代码的人就是这样的,他们不知道自己的文化来自哪里。
>
> —— 阿兰・凯接受 Dobb 博士的杂志采访时2012 年)
## 目录
### [序言](/preface)
### [第一部分:数据系统基础](/part-i)
* [第一章:数据系统架构中的利弊权衡](/v2/ch1)
* [第二章:定义非功能性要求](/v2/ch2)
* [第三章:数据模型与查询语言](/v2/ch3)
* [第四章:编码与演化](/v2/ch4) TBD
### [第二部分:分布式数据](/part-ii)
* [第五章:复制](/v2/ch5)TBD
* [第六章:分区](/v2/ch6)TBD
* [第七章:事务](/v2/ch7)TBD
* [第八章:分布式系统的麻烦](/v2/ch8)TBD
* [第九章:一致性与共识](/v2/ch9)TBD
### [第三部分:衍生数据](/part-iii)
* [第十章:批处理](/v2/ch10)(尚未发布)
* [第十一章:流处理](/v2/ch11)(尚未发布)
* [第十二章:数据系统的未来](/v2/ch12)(尚未发布)
### [术语表](/v2/glossary)
### [后记](/v2/colophon)
<br>
---------
## 法律声明
从原作者处得知,已经有简体中文的翻译计划,将于 2018 年末完成。[购买地址](https://search.jd.com/Search?keyword=设计数据密集型应用)
译者纯粹出于 **学习目的****个人兴趣** 翻译本书,不追求任何经济利益。
译者保留对此版本译文的署名权,其他权利以原作者和出版社的主张为准。
本译文只供学习研究参考之用,不得公开传播发行或用于商业用途。有能力阅读英文书籍者请购买正版支持。
---------
## 贡献
0. 全文校订 by [@yingang](https://github.com/Vonng/ddia/commits?author=yingang)
1. [序言初翻修正](https://github.com/Vonng/ddia/commit/afb5edab55c62ed23474149f229677e3b42dfc2c) by [@seagullbird](https://github.com/Vonng/ddia/commits?author=seagullbird)
2. [第一章语法标点校正](https://github.com/Vonng/ddia/commit/973b12cd8f8fcdf4852f1eb1649ddd9d187e3644) by [@nevertiree](https://github.com/Vonng/ddia/commits?author=nevertiree)
3. [第六章部分校正](https://github.com/Vonng/ddia/commit/d4eb0852c0ec1e93c8aacc496c80b915bb1e6d48) 与[第十章的初翻](https://github.com/Vonng/ddia/commit/9de8dbd1bfe6fbb03b3bf6c1a1aa2291aed2490e) by [@MuAlex](https://github.com/Vonng/ddia/commits?author=MuAlex)
4. [第一部分](/part-i)前言,[ch2](/v2/ch2)校正 by [@jiajiadebug](https://github.com/Vonng/ddia/commits?author=jiajiadebug)
5. [词汇表](/glossary)、[后记](/colophon)关于野猪的部分 by [@Chowss](https://github.com/Vonng/ddia/commits?author=Chowss)
6. [繁體中文](https://github.com/Vonng/ddia/pulls)版本与转换脚本 by [@afunTW](https://github.com/afunTW)
7. 多处翻译修正 by [@songzhibin97](https://github.com/Vonng/ddia/commits?author=songzhibin97) [@MamaShip](https://github.com/Vonng/ddia/commits?author=MamaShip) [@FangYuan33](https://github.com/Vonng/ddia/commits?author=FangYuan33)
8. [感谢所有作出贡献,提出意见的朋友们](/contrib)
<details>
<summary><a href="https://github.com/Vonng/ddia/pulls">Pull Requests</a> & <a href="https://github.com/Vonng/ddia/issues">Issues</a></summary>
| ISSUE & Pull Requests | USER | Title |
|-------------------------------------------------|------------------------------------------------------------|----------------------------------------------------------------|
| [359](https://github.com/Vonng/ddia/pull/359) | [@c25423](https://github.com/c25423) | ch10: 修正一处拼写错误 |
| [358](https://github.com/Vonng/ddia/pull/358) | [@lewiszlw](https://github.com/lewiszlw) | ch4: 修正一处拼写错误 |
| [356](https://github.com/Vonng/ddia/pull/356) | [@lewiszlw](https://github.com/lewiszlw) | ch2: 修正一处标点错误 |
| [355](https://github.com/Vonng/ddia/pull/355) | [@DuroyGeorge](https://github.com/DuroyGeorge) | ch12: 修正一处格式错误 |
| [354](https://github.com/Vonng/ddia/pull/354) | [@justlorain](https://github.com/justlorain) | ch7: 修正一处参考链接 |
| [353](https://github.com/Vonng/ddia/pull/353) | [@fantasyczl](https://github.com/fantasyczl) | ch3&9: 修正两处引用错误 |
| [352](https://github.com/Vonng/ddia/pull/352) | [@fantasyczl](https://github.com/fantasyczl) | 支持输出为 EPUB 格式 |
| [349](https://github.com/Vonng/ddia/pull/349) | [@xiyihan0](https://github.com/xiyihan0) | ch1: 修正一处格式错误 |
| [348](https://github.com/Vonng/ddia/pull/348) | [@omegaatt36](https://github.com/omegaatt36) | ch3: 修正一处图像链接 |
| [346](https://github.com/Vonng/ddia/issues/346) | [@Vermouth1995](https://github.com/Vermouth1995) | ch1: 优化一处翻译 |
| [343](https://github.com/Vonng/ddia/pull/343) | [@kehao-chen](https://github.com/kehao-chen) | ch10: 优化一处翻译 |
| [341](https://github.com/Vonng/ddia/pull/341) | [@YKIsTheBest](https://github.com/YKIsTheBest) | ch3: 优化两处翻译 |
| [340](https://github.com/Vonng/ddia/pull/340) | [@YKIsTheBest](https://github.com/YKIsTheBest) | ch2: 优化多处翻译 |
| [338](https://github.com/Vonng/ddia/pull/338) | [@YKIsTheBest](https://github.com/YKIsTheBest) | ch1: 优化一处翻译 |
| [335](https://github.com/Vonng/ddia/pull/335) | [@kimi0230](https://github.com/kimi0230) | 修正一处繁体中文错误 |
| [334](https://github.com/Vonng/ddia/pull/334) | [@soulrrrrr](https://github.com/soulrrrrr) | ch2: 修正一处繁体中文错误 |
| [332](https://github.com/Vonng/ddia/pull/332) | [@justlorain](https://github.com/justlorain) | ch5: 修正一处翻译错误 |
| [331](https://github.com/Vonng/ddia/pull/331) | [@Lyianu](https://github.com/Lyianu) | ch9: 更正几处拼写错误 |
| [330](https://github.com/Vonng/ddia/pull/330) | [@Lyianu](https://github.com/Lyianu) | ch7: 优化一处翻译 |
| [329](https://github.com/Vonng/ddia/issues/329) | [@Lyianu](https://github.com/Lyianu) | ch6: 指出一处翻译错误 |
| [328](https://github.com/Vonng/ddia/pull/328) | [@justlorain](https://github.com/justlorain) | ch4: 更正一处翻译遗漏 |
| [326](https://github.com/Vonng/ddia/pull/326) | [@liangGTY](https://github.com/liangGTY) | ch1: 优化一处翻译 |
| [323](https://github.com/Vonng/ddia/pull/323) | [@marvin263](https://github.com/marvin263) | ch5: 优化一处翻译 |
| [322](https://github.com/Vonng/ddia/pull/322) | [@marvin263](https://github.com/marvin263) | ch8: 优化一处翻译 |
| [304](https://github.com/Vonng/ddia/pull/304) | [@spike014](https://github.com/spike014) | ch11: 优化一处翻译 |
| [298](https://github.com/Vonng/ddia/pull/298) | [@Makonike](https://github.com/Makonike) | ch11&12: 修正两处错误 |
| [284](https://github.com/Vonng/ddia/pull/284) | [@WAangzE](https://github.com/WAangzE) | ch4: 更正一处列表错误 |
| [283](https://github.com/Vonng/ddia/pull/283) | [@WAangzE](https://github.com/WAangzE) | ch3: 更正一处错别字 |
| [282](https://github.com/Vonng/ddia/pull/282) | [@WAangzE](https://github.com/WAangzE) | ch2: 更正一处公式问题 |
| [281](https://github.com/Vonng/ddia/pull/281) | [@lyuxi99](https://github.com/lyuxi99) | 更正多处内部链接错误 |
| [280](https://github.com/Vonng/ddia/pull/280) | [@lyuxi99](https://github.com/lyuxi99) | ch9: 更正内部链接错误 |
| [279](https://github.com/Vonng/ddia/issues/279) | [@codexvn](https://github.com/codexvn) | ch9: 指出公式在 GitHub Pages 显示的问题 |
| [278](https://github.com/Vonng/ddia/pull/278) | [@LJlkdskdjflsa](https://github.com/LJlkdskdjflsa) | 发现了繁体中文版本中的错误翻译 |
| [275](https://github.com/Vonng/ddia/pull/275) | [@117503445](https://github.com/117503445) | 更正 LICENSE 链接 |
| [274](https://github.com/Vonng/ddia/pull/274) | [@uncle-lv](https://github.com/uncle-lv) | ch7: 修正错别字 |
| [273](https://github.com/Vonng/ddia/pull/273) | [@Sdot-Python](https://github.com/Sdot-Python) | ch7: 统一了 write skew 的翻译 |
| [271](https://github.com/Vonng/ddia/pull/271) | [@Makonike](https://github.com/Makonike) | ch6: 统一了 rebalancing 的翻译 |
| [270](https://github.com/Vonng/ddia/pull/270) | [@Ynjxsjmh](https://github.com/Ynjxsjmh) | ch7: 修正不一致的翻译 |
| [263](https://github.com/Vonng/ddia/pull/263) | [@zydmayday](https://github.com/zydmayday) | ch5: 修正译文中的重复单词 |
| [260](https://github.com/Vonng/ddia/pull/260) | [@haifeiWu](https://github.com/haifeiWu) | ch4: 修正部分不准确的翻译 |
| [258](https://github.com/Vonng/ddia/pull/258) | [@bestgrc](https://github.com/bestgrc) | ch3: 修正一处翻译错误 |
| [257](https://github.com/Vonng/ddia/pull/257) | [@UnderSam](https://github.com/UnderSam) | ch8: 修正一处拼写错误 |
| [256](https://github.com/Vonng/ddia/pull/256) | [@AlphaWang](https://github.com/AlphaWang) | ch7: 修正“可串行化”相关内容的多处翻译不当 |
| [255](https://github.com/Vonng/ddia/pull/255) | [@AlphaWang](https://github.com/AlphaWang) | ch7: 修正“可重复读”相关内容的多处翻译不当 |
| [253](https://github.com/Vonng/ddia/pull/253) | [@AlphaWang](https://github.com/AlphaWang) | ch7: 修正“读已提交”相关内容的多处翻译不当 |
| [246](https://github.com/Vonng/ddia/pull/246) | [@derekwu0101](https://github.com/derekwu0101) | ch3: 修正繁体中文的转译错误 |
| [245](https://github.com/Vonng/ddia/pull/245) | [@skyran1278](https://github.com/skyran1278) | ch12: 修正繁体中文的转译错误 |
| [244](https://github.com/Vonng/ddia/pull/244) | [@Axlgrep](https://github.com/Axlgrep) | ch9: 修正不通顺的翻译 |
| [242](https://github.com/Vonng/ddia/pull/242) | [@lynkeib](https://github.com/lynkeib) | ch9: 修正不通顺的翻译 |
| [241](https://github.com/Vonng/ddia/pull/241) | [@lynkeib](https://github.com/lynkeib) | ch8: 修正不正确的公式格式 |
| [240](https://github.com/Vonng/ddia/pull/240) | [@8da2k](https://github.com/8da2k) | ch9: 修正不通顺的翻译 |
| [239](https://github.com/Vonng/ddia/pull/239) | [@BeBraveBeCurious](https://github.com/BeBraveBeCurious) | ch7: 修正不一致的翻译 |
| [237](https://github.com/Vonng/ddia/pull/237) | [@zhangnew](https://github.com/zhangnew) | ch3: 修正错误的图片链接 |
| [229](https://github.com/Vonng/ddia/pull/229) | [@lis186](https://github.com/lis186) | 指出繁体中文的转译错误:复杂 |
| [226](https://github.com/Vonng/ddia/pull/226) | [@chroming](https://github.com/chroming) | ch1: 修正导航栏中的章节名称 |
| [220](https://github.com/Vonng/ddia/pull/220) | [@skyran1278](https://github.com/skyran1278) | ch9: 修正线性一致的繁体中文翻译 |
| [194](https://github.com/Vonng/ddia/pull/194) | [@BeBraveBeCurious](https://github.com/BeBraveBeCurious) | ch4: 修正错误的翻译 |
| [193](https://github.com/Vonng/ddia/pull/193) | [@BeBraveBeCurious](https://github.com/BeBraveBeCurious) | ch4: 优化译文 |
| [192](https://github.com/Vonng/ddia/pull/192) | [@BeBraveBeCurious](https://github.com/BeBraveBeCurious) | ch4: 修正不一致和不通顺的翻译 |
| [190](https://github.com/Vonng/ddia/pull/190) | [@Pcrab](https://github.com/Pcrab) | ch1: 修正不准确的翻译 |
| [187](https://github.com/Vonng/ddia/pull/187) | [@narojay](https://github.com/narojay) | ch9: 修正生硬的翻译 |
| [186](https://github.com/Vonng/ddia/pull/186) | [@narojay](https://github.com/narojay) | ch8: 修正错别字 |
| [185](https://github.com/Vonng/ddia/issues/185) | [@8da2k](https://github.com/8da2k) | 指出小标题跳转的问题 |
| [184](https://github.com/Vonng/ddia/pull/184) | [@DavidZhiXing](https://github.com/DavidZhiXing) | ch10: 修正失效的网址 |
| [183](https://github.com/Vonng/ddia/pull/183) | [@OneSizeFitsQuorum](https://github.com/OneSizeFitsQuorum) | ch8: 修正错别字 |
| [182](https://github.com/Vonng/ddia/issues/182) | [@lroolle](https://github.com/lroolle) | 建议docsify的主题风格 |
| [181](https://github.com/Vonng/ddia/pull/181) | [@YunfengGao](https://github.com/YunfengGao) | ch2: 修正翻译错误 |
| [180](https://github.com/Vonng/ddia/pull/180) | [@skyran1278](https://github.com/skyran1278) | ch3: 指出繁体中文的转译错误 |
| [177](https://github.com/Vonng/ddia/pull/177) | [@exzhawk](https://github.com/exzhawk) | 支持 Github Pages 里的公式显示 |
| [176](https://github.com/Vonng/ddia/pull/176) | [@haifeiWu](https://github.com/haifeiWu) | ch2: 语义网相关翻译更正 |
| [175](https://github.com/Vonng/ddia/pull/175) | [@cwr31](https://github.com/cwr31) | ch7: 不变式相关翻译更正 |
| [174](https://github.com/Vonng/ddia/pull/174) | [@BeBraveBeCurious](https://github.com/BeBraveBeCurious) | README & preface: 更正不正确的中文用词和标点符号 |
| [173](https://github.com/Vonng/ddia/pull/173) | [@ZvanYang](https://github.com/ZvanYang) | ch12: 修正不完整的翻译 |
| [171](https://github.com/Vonng/ddia/pull/171) | [@ZvanYang](https://github.com/ZvanYang) | ch12: 修正重复的译文 |
| [169](https://github.com/Vonng/ddia/pull/169) | [@ZvanYang](https://github.com/ZvanYang) | ch12: 更正不太通顺的翻译 |
| [166](https://github.com/Vonng/ddia/pull/166) | [@bp4m4h94](https://github.com/bp4m4h94) | ch1: 发现错误的文献索引 |
| [164](https://github.com/Vonng/ddia/pull/164) | [@DragonDriver](https://github.com/DragonDriver) | preface: 更正错误的标点符号 |
| [163](https://github.com/Vonng/ddia/pull/163) | [@llmmddCoder](https://github.com/llmmddCoder) | ch1: 更正错误字 |
| [160](https://github.com/Vonng/ddia/pull/160) | [@Zhayhp](https://github.com/Zhayhp) | ch2: 建议将 network model 翻译为网状模型 |
| [159](https://github.com/Vonng/ddia/pull/159) | [@1ess](https://github.com/1ess) | ch4: 更正错误字 |
| [157](https://github.com/Vonng/ddia/pull/157) | [@ZvanYang](https://github.com/ZvanYang) | ch7: 更正不太通顺的翻译 |
| [155](https://github.com/Vonng/ddia/pull/155) | [@ZvanYang](https://github.com/ZvanYang) | ch7: 更正不太通顺的翻译 |
| [153](https://github.com/Vonng/ddia/pull/153) | [@DavidZhiXing](https://github.com/DavidZhiXing) | ch9: 修正缩略图的错别字 |
| [152](https://github.com/Vonng/ddia/pull/152) | [@ZvanYang](https://github.com/ZvanYang) | ch7: 除重->去重 |
| [151](https://github.com/Vonng/ddia/pull/151) | [@ZvanYang](https://github.com/ZvanYang) | ch5: 修订sibling相关的翻译 |
| [147](https://github.com/Vonng/ddia/pull/147) | [@ZvanYang](https://github.com/ZvanYang) | ch5: 更正一处不准确的翻译 |
| [145](https://github.com/Vonng/ddia/pull/145) | [@Hookey](https://github.com/Hookey) | 识别了当前简繁转译过程中处理不当的地方,暂通过转换脚本规避 |
| [144](https://github.com/Vonng/ddia/issues/144) | [@secret4233](https://github.com/secret4233) | ch7: 不翻译`next-key locking` |
| [143](https://github.com/Vonng/ddia/issues/143) | [@imcheney](https://github.com/imcheney) | ch3: 更新残留的机翻段落 |
| [142](https://github.com/Vonng/ddia/issues/142) | [@XIJINIAN](https://github.com/XIJINIAN) | 建议去除段首的制表符 |
| [141](https://github.com/Vonng/ddia/issues/141) | [@Flyraty](https://github.com/Flyraty) | ch5: 发现一处错误格式的章节引用 |
| [140](https://github.com/Vonng/ddia/pull/140) | [@Bowser1704](https://github.com/Bowser1704) | ch5: 修正章节Summary中多处不通顺的翻译 |
| [139](https://github.com/Vonng/ddia/pull/139) | [@Bowser1704](https://github.com/Bowser1704) | ch2&ch3: 修正多处不通顺的或错误的翻译 |
| [137](https://github.com/Vonng/ddia/pull/137) | [@fuxuemingzhu](https://github.com/fuxuemingzhu) | ch5&ch6: 优化多处不通顺的或错误的翻译 |
| [134](https://github.com/Vonng/ddia/pull/134) | [@fuxuemingzhu](https://github.com/fuxuemingzhu) | ch4: 优化多处不通顺的或错误的翻译 |
| [133](https://github.com/Vonng/ddia/pull/133) | [@fuxuemingzhu](https://github.com/fuxuemingzhu) | ch3: 优化多处错误的或不通顺的翻译 |
| [132](https://github.com/Vonng/ddia/pull/132) | [@fuxuemingzhu](https://github.com/fuxuemingzhu) | ch3: 优化一处容易产生歧义的翻译 |
| [131](https://github.com/Vonng/ddia/pull/131) | [@rwwg4](https://github.com/rwwg4) | ch6: 修正两处错误的翻译 |
| [129](https://github.com/Vonng/ddia/pull/129) | [@anaer](https://github.com/anaer) | ch4: 修正两处强调文本和四处代码变量名称 |
| [128](https://github.com/Vonng/ddia/pull/128) | [@meilin96](https://github.com/meilin96) | ch5: 修正一处错误的引用 |
| [126](https://github.com/Vonng/ddia/pull/126) | [@cwr31](https://github.com/cwr31) | ch10: 修正一处错误的翻译(功能 -> 函数) |
| [125](https://github.com/Vonng/ddia/pull/125) | [@dch1228](https://github.com/dch1228) | ch2: 优化 how best 的翻译(如何以最佳方式) |
| [123](https://github.com/Vonng/ddia/pull/123) | [@yingang](https://github.com/yingang) | translation updates (chapter 9, TOC in readme, glossary, etc.) |
| [121](https://github.com/Vonng/ddia/pull/121) | [@yingang](https://github.com/yingang) | translation updates (chapter 5 to chapter 8) |
| [120](https://github.com/Vonng/ddia/pull/120) | [@jiong-han](https://github.com/jiong-han) | Typo fix: 呲之以鼻 -> 嗤之以鼻 |
| [119](https://github.com/Vonng/ddia/pull/119) | [@cclauss](https://github.com/cclauss) | Streamline file operations in convert() |
| [118](https://github.com/Vonng/ddia/pull/118) | [@yingang](https://github.com/yingang) | translation updates (chapter 2 to chapter 4) |
| [117](https://github.com/Vonng/ddia/pull/117) | [@feeeei](https://github.com/feeeei) | 统一每章的标题格式 |
| [115](https://github.com/Vonng/ddia/pull/115) | [@NageNalock](https://github.com/NageNalock) | 第七章病句修改: 重复词语 |
| [114](https://github.com/Vonng/ddia/pull/114) | [@Sunt-ing](https://github.com/Sunt-ing) | Update README.md: correct the book name |
| [113](https://github.com/Vonng/ddia/pull/113) | [@lpxxn](https://github.com/lpxxn) | 修改语句 |
| [112](https://github.com/Vonng/ddia/pull/112) | [@ibyte2011](https://github.com/ibyte2011) | Update ch9.md |
| [110](https://github.com/Vonng/ddia/pull/110) | [@lpxxn](https://github.com/lpxxn) | 读已写入数据 |
| [107](https://github.com/Vonng/ddia/pull/107) | [@abbychau](https://github.com/abbychau) | 單調鐘和好死还是赖活着 |
| [106](https://github.com/Vonng/ddia/pull/106) | [@enochii](https://github.com/enochii) | typo in ch2: fix braces typo |
| [105](https://github.com/Vonng/ddia/pull/105) | [@LiminCode](https://github.com/LiminCode) | Chronicle translation error |
| [104](https://github.com/Vonng/ddia/pull/104) | [@Sunt-ing](https://github.com/Sunt-ing) | several advice for better translation |
| [103](https://github.com/Vonng/ddia/pull/103) | [@Sunt-ing](https://github.com/Sunt-ing) | typo in ch4: should be 完成 rather than 完全 |
| [102](https://github.com/Vonng/ddia/pull/102) | [@Sunt-ing](https://github.com/Sunt-ing) | ch4: better-translation: 扼杀 → 破坏 |
| [101](https://github.com/Vonng/ddia/pull/101) | [@Sunt-ing](https://github.com/Sunt-ing) | typo in Ch4: should be "改变" rathr than "盖面" |
| [100](https://github.com/Vonng/ddia/pull/100) | [@LiminCode](https://github.com/LiminCode) | fix missing translation |
| [99 ](https://github.com/Vonng/ddia/pull/99) | [@mrdrivingduck](https://github.com/mrdrivingduck) | ch6: fix the word rebalancing |
| [98 ](https://github.com/Vonng/ddia/pull/98) | [@jacklightChen](https://github.com/jacklightChen) | fix ch7.md: fix wrong references |
| [97 ](https://github.com/Vonng/ddia/pull/97) | [@jenac](https://github.com/jenac) | 96 |
| [96 ](https://github.com/Vonng/ddia/pull/96) | [@PragmaTwice](https://github.com/PragmaTwice) | ch2: fix typo about 'may or may not be' |
| [95 ](https://github.com/Vonng/ddia/pull/95) | [@EvanMu96](https://github.com/EvanMu96) | fix translation of "the battle cry" in ch5 |
| [94 ](https://github.com/Vonng/ddia/pull/94) | [@kemingy](https://github.com/kemingy) | ch6: fix markdown and punctuations |
| [93 ](https://github.com/Vonng/ddia/pull/93) | [@kemingy](https://github.com/kemingy) | ch5: fix markdown and some typos |
| [92 ](https://github.com/Vonng/ddia/pull/92) | [@Gilbert1024](https://github.com/Gilbert1024) | Merge pull request #1 from Vonng/master |
| [88 ](https://github.com/Vonng/ddia/pull/88) | [@kemingy](https://github.com/kemingy) | fix typo for ch1, ch2, ch3, ch4 |
| [87 ](https://github.com/Vonng/ddia/pull/87) | [@wynn5a](https://github.com/wynn5a) | Update ch3.md |
| [86 ](https://github.com/Vonng/ddia/pull/86) | [@northmorn](https://github.com/northmorn) | Update ch1.md |
| [85 ](https://github.com/Vonng/ddia/pull/85) | [@sunbuhui](https://github.com/sunbuhui) | fix ch2.md: fix ch2 ambiguous translation |
| [84 ](https://github.com/Vonng/ddia/pull/84) | [@ganler](https://github.com/ganler) | Fix translation: use up |
| [83 ](https://github.com/Vonng/ddia/pull/83) | [@afunTW](https://github.com/afunTW) | Using OpenCC to convert from zh-cn to zh-tw |
| [82 ](https://github.com/Vonng/ddia/pull/82) | [@kangni](https://github.com/kangni) | fix gitbook url |
| [78 ](https://github.com/Vonng/ddia/pull/78) | [@hanyu2](https://github.com/hanyu2) | Fix unappropriated translation |
| [77 ](https://github.com/Vonng/ddia/pull/77) | [@Ozarklake](https://github.com/Ozarklake) | fix typo |
| [75 ](https://github.com/Vonng/ddia/pull/75) | [@2997ms](https://github.com/2997ms) | Fix typo |
| [74 ](https://github.com/Vonng/ddia/pull/74) | [@2997ms](https://github.com/2997ms) | Update ch9.md |
| [70 ](https://github.com/Vonng/ddia/pull/70) | [@2997ms](https://github.com/2997ms) | Update ch7.md |
| [67 ](https://github.com/Vonng/ddia/pull/67) | [@jiajiadebug](https://github.com/jiajiadebug) | fix issues in ch2 - ch9 and glossary |
| [66 ](https://github.com/Vonng/ddia/pull/66) | [@blindpirate](https://github.com/blindpirate) | Fix typo |
| [63 ](https://github.com/Vonng/ddia/pull/63) | [@haifeiWu](https://github.com/haifeiWu) | Update ch10.md |
| [62 ](https://github.com/Vonng/ddia/pull/62) | [@ych](https://github.com/ych) | fix ch1.md typesetting problem |
| [61 ](https://github.com/Vonng/ddia/pull/61) | [@xianlaioy](https://github.com/xianlaioy) | docs:钟-->种去掉ou |
| [60 ](https://github.com/Vonng/ddia/pull/60) | [@Zombo1296](https://github.com/Zombo1296) | 否则 -> 或者 |
| [59 ](https://github.com/Vonng/ddia/pull/59) | [@AlexanderMisel](https://github.com/AlexanderMisel) | 呼叫->调用,显着->显著 |
| [58 ](https://github.com/Vonng/ddia/pull/58) | [@ibyte2011](https://github.com/ibyte2011) | Update ch8.md |
| [55 ](https://github.com/Vonng/ddia/pull/55) | [@saintube](https://github.com/saintube) | ch8: 修改链接错误 |
| [54 ](https://github.com/Vonng/ddia/pull/54) | [@Panmax](https://github.com/Panmax) | Update ch2.md |
| [53 ](https://github.com/Vonng/ddia/pull/53) | [@ibyte2011](https://github.com/ibyte2011) | Update ch9.md |
| [52 ](https://github.com/Vonng/ddia/pull/52) | [@hecenjie](https://github.com/hecenjie) | Update ch1.md |
| [51 ](https://github.com/Vonng/ddia/pull/51) | [@latavin243](https://github.com/latavin243) | fix 修正ch3 ch4几处翻译 |
| [50 ](https://github.com/Vonng/ddia/pull/50) | [@AlexZFX](https://github.com/AlexZFX) | 几个疏漏和格式错误 |
| [49 ](https://github.com/Vonng/ddia/pull/49) | [@haifeiWu](https://github.com/haifeiWu) | Update ch1.md |
| [48 ](https://github.com/Vonng/ddia/pull/48) | [@scaugrated](https://github.com/scaugrated) | fix typo |
| [47 ](https://github.com/Vonng/ddia/pull/47) | [@lzwill](https://github.com/lzwill) | Fixed typos in ch2 |
| [45 ](https://github.com/Vonng/ddia/pull/45) | [@zenuo](https://github.com/zenuo) | 删除一个多余的右括号 |
| [44 ](https://github.com/Vonng/ddia/pull/44) | [@akxxsb](https://github.com/akxxsb) | 修正第七章底部链接错误 |
| [43 ](https://github.com/Vonng/ddia/pull/43) | [@baijinping](https://github.com/baijinping) | "更假简单"->"更加简单" |
| [42 ](https://github.com/Vonng/ddia/pull/42) | [@tisonkun](https://github.com/tisonkun) | 修复 ch1 中的无序列表格式 |
| [38 ](https://github.com/Vonng/ddia/pull/38) | [@renjie-c](https://github.com/renjie-c) | 纠正多处的翻译小错误 |
| [37 ](https://github.com/Vonng/ddia/pull/37) | [@tankilo](https://github.com/tankilo) | fix translation mistakes in ch4.md |
| [36 ](https://github.com/Vonng/ddia/pull/36) | [@wwek](https://github.com/wwek) | 1.修复多个链接错误 2.名词优化修订 3.错误修订 |
| [35 ](https://github.com/Vonng/ddia/pull/35) | [@wwek](https://github.com/wwek) | fix ch7.md to ch8.md link error |
| [34 ](https://github.com/Vonng/ddia/pull/34) | [@wwek](https://github.com/wwek) | Merge pull request #1 from Vonng/master |
| [33 ](https://github.com/Vonng/ddia/pull/33) | [@wwek](https://github.com/wwek) | fix part-ii.md link error |
| [32 ](https://github.com/Vonng/ddia/pull/32) | [@JCYoky](https://github.com/JCYoky) | Update ch2.md |
| [31 ](https://github.com/Vonng/ddia/pull/31) | [@elsonLee](https://github.com/elsonLee) | Update ch7.md |
| [26 ](https://github.com/Vonng/ddia/pull/26) | [@yjhmelody](https://github.com/yjhmelody) | 修复一些明显错误 |
| [25 ](https://github.com/Vonng/ddia/pull/25) | [@lqbilbo](https://github.com/lqbilbo) | 修复链接错误 |
| [24 ](https://github.com/Vonng/ddia/pull/24) | [@artiship](https://github.com/artiship) | 修改词语顺序 |
| [23 ](https://github.com/Vonng/ddia/pull/23) | [@artiship](https://github.com/artiship) | 修正错别字 |
| [22 ](https://github.com/Vonng/ddia/pull/22) | [@artiship](https://github.com/artiship) | 纠正翻译错误 |
| [21 ](https://github.com/Vonng/ddia/pull/21) | [@zhtisi](https://github.com/zhtisi) | 修正目录和本章标题不符的情况 |
| [20 ](https://github.com/Vonng/ddia/pull/20) | [@rentiansheng](https://github.com/rentiansheng) | Update ch7.md |
| [19 ](https://github.com/Vonng/ddia/pull/19) | [@LHRchina](https://github.com/LHRchina) | 修复语句小bug |
| [16 ](https://github.com/Vonng/ddia/pull/16) | [@MuAlex](https://github.com/MuAlex) | Master |
| [15 ](https://github.com/Vonng/ddia/pull/15) | [@cg-zhou](https://github.com/cg-zhou) | Update translation progress |
| [14 ](https://github.com/Vonng/ddia/pull/14) | [@cg-zhou](https://github.com/cg-zhou) | Translate glossary |
| [13 ](https://github.com/Vonng/ddia/pull/13) | [@cg-zhou](https://github.com/cg-zhou) | 详细修改了后记中和印度野猪相关的描述 |
| [12 ](https://github.com/Vonng/ddia/pull/12) | [@ibyte2011](https://github.com/ibyte2011) | 修改了部分翻译 |
| [11 ](https://github.com/Vonng/ddia/pull/11) | [@jiajiadebug](https://github.com/jiajiadebug) | ch2 100% |
| [10 ](https://github.com/Vonng/ddia/pull/10) | [@jiajiadebug](https://github.com/jiajiadebug) | ch2 20% |
| [9 ](https://github.com/Vonng/ddia/pull/9) | [@jiajiadebug](https://github.com/jiajiadebug) | Preface, ch1, part-i translation minor fixes |
| [7 ](https://github.com/Vonng/ddia/pull/7) | [@MuAlex](https://github.com/MuAlex) | Ch6 translation pull request |
| [6 ](https://github.com/Vonng/ddia/pull/6) | [@MuAlex](https://github.com/MuAlex) | Ch6 change version1 |
| [5 ](https://github.com/Vonng/ddia/pull/5) | [@nevertiree](https://github.com/nevertiree) | Chapter 01语法微调 |
| [2 ](https://github.com/Vonng/ddia/pull/2) | [@seagullbird](https://github.com/seagullbird) | 序言初翻 |
</details><br />
---------
## 许可证
本项目采用 [CC-BY 4.0](https://github.com/Vonng/ddia/blob/master/LICENSE) 许可证,您可以在这里找到完整说明:
- [署名 4.0 协议国际版 CC BY 4.0 Deed](https://creativecommons.org/licenses/by/4.0/deed.zh-hans)
- [Attribution 4.0 International CC BY 4.0](https://creativecommons.org/licenses/by/4.0/deed.en)

875
content/v2/ch1.md Normal file
View File

@@ -0,0 +1,875 @@
---
title: "第一章:数据系统架构中的利弊权衡"
linkTitle: "1. 数据系统架构中的利弊权衡"
weight: 101
breadcrumbs: false
---
![img](/img/ch1.png)
> *没有解决方案,只有利弊权衡。[…] 尽你所能获取最好的利弊权衡,这是你唯一能指望的事。*
>
> [Thomas Sowell](https://www.youtube.com/watch?v=2YUtKr8-_Fg), 与 Fred Barnes 的采访 (2005)
数据在今天的许多应用程序开发中居于核心地位。随着网络和移动应用、软件即服务SaaS以及云服务的普及将来自不同用户的数据存储在共享的基于服务器的数据基础设施中已成为常态。需要存储和供分析使用的数据包括用户活动、商业交易、设备和传感器的数据。当用户与应用程序交互时他们既读取存储的数据也生成更多数据。
小量数据,可在单一机器上存储和处理,通常相对容易处理。然而,随着数据量或查询率的增加,需要将数据分布到多台机器上,这引入了许多挑战。随着应用程序需求的复杂化,仅在一个系统中存储所有数据已不再足够,可能需要结合多个提供不同功能的存储或处理系统。
如果数据管理是开发应用程序的主要挑战之一,我们称这类应用为*数据密集型* [[1](ch01.html#Kouzes2009)]。而在*计算密集型*系统中,挑战在于并行处理一些非常大的计算,在数据密集型应用中,我们通常更关心的是如何存储和处理大数据量、管理数据变化、在出现故障和并发时确保一致性以及确保服务的高可用性。
这类应用通常由提供常用功能的标准构建块构成。例如,许多应用需要:
- 存储数据,以便它们或其他应用程序稍后可以再次找到它(*数据库*
- 记住一次昂贵操作的结果,以加速读取(*缓存*
- 允许用户按关键词搜索数据或以各种方式过滤数据(*搜索索引*
- 当事件和数据变化发生时立即处理(*流处理*
- 定期处理大量积累的数据(*批处理*
在构建应用程序时,我们通常会采用几个软件系统或服务,如数据库或 API并用一些应用代码将它们粘合在一起。如果你完全按照数据系统的设计目的去做那么这个过程可能会非常容易。
然而,随着你的应用变得更加雄心勃勃,挑战也随之而来。有许多不同特性的数据库系统,适用于不同的目的——你该如何选择使用哪一个?有各种各样的缓存方法,几种构建搜索索引的方式等等——你该如何权衡它们的利弊?你需要弄清楚哪些工具和哪些方法最适合手头的任务,而且将多款工具组合起来以完成单独一款工具无法完成的事情也可能是很困难的。
本书是一本指南,旨在帮助你做出关于使用哪些技术以及如何组合它们的决策。正如你将看到的,没有任何一种方法从根本上比其他所有方法都好;每种方法都有其优缺点。通过这本书,你将学会提出正确的问题,以评估和比较数据系统,从而找出最适合你的特定应用需求的方法。
我们将从探索数据在当今组织中的典型使用方式开始我们的旅程。这里的许多想法起源于*企业软件*(即大型组织如大公司和政府的软件需求和工程实践),因为历史上只有大型组织拥有需要复杂技术解决方案的大数据量。如果你的数据量足够小,你甚至可以简单地将其保存在电子表格中!然而,最近,较小的公司和初创企业管理大数据量并构建数据密集型系统也变得普遍。
关于数据系统的一个关键挑战是,不同的人需要用数据做非常不同的事情。如果你在一家公司工作,你和你的团队会有一套优先事项,而另一个团队可能完全有不同的目标,尽管你们可能都在处理同一数据集!此外,这些目标可能不会明确表达,这可能会导致误解和对正确方法的争议。
为了帮助你了解你可以做出哪些选择,本章将比较几个对比概念,并探讨它们的利弊:
- 事务处理与分析之间的区别([“事务处理与分析”](#事务处理与分析)
- 云服务与自托管系统的优缺点([“云服务与自托管”](#云服务与自托管)
- 何时从单节点系统迁移到分布式系统([“分布式与单节点系统”](#分布式与单节点系统)
- 平衡业务需求与用户权利([“数据系统、法律与社会”](#数据系统法律与社会)
此外,本章将为我们接下来的书中的内容提供必需的术语。
Data is central to much application development today. With web and mobile apps, software as a service (SaaS), and cloud services, it has become normal to store data from many different users in a shared server-based data infrastructure. Data from user activity, business transactions, devices and sensors needs to be stored and made available for analysis. As users interact with an application, they both read the data that is stored, and also generate more data.
Small amounts of data, which can be stored and processed on a single machine, are often fairly easy to deal with. However, as the data volume or the rate of queries grows, it needs to be distributed across multiple machines, which introduces many challenges. As the needs of the application become more complex, it is no longer sufficient to store everything in one system, but it might be necessary to combine multiple storage or processing systems that provide different capabilities.
We call an application *data-intensive* if data management is one of the primary challenges in developing the application [[1](ch01.html#Kouzes2009)]. While in *compute-intensive* systems the challenge is parallelizing some very large computation, in data-intensive applications we usually worry more about things like storing and processing large data volumes, managing changes to data, ensuring consistency in the face of failures and concurrency, and making sure services are highly available.
Such applications are typically built from standard building blocks that provide commonly needed functionality. For example, many applications need to:
- Store data so that they, or another application, can find it again later (*databases*)
- Remember the result of an expensive operation, to speed up reads (*caches*)
- Allow users to search data by keyword or filter it in various ways (*search indexes*)
- Handle events and data changes as soon as they occur (*stream processing*)
- Periodically crunch a large amount of accumulated data (*batch processing*)
In building an application we typically take several software systems or services, such as databases or APIs, and glue them together with some application code. If you are doing exactly what the data systems were designed for, then this process can be quite easy.
However, as your application becomes more ambitious, challenges arise. There are many database systems with different characteristics, suitable for different purposes—how do you choose which one to use? There are various approaches to caching, several ways of building search indexes, and so on—how do you reason about their trade-offs? You need to figure out which tools and which approaches are the most appropriate for the task at hand, and it can be difficult to combine tools when you need to do something that a single tool cannot do alone.
This book is a guide to help you make decisions about which technologies to use and how to combine them. As you will see, there is no one approach that is fundamentally better than others; everything has pros and cons. With this book, you will learn to ask the right questions to evaluate and compare data systems, so that you can figure out which approach will best serve the needs of your particular application.
We will start our journey by looking at some of the ways that data is typically used in organizations today. Many of the ideas here have their origin in *enterprise software* (i.e., the software needs and engineering practices of large organizations, such as big corporations and governments), since historically, only large organizations had the large data volumes that required sophisticated technical solutions. If your data volume is small enough, you can simply keep it in a spreadsheet! However, more recently it has also become common for smaller companies and startups to manage large data volumes and build data-intensive systems.
One of the key challenges with data systems is that different people need to do very different things with data. If you are working at a company, you and your team will have one set of priorities, while another team may have entirely different goals, although you might even be working with the same dataset! Moreover, those goals might not be explicitly articulated, which can lead to misunderstandings and disagreement about the right approach.
To help you understand what choices you can make, this chapter compares several contrasting concepts, and explores their trade-offs:
- the difference between transaction processing and analytics ([“Transaction Processing versus Analytics”](ch01.html#sec_introduction_analytics));
- pros and cons of cloud services and self-hosted systems ([“Cloud versus Self-Hosting”](ch01.html#sec_introduction_cloud));
- when to move from single-node systems to distributed systems ([“Distributed versus Single-Node Systems”](ch01.html#sec_introduction_distributed)); and
- balancing the needs of the business and the rights of the user ([“Data Systems, Law, and Society”](ch01.html#sec_introduction_compliance)).
Moreover, this chapter will provide you with terminology that we will need for the rest of the book.
--------
### 术语:前端与后端
我们在本书中将讨论的许多内容涉及*后端开发*。解释该术语:对于网络应用程序,客户端代码(在网页浏览器中运行)被称为*前端*,处理用户请求的服务器端代码被称为*后端*。移动应用与前端类似,它们提供用户界面,通常通过互联网与服务器端后端通信。前端有时会在用户设备上本地管理数据[[2](ch01.html#Kleppmann2019)],但最大的数据基础设施挑战通常存在于后端:前端只需要处理一个用户的数据,而后端则代表*所有*用户管理数据。
后端服务通常可以通过 HTTP 访问;它通常包含一些应用程序代码,这些代码在一个或多个数据库中读写数据,有时还会与额外的数据系统(如缓存或消息队列)交互(我们可能统称为*数据基础设施*)。应用程序代码通常是*无状态的*(即,当它完成处理一个 HTTP 请求后,它会忘记该请求的所有信息),并且任何需要从一个请求传递到另一个请求的信息都需要存储在客户端或服务器端的数据基础设施中。
Much of what we will discuss in this book relates to *backend development*. To explain that term: for web applications, the client-side code (which runs in a web browser) is called the *frontend*, and the server-side code that handles user requests is known as the *backend*. Mobile apps are similar to frontends in that they provide user interfaces, which often communicate over the Internet with a server-side backend. Frontends sometimes manage data locally on the users device [[2](ch01.html#Kleppmann2019)], but the greatest data infrastructure challenges often lie in the backend: a frontend only needs to handle one users data, whereas the backend manages data on behalf of *all* of the users.
A backend service is often reachable via HTTP; it usually consists of some application code that reads and writes data in one or more databases, and sometimes interfaces with additional data systems such as caches or message queues (which we might collectively call *data infrastructure*). The application code is often *stateless* (i.e., when it finishes handling one HTTP request, it forgets everything about that request), and any information that needs to persist from one request to another needs to be stored either on the client, or in the server-side data infrastructure.
--------
## 事务处理与分析
如果你在企业中从事数据系统工作,你可能会遇到几种不同类型的处理数据的人。第一种是*后端工程师*,他们构建处理读取和更新数据请求的服务;这些服务通常直接或间接通过其他服务为外部用户提供服务(见[“微服务和无服务器”](ch01.html#sec_introduction_microservices))。有时服务是供组织内部其他部分使用的。
除了管理后端服务的团队外,还有两个群体通常需要访问组织的数据:*商业分析师*,他们生成有关组织活动的报告以帮助管理层做出更好的决策(*商业智能*或*BI*),以及*数据科学家*,他们在数据中寻找新的见解或创建由数据分析和机器学习/AI支持的面向用户的产品功能例如电子商务网站上的“购买 X 的人也购买了 Y”推荐、风险评分或垃圾邮件过滤等预测分析以及搜索结果的排名
尽管商业分析师和数据科学家倾向于使用不同的工具并以不同的方式操作,但他们有一些共同点:两者都进行*分析*,这意味着他们查看用户和后端服务生成的数据,但他们通常不修改这些数据(除了可能修正错误)。他们可能创建派生数据集,其中原始数据已以某种方式处理。这导致了两种系统之间的分离——这是我们将在整本书中使用的区分:
- *业务系统*包括后端服务和数据基础设施,数据是在那里创建的,例如通过服务外部用户。在这里,应用程序代码根据用户的操作读取并修改其数据库中的数据。
- *分析系统*满足商业分析师和数据科学家的需求。它们包含来自业务系统的数据的只读副本,并针对分析所需的数据处理类型进行了优化。
正如我们将在下一节中看到的,出于充分的理由,业务和分析系统通常保持独立。随着这些系统的成熟,出现了两个新的专业角色:*数据工程师*和*分析工程师*。数据工程师是了解如何整合业务和分析系统的人,他们负责组织的数据基础设施的更广泛管理[[3](ch01.html#Reis2022)]。分析工程师建模和转换数据,使其对查询组织中的数据的最终用户更有用[[4](ch01.html#Machado2023)]。
许多工程师专注于业务或分析的一侧。然而,这本书涵盖了业务和分析数据系统,因为两者在组织内的数据生命周期中都扮演着重要的角色。我们将深入探讨用于向内部和外部用户提供服务的数据基础设施,以便你能更好地与这一界限另一侧的同事合作。
If you are working on data systems in an enterprise, you are likely to encounter several different types of people who work with data. The first type are *backend engineers* who build services that handle requests for reading and updating data; these services often serve external users, either directly or indirectly via other services (see [“Microservices and Serverless”](ch01.html#sec_introduction_microservices)). Sometimes services are for internal use by other parts of the organization.
In addition to the teams managing backend services, two other groups of people typically require access to an organizations data: *business analysts*, who generate reports about the activities of the organization in order to help the management make better decisions (*business intelligence* or *BI*), and *data scientists*, who look for novel insights in data or who create user-facing product features that are enabled by data analysis and machine learning/AI (for example, “people who bought X also bought Y” recommendations on an e-commerce website, predictive analytics such as risk scoring or spam filtering, and ranking of search results).
Although business analysts and data scientists tend to use different tools and operate in different ways, they have some things in common: both perform *analytics*, which means they look at the data that the users and backend services have generated, but they generally do not modify this data (except perhaps for fixing mistakes). They might create derived datasets in which the original data has been processed in some way. This has led to a split between two types of systems—a distinction that we will use throughout this book:
- *Operational systems* consist of the backend services and data infrastructure where data is created, for example by serving external users. Here, the application code both reads and modifies the data in its databases, based on the actions performed by the users.
- *Analytical systems* serve the needs of business analysts and data scientists. They contain a read-only copy of the data from the operational systems, and they are optimized for the types of data processing that are needed for analytics.
As we shall see in the next section, operational and analytical systems are often kept separate, for good reasons. As these systems have matured, two new specialized roles have emerged: *data engineers* and *analytics engineers*. Data engineers are the people who know how to integrate the operational and the analytical systems, and who take responsibility for the organizations data infrastructure more widely [[3](ch01.html#Reis2022)]. Analytics engineers model and transform data to make it more useful for end users querying data in an organization [[4](ch01.html#Machado2023)].
Many engineers specialize on either the operational or the analytical side. However, this book covers both operational and analytical data systems, since both play an important role in the lifecycle of data within an organization. We will explore in-depth the data infrastructure that is used to deliver services both to internal and external users, so that you can work better with your colleagues on the other side of this divide.
### 分析与业务系统的特征
在商业数据处理的早期,数据库的写入通常对应于正在发生的*商业交易*:进行销售、向供应商下订单、支付员工的薪水等。随着数据库扩展到不涉及金钱交换的领域,*交易*一词仍然沿用,指的是构成逻辑单元的一组读写操作。
In the early days of business data processing, a write to the database typically corresponded to a *commercial transaction* taking place: making a sale, placing an order with a supplier, paying an employees salary, etc. As databases expanded into areas that didnt involve money changing hands, the term *transaction* nevertheless stuck, referring to a group of reads and writes that form a logical unit.
> **注意**
>
> [即将提供链接]将详细探讨我们对交易的定义。本章宽泛地使用这个术语,指代低延迟的读写操作。
尽管数据库开始被用于许多不同类型的数据——社交媒体上的帖子、游戏中的移动、地址簿中的联系人等——基本的访问模式仍与处理商业交易类似。业务系统通常通过某个键查找少量记录(这称为*点查询*)。根据用户的输入,记录被插入、更新或删除。因为这些应用是交互式的,这种访问模式被称为*在线事务处理*OLTP
然而,数据库也越来越多地被用于分析,其访问模式与 OLTP 有很大不同。通常,分析查询会扫描大量记录,并计算聚合统计数据(如计数、求和或平均值),而不是将个别记录返回给用户。例如,连锁超市的商业分析师可能希望回答诸如此类的问题:
- 我们的每家店在一月份的总收入是多少?
- 我们在最近的促销活动中卖出的香蕉比平时多多少?
- 哪种品牌的婴儿食品最常与某品牌的尿布一起购买?
这些类型的查询所产生的报告对于商业智能至关重要,帮助管理层决定下一步做什么。为了区分使用数据库的这种模式与事务处理的不同,它被称为*在线分析处理*OLAP[[5](ch01.html#Codd1993)]。OLTP 和分析之间的区别并不总是明确的,但[表1-1](ch01.html#tab_oltp_vs_olap)列出了一些典型的特征。
| 属性 | 业务系统 (OLTP) | 分析系统 (OLAP) |
|--------|----------------|---------------|
| 主要读取模式 | 点查询(按键提取个别记录) | 在大量记录上聚合 |
| 主要写入模式 | 创建、更新和删除个别记录 | 批量导入ETL或事件流 |
| 人类用户示例 | 网络/移动应用的终端用户 | 内部分析师,用于决策支持 |
| 机器使用示例 | 检查是否授权某项行动 | 检测欺诈/滥用模式 |
| 查询类型 | 固定的查询集合,由应用预定义 | 分析师可以进行任意查询 |
| 数据表示 | 数据的最新状态(当前时间点) | 随时间发生的事件历史 |
| 数据集大小 | 千兆字节至太字节 | 太字节至拍字节 |
[Link to Come] explores in detail what we mean with a transaction. This chapter uses the term loosely to refer to low-latency reads and writes.
Even though databases started being used for many different kinds of data—posts on social media, moves in a game, contacts in an address book, and many others—the basic access pattern remained similar to processing business transactions. An operational system typically looks up a small number of records by some key (this is called a *point query*). Records are inserted, updated, or deleted based on the users input. Because these applications are interactive, this access pattern became known as *online transaction processing* (OLTP).
However, databases also started being increasingly used for analytics, which has very different access patterns compared to OLTP. Usually an analytic query scans over a huge number of records, and calculates aggregate statistics (such as count, sum, or average) rather than returning the individual records to the user. For example, a business analyst at a supermarket chain may want to answer analytic queries such as:
- What was the total revenue of each of our stores in January?
- How many more bananas than usual did we sell during our latest promotion?
- Which brand of baby food is most often purchased together with brand X diapers?
The reports that result from these types of queries are important for business intelligence, helping the management decide what to do next. In order to differentiate this pattern of using databases from transaction processing, it has been called *online analytic processing* (OLAP) [[5](ch01.html#Codd1993)]. The difference between OLTP and analytics is not always clear-cut, but some typical characteristics are listed in [Table 1-1](ch01.html#tab_oltp_vs_olap).
| Property | Operational System (OLTP) | Analytical System (OLAP) |
|:--------------------|:------------------------------------------------|:------------------------------------------|
| Main read pattern | Point queries (fetch individual records by key) | Aggregate over large number of records |
| Main write pattern | Create, update, and delete individual records | Bulk import (ETL) or event stream |
| Human user example | End user of web/mobile application | Internal analyst, for decision support |
| Machine use example | Checking if an action is authorized | Detecting fraud/abuse patterns |
| Type of queries | Fixed set of queries, predefined by application | Analyst can make arbitrary queries |
| Data represents | Latest state of data (current point in time) | History of events that happened over time |
| Dataset size | Gigabytes to terabytes | Terabytes to petabytes |
> 注意
>
> *在线分析处理*中的*在线*一词的含义并不清晰;它可能指的是分析师不仅仅查询预定义的报告,而且还可以交互式地进行探索性的查询。
在业务系统中,用户通常不被允许构建自定义 SQL 查询并在数据库上运行因为这可能允许他们读取或修改他们无权访问的数据。此外他们可能编写执行成本高昂的查询从而影响其他用户的数据库性能。因此OLTP 系统大多运行固定的查询集,这些查询嵌入在应用程序代码中,仅偶尔使用一次性自定义查询进行维护或故障排除。另一方面,分析数据库通常允许用户手动编写任意 SQL 查询,或使用数据可视化或仪表板工具(如 Tableau、Looker 或 Microsoft Power BI自动生成查询。
The meaning of *online* in *OLAP* is unclear; it probably refers to the fact that queries are not just for predefined reports, but that analysts use the OLAP system interactively for explorative queries.
With operational systems, users are generally not allowed to construct custom SQL queries and run them on the database, since that would potentially allow them to read or modify data that they do not have permission to access. Moreover, they might write queries that are expensive to execute, and hence affect the database performance for other users. For these reasons, OLTP systems mostly run a fixed set of queries that are baked into the application code, and use one-off custom queries only occasionally for maintenance or troubleshooting. On the other hand, analytic databases usually give their users the freedom to write arbitrary SQL queries by hand, or to generate queries automatically using a data visualization or dashboard tool such as Tableau, Looker, or Microsoft Power BI.
### 数据仓库
起初同一数据库既用于交易处理也用于分析查询。SQL在这方面证明是相当灵活的它适用于两种类型的查询。然而在1980年代末和1990年代初公司停止使用OLTP系统进行分析目的并在单独的数据库系统上运行分析的趋势日益明显。这种单独的数据库被称为*数据仓库*。
一家大型企业可能有几十个甚至上百个操作性交易处理系统:支撑面向客户的网站、控制实体店的销售点(结账)系统、跟踪仓库库存、规划车辆路线、管理供应商、管理员工以及执行许多其他任务的系统。每个系统都很复杂,需要一个团队来维护,因此这些系统大多独立运行。
通常不希望商业分析师和数据科学家直接查询这些OLTP系统原因有几个
- 感兴趣的数据可能分布在多个业务系统中,将这些数据集合并到单一查询中很困难(一个称为*数据孤岛*的问题);
- 适合OLTP的模式和数据布局不太适合分析见[“星型和雪花型:分析的模式”](ch03.html#sec_datamodels_analytics)
- 分析查询可能相当昂贵如果在OLTP数据库上运行将影响其他用户的性能以及
- OLTP系统可能位于一个不允许用户直接访问的单独网络中出于安全或合规原因。
与此相反,*数据仓库*是一个单独的数据库分析师可以尽情查询而不影响OLTP操作[[6](ch01.html#Chaudhuri1997)]。正如我们将在[即将提供链接]中看到的数据仓库通常以与OLTP数据库非常不同的方式存储数据以优化常见于分析的查询类型。
数据仓库包含公司所有各种OLTP系统中的数据的只读副本。数据从OLTP数据库中提取使用定期数据转储或持续更新流转换成便于分析的模式清理后然后加载到数据仓库中。将数据获取到数据仓库的过程称为*提取-转换-加载*ETL并在[图1-1](ch01.html#fig_dwh_etl)中进行了说明。有时*转换*和*加载*的顺序被交换(即在数据仓库中加载后进行转换),这就变成了*ELT*。
At first, the same databases were used for both transaction processing and analytic queries. SQL turned out to be quite flexible in this regard: it works well for both types of queries. Nevertheless, in the late 1980s and early 1990s, there was a trend for companies to stop using their OLTP systems for analytics purposes, and to run the analytics on a separate database system instead. This separate database was called a *data warehouse*.
A large enterprise may have dozens, even hundreds, of operational transaction processing systems: systems powering the customer-facing website, controlling point of sale (checkout) systems in physical stores, tracking inventory in warehouses, planning routes for vehicles, managing suppliers, administering employees, and performing many other tasks. Each of these systems is complex and needs a team of people to maintain it, so these systems end up operating mostly independently from each other.
It is usually undesirable for business analysts and data scientists to directly query these OLTP systems, for several reasons:
- the data of interest may be spread across multiple operational systems, making it difficult to combine those datasets in a single query (a problem known as *data silos*);
- the kinds of schemas and data layouts that are good for OLTP are less well suited for analytics (see [“Stars and Snowflakes: Schemas for Analytics”](ch03.html#sec_datamodels_analytics));
- analytic queries can be quite expensive, and running them on an OLTP database would impact the performance for other users; and
- the OLTP systems might reside in a separate network that users are not allowed direct access to for security or compliance reasons.
A *data warehouse*, by contrast, is a separate database that analysts can query to their hearts content, without affecting OLTP operations [[6](ch01.html#Chaudhuri1997)]. As we shall see in [Link to Come], data warehouses often store data in a way that is very different from OLTP databases, in order to optimize for the types of queries that are common in analytics.
The data warehouse contains a read-only copy of the data in all the various OLTP systems in the company. Data is extracted from OLTP databases (using either a periodic data dump or a continuous stream of updates), transformed into an analysis-friendly schema, cleaned up, and then loaded into the data warehouse. This process of getting data into the data warehouse is known as *ExtractTransformLoad* (ETL) and is illustrated in [Figure 1-1](ch01.html#fig_dwh_etl). Sometimes the order of the *transform* and *load* steps is swapped (i.e., the transformation is done in the data warehouse, after loading), resulting in *ELT*.
![ddia 0308](img/ddia_0308.png)
###### 图1-1 数仓ETL简化框架
在某些情况下ETL过程的数据来源是外部的SaaS产品如客户关系管理CRM、电子邮件营销或信用卡处理系统。在这些情况下你无法直接访问原始数据库因为它只能通过软件供应商的API访问。将这些外部系统的数据引入你自己的数据仓库可以启用SaaS API无法实现的分析。对于SaaS API的ETL通常由专业的数据连接服务实现如Fivetran、Singer或AirByte。
有些数据库系统提供*混合事务/分析处理*HTAP旨在在单一系统中同时启用OLTP和分析无需从一个系统向另一个系统进行ETL [[7](ch01.html#Ozcan2017)[8](ch01.html#Prout2022)]。然而许多HTAP系统内部由一个OLTP系统与一个独立的分析系统组成这些系统通过一个公共界面隐藏——因此理解这两者之间的区别对于理解这些系统的工作方式非常重要。
此外尽管存在HTAP由于它们目标和要求的不同事务性和分析性系统之间的分离仍然很常见。特别是每个业务系统拥有自己的数据库被视为良好的实践见[“微服务与无服务器”](ch01.html#sec_introduction_microservices)),导致有数百个独立的操作数据库;另一方面,一个企业通常只有一个数据仓库,这样业务分析师可以在单个查询中合并来自几个业务系统的数据。
业务系统和分析系统之间的分离是一个更广泛趋势的一部分:随着工作负载变得更加苛刻,系统变得更加专业化,并为特定工作负载优化。通用系统可以舒适地处理小数据量,但规模越大,系统趋向于变得更加专业化 [[9](ch01.html#Stonebraker2005fitsall)]。
In some cases the data sources of the ETL processes are external SaaS products such as customer relationship management (CRM), email marketing, or credit card processing systems. In those cases, you do not have direct access to the original database, since it is accessible only via the software vendors API. Bringing the data from these external systems into your own data warehouse can enable analyses that are not possible via the SaaS API. ETL for SaaS APIs is often implemented by specialist data connector services such as Fivetran, Singer, or AirByte.
Some database systems offer *hybrid transactional/analytic processing* (HTAP), which aims to enable OLTP and analytics in a single system without requiring ETL from one system into another [[7](ch01.html#Ozcan2017), [8](ch01.html#Prout2022)]. However, many HTAP systems internally consist of an OLTP system coupled with a separate analytical system, hidden behind a common interface—so the distinction beween the two remains important for understanding how these systems work.
Moreover, even though HTAP exists, it is common to have a separation between transactional and analytic systems due to their different goals and requirements. In particular, it is considered good practice for each operational system to have its own database (see [“Microservices and Serverless”](ch01.html#sec_introduction_microservices)), leading to hundreds of separate operational databases; on the other hand, an enterprise usually has a single data warehouse, so that business analysts can combine data from several operational systems in a single query.
The separation between operational and analytical systems is part of a wider trend: as workloads have become more demanding, systems have become more specialized and optimized for particular workloads. General-purpose systems can handle small data volumes comfortably, but the greater the scale, the more specialized systems tend to become [[9](ch01.html#Stonebraker2005fitsall)].
#### 从数据仓库到数据湖
数据仓库通常使用*关系*数据模型通过SQL查询见[第3章](ch03.html#ch_datamodels)),可能使用专业的商业智能软件。这种模型很适合业务分析师需要进行的类型的查询,但它不太适合数据科学家的需求,他们可能需要执行的任务如下:
- 将数据转换成适合训练机器学习模型的形式;这通常需要将数据库表的行和列转换为称为*特征*的数字值向量或矩阵。以一种最大化训练模型性能的方式执行这种转换的过程称为*特征工程*它通常需要使用SQL难以表达的自定义代码。
- 获取文本数据(例如,产品评论)并使用自然语言处理技术尝试从中提取结构化信息(例如,作者的情感或他们提到的主题)。类似地,他们可能需要使用计算机视觉技术从照片中提取结构化信息。
尽管已经努力在SQL数据模型中添加机器学习操作符 [[10](ch01.html#Cohen2009)] 并在关系基础上构建高效的机器学习系统 [[11](ch01.html#Olteanu2020)]许多数据科学家更喜欢不在数据仓库这类关系数据库中工作。相反许多人更喜欢使用如pandas和scikit-learn这样的Python数据分析库统计分析语言如R以及分布式分析框架如Spark [[12](ch01.html#Bornstein2020)]。我们在[“数据框架、矩阵和数组”](ch03.html#sec_datamodels_dataframes)中进一步讨论这些内容。
因此,组织面临着使数据以适合数据科学家使用的形式可用的需求。答案是*数据湖*一个集中的数据存储库存放可能对分析有用的任何数据通过ETL过程从业务系统获取。与数据仓库的不同之处在于数据湖只包含文件不强加任何特定的文件格式或数据模型。数据湖中的文件可能是使用如Avro或Parquet等文件格式编码的数据库记录集合见[链接即将到来]),但它们同样可能包含文本、图像、视频、传感器读数、稀疏矩阵、特征向量、基因序列或任何其他类型的数据 [[13](ch01.html#Fowler2015)]。
ETL过程已经概括为*数据管道*,在某些情况下,数据湖已成为从业务系统到数据仓库的中间停靠点。数据湖包含由业务系统产生的“原始”形式的数据,而不是转换成关系数据仓库架构的数据。这种方法的优点是,每个数据的消费者都可以将原始数据转换成最适合其需要的形式。这被称为*寿司原则*:“原始数据更好” [[14](ch01.html#Johnson2015)]。
除了从数据湖加载数据到单独的数据仓库外还可以直接在数据湖中的文件上运行典型的数据仓库工作负载SQL查询和商业分析以及数据科学/机器学习工作负载。这种架构被称为*数据湖仓*,它需要一个查询执行引擎和一个元数据(例如,模式管理)层来扩展数据湖的文件存储 [[15](ch01.html#Armbrust2021)]。Apache Hive、Spark SQL、Presto和Trino是这种方法的例子。
A data warehouse often uses a *relational* data model that is queried through SQL (see [Chapter 3](ch03.html#ch_datamodels)), perhaps using specialized business intelligence software. This model works well for the types of queries that business analysts need to make, but it is less well suited to the needs of data scientists, who might need to perform tasks such as:
- Transform data into a form that is suitable for training a machine learning model; often this requires turning the rows and columns of a database table into a vector or matrix of numerical values called *features*. The process of performing this transformation in a way that maximizes the performance of the trained model is called *feature engineering*, and it often requires custom code that is difficult to express using SQL.
- Take textual data (e.g., reviews of a product) and use natural language processing techniques to try to extract structured information from it (e.g., the sentiment of the author, or which topics they mention). Similarly, they might need to extract structured information from photos using computer vision techniques.
Although there have been efforts to add machine learning operators to a SQL data model [[10](ch01.html#Cohen2009)] and to build efficient machine learning systems on top of a relational foundation [[11](ch01.html#Olteanu2020)], many data scientists prefer not to work in a relational database such as a data warehouse. Instead, many prefer to use Python data analysis libraries such as pandas and scikit-learn, statistical analysis languages such as R, and distributed analytics frameworks such as Spark [[12](ch01.html#Bornstein2020)]. We discuss these further in [“Dataframes, Matrices, and Arrays”](ch03.html#sec_datamodels_dataframes).
Consequently, organizations face a need to make data available in a form that is suitable for use by data scientists. The answer is a *data lake*: a centralized data repository that holds a copy of any data that might be useful for analysis, obtained from operational systems via ETL processes. The difference from a data warehouse is that a data lake simply contains files, without imposing any particular file format or data model. Files in a data lake might be collections of database records, encoded using a file format such as Avro or Parquet (see [Link to Come]), but they can equally well contain text, images, videos, sensor readings, sparse matrices, feature vectors, genome sequences, or any other kind of data [[13](ch01.html#Fowler2015)].
ETL processes have been generalized to *data pipelines*, and in some cases the data lake has become an intermediate stop on the path from the operational systems to the data warehouse. The data lake contains data in a “raw” form produced by the operational systems, without the transformation into a relational data warehouse schema. This approach has the advantage that each consumer of the data can transform the raw data into a form that best suits their needs. It has been dubbed the *sushi principle*: “raw data is better” [[14](ch01.html#Johnson2015)].
Besides loading data from a data lake into a separate data warehouse, it is also possible to run typical data warehousing workloads (SQL queries and business analytics) directly on the files in the data lake, alongside data science/machine learning workloads. This architecture is known as a *data lakehouse*, and it requires a query execution engine and a metadata (e.g., schema management) layer that extend the data lakes file storage [[15](ch01.html#Armbrust2021)]. Apache Hive, Spark SQL, Presto, and Trino are examples of this approach.
#### 数据湖之外
随着分析实践的成熟组织越来越关注分析系统和数据管道的管理和运营例如在DataOps宣言中捕捉到的内容 [[16](ch01.html#DataOps)]。其中包括治理、隐私和遵守像GDPR和CCPA这样的法规问题我们将在[“数据系统、法律与社会”](ch01.html#sec_introduction_compliance)和[即将到来的链接]中讨论。
此外,分析数据越来越多地不仅以文件和关系表的形式提供,还以事件流的形式提供(见[即将到来的链接])。使用基于文件的数据分析,你可以定期(例如,每天)重新运行分析,以响应数据的变化,但流处理允许分析系统更快地响应事件,大约在几秒钟的数量级。根据应用程序和时间敏感性,流处理方法可以很有价值,例如识别并阻止潜在的欺诈或滥用行为。
在某些情况下,分析系统的输出会提供给业务系统(有时被称为*反向ETL* [[17](ch01.html#Manohar2021)]。例如一个在分析系统中训练的机器学习模型可能被部署到生产中以便它可以为终端用户生成推荐如“购买X的人也买了Y”。这些部署的分析系统输出也被称为*数据产品* [[18](ch01.html#ORegan2018)]。机器学习模型可以使用TFX、Kubeflow或MLflow等专门工具部署到业务系统中。
As analytics practices have matured, organizations have been increasingly paying attention to the management and operations of analytics systems and data pipelines, as captured for example in the DataOps manifesto [[16](ch01.html#DataOps)]. Part of this are issues of governance, privacy, and compliance with regulation such as GDPR and CCPA, which we discuss in [“Data Systems, Law, and Society”](ch01.html#sec_introduction_compliance) and [Link to Come].
Moreover, analytical data is increasingly made available not only as files and relational tables, but also as streams of events (see [Link to Come]). With file-based data analysis you can re-run the analysis periodically (e.g., daily) in order to respond to changes in the data, but stream processing allows analytics systems to respond to events much faster, on the order of seconds. Depending on the application and how time-sensitive it is, a stream processing approach can be valuable, for example to identify and block potentially fraudulent or abusive activity.
In some cases the outputs of analytics systems are made available to operational systems (a process sometimes known as *reverse ETL* [[17](ch01.html#Manohar2021)]). For example, a machine-learning model that was trained on data in an analytics system may be deployed to production, so that it can generate recommendations for end-users, such as “people who bought X also bought Y”. Such deployed outputs of analytics systems are also known as *data products* [[18](ch01.html#ORegan2018)]. Machine learning models can be deployed to operational systems using specialized tools such as TFX, Kubeflow, or MLflow.
### 记录系统与衍生数据系统
与业务系统和分析系统之间的区别相关,本书还区分了*记录系统*和*衍生数据系统*。这些术语有用,因为它们可以帮助你澄清系统中的数据流动:
- 记录系统
记录系统,也称为*真实来源*,持有某些数据的权威或*规范*版本。当新数据进入时,例如作为用户输入,首先在此处写入。每个事实只表示一次(通常是*规范化*的;见[“规范化、反规范化和连接”](ch03.html#sec_datamodels_normalization))。如果另一个系统与记录系统之间存在任何差异,则记录系统中的值(按定义)是正确的。
- 衍生数据系统
衍生系统中的数据是从另一个系统获取一些现有数据并以某种方式转换或处理的结果。如果你丢失了衍生数据,你可以从原始来源重新创建它。一个典型的例子是缓存:如果存在,可以从缓存中提供数据,但如果缓存中没有你需要的内容,你可以回退到底层数据库。非规范化的值、索引、物化视图、转换的数据表示和在数据集上训练的模型也属于这一类别。
从技术上讲,衍生数据是*冗余的*,因为它复制了现有的信息。然而,它通常对于读取查询的良好性能是必不可少的。你可以从单一来源派生出几个不同的数据集,使你能够从不同的“视点”查看数据。
分析系统通常是衍生数据系统,因为它们是在其他地方创建的数据的消费者。操作服务可能包含记录系统和衍生数据系统的混合。记录系统是首次写入数据的主要数据库,而衍生数据系统是加速常见读取操作的索引和缓存,特别是对于记录系统无法有效回答的查询。
大多数数据库、存储引擎和查询语言本质上不是记录系统或衍生系统。数据库只是一个工具:如何使用它取决于你。记录系统和衍生数据系统之间的区别不在于工具,而在于你如何在应用程序中使用它。通过明确哪些数据是从哪些其他数据衍生的,你可以为一个否则可能混乱的系统架构带来清晰度。
当一个系统中的数据是从另一个系统的数据衍生的时候,你需要一个过程来更新衍生数据,当记录系统中的原始数据发生变化时。不幸的是,许多数据库的设计基于这样的假设:你的应用程序只需要使用那一个数据库,它们并不容易整合多个系统以传播这些更新。在[即将到来的链接]中,我们将讨论*数据集成*的方法,这些方法允许我们组合多个数据系统来实现一个系统无法单独做到的事情。
这标志着我们对分析和交易处理的比较的结束。在下一节中,我们将探讨一个你可能已经看到多次争论的另一个折衷方案。
Related to the distinction between operational and analytical systems, this book also distinguishes between *systems of record* and *derived data systems*. These terms are useful because they can help you clarify the flow of data through a system:
- Systems of record
A system of record, also known as *source of truth*, holds the authoritative or *canonical* version of some data. When new data comes in, e.g., as user input, it is first written here. Each fact is represented exactly once (the representation is typically *normalized*; see [“Normalization, Denormalization, and Joins”](ch03.html#sec_datamodels_normalization)). If there is any discrepancy between another system and the system of record, then the value in the system of record is (by definition) the correct one.
- Derived data systems
Data in a derived system is the result of taking some existing data from another system and transforming or processing it in some way. If you lose derived data, you can recreate it from the original source. A classic example is a cache: data can be served from the cache if present, but if the cache doesnt contain what you need, you can fall back to the underlying database. Denormalized values, indexes, materialized views, transformed data representations, and models trained on a dataset also fall into this category.
Technically speaking, derived data is *redundant*, in the sense that it duplicates existing information. However, it is often essential for getting good performance on read queries. You can derive several different datasets from a single source, enabling you to look at the data from different “points of view.”
Analytical systems are usually derived data systems, because they are consumers of data created elsewhere. Operational services may contain a mixture of systems of record and derived data systems. The systems of record are the primary databases to which data is first written, whereas the derived data systems are the indexes and caches that speed up common read operations, especially for queries that the system of record cannot answer efficiently.
Most databases, storage engines, and query languages are not inherently a system of record or a derived system. A database is just a tool: how you use it is up to you. The distinction between system of record and derived data system depends not on the tool, but on how you use it in your application. By being clear about which data is derived from which other data, you can bring clarity to an otherwise confusing system architecture.
When the data in one system is derived from the data in another, you need a process for updating the derived data when the original in the system of record changes. Unfortunately, many databases are designed based on the assumption that your application only ever needs to use that one database, and they do not make it easy to integrate multiple systems in order to propagate such updates. In [Link to Come] we will discuss approaches to *data integration*, which allow us to compose multiple data systems to achieve things that one system alone cannot do.
That brings us to the end of our comparison of analytics and transaction processing. In the next section, we will examine another trade-off that you might have already seen debated multiple times.
--------
## 云服务与自托管
对于组织需要执行的任何事务,首先要问的问题之一是:应该在内部完成还是外包?您应该自行构建还是购买?
这最终是一个关于业务优先级的问题。管理学的普遍观点是,作为组织的核心能力或竞争优势的事物应该在内部完成,而非核心、常规或普通的事务则应交给供应商处理 [[19](ch01.html#Fournier2021)]。举一个极端的例子,大多数公司不会自己发电(除非它们是能源公司,且不考虑紧急备用电力),因为从电网购买电力更便宜。
在软件方面,需要做出的两个重要决策是谁来构建软件以及谁来部署它。有一个将每个决策外包出去的可能性的范围,如[图 1-2](ch01.html#fig_cloud_spectrum)所示。一个极端是你编写并在内部运行的定制软件另一个极端是广泛使用的云服务或软件即服务SaaS产品由外部供应商实施和操作你只能通过Web界面或API访问。
With anything that an organization needs to do, one of the first questions is: should it be done in-house, or should it be outsourced? Should you build or should you buy?
Ultimately, this is a question about business priorities. The received management wisdom is that things that are a core competency or a competitive advantage of your organization should be done in-house, whereas things that are non-core, routine, or commonplace should be left to a vendor [[19](ch01.html#Fournier2021)]. To give an extreme example, most companies do not generate their own electricity (unless they are an energy company, and leaving aside emergency backup power), since it is cheaper to buy electricity from the grid.
With software, two important decisions to be made are who builds the software and who deploys it. There is a spectrum of possibilities that outsource each decision to various degrees, as illustrated in [Figure 1-2](ch01.html#fig_cloud_spectrum). At one extreme is bespoke software that you write and run in-house; at the other extreme are widely-used cloud services or Software as a Service (SaaS) products that are implemented and operated by an external vendor, and which you only access through a web interface or API.
![ddia 0101](img/ddia_0101.png)
###### 图 1-2. 软件及其运营的类型范围。 A spectrum of types of software and its operations.
中间地带是你自行托管的现成软件开源或商业的即自己部署的软件——例如如果你下载MySQL并将其安装在你控制的服务器上。这可能是在你自己的硬件上通常称为*本地部署*,即使服务器实际上位于租用的数据中心机架中,也不一定真的在你自己的场所内),或者在云中的虚拟机上(即*基础设施即服务*或IaaS。在这个范围中还有更多点例如运行修改过的开源软件。
与此范围分开的还有一个问题即你是如何部署服务的无论是在云中还是本地——例如你是否使用像Kubernetes这样的编排框架。然而部署工具的选择超出了本书的范围因为其他因素对数据系统的架构有更大的影响。
The middle ground is off-the-shelf software (open source or commercial) that you *self-host*, i.e., deploy yourself—for example, if you download MySQL and install it on a server you control. This could be on your own hardware (often called *on-premises*, even if the server is actually in a rented datacenter rack and not literally on your own premises), or on a virtual machine in the cloud (*Infrastructure as a Service* or IaaS). There are still more points along this spectrum, e.g., taking open source software and running a modified version of it.
Seperately from this spectrum there is also the question of *how* you deploy services, either in the cloud or on-premises—for example, whether you use an orchestration framework such as Kubernetes. However, choice of deployment tooling is out of scope of this book, since other factors have a greater influence on the architecture of data systems.
### 云服务的优缺点
使用云服务,而不是自己运行可比软件,本质上是将该软件的运营外包给云提供商。支持和反对使用云服务的理由都很充分。云提供商声称使用他们的服务可以节省时间和金钱,并允许你比建立自己的基础设施更快地行动。
云服务是否实际上比自托管更便宜和更容易,很大程度上取决于你的技能和系统的工作负载。如果你已经有设置和操作所需系统的经验,并且你的负载相当可预测(即,你需要的机器数量不会剧烈波动),那么通常购买自己的机器并自己运行软件会更便宜 [[20](ch01.html#HeinemeierHansson2022), [21](ch01.html#Badizadegan2022)]。
另一方面,如果你需要一个你不知道如何部署和操作的系统,那么采用云服务通常比自己学习管理系统更容易且更快。如果你必须雇佣并培训专门的员工来维护和业务系统,这可能非常昂贵。当你使用云时,仍然需要一个运营团队(见[“云时代的运营”](ch01.html#sec_introduction_operations)),但将基本的系统管理外包可以释放你的团队,专注于更高层次的问题。
当你将系统的运营外包给专门运营该服务的公司时,这可能会带来更好的服务,因为提供商从为许多客户提供服务中获得运营专长。另一方面,如果你自己运行服务,你可以配置并调整它以在你特定的工作负载上表现良好;云服务不太可能愿意代表你进行此类定制。
如果你的系统负载随时间变化很大,云服务特别有价值。如果你配置你的机器能够处理高峰负载,但这些计算资源大部分时间都处于空闲状态,系统的成本效益就会降低。在这种情况下,云服务的优势在于它们可以更容易地根据需求变化扩展或缩减你的计算资源。
例如,分析系统的负载通常变化极大:快速运行大型分析查询需要大量并行的计算资源,但一旦查询完成,这些资源就会闲置,直到用户发出下一个查询。预定义的查询(例如,用于日常报告的查询)可以排队并安排以平滑负载,但对于交互式查询,你希望它们完成得越快,工作负载就越变化无常。如果你的数据集非常大,以至于快速查询需要大量计算资源,使用云可以节省金钱,因为你可以将未使用的资源返回给提供商,而不是让它们闲置。对于较小的数据集,这种差异不那么显著。
云服务最大的缺点是你对它没有控制权:
- 如果它缺少你需要的功能,你唯一能做的就是礼貌地询问供应商是否会添加它;你通常无法自己实现它。
- 如果服务出现故障,你只能等待它恢复。
- 如果你以某种方式使用服务,触发了一个错误或导致性能问题,你很难诊断问题。对于你自己运行的软件,你可以从业务系统获取性能指标和调试信息来帮助你了解其行为,你可以查看服务器日志,但使用供应商托管的服务时,你通常无法访问这些内部信息。
- 此外,如果服务关闭或变得无法接受地昂贵,或者如果供应商决定以你不喜欢的方式更改其产品,你将受制于他们——继续运行软件的旧版本通常不是一个选项,因此你将被迫迁移到另一个服务 [[22](ch01.html#Yegge2020)]。如果有提供兼容API的替代服务这种风险可以缓解但对于许多云服务没有标准的API这增加了切换的成本使供应商锁定成为一个问题。
尽管存在这些风险,组织构建基于云服务的新应用变得越来越流行。然而,云服务并不能取代所有的内部数据系统:许多旧系统早于云技术,且对于那些现有云服务无法满足的特殊需求,内部系统仍然是必需的。例如,像高频交易这样对延迟极其敏感的应用需要完全控制硬件。
Using a cloud service, rather than running comparable software yourself, essentially outsources the operation of that software to the cloud provider. There are good arguments for and against cloud services. Cloud providers claim that using their services saves you time and money, and allows you to move faster compared to setting up your own infrastructure.
Whether a cloud service is actually cheaper and easier than self-hosting depends very much on your skills and the workload on your systems. If you already have experience setting up and operating the systems you need, and if your load is quite predictable (i.e., the number of machines you need does not fluctuate wildly), then its often cheaper to buy your own machines and run the software on them yourself [[20](ch01.html#HeinemeierHansson2022), [21](ch01.html#Badizadegan2022)].
On the other hand, if you need a system that you dont already know how to deploy and operate, then adopting a cloud service is often easier and quicker than learning to manage the system yourself. If you have to hire and train staff specifically to maintain and operate the system, that can get very expensive. You still need an operations team when youre using the cloud (see [“Operations in the Cloud Era”](ch01.html#sec_introduction_operations)), but outsourcing the basic system administration can free up your team to focus on higher-level concerns.
When you outsource the operation of a system to a company that specializes in running that service, that can potentially result in a better service, since the provider gains operational expertise from providing the service to many customers. On the other hand, if you run the service yourself, you can configure and tune it to perform well on your particular workload; it is unlikely that a cloud service would be willing to make such customizations on your behalf.
Cloud services are particularly valuable if the load on your systems varies a lot over time. If you provision your machines to be able to handle peak load, but those computing resources are idle most of the time, the system becomes less cost-effective. In this situation, cloud services have the advantage that they can make it easier to scale your computing resources up or down in response to changes in demand.
For example, analytics systems often have extremely variable load: running a large analytical query quickly requires a lot of computing resources in parallel, but once the query completes, those resources sit idle until the user makes the next query. Predefined queries (e.g., for daily reports) can be enqueued and scheduled to smooth out the load, but for interactive queries, the faster you want them to complete, the more variable the workload becomes. If your dataset is so large that querying it quickly requires significant computing resources, using the cloud can save money, since you can return unused resources to the provider rather than leaving them idle. For smaller datasets, this difference is less significant.
The biggest downside of a cloud service is that you have no control over it:
- If it is lacking a feature you need, all you can do is to politely ask the vendor whether they will add it; you generally cannot implement it yourself.
- If the service goes down, all you can do is to wait for it to recover.
- If you are using the service in a way that triggers a bug or causes performance problems, it will be difficult for you to diagnose the issue. With software that you run yourself, you can get performance metrics and debugging information from the operating system to help you understand its behavior, and you can look at the server logs, but with a service hosted by a vendor you usually do not have access to these internals.
- Moreover, if the service shuts down or becomes unacceptably expensive, or if the vendor decides to change their product in a way you dont like, you are at their mercy—continuing to run an old version of the software is usually not an option, so you will be forced to migrate to an alternative service [[22](ch01.html#Yegge2020)]. This risk is mitigated if there are alternative services that expose a compatible API, but for many cloud services there are no standard APIs, which raises the cost of switching, making vendor lock-in a problem.
Despite all these risks, it has become more and more popular for organizations to build new applications on top of cloud services. However, cloud services will not subsume all in-house data systems: many older systems predate the cloud, and for any services that have specialist requirements that existing cloud services cannot meet, in-house systems remain necessary. For example, very latency-sensitive applications such as high-frequency trading require full control of the hardware.
--------
### 云原生系统架构
除了经济模式的不同(订阅服务而非购买硬件并在其上运行许可软件),云计算的兴起还在技术层面深刻影响了数据系统的实施方式。*云原生* 一词用来描述一种旨在利用云服务优势的架构。
原则上,几乎任何你可以自行托管的软件也可以作为云服务提供,实际上,许多流行的数据系统现在已经有了这样的托管服务。然而,从底层设计为云原生的系统显示出多项优势:在相同硬件上有更好的性能,从失败中更快恢复,能迅速扩展计算资源以匹配负载,并支持更大的数据集[[23](ch01.html#Verbitski2017), [24](ch01.html#Antonopoulos2019_ch1), [25](ch01.html#Vuppalapati2020)]。[表 1-2](ch01.html#tab_cloud_native_dbs)列出了这两类系统的一些例子。
| 类别 | 自托管系统 | 云原生系统 |
|----------|-----------------------------|---------------------------------------------------------------------|
| 事务型/OLTP | MySQL, PostgreSQL, MongoDB | AWS Aurora 【23】, Azure SQL DB Hyperscale 【24】, Google Cloud Spanner |
| 分析型/OLAP | Teradata, ClickHouse, Spark | Snowflake 【25】, Google BigQuery, Azure Synapse Analytics |
Besides having a different economic model (subscribing to a service instead of buying hardware and licensing software to run on it), the rise of the cloud has also had a profound effect on how data systems are implemented on a technical level. The term *cloud-native* is used to describe an architecture that is designed to take advantage of cloud services.
In principle, almost any software that you can self-host could also be provided as a cloud service, and indeed such managed services are now available for many popular data systems. However, systems that have been designed from the ground up to be cloud-native have been shown to have several advantages: better performance on the same hardware, faster recovery from failures, being able to quickly scale computing resources to match the load, and supporting larger datasets [[23](ch01.html#Verbitski2017), [24](ch01.html#Antonopoulos2019_ch1), [25](ch01.html#Vuppalapati2020)]. [Table 1-2](ch01.html#tab_cloud_native_dbs) lists some examples of both types of systems.
#### 云服务的分层
许多自托管的数据系统具有非常简单的系统要求:它们运行在常规业务系统如 Linux 或 Windows 上,它们将数据存储为文件系统上的文件,并通过标准网络协议如 TCP/IP 进行通信。一些系统依赖于特殊硬件,如用于机器学习的 GPU 或 RDMA 网络接口但总体来说自托管软件倾向于使用非常通用的计算资源CPU、RAM、文件系统和 IP 网络。
在云中这类软件可以在基础设施即服务IaaS环境中运行使用一个或多个具有一定CPU、内存、磁盘和网络带宽配额的虚拟机或*实例*)。与物理机相比,云实例可以更快地配置,并且大小种类更多,但在其他方面它们类似于传统计算机:你可以在其上运行任何软件,但你需要自己负责管理。
相比之下,云原生服务的关键思想是不仅使用由业务系统管理的计算资源,还要构建在更低层级的云服务之上,创建更高层级的服务。例如:
- *对象存储*服务,如亚马逊 S3、Azure Blob 存储和 Cloudflare R2 存储大文件。它们提供的 API 比典型文件系统的 API 更有限(基本的文件读写),但它们的优势在于隐藏了底层的物理机器:服务自动将数据分布在许多机器上,因此你无需担心任何一台机器上的磁盘空间耗尽。即使某些机器或其磁盘完全失败,也不会丢失数据。
- 许多其他服务又是建立在对象存储和其他云服务之上的例如Snowflake 是一种基于云的分析数据库(数据仓库),依赖于 S3 进行数据存储 [[25](ch01.html#Vuppalapati2020)],还有一些服务又建立在 Snowflake 之上。
正如计算中的抽象总是一样,关于你应该使用什么,没有一个正确的答案。一般规则是,更高层次的抽象往往更针对特定用例。如果你的需求与更高层系统设计的情况匹配,使用现有的更高层系统可能会比从更低层系统自行构建省去许多麻烦。另一方面,如果没有高层系统满足你的需求,那么自己从更低层组件构建是唯一的选择。
Many self-hosted data systems have very simple system requirements: they run on a conventional operating system such as Linux or Windows, they store their data as files on the filesystem, and they communicate via standard network protocols such as TCP/IP. A few systems depend on special hardware such as GPUs (for machine learning) or RDMA network interfaces, but on the whole, self-hosted software tends to use very generic computing resources: CPU, RAM, a filesystem, and an IP network.
In a cloud, this type of software can be run on an Infrastructure-as-a-Service environment, using one or more virtual machines (or *instances*) with a certain allocation of CPUs, memory, disk, and network bandwidth. Compared to physical machines, cloud instances can be provisioned faster and they come in a greater variety of sizes, but otherwise they are similar to a traditional computer: you can run any software you like on it, but you are responsible for administering it yourself.
In contrast, the key idea of cloud-native services is to use not only the computing resources managed by your operating system, but also to build upon lower-level cloud services to create higher-level services. For example:
- *Object storage* services such as Amazon S3, Azure Blob Storage, and Cloudflare R2 store large files. They provide more limited APIs than a typical filesystem (basic file reads and writes), but they have the advantage that they hide the underlying physical machines: the service automatically distributes the data across many machines, so that you dont have to worry about running out of disk space on any one machine. Even if some machines or their disks fail entirely, no data is lost.
- Many other services are in turn built upon object storage and other cloud services: for example, Snowflake is a cloud-based analytic database (data warehouse) that relies on S3 for data storage [[25](ch01.html#Vuppalapati2020)], and some other services in turn build upon Snowflake.
As always with abstractions in computing, there is no one right answer to what you should use. As a general rule, higher-level abstractions tend to be more oriented towards particular use cases. If your needs match the situations for which a higher-level system is designed, using the existing higher-level system will probably provide what you need with much less hassle than building it yourself from lower-level systems. On the other hand, if there is no high-level system that meets your needs, then building it yourself from lower-level components is the only option.
#### 存储与计算分离
在传统计算中,磁盘存储被视为持久的(我们假设一旦某些内容被写入磁盘,它就不会丢失);为了容忍单个硬盘的失败,经常使用 RAID 来在几个磁盘上维护数据的副本。在云中,计算实例(虚拟机)也可能有本地磁盘附加,但云原生系统通常将这些磁盘更像是临时缓存,而不是长期存储。这是因为如果关联实例失败,或者为了适应负载变化而用更大或更小的实例替换实例(在不同的物理机上),本地磁盘将变得无法访问。
作为本地磁盘的替代云服务还提供了可以从一个实例分离并连接到另一个实例的虚拟磁盘存储Amazon EBS、Azure 管理磁盘和 Google Cloud 中的持久磁盘)。这种虚拟磁盘实际上不是物理磁盘,而是由一组独立机器提供的云服务,模拟磁盘(块设备)的行为(每个块通常为 4 KiB 大小)。这项技术使得在云中运行传统基于磁盘的软件成为可能,但它通常表现出较差的性能和可扩展性 [[23](ch01.html#Verbitski2017)]。
为解决这个问题,云原生服务通常避免使用虚拟磁盘,而是建立在专门为特定工作负载优化的专用存储服务之上。如 S3 等对象存储服务旨在长期存储相对较大的文件,大小从数百千字节到几个千兆字节不等。存储在数据库中的单独行或值通常比这小得多;因此云数据库通常在单独的服务中管理更小的值,并在对象存储中存储更大的数据块(包含许多单独的值) [[24](ch01.html#Antonopoulos2019_ch1)]。
在传统的系统架构中同一台计算机负责存储磁盘和计算CPU 和 RAM但在云原生系统中这两种责任已经有所分离或*解耦* [[8](ch01.html#Prout2022), [25](ch01.html#Vuppalapati2020), [26](https://learning.oreilly.com/library/view/designing-data-intensive-applications/)]。例如S3仅存储文件如果你想分析那些数据你将不得不在 S3 外部的某处运行分析代码。这意味着需要通过网络传输数据,我们将在[“分布式与单节点系统”](ch01.html#sec_introduction_distributed)中进一步讨论这一点。
此外,云原生系统通常是*多租户*的,这意味着它们不是为每个客户配置单独的机器,而是在同一共享硬件上由同一服务处理来自几个不同客户的数据和计算 [[28](ch01.html#Vanlightly2023)]。多租户可以实现更好的硬件利用率、更容易的可扩展性和云提供商更容易的管理,但它也需要精心的工程设计,以确保一个客户的活动不影响系统对其他客户的性能或安全性 [[29](ch01.html#Jonas2019)]。
In traditional computing, disk storage is regarded as durable (we assume that once something is written to disk, it will not be lost); to tolerate the failure of an individual hard disk, RAID is often used to maintain copies of the data on several disks. In the cloud, compute instances (virtual machines) may also have local disks attached, but cloud-native systems typically treat these disks more like an ephemeral cache, and less like long-term storage. This is because the local disk becomes inaccessible if the associated instance fails, or if the instance is replaced with a bigger or a smaller one (on a different physical machine) in order to adapt to changes in load.
As an alternative to local disks, cloud services also offer virtual disk storage that can be detached from one instance and attached to a different one (Amazon EBS, Azure managed disks, and persistent disks in Google Cloud). Such a virtual disk is not actually a physical disk, but rather a cloud service provided by a separate set of machines, which emulates the behavior of a disk (a *block device*, where each block is typically 4 KiB in size). This technology makes it possible to run traditional disk-based software in the cloud, but it often suffers from poor performance and poor scalability [[23](ch01.html#Verbitski2017)].
To address this problem, cloud-native services generally avoid using virtual disks, and instead build on dedicated storage services that are optimized for particular workloads. Object storage services such as S3 are designed for long-term storage of fairly large files, ranging from hundreds of kilobytes to several gigabytes in size. The individual rows or values stored in a database are typically much smaller than this; cloud databases therefore typically manage smaller values in a separate service, and store larger data blocks (containing many individual values) in an object store [[24](ch01.html#Antonopoulos2019_ch1)].
In a traditional systems architecture, the same computer is responsible for both storage (disk) and computation (CPU and RAM), but in cloud-native systems, these two responsibilities have become somewhat separated or *disaggregated* [[8](ch01.html#Prout2022), [25](ch01.html#Vuppalapati2020), [26](ch01.html#Shapira2023), [27](ch01.html#Murthy2022)]: for example, S3 only stores files, and if you want to analyze that data, you will have to run the analysis code somewhere outside of S3. This implies transferring the data over the network, which we will discuss further in [“Distributed versus Single-Node Systems”](ch01.html#sec_introduction_distributed).
Moreover, cloud-native systems are often *multitenant*, which means that rather than having a separate machine for each customer, data and computation from several different customers are handled on the same shared hardware by the same service [[28](ch01.html#Vanlightly2023)]. Multitenancy can enable better hardware utilization, easier scalability, and easier management by the cloud provider, but it also requires careful engineering to ensure that one customers activity does not affect the performance or security of the system for other customers [[29](ch01.html#Jonas2019)].
--------
### 在云时代的运营
传统上,管理组织服务器端数据基础设施的人被称为*数据库管理员*DBAs或*系统管理员*sysadmins。近年来许多组织试图将软件开发和运营的角色整合到一个团队中共同负责后端服务和数据基础设施*DevOps*哲学指导了这一趋势。*站点可靠性工程师*SREs是谷歌实施这一理念的方式 [[30](ch01.html#Beyer2016)]。
运营的角色是确保服务可靠地交付给用户(包括配置基础设施和部署应用程序),并确保稳定的生产环境(包括监控和诊断可能影响可靠性的问题)。对于自托管系统,运营传统上涉及大量单机层面的工作,如容量规划(例如,监控可用磁盘空间并在空间用尽前添加更多磁盘)、配置新机器、将服务从一台机器移至另一台以及安装业务系统补丁。
许多云服务提供了一个API隐藏了实际实现服务的单个机器。例如云存储用*计量计费*取代了固定大小的磁盘,您可以在不提前规划容量需求的情况下存储数据,并根据实际使用的空间收费。此外,许多云服务即使单个机器失败也能保持高可用性(见[“可靠性和容错”](ch02.html#sec_introduction_reliability))。
从单个机器到服务的这种重点转变伴随着运营角色的变化。提供可靠服务的高级目标仍然相同但过程和工具已经演变。DevOps/SRE哲学更加强调
- 自动化——偏好可重复的过程而不是一次性的手工作业,
- 偏好短暂的虚拟机和服务而不是长时间运行的服务器,
- 促进频繁的应用更新,
- 从事件中学习,
- 即使个别人员来去,也要保留组织对系统的知识 [[31](ch01.html#Limoncelli2020)]。
随着云服务的兴起,角色出现了分化:基础设施公司的运营团队专注于向大量客户提供可靠服务的细节,而服务的客户尽可能少地花时间和精力在基础设施上 [[32](ch01.html#Majors2020)]。
云服务的客户仍然需要运营,但他们关注的方面不同,如选择最适合特定任务的服务、将不同服务相互集成以及从一个服务迁移到另一个服务。尽管计量计费消除了传统意义上的容量规划的需要,但仍然重要的是了解您正在使用哪些资源以及用途,以免在不需要的云资源上浪费金钱:容量规划变成了财务规划,性能优化变成了成本优化 [[33](ch01.html#Cherkasky2021)]。此外,云服务确实有资源限制或*配额*(如您可以同时运行的最大进程数),您需要了解并计划这些限制,以免遇到问题 [[34](ch01.html#Kushchi2023)]。
采用云服务可能比运行自己的基础设施更容易且更快,尽管即使在这里,学习如何使用它和可能绕过其限制也有成本。随着越来越多的供应商提供针对不同用例的更广泛的云服务,不同服务之间的集成成为特别的挑战 [[35](ch01.html#Bernhardsson2021), [36](ch01.html#Stancil2021)]。ETL见[“数据仓库”](ch01.html#sec_introduction_dwh))只是故事的一部分;运营云服务也需要相互集成。目前缺乏促进此类集成的标准,因此它通常涉及大量的手动努力。
其他不能完全外包给云服务的运营方面包括维护应用程序及其使用的库的安全性、管理自己的服务之间的互动、监控服务的负载以及追踪性能下降或中断等问题的原因。虽然云正在改变运营的角色,但运营的需求依旧迫切。
Traditionally, the people managing an organizations server-side data infrastructure were known as *database administrators* (DBAs) or *system administrators* (sysadmins). More recently, many organizations have tried to integrate the roles of software development and operations into teams with a shared responsibility for both backend services and data infrastructure; the *DevOps* philosophy has guided this trend. *Site Reliability Engineers* (SREs) are Googles implementation of this idea [[30](ch01.html#Beyer2016)].
The role of operations is to ensure services are reliably delivered to users (including configuring infrastructure and deploying applications), and to ensure a stable production environment (including monitoring and diagnosing any problems that may affect reliability). For self-hosted systems, operations traditionally involves a significant amount of work at the level of individual machines, such as capacity planning (e.g., monitoring available disk space and adding more disks before you run out of space), provisioning new machines, moving services from one machine to another, and installing operating system patches.
Many cloud services present an API that hides the individual machines that actually implement the service. For example, cloud storage replaces fixed-size disks with *metered billing*, where you can store data without planning your capacity needs in advance, and you are then charged based on the space actually used. Moreover, many cloud services remain highly available, even when individual machines have failed (see [“Reliability and Fault Tolerance”](ch02.html#sec_introduction_reliability)).
This shift in emphasis from individual machines to services has been accompanied by a change in the role of operations. The high-level goal of providing a reliable service remains the same, but the processes and tools have evolved. The DevOps/SRE philosophy places greater emphasis on:
- automation—preferring repeatable processes over manual one-off jobs,
- preferring ephemeral virtual machines and services over long running servers,
- enabling frequent application updates,
- learning from incidents, and
- preserving the organizations knowledge about the system, even as individual people come and go [[31](ch01.html#Limoncelli2020)].
With the rise of cloud services, there has been a bifurcation of roles: operations teams at infrastructure companies specialize in the details of providing a reliable service to a large number of customers, while the customers of the service spend as little time and effort as possible on infrastructure [[32](ch01.html#Majors2020)].
Customers of cloud services still require operations, but they focus on different aspects, such as choosing the most appropriate service for a given task, integrating different services with each other, and migrating from one service to another. Even though metered billing removes the need for capacity planning in the traditional sense, its still important to know what resources you are using for which purpose, so that you dont waste money on cloud resources that are not needed: capacity planning becomes financial planning, and performance optimization becomes cost optimization [[33](ch01.html#Cherkasky2021)]. Moreover, cloud services do have resource limits or *quotas* (such as the maximum number of processes you can run concurrently), which you need to know about and plan for before you run into them [[34](ch01.html#Kushchi2023)].
Adopting a cloud service can be easier and quicker than running your own infrastructure, although even here there is a cost in learning how to use it, and perhaps working around its limitations. Integration between different services becomes a particular challenge as a growing number of vendors offers an ever broader range of cloud services targeting different use cases [[35](ch01.html#Bernhardsson2021), [36](ch01.html#Stancil2021)]. ETL (see [“Data Warehousing”](ch01.html#sec_introduction_dwh)) is only part of the story; operational cloud services also need to be integrated with each other. At present, there is a lack of standards that would facilitate this sort of integration, so it often involves significant manual effort.
Other operational aspects that cannot fully be outsourced to cloud services include maintaining the security of an application and the libraries it uses, managing the interactions between your own services, monitoring the load on your services, and tracking down the cause of problems such as performance degradations or outages. While the cloud is changing the role of operations, the need for operations is as great as ever.
-------
## 分布式与单节点系统
一个涉及通过网络进行通信的多台机器的系统被称为*分布式系统*。参与分布式系统的每个进程被称为*节点*。您可能希望系统分布式的原因有多种:
- 固有的分布式系统
如果一个应用程序涉及两个或更多互动的用户,每个用户都使用自己的设备,那么该系统不可避免地是分布式的:设备之间的通信必须通过网络进行。
- 云服务间的请求
如果数据存储在一个服务中但在另一个服务中处理,则必须通过网络从一个服务传输到另一个服务。
- 容错/高可用性
如果您的应用程序需要在一台机器(或多台机器、网络或整个数据中心)宕机时仍然继续工作,您可以使用多台机器来提供冗余。当一台机器失败时,另一台可以接管。见[“可靠性和容错”](ch02.html#sec_introduction_reliability)。
- 可扩展性
如果您的数据量或计算需求超过单台机器的处理能力,您可以将负载分散到多台机器上。见[“可扩展性”](ch02.html#sec_introduction_scalability)。
- 延迟
如果您的用户遍布全球,您可能希望在全球各地设置服务器,以便每个用户都可以从地理位置靠近他们的数据中心获得服务。这避免了用户必须等待网络包绕地球半圈来响应他们的请求。见[“描述性能”](ch02.html#sec_introduction_percentiles)。
- 弹性
如果您的应用程序在某些时候忙碌而在其他时候空闲,云部署可以根据需求扩展或缩减,因此您只需为您实际使用的资源付费。这在单台机器上更难实现,因为它需要预先配置好以应对最大负载,即使在很少使用时也是如此。
- 使用专用硬件
系统的不同部分可以利用不同类型的硬件来匹配它们的工作负载。例如对象存储可能使用多硬盘但CPU较少的机器而数据分析系统可能使用CPU和内存多但没有硬盘的机器机器学习系统可能使用GPU对于训练深度神经网络和其他机器学习任务比CPU更高效的机器。
- 法律合规
一些国家有数据居留法律,要求在其管辖区内的人的数据必须在该国地理范围内存储和处理 [[37](ch01.html#Korolov2022)]。这些规则的范围各不相同——例如,在某些情况下,它仅适用于医疗或财务数据,而其他情况则更广泛。因此,一个在几个这样的司法管辖区有用户的服务将不得不将其数据分布在几个位置的服务器上。
这些原因适用于您自己编写的服务(应用程序代码)和由现成软件组成的服务(例如数据库)。
A system that involves several machines communicating via a network is called a *distributed system*. Each of the processes participating in a distributed system is called a *node*. There are various reasons why you might want a system to be distributed:
- Inherently distributed systems
If an application involves two or more interacting users, each using their own device, then the system is unavoidably distributed: the communication between the devices will have to go via a network.
- Requests between cloud services
If data is stored in one service but processed in another, it must be transferred over the network from one service to the other.
- Fault tolerance/high availability
If your application needs to continue working even if one machine (or several machines, or the network, or an entire datacenter) goes down, you can use multiple machines to give you redundancy. When one fails, another one can take over. See [“Reliability and Fault Tolerance”](ch02.html#sec_introduction_reliability).
- Scalability
If your data volume or computing requirements grow bigger than a single machine can handle, you can potentially spread the load across multiple machines. See [“Scalability”](ch02.html#sec_introduction_scalability).
- Latency
If you have users around the world, you might want to have servers at various locations worldwide so that each user can be served from a datacenter that is geographically close to them. That avoids the users having to wait for network packets to travel halfway around the world to answer their requests. See [“Describing Performance”](ch02.html#sec_introduction_percentiles).
- Elasticity
If your application is busy at some times and idle at other times, a cloud deployment can scale up or down to meet the demand, so that you pay only for resources you are actively using. This more difficult on a single machine, which needs to be provisioned to handle the maximum load, even at times when it is barely used.
- Using specialized hardware
Different parts of the system can take advantage of different types of hardware to match their workload. For example, an object store may use machines with many disks but few CPUs, whereas a data analysis system may use machines with lots of CPU and memory but no disks, and a machine learning system may use machines with GPUs (which are much more efficient than CPUs for training deep neural networks and other machine learning tasks).
- Legal compliance
Some countries have data residency laws that require data about people in their jurisdiction to be stored and processed geographically within that country [[37](ch01.html#Korolov2022)]. The scope of these rules varies—for example, in some cases it applies only to medical or financial data, while other cases are broader. A service with users in several such jurisdictions will therefore have to distribute their data across servers in several locations.
These reasons apply both to services that you write yourself (application code) and services consisting of off-the-shelf software (such as databases).
--------
### 分布式系统的问题
分布式系统也有其不利之处。通过网络传输的每个请求和API调用都需要处理可能发生的故障网络可能中断服务可能过载或崩溃因此任何请求都可能在未收到响应的情况下超时。在这种情况下我们不知道服务是否收到了请求简单地重试可能不安全。我们将在[链接待补充]中详细讨论这些问题。
尽管数据中心网络速度很快,但调用另一个服务的速度仍然比在同一进程中调用函数要慢得多 [38]。在处理大量数据时,与其将数据从存储传输到另一台处理它的机器,不如将计算带到已经拥有数据的机器上,这样可能更快 [39]。更多的节点并不总是更快在某些情况下一台计算机上的简单单线程程序可能比拥有超过100个CPU核心的集群表现得更好 [40]。
调试分布式系统通常很困难:如果系统响应缓慢,您如何确定问题所在?在可观测性的标题下开发了分布式系统问题诊断技术 [41, 42]这涉及收集关于系统执行的数据并允许以可以分析高级指标和个别事件的方式查询这些数据。追踪工具如OpenTelemetry允许您跟踪哪个客户端为哪个操作调用了哪个服务器以及每个调用花费了多长时间 [43]。
数据库提供了各种机制来确保数据一致性,我们将在[链接待补充]和[链接待补充]中看到。然而,当每个服务都有自己的数据库时,跨这些不同服务维护数据一致性成为应用程序的问题。我们将在[链接待补充]中探讨的分布式事务是确保一致性的一种可能技术,但它们在微服务环境中很少使用,因为它们与使服务相互独立的目标相悖 [44]。
基于所有这些原因,如果您可以在单台机器上完成某项任务,这通常比建立分布式系统简单得多 [21]。CPU、内存和硬盘已变得更大、更快和更可靠。结合单节点数据库如DuckDB、SQLite和KùzuDB现在许多工作负载都可以在单个节点上运行。我们将在[链接待补充]中进一步探讨这个话题。
--------
### 微服务与无服务
分布式系统通常将系统分布在多台机器上,最常见的方式是将它们分为客户端和服务器,并让客户端向服务器发出请求。如我们将在[链接待补充]中讨论的这种通信最常使用HTTP。同一个过程可能既是服务器处理传入请求也是客户端向其他服务发出传出请求
这种构建应用程序的方式传统上被称为*面向服务的架构*SOA最近这个想法被细化为*微服务*架构 [[45](ch01.html#Newman2021_ch1), [46](ch01.html#Richardson2014)]。在这种架构中每个服务都有一个明确定义的目的例如在S3的情况下这将是文件存储每个服务都暴露一个可以通过网络由客户端调用的API并且每个服务都有一个负责其维护的团队。因此一个复杂的应用程序可以被分解为多个互动的服务每个服务由一个单独的团队管理。
将复杂的软件分解为多个服务有几个优点每个服务都可以独立更新减少团队间的协调工作每个服务可以被分配其所需的硬件资源通过在API后面隐藏实现细节服务所有者可以自由更改实现而不影响客户端。在数据存储方面通常每个服务都有自己的数据库并且服务之间不共享数据库共享数据库将有效地使整个数据库结构成为服务API的一部分然后更改该结构将会很困难。共享的数据库还可能导致一个服务的查询负面影响其他服务的性能。
另一方面,拥有许多服务本身可能产生复杂性:每个服务都需要基础设施来部署新版本,调整分配的硬件资源以匹配负载,收集日志,监控服务健康,并在出现问题时通知值班工程师。*编排*框架如Kubernetes已成为部署服务的流行方式因为它们为这些基础设施提供了基础。在开发过程中测试服务可能很复杂因为您还需要运行它所依赖的所有其他服务。
微服务API的演进可能具有挑战性。调用API的客户端希望API具有某些字段。开发人员可能希望根据业务需求的变化添加或删除API中的字段但这样做可能导致客户端失败。更糟糕的是这种失败通常直到开发周期后期当更新的服务API部署到暂存或生产环境时才被发现。API描述标准如OpenAPI和gRPC有助于管理客户端和服务器API之间的关系我们将在[链接待补充]中进一步讨论这些内容。
微服务主要是对人的问题的技术解决方案:允许不同团队独立进展,无需彼此协调。这在大公司中很有价值,但在小公司中,如果没有许多团队,使用微服务可能是不必要的开销,更倾向于以最简单的方式实现应用程序 [[45](ch01.html#Newman2021_ch1)]。
*无服务器*,或*功能即服务*FaaS是部署服务的另一种方法其中基础设施的管理被外包给云供应商 [[29](ch01.html#Jonas2019)]。使用虚拟机时,您必须明确选择何时启动或关闭实例;相比之下,在无服务器模型中,云提供商根据对您服务的传入请求,自动分配和释放硬件资源 [[47](ch01.html#Shahrad2020)]。“无服务器”的术语可能会产生误导:每个无服务器功能执行仍然在服务器上运行,但后续执行可能在不同的服务器上进行。
就像云存储用计量计费模式取代了容量规划(提前决定购买多少硬盘)一样,无服务器方法正在将计量计费带到代码执行:您只需为应用程序代码实际运行的时间付费,而不必提前预配资源。
The most common way of distributing a system across multiple machines is to divide them into clients and servers, and let the clients make requests to the servers. Most commonly HTTP is used for this communication, as we will discuss in [Link to Come]. The same process may be both a server (handling incoming requests) and a client (making outbound requests to other services).
This way of building applications has traditionally been called a *service-oriented architecture* (SOA); more recently the idea has been refined into a *microservices* architecture [[45](ch01.html#Newman2021_ch1), [46](ch01.html#Richardson2014)]. In this architecture, a service has one well-defined purpose (for example, in the case of S3, this would be file storage); each service exposes an API that can be called by clients via the network, and each service has one team that is responsible for its maintenance. A complex application can thus be decomposed into multiple interacting services, each managed by a separate team.
There are several advantages to breaking down a complex piece of software into multiple services: each service can be updated independently, reducing coordination effort among teams; each service can be assigned the hardware resources it needs; and by hiding the implementation details behind an API, the service owners are free to change the implementation without affecting clients. In terms of data storage, it is common for each service to have its own databases, and not to share databases between services: sharing a database would effectively make the entire database structure a part of the services API, and then that structure would be difficult to change. Shared databases could also cause one services queries to negatively impact the performance of other services.
On the other hand, having many services can itself breed complexity: each service requires infrastructure for deploying new releases, adjusting the allocated hardware resources to match the load, collecting logs, monitoring service health, and alerting an on-call engineer in the case of a problem. *Orchestration* frameworks such as Kubernetes have become a popular way of deploying services, since they provide a foundation for this infrastructure. Testing a service during development can be complicated, since you also need to run all the other services that it depends on.
Microservice APIs can be challenging to evolve. Clients that call an API expect the API to have certain fields. Developers might wish to add or remove fields to an API as business needs change, but doing so can cause clients to fail. Worse still, such failures are often not discovered until late in the development cycle when the updated service API is deployed to a staging or production environment. API description standards such as OpenAPI and gRPC help manage the relationship between client and server APIs; we discuss these further in [Link to Come].
Microservices are primarily a technical solution to a people problem: allowing different teams to make progress independently without having to coordinate with each other. This is valuable in a large company, but in a small company where there are not many teams, using microservices is likely to be unnecessary overhead, and it is preferable to implement the application in the simplest way possible [[45](ch01.html#Newman2021_ch1)].
*Serverless*, or *function-as-a-service* (FaaS), is another approach to deploying services, in which the management of the infrastructure is outsourced to a cloud vendor [[29](ch01.html#Jonas2019)]. When using virtual machines, you have to explicitly choose when to start up or shut down an instance; in contrast, with the serverless model, the cloud provider automatically allocates and frees hardware resources as needed, based on the incoming requests to your service [[47](ch01.html#Shahrad2020)]. The term “serverless” can misleading: each serverless function execution still runs on a server, but subsequent executions might run on a different one.
Just like cloud storage replaced capacity planning (deciding in advance how many disks to buy) with a metered billing model, the serverless approach is bringing metered billing to code execution: you only pay for the time that your application code is actually running, rather than having to provision resources in advance.
--------
### 云计算与超算
云计算并非构建大规模计算系统的唯一方式;另一种选择是*高性能计算*HPC也称为*超级计算*。虽然有一些重叠但HPC通常有不同的优先级并采用与云计算和企业数据中心系统不同的技术。其中一些差异包括
- 超级计算机通常用于计算密集型的科学计算任务,如天气预报、分子动力学(模拟原子和分子的运动)、复杂的优化问题和求解偏微分方程。另一方面,云计算倾向于用于在线服务、商业数据系统和需要高可用性服务用户请求的类似系统。
- 超级计算机通常运行大型批处理作业,这些作业会不时地将计算状态检查点保存到磁盘。如果节点失败,一个常见的解决方案是简单地停止整个集群工作,修复故障节点,然后从最后一个检查点重新开始计算 [[48](ch01.html#Barroso2018), [49](ch01.html#Fiala2012)]。在云服务中,通常不希望停止整个集群,因为服务需要持续地以最小的中断服务于用户。
- 超级计算机通常由专用硬件构建,每个节点都相当可靠。云服务中的节点通常由商品机构建,这些商品机由于规模经济可以以较低成本提供等效性能,但也具有更高的故障率(见[“硬件和软件故障”](ch02.html#sec_introduction_hardware_faults))。
- 超级计算机节点通常通过共享内存和远程直接内存访问RDMA进行通信这支持高带宽和低延迟但假设系统用户之间有高度的信任 [[50](ch01.html#KornfeldSimpson2020)]。在云计算中,网络和机器经常由互不信任的组织共享,需要更强的安全机制,如资源隔离(例如,虚拟机)、加密和认证。
- 云数据中心网络通常基于IP和以太网按Clos拓扑排列以提供高切面带宽——这是衡量网络整体性能的常用指标 [[48](ch01.html#Barroso2018), [51](ch01.html#Singh2015)]。超级计算机通常使用专用的网络拓扑,如多维网格和环面 [[52](ch01.html#Lockwood2014)]这为具有已知通信模式的HPC工作负载提供了更好的性能。
- 云计算允许节点分布在多个地理位置,而超级计算机通常假设其所有节点都靠近在一起。
大规模分析系统有时与超级计算共享一些特征,这就是为什么如果您在这一领域工作,了解这些技术可能是值得的。然而,本书主要关注需要持续可用的服务,如[“可靠性和容错”](ch02.html#sec_introduction_reliability)中所讨论的。
Cloud computing is not the only way of building large-scale computing systems; an alternative is *high-performance computing* (HPC), also known as *supercomputing*. Although there are overlaps, HPC often has different priorities and uses different techniques compared to cloud computing and enterprise datacenter systems. Some of those differences are:
- Supercomputers are typically used for computationally intensive scientific computing tasks, such as weather forecasting, molecular dynamics (simulating the movement of atoms and molecules), complex optimization problems, and solving partial differential equations. On the other hand, cloud computing tends to be used for online services, business data systems, and similar systems that need to serve user requests with high availability.
- A supercomputer typically runs large batch jobs that checkpoint the state of their computation to disk from time to time. If a node fails, a common solution is to simply stop the entire cluster workload, repair the faulty node, and then restart the computation from the last checkpoint [[48](ch01.html#Barroso2018), [49](ch01.html#Fiala2012)]. With cloud services, it is usually not desirable to stop the entire cluster, since the services need to continually serve users with minimal interruptions.
- Supercomputers are typically built from specialized hardware, where each node is quite reliable. Nodes in cloud services are usually built from commodity machines, which can provide equivalent performance at lower cost due to economies of scale, but which also have higher failure rates (see [“Hardware and Software Faults”](ch02.html#sec_introduction_hardware_faults)).
- Supercomputer nodes typically communicate through shared memory and remote direct memory access (RDMA), which support high bandwidth and low latency, but assume a high level of trust among the users of the system [[50](ch01.html#KornfeldSimpson2020)]. In cloud computing, the network and the machines are often shared by mutually untrusting organizations, requiring stronger security mechanisms such as resource isolation (e.g., virtual machines), encryption and authentication.
- Cloud datacenter networks are often based on IP and Ethernet, arranged in Clos topologies to provide high bisection bandwidth—a commonly used measure of a networks overall performance [[48](ch01.html#Barroso2018), [51](ch01.html#Singh2015)]. Supercomputers often use specialized network topologies, such as multi-dimensional meshes and toruses [[52](ch01.html#Lockwood2014)], which yield better performance for HPC workloads with known communication patterns.
- Cloud computing allows nodes to be distributed across multiple geographic locations, whereas supercomputers generally assume that all of their nodes are close together.
Large-scale analytics systems sometimes share some characteristics with supercomputing, which is why it can be worth knowing about these techniques if you are working in this area. However, this book is mostly concerned with services that need to be continually available, as discussed in [“Reliability and Fault Tolerance”](ch02.html#sec_introduction_reliability).
--------
## 数据系统,法律与社会
到目前为止,您已经看到本章中数据系统的架构不仅受到技术目标和需求的影响,还受到它们支持的组织的人类需求的影响。越来越多的数据系统工程师意识到,仅仅满足自己业务的需求是不够的:我们还对整个社会负有责任。
特别需要关注的是存储关于人们及其行为的数据的系统。自2018年以来*通用数据保护条例*GDPR为许多欧洲国家的居民提供了更大的控制权和法律权利用以管理他们的个人数据类似的隐私法规也在世界各地的不同国家和地区得到采纳例如加利福尼亚消费者隐私法案CCPA。围绕人工智能的法规如*欧盟人工智能法案*,对个人数据的使用施加了进一步的限制。
此外,即使在不直接受法规约束的领域,也越来越多地认识到计算机系统对人和社会的影响。社交媒体改变了个人获取新闻的方式,这影响了他们的政治观点,从而可能影响选举结果。自动化系统越来越多地做出对个人有深远影响的决定,例如决定谁应获得贷款或保险,谁应被邀请参加工作面试,或者谁应被怀疑犯有罪行 [[53](ch01.html#ONeil2016_ch1)]。
从事这些系统的每个人都负有考虑其伦理影响并确保遵守相关法律的责任。并不是每个人都必须成为法律和伦理的专家,但基本的法律和伦理原则意识与分布式系统的一些基础知识同样重要。
法律考量正在影响数据系统设计的基础 [[54](ch01.html#Shastri2020)]。例如GDPR授予个人在请求时删除其数据的权利有时称为*被遗忘权*)。然而,正如我们在本书中将看到的,许多数据系统依赖于不可变构造,如作为设计一部分的仅追加日志;我们如何确保在一个本应不可变的文件中删除某些数据?我们如何处理已并入派生数据集的数据的删除问题(见[“记录系统与派生数据”](ch01.html#sec_introduction_derived)),如机器学习模型的训练数据?回答这些问题创造了新的工程挑战。
目前我们没有明确的指南来判断哪些特定技术或系统架构应被视为“符合GDPR”的。法规故意没有规定特定的技术因为这些可能随着技术的进步而迅速变化。相反法律文本提出了需要解释的高级原则。这意味着关于如何遵守隐私法规的问题没有简单的答案但我们将通过这个视角审视本书中的一些技术。
一般来说,我们存储数据是因为我们认为其价值大于存储它的成本。然而,值得记住的是,存储成本不仅仅是您为亚马逊 S3 或其他服务支付的账单:成本效益计算还应考虑数据泄露或被敌对方妥协时的责任和声誉损害风险,以及如果数据的存储和处理被发现不符合法律的风险,还有法律费用和罚款的风险。
政府或警察部门也可能强制公司交出数据。当存在数据可能揭示被刑事化行为的风险时(例如,在几个中东和非洲国家的同性恋行为,或在几个美国州寻求堕胎),存储该数据为用户创造了真正的安全风险。例如,通过位置数据很容易揭露到堕胎诊所的旅行,甚至可能通过一段时间内用户 IP 地址的日志(表明大致位置)揭露。
一旦考虑到所有风险,可能会合理地决定某些数据根本不值得存储,因此应该将其删除。*数据最小化*原则(有时称为德语术语*Datensparsamkeit*)与存储大量数据的“大数据”哲学相悖,以防它在未来证明有用 [[55](ch01.html#Datensparsamkeit)]。但这与 GDPR 相符,后者规定只能为特定的、明确的目的收集个人数据,这些数据以后不能用于任何其他目的,且为了收集目的,保存的数据不得超过必要的时间 [[56](ch01.html#GDPR)]。
企业也注意到了隐私和安全问题。信用卡公司要求支付处理业务遵守严格的支付卡行业PCI标准。处理者经常接受独立审计师的评估以验证持续合规。软件供应商也看到了增加的审查。现在许多买家要求其供应商符合服务组织控制SOC类型 2 标准。与 PCI 合规一样,供应商接受第三方审计以验证遵守情况。
总的来说,平衡您的业务需求与您收集和处理的数据的人的需求很重要。这个话题还有更多内容;在[链接待补充]中,我们将更深入地探讨伦理和法律合规问题,包括偏见和歧视的问题。
So far youve seen in this chapter that the architecture of data systems is influenced not only by technical goals and requirements, but also by the human needs of the organizations that they support. Increasingly, data systems engineers are realizing that serving the needs of their own business is not enough: we also have a responsibility towards society at large.
One particular concern are systems that store data about people and their behavior. Since 2018 the *General Data Protection Regulation* (GDPR) has given residents of many European countries greater control and legal rights over their personal data, and similar privacy regulation has been adopted in various other countries and states around the world, including for example the California Consumer Privacy Act (CCPA). Regulations around AI, such as the *EU AI Act*, place further restrictions on how personal data can be used.
Moreover, even in areas that are not directly subject to regulation, there is increasing recognition of the effects that computer systems have on people and society. Social media has changed how individuals consume news, which influences their political opinions and hence may affect the outcome of elections. Automated systems increasingly make decisions that have profound consequences for individuals, such as deciding who should be given a loan or insurance coverage, who should be invited to a job interview, or who should be suspected of a crime [[53](ch01.html#ONeil2016_ch1)].
Everyone who works on such systems shares a responsibility for considering the ethical impact and ensuring that they comply with relevant law. It is not necessary for everybody to become an expert in law and ethics, but a basic awareness of legal and ethical principles is just as important as, say, some foundational knowledge in distributed systems.
Legal considerations are influencing the very foundations of how data systems are being designed [[54](ch01.html#Shastri2020)]. For example, the GDPR grants individuals the right to have their data erased on request (sometimes known as the *right to be forgotten*). However, as we shall see in this book, many data systems rely on immutable constructs such as append-only logs as part of their design; how can we ensure deletion of some data in the middle of a file that is supposed to be immutable? How do we handle deletion of data that has been incorporated into derived datasets (see [“Systems of Record and Derived Data”](ch01.html#sec_introduction_derived)), such as training data for machine learning models? Answering these questions creates new engineering challenges.
At present we dont have clear guidelines on which particular technologies or system architectures should be considered “GDPR-compliant” or not. The regulation deliberately does not mandate particular technologies, because these may quickly change as technology progresses. Instead, the legal texts set out high-level principles that are subject to interpretation. This means that there are no simple answers to the question of how to comply with privacy regulation, but we will look at some of the technologies in this book through this lens.
In general, we store data because we think that its value is greater than the costs of storing it. However, it is worth remembering that the costs of storage are not just the bill you pay for Amazon S3 or another service: the cost-benefit calculation should also take into account the risks of liability and reputational damage if the data were to be leaked or compromised by adversaries, and the risk of legal costs and fines if the storage and processing of the data is found not to be compliant with the law.
Governments or police forces might also compel companies to hand over data. When there is a risk that the data may reveal criminalized behaviors (for example, homosexuality in several Middle Eastern and African countries, or seeking an abortion in several US states), storing that data creates real safety risks for users. Travel to an abortion clinic, for example, could easily be revealed by location data, perhaps even by a log of the users IP addresses over time (which indicate approximate location).
Once all the risks are taken into account, it might be reasonable to decide that some data is simply not worth storing, and that it should therefore be deleted. This principle of *data minimization* (sometimes known by the German term *Datensparsamkeit*) runs counter to the “big data” philosophy of storing lots of data speculatively in case it turns out to be useful in the future [[55](ch01.html#Datensparsamkeit)]. But it fits with the GDPR, which mandates that personal data many only be collected for a specified, explicit purpose, that this data may not later be used for any other purpose, and that the data must not be kept for longer than necessary for the purposes for which it was collected [[56](ch01.html#GDPR)].
Businesses have also taken notice of privacy and safety concerns. Credit card companies require payment processing businesses to adhere to strict payment card industry (PCI) standards. Processors undergo frequent evaluations from independent auditors to verify continued compliance. Software vendors have also seen increased scrutiny. Many buyers now require their vendors to comply with Service Organization Control (SOC) Type 2 standards. As with PCI compliance, vendors undergo third party audits to verify adherence.
Generally, it is important to balance the needs of your business against the needs of the people whose data you are collecting and processing. There is much more to this topic; in [Link to Come] we will go deeper into the topics of ethics and legal compliance, including the problems of bias and discrimination.
--------
## 本章小结
本章的主题是理解权衡:即,认识到对于许多问题并没有唯一的正确答案,而是有几种不同的方法,每种方法都有各自的优缺点。我们探讨了影响数据系统架构的一些重要选择,并介绍了在本书余下部分将需要用到的术语。
我们首先区分了操作型事务处理OLTP和分析型OLAP系统并看到了它们的不同特点不仅管理不同类型的数据访问模式也不同而且服务于不同的受众。我们遇到了数据仓库和数据湖的概念这些系统通过 ETL 从业务系统接收数据。在[链接待补充]中,我们将看到,由于需要服务的查询类型不同,操作型和分析型系统通常使用非常不同的内部数据布局。
然后,我们比较了云服务(一种相对较新的发展)和之前主导数据系统架构的传统自托管软件范式。这两种方法哪种更具成本效益很大程度上取决于您的具体情况,但不可否认的是,云原生方法正在改变数据系统的架构方式,例如它们如何分离存储和计算。
云系统本质上是分布式的,我们简要考察了与使用单一机器相比,分布式系统的一些权衡。在某些情况下,您无法避免采用分布式,但如果有可能保持在单一机器上,建议不要急于使系统分布式化。在[链接待补充]和[链接待补充]中,我们将更详细地介绍分布式系统的挑战。
最后,我们看到,数据系统架构不仅由部署系统的业务需求决定,还由保护被处理数据人员权利的隐私法规决定——这是许多工程师容易忽视的一个方面。如何将法律要求转化为技术实现尚未被充分理解,但在我们翻阅本书的其余部分时,保持对这个问题的关注是很重要的。
The theme of this chapter has been to understand trade-offs: that is, to recognize that for many questions there is not one right answer, but several different approaches that each have various pros and cons. We explored some of the most important choices that affect the architecture of data systems, and introduced terminology that will be needed throughout the rest of this book.
We started by making a distinction between operational (transaction-processing, OLTP) and analytical (OLAP) systems, and saw their different characteristics: not only managing different types of data with different access patterns, but also serving different audiences. We encountered the concept of a data warehouse and data lake, which receive data feeds from operational systems via ETL. In [Link to Come] we will see that operational and analytical systems often use very different internal data layouts because of the different types of queries they need to serve.
We then compared cloud services, a comparatively recent development, to the traditional paradigm of self-hosted software that has previously dominated data systems architecture. Which of these approaches is more cost-effective depends a lot on your particular situation, but its undeniable that cloud-native approaches are bringing big changes to the way data systems are architected, for example in the way they separate storage and compute.
Cloud systems are intrinsically distributed, and we briefly examined some of the trade-offs of distributed systems compared to using a single machine. There are situations in which you cant avoid going distributed, but its advisable not to rush into making a system distributed if its possible to keep it on a single machine. In [Link to Come] and [Link to Come] we will cover the challenges with distributed systems in more detail.
Finally, we saw that data systems architecture is determined not only by the needs of the business deploying the system, but also by privacy regulation that protects the rights of the people whose data is being processed—an aspect that many engineers are prone to ignoring. How we translate legal requirements into technical implementations is not yet well understood, but its important to keep this question in mind as we move through the rest of this book.
--------
## 参考文献
[[1](ch01.html#Kouzes2009-marker)] Richard T. Kouzes, Gordon A. Anderson, Stephen T. Elbert, Ian Gorton, and Deborah K. Gracio. [The Changing Paradigm of Data-Intensive Computing](http://www2.ic.uff.br/~boeres/slides_AP/papers/TheChanginParadigmDataIntensiveComputing_2009.pdf). *IEEE Computer*, volume 42, issue 1, January 2009. [doi:10.1109/MC.2009.26](https://doi.org/10.1109/MC.2009.26)
[[2](ch01.html#Kleppmann2019-marker)] Martin Kleppmann, Adam Wiggins, Peter van Hardenberg, and Mark McGranaghan. [Local-first software: you own your data, in spite of the cloud](https://www.inkandswitch.com/local-first/). At *2019 ACM SIGPLAN International Symposium on New Ideas, New Paradigms, and Reflections on Programming and Software* (Onward!), October 2019. [doi:10.1145/3359591.3359737](https://doi.org/10.1145/3359591.3359737)
[[3](ch01.html#Reis2022-marker)] Joe Reis and Matt Housley. [*Fundamentals of Data Engineering*](https://www.oreilly.com/library/view/fundamentals-of-data/9781098108298/). OReilly Media, 2022. ISBN: 9781098108304
[[4](ch01.html#Machado2023-marker)] Rui Pedro Machado and Helder Russa. [*Analytics Engineering with SQL and dbt*](https://www.oreilly.com/library/view/analytics-engineering-with/9781098142377/). OReilly Media, 2023. ISBN: 9781098142384
[[5](ch01.html#Codd1993-marker)] Edgar F. Codd, S. B. Codd, and C. T. Salley. [Providing OLAP to User-Analysts: An IT Mandate](http://www.estgv.ipv.pt/PaginasPessoais/jloureiro/ESI_AID2007_2008/fichas/codd.pdf). E. F. Codd Associates, 1993. Archived at [perma.cc/RKX8-2GEE](https://perma.cc/RKX8-2GEE)
[[6](ch01.html#Chaudhuri1997-marker)] Surajit Chaudhuri and Umeshwar Dayal. [An Overview of Data Warehousing and OLAP Technology](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/sigrecord.pdf). *ACM SIGMOD Record*, volume 26, issue 1, pages 6574, March 1997. [doi:10.1145/248603.248616](https://doi.org/10.1145/248603.248616)
[[7](ch01.html#Ozcan2017-marker)] Fatma Özcan, Yuanyuan Tian, and Pinar Tözün. [Hybrid Transactional/Analytical Processing: A Survey](https://humming80.github.io/papers/sigmod-htaptut.pdf). At *ACM International Conference on Management of Data* (SIGMOD), May 2017. [doi:10.1145/3035918.3054784](https://doi.org/10.1145/3035918.3054784)
[[8](ch01.html#Prout2022-marker)] Adam Prout, Szu-Po Wang, Joseph Victor, Zhou Sun, Yongzhu Li, Jack Chen, Evan Bergeron, Eric Hanson, Robert Walzer, Rodrigo Gomes, and Nikita Shamgunov. [Cloud-Native Transactions and Analytics in SingleStore](https://dl.acm.org/doi/abs/10.1145/3514221.3526055). At *International Conference on Management of Data* (SIGMOD), June 2022. [doi:10.1145/3514221.3526055](https://doi.org/10.1145/3514221.3526055)
[[9](ch01.html#Stonebraker2005fitsall-marker)] Michael Stonebraker and Uğur Çetintemel. [One Size Fits All: An Idea Whose Time Has Come and Gone](https://pages.cs.wisc.edu/~shivaram/cs744-readings/fits_all.pdf). At *21st International Conference on Data Engineering* (ICDE), April 2005. [doi:10.1109/ICDE.2005.1](https://doi.org/10.1109/ICDE.2005.1)
[[10](ch01.html#Cohen2009-marker)] Jeffrey Cohen, Brian Dolan, Mark Dunlap, Joseph M Hellerstein, and Caleb Welton. [MAD Skills: New Analysis Practices for Big Data](http://www.vldb.org/pvldb/vol2/vldb09-219.pdf). *Proceedings of the VLDB Endowment*, volume 2, issue 2, pages 14811492, August 2009. [doi:10.14778/1687553.1687576](https://doi.org/10.14778/1687553.1687576)
[[11](ch01.html#Olteanu2020-marker)] Dan Olteanu. [The Relational Data Borg is Learning](http://www.vldb.org/pvldb/vol13/p3502-olteanu.pdf). *Proceedings of the VLDB Endowment*, volume 13, issue 12, August 2020. [doi:10.14778/3415478.3415572](https://doi.org/10.14778/3415478.3415572)
[[12](ch01.html#Bornstein2020-marker)] Matt Bornstein, Martin Casado, and Jennifer Li. [Emerging Architectures for Modern Data Infrastructure: 2020](https://future.a16z.com/emerging-architectures-for-modern-data-infrastructure-2020/). *future.a16z.com*, October 2020. Archived at [perma.cc/LF8W-KDCC](https://perma.cc/LF8W-KDCC)
[[13](ch01.html#Fowler2015-marker)] Martin Fowler. [DataLake](https://www.martinfowler.com/bliki/DataLake.html). *martinfowler.com*, February 2015. Archived at [perma.cc/4WKN-CZUK](https://perma.cc/4WKN-CZUK)
[[14](ch01.html#Johnson2015-marker)] Bobby Johnson and Joseph Adler. [The Sushi Principle: Raw Data Is Better](https://learning.oreilly.com/videos/strata-hadoop/9781491924143/9781491924143-video210840/). At *Strata+Hadoop World*, February 2015.
[[15](ch01.html#Armbrust2021-marker)] Michael Armbrust, Ali Ghodsi, Reynold Xin, and Matei Zaharia. [Lakehouse: A New Generation of Open Platforms that Unify Data Warehousing and Advanced Analytics](https://www.cidrdb.org/cidr2021/papers/cidr2021_paper17.pdf). At *11th Annual Conference on Innovative Data Systems Research* (CIDR), January 2021.
[[16](ch01.html#DataOps-marker)] DataKitchen, Inc. [The DataOps Manifesto](https://dataopsmanifesto.org/en/). *dataopsmanifesto.org*, 2017. Archived at [perma.cc/3F5N-FUQ4](https://perma.cc/3F5N-FUQ4)
[[17](ch01.html#Manohar2021-marker)] Tejas Manohar. [What is Reverse ETL: A Definition & Why Its Taking Off](https://hightouch.io/blog/reverse-etl/). *hightouch.io*, November 2021. Archived at [perma.cc/A7TN-GLYJ](https://perma.cc/A7TN-GLYJ)
[[18](ch01.html#ORegan2018-marker)] Simon ORegan. [Designing Data Products](https://towardsdatascience.com/designing-data-products-b6b93edf3d23). *towardsdatascience.com*, August 2018. Archived at [perma.cc/HU67-3RV8](https://perma.cc/HU67-3RV8)
[[19](ch01.html#Fournier2021-marker)] Camille Fournier. [Why is it so hard to decide to buy?](https://skamille.medium.com/why-is-it-so-hard-to-decide-to-buy-d86fee98e88e) *skamille.medium.com*, July 2021. Archived at [perma.cc/6VSG-HQ5X](https://perma.cc/6VSG-HQ5X)
[[20](ch01.html#HeinemeierHansson2022-marker)] David Heinemeier Hansson. [Why were leaving the cloud](https://world.hey.com/dhh/why-we-re-leaving-the-cloud-654b47e0). *world.hey.com*, October 2022. Archived at [perma.cc/82E6-UJ65](https://perma.cc/82E6-UJ65)
[[21](ch01.html#Badizadegan2022-marker)] Nima Badizadegan. [Use One Big Server](https://specbranch.com/posts/one-big-server/). *specbranch.com*, August 2022. Archived at [perma.cc/M8NB-95UK](https://perma.cc/M8NB-95UK)
[[22](ch01.html#Yegge2020-marker)] Steve Yegge. [Dear Google Cloud: Your Deprecation Policy is Killing You](https://steve-yegge.medium.com/dear-google-cloud-your-deprecation-policy-is-killing-you-ee7525dc05dc). *steve-yegge.medium.com*, August 2020. Archived at [perma.cc/KQP9-SPGU](https://perma.cc/KQP9-SPGU)
[[23](ch01.html#Verbitski2017-marker)] Alexandre Verbitski, Anurag Gupta, Debanjan Saha, Murali Brahmadesam, Kamal Gupta, Raman Mittal, Sailesh Krishnamurthy, Sandor Maurice, Tengiz Kharatishvili, and Xiaofeng Bao. [Amazon Aurora: Design Considerations for High Throughput Cloud-Native Relational Databases](https://media.amazonwebservices.com/blog/2017/aurora-design-considerations-paper.pdf). At *ACM International Conference on Management of Data* (SIGMOD), pages 10411052, May 2017. [doi:10.1145/3035918.3056101](https://doi.org/10.1145/3035918.3056101)
[[24](ch01.html#Antonopoulos2019_ch1-marker)] Panagiotis Antonopoulos, Alex Budovski, Cristian Diaconu, Alejandro Hernandez Saenz, Jack Hu, Hanuma Kodavalla, Donald Kossmann, Sandeep Lingam, Umar Farooq Minhas, Naveen Prakash, Vijendra Purohit, Hugh Qu, Chaitanya Sreenivas Ravella, Krystyna Reisteter, Sheetal Shrotri, Dixin Tang, and Vikram Wakade. [Socrates: The New SQL Server in the Cloud](https://www.microsoft.com/en-us/research/uploads/prod/2019/05/socrates.pdf). At *ACM International Conference on Management of Data* (SIGMOD), pages 17431756, June 2019. [doi:10.1145/3299869.3314047](https://doi.org/10.1145/3299869.3314047)
[[25](ch01.html#Vuppalapati2020-marker)] Midhul Vuppalapati, Justin Miron, Rachit Agarwal, Dan Truong, Ashish Motivala, and Thierry Cruanes. [Building An Elastic Query Engine on Disaggregated Storage](https://www.usenix.org/system/files/nsdi20-paper-vuppalapati.pdf). At *17th USENIX Symposium on Networked Systems Design and Implementation* (NSDI), February 2020.
[[26](ch01.html#Shapira2023-marker)] Gwen Shapira. [Compute-Storage Separation Explained](https://www.thenile.dev/blog/storage-compute). *thenile.dev*, January 2023. Archived at [perma.cc/QCV3-XJNZ](https://perma.cc/QCV3-XJNZ)
[[27](ch01.html#Murthy2022-marker)] Ravi Murthy and Gurmeet Goindi. [AlloyDB for PostgreSQL under the hood: Intelligent, database-aware storage](https://cloud.google.com/blog/products/databases/alloydb-for-postgresql-intelligent-scalable-storage). *cloud.google.com*, May 2022. Archived at [archive.org](https://web.archive.org/web/20220514021120/https://cloud.google.com/blog/products/databases/alloydb-for-postgresql-intelligent-scalable-storage)
[[28](ch01.html#Vanlightly2023-marker)] Jack Vanlightly. [The Architecture of Serverless Data Systems](https://jack-vanlightly.com/blog/2023/11/14/the-architecture-of-serverless-data-systems). *jack-vanlightly.com*, November 2023. Archived at [perma.cc/UDV4-TNJ5](https://perma.cc/UDV4-TNJ5)
[[29](ch01.html#Jonas2019-marker)] Eric Jonas, Johann Schleier-Smith, Vikram Sreekanti, Chia-Che Tsai, Anurag Khandelwal, Qifan Pu, Vaishaal Shankar, Joao Carreira, Karl Krauth, Neeraja Yadwadkar, Joseph E Gonzalez, Raluca Ada Popa, Ion Stoica, David A Patterson. [Cloud Programming Simplified: A Berkeley View on Serverless Computing](https://arxiv.org/abs/1902.03383). *arxiv.org*, February 2019.
[[30](ch01.html#Beyer2016-marker)] Betsy Beyer, Jennifer Petoff, Chris Jones, and Niall Richard Murphy. [*Site Reliability Engineering: How Google Runs Production Systems*](https://www.oreilly.com/library/view/site-reliability-engineering/9781491929117/). OReilly Media, 2016. ISBN: 9781491929124
[[31](ch01.html#Limoncelli2020-marker)] Thomas Limoncelli. [The Time I Stole $10,000 from Bell Labs](https://queue.acm.org/detail.cfm?id=3434773). *ACM Queue*, volume 18, issue 5, November 2020. [doi:10.1145/3434571.3434773](https://doi.org/10.1145/3434571.3434773)
[[32](ch01.html#Majors2020-marker)] Charity Majors. [The Future of Ops Jobs](https://acloudguru.com/blog/engineering/the-future-of-ops-jobs). *acloudguru.com*, August 2020. Archived at [perma.cc/GRU2-CZG3](https://perma.cc/GRU2-CZG3)
[[33](ch01.html#Cherkasky2021-marker)] Boris Cherkasky. [(Over)Pay As You Go for Your Datastore](https://medium.com/riskified-technology/over-pay-as-you-go-for-your-datastore-11a29ae49a8b). *medium.com*, September 2021. Archived at [perma.cc/Q8TV-2AM2](https://perma.cc/Q8TV-2AM2)
[[34](ch01.html#Kushchi2023-marker)] Shlomi Kushchi. [Serverless Doesnt Mean DevOpsLess or NoOps](https://thenewstack.io/serverless-doesnt-mean-devopsless-or-noops/). *thenewstack.io*, February 2023. Archived at [perma.cc/3NJR-AYYU](https://perma.cc/3NJR-AYYU)
[[35](ch01.html#Bernhardsson2021-marker)] Erik Bernhardsson. [Storm in the stratosphere: how the cloud will be reshuffled](https://erikbern.com/2021/11/30/storm-in-the-stratosphere-how-the-cloud-will-be-reshuffled.html). *erikbern.com*, November 2021. Archived at [perma.cc/SYB2-99P3](https://perma.cc/SYB2-99P3)
[[36](ch01.html#Stancil2021-marker)] Benn Stancil. [The data OS](https://benn.substack.com/p/the-data-os). *benn.substack.com*, September 2021. Archived at [perma.cc/WQ43-FHS6](https://perma.cc/WQ43-FHS6)
[[37](ch01.html#Korolov2022-marker)] Maria Korolov. [Data residency laws pushing companies toward residency as a service](https://www.csoonline.com/article/3647761/data-residency-laws-pushing-companies-toward-residency-as-a-service.html). *csoonline.com*, January 2022. Archived at [perma.cc/CHE4-XZZ2](https://perma.cc/CHE4-XZZ2)
[[38](ch01.html#Nath2019-marker)] Kousik Nath. [These are the numbers every computer engineer should know](https://www.freecodecamp.org/news/must-know-numbers-for-every-computer-engineer/). *freecodecamp.org*, September 2019. Archived at [perma.cc/RW73-36RL](https://perma.cc/RW73-36RL)
[[39](ch01.html#Hellerstein2019-marker)] Joseph M Hellerstein, Jose Faleiro, Joseph E Gonzalez, Johann Schleier-Smith, Vikram Sreekanti, Alexey Tumanov, and Chenggang Wu. [Serverless Computing: One Step Forward, Two Steps Back](https://arxiv.org/abs/1812.03651). At *Conference on Innovative Data Systems Research* (CIDR), January 2019.
[[40](ch01.html#McSherry2015_ch1-marker)] Frank McSherry, Michael Isard, and Derek G. Murray. [Scalability! But at What COST?](https://www.usenix.org/system/files/conference/hotos15/hotos15-paper-mcsherry.pdf) At *15th USENIX Workshop on Hot Topics in Operating Systems* (HotOS), May 2015.
[[41](ch01.html#Sridharan2018-marker)] Cindy Sridharan. *[Distributed Systems Observability: A Guide to Building Robust Systems](https://unlimited.humio.com/rs/756-LMY-106/images/Distributed-Systems-Observability-eBook.pdf)*. Report, OReilly Media, May 2018. Archived at [perma.cc/M6JL-XKCM](https://perma.cc/M6JL-XKCM)
[[42](ch01.html#Majors2019-marker)] Charity Majors. [Observability — A 3-Year Retrospective](https://thenewstack.io/observability-a-3-year-retrospective/). *thenewstack.io*, August 2019. Archived at [perma.cc/CG62-TJWL](https://perma.cc/CG62-TJWL)
[[43](ch01.html#Sigelman2010-marker)] Benjamin H. Sigelman, Luiz André Barroso, Mike Burrows, Pat Stephenson, Manoj Plakal, Donald Beaver, Saul Jaspan, and Chandan Shanbhag. [Dapper, a Large-Scale Distributed Systems Tracing Infrastructure](https://research.google/pubs/pub36356/). Google Technical Report dapper-2010-1, April 2010. Archived at [perma.cc/K7KU-2TMH](https://perma.cc/K7KU-2TMH)
[[44](ch01.html#Laigner2021-marker)] Rodrigo Laigner, Yongluan Zhou, Marcos Antonio Vaz Salles, Yijian Liu, and Marcos Kalinowski. [Data management in microservices: State of the practice, challenges, and research directions](http://www.vldb.org/pvldb/vol14/p3348-laigner.pdf). *Proceedings of the VLDB Endowment*, volume 14, issue 13, pages 33483361, September 2021. [doi:10.14778/3484224.3484232](https://doi.org/10.14778/3484224.3484232)
[[45](ch01.html#Newman2021_ch1-marker)] Sam Newman. [*Building Microservices*, second edition](https://www.oreilly.com/library/view/building-microservices-2nd/9781492034018/). OReilly Media, 2021. ISBN: 9781492034025
[[46](ch01.html#Richardson2014-marker)] Chris Richardson. [Microservices: Decomposing Applications for Deployability and Scalability](http://www.infoq.com/articles/microservices-intro). *infoq.com*, May 2014. Archived at [perma.cc/CKN4-YEQ2](https://perma.cc/CKN4-YEQ2)
[[47](ch01.html#Shahrad2020-marker)] Mohammad Shahrad, Rodrigo Fonseca, Íñigo Goiri, Gohar Chaudhry, Paul Batum, Jason Cooke, Eduardo Laureano, Colby Tresness, Mark Russinovich, Ricardo Bianchini. [Serverless in the Wild: Characterizing and Optimizing the Serverless Workload at a Large Cloud Provider](https://www.usenix.org/system/files/atc20-shahrad.pdf). At *USENIX Annual Technical Conference* (ATC), July 2020.
[[48](ch01.html#Barroso2018-marker)] Luiz André Barroso, Urs Hölzle, and Parthasarathy Ranganathan. [The Datacenter as a Computer: Designing Warehouse-Scale Machines](https://www.morganclaypool.com/doi/10.2200/S00874ED3V01Y201809CAC046), third edition. Morgan & Claypool Synthesis Lectures on Computer Architecture, October 2018. [doi:10.2200/S00874ED3V01Y201809CAC046](https://doi.org/10.2200/S00874ED3V01Y201809CAC046)
[[49](ch01.html#Fiala2012-marker)] David Fiala, Frank Mueller, Christian Engelmann, Rolf Riesen, Kurt Ferreira, and Ron Brightwell. [Detection and Correction of Silent Data Corruption for Large-Scale High-Performance Computing](http://moss.csc.ncsu.edu/~mueller/ftp/pub/mueller/papers/sc12.pdf),” at *International Conference for High Performance Computing, Networking, Storage and Analysis* (SC), November 2012. [doi:10.1109/SC.2012.49](https://doi.org/10.1109/SC.2012.49)
[[50](ch01.html#KornfeldSimpson2020-marker)] Anna Kornfeld Simpson, Adriana Szekeres, Jacob Nelson, and Irene Zhang. [Securing RDMA for High-Performance Datacenter Storage Systems](https://www.usenix.org/conference/hotcloud20/presentation/kornfeld-simpson). At *12th USENIX Workshop on Hot Topics in Cloud Computing* (HotCloud), July 2020.
[[51](ch01.html#Singh2015-marker)] Arjun Singh, Joon Ong, Amit Agarwal, Glen Anderson, Ashby Armistead, Roy Bannon, Seb Boving, Gaurav Desai, Bob Felderman, Paulie Germano, Anand Kanagala, Jeff Provost, Jason Simmons, Eiichi Tanda, Jim Wanderer, Urs Hölzle, Stephen Stuart, and Amin Vahdat. [Jupiter Rising: A Decade of Clos Topologies and Centralized Control in Googles Datacenter Network](http://conferences.sigcomm.org/sigcomm/2015/pdf/papers/p183.pdf). At *Annual Conference of the ACM Special Interest Group on Data Communication* (SIGCOMM), August 2015. [doi:10.1145/2785956.2787508](https://doi.org/10.1145/2785956.2787508)
[[52](ch01.html#Lockwood2014-marker)] Glenn K. Lockwood. [Hadoops Uncomfortable Fit in HPC](http://glennklockwood.blogspot.co.uk/2014/05/hadoops-uncomfortable-fit-in-hpc.html). *glennklockwood.blogspot.co.uk*, May 2014. Archived at [perma.cc/S8XX-Y67B](https://perma.cc/S8XX-Y67B)
[[53](ch01.html#ONeil2016_ch1-marker)] Cathy ONeil: *Weapons of Math Destruction: How Big Data Increases Inequality and Threatens Democracy*. Crown Publishing, 2016. ISBN: 9780553418811
[[54](ch01.html#Shastri2020-marker)] Supreeth Shastri, Vinay Banakar, Melissa Wasserman, Arun Kumar, and Vijay Chidambaram. [Understanding and Benchmarking the Impact of GDPR on Database Systems](http://www.vldb.org/pvldb/vol13/p1064-shastri.pdf). *Proceedings of the VLDB Endowment*, volume 13, issue 7, pages 10641077, March 2020. [doi:10.14778/3384345.3384354](https://doi.org/10.14778/3384345.3384354)
[[55](ch01.html#Datensparsamkeit-marker)] Martin Fowler. [Datensparsamkeit](https://www.martinfowler.com/bliki/Datensparsamkeit.html). *martinfowler.com*, December 2013. Archived at [perma.cc/R9QX-CME6](https://perma.cc/R9QX-CME6)
[[56](ch01.html#GDPR-marker)] [Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016 (General Data Protection Regulation)](https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32016R0679&from=EN). *Official Journal of the European Union* L 119/1, May 2016.

913
content/v2/ch2.md Normal file
View File

@@ -0,0 +1,913 @@
---
title: "第二章:定义非功能性要求"
linkTitle: "2. 定义非功能性要求"
weight: 102
breadcrumbs: false
---
> 互联网做得太棒了,以至于大多数人将它看作像太平洋这样的自然资源,而不是什么人工产物。上一次出现这种大规模且无差错的技术,你还记得是什么时候吗?
>
> —— [艾伦・凯](http://www.drdobbs.com/architecture-and-design/interview-with-alan-kay/240003442) 在接受 Dobb 博士杂志采访时说2012 年)
--------
如果您正在构建应用程序,您将由一系列需求所驱动。在您需求列表的最顶端,很可能是应用程序必须提供的功能:需要哪些屏幕和按钮,以及每个操作应如何执行以满足软件的目的。这些是您的*功能性需求*。
此外,您可能还有一些*非功能性需求*:例如,应用应该快速、可靠、安全、合法合规,并且易于维护。这些需求可能没有明确书写下来,因为它们似乎有些显而易见,但它们和应用的功能一样重要:一个异常缓慢或不可靠的应用可能根本无法存在。
并非所有非功能性需求都属于本书的讨论范围,但有几个是如此。在本章中,我们将介绍几个技术概念,这将帮助您明确自己系统的非功能性需求:
- 如何定义和衡量系统的*性能*(见[“描述性能”](#描述性能)
- 服务*可靠*的含义——即使在出现问题时,也能继续正确工作(见[“可靠性与容错”](#可靠性与容错)
- 允许系统通过有效地增加计算能力来*可扩展*,随着系统负载的增长(见[“可伸缩性”](#可伸缩性));以及
- 长期易于维护系统(见[“可维护性”](#可维护性))。
本章引入的术语在后续章节中也将非常有用,当我们详细探讨数据密集型系统的实现方式时。然而,抽象的定义可能相当枯燥;为了使这些概念更具体,我们将从社交网络服务的案例研究开始本章,这将提供性能和可扩展性的实际示例。
If you are building an application, you will be driven by a list of requirements. At the top of your list is most likely the functionality that the application must offer: what screens and what buttons you need, and what each operation is supposed to do in order to fulfill the purpose of your software. These are your *functional requirements*.
In addition, you probably also have some *nonfunctional requirements*: for example, the app should be fast, reliable, secure, legally compliant, and easy to maintain. These requirements might not be explicitly written down, because they may seem somewhat obvious, but they are just as important as the apps functionality: an app that is unbearably slow or unreliable might as well not exist.
Not all nonfunctional requirements fall within the scope of this book, but several do. In this chapter we will introduce several technical concepts that will help you articulate the nonfunctional requirements for your own systems:
- How to define and measure the *performance* of a system (see [“Describing Performance”](ch02.html#sec_introduction_percentiles));
- What it means for a service to be *reliable*—namely, continuing to work correctly, even when things go wrong (see [“Reliability and Fault Tolerance”](ch02.html#sec_introduction_reliability));
- Allowing a system to be *scalable* by having efficient ways of adding computing capacity as the load on the system grows (see [“Scalability”](ch02.html#sec_introduction_scalability)); and
- Making it easier to maintain a system in the long term (see [“Maintainability”](ch02.html#sec_introduction_maintainability)).
The terminology introduced in this chapter will also be useful in the following chapters, when we go into the details of how data-intensive systems are implemented. However, abstract definitions can be quite dry; to make the ideas more concrete, we will start this chapter with a case study of how a social networking service might work, which will provide practical examples of performance and scalability.
--------
## 案例学习:社交网络主页时间线
假设你被分配了一个任务要实现一个类似X前身为Twitter的社交网络在这个网络中用户可以发布消息并关注其他用户。这将是对这种服务实际工作方式的极大简化 [[1](ch02.html#Cvet2016), [2](ch02.html#Krikorian2012_ch2), [3](ch02.html#Twitter2023)],但它将有助于说明大规模系统中出现的一些问题。
假设用户每天发布 5 亿条消息,平均每秒 5700 条消息。偶尔,这个速率可能会激增至每秒 150,000 条消息 [[4](ch02.html#Krikorian2013)]。我们还假设平均每个用户关注 200 人,拥有 200 名粉丝(尽管这个范围非常广泛:大多数人只有少数几个粉丝,而像巴拉克·奥巴马这样的名人粉丝超过 1 亿)。
Imagine you are given the task of implementing a social network in the style of X (formerly Twitter), in which users can post messages and follow other users. This will be a huge simplification of how such a service actually works [[1](ch02.html#Cvet2016), [2](ch02.html#Krikorian2012_ch2), [3](ch02.html#Twitter2023)], but it will help illustrate some of the issues that arise in large-scale systems.
Lets assume that users make 500 million posts per day, or 5,700 posts per second on average. Occasionally, the rate can spike as high as 150,000 posts/second [[4](ch02.html#Krikorian2013)]. Lets also assume that the average user follows 200 people and has 200 followers (although there is a very wide range: most people have only a handful of followers, and a few celebrities such as Barack Obama have over 100 million followers).
### 用户、帖子和关注关系的表示
设想我们将所有数据保存在关系数据库中,如 [图 2-1](ch02.html#fig_twitter_relational) 所示。我们有一个用户表、一个帖子表和一个关注关系表。
Imagine we keep all of the data in a relational database as shown in [Figure 2-1](ch02.html#fig_twitter_relational). We have one table for users, one table for posts, and one table for follow relationships.
![ddia 0102](img/ddia_0102.png)
> 图 2-1. 社交网络的简单关系模式,其中用户可以相互关注。
假设我们的社交网络需要支持的主要读操作是*首页时间线*,它显示你所关注的人最近的帖子(为简单起见,我们将忽略广告、来自你未关注的人的建议帖子以及其他扩展)。我们可以编写以下 SQL 查询来获取特定用户的首页时间线:
> Figure 2-1. Simple relational schema for a social network in which users can follow each other.
Lets say the main read operation that our social network must support is the *home timeline*, which displays recent posts by people you are following (for simplicity we will ignore ads, suggested posts from people you are not following, and other extensions). We could write the following SQL query to get the home timeline for a particular user:
```sql
SELECT posts.*, users.* FROM posts
JOIN follows ON posts.sender_id = follows.followee_id
JOIN users ON posts.sender_id = users.id
WHERE follows.follower_id = current_user
ORDER BY posts.timestamp DESC
LIMIT 1000
```
为了执行这个查询,数据库将使用 `follows` 表来查找 `current_user` 正在关注的所有人,查找这些用户的最近帖子,并按时间戳排序以获得被关注用户的最新 1000 条帖子。
帖子应当是及时的,因此假设某人发帖后,我们希望他们的关注者在 5 秒内能看到。一种实现这一目标的方法是,当用户在线时,其客户端每 5 秒重复上述查询一次(这被称为*轮询*)。如果我们假设有 1000 万用户同时在线并登录,这意味着每秒需要运行 200 万次查询。即使你增加轮询间隔,这也是一个庞大的数字。
此外,上述查询相当昂贵:如果你关注了 200 人,它需要获取这 200 人的最近帖子列表,并合并这些列表。每秒 200 万次时间线查询意味着数据库需要每秒查找某些发送者的最近帖子 4 亿次——这是一个巨大的数字。而这只是平均情况。有些用户关注了成千上万的账户;对他们而言,这个查询非常昂贵,难以快速执行。
To execute this query, the database will use the `follows` table to find everybody who `current_user` is following, look up recent posts by those users, and sort them by timestamp to get the most recent 1,000 posts by any of the followed users.
Posts are supposed to be timely, so lets assume that after somebody makes a post, we want their followers to be able to see it within 5 seconds. One way of doing that would be for the users client to repeat the query above every 5 seconds while the user is online (this is known as *polling*). If we assume that 10 million users are online and logged in at the same time, that would mean running the query 2 million times per second. Even if you increase the polling interval, this is a lot.
Moreover, the query above is quite expensive: if you are following 200 people, it needs to fetch a list of recent posts by each of those 200 people, and merge those lists. 2 million timeline queries per second then means that the database needs to look up the recent posts from some sender 400 million times per second—a huge number. And that is the average case. Some users follow tens of thousands of accounts; for them, this query is very expensive to execute, and difficult to make fast.
### 物化与更新时间线
我们怎样才能做得更好?首先,与其使用轮询,不如让服务器主动将新帖推送给当前在线的任何关注者。其次,我们应该预计算上述查询的结果,以便用户请求他们的首页时间线时可以从缓存中获取。
想象一下,对于每个用户,我们存储一个包含他们首页时间线的数据结构,即他们所关注的人的最近帖子。每当用户发表帖子时,我们查找他们所有的关注者,并将该帖子插入到每个关注者的首页时间线中——就像将信息送达邮箱一样。现在,当用户登录时,我们可以简单地提供我们预计算的这个首页时间线。此外,为了接收其时间线上任何新帖子的通知,用户的客户端只需订阅被添加到他们首页时间线的帖子流。
这种方法的缺点是,每当用户发帖时,我们都需要做更多的工作,因为首页时间线是派生数据,需要更新。这一过程在 [图 2-2](ch02.html#fig_twitter_timelines) 中有所示。当一个初始请求导致执行多个下游请求时,我们使用*扩散*一词来描述请求数量的增加因素。
How can we do better? Firstly, instead of polling, it would be better if the server actively pushed new posts to any followers who are currently online. Secondly, we should precompute the results of the query above so that a users request for their home timeline can be served from a cache.
Imagine that for each user we store a data structure containing their home timeline, i.e., the recent posts by people they are following. Every time a user makes a post, we look up all of their followers, and insert that post into the home timeline of each follower—like delivering a message to a mailbox. Now when a user logs in, we can simply give them this home timeline that we precomputed. Moreover, to receive a notification about any new posts on their timeline, the users client simply needs to subscribe to the stream of posts being added to their home timeline.
The downside of this approach is that we now need to do more work every time a user makes a post, because the home timelines are derived data that needs to be updated. The process is illustrated in [Figure 2-2](ch02.html#fig_twitter_timelines). When one initial request results in several downstream requests being carried out, we use the term *fan-out* to describe the factor by which the number of requests increases.
![ddia 0103](img/ddia_0103.png)
> 图 2-2. 扇出: 将新推文传达给发帖用户的每个关注者
以每秒 5700 帖的速率,如果平均每个帖子达到 200 个关注者(即扩散因子为 200我们将需要每秒执行超过 100 万次首页时间线写入。这个数字虽然大,但与我们原本需要执行的每秒 4 亿次按发送者查找帖子相比,仍然是一个显著的节省。
如果由于某些特殊事件导致帖子发布率激增,我们不必立即执行时间线传递——我们可以将它们排队,并接受帖子在关注者时间线上显示出来可能会暂时延迟一些。即使在此类负载激增期间,时间线的加载仍然很快,因为我们只需从缓存中提供它们。
这种预计算和更新查询结果的过程被称为*实体化*,而时间线缓存则是一个*实体化视图*的例子(这是我们将进一步讨论的一个概念)。实体化的缺点是,每当一位名人发帖时,我们现在必须做大量的工作,将那篇帖子插入他们数百万关注者的首页时间线中。
解决这个问题的一种方法是将名人的帖子与其他人的帖子分开处理:我们可以通过将名人的帖子单独存储并在读取时与实体化时间线合并,从而避免将它们添加到数百万时间线上的努力。尽管有此类优化,处理社交网络上的名人可能需要大量的基础设施 [[5](ch02.html#Axon2010_ch2)]。
At a rate of 5,700 posts posted per second, if the average post reaches 200 followers (i.e., a fan-out factor of 200), we will need to do just over 1 million home timeline writes per second. This is a lot, but its still a significant saving compared to the 400 million per-sender post lookups per second that we would otherwise have to do.
If the rate of posts spikes due to some special event, we dont have to do the timeline deliveries immediately—we can enqueue them and accept that it will temporarily take a bit longer for posts to show up in followers timelines. Even during such load spikes, timelines remain fast to load, since we simply serve them from a cache.
This process of precomputing and updating the results of a query is called *materialization*, and the timeline cache is an example of a *materialized view* (a concept we will discuss further in [Link to Come]). The downside of materialization is that every time a celebrity makes a post, we now have to do a large amount of work to insert that post into the home timelines of each of their millions of followers.
One way of solving this problem is to handle celebrity posts separately from everyone elses posts: we can save ourselves the effort of adding them to millions of timelines by storing the celebrity posts separately and merging them with the materialized timeline when it is read. Despite such optimizations, handling celebrities on a social network can require a lot of infrastructure [[5](ch02.html#Axon2010_ch2)].
--------
## 描述性能
在软件性能的讨论中,通常考虑两种主要的度量指标:
- **响应时间**Response Time
从用户发出请求的那一刻到他们接收到请求的答案所经历的时间。测量单位是秒。
- **吞吐量**Throughput
系统每秒处理的请求数量或每秒处理的数据量。对于给定的硬件资源配置,存在一个*最大吞吐量*。测量单位是“每秒某事物数”。
在社交网络案例研究中,“每秒帖子数”和“每秒时间线写入数”是吞吐量指标,而“加载首页时间线所需的时间”或“帖子传递给关注者的时间”是响应时间指标。
吞吐量与响应时间之间通常存在联系;在线服务中这种关系的一个示例在 [图 2-3](ch02.html#fig_throughput) 中进行了描述。当请求吞吐量低时,服务具有低响应时间,但随着负载增加,响应时间会增长。这是因为*排队*:当请求到达一个负载较高的系统时,很可能 CPU 正在处理先前的请求,因此新来的请求需要等待直到先前的请求完成。当吞吐量接近硬件能够处理的最大值时,排队延迟会急剧增加。
Most discussions of software performance consider two main types of metric:
- Response Time
The elapsed time from the moment when a user makes a request until they receive the requested answer. The unit of measurement is seconds.
- Throughput
The number of requests per second, or the data volume per second, that the system is processing. For a given a particular allocation of hardware resources, there is a *maximum throughput* that can be handled. The unit of measurement is “somethings per second”.
In the social network case study, “posts per second” and “timeline writes per second” are throughput metrics, whereas the “time it takes to load the home timeline” or the “time until a post is delivered to followers” are response time metrics.
There is often a connection between throughput and response time; an example of such a relationship for an online service is sketched in [Figure 2-3](ch02.html#fig_throughput). The service has a low response time when request throughput is low, but response time increases as load increases. This is because of *queueing*: when a request arrives on a highly loaded system, its likely that the CPU is already in the process of handling an earlier request, and therefore the incoming request needs to wait until the earlier request has been completed. As throughput approaches the maximum that the hardware can handle, queueing delays increase sharply.
![ddia 0104b](img/ddia_0104b.png)
> 图2-3. 当服务吞吐量接近容量时,响应时间会由于排队而急剧增加
#### 当过载系统无法恢复时
如果系统接近过载,吞吐量接近极限,有时会进入一个恶性循环,使得系统变得效率更低,从而更加过载。例如,如果有大量请求在排队等待处理,响应时间可能会增加到客户端超时并重新发送请求的程度。这会导致请求率进一步增加,使问题更加严重——这就是所谓的*重试风暴*。即使负载再次减少,这样的系统也可能仍处于过载状态,直到重新启动或以其他方式重置。这种现象称为*亚稳定故障*,可能会导致生产系统中严重的中断[[6](ch02.html#Bronson2021), [7](ch02.html#Brooker2021)]。
为了避免重试过度加载服务,你可以增加并随机化客户端连续重试之间的时间(*指数退避*[[8](ch02.html#Brooker2015), [9](ch02.html#Brooker2022backoff)]),并暂时停止向最近返回错误或超时的服务发送请求(使用*断路器*[[10](ch02.html#Nygard2018)]或*令牌桶*算法[[11](ch02.html#Brooker2022retries)])。服务器也可以检测到它即将过载,并开始主动拒绝请求(*减载*[[12](ch02.html#YanacekLoadShedding)]),并发送回响应要求客户端减慢速度(*反压力*[[1](ch02.html#Cvet2016), [13](ch02.html#Sackman2016_ch2)])。队列和负载平衡算法的选择也可以有所不同[[14](ch02.html#Kopytkov2018)]。
在性能指标方面,响应时间通常是用户最关心的,而吞吐量决定了所需的计算资源(例如,你需要多少服务器),从而决定了服务特定工作负载的成本。如果吞吐量可能超过当前硬件能够处理的范围,就需要扩展容量;如果一个系统能够通过增加计算资源显著提高其最大吞吐量,则称该系统具有*可扩展性*。
在本节中,我们将主要关注响应时间,并将在[“可扩展性”](ch02.html#sec_introduction_scalability)一节中回归讨论吞吐量和可扩展性。
If a system is close to overload, with throughput pushed close to the limit, it can sometimes enter a vicious cycle where it becomes less efficient and hence even more overloaded. For example, if there is a long queue of requests waiting to be handled, response times may increase so much that clients time out and resend their request. This causes the rate of requests to increase even further, making the problem worse—a *retry storm*. Even when the load is reduced again, such a system may remain in an overloaded state until it is rebooted or otherwise reset. This phenomenon is called a *metastable failure*, and it can cause serious outages in production systems [[6](ch02.html#Bronson2021), [7](ch02.html#Brooker2021)].
To avoid retries overloading a service, you can increase and randomize the time between successive retries on the client side (*exponential backoff* [[8](ch02.html#Brooker2015), [9](ch02.html#Brooker2022backoff)]), and temporarily stop sending requests to a service that has returned errors or timed out recently (using a *circuit breaker* [[10](ch02.html#Nygard2018)] or *token bucket* algorithm [[11](ch02.html#Brooker2022retries)]). The server can also detect when it is approaching overload and start proactively rejecting requests (*load shedding* [[12](ch02.html#YanacekLoadShedding)]), and send back responses asking clients to slow down (*backpressure* [[1](ch02.html#Cvet2016), [13](ch02.html#Sackman2016_ch2)]). The choice of queueing and load-balancing algorithms can also make a difference [[14](ch02.html#Kopytkov2018)].
In terms of performance metrics, the response time is usually what users care about the most, whereas the throughput determines the required computing resources (e.g., how many servers you need), and hence the cost of serving a particular workload. If throughput is likely to increase beyond what the current hardware can handle, the capacity needs to be expanded; a system is said to be *scalable* if its maximum throughput can be significantly increased by adding computing resources.
In this section we will focus primarily on response times, and we will return to throughput and scalability in [“Scalability”](ch02.html#sec_introduction_scalability).
### 延迟与响应时间
“Latency”和“response time”有时被交替使用但在本书中我们将以特定的方式使用这些术语如[图2-4](ch02.html#fig_response_time)所示):
- *响应时间*是客户端所看到的;它包括系统中任何地方产生的所有延迟。
- *服务时间*是服务实际处理用户请求的持续时间。
- *排队延迟*可以在流程的几个点出现:例如,接收到请求后
- *延迟* 是一个包罗万象的术语,用于描述请求未被积极处理的时间,即处于 *潜伏状态* 的时间。特别是,*网络延迟* 或 *网络延迟* 指的是请求和响应在网络中传输的时间。
“Latency” and “response time” are sometimes used interchangeably, but in this book we will use the terms in a specific way (illustrated in [Figure 2-4](ch02.html#fig_response_time)):
- The *response time* is what the client sees; it includes all delays incurred anywhere in the system.
- The *service time* is the duration for which the service is actively processing the user request.
- *Queueing delays* can occur at several points in the flow: for example, after a request is received, it might need to wait until a CPU is available before it can be processed; a response packet might need to be buffered before it is sent over the network if other tasks on the same machine are sending a lot of data via the outbound network interface.
- *Latency* is a catch-all term for time during which a request is not being actively processed, i.e., during which it is *latent*. In particular, *network latency* or *network delay* refers to the time that request and response spend traveling through the network.
![ddia 0104a](img/ddia_0104a.png)
> 图2-4. 响应时间、服务时间、网络延迟和排队延迟
即使反复发出同一请求,响应时间也可能因请求而异,差异显著。许多因素可能会导致随机延迟:例如,切换到后台进程的上下文切换,网络数据包丢失和 TCP 重传,垃圾收集暂停,页面错误强制从磁盘读取,服务器架的机械振动[[15](ch02.html#Gunawi2018)],或许多其他原因。我们将在 [未来链接] 中更详细地讨论这个话题。
排队延迟通常是响应时间变化性的一个重要部分。由于服务器同时只能处理少量事务(例如,受其 CPU 核心数量的限制),只需少数几个慢请求就足以阻塞后续请求的处理——这种效应被称为 *队首阻塞*。即使那些后续请求的服务时间很快,客户端也会因为等待先前请求完成而感觉到整体响应时间的缓慢。排队延迟不属于服务时间的一部分,因此在客户端测量响应时间十分重要。
The response time can vary significantly from one request to the next, even if you keep making the same request over and over again. Many factors can add random delays: for example, a context switch to a background process, the loss of a network packet and TCP retransmission, a garbage collection pause, a page fault forcing a read from disk, mechanical vibrations in the server rack [[15](ch02.html#Gunawi2018)], or many other causes. We will discuss this topic in more detail in [Link to Come].
Queueing delays often account for a large part of the variability in response times. As a server can only process a small number of things in parallel (limited, for example, by its number of CPU cores), it only takes a small number of slow requests to hold up the processing of subsequent requests—an effect known as *head-of-line blocking*. Even if those subsequent requests have fast service times, the client will see a slow overall response time due to the time waiting for the prior request to complete. The queueing delay is not part of the service time, and for this reason it is important to measure response times on the client side.
### 平均数,中位数与百分位点
因为响应时间从一个请求到另一个请求都在变化,我们需要把它视为一个你可以测量的值的 *分布*,而不是一个单一的数字。在 [图 2-5](ch02.html#fig_lognormal),每个灰色条代表对一个服务的请求,其高度显示了该请求所需的时间。大多数请求相当快,但偶尔也有 *异常值* 花费的时间要长得多。网络延迟的变化也被称为 *抖动*
Because the response time varies from one request to the next, we need to think of it not as a single number, but as a *distribution* of values that you can measure. In [Figure 2-5](ch02.html#fig_lognormal), each gray bar represents a request to a service, and its height shows how long that request took. Most requests are reasonably fast, but there are occasional *outliers* that take much longer. Variation in network delay is also known as *jitter*.
![ddia 0104](img/ddia_0104.png)
> 图 2-5. 描述平均值和百分位数对某服务100次请求的响应时间。
>
> Figure 2-5. Illustrating mean and percentiles: response times for a sample of 100 requests to a service.
通常我们会报告服务的*平均*响应时间(技术上说是*算术平均值*:即总和所有的响应时间,然后除以请求的数量)。然而,如果你想了解你的“典型”响应时间,平均值并不是一个很好的度量,因为它不能告诉你有多少用户实际经历了那种延迟。
通常使用*百分位数*会更好。如果你将响应时间列表从最快到最慢排序,那么*中位数*是中间点例如如果你的中位响应时间是200毫秒这意味着你一半的请求在200毫秒内返回另一半请求需要超过这个时间。这使得中位数成为一个好的度量如果你想知道用户通常需要等待多久。中位数也被称为*第50百分位*,有时缩写为*p50*。
为了弄清楚你的异常值有多严重,你可以查看更高的百分位数:*第95、第99和第99.9百分位*是常见的(缩写为*p95、p99和p999*。它们是响应时间的阈值即95%、99%或99.9%的请求比该特定阈值快。例如如果第95百分位的响应时间是1.5秒这意味着100次请求中有95次不到1.5秒有5次需要1.5秒或更多时间。这在[图 2-5](ch02.html#fig_lognormal)中有所示。
响应时间的高百分位数,也称为*尾部延迟*很重要因为它们直接影响用户对服务的体验。例如亚马逊描述其内部服务的响应时间要求是以第99.9百分位来衡量尽管它只影响1/1000的请求。这是因为请求最慢的客户往往是那些在他们的账户上有最多数据的客户因为他们进行了许多购买——即他们是最有价值的客户[[16](ch02.html#DeCandia2007_ch1)]。保证网站对他们来说快速是很重要的,以保持这些客户的满意。
另一方面优化第99.99百分位最慢的1/10,000的请求被认为过于昂贵且对亚马逊的目的来说收益不足。在非常高的百分位数上减少响应时间是困难的因为它们容易受到你无法控制的随机事件的影响而且收益递减。
Its common to report the *average* response time of a service (technically, the *arithmetic mean*: that is, sum all the response times, and divide by the number of requests). However, the mean is not a very good metric if you want to know your “typical” response time, because it doesnt tell you how many users actually experienced that delay.
Usually it is better to use *percentiles*. If you take your list of response times and sort it from fastest to slowest, then the *median* is the halfway point: for example, if your median response time is 200 ms, that means half your requests return in less than 200 ms, and half your requests take longer than that. This makes the median a good metric if you want to know how long users typically have to wait. The median is also known as the *50th percentile*, and sometimes abbreviated as *p50*.
In order to figure out how bad your outliers are, you can look at higher percentiles: the *95th*, *99th*, and *99.9th* percentiles are common (abbreviated *p95*, *p99*, and *p999*). They are the response time thresholds at which 95%, 99%, or 99.9% of requests are faster than that particular threshold. For example, if the 95th percentile response time is 1.5 seconds, that means 95 out of 100 requests take less than 1.5 seconds, and 5 out of 100 requests take 1.5 seconds or more. This is illustrated in [Figure 2-5](ch02.html#fig_lognormal).
High percentiles of response times, also known as *tail latencies*, are important because they directly affect users experience of the service. For example, Amazon describes response time requirements for internal services in terms of the 99.9th percentile, even though it only affects 1 in 1,000 requests. This is because the customers with the slowest requests are often those who have the most data on their accounts because they have made many purchases—that is, theyre the most valuable customers [[16](ch02.html#DeCandia2007_ch1)]. Its important to keep those customers happy by ensuring the website is fast for them.
On the other hand, optimizing the 99.99th percentile (the slowest 1 in 10,000 requests) was deemed too expensive and to not yield enough benefit for Amazons purposes. Reducing response times at very high percentiles is difficult because they are easily affected by random events outside of your control, and the benefits are diminishing.
### 响应时间对用户的影响
直觉上看,快速服务比慢服务更有利于用户似乎是显而易见的[[17](ch02.html#Whitenton2020)]。然而,要获取可靠数据来量化延迟对用户行为的影响却出奇地困难。
一些经常被引用的统计数据是不可靠的。2006年谷歌报告称搜索结果从400毫秒减慢到900毫秒导致流量和收入下降20%[[18](ch02.html#Linden2006)]。然而谷歌在2009年的另一项研究报告称延迟增加400毫秒仅导致每天的搜索量减少0.6%[[19](ch02.html#Brutlag2009)]同年必应发现加载时间增加两秒钟广告收入减少了4.3%[[20](ch02.html#Schurman2009)]。这些公司的更新数据似乎没有公开。
Akamai的一项较新研究[[21](ch02.html#Akamai2017)]声称响应时间增加100毫秒会使电子商务网站的转化率降低多达7%;然而,仔细检查同一研究发现,非常*快*的页面加载时间也与较低的转化率相关这种看似矛盾的结果是由于最快加载的页面往往是那些没有有用内容的页面例如404错误页面。然而由于该研究没有努力区分页面内容和加载时间的影响其结果可能没有意义。
雅虎的一项研究[[22](ch02.html#Bai2017)]比较了快速加载与慢速加载搜索结果的点击率控制搜索结果的质量。研究发现当快速和慢速响应之间的差异在1.25秒或更多时快速搜索的点击率增加了20-30%。
It seems intuitively obvious that a fast service is better for users than a slow service [[17](ch02.html#Whitenton2020)]. However, it is surprisingly difficult to get hold of reliable data to quantify the effect that latency has on user behavior.
Some often-cited statistics are unreliable. In 2006 Google reported that a slowdown in search results from 400 ms to 900 ms was associated with a 20% drop in traffic and revenue [[18](ch02.html#Linden2006)]. However, another Google study from 2009 reported that a 400 ms increase in latency resulted in only 0.6% fewer searches per day [[19](ch02.html#Brutlag2009)], and in the same year Bing found that a two-second increase in load time reduced ad revenue by 4.3% [[20](ch02.html#Schurman2009)]. Newer data from these companies appears not to be publicly available.
A more recent Akamai study [[21](ch02.html#Akamai2017)] claims that a 100 ms increase in response time reduced the conversion rate of e-commerce sites by up to 7%; however, on closer inspection, the same study reveals that very *fast* page load times are also correlated with lower conversion rates! This seemingly paradoxical result is explained by the fact that the pages that load fastest are often those that have no useful content (e.g., 404 error pages). However, since the study makes no effort to separate the effects of page content from the effects of load time, its results are probably not meaningful.
A study by Yahoo [[22](ch02.html#Bai2017)] compares click-through rates on fast-loading versus slow-loading search results, controlling for quality of search results. It finds 2030% more clicks on fast searches when the difference between fast and slow responses is 1.25 seconds or more.
#### 使用响应时间指标
高百分位数在后端服务中尤其重要,这些服务在处理单个最终用户请求时会被多次调用。即使你并行进行调用,最终用户请求仍然需要等待并行调用中最慢的一个完成。正如[图 2-6](ch02.html#fig_tail_amplification)所示,只需一个慢调用就能使整个最终用户请求变慢。即使只有少数后端调用较慢,如果最终用户请求需要多次后端调用,获得慢调用的几率就会增加,因此更高比例的最终用户请求最终变慢(这种效应被称为*尾延迟放大*[[23](ch02.html#Dean2013)])。
High percentiles are especially important in backend services that are called multiple times as part of serving a single end-user request. Even if you make the calls in parallel, the end-user request still needs to wait for the slowest of the parallel calls to complete. It takes just one slow call to make the entire end-user request slow, as illustrated in [Figure 2-6](ch02.html#fig_tail_amplification). Even if only a small percentage of backend calls are slow, the chance of getting a slow call increases if an end-user request requires multiple backend calls, and so a higher proportion of end-user requests end up being slow (an effect known as *tail latency amplification* [[23](ch02.html#Dean2013)]).
![ddia 0105](img/ddia_0105.png)
> 图 2-6. 当一个请求需要多次后端调用时,只需要一个缓慢的后端请求,就能拖慢整个终端用户的请求
百分位数通常用于*服务级别目标*SLOs和*服务级别协议*SLAs作为定义服务预期性能和可用性的方式[[24](ch02.html#Hidalgo2020)]。例如SLO可能设定一个目标要求服务的中位响应时间少于200毫秒第99百分位在1秒以下并且至少99.9%的有效请求结果为非错误响应。SLA是一份合同规定如果未达到SLO将发生什么例如客户可能有权获得退款。至少基本思想是这样的实际上为SLOs和SLAs定义良好的可用性指标并不简单[[25](ch02.html#Mogul2019), 26]。
Percentiles are often used in *service level objectives* (SLOs) and *service level agreements* (SLAs) as ways of defining the expected performance and availability of a service [[24](ch02.html#Hidalgo2020)]. For example, an SLO may set a target for a service to have a median response time of less than 200 ms and a 99th percentile under 1 s, and a target that at least 99.9% of valid requests result in non-error responses. An SLA is a contract that specifies what happens if the SLO is not met (for example, customers may be entitled to a refund). That is the basic idea, at least; in practice, defining good availability metrics for SLOs and SLAs is not straightforward [[25](ch02.html#Mogul2019), [26](ch02.html#Hauer2020)].
#### 计算百分位点
如果你想在服务的监控仪表板上添加响应时间百分位数你需要持续有效地计算它们。例如你可能希望保持一个最近10分钟内请求响应时间的滚动窗口。每分钟你都会计算该窗口中的中位数和各种百分位数并将这些指标绘制在图表上。
最简单的实现方式是保留时间窗口内所有请求的响应时间列表并每分钟对该列表进行排序。如果这对你来说效率太低有些算法可以以最小的CPU和内存成本计算出百分位数的良好近似值。开源的百分位数估计库包括 HdrHistogram、t-digest [[27](ch02.html#Dunning2021), [28](ch02.html#Kohn2021)]、OpenHistogram [[29](ch02.html#Hartmann2020)] 和 DDSketch [[30](ch02.html#Masson2019)]。
注意,对百分位数进行平均化,例如为了降低时间分辨率或将来自几台机器的数据结合在一起,从数学上讲是没有意义的——聚合响应时间数据的正确方法是添加直方图[[31](ch02.html#Schwartz2015)]。
If you want to add response time percentiles to the monitoring dashboards for your services, you need to efficiently calculate them on an ongoing basis. For example, you may want to keep a rolling window of response times of requests in the last 10 minutes. Every minute, you calculate the median and various percentiles over the values in that window and plot those metrics on a graph.
The simplest implementation is to keep a list of response times for all requests within the time window and to sort that list every minute. If that is too inefficient for you, there are algorithms that can calculate a good approximation of percentiles at minimal CPU and memory cost. Open source percentile estimation libraries include HdrHistogram, t-digest [[27](ch02.html#Dunning2021), [28](ch02.html#Kohn2021)], OpenHistogram [[29](ch02.html#Hartmann2020)], and DDSketch [[30](ch02.html#Masson2019)].
Beware that averaging percentiles, e.g., to reduce the time resolution or to combine data from several machines, is mathematically meaningless—the right way of aggregating response time data is to add the histograms [[31](ch02.html#Schwartz2015)].
--------
## 可靠性与容错
每个人对于一个东西可靠不可靠都有自己的直观想法。对于软件来说,典型的期望包括:
* 应用程序表现出用户所期望的功能。
* 软件允许用户犯错,或以意料之外的方式来使用软件。
* 在预期的负载和数据量下,性能可以满足要求。
* 系统能够阻止未经授权的访问和滥用。
如果把所有这些要求放一块儿意味着 “正确工作”,那么我们可以把 *可靠性* 粗略理解为:“即使出现问题,也能继续正常工作”。为了更准确地描述问题的发生,我们将区分*故障*和*失败*[[32](ch02.html#Heimerdinger1992), [33](ch02.html#Gaertner1999)]
- **故障**fault
故障是指系统的某个部分停止正常工作:例如,单个硬盘故障,或者单台机器崩溃,或者系统依赖的外部服务出现中断。
A fault is when a particular *part* of a system stops working correctly: for example, if a single hard drive malfunctions, or a single machine crashes, or an external service (that the system depends on) has an outage.
- **失效**Failure
失效是指系统整体停止向用户提供所需服务换句话说就是未达到服务级别目标SLO
A failure is when the system *as a whole* stops providing the required service to the user; in other words, when it does not meet the service level objective (SLO).
故障与失败之间的区别可能会引起混淆,因为它们是同一件事,只是在不同的层级上。例如,如果一个硬盘停止工作,我们说硬盘发生了失败:如果系统只由那一个硬盘组成,它就停止提供所需的服务。然而,如果你所说的系统包含多个硬盘,那么单个硬盘的失败只是从更大系统的角度看是一个故障,并且更大的系统可能能够通过在另一个硬盘上有数据的副本来容忍这个故障。
The distinction between fault and failure can be confusing because they are the same thing, just at different levels. For example, if a hard drive stops working, we say that the hard drive has failed: if the system consists only of that one hard drive, it has stopped providing the required service. However, if the system youre talking about contains many hard drives, then the failure of a single hard drive is only a fault from the point of view of the bigger system, and the bigger system might be able to tolerate that fault by having a copy of the data on another hard drive.
### 容错
如果系统在某些故障发生时仍继续向用户提供所需服务,我们称该系统为*容错*系统。如果系统不能容忍某部分出现故障,我们称该部分为*单点故障*SPOF因为该部分的故障会升级为导致整个系统的失败。
例如,在社交网络案例研究中,可能发生的故障是在广播过程中,参与更新物化时间线的机器崩溃或变得不可用。为了使这个过程具有容错性,我们需要确保另一台机器能够接管这个任务,不遗漏任何本应传送的帖子,也不重复任何帖子。(这个概念被称为*精确一次语义*,我们将在[未来链接]中详细讨论)
We call a system *fault-tolerant* if it continues providing the required service to the user in spite of certain faults occurring. If a system cannot tolerate a certain part becoming faulty, we call that part a *single point of failure* (SPOF), because a fault in that part escalates to cause the failure of the whole system.
For example, in the social network case study, a fault that might happen is that during the fan-out process, a machine involved in updating the materialized timelines crashes or become unavailable. To make this process fault-tolerant, we would need to ensure that another machine can take over this task without missing any posts that should have been delivered, and without duplicating any posts. (This idea is known as *exactly-once semantics*, and we will examine it in detail in [Link to Come].)
容错性始终仅限于一定数量的特定类型的故障。例如,一个系统可能能够同时容忍最多两个硬盘故障,或者三个节点中最多有一个崩溃。容忍任意数量的故障是没有意义的:如果所有节点都崩溃了,那就无计可施。如果整个地球(及其上的所有服务器)被黑洞吞噬,那么要容忍这种故障就需要在太空中进行网络托管——祝你好运,让这个预算项目获批。
违反直觉的是,在这样的容错系统中,通过故意触发故障来*增加*故障率是有意义的——例如,随机无预警地终止个别进程。许多关键性的错误实际上是由于错误处理不当引起的[[34](ch02.html#Yuan2014)];通过故意诱发故障,你确保了容错机制不断地得到运用和测试,这可以增强你的信心,相信在自然发生故障时能够得到正确处理。*混沌工程*是一门旨在通过诸如故意注入故障的实验来提高对容错机制信心的学科[[35](ch02.html#Rosenthal2020)]。
虽然我们通常倾向于容忍故障而非预防故障,但在某些情况下,预防比治疗更好(例如,因为没有治疗方法)。在安全问题上就是这样,例如:如果攻击者已经侵入系统并获取了敏感数据,那个事件是无法撤销的。然而,本书主要讨论的是可以治愈的故障类型,如下文所述。
Fault tolerance is always limited to a certain number of certain types of faults. For example, a system might be able to tolerate a maximum of two hard drives failing at the same time, or a maximum of one out of three nodes crashing. It would not make sense to tolerate any number of faults: if all nodes crash, there is nothing that can be done. If the entire planet Earth (and all servers on it) were swallowed by a black hole, tolerance of that fault would require web hosting in space—good luck getting that budget item approved.
Counter-intuitively, in such fault-tolerant systems, it can make sense to *increase* the rate of faults by triggering them deliberately—for example, by randomly killing individual processes without warning. Many critical bugs are actually due to poor error handling [[34](ch02.html#Yuan2014)]; by deliberately inducing faults, you ensure that the fault-tolerance machinery is continually exercised and tested, which can increase your confidence that faults will be handled correctly when they occur naturally. *Chaos engineering* is a discipline that aims to improve confidence in fault-tolerance mechanisms through experiments such as deliberately injecting faults [[35](ch02.html#Rosenthal2020)].
Although we generally prefer tolerating faults over preventing faults, there are cases where prevention is better than cure (e.g., because no cure exists). This is the case with security matters, for example: if an attacker has compromised a system and gained access to sensitive data, that event cannot be undone. However, this book mostly deals with the kinds of faults that can be cured, as described in the following sections.
### 硬件与软件缺陷
当我们思考系统故障的原因时,硬件故障很快浮现脑海:
- 每年大约有 2-5% 的磁盘硬盘出现故障[[36](ch02.html#Pinheiro2007), [37](ch02.html#Schroeder2007)];在一个拥有 10,000 块硬盘的存储集群中,我们因此可以预计平均每天会有一块硬盘故障。最近的数据表明硬盘越来越可靠,但故障率仍然显著[[38](ch02.html#Klein2021)]。
- 每年大约有 0.5-1% 的固态硬盘SSD故障[[39](ch02.html#Narayanan2016)]。少量的位错误可以自动纠正[[40](ch02.html#Alibaba2019_ch2)],但不可纠正的错误大约每年每块硬盘发生一次,即使是相当新的硬盘(即,磨损较少的硬盘);这种错误率高于磁盘硬盘[[41](ch02.html#Schroeder2016), [42](ch02.html#Alter2019)]。
- 其他硬件组件如电源供应器、RAID 控制器和内存模块也会发生故障,尽管频率低于硬盘[[43](ch02.html#Ford2010), [44](ch02.html#Vishwanath2010)]。
- 大约每 1,000 台机器中就有一台的 CPU 核心偶尔计算出错误的结果,这很可能是由制造缺陷引起的[[45](ch02.html#Hochschild2021), [46](ch02.html#Dixit2021), [47](ch02.html#Behrens2015)]。在某些情况下,错误的计算会导致崩溃,但在其他情况下,它会导致程序简单地返回错误的结果。
- RAM 中的数据也可能被破坏原因可能是宇宙射线等随机事件或是永久性物理缺陷。即使使用了具有纠错码ECC的内存超过 1% 的机器在给定年份遇到不可纠正的错误,这通常会导致机器和受影响的内存模块崩溃并需要更换[[48](ch02.html#Schroeder2009)]。此外,某些病态的内存访问模式可以高概率地翻转位[[49](ch02.html#Kim2014)]。
- 整个数据中心可能变得不可用(例如,由于停电或网络配置错误)或甚至被永久性破坏(例如火灾或洪水)。尽管这种大规模故障很少见,但如果一项服务不能容忍数据中心的丢失,其影响可能是灾难性的[[50](ch02.html#Cockcroft2019)]。
这些事件足够罕见,以至于在处理小型系统时你通常不需要担心它们,只要你可以轻松替换变得有故障的硬件。然而,在大规模系统中,硬件故障发生得足够频繁,以至于它们成为正常系统运作的一部分。
When we think of causes of system failure, hardware faults quickly come to mind:
- Approximately 25% of magnetic hard drives fail per year [[36](ch02.html#Pinheiro2007), [37](ch02.html#Schroeder2007)]; in a storage cluster with 10,000 disks, we should therefore expect on average one disk failure per day. Recent data suggests that disks are getting more reliable, but failure rates remain significant [[38](ch02.html#Klein2021)].
- Approximately 0.51% of solid state drives (SSDs) fail per year [[39](ch02.html#Narayanan2016)]. Small numbers of bit errors are corrected automatically [[40](ch02.html#Alibaba2019_ch2)], but uncorrectable errors occur approximately once per year per drive, even in drives that are fairly new (i.e., that have experienced little wear); this error rate is higher than that of magnetic hard drives [[41](ch02.html#Schroeder2016), [42](ch02.html#Alter2019)].
- Other hardware components such as power supplies, RAID controllers, and memory modules also fail, although less frequently than hard drives [[43](ch02.html#Ford2010), [44](ch02.html#Vishwanath2010)].
- Approximately one in 1,000 machines has a CPU core that occasionally computes the wrong result, likely due to manufacturing defects [[45](ch02.html#Hochschild2021), [46](ch02.html#Dixit2021), [47](ch02.html#Behrens2015)]. In some cases, an erroneous computation leads to a crash, but in other cases it leads to a program simply returning the wrong result.
- Data in RAM can also be corrupted, either due to random events such as cosmic rays, or due to permanent physical defects. Even when memory with error-correcting codes (ECC) is used, more than 1% of machines encounter an uncorrectable error in a given year, which typically leads to a crash of the machine and the affected memory module needing to be replaced [[48](ch02.html#Schroeder2009)]. Moreover, certain pathological memory access patterns can flip bits with high probability [[49](ch02.html#Kim2014)].
- An entire datacenter might become unavailable (for example, due to power outage or network misconfiguration) or even be permanently destroyed (for example by fire or flood). Although such large-scale failures are rare, their impact can be catastrophic if a service cannot tolerate the loss of a datacenter [[50](ch02.html#Cockcroft2019)].
These events are rare enough that you often dont need to worry about them when working on a small system, as long as you can easily replace hardware that becomes faulty. However, in a large-scale system, hardware faults happen often enough that they become part of the normal system operation.
#### 通过冗余容忍硬件缺陷
Our first response to unreliable hardware is usually to add redundancy to the individual hardware components in order to reduce the failure rate of the system. Disks may be set up in a RAID configuration (spreading data across multiple disks in the same machine so that a failed disk does not cause data loss), servers may have dual power supplies and hot-swappable CPUs, and datacenters may have batteries and diesel generators for backup power. Such redundancy can often keep a machine running uninterrupted for years.
Redundancy is most effective when component faults are independent, that is, the occurrence of one fault does not change how likely it is that another fault will occur. However, experience has shown that there are often significant correlations between component failures [[37](ch02.html#Schroeder2007), [51](ch02.html#Han2021), [52](ch02.html#Nightingale2011)]; unavailability of an entire server rack or an entire datacenter still happens more often than we would like.
Hardware redundancy increases the uptime of a single machine; however, as discussed in [“Distributed versus Single-Node Systems”](ch01.html#sec_introduction_distributed), there are advantages to using a distributed system, such as being able to tolerate a complete outage of one datacenter. For this reason, cloud systems tend to focus less on the reliability of individual machines, and instead aim to make services highly available by tolerating faulty nodes at the software level. Cloud providers use *availability zones* to identify which resources are physically co-located; resources in the same place are more likely to fail at the same time than geographically separated resources.
The fault-tolerance techniques we discuss in this book are designed to tolerate the loss of entire machines, racks, or availability zones. They generally work by allowing a machine in one datacenter to take over when a machine in another datacenter fails or becomes unreachable. We will discuss such techniques for fault tolerance in [Link to Come], [Link to Come], and at various other points in this book.
Systems that can tolerate the loss of entire machines also have operational advantages: a single-server system requires planned downtime if you need to reboot the machine (to apply operating system security patches, for example), whereas a multi-node fault-tolerant system can be patched by restarting one node at a time, without affecting the service for users. This is called a *rolling upgrade*, and we will discuss it further in [Link to Come].
#### 软件缺陷
我们通常认为硬件故障是随机的、相互独立的:一台机器的磁盘失效并不意味着另一台机器的磁盘也会失效。虽然大量硬件组件之间可能存在微弱的相关性(例如服务器机架的温度等共同的原因),但同时发生故障也是极为罕见的。
另一类错误是内部的 **系统性错误systematic error**【8】。这类错误难以预料而且因为是跨节点相关的所以比起不相关的硬件故障往往可能造成更多的 **系统失效**【5】。例子包括
* 接受特定的错误输入,便导致所有应用服务器实例崩溃的 BUG。例如 2012 年 6 月 30 日的闰秒,由于 Linux 内核中的一个错误【9】许多应用同时挂掉了。
* 失控进程会用尽一些共享资源,包括 CPU 时间、内存、磁盘空间或网络带宽。
* 系统依赖的服务变慢,没有响应,或者开始返回错误的响应。
* 级联故障一个组件中的小故障触发另一个组件中的故障进而触发更多的故障【10】。
导致这类软件故障的 BUG 通常会潜伏很长时间,直到被异常情况触发为止。这种情况意味着软件对其环境做出了某种假设 —— 虽然这种假设通常来说是正确的但由于某种原因最后不再成立了【11】。
虽然软件中的系统性故障没有速效药,但我们还是有很多小办法,例如:仔细考虑系统中的假设和交互;彻底的测试;进程隔离;允许进程崩溃并重启;测量、监控并分析生产环境中的系统行为。如果系统能够提供一些保证(例如在一个消息队列中,进入与发出的消息数量相等),那么系统就可以在运行时不断自检,并在出现 **差异discrepancy** 时报警【12】。
Although hardware failures can be weakly correlated, they are still mostly independent: for example, if one disk fails, its likely that other disks in the same machine will be fine for another while. On the other hand, software faults are often very highly correlated, because it is common for many nodes to run the same software and thus have the same bugs [[53](ch02.html#Gunawi2014), [54](ch02.html#Kreps2012_ch1)]. Such faults are harder to anticipate, and they tend to cause many more system failures than uncorrelated hardware faults [[43](ch02.html#Ford2010)]. For example:
- A software bug that causes every node to fail at the same time in particular circumstances. For example, on June 30, 2012, a leap second caused many Java applications to hang simultaneously due to a bug in the Linux kernel, bringing down many Internet services [[55](ch02.html#Minar2012_ch1)]. Due to a firmware bug, all SSDs of certain models suddenly fail after precisely 32,768 hours of operation (less than 4 years), rendering the data on them unrecoverable [[56](ch02.html#HPE2019)].
- A runaway process that uses up some shared, limited resource, such as CPU time, memory, disk space, network bandwidth, or threads [[57](ch02.html#Hochstein2020)]. For example, a process that consumes too much memory while processing a large request may be killed by the operating system.
- A service that the system depends on slows down, becomes unresponsive, or starts returning corrupted responses.
- An interaction between different systems results in emergent behavior that does not occur when each system was tested in isolation [[58](ch02.html#Tang2023)].
- Cascading failures, where a problem in one component causes another component to become overloaded and slow down, which in turn brings down another component [[59](ch02.html#Ulrich2016), [60](ch02.html#Fassbender2022)].
The bugs that cause these kinds of software faults often lie dormant for a long time until they are triggered by an unusual set of circumstances. In those circumstances, it is revealed that the software is making some kind of assumption about its environment—and while that assumption is usually true, it eventually stops being true for some reason [[61](ch02.html#Cook2000), [62](ch02.html#Woods2017)].
There is no quick solution to the problem of systematic faults in software. Lots of small things can help: carefully thinking about assumptions and interactions in the system; thorough testing; process isolation; allowing processes to crash and restart; avoiding feedback loops such as retry storms (see [“When an overloaded system wont recover”](ch02.html#sidebar_metastable)); measuring, monitoring, and analyzing system behavior in production.
### 人类与可靠性
设计并构建了软件系统的工程师是人类,维持系统运行的运维也是人类。即使他们怀有最大的善意,人类也是不可靠的。举个例子,一项关于大型互联网服务的研究发现,运维配置错误是导致服务中断的首要原因,而硬件故障(服务器或网络)仅导致了 10-25% 的服务中断【13】。
尽管人类不可靠,但怎么做才能让系统变得可靠?最好的系统会组合使用以下几种办法:
* 以最小化犯错机会的方式设计系统。例如精心设计的抽象、API 和管理后台使做对事情更容易,搞砸事情更困难。但如果接口限制太多,人们就会忽略它们的好处而想办法绕开。很难正确把握这种微妙的平衡。
* 将人们最容易犯错的地方与可能导致失效的地方 **解耦decouple**。特别是提供一个功能齐全的非生产环境 **沙箱sandbox**,使人们可以在不影响真实用户的情况下,使用真实数据安全地探索和实验。
* 在各个层次进行彻底的测试【3】从单元测试、全系统集成测试到手动测试。自动化测试易于理解已经被广泛使用特别适合用来覆盖正常情况中少见的 **边缘场景corner case**
* 允许从人为错误中简单快速地恢复,以最大限度地减少失效情况带来的影响。例如,快速回滚配置变更,分批发布新代码(以便任何意外错误只影响一小部分用户),并提供数据重算工具(以备旧的计算出错)。
* 配置详细和明确的监控,比如性能指标和错误率。在其他工程学科中这指的是 **遥测telemetry**(一旦火箭离开了地面,遥测技术对于跟踪发生的事情和理解失败是至关重要的)。监控可以向我们发出预警信号,并允许我们检查是否有任何地方违反了假设和约束。当出现问题时,指标数据对于问题诊断是非常宝贵的。
* 良好的管理实践与充分的培训 —— 一个复杂而重要的方面,但超出了本书的范围。
Humans design and build software systems, and the operators who keep the systems running are also human. Unlike machines, humans dont just follow rules; their strength is being creative and adaptive in getting their job done. However, this characteristic also leads to unpredictability, and sometimes mistakes that can lead to failures, despite best intentions. For example, one study of large internet services found that configuration changes by operators were the leading cause of outages, whereas hardware faults (servers or network) played a role in only 1025% of outages [[63](ch02.html#Oppenheimer2003)].
It is tempting to label such problems as “human error” and to wish that they could be solved by better controlling human behavior through tighter procedures and compliance with rules. However, blaming people for mistakes is counterproductive. What we call “human error” is not really the cause of an incident, but rather a symptom of a problem with the sociotechnical system in which people are trying their best to do their jobs [[64](ch02.html#Dekker2017)].
Various technical measures can help minimize the impact of human mistakes, including thorough testing [[34](ch02.html#Yuan2014)], rollback mechanisms for quickly reverting configuration changes, gradual roll-outs of new code, detailed and clear monitoring, observability tools for diagnosing production issues (see [“Problems with Distributed Systems”](ch01.html#sec_introduction_dist_sys_problems)), and well-designed interfaces that encourage “the right thing” and discourage “the wrong thing”.
However, these things require an investment of time and money, and in the pragmatic reality of everyday business, organizations often prioritize revenue-generating activities over measures that increase their resilience against mistakes. If there is a choice between more features and more testing, many organizations understandably choose features. Given this choice, when a preventable mistake inevitably occurs, it does not make sense to blame the person who made the mistake—the problem is the organizations priorities.
Increasingly, organizations are adopting a culture of *blameless postmortems*: after an incident, the people involved are encouraged to share full details about what happened, without fear of punishment, since this allows others in the organization to learn how to prevent similar problems in the future [[65](ch02.html#Allspaw2012)]. This process may uncover a need to change business priorities, a need to invest in areas that have been neglected, a need to change the incentives for the people involved, or some other systemic issue that needs to be brought to the managements attention.
As a general principle, when investigating an incident, you should be suspicious of simplistic answers. “Bob should have been more careful when deploying that change” is not productive, but neither is “We must rewrite the backend in Haskell.” Instead, management should take the opportunity to learn the details of how the sociotechnical system works from the point of view of the people who work with it every day, and take steps to improve it based on this feedback [[64](ch02.html#Dekker2017)].
### 可靠性到底有多重要?
可靠性不仅仅是针对核电站和空中交通管制软件而言,我们也期望更多平凡的应用能可靠地运行。商务应用中的错误会导致生产力损失(也许数据报告不完整还会有法律风险),而电商网站的中断则可能会导致收入和声誉的巨大损失。
即使在 “非关键” 应用中我们也对用户负有责任。试想一位家长把所有的照片和孩子的视频储存在你的照片应用里【15】。如果数据库突然损坏他们会感觉如何他们可能会知道如何从备份恢复吗
在某些情况下,我们可能会选择牺牲可靠性来降低开发成本(例如为未经证实的市场开发产品原型)或运营成本(例如利润率极低的服务),但我们偷工减料时,应该清楚意识到自己在做什么。
Reliability is not just for nuclear power stations and air traffic control—more mundane applications are also expected to work reliably. Bugs in business applications cause lost productivity (and legal risks if figures are reported incorrectly), and outages of e-commerce sites can have huge costs in terms of lost revenue and damage to reputation.
In many applications, a temporary outage of a few minutes or even a few hours is tolerable [[66](ch02.html#Sabo2023)], but permanent data loss or corruption would be catastrophic. Consider a parent who stores all their pictures and videos of their children in your photo application [[67](ch02.html#Jurewitz2013)]. How would they feel if that database was suddenly corrupted? Would they know how to restore it from a backup?
As another example of how unreliable software can harm people, consider the Post Office Horizon scandal. Between 1999 and 2019, hundreds of people managing Post Office branches in Britain were convicted of theft or fraud because the accounting software showed a shortfall in their accounts. Eventually it became clear that many of these shortfalls were due to bugs in the software, and many convictions have since been overturned [[68](ch02.html#Siddique2021)]. What led to this, probably the largest miscarriage of justice in British history, is the fact that English law assumes that computers operate correctly (and hence, evidence produced by computers is reliable) unless there is evidence to the contrary [[69](ch02.html#Bohm2022)]. Software engineers may laugh at the idea that software could ever be bug-free, but this is little solace to the people who were wrongfully imprisoned, declared bankrupt, or even committed suicide as a result of a wrongful conviction due to an unreliable computer system.
There are situations in which we may choose to sacrifice reliability in order to reduce development cost (e.g., when developing a prototype product for an unproven market)—but we should be very conscious of when we are cutting corners and keep in mind the potential consequences.
--------
## 可伸缩性
即使系统今天运行可靠也不意味着将来一定能保持可靠。退化的一个常见原因是负载增加可能系统从1万并发用户增长到了10万并发用户或从100万增加到了1000万。也许它正在处理比以前更大的数据量。
可扩展性是我们用来描述系统应对增加负载能力的术语。有时,在讨论可扩展性时,人们会这样评论:“你不是谷歌或亚马逊。不用担心规模,只用关系型数据库就好。”这个格言是否适用于你,取决于你正在构建的应用类型。
如果你正在为一个刚起步的公司构建一个新产品,目前只有少数用户,通常最重要的工程目标是保持系统尽可能简单和灵活,以便你可以根据对客户需求的了解轻松修改和适应产品功能[70]。在这种环境下,担心未来可能需要的假设性规模是适得其反的:在最好的情况下,投资于可扩展性是浪费努力和过早的优化;在最坏的情况下,它们会让你陷入僵化的设计,使得应用难以进化。
原因是可扩展性不是一维标签说“X可扩展”或“Y不可扩展”是没有意义的。相反讨论可扩展性意味着考虑诸如此类的问题
“如果系统以特定方式增长,我们有哪些应对增长的选项?”
“我们如何增加计算资源来处理额外的负载?”
“基于当前的增长预测,我们何时会达到当前架构的极限?”
如果你成功地让你的应用受欢迎,因此处理了越来越多的负载,你将了解你的性能瓶颈在哪里,因此你将知道你需要沿哪些维度进行扩展。到了那个时候,就是开始担心扩展技术的时候了。
Even if a system is working reliably today, that doesnt mean it will necessarily work reliably in the future. One common reason for degradation is increased load: perhaps the system has grown from 10,000 concurrent users to 100,000 concurrent users, or from 1 million to 10 million. Perhaps it is processing much larger volumes of data than it did before.
*Scalability* is the term we use to describe a systems ability to cope with increased load. Sometimes, when discussing scalability, people make comments along the lines of, “Youre not Google or Amazon. Stop worrying about scale and just use a relational database.” Whether this maxim applies to you depends on the type of application you are building.
If you are building a new product that currently only has a small number of users, perhaps at a startup, the overriding engineering goal is usually to keep the system as simple and flexible as possible, so that you can easily modify and adapt the features of your product as you learn more about customers needs [[70](ch02.html#McKinley2015)]. In such an environment, it is counterproductive to worry about hypothetical scale that might be needed in the future: in the best case, investments in scalability are wasted effort and premature optimization; in the worst case, they lock you into an inflexible design and make it harder to evolve your application.
The reason is that scalability is not a one-dimensional label: it is meaningless to say “X is scalable” or “Y doesnt scale.” Rather, discussing scalability means considering questions like:
- “If the system grows in a particular way, what are our options for coping with the growth?”
- “How can we add computing resources to handle the additional load?”
- “Based on current growth projections, when will we hit the limits of our current architecture?”
If you succeed in making your application popular, and therefore handling a growing amount of load, you will learn where your performance bottlenecks lie, and therefore you will know along which dimensions you need to scale. At that point its time to start worrying about techniques for scalability.
### 描述负载
首先,我们需要简洁地描述系统当前的负载;只有这样,我们才能讨论增长问题(如果我们的负载翻倍会发生什么?)。这通常是通过吞吐量来衡量的:例如,每秒向服务的请求数量、每天新增多少吉字节的数据,或者每小时有多少购物车结账。有时你关心某些变量的峰值,比如同时在线用户的数量,如[“案例研究:社交网络首页时间线”](ch02.html#sec_introduction_twitter)中所述。
负载的其他统计特性也可能影响访问模式,从而影响可扩展性需求。例如,你可能需要知道数据库中读写的比例、缓存的命中率,或每个用户的数据项数量(例如,社交网络案例研究中的关注者数量)。也许平均情况是你关心的,或许你的瓶颈由少数极端情况主导。这一切都取决于你特定应用的细节。
一旦你描述了系统的负载,你就可以探究当负载增加时会发生什么。你可以从两个方面考虑这个问题:
- 当你以某种方式增加负载并保持系统资源CPU、内存、网络带宽等不变时你的系统性能会受到什么影响
- 当你以某种方式增加负载时,如果你想保持性能不变,你需要增加多少资源?
通常我们的目标是在最小化运行系统的成本的同时保持系统性能符合SLA的要求见[“响应时间指标的使用”](ch02.html#sec_introduction_slo_sla))。所需的计算资源越多,成本就越高。可能某些类型的硬件比其他类型更具成本效益,随着新型硬件的出现,这些因素可能会随时间而变化。
如果你可以通过加倍资源来处理双倍的负载,同时保持性能不变,我们就说你实现了*线性可扩展性*,这被认为是一件好事。偶尔也可能通过不到双倍的资源来处理双倍的负载,这得益于规模经济或更好的高峰负载分配[[71](ch02.html#Warfield2023)[72](ch02.html#Brooker2023)]。更常见的情况是,成本增长超过线性,可能有许多原因导致这种低效。例如,如果你有大量数据,那么处理单个写请求可能涉及的工作量比你的数据量小的时候要多,即使请求的大小相同。
First, we need to succinctly describe the current load on the system; only then can we discuss growth questions (what happens if our load doubles?). Often this will be a measure of throughput: for example, the number of requests per second to a service, how many gigabytes of new data arrive per day, or the number of shopping cart checkouts per hour. Sometimes you care about the peak of some variable quantity, such as the number of simultaneously online users in [“Case Study: Social Network Home Timelines”](ch02.html#sec_introduction_twitter).
Often there are other statistical characteristics of the load that also affect the access patterns and hence the scalability requirements. For example, you may need to know the ratio of reads to writes in a database, the hit rate on a cache, or the number of data items per user (for example, the number of followers in the social network case study). Perhaps the average case is what matters for you, or perhaps your bottleneck is dominated by a small number of extreme cases. It all depends on the details of your particular application.
Once you have described the load on your system, you can investigate what happens when the load increases. You can look at it in two ways:
- When you increase the load in a certain way and keep the system resources (CPUs, memory, network bandwidth, etc.) unchanged, how is the performance of your system affected?
- When you increase the load in a certain way, how much do you need to increase the resources if you want to keep performance unchanged?
Usually our goal is to keep the performance of the system within the requirements of the SLA (see [“Use of Response Time Metrics”](ch02.html#sec_introduction_slo_sla)) while also minimizing the cost of running the system. The greater the required computing resources, the higher the cost. It might be that some types of hardware are more cost-effective than others, and these factors may change over time as new types of hardware become available.
If you can double the resources in order to handle twice the load, while keeping performance the same, we say that you have *linear scalability*, and this is considered a good thing. Occasionally it is possible to handle twice the load with less than double the resources, due to economies of scale or a better distribution of peak load [[71](ch02.html#Warfield2023), [72](ch02.html#Brooker2023)]. Much more likely is that the cost grows faster than linearly, and there may be many reasons for the inefficiency. For example, if you have a lot of data, then processing a single write request may involve more work than if you have a small amount of data, even if the size of the request is the same.
### 共享内存,共享磁盘,无共享架构
增加服务的硬件资源最简单的方式是将其迁移到更强大的机器上。单个CPU核心的速度不再显著提升但您可以购买或租用云实例一个拥有更多CPU核心、更多RAM和更多磁盘空间的机器。这种方法被称为*垂直扩展*或*向上扩展*。
在单台机器上您可以通过使用多个进程或线程来实现并行性。属于同一进程的所有线程可以访问同一RAM因此这种方法也被称为*共享内存架构*。共享内存方法的问题在于成本增长超过线性:拥有双倍硬件资源的高端机器通常的成本显著高于两倍。而且由于瓶颈,一台规模加倍的机器往往处理的负载不到两倍。
另一种方法是*共享磁盘架构*它使用多台拥有独立CPU和RAM的机器但将数据存储在一个磁盘阵列上这些磁盘阵列在机器之间通过快速网络共享*网络附加存储*NAS或*存储区域网络*SAN。这种架构传统上用于本地数据仓库工作负载但争用和锁定开销限制了共享磁盘方法的可扩展性[[73](ch02.html#Stopford2009)]。
相比之下,*无共享架构* [[74](ch02.html#Stonebraker1986)](也称为*水平扩展*或*向外扩展*获得了很大的流行。在这种方法中我们使用一个具有多个节点的分布式系统每个节点都拥有自己的CPU、RAM和磁盘。节点之间的任何协调都在软件层面通过常规网络完成。
无共享的优势在于它有潜力线性扩展,它可以使用提供最佳价格/性能比的任何硬件(特别是在云中),它可以随着负载的增减更容易地调整其硬件资源,并且通过在多个数据中心和地区分布系统,它可以实现更大的容错性。缺点是它需要显式的数据分区(见[链接即将到来]),并且带来了分布式系统的所有复杂性(见[链接即将到来])。
一些云原生数据库系统使用独立的服务来执行存储和事务处理(见[“存储与计算的分离”](ch01.html#sec_introduction_storage_compute)多个计算节点共享访问同一个存储服务。这种模型与共享磁盘架构有些相似但它避免了旧系统的可扩展性问题存储服务不提供文件系统NAS或块设备SAN抽象而是提供了专门为数据库需求设计的专用API[[75](ch02.html#Antonopoulos2019_ch2)]。
The simplest way of increasing the hardware resources of a service is to move it to a more powerful machine. Individual CPU cores are no longer getting significantly faster, but you can buy a machine (or rent a cloud instance) with more CPU cores, more RAM, and more disk space. This approach is called *vertical scaling* or *scaling up*.
You can get parallelism on a single machine by using multiple processes or threads. All the threads belonging to the same process can access the same RAM, and hence this approach is also called a *shared-memory architecture*. The problem with a shared-memory approach is that the cost grows faster than linearly: a high-end machine with twice the hardware resources typically costs significantly more than twice as much. And due to bottlenecks, a machine twice the size can often handle less than twice the load.
Another approach is the *shared-disk architecture*, which uses several machines with independent CPUs and RAM, but which stores data on an array of disks that is shared between the machines, which are connected via a fast network: *Network-Attached Storage* (NAS) or *Storage Area Network* (SAN). This architecture has traditionally been used for on-premises data warehousing workloads, but contention and the overhead of locking limit the scalability of the shared-disk approach [[73](ch02.html#Stopford2009)].
By contrast, the *shared-nothing architecture* [[74](ch02.html#Stonebraker1986)] (also called *horizontal scaling* or *scaling out*) has gained a lot of popularity. In this approach, we use a distributed system with multiple nodes, each of which has its own CPUs, RAM, and disks. Any coordination between nodes is done at the software level, via a conventional network.
The advantages of shared-nothing are that it has the potential to scale linearly, it can use whatever hardware offers the best price/performance ratio (especially in the cloud), it can more easily adjust its hardware resources as load increases or decreases, and it can achieve greater fault tolerance by distributing the system across multiple data centers and regions. The downsides are that it requires explicit data partitioning (see [Link to Come]), and it incurs all the complexity of distributed systems ([Link to Come]).
Some cloud-native database systems use separate services for storage and transaction execution (see [“Separation of storage and compute”](ch01.html#sec_introduction_storage_compute)), with multiple compute nodes sharing access to the same storage service. This model has some similarity to a shared-disk architecture, but it avoids the scalability problems of older systems: instead of providing a filesystem (NAS) or block device (SAN) abstraction, the storage service offers a specialized API that is designed for the specific needs of the database [[75](ch02.html#Antonopoulos2019_ch2)].
### 可伸缩性原则
在大规模运行的系统架构通常高度特定于应用——没有所谓的通用、一刀切的可扩展架构(非正式称为*魔法扩展酱*。例如一个设计为每秒处理100,000个请求每个请求1 kB大小的系统与一个设计为每分钟处理3个请求每个请求2 GB大小的系统看起来完全不同——尽管这两个系统有相同的数据吞吐量100 MB/秒)。
此外适用于某一负载水平的架构不太可能应对10倍的负载。因此如果您正在处理一个快速增长的服务很可能您需要在每个数量级负载增加时重新思考您的架构。由于应用的需求可能会发展变化通常不值得提前超过一个数量级来规划未来的扩展需求。
一个关于可扩展性的好的一般原则是将系统分解成可以相对独立运行的小组件。这是微服务背后的基本原则(见[“微服务与无服务器”](ch01.html#sec_introduction_microservices))、分区([链接即将到来])、流处理([链接即将到来])和无共享架构。然而,挑战在于知道在应该在一起的事物和应该分开的事物之间划线的位置。关于微服务的设计指南可以在其他书籍中找到[[76](ch02.html#Newman2021_ch2)],我们将在[链接即将到来]中讨论无共享系统的分区。
另一个好的原则是不要让事情变得比必要的更复杂。如果单机数据库可以完成工作,它可能比复杂的分布式设置更可取。自动扩展系统(根据需求自动增加或减少资源)很酷,但如果您的负载相当可预测,手动扩展的系统可能会有更少的运营惊喜(见[链接即将到来])。一个拥有五个服务的系统比拥有五十个服务的系统简单。好的架构通常涉及到方法的实用混合。
The architecture of systems that operate at large scale is usually highly specific to the application—there is no such thing as a generic, one-size-fits-all scalable architecture (informally known as *magic scaling sauce*). For example, a system that is designed to handle 100,000 requests per second, each 1 kB in size, looks very different from a system that is designed for 3 requests per minute, each 2 GB in size—even though the two systems have the same data throughput (100 MB/sec).
Moreover, an architecture that is appropriate for one level of load is unlikely to cope with 10 times that load. If you are working on a fast-growing service, it is therefore likely that you will need to rethink your architecture on every order of magnitude load increase. As the needs of the application are likely to evolve, it is usually not worth planning future scaling needs more than one order of magnitude in advance.
A good general principle for scalability is to break a system down into smaller components that can operate largely independently from each other. This is the underlying principle behind microservices (see [“Microservices and Serverless”](ch01.html#sec_introduction_microservices)), partitioning ([Link to Come]), stream processing ([Link to Come]), and shared-nothing architectures. However, the challenge is in knowing where to draw the line between things that should be together, and things that should be apart. Design guidelines for microservices can be found in other books [[76](ch02.html#Newman2021_ch2)], and we discuss partitioning of shared-nothing systems in [Link to Come].
Another good principle is not to make things more complicated than necessary. If a single-machine database will do the job, its probably preferable to a complicated distributed setup. Auto-scaling systems (which automatically add or remove resources in response to demand) are cool, but if your load is fairly predictable, a manually scaled system may have fewer operational surprises (see [Link to Come]). A system with five services is simpler than one with fifty. Good architectures usually involve a pragmatic mixture of approaches.
--------
## 可维护性
软件不会磨损或遭受材料疲劳,因此它的损坏方式与机械物体不同。但应用程序的需求经常变化,软件运行的环境也在变化(如其依赖关系和底层平台),并且它有需要修复的错误。
广泛认为,软件的大部分成本不在于初始开发,而在于持续的维护——修复错误、保持系统运行、调查故障、适应新平台、针对新用例修改软件、偿还技术债务以及添加新功能[7778]。
然而维护也很困难。如果一个系统已经成功运行很长时间它可能会使用一些今天很少有工程师理解的过时技术如大型机和COBOL代码随着人员离职关于系统如何以及为什么以某种方式设计的机构知识可能已经丢失可能需要修复其他人的错误。此外计算机系统往往与它支持的人类组织交织在一起这意味着维护这种遗留系统既是一个人的问题也是一个技术问题[79]。
如果一个系统足够有价值,能长时间存活,我们今天创建的每个系统终将成为遗留系统。为了最小化未来维护我们软件的后代所承受的痛苦,我们应当在设计时考虑维护问题。虽然我们无法总是预测哪些决策将在未来造成维护难题,但在本书中,我们将关注几个广泛适用的原则:
Software does not wear out or suffer material fatigue, so it does not break in the same ways as mechanical objects do. But the requirements for an application frequently change, the environment that the software runs in changes (such as its dependencies and the underlying platform), and it has bugs that need fixing.
It is widely recognized that the majority of the cost of software is not in its initial development, but in its ongoing maintenance—fixing bugs, keeping its systems operational, investigating failures, adapting it to new platforms, modifying it for new use cases, repaying technical debt, and adding new features [[77](ch02.html#Ensmenger2016), [78](ch02.html#Glass2002)].
However, maintenance is also difficult. If a system has been successfully running for a long time, it may well use outdated technologies that not many engineers understand today (such as mainframes and COBOL code); institutional knowledge of how and why a system was designed in a certain way may have been lost as people have left the organization; it might be necessary to fix other peoples mistakes. Moreover, the computer system is often intertwined with the human organization that it supports, which means that maintenance of such *legacy* systems is as much a people problem as a technical one [[79](ch02.html#Bellotti2021)].
Every system we create today will one day become a legacy system if it is valuable enough to survive for a long time. In order to minimize the pain for future generations who need to maintain our software, we should design it with maintenance concerns in mind. Although we cannot always predict which decisions might create maintenance headaches in the future, in this book we will pay attention to several principles that are widely applicable:
* 可操作性Operability
便于运维团队保持系统平稳运行。
* 简单性Simplicity
让新工程师也能轻松理解系统 —— 通过使用众所周知、协调一致的模式和结构来实现系统,并避免不必要的**复杂性Complexity**。
* 可演化性Evolvability
使工程师能够轻松地对系统进行改造,并在未来出现需求变化时,能使其适应和扩展到新的应用场景中。
### 可操作性:人生苦短,关爱运维
我们先前在[云时代的运营](/v2/ch1#在云时代的运营)中讨论过运维的角色,不难发现在这个过程中人类扮演的角色至少也是与工具一样重要的。 实际上有人认为,“良好的运维经常可以绕开垃圾(或不完整)软件的局限性,而再好的软件摊上垃圾运维也没法可靠运行”。尽管运维的某些方面可以,而且应该是自动化的,但在最初建立正确运作的自动化机制仍然取决于人。
运维团队对于保持软件系统顺利运行至关重要。一个优秀运维团队的典型职责如下或者更多【29】
* 监控系统的运行状况,并在服务状态不佳时快速恢复服务。
* 跟踪问题的原因,例如系统故障或性能下降。
* 及时更新软件和平台,比如安全补丁。
* 了解系统间的相互作用,以便在异常变更造成损失前进行规避。
* 预测未来的问题,并在问题出现之前加以解决(例如,容量规划)。
* 建立部署、配置、管理方面的良好实践,编写相应工具。
* 执行复杂的维护任务,例如将应用程序从一个平台迁移到另一个平台。
* 当配置变更时,维持系统的安全性。
* 定义工作流程,使运维操作可预测,并保持生产环境稳定。
* 铁打的营盘流水的兵,维持组织对系统的了解。
良好的可操作性意味着更轻松的日常工作,进而运维团队能专注于高价值的事情。数据系统可以通过各种方式使日常任务更轻松:
* 通过良好的监控,提供对系统内部状态和运行时行为的 **可见性visibility**
* 为自动化提供良好支持,将系统与标准化工具相集成。
* 避免依赖单台机器(在整个系统继续不间断运行的情况下允许机器停机维护)。
* 提供良好的文档和易于理解的操作模型(“如果做 X会发生 Y”
* 提供良好的默认行为,但需要时也允许管理员自由覆盖默认值。
* 有条件时进行自我修复,但需要时也允许管理员手动控制系统状态。
* 行为可预测,最大限度减少意外。
We previously discussed the role of operations in [“Operations in the Cloud Era”](ch01.html#sec_introduction_operations), and we saw that human processes are at least as important for reliable operations as software tools. In fact, it has been suggested that “good operations can often work around the limitations of bad (or incomplete) software, but good software cannot run reliably with bad operations” [[54](ch02.html#Kreps2012_ch1)].
In large-scale systems consisting of many thousands of machines, manual maintenance would be unreasonably expensive, and automation is essential. However, automation can be a two-edged sword: there will always be edge cases (such as rare failure scenarios) that require manual intervention from the operations team. Since the cases that cannot be handled automatically are the most complex issues, greater automation requires a *more* skilled operations team that can resolve those issues [[80](ch02.html#Bainbridge1983)].
Moreover, if an automated system goes wrong, it is often harder to troubleshoot than a system that relies on an operator to perform some actions manually. For that reason, it is not the case that more automation is always better for operability. However, some amount of automation is important, and the sweet spot will depend on the specifics of your particular application and organization.
Good operability means making routine tasks easy, allowing the operations team to focus their efforts on high-value activities. Data systems can do various things to make routine tasks easy, including [[81](ch02.html#Hamilton2007)]:
- Allowing monitoring tools to check the systems key metrics, and supporting observability tools (see [“Problems with Distributed Systems”](ch01.html#sec_introduction_dist_sys_problems)) to give insights into the systems runtime behavior. A variety of commercial and open source tools can help here [[82](ch02.html#Horovits2021)].
- Avoiding dependency on individual machines (allowing machines to be taken down for maintenance while the system as a whole continues running uninterrupted)
- Providing good documentation and an easy-to-understand operational model (“If I do X, Y will happen”)
- Providing good default behavior, but also giving administrators the freedom to override defaults when needed
- Self-healing where appropriate, but also giving administrators manual control over the system state when needed
- Exhibiting predictable behavior, minimizing surprises
### 简单性:管理复杂度
小型软件项目可以使用简单讨喜的、富表现力的代码,但随着项目越来越大,代码往往变得非常复杂,难以理解。这种复杂度拖慢了所有系统相关人员,进一步增加了维护成本。一个陷入复杂泥潭的软件项目有时被描述为 **烂泥潭a big ball of mud** 【30】。
**复杂度complexity** 有各种可能的症状,例如:状态空间激增、模块间紧密耦合、纠结的依赖关系、不一致的命名和术语、解决性能问题的 Hack、需要绕开的特例等等现在已经有很多关于这个话题的讨论【31,32,33】。
因为复杂度导致维护困难时,预算和时间安排通常会超支。在复杂的软件中进行变更,引入错误的风险也更大:当开发人员难以理解系统时,隐藏的假设、无意的后果和意外的交互就更容易被忽略。相反,降低复杂度能极大地提高软件的可维护性,因此简单性应该是构建系统的一个关键目标。
简化系统并不一定意味着减少功能;它也可以意味着消除 **额外的accidental** 的复杂度。Moseley 和 Marks【32】把 **额外复杂度** 定义为:由具体实现中涌现,而非(从用户视角看,系统所解决的)问题本身固有的复杂度。
用于消除 **额外复杂度** 的最好工具之一是 **抽象abstraction**。一个好的抽象可以将大量实现细节隐藏在一个干净,简单易懂的外观下面。一个好的抽象也可以广泛用于各类不同应用。比起重复造很多轮子,重用抽象不仅更有效率,而且有助于开发高质量的软件。抽象组件的质量改进将使所有使用它的应用受益。
例如高级编程语言是一种抽象隐藏了机器码、CPU 寄存器和系统调用。SQL 也是一种抽象,隐藏了复杂的磁盘 / 内存数据结构、来自其他客户端的并发请求、崩溃后的不一致性。当然在用高级语言编程时,我们仍然用到了机器码;只不过没有 **直接directly** 使用罢了,正是因为编程语言的抽象,我们才不必去考虑这些实现细节。
抽象可以帮助我们将系统的复杂度控制在可管理的水平,不过,找到好的抽象是非常困难的。在分布式系统领域虽然有许多好的算法,但我们并不清楚它们应该打包成什么样抽象。
本书将紧盯那些允许我们将大型系统的部分提取为定义明确的、可重用的组件的优秀抽象。
Small software projects can have delightfully simple and expressive code, but as projects get larger, they often become very complex and difficult to understand. This complexity slows down everyone who needs to work on the system, further increasing the cost of maintenance. A software project mired in complexity is sometimes described as a *big ball of mud* [[83](ch02.html#Foote1997)].
When complexity makes maintenance hard, budgets and schedules are often overrun. In complex software, there is also a greater risk of introducing bugs when making a change: when the system is harder for developers to understand and reason about, hidden assumptions, unintended consequences, and unexpected interactions are more easily overlooked [[62](ch02.html#Woods2017)]. Conversely, reducing complexity greatly improves the maintainability of software, and thus simplicity should be a key goal for the systems we build.
Simple systems are easier to understand, and therefore we should try to solve a given problem in the simplest way possible. Unfortunately, this is easier said than done. Whether something is simple or not is often a subjective matter of taste, as there is no objective standard of simplicity [[84](ch02.html#Brooker2022)]. For example, one system may hide a complex implementation behind a simple interface, whereas another may have a simple implementation that exposes more internal detail to its users—which one is simpler?
One attempt at reasoning about complexity has been to break it down into two categories, *essential* and *accidental* complexity [[85](ch02.html#Brooks1995)]. The idea is that essential complexity is inherent in the problem domain of the application, while accidental complexity arises only because of limitations of our tooling. Unfortunately, this distinction is also flawed, because boundaries between the essential and the accidental shift as our tooling evolves [[86](ch02.html#Luu2020)].
One of the best tools we have for managing complexity is *abstraction*. A good abstraction can hide a great deal of implementation detail behind a clean, simple-to-understand façade. A good abstraction can also be used for a wide range of different applications. Not only is this reuse more efficient than reimplementing a similar thing multiple times, but it also leads to higher-quality software, as quality improvements in the abstracted component benefit all applications that use it.
For example, high-level programming languages are abstractions that hide machine code, CPU registers, and syscalls. SQL is an abstraction that hides complex on-disk and in-memory data structures, concurrent requests from other clients, and inconsistencies after crashes. Of course, when programming in a high-level language, we are still using machine code; we are just not using it *directly*, because the programming language abstraction saves us from having to think about it.
Abstractions for application code, which aim to reduce its complexity, can be created using methodologies such as *design patterns* [[87](ch02.html#Gamma1994)] and *domain-driven design* (DDD) [[88](ch02.html#Evans2003)]. This book is not about such application-specific abstractions, but rather about general-purpose abstractions on top of which you can build your applications, such as database transactions, indexes, and event logs. If you want to use techniques such as DDD, you can implement them on top of the foundations described in this book.
### 可演化性:让变更更容易
系统的需求永远不变,基本是不可能的。更可能的情况是,它们处于常态的变化中,例如:你了解了新的事实、出现意想不到的应用场景、业务优先级发生变化、用户要求新功能、新平台取代旧平台、法律或监管要求发生变化、系统增长迫使架构变化等。
在组织流程方面,**敏捷agile** 工作模式为适应变化提供了一个框架。敏捷社区还开发了对在频繁变化的环境中开发软件很有帮助的技术工具和模式,如 **测试驱动开发TDD, test-driven development****重构refactoring**
这些敏捷技术的大部分讨论都集中在相当小的规模(同一个应用中的几个代码文件)。本书将探索在更大数据系统层面上提高敏捷性的方法,可能由几个不同的应用或服务组成。例如,为了将装配主页时间线的方法从方法 1 变为方法 2你会如何 “重构” 推特的架构
修改数据系统并使其适应不断变化需求的容易程度,是与 **简单性****抽象性** 密切相关的:简单易懂的系统通常比复杂系统更容易修改。但由于这是一个非常重要的概念,我们将用一个不同的词来指代数据系统层面的敏捷性: **可演化性evolvability** 【34】。
Its extremely unlikely that your systems requirements will remain unchanged forever. They are much more likely to be in constant flux: you learn new facts, previously unanticipated use cases emerge, business priorities change, users request new features, new platforms replace old platforms, legal or regulatory requirements change, growth of the system forces architectural changes, etc.
In terms of organizational processes, *Agile* working patterns provide a framework for adapting to change. The Agile community has also developed technical tools and processes that are helpful when developing software in a frequently changing environment, such as test-driven development (TDD) and refactoring. In this book, we search for ways of increasing agility at the level of a system consisting of several different applications or services with different characteristics.
The ease with which you can modify a data system, and adapt it to changing requirements, is closely linked to its simplicity and its abstractions: simple and easy-to-understand systems are usually easier to modify than complex ones. Since this is such an important idea, we will use a different word to refer to agility on a data system level: *evolvability* [[89](ch02.html#Breivold2008)].
One major factor that makes change difficult in large systems is when some action is irreversible, and therefore that action needs to be taken very carefully [[90](ch02.html#Zaninotto2002)]. For example, say you are migrating from one database to another: if you cannot switch back to the old system in case of problems wth the new one, the stakes are much higher than if you can easily go back. Minimizing irreversibility improves flexibility.
--------
## 本章小结
在本章中,我们检查了几个非功能性需求的示例:性能、可靠性、可扩展性和可维护性。通过这些话题,我们还遇到了我们在本书其余部分将需要的原则和术语。我们从一个案例研究开始,探讨了如何在社交网络中实现首页时间线,这展示了在规模扩大时可能出现的一些挑战。
我们讨论了如何测量性能例如使用响应时间百分位数、系统负载例如使用吞吐量指标以及它们如何在SLA中使用。可扩展性是一个密切相关的概念即确保在负载增长时性能保持不变。我们看到了一些可扩展性的一般原则如将任务分解成可以独立操作的小部分并将在后续章节中深入技术细节探讨可扩展性技术。
为了实现可靠性,您可以使用容错技术,即使系统的某个组件(例如,磁盘、机器或其他服务)出现故障,也能继续提供服务。我们看到了可能发生的硬件故障示例,并将其与软件故障区分开来,后者可能更难处理,因为它们往往具有强相关性。实现可靠性的另一个方面是构建对人为错误的抵抗力,我们看到了无责任事故报告作为从事件中学习的一种技术。
最后,我们检查了几个维护性的方面,包括支持运营团队的工作、管理复杂性,以及使应用功能随时间易于演进。关于如何实现这些目标没有简单的答案,但有一件事可以帮助,那就是使用提供有用抽象的、众所周知的构建块来构建应用程序。本书的其余部分将介绍一些最重要的这类构建块。
In this chapter we examined several examples of nonfunctional requirements: performance, reliability, scalability, and maintainability. Through these topics we have also encountered principles and terminology that we will need throughout the rest of the book. We started with a case study of how one might implement home timelines in a social network, which illustrated some of the challenges that arise at scale.
We discussed how to measure performance (e.g., using response time percentiles), the load on a system (e.g., using throughput metrics), and how they are used in SLAs. Scalability is a closely related concept: that is, ensuring performance stays the same when the load grows. We saw some general principles for scalability, such as breaking a task down into smaller parts that can operate independently, and we will dive into deep technical detail on scalability techniques in the following chapters.
To achieve reliability, you can use fault tolerance techniques, which allow a system to continue providing its service even if some component (e.g., a disk, a machine, or another service) is faulty. We saw examples of hardware faults that can occur, and distinguished them from software faults, which can be harder to deal with because they are often strongly correlated. Another aspect of achieving reliability is to build resilience against humans making mistakes, and we saw blameless postmortems as a technique for learning from incidents.
Finally, we examined several facets of maintainability, including supporting the work of operations teams, managing complexity, and making it easy to evolve an applications functionality over time. There are no easy answers on how to achieve these things, but one thing that can help is to build applications using well-understood building blocks that provide useful abstractions. The rest of this book will cover a selection of the most important such building blocks.
--------
## 参考文献
[[1](ch02.html#Cvet2016-marker)] Mike Cvet. [How We Learned to Stop Worrying and Love Fan-In at Twitter](https://www.youtube.com/watch?v=WEgCjwyXvwc). At *QCon San Francisco*, December 2016.
[[2](ch02.html#Krikorian2012_ch2-marker)] Raffi Krikorian. [Timelines at Scale](http://www.infoq.com/presentations/Twitter-Timeline-Scalability). At *QCon San Francisco*, November 2012. Archived at [perma.cc/V9G5-KLYK](https://perma.cc/V9G5-KLYK)
[[3](ch02.html#Twitter2023-marker)] Twitter. [Twitters Recommendation Algorithm](https://blog.twitter.com/engineering/en_us/topics/open-source/2023/twitter-recommendation-algorithm). *blog.twitter.com*, March 2023. Archived at [perma.cc/L5GT-229T](https://perma.cc/L5GT-229T)
[[4](ch02.html#Krikorian2013-marker)] Raffi Krikorian. [New Tweets per second record, and how!](https://blog.twitter.com/engineering/en_us/a/2013/new-tweets-per-second-record-and-how) *blog.twitter.com*, August 2013. Archived at [perma.cc/6JZN-XJYN](https://perma.cc/6JZN-XJYN)
[[5](ch02.html#Axon2010_ch2-marker)] Samuel Axon. [3% of Twitters Servers Dedicated to Justin Bieber](http://mashable.com/2010/09/07/justin-bieber-twitter/). *mashable.com*, September 2010. Archived at [perma.cc/F35N-CGVX](https://perma.cc/F35N-CGVX)
[[6](ch02.html#Bronson2021-marker)] Nathan Bronson, Abutalib Aghayev, Aleksey Charapko, and Timothy Zhu. [Metastable Failures in Distributed Systems](https://sigops.org/s/conferences/hotos/2021/papers/hotos21-s11-bronson.pdf). At *Workshop on Hot Topics in Operating Systems* (HotOS), May 2021. [doi:10.1145/3458336.3465286](https://doi.org/10.1145/3458336.3465286)
[[7](ch02.html#Brooker2021-marker)] Marc Brooker. [Metastability and Distributed Systems](https://brooker.co.za/blog/2021/05/24/metastable.html). *brooker.co.za*, May 2021. Archived at [archive.org](https://web.archive.org/web/20230324043015/https://brooker.co.za/blog/2021/05/24/metastable.html)
[[8](ch02.html#Brooker2015-marker)] Marc Brooker. [Exponential Backoff And Jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/). *aws.amazon.com*, March 2015. Archived at [perma.cc/R6MS-AZKH](https://perma.cc/R6MS-AZKH)
[[9](ch02.html#Brooker2022backoff-marker)] Marc Brooker. [What is Backoff For?](https://brooker.co.za/blog/2022/08/11/backoff.html) *brooker.co.za*, August 2022. Archived at [archive.org](https://web.archive.org/web/20230331022111/https://brooker.co.za/blog/2022/08/11/backoff.html)
[[10](ch02.html#Nygard2018-marker)] Michael T. Nygard. [*Release It!*](https://learning.oreilly.com/library/view/release-it-2nd/9781680504552/), 2nd Edition. Pragmatic Bookshelf, January 2018. ISBN: 9781680502398
[[11](ch02.html#Brooker2022retries-marker)] Marc Brooker. [Fixing retries with token buckets and circuit breakers](https://brooker.co.za/blog/2022/02/28/retries.html). *brooker.co.za*, February 2022. Archived at [archive.org](https://web.archive.org/web/20230325195445/https://brooker.co.za/blog/2022/02/28/retries.html)
[[12](ch02.html#YanacekLoadShedding-marker)] David Yanacek. [Using load shedding to avoid overload](https://aws.amazon.com/builders-library/using-load-shedding-to-avoid-overload/). Amazon Builders Library, *aws.amazon.com*. Archived at [perma.cc/9SAW-68MP](https://perma.cc/9SAW-68MP)
[[13](ch02.html#Sackman2016_ch2-marker)] Matthew Sackman. [Pushing Back](https://wellquite.org/posts/lshift/pushing_back/). *wellquite.org*, May 2016. Archived at [perma.cc/3KCZ-RUFY](https://perma.cc/3KCZ-RUFY)
[[14](ch02.html#Kopytkov2018-marker)] Dmitry Kopytkov and Patrick Lee. [Meet Bandaid, the Dropbox service proxy](https://dropbox.tech/infrastructure/meet-bandaid-the-dropbox-service-proxy). *dropbox.tech*, March 2018. Archived at [perma.cc/KUU6-YG4S](https://perma.cc/KUU6-YG4S)
[[15](ch02.html#Gunawi2018-marker)] Haryadi S. Gunawi, Riza O. Suminto, Russell Sears, Casey Golliher, Swaminathan Sundararaman, Xing Lin, Tim Emami, Weiguang Sheng, Nematollah Bidokhti, Caitie McCaffrey, Gary Grider, Parks M. Fields, Kevin Harms, Robert B. Ross, Andree Jacobson, Robert Ricci, Kirk Webb, Peter Alvaro, H. Birali Runesha, Mingzhe Hao, and Huaicheng Li. [Fail-Slow at Scale: Evidence of Hardware Performance Faults in Large Production Systems](https://www.usenix.org/system/files/conference/fast18/fast18-gunawi.pdf). At *16th USENIX Conference on File and Storage Technologies*, February 2018.
[[16](ch02.html#DeCandia2007_ch1-marker)] Giuseppe DeCandia, Deniz Hastorun, Madan Jampani, Gunavardhan Kakulapati, Avinash Lakshman, Alex Pilchin, Swaminathan Sivasubramanian, Peter Vosshall, and Werner Vogels. [Dynamo: Amazons Highly Available Key-Value Store](http://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf). At *21st ACM Symposium on Operating Systems Principles* (SOSP), October 2007. [doi:10.1145/1294261.1294281](https://doi.org/10.1145/1294261.1294281)
[[17](ch02.html#Whitenton2020-marker)] Kathryn Whitenton. [The Need for Speed, 23 Years Later](https://www.nngroup.com/articles/the-need-for-speed/). *nngroup.com*, May 2020. Archived at [perma.cc/C4ER-LZYA](https://perma.cc/C4ER-LZYA)
[[18](ch02.html#Linden2006-marker)] Greg Linden. [Marissa Mayer at Web 2.0](https://glinden.blogspot.com/2006/11/marissa-mayer-at-web-20.html). *glinden.blogspot.com*, November 2005. Archived at [perma.cc/V7EA-3VXB](https://perma.cc/V7EA-3VXB)
[[19](ch02.html#Brutlag2009-marker)] Jake Brutlag. [Speed Matters for Google Web Search](https://services.google.com/fh/files/blogs/google_delayexp.pdf). *services.google.com*, June 2009. Archived at [perma.cc/BK7R-X7M2](https://perma.cc/BK7R-X7M2)
[[20](ch02.html#Schurman2009-marker)] Eric Schurman and Jake Brutlag. [Performance Related Changes and their User Impact](https://www.youtube.com/watch?v=bQSE51-gr2s). Talk at *Velocity 2009*.
[[21](ch02.html#Akamai2017-marker)] Akamai Technologies, Inc. [The State of Online Retail Performance](https://web.archive.org/web/20210729180749/https://www.akamai.com/us/en/multimedia/documents/report/akamai-state-of-online-retail-performance-spring-2017.pdf). *akamai.com*, April 2017. Archived at [perma.cc/UEK2-HYCS](https://perma.cc/UEK2-HYCS)
[[22](ch02.html#Bai2017-marker)] Xiao Bai, Ioannis Arapakis, B. Barla Cambazoglu, and Ana Freire. [Understanding and Leveraging the Impact of Response Latency on User Behaviour in Web Search](https://iarapakis.github.io/papers/TOIS17.pdf). *ACM Transactions on Information Systems*, volume 36, issue 2, article 21, April 2018. [doi:10.1145/3106372](https://doi.org/10.1145/3106372)
[[23](ch02.html#Dean2013-marker)] Jeffrey Dean and Luiz André Barroso. [The Tail at Scale](http://cacm.acm.org/magazines/2013/2/160173-the-tail-at-scale/fulltext). *Communications of the ACM*, volume 56, issue 2, pages 7480, February 2013. [doi:10.1145/2408776.2408794](https://doi.org/10.1145/2408776.2408794)
[[24](ch02.html#Hidalgo2020-marker)] Alex Hidalgo. [*Implementing Service Level Objectives: A Practical Guide to SLIs, SLOs, and Error Budgets*](https://www.oreilly.com/library/view/implementing-service-level/9781492076803/). OReilly Media, September 2020. ISBN: 1492076813
[[25](ch02.html#Mogul2019-marker)] Jeffrey C. Mogul and John Wilkes. [Nines are Not Enough: Meaningful Metrics for Clouds](https://research.google/pubs/pub48033/). At *17th Workshop on Hot Topics in Operating Systems* (HotOS), May 2019. [doi:10.1145/3317550.3321432](https://doi.org/10.1145/3317550.3321432)
[[26](ch02.html#Hauer2020-marker)] Tamás Hauer, Philipp Hoffmann, John Lunney, Dan Ardelean, and Amer Diwan. [Meaningful Availability](https://www.usenix.org/conference/nsdi20/presentation/hauer). At *17th USENIX Symposium on Networked Systems Design and Implementation* (NSDI), February 2020.
[[27](ch02.html#Dunning2021-marker)] Ted Dunning. [The t-digest: Efficient estimates of distributions](https://www.sciencedirect.com/science/article/pii/S2665963820300403). *Software Impacts*, volume 7, article 100049, February 2021. [doi:10.1016/j.simpa.2020.100049](https://doi.org/10.1016/j.simpa.2020.100049)
[[28](ch02.html#Kohn2021-marker)] David Kohn. [How percentile approximation works (and why its more useful than averages)](https://www.timescale.com/blog/how-percentile-approximation-works-and-why-its-more-useful-than-averages/). *timescale.com*, September 2021. Archived at [perma.cc/3PDP-NR8B](https://perma.cc/3PDP-NR8B)
[[29](ch02.html#Hartmann2020-marker)] Heinrich Hartmann and Theo Schlossnagle. [Circllhist — A Log-Linear Histogram Data Structure for IT Infrastructure Monitoring](https://arxiv.org/pdf/2001.06561.pdf). *arxiv.org*, January 2020.
[[30](ch02.html#Masson2019-marker)] Charles Masson, Jee E. Rim, and Homin K. Lee. [DDSketch: A Fast and Fully-Mergeable Quantile Sketch with Relative-Error Guarantees](http://www.vldb.org/pvldb/vol12/p2195-masson.pdf). *Proceedings of the VLDB Endowment*, volume 12, issue 12, pages 21952205, August 2019. [doi:10.14778/3352063.3352135](https://doi.org/10.14778/3352063.3352135)
[[31](ch02.html#Schwartz2015-marker)] Baron Schwartz. [Why Percentiles Dont Work the Way You Think](https://orangematter.solarwinds.com/2016/11/18/why-percentiles-dont-work-the-way-you-think/). *solarwinds.com*, November 2016. Archived at [perma.cc/469T-6UGB](https://perma.cc/469T-6UGB)
[[32](ch02.html#Heimerdinger1992-marker)] Walter L. Heimerdinger and Charles B. Weinstock. [A Conceptual Framework for System Fault Tolerance](https://resources.sei.cmu.edu/asset_files/TechnicalReport/1992_005_001_16112.pdf). Technical Report CMU/SEI-92-TR-033, Software Engineering Institute, Carnegie Mellon University, October 1992. Archived at [perma.cc/GD2V-DMJW](https://perma.cc/GD2V-DMJW)
[[33](ch02.html#Gaertner1999-marker)] Felix C. Gärtner. [Fundamentals of fault-tolerant distributed computing in asynchronous environments](https://dl.acm.org/doi/pdf/10.1145/311531.311532). *ACM Computing Surveys*, volume 31, issue 1, pages 126, March 1999. [doi:10.1145/311531.311532](https://doi.org/10.1145/311531.311532)
[[34](ch02.html#Yuan2014-marker)] Ding Yuan, Yu Luo, Xin Zhuang, Guilherme Renna Rodrigues, Xu Zhao, Yongle Zhang, Pranay U. Jain, and Michael Stumm. [Simple Testing Can Prevent Most Critical Failures: An Analysis of Production Failures in Distributed Data-Intensive Systems](https://www.usenix.org/system/files/conference/osdi14/osdi14-paper-yuan.pdf). At *11th USENIX Symposium on Operating Systems Design and Implementation* (OSDI), October 2014.
[[35](ch02.html#Rosenthal2020-marker)] Casey Rosenthal and Nora Jones. [*Chaos Engineering*](https://learning.oreilly.com/library/view/chaos-engineering/9781492043850/). OReilly Media, April 2020. ISBN: 9781492043867
[[36](ch02.html#Pinheiro2007-marker)] Eduardo Pinheiro, Wolf-Dietrich Weber, and Luiz Andre Barroso. [Failure Trends in a Large Disk Drive Population](https://www.usenix.org/legacy/events/fast07/tech/full_papers/pinheiro/pinheiro_old.pdf). At *5th USENIX Conference on File and Storage Technologies* (FAST), February 2007.
[[37](ch02.html#Schroeder2007-marker)] Bianca Schroeder and Garth A. Gibson. [Disk failures in the real world: What does an MTTF of 1,000,000 hours mean to you?](https://www.usenix.org/legacy/events/fast07/tech/schroeder/schroeder.pdf) At *5th USENIX Conference on File and Storage Technologies* (FAST), February 2007.
[[38](ch02.html#Klein2021-marker)] Andy Klein. [Backblaze Drive Stats for Q2 2021](https://www.backblaze.com/blog/backblaze-drive-stats-for-q2-2021/). *backblaze.com*, August 2021. Archived at [perma.cc/2943-UD5E](https://perma.cc/2943-UD5E)
[[39](ch02.html#Narayanan2016-marker)] Iyswarya Narayanan, Di Wang, Myeongjae Jeon, Bikash Sharma, Laura Caulfield, Anand Sivasubramaniam, Ben Cutler, Jie Liu, Badriddine Khessib, and Kushagra Vaid. [SSD Failures in Datacenters: What? When? and Why?](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/08/a7-narayanan.pdf) At *9th ACM International on Systems and Storage Conference* (SYSTOR), June 2016. [doi:10.1145/2928275.2928278](https://doi.org/10.1145/2928275.2928278)
[[40](ch02.html#Alibaba2019_ch2-marker)] Alibaba Cloud Storage Team. [Storage System Design Analysis: Factors Affecting NVMe SSD Performance (1)](https://www.alibabacloud.com/blog/594375). *alibabacloud.com*, January 2019. Archived at [archive.org](https://web.archive.org/web/20230522005034/https://www.alibabacloud.com/blog/594375)
[[41](ch02.html#Schroeder2016-marker)] Bianca Schroeder, Raghav Lagisetty, and Arif Merchant. [Flash Reliability in Production: The Expected and the Unexpected](https://www.usenix.org/system/files/conference/fast16/fast16-papers-schroeder.pdf). At *14th USENIX Conference on File and Storage Technologies* (FAST), February 2016.
[[42](ch02.html#Alter2019-marker)] Jacob Alter, Ji Xue, Alma Dimnaku, and Evgenia Smirni. [SSD failures in the field: symptoms, causes, and prediction models](https://dl.acm.org/doi/pdf/10.1145/3295500.3356172). At *International Conference for High Performance Computing, Networking, Storage and Analysis* (SC), November 2019. [doi:10.1145/3295500.3356172](https://doi.org/10.1145/3295500.3356172)
[[43](ch02.html#Ford2010-marker)] Daniel Ford, François Labelle, Florentina I. Popovici, Murray Stokely, Van-Anh Truong, Luiz Barroso, Carrie Grimes, and Sean Quinlan. [Availability in Globally Distributed Storage Systems](https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Ford.pdf). At *9th USENIX Symposium on Operating Systems Design and Implementation* (OSDI), October 2010.
[[44](ch02.html#Vishwanath2010-marker)] Kashi Venkatesh Vishwanath and Nachiappan Nagappan. [Characterizing Cloud Computing Hardware Reliability](https://www.microsoft.com/en-us/research/wp-content/uploads/2010/06/socc088-vishwanath.pdf). At *1st ACM Symposium on Cloud Computing* (SoCC), June 2010. [doi:10.1145/1807128.1807161](https://doi.org/10.1145/1807128.1807161)
[[45](ch02.html#Hochschild2021-marker)] Peter H. Hochschild, Paul Turner, Jeffrey C. Mogul, Rama Govindaraju, Parthasarathy Ranganathan, David E. Culler, and Amin Vahdat. [Cores that dont count](https://sigops.org/s/conferences/hotos/2021/papers/hotos21-s01-hochschild.pdf). At *Workshop on Hot Topics in Operating Systems* (HotOS), June 2021. [doi:10.1145/3458336.3465297](https://doi.org/10.1145/3458336.3465297)
[[46](ch02.html#Dixit2021-marker)] Harish Dattatraya Dixit, Sneha Pendharkar, Matt Beadon, Chris Mason, Tejasvi Chakravarthy, Bharath Muthiah, and Sriram Sankar. [Silent Data Corruptions at Scale](https://arxiv.org/abs/2102.11245). *arXiv:2102.11245*, February 2021.
[[47](ch02.html#Behrens2015-marker)] Diogo Behrens, Marco Serafini, Sergei Arnautov, Flavio P. Junqueira, and Christof Fetzer. [Scalable Error Isolation for Distributed Systems](https://www.usenix.org/conference/nsdi15/technical-sessions/presentation/behrens). At *12th USENIX Symposium on Networked Systems Design and Implementation* (NSDI), May 2015.
[[48](ch02.html#Schroeder2009-marker)] Bianca Schroeder, Eduardo Pinheiro, and Wolf-Dietrich Weber. [DRAM Errors in the Wild: A Large-Scale Field Study](https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/35162.pdf). At *11th International Joint Conference on Measurement and Modeling of Computer Systems* (SIGMETRICS), June 2009. [doi:10.1145/1555349.1555372](https://doi.org/10.1145/1555349.1555372)
[[49](ch02.html#Kim2014-marker)] Yoongu Kim, Ross Daly, Jeremie Kim, Chris Fallin, Ji Hye Lee, Donghyuk Lee, Chris Wilkerson, Konrad Lai, and Onur Mutlu. [Flipping Bits in Memory Without Accessing Them: An Experimental Study of DRAM Disturbance Errors](https://users.ece.cmu.edu/~yoonguk/papers/kim-isca14.pdf). At *41st Annual International Symposium on Computer Architecture* (ISCA), June 2014. [doi:10.5555/2665671.2665726](https://doi.org/10.5555/2665671.2665726)
[[50](ch02.html#Cockcroft2019-marker)] Adrian Cockcroft. [Failure Modes and Continuous Resilience](https://adrianco.medium.com/failure-modes-and-continuous-resilience-6553078caad5). *adrianco.medium.com*, November 2019. Archived at [perma.cc/7SYS-BVJP](https://perma.cc/7SYS-BVJP)
[[51](ch02.html#Han2021-marker)] Shujie Han, Patrick P. C. Lee, Fan Xu, Yi Liu, Cheng He, and Jiongzhou Liu. [An In-Depth Study of Correlated Failures in Production SSD-Based Data Centers](https://www.usenix.org/conference/fast21/presentation/han). At *19th USENIX Conference on File and Storage Technologies* (FAST), February 2021.
[[52](ch02.html#Nightingale2011-marker)] Edmund B. Nightingale, John R. Douceur, and Vince Orgovan. [Cycles, Cells and Platters: An Empirical Analysis of Hardware Failures on a Million Consumer PCs](https://eurosys2011.cs.uni-salzburg.at/pdf/eurosys2011-nightingale.pdf). At *6th European Conference on Computer Systems* (EuroSys), April 2011. [doi:10.1145/1966445.1966477](https://doi.org/10.1145/1966445.1966477)
[[53](ch02.html#Gunawi2014-marker)] Haryadi S. Gunawi, Mingzhe Hao, Tanakorn Leesatapornwongsa, Tiratat Patana-anake, Thanh Do, Jeffry Adityatama, Kurnia J. Eliazar, Agung Laksono, Jeffrey F. Lukman, Vincentius Martin, and Anang D. Satria. [What Bugs Live in the Cloud?](http://ucare.cs.uchicago.edu/pdf/socc14-cbs.pdf) At *5th ACM Symposium on Cloud Computing* (SoCC), November 2014. [doi:10.1145/2670979.2670986](https://doi.org/10.1145/2670979.2670986)
[[54](ch02.html#Kreps2012_ch1-marker)] Jay Kreps. [Getting Real About Distributed System Reliability](http://blog.empathybox.com/post/19574936361/getting-real-about-distributed-system-reliability). *blog.empathybox.com*, March 2012. Archived at [perma.cc/9B5Q-AEBW](https://perma.cc/9B5Q-AEBW)
[[55](ch02.html#Minar2012_ch1-marker)] Nelson Minar. [Leap Second Crashes Half the Internet](http://www.somebits.com/weblog/tech/bad/leap-second-2012.html). *somebits.com*, July 2012. Archived at [perma.cc/2WB8-D6EU](https://perma.cc/2WB8-D6EU)
[[56](ch02.html#HPE2019-marker)] Hewlett Packard Enterprise. [Support Alerts Customer Bulletin a00092491en_us](https://support.hpe.com/hpesc/public/docDisplay?docId=emr_na-a00092491en_us). *support.hpe.com*, November 2019. Archived at [perma.cc/S5F6-7ZAC](https://perma.cc/S5F6-7ZAC)
[[57](ch02.html#Hochstein2020-marker)] Lorin Hochstein. [awesome limits](https://github.com/lorin/awesome-limits). *github.com*, November 2020. Archived at [perma.cc/3R5M-E5Q4](https://perma.cc/3R5M-E5Q4)
[[58](ch02.html#Tang2023-marker)] Lilia Tang, Chaitanya Bhandari, Yongle Zhang, Anna Karanika, Shuyang Ji, Indranil Gupta, and Tianyin Xu. [Fail through the Cracks: Cross-System Interaction Failures in Modern Cloud Systems](https://tianyin.github.io/pub/csi-failures.pdf). At *18th European Conference on Computer Systems* (EuroSys), May 2023. [doi:10.1145/3552326.3587448](https://doi.org/10.1145/3552326.3587448)
[[59](ch02.html#Ulrich2016-marker)] Mike Ulrich. [Addressing Cascading Failures](https://sre.google/sre-book/addressing-cascading-failures/). In Betsy Beyer, Jennifer Petoff, Chris Jones, and Niall Richard Murphy (ed). [*Site Reliability Engineering: How Google Runs Production Systems*](https://www.oreilly.com/library/view/site-reliability-engineering/9781491929117/). OReilly Media, 2016. ISBN: 9781491929124
[[60](ch02.html#Fassbender2022-marker)] Harri Faßbender. [Cascading failures in large-scale distributed systems](https://blog.mi.hdm-stuttgart.de/index.php/2022/03/03/cascading-failures-in-large-scale-distributed-systems/). *blog.mi.hdm-stuttgart.de*, March 2022. Archived at [perma.cc/K7VY-YJRX](https://perma.cc/K7VY-YJRX)
[[61](ch02.html#Cook2000-marker)] Richard I. Cook. [How Complex Systems Fail](https://www.adaptivecapacitylabs.com/HowComplexSystemsFail.pdf). Cognitive Technologies Laboratory, April 2000. Archived at [perma.cc/RDS6-2YVA](https://perma.cc/RDS6-2YVA)
[[62](ch02.html#Woods2017-marker)] David D Woods. [STELLA: Report from the SNAFUcatchers Workshop on Coping With Complexity](https://snafucatchers.github.io/). *snafucatchers.github.io*, March 2017. Archived at [archive.org](https://web.archive.org/web/20230306130131/https://snafucatchers.github.io/)
[[63](ch02.html#Oppenheimer2003-marker)] David Oppenheimer, Archana Ganapathi, and David A. Patterson. [Why Do Internet Services Fail, and What Can Be Done About It?](http://static.usenix.org/legacy/events/usits03/tech/full_papers/oppenheimer/oppenheimer.pdf) At *4th USENIX Symposium on Internet Technologies and Systems* (USITS), March 2003.
[[64](ch02.html#Dekker2017-marker)] Sidney Dekker. [*The Field Guide to Understanding Human Error, 3rd Edition*](https://learning.oreilly.com/library/view/the-field-guide/9781317031833/). CRC Press, November 2017. ISBN: 9781472439055
[[65](ch02.html#Allspaw2012-marker)] John Allspaw. [Blameless PostMortems and a Just Culture](https://www.etsy.com/codeascraft/blameless-postmortems/). *etsy.com*, May 2012. Archived at [perma.cc/YMJ7-NTAP](https://perma.cc/YMJ7-NTAP)
[[66](ch02.html#Sabo2023-marker)] Itzy Sabo. [Uptime Guarantees — A Pragmatic Perspective](https://world.hey.com/itzy/uptime-guarantees-a-pragmatic-perspective-736d7ea4). *world.hey.com*, March 2023. Archived at [perma.cc/F7TU-78JB](https://perma.cc/F7TU-78JB)
[[67](ch02.html#Jurewitz2013-marker)] Michael Jurewitz. [The Human Impact of Bugs](http://jury.me/blog/2013/3/14/the-human-impact-of-bugs). *jury.me*, March 2013. Archived at [perma.cc/5KQ4-VDYL](https://perma.cc/5KQ4-VDYL)
[[68](ch02.html#Siddique2021-marker)] Haroon Siddique and Ben Quinn. [Court clears 39 post office operators convicted due to corrupt data](https://www.theguardian.com/uk-news/2021/apr/23/court-clears-39-post-office-staff-convicted-due-to-corrupt-data). *theguardian.com*, April 2021. Archived at [archive.org](https://web.archive.org/web/20220630124107/https://www.theguardian.com/uk-news/2021/apr/23/court-clears-39-post-office-staff-convicted-due-to-corrupt-data)
[[69](ch02.html#Bohm2022-marker)] Nicholas Bohm, James Christie, Peter Bernard Ladkin, Bev Littlewood, Paul Marshall, Stephen Mason, Martin Newby, Steven J. Murdoch, Harold Thimbleby, and Martyn Thomas. [The legal rule that computers are presumed to be operating correctly unforeseen and unjust consequences](https://www.benthamsgaze.org/wp-content/uploads/2022/06/briefing-presumption-that-computers-are-reliable.pdf). Briefing note, *benthamsgaze.org*, June 2022. Archived at [perma.cc/WQ6X-TMW4](https://perma.cc/WQ6X-TMW4)
[[70](ch02.html#McKinley2015-marker)] Dan McKinley. [Choose Boring Technology](https://mcfunley.com/choose-boring-technology). *mcfunley.com*, March 2015. Archived at [perma.cc/7QW7-J4YP](https://perma.cc/7QW7-J4YP)
[[71](ch02.html#Warfield2023-marker)] Andy Warfield. [Building and operating a pretty big storage system called S3](https://www.allthingsdistributed.com/2023/07/building-and-operating-a-pretty-big-storage-system.html). *allthingsdistributed.com*, July 2023. Archived at [perma.cc/7LPK-TP7V](https://perma.cc/7LPK-TP7V)
[[72](ch02.html#Brooker2023-marker)] Marc Brooker. [Surprising Scalability of Multitenancy](https://brooker.co.za/blog/2023/03/23/economics.html). *brooker.co.za*, March 2023. Archived at [archive.org](https://web.archive.org/web/20230404065818/https://brooker.co.za/blog/2023/03/23/economics.html)
[[73](ch02.html#Stopford2009-marker)] Ben Stopford. [Shared Nothing vs. Shared Disk Architectures: An Independent View](http://www.benstopford.com/2009/11/24/understanding-the-shared-nothing-architecture/). *benstopford.com*, November 2009. Archived at [perma.cc/7BXH-EDUR](https://perma.cc/7BXH-EDUR)
[[74](ch02.html#Stonebraker1986-marker)] Michael Stonebraker. [The Case for Shared Nothing](http://db.cs.berkeley.edu/papers/hpts85-nothing.pdf). *IEEE Database Engineering Bulletin*, volume 9, issue 1, pages 49, March 1986.
[[75](ch02.html#Antonopoulos2019_ch2-marker)] Panagiotis Antonopoulos, Alex Budovski, Cristian Diaconu, Alejandro Hernandez Saenz, Jack Hu, Hanuma Kodavalla, Donald Kossmann, Sandeep Lingam, Umar Farooq Minhas, Naveen Prakash, Vijendra Purohit, Hugh Qu, Chaitanya Sreenivas Ravella, Krystyna Reisteter, Sheetal Shrotri, Dixin Tang, and Vikram Wakade. [Socrates: The New SQL Server in the Cloud](https://www.microsoft.com/en-us/research/uploads/prod/2019/05/socrates.pdf). At *ACM International Conference on Management of Data* (SIGMOD), pages 17431756, June 2019. [doi:10.1145/3299869.3314047](https://doi.org/10.1145/3299869.3314047)
[[76](ch02.html#Newman2021_ch2-marker)] Sam Newman. [*Building Microservices*, second edition](https://www.oreilly.com/library/view/building-microservices-2nd/9781492034018/). OReilly Media, 2021. ISBN: 9781492034025
[[77](ch02.html#Ensmenger2016-marker)] Nathan Ensmenger. [When Good Software Goes Bad: The Surprising Durability of an Ephemeral Technology](https://themaintainers.wpengine.com/wp-content/uploads/2021/04/ensmenger-maintainers-v2.pdf). At *The Maintainers Conference*, April 2016. Archived at [perma.cc/ZXT4-HGZB](https://perma.cc/ZXT4-HGZB)
[[78](ch02.html#Glass2002-marker)] Robert L. Glass. [*Facts and Fallacies of Software Engineering*](https://learning.oreilly.com/library/view/facts-and-fallacies/0321117425/). Addison-Wesley Professional, October 2002. ISBN: 9780321117427
[[79](ch02.html#Bellotti2021-marker)] Marianne Bellotti. [*Kill It with Fire*](https://learning.oreilly.com/library/view/kill-it-with/9781098128883/). No Starch Press, April 2021. ISBN: 9781718501188
[[80](ch02.html#Bainbridge1983-marker)] Lisanne Bainbridge. [Ironies of automation](https://www.adaptivecapacitylabs.com/IroniesOfAutomation-Bainbridge83.pdf). *Automatica*, volume 19, issue 6, pages 775779, November 1983. [doi:10.1016/0005-1098(83)90046-8](https://doi.org/10.1016/0005-1098(83)90046-8)
[[81](ch02.html#Hamilton2007-marker)] James Hamilton. [On Designing and Deploying Internet-Scale Services](https://www.usenix.org/legacy/events/lisa07/tech/full_papers/hamilton/hamilton.pdf). At *21st Large Installation System Administration Conference* (LISA), November 2007.
[[82](ch02.html#Horovits2021-marker)] Dotan Horovits. [Open Source for Better Observability](https://horovits.medium.com/open-source-for-better-observability-8c65b5630561). *horovits.medium.com*, October 2021. Archived at [perma.cc/R2HD-U2ZT](https://perma.cc/R2HD-U2ZT)
[[83](ch02.html#Foote1997-marker)] Brian Foote and Joseph Yoder. [Big Ball of Mud](http://www.laputan.org/pub/foote/mud.pdf). At *4th Conference on Pattern Languages of Programs* (PLoP), September 1997. Archived at [perma.cc/4GUP-2PBV](https://perma.cc/4GUP-2PBV)
[[84](ch02.html#Brooker2022-marker)] Marc Brooker. [What is a simple system?](https://brooker.co.za/blog/2022/05/03/simplicity.html) *brooker.co.za*, May 2022. Archived at [archive.org](https://web.archive.org/web/20220602141902/https://brooker.co.za/blog/2022/05/03/simplicity.html)
[[85](ch02.html#Brooks1995-marker)] Frederick P Brooks. [No Silver Bullet Essence and Accident in Software Engineering](http://worrydream.com/refs/Brooks-NoSilverBullet.pdf). In [*The Mythical Man-Month*](https://www.oreilly.com/library/view/mythical-man-month-the/0201835959/), Anniversary edition, Addison-Wesley, 1995. ISBN: 9780201835953
[[86](ch02.html#Luu2020-marker)] Dan Luu. [Against essential and accidental complexity](https://danluu.com/essential-complexity/). *danluu.com*, December 2020. Archived at [perma.cc/H5ES-69KC](https://perma.cc/H5ES-69KC)
[[87](ch02.html#Gamma1994-marker)] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. [*Design Patterns: Elements of Reusable Object-Oriented Software*](https://learning.oreilly.com/library/view/design-patterns-elements/0201633612/). Addison-Wesley Professional, October 1994. ISBN: 9780201633610
[[88](ch02.html#Evans2003-marker)] Eric Evans. [*Domain-Driven Design: Tackling Complexity in the Heart of Software*](https://learning.oreilly.com/library/view/domain-driven-design-tackling/0321125215/). Addison-Wesley Professional, August 2003. ISBN: 9780321125217
[[89](ch02.html#Breivold2008-marker)] Hongyu Pei Breivold, Ivica Crnkovic, and Peter J. Eriksson. [Analyzing Software Evolvability](http://www.es.mdh.se/pdf_publications/1251.pdf). at *32nd Annual IEEE International Computer Software and Applications Conference* (COMPSAC), July 2008. [doi:10.1109/COMPSAC.2008.50](https://doi.org/10.1109/COMPSAC.2008.50)
[[90](ch02.html#Zaninotto2002-marker)] Enrico Zaninotto. [From X programming to the X organisation](https://martinfowler.com/articles/zaninotto.pdf). At *XP Conference*, May 2002. Archived at [perma.cc/R9AR-QCKZ](https://perma.cc/R9AR-QCKZ)

1234
content/v2/ch3.md Normal file

File diff suppressed because it is too large Load Diff

92
content/v2/toc.md Normal file
View File

@@ -0,0 +1,92 @@
---
title: "目录"
linkTitle: "目录"
weight: 10
breadcrumbs: false
---
![](/img/title.png)
## [序言](/preface)
## [第一部分:数据系统基础](/part-i)
### [第一章:数据系统架构中的利弊权衡](/v2/ch1)
* [关于数据系统的思考](/v2/ch1#关于数据系统的思考)
* [可靠性](/v2/ch1#术语:前端与后端)
* [可伸缩性](/v2/ch1#可伸缩性)
* [可维护性](/v2/ch1#可维护性)
* [本章小结](/v2/ch1#本章小结)
### [第二章:数据模型与查询语言](/v2/ch2)
* [关系模型与文档模型](/v2/ch2#关系模型与文档模型)
* [数据查询语言](/v2/ch2#数据查询语言)
* [图数据模型](/v2/ch2#图数据模型)
* [本章小结](/v2/ch2#本章小结)
### [第三章:存储与检索](/v2/ch3)
* [驱动数据库的数据结构](/v2/ch3#驱动数据库的数据结构)
* [事务处理还是分析?](/v2/ch3#事务处理还是分析)
* [列式存储](/v2/ch3#列式存储)
* [本章小结](/v2/ch3#本章小结)
### [第四章:编码与演化](/v2/ch4)
* [编码数据的格式](/v2/ch4#编码数据的格式)
* [数据流的类型](/v2/ch4#数据流的类型)
* [本章小结](/v2/ch4#本章小结)
## [第二部分:分布式数据](/part-ii)
### [第五章:复制](/v2/ch5)
* [领导者与追随者](/v2/ch5#领导者与追随者)
* [复制延迟问题](/v2/ch5#复制延迟问题)
* [多主复制](/v2/ch5#多主复制)
* [无主复制](/v2/ch5#无主复制)
* [本章小结](/v2/ch5#本章小结)
### [第六章:分区](/v2/ch6)
* [分区与复制](/v2/ch6#分区与复制)
* [键值数据的分区](/v2/ch6#键值数据的分区)
* [分区与次级索引](/v2/ch6#分区与次级索引)
* [分区再平衡](/v2/ch6#分区再平衡)
* [请求路由](/v2/ch6#请求路由)
* [本章小结](/v2/ch6#本章小结)
### [第七章:事务](/v2/ch7)
* [事务的棘手概念](/v2/ch7#事务的棘手概念)
* [弱隔离级别](/v2/ch7#弱隔离级别)
* [可串行化](/v2/ch7#可串行化)
* [本章小结](/v2/ch7#本章小结)
### [第八章:分布式系统的麻烦](/v2/ch8)
* [故障与部分失效](/v2/ch8#故障与部分失效)
* [不可靠的网络](/v2/ch8#不可靠的网络)
* [不可靠的时钟](/v2/ch8#不可靠的时钟)
* [知识、真相与谎言](/v2/ch8#知识真相与谎言)
* [本章小结](/v2/ch8#本章小结)
### [第九章:一致性与共识](/v2/ch9)
* [一致性保证](/v2/ch9#一致性保证)
* [线性一致性](/v2/ch9#线性一致性)
* [顺序保证](/v2/ch9#顺序保证)
* [分布式事务与共识](/v2/ch9#分布式事务与共识)
* [本章小结](/v2/ch9#本章小结)
## [第三部分:衍生数据](/part-iii)
### [第十章:批处理](/v2/ch10)
* [使用Unix工具的批处理](/v2/ch10#使用Unix工具的批处理)
* [MapReduce和分布式文件系统](/v2/ch10#MapReduce和分布式文件系统)
* [MapReduce之后](/v2/ch10#MapReduce之后)
* [本章小结](/v2/ch10#本章小结)
### [第十一章:流处理](/v2/ch11)
* [传递事件流](/v2/ch11#传递事件流)
* [数据库与流](/v2/ch11#数据库与流)
* [流处理](/v2/ch11#流处理)
* [本章小结](/v2/ch11#本章小结)
### [第十二章:数据系统的未来](/v2/ch12)
* [数据集成](/v2/ch12#数据集成)
* [分拆数据库](/v2/ch12#分拆数据库)
* [将事情做正确](/v2/ch12#将事情做正确)
* [做正确的事情](/v2/ch12#做正确的事情)
* [本章小结](/v2/ch12#本章小结)
### [术语表](/glossary)
### [后记](/colophon)

315
content/zh/_index.md Normal file
View File

@@ -0,0 +1,315 @@
---
title: 设计数据密集型应用
linkTitle: DDIA
cascade:
type: docs
breadcrumbs: false
---
**作者** [Martin Kleppmann](https://martin.kleppmann.com)[《Designing Data-Intensive Applications 2nd Edition》](https://learning.oreilly.com/library/view/designing-data-intensive-applications/9781098119058/ch01.html) 英国剑桥大学分布式系统研究员,演讲者,博主和开源贡献者,软件工程师和企业家,曾在 LinkedIn 和 Rapportive 负责数据基础架构。
**译者**[**冯若航**](https://vonng.com),网名 [@Vonng](https://github.com/Vonng)。
PostgreSQL 专家,数据库老司机,云计算泥石流。
[**Pigsty**](https://pgsty.com) 作者与创始人。
架构师DBA全栈工程师 @ TanTanAlibabaApple。
独立开源贡献者,[GitStar Ranking 585](https://gitstar-ranking.com/Vonng)[国区活跃 Top20](https://committers.top/china)。
[DDIA](https://ddia.pigsty.io) / [PG Internal](https://pgint.vonng.com) 中文版译者,公众号:《老冯云数》,数据库 KOL。
**校订** [@yingang](https://github.com/yingang) [繁體中文](/tw) **版本维护** by [@afunTW](https://github.com/afunTW) [完整贡献者列表](/contrib)
> [!NOTE]
> DDIA [**第二版**](https://github.com/Vonng/ddia/tree/v2) 正在翻译中 ([`v2`](https://github.com/Vonng/ddia/tree/v2)分支),欢迎加入并提出您的宝贵意见!
## 译序
> 不懂数据库的全栈工程师不是好架构师 —— 冯若航 / Vonng
现今尤其是在互联网领域大多数应用都属于数据密集型应用。本书从底层数据结构到顶层架构设计将数据系统设计中的精髓娓娓道来。其中的宝贵经验无论是对架构师、DBA、还是后端工程师、甚至产品经理都会有帮助。
这是一本理论结合实践的书,书中很多问题,译者在实际场景中都曾遇到过,读来让人击节扼腕。如果能早点读到这本书,该少走多少弯路啊!
这也是一本深入浅出的书,讲述概念的来龙去脉而不是卖弄定义,介绍事物发展演化历程而不是事实堆砌,将复杂的概念讲述的浅显易懂,但又直击本质不失深度。每章最后的引用质量非常好,是深入学习各个主题的绝佳索引。
本书为数据系统的设计、实现、与评价提供了很好的概念框架。读完并理解本书内容后,读者可以轻松看破大多数的技术忽悠,与技术砖家撕起来虎虎生风。
这是 2017 年译者读过最好的一本技术类书籍,这么好的书没有中文翻译,实在是遗憾。某不才,愿为先进技术文化的传播贡献一份力量。既可以深入学习有趣的技术主题,又可以锻炼中英文语言文字功底,何乐而不为?
## 前言
> 在我们的社会中,技术是一种强大的力量。数据、软件、通信可以用于坏的方面:不公平的阶级固化,损害公民权利,保护既得利益集团。但也可以用于好的方面:让底层人民发出自己的声音,让每个人都拥有机会,避免灾难。本书献给所有将技术用于善途的人们。
> 计算是一种流行文化,流行文化鄙视历史。流行文化关乎个体身份和参与感,但与合作无关。流行文化活在当下,也与过去和未来无关。我认为大部分(为了钱)编写代码的人就是这样的,他们不知道自己的文化来自哪里。
>
> —— 阿兰・凯接受 Dobb 博士的杂志采访时2012 年)
## 目录
### [序言](/preface)
### [第一部分:数据系统基础](/part-i)
* [第一章:可靠性、可伸缩性和可维护性](/ch1)
* [第二章:数据模型与查询语言](/ch2)
* [第三章:存储与检索](/ch3)
* [第四章:编码与演化](/ch4)
### [第二部分:分布式数据](/part-ii)
* [第五章:复制](/ch5)
* [第六章:分区](/ch6)
* [第七章:事务](/ch7)
* [第八章:分布式系统的麻烦](/ch8)
* [第九章:一致性与共识](/ch9)
### [第三部分:衍生数据](/part-iii)
* [第十章:批处理](/ch10)
* [第十一章:流处理](/ch11)
* [第十二章:数据系统的未来](/ch12)
### [术语表](/glossary)
### [后记](/colophon)
<br>
---------
## 法律声明
从原作者处得知,已经有简体中文的翻译计划,将于 2018 年末完成。[购买地址](https://search.jd.com/Search?keyword=设计数据密集型应用)
译者纯粹出于 **学习目的****个人兴趣** 翻译本书,不追求任何经济利益。
译者保留对此版本译文的署名权,其他权利以原作者和出版社的主张为准。
本译文只供学习研究参考之用,不得公开传播发行或用于商业用途。有能力阅读英文书籍者请购买正版支持。
---------
## 贡献
0. 全文校订 by [@yingang](https://github.com/Vonng/ddia/commits?author=yingang)
1. [序言初翻修正](https://github.com/Vonng/ddia/commit/afb5edab55c62ed23474149f229677e3b42dfc2c) by [@seagullbird](https://github.com/Vonng/ddia/commits?author=seagullbird)
2. [第一章语法标点校正](https://github.com/Vonng/ddia/commit/973b12cd8f8fcdf4852f1eb1649ddd9d187e3644) by [@nevertiree](https://github.com/Vonng/ddia/commits?author=nevertiree)
3. [第六章部分校正](https://github.com/Vonng/ddia/commit/d4eb0852c0ec1e93c8aacc496c80b915bb1e6d48) 与[第十章的初翻](https://github.com/Vonng/ddia/commit/9de8dbd1bfe6fbb03b3bf6c1a1aa2291aed2490e) by [@MuAlex](https://github.com/Vonng/ddia/commits?author=MuAlex)
4. [第一部分](/part-i)前言,[ch2](/ch2)校正 by [@jiajiadebug](https://github.com/Vonng/ddia/commits?author=jiajiadebug)
5. [词汇表](/glossary)、[后记](/colophon)关于野猪的部分 by [@Chowss](https://github.com/Vonng/ddia/commits?author=Chowss)
6. [繁體中文](https://github.com/Vonng/ddia/pulls)版本与转换脚本 by [@afunTW](https://github.com/afunTW)
7. 多处翻译修正 by [@songzhibin97](https://github.com/Vonng/ddia/commits?author=songzhibin97) [@MamaShip](https://github.com/Vonng/ddia/commits?author=MamaShip) [@FangYuan33](https://github.com/Vonng/ddia/commits?author=FangYuan33)
8. [感谢所有作出贡献,提出意见的朋友们](/contrib)
<details>
<summary><a href="https://github.com/Vonng/ddia/pulls">Pull Requests</a> & <a href="https://github.com/Vonng/ddia/issues">Issues</a></summary>
| ISSUE & Pull Requests | USER | Title |
|-------------------------------------------------|------------------------------------------------------------|----------------------------------------------------------------|
| [359](https://github.com/Vonng/ddia/pull/359) | [@c25423](https://github.com/c25423) | ch10: 修正一处拼写错误 |
| [358](https://github.com/Vonng/ddia/pull/358) | [@lewiszlw](https://github.com/lewiszlw) | ch4: 修正一处拼写错误 |
| [356](https://github.com/Vonng/ddia/pull/356) | [@lewiszlw](https://github.com/lewiszlw) | ch2: 修正一处标点错误 |
| [355](https://github.com/Vonng/ddia/pull/355) | [@DuroyGeorge](https://github.com/DuroyGeorge) | ch12: 修正一处格式错误 |
| [354](https://github.com/Vonng/ddia/pull/354) | [@justlorain](https://github.com/justlorain) | ch7: 修正一处参考链接 |
| [353](https://github.com/Vonng/ddia/pull/353) | [@fantasyczl](https://github.com/fantasyczl) | ch3&9: 修正两处引用错误 |
| [352](https://github.com/Vonng/ddia/pull/352) | [@fantasyczl](https://github.com/fantasyczl) | 支持输出为 EPUB 格式 |
| [349](https://github.com/Vonng/ddia/pull/349) | [@xiyihan0](https://github.com/xiyihan0) | ch1: 修正一处格式错误 |
| [348](https://github.com/Vonng/ddia/pull/348) | [@omegaatt36](https://github.com/omegaatt36) | ch3: 修正一处图像链接 |
| [346](https://github.com/Vonng/ddia/issues/346) | [@Vermouth1995](https://github.com/Vermouth1995) | ch1: 优化一处翻译 |
| [343](https://github.com/Vonng/ddia/pull/343) | [@kehao-chen](https://github.com/kehao-chen) | ch10: 优化一处翻译 |
| [341](https://github.com/Vonng/ddia/pull/341) | [@YKIsTheBest](https://github.com/YKIsTheBest) | ch3: 优化两处翻译 |
| [340](https://github.com/Vonng/ddia/pull/340) | [@YKIsTheBest](https://github.com/YKIsTheBest) | ch2: 优化多处翻译 |
| [338](https://github.com/Vonng/ddia/pull/338) | [@YKIsTheBest](https://github.com/YKIsTheBest) | ch1: 优化一处翻译 |
| [335](https://github.com/Vonng/ddia/pull/335) | [@kimi0230](https://github.com/kimi0230) | 修正一处繁体中文错误 |
| [334](https://github.com/Vonng/ddia/pull/334) | [@soulrrrrr](https://github.com/soulrrrrr) | ch2: 修正一处繁体中文错误 |
| [332](https://github.com/Vonng/ddia/pull/332) | [@justlorain](https://github.com/justlorain) | ch5: 修正一处翻译错误 |
| [331](https://github.com/Vonng/ddia/pull/331) | [@Lyianu](https://github.com/Lyianu) | ch9: 更正几处拼写错误 |
| [330](https://github.com/Vonng/ddia/pull/330) | [@Lyianu](https://github.com/Lyianu) | ch7: 优化一处翻译 |
| [329](https://github.com/Vonng/ddia/issues/329) | [@Lyianu](https://github.com/Lyianu) | ch6: 指出一处翻译错误 |
| [328](https://github.com/Vonng/ddia/pull/328) | [@justlorain](https://github.com/justlorain) | ch4: 更正一处翻译遗漏 |
| [326](https://github.com/Vonng/ddia/pull/326) | [@liangGTY](https://github.com/liangGTY) | ch1: 优化一处翻译 |
| [323](https://github.com/Vonng/ddia/pull/323) | [@marvin263](https://github.com/marvin263) | ch5: 优化一处翻译 |
| [322](https://github.com/Vonng/ddia/pull/322) | [@marvin263](https://github.com/marvin263) | ch8: 优化一处翻译 |
| [304](https://github.com/Vonng/ddia/pull/304) | [@spike014](https://github.com/spike014) | ch11: 优化一处翻译 |
| [298](https://github.com/Vonng/ddia/pull/298) | [@Makonike](https://github.com/Makonike) | ch11&12: 修正两处错误 |
| [284](https://github.com/Vonng/ddia/pull/284) | [@WAangzE](https://github.com/WAangzE) | ch4: 更正一处列表错误 |
| [283](https://github.com/Vonng/ddia/pull/283) | [@WAangzE](https://github.com/WAangzE) | ch3: 更正一处错别字 |
| [282](https://github.com/Vonng/ddia/pull/282) | [@WAangzE](https://github.com/WAangzE) | ch2: 更正一处公式问题 |
| [281](https://github.com/Vonng/ddia/pull/281) | [@lyuxi99](https://github.com/lyuxi99) | 更正多处内部链接错误 |
| [280](https://github.com/Vonng/ddia/pull/280) | [@lyuxi99](https://github.com/lyuxi99) | ch9: 更正内部链接错误 |
| [279](https://github.com/Vonng/ddia/issues/279) | [@codexvn](https://github.com/codexvn) | ch9: 指出公式在 GitHub Pages 显示的问题 |
| [278](https://github.com/Vonng/ddia/pull/278) | [@LJlkdskdjflsa](https://github.com/LJlkdskdjflsa) | 发现了繁体中文版本中的错误翻译 |
| [275](https://github.com/Vonng/ddia/pull/275) | [@117503445](https://github.com/117503445) | 更正 LICENSE 链接 |
| [274](https://github.com/Vonng/ddia/pull/274) | [@uncle-lv](https://github.com/uncle-lv) | ch7: 修正错别字 |
| [273](https://github.com/Vonng/ddia/pull/273) | [@Sdot-Python](https://github.com/Sdot-Python) | ch7: 统一了 write skew 的翻译 |
| [271](https://github.com/Vonng/ddia/pull/271) | [@Makonike](https://github.com/Makonike) | ch6: 统一了 rebalancing 的翻译 |
| [270](https://github.com/Vonng/ddia/pull/270) | [@Ynjxsjmh](https://github.com/Ynjxsjmh) | ch7: 修正不一致的翻译 |
| [263](https://github.com/Vonng/ddia/pull/263) | [@zydmayday](https://github.com/zydmayday) | ch5: 修正译文中的重复单词 |
| [260](https://github.com/Vonng/ddia/pull/260) | [@haifeiWu](https://github.com/haifeiWu) | ch4: 修正部分不准确的翻译 |
| [258](https://github.com/Vonng/ddia/pull/258) | [@bestgrc](https://github.com/bestgrc) | ch3: 修正一处翻译错误 |
| [257](https://github.com/Vonng/ddia/pull/257) | [@UnderSam](https://github.com/UnderSam) | ch8: 修正一处拼写错误 |
| [256](https://github.com/Vonng/ddia/pull/256) | [@AlphaWang](https://github.com/AlphaWang) | ch7: 修正“可串行化”相关内容的多处翻译不当 |
| [255](https://github.com/Vonng/ddia/pull/255) | [@AlphaWang](https://github.com/AlphaWang) | ch7: 修正“可重复读”相关内容的多处翻译不当 |
| [253](https://github.com/Vonng/ddia/pull/253) | [@AlphaWang](https://github.com/AlphaWang) | ch7: 修正“读已提交”相关内容的多处翻译不当 |
| [246](https://github.com/Vonng/ddia/pull/246) | [@derekwu0101](https://github.com/derekwu0101) | ch3: 修正繁体中文的转译错误 |
| [245](https://github.com/Vonng/ddia/pull/245) | [@skyran1278](https://github.com/skyran1278) | ch12: 修正繁体中文的转译错误 |
| [244](https://github.com/Vonng/ddia/pull/244) | [@Axlgrep](https://github.com/Axlgrep) | ch9: 修正不通顺的翻译 |
| [242](https://github.com/Vonng/ddia/pull/242) | [@lynkeib](https://github.com/lynkeib) | ch9: 修正不通顺的翻译 |
| [241](https://github.com/Vonng/ddia/pull/241) | [@lynkeib](https://github.com/lynkeib) | ch8: 修正不正确的公式格式 |
| [240](https://github.com/Vonng/ddia/pull/240) | [@8da2k](https://github.com/8da2k) | ch9: 修正不通顺的翻译 |
| [239](https://github.com/Vonng/ddia/pull/239) | [@BeBraveBeCurious](https://github.com/BeBraveBeCurious) | ch7: 修正不一致的翻译 |
| [237](https://github.com/Vonng/ddia/pull/237) | [@zhangnew](https://github.com/zhangnew) | ch3: 修正错误的图片链接 |
| [229](https://github.com/Vonng/ddia/pull/229) | [@lis186](https://github.com/lis186) | 指出繁体中文的转译错误:复杂 |
| [226](https://github.com/Vonng/ddia/pull/226) | [@chroming](https://github.com/chroming) | ch1: 修正导航栏中的章节名称 |
| [220](https://github.com/Vonng/ddia/pull/220) | [@skyran1278](https://github.com/skyran1278) | ch9: 修正线性一致的繁体中文翻译 |
| [194](https://github.com/Vonng/ddia/pull/194) | [@BeBraveBeCurious](https://github.com/BeBraveBeCurious) | ch4: 修正错误的翻译 |
| [193](https://github.com/Vonng/ddia/pull/193) | [@BeBraveBeCurious](https://github.com/BeBraveBeCurious) | ch4: 优化译文 |
| [192](https://github.com/Vonng/ddia/pull/192) | [@BeBraveBeCurious](https://github.com/BeBraveBeCurious) | ch4: 修正不一致和不通顺的翻译 |
| [190](https://github.com/Vonng/ddia/pull/190) | [@Pcrab](https://github.com/Pcrab) | ch1: 修正不准确的翻译 |
| [187](https://github.com/Vonng/ddia/pull/187) | [@narojay](https://github.com/narojay) | ch9: 修正生硬的翻译 |
| [186](https://github.com/Vonng/ddia/pull/186) | [@narojay](https://github.com/narojay) | ch8: 修正错别字 |
| [185](https://github.com/Vonng/ddia/issues/185) | [@8da2k](https://github.com/8da2k) | 指出小标题跳转的问题 |
| [184](https://github.com/Vonng/ddia/pull/184) | [@DavidZhiXing](https://github.com/DavidZhiXing) | ch10: 修正失效的网址 |
| [183](https://github.com/Vonng/ddia/pull/183) | [@OneSizeFitsQuorum](https://github.com/OneSizeFitsQuorum) | ch8: 修正错别字 |
| [182](https://github.com/Vonng/ddia/issues/182) | [@lroolle](https://github.com/lroolle) | 建议docsify的主题风格 |
| [181](https://github.com/Vonng/ddia/pull/181) | [@YunfengGao](https://github.com/YunfengGao) | ch2: 修正翻译错误 |
| [180](https://github.com/Vonng/ddia/pull/180) | [@skyran1278](https://github.com/skyran1278) | ch3: 指出繁体中文的转译错误 |
| [177](https://github.com/Vonng/ddia/pull/177) | [@exzhawk](https://github.com/exzhawk) | 支持 Github Pages 里的公式显示 |
| [176](https://github.com/Vonng/ddia/pull/176) | [@haifeiWu](https://github.com/haifeiWu) | ch2: 语义网相关翻译更正 |
| [175](https://github.com/Vonng/ddia/pull/175) | [@cwr31](https://github.com/cwr31) | ch7: 不变式相关翻译更正 |
| [174](https://github.com/Vonng/ddia/pull/174) | [@BeBraveBeCurious](https://github.com/BeBraveBeCurious) | README & preface: 更正不正确的中文用词和标点符号 |
| [173](https://github.com/Vonng/ddia/pull/173) | [@ZvanYang](https://github.com/ZvanYang) | ch12: 修正不完整的翻译 |
| [171](https://github.com/Vonng/ddia/pull/171) | [@ZvanYang](https://github.com/ZvanYang) | ch12: 修正重复的译文 |
| [169](https://github.com/Vonng/ddia/pull/169) | [@ZvanYang](https://github.com/ZvanYang) | ch12: 更正不太通顺的翻译 |
| [166](https://github.com/Vonng/ddia/pull/166) | [@bp4m4h94](https://github.com/bp4m4h94) | ch1: 发现错误的文献索引 |
| [164](https://github.com/Vonng/ddia/pull/164) | [@DragonDriver](https://github.com/DragonDriver) | preface: 更正错误的标点符号 |
| [163](https://github.com/Vonng/ddia/pull/163) | [@llmmddCoder](https://github.com/llmmddCoder) | ch1: 更正错误字 |
| [160](https://github.com/Vonng/ddia/pull/160) | [@Zhayhp](https://github.com/Zhayhp) | ch2: 建议将 network model 翻译为网状模型 |
| [159](https://github.com/Vonng/ddia/pull/159) | [@1ess](https://github.com/1ess) | ch4: 更正错误字 |
| [157](https://github.com/Vonng/ddia/pull/157) | [@ZvanYang](https://github.com/ZvanYang) | ch7: 更正不太通顺的翻译 |
| [155](https://github.com/Vonng/ddia/pull/155) | [@ZvanYang](https://github.com/ZvanYang) | ch7: 更正不太通顺的翻译 |
| [153](https://github.com/Vonng/ddia/pull/153) | [@DavidZhiXing](https://github.com/DavidZhiXing) | ch9: 修正缩略图的错别字 |
| [152](https://github.com/Vonng/ddia/pull/152) | [@ZvanYang](https://github.com/ZvanYang) | ch7: 除重->去重 |
| [151](https://github.com/Vonng/ddia/pull/151) | [@ZvanYang](https://github.com/ZvanYang) | ch5: 修订sibling相关的翻译 |
| [147](https://github.com/Vonng/ddia/pull/147) | [@ZvanYang](https://github.com/ZvanYang) | ch5: 更正一处不准确的翻译 |
| [145](https://github.com/Vonng/ddia/pull/145) | [@Hookey](https://github.com/Hookey) | 识别了当前简繁转译过程中处理不当的地方,暂通过转换脚本规避 |
| [144](https://github.com/Vonng/ddia/issues/144) | [@secret4233](https://github.com/secret4233) | ch7: 不翻译`next-key locking` |
| [143](https://github.com/Vonng/ddia/issues/143) | [@imcheney](https://github.com/imcheney) | ch3: 更新残留的机翻段落 |
| [142](https://github.com/Vonng/ddia/issues/142) | [@XIJINIAN](https://github.com/XIJINIAN) | 建议去除段首的制表符 |
| [141](https://github.com/Vonng/ddia/issues/141) | [@Flyraty](https://github.com/Flyraty) | ch5: 发现一处错误格式的章节引用 |
| [140](https://github.com/Vonng/ddia/pull/140) | [@Bowser1704](https://github.com/Bowser1704) | ch5: 修正章节Summary中多处不通顺的翻译 |
| [139](https://github.com/Vonng/ddia/pull/139) | [@Bowser1704](https://github.com/Bowser1704) | ch2&ch3: 修正多处不通顺的或错误的翻译 |
| [137](https://github.com/Vonng/ddia/pull/137) | [@fuxuemingzhu](https://github.com/fuxuemingzhu) | ch5&ch6: 优化多处不通顺的或错误的翻译 |
| [134](https://github.com/Vonng/ddia/pull/134) | [@fuxuemingzhu](https://github.com/fuxuemingzhu) | ch4: 优化多处不通顺的或错误的翻译 |
| [133](https://github.com/Vonng/ddia/pull/133) | [@fuxuemingzhu](https://github.com/fuxuemingzhu) | ch3: 优化多处错误的或不通顺的翻译 |
| [132](https://github.com/Vonng/ddia/pull/132) | [@fuxuemingzhu](https://github.com/fuxuemingzhu) | ch3: 优化一处容易产生歧义的翻译 |
| [131](https://github.com/Vonng/ddia/pull/131) | [@rwwg4](https://github.com/rwwg4) | ch6: 修正两处错误的翻译 |
| [129](https://github.com/Vonng/ddia/pull/129) | [@anaer](https://github.com/anaer) | ch4: 修正两处强调文本和四处代码变量名称 |
| [128](https://github.com/Vonng/ddia/pull/128) | [@meilin96](https://github.com/meilin96) | ch5: 修正一处错误的引用 |
| [126](https://github.com/Vonng/ddia/pull/126) | [@cwr31](https://github.com/cwr31) | ch10: 修正一处错误的翻译(功能 -> 函数) |
| [125](https://github.com/Vonng/ddia/pull/125) | [@dch1228](https://github.com/dch1228) | ch2: 优化 how best 的翻译(如何以最佳方式) |
| [123](https://github.com/Vonng/ddia/pull/123) | [@yingang](https://github.com/yingang) | translation updates (chapter 9, TOC in readme, glossary, etc.) |
| [121](https://github.com/Vonng/ddia/pull/121) | [@yingang](https://github.com/yingang) | translation updates (chapter 5 to chapter 8) |
| [120](https://github.com/Vonng/ddia/pull/120) | [@jiong-han](https://github.com/jiong-han) | Typo fix: 呲之以鼻 -> 嗤之以鼻 |
| [119](https://github.com/Vonng/ddia/pull/119) | [@cclauss](https://github.com/cclauss) | Streamline file operations in convert() |
| [118](https://github.com/Vonng/ddia/pull/118) | [@yingang](https://github.com/yingang) | translation updates (chapter 2 to chapter 4) |
| [117](https://github.com/Vonng/ddia/pull/117) | [@feeeei](https://github.com/feeeei) | 统一每章的标题格式 |
| [115](https://github.com/Vonng/ddia/pull/115) | [@NageNalock](https://github.com/NageNalock) | 第七章病句修改: 重复词语 |
| [114](https://github.com/Vonng/ddia/pull/114) | [@Sunt-ing](https://github.com/Sunt-ing) | Update README.md: correct the book name |
| [113](https://github.com/Vonng/ddia/pull/113) | [@lpxxn](https://github.com/lpxxn) | 修改语句 |
| [112](https://github.com/Vonng/ddia/pull/112) | [@ibyte2011](https://github.com/ibyte2011) | Update ch9.md |
| [110](https://github.com/Vonng/ddia/pull/110) | [@lpxxn](https://github.com/lpxxn) | 读已写入数据 |
| [107](https://github.com/Vonng/ddia/pull/107) | [@abbychau](https://github.com/abbychau) | 單調鐘和好死还是赖活着 |
| [106](https://github.com/Vonng/ddia/pull/106) | [@enochii](https://github.com/enochii) | typo in ch2: fix braces typo |
| [105](https://github.com/Vonng/ddia/pull/105) | [@LiminCode](https://github.com/LiminCode) | Chronicle translation error |
| [104](https://github.com/Vonng/ddia/pull/104) | [@Sunt-ing](https://github.com/Sunt-ing) | several advice for better translation |
| [103](https://github.com/Vonng/ddia/pull/103) | [@Sunt-ing](https://github.com/Sunt-ing) | typo in ch4: should be 完成 rather than 完全 |
| [102](https://github.com/Vonng/ddia/pull/102) | [@Sunt-ing](https://github.com/Sunt-ing) | ch4: better-translation: 扼杀 → 破坏 |
| [101](https://github.com/Vonng/ddia/pull/101) | [@Sunt-ing](https://github.com/Sunt-ing) | typo in Ch4: should be "改变" rathr than "盖面" |
| [100](https://github.com/Vonng/ddia/pull/100) | [@LiminCode](https://github.com/LiminCode) | fix missing translation |
| [99 ](https://github.com/Vonng/ddia/pull/99) | [@mrdrivingduck](https://github.com/mrdrivingduck) | ch6: fix the word rebalancing |
| [98 ](https://github.com/Vonng/ddia/pull/98) | [@jacklightChen](https://github.com/jacklightChen) | fix ch7.md: fix wrong references |
| [97 ](https://github.com/Vonng/ddia/pull/97) | [@jenac](https://github.com/jenac) | 96 |
| [96 ](https://github.com/Vonng/ddia/pull/96) | [@PragmaTwice](https://github.com/PragmaTwice) | ch2: fix typo about 'may or may not be' |
| [95 ](https://github.com/Vonng/ddia/pull/95) | [@EvanMu96](https://github.com/EvanMu96) | fix translation of "the battle cry" in ch5 |
| [94 ](https://github.com/Vonng/ddia/pull/94) | [@kemingy](https://github.com/kemingy) | ch6: fix markdown and punctuations |
| [93 ](https://github.com/Vonng/ddia/pull/93) | [@kemingy](https://github.com/kemingy) | ch5: fix markdown and some typos |
| [92 ](https://github.com/Vonng/ddia/pull/92) | [@Gilbert1024](https://github.com/Gilbert1024) | Merge pull request #1 from Vonng/master |
| [88 ](https://github.com/Vonng/ddia/pull/88) | [@kemingy](https://github.com/kemingy) | fix typo for ch1, ch2, ch3, ch4 |
| [87 ](https://github.com/Vonng/ddia/pull/87) | [@wynn5a](https://github.com/wynn5a) | Update ch3.md |
| [86 ](https://github.com/Vonng/ddia/pull/86) | [@northmorn](https://github.com/northmorn) | Update ch1.md |
| [85 ](https://github.com/Vonng/ddia/pull/85) | [@sunbuhui](https://github.com/sunbuhui) | fix ch2.md: fix ch2 ambiguous translation |
| [84 ](https://github.com/Vonng/ddia/pull/84) | [@ganler](https://github.com/ganler) | Fix translation: use up |
| [83 ](https://github.com/Vonng/ddia/pull/83) | [@afunTW](https://github.com/afunTW) | Using OpenCC to convert from zh-cn to zh-tw |
| [82 ](https://github.com/Vonng/ddia/pull/82) | [@kangni](https://github.com/kangni) | fix gitbook url |
| [78 ](https://github.com/Vonng/ddia/pull/78) | [@hanyu2](https://github.com/hanyu2) | Fix unappropriated translation |
| [77 ](https://github.com/Vonng/ddia/pull/77) | [@Ozarklake](https://github.com/Ozarklake) | fix typo |
| [75 ](https://github.com/Vonng/ddia/pull/75) | [@2997ms](https://github.com/2997ms) | Fix typo |
| [74 ](https://github.com/Vonng/ddia/pull/74) | [@2997ms](https://github.com/2997ms) | Update ch9.md |
| [70 ](https://github.com/Vonng/ddia/pull/70) | [@2997ms](https://github.com/2997ms) | Update ch7.md |
| [67 ](https://github.com/Vonng/ddia/pull/67) | [@jiajiadebug](https://github.com/jiajiadebug) | fix issues in ch2 - ch9 and glossary |
| [66 ](https://github.com/Vonng/ddia/pull/66) | [@blindpirate](https://github.com/blindpirate) | Fix typo |
| [63 ](https://github.com/Vonng/ddia/pull/63) | [@haifeiWu](https://github.com/haifeiWu) | Update ch10.md |
| [62 ](https://github.com/Vonng/ddia/pull/62) | [@ych](https://github.com/ych) | fix ch1.md typesetting problem |
| [61 ](https://github.com/Vonng/ddia/pull/61) | [@xianlaioy](https://github.com/xianlaioy) | docs:钟-->种去掉ou |
| [60 ](https://github.com/Vonng/ddia/pull/60) | [@Zombo1296](https://github.com/Zombo1296) | 否则 -> 或者 |
| [59 ](https://github.com/Vonng/ddia/pull/59) | [@AlexanderMisel](https://github.com/AlexanderMisel) | 呼叫->调用,显着->显著 |
| [58 ](https://github.com/Vonng/ddia/pull/58) | [@ibyte2011](https://github.com/ibyte2011) | Update ch8.md |
| [55 ](https://github.com/Vonng/ddia/pull/55) | [@saintube](https://github.com/saintube) | ch8: 修改链接错误 |
| [54 ](https://github.com/Vonng/ddia/pull/54) | [@Panmax](https://github.com/Panmax) | Update ch2.md |
| [53 ](https://github.com/Vonng/ddia/pull/53) | [@ibyte2011](https://github.com/ibyte2011) | Update ch9.md |
| [52 ](https://github.com/Vonng/ddia/pull/52) | [@hecenjie](https://github.com/hecenjie) | Update ch1.md |
| [51 ](https://github.com/Vonng/ddia/pull/51) | [@latavin243](https://github.com/latavin243) | fix 修正ch3 ch4几处翻译 |
| [50 ](https://github.com/Vonng/ddia/pull/50) | [@AlexZFX](https://github.com/AlexZFX) | 几个疏漏和格式错误 |
| [49 ](https://github.com/Vonng/ddia/pull/49) | [@haifeiWu](https://github.com/haifeiWu) | Update ch1.md |
| [48 ](https://github.com/Vonng/ddia/pull/48) | [@scaugrated](https://github.com/scaugrated) | fix typo |
| [47 ](https://github.com/Vonng/ddia/pull/47) | [@lzwill](https://github.com/lzwill) | Fixed typos in ch2 |
| [45 ](https://github.com/Vonng/ddia/pull/45) | [@zenuo](https://github.com/zenuo) | 删除一个多余的右括号 |
| [44 ](https://github.com/Vonng/ddia/pull/44) | [@akxxsb](https://github.com/akxxsb) | 修正第七章底部链接错误 |
| [43 ](https://github.com/Vonng/ddia/pull/43) | [@baijinping](https://github.com/baijinping) | "更假简单"->"更加简单" |
| [42 ](https://github.com/Vonng/ddia/pull/42) | [@tisonkun](https://github.com/tisonkun) | 修复 ch1 中的无序列表格式 |
| [38 ](https://github.com/Vonng/ddia/pull/38) | [@renjie-c](https://github.com/renjie-c) | 纠正多处的翻译小错误 |
| [37 ](https://github.com/Vonng/ddia/pull/37) | [@tankilo](https://github.com/tankilo) | fix translation mistakes in ch4.md |
| [36 ](https://github.com/Vonng/ddia/pull/36) | [@wwek](https://github.com/wwek) | 1.修复多个链接错误 2.名词优化修订 3.错误修订 |
| [35 ](https://github.com/Vonng/ddia/pull/35) | [@wwek](https://github.com/wwek) | fix ch7.md to ch8.md link error |
| [34 ](https://github.com/Vonng/ddia/pull/34) | [@wwek](https://github.com/wwek) | Merge pull request #1 from Vonng/master |
| [33 ](https://github.com/Vonng/ddia/pull/33) | [@wwek](https://github.com/wwek) | fix part-ii.md link error |
| [32 ](https://github.com/Vonng/ddia/pull/32) | [@JCYoky](https://github.com/JCYoky) | Update ch2.md |
| [31 ](https://github.com/Vonng/ddia/pull/31) | [@elsonLee](https://github.com/elsonLee) | Update ch7.md |
| [26 ](https://github.com/Vonng/ddia/pull/26) | [@yjhmelody](https://github.com/yjhmelody) | 修复一些明显错误 |
| [25 ](https://github.com/Vonng/ddia/pull/25) | [@lqbilbo](https://github.com/lqbilbo) | 修复链接错误 |
| [24 ](https://github.com/Vonng/ddia/pull/24) | [@artiship](https://github.com/artiship) | 修改词语顺序 |
| [23 ](https://github.com/Vonng/ddia/pull/23) | [@artiship](https://github.com/artiship) | 修正错别字 |
| [22 ](https://github.com/Vonng/ddia/pull/22) | [@artiship](https://github.com/artiship) | 纠正翻译错误 |
| [21 ](https://github.com/Vonng/ddia/pull/21) | [@zhtisi](https://github.com/zhtisi) | 修正目录和本章标题不符的情况 |
| [20 ](https://github.com/Vonng/ddia/pull/20) | [@rentiansheng](https://github.com/rentiansheng) | Update ch7.md |
| [19 ](https://github.com/Vonng/ddia/pull/19) | [@LHRchina](https://github.com/LHRchina) | 修复语句小bug |
| [16 ](https://github.com/Vonng/ddia/pull/16) | [@MuAlex](https://github.com/MuAlex) | Master |
| [15 ](https://github.com/Vonng/ddia/pull/15) | [@cg-zhou](https://github.com/cg-zhou) | Update translation progress |
| [14 ](https://github.com/Vonng/ddia/pull/14) | [@cg-zhou](https://github.com/cg-zhou) | Translate glossary |
| [13 ](https://github.com/Vonng/ddia/pull/13) | [@cg-zhou](https://github.com/cg-zhou) | 详细修改了后记中和印度野猪相关的描述 |
| [12 ](https://github.com/Vonng/ddia/pull/12) | [@ibyte2011](https://github.com/ibyte2011) | 修改了部分翻译 |
| [11 ](https://github.com/Vonng/ddia/pull/11) | [@jiajiadebug](https://github.com/jiajiadebug) | ch2 100% |
| [10 ](https://github.com/Vonng/ddia/pull/10) | [@jiajiadebug](https://github.com/jiajiadebug) | ch2 20% |
| [9 ](https://github.com/Vonng/ddia/pull/9) | [@jiajiadebug](https://github.com/jiajiadebug) | Preface, ch1, part-i translation minor fixes |
| [7 ](https://github.com/Vonng/ddia/pull/7) | [@MuAlex](https://github.com/MuAlex) | Ch6 translation pull request |
| [6 ](https://github.com/Vonng/ddia/pull/6) | [@MuAlex](https://github.com/MuAlex) | Ch6 change version1 |
| [5 ](https://github.com/Vonng/ddia/pull/5) | [@nevertiree](https://github.com/nevertiree) | Chapter 01语法微调 |
| [2 ](https://github.com/Vonng/ddia/pull/2) | [@seagullbird](https://github.com/seagullbird) | 序言初翻 |
</details><br />
---------
## 许可证
本项目采用 [CC-BY 4.0](https://github.com/Vonng/ddia/blob/master/LICENSE) 许可证,您可以在这里找到完整说明:
- [署名 4.0 协议国际版 CC BY 4.0 Deed](https://creativecommons.org/licenses/by/4.0/deed.zh-hans)
- [Attribution 4.0 International CC BY 4.0](https://creativecommons.org/licenses/by/4.0/deed.en)

View File

@@ -1,15 +1,17 @@
# 第一章:可靠性、可伸缩性和可维护性
---
title: "第一章:可靠性、可伸缩性和可维护性"
linkTitle: "1. 可靠性、可伸缩性和可维护性"
weight: 101
breadcrumbs: false
---
![](img/ch1.png)
![](/img/ch1.png)
> 互联网做得太棒了,以至于大多数人将它看作像太平洋这样的自然资源,而不是什么人工产物。上一次出现这种大规模且无差错的技术,你还记得是什么时候吗?
>
> —— [艾伦・凯](http://www.drdobbs.com/architecture-and-design/interview-with-alan-kay/240003442) 在接受 Dobb 博士杂志采访时说2012 年)
-----------------------
[TOC]
现今很多应用程序都是 **数据密集型data-intensive** 的,而非 **计算密集型compute-intensive** 的。因此 CPU 很少成为这类应用的瓶颈,更大的问题通常来自数据量、数据复杂性、以及数据的变更速度。
数据密集型应用通常由标准组件构建而成,标准组件提供了很多通用的功能;例如,许多应用程序都需要:
@@ -39,9 +41,9 @@
其次,越来越多的应用程序有着各种严格而广泛的要求,单个工具不足以满足所有的数据处理和存储需求。取而代之的是,总体工作被拆分成一系列能被单个工具高效完成的任务,并通过应用代码将它们缝合起来。
例如如果将缓存应用管理的缓存层Memcached 或同类产品)和全文搜索(全文搜索服务器,例如 Elasticsearch 或 Solr功能从主数据库剥离出来那么使缓存 / 索引与主数据库保持同步通常是应用代码的责任。[图 1-1](img/fig1-1.png) 给出了这种架构可能的样子(细节将在后面的章节中详细介绍)。
例如如果将缓存应用管理的缓存层Memcached 或同类产品)和全文搜索(全文搜索服务器,例如 Elasticsearch 或 Solr功能从主数据库剥离出来那么使缓存 / 索引与主数据库保持同步通常是应用代码的责任。[图 1-1](/img/fig1-1.png) 给出了这种架构可能的样子(细节将在后面的章节中详细介绍)。
![](img/fig1-1.png)
![](/img/fig1-1.png)
**图 1-1 一个可能的组合使用多个组件的数据系统架构**
@@ -166,7 +168,7 @@
大体上讲,这一对操作有两种实现方式。
1. 发布推文时,只需将新推文插入全局推文集合即可。当一个用户请求自己的主页时间线时,首先查找他关注的所有人,查询这些被关注用户发布的推文并按时间顺序合并。在如 [图 1-2](img/fig1-2.png) 所示的关系型数据库中,可以编写这样的查询:
1. 发布推文时,只需将新推文插入全局推文集合即可。当一个用户请求自己的主页时间线时,首先查找他关注的所有人,查询这些被关注用户发布的推文并按时间顺序合并。在如 [图 1-2](/img/fig1-2.png) 所示的关系型数据库中,可以编写这样的查询:
```sql
SELECT tweets.*, users.*
@@ -176,13 +178,13 @@
WHERE follows.follower_id = current_user
```
![](img/fig1-2.png)
![](/img/fig1-2.png)
**图 1-2 推特主页时间线的关系型模式简单实现**
2. 为每个用户的主页时间线维护一个缓存,就像每个用户的推文收件箱([图 1-3](img/fig1-3.png))。当一个用户发布推文时,查找所有关注该用户的人,并将新的推文插入到每个主页时间线缓存中。因此读取主页时间线的请求开销很小,因为结果已经提前计算好了。
2. 为每个用户的主页时间线维护一个缓存,就像每个用户的推文收件箱([图 1-3](/img/fig1-3.png))。当一个用户发布推文时,查找所有关注该用户的人,并将新的推文插入到每个主页时间线缓存中。因此读取主页时间线的请求开销很小,因为结果已经提前计算好了。
![](img/fig1-3.png)
![](/img/fig1-3.png)
**图 1-3 用于分发推特至关注者的数据流水线2012 年 11 月的负载参数【16】**
@@ -192,7 +194,7 @@
在推特的例子中,每个用户粉丝数的分布(可能按这些用户的发推频率来加权)是探讨可伸缩性的一个关键负载参数,因为它决定了扇出负载。你的应用程序可能具有非常不同的特征,但可以采用相似的原则来考虑它的负载。
推特轶事的最终转折:现在已经稳健地实现了方法 2推特逐步转向了两种方法的混合。大多数用户发的推文会被扇出写入其粉丝主页时间线缓存中。但是少数拥有海量粉丝的用户即名流会被排除在外。当用户读取主页时间线时分别地获取出该用户所关注的每位名流的推文再与用户的主页时间线缓存合并如方法 1 所示。这种混合方法能始终如一地提供良好性能。在 [第十二章](ch12.md) 中我们将重新讨论这个例子,这在覆盖更多技术层面之后。
推特轶事的最终转折:现在已经稳健地实现了方法 2推特逐步转向了两种方法的混合。大多数用户发的推文会被扇出写入其粉丝主页时间线缓存中。但是少数拥有海量粉丝的用户即名流会被排除在外。当用户读取主页时间线时分别地获取出该用户所关注的每位名流的推文再与用户的主页时间线缓存合并如方法 1 所示。这种混合方法能始终如一地提供良好性能。在 [第十二章](/ch12) 中我们将重新讨论这个例子,这在覆盖更多技术层面之后。
### 描述性能
@@ -213,9 +215,9 @@
即使不断重复发送同样的请求,每次得到的响应时间也都会略有不同。现实世界的系统会处理各式各样的请求,响应时间可能会有很大差异。因此我们需要将响应时间视为一个可以测量的数值 **分布distribution**,而不是单个数值。
在 [图 1-4](img/fig1-4.png) 中,每个灰条代表一次对服务的请求,其高度表示请求花费了多长时间。大多数请求是相当快的,但偶尔会出现需要更长的时间的异常值。这也许是因为缓慢的请求实质上开销更大,例如它们可能会处理更多的数据。但即使(你认为)所有请求都花费相同时间的情况下,随机的附加延迟也会导致结果变化,例如:上下文切换到后台进程,网络数据包丢失与 TCP 重传垃圾收集暂停强制从磁盘读取的页面错误服务器机架中的震动【18】还有很多其他原因。
在 [图 1-4](/img/fig1-4.png) 中,每个灰条代表一次对服务的请求,其高度表示请求花费了多长时间。大多数请求是相当快的,但偶尔会出现需要更长的时间的异常值。这也许是因为缓慢的请求实质上开销更大,例如它们可能会处理更多的数据。但即使(你认为)所有请求都花费相同时间的情况下,随机的附加延迟也会导致结果变化,例如:上下文切换到后台进程,网络数据包丢失与 TCP 重传垃圾收集暂停强制从磁盘读取的页面错误服务器机架中的震动【18】还有很多其他原因。
![](img/fig1-4.png)
![](/img/fig1-4.png)
**图 1-4 展示了一个服务 100 次请求响应时间的均值与百分位数**
@@ -225,7 +227,7 @@
如果想知道典型场景下用户需要等待多长时间,那么中位数是一个好的度量标准:一半用户请求的响应时间少于响应时间的中位数,另一半服务时间比中位数长。中位数也被称为第 50 百分位点,有时缩写为 p50。注意中位数是关于单个请求的如果用户同时发出几个请求在一个会话过程中或者由于一个页面中包含了多个资源则至少一个请求比中位数慢的概率远大于 50%。
为了弄清异常值有多糟糕,可以看看更高的百分位点,例如第 95、99 和 99.9 百分位点(缩写为 p95p99 和 p999。它们意味着 95%、99% 或 99.9% 的请求响应时间要比该阈值快,例如:如果第 95 百分位点响应时间是 1.5 秒,则意味着 100 个请求中的 95 个响应时间快于 1.5 秒,而 100 个请求中的 5 个响应时间超过 1.5 秒。如 [图 1-4](img/fig1-4.png) 所示。
为了弄清异常值有多糟糕,可以看看更高的百分位点,例如第 95、99 和 99.9 百分位点(缩写为 p95p99 和 p999。它们意味着 95%、99% 或 99.9% 的请求响应时间要比该阈值快,例如:如果第 95 百分位点响应时间是 1.5 秒,则意味着 100 个请求中的 95 个响应时间快于 1.5 秒,而 100 个请求中的 5 个响应时间超过 1.5 秒。如 [图 1-4](/img/fig1-4.png) 所示。
响应时间的高百分位点(也称为 **尾部延迟**,即 **tail latencies**)非常重要,因为它们直接影响用户的服务体验。例如亚马逊在描述内部服务的响应时间要求时是以 99.9 百分位点为准,即使它只影响一千个请求中的一个。这是因为请求响应最慢的客户往往也是数据最多的客户,也可以说是最有价值的客户 —— 因为他们掏钱了【19】。保证网站响应迅速对于保持客户的满意度非常重要亚马逊观察到响应时间增加 100 毫秒,销售量就减少 1%【20】而另一些报告说慢 1 秒钟会让客户满意度指标减少 16%【2122】。
@@ -239,13 +241,13 @@
> #### 实践中的百分位点
>
> 在多重调用的后端服务里,高百分位数变得特别重要。即使并行调用,最终用户请求仍然需要等待最慢的并行调用完成。如 [图 1-5](img/fig1-5.png) 所示,只需要一个缓慢的调用就可以使整个最终用户请求变慢。即使只有一小部分后端调用速度较慢,如果最终用户请求需要多个后端调用,则获得较慢调用的机会也会增加,因此较高比例的最终用户请求速度会变慢(该效果称为尾部延迟放大,即 tail latency amplification【24】
> 在多重调用的后端服务里,高百分位数变得特别重要。即使并行调用,最终用户请求仍然需要等待最慢的并行调用完成。如 [图 1-5](/img/fig1-5.png) 所示,只需要一个缓慢的调用就可以使整个最终用户请求变慢。即使只有一小部分后端调用速度较慢,如果最终用户请求需要多个后端调用,则获得较慢调用的机会也会增加,因此较高比例的最终用户请求速度会变慢(该效果称为尾部延迟放大,即 tail latency amplification【24】
>
> 如果你想将响应时间百分点添加到你的服务的监视仪表板则需要持续有效地计算它们。例如你可以使用滑动窗口来跟踪连续10分钟内的请求响应时间。每一分钟你都会计算出该窗口中的响应时间中值和各种百分数并将这些度量值绘制在图上。
>
> 简单的实现是在时间窗口内保存所有请求的响应时间列表,并且每分钟对列表进行排序。如果对你来说效率太低,那么有一些算法能够以最小的 CPU 和内存成本如前向衰减【25】、t-digest【26】或 HdrHistogram 【27】来计算百分位数的近似值。请注意平均百分比例如减少时间分辨率或合并来自多台机器的数据在数学上没有意义 - 聚合响应时间数据的正确方法是添加直方图【28】。
![](img/fig1-5.png)
![](/img/fig1-5.png)
**图 1-5 当一个请求需要多个后端请求时,单个后端慢请求就会拖慢整个终端用户的请求**
@@ -257,7 +259,7 @@
人们经常讨论 **纵向伸缩**scaling up也称为垂直伸缩即 vertical scaling转向更强大的机器**横向伸缩**scaling out也称为水平伸缩即 horizontal scaling将负载分布到多台小机器上之间的对立。跨多台机器分配负载也称为 “**无共享shared-nothing**” 架构。可以在单台机器上运行的系统通常更简单,但高端机器可能非常贵,所以非常密集的负载通常无法避免地需要横向伸缩。现实世界中的优秀架构需要将这两种方法务实地结合,因为使用几台足够强大的机器可能比使用大量的小型虚拟机更简单也更便宜。
有些系统是 **弹性elastic** 的,这意味着可以在检测到负载增加时自动增加计算资源,而其他系统则是手动伸缩(人工分析容量并决定向系统添加更多的机器)。如果负载 **极难预测highly unpredictable**,则弹性系统可能很有用,但手动伸缩系统更简单,并且意外操作可能会更少(请参阅 “[分区再平衡](ch6.md#分区再平衡)”)。
有些系统是 **弹性elastic** 的,这意味着可以在检测到负载增加时自动增加计算资源,而其他系统则是手动伸缩(人工分析容量并决定向系统添加更多的机器)。如果负载 **极难预测highly unpredictable**,则弹性系统可能很有用,但手动伸缩系统更简单,并且意外操作可能会更少(请参阅 “[分区再平衡](/ch6#分区再平衡)”)。
跨多台机器部署 **无状态服务stateless services** 非常简单,但将带状态的数据系统从单节点变为分布式配置则可能引入许多额外复杂度。出于这个原因,常识告诉我们应该将数据库放在单个节点上(纵向伸缩),直到伸缩成本或可用性需求迫使其改为分布式。
@@ -366,7 +368,7 @@
不幸的是,使应用可靠、可伸缩或可维护并不容易。但是某些模式和技术会不断重新出现在不同的应用中。在接下来的几章中,我们将看到一些数据系统的例子,并分析它们如何实现这些目标。
在本书后面的 [第三部分](part-iii.md) 中,我们将看到一种模式:几个组件协同工作以构成一个完整的系统(如 [图 1-1](img/fig1-1.png) 中的例子)
在本书后面的 [第三部分](/part-iii) 中,我们将看到一种模式:几个组件协同工作以构成一个完整的系统(如 [图 1-1](/img/fig1-1.png) 中的例子)
## 参考文献
@@ -404,11 +406,4 @@
1. Frederick P Brooks: “No Silver Bullet Essence and Accident in Software Engineering,” in *The Mythical Man-Month*, Anniversary edition, Addison-Wesley, 1995. ISBN: 978-0-201-83595-3
1. Ben Moseley and Peter Marks: “[Out of the Tar Pit](https://curtclifton.net/papers/MoseleyMarks06a.pdf),” at *BCS Software Practice Advancement* (SPA), 2006.
1. Rich Hickey: “[Simple Made Easy](http://www.infoq.com/presentations/Simple-Made-Easy),” at *Strange Loop*, September 2011.
1. Hongyu Pei Breivold, Ivica Crnkovic, and Peter J. Eriksson: “[Analyzing Software Evolvability](http://www.es.mdh.se/pdf_publications/1251.pdf),” at *32nd Annual IEEE International Computer Software and Applications Conference* (COMPSAC), July 2008. [doi:10.1109/COMPSAC.2008.50](http://dx.doi.org/10.1109/COMPSAC.2008.50)
------
| 上一章 | 目录 | 下一章 |
| ----------------------------------- | ------------------------------- | ------------------------------------ |
| [第一部分:数据系统基础](part-i.md) | [设计数据密集型应用](README.md) | [第二章:数据模型与查询语言](ch2.md) |
1. Hongyu Pei Breivold, Ivica Crnkovic, and Peter J. Eriksson: “[Analyzing Software Evolvability](http://www.es.mdh.se/pdf_publications/1251.pdf),” at *32nd Annual IEEE International Computer Software and Applications Conference* (COMPSAC), July 2008. [doi:10.1109/COMPSAC.2008.50](http://dx.doi.org/10.1109/COMPSAC.2008.50)

View File

@@ -1,18 +1,19 @@
# 第十章:批处理
---
title: "第十章:批处理"
linkTitle: "10. 批处理"
weight: 310
breadcrumbs: false
---
![](img/ch10.png)
![](/img/ch10.png)
> 带有太强个人色彩的系统无法成功。当最初的设计完成并且相对稳定时,不同的人们以自己的方式进行测试,真正的考验才开始。
>
> —— 高德纳
---------------
[TOC]
在本书的前两部分中,我们讨论了很多关于 **请求****查询** 以及相应的 **响应****结果**。许多现有数据系统中都采用这种数据处理方式你发送请求指令一段时间后我们期望系统会给出一个结果。数据库、缓存、搜索索引、Web 服务器以及其他一些系统都以这种方式工作。
像这样的 **在线online** 系统,无论是浏览器请求页面还是调用远程 API 的服务,我们通常认为请求是由人类用户触发的,并且正在等待响应。他们不应该等太久,所以我们非常关注系统的响应时间(请参阅 “[描述性能](ch1.md#描述性能)”)。
像这样的 **在线online** 系统,无论是浏览器请求页面还是调用远程 API 的服务,我们通常认为请求是由人类用户触发的,并且正在等待响应。他们不应该等太久,所以我们非常关注系统的响应时间(请参阅 “[描述性能](/ch1#描述性能)”)。
Web 和越来越多的基于 HTTP/REST 的 API 使交互的请求 / 响应风格变得如此普遍,以至于很容易将其视为理所当然。但我们应该记住,这不是构建系统的唯一方式,其他方法也有其优点。我们来看看三种不同类型的系统:
@@ -26,7 +27,7 @@ Web 和越来越多的基于 HTTP/REST 的 API 使交互的请求 / 响应风格
* 流处理系统(准实时系统)
流处理介于在线和离线(批处理)之间,所以有时候被称为 **准实时near-real-time****准在线nearline** 处理。像批处理系统一样,流处理消费输入并产生输出(并不需要响应请求)。但是,流式作业在事件发生后不久就会对事件进行操作,而批处理作业则需等待固定的一组输入数据。这种差异使流处理系统比起批处理系统具有更低的延迟。由于流处理基于批处理,我们将在 [第十一章](ch11.md) 讨论它。
流处理介于在线和离线(批处理)之间,所以有时候被称为 **准实时near-real-time****准在线nearline** 处理。像批处理系统一样,流处理消费输入并产生输出(并不需要响应请求)。但是,流式作业在事件发生后不久就会对事件进行操作,而批处理作业则需等待固定的一组输入数据。这种差异使流处理系统比起批处理系统具有更低的延迟。由于流处理基于批处理,我们将在 [第十一章](/ch11) 讨论它。
正如我们将在本章中看到的那样批处理是构建可靠、可伸缩和可维护应用程序的重要组成部分。例如2004 年发布的批处理算法 Map-Reduce可能被过分热情地被称为 “造就 Google 大规模可伸缩性的算法”【2】。随后在各种开源数据系统中得到应用包括 Hadoop、CouchDB 和 MongoDB。
@@ -122,7 +123,7 @@ Ruby 脚本在内存中保存了一个 URL 的哈希表,将每个 URL 映射
哪种方法更好?这取决于你有多少个不同的 URL。对于大多数中小型网站你可能可以为所有不同网址提供一个计数器假设我们使用 1GB 内存)。在此例中,作业的 **工作集**working set即作业需要随机访问的内存大小仅取决于不同 URL 的数量:如果日志中只有单个 URL重复出现一百万次则散列表所需的空间表就只有一个 URL 加上一个计数器的大小。当工作集足够小时,内存散列表表现良好,甚至在性能较差的笔记本电脑上也可以正常工作。
另一方面,如果作业的工作集大于可用内存,则排序方法的优点是可以高效地使用磁盘。这与我们在 “[SSTables 和 LSM 树](ch3.md#SSTables和LSM树)” 中讨论过的原理是一样的:数据块可以在内存中排序并作为段文件写入磁盘,然后多个排序好的段可以合并为一个更大的排序文件。归并排序具有在磁盘上运行良好的顺序访问模式。(请记住,针对顺序 I/O 进行优化是 [第三章](ch3.md) 中反复出现的主题,相同的模式在此重现)
另一方面,如果作业的工作集大于可用内存,则排序方法的优点是可以高效地使用磁盘。这与我们在 “[SSTables 和 LSM 树](/ch3#SSTables和LSM树)” 中讨论过的原理是一样的:数据块可以在内存中排序并作为段文件写入磁盘,然后多个排序好的段可以合并为一个更大的排序文件。归并排序具有在磁盘上运行良好的顺序访问模式。(请记住,针对顺序 I/O 进行优化是 [第三章](/ch3) 中反复出现的主题,相同的模式在此重现)
GNU CoreutilsLinux中的 `sort` 程序通过溢出至磁盘的方式来自动应对大于内存的数据集,并能同时使用多个 CPU 核进行并行排序【9】。这意味着我们之前看到的简单的 Unix 命令链很容易伸缩至大数据集,且不会耗尽内存。瓶颈可能是从磁盘读取输入文件的速度。
@@ -203,11 +204,11 @@ MapReduce 有点像 Unix 工具,但分布在数千台机器上。像 Unix 工
[^iv]: 一个不同之处在于,对于 HDFS可以将计算任务安排在存储特定文件副本的计算机上运行而对象存储通常将存储和计算分开。如果网络带宽是一个瓶颈从本地磁盘读取有性能优势。但是请注意如果使用纠删码Erasure Coding则会丢失局部性因为来自多台机器的数据必须进行合并以重建原始文件【20】。
与网络连接存储NAS和存储区域网络SAN架构的共享磁盘方法相比HDFS 基于 **无共享** 原则(请参阅 [第二部分](part-ii.md) 的介绍)。共享磁盘存储由集中式存储设备实现,通常使用定制硬件和专用网络基础设施(如光纤通道)。而另一方面,无共享方法不需要特殊的硬件,只需要通过传统数据中心网络连接的计算机。
与网络连接存储NAS和存储区域网络SAN架构的共享磁盘方法相比HDFS 基于 **无共享** 原则(请参阅 [第二部分](/part-ii) 的介绍)。共享磁盘存储由集中式存储设备实现,通常使用定制硬件和专用网络基础设施(如光纤通道)。而另一方面,无共享方法不需要特殊的硬件,只需要通过传统数据中心网络连接的计算机。
HDFS 在每台机器上运行了一个守护进程,它对外暴露网络服务,允许其他节点访问存储在该机器上的文件(假设数据中心中的每台通用计算机都挂载着一些磁盘)。名为 **NameNode** 的中央服务器会跟踪哪个文件块存储在哪台机器上。因此HDFS 在概念上创建了一个大型文件系统,可以使用所有运行有守护进程的机器的磁盘。
为了容忍机器和磁盘故障,文件块被复制到多台机器上。复制可能意味着多个机器上的相同数据的多个副本,如 [第五章](ch5.md) 中所述,或者诸如 Reed-Solomon 码这样的纠删码方案它能以比完全复制更低的存储开销来支持恢复丢失的数据【20,22】。这些技术与 RAID 相似,后者可以在连接到同一台机器的多个磁盘上提供冗余;区别在于在分布式文件系统中,文件访问和复制是在传统的数据中心网络上完成的,没有特殊的硬件。
为了容忍机器和磁盘故障,文件块被复制到多台机器上。复制可能意味着多个机器上的相同数据的多个副本,如 [第五章](/ch5) 中所述,或者诸如 Reed-Solomon 码这样的纠删码方案它能以比完全复制更低的存储开销来支持恢复丢失的数据【20,22】。这些技术与 RAID 相似,后者可以在连接到同一台机器的多个磁盘上提供冗余;区别在于在分布式文件系统中,文件访问和复制是在传统的数据中心网络上完成的,没有特殊的硬件。
HDFS 的可伸缩性已经很不错了:在撰写本书时,最大的 HDFS 部署运行在上万台机器上,总存储容量达数百 PB【23】。如此大的规模已经变得可行因为使用商品硬件和开源软件的 HDFS 上的数据存储和访问成本远低于在专用存储设备上支持同等容量的成本【24】。
@@ -222,7 +223,7 @@ MapReduce 是一个编程框架,你可以使用它编写代码来处理 HDFS
这四个步骤可以作为一个 MapReduce 作业执行。步骤 2Map和 4Reduce是你编写自定义数据处理代码的地方。步骤 1将文件分解成记录由输入格式解析器处理。步骤 3 中的排序步骤隐含在 MapReduce 中 —— 你不必编写它,因为 Mapper 的输出始终在送往 Reducer 之前进行排序。
要创建 MapReduce 作业你需要实现两个回调函数Mapper 和 Reducer其行为如下请参阅 “[MapReduce 查询](ch2.md#MapReduce查询)”):
要创建 MapReduce 作业你需要实现两个回调函数Mapper 和 Reducer其行为如下请参阅 “[MapReduce 查询](/ch2#MapReduce查询)”):
* Mapper
@@ -238,21 +239,21 @@ MapReduce 是一个编程框架,你可以使用它编写代码来处理 HDFS
MapReduce 与 Unix 命令管道的主要区别在于MapReduce 可以在多台机器上并行执行计算而无需编写代码来显式处理并行问题。Mapper 和 Reducer 一次只能处理一条记录;它们不需要知道它们的输入来自哪里,或者输出去往什么地方,所以框架可以处理在机器之间移动数据的复杂性。
在分布式计算中可以使用标准的 Unix 工具作为 Mapper 和 Reducer【25】但更常见的是它们被实现为传统编程语言的函数。在 Hadoop MapReduce 中Mapper 和 Reducer 都是实现特定接口的 Java 类。在 MongoDB 和 CouchDB 中Mapper 和 Reducer 都是 JavaScript 函数(请参阅 “[MapReduce 查询](ch2.md#MapReduce查询)”)。
在分布式计算中可以使用标准的 Unix 工具作为 Mapper 和 Reducer【25】但更常见的是它们被实现为传统编程语言的函数。在 Hadoop MapReduce 中Mapper 和 Reducer 都是实现特定接口的 Java 类。在 MongoDB 和 CouchDB 中Mapper 和 Reducer 都是 JavaScript 函数(请参阅 “[MapReduce 查询](/ch2#MapReduce查询)”)。
[图 10-1](img/fig10-1.png) 显示了 Hadoop MapReduce 作业中的数据流。其并行化基于分区(请参阅 [第六章](ch6.md)):作业的输入通常是 HDFS 中的一个目录,输入目录中的每个文件或文件块都被认为是一个单独的分区,可以单独处理 map 任务([图 10-1](img/fig10-1.png) 中的 m1m2 和 m3 标记)。
[图 10-1](/img/fig10-1.png) 显示了 Hadoop MapReduce 作业中的数据流。其并行化基于分区(请参阅 [第六章](/ch6)):作业的输入通常是 HDFS 中的一个目录,输入目录中的每个文件或文件块都被认为是一个单独的分区,可以单独处理 map 任务([图 10-1](/img/fig10-1.png) 中的 m1m2 和 m3 标记)。
每个输入文件的大小通常是数百兆字节。MapReduce 调度器(图中未显示)试图在其中一台存储输入文件副本的机器上运行每个 Mapper只要该机器有足够的备用 RAM 和 CPU 资源来运行 Mapper 任务【26】。这个原则被称为 **将计算放在数据附近**【27】它节省了通过网络复制输入文件的开销减少网络负载并增加局部性。
![](img/fig10-1.png)
![](/img/fig10-1.png)
**图 10-1 具有三个 Mapper 和三个 Reducer 的 MapReduce 任务**
在大多数情况下,应该在 Mapper 任务中运行的应用代码在将要运行它的机器上还不存在,所以 MapReduce 框架首先将代码(例如 Java 程序中的 JAR 文件)复制到适当的机器。然后启动 Map 任务并开始读取输入文件,一次将一条记录传入 Mapper 回调函数。Mapper 的输出由键值对组成。
计算的 Reduce 端也被分区。虽然 Map 任务的数量由输入文件块的数量决定,但 Reducer 的任务的数量是由作业作者配置的(它可以不同于 Map 任务的数量)。为了确保具有相同键的所有键值对最终落在相同的 Reducer 处,框架使用键的散列值来确定哪个 Reduce 任务应该接收到特定的键值对(请参阅 “[根据键的散列分区](ch6.md#根据键的散列分区)”)。
计算的 Reduce 端也被分区。虽然 Map 任务的数量由输入文件块的数量决定,但 Reducer 的任务的数量是由作业作者配置的(它可以不同于 Map 任务的数量)。为了确保具有相同键的所有键值对最终落在相同的 Reducer 处,框架使用键的散列值来确定哪个 Reduce 任务应该接收到特定的键值对(请参阅 “[根据键的散列分区](/ch6#根据键的散列分区)”)。
键值对必须进行排序,但数据集可能太大,无法在单台机器上使用常规排序算法进行排序。相反,分类是分阶段进行的。首先每个 Map 任务都按照 Reducer 对输出进行分区。每个分区都被写入 Mapper 程序的本地磁盘,使用的技术与我们在 “[SSTables 与 LSM 树](ch3.md#SSTables和LSM树)” 中讨论的类似。
键值对必须进行排序,但数据集可能太大,无法在单台机器上使用常规排序算法进行排序。相反,分类是分阶段进行的。首先每个 Map 任务都按照 Reducer 对输出进行分区。每个分区都被写入 Mapper 程序的本地磁盘,使用的技术与我们在 “[SSTables 与 LSM 树](/ch3#SSTables和LSM树)” 中讨论的类似。
只要当 Mapper 读取完输入文件并写完排序后的输出文件MapReduce 调度器就会通知 Reducer 可以从该 Mapper 开始获取输出文件。Reducer 连接到每个 Mapper并下载自己相应分区的有序键值对文件。按 Reducer 分区,排序,从 Mapper 向 Reducer 复制分区数据,这一整个过程被称为 **混洗shuffle**【26】一个容易混淆的术语 —— 不像洗牌,在 MapReduce 中的混洗没有随机性)。
@@ -276,23 +277,23 @@ Hadoop 的各种高级工具(如 Pig 【30】、Hive 【31】、Cascading 【3
### Reduce侧连接与分组
我们在 [第二章](ch2.md) 中讨论了数据模型和查询语言的连接,但是我们还没有深入探讨连接是如何实现的。现在是我们再次捡起这条线索的时候了。
我们在 [第二章](/ch2) 中讨论了数据模型和查询语言的连接,但是我们还没有深入探讨连接是如何实现的。现在是我们再次捡起这条线索的时候了。
在许多数据集中,一条记录与另一条记录存在关联是很常见的:关系模型中的 **外键**,文档模型中的 **文档引用** 或图模型中的 **边**。当你需要同时访问这一关联的两侧(持有引用的记录与被引用的记录)时,连接就是必须的。正如 [第二章](ch2.md) 所讨论的,非规范化可以减少对连接的需求,但通常无法将其完全移除 [^v]。
在许多数据集中,一条记录与另一条记录存在关联是很常见的:关系模型中的 **外键**,文档模型中的 **文档引用** 或图模型中的 **边**。当你需要同时访问这一关联的两侧(持有引用的记录与被引用的记录)时,连接就是必须的。正如 [第二章](/ch2) 所讨论的,非规范化可以减少对连接的需求,但通常无法将其完全移除 [^v]。
[^v]: 我们在本书中讨论的连接通常是等值连接,即最常见的连接类型,其中记录通过与其他记录在特定字段(例如 ID中具有 **相同值** 相关联。有些数据库支持更通用的连接类型,例如使用小于运算符而不是等号运算符,但是我们没有地方来讲这些东西。
在数据库中,如果执行只涉及少量记录的查询,数据库通常会使用 **索引** 来快速定位感兴趣的记录(请参阅 [第三章](ch3.md))。如果查询涉及到连接,则可能涉及到查找多个索引。然而 MapReduce 没有索引的概念 —— 至少在通常意义上没有。
在数据库中,如果执行只涉及少量记录的查询,数据库通常会使用 **索引** 来快速定位感兴趣的记录(请参阅 [第三章](/ch3))。如果查询涉及到连接,则可能涉及到查找多个索引。然而 MapReduce 没有索引的概念 —— 至少在通常意义上没有。
当 MapReduce 作业被赋予一组文件作为输入时,它读取所有这些文件的全部内容;数据库会将这种操作称为 **全表扫描**。如果你只想读取少量的记录,则全表扫描与索引查询相比,代价非常高昂。但是在分析查询中(请参阅 “[事务处理还是分析?](ch3.md#事务处理还是分析?)”),通常需要计算大量记录的聚合。在这种情况下,特别是如果能在多台机器上并行处理时,扫描整个输入可能是相当合理的事情。
当 MapReduce 作业被赋予一组文件作为输入时,它读取所有这些文件的全部内容;数据库会将这种操作称为 **全表扫描**。如果你只想读取少量的记录,则全表扫描与索引查询相比,代价非常高昂。但是在分析查询中(请参阅 “[事务处理还是分析?](/ch3#事务处理还是分析?)”),通常需要计算大量记录的聚合。在这种情况下,特别是如果能在多台机器上并行处理时,扫描整个输入可能是相当合理的事情。
当我们在批处理的语境中讨论连接时,我们指的是在数据集中解析某种关联的全量存在。例如我们假设一个作业是同时处理所有用户的数据,而非仅仅是为某个特定用户查找数据(而这能通过索引更高效地完成)。
#### 示例:用户活动事件分析
[图 10-2](img/fig10-2.png) 给出了一个批处理作业中连接的典型例子。左侧是事件日志,描述登录用户在网站上做的事情(称为 **活动事件**,即 activity events**点击流数据**,即 clickstream data右侧是用户数据库。你可以将此示例看作是星型模式的一部分请参阅 “[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”):事件日志是事实表,用户数据库是其中的一个维度。
[图 10-2](/img/fig10-2.png) 给出了一个批处理作业中连接的典型例子。左侧是事件日志,描述登录用户在网站上做的事情(称为 **活动事件**,即 activity events**点击流数据**,即 clickstream data右侧是用户数据库。你可以将此示例看作是星型模式的一部分请参阅 “[星型和雪花型:分析的模式](/ch3#星型和雪花型:分析的模式)”):事件日志是事实表,用户数据库是其中的一个维度。
![](img/fig10-2.png)
![](/img/fig10-2.png)
**图 10-2 用户行为日志与用户档案的连接**
@@ -302,13 +303,13 @@ Hadoop 的各种高级工具(如 Pig 【30】、Hive 【31】、Cascading 【3
为了在批处理过程中实现良好的吞吐量,计算必须(尽可能)限于单台机器上进行。为待处理的每条记录发起随机访问的网络请求实在是太慢了。而且,查询远程数据库意味着批处理作业变为 **非确定的nondeterministic**,因为远程数据库中的数据可能会改变。
因此,更好的方法是获取用户数据库的副本(例如,使用 ETL 进程从数据库备份中提取数据,请参阅 “[数据仓库](ch3.md#数据仓库)”),并将它和用户行为日志放入同一个分布式文件系统中。然后你可以将用户数据库存储在 HDFS 中的一组文件中,而用户活动记录存储在另一组文件中,并能用 MapReduce 将所有相关记录集中到同一个地方进行高效处理。
因此,更好的方法是获取用户数据库的副本(例如,使用 ETL 进程从数据库备份中提取数据,请参阅 “[数据仓库](/ch3#数据仓库)”),并将它和用户行为日志放入同一个分布式文件系统中。然后你可以将用户数据库存储在 HDFS 中的一组文件中,而用户活动记录存储在另一组文件中,并能用 MapReduce 将所有相关记录集中到同一个地方进行高效处理。
#### 排序合并连接
回想一下Mapper 的目的是从每个输入记录中提取一对键值。在 [图 10-2](img/fig10-2.png) 的情况下,这个键就是用户 ID一组 Mapper 会扫过活动事件(提取用户 ID 作为键,活动事件作为值),而另一组 Mapper 将会扫过用户数据库(提取用户 ID 作为键,用户的出生日期作为值)。这个过程如 [图 10-3](img/fig10-3.png) 所示。
回想一下Mapper 的目的是从每个输入记录中提取一对键值。在 [图 10-2](/img/fig10-2.png) 的情况下,这个键就是用户 ID一组 Mapper 会扫过活动事件(提取用户 ID 作为键,活动事件作为值),而另一组 Mapper 将会扫过用户数据库(提取用户 ID 作为键,用户的出生日期作为值)。这个过程如 [图 10-3](/img/fig10-3.png) 所示。
![](img/fig10-3.png)
![](/img/fig10-3.png)
**图 10-3 在用户 ID 上进行的 Reduce 端连接。如果输入数据集分区为多个文件,则每个分区都会被多个 Mapper 并行处理**
@@ -344,11 +345,11 @@ Hadoop 的各种高级工具(如 Pig 【30】、Hive 【31】、Cascading 【3
如果存在与单个键关联的大量数据,则 “将具有相同键的所有记录放到相同的位置” 这种模式就被破坏了。例如在社交网络中,大多数用户可能会与几百人有连接,但少数名人可能有数百万的追随者。这种不成比例的活动数据库记录被称为 **关键对象linchpin object**【38】或 **热键hot key**
在单个 Reducer 中收集与某个名人相关的所有活动(例如他们发布内容的回复)可能导致严重的 **偏斜**(也称为 **热点**,即 hot spot—— 也就是说,一个 Reducer 必须比其他 Reducer 处理更多的记录(请参阅 “[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)”)。由于 MapReduce 作业只有在所有 Mapper 和 Reducer 都完成时才完成,所有后续作业必须等待最慢的 Reducer 才能启动。
在单个 Reducer 中收集与某个名人相关的所有活动(例如他们发布内容的回复)可能导致严重的 **偏斜**(也称为 **热点**,即 hot spot—— 也就是说,一个 Reducer 必须比其他 Reducer 处理更多的记录(请参阅 “[负载偏斜与热点消除](/ch6#负载偏斜与热点消除)”)。由于 MapReduce 作业只有在所有 Mapper 和 Reducer 都完成时才完成,所有后续作业必须等待最慢的 Reducer 才能启动。
如果连接的输入存在热键可以使用一些算法进行补偿。例如Pig 中的 **偏斜连接skewed join** 方法首先运行一个抽样作业Sampling Job来确定哪些键是热键【39】。连接实际执行时Mapper 会将热键的关联记录 **随机**(相对于传统 MapReduce 基于键散列的确定性方法)发送到几个 Reducer 之一。对于另外一侧的连接输入,与热键相关的记录需要被复制到 **所有** 处理该键的 Reducer 上【40】。
这种技术将处理热键的工作分散到多个 Reducer 上,这样可以使其更好地并行化,代价是需要将连接另一侧的输入记录复制到多个 Reducer 上。Crunch 中的 **分片连接sharded join** 方法与之类似,但需要显式指定热键而不是使用抽样作业。这种技术也非常类似于我们在 “[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)” 中讨论的技术,使用随机化来缓解分区数据库中的热点。
这种技术将处理热键的工作分散到多个 Reducer 上,这样可以使其更好地并行化,代价是需要将连接另一侧的输入记录复制到多个 Reducer 上。Crunch 中的 **分片连接sharded join** 方法与之类似,但需要显式指定热键而不是使用抽样作业。这种技术也非常类似于我们在 “[负载偏斜与热点消除](/ch6#负载偏斜与热点消除)” 中讨论的技术,使用随机化来缓解分区数据库中的热点。
Hive 的偏斜连接优化采取了另一种方法。它需要在表格元数据中显式指定热键,并将与这些键相关的记录单独存放,与其它文件分开。当在该表上执行连接时,对于热键,它会使用 Map 端连接(请参阅下一节)。
@@ -367,11 +368,11 @@ Reduce 侧方法的优点是不需要对输入数据做任何假设:无论其
适用于执行 Map 端连接的最简单场景是大数据集与小数据集连接的情况。要点在于小数据集需要足够小,以便可以将其全部加载到每个 Mapper 的内存中。
例如,假设在 [图 10-2](img/fig10-2.png) 的情况下,用户数据库小到足以放进内存中。在这种情况下,当 Mapper 启动时它可以首先将用户数据库从分布式文件系统读取到内存中的散列表中。完成此操作后Mapper 可以扫描用户活动事件,并简单地在散列表中查找每个事件的用户 ID [^vi]。
例如,假设在 [图 10-2](/img/fig10-2.png) 的情况下,用户数据库小到足以放进内存中。在这种情况下,当 Mapper 启动时它可以首先将用户数据库从分布式文件系统读取到内存中的散列表中。完成此操作后Mapper 可以扫描用户活动事件,并简单地在散列表中查找每个事件的用户 ID [^vi]。
[^vi]: 这个例子假定散列表中的每个键只有一个条目,这对用户数据库(用户 ID 唯一标识一个用户)可能是正确的。通常,哈希表可能需要包含具有相同键的多个条目,而连接运算符将对每个键输出所有的匹配。
参与连接的较大输入的每个文件块各有一个 Mapper在 [图 10-2](img/fig10-2.png) 的例子中活动事件是较大的输入)。每个 Mapper 都会将较小输入整个加载到内存中。
参与连接的较大输入的每个文件块各有一个 Mapper在 [图 10-2](/img/fig10-2.png) 的例子中活动事件是较大的输入)。每个 Mapper 都会将较小输入整个加载到内存中。
这种简单有效的算法被称为 **广播散列连接broadcast hash join****广播** 一词反映了这样一个事实,每个连接较大输入端分区的 Mapper 都会将较小输入端数据集整个读入内存中(所以较小输入实际上 “广播” 到较大数据的所有分区上),**散列** 一词反映了它使用一个散列表。Pig名为 “**复制链接replicated join**”Hive“**MapJoin**”Cascading 和 Crunch 支持这种连接。它也被诸如 Impala 的数据仓库查询引擎使用【41】。
@@ -379,7 +380,7 @@ Reduce 侧方法的优点是不需要对输入数据做任何假设:无论其
#### 分区散列连接
如果 Map 侧连接的输入以相同的方式进行分区,则散列连接方法可以独立应用于每个分区。在 [图 10-2](img/fig10-2.png) 的情况中,你可以根据用户 ID 的最后一位十进制数字来对活动事件和用户数据库进行分区(因此连接两侧各有 10 个分区。例如Mapper3 首先将所有具有以 3 结尾的 ID 的用户加载到散列表中,然后扫描 ID 为 3 的每个用户的所有活动事件。
如果 Map 侧连接的输入以相同的方式进行分区,则散列连接方法可以独立应用于每个分区。在 [图 10-2](/img/fig10-2.png) 的情况中,你可以根据用户 ID 的最后一位十进制数字来对活动事件和用户数据库进行分区(因此连接两侧各有 10 个分区。例如Mapper3 首先将所有具有以 3 结尾的 ID 的用户加载到散列表中,然后扫描 ID 为 3 的每个用户的所有活动事件。
如果分区正确无误,可以确定的是,所有你可能需要连接的记录都落在同一个编号的分区中。因此每个 Mapper 只需要从输入两端各读取一个分区就足够了。好处是每个 Mapper 都可以在内存散列表中少放点数据。
@@ -406,7 +407,7 @@ Reduce 侧方法的优点是不需要对输入数据做任何假设:无论其
我们已经说了很多用于实现 MapReduce 工作流的算法,但却忽略了一个重要的问题:这些处理完成之后的最终结果是什么?我们最开始为什么要跑这些作业?
在数据库查询的场景中我们将事务处理OLTP与分析两种目的区分开来请参阅 “[事务处理还是分析?](ch3.md#事务处理还是分析?)”。我们看到OLTP 查询通常根据键查找少量记录,使用索引,并将其呈现给用户(比如在网页上)。另一方面,分析查询通常会扫描大量记录,执行分组与聚合,输出通常有着报告的形式:显示某个指标随时间变化的图表,或按照某种排位取前 10 项,或将一些数字细化为子类。这种报告的消费者通常是需要做出商业决策的分析师或经理。
在数据库查询的场景中我们将事务处理OLTP与分析两种目的区分开来请参阅 “[事务处理还是分析?](/ch3#事务处理还是分析?)”。我们看到OLTP 查询通常根据键查找少量记录,使用索引,并将其呈现给用户(比如在网页上)。另一方面,分析查询通常会扫描大量记录,执行分组与聚合,输出通常有着报告的形式:显示某个指标随时间变化的图表,或按照某种排位取前 10 项,或将一些数字细化为子类。这种报告的消费者通常是需要做出商业决策的分析师或经理。
批处理放哪里合适?它不属于事务处理,也不是分析。它和分析比较接近,因为批处理通常会扫过输入数据集的绝大部分。然而 MapReduce 作业工作流与用于分析目的的 SQL 查询是不同的(请参阅 “[Hadoop 与分布式数据库的对比](#Hadoop与分布式数据库的对比)”)。批处理过程的输出通常不是报表,而是一些其他类型的结构。
@@ -414,15 +415,15 @@ Reduce 侧方法的优点是不需要对输入数据做任何假设:无论其
Google 最初使用 MapReduce 是为其搜索引擎建立索引,其实现为由 5 到 10 个 MapReduce 作业组成的工作流【1】。虽然 Google 后来也不仅仅是为这个目的而使用 MapReduce 【43】但如果从构建搜索索引的角度来看更能帮助理解 MapReduce。直至今日Hadoop MapReduce 仍然是为 Lucene/Solr 构建索引的好方法【44】
我们在 “[全文搜索和模糊索引](ch3.md#全文搜索和模糊索引)” 中简要地了解了 Lucene 这样的全文搜索索引是如何工作的:它是一个文件(关键词字典),你可以在其中高效地查找特定关键字,并找到包含该关键字的所有文档 ID 列表(文章列表)。这是一种非常简化的看法 —— 实际上,搜索索引需要各种额外数据,以便根据相关性对搜索结果进行排名、纠正拼写错误、解析同义词等等 —— 但这个原则是成立的。
我们在 “[全文搜索和模糊索引](/ch3#全文搜索和模糊索引)” 中简要地了解了 Lucene 这样的全文搜索索引是如何工作的:它是一个文件(关键词字典),你可以在其中高效地查找特定关键字,并找到包含该关键字的所有文档 ID 列表(文章列表)。这是一种非常简化的看法 —— 实际上,搜索索引需要各种额外数据,以便根据相关性对搜索结果进行排名、纠正拼写错误、解析同义词等等 —— 但这个原则是成立的。
如果需要对一组固定文档执行全文搜索则批处理是一种构建索引的高效方法Mapper 根据需要对文档集合进行分区,每个 Reducer 构建该分区的索引,并将索引文件写入分布式文件系统。构建这样的文档分区索引(请参阅 “[分区与次级索引](ch6.md#分区与次级索引)”)并行处理效果拔群。
如果需要对一组固定文档执行全文搜索则批处理是一种构建索引的高效方法Mapper 根据需要对文档集合进行分区,每个 Reducer 构建该分区的索引,并将索引文件写入分布式文件系统。构建这样的文档分区索引(请参阅 “[分区与次级索引](/ch6#分区与次级索引)”)并行处理效果拔群。
由于按关键字查询搜索索引是只读操作,因而这些索引文件一旦创建就是不可变的。
如果索引的文档集合发生更改,一种选择是定期重跑整个索引工作流,并在完成后用新的索引文件批量替换以前的索引文件。如果只有少量的文档发生了变化,这种方法的计算成本可能会很高。但它的优点是索引过程很容易理解:文档进,索引出。
另一个选择是,可以增量建立索引。如 [第三章](ch3.md) 中讨论的如果要在索引中添加删除或更新文档Lucene 会写新的段文件,并在后台异步合并压缩段文件。我们将在 [第十一章](ch11.md) 中看到更多这种增量处理。
另一个选择是,可以增量建立索引。如 [第三章](/ch3) 中讨论的如果要在索引中添加删除或更新文档Lucene 会写新的段文件,并在后台异步合并压缩段文件。我们将在 [第十一章](/ch11) 中看到更多这种增量处理。
#### 键值存储作为批处理输出
@@ -440,7 +441,7 @@ Google 最初使用 MapReduce 是为其搜索引擎建立索引,其实现为
更好的解决方案是在批处理作业 **内** 创建一个全新的数据库,并将其作为文件写入分布式文件系统中作业的输出目录,就像上节中的搜索索引一样。这些数据文件一旦写入就是不可变的,可以批量加载到处理只读查询的服务器中。不少键值存储都支持在 MapReduce 作业中构建数据库文件,包括 Voldemort 【46】、Terrapin 【47】、ElephantDB 【48】和 HBase 批量加载【49】。
构建这些数据库文件是 MapReduce 的一种好用法:使用 Mapper 提取出键并按该键排序已经完成了构建索引所必需的大量工作。由于这些键值存储大多都是只读的文件只能由批处理作业一次性写入然后就不可变所以数据结构非常简单。比如它们就不需要预写式日志WAL请参阅 “[让 B 树更可靠](ch3.md#让B树更可靠)”)。
构建这些数据库文件是 MapReduce 的一种好用法:使用 Mapper 提取出键并按该键排序已经完成了构建索引所必需的大量工作。由于这些键值存储大多都是只读的文件只能由批处理作业一次性写入然后就不可变所以数据结构非常简单。比如它们就不需要预写式日志WAL请参阅 “[让 B 树更可靠](/ch3#让B树更可靠)”)。
将数据加载到 Voldemort 时服务器将继续用旧数据文件服务请求同时将新数据文件从分布式文件系统复制到服务器的本地磁盘。一旦复制完成服务器会自动将查询切换到新文件。如果在这个过程中出现任何问题它可以轻易回滚至旧文件因为它们仍然存在而且不可变【46】。
@@ -456,7 +457,7 @@ MapReduce 作业的输出处理遵循同样的原理。通过将输入视为不
- 同一组文件可用作各种不同作业的输入,包括计算指标的监控作业并且评估作业的输出是否具有预期的性质(例如,将其与前一次运行的输出进行比较并测量差异) 。
- 与 Unix 工具类似MapReduce 作业将逻辑与布线(配置输入和输出目录)分离,这使得关注点分离,可以重用代码:一个团队可以专注实现一个做好一件事的作业;而其他团队可以决定何时何地运行这项作业。
在这些领域,在 Unix 上表现良好的设计原则似乎也适用于 Hadoop但 Unix 和 Hadoop 在某些方面也有所不同。例如,因为大多数 Unix 工具都假设输入输出是无类型文本文件,所以它们必须做大量的输入解析工作(本章开头的日志分析示例使用 `{print $7}` 来提取 URL。在 Hadoop 上可以通过使用更结构化的文件格式消除一些低价值的语法转换:比如 Avro请参阅 “[Avro](ch4.md#Avro)”)和 Parquet请参阅 “[列式存储](ch3.md#列式存储)”)经常使用,因为它们提供了基于模式的高效编码,并允许模式随时间推移而演进(见 [第四章](ch4.md))。
在这些领域,在 Unix 上表现良好的设计原则似乎也适用于 Hadoop但 Unix 和 Hadoop 在某些方面也有所不同。例如,因为大多数 Unix 工具都假设输入输出是无类型文本文件,所以它们必须做大量的输入解析工作(本章开头的日志分析示例使用 `{print $7}` 来提取 URL。在 Hadoop 上可以通过使用更结构化的文件格式消除一些低价值的语法转换:比如 Avro请参阅 “[Avro](/ch4#Avro)”)和 Parquet请参阅 “[列式存储](/ch3#列式存储)”)经常使用,因为它们提供了基于模式的高效编码,并允许模式随时间推移而演进(见 [第四章](/ch4))。
### Hadoop与分布式数据库的对比
@@ -474,11 +475,11 @@ MapReduce 作业的输出处理遵循同样的原理。通过将输入视为不
在纯粹主义者看来,这种仔细的建模和导入似乎是可取的,因为这意味着数据库的用户有更高质量的数据来处理。然而实践经验表明,简单地使数据快速可用 —— 即使它很古怪,难以使用,使用原始格式 —— 也通常要比事先决定理想数据模型要更有价值【54】。
这个想法与数据仓库类似(请参阅 “[数据仓库](ch3.md#数据仓库)”将大型组织的各个部分的数据集中在一起是很有价值的因为它可以跨越以前相互分离的数据集进行连接。MPP 数据库所要求的谨慎模式设计拖慢了集中式数据收集速度;以原始形式收集数据,稍后再操心模式的设计,能使数据收集速度加快(有时被称为 “**数据湖data lake**” 或 “**企业数据中心enterprise data hub**”【55】
这个想法与数据仓库类似(请参阅 “[数据仓库](/ch3#数据仓库)”将大型组织的各个部分的数据集中在一起是很有价值的因为它可以跨越以前相互分离的数据集进行连接。MPP 数据库所要求的谨慎模式设计拖慢了集中式数据收集速度;以原始形式收集数据,稍后再操心模式的设计,能使数据收集速度加快(有时被称为 “**数据湖data lake**” 或 “**企业数据中心enterprise data hub**”【55】
不加区分的数据转储转移了解释数据的负担:数据集的生产者不再需要强制将其转化为标准格式,数据的解释成为消费者的问题(**读时模式** 方法【56】请参阅 “[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)”)。如果生产者和消费者是不同优先级的不同团队,这可能是一种优势。甚至可能不存在一个理想的数据模型,对于不同目的有不同的合适视角。以原始形式简单地转储数据,可以允许多种这样的转换。这种方法被称为 **寿司原则sushi principle**“原始数据更好”【57】。
不加区分的数据转储转移了解释数据的负担:数据集的生产者不再需要强制将其转化为标准格式,数据的解释成为消费者的问题(**读时模式** 方法【56】请参阅 “[文档模型中的模式灵活性](/ch2#文档模型中的模式灵活性)”)。如果生产者和消费者是不同优先级的不同团队,这可能是一种优势。甚至可能不存在一个理想的数据模型,对于不同目的有不同的合适视角。以原始形式简单地转储数据,可以允许多种这样的转换。这种方法被称为 **寿司原则sushi principle**“原始数据更好”【57】。
因此Hadoop 经常被用于实现 ETL 过程(请参阅 “[数据仓库](ch3.md#数据仓库)”):事务处理系统中的数据以某种原始形式转储到分布式文件系统中,然后编写 MapReduce 作业来清理数据,将其转换为关系形式,并将其导入 MPP 数据仓库以进行分析。数据建模仍然在进行,但它在一个单独的步骤中进行,与数据收集相解耦。这种解耦是可行的,因为分布式文件系统支持以任何格式编码的数据。
因此Hadoop 经常被用于实现 ETL 过程(请参阅 “[数据仓库](/ch3#数据仓库)”):事务处理系统中的数据以某种原始形式转储到分布式文件系统中,然后编写 MapReduce 作业来清理数据,将其转换为关系形式,并将其导入 MPP 数据仓库以进行分析。数据建模仍然在进行,但它在一个单独的步骤中进行,与数据收集相解耦。这种解耦是可行的,因为分布式文件系统支持以任何格式编码的数据。
#### 处理模型的多样性
@@ -492,7 +493,7 @@ MapReduce 使工程师能够轻松地在大型数据集上运行自己的代码
至关重要的是,这些不同的处理模型都可以在共享的单个机器集群上运行,所有这些机器都可以访问分布式文件系统上的相同文件。在 Hadoop 方式中,不需要将数据导入到几个不同的专用系统中进行不同类型的处理:系统足够灵活,可以支持同一个集群内不同的工作负载。不需要移动数据,使得从数据中挖掘价值变得容易得多,也使采用新的处理模型容易的多。
Hadoop 生态系统包括随机访问的 OLTP 数据库,如 HBase请参阅 “[SSTables 和 LSM 树](ch3.md#SSTables和LSM树)”)和 MPP 风格的分析型数据库,如 Impala 【41】。HBase 与 Impala 都不使用 MapReduce但都使用 HDFS 进行存储。它们是迥异的数据访问与处理方法,但是它们可以共存,并被集成到同一个系统中。
Hadoop 生态系统包括随机访问的 OLTP 数据库,如 HBase请参阅 “[SSTables 和 LSM 树](/ch3#SSTables和LSM树)”)和 MPP 风格的分析型数据库,如 Impala 【41】。HBase 与 Impala 都不使用 MapReduce但都使用 HDFS 进行存储。它们是迥异的数据访问与处理方法,但是它们可以共存,并被集成到同一个系统中。
#### 针对频繁故障设计
@@ -528,7 +529,7 @@ MapReduce 方式更适用于较大的作业:要处理如此之多的数据并
但是MapReduce 执行模型本身也存在一些问题这些问题并没有通过增加另一个抽象层次而解决而对于某些类型的处理它表现得非常差劲。一方面MapReduce 非常稳健:你可以使用它在任务会频繁终止的多租户系统上处理几乎任意大量级的数据,并且仍然可以完成工作(虽然速度很慢)。另一方面,对于某些类型的处理而言,其他工具有时会快上几个数量级。
在本章的其余部分中,我们将介绍一些批处理方法。在 [第十一章](ch11.md) 我们将转向流处理,它可以看作是加速批处理的另一种方法。
在本章的其余部分中,我们将介绍一些批处理方法。在 [第十一章](/ch11) 我们将转向流处理,它可以看作是加速批处理的另一种方法。
### 物化中间状态
@@ -538,7 +539,7 @@ MapReduce 方式更适用于较大的作业:要处理如此之多的数据并
但在很多情况下,你知道一个作业的输出只能用作另一个作业的输入,这些作业由同一个团队维护。在这种情况下,分布式文件系统上的文件只是简单的 **中间状态intermediate state**:一种将数据从一个作业传递到下一个作业的方式。在一个用于构建推荐系统的,由 50 或 100 个 MapReduce 作业组成的复杂工作流中存在着很多这样的中间状态【29】。
将这个中间状态写入文件的过程称为 **物化materialization**。(在 “[聚合:数据立方体和物化视图](ch3.md#聚合:数据立方体和物化视图)” 中已经在物化视图的背景中遇到过这个术语。它意味着对某个操作的结果立即求值并写出来,而不是在请求时按需计算)
将这个中间状态写入文件的过程称为 **物化materialization**。(在 “[聚合:数据立方体和物化视图](/ch3#聚合:数据立方体和物化视图)” 中已经在物化视图的背景中遇到过这个术语。它意味着对某个操作的结果立即求值并写出来,而不是在请求时按需计算)
作为对照,本章开头的日志分析示例使用 Unix 管道将一个命令的输出与另一个命令的输入连接起来。管道并没有完全物化中间状态,而是只使用一个小的内存缓冲区,将输出增量地 **流stream** 向输入。
@@ -597,13 +598,13 @@ Spark、Flink 和 Tez 避免将中间状态写入 HDFS因此它们采取了
### 图与迭代处理
在 “[图数据模型](ch2.md#图数据模型)” 中,我们讨论了使用图来建模数据,并使用图查询语言来遍历图中的边与点。[第二章](ch2.md) 的讨论集中在 OLTP 风格的应用场景:快速执行查询来查找少量符合特定条件的顶点。
在 “[图数据模型](/ch2#图数据模型)” 中,我们讨论了使用图来建模数据,并使用图查询语言来遍历图中的边与点。[第二章](/ch2) 的讨论集中在 OLTP 风格的应用场景:快速执行查询来查找少量符合特定条件的顶点。
批处理上下文中的图也很有趣,其目标是在整个图上执行某种离线处理或分析。这种需求经常出现在机器学习应用(如推荐引擎)或排序系统中。例如,最着名的图形分析算法之一是 PageRank 【69】它试图根据链接到某个网页的其他网页来估计该网页的流行度。它作为配方的一部分用于确定网络搜索引擎呈现结果的顺序。
> 像 Spark、Flink 和 Tez 这样的数据流引擎(请参阅 “[物化中间状态](#物化中间状态)”)通常将算子作为 **有向无环图DAG** 的一部分安排在作业中。这与图处理不一样:在数据流引擎中,**从一个算子到另一个算子的数据流** 被构造成一个图,而数据本身通常由关系型元组构成。在图处理中,数据本身具有图的形式。又一个不幸的命名混乱!
许多图算法是通过一次遍历一条边来表示的,将一个顶点与近邻的顶点连接起来,以传播一些信息,并不断重复,直到满足一些条件为止 —— 例如,直到没有更多的边要跟进,或直到一些指标收敛。我们在 [图 2-6](img/fig2-6.png) 中看到一个例子,它通过重复跟进标明地点归属关系的边,生成了数据库中北美包含的所有地点列表(这种算法被称为 **传递闭包**,即 transitive closure
许多图算法是通过一次遍历一条边来表示的,将一个顶点与近邻的顶点连接起来,以传播一些信息,并不断重复,直到满足一些条件为止 —— 例如,直到没有更多的边要跟进,或直到一些指标收敛。我们在 [图 2-6](/img/fig2-6.png) 中看到一个例子,它通过重复跟进标明地点归属关系的边,生成了数据库中北美包含的所有地点列表(这种算法被称为 **传递闭包**,即 transitive closure
可以在分布式文件系统中存储图(包含顶点和边的列表的文件),但是这种 “重复至完成” 的想法不能用普通的 MapReduce 来表示,因为它只扫过一趟数据。这种算法因此经常以 **迭代** 的风格实现:
@@ -621,13 +622,13 @@ Spark、Flink 和 Tez 避免将中间状态写入 HDFS因此它们采取了
在每次迭代中,为每个顶点调用一个函数,将所有发送给它的消息传递给它 —— 就像调用 Reducer 一样。与 MapReduce 的不同之处在于,在 Pregel 模型中,顶点在一次迭代到下一次迭代的过程中会记住它的状态,所以这个函数只需要处理新的传入消息。如果图的某个部分没有被发送消息,那里就不需要做任何工作。
这与 Actor 模型有些相似(请参阅 “[分布式的 Actor 框架](ch4.md#分布式的Actor框架)”除了顶点状态和顶点之间的消息具有容错性和持久性且通信以固定的回合进行在每次迭代中框架递送上次迭代中发送的所有消息。Actor 通常没有这样的时序保证。
这与 Actor 模型有些相似(请参阅 “[分布式的 Actor 框架](/ch4#分布式的Actor框架)”除了顶点状态和顶点之间的消息具有容错性和持久性且通信以固定的回合进行在每次迭代中框架递送上次迭代中发送的所有消息。Actor 通常没有这样的时序保证。
#### 容错
顶点只能通过消息传递进行通信(而不是直接相互查询)的事实有助于提高 Pregel 作业的性能,因为消息可以成批处理,且等待通信的次数也减少了。唯一的等待是在迭代之间:由于 Pregel 模型保证所有在一轮迭代中发送的消息都在下轮迭代中送达,所以在下一轮迭代开始前,先前的迭代必须完全完成,而所有的消息必须在网络上完成复制。
即使底层网络可能丢失、重复或任意延迟消息(请参阅 “[不可靠的网络](ch8.md#不可靠的网络)”Pregel 的实现能保证在后续迭代中消息在其目标顶点恰好处理一次。像 MapReduce 一样,框架能从故障中透明地恢复,以简化在 Pregel 上实现算法的编程模型。
即使底层网络可能丢失、重复或任意延迟消息(请参阅 “[不可靠的网络](/ch8#不可靠的网络)”Pregel 的实现能保证在后续迭代中消息在其目标顶点恰好处理一次。像 MapReduce 一样,框架能从故障中透明地恢复,以简化在 Pregel 上实现算法的编程模型。
这种容错是通过在迭代结束时定期存档所有顶点的状态来实现的即将其全部状态写入持久化存储。如果某个节点发生故障并且其内存中的状态丢失则最简单的解决方法是将整个图计算回滚到上一个存档点然后重启计算。如果算法是确定性的且消息记录在日志中那么也可以选择性地只恢复丢失的分区就像之前讨论过的数据流引擎【72】。
@@ -658,13 +659,13 @@ Spark、Flink 和 Tez 避免将中间状态写入 HDFS因此它们采取了
与硬写执行连接的代码相比指定连接关系算子的优点是框架可以分析连接输入的属性并自动决定哪种上述连接算法最适合当前任务。Hive、Spark 和 Flink 都有基于代价的查询优化器可以做到这一点甚至可以改变连接顺序最小化中间状态的数量【66,77,78,79】。
连接算法的选择可以对批处理作业的性能产生巨大影响,而无需理解和记住本章中讨论的各种连接算法。如果连接是以 **声明式declarative** 的方式指定的,那这就这是可行的:应用只是简单地说明哪些连接是必需的,查询优化器决定如何最好地执行连接。我们以前在 “[数据查询语言](ch2.md#数据查询语言)” 中见过这个想法。
连接算法的选择可以对批处理作业的性能产生巨大影响,而无需理解和记住本章中讨论的各种连接算法。如果连接是以 **声明式declarative** 的方式指定的,那这就这是可行的:应用只是简单地说明哪些连接是必需的,查询优化器决定如何最好地执行连接。我们以前在 “[数据查询语言](/ch2#数据查询语言)” 中见过这个想法。
但 MapReduce 及其数据流后继者在其他方面,与 SQL 的完全声明式查询模型有很大区别。MapReduce 是围绕着回调函数的概念建立的对于每条记录或者一组记录调用一个用户定义的函数Mapper 或 Reducer并且该函数可以自由地调用任意代码来决定输出什么。这种方法的优点是可以基于大量已有库的生态系统创作解析、自然语言分析、图像分析以及运行数值或统计算法等。
自由运行任意代码,长期以来都是传统 MapReduce 批处理系统与 MPP 数据库的区别所在(请参阅 “[Hadoop 与分布式数据库的对比](#Hadoop与分布式数据库的对比)” 一节)。虽然数据库具有编写用户定义函数的功能,但是它们通常使用起来很麻烦,而且与大多数编程语言中广泛使用的程序包管理器和依赖管理系统兼容不佳(例如 Java 的 Maven、Javascript 的 npm 以及 Ruby 的 gems
然而数据流引擎已经发现,支持除连接之外的更多 **声明式特性** 还有其他的优势。例如,如果一个回调函数只包含一个简单的过滤条件,或者只是从一条记录中选择了一些字段,那么在为每条记录调用函数时会有相当大的额外 CPU 开销。如果以声明方式表示这些简单的过滤和映射操作,那么查询优化器可以利用列式存储布局(请参阅 “[列式存储](ch3.md#列式存储)”只从磁盘读取所需的列。Hive、Spark DataFrames 和 Impala 还使用了向量化执行(请参阅 “[内存带宽和矢量化处理](ch3.md#内存带宽和矢量化处理)”):在对 CPU 缓存友好的内部循环中迭代数据避免函数调用。Spark 生成 JVM 字节码【79】Impala 使用 LLVM 为这些内部循环生成本机代码【41】。
然而数据流引擎已经发现,支持除连接之外的更多 **声明式特性** 还有其他的优势。例如,如果一个回调函数只包含一个简单的过滤条件,或者只是从一条记录中选择了一些字段,那么在为每条记录调用函数时会有相当大的额外 CPU 开销。如果以声明方式表示这些简单的过滤和映射操作,那么查询优化器可以利用列式存储布局(请参阅 “[列式存储](/ch3#列式存储)”只从磁盘读取所需的列。Hive、Spark DataFrames 和 Impala 还使用了向量化执行(请参阅 “[内存带宽和矢量化处理](/ch3#内存带宽和矢量化处理)”):在对 CPU 缓存友好的内部循环中迭代数据避免函数调用。Spark 生成 JVM 字节码【79】Impala 使用 LLVM 为这些内部循环生成本机代码【41】。
通过在高级 API 中引入声明式的部分,并使查询优化器可以在执行期间利用这些来做优化,批处理框架看起来越来越像 MPP 数据库了(并且能实现可与之媲美的性能)。同时,通过拥有运行任意代码和以任意格式读取数据的可扩展性,它们保持了灵活性的优势。
@@ -804,10 +805,3 @@ Spark、Flink 和 Tez 避免将中间状态写入 HDFS因此它们采取了
1. Michael Armbrust, Reynold S Xin, Cheng Lian, et al.: “[Spark SQL: Relational Data Processing in Spark](http://people.csail.mit.edu/matei/papers/2015/sigmod_spark_sql.pdf),” at *ACM International Conference on Management of Data* (SIGMOD), June 2015. [doi:10.1145/2723372.2742797](http://dx.doi.org/10.1145/2723372.2742797)
1. Daniel Blazevski: “[Planting Quadtrees for Apache Flink](https://blog.insightdatascience.com/planting-quadtrees-for-apache-flink-b396ebc80d35),” *insightdataengineering.com*, March 25, 2016.
1. Tom White: “[Genome Analysis Toolkit: Now Using Apache Spark for Data Processing](https://web.archive.org/web/20190215132904/http://blog.cloudera.com/blog/2016/04/genome-analysis-toolkit-now-using-apache-spark-for-data-processing/),” *blog.cloudera.com*, April 6, 2016.
------
| 上一章 | 目录 | 下一章 |
| --------------------------------- | ------------------------------- | ------------------------ |
| [第三部分:衍生数据](part-iii.md) | [设计数据密集型应用](README.md) | [第十一章:流处理](ch11.md) |

View File

@@ -1,18 +1,21 @@
# 第十一章:流处理
---
title: "第十一章:流处理"
linkTitle: "11. 流处理"
weight: 311
breadcrumbs: false
---
![](img/ch11.png)
![](/img/ch11.png)
> 有效的复杂系统总是从简单的系统演化而来。反之亦然:从零设计的复杂系统没一个能有效工作的。
>
> —— 约翰・加尔Systemantics1975
---------------
[TOC]
在 [第十章](/ch10) 中,我们讨论了批处理技术,它读取一组文件作为输入,并生成一组新的文件作为输出。输出是 **衍生数据derived data** 的一种形式;也就是说,如果需要,可以通过再次运行批处理过程来重新创建数据集。我们看到了如何使用这个简单而强大的想法来建立搜索索引、推荐系统、做分析等等。
在 [第十章](ch10.md) 中,我们讨论了批处理技术,它读取一组文件作为输入,并生成一组新的文件作为输出。输出是 **衍生数据derived data** 的一种形式;也就是说,如果需要,可以通过再次运行批处理过程来重新创建数据集。我们看到了如何使用这个简单而强大的想法来建立搜索索引、推荐系统、做分析等等
然而,在 [第十章](ch10.md) 中仍然有一个很大的假设即输入是有界的即已知和有限的大小所以批处理知道它何时完成输入的读取。例如MapReduce 核心的排序操作必须读取其全部输入,然后才能开始生成输出:可能发生这种情况:最后一条输入记录具有最小的键,因此需要第一个被输出,所以提早开始输出是不可行的。
然而,在 [第十章](/ch10) 中仍然有一个很大的假设即输入是有界的即已知和有限的大小所以批处理知道它何时完成输入的读取。例如MapReduce 核心的排序操作必须读取其全部输入,然后才能开始生成输出:可能发生这种情况:最后一条输入记录具有最小的键,因此需要第一个被输出,所以提早开始输出是不可行的
实际上,很多数据是 **无界限** 的,因为它随着时间的推移而逐渐到达:你的用户在昨天和今天产生了数据,明天他们将继续产生更多的数据。除非你停业,否则这个过程永远都不会结束,所以数据集从来就不会以任何有意义的方式 “完成”【1】。因此批处理程序必须将数据人为地分成固定时间段的数据块例如在每天结束时处理一天的数据或者在每小时结束时处理一小时的数据。
@@ -27,11 +30,11 @@
在批处理领域,作业的输入和输出是文件(也许在分布式文件系统上)。流处理领域中的等价物看上去是什么样子的?
当输入是一个文件(一个字节序列),第一个处理步骤通常是将其解析为一系列记录。在流处理的上下文中,记录通常被叫做 **事件event** ,但它本质上是一样的:一个小的、自包含的、不可变的对象,包含某个时间点发生的某件事情的细节。一个事件通常包含一个来自日历时钟的时间戳,以指明事件发生的时间(请参阅 “[单调钟与日历时钟](ch8.md#单调钟与日历时钟)”)。
当输入是一个文件(一个字节序列),第一个处理步骤通常是将其解析为一系列记录。在流处理的上下文中,记录通常被叫做 **事件event** ,但它本质上是一样的:一个小的、自包含的、不可变的对象,包含某个时间点发生的某件事情的细节。一个事件通常包含一个来自日历时钟的时间戳,以指明事件发生的时间(请参阅 “[单调钟与日历时钟](/ch8#单调钟与日历时钟)”)。
例如,发生的事件可能是用户采取的行动,例如查看页面或进行购买。它也可能来源于机器,例如对温度传感器或 CPU 利用率的周期性测量。在 “[使用 Unix 工具的批处理](ch10.md#使用Unix工具的批处理)” 的示例中Web 服务器日志的每一行都是一个事件。
例如,发生的事件可能是用户采取的行动,例如查看页面或进行购买。它也可能来源于机器,例如对温度传感器或 CPU 利用率的周期性测量。在 “[使用 Unix 工具的批处理](/ch10#使用Unix工具的批处理)” 的示例中Web 服务器日志的每一行都是一个事件。
事件可能被编码为文本字符串或 JSON或者某种二进制编码如 [第四章](ch4.md) 所述。这种编码允许你存储一个事件,例如将其追加到一个文件,将其插入关系表,或将其写入文档数据库。它还允许你通过网络将事件发送到另一个节点以进行处理。
事件可能被编码为文本字符串或 JSON或者某种二进制编码如 [第四章](/ch4) 所述。这种编码允许你存储一个事件,例如将其追加到一个文件,将其插入关系表,或将其写入文档数据库。它还允许你通过网络将事件发送到另一个节点以进行处理。
在批处理中,文件被写入一次,然后可能被多个作业读取。类似地,在流处理术语中,一个事件由 **生产者producer** (也称为 **发布者publisher****发送者sender** )生成一次,然后可能由多个 **消费者consumer** **订阅者subscribers****接收者recipients** 进行处理【3】。在文件系统中文件名标识一组相关记录在流式系统中相关的事件通常被聚合为一个 **主题topic****流stream**
@@ -44,21 +47,21 @@
### 消息传递系统
向消费者通知新事件的常用方式是使用 **消息传递系统messaging system**:生产者发送包含事件的消息,然后将消息推送给消费者。我们之前在 “[消息传递中的数据流](ch4.md#消息传递中的数据流)” 中谈到了这些系统,但现在我们将详细介绍这些系统。
向消费者通知新事件的常用方式是使用 **消息传递系统messaging system**:生产者发送包含事件的消息,然后将消息推送给消费者。我们之前在 “[消息传递中的数据流](/ch4#消息传递中的数据流)” 中谈到了这些系统,但现在我们将详细介绍这些系统。
像生产者和消费者之间的 Unix 管道或 TCP 连接这样的直接信道是实现消息传递系统的简单方法。但是大多数消息传递系统都在这一基本模型上进行了扩展。特别的是Unix 管道和 TCP 将恰好一个发送者与恰好一个接收者连接,而一个消息传递系统允许多个生产者节点将消息发送到同一个主题,并允许多个消费者节点接收主题中的消息。
在这个 **发布 / 订阅** 模式中,不同的系统采取各种各样的方法,并没有针对所有目的的通用答案。为了区分这些系统,问一下这两个问题会特别有帮助:
1. **如果生产者发送消息的速度比消费者能够处理的速度快会发生什么?** 一般来说,有三种选择:系统可以丢掉消息,将消息放入缓冲队列,或使用 **背压**backpressure也称为 **流量控制**,即 flow control阻塞生产者以免其发送更多的消息。例如 Unix 管道和 TCP 就使用了背压:它们有一个固定大小的小缓冲区,如果填满,发送者会被阻塞,直到接收者从缓冲区中取出数据(请参阅 “[网络拥塞和排队](ch8.md#网络拥塞和排队)”)。
1. **如果生产者发送消息的速度比消费者能够处理的速度快会发生什么?** 一般来说,有三种选择:系统可以丢掉消息,将消息放入缓冲队列,或使用 **背压**backpressure也称为 **流量控制**,即 flow control阻塞生产者以免其发送更多的消息。例如 Unix 管道和 TCP 就使用了背压:它们有一个固定大小的小缓冲区,如果填满,发送者会被阻塞,直到接收者从缓冲区中取出数据(请参阅 “[网络拥塞和排队](/ch8#网络拥塞和排队)”)。
如果消息被缓存在队列中那么理解队列增长会发生什么是很重要的。当队列装不进内存时系统会崩溃吗还是将消息写入磁盘如果是这样磁盘访问又会如何影响消息传递系统的性能【6】
2. **如果节点崩溃或暂时脱机,会发生什么情况? —— 是否会有消息丢失?** 与数据库一样,持久性可能需要写入磁盘和 / 或复制的某种组合(请参阅 “[复制与持久性](ch7.md#复制与持久性)”),这是有代价的。如果你能接受有时消息会丢失,则可能在同一硬件上获得更高的吞吐量和更低的延迟。
2. **如果节点崩溃或暂时脱机,会发生什么情况? —— 是否会有消息丢失?** 与数据库一样,持久性可能需要写入磁盘和 / 或复制的某种组合(请参阅 “[复制与持久性](/ch7#复制与持久性)”),这是有代价的。如果你能接受有时消息会丢失,则可能在同一硬件上获得更高的吞吐量和更低的延迟。
是否可以接受消息丢失取决于应用。例如对于周期传输的传感器读数和指标偶尔丢失的数据点可能并不重要因为更新的值会在短时间内发出。但要注意如果大量的消息被丢弃可能无法立刻意识到指标已经不正确了【7】。如果你正在对事件计数那么它们能够可靠送达是更重要的因为每个丢失的消息都意味着使计数器的错误扩大。
我们在 [第十章](ch10.md) 中探讨的批处理系统的一个很好的特性是,它们提供了强大的可靠性保证:失败的任务会自动重试,失败任务的部分输出会自动丢弃。这意味着输出与没有发生故障一样,这有助于简化编程模型。在本章的后面,我们将研究如何在流处理的上下文中提供类似的保证。
我们在 [第十章](/ch10) 中探讨的批处理系统的一个很好的特性是,它们提供了强大的可靠性保证:失败的任务会自动重试,失败任务的部分输出会自动丢弃。这意味着输出与没有发生故障一样,这有助于简化编程模型。在本章的后面,我们将研究如何在流处理的上下文中提供类似的保证。
#### 直接从生产者传递给消费者
@@ -66,8 +69,8 @@
* UDP 组播广泛应用于金融行业例如股票市场其中低时延非常重要【8】。虽然 UDP 本身是不可靠的,但应用层的协议可以恢复丢失的数据包(生产者必须记住它发送的数据包,以便能按需重新发送数据包)。
* 无代理的消息库,如 ZeroMQ 【9】和 nanomsg 采取类似的方法,通过 TCP 或 IP 多播实现发布 / 订阅消息传递。
* StatsD 【10】和 Brubeck 【7】使用不可靠的 UDP 消息传递来收集网络中所有机器的指标并对其进行监控。(在 StatsD 协议中,只有接收到所有消息,才认为计数器指标是正确的;使用 UDP 将使得指标处在一种最佳近似状态【11】。另请参阅 “[TCP 与 UDP](ch8.md#TCP与UDP)”
* 如果消费者在网络上公开了服务,生产者可以直接发送 HTTP 或 RPC 请求(请参阅 “[服务中的数据流REST 与 RPC](ch4.md#服务中的数据流REST与RPC)”)将消息推送给使用者。这就是 webhooks 背后的想法【12】一种服务的回调 URL 被注册到另一个服务中,并且每当事件发生时都会向该 URL 发出请求。
* StatsD 【10】和 Brubeck 【7】使用不可靠的 UDP 消息传递来收集网络中所有机器的指标并对其进行监控。(在 StatsD 协议中,只有接收到所有消息,才认为计数器指标是正确的;使用 UDP 将使得指标处在一种最佳近似状态【11】。另请参阅 “[TCP 与 UDP](/ch8#TCP与UDP)”
* 如果消费者在网络上公开了服务,生产者可以直接发送 HTTP 或 RPC 请求(请参阅 “[服务中的数据流REST 与 RPC](/ch4#服务中的数据流REST与RPC)”)将消息推送给使用者。这就是 webhooks 背后的想法【12】一种服务的回调 URL 被注册到另一个服务中,并且每当事件发生时都会向该 URL 发出请求。
尽管这些直接消息传递系统在设计它们的环境中运行良好,但是它们通常要求应用代码意识到消息丢失的可能性。它们的容错程度极为有限:即使协议检测到并重传在网络中丢失的数据包,它们通常也只是假设生产者和消费者始终在线。
@@ -83,7 +86,7 @@
#### 消息代理与数据库的对比
有些消息代理甚至可以使用 XA 或 JTA 参与两阶段提交协议(请参阅 “[实践中的分布式事务](ch9.md#实践中的分布式事务)”)。这个功能与数据库在本质上非常相似,尽管消息代理和数据库之间仍存在实践上很重要的差异:
有些消息代理甚至可以使用 XA 或 JTA 参与两阶段提交协议(请参阅 “[实践中的分布式事务](/ch9#实践中的分布式事务)”)。这个功能与数据库在本质上非常相似,尽管消息代理和数据库之间仍存在实践上很重要的差异:
* 数据库通常保留数据直至显式删除,而大多数消息代理在消息成功递送给消费者时会自动删除消息。这样的消息代理不适合长期的数据存储。
* 由于它们很快就能删除消息,大多数消息代理都认为它们的工作集相当小 —— 即队列很短。如果代理需要缓冲很多消息比如因为消费者速度较慢如果内存装不下消息可能会溢出到磁盘每个消息需要更长的处理时间整体吞吐量可能会恶化【6】。
@@ -94,7 +97,7 @@
#### 多个消费者
当多个消费者从同一主题中读取消息时,有两种主要的消息传递模式,如 [图 11-1](img/fig11-1.png) 所示:
当多个消费者从同一主题中读取消息时,有两种主要的消息传递模式,如 [图 11-1](/img/fig11-1.png) 所示:
* 负载均衡load balancing
@@ -104,7 +107,7 @@
每条消息都被传递给 **所有** 消费者。扇出允许几个独立的消费者各自 “收听” 相同的消息广播,而不会相互影响 —— 这个流处理中的概念对应批处理中多个不同批处理作业读取同一份输入文件 JMS 中的主题订阅与 AMQP 中的交叉绑定提供了这一功能)。
![](img/fig11-1.png)
![](/img/fig11-1.png)
**图 11-1 a负载平衡在消费者间共享消费主题b扇出将每条消息传递给多个消费者。**
@@ -114,11 +117,11 @@
消费者随时可能会崩溃,所以有一种可能的情况是:代理向消费者递送消息,但消费者没有处理,或者在消费者崩溃之前只进行了部分处理。为了确保消息不会丢失,消息代理使用 **确认acknowledgments**:客户端必须显式告知代理消息处理完毕的时间,以便代理能将消息从队列中移除。
如果与客户端的连接关闭,或者代理超出一段时间未收到确认,代理则认为消息没有被处理,因此它将消息再递送给另一个消费者。(请注意可能发生这样的情况,消息 **实际上是** 处理完毕的,但 **确认** 在网络中丢失了。需要一种原子提交协议才能处理这种情况,正如在 “[实践中的分布式事务](ch9.md#实践中的分布式事务)” 中所讨论的那样)
如果与客户端的连接关闭,或者代理超出一段时间未收到确认,代理则认为消息没有被处理,因此它将消息再递送给另一个消费者。(请注意可能发生这样的情况,消息 **实际上是** 处理完毕的,但 **确认** 在网络中丢失了。需要一种原子提交协议才能处理这种情况,正如在 “[实践中的分布式事务](/ch9#实践中的分布式事务)” 中所讨论的那样)
当与负载均衡相结合时,这种重传行为对消息的顺序有种有趣的影响。在 [图 11-2](img/fig11-2.png) 中,消费者通常按照生产者发送的顺序处理消息。然而消费者 2 在处理消息 m3 时崩溃,与此同时消费者 1 正在处理消息 m4。未确认的消息 m3 随后被重新发送给消费者 1结果消费者 1 按照 m4m3m5 的顺序处理消息。因此 m3 和 m4 的交付顺序与生产者 1 的发送顺序不同。
当与负载均衡相结合时,这种重传行为对消息的顺序有种有趣的影响。在 [图 11-2](/img/fig11-2.png) 中,消费者通常按照生产者发送的顺序处理消息。然而消费者 2 在处理消息 m3 时崩溃,与此同时消费者 1 正在处理消息 m4。未确认的消息 m3 随后被重新发送给消费者 1结果消费者 1 按照 m4m3m5 的顺序处理消息。因此 m3 和 m4 的交付顺序与生产者 1 的发送顺序不同。
![](img/fig11-2.png)
![](/img/fig11-2.png)
**图 11-2 在处理 m3 时消费者 2 崩溃,因此稍后重传至消费者 1**
@@ -130,7 +133,7 @@
数据库和文件系统采用截然相反的方法论:至少在某人显式删除前,通常写入数据库或文件的所有内容都要被永久记录下来。
这种思维方式上的差异对创建衍生数据的方式有巨大影响。如 [第十章](ch10.md) 所述,批处理过程的一个关键特性是,你可以反复运行它们,试验处理步骤,不用担心损坏输入(因为输入是只读的)。而 AMQP/JMS 风格的消息传递并非如此:收到消息是具有破坏性的,因为确认可能导致消息从代理中被删除,因此你不能期望再次运行同一个消费者能得到相同的结果。
这种思维方式上的差异对创建衍生数据的方式有巨大影响。如 [第十章](/ch10) 所述,批处理过程的一个关键特性是,你可以反复运行它们,试验处理步骤,不用担心损坏输入(因为输入是只读的)。而 AMQP/JMS 风格的消息传递并非如此:收到消息是具有破坏性的,因为确认可能导致消息从代理中被删除,因此你不能期望再次运行同一个消费者能得到相同的结果。
如果你将新的消费者添加到消息传递系统,通常只能接收到消费者注册之后开始发送的消息。先前的任何消息都随风而逝,一去不复返。作为对比,你可以随时为文件和数据库添加新的客户端,且能读取任意久远的数据(只要应用没有显式覆盖或删除这些数据)。
@@ -138,15 +141,15 @@
#### 使用日志进行消息存储
日志只是磁盘上简单的仅追加记录序列。我们先前在 [第三章](ch3.md) 中日志结构存储引擎和预写式日志的上下文中讨论了日志,在 [第五章](ch5.md) 复制的上下文里也讨论了它。
日志只是磁盘上简单的仅追加记录序列。我们先前在 [第三章](/ch3) 中日志结构存储引擎和预写式日志的上下文中讨论了日志,在 [第五章](/ch5) 复制的上下文里也讨论了它。
同样的结构可以用于实现消息代理生产者通过将消息追加到日志末尾来发送消息而消费者通过依次读取日志来接收消息。如果消费者读到日志末尾则会等待新消息追加的通知。Unix 工具 `tail -f` 能监视文件被追加写入的数据,基本上就是这样工作的。
为了伸缩超出单个磁盘所能提供的更高吞吐量,可以对日志进行 **分区**(按 [第六章](ch6.md) 的定义)。不同的分区可以托管在不同的机器上,使得每个分区都有一份能独立于其他分区进行读写的日志。一个主题可以定义为一组携带相同类型消息的分区。这种方法如 [图 11-3](img/fig11-3.png) 所示。
为了伸缩超出单个磁盘所能提供的更高吞吐量,可以对日志进行 **分区**(按 [第六章](/ch6) 的定义)。不同的分区可以托管在不同的机器上,使得每个分区都有一份能独立于其他分区进行读写的日志。一个主题可以定义为一组携带相同类型消息的分区。这种方法如 [图 11-3](/img/fig11-3.png) 所示。
在每个分区内,代理为每个消息分配一个单调递增的序列号或 **偏移量**offset在 [图 11-3](img/fig11-3.png) 中,框中的数字是消息偏移量)。这种序列号是有意义的,因为分区是仅追加写入的,所以分区内的消息是完全有序的。没有跨不同分区的顺序保证。
在每个分区内,代理为每个消息分配一个单调递增的序列号或 **偏移量**offset在 [图 11-3](/img/fig11-3.png) 中,框中的数字是消息偏移量)。这种序列号是有意义的,因为分区是仅追加写入的,所以分区内的消息是完全有序的。没有跨不同分区的顺序保证。
![](img/fig11-3.png)
![](/img/fig11-3.png)
**图 11-3 生产者通过将消息追加写入主题分区文件来发送消息,消费者依次读取这些文件**
@@ -159,7 +162,7 @@ Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 Distri
然后每个客户端将消费被指派分区中的 **所有** 消息。通常情况下,当一个用户被指派了一个日志分区时,它会以简单的单线程方式顺序地读取分区中的消息。这种粗粒度的负载均衡方法有一些缺点:
* 共享消费主题工作的节点数,最多为该主题中的日志分区数,因为同一个分区内的所有消息被递送到同一个节点 [^i]。
* 如果某条消息处理缓慢,则它会阻塞该分区中后续消息的处理(一种行首阻塞的形式;请参阅 “[描述性能](ch1.md#描述性能)”)。
* 如果某条消息处理缓慢,则它会阻塞该分区中后续消息的处理(一种行首阻塞的形式;请参阅 “[描述性能](/ch1#描述性能)”)。
因此在消息处理代价高昂希望逐条并行处理以及消息的顺序并没有那么重要的情况下JMS/AMQP 风格的消息代理是可取的。另一方面,在消息吞吐量很高,处理迅速,顺序很重要的情况下,基于日志的方法表现得非常好。
@@ -169,7 +172,7 @@ Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 Distri
顺序消费一个分区使得判断消息是否已经被处理变得相当容易:所有偏移量小于消费者的当前偏移量的消息已经被处理,而具有更大偏移量的消息还没有被看到。因此,代理不需要跟踪确认每条消息,只需要定期记录消费者的偏移即可。这种方法减少了额外簿记开销,而且在批处理和流处理中采用这种方法有助于提高基于日志的系统的吞吐量。
实际上,这种偏移量与单领导者数据库复制中常见的日志序列号非常相似,我们在 “[设置新从库](ch5.md#设置新从库)” 中讨论了这种情况。在数据库复制中,日志序列号允许跟随者断开连接后,重新连接到领导者,并在不跳过任何写入的情况下恢复复制。这里原理完全相同:消息代理表现得像一个主库,而消费者就像一个从库。
实际上,这种偏移量与单领导者数据库复制中常见的日志序列号非常相似,我们在 “[设置新从库](/ch5#设置新从库)” 中讨论了这种情况。在数据库复制中,日志序列号允许跟随者断开连接后,重新连接到领导者,并在不跳过任何写入的情况下恢复复制。这里原理完全相同:消息代理表现得像一个主库,而消费者就像一个从库。
如果消费者节点失效,则失效消费者的分区将指派给其他节点,并从最后记录的偏移量开始消费消息。如果消费者已经处理了后续的消息,但还没有记录它们的偏移量,那么重启后这些消息将被处理两次。我们将在本章后面讨论这个问题的处理方法。
@@ -208,9 +211,9 @@ Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 Distri
我们之前曾经说过,事件是某个时刻发生的事情的记录。发生的事情可能是用户操作(例如键入搜索查询)或读取传感器,但也可能是 **写入数据库**。某些东西被写入数据库的事实是可以被捕获、存储和处理的事件。这一观察结果表明,数据库和数据流之间的联系不仅仅是磁盘日志的物理存储 —— 而是更深层的联系。
事实上,复制日志(请参阅 “[复制日志的实现](ch5.md#复制日志的实现)”)是一个由数据库写入事件组成的流,由主库在处理事务时生成。从库将写入流应用到它们自己的数据库副本,从而最终得到相同数据的精确副本。复制日志中的事件描述发生的数据更改。
事实上,复制日志(请参阅 “[复制日志的实现](/ch5#复制日志的实现)”)是一个由数据库写入事件组成的流,由主库在处理事务时生成。从库将写入流应用到它们自己的数据库副本,从而最终得到相同数据的精确副本。复制日志中的事件描述发生的数据更改。
我们还在 “[全序广播](ch9.md#全序广播)” 中遇到了状态机复制原理,其中指出:如果每个事件代表对数据库的写入,并且每个副本按相同的顺序处理相同的事件,则副本将达到相同的最终状态 (假设事件处理是一个确定性的操作)。这是事件流的又一种场景!
我们还在 “[全序广播](/ch9#全序广播)” 中遇到了状态机复制原理,其中指出:如果每个事件代表对数据库的写入,并且每个副本按相同的顺序处理相同的事件,则副本将达到相同的最终状态 (假设事件处理是一个确定性的操作)。这是事件流的又一种场景!
在本节中,我们将首先看看异构数据系统中出现的一个问题,然后探讨如何通过将事件流的想法带入数据库来解决这个问题。
@@ -218,21 +221,21 @@ Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 Distri
正如我们在本书中所看到的,没有一个系统能够满足所有的数据存储、查询和处理需求。在实践中,大多数重要应用都需要组合使用几种不同的技术来满足所有的需求:例如,使用 OLTP 数据库来为用户请求提供服务,使用缓存来加速常见请求,使用全文索引来处理搜索查询,使用数据仓库用于分析。每一种技术都有自己的数据副本,并根据自己的目的进行存储方式的优化。
由于相同或相关的数据出现在了不同的地方,因此相互间需要保持同步:如果某个项目在数据库中被更新,它也应当在缓存、搜索索引和数据仓库中被更新。对于数据仓库,这种同步通常由 ETL 进程执行(请参阅 “[数据仓库](ch3.md#数据仓库)”),通常是先取得数据库的完整副本,然后执行转换,并批量加载到数据仓库中 —— 换句话说,批处理。我们在 “[批处理工作流的输出](ch10.md#批处理工作流的输出)” 中同样看到了如何使用批处理创建搜索索引、推荐系统和其他衍生数据系统。
由于相同或相关的数据出现在了不同的地方,因此相互间需要保持同步:如果某个项目在数据库中被更新,它也应当在缓存、搜索索引和数据仓库中被更新。对于数据仓库,这种同步通常由 ETL 进程执行(请参阅 “[数据仓库](/ch3#数据仓库)”),通常是先取得数据库的完整副本,然后执行转换,并批量加载到数据仓库中 —— 换句话说,批处理。我们在 “[批处理工作流的输出](/ch10#批处理工作流的输出)” 中同样看到了如何使用批处理创建搜索索引、推荐系统和其他衍生数据系统。
如果周期性的完整数据库转储过于缓慢,有时会使用的替代方法是 **双写dual write**,其中应用代码在数据变更时明确写入每个系统:例如,首先写入数据库,然后更新搜索索引,然后使缓存项失效(甚至同时执行这些写入)。
但是,双写有一些严重的问题,其中一个是竞争条件,如 [图 11-4](img/fig11-4.png) 所示。在这个例子中,两个客户端同时想要更新一个项目 X客户端 1 想要将值设置为 A客户端 2 想要将其设置为 B。两个客户端首先将新值写入数据库然后将其写入到搜索索引。因为运气不好这些请求的时序是交错的数据库首先看到来自客户端 1 的写入将值设置为 A然后来自客户端 2 的写入将值设置为 B因此数据库中的最终值为 B。搜索索引首先看到来自客户端 2 的写入,然后是客户端 1 的写入,所以搜索索引中的最终值是 A。即使没发生错误这两个系统现在也永久地不一致了。
但是,双写有一些严重的问题,其中一个是竞争条件,如 [图 11-4](/img/fig11-4.png) 所示。在这个例子中,两个客户端同时想要更新一个项目 X客户端 1 想要将值设置为 A客户端 2 想要将其设置为 B。两个客户端首先将新值写入数据库然后将其写入到搜索索引。因为运气不好这些请求的时序是交错的数据库首先看到来自客户端 1 的写入将值设置为 A然后来自客户端 2 的写入将值设置为 B因此数据库中的最终值为 B。搜索索引首先看到来自客户端 2 的写入,然后是客户端 1 的写入,所以搜索索引中的最终值是 A。即使没发生错误这两个系统现在也永久地不一致了。
![](img/fig11-4.png)
![](/img/fig11-4.png)
**图 11-4 在数据库中 X 首先被设置为 A然后被设置为 B而在搜索索引处写入以相反的顺序到达**
除非有一些额外的并发检测机制,例如我们在 “[检测并发写入](ch5.md#检测并发写入)” 中讨论的版本向量,否则你甚至不会意识到发生了并发写入 —— 一个值将简单地以无提示方式覆盖另一个值。
除非有一些额外的并发检测机制,例如我们在 “[检测并发写入](/ch5#检测并发写入)” 中讨论的版本向量,否则你甚至不会意识到发生了并发写入 —— 一个值将简单地以无提示方式覆盖另一个值。
双重写入的另一个问题是,其中一个写入可能会失败,而另一个成功。这是一个容错问题,而不是一个并发问题,但也会造成两个系统互相不一致的结果。确保它们要么都成功要么都失败,是原子提交问题的一个例子,解决这个问题的代价是昂贵的(请参阅 “[原子提交与两阶段提交](ch9.md#原子提交与两阶段提交)”)。
双重写入的另一个问题是,其中一个写入可能会失败,而另一个成功。这是一个容错问题,而不是一个并发问题,但也会造成两个系统互相不一致的结果。确保它们要么都成功要么都失败,是原子提交问题的一个例子,解决这个问题的代价是昂贵的(请参阅 “[原子提交与两阶段提交](/ch9#原子提交与两阶段提交)”)。
如果你只有一个单领导者复制的数据库,那么这个领导者决定了写入顺序,而状态机复制方法可以在数据库副本上工作。然而,在 [图 11-4](img/fig11-4.png) 中,没有单个主库:数据库可能有一个领导者,搜索索引也可能有一个领导者,但是两者都不追随对方,所以可能会发生冲突(请参阅 “[多主复制](ch5.md#多主复制)”)。
如果你只有一个单领导者复制的数据库,那么这个领导者决定了写入顺序,而状态机复制方法可以在数据库副本上工作。然而,在 [图 11-4](/img/fig11-4.png) 中,没有单个主库:数据库可能有一个领导者,搜索索引也可能有一个领导者,但是两者都不追随对方,所以可能会发生冲突(请参阅 “[多主复制](/ch5#多主复制)”)。
如果实际上只有一个领导者 —— 例如,数据库 —— 而且我们能让搜索索引成为数据库的追随者,情况要好得多。但这在实践中可能吗?
@@ -244,29 +247,29 @@ Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 Distri
最近,人们对 **变更数据捕获change data capture, CDC** 越来越感兴趣这是一种观察写入数据库的所有数据变更并将其提取并转换为可以复制到其他系统中的形式的过程。CDC 是非常有意思的,尤其是当变更能在被写入后立刻用于流时。
例如,你可以捕获数据库中的变更,并不断将相同的变更应用至搜索索引。如果变更日志以相同的顺序应用,则可以预期搜索索引中的数据与数据库中的数据是匹配的。搜索索引和任何其他衍生数据系统只是变更流的消费者,如 [图 11-5](img/fig11-5.png) 所示。
例如,你可以捕获数据库中的变更,并不断将相同的变更应用至搜索索引。如果变更日志以相同的顺序应用,则可以预期搜索索引中的数据与数据库中的数据是匹配的。搜索索引和任何其他衍生数据系统只是变更流的消费者,如 [图 11-5](/img/fig11-5.png) 所示。
![](img/fig11-5.png)
![](/img/fig11-5.png)
**图 11-5 将数据按顺序写入一个数据库,然后按照相同的顺序将这些更改应用到其他系统**
#### 变更数据捕获的实现
我们可以将日志消费者叫做 **衍生数据系统**,正如在 [第三部分](part-iii.md) 的介绍中所讨论的:存储在搜索索引和数据仓库中的数据,只是 **记录系统** 数据的额外视图。变更数据捕获是一种机制,可确保对记录系统所做的所有更改都反映在衍生数据系统中,以便衍生系统具有数据的准确副本。
我们可以将日志消费者叫做 **衍生数据系统**,正如在 [第三部分](/part-iii) 的介绍中所讨论的:存储在搜索索引和数据仓库中的数据,只是 **记录系统** 数据的额外视图。变更数据捕获是一种机制,可确保对记录系统所做的所有更改都反映在衍生数据系统中,以便衍生系统具有数据的准确副本。
从本质上说,变更数据捕获使得一个数据库成为领导者(被捕获变化的数据库),并将其他组件变为追随者。基于日志的消息代理非常适合从源数据库传输变更事件,因为它保留了消息的顺序(避免了 [图 11-2](img/fig11-2.png) 的重新排序问题)。
从本质上说,变更数据捕获使得一个数据库成为领导者(被捕获变化的数据库),并将其他组件变为追随者。基于日志的消息代理非常适合从源数据库传输变更事件,因为它保留了消息的顺序(避免了 [图 11-2](/img/fig11-2.png) 的重新排序问题)。
数据库触发器可用来实现变更数据捕获(请参阅 “[基于触发器的复制](ch5.md#基于触发器的复制)”),通过注册观察所有变更的触发器,并将相应的变更项写入变更日志表中。但是它们往往是脆弱的,而且有显著的性能开销。解析复制日志可能是一种更稳健的方法,但它也很有挑战,例如如何应对模式变更。
数据库触发器可用来实现变更数据捕获(请参阅 “[基于触发器的复制](/ch5#基于触发器的复制)”),通过注册观察所有变更的触发器,并将相应的变更项写入变更日志表中。但是它们往往是脆弱的,而且有显著的性能开销。解析复制日志可能是一种更稳健的方法,但它也很有挑战,例如如何应对模式变更。
LinkedIn 的 Databus【25】Facebook 的 Wormhole【26】和 Yahoo! 的 Sherpa【27】大规模地应用这个思路。Bottled Water 使用解码 WAL 的 API 实现了 PostgreSQL 的 CDC【28】Maxwell 和 Debezium 通过解析 binlog 对 MySQL 做了类似的事情【29,30,31】Mongoriver 读取 MongoDB oplog【32,33】而 GoldenGate 为 Oracle 提供类似的功能【34,35】。
类似于消息代理,变更数据捕获通常是异步的:记录数据库系统在提交变更之前不会等待消费者应用变更。这种设计具有的运维优势是,添加缓慢的消费者不会过度影响记录系统。不过,所有复制延迟可能有的问题在这里都可能出现(请参阅 “[复制延迟问题](ch5.md#复制延迟问题)”)。
类似于消息代理,变更数据捕获通常是异步的:记录数据库系统在提交变更之前不会等待消费者应用变更。这种设计具有的运维优势是,添加缓慢的消费者不会过度影响记录系统。不过,所有复制延迟可能有的问题在这里都可能出现(请参阅 “[复制延迟问题](/ch5#复制延迟问题)”)。
#### 初始快照
如果你拥有 **所有** 对数据库进行变更的日志,则可以通过重播该日志,来重建数据库的完整状态。但是在许多情况下,永远保留所有更改会耗费太多磁盘空间,且重播过于费时,因此日志需要被截断。
例如,构建新的全文索引需要整个数据库的完整副本 —— 仅仅应用最近变更的日志是不够的,因为这样会丢失最近未曾更新的项目。因此,如果你没有完整的历史日志,则需要从一个一致的快照开始,如先前的 “[设置新从库](ch5.md#设置新从库)” 中所述。
例如,构建新的全文索引需要整个数据库的完整副本 —— 仅仅应用最近变更的日志是不够的,因为这样会丢失最近未曾更新的项目。因此,如果你没有完整的历史日志,则需要从一个一致的快照开始,如先前的 “[设置新从库](/ch5#设置新从库)” 中所述。
数据库的快照必须与变更日志中的已知位置或偏移量相对应,以便在处理完快照后知道从哪里开始应用变更。一些 CDC 工具集成了这种快照功能,而其他工具则把它留给你手动执行。
@@ -274,7 +277,7 @@ LinkedIn 的 Databus【25】Facebook 的 Wormhole【26】和 Yahoo! 的 Sherp
如果你只能保留有限的历史日志,则每次要添加新的衍生数据系统时,都需要做一次快照。但 **日志压缩log compaction** 提供了一个很好的备选方案。
我们之前在 “[散列索引](ch3.md#散列索引)” 中关于日志结构存储引擎的上下文中讨论了日志压缩(请参阅 [图 3-2](img/fig3-2.png) 的示例)。原理很简单:存储引擎定期在日志中查找具有相同键的记录,丢掉所有重复的内容,并只保留每个键的最新更新。这个压缩与合并过程在后台运行。
我们之前在 “[散列索引](/ch3#散列索引)” 中关于日志结构存储引擎的上下文中讨论了日志压缩(请参阅 [图 3-2](/img/fig3-2.png) 的示例)。原理很简单:存储引擎定期在日志中查找具有相同键的记录,丢掉所有重复的内容,并只保留每个键的最新更新。这个压缩与合并过程在后台运行。
在日志结构存储引擎中,具有特殊值 NULL**墓碑**,即 tombstone的更新表示该键被删除并会在日志压缩过程中被移除。但只要键不被覆盖或删除它就会永远留在日志中。这种压缩日志所需的磁盘空间仅取决于数据库的当前内容而不取决于数据库中曾经发生的写入次数。如果相同的键经常被覆盖写入则先前的值将最终将被垃圾回收只有最新的值会保留下来。
@@ -298,14 +301,14 @@ Kafka Connect【41】致力于将广泛的数据库系统的变更数据捕获
与变更数据捕获类似,事件溯源涉及到 **将所有对应用状态的变更** 存储为变更事件日志。最大的区别是事件溯源将这一想法应用到了一个不同的抽象层次上:
* 在变更数据捕获中,应用以 **可变方式mutable way** 使用数据库,可以任意更新和删除记录。变更日志是从数据库的底层提取的(例如,通过解析复制日志),从而确保从数据库中提取的写入顺序与实际写入的顺序相匹配,从而避免 [图 11-4](img/fig11-4.png) 中的竞态条件。写入数据库的应用不需要知道 CDC 的存在。
* 在变更数据捕获中,应用以 **可变方式mutable way** 使用数据库,可以任意更新和删除记录。变更日志是从数据库的底层提取的(例如,通过解析复制日志),从而确保从数据库中提取的写入顺序与实际写入的顺序相匹配,从而避免 [图 11-4](/img/fig11-4.png) 中的竞态条件。写入数据库的应用不需要知道 CDC 的存在。
* 在事件溯源中,应用逻辑显式构建在写入事件日志的不可变事件之上。在这种情况下,事件存储是仅追加写入的,更新与删除是不鼓励的或禁止的。事件被设计为旨在反映应用层面发生的事情,而不是底层的状态变更。
事件溯源是一种强大的数据建模技术:从应用的角度来看,将用户的行为记录为不可变的事件更有意义,而不是在可变数据库中记录这些行为的影响。事件溯源使得应用随时间演化更为容易,通过更容易理解事情发生的原因来帮助调试的进行,并有利于防止应用 Bug请参阅 “[不可变事件的优点](#不可变事件的优点)”)。
例如,存储 “学生取消选课” 事件以中性的方式清楚地表达了单个行为的意图,而其副作用 “从登记表中删除了一个条目,而一条取消原因的记录被添加到学生反馈表” 则嵌入了很多有关稍后对数据的使用方式的假设。如果引入一个新的应用功能,例如 “将位置留给等待列表中的下一个人” —— 事件溯源方法允许将新的副作用轻松地从现有事件中脱开。
事件溯源类似于 **编年史chronicle** 数据模型【45】事件日志与星型模式中的事实表之间也存在相似之处请参阅 “[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”) 。
事件溯源类似于 **编年史chronicle** 数据模型【45】事件日志与星型模式中的事实表之间也存在相似之处请参阅 “[星型和雪花型:分析的模式](/ch3#星型和雪花型:分析的模式)”) 。
诸如 Event Store【46】这样的专业数据库已经被开发出来供使用事件溯源的应用使用但总的来说这种方法独立于任何特定的工具。传统的数据库或基于日志的消息代理也可以用来构建这种风格的应用。
@@ -326,17 +329,17 @@ Kafka Connect【41】致力于将广泛的数据库系统的变更数据捕获
事件溯源的哲学是仔细区分 **事件event****命令command**【48】。当来自用户的请求刚到达时它一开始是一个命令在这个时间点上它仍然可能失败比如因为违反了一些完整性条件。应用必须首先验证它是否可以执行该命令。如果验证成功并且命令被接受则它变为一个持久化且不可变的事件。
例如,如果用户试图注册特定用户名,或预定飞机或剧院的座位,则应用需要检查用户名或座位是否已被占用。(先前在 “[容错共识](ch9.md#容错共识)” 中讨论过这个例子)当检查成功时,应用可以生成一个事件,指示特定的用户名是由特定的用户 ID 注册的,或者座位已经预留给特定的顾客。
例如,如果用户试图注册特定用户名,或预定飞机或剧院的座位,则应用需要检查用户名或座位是否已被占用。(先前在 “[容错共识](/ch9#容错共识)” 中讨论过这个例子)当检查成功时,应用可以生成一个事件,指示特定的用户名是由特定的用户 ID 注册的,或者座位已经预留给特定的顾客。
在事件生成的时刻,它就成为了 **事实fact**。即使客户稍后决定更改或取消预订,他们之前曾预定了某个特定座位的事实仍然成立,而更改或取消是之后添加的单独的事件。
事件流的消费者不允许拒绝事件:当消费者看到事件时,它已经成为日志中不可变的一部分,并且可能已经被其他消费者看到了。因此任何对命令的验证,都需要在它成为事件之前同步完成。例如,通过使用一个可以原子性地自动验证命令并发布事件的可串行事务。
或者,预订座位的用户请求可以拆分为两个事件:第一个是暂时预约,第二个是验证预约后的独立的确认事件(如 “[使用全序广播实现线性一致的存储](ch9.md#使用全序广播实现线性一致的存储)” 中所述) 。这种分割方式允许验证发生在一个异步的过程中。
或者,预订座位的用户请求可以拆分为两个事件:第一个是暂时预约,第二个是验证预约后的独立的确认事件(如 “[使用全序广播实现线性一致的存储](/ch9#使用全序广播实现线性一致的存储)” 中所述) 。这种分割方式允许验证发生在一个异步的过程中。
### 状态、流和不变性
我们在 [第十章](ch10.md) 中看到,批处理因其输入文件不变性而受益良多,你可以在现有输入文件上运行实验性处理作业,而不用担心损坏它们。这种不变性原则也是使得事件溯源与变更数据捕获如此强大的原因。
我们在 [第十章](/ch10) 中看到,批处理因其输入文件不变性而受益良多,你可以在现有输入文件上运行实验性处理作业,而不用担心损坏它们。这种不变性原则也是使得事件溯源与变更数据捕获如此强大的原因。
我们通常将数据库视为应用程序当前状态的存储 —— 这种表示针对读取进行了优化,而且通常对于服务查询而言是最为方便的表示。状态的本质是,它会变化,所以数据库才会支持数据的增删改。这又该如何匹配不变性呢?
@@ -344,13 +347,13 @@ Kafka Connect【41】致力于将广泛的数据库系统的变更数据捕获
无论状态如何变化,总是有一系列事件导致了这些变化。即使事情已经执行与回滚,这些事件出现是始终成立的。关键的想法是:可变的状态与不可变事件的仅追加日志相互之间并不矛盾:它们是一体两面,互为阴阳的。所有变化的日志 —— **变化日志changelog**,表示了随时间演变的状态。
如果你倾向于数学表示,那么你可能会说,应用状态是事件流对时间求积分得到的结果,而变更流是状态对时间求微分的结果,如 [图 11-6](img/fig11-6.png) 所示【49,50,51】。这个比喻有一些局限性例如状态的二阶导似乎没有意义但这是考虑数据的一个实用出发点。
如果你倾向于数学表示,那么你可能会说,应用状态是事件流对时间求积分得到的结果,而变更流是状态对时间求微分的结果,如 [图 11-6](/img/fig11-6.png) 所示【49,50,51】。这个比喻有一些局限性例如状态的二阶导似乎没有意义但这是考虑数据的一个实用出发点。
$$
state(now) = \int_{t=0}^{now}{stream(t) \ dt} \\
stream(t) = \frac{d\ state(t)}{dt}
$$
![](img/fig11-6.png)
![](/img/fig11-6.png)
**图 11-6 应用当前状态与事件流之间的关系**
@@ -366,35 +369,35 @@ $$
如果发生错误,会计师不会删除或更改分类帐中的错误交易 —— 而是添加另一笔交易以补偿错误例如退还一笔不正确的费用。不正确的交易将永远保留在分类帐中对于审计而言可能非常重要。如果从不正确的分类账衍生出的错误数字已经公布那么下一个会计周期的数字就会包括一个更正。这个过程在会计事务中是很常见的【54】。
尽管这种可审计性只在金融系统中尤其重要,但对于不受这种严格监管的许多其他系统,也是很有帮助的。如 “[批处理输出的哲学](ch10.md#批处理输出的哲学)” 中所讨论的,如果你意外地部署了将错误数据写入数据库的错误代码,当代码会破坏性地覆写数据时,恢复要困难得多。使用不可变事件的仅追加日志,诊断问题与故障恢复就要容易的多。
尽管这种可审计性只在金融系统中尤其重要,但对于不受这种严格监管的许多其他系统,也是很有帮助的。如 “[批处理输出的哲学](/ch10#批处理输出的哲学)” 中所讨论的,如果你意外地部署了将错误数据写入数据库的错误代码,当代码会破坏性地覆写数据时,恢复要困难得多。使用不可变事件的仅追加日志,诊断问题与故障恢复就要容易的多。
不可变的事件也包含了比当前状态更多的信息。例如在购物网站上顾客可以将物品添加到他们的购物车然后再将其移除。虽然从履行订单的角度第二个事件取消了第一个事件但对分析目的而言知道客户考虑过某个特定项而之后又反悔可能是很有用的。也许他们会选择在未来购买或者他们已经找到了替代品。这个信息被记录在事件日志中但对于移出购物车就删除记录的数据库而言这个信息在移出购物车时可能就丢失了【42】。
#### 从同一事件日志中派生多个视图
此外,通过从不变的事件日志中分离出可变的状态,你可以针对不同的读取方式,从相同的事件日志中衍生出几种不同的表现形式。效果就像一个流的多个消费者一样([图 11-5](img/fig11-5.png)):例如,分析型数据库 Druid 使用这种方式直接从 Kafka 摄取数据【55】Pistachio 是一个分布式的键值存储,使用 Kafka 作为提交日志【56】Kafka Connect 能将来自 Kafka 的数据导出到各种不同的数据库与索引【41】。这对于许多其他存储和索引系统如搜索服务器来说是很有意义的当系统要从分布式日志中获取输入时亦然请参阅 “[保持系统同步](#保持系统同步)”)。
此外,通过从不变的事件日志中分离出可变的状态,你可以针对不同的读取方式,从相同的事件日志中衍生出几种不同的表现形式。效果就像一个流的多个消费者一样([图 11-5](/img/fig11-5.png)):例如,分析型数据库 Druid 使用这种方式直接从 Kafka 摄取数据【55】Pistachio 是一个分布式的键值存储,使用 Kafka 作为提交日志【56】Kafka Connect 能将来自 Kafka 的数据导出到各种不同的数据库与索引【41】。这对于许多其他存储和索引系统如搜索服务器来说是很有意义的当系统要从分布式日志中获取输入时亦然请参阅 “[保持系统同步](#保持系统同步)”)。
添加从事件日志到数据库的显式转换能够使应用更容易地随时间演进如果你想要引入一个新功能以新的方式表示现有数据则可以使用事件日志来构建一个单独的、针对新功能的读取优化视图无需修改现有系统而与之共存。并行运行新旧系统通常比在现有系统中执行复杂的模式迁移更容易。一旦不再需要旧的系统你可以简单地关闭它并回收其资源【47,57】。
如果你不需要担心如何查询与访问数据,那么存储数据通常是非常简单的。模式设计、索引和存储引擎的许多复杂性,都是希望支持某些特定查询和访问模式的结果(请参阅 [第三章](ch3.md))。出于这个原因,通过将数据写入的形式与读取形式相分离,并允许几个不同的读取视图,你能获得很大的灵活性。这个想法有时被称为 **命令查询责任分离command query responsibility segregation, CQRS**【42,58,59】。
如果你不需要担心如何查询与访问数据,那么存储数据通常是非常简单的。模式设计、索引和存储引擎的许多复杂性,都是希望支持某些特定查询和访问模式的结果(请参阅 [第三章](/ch3))。出于这个原因,通过将数据写入的形式与读取形式相分离,并允许几个不同的读取视图,你能获得很大的灵活性。这个想法有时被称为 **命令查询责任分离command query responsibility segregation, CQRS**【42,58,59】。
数据库和模式设计的传统方法是基于这样一种谬论,数据必须以与查询相同的形式写入。如果可以将数据从针对写入优化的事件日志转换为针对读取优化的应用状态,那么有关规范化和非规范化的争论就变得无关紧要了(请参阅 “[多对一和多对多的关系](ch2.md#多对一和多对多的关系)”):在针对读取优化的视图中对数据进行非规范化是完全合理的,因为翻译过程提供了使其与事件日志保持一致的机制。
数据库和模式设计的传统方法是基于这样一种谬论,数据必须以与查询相同的形式写入。如果可以将数据从针对写入优化的事件日志转换为针对读取优化的应用状态,那么有关规范化和非规范化的争论就变得无关紧要了(请参阅 “[多对一和多对多的关系](/ch2#多对一和多对多的关系)”):在针对读取优化的视图中对数据进行非规范化是完全合理的,因为翻译过程提供了使其与事件日志保持一致的机制。
在 “[描述负载](ch1.md#描述负载)” 中,我们讨论了推特主页时间线,它是特定用户关注的人群所发推特的缓存(类似邮箱)。这是 **针对读取优化的状态** 的又一个例子:主页时间线是高度非规范化的,因为你的推文与你所有粉丝的时间线都构成了重复。然而,扇出服务保持了这种重复状态与新推特以及新关注关系的同步,从而保证了重复的可管理性。
在 “[描述负载](/ch1#描述负载)” 中,我们讨论了推特主页时间线,它是特定用户关注的人群所发推特的缓存(类似邮箱)。这是 **针对读取优化的状态** 的又一个例子:主页时间线是高度非规范化的,因为你的推文与你所有粉丝的时间线都构成了重复。然而,扇出服务保持了这种重复状态与新推特以及新关注关系的同步,从而保证了重复的可管理性。
#### 并发控制
事件溯源和变更数据捕获的最大缺点是,事件日志的消费者通常是异步的,所以可能会出现这样的情况:用户会写入日志,然后从日志衍生视图中读取,结果发现他的写入还没有反映在读取视图中。我们之前在 “[读己之写](ch5.md#读己之写)” 中讨论了这个问题以及可能的解决方案。
事件溯源和变更数据捕获的最大缺点是,事件日志的消费者通常是异步的,所以可能会出现这样的情况:用户会写入日志,然后从日志衍生视图中读取,结果发现他的写入还没有反映在读取视图中。我们之前在 “[读己之写](/ch5#读己之写)” 中讨论了这个问题以及可能的解决方案。
一种解决方案是将事件追加到日志时同步执行读取视图的更新。而将这些写入操作合并为一个原子单元需要 **事务**,所以要么将事件日志和读取视图保存在同一个存储系统中,要么就需要跨不同系统进行分布式事务。或者,你也可以使用在 “[使用全序广播实现线性一致的存储](ch9.md#使用全序广播实现线性一致的存储)” 中讨论的方法。
一种解决方案是将事件追加到日志时同步执行读取视图的更新。而将这些写入操作合并为一个原子单元需要 **事务**,所以要么将事件日志和读取视图保存在同一个存储系统中,要么就需要跨不同系统进行分布式事务。或者,你也可以使用在 “[使用全序广播实现线性一致的存储](/ch9#使用全序广播实现线性一致的存储)” 中讨论的方法。
另一方面,从事件日志导出当前状态也简化了并发控制的某些部分。许多对于多对象事务的需求(请参阅 “[单对象和多对象操作](ch7.md#单对象和多对象操作)”)源于单个用户操作需要在多个不同的位置更改数据。通过事件溯源,你可以设计一个自包含的事件以表示一个用户操作。然后用户操作就只需要在一个地方进行单次写入操作 —— 即将事件附加到日志中 —— 这个还是很容易使原子化的。
另一方面,从事件日志导出当前状态也简化了并发控制的某些部分。许多对于多对象事务的需求(请参阅 “[单对象和多对象操作](/ch7#单对象和多对象操作)”)源于单个用户操作需要在多个不同的位置更改数据。通过事件溯源,你可以设计一个自包含的事件以表示一个用户操作。然后用户操作就只需要在一个地方进行单次写入操作 —— 即将事件附加到日志中 —— 这个还是很容易使原子化的。
如果事件日志与应用状态以相同的方式分区(例如,处理分区 3 中的客户事件只需要更新分区 3 中的应用状态),那么直接使用单线程日志消费者就不需要写入并发控制了。它从设计上一次只处理一个事件(请参阅 “[真的串行执行](ch7.md#真的串行执行)”。日志通过在分区中定义事件的序列顺序消除了并发性的不确定性【24】。如果一个事件触及多个状态分区那么需要做更多的工作我们将在 [第十二章](ch12.md) 讨论。
如果事件日志与应用状态以相同的方式分区(例如,处理分区 3 中的客户事件只需要更新分区 3 中的应用状态),那么直接使用单线程日志消费者就不需要写入并发控制了。它从设计上一次只处理一个事件(请参阅 “[真的串行执行](/ch7#真的串行执行)”。日志通过在分区中定义事件的序列顺序消除了并发性的不确定性【24】。如果一个事件触及多个状态分区那么需要做更多的工作我们将在 [第十二章](/ch12) 讨论。
#### 不变性的局限性
许多不使用事件溯源模型的系统也还是依赖不可变性:各种数据库在内部使用不可变的数据结构或多版本数据来支持时间点快照(请参阅 “[索引和快照隔离](ch7.md#索引和快照隔离)” 。Git、Mercurial 和 Fossil 等版本控制系统也依靠不可变的数据来保存文件的版本历史记录。
许多不使用事件溯源模型的系统也还是依赖不可变性:各种数据库在内部使用不可变的数据结构或多版本数据来支持时间点快照(请参阅 “[索引和快照隔离](/ch7#索引和快照隔离)” 。Git、Mercurial 和 Fossil 等版本控制系统也依靠不可变的数据来保存文件的版本历史记录。
永远保持所有变更的不变历史,在多大程度上是可行的?答案取决于数据集的流失率。一些工作负载主要是添加数据,很少更新或删除;它们很容易保持不变。其他工作负载在相对较小的数据集上有较高的更新 / 删除率在这些情况下不可变的历史可能增至难以接受的巨大碎片化可能成为一个问题压缩与垃圾收集的表现对于运维的稳健性变得至关重要【60,61】。
@@ -402,7 +405,7 @@ $$
在这种情况下,仅仅在日志中添加另一个事件来指明先前的数据应该被视为删除是不够的 —— 你实际上是想改写历史并假装数据从一开始就没有写入。例如Datomic 管这个特性叫 **切除excision** 【62】而 Fossil 版本控制系统有一个类似的概念叫 **避免shunning** 【63】。
真正删除数据是非常非常困难的【64】因为副本可能存在于很多地方例如存储引擎文件系统和 SSD 通常会向一个新位置写入而不是原地覆盖旧数据【52】而备份通常是特意做成不可变的防止意外删除或损坏。删除操作更多的是指 “使取回数据更困难”,而不是指 “使取回数据不可能”。无论如何,有时你必须得尝试,正如我们在 “[立法与自律](ch12.md#立法与自律)” 中所看到的。
真正删除数据是非常非常困难的【64】因为副本可能存在于很多地方例如存储引擎文件系统和 SSD 通常会向一个新位置写入而不是原地覆盖旧数据【52】而备份通常是特意做成不可变的防止意外删除或损坏。删除操作更多的是指 “使取回数据更困难”,而不是指 “使取回数据不可能”。无论如何,有时你必须得尝试,正如我们在 “[立法与自律](/ch12#立法与自律)” 中所看到的。
## 流处理
@@ -411,15 +414,15 @@ $$
剩下的就是讨论一下你可以用流做什么 —— 也就是说,你可以处理它。一般来说,有三种选项:
1. 你可以将事件中的数据写入数据库、缓存、搜索索引或类似的存储系统,然后能被其他客户端查询。如 [图 11-5](img/fig11-5.png) 所示,这是数据库与系统其他部分所发生的变更保持同步的好方法 —— 特别是当流消费者是写入数据库的唯一客户端时。如 “[批处理工作流的输出](ch10.md#批处理工作流的输出)” 中所讨论的,它是写入存储系统的流等价物。
1. 你可以将事件中的数据写入数据库、缓存、搜索索引或类似的存储系统,然后能被其他客户端查询。如 [图 11-5](/img/fig11-5.png) 所示,这是数据库与系统其他部分所发生的变更保持同步的好方法 —— 特别是当流消费者是写入数据库的唯一客户端时。如 “[批处理工作流的输出](/ch10#批处理工作流的输出)” 中所讨论的,它是写入存储系统的流等价物。
2. 你能以某种方式将事件推送给用户,例如发送报警邮件或推送通知,或将事件流式传输到可实时显示的仪表板上。在这种情况下,人是流的最终消费者。
3. 你可以处理一个或多个输入流,并产生一个或多个输出流。流可能会经过由几个这样的处理阶段组成的流水线,最后再输出(选项 1 或 2
在本章的剩余部分中,我们将讨论选项 3处理流以产生其他衍生流。处理这样的流的代码片段被称为 **算子operator****作业job**。它与我们在 [第十章](ch10.md) 中讨论过的 Unix 进程和 MapReduce 作业密切相关,数据流的模式是相似的:一个流处理器以只读的方式使用输入流,并将其输出以仅追加的方式写入一个不同的位置。
在本章的剩余部分中,我们将讨论选项 3处理流以产生其他衍生流。处理这样的流的代码片段被称为 **算子operator****作业job**。它与我们在 [第十章](/ch10) 中讨论过的 Unix 进程和 MapReduce 作业密切相关,数据流的模式是相似的:一个流处理器以只读的方式使用输入流,并将其输出以仅追加的方式写入一个不同的位置。
流处理中的分区和并行化模式也非常类似于 [第十章](ch10.md) 中介绍的 MapReduce 和数据流引擎,因此我们不再重复这些主题。基本的 Map 操作(如转换和过滤记录)也是一样的。
流处理中的分区和并行化模式也非常类似于 [第十章](/ch10) 中介绍的 MapReduce 和数据流引擎,因此我们不再重复这些主题。基本的 Map 操作(如转换和过滤记录)也是一样的。
与批量作业相比的一个关键区别是,流不会结束。这种差异会带来很多隐含的结果。正如本章开始部分所讨论的,排序对无界数据集没有意义,因此无法使用 **排序合并连接**(请参阅 “[Reduce 侧连接与分组](ch10.md#Reduce侧连接与分组)”)。容错机制也必须改变:对于已经运行了几分钟的批处理作业,可以简单地从头开始重启失败任务,但是对于已经运行数年的流作业,重启后从头开始跑可能并不是一个可行的选项。
与批量作业相比的一个关键区别是,流不会结束。这种差异会带来很多隐含的结果。正如本章开始部分所讨论的,排序对无界数据集没有意义,因此无法使用 **排序合并连接**(请参阅 “[Reduce 侧连接与分组](/ch10#Reduce侧连接与分组)”)。容错机制也必须改变:对于已经运行了几分钟的批处理作业,可以简单地从头开始重启失败任务,但是对于已经运行数年的流作业,重启后从头开始跑可能并不是一个可行的选项。
### 流处理的应用
@@ -452,13 +455,13 @@ CEP 的实现包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
这些统计值通常是在固定时间区间内进行计算的,例如,你可能想知道在过去 5 分钟内服务每秒查询次数的均值,以及此时间段内响应时间的第 99 百分位点。在几分钟内取平均,能抹平秒和秒之间的无关波动,且仍然能向你展示流量模式的时间图景。聚合的时间间隔称为 **窗口window**,我们将在 “[时间推理](#时间推理)” 中更详细地讨论窗口。
流分析系统有时会使用概率算法,例如 Bloom filter我们在 “[性能优化](ch3.md#性能优化)” 中遇到过来管理成员资格HyperLogLog【72】用于基数估计以及各种百分比估计算法请参阅 “[实践中的百分位点](ch1.md#实践中的百分位点)”。概率算法产出近似的结果但比起精确算法的优点是内存使用要少得多。使用近似算法有时让人们觉得流处理系统总是有损的和不精确的但这是错误看法流处理并没有任何内在的近似性而概率算法只是一种优化【73】。
流分析系统有时会使用概率算法,例如 Bloom filter我们在 “[性能优化](/ch3#性能优化)” 中遇到过来管理成员资格HyperLogLog【72】用于基数估计以及各种百分比估计算法请参阅 “[实践中的百分位点](/ch1#实践中的百分位点)”。概率算法产出近似的结果但比起精确算法的优点是内存使用要少得多。使用近似算法有时让人们觉得流处理系统总是有损的和不精确的但这是错误看法流处理并没有任何内在的近似性而概率算法只是一种优化【73】。
许多开源分布式流处理框架的设计都是针对分析设计的:例如 Apache Storm、Spark Streaming、Flink、Concord、Samza 和 Kafka Streams 【74】。托管服务包括 Google Cloud Dataflow 和 Azure Stream Analytics。
#### 维护物化视图
我们在 “[数据库与流](#数据库与流)” 中看到,数据库的变更流可以用于维护衍生数据系统(如缓存、搜索索引和数据仓库),并使其与源数据库保持最新。我们可以将这些示例视作维护 **物化视图materialized view** 的一种具体场景(请参阅 “[聚合:数据立方体和物化视图](ch3.md#聚合:数据立方体和物化视图)”在某个数据集上衍生出一个替代视图以便高效查询并在底层数据变更时更新视图【50】。
我们在 “[数据库与流](#数据库与流)” 中看到,数据库的变更流可以用于维护衍生数据系统(如缓存、搜索索引和数据仓库),并使其与源数据库保持最新。我们可以将这些示例视作维护 **物化视图materialized view** 的一种具体场景(请参阅 “[聚合:数据立方体和物化视图](/ch3#聚合:数据立方体和物化视图)”在某个数据集上衍生出一个替代视图以便高效查询并在底层数据变更时更新视图【50】。
同样,在事件溯源中,应用程序的状态是通过应用事件日志来维护的;这里的应用程序状态也是一种物化视图。与流分析场景不同的是,仅考虑某个时间窗口内的事件通常是不够的:构建物化视图可能需要任意时间段内的 **所有** 事件,除了那些可能由日志压缩丢弃的过时事件(请参阅 “[日志压缩](#日志压缩)”)。实际上,你需要一个可以一直延伸到时间开端的窗口。
@@ -474,13 +477,13 @@ CEP 的实现包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
#### 消息传递和RPC
在 “[消息传递中的数据流](ch4.md#消息传递中的数据流)” 中我们讨论过,消息传递系统可以作为 RPC 的替代方案,即作为一种服务间通信的机制,比如在 Actor 模型中所使用的那样。尽管这些系统也是基于消息和事件,但我们通常不会将其视作流处理组件:
在 “[消息传递中的数据流](/ch4#消息传递中的数据流)” 中我们讨论过,消息传递系统可以作为 RPC 的替代方案,即作为一种服务间通信的机制,比如在 Actor 模型中所使用的那样。尽管这些系统也是基于消息和事件,但我们通常不会将其视作流处理组件:
* Actor 框架主要是管理模块通信的并发和分布式执行的一种机制,而流处理主要是一种数据管理技术。
* Actor 之间的交流往往是短暂的、一对一的;而事件日志则是持久的、多订阅者的。
* Actor 可以以任意方式进行通信(包括循环的请求 / 响应模式),但流处理通常配置在无环流水线中,其中每个流都是一个特定作业的输出,由良好定义的输入流中派生而来。
也就是说RPC 类系统与流处理之间有一些交叉领域。例如Apache Storm 有一个称为 **分布式 RPC** 的功能它允许将用户查询分散到一系列也处理事件流的节点上然后这些查询与来自输入流的事件交织而结果可以被汇总并发回给用户【78】另请参阅 “[多分区数据处理](ch12.md#多分区数据处理)”)。
也就是说RPC 类系统与流处理之间有一些交叉领域。例如Apache Storm 有一个称为 **分布式 RPC** 的功能它允许将用户查询分散到一系列也处理事件流的节点上然后这些查询与来自输入流的事件交织而结果可以被汇总并发回给用户【78】另请参阅 “[多分区数据处理](/ch12#多分区数据处理)”)。
也可以使用 Actor 框架来处理流。但是,很多这样的框架在崩溃时不能保证消息的传递,除非你实现了额外的重试逻辑,否则这种处理不是容错的。
@@ -490,13 +493,13 @@ CEP 的实现包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
在批处理中过程中,大量的历史事件被快速地处理。如果需要按时间来分析,批处理器需要检查每个事件中嵌入的时间戳。读取运行批处理机器的系统时钟没有任何意义,因为处理运行的时间与事件实际发生的时间无关。
批处理可以在几分钟内读取一年的历史事件;在大多数情况下,感兴趣的时间线是历史中的一年,而不是处理中的几分钟。而且使用事件中的时间戳,使得处理是 **确定性** 的:在相同的输入上再次运行相同的处理过程会得到相同的结果(请参阅 “[容错](ch10.md#容错)”)。
批处理可以在几分钟内读取一年的历史事件;在大多数情况下,感兴趣的时间线是历史中的一年,而不是处理中的几分钟。而且使用事件中的时间戳,使得处理是 **确定性** 的:在相同的输入上再次运行相同的处理过程会得到相同的结果(请参阅 “[容错](/ch10#容错)”)。
另一方面,许多流处理框架使用处理机器上的本地系统时钟(**处理时间**,即 processing time来确定 **窗口windowing**【79】。这种方法的优点是简单如果事件创建与事件处理之间的延迟可以忽略不计那也是合理的。然而如果存在任何显著的处理延迟 —— 即,事件处理显著地晚于事件实际发生的时间,这种处理方式就失效了。
#### 事件时间与处理时间
很多原因都可能导致处理延迟:排队,网络故障(请参阅 “[不可靠的网络](ch8.md#不可靠的网络)”),性能问题导致消息代理 / 消息处理器出现争用,流消费者重启,从故障中恢复时重新处理过去的事件(请参阅 “[重播旧消息](#重播旧消息)”),或者在修复代码 BUG 之后。
很多原因都可能导致处理延迟:排队,网络故障(请参阅 “[不可靠的网络](/ch8#不可靠的网络)”),性能问题导致消息代理 / 消息处理器出现争用,流消费者重启,从故障中恢复时重新处理过去的事件(请参阅 “[重播旧消息](#重播旧消息)”),或者在修复代码 BUG 之后。
而且,消息延迟还可能导致无法预测消息顺序。例如,假设用户首先发出一个 Web 请求(由 Web 服务器 A 处理),然后发出第二个请求(由服务器 B 处理。A 和 B 发出描述它们所处理请求的事件,但是 B 的事件在 A 的事件发生之前到达消息代理。现在,流处理器将首先看到 B 事件,然后看到 A 事件,即使它们实际上是以相反的顺序发生的。
@@ -504,9 +507,9 @@ CEP 的实现包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
[^ii]: 感谢 Flink 社区的 Kostas Kloudas 提出这个比喻。
将事件时间和处理时间搞混会导致错误的数据。例如,假设你有一个流处理器用于测量请求速率(计算每秒请求数)。如果你重新部署流处理器,它可能会停止一分钟,并在恢复之后处理积压的事件。如果你按处理时间来衡量速率,那么在处理积压日志时,请求速率看上去就像有一个异常的突发尖峰,而实际上请求速率是稳定的([图 11-7](img/fig11-7.png))。
将事件时间和处理时间搞混会导致错误的数据。例如,假设你有一个流处理器用于测量请求速率(计算每秒请求数)。如果你重新部署流处理器,它可能会停止一分钟,并在恢复之后处理积压的事件。如果你按处理时间来衡量速率,那么在处理积压日志时,请求速率看上去就像有一个异常的突发尖峰,而实际上请求速率是稳定的([图 11-7](/img/fig11-7.png))。
![](img/fig11-7.png)
![](/img/fig11-7.png)
**图 11-7 按处理时间分窗,会因为处理速率的变动引入人为因素**
@@ -527,7 +530,7 @@ CEP 的实现包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
当事件可能在系统内多个地方进行缓冲时,为事件分配时间戳更加困难了。例如,考虑一个移动应用向服务器上报关于用量的事件。该应用可能会在设备处于脱机状态时被使用,在这种情况下,它将在设备本地缓冲事件,并在下一次互联网连接可用时向服务器上报这些事件(可能是几小时甚至几天)。对于这个流的任意消费者而言,它们就如延迟极大的滞留事件一样。
在这种情况下,事件上的事件戳实际上应当是用户交互发生的时间,取决于移动设备的本地时钟。然而用户控制的设备上的时钟通常是不可信的,因为它可能会被无意或故意设置成错误的时间(请参阅 “[时钟同步与准确性](ch8.md#时钟同步与准确性)”)。服务器收到事件的时间(取决于服务器的时钟)可能是更准确的,因为服务器在你的控制之下,但在描述用户交互方面意义不大。
在这种情况下,事件上的事件戳实际上应当是用户交互发生的时间,取决于移动设备的本地时钟。然而用户控制的设备上的时钟通常是不可信的,因为它可能会被无意或故意设置成错误的时间(请参阅 “[时钟同步与准确性](/ch8#时钟同步与准确性)”)。服务器收到事件的时间(取决于服务器的时钟)可能是更准确的,因为服务器在你的控制之下,但在描述用户交互方面意义不大。
要校正不正确的设备时钟一种方法是记录三个时间戳【82】
@@ -557,11 +560,11 @@ CEP 的实现包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
* 会话窗口Session window
与其他窗口类型不同,会话窗口没有固定的持续时间,而定义为:将同一用户出现时间相近的所有事件分组在一起,而当用户一段时间没有活动时(例如,如果 30 分钟内没有事件)窗口结束。会话切分是网站分析的常见需求(请参阅 “[分组](ch10.md#分组)”)。
与其他窗口类型不同,会话窗口没有固定的持续时间,而定义为:将同一用户出现时间相近的所有事件分组在一起,而当用户一段时间没有活动时(例如,如果 30 分钟内没有事件)窗口结束。会话切分是网站分析的常见需求(请参阅 “[分组](/ch10#分组)”)。
### 流连接
在 [第十章](ch10.md) 中,我们讨论了批处理作业如何通过键来连接数据集,以及这种连接是如何成为数据管道的重要组成部分的。由于流处理将数据管道泛化为对无限数据集进行增量处理,因此对流进行连接的需求也是完全相同的。
在 [第十章](/ch10) 中,我们讨论了批处理作业如何通过键来连接数据集,以及这种连接是如何成为数据管道的重要组成部分的。由于流处理将数据管道泛化为对无限数据集进行增量处理,因此对流进行连接的需求也是完全相同的。
然而,新事件随时可能出现在一个流中,这使得流连接要比批处理连接更具挑战性。为了更好地理解情况,让我们先来区分三种不同类型的连接:**流 - 流** 连接,**流 - 表** 连接,与 **表 - 表** 连接【84】。我们将在下面的章节中通过例子来说明。
@@ -577,11 +580,11 @@ CEP 的实现包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
#### 流表连接(流扩充)
在 “[示例:用户活动事件分析](ch10.md#示例:用户活动事件分析)”([图 10-2](img/fig10-2.png))中,我们看到了连接两个数据集的批处理作业示例:一组用户活动事件和一个用户档案数据库。将用户活动事件视为流,并在流处理器中连续执行相同的连接是很自然的想法:输入是包含用户 ID 的活动事件流,而输出还是活动事件流,但其中用户 ID 已经被扩展为用户的档案信息。这个过程有时被称为使用数据库的信息来 **扩充enriching** 活动事件。
在 “[示例:用户活动事件分析](/ch10#示例:用户活动事件分析)”([图 10-2](/img/fig10-2.png))中,我们看到了连接两个数据集的批处理作业示例:一组用户活动事件和一个用户档案数据库。将用户活动事件视为流,并在流处理器中连续执行相同的连接是很自然的想法:输入是包含用户 ID 的活动事件流,而输出还是活动事件流,但其中用户 ID 已经被扩展为用户的档案信息。这个过程有时被称为使用数据库的信息来 **扩充enriching** 活动事件。
要执行此连接,流处理器需要一次处理一个活动事件,在数据库中查找事件的用户 ID并将档案信息添加到活动事件中。数据库查询可以通过查询远程数据库来实现。但正如在 “[示例:用户活动事件分析](ch10.md#示例:用户活动事件分析)” 一节中讨论的此类远程查询可能会很慢并且有可能导致数据库过载【75】。
要执行此连接,流处理器需要一次处理一个活动事件,在数据库中查找事件的用户 ID并将档案信息添加到活动事件中。数据库查询可以通过查询远程数据库来实现。但正如在 “[示例:用户活动事件分析](/ch10#示例:用户活动事件分析)” 一节中讨论的此类远程查询可能会很慢并且有可能导致数据库过载【75】。
另一种方法是将数据库副本加载到流处理器中,以便在本地进行查询而无需网络往返。这种技术与我们在 “[Map 侧连接](ch10.md#Map侧连接)” 中讨论的散列连接非常相似:如果数据库的本地副本足够小,则可以是内存中的散列表,比较大的话也可以是本地磁盘上的索引。
另一种方法是将数据库副本加载到流处理器中,以便在本地进行查询而无需网络往返。这种技术与我们在 “[Map 侧连接](/ch10#Map侧连接)” 中讨论的散列连接非常相似:如果数据库的本地副本足够小,则可以是内存中的散列表,比较大的话也可以是本地磁盘上的索引。
与批处理作业的区别在于,批处理作业使用数据库的时间点快照作为输入,而流处理器是长时间运行的,且数据库的内容可能随时间而改变,所以流处理器数据库的本地副本需要保持更新。这个问题可以通过变更数据捕获来解决:流处理器可以订阅用户档案数据库的更新日志,如同活动事件流一样。当增添或修改档案时,流处理器会更新其本地副本。因此,我们有了两个流之间的连接:活动事件和档案更新。
@@ -589,7 +592,7 @@ CEP 的实现包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
#### 表表连接(维护物化视图)
我们在 “[描述负载](ch1.md#描述负载)” 中讨论的推特时间线例子时说过,当用户想要查看他们的主页时间线时,迭代用户所关注人群的推文并合并它们是一个开销巨大的操作。
我们在 “[描述负载](/ch1#描述负载)” 中讨论的推特时间线例子时说过,当用户想要查看他们的主页时间线时,迭代用户所关注人群的推文并合并它们是一个开销巨大的操作。
相反,我们需要一个时间线缓存:一种每个用户的 “收件箱”,在发送推文的时候写入这些信息,因而读取时间线时只需要简单地查询即可。物化与维护这个缓存需要处理以下事件:
@@ -612,7 +615,7 @@ GROUP BY follows.follower_id
流连接直接对应于这个查询中的表连接。时间线实际上是这个查询结果的缓存,每当底层的表发生变化时都会更新 [^iii]。
[^iii]: 如果你将流视作表的衍生物,如 [图 11-6](img/fig11-6.png) 所示而把一个连接看作是两个表的乘法u·v那么会发生一些有趣的事情物化连接的变化流遵循乘积法则(u·v)'= u'v + uv'。换句话说任何推文的变化量都与当前的关注联系在一起任何关注的变化量都与当前的推文相连接【49,50】。
[^iii]: 如果你将流视作表的衍生物,如 [图 11-6](/img/fig11-6.png) 所示而把一个连接看作是两个表的乘法u·v那么会发生一些有趣的事情物化连接的变化流遵循乘积法则(u·v)'= u'v + uv'。换句话说任何推文的变化量都与当前的关注联系在一起任何关注的变化量都与当前的推文相连接【49,50】。
#### 连接的时间依赖性
@@ -630,7 +633,7 @@ GROUP BY follows.follower_id
### 容错
在本章的最后一节中,让我们看一看流处理是如何容错的。我们在 [第十章](ch10.md) 中看到,批处理框架可以很容易地容错:如果 MapReduce 作业中的任务失败,可以简单地在另一台机器上再次启动,并且丢弃失败任务的输出。这种透明的重试是可能的,因为输入文件是不可变的,每个任务都将其输出写入到 HDFS 上的独立文件中,而输出仅当任务成功完成后可见。
在本章的最后一节中,让我们看一看流处理是如何容错的。我们在 [第十章](/ch10) 中看到,批处理框架可以很容易地容错:如果 MapReduce 作业中的任务失败,可以简单地在另一台机器上再次启动,并且丢弃失败任务的输出。这种透明的重试是可能的,因为输入文件是不可变的,每个任务都将其输出写入到 HDFS 上的独立文件中,而输出仅当任务成功完成后可见。
特别是,批处理容错方法可确保批处理作业的输出与没有出错的情况相同,即使实际上某些任务失败了。看起来好像每条输入记录都被处理了恰好一次 —— 没有记录被跳过,而且没有记录被处理两次。尽管重启任务意味着实际上可能会多次处理记录,但输出中的可见效果看上去就像只处理过一次。这个原则被称为 **恰好一次语义exactly-once semantics**,尽管 **等效一次effectively-once** 可能会是一个更写实的术语【90】。
@@ -650,9 +653,9 @@ Apache Flink 则使用不同的方法,它会定期生成状态的滚动存档
为了在出现故障时表现出恰好处理一次的样子,我们需要确保事件处理的所有输出和副作用 **当且仅当** 处理成功时才会生效。这些影响包括发送给下游算子或外部消息传递系统(包括电子邮件或推送通知)的任何消息,任何数据库写入,对算子状态的任何变更,以及对输入消息的任何确认(包括在基于日志的消息代理中将消费者偏移量前移)。
这些事情要么都原子地发生,要么都不发生,但是它们不应当失去同步。如果这种方法听起来很熟悉,那是因为我们在分布式事务和两阶段提交的上下文中讨论过它(请参阅 “[恰好一次的消息处理](ch9.md#恰好一次的消息处理)”)。
这些事情要么都原子地发生,要么都不发生,但是它们不应当失去同步。如果这种方法听起来很熟悉,那是因为我们在分布式事务和两阶段提交的上下文中讨论过它(请参阅 “[恰好一次的消息处理](/ch9#恰好一次的消息处理)”)。
在 [第九章](ch9.md) 中,我们讨论了分布式事务传统实现中的问题(如 XA。然而在限制更为严苛的环境中也是有可能高效实现这种原子提交机制的。Google Cloud Dataflow【81,92】和 VoltDB 【94】中使用了这种方法Apache Kafka 有计划加入类似的功能【95,96】。与 XA 不同,这些实现不会尝试跨异构技术提供事务,而是通过在流处理框架中同时管理状态变更与消息传递来内化事务。事务协议的开销可以通过在单个事务中处理多个输入消息来分摊。
在 [第九章](/ch9) 中,我们讨论了分布式事务传统实现中的问题(如 XA。然而在限制更为严苛的环境中也是有可能高效实现这种原子提交机制的。Google Cloud Dataflow【81,92】和 VoltDB 【94】中使用了这种方法Apache Kafka 有计划加入类似的功能【95,96】。与 XA 不同,这些实现不会尝试跨异构技术提供事务,而是通过在流处理框架中同时管理状态变更与消息传递来内化事务。事务协议的开销可以通过在单个事务中处理多个输入消息来分摊。
#### 幂等性
@@ -664,7 +667,7 @@ Apache Flink 则使用不同的方法,它会定期生成状态的滚动存档
Storm 的 Trident 基于类似的想法来处理状态【78】。依赖幂等性意味着隐含了一些假设重启一个失败的任务必须以相同的顺序重播相同的消息基于日志的消息代理能做这些事处理必须是确定性的没有其他节点能同时更新相同的值【98,99】。
当从一个处理节点故障切换到另一个节点时,可能需要进行 **防护**fencing请参阅 “[领导者和锁](ch8.md#领导者和锁)”),以防止被假死节点干扰。尽管有这么多注意事项,幂等操作是一种实现 **恰好一次语义** 的有效方式,仅需很小的额外开销。
当从一个处理节点故障切换到另一个节点时,可能需要进行 **防护**fencing请参阅 “[领导者和锁](/ch8#领导者和锁)”),以防止被假死节点干扰。尽管有这么多注意事项,幂等操作是一种实现 **恰好一次语义** 的有效方式,仅需很小的额外开销。
#### 失败后重建状态
@@ -672,7 +675,7 @@ Storm 的 Trident 基于类似的想法来处理状态【78】。依赖幂等性
一种选择是将状态保存在远程数据存储中,并进行复制,然而正如在 “[流表连接(流扩充)](#流表连接(流扩充))” 中所述,每个消息都要查询远程数据库可能会很慢。另一种方法是在流处理器本地保存状态,并定期复制。然后当流处理器从故障中恢复时,新任务可以读取状态副本,恢复处理而不丢失数据。
例如Flink 定期捕获算子状态的快照,并将它们写入 HDFS 等持久存储中【92,93】。Samza 和 Kafka Streams 通过将状态变更发送到具有日志压缩功能的专用 Kafka 主题来复制状态变更这与变更数据捕获类似【84,100】。VoltDB 通过在多个节点上对每个输入消息进行冗余处理来复制状态(请参阅 “[真的串行执行](ch7.md#真的串行执行)”)。
例如Flink 定期捕获算子状态的快照,并将它们写入 HDFS 等持久存储中【92,93】。Samza 和 Kafka Streams 通过将状态变更发送到具有日志压缩功能的专用 Kafka 主题来复制状态变更这与变更数据捕获类似【84,100】。VoltDB 通过在多个节点上对每个输入消息进行冗余处理来复制状态(请参阅 “[真的串行执行](/ch7#真的串行执行)”)。
在某些情况下,甚至可能都不需要复制状态,因为它可以从输入流重建。例如,如果状态是从相当短的窗口中聚合而成,则简单地重播该窗口中的输入事件可能是足够快的。如果状态是通过变更数据捕获来维护的数据库的本地副本,那么也可以从日志压缩的变更流中重建数据库(请参阅 “[日志压缩](#日志压缩)”)。
@@ -681,19 +684,19 @@ Storm 的 Trident 基于类似的想法来处理状态【78】。依赖幂等性
## 本章小结
在本章中,我们讨论了事件流,它们所服务的目的,以及如何处理它们。在某些方面,流处理非常类似于在 [第十章](ch10.md) 中讨论的批处理,不过是在无限的(永无止境的)流而不是固定大小的输入上持续进行。从这个角度来看,消息代理和事件日志可以视作文件系统的流式等价物。
在本章中,我们讨论了事件流,它们所服务的目的,以及如何处理它们。在某些方面,流处理非常类似于在 [第十章](/ch10) 中讨论的批处理,不过是在无限的(永无止境的)流而不是固定大小的输入上持续进行。从这个角度来看,消息代理和事件日志可以视作文件系统的流式等价物。
我们花了一些时间比较两种消息代理:
* AMQP/JMS 风格的消息代理
代理将单条消息分配给消费者,消费者在成功处理单条消息后确认消息。消息被确认后从代理中删除。这种方法适合作为一种异步形式的 RPC另请参阅 “[消息传递中的数据流](ch4.md#消息传递中的数据流)”),例如在任务队列中,消息处理的确切顺序并不重要,而且消息在处理完之后,不需要回头重新读取旧消息。
代理将单条消息分配给消费者,消费者在成功处理单条消息后确认消息。消息被确认后从代理中删除。这种方法适合作为一种异步形式的 RPC另请参阅 “[消息传递中的数据流](/ch4#消息传递中的数据流)”),例如在任务队列中,消息处理的确切顺序并不重要,而且消息在处理完之后,不需要回头重新读取旧消息。
* 基于日志的消息代理
代理将一个分区中的所有消息分配给同一个消费者节点,并始终以相同的顺序传递消息。并行是通过分区实现的,消费者通过存档最近处理消息的偏移量来跟踪工作进度。消息代理将消息保留在磁盘上,因此如有必要的话,可以回跳并重新读取旧消息。
基于日志的方法与数据库中的复制日志(请参阅 [第五章](ch5.md))和日志结构存储引擎(请参阅 [第三章](ch3.md))有相似之处。我们看到,这种方法对于消费输入流,并产生衍生状态或衍生输出数据流的系统而言特别适用。
基于日志的方法与数据库中的复制日志(请参阅 [第五章](/ch5))和日志结构存储引擎(请参阅 [第三章](/ch3))有相似之处。我们看到,这种方法对于消费输入流,并产生衍生状态或衍生输出数据流的系统而言特别适用。
就流的来源而言,我们讨论了几种可能性:用户活动事件,定期读数的传感器,和 Feed 数据(例如,金融中的市场数据)能够自然地表示为流。我们发现将数据库写入视作流也是很有用的:我们可以捕获变更日志 —— 即对数据库所做的所有变更的历史记录 —— 隐式地通过变更数据捕获,或显式地通过事件溯源。日志压缩允许流也能保有数据库内容的完整副本。
@@ -822,9 +825,3 @@ Storm 的 Trident 基于类似的想法来处理状态【78】。依赖幂等性
1. E. N. (Mootaz) Elnozahy, Lorenzo Alvisi, Yi-Min Wang, and David B. Johnson: “[A Survey of Rollback-Recovery Protocols in Message-Passing Systems](http://www.cs.utexas.edu/~lorenzo/papers/SurveyFinal.pdf),” *ACM Computing Surveys*, volume 34, number 3, pages 375408, September 2002. [doi:10.1145/568522.568525](http://dx.doi.org/10.1145/568522.568525)
1. Adam Warski: “[Kafka Streams How Does It Fit the Stream Processing Landscape?](https://softwaremill.com/kafka-streams-how-does-it-fit-stream-landscape/),” *softwaremill.com*, June 1, 2016.
------
| 上一章 | 目录 | 下一章 |
| ------------------------- | ------------------------------- | ---------------------------------- |
| [第十章:批处理](ch10.md) | [设计数据密集型应用](README.md) | [第十二章:数据系统的未来](ch12.md) |

View File

@@ -1,24 +1,26 @@
# 第十二章:数据系统的未来
---
title: "第十二章:数据系统的未来"
linkTitle: "12. 数据系统的未来"
weight: 312
breadcrumbs: false
---
![](img/ch12.png)
![](/img/ch12.png)
> 如果船长的终极目标是保护船只,他应该永远待在港口。
>
> —— 圣托马斯・阿奎那《神学大全》1265-1274
---------------
[TOC]
到目前为止,本书主要描述的是 **现状**。在这最后一章中,我们将放眼 **未来**,讨论应该是怎么样的:我将提出一些想法与方法,我相信它们能从根本上改进我们设计与构建应用的方式。
对未来的看法与推测当然具有很大的主观性。所以在撰写本章时,当提及我个人的观点时会使用第一人称。你完全可以不同意这些观点并提出自己的看法,但我希望本章中的概念,至少能成为富有成效的讨论出发点,并澄清一些经常被混淆的概念。
[第一章](ch1.md) 概述了本书的目标:探索如何创建 **可靠**、**可伸缩** 和 **可维护** 的应用与系统。这一主题贯穿了所有的章节:例如,我们讨论了许多有助于提高可靠性的容错算法,有助于提高可伸缩性的分区,以及有助于提高可维护性的演化与抽象机制。在本章中,我们将把所有这些想法结合在一起,并在它们的基础上展望未来。我们的目标是,发现如何设计出比现有应用更好的应用 —— 健壮、正确、可演化、且最终对人类有益。
[第一章](/ch1) 概述了本书的目标:探索如何创建 **可靠**、**可伸缩** 和 **可维护** 的应用与系统。这一主题贯穿了所有的章节:例如,我们讨论了许多有助于提高可靠性的容错算法,有助于提高可伸缩性的分区,以及有助于提高可维护性的演化与抽象机制。在本章中,我们将把所有这些想法结合在一起,并在它们的基础上展望未来。我们的目标是,发现如何设计出比现有应用更好的应用 —— 健壮、正确、可演化、且最终对人类有益。
## 数据集成
本书中反复出现的主题是,对于任何给定的问题都会有好几种解决方案,所有这些解决方案都有不同的优缺点与利弊权衡。例如在 [第三章](ch3.md) 讨论存储引擎时我们看到了日志结构存储、B 树以及列式存储。在 [第五章](ch5.md) 讨论复制时,我们看到了单领导者、多领导者和无领导者的方法。
本书中反复出现的主题是,对于任何给定的问题都会有好几种解决方案,所有这些解决方案都有不同的优缺点与利弊权衡。例如在 [第三章](/ch3) 讨论存储引擎时我们看到了日志结构存储、B 树以及列式存储。在 [第五章](/ch5) 讨论复制时,我们看到了单领导者、多领导者和无领导者的方法。
如果你有一个类似于 “我想存储一些数据并稍后再查询” 的问题,那么并没有一种正确的解决方案。但对于不同的具体环境,总会有不同的合适方法。软件实现通常必须选择一种特定的方法。使单条代码路径能做到稳定健壮且表现良好已经是一件非常困难的事情了 —— 尝试在单个软件中完成所有事情,几乎可以保证,实现效果会很差。
@@ -32,7 +34,7 @@
例如,为了处理任意关键词的搜索查询,将 OLTP 数据库与全文搜索索引集成在一起是很常见的需求。尽管一些数据库(例如 PostgreSQL包含了全文索引功能对于简单的应用完全够了【1】但更复杂的搜索能力就需要专业的信息检索工具了。相反的是搜索索引通常不适合作为持久的记录系统因此许多应用需要组合这两种不同的工具以满足所有需求。
我们在 “[保持系统同步](ch11.md#保持系统同步)” 中接触过集成数据系统的问题。随着数据不同表示形式的增加,集成问题变得越来越困难。除了数据库和搜索索引之外,也许你需要在分析系统(数据仓库,或批处理和流处理系统)中维护数据副本;维护从原始数据中衍生的缓存,或反规范化的数据版本;将数据灌入机器学习、分类、排名或推荐系统中;或者基于数据变更发送通知。
我们在 “[保持系统同步](/ch11#保持系统同步)” 中接触过集成数据系统的问题。随着数据不同表示形式的增加,集成问题变得越来越困难。除了数据库和搜索索引之外,也许你需要在分析系统(数据仓库,或批处理和流处理系统)中维护数据副本;维护从原始数据中衍生的缓存,或反规范化的数据版本;将数据灌入机器学习、分类、排名或推荐系统中;或者基于数据变更发送通知。
令人惊讶的是我经常看到软件工程师做出这样的陈述“根据我的经验99% 的人只需要 X” 或者 “...... 不需要 X”对于各种各样的 X。我认为这种陈述更像是发言人自己的经验而不是技术实际上的实用性。可能对数据执行的操作其范围极其宽广。某人认为鸡肋而毫无意义的功能可能是别人的核心需求。当你拉高视角并考虑跨越整个组织范围的数据流时数据集成的需求往往就会变得明显起来。
@@ -40,23 +42,23 @@
当需要在多个存储系统中维护相同数据的副本以满足不同的访问模式时,你要对输入和输出了如指掌:哪些数据先写入,哪些数据表示衍生自哪些来源?如何以正确的格式,将所有数据导入正确的地方?
例如,你可能会首先将数据写入 **记录系统** 数据库,捕获对该数据库所做的变更(请参阅 “[变更数据捕获](ch11.md#变更数据捕获)”然后将变更以相同的顺序应用于搜索索引。如果变更数据捕获CDC是更新索引的唯一方式则可以确定该索引完全派生自记录系统因此与其保持一致除软件错误外。写入数据库是向该系统提供新输入的唯一方式。
例如,你可能会首先将数据写入 **记录系统** 数据库,捕获对该数据库所做的变更(请参阅 “[变更数据捕获](/ch11#变更数据捕获)”然后将变更以相同的顺序应用于搜索索引。如果变更数据捕获CDC是更新索引的唯一方式则可以确定该索引完全派生自记录系统因此与其保持一致除软件错误外。写入数据库是向该系统提供新输入的唯一方式。
允许应用程序直接写入搜索索引和数据库引入了如 [图 11-4](img/fig11-4.png) 所示的问题,其中两个客户端同时发送冲突的写入,且两个存储系统按不同顺序处理它们。在这种情况下,既不是数据库说了算,也不是搜索索引说了算,所以它们做出了相反的决定,进入彼此间持久性的不一致状态。
允许应用程序直接写入搜索索引和数据库引入了如 [图 11-4](/img/fig11-4.png) 所示的问题,其中两个客户端同时发送冲突的写入,且两个存储系统按不同顺序处理它们。在这种情况下,既不是数据库说了算,也不是搜索索引说了算,所以它们做出了相反的决定,进入彼此间持久性的不一致状态。
如果你可以通过单个系统来提供所有用户输入,从而决定所有写入的排序,则通过按相同顺序处理写入,可以更容易地衍生出其他数据表示。这是状态机复制方法的一个应用,我们在 “[全序广播](ch9.md#全序广播)” 中看到。无论你使用变更数据捕获还是事件溯源日志,都不如简单的基于全序的决策原则更重要。
如果你可以通过单个系统来提供所有用户输入,从而决定所有写入的排序,则通过按相同顺序处理写入,可以更容易地衍生出其他数据表示。这是状态机复制方法的一个应用,我们在 “[全序广播](/ch9#全序广播)” 中看到。无论你使用变更数据捕获还是事件溯源日志,都不如简单的基于全序的决策原则更重要。
基于事件日志来更新衍生数据的系统,通常可以做到 **确定性****幂等性**(请参阅 “[幂等性](ch11.md#幂等性)”),使得从故障中恢复相当容易。
基于事件日志来更新衍生数据的系统,通常可以做到 **确定性****幂等性**(请参阅 “[幂等性](/ch11#幂等性)”),使得从故障中恢复相当容易。
#### 衍生数据与分布式事务
保持不同数据系统彼此一致的经典方法涉及分布式事务,如 “[原子提交与两阶段提交](ch9.md#原子提交与两阶段提交)” 中所述。与分布式事务相比,使用衍生数据系统的方法如何?
保持不同数据系统彼此一致的经典方法涉及分布式事务,如 “[原子提交与两阶段提交](/ch9#原子提交与两阶段提交)” 中所述。与分布式事务相比,使用衍生数据系统的方法如何?
在抽象层面,它们通过不同的方式达到类似的目标。分布式事务通过 **锁** 进行互斥来决定写入的顺序(请参阅 “[两阶段锁定](ch7.md#两阶段锁定)”),而 CDC 和事件溯源使用日志进行排序。分布式事务使用原子提交来确保变更只生效一次,而基于日志的系统通常基于 **确定性重试****幂等性**
在抽象层面,它们通过不同的方式达到类似的目标。分布式事务通过 **锁** 进行互斥来决定写入的顺序(请参阅 “[两阶段锁定](/ch7#两阶段锁定)”),而 CDC 和事件溯源使用日志进行排序。分布式事务使用原子提交来确保变更只生效一次,而基于日志的系统通常基于 **确定性重试****幂等性**
最大的不同之处在于事务系统通常提供 [线性一致性](ch9.md#线性一致性),这包含着有用的保证,例如 [读己之写](ch5.md#读己之写)。另一方面,衍生数据系统通常是异步更新的,因此它们默认不会提供相同的时序保证。
最大的不同之处在于事务系统通常提供 [线性一致性](/ch9#线性一致性),这包含着有用的保证,例如 [读己之写](/ch5#读己之写)。另一方面,衍生数据系统通常是异步更新的,因此它们默认不会提供相同的时序保证。
在愿意为分布式事务付出代价的有限场景中,它们已被成功应用。但是,我认为 XA 的容错能力和性能很差劲(请参阅 “[实践中的分布式事务](ch9.md#实践中的分布式事务)”),这严重限制了它的实用性。我相信为分布式事务设计一种更好的协议是可行的。但使这样一种协议被现有工具广泛接受是很有挑战的,且不是立竿见影的事。
在愿意为分布式事务付出代价的有限场景中,它们已被成功应用。但是,我认为 XA 的容错能力和性能很差劲(请参阅 “[实践中的分布式事务](/ch9#实践中的分布式事务)”),这严重限制了它的实用性。我相信为分布式事务设计一种更好的协议是可行的。但使这样一种协议被现有工具广泛接受是很有挑战的,且不是立竿见影的事。
在没有广泛支持的良好分布式事务协议的情况下,我认为基于日志的衍生数据是集成不同数据系统的最有前途的方法。然而,诸如读己之写的保证是有用的,我认为告诉所有人 “最终一致性是不可避免的 —— 忍一忍并学会和它打交道” 是没有什么建设性的(至少在缺乏 **如何** 应对的良好指导时)。
@@ -66,26 +68,26 @@
对于足够小的系统,构建一个完全有序的事件日志是完全可行的(正如单主复制数据库的流行所证明的那样,它正好建立了这样一种日志)。但是,随着系统向更大更复杂的工作负载伸缩,限制开始出现:
* 在大多数情况下,构建完全有序的日志,需要所有事件汇集于决定顺序的 **单个领导者节点**。如果事件吞吐量大于单台计算机的处理能力,则需要将其分区到多台计算机上(请参阅 “[分区日志](ch11.md#分区日志)”)。然后两个不同分区中的事件顺序关系就不明确了。
* 如果服务器分布在多个 **地理位置分散** 的数据中心上,例如为了容忍整个数据中心掉线,你通常在每个数据中心都有单独的主库,因为网络延迟会导致同步的跨数据中心协调效率低下(请参阅 “[多主复制](ch5.md#多主复制)”)。这意味着源自两个不同数据中心的事件顺序未定义。
* 将应用程序部署为微服务时(请参阅 “[服务中的数据流REST 与 RPC](ch4.md#服务中的数据流REST与RPC)”),常见的设计选择是将每个服务及其持久状态作为独立单元进行部署,服务之间不共享持久状态。当两个事件来自不同的服务时,这些事件间的顺序未定义。
* 某些应用程序在客户端保存状态,该状态在用户输入时立即更新(无需等待服务器确认),甚至可以继续脱机工作(请参阅 “[需要离线操作的客户端](ch5.md#需要离线操作的客户端)”)。对于这样的应用程序,客户端和服务器很可能以不同的顺序看到事件。
* 在大多数情况下,构建完全有序的日志,需要所有事件汇集于决定顺序的 **单个领导者节点**。如果事件吞吐量大于单台计算机的处理能力,则需要将其分区到多台计算机上(请参阅 “[分区日志](/ch11#分区日志)”)。然后两个不同分区中的事件顺序关系就不明确了。
* 如果服务器分布在多个 **地理位置分散** 的数据中心上,例如为了容忍整个数据中心掉线,你通常在每个数据中心都有单独的主库,因为网络延迟会导致同步的跨数据中心协调效率低下(请参阅 “[多主复制](/ch5#多主复制)”)。这意味着源自两个不同数据中心的事件顺序未定义。
* 将应用程序部署为微服务时(请参阅 “[服务中的数据流REST 与 RPC](/ch4#服务中的数据流REST与RPC)”),常见的设计选择是将每个服务及其持久状态作为独立单元进行部署,服务之间不共享持久状态。当两个事件来自不同的服务时,这些事件间的顺序未定义。
* 某些应用程序在客户端保存状态,该状态在用户输入时立即更新(无需等待服务器确认),甚至可以继续脱机工作(请参阅 “[需要离线操作的客户端](/ch5#需要离线操作的客户端)”)。对于这样的应用程序,客户端和服务器很可能以不同的顺序看到事件。
在形式上,决定事件的全局顺序称为 **全序广播**,相当于 **共识**(请参阅 “[共识算法和全序广播](ch9.md#共识算法和全序广播)”)。大多数共识算法都是针对单个节点的吞吐量足以处理整个事件流的情况而设计的,并且这些算法不提供多个节点共享事件排序工作的机制。设计可以伸缩至单个节点的吞吐量之上,且在地理位置分散的环境中仍然工作良好的的共识算法仍然是一个开放的研究问题。
在形式上,决定事件的全局顺序称为 **全序广播**,相当于 **共识**(请参阅 “[共识算法和全序广播](/ch9#共识算法和全序广播)”)。大多数共识算法都是针对单个节点的吞吐量足以处理整个事件流的情况而设计的,并且这些算法不提供多个节点共享事件排序工作的机制。设计可以伸缩至单个节点的吞吐量之上,且在地理位置分散的环境中仍然工作良好的的共识算法仍然是一个开放的研究问题。
#### 排序事件以捕获因果关系
在事件之间不存在因果关系的情况下,全序的缺乏并不是一个大问题,因为并发事件可以任意排序。其他一些情况很容易处理:例如,当同一对象有多个更新时,它们可以通过将特定对象 ID 的所有更新路由到相同的日志分区来完全排序。然而,因果关系有时会以更微妙的方式出现(请参阅 “[顺序与因果关系](ch9.md#顺序与因果关系)”)。
在事件之间不存在因果关系的情况下,全序的缺乏并不是一个大问题,因为并发事件可以任意排序。其他一些情况很容易处理:例如,当同一对象有多个更新时,它们可以通过将特定对象 ID 的所有更新路由到相同的日志分区来完全排序。然而,因果关系有时会以更微妙的方式出现(请参阅 “[顺序与因果关系](/ch9#顺序与因果关系)”)。
例如,考虑一个社交网络服务,以及一对曾处于恋爱关系但刚分手的用户。其中一个用户将另一个用户从好友中移除,然后向剩余的好友发送消息,抱怨他们的前任。用户的心思是他们的前任不应该看到这些粗鲁的消息,因为消息是在好友状态解除后发送的。
但是如果好友关系状态与消息存储在不同的地方,在这样一个系统中,可能会出现 **解除好友** 事件与 **发送消息** 事件之间的因果依赖丢失的情况。如果因果依赖关系没有被捕捉到,则发送有关新消息的通知的服务可能会在 **解除好友** 事件之前处理 **发送消息** 事件,从而错误地向前任发送通知。
在本例中,通知实际上是消息和好友列表之间的连接,使得它与我们先前讨论的连接的时序问题有关(请参阅 “[连接的时间依赖性](ch11.md#连接的时间依赖性)”。不幸的是这个问题似乎并没有一个简单的答案【2,3】。起点包括
在本例中,通知实际上是消息和好友列表之间的连接,使得它与我们先前讨论的连接的时序问题有关(请参阅 “[连接的时间依赖性](/ch11#连接的时间依赖性)”。不幸的是这个问题似乎并没有一个简单的答案【2,3】。起点包括
* 逻辑时间戳可以提供无需协调的全局顺序(请参阅 “[序列号顺序](ch9.md#序列号顺序)”),因此它们可能有助于全序广播不可行的情况。但是,他们仍然要求收件人处理不按顺序发送的事件,并且需要传递其他元数据。
* 逻辑时间戳可以提供无需协调的全局顺序(请参阅 “[序列号顺序](/ch9#序列号顺序)”),因此它们可能有助于全序广播不可行的情况。但是,他们仍然要求收件人处理不按顺序发送的事件,并且需要传递其他元数据。
* 如果你可以记录一个事件来记录用户在做出决定之前所看到的系统状态并给该事件一个唯一的标识符那么后面的任何事件都可以引用该事件标识符来记录因果关系【4】。我们将在 “[读也是事件](#读也是事件)” 中回到这个想法。
* 冲突解决算法(请参阅 “[自动冲突解决](ch5.md#自动冲突解决)”)有助于处理以意外顺序传递的事件。它们对于维护状态很有用,但如果行为有外部副作用(例如,给用户发送通知),就没什么帮助了。
* 冲突解决算法(请参阅 “[自动冲突解决](/ch5#自动冲突解决)”)有助于处理以意外顺序传递的事件。它们对于维护状态很有用,但如果行为有外部副作用(例如,给用户发送通知),就没什么帮助了。
也许,随着时间的推移,应用开发模式将出现,使得能够有效地捕获因果依赖关系,并且保持正确的衍生状态,而不会迫使所有事件经历全序广播的瓶颈)。
@@ -93,27 +95,27 @@
我会说数据集成的目标是,确保数据最终能在所有正确的地方表现出正确的形式。这样做需要消费输入、转换、连接、过滤、聚合、训练模型、评估、以及最终写出适当的输出。批处理和流处理是实现这一目标的工具。
批处理和流处理的输出是衍生数据集,例如搜索索引、物化视图、向用户显示的建议、聚合指标等(请参阅 “[批处理工作流的输出](ch10.md#批处理工作流的输出)” 和 “[流处理的应用](ch11.md#流处理的应用)”)。
批处理和流处理的输出是衍生数据集,例如搜索索引、物化视图、向用户显示的建议、聚合指标等(请参阅 “[批处理工作流的输出](/ch10#批处理工作流的输出)” 和 “[流处理的应用](/ch11#流处理的应用)”)。
正如我们在 [第十章](ch10.md) 和 [第十一章](ch11.md) 中看到的,批处理和流处理有许多共同的原则,主要的根本区别在于流处理器在无限数据集上运行,而批处理输入是已知的有限大小。处理引擎的实现方式也有很多细节上的差异,但是这些区别已经开始模糊。
正如我们在 [第十章](/ch10) 和 [第十一章](/ch11) 中看到的,批处理和流处理有许多共同的原则,主要的根本区别在于流处理器在无限数据集上运行,而批处理输入是已知的有限大小。处理引擎的实现方式也有很多细节上的差异,但是这些区别已经开始模糊。
Spark 在批处理引擎上执行流处理,将流分解为 **微批次microbatches**,而 Apache Flink 则在流处理引擎上执行批处理【5】。原则上一种类型的处理可以用另一种类型来模拟但是性能特征会有所不同例如在跳跃或滑动窗口上微批次可能表现不佳【6】。
#### 维护衍生状态
批处理有着很强的函数式风格(即使其代码不是用函数式语言编写的):它鼓励确定性的纯函数,其输出仅依赖于输入,除了显式输出外没有副作用,将输入视作不可变的,且输出是仅追加的。流处理与之类似,但它扩展了算子以允许受管理的、容错的状态(请参阅 “[失败后重建状态”](ch11.md#失败后重建状态))。
批处理有着很强的函数式风格(即使其代码不是用函数式语言编写的):它鼓励确定性的纯函数,其输出仅依赖于输入,除了显式输出外没有副作用,将输入视作不可变的,且输出是仅追加的。流处理与之类似,但它扩展了算子以允许受管理的、容错的状态(请参阅 “[失败后重建状态”](/ch11#失败后重建状态))。
具有良好定义的输入和输出的确定性函数的原理不仅有利于容错(请参阅 “[幂等性](ch11.md#幂等性)”也简化了有关组织中数据流的推理【7】。无论衍生数据是搜索索引、统计模型还是缓存采用这种观点思考都是很有帮助的将其视为从一个东西衍生出另一个的数据管道通过函数式应用代码推送一个系统的状态变更并将其效果应用至衍生系统中。
具有良好定义的输入和输出的确定性函数的原理不仅有利于容错(请参阅 “[幂等性](/ch11#幂等性)”也简化了有关组织中数据流的推理【7】。无论衍生数据是搜索索引、统计模型还是缓存采用这种观点思考都是很有帮助的将其视为从一个东西衍生出另一个的数据管道通过函数式应用代码推送一个系统的状态变更并将其效果应用至衍生系统中。
原则上,衍生数据系统可以同步地维护,就像关系数据库在与索引表写入操作相同的事务中同步更新次级索引一样。然而,异步是使基于事件日志的系统稳健的原因:它允许系统的一部分故障被抑制在本地。而如果任何一个参与者失败,分布式事务将中止,因此它们倾向于通过将故障传播到系统的其余部分来放大故障(请参阅 “[分布式事务的限制](ch9.md#分布式事务的限制)”)。
原则上,衍生数据系统可以同步地维护,就像关系数据库在与索引表写入操作相同的事务中同步更新次级索引一样。然而,异步是使基于事件日志的系统稳健的原因:它允许系统的一部分故障被抑制在本地。而如果任何一个参与者失败,分布式事务将中止,因此它们倾向于通过将故障传播到系统的其余部分来放大故障(请参阅 “[分布式事务的限制](/ch9#分布式事务的限制)”)。
我们在 “[分区与次级索引](ch6.md#分区与次级索引)” 中看到次级索引经常跨越分区边界。具有次级索引的分区系统需要将写入发送到多个分区如果索引按关键词分区的话或将读取发送到所有分区如果索引是按文档分区的话。如果索引是异步维护的这种跨分区通信也是最可靠和最可伸缩的【8】另请参阅 “[多分区数据处理](#多分区数据处理)”)。
我们在 “[分区与次级索引](/ch6#分区与次级索引)” 中看到次级索引经常跨越分区边界。具有次级索引的分区系统需要将写入发送到多个分区如果索引按关键词分区的话或将读取发送到所有分区如果索引是按文档分区的话。如果索引是异步维护的这种跨分区通信也是最可靠和最可伸缩的【8】另请参阅 “[多分区数据处理](#多分区数据处理)”)。
#### 应用演化后重新处理数据
在维护衍生数据时,批处理和流处理都是有用的。流处理允许将输入中的变化以低延迟反映在衍生视图中,而批处理允许重新处理大量累积的历史数据以便将新视图导出到现有数据集上。
特别是,重新处理现有数据为维护系统、演化并支持新功能和需求变更提供了一个良好的机制(请参阅 [第四章](ch4.md))。没有重新进行处理,模式演化将仅限于简单的变化,例如向记录中添加新的可选字段或添加新类型的记录。无论是在写时模式还是在读时模式中都是如此(请参阅 “[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)”)。另一方面,通过重新处理,可以将数据集重组为一个完全不同的模型,以便更好地满足新的要求。
特别是,重新处理现有数据为维护系统、演化并支持新功能和需求变更提供了一个良好的机制(请参阅 [第四章](/ch4))。没有重新进行处理,模式演化将仅限于简单的变化,例如向记录中添加新的可选字段或添加新类型的记录。无论是在写时模式还是在读时模式中都是如此(请参阅 “[文档模型中的模式灵活性](/ch2#文档模型中的模式灵活性)”)。另一方面,通过重新处理,可以将数据集重组为一个完全不同的模型,以便更好地满足新的要求。
> ### 铁路上的模式迁移
>
@@ -131,15 +133,15 @@ Spark 在批处理引擎上执行流处理,将流分解为 **微批次micro
如果批处理用于重新处理历史数据而流处理用于处理最近的更新那么如何将这两者结合起来Lambda 架构【12】是这方面的一个建议引起了很多关注。
Lambda 架构的核心思想是通过将不可变事件附加到不断增长的数据集来记录传入数据,这类似于事件溯源(请参阅 “[事件溯源](ch11.md#事件溯源)”。为了从这些事件中衍生出读取优化的视图Lambda 架构建议并行运行两个不同的系统:批处理系统(如 Hadoop MapReduce和独立的流处理系统如 Storm
Lambda 架构的核心思想是通过将不可变事件附加到不断增长的数据集来记录传入数据,这类似于事件溯源(请参阅 “[事件溯源](/ch11#事件溯源)”。为了从这些事件中衍生出读取优化的视图Lambda 架构建议并行运行两个不同的系统:批处理系统(如 Hadoop MapReduce和独立的流处理系统如 Storm
在 Lambda 方法中,流处理器消耗事件并快速生成对视图的近似更新;批处理器稍后将使用同一组事件并生成衍生视图的更正版本。这个设计背后的原因是批处理更简单,因此不易出错,而流处理器被认为是不太可靠和难以容错的(请参阅 “[容错](ch11.md#容错)”)。而且,流处理可以使用快速近似算法,而批处理使用较慢的精确算法。
在 Lambda 方法中,流处理器消耗事件并快速生成对视图的近似更新;批处理器稍后将使用同一组事件并生成衍生视图的更正版本。这个设计背后的原因是批处理更简单,因此不易出错,而流处理器被认为是不太可靠和难以容错的(请参阅 “[容错](/ch11#容错)”)。而且,流处理可以使用快速近似算法,而批处理使用较慢的精确算法。
Lambda 架构是一种有影响力的想法,它将数据系统的设计变得更好,尤其是通过推广这样的原则:在不可变事件流上建立衍生视图,并在需要时重新处理事件。但是我也认为它有一些实际问题:
* 在批处理和流处理框架中维护相同的逻辑是很显著的额外工作。虽然像 Summingbird【13】这样的库提供了一种可以在批处理和流处理的上下文中运行的计算抽象。调试、调整和维护两个不同系统的操作复杂性依然存在【14】。
* 由于流管道和批处理管道产生独立的输出,因此需要合并它们以响应用户请求。如果计算是基于滚动窗口的简单聚合,则合并相当容易,但如果视图基于更复杂的操作(例如连接和会话化)而导出,或者输出不是时间序列,则会变得非常困难。
* 尽管有能力重新处理整个历史数据集是很好的,但在大型数据集上这样做经常会开销巨大。因此,批处理流水线通常需要设置为处理增量批处理(例如,在每小时结束时处理一小时的数据),而不是重新处理所有内容。这引发了 “[时间推理](ch11.md#时间推理)” 中讨论的问题,例如处理滞留事件和处理跨批次边界的窗口。增量化批处理计算会增加复杂性,使其更类似于流式传输层,这与保持批处理层尽可能简单的目标背道而驰。
* 尽管有能力重新处理整个历史数据集是很好的,但在大型数据集上这样做经常会开销巨大。因此,批处理流水线通常需要设置为处理增量批处理(例如,在每小时结束时处理一小时的数据),而不是重新处理所有内容。这引发了 “[时间推理](/ch11#时间推理)” 中讨论的问题,例如处理滞留事件和处理跨批次边界的窗口。增量化批处理计算会增加复杂性,使其更类似于流式传输层,这与保持批处理层尽可能简单的目标背道而驰。
#### 统一批处理和流处理
@@ -147,14 +149,14 @@ Lambda 架构是一种有影响力的想法,它将数据系统的设计变得
在一个系统中统一批处理和流处理需要以下功能,这些功能也正在越来越广泛地被提供:
* 通过处理最近事件流的相同处理引擎来重播历史事件的能力。例如,基于日志的消息代理可以重播消息(请参阅 “[重播旧消息](ch11.md#重播旧消息)”),某些流处理器可以从 HDFS 等分布式文件系统读取输入。
* 对于流处理器来说,恰好一次语义 —— 即确保输出与未发生故障的输出相同,即使事实上发生故障(请参阅 “[容错](ch11.md#容错)”)。与批处理一样,这需要丢弃任何失败任务的部分输出。
* 按事件时间进行窗口化的工具,而不是按处理时间进行窗口化,因为处理历史事件时,处理时间毫无意义(请参阅 “[时间推理](ch11.md#时间推理)”。例如Apache Beam 提供了用于表达这种计算的 API可以在 Apache Flink 或 Google Cloud Dataflow 使用。
* 通过处理最近事件流的相同处理引擎来重播历史事件的能力。例如,基于日志的消息代理可以重播消息(请参阅 “[重播旧消息](/ch11#重播旧消息)”),某些流处理器可以从 HDFS 等分布式文件系统读取输入。
* 对于流处理器来说,恰好一次语义 —— 即确保输出与未发生故障的输出相同,即使事实上发生故障(请参阅 “[容错](/ch11#容错)”)。与批处理一样,这需要丢弃任何失败任务的部分输出。
* 按事件时间进行窗口化的工具,而不是按处理时间进行窗口化,因为处理历史事件时,处理时间毫无意义(请参阅 “[时间推理](/ch11#时间推理)”。例如Apache Beam 提供了用于表达这种计算的 API可以在 Apache Flink 或 Google Cloud Dataflow 使用。
## 分拆数据库
在最抽象的层面上数据库Hadoop 和操作系统都发挥相同的功能它们存储一些数据并允许你处理和查询这些数据【16】。数据库将数据存储为特定数据模型的记录表中的行、文档、图中的顶点等而操作系统的文件系统则将数据存储在文件中 —— 但其核心都是 “信息管理” 系统【17】。正如我们在 [第十章](ch10.md) 中看到的Hadoop 生态系统有点像 Unix 的分布式版本。
在最抽象的层面上数据库Hadoop 和操作系统都发挥相同的功能它们存储一些数据并允许你处理和查询这些数据【16】。数据库将数据存储为特定数据模型的记录表中的行、文档、图中的顶点等而操作系统的文件系统则将数据存储在文件中 —— 但其核心都是 “信息管理” 系统【17】。正如我们在 [第十章](/ch10) 中看到的Hadoop 生态系统有点像 Unix 的分布式版本。
当然,有很多实际的差异。例如,许多文件系统都不能很好地处理包含 1000 万个小文件的目录,而包含 1000 万个小记录的数据库完全是寻常而不起眼的。无论如何,操作系统和数据库之间的相似之处和差异值得探讨。
@@ -170,12 +172,12 @@ Unix 和关系数据库以非常不同的哲学来处理信息管理问题。Uni
在本书的过程中,我们讨论了数据库提供的各种功能及其工作原理,其中包括:
* 次级索引,使你可以根据字段的值有效地搜索记录(请参阅 “[其他索引结构](ch3.md#其他索引结构)”)
* 物化视图,这是一种预计算的查询结果缓存(请参阅 “[聚合:数据立方体和物化视图](ch3.md#聚合:数据立方体和物化视图)”)
* 复制日志,保持其他节点上数据的副本最新(请参阅 “[复制日志的实现](ch5.md#复制日志的实现)”)
* 全文搜索索引,允许在文本中进行关键字搜索(请参阅 “[全文搜索和模糊索引](ch3.md#全文搜索和模糊索引)”也内置于某些关系数据库【1】
* 次级索引,使你可以根据字段的值有效地搜索记录(请参阅 “[其他索引结构](/ch3#其他索引结构)”)
* 物化视图,这是一种预计算的查询结果缓存(请参阅 “[聚合:数据立方体和物化视图](/ch3#聚合:数据立方体和物化视图)”)
* 复制日志,保持其他节点上数据的副本最新(请参阅 “[复制日志的实现](/ch5#复制日志的实现)”)
* 全文搜索索引,允许在文本中进行关键字搜索(请参阅 “[全文搜索和模糊索引](/ch3#全文搜索和模糊索引)”也内置于某些关系数据库【1】
在 [第十章](ch10.md) 和 [第十一章](ch11.md) 中,出现了类似的主题。我们讨论了如何构建全文搜索索引(请参阅 “[批处理工作流的输出](ch10.md#批处理工作流的输出)”),了解了如何维护物化视图(请参阅 “[维护物化视图](ch11.md#维护物化视图)”)以及如何将变更从数据库复制到衍生数据系统(请参阅 “[变更数据捕获](ch11.md#变更数据捕获)”)。
在 [第十章](/ch10) 和 [第十一章](/ch11) 中,出现了类似的主题。我们讨论了如何构建全文搜索索引(请参阅 “[批处理工作流的输出](/ch10#批处理工作流的输出)”),了解了如何维护物化视图(请参阅 “[维护物化视图](/ch11#维护物化视图)”)以及如何将变更从数据库复制到衍生数据系统(请参阅 “[变更数据捕获](/ch11#变更数据捕获)”)。
数据库中内置的功能与人们用批处理和流处理器构建的衍生数据系统似乎有相似之处。
@@ -183,15 +185,15 @@ Unix 和关系数据库以非常不同的哲学来处理信息管理问题。Uni
想想当你运行 `CREATE INDEX` 在关系数据库中创建一个新的索引时会发生什么。数据库必须扫描表的一致性快照,挑选出所有被索引的字段值,对它们进行排序,然后写出索引。然后它必须处理自一致快照以来所做的写入操作(假设表在创建索引时未被锁定,所以写操作可能会继续)。一旦完成,只要事务写入表中,数据库就必须继续保持索引最新。
此过程非常类似于设置新的从库副本(请参阅 “[设置新从库](ch5.md#设置新从库)”),也非常类似于流处理系统中的 **引导bootstrap** 变更数据捕获(请参阅 “[初始快照](ch11.md#初始快照)”)。
此过程非常类似于设置新的从库副本(请参阅 “[设置新从库](/ch5#设置新从库)”),也非常类似于流处理系统中的 **引导bootstrap** 变更数据捕获(请参阅 “[初始快照](/ch11#初始快照)”)。
无论何时运行 `CREATE INDEX`,数据库都会重新处理现有数据集(如 “[应用演化后重新处理数据](#应用演化后重新处理数据)” 中所述),并将该索引作为新视图导出到现有数据上。现有数据可能是状态的快照,而不是所有发生变化的日志,但两者密切相关(请参阅 “[状态、流和不变性](ch11.md#状态、流和不变性)”)。
无论何时运行 `CREATE INDEX`,数据库都会重新处理现有数据集(如 “[应用演化后重新处理数据](#应用演化后重新处理数据)” 中所述),并将该索引作为新视图导出到现有数据上。现有数据可能是状态的快照,而不是所有发生变化的日志,但两者密切相关(请参阅 “[状态、流和不变性](/ch11#状态、流和不变性)”)。
#### 一切的元数据库
有鉴于此我认为整个组织的数据流开始像一个巨大的数据库【7】。每当批处理、流或 ETL 过程将数据从一个地方传输到另一个地方并组装时,它表现地就像数据库子系统一样,使索引或物化视图保持最新。
从这种角度来看,批处理和流处理器就像精心实现的触发器、存储过程和物化视图维护例程。它们维护的衍生数据系统就像不同的索引类型。例如,关系数据库可能支持 B 树索引、散列索引、空间索引(请参阅 “[多列索引](ch3.md#多列索引)”)以及其他类型的索引。在新兴的衍生数据系统架构中,不是将这些设施作为单个集成数据库产品的功能实现,而是由各种不同的软件提供,运行在不同的机器上,由不同的团队管理。
从这种角度来看,批处理和流处理器就像精心实现的触发器、存储过程和物化视图维护例程。它们维护的衍生数据系统就像不同的索引类型。例如,关系数据库可能支持 B 树索引、散列索引、空间索引(请参阅 “[多列索引](/ch3#多列索引)”)以及其他类型的索引。在新兴的衍生数据系统架构中,不是将这些设施作为单个集成数据库产品的功能实现,而是由各种不同的软件提供,运行在不同的机器上,由不同的团队管理。
这些发展在未来将会把我们带到哪里?如果我们从没有适合所有访问模式的单一数据模型或存储格式的前提出发,我推测有两种途径可以将不同的存储和处理工具组合成一个有凝聚力的系统:
@@ -213,20 +215,20 @@ Unix 和关系数据库以非常不同的哲学来处理信息管理问题。Uni
传统的同步写入方法需要跨异构存储系统的分布式事务【18】我认为这是错误的解决方案请参阅 “[衍生数据与分布式事务](#衍生数据与分布式事务)”)。单个存储或流处理系统内的事务是可行的,但是当数据跨越不同技术之间的边界时,我认为具有幂等写入的异步事件日志是一种更加健壮和实用的方法。
例如,分布式事务在某些流处理组件内部使用,以匹配 **恰好一次exactly-once** 语义(请参阅 “[原子提交再现](ch11.md#原子提交再现)”),这可以很好地工作。然而,当事务需要涉及由不同人群编写的系统时(例如,当数据从流处理组件写入分布式键值存储或搜索索引时),缺乏标准化的事务协议会使集成更难。有幂等消费者的有序事件日志(请参阅 “[幂等性](ch11.md#幂等性)”是一种更简单的抽象因此在异构系统中实现更加可行【7】。
例如,分布式事务在某些流处理组件内部使用,以匹配 **恰好一次exactly-once** 语义(请参阅 “[原子提交再现](/ch11#原子提交再现)”),这可以很好地工作。然而,当事务需要涉及由不同人群编写的系统时(例如,当数据从流处理组件写入分布式键值存储或搜索索引时),缺乏标准化的事务协议会使集成更难。有幂等消费者的有序事件日志(请参阅 “[幂等性](/ch11#幂等性)”是一种更简单的抽象因此在异构系统中实现更加可行【7】。
基于日志的集成的一大优势是各个组件之间的 **松散耦合loose coupling**,这体现在两个方面:
1. 在系统级别,异步事件流使整个系统在个别组件的中断或性能下降时更加稳健。如果消费者运行缓慢或失败,那么事件日志可以缓冲消息(请参阅 “[磁盘空间使用](ch11.md#磁盘空间使用)”),以便生产者和任何其他消费者可以继续不受影响地运行。有问题的消费者可以在问题修复后赶上,因此不会错过任何数据,并且包含故障。相比之下,分布式事务的同步交互往往会将本地故障升级为大规模故障(请参阅 “[分布式事务的限制](ch9.md#分布式事务的限制)”)。
1. 在系统级别,异步事件流使整个系统在个别组件的中断或性能下降时更加稳健。如果消费者运行缓慢或失败,那么事件日志可以缓冲消息(请参阅 “[磁盘空间使用](/ch11#磁盘空间使用)”),以便生产者和任何其他消费者可以继续不受影响地运行。有问题的消费者可以在问题修复后赶上,因此不会错过任何数据,并且包含故障。相比之下,分布式事务的同步交互往往会将本地故障升级为大规模故障(请参阅 “[分布式事务的限制](/ch9#分布式事务的限制)”)。
2. 在人力方面,分拆数据系统允许不同的团队独立开发,改进和维护不同的软件组件和服务。专业化使得每个团队都可以专注于做好一件事,并与其他团队的系统以明确的接口交互。事件日志提供了一个足够强大的接口,以捕获相当强的一致性属性(由于持久性和事件的顺序),但也足够普适于几乎任何类型的数据。
#### 分拆系统vs集成系统
如果分拆确实成为未来的方式,它也不会取代目前形式的数据库 —— 它们仍然会像以往一样被需要。为了维护流处理组件中的状态,数据库仍然是需要的,并且为批处理和流处理器的输出提供查询服务(请参阅 “[批处理工作流的输出](ch10.md#批处理工作流的输出)” 与 “[流处理](ch11.md#流处理)”。专用查询引擎对于特定的工作负载仍然非常重要例如MPP 数据仓库中的查询引擎针对探索性分析查询进行了优化,并且能够很好地处理这种类型的工作负载(请参阅 “[Hadoop 与分布式数据库的对比](ch10.md#Hadoop与分布式数据库的对比)”)。
如果分拆确实成为未来的方式,它也不会取代目前形式的数据库 —— 它们仍然会像以往一样被需要。为了维护流处理组件中的状态,数据库仍然是需要的,并且为批处理和流处理器的输出提供查询服务(请参阅 “[批处理工作流的输出](/ch10#批处理工作流的输出)” 与 “[流处理](/ch11#流处理)”。专用查询引擎对于特定的工作负载仍然非常重要例如MPP 数据仓库中的查询引擎针对探索性分析查询进行了优化,并且能够很好地处理这种类型的工作负载(请参阅 “[Hadoop 与分布式数据库的对比](/ch10#Hadoop与分布式数据库的对比)”)。
运行几种不同基础设施的复杂性可能是一个问题每种软件都有一个学习曲线配置问题和操作怪癖因此部署尽可能少的移动部件是很有必要的。比起使用应用代码拼接多个工具而成的系统单一集成软件产品也可以在其设计应对的工作负载类型上实现更好、更可预测的性能【23】。正如在前言中所说的那样为了不需要的规模而构建系统是白费精力而且可能会将你锁死在一个不灵活的设计中。实际上这是一种过早优化的形式。
分拆的目标不是要针对个别数据库与特定工作负载的性能进行竞争;我们的目标是允许你结合多个不同的数据库,以便在比单个软件可能实现的更广泛的工作负载范围内实现更好的性能。这是关于广度,而不是深度 —— 与我们在 “[Hadoop 与分布式数据库的对比](ch10.md#Hadoop与分布式数据库的对比)” 中讨论的存储和处理模型的多样性一样。
分拆的目标不是要针对个别数据库与特定工作负载的性能进行竞争;我们的目标是允许你结合多个不同的数据库,以便在比单个软件可能实现的更广泛的工作负载范围内实现更好的性能。这是关于广度,而不是深度 —— 与我们在 “[Hadoop 与分布式数据库的对比](/ch10#Hadoop与分布式数据库的对比)” 中讨论的存储和处理模型的多样性一样。
因此,如果有一项技术可以满足你的所有需求,那么最好使用该产品,而不是试图用更低层级的组件重新实现它。只有当没有单一软件满足你的所有需求时,才会出现拆分和联合的优势。
@@ -236,7 +238,7 @@ Unix 和关系数据库以非常不同的哲学来处理信息管理问题。Uni
例如,如果我们可以简单地声明 `mysql | elasticsearch`,类似于 Unix 管道【22】成为 `CREATE INDEX` 的分拆等价物:它将读取 MySQL 数据库中的所有文档并将其索引到 Elasticsearch 集群中。然后它会不断捕获对数据库所做的所有变更,并自动将它们应用于搜索索引,而无需编写自定义应用代码。这种集成应当支持几乎任何类型的存储或索引系统。
同样,能够更容易地预先计算和更新缓存将是一件好事。回想一下,物化视图本质上是一个预先计算的缓存,所以你可以通过为复杂查询声明指定物化视图来创建缓存,包括图上的递归查询(请参阅 “[图数据模型](ch2.md#图数据模型)”)和应用逻辑。在这方面有一些有趣的早期研究,如 **差分数据流differential dataflow**【24,25】我希望这些想法能够在生产系统中找到自己的方法。
同样,能够更容易地预先计算和更新缓存将是一件好事。回想一下,物化视图本质上是一个预先计算的缓存,所以你可以通过为复杂查询声明指定物化视图来创建缓存,包括图上的递归查询(请参阅 “[图数据模型](/ch2#图数据模型)”)和应用逻辑。在这方面有一些有趣的早期研究,如 **差分数据流differential dataflow**【24,25】我希望这些想法能够在生产系统中找到自己的方法。
### 围绕数据流设计应用
@@ -254,14 +256,14 @@ Unix 和关系数据库以非常不同的哲学来处理信息管理问题。Uni
当一个数据集衍生自另一个数据集时,它会经历某种转换函数。例如:
* 次级索引是由一种直白的转换函数生成的衍生数据集:对于基础表中的每行或每个文档,它挑选被索引的列或字段中的值,并按这些值排序(假设使用 B 树或 SSTable 索引,按键排序,如 [第三章](ch3.md) 所述)。
* 次级索引是由一种直白的转换函数生成的衍生数据集:对于基础表中的每行或每个文档,它挑选被索引的列或字段中的值,并按这些值排序(假设使用 B 树或 SSTable 索引,按键排序,如 [第三章](/ch3) 所述)。
* 全文搜索索引是通过应用各种自然语言处理函数而创建的,诸如语言检测、分词、词干或词汇化、拼写纠正和同义词识别,然后构建用于高效查找的数据结构(例如倒排索引)。
* 在机器学习系统中,我们可以将模型视作从训练数据通过应用各种特征提取、统计分析函数衍生的数据,当模型应用于新的输入数据时,模型的输出是从输入和模型(因此间接地从训练数据)中衍生的。
* 缓存通常包含将以用户界面UI显示的形式的数据聚合。因此填充缓存需要知道 UI 中引用的字段UI 中的变更可能需要更新缓存填充方式的定义,并重建缓存。
用于次级索引的衍生函数是如此常用的需求,以致于它作为核心功能被内建至许多数据库中,你可以简单地通过 `CREATE INDEX` 来调用它。对于全文索引常见语言的基本语言特征可能内置到数据库中但更复杂的特征通常需要领域特定的调整。在机器学习中特征工程是众所周知的特定于应用的特征通常需要包含很多关于用户交互与应用部署的详细知识【35】。
当创建衍生数据集的函数不是像创建次级索引那样的标准搬砖函数时,需要自定义代码来处理特定于应用的东西。而这个自定义代码是让许多数据库挣扎的地方,虽然关系数据库通常支持触发器、存储过程和用户定义的函数,可以用它们来在数据库中执行应用代码,但它们有点像数据库设计里的事后反思。(请参阅 “[传递事件流](ch11.md#传递事件流)”)。
当创建衍生数据集的函数不是像创建次级索引那样的标准搬砖函数时,需要自定义代码来处理特定于应用的东西。而这个自定义代码是让许多数据库挣扎的地方,虽然关系数据库通常支持触发器、存储过程和用户定义的函数,可以用它们来在数据库中执行应用代码,但它们有点像数据库设计里的事后反思。(请参阅 “[传递事件流](/ch11#传递事件流)”)。
#### 应用代码和状态的分离
@@ -279,19 +281,19 @@ Unix 和关系数据库以非常不同的哲学来处理信息管理问题。Uni
但是,在大多数编程语言中,你无法订阅可变变量中的变更 —— 你只能定期读取它。与电子表格不同,如果变量的值发生变化,变量的读者不会收到通知(你可以在自己的代码中实现这样的通知 —— 这被称为 **观察者模式** —— 但大多数语言没有将这种模式作为内置功能)。
数据库继承了这种可变数据的被动方法:如果你想知道数据库的内容是否发生了变化,通常你唯一的选择就是轮询(即定期重复你的查询)。订阅变更只是刚刚开始出现的功能(请参阅 “[变更流的 API 支持](ch11.md#变更流的API支持)”)。
数据库继承了这种可变数据的被动方法:如果你想知道数据库的内容是否发生了变化,通常你唯一的选择就是轮询(即定期重复你的查询)。订阅变更只是刚刚开始出现的功能(请参阅 “[变更流的 API 支持](/ch11#变更流的API支持)”)。
#### 数据流:应用代码与状态变化的交互
从数据流的角度思考应用程序,意味着重新协调应用代码和状态管理之间的关系。我们不再将数据库视作被应用操纵的被动变量,取而代之的是更多地考虑状态,状态变更和处理它们的代码之间的相互作用与协同关系。应用代码通过在另一个地方触发状态变更来响应状态变更。
我们在 “[数据库与流](ch11.md#数据库与流)” 中看到了这一思路,我们讨论了将数据库的变更日志视为一种我们可以订阅的事件流。诸如 Actor 的消息传递系统(请参阅 “[消息传递中的数据流](ch4.md#消息传递中的数据流)”)也具有响应事件的概念。早在 20 世纪 80 年代,**元组空间tuple space** 模型就已经探索了表达分布式计算的方式观察状态变更并作出反应的过程【38,39】。
我们在 “[数据库与流](/ch11#数据库与流)” 中看到了这一思路,我们讨论了将数据库的变更日志视为一种我们可以订阅的事件流。诸如 Actor 的消息传递系统(请参阅 “[消息传递中的数据流](/ch4#消息传递中的数据流)”)也具有响应事件的概念。早在 20 世纪 80 年代,**元组空间tuple space** 模型就已经探索了表达分布式计算的方式观察状态变更并作出反应的过程【38,39】。
如前所述,当触发器由于数据变更而被触发时,或次级索引更新以反映索引表中的变更时,数据库内部也发生着类似的情况。分拆数据库意味着将这个想法应用于在主数据库之外,用于创建衍生数据集:缓存、全文搜索索引、机器学习或分析系统。我们可以为此使用流处理和消息传递系统。
需要记住的重要一点是,维护衍生数据不同于执行异步任务。传统的消息传递系统通常是为执行异步任务设计的(请参阅 “[日志与传统的消息传递相比](ch11.md#日志与传统的消息传递相比)”):
需要记住的重要一点是,维护衍生数据不同于执行异步任务。传统的消息传递系统通常是为执行异步任务设计的(请参阅 “[日志与传统的消息传递相比](/ch11#日志与传统的消息传递相比)”):
* 在维护衍生数据时,状态变更的顺序通常很重要(如果多个视图是从事件日志衍生的,则需要按照相同的顺序处理事件,以便它们之间保持一致)。如 “[确认与重新传递](ch11.md#确认与重新传递)” 中所述,许多消息代理在重传未确认消息时没有此属性,双写也被排除在外(请参阅 “[保持系统同步](ch11.md#保持系统同步)”)。
* 在维护衍生数据时,状态变更的顺序通常很重要(如果多个视图是从事件日志衍生的,则需要按照相同的顺序处理事件,以便它们之间保持一致)。如 “[确认与重新传递](/ch11#确认与重新传递)” 中所述,许多消息代理在重传未确认消息时没有此属性,双写也被排除在外(请参阅 “[保持系统同步](/ch11#保持系统同步)”)。
* 容错是衍生数据的关键:仅仅丢失单个消息就会导致衍生数据集永远与其数据源失去同步。消息传递和衍生状态更新都必须可靠。例如,许多 Actor 系统默认在内存中维护 Actor 的状态和消息,所以如果运行 Actor 的机器崩溃,状态和消息就会丢失。
稳定的消息排序和容错消息处理是相当严格的要求,但与分布式事务相比,它们开销更小,运行更稳定。现代流处理组件可以提供这些排序和可靠性保证,并允许应用代码以流算子的形式运行。
@@ -300,28 +302,28 @@ Unix 和关系数据库以非常不同的哲学来处理信息管理问题。Uni
#### 流处理器和服务
当今流行的应用开发风格涉及将功能分解为一组通过同步网络请求(如 REST API进行通信的 **服务**service请参阅 “[服务中的数据流REST 与 RPC](ch4.md#服务中的数据流REST与RPC)”)。这种面向服务的架构优于单一庞大应用的优势主要在于:通过松散耦合来提供组织上的可伸缩性:不同的团队可以专职于不同的服务上,从而减少团队之间的协调工作(因为服务可以独立部署和更新)。
当今流行的应用开发风格涉及将功能分解为一组通过同步网络请求(如 REST API进行通信的 **服务**service请参阅 “[服务中的数据流REST 与 RPC](/ch4#服务中的数据流REST与RPC)”)。这种面向服务的架构优于单一庞大应用的优势主要在于:通过松散耦合来提供组织上的可伸缩性:不同的团队可以专职于不同的服务上,从而减少团队之间的协调工作(因为服务可以独立部署和更新)。
在数据流中组装流算子与微服务方法有很多相似之处【40】。但底层通信机制是有很大区别数据流采用单向异步消息流而不是同步的请求 / 响应式交互。
除了在 “[消息传递中的数据流](ch4.md#消息传递中的数据流)” 中列出的优点如更好的容错性数据流系统还能实现更好的性能。例如假设客户正在购买以一种货币定价但以另一种货币支付的商品。为了执行货币换算你需要知道当前的汇率。这个操作可以通过两种方式实现【40,41】
除了在 “[消息传递中的数据流](/ch4#消息传递中的数据流)” 中列出的优点如更好的容错性数据流系统还能实现更好的性能。例如假设客户正在购买以一种货币定价但以另一种货币支付的商品。为了执行货币换算你需要知道当前的汇率。这个操作可以通过两种方式实现【40,41】
1. 在微服务方法中,处理购买的代码可能会查询汇率服务或数据库,以获取特定货币的当前汇率。
2. 在数据流方法中,处理订单的代码会提前订阅汇率变更流,并在汇率发生变动时将当前汇率存储在本地数据库中。处理订单时只需查询本地数据库即可。
第二种方法能将对另一服务的同步网络请求替换为对本地数据库的查询(可能在同一台机器甚至同一个进程中)[^ii]。数据流方法不仅更快,而且当其他服务失效时也更稳健。最快且最可靠的网络请求就是压根没有网络请求!我们现在不再使用 RPC而是在购买事件和汇率更新事件之间建立流联接请参阅 “[流表连接(流扩充)](ch11.md#流表连接(流扩充))”)。
第二种方法能将对另一服务的同步网络请求替换为对本地数据库的查询(可能在同一台机器甚至同一个进程中)[^ii]。数据流方法不仅更快,而且当其他服务失效时也更稳健。最快且最可靠的网络请求就是压根没有网络请求!我们现在不再使用 RPC而是在购买事件和汇率更新事件之间建立流联接请参阅 “[流表连接(流扩充)](/ch11#流表连接(流扩充))”)。
[^ii]: 在微服务方法中,你也可以通过在处理购买的服务中本地缓存汇率来避免同步网络请求。但是为了保证缓存的新鲜度,你需要定期轮询汇率以获取其更新,或订阅变更流 —— 这恰好是数据流方法中发生的事情。
连接是时间相关的:如果购买事件在稍后的时间点被重新处理,汇率可能已经改变。如果要重建原始输出,则需要获取原始购买时的历史汇率。无论是查询服务还是订阅汇率更新流,你都需要处理这种时间相关性(请参阅 “[连接的时间依赖性](ch11.md#连接的时间依赖性)”)。
连接是时间相关的:如果购买事件在稍后的时间点被重新处理,汇率可能已经改变。如果要重建原始输出,则需要获取原始购买时的历史汇率。无论是查询服务还是订阅汇率更新流,你都需要处理这种时间相关性(请参阅 “[连接的时间依赖性](/ch11#连接的时间依赖性)”)。
订阅变更流,而不是在需要时查询当前状态,使我们更接近类似电子表格的计算模型:当某些数据发生变更时,依赖于此的所有衍生数据都可以快速更新。还有很多未解决的问题,例如关于时间相关连接等问题,但我认为围绕数据流构建应用的想法是一个非常有希望的方向。
### 观察衍生数据状态
在抽象层面,上一节讨论的数据流系统提供了创建衍生数据集(例如搜索索引、物化视图和预测模型)并使其保持更新的过程。我们将这个过程称为 **写路径write path**:只要某些信息被写入系统,它可能会经历批处理与流处理的多个阶段,而最终每个衍生数据集都会被更新,以适配写入的数据。[图 12-1](img/fig12-1.png) 显示了一个更新搜索索引的例子。
在抽象层面,上一节讨论的数据流系统提供了创建衍生数据集(例如搜索索引、物化视图和预测模型)并使其保持更新的过程。我们将这个过程称为 **写路径write path**:只要某些信息被写入系统,它可能会经历批处理与流处理的多个阶段,而最终每个衍生数据集都会被更新,以适配写入的数据。[图 12-1](/img/fig12-1.png) 显示了一个更新搜索索引的例子。
![](img/fig12-1.png)
![](/img/fig12-1.png)
**图 12-1 在搜索索引中,写(文档更新)遇上读(查询)**
@@ -329,7 +331,7 @@ Unix 和关系数据库以非常不同的哲学来处理信息管理问题。Uni
总而言之,写路径和读路径涵盖了数据的整个旅程,从收集数据开始,到使用数据结束(可能是由另一个人)。写路径是预计算过程的一部分 —— 即,一旦数据进入,即刻完成,无论是否有人需要看它。读路径是这个过程中只有当有人请求时才会发生的部分。如果你熟悉函数式编程语言,则可能会注意到写路径类似于立即求值,读路径类似于惰性求值。
如 [图 12-1](img/fig12-1.png) 所示,衍生数据集是写路径和读路径相遇的地方。它代表了在写入时需要完成的工作量与在读取时需要完成的工作量之间的权衡。
如 [图 12-1](/img/fig12-1.png) 所示,衍生数据集是写路径和读路径相遇的地方。它代表了在写入时需要完成的工作量与在读取时需要完成的工作量之间的权衡。
#### 物化视图和缓存
@@ -345,7 +347,7 @@ Unix 和关系数据库以非常不同的哲学来处理信息管理问题。Uni
从这个例子中我们可以看到,索引不是写路径和读路径之间唯一可能的边界;缓存常见搜索结果也是可行的;而在少量文档上使用没有索引的类 grep 扫描也是可行的。由此来看,缓存,索引和物化视图的作用很简单:它们改变了读路径与写路径之间的边界。通过预先计算结果,从而允许我们在写路径上做更多的工作,以节省读路径上的工作量。
在写路径上完成的工作和读路径之间的界限,实际上是本书开始处在 “[描述负载](ch1.md#描述负载)” 中推特例子里谈到的主题。在该例中,我们还看到了与普通用户相比,名人的写路径和读路径可能有所不同。在 500 页之后,我们已经绕回了起点!
在写路径上完成的工作和读路径之间的界限,实际上是本书开始处在 “[描述负载](/ch1#描述负载)” 中推特例子里谈到的主题。在该例中,我们还看到了与普通用户相比,名人的写路径和读路径可能有所不同。在 500 页之后,我们已经绕回了起点!
#### 有状态、可离线的客户端
@@ -355,7 +357,7 @@ Unix 和关系数据库以非常不同的哲学来处理信息管理问题。Uni
传统上,网络浏览器是无状态的客户端,只有当连接到互联网时才能做一些有用的事情(能离线执行的唯一事情基本上就是上下滚动之前在线时加载好的页面)。然而,最近的 “单页面” JavaScript Web 应用已经获得了很多有状态的功能,包括客户端用户界面交互,以及 Web 浏览器中的持久化本地存储。移动应用可以类似地在设备上存储大量状态,而且大多数用户交互都不需要与服务器往返交互。
这些不断变化的功能重新引发了对 **离线优先offline-first** 应用的兴趣这些应用尽可能地在同一设备上使用本地数据库无需连接互联网并在后台网络连接可用时与远程服务器同步【42】。由于移动设备通常具有缓慢且不可靠的蜂窝网络连接因此如果用户的用户界面不必等待同步网络请求且应用主要是离线工作的则这是一个巨大优势请参阅 “[需要离线操作的客户端](ch5.md#需要离线操作的客户端)”)。
这些不断变化的功能重新引发了对 **离线优先offline-first** 应用的兴趣这些应用尽可能地在同一设备上使用本地数据库无需连接互联网并在后台网络连接可用时与远程服务器同步【42】。由于移动设备通常具有缓慢且不可靠的蜂窝网络连接因此如果用户的用户界面不必等待同步网络请求且应用主要是离线工作的则这是一个巨大优势请参阅 “[需要离线操作的客户端](/ch5#需要离线操作的客户端)”)。
当我们摆脱无状态客户端与中央数据库交互的假设,并转向在终端用户设备上维护状态时,这就开启了新世界的大门。特别是,我们可以将设备上的状态视为 **服务器状态的缓存**。屏幕上的像素是客户端应用中模型对象的物化视图模型对象是远程数据中心的本地状态副本【27】。
@@ -367,17 +369,17 @@ Unix 和关系数据库以非常不同的哲学来处理信息管理问题。Uni
用我们的写路径与读路径模型来讲主动将状态变更推至到客户端设备意味着将写路径一直延伸到终端用户。当客户端首次初始化时它仍然需要使用读路径来获取其初始状态但此后它就能够依赖服务器发送的状态变更流了。我们在流处理和消息传递部分讨论的想法并不局限于数据中心中我们可以进一步采纳这些想法并将它们一直延伸到终端用户设备【43】。
这些设备有时会离线,并在此期间无法收到服务器状态变更的任何通知。但是我们已经解决了这个问题:在 “[消费者偏移量](ch11.md#消费者偏移量)” 中,我们讨论了基于日志的消息代理的消费者能在失败或断开连接后重连,并确保它不会错过掉线期间任何到达的消息。同样的技术适用于单个用户,每个设备都是一个小事件流的小小订阅者。
这些设备有时会离线,并在此期间无法收到服务器状态变更的任何通知。但是我们已经解决了这个问题:在 “[消费者偏移量](/ch11#消费者偏移量)” 中,我们讨论了基于日志的消息代理的消费者能在失败或断开连接后重连,并确保它不会错过掉线期间任何到达的消息。同样的技术适用于单个用户,每个设备都是一个小事件流的小小订阅者。
#### 端到端的事件流
最近用于开发有状态的客户端与用户界面的工具,例如如 Elm 语言【30】和 Facebook 的 React、Flux 和 Redux 工具链,已经通过订阅表示用户输入或服务器响应的事件流来管理客户端的内部状态,其结构与事件溯源相似(请参阅 “[事件溯源](ch11.md#事件溯源)”)。
最近用于开发有状态的客户端与用户界面的工具,例如如 Elm 语言【30】和 Facebook 的 React、Flux 和 Redux 工具链,已经通过订阅表示用户输入或服务器响应的事件流来管理客户端的内部状态,其结构与事件溯源相似(请参阅 “[事件溯源](/ch11#事件溯源)”)。
将这种编程模型扩展为:允许服务器将状态变更事件推送到客户端的事件管道中,是非常自然的。因此,状态变化可以通过 **端到端end-to-end** 的写路径流动:从一个设备上的交互触发状态变更开始,经由事件日志,并穿过几个衍生数据系统与流处理器,一直到另一台设备上的用户界面,而有人正在观察用户界面上的状态变化。这些状态变化能以相当低的延迟传播 —— 比如说,在一秒内从一端到另一端。
一些应用(如即时消息传递与在线游戏)已经具有这种 “实时” 架构(在低延迟交互的意义上,不是在 “[响应时间保证](ch8.md#响应时间保证)” 中的意义上)。但我们为什么不用这种方式构建所有的应用?
一些应用(如即时消息传递与在线游戏)已经具有这种 “实时” 架构(在低延迟交互的意义上,不是在 “[响应时间保证](/ch8#响应时间保证)” 中的意义上)。但我们为什么不用这种方式构建所有的应用?
挑战在于,关于无状态客户端和请求 / 响应交互的假设已经根深蒂固地植入在我们的数据库、库、框架以及协议之中。许多数据存储支持读取与写入操作,为请求返回一个响应,但只有极少数提供订阅变更的能力 —— 请求返回一个随时间推移的响应流(请参阅 “[变更流的 API 支持](ch11.md#变更流的API支持)” )。
挑战在于,关于无状态客户端和请求 / 响应交互的假设已经根深蒂固地植入在我们的数据库、库、框架以及协议之中。许多数据存储支持读取与写入操作,为请求返回一个响应,但只有极少数提供订阅变更的能力 —— 请求返回一个随时间推移的响应流(请参阅 “[变更流的 API 支持](/ch11#变更流的API支持)” )。
为了将写路径延伸至终端用户,我们需要从根本上重新思考我们构建这些系统的方式:从请求 / 响应交互转向发布 / 订阅数据流【27】。更具响应性的用户界面与更好的离线支持我认为这些优势值得我们付出努力。如果你正在设计数据系统我希望你对订阅变更的选项留有印象而不只是查询当前状态。
@@ -385,11 +387,11 @@ Unix 和关系数据库以非常不同的哲学来处理信息管理问题。Uni
我们讨论过,当流处理器将衍生数据写入存储(数据库,缓存或索引)时,以及当用户请求查询该存储时,存储将充当写路径和读路径之间的边界。该存储应当允许对数据进行随机访问的读取查询,否则这些查询将需要扫描整个事件日志。
在很多情况下,数据存储与流处理系统是分开的。但回想一下,流处理器还是需要维护状态以执行聚合和连接的(请参阅 “[流连接](ch11.md#流连接)”。这种状态通常隐藏在流处理器内部但一些框架也允许这些状态被外部客户端查询【45】将流处理器本身变成一种简单的数据库。
在很多情况下,数据存储与流处理系统是分开的。但回想一下,流处理器还是需要维护状态以执行聚合和连接的(请参阅 “[流连接](/ch11#流连接)”。这种状态通常隐藏在流处理器内部但一些框架也允许这些状态被外部客户端查询【45】将流处理器本身变成一种简单的数据库。
我愿意进一步思考这个想法。正如到目前为止所讨论的那样对存储的写入是通过事件日志进行的而读取是临时的网络请求直接流向存储着待查数据的节点。这是一个合理的设计但不是唯一可行的设计。也可以将读取请求表示为事件流并同时将读事件与写事件送往流处理器流处理器通过将读取结果发送到输出流来响应读取事件【46】。
当写入和读取都被表示为事件,并且被路由到同一个流算子以便处理时,我们实际上是在读取查询流和数据库之间执行流表连接。读取事件需要被送往保存数据的数据库分区(请参阅 “[请求路由](ch6.md#请求路由)”),就像批处理和流处理器在连接时需要在同一个键上对输入分区一样(请参阅 “[Reduce 侧连接与分组](ch10.md#Reduce侧连接与分组)”)。
当写入和读取都被表示为事件,并且被路由到同一个流算子以便处理时,我们实际上是在读取查询流和数据库之间执行流表连接。读取事件需要被送往保存数据的数据库分区(请参阅 “[请求路由](/ch6#请求路由)”),就像批处理和流处理器在连接时需要在同一个键上对输入分区一样(请参阅 “[Reduce 侧连接与分组](/ch10#Reduce侧连接与分组)”)。
服务请求与执行连接之间的这种相似之处是非常关键的【47】。一次性读取请求只是将请求传过连接算子然后请求马上就被忘掉了而一个订阅请求则是与连接另一侧过去与未来事件的持久化连接。
@@ -401,20 +403,20 @@ Unix 和关系数据库以非常不同的哲学来处理信息管理问题。Uni
对于只涉及单个分区的查询,通过流来发送查询与收集响应可能是杀鸡用牛刀了。然而,这个想法开启了分布式执行复杂查询的可能性,这需要合并来自多个分区的数据,利用了流处理器已经提供的消息路由、分区和连接的基础设施。
Storm 的分布式 RPC 功能支持这种使用模式(请参阅 “[消息传递和 RPC](ch11.md#消息传递和RPC)”)。例如,它已经被用来计算浏览过某个推特 URL 的人数 —— 即,发推包含该 URL 的所有人的粉丝集合的并集【48】。由于推特的用户是分区的因此这种计算需要合并来自多个分区的结果。
Storm 的分布式 RPC 功能支持这种使用模式(请参阅 “[消息传递和 RPC](/ch11#消息传递和RPC)”)。例如,它已经被用来计算浏览过某个推特 URL 的人数 —— 即,发推包含该 URL 的所有人的粉丝集合的并集【48】。由于推特的用户是分区的因此这种计算需要合并来自多个分区的结果。
这种模式的另一个例子是欺诈预防:为了评估特定购买事件是否具有欺诈风险,你可以检查该用户 IP 地址电子邮件地址帐单地址送货地址的信用分。这些信用数据库中的每一个都是有分区的因此为特定购买事件采集分数需要连接一系列不同的分区数据集【49】。
MPP 数据库的内部查询执行图有着类似的特征(请参阅 “[Hadoop 与分布式数据库的对比](ch10.md#Hadoop与分布式数据库的对比)”)。如果需要执行这种多分区连接,则直接使用提供此功能的数据库,可能要比使用流处理器实现它要更简单。然而将查询视为流提供了一种选项,可以用于实现超出传统现成解决方案的大规模应用。
MPP 数据库的内部查询执行图有着类似的特征(请参阅 “[Hadoop 与分布式数据库的对比](/ch10#Hadoop与分布式数据库的对比)”)。如果需要执行这种多分区连接,则直接使用提供此功能的数据库,可能要比使用流处理器实现它要更简单。然而将查询视为流提供了一种选项,可以用于实现超出传统现成解决方案的大规模应用。
## 将事情做正确
对于只读取数据的无状态服务出问题也没什么大不了的你可以修复该错误并重启服务而一切都恢复正常。像数据库这样的有状态系统就没那么简单了它们被设计为永远记住事物或多或少所以如果出现问题这种错误的效果也将潜在地永远持续下去这意味着它们需要更仔细的思考【50】。
我们希望构建可靠且 **正确** 的应用(即使面对各种故障,程序的语义也能被很好地定义与理解)。约四十年来,原子性、隔离性和持久性([第七章](ch7.md))等事务特性一直是构建正确应用的首选工具。然而这些地基没有看上去那么牢固:例如弱隔离级别带来的困惑可以佐证(请参阅 “[弱隔离级别](ch7.md#弱隔离级别)”)。
我们希望构建可靠且 **正确** 的应用(即使面对各种故障,程序的语义也能被很好地定义与理解)。约四十年来,原子性、隔离性和持久性([第七章](/ch7))等事务特性一直是构建正确应用的首选工具。然而这些地基没有看上去那么牢固:例如弱隔离级别带来的困惑可以佐证(请参阅 “[弱隔离级别](/ch7#弱隔离级别)”)。
事务在某些领域被完全抛弃,并被提供更好性能与可伸缩性的模型取代,但后者有更复杂的语义(例如,请参阅 “[无主复制](ch5.md#无主复制)”)。**一致性Consistency** 经常被谈起,但其定义并不明确(请参阅 “[一致性](ch7.md#一致性)” 和 [第九章](ch9.md))。有些人断言我们应当为了高可用而 “拥抱弱一致性”,但却对这些概念实际上意味着什么缺乏清晰的认识。
事务在某些领域被完全抛弃,并被提供更好性能与可伸缩性的模型取代,但后者有更复杂的语义(例如,请参阅 “[无主复制](/ch5#无主复制)”)。**一致性Consistency** 经常被谈起,但其定义并不明确(请参阅 “[一致性](/ch7#一致性)” 和 [第九章](/ch9))。有些人断言我们应当为了高可用而 “拥抱弱一致性”,但却对这些概念实际上意味着什么缺乏清晰的认识。
对于如此重要的话题我们的理解以及我们的工程方法却是惊人地薄弱。例如确定在特定事务隔离等级或复制配置下运行特定应用是否安全是非常困难的【51,52】。通常简单的解决方案似乎在低并发性的情况下工作正常并且没有错误但在要求更高的情况下却会出现许多微妙的错误。
@@ -428,23 +430,23 @@ MPP 数据库的内部查询执行图有着类似的特征(请参阅 “[Hadoo
仅仅因为一个应用程序使用了具有相对较强安全属性的数据系统(例如可串行化的事务),并不意味着就可以保证没有数据丢失或损坏。例如,如果某个应用有个 Bug导致它写入不正确的数据或者从数据库中删除数据那么可串行化的事务也救不了你。
这个例子可能看起来很无聊,但值得认真对待:应用会出 Bug而人也会犯错误。我在 “[状态、流和不变性](ch11.md#状态、流和不变性)” 中使用了这个例子来支持不可变和仅追加的数据,阉割掉错误代码摧毁良好数据的能力,能让从错误中恢复更为容易。
这个例子可能看起来很无聊,但值得认真对待:应用会出 Bug而人也会犯错误。我在 “[状态、流和不变性](/ch11#状态、流和不变性)” 中使用了这个例子来支持不可变和仅追加的数据,阉割掉错误代码摧毁良好数据的能力,能让从错误中恢复更为容易。
虽然不变性很有用,但它本身并非万灵药。让我们来看一个可能发生的、非常微妙的数据损坏案例。
#### 正好执行一次操作
在 “[容错](ch11.md#容错)” 中,我们见到了 **恰好一次**(或 **等效一次**)语义的概念。如果在处理消息时出现问题,你可以选择放弃(丢弃消息 —— 导致数据丢失)或重试。如果重试,就会有这种风险:第一次实际上成功了,只不过你没有发现。结果这个消息就被处理了两次。
在 “[容错](/ch11#容错)” 中,我们见到了 **恰好一次**(或 **等效一次**)语义的概念。如果在处理消息时出现问题,你可以选择放弃(丢弃消息 —— 导致数据丢失)或重试。如果重试,就会有这种风险:第一次实际上成功了,只不过你没有发现。结果这个消息就被处理了两次。
处理两次是数据损坏的一种形式:为同样的服务向客户收费两次(收费太多)或增长计数器两次(夸大指标)都不是我们想要的。在这种情况下,恰好一次意味着安排计算,使得最终效果与没有发生错误的情况一样,即使操作实际上因为某种错误而重试。我们先前讨论过实现这一目标的几种方法。
最有效的方法之一是使操作 **幂等**idempotent请参阅 “[幂等性](ch11.md#幂等性)”):即确保它无论是执行一次还是执行多次都具有相同的效果。但是,将不是天生幂等的操作变为幂等的操作需要一些额外的努力与关注:你可能需要维护一些额外的元数据(例如更新了值的操作 ID 集合),并在从一个节点故障切换至另一个节点时做好防护(请参阅 “[领导者和锁](ch8.md#领导者和锁)”)。
最有效的方法之一是使操作 **幂等**idempotent请参阅 “[幂等性](/ch11#幂等性)”):即确保它无论是执行一次还是执行多次都具有相同的效果。但是,将不是天生幂等的操作变为幂等的操作需要一些额外的努力与关注:你可能需要维护一些额外的元数据(例如更新了值的操作 ID 集合),并在从一个节点故障切换至另一个节点时做好防护(请参阅 “[领导者和锁](/ch8#领导者和锁)”)。
#### 抑制重复
除了流处理之外其他许多地方也需要抑制重复的模式。例如TCP 使用了数据包上的序列号以便接收方可以将它们正确排序并确定网络上是否有数据包丢失或重复。在将数据交付应用前TCP 协议栈会重新传输任何丢失的数据包,也会移除任何重复的数据包。
但是,这种重复抑制仅适用于单条 TCP 连接的场景中。假设 TCP 连接是一个客户端与数据库的连接,并且它正在执行 [例 12-1]() 中的事务。在许多数据库中,事务是绑定在客户端连接上的(如果客户端发送了多个查询,数据库就知道它们属于同一个事务,因为它们是在同一个 TCP 连接上发送的)。如果客户端在发送 `COMMIT` 之后并在从数据库服务器收到响应之前遇到网络中断与连接超时,客户端是不知道事务是否已经被提交的([图 8-1](img/fig8-1.png))。
但是,这种重复抑制仅适用于单条 TCP 连接的场景中。假设 TCP 连接是一个客户端与数据库的连接,并且它正在执行 [例 12-1]() 中的事务。在许多数据库中,事务是绑定在客户端连接上的(如果客户端发送了多个查询,数据库就知道它们属于同一个事务,因为它们是在同一个 TCP 连接上发送的)。如果客户端在发送 `COMMIT` 之后并在从数据库服务器收到响应之前遇到网络中断与连接超时,客户端是不知道事务是否已经被提交的([图 8-1](/img/fig8-1.png))。
**例 12-1 资金从一个账户到另一个账户的非幂等转移**
@@ -457,7 +459,7 @@ COMMIT;
客户端可以重连到数据库并重试事务,但现在已经处于 TCP 重复抑制的范围之外了。因为 [例 12-1]() 中的事务不是幂等的,可能会发生转了 \$22 而不是期望的 \$11。因此尽管 [例 12-1]() 是一个事务原子性的标准样例但它实际上并不正确而真正的银行并不会这样办事【3】。
两阶段提交(请参阅 “[原子提交与两阶段提交](ch9.md#原子提交与两阶段提交)”)协议会破坏 TCP 连接与事务之间的 1:1 映射,因为它们必须在故障后允许事务协调器重连到数据库,告诉数据库将存疑事务提交还是中止。这足以确保事务只被恰好执行一次吗?不幸的是,并不能。
两阶段提交(请参阅 “[原子提交与两阶段提交](/ch9#原子提交与两阶段提交)”)协议会破坏 TCP 连接与事务之间的 1:1 映射,因为它们必须在故障后允许事务协调器重连到数据库,告诉数据库将存疑事务提交还是中止。这足以确保事务只被恰好执行一次吗?不幸的是,并不能。
即使我们可以抑制数据库客户端与服务器之间的重复事务,我们仍然需要担心终端用户设备与应用服务器之间的网络。例如,如果终端用户的客户端是 Web 浏览器,则它可能会使用 HTTP POST 请求向服务器提交指令。也许用户正处于一个信号微弱的蜂窝数据网络连接中,它们成功地发送了 POST但却在能够从服务器接收响应之前没了信号。
@@ -482,9 +484,9 @@ BEGIN TRANSACTION;
COMMIT;
```
[例 12-2]() 依赖于 `request_id` 列上的唯一约束。如果一个事务尝试插入一个已经存在的 ID那么 `INSERT` 失败,事务被中止,使其无法生效两次。即使在较弱的隔离级别下,关系数据库也能正确地维护唯一性约束(而在 “[写入偏差与幻读](ch7.md#写入偏差与幻读)” 中讨论过,应用级别的 **检查 - 然后 - 插入** 可能会在不可串行化的隔离下失败)。
[例 12-2]() 依赖于 `request_id` 列上的唯一约束。如果一个事务尝试插入一个已经存在的 ID那么 `INSERT` 失败,事务被中止,使其无法生效两次。即使在较弱的隔离级别下,关系数据库也能正确地维护唯一性约束(而在 “[写入偏差与幻读](/ch7#写入偏差与幻读)” 中讨论过,应用级别的 **检查 - 然后 - 插入** 可能会在不可串行化的隔离下失败)。
除了抑制重复的请求之外,[例 12-2]() 中的请求表表现得就像一种事件日志,暗示着事件溯源的想法(请参阅 “[事件溯源](ch11.md#事件溯源)”)。更新账户余额事实上不必与插入事件发生在同一个事务中,因为它们是冗余的,而能由下游消费者从请求事件中衍生出来 —— 只要该事件被恰好处理一次,这又一次可以使用请求 ID 来强制执行。
除了抑制重复的请求之外,[例 12-2]() 中的请求表表现得就像一种事件日志,暗示着事件溯源的想法(请参阅 “[事件溯源](/ch11#事件溯源)”)。更新账户余额事实上不必与插入事件发生在同一个事务中,因为它们是冗余的,而能由下游消费者从请求事件中衍生出来 —— 只要该事件被恰好处理一次,这又一次可以使用请求 ID 来强制执行。
#### 端到端原则
@@ -507,9 +509,9 @@ COMMIT;
这实在是一个遗憾,因为容错机制很难弄好。低层级的可靠机制(比如 TCP 中的那些)运行的相当好,因而剩下的高层级错误基本很少出现。如果能将这些剩下的高层级容错机制打包成抽象,而应用不需要再去操心,那该多好呀 —— 但恐怕我们还没有找到这一正确的抽象。
长期以来,事务被认为是一个很好的抽象,我相信它们确实是很有用的。正如 [第七章](ch7.md) 导言中所讨论的,它们将各种可能的问题(并发写入、违背约束、崩溃、网络中断、磁盘故障)合并为两种可能结果:提交或中止。这是对编程模型而言是一种巨大的简化,但恐怕这还不够。
长期以来,事务被认为是一个很好的抽象,我相信它们确实是很有用的。正如 [第七章](/ch7) 导言中所讨论的,它们将各种可能的问题(并发写入、违背约束、崩溃、网络中断、磁盘故障)合并为两种可能结果:提交或中止。这是对编程模型而言是一种巨大的简化,但恐怕这还不够。
事务是代价高昂的,当涉及异构存储技术时尤为甚(请参阅 “[实践中的分布式事务](ch9.md#实践中的分布式事务)”)。我们拒绝使用分布式事务是因为它开销太大,结果我们最后不得不在应用代码中重新实现容错机制。正如本书中大量的例子所示,对并发性与部分失败的推理是困难且违反直觉的,所以我怀疑大多数应用级别的机制都不能正确工作,最终结果是数据丢失或损坏。
事务是代价高昂的,当涉及异构存储技术时尤为甚(请参阅 “[实践中的分布式事务](/ch9#实践中的分布式事务)”)。我们拒绝使用分布式事务是因为它开销太大,结果我们最后不得不在应用代码中重新实现容错机制。正如本书中大量的例子所示,对并发性与部分失败的推理是困难且违反直觉的,所以我怀疑大多数应用级别的机制都不能正确工作,最终结果是数据丢失或损坏。
出于这些原因,我认为探索对容错的抽象是很有价值的。它使提供应用特定的端到端的正确性属性变得更简单,而且还能在大规模分布式环境中提供良好的性能与运维特性。
@@ -517,33 +519,33 @@ COMMIT;
让我们思考一下在 [分拆数据库](#分拆数据库) 上下文中的 **正确性correctness**。我们看到端到端的除重可以通过从客户端一路透传到数据库的请求 ID 实现。那么其他类型的约束呢?
我们先来特别关注一下 **唯一性约束** —— 例如我们在 [例 12-2]() 中所依赖的约束。在 “[约束和唯一性保证](ch9.md#约束和唯一性保证)” 中,我们看到了几个其他需要强制实施唯一性的应用功能例子:用户名或电子邮件地址必须唯一标识用户,文件存储服务不能包含多个重名文件,两个人不能在航班或剧院预订同一个座位。
我们先来特别关注一下 **唯一性约束** —— 例如我们在 [例 12-2]() 中所依赖的约束。在 “[约束和唯一性保证](/ch9#约束和唯一性保证)” 中,我们看到了几个其他需要强制实施唯一性的应用功能例子:用户名或电子邮件地址必须唯一标识用户,文件存储服务不能包含多个重名文件,两个人不能在航班或剧院预订同一个座位。
其他类型的约束也非常类似:例如,确保帐户余额永远不会变为负数,确保不会超卖库存,或者会议室没有重复的预订。执行唯一性约束的技术通常也可以用于这些约束。
#### 唯一性约束需要达成共识
在 [第九章](ch9.md) 中我们看到,在分布式环境中,强制执行唯一性约束需要共识:如果存在多个具有相同值的并发请求,则系统需要决定冲突操作中的哪一个被接受,并拒绝其他违背约束的操作。
在 [第九章](/ch9) 中我们看到,在分布式环境中,强制执行唯一性约束需要共识:如果存在多个具有相同值的并发请求,则系统需要决定冲突操作中的哪一个被接受,并拒绝其他违背约束的操作。
达成这一共识的最常见方式是使单个节点作为领导,并使其负责所有决策。只要你不介意所有请求都挤过单个节点(即使客户端位于世界的另一端),只要该节点没有失效,系统就能正常工作。如果你需要容忍领导者失效,那么就又回到了共识问题(请参阅 “[单主复制与共识](ch9.md#单主复制与共识)”)。
达成这一共识的最常见方式是使单个节点作为领导,并使其负责所有决策。只要你不介意所有请求都挤过单个节点(即使客户端位于世界的另一端),只要该节点没有失效,系统就能正常工作。如果你需要容忍领导者失效,那么就又回到了共识问题(请参阅 “[单主复制与共识](/ch9#单主复制与共识)”)。
唯一性检查可以通过对唯一性字段分区做横向伸缩。例如,如果需要通过请求 ID 确保唯一性(如 [例 12-2]() 所示),你可以确保所有具有相同请求 ID 的请求都被路由到同一分区(请参阅 [第六章](ch6.md))。如果你需要让用户名是唯一的,则可以按用户名的散列值做分区。
唯一性检查可以通过对唯一性字段分区做横向伸缩。例如,如果需要通过请求 ID 确保唯一性(如 [例 12-2]() 所示),你可以确保所有具有相同请求 ID 的请求都被路由到同一分区(请参阅 [第六章](/ch6))。如果你需要让用户名是唯一的,则可以按用户名的散列值做分区。
但异步多主复制排除在外,因为可能会发生不同主库同时接受冲突写操作的情况,因而这些值不再是唯一的(请参阅 “[实现线性一致的系统](ch9.md#实现线性一致的系统)”。如果你想立刻拒绝任何违背约束的写入同步协调是无法避免的【56】。
但异步多主复制排除在外,因为可能会发生不同主库同时接受冲突写操作的情况,因而这些值不再是唯一的(请参阅 “[实现线性一致的系统](/ch9#实现线性一致的系统)”。如果你想立刻拒绝任何违背约束的写入同步协调是无法避免的【56】。
#### 基于日志消息传递中的唯一性
日志确保所有消费者以相同的顺序看见消息 —— 这种保证在形式上被称为 **全序广播total order boardcast** 并且等价于共识(请参阅 “[全序广播](ch9.md#全序广播)”)。在使用基于日志的消息传递的分拆数据库方法中,我们可以使用非常类似的方法来执行唯一性约束。
日志确保所有消费者以相同的顺序看见消息 —— 这种保证在形式上被称为 **全序广播total order boardcast** 并且等价于共识(请参阅 “[全序广播](/ch9#全序广播)”)。在使用基于日志的消息传递的分拆数据库方法中,我们可以使用非常类似的方法来执行唯一性约束。
流处理器在单个线程上依次消费单个日志分区中的所有消息(请参阅 “[日志与传统的消息传递相比](ch11.md#日志与传统的消息传递相比)”。因此如果日志是按需要确保唯一的值做的分区则流处理器可以无歧义地、确定性地决定几个冲突操作中的哪一个先到达。例如在多个用户尝试宣告相同用户名的情况下【57】
流处理器在单个线程上依次消费单个日志分区中的所有消息(请参阅 “[日志与传统的消息传递相比](/ch11#日志与传统的消息传递相比)”。因此如果日志是按需要确保唯一的值做的分区则流处理器可以无歧义地、确定性地决定几个冲突操作中的哪一个先到达。例如在多个用户尝试宣告相同用户名的情况下【57】
1. 每个对用户名的请求都被编码为一条消息,并追加到按用户名散列值确定的分区。
2. 流处理器依序读取日志中的请求,并使用本地数据库来追踪哪些用户名已经被占用了。对于所有申请可用用户名的请求,它都会记录该用户名,并向输出流发送一条成功消息。对于所有申请已占用用户名的请求,它都会向输出流发送一条拒绝消息。
3. 请求用户名的客户端监视输出流,等待与其请求相对应的成功或拒绝消息。
该算法基本上与 “[使用全序广播实现线性一致的存储](ch9.md#使用全序广播实现线性一致的存储)” 中的算法相同。它可以简单地通过增加分区数伸缩至较大的请求吞吐量,因为每个分区都可以被独立处理。
该算法基本上与 “[使用全序广播实现线性一致的存储](/ch9#使用全序广播实现线性一致的存储)” 中的算法相同。它可以简单地通过增加分区数伸缩至较大的请求吞吐量,因为每个分区都可以被独立处理。
该方法不仅适用于唯一性约束,而且适用于许多其他类型的约束。其基本原理是,任何可能冲突的写入都会路由到相同的分区并按顺序处理。正如 “[什么是冲突?](ch5.md#什么是冲突?)” 与 “[写入偏差与幻读](ch7.md#写入偏差与幻读)” 中所述,冲突的定义可能取决于应用,但流处理器可以使用任意逻辑来验证请求。这个想法与 Bayou 在 90 年代开创的方法类似【58】。
该方法不仅适用于唯一性约束,而且适用于许多其他类型的约束。其基本原理是,任何可能冲突的写入都会路由到相同的分区并按顺序处理。正如 “[什么是冲突?](/ch5#什么是冲突?)” 与 “[写入偏差与幻读](/ch7#写入偏差与幻读)” 中所述,冲突的定义可能取决于应用,但流处理器可以使用任意逻辑来验证请求。这个想法与 Bayou 在 90 年代开创的方法类似【58】。
#### 多分区请求处理
@@ -557,17 +559,17 @@ COMMIT;
2. 流处理器读取请求日志。对于每个请求消息,它向输出流发出两条消息:付款人账户 A 的借记指令(按 A 分区),收款人 B 的贷记指令(按 B 分区)。被发出的消息中会带有原始的请求 ID。
3. 后续处理器消费借记 / 贷记指令流,按照请求 ID 除重,并将变更应用至账户余额。
步骤 1 和步骤 2 是必要的,因为如果客户直接发送贷记与借记指令,则需要在这两个分区之间进行原子提交,以确保两者要么都发生或都不发生。为了避免对分布式事务的需要,我们首先将请求持久化记录为单条消息,然后从这第一条消息中衍生出贷记指令与借记指令。几乎在所有数据系统中,单对象写入都是原子性的(请参阅 “[单对象写入](ch7.md#单对象写入)),因此请求要么出现在日志中,要么就不出现,无需多分区原子提交。
步骤 1 和步骤 2 是必要的,因为如果客户直接发送贷记与借记指令,则需要在这两个分区之间进行原子提交,以确保两者要么都发生或都不发生。为了避免对分布式事务的需要,我们首先将请求持久化记录为单条消息,然后从这第一条消息中衍生出贷记指令与借记指令。几乎在所有数据系统中,单对象写入都是原子性的(请参阅 “[单对象写入](/ch7#单对象写入)),因此请求要么出现在日志中,要么就不出现,无需多分区原子提交。
如果流处理器在步骤 2 中崩溃,则它会从上一个存档点恢复处理。这样做时,它不会跳过任何请求消息,但可能会多次处理请求并产生重复的贷记与借记指令。但由于它是确定性的,因此它只是再次生成相同的指令,而步骤 3 中的处理器可以使用端到端请求 ID 轻松地对其除重。
如果你想确保付款人的帐户不会因此次转账而透支,则可以使用一个额外的流处理器来维护账户余额并校验事务(按付款人账户分区),只有有效的事务会被记录在步骤 1 中的请求日志中。
通过将多分区事务分解为两个不同分区方式的阶段,并使用端到端的请求 ID我们实现了同样的正确性属性每个请求对付款人与收款人都恰好生效一次即使在出现故障且没有使用原子提交协议的情况下依然如此。使用多个不同分区阶段的想法与我们在 “[多分区数据处理](#多分区数据处理)” 中讨论的想法类似(也请参阅 “[并发控制](ch11.md#并发控制)”)。
通过将多分区事务分解为两个不同分区方式的阶段,并使用端到端的请求 ID我们实现了同样的正确性属性每个请求对付款人与收款人都恰好生效一次即使在出现故障且没有使用原子提交协议的情况下依然如此。使用多个不同分区阶段的想法与我们在 “[多分区数据处理](#多分区数据处理)” 中讨论的想法类似(也请参阅 “[并发控制](/ch11#并发控制)”)。
### 及时性与完整性
事务的一个便利属性是,它们通常是线性一致的(请参阅 “[线性一致性](ch9.md#线性一致性)”),也就是说,写入者会等到事务提交,而之后其写入立刻对所有读取者可见。
事务的一个便利属性是,它们通常是线性一致的(请参阅 “[线性一致性](/ch9#线性一致性)”),也就是说,写入者会等到事务提交,而之后其写入立刻对所有读取者可见。
当我们把一个操作拆分为跨越多个阶段的流处理器时,却并非如此:日志的消费者在设计上就是异步的,因此发送者不会等其消息被消费者处理完。但是,客户端等待输出流中的特定消息是可能的。这正是我们在 “[基于日志消息传递中的唯一性](#基于日志消息传递中的唯一性)” 一节中检查唯一性约束时所做的事情。
@@ -577,15 +579,15 @@ COMMIT;
* 及时性Timeliness
及时性意味着确保用户观察到系统的最新状态。我们之前看到,如果用户从陈旧的数据副本中读取数据,它们可能会观察到系统处于不一致的状态(请参阅 “[复制延迟问题](ch5.md#复制延迟问题)”)。但这种不一致是暂时的,而最终会通过等待与重试简单地得到解决。
及时性意味着确保用户观察到系统的最新状态。我们之前看到,如果用户从陈旧的数据副本中读取数据,它们可能会观察到系统处于不一致的状态(请参阅 “[复制延迟问题](/ch5#复制延迟问题)”)。但这种不一致是暂时的,而最终会通过等待与重试简单地得到解决。
CAP 定理(请参阅 “[线性一致性的代价](ch9.md#线性一致性的代价)”)使用 **线性一致性linearizability** 意义上的一致性,这是实现及时性的强有力方法。像 **写后读** 这样及时性更弱的一致性也很有用(请参阅 “[读己之写](ch5.md#读己之写)”)。
CAP 定理(请参阅 “[线性一致性的代价](/ch9#线性一致性的代价)”)使用 **线性一致性linearizability** 意义上的一致性,这是实现及时性的强有力方法。像 **写后读** 这样及时性更弱的一致性也很有用(请参阅 “[读己之写](/ch5#读己之写)”)。
* 完整性Integrity
完整性意味着没有损坏;即没有数据丢失,并且没有矛盾或错误的数据。尤其是如果某些衍生数据集是作为底层数据之上的视图而维护的(请参阅 “[从事件日志中派生出当前状态](ch11.md#从事件日志中派生出当前状态)”),这种衍生必须是正确的。例如,数据库索引必须正确地反映数据库的内容 —— 缺失某些记录的索引并不是很有用。
完整性意味着没有损坏;即没有数据丢失,并且没有矛盾或错误的数据。尤其是如果某些衍生数据集是作为底层数据之上的视图而维护的(请参阅 “[从事件日志中派生出当前状态](/ch11#从事件日志中派生出当前状态)”),这种衍生必须是正确的。例如,数据库索引必须正确地反映数据库的内容 —— 缺失某些记录的索引并不是很有用。
如果完整性被违背,这种不一致是永久的:在大多数情况下,等待与重试并不能修复数据库损坏。相反的是,需要显式地检查与修复。在 ACID 事务的上下文中(请参阅 “[ACID 的含义](ch7.md#ACID的含义)”),一致性通常被理解为某种特定于应用的完整性概念。原子性和持久性是保持完整性的重要工具。
如果完整性被违背,这种不一致是永久的:在大多数情况下,等待与重试并不能修复数据库损坏。相反的是,需要显式地检查与修复。在 ACID 事务的上下文中(请参阅 “[ACID 的含义](/ch7#ACID的含义)”),一致性通常被理解为某种特定于应用的完整性概念。原子性和持久性是保持完整性的重要工具。
口号形式:违反及时性,“最终一致性”;违反完整性,“永无一致性”。
@@ -600,14 +602,14 @@ ACID 事务通常既提供及时性(例如线性一致性)也提供完整性
另一方面,对于在本章中讨论的基于事件的数据流系统而言,它们的一个有趣特性就是将及时性与完整性分开。在异步处理事件流时不能保证及时性,除非你显式构建一个在返回之前明确等待特定消息到达的消费者。但完整性实际上才是流处理系统的核心。
**恰好一次** 或 **等效一次** 语义(请参阅 “[容错](ch11.md#容错)”)是一种保持完整性的机制。如果事件丢失或者生效两次,就有可能违背数据系统的完整性。因此在出现故障时,容错消息传递与重复抑制(例如,幂等操作)对于维护数据系统的完整性是很重要的。
**恰好一次** 或 **等效一次** 语义(请参阅 “[容错](/ch11#容错)”)是一种保持完整性的机制。如果事件丢失或者生效两次,就有可能违背数据系统的完整性。因此在出现故障时,容错消息传递与重复抑制(例如,幂等操作)对于维护数据系统的完整性是很重要的。
正如我们在上一节看到的那样,可靠的流处理系统可以在无需分布式事务与原子提交协议的情况下保持完整性,这意味着它们有潜力达到与后者相当的正确性,同时还具备好得多的性能与运维稳健性。为了达成这种正确性,我们组合使用了多种机制:
* 将写入操作的内容表示为单条消息,从而可以轻松地被原子写入 —— 与事件溯源搭配效果拔群(请参阅 “[事件溯源](ch11.md#事件溯源)”)。
* 使用与存储过程类似的确定性衍生函数,从这一消息中衍生出所有其他的状态变更(请参阅 “[真的串行执行](ch7.md#真的串行执行)” 和 “[应用代码作为衍生函数](ch12.md#应用代码作为衍生函数)”)
* 将写入操作的内容表示为单条消息,从而可以轻松地被原子写入 —— 与事件溯源搭配效果拔群(请参阅 “[事件溯源](/ch11#事件溯源)”)。
* 使用与存储过程类似的确定性衍生函数,从这一消息中衍生出所有其他的状态变更(请参阅 “[真的串行执行](/ch7#真的串行执行)” 和 “[应用代码作为衍生函数](/ch12#应用代码作为衍生函数)”)
* 将客户端生成的请求 ID 传递通过所有的处理层次,从而允许端到端的除重,带来幂等性。
* 使消息不可变,并允许衍生数据能随时被重新处理,这使从错误中恢复更加容易(请参阅 “[不可变事件的优点](ch11.md#不可变事件的优点)”)
* 使消息不可变,并允许衍生数据能随时被重新处理,这使从错误中恢复更加容易(请参阅 “[不可变事件的优点](/ch11#不可变事件的优点)”)
这种机制组合在我看来,是未来构建容错应用的一个非常有前景的方向。
@@ -626,7 +628,7 @@ ACID 事务通常既提供及时性(例如线性一致性)也提供完整性
道歉的成本是否能接受是一个商业决策。如果可以接受的话,在写入数据之前检查所有约束的传统模型反而会带来不必要的限制,而线性一致性的约束也不是必须的。乐观写入,事后检查可能是一种合理的选择。你仍然可以在做一些挽回成本高昂的事情前确保有相关的验证,但这并不意味着写入数据之前必须先进行验证。
这些应用 **确实** 需要完整性:你不会希望丢失预订信息,或者由于借方贷方不匹配导致资金消失。但是它们在执行约束时 **并不需要** 及时性:如果你销售的货物多于仓库中的库存,可以在事后道歉后并弥补问题。这种做法与我们在 “[处理写入冲突](ch5.md#处理写入冲突)” 中讨论的冲突解决方法类似。
这些应用 **确实** 需要完整性:你不会希望丢失预订信息,或者由于借方贷方不匹配导致资金消失。但是它们在执行约束时 **并不需要** 及时性:如果你销售的货物多于仓库中的库存,可以在事后道歉后并弥补问题。这种做法与我们在 “[处理写入冲突](/ch5#处理写入冲突)” 中讨论的冲突解决方法类似。
#### 无协调数据系统
@@ -639,17 +641,17 @@ ACID 事务通常既提供及时性(例如线性一致性)也提供完整性
例如,这种系统可以使用多领导者配置运维,跨越多个数据中心,在区域间异步复制。任何一个数据中心都可以持续独立运行,因为不需要同步的跨区域协调。这样的系统的及时性保证会很弱 —— 如果不引入协调它是不可能是线性一致的 —— 但它仍然可以提供有力的完整性保证。
在这种情况下可串行化事务作为维护衍生状态的一部分仍然是有用的但它们只能在小范围内运行在那里它们工作得很好【8】。异构分布式事务如 XA 事务,请参阅 “[实践中的分布式事务](ch9.md#实践中的分布式事务)”不是必需的。同步协调仍然可以在需要的地方引入例如在无法恢复的操作之前强制执行严格的约束但是如果只是应用的一小部分地方需要它没必要让所有操作都付出协调的代价。【43】。
在这种情况下可串行化事务作为维护衍生状态的一部分仍然是有用的但它们只能在小范围内运行在那里它们工作得很好【8】。异构分布式事务如 XA 事务,请参阅 “[实践中的分布式事务](/ch9#实践中的分布式事务)”不是必需的。同步协调仍然可以在需要的地方引入例如在无法恢复的操作之前强制执行严格的约束但是如果只是应用的一小部分地方需要它没必要让所有操作都付出协调的代价。【43】。
另一种审视协调与约束的角度是:它们减少了由于不一致而必须做出的道歉数量,但也可能会降低系统的性能和可用性,从而可能增加由于宕机中断而需要做出的道歉数量。你不可能将道歉数量减少到零,但可以根据自己的需求寻找最佳平衡点 —— 既不存在太多不一致性,又不存在太多可用性问题。
### 信任但验证
我们所有关于正确性,完整性和容错的讨论都基于一些假设,假设某些事情可能会出错,但其他事情不会。我们将这些假设称为我们的 **系统模型**system model请参阅 “[将系统模型映射到现实世界](ch8.md#将系统模型映射到现实世界)”):例如,我们应该假设进程可能会崩溃,机器可能突然断电,网络可能会任意延迟或丢弃消息。但是我们也可能假设写入磁盘的数据在执行 `fsync` 后不会丢失,内存中的数据没有损坏,而 CPU 的乘法指令总是能返回正确的结果。
我们所有关于正确性,完整性和容错的讨论都基于一些假设,假设某些事情可能会出错,但其他事情不会。我们将这些假设称为我们的 **系统模型**system model请参阅 “[将系统模型映射到现实世界](/ch8#将系统模型映射到现实世界)”):例如,我们应该假设进程可能会崩溃,机器可能突然断电,网络可能会任意延迟或丢弃消息。但是我们也可能假设写入磁盘的数据在执行 `fsync` 后不会丢失,内存中的数据没有损坏,而 CPU 的乘法指令总是能返回正确的结果。
这些假设是相当合理的,因为大多数时候它们都是成立的,如果我们不得不经常担心计算机出错,那么基本上寸步难行。在传统上,系统模型采用二元方法处理故障:我们假设有些事情可能会发生,而其他事情 **永远** 不会发生。实际上,这更像是一个概率问题:有些事情更有可能,其他事情不太可能。问题在于违反我们假设的情况是否经常发生,以至于我们可能在实践中遇到它们。
我们已经看到,数据可能会在尚未落盘时损坏(请参阅 “[复制与持久性](ch7.md#复制与持久性)”),而网络上的数据损坏有时可能规避了 TCP 校验和(请参阅 “[弱谎言形式](ch8.md#弱谎言形式)” )。也许我们应当更关注这些事情?
我们已经看到,数据可能会在尚未落盘时损坏(请参阅 “[复制与持久性](/ch7#复制与持久性)”),而网络上的数据损坏有时可能规避了 TCP 校验和(请参阅 “[弱谎言形式](/ch8#弱谎言形式)” )。也许我们应当更关注这些事情?
我过去所从事的一个应用收集了来自客户端的崩溃报告我们收到的一些报告只有在这些设备内存中出现了随机位翻转才解释的通。这看起来不太可能但是如果有足够多的设备运行你的软件那么即使再不可能发生的事也确实会发生。除了由于硬件故障或辐射导致的随机存储器损坏之外一些病态的存储器访问模式甚至可以在没有故障的存储器中翻转位【62】 —— 一种可用于破坏操作系统安全机制的效应【63】这种技术被称为 **Rowhammer**)。一旦你仔细观察,硬件并不是看上去那样完美的抽象。
@@ -663,13 +665,13 @@ ACID 事务通常既提供及时性(例如线性一致性)也提供完整性
而对于应用代码我们不得不假设会有更多的错误因为绝大多数应用的代码经受的评审与测试远远无法与数据库的代码相比。许多应用甚至没有正确使用数据库提供的用于维持完整性的功能例如外键或唯一性约束【36】。
ACID 意义下的一致性(请参阅 “[一致性](ch7.md#一致性)”)基于这样一种想法:数据库以一致的状态启动,而事务将其从一个一致状态转换至另一个一致的状态。因此,我们期望数据库始终处于一致状态。然而,只有当你假设事务没有 Bug 时,这种想法才有意义。如果应用以某种错误的方式使用数据库,例如,不安全地使用弱隔离等级,数据库的完整性就无法得到保证。
ACID 意义下的一致性(请参阅 “[一致性](/ch7#一致性)”)基于这样一种想法:数据库以一致的状态启动,而事务将其从一个一致状态转换至另一个一致的状态。因此,我们期望数据库始终处于一致状态。然而,只有当你假设事务没有 Bug 时,这种想法才有意义。如果应用以某种错误的方式使用数据库,例如,不安全地使用弱隔离等级,数据库的完整性就无法得到保证。
#### 不要盲目信任承诺
由于硬件和软件并不总是符合我们的理想,所以数据损坏似乎早晚不可避免。因此,我们至少应该有办法查明数据是否已经损坏,以便我们能够修复它,并尝试追查错误的来源。检查数据完整性称为 **审计auditing**
如 “[不可变事件的优点](ch11.md#不可变事件的优点)” 一节中所述,审计不仅仅适用于财务应用程序。不过,可审计性在财务中是非常非常重要的,因为每个人都知道错误总会发生,我们也都认为能够检测和解决问题是合理的需求。
如 “[不可变事件的优点](/ch11#不可变事件的优点)” 一节中所述,审计不仅仅适用于财务应用程序。不过,可审计性在财务中是非常非常重要的,因为每个人都知道错误总会发生,我们也都认为能够检测和解决问题是合理的需求。
成熟的系统同样倾向于考虑不太可能的事情出错的可能性并管理这种风险。例如HDFS 和 Amazon S3 等大规模存储系统并不完全信任磁盘它们运行后台进程持续回读文件并将其与其他副本进行比较并将文件从一个磁盘移动到另一个以便降低静默损坏的风险【67】。
@@ -685,11 +687,11 @@ ACID 意义下的一致性(请参阅 “[一致性](ch7.md#一致性)”)基
#### 为可审计性而设计
如果一个事务在一个数据库中改变了多个对象,在这一事实发生后,很难说清这个事务到底意味着什么。即使你捕获了事务日志(请参阅 “[变更数据捕获](ch11.md#变更数据捕获)”),各种表中的插入、更新和删除操作并不一定能清楚地表明 **为什么** 要执行这些变更。决定这些变更的是应用逻辑中的调用,而这一应用逻辑稍纵即逝,无法重现。
如果一个事务在一个数据库中改变了多个对象,在这一事实发生后,很难说清这个事务到底意味着什么。即使你捕获了事务日志(请参阅 “[变更数据捕获](/ch11#变更数据捕获)”),各种表中的插入、更新和删除操作并不一定能清楚地表明 **为什么** 要执行这些变更。决定这些变更的是应用逻辑中的调用,而这一应用逻辑稍纵即逝,无法重现。
相比之下,基于事件的系统可以提供更好的可审计性。在事件溯源方法中,系统的用户输入被表示为一个单一不可变事件,而任何其导致的状态变更都衍生自该事件。衍生可以实现为具有确定性与可重复性,因而相同的事件日志通过相同版本的衍生代码时,会导致相同的状态变更。
显式处理数据流(请参阅 “[批处理输出的哲学](ch10.md#批处理输出的哲学)”)可以使数据的 **来龙去脉provenance** 更加清晰,从而使完整性检查更具可行性。对于事件日志,我们可以使用散列来检查事件存储没有被破坏。对于任何衍生状态,我们可以重新运行从事件日志中衍生它的批处理器与流处理器,以检查是否获得相同的结果,或者,甚至并行运行冗余的衍生流程。
显式处理数据流(请参阅 “[批处理输出的哲学](/ch10#批处理输出的哲学)”)可以使数据的 **来龙去脉provenance** 更加清晰,从而使完整性检查更具可行性。对于事件日志,我们可以使用散列来检查事件存储没有被破坏。对于任何衍生状态,我们可以重新运行从事件日志中衍生它的批处理器与流处理器,以检查是否获得相同的结果,或者,甚至并行运行冗余的衍生流程。
具有确定性且定义良好的数据流,也使调试与跟踪系统的执行变得容易,以便确定它 **为什么** 做了某些事情【4,69】。如果出现意想之外的事情那么重现导致意外事件的确切事故现场的诊断能力 —— 一种时间旅行调试功能是非常有价值的。
@@ -709,7 +711,7 @@ ACID 意义下的一致性(请参阅 “[一致性](ch7.md#一致性)”)基
我没有资格评论这些技术用于货币,或者合同商定机制的价值。但从数据系统的角度来看,它们包含了一些有趣的想法。实质上,它们是分布式数据库,具有数据模型与事务机制,而不同副本可以由互不信任的组织托管。副本不断检查其他副本的完整性,并使用共识协议对应当执行的事务达成一致。
我对这些技术的拜占庭容错方面有些怀疑(请参阅 “[拜占庭故障](ch8.md#拜占庭故障)”),而且我发现 **工作证明proof of work** 技术非常浪费(比如,比特币挖矿)。比特币的交易吞吐量相当低,尽管更多是出于政治与经济原因而非技术上的原因。不过,完整性检查的方面是很有趣的。
我对这些技术的拜占庭容错方面有些怀疑(请参阅 “[拜占庭故障](/ch8#拜占庭故障)”),而且我发现 **工作证明proof of work** 技术非常浪费(比如,比特币挖矿)。比特币的交易吞吐量相当低,尽管更多是出于政治与经济原因而非技术上的原因。不过,完整性检查的方面是很有趣的。
密码学审计与完整性检查通常依赖 **默克尔树Merkle tree**【74】这是一颗散列值的树能够用于高效地证明一条记录出现在一个数据集中以及其他一些特性。除了炒作的沸沸扬扬的加密货币之外**证书透明性certificate transparency** 也是一种依赖 Merkle 树的安全技术,用来检查 TLS/SSL 证书的有效性【75,76】。
@@ -862,7 +864,7 @@ ACID 意义下的一致性(请参阅 “[一致性](ch7.md#一致性)”)基
我们应该允许每个人保留自己的隐私 —— 即,对自己数据的控制 —— 而不是通过监视来窃取这种控制权。我们控制自己数据的个体权利就像是国家公园的自然环境:如果我们不去明确地保护它、关心它,它就会被破坏。这将是公地的悲剧,我们都会因此而变得更糟。无所不在的监视并非不可避免的 —— 我们现在仍然能阻止它。
我们究竟能做到哪一步是一个开放的问题。首先我们不应该永久保留数据而是一旦不再需要就立即清除数据【111,112】。清除数据与不变性的想法背道而驰请参阅 “[不变性的局限性](ch11.md#不变性的局限性)”但这是可以解决的问题。我所看到的一种很有前景的方法是通过加密协议来实施访问控制而不仅仅是通过策略【113,114】。总的来说文化与态度的改变是必要的。
我们究竟能做到哪一步是一个开放的问题。首先我们不应该永久保留数据而是一旦不再需要就立即清除数据【111,112】。清除数据与不变性的想法背道而驰请参阅 “[不变性的局限性](/ch11#不变性的局限性)”但这是可以解决的问题。我所看到的一种很有前景的方法是通过加密协议来实施访问控制而不仅仅是通过策略【113,114】。总的来说文化与态度的改变是必要的。
## 本章小结
@@ -1002,11 +1004,3 @@ ACID 意义下的一致性(请参阅 “[一致性](ch7.md#一致性)”)基
1. Sam Thielman: “[You Are Not What You Read: Librarians Purge User Data to Protect Privacy](https://www.theguardian.com/us-news/2016/jan/13/us-library-records-purged-data-privacy),” *theguardian.com*, January 13, 2016.
1. Conor Friedersdorf: “[Edward Snowdens Other Motive for Leaking](http://www.theatlantic.com/politics/archive/2014/05/edward-snowdens-other-motive-for-leaking/370068/),” *theatlantic.com*, May 13, 2014.
1. Phillip Rogaway: “[The Moral Character of Cryptographic Work](http://web.cs.ucdavis.edu/~rogaway/papers/moral-fn.pdf),” Cryptology ePrint 2015/1162, December 2015.
------
| 上一章 | 目录 | 下一章 |
| --------------------------- | ------------------------------- | ------------------- |
| [第十一章:流处理](ch11.md) | [设计数据密集型应用](README.md) | [后记](colophon.md) |

View File

@@ -1,15 +1,17 @@
# 第二章:数据模型与查询语言
---
title: "第二章:数据模型与查询语言"
linkTitle: "2. 数据模型与查询语言"
weight: 102
breadcrumbs: false
---
![](img/ch2.png)
![](/img/ch2.png)
> 语言的边界就是思想的边界。
>
> —— 路德维奇・维特根斯坦《逻辑哲学》1922
>
-------------------
[TOC]
数据模型可能是软件开发中最重要的部分了,因为它们的影响如此深远:不仅仅影响着软件的编写方式,而且影响着我们的 **解题思路**
@@ -26,7 +28,7 @@
掌握一个数据模型需要花费很多精力(想想关系数据建模有多少本书)。即便只使用一个数据模型,不用操心其内部工作机制,构建软件也是非常困难的。然而,因为数据模型对上层软件的功能(能做什么,不能做什么)有着至深的影响,所以选择一个适合的数据模型是非常重要的。
在本章中,我们将研究一系列用于数据存储和查询的通用数据模型(前面列表中的第 2 点)。特别地,我们将比较关系模型,文档模型和少量基于图形的数据模型。我们还将查看各种查询语言并比较它们的用例。在 [第三章](ch3.md) 中,我们将讨论存储引擎是如何工作的。也就是说,这些数据模型实际上是如何实现的(列表中的第 3 点)。
在本章中,我们将研究一系列用于数据存储和查询的通用数据模型(前面列表中的第 2 点)。特别地,我们将比较关系模型,文档模型和少量基于图形的数据模型。我们还将查看各种查询语言并比较它们的用例。在 [第三章](/ch3) 中,我们将讨论存储引擎是如何工作的。也就是说,这些数据模型实际上是如何实现的(列表中的第 3 点)。
## 关系模型与文档模型
@@ -64,13 +66,13 @@
像 ActiveRecord 和 Hibernate 这样的 **对象关系映射ORM object-relational mapping** 框架可以减少这个转换层所需的样板代码的数量,但是它们不能完全隐藏这两个模型之间的差异。
![](img/fig2-1.png)
![](/img/fig2-1.png)
**图 2-1 使用关系型模式来表示领英简介**
例如,[图 2-1](img/fig2-1.png) 展示了如何在关系模式中表示简历(一个 LinkedIn 简介)。整个简介可以通过一个唯一的标识符 `user_id` 来标识。像 `first_name``last_name` 这样的字段每个用户只出现一次,所以可以在 User 表上将其建模为列。但是,大多数人在职业生涯中拥有多于一份的工作,人们可能有不同样的教育阶段和任意数量的联系信息。从用户到这些项目之间存在一对多的关系,可以用多种方式来表示:
例如,[图 2-1](/img/fig2-1.png) 展示了如何在关系模式中表示简历(一个 LinkedIn 简介)。整个简介可以通过一个唯一的标识符 `user_id` 来标识。像 `first_name``last_name` 这样的字段每个用户只出现一次,所以可以在 User 表上将其建模为列。但是,大多数人在职业生涯中拥有多于一份的工作,人们可能有不同样的教育阶段和任意数量的联系信息。从用户到这些项目之间存在一对多的关系,可以用多种方式来表示:
* 传统 SQL 模型SQL1999 之前)中,最常见的规范化表示形式是将职位,教育和联系信息放在单独的表中,对 User 表提供外键引用,如 [图 2-1](img/fig2-1.png) 所示。
* 传统 SQL 模型SQL1999 之前)中,最常见的规范化表示形式是将职位,教育和联系信息放在单独的表中,对 User 表提供外键引用,如 [图 2-1](/img/fig2-1.png) 所示。
* 后续的 SQL 标准增加了对结构化数据类型和 XML 数据的支持;这允许将多值数据存储在单行内,并支持在这些文档内查询和索引。这些功能在 OracleIBM DB2MS SQL Server 和 PostgreSQL 中都有不同程度的支持【6,7】。JSON 数据类型也得到多个数据库的支持,包括 IBM DB2MySQL 和 PostgreSQL 【8】。
* 第三种选择是将职业,教育和联系信息编码为 JSON 或 XML 文档,将其存储在数据库的文本列中,并让应用程序解析其结构和内容。这种配置下,通常不能使用数据库来查询该编码列中的值。
@@ -116,13 +118,13 @@
}
```
有一些开发人员认为 JSON 模型减少了应用程序代码和存储层之间的阻抗不匹配。不过,正如我们将在 [第四章](ch4.md) 中看到的那样JSON 作为数据编码格式也存在问题。无模式对 JSON 模型来说往往被认为是一个优势;我们将在 “[文档模型中的模式灵活性](#文档模型中的模式灵活性)” 中讨论这个问题。
有一些开发人员认为 JSON 模型减少了应用程序代码和存储层之间的阻抗不匹配。不过,正如我们将在 [第四章](/ch4) 中看到的那样JSON 作为数据编码格式也存在问题。无模式对 JSON 模型来说往往被认为是一个优势;我们将在 “[文档模型中的模式灵活性](#文档模型中的模式灵活性)” 中讨论这个问题。
JSON 表示比 [图 2-1](img/fig2-1.png) 中的多表模式具有更好的 **局部性locality**。如果在前面的关系型示例中获取简介,那需要执行多个查询(通过 `user_id` 查询每个表),或者在 User 表与其下属表之间混乱地执行多路连接。而在 JSON 表示中,所有相关信息都在同一个地方,一个查询就足够了。
JSON 表示比 [图 2-1](/img/fig2-1.png) 中的多表模式具有更好的 **局部性locality**。如果在前面的关系型示例中获取简介,那需要执行多个查询(通过 `user_id` 查询每个表),或者在 User 表与其下属表之间混乱地执行多路连接。而在 JSON 表示中,所有相关信息都在同一个地方,一个查询就足够了。
从用户简介文件到用户职位,教育历史和联系信息,这种一对多关系隐含了数据中的一个树状结构,而 JSON 表示使得这个树状结构变得明确(见 [图 2-2](img/fig2-2.png))。
从用户简介文件到用户职位,教育历史和联系信息,这种一对多关系隐含了数据中的一个树状结构,而 JSON 表示使得这个树状结构变得明确(见 [图 2-2](/img/fig2-2.png))。
![](img/fig2-2.png)
![](/img/fig2-2.png)
**图 2-2 一对多关系构建了一个树结构**
@@ -144,7 +146,7 @@ JSON 表示比 [图 2-1](img/fig2-1.png) 中的多表模式具有更好的 **局
[^ii]: 关于关系模型的文献区分了几种不同的规范形式,但这些区别几乎没有实际意义。一个经验法则是,如果重复存储了可以存储在一个地方的值,则模式就不是 **规范化normalized** 的。
> 数据库管理员和开发人员喜欢争论规范化和非规范化,让我们暂时保留判断吧。在本书的 [第三部分](part-iii.md),我们将回到这个话题,探讨系统的方法用以处理缓存,非规范化和衍生数据。
> 数据库管理员和开发人员喜欢争论规范化和非规范化,让我们暂时保留判断吧。在本书的 [第三部分](/part-iii),我们将回到这个话题,探讨系统的方法用以处理缓存,非规范化和衍生数据。
不幸的是,对这些数据进行规范化需要多对一的关系(许多人生活在一个特定的地区,许多人在一个特定的行业工作),这与文档模型不太吻合。在关系数据库中,通过 ID 来引用其他表中的行是正常的,因为连接很容易。在文档数据库中,一对多树结构没有必要用连接,对连接的支持通常很弱 [^iii]。
@@ -156,19 +158,19 @@ JSON 表示比 [图 2-1](img/fig2-1.png) 中的多表模式具有更好的 **局
* 组织和学校作为实体
在前面的描述中,`organization`(用户工作的公司)和 `school_name`(他们学习的地方)只是字符串。也许他们应该是对实体的引用呢?然后,每个组织、学校或大学都可以拥有自己的网页(标识、新闻提要等)。每个简历可以链接到它所提到的组织和学校,并且包括他们的图标和其他信息(请参阅 [图 2-3](img/fig2-3.png),来自 LinkedIn 的一个例子)。
在前面的描述中,`organization`(用户工作的公司)和 `school_name`(他们学习的地方)只是字符串。也许他们应该是对实体的引用呢?然后,每个组织、学校或大学都可以拥有自己的网页(标识、新闻提要等)。每个简历可以链接到它所提到的组织和学校,并且包括他们的图标和其他信息(请参阅 [图 2-3](/img/fig2-3.png),来自 LinkedIn 的一个例子)。
* 推荐
假设你想添加一个新的功能:一个用户可以为另一个用户写一个推荐。在用户的简历上显示推荐,并附上推荐用户的姓名和照片。如果推荐人更新他们的照片,那他们写的任何推荐都需要显示新的照片。因此,推荐应该拥有作者个人简介的引用。
![](img/fig2-3.png)
![](/img/fig2-3.png)
**图 2-3 公司名不仅是字符串还是一个指向公司实体的链接LinkedIn 截图)**
[图 2-4](img/fig2-4.png) 阐明了这些新功能需要如何使用多对多关系。每个虚线矩形内的数据可以分组成一个文档,但是对单位,学校和其他用户的引用需要表示成引用,并且在查询时需要连接。
[图 2-4](/img/fig2-4.png) 阐明了这些新功能需要如何使用多对多关系。每个虚线矩形内的数据可以分组成一个文档,但是对单位,学校和其他用户的引用需要表示成引用,并且在查询时需要连接。
![](img/fig2-4.png)
![](/img/fig2-4.png)
**图 2-4 使用多对多关系扩展简历**
@@ -178,7 +180,7 @@ JSON 表示比 [图 2-1](img/fig2-1.png) 中的多表模式具有更好的 **局
20 世纪 70 年代最受欢迎的业务数据处理数据库是 IBM 的信息管理系统IMS最初是为了阿波罗太空计划的库存管理而开发的并于 1968 年有了首次商业发布【13】。目前它仍在使用和维护运行在 IBM 大型机的 OS/390 上【14】。
IMS 的设计中使用了一个相当简单的数据模型,称为 **层次模型hierarchical model**,它与文档数据库使用的 JSON 模型有一些惊人的相似之处【2】。它将所有数据表示为嵌套在记录中的记录树这很像 [图 2-2](img/fig2-2.png) 的 JSON 结构。
IMS 的设计中使用了一个相当简单的数据模型,称为 **层次模型hierarchical model**,它与文档数据库使用的 JSON 模型有一些惊人的相似之处【2】。它将所有数据表示为嵌套在记录中的记录树这很像 [图 2-2](/img/fig2-2.png) 的 JSON 结构。
同文档数据库一样IMS 能良好处理一对多的关系但是很难应对多对多的关系并且不支持连接。开发人员必须决定是否复制非规范化数据或手动解决从一个记录到另一个记录的引用。这些二十世纪六七十年代的问题与现在开发人员遇到的文档数据库问题非常相似【15】。
@@ -214,19 +216,19 @@ CODASYL 中的查询是通过利用遍历记录列和跟随访问路径表在数
#### 与文档数据库相比
在一个方面,文档数据库还原为层次模型:在其父记录中存储嵌套记录([图 2-1](img/fig2-1.png) 中的一对多关系,如 `positions``education``contact_info`),而不是在单独的表中。
在一个方面,文档数据库还原为层次模型:在其父记录中存储嵌套记录([图 2-1](/img/fig2-1.png) 中的一对多关系,如 `positions``education``contact_info`),而不是在单独的表中。
但是,在表示多对一和多对多的关系时,关系数据库和文档数据库并没有根本的不同:在这两种情况下,相关项目都被一个唯一的标识符引用,这个标识符在关系模型中被称为 **外键**,在文档模型中称为 **文档引用**【9】。该标识符在读取时通过连接或后续查询来解析。迄今为止文档数据库没有走 CODASYL 的老路。
### 关系型数据库与文档数据库在今日的对比
将关系数据库与文档数据库进行比较时,可以考虑许多方面的差异,包括它们的容错属性(请参阅 [第五章](ch5.md))和处理并发性(请参阅 [第七章](ch7.md))。本章将只关注数据模型中的差异。
将关系数据库与文档数据库进行比较时,可以考虑许多方面的差异,包括它们的容错属性(请参阅 [第五章](/ch5))和处理并发性(请参阅 [第七章](/ch7))。本章将只关注数据模型中的差异。
支持文档数据模型的主要论据是架构灵活性,因局部性而拥有更好的性能,以及对于某些应用程序而言更接近于应用程序使用的数据结构。关系模型通过为连接提供更好的支持以及支持多对一和多对多的关系来反击。
#### 哪种数据模型更有助于简化应用代码?
如果应用程序中的数据具有类似文档的结构(即,一对多关系树,通常一次性加载整个树),那么使用文档模型可能是一个好主意。将类似文档的结构分解成多个表(如 [图 2-1](img/fig2-1.png) 中的 `positions``education``contact_info`)的关系技术可能导致繁琐的模式和不必要的复杂的应用程序代码。
如果应用程序中的数据具有类似文档的结构(即,一对多关系树,通常一次性加载整个树),那么使用文档模型可能是一个好主意。将类似文档的结构分解成多个表(如 [图 2-1](/img/fig2-1.png) 中的 `positions``education``contact_info`)的关系技术可能导致繁琐的模式和不必要的复杂的应用程序代码。
文档模型有一定的局限性:例如,不能直接引用文档中的嵌套的项目,而是需要说 “用户 251 的位置列表中的第二项”(很像层次模型中的访问路径)。但是,只要文件嵌套不太深,这通常不是问题。
@@ -274,13 +276,13 @@ UPDATE users SET first_name = substring_index(name, ' ', 1); -- MySQL
#### 查询的数据局部性
文档通常以单个连续字符串形式进行存储,编码为 JSON、XML 或其二进制变体(如 MongoDB 的 BSON。如果应用程序经常需要访问整个文档例如将其渲染至网页那么存储局部性会带来性能优势。如果将数据分割到多个表中如 [图 2-1](img/fig2-1.png) 所示),则需要进行多次索引查找才能将其全部检索出来,这可能需要更多的磁盘查找并花费更多的时间。
文档通常以单个连续字符串形式进行存储,编码为 JSON、XML 或其二进制变体(如 MongoDB 的 BSON。如果应用程序经常需要访问整个文档例如将其渲染至网页那么存储局部性会带来性能优势。如果将数据分割到多个表中如 [图 2-1](/img/fig2-1.png) 所示),则需要进行多次索引查找才能将其全部检索出来,这可能需要更多的磁盘查找并花费更多的时间。
局部性仅仅适用于同时需要文档绝大部分内容的情况。即使只访问文档其中的一小部分数据库通常需要加载整个文档对于大型文档来说这种加载行为是很浪费的。更新文档时通常需要整个重写。只有不改变文档大小的修改才可以容易地原地执行。因此通常建议保持相对小的文档并避免增加文档大小的写入【9】。这些性能限制大大减少了文档数据库的实用场景。
值得指出的是为了局部性而分组集合相关数据的想法并不局限于文档模型。例如Google 的 Spanner 数据库在关系数据模型中提供了同样的局部性属性允许模式声明一个表的行应该交错嵌套在父表内【27】。Oracle 类似地允许使用一个称为 **多表索引集群表multi-table index cluster tables** 的类似特性【28】。Bigtable 数据模型(用于 Cassandra 和 HBase中的 **列族column-family** 概念与管理局部性的目的类似【29】。
在 [第三章](ch3.md) 将还会看到更多关于局部性的内容。
在 [第三章](/ch3) 将还会看到更多关于局部性的内容。
#### 文档和关系数据库的融合
@@ -420,7 +422,7 @@ for (var i = 0; i < liElements.length; i++) {
MapReduce 是一个由 Google 推广的编程模型用于在多台机器上批量处理大规模的数据【33】。一些 NoSQL 数据存储(包括 MongoDB 和 CouchDB支持有限形式的 MapReduce作为在多个文档中执行只读查询的机制。
关于 MapReduce 更详细的介绍在 [第十章](ch10.md)。现在我们只简要讨论一下 MongoDB 使用的模型。
关于 MapReduce 更详细的介绍在 [第十章](/ch10)。现在我们只简要讨论一下 MongoDB 使用的模型。
MapReduce 既不是一个声明式的查询语言,也不是一个完全命令式的查询 API而是处于两者之间查询的逻辑用代码片段来表示这些代码片段会被处理框架重复性调用。它基于 `map`(也称为 `collect`)和 `reduce`(也称为 `fold``inject`)函数,两个函数存在于许多函数式编程语言中。
@@ -488,7 +490,7 @@ db.observations.mapReduce(function map() {
map 和 reduce 函数在功能上有所限制:它们必须是 **纯** 函数这意味着它们只使用传递给它们的数据作为输入它们不能执行额外的数据库查询也不能有任何副作用。这些限制允许数据库以任何顺序运行任何功能并在失败时重新运行它们。然而map 和 reduce 函数仍然是强大的:它们可以解析字符串、调用库函数、执行计算等等。
MapReduce 是一个相当底层的编程模型,用于计算机集群上的分布式执行。像 SQL 这样的更高级的查询语言可以用一系列的 MapReduce 操作来实现(见 [第十章](ch10.md)),但是也有很多不使用 MapReduce 的分布式 SQL 实现。須注意SQL 并没有限制它只能在单一机器上运行,而 MapReduce 也并没有垄断所有的分布式查询执行。
MapReduce 是一个相当底层的编程模型,用于计算机集群上的分布式执行。像 SQL 这样的更高级的查询语言可以用一系列的 MapReduce 操作来实现(见 [第十章](/ch10)),但是也有很多不使用 MapReduce 的分布式 SQL 实现。須注意SQL 并没有限制它只能在单一机器上运行,而 MapReduce 也并没有垄断所有的分布式查询执行。
能够在查询中使用 JavaScript 代码是高级查询的一个重要特性,但这不限于 MapReduce一些 SQL 数据库也可以用 JavaScript 函数进行扩展【34】。
@@ -533,13 +535,13 @@ db.observations.aggregate([
在刚刚给出的例子中图中的所有顶点代表了相同类型的事物人、网页或交叉路口。不过图并不局限于这样的同类数据同样强大地是图提供了一种一致的方式用来在单个数据存储中存储完全不同类型的对象。例如Facebook 维护一个包含许多不同类型的顶点和边的单个图顶点表示人、地点、事件、签到和用户的评论边表示哪些人是好友、签到发生在哪里、谁评论了什么帖子、谁参与了什么事件等等【35】。
在本节中,我们将使用 [图 2-5](img/fig2-5.png) 所示的示例。它可以从社交网络或系谱数据库中获得:它显示了两个人,来自爱达荷州的 Lucy 和来自法国 Beaune 的 Alain。他们已婚住在伦敦。
在本节中,我们将使用 [图 2-5](/img/fig2-5.png) 所示的示例。它可以从社交网络或系谱数据库中获得:它显示了两个人,来自爱达荷州的 Lucy 和来自法国 Beaune 的 Alain。他们已婚住在伦敦。
![](img/fig2-5.png)
![](/img/fig2-5.png)
**图 2-5 图数据结构示例(框代表顶点,箭头代表边)**
有几种不同但相关的方法用来构建和查询图表中的数据。在本节中,我们将讨论属性图模型(由 Neo4jTitan 和 InfiniteGraph 实现和三元组存储triple-store模型由 Datomic、AllegroGraph 等实现。我们将查看图的三种声明式查询语言CypherSPARQL 和 Datalog。除此之外还有像 Gremlin 【36】这样的图形查询语言和像 Pregel 这样的图形处理框架(见 [第十章](ch10.md))。
有几种不同但相关的方法用来构建和查询图表中的数据。在本节中,我们将讨论属性图模型(由 Neo4jTitan 和 InfiniteGraph 实现和三元组存储triple-store模型由 Datomic、AllegroGraph 等实现。我们将查看图的三种声明式查询语言CypherSPARQL 和 Datalog。除此之外还有像 Gremlin 【36】这样的图形查询语言和像 Pregel 这样的图形处理框架(见 [第十章](/ch10))。
### 属性图
@@ -586,7 +588,7 @@ CREATE INDEX edges_heads ON edges (head_vertex);
2. 给定任何顶点,可以高效地找到它的入边和出边,从而遍历图,即沿着一系列顶点的路径前后移动(这就是为什么 [例 2-2]() 在 `tail_vertex``head_vertex` 列上都有索引的原因)。
3. 通过对不同类型的关系使用不同的标签,可以在一个图中存储几种不同的信息,同时仍然保持一个清晰的数据模型。
这些特性为数据建模提供了很大的灵活性,如 [图 2-5](img/fig2-5.png) 所示。图中显示了一些传统关系模式难以表达的事情例如不同国家的不同地区结构法国有省和大区美国有县和州国中国的怪事先忽略主权国家和民族错综复杂的烂摊子不同的数据粒度Lucy 现在的住所记录具体到城市,而她的出生地点只是在一个州的级别)。
这些特性为数据建模提供了很大的灵活性,如 [图 2-5](/img/fig2-5.png) 所示。图中显示了一些传统关系模式难以表达的事情例如不同国家的不同地区结构法国有省和大区美国有县和州国中国的怪事先忽略主权国家和民族错综复杂的烂摊子不同的数据粒度Lucy 现在的住所记录具体到城市,而她的出生地点只是在一个州的级别)。
你可以想象该图还能延伸出许多关于 Lucy 和 Alain 的事实,或其他人的其他更多的事实。例如,你可以用它来表示食物过敏(为每个过敏源增加一个顶点,并增加人与过敏源之间的一条边来指示一种过敏情况),并链接到过敏源,每个过敏源具有一组顶点用来显示哪些食物含有哪些物质。然后,你可以写一个查询,找出每个人吃什么是安全的。图在可演化性方面是富有优势的:当你向应用程序添加功能时,可以轻松扩展图以适应程序数据结构的变化。
@@ -594,7 +596,7 @@ CREATE INDEX edges_heads ON edges (head_vertex);
Cypher 是属性图的声明式查询语言,为 Neo4j 图形数据库而发明【37】它是以电影 “黑客帝国” 中的一个角色来命名的而与密码学中的加密算法无关【38】
[例 2-3]() 显示了将 [图 2-5](img/fig2-5.png) 的左边部分插入图形数据库的 Cypher 查询。你可以以类似的方式把图的剩余部分添加进去,但这里为了文章可閱读性而省略这部分的示例。每个顶点都有一个像 `USA``Idaho` 这样的符号名称,查询的其他部分可以使用这些名称在顶点之间创建边,使用箭头符号:`Idaho - [WITHIN] ->USA` 创建一条标记为 `WITHIN` 的边,`Idaho` 为尾节点,`USA` 为头节点。
[例 2-3]() 显示了将 [图 2-5](/img/fig2-5.png) 的左边部分插入图形数据库的 Cypher 查询。你可以以类似的方式把图的剩余部分添加进去,但这里为了文章可閱读性而省略这部分的示例。每个顶点都有一个像 `USA``Idaho` 这样的符号名称,查询的其他部分可以使用这些名称在顶点之间创建边,使用箭头符号:`Idaho - [WITHIN] ->USA` 创建一条标记为 `WITHIN` 的边,`Idaho` 为尾节点,`USA` 为头节点。
**例 2-3 将图 2-5 中的数据子集表示为 Cypher 查询**
@@ -608,7 +610,7 @@ CREATE
(Lucy) -[:BORN_IN]-> (Idaho)
```
当 [图 2-5](img/fig2-5.png) 的所有顶点和边被添加到数据库后,让我们提些有趣的问题:例如,找到所有从美国移民到欧洲的人的名字。更确切地说,这里我们想要找到符合下面条件的所有顶点,并且返回这些顶点的 `name` 属性:该顶点拥有一条连到美国任一位置的 `BORN_IN` 边,和一条连到欧洲的任一位置的 `LIVING_IN` 边。
当 [图 2-5](/img/fig2-5.png) 的所有顶点和边被添加到数据库后,让我们提些有趣的问题:例如,找到所有从美国移民到欧洲的人的名字。更确切地说,这里我们想要找到符合下面条件的所有顶点,并且返回这些顶点的 `name` 属性:该顶点拥有一条连到美国任一位置的 `BORN_IN` 边,和一条连到欧洲的任一位置的 `LIVING_IN` 边。
[例 2-4]() 展示了如何在 Cypher 中表达这个查询。在 MATCH 子句中使用相同的箭头符号来查找图中的模式:`(person) -[:BORN_IN]-> ()` 可以匹配 `BORN_IN` 边的任意两个顶点。该边的尾节点被绑定了变量 `person`,头节点则未被绑定。
@@ -895,9 +897,9 @@ Cypher 和 SPARQL 使用 SELECT 立即跳转,但是 Datalog 一次只进行一
2. 数据库存在 `within (usa, namerica)`,在上一步骤中生成 `within_recursive (namerica, 'North America')`,故运用规则 2。它会产生 `within_recursive (usa, 'North America')`
3. 数据库存在 `within (idaho, usa)`,在上一步生成 `within_recursive (usa, 'North America')`,故运用规则 2。它产生 `within_recursive (idaho, 'North America')`
通过重复应用规则 1 和 2`within_recursive` 谓语可以告诉我们在数据库中包含北美(或任何其他位置名称)的所有位置。这个过程如 [图 2-6](img/fig2-6.png) 所示。
通过重复应用规则 1 和 2`within_recursive` 谓语可以告诉我们在数据库中包含北美(或任何其他位置名称)的所有位置。这个过程如 [图 2-6](/img/fig2-6.png) 所示。
![](img/fig2-6.png)
![](/img/fig2-6.png)
**图 2-6 使用示例 2-11 中的 Datalog 规则来确定爱达荷州在北美。**
@@ -927,7 +929,7 @@ Cypher 和 SPARQL 使用 SELECT 立即跳转,但是 Datalog 一次只进行一
* 粒子物理学家数十年来一直在进行大数据类型的大规模数据分析像大型强子对撞机LHC这样的项目现在会处理数百 PB 的数据在这样的规模下需要定制解决方案来阻止硬件成本的失控【49】。
* **全文搜索** 可以说是一种经常与数据库一起使用的数据模型。信息检索是一个很大的专业课题,我们不会在本书中详细介绍,但是我们将在第三章和第三部分中介绍搜索索引。
让我们暂时将其放在一边。在 [下一章](ch3.md) 中,我们将讨论在 **实现** 本章描述的数据模型时会遇到的一些权衡。
让我们暂时将其放在一边。在 [下一章](/ch3) 中,我们将讨论在 **实现** 本章描述的数据模型时会遇到的一些权衡。
## 参考文献
@@ -981,9 +983,3 @@ Cypher 和 SPARQL 使用 SELECT 立即跳转,但是 Datalog 一次只进行一
1. Nathan Marz: “[Cascalog](https://github.com/nathanmarz/cascalog)," *github.com*.
1. Dennis A. Benson, Ilene Karsch-Mizrachi, David J. Lipman, et al.: “[GenBank](https://academic.oup.com/nar/article/36/suppl_1/D25/2507746),” *Nucleic Acids Research*, volume 36, Database issue, pages D25D30, December 2007. [doi:10.1093/nar/gkm929](http://dx.doi.org/10.1093/nar/gkm929)
1. Fons Rademakers: “[ROOT for Big Data Analysis](https://indico.cern.ch/event/246453/contributions/1566610/attachments/423154/587535/ROOT-BigData-Analysis-London-2013.pdf),” at *Workshop on the Future of Big Data Management*, London, UK, June 2013.
------
| 上一章 | 目录 | 下一章 |
| -------------------------------------------- | ------------------------------- | ---------------------------- |
| [第一章:可靠性、可伸缩性和可维护性](ch1.md) | [设计数据密集型应用](README.md) | [第三章:存储与检索](ch3.md) |

View File

@@ -1,19 +1,20 @@
# 第三章:存储与检索
---
title: "第三章:存储与检索"
linkTitle: "3. 存储与检索"
weight: 103
breadcrumbs: false
---
![](img/ch3.png)
![](/img/ch3.png)
> 建立秩序,省却搜索
>
> —— 德国谚语
>
-------------------
[TOC]
一个数据库在最基础的层次上需要完成两件事情:当你把数据交给数据库时,它应当把数据存储起来;而后当你向数据库要数据时,它应当把数据返回给你。
在 [第二章](ch2.md) 中,我们讨论了数据模型和查询语言,即程序员将数据录入数据库的格式,以及再次要回数据的机制。在本章中我们会从数据库的视角来讨论同样的问题:数据库如何存储我们提供的数据,以及如何在我们需要时重新找到数据。
在 [第二章](/ch2) 中,我们讨论了数据模型和查询语言,即程序员将数据录入数据库的格式,以及再次要回数据的机制。在本章中我们会从数据库的视角来讨论同样的问题:数据库如何存储我们提供的数据,以及如何在我们需要时重新找到数据。
作为程序员,为什么要关心数据库内部存储与检索的机理?你可能不会去从头开始实现自己的存储引擎,但是你 **确实** 需要从许多可用的存储引擎中选择一个合适的。而且为了让存储引擎能在你的工作负载类型上运行良好,你也需要大致了解存储引擎在底层究竟做了什么。
@@ -82,9 +83,9 @@ $ cat database
键值存储与在大多数编程语言中可以找到的 **字典dictionary** 类型非常相似,通常字典都是用 **散列映射hash map****散列表hash table** 实现的。散列映射在许多算法教科书中都有描述【1,2】所以这里我们不会讨论它的工作细节。既然我们已经可以用散列映射来表示 **内存中** 的数据结构,为什么不使用它来索引 **硬盘上** 的数据呢?
假设我们的数据存储只是一个追加写入的文件,就像前面的例子一样,那么最简单的索引策略就是:保留一个内存中的散列映射,其中每个键都映射到数据文件中的一个字节偏移量,指明了可以找到对应值的位置,如 [图 3-1](img/fig3-1.png) 所示。当你将新的键值对追加写入文件中时,还要更新散列映射,以反映刚刚写入的数据的偏移量(这同时适用于插入新键与更新现有键)。当你想查找一个值时,使用散列映射来查找数据文件中的偏移量,**寻找seek** 该位置并读取该值即可。
假设我们的数据存储只是一个追加写入的文件,就像前面的例子一样,那么最简单的索引策略就是:保留一个内存中的散列映射,其中每个键都映射到数据文件中的一个字节偏移量,指明了可以找到对应值的位置,如 [图 3-1](/img/fig3-1.png) 所示。当你将新的键值对追加写入文件中时,还要更新散列映射,以反映刚刚写入的数据的偏移量(这同时适用于插入新键与更新现有键)。当你想查找一个值时,使用散列映射来查找数据文件中的偏移量,**寻找seek** 该位置并读取该值即可。
![](img/fig3-1.png)
![](/img/fig3-1.png)
**图 3-1 以类 CSV 格式存储键值对的日志,并使用内存散列映射进行索引。**
@@ -92,15 +93,15 @@ $ cat database
像 Bitcask 这样的存储引擎非常适合每个键的值经常更新的情况。例如键可能是某个猫咪视频的网址URL而值可能是该视频被播放的次数每次有人点击播放按钮时递增。在这种类型的工作负载中有很多写操作但是没有太多不同的键 —— 每个键有很多的写操作,但是将所有键保存在内存中是可行的。
到目前为止,我们只是在追加写入一个文件 —— 所以如何避免最终用完硬盘空间?一种好的解决方案是,将日志分为特定大小的 **段segment**,当日志增长到特定尺寸时关闭当前段文件,并开始写入一个新的段文件。然后,我们就可以对这些段进行 **压缩compaction**,如 [图 3-2](img/fig3-2.png) 所示。这里的压缩意味着在日志中丢弃重复的键,只保留每个键的最近更新。
到目前为止,我们只是在追加写入一个文件 —— 所以如何避免最终用完硬盘空间?一种好的解决方案是,将日志分为特定大小的 **段segment**,当日志增长到特定尺寸时关闭当前段文件,并开始写入一个新的段文件。然后,我们就可以对这些段进行 **压缩compaction**,如 [图 3-2](/img/fig3-2.png) 所示。这里的压缩意味着在日志中丢弃重复的键,只保留每个键的最近更新。
![](img/fig3-2.png)
![](/img/fig3-2.png)
**图 3-2 键值更新日志(统计猫咪视频的播放次数)的压缩,只保留每个键的最近值**
而且,由于压缩经常会使得段变得很小(假设在一个段内键被平均重写了好几次),我们也可以在执行压缩的同时将多个段合并在一起,如 [图 3-3](img/fig3-3.png) 所示。段被写入后永远不会被修改,所以合并的段被写入一个新的文件。冻结段的合并和压缩可以在后台线程中完成,这个过程进行的同时,我们仍然可以继续使用旧的段文件来正常提供读写请求。合并过程完成后,我们将读取请求转换为使用新合并的段而不是旧的段 —— 然后旧的段文件就可以简单地删除掉了。
而且,由于压缩经常会使得段变得很小(假设在一个段内键被平均重写了好几次),我们也可以在执行压缩的同时将多个段合并在一起,如 [图 3-3](/img/fig3-3.png) 所示。段被写入后永远不会被修改,所以合并的段被写入一个新的文件。冻结段的合并和压缩可以在后台线程中完成,这个过程进行的同时,我们仍然可以继续使用旧的段文件来正常提供读写请求。合并过程完成后,我们将读取请求转换为使用新合并的段而不是旧的段 —— 然后旧的段文件就可以简单地删除掉了。
![](img/fig3-3.png)
![](/img/fig3-3.png)
**图 3-3 同时执行压缩和分段合并**
@@ -130,7 +131,7 @@ $ cat database
乍一看,仅追加日志似乎很浪费:为什么不直接在文件里更新,用新值覆盖旧值?仅追加的设计之所以是个好的设计,有如下几个原因:
* 追加和分段合并都是顺序写入操作,通常比随机写入快得多,尤其是在磁性机械硬盘上。在某种程度上,顺序写入在基于闪存的 **固态硬盘SSD** 上也是好的选择【4】。我们将在“[比较 B 树和 LSM 树](#比较B树和LSM)”中进一步讨论这个问题。
* 追加和分段合并都是顺序写入操作,通常比随机写入快得多,尤其是在磁性机械硬盘上。在某种程度上,顺序写入在基于闪存的 **固态硬盘SSD** 上也是好的选择【4】。我们将在“[比较 B 树和 LSM 树](#比较b树和lsm)”中进一步讨论这个问题。
* 如果段文件是仅追加的或不可变的,并发和崩溃恢复就简单多了。例如,当一个数据值被更新的时候发生崩溃,你不用担心文件里将会同时包含旧值和新值各自的一部分。
* 合并旧段的处理也可以避免数据文件随着时间的推移而碎片化的问题。
@@ -144,23 +145,23 @@ $ cat database
### SSTables和LSM树
在 [图 3-3](img/fig3-3.png) 中,每个日志结构存储段都是一系列键值对。这些键值对按照它们写入的顺序排列,日志中稍后的值优先于日志中较早的相同键的值。除此之外,文件中键值对的顺序并不重要。
在 [图 3-3](/img/fig3-3.png) 中,每个日志结构存储段都是一系列键值对。这些键值对按照它们写入的顺序排列,日志中稍后的值优先于日志中较早的相同键的值。除此之外,文件中键值对的顺序并不重要。
现在我们可以对段文件的格式做一个简单的改变:要求键值对的序列按键排序。乍一看,这个要求似乎打破了我们使用顺序写入的能力,我们将稍后再回到这个问题。
我们把这个格式称为 **排序字符串表Sorted String Table**,简称 SSTable。我们还要求每个键只在每个合并的段文件中出现一次压缩过程已经保证。与使用散列索引的日志段相比SSTable 有几个大的优势:
1. 即使文件大于可用内存,合并段的操作仍然是简单而高效的。这种方法就像归并排序算法中使用的方法一样,如 [图 3-4](img/fig3-4.png) 所示:你开始并排读取多个输入文件,查看每个文件中的第一个键,复制最低的键(根据排序顺序)到输出文件,不断重复此步骤,将产生一个新的合并段文件,而且它也是也按键排序的。
1. 即使文件大于可用内存,合并段的操作仍然是简单而高效的。这种方法就像归并排序算法中使用的方法一样,如 [图 3-4](/img/fig3-4.png) 所示:你开始并排读取多个输入文件,查看每个文件中的第一个键,复制最低的键(根据排序顺序)到输出文件,不断重复此步骤,将产生一个新的合并段文件,而且它也是也按键排序的。
![](img/fig3-4.png)
![](/img/fig3-4.png)
**图 3-4 合并几个 SSTable 段,只保留每个键的最新值**
如果在几个输入段中出现相同的键,该怎么办?请记住,每个段都包含在一段时间内写入数据库的所有值。这意味着一个输入段中的所有值一定比另一个段中的所有值都更近(假设我们总是合并相邻的段)。当多个段包含相同的键时,我们可以保留最近段的值,并丢弃旧段中的值。
2. 为了在文件中找到一个特定的键,你不再需要在内存中保存所有键的索引。以 [图 3-5](img/fig3-5.png) 为例:假设你正在内存中寻找键 `handiwork`,但是你不知道这个键在段文件中的确切偏移量。然而,你知道 `handbag``handsome` 的偏移,而且由于排序特性,你知道 `handiwork` 必须出现在这两者之间。这意味着你可以跳到 `handbag` 的偏移位置并从那里扫描,直到你找到 `handiwork`(或没找到,如果该文件中没有该键)。
2. 为了在文件中找到一个特定的键,你不再需要在内存中保存所有键的索引。以 [图 3-5](/img/fig3-5.png) 为例:假设你正在内存中寻找键 `handiwork`,但是你不知道这个键在段文件中的确切偏移量。然而,你知道 `handbag``handsome` 的偏移,而且由于排序特性,你知道 `handiwork` 必须出现在这两者之间。这意味着你可以跳到 `handbag` 的偏移位置并从那里扫描,直到你找到 `handiwork`(或没找到,如果该文件中没有该键)。
![](img/fig3-5.png)
![](/img/fig3-5.png)
**图 3-5 具有内存索引的 SSTable**
@@ -168,7 +169,7 @@ $ cat database
[^i]: 如果所有的键与值都是定长的,你可以使用段文件上的二分查找并完全避免使用内存索引。然而实践中的键和值通常都是变长的,因此如果没有索引,就很难知道记录的分界点(前一条记录结束以及后一条记录开始的地方)。
3. 由于读取请求无论如何都需要扫描所请求范围内的多个键值对因此可以将这些记录分组为块block并在将其写入硬盘之前对其进行压缩如 [图 3-5](img/fig3-5.png) 中的阴影区域所示)[^译注i] 。稀疏内存索引中的每个条目都指向压缩块的开始处。除了节省硬盘空间之外,压缩还可以减少对 I/O 带宽的使用。
3. 由于读取请求无论如何都需要扫描所请求范围内的多个键值对因此可以将这些记录分组为块block并在将其写入硬盘之前对其进行压缩如 [图 3-5](/img/fig3-5.png) 中的阴影区域所示)[^译注i] 。稀疏内存索引中的每个条目都指向压缩块的开始处。除了节省硬盘空间之外,压缩还可以减少对 I/O 带宽的使用。
[^译注i]: 这里的压缩是 compression不是前文的 compaction请注意区分。
@@ -214,23 +215,23 @@ Lucene是一种全文搜索的索引引擎在 Elasticsearch 和 Solr 被
我们前面看到的日志结构索引将数据库分解为可变大小的段通常是几兆字节或更大的大小并且总是按顺序写入段。相比之下B 树将数据库分解成固定大小的 **块block****分页page**,传统上大小为 4KB有时会更大并且一次只能读取或写入一个页面。这种设计更接近于底层硬件因为硬盘空间也是按固定大小的块来组织的。
每个页面都可以使用地址或位置来标识,这允许一个页面引用另一个页面 —— 类似于指针,但在硬盘而不是在内存中。我们可以使用这些页面引用来构建一个页面树,如 [图 3-6](img/fig3-6.png) 所示。
每个页面都可以使用地址或位置来标识,这允许一个页面引用另一个页面 —— 类似于指针,但在硬盘而不是在内存中。我们可以使用这些页面引用来构建一个页面树,如 [图 3-6](/img/fig3-6.png) 所示。
![](img/fig3-6.png)
![](/img/fig3-6.png)
**图 3-6 使用 B 树索引查找一个键**
一个页面会被指定为 B 树的根;在索引中查找一个键时,就从这里开始。该页面包含几个键和对子页面的引用。每个子页面负责一段连续范围的键,根页面上每两个引用之间的键,表示相邻子页面管理的键的范围(边界)。
在 [图 3-6](img/fig3-6.png) 的例子中,我们正在寻找键 251 ,所以我们知道我们需要跟踪边界 200 和 300 之间的页面引用。这将我们带到一个类似的页面,进一步将 200 到 300 的范围拆分到子范围。
在 [图 3-6](/img/fig3-6.png) 的例子中,我们正在寻找键 251 ,所以我们知道我们需要跟踪边界 200 和 300 之间的页面引用。这将我们带到一个类似的页面,进一步将 200 到 300 的范围拆分到子范围。
最终我们将到达某个包含单个键的页面叶子页面leaf page该页面或者直接包含每个键的值或者包含了对可以找到值的页面的引用。
在 B 树的一个页面中对子页面的引用的数量称为 **分支因子branching factor**。例如,在 [图 3-6](img/fig3-6.png) 中,分支因子是 6。在实践中分支因子的大小取决于存储页面引用和范围边界所需的空间但这个值通常是几百。
在 B 树的一个页面中对子页面的引用的数量称为 **分支因子branching factor**。例如,在 [图 3-6](/img/fig3-6.png) 中,分支因子是 6。在实践中分支因子的大小取决于存储页面引用和范围边界所需的空间但这个值通常是几百。
如果要更新 B 树中现有键的值,需要搜索包含该键的叶子页面,更改该页面中的值,并将该页面写回到硬盘(对该页面的任何引用都将保持有效)。如果你想添加一个新的键,你需要找到其范围能包含新键的页面,并将其添加到该页面。如果页面中没有足够的可用空间容纳新键,则将其分成两个半满页面,并更新父页面以反映新的键范围分区,如 [图 3-7](img/fig3-7.png) 所示 [^ii]。
如果要更新 B 树中现有键的值,需要搜索包含该键的叶子页面,更改该页面中的值,并将该页面写回到硬盘(对该页面的任何引用都将保持有效)。如果你想添加一个新的键,你需要找到其范围能包含新键的页面,并将其添加到该页面。如果页面中没有足够的可用空间容纳新键,则将其分成两个半满页面,并更新父页面以反映新的键范围分区,如 [图 3-7](/img/fig3-7.png) 所示 [^ii]。
![](img/fig3-7.png)
![](/img/fig3-7.png)
**图 3-7 通过分割页面来生长 B 树**
@@ -254,7 +255,7 @@ B 树的基本底层写操作是用新数据覆写硬盘上的页面,并假定
由于 B 树已经存在了很久,所以并不奇怪这么多年下来有很多优化的设计被开发出来,仅举几例:
* 不同于覆写页面并维护 WAL 以支持崩溃恢复,一些数据库(如 LMDB使用写时复制方案【21】。经过修改的页面被写入到不同的位置并且还在树中创建了父页面的新版本以指向新的位置。这种方法对于并发控制也很有用我们将在 “[快照隔离和可重复读](ch7.md#快照隔离和可重复读)” 中看到。
* 不同于覆写页面并维护 WAL 以支持崩溃恢复,一些数据库(如 LMDB使用写时复制方案【21】。经过修改的页面被写入到不同的位置并且还在树中创建了父页面的新版本以指向新的位置。这种方法对于并发控制也很有用我们将在 “[快照隔离和可重复读](/ch7#快照隔离和可重复读)” 中看到。
* 我们可以通过不存储整个键,而是缩短其大小,来节省页面空间。特别是在树内部的页面上,键只需要提供足够的信息来充当键范围之间的边界。在页面中包含更多的键允许树具有更高的分支因子,因此也就允许更少的层级 [^iii]。
* 通常,页面可以放置在硬盘上的任何位置;没有什么要求相邻键范围的页面也放在硬盘上相邻的区域。如果某个查询需要按照排序顺序扫描大部分的键范围,那么这种按页面存储的布局可能会效率低下,因为每个页面的读取都需要执行一次硬盘查找。因此,许多 B 树的实现在布局树时会尽量使叶子页面按顺序出现在硬盘上。但是,随着树的增长,要维持这个顺序是很困难的。相比之下,由于 LSM 树在合并过程中一次性重写一大段存储,所以它们更容易使顺序键在硬盘上连续存储。
* 额外的指针被添加到树中。例如,每个叶子页面可以引用其左边和右边的兄弟页面,使得不用跳回父页面就能按顺序对键进行扫描。
@@ -284,13 +285,13 @@ LSM 树可以被压缩得更好,因此通常能比 B 树在硬盘上产生更
#### LSM树的缺点
日志结构存储的缺点是压缩过程有时会干扰正在进行的读写操作。尽管存储引擎尝试增量地执行压缩以尽量不影响并发访问,但是硬盘资源有限,所以很容易发生某个请求需要等待硬盘先完成昂贵的压缩操作。对吞吐量和平均响应时间的影响通常很小,但是日志结构化存储引擎在更高百分位的响应时间(请参阅 “[描述性能](ch1.md#描述性能)”)有时会相当长,而 B 树的行为则相对更具有可预测性【28】。
日志结构存储的缺点是压缩过程有时会干扰正在进行的读写操作。尽管存储引擎尝试增量地执行压缩以尽量不影响并发访问,但是硬盘资源有限,所以很容易发生某个请求需要等待硬盘先完成昂贵的压缩操作。对吞吐量和平均响应时间的影响通常很小,但是日志结构化存储引擎在更高百分位的响应时间(请参阅 “[描述性能](/ch1#描述性能)”)有时会相当长,而 B 树的行为则相对更具有可预测性【28】。
压缩的另一个问题出现在高写入吞吐量时:硬盘的有限写入带宽需要在初始写入(记录日志和刷新内存表到硬盘)和在后台运行的压缩线程之间共享。写入空数据库时,可以使用全硬盘带宽进行初始写入,但数据库越大,压缩所需的硬盘带宽就越多。
如果写入吞吐量很高,并且压缩没有仔细配置好,有可能导致压缩跟不上写入速率。在这种情况下,硬盘上未合并段的数量不断增加,直到硬盘空间用完,读取速度也会减慢,因为它们需要检查更多的段文件。通常情况下,即使压缩无法跟上,基于 SSTable 的存储引擎也不会限制传入写入的速率所以你需要进行明确的监控来检测这种情况【29,30】。
B 树的一个优点是每个键只存在于索引中的一个位置,而日志结构化的存储引擎可能在不同的段中有相同键的多个副本。这个方面使得 B 树在想要提供强大的事务语义的数据库中很有吸引力:在许多关系数据库中,事务隔离是通过在键范围上使用锁来实现的,在 B 树索引中这些锁可以直接附加到树上【5】。在 [第七章](ch7.md) 中,我们将更详细地讨论这一点。
B 树的一个优点是每个键只存在于索引中的一个位置,而日志结构化的存储引擎可能在不同的段中有相同键的多个副本。这个方面使得 B 树在想要提供强大的事务语义的数据库中很有吸引力:在许多关系数据库中,事务隔离是通过在键范围上使用锁来实现的,在 B 树索引中这些锁可以直接附加到树上【5】。在 [第七章](/ch7) 中,我们将更详细地讨论这一点。
B 树在数据库架构中是非常根深蒂固的,为许多工作负载都提供了始终如一的良好性能,所以它们不可能在短期内消失。在新的数据库中,日志结构化索引变得越来越流行。没有简单易行的办法来判断哪种类型的存储引擎对你的使用场景更好,所以需要通过一些测试来得到相关经验。
@@ -298,7 +299,7 @@ B 树在数据库架构中是非常根深蒂固的,为许多工作负载都提
到目前为止,我们只讨论了键值索引,它们就像关系模型中的 **主键primary key** 索引。主键唯一标识关系表中的一行,或文档数据库中的一个文档或图形数据库中的一个顶点。数据库中的其他记录可以通过其主键(或 ID引用该行 / 文档 / 顶点,索引就被用于解析这样的引用。
次级索引secondary indexes也很常见。在关系数据库中你可以使用 `CREATE INDEX` 命令在同一个表上创建多个次级索引而且这些索引通常对于有效地执行联接join而言至关重要。例如在 [第二章](ch2.md) 中的 [图 2-1](img/fig2-1.png) 中,很可能在 `user_id` 列上有一个次级索引,以便你可以在每个表中找到属于同一用户的所有行。
次级索引secondary indexes也很常见。在关系数据库中你可以使用 `CREATE INDEX` 命令在同一个表上创建多个次级索引而且这些索引通常对于有效地执行联接join而言至关重要。例如在 [第二章](/ch2) 中的 [图 2-1](/img/fig2-1.png) 中,很可能在 `user_id` 列上有一个次级索引,以便你可以在每个表中找到属于同一用户的所有行。
次级索引可以很容易地从键值索引构建。次级索引主要的不同是键不是唯一的即可能有许多行文档顶点具有相同的键。这可以通过两种方式来解决将匹配行标识符的列表作为索引里的值就像全文索引中的记录列表或者通过向每个键添加行标识符来使键唯一。无论哪种方式B 树和日志结构索引都可以用作次级索引。
@@ -368,7 +369,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
在早期的业务数据处理过程中,一次典型的数据库写入通常与一笔 *商业交易commercial transaction* 相对应:卖个货、向供应商下订单、支付员工工资等等。但随着数据库开始应用到那些不涉及到钱的领域,术语 **交易 / 事务transaction** 仍留了下来,用于指代一组读写操作构成的逻辑单元。
> 事务不一定具有 ACID原子性一致性隔离性和持久性属性。事务处理只是意味着允许客户端进行低延迟的读取和写入 —— 而不是只能定期运行(例如每天一次)的批处理作业。我们在 [第七章](ch7.md) 中讨论 ACID 属性,在 [第十章](ch10.md) 中讨论批处理。
> 事务不一定具有 ACID原子性一致性隔离性和持久性属性。事务处理只是意味着允许客户端进行低延迟的读取和写入 —— 而不是只能定期运行(例如每天一次)的批处理作业。我们在 [第七章](/ch7) 中讨论 ACID 属性,在 [第十章](/ch10) 中讨论批处理。
即使数据库开始被用于许多不同类型的数据,比如博客文章的评论、游戏中的动作、地址簿中的联系人等等,基本的访问模式仍然类似于处理商业交易。应用程序通常使用索引通过某个键找少量记录。根据用户的输入来插入或更新记录。由于这些应用程序是交互式的,这种访问模式被称为 **在线事务处理OLTP, OnLine Transaction Processing**
@@ -400,9 +401,9 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
这些 OLTP 系统往往对业务运作至关重要,因而通常会要求 **高可用****低延迟**。所以 DBA 会密切关注他们的 OLTP 数据库,他们通常不愿意让业务分析人员在 OLTP 数据库上运行临时的分析查询,因为这些查询通常开销巨大,会扫描大部分数据集,这会损害同时在执行的事务的性能。
相比之下,数据仓库是一个独立的数据库,分析人员可以查询他们想要的内容而不影响 OLTP 操作【48】。数据仓库包含公司各种 OLTP 系统中所有的只读数据副本。从 OLTP 数据库中提取数据(使用定期的数据转储或连续的更新流),转换成适合分析的模式,清理并加载到数据仓库中。将数据存入仓库的过程称为 “**抽取 - 转换 - 加载ETL**”,如 [图 3-8](img/fig3-8.png) 所示。
相比之下,数据仓库是一个独立的数据库,分析人员可以查询他们想要的内容而不影响 OLTP 操作【48】。数据仓库包含公司各种 OLTP 系统中所有的只读数据副本。从 OLTP 数据库中提取数据(使用定期的数据转储或连续的更新流),转换成适合分析的模式,清理并加载到数据仓库中。将数据存入仓库的过程称为 “**抽取 - 转换 - 加载ETL**”,如 [图 3-8](/img/fig3-8.png) 所示。
![](img/fig3-8.png)
![](/img/fig3-8.png)
**图 3-8 ETL 至数据仓库的简化提纲**
@@ -422,11 +423,11 @@ Teradata、Vertica、SAP HANA 和 ParAccel 等数据仓库供应商通常使用
### 星型和雪花型:分析的模式
正如 [第二章](ch2.md) 所探讨的根据应用程序的需要在事务处理领域中使用了大量不同的数据模型。另一方面在分析型业务中数据模型的多样性则少得多。许多数据仓库都以相当公式化的方式使用被称为星型模式也称为维度建模【55】
正如 [第二章](/ch2) 所探讨的根据应用程序的需要在事务处理领域中使用了大量不同的数据模型。另一方面在分析型业务中数据模型的多样性则少得多。许多数据仓库都以相当公式化的方式使用被称为星型模式也称为维度建模【55】
[图 3-9](img/fig3-9.png) 中的示例模式显示了可能在食品零售商处找到的数据仓库。在模式的中心是一个所谓的事实表(在这个例子中,它被称为 `fact_sales`)。事实表的每一行代表在特定时间发生的事件(这里,每一行代表客户购买的产品)。如果我们分析的是网站流量而不是零售量,则每行可能代表一个用户的页面浏览或点击。
[图 3-9](/img/fig3-9.png) 中的示例模式显示了可能在食品零售商处找到的数据仓库。在模式的中心是一个所谓的事实表(在这个例子中,它被称为 `fact_sales`)。事实表的每一行代表在特定时间发生的事件(这里,每一行代表客户购买的产品)。如果我们分析的是网站流量而不是零售量,则每行可能代表一个用户的页面浏览或点击。
![](img/fig3-9.png)
![](/img/fig3-9.png)
**图 3-9 用于数据仓库的星型模式的示例**
@@ -434,7 +435,7 @@ Teradata、Vertica、SAP HANA 和 ParAccel 等数据仓库供应商通常使用
事实表中的一些列是属性,例如产品销售的价格和从供应商那里购买的成本(可以用来计算利润率)。事实表中的其他列是对其他表(称为维度表)的外键引用。由于事实表中的每一行都表示一个事件,因此这些维度代表事件发生的对象、内容、地点、时间、方式和原因。
例如,在 [图 3-9](img/fig3-9.png) 中,其中一个维度是已售出的产品。`dim_product` 表中的每一行代表一种待售产品包括库存单位SKU、产品描述、品牌名称、类别、脂肪含量、包装尺寸等。`fact_sales` 表中的每一行都使用外键表明在特定交易中销售了什么产品。(简单起见,如果客户一次购买了几种不同的产品,则它们在事实表中被表示为单独的行)。
例如,在 [图 3-9](/img/fig3-9.png) 中,其中一个维度是已售出的产品。`dim_product` 表中的每一行代表一种待售产品包括库存单位SKU、产品描述、品牌名称、类别、脂肪含量、包装尺寸等。`fact_sales` 表中的每一行都使用外键表明在特定交易中销售了什么产品。(简单起见,如果客户一次购买了几种不同的产品,则它们在事实表中被表示为单独的行)。
甚至日期和时间也通常使用维度表来表示,因为这允许对日期的附加信息(诸如公共假期)进行编码,从而允许区分假期和非假期的销售查询。
@@ -470,13 +471,13 @@ GROUP BY
我们如何有效地执行这个查询?
在大多数 OLTP 数据库中,存储都是以面向行的方式进行布局的:表格的一行中的所有值都相邻存储。文档数据库也是相似的:整个文档通常存储为一个连续的字节序列。你可以在 [图 3-1](img/fig3-1.png) 的 CSV 例子中看到这个。
在大多数 OLTP 数据库中,存储都是以面向行的方式进行布局的:表格的一行中的所有值都相邻存储。文档数据库也是相似的:整个文档通常存储为一个连续的字节序列。你可以在 [图 3-1](/img/fig3-1.png) 的 CSV 例子中看到这个。
为了处理像 [例 3-1]() 这样的查询,你可能在 `fact_sales.date_key``fact_sales.product_sk` 上有索引,它们告诉存储引擎在哪里查找特定日期或特定产品的所有销售情况。但是,面向行的存储引擎仍然需要将所有这些行(每个包含超过 100 个属性)从硬盘加载到内存中,解析它们,并过滤掉那些不符合要求的属性。这可能需要很长时间。
列式存储背后的想法很简单:不要将所有来自一行的值存储在一起,而是将来自每一列的所有值存储在一起。如果每个列式存储在一个单独的文件中,查询只需要读取和解析查询中使用的那些列,这可以节省大量的工作。这个原理如 [图 3-10](img/fig3-10.png) 所示。
列式存储背后的想法很简单:不要将所有来自一行的值存储在一起,而是将来自每一列的所有值存储在一起。如果每个列式存储在一个单独的文件中,查询只需要读取和解析查询中使用的那些列,这可以节省大量的工作。这个原理如 [图 3-10](/img/fig3-10.png) 所示。
![](img/fig3-10.png)
![](/img/fig3-10.png)
**图 3-10 按列存储关系型数据,而不是行**
@@ -489,9 +490,9 @@ GROUP BY
除了仅从硬盘加载查询所需的列以外,我们还可以通过压缩数据来进一步降低对硬盘吞吐量的需求。幸运的是,列式存储通常很适合压缩。
看看 [图 3-10](img/fig3-10.png) 中每一列的值序列:它们通常看起来是相当重复的,这是压缩的好兆头。根据列中的数据,可以使用不同的压缩技术。在数据仓库中特别有效的一种技术是位图编码,如 [图 3-11](img/fig3-11.png) 所示。
看看 [图 3-10](/img/fig3-10.png) 中每一列的值序列:它们通常看起来是相当重复的,这是压缩的好兆头。根据列中的数据,可以使用不同的压缩技术。在数据仓库中特别有效的一种技术是位图编码,如 [图 3-11](/img/fig3-11.png) 所示。
![](img/fig3-11.png)
![](/img/fig3-11.png)
**图 3-11 压缩的位图索引存储布局**
@@ -535,9 +536,9 @@ WHERE product_sk = 31 AND store_sk = 3
相反,数据的排序需要对一整行统一操作,即使它们的存储方式是按列的。数据库管理员可以根据他们对常用查询的了解,来选择表格中用来排序的列。例如,如果查询通常以日期范围为目标,例如“上个月”,则可以将 `date_key` 作为第一个排序键。这样查询优化器就可以只扫描近1个月范围的行了这比扫描所有行要快得多。
对于第一排序列中具有相同值的行,可以用第二排序列来进一步排序。例如,如果 `date_key` 是 [图 3-10](img/fig3-10.png) 中的第一个排序关键字,那么 `product_sk` 可能是第二个排序关键字,以便同一天的同一产品的所有销售数据都被存储在相邻位置。这将有助于需要在特定日期范围内按产品对销售进行分组或过滤的查询。
对于第一排序列中具有相同值的行,可以用第二排序列来进一步排序。例如,如果 `date_key` 是 [图 3-10](/img/fig3-10.png) 中的第一个排序关键字,那么 `product_sk` 可能是第二个排序关键字,以便同一天的同一产品的所有销售数据都被存储在相邻位置。这将有助于需要在特定日期范围内按产品对销售进行分组或过滤的查询。
按顺序排序的另一个好处是它可以帮助压缩列。如果主要排序列没有太多个不同的值,那么在排序之后,将会得到一个相同的值连续重复多次的序列。一个简单的游程编码(就像我们用于 [图 3-11](img/fig3-11.png) 中的位图一样)可以将该列压缩到几 KB —— 即使表中有数十亿行。
按顺序排序的另一个好处是它可以帮助压缩列。如果主要排序列没有太多个不同的值,那么在排序之后,将会得到一个相同的值连续重复多次的序列。一个简单的游程编码(就像我们用于 [图 3-11](/img/fig3-11.png) 中的位图一样)可以将该列压缩到几 KB —— 即使表中有数十亿行。
第一个排序键的压缩效果最强。第二和第三个排序键会更混乱,因此不会有这么长的连续的重复值。排序优先级更低的列以几乎随机的顺序出现,所以可能不会被压缩。但对前几列做排序在整体上仍然是有好处的。
@@ -567,13 +568,13 @@ WHERE product_sk = 31 AND store_sk = 3
当底层数据发生变化时,物化视图需要更新,因为它是数据的非规范化副本。数据库可以自动完成该操作,但是这样的更新使得写入成本更高,这就是在 OLTP 数据库中不经常使用物化视图的原因。在读取繁重的数据仓库中,它们可能更有意义(它们是否实际上改善了读取性能取决于使用场景)。
物化视图的常见特例称为数据立方体或 OLAP 立方【64】。它是按不同维度分组的聚合网格。[图 3-12](img/fig3-12.png) 显示了一个例子。
物化视图的常见特例称为数据立方体或 OLAP 立方【64】。它是按不同维度分组的聚合网格。[图 3-12](/img/fig3-12.png) 显示了一个例子。
![](img/fig3-12.png)
![](/img/fig3-12.png)
**图 3-12 数据立方的两个维度,通过求和聚合**
想象一下,现在每个事实都只有两个维度表的外键 —— 在 [图 3-12](img/fig3-12.png) 中分别是日期和产品。你现在可以绘制一个二维表格,一个轴线上是日期,另一个轴线上是产品。每个单元格包含具有该日期 - 产品组合的所有事实的属性(例如 `net_price`)的聚合(例如 `SUM`)。然后,你可以沿着每行或每列应用相同的汇总,并获得减少了一个维度的汇总(按产品的销售额,无论日期,或者按日期的销售额,无论产品)。
想象一下,现在每个事实都只有两个维度表的外键 —— 在 [图 3-12](/img/fig3-12.png) 中分别是日期和产品。你现在可以绘制一个二维表格,一个轴线上是日期,另一个轴线上是产品。每个单元格包含具有该日期 - 产品组合的所有事实的属性(例如 `net_price`)的聚合(例如 `SUM`)。然后,你可以沿着每行或每列应用相同的汇总,并获得减少了一个维度的汇总(按产品的销售额,无论日期,或者按日期的销售额,无论产品)。
一般来说,事实往往有两个以上的维度。在图 3-9 中有五个维度:日期、产品、商店、促销和客户。要想象一个五维超立方体是什么样子是很困难的,但是原理是一样的:每个单元格都包含特定日期 - 产品 - 商店 - 促销 - 客户组合的销售额。这些值可以在每个维度上求和汇总。
@@ -673,10 +674,3 @@ WHERE product_sk = 31 AND store_sk = 3
1. Andrew Lamb, Matt Fuller, Ramakrishna Varadarajan, et al.: “[The Vertica Analytic Database: C-Store 7 Years Later](http://vldb.org/pvldb/vol5/p1790_andrewlamb_vldb2012.pdf),” *Proceedings of the VLDB Endowment*, volume 5, number 12, pages 17901801, August 2012.
1. Julien Le Dem and Nong Li: “[Efficient Data Storage for Analytics with Apache Parquet 2.0](http://www.slideshare.net/julienledem/th-210pledem),” at *Hadoop Summit*, San Jose, June 2014.
1. Jim Gray, Surajit Chaudhuri, Adam Bosworth, et al.: “[Data Cube: A Relational Aggregation Operator Generalizing Group-By, Cross-Tab, and Sub-Totals](http://arxiv.org/pdf/cs/0701155.pdf),” *Data Mining and Knowledge Discovery*, volume 1, number 1, pages 2953, March 2007. [doi:10.1023/A:1009726021843](http://dx.doi.org/10.1023/A:1009726021843)
------
| 上一章 | 目录 | 下一章 |
| ------------------------------------ | ------------------------------- | ---------------------------- |
| [第二章:数据模型与查询语言](ch2.md) | [设计数据密集型应用](README.md) | [第四章:编码与演化](ch4.md) |

View File

@@ -1,21 +1,22 @@
# 第四章:编码与演化
---
title: "第四章:编码与演化"
linkTitle: "4. 编码与演化"
weight: 104
breadcrumbs: false
---
![](img/ch4.png)
![](/img/ch4.png)
> 唯变所适
>
> —— 以弗所的赫拉克利特,为柏拉图所引(公元前 360 年)
>
-------------------
[TOC]
应用程序不可避免地随时间而变化。新产品的推出,对需求的深入理解,或者商业环境的变化,总会伴随着 **功能feature** 的增增改改。[第一章](ch1.md) 介绍了 **可演化性evolvability** 的概念:应该尽力构建能灵活适应变化的系统(请参阅 “[可演化性:拥抱变化](ch1.md#可演化性:拥抱变化)”)。
应用程序不可避免地随时间而变化。新产品的推出,对需求的深入理解,或者商业环境的变化,总会伴随着 **功能feature** 的增增改改。[第一章](/ch1) 介绍了 **可演化性evolvability** 的概念:应该尽力构建能灵活适应变化的系统(请参阅 “[可演化性:拥抱变化](/ch1#可演化性:拥抱变化)”)。
在大多数情况下,修改应用程序的功能也意味着需要更改其存储的数据:可能需要使用新的字段或记录类型,或者以新方式展示现有数据。
我们在 [第二章](ch2.md) 讨论的数据模型有不同的方法来应对这种变化。关系数据库通常假定数据库中的所有数据都遵循一个模式:尽管可以更改该模式(通过模式迁移,即 `ALTER` 语句),但是在任何时间点都有且仅有一个正确的模式。相比之下,**读时模式**schema-on-read**无模式**,即 schemaless数据库不会强制一个模式因此数据库可以包含在不同时间写入的新老数据格式的混合请参阅 “[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)” )。
我们在 [第二章](/ch2) 讨论的数据模型有不同的方法来应对这种变化。关系数据库通常假定数据库中的所有数据都遵循一个模式:尽管可以更改该模式(通过模式迁移,即 `ALTER` 语句),但是在任何时间点都有且仅有一个正确的模式。相比之下,**读时模式**schema-on-read**无模式**,即 schemaless数据库不会强制一个模式因此数据库可以包含在不同时间写入的新老数据格式的混合请参阅 “[文档模型中的模式灵活性](/ch2#文档模型中的模式灵活性)” )。
当数据 **格式format****模式schema** 发生变化时,通常需要对应用程序代码进行相应的更改(例如,为记录添加新字段,然后修改程序开始读写该字段)。但在大型应用程序中,代码变更通常不会立即完成:
@@ -45,7 +46,7 @@
1. 在内存中,数据保存在对象、结构体、列表、数组、散列表、树等中。这些数据结构针对 CPU 的高效访问和操作进行了优化(通常使用指针)。
2. 如果要将数据写入文件,或通过网络发送,则必须将其 **编码encode** 为某种自包含的字节序列例如JSON 文档)。由于每个进程都有自己独立的地址空间,一个进程中的指针对任何其他进程都没有意义,所以这个字节序列表示会与通常在内存中使用的数据结构完全不同 [^i]。
[^i]: 除一些特殊情况外,例如某些内存映射文件或直接在压缩数据上操作(如 “[列压缩](ch3.md#列压缩)” 中所述)。
[^i]: 除一些特殊情况外,例如某些内存映射文件或直接在压缩数据上操作(如 “[列压缩](/ch3#列压缩)” 中所述)。
所以,需要在两种表示之间进行某种类型的翻译。从内存中表示到字节序列的转换称为 **编码Encoding** (也称为 **序列化serialization****编组marshalling**),反过来称为 **解码Decoding**[^ii]**解析Parsing****反序列化deserialization****反编组unmarshalling**[^译i]。
@@ -53,7 +54,7 @@
[^译i]: Marshal 与 Serialization 的区别Marshal 不仅传输对象的状态,而且会一起传输对象的方法(相关代码)。
> #### 术语冲突
> 不幸的是,在 [第七章](ch7.md) **事务Transaction** 的上下文里,**序列化Serialization** 这个术语也出现了,而且具有完全不同的含义。尽管序列化可能是更常见的术语,为了避免术语重载,本书中坚持使用 **编码Encoding** 表达此含义。
> 不幸的是,在 [第七章](/ch7) **事务Transaction** 的上下文里,**序列化Serialization** 这个术语也出现了,而且具有完全不同的含义。尽管序列化可能是更常见的术语,为了避免术语重载,本书中坚持使用 **编码Encoding** 表达此含义。
这是一个常见的问题,因而有许多库和编码格式可供选择。首先让我们概览一下。
@@ -113,7 +114,7 @@ JSON 比 XML 简洁,但与二进制格式相比还是太占空间。这一事
在下面的章节中,能达到比这好得多的结果,只用 32 个字节对相同的记录进行编码。
![](img/fig4-1.png)
![](/img/fig4-1.png)
**图 4-1 使用 MessagePack 编码的记录(例 4-1**
@@ -141,27 +142,27 @@ message Person {
```
Thrift 和 Protocol Buffers 每一个都带有一个代码生成工具它采用了类似于这里所示的模式定义并且生成了以各种编程语言实现模式的类【18】。你的应用程序代码可以调用此生成的代码来对模式的记录进行编码或解码。
用这个模式编码的数据是什么样的令人困惑的是Thrift 有两种不同的二进制编码格式 [^iii],分别称为 BinaryProtocol 和 CompactProtocol。先来看看 BinaryProtocol。使用这种格式的编码来编码 [例 4-1]() 中的消息只需要 59 个字节,如 [图 4-2](img/fig4-2.png) 所示【19】。
用这个模式编码的数据是什么样的令人困惑的是Thrift 有两种不同的二进制编码格式 [^iii],分别称为 BinaryProtocol 和 CompactProtocol。先来看看 BinaryProtocol。使用这种格式的编码来编码 [例 4-1]() 中的消息只需要 59 个字节,如 [图 4-2](/img/fig4-2.png) 所示【19】。
![](img/fig4-2.png)
![](/img/fig4-2.png)
**图 4-2 使用 Thrift 二进制协议编码的记录**
[^iii]: 实际上Thrift 有三种二进制协议BinaryProtocol、CompactProtocol 和 DenseProtocol尽管 DenseProtocol 只支持 C ++ 实现所以不算作跨语言【18】。除此之外它还有两种不同的基于 JSON 的编码格式【19】。真逗
与 [图 4-1](Img/fig4-1.png) 类似,每个字段都有一个类型注释(用于指示它是一个字符串、整数、列表等),还可以根据需要指定长度(字符串的长度,列表中的项目数) 。出现在数据中的字符串 `(“Martin”, “daydreaming”, “hacking”)` 也被编码为 ASCII或者说UTF-8与之前类似。
与 [图 4-1](/img/fig4-1.png) 类似,每个字段都有一个类型注释(用于指示它是一个字符串、整数、列表等),还可以根据需要指定长度(字符串的长度,列表中的项目数) 。出现在数据中的字符串 `(“Martin”, “daydreaming”, “hacking”)` 也被编码为 ASCII或者说UTF-8与之前类似。
与 [图 4-1](img/fig4-1.png) 相比,最大的区别是没有字段名 `(userName, favoriteNumber, interests)`。相反,编码数据包含字段标签,它们是数字 `(1, 2 和 3)`。这些是模式定义中出现的数字。字段标记就像字段的别名 - 它们是说我们正在谈论的字段的一种紧凑的方式,而不必拼出字段名称。
与 [图 4-1](/img/fig4-1.png) 相比,最大的区别是没有字段名 `(userName, favoriteNumber, interests)`。相反,编码数据包含字段标签,它们是数字 `(1, 2 和 3)`。这些是模式定义中出现的数字。字段标记就像字段的别名 - 它们是说我们正在谈论的字段的一种紧凑的方式,而不必拼出字段名称。
Thrift CompactProtocol 编码在语义上等同于 BinaryProtocol但是如 [图 4-3](img/fig4-3.png) 所示,它只将相同的信息打包成只有 34 个字节。它通过将字段类型和标签号打包到单个字节中,并使用可变长度整数来实现。数字 1337 不是使用全部八个字节,而是用两个字节编码,每个字节的最高位用来指示是否还有更多的字节。这意味着 - 64 到 63 之间的数字被编码为一个字节,-8192 和 8191 之间的数字以两个字节编码,等等。较大的数字使用更多的字节。
Thrift CompactProtocol 编码在语义上等同于 BinaryProtocol但是如 [图 4-3](/img/fig4-3.png) 所示,它只将相同的信息打包成只有 34 个字节。它通过将字段类型和标签号打包到单个字节中,并使用可变长度整数来实现。数字 1337 不是使用全部八个字节,而是用两个字节编码,每个字节的最高位用来指示是否还有更多的字节。这意味着 - 64 到 63 之间的数字被编码为一个字节,-8192 和 8191 之间的数字以两个字节编码,等等。较大的数字使用更多的字节。
![](img/fig4-3.png)
![](/img/fig4-3.png)
**图 4-3 使用 Thrift 压缩协议编码的记录**
最后Protocol Buffers只有一种二进制编码格式对相同的数据进行编码如 [图 4-4](img/fig4-4.png) 所示。它的打包方式稍有不同,但与 Thrift 的 CompactProtocol 非常相似。Protobuf 将同样的记录塞进了 33 个字节中。
最后Protocol Buffers只有一种二进制编码格式对相同的数据进行编码如 [图 4-4](/img/fig4-4.png) 所示。它的打包方式稍有不同,但与 Thrift 的 CompactProtocol 非常相似。Protobuf 将同样的记录塞进了 33 个字节中。
![](img/fig4-4.png)
![](/img/fig4-4.png)
**图 4-4 使用 Protobuf 编码的记录**
@@ -183,7 +184,7 @@ Thrift CompactProtocol 编码在语义上等同于 BinaryProtocol但是如 [
如何改变字段的数据类型?这也许是可能的 —— 详细信息请查阅相关的文档 —— 但是有一个风险,值将失去精度或被截断。例如,假设你将一个 32 位的整数变成一个 64 位的整数。新代码可以轻松读取旧代码写入的数据,因为解析器可以用零填充任何缺失的位。但是,如果旧代码读取由新代码写入的数据,则旧代码仍使用 32 位变量来保存该值。如果解码的 64 位值不适合 32 位,则它将被截断。
Protobuf 的一个奇怪的细节是,它没有列表或数组数据类型,而是有一个字段的重复标记(`repeated`,这是除必需和可选之外的第三个选项)。如 [图 4-4](img/fig4-4.png) 所示,重复字段的编码正如它所说的那样:同一个字段标记只是简单地出现在记录中。这具有很好的效果,可以将可选(单值)字段更改为重复(多值)字段。读取旧数据的新代码会看到一个包含零个或一个元素的列表(取决于该字段是否存在)。读取新数据的旧代码只能看到列表的最后一个元素。
Protobuf 的一个奇怪的细节是,它没有列表或数组数据类型,而是有一个字段的重复标记(`repeated`,这是除必需和可选之外的第三个选项)。如 [图 4-4](/img/fig4-4.png) 所示,重复字段的编码正如它所说的那样:同一个字段标记只是简单地出现在记录中。这具有很好的效果,可以将可选(单值)字段更改为重复(多值)字段。读取旧数据的新代码会看到一个包含零个或一个元素的列表(取决于该字段是否存在)。读取新数据的旧代码只能看到列表的最后一个元素。
Thrift 有一个专用的列表数据类型,它使用列表元素的数据类型进行参数化。这不允许 Protocol Buffers 所做的从单值到多值的演变,但是它具有支持嵌套列表的优点。
@@ -217,11 +218,11 @@ record Person {
}
```
首先,请注意模式中没有标签号码。如果我们使用这个模式编码我们的例子记录([例 4-1]()Avro 二进制编码只有 32 个字节长,这是我们所见过的所有编码中最紧凑的。编码字节序列的分解如 [图 4-5](img/fig4-5.png) 所示。
首先,请注意模式中没有标签号码。如果我们使用这个模式编码我们的例子记录([例 4-1]()Avro 二进制编码只有 32 个字节长,这是我们所见过的所有编码中最紧凑的。编码字节序列的分解如 [图 4-5](/img/fig4-5.png) 所示。
如果你检查字节序列,你可以看到没有什么可以识别字段或其数据类型。编码只是由连在一起的值组成。一个字符串只是一个长度前缀,后跟 UTF-8 字节,但是在被包含的数据中没有任何内容告诉你它是一个字符串。它可以是一个整数,也可以是其他的整数。整数使用可变长度编码(与 Thrift 的 CompactProtocol 相同)进行编码。
![](img/fig4-5.png)
![](/img/fig4-5.png)
**图 4-5 使用 Avro 编码的记录**
@@ -235,11 +236,11 @@ record Person {
当一个应用程序想要解码一些数据(从一个文件或数据库读取数据、从网络接收数据等)时,它希望数据在某个模式中,这就是 Reader 模式。这是应用程序代码所依赖的模式,在应用程序的构建过程中,代码可能已经从该模式生成。
Avro 的关键思想是 Writer 模式和 Reader 模式不必是相同的 - 他们只需要兼容。当数据解码读取Avro 库通过并排查看 Writer 模式和 Reader 模式并将数据从 Writer 模式转换到 Reader 模式来解决差异。Avro 规范【20】确切地定义了这种解析的工作原理如 [图 4-6](img/fig4-6.png) 所示。
Avro 的关键思想是 Writer 模式和 Reader 模式不必是相同的 - 他们只需要兼容。当数据解码读取Avro 库通过并排查看 Writer 模式和 Reader 模式并将数据从 Writer 模式转换到 Reader 模式来解决差异。Avro 规范【20】确切地定义了这种解析的工作原理如 [图 4-6](/img/fig4-6.png) 所示。
例如,如果 Writer 模式和 Reader 模式的字段顺序不同,这是没有问题的,因为模式解析通过字段名匹配字段。如果读取数据的代码遇到出现在 Writer 模式中但不在 Reader 模式中的字段,则忽略它。如果读取数据的代码需要某个字段,但是 Writer 模式不包含该名称的字段,则使用在 Reader 模式中声明的默认值填充。
![](img/fig4-6.png)
![](/img/fig4-6.png)
**图 4-6 一个 Avro Reader 解决读写模式的差异**
@@ -265,7 +266,7 @@ Avro 的关键思想是 Writer 模式和 Reader 模式不必是相同的 - 他
* 有很多记录的大文件
Avro 的一个常见用途 - 尤其是在 Hadoop 环境中 - 用于存储包含数百万条记录的大文件,所有记录都使用相同的模式进行编码(我们将在 [第十章](ch10.md) 讨论这种情况)。在这种情况下,该文件的作者可以在文件的开头只包含一次 Writer 模式。Avro 指定了一个文件格式(对象容器文件)来做到这一点。
Avro 的一个常见用途 - 尤其是在 Hadoop 环境中 - 用于存储包含数百万条记录的大文件,所有记录都使用相同的模式进行编码(我们将在 [第十章](/ch10) 讨论这种情况)。在这种情况下,该文件的作者可以在文件的开头只包含一次 Writer 模式。Avro 指定了一个文件格式(对象容器文件)来做到这一点。
* 支持独立写入的记录的数据库
@@ -312,7 +313,7 @@ Avro 为静态类型编程语言提供了可选的代码生成功能,但是它
* 维护一个模式的数据库允许你在部署任何内容之前检查模式更改的向前和向后兼容性。
* 对于静态类型编程语言的用户来说,从模式生成代码的能力是有用的,因为它可以在编译时进行类型检查。
总而言之,模式演化保持了与 JSON 数据库提供的无模式 / 读时模式相同的灵活性(请参阅 “[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)”),同时还可以更好地保证你的数据并提供更好的工具。
总而言之,模式演化保持了与 JSON 数据库提供的无模式 / 读时模式相同的灵活性(请参阅 “[文档模型中的模式灵活性](/ch2#文档模型中的模式灵活性)”),同时还可以更好地保证你的数据并提供更好的工具。
## 数据流的类型
@@ -342,7 +343,7 @@ Avro 为静态类型编程语言提供了可选的代码生成功能,但是它
前面讨论的编码格式支持未知字段的保存,但是有时候需要在应用程序层面保持谨慎,如图 4-7 所示。例如,如果将数据库值解码为应用程序中的模型对象,稍后重新编码这些模型对象,那么未知字段可能会在该翻译过程中丢失。解决这个问题不是一个难题,你只需要意识到它。
![](img/fig4-7.png)
![](/img/fig4-7.png)
**图 4-7 当较旧版本的应用程序更新以前由较新版本的应用程序编写的数据时,如果不小心,数据可能会丢失。**
@@ -356,16 +357,16 @@ Avro 为静态类型编程语言提供了可选的代码生成功能,但是它
因此,模式演变允许整个数据库看起来好像是用单个模式编码的,即使底层存储可能包含用各种历史版本的模式编码的记录。
[^v]: 除了 MySQL即使并非真的必要它也经常会重写整个表正如 “[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)” 中所提到的。
[^v]: 除了 MySQL即使并非真的必要它也经常会重写整个表正如 “[文档模型中的模式灵活性](/ch2#文档模型中的模式灵活性)” 中所提到的。
#### 归档存储
也许你不时为数据库创建一个快照,例如备份或加载到数据仓库(请参阅 “[数据仓库](ch3.md#数据仓库)”)。在这种情况下,即使源数据库中的原始编码包含来自不同时代的模式版本的混合,数据转储通常也将使用最新模式进行编码。既然你不管怎样都要拷贝数据,那么你可以对这个数据拷贝进行一致的编码。
也许你不时为数据库创建一个快照,例如备份或加载到数据仓库(请参阅 “[数据仓库](/ch3#数据仓库)”)。在这种情况下,即使源数据库中的原始编码包含来自不同时代的模式版本的混合,数据转储通常也将使用最新模式进行编码。既然你不管怎样都要拷贝数据,那么你可以对这个数据拷贝进行一致的编码。
由于数据转储是一次写入的,而且以后是不可变的,所以 Avro 对象容器文件等格式非常适合。这也是一个很好的机会,可以将数据编码为面向分析的列式格式,例如 Parquet请参阅 “[列压缩](ch3.md#列压缩)”)。
由于数据转储是一次写入的,而且以后是不可变的,所以 Avro 对象容器文件等格式非常适合。这也是一个很好的机会,可以将数据编码为面向分析的列式格式,例如 Parquet请参阅 “[列压缩](/ch3#列压缩)”)。
在 [第十章](ch10.md) 中,我们将详细讨论使用档案存储中的数据。
在 [第十章](/ch10) 中,我们将详细讨论使用档案存储中的数据。
### 服务中的数据流REST与RPC
@@ -378,7 +379,7 @@ Web 浏览器不是唯一的客户端类型。例如,在移动设备或桌面
此外,服务器本身可以是另一个服务的客户端(例如,典型的 Web 应用服务器充当数据库的客户端)。这种方法通常用于将大型应用程序按照功能区域分解为较小的服务,这样当一个服务需要来自另一个服务的某些功能或数据时,就会向另一个服务发出请求。这种构建应用程序的方式传统上被称为 **面向服务的体系结构service-oriented architectureSOA**,最近被改进和更名为 **微服务架构**【31,32】。
在某些方面,服务类似于数据库:它们通常允许客户端提交和查询数据。但是,虽然数据库允许使用我们在 [第二章](ch2.md) 中讨论的查询语言进行任意查询,但是服务公开了一个特定于应用程序的 API它只允许由服务的业务逻辑应用程序代码预定的输入和输出【33】。这种限制提供了一定程度的封装服务能够对客户可以做什么和不可以做什么施加细粒度的限制。
在某些方面,服务类似于数据库:它们通常允许客户端提交和查询数据。但是,虽然数据库允许使用我们在 [第二章](/ch2) 中讨论的查询语言进行任意查询,但是服务公开了一个特定于应用程序的 API它只允许由服务的业务逻辑应用程序代码预定的输入和输出【33】。这种限制提供了一定程度的封装服务能够对客户可以做什么和不可以做什么施加细粒度的限制。
面向服务 / 微服务架构的一个关键设计目标是通过使服务独立部署和演化来使应用程序更易于更改和维护。例如,每个服务应该由一个团队拥有,并且该团队应该能够经常发布新版本的服务,而不必与其他团队协调。换句话说,我们应该期望服务器和客户端的旧版本和新版本同时运行,因此服务器和客户端使用的数据编码必须在不同版本的服务 API 之间兼容 —— 这正是我们在本章所一直在谈论的。
@@ -415,8 +416,8 @@ Web 服务仅仅是通过网络进行 API 请求的一系列技术的最新版
所有这些都是基于 **远程过程调用RPC** 的思想,该过程调用自 20 世纪 70 年代以来一直存在【42】。RPC 模型试图向远程网络服务发出请求,看起来与在同一进程中调用编程语言中的函数或方法相同(这种抽象称为位置透明)。尽管 RPC 起初看起来很方便但这种方法根本上是有缺陷的【43,44】。网络请求与本地函数调用非常不同
* 本地函数调用是可预测的,并且成功或失败仅取决于受你控制的参数。网络请求是不可预测的:请求或响应可能由于网络问题会丢失,或者远程计算机可能很慢或不可用,这些问题完全不在你的控制范围之内。网络问题很常见,因此必须有所准备,例如重试失败的请求。
* 本地函数调用要么返回结果,要么抛出异常,或者永远不返回(因为进入无限循环或进程崩溃)。网络请求有另一个可能的结果:由于超时,它返回时可能没有结果。在这种情况下,你根本不知道发生了什么:如果你没有得到来自远程服务的响应,你无法知道请求是否通过(我们将在 [第八章](ch8.md) 更详细地讨论这个问题)。
* 如果你重试失败的网络请求,可能会发生请求实际上已经完成,只是响应丢失的情况。在这种情况下,重试将导致该操作被执行多次,除非你在协议中建立数据去重机制(**幂等性**,即 idempotence。本地函数调用时没有这样的问题。在 [第十一章](ch11.md) 更详细地讨论幂等性)
* 本地函数调用要么返回结果,要么抛出异常,或者永远不返回(因为进入无限循环或进程崩溃)。网络请求有另一个可能的结果:由于超时,它返回时可能没有结果。在这种情况下,你根本不知道发生了什么:如果你没有得到来自远程服务的响应,你无法知道请求是否通过(我们将在 [第八章](/ch8) 更详细地讨论这个问题)。
* 如果你重试失败的网络请求,可能会发生请求实际上已经完成,只是响应丢失的情况。在这种情况下,重试将导致该操作被执行多次,除非你在协议中建立数据去重机制(**幂等性**,即 idempotence。本地函数调用时没有这样的问题。在 [第十一章](/ch11) 更详细地讨论幂等性)
* 每次调用本地函数时,通常需要大致相同的时间来执行。网络请求比函数调用要慢得多,而且其延迟也是非常可变的:好的时候它可能会在不到一毫秒的时间内完成,但是当网络拥塞或者远程服务超载时,可能需要几秒钟的时间才能完成相同的操作。
* 调用本地函数时,可以高效地将引用(指针)传递给本地内存中的对象。当你发出一个网络请求时,所有这些参数都需要被编码成可以通过网络发送的一系列字节。如果参数是像数字或字符串这样的基本类型倒是没关系,但是对于较大的对象很快就会出现问题。
* 客户端和服务可以用不同的编程语言实现,所以 RPC 框架必须将数据类型从一种语言翻译成另一种语言。这可能会变得很丑陋,因为不是所有的语言都具有相同的类型 —— 例如回想一下 JavaScript 的数字大于 $2^{53}$ 的问题(请参阅 “[JSON、XML 和二进制变体](#JSON、XML和二进制变体)”)。用单一语言编写的单个进程中不存在此问题。
@@ -429,7 +430,7 @@ Web 服务仅仅是通过网络进行 API 请求的一系列技术的最新版
这种新一代的 RPC 框架更加明确的是远程请求与本地函数调用不同。例如Finagle 和 Rest.li 使用 futurespromises来封装可能失败的异步操作。`Futures` 还可以简化需要并行发出多项服务并将其结果合并的情况【45】。gRPC 支持流其中一个调用不仅包括一个请求和一个响应还可以是随时间的一系列请求和响应【46】。
其中一些框架还提供服务发现,即允许客户端找出在哪个 IP 地址和端口号上可以找到特定的服务。我们将在 “[请求路由](ch6.md#请求路由)” 中回到这个主题。
其中一些框架还提供服务发现,即允许客户端找出在哪个 IP 地址和端口号上可以找到特定的服务。我们将在 “[请求路由](/ch6#请求路由)” 中回到这个主题。
使用二进制编码格式的自定义 RPC 协议可以实现比通用的 JSON over REST 更好的性能。但是RESTful API 还有其他一些显著的优点:方便实验和调试(只需使用 Web 浏览器或命令行工具 curl无需任何代码生成或软件安装即可向其请求能被所有主流的编程语言和平台所支持还有大量可用的工具服务器、缓存、负载平衡器、代理、防火墙、监控、调试工具、测试工具等的生态系统。
@@ -467,15 +468,15 @@ RPC 方案的向后和向前兼容性属性是从它使用的编码方式中继
#### 消息代理
过去,**消息代理Message Broker** 主要是 TIBCO、IBM WebSphere 和 webMethods 等公司的商业软件的秀场。最近像 RabbitMQ、ActiveMQ、HornetQ、NATS 和 Apache Kafka 这样的开源实现已经流行起来。我们将在 [第十一章](ch11.md) 中对它们进行更详细的比较。
过去,**消息代理Message Broker** 主要是 TIBCO、IBM WebSphere 和 webMethods 等公司的商业软件的秀场。最近像 RabbitMQ、ActiveMQ、HornetQ、NATS 和 Apache Kafka 这样的开源实现已经流行起来。我们将在 [第十一章](/ch11) 中对它们进行更详细的比较。
详细的交付语义因实现和配置而异,但通常情况下,消息代理的使用方式如下:一个进程将消息发送到指定的队列或主题,代理确保将消息传递给那个队列或主题的一个或多个消费者或订阅者。在同一主题上可以有许多生产者和许多消费者。
一个主题只提供单向数据流。但是,消费者本身可能会将消息发布到另一个主题上(因此,可以将它们链接在一起,就像我们将在 [第十一章](ch11.md) 中看到的那样),或者发送给原始消息的发送者使用的回复队列(允许请求 / 响应数据流,类似于 RPC
一个主题只提供单向数据流。但是,消费者本身可能会将消息发布到另一个主题上(因此,可以将它们链接在一起,就像我们将在 [第十一章](/ch11) 中看到的那样),或者发送给原始消息的发送者使用的回复队列(允许请求 / 响应数据流,类似于 RPC
消息代理通常不会执行任何特定的数据模型 —— 消息只是包含一些元数据的字节序列,因此你可以使用任何编码格式。如果编码是向后和向前兼容的,你可以灵活地对发布者和消费者的编码进行独立的修改,并以任意顺序进行部署。
如果消费者重新发布消息到另一个主题,则可能需要小心保留未知字段,以防止前面在数据库环境中描述的问题([图 4-7](img/fig4-7.png))。
如果消费者重新发布消息到另一个主题,则可能需要小心保留未知字段,以防止前面在数据库环境中描述的问题([图 4-7](/img/fig4-7.png))。
#### 分布式的Actor框架
@@ -573,9 +574,3 @@ Actor 模型是单个进程中并发的编程模型。逻辑被封装在 actor
1. “[Microsoft Project Orleans Documentation](http://dotnet.github.io/orleans/),” Microsoft Research, *dotnet.github.io*, 2015.
1. David Mercer, Sean Hinde, Yinso Chen, and Richard A O'Keefe: “[beginner: Updating Data Structures](http://erlang.org/pipermail/erlang-questions/2007-October/030318.html),” email thread on *erlang-questions* mailing list, *erlang.com*, October 29, 2007.
1. Fred Hebert: “[Postscript: Maps](http://learnyousomeerlang.com/maps),” *learnyousomeerlang.com*, April 9, 2014.
------
| 上一章 | 目录 | 下一章 |
| ---------------------------- | ------------------------------- | --------------------------------- |
| [第三章:存储与检索](ch3.md) | [设计数据密集型应用](README.md) | [第二部分:分布式数据](part-ii.md) |

View File

@@ -1,22 +1,24 @@
# 第五章:复制
---
title: "第五章:复制"
linkTitle: "5. 复制"
weight: 205
breadcrumbs: false
---
![](img/ch5.png)
![](/img/ch5.png)
> 与可能出错的东西比,“不可能”出错的东西最显著的特点就是:一旦真的出错,通常就彻底玩完了。
>
> —— 道格拉斯・亚当斯1992
------
[TOC]
复制意味着在通过网络连接的多台机器上保留相同数据的副本。正如在 [第二部分](part-ii.md) 的介绍中所讨论的那样,我们希望能复制数据,可能出于各种各样的原因:
复制意味着在通过网络连接的多台机器上保留相同数据的副本。正如在 [第二部分](/part-ii) 的介绍中所讨论的那样,我们希望能复制数据,可能出于各种各样的原因:
* 使得数据与用户在地理上接近(从而减少延迟)
* 即使系统的一部分出现故障,系统也能继续工作(从而提高可用性)
* 伸缩可以接受读请求的机器数量(从而提高读取吞吐量)
本章将假设你的数据集非常小,每台机器都可以保存整个数据集的副本。在 [第六章](ch6.md) 中将放宽这个假设,讨论对单个机器来说太大的数据集的分割(分片)。在后面的章节中,我们将讨论复制数据系统中可能发生的各种故障,以及如何处理这些故障。
本章将假设你的数据集非常小,每台机器都可以保存整个数据集的副本。在 [第六章](/ch6) 中将放宽这个假设,讨论对单个机器来说太大的数据集的分割(分片)。在后面的章节中,我们将讨论复制数据系统中可能发生的各种故障,以及如何处理这些故障。
如果复制中的数据不会随时间而改变,那复制就很简单:将数据复制到每个节点一次就万事大吉。复制的困难之处在于处理复制数据的 **变更change**,这就是本章所要讲的。我们将讨论三种流行的变更复制算法:**单领导者single leader单主****多领导者multi leader多主** 和 **无领导者leaderless无主**。几乎所有分布式数据库都使用这三种方法之一。
@@ -36,7 +38,7 @@
[^i]: 不同的人对 **热hot**、**温warm** 和 **冷cold** 备份服务器有不同的定义。例如在 PostgreSQL 中,**热备hot standby** 指的是能接受客户端读请求的副本。而 **温备warm standby** 只是追随领导者,但不处理客户端的任何查询。就本书而言,这些差异并不重要。
![](img/fig5-1.png)
![](/img/fig5-1.png)
**图 5-1 基于领导者的(主/从)复制**
@@ -48,13 +50,13 @@
想象一下 [图 5-1](fig5-1.png) 中发生的场景,即网站的用户更新他们的个人头像。在某个时间点,客户向主库发送更新请求;不久之后主库就收到了请求。在某个时间点,主库又会将数据变更转发给自己的从库。最终,主库通知客户更新成功。
[图 5-2](img/fig5-2.png) 显示了系统各个组件之间的通信:用户客户端、主库和两个从库。时间从左向右流动。请求或响应消息用粗箭头表示。
[图 5-2](/img/fig5-2.png) 显示了系统各个组件之间的通信:用户客户端、主库和两个从库。时间从左向右流动。请求或响应消息用粗箭头表示。
![](img/fig5-2.png)
![](/img/fig5-2.png)
**图 5-2 基于领导者的复制:一个同步从库和一个异步从库**
在 [图 5-2](img/fig5-2.png) 的示例中,从库 1 的复制是同步的:在向用户报告写入成功并使结果对其他用户可见之前,主库需要等待从库 1 的确认,确保从库 1 已经收到写入操作。而从库 2 的复制是异步的:主库发送消息,但不等待该从库的响应。
在 [图 5-2](/img/fig5-2.png) 的示例中,从库 1 的复制是同步的:在向用户报告写入成功并使结果对其他用户可见之前,主库需要等待从库 1 的确认,确保从库 1 已经收到写入操作。而从库 2 的复制是异步的:主库发送消息,但不等待该从库的响应。
在这幅图中,从库 2 处理消息前存在一个显著的延迟。通常情况下,复制的速度相当快:大多数数据库系统能在不到一秒内完成从库的同步,但它们不能提供复制用时的保证。有些情况下,从库可能落后主库几分钟或更久,例如:从库正在从故障中恢复,系统正在最大容量附近运行,或者当节点间存在网络问题时。
@@ -70,7 +72,7 @@
>
> 对于异步复制系统而言,主库故障时会丢失数据可能是一个严重的问题,因此研究人员仍在研究不丢数据但仍能提供良好性能和可用性的复制方法。例如,**链式复制chain replication**【8,9】是同步复制的一种变体已经在一些系统如 Microsoft Azure Storage【10,11】中成功实现。
>
> 复制的一致性与 **共识**consensus使几个节点就某个值达成一致之间有着密切的联系[第九章](ch9.md) 将详细地探讨这一领域的理论。本章主要讨论实践中的数据库常用的简单复制形式。
> 复制的一致性与 **共识**consensus使几个节点就某个值达成一致之间有着密切的联系[第九章](/ch9) 将详细地探讨这一领域的理论。本章主要讨论实践中的数据库常用的简单复制形式。
>
### 设置新从库
@@ -105,8 +107,8 @@
故障切换可以手动进行(通知管理员主库挂了,并采取必要的步骤来创建新的主库)或自动进行。自动的故障切换过程通常由以下步骤组成:
1. 确认主库失效。有很多事情可能会出错:崩溃、停电、网络问题等等。没有万无一失的方法来检测出现了什么问题,所以大多数系统只是简单使用 **超时Timeout** :节点频繁地相互来回传递消息,如果一个节点在一段时间内(例如 30 秒)没有响应,就认为它挂了(因为计划内维护而故意关闭主库不算)。
2. 选择一个新的主库。这可以通过选举过程(主库由剩余副本以多数选举产生)来完成,或者可以由之前选定的 **控制器节点controller node** 来指定新的主库。主库的最佳人选通常是拥有旧主库最新数据副本的从库(以最小化数据损失)。让所有的节点同意一个新的领导者,是一个 **共识** 问题,将在 [第九章](ch9.md) 详细讨论。
3. 重新配置系统以启用新的主库。客户端现在需要将它们的写请求发送给新主库(将在 “[请求路由](ch6.md#请求路由)” 中讨论这个问题)。如果旧主库恢复,可能仍然认为自己是主库,而没有意识到其他副本已经让它失去领导权了。系统需要确保旧主库意识到新主库的存在,并成为一个从库。
2. 选择一个新的主库。这可以通过选举过程(主库由剩余副本以多数选举产生)来完成,或者可以由之前选定的 **控制器节点controller node** 来指定新的主库。主库的最佳人选通常是拥有旧主库最新数据副本的从库(以最小化数据损失)。让所有的节点同意一个新的领导者,是一个 **共识** 问题,将在 [第九章](/ch9) 详细讨论。
3. 重新配置系统以启用新的主库。客户端现在需要将它们的写请求发送给新主库(将在 “[请求路由](/ch6#请求路由)” 中讨论这个问题)。如果旧主库恢复,可能仍然认为自己是主库,而没有意识到其他副本已经让它失去领导权了。系统需要确保旧主库意识到新主库的存在,并成为一个从库。
故障切换的过程中有很多地方可能出错:
@@ -114,15 +116,15 @@
* 如果数据库需要和其他外部存储相协调,那么丢弃写入内容是极其危险的操作。例如在 GitHub 【13】的一场事故中一个过时的 MySQL 从库被提升为主库。数据库使用自增 ID 作为主键,因为新主库的计数器落后于老主库的计数器,所以新主库重新分配了一些已经被老主库分配掉的 ID 作为主键。这些主键也在 Redis 中使用,主键重用使得 MySQL 和 Redis 中的数据产生不一致,最后导致一些私有数据泄漏到错误的用户手中。
* 发生某些故障时(见 [第八章](ch8.md))可能会出现两个节点都以为自己是主库的情况。这种情况称为 **脑裂split brain**,非常危险:如果两个主库都可以接受写操作,却没有冲突解决机制(请参阅 “[多主复制](#多主复制)”),那么数据就可能丢失或损坏。一些系统采取了安全防范措施:当检测到两个主库节点同时存在时会关闭其中一个节点 [^ii]但设计粗糙的机制可能最后会导致两个节点都被关闭【14】。
* 发生某些故障时(见 [第八章](/ch8))可能会出现两个节点都以为自己是主库的情况。这种情况称为 **脑裂split brain**,非常危险:如果两个主库都可以接受写操作,却没有冲突解决机制(请参阅 “[多主复制](#多主复制)”),那么数据就可能丢失或损坏。一些系统采取了安全防范措施:当检测到两个主库节点同时存在时会关闭其中一个节点 [^ii]但设计粗糙的机制可能最后会导致两个节点都被关闭【14】。
[^ii]: 这种机制称为 **屏障fencing**,或者更充满感情的术语是:**爆彼之头Shoot The Other Node In The Head, STONITH**。我们将在 “[领导者和锁](ch8.md#领导者和锁)” 中对屏障进行详细讨论。
[^ii]: 这种机制称为 **屏障fencing**,或者更充满感情的术语是:**爆彼之头Shoot The Other Node In The Head, STONITH**。我们将在 “[领导者和锁](/ch8#领导者和锁)” 中对屏障进行详细讨论。
* 主库被宣告死亡之前的正确超时应该怎么配置?在主库失效的情况下,超时时间越长意味着恢复时间也越长。但是如果超时设置太短,又可能会出现不必要的故障切换。例如,临时的负载峰值可能导致节点的响应时间增加到超出超时时间,或者网络故障也可能导致数据包延迟。如果系统已经处于高负载或网络问题的困扰之中,那么不必要的故障切换可能会让情况变得更糟糕。
这些问题没有简单的解决方案。因此,即使软件支持自动故障切换,不少运维团队还是更愿意手动执行故障切换。
节点故障、不可靠的网络、对副本一致性、持久性、可用性和延迟的权衡,这些问题实际上是分布式系统中的基本问题。[第八章](ch8.md) 和 [第九章](ch9.md) 将更深入地讨论它们。
节点故障、不可靠的网络、对副本一致性、持久性、可用性和延迟的权衡,这些问题实际上是分布式系统中的基本问题。[第八章](/ch8) 和 [第九章](/ch9) 将更深入地讨论它们。
### 复制日志的实现
@@ -144,10 +146,10 @@
#### 传输预写式日志WAL
在 [第三章](ch3.md) 中,我们讨论了存储引擎如何在磁盘上表示数据,我们也发现了通常会将写操作追加到日志中:
在 [第三章](/ch3) 中,我们讨论了存储引擎如何在磁盘上表示数据,我们也发现了通常会将写操作追加到日志中:
* 对于日志结构存储引擎(请参阅 “[SSTables 和 LSM 树](ch3.md#SSTables和LSM树)”),日志是主要的存储位置。日志段在后台压缩,并进行垃圾回收。
* 对于覆写单个磁盘块的 [B 树](ch3.md#B树),每次修改都会先写入 **预写式日志Write Ahead Log, WAL**,以便崩溃后索引可以恢复到一个一致的状态。
* 对于日志结构存储引擎(请参阅 “[SSTables 和 LSM 树](/ch3#SSTables和LSM树)”),日志是主要的存储位置。日志段在后台压缩,并进行垃圾回收。
* 对于覆写单个磁盘块的 [B 树](/ch3#B树),每次修改都会先写入 **预写式日志Write Ahead Log, WAL**,以便崩溃后索引可以恢复到一个一致的状态。
在任何一种情况下,该日志都是包含了所有数据库写入的仅追加字节序列。可以使用完全相同的日志在另一个节点上构建副本:除了将日志写入磁盘之外,主库还可以通过网络将其发送给从库。
@@ -171,7 +173,7 @@
由于逻辑日志与存储引擎的内部实现是解耦的,系统可以更容易地做到向后兼容,从而使主库和从库能够运行不同版本的数据库软件,或者甚至不同的存储引擎。
对于外部应用程序来说逻辑日志格式也更容易解析。如果要将数据库的内容发送到外部系统例如复制到数据仓库进行离线分析或建立自定义索引和缓存【18】这一点会很有用。这种技术被称为 **数据变更捕获change data capture**[第十一章](ch11.md) 将重新讲到它。
对于外部应用程序来说逻辑日志格式也更容易解析。如果要将数据库的内容发送到外部系统例如复制到数据仓库进行离线分析或建立自定义索引和缓存【18】这一点会很有用。这种技术被称为 **数据变更捕获change data capture**[第十一章](/ch11) 将重新讲到它。
#### 基于触发器的复制
@@ -186,7 +188,7 @@
## 复制延迟问题
容忍节点故障只是需要复制的一个原因。正如在 [第二部分](part-ii.md) 的介绍中提到的,其它原因还包括可伸缩性(处理比单个机器更多的请求)和延迟(让副本在地理位置上更接近用户)。
容忍节点故障只是需要复制的一个原因。正如在 [第二部分](/part-ii) 的介绍中提到的,其它原因还包括可伸缩性(处理比单个机器更多的请求)和延迟(让副本在地理位置上更接近用户)。
基于领导者的复制要求所有写入都由单个节点处理但只读查询可以由任何一个副本来处理。所以对于读多写少的场景Web 上的常见模式),一个有吸引力的选择是创建很多从库,并将读请求分散到所有的从库上去。这样能减小主库的负载,并允许由附近的副本来处理读请求。
@@ -206,7 +208,7 @@
但对于异步复制,问题就来了。如 [图 5-3](fig5-3.png) 所示:如果用户在写入后马上就查看数据,则新数据可能尚未到达副本。对用户而言,看起来好像是刚提交的数据丢失了,所以他们不高兴是可以理解的。
![](img/fig5-3.png)
![](/img/fig5-3.png)
**图 5-3 用户写入后从旧副本中读取数据。需要写后读 (read-after-write) 的一致性来防止这种异常**
@@ -218,7 +220,7 @@
* 如果应用中的大部分内容都可能被用户编辑,那这种方法就没用了,因为大部分内容都必须从主库读取(读伸缩就没效果了)。在这种情况下可以使用其他标准来决定是否从主库读取。例如可以跟踪上次更新的时间,在上次更新后的一分钟内,从主库读。还可以监控从库的复制延迟,防止向任何滞后主库超过一分钟的从库发出查询。
* 客户端可以记住最近一次写入的时间戳,系统需要确保从库在处理该用户的读取请求时,该时间戳前的变更都已经传播到了本从库中。如果当前从库不够新,则可以从另一个从库读取,或者等待从库追赶上来。这里的时间戳可以是逻辑时间戳(表示写入顺序的东西,例如日志序列号)或实际的系统时钟(在这种情况下,时钟同步变得至关重要,请参阅 “[不可靠的时钟](ch8.md#不可靠的时钟)”)。
* 客户端可以记住最近一次写入的时间戳,系统需要确保从库在处理该用户的读取请求时,该时间戳前的变更都已经传播到了本从库中。如果当前从库不够新,则可以从另一个从库读取,或者等待从库追赶上来。这里的时间戳可以是逻辑时间戳(表示写入顺序的东西,例如日志序列号)或实际的系统时钟(在这种情况下,时钟同步变得至关重要,请参阅 “[不可靠的时钟](/ch8#不可靠的时钟)”)。
* 如果你的副本分布在多个数据中心(为了在地理上接近用户或者出于可用性目的),还会有额外的复杂性。任何需要由主库提供服务的请求都必须路由到包含该主库的数据中心。
@@ -234,9 +236,9 @@
在从异步从库读取时可能发生的异常的第二个例子是用户可能会遇到 **时光倒流moving backward in time**
如果用户从不同从库进行多次读取,就可能发生这种情况。例如,[图 5-4](img/fig5-4.png) 显示了用户 2345 两次进行相同的查询,首先查询了一个延迟很小的从库,然后是一个延迟较大的从库(如果用户刷新网页时每个请求都被路由到一个随机的服务器,这种情况就很有可能发生)。第一个查询返回了最近由用户 1234 添加的评论,但是第二个查询不返回任何东西,因为滞后的从库还没有拉取到该写入内容。实际上可以认为第二个查询是在比第一个查询更早的时间点上观察系统。如果第一个查询没有返回任何内容,那问题并不大,因为用户 2345 可能不知道用户 1234 最近添加了评论。但如果用户 2345 先看见用户 1234 的评论,然后又看到它消失,这就会让人觉得非常困惑了。
如果用户从不同从库进行多次读取,就可能发生这种情况。例如,[图 5-4](/img/fig5-4.png) 显示了用户 2345 两次进行相同的查询,首先查询了一个延迟很小的从库,然后是一个延迟较大的从库(如果用户刷新网页时每个请求都被路由到一个随机的服务器,这种情况就很有可能发生)。第一个查询返回了最近由用户 1234 添加的评论,但是第二个查询不返回任何东西,因为滞后的从库还没有拉取到该写入内容。实际上可以认为第二个查询是在比第一个查询更早的时间点上观察系统。如果第一个查询没有返回任何内容,那问题并不大,因为用户 2345 可能不知道用户 1234 最近添加了评论。但如果用户 2345 先看见用户 1234 的评论,然后又看到它消失,这就会让人觉得非常困惑了。
![](img/fig5-4.png)
![](/img/fig5-4.png)
**图 5-4 用户首先从新副本读取,然后从旧副本读取。时间看上去回退了。为了防止这种异常,我们需要单调的读取。**
@@ -257,7 +259,7 @@
这两句话之间有因果关系Cake 夫人听到了 Poons 先生的问题并回答了这个问题。
现在想象第三个人正在通过从库来听这个对话。Cake 夫人说的内容是从一个延迟很低的从库读取的,但 Poons 先生所说的内容,从库的延迟要大的多(见 [图 5-5](img/fig5-5.png))。于是,这个观察者会听到以下内容:
现在想象第三个人正在通过从库来听这个对话。Cake 夫人说的内容是从一个延迟很低的从库读取的,但 Poons 先生所说的内容,从库的延迟要大的多(见 [图 5-5](/img/fig5-5.png))。于是,这个观察者会听到以下内容:
*Mrs. Cake*
> 通常约十秒钟Mr. Poons.
@@ -267,13 +269,13 @@
对于观察者来说,看起来好像 Cake 夫人在 Poons 先生提问前就回答了这个问题。这种超能力让人印象深刻但也会把人搞糊涂。【25】。
![](img/fig5-5.png)
![](/img/fig5-5.png)
**图 5-5 如果某些分区的复制速度慢于其他分区,那么观察者可能会在看到问题之前先看到答案。**
要防止这种异常,需要另一种类型的保证:**一致前缀读consistent prefix reads**【23】。这个保证的意思是说如果一系列写入按某个顺序发生那么任何人读取这些写入时也会看见它们以同样的顺序出现。
这是 **分区partitioned****分片sharded** 数据库中的一个特殊问题,我们将在 [第六章](ch6.md) 中讨论分区数据库。如果数据库总是以相同的顺序应用写入,而读取总是看到一致的前缀,那么这种异常不会发生。但是在许多分布式数据库中,不同的分区独立运行,因此不存在 **全局的写入顺序**:当用户从数据库中读取数据时,可能会看到数据库的某些部分处于较旧的状态,而某些则处于较新的状态。
这是 **分区partitioned****分片sharded** 数据库中的一个特殊问题,我们将在 [第六章](/ch6) 中讨论分区数据库。如果数据库总是以相同的顺序应用写入,而读取总是看到一致的前缀,那么这种异常不会发生。但是在许多分布式数据库中,不同的分区独立运行,因此不存在 **全局的写入顺序**:当用户从数据库中读取数据时,可能会看到数据库的某些部分处于较旧的状态,而某些则处于较新的状态。
一种解决方案是,确保任何因果相关的写入都写入相同的分区,但在一些应用中可能无法高效地完成这种操作。还有一些显式跟踪因果依赖关系的算法,我们将在 “[“此前发生” 的关系和并发](#“此前发生”的关系和并发)” 一节中回到这个话题。
@@ -285,7 +287,7 @@
如果应用程序开发人员不必担心微妙的复制问题,并可以信赖他们的数据库 “做了正确的事情”,那该多好呀。这就是 **事务transaction** 存在的原因:**数据库通过事务提供强大的保证**,所以应用程序可以更加简单。
单节点事务已经存在了很长时间。然而在走向分布式(复制和分区)数据库时,许多系统放弃了事务,声称事务在性能和可用性上的代价太高,并断言在可伸缩系统中最终一致性是不可避免的。这个叙述有一些道理,但过于简单了,本书其余部分将提出更为细致的观点。我们将在 [第七章](ch7.md) 和 [第九章](ch9.md) 回到事务的话题,并将在 [第三部分](part-iii.md) 讨论一些替代机制。
单节点事务已经存在了很长时间。然而在走向分布式(复制和分区)数据库时,许多系统放弃了事务,声称事务在性能和可用性上的代价太高,并断言在可伸缩系统中最终一致性是不可避免的。这个叙述有一些道理,但过于简单了,本书其余部分将提出更为细致的观点。我们将在 [第七章](/ch7) 和 [第九章](/ch9) 回到事务的话题,并将在 [第三部分](/part-iii) 讨论一些替代机制。
## 多主复制
@@ -294,7 +296,7 @@
基于领导者的复制有一个主要的缺点:只有一个主库,而且所有的写入都必须通过它 [^iv]。如果出于任何原因(例如和主库之间的网络连接中断)无法连接到主库,就无法向数据库写入。
[^iv]: 如果数据库被分区(见 [第六章](ch6.md)),每个分区都有一个主库。不同的分区的主库可能在不同的节点上,但是每个分区都必须有一个主库。
[^iv]: 如果数据库被分区(见 [第六章](/ch6)),每个分区都有一个主库。不同的分区的主库可能在不同的节点上,但是每个分区都必须有一个主库。
基于领导者的复制模型的自然延伸是允许多个节点接受写入。复制仍然以同样的方式发生:处理写入的每个节点都必须将该数据变更转发给所有其他节点。我们将其称之为 **多领导者配置**multi-leader configuration也称多主、多活复制即 master-master replication 或 active/active replication。在这种情况下每个主库同时是其他主库的从库。
@@ -306,9 +308,9 @@
假如你有一个数据库,副本分散在好几个不同的数据中心(可能会用来容忍单个数据中心的故障,或者为了在地理上更接近用户)。如果使用常规的基于领导者的复制设置,主库必须位于其中一个数据中心,且所有写入都必须经过该数据中心。
多主配置中可以在每个数据中心都有主库。[图 5-6](img/fig5-6.png) 展示了这个架构。在每个数据中心内使用常规的主从复制;在数据中心之间,每个数据中心的主库都会将其更改复制到其他数据中心的主库中。
多主配置中可以在每个数据中心都有主库。[图 5-6](/img/fig5-6.png) 展示了这个架构。在每个数据中心内使用常规的主从复制;在数据中心之间,每个数据中心的主库都会将其更改复制到其他数据中心的主库中。
![](img/fig5-6.png)
![](/img/fig5-6.png)
**图 5-6 跨多个数据中心的多主复制**
@@ -328,7 +330,7 @@
有些数据库默认情况下支持多主配置,但使用外部工具实现也很常见,例如用于 MySQL 的 Tungsten Replicator 【26】用于 PostgreSQL 的 BDR【27】以及用于 Oracle 的 GoldenGate 【19】。
尽管多主复制有这些优势,但也有一个很大的缺点:两个不同的数据中心可能会同时修改相同的数据,写冲突是必须解决的(如 [图 5-6](img/fig5-6.png) 中的 “冲突解决conflict resolution。本书将在 “[处理写入冲突](#处理写入冲突)” 中详细讨论这个问题。
尽管多主复制有这些优势,但也有一个很大的缺点:两个不同的数据中心可能会同时修改相同的数据,写冲突是必须解决的(如 [图 5-6](/img/fig5-6.png) 中的 “冲突解决conflict resolution。本书将在 “[处理写入冲突](#处理写入冲突)” 中详细讨论这个问题。
由于多主复制在许多数据库中都属于改装的功能所以常常存在微妙的配置缺陷且经常与其他数据库功能之间出现意外的反应。比如自增主键、触发器、完整性约束等都可能会有麻烦。因此多主复制往往被认为是危险的领域应尽可能避免【28】。
@@ -356,9 +358,9 @@
多主复制的最大问题是可能发生写冲突,这意味着需要解决冲突。
例如,考虑一个由两个用户同时编辑的维基页面,如 [图 5-7](img/fig5-7.png) 所示。用户 1 将页面的标题从 A 更改为 B并且用户 2 同时将标题从 A 更改为 C。每个用户的更改已成功应用到其本地主库。但当异步复制时会发现冲突【33】。单主数据库中不会出现此问题。
例如,考虑一个由两个用户同时编辑的维基页面,如 [图 5-7](/img/fig5-7.png) 所示。用户 1 将页面的标题从 A 更改为 B并且用户 2 同时将标题从 A 更改为 C。每个用户的更改已成功应用到其本地主库。但当异步复制时会发现冲突【33】。单主数据库中不会出现此问题。
![](img/fig5-7.png)
![](/img/fig5-7.png)
**图 5-7 两个主库同时更新同一记录引起的写入冲突**
@@ -380,7 +382,7 @@
单主数据库按顺序进行写操作:如果同一个字段有多个更新,则最后一个写操作将决定该字段的最终值。
在多主配置中,没有明确的写入顺序,所以最终值应该是什么并不清楚。在 [图 5-7](img/fig5-7.png) 中,在主库 1 中标题首先更新为 B 而后更新为 C在主库 2 中,首先更新为 C然后更新为 B。两种顺序都不比另一种“更正确”。
在多主配置中,没有明确的写入顺序,所以最终值应该是什么并不清楚。在 [图 5-7](/img/fig5-7.png) 中,在主库 1 中标题首先更新为 B 而后更新为 C在主库 2 中,首先更新为 C然后更新为 B。两种顺序都不比另一种“更正确”。
如果每个副本只是按照它看到写入的顺序写入,那么数据库最终将处于不一致的状态:最终值将是在主库 1 的 C 和主库 2 的 B。这是不可接受的每个复制方案都必须确保数据最终在所有副本中都是相同的。因此数据库必须以一种 **收敛convergent** 的方式解决冲突,这意味着所有副本必须在所有变更复制完成时收敛至一个相同的最终值。
@@ -388,7 +390,7 @@
* 给每个写入一个唯一的 ID例如时间戳、长随机数、UUID 或者键和值的哈希),挑选最高 ID 的写入作为胜利者,并丢弃其他写入。如果使用时间戳,这种技术被称为 **最后写入胜利LWW, last write wins**。虽然这种方法很流行但是很容易造成数据丢失【35】。我们将在本章末尾的 [检测并发写入](#检测并发写入) 一节更详细地讨论 LWW。
* 为每个副本分配一个唯一的 IDID 编号更高的写入具有更高的优先级。这种方法也意味着数据丢失。
* 以某种方式将这些值合并在一起 - 例如,按字母顺序排序,然后连接它们(在 [图 5-7](img/fig5-7.png) 中,合并的标题可能类似于 “B/C”
* 以某种方式将这些值合并在一起 - 例如,按字母顺序排序,然后连接它们(在 [图 5-7](/img/fig5-7.png) 中,合并的标题可能类似于 “B/C”
* 用一种可保留所有信息的显式数据结构来记录冲突,并编写解决冲突的应用程序代码(也许通过提示用户的方式)。
@@ -404,7 +406,7 @@
当检测到冲突时,所有冲突写入被存储。下一次读取数据时,会将这些多个版本的数据返回给应用程序。应用程序可以提示用户或自动解决冲突,并将结果写回数据库。例如 CouchDB 就以这种方式工作。
请注意冲突解决通常适用于单行记录或单个文档的层面而不是整个事务【36】。因此如果你有一个事务会原子性地进行几次不同的写入请参阅 [第七章](ch7.md)),对于冲突解决而言,每个写入仍需分开单独考虑。
请注意冲突解决通常适用于单行记录或单个文档的层面而不是整个事务【36】。因此如果你有一个事务会原子性地进行几次不同的写入请参阅 [第七章](/ch7)),对于冲突解决而言,每个写入仍需分开单独考虑。
> #### 自动冲突解决
@@ -422,38 +424,38 @@
#### 什么是冲突?
有些冲突是显而易见的。在 [图 5-7](img/fig5-7.png) 的例子中,两个写操作并发地修改了同一条记录中的同一个字段,并将其设置为两个不同的值。毫无疑问这是一个冲突。
有些冲突是显而易见的。在 [图 5-7](/img/fig5-7.png) 的例子中,两个写操作并发地修改了同一条记录中的同一个字段,并将其设置为两个不同的值。毫无疑问这是一个冲突。
其他类型的冲突可能更为微妙而难以发现。例如,考虑一个会议室预订系统:它记录谁订了哪个时间段的哪个房间。应用程序需要确保每个房间在任意时刻都只能被一组人进行预定(即不得有相同房间的重叠预订)。在这种情况下,如果为同一个房间同时创建两个不同的预订,则可能会发生冲突。即使应用程序在允许用户进行预订之前先检查会议室的可用性,如果两次预订是由两个不同的主库进行的,则仍然可能会有冲突。
虽然现在还没有一个现成的答案,但在接下来的章节中,我们将更好地了解这个问题。我们将在 [第七章](ch7.md) 中看到更多的冲突示例,在 [第十二章](ch12.md) 中我们将讨论用于检测和解决复制系统中冲突的可伸缩方法。
虽然现在还没有一个现成的答案,但在接下来的章节中,我们将更好地了解这个问题。我们将在 [第七章](/ch7) 中看到更多的冲突示例,在 [第十二章](/ch12) 中我们将讨论用于检测和解决复制系统中冲突的可伸缩方法。
### 多主复制拓扑
**复制拓扑**replication topology用来描述写入操作从一个节点传播到另一个节点的通信路径。如果你有两个主库如 [图 5-7](img/fig5-7.png) 所示,只有一个合理的拓扑结构:主库 1 必须把它所有的写入都发送到主库 2反之亦然。当有两个以上的主库多种不同的拓扑都是可能的。[图 5-8](img/fig5-8.png) 举例说明了一些例子。
**复制拓扑**replication topology用来描述写入操作从一个节点传播到另一个节点的通信路径。如果你有两个主库如 [图 5-7](/img/fig5-7.png) 所示,只有一个合理的拓扑结构:主库 1 必须把它所有的写入都发送到主库 2反之亦然。当有两个以上的主库多种不同的拓扑都是可能的。[图 5-8](/img/fig5-8.png) 举例说明了一些例子。
![](img/fig5-8.png)
![](/img/fig5-8.png)
**图 5-8 三种可以在多主复制中使用的拓扑示例。**
最常见的拓扑是全部到全部all-to-all如 [图 5-8 (c)](img/fig5-8.png)),其中每个主库都将其写入发送给其他所有的主库。然而,一些更受限的拓扑也会被使用到:例如,默认情况下 MySQL 仅支持 **环形拓扑circular topology**【34】其中每个节点都从一个节点接收写入并将这些写入加上自己的写入转发给另一个节点。另一种流行的拓扑结构具有星形的形状 [^v]:一个指定的根节点将写入转发给所有其他节点。星形拓扑可以推广到树。
最常见的拓扑是全部到全部all-to-all如 [图 5-8 (c)](/img/fig5-8.png)),其中每个主库都将其写入发送给其他所有的主库。然而,一些更受限的拓扑也会被使用到:例如,默认情况下 MySQL 仅支持 **环形拓扑circular topology**【34】其中每个节点都从一个节点接收写入并将这些写入加上自己的写入转发给另一个节点。另一种流行的拓扑结构具有星形的形状 [^v]:一个指定的根节点将写入转发给所有其他节点。星形拓扑可以推广到树。
[^v]: 不要与星型模式混淆(请参阅 “[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”),其中描述了数据模型的结构,而不是节点之间的通信拓扑。
[^v]: 不要与星型模式混淆(请参阅 “[星型和雪花型:分析的模式](/ch3#星型和雪花型:分析的模式)”),其中描述了数据模型的结构,而不是节点之间的通信拓扑。
在环形和星形拓扑中写入可能需要在到达所有副本之前通过多个节点。因此节点需要转发从其他节点收到的数据更改。为了防止无限复制循环每个节点被赋予一个唯一的标识符并且在复制日志中每次写入都会使用其经过的所有节点的标识符进行标记【43】。当一个节点收到用自己的标识符标记的数据更改时该数据更改将被忽略因为节点知道它已经被处理过。
环形和星形拓扑的问题是,如果只有一个节点发生故障,则可能会中断其他节点之间的复制消息流,导致它们无法通信,除非节点被修复。拓扑结构可以重新配置为跳过发生故障的节点,但在大多数部署中,这种重新配置必须手动完成。更密集连接的拓扑结构(例如全部到全部)的容错性更好,因为它允许消息沿着不同的路径传播,可以避免单点故障。
另一方面,全部到全部的拓扑也可能有问题。特别是,一些网络链接可能比其他网络链接更快(例如由于网络拥塞),结果是一些复制消息可能 “超越” 其他复制消息,如 [图 5-9](img/fig5-9.png) 所示。
另一方面,全部到全部的拓扑也可能有问题。特别是,一些网络链接可能比其他网络链接更快(例如由于网络拥塞),结果是一些复制消息可能 “超越” 其他复制消息,如 [图 5-9](/img/fig5-9.png) 所示。
![](img/fig5-9.png)
![](/img/fig5-9.png)
**图 5-9 使用多主复制时,写入可能会以错误的顺序到达某些副本。**
在 [图 5-9](img/fig5-9.png) 中,客户端 A 向主库 1 的表中插入一行,客户端 B 在主库 3 上更新该行。然而,主库 2 可以以不同的顺序接收写入:它可能先接收到更新(从它的角度来看,是对数据库中不存在的行的更新),稍后才接收到相应的插入(其应该在更新之前)。
在 [图 5-9](/img/fig5-9.png) 中,客户端 A 向主库 1 的表中插入一行,客户端 B 在主库 3 上更新该行。然而,主库 2 可以以不同的顺序接收写入:它可能先接收到更新(从它的角度来看,是对数据库中不存在的行的更新),稍后才接收到相应的插入(其应该在更新之前)。
这是一个因果关系的问题,类似于我们在 “[一致前缀读](#一致前缀读)” 中看到的:更新取决于先前的插入,所以我们需要确保所有节点先处理插入,然后再处理更新。仅仅在每一次写入时添加一个时间戳是不够的,因为时钟不可能被充分地同步,所以主库 2 就无法正确地对这些事件进行排序(见 [第八章](ch8.md))。
这是一个因果关系的问题,类似于我们在 “[一致前缀读](#一致前缀读)” 中看到的:更新取决于先前的插入,所以我们需要确保所有节点先处理插入,然后再处理更新。仅仅在每一次写入时添加一个时间戳是不够的,因为时钟不可能被充分地同步,所以主库 2 就无法正确地对这些事件进行排序(见 [第八章](/ch8))。
要正确排序这些事件,可以使用一种称为 **版本向量version vectors** 的技术,本章稍后将讨论这种技术(请参阅 “[检测并发写入](#检测并发写入)”。然而许多多主复制系统中的冲突检测技术实现得并不好。例如在撰写本文时PostgreSQL BDR 不提供写入的因果排序【27】而 Tungsten Replicator for MySQL 甚至都不做检测冲突【34】。
@@ -474,9 +476,9 @@
假设你有一个带有三个副本的数据库,而其中一个副本目前不可用,或许正在重新启动以安装系统更新。在基于领导者的配置中,如果要继续处理写入,则可能需要执行故障切换(请参阅「[处理节点宕机](#处理节点宕机)」)。
另一方面,在无主配置中,不存在故障转移。[图 5-10](img/fig5-10.png) 演示了会发生了什么事情:客户端(用户 1234并行发送写入到所有三个副本并且两个可用副本接受写入但是不可用副本错过了它。假设三个副本中的两个承认写入是足够的在用户 1234 已经收到两个确定的响应之后,我们认为写入成功。客户简单地忽略了其中一个副本错过了写入的事实。
另一方面,在无主配置中,不存在故障转移。[图 5-10](/img/fig5-10.png) 演示了会发生了什么事情:客户端(用户 1234并行发送写入到所有三个副本并且两个可用副本接受写入但是不可用副本错过了它。假设三个副本中的两个承认写入是足够的在用户 1234 已经收到两个确定的响应之后,我们认为写入成功。客户简单地忽略了其中一个副本错过了写入的事实。
![](img/fig5-10.png)
![](/img/fig5-10.png)
**图 5-10 法定写入,法定读取,并在节点中断后读修复。**
@@ -492,7 +494,7 @@
* 读修复Read repair
当客户端并行读取多个节点时,它可以检测到任何陈旧的响应。例如,在 [图 5-10](img/fig5-10.png) 中,用户 2345 获得了来自副本 3 的版本 6 值和来自副本 1 和 2 的版本 7 值。客户端发现副本 3 具有陈旧值,并将新值写回到该副本。这种方法适用于读频繁的值。
当客户端并行读取多个节点时,它可以检测到任何陈旧的响应。例如,在 [图 5-10](/img/fig5-10.png) 中,用户 2345 获得了来自副本 3 的版本 6 值和来自副本 1 和 2 的版本 7 值。客户端发现副本 3 具有陈旧值,并将新值写回到该副本。这种方法适用于读频繁的值。
* 反熵过程Anti-entropy process
@@ -502,7 +504,7 @@
#### 读写的法定人数
在 [图 5-10](img/fig5-10.png) 的示例中,我们认为即使仅在三个副本中的两个上进行处理,写入仍然是成功的。如果三个副本中只有一个接受了写入,会怎样?以此类推,究竟多少个副本完成才可以认为写入成功?
在 [图 5-10](/img/fig5-10.png) 的示例中,我们认为即使仅在三个副本中的两个上进行处理,写入仍然是成功的。如果三个副本中只有一个接受了写入,会怎样?以此类推,究竟多少个副本完成才可以认为写入成功?
如果我们知道,每个成功的写操作意味着在三个副本中至少有两个出现,这意味着至多有一个副本可能是陈旧的。因此,如果我们从至少两个副本读取,我们可以确定至少有一个是最新的。如果第三个副本停机或响应速度缓慢,则读取仍可以继续返回最新值。
@@ -512,17 +514,17 @@
在 Dynamo 风格的数据库中,参数 n、w 和 r 通常是可配置的。一个常见的选择是使 n 为奇数(通常为 3 或 5并设置 $w = r = (n + 1) / 2$(向上取整)。但是你可以根据需要更改数字。例如,写入次数较少且读取次数较多的工作负载可以从设置 $w = n$ 和 $r = 1$中受益。这会使得读取速度更快,但缺点是只要有一个不可用的节点就会导致所有的数据库写入都失败。
> 集群中可能有多于 n 个的节点(集群的机器数可能多于副本数目)。但是任何给定的值只能存储在 n 个节点上。这允许对数据集进行分区,从而可以支持比单个节点的存储能力更大的数据集。我们将在 [第六章](ch6.md) 继续讨论分区。
> 集群中可能有多于 n 个的节点(集群的机器数可能多于副本数目)。但是任何给定的值只能存储在 n 个节点上。这允许对数据集进行分区,从而可以支持比单个节点的存储能力更大的数据集。我们将在 [第六章](/ch6) 继续讨论分区。
法定人数条件 $w + r > n$ 允许系统容忍不可用的节点,如下所示:
* 如果 $w < n$,当节点不可用时,我们仍然可以处理写入。
* 如果 $r < n$,当节点不可用时,我们仍然可以处理读取。
* 对于 $n = 3w = 2r = 2$,我们可以容忍一个不可用的节点。
* 对于 $n = 5w = 3r = 3$,我们可以容忍两个不可用的节点。这个案例如 [图 5-11](img/fig5-11.png) 所示。
* 对于 $n = 5w = 3r = 3$,我们可以容忍两个不可用的节点。这个案例如 [图 5-11](/img/fig5-11.png) 所示。
* 通常,读取和写入操作始终并行发送到所有 n 个副本。参数 w 和 r 决定我们等待多少个节点,即在我们认为读或写成功之前,有多少个节点需要报告成功。
![](img/fig5-11.png)
![](/img/fig5-11.png)
**图 5-11 如果 $w + r > n$,读取 r 个副本,至少有一个副本必然包含了最近的成功写入。**
@@ -531,7 +533,7 @@
### 法定人数一致性的局限性
如果你有 n 个副本,并且你选择了满足 $w + r > n$ 的 w 和 r你通常可以期望每次读取都能返回最近写入的值。情况就是这样因为你写入的节点集合和你读取的节点集合必然有重叠。也就是说你读取的节点中必然至少有一个节点具有最新值如 [图 5-11](img/fig5-11.png) 所示)。
如果你有 n 个副本,并且你选择了满足 $w + r > n$ 的 w 和 r你通常可以期望每次读取都能返回最近写入的值。情况就是这样因为你写入的节点集合和你读取的节点集合必然有重叠。也就是说你读取的节点中必然至少有一个节点具有最新值如 [图 5-11](/img/fig5-11.png) 所示)。
通常r 和 w 被选为多数(超过 $n/2$ )节点,因为这确保了 $w + r > n$,同时仍然容忍多达 $n/2$ 个节点故障。但是法定人数不一定必须是大多数重要的是读写使用的节点至少有一个节点的交集。其他法定人数的配置是可能的这使得分布式算法的设计有一定的灵活性【45】。
@@ -546,11 +548,11 @@
* 如果写操作与读操作同时发生,写操作可能仅反映在某些副本上。在这种情况下,不确定读取返回的是旧值还是新值。
* 如果写操作在某些副本上成功,而在其他节点上失败(例如,因为某些节点上的磁盘已满),在小于 w 个副本上写入成功。所以整体判定写入失败但整体写入失败并没有在写入成功的副本上回滚。这意味着一个写入虽然报告失败后续的读取仍然可能会读取这次失败写入的值【47】。
* 如果携带新值的节点发生故障,需要从其他带有旧值的副本进行恢复,则存储新值的副本数可能会低于 w从而打破法定人数条件。
* 即使一切工作正常,有时也会不幸地出现关于 **时序timing** 的边缘情况,我们将在 “[线性一致性和法定人数](ch9.md#线性一致性和法定人数)” 中看到这点。
* 即使一切工作正常,有时也会不幸地出现关于 **时序timing** 的边缘情况,我们将在 “[线性一致性和法定人数](/ch9#线性一致性和法定人数)” 中看到这点。
因此尽管法定人数似乎保证读取返回最新的写入值但在实践中并不那么简单。Dynamo 风格的数据库通常针对可以忍受最终一致性的用例进行优化。你可以通过参数 w 和 r 来调整读取到陈旧值的概率,但把它们当成绝对的保证是不明智的。
尤其是,因为通常得不到 “[复制延迟问题](#复制延迟问题)” 中讨论的那些保证(读己之写,单调读,一致前缀读),前面提到的异常可能会发生在应用程序中。更强有力的保证通常需要 **事务****共识**。我们将在 [第七章](ch7.md) 和 [第九章](ch9.md) 回到这些话题。
尤其是,因为通常得不到 “[复制延迟问题](#复制延迟问题)” 中讨论的那些保证(读己之写,单调读,一致前缀读),前面提到的异常可能会发生在应用程序中。更强有力的保证通常需要 **事务****共识**。我们将在 [第七章](/ch7) 和 [第九章](/ch9) 回到这些话题。
#### 监控陈旧度
@@ -595,17 +597,17 @@ Riak 将客户端和数据库节点之间的所有通信保持在一个本地的
Dynamo 风格的数据库允许多个客户端同时写入相同的键Key这意味着即使使用严格的法定人数也会发生冲突。这种情况与多主复制相似请参阅 “[处理写入冲突](#处理写入冲突)”),但在 Dynamo 风格的数据库中,在 **读修复****提示移交** 期间也可能会产生冲突。
其问题在于,由于可变的网络延迟和部分节点的故障,事件可能以不同的顺序到达不同的节点。例如,[图 5-12](img/fig5-12.png) 显示了两个客户机 A 和 B 同时写入三节点数据存储中的键 X
其问题在于,由于可变的网络延迟和部分节点的故障,事件可能以不同的顺序到达不同的节点。例如,[图 5-12](/img/fig5-12.png) 显示了两个客户机 A 和 B 同时写入三节点数据存储中的键 X
* 节点 1 接收来自 A 的写入,但由于暂时中断,未接收到来自 B 的写入。
* 节点 2 首先接收来自 A 的写入,然后接收来自 B 的写入。
* 节点 3 首先接收来自 B 的写入,然后从 A 写入。
![](img/fig5-12.png)
![](/img/fig5-12.png)
**图 5-12 并发写入 Dynamo 风格的数据存储:没有明确定义的顺序。**
如果每个节点只要接收到来自客户端的写入请求就简单地覆写某个键值,那么节点就会永久地不一致,如 [图 5-12](img/fig5-12.png) 中的最终获取请求所示:节点 2 认为 X 的最终值是 B而其他节点认为值是 A 。
如果每个节点只要接收到来自客户端的写入请求就简单地覆写某个键值,那么节点就会永久地不一致,如 [图 5-12](/img/fig5-12.png) 中的最终获取请求所示:节点 2 认为 X 的最终值是 B而其他节点认为值是 A 。
为了最终达成一致,副本应该趋于相同的值。如何做到这一点?有人可能希望复制的数据库能够自动处理,但不幸的是,大多数的实现都很糟糕:如果你想避免丢失数据,你(应用程序开发人员)需要知道很多有关数据库冲突处理的内部信息。
@@ -615,11 +617,11 @@ Dynamo 风格的数据库允许多个客户端同时写入相同的键Key
实现最终收敛的一种方法是声明每个副本只需要存储 **“最近”** 的值,并允许 **“更旧”** 的值被覆盖和抛弃。然后,只要我们有一种明确的方式来确定哪个写是 “最近的”,并且每个写入最终都被复制到每个副本,那么复制最终会收敛到相同的值。
正如 **“最近”** 的引号所表明的,这个想法其实颇具误导性。在 [图 5-12](img/fig5-12.png) 的例子中,当客户端向数据库节点发送写入请求时,两个客户端都不知道另一个客户端,因此不清楚哪一个先发送请求。事实上,说这两种情况谁先发送请求是没有意义的:既然我们说写入是 **并发concurrent** 的,那么它们的顺序就是不确定的。
正如 **“最近”** 的引号所表明的,这个想法其实颇具误导性。在 [图 5-12](/img/fig5-12.png) 的例子中,当客户端向数据库节点发送写入请求时,两个客户端都不知道另一个客户端,因此不清楚哪一个先发送请求。事实上,说这两种情况谁先发送请求是没有意义的:既然我们说写入是 **并发concurrent** 的,那么它们的顺序就是不确定的。
即使写入没有自然的排序,我们也可以强制进行排序。例如,可以为每个写入附加一个时间戳,然后挑选最大的时间戳作为 **“最近的”**,并丢弃具有较早时间戳的任何写入。这种冲突解决算法被称为 **最后写入胜利LWW, last write wins**,是 Cassandra 唯一支持的冲突解决方法【53】也是 Riak 中的一个可选特征【35】。
LWW 实现了最终收敛的目标,但以 **持久性** 为代价:如果同一个键有多个并发写入,即使它们反馈给客户端的结果都是成功的(因为它们被写入 w 个副本也只有一个写入将被保留而其他写入将被默默地丢弃。此外LWW 甚至可能会丢弃不是并发的写入,我们将在 “[有序事件的时间戳](ch8.md#有序事件的时间戳)” 中进行讨论。
LWW 实现了最终收敛的目标,但以 **持久性** 为代价:如果同一个键有多个并发写入,即使它们反馈给客户端的结果都是成功的(因为它们被写入 w 个副本也只有一个写入将被保留而其他写入将被默默地丢弃。此外LWW 甚至可能会丢弃不是并发的写入,我们将在 “[有序事件的时间戳](/ch8#有序事件的时间戳)” 中进行讨论。
在类似缓存的一些情况下写入丢失可能是可以接受的。但如果数据丢失不可接受LWW 是解决冲突的一个很烂的选择。
@@ -639,7 +641,7 @@ LWW 实现了最终收敛的目标,但以 **持久性** 为代价:如果同
> #### 并发性、时间和相对性
>
> 如果两个操作 **“同时”** 发生,似乎应该称为并发 —— 但事实上,它们在字面时间上重叠与否并不重要。由于分布式系统中的时钟问题,现实中是很难判断两个事件是否是 **同时** 发生的,这个问题我们将在 [第八章](ch8.md) 中详细讨论。
> 如果两个操作 **“同时”** 发生,似乎应该称为并发 —— 但事实上,它们在字面时间上重叠与否并不重要。由于分布式系统中的时钟问题,现实中是很难判断两个事件是否是 **同时** 发生的,这个问题我们将在 [第八章](/ch8) 中详细讨论。
>
> 为了定义并发性,确切的时间并不重要:如果两个操作都意识不到对方的存在,就称这两个操作 **并发**而不管它们实际发生的物理时间。人们有时把这个原理和物理学中的狭义相对论联系起来【54】该理论引入了信息不能比光速更快的思想。因此如果两个事件发生的时间差小于光通过它们之间的距离所需要的时间那么这两个事件不可能相互影响。
>
@@ -650,7 +652,7 @@ LWW 实现了最终收敛的目标,但以 **持久性** 为代价:如果同
我们来看一个算法,它可以确定两个操作是否为并发的,还是一个在另一个之前。简单起见,我们从一个只有一个副本的数据库开始。一旦我们知道了如何在单个副本上完成这项工作,我们可以将该方法推广到具有多个副本的无主数据库。
[图 5-13](img/fig5-13.png) 显示了两个客户端同时向同一购物车添加项目。(如果这样的例子让你觉得无趣,那么可以想象一下两个空中交通管制员同时把飞机添加到他们正在跟踪的区域。)最初,购物车是空的。然后客户端向数据库发出五次写入:
[图 5-13](/img/fig5-13.png) 显示了两个客户端同时向同一购物车添加项目。(如果这样的例子让你觉得无趣,那么可以想象一下两个空中交通管制员同时把飞机添加到他们正在跟踪的区域。)最初,购物车是空的。然后客户端向数据库发出五次写入:
1. 客户端 1 将牛奶加入购物车。这是该键的第一次写入,服务器成功存储了它并为其分配版本号 1最后将值与版本号一起回送给客户端。
2. 客户端 2 将鸡蛋加入购物车,不知道客户端 1 同时添加了牛奶(客户端 2 认为它的鸡蛋是购物车中的唯一物品)。服务器为此写入分配版本号 2并将鸡蛋和牛奶存储为两个单独的值。然后它将这两个值 **都** 返回给客户端 2 ,并附上版本号 2。
@@ -658,13 +660,13 @@ LWW 实现了最终收敛的目标,但以 **持久性** 为代价:如果同
4. 同时,客户端 2 想要加入火腿,不知道客户端 1 刚刚加了面粉。客户端 2 在最近一次响应中从服务器收到了两个值 [牛奶] 和 [鸡蛋],所以客户端 2 现在合并这些值,并添加火腿形成一个新的值 [鸡蛋,牛奶,火腿]。它将这个值发送到服务器,带着之前的版本号 2 。服务器检测到新值会覆盖版本 2 的值 [鸡蛋],但新值也会与版本 3 的值 [牛奶,面粉] **并发**,所以剩下的两个值是版本 3 的 [牛奶,面粉],和版本 4 的 [鸡蛋,牛奶,火腿]。
5. 最后,客户端 1 想要加培根。它之前从服务器接收到了版本 3 的 [牛奶,面粉] 和 [鸡蛋],所以它合并这些,添加培根,并将最终值 [牛奶,面粉,鸡蛋,培根] 连同版本号 3 发往服务器。这会覆盖版本 3 的值 [牛奶,面粉](请注意 [鸡蛋] 已经在上一步被覆盖),但与版本 4 的值 [鸡蛋,牛奶,火腿] 并发,所以服务器将保留这两个并发值。
![](img/fig5-13.png)
![](/img/fig5-13.png)
**图 5-13 在同时编辑购物车时捕获两个客户端之间的因果关系。**
[图 5-13](img/fig5-13.png) 中的操作之间的数据流如 [图 5-14](img/fig5-14.png) 所示。箭头表示哪个操作发生在其他操作之前,意味着后面的操作知道或依赖于较早的操作。在这个例子中,客户端永远不会完全拿到服务器上的最新数据,因为总是有另一个操作同时进行。但是旧版本的值最终会被覆盖,并且不会丢失任何写入。
[图 5-13](/img/fig5-13.png) 中的操作之间的数据流如 [图 5-14](/img/fig5-14.png) 所示。箭头表示哪个操作发生在其他操作之前,意味着后面的操作知道或依赖于较早的操作。在这个例子中,客户端永远不会完全拿到服务器上的最新数据,因为总是有另一个操作同时进行。但是旧版本的值最终会被覆盖,并且不会丢失任何写入。
![](img/fig5-14.png)
![](/img/fig5-14.png)
**图 5-14 图 5-13 中的因果依赖关系图。**
@@ -683,21 +685,21 @@ LWW 实现了最终收敛的目标,但以 **持久性** 为代价:如果同
合并并发值,本质上是与多主复制中的冲突解决问题相同,我们先前讨论过(请参阅 “[处理写入冲突](#处理写入冲突)”)。一个简单的方法是根据版本号或时间戳(最后写入胜利)来选择一个值,但这意味着丢失数据。所以,你可能需要在应用程序代码中额外做些更聪明的事情。
以购物车为例,一种合理的合并值的方法就是做并集。在 [图 5-14](img/fig5-14.png) 中,最后的两个兄弟是 [牛奶,面粉,鸡蛋,培根] 和 [鸡蛋,牛奶,火腿]。注意牛奶和鸡蛋虽然同时出现在两个并发值里,但他们每个只被写过一次。合并的值可以是 [牛奶,面粉,鸡蛋,培根,火腿],不再有重复了。
以购物车为例,一种合理的合并值的方法就是做并集。在 [图 5-14](/img/fig5-14.png) 中,最后的两个兄弟是 [牛奶,面粉,鸡蛋,培根] 和 [鸡蛋,牛奶,火腿]。注意牛奶和鸡蛋虽然同时出现在两个并发值里,但他们每个只被写过一次。合并的值可以是 [牛奶,面粉,鸡蛋,培根,火腿],不再有重复了。
然而,如果你想让人们也可以从他们的购物车中 **移除** 东西而不是仅仅添加东西那么把并发值做并集可能不会产生正确的结果如果你合并了两个客户端的购物车并且只在其中一个客户端里面移除了一个项目那么被移除的项目将会重新出现在这两个客户端的交集结果中【37】。为了防止这个问题要移除一个项目时不能简单地直接从数据库中删除相反系统必须留下一个具有适当版本号的标记以在兄弟合并时表明该项目已被移除。这种删除标记被称为 **墓碑tombstone**(我们上一次看到墓碑是在 “[散列索引”](ch3.md#散列索引) 章节的日志压缩部分)。
然而,如果你想让人们也可以从他们的购物车中 **移除** 东西而不是仅仅添加东西那么把并发值做并集可能不会产生正确的结果如果你合并了两个客户端的购物车并且只在其中一个客户端里面移除了一个项目那么被移除的项目将会重新出现在这两个客户端的交集结果中【37】。为了防止这个问题要移除一个项目时不能简单地直接从数据库中删除相反系统必须留下一个具有适当版本号的标记以在兄弟合并时表明该项目已被移除。这种删除标记被称为 **墓碑tombstone**(我们上一次看到墓碑是在 “[散列索引”](/ch3#散列索引) 章节的日志压缩部分)。
因为在应用程序代码中做兄弟合并是复杂且容易出错的,所以有一些数据结构被设计出来用于自动执行这种合并,比如在 “[自动冲突解决](#自动冲突解决)” 中讨论过的那些。举例来说Riak 的数据类型就支持使用称为 CRDT 【38,39,55】的能以合理方式自动进行兄弟合并的数据结构家族包括对保留删除的支持。
#### 版本向量
[图 5-13](img/fig5-13.png) 中的示例只使用了一个副本。当有多个副本但又没有主库时,算法该如何修改?
[图 5-13](/img/fig5-13.png) 中的示例只使用了一个副本。当有多个副本但又没有主库时,算法该如何修改?
[图 5-13](img/fig5-13.png) 使用单个版本号来捕获操作之间的依赖关系,但是当多个副本并发接受写入时,这是不够的。相反,除了对每个键,我们还需要对 **每个副本** 使用版本号。每个副本在处理写入时增加自己的版本号,并且跟踪从其他副本中看到的版本号。这个信息指出了要覆盖哪些并发值,以及要保留哪些并发值或兄弟值。
[图 5-13](/img/fig5-13.png) 使用单个版本号来捕获操作之间的依赖关系,但是当多个副本并发接受写入时,这是不够的。相反,除了对每个键,我们还需要对 **每个副本** 使用版本号。每个副本在处理写入时增加自己的版本号,并且跟踪从其他副本中看到的版本号。这个信息指出了要覆盖哪些并发值,以及要保留哪些并发值或兄弟值。
所有副本的版本号集合称为 **版本向量version vector**【56】。这个想法的一些变体正在被使用但最有趣的可能是在 Riak 2.0 【58,59】中使用的 **虚线版本向量dotted version vector**【57】。我们不会深入细节但是它的工作方式与我们在购物车示例中看到的非常相似。
与 [图 5-13](img/fig5-13.png) 中的版本号一样当读取值时版本向量会从数据库副本发送到客户端并且随后写入值时需要将其发送回数据库。Riak 将版本向量编码为一个字符串,并称其为 **因果上下文**,即 causal context。版本向量允许数据库区分覆盖写入和并发写入。
与 [图 5-13](/img/fig5-13.png) 中的版本号一样当读取值时版本向量会从数据库副本发送到客户端并且随后写入值时需要将其发送回数据库。Riak 将版本向量编码为一个字符串,并称其为 **因果上下文**,即 causal context。版本向量允许数据库区分覆盖写入和并发写入。
另外,就像在单个副本中的情况一样,应用程序可能需要合并并发值。版本向量结构能够确保从一个副本读取并随后写回到另一个副本是安全的。这样做虽然可能会在其他副本上面创建数据,但只要能正确合并就不会丢失数据。
@@ -830,8 +832,3 @@ LWW 实现了最终收敛的目标,但以 **持久性** 为代价:如果同
1. Carlos Baquero: “[Version Vectors Are Not Vector Clocks](https://haslab.wordpress.com/2011/07/08/version-vectors-are-not-vector-clocks/),” *haslab.wordpress.com*, July 8, 2011.
1. Reinhard Schwarz and Friedemann Mattern: “[Detecting Causal Relationships in Distributed Computations: In Search of the Holy Grail](http://dcg.ethz.ch/lectures/hs08/seminar/papers/mattern4.pdf),” *Distributed Computing*, volume 7, number 3, pages 149174, March 1994. [doi:10.1007/BF02277859](http://dx.doi.org/10.1007/BF02277859)
--------
| 上一章 | 目录 | 下一章 |
| :--------------------------------: | :-----------------------------: | :--------------------: |
| [第二部分:分布式数据](part-ii.md) | [设计数据密集型应用](README.md) | [第六章:分区](ch6.md) |

View File

@@ -1,19 +1,21 @@
# 第六章:分区
---
linktitle: "第六章:分区"
linkTitle: "6. 分区"
weight: 206
breadcrumbs: false
---
![](img/ch6.png)
![](/img/ch6.png)
> 我们必须跳出电脑指令序列的窠臼。叙述定义、描述元数据、梳理关系,而不是编写过程。
>
> —— Grace Murray Hopper未来的计算机及其管理1962
>
-------------
[TOC]
在 [第五章](/ch5) 中,我们讨论了复制 —— 即数据在不同节点上的副本,对于非常大的数据集,或非常高的吞吐量,仅仅进行复制是不够的:我们需要将数据进行 **分区partitions**,也称为 **分片sharding**[^i]。
在 [第五章](ch5.md) 中,我们讨论了复制 —— 即数据在不同节点上的副本,对于非常大的数据集,或非常高的吞吐量,仅仅进行复制是不够的:我们需要将数据进行 **分区partitions**,也称为 **分片sharding**[^i]
[^i]: 正如本章所讨论的,分区是一种有意将大型数据库分解成小型数据库的方式。它与 **网络分区network partitions, netsplits** 无关,这是节点之间网络故障的一种。我们将在 [第八章](ch8.md) 讨论这些错误。
[^i]: 正如本章所讨论的,分区是一种有意将大型数据库分解成小型数据库的方式。它与 **网络分区(network partitions, netsplits** 无关,这是节点之间网络故障的一种。我们将在 [第八章](/ch8) 讨论这些错误
> #### 术语澄清
>
@@ -22,11 +24,11 @@
通常情况下,每条数据(每条记录,每行或每个文档)属于且仅属于一个分区。有很多方法可以实现这一点,本章将进行深入讨论。实际上,每个分区都是自己的小型数据库,尽管数据库可能支持同时进行多个分区的操作。
分区主要是为了 **可伸缩性**。不同的分区可以放在不共享集群中的不同节点上(请参阅 [第二部分](part-ii.md) 关于 [无共享架构](part-ii.md#无共享架构) 的定义)。因此,大数据集可以分布在多个磁盘上,并且查询负载可以分布在多个处理器上。
分区主要是为了 **可伸缩性**。不同的分区可以放在不共享集群中的不同节点上(请参阅 [第二部分](/part-ii) 关于 [无共享架构](/part-ii#无共享架构) 的定义)。因此,大数据集可以分布在多个磁盘上,并且查询负载可以分布在多个处理器上。
对于在单个分区上运行的查询,每个节点可以独立执行对自己的查询,因此可以通过添加更多的节点来扩大查询吞吐量。大型,复杂的查询可能会跨越多个节点并行处理,尽管这也带来了新的困难。
分区数据库在 20 世纪 80 年代由 Teradata 和 NonStop SQL【1】等产品率先推出最近因为 NoSQL 数据库和基于 Hadoop 的数据仓库重新被关注。有些系统是为事务性工作设计的,有些系统则用于分析(请参阅 “[事务处理还是分析](ch3.md#事务处理还是分析?)”):这种差异会影响系统的运作方式,但是分区的基本原理均适用于这两种工作方式。
分区数据库在 20 世纪 80 年代由 Teradata 和 NonStop SQL【1】等产品率先推出最近因为 NoSQL 数据库和基于 Hadoop 的数据仓库重新被关注。有些系统是为事务性工作设计的,有些系统则用于分析(请参阅 “[事务处理还是分析](/ch3#事务处理还是分析?)”):这种差异会影响系统的运作方式,但是分区的基本原理均适用于这两种工作方式。
在本章中,我们将首先介绍分割大型数据集的不同方法,并观察索引如何与分区配合。然后我们将讨论 [分区再平衡rebalancing](#分区再平衡),如果想要添加或删除集群中的节点,则必须进行再平衡。最后,我们将概述数据库如何将请求路由到正确的分区并执行查询。
@@ -34,11 +36,11 @@
分区通常与复制结合使用,使得每个分区的副本存储在多个节点上。这意味着,即使每条记录属于一个分区,它仍然可以存储在多个不同的节点上以获得容错能力。
一个节点可能存储多个分区。如果使用主从复制模型,则分区和复制的组合如 [图 6-1](img/fig6-1.png) 所示。每个分区领导者(主库)被分配给一个节点,追随者(从库)被分配给其他节点。每个节点可能是某些分区的主库,同时是其他分区的从库。
一个节点可能存储多个分区。如果使用主从复制模型,则分区和复制的组合如 [图 6-1](/img/fig6-1.png) 所示。每个分区领导者(主库)被分配给一个节点,追随者(从库)被分配给其他节点。每个节点可能是某些分区的主库,同时是其他分区的从库。
我们在 [第五章](ch5.md) 讨论的关于数据库复制的所有内容同样适用于分区的复制。大多数情况下,分区方案的选择与复制方案的选择是独立的,为简单起见,本章中将忽略复制。
我们在 [第五章](/ch5) 讨论的关于数据库复制的所有内容同样适用于分区的复制。大多数情况下,分区方案的选择与复制方案的选择是独立的,为简单起见,本章中将忽略复制。
![](img/fig6-1.png)
![](/img/fig6-1.png)
**图 6-1 组合使用复制和分区:每个节点充当某些分区的主库,其他分区充当从库。**
@@ -56,17 +58,17 @@
### 根据键的范围分区
一种分区的方法是为每个分区指定一块连续的键范围(从最小值到最大值),如纸质百科全书的卷([图 6-2](img/fig6-2.png))。如果知道范围之间的边界,则可以轻松确定哪个分区包含某个值。如果你还知道分区所在的节点,那么可以直接向相应的节点发出请求(对于百科全书而言,就像从书架上选取正确的书籍)。
一种分区的方法是为每个分区指定一块连续的键范围(从最小值到最大值),如纸质百科全书的卷([图 6-2](/img/fig6-2.png))。如果知道范围之间的边界,则可以轻松确定哪个分区包含某个值。如果你还知道分区所在的节点,那么可以直接向相应的节点发出请求(对于百科全书而言,就像从书架上选取正确的书籍)。
![](img/fig6-2.png)
![](/img/fig6-2.png)
**图 6-2 印刷版百科全书按照关键字范围进行分区**
键的范围不一定均匀分布,因为数据也很可能不均匀分布。例如在 [图 6-2](img/fig6-2.png) 中,第 1 卷包含以 A 和 B 开头的单词,但第 12 卷则包含以 T、U、V、X、Y 和 Z 开头的单词。只是简单的规定每个卷包含两个字母会导致一些卷比其他卷大。为了均匀分配数据,分区边界需要依据数据调整。
键的范围不一定均匀分布,因为数据也很可能不均匀分布。例如在 [图 6-2](/img/fig6-2.png) 中,第 1 卷包含以 A 和 B 开头的单词,但第 12 卷则包含以 T、U、V、X、Y 和 Z 开头的单词。只是简单的规定每个卷包含两个字母会导致一些卷比其他卷大。为了均匀分配数据,分区边界需要依据数据调整。
分区边界可以由管理员手动选择,也可以由数据库自动选择(我们会在 “[分区再平衡](#分区再平衡)” 中更详细地讨论分区边界的选择。Bigtable 使用了这种分区策略,以及其开源等价物 HBase 【2, 3】、RethinkDB 和 2.4 版本之前的 MongoDB 【4】。
在每个分区中,我们可以按照一定的顺序保存键(请参阅 “[SSTables 和 LSM 树](ch3.md#SSTables和LSM树)”)。好处是进行范围扫描非常简单,你可以将键作为联合索引来处理,以便在一次查询中获取多个相关记录(请参阅 “[多列索引](ch3.md#多列索引)”)。例如,假设我们有一个程序来存储传感器网络的数据,其中主键是测量的时间戳(年月日时分秒)。范围扫描在这种情况下非常有用,因为我们可以轻松获取某个月份的所有数据。
在每个分区中,我们可以按照一定的顺序保存键(请参阅 “[SSTables 和 LSM 树](/ch3#SSTables和LSM树)”)。好处是进行范围扫描非常简单,你可以将键作为联合索引来处理,以便在一次查询中获取多个相关记录(请参阅 “[多列索引](/ch3#多列索引)”)。例如,假设我们有一个程序来存储传感器网络的数据,其中主键是测量的时间戳(年月日时分秒)。范围扫描在这种情况下非常有用,因为我们可以轻松获取某个月份的所有数据。
然而Key Range 分区的缺点是某些特定的访问模式会导致热点。如果主键是时间戳则分区对应于时间范围例如给每天分配一个分区。不幸的是由于我们在测量发生时将数据从传感器写入数据库因此所有写入操作都会转到同一个分区即今天的分区这样分区可能会因写入而过载而其他分区则处于空闲状态【5】。
@@ -80,9 +82,9 @@
出于分区的目的散列函数不需要多么强壮的加密算法例如Cassandra 和 MongoDB 使用 MD5Voldemort 使用 Fowler-Noll-Vo 函数。许多编程语言都有内置的简单哈希函数(它们用于散列表),但是它们可能不适合分区:例如,在 Java 的 `Object.hashCode()` 和 Ruby 的 `Object#hash`同一个键可能在不同的进程中有不同的哈希值【6】。
一旦你有一个合适的键散列函数,你可以为每个分区分配一个散列范围(而不是键的范围),每个通过哈希散列落在分区范围内的键将被存储在该分区中。如 [图 6-3](img/fig6-3.png) 所示。
一旦你有一个合适的键散列函数,你可以为每个分区分配一个散列范围(而不是键的范围),每个通过哈希散列落在分区范围内的键将被存储在该分区中。如 [图 6-3](/img/fig6-3.png) 所示。
![](img/fig6-3.png)
![](/img/fig6-3.png)
**图 6-3 按哈希键分区**
@@ -90,7 +92,7 @@
> #### 一致性哈希
>
> 一致性哈希由 Karger 等人定义。【7】 用于跨互联网级别的缓存系统,例如 CDN 中,是一种能均匀分配负载的方法。它使用随机选择的 **分区边界partition boundaries** 来避免中央控制或分布式共识的需要。请注意,这里的一致性与复制一致性(请参阅 [第五章](ch5.md))或 ACID 一致性(请参阅 [第七章](ch7.md)无关而只是描述了一种再平衡rebalancing的特定方法。
> 一致性哈希由 Karger 等人定义。【7】 用于跨互联网级别的缓存系统,例如 CDN 中,是一种能均匀分配负载的方法。它使用随机选择的 **分区边界partition boundaries** 来避免中央控制或分布式共识的需要。请注意,这里的一致性与复制一致性(请参阅 [第五章](/ch5))或 ACID 一致性(请参阅 [第七章](/ch7)无关而只是描述了一种再平衡rebalancing的特定方法。
>
> 正如我们将在 “[分区再平衡](#分区再平衡)” 中所看到的,这种特殊的方法对于数据库实际上并不是很好,所以在实际中很少使用(某些数据库的文档仍然会使用一致性哈希的说法,但是它往往是不准确的)。因为有可能产生混淆,所以最好避免使用一致性哈希这个术语,而只是把它称为 **散列分区hash partitioning**
@@ -118,7 +120,7 @@ Cassandra 采取了折衷的策略【11, 12, 13】。Cassandra 中的表可以
到目前为止,我们讨论的分区方案依赖于键值数据模型。如果只通过主键访问记录,我们可以从该键确定分区,并使用它来将读写请求路由到负责该键的分区。
如果涉及次级索引,情况会变得更加复杂(参考 “[其他索引结构](ch3.md#其他索引结构)”)。次级索引通常并不能唯一地标识记录,而是一种搜索记录中出现特定值的方式:查找用户 123 的所有操作、查找包含词语 `hogwash` 的所有文章、查找所有颜色为红色的车辆等等。
如果涉及次级索引,情况会变得更加复杂(参考 “[其他索引结构](/ch3#其他索引结构)”)。次级索引通常并不能唯一地标识记录,而是一种搜索记录中出现特定值的方式:查找用户 123 的所有操作、查找包含词语 `hogwash` 的所有文章、查找所有颜色为红色的车辆等等。
次级索引是关系型数据库的基础,并且在文档数据库中也很普遍。许多键值存储(如 HBase 和 Volde-mort为了减少实现的复杂度而放弃了次级索引但是一些如 Riak已经开始添加它们因为它们对于数据模型实在是太有用了。并且次级索引也是 Solr 和 Elasticsearch 等搜索服务器的基石。
@@ -126,31 +128,31 @@ Cassandra 采取了折衷的策略【11, 12, 13】。Cassandra 中的表可以
### 基于文档的次级索引进行分区
假设你正在经营一个销售二手车的网站(如 [图 6-4](img/fig6-4.png) 所示)。每个列表都有一个唯一的 ID—— 称之为文档 ID—— 并且用文档 ID 对数据库进行分区(例如,分区 0 中的 ID 0 到 499分区 1 中的 ID 500 到 999 等)。
假设你正在经营一个销售二手车的网站(如 [图 6-4](/img/fig6-4.png) 所示)。每个列表都有一个唯一的 ID—— 称之为文档 ID—— 并且用文档 ID 对数据库进行分区(例如,分区 0 中的 ID 0 到 499分区 1 中的 ID 500 到 999 等)。
你想让用户搜索汽车,允许他们通过颜色和厂商过滤,所以需要一个在颜色和厂商上的次级索引(文档数据库中这些是 **字段field**,关系数据库中这些是 **列column** )。如果你声明了索引,则数据库可以自动执行索引 [^ii]。例如,无论何时将红色汽车添加到数据库,数据库分区都会自动将其添加到索引条目 `color:red` 的文档 ID 列表中。
[^ii]: 如果数据库仅支持键值模型,则你可能会尝试在应用程序代码中创建从值到文档 ID 的映射来实现次级索引。如果沿着这条路线走下去,请万分小心,确保你的索引与底层数据保持一致。竞争条件和间歇性写入失败(其中一些更改已保存,但其他更改未保存)很容易导致数据不同步 - 请参阅 “[多对象事务的需求](ch7.md#多对象事务的需求)”。
[^ii]: 如果数据库仅支持键值模型,则你可能会尝试在应用程序代码中创建从值到文档 ID 的映射来实现次级索引。如果沿着这条路线走下去,请万分小心,确保你的索引与底层数据保持一致。竞争条件和间歇性写入失败(其中一些更改已保存,但其他更改未保存)很容易导致数据不同步 - 请参阅 “[多对象事务的需求](/ch7#多对象事务的需求)”。
![](img/fig6-4.png)
![](/img/fig6-4.png)
**图 6-4 基于文档的次级索引进行分区**
在这种索引方法中,每个分区是完全独立的:每个分区维护自己的次级索引,仅覆盖该分区中的文档。它不关心存储在其他分区的数据。无论何时你需要写入数据库(添加,删除或更新文档),只需处理包含你正在编写的文档 ID 的分区即可。出于这个原因,**文档分区索引** 也被称为 **本地索引**(而不是将在下一节中描述的 **全局索引**)。
但是,从文档分区索引中读取需要注意:除非你对文档 ID 做了特别的处理,否则没有理由将所有具有特定颜色或特定品牌的汽车放在同一个分区中。在 [图 6-4](img/fig6-4.png) 中,红色汽车出现在分区 0 和分区 1 中。因此,如果要搜索红色汽车,则需要将查询发送到所有分区,并合并所有返回的结果。
但是,从文档分区索引中读取需要注意:除非你对文档 ID 做了特别的处理,否则没有理由将所有具有特定颜色或特定品牌的汽车放在同一个分区中。在 [图 6-4](/img/fig6-4.png) 中,红色汽车出现在分区 0 和分区 1 中。因此,如果要搜索红色汽车,则需要将查询发送到所有分区,并合并所有返回的结果。
这种查询分区数据库的方法有时被称为 **分散 / 聚集scatter/gather**,并且可能会使次级索引上的读取查询相当昂贵。即使并行查询分区,分散 / 聚集也容易导致尾部延迟放大(请参阅 “[实践中的百分位点](ch1.md#实践中的百分位点)”。然而它被广泛使用MongoDBRiak 【15】Cassandra 【16】Elasticsearch 【17】SolrCloud 【18】和 VoltDB 【19】都使用文档分区次级索引。大多数数据库供应商建议你构建一个能从单个分区提供次级索引查询的分区方案但这并不总是可行尤其是当在单个查询中使用多个次级索引时例如同时需要按颜色和制造商查询
这种查询分区数据库的方法有时被称为 **分散 / 聚集scatter/gather**,并且可能会使次级索引上的读取查询相当昂贵。即使并行查询分区,分散 / 聚集也容易导致尾部延迟放大(请参阅 “[实践中的百分位点](/ch1#实践中的百分位点)”。然而它被广泛使用MongoDBRiak 【15】Cassandra 【16】Elasticsearch 【17】SolrCloud 【18】和 VoltDB 【19】都使用文档分区次级索引。大多数数据库供应商建议你构建一个能从单个分区提供次级索引查询的分区方案但这并不总是可行尤其是当在单个查询中使用多个次级索引时例如同时需要按颜色和制造商查询
### 基于关键词(Term)的次级索引进行分区
我们可以构建一个覆盖所有分区数据的 **全局索引**,而不是给每个分区创建自己的次级索引(本地索引)。但是,我们不能只把这个索引存储在一个节点上,因为它可能会成为瓶颈,违背了分区的目的。全局索引也必须进行分区,但可以采用与主键不同的分区方式。
[图 6-5](img/fig6-5.png) 描述了这可能是什么样子:来自所有分区的红色汽车在红色索引中,并且索引是分区的,首字母从 `a``r` 的颜色在分区 0 中,`s``z` 的在分区 1。汽车制造商的索引也与之类似分区边界在 `f``h` 之间)。
[图 6-5](/img/fig6-5.png) 描述了这可能是什么样子:来自所有分区的红色汽车在红色索引中,并且索引是分区的,首字母从 `a``r` 的颜色在分区 0 中,`s``z` 的在分区 1。汽车制造商的索引也与之类似分区边界在 `f``h` 之间)。
![](img/fig6-5.png)
![](/img/fig6-5.png)
**图 6-5 基于关键词对次级索引进行分区**
@@ -160,11 +162,11 @@ Cassandra 采取了折衷的策略【11, 12, 13】。Cassandra 中的表可以
关键词分区的全局索引优于文档分区索引的地方点是它可以使读取更有效率:不需要 **分散 / 收集** 所有分区,客户端只需要向包含关键词的分区发出请求。全局索引的缺点在于写入速度较慢且较为复杂,因为写入单个文档现在可能会影响索引的多个分区(文档中的每个关键词可能位于不同的分区或者不同的节点上) 。
理想情况下,索引总是最新的,写入数据库的每个文档都会立即反映在索引中。但是,在关键词分区索引中,这需要跨分区的分布式事务,并不是所有数据库都支持(请参阅 [第七章](ch7.md) 和 [第九章](ch9.md))。
理想情况下,索引总是最新的,写入数据库的每个文档都会立即反映在索引中。但是,在关键词分区索引中,这需要跨分区的分布式事务,并不是所有数据库都支持(请参阅 [第七章](/ch7) 和 [第九章](/ch9))。
在实践中,对全局次级索引的更新通常是 **异步**也就是说如果在写入之后不久读取索引刚才所做的更改可能尚未反映在索引中。例如Amazon DynamoDB 声称在正常情况下其全局次级索引会在不到一秒的时间内更新但在基础架构出现故障的情况下可能会有延迟【20】。
全局关键词分区索引的其他用途包括 Riak 的搜索功能【21】和 Oracle 数据仓库它允许你在本地和全局索引之间进行选择【22】。我们将在 [第十二章](ch12.md) 中继续关键词分区次级索引实现的话题。
全局关键词分区索引的其他用途包括 Riak 的搜索功能【21】和 Oracle 数据仓库它允许你在本地和全局索引之间进行选择【22】。我们将在 [第十二章](/ch12) 中继续关键词分区次级索引实现的话题。
## 分区再平衡
@@ -189,7 +191,7 @@ Cassandra 采取了折衷的策略【11, 12, 13】。Cassandra 中的表可以
#### 反面教材hash mod N
我们在前面说过([图 6-3](img/fig6-3.png)),最好将可能的散列分成不同的范围,并将每个范围分配给一个分区(例如,如果 $0 ≤ hash(key)< b_0$,则将键分配给分区 0如果 $b_0 ≤ hash(key) < b_1$,则分配给分区 1
我们在前面说过([图 6-3](/img/fig6-3.png)),最好将可能的散列分成不同的范围,并将每个范围分配给一个分区(例如,如果 $0 ≤ hash(key)< b_0$,则将键分配给分区 0如果 $b_0 ≤ hash(key) < b_1$,则分配给分区 1
也许你想知道为什么我们不使用 ***取模mod***(许多编程语言中的 % 运算符)。例如,`hash(key) mod 10` 会返回一个介于 0 和 9 之间的数字(如果我们将散列写为十进制数,散列模 10 将是最后一个数字)。如果我们有 10 个节点,编号为 0 到 9这似乎是将每个键分配给一个节点的简单方法。
@@ -201,11 +203,11 @@ Cassandra 采取了折衷的策略【11, 12, 13】。Cassandra 中的表可以
幸运的是,有一个相当简单的解决方案:创建比节点更多的分区,并为每个节点分配多个分区。例如,运行在 10 个节点的集群上的数据库可能会从一开始就被拆分为 1,000 个分区,因此大约有 100 个分区被分配给每个节点。
现在,如果一个节点被添加到集群中,新节点可以从当前每个节点中 **窃取** 一些分区,直到分区再次公平分配。这个过程如 [图 6-6](img/fig6-6.png) 所示。如果从集群中删除一个节点,则会发生相反的情况。
现在,如果一个节点被添加到集群中,新节点可以从当前每个节点中 **窃取** 一些分区,直到分区再次公平分配。这个过程如 [图 6-6](/img/fig6-6.png) 所示。如果从集群中删除一个节点,则会发生相反的情况。
只有分区在节点之间的移动。分区的数量不会改变,键所指定的分区也不会改变。唯一改变的是分区所在的节点。这种变更并不是即时的 — 在网络上传输大量的数据需要一些时间 — 所以在传输过程中,原有分区仍然会接受读写操作。
![](img/fig6-6.png)
![](/img/fig6-6.png)
**图 6-6 将新节点添加到每个节点具有多个分区的数据库集群。**
@@ -219,7 +221,7 @@ Cassandra 采取了折衷的策略【11, 12, 13】。Cassandra 中的表可以
对于使用键范围分区的数据库(请参阅 “[根据键的范围分区](#根据键的范围分区)”),具有固定边界的固定数量的分区将非常不便:如果边界设置错误,可能会导致所有数据都在一个分区中,而其他分区则为空。手动重新配置分区边界将非常繁琐。
出于这个原因,按键的范围进行分区的数据库(如 HBase 和 RethinkDB会动态创建分区。当分区增长到超过配置的大小时在 HBase 上,默认值是 10GB会被分成两个分区每个分区约占一半的数据【26】。与之相反如果大量数据被删除并且分区缩小到某个阈值以下则可以将其与相邻分区合并。此过程与 B 树顶层发生的过程类似(请参阅 “[B 树](ch3.md#B树)”)。
出于这个原因,按键的范围进行分区的数据库(如 HBase 和 RethinkDB会动态创建分区。当分区增长到超过配置的大小时在 HBase 上,默认值是 10GB会被分成两个分区每个分区约占一半的数据【26】。与之相反如果大量数据被删除并且分区缩小到某个阈值以下则可以将其与相邻分区合并。此过程与 B 树顶层发生的过程类似(请参阅 “[B 树](/ch3#B树)”)。
每个分区分配给一个节点,每个节点可以处理多个分区,就像固定数量的分区一样。大型分区拆分后,可以将其中的一半转移到另一个节点,以平衡负载。在 HBase 中,分区文件的传输通过 HDFS底层使用的分布式文件系统来实现【3】。
@@ -265,21 +267,21 @@ Cassandra 和 Ketama 使用的第三种方法是使分区数与节点数成正
以上所有情况中的关键问题是:作出路由决策的组件(可能是节点之一,还是路由层或客户端)如何了解分区 - 节点之间的分配关系变化?
![](img/fig6-7.png)
![](/img/fig6-7.png)
**图 6-7 将请求路由到正确节点的三种不同方式。**
这是一个具有挑战性的问题,因为重要的是所有参与者都达成共识 - 否则请求将被发送到错误的节点,得不到正确的处理。在分布式系统中有达成共识的协议,但很难正确地实现(见 [第九章](ch9.md))。
这是一个具有挑战性的问题,因为重要的是所有参与者都达成共识 - 否则请求将被发送到错误的节点,得不到正确的处理。在分布式系统中有达成共识的协议,但很难正确地实现(见 [第九章](/ch9))。
许多分布式数据系统都依赖于一个独立的协调服务,比如 ZooKeeper 来跟踪集群元数据,如 [图 6-8](img/fig6-8.png) 所示。每个节点在 ZooKeeper 中注册自己ZooKeeper 维护分区到节点的可靠映射。其他参与者(如路由层或分区感知客户端)可以在 ZooKeeper 中订阅此信息。只要分区分配发生了改变或者集群中添加或删除了一个节点ZooKeeper 就会通知路由层使路由信息保持最新状态。
许多分布式数据系统都依赖于一个独立的协调服务,比如 ZooKeeper 来跟踪集群元数据,如 [图 6-8](/img/fig6-8.png) 所示。每个节点在 ZooKeeper 中注册自己ZooKeeper 维护分区到节点的可靠映射。其他参与者(如路由层或分区感知客户端)可以在 ZooKeeper 中订阅此信息。只要分区分配发生了改变或者集群中添加或删除了一个节点ZooKeeper 就会通知路由层使路由信息保持最新状态。
![](img/fig6-8.png)
![](/img/fig6-8.png)
**图 6-8 使用 ZooKeeper 跟踪分区分配给节点。**
例如LinkedIn的Espresso使用Helix 【31】进行集群管理依靠ZooKeeper实现了如[图6-8](img/fig6-8.png)所示的路由层。HBase、SolrCloud和Kafka也使用ZooKeeper来跟踪分区分配。MongoDB具有类似的体系结构但它依赖于自己的**配置服务器config server** 实现和mongos守护进程作为路由层。
例如LinkedIn的Espresso使用Helix 【31】进行集群管理依靠ZooKeeper实现了如[图6-8](/img/fig6-8.png)所示的路由层。HBase、SolrCloud和Kafka也使用ZooKeeper来跟踪分区分配。MongoDB具有类似的体系结构但它依赖于自己的**配置服务器config server** 实现和mongos守护进程作为路由层。
Cassandra 和 Riak 采取不同的方法:他们在节点之间使用 **流言协议gossip protocol** 来传播集群状态的变化。请求可以发送到任意节点,该节点会转发到包含所请求的分区的适当节点([图 6-7](img/fig6-7.png) 中的方法 1。这个模型在数据库节点中增加了更多的复杂性但是避免了对像 ZooKeeper 这样的外部协调服务的依赖。
Cassandra 和 Riak 采取不同的方法:他们在节点之间使用 **流言协议gossip protocol** 来传播集群状态的变化。请求可以发送到任意节点,该节点会转发到包含所请求的分区的适当节点([图 6-7](/img/fig6-7.png) 中的方法 1。这个模型在数据库节点中增加了更多的复杂性但是避免了对像 ZooKeeper 这样的外部协调服务的依赖。
Couchbase 不会自动进行再平衡,这简化了设计。通常情况下,它配置了一个名为 moxi 的路由层它会从集群节点了解路由变化【32】。
@@ -291,7 +293,7 @@ Couchbase 不会自动进行再平衡,这简化了设计。通常情况下,
然而,通常用于分析的 **大规模并行处理MPP, Massively parallel processing** 关系型数据库产品在其支持的查询类型方面要复杂得多。一个典型的数据仓库查询包含多个连接过滤分组和聚合操作。MPP 查询优化器将这个复杂的查询分解成许多执行阶段和分区,其中许多可以在数据库集群的不同节点上并行执行。涉及扫描大规模数据集的查询特别受益于这种并行执行。
数据仓库查询的快速并行执行是一个专门的话题,由于分析有很重要的商业意义,可以带来很多利益。我们将在 [第十章](ch10.md) 讨论并行查询执行的一些技巧。有关并行数据库中使用的技术的更详细的概述请参阅参考文献【1,33】。
数据仓库查询的快速并行执行是一个专门的话题,由于分析有很重要的商业意义,可以带来很多利益。我们将在 [第十章](/ch10) 讨论并行查询执行的一些技巧。有关并行数据库中使用的技术的更详细的概述请参阅参考文献【1,33】。
## 本章小结
@@ -358,11 +360,3 @@ Couchbase 不会自动进行再平衡,这简化了设计。通常情况下,
1. Kishore Gopalakrishna, Shi Lu, Zhen Zhang, et al.: “[Untangling Cluster Management with Helix](http://www.socc2012.org/helix_onecol.pdf?attredirects=0),” at *ACM Symposium on Cloud Computing* (SoCC), October 2012. [doi:10.1145/2391229.2391248](http://dx.doi.org/10.1145/2391229.2391248)
1. “[Moxi 1.8 Manual](http://docs.couchbase.com/moxi-manual-1.8/),” Couchbase, Inc., 2014.
1. Shivnath Babu and Herodotos Herodotou: “[Massively Parallel Databases and MapReduce Systems](https://www.microsoft.com/en-us/research/wp-content/uploads/2013/11/db-mr-survey-final.pdf),” *Foundations and Trends in Databases*, volume 5, number 1, pages 1104, November 2013. [doi:10.1561/1900000036](http://dx.doi.org/10.1561/1900000036)
------
| 上一章 | 目录 | 下一章 |
| :--------------------: | :-----------------------------: | :--------------------: |
| [第五章:复制](ch5.md) | [设计数据密集型应用](README.md) | [第七章:事务](ch7.md) |

View File

@@ -1,15 +1,16 @@
# 第七章:事务
---
title: "第七章:事务"
linkTitle: "7. 事务"
weight: 207
breadcrumbs: false
---
![](img/ch7.png)
![](/img/ch7.png)
> 一些作者声称,支持通用的两阶段提交代价太大,会带来性能与可用性的问题。让程序员来处理过度使用事务导致的性能问题,总比缺少事务编程好得多。
>
> —— James Corbett 等人SpannerGoogle 的全球分布式数据库2012
------
[TOC]
在数据系统的残酷现实中,很多事情都可能出错:
- 数据库软件、硬件可能在任意时刻发生故障(包括写操作进行到一半时)。
@@ -31,14 +32,14 @@
本章将研究许多出错案例,并探索数据库用于防范这些问题的算法。尤其会深入 **并发控制** 的领域,讨论各种可能发生的竞争条件,以及数据库如何实现 **读已提交read committed****快照隔离snapshot isolation** 和 **可串行化serializability** 等隔离级别。
本章同时适用于单机数据库与分布式数据库;在 [第八章](ch8.md) 中将重点讨论仅出现在分布式系统中的特殊挑战。
本章同时适用于单机数据库与分布式数据库;在 [第八章](/ch8) 中将重点讨论仅出现在分布式系统中的特殊挑战。
## 事务的棘手概念
现今,几乎所有的关系型数据库和一些非关系数据库都支持 **事务**。其中大多数遵循 IBM System R第一个 SQL 数据库)在 1975 年引入的风格【1,2,3】。40 年里尽管一些实现细节发生了变化但总体思路大同小异MySQL、PostgreSQL、Oracle 和 SQL Server 等数据库中的事务支持与 System R 异乎寻常地相似。
2000 年以后非关系NoSQL数据库开始普及。它们的目标是在关系数据库的现状基础上通过提供新的数据模型选择请参阅 [第二章](ch2.md)并默认包含复制第五章和分区第六章来进一步提升。事务是这次运动的主要牺牲品这些新一代数据库中的许多数据库完全放弃了事务或者重新定义了这个词描述比以前所理解的更弱得多的一套保证【4】。
2000 年以后非关系NoSQL数据库开始普及。它们的目标是在关系数据库的现状基础上通过提供新的数据模型选择请参阅 [第二章](/ch2)并默认包含复制第五章和分区第六章来进一步提升。事务是这次运动的主要牺牲品这些新一代数据库中的许多数据库完全放弃了事务或者重新定义了这个词描述比以前所理解的更弱得多的一套保证【4】。
随着这种新型分布式数据库的炒作人们普遍认为事务是可伸缩性的对立面任何大型系统都必须放弃事务以保持良好的性能和高可用性【5,6】。另一方面数据库厂商有时将事务保证作为 “重要应用” 和 “有价值数据” 的基本要求。这两种观点都是 **纯粹的夸张**
@@ -70,9 +71,9 @@ ACID 原子性的定义特征是:**能够在错误时中止事务,丢弃该
一致性这个词被赋予太多含义:
* 在 [第五章](ch5.md) 中,我们讨论了副本一致性,以及异步复制系统中的最终一致性问题(请参阅 “[复制延迟问题](ch5.md#复制延迟问题)”)。
* [一致性哈希](ch6.md#一致性哈希) 是某些系统用于重新分区的一种分区方法。
* 在 [CAP 定理](ch9.md#CAP定理) 中,一致性一词用于表示 [线性一致性](ch9.md#线性一致性)。
* 在 [第五章](/ch5) 中,我们讨论了副本一致性,以及异步复制系统中的最终一致性问题(请参阅 “[复制延迟问题](/ch5#复制延迟问题)”)。
* [一致性哈希](/ch6#一致性哈希) 是某些系统用于重新分区的一种分区方法。
* 在 [CAP 定理](/ch9#CAP定理) 中,一致性一词用于表示 [线性一致性](/ch9#线性一致性)。
* 在 ACID 的上下文中,**一致性** 是指数据库在应用程序的特定概念中处于 “良好状态”。
很不幸,这一个词就至少有四种不同的含义。
@@ -89,11 +90,11 @@ ACID 一致性的概念是,**对数据的一组特定约束必须始终成立*
大多数数据库都会同时被多个客户端访问。如果它们各自读写数据库的不同部分,这是没有问题的,但是如果它们访问相同的数据库记录,则可能会遇到 **并发** 问题(**竞争条件**,即 race conditions
[图 7-1](img/fig7-1.png) 是这类问题的一个简单例子。假设你有两个客户端同时在数据库中增长一个计数器。(假设数据库没有内建的自增操作)每个客户端需要读取计数器的当前值,加 1 ,再回写新值。[图 7-1](img/fig7-1.png) 中,因为发生了两次增长,计数器应该从 42 增至 44但由于竞态条件实际上只增至 43 。
[图 7-1](/img/fig7-1.png) 是这类问题的一个简单例子。假设你有两个客户端同时在数据库中增长一个计数器。(假设数据库没有内建的自增操作)每个客户端需要读取计数器的当前值,加 1 ,再回写新值。[图 7-1](/img/fig7-1.png) 中,因为发生了两次增长,计数器应该从 42 增至 44但由于竞态条件实际上只增至 43 。
ACID 意义上的隔离性意味着,**同时执行的事务是相互隔离的**:它们不能相互冒犯。传统的数据库教科书将隔离性形式化为 **可串行化Serializability**这意味着每个事务可以假装它是唯一在整个数据库上运行的事务。数据库确保当多个事务被提交时结果与它们串行运行一个接一个是一样的尽管实际上它们可能是并发运行的【10】。
![](img/fig7-1.png)
![](/img/fig7-1.png)
**图 7-1 两个客户之间的竞争状态同时递增计数器**
@@ -103,9 +104,9 @@ ACID 意义上的隔离性意味着,**同时执行的事务是相互隔离的*
数据库系统的目的是,提供一个安全的地方存储数据,而不用担心丢失。**持久性** 是一个承诺,即一旦事务成功完成,即使发生硬件故障或数据库崩溃,写入的任何数据也不会丢失。
在单节点数据库中,持久性通常意味着数据已被写入非易失性存储设备,如硬盘或 SSD。它通常还包括预写日志或类似的文件请参阅 “[让 B 树更可靠](ch3.md#让B树更可靠)”),以便在磁盘上的数据结构损坏时进行恢复。在带复制的数据库中,持久性可能意味着数据已成功复制到一些节点。为了提供持久性保证,数据库必须等到这些写入或复制完成后,才能报告事务成功提交。
在单节点数据库中,持久性通常意味着数据已被写入非易失性存储设备,如硬盘或 SSD。它通常还包括预写日志或类似的文件请参阅 “[让 B 树更可靠](/ch3#让B树更可靠)”),以便在磁盘上的数据结构损坏时进行恢复。在带复制的数据库中,持久性可能意味着数据已成功复制到一些节点。为了提供持久性保证,数据库必须等到这些写入或复制完成后,才能报告事务成功提交。
如 “[可靠性](ch1.md#可靠性)” 一节所述,**完美的持久性是不存在的** :如果所有硬盘和所有备份同时被销毁,那显然没有任何数据库能救得了你。
如 “[可靠性](/ch1#可靠性)” 一节所述,**完美的持久性是不存在的** :如果所有硬盘和所有备份同时被销毁,那显然没有任何数据库能救得了你。
> #### 复制与持久性
>
@@ -114,8 +115,8 @@ ACID 意义上的隔离性意味着,**同时执行的事务是相互隔离的*
> 真相是,没有什么是完美的:
>
> * 如果你写入磁盘然后机器宕机,即使数据没有丢失,在修复机器或将磁盘转移到其他机器之前,也是无法访问的。这种情况下,复制系统可以保持可用性。
> * 一个相关性故障(停电,或一个特定输入导致所有节点崩溃的 Bug可能会一次性摧毁所有副本请参阅「[可靠性](ch1.md#可靠性)」),任何仅存储在内存中的数据都会丢失,故内存数据库仍然要和磁盘写入打交道。
> * 在异步复制系统中,当主库不可用时,最近的写入操作可能会丢失(请参阅「[处理节点宕机](ch5.md#处理节点宕机)」)。
> * 一个相关性故障(停电,或一个特定输入导致所有节点崩溃的 Bug可能会一次性摧毁所有副本请参阅「[可靠性](/ch1#可靠性)」),任何仅存储在内存中的数据都会丢失,故内存数据库仍然要和磁盘写入打交道。
> * 在异步复制系统中,当主库不可用时,最近的写入操作可能会丢失(请参阅「[处理节点宕机](/ch5#处理节点宕机)」)。
> * 当电源突然断电时,特别是固态硬盘,有证据显示有时会违反应有的保证:甚至 fsync 也不能保证正常工作【12】。硬盘固件可能有错误就像任何其他类型的软件一样【13,14】。
> * 存储引擎和文件系统之间的微妙交互可能会导致难以追踪的错误并可能导致磁盘上的文件在崩溃后被损坏【15,16】。
> * 磁盘上的数据可能会在没有检测到的情况下逐渐损坏【17】。如果数据已损坏一段时间副本和最近的备份也可能损坏。这种情况下需要尝试从历史备份中恢复数据。
@@ -136,7 +137,7 @@ ACID 意义上的隔离性意味着,**同时执行的事务是相互隔离的*
同时运行的事务不应该互相干扰。例如,如果一个事务进行多次写入,则另一个事务要么看到全部写入结果,要么什么都看不到,但不应该是一些子集。
这些定义假设你想同时修改多个对象(行,文档,记录)。通常需要 **多对象事务multi-object transaction** 来保持多块数据同步。[图 7-2](img/fig7-2.png) 展示了一个来自电邮应用的例子。执行以下查询来显示用户未读邮件数量:
这些定义假设你想同时修改多个对象(行,文档,记录)。通常需要 **多对象事务multi-object transaction** 来保持多块数据同步。[图 7-2](/img/fig7-2.png) 展示了一个来自电邮应用的例子。执行以下查询来显示用户未读邮件数量:
```sql
SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
@@ -144,23 +145,23 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
但如果邮件太多,你可能会觉得这个查询太慢,并决定用单独的字段存储未读邮件的数量(一种反规范化)。现在每当一个新消息写入时,必须也增长未读计数器,每当一个消息被标记为已读时,也必须减少未读计数器。
在 [图 7-2](img/fig7-2.png) 中,用户 2 遇到异常情况:邮件列表里显示有未读消息,但计数器显示为零未读消息,因为计数器增长还没有发生 [^ii]。隔离性可以避免这个问题:通过确保用户 2 要么同时看到新邮件和增长后的计数器,要么都看不到,而不是一个前后矛盾的中间结果。
在 [图 7-2](/img/fig7-2.png) 中,用户 2 遇到异常情况:邮件列表里显示有未读消息,但计数器显示为零未读消息,因为计数器增长还没有发生 [^ii]。隔离性可以避免这个问题:通过确保用户 2 要么同时看到新邮件和增长后的计数器,要么都看不到,而不是一个前后矛盾的中间结果。
[^ii]: 可以说邮件应用中的错误计数器并不是什么特别重要的问题。但换种方式来看,你可以把未读计数器换成客户账户余额,把邮件收发看成支付交易。
![](img/fig7-2.png)
![](/img/fig7-2.png)
**图 7-2 违反隔离性:一个事务读取另一个事务的未被执行的写入(“脏读”)。**
[图 7-3](img/fig7-3.png) 说明了对原子性的需求:如果在事务过程中发生错误,邮箱和未读计数器的内容可能会失去同步。在原子事务中,如果对计数器的更新失败,事务将被中止,并且插入的电子邮件将被回滚。
[图 7-3](/img/fig7-3.png) 说明了对原子性的需求:如果在事务过程中发生错误,邮箱和未读计数器的内容可能会失去同步。在原子事务中,如果对计数器的更新失败,事务将被中止,并且插入的电子邮件将被回滚。
![](img/fig7-3.png)
![](/img/fig7-3.png)
**图 7-3 原子性确保发生错误时,事务先前的任何写入都会被撤消,以避免状态不一致**
多对象事务需要某种方式来确定哪些读写操作属于同一个事务。在关系型数据库中,通常基于客户端与数据库服务器的 TCP 连接:在任何特定连接上,`BEGIN TRANSACTION``COMMIT` 语句之间的所有内容,被认为是同一事务的一部分.[^iii]
[^iii]: 这并不完美。如果 TCP 连接中断,则事务必须中止。如果中断发生在客户端请求提交之后,但在服务器确认提交发生之前,客户端并不知道事务是否已提交。为了解决这个问题,事务管理器可以通过一个唯一事务标识符来对操作进行分组,这个标识符并未绑定到特定 TCP 连接。后续再 “[数据库的端到端原则](ch12.md#数据库的端到端原则)” 一节将回到这个主题。
[^iii]: 这并不完美。如果 TCP 连接中断,则事务必须中止。如果中断发生在客户端请求提交之后,但在服务器确认提交发生之前,客户端并不知道事务是否已提交。为了解决这个问题,事务管理器可以通过一个唯一事务标识符来对操作进行分组,这个标识符并未绑定到特定 TCP 连接。后续再 “[数据库的端到端原则](/ch12#数据库的端到端原则)” 一节将回到这个主题。
另一方面,许多非关系数据库并没有将这些操作组合在一起的方法。即使存在多对象 API例如某键值存储可能具有在一个操作中更新几个键的 multi-put 操作),但这并不一定意味着它具有事务语义:该命令可能在一些键上成功,在其他的键上失败,使数据库处于部分更新的状态。
@@ -172,9 +173,9 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
- 如果在数据库正在覆盖磁盘上的前一个值的过程中电源发生故障,是否最终将新旧值拼接在一起?
- 如果另一个客户端在写入过程中读取该文档,是否会看到部分更新的值?
这些问题非常让人头大,故存储引擎一个几乎普遍的目标是:对单节点上的单个对象(例如键值对)上提供原子性和隔离性。原子性可以通过使用日志来实现崩溃恢复(请参阅 “[让 B 树更可靠](ch3.md#让B树更可靠)”),并且可以使用每个对象上的锁来实现隔离(每次只允许一个线程访问对象) 。
这些问题非常让人头大,故存储引擎一个几乎普遍的目标是:对单节点上的单个对象(例如键值对)上提供原子性和隔离性。原子性可以通过使用日志来实现崩溃恢复(请参阅 “[让 B 树更可靠](/ch3#让B树更可靠)”),并且可以使用每个对象上的锁来实现隔离(每次只允许一个线程访问对象) 。
一些数据库也提供更复杂的原子操作 [^iv],例如自增操作,这样就不再需要像 [图 7-1](img/fig7-1.png) 那样的读取 - 修改 - 写入序列了。同样流行的是 **[比较和设置CAS, compare-and-set](#比较并设置CAS)** 操作,仅当值没有被其他并发修改过时,才允许执行写操作。
一些数据库也提供更复杂的原子操作 [^iv],例如自增操作,这样就不再需要像 [图 7-1](/img/fig7-1.png) 那样的读取 - 修改 - 写入序列了。同样流行的是 **[比较和设置CAS, compare-and-set](#比较并设置CAS)** 操作,仅当值没有被其他并发修改过时,才允许执行写操作。
[^iv]: 严格地说,**原子自增atomic increment** 这个术语在多线程编程的意义上使用了原子这个词。在 ACID 的情况下,它实际上应该被称为 **隔离的isolated** 的或 **可串行的serializable** 的增量。但这就太吹毛求疵了。
@@ -182,23 +183,23 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
#### 多对象事务的需求
许多分布式数据存储已经放弃了多对象事务,因为多对象事务很难跨分区实现,而且在需要高可用性或高性能的情况下,它们可能会碍事。但说到底,在分布式数据库中实现事务,并没有什么根本性的障碍。[第九章](ch9.md) 将讨论分布式事务的实现。
许多分布式数据存储已经放弃了多对象事务,因为多对象事务很难跨分区实现,而且在需要高可用性或高性能的情况下,它们可能会碍事。但说到底,在分布式数据库中实现事务,并没有什么根本性的障碍。[第九章](/ch9) 将讨论分布式事务的实现。
但是我们是否需要多对象事务?**是否有可能只用键值数据模型和单对象操作来实现任何应用程序?**
有一些场景中,单对象插入,更新和删除是足够的。但是许多其他场景需要协调写入几个不同的对象:
* 在关系数据模型中,一个表中的行通常具有对另一个表中的行的外键引用。(类似的是,在一个图数据模型中,一个顶点有着到其他顶点的边)。多对象事务使你确保这些引用始终有效:当插入几个相互引用的记录时,外键必须是正确的和最新的,不然数据就没有意义。
* 在文档数据模型中,需要一起更新的字段通常在同一个文档中,这被视为单个对象 —— 更新单个文档时不需要多对象事务。但是,缺乏连接功能的文档数据库会鼓励非规范化(请参阅 “[关系型数据库与文档数据库在今日的对比](ch2.md#关系型数据库与文档数据库在今日的对比)”)。当需要更新非规范化的信息时,如 [图 7-2](img/fig7-2.png) 所示,需要一次更新多个文档。事务在这种情况下非常有用,可以防止非规范化的数据不同步。
* 在文档数据模型中,需要一起更新的字段通常在同一个文档中,这被视为单个对象 —— 更新单个文档时不需要多对象事务。但是,缺乏连接功能的文档数据库会鼓励非规范化(请参阅 “[关系型数据库与文档数据库在今日的对比](/ch2#关系型数据库与文档数据库在今日的对比)”)。当需要更新非规范化的信息时,如 [图 7-2](/img/fig7-2.png) 所示,需要一次更新多个文档。事务在这种情况下非常有用,可以防止非规范化的数据不同步。
* 在具有次级索引的数据库中(除了纯粹的键值存储以外几乎都有),每次更改值时都需要更新索引。从事务角度来看,这些索引是不同的数据库对象:例如,如果没有事务隔离性,记录可能出现在一个索引中,但没有出现在另一个索引中,因为第二个索引的更新还没有发生。
这些应用仍然可以在没有事务的情况下实现。然而,**没有原子性,错误处理就要复杂得多,缺乏隔离性,就会导致并发问题**。我们将在 “[弱隔离级别](#弱隔离级别)” 中讨论这些问题,并在 [第十二章](ch12.md) 中探讨其他方法。
这些应用仍然可以在没有事务的情况下实现。然而,**没有原子性,错误处理就要复杂得多,缺乏隔离性,就会导致并发问题**。我们将在 “[弱隔离级别](#弱隔离级别)” 中讨论这些问题,并在 [第十二章](/ch12) 中探讨其他方法。
#### 处理错误和中止
事务的一个关键特性是如果发生错误它可以中止并安全地重试。ACID 数据库基于这样的哲学:如果数据库有违反其原子性,隔离性或持久性的危险,则宁愿完全放弃事务,而不是留下半成品。
然而并不是所有的系统都遵循这个哲学。特别是具有 [无主复制](ch5.md#无主复制) 的数据存储,主要是在 “尽力而为” 的基础上进行工作。可以概括为 “数据库将做尽可能多的事,运行遇到错误时,它不会撤消它已经完成的事情” —— 所以,从错误中恢复是应用程序的责任。
然而并不是所有的系统都遵循这个哲学。特别是具有 [无主复制](/ch5#无主复制) 的数据存储,主要是在 “尽力而为” 的基础上进行工作。可以概括为 “数据库将做尽可能多的事,运行遇到错误时,它不会撤消它已经完成的事情” —— 所以,从错误中恢复是应用程序的责任。
错误发生不可避免,但许多软件开发人员倾向于只考虑乐观情况,而不是错误处理的复杂性。例如,像 Rails 的 ActiveRecord 和 Django 这样的 **对象关系映射ORM, object-relation Mapping** 框架不会重试中断的事务 —— 这个错误通常会导致一个从堆栈向上传播的异常,所以任何用户输入都会被丢弃,用户拿到一个错误信息。这实在是太耻辱了,因为中止的重点就是允许安全的重试。
@@ -207,7 +208,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
- 如果事务实际上成功了,但是在服务器试图向客户端确认提交成功时网络发生故障(所以客户端认为提交失败了),那么重试事务会导致事务被执行两次 —— 除非你有一个额外的应用级去重机制。
- 如果错误是由于负载过大造成的,则重试事务将使问题变得更糟,而不是更好。为了避免这种正反馈循环,可以限制重试次数,使用指数退避算法,并单独处理与过载相关的错误(如果允许)。
- 仅在临时性错误(例如,由于死锁,异常情况,临时性网络中断和故障切换)后才值得重试。在发生永久性错误(例如,违反约束)之后重试是毫无意义的。
- 如果事务在数据库之外也有副作用,即使事务被中止,也可能发生这些副作用。例如,如果你正在发送电子邮件,那你肯定不希望每次重试事务时都重新发送电子邮件。如果你想确保几个不同的系统一起提交或放弃,**两阶段提交2PC, two-phase commit** 可以提供帮助(“[原子提交与两阶段提交](ch9.md#原子提交与两阶段提交)” 中将讨论这个问题)。
- 如果事务在数据库之外也有副作用,即使事务被中止,也可能发生这些副作用。例如,如果你正在发送电子邮件,那你肯定不希望每次重试事务时都重新发送电子邮件。如果你想确保几个不同的系统一起提交或放弃,**两阶段提交2PC, two-phase commit** 可以提供帮助(“[原子提交与两阶段提交](/ch9#原子提交与两阶段提交)” 中将讨论这个问题)。
- 如果客户端进程在重试中失效,任何试图写入数据库的数据都将丢失。
## 弱隔离级别
@@ -243,16 +244,16 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
设想一个事务已经将一些数据写入数据库,但事务还没有提交或中止。另一个事务可以看到未提交的数据吗?如果是的话,那就叫做 **脏读dirty reads**【2】。
**读已提交** 隔离级别运行的事务必须防止脏读。这意味着事务的任何写入操作只有在该事务提交时才能被其他人看到(然后所有的写入操作都会立即变得可见)。如 [图 7-4](img/fig7-4.png) 所示,用户 1 设置了 `x = 3`,但用户 2 的 `get x` 仍旧返回旧值 2 (当用户 1 尚未提交时)。
**读已提交** 隔离级别运行的事务必须防止脏读。这意味着事务的任何写入操作只有在该事务提交时才能被其他人看到(然后所有的写入操作都会立即变得可见)。如 [图 7-4](/img/fig7-4.png) 所示,用户 1 设置了 `x = 3`,但用户 2 的 `get x` 仍旧返回旧值 2 (当用户 1 尚未提交时)。
![](img/fig7-4.png)
![](/img/fig7-4.png)
**图 7-4 没有脏读:用户 2 只有在用户 1 的事务已经提交后才能看到 x 的新值。**
为什么要防止脏读,有几个原因:
- 如果事务需要更新多个对象,脏读取意味着另一个事务可能会只看到一部分更新。例如,在 [图 7-2](img/fig7-2.png) 中,用户看到新的未读电子邮件,但看不到更新的计数器。这就是电子邮件的脏读。看到处于部分更新状态的数据库会让用户感到困惑,并可能导致其他事务做出错误的决定。
- 如果事务中止,则所有写入操作都需要回滚(如 [图 7-3](img/fig7-3.png) 所示)。如果数据库允许脏读,那就意味着一个事务可能会看到稍后需要回滚的数据,即从未实际提交给数据库的数据。想想后果就让人头大。
- 如果事务需要更新多个对象,脏读取意味着另一个事务可能会只看到一部分更新。例如,在 [图 7-2](/img/fig7-2.png) 中,用户看到新的未读电子邮件,但看不到更新的计数器。这就是电子邮件的脏读。看到处于部分更新状态的数据库会让用户感到困惑,并可能导致其他事务做出错误的决定。
- 如果事务中止,则所有写入操作都需要回滚(如 [图 7-3](/img/fig7-3.png) 所示)。如果数据库允许脏读,那就意味着一个事务可能会看到稍后需要回滚的数据,即从未实际提交给数据库的数据。想想后果就让人头大。
#### 没有脏写
@@ -262,10 +263,10 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
通过防止脏写,这个隔离级别避免了一些并发问题:
- 如果事务更新多个对象,脏写会导致不好的结果。例如,考虑 [图 7-5](img/fig7-5.png)以一个二手车销售网站为例Alice 和 Bob 两个人同时试图购买同一辆车。购买汽车需要两次数据库写入:网站上的商品列表需要更新,以反映买家的购买,销售发票需要发送给买家。在 [图 7-5](img/fig7-5.png) 的情况下,销售是属于 Bob 的(因为他成功更新了商品列表),但发票却寄送给了 Alice因为她成功更新了发票表。读已提交会防止这样的事故。
- 但是,读已提交并不能防止 [图 7-1](img/fig7-1.png) 中两个计数器增量之间的竞争状态。在这种情况下,第二次写入发生在第一个事务提交后,所以它不是一个脏写。这仍然是不正确的,但是出于不同的原因,在 “[防止丢失更新](#防止丢失更新)” 中将讨论如何使这种计数器增量安全。
- 如果事务更新多个对象,脏写会导致不好的结果。例如,考虑 [图 7-5](/img/fig7-5.png)以一个二手车销售网站为例Alice 和 Bob 两个人同时试图购买同一辆车。购买汽车需要两次数据库写入:网站上的商品列表需要更新,以反映买家的购买,销售发票需要发送给买家。在 [图 7-5](/img/fig7-5.png) 的情况下,销售是属于 Bob 的(因为他成功更新了商品列表),但发票却寄送给了 Alice因为她成功更新了发票表。读已提交会防止这样的事故。
- 但是,读已提交并不能防止 [图 7-1](/img/fig7-1.png) 中两个计数器增量之间的竞争状态。在这种情况下,第二次写入发生在第一个事务提交后,所以它不是一个脏写。这仍然是不正确的,但是出于不同的原因,在 “[防止丢失更新](#防止丢失更新)” 中将讨论如何使这种计数器增量安全。
![](img/fig7-5.png)
![](/img/fig7-5.png)
**图 7-5 如果存在脏写,来自不同事务的冲突写入可能会混淆在一起**
@@ -279,7 +280,7 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
但是要求读锁的办法在实践中效果并不好。因为一个长时间运行的写入事务会迫使许多只读事务等到这个慢写入事务完成。这会影响只读事务的响应时间,并且不利于可操作性:因为等待锁,应用某个部分的迟缓可能由于连锁效应,导致其他部分出现问题。
出于这个原因,大多数数据库 [^vi] 使用 [图 7-4](img/fig7-4.png) 的方式防止脏读:对于写入的每个对象,数据库都会记住旧的已提交值,和由当前持有写入锁的事务设置的新值。当事务正在进行时,任何其他读取对象的事务都会拿到旧值。只有当新值提交后,事务才会切换到读取新值。
出于这个原因,大多数数据库 [^vi] 使用 [图 7-4](/img/fig7-4.png) 的方式防止脏读:对于写入的每个对象,数据库都会记住旧的已提交值,和由当前持有写入锁的事务设置的新值。当事务正在进行时,任何其他读取对象的事务都会拿到旧值。只有当新值提交后,事务才会切换到读取新值。
[^vi]: 在撰写本文时,唯一在读已提交隔离级别使用读锁的主流数据库是 IBM DB2 和使用 `read_committed_snapshot = off` 配置的 Microsoft SQL Server【23,36】。
@@ -287,9 +288,9 @@ SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true
如果只从表面上看读已提交隔离级别,你可能就认为它完成了事务所需的一切,这是情有可原的。它允许 **中止**(原子性的要求);它防止读取不完整的事务结果,并且防止并发写入造成的混乱。事实上这些功能非常有用,比起没有事务的系统来,可以提供更多的保证。
但是在使用此隔离级别时,仍然有很多地方可能会产生并发错误。例如 [图 7-6](img/fig7-6.png) 说明了读已提交时可能发生的问题。
但是在使用此隔离级别时,仍然有很多地方可能会产生并发错误。例如 [图 7-6](/img/fig7-6.png) 说明了读已提交时可能发生的问题。
![](img/fig7-6.png)
![](/img/fig7-6.png)
**图 7-6 读取偏差Alice 观察数据库处于不一致的状态**
@@ -297,7 +298,7 @@ Alice 在银行有 1000 美元的储蓄,分为两个账户,每个 500 美元
这种异常被称为 **不可重复读nonrepeatable read****读取偏差read skew**:如果 Alice 在事务结束时再次读取账户 1 的余额她将看到与她之前的查询中看到的不同的值600 美元)。在读已提交的隔离条件下,**不可重复读** 被认为是可接受的Alice 看到的帐户余额确实在阅读时已经提交了。
> 不幸的是,术语 **偏差skew** 这个词是过载的:以前使用它是因为热点的不平衡工作量(请参阅 “[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)”),而这里偏差意味着异常的时序。
> 不幸的是,术语 **偏差skew** 这个词是过载的:以前使用它是因为热点的不平衡工作量(请参阅 “[负载偏斜与热点消除](/ch6#负载偏斜与热点消除)”),而这里偏差意味着异常的时序。
对于 Alice 的情况,这不是一个长期持续的问题。因为如果她几秒钟后刷新银行网站的页面,她很可能会看到一致的帐户余额。但是有些情况下,不能容忍这种暂时的不一致:
@@ -307,7 +308,7 @@ Alice 在银行有 1000 美元的储蓄,分为两个账户,每个 500 美元
* 分析查询和完整性检查
有时,你可能需要运行一个查询,扫描大部分的数据库。这样的查询在分析中很常见(请参阅 “[事务处理还是分析?](ch3.md#事务处理还是分析?)”),也可能是定期完整性检查(即监视数据损坏)的一部分。如果这些查询在不同时间点观察数据库的不同部分,则可能会返回毫无意义的结果。
有时,你可能需要运行一个查询,扫描大部分的数据库。这样的查询在分析中很常见(请参阅 “[事务处理还是分析?](/ch3#事务处理还是分析?)”),也可能是定期完整性检查(即监视数据损坏)的一部分。如果这些查询在不同时间点观察数据库的不同部分,则可能会返回毫无意义的结果。
**快照隔离snapshot isolation**【28】是这个问题最常见的解决方案。想法是每个事务都从数据库的 **一致快照consistent snapshot** 中读取 —— 也就是说,事务可以看到事务开始时在数据库中提交的所有数据。即使这些数据随后被另一个事务更改,每个事务也只能看到该特定时间点的旧数据。
@@ -319,15 +320,15 @@ Alice 在银行有 1000 美元的储蓄,分为两个账户,每个 500 美元
与读取提交的隔离类似,快照隔离的实现通常使用写锁来防止脏写(请参阅 “[读已提交](#读已提交)”),这意味着进行写入的事务会阻止另一个事务修改同一个对象。但是读取则不需要加锁。从性能的角度来看,快照隔离的一个关键原则是:**读不阻塞写,写不阻塞读**。这允许数据库在处理一致性快照上的长时间查询时,可以正常地同时处理写入操作,且两者间没有任何锁争用。
为了实现快照隔离,数据库使用了我们看到的用于防止 [图 7-4](img/fig7-4.png) 中的脏读的机制的一般化。数据库必须可能保留一个对象的几个不同的提交版本,因为各种正在进行的事务可能需要看到数据库在不同的时间点的状态。因为它同时维护着单个对象的多个版本,所以这种技术被称为 **多版本并发控制MVCC, multi-version concurrency control**
为了实现快照隔离,数据库使用了我们看到的用于防止 [图 7-4](/img/fig7-4.png) 中的脏读的机制的一般化。数据库必须可能保留一个对象的几个不同的提交版本,因为各种正在进行的事务可能需要看到数据库在不同的时间点的状态。因为它同时维护着单个对象的多个版本,所以这种技术被称为 **多版本并发控制MVCC, multi-version concurrency control**
如果一个数据库只需要提供 **读已提交** 的隔离级别,而不提供 **快照隔离**,那么保留一个对象的两个版本就足够了:已提交的版本和被覆盖但尚未提交的版本。不过支持快照隔离的存储引擎通常也使用 MVCC 来实现 **读已提交** 隔离级别。一种典型的方法是 **读已提交** 为每个查询使用单独的快照,而 **快照隔离** 对整个事务使用相同的快照。
[图 7-7](img/fig7-7.png) 说明了 PostgreSQL 如何实现基于 MVCC 的快照隔离【31】其他实现类似。当一个事务开始时它被赋予一个唯一的永远增长 [^vii] 的事务 ID`txid`)。每当事务向数据库写入任何内容时,它所写入的数据都会被标记上写入者的事务 ID。
[图 7-7](/img/fig7-7.png) 说明了 PostgreSQL 如何实现基于 MVCC 的快照隔离【31】其他实现类似。当一个事务开始时它被赋予一个唯一的永远增长 [^vii] 的事务 ID`txid`)。每当事务向数据库写入任何内容时,它所写入的数据都会被标记上写入者的事务 ID。
[^vii]: 事实上,事务 ID 是 32 位整数,所以大约会在 40 亿次事务之后溢出。PostgreSQL 的 Vacuum 过程会清理老旧的事务 ID确保事务 ID 溢出(回卷)不会影响到数据。
![](img/fig7-7.png)
![](/img/fig7-7.png)
**图 7-7 使用多版本对象实现快照隔离**
@@ -335,7 +336,7 @@ Alice 在银行有 1000 美元的储蓄,分为两个账户,每个 500 美元
[^译注ii]: 在 PostgreSQL 中,`created_by` 的实际名称为 `xmin``deleted_by` 的实际名称为 `xmax`
`UPDATE` 操作在内部翻译为 `DELETE``INSERT` 。例如,在 [图 7-7](img/fig7-7.png) 中,事务 13 从账户 2 中扣除 100 美元,将余额从 500 美元改为 400 美元。实际上包含两条账户 2 的记录:余额为 \$500 的行被标记为 **被事务 13 删除**,余额为 \$400 的行 **由事务 13 创建**
`UPDATE` 操作在内部翻译为 `DELETE``INSERT` 。例如,在 [图 7-7](/img/fig7-7.png) 中,事务 13 从账户 2 中扣除 100 美元,将余额从 500 美元改为 400 美元。实际上包含两条账户 2 的记录:余额为 \$500 的行被标记为 **被事务 13 删除**,余额为 \$400 的行 **由事务 13 创建**
#### 观察一致性快照的可见性规则
@@ -346,7 +347,7 @@ Alice 在银行有 1000 美元的储蓄,分为两个账户,每个 500 美元
3. 由具有较晚事务 ID在当前事务开始之后开始的的事务所做的任何写入都被忽略而不管这些事务是否已经提交。
4. 所有其他写入,对应用都是可见的。
这些规则适用于创建和删除对象。在 [图 7-7](img/fig7-7.png) 中,当事务 12 从账户 2 读取时,它会看到 \$500 的余额,因为 \$500 余额的删除是由事务 13 完成的(根据规则 3事务 12 看不到事务 13 执行的删除),且 400 美元记录的创建也是不可见的(按照相同的规则)。
这些规则适用于创建和删除对象。在 [图 7-7](/img/fig7-7.png) 中,当事务 12 从账户 2 读取时,它会看到 \$500 的余额,因为 \$500 余额的删除是由事务 13 完成的(根据规则 3事务 12 看不到事务 13 执行的删除),且 400 美元记录的创建也是不可见的(按照相同的规则)。
换句话说,如果以下两个条件都成立,则可见一个对象:
@@ -361,7 +362,7 @@ Alice 在银行有 1000 美元的储蓄,分为两个账户,每个 500 美元
在实践中许多实现细节决定了多版本并发控制的性能。例如如果同一对象的不同版本可以放入同一个页面中PostgreSQL 的优化可以避免更新索引【31】。
在 CouchDB、Datomic 和 LMDB 中使用另一种方法。虽然它们也使用 [B 树](ch3.md#B树),但它们使用的是一种 **仅追加 / 写时拷贝append-only/copy-on-write** 的变体它们在更新时不覆盖树的页面而为每个修改页面创建一份副本。从父页面直到树根都会级联更新以指向它们子页面的新版本。任何不受写入影响的页面都不需要被复制并且保持不变【33,34,35】。
在 CouchDB、Datomic 和 LMDB 中使用另一种方法。虽然它们也使用 [B 树](/ch3#B树),但它们使用的是一种 **仅追加 / 写时拷贝append-only/copy-on-write** 的变体它们在更新时不覆盖树的页面而为每个修改页面创建一份副本。从父页面直到树根都会级联更新以指向它们子页面的新版本。任何不受写入影响的页面都不需要被复制并且保持不变【33,34,35】。
使用仅追加的 B 树,每个写入事务(或一批事务)都会创建一棵新的 B 树,当创建时,从该特定树根生长的树就是数据库的一个一致性快照。没必要根据事务 ID 过滤掉对象,因为后续写入不能修改现有的 B 树;它们只能创建新的树根。但这种方法也需要一个负责压缩和垃圾收集的后台进程。
@@ -379,7 +380,7 @@ Alice 在银行有 1000 美元的储蓄,分为两个账户,每个 500 美元
到目前为止已经讨论的 **读已提交****快照隔离** 级别,主要保证了 **只读事务在并发写入时** 可以看到什么。却忽略了两个事务并发写入的问题 —— 我们只讨论了脏写(请参阅 “[没有脏写](#没有脏写)”),一种特定类型的写 - 写冲突是可能出现的。
并发的写入事务之间还有其他几种有趣的冲突。其中最著名的是 **丢失更新lost update** 问题,如 [图 7-1](img/fig7-1.png) 所示,以两个并发计数器增量为例。
并发的写入事务之间还有其他几种有趣的冲突。其中最著名的是 **丢失更新lost update** 问题,如 [图 7-1](/img/fig7-1.png) 所示,以两个并发计数器增量为例。
如果应用从数据库中读取一些值,修改它并写回修改的值(读取 - 修改 - 写入序列),则可能会发生丢失更新的问题。如果两个事务同时执行,则其中一个的修改可能会丢失,因为第二个写入的内容并没有包括第一个事务的修改(有时会说后面写入 **狠揍clobber** 了前面的写入)这种模式发生在各种不同的情况下:
@@ -399,7 +400,7 @@ UPDATE counters SET value = value + 1 WHERE key = 'foo';
类似地,像 MongoDB 这样的文档数据库提供了对 JSON 文档的一部分进行本地修改的原子操作Redis 提供了修改数据结构(如优先级队列)的原子操作。并不是所有的写操作都可以用原子操作的方式来表达,例如 wiki 页面的更新涉及到任意文本编辑 [^viii],但是在可以使用原子操作的情况下,它们通常是最好的选择。
[^viii]: 将文本文档的编辑表示为原子的变化流是可能的,尽管相当复杂。请参阅 “[自动冲突解决](ch5.md#自动冲突解决)”。
[^viii]: 将文本文档的编辑表示为原子的变化流是可能的,尽管相当复杂。请参阅 “[自动冲突解决](/ch5#自动冲突解决)”。
原子操作通常通过在读取对象时,获取其上的排它锁来实现。以便更新完成之前没有其他事务可以读取它。这种技术有时被称为 **游标稳定性cursor stability**【36,37】。另一个选择是简单地强制所有的原子操作在单一线程上执行。
@@ -452,15 +453,15 @@ UPDATE wiki_pages SET content = '新内容'
#### 冲突解决和复制
在复制数据库中(请参阅 [第五章](ch5.md)),防止丢失的更新需要考虑另一个维度:由于在多个节点上存在数据副本,并且在不同节点上的数据可能被并发地修改,因此需要采取一些额外的步骤来防止丢失更新。
在复制数据库中(请参阅 [第五章](/ch5)),防止丢失的更新需要考虑另一个维度:由于在多个节点上存在数据副本,并且在不同节点上的数据可能被并发地修改,因此需要采取一些额外的步骤来防止丢失更新。
锁和 CAS 操作假定只有一个最新的数据副本。但是多主或无主复制的数据库通常允许多个写入并发执行,并异步复制到副本上,因此无法保证只有一个最新数据的副本。所以基于锁或 CAS 操作的技术不适用于这种情况(我们将在 “[线性一致性](ch9.md#线性一致性)” 中更详细地讨论这个问题)。
锁和 CAS 操作假定只有一个最新的数据副本。但是多主或无主复制的数据库通常允许多个写入并发执行,并异步复制到副本上,因此无法保证只有一个最新数据的副本。所以基于锁或 CAS 操作的技术不适用于这种情况(我们将在 “[线性一致性](/ch9#线性一致性)” 中更详细地讨论这个问题)。
相反,如 “[检测并发写入](ch5.md#检测并发写入)” 一节所述,这种复制数据库中的一种常见方法是允许并发写入创建多个冲突版本的值(也称为兄弟),并使用应用代码或特殊数据结构在事实发生之后解决和合并这些版本。
相反,如 “[检测并发写入](/ch5#检测并发写入)” 一节所述,这种复制数据库中的一种常见方法是允许并发写入创建多个冲突版本的值(也称为兄弟),并使用应用代码或特殊数据结构在事实发生之后解决和合并这些版本。
原子操作可以在复制的上下文中很好地工作,尤其当它们具有可交换性时(即,可以在不同的副本上以不同的顺序应用它们,且仍然可以得到相同的结果)。例如,递增计数器或向集合添加元素是可交换的操作。这是 Riak 2.0 数据类型背后的思想它可以防止复制副本丢失更新。当不同的客户端同时更新一个值时Riak 自动将更新合并在一起以免丢失更新【39】。
另一方面最后写入胜利LWW的冲突解决方法很容易丢失更新如 “[最后写入胜利(丢弃并发写入)](ch5.md#最后写入胜利(丢弃并发写入))” 中所述。不幸的是LWW 是许多复制数据库中的默认方案。
另一方面最后写入胜利LWW的冲突解决方法很容易丢失更新如 “[最后写入胜利(丢弃并发写入)](/ch5#最后写入胜利(丢弃并发写入))” 中所述。不幸的是LWW 是许多复制数据库中的默认方案。
### 写入偏差与幻读
@@ -470,9 +471,9 @@ UPDATE wiki_pages SET content = '新内容'
首先想象一下这个例子你正在为医院写一个医生轮班管理程序。医院通常会同时要求几位医生待命但底线是至少有一位医生在待命。医生可以放弃他们的班次例如如果他们自己生病了只要至少有一个同事在这一班中继续工作【40,41】。
现在想象一下Alice 和 Bob 是两位值班医生。两人都感到不适,所以他们都决定请假。不幸的是,他们恰好在同一时间点击按钮下班。[图 7-8](img/fig7-8.png) 说明了接下来的事情。
现在想象一下Alice 和 Bob 是两位值班医生。两人都感到不适,所以他们都决定请假。不幸的是,他们恰好在同一时间点击按钮下班。[图 7-8](/img/fig7-8.png) 说明了接下来的事情。
![](img/fig7-8.png)
![](/img/fig7-8.png)
**图 7-8 写入偏差导致应用程序错误的示例**
@@ -595,7 +596,7 @@ COMMIT;
- **两阶段锁定2PL, two-phase locking**,几十年来唯一可行的选择(请参阅 “[两阶段锁定](#两阶段锁定)”)
- 乐观并发控制技术,例如 **可串行化快照隔离**serializable snapshot isolation请参阅 “[可串行化快照隔离](#可串行化快照隔离)”)
现在将主要在单节点数据库的背景下讨论这些技术;在 [第九章](ch9.md) 中,我们将研究如何将它们推广到涉及分布式系统中多个节点的事务。
现在将主要在单节点数据库的背景下讨论这些技术;在 [第九章](/ch9) 中,我们将研究如何将它们推广到涉及分布式系统中多个节点的事务。
### 真的串行执行
@@ -605,8 +606,8 @@ COMMIT;
两个进展引发了这个反思:
- RAM 足够便宜了,许多场景现在都可以将完整的活跃数据集保存在内存中(请参阅 “[在内存中存储一切](ch3.md#在内存中存储一切)”)。当事务需要访问的所有数据都在内存中时,事务处理的执行速度要比等待数据从磁盘加载时快得多。
- 数据库设计人员意识到 OLTP 事务通常很短,而且只进行少量的读写操作(请参阅 “[事务处理还是分析?](ch3.md#事务处理还是分析?)”)。相比之下,长时间运行的分析查询通常是只读的,因此它们可以在串行执行循环之外的一致快照(使用快照隔离)上运行。
- RAM 足够便宜了,许多场景现在都可以将完整的活跃数据集保存在内存中(请参阅 “[在内存中存储一切](/ch3#在内存中存储一切)”)。当事务需要访问的所有数据都在内存中时,事务处理的执行速度要比等待数据从磁盘加载时快得多。
- 数据库设计人员意识到 OLTP 事务通常很短,而且只进行少量的读写操作(请参阅 “[事务处理还是分析?](/ch3#事务处理还是分析?)”)。相比之下,长时间运行的分析查询通常是只读的,因此它们可以在串行执行循环之外的一致快照(使用快照隔离)上运行。
串行执行事务的方法在 VoltDB/H-Store、Redis 和 Datomic 中实现【46,47,48】。设计用于单线程执行的系统有时可以比支持并发的系统性能更好因为它可以避免锁的协调开销。但是其吞吐量仅限于单个 CPU 核的吞吐量。为了充分利用单一线程,需要有与传统形式的事务不同的结构。
@@ -620,9 +621,9 @@ COMMIT;
在这种交互式的事务方式中,应用程序和数据库之间的网络通信耗费了大量的时间。如果不允许在数据库中进行并发处理,且一次只处理一个事务,则吞吐量将会非常糟糕,因为数据库大部分的时间都花费在等待应用程序发出当前事务的下一个查询。在这种数据库中,为了获得合理的性能,需要同时处理多个事务。
出于这个原因,具有单线程串行事务处理的系统不允许交互式的多语句事务。取而代之,应用程序必须提前将整个事务代码作为存储过程提交给数据库。这些方法之间的差异如 [图 7-9](img/fig7-9.png) 所示。如果事务所需的所有数据都在内存中,则存储过程可以非常快地执行,而不用等待任何网络或磁盘 I/O。
出于这个原因,具有单线程串行事务处理的系统不允许交互式的多语句事务。取而代之,应用程序必须提前将整个事务代码作为存储过程提交给数据库。这些方法之间的差异如 [图 7-9](/img/fig7-9.png) 所示。如果事务所需的所有数据都在内存中,则存储过程可以非常快地执行,而不用等待任何网络或磁盘 I/O。
![](img/fig7-9.png)
![](/img/fig7-9.png)
**图 7-9 交互式事务和存储过程之间的区别(使用图 7-8 的示例事务)**
@@ -644,13 +645,13 @@ VoltDB 还使用存储过程进行复制:但不是将事务的写入结果从
顺序执行所有事务使并发控制简单多了,但数据库的事务吞吐量被限制为单机单核的速度。只读事务可以使用快照隔离在其它地方执行,但对于写入吞吐量较高的应用,单线程事务处理器可能成为一个严重的瓶颈。
为了伸缩至多个 CPU 核心和多个节点,可以对数据进行分区(请参阅 [第六章](ch6.md)),在 VoltDB 中支持这样做。如果你可以找到一种对数据集进行分区的方法,以便每个事务只需要在单个分区中读写数据,那么每个分区就可以拥有自己独立运行的事务处理线程。在这种情况下可以为每个分区指派一个独立的 CPU 核,事务吞吐量就可以与 CPU 核数保持线性伸缩【47】。
为了伸缩至多个 CPU 核心和多个节点,可以对数据进行分区(请参阅 [第六章](/ch6)),在 VoltDB 中支持这样做。如果你可以找到一种对数据集进行分区的方法,以便每个事务只需要在单个分区中读写数据,那么每个分区就可以拥有自己独立运行的事务处理线程。在这种情况下可以为每个分区指派一个独立的 CPU 核,事务吞吐量就可以与 CPU 核数保持线性伸缩【47】。
但是,对于需要访问多个分区的任何事务,数据库必须在触及的所有分区之间协调事务。存储过程需要跨越所有分区锁定执行,以确保整个系统的可串行性。
由于跨分区事务具有额外的协调开销所以它们比单分区事务慢得多。VoltDB 报告的吞吐量大约是每秒 1000 个跨分区写入比单分区吞吐量低几个数量级并且不能通过增加更多的机器来增加吞吐量【49】。
事务是否可以是划分至单个分区很大程度上取决于应用数据的结构。简单的键值数据通常可以非常容易地进行分区,但是具有多个次级索引的数据可能需要大量的跨分区协调(请参阅 “[分区与次级索引](ch6.md#分区与次级索引)”)。
事务是否可以是划分至单个分区很大程度上取决于应用数据的结构。简单的键值数据通常可以非常容易地进行分区,但是具有多个次级索引的数据可能需要大量的跨分区协调(请参阅 “[分区与次级索引](/ch6#分区与次级索引)”)。
#### 串行执行小结
@@ -661,7 +662,7 @@ VoltDB 还使用存储过程进行复制:但不是将事务的写入结果从
- 写入吞吐量必须低到能在单个 CPU 核上处理,如若不然,事务需要能划分至单个分区,且不需要跨分区协调。
- 跨分区事务是可能的,但是它们能被使用的程度有很大的限制。
[^x]: 如果事务需要访问不在内存中的数据,最好的解决方案可能是中止事务,异步地将数据提取到内存中,同时继续处理其他事务,然后在数据加载完毕时重新启动事务。这种方法被称为 **反缓存anti-caching**,正如前面在 “[在内存中存储一切](ch3.md#在内存中存储一切)” 中所述。
[^x]: 如果事务需要访问不在内存中的数据,最好的解决方案可能是中止事务,异步地将数据提取到内存中,同时继续处理其他事务,然后在数据加载完毕时重新启动事务。这种方法被称为 **反缓存anti-caching**,正如前面在 “[在内存中存储一切](/ch3#在内存中存储一切)” 中所述。
### 两阶段锁定
@@ -671,14 +672,14 @@ VoltDB 还使用存储过程进行复制:但不是将事务的写入结果从
> #### 2PL不是2PC
>
> 请注意虽然两阶段锁定2PL听起来非常类似于两阶段提交2PC但它们是完全不同的东西。我们将在 [第九章](ch9.md) 讨论 2PC。
> 请注意虽然两阶段锁定2PL听起来非常类似于两阶段提交2PC但它们是完全不同的东西。我们将在 [第九章](/ch9) 讨论 2PC。
之前我们看到锁通常用于防止脏写(请参阅 “[没有脏写](#没有脏写)” 一节):如果两个事务同时尝试写入同一个对象,则锁可确保第二个写入必须等到第一个写入完成事务(中止或提交),然后才能继续。
两阶段锁定类似,但是锁的要求更强得多。只要没有写入,就允许多个事务同时读取同一个对象。但对象只要有写入(修改或删除),就需要 **独占访问exclusive access** 权限:
- 如果事务 A 读取了一个对象,并且事务 B 想要写入该对象,那么 B 必须等到 A 提交或中止才能继续(这确保 B 不能在 A 底下意外地改变对象)。
- 如果事务 A 写入了一个对象,并且事务 B 想要读取该对象,则 B 必须等到 A 提交或中止才能继续(像 [图 7-1](img/fig7-1.png) 那样读取旧版本的对象在 2PL 下是不可接受的)。
- 如果事务 A 写入了一个对象,并且事务 B 想要读取该对象,则 B 必须等到 A 提交或中止才能继续(像 [图 7-1](/img/fig7-1.png) 那样读取旧版本的对象在 2PL 下是不可接受的)。
在 2PL 中,写入不仅会阻塞其他写入,也会阻塞读,反之亦然。快照隔离使得 **读不阻塞写,写也不阻塞读**(请参阅 “[实现快照隔离](#实现快照隔离)”),这是 2PL 和快照隔离之间的关键区别。另一方面,因为 2PL 提供了可串行化的性质,它可以防止早先讨论的所有竞争条件,包括丢失更新和写入偏差。
@@ -703,7 +704,7 @@ VoltDB 还使用存储过程进行复制:但不是将事务的写入结果从
传统的关系数据库不限制事务的持续时间,因为它们是为等待人类输入的交互式应用而设计的。因此,当一个事务需要等待另一个事务时,等待的时长并没有限制。即使你保证所有的事务都很短,如果有多个事务想要访问同一个对象,那么可能会形成一个队列,所以事务可能需要等待几个其他事务才能完成。
因此,运行 2PL 的数据库可能具有相当不稳定的延迟,如果在工作负载中存在争用,那么可能高百分位点处的响应会非常的慢(请参阅 “[描述性能](ch1.md#描述性能)”)。可能只需要一个缓慢的事务,或者一个访问大量数据并获取许多锁的事务,就能把系统的其他部分拖慢,甚至迫使系统停机。当需要稳健的操作时,这种不稳定性是有问题的。
因此,运行 2PL 的数据库可能具有相当不稳定的延迟,如果在工作负载中存在争用,那么可能高百分位点处的响应会非常的慢(请参阅 “[描述性能](/ch1#描述性能)”)。可能只需要一个缓慢的事务,或者一个访问大量数据并获取许多锁的事务,就能把系统的其他部分拖慢,甚至迫使系统停机。当需要稳健的操作时,这种不稳定性是有问题的。
基于锁实现的读已提交隔离级别可能发生死锁,但在基于 2PL 实现的可串行化隔离级别中,它们会出现的频繁的多(取决于事务的访问模式)。这可能是一个额外的性能问题:当事务由于死锁而被中止并被重试时,它需要从头重做它的工作。如果死锁很频繁,这可能意味着巨大的浪费。
@@ -784,9 +785,9 @@ WHERE room_id = 123 AND
#### 检测旧MVCC读取
回想一下快照隔离通常是通过多版本并发控制MVCC见 [图 7-10](img/fig7-10.png))来实现的。当一个事务从 MVCC 数据库中的一致快照读时,它将忽略取快照时尚未提交的任何其他事务所做的写入。在 [图 7-10](img/fig7-10.png) 中,事务 43 认为 Alice 的 `on_call = true` ,因为事务 42修改 Alice 的待命状态)未被提交。然而,在事务 43 想要提交时,事务 42 已经提交。这意味着在读一致性快照时被忽略的写入已经生效,事务 43 的前提不再为真。
回想一下快照隔离通常是通过多版本并发控制MVCC见 [图 7-10](/img/fig7-10.png))来实现的。当一个事务从 MVCC 数据库中的一致快照读时,它将忽略取快照时尚未提交的任何其他事务所做的写入。在 [图 7-10](/img/fig7-10.png) 中,事务 43 认为 Alice 的 `on_call = true` ,因为事务 42修改 Alice 的待命状态)未被提交。然而,在事务 43 想要提交时,事务 42 已经提交。这意味着在读一致性快照时被忽略的写入已经生效,事务 43 的前提不再为真。
![](img/fig7-10.png)
![](/img/fig7-10.png)
**图 7-10 检测事务何时从 MVCC 快照读取过时的值**
@@ -796,19 +797,19 @@ WHERE room_id = 123 AND
#### 检测影响之前读取的写入
第二种情况要考虑的是另一个事务在读取数据之后修改数据。这种情况如 [图 7-11](img/fig7-11.png) 所示。
第二种情况要考虑的是另一个事务在读取数据之后修改数据。这种情况如 [图 7-11](/img/fig7-11.png) 所示。
![](img/fig7-11.png)
![](/img/fig7-11.png)
**图 7-11 在可串行化快照隔离中,检测一个事务何时修改另一个事务的读取。**
在两阶段锁定的上下文中,我们讨论了索引范围锁(请参阅 “[索引范围锁](#索引范围锁)”),它允许数据库锁定与某个搜索查询匹配的所有行的访问权,例如 `WHERE shift_id = 1234`。可以在这里使用类似的技术,除了 SSI 锁不会阻塞其他事务。
在 [图 7-11](img/fig7-11.png) 中,事务 42 和 43 都在班次 1234 查找值班医生。如果在 `shift_id` 上有索引,则数据库可以使用索引项 1234 来记录事务 42 和 43 读取这个数据的事实。(如果没有索引,这个信息可以在表级别进行跟踪)。这个信息只需要保留一段时间:在一个事务完成(提交或中止),并且所有的并发事务完成之后,数据库就可以忘记它读取的数据了。
在 [图 7-11](/img/fig7-11.png) 中,事务 42 和 43 都在班次 1234 查找值班医生。如果在 `shift_id` 上有索引,则数据库可以使用索引项 1234 来记录事务 42 和 43 读取这个数据的事实。(如果没有索引,这个信息可以在表级别进行跟踪)。这个信息只需要保留一段时间:在一个事务完成(提交或中止),并且所有的并发事务完成之后,数据库就可以忘记它读取的数据了。
当事务写入数据库时,它必须在索引中查找最近曾读取受影响数据的其他事务。这个过程类似于在受影响的键范围上获取写锁,但锁并不会阻塞事务直到其他读事务完成,而是像警戒线一样只是简单通知其他事务:你们读过的数据可能不是最新的啦。
在 [图 7-11](img/fig7-11.png) 中,事务 43 通知事务 42 其先前读已过时,反之亦然。事务 42 首先提交并成功,尽管事务 43 的写影响了 42 ,但因为事务 43 尚未提交,所以写入尚未生效。然而当事务 43 想要提交时,来自事务 42 的冲突写入已经被提交,所以事务 43 必须中止。
在 [图 7-11](/img/fig7-11.png) 中,事务 43 通知事务 42 其先前读已过时,反之亦然。事务 42 首先提交并成功,尽管事务 43 的写影响了 42 ,但因为事务 43 尚未提交,所以写入尚未生效。然而当事务 43 想要提交时,来自事务 42 的冲突写入已经被提交,所以事务 43 必须中止。
#### 可串行化快照隔离的性能
@@ -932,10 +933,3 @@ WHERE room_id = 123 AND
1. D. Z. Badal: “[Correctness of Concurrency Control and Implications in Distributed Databases](http://ieeexplore.ieee.org/abstract/document/762563/),” at *3rd International IEEE Computer Software and Applications Conference* (COMPSAC), November 1979.
1. Rakesh Agrawal, Michael J. Carey, and Miron Livny: “[Concurrency Control Performance Modeling: Alternatives and Implications](http://www.eecs.berkeley.edu/~brewer/cs262/ConcControl.pdf),” *ACM Transactions on Database Systems* (TODS), volume 12, number 4, pages 609654, December 1987. [doi:10.1145/32204.32220](http://dx.doi.org/10.1145/32204.32220)
1. Dave Rosenthal: “[Databases at 14.4MHz](http://web.archive.org/web/20150427041746/http://blog.foundationdb.com/databases-at-14.4mhz),” *blog.foundationdb.com*, December 10, 2014.
------
| 上一章 | 目录 | 下一章 |
| ---------------------- | ------------------------------- | ---------------------------------- |
| [第六章:分区](ch6.md) | [设计数据密集型应用](README.md) | [第八章:分布式系统的麻烦](ch8.md) |

View File

@@ -1,6 +1,12 @@
# 第八章:分布式系统的麻烦
---
title: "第八章:分布式系统的麻烦"
linkTitle: "8. 分布式系统的麻烦"
weight: 208
breadcrumbs: false
---
![](img/ch8.png)
![](/img/ch8.png)
> 邂逅相遇
>
@@ -12,11 +18,8 @@
>
> —— Kyle Kingsbury, Carly Rae Jepsen 《网络分区的危害》2013 年)[^译著1]
---------
[TOC]
最近几章中反复出现的主题是,系统如何处理错误的事情。例如,我们讨论了 **副本故障切换**(“[处理节点中断](ch5.md#处理节点宕机)”),**复制延迟**(“[复制延迟问题](ch5.md#复制延迟问题)”)和事务控制(“[弱隔离级别](ch7.md#弱隔离级别)”)。当我们了解可能在实际系统中出现的各种边缘情况时,我们会更好地处理它们。
最近几章中反复出现的主题是,系统如何处理错误的事情。例如,我们讨论了 **副本故障切换**(“[处理节点中断](/ch5#处理节点宕机)”),**复制延迟**(“[复制延迟问题](/ch5#复制延迟问题)”)和事务控制(“[弱隔离级别](/ch7#弱隔离级别)”)。当我们了解可能在实际系统中出现的各种边缘情况时,我们会更好地处理它们。
但是,尽管我们已经谈了很多错误,但之前几章仍然过于乐观。现实更加黑暗。我们现在将悲观主义最大化,假设任何可能出错的东西 **都会** 出错 [^i]。(经验丰富的系统运维会告诉你,这是一个合理的假设。如果你问得好,他们可能会一边治疗心理创伤一边告诉你一些可怕的故事)
@@ -24,7 +27,7 @@
使用分布式系统与在一台计算机上编写软件有着根本的区别主要的区别在于有许多新颖和刺激的方法可以使事情出错【1,2】。在这一章中我们将了解实践中出现的问题理解我们能够依赖和不可以依赖的东西。
最后,作为工程师,我们的任务是构建能够完成工作的系统(即满足用户期望的保证),尽管一切都出错了。在 [第九章](ch9.md) 中,我们将看看一些可以在分布式系统中提供这种保证的算法的例子。但首先,在本章中,我们必须了解我们面临的挑战。
最后,作为工程师,我们的任务是构建能够完成工作的系统(即满足用户期望的保证),尽管一切都出错了。在 [第九章](/ch9) 中,我们将看看一些可以在分布式系统中提供这种保证的算法的例子。但首先,在本章中,我们必须了解我们面临的挑战。
本章对分布式系统中可能出现的问题进行彻底的悲观和沮丧的总结。我们将研究网络的问题(“[不可靠的网络](#不可靠的网络)”); 时钟和时序问题(“[不可靠的时钟](#不可靠的时钟)”); 我们将讨论他们可以避免的程度。所有这些问题的后果都是困惑的,所以我们将探索如何思考一个分布式系统的状态,以及如何推理发生的事情(“[知识、真相与谎言](#知识、真相与谎言)”)。
@@ -67,11 +70,11 @@
* 系统越大其组件之一就越有可能坏掉。随着时间的推移坏掉的东西得到修复新的东西又坏掉但是在一个有成千上万个节点的系统中有理由认为总是有一些东西是坏掉的【7】。当错误处理的策略只由简单放弃组成时一个大的系统最终会花费大量时间从错误中恢复而不是做有用的工作【8】。
* 如果系统可以容忍发生故障的节点,并继续保持整体工作状态,那么这对于运营和维护非常有用:例如,可以执行滚动升级(请参阅 [第四章](ch4.md)),一次重新启动一个节点,同时继续给用户提供不中断的服务。在云环境中,如果一台虚拟机运行不佳,可以杀死它并请求一台新的虚拟机(希望新的虚拟机速度更快)。
* 如果系统可以容忍发生故障的节点,并继续保持整体工作状态,那么这对于运营和维护非常有用:例如,可以执行滚动升级(请参阅 [第四章](/ch4)),一次重新启动一个节点,同时继续给用户提供不中断的服务。在云环境中,如果一台虚拟机运行不佳,可以杀死它并请求一台新的虚拟机(希望新的虚拟机速度更快)。
* 在地理位置分散的部署中(保持数据在地理位置上接近用户以减少访问延迟),通信很可能通过互联网进行,与本地网络相比,通信速度缓慢且不可靠。超级计算机通常假设它们的所有节点都靠近在一起。
如果要使分布式系统工作,就必须接受部分故障的可能性,并在软件中建立容错机制。换句话说,我们需要从不可靠的组件构建一个可靠的系统(正如 “[可靠性](ch1.md#可靠性)” 中所讨论的那样,没有完美的可靠性,所以我们需要理解我们可以实际承诺的极限)。
如果要使分布式系统工作,就必须接受部分故障的可能性,并在软件中建立容错机制。换句话说,我们需要从不可靠的组件构建一个可靠的系统(正如 “[可靠性](/ch1#可靠性)” 中所讨论的那样,没有完美的可靠性,所以我们需要理解我们可以实际承诺的极限)。
即使在只有少数节点的小型系统中,考虑部分故障也是很重要的。在一个小系统中,很可能大部分组件在大部分时间都正常工作。然而,迟早会有一部分系统出现故障,软件必须以某种方式处理。故障处理必须是软件设计的一部分,并且作为软件的运维,你需要知道在发生故障的情况下,软件可能会表现出怎样的行为。
@@ -86,16 +89,16 @@
>
> 虽然这个系统可以比它的底层部分更可靠但它的可靠性总是有限的。例如纠错码可以处理少量的单比特错误但是如果你的信号被干扰所淹没那么通过信道可以得到多少数据是有根本性的限制的【13】。TCP 可以隐藏数据包的丢失,重复和重新排序,但是它不能神奇地消除网络中的延迟。
>
> 虽然更可靠的高级系统并不完美,但它仍然有用,因为它处理了一些棘手的低级错误,所以其余的错误通常更容易推理和处理。我们将在 “[数据库的端到端原则](ch12.md#数据库的端到端原则)” 中进一步探讨这个问题。
> 虽然更可靠的高级系统并不完美,但它仍然有用,因为它处理了一些棘手的低级错误,所以其余的错误通常更容易推理和处理。我们将在 “[数据库的端到端原则](/ch12#数据库的端到端原则)” 中进一步探讨这个问题。
## 不可靠的网络
正如在 [第二部分](part-ii.md) 的介绍中所讨论的那样,我们在本书中关注的分布式系统是无共享的系统,即通过网络连接的一堆机器。网络是这些机器可以通信的唯一途径 —— 我们假设每台机器都有自己的内存和磁盘,一台机器不能访问另一台机器的内存或磁盘(除了通过网络向服务器发出请求)。
正如在 [第二部分](/part-ii) 的介绍中所讨论的那样,我们在本书中关注的分布式系统是无共享的系统,即通过网络连接的一堆机器。网络是这些机器可以通信的唯一途径 —— 我们假设每台机器都有自己的内存和磁盘,一台机器不能访问另一台机器的内存或磁盘(除了通过网络向服务器发出请求)。
**无共享** 并不是构建系统的唯一方式,但它已经成为构建互联网服务的主要方式,其原因如下:相对便宜,因为它不需要特殊的硬件,可以利用商品化的云计算服务,通过跨多个地理分布的数据中心进行冗余可以实现高可靠性。
互联网和数据中心(通常是以太网)中的大多数内部网络都是 **异步分组网络asynchronous packet networks**。在这种网络中,一个节点可以向另一个节点发送一个消息(一个数据包),但是网络不能保证它什么时候到达,或者是否到达。如果你发送请求并期待响应,则很多事情可能会出错(其中一些如 [图 8-1](img/fig8-1.png) 所示):
互联网和数据中心(通常是以太网)中的大多数内部网络都是 **异步分组网络asynchronous packet networks**。在这种网络中,一个节点可以向另一个节点发送一个消息(一个数据包),但是网络不能保证它什么时候到达,或者是否到达。如果你发送请求并期待响应,则很多事情可能会出错(其中一些如 [图 8-1](/img/fig8-1.png) 所示):
1. 请求可能已经丢失(可能有人拔掉了网线)。
2. 请求可能正在排队,稍后将交付(也许网络或接收方过载)。
@@ -104,7 +107,7 @@
5. 远程节点可能已经处理了请求,但是网络上的响应已经丢失(可能是网络交换机配置错误)。
6. 远程节点可能已经处理了请求,但是响应已经被延迟,并且稍后将被传递(可能是网络或者你自己的机器过载)。
![](img/fig8-1.png)
![](/img/fig8-1.png)
**图 8-1 如果发送请求并没有得到响应则无法区分a请求是否丢失b远程节点是否关闭c响应是否丢失。**
@@ -122,20 +125,20 @@
> #### 网络分区
>
> 当网络的一部分由于网络故障而被切断时,有时称为 **网络分区network partition****网络断裂netsplit**。在本书中,我们通常会坚持使用更一般的术语 **网络故障network fault**,以避免与 [第六章](ch6.md) 讨论的存储系统的分区(分片)相混淆。
> 当网络的一部分由于网络故障而被切断时,有时称为 **网络分区network partition****网络断裂netsplit**。在本书中,我们通常会坚持使用更一般的术语 **网络故障network fault**,以避免与 [第六章](/ch6) 讨论的存储系统的分区(分片)相混淆。
即使网络故障在你的环境中非常罕见,故障可能发生的事实,意味着你的软件需要能够处理它们。无论何时通过网络进行通信,都可能会失败,这是无法避免的。
如果网络故障的错误处理没有定义与测试武断地讲各种错误可能都会发生例如即使网络恢复【20】集群可能会发生 **死锁**永久无法为请求提供服务甚至可能会删除所有的数据【21】。如果软件被置于意料之外的情况下它可能会做出出乎意料的事情。
处理网络故障并不意味着容忍它们:如果你的网络通常是相当可靠的,一个有效的方法可能是当你的网络遇到问题时,简单地向用户显示一条错误信息。但是,你确实需要知道你的软件如何应对网络问题,并确保系统能够从中恢复。有意识地触发网络问题并测试系统响应(这是 Chaos Monkey 背后的想法;请参阅 “[可靠性](ch1.md#可靠性)”)。
处理网络故障并不意味着容忍它们:如果你的网络通常是相当可靠的,一个有效的方法可能是当你的网络遇到问题时,简单地向用户显示一条错误信息。但是,你确实需要知道你的软件如何应对网络问题,并确保系统能够从中恢复。有意识地触发网络问题并测试系统响应(这是 Chaos Monkey 背后的想法;请参阅 “[可靠性](/ch1#可靠性)”)。
### 检测故障
许多系统需要自动检测故障节点。例如:
* 负载平衡器需要停止向已死亡的节点转发请求(从轮询列表移出,即 out of rotation
* 在单主复制功能的分布式数据库中,如果主库失效,则需要将从库之一升级为新主库(请参阅 “[处理节点宕机](ch5.md#处理节点宕机)”)。
* 在单主复制功能的分布式数据库中,如果主库失效,则需要将从库之一升级为新主库(请参阅 “[处理节点宕机](/ch5#处理节点宕机)”)。
不幸的是,网络的不确定性使得很难判断一个节点是否工作。在某些特定的情况下,你可能会收到一些反馈信息,明确告诉你某些事情没有成功:
@@ -154,7 +157,7 @@
长时间的超时意味着长时间等待,直到一个节点被宣告死亡(在这段时间内,用户可能不得不等待,或者看到错误信息)。短的超时可以更快地检测到故障,但有更高地风险误将一个节点宣布为失效,而该节点实际上只是暂时地变慢了(例如由于节点或网络上的负载峰值)。
过早地声明一个节点已经死了是有问题的:如果这个节点实际上是活着的,并且正在执行一些动作(例如,发送一封电子邮件),而另一个节点接管,那么这个动作可能会最终执行两次。我们将在 “[知识、真相与谎言](#知识、真相与谎言)” 以及 [第九章](ch9.md) 和 [第十一章](ch11.md) 中更详细地讨论这个问题。
过早地声明一个节点已经死了是有问题的:如果这个节点实际上是活着的,并且正在执行一些动作(例如,发送一封电子邮件),而另一个节点接管,那么这个动作可能会最终执行两次。我们将在 “[知识、真相与谎言](#知识、真相与谎言)” 以及 [第九章](/ch9) 和 [第十一章](/ch11) 中更详细地讨论这个问题。
当一个节点被宣告死亡时,它的职责需要转移到其他节点,这会给其他节点和网络带来额外的负担。如果系统已经处于高负荷状态,则过早宣告节点死亡会使问题更严重。特别是如果节点实际上没有死亡,只是由于过载导致其响应缓慢;这时将其负载转移到其他节点可能会导致 **级联失效**(即 cascading failure表示在极端情况下所有节点都宣告对方死亡所有节点都将停止工作
@@ -166,12 +169,12 @@
在驾驶汽车时由于交通拥堵道路交通网络的通行时间往往不尽相同。同样计算机网络上数据包延迟的可变性通常是由于排队【25】
* 如果多个不同的节点同时尝试将数据包发送到同一目的地,则网络交换机必须将它们排队并将它们逐个送入目标网络链路(如 [图 8-2](img/fig8-2.png) 所示)。在繁忙的网络链路上,数据包可能需要等待一段时间才能获得一个插槽(这称为网络拥塞)。如果传入的数据太多,交换机队列填满,数据包将被丢弃,因此需要重新发送数据包 - 即使网络运行良好。
* 如果多个不同的节点同时尝试将数据包发送到同一目的地,则网络交换机必须将它们排队并将它们逐个送入目标网络链路(如 [图 8-2](/img/fig8-2.png) 所示)。在繁忙的网络链路上,数据包可能需要等待一段时间才能获得一个插槽(这称为网络拥塞)。如果传入的数据太多,交换机队列填满,数据包将被丢弃,因此需要重新发送数据包 - 即使网络运行良好。
* 当数据包到达目标机器时,如果所有 CPU 内核当前都处于繁忙状态,则来自网络的传入请求将被操作系统排队,直到应用程序准备好处理它为止。根据机器上的负载,这可能需要一段任意的时间。
* 在虚拟化环境中,正在运行的操作系统经常暂停几十毫秒,因为另一个虚拟机正在使用 CPU 内核。在这段时间内,虚拟机不能从网络中消耗任何数据,所以传入的数据被虚拟机监视器 【26】排队缓冲进一步增加了网络延迟的可变性。
* TCP 执行 **流量控制**flow control也称为 **拥塞避免**,即 congestion avoidance**背压**,即 backpressure其中节点会限制自己的发送速率以避免网络链路或接收节点过载【27】。这意味着甚至在数据进入网络之前在发送者处就需要进行额外的排队。
![](img/fig8-2.png)
![](/img/fig8-2.png)
**图 8-2 如果有多台机器将网络流量发送到同一目的地,则其交换机队列可能会被填满。在这里,端口 1,2 和 4 都试图发送数据包到端口 3**
@@ -186,7 +189,7 @@
所有这些因素都会造成网络延迟的变化。当系统接近其最大容量时,排队延迟的变化范围特别大:拥有足够备用容量的系统可以轻松排空队列,而在高利用率的系统中,很快就能积累很长的队列。
在公共云和多租户数据中心中,资源被许多客户共享:网络链接和交换机,甚至每个机器的网卡和 CPU在虚拟机上运行时。批处理工作负载如 MapReduce请参阅 [第十章](ch10.md)能够很容易使网络链接饱和。由于无法控制或了解其他客户对共享资源的使用情况如果附近的某个人嘈杂的邻居正在使用大量资源则网络延迟可能会发生剧烈变化【28,29】。
在公共云和多租户数据中心中,资源被许多客户共享:网络链接和交换机,甚至每个机器的网卡和 CPU在虚拟机上运行时。批处理工作负载如 MapReduce请参阅 [第十章](/ch10)能够很容易使网络链接饱和。由于无法控制或了解其他客户对共享资源的使用情况如果附近的某个人嘈杂的邻居正在使用大量资源则网络延迟可能会发生剧烈变化【28,29】。
在这种环境下,你只能通过实验方式选择超时:在一段较长的时期内、在多台机器上测量网络往返时间的分布,以确定延迟的预期变化。然后,考虑到应用程序的特性,可以确定 **故障检测延迟****过早超时风险** 之间的适当折衷。
@@ -250,7 +253,7 @@
7. 这个缓存条目何时到期?
8. 日志文件中此错误消息的时间戳是什么?
[例 1-4](ch1.md) 测量了 **持续时间**durations例如请求发送与响应接收之间的时间间隔而 [例 5-8](ch5.md) 描述了 **时间点**point in time在特定日期和和特定时间发生的事件
[例 1-4](/ch1) 测量了 **持续时间**durations例如请求发送与响应接收之间的时间间隔而 [例 5-8](/ch5) 描述了 **时间点**point in time在特定日期和和特定时间发生的事件
在分布式系统中,时间是一件棘手的事情,因为通信不是即时的:消息通过网络从一台机器传送到另一台机器需要时间。收到消息的时间总是晚于发送的时间,但是由于网络中的可变延迟,我们不知道晚了多少时间。这个事实导致有时很难确定在涉及多台机器时发生事情的顺序。
@@ -313,27 +316,27 @@
让我们考虑一个特别的情况,一件很有诱惑但也很危险的事情:依赖时钟,在多个节点上对事件进行排序。例如,如果两个客户端写入分布式数据库,谁先到达? 哪一个更近?
[图 8-3](img/fig8-3.png) 显示了在具有多主复制的数据库中对时钟的危险使用(该例子类似于 [图 5-9](img/fig5-9.png))。客户端 A 在节点 1 上写入 `x = 1`;写入被复制到节点 3客户端 B 在节点 3 上增加 x我们现在有 `x = 2`);最后这两个写入都被复制到节点 2。
[图 8-3](/img/fig8-3.png) 显示了在具有多主复制的数据库中对时钟的危险使用(该例子类似于 [图 5-9](/img/fig5-9.png))。客户端 A 在节点 1 上写入 `x = 1`;写入被复制到节点 3客户端 B 在节点 3 上增加 x我们现在有 `x = 2`);最后这两个写入都被复制到节点 2。
![](img/fig8-3.png)
![](/img/fig8-3.png)
**图 8-3 客户端 B 的写入比客户端 A 的写入要晚,但是 B 的写入具有较早的时间戳。**
在 [图 8-3](img/fig8-3.png) 中,当一个写入被复制到其他节点时,它会根据发生写入的节点上的日历时钟标记一个时间戳。在这个例子中,时钟同步是非常好的:节点 1 和节点 3 之间的偏差小于 3ms这可能比你在实践中能预期的更好。
在 [图 8-3](/img/fig8-3.png) 中,当一个写入被复制到其他节点时,它会根据发生写入的节点上的日历时钟标记一个时间戳。在这个例子中,时钟同步是非常好的:节点 1 和节点 3 之间的偏差小于 3ms这可能比你在实践中能预期的更好。
尽管如此,[图 8-3](img/fig8-3.png) 中的时间戳却无法正确排列事件:写入 `x = 1` 的时间戳为 42.004 秒,但写入 `x = 2` 的时间戳为 42.003 秒,即使 `x = 2` 在稍后出现。当节点 2 接收到这两个事件时,会错误地推断出 `x = 1` 是最近的值,而丢弃写入 `x = 2`。效果上表现为,客户端 B 的增量操作会丢失。
尽管如此,[图 8-3](/img/fig8-3.png) 中的时间戳却无法正确排列事件:写入 `x = 1` 的时间戳为 42.004 秒,但写入 `x = 2` 的时间戳为 42.003 秒,即使 `x = 2` 在稍后出现。当节点 2 接收到这两个事件时,会错误地推断出 `x = 1` 是最近的值,而丢弃写入 `x = 2`。效果上表现为,客户端 B 的增量操作会丢失。
这种冲突解决策略被称为 **最后写入胜利LWW**,它在多主复制和无主数据库(如 Cassandra 【53】和 Riak 【54】中被广泛使用请参阅 “[最后写入胜利(丢弃并发写入)](ch5.md#最后写入胜利(丢弃并发写入))” 一节)。有些实现会在客户端而不是服务器上生成时间戳,但这并不能改变 LWW 的基本问题:
这种冲突解决策略被称为 **最后写入胜利LWW**,它在多主复制和无主数据库(如 Cassandra 【53】和 Riak 【54】中被广泛使用请参阅 “[最后写入胜利(丢弃并发写入)](/ch5#最后写入胜利(丢弃并发写入))” 一节)。有些实现会在客户端而不是服务器上生成时间戳,但这并不能改变 LWW 的基本问题:
* 数据库写入可能会神秘地消失具有滞后时钟的节点无法覆盖之前具有快速时钟的节点写入的值直到节点之间的时钟偏差消逝【54,55】。此方案可能导致一定数量的数据被悄悄丢弃而未向应用报告任何错误。
* LWW 无法区分 **高频顺序写入**(在 [图 8-3](img/fig8-3.png) 中,客户端 B 的增量操作 **一定** 发生在客户端 A 的写入之后)和 **真正并发写入**(写入者意识不到其他写入者)。需要额外的因果关系跟踪机制(例如版本向量),以防止违背因果关系(请参阅 “[检测并发写入](ch5.md#检测并发写入)”)。
* LWW 无法区分 **高频顺序写入**(在 [图 8-3](/img/fig8-3.png) 中,客户端 B 的增量操作 **一定** 发生在客户端 A 的写入之后)和 **真正并发写入**(写入者意识不到其他写入者)。需要额外的因果关系跟踪机制(例如版本向量),以防止违背因果关系(请参阅 “[检测并发写入](/ch5#检测并发写入)”)。
* 两个节点很可能独立地生成具有相同时间戳的写入,特别是在时钟仅具有毫秒分辨率的情况下。为了解决这样的冲突,还需要一个额外的 **决胜值**tiebreaker可以简单地是一个大随机数但这种方法也可能会导致违背因果关系【53】。
因此,尽管通过保留 “最近” 的值并放弃其他值来解决冲突是很诱惑人的,但是要注意,“最近” 的定义取决于本地的 **日历时钟**,这很可能是不正确的。即使用严格同步的 NTP 时钟,一个数据包也可能在时间戳 100 毫秒(根据发送者的时钟)时发送,并在时间戳 99 毫秒(根据接收者的时钟)处到达 —— 看起来好像数据包在发送之前已经到达,这是不可能的。
NTP 同步是否能足够准确,以至于这种不正确的排序不会发生?也许不能,因为 NTP 的同步精度本身,除了石英钟漂移这类误差源之外,还受到网络往返时间的限制。为了进行正确的排序,你需要一个比测量对象(即网络延迟)要精确得多的时钟。
所谓的 **逻辑时钟logic clock**【56,57】是基于递增计数器而不是振荡石英晶体对于排序事件来说是更安全的选择请参阅 “[检测并发写入](ch5.md#检测并发写入)”)。逻辑时钟不测量一天中的时间或经过的秒数,而仅测量事件的相对顺序(无论一个事件发生在另一个事件之前还是之后)。相反,用来测量实际经过时间的 **日历时钟****单调钟** 也被称为 **物理时钟physical clock**。我们将在 “[顺序保证](ch9.md#顺序保证)” 中来看顺序问题。
所谓的 **逻辑时钟logic clock**【56,57】是基于递增计数器而不是振荡石英晶体对于排序事件来说是更安全的选择请参阅 “[检测并发写入](/ch5#检测并发写入)”)。逻辑时钟不测量一天中的时间或经过的秒数,而仅测量事件的相对顺序(无论一个事件发生在另一个事件之前还是之后)。相反,用来测量实际经过时间的 **日历时钟****单调钟** 也被称为 **物理时钟physical clock**。我们将在 “[顺序保证](/ch9#顺序保证)” 中来看顺序问题。
#### 时钟读数存在置信区间
@@ -349,13 +352,13 @@ NTP 同步是否能足够准确,以至于这种不正确的排序不会发生
#### 全局快照的同步时钟
在 “[快照隔离和可重复读](ch7.md#快照隔离和可重复读)” 中,我们讨论了快照隔离,这是数据库中非常有用的功能,需要支持小型快速读写事务和大型长时间运行的只读事务(用于备份或分析)。它允许只读事务看到特定时间点的处于一致状态的数据库,且不会锁定和干扰读写事务。
在 “[快照隔离和可重复读](/ch7#快照隔离和可重复读)” 中,我们讨论了快照隔离,这是数据库中非常有用的功能,需要支持小型快速读写事务和大型长时间运行的只读事务(用于备份或分析)。它允许只读事务看到特定时间点的处于一致状态的数据库,且不会锁定和干扰读写事务。
快照隔离最常见的实现需要单调递增的事务 ID。如果写入比快照晚写入具有比快照更大的事务 ID则该写入对于快照事务是不可见的。在单节点数据库上一个简单的计数器就足以生成事务 ID。
但是当数据库分布在许多机器上,也许可能在多个数据中心中时,由于需要协调,(跨所有分区)全局单调递增的事务 ID 会很难生成。事务 ID 必须反映因果关系:如果事务 B 读取由事务 A 写入的值,则 B 必须具有比 A 更大的事务 ID否则快照就无法保持一致。在有大量的小规模、高频率的事务情景下在分布式系统中创建事务 ID 成为一个难以处理的瓶颈 [^vi]。
[^vi]: 存在分布式序列号生成器,例如 Twitter 的雪花Snowflake其以可伸缩的方式例如通过将 ID 空间的块分配给不同节点)近似单调地增加唯一 ID。但是它们通常无法保证与因果关系一致的排序因为分配的 ID 块的时间范围比数据库读取和写入的时间范围要长。另请参阅 “[顺序保证](ch9.md#顺序保证)”。
[^vi]: 存在分布式序列号生成器,例如 Twitter 的雪花Snowflake其以可伸缩的方式例如通过将 ID 空间的块分配给不同节点)近似单调地增加唯一 ID。但是它们通常无法保证与因果关系一致的排序因为分配的 ID 块的时间范围比数据库读取和写入的时间范围要长。另请参阅 “[顺序保证](/ch9#顺序保证)”。
我们可以使用同步时钟的时间戳作为事务 ID 吗?如果我们能够获得足够好的同步性,那么这种方法将具有很合适的属性:更晚的事务会有更大的时间戳。当然,问题在于时钟精度的不确定性。
@@ -419,7 +422,7 @@ while (true) {
> #### 实时是真的吗?
>
> 在嵌入式系统中,实时是指系统经过精心设计和测试,以满足所有情况下的特定时间保证。这个含义与 Web 上对实时术语的模糊使用相反,后者描述了服务器将数据推送到客户端以及没有严格的响应时间限制的流处理(见 [第十一章](ch11.md))。
> 在嵌入式系统中,实时是指系统经过精心设计和测试,以满足所有情况下的特定时间保证。这个含义与 Web 上对实时术语的模糊使用相反,后者描述了服务器将数据推送到客户端以及没有严格的响应时间限制的流处理(见 [第十一章](/ch11))。
例如,如果车载传感器检测到当前正在经历碰撞,你肯定不希望安全气囊释放系统因为 GC 暂停而延迟弹出。
@@ -435,7 +438,7 @@ while (true) {
一个新兴的想法是将 GC 暂停视为一个节点的短暂计划中断,并在这个节点收集其垃圾的同时,让其他节点处理来自客户端的请求。如果运行时可以警告应用程序一个节点很快需要 GC 暂停,那么应用程序可以停止向该节点发送新的请求,等待它完成处理未完成的请求,然后在没有请求正在进行时执行 GC。这个技巧向客户端隐藏了 GC 暂停并降低了响应时间的高百分比【70,71】。一些对延迟敏感的金融交易系统【72】使用这种方法。
这个想法的一个变种是只用垃圾收集器来处理短命对象(这些对象可以快速收集),并定期在积累大量长寿对象(因此需要完整 GC之前重新启动进程【65,73】。一次可以重新启动一个节点在计划重新启动之前流量可以从该节点移开就像 [第四章](ch4.md) 里描述的滚动升级一样。
这个想法的一个变种是只用垃圾收集器来处理短命对象(这些对象可以快速收集),并定期在积累大量长寿对象(因此需要完整 GC之前重新启动进程【65,73】。一次可以重新启动一个节点在计划重新启动之前流量可以从该节点移开就像 [第四章](/ch4) 里描述的滚动升级一样。
这些措施不能完全阻止垃圾回收暂停,但可以有效地减少它们对应用的影响。
@@ -450,7 +453,7 @@ while (true) {
幸运的是,我们不需要去搞清楚生命的意义。在分布式系统中,我们可以陈述关于行为(系统模型)的假设,并以满足这些假设的方式设计实际系统。算法可以被证明在某个系统模型中正确运行。这意味着即使底层系统模型提供了很少的保证,也可以实现可靠的行为。
但是,尽管可以使软件在不可靠的系统模型中表现良好,但这并不是可以直截了当实现的。在本章的其余部分中,我们将进一步探讨分布式系统中的知识和真相的概念,这将有助于我们思考我们可以做出的各种假设以及我们可能希望提供的保证。在 [第九章](ch9.md) 中,我们将着眼于分布式系统的一些例子,这些算法在特定的假设条件下提供了特定的保证。
但是,尽管可以使软件在不可靠的系统模型中表现良好,但这并不是可以直截了当实现的。在本章的其余部分中,我们将进一步探讨分布式系统中的知识和真相的概念,这将有助于我们思考我们可以做出的各种假设以及我们可能希望提供的保证。在 [第九章](/ch9) 中,我们将着眼于分布式系统的一些例子,这些算法在特定的假设条件下提供了特定的保证。
### 真相由多数所定义
@@ -460,17 +463,17 @@ while (true) {
第三种情况,想象一个正在经历长时间 **垃圾收集暂停stop-the-world GC Pause** 的节点,节点的所有线程被 GC 抢占并暂停一分钟因此没有请求被处理也没有响应被发送。其他节点等待重试不耐烦并最终宣布节点死亡并将其丢到灵车上。最后GC 完成节点的线程继续好像什么也没有发生。其他节点感到惊讶因为所谓的死亡节点突然从棺材中抬起头来身体健康开始和旁观者高兴地聊天。GC 后的节点最初甚至没有意识到已经经过了整整一分钟,而且自己已被宣告死亡。从它自己的角度来看,从最后一次与其他节点交谈以来,几乎没有经过任何时间。
这些故事的寓意是,节点不一定能相信自己对于情况的判断。分布式系统不能完全依赖单个节点,因为节点可能随时失效,可能会使系统卡死,无法恢复。相反,许多分布式算法都依赖于法定人数,即在节点之间进行投票(请参阅 “[读写的法定人数](ch5.md#读写的法定人数)”):决策需要来自多个节点的最小投票数,以减少对于某个特定节点的依赖。
这些故事的寓意是,节点不一定能相信自己对于情况的判断。分布式系统不能完全依赖单个节点,因为节点可能随时失效,可能会使系统卡死,无法恢复。相反,许多分布式算法都依赖于法定人数,即在节点之间进行投票(请参阅 “[读写的法定人数](/ch5#读写的法定人数)”):决策需要来自多个节点的最小投票数,以减少对于某个特定节点的依赖。
这也包括关于宣告节点死亡的决定。如果法定数量的节点宣告另一个节点已经死亡,那么即使该节点仍感觉自己活着,它也必须被认为是死的。个体节点必须遵守法定决定并下台。
最常见的法定人数是超过一半的绝对多数(尽管其他类型的法定人数也是可能的)。多数法定人数允许系统继续工作,如果单个节点发生故障(三个节点可以容忍单节点故障;五个节点可以容忍双节点故障)。系统仍然是安全的,因为在这个制度中只能有一个多数 —— 不能同时存在两个相互冲突的多数决定。当我们在 [第九章](ch9.md) 中讨论 **共识算法consensus algorithms** 时,我们将更详细地讨论法定人数的应用。
最常见的法定人数是超过一半的绝对多数(尽管其他类型的法定人数也是可能的)。多数法定人数允许系统继续工作,如果单个节点发生故障(三个节点可以容忍单节点故障;五个节点可以容忍双节点故障)。系统仍然是安全的,因为在这个制度中只能有一个多数 —— 不能同时存在两个相互冲突的多数决定。当我们在 [第九章](/ch9) 中讨论 **共识算法consensus algorithms** 时,我们将更详细地讨论法定人数的应用。
#### 领导者和锁
通常情况下,一些东西在一个系统中只能有一个。例如:
* 数据库分区的领导者只能有一个节点,以避免 **脑裂**(即 split brain请参阅 “[处理节点宕机](ch5.md#处理节点宕机)”)。
* 数据库分区的领导者只能有一个节点,以避免 **脑裂**(即 split brain请参阅 “[处理节点宕机](/ch5#处理节点宕机)”)。
* 特定资源的锁或对象只允许一个事务 / 客户端持有,以防同时写入和损坏。
* 一个特定的用户名只能被一个用户所注册,因为用户名必须唯一标识一个用户。
@@ -478,9 +481,9 @@ while (true) {
如果一个节点继续表现为 **天选者**,即使大多数节点已经声明它已经死了,则在考虑不周的系统中可能会导致问题。这样的节点能以自己赋予的权能向其他节点发送消息,如果其他节点相信,整个系统可能会做一些不正确的事情。
例如,[图 8-4](img/fig8-4.png) 显示了由于不正确的锁实现导致的数据损坏错误。这个错误不仅仅是理论上的HBase 曾经有这个问题【74,75】假设你要确保一个存储服务中的文件一次只能被一个客户访问因为如果多个客户试图对此写入该文件将被损坏。你尝试通过在访问文件之前要求客户端从锁定服务获取租约来实现此目的。
例如,[图 8-4](/img/fig8-4.png) 显示了由于不正确的锁实现导致的数据损坏错误。这个错误不仅仅是理论上的HBase 曾经有这个问题【74,75】假设你要确保一个存储服务中的文件一次只能被一个客户访问因为如果多个客户试图对此写入该文件将被损坏。你尝试通过在访问文件之前要求客户端从锁定服务获取租约来实现此目的。
![](img/fig8-4.png)
![](/img/fig8-4.png)
**图 8-4 分布式锁的实现不正确:客户端 1 认为它仍然具有有效的租约,即使它已经过期,从而破坏了存储中的文件**
@@ -488,15 +491,15 @@ while (true) {
#### 防护令牌
当使用锁或租约来保护对某些资源(如 [图 8-4](img/fig8-4.png) 中的文件存储)的访问时,需要确保一个被误认为自己是 “天选者” 的节点不能扰乱系统的其它部分。实现这一目标的一个相当简单的技术就是 **防护fencing**,如 [图 8-5](img/fig8-5.png) 所示
当使用锁或租约来保护对某些资源(如 [图 8-4](/img/fig8-4.png) 中的文件存储)的访问时,需要确保一个被误认为自己是 “天选者” 的节点不能扰乱系统的其它部分。实现这一目标的一个相当简单的技术就是 **防护fencing**,如 [图 8-5](/img/fig8-5.png) 所示
![](img/fig8-5.png)
![](/img/fig8-5.png)
**图 8-5 只允许以增加防护令牌的顺序进行写操作,从而保证存储安全**
我们假设每次锁定服务器授予锁或租约时,它还会返回一个 **防护令牌fencing token**,这个数字在每次授予锁定时都会增加(例如,由锁定服务增加)。然后,我们可以要求客户端每次向存储服务发送写入请求时,都必须包含当前的防护令牌。
在 [图 8-5](img/fig8-5.png) 中,客户端 1 以 33 的令牌获得租约,但随后进入一个长时间的停顿并且租约到期。客户端 2 以 34 的令牌(该数字总是增加)获取租约,然后将其写入请求发送到存储服务,包括 34 的令牌。稍后,客户端 1 恢复生机并将其写入存储服务,包括其令牌值 33。但是存储服务器会记住它已经处理了一个具有更高令牌编号34的写入因此它会拒绝带有令牌 33 的请求。
在 [图 8-5](/img/fig8-5.png) 中,客户端 1 以 33 的令牌获得租约,但随后进入一个长时间的停顿并且租约到期。客户端 2 以 34 的令牌(该数字总是增加)获取租约,然后将其写入请求发送到存储服务,包括 34 的令牌。稍后,客户端 1 恢复生机并将其写入存储服务,包括其令牌值 33。但是存储服务器会记住它已经处理了一个具有更高令牌编号34的写入因此它会拒绝带有令牌 33 的请求。
如果将 ZooKeeper 用作锁定服务,则可将事务标识 `zxid` 或节点版本 `cversion` 用作防护令牌。由于它们保证单调递增因此它们具有所需的属性【74】。
@@ -514,7 +517,7 @@ while (true) {
> ### 拜占庭将军问题
>
> 拜占庭将军问题是对所谓 “两将军问题” 的泛化【78】它想象两个将军需要就战斗计划达成一致的情况。由于他们在两个不同的地点建立了营地他们只能通过信使进行沟通信使有时会被延迟或丢失就像网络中的信息包一样。我们将在 [第九章](ch9.md) 讨论这个共识问题。
> 拜占庭将军问题是对所谓 “两将军问题” 的泛化【78】它想象两个将军需要就战斗计划达成一致的情况。由于他们在两个不同的地点建立了营地他们只能通过信使进行沟通信使有时会被延迟或丢失就像网络中的信息包一样。我们将在 [第九章](/ch9) 讨论这个共识问题。
>
> 在这个问题的拜占庭版本里,有 n 位将军需要同意,他们的努力因为有一些叛徒在他们中间而受到阻碍。大多数的将军都是忠诚的,因而发出了真实的信息,但是叛徒可能会试图通过发送虚假或不真实的信息来欺骗和混淆他人(在试图保持未被发现的同时)。事先并不知道叛徒是谁。
>
@@ -543,7 +546,7 @@ Web 应用程序确实需要预期受终端用户控制的客户端(如 Web
### 系统模型与现实
已经有很多算法被设计以解决分布式系统问题 —— 例如,我们将在 [第九章](ch9.md) 讨论共识问题的解决方案。为了有用,这些算法需要容忍我们在本章中讨论的分布式系统的各种故障。
已经有很多算法被设计以解决分布式系统问题 —— 例如,我们将在 [第九章](/ch9) 讨论共识问题的解决方案。为了有用,这些算法需要容忍我们在本章中讨论的分布式系统的各种故障。
算法的编写方式不应该过分依赖于运行的硬件和软件配置的细节。这就要求我们以某种方式将我们期望在系统中发生的错误形式化。我们通过定义一个系统模型来做到这一点,这个模型是一个抽象,描述一个算法可以假设的事情。
@@ -619,7 +622,7 @@ Web 应用程序确实需要预期受终端用户控制的客户端(如 Web
例如,在崩溃 - 恢复crash-recovery模型中的算法通常假设稳定存储器中的数据在崩溃后可以幸存。但是如果磁盘上的数据被破坏或者由于硬件错误或错误配置导致数据被清除会发生什么情况【91】如果服务器存在固件错误并且在重新启动时无法识别其硬盘驱动器即使驱动器已正确连接到服务器那又会发生什么情况【92】
法定人数算法(请参阅 “[读写的法定人数](ch5.md#读写的法定人数)”)依赖节点来记住它声称存储的数据。如果一个节点可能患有健忘症,忘记了以前存储的数据,这会打破法定条件,从而破坏算法的正确性。也许需要一个新的系统模型,在这个模型中,我们假设稳定的存储大多能在崩溃后幸存,但有时也可能会丢失。但是那个模型就变得更难以推理了。
法定人数算法(请参阅 “[读写的法定人数](/ch5#读写的法定人数)”)依赖节点来记住它声称存储的数据。如果一个节点可能患有健忘症,忘记了以前存储的数据,这会打破法定条件,从而破坏算法的正确性。也许需要一个新的系统模型,在这个模型中,我们假设稳定的存储大多能在崩溃后幸存,但有时也可能会丢失。但是那个模型就变得更难以推理了。
算法的理论描述可以简单宣称一些事是不会发生的 —— 在非拜占庭式系统中,我们确实需要对可能发生和不可能发生的故障做出假设。然而,真实世界的实现,仍然会包括处理 “假设上不可能” 情况的代码,即使代码可能就是 `printf("Sucks to be you")``exit(666)`实际上也就是留给运维来擦屁股【93】。这可以说是计算机科学和软件工程间的一个差异
@@ -644,13 +647,13 @@ Web 应用程序确实需要预期受终端用户控制的客户端(如 Web
如果你习惯于在理想化的数学完美的单机环境同一个操作总能确定地返回相同的结果中编写软件那么转向分布式系统的凌乱的物理现实可能会有些令人震惊。相反如果能够在单台计算机上解决一个问题那么分布式系统工程师通常会认为这个问题是平凡的【5】现在单个计算机确实可以做很多事情【95】。如果你可以避免打开潘多拉的盒子把东西放在一台机器上那么通常是值得的。
但是,正如在 [第二部分](part-ii.md) 的介绍中所讨论的那样,可伸缩性并不是使用分布式系统的唯一原因。容错和低延迟(通过将数据放置在距离用户较近的地方)是同等重要的目标,而这些不能用单个节点实现。
但是,正如在 [第二部分](/part-ii) 的介绍中所讨论的那样,可伸缩性并不是使用分布式系统的唯一原因。容错和低延迟(通过将数据放置在距离用户较近的地方)是同等重要的目标,而这些不能用单个节点实现。
在本章中,我们也转换了几次话题,探讨了网络、时钟和进程的不可靠性是否是不可避免的自然规律。我们看到这并不是:有可能给网络提供硬实时的响应保证和有限的延迟,但是这样做非常昂贵,且导致硬件资源的利用率降低。大多数非安全关键系统会选择 **便宜而不可靠**,而不是 **昂贵和可靠**
我们还谈到了超级计算机,它们采用可靠的组件,因此当组件发生故障时必须完全停止并重新启动。相比之下,分布式系统可以永久运行而不会在服务层面中断,因为所有的错误和维护都可以在节点级别进行处理 —— 至少在理论上是如此。(实际上,如果一个错误的配置变更被应用到所有的节点,仍然会使分布式系统瘫痪)。
本章一直在讲存在的问题,给我们展现了一幅黯淡的前景。在 [下一章](ch9.md) 中,我们将继续讨论解决方案,并讨论一些旨在解决分布式系统中所有问题的算法。
本章一直在讲存在的问题,给我们展现了一幅黯淡的前景。在 [下一章](/ch9) 中,我们将继续讨论解决方案,并讨论一些旨在解决分布式系统中所有问题的算法。
## 参考文献
@@ -752,11 +755,3 @@ Web 应用程序确实需要预期受终端用户控制的客户端(如 Web
1. Frank McSherry, Michael Isard, and Derek G. Murray: “[Scalability! But at What COST?](http://www.frankmcsherry.org/assets/COST.pdf),” at *15th USENIX Workshop on Hot Topics in Operating Systems* (HotOS), May 2015.
[^译著1]: 原诗为Hey I just met you. The networks laggy. But heres my data. So store it maybe.Hey, 应改编自《Call Me Maybe》歌词I just met you, And this is crazy, But here's my number, So call me, maybe?
------
| 上一章 | 目录 | 下一章 |
| ---------------------- | ------------------------------- | ------------------------------ |
| [第七章:事务](ch7.md) | [设计数据密集型应用](README.md) | [第九章:一致性与共识](ch9.md) |

View File

@@ -1,23 +1,26 @@
# 第九章:一致性与共识
---
title: "第九章:一致性与共识"
linkTitle: "9. 一致性与共识"
weight: 209
breadcrumbs: false
---
![](img/ch9.png)
![](/img/ch9.png)
> 好死还是赖活着?
> —— Jay Kreps, 关于 Kafka 与 Jepsen 的若干笔记 (2013)
---------------
[TOC]
正如 [第八章](/ch8) 所讨论的,分布式系统中的许多事情可能会出错。处理这种故障的最简单方法是简单地让整个服务失效,并向用户显示错误消息。如果无法接受这个解决方案,我们就需要找到容错的方法 —— 即使某些内部组件出现故障,服务也能正常运行。
正如 [第八章](ch8.md) 所讨论的,分布式系统中的许多事情可能会出错。处理这种故障的最简单方法是简单地让整个服务失效,并向用户显示错误消息。如果无法接受这个解决方案,我们就需要找到容错的方法 —— 即使某些内部组件出现故障,服务也能正常运行
在本章中,我们将讨论构建容错分布式系统的算法和协议的一些例子。我们将假设 [第八章](/ch8) 的所有问题都可能发生:网络中的数据包可能会丢失、重新排序、重复推送或任意延迟;时钟只是尽其所能地近似;且节点可以暂停(例如,由于垃圾收集)或随时崩溃
在本章中,我们将讨论构建容错分布式系统的算法和协议的一些例子。我们将假设 [](ch8.md) 的所有问题都可能发生:网络中的数据包可能会丢失、重新排序、重复推送或任意延迟;时钟只是尽其所能地近似;且节点可以暂停(例如,由于垃圾收集)或随时崩溃
构建容错系统的最好方法,是找到一些带有实用保证的通用抽象,实现一次,然后让应用依赖这些保证。这与 [第七章](ch7.md) 中的事务处理方法相同:通过使用事务,应用可以假装没有崩溃(原子性),没有其他人同时访问数据库(隔离),存储设备是完全可靠的(持久性)。即使发生崩溃,竞态条件和磁盘故障,事务抽象隐藏了这些问题,因此应用不必担心它们。
构建容错系统的最好方法,是找到一些带有实用保证的通用抽象,实现一次,然后让应用依赖这些保证。这与 [](/ch7) 中的事务处理方法相同:通过使用事务,应用可以假装没有崩溃(原子性),没有其他人同时访问数据库(隔离),存储设备是完全可靠的(持久性)。即使发生崩溃,竞态条件和磁盘故障,事务抽象隐藏了这些问题,因此应用不必担心它们
现在我们将继续沿着同样的路线前进,寻求可以让应用忽略分布式系统部分问题的抽象概念。例如,分布式系统最重要的抽象之一就是 **共识consensus****就是让所有的节点对某件事达成一致**。正如我们在本章中将会看到的那样,要可靠地达成共识,且不被网络故障和进程故障所影响,是一个令人惊讶的棘手问题。
一旦达成共识,应用可以将其用于各种目的。例如,假设你有一个单主复制的数据库。如果主库挂掉,并且需要故障切换到另一个节点,剩余的数据库节点可以使用共识来选举新的领导者。正如在 “[处理节点宕机](ch5.md#处理节点宕机)” 中所讨论的那样,重要的是只有一个领导者,且所有的节点都认同其领导。如果两个节点都认为自己是领导者,这种情况被称为 **脑裂split brain**,它经常会导致数据丢失。正确实现共识有助于避免这种问题。
一旦达成共识,应用可以将其用于各种目的。例如,假设你有一个单主复制的数据库。如果主库挂掉,并且需要故障切换到另一个节点,剩余的数据库节点可以使用共识来选举新的领导者。正如在 “[处理节点宕机](/ch5#处理节点宕机)” 中所讨论的那样,重要的是只有一个领导者,且所有的节点都认同其领导。如果两个节点都认为自己是领导者,这种情况被称为 **脑裂split brain**,它经常会导致数据丢失。正确实现共识有助于避免这种问题。
在本章后面的 “[分布式事务与共识](#分布式事务与共识)” 中,我们将研究解决共识和相关问题的算法。但首先,我们首先需要探索可以在分布式系统中提供的保证和抽象的范围。
@@ -28,11 +31,11 @@
## 一致性保证
在 “[复制延迟问题](ch5.md#复制延迟问题)” 中,我们看到了数据库复制中发生的一些时序问题。如果你在同一时刻查看两个数据库节点,则可能在两个节点上看到不同的数据,因为写请求在不同的时间到达不同的节点。无论数据库使用何种复制方法(单主复制、多主复制或无主复制),都会出现这些不一致情况。
在 “[复制延迟问题](/ch5#复制延迟问题)” 中,我们看到了数据库复制中发生的一些时序问题。如果你在同一时刻查看两个数据库节点,则可能在两个节点上看到不同的数据,因为写请求在不同的时间到达不同的节点。无论数据库使用何种复制方法(单主复制、多主复制或无主复制),都会出现这些不一致情况。
大多数复制的数据库至少提供了 **最终一致性**这意味着如果你停止向数据库写入数据并等待一段不确定的时间那么最终所有的读取请求都会返回相同的值【1】。换句话说不一致性是暂时的最终会自行解决假设网络中的任何故障最终都会被修复。最终一致性的一个更好的名字可能是 **收敛convergence**因为我们预计所有的副本最终会收敛到相同的值【2】。
然而,这是一个非常弱的保证 —— 它并没有说什么时候副本会收敛。在收敛之前读操作可能会返回任何东西或什么都没有【1】。例如如果你写入了一个值然后立即再次读取这并不能保证你能看到刚才写入的值因为读请求可能会被路由到另外的副本上。请参阅 “[读己之写](ch5.md#读己之写)” )。
然而,这是一个非常弱的保证 —— 它并没有说什么时候副本会收敛。在收敛之前读操作可能会返回任何东西或什么都没有【1】。例如如果你写入了一个值然后立即再次读取这并不能保证你能看到刚才写入的值因为读请求可能会被路由到另外的副本上。请参阅 “[读己之写](/ch5#读己之写)” )。
对于应用开发人员而言最终一致性是很困难的因为它与普通单线程程序中变量的行为有很大区别。对于后者如果将一个值赋给一个变量然后很快地再次读取不可能读到旧的值或者读取失败。数据库表面上看起来像一个你可以读写的变量但实际上它有更复杂的语义【3】。
@@ -40,7 +43,7 @@
本章将探索数据系统可能选择提供的更强一致性模型。它不是免费的:具有较强保证的系统可能会比保证较差的系统具有更差的性能或更少的容错性。尽管如此,更强的保证能够吸引人,因为它们更容易用对。只有见过不同的一致性模型后,才能更好地决定哪一个最适合自己的需求。
**分布式一致性模型** 和我们之前讨论的事务隔离级别的层次结构有一些相似之处【4,5】请参阅 “[弱隔离级别](ch7.md#弱隔离级别)”)。尽管两者有一部分内容重叠,但它们大多是无关的问题:事务隔离主要是为了 **避免由于同时执行事务而导致的竞争状态**,而分布式一致性主要关于 **在面对延迟和故障时如何协调副本间的状态**
**分布式一致性模型** 和我们之前讨论的事务隔离级别的层次结构有一些相似之处【4,5】请参阅 “[弱隔离级别](/ch7#弱隔离级别)”)。尽管两者有一部分内容重叠,但它们大多是无关的问题:事务隔离主要是为了 **避免由于同时执行事务而导致的竞争状态**,而分布式一致性主要关于 **在面对延迟和故障时如何协调副本间的状态**
本章涵盖了广泛的话题,但我们将会看到这些领域实际上是紧密联系在一起的:
@@ -57,11 +60,11 @@
在一个线性一致的系统中,只要一个客户端成功完成写操作,所有客户端从数据库中读取数据必须能够看到刚刚写入的值。要维护数据的单个副本的假象,系统应保障读到的值是最近的、最新的,而不是来自陈旧的缓存或副本。换句话说,线性一致性是一个 **新鲜度保证recency guarantee**。为了阐明这个想法,我们来看看一个非线性一致系统的例子。
![](img/fig9-1.png)
![](/img/fig9-1.png)
**图 9-1 这个系统是非线性一致的,导致了球迷的困惑**
[图 9-1](img/fig9-1.png) 展示了一个关于体育网站的非线性一致例子【9】。Alice 和 Bob 正坐在同一个房间里,都盯着各自的手机,关注着 2014 年 FIFA 世界杯决赛的结果。在最后得分公布后Alice 刷新页面,看到宣布了获胜者,并兴奋地告诉 Bob。Bob 难以置信地刷新了自己的手机,但他的请求路由到了一个落后的数据库副本上,手机显示比赛仍在进行。
[图 9-1](/img/fig9-1.png) 展示了一个关于体育网站的非线性一致例子【9】。Alice 和 Bob 正坐在同一个房间里,都盯着各自的手机,关注着 2014 年 FIFA 世界杯决赛的结果。在最后得分公布后Alice 刷新页面,看到宣布了获胜者,并兴奋地告诉 Bob。Bob 难以置信地刷新了自己的手机,但他的请求路由到了一个落后的数据库副本上,手机显示比赛仍在进行。
如果 Alice 和 Bob 在同一时间刷新并获得了两个不同的查询结果,也许就没有那么令人惊讶了。因为他们不知道服务器处理他们请求的精确时刻。然而 Bob 是在听到 Alice 惊呼最后得分 **之后**,点击了刷新按钮(启动了他的查询),因此他希望查询结果至少与爱丽丝一样新鲜。但他的查询返回了陈旧结果,这一事实违背了线性一致性的要求。
@@ -69,22 +72,22 @@
线性一致性背后的基本思想很简单:使系统看起来好像只有一个数据副本。然而确切来讲,实际上有更多要操心的地方。为了更好地理解线性一致性,让我们再看几个例子。
[图 9-2](img/fig9-2.png) 显示了三个客户端在线性一致数据库中同时读写相同的键 `x`。在分布式系统文献中,`x` 被称为 **寄存器register**,例如,它可以是键值存储中的一个 **键**,关系数据库中的一 **行**,或文档数据库中的一个 **文档**
[图 9-2](/img/fig9-2.png) 显示了三个客户端在线性一致数据库中同时读写相同的键 `x`。在分布式系统文献中,`x` 被称为 **寄存器register**,例如,它可以是键值存储中的一个 **键**,关系数据库中的一 **行**,或文档数据库中的一个 **文档**
![](img/fig9-2.png)
![](/img/fig9-2.png)
**图 9-2 如果读取请求与写入请求并发,则可能会返回旧值或新值**
为了简单起见,[图 9-2](img/fig9-2.png) 采用了用户请求的视角,而不是数据库内部的视角。每个横柱都是由客户端发出的请求,其中柱头是请求发送的时刻,柱尾是客户端收到响应的时刻。因为网络延迟变化无常,客户端不知道数据库处理其请求的精确时间 —— 只知道它发生在发送请求和接收响应之间的某个时刻。[^i]
为了简单起见,[图 9-2](/img/fig9-2.png) 采用了用户请求的视角,而不是数据库内部的视角。每个横柱都是由客户端发出的请求,其中柱头是请求发送的时刻,柱尾是客户端收到响应的时刻。因为网络延迟变化无常,客户端不知道数据库处理其请求的精确时间 —— 只知道它发生在发送请求和接收响应之间的某个时刻。[^i]
[^i]: 这个图的一个微妙的细节是它假定存在一个全局时钟,由水平轴表示。虽然真实的系统通常没有准确的时钟(请参阅 “[不可靠的时钟](ch8.md#不可靠的时钟)”但这种假设是允许的为了分析分布式算法我们可以假设存在一个精确的全局时钟不过算法无法访问它【47】。算法只能看到由石英振荡器和 NTP 产生的对真实时间的逼近。
[^i]: 这个图的一个微妙的细节是它假定存在一个全局时钟,由水平轴表示。虽然真实的系统通常没有准确的时钟(请参阅 “[不可靠的时钟](/ch8#不可靠的时钟)”但这种假设是允许的为了分析分布式算法我们可以假设存在一个精确的全局时钟不过算法无法访问它【47】。算法只能看到由石英振荡器和 NTP 产生的对真实时间的逼近。
在这个例子中,寄存器有两种类型的操作:
* $read(x)⇒v$表示客户端请求读取寄存器 `x` 的值,数据库返回值 `v`
* $write(x,v)⇒r$ 表示客户端请求将寄存器 `x` 设置为值 `v` ,数据库返回响应 `r` (可能正确,可能错误)。
在 [图 9-2](img/fig9-2.png) 中,`x` 的值最初为 `0`,客户端 C 执行写请求将其设置为 `1`。发生这种情况时,客户端 A 和 B 反复轮询数据库以读取最新值。A 和 B 的请求可能会收到怎样的响应?
在 [图 9-2](/img/fig9-2.png) 中,`x` 的值最初为 `0`,客户端 C 执行写请求将其设置为 `1`。发生这种情况时,客户端 A 和 B 反复轮询数据库以读取最新值。A 和 B 的请求可能会收到怎样的响应?
* 客户端 A 的第一个读操作,完成于写操作开始之前,因此必须返回旧值 `0`
* 客户端 A 的最后一个读操作,开始于写操作完成之后。如果数据库是线性一致性的,它必然返回新值 `1`:因为读操作和写操作一定是在其各自的起止区间内的某个时刻被处理。如果在写入结束后开始读取,则读取处理一定发生在写入完成之后,因此它必须看到写入的新值。
@@ -94,31 +97,31 @@
[^ii]: 如果读取(与写入同时发生时)可能返回旧值或新值,则称该寄存器为 **常规寄存器regular register**【7,25】
为了使系统线性一致,我们需要添加另一个约束,如 [图 9-3](img/fig9-3.png) 所示
为了使系统线性一致,我们需要添加另一个约束,如 [图 9-3](/img/fig9-3.png) 所示
![](img/fig9-3.png)
![](/img/fig9-3.png)
**图 9-3 任何一个读取返回新值后,所有后续读取(在相同或其他客户端上)也必须返回新值。**
在一个线性一致的系统中,我们可以想象,在 `x` 的值从 `0` 自动翻转到 `1` 的时候(在写操作的开始和结束之间)必定有一个时间点。因此,如果一个客户端的读取返回新的值 `1`,即使写操作尚未完成,所有后续读取也必须返回新值。
[图 9-3](img/fig9-3.png) 中的箭头说明了这个时序依赖关系。客户端 A 是第一个读取新的值 `1` 的位置。在 A 的读取返回之后B 开始新的读取。由于 B 的读取严格发生于 A 的读取之后,因此即使 C 的写入仍在进行中,也必须返回 `1`(与 [图 9-1](img/fig9-1.png) 中的 Alice 和 Bob 的情况相同:在 Alice 读取新值之后Bob 也希望读取新的值)。
[图 9-3](/img/fig9-3.png) 中的箭头说明了这个时序依赖关系。客户端 A 是第一个读取新的值 `1` 的位置。在 A 的读取返回之后B 开始新的读取。由于 B 的读取严格发生于 A 的读取之后,因此即使 C 的写入仍在进行中,也必须返回 `1`(与 [图 9-1](/img/fig9-1.png) 中的 Alice 和 Bob 的情况相同:在 Alice 读取新值之后Bob 也希望读取新的值)。
我们可以进一步细化这个时序图,展示每个操作是如何在特定时刻原子性生效的。[图 9-4](img/fig9-4.png) 显示了一个更复杂的例子【10】。
我们可以进一步细化这个时序图,展示每个操作是如何在特定时刻原子性生效的。[图 9-4](/img/fig9-4.png) 显示了一个更复杂的例子【10】。
在 [图 9-4](img/fig9-4.png) 中,除了读写之外,还增加了第三种类型的操作:
在 [图 9-4](/img/fig9-4.png) 中,除了读写之外,还增加了第三种类型的操作:
* $cas(x, v_{old}, v_{new})⇒r$ 表示客户端请求进行原子性的 [**比较与设置**](ch7.md#比较并设置CAS) 操作。如果寄存器 $x$ 的当前值等于 $v_{old}$ ,则应该原子地设置为 $v_{new}$ 。如果 $x$ 不等于 $v_{old}$ ,则操作应该保持寄存器不变并返回一个错误。$r$ 是数据库的响应(正确或错误)。
* $cas(x, v_{old}, v_{new})⇒r$ 表示客户端请求进行原子性的 [**比较与设置**](/ch7#比较并设置CAS) 操作。如果寄存器 $x$ 的当前值等于 $v_{old}$ ,则应该原子地设置为 $v_{new}$ 。如果 $x$ 不等于 $v_{old}$ ,则操作应该保持寄存器不变并返回一个错误。$r$ 是数据库的响应(正确或错误)。
[图 9-4](img/fig9-4.png) 中的每个操作都在我们认为操作被执行的时候用竖线标出(在每个操作的横柱之内)。这些标记按顺序连在一起,其结果必须是一个有效的寄存器读写序列(**每次读取都必须返回最近一次写入设置的值**)。
[图 9-4](/img/fig9-4.png) 中的每个操作都在我们认为操作被执行的时候用竖线标出(在每个操作的横柱之内)。这些标记按顺序连在一起,其结果必须是一个有效的寄存器读写序列(**每次读取都必须返回最近一次写入设置的值**)。
线性一致性的要求是,操作标记的连线总是按时间(从左到右)向前移动,而不是向后移动。这个要求确保了我们之前讨论的新鲜度保证:一旦新的值被写入或读取,所有后续的读都会看到写入的值,直到它被再次覆盖。
![](img/fig9-4.png)
![](/img/fig9-4.png)
**图 9-4 将读取和写入看起来已经生效的时间点进行可视化。客户端 B 的最后一次读取不是线性一致的**
[图 9-4](img/fig9-4.png) 中有一些有趣的细节需要指出:
[图 9-4](/img/fig9-4.png) 中有一些有趣的细节需要指出:
* 第一个客户端 B 发送一个读取 `x` 的请求,然后客户端 D 发送一个请求将 `x` 设置为 `0`,然后客户端 A 发送请求将 `x` 设置为 `1`。然而,返回给 B 的读取值为 `1`(由 A 写入的值)。这是可以的:这意味着数据库首先处理 D 的写入,然后是 A 的写入,最后是 B 的读取。虽然这不是请求发送的顺序,但这是一个可以接受的顺序,因为这三个请求是并发的。也许 B 的读请求在网络上略有延迟,所以它在两次写入之后才到达数据库。
@@ -126,26 +129,26 @@
* 此模型不假设有任何事务隔离另一个客户端可能随时更改值。例如C 首先读取到 `1` ,然后读取到 `2` ,因为两次读取之间的值被 B 所更改。可以使用原子 **比较并设置cas** 操作来检查该值是否未被另一客户端同时更改B 和 C 的 **cas** 请求成功,但是 D 的 **cas** 请求失败(在数据库处理它时,`x` 的值不再是 `0` )。
* 客户 B 的最后一次读取(阴影条柱中)不是线性一致的。该操作与 C 的 **cas** 写操作并发(它将 `x``2` 更新为 `4` 。在没有其他请求的情况下B 的读取返回 `2` 是可以的。然而,在 B 的读取开始之前,客户端 A 已经读取了新的值 `4` ,因此不允许 B 读取比 A 更旧的值。再次,与 [图 9-1](img/fig9-1.png) 中的 Alice 和 Bob 的情况相同。
* 客户 B 的最后一次读取(阴影条柱中)不是线性一致的。该操作与 C 的 **cas** 写操作并发(它将 `x``2` 更新为 `4` 。在没有其他请求的情况下B 的读取返回 `2` 是可以的。然而,在 B 的读取开始之前,客户端 A 已经读取了新的值 `4` ,因此不允许 B 读取比 A 更旧的值。再次,与 [图 9-1](/img/fig9-1.png) 中的 Alice 和 Bob 的情况相同。
这就是线性一致性背后的直觉。正式的定义【6】更准确地描述了它。通过记录所有请求和响应的时序并检查它们是否可以排列成有效的顺序以测试一个系统的行为是否线性一致性是可能的尽管在计算上是昂贵的【11】。
> ### 线性一致性与可串行化
>
> **线性一致性** 容易和 [**可串行化**](ch7.md#可串行化) 相混淆,因为两个词似乎都是类似 “可以按顺序排列” 的东西。但它们是两种完全不同的保证,区分两者非常重要:
> **线性一致性** 容易和 [**可串行化**](/ch7#可串行化) 相混淆,因为两个词似乎都是类似 “可以按顺序排列” 的东西。但它们是两种完全不同的保证,区分两者非常重要:
>
> ***可串行化***
>
> **可串行化Serializability** 是事务的隔离属性,每个事务可以读写多个对象(行,文档,记录)—— 请参阅 “[单对象和多对象操作](ch7.md#单对象和多对象操作)”。它确保事务的行为,与它们按照 **某种** 顺序依次执行的结果相同每个事务在下一个事务开始之前运行完成。这种执行顺序可以与事务实际执行的顺序不同。【12】。
> **可串行化Serializability** 是事务的隔离属性,每个事务可以读写多个对象(行,文档,记录)—— 请参阅 “[单对象和多对象操作](/ch7#单对象和多对象操作)”。它确保事务的行为,与它们按照 **某种** 顺序依次执行的结果相同每个事务在下一个事务开始之前运行完成。这种执行顺序可以与事务实际执行的顺序不同。【12】。
>
> ***线性一致性***
>
> **线性一致性Linearizability** 是读取和写入寄存器(单个对象)的 **新鲜度保证**。它不会将操作组合为事务,因此它也不会阻止写入偏差等问题(请参阅 “[写入偏差和幻读](ch7.md#写入偏差与幻读)”),除非采取其他措施(例如 [物化冲突](ch7.md#物化冲突))。
> **线性一致性Linearizability** 是读取和写入寄存器(单个对象)的 **新鲜度保证**。它不会将操作组合为事务,因此它也不会阻止写入偏差等问题(请参阅 “[写入偏差和幻读](/ch7#写入偏差与幻读)”),除非采取其他措施(例如 [物化冲突](/ch7#物化冲突))。
>
> 一个数据库可以提供可串行化和线性一致性,这种组合被称为严格的可串行化或 **强的单副本可串行化strong-1SR**【4,13】。基于两阶段锁定的可串行化实现请参阅 “[两阶段锁定](ch7.md#两阶段锁定)” 一节)或 **真的串行执行**(请参阅 “[真的串行执行](ch7.md#真的串行执行)”一节)通常是线性一致性的。
> 一个数据库可以提供可串行化和线性一致性,这种组合被称为严格的可串行化或 **强的单副本可串行化strong-1SR**【4,13】。基于两阶段锁定的可串行化实现请参阅 “[两阶段锁定](/ch7#两阶段锁定)” 一节)或 **真的串行执行**(请参阅 “[真的串行执行](/ch7#真的串行执行)”一节)通常是线性一致性的。
>
> 但是,可串行化的快照隔离(请参阅 “[可串行化快照隔离](ch7.md#可串行化快照隔离)”)不是线性一致性的:按照设计,它从一致的快照中进行读取,以避免读者和写者之间的锁竞争。一致性快照的要点就在于 **它不会包括该快照之后的写入**,因此从快照读取不是线性一致性的。
> 但是,可串行化的快照隔离(请参阅 “[可串行化快照隔离](/ch7#可串行化快照隔离)”)不是线性一致性的:按照设计,它从一致的快照中进行读取,以避免读者和写者之间的锁竞争。一致性快照的要点就在于 **它不会包括该快照之后的写入**,因此从快照读取不是线性一致性的。
### 依赖线性一致性
@@ -156,7 +159,7 @@
一个使用单主复制的系统需要确保领导者真的只有一个而不是几个脑裂。一种选择领导者的方法是使用锁每个节点在启动时尝试获取锁成功者成为领导者【14】。不管这个锁是如何实现的它必须是线性一致的所有节点必须就哪个节点拥有锁达成一致否则就没用了。
诸如 Apache ZooKeeper 【15】和 etcd 【16】之类的协调服务通常用于实现分布式锁和领导者选举。它们使用一致性算法以容错的方式实现线性一致的操作在本章后面的 “[容错共识](#容错共识)” 中讨论此类算法)[^iii]。还有许多微妙的细节来正确地实现锁和领导者选举(例如,请参阅 “[领导者和锁](ch8.md#领导者和锁)” 中的防护问题),而像 Apache Curator 【17】这样的库则通过在 ZooKeeper 之上提供更高级别的配方来提供帮助。但是,线性一致性存储服务是这些协调任务的基础。
诸如 Apache ZooKeeper 【15】和 etcd 【16】之类的协调服务通常用于实现分布式锁和领导者选举。它们使用一致性算法以容错的方式实现线性一致的操作在本章后面的 “[容错共识](#容错共识)” 中讨论此类算法)[^iii]。还有许多微妙的细节来正确地实现锁和领导者选举(例如,请参阅 “[领导者和锁](/ch8#领导者和锁)” 中的防护问题),而像 Apache Curator 【17】这样的库则通过在 ZooKeeper 之上提供更高级别的配方来提供帮助。但是,线性一致性存储服务是这些协调任务的基础。
[^iii]: 严格地说ZooKeeper 和 etcd 提供线性一致性的写操作但读取可能是陈旧的因为默认情况下它们可以由任何一个副本提供服务。你可以选择请求线性一致性读取etcd 称之为 **法定人数读取quorum read**【16】而在 ZooKeeper 中,你需要在读取之前调用 `sync()`【15】。请参阅 “[使用全序广播实现线性一致的存储](#使用全序广播实现线性一致的存储)”。
@@ -170,27 +173,27 @@
如果想要确保银行账户余额永远不会为负数,或者不会出售比仓库里的库存更多的物品,或者两个人不会都预定了航班或剧院里同一时间的同一个位置。这些约束条件都要求所有节点都同意一个最新的值(账户余额,库存水平,座位占用率)。
在实际应用中,宽松地处理这些限制有时是可以接受的(例如,如果航班超额预订,你可以将客户转移到不同的航班并为其提供补偿)。在这种情况下,可能不需要线性一致性,我们将在 “[及时性与完整性](ch12.md#及时性与完整性)” 中讨论这种宽松的约束。
在实际应用中,宽松地处理这些限制有时是可以接受的(例如,如果航班超额预订,你可以将客户转移到不同的航班并为其提供补偿)。在这种情况下,可能不需要线性一致性,我们将在 “[及时性与完整性](/ch12#及时性与完整性)” 中讨论这种宽松的约束。
然而一个硬性的唯一性约束关系型数据库中常见的那种需要线性一致性。其他类型的约束如外键或属性约束可以不需要线性一致性【19】。
#### 跨信道的时序依赖
注意 [图 9-1](img/fig9-1.png) 中的一个细节:如果 Alice 没有惊呼得分Bob 就不会知道他的查询结果是陈旧的。他会在几秒钟之后再次刷新页面并最终看到最后的分数。由于系统中存在额外的信道Alice 的声音传到了 Bob 的耳朵中),线性一致性的违背才被注意到。
注意 [图 9-1](/img/fig9-1.png) 中的一个细节:如果 Alice 没有惊呼得分Bob 就不会知道他的查询结果是陈旧的。他会在几秒钟之后再次刷新页面并最终看到最后的分数。由于系统中存在额外的信道Alice 的声音传到了 Bob 的耳朵中),线性一致性的违背才被注意到。
计算机系统也会出现类似的情况。例如,假设有一个网站,用户可以上传照片,一个后台进程会调整照片大小,降低分辨率以加快下载速度(缩略图)。该系统的架构和数据流如 [图 9-5](img/fig9-5.png) 所示。
计算机系统也会出现类似的情况。例如,假设有一个网站,用户可以上传照片,一个后台进程会调整照片大小,降低分辨率以加快下载速度(缩略图)。该系统的架构和数据流如 [图 9-5](/img/fig9-5.png) 所示。
图像缩放器需要明确的指令来执行尺寸缩放作业,指令是 Web 服务器通过消息队列发送的(请参阅 [第十一章](ch11.md)。Web 服务器不会将整个照片放在队列中,因为大多数消息代理都是针对较短的消息而设计的,而一张照片的空间占用可能达到几兆字节。取而代之的是,首先将照片写入文件存储服务,写入完成后再将给缩放器的指令放入消息队列。
图像缩放器需要明确的指令来执行尺寸缩放作业,指令是 Web 服务器通过消息队列发送的(请参阅 [第十一章](/ch11)。Web 服务器不会将整个照片放在队列中,因为大多数消息代理都是针对较短的消息而设计的,而一张照片的空间占用可能达到几兆字节。取而代之的是,首先将照片写入文件存储服务,写入完成后再将给缩放器的指令放入消息队列。
![](img/fig9-5.png)
![](/img/fig9-5.png)
**图 9-5 Web 服务器和图像缩放器通过文件存储和消息队列进行通信,打开竞争条件的可能性。**
如果文件存储服务是线性一致的,那么这个系统应该可以正常工作。如果它不是线性一致的,则存在竞争条件的风险:消息队列([图 9-5](img/fig9-5.png) 中的步骤 3 和 4可能比存储服务内部的复制replication更快。在这种情况下当缩放器读取图像步骤 5可能会看到图像的旧版本或者什么都没有。如果它处理的是旧版本的图像则文件存储中的全尺寸图和缩略图就产生了永久性的不一致。
如果文件存储服务是线性一致的,那么这个系统应该可以正常工作。如果它不是线性一致的,则存在竞争条件的风险:消息队列([图 9-5](/img/fig9-5.png) 中的步骤 3 和 4可能比存储服务内部的复制replication更快。在这种情况下当缩放器读取图像步骤 5可能会看到图像的旧版本或者什么都没有。如果它处理的是旧版本的图像则文件存储中的全尺寸图和缩略图就产生了永久性的不一致。
出现这个问题是因为 Web 服务器和缩放器之间存在两个不同的信道:文件存储与消息队列。没有线性一致性的新鲜性保证,这两个信道之间的竞争条件是可能的。这种情况类似于 [图 9-1](img/fig9-1.png),数据库复制与 Alice 的嘴到 Bob 耳朵之间的真人音频信道之间也存在竞争条件。
出现这个问题是因为 Web 服务器和缩放器之间存在两个不同的信道:文件存储与消息队列。没有线性一致性的新鲜性保证,这两个信道之间的竞争条件是可能的。这种情况类似于 [图 9-1](/img/fig9-1.png),数据库复制与 Alice 的嘴到 Bob 耳朵之间的真人音频信道之间也存在竞争条件。
线性一致性并不是避免这种竞争条件的唯一方法,但它是最容易理解的。如果你可以控制额外信道(例如消息队列的例子,而不是在 Alice 和 Bob 的例子),则可以使用在 “[读己之写](ch5.md#读己之写)” 讨论过的类似方法,不过会有额外的复杂度代价。
线性一致性并不是避免这种竞争条件的唯一方法,但它是最容易理解的。如果你可以控制额外信道(例如消息队列的例子,而不是在 Alice 和 Bob 的例子),则可以使用在 “[读己之写](/ch5#读己之写)” 讨论过的类似方法,不过会有额外的复杂度代价。
### 实现线性一致的系统
@@ -198,15 +201,15 @@
由于线性一致性本质上意味着 “表现得好像只有一个数据副本,而且所有的操作都是原子的”,所以最简单的答案就是,真的只用一个数据副本。但是这种方法无法容错:如果持有该副本的节点失效,数据将会丢失,或者至少无法访问,直到节点重新启动。
使系统容错最常用的方法是使用复制。我们再来回顾 [第五章](ch5.md) 中的复制方法,并比较它们是否可以满足线性一致性:
使系统容错最常用的方法是使用复制。我们再来回顾 [第五章](/ch5) 中的复制方法,并比较它们是否可以满足线性一致性:
* 单主复制(可能线性一致)
在具有单主复制功能的系统中(请参阅 “[领导者与追随者](ch5.md#领导者与追随者)”),主库具有用于写入的数据的主副本,而追随者在其他节点上保留数据的备份副本。如果从主库或同步更新的从库读取数据,它们 **可能potential** 是线性一致性的 [^iv]。然而实际上并不是每个单主数据库都是线性一致性的无论是因为设计的原因例如因为使用了快照隔离还是因为在并发处理上存在错误【10】。
在具有单主复制功能的系统中(请参阅 “[领导者与追随者](/ch5#领导者与追随者)”),主库具有用于写入的数据的主副本,而追随者在其他节点上保留数据的备份副本。如果从主库或同步更新的从库读取数据,它们 **可能potential** 是线性一致性的 [^iv]。然而实际上并不是每个单主数据库都是线性一致性的无论是因为设计的原因例如因为使用了快照隔离还是因为在并发处理上存在错误【10】。
[^iv]: 对单主数据库进行分区(分片),使得每个分区有一个单独的领导者,不会影响线性一致性,因为线性一致性只是对单一对象的保证。交叉分区事务是一个不同的问题(请参阅 “[分布式事务与共识](#分布式事务与共识)”)。
从主库读取依赖一个假设,你确切地知道领导者是谁。正如在 “[真相由多数所定义](ch8.md#真相由多数所定义)” 中所讨论的那样,一个节点很可能会认为它是领导者,而事实上并非如此 —— 如果具有错觉的领导者继续为请求提供服务可能违反线性一致性【20】。使用异步复制故障切换时甚至可能会丢失已提交的写入请参阅 “[处理节点宕机](ch5.md#处理节点宕机)”),这同时违反了持久性和线性一致性。
从主库读取依赖一个假设,你确切地知道领导者是谁。正如在 “[真相由多数所定义](/ch8#真相由多数所定义)” 中所讨论的那样,一个节点很可能会认为它是领导者,而事实上并非如此 —— 如果具有错觉的领导者继续为请求提供服务可能违反线性一致性【20】。使用异步复制故障切换时甚至可能会丢失已提交的写入请参阅 “[处理节点宕机](/ch5#处理节点宕机)”),这同时违反了持久性和线性一致性。
* 共识算法(线性一致)
@@ -214,27 +217,27 @@
* 多主复制(非线性一致)
具有多主程序复制的系统通常不是线性一致的,因为它们同时在多个节点上处理写入,并将其异步复制到其他节点。因此,它们可能会产生需要被解决的写入冲突(请参阅 “[处理写入冲突](ch5.md#处理写入冲突)”)。这种冲突是因为缺少单一数据副本所导致的。
具有多主程序复制的系统通常不是线性一致的,因为它们同时在多个节点上处理写入,并将其异步复制到其他节点。因此,它们可能会产生需要被解决的写入冲突(请参阅 “[处理写入冲突](/ch5#处理写入冲突)”)。这种冲突是因为缺少单一数据副本所导致的。
* 无主复制(也许不是线性一致的)
对于无主复制的系统Dynamo 风格;请参阅 “[无主复制](ch5.md#无主复制)”),有时候人们会声称通过要求法定人数读写( $w + r > n$ )可以获得 “强一致性”。这取决于法定人数的具体配置,以及强一致性如何定义(通常不完全正确)。
对于无主复制的系统Dynamo 风格;请参阅 “[无主复制](/ch5#无主复制)”),有时候人们会声称通过要求法定人数读写( $w + r > n$ )可以获得 “强一致性”。这取决于法定人数的具体配置,以及强一致性如何定义(通常不完全正确)。
基于日历时钟(例如,在 Cassandra 中;请参阅 “[依赖同步时钟](ch8.md#依赖同步时钟)”)的 “最后写入胜利” 冲突解决方法几乎可以确定是非线性一致的,由于时钟偏差,不能保证时钟的时间戳与实际事件顺序一致。宽松的法定人数(请参阅 “[宽松的法定人数与提示移交](ch5.md#宽松的法定人数与提示移交)”)也破坏了线性一致的可能性。即使使用严格的法定人数,非线性一致的行为也是可能的,如下节所示。
基于日历时钟(例如,在 Cassandra 中;请参阅 “[依赖同步时钟](/ch8#依赖同步时钟)”)的 “最后写入胜利” 冲突解决方法几乎可以确定是非线性一致的,由于时钟偏差,不能保证时钟的时间戳与实际事件顺序一致。宽松的法定人数(请参阅 “[宽松的法定人数与提示移交](/ch5#宽松的法定人数与提示移交)”)也破坏了线性一致的可能性。即使使用严格的法定人数,非线性一致的行为也是可能的,如下节所示。
#### 线性一致性和法定人数
直觉上在 Dynamo 风格的模型中,严格的法定人数读写应该是线性一致性的。但是当我们有可变的网络延迟时,就可能存在竞争条件,如 [图 9-6](img/fig9-6.png) 所示。
直觉上在 Dynamo 风格的模型中,严格的法定人数读写应该是线性一致性的。但是当我们有可变的网络延迟时,就可能存在竞争条件,如 [图 9-6](/img/fig9-6.png) 所示。
![](img/fig9-6.png)
![](/img/fig9-6.png)
**图 9-6 非线性一致的执行,尽管使用了严格的法定人数**
在 [图 9-6](img/fig9-6.png) 中,$x$ 的初始值为 0写入客户端通过向所有三个副本 $n = 3, w = 3$ )发送写入将 $x$ 更新为 `1`。客户端 A 并发地从两个节点组成的法定人群( $r = 2$ )中读取数据,并在其中一个节点上看到新值 `1` 。客户端 B 也并发地从两个不同的节点组成的法定人数中读取,并从两个节点中取回了旧值 `0`
在 [图 9-6](/img/fig9-6.png) 中,$x$ 的初始值为 0写入客户端通过向所有三个副本 $n = 3, w = 3$ )发送写入将 $x$ 更新为 `1`。客户端 A 并发地从两个节点组成的法定人群( $r = 2$ )中读取数据,并在其中一个节点上看到新值 `1` 。客户端 B 也并发地从两个不同的节点组成的法定人数中读取,并从两个节点中取回了旧值 `0`
法定人数条件满足( $w + r> n$ 但是这个执行是非线性一致的B 的请求在 A 的请求完成后开始,但是 B 返回旧值,而 A 返回新值。(又一次,如同 Alice 和 Bob 的例子 [图 9-1](img/fig9-1.png)
法定人数条件满足( $w + r> n$ 但是这个执行是非线性一致的B 的请求在 A 的请求完成后开始,但是 B 返回旧值,而 A 返回新值。(又一次,如同 Alice 和 Bob 的例子 [图 9-1](/img/fig9-1.png)
有趣的是,通过牺牲性能,可以使 Dynamo 风格的法定人数线性化:读取者必须在将结果返回给应用之前,同步执行读修复(请参阅 “[读修复和反熵](ch5.md#读修复和反熵)”) 并且写入者必须在发送写入之前读取法定数量节点的最新状态【24,25】。然而由于性能损失Riak 不执行同步读修复【26】。Cassandra 在进行法定人数读取时,**确实** 在等待读修复完成【27】但是由于使用了最后写入胜利的冲突解决方案当同一个键有多个并发写入时将不能保证线性一致性。
有趣的是,通过牺牲性能,可以使 Dynamo 风格的法定人数线性化:读取者必须在将结果返回给应用之前,同步执行读修复(请参阅 “[读修复和反熵](/ch5#读修复和反熵)”) 并且写入者必须在发送写入之前读取法定数量节点的最新状态【24,25】。然而由于性能损失Riak 不执行同步读修复【26】。Cassandra 在进行法定人数读取时,**确实** 在等待读修复完成【27】但是由于使用了最后写入胜利的冲突解决方案当同一个键有多个并发写入时将不能保证线性一致性。
而且这种方式只能实现线性一致的读写不能实现线性一致的比较和设置CAS操作因为它需要一个共识算法【28】。
@@ -245,9 +248,9 @@
一些复制方法可以提供线性一致性,另一些复制方法则不能,因此深入地探讨线性一致性的优缺点是很有趣的。
我们已经在 [第五章](ch5.md) 中讨论了不同复制方法的一些用例。例如对多数据中心的复制而言,多主复制通常是理想的选择(请参阅 “[运维多个数据中心](ch5.md#运维多个数据中心)”)。[图 9-7](img/fig9-7.png) 说明了这种部署的一个例子。
我们已经在 [第五章](/ch5) 中讨论了不同复制方法的一些用例。例如对多数据中心的复制而言,多主复制通常是理想的选择(请参阅 “[运维多个数据中心](/ch5#运维多个数据中心)”)。[图 9-7](/img/fig9-7.png) 说明了这种部署的一个例子。
![](img/fig9-7.png)
![](/img/fig9-7.png)
**图 9-7 网络中断迫使在线性一致性和可用性之间做出选择。**
@@ -286,7 +289,7 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】它只考虑了
在分布式系统中有更多有趣的 “不可能” 的结果【41】且 CAP 定理现在已经被更精确的结果取代【2,42】所以它现在基本上成了历史古迹了。
[^vi]: 正如 “[真实世界的网络故障](ch8.md#真实世界的网络故障)” 中所讨论的,本书使用 **分区partition** 指代将大数据集细分为小数据集的操作(分片;请参阅 [第六章](ch6.md))。与之对应的是,**网络分区network partition** 是一种特定类型的网络故障,我们通常不会将其与其他类型的故障分开考虑。但是,由于它是 CAP 的 P所以这种情况下我们无法避免混乱。
[^vi]: 正如 “[真实世界的网络故障](/ch8#真实世界的网络故障)” 中所讨论的,本书使用 **分区partition** 指代将大数据集细分为小数据集的操作(分片;请参阅 [第六章](/ch6))。与之对应的是,**网络分区network partition** 是一种特定类型的网络故障,我们通常不会将其与其他类型的故障分开考虑。但是,由于它是 CAP 的 P所以这种情况下我们无法避免混乱。
#### 线性一致性和网络延迟
@@ -298,18 +301,18 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】它只考虑了
许多分布式数据库也是如此:它们是 **为了提高性能** 而选择了牺牲线性一致性而不是为了容错【46】。线性一致的速度很慢 —— 这始终是事实,而不仅仅是网络故障期间。
能找到一个更高效的线性一致存储实现吗看起来答案是否定的Attiya 和 Welch 【47】证明如果你想要线性一致性读写请求的响应时间至少与网络延迟的不确定性成正比。在像大多数计算机网络一样具有高度可变延迟的网络中请参阅 “[超时与无穷的延迟](ch8.md#超时与无穷的延迟)”),线性读写的响应时间不可避免地会很高。更快地线性一致算法不存在,但更弱的一致性模型可以快得多,所以对延迟敏感的系统而言,这类权衡非常重要。在 [第十二章](ch12.md) 中将讨论一些在不牺牲正确性的前提下,绕开线性一致性的方法。
能找到一个更高效的线性一致存储实现吗看起来答案是否定的Attiya 和 Welch 【47】证明如果你想要线性一致性读写请求的响应时间至少与网络延迟的不确定性成正比。在像大多数计算机网络一样具有高度可变延迟的网络中请参阅 “[超时与无穷的延迟](/ch8#超时与无穷的延迟)”),线性读写的响应时间不可避免地会很高。更快地线性一致算法不存在,但更弱的一致性模型可以快得多,所以对延迟敏感的系统而言,这类权衡非常重要。在 [第十二章](/ch12) 中将讨论一些在不牺牲正确性的前提下,绕开线性一致性的方法。
## 顺序保证
之前说过,线性一致寄存器的行为就好像只有单个数据副本一样,且每个操作似乎都是在某个时间点以原子性的方式生效的。这个定义意味着操作是按照某种良好定义的顺序执行的。我们将操作以看上去被执行的顺序连接起来,以此说明了 [图 9-4](img/fig9-4.png) 中的顺序。
之前说过,线性一致寄存器的行为就好像只有单个数据副本一样,且每个操作似乎都是在某个时间点以原子性的方式生效的。这个定义意味着操作是按照某种良好定义的顺序执行的。我们将操作以看上去被执行的顺序连接起来,以此说明了 [图 9-4](/img/fig9-4.png) 中的顺序。
**顺序ordering** 这一主题在本书中反复出现,这表明它可能是一个重要的基础性概念。让我们简要回顾一下其它曾经出现过 **顺序** 的上下文:
* 在 [第五章](ch5.md) 中我们看到,领导者在单主复制中的主要目的就是,在复制日志中确定 **写入顺序order of write**—— 也就是从库应用这些写入的顺序。如果不存在一个领导者,则并发操作可能导致冲突(请参阅 “[处理写入冲突](ch5.md#处理写入冲突)”)。
* 在 [第七章](ch7.md) 中讨论的 **可串行化**,是关于事务表现的像按 **某种先后顺序some sequential order** 执行的保证。它可以字面意义上地以 **串行顺序serial order** 执行事务来实现,或者允许并行执行,但同时防止序列化冲突来实现(通过锁或中止事务)。
* 在 [第八章](ch8.md) 讨论过的在分布式系统中使用时间戳和时钟(请参阅 “[依赖同步时钟](ch8.md#依赖同步时钟)”)是另一种将顺序引入无序世界的尝试,例如,确定两个写入操作哪一个更晚发生。
* 在 [第五章](/ch5) 中我们看到,领导者在单主复制中的主要目的就是,在复制日志中确定 **写入顺序order of write**—— 也就是从库应用这些写入的顺序。如果不存在一个领导者,则并发操作可能导致冲突(请参阅 “[处理写入冲突](/ch5#处理写入冲突)”)。
* 在 [第七章](/ch7) 中讨论的 **可串行化**,是关于事务表现的像按 **某种先后顺序some sequential order** 执行的保证。它可以字面意义上地以 **串行顺序serial order** 执行事务来实现,或者允许并行执行,但同时防止序列化冲突来实现(通过锁或中止事务)。
* 在 [第八章](/ch8) 讨论过的在分布式系统中使用时间戳和时钟(请参阅 “[依赖同步时钟](/ch8#依赖同步时钟)”)是另一种将顺序引入无序世界的尝试,例如,确定两个写入操作哪一个更晚发生。
事实证明,顺序、线性一致性和共识之间有着深刻的联系。尽管这个概念比本书其他部分更加理论化和抽象,但对于明确系统的能力范围(可以做什么和不可以做什么)而言是非常有帮助的。我们将在接下来的几节中探讨这个话题。
@@ -317,12 +320,12 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】它只考虑了
**顺序** 反复出现有几个原因,其中一个原因是,它有助于保持 **因果关系causality**。在本书中我们已经看到了几个例子,其中因果关系是很重要的:
* 在 “[一致前缀读](ch5.md#一致前缀读)”([图 5-5](img/fig5-5.png))中,我们看到一个例子:一个对话的观察者首先看到问题的答案,然后才看到被回答的问题。这是令人困惑的,因为它违背了我们对 **因cause****果effect** 的直觉:如果一个问题被回答,显然问题本身得先在那里,因为给出答案的人必须先看到这个问题(假如他们并没有预见未来的超能力)。我们认为在问题和答案之间存在 **因果依赖causal dependency**
* [图 5-9](img/fig5-9.png) 中出现了类似的模式,我们看到三位领导者之间的复制,并注意到由于网络延迟,一些写入可能会 “压倒” 其他写入。从其中一个副本的角度来看,好像有一个对尚不存在的记录的更新操作。这里的因果意味着,一条记录必须先被创建,然后才能被更新。
* 在 “[检测并发写入](ch5.md#检测并发写入)” 中我们观察到,如果有两个操作 A 和 B则存在三种可能性A 发生在 B 之前,或 B 发生在 A 之前,或者 A 和 B**并发**。这种 **此前发生happened before** 关系是因果关系的另一种表述:如果 A 在 B 前发生,那么意味着 B 可能已经知道了 A或者建立在 A 的基础上,或者依赖于 A。如果 A 和 B 是 **并发** 的,那么它们之间并没有因果联系;换句话说,我们确信 A 和 B 不知道彼此。
* 在事务快照隔离的上下文中(“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”),我们说事务是从一致性快照中读取的。但此语境中 “一致” 到底又是什么意思?这意味着 **与因果关系保持一致consistent with causality**如果快照包含答案它也必须包含被回答的问题【48】。在某个时间点观察整个数据库与因果关系保持一致意味着因果上在该时间点之前发生的所有操作其影响都是可见的但因果上在该时间点之后发生的操作其影响对观察者不可见。**读偏差read skew** 意味着读取的数据处于违反因果关系的状态(不可重复读,如 [图 7-6](img/fig7-6.png) 所示)。
* 事务之间 **写偏差write skew** 的例子(请参阅 “[写入偏差与幻读](ch7.md#写入偏差与幻读)”)也说明了因果依赖:在 [图 7-8](img/fig7-8.png) 中,爱丽丝被允许离班,因为事务认为鲍勃仍在值班,反之亦然。在这种情况下,离班的动作因果依赖于对当前值班情况的观察。[可串行化快照隔离](ch7.md#可串行化快照隔离) 通过跟踪事务之间的因果依赖来检测写偏差。
* 在爱丽丝和鲍勃看球的例子中([图 9-1](img/fig9-1.png)),在听到爱丽丝惊呼比赛结果后,鲍勃从服务器得到陈旧结果的事实违背了因果关系:爱丽丝的惊呼因果依赖于得分宣告,所以鲍勃应该也能在听到爱丽斯惊呼后查询到比分。相同的模式在 “[跨信道的时序依赖](#跨信道的时序依赖)” 一节中,以 “图像大小调整服务” 的伪装再次出现。
* 在 “[一致前缀读](/ch5#一致前缀读)”([图 5-5](/img/fig5-5.png))中,我们看到一个例子:一个对话的观察者首先看到问题的答案,然后才看到被回答的问题。这是令人困惑的,因为它违背了我们对 **因cause****果effect** 的直觉:如果一个问题被回答,显然问题本身得先在那里,因为给出答案的人必须先看到这个问题(假如他们并没有预见未来的超能力)。我们认为在问题和答案之间存在 **因果依赖causal dependency**
* [图 5-9](/img/fig5-9.png) 中出现了类似的模式,我们看到三位领导者之间的复制,并注意到由于网络延迟,一些写入可能会 “压倒” 其他写入。从其中一个副本的角度来看,好像有一个对尚不存在的记录的更新操作。这里的因果意味着,一条记录必须先被创建,然后才能被更新。
* 在 “[检测并发写入](/ch5#检测并发写入)” 中我们观察到,如果有两个操作 A 和 B则存在三种可能性A 发生在 B 之前,或 B 发生在 A 之前,或者 A 和 B**并发**。这种 **此前发生happened before** 关系是因果关系的另一种表述:如果 A 在 B 前发生,那么意味着 B 可能已经知道了 A或者建立在 A 的基础上,或者依赖于 A。如果 A 和 B 是 **并发** 的,那么它们之间并没有因果联系;换句话说,我们确信 A 和 B 不知道彼此。
* 在事务快照隔离的上下文中(“[快照隔离和可重复读](/ch7#快照隔离和可重复读)”),我们说事务是从一致性快照中读取的。但此语境中 “一致” 到底又是什么意思?这意味着 **与因果关系保持一致consistent with causality**如果快照包含答案它也必须包含被回答的问题【48】。在某个时间点观察整个数据库与因果关系保持一致意味着因果上在该时间点之前发生的所有操作其影响都是可见的但因果上在该时间点之后发生的操作其影响对观察者不可见。**读偏差read skew** 意味着读取的数据处于违反因果关系的状态(不可重复读,如 [图 7-6](/img/fig7-6.png) 所示)。
* 事务之间 **写偏差write skew** 的例子(请参阅 “[写入偏差与幻读](/ch7#写入偏差与幻读)”)也说明了因果依赖:在 [图 7-8](/img/fig7-8.png) 中,爱丽丝被允许离班,因为事务认为鲍勃仍在值班,反之亦然。在这种情况下,离班的动作因果依赖于对当前值班情况的观察。[可串行化快照隔离](/ch7#可串行化快照隔离) 通过跟踪事务之间的因果依赖来检测写偏差。
* 在爱丽丝和鲍勃看球的例子中([图 9-1](/img/fig9-1.png)),在听到爱丽丝惊呼比赛结果后,鲍勃从服务器得到陈旧结果的事实违背了因果关系:爱丽丝的惊呼因果依赖于得分宣告,所以鲍勃应该也能在听到爱丽斯惊呼后查询到比分。相同的模式在 “[跨信道的时序依赖](#跨信道的时序依赖)” 一节中,以 “图像大小调整服务” 的伪装再次出现。
因果关系对事件施加了一种 **顺序**:因在果之前;消息发送在消息收取之前。而且就像现实生活中一样,一件事会导致另一件事:某个节点读取了一些数据然后写入一些结果,另一个节点读取其写入的内容,并依次写入一些其他内容,等等。这些因果依赖的操作链定义了系统中的因果顺序,即,什么在什么之前发生。
@@ -341,21 +344,21 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】它只考虑了
* 线性一致性
在线性一致的系统中,操作是全序的:如果系统表现的就好像只有一个数据副本,并且所有操作都是原子性的,这意味着对任何两个操作,我们总是能判定哪个操作先发生。这个全序在 [图 9-4](img/fig9-4.png) 中以时间线表示。
在线性一致的系统中,操作是全序的:如果系统表现的就好像只有一个数据副本,并且所有操作都是原子性的,这意味着对任何两个操作,我们总是能判定哪个操作先发生。这个全序在 [图 9-4](/img/fig9-4.png) 中以时间线表示。
* 因果性
我们说过,如果两个操作都没有在彼此 **之前发生**,那么这两个操作是并发的(请参阅 [“此前发生” 的关系和并发](ch5.md#“此前发生”的关系和并发))。换句话说,如果两个事件是因果相关的(一个发生在另一个事件之前),则它们之间是有序的,但如果它们是并发的,则它们之间的顺序是无法比较的。这意味着因果关系定义了一个偏序,而不是一个全序:一些操作相互之间是有顺序的,但有些则是无法比较的。
我们说过,如果两个操作都没有在彼此 **之前发生**,那么这两个操作是并发的(请参阅 [“此前发生” 的关系和并发](/ch5#“此前发生”的关系和并发))。换句话说,如果两个事件是因果相关的(一个发生在另一个事件之前),则它们之间是有序的,但如果它们是并发的,则它们之间的顺序是无法比较的。这意味着因果关系定义了一个偏序,而不是一个全序:一些操作相互之间是有顺序的,但有些则是无法比较的。
因此,根据这个定义,在线性一致的数据存储中是不存在并发操作的:必须有且仅有一条时间线,所有的操作都在这条时间线上,构成一个全序关系。可能有几个请求在等待处理,但是数据存储确保了每个请求都是在唯一时间线上的某个时间点自动处理的,不存在任何并发。
并发意味着时间线会分岔然后合并 —— 在这种情况下,不同分支上的操作是无法比较的(即并发操作)。在 [第五章](ch5.md) 中我们看到了这种现象:例如,[图 5-14](img/fig5-14.png) 并不是一条直线的全序关系,而是一堆不同的操作并发进行。图中的箭头指明了因果依赖 —— 操作的偏序。
并发意味着时间线会分岔然后合并 —— 在这种情况下,不同分支上的操作是无法比较的(即并发操作)。在 [第五章](/ch5) 中我们看到了这种现象:例如,[图 5-14](/img/fig5-14.png) 并不是一条直线的全序关系,而是一堆不同的操作并发进行。图中的箭头指明了因果依赖 —— 操作的偏序。
如果你熟悉像 Git 这样的分布式版本控制系统,那么其版本历史与因果关系图极其相似。通常,一个 **提交Commit** 发生在另一个提交之后,在一条直线上。但是有时你会遇到分支(当多个人同时在一个项目上工作时),**合并Merge** 会在这些并发创建的提交相融合时创建。
#### 线性一致性强于因果一致性
那么因果顺序和线性一致性之间的关系是什么?答案是线性一致性 **隐含着implies** 因果关系任何线性一致的系统都能正确保持因果性【7】。特别是如果系统中有多个通信通道如 [图 9-5](img/fig9-5.png) 中的消息队列和文件存储服务),线性一致性可以自动保证因果性,系统无需任何特殊操作(如在不同组件间传递时间戳)。
那么因果顺序和线性一致性之间的关系是什么?答案是线性一致性 **隐含着implies** 因果关系任何线性一致的系统都能正确保持因果性【7】。特别是如果系统中有多个通信通道如 [图 9-5](/img/fig9-5.png) 中的消息队列和文件存储服务),线性一致性可以自动保证因果性,系统无需任何特殊操作(如在不同组件间传递时间戳)。
线性一致性确保因果性的事实使线性一致系统变得简单易懂,更有吸引力。然而,正如 “[线性一致性的代价](#线性一致性的代价)” 中所讨论的,使系统线性一致可能会损害其性能和可用性,尤其是在系统具有严重的网络延迟的情况下(例如,如果系统在地理上散布)。出于这个原因,一些分布式数据系统已经放弃了线性一致性,从而获得更好的性能,但它们用起来也更为困难。
@@ -373,16 +376,16 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】它只考虑了
为了确定因果依赖,我们需要一些方法来描述系统中节点的 “知识”。如果节点在发出写入 Y 的请求时已经看到了 X 的值,则 X 和 Y 可能存在因果关系。这个分析使用了那些在欺诈指控刑事调查中常见的问题CEO 在做出决定 Y 时是否 **知道** X
用于确定 *哪些操作发生在其他操作之前* 的技术,与我们在 “[检测并发写入](ch5.md#检测并发写入)” 中所讨论的内容类似。那一节讨论了无领导者数据存储中的因果性为了防止丢失更新我们需要检测到对同一个键的并发写入。因果一致性则更进一步它需要跟踪整个数据库中的因果依赖而不仅仅是一个键。可以推广版本向量以解决此类问题【54】。
用于确定 *哪些操作发生在其他操作之前* 的技术,与我们在 “[检测并发写入](/ch5#检测并发写入)” 中所讨论的内容类似。那一节讨论了无领导者数据存储中的因果性为了防止丢失更新我们需要检测到对同一个键的并发写入。因果一致性则更进一步它需要跟踪整个数据库中的因果依赖而不仅仅是一个键。可以推广版本向量以解决此类问题【54】。
为了确定因果顺序,数据库需要知道应用读取了哪个版本的数据。这就是为什么在 [图 5-13](img/fig5-13.png) 中,来自先前操作的版本号在写入时被传回到数据库的原因。在 SSI 的冲突检测中会出现类似的想法,如 “[可串行化快照隔离](ch7.md#可串行化快照隔离)” 中所述:当事务要提交时,数据库将检查它所读取的数据版本是否仍然是最新的。为此,数据库跟踪哪些数据被哪些事务所读取。
为了确定因果顺序,数据库需要知道应用读取了哪个版本的数据。这就是为什么在 [图 5-13](/img/fig5-13.png) 中,来自先前操作的版本号在写入时被传回到数据库的原因。在 SSI 的冲突检测中会出现类似的想法,如 “[可串行化快照隔离](/ch7#可串行化快照隔离)” 中所述:当事务要提交时,数据库将检查它所读取的数据版本是否仍然是最新的。为此,数据库跟踪哪些数据被哪些事务所读取。
### 序列号顺序
虽然因果是一个重要的理论概念,但实际上跟踪所有的因果关系是不切实际的。在许多应用中,客户端在写入内容之前会先读取大量数据,我们无法弄清写入因果依赖于先前全部的读取内容,还是仅包括其中一部分。显式跟踪所有已读数据意味着巨大的额外开销。
但还有一个更好的方法:我们可以使用 **序列号sequence number****时间戳timestamp** 来排序事件。时间戳不一定来自日历时钟(或物理时钟,它们存在许多问题,如 “[不可靠的时钟](ch8.md#不可靠的时钟)” 中所述)。它可以来自一个 **逻辑时钟logical clock**,这是一个用来生成标识操作的数字序列的算法,典型实现是使用一个每次操作自增的计数器。
但还有一个更好的方法:我们可以使用 **序列号sequence number****时间戳timestamp** 来排序事件。时间戳不一定来自日历时钟(或物理时钟,它们存在许多问题,如 “[不可靠的时钟](/ch8#不可靠的时钟)” 中所述)。它可以来自一个 **逻辑时钟logical clock**,这是一个用来生成标识操作的数字序列的算法,典型实现是使用一个每次操作自增的计数器。
这样的序列号或时间戳是紧凑的(只有几个字节大小),它提供了一个全序关系:也就是说每个操作都有一个唯一的序列号,而且总是可以比较两个序列号,确定哪一个更大(即哪些操作后发生)。
@@ -390,14 +393,14 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】它只考虑了
[^vii]: 与因果关系不一致的全序很容易创建,但没啥用。例如你可以为每个操作生成随机的 UUID并按照字典序比较 UUID以定义操作的全序。这是一个有效的全序但是随机的 UUID 并不能告诉你哪个操作先发生,或者操作是否为并发的。
在单主复制的数据库中(请参阅 “[领导者与追随者](ch5.md#领导者与追随者)”),复制日志定义了与因果一致的写操作。主库可以简单地为每个操作自增一个计数器,从而为复制日志中的每个操作分配一个单调递增的序列号。如果一个从库按照它们在复制日志中出现的顺序来应用写操作,那么从库的状态始终是因果一致的(即使它落后于领导者)。
在单主复制的数据库中(请参阅 “[领导者与追随者](/ch5#领导者与追随者)”),复制日志定义了与因果一致的写操作。主库可以简单地为每个操作自增一个计数器,从而为复制日志中的每个操作分配一个单调递增的序列号。如果一个从库按照它们在复制日志中出现的顺序来应用写操作,那么从库的状态始终是因果一致的(即使它落后于领导者)。
#### 非因果序列号生成器
如果主库不存在(可能因为使用了多主数据库或无主数据库,或者因为使用了分区的数据库),如何为操作生成序列号就没有那么明显了。在实践中有各种各样的方法:
* 每个节点都可以生成自己独立的一组序列号。例如有两个节点,一个节点只能生成奇数,而另一个节点只能生成偶数。通常,可以在序列号的二进制表示中预留一些位,用于唯一的节点标识符,这样可以确保两个不同的节点永远不会生成相同的序列号。
*可以将日历时钟物理时钟的时间戳附加到每个操作上【55】。这种时间戳并不连续但是如果它具有足够高的分辨率那也许足以提供一个操作的全序关系。这一事实应用于* 最后写入胜利 * 的冲突解决方法中(请参阅 “[有序事件的时间戳](ch8.md#有序事件的时间戳)”)。
*可以将日历时钟物理时钟的时间戳附加到每个操作上【55】。这种时间戳并不连续但是如果它具有足够高的分辨率那也许足以提供一个操作的全序关系。这一事实应用于* 最后写入胜利 * 的冲突解决方法中(请参阅 “[有序事件的时间戳](/ch8#有序事件的时间戳)”)。
* 可以预先分配序列号区块。例如,节点 A 可能要求从序列号 1 到 1,000 区块的所有权,而节点 B 可能要求序列号 1,001 到 2,000 区块的所有权。然后每个节点可以独立分配所属区块中的序列号,并在序列号告急时请求分配一个新的区块。
这三个选项都比单一主库的自增计数器表现要好,并且更具可伸缩性。它们为每个操作生成一个唯一的,近似自增的序列号。然而它们都有同一个问题:生成的序列号与因果不一致。
@@ -406,9 +409,9 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】它只考虑了
* 每个节点每秒可以处理不同数量的操作。因此,如果一个节点产生偶数序列号而另一个产生奇数序列号,则偶数计数器可能落后于奇数计数器,反之亦然。如果你有一个奇数编号的操作和一个偶数编号的操作,你无法准确地说出哪一个操作在因果上先发生。
* 来自物理时钟的时间戳会受到时钟偏移的影响,这可能会使其与因果不一致。例如 [图 8-3](img/fig8-3.png) 展示了一个例子,其中因果上晚发生的操作,却被分配了一个更早的时间戳。[^viii]
* 来自物理时钟的时间戳会受到时钟偏移的影响,这可能会使其与因果不一致。例如 [图 8-3](/img/fig8-3.png) 展示了一个例子,其中因果上晚发生的操作,却被分配了一个更早的时间戳。[^viii]
[^viii]: 可以使物理时钟时间戳与因果关系保持一致:在 “[全局快照的同步时钟](ch8.md#全局快照的同步时钟)” 中,我们讨论了 Google 的 Spanner它可以估计预期的时钟偏差并在提交写入之前等待不确定性间隔。这种方法确保了实际上靠后的事务会有更大的时间戳。但是大多数时钟不能提供这种所需的不确定性度量。
[^viii]: 可以使物理时钟时间戳与因果关系保持一致:在 “[全局快照的同步时钟](/ch8#全局快照的同步时钟)” 中,我们讨论了 Google 的 Spanner它可以估计预期的时钟偏差并在提交写入之前等待不确定性间隔。这种方法确保了实际上靠后的事务会有更大的时间戳。但是大多数时钟不能提供这种所需的不确定性度量。
* 在分配区块的情况下,某个操作可能会被赋予一个范围在 1,001 到 2,000 内的序列号,然而一个因果上更晚的操作可能被赋予一个范围在 1 到 1,000 之间的数字。这里序列号与因果关系也是不一致的。
@@ -417,9 +420,9 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】它只考虑了
尽管刚才描述的三个序列号生成器与因果不一致但实际上有一个简单的方法来产生与因果关系一致的序列号。它被称为兰伯特时间戳莱斯利・兰伯特Leslie Lamport于 1978 年提出【56】现在是分布式系统领域中被引用最多的论文之一。
[图 9-8](img/fig9-8.png) 说明了兰伯特时间戳的应用。每个节点都有一个唯一标识符,和一个保存自己执行操作数量的计数器。兰伯特时间戳就是两者的简单组合:(计数器,节点 ID$(counter, node ID)$。两个节点有时可能具有相同的计数器值,但通过在时间戳中包含节点 ID每个时间戳都是唯一的。
[图 9-8](/img/fig9-8.png) 说明了兰伯特时间戳的应用。每个节点都有一个唯一标识符,和一个保存自己执行操作数量的计数器。兰伯特时间戳就是两者的简单组合:(计数器,节点 ID$(counter, node ID)$。两个节点有时可能具有相同的计数器值,但通过在时间戳中包含节点 ID每个时间戳都是唯一的。
![](img/fig9-8.png)
![](/img/fig9-8.png)
**图 9-8 Lamport 时间戳提供了与因果关系一致的全序。**
@@ -428,17 +431,17 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】它只考虑了
迄今,这个描述与上节所述的奇偶计数器基本类似。使兰伯特时间戳因果一致的关键思想如下所示:每个节点和每个客户端跟踪迄今为止所见到的最大 **计数器** 值,并在每个请求中包含这个最大计数器值。当一个节点收到最大计数器值大于自身计数器值的请求或响应时,它立即将自己的计数器设置为这个最大值。
这如 [图 9-8](img/fig9-8.png) 所示,其中客户端 A 从节点 2 接收计数器值 `5` ,然后将最大值 `5` 发送到节点 1 。此时,节点 1 的计数器仅为 `1` ,但是它立即前移至 `5` ,所以下一个操作的计数器的值为 `6`
这如 [图 9-8](/img/fig9-8.png) 所示,其中客户端 A 从节点 2 接收计数器值 `5` ,然后将最大值 `5` 发送到节点 1 。此时,节点 1 的计数器仅为 `1` ,但是它立即前移至 `5` ,所以下一个操作的计数器的值为 `6`
只要每一个操作都携带着最大计数器值,这个方案确保兰伯特时间戳的排序与因果一致,因为每个因果依赖都会导致时间戳增长。
兰伯特时间戳有时会与我们在 “[检测并发写入](ch5.md#检测并发写入)” 中看到的版本向量相混淆。虽然两者有一些相似之处,但它们有着不同的目的:版本向量可以区分两个操作是并发的,还是一个因果依赖另一个;而兰伯特时间戳总是施行一个全序。从兰伯特时间戳的全序中,你无法分辨两个操作是并发的还是因果依赖的。兰伯特时间戳优于版本向量的地方是,它更加紧凑。
兰伯特时间戳有时会与我们在 “[检测并发写入](/ch5#检测并发写入)” 中看到的版本向量相混淆。虽然两者有一些相似之处,但它们有着不同的目的:版本向量可以区分两个操作是并发的,还是一个因果依赖另一个;而兰伯特时间戳总是施行一个全序。从兰伯特时间戳的全序中,你无法分辨两个操作是并发的还是因果依赖的。兰伯特时间戳优于版本向量的地方是,它更加紧凑。
#### 光有时间戳排序还不够
虽然兰伯特时间戳定义了一个与因果一致的全序,但它还不足以解决分布式系统中的许多常见问题。
例如,考虑一个需要确保用户名能唯一标识用户帐户的系统。如果两个用户同时尝试使用相同的用户名创建帐户,则其中一个应该成功,另一个应该失败(我们之前在 “[领导者和锁](ch8.md#领导者和锁)” 中提到过这个问题)。
例如,考虑一个需要确保用户名能唯一标识用户帐户的系统。如果两个用户同时尝试使用相同的用户名创建帐户,则其中一个应该成功,另一个应该失败(我们之前在 “[领导者和锁](/ch8#领导者和锁)” 中提到过这个问题)。
乍看之下,似乎操作的全序关系足以解决这一问题(例如使用兰伯特时间戳):如果创建了两个具有相同用户名的帐户,选择时间戳较小的那个作为胜者(第一个抓到用户名的人),并让带有更大时间戳者失败。由于时间戳上有全序关系,所以这个比较总是可行的。
@@ -456,7 +459,7 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】它只考虑了
如果你的程序只运行在单个 CPU 核上,那么定义一个操作全序是很容易的:可以简单认为就是 CPU 执行这些操作的顺序。但是在分布式系统中,让所有节点对同一个全局操作顺序达成一致可能相当棘手。在上一节中,我们讨论了按时间戳或序列号进行排序,但发现它还不如单主复制给力(如果你使用时间戳排序来实现唯一性约束,就不能容忍任何错误,因为你必须要从每个节点都获取到最新的序列号)。
如前所述,单主复制通过选择一个节点作为主库来确定操作的全序,并在主库的单个 CPU 核上对所有操作进行排序。接下来的挑战是,如果吞吐量超出单个主库的处理能力,这种情况下如何扩展系统;以及,如果主库失效(“[处理节点宕机](ch5.md#处理节点宕机)”),如何处理故障切换。在分布式系统文献中,这个问题被称为 **全序广播total order broadcast****原子广播atomic broadcast**[^ix]【25,57,58】。
如前所述,单主复制通过选择一个节点作为主库来确定操作的全序,并在主库的单个 CPU 核上对所有操作进行排序。接下来的挑战是,如果吞吐量超出单个主库的处理能力,这种情况下如何扩展系统;以及,如果主库失效(“[处理节点宕机](/ch5#处理节点宕机)”),如何处理故障切换。在分布式系统文献中,这个问题被称为 **全序广播total order broadcast****原子广播atomic broadcast**[^ix]【25,57,58】。
[^ix]: “原子广播” 是一个传统的术语,非常混乱,而且与 “原子” 一词的其他用法不一致:它与 ACID 事务中的原子性没有任何关系,只是与原子操作(在多线程编程的意义上 或原子寄存器线性一致存储有间接的联系。全序组播total order multicast是另一个同义词。
@@ -480,19 +483,19 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】它只考虑了
像 ZooKeeper 和 etcd 这样的共识服务实际上实现了全序广播。这一事实暗示了全序广播与共识之间有着紧密联系,我们将在本章稍后进行探讨。
全序广播正是数据库复制所需的:如果每个消息都代表一次数据库的写入,且每个副本都按相同的顺序处理相同的写入,那么副本间将相互保持一致(除了临时的复制延迟)。这个原理被称为 **状态机复制state machine replication**【60】我们将在 [第十一章](ch11.md) 中重新回到这个概念。
全序广播正是数据库复制所需的:如果每个消息都代表一次数据库的写入,且每个副本都按相同的顺序处理相同的写入,那么副本间将相互保持一致(除了临时的复制延迟)。这个原理被称为 **状态机复制state machine replication**【60】我们将在 [第十一章](/ch11) 中重新回到这个概念。
与之类似,可以使用全序广播来实现可串行化的事务:如 “[真的串行执行](ch7.md#真的串行执行)” 中所述如果每个消息都表示一个确定性事务以存储过程的形式来执行且每个节点都以相同的顺序处理这些消息那么数据库的分区和副本就可以相互保持一致【61】。
与之类似,可以使用全序广播来实现可串行化的事务:如 “[真的串行执行](/ch7#真的串行执行)” 中所述如果每个消息都表示一个确定性事务以存储过程的形式来执行且每个节点都以相同的顺序处理这些消息那么数据库的分区和副本就可以相互保持一致【61】。
全序广播的一个重要表现是,顺序在消息送达时被固化:如果后续的消息已经送达,节点就不允许追溯地将(先前)消息插入顺序中的较早位置。这个事实使得全序广播比时间戳排序更强。
考量全序广播的另一种方式是,这是一种创建日志的方式(如在复制日志、事务日志或预写式日志中):传递消息就像追加写入日志。由于所有节点必须以相同的顺序传递相同的消息,因此所有节点都可以读取日志,并看到相同的消息序列。
全序广播对于实现提供防护令牌的锁服务也很有用(请参阅 “[防护令牌](ch8.md#防护令牌)”)。每个获取锁的请求都作为一条消息追加到日志末尾,并且所有的消息都按它们在日志中出现的顺序依次编号。序列号可以当成防护令牌用,因为它是单调递增的。在 ZooKeeper 中,这个序列号被称为 `zxid` 【15】。
全序广播对于实现提供防护令牌的锁服务也很有用(请参阅 “[防护令牌](/ch8#防护令牌)”)。每个获取锁的请求都作为一条消息追加到日志末尾,并且所有的消息都按它们在日志中出现的顺序依次编号。序列号可以当成防护令牌用,因为它是单调递增的。在 ZooKeeper 中,这个序列号被称为 `zxid` 【15】。
#### 使用全序广播实现线性一致的存储
如 [图 9-4](img/fig9-4.png) 所示,在线性一致的系统中,存在操作的全序。这是否意味着线性一致与全序广播一样?不尽然,但两者之间有着密切的联系 [^x]。
如 [图 9-4](/img/fig9-4.png) 所示,在线性一致的系统中,存在操作的全序。这是否意味着线性一致与全序广播一样?不尽然,但两者之间有着密切的联系 [^x]。
[^x]: 从形式上讲,线性一致读写寄存器是一个 “更容易” 的问题。全序广播等价于共识【67】而共识问题在异步的崩溃 - 停止模型【68】中没有确定性的解决方案而线性一致的读写寄存器 **可以** 在这种模型中实现【23,24,25】。然而支持诸如 **比较并设置CAS, compare-and-set**,或 **自增并返回increment-and-get** 的原子操作使它等价于共识问题【28】。因此共识问题与线性一致寄存器问题密切相关。
@@ -516,7 +519,7 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】它只考虑了
* 你可以通过在日志中追加一条消息然后读取日志直到该消息被读回才执行实际的读取操作。消息在日志中的位置因此定义了读取发生的时间点etcd 的法定人数读取有些类似这种情况【16】
* 如果日志允许以线性一致的方式获取最新日志消息的位置,则可以查询该位置,等待该位置前的所有消息都传达到你,然后执行读取。(这是 Zookeeper `sync()` 操作背后的思想【15】
* 你可以从同步更新的副本中进行读取因此可以确保结果是最新的这种技术用于链式复制chain replication【63】请参阅 “[关于复制的研究](ch5.md#关于复制的研究)”)。
* 你可以从同步更新的副本中进行读取因此可以确保结果是最新的这种技术用于链式复制chain replication【63】请参阅 “[关于复制的研究](/ch5#关于复制的研究)”)。
#### 使用线性一致性存储实现全序广播
@@ -539,17 +542,17 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】它只考虑了
**共识** 是分布式计算中最重要也是最基本的问题之一。从表面上看似乎很简单:非正式地讲,目标只是 **让几个节点达成一致get serveral nodes to agree on something**。你也许会认为这不会太难。不幸的是,许多出故障的系统都是因为错误地轻信这个问题很容易解决。
尽管共识非常重要,但关于它的内容出现在本书的后半部分,因为这个主题非常微妙,欣赏细微之处需要一些必要的知识。即使在学术界,对共识的理解也是在几十年的过程中逐渐沉淀而来,一路上也有着许多误解。现在我们已经讨论了复制([第五章](ch5.md)),事务([第七章](ch7.md)),系统模型([第八章](ch8.md)),线性一致以及全序广播(本章),我们终于准备好解决共识问题了。
尽管共识非常重要,但关于它的内容出现在本书的后半部分,因为这个主题非常微妙,欣赏细微之处需要一些必要的知识。即使在学术界,对共识的理解也是在几十年的过程中逐渐沉淀而来,一路上也有着许多误解。现在我们已经讨论了复制([第五章](/ch5)),事务([第七章](/ch7)),系统模型([第八章](/ch8)),线性一致以及全序广播(本章),我们终于准备好解决共识问题了。
节点能达成一致,在很多场景下都非常重要,例如:
* 领导选举
在单主复制的数据库中,所有节点需要就哪个节点是领导者达成一致。如果一些节点由于网络故障而无法与其他节点通信,则可能会对领导权的归属引起争议。在这种情况下,共识对于避免错误的故障切换非常重要。错误的故障切换会导致两个节点都认为自己是领导者(**脑裂**,请参阅 “[处理节点宕机](ch5.md#处理节点宕机)”)。如果有两个领导者,它们都会接受写入,它们的数据会发生分歧,从而导致不一致和数据丢失。
在单主复制的数据库中,所有节点需要就哪个节点是领导者达成一致。如果一些节点由于网络故障而无法与其他节点通信,则可能会对领导权的归属引起争议。在这种情况下,共识对于避免错误的故障切换非常重要。错误的故障切换会导致两个节点都认为自己是领导者(**脑裂**,请参阅 “[处理节点宕机](/ch5#处理节点宕机)”)。如果有两个领导者,它们都会接受写入,它们的数据会发生分歧,从而导致不一致和数据丢失。
* 原子提交
在支持跨多节点或跨多分区事务的数据库中,一个事务可能在某些节点上失败,但在其他节点上成功。如果我们想要维护事务的原子性(就 ACID 而言,请参阅 “[原子性](ch7.md#原子性)”),我们必须让所有节点对事务的结果达成一致:要么全部中止 / 回滚(如果出现任何错误),要么它们全部提交(如果没有出错)。这个共识的例子被称为 **原子提交atomic commit** 问题 [^xii]。
在支持跨多节点或跨多分区事务的数据库中,一个事务可能在某些节点上失败,但在其他节点上成功。如果我们想要维护事务的原子性(就 ACID 而言,请参阅 “[原子性](/ch7#原子性)”),我们必须让所有节点对事务的结果达成一致:要么全部中止 / 回滚(如果出现任何错误),要么它们全部提交(如果没有出错)。这个共识的例子被称为 **原子提交atomic commit** 问题 [^xii]。
[^xii]: 原子提交的形式化与共识稍有不同:原子事务只有在 **所有** 参与者投票提交的情况下才能提交,如果有任何参与者需要中止,则必须中止。共识则允许就 **任意一个** 被参与者提出的候选值达成一致。然而原子提交和共识可以相互简化为对方【70,71】。**非阻塞** 原子提交则要比共识更为困难 —— 请参阅 “[三阶段提交](#三阶段提交)”。
@@ -557,7 +560,7 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】它只考虑了
>
> 你可能已经听说过以作者 FischerLynch 和 Paterson 命名的 FLP 结果【68】它证明如果存在节点可能崩溃的风险则不存在 **总是** 能够达成共识的算法。在分布式系统中,我们必须假设节点可能会崩溃,所以可靠的共识是不可能的。然而这里我们正在讨论达成共识的算法,到底是怎么回事?
>
> 答案是 FLP 结果是在 **异步系统模型** 中被证明的(请参阅 “[系统模型与现实](ch8.md#系统模型与现实)”),而这是一种限制性很强的模型,它假定确定性算法不能使用任何时钟或超时。如果允许算法使用 **超时** 或其他方法来识别可疑的崩溃节点即使怀疑有时是错误的则共识变为一个可解的问题【67】。即使仅仅允许算法使用随机数也足以绕过这个不可能的结果【69】。
> 答案是 FLP 结果是在 **异步系统模型** 中被证明的(请参阅 “[系统模型与现实](/ch8#系统模型与现实)”),而这是一种限制性很强的模型,它假定确定性算法不能使用任何时钟或超时。如果允许算法使用 **超时** 或其他方法来识别可疑的崩溃节点即使怀疑有时是错误的则共识变为一个可解的问题【67】。即使仅仅允许算法使用随机数也足以绕过这个不可能的结果【69】。
>
> 因此,虽然 FLP 是关于共识不可能性的重要理论结果,但现实中的分布式系统通常是可以达成共识的。
@@ -568,17 +571,17 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】它只考虑了
### 原子提交与两阶段提交
在 [第七章](ch7.md) 中我们了解到,事务原子性的目的是在多次写操作中途出错的情况下,提供一种简单的语义。事务的结果要么是成功提交,在这种情况下,事务的所有写入都是持久化的;要么是中止,在这种情况下,事务的所有写入都被回滚(即撤消或丢弃)。
在 [第七章](/ch7) 中我们了解到,事务原子性的目的是在多次写操作中途出错的情况下,提供一种简单的语义。事务的结果要么是成功提交,在这种情况下,事务的所有写入都是持久化的;要么是中止,在这种情况下,事务的所有写入都被回滚(即撤消或丢弃)。
原子性可以防止失败的事务搅乱数据库,避免数据库陷入半成品结果和半更新状态。这对于多对象事务(请参阅 “[单对象和多对象操作](ch7.md#单对象和多对象操作)”)和维护次级索引的数据库尤其重要。每个次级索引都是与主数据相分离的数据结构 —— 因此,如果你修改了一些数据,则还需要在次级索引中进行相应的更改。原子性确保次级索引与主数据保持一致(如果索引与主数据不一致,就没什么用了)。
原子性可以防止失败的事务搅乱数据库,避免数据库陷入半成品结果和半更新状态。这对于多对象事务(请参阅 “[单对象和多对象操作](/ch7#单对象和多对象操作)”)和维护次级索引的数据库尤其重要。每个次级索引都是与主数据相分离的数据结构 —— 因此,如果你修改了一些数据,则还需要在次级索引中进行相应的更改。原子性确保次级索引与主数据保持一致(如果索引与主数据不一致,就没什么用了)。
#### 从单节点到分布式原子提交
对于在单个数据库节点执行的事务,原子性通常由存储引擎实现。当客户端请求数据库节点提交事务时,数据库将使事务的写入持久化(通常在预写式日志中,请参阅 “[让 B 树更可靠](ch3.md#让B树更可靠)”),然后将提交记录追加到磁盘中的日志里。如果数据库在这个过程中间崩溃,当节点重启时,事务会从日志中恢复:如果提交记录在崩溃之前成功地写入磁盘,则认为事务被提交;否则来自该事务的任何写入都被回滚。
对于在单个数据库节点执行的事务,原子性通常由存储引擎实现。当客户端请求数据库节点提交事务时,数据库将使事务的写入持久化(通常在预写式日志中,请参阅 “[让 B 树更可靠](/ch3#让B树更可靠)”),然后将提交记录追加到磁盘中的日志里。如果数据库在这个过程中间崩溃,当节点重启时,事务会从日志中恢复:如果提交记录在崩溃之前成功地写入磁盘,则认为事务被提交;否则来自该事务的任何写入都被回滚。
因此,在单个节点上,事务的提交主要取决于数据持久化落盘的 **顺序**首先是数据然后是提交记录【72】。事务提交或终止的关键决定时刻是磁盘完成写入提交记录的时刻在此之前仍有可能中止由于崩溃但在此之后事务已经提交即使数据库崩溃。因此是单一的设备连接到单个磁盘的控制器且挂载在单台机器上使得提交具有原子性。
但是,如果一个事务中涉及多个节点呢?例如,你也许在分区数据库中会有一个多对象事务,或者是一个按关键词分区的次级索引(其中索引条目可能位于与主数据不同的节点上;请参阅 “[分区与次级索引](ch6.md#分区与次级索引)”)。大多数 “NoSQL” 分布式数据存储不支持这种分布式事务,但是很多关系型数据库集群支持(请参阅 “[实践中的分布式事务](#实践中的分布式事务)”)。
但是,如果一个事务中涉及多个节点呢?例如,你也许在分区数据库中会有一个多对象事务,或者是一个按关键词分区的次级索引(其中索引条目可能位于与主数据不同的节点上;请参阅 “[分区与次级索引](/ch6#分区与次级索引)”)。大多数 “NoSQL” 分布式数据存储不支持这种分布式事务,但是很多关系型数据库集群支持(请参阅 “[实践中的分布式事务](#实践中的分布式事务)”)。
在这些情况下,仅向所有节点发送提交请求并独立提交每个节点的事务是不够的。这样很容易发生违反原子性的情况:提交在某些节点上成功,而在其他节点上失败:
@@ -586,9 +589,9 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】它只考虑了
* 某些提交请求可能在网络中丢失,最终由于超时而中止,而其他提交请求则通过。
* 在提交记录完全写入之前,某些节点可能会崩溃,并在恢复时回滚,而其他节点则成功提交。
如果某些节点提交了事务,但其他节点却放弃了这些事务,那么这些节点就会彼此不一致(如 [图 7-3](img/fig7-3.png) 所示)。而且一旦在某个节点上提交了一个事务,如果事后发现它在其它节点上被中止了,它是无法撤回的。出于这个原因,一旦确定事务中的所有其他节点也将提交,节点就必须进行提交。
如果某些节点提交了事务,但其他节点却放弃了这些事务,那么这些节点就会彼此不一致(如 [图 7-3](/img/fig7-3.png) 所示)。而且一旦在某个节点上提交了一个事务,如果事后发现它在其它节点上被中止了,它是无法撤回的。出于这个原因,一旦确定事务中的所有其他节点也将提交,节点就必须进行提交。
事务提交必须是不可撤销的 —— 事务提交之后,你不能改变主意,并追溯性地中止事务。这个规则的原因是,一旦数据被提交,其结果就对其他事务可见,因此其他客户端可能会开始依赖这些数据。这个原则构成了 **读已提交** 隔离等级的基础,在 “[读已提交](ch7.md#读已提交)” 一节中讨论了这个问题。如果一个事务在提交后被允许中止,所有那些读取了 **已提交却又被追溯声明不存在数据** 的事务也必须回滚。
事务提交必须是不可撤销的 —— 事务提交之后,你不能改变主意,并追溯性地中止事务。这个规则的原因是,一旦数据被提交,其结果就对其他事务可见,因此其他客户端可能会开始依赖这些数据。这个原则构成了 **读已提交** 隔离等级的基础,在 “[读已提交](/ch7#读已提交)” 一节中讨论了这个问题。如果一个事务在提交后被允许中止,所有那些读取了 **已提交却又被追溯声明不存在数据** 的事务也必须回滚。
提交事务的结果有可能通过事后执行另一个补偿事务compensating transaction来取消【73,74】但从数据库的角度来看这是一个单独的事务因此任何关于跨事务正确性的保证都是应用自己的问题。
@@ -596,15 +599,15 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】它只考虑了
**两阶段提交two-phase commit** 是一种用于实现跨多个节点的原子事务提交的算法即确保所有节点提交或所有节点中止。它是分布式数据库中的经典算法【13,35,75】。2PC 在某些数据库内部使用,也以 **XA 事务** 的形式对应用可用【76,77】例如 Java Transaction API 支持)或以 SOAP Web 服务的 `WS-AtomicTransaction` 形式提供给应用【78,79】。
[图 9-9](img/fig9-9.png) 说明了 2PC 的基本流程。2PC 中的提交 / 中止过程分为两个阶段(因此而得名),而不是单节点事务中的单个提交请求。
[图 9-9](/img/fig9-9.png) 说明了 2PC 的基本流程。2PC 中的提交 / 中止过程分为两个阶段(因此而得名),而不是单节点事务中的单个提交请求。
![](img/fig9-9.png)
![](/img/fig9-9.png)
**图 9-9 两阶段提交2PC的成功执行**
> #### 不要把2PC和2PL搞混了
>
> 两阶段提交2PC和两阶段锁定请参阅 “[两阶段锁定](ch7.md#两阶段锁定)”是两个完全不同的东西。2PC 在分布式数据库中提供原子提交,而 2PL 提供可串行化的隔离等级。为了避免混淆,最好把它们看作完全独立的概念,并忽略名称中不幸的相似性。
> 两阶段提交2PC和两阶段锁定请参阅 “[两阶段锁定](/ch7#两阶段锁定)”是两个完全不同的东西。2PC 在分布式数据库中提供原子提交,而 2PL 提供可串行化的隔离等级。为了避免混淆,最好把它们看作完全独立的概念,并忽略名称中不幸的相似性。
2PC 使用一个通常不会出现在单节点事务中的新组件:**协调者**coordinator也称为 **事务管理器**,即 transaction manager。协调者通常在请求事务的相同应用进程中以库的形式实现例如嵌入在 Java EE 容器中),但也可以是单独的进程或服务。这种协调者的例子包括 Narayana、JOTM、BTM 或 MSDTC。
@@ -638,9 +641,9 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】它只考虑了
如果协调者在发送 **准备** 请求之前失败,参与者可以安全地中止事务。但是,一旦参与者收到了准备请求并投了 “是”,就不能再单方面放弃 —— 必须等待协调者回答事务是否已经提交或中止。如果此时协调者崩溃或网络出现故障,参与者什么也做不了只能等待。参与者的这种事务状态称为 **存疑in doubt** 的或 **不确定uncertain** 的。
情况如 [图 9-10](img/fig9-10.png) 所示。在这个特定的例子中,协调者实际上决定提交,数据库 2 收到提交请求。但是,协调者在将提交请求发送到数据库 1 之前发生崩溃,因此数据库 1 不知道是否提交或中止。即使 **超时** 在这里也没有帮助:如果数据库 1 在超时后单方面中止,它将最终与执行提交的数据库 2 不一致。同样,单方面提交也是不安全的,因为另一个参与者可能已经中止了。
情况如 [图 9-10](/img/fig9-10.png) 所示。在这个特定的例子中,协调者实际上决定提交,数据库 2 收到提交请求。但是,协调者在将提交请求发送到数据库 1 之前发生崩溃,因此数据库 1 不知道是否提交或中止。即使 **超时** 在这里也没有帮助:如果数据库 1 在超时后单方面中止,它将最终与执行提交的数据库 2 不一致。同样,单方面提交也是不安全的,因为另一个参与者可能已经中止了。
![](img/fig9-10.png)
![](/img/fig9-10.png)
**图 9-10 参与者投赞成票后,协调者崩溃。数据库 1 不知道是否提交或中止**
@@ -652,7 +655,7 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】它只考虑了
两阶段提交被称为 **阻塞blocking**- 原子提交协议,因为存在 2PC 可能卡住并等待协调者恢复的情况。理论上,可以使一个原子提交协议变为 **非阻塞nonblocking** 的,以便在节点失败时不会卡住。但是让这个协议能在实践中工作并没有那么简单。
作为 2PC 的替代方案,已经提出了一种称为 **三阶段提交3PC** 的算法【13,80】。然而3PC 假定网络延迟有界,节点响应时间有限;在大多数具有无限网络延迟和进程暂停的实际系统中(见 [第八章](ch8.md)),它并不能保证原子性。
作为 2PC 的替代方案,已经提出了一种称为 **三阶段提交3PC** 的算法【13,80】。然而3PC 假定网络延迟有界,节点响应时间有限;在大多数具有无限网络延迟和进程暂停的实际系统中(见 [第八章](/ch8)),它并不能保证原子性。
通常,非阻塞原子提交需要一个 **完美的故障检测器perfect failure detector**【67,71】—— 即一个可靠的机制来判断一个节点是否已经崩溃。在具有无限延迟的网络中超时并不是一种可靠的故障检测机制因为即使没有节点崩溃请求也可能由于网络问题而超时。出于这个原因2PC 仍然被使用,尽管大家都清楚可能存在协调者故障的问题。
@@ -683,7 +686,7 @@ CAP 定理的正式定义仅限于很狭隘的范围【30】它只考虑了
然而,只有当所有受事务影响的系统都使用同样的 **原子提交协议atomic commit protocol** 时,这样的分布式事务才是可能的。例如,假设处理消息的副作用是发送一封邮件,而邮件服务器并不支持两阶段提交:如果消息处理失败并重试,则可能会发送两次或更多次的邮件。但如果处理消息的所有副作用都可以在事务中止时回滚,那么这样的处理流程就可以安全地重试,就好像什么都没有发生过一样。
在 [第十一章](ch11.md) 中将再次回到 “恰好一次” 消息处理的主题。让我们先来看看允许这种异构分布式事务的原子提交协议。
在 [第十一章](/ch11) 中将再次回到 “恰好一次” 消息处理的主题。让我们先来看看允许这种异构分布式事务的原子提交协议。
#### XA事务
@@ -701,9 +704,9 @@ XA 假定你的应用使用网络驱动或客户端库来与 **参与者**(数
为什么我们这么关心存疑事务?系统的其他部分就不能继续正常工作,无视那些终将被清理的存疑事务吗?
问题在于 **锁locking**。正如在 “[读已提交](ch7.md#读已提交)” 中所讨论的那样,数据库事务通常获取待修改的行上的 **行级排他锁**,以防止脏写。此外,如果要使用可串行化的隔离等级,则使用两阶段锁定的数据库也必须为事务所读取的行加上共享锁(请参阅 “[两阶段锁定](ch7.md#两阶段锁定)”)。
问题在于 **锁locking**。正如在 “[读已提交](/ch7#读已提交)” 中所讨论的那样,数据库事务通常获取待修改的行上的 **行级排他锁**,以防止脏写。此外,如果要使用可串行化的隔离等级,则使用两阶段锁定的数据库也必须为事务所读取的行加上共享锁(请参阅 “[两阶段锁定](/ch7#两阶段锁定)”)。
在事务提交或中止之前,数据库不能释放这些锁(如 [图 9-9](img/fig9-9.png) 中的阴影区域所示)。因此,在使用两阶段提交时,事务必须在整个存疑期间持有这些锁。如果协调者已经崩溃,需要 20 分钟才能重启,那么这些锁将会被持有 20 分钟。如果协调者的日志由于某种原因彻底丢失,这些锁将被永久持有 —— 或至少在管理员手动解决该情况之前。
在事务提交或中止之前,数据库不能释放这些锁(如 [图 9-9](/img/fig9-9.png) 中的阴影区域所示)。因此,在使用两阶段提交时,事务必须在整个存疑期间持有这些锁。如果协调者已经崩溃,需要 20 分钟才能重启,那么这些锁将会被持有 20 分钟。如果协调者的日志由于某种原因彻底丢失,这些锁将被永久持有 —— 或至少在管理员手动解决该情况之前。
当这些锁被持有时,其他事务不能修改这些行。根据数据库的不同,其他事务甚至可能因为读取这些行而被阻塞。因此,其他事务没法儿简单地继续它们的业务了 —— 如果它们要访问同样的数据,就会被阻塞。这可能会导致应用大面积进入不可用状态,直到存疑事务被解决。
@@ -723,10 +726,10 @@ XA 事务解决了保持多个参与者(数据系统)相互一致的现实
* 如果协调者没有复制,而是只在单台机器上运行,那么它是整个系统的失效单点(因为它的失效会导致其他应用服务器阻塞在存疑事务持有的锁上)。令人惊讶的是,许多协调者实现默认情况下并不是高可用的,或者只有基本的复制支持。
* 许多服务器端应用都是使用无状态模式开发的(受 HTTP 的青睐),所有持久状态都存储在数据库中,因此具有应用服务器可随意按需添加删除的优点。但是,当协调者成为应用服务器的一部分时,它会改变部署的性质。突然间,协调者的日志成为持久系统状态的关键部分 —— 与数据库本身一样重要,因为协调者日志是为了在崩溃后恢复存疑事务所必需的。这样的应用服务器不再是无状态的了。
* 由于 XA 需要兼容各种数据系统,因此它必须是所有系统的最小公分母。例如,它不能检测不同系统间的死锁(因为这将需要一个标准协议来让系统交换每个事务正在等待的锁的信息),而且它无法与 SSI请参阅 [可串行化快照隔离](ch7.md#可串行化快照隔离))协同工作,因为这需要一个跨系统定位冲突的协议。
* 由于 XA 需要兼容各种数据系统,因此它必须是所有系统的最小公分母。例如,它不能检测不同系统间的死锁(因为这将需要一个标准协议来让系统交换每个事务正在等待的锁的信息),而且它无法与 SSI请参阅 [可串行化快照隔离](/ch7#可串行化快照隔离))协同工作,因为这需要一个跨系统定位冲突的协议。
* 对于数据库内部的分布式事务(不是 XA限制没有这么大 —— 例如,分布式版本的 SSI 是可能的。然而仍然存在问题2PC 成功提交一个事务需要所有参与者的响应。因此,如果系统的 **任何** 部分损坏,事务也会失败。因此,分布式事务又有 **扩大失效amplifying failures** 的趋势,这又与我们构建容错系统的目标背道而驰。
这些事实是否意味着我们应该放弃保持几个系统相互一致的所有希望?不完全是 —— 还有其他的办法,可以让我们在没有异构分布式事务的痛苦的情况下实现同样的事情。我们将在 [第十一章](ch11.md) 和 [第十二章](ch12.md) 回到这些话题。但首先,我们应该概括一下关于 **共识** 的话题。
这些事实是否意味着我们应该放弃保持几个系统相互一致的所有希望?不完全是 —— 还有其他的办法,可以让我们在没有异构分布式事务的痛苦的情况下实现同样的事情。我们将在 [第十一章](/ch11) 和 [第十二章](/ch12) 回到这些话题。但首先,我们应该概括一下关于 **共识** 的话题。
### 容错共识
@@ -759,15 +762,15 @@ XA 事务解决了保持多个参与者(数据系统)相互一致的现实
如果你不关心容错,那么满足前三个属性很容易:你可以将一个节点硬编码为 “独裁者”,并让该节点做出所有的决定。但如果该节点失效,那么系统就无法再做出任何决定。事实上,这就是我们在两阶段提交的情况中所看到的:如果协调者失效,那么存疑的参与者就无法决定提交还是中止。
**终止** 属性形式化了容错的思想。它实质上说的是,一个共识算法不能简单地永远闲坐着等死 —— 换句话说,它必须取得进展。即使部分节点出现故障,其他节点也必须达成一项决定(**终止** 是一种 **活性属性**,而另外三种是 **安全属性** —— 请参阅 “[安全性和活性](ch8.md#安全性和活性)”)。
**终止** 属性形式化了容错的思想。它实质上说的是,一个共识算法不能简单地永远闲坐着等死 —— 换句话说,它必须取得进展。即使部分节点出现故障,其他节点也必须达成一项决定(**终止** 是一种 **活性属性**,而另外三种是 **安全属性** —— 请参阅 “[安全性和活性](/ch8#安全性和活性)”)。
共识的系统模型假设,当一个节点 “崩溃” 时,它会突然消失而且永远不会回来。(不像软件崩溃,想象一下地震,包含你的节点的数据中心被山体滑坡所摧毁,你必须假设节点被埋在 30 英尺以下的泥土中,并且永远不会重新上线)在这个系统模型中,任何需要等待节点恢复的算法都不能满足 **终止** 属性。特别是2PC 不符合终止属性的要求。
当然如果 **所有** 的节点都崩溃了,没有一个在运行,那么所有算法都不可能决定任何事情。算法可以容忍的失效数量是有限的:事实上可以证明,任何共识算法都需要至少占总体 **多数majority** 的节点正确工作以确保终止属性【67】。多数可以安全地组成法定人数请参阅 “[读写的法定人数](ch5.md#读写的法定人数)”)。
当然如果 **所有** 的节点都崩溃了,没有一个在运行,那么所有算法都不可能决定任何事情。算法可以容忍的失效数量是有限的:事实上可以证明,任何共识算法都需要至少占总体 **多数majority** 的节点正确工作以确保终止属性【67】。多数可以安全地组成法定人数请参阅 “[读写的法定人数](/ch5#读写的法定人数)”)。
因此 **终止** 属性取决于一个假设,**不超过一半的节点崩溃或不可达**。然而即使多数节点出现故障或存在严重的网络问题,绝大多数共识的实现都能始终确保安全属性得到满足 —— 一致同意完整性和有效性【92】。因此大规模的中断可能会阻止系统处理请求但是它不能通过使系统做出无效的决定来破坏共识系统。
大多数共识算法假设不存在 **拜占庭式错误**,正如在 “[拜占庭故障](ch8.md#拜占庭故障)” 一节中所讨论的那样。也就是说如果一个节点没有正确地遵循协议例如如果它向不同节点发送矛盾的消息它就可能会破坏协议的安全属性。克服拜占庭故障稳健地达成共识是可能的只要少于三分之一的节点存在拜占庭故障【25,93】。但我们没有地方在本书中详细讨论这些算法了。
大多数共识算法假设不存在 **拜占庭式错误**,正如在 “[拜占庭故障](/ch8#拜占庭故障)” 一节中所讨论的那样。也就是说如果一个节点没有正确地遵循协议例如如果它向不同节点发送矛盾的消息它就可能会破坏协议的安全属性。克服拜占庭故障稳健地达成共识是可能的只要少于三分之一的节点存在拜占庭故障【25,93】。但我们没有地方在本书中详细讨论这些算法了。
#### 共识算法和全序广播
@@ -788,11 +791,11 @@ XA 事务解决了保持多个参与者(数据系统)相互一致的现实
#### 单主复制与共识
在 [第五章](ch5.md) 中,我们讨论了单主复制(请参阅 “[领导者与追随者](ch5.md#领导者与追随者)”),它将所有的写入操作都交给主库,并以相同的顺序将它们应用到从库,从而使副本保持在最新状态。这实际上不就是一个全序广播吗?为什么我们在 [第五章](ch5.md) 里一点都没担心过共识问题呢?
在 [第五章](/ch5) 中,我们讨论了单主复制(请参阅 “[领导者与追随者](/ch5#领导者与追随者)”),它将所有的写入操作都交给主库,并以相同的顺序将它们应用到从库,从而使副本保持在最新状态。这实际上不就是一个全序广播吗?为什么我们在 [第五章](/ch5) 里一点都没担心过共识问题呢?
答案取决于如何选择领导者。如果主库是由运维人员手动选择和配置的,那么你实际上拥有一种 **独裁类型** 的 “共识算法”:只有一个节点被允许接受写入(即决定写入复制日志的顺序),如果该节点发生故障,则系统将无法写入,直到运维手动配置其他节点作为主库。这样的系统在实践中可以表现良好,但它无法满足共识的 **终止** 属性,因为它需要人为干预才能取得 **进展**
一些数据库会自动执行领导者选举和故障切换,如果旧主库失效,会提拔一个从库为新主库(请参阅 “[处理节点宕机](ch5.md#处理节点宕机)”)。这使我们向容错的全序广播更进一步,从而达成共识。
一些数据库会自动执行领导者选举和故障切换,如果旧主库失效,会提拔一个从库为新主库(请参阅 “[处理节点宕机](/ch5#处理节点宕机)”)。这使我们向容错的全序广播更进一步,从而达成共识。
但是还有一个问题。我们之前曾经讨论过脑裂的问题,并且说过所有的节点都需要同意是谁领导,否则两个不同的节点都会认为自己是领导者,从而导致数据库进入不一致的状态。因此,选出一位领导者需要共识。但如果这里描述的共识算法实际上是全序广播算法,并且全序广播就像单主复制,而单主复制需要一个领导者,那么...
@@ -804,9 +807,9 @@ XA 事务解决了保持多个参与者(数据系统)相互一致的现实
每次当现任领导被认为挂掉的时候,节点间就会开始一场投票,以选出一个新领导。这次选举被赋予一个递增的纪元编号,因此纪元编号是全序且单调递增的。如果两个不同的时代的领导者之间出现冲突(也许是因为前任领导者实际上并未死亡),那么带有更高纪元编号的领导说了算。
在任何领导者被允许决定任何事情之前,必须先检查是否存在其他带有更高纪元编号的领导者,它们可能会做出相互冲突的决定。领导者如何知道自己没有被另一个节点赶下台?回想一下在 “[真相由多数所定义](ch8.md#真相由多数所定义)” 中提到的:一个节点不一定能相信自己的判断 —— 因为只有节点自己认为自己是领导者,并不一定意味着其他节点接受它作为它们的领导者。
在任何领导者被允许决定任何事情之前,必须先检查是否存在其他带有更高纪元编号的领导者,它们可能会做出相互冲突的决定。领导者如何知道自己没有被另一个节点赶下台?回想一下在 “[真相由多数所定义](/ch8#真相由多数所定义)” 中提到的:一个节点不一定能相信自己的判断 —— 因为只有节点自己认为自己是领导者,并不一定意味着其他节点接受它作为它们的领导者。
相反,它必须从 **法定人数quorum** 的节点中获取选票(请参阅 “[读写的法定人数](ch5.md#读写的法定人数)”。对领导者想要做出的每一个决定都必须将提议值发送给其他节点并等待法定人数的节点响应并赞成提案。法定人数通常但不总是由多数节点组成【105】。只有在没有意识到任何带有更高纪元编号的领导者的情况下一个节点才会投票赞成提议。
相反,它必须从 **法定人数quorum** 的节点中获取选票(请参阅 “[读写的法定人数](/ch5#读写的法定人数)”。对领导者想要做出的每一个决定都必须将提议值发送给其他节点并等待法定人数的节点响应并赞成提案。法定人数通常但不总是由多数节点组成【105】。只有在没有意识到任何带有更高纪元编号的领导者的情况下一个节点才会投票赞成提议。
因此,我们有两轮投票:第一次是为了选出一位领导者,第二次是对领导者的提议进行表决。关键的洞察在于,这两次投票的 **法定人群** 必须相互 **重叠overlap**如果一个提案的表决通过则至少得有一个参与投票的节点也必须参加过最近的领导者选举【105】。因此如果在一个提案的表决过程中没有出现更高的纪元编号。那么现任领导者就可以得出这样的结论没有发生过更高时代的领导选举因此可以确定自己仍然在领导。然后它就可以安全地对提议值做出决定。
@@ -818,7 +821,7 @@ XA 事务解决了保持多个参与者(数据系统)相互一致的现实
尽管如此,它们并不是在所有地方都用上了,因为好处总是有代价的。
节点在做出决定之前对提议进行投票的过程是一种同步复制。如 “[同步复制与异步复制](ch5.md#同步复制与异步复制)” 中所述,通常数据库会配置为异步复制模式。在这种配置中发生故障切换时,一些已经提交的数据可能会丢失 —— 但是为了获得更好的性能,许多人选择接受这种风险。
节点在做出决定之前对提议进行投票的过程是一种同步复制。如 “[同步复制与异步复制](/ch5#同步复制与异步复制)” 中所述,通常数据库会配置为异步复制模式。在这种配置中发生故障切换时,一些已经提交的数据可能会丢失 —— 但是为了获得更好的性能,许多人选择接受这种风险。
共识系统总是需要严格多数来运转。这意味着你至少需要三个节点才能容忍单节点故障(其余两个构成多数),或者至少有五个节点来容忍两个节点发生故障(其余三个构成多数)。如果网络故障切断了某些节点同其他节点的连接,则只有多数节点所在的网络可以继续工作,其余部分将被阻塞(请参阅 “[线性一致性的代价](#线性一致性的代价)”)。
@@ -840,11 +843,11 @@ ZooKeeper 模仿了 Google 的 Chubby 锁服务【14,98】不仅实现了全
* 线性一致性的原子操作
使用原子 CAS 操作可以实现锁:如果多个节点同时尝试执行相同的操作,只有一个节点会成功。共识协议保证了操作的原子性和线性一致性,即使节点发生故障或网络在任意时刻中断。分布式锁通常以 **租约lease** 的形式实现,租约有一个到期时间,以便在客户端失效的情况下最终能被释放(请参阅 “[进程暂停](ch8.md#进程暂停)”)。
使用原子 CAS 操作可以实现锁:如果多个节点同时尝试执行相同的操作,只有一个节点会成功。共识协议保证了操作的原子性和线性一致性,即使节点发生故障或网络在任意时刻中断。分布式锁通常以 **租约lease** 的形式实现,租约有一个到期时间,以便在客户端失效的情况下最终能被释放(请参阅 “[进程暂停](/ch8#进程暂停)”)。
* 操作的全序排序
如 “[领导者和锁](ch8.md#领导者和锁)” 中所述当某个资源受到锁或租约的保护时你需要一个防护令牌来防止客户端在进程暂停的情况下彼此冲突。防护令牌是每次锁被获取时单调增加的数字。ZooKeeper 通过全序化所有操作来提供这个功能,它为每个操作提供一个单调递增的事务 ID`zxid`)和版本号(`cversion`【15】。
如 “[领导者和锁](/ch8#领导者和锁)” 中所述当某个资源受到锁或租约的保护时你需要一个防护令牌来防止客户端在进程暂停的情况下彼此冲突。防护令牌是每次锁被获取时单调增加的数字。ZooKeeper 通过全序化所有操作来提供这个功能,它为每个操作提供一个单调递增的事务 ID`zxid`)和版本号(`cversion`【15】。
* 失效检测
@@ -860,7 +863,7 @@ ZooKeeper 模仿了 Google 的 Chubby 锁服务【14,98】不仅实现了全
ZooKeeper/Chubby 模型运行良好的一个例子是,如果你有几个进程实例或服务,需要选择其中一个实例作为主库或首选服务。如果领导者失败,其他节点之一应该接管。这对单主数据库当然非常实用,但对作业调度程序和类似的有状态系统也很好用。
另一个例子是,当你有一些分区资源(数据库、消息流、文件存储、分布式 Actor 系统等),并需要决定将哪个分区分配给哪个节点时。当新节点加入集群时,需要将某些分区从现有节点移动到新节点,以便重新平衡负载(请参阅 “[分区再平衡](ch6.md#分区再平衡)”)。当节点被移除或失效时,其他节点需要接管失效节点的工作。
另一个例子是,当你有一些分区资源(数据库、消息流、文件存储、分布式 Actor 系统等),并需要决定将哪个分区分配给哪个节点时。当新节点加入集群时,需要将某些分区从现有节点移动到新节点,以便重新平衡负载(请参阅 “[分区再平衡](/ch6#分区再平衡)”)。当节点被移除或失效时,其他节点需要接管失效节点的工作。
这类任务可以通过在 ZooKeeper 中明智地使用原子操作,临时节点与通知来实现。如果设计得当,这种方法允许应用自动从故障中恢复而无需人工干预。不过这并不容易,尽管已经有不少在 ZooKeeper 客户端 API 基础之上提供更高层工具的库,例如 Apache Curator 【17】。但它仍然要比尝试从头实现必要的共识算法要好得多这样的尝试鲜有成功记录【107】。
@@ -880,7 +883,7 @@ ZooKeeper、etcd 和 Consul 也经常用于服务发现 —— 也就是找出
ZooKeeper 和它的小伙伴们可以看作是成员资格服务membership services研究的悠久历史的一部分这个历史可以追溯到 20 世纪 80 年代并且对建立高度可靠的系统例如空中交通管制非常重要【110】。
成员资格服务确定哪些节点当前处于活动状态并且是集群的活动成员。正如我们在 [第八章](ch8.md) 中看到的那样,由于无限的网络延迟,无法可靠地检测到另一个节点是否发生故障。但是,如果你通过共识来进行故障检测,那么节点可以就哪些节点应该被认为是存在或不存在达成一致。
成员资格服务确定哪些节点当前处于活动状态并且是集群的活动成员。正如我们在 [第八章](/ch8) 中看到的那样,由于无限的网络延迟,无法可靠地检测到另一个节点是否发生故障。但是,如果你通过共识来进行故障检测,那么节点可以就哪些节点应该被认为是存在或不存在达成一致。
即使它确实存在,仍然可能发生一个节点被共识错误地宣告死亡。但是对于一个系统来说,知道哪些节点构成了当前的成员关系是非常有用的。例如,选择领导者可能意味着简单地选择当前成员中编号最小的成员,但如果不同的节点对现有的成员都有谁有不同意见,则这种方法将不起作用。
@@ -929,13 +932,13 @@ ZooKeeper 和它的小伙伴们可以看作是成员资格服务membership se
尽管单领导者数据库可以提供线性一致性,且无需对每个写操作都执行共识算法,但共识对于保持及变更领导权仍然是必须的。因此从某种意义上说,使用单个领导者不过是 “缓兵之计”:共识仍然是需要的,只是在另一个地方,而且没那么频繁。好消息是,容错的共识算法与容错的共识系统是存在的,我们在本章中简要地讨论了它们。
像 ZooKeeper 这样的工具为应用提供了 “外包” 的共识、故障检测和成员服务。它们扮演了重要的角色,虽说使用不易,但总比自己去开发一个能经受 [第八章](ch8.md) 中所有问题考验的算法要好得多。如果你发现自己想要解决的问题可以归结为共识,并且希望它能容错,使用一个类似 ZooKeeper 的东西是明智之举。
像 ZooKeeper 这样的工具为应用提供了 “外包” 的共识、故障检测和成员服务。它们扮演了重要的角色,虽说使用不易,但总比自己去开发一个能经受 [第八章](/ch8) 中所有问题考验的算法要好得多。如果你发现自己想要解决的问题可以归结为共识,并且希望它能容错,使用一个类似 ZooKeeper 的东西是明智之举。
尽管如此,并不是所有系统都需要共识:例如,无领导者复制和多领导者复制系统通常不会使用全局的共识。这些系统中出现的冲突(请参阅 “[处理写入冲突](ch5.md#处理写入冲突)”)正是不同领导者之间没有达成共识的结果,但这也许并没有关系:也许我们只是需要接受没有线性一致性的事实,并学会更好地与具有分支与合并版本历史的数据打交道。
尽管如此,并不是所有系统都需要共识:例如,无领导者复制和多领导者复制系统通常不会使用全局的共识。这些系统中出现的冲突(请参阅 “[处理写入冲突](/ch5#处理写入冲突)”)正是不同领导者之间没有达成共识的结果,但这也许并没有关系:也许我们只是需要接受没有线性一致性的事实,并学会更好地与具有分支与合并版本历史的数据打交道。
本章引用了大量关于分布式系统理论的研究。虽然理论论文和证明并不总是容易理解,有时也会做出不切实际的假设,但它们对于指导这一领域的实践有着极其重要的价值:它们帮助我们推理什么可以做,什么不可以做,帮助我们找到反直觉的分布式系统缺陷。如果你有时间,这些参考资料值得探索。
这里已经到了本书 [第二部分](part-ii.md) 的末尾,第二部介绍了复制([第五章](ch5.md))、分区([第六章](ch6.md))、事务([第七章](ch7.md))、分布式系统的故障模型([第八章](ch8.md))以及最后的一致性与共识([第九章](ch9.md))。现在我们已经奠定了扎实的理论基础,我们将在 [第三部分](part-iii.md) 再次转向更实际的系统,并讨论如何使用异构的组件积木块构建强大的应用。
这里已经到了本书 [第二部分](/part-ii) 的末尾,第二部介绍了复制([第五章](/ch5))、分区([第六章](/ch6))、事务([第七章](/ch7))、分布式系统的故障模型([第八章](/ch8))以及最后的一致性与共识([第九章](/ch9))。现在我们已经奠定了扎实的理论基础,我们将在 [第三部分](/part-iii) 再次转向更实际的系统,并讨论如何使用异构的组件积木块构建强大的应用。
## 参考文献
@@ -1050,11 +1053,3 @@ ZooKeeper 和它的小伙伴们可以看作是成员资格服务membership se
1. Ivan Kelly: “[BookKeeper Tutorial](https://github.com/ivankelly/bookkeeper-tutorial),” *github.com*, October 2014.
1. Camille Fournier: “[Consensus Systems for the Skeptical Architect](https://vimeo.com/102667163),” at *Philly ETE*, Philadelphia, PA, USA, April 2014.
1. Kenneth P. Birman: “[A History of the Virtual Synchrony Replication Model](https://ptolemy.berkeley.edu/projects/truststc/pubs/713/History%20of%20the%20Virtual%20Synchrony%20Replication%20Model%202010.pdf),” in *Replication: Theory and Practice*, Springer LNCS volume 5959, chapter 6, pages 91120, 2010. ISBN: 978-3-642-11293-5, [doi:10.1007/978-3-642-11294-2_6](http://dx.doi.org/10.1007/978-3-642-11294-2_6)
------
| 上一章 | 目录 | 下一章 |
| ---------------------------------- | ------------------------------- | --------------------------------- |
| [第八章:分布式系统的麻烦](ch8.md) | [设计数据密集型应用](README.md) | [第三部分:衍生数据](part-iii.md) |

View File

@@ -1,4 +1,8 @@
# 后记
---
title: 后记
weight: 600
breadcrumbs: false
---
## 关于作者
@@ -11,11 +15,13 @@ Martin 是一位常规会议演讲者,博主和开源贡献者。他认为,
## 关于译者
[冯若航](https://vonng.com/about)
[**冯若航**](https://vonng.com),网名 [@Vonng](https://github.com/Vonng)
PostgreSQL 专家,数据库老司机,云计算泥石流。
PostgreSQL 发行版 [**Pigsty**](https://pgsty.com) 作者与创始人。
架构师DBA全栈工程师 @ TanTanAlibabaApple。
独立开源贡献者,[GitStar Ranking 585](https://gitstar-ranking.com/Vonng)[国区活跃 Top20](https://committers.top/china)。
[DDIA](https://ddia.pigsty.io) / [PG Internal](https://pgint.vonng.com) 中文版译者,数据库/云计算 KOL。
PostgreSQL DBA @ TanTan
Alibaba+-Finplus 架构师/全栈工程师 (2015 ~ 2017)
## 后记

232
content/zh/contrib.md Normal file
View File

@@ -0,0 +1,232 @@
---
title: 贡献者
weight: 800
breadcrumbs: false
---
## 译者
[**冯若航**](https://vonng.com),网名 [@Vonng](https://github.com/Vonng)。
PostgreSQL 专家,数据库老司机,云计算泥石流。
[**Pigsty**](https://pgsty.com) 作者与创始人。
架构师DBA全栈工程师 @ TanTanAlibabaApple。
独立开源贡献者,[GitStar Ranking 585](https://gitstar-ranking.com/Vonng)[国区活跃 Top20](https://committers.top/china)。
[DDIA](https://ddia.pigsty.io) / [PG Internal](https://pgint.vonng.com) 中文版译者,公众号:《老冯云数》,数据库 KOL。
## 校订与维护
YinGang [@yingang](https://github.com/yingang) 对本书进行了全文校订,并持续维护。
## 繁体中文版本
[繁體中文](/tw) **版本维护** by [@afunTW](https://github.com/afunTW)
## 贡献列表
[GitHub 贡献者列表](https://github.com/Vonng/ddia/graphs/contributors)
0. 全文校订 by [@yingang](https://github.com/Vonng/ddia/commits?author=yingang)
1. [序言初翻修正](https://github.com/Vonng/ddia/commit/afb5edab55c62ed23474149f229677e3b42dfc2c) by [@seagullbird](https://github.com/Vonng/ddia/commits?author=seagullbird)
2. [第一章语法标点校正](https://github.com/Vonng/ddia/commit/973b12cd8f8fcdf4852f1eb1649ddd9d187e3644) by [@nevertiree](https://github.com/Vonng/ddia/commits?author=nevertiree)
3. [第六章部分校正](https://github.com/Vonng/ddia/commit/d4eb0852c0ec1e93c8aacc496c80b915bb1e6d48) 与[第十章的初翻](https://github.com/Vonng/ddia/commit/9de8dbd1bfe6fbb03b3bf6c1a1aa2291aed2490e) by [@MuAlex](https://github.com/Vonng/ddia/commits?author=MuAlex)
4. [第一部分](/part-i)前言,[ch2](/ch2)校正 by [@jiajiadebug](https://github.com/Vonng/ddia/commits?author=jiajiadebug)
5. [词汇表](/glossary)、[后记](/colophon)关于野猪的部分 by [@Chowss](https://github.com/Vonng/ddia/commits?author=Chowss)
6. [繁體中文](https://github.com/Vonng/ddia/pulls)版本与转换脚本 by [@afunTW](https://github.com/afunTW)
7. 多处翻译修正 by [@songzhibin97](https://github.com/Vonng/ddia/commits?author=songzhibin97) [@MamaShip](https://github.com/Vonng/ddia/commits?author=MamaShip) [@FangYuan33](https://github.com/Vonng/ddia/commits?author=FangYuan33)
感谢所有提出意见,作出贡献的朋友们,您可以在这里找到所有贡献的 [Issue 列表](https://github.com/Vonng/ddia/issues) 与 [PR 列表](https://github.com/Vonng/ddia/pulls)
| ISSUE & Pull Requests | USER | Title |
|-------------------------------------------------|------------------------------------------------------------|----------------------------------------------------------------|
| [359](https://github.com/Vonng/ddia/pull/359) | [@c25423](https://github.com/c25423) | ch10: 修正一处拼写错误 |
| [358](https://github.com/Vonng/ddia/pull/358) | [@lewiszlw](https://github.com/lewiszlw) | ch4: 修正一处拼写错误 |
| [356](https://github.com/Vonng/ddia/pull/356) | [@lewiszlw](https://github.com/lewiszlw) | ch2: 修正一处标点错误 |
| [355](https://github.com/Vonng/ddia/pull/355) | [@DuroyGeorge](https://github.com/DuroyGeorge) | ch12: 修正一处格式错误 |
| [354](https://github.com/Vonng/ddia/pull/354) | [@justlorain](https://github.com/justlorain) | ch7: 修正一处参考链接 |
| [353](https://github.com/Vonng/ddia/pull/353) | [@fantasyczl](https://github.com/fantasyczl) | ch3&9: 修正两处引用错误 |
| [352](https://github.com/Vonng/ddia/pull/352) | [@fantasyczl](https://github.com/fantasyczl) | 支持输出为 EPUB 格式 |
| [349](https://github.com/Vonng/ddia/pull/349) | [@xiyihan0](https://github.com/xiyihan0) | ch1: 修正一处格式错误 |
| [348](https://github.com/Vonng/ddia/pull/348) | [@omegaatt36](https://github.com/omegaatt36) | ch3: 修正一处图像链接 |
| [346](https://github.com/Vonng/ddia/issues/346) | [@Vermouth1995](https://github.com/Vermouth1995) | ch1: 优化一处翻译 |
| [343](https://github.com/Vonng/ddia/pull/343) | [@kehao-chen](https://github.com/kehao-chen) | ch10: 优化一处翻译 |
| [341](https://github.com/Vonng/ddia/pull/341) | [@YKIsTheBest](https://github.com/YKIsTheBest) | ch3: 优化两处翻译 |
| [340](https://github.com/Vonng/ddia/pull/340) | [@YKIsTheBest](https://github.com/YKIsTheBest) | ch2: 优化多处翻译 |
| [338](https://github.com/Vonng/ddia/pull/338) | [@YKIsTheBest](https://github.com/YKIsTheBest) | ch1: 优化一处翻译 |
| [335](https://github.com/Vonng/ddia/pull/335) | [@kimi0230](https://github.com/kimi0230) | 修正一处繁体中文错误 |
| [334](https://github.com/Vonng/ddia/pull/334) | [@soulrrrrr](https://github.com/soulrrrrr) | ch2: 修正一处繁体中文错误 |
| [332](https://github.com/Vonng/ddia/pull/332) | [@justlorain](https://github.com/justlorain) | ch5: 修正一处翻译错误 |
| [331](https://github.com/Vonng/ddia/pull/331) | [@Lyianu](https://github.com/Lyianu) | ch9: 更正几处拼写错误 |
| [330](https://github.com/Vonng/ddia/pull/330) | [@Lyianu](https://github.com/Lyianu) | ch7: 优化一处翻译 |
| [329](https://github.com/Vonng/ddia/issues/329) | [@Lyianu](https://github.com/Lyianu) | ch6: 指出一处翻译错误 |
| [328](https://github.com/Vonng/ddia/pull/328) | [@justlorain](https://github.com/justlorain) | ch4: 更正一处翻译遗漏 |
| [326](https://github.com/Vonng/ddia/pull/326) | [@liangGTY](https://github.com/liangGTY) | ch1: 优化一处翻译 |
| [323](https://github.com/Vonng/ddia/pull/323) | [@marvin263](https://github.com/marvin263) | ch5: 优化一处翻译 |
| [322](https://github.com/Vonng/ddia/pull/322) | [@marvin263](https://github.com/marvin263) | ch8: 优化一处翻译 |
| [304](https://github.com/Vonng/ddia/pull/304) | [@spike014](https://github.com/spike014) | ch11: 优化一处翻译 |
| [298](https://github.com/Vonng/ddia/pull/298) | [@Makonike](https://github.com/Makonike) | ch11&12: 修正两处错误 |
| [284](https://github.com/Vonng/ddia/pull/284) | [@WAangzE](https://github.com/WAangzE) | ch4: 更正一处列表错误 |
| [283](https://github.com/Vonng/ddia/pull/283) | [@WAangzE](https://github.com/WAangzE) | ch3: 更正一处错别字 |
| [282](https://github.com/Vonng/ddia/pull/282) | [@WAangzE](https://github.com/WAangzE) | ch2: 更正一处公式问题 |
| [281](https://github.com/Vonng/ddia/pull/281) | [@lyuxi99](https://github.com/lyuxi99) | 更正多处内部链接错误 |
| [280](https://github.com/Vonng/ddia/pull/280) | [@lyuxi99](https://github.com/lyuxi99) | ch9: 更正内部链接错误 |
| [279](https://github.com/Vonng/ddia/issues/279) | [@codexvn](https://github.com/codexvn) | ch9: 指出公式在 GitHub Pages 显示的问题 |
| [278](https://github.com/Vonng/ddia/pull/278) | [@LJlkdskdjflsa](https://github.com/LJlkdskdjflsa) | 发现了繁体中文版本中的错误翻译 |
| [275](https://github.com/Vonng/ddia/pull/275) | [@117503445](https://github.com/117503445) | 更正 LICENSE 链接 |
| [274](https://github.com/Vonng/ddia/pull/274) | [@uncle-lv](https://github.com/uncle-lv) | ch7: 修正错别字 |
| [273](https://github.com/Vonng/ddia/pull/273) | [@Sdot-Python](https://github.com/Sdot-Python) | ch7: 统一了 write skew 的翻译 |
| [271](https://github.com/Vonng/ddia/pull/271) | [@Makonike](https://github.com/Makonike) | ch6: 统一了 rebalancing 的翻译 |
| [270](https://github.com/Vonng/ddia/pull/270) | [@Ynjxsjmh](https://github.com/Ynjxsjmh) | ch7: 修正不一致的翻译 |
| [263](https://github.com/Vonng/ddia/pull/263) | [@zydmayday](https://github.com/zydmayday) | ch5: 修正译文中的重复单词 |
| [260](https://github.com/Vonng/ddia/pull/260) | [@haifeiWu](https://github.com/haifeiWu) | ch4: 修正部分不准确的翻译 |
| [258](https://github.com/Vonng/ddia/pull/258) | [@bestgrc](https://github.com/bestgrc) | ch3: 修正一处翻译错误 |
| [257](https://github.com/Vonng/ddia/pull/257) | [@UnderSam](https://github.com/UnderSam) | ch8: 修正一处拼写错误 |
| [256](https://github.com/Vonng/ddia/pull/256) | [@AlphaWang](https://github.com/AlphaWang) | ch7: 修正“可串行化”相关内容的多处翻译不当 |
| [255](https://github.com/Vonng/ddia/pull/255) | [@AlphaWang](https://github.com/AlphaWang) | ch7: 修正“可重复读”相关内容的多处翻译不当 |
| [253](https://github.com/Vonng/ddia/pull/253) | [@AlphaWang](https://github.com/AlphaWang) | ch7: 修正“读已提交”相关内容的多处翻译不当 |
| [246](https://github.com/Vonng/ddia/pull/246) | [@derekwu0101](https://github.com/derekwu0101) | ch3: 修正繁体中文的转译错误 |
| [245](https://github.com/Vonng/ddia/pull/245) | [@skyran1278](https://github.com/skyran1278) | ch12: 修正繁体中文的转译错误 |
| [244](https://github.com/Vonng/ddia/pull/244) | [@Axlgrep](https://github.com/Axlgrep) | ch9: 修正不通顺的翻译 |
| [242](https://github.com/Vonng/ddia/pull/242) | [@lynkeib](https://github.com/lynkeib) | ch9: 修正不通顺的翻译 |
| [241](https://github.com/Vonng/ddia/pull/241) | [@lynkeib](https://github.com/lynkeib) | ch8: 修正不正确的公式格式 |
| [240](https://github.com/Vonng/ddia/pull/240) | [@8da2k](https://github.com/8da2k) | ch9: 修正不通顺的翻译 |
| [239](https://github.com/Vonng/ddia/pull/239) | [@BeBraveBeCurious](https://github.com/BeBraveBeCurious) | ch7: 修正不一致的翻译 |
| [237](https://github.com/Vonng/ddia/pull/237) | [@zhangnew](https://github.com/zhangnew) | ch3: 修正错误的图片链接 |
| [229](https://github.com/Vonng/ddia/pull/229) | [@lis186](https://github.com/lis186) | 指出繁体中文的转译错误:复杂 |
| [226](https://github.com/Vonng/ddia/pull/226) | [@chroming](https://github.com/chroming) | ch1: 修正导航栏中的章节名称 |
| [220](https://github.com/Vonng/ddia/pull/220) | [@skyran1278](https://github.com/skyran1278) | ch9: 修正线性一致的繁体中文翻译 |
| [194](https://github.com/Vonng/ddia/pull/194) | [@BeBraveBeCurious](https://github.com/BeBraveBeCurious) | ch4: 修正错误的翻译 |
| [193](https://github.com/Vonng/ddia/pull/193) | [@BeBraveBeCurious](https://github.com/BeBraveBeCurious) | ch4: 优化译文 |
| [192](https://github.com/Vonng/ddia/pull/192) | [@BeBraveBeCurious](https://github.com/BeBraveBeCurious) | ch4: 修正不一致和不通顺的翻译 |
| [190](https://github.com/Vonng/ddia/pull/190) | [@Pcrab](https://github.com/Pcrab) | ch1: 修正不准确的翻译 |
| [187](https://github.com/Vonng/ddia/pull/187) | [@narojay](https://github.com/narojay) | ch9: 修正生硬的翻译 |
| [186](https://github.com/Vonng/ddia/pull/186) | [@narojay](https://github.com/narojay) | ch8: 修正错别字 |
| [185](https://github.com/Vonng/ddia/issues/185) | [@8da2k](https://github.com/8da2k) | 指出小标题跳转的问题 |
| [184](https://github.com/Vonng/ddia/pull/184) | [@DavidZhiXing](https://github.com/DavidZhiXing) | ch10: 修正失效的网址 |
| [183](https://github.com/Vonng/ddia/pull/183) | [@OneSizeFitsQuorum](https://github.com/OneSizeFitsQuorum) | ch8: 修正错别字 |
| [182](https://github.com/Vonng/ddia/issues/182) | [@lroolle](https://github.com/lroolle) | 建议docsify的主题风格 |
| [181](https://github.com/Vonng/ddia/pull/181) | [@YunfengGao](https://github.com/YunfengGao) | ch2: 修正翻译错误 |
| [180](https://github.com/Vonng/ddia/pull/180) | [@skyran1278](https://github.com/skyran1278) | ch3: 指出繁体中文的转译错误 |
| [177](https://github.com/Vonng/ddia/pull/177) | [@exzhawk](https://github.com/exzhawk) | 支持 Github Pages 里的公式显示 |
| [176](https://github.com/Vonng/ddia/pull/176) | [@haifeiWu](https://github.com/haifeiWu) | ch2: 语义网相关翻译更正 |
| [175](https://github.com/Vonng/ddia/pull/175) | [@cwr31](https://github.com/cwr31) | ch7: 不变式相关翻译更正 |
| [174](https://github.com/Vonng/ddia/pull/174) | [@BeBraveBeCurious](https://github.com/BeBraveBeCurious) | README & preface: 更正不正确的中文用词和标点符号 |
| [173](https://github.com/Vonng/ddia/pull/173) | [@ZvanYang](https://github.com/ZvanYang) | ch12: 修正不完整的翻译 |
| [171](https://github.com/Vonng/ddia/pull/171) | [@ZvanYang](https://github.com/ZvanYang) | ch12: 修正重复的译文 |
| [169](https://github.com/Vonng/ddia/pull/169) | [@ZvanYang](https://github.com/ZvanYang) | ch12: 更正不太通顺的翻译 |
| [166](https://github.com/Vonng/ddia/pull/166) | [@bp4m4h94](https://github.com/bp4m4h94) | ch1: 发现错误的文献索引 |
| [164](https://github.com/Vonng/ddia/pull/164) | [@DragonDriver](https://github.com/DragonDriver) | preface: 更正错误的标点符号 |
| [163](https://github.com/Vonng/ddia/pull/163) | [@llmmddCoder](https://github.com/llmmddCoder) | ch1: 更正错误字 |
| [160](https://github.com/Vonng/ddia/pull/160) | [@Zhayhp](https://github.com/Zhayhp) | ch2: 建议将 network model 翻译为网状模型 |
| [159](https://github.com/Vonng/ddia/pull/159) | [@1ess](https://github.com/1ess) | ch4: 更正错误字 |
| [157](https://github.com/Vonng/ddia/pull/157) | [@ZvanYang](https://github.com/ZvanYang) | ch7: 更正不太通顺的翻译 |
| [155](https://github.com/Vonng/ddia/pull/155) | [@ZvanYang](https://github.com/ZvanYang) | ch7: 更正不太通顺的翻译 |
| [153](https://github.com/Vonng/ddia/pull/153) | [@DavidZhiXing](https://github.com/DavidZhiXing) | ch9: 修正缩略图的错别字 |
| [152](https://github.com/Vonng/ddia/pull/152) | [@ZvanYang](https://github.com/ZvanYang) | ch7: 除重->去重 |
| [151](https://github.com/Vonng/ddia/pull/151) | [@ZvanYang](https://github.com/ZvanYang) | ch5: 修订sibling相关的翻译 |
| [147](https://github.com/Vonng/ddia/pull/147) | [@ZvanYang](https://github.com/ZvanYang) | ch5: 更正一处不准确的翻译 |
| [145](https://github.com/Vonng/ddia/pull/145) | [@Hookey](https://github.com/Hookey) | 识别了当前简繁转译过程中处理不当的地方,暂通过转换脚本规避 |
| [144](https://github.com/Vonng/ddia/issues/144) | [@secret4233](https://github.com/secret4233) | ch7: 不翻译`next-key locking` |
| [143](https://github.com/Vonng/ddia/issues/143) | [@imcheney](https://github.com/imcheney) | ch3: 更新残留的机翻段落 |
| [142](https://github.com/Vonng/ddia/issues/142) | [@XIJINIAN](https://github.com/XIJINIAN) | 建议去除段首的制表符 |
| [141](https://github.com/Vonng/ddia/issues/141) | [@Flyraty](https://github.com/Flyraty) | ch5: 发现一处错误格式的章节引用 |
| [140](https://github.com/Vonng/ddia/pull/140) | [@Bowser1704](https://github.com/Bowser1704) | ch5: 修正章节Summary中多处不通顺的翻译 |
| [139](https://github.com/Vonng/ddia/pull/139) | [@Bowser1704](https://github.com/Bowser1704) | ch2&ch3: 修正多处不通顺的或错误的翻译 |
| [137](https://github.com/Vonng/ddia/pull/137) | [@fuxuemingzhu](https://github.com/fuxuemingzhu) | ch5&ch6: 优化多处不通顺的或错误的翻译 |
| [134](https://github.com/Vonng/ddia/pull/134) | [@fuxuemingzhu](https://github.com/fuxuemingzhu) | ch4: 优化多处不通顺的或错误的翻译 |
| [133](https://github.com/Vonng/ddia/pull/133) | [@fuxuemingzhu](https://github.com/fuxuemingzhu) | ch3: 优化多处错误的或不通顺的翻译 |
| [132](https://github.com/Vonng/ddia/pull/132) | [@fuxuemingzhu](https://github.com/fuxuemingzhu) | ch3: 优化一处容易产生歧义的翻译 |
| [131](https://github.com/Vonng/ddia/pull/131) | [@rwwg4](https://github.com/rwwg4) | ch6: 修正两处错误的翻译 |
| [129](https://github.com/Vonng/ddia/pull/129) | [@anaer](https://github.com/anaer) | ch4: 修正两处强调文本和四处代码变量名称 |
| [128](https://github.com/Vonng/ddia/pull/128) | [@meilin96](https://github.com/meilin96) | ch5: 修正一处错误的引用 |
| [126](https://github.com/Vonng/ddia/pull/126) | [@cwr31](https://github.com/cwr31) | ch10: 修正一处错误的翻译(功能 -> 函数) |
| [125](https://github.com/Vonng/ddia/pull/125) | [@dch1228](https://github.com/dch1228) | ch2: 优化 how best 的翻译(如何以最佳方式) |
| [123](https://github.com/Vonng/ddia/pull/123) | [@yingang](https://github.com/yingang) | translation updates (chapter 9, TOC in readme, glossary, etc.) |
| [121](https://github.com/Vonng/ddia/pull/121) | [@yingang](https://github.com/yingang) | translation updates (chapter 5 to chapter 8) |
| [120](https://github.com/Vonng/ddia/pull/120) | [@jiong-han](https://github.com/jiong-han) | Typo fix: 呲之以鼻 -> 嗤之以鼻 |
| [119](https://github.com/Vonng/ddia/pull/119) | [@cclauss](https://github.com/cclauss) | Streamline file operations in convert() |
| [118](https://github.com/Vonng/ddia/pull/118) | [@yingang](https://github.com/yingang) | translation updates (chapter 2 to chapter 4) |
| [117](https://github.com/Vonng/ddia/pull/117) | [@feeeei](https://github.com/feeeei) | 统一每章的标题格式 |
| [115](https://github.com/Vonng/ddia/pull/115) | [@NageNalock](https://github.com/NageNalock) | 第七章病句修改: 重复词语 |
| [114](https://github.com/Vonng/ddia/pull/114) | [@Sunt-ing](https://github.com/Sunt-ing) | Update README.md: correct the book name |
| [113](https://github.com/Vonng/ddia/pull/113) | [@lpxxn](https://github.com/lpxxn) | 修改语句 |
| [112](https://github.com/Vonng/ddia/pull/112) | [@ibyte2011](https://github.com/ibyte2011) | Update ch9.md |
| [110](https://github.com/Vonng/ddia/pull/110) | [@lpxxn](https://github.com/lpxxn) | 读已写入数据 |
| [107](https://github.com/Vonng/ddia/pull/107) | [@abbychau](https://github.com/abbychau) | 單調鐘和好死还是赖活着 |
| [106](https://github.com/Vonng/ddia/pull/106) | [@enochii](https://github.com/enochii) | typo in ch2: fix braces typo |
| [105](https://github.com/Vonng/ddia/pull/105) | [@LiminCode](https://github.com/LiminCode) | Chronicle translation error |
| [104](https://github.com/Vonng/ddia/pull/104) | [@Sunt-ing](https://github.com/Sunt-ing) | several advice for better translation |
| [103](https://github.com/Vonng/ddia/pull/103) | [@Sunt-ing](https://github.com/Sunt-ing) | typo in ch4: should be 完成 rather than 完全 |
| [102](https://github.com/Vonng/ddia/pull/102) | [@Sunt-ing](https://github.com/Sunt-ing) | ch4: better-translation: 扼杀 → 破坏 |
| [101](https://github.com/Vonng/ddia/pull/101) | [@Sunt-ing](https://github.com/Sunt-ing) | typo in Ch4: should be "改变" rathr than "盖面" |
| [100](https://github.com/Vonng/ddia/pull/100) | [@LiminCode](https://github.com/LiminCode) | fix missing translation |
| [99 ](https://github.com/Vonng/ddia/pull/99) | [@mrdrivingduck](https://github.com/mrdrivingduck) | ch6: fix the word rebalancing |
| [98 ](https://github.com/Vonng/ddia/pull/98) | [@jacklightChen](https://github.com/jacklightChen) | fix ch7.md: fix wrong references |
| [97 ](https://github.com/Vonng/ddia/pull/97) | [@jenac](https://github.com/jenac) | 96 |
| [96 ](https://github.com/Vonng/ddia/pull/96) | [@PragmaTwice](https://github.com/PragmaTwice) | ch2: fix typo about 'may or may not be' |
| [95 ](https://github.com/Vonng/ddia/pull/95) | [@EvanMu96](https://github.com/EvanMu96) | fix translation of "the battle cry" in ch5 |
| [94 ](https://github.com/Vonng/ddia/pull/94) | [@kemingy](https://github.com/kemingy) | ch6: fix markdown and punctuations |
| [93 ](https://github.com/Vonng/ddia/pull/93) | [@kemingy](https://github.com/kemingy) | ch5: fix markdown and some typos |
| [92 ](https://github.com/Vonng/ddia/pull/92) | [@Gilbert1024](https://github.com/Gilbert1024) | Merge pull request #1 from Vonng/master |
| [88 ](https://github.com/Vonng/ddia/pull/88) | [@kemingy](https://github.com/kemingy) | fix typo for ch1, ch2, ch3, ch4 |
| [87 ](https://github.com/Vonng/ddia/pull/87) | [@wynn5a](https://github.com/wynn5a) | Update ch3.md |
| [86 ](https://github.com/Vonng/ddia/pull/86) | [@northmorn](https://github.com/northmorn) | Update ch1.md |
| [85 ](https://github.com/Vonng/ddia/pull/85) | [@sunbuhui](https://github.com/sunbuhui) | fix ch2.md: fix ch2 ambiguous translation |
| [84 ](https://github.com/Vonng/ddia/pull/84) | [@ganler](https://github.com/ganler) | Fix translation: use up |
| [83 ](https://github.com/Vonng/ddia/pull/83) | [@afunTW](https://github.com/afunTW) | Using OpenCC to convert from zh-cn to zh-tw |
| [82 ](https://github.com/Vonng/ddia/pull/82) | [@kangni](https://github.com/kangni) | fix gitbook url |
| [78 ](https://github.com/Vonng/ddia/pull/78) | [@hanyu2](https://github.com/hanyu2) | Fix unappropriated translation |
| [77 ](https://github.com/Vonng/ddia/pull/77) | [@Ozarklake](https://github.com/Ozarklake) | fix typo |
| [75 ](https://github.com/Vonng/ddia/pull/75) | [@2997ms](https://github.com/2997ms) | Fix typo |
| [74 ](https://github.com/Vonng/ddia/pull/74) | [@2997ms](https://github.com/2997ms) | Update ch9.md |
| [70 ](https://github.com/Vonng/ddia/pull/70) | [@2997ms](https://github.com/2997ms) | Update ch7.md |
| [67 ](https://github.com/Vonng/ddia/pull/67) | [@jiajiadebug](https://github.com/jiajiadebug) | fix issues in ch2 - ch9 and glossary |
| [66 ](https://github.com/Vonng/ddia/pull/66) | [@blindpirate](https://github.com/blindpirate) | Fix typo |
| [63 ](https://github.com/Vonng/ddia/pull/63) | [@haifeiWu](https://github.com/haifeiWu) | Update ch10.md |
| [62 ](https://github.com/Vonng/ddia/pull/62) | [@ych](https://github.com/ych) | fix ch1.md typesetting problem |
| [61 ](https://github.com/Vonng/ddia/pull/61) | [@xianlaioy](https://github.com/xianlaioy) | docs:钟-->种去掉ou |
| [60 ](https://github.com/Vonng/ddia/pull/60) | [@Zombo1296](https://github.com/Zombo1296) | 否则 -> 或者 |
| [59 ](https://github.com/Vonng/ddia/pull/59) | [@AlexanderMisel](https://github.com/AlexanderMisel) | 呼叫->调用,显着->显著 |
| [58 ](https://github.com/Vonng/ddia/pull/58) | [@ibyte2011](https://github.com/ibyte2011) | Update ch8.md |
| [55 ](https://github.com/Vonng/ddia/pull/55) | [@saintube](https://github.com/saintube) | ch8: 修改链接错误 |
| [54 ](https://github.com/Vonng/ddia/pull/54) | [@Panmax](https://github.com/Panmax) | Update ch2.md |
| [53 ](https://github.com/Vonng/ddia/pull/53) | [@ibyte2011](https://github.com/ibyte2011) | Update ch9.md |
| [52 ](https://github.com/Vonng/ddia/pull/52) | [@hecenjie](https://github.com/hecenjie) | Update ch1.md |
| [51 ](https://github.com/Vonng/ddia/pull/51) | [@latavin243](https://github.com/latavin243) | fix 修正ch3 ch4几处翻译 |
| [50 ](https://github.com/Vonng/ddia/pull/50) | [@AlexZFX](https://github.com/AlexZFX) | 几个疏漏和格式错误 |
| [49 ](https://github.com/Vonng/ddia/pull/49) | [@haifeiWu](https://github.com/haifeiWu) | Update ch1.md |
| [48 ](https://github.com/Vonng/ddia/pull/48) | [@scaugrated](https://github.com/scaugrated) | fix typo |
| [47 ](https://github.com/Vonng/ddia/pull/47) | [@lzwill](https://github.com/lzwill) | Fixed typos in ch2 |
| [45 ](https://github.com/Vonng/ddia/pull/45) | [@zenuo](https://github.com/zenuo) | 删除一个多余的右括号 |
| [44 ](https://github.com/Vonng/ddia/pull/44) | [@akxxsb](https://github.com/akxxsb) | 修正第七章底部链接错误 |
| [43 ](https://github.com/Vonng/ddia/pull/43) | [@baijinping](https://github.com/baijinping) | "更假简单"->"更加简单" |
| [42 ](https://github.com/Vonng/ddia/pull/42) | [@tisonkun](https://github.com/tisonkun) | 修复 ch1 中的无序列表格式 |
| [38 ](https://github.com/Vonng/ddia/pull/38) | [@renjie-c](https://github.com/renjie-c) | 纠正多处的翻译小错误 |
| [37 ](https://github.com/Vonng/ddia/pull/37) | [@tankilo](https://github.com/tankilo) | fix translation mistakes in ch4.md |
| [36 ](https://github.com/Vonng/ddia/pull/36) | [@wwek](https://github.com/wwek) | 1.修复多个链接错误 2.名词优化修订 3.错误修订 |
| [35 ](https://github.com/Vonng/ddia/pull/35) | [@wwek](https://github.com/wwek) | fix ch7.md to ch8.md link error |
| [34 ](https://github.com/Vonng/ddia/pull/34) | [@wwek](https://github.com/wwek) | Merge pull request #1 from Vonng/master |
| [33 ](https://github.com/Vonng/ddia/pull/33) | [@wwek](https://github.com/wwek) | fix part-ii.md link error |
| [32 ](https://github.com/Vonng/ddia/pull/32) | [@JCYoky](https://github.com/JCYoky) | Update ch2.md |
| [31 ](https://github.com/Vonng/ddia/pull/31) | [@elsonLee](https://github.com/elsonLee) | Update ch7.md |
| [26 ](https://github.com/Vonng/ddia/pull/26) | [@yjhmelody](https://github.com/yjhmelody) | 修复一些明显错误 |
| [25 ](https://github.com/Vonng/ddia/pull/25) | [@lqbilbo](https://github.com/lqbilbo) | 修复链接错误 |
| [24 ](https://github.com/Vonng/ddia/pull/24) | [@artiship](https://github.com/artiship) | 修改词语顺序 |
| [23 ](https://github.com/Vonng/ddia/pull/23) | [@artiship](https://github.com/artiship) | 修正错别字 |
| [22 ](https://github.com/Vonng/ddia/pull/22) | [@artiship](https://github.com/artiship) | 纠正翻译错误 |
| [21 ](https://github.com/Vonng/ddia/pull/21) | [@zhtisi](https://github.com/zhtisi) | 修正目录和本章标题不符的情况 |
| [20 ](https://github.com/Vonng/ddia/pull/20) | [@rentiansheng](https://github.com/rentiansheng) | Update ch7.md |
| [19 ](https://github.com/Vonng/ddia/pull/19) | [@LHRchina](https://github.com/LHRchina) | 修复语句小bug |
| [16 ](https://github.com/Vonng/ddia/pull/16) | [@MuAlex](https://github.com/MuAlex) | Master |
| [15 ](https://github.com/Vonng/ddia/pull/15) | [@cg-zhou](https://github.com/cg-zhou) | Update translation progress |
| [14 ](https://github.com/Vonng/ddia/pull/14) | [@cg-zhou](https://github.com/cg-zhou) | Translate glossary |
| [13 ](https://github.com/Vonng/ddia/pull/13) | [@cg-zhou](https://github.com/cg-zhou) | 详细修改了后记中和印度野猪相关的描述 |
| [12 ](https://github.com/Vonng/ddia/pull/12) | [@ibyte2011](https://github.com/ibyte2011) | 修改了部分翻译 |
| [11 ](https://github.com/Vonng/ddia/pull/11) | [@jiajiadebug](https://github.com/jiajiadebug) | ch2 100% |
| [10 ](https://github.com/Vonng/ddia/pull/10) | [@jiajiadebug](https://github.com/jiajiadebug) | ch2 20% |
| [9 ](https://github.com/Vonng/ddia/pull/9) | [@jiajiadebug](https://github.com/jiajiadebug) | Preface, ch1, part-i translation minor fixes |
| [7 ](https://github.com/Vonng/ddia/pull/7) | [@MuAlex](https://github.com/MuAlex) | Ch6 translation pull request |
| [6 ](https://github.com/Vonng/ddia/pull/6) | [@MuAlex](https://github.com/MuAlex) | Ch6 change version1 |
| [5 ](https://github.com/Vonng/ddia/pull/5) | [@nevertiree](https://github.com/nevertiree) | Chapter 01语法微调 |
| [2 ](https://github.com/Vonng/ddia/pull/2) | [@seagullbird](https://github.com/seagullbird) | 序言初翻 |

View File

@@ -1,252 +1,256 @@
# 术语表
---
title: 术语表
weight: 500
breadcrumbs: false
---
> 请注意,本术语表中的定义简短而简单,旨在传达核心思想,而不是术语的完整细微之处。有关更多详细信息,请参阅正文中的参考资料。
> 请注意,本术语表中的定义简短而简单,旨在传达核心思想,而非死扣完整细节。有关更多详细信息,请参阅正文中的参考资料。
* **异步asynchronous**
## **异步asynchronous**
不等待某些事情完成(例如,将数据发送到网络中的另一个节点),并且不会假设要花多长时间。请参阅“[同步复制与异步复制](ch5.md#同步复制与异步复制)”、“[同步网络与异步网络](ch8.md#同步网络与异步网络)”以及“[系统模型与现实](ch8.md#系统模型与现实)”。
不等待某些事情完成(例如,将数据发送到网络中的另一个节点),并且不会假设要花多长时间。请参阅“[同步复制与异步复制](/ch5#同步复制与异步复制)”、“[同步网络与异步网络](/ch8#同步网络与异步网络)”以及“[系统模型与现实](/ch8#系统模型与现实)”。
* **原子atomic**
## **原子atomic**
在并发操作的上下文中:描述一个在单个时间点看起来生效的操作,所以另一个并发进程永远不会遇到处于“半完成”状态的操作。另见隔离。
在事务的上下文中:将一些写入操作分为一组,这组写入要么全部提交成功,要么遇到错误时全部回滚。请参阅“[原子性](ch7.md#原子性)”和“[原子提交与两阶段提交](ch9.md#原子提交与两阶段提交)”。
在事务的上下文中:将一些写入操作分为一组,这组写入要么全部提交成功,要么遇到错误时全部回滚。请参阅“[原子性](/ch7#原子性)”和“[原子提交与两阶段提交](/ch9#原子提交与两阶段提交)”。
* **背压backpressure**
## **背压backpressure**
接收方接收数据速度较慢时,强制降低发送方的数据发送速度。也称为流量控制。请参阅“[消息传递系统](ch11.md#消息传递系统)”。
接收方接收数据速度较慢时,强制降低发送方的数据发送速度。也称为流量控制。请参阅“[消息传递系统](/ch11#消息传递系统)”。
* **批处理batch process**
## **批处理batch process**
一种计算,它将一些固定的(通常是大的)数据集作为输入,并将其他一些数据作为输出,而不修改输入。见[第十章](ch10.md)。
一种计算,它将一些固定的(通常是大的)数据集作为输入,并将其他一些数据作为输出,而不修改输入。见[第十章](/ch10)。
* **边界bounded**
## **边界bounded**
有一些已知的上限或大小。例如,网络延迟情况(请参阅“[超时与无穷的延迟](ch8.md#超时与无穷的延迟)”)和数据集(请参阅[第十一章](ch11.md)的介绍)。
有一些已知的上限或大小。例如,网络延迟情况(请参阅“[超时与无穷的延迟](/ch8#超时与无穷的延迟)”)和数据集(请参阅[第十一章](/ch11)的介绍)。
* **拜占庭故障Byzantine fault**
## **拜占庭故障Byzantine fault**
表现异常的节点,这种异常可能以任意方式出现,例如向其他节点发送矛盾或恶意消息。请参阅“[拜占庭故障](ch8.md#拜占庭故障)”。
表现异常的节点,这种异常可能以任意方式出现,例如向其他节点发送矛盾或恶意消息。请参阅“[拜占庭故障](/ch8#拜占庭故障)”。
* **缓存cache**
## **缓存cache**
一种组件,通过存储最近使用过的数据,加快未来对相同数据的读取速度。缓存中通常存放部分数据:因此,如果缓存中缺少某些数据,则必须从某些底层较慢的数据存储系统中,获取完整的数据副本。
* **CAP定理CAP theorem**
## **CAP定理CAP theorem**
一个被广泛误解的理论结果,在实践中是没有用的。请参阅“[CAP定理](ch9.md#CAP定理)”。
一个被广泛误解的理论结果,在实践中是没有用的。请参阅“[CAP定理](/ch9#CAP定理)”。
* **因果关系causality**
## **因果关系causality**
事件之间的依赖关系,当一件事发生在另一件事情之前。例如,后面的事件是对早期事件的回应,或者依赖于更早的事件,或者应该根据先前的事件来理解。请参阅“[“此前发生”的关系和并发](ch5.md#“此前发生”的关系和并发)”和“[顺序与因果关系](ch9.md#顺序与因果关系)”。
事件之间的依赖关系,当一件事发生在另一件事情之前。例如,后面的事件是对早期事件的回应,或者依赖于更早的事件,或者应该根据先前的事件来理解。请参阅“[“此前发生”的关系和并发](/ch5#“此前发生”的关系和并发)”和“[顺序与因果关系](/ch9#顺序与因果关系)”。
* **共识consensus**
## **共识consensus**
分布式计算的一个基本问题,就是让几个节点同意某些事情(例如,哪个节点应该是数据库集群的领导者)。问题比乍看起来要困难得多。请参阅“[容错共识](ch9.md#容错共识)”。
分布式计算的一个基本问题,就是让几个节点同意某些事情(例如,哪个节点应该是数据库集群的领导者)。问题比乍看起来要困难得多。请参阅“[容错共识](/ch9#容错共识)”。
* **数据仓库data warehouse**
## **数据仓库data warehouse**
一个数据库其中来自几个不同的OLTP系统的数据已经被合并和准备用于分析目的。请参阅“[数据仓库](ch3.md#数据仓库)”。
一个数据库其中来自几个不同的OLTP系统的数据已经被合并和准备用于分析目的。请参阅“[数据仓库](/ch3#数据仓库)”。
* **声明式declarative**
## **声明式declarative**
描述某些东西应有的属性,但不知道如何实现它的确切步骤。在查询的上下文中,查询优化器采用声明性查询并决定如何最好地执行它。请参阅“[数据查询语言](ch2.md#数据查询语言)”。
描述某些东西应有的属性,但不知道如何实现它的确切步骤。在查询的上下文中,查询优化器采用声明性查询并决定如何最好地执行它。请参阅“[数据查询语言](/ch2#数据查询语言)”。
* **非规范化denormalize**
## **非规范化denormalize**
为了加速读取,在标准数据集中引入一些冗余或重复数据,通常采用缓存或索引的形式。非规范化的值是一种预先计算的查询结果,像物化视图。请参阅“[单对象和多对象操作](ch7.md#单对象和多对象操作)”和“[从同一事件日志中派生多个视图](ch11.md#从同一事件日志中派生多个视图)”。
为了加速读取,在标准数据集中引入一些冗余或重复数据,通常采用缓存或索引的形式。非规范化的值是一种预先计算的查询结果,像物化视图。请参阅“[单对象和多对象操作](/ch7#单对象和多对象操作)”和“[从同一事件日志中派生多个视图](/ch11#从同一事件日志中派生多个视图)”。
* **衍生数据derived data**
## **衍生数据derived data**
一种数据集,根据其他数据通过可重复运行的流程创建。必要时,你可以运行该流程再次创建衍生数据。衍生数据通常用于提高特定数据的读取速度。常见的衍生数据有索引、缓存和物化视图。请参阅[第三部分](part-iii.md)的介绍。
一种数据集,根据其他数据通过可重复运行的流程创建。必要时,你可以运行该流程再次创建衍生数据。衍生数据通常用于提高特定数据的读取速度。常见的衍生数据有索引、缓存和物化视图。请参阅[第三部分](/part-iii)的介绍。
* **确定性deterministic**
## **确定性deterministic**
描述一个函数,如果给它相同的输入,则总是产生相同的输出。这意味着它不能依赖于随机数字、时间、网络通信或其他不可预测的事情。
* **分布式distributed**
## **分布式distributed**
在由网络连接的多个节点上运行。对于部分节点故障,具有容错性:系统的一部分发生故障时,其他部分仍可以正常工作,通常情况下,软件无需了解故障相关的确切情况。请参阅“[故障与部分失效](ch8.md#故障与部分失效)”。
在由网络连接的多个节点上运行。对于部分节点故障,具有容错性:系统的一部分发生故障时,其他部分仍可以正常工作,通常情况下,软件无需了解故障相关的确切情况。请参阅“[故障与部分失效](/ch8#故障与部分失效)”。
* **持久durable**
## **持久durable**
以某种方式存储数据,即使发生各种故障,也不会丢失数据。请参阅“[持久性](ch7.md#持久性)”。
以某种方式存储数据,即使发生各种故障,也不会丢失数据。请参阅“[持久性](/ch7#持久性)”。
* **ETLExtract-Transform-Load**
## **ETLExtract-Transform-Load**
提取-转换-加载Extract-Transform-Load。从源数据库中提取数据将其转换为更适合分析查询的形式并将其加载到数据仓库或批处理系统中的过程。请参阅“[数据仓库](ch3.md#数据仓库)”。
提取-转换-加载Extract-Transform-Load。从源数据库中提取数据将其转换为更适合分析查询的形式并将其加载到数据仓库或批处理系统中的过程。请参阅“[数据仓库](/ch3#数据仓库)”。
* **故障切换failover**
## **故障切换failover**
在具有单一领导者的系统中,故障切换是将领导角色从一个节点转移到另一个节点的过程。请参阅“[处理节点宕机](ch5.md#处理节点宕机)”。
在具有单一领导者的系统中,故障切换是将领导角色从一个节点转移到另一个节点的过程。请参阅“[处理节点宕机](/ch5#处理节点宕机)”。
* **容错fault-tolerant**
## **容错fault-tolerant**
如果出现问题(例如,机器崩溃或网络连接失败),可以自动恢复。请参阅“[可靠性](ch1.md#可靠性)”。
如果出现问题(例如,机器崩溃或网络连接失败),可以自动恢复。请参阅“[可靠性](/ch1#可靠性)”。
* **流量控制flow control**
## **流量控制flow control**
见背压backpressure
* **追随者follower**
## **追随者follower**
一种数据副本,仅处理领导者或主库发出的数据变更,不直接接受来自客户端的任何写入。也称为备库、从库、只读副本或热备份。请参阅“[领导者与追随者](ch5.md#领导者与追随者)”。
一种数据副本,仅处理领导者或主库发出的数据变更,不直接接受来自客户端的任何写入。也称为备库、从库、只读副本或热备份。请参阅“[领导者与追随者](/ch5#领导者与追随者)”。
* **全文检索full-text search**
## **全文检索full-text search**
通过任意关键字来搜索文本,通常具有附加特征,例如匹配类似的拼写词或同义词。全文索引是一种支持这种查询的次级索引。请参阅“[全文搜索和模糊索引](ch3.md#全文搜索和模糊索引)”。
通过任意关键字来搜索文本,通常具有附加特征,例如匹配类似的拼写词或同义词。全文索引是一种支持这种查询的次级索引。请参阅“[全文搜索和模糊索引](/ch3#全文搜索和模糊索引)”。
* **图graph**
## **图graph**
一种数据结构,由顶点(可以指向的东西,也称为节点或实体)和边(从一个顶点到另一个顶点的连接,也称为关系或弧)组成。请参阅“[图数据模型](ch2.md#图数据模型)”。
一种数据结构,由顶点(可以指向的东西,也称为节点或实体)和边(从一个顶点到另一个顶点的连接,也称为关系或弧)组成。请参阅“[图数据模型](/ch2#图数据模型)”。
* **散列hash**
## **散列hash**
将输入转换为看起来像随机数值的函数。相同的输入会转换为相同的数值,不同的输入一般会转换为不同的数值,也可能转换为相同数值(也被称为冲突)。请参阅“[根据键的散列分区](ch6.md#根据键的散列分区)”。
将输入转换为看起来像随机数值的函数。相同的输入会转换为相同的数值,不同的输入一般会转换为不同的数值,也可能转换为相同数值(也被称为冲突)。请参阅“[根据键的散列分区](/ch6#根据键的散列分区)”。
* **幂等idempotent**
## **幂等idempotent**
用于描述一种操作可以安全地重试执行,即执行多次的效果和执行一次的效果相同。请参阅“[幂等性](ch11.md#幂等性)”。
用于描述一种操作可以安全地重试执行,即执行多次的效果和执行一次的效果相同。请参阅“[幂等性](/ch11#幂等性)”。
* **索引index**
## **索引index**
一种数据结构。通过索引,你可以根据特定字段的值,在所有数据记录中进行高效检索。请参阅“[驱动数据库的数据结构](ch3.md#驱动数据库的数据结构)”。
一种数据结构。通过索引,你可以根据特定字段的值,在所有数据记录中进行高效检索。请参阅“[驱动数据库的数据结构](/ch3#驱动数据库的数据结构)”。
* **隔离性isolation**
## **隔离性isolation**
在事务上下文中,用于描述并发执行事务的互相干扰程度。串行运行具有最强的隔离性,不过其它程度的隔离也通常被使用。请参阅“[隔离性](ch7.md#隔离性)”。
在事务上下文中,用于描述并发执行事务的互相干扰程度。串行运行具有最强的隔离性,不过其它程度的隔离也通常被使用。请参阅“[隔离性](/ch7#隔离性)”。
* **连接join**
## **连接join**
汇集有共同点的记录。在一个记录与另一个记录有关(外键,文档参考,图中的边)的情况下最常用,查询需要获取参考所指向的记录。请参阅“[多对一和多对多的关系](ch2.md#多对一和多对多的关系)”和“[Reduce侧连接与分组](ch10.md#Reduce侧连接与分组)”。
汇集有共同点的记录。在一个记录与另一个记录有关(外键,文档参考,图中的边)的情况下最常用,查询需要获取参考所指向的记录。请参阅“[多对一和多对多的关系](/ch2#多对一和多对多的关系)”和“[Reduce侧连接与分组](/ch10#Reduce侧连接与分组)”。
* **领导者leader**
## **领导者leader**
当数据或服务被复制到多个节点时,领导者是被指定为可以接受数据变更的副本。领导者可以通过某些协议选举产生,也可以由管理员手动选择。领导者也被称为主库。请参阅“[领导者与追随者](ch5.md#领导者与追随者)”。
当数据或服务被复制到多个节点时,领导者是被指定为可以接受数据变更的副本。领导者可以通过某些协议选举产生,也可以由管理员手动选择。领导者也被称为主库。请参阅“[领导者与追随者](/ch5#领导者与追随者)”。
* **线性化linearizable**
## **线性化linearizable**
表现为系统中只有一份通过原子操作更新的数据副本。请参阅“[线性一致性](ch9.md#线性一致性)”。
表现为系统中只有一份通过原子操作更新的数据副本。请参阅“[线性一致性](/ch9#线性一致性)”。
* **局部性locality**
## **局部性locality**
一种性能优化方式,如果经常在相同的时间请求一些离散数据,把这些数据放到一个位置。请参阅“[查询的数据局部性](ch2.md#查询的数据局部性)”。
一种性能优化方式,如果经常在相同的时间请求一些离散数据,把这些数据放到一个位置。请参阅“[查询的数据局部性](/ch2#查询的数据局部性)”。
* **锁lock**
## **锁lock**
一种保证只有一个线程、节点或事务可以访问的机制,如果其它线程、节点或事务想访问相同元素,则必须等待锁被释放。请参阅“[两阶段锁定](ch7.md#两阶段锁定)”和“[领导者和锁](ch8.md#领导者和锁)”。
一种保证只有一个线程、节点或事务可以访问的机制,如果其它线程、节点或事务想访问相同元素,则必须等待锁被释放。请参阅“[两阶段锁定](/ch7#两阶段锁定)”和“[领导者和锁](/ch8#领导者和锁)”。
* **日志log**
## **日志log**
日志是一个只能以追加方式写入的文件,用于存放数据。预写式日志用于在存储引擎崩溃时恢复数据(请参阅“[让B树更可靠](ch3.md#让B树更可靠)”);结构化日志存储引擎使用日志作为它的主要存储格式(请参阅“[SSTables和LSM树](ch3.md#SSTables和LSM树)”);复制型日志用于把写入从领导者复制到追随者(请参阅“[领导者与追随者](ch5.md#领导者与追随者)”);事件性日志可以表现为数据流(请参阅“[分区日志](ch11.md#分区日志)”)。
日志是一个只能以追加方式写入的文件,用于存放数据。预写式日志用于在存储引擎崩溃时恢复数据(请参阅“[让B树更可靠](/ch3#让B树更可靠)”);结构化日志存储引擎使用日志作为它的主要存储格式(请参阅“[SSTables和LSM树](/ch3#SSTables和LSM树)”);复制型日志用于把写入从领导者复制到追随者(请参阅“[领导者与追随者](/ch5#领导者与追随者)”);事件性日志可以表现为数据流(请参阅“[分区日志](/ch11#分区日志)”)。
* **物化materialize**
## **物化materialize**
急切地计算并写出结果,而不是在请求时计算。请参阅“[聚合:数据立方体和物化视图](ch3.md#聚合:数据立方体和物化视图)”和“[物化中间状态](ch10.md#物化中间状态)”。
急切地计算并写出结果,而不是在请求时计算。请参阅“[聚合:数据立方体和物化视图](/ch3#聚合:数据立方体和物化视图)”和“[物化中间状态](/ch10#物化中间状态)”。
* **节点node**
## **节点node**
计算机上运行的一些软件的实例,通过网络与其他节点通信以完成某项任务。
* **规范化normalized**
## **规范化normalized**
以没有冗余或重复的方式进行结构化。在规范化数据库中,当某些数据发生变化时,你只需要在一个地方进行更改,而不是在许多不同的地方复制很多次。请参阅“[多对一和多对多的关系](ch2.md#多对一和多对多的关系)”。
以没有冗余或重复的方式进行结构化。在规范化数据库中,当某些数据发生变化时,你只需要在一个地方进行更改,而不是在许多不同的地方复制很多次。请参阅“[多对一和多对多的关系](/ch2#多对一和多对多的关系)”。
* **OLAPOnline Analytic Processing**
## **OLAPOnline Analytic Processing**
在线分析处理。通过对大量记录进行聚合(例如,计数,总和,平均)来表征的访问模式。请参阅“[事务处理还是分析?](ch3.md#事务处理还是分析?)”。
在线分析处理。通过对大量记录进行聚合(例如,计数,总和,平均)来表征的访问模式。请参阅“[事务处理还是分析?](/ch3#事务处理还是分析?)”。
* **OLTPOnline Transaction Processing**
## **OLTPOnline Transaction Processing**
在线事务处理。访问模式的特点是快速查询,读取或写入少量记录,这些记录通常通过键索引。请参阅“[事务处理还是分析?](ch3.md#事务处理还是分析?)”。
在线事务处理。访问模式的特点是快速查询,读取或写入少量记录,这些记录通常通过键索引。请参阅“[事务处理还是分析?](/ch3#事务处理还是分析?)”。
* **分区partitioning**
## **分区partitioning**
将单机上的大型数据集或计算结果拆分为较小部分,并将其分布到多台机器上。也称为分片。见[第六章](ch6.md)。
将单机上的大型数据集或计算结果拆分为较小部分,并将其分布到多台机器上。也称为分片。见[第六章](/ch6)。
* **百分位点percentile**
## **百分位点percentile**
通过计算有多少值高于或低于某个阈值来衡量值分布的方法。例如某个时间段的第95个百分位响应时间是时间t则该时间段中95%的请求完成时间小于t5%的请求完成时间要比t长。请参阅“[描述性能](ch1.md#描述性能)”。
通过计算有多少值高于或低于某个阈值来衡量值分布的方法。例如某个时间段的第95个百分位响应时间是时间t则该时间段中95%的请求完成时间小于t5%的请求完成时间要比t长。请参阅“[描述性能](/ch1#描述性能)”。
* **主键primary key**
## **主键primary key**
唯一标识记录的值(通常是数字或字符串)。在许多应用程序中,主键由系统在创建记录时生成(例如,按顺序或随机); 它们通常不由用户设置。另请参阅次级索引。
* **法定人数quorum**
## **法定人数quorum**
在操作完成之前,需要对操作进行投票的最少节点数量。请参阅“[读写的法定人数](ch5.md#读写的法定人数)”。
在操作完成之前,需要对操作进行投票的最少节点数量。请参阅“[读写的法定人数](/ch5#读写的法定人数)”。
* **再平衡rebalance**
## **再平衡rebalance**
将数据或服务从一个节点移动到另一个节点以实现负载均衡。请参阅“[分区再平衡](ch6.md#分区再平衡)”。
将数据或服务从一个节点移动到另一个节点以实现负载均衡。请参阅“[分区再平衡](/ch6#分区再平衡)”。
* **复制replication**
## **复制replication**
在几个节点(副本)上保留相同数据的副本,以便在某些节点无法访问时,数据仍可访问。请参阅[第五章](ch5.md)。
在几个节点(副本)上保留相同数据的副本,以便在某些节点无法访问时,数据仍可访问。请参阅[第五章](/ch5)。
* **模式schema**
## **模式schema**
一些数据结构的描述,包括其字段和数据类型。可以在数据生命周期的不同点检查某些数据是否符合模式(请参阅“[文档模型中的模式灵活性](ch2.md#文档模型中的模式灵活性)”),模式可以随时间变化(请参阅[第四章](ch4.md))。
一些数据结构的描述,包括其字段和数据类型。可以在数据生命周期的不同点检查某些数据是否符合模式(请参阅“[文档模型中的模式灵活性](/ch2#文档模型中的模式灵活性)”),模式可以随时间变化(请参阅[第四章](/ch4))。
* **次级索引secondary index**
## **次级索引secondary index**
与主要数据存储器一起维护的附加数据结构,使你可以高效地搜索与某种条件相匹配的记录。请参阅“[其他索引结构](ch3.md#其他索引结构)”和“[分区与次级索引](ch6.md#分区与次级索引)”。
与主要数据存储器一起维护的附加数据结构,使你可以高效地搜索与某种条件相匹配的记录。请参阅“[其他索引结构](/ch3#其他索引结构)”和“[分区与次级索引](/ch6#分区与次级索引)”。
* **可串行化serializable**
## **可串行化serializable**
保证多个并发事务同时执行时,它们的行为与按顺序逐个执行事务相同。请参阅第七章的“[可串行化](ch7.md#可串行化)”。
保证多个并发事务同时执行时,它们的行为与按顺序逐个执行事务相同。请参阅第七章的“[可串行化](/ch7#可串行化)”。
* **无共享shared-nothing**
## **无共享shared-nothing**
与共享内存或共享磁盘架构相比独立节点每个节点都有自己的CPU内存和磁盘通过传统网络连接。见[第二部分](part-ii.md)的介绍。
与共享内存或共享磁盘架构相比独立节点每个节点都有自己的CPU内存和磁盘通过传统网络连接。见[第二部分](/part-ii)的介绍。
* **偏斜skew**
## **偏斜skew**
各分区负载不平衡,例如某些分区有大量请求或数据,而其他分区则少得多。也被称为热点。请参阅“[负载偏斜与热点消除](ch6.md#负载偏斜与热点消除)”和“[处理偏斜](ch10.md#处理偏斜)”。
各分区负载不平衡,例如某些分区有大量请求或数据,而其他分区则少得多。也被称为热点。请参阅“[负载偏斜与热点消除](/ch6#负载偏斜与热点消除)”和“[处理偏斜](/ch10#处理偏斜)”。
时间线异常导致事件以不期望的顺序出现。请参阅“[快照隔离和可重复读](ch7.md#快照隔离和可重复读)”中的关于读取偏差的讨论,“[写入偏差与幻读](ch7.md#写入偏差与幻读)”中的写入偏差以及“[有序事件的时间戳](ch8.md#有序事件的时间戳)”中的时钟偏斜。
时间线异常导致事件以不期望的顺序出现。请参阅“[快照隔离和可重复读](/ch7#快照隔离和可重复读)”中的关于读取偏差的讨论,“[写入偏差与幻读](/ch7#写入偏差与幻读)”中的写入偏差以及“[有序事件的时间戳](/ch8#有序事件的时间戳)”中的时钟偏斜。
* **脑裂split brain**
## **脑裂split brain**
两个节点同时认为自己是领导者的情况,这种情况可能违反系统担保。请参阅“[处理节点宕机](ch5.md#处理节点宕机)”和“[真相由多数所定义](ch8.md#真相由多数所定义)”。
两个节点同时认为自己是领导者的情况,这种情况可能违反系统担保。请参阅“[处理节点宕机](/ch5#处理节点宕机)”和“[真相由多数所定义](/ch8#真相由多数所定义)”。
* **存储过程stored procedure**
## **存储过程stored procedure**
一种对事务逻辑进行编码的方式,它可以完全在数据库服务器上执行,事务执行期间无需与客户端通信。请参阅“[真的串行执行](ch7.md#真的串行执行)”。
一种对事务逻辑进行编码的方式,它可以完全在数据库服务器上执行,事务执行期间无需与客户端通信。请参阅“[真的串行执行](/ch7#真的串行执行)”。
* **流处理stream process**
## **流处理stream process**
持续运行的计算。可以持续接收事件流作为输入,并得出一些输出。见[第十一章](ch11.md)。
持续运行的计算。可以持续接收事件流作为输入,并得出一些输出。见[第十一章](/ch11)。
* **同步synchronous**
## **同步synchronous**
异步的反义词。
* **记录系统system of record**
## **记录系统system of record**
一个保存主要权威版本数据的系统,也被称为真相的来源。首先在这里写入数据变更,其他数据集可以从记录系统衍生。请参阅[第三部分](part-iii.md)的介绍。
一个保存主要权威版本数据的系统,也被称为真相的来源。首先在这里写入数据变更,其他数据集可以从记录系统衍生。请参阅[第三部分](/part-iii)的介绍。
* **超时timeout**
## **超时timeout**
检测故障的最简单方法之一,即在一段时间内观察是否缺乏响应。但是,不可能知道超时是由于远程节点的问题还是网络中的问题造成的。请参阅“[超时与无穷的延迟](ch8.md#超时与无穷的延迟)”。
检测故障的最简单方法之一,即在一段时间内观察是否缺乏响应。但是,不可能知道超时是由于远程节点的问题还是网络中的问题造成的。请参阅“[超时与无穷的延迟](/ch8#超时与无穷的延迟)”。
* **全序total order**
## **全序total order**
一种比较事物的方法(例如时间戳),可以让你总是说出两件事中哪一件更大,哪件更小。总的来说,有些东西是无法比拟的(不能说哪个更大或更小)的顺序称为偏序。请参阅“[因果顺序不是全序的](ch9.md#因果顺序不是全序的)”。
一种比较事物的方法(例如时间戳),可以让你总是说出两件事中哪一件更大,哪件更小。总的来说,有些东西是无法比拟的(不能说哪个更大或更小)的顺序称为偏序。请参阅“[因果顺序不是全序的](/ch9#因果顺序不是全序的)”。
* **事务transaction**
## **事务transaction**
为了简化错误处理和并发问题,将几个读写操作分组到一个逻辑单元中。见[第七章](ch7.md)。
为了简化错误处理和并发问题,将几个读写操作分组到一个逻辑单元中。见[第七章](/ch7)。
* **两阶段提交2PC, two-phase commit**
## **两阶段提交2PC, two-phase commit**
一种确保多个数据库节点全部提交或全部中止事务的算法。请参阅“[原子提交与两阶段提交](ch9.md#原子提交与两阶段提交)”。
一种确保多个数据库节点全部提交或全部中止事务的算法。请参阅“[原子提交与两阶段提交](/ch9#原子提交与两阶段提交)”。
* **两阶段锁定2PL, two-phase locking**
## **两阶段锁定2PL, two-phase locking**
一种用于实现可串行化隔离的算法,该算法通过事务获取对其读取或写入的所有数据的锁,直到事务结束。请参阅“[两阶段锁定](ch7.md#两阶段锁定)”。
一种用于实现可串行化隔离的算法,该算法通过事务获取对其读取或写入的所有数据的锁,直到事务结束。请参阅“[两阶段锁定](/ch7#两阶段锁定)”。
* **无边界unbounded**
## **无边界unbounded**
没有任何已知的上限或大小。反义词是边界bounded

38
content/zh/part-i.md Normal file
View File

@@ -0,0 +1,38 @@
---
title: 第一部分:数据系统基础
weight: 100
breadcrumbs: false
---
本书前四章介绍了数据系统底层的基础概念,无论是在单台机器上运行的单点数据系统,还是分布在多台机器上的分布式数据系统都适用。
1. [第一章](/ch1) 将介绍本书使用的术语和方法。**可靠性,可伸缩性和可维护性** ,这些词汇到底意味着什么?如何实现这些目标?
2. [第二章](/ch2) 将对几种不同的 **数据模型和查询语言** 进行比较。从程序员的角度看,这是数据库之间最明显的区别。不同的数据模型适用于不同的应用场景。
3. [第三章](/ch3) 将深入 **存储引擎** 内部,研究数据库如何在磁盘上摆放数据。不同的存储引擎针对不同的负载进行优化,选择合适的存储引擎对系统性能有巨大影响。
4. [第四章](/ch4) 将对几种不同的 **数据编码** 进行比较。特别研究了这些格式在应用需求经常变化、模式需要随时间演变的环境中表现如何。
第二部分将专门讨论在 **分布式数据系统** 中特有的问题。
## 索引
* [第一章:可靠性、可伸缩性和可维护性](/ch1)
* [关于数据系统的思考](/ch1#关于数据系统的思考)
* [可靠性](/ch1#可靠性)
* [可伸缩性](/ch1#可伸缩性)
* [可维护性](/ch1#可维护性)
* [本章小结](/ch1#本章小结)
* [第二章:数据模型与查询语言](/ch2)
* [关系模型与文档模型](/ch2#关系模型与文档模型)
* [数据查询语言](/ch2#数据查询语言)
* [图数据模型](/ch2#图数据模型)
* [本章小结](/ch2#本章小结)
* [第三章:存储与检索](/ch3)
* [驱动数据库的数据结构](/ch3#驱动数据库的数据结构)
* [事务处理还是分析?](/ch3#事务处理还是分析)
* [列式存储](/ch3#列式存储)
* [本章小结](/ch3#本章小结)
* [第四章:编码与演化](/ch4)
* [编码数据的格式](/ch4#编码数据的格式)
* [数据流的类型](/ch4#数据流的类型)
* [本章小结](/ch4#本章小结)

View File

@@ -1,4 +1,8 @@
# 第二部分:分布式数据
---
title: 第二部分:分布式数据
weight: 200
breadcrumbs: false
---
> 一个成功的技术,现实的优先级必须高于公关,你可以糊弄别人,但糊弄不了自然规律。
>
@@ -7,7 +11,7 @@
-------
在本书的 [第一部分](part-i.md) 中,我们讨论了数据系统的各个方面,但仅限于数据存储在单台机器上的情况。现在我们到了 [第二部分](part-ii.md),进入更高的层次,并提出一个问题:如果 **多台机器** 参与数据的存储和检索,会发生什么?
在本书的 [第一部分](/part-i) 中,我们讨论了数据系统的各个方面,但仅限于数据存储在单台机器上的情况。现在我们到了 [第二部分](/part-ii),进入更高的层次,并提出一个问题:如果 **多台机器** 参与数据的存储和检索,会发生什么?
你可能会出于各种各样的原因,希望将数据库分布到多台机器上:
@@ -53,30 +57,55 @@
* 复制Replication
在几个不同的节点上保存数据的相同副本,可能放在不同的位置。复制提供了冗余:如果一些节点不可用,剩余的节点仍然可以提供数据服务。复制也有助于改善性能。[第五章](ch5.md) 将讨论复制。
在几个不同的节点上保存数据的相同副本,可能放在不同的位置。复制提供了冗余:如果一些节点不可用,剩余的节点仍然可以提供数据服务。复制也有助于改善性能。[第五章](/ch5) 将讨论复制。
* 分区 (Partitioning)
将一个大型数据库拆分成较小的子集(称为 **分区**,即 partitions从而不同的分区可以指派给不同的 **节点**nodes亦称 **分片**,即 sharding。[第六章](ch6.md) 将讨论分区。
将一个大型数据库拆分成较小的子集(称为 **分区**,即 partitions从而不同的分区可以指派给不同的 **节点**nodes亦称 **分片**,即 sharding。[第六章](/ch6) 将讨论分区。
复制和分区是不同的机制,但它们经常同时使用。如 [图 II-1](img/figii-1.png) 所示。
复制和分区是不同的机制,但它们经常同时使用。如 [图 II-1](/img/figii-1.png) 所示。
![](img/figii-1.png)
![](/img/figii-1.png)
**图 II-1 一个数据库切分为两个分区,每个分区都有两个副本**
理解了这些概念,就可以开始讨论在分布式系统中需要做出的困难抉择。[第七章](ch7.md) 将讨论 **事务Transaction**,这对于了解数据系统中可能出现的各种问题,以及我们可以做些什么很有帮助。[第八章](ch8.md) 和 [第九章](ch9.md) 将讨论分布式系统的根本局限性。
理解了这些概念,就可以开始讨论在分布式系统中需要做出的困难抉择。[第七章](/ch7) 将讨论 **事务Transaction**,这对于了解数据系统中可能出现的各种问题,以及我们可以做些什么很有帮助。[第八章](/ch8) 和 [第九章](/ch9) 将讨论分布式系统的根本局限性。
在本书的 [第三部分](part-iii.md) 中,将讨论如何将多个(可能是分布式的)数据存储集成为一个更大的系统,以满足复杂的应用需求。但首先,我们来聊聊分布式的数据。
在本书的 [第三部分](/part-iii) 中,将讨论如何将多个(可能是分布式的)数据存储集成为一个更大的系统,以满足复杂的应用需求。但首先,我们来聊聊分布式的数据。
## 索引
5. [复制](ch5.md)
6. [分区](ch6.md)
7. [事务](ch7.md)
8. [分布式系统的麻烦](ch8.md)
9. [一致性与共识](ch9.md)
* [第五章:复制](/ch5)
* [领导者与追随者](/ch5#领导者与追随者)
* [复制延迟问题](/ch5#复制延迟问题)
* [多主复制](/ch5#多主复制)
* [无主复制](/ch5#无主复制)
* [本章小结](/ch5#本章小结)
* [第六章:分区](/ch6)
* [分区与复制](/ch6#分区与复制)
* [键值数据的分区](/ch6#键值数据的分区)
* [分区与次级索引](/ch6#分区与次级索引)
* [分区再平衡](/ch6#分区再平衡)
* [请求路由](/ch6#请求路由)
* [本章小结](/ch6#本章小结)
* [第七章:事务](/ch7)
* [事务的棘手概念](/ch7#事务的棘手概念)
* [弱隔离级别](/ch7#弱隔离级别)
* [可串行化](/ch7#可串行化)
* [本章小结](/ch7#本章小结)
* [第八章:分布式系统的麻烦](/ch8)
* [故障与部分失效](/ch8#故障与部分失效)
* [不可靠的网络](/ch8#不可靠的网络)
* [不可靠的时钟](/ch8#不可靠的时钟)
* [知识、真相与谎言](/ch8#知识真相与谎言)
* [本章小结](/ch8#本章小结)
* [第九章:一致性与共识](/ch9)
* [一致性保证](/ch9#一致性保证)
* [线性一致性](/ch9#线性一致性)
* [顺序保证](/ch9#顺序保证)
* [分布式事务与共识](/ch9#分布式事务与共识)
* [本章小结](/ch9#本章小结)
## 参考文献
@@ -84,10 +113,4 @@
1. Ulrich Drepper: “[What Every Programmer Should Know About Memory](https://people.freebsd.org/~lstewart/articles/cpumemory.pdf),” akkadia.org, November 21, 2007.
1. Ben Stopford: “[Shared Nothing vs. Shared Disk Architectures: An Independent View](http://www.benstopford.com/2009/11/24/understanding-the-shared-nothing-architecture/),” benstopford.com, November 24, 2009.
1. Michael Stonebraker: “[The Case for Shared Nothing](http://db.cs.berkeley.edu/papers/hpts85-nothing.pdf),” IEEE Database EngineeringBulletin, volume 9, number 1, pages 49, March 1986.
1. Frank McSherry, Michael Isard, and Derek G. Murray: “[Scalability! But at What COST?](http://www.frankmcsherry.org/assets/COST.pdf),” at 15th USENIX Workshop on Hot Topics in Operating Systems (HotOS),May 2015.
------
| 上一章 | 目录 | 下一章 |
| ---------------------------- | ------------------------------- | ---------------------- |
| [第四章:编码与演化](ch4.md) | [设计数据密集型应用](README.md) | [第五章:复制](ch5.md) |
1. Frank McSherry, Michael Isard, and Derek G. Murray: “[Scalability! But at What COST?](http://www.frankmcsherry.org/assets/COST.pdf),” at 15th USENIX Workshop on Hot Topics in Operating Systems (HotOS),May 2015.

View File

@@ -1,6 +1,11 @@
# 第三部分:衍生数据
---
title: 第三部分:衍生数据
weight: 300
breadcrumbs: false
---
在本书的 [第一部分](part-i.md) 和 [第二部分](part-ii.md) 中,我们自底向上地把所有关于分布式数据库的主要考量都过了一遍。从数据在磁盘上的布局,一直到出现故障时分布式系统一致性的局限。但所有的讨论都假定了应用中只用了一种数据库。
在本书的 [第一部分](/part-i) 和 [第二部分](/part-ii) 中,我们自底向上地把所有关于分布式数据库的主要考量都过了一遍。从数据在磁盘上的布局,一直到出现故障时分布式系统一致性的局限。但所有的讨论都假定了应用中只用了一种数据库。
现实世界中的数据系统往往更为复杂。大型应用程序经常需要以多种方式访问和处理数据,没有一个数据库可以同时满足所有这些不同的需求。因此应用程序通常组合使用多种组件:数据存储、索引、缓存、分析系统等等,并实现在这些组件中移动数据的机制。
@@ -28,17 +33,23 @@
## 章节概述
我们将从 [第十章](ch10.md) 开始,研究例如 MapReduce 这样 **面向批处理batch-oriented** 的数据流系统。对于建设大规模数据系统,我们将看到,它们提供了优秀的工具和思想。[第十一章](ch11.md) 将把这些思想应用到 **流式数据data streams** 中,使我们能用更低的延迟完成同样的任务。[第十二章](ch12.md) 将对本书进行总结,探讨如何使用这些工具来构建可靠,可伸缩和可维护的应用。
我们将从 [第十章](/ch10) 开始,研究例如 MapReduce 这样 **面向批处理batch-oriented** 的数据流系统。对于建设大规模数据系统,我们将看到,它们提供了优秀的工具和思想。[第十一章](/ch11) 将把这些思想应用到 **流式数据data streams** 中,使我们能用更低的延迟完成同样的任务。[第十二章](/ch12) 将对本书进行总结,探讨如何使用这些工具来构建可靠,可伸缩和可维护的应用。
## 索引
10. [批处理](ch10.md)
11. [处理](ch11.md)
12. [数据系统的未来](ch12.md)
------
| 上一章 | 目录 | 下一章 |
| ------------------------------ | ------------------------------- | ------------------------- |
| [第九章:一致性与共识](ch9.md) | [设计数据密集型应用](README.md) | [第十章:批处理](ch10.md) |
* [第十章:批处理](/ch10)
* [使用Unix工具的批处理](/ch10#使用Unix工具的批处理)
* [MapReduce和分布式文件系统](/ch10#MapReduce和分布式文件系统)
* [MapReduce之后](/ch10#MapReduce之后)
* [本章小结](/ch10#本章小结)
* [第十一章:流处理](/ch11)
* [传递事件流](/ch11#传递事件流)
* [数据库与流](/ch11#数据库与流)
* [流处理](/ch11#流处理)
* [本章小结](/ch11#本章小结)
* [第十二章:数据系统的未来](/ch12)
* [数据集成](/ch12#数据集成)
* [分拆数据库](/ch12#分拆数据库)
* [将事情做正确](/ch12#将事情做正确)
* [做正确的事情](/ch12#做正确的事情)
* [本章小结](/ch12#本章小结)

View File

@@ -1,4 +1,9 @@
# 序言
---
title: 序言
weight: 50
breadcrumbs: false
---
如果近几年从业于软件工程,特别是服务器端和后端系统开发,那么你很有可能已经被大量关于数据存储和处理的时髦词汇轰炸过了: NoSQL大数据Web-Scale分片最终一致性ACIDCAP 定理云服务MapReduce实时
@@ -60,11 +65,11 @@
本书分为三部分:
1. 在 [第一部分](part-i.md) 中,我们会讨论设计数据密集型应用所赖的基本思想。我们从 [第一章](ch1.md) 开始,讨论我们实际要达到的目标:可靠性、可伸缩性和可维护性;我们该如何思考这些概念;以及如何实现它们。在 [第二章](ch2.md) 中,我们比较了几种不同的数据模型和查询语言,看看它们如何适用于不同的场景。在 [第三章](ch3.md) 中将讨论存储引擎:数据库如何在磁盘上摆放数据,以便能高效地再次找到它。[第四章](ch4.md) 转向数据编码(序列化),以及随时间演化的模式。
1. 在 [第一部分](/part-i) 中,我们会讨论设计数据密集型应用所赖的基本思想。我们从 [第一章](/ch1) 开始,讨论我们实际要达到的目标:可靠性、可伸缩性和可维护性;我们该如何思考这些概念;以及如何实现它们。在 [第二章](/ch2) 中,我们比较了几种不同的数据模型和查询语言,看看它们如何适用于不同的场景。在 [第三章](/ch3) 中将讨论存储引擎:数据库如何在磁盘上摆放数据,以便能高效地再次找到它。[第四章](/ch4) 转向数据编码(序列化),以及随时间演化的模式。
2. 在 [第二部分](part-ii.md) 中,我们从讨论存储在一台机器上的数据转向讨论分布在多台机器上的数据。这对于可伸缩性通常是必需的,但带来了各种独特的挑战。我们首先讨论复制([第五章](ch5.md))、分区 / 分片([第六章](ch6.md))和事务([第七章](ch7.md))。然后我们将探索关于分布式系统问题的更多细节([第八章](ch8.md)),以及在分布式系统中实现一致性与共识意味着什么([第九章](ch9.md))。
2. 在 [第二部分](/part-ii) 中,我们从讨论存储在一台机器上的数据转向讨论分布在多台机器上的数据。这对于可伸缩性通常是必需的,但带来了各种独特的挑战。我们首先讨论复制([第五章](/ch5))、分区 / 分片([第六章](/ch6))和事务([第七章](/ch7))。然后我们将探索关于分布式系统问题的更多细节([第八章](/ch8)),以及在分布式系统中实现一致性与共识意味着什么([第九章](/ch9))。
3. 在 [第三部分](part-iii.md) 中,我们讨论那些从其他数据集衍生出一些数据集的系统。衍生数据经常出现在异构系统中:当没有单个数据库可以把所有事情都做的很好时,应用需要集成几种不同的数据库、缓存、索引等。在 [第十章](ch10.md) 中我们将从一种衍生数据的批处理方法开始,然后在此基础上建立在 [第十一章](ch11.md) 中讨论的流处理。最后,在 [第十二章](ch12.md) 中,我们将所有内容汇总,讨论在将来构建可靠、可伸缩和可维护的应用程序的方法。
3. 在 [第三部分](/part-iii) 中,我们讨论那些从其他数据集衍生出一些数据集的系统。衍生数据经常出现在异构系统中:当没有单个数据库可以把所有事情都做的很好时,应用需要集成几种不同的数据库、缓存、索引等。在 [第十章](/ch10) 中我们将从一种衍生数据的批处理方法开始,然后在此基础上建立在 [第十一章](/ch11) 中讨论的流处理。最后,在 [第十二章](/ch12) 中,我们将所有内容汇总,讨论在将来构建可靠、可伸缩和可维护的应用程序的方法。
## 参考文献与延伸阅读

92
content/zh/toc.md Normal file
View File

@@ -0,0 +1,92 @@
---
title: "目录"
linkTitle: "目录"
weight: 10
breadcrumbs: false
---
![](/img/title.png)
## [序言](/preface)
## [第一部分:数据系统基础](/part-i)
### [第一章:可靠性、可伸缩性和可维护性](/ch1)
* [关于数据系统的思考](/ch1#关于数据系统的思考)
* [可靠性](/ch1#可靠性)
* [可伸缩性](/ch1#可伸缩性)
* [可维护性](/ch1#可维护性)
* [本章小结](/ch1#本章小结)
### [第二章:数据模型与查询语言](/ch2)
* [关系模型与文档模型](/ch2#关系模型与文档模型)
* [数据查询语言](/ch2#数据查询语言)
* [图数据模型](/ch2#图数据模型)
* [本章小结](/ch2#本章小结)
### [第三章:存储与检索](/ch3)
* [驱动数据库的数据结构](/ch3#驱动数据库的数据结构)
* [事务处理还是分析?](/ch3#事务处理还是分析)
* [列式存储](/ch3#列式存储)
* [本章小结](/ch3#本章小结)
### [第四章:编码与演化](/ch4)
* [编码数据的格式](/ch4#编码数据的格式)
* [数据流的类型](/ch4#数据流的类型)
* [本章小结](/ch4#本章小结)
## [第二部分:分布式数据](/part-ii)
### [第五章:复制](/ch5)
* [领导者与追随者](/ch5#领导者与追随者)
* [复制延迟问题](/ch5#复制延迟问题)
* [多主复制](/ch5#多主复制)
* [无主复制](/ch5#无主复制)
* [本章小结](/ch5#本章小结)
### [第六章:分区](/ch6)
* [分区与复制](/ch6#分区与复制)
* [键值数据的分区](/ch6#键值数据的分区)
* [分区与次级索引](/ch6#分区与次级索引)
* [分区再平衡](/ch6#分区再平衡)
* [请求路由](/ch6#请求路由)
* [本章小结](/ch6#本章小结)
### [第七章:事务](/ch7)
* [事务的棘手概念](/ch7#事务的棘手概念)
* [弱隔离级别](/ch7#弱隔离级别)
* [可串行化](/ch7#可串行化)
* [本章小结](/ch7#本章小结)
### [第八章:分布式系统的麻烦](/ch8)
* [故障与部分失效](/ch8#故障与部分失效)
* [不可靠的网络](/ch8#不可靠的网络)
* [不可靠的时钟](/ch8#不可靠的时钟)
* [知识、真相与谎言](/ch8#知识真相与谎言)
* [本章小结](/ch8#本章小结)
### [第九章:一致性与共识](/ch9)
* [一致性保证](/ch9#一致性保证)
* [线性一致性](/ch9#线性一致性)
* [顺序保证](/ch9#顺序保证)
* [分布式事务与共识](/ch9#分布式事务与共识)
* [本章小结](/ch9#本章小结)
## [第三部分:衍生数据](/part-iii)
### [第十章:批处理](/ch10)
* [使用Unix工具的批处理](/ch10#使用Unix工具的批处理)
* [MapReduce和分布式文件系统](/ch10#MapReduce和分布式文件系统)
* [MapReduce之后](/ch10#MapReduce之后)
* [本章小结](/ch10#本章小结)
### [第十一章:流处理](/ch11)
* [传递事件流](/ch11#传递事件流)
* [数据库与流](/ch11#数据库与流)
* [流处理](/ch11#流处理)
* [本章小结](/ch11#本章小结)
### [第十二章:数据系统的未来](/ch12)
* [数据集成](/ch12#数据集成)
* [分拆数据库](/ch12#分拆数据库)
* [将事情做正确](/ch12#将事情做正确)
* [做正确的事情](/ch12#做正确的事情)
* [本章小结](/ch12#本章小结)
### [术语表](/glossary)
### [后记](/colophon)

View File

@@ -1,21 +0,0 @@
# Summary
* [Preface](preface.md)
* [Part I: Foundations of Data Systems](part-i.md)
- [1. Reliable, Scalable, and Maintainable Applications](ch1.md)
- [2. Data Models and Query Languages](ch2.md)
- [3. Storage and Retrieval](ch3.md)
- [4. Encoding and Evolution](ch4.md)
* [Part II: Distributed Data](part-ii.md)
- [5. Replication](ch5.md)
- [6. Partitioning](ch6.md)
- [7. Transactions](ch7.md)
- [8. The Trouble with Distributed Systems](ch8.md)
- [9. Consistency and Consensus](ch9.md)
* [Part III: Derived Data](part-iii.md)
- [10. Batch Processing](ch10.md)
- [11. Stream Processing](ch11.md)
- [12. The Future of Data Systems](ch12.md)
* [Glossary](glossary.md)
* [Colophon](colophon.md)

View File

@@ -1,4 +0,0 @@
- Languages
- [Chineses](/)
- [Chinese (Traditional)](/zh-tw/)
- [English](/en-us/)

View File

@@ -1,19 +0,0 @@
- [Preface](preface.md)
- [Part I: Foundations of Data Systems](part-i.md)
- [1. Reliable, Scalable, and Maintainable Applications](ch1.md)
- [2. Data Models and Query Languages](ch2.md)
- [3. Storage and Retrieval](ch3.md)
- [4. Encoding and Evolution](ch4.md)
- [Part II: Distributed Data](part-ii.md)
- [5. Replication](ch5.md)
- [6. Partitioning](ch6.md)
- [7. Transactions](ch7.md)
- [8. The Trouble with Distributed Systems](ch8.md)
- [9. Consistency and Consensus](ch9.md)
- [Part III: Derived Data](part-iii.md)
- [10. Batch Processing](ch10.md)
- [11. Stream Processing](ch11.md)
- [12. The Future of Data Systems](ch12.md)
- [Glossary](glossary.md)
- [Colophon](colophon.md)

View File

@@ -1,13 +0,0 @@
# PART I: Foundations of Data Systems
The first four chapters go through the fundamental ideas that apply to all data sys tems, whether running on a single machine or distributed across a cluster of machines:
1. [Chapter 1](ch1.md) introduces the terminology and approach that were going to use throughout this book. It examines what we actually mean by words like *reliabil ity*, *scalability*, and *maintainability*, and how we can try to achieve these goals.
2. [Chapter 2](ch2.md) compares several different data models and query languages—the most visible distinguishing factor between databases from a developers point of view. We will see how different models are appropriate to different situations.
3. [Chapter 3](ch4.md) turns to the internals of storage engines and looks at how databases lay out data on disk. Different storage engines are optimized for different workloads, and choosing the right one can have a huge effect on performance.
4. [Chapter 4](ch4.md) compares various formats for data encoding (serialization) and espe cially examines how they fare in an environment where application requirements change and schemas need to adapt over time.
Later, [Part II](part-ii.md) will turn to the particular issues of distributed data systems.

11
giscus.json Normal file
View File

@@ -0,0 +1,11 @@
{
"origins": [
"https://vonng.github.com/ddia",
"https://ddia.vonng.com",
"https://ddia.pgsty.com",
"http://localhost:1313"
],
"originsRegex": [
"http://localhost:[0-9]+"
]
}

5
go.mod Normal file
View File

@@ -0,0 +1,5 @@
module github.com/Vonng/pg-internal
go 1.24.5
require github.com/imfing/hextra v0.9.7 // indirect

2
go.sum Normal file
View File

@@ -0,0 +1,2 @@
github.com/imfing/hextra v0.9.7 h1:Zg5n24us36Bn/S/5mEUPkRW6uwE6vHHEqWSgN0bPXaM=
github.com/imfing/hextra v0.9.7/go.mod h1:cEfel3lU/bSx7lTE/+uuR4GJaphyOyiwNR3PTqFTXpI=

207
hugo.yaml Normal file
View File

@@ -0,0 +1,207 @@
baseURL: 'https://ddia.vonng.com/'
languageCode: 'zh-CN'
title: '设计数据密集型应用'
enableRobotsTXT: true
# Parse Git commit
enableGitInfo: true
# enableEmoji: false
hasCJKLanguage: true
outputs:
home: [HTML]
page: [HTML]
section: [HTML, RSS]
module:
imports:
- path: github.com/imfing/hextra
defaultContentLanguage: zh
languages:
zh:
languageName: 简体中文
languageCode: zh
contentDir: content/zh
weight: 1
title: 设计数据密集型应用
v2:
languageName: 第二版
languageCode: v2
contentDir: content/v2
weight: 2
title: 设计数据密集型应用(第二版)
tw:
languageName: 繁体中文
languageCode: tw
contentDir: content/tw
weight: 3
title: 設計資料密集型應用
en:
languageName: English
languageCode: en
contentDir: content/en
weight: 4
title: Designing Data-Intensive Applications
markup:
highlight:
noClasses: false
goldmark:
renderer:
unsafe: true
extensions:
passthrough:
delimiters:
block: [['\[', '\]'], ['$$', '$$']]
inline: [['\(', '\)']]
enable: true
menu:
main:
- name: Search
weight: 1
params:
type: search
- name: GitHub
weight: 2
url: "https://github.com/Vonng/ddia"
params:
icon: github
sidebar:
- identifier: ver
name: 语言版本
weight: 1
params:
type: separator
- identifier: simplified-chinese
name: "简体中文 ↗"
url: "/"
weight: 2
- identifier: simplified-chinese2
name: "第二版 ↗"
url: "/v2"
weight: 3
- identifier: traditional-chinese
name: "繁體中文 ↗"
url: "/tw"
weight: 4
- identifier: english
name: "English ↗"
url: "/en"
weight: 5
- identifier: more
name: 参考链接
params:
type: separator
weight: 6
- identifier: pgint
name: "PostgreSQL 内幕探索 ↗"
url: "https://pgint.vonng.com/"
weight: 7
- identifier: pgint14
name: "PostgreSQL 内参 ↗"
url: "https://postgres-internals.cn/"
weight: 8
- identifier: pigsty
name: "Pigsty 文档 ↗"
url: "https://pgsty.com/zh"
weight: 9
- identifier: pgext
name: "PG 扩展目录 ↗"
url: "https://ext.pgsty.com/zh"
weight: 10
params:
description: DDIA 中文版翻译,设计数据密集型应用是一本关于数据系统设计的书籍,它深入探讨了数据密集型应用的架构和设计原则,涵盖了分布式系统、数据存储、消息传递等主题。
navbar:
displayTitle: true
displayLogo: true
logo:
path: /logo.png
dark: /logo.png
# width: 40
# height: 20
# link: /
width: wide
page:
# full (100%), wide (90rem), normal (80rem)
width: full
theme:
# light | dark | system
default: system
displayToggle: true
footer:
enable: true
displayCopyright: true
displayPoweredBy: false
width: normal
# Display the last modification date
displayUpdatedDate: true
dateFormat: "2006-01-02"
# Search
# flexsearch is enabled by default
search:
enable: true
type: flexsearch
flexsearch:
# index page by: content | summary | heading | title
index: content
# full | forward | reverse | strict
# https://github.com/nextapps-de/flexsearch/#tokenizer-prefix-search
tokenize: forward
editURL:
enable: true
base: "https://github.com/Vonng/ddia/edit/main/content"
toc:
displayTags: true
highlight:
copy:
enable: true
# hover | always
display: hover
comments:
enable: true
type: giscus
# https://giscus.app/
giscus:
repo: "Vonng/ddia"
repoId: "MDEwOlJlcG9zaXRvcnkxMjA3MTA2NDQ"
category: "Announcements"
categoryId: "DIC_kwDOBzHl9M4CtlWB"
mapping: pathname
strict: 0
reactionsEnabled: 1
emitMetadata: 0
inputPosition: bottom
lang: zh-CN
theme: preferred_color_scheme
# repo: imfing/hextra
# repoId: R_kgDOJ9fJag
# category: General
# categoryId: DIC_kwDOJ9fJas4CY7gW
# mapping: pathname
# strict: 0
# reactionsEnabled: 1
# emitMetadata: 0
# inputPosition: top
# lang: en
# theme: noborder_dark

14
i18n/tw.yaml Normal file
View File

@@ -0,0 +1,14 @@
backToTop: "返回頂部"
changeLanguage: "切換語言"
changeTheme: "切換主題"
copyright: "© 2025 Hextra Project."
dark: "深色"
editThisPage: "在 GitHub 上編輯此頁 →"
lastUpdated: "最後更新於"
light: "淺色"
noResultsFound: "無結果"
onThisPage: "此頁上"
tags: "標籤"
poweredBy: "由 Hextra 驅動"
readMore: "更多 →"
searchPlaceholder: "搜尋文檔..."

16
i18n/v2.yaml Normal file
View File

@@ -0,0 +1,16 @@
backToTop: "返回顶部"
changeLanguage: "切换语言"
changeTheme: "切换主题"
copyright: "© 2025 Hextra Project."
dark: "深色"
editThisPage: "在 GitHub 上编辑此页 →"
lastUpdated: "最后更新于"
light: "浅色"
noResultsFound: "无结果"
onThisPage: "此页上"
tags: "标签"
poweredBy: "由 Hextra 驱动"
readMore: "更多 →"
searchPlaceholder: "搜索文档..."
previous: "上一页"
next: "下一页"

16
i18n/zh.yaml Normal file
View File

@@ -0,0 +1,16 @@
backToTop: "返回顶部"
changeLanguage: "切换语言"
changeTheme: "切换主题"
copyright: "© 2025 Hextra Project."
dark: "深色"
editThisPage: "在 GitHub 上编辑此页 →"
lastUpdated: "最后更新于"
light: "浅色"
noResultsFound: "无结果"
onThisPage: "此页上"
tags: "标签"
poweredBy: "由 Hextra 驱动"
readMore: "更多 →"
searchPlaceholder: "搜索文档..."
previous: "上一页"
next: "下一页"

View File

@@ -1,26 +0,0 @@
# 第一部分:数据系统基础
本书前四章介绍了数据系统底层的基础概念,无论是在单台机器上运行的单点数据系统,还是分布在多台机器上的分布式数据系统都适用。
1. [第一章](ch1.md) 将介绍本书使用的术语和方法。**可靠性,可伸缩性和可维护性** ,这些词汇到底意味着什么?如何实现这些目标?
2. [第二章](ch2.md) 将对几种不同的 **数据模型和查询语言** 进行比较。从程序员的角度看,这是数据库之间最明显的区别。不同的数据模型适用于不同的应用场景。
3. [第三章](ch3.md) 将深入 **存储引擎** 内部,研究数据库如何在磁盘上摆放数据。不同的存储引擎针对不同的负载进行优化,选择合适的存储引擎对系统性能有巨大影响。
4. [第四章](ch4) 将对几种不同的 **数据编码** 进行比较。特别研究了这些格式在应用需求经常变化、模式需要随时间演变的环境中表现如何。
第二部分将专门讨论在 **分布式数据系统** 中特有的问题。
## 目录
1. [可靠性、可伸缩性和可维护性](ch1.md)
2. [数据模型与查询语言](ch2.md)
3. [存储与检索](ch3.md)
4. [编码与演化](ch4.md)
------
| 上一章 | 目录 | 下一章 |
| ------------------ | ------------------------------- | -------------------------------------------- |
| [序言](preface.md) | [设计数据密集型应用](README.md) | [第一章:可靠性、可伸缩性和可维护性](ch1.md) |

View File

Before

Width:  |  Height:  |  Size: 586 KiB

After

Width:  |  Height:  |  Size: 586 KiB

View File

Before

Width:  |  Height:  |  Size: 834 KiB

After

Width:  |  Height:  |  Size: 834 KiB

View File

Before

Width:  |  Height:  |  Size: 966 KiB

After

Width:  |  Height:  |  Size: 966 KiB

View File

Before

Width:  |  Height:  |  Size: 737 KiB

After

Width:  |  Height:  |  Size: 737 KiB

View File

Before

Width:  |  Height:  |  Size: 810 KiB

After

Width:  |  Height:  |  Size: 810 KiB

View File

Before

Width:  |  Height:  |  Size: 975 KiB

After

Width:  |  Height:  |  Size: 975 KiB

View File

Before

Width:  |  Height:  |  Size: 813 KiB

After

Width:  |  Height:  |  Size: 813 KiB

View File

Before

Width:  |  Height:  |  Size: 821 KiB

After

Width:  |  Height:  |  Size: 821 KiB

View File

Before

Width:  |  Height:  |  Size: 646 KiB

After

Width:  |  Height:  |  Size: 646 KiB

View File

Before

Width:  |  Height:  |  Size: 832 KiB

After

Width:  |  Height:  |  Size: 832 KiB

View File

Before

Width:  |  Height:  |  Size: 905 KiB

After

Width:  |  Height:  |  Size: 905 KiB

View File

Before

Width:  |  Height:  |  Size: 1016 KiB

After

Width:  |  Height:  |  Size: 1016 KiB

View File

Before

Width:  |  Height:  |  Size: 267 KiB

After

Width:  |  Height:  |  Size: 267 KiB

Some files were not shown because too many files have changed in this diff Show More