2
0
Fork 0
mirror of https://github.com/Vonng/ddia.git synced 2026-06-21 00:47:05 +08:00

move zh-tw to content/tw

This commit is contained in:
Feng Ruohang 2025-07-31 07:24:12 +08:00
parent 1412470a6d
commit a8c07bb5e6
25 changed files with 1417 additions and 1243 deletions

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 負責資料基礎架構。 **作者** [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/) 以獲取最佳閱讀體驗。 > [!NOTE]
> DDIA [**第二版**](https://github.com/Vonng/ddia/tree/v2) 正在翻譯中 ([`v2`](https://github.com/Vonng/ddia/tree/v2)分支),歡迎加入並提出您的寶貴意見!
**通知**DDIA [**第二版**](https://github.com/Vonng/ddia/tree/v2) 正在翻譯中 ([`v2`](https://github.com/Vonng/ddia/tree/v2)分支),歡迎加入並提出您的寶貴意見!
---------
## 譯序 ## 譯序
> 不懂資料庫的全棧工程師不是好架構師 > 不懂資料庫的全棧工程師不是好架構師 —— 馮若航 / Vonng
>
> —— 馮若航 / Vonng
現今尤其是在網際網路領域大多數應用都屬於資料密集型應用。本書從底層資料結構到頂層架構設計將資料系統設計中的精髓娓娓道來。其中的寶貴經驗無論是對架構師、DBA、還是後端工程師、甚至產品經理都會有幫助。 現今尤其是在網際網路領域大多數應用都屬於資料密集型應用。本書從底層資料結構到頂層架構設計將資料系統設計中的精髓娓娓道來。其中的寶貴經驗無論是對架構師、DBA、還是後端工程師、甚至產品經理都會有幫助。
@ -30,110 +31,53 @@
這也是一本深入淺出的書,講述概念的來龍去脈而不是賣弄定義,介紹事物發展演化歷程而不是事實堆砌,將複雜的概念講述的淺顯易懂,但又直擊本質不失深度。每章最後的引用質量非常好,是深入學習各個主題的絕佳索引。 這也是一本深入淺出的書,講述概念的來龍去脈而不是賣弄定義,介紹事物發展演化歷程而不是事實堆砌,將複雜的概念講述的淺顯易懂,但又直擊本質不失深度。每章最後的引用質量非常好,是深入學習各個主題的絕佳索引。
本書為資料系統的設計、實現、與評價提供了很好的概念框架。讀完並理解本書內容後,讀者可以輕鬆看破大多數的技術忽悠,與技術磚家撕起來虎虎生風🤣 本書為資料系統的設計、實現、與評價提供了很好的概念框架。讀完並理解本書內容後,讀者可以輕鬆看破大多數的技術忽悠,與技術磚家撕起來虎虎生風。
這是 2017 年譯者讀過最好的一本技術類書籍,這麼好的書沒有中文翻譯,實在是遺憾。某不才,願為先進技術文化的傳播貢獻一份力量。既可以深入學習有趣的技術主題,又可以鍛鍊中英文語言文字功底,何樂而不為? 這是 2017 年譯者讀過最好的一本技術類書籍,這麼好的書沒有中文翻譯,實在是遺憾。某不才,願為先進技術文化的傳播貢獻一份力量。既可以深入學習有趣的技術主題,又可以鍛鍊中英文語言文字功底,何樂而不為?
---------
## 前言 ## 前言
> 在我們的社會中,技術是一種強大的力量。資料、軟體、通訊可以用於壞的方面:不公平的階級固化,損害公民權利,保護既得利益集團。但也可以用於好的方面:讓底層人民發出自己的聲音,讓每個人都擁有機會,避免災難。本書獻給所有將技術用於善途的人們。 > 在我們的社會中,技術是一種強大的力量。資料、軟體、通訊可以用於壞的方面:不公平的階級固化,損害公民權利,保護既得利益集團。但也可以用於好的方面:讓底層人民發出自己的聲音,讓每個人都擁有機會,避免災難。本書獻給所有將技術用於善途的人們。
---------
> 計算是一種流行文化,流行文化鄙視歷史。流行文化關乎個體身份和參與感,但與合作無關。流行文化活在當下,也與過去和未來無關。我認為大部分(為了錢)編寫程式碼的人就是這樣的,他們不知道自己的文化來自哪裡。 > 計算是一種流行文化,流行文化鄙視歷史。流行文化關乎個體身份和參與感,但與合作無關。流行文化活在當下,也與過去和未來無關。我認為大部分(為了錢)編寫程式碼的人就是這樣的,他們不知道自己的文化來自哪裡。
> >
> —— 阿蘭・凱接受 Dobb 博士的雜誌採訪時2012 年) > —— 阿蘭・凱接受 Dobb 博士的雜誌採訪時2012 年)
---------
## 目錄 ## 目錄
### [序言](preface.md) ### [序言](/tw/preface)
### [第一部分:資料系統基礎](part-i.md) ### [第一部分:資料系統基礎](/tw/part-i)
* [第一章:可靠性、可伸縮性和可維護性](ch1.md) * [第一章:可靠性、可伸縮性和可維護性](/tw/ch1)
* [關於資料系統的思考](ch1.md#關於資料系統的思考) * [第二章:資料模型與查詢語言](/tw/ch2)
* [可靠性](ch1.md#可靠性) * [第三章:儲存與檢索](/tw/ch3)
* [可伸縮性](ch1.md#可伸縮性) * [第四章:編碼與演化](/tw/ch4)
* [可維護性](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) ### [第二部分:分散式資料](/tw/part-ii)
* [第五章:複製](ch5.md) * [第五章:複製](/tw/ch5)
* [領導者與追隨者](ch5.md#領導者與追隨者) * [第六章:分割槽](/tw/ch6)
* [複製延遲問題](ch5.md#複製延遲問題) * [第七章:事務](/tw/ch7)
* [多主複製](ch5.md#多主複製) * [第八章:分散式系統的麻煩](/tw/ch8)
* [無主複製](ch5.md#無主複製) * [第九章:一致性與共識](/tw/ch9)
* [本章小結](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) ### [第三部分:衍生資料](/tw/part-iii)
* [第十章:批處理](ch10.md) * [第十章:批處理](/tw/ch10)
* [使用Unix工具的批處理](ch10.md#使用Unix工具的批處理) * [第十一章:流處理](/tw/ch11)
* [MapReduce和分散式檔案系統](ch10.md#MapReduce和分散式檔案系統) * [第十二章:資料系統的未來](/tw/ch12)
* [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) ### [術語表](/tw/glossary)
### [後記](colophon.md)
### [後記](/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) 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) 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) 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) 4. [第一部分](/tw/part-i)前言,[ch2](/tw/ch2)校正 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) 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) 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) 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> <details>
<summary><a href="https://github.com/Vonng/ddia/pulls">Pull Requests</a> & <a href="https://github.com/Vonng/ddia/issues">Issues</a></summary> <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 | | ISSUE & Pull Requests | USER | Title |
|-------------------------------------------------|------------------------------------------------------------|----------------------------------------------------------------| |-------------------------------------------------|------------------------------------------------------------|----------------------------------------------------------------|
| [359](https://github.com/Vonng/ddia/pull/359) | [@c25423](https://github.com/c25423) | ch10: 修正一處拼寫錯誤 | | [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: 修正一處拼寫錯誤 | | [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: 修正一處標點錯誤 | | [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: 修正一處格式錯誤 | | [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: 修正一處參考連結 | | [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: 修正兩處引用錯誤 | | [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 格式 | | [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: 修正一處格式錯誤 | | [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: 修正一處影像連結 | | [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: 最佳化一處翻譯 | | [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: 最佳化一處翻譯 | | [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: 最佳化兩處翻譯 | | [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: 最佳化多處翻譯 | | [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 | | [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語法微調 | | [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) | 序言初翻 | | [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 年) > —— [艾倫・凱](http://www.drdobbs.com/architecture-and-design/interview-with-alan-kay/240003442) 在接受 Dobb 博士雜誌採訪時說2012 年)
-----------------------
[TOC]
現今很多應用程式都是 **資料密集型data-intensive** 的,而非 **計算密集型compute-intensive** 的。因此 CPU 很少成為這類應用的瓶頸,更大的問題通常來自資料量、資料複雜性、以及資料的變更速度。 現今很多應用程式都是 **資料密集型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 一個可能的組合使用多個元件的資料系統架構** **圖 1-1 一個可能的組合使用多個元件的資料系統架構**
@ -166,7 +168,7 @@
大體上講,這一對操作有兩種實現方式。 大體上講,這一對操作有兩種實現方式。
1. 釋出推文時,只需將新推文插入全域性推文集合即可。當一個使用者請求自己的主頁時間線時,首先查詢他關注的所有人,查詢這些被關注使用者釋出的推文並按時間順序合併。在如 [圖 1-2](../img/fig1-2.png) 所示的關係型資料庫中,可以編寫這樣的查詢: 1. 釋出推文時,只需將新推文插入全域性推文集合即可。當一個使用者請求自己的主頁時間線時,首先查詢他關注的所有人,查詢這些被關注使用者釋出的推文並按時間順序合併。在如 [圖 1-2](/img/fig1-2.png) 所示的關係型資料庫中,可以編寫這樣的查詢:
```sql ```sql
SELECT tweets.*, users.* SELECT tweets.*, users.*
@ -176,13 +178,13 @@
WHERE follows.follower_id = current_user WHERE follows.follower_id = current_user
``` ```
![](../img/fig1-2.png) ![](/img/fig1-2.png)
**圖 1-2 推特主頁時間線的關係型模式簡單實現** **圖 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】** **圖 1-3 用於分發推特至關注者的資料流水線2012 年 11 月的負載引數【16】**
@ -192,7 +194,7 @@
在推特的例子中,每個使用者粉絲數的分佈(可能按這些使用者的發推頻率來加權)是探討可伸縮性的一個關鍵負載引數,因為它決定了扇出負載。你的應用程式可能具有非常不同的特徵,但可以採用相似的原則來考慮它的負載。 在推特的例子中,每個使用者粉絲數的分佈(可能按這些使用者的發推頻率來加權)是探討可伸縮性的一個關鍵負載引數,因為它決定了扇出負載。你的應用程式可能具有非常不同的特徵,但可以採用相似的原則來考慮它的負載。
推特軼事的最終轉折:現在已經穩健地實現了方法 2推特逐步轉向了兩種方法的混合。大多數使用者發的推文會被扇出寫入其粉絲主頁時間線快取中。但是少數擁有海量粉絲的使用者即名流會被排除在外。當用戶讀取主頁時間線時分別地獲取出該使用者所關注的每位名流的推文再與使用者的主頁時間線快取合併如方法 1 所示。這種混合方法能始終如一地提供良好效能。在 [第十二章](ch12.md) 中我們將重新討論這個例子,這在覆蓋更多技術層面之後。 推特軼事的最終轉折:現在已經穩健地實現了方法 2推特逐步轉向了兩種方法的混合。大多數使用者發的推文會被扇出寫入其粉絲主頁時間線快取中。但是少數擁有海量粉絲的使用者即名流會被排除在外。當用戶讀取主頁時間線時分別地獲取出該使用者所關注的每位名流的推文再與使用者的主頁時間線快取合併如方法 1 所示。這種混合方法能始終如一地提供良好效能。在 [第十二章](/tw/ch12) 中我們將重新討論這個例子,這在覆蓋更多技術層面之後。
### 描述效能 ### 描述效能
@ -213,9 +215,9 @@
即使不斷重複傳送同樣的請求,每次得到的響應時間也都會略有不同。現實世界的系統會處理各式各樣的請求,響應時間可能會有很大差異。因此我們需要將響應時間視為一個可以測量的數值 **分佈distribution**,而不是單個數值。 即使不斷重複傳送同樣的請求,每次得到的響應時間也都會略有不同。現實世界的系統會處理各式各樣的請求,響應時間可能會有很大差異。因此我們需要將響應時間視為一個可以測量的數值 **分佈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 次請求響應時間的均值與百分位數** **圖 1-4 展示了一個服務 100 次請求響應時間的均值與百分位數**
@ -225,7 +227,7 @@
如果想知道典型場景下使用者需要等待多長時間,那麼中位數是一個好的度量標準:一半使用者請求的響應時間少於響應時間的中位數,另一半服務時間比中位數長。中位數也被稱為第 50 百分位點,有時縮寫為 p50。注意中位數是關於單個請求的如果使用者同時發出幾個請求在一個會話過程中或者由於一個頁面中包含了多個資源則至少一個請求比中位數慢的機率遠大於 50%。 如果想知道典型場景下使用者需要等待多長時間,那麼中位數是一個好的度量標準:一半使用者請求的響應時間少於響應時間的中位數,另一半服務時間比中位數長。中位數也被稱為第 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】。 響應時間的高百分位點(也稱為 **尾部延遲**,即 **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分鐘內的請求響應時間。每一分鐘你都會計算出該視窗中的響應時間中值和各種百分數並將這些度量值繪製在圖上。 > 如果你想將響應時間百分點新增到你的服務的監視儀表板則需要持續有效地計算它們。例如你可以使用滑動視窗來跟蹤連續10分鐘內的請求響應時間。每一分鐘你都會計算出該視窗中的響應時間中值和各種百分數並將這些度量值繪製在圖上。
> >
> 簡單的實現是在時間視窗內儲存所有請求的響應時間列表,並且每分鐘對列表進行排序。如果對你來說效率太低,那麼有一些演算法能夠以最小的 CPU 和記憶體成本如前向衰減【25】、t-digest【26】或 HdrHistogram 【27】來計算百分位數的近似值。請注意平均百分比例如減少時間解析度或合併來自多臺機器的資料在數學上沒有意義 - 聚合響應時間資料的正確方法是新增直方圖【28】。 > 簡單的實現是在時間視窗內儲存所有請求的響應時間列表,並且每分鐘對列表進行排序。如果對你來說效率太低,那麼有一些演算法能夠以最小的 CPU 和記憶體成本如前向衰減【25】、t-digest【26】或 HdrHistogram 【27】來計算百分位數的近似值。請注意平均百分比例如減少時間解析度或合併來自多臺機器的資料在數學上沒有意義 - 聚合響應時間資料的正確方法是新增直方圖【28】。
![](../img/fig1-5.png) ![](/img/fig1-5.png)
**圖 1-5 當一個請求需要多個後端請求時,單個後端慢請求就會拖慢整個終端使用者的請求** **圖 1-5 當一個請求需要多個後端請求時,單個後端慢請求就會拖慢整個終端使用者的請求**
@ -257,7 +259,7 @@
人們經常討論 **縱向伸縮**scaling up也稱為垂直伸縮即 vertical scaling轉向更強大的機器**橫向伸縮**scaling out也稱為水平伸縮即 horizontal scaling將負載分佈到多臺小機器上之間的對立。跨多臺機器分配負載也稱為 “**無共享shared-nothing**” 架構。可以在單臺機器上執行的系統通常更簡單,但高階機器可能非常貴,所以非常密集的負載通常無法避免地需要橫向伸縮。現實世界中的優秀架構需要將這兩種方法務實地結合,因為使用幾臺足夠強大的機器可能比使用大量的小型虛擬機器更簡單也更便宜。 人們經常討論 **縱向伸縮**scaling up也稱為垂直伸縮即 vertical scaling轉向更強大的機器**橫向伸縮**scaling out也稱為水平伸縮即 horizontal scaling將負載分佈到多臺小機器上之間的對立。跨多臺機器分配負載也稱為 “**無共享shared-nothing**” 架構。可以在單臺機器上執行的系統通常更簡單,但高階機器可能非常貴,所以非常密集的負載通常無法避免地需要橫向伸縮。現實世界中的優秀架構需要將這兩種方法務實地結合,因為使用幾臺足夠強大的機器可能比使用大量的小型虛擬機器更簡單也更便宜。
有些系統是 **彈性elastic** 的,這意味著可以在檢測到負載增加時自動增加計算資源,而其他系統則是手動伸縮(人工分析容量並決定向系統新增更多的機器)。如果負載 **極難預測highly unpredictable**,則彈性系統可能很有用,但手動伸縮系統更簡單,並且意外操作可能會更少(請參閱 “[分割槽再平衡](ch6.md#分割槽再平衡)”)。 有些系統是 **彈性elastic** 的,這意味著可以在檢測到負載增加時自動增加計算資源,而其他系統則是手動伸縮(人工分析容量並決定向系統新增更多的機器)。如果負載 **極難預測highly unpredictable**,則彈性系統可能很有用,但手動伸縮系統更簡單,並且意外操作可能會更少(請參閱 “[分割槽再平衡](/tw/ch6#分割槽再平衡)”)。
跨多臺機器部署 **無狀態服務stateless services** 非常簡單,但將帶狀態的資料系統從單節點變為分散式配置則可能引入許多額外複雜度。出於這個原因,常識告訴我們應該將資料庫放在單個節點上(縱向伸縮),直到伸縮成本或可用性需求迫使其改為分散式。 跨多臺機器部署 **無狀態服務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. 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. 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. 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)
------
| 上一章 | 目錄 | 下一章 |
| ----------------------------------- | ------------------------------- | ------------------------------------ |
| [第一部分:資料系統基礎](part-i.md) | [設計資料密集型應用](README.md) | [第二章:資料模型與查詢語言](ch2.md) |

View file

@ -1,18 +1,19 @@
# 第十章:批處理 ---
title: "第十章:批處理"
linkTitle: "10. 批處理"
weight: 310
breadcrumbs: false
---
![](../img/ch10.png) ![](/img/ch10.png)
> 帶有太強個人色彩的系統無法成功。當最初的設計完成並且相對穩定時,不同的人們以自己的方式進行測試,真正的考驗才開始。 > 帶有太強個人色彩的系統無法成功。當最初的設計完成並且相對穩定時,不同的人們以自己的方式進行測試,真正的考驗才開始。
> >
> —— 高德納 > —— 高德納
---------------
[TOC]
在本書的前兩部分中,我們討論了很多關於 **請求****查詢** 以及相應的 **響應****結果**。許多現有資料系統中都採用這種資料處理方式你傳送請求指令一段時間後我們期望系統會給出一個結果。資料庫、快取、搜尋索引、Web 伺服器以及其他一些系統都以這種方式工作。 在本書的前兩部分中,我們討論了很多關於 **請求****查詢** 以及相應的 **響應****結果**。許多現有資料系統中都採用這種資料處理方式你傳送請求指令一段時間後我們期望系統會給出一個結果。資料庫、快取、搜尋索引、Web 伺服器以及其他一些系統都以這種方式工作。
像這樣的 **線上online** 系統,無論是瀏覽器請求頁面還是呼叫遠端 API 的服務,我們通常認為請求是由人類使用者觸發的,並且正在等待響應。他們不應該等太久,所以我們非常關注系統的響應時間(請參閱 “[描述效能](ch1.md#描述效能)”)。 像這樣的 **線上online** 系統,無論是瀏覽器請求頁面還是呼叫遠端 API 的服務,我們通常認為請求是由人類使用者觸發的,並且正在等待響應。他們不應該等太久,所以我們非常關注系統的響應時間(請參閱 “[描述效能](/tw/ch1#描述效能)”)。
Web 和越來越多的基於 HTTP/REST 的 API 使互動的請求 / 響應風格變得如此普遍,以至於很容易將其視為理所當然。但我們應該記住,這不是構建系統的唯一方式,其他方法也有其優點。我們來看看三種不同型別的系統: 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。 正如我們將在本章中看到的那樣批處理是構建可靠、可伸縮和可維護應用程式的重要組成部分。例如2004 年釋出的批處理演算法 Map-Reduce可能被過分熱情地被稱為 “造就 Google 大規模可伸縮性的演算法”【2】。隨後在各種開源資料系統中得到應用包括 Hadoop、CouchDB 和 MongoDB。
@ -122,7 +123,7 @@ Ruby 指令碼在記憶體中儲存了一個 URL 的雜湊表,將每個 URL
哪種方法更好?這取決於你有多少個不同的 URL。對於大多數中小型網站你可能可以為所有不同網址提供一個計數器假設我們使用 1GB 記憶體)。在此例中,作業的 **工作集**working set即作業需要隨機訪問的記憶體大小僅取決於不同 URL 的數量:如果日誌中只有單個 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 命令鏈很容易伸縮至大資料集,且不會耗盡記憶體。瓶頸可能是從磁碟讀取輸入檔案的速度。 GNU CoreutilsLinux中的 `sort` 程式透過溢位至磁碟的方式來自動應對大於記憶體的資料集,並能同時使用多個 CPU 核進行並行排序【9】。這意味著我們之前看到的簡單的 Unix 命令鏈很容易伸縮至大資料集,且不會耗盡記憶體。瓶頸可能是從磁碟讀取輸入檔案的速度。
@ -203,11 +204,11 @@ MapReduce 有點像 Unix 工具,但分佈在數千臺機器上。像 Unix 工
[^iv]: 一個不同之處在於,對於 HDFS可以將計算任務安排在儲存特定檔案副本的計算機上執行而物件儲存通常將儲存和計算分開。如果網路頻寬是一個瓶頸從本地磁碟讀取有效能優勢。但是請注意如果使用糾刪碼Erasure Coding則會丟失區域性因為來自多臺機器的資料必須進行合併以重建原始檔案【20】。 [^iv]: 一個不同之處在於,對於 HDFS可以將計算任務安排在儲存特定檔案副本的計算機上執行而物件儲存通常將儲存和計算分開。如果網路頻寬是一個瓶頸從本地磁碟讀取有效能優勢。但是請注意如果使用糾刪碼Erasure Coding則會丟失區域性因為來自多臺機器的資料必須進行合併以重建原始檔案【20】。
與網路連線儲存NAS和儲存區域網路SAN架構的共享磁碟方法相比HDFS 基於 **無共享** 原則(請參閱 [第二部分](part-ii.md) 的介紹)。共享磁碟儲存由集中式儲存裝置實現,通常使用定製硬體和專用網路基礎設施(如光纖通道)。而另一方面,無共享方法不需要特殊的硬體,只需要透過傳統資料中心網路連線的計算機。 與網路連線儲存NAS和儲存區域網路SAN架構的共享磁碟方法相比HDFS 基於 **無共享** 原則(請參閱 [第二部分](/tw/part-ii) 的介紹)。共享磁碟儲存由集中式儲存裝置實現,通常使用定製硬體和專用網路基礎設施(如光纖通道)。而另一方面,無共享方法不需要特殊的硬體,只需要透過傳統資料中心網路連線的計算機。
HDFS 在每臺機器上運行了一個守護程序,它對外暴露網路服務,允許其他節點訪問儲存在該機器上的檔案(假設資料中心中的每臺通用計算機都掛載著一些磁碟)。名為 **NameNode** 的中央伺服器會跟蹤哪個檔案塊儲存在哪臺機器上。因此HDFS 在概念上建立了一個大型檔案系統,可以使用所有執行有守護程序的機器的磁碟。 HDFS 在每臺機器上運行了一個守護程序,它對外暴露網路服務,允許其他節點訪問儲存在該機器上的檔案(假設資料中心中的每臺通用計算機都掛載著一些磁碟)。名為 **NameNode** 的中央伺服器會跟蹤哪個檔案塊儲存在哪臺機器上。因此HDFS 在概念上建立了一個大型檔案系統,可以使用所有執行有守護程序的機器的磁碟。
為了容忍機器和磁碟故障,檔案塊被複制到多臺機器上。複製可能意味著多個機器上的相同資料的多個副本,如 [第五章](ch5.md) 中所述,或者諸如 Reed-Solomon 碼這樣的糾刪碼方案它能以比完全複製更低的儲存開銷來支援恢復丟失的資料【20,22】。這些技術與 RAID 相似,後者可以在連線到同一臺機器的多個磁碟上提供冗餘;區別在於在分散式檔案系統中,檔案訪問和複製是在傳統的資料中心網路上完成的,沒有特殊的硬體。 為了容忍機器和磁碟故障,檔案塊被複制到多臺機器上。複製可能意味著多個機器上的相同資料的多個副本,如 [第五章](/tw/ch5) 中所述,或者諸如 Reed-Solomon 碼這樣的糾刪碼方案它能以比完全複製更低的儲存開銷來支援恢復丟失的資料【20,22】。這些技術與 RAID 相似,後者可以在連線到同一臺機器的多個磁碟上提供冗餘;區別在於在分散式檔案系統中,檔案訪問和複製是在傳統的資料中心網路上完成的,沒有特殊的硬體。
HDFS 的可伸縮性已經很不錯了:在撰寫本書時,最大的 HDFS 部署執行在上萬臺機器上,總儲存容量達數百 PB【23】。如此大的規模已經變得可行因為使用商品硬體和開源軟體的 HDFS 上的資料儲存和訪問成本遠低於在專用儲存裝置上支援同等容量的成本【24】。 HDFS 的可伸縮性已經很不錯了:在撰寫本書時,最大的 HDFS 部署執行在上萬臺機器上,總儲存容量達數百 PB【23】。如此大的規模已經變得可行因為使用商品硬體和開源軟體的 HDFS 上的資料儲存和訪問成本遠低於在專用儲存裝置上支援同等容量的成本【24】。
@ -222,7 +223,7 @@ MapReduce 是一個程式設計框架,你可以使用它編寫程式碼來處
這四個步驟可以作為一個 MapReduce 作業執行。步驟 2Map和 4Reduce是你編寫自定義資料處理程式碼的地方。步驟 1將檔案分解成記錄由輸入格式解析器處理。步驟 3 中的排序步驟隱含在 MapReduce 中 —— 你不必編寫它,因為 Mapper 的輸出始終在送往 Reducer 之前進行排序。 這四個步驟可以作為一個 MapReduce 作業執行。步驟 2Map和 4Reduce是你編寫自定義資料處理程式碼的地方。步驟 1將檔案分解成記錄由輸入格式解析器處理。步驟 3 中的排序步驟隱含在 MapReduce 中 —— 你不必編寫它,因為 Mapper 的輸出始終在送往 Reducer 之前進行排序。
要建立 MapReduce 作業你需要實現兩個回撥函式Mapper 和 Reducer其行為如下請參閱 “[MapReduce 查詢](ch2.md#MapReduce查詢)”): 要建立 MapReduce 作業你需要實現兩個回撥函式Mapper 和 Reducer其行為如下請參閱 “[MapReduce 查詢](/tw/ch2#MapReduce查詢)”):
* Mapper * Mapper
@ -238,21 +239,21 @@ MapReduce 是一個程式設計框架,你可以使用它編寫程式碼來處
MapReduce 與 Unix 命令管道的主要區別在於MapReduce 可以在多臺機器上並行執行計算而無需編寫程式碼來顯式處理並行問題。Mapper 和 Reducer 一次只能處理一條記錄;它們不需要知道它們的輸入來自哪裡,或者輸出去往什麼地方,所以框架可以處理在機器之間移動資料的複雜性。 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】它節省了透過網路複製輸入檔案的開銷減少網路負載並增加區域性。 每個輸入檔案的大小通常是數百兆位元組。MapReduce 排程器(圖中未顯示)試圖在其中一臺儲存輸入檔案副本的機器上執行每個 Mapper只要該機器有足夠的備用 RAM 和 CPU 資源來執行 Mapper 任務【26】。這個原則被稱為 **將計算放在資料附近**【27】它節省了透過網路複製輸入檔案的開銷減少網路負載並增加區域性。
![](../img/fig10-1.png) ![](/img/fig10-1.png)
**圖 10-1 具有三個 Mapper 和三個 Reducer 的 MapReduce 任務** **圖 10-1 具有三個 Mapper 和三個 Reducer 的 MapReduce 任務**
在大多數情況下,應該在 Mapper 任務中執行的應用程式碼在將要執行它的機器上還不存在,所以 MapReduce 框架首先將程式碼(例如 Java 程式中的 JAR 檔案)複製到適當的機器。然後啟動 Map 任務並開始讀取輸入檔案,一次將一條記錄傳入 Mapper 回撥函式。Mapper 的輸出由鍵值對組成。 在大多數情況下,應該在 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 中的混洗沒有隨機性)。 只要當 Mapper 讀取完輸入檔案並寫完排序後的輸出檔案MapReduce 排程器就會通知 Reducer 可以從該 Mapper 開始獲取輸出檔案。Reducer 連線到每個 Mapper並下載自己相應分割槽的有序鍵值對檔案。按 Reducer 分割槽,排序,從 Mapper 向 Reducer 複製分割槽資料,這一整個過程被稱為 **混洗shuffle**【26】一個容易混淆的術語 —— 不像洗牌,在 MapReduce 中的混洗沒有隨機性)。
@ -276,23 +277,23 @@ Hadoop 的各種高階工具(如 Pig 【30】、Hive 【31】、Cascading 【3
### Reduce側連線與分組 ### Reduce側連線與分組
我們在 [第二章](ch2.md) 中討論了資料模型和查詢語言的連線,但是我們還沒有深入探討連線是如何實現的。現在是我們再次撿起這條線索的時候了。 我們在 [第二章](/tw/ch2) 中討論了資料模型和查詢語言的連線,但是我們還沒有深入探討連線是如何實現的。現在是我們再次撿起這條線索的時候了。
在許多資料集中,一條記錄與另一條記錄存在關聯是很常見的:關係模型中的 **外部索引鍵**,文件模型中的 **文件引用** 或圖模型中的 **邊**。當你需要同時訪問這一關聯的兩側(持有引用的記錄與被引用的記錄)時,連線就是必須的。正如 [第二章](ch2.md) 所討論的,非規範化可以減少對連線的需求,但通常無法將其完全移除 [^v]。 在許多資料集中,一條記錄與另一條記錄存在關聯是很常見的:關係模型中的 **外部索引鍵**,文件模型中的 **文件引用** 或圖模型中的 **邊**。當你需要同時訪問這一關聯的兩側(持有引用的記錄與被引用的記錄)時,連線就是必須的。正如 [第二章](/tw/ch2) 所討論的,非規範化可以減少對連線的需求,但通常無法將其完全移除 [^v]。
[^v]: 我們在本書中討論的連線通常是等值連線,即最常見的連線型別,其中記錄透過與其他記錄在特定欄位(例如 ID中具有 **相同值** 相關聯。有些資料庫支援更通用的連線型別,例如使用小於運算子而不是等號運算子,但是我們沒有地方來講這些東西。 [^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 使用者行為日誌與使用者檔案的連線** **圖 10-2 使用者行為日誌與使用者檔案的連線**
@ -302,13 +303,13 @@ Hadoop 的各種高階工具(如 Pig 【30】、Hive 【31】、Cascading 【3
為了在批處理過程中實現良好的吞吐量,計算必須(儘可能)限於單臺機器上進行。為待處理的每條記錄發起隨機訪問的網路請求實在是太慢了。而且,查詢遠端資料庫意味著批處理作業變為 **非確定的nondeterministic**,因為遠端資料庫中的資料可能會改變。 為了在批處理過程中實現良好的吞吐量,計算必須(儘可能)限於單臺機器上進行。為待處理的每條記錄發起隨機訪問的網路請求實在是太慢了。而且,查詢遠端資料庫意味著批處理作業變為 **非確定的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 並行處理** **圖 10-3 在使用者 ID 上進行的 Reduce 端連線。如果輸入資料集分割槽為多個檔案,則每個分割槽都會被多個 Mapper 並行處理**
@ -344,11 +345,11 @@ Hadoop 的各種高階工具(如 Pig 【30】、Hive 【31】、Cascading 【3
如果存在與單個鍵關聯的大量資料,則 “將具有相同鍵的所有記錄放到相同的位置” 這種模式就被破壞了。例如在社交網路中,大多數使用者可能會與幾百人有連線,但少數名人可能有數百萬的追隨者。這種不成比例的活動資料庫記錄被稱為 **關鍵物件linchpin object**【38】或 **熱鍵hot key** 如果存在與單個鍵關聯的大量資料,則 “將具有相同鍵的所有記錄放到相同的位置” 這種模式就被破壞了。例如在社交網路中,大多數使用者可能會與幾百人有連線,但少數名人可能有數百萬的追隨者。這種不成比例的活動資料庫記錄被稱為 **關鍵物件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】。 如果連線的輸入存在熱鍵可以使用一些演算法進行補償。例如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 端連線(請參閱下一節)。 Hive 的偏斜連線最佳化採取了另一種方法。它需要在表格元資料中顯式指定熱鍵,並將與這些鍵相關的記錄單獨存放,與其它檔案分開。當在該表上執行連線時,對於熱鍵,它會使用 Map 端連線(請參閱下一節)。
@ -367,11 +368,11 @@ Reduce 側方法的優點是不需要對輸入資料做任何假設:無論其
適用於執行 Map 端連線的最簡單場景是大資料集與小資料集連線的情況。要點在於小資料集需要足夠小,以便可以將其全部載入到每個 Mapper 的記憶體中。 適用於執行 Map 端連線的最簡單場景是大資料集與小資料集連線的情況。要點在於小資料集需要足夠小,以便可以將其全部載入到每個 Mapper 的記憶體中。
例如,假設在 [圖 10-2](../img/fig10-2.png) 的情況下,使用者資料庫小到足以放進記憶體中。在這種情況下,當 Mapper 啟動時它可以首先將使用者資料庫從分散式檔案系統讀取到記憶體中的散列表中。完成此操作後Mapper 可以掃描使用者活動事件,並簡單地在散列表中查詢每個事件的使用者 ID [^vi]。 例如,假設在 [圖 10-2](/img/fig10-2.png) 的情況下,使用者資料庫小到足以放進記憶體中。在這種情況下,當 Mapper 啟動時它可以首先將使用者資料庫從分散式檔案系統讀取到記憶體中的散列表中。完成此操作後Mapper 可以掃描使用者活動事件,並簡單地在散列表中查詢每個事件的使用者 ID [^vi]。
[^vi]: 這個例子假定散列表中的每個鍵只有一個條目,這對使用者資料庫(使用者 ID 唯一標識一個使用者)可能是正確的。通常,雜湊表可能需要包含具有相同鍵的多個條目,而連線運算子將對每個鍵輸出所有的匹配。 [^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】。 這種簡單有效的演算法被稱為 **廣播雜湊連線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 都可以在記憶體散列表中少放點資料。 如果分割槽正確無誤,可以確定的是,所有你可能需要連線的記錄都落在同一個編號的分割槽中。因此每個 Mapper 只需要從輸入兩端各讀取一個分割槽就足夠了。好處是每個 Mapper 都可以在記憶體散列表中少放點資料。
@ -406,7 +407,7 @@ Reduce 側方法的優點是不需要對輸入資料做任何假設:無論其
我們已經說了很多用於實現 MapReduce 工作流的演算法,但卻忽略了一個重要的問題:這些處理完成之後的最終結果是什麼?我們最開始為什麼要跑這些作業? 我們已經說了很多用於實現 MapReduce 工作流的演算法,但卻忽略了一個重要的問題:這些處理完成之後的最終結果是什麼?我們最開始為什麼要跑這些作業?
在資料庫查詢的場景中我們將事務處理OLTP與分析兩種目的區分開來請參閱 “[事務處理還是分析?](ch3.md#事務處理還是分析?)”。我們看到OLTP 查詢通常根據鍵查詢少量記錄,使用索引,並將其呈現給使用者(比如在網頁上)。另一方面,分析查詢通常會掃描大量記錄,執行分組與聚合,輸出通常有著報告的形式:顯示某個指標隨時間變化的圖表,或按照某種排位取前 10 項,或將一些數字細化為子類。這種報告的消費者通常是需要做出商業決策的分析師或經理。 在資料庫查詢的場景中我們將事務處理OLTP與分析兩種目的區分開來請參閱 “[事務處理還是分析?](/tw/ch3#事務處理還是分析?)”。我們看到OLTP 查詢通常根據鍵查詢少量記錄,使用索引,並將其呈現給使用者(比如在網頁上)。另一方面,分析查詢通常會掃描大量記錄,執行分組與聚合,輸出通常有著報告的形式:顯示某個指標隨時間變化的圖表,或按照某種排位取前 10 項,或將一些數字細化為子類。這種報告的消費者通常是需要做出商業決策的分析師或經理。
批處理放哪裡合適?它不屬於事務處理,也不是分析。它和分析比較接近,因為批處理通常會掃過輸入資料集的絕大部分。然而 MapReduce 作業工作流與用於分析目的的 SQL 查詢是不同的(請參閱 “[Hadoop 與分散式資料庫的對比](#Hadoop與分散式資料庫的對比)”)。批處理過程的輸出通常不是報表,而是一些其他型別的結構。 批處理放哪裡合適?它不屬於事務處理,也不是分析。它和分析比較接近,因為批處理通常會掃過輸入資料集的絕大部分。然而 MapReduce 作業工作流與用於分析目的的 SQL 查詢是不同的(請參閱 “[Hadoop 與分散式資料庫的對比](#Hadoop與分散式資料庫的對比)”)。批處理過程的輸出通常不是報表,而是一些其他型別的結構。
@ -414,15 +415,15 @@ Reduce 側方法的優點是不需要對輸入資料做任何假設:無論其
Google 最初使用 MapReduce 是為其搜尋引擎建立索引,其實現為由 5 到 10 個 MapReduce 作業組成的工作流【1】。雖然 Google 後來也不僅僅是為這個目的而使用 MapReduce 【43】但如果從構建搜尋索引的角度來看更能幫助理解 MapReduce。直至今日Hadoop MapReduce 仍然是為 Lucene/Solr 構建索引的好方法【44】 Google 最初使用 MapReduce 是為其搜尋引擎建立索引,其實現為由 5 到 10 個 MapReduce 作業組成的工作流【1】。雖然 Google 後來也不僅僅是為這個目的而使用 MapReduce 【43】但如果從構建搜尋索引的角度來看更能幫助理解 MapReduce。直至今日Hadoop MapReduce 仍然是為 Lucene/Solr 構建索引的好方法【44】
我們在 “[全文搜尋和模糊索引](ch3.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 作業中構建資料庫檔案,包括 Voldemort 【46】、Terrapin 【47】、ElephantDB 【48】和 HBase 批次載入【49】。
構建這些資料庫檔案是 MapReduce 的一種好用法:使用 Mapper 提取出鍵並按該鍵排序已經完成了構建索引所必需的大量工作。由於這些鍵值儲存大多都是隻讀的檔案只能由批處理作業一次性寫入然後就不可變所以資料結構非常簡單。比如它們就不需要預寫式日誌WAL請參閱 “[讓 B 樹更可靠](ch3.md#讓B樹更可靠)”)。 構建這些資料庫檔案是 MapReduce 的一種好用法:使用 Mapper 提取出鍵並按該鍵排序已經完成了構建索引所必需的大量工作。由於這些鍵值儲存大多都是隻讀的檔案只能由批處理作業一次性寫入然後就不可變所以資料結構非常簡單。比如它們就不需要預寫式日誌WAL請參閱 “[讓 B 樹更可靠](/tw/ch3#讓B樹更可靠)”)。
將資料載入到 Voldemort 時伺服器將繼續用舊資料檔案服務請求同時將新資料檔案從分散式檔案系統複製到伺服器的本地磁碟。一旦複製完成伺服器會自動將查詢切換到新檔案。如果在這個過程中出現任何問題它可以輕易回滾至舊檔案因為它們仍然存在而且不可變【46】。 將資料載入到 Voldemort 時伺服器將繼續用舊資料檔案服務請求同時將新資料檔案從分散式檔案系統複製到伺服器的本地磁碟。一旦複製完成伺服器會自動將查詢切換到新檔案。如果在這個過程中出現任何問題它可以輕易回滾至舊檔案因為它們仍然存在而且不可變【46】。
@ -456,7 +457,7 @@ MapReduce 作業的輸出處理遵循同樣的原理。透過將輸入視為不
- 同一組檔案可用作各種不同作業的輸入,包括計算指標的監控作業並且評估作業的輸出是否具有預期的性質(例如,將其與前一次執行的輸出進行比較並測量差異) 。 - 同一組檔案可用作各種不同作業的輸入,包括計算指標的監控作業並且評估作業的輸出是否具有預期的性質(例如,將其與前一次執行的輸出進行比較並測量差異) 。
- 與 Unix 工具類似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與分散式資料庫的對比 ### Hadoop與分散式資料庫的對比
@ -474,11 +475,11 @@ MapReduce 作業的輸出處理遵循同樣的原理。透過將輸入視為不
在純粹主義者看來,這種仔細的建模和匯入似乎是可取的,因為這意味著資料庫的使用者有更高質量的資料來處理。然而實踐經驗表明,簡單地使資料快速可用 —— 即使它很古怪,難以使用,使用原始格式 —— 也通常要比事先決定理想資料模型要更有價值【54】。 在純粹主義者看來,這種仔細的建模和匯入似乎是可取的,因為這意味著資料庫的使用者有更高質量的資料來處理。然而實踐經驗表明,簡單地使資料快速可用 —— 即使它很古怪,難以使用,使用原始格式 —— 也通常要比事先決定理想資料模型要更有價值【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 方式中,不需要將資料匯入到幾個不同的專用系統中進行不同型別的處理:系統足夠靈活,可以支援同一個叢集內不同的工作負載。不需要移動資料,使得從資料中挖掘價值變得容易得多,也使採用新的處理模型容易的多。
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 非常穩健:你可以使用它在任務會頻繁終止的多租戶系統上處理幾乎任意大量級的資料,並且仍然可以完成工作(雖然速度很慢)。另一方面,對於某些型別的處理而言,其他工具有時會快上幾個數量級。 但是MapReduce 執行模型本身也存在一些問題這些問題並沒有透過增加另一個抽象層次而解決而對於某些型別的處理它表現得非常差勁。一方面MapReduce 非常穩健:你可以使用它在任務會頻繁終止的多租戶系統上處理幾乎任意大量級的資料,並且仍然可以完成工作(雖然速度很慢)。另一方面,對於某些型別的處理而言,其他工具有時會快上幾個數量級。
在本章的其餘部分中,我們將介紹一些批處理方法。在 [第十一章](ch11.md) 我們將轉向流處理,它可以看作是加速批處理的另一種方法。 在本章的其餘部分中,我們將介紹一些批處理方法。在 [第十一章](/tw/ch11) 我們將轉向流處理,它可以看作是加速批處理的另一種方法。
### 物化中間狀態 ### 物化中間狀態
@ -538,7 +539,7 @@ MapReduce 方式更適用於較大的作業:要處理如此之多的資料並
但在很多情況下,你知道一個作業的輸出只能用作另一個作業的輸入,這些作業由同一個團隊維護。在這種情況下,分散式檔案系統上的檔案只是簡單的 **中間狀態intermediate state**:一種將資料從一個作業傳遞到下一個作業的方式。在一個用於構建推薦系統的,由 50 或 100 個 MapReduce 作業組成的複雜工作流中存在著很多這樣的中間狀態【29】。 但在很多情況下,你知道一個作業的輸出只能用作另一個作業的輸入,這些作業由同一個團隊維護。在這種情況下,分散式檔案系統上的檔案只是簡單的 **中間狀態intermediate state**:一種將資料從一個作業傳遞到下一個作業的方式。在一個用於構建推薦系統的,由 50 或 100 個 MapReduce 作業組成的複雜工作流中存在著很多這樣的中間狀態【29】。
將這個中間狀態寫入檔案的過程稱為 **物化materialization**。(在 “[聚合:資料立方體和物化檢視](ch3.md#聚合:資料立方體和物化檢視)” 中已經在物化檢視的背景中遇到過這個術語。它意味著對某個操作的結果立即求值並寫出來,而不是在請求時按需計算) 將這個中間狀態寫入檔案的過程稱為 **物化materialization**。(在 “[聚合:資料立方體和物化檢視](/tw/ch3#聚合:資料立方體和物化檢視)” 中已經在物化檢視的背景中遇到過這個術語。它意味著對某個操作的結果立即求值並寫出來,而不是在請求時按需計算)
作為對照,本章開頭的日誌分析示例使用 Unix 管道將一個命令的輸出與另一個命令的輸入連線起來。管道並沒有完全物化中間狀態,而是隻使用一個小的記憶體緩衝區,將輸出增量地 **流stream** 向輸入。 作為對照,本章開頭的日誌分析示例使用 Unix 管道將一個命令的輸出與另一個命令的輸入連線起來。管道並沒有完全物化中間狀態,而是隻使用一個小的記憶體緩衝區,將輸出增量地 **流stream** 向輸入。
@ -597,13 +598,13 @@ Spark、Flink 和 Tez 避免將中間狀態寫入 HDFS因此它們採取了
### 圖與迭代處理 ### 圖與迭代處理
在 “[圖資料模型](ch2.md#圖資料模型)” 中,我們討論了使用圖來建模資料,並使用圖查詢語言來遍歷圖中的邊與點。[第二章](ch2.md) 的討論集中在 OLTP 風格的應用場景:快速執行查詢來查詢少量符合特定條件的頂點。 在 “[圖資料模型](/tw/ch2#圖資料模型)” 中,我們討論了使用圖來建模資料,並使用圖查詢語言來遍歷圖中的邊與點。[第二章](/tw/ch2) 的討論集中在 OLTP 風格的應用場景:快速執行查詢來查詢少量符合特定條件的頂點。
批處理上下文中的圖也很有趣,其目標是在整個圖上執行某種離線處理或分析。這種需求經常出現在機器學習應用(如推薦引擎)或排序系統中。例如,最著名的圖形分析演算法之一是 PageRank 【69】它試圖根據連結到某個網頁的其他網頁來估計該網頁的流行度。它作為配方的一部分用於確定網路搜尋引擎呈現結果的順序。 批處理上下文中的圖也很有趣,其目標是在整個圖上執行某種離線處理或分析。這種需求經常出現在機器學習應用(如推薦引擎)或排序系統中。例如,最著名的圖形分析演算法之一是 PageRank 【69】它試圖根據連結到某個網頁的其他網頁來估計該網頁的流行度。它作為配方的一部分用於確定網路搜尋引擎呈現結果的順序。
> 像 Spark、Flink 和 Tez 這樣的資料流引擎(請參閱 “[物化中間狀態](#物化中間狀態)”)通常將運算元作為 **有向無環圖DAG** 的一部分安排在作業中。這與圖處理不一樣:在資料流引擎中,**從一個運算元到另一個運算元的資料流** 被構造成一個圖,而資料本身通常由關係型元組構成。在圖處理中,資料本身具有圖的形式。又一個不幸的命名混亂! > 像 Spark、Flink 和 Tez 這樣的資料流引擎(請參閱 “[物化中間狀態](#物化中間狀態)”)通常將運算元作為 **有向無環圖DAG** 的一部分安排在作業中。這與圖處理不一樣:在資料流引擎中,**從一個運算元到另一個運算元的資料流** 被構造成一個圖,而資料本身通常由關係型元組構成。在圖處理中,資料本身具有圖的形式。又一個不幸的命名混亂!
許多圖演算法是透過一次遍歷一條邊來表示的,將一個頂點與近鄰的頂點連線起來,以傳播一些資訊,並不斷重複,直到滿足一些條件為止 —— 例如,直到沒有更多的邊要跟進,或直到一些指標收斂。我們在 [圖 2-6](../img/fig2-6.png) 中看到一個例子,它透過重複跟進標明地點歸屬關係的邊,生成了資料庫中北美包含的所有地點列表(這種演算法被稱為 **傳遞閉包**,即 transitive closure 許多圖演算法是透過一次遍歷一條邊來表示的,將一個頂點與近鄰的頂點連線起來,以傳播一些資訊,並不斷重複,直到滿足一些條件為止 —— 例如,直到沒有更多的邊要跟進,或直到一些指標收斂。我們在 [圖 2-6](/img/fig2-6.png) 中看到一個例子,它透過重複跟進標明地點歸屬關係的邊,生成了資料庫中北美包含的所有地點列表(這種演算法被稱為 **傳遞閉包**,即 transitive closure
可以在分散式檔案系統中儲存圖(包含頂點和邊的列表的檔案),但是這種 “重複至完成” 的想法不能用普通的 MapReduce 來表示,因為它只掃過一趟資料。這種演算法因此經常以 **迭代** 的風格實現: 可以在分散式檔案系統中儲存圖(包含頂點和邊的列表的檔案),但是這種 “重複至完成” 的想法不能用普通的 MapReduce 來表示,因為它只掃過一趟資料。這種演算法因此經常以 **迭代** 的風格實現:
@ -621,13 +622,13 @@ Spark、Flink 和 Tez 避免將中間狀態寫入 HDFS因此它們採取了
在每次迭代中,為每個頂點呼叫一個函式,將所有傳送給它的訊息傳遞給它 —— 就像呼叫 Reducer 一樣。與 MapReduce 的不同之處在於,在 Pregel 模型中,頂點在一次迭代到下一次迭代的過程中會記住它的狀態,所以這個函式只需要處理新的傳入訊息。如果圖的某個部分沒有被傳送訊息,那裡就不需要做任何工作。 在每次迭代中,為每個頂點呼叫一個函式,將所有傳送給它的訊息傳遞給它 —— 就像呼叫 Reducer 一樣。與 MapReduce 的不同之處在於,在 Pregel 模型中,頂點在一次迭代到下一次迭代的過程中會記住它的狀態,所以這個函式只需要處理新的傳入訊息。如果圖的某個部分沒有被傳送訊息,那裡就不需要做任何工作。
這與 Actor 模型有些相似(請參閱 “[分散式的 Actor 框架](ch4.md#分散式的Actor框架)”除了頂點狀態和頂點之間的訊息具有容錯性和永續性且通訊以固定的回合進行在每次迭代中框架遞送上次迭代中傳送的所有訊息。Actor 通常沒有這樣的時序保證。 這與 Actor 模型有些相似(請參閱 “[分散式的 Actor 框架](/tw/ch4#分散式的Actor框架)”除了頂點狀態和頂點之間的訊息具有容錯性和永續性且通訊以固定的回合進行在每次迭代中框架遞送上次迭代中傳送的所有訊息。Actor 通常沒有這樣的時序保證。
#### 容錯 #### 容錯
頂點只能透過訊息傳遞進行通訊(而不是直接相互查詢)的事實有助於提高 Pregel 作業的效能,因為訊息可以成批處理,且等待通訊的次數也減少了。唯一的等待是在迭代之間:由於 Pregel 模型保證所有在一輪迭代中傳送的訊息都在下輪迭代中送達,所以在下一輪迭代開始前,先前的迭代必須完全完成,而所有的訊息必須在網路上完成複製。 頂點只能透過訊息傳遞進行通訊(而不是直接相互查詢)的事實有助於提高 Pregel 作業的效能,因為訊息可以成批處理,且等待通訊的次數也減少了。唯一的等待是在迭代之間:由於 Pregel 模型保證所有在一輪迭代中傳送的訊息都在下輪迭代中送達,所以在下一輪迭代開始前,先前的迭代必須完全完成,而所有的訊息必須在網路上完成複製。
即使底層網路可能丟失、重複或任意延遲訊息(請參閱 “[不可靠的網路](ch8.md#不可靠的網路)”Pregel 的實現能保證在後續迭代中訊息在其目標頂點恰好處理一次。像 MapReduce 一樣,框架能從故障中透明地恢復,以簡化在 Pregel 上實現演算法的程式設計模型。 即使底層網路可能丟失、重複或任意延遲訊息(請參閱 “[不可靠的網路](/tw/ch8#不可靠的網路)”Pregel 的實現能保證在後續迭代中訊息在其目標頂點恰好處理一次。像 MapReduce 一樣,框架能從故障中透明地恢復,以簡化在 Pregel 上實現演算法的程式設計模型。
這種容錯是透過在迭代結束時定期存檔所有頂點的狀態來實現的即將其全部狀態寫入持久化儲存。如果某個節點發生故障並且其記憶體中的狀態丟失則最簡單的解決方法是將整個圖計算回滾到上一個存檔點然後重啟計算。如果演算法是確定性的且訊息記錄在日誌中那麼也可以選擇性地只恢復丟失的分割槽就像之前討論過的資料流引擎【72】。 這種容錯是透過在迭代結束時定期存檔所有頂點的狀態來實現的即將其全部狀態寫入持久化儲存。如果某個節點發生故障並且其記憶體中的狀態丟失則最簡單的解決方法是將整個圖計算回滾到上一個存檔點然後重啟計算。如果演算法是確定性的且訊息記錄在日誌中那麼也可以選擇性地只恢復丟失的分割槽就像之前討論過的資料流引擎【72】。
@ -658,13 +659,13 @@ Spark、Flink 和 Tez 避免將中間狀態寫入 HDFS因此它們採取了
與硬寫執行連線的程式碼相比指定連線關係運算元的優點是框架可以分析連線輸入的屬性並自動決定哪種上述連線演算法最適合當前任務。Hive、Spark 和 Flink 都有基於代價的查詢最佳化器可以做到這一點甚至可以改變連線順序最小化中間狀態的數量【66,77,78,79】。 與硬寫執行連線的程式碼相比指定連線關係運算元的優點是框架可以分析連線輸入的屬性並自動決定哪種上述連線演算法最適合當前任務。Hive、Spark 和 Flink 都有基於代價的查詢最佳化器可以做到這一點甚至可以改變連線順序最小化中間狀態的數量【66,77,78,79】。
連線演算法的選擇可以對批處理作業的效能產生巨大影響,而無需理解和記住本章中討論的各種連線演算法。如果連線是以 **宣告式declarative** 的方式指定的,那這就這是可行的:應用只是簡單地說明哪些連線是必需的,查詢最佳化器決定如何最好地執行連線。我們以前在 “[資料查詢語言](ch2.md#資料查詢語言)” 中見過這個想法。 連線演算法的選擇可以對批處理作業的效能產生巨大影響,而無需理解和記住本章中討論的各種連線演算法。如果連線是以 **宣告式declarative** 的方式指定的,那這就這是可行的:應用只是簡單地說明哪些連線是必需的,查詢最佳化器決定如何最好地執行連線。我們以前在 “[資料查詢語言](/tw/ch2#資料查詢語言)” 中見過這個想法。
但 MapReduce 及其資料流後繼者在其他方面,與 SQL 的完全宣告式查詢模型有很大區別。MapReduce 是圍繞著回撥函式的概念建立的對於每條記錄或者一組記錄呼叫一個使用者定義的函式Mapper 或 Reducer並且該函式可以自由地呼叫任意程式碼來決定輸出什麼。這種方法的優點是可以基於大量已有庫的生態系統創作解析、自然語言分析、影像分析以及執行數值或統計算法等。 但 MapReduce 及其資料流後繼者在其他方面,與 SQL 的完全宣告式查詢模型有很大區別。MapReduce 是圍繞著回撥函式的概念建立的對於每條記錄或者一組記錄呼叫一個使用者定義的函式Mapper 或 Reducer並且該函式可以自由地呼叫任意程式碼來決定輸出什麼。這種方法的優點是可以基於大量已有庫的生態系統創作解析、自然語言分析、影像分析以及執行數值或統計算法等。
自由執行任意程式碼,長期以來都是傳統 MapReduce 批處理系統與 MPP 資料庫的區別所在(請參閱 “[Hadoop 與分散式資料庫的對比](#Hadoop與分散式資料庫的對比)” 一節)。雖然資料庫具有編寫使用者定義函式的功能,但是它們通常使用起來很麻煩,而且與大多數程式語言中廣泛使用的程式包管理器和依賴管理系統相容不佳(例如 Java 的 Maven、Javascript 的 npm 以及 Ruby 的 gems 自由執行任意程式碼,長期以來都是傳統 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 資料庫了(並且能實現可與之媲美的效能)。同時,透過擁有執行任意程式碼和以任意格式讀取資料的可擴充套件性,它們保持了靈活性的優勢。 透過在高階 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. 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. 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. 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. 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 > —— 約翰・加爾Systemantics1975
---------------
[TOC] 在 [第十章](/tw/ch10) 中,我們討論了批處理技術,它讀取一組檔案作為輸入,並生成一組新的檔案作為輸出。輸出是 **衍生資料derived data** 的一種形式;也就是說,如果需要,可以透過再次執行批處理過程來重新建立資料集。我們看到了如何使用這個簡單而強大的想法來建立搜尋索引、推薦系統、做分析等等。
在 [第十章](ch10.md) 中,我們討論了批處理技術,它讀取一組檔案作為輸入,並生成一組新的檔案作為輸出。輸出是 **衍生資料derived data** 的一種形式;也就是說,如果需要,可以透過再次執行批處理過程來重新建立資料集。我們看到了如何使用這個簡單而強大的想法來建立搜尋索引、推薦系統、做分析等等。 然而,在 [第十章](/tw/ch10) 中仍然有一個很大的假設即輸入是有界的即已知和有限的大小所以批處理知道它何時完成輸入的讀取。例如MapReduce 核心的排序操作必須讀取其全部輸入,然後才能開始生成輸出:可能發生這種情況:最後一條輸入記錄具有最小的鍵,因此需要第一個被輸出,所以提早開始輸出是不可行的。
然而,在 [第十章](ch10.md) 中仍然有一個很大的假設即輸入是有界的即已知和有限的大小所以批處理知道它何時完成輸入的讀取。例如MapReduce 核心的排序操作必須讀取其全部輸入,然後才能開始生成輸出:可能發生這種情況:最後一條輸入記錄具有最小的鍵,因此需要第一個被輸出,所以提早開始輸出是不可行的。
實際上,很多資料是 **無界限** 的,因為它隨著時間的推移而逐漸到達:你的使用者在昨天和今天產生了資料,明天他們將繼續產生更多的資料。除非你停業,否則這個過程永遠都不會結束,所以資料集從來就不會以任何有意義的方式 “完成”【1】。因此批處理程式必須將資料人為地分成固定時間段的資料塊例如在每天結束時處理一天的資料或者在每小時結束時處理一小時的資料。 實際上,很多資料是 **無界限** 的,因為它隨著時間的推移而逐漸到達:你的使用者在昨天和今天產生了資料,明天他們將繼續產生更多的資料。除非你停業,否則這個過程永遠都不會結束,所以資料集從來就不會以任何有意義的方式 “完成”【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** 在批處理中,檔案被寫入一次,然後可能被多個作業讀取。類似地,在流處理術語中,一個事件由 **生產者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 將恰好一個傳送者與恰好一個接收者連線,而一個訊息傳遞系統允許多個生產者節點將訊息傳送到同一個主題,並允許多個消費者節點接收主題中的訊息。 像生產者和消費者之間的 Unix 管道或 TCP 連線這樣的直接通道是實現訊息傳遞系統的簡單方法。但是大多數訊息傳遞系統都在這一基本模型上進行了擴充套件。特別的是Unix 管道和 TCP 將恰好一個傳送者與恰好一個接收者連線,而一個訊息傳遞系統允許多個生產者節點將訊息傳送到同一個主題,並允許多個消費者節點接收主題中的訊息。
在這個 **釋出 / 訂閱** 模式中,不同的系統採取各種各樣的方法,並沒有針對所有目的的通用答案。為了區分這些系統,問一下這兩個問題會特別有幫助: 在這個 **釋出 / 訂閱** 模式中,不同的系統採取各種各樣的方法,並沒有針對所有目的的通用答案。為了區分這些系統,問一下這兩個問題會特別有幫助:
1. **如果生產者傳送訊息的速度比消費者能夠處理的速度快會發生什麼?** 一般來說,有三種選擇:系統可以丟掉訊息,將訊息放入緩衝佇列,或使用 **背壓**backpressure也稱為 **流量控制**,即 flow control阻塞生產者以免其傳送更多的訊息。例如 Unix 管道和 TCP 就使用了背壓:它們有一個固定大小的小緩衝區,如果填滿,傳送者會被阻塞,直到接收者從緩衝區中取出資料(請參閱 “[網路擁塞和排隊](ch8.md#網路擁塞和排隊)”)。 1. **如果生產者傳送訊息的速度比消費者能夠處理的速度快會發生什麼?** 一般來說,有三種選擇:系統可以丟掉訊息,將訊息放入緩衝佇列,或使用 **背壓**backpressure也稱為 **流量控制**,即 flow control阻塞生產者以免其傳送更多的訊息。例如 Unix 管道和 TCP 就使用了背壓:它們有一個固定大小的小緩衝區,如果填滿,傳送者會被阻塞,直到接收者從緩衝區中取出資料(請參閱 “[網路擁塞和排隊](/tw/ch8#網路擁塞和排隊)”)。
如果訊息被快取在佇列中那麼理解佇列增長會發生什麼是很重要的。當佇列裝不進記憶體時系統會崩潰嗎還是將訊息寫入磁碟如果是這樣磁碟訪問又會如何影響訊息傳遞系統的效能【6】 如果訊息被快取在佇列中那麼理解佇列增長會發生什麼是很重要的。當佇列裝不進記憶體時系統會崩潰嗎還是將訊息寫入磁碟如果是這樣磁碟訪問又會如何影響訊息傳遞系統的效能【6】
2. **如果節點崩潰或暫時離線,會發生什麼情況? —— 是否會有訊息丟失?** 與資料庫一樣,永續性可能需要寫入磁碟和 / 或複製的某種組合(請參閱 “[複製與永續性](ch7.md#複製與永續性)”),這是有代價的。如果你能接受有時訊息會丟失,則可能在同一硬體上獲得更高的吞吐量和更低的延遲。 2. **如果節點崩潰或暫時離線,會發生什麼情況? —— 是否會有訊息丟失?** 與資料庫一樣,永續性可能需要寫入磁碟和 / 或複製的某種組合(請參閱 “[複製與永續性](/tw/ch7#複製與永續性)”),這是有代價的。如果你能接受有時訊息會丟失,則可能在同一硬體上獲得更高的吞吐量和更低的延遲。
是否可以接受訊息丟失取決於應用。例如對於週期傳輸的感測器讀數和指標偶爾丟失的資料點可能並不重要因為更新的值會在短時間內發出。但要注意如果大量的訊息被丟棄可能無法立刻意識到指標已經不正確了【7】。如果你正在對事件計數那麼它們能夠可靠送達是更重要的因為每個丟失的訊息都意味著使計數器的錯誤擴大。 是否可以接受訊息丟失取決於應用。例如對於週期傳輸的感測器讀數和指標偶爾丟失的資料點可能並不重要因為更新的值會在短時間內發出。但要注意如果大量的訊息被丟棄可能無法立刻意識到指標已經不正確了【7】。如果你正在對事件計數那麼它們能夠可靠送達是更重要的因為每個丟失的訊息都意味著使計數器的錯誤擴大。
我們在 [第十章](ch10.md) 中探討的批處理系統的一個很好的特性是,它們提供了強大的可靠性保證:失敗的任務會自動重試,失敗任務的部分輸出會自動丟棄。這意味著輸出與沒有發生故障一樣,這有助於簡化程式設計模型。在本章的後面,我們將研究如何在流處理的上下文中提供類似的保證。 我們在 [第十章](/tw/ch10) 中探討的批處理系統的一個很好的特性是,它們提供了強大的可靠性保證:失敗的任務會自動重試,失敗任務的部分輸出會自動丟棄。這意味著輸出與沒有發生故障一樣,這有助於簡化程式設計模型。在本章的後面,我們將研究如何在流處理的上下文中提供類似的保證。
#### 直接從生產者傳遞給消費者 #### 直接從生產者傳遞給消費者
@ -66,8 +69,8 @@
* UDP 組播廣泛應用於金融行業例如股票市場其中低時延非常重要【8】。雖然 UDP 本身是不可靠的,但應用層的協議可以恢復丟失的資料包(生產者必須記住它傳送的資料包,以便能按需重新發送資料包)。 * UDP 組播廣泛應用於金融行業例如股票市場其中低時延非常重要【8】。雖然 UDP 本身是不可靠的,但應用層的協議可以恢復丟失的資料包(生產者必須記住它傳送的資料包,以便能按需重新發送資料包)。
* 無代理的訊息庫,如 ZeroMQ 【9】和 nanomsg 採取類似的方法,透過 TCP 或 IP 多播實現釋出 / 訂閱訊息傳遞。 * 無代理的訊息庫,如 ZeroMQ 【9】和 nanomsg 採取類似的方法,透過 TCP 或 IP 多播實現釋出 / 訂閱訊息傳遞。
* StatsD 【10】和 Brubeck 【7】使用不可靠的 UDP 訊息傳遞來收集網路中所有機器的指標並對其進行監控。(在 StatsD 協議中,只有接收到所有訊息,才認為計數器指標是正確的;使用 UDP 將使得指標處在一種最佳近似狀態【11】。另請參閱 “[TCP 與 UDP](ch8.md#TCP與UDP)” * StatsD 【10】和 Brubeck 【7】使用不可靠的 UDP 訊息傳遞來收集網路中所有機器的指標並對其進行監控。(在 StatsD 協議中,只有接收到所有訊息,才認為計數器指標是正確的;使用 UDP 將使得指標處在一種最佳近似狀態【11】。另請參閱 “[TCP 與 UDP](/tw/ch8#TCP與UDP)”
* 如果消費者在網路上公開了服務,生產者可以直接傳送 HTTP 或 RPC 請求(請參閱 “[服務中的資料流REST 與 RPC](ch4.md#服務中的資料流REST與RPC)”)將訊息推送給使用者。這就是 webhooks 背後的想法【12】一種服務的回撥 URL 被註冊到另一個服務中,並且每當事件發生時都會向該 URL 發出請求。 * 如果消費者在網路上公開了服務,生產者可以直接傳送 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】。 * 由於它們很快就能刪除訊息,大多數訊息代理都認為它們的工作集相當小 —— 即佇列很短。如果代理需要緩衝很多訊息比如因為消費者速度較慢如果記憶體裝不下訊息可能會溢位到磁碟每個訊息需要更長的處理時間整體吞吐量可能會惡化【6】。
@ -94,7 +97,7 @@
#### 多個消費者 #### 多個消費者
當多個消費者從同一主題中讀取訊息時,有兩種主要的訊息傳遞模式,如 [圖 11-1](../img/fig11-1.png) 所示: 當多個消費者從同一主題中讀取訊息時,有兩種主要的訊息傳遞模式,如 [圖 11-1](/img/fig11-1.png) 所示:
* 負載均衡load balancing * 負載均衡load balancing
@ -104,7 +107,7 @@
每條訊息都被傳遞給 **所有** 消費者。扇出允許幾個獨立的消費者各自 “收聽” 相同的訊息廣播,而不會相互影響 —— 這個流處理中的概念對應批處理中多個不同批處理作業讀取同一份輸入檔案 JMS 中的主題訂閱與 AMQP 中的交叉繫結提供了這一功能)。 每條訊息都被傳遞給 **所有** 消費者。扇出允許幾個獨立的消費者各自 “收聽” 相同的訊息廣播,而不會相互影響 —— 這個流處理中的概念對應批處理中多個不同批處理作業讀取同一份輸入檔案 JMS 中的主題訂閱與 AMQP 中的交叉繫結提供了這一功能)。
![](../img/fig11-1.png) ![](/img/fig11-1.png)
**圖 11-1 a負載平衡在消費者間共享消費主題b扇出將每條訊息傳遞給多個消費者。** **圖 11-1 a負載平衡在消費者間共享消費主題b扇出將每條訊息傳遞給多個消費者。**
@ -114,11 +117,11 @@
消費者隨時可能會崩潰,所以有一種可能的情況是:代理向消費者遞送訊息,但消費者沒有處理,或者在消費者崩潰之前只進行了部分處理。為了確保訊息不會丟失,訊息代理使用 **確認acknowledgments**:客戶端必須顯式告知代理訊息處理完畢的時間,以便代理能將訊息從佇列中移除。 消費者隨時可能會崩潰,所以有一種可能的情況是:代理向消費者遞送訊息,但消費者沒有處理,或者在消費者崩潰之前只進行了部分處理。為了確保訊息不會丟失,訊息代理使用 **確認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** **圖 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` 能監視檔案被追加寫入的資料,基本上就是這樣工作的。 同樣的結構可以用於實現訊息代理生產者透過將訊息追加到日誌末尾來發送訊息而消費者透過依次讀取日誌來接收訊息。如果消費者讀到日誌末尾則會等待新訊息追加的通知。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 生產者透過將訊息追加寫入主題分割槽檔案來發送訊息,消費者依次讀取這些檔案** **圖 11-3 生產者透過將訊息追加寫入主題分割槽檔案來發送訊息,消費者依次讀取這些檔案**
@ -159,7 +162,7 @@ Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 Distri
然後每個客戶端將消費被指派分割槽中的 **所有** 訊息。通常情況下,當一個使用者被指派了一個日誌分割槽時,它會以簡單的單執行緒方式順序地讀取分割槽中的訊息。這種粗粒度的負載均衡方法有一些缺點: 然後每個客戶端將消費被指派分割槽中的 **所有** 訊息。通常情況下,當一個使用者被指派了一個日誌分割槽時,它會以簡單的單執行緒方式順序地讀取分割槽中的訊息。這種粗粒度的負載均衡方法有一些缺點:
* 共享消費主題工作的節點數,最多為該主題中的日誌分割槽數,因為同一個分割槽內的所有訊息被遞送到同一個節點 [^i]。 * 共享消費主題工作的節點數,最多為該主題中的日誌分割槽數,因為同一個分割槽內的所有訊息被遞送到同一個節點 [^i]。
* 如果某條訊息處理緩慢,則它會阻塞該分割槽中後續訊息的處理(一種行首阻塞的形式;請參閱 “[描述效能](ch1.md#描述效能)”)。 * 如果某條訊息處理緩慢,則它會阻塞該分割槽中後續訊息的處理(一種行首阻塞的形式;請參閱 “[描述效能](/tw/ch1#描述效能)”)。
因此在訊息處理代價高昂希望逐條並行處理以及訊息的順序並沒有那麼重要的情況下JMS/AMQP 風格的訊息代理是可取的。另一方面,在訊息吞吐量很高,處理迅速,順序很重要的情況下,基於日誌的方法表現得非常好。 因此在訊息處理代價高昂希望逐條並行處理以及訊息的順序並沒有那麼重要的情況下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 資料庫來為使用者請求提供服務,使用快取來加速常見請求,使用全文索引來處理搜尋查詢,使用資料倉庫用於分析。每一種技術都有自己的資料副本,並根據自己的目的進行儲存方式的最佳化。 正如我們在本書中所看到的,沒有一個系統能夠滿足所有的資料儲存、查詢和處理需求。在實踐中,大多數重要應用都需要組合使用幾種不同的技術來滿足所有的需求:例如,使用 OLTP 資料庫來為使用者請求提供服務,使用快取來加速常見請求,使用全文索引來處理搜尋查詢,使用資料倉庫用於分析。每一種技術都有自己的資料副本,並根據自己的目的進行儲存方式的最佳化。
由於相同或相關的資料出現在了不同的地方,因此相互間需要保持同步:如果某個專案在資料庫中被更新,它也應當在快取、搜尋索引和資料倉庫中被更新。對於資料倉庫,這種同步通常由 ETL 程序執行(請參閱 “[資料倉庫](ch3.md#資料倉庫)”),通常是先取得資料庫的完整副本,然後執行轉換,並批次載入到資料倉庫中 —— 換句話說,批處理。我們在 “[批處理工作流的輸出](ch10.md#批處理工作流的輸出)” 中同樣看到了如何使用批處理建立搜尋索引、推薦系統和其他衍生資料系統。 由於相同或相關的資料出現在了不同的地方,因此相互間需要保持同步:如果某個專案在資料庫中被更新,它也應當在快取、搜尋索引和資料倉庫中被更新。對於資料倉庫,這種同步通常由 ETL 程序執行(請參閱 “[資料倉庫](/tw/ch3#資料倉庫)”),通常是先取得資料庫的完整副本,然後執行轉換,並批次載入到資料倉庫中 —— 換句話說,批處理。我們在 “[批處理工作流的輸出](/tw/ch10#批處理工作流的輸出)” 中同樣看到了如何使用批處理建立搜尋索引、推薦系統和其他衍生資料系統。
如果週期性的完整資料庫轉儲過於緩慢,有時會使用的替代方法是 **雙寫dual write**,其中應用程式碼在資料變更時明確寫入每個系統:例如,首先寫入資料庫,然後更新搜尋索引,然後使快取項失效(甚至同時執行這些寫入)。 如果週期性的完整資料庫轉儲過於緩慢,有時會使用的替代方法是 **雙寫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而在搜尋索引處寫入以相反的順序到達** **圖 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 是非常有意思的,尤其是當變更能在被寫入後立刻用於流時。 最近,人們對 **變更資料捕獲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 將資料按順序寫入一個數據庫,然後按照相同的順序將這些更改應用到其他系統** **圖 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】。 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 工具集成了這種快照功能,而其他工具則把它留給你手動執行。 資料庫的快照必須與變更日誌中的已知位置或偏移量相對應,以便在處理完快照後知道從哪裡開始應用變更。一些 CDC 工具集成了這種快照功能,而其他工具則把它留給你手動執行。
@ -274,7 +277,7 @@ LinkedIn 的 Databus【25】Facebook 的 Wormhole【26】和 Yahoo! 的 Sherp
如果你只能保留有限的歷史日誌,則每次要新增新的衍生資料系統時,都需要做一次快照。但 **日誌壓縮log compaction** 提供了一個很好的備選方案。 如果你只能保留有限的歷史日誌,則每次要新增新的衍生資料系統時,都需要做一次快照。但 **日誌壓縮log compaction** 提供了一個很好的備選方案。
我們之前在 “[雜湊索引](ch3.md#雜湊索引)” 中關於日誌結構儲存引擎的上下文中討論了日誌壓縮(請參閱 [圖 3-2](../img/fig3-2.png) 的示例)。原理很簡單:儲存引擎定期在日誌中查詢具有相同鍵的記錄,丟掉所有重複的內容,並只保留每個鍵的最新更新。這個壓縮與合併過程在後臺執行。 我們之前在 “[雜湊索引](/tw/ch3#雜湊索引)” 中關於日誌結構儲存引擎的上下文中討論了日誌壓縮(請參閱 [圖 3-2](/img/fig3-2.png) 的示例)。原理很簡單:儲存引擎定期在日誌中查詢具有相同鍵的記錄,丟掉所有重複的內容,並只保留每個鍵的最新更新。這個壓縮與合併過程在後臺執行。
在日誌結構儲存引擎中,具有特殊值 NULL**墓碑**,即 tombstone的更新表示該鍵被刪除並會在日誌壓縮過程中被移除。但只要鍵不被覆蓋或刪除它就會永遠留在日誌中。這種壓縮日誌所需的磁碟空間僅取決於資料庫的當前內容而不取決於資料庫中曾經發生的寫入次數。如果相同的鍵經常被覆蓋寫入則先前的值將最終將被垃圾回收只有最新的值會保留下來。 在日誌結構儲存引擎中,具有特殊值 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請參閱 “[不可變事件的優點](#不可變事件的優點)”)。 事件溯源是一種強大的資料建模技術:從應用的角度來看,將使用者的行為記錄為不可變的事件更有意義,而不是在可變資料庫中記錄這些行為的影響。事件溯源使得應用隨時間演化更為容易,透過更容易理解事情發生的原因來幫助除錯的進行,並有利於防止應用 Bug請參閱 “[不可變事件的優點](#不可變事件的優點)”)。
例如,儲存 “學生取消選課” 事件以中性的方式清楚地表達了單個行為的意圖,而其副作用 “從登記表中刪除了一個條目,而一條取消原因的記錄被新增到學生反饋表” 則嵌入了很多有關稍後對資料的使用方式的假設。如果引入一個新的應用功能,例如 “將位置留給等待列表中的下一個人” —— 事件溯源方法允許將新的副作用輕鬆地從現有事件中脫開。 例如,儲存 “學生取消選課” 事件以中性的方式清楚地表達了單個行為的意圖,而其副作用 “從登記表中刪除了一個條目,而一條取消原因的記錄被新增到學生反饋表” 則嵌入了很多有關稍後對資料的使用方式的假設。如果引入一個新的應用功能,例如 “將位置留給等待列表中的下一個人” —— 事件溯源方法允許將新的副作用輕鬆地從現有事件中脫開。
事件溯源類似於 **編年史chronicle** 資料模型【45】事件日誌與星型模式中的事實表之間也存在相似之處請參閱 “[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”) 。 事件溯源類似於 **編年史chronicle** 資料模型【45】事件日誌與星型模式中的事實表之間也存在相似之處請參閱 “[星型和雪花型:分析的模式](/tw/ch3#星型和雪花型:分析的模式)”) 。
諸如 Event Store【46】這樣的專業資料庫已經被開發出來供使用事件溯源的應用使用但總的來說這種方法獨立於任何特定的工具。傳統的資料庫或基於日誌的訊息代理也可以用來構建這種風格的應用。 諸如 Event Store【46】這樣的專業資料庫已經被開發出來供使用事件溯源的應用使用但總的來說這種方法獨立於任何特定的工具。傳統的資料庫或基於日誌的訊息代理也可以用來構建這種風格的應用。
@ -326,17 +329,17 @@ Kafka Connect【41】致力於將廣泛的資料庫系統的變更資料捕獲
事件溯源的哲學是仔細區分 **事件event****命令command**【48】。當來自使用者的請求剛到達時它一開始是一個命令在這個時間點上它仍然可能失敗比如因為違反了一些完整性條件。應用必須首先驗證它是否可以執行該命令。如果驗證成功並且命令被接受則它變為一個持久化且不可變的事件。 事件溯源的哲學是仔細區分 **事件event****命令command**【48】。當來自使用者的請求剛到達時它一開始是一個命令在這個時間點上它仍然可能失敗比如因為違反了一些完整性條件。應用必須首先驗證它是否可以執行該命令。如果驗證成功並且命令被接受則它變為一個持久化且不可變的事件。
例如,如果使用者試圖註冊特定使用者名稱,或預定飛機或劇院的座位,則應用需要檢查使用者名稱或座位是否已被佔用。(先前在 “[容錯共識](ch9.md#容錯共識)” 中討論過這個例子)當檢查成功時,應用可以生成一個事件,指示特定的使用者名稱是由特定的使用者 ID 註冊的,或者座位已經預留給特定的顧客。 例如,如果使用者試圖註冊特定使用者名稱,或預定飛機或劇院的座位,則應用需要檢查使用者名稱或座位是否已被佔用。(先前在 “[容錯共識](/tw/ch9#容錯共識)” 中討論過這個例子)當檢查成功時,應用可以生成一個事件,指示特定的使用者名稱是由特定的使用者 ID 註冊的,或者座位已經預留給特定的顧客。
在事件生成的時刻,它就成為了 **事實fact**。即使客戶稍後決定更改或取消預訂,他們之前曾預定了某個特定座位的事實仍然成立,而更改或取消是之後新增的單獨的事件。 在事件生成的時刻,它就成為了 **事實fact**。即使客戶稍後決定更改或取消預訂,他們之前曾預定了某個特定座位的事實仍然成立,而更改或取消是之後新增的單獨的事件。
事件流的消費者不允許拒絕事件:當消費者看到事件時,它已經成為日誌中不可變的一部分,並且可能已經被其他消費者看到了。因此任何對命令的驗證,都需要在它成為事件之前同步完成。例如,透過使用一個可以原子性地自動驗證命令併發布事件的可序列事務。 事件流的消費者不允許拒絕事件:當消費者看到事件時,它已經成為日誌中不可變的一部分,並且可能已經被其他消費者看到了。因此任何對命令的驗證,都需要在它成為事件之前同步完成。例如,透過使用一個可以原子性地自動驗證命令併發布事件的可序列事務。
或者,預訂座位的使用者請求可以拆分為兩個事件:第一個是暫時預約,第二個是驗證預約後的獨立的確認事件(如 “[使用全序廣播實現線性一致的儲存](ch9.md#使用全序廣播實現線性一致的儲存)” 中所述) 。這種分割方式允許驗證發生在一個非同步的過程中。 或者,預訂座位的使用者請求可以拆分為兩個事件:第一個是暫時預約,第二個是驗證預約後的獨立的確認事件(如 “[使用全序廣播實現線性一致的儲存](/tw/ch9#使用全序廣播實現線性一致的儲存)” 中所述) 。這種分割方式允許驗證發生在一個非同步的過程中。
### 狀態、流和不變性 ### 狀態、流和不變性
我們在 [第十章](ch10.md) 中看到,批處理因其輸入檔案不變性而受益良多,你可以在現有輸入檔案上執行實驗性處理作業,而不用擔心損壞它們。這種不變性原則也是使得事件溯源與變更資料捕獲如此強大的原因。 我們在 [第十章](/tw/ch10) 中看到,批處理因其輸入檔案不變性而受益良多,你可以在現有輸入檔案上執行實驗性處理作業,而不用擔心損壞它們。這種不變性原則也是使得事件溯源與變更資料捕獲如此強大的原因。
我們通常將資料庫視為應用程式當前狀態的儲存 —— 這種表示針對讀取進行了最佳化,而且通常對於服務查詢而言是最為方便的表示。狀態的本質是,它會變化,所以資料庫才會支援資料的增刪改。這又該如何匹配不變性呢? 我們通常將資料庫視為應用程式當前狀態的儲存 —— 這種表示針對讀取進行了最佳化,而且通常對於服務查詢而言是最為方便的表示。狀態的本質是,它會變化,所以資料庫才會支援資料的增刪改。這又該如何匹配不變性呢?
@ -344,13 +347,13 @@ Kafka Connect【41】致力於將廣泛的資料庫系統的變更資料捕獲
無論狀態如何變化,總是有一系列事件導致了這些變化。即使事情已經執行與回滾,這些事件出現是始終成立的。關鍵的想法是:可變的狀態與不可變事件的僅追加日誌相互之間並不矛盾:它們是一體兩面,互為陰陽的。所有變化的日誌 —— **變化日誌changelog**,表示了隨時間演變的狀態。 無論狀態如何變化,總是有一系列事件導致了這些變化。即使事情已經執行與回滾,這些事件出現是始終成立的。關鍵的想法是:可變的狀態與不可變事件的僅追加日誌相互之間並不矛盾:它們是一體兩面,互為陰陽的。所有變化的日誌 —— **變化日誌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} \\ state(now) = \int_{t=0}^{now}{stream(t) \ dt} \\
stream(t) = \frac{d\ state(t)}{dt} stream(t) = \frac{d\ state(t)}{dt}
$$ $$
![](../img/fig11-6.png) ![](/img/fig11-6.png)
**圖 11-6 應用當前狀態與事件流之間的關係** **圖 11-6 應用當前狀態與事件流之間的關係**
@ -366,35 +369,35 @@ $$
如果發生錯誤,會計師不會刪除或更改分類帳中的錯誤交易 —— 而是新增另一筆交易以補償錯誤例如退還一筆不正確的費用。不正確的交易將永遠保留在分類帳中對於審計而言可能非常重要。如果從不正確的分類賬衍生出的錯誤數字已經公佈那麼下一個會計週期的數字就會包括一個更正。這個過程在會計事務中是很常見的【54】。 如果發生錯誤,會計師不會刪除或更改分類帳中的錯誤交易 —— 而是新增另一筆交易以補償錯誤例如退還一筆不正確的費用。不正確的交易將永遠保留在分類帳中對於審計而言可能非常重要。如果從不正確的分類賬衍生出的錯誤數字已經公佈那麼下一個會計週期的數字就會包括一個更正。這個過程在會計事務中是很常見的【54】。
儘管這種可審計性只在金融系統中尤其重要,但對於不受這種嚴格監管的許多其他系統,也是很有幫助的。如 “[批處理輸出的哲學](ch10.md#批處理輸出的哲學)” 中所討論的,如果你意外地部署了將錯誤資料寫入資料庫的錯誤程式碼,當代碼會破壞性地覆寫資料時,恢復要困難得多。使用不可變事件的僅追加日誌,診斷問題與故障恢復就要容易的多。 儘管這種可審計性只在金融系統中尤其重要,但對於不受這種嚴格監管的許多其他系統,也是很有幫助的。如 “[批處理輸出的哲學](/tw/ch10#批處理輸出的哲學)” 中所討論的,如果你意外地部署了將錯誤資料寫入資料庫的錯誤程式碼,當代碼會破壞性地覆寫資料時,恢復要困難得多。使用不可變事件的僅追加日誌,診斷問題與故障恢復就要容易的多。
不可變的事件也包含了比當前狀態更多的資訊。例如在購物網站上顧客可以將物品新增到他們的購物車然後再將其移除。雖然從履行訂單的角度第二個事件取消了第一個事件但對分析目的而言知道客戶考慮過某個特定項而之後又反悔可能是很有用的。也許他們會選擇在未來購買或者他們已經找到了替代品。這個資訊被記錄在事件日誌中但對於移出購物車就刪除記錄的資料庫而言這個資訊在移出購物車時可能就丟失了【42】。 不可變的事件也包含了比當前狀態更多的資訊。例如在購物網站上顧客可以將物品新增到他們的購物車然後再將其移除。雖然從履行訂單的角度第二個事件取消了第一個事件但對分析目的而言知道客戶考慮過某個特定項而之後又反悔可能是很有用的。也許他們會選擇在未來購買或者他們已經找到了替代品。這個資訊被記錄在事件日誌中但對於移出購物車就刪除記錄的資料庫而言這個資訊在移出購物車時可能就丟失了【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】。 新增從事件日誌到資料庫的顯式轉換能夠使應用更容易地隨時間演進如果你想要引入一個新功能以新的方式表示現有資料則可以使用事件日誌來構建一個單獨的、針對新功能的讀取最佳化檢視無需修改現有系統而與之共存。並行執行新舊系統通常比在現有系統中執行複雜的模式遷移更容易。一旦不再需要舊的系統你可以簡單地關閉它並回收其資源【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】。 永遠保持所有變更的不變歷史,在多大程度上是可行的?答案取決於資料集的流失率。一些工作負載主要是新增資料,很少更新或刪除;它們很容易保持不變。其他工作負載在相對較小的資料集上有較高的更新 / 刪除率在這些情況下不可變的歷史可能增至難以接受的巨大碎片化可能成為一個問題壓縮與垃圾收集的表現對於運維的穩健性變得至關重要【60,61】。
@ -402,7 +405,7 @@ $$
在這種情況下,僅僅在日誌中新增另一個事件來指明先前的資料應該被視為刪除是不夠的 —— 你實際上是想改寫歷史並假裝資料從一開始就沒有寫入。例如Datomic 管這個特性叫 **切除excision** 【62】而 Fossil 版本控制系統有一個類似的概念叫 **避免shunning** 【63】。 在這種情況下,僅僅在日誌中新增另一個事件來指明先前的資料應該被視為刪除是不夠的 —— 你實際上是想改寫歷史並假裝資料從一開始就沒有寫入。例如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. 你能以某種方式將事件推送給使用者,例如傳送報警郵件或推送通知,或將事件流式傳輸到可即時顯示的儀表板上。在這種情況下,人是流的最終消費者。 2. 你能以某種方式將事件推送給使用者,例如傳送報警郵件或推送通知,或將事件流式傳輸到可即時顯示的儀表板上。在這種情況下,人是流的最終消費者。
3. 你可以處理一個或多個輸入流,併產生一個或多個輸出流。流可能會經過由幾個這樣的處理階段組成的流水線,最後再輸出(選項 1 或 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**,我們將在 “[時間推理](#時間推理)” 中更詳細地討論視窗。 這些統計值通常是在固定時間區間內進行計算的,例如,你可能想知道在過去 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。 許多開源分散式流處理框架的設計都是針對分析設計的:例如 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 #### 訊息傳遞和RPC
在 “[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)” 中我們討論過,訊息傳遞系統可以作為 RPC 的替代方案,即作為一種服務間通訊的機制,比如在 Actor 模型中所使用的那樣。儘管這些系統也是基於訊息和事件,但我們通常不會將其視作流處理元件: 在 “[訊息傳遞中的資料流](/tw/ch4#訊息傳遞中的資料流)” 中我們討論過,訊息傳遞系統可以作為 RPC 的替代方案,即作為一種服務間通訊的機制,比如在 Actor 模型中所使用的那樣。儘管這些系統也是基於訊息和事件,但我們通常不會將其視作流處理元件:
* Actor 框架主要是管理模組通訊的併發和分散式執行的一種機制,而流處理主要是一種資料管理技術。 * Actor 框架主要是管理模組通訊的併發和分散式執行的一種機制,而流處理主要是一種資料管理技術。
* Actor 之間的交流往往是短暫的、一對一的;而事件日誌則是持久的、多訂閱者的。 * Actor 之間的交流往往是短暫的、一對一的;而事件日誌則是持久的、多訂閱者的。
* Actor 可以以任意方式進行通訊(包括迴圈的請求 / 響應模式),但流處理通常配置在無環流水線中,其中每個流都是一個特定作業的輸出,由良好定義的輸入流中派生而來。 * Actor 可以以任意方式進行通訊(包括迴圈的請求 / 響應模式),但流處理通常配置在無環流水線中,其中每個流都是一個特定作業的輸出,由良好定義的輸入流中派生而來。
也就是說RPC 類系統與流處理之間有一些交叉領域。例如Apache Storm 有一個稱為 **分散式 RPC** 的功能它允許將使用者查詢分散到一系列也處理事件流的節點上然後這些查詢與來自輸入流的事件交織而結果可以被彙總併發回給使用者【78】另請參閱 “[多分割槽資料處理](ch12.md#多分割槽資料處理)”)。 也就是說RPC 類系統與流處理之間有一些交叉領域。例如Apache Storm 有一個稱為 **分散式 RPC** 的功能它允許將使用者查詢分散到一系列也處理事件流的節點上然後這些查詢與來自輸入流的事件交織而結果可以被彙總併發回給使用者【78】另請參閱 “[多分割槽資料處理](/tw/ch12#多分割槽資料處理)”)。
也可以使用 Actor 框架來處理流。但是,很多這樣的框架在崩潰時不能保證訊息的傳遞,除非你實現了額外的重試邏輯,否則這種處理不是容錯的。 也可以使用 Actor 框架來處理流。但是,很多這樣的框架在崩潰時不能保證訊息的傳遞,除非你實現了額外的重試邏輯,否則這種處理不是容錯的。
@ -490,13 +493,13 @@ CEP 的實現包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
在批處理中過程中,大量的歷史事件被快速地處理。如果需要按時間來分析,批處理器需要檢查每個事件中嵌入的時間戳。讀取執行批處理機器的系統時鐘沒有任何意義,因為處理執行的時間與事件實際發生的時間無關。 在批處理中過程中,大量的歷史事件被快速地處理。如果需要按時間來分析,批處理器需要檢查每個事件中嵌入的時間戳。讀取執行批處理機器的系統時鐘沒有任何意義,因為處理執行的時間與事件實際發生的時間無關。
批處理可以在幾分鐘內讀取一年的歷史事件;在大多數情況下,感興趣的時間線是歷史中的一年,而不是處理中的幾分鐘。而且使用事件中的時間戳,使得處理是 **確定性** 的:在相同的輸入上再次執行相同的處理過程會得到相同的結果(請參閱 “[容錯](ch10.md#容錯)”)。 批處理可以在幾分鐘內讀取一年的歷史事件;在大多數情況下,感興趣的時間線是歷史中的一年,而不是處理中的幾分鐘。而且使用事件中的時間戳,使得處理是 **確定性** 的:在相同的輸入上再次執行相同的處理過程會得到相同的結果(請參閱 “[容錯](/tw/ch10#容錯)”)。
另一方面,許多流處理框架使用處理機器上的本地系統時鐘(**處理時間**,即 processing time來確定 **視窗windowing**【79】。這種方法的優點是簡單如果事件建立與事件處理之間的延遲可以忽略不計那也是合理的。然而如果存在任何顯著的處理延遲 —— 即,事件處理顯著地晚於事件實際發生的時間,這種處理方式就失效了。 另一方面,許多流處理框架使用處理機器上的本地系統時鐘(**處理時間**,即 processing time來確定 **視窗windowing**【79】。這種方法的優點是簡單如果事件建立與事件處理之間的延遲可以忽略不計那也是合理的。然而如果存在任何顯著的處理延遲 —— 即,事件處理顯著地晚於事件實際發生的時間,這種處理方式就失效了。
#### 事件時間與處理時間 #### 事件時間與處理時間
很多原因都可能導致處理延遲:排隊,網路故障(請參閱 “[不可靠的網路](ch8.md#不可靠的網路)”),效能問題導致訊息代理 / 訊息處理器出現爭用,流消費者重啟,從故障中恢復時重新處理過去的事件(請參閱 “[重播舊訊息](#重播舊訊息)”),或者在修復程式碼 BUG 之後。 很多原因都可能導致處理延遲:排隊,網路故障(請參閱 “[不可靠的網路](/tw/ch8#不可靠的網路)”),效能問題導致訊息代理 / 訊息處理器出現爭用,流消費者重啟,從故障中恢復時重新處理過去的事件(請參閱 “[重播舊訊息](#重播舊訊息)”),或者在修復程式碼 BUG 之後。
而且,訊息延遲還可能導致無法預測訊息順序。例如,假設使用者首先發出一個 Web 請求(由 Web 伺服器 A 處理),然後發出第二個請求(由伺服器 B 處理。A 和 B 發出描述它們所處理請求的事件,但是 B 的事件在 A 的事件發生之前到達訊息代理。現在,流處理器將首先看到 B 事件,然後看到 A 事件,即使它們實際上是以相反的順序發生的。 而且,訊息延遲還可能導致無法預測訊息順序。例如,假設使用者首先發出一個 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 提出這個比喻。 [^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 按處理時間分窗,會因為處理速率的變動引入人為因素** **圖 11-7 按處理時間分窗,會因為處理速率的變動引入人為因素**
@ -527,7 +530,7 @@ CEP 的實現包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
當事件可能在系統內多個地方進行緩衝時,為事件分配時間戳更加困難了。例如,考慮一個移動應用向伺服器上報關於用量的事件。該應用可能會在裝置處於離線狀態時被使用,在這種情況下,它將在裝置本地緩衝事件,並在下一次網際網路連線可用時向伺服器上報這些事件(可能是幾小時甚至幾天)。對於這個流的任意消費者而言,它們就如延遲極大的滯留事件一樣。 當事件可能在系統內多個地方進行緩衝時,為事件分配時間戳更加困難了。例如,考慮一個移動應用向伺服器上報關於用量的事件。該應用可能會在裝置處於離線狀態時被使用,在這種情況下,它將在裝置本地緩衝事件,並在下一次網際網路連線可用時向伺服器上報這些事件(可能是幾小時甚至幾天)。對於這個流的任意消費者而言,它們就如延遲極大的滯留事件一樣。
在這種情況下,事件上的事件戳實際上應當是使用者交互發生的時間,取決於移動裝置的本地時鐘。然而使用者控制的裝置上的時鐘通常是不可信的,因為它可能會被無意或故意設定成錯誤的時間(請參閱 “[時鐘同步與準確性](ch8.md#時鐘同步與準確性)”)。伺服器收到事件的時間(取決於伺服器的時鐘)可能是更準確的,因為伺服器在你的控制之下,但在描述使用者互動方面意義不大。 在這種情況下,事件上的事件戳實際上應當是使用者交互發生的時間,取決於移動裝置的本地時鐘。然而使用者控制的裝置上的時鐘通常是不可信的,因為它可能會被無意或故意設定成錯誤的時間(請參閱 “[時鐘同步與準確性](/tw/ch8#時鐘同步與準確性)”)。伺服器收到事件的時間(取決於伺服器的時鐘)可能是更準確的,因為伺服器在你的控制之下,但在描述使用者互動方面意義不大。
要校正不正確的裝置時鐘一種方法是記錄三個時間戳【82】 要校正不正確的裝置時鐘一種方法是記錄三個時間戳【82】
@ -557,11 +560,11 @@ CEP 的實現包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB
* 會話視窗Session window * 會話視窗Session window
與其他視窗型別不同,會話視窗沒有固定的持續時間,而定義為:將同一使用者出現時間相近的所有事件分組在一起,而當用戶一段時間沒有活動時(例如,如果 30 分鐘內沒有事件)視窗結束。會話切分是網站分析的常見需求(請參閱 “[分組](ch10.md#分組)”)。 與其他視窗型別不同,會話視窗沒有固定的持續時間,而定義為:將同一使用者出現時間相近的所有事件分組在一起,而當用戶一段時間沒有活動時(例如,如果 30 分鐘內沒有事件)視窗結束。會話切分是網站分析的常見需求(請參閱 “[分組](/tw/ch10#分組)”)。
### 流連線 ### 流連線
在 [第十章](ch10.md) 中,我們討論了批處理作業如何透過鍵來連線資料集,以及這種連線是如何成為資料管道的重要組成部分的。由於流處理將資料管道泛化為對無限資料集進行增量處理,因此對流進行連線的需求也是完全相同的。 在 [第十章](/tw/ch10) 中,我們討論了批處理作業如何透過鍵來連線資料集,以及這種連線是如何成為資料管道的重要組成部分的。由於流處理將資料管道泛化為對無限資料集進行增量處理,因此對流進行連線的需求也是完全相同的。
然而,新事件隨時可能出現在一個流中,這使得流連線要比批處理連線更具挑戰性。為了更好地理解情況,讓我們先來區分三種不同型別的連線:**流 - 流** 連線,**流 - 表** 連線,與 **表 - 表** 連線【84】。我們將在下面的章節中透過例子來說明。 然而,新事件隨時可能出現在一個流中,這使得流連線要比批處理連線更具挑戰性。為了更好地理解情況,讓我們先來區分三種不同型別的連線:**流 - 流** 連線,**流 - 表** 連線,與 **表 - 表** 連線【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]。
[^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】。 特別是,批處理容錯方法可確保批處理作業的輸出與沒有出錯的情況相同,即使實際上某些任務失敗了。看起來好像每條輸入記錄都被處理了恰好一次 —— 沒有記錄被跳過,而且沒有記錄被處理兩次。儘管重啟任務意味著實際上可能會多次處理記錄,但輸出中的可見效果看上去就像只處理過一次。這個原則被稱為 **恰好一次語義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】。 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 風格的訊息代理 * AMQP/JMS 風格的訊息代理
代理將單條訊息分配給消費者,消費者在成功處理單條訊息後確認訊息。訊息被確認後從代理中刪除。這種方法適合作為一種非同步形式的 RPC另請參閱 “[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)”),例如在任務佇列中,訊息處理的確切順序並不重要,而且訊息在處理完之後,不需要回頭重新讀取舊訊息。 代理將單條訊息分配給消費者,消費者在成功處理單條訊息後確認訊息。訊息被確認後從代理中刪除。這種方法適合作為一種非同步形式的 RPC另請參閱 “[訊息傳遞中的資料流](/tw/ch4#訊息傳遞中的資料流)”),例如在任務佇列中,訊息處理的確切順序並不重要,而且訊息在處理完之後,不需要回頭重新讀取舊訊息。
* 基於日誌的訊息代理 * 基於日誌的訊息代理
代理將一個分割槽中的所有訊息分配給同一個消費者節點,並始終以相同的順序傳遞訊息。並行是透過分割槽實現的,消費者透過存檔最近處理訊息的偏移量來跟蹤工作進度。訊息代理將訊息保留在磁碟上,因此如有必要的話,可以回跳並重新讀取舊訊息。 代理將一個分割槽中的所有訊息分配給同一個消費者節點,並始終以相同的順序傳遞訊息。並行是透過分割槽實現的,消費者透過存檔最近處理訊息的偏移量來跟蹤工作進度。訊息代理將訊息保留在磁碟上,因此如有必要的話,可以回跳並重新讀取舊訊息。
基於日誌的方法與資料庫中的複製日誌(請參閱 [第五章](ch5.md))和日誌結構儲存引擎(請參閱 [第三章](ch3.md))有相似之處。我們看到,這種方法對於消費輸入流,併產生衍生狀態或衍生輸出資料流的系統而言特別適用。 基於日誌的方法與資料庫中的複製日誌(請參閱 [第五章](/tw/ch5))和日誌結構儲存引擎(請參閱 [第三章](/tw/ch3))有相似之處。我們看到,這種方法對於消費輸入流,併產生衍生狀態或衍生輸出資料流的系統而言特別適用。
就流的來源而言,我們討論了幾種可能性:使用者活動事件,定期讀數的感測器,和 Feed 資料(例如,金融中的市場資料)能夠自然地表示為流。我們發現將資料庫寫入視作流也是很有用的:我們可以捕獲變更日誌 —— 即對資料庫所做的所有變更的歷史記錄 —— 隱式地透過變更資料捕獲,或顯式地透過事件溯源。日誌壓縮允許流也能保有資料庫內容的完整副本。 就流的來源而言,我們討論了幾種可能性:使用者活動事件,定期讀數的感測器,和 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. 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. 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. 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 > —— 聖托馬斯・阿奎那《神學大全》1265-1274
---------------
[TOC]
到目前為止,本書主要描述的是 **現狀**。在這最後一章中,我們將放眼 **未來**,討論應該是怎麼樣的:我將提出一些想法與方法,我相信它們能從根本上改進我們設計與構建應用的方式。 到目前為止,本書主要描述的是 **現狀**。在這最後一章中,我們將放眼 **未來**,討論應該是怎麼樣的:我將提出一些想法與方法,我相信它們能從根本上改進我們設計與構建應用的方式。
對未來的看法與推測當然具有很大的主觀性。所以在撰寫本章時,當提及我個人的觀點時會使用第一人稱。你完全可以不同意這些觀點並提出自己的看法,但我希望本章中的概念,至少能成為富有成效的討論出發點,並澄清一些經常被混淆的概念。 對未來的看法與推測當然具有很大的主觀性。所以在撰寫本章時,當提及我個人的觀點時會使用第一人稱。你完全可以不同意這些觀點並提出自己的看法,但我希望本章中的概念,至少能成為富有成效的討論出發點,並澄清一些經常被混淆的概念。
[第一章](ch1.md) 概述了本書的目標:探索如何建立 **可靠**、**可伸縮** 和 **可維護** 的應用與系統。這一主題貫穿了所有的章節:例如,我們討論了許多有助於提高可靠性的容錯演算法,有助於提高可伸縮性的分割槽,以及有助於提高可維護性的演化與抽象機制。在本章中,我們將把所有這些想法結合在一起,並在它們的基礎上展望未來。我們的目標是,發現如何設計出比現有應用更好的應用 —— 健壯、正確、可演化、且最終對人類有益。 [第一章](/tw/ch1) 概述了本書的目標:探索如何建立 **可靠**、**可伸縮** 和 **可維護** 的應用與系統。這一主題貫穿了所有的章節:例如,我們討論了許多有助於提高可靠性的容錯演算法,有助於提高可伸縮性的分割槽,以及有助於提高可維護性的演化與抽象機制。在本章中,我們將把所有這些想法結合在一起,並在它們的基礎上展望未來。我們的目標是,發現如何設計出比現有應用更好的應用 —— 健壯、正確、可演化、且最終對人類有益。
## 資料整合 ## 資料整合
本書中反覆出現的主題是,對於任何給定的問題都會有好幾種解決方案,所有這些解決方案都有不同的優缺點與利弊權衡。例如在 [第三章](ch3.md) 討論儲存引擎時我們看到了日誌結構儲存、B 樹以及列式儲存。在 [第五章](ch5.md) 討論複製時,我們看到了單領導者、多領導者和無領導者的方法。 本書中反覆出現的主題是,對於任何給定的問題都會有好幾種解決方案,所有這些解決方案都有不同的優缺點與利弊權衡。例如在 [第三章](/tw/ch3) 討論儲存引擎時我們看到了日誌結構儲存、B 樹以及列式儲存。在 [第五章](/tw/ch5) 討論複製時,我們看到了單領導者、多領導者和無領導者的方法。
如果你有一個類似於 “我想儲存一些資料並稍後再查詢” 的問題,那麼並沒有一種正確的解決方案。但對於不同的具體環境,總會有不同的合適方法。軟體實現通常必須選擇一種特定的方法。使單條程式碼路徑能做到穩定健壯且表現良好已經是一件非常困難的事情了 —— 嘗試在單個軟體中完成所有事情,幾乎可以保證,實現效果會很差。 如果你有一個類似於 “我想儲存一些資料並稍後再查詢” 的問題,那麼並沒有一種正確的解決方案。但對於不同的具體環境,總會有不同的合適方法。軟體實現通常必須選擇一種特定的方法。使單條程式碼路徑能做到穩定健壯且表現良好已經是一件非常困難的事情了 —— 嘗試在單個軟體中完成所有事情,幾乎可以保證,實現效果會很差。
@ -32,7 +34,7 @@
例如,為了處理任意關鍵詞的搜尋查詢,將 OLTP 資料庫與全文搜尋索引整合在一起是很常見的需求。儘管一些資料庫(例如 PostgreSQL包含了全文索引功能對於簡單的應用完全夠了【1】但更複雜的搜尋能力就需要專業的資訊檢索工具了。相反的是搜尋索引通常不適合作為持久的記錄系統因此許多應用需要組合這兩種不同的工具以滿足所有需求。 例如,為了處理任意關鍵詞的搜尋查詢,將 OLTP 資料庫與全文搜尋索引整合在一起是很常見的需求。儘管一些資料庫(例如 PostgreSQL包含了全文索引功能對於簡單的應用完全夠了【1】但更複雜的搜尋能力就需要專業的資訊檢索工具了。相反的是搜尋索引通常不適合作為持久的記錄系統因此許多應用需要組合這兩種不同的工具以滿足所有需求。
我們在 “[保持系統同步](ch11.md#保持系統同步)” 中接觸過整合資料系統的問題。隨著資料不同表示形式的增加,整合問題變得越來越困難。除了資料庫和搜尋索引之外,也許你需要在分析系統(資料倉庫,或批處理和流處理系統)中維護資料副本;維護從原始資料中衍生的快取,或反規範化的資料版本;將資料灌入機器學習、分類、排名或推薦系統中;或者基於資料變更傳送通知。 我們在 “[保持系統同步](/tw/ch11#保持系統同步)” 中接觸過整合資料系統的問題。隨著資料不同表示形式的增加,整合問題變得越來越困難。除了資料庫和搜尋索引之外,也許你需要在分析系統(資料倉庫,或批處理和流處理系統)中維護資料副本;維護從原始資料中衍生的快取,或反規範化的資料版本;將資料灌入機器學習、分類、排名或推薦系統中;或者基於資料變更傳送通知。
令人驚訝的是我經常看到軟體工程師做出這樣的陳述“根據我的經驗99% 的人只需要 X” 或者 “...... 不需要 X”對於各種各樣的 X。我認為這種陳述更像是發言人自己的經驗而不是技術實際上的實用性。可能對資料執行的操作其範圍極其寬廣。某人認為雞肋而毫無意義的功能可能是別人的核心需求。當你拉高視角並考慮跨越整個組織範圍的資料流時資料整合的需求往往就會變得明顯起來。 令人驚訝的是我經常看到軟體工程師做出這樣的陳述“根據我的經驗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#分割槽日誌)”)。然後兩個不同分割槽中的事件順序關係就不明確了。 * 在大多數情況下,構建完全有序的日誌,需要所有事件彙集於決定順序的 **單個領導者節點**。如果事件吞吐量大於單臺計算機的處理能力,則需要將其分割槽到多臺計算機上(請參閱 “[分割槽日誌](/tw/ch11#分割槽日誌)”)。然後兩個不同分割槽中的事件順序關係就不明確了。
* 如果伺服器分佈在多個 **地理位置分散** 的資料中心上,例如為了容忍整個資料中心掉線,你通常在每個資料中心都有單獨的主庫,因為網路延遲會導致同步的跨資料中心協調效率低下(請參閱 “[多主複製](ch5.md#多主複製)”)。這意味著源自兩個不同資料中心的事件順序未定義。 * 如果伺服器分佈在多個 **地理位置分散** 的資料中心上,例如為了容忍整個資料中心掉線,你通常在每個資料中心都有單獨的主庫,因為網路延遲會導致同步的跨資料中心協調效率低下(請參閱 “[多主複製](/tw/ch5#多主複製)”)。這意味著源自兩個不同資料中心的事件順序未定義。
* 將應用程式部署為微服務時(請參閱 “[服務中的資料流REST 與 RPC](ch4.md#服務中的資料流REST與RPC)”),常見的設計選擇是將每個服務及其持久狀態作為獨立單元進行部署,服務之間不共享持久狀態。當兩個事件來自不同的服務時,這些事件間的順序未定義。 * 將應用程式部署為微服務時(請參閱 “[服務中的資料流REST 與 RPC](/tw/ch4#服務中的資料流REST與RPC)”),常見的設計選擇是將每個服務及其持久狀態作為獨立單元進行部署,服務之間不共享持久狀態。當兩個事件來自不同的服務時,這些事件間的順序未定義。
* 某些應用程式在客戶端儲存狀態,該狀態在使用者輸入時立即更新(無需等待伺服器確認),甚至可以繼續離線工作(請參閱 “[需要離線操作的客戶端](ch5.md#需要離線操作的客戶端)”)。對於這樣的應用程式,客戶端和伺服器很可能以不同的順序看到事件。 * 某些應用程式在客戶端儲存狀態,該狀態在使用者輸入時立即更新(無需等待伺服器確認),甚至可以繼續離線工作(請參閱 “[需要離線操作的客戶端](/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】。我們將在 “[讀也是事件](#讀也是事件)” 中回到這個想法。 * 如果你可以記錄一個事件來記錄使用者在做出決定之前所看到的系統狀態並給該事件一個唯一的識別符號那麼後面的任何事件都可以引用該事件識別符號來記錄因果關係【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】。 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 架構【12】是這方面的一個建議引起了很多關注。
Lambda 架構的核心思想是透過將不可變事件附加到不斷增長的資料集來記錄傳入資料,這類似於事件溯源(請參閱 “[事件溯源](ch11.md#事件溯源)”。為了從這些事件中衍生出讀取最佳化的檢視Lambda 架構建議並行執行兩個不同的系統:批處理系統(如 Hadoop MapReduce和獨立的流處理系統如 Storm Lambda 架構的核心思想是透過將不可變事件附加到不斷增長的資料集來記錄傳入資料,這類似於事件溯源(請參閱 “[事件溯源](/tw/ch11#事件溯源)”。為了從這些事件中衍生出讀取最佳化的檢視Lambda 架構建議並行執行兩個不同的系統:批處理系統(如 Hadoop MapReduce和獨立的流處理系統如 Storm
在 Lambda 方法中,流處理器消耗事件並快速生成對檢視的近似更新;批處理器稍後將使用同一組事件並生成衍生檢視的更正版本。這個設計背後的原因是批處理更簡單,因此不易出錯,而流處理器被認為是不太可靠和難以容錯的(請參閱 “[容錯](ch11.md#容錯)”)。而且,流處理可以使用快速近似演算法,而批處理使用較慢的精確演算法。 在 Lambda 方法中,流處理器消耗事件並快速生成對檢視的近似更新;批處理器稍後將使用同一組事件並生成衍生檢視的更正版本。這個設計背後的原因是批處理更簡單,因此不易出錯,而流處理器被認為是不太可靠和難以容錯的(請參閱 “[容錯](/tw/ch11#容錯)”)。而且,流處理可以使用快速近似演算法,而批處理使用較慢的精確演算法。
Lambda 架構是一種有影響力的想法,它將資料系統的設計變得更好,尤其是透過推廣這樣的原則:在不可變事件流上建立衍生檢視,並在需要時重新處理事件。但是我也認為它有一些實際問題: Lambda 架構是一種有影響力的想法,它將資料系統的設計變得更好,尤其是透過推廣這樣的原則:在不可變事件流上建立衍生檢視,並在需要時重新處理事件。但是我也認為它有一些實際問題:
* 在批處理和流處理框架中維護相同的邏輯是很顯著的額外工作。雖然像 Summingbird【13】這樣的庫提供了一種可以在批處理和流處理的上下文中執行的計算抽象。除錯、調整和維護兩個不同系統的操作複雜性依然存在【14】。 * 在批處理和流處理框架中維護相同的邏輯是很顯著的額外工作。雖然像 Summingbird【13】這樣的庫提供了一種可以在批處理和流處理的上下文中執行的計算抽象。除錯、調整和維護兩個不同系統的操作複雜性依然存在【14】。
* 由於流管道和批處理管道產生獨立的輸出,因此需要合併它們以響應使用者請求。如果計算是基於滾動視窗的簡單聚合,則合併相當容易,但如果檢視基於更複雜的操作(例如連線和會話化)而匯出,或者輸出不是時間序列,則會變得非常困難。 * 由於流管道和批處理管道產生獨立的輸出,因此需要合併它們以響應使用者請求。如果計算是基於滾動視窗的簡單聚合,則合併相當容易,但如果檢視基於更複雜的操作(例如連線和會話化)而匯出,或者輸出不是時間序列,則會變得非常困難。
* 儘管有能力重新處理整個歷史資料集是很好的,但在大型資料集上這樣做經常會開銷巨大。因此,批處理流水線通常需要設定為處理增量批處理(例如,在每小時結束時處理一小時的資料),而不是重新處理所有內容。這引發了 “[時間推理](ch11.md#時間推理)” 中討論的問題,例如處理滯留事件和處理跨批次邊界的視窗。增量化批處理計算會增加複雜性,使其更類似於流式傳輸層,這與保持批處理層儘可能簡單的目標背道而馳。 * 儘管有能力重新處理整個歷史資料集是很好的,但在大型資料集上這樣做經常會開銷巨大。因此,批處理流水線通常需要設定為處理增量批處理(例如,在每小時結束時處理一小時的資料),而不是重新處理所有內容。這引發了 “[時間推理](/tw/ch11#時間推理)” 中討論的問題,例如處理滯留事件和處理跨批次邊界的視窗。增量化批處理計算會增加複雜性,使其更類似於流式傳輸層,這與保持批處理層儘可能簡單的目標背道而馳。
#### 統一批處理和流處理 #### 統一批處理和流處理
@ -147,14 +149,14 @@ Lambda 架構是一種有影響力的想法,它將資料系統的設計變得
在一個系統中統一批處理和流處理需要以下功能,這些功能也正在越來越廣泛地被提供: 在一個系統中統一批處理和流處理需要以下功能,這些功能也正在越來越廣泛地被提供:
* 透過處理最近事件流的相同處理引擎來重播歷史事件的能力。例如,基於日誌的訊息代理可以重播訊息(請參閱 “[重播舊訊息](ch11.md#重播舊訊息)”),某些流處理器可以從 HDFS 等分散式檔案系統讀取輸入。 * 透過處理最近事件流的相同處理引擎來重播歷史事件的能力。例如,基於日誌的訊息代理可以重播訊息(請參閱 “[重播舊訊息](/tw/ch11#重播舊訊息)”),某些流處理器可以從 HDFS 等分散式檔案系統讀取輸入。
* 對於流處理器來說,恰好一次語義 —— 即確保輸出與未發生故障的輸出相同,即使事實上發生故障(請參閱 “[容錯](ch11.md#容錯)”)。與批處理一樣,這需要丟棄任何失敗任務的部分輸出。 * 對於流處理器來說,恰好一次語義 —— 即確保輸出與未發生故障的輸出相同,即使事實上發生故障(請參閱 “[容錯](/tw/ch11#容錯)”)。與批處理一樣,這需要丟棄任何失敗任務的部分輸出。
* 按事件時間進行視窗化的工具,而不是按處理時間進行視窗化,因為處理歷史事件時,處理時間毫無意義(請參閱 “[時間推理](ch11.md#時間推理)”。例如Apache Beam 提供了用於表達這種計算的 API可以在 Apache Flink 或 Google Cloud Dataflow 使用。 * 按事件時間進行視窗化的工具,而不是按處理時間進行視窗化,因為處理歷史事件時,處理時間毫無意義(請參閱 “[時間推理](/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 萬個小記錄的資料庫完全是尋常而不起眼的。無論如何,作業系統和資料庫之間的相似之處和差異值得探討。 當然,有很多實際的差異。例如,許多檔案系統都不能很好地處理包含 1000 萬個小檔案的目錄,而包含 1000 萬個小記錄的資料庫完全是尋常而不起眼的。無論如何,作業系統和資料庫之間的相似之處和差異值得探討。
@ -170,12 +172,12 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
在本書的過程中,我們討論了資料庫提供的各種功能及其工作原理,其中包括: 在本書的過程中,我們討論了資料庫提供的各種功能及其工作原理,其中包括:
* 次級索引,使你可以根據欄位的值有效地搜尋記錄(請參閱 “[其他索引結構](ch3.md#其他索引結構)”) * 次級索引,使你可以根據欄位的值有效地搜尋記錄(請參閱 “[其他索引結構](/tw/ch3#其他索引結構)”)
* 物化檢視,這是一種預計算的查詢結果快取(請參閱 “[聚合:資料立方體和物化檢視](ch3.md#聚合:資料立方體和物化檢視)”) * 物化檢視,這是一種預計算的查詢結果快取(請參閱 “[聚合:資料立方體和物化檢視](/tw/ch3#聚合:資料立方體和物化檢視)”)
* 複製日誌,保持其他節點上資料的副本最新(請參閱 “[複製日誌的實現](ch5.md#複製日誌的實現)”) * 複製日誌,保持其他節點上資料的副本最新(請參閱 “[複製日誌的實現](/tw/ch5#複製日誌的實現)”)
* 全文搜尋索引,允許在文字中進行關鍵字搜尋(請參閱 “[全文搜尋和模糊索引](ch3.md#全文搜尋和模糊索引)”也內置於某些關係資料庫【1】 * 全文搜尋索引,允許在文字中進行關鍵字搜尋(請參閱 “[全文搜尋和模糊索引](/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` 在關係資料庫中建立一個新的索引時會發生什麼。資料庫必須掃描表的一致性快照,挑選出所有被索引的欄位值,對它們進行排序,然後寫出索引。然後它必須處理自一致快照以來所做的寫入操作(假設表在建立索引時未被鎖定,所以寫操作可能會繼續)。一旦完成,只要事務寫入表中,資料庫就必須繼續保持索引最新。 想想當你執行 `CREATE INDEX` 在關係資料庫中建立一個新的索引時會發生什麼。資料庫必須掃描表的一致性快照,挑選出所有被索引的欄位值,對它們進行排序,然後寫出索引。然後它必須處理自一致快照以來所做的寫入操作(假設表在建立索引時未被鎖定,所以寫操作可能會繼續)。一旦完成,只要事務寫入表中,資料庫就必須繼續保持索引最新。
此過程非常類似於設定新的從庫副本(請參閱 “[設定新從庫](ch5.md#設定新從庫)”),也非常類似於流處理系統中的 **引導bootstrap** 變更資料捕獲(請參閱 “[初始快照](ch11.md#初始快照)”)。 此過程非常類似於設定新的從庫副本(請參閱 “[設定新從庫](/tw/ch5#設定新從庫)”),也非常類似於流處理系統中的 **引導bootstrap** 變更資料捕獲(請參閱 “[初始快照](/tw/ch11#初始快照)”)。
無論何時執行 `CREATE INDEX`,資料庫都會重新處理現有資料集(如 “[應用演化後重新處理資料](#應用演化後重新處理資料)” 中所述),並將該索引作為新檢視匯出到現有資料上。現有資料可能是狀態的快照,而不是所有發生變化的日誌,但兩者密切相關(請參閱 “[狀態、流和不變性](ch11.md#狀態、流和不變性)”)。 無論何時執行 `CREATE INDEX`,資料庫都會重新處理現有資料集(如 “[應用演化後重新處理資料](#應用演化後重新處理資料)” 中所述),並將該索引作為新檢視匯出到現有資料上。現有資料可能是狀態的快照,而不是所有發生變化的日誌,但兩者密切相關(請參閱 “[狀態、流和不變性](/tw/ch11#狀態、流和不變性)”)。
#### 一切的元資料庫 #### 一切的元資料庫
有鑑於此我認為整個組織的資料流開始像一個巨大的資料庫【7】。每當批處理、流或 ETL 過程將資料從一個地方傳輸到另一個地方並組裝時,它表現地就像資料庫子系統一樣,使索引或物化檢視保持最新。 有鑑於此我認為整個組織的資料流開始像一個巨大的資料庫【7】。每當批處理、流或 ETL 過程將資料從一個地方傳輸到另一個地方並組裝時,它表現地就像資料庫子系統一樣,使索引或物化檢視保持最新。
從這種角度來看,批處理和流處理器就像精心實現的觸發器、儲存過程和物化檢視維護例程。它們維護的衍生資料系統就像不同的索引型別。例如,關係資料庫可能支援 B 樹索引、雜湊索引、空間索引(請參閱 “[多列索引](ch3.md#多列索引)”)以及其他型別的索引。在新興的衍生資料系統架構中,不是將這些設施作為單個整合資料庫產品的功能實現,而是由各種不同的軟體提供,執行在不同的機器上,由不同的團隊管理。 從這種角度來看,批處理和流處理器就像精心實現的觸發器、儲存過程和物化檢視維護例程。它們維護的衍生資料系統就像不同的索引型別。例如,關係資料庫可能支援 B 樹索引、雜湊索引、空間索引(請參閱 “[多列索引](/tw/ch3#多列索引)”)以及其他型別的索引。在新興的衍生資料系統架構中,不是將這些設施作為單個整合資料庫產品的功能實現,而是由各種不同的軟體提供,執行在不同的機器上,由不同的團隊管理。
這些發展在未來將會把我們帶到哪裡?如果我們從沒有適合所有訪問模式的單一資料模型或儲存格式的前提出發,我推測有兩種途徑可以將不同的儲存和處理工具組合成一個有凝聚力的系統: 這些發展在未來將會把我們帶到哪裡?如果我們從沒有適合所有訪問模式的單一資料模型或儲存格式的前提出發,我推測有兩種途徑可以將不同的儲存和處理工具組合成一個有凝聚力的系統:
@ -213,20 +215,20 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
傳統的同步寫入方法需要跨異構儲存系統的分散式事務【18】我認為這是錯誤的解決方案請參閱 “[衍生資料與分散式事務](#衍生資料與分散式事務)”)。單個儲存或流處理系統內的事務是可行的,但是當資料跨越不同技術之間的邊界時,我認為具有冪等寫入的非同步事件日誌是一種更加健壯和實用的方法。 傳統的同步寫入方法需要跨異構儲存系統的分散式事務【18】我認為這是錯誤的解決方案請參閱 “[衍生資料與分散式事務](#衍生資料與分散式事務)”)。單個儲存或流處理系統內的事務是可行的,但是當資料跨越不同技術之間的邊界時,我認為具有冪等寫入的非同步事件日誌是一種更加健壯和實用的方法。
例如,分散式事務在某些流處理元件內部使用,以匹配 **恰好一次exactly-once** 語義(請參閱 “[原子提交再現](ch11.md#原子提交再現)”),這可以很好地工作。然而,當事務需要涉及由不同人群編寫的系統時(例如,當資料從流處理元件寫入分散式鍵值儲存或搜尋索引時),缺乏標準化的事務協議會使整合更難。有冪等消費者的有序事件日誌(請參閱 “[冪等性](ch11.md#冪等性)”是一種更簡單的抽象因此在異構系統中實現更加可行【7】。 例如,分散式事務在某些流處理元件內部使用,以匹配 **恰好一次exactly-once** 語義(請參閱 “[原子提交再現](/tw/ch11#原子提交再現)”),這可以很好地工作。然而,當事務需要涉及由不同人群編寫的系統時(例如,當資料從流處理元件寫入分散式鍵值儲存或搜尋索引時),缺乏標準化的事務協議會使整合更難。有冪等消費者的有序事件日誌(請參閱 “[冪等性](/tw/ch11#冪等性)”是一種更簡單的抽象因此在異構系統中實現更加可行【7】。
基於日誌的整合的一大優勢是各個元件之間的 **鬆散耦合loose coupling**,這體現在兩個方面: 基於日誌的整合的一大優勢是各個元件之間的 **鬆散耦合loose coupling**,這體現在兩個方面:
1. 在系統級別,非同步事件流使整個系統在個別元件的中斷或效能下降時更加穩健。如果消費者執行緩慢或失敗,那麼事件日誌可以緩衝訊息(請參閱 “[磁碟空間使用](ch11.md#磁碟空間使用)”),以便生產者和任何其他消費者可以繼續不受影響地執行。有問題的消費者可以在問題修復後趕上,因此不會錯過任何資料,並且包含故障。相比之下,分散式事務的同步互動往往會將本地故障升級為大規模故障(請參閱 “[分散式事務的限制](ch9.md#分散式事務的限制)”)。 1. 在系統級別,非同步事件流使整個系統在個別元件的中斷或效能下降時更加穩健。如果消費者執行緩慢或失敗,那麼事件日誌可以緩衝訊息(請參閱 “[磁碟空間使用](/tw/ch11#磁碟空間使用)”),以便生產者和任何其他消費者可以繼續不受影響地執行。有問題的消費者可以在問題修復後趕上,因此不會錯過任何資料,並且包含故障。相比之下,分散式事務的同步互動往往會將本地故障升級為大規模故障(請參閱 “[分散式事務的限制](/tw/ch9#分散式事務的限制)”)。
2. 在人力方面,分拆資料系統允許不同的團隊獨立開發,改進和維護不同的軟體元件和服務。專業化使得每個團隊都可以專注於做好一件事,並與其他團隊的系統以明確的介面互動。事件日誌提供了一個足夠強大的介面,以捕獲相當強的一致性屬性(由於永續性和事件的順序),但也足夠普適於幾乎任何型別的資料。 2. 在人力方面,分拆資料系統允許不同的團隊獨立開發,改進和維護不同的軟體元件和服務。專業化使得每個團隊都可以專注於做好一件事,並與其他團隊的系統以明確的介面互動。事件日誌提供了一個足夠強大的介面,以捕獲相當強的一致性屬性(由於永續性和事件的順序),但也足夠普適於幾乎任何型別的資料。
#### 分拆系統vs整合系統 #### 分拆系統vs整合系統
如果分拆確實成為未來的方式,它也不會取代目前形式的資料庫 —— 它們仍然會像以往一樣被需要。為了維護流處理元件中的狀態,資料庫仍然是需要的,並且為批處理和流處理器的輸出提供查詢服務(請參閱 “[批處理工作流的輸出](ch10.md#批處理工作流的輸出)” 與 “[流處理](ch11.md#流處理)”。專用查詢引擎對於特定的工作負載仍然非常重要例如MPP 資料倉庫中的查詢引擎針對探索性分析查詢進行了最佳化,並且能夠很好地處理這種型別的工作負載(請參閱 “[Hadoop 與分散式資料庫的對比](ch10.md#Hadoop與分散式資料庫的對比)”)。 如果分拆確實成為未來的方式,它也不會取代目前形式的資料庫 —— 它們仍然會像以往一樣被需要。為了維護流處理元件中的狀態,資料庫仍然是需要的,並且為批處理和流處理器的輸出提供查詢服務(請參閱 “[批處理工作流的輸出](/tw/ch10#批處理工作流的輸出)” 與 “[流處理](/tw/ch11#流處理)”。專用查詢引擎對於特定的工作負載仍然非常重要例如MPP 資料倉庫中的查詢引擎針對探索性分析查詢進行了最佳化,並且能夠很好地處理這種型別的工作負載(請參閱 “[Hadoop 與分散式資料庫的對比](/tw/ch10#Hadoop與分散式資料庫的對比)”)。
執行幾種不同基礎設施的複雜性可能是一個問題每種軟體都有一個學習曲線配置問題和操作怪癖因此部署儘可能少的移動部件是很有必要的。比起使用應用程式碼拼接多個工具而成的系統單一整合軟體產品也可以在其設計應對的工作負載型別上實現更好、更可預測的效能【23】。正如在前言中所說的那樣為了不需要的規模而構建系統是白費精力而且可能會將你鎖死在一個不靈活的設計中。實際上這是一種過早最佳化的形式。 執行幾種不同基礎設施的複雜性可能是一個問題每種軟體都有一個學習曲線配置問題和操作怪癖因此部署儘可能少的移動部件是很有必要的。比起使用應用程式碼拼接多個工具而成的系統單一整合軟體產品也可以在其設計應對的工作負載型別上實現更好、更可預測的效能【23】。正如在前言中所說的那樣為了不需要的規模而構建系統是白費精力而且可能會將你鎖死在一個不靈活的設計中。實際上這是一種過早最佳化的形式。
分拆的目標不是要針對個別資料庫與特定工作負載的效能進行競爭;我們的目標是允許你結合多個不同的資料庫,以便在比單個軟體可能實現的更廣泛的工作負載範圍內實現更好的效能。這是關於廣度,而不是深度 —— 與我們在 “[Hadoop 與分散式資料庫的對比](ch10.md#Hadoop與分散式資料庫的對比)” 中討論的儲存和處理模型的多樣性一樣。 分拆的目標不是要針對個別資料庫與特定工作負載的效能進行競爭;我們的目標是允許你結合多個不同的資料庫,以便在比單個軟體可能實現的更廣泛的工作負載範圍內實現更好的效能。這是關於廣度,而不是深度 —— 與我們在 “[Hadoop 與分散式資料庫的對比](/tw/ch10#Hadoop與分散式資料庫的對比)” 中討論的儲存和處理模型的多樣性一樣。
因此,如果有一項技術可以滿足你的所有需求,那麼最好使用該產品,而不是試圖用更低層級的元件重新實現它。只有當沒有單一軟體滿足你的所有需求時,才會出現拆分和聯合的優勢。 因此,如果有一項技術可以滿足你的所有需求,那麼最好使用該產品,而不是試圖用更低層級的元件重新實現它。只有當沒有單一軟體滿足你的所有需求時,才會出現拆分和聯合的優勢。
@ -236,7 +238,7 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
例如,如果我們可以簡單地宣告 `mysql | elasticsearch`,類似於 Unix 管道【22】成為 `CREATE INDEX` 的分拆等價物:它將讀取 MySQL 資料庫中的所有文件並將其索引到 Elasticsearch 叢集中。然後它會不斷捕獲對資料庫所做的所有變更,並自動將它們應用於搜尋索引,而無需編寫自定義應用程式碼。這種整合應當支援幾乎任何型別的儲存或索引系統。 例如,如果我們可以簡單地宣告 `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 中的變更可能需要更新快取填充方式的定義,並重建快取。 * 快取通常包含將以使用者介面UI顯示的形式的資料聚合。因此填充快取需要知道 UI 中引用的欄位UI 中的變更可能需要更新快取填充方式的定義,並重建快取。
用於次級索引的衍生函式是如此常用的需求,以致於它作為核心功能被內建至許多資料庫中,你可以簡單地透過 `CREATE INDEX` 來呼叫它。對於全文索引常見語言的基本語言特徵可能內建到資料庫中但更複雜的特徵通常需要領域特定的調整。在機器學習中特徵工程是眾所周知的特定於應用的特徵通常需要包含很多關於使用者互動與應用部署的詳細知識【35】。 用於次級索引的衍生函式是如此常用的需求,以致於它作為核心功能被內建至許多資料庫中,你可以簡單地透過 `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 的機器崩潰,狀態和訊息就會丟失。 * 容錯是衍生資料的關鍵:僅僅丟失單個訊息就會導致衍生資料集永遠與其資料來源失去同步。訊息傳遞和衍生狀態更新都必須可靠。例如,許多 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】。但底層通訊機制是有很大區別資料流採用單向非同步訊息流而不是同步的請求 / 響應式互動。 在資料流中組裝流運算元與微服務方法有很多相似之處【40】。但底層通訊機制是有很大區別資料流採用單向非同步訊息流而不是同步的請求 / 響應式互動。
除了在 “[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)” 中列出的優點如更好的容錯性資料流系統還能實現更好的效能。例如假設客戶正在購買以一種貨幣定價但以另一種貨幣支付的商品。為了執行貨幣換算你需要知道當前的匯率。這個操作可以透過兩種方式實現【40,41】 除了在 “[訊息傳遞中的資料流](/tw/ch4#訊息傳遞中的資料流)” 中列出的優點如更好的容錯性資料流系統還能實現更好的效能。例如假設客戶正在購買以一種貨幣定價但以另一種貨幣支付的商品。為了執行貨幣換算你需要知道當前的匯率。這個操作可以透過兩種方式實現【40,41】
1. 在微服務方法中,處理購買的程式碼可能會查詢匯率服務或資料庫,以獲取特定貨幣的當前匯率。 1. 在微服務方法中,處理購買的程式碼可能會查詢匯率服務或資料庫,以獲取特定貨幣的當前匯率。
2. 在資料流方法中,處理訂單的程式碼會提前訂閱匯率變更流,並在匯率發生變動時將當前匯率儲存在本地資料庫中。處理訂單時只需查詢本地資料庫即可。 2. 在資料流方法中,處理訂單的程式碼會提前訂閱匯率變更流,並在匯率發生變動時將當前匯率儲存在本地資料庫中。處理訂單時只需查詢本地資料庫即可。
第二種方法能將對另一服務的同步網路請求替換為對本地資料庫的查詢(可能在同一臺機器甚至同一個程序中)[^ii]。資料流方法不僅更快,而且當其他服務失效時也更穩健。最快且最可靠的網路請求就是壓根沒有網路請求!我們現在不再使用 RPC而是在購買事件和匯率更新事件之間建立流聯接請參閱 “[流表連線(流擴充)](ch11.md#流表連線(流擴充))”)。 第二種方法能將對另一服務的同步網路請求替換為對本地資料庫的查詢(可能在同一臺機器甚至同一個程序中)[^ii]。資料流方法不僅更快,而且當其他服務失效時也更穩健。最快且最可靠的網路請求就是壓根沒有網路請求!我們現在不再使用 RPC而是在購買事件和匯率更新事件之間建立流聯接請參閱 “[流表連線(流擴充)](/tw/ch11#流表連線(流擴充))”)。
[^ii]: 在微服務方法中,你也可以透過在處理購買的服務中本地快取匯率來避免同步網路請求。但是為了保證快取的新鮮度,你需要定期輪詢匯率以獲取其更新,或訂閱變更流 —— 這恰好是資料流方法中發生的事情。 [^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 在搜尋索引中,寫(文件更新)遇上讀(查詢)** **圖 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 掃描也是可行的。由此來看,快取,索引和物化檢視的作用很簡單:它們改變了讀路徑與寫路徑之間的邊界。透過預先計算結果,從而允許我們在寫路徑上做更多的工作,以節省讀路徑上的工作量。 從這個例子中我們可以看到,索引不是寫路徑和讀路徑之間唯一可能的邊界;快取常見搜尋結果也是可行的;而在少量文件上使用沒有索引的類 grep 掃描也是可行的。由此來看,快取,索引和物化檢視的作用很簡單:它們改變了讀路徑與寫路徑之間的邊界。透過預先計算結果,從而允許我們在寫路徑上做更多的工作,以節省讀路徑上的工作量。
在寫路徑上完成的工作和讀路徑之間的界限,實際上是本書開始處在 “[描述負載](ch1.md#描述負載)” 中推特例子裡談到的主題。在該例中,我們還看到了與普通使用者相比,名人的寫路徑和讀路徑可能有所不同。在 500 頁之後,我們已經繞回了起點! 在寫路徑上完成的工作和讀路徑之間的界限,實際上是本書開始處在 “[描述負載](/tw/ch1#描述負載)” 中推特例子裡談到的主題。在該例中,我們還看到了與普通使用者相比,名人的寫路徑和讀路徑可能有所不同。在 500 頁之後,我們已經繞回了起點!
#### 有狀態、可離線的客戶端 #### 有狀態、可離線的客戶端
@ -355,7 +357,7 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
傳統上,網路瀏覽器是無狀態的客戶端,只有當連線到網際網路時才能做一些有用的事情(能離線執行的唯一事情基本上就是上下滾動之前線上時載入好的頁面)。然而,最近的 “單頁面” JavaScript Web 應用已經獲得了很多有狀態的功能,包括客戶端使用者介面互動,以及 Web 瀏覽器中的持久化本地儲存。移動應用可以類似地在裝置上儲存大量狀態,而且大多數使用者互動都不需要與伺服器往返互動。 傳統上,網路瀏覽器是無狀態的客戶端,只有當連線到網際網路時才能做一些有用的事情(能離線執行的唯一事情基本上就是上下滾動之前線上時載入好的頁面)。然而,最近的 “單頁面” JavaScript Web 應用已經獲得了很多有狀態的功能,包括客戶端使用者介面互動,以及 Web 瀏覽器中的持久化本地儲存。移動應用可以類似地在裝置上儲存大量狀態,而且大多數使用者互動都不需要與伺服器往返互動。
這些不斷變化的功能重新引發了對 **離線優先offline-first** 應用的興趣這些應用盡可能地在同一裝置上使用本地資料庫無需連線網際網路並在後臺網路連線可用時與遠端伺服器同步【42】。由於移動裝置通常具有緩慢且不可靠的蜂窩網路連線因此如果使用者的使用者介面不必等待同步網路請求且應用主要是離線工作的則這是一個巨大優勢請參閱 “[需要離線操作的客戶端](ch5.md#需要離線操作的客戶端)”)。 這些不斷變化的功能重新引發了對 **離線優先offline-first** 應用的興趣這些應用盡可能地在同一裝置上使用本地資料庫無需連線網際網路並在後臺網路連線可用時與遠端伺服器同步【42】。由於移動裝置通常具有緩慢且不可靠的蜂窩網路連線因此如果使用者的使用者介面不必等待同步網路請求且應用主要是離線工作的則這是一個巨大優勢請參閱 “[需要離線操作的客戶端](/tw/ch5#需要離線操作的客戶端)”)。
當我們擺脫無狀態客戶端與中央資料庫互動的假設,並轉向在終端使用者裝置上維護狀態時,這就開啟了新世界的大門。特別是,我們可以將裝置上的狀態視為 **伺服器狀態的快取**。螢幕上的畫素是客戶端應用中模型物件的物化檢視模型物件是遠端資料中心的本地狀態副本【27】。 當我們擺脫無狀態客戶端與中央資料庫互動的假設,並轉向在終端使用者裝置上維護狀態時,這就開啟了新世界的大門。特別是,我們可以將裝置上的狀態視為 **伺服器狀態的快取**。螢幕上的畫素是客戶端應用中模型物件的物化檢視模型物件是遠端資料中心的本地狀態副本【27】。
@ -367,17 +369,17 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
用我們的寫路徑與讀路徑模型來講主動將狀態變更推至到客戶端裝置意味著將寫路徑一直延伸到終端使用者。當客戶端首次初始化時它仍然需要使用讀路徑來獲取其初始狀態但此後它就能夠依賴伺服器傳送的狀態變更流了。我們在流處理和訊息傳遞部分討論的想法並不侷限於資料中心中我們可以進一步採納這些想法並將它們一直延伸到終端使用者裝置【43】。 用我們的寫路徑與讀路徑模型來講主動將狀態變更推至到客戶端裝置意味著將寫路徑一直延伸到終端使用者。當客戶端首次初始化時它仍然需要使用讀路徑來獲取其初始狀態但此後它就能夠依賴伺服器傳送的狀態變更流了。我們在流處理和訊息傳遞部分討論的想法並不侷限於資料中心中我們可以進一步採納這些想法並將它們一直延伸到終端使用者裝置【43】。
這些裝置有時會離線,並在此期間無法收到伺服器狀態變更的任何通知。但是我們已經解決了這個問題:在 “[消費者偏移量](ch11.md#消費者偏移量)” 中,我們討論了基於日誌的訊息代理的消費者能在失敗或斷開連線後重連,並確保它不會錯過掉線期間任何到達的訊息。同樣的技術適用於單個使用者,每個裝置都是一個小事件流的小小訂閱者。 這些裝置有時會離線,並在此期間無法收到伺服器狀態變更的任何通知。但是我們已經解決了這個問題:在 “[消費者偏移量](/tw/ch11#消費者偏移量)” 中,我們討論了基於日誌的訊息代理的消費者能在失敗或斷開連線後重連,並確保它不會錯過掉線期間任何到達的訊息。同樣的技術適用於單個使用者,每個裝置都是一個小事件流的小小訂閱者。
#### 端到端的事件流 #### 端到端的事件流
最近用於開發有狀態的客戶端與使用者介面的工具,例如如 Elm 語言【30】和 Facebook 的 React、Flux 和 Redux 工具鏈,已經透過訂閱表示使用者輸入或伺服器響應的事件流來管理客戶端的內部狀態,其結構與事件溯源相似(請參閱 “[事件溯源](ch11.md#事件溯源)”)。 最近用於開發有狀態的客戶端與使用者介面的工具,例如如 Elm 語言【30】和 Facebook 的 React、Flux 和 Redux 工具鏈,已經透過訂閱表示使用者輸入或伺服器響應的事件流來管理客戶端的內部狀態,其結構與事件溯源相似(請參閱 “[事件溯源](/tw/ch11#事件溯源)”)。
將這種程式設計模型擴充套件為:允許伺服器將狀態變更事件推送到客戶端的事件管道中,是非常自然的。因此,狀態變化可以透過 **端到端end-to-end** 的寫路徑流動:從一個裝置上的互動觸發狀態變更開始,經由事件日誌,並穿過幾個衍生資料系統與流處理器,一直到另一臺裝置上的使用者介面,而有人正在觀察使用者介面上的狀態變化。這些狀態變化能以相當低的延遲傳播 —— 比如說,在一秒內從一端到另一端。 將這種程式設計模型擴充套件為:允許伺服器將狀態變更事件推送到客戶端的事件管道中,是非常自然的。因此,狀態變化可以透過 **端到端end-to-end** 的寫路徑流動:從一個裝置上的互動觸發狀態變更開始,經由事件日誌,並穿過幾個衍生資料系統與流處理器,一直到另一臺裝置上的使用者介面,而有人正在觀察使用者介面上的狀態變化。這些狀態變化能以相當低的延遲傳播 —— 比如說,在一秒內從一端到另一端。
一些應用(如即時訊息傳遞與線上遊戲)已經具有這種 “即時” 架構(在低延遲互動的意義上,不是在 “[響應時間保證](ch8.md#響應時間保證)” 中的意義上)。但我們為什麼不用這種方式構建所有的應用? 一些應用(如即時訊息傳遞與線上遊戲)已經具有這種 “即時” 架構(在低延遲互動的意義上,不是在 “[響應時間保證](/tw/ch8#響應時間保證)” 中的意義上)。但我們為什麼不用這種方式構建所有的應用?
挑戰在於,關於無狀態客戶端和請求 / 響應互動的假設已經根深蒂固地植入在我們的資料庫、庫、框架以及協議之中。許多資料儲存支援讀取與寫入操作,為請求返回一個響應,但只有極少數提供訂閱變更的能力 —— 請求返回一個隨時間推移的響應流(請參閱 “[變更流的 API 支援](ch11.md#變更流的API支援)” )。 挑戰在於,關於無狀態客戶端和請求 / 響應互動的假設已經根深蒂固地植入在我們的資料庫、庫、框架以及協議之中。許多資料儲存支援讀取與寫入操作,為請求返回一個響應,但只有極少數提供訂閱變更的能力 —— 請求返回一個隨時間推移的響應流(請參閱 “[變更流的 API 支援](/tw/ch11#變更流的API支援)” )。
為了將寫路徑延伸至終端使用者,我們需要從根本上重新思考我們構建這些系統的方式:從請求 / 響應互動轉向釋出 / 訂閱資料流【27】。更具響應性的使用者介面與更好的離線支援我認為這些優勢值得我們付出努力。如果你正在設計資料系統我希望你對訂閱變更的選項留有印象而不只是查詢當前狀態。 為了將寫路徑延伸至終端使用者,我們需要從根本上重新思考我們構建這些系統的方式:從請求 / 響應互動轉向釋出 / 訂閱資料流【27】。更具響應性的使用者介面與更好的離線支援我認為這些優勢值得我們付出努力。如果你正在設計資料系統我希望你對訂閱變更的選項留有印象而不只是查詢當前狀態。
@ -385,11 +387,11 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni
我們討論過,當流處理器將衍生資料寫入儲存(資料庫,快取或索引)時,以及當用戶請求查詢該儲存時,儲存將充當寫路徑和讀路徑之間的邊界。該儲存應當允許對資料進行隨機訪問的讀取查詢,否則這些查詢將需要掃描整個事件日誌。 我們討論過,當流處理器將衍生資料寫入儲存(資料庫,快取或索引)時,以及當用戶請求查詢該儲存時,儲存將充當寫路徑和讀路徑之間的邊界。該儲存應當允許對資料進行隨機訪問的讀取查詢,否則這些查詢將需要掃描整個事件日誌。
在很多情況下,資料儲存與流處理系統是分開的。但回想一下,流處理器還是需要維護狀態以執行聚合和連線的(請參閱 “[流連線](ch11.md#流連線)”。這種狀態通常隱藏在流處理器內部但一些框架也允許這些狀態被外部客戶端查詢【45】將流處理器本身變成一種簡單的資料庫。 在很多情況下,資料儲存與流處理系統是分開的。但回想一下,流處理器還是需要維護狀態以執行聚合和連線的(請參閱 “[流連線](/tw/ch11#流連線)”。這種狀態通常隱藏在流處理器內部但一些框架也允許這些狀態被外部客戶端查詢【45】將流處理器本身變成一種簡單的資料庫。
我願意進一步思考這個想法。正如到目前為止所討論的那樣對儲存的寫入是透過事件日誌進行的而讀取是臨時的網路請求直接流向儲存著待查資料的節點。這是一個合理的設計但不是唯一可行的設計。也可以將讀取請求表示為事件流並同時將讀事件與寫事件送往流處理器流處理器透過將讀取結果傳送到輸出流來響應讀取事件【46】。 我願意進一步思考這個想法。正如到目前為止所討論的那樣對儲存的寫入是透過事件日誌進行的而讀取是臨時的網路請求直接流向儲存著待查資料的節點。這是一個合理的設計但不是唯一可行的設計。也可以將讀取請求表示為事件流並同時將讀事件與寫事件送往流處理器流處理器透過將讀取結果傳送到輸出流來響應讀取事件【46】。
當寫入和讀取都被表示為事件,並且被路由到同一個流運算元以便處理時,我們實際上是在讀取查詢流和資料庫之間執行流表連線。讀取事件需要被送往儲存資料的資料庫分割槽(請參閱 “[請求路由](ch6.md#請求路由)”),就像批處理和流處理器在連線時需要在同一個鍵上對輸入分割槽一樣(請參閱 “[Reduce 側連線與分組](ch10.md#Reduce側連線與分組)”)。 當寫入和讀取都被表示為事件,並且被路由到同一個流運算元以便處理時,我們實際上是在讀取查詢流和資料庫之間執行流表連線。讀取事件需要被送往儲存資料的資料庫分割槽(請參閱 “[請求路由](/tw/ch6#請求路由)”),就像批處理和流處理器在連線時需要在同一個鍵上對輸入分割槽一樣(請參閱 “[Reduce 側連線與分組](/tw/ch10#Reduce側連線與分組)”)。
服務請求與執行連線之間的這種相似之處是非常關鍵的【47】。一次性讀取請求只是將請求傳過連線運算元然後請求馬上就被忘掉了而一個訂閱請求則是與連線另一側過去與未來事件的持久化連線。 服務請求與執行連線之間的這種相似之處是非常關鍵的【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】。 這種模式的另一個例子是欺詐預防:為了評估特定購買事件是否具有欺詐風險,你可以檢查該使用者 IP 地址電子郵件地址帳單地址送貨地址的信用分。這些信用資料庫中的每一個都是有分割槽的因此為特定購買事件採集分數需要連線一系列不同的分割槽資料集【49】。
MPP 資料庫的內部查詢執行圖有著類似的特徵(請參閱 “[Hadoop 與分散式資料庫的對比](ch10.md#Hadoop與分散式資料庫的對比)”)。如果需要執行這種多分割槽連線,則直接使用提供此功能的資料庫,可能要比使用流處理器實現它要更簡單。然而將查詢視為流提供了一種選項,可以用於實現超出傳統現成解決方案的大規模應用。 MPP 資料庫的內部查詢執行圖有著類似的特徵(請參閱 “[Hadoop 與分散式資料庫的對比](/tw/ch10#Hadoop與分散式資料庫的對比)”)。如果需要執行這種多分割槽連線,則直接使用提供此功能的資料庫,可能要比使用流處理器實現它要更簡單。然而將查詢視為流提供了一種選項,可以用於實現超出傳統現成解決方案的大規模應用。
## 將事情做正確 ## 將事情做正確
對於只讀取資料的無狀態服務出問題也沒什麼大不了的你可以修復該錯誤並重啟服務而一切都恢復正常。像資料庫這樣的有狀態系統就沒那麼簡單了它們被設計為永遠記住事物或多或少所以如果出現問題這種錯誤的效果也將潛在地永遠持續下去這意味著它們需要更仔細的思考【50】。 對於只讀取資料的無狀態服務出問題也沒什麼大不了的你可以修復該錯誤並重啟服務而一切都恢復正常。像資料庫這樣的有狀態系統就沒那麼簡單了它們被設計為永遠記住事物或多或少所以如果出現問題這種錯誤的效果也將潛在地永遠持續下去這意味著它們需要更仔細的思考【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】。通常簡單的解決方案似乎在低併發性的情況下工作正常並且沒有錯誤但在要求更高的情況下卻會出現許多微妙的錯誤。 對於如此重要的話題我們的理解以及我們的工程方法卻是驚人地薄弱。例如確定在特定事務隔離等級或複製配置下執行特定應用是否安全是非常困難的【51,52】。通常簡單的解決方案似乎在低併發性的情況下工作正常並且沒有錯誤但在要求更高的情況下卻會出現許多微妙的錯誤。
@ -428,23 +430,23 @@ MPP 資料庫的內部查詢執行圖有著類似的特徵(請參閱 “[Hadoo
僅僅因為一個應用程式使用了具有相對較強安全屬性的資料系統(例如可序列化的事務),並不意味著就可以保證沒有資料丟失或損壞。例如,如果某個應用有個 Bug導致它寫入不正確的資料或者從資料庫中刪除資料那麼可序列化的事務也救不了你。 僅僅因為一個應用程式使用了具有相對較強安全屬性的資料系統(例如可序列化的事務),並不意味著就可以保證沒有資料丟失或損壞。例如,如果某個應用有個 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 協議棧會重新傳輸任何丟失的資料包,也會移除任何重複的資料包。
但是,這種重複抑制僅適用於單條 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 資金從一個賬戶到另一個賬戶的非冪等轉移** **例 12-1 資金從一個賬戶到另一個賬戶的非冪等轉移**
@ -457,7 +459,7 @@ COMMIT;
客戶端可以重連到資料庫並重試事務,但現在已經處於 TCP 重複抑制的範圍之外了。因為 [例 12-1]() 中的事務不是冪等的,可能會發生轉了 \$22 而不是期望的 \$11。因此儘管 [例 12-1]() 是一個事務原子性的標準樣例但它實際上並不正確而真正的銀行並不會這樣辦事【3】。 客戶端可以重連到資料庫並重試事務,但現在已經處於 TCP 重複抑制的範圍之外了。因為 [例 12-1]() 中的事務不是冪等的,可能會發生轉了 \$22 而不是期望的 \$11。因此儘管 [例 12-1]() 是一個事務原子性的標準樣例但它實際上並不正確而真正的銀行並不會這樣辦事【3】。
兩階段提交(請參閱 “[原子提交與兩階段提交](ch9.md#原子提交與兩階段提交)”)協議會破壞 TCP 連線與事務之間的 1:1 對映,因為它們必須在故障後允許事務協調器重連到資料庫,告訴資料庫將存疑事務提交還是中止。這足以確保事務只被恰好執行一次嗎?不幸的是,並不能。 兩階段提交(請參閱 “[原子提交與兩階段提交](/tw/ch9#原子提交與兩階段提交)”)協議會破壞 TCP 連線與事務之間的 1:1 對映,因為它們必須在故障後允許事務協調器重連到資料庫,告訴資料庫將存疑事務提交還是中止。這足以確保事務只被恰好執行一次嗎?不幸的是,並不能。
即使我們可以抑制資料庫客戶端與伺服器之間的重複事務,我們仍然需要擔心終端使用者裝置與應用伺服器之間的網路。例如,如果終端使用者的客戶端是 Web 瀏覽器,則它可能會使用 HTTP POST 請求向伺服器提交指令。也許使用者正處於一個訊號微弱的蜂窩資料網路連線中,它們成功地傳送了 POST但卻在能夠從伺服器接收響應之前沒了訊號。 即使我們可以抑制資料庫客戶端與伺服器之間的重複事務,我們仍然需要擔心終端使用者裝置與應用伺服器之間的網路。例如,如果終端使用者的客戶端是 Web 瀏覽器,則它可能會使用 HTTP POST 請求向伺服器提交指令。也許使用者正處於一個訊號微弱的蜂窩資料網路連線中,它們成功地傳送了 POST但卻在能夠從伺服器接收響應之前沒了訊號。
@ -482,9 +484,9 @@ BEGIN TRANSACTION;
COMMIT; 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 中的那些)執行的相當好,因而剩下的高層級錯誤基本很少出現。如果能將這些剩下的高層級容錯機制打包成抽象,而應用不需要再去操心,那該多好呀 —— 但恐怕我們還沒有找到這一正確的抽象。 這實在是一個遺憾,因為容錯機制很難弄好。低層級的可靠機制(比如 TCP 中的那些)執行的相當好,因而剩下的高層級錯誤基本很少出現。如果能將這些剩下的高層級容錯機制打包成抽象,而應用不需要再去操心,那該多好呀 —— 但恐怕我們還沒有找到這一正確的抽象。
長期以來,事務被認為是一個很好的抽象,我相信它們確實是很有用的。正如 [第七章](ch7.md) 導言中所討論的,它們將各種可能的問題(併發寫入、違背約束、崩潰、網路中斷、磁碟故障)合併為兩種可能結果:提交或中止。這是對程式設計模型而言是一種巨大的簡化,但恐怕這還不夠。 長期以來,事務被認為是一個很好的抽象,我相信它們確實是很有用的。正如 [第七章](/tw/ch7) 導言中所討論的,它們將各種可能的問題(併發寫入、違背約束、崩潰、網路中斷、磁碟故障)合併為兩種可能結果:提交或中止。這是對程式設計模型而言是一種巨大的簡化,但恐怕這還不夠。
事務是代價高昂的,當涉及異構儲存技術時尤為甚(請參閱 “[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”)。我們拒絕使用分散式事務是因為它開銷太大,結果我們最後不得不在應用程式碼中重新實現容錯機制。正如本書中大量的例子所示,對併發性與部分失敗的推理是困難且違反直覺的,所以我懷疑大多數應用級別的機制都不能正確工作,最終結果是資料丟失或損壞。 事務是代價高昂的,當涉及異構儲存技術時尤為甚(請參閱 “[實踐中的分散式事務](/tw/ch9#實踐中的分散式事務)”)。我們拒絕使用分散式事務是因為它開銷太大,結果我們最後不得不在應用程式碼中重新實現容錯機制。正如本書中大量的例子所示,對併發性與部分失敗的推理是困難且違反直覺的,所以我懷疑大多數應用級別的機制都不能正確工作,最終結果是資料丟失或損壞。
出於這些原因,我認為探索對容錯的抽象是很有價值的。它使提供應用特定的端到端的正確性屬性變得更簡單,而且還能在大規模分散式環境中提供良好的效能與運維特性。 出於這些原因,我認為探索對容錯的抽象是很有價值的。它使提供應用特定的端到端的正確性屬性變得更簡單,而且還能在大規模分散式環境中提供良好的效能與運維特性。
@ -517,33 +519,33 @@ COMMIT;
讓我們思考一下在 [分拆資料庫](#分拆資料庫) 上下文中的 **正確性correctness**。我們看到端到端的除重可以透過從客戶端一路透傳到資料庫的請求 ID 實現。那麼其他型別的約束呢? 讓我們思考一下在 [分拆資料庫](#分拆資料庫) 上下文中的 **正確性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. 每個對使用者名稱的請求都被編碼為一條訊息,並追加到按使用者名稱雜湊值確定的分割槽。 1. 每個對使用者名稱的請求都被編碼為一條訊息,並追加到按使用者名稱雜湊值確定的分割槽。
2. 流處理器依序讀取日誌中的請求,並使用本地資料庫來追蹤哪些使用者名稱已經被佔用了。對於所有申請可用使用者名稱的請求,它都會記錄該使用者名稱,並向輸出流傳送一條成功訊息。對於所有申請已佔用使用者名稱的請求,它都會向輸出流傳送一條拒絕訊息。 2. 流處理器依序讀取日誌中的請求,並使用本地資料庫來追蹤哪些使用者名稱已經被佔用了。對於所有申請可用使用者名稱的請求,它都會記錄該使用者名稱,並向輸出流傳送一條成功訊息。對於所有申請已佔用使用者名稱的請求,它都會向輸出流傳送一條拒絕訊息。
3. 請求使用者名稱的客戶端監視輸出流,等待與其請求相對應的成功或拒絕訊息。 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。 2. 流處理器讀取請求日誌。對於每個請求訊息,它向輸出流發出兩條訊息:付款人賬戶 A 的借記指令(按 A 分割槽),收款人 B 的貸記指令(按 B 分割槽)。被發出的訊息中會帶有原始的請求 ID。
3. 後續處理器消費借記 / 貸記指令流,按照請求 ID 除重,並將變更應用至賬戶餘額。 3. 後續處理器消費借記 / 貸記指令流,按照請求 ID 除重,並將變更應用至賬戶餘額。
步驟 1 和步驟 2 是必要的,因為如果客戶直接傳送貸記與借記指令,則需要在這兩個分割槽之間進行原子提交,以確保兩者要麼都發生或都不發生。為了避免對分散式事務的需要,我們首先將請求持久化記錄為單條訊息,然後從這第一條訊息中衍生出貸記指令與借記指令。幾乎在所有資料系統中,單物件寫入都是原子性的(請參閱 “[單物件寫入](ch7.md#單物件寫入)),因此請求要麼出現在日誌中,要麼就不出現,無需多分割槽原子提交。 步驟 1 和步驟 2 是必要的,因為如果客戶直接傳送貸記與借記指令,則需要在這兩個分割槽之間進行原子提交,以確保兩者要麼都發生或都不發生。為了避免對分散式事務的需要,我們首先將請求持久化記錄為單條訊息,然後從這第一條訊息中衍生出貸記指令與借記指令。幾乎在所有資料系統中,單物件寫入都是原子性的(請參閱 “[單物件寫入](/tw/ch7#單物件寫入)),因此請求要麼出現在日誌中,要麼就不出現,無需多分割槽原子提交。
如果流處理器在步驟 2 中崩潰,則它會從上一個存檔點恢復處理。這樣做時,它不會跳過任何請求訊息,但可能會多次處理請求併產生重複的貸記與借記指令。但由於它是確定性的,因此它只是再次生成相同的指令,而步驟 3 中的處理器可以使用端到端請求 ID 輕鬆地對其除重。 如果流處理器在步驟 2 中崩潰,則它會從上一個存檔點恢復處理。這樣做時,它不會跳過任何請求訊息,但可能會多次處理請求併產生重複的貸記與借記指令。但由於它是確定性的,因此它只是再次生成相同的指令,而步驟 3 中的處理器可以使用端到端請求 ID 輕鬆地對其除重。
如果你想確保付款人的帳戶不會因此次轉賬而透支,則可以使用一個額外的流處理器來維護賬戶餘額並校驗事務(按付款人賬戶分割槽),只有有效的事務會被記錄在步驟 1 中的請求日誌中。 如果你想確保付款人的帳戶不會因此次轉賬而透支,則可以使用一個額外的流處理器來維護賬戶餘額並校驗事務(按付款人賬戶分割槽),只有有效的事務會被記錄在步驟 1 中的請求日誌中。
透過將多分割槽事務分解為兩個不同分割槽方式的階段,並使用端到端的請求 ID我們實現了同樣的正確性屬性每個請求對付款人與收款人都恰好生效一次即使在出現故障且沒有使用原子提交協議的情況下依然如此。使用多個不同分割槽階段的想法與我們在 “[多分割槽資料處理](#多分割槽資料處理)” 中討論的想法類似(也請參閱 “[併發控制](ch11.md#併發控制)”)。 透過將多分割槽事務分解為兩個不同分割槽方式的階段,並使用端到端的請求 ID我們實現了同樣的正確性屬性每個請求對付款人與收款人都恰好生效一次即使在出現故障且沒有使用原子提交協議的情況下依然如此。使用多個不同分割槽階段的想法與我們在 “[多分割槽資料處理](#多分割槽資料處理)” 中討論的想法類似(也請參閱 “[併發控制](/tw/ch11#併發控制)”)。
### 及時性與完整性 ### 及時性與完整性
事務的一個便利屬性是,它們通常是線性一致的(請參閱 “[線性一致性](ch9.md#線性一致性)”),也就是說,寫入者會等到事務提交,而之後其寫入立刻對所有讀取者可見。 事務的一個便利屬性是,它們通常是線性一致的(請參閱 “[線性一致性](/tw/ch9#線性一致性)”),也就是說,寫入者會等到事務提交,而之後其寫入立刻對所有讀取者可見。
當我們把一個操作拆分為跨越多個階段的流處理器時,卻並非如此:日誌的消費者在設計上就是非同步的,因此傳送者不會等其訊息被消費者處理完。但是,客戶端等待輸出流中的特定訊息是可能的。這正是我們在 “[基於日誌訊息傳遞中的唯一性](#基於日誌訊息傳遞中的唯一性)” 一節中檢查唯一性約束時所做的事情。 當我們把一個操作拆分為跨越多個階段的流處理器時,卻並非如此:日誌的消費者在設計上就是非同步的,因此傳送者不會等其訊息被消費者處理完。但是,客戶端等待輸出流中的特定訊息是可能的。這正是我們在 “[基於日誌訊息傳遞中的唯一性](#基於日誌訊息傳遞中的唯一性)” 一節中檢查唯一性約束時所做的事情。
@ -577,15 +579,15 @@ COMMIT;
* 及時性Timeliness * 及時性Timeliness
及時性意味著確保使用者觀察到系統的最新狀態。我們之前看到,如果使用者從陳舊的資料副本中讀取資料,它們可能會觀察到系統處於不一致的狀態(請參閱 “[複製延遲問題](ch5.md#複製延遲問題)”)。但這種不一致是暫時的,而最終會透過等待與重試簡單地得到解決。 及時性意味著確保使用者觀察到系統的最新狀態。我們之前看到,如果使用者從陳舊的資料副本中讀取資料,它們可能會觀察到系統處於不一致的狀態(請參閱 “[複製延遲問題](/tw/ch5#複製延遲問題)”)。但這種不一致是暫時的,而最終會透過等待與重試簡單地得到解決。
CAP 定理(請參閱 “[線性一致性的代價](ch9.md#線性一致性的代價)”)使用 **線性一致性linearizability** 意義上的一致性,這是實現及時性的強有力方法。像 **寫後讀** 這樣及時性更弱的一致性也很有用(請參閱 “[讀己之寫](ch5.md#讀己之寫)”)。 CAP 定理(請參閱 “[線性一致性的代價](/tw/ch9#線性一致性的代價)”)使用 **線性一致性linearizability** 意義上的一致性,這是實現及時性的強有力方法。像 **寫後讀** 這樣及時性更弱的一致性也很有用(請參閱 “[讀己之寫](/tw/ch5#讀己之寫)”)。
* 完整性Integrity * 完整性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#事件溯源)”)。 * 將寫入操作的內容表示為單條訊息,從而可以輕鬆地被原子寫入 —— 與事件溯源搭配效果拔群(請參閱 “[事件溯源](/tw/ch11#事件溯源)”)。
* 使用與儲存過程類似的確定性衍生函式,從這一訊息中衍生出所有其他的狀態變更(請參閱 “[真的序列執行](ch7.md#真的序列執行)” 和 “[應用程式碼作為衍生函式](ch12.md#應用程式碼作為衍生函式)”) * 使用與儲存過程類似的確定性衍生函式,從這一訊息中衍生出所有其他的狀態變更(請參閱 “[真的序列執行](/tw/ch7#真的序列執行)” 和 “[應用程式碼作為衍生函式](/tw/ch12#應用程式碼作為衍生函式)”)
* 將客戶端生成的請求 ID 傳遞透過所有的處理層次,從而允許端到端的除重,帶來冪等性。 * 將客戶端生成的請求 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**)。一旦你仔細觀察,硬體並不是看上去那樣完美的抽象。 我過去所從事的一個應用收集了來自客戶端的崩潰報告我們收到的一些報告只有在這些裝置記憶體中出現了隨機位翻轉才解釋的通。這看起來不太可能但是如果有足夠多的裝置執行你的軟體那麼即使再不可能發生的事也確實會發生。除了由於硬體故障或輻射導致的隨機儲存器損壞之外一些病態的儲存器訪問模式甚至可以在沒有故障的儲存器中翻轉位【62】 —— 一種可用於破壞作業系統安全機制的效應【63】這種技術被稱為 **Rowhammer**)。一旦你仔細觀察,硬體並不是看上去那樣完美的抽象。
@ -663,13 +665,13 @@ ACID 事務通常既提供及時性(例如線性一致性)也提供完整性
而對於應用程式碼我們不得不假設會有更多的錯誤因為絕大多數應用的程式碼經受的評審與測試遠遠無法與資料庫的程式碼相比。許多應用甚至沒有正確使用資料庫提供的用於維持完整性的功能例如外部索引鍵或唯一性約束【36】。 而對於應用程式碼我們不得不假設會有更多的錯誤因為絕大多數應用的程式碼經受的評審與測試遠遠無法與資料庫的程式碼相比。許多應用甚至沒有正確使用資料庫提供的用於維持完整性的功能例如外部索引鍵或唯一性約束【36】。
ACID 意義下的一致性(請參閱 “[一致性](ch7.md#一致性)”)基於這樣一種想法:資料庫以一致的狀態啟動,而事務將其從一個一致狀態轉換至另一個一致的狀態。因此,我們期望資料庫始終處於一致狀態。然而,只有當你假設事務沒有 Bug 時,這種想法才有意義。如果應用以某種錯誤的方式使用資料庫,例如,不安全地使用弱隔離等級,資料庫的完整性就無法得到保證。 ACID 意義下的一致性(請參閱 “[一致性](/tw/ch7#一致性)”)基於這樣一種想法:資料庫以一致的狀態啟動,而事務將其從一個一致狀態轉換至另一個一致的狀態。因此,我們期望資料庫始終處於一致狀態。然而,只有當你假設事務沒有 Bug 時,這種想法才有意義。如果應用以某種錯誤的方式使用資料庫,例如,不安全地使用弱隔離等級,資料庫的完整性就無法得到保證。
#### 不要盲目信任承諾 #### 不要盲目信任承諾
由於硬體和軟體並不總是符合我們的理想,所以資料損壞似乎早晚不可避免。因此,我們至少應該有辦法查明資料是否已經損壞,以便我們能夠修復它,並嘗試追查錯誤的來源。檢查資料完整性稱為 **審計auditing** 由於硬體和軟體並不總是符合我們的理想,所以資料損壞似乎早晚不可避免。因此,我們至少應該有辦法查明資料是否已經損壞,以便我們能夠修復它,並嘗試追查錯誤的來源。檢查資料完整性稱為 **審計auditing**
如 “[不可變事件的優點](ch11.md#不可變事件的優點)” 一節中所述,審計不僅僅適用於財務應用程式。不過,可審計性在財務中是非常非常重要的,因為每個人都知道錯誤總會發生,我們也都認為能夠檢測和解決問題是合理的需求。 如 “[不可變事件的優點](/tw/ch11#不可變事件的優點)” 一節中所述,審計不僅僅適用於財務應用程式。不過,可審計性在財務中是非常非常重要的,因為每個人都知道錯誤總會發生,我們也都認為能夠檢測和解決問題是合理的需求。
成熟的系統同樣傾向於考慮不太可能的事情出錯的可能性並管理這種風險。例如HDFS 和 Amazon S3 等大規模儲存系統並不完全信任磁碟它們執行後臺程序持續回讀檔案並將其與其他副本進行比較並將檔案從一個磁碟移動到另一個以便降低靜默損壞的風險【67】。 成熟的系統同樣傾向於考慮不太可能的事情出錯的可能性並管理這種風險。例如HDFS 和 Amazon S3 等大規模儲存系統並不完全信任磁碟它們執行後臺程序持續回讀檔案並將其與其他副本進行比較並將檔案從一個磁碟移動到另一個以便降低靜默損壞的風險【67】。
@ -685,11 +687,11 @@ ACID 意義下的一致性(請參閱 “[一致性](ch7.md#一致性)”)基
#### 為可審計性而設計 #### 為可審計性而設計
如果一個事務在一個數據庫中改變了多個物件,在這一事實發生後,很難說清這個事務到底意味著什麼。即使你捕獲了事務日誌(請參閱 “[變更資料捕獲](ch11.md#變更資料捕獲)”),各種表中的插入、更新和刪除操作並不一定能清楚地表明 **為什麼** 要執行這些變更。決定這些變更的是應用邏輯中的呼叫,而這一應用邏輯稍縱即逝,無法重現。 如果一個事務在一個數據庫中改變了多個物件,在這一事實發生後,很難說清這個事務到底意味著什麼。即使你捕獲了事務日誌(請參閱 “[變更資料捕獲](/tw/ch11#變更資料捕獲)”),各種表中的插入、更新和刪除操作並不一定能清楚地表明 **為什麼** 要執行這些變更。決定這些變更的是應用邏輯中的呼叫,而這一應用邏輯稍縱即逝,無法重現。
相比之下,基於事件的系統可以提供更好的可審計性。在事件溯源方法中,系統的使用者輸入被表示為一個單一不可變事件,而任何其導致的狀態變更都衍生自該事件。衍生可以實現為具有確定性與可重複性,因而相同的事件日誌透過相同版本的衍生程式碼時,會導致相同的狀態變更。 相比之下,基於事件的系統可以提供更好的可審計性。在事件溯源方法中,系統的使用者輸入被表示為一個單一不可變事件,而任何其導致的狀態變更都衍生自該事件。衍生可以實現為具有確定性與可重複性,因而相同的事件日誌透過相同版本的衍生程式碼時,會導致相同的狀態變更。
顯式處理資料流(請參閱 “[批處理輸出的哲學](ch10.md#批處理輸出的哲學)”)可以使資料的 **來龍去脈provenance** 更加清晰,從而使完整性檢查更具可行性。對於事件日誌,我們可以使用雜湊來檢查事件儲存沒有被破壞。對於任何衍生狀態,我們可以重新執行從事件日誌中衍生它的批處理器與流處理器,以檢查是否獲得相同的結果,或者,甚至並行執行冗餘的衍生流程。 顯式處理資料流(請參閱 “[批處理輸出的哲學](/tw/ch10#批處理輸出的哲學)”)可以使資料的 **來龍去脈provenance** 更加清晰,從而使完整性檢查更具可行性。對於事件日誌,我們可以使用雜湊來檢查事件儲存沒有被破壞。對於任何衍生狀態,我們可以重新執行從事件日誌中衍生它的批處理器與流處理器,以檢查是否獲得相同的結果,或者,甚至並行執行冗餘的衍生流程。
具有確定性且定義良好的資料流,也使除錯與跟蹤系統的執行變得容易,以便確定它 **為什麼** 做了某些事情【4,69】。如果出現意想之外的事情那麼重現導致意外事件的確切事故現場的診斷能力 —— 一種時間旅行除錯功能是非常有價值的。 具有確定性且定義良好的資料流,也使除錯與跟蹤系統的執行變得容易,以便確定它 **為什麼** 做了某些事情【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】。 密碼學審計與完整性檢查通常依賴 **默克爾樹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. 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. 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. 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. 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 > —— 路德維奇・維特根斯坦《邏輯哲學》1922
>
-------------------
[TOC]
資料模型可能是軟體開發中最重要的部分了,因為它們的影響如此深遠:不僅僅影響著軟體的編寫方式,而且影響著我們的 **解題思路** 資料模型可能是軟體開發中最重要的部分了,因為它們的影響如此深遠:不僅僅影響著軟體的編寫方式,而且影響著我們的 **解題思路**
@ -26,7 +28,7 @@
掌握一個數據模型需要花費很多精力(想想關係資料建模有多少本書)。即便只使用一個數據模型,不用操心其內部工作機制,構建軟體也是非常困難的。然而,因為資料模型對上層軟體的功能(能做什麼,不能做什麼)有著至深的影響,所以選擇一個適合的資料模型是非常重要的。 掌握一個數據模型需要花費很多精力(想想關係資料建模有多少本書)。即便只使用一個數據模型,不用操心其內部工作機制,構建軟體也是非常困難的。然而,因為資料模型對上層軟體的功能(能做什麼,不能做什麼)有著至深的影響,所以選擇一個適合的資料模型是非常重要的。
在本章中,我們將研究一系列用於資料儲存和查詢的通用資料模型(前面列表中的第 2 點)。特別地,我們將比較關係模型,文件模型和少量基於圖形的資料模型。我們還將檢視各種查詢語言並比較它們的用例。在 [第三章](ch3.md) 中,我們將討論儲存引擎是如何工作的。也就是說,這些資料模型實際上是如何實現的(列表中的第 3 點)。 在本章中,我們將研究一系列用於資料儲存和查詢的通用資料模型(前面列表中的第 2 點)。特別地,我們將比較關係模型,文件模型和少量基於圖形的資料模型。我們還將檢視各種查詢語言並比較它們的用例。在 [第三章](/tw/ch3) 中,我們將討論儲存引擎是如何工作的。也就是說,這些資料模型實際上是如何實現的(列表中的第 3 點)。
## 關係模型與文件模型 ## 關係模型與文件模型
@ -64,13 +66,13 @@
像 ActiveRecord 和 Hibernate 這樣的 **物件關係對映ORM object-relational mapping** 框架可以減少這個轉換層所需的樣板程式碼的數量,但是它們不能完全隱藏這兩個模型之間的差異。 像 ActiveRecord 和 Hibernate 這樣的 **物件關係對映ORM object-relational mapping** 框架可以減少這個轉換層所需的樣板程式碼的數量,但是它們不能完全隱藏這兩個模型之間的差異。
![](../img/fig2-1.png) ![](/img/fig2-1.png)
**圖 2-1 使用關係型模式來表示領英簡介** **圖 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】。 * 後續的 SQL 標準增加了對結構化資料型別和 XML 資料的支援;這允許將多值資料儲存在單行內,並支援在這些文件內查詢和索引。這些功能在 OracleIBM DB2MS SQL Server 和 PostgreSQL 中都有不同程度的支援【6,7】。JSON 資料型別也得到多個數據庫的支援,包括 IBM DB2MySQL 和 PostgreSQL 【8】。
* 第三種選擇是將職業,教育和聯絡資訊編碼為 JSON 或 XML 文件,將其儲存在資料庫的文字列中,並讓應用程式解析其結構和內容。這種配置下,通常不能使用資料庫來查詢該編碼列中的值。 * 第三種選擇是將職業,教育和聯絡資訊編碼為 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 一對多關係構建了一個樹結構** **圖 2-2 一對多關係構建了一個樹結構**
@ -144,7 +146,7 @@ JSON 表示比 [圖 2-1](../img/fig2-1.png) 中的多表模式具有更好的 **
[^ii]: 關於關係模型的文獻區分了幾種不同的規範形式,但這些區別幾乎沒有實際意義。一個經驗法則是,如果重複儲存了可以儲存在一個地方的值,則模式就不是 **規範化normalized** 的。 [^ii]: 關於關係模型的文獻區分了幾種不同的規範形式,但這些區別幾乎沒有實際意義。一個經驗法則是,如果重複儲存了可以儲存在一個地方的值,則模式就不是 **規範化normalized** 的。
> 資料庫管理員和開發人員喜歡爭論規範化和非規範化,讓我們暫時保留判斷吧。在本書的 [第三部分](part-iii.md),我們將回到這個話題,探討系統的方法用以處理快取,非規範化和衍生資料。 > 資料庫管理員和開發人員喜歡爭論規範化和非規範化,讓我們暫時保留判斷吧。在本書的 [第三部分](/tw/part-iii),我們將回到這個話題,探討系統的方法用以處理快取,非規範化和衍生資料。
不幸的是,對這些資料進行規範化需要多對一的關係(許多人生活在一個特定的地區,許多人在一個特定的行業工作),這與文件模型不太吻合。在關係資料庫中,透過 ID 來引用其他表中的行是正常的,因為連線很容易。在文件資料庫中,一對多樹結構沒有必要用連線,對連線的支援通常很弱 [^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-3 公司名不僅是字串還是一個指向公司實體的連結LinkedIn 截圖)**
[圖 2-4](../img/fig2-4.png) 闡明瞭這些新功能需要如何使用多對多關係。每個虛線矩形內的資料可以分組成一個文件,但是對單位,學校和其他使用者的引用需要表示成引用,並且在查詢時需要連線。 [圖 2-4](/img/fig2-4.png) 闡明瞭這些新功能需要如何使用多對多關係。每個虛線矩形內的資料可以分組成一個文件,但是對單位,學校和其他使用者的引用需要表示成引用,並且在查詢時需要連線。
![](../img/fig2-4.png) ![](/img/fig2-4.png)
**圖 2-4 使用多對多關係擴充套件簡歷** **圖 2-4 使用多對多關係擴充套件簡歷**
@ -178,7 +180,7 @@ JSON 表示比 [圖 2-1](../img/fig2-1.png) 中的多表模式具有更好的 **
20 世紀 70 年代最受歡迎的業務資料處理資料庫是 IBM 的資訊管理系統IMS最初是為了阿波羅太空計劃的庫存管理而開發的並於 1968 年有了首次商業釋出【13】。目前它仍在使用和維護執行在 IBM 大型機的 OS/390 上【14】。 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】。 同文檔資料庫一樣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 的老路。 但是,在表示多對一和多對多的關係時,關係資料庫和文件資料庫並沒有根本的不同:在這兩種情況下,相關專案都被一個唯一的識別符號引用,這個識別符號在關係模型中被稱為 **外部索引鍵**,在文件模型中稱為 **文件引用**【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 的位置列表中的第二項”(很像層次模型中的訪問路徑)。但是,只要檔案巢狀不太深,這通常不是問題。 文件模型有一定的侷限性:例如,不能直接引用文件中的巢狀的專案,而是需要說 “使用者 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】。這些效能限制大大減少了文件資料庫的實用場景。 區域性僅僅適用於同時需要文件絕大部分內容的情況。即使只訪問文件其中的一小部分資料庫通常需要載入整個文件對於大型文件來說這種載入行為是很浪費的。更新文件時通常需要整個重寫。只有不改變文件大小的修改才可以容易地原地執行。因此通常建議保持相對小的文件並避免增加文件大小的寫入【9】。這些效能限制大大減少了文件資料庫的實用場景。
值得指出的是為了區域性而分組集合相關資料的想法並不侷限於文件模型。例如Google 的 Spanner 資料庫在關係資料模型中提供了同樣的區域性屬性允許模式宣告一個表的行應該交錯巢狀在父表內【27】。Oracle 類似地允許使用一個稱為 **多表索引叢集表multi-table index cluster tables** 的類似特性【28】。Bigtable 資料模型(用於 Cassandra 和 HBase中的 **列族column-family** 概念與管理區域性的目的類似【29】。 值得指出的是為了區域性而分組集合相關資料的想法並不侷限於文件模型。例如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 是一個由 Google 推廣的程式設計模型用於在多臺機器上批次處理大規模的資料【33】。一些 NoSQL 資料儲存(包括 MongoDB 和 CouchDB支援有限形式的 MapReduce作為在多個文件中執行只讀查詢的機制。
關於 MapReduce 更詳細的介紹在 [第十章](ch10.md)。現在我們只簡要討論一下 MongoDB 使用的模型。 關於 MapReduce 更詳細的介紹在 [第十章](/tw/ch10)。現在我們只簡要討論一下 MongoDB 使用的模型。
MapReduce 既不是一個宣告式的查詢語言,也不是一個完全命令式的查詢 API而是處於兩者之間查詢的邏輯用程式碼片段來表示這些程式碼片段會被處理框架重複性呼叫。它基於 `map`(也稱為 `collect`)和 `reduce`(也稱為 `fold``inject`)函式,兩個函式存在於許多函數語言程式設計語言中。 MapReduce 既不是一個宣告式的查詢語言,也不是一個完全命令式的查詢 API而是處於兩者之間查詢的邏輯用程式碼片段來表示這些程式碼片段會被處理框架重複性呼叫。它基於 `map`(也稱為 `collect`)和 `reduce`(也稱為 `fold``inject`)函式,兩個函式存在於許多函數語言程式設計語言中。
@ -488,7 +490,7 @@ db.observations.mapReduce(function map() {
map 和 reduce 函式在功能上有所限制:它們必須是 **純** 函式這意味著它們只使用傳遞給它們的資料作為輸入它們不能執行額外的資料庫查詢也不能有任何副作用。這些限制允許資料庫以任何順序執行任何功能並在失敗時重新執行它們。然而map 和 reduce 函式仍然是強大的:它們可以解析字串、呼叫庫函式、執行計算等等。 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】。 能夠在查詢中使用 JavaScript 程式碼是高階查詢的一個重要特性,但這不限於 MapReduce一些 SQL 資料庫也可以用 JavaScript 函式進行擴充套件【34】。
@ -533,13 +535,13 @@ db.observations.aggregate([
在剛剛給出的例子中圖中的所有頂點代表了相同型別的事物人、網頁或交叉路口。不過圖並不侷限於這樣的同類資料同樣強大地是圖提供了一種一致的方式用來在單個數據儲存中儲存完全不同型別的物件。例如Facebook 維護一個包含許多不同型別的頂點和邊的單個圖頂點表示人、地點、事件、簽到和使用者的評論邊表示哪些人是好友、簽到發生在哪裡、誰評論了什麼帖子、誰參與了什麼事件等等【35】。 在剛剛給出的例子中圖中的所有頂點代表了相同型別的事物人、網頁或交叉路口。不過圖並不侷限於這樣的同類資料同樣強大地是圖提供了一種一致的方式用來在單個數據儲存中儲存完全不同型別的物件。例如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 圖資料結構示例(框代表頂點,箭頭代表邊)** **圖 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` 列上都有索引的原因)。 2. 給定任何頂點,可以高效地找到它的入邊和出邊,從而遍歷圖,即沿著一系列頂點的路徑前後移動(這就是為什麼 [例 2-2]() 在 `tail_vertex``head_vertex` 列上都有索引的原因)。
3. 透過對不同型別的關係使用不同的標籤,可以在一個圖中儲存幾種不同的資訊,同時仍然保持一個清晰的資料模型。 3. 透過對不同型別的關係使用不同的標籤,可以在一個圖中儲存幾種不同的資訊,同時仍然保持一個清晰的資料模型。
這些特性為資料建模提供了很大的靈活性,如 [圖 2-5](../img/fig2-5.png) 所示。圖中顯示了一些傳統關係模式難以表達的事情例如不同國家的不同地區結構法國有省和大區美國有縣和州國中國的怪事先忽略主權國家和民族錯綜複雜的爛攤子不同的資料粒度Lucy 現在的住所記錄具體到城市,而她的出生地點只是在一個州的級別)。 這些特性為資料建模提供了很大的靈活性,如 [圖 2-5](/img/fig2-5.png) 所示。圖中顯示了一些傳統關係模式難以表達的事情例如不同國家的不同地區結構法國有省和大區美國有縣和州國中國的怪事先忽略主權國家和民族錯綜複雜的爛攤子不同的資料粒度Lucy 現在的住所記錄具體到城市,而她的出生地點只是在一個州的級別)。
你可以想象該圖還能延伸出許多關於 Lucy 和 Alain 的事實,或其他人的其他更多的事實。例如,你可以用它來表示食物過敏(為每個過敏源增加一個頂點,並增加人與過敏源之間的一條邊來指示一種過敏情況),並連結到過敏源,每個過敏源具有一組頂點用來顯示哪些食物含有哪些物質。然後,你可以寫一個查詢,找出每個人吃什麼是安全的。圖在可演化性方面是富有優勢的:當你嚮應用程式新增功能時,可以輕鬆擴充套件圖以適應程式資料結構的變化。 你可以想象該圖還能延伸出許多關於 Lucy 和 Alain 的事實,或其他人的其他更多的事實。例如,你可以用它來表示食物過敏(為每個過敏源增加一個頂點,並增加人與過敏源之間的一條邊來指示一種過敏情況),並連結到過敏源,每個過敏源具有一組頂點用來顯示哪些食物含有哪些物質。然後,你可以寫一個查詢,找出每個人吃什麼是安全的。圖在可演化性方面是富有優勢的:當你嚮應用程式新增功能時,可以輕鬆擴充套件圖以適應程式資料結構的變化。
@ -594,7 +596,7 @@ CREATE INDEX edges_heads ON edges (head_vertex);
Cypher 是屬性圖的宣告式查詢語言,為 Neo4j 圖形資料庫而發明【37】它是以電影 “駭客帝國” 中的一個角色來命名的而與密碼學中的加密演算法無關【38】 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 查詢** **例 2-3 將圖 2-5 中的資料子集表示為 Cypher 查詢**
@ -608,7 +610,7 @@ CREATE
(Lucy) -[:BORN_IN]-> (Idaho) (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`,頭節點則未被繫結。 [例 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')` 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')` 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 規則來確定愛達荷州在北美。** **圖 2-6 使用示例 2-11 中的 Datalog 規則來確定愛達荷州在北美。**
@ -927,7 +929,7 @@ Cypher 和 SPARQL 使用 SELECT 立即跳轉,但是 Datalog 一次只進行一
* 粒子物理學家數十年來一直在進行大資料型別的大規模資料分析像大型強子對撞機LHC這樣的專案現在會處理數百 PB 的資料在這樣的規模下需要定製解決方案來阻止硬體成本的失控【49】。 * 粒子物理學家數十年來一直在進行大資料型別的大規模資料分析像大型強子對撞機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. 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. 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. 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. 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) 中,我們討論了資料模型和查詢語言,即程式設計師將資料錄入資料庫的格式,以及再次要回資料的機制。在本章中我們會從資料庫的視角來討論同樣的問題:資料庫如何儲存我們提供的資料,以及如何在我們需要時重新找到資料。 在 [第二章](/tw/ch2) 中,我們討論了資料模型和查詢語言,即程式設計師將資料錄入資料庫的格式,以及再次要回資料的機制。在本章中我們會從資料庫的視角來討論同樣的問題:資料庫如何儲存我們提供的資料,以及如何在我們需要時重新找到資料。
作為程式設計師,為什麼要關心資料庫內部儲存與檢索的機理?你可能不會去從頭開始實現自己的儲存引擎,但是你 **確實** 需要從許多可用的儲存引擎中選擇一個合適的。而且為了讓儲存引擎能在你的工作負載型別上執行良好,你也需要大致瞭解儲存引擎在底層究竟做了什麼。 作為程式設計師,為什麼要關心資料庫內部儲存與檢索的機理?你可能不會去從頭開始實現自己的儲存引擎,但是你 **確實** 需要從許多可用的儲存引擎中選擇一個合適的。而且為了讓儲存引擎能在你的工作負載型別上執行良好,你也需要大致瞭解儲存引擎在底層究竟做了什麼。
@ -82,9 +83,9 @@ $ cat database
鍵值儲存與在大多數程式語言中可以找到的 **字典dictionary** 型別非常相似,通常字典都是用 **雜湊對映hash map****散列表hash table** 實現的。雜湊對映在許多演算法教科書中都有描述【1,2】所以這裡我們不會討論它的工作細節。既然我們已經可以用雜湊對映來表示 **記憶體中** 的資料結構,為什麼不使用它來索引 **硬碟上** 的資料呢? 鍵值儲存與在大多數程式語言中可以找到的 **字典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 格式儲存鍵值對的日誌,並使用記憶體雜湊對映進行索引。** **圖 3-1 以類 CSV 格式儲存鍵值對的日誌,並使用記憶體雜湊對映進行索引。**
@ -92,15 +93,15 @@ $ cat database
像 Bitcask 這樣的儲存引擎非常適合每個鍵的值經常更新的情況。例如鍵可能是某個貓咪影片的網址URL而值可能是該影片被播放的次數每次有人點選播放按鈕時遞增。在這種型別的工作負載中有很多寫操作但是沒有太多不同的鍵 —— 每個鍵有很多的寫操作,但是將所有鍵儲存在記憶體中是可行的。 像 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-2 鍵值更新日誌(統計貓咪影片的播放次數)的壓縮,只保留每個鍵的最近值**
而且,由於壓縮經常會使得段變得很小(假設在一個段內鍵被平均重寫了好幾次),我們也可以在執行壓縮的同時將多個段合併在一起,如 [圖 3-3](../img/fig3-3.png) 所示。段被寫入後永遠不會被修改,所以合併的段被寫入一個新的檔案。凍結段的合併和壓縮可以在後臺執行緒中完成,這個過程進行的同時,我們仍然可以繼續使用舊的段檔案來正常提供讀寫請求。合併過程完成後,我們將讀取請求轉換為使用新合併的段而不是舊的段 —— 然後舊的段檔案就可以簡單地刪除掉了。 而且,由於壓縮經常會使得段變得很小(假設在一個段內鍵被平均重寫了好幾次),我們也可以在執行壓縮的同時將多個段合併在一起,如 [圖 3-3](/img/fig3-3.png) 所示。段被寫入後永遠不會被修改,所以合併的段被寫入一個新的檔案。凍結段的合併和壓縮可以在後臺執行緒中完成,這個過程進行的同時,我們仍然可以繼續使用舊的段檔案來正常提供讀寫請求。合併過程完成後,我們將讀取請求轉換為使用新合併的段而不是舊的段 —— 然後舊的段檔案就可以簡單地刪除掉了。
![](../img/fig3-3.png) ![](/img/fig3-3.png)
**圖 3-3 同時執行壓縮和分段合併** **圖 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樹 ### SSTables和LSM樹
在 [圖 3-3](../img/fig3-3.png) 中,每個日誌結構儲存段都是一系列鍵值對。這些鍵值對按照它們寫入的順序排列,日誌中稍後的值優先於日誌中較早的相同鍵的值。除此之外,檔案中鍵值對的順序並不重要。 在 [圖 3-3](/img/fig3-3.png) 中,每個日誌結構儲存段都是一系列鍵值對。這些鍵值對按照它們寫入的順序排列,日誌中稍後的值優先於日誌中較早的相同鍵的值。除此之外,檔案中鍵值對的順序並不重要。
現在我們可以對段檔案的格式做一個簡單的改變:要求鍵值對的序列按鍵排序。乍一看,這個要求似乎打破了我們使用順序寫入的能力,我們將稍後再回到這個問題。 現在我們可以對段檔案的格式做一個簡單的改變:要求鍵值對的序列按鍵排序。乍一看,這個要求似乎打破了我們使用順序寫入的能力,我們將稍後再回到這個問題。
我們把這個格式稱為 **排序字串表Sorted String Table**,簡稱 SSTable。我們還要求每個鍵只在每個合併的段檔案中出現一次壓縮過程已經保證。與使用雜湊索引的日誌段相比SSTable 有幾個大的優勢: 我們把這個格式稱為 **排序字串表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 段,只保留每個鍵的最新值** **圖 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** **圖 3-5 具有記憶體索引的 SSTable**
@ -168,7 +169,7 @@ $ cat database
[^i]: 如果所有的鍵與值都是定長的,你可以使用段檔案上的二分查詢並完全避免使用記憶體索引。然而實踐中的鍵和值通常都是變長的,因此如果沒有索引,就很難知道記錄的分界點(前一條記錄結束以及後一條記錄開始的地方)。 [^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請注意區分。 [^譯註i]: 這裡的壓縮是 compression不是前文的 compaction請注意區分。
@ -214,23 +215,23 @@ Lucene是一種全文搜尋的索引引擎在 Elasticsearch 和 Solr 被
我們前面看到的日誌結構索引將資料庫分解為可變大小的段通常是幾兆位元組或更大的大小並且總是按順序寫入段。相比之下B 樹將資料庫分解成固定大小的 **塊block****分頁page**,傳統上大小為 4KB有時會更大並且一次只能讀取或寫入一個頁面。這種設計更接近於底層硬體因為硬碟空間也是按固定大小的塊來組織的。 我們前面看到的日誌結構索引將資料庫分解為可變大小的段通常是幾兆位元組或更大的大小並且總是按順序寫入段。相比之下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 樹索引查詢一個鍵** **圖 3-6 使用 B 樹索引查詢一個鍵**
一個頁面會被指定為 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該頁面或者直接包含每個鍵的值或者包含了對可以找到值的頁面的引用。 最終我們將到達某個包含單個鍵的頁面葉子頁面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 樹** **圖 3-7 透過分割頁面來生長 B 樹**
@ -254,7 +255,7 @@ B 樹的基本底層寫操作是用新資料覆寫硬碟上的頁面,並假定
由於 B 樹已經存在了很久,所以並不奇怪這麼多年下來有很多最佳化的設計被開發出來,僅舉幾例: 由於 B 樹已經存在了很久,所以並不奇怪這麼多年下來有很多最佳化的設計被開發出來,僅舉幾例:
* 不同於覆寫頁面並維護 WAL 以支援崩潰恢復,一些資料庫(如 LMDB使用寫時複製方案【21】。經過修改的頁面被寫入到不同的位置並且還在樹中建立了父頁面的新版本以指向新的位置。這種方法對於併發控制也很有用我們將在 “[快照隔離和可重複讀](ch7.md#快照隔離和可重複讀)” 中看到。 * 不同於覆寫頁面並維護 WAL 以支援崩潰恢復,一些資料庫(如 LMDB使用寫時複製方案【21】。經過修改的頁面被寫入到不同的位置並且還在樹中建立了父頁面的新版本以指向新的位置。這種方法對於併發控制也很有用我們將在 “[快照隔離和可重複讀](/tw/ch7#快照隔離和可重複讀)” 中看到。
* 我們可以透過不儲存整個鍵,而是縮短其大小,來節省頁面空間。特別是在樹內部的頁面上,鍵只需要提供足夠的資訊來充當鍵範圍之間的邊界。在頁面中包含更多的鍵允許樹具有更高的分支因子,因此也就允許更少的層級 [^iii]。 * 我們可以透過不儲存整個鍵,而是縮短其大小,來節省頁面空間。特別是在樹內部的頁面上,鍵只需要提供足夠的資訊來充當鍵範圍之間的邊界。在頁面中包含更多的鍵允許樹具有更高的分支因子,因此也就允許更少的層級 [^iii]。
* 通常,頁面可以放置在硬碟上的任何位置;沒有什麼要求相鄰鍵範圍的頁面也放在硬碟上相鄰的區域。如果某個查詢需要按照排序順序掃描大部分的鍵範圍,那麼這種按頁面儲存的佈局可能會效率低下,因為每個頁面的讀取都需要執行一次硬碟查詢。因此,許多 B 樹的實現在佈局樹時會盡量使葉子頁面按順序出現在硬碟上。但是,隨著樹的增長,要維持這個順序是很困難的。相比之下,由於 LSM 樹在合併過程中一次性重寫一大段儲存,所以它們更容易使順序鍵在硬碟上連續儲存。 * 通常,頁面可以放置在硬碟上的任何位置;沒有什麼要求相鄰鍵範圍的頁面也放在硬碟上相鄰的區域。如果某個查詢需要按照排序順序掃描大部分的鍵範圍,那麼這種按頁面儲存的佈局可能會效率低下,因為每個頁面的讀取都需要執行一次硬碟查詢。因此,許多 B 樹的實現在佈局樹時會盡量使葉子頁面按順序出現在硬碟上。但是,隨著樹的增長,要維持這個順序是很困難的。相比之下,由於 LSM 樹在合併過程中一次性重寫一大段儲存,所以它們更容易使順序鍵在硬碟上連續儲存。
* 額外的指標被新增到樹中。例如,每個葉子頁面可以引用其左邊和右邊的兄弟頁面,使得不用跳回父頁面就能按順序對鍵進行掃描。 * 額外的指標被新增到樹中。例如,每個葉子頁面可以引用其左邊和右邊的兄弟頁面,使得不用跳回父頁面就能按順序對鍵進行掃描。
@ -284,13 +285,13 @@ LSM 樹可以被壓縮得更好,因此通常能比 B 樹在硬碟上產生更
#### LSM樹的缺點 #### LSM樹的缺點
日誌結構儲存的缺點是壓縮過程有時會干擾正在進行的讀寫操作。儘管儲存引擎嘗試增量地執行壓縮以儘量不影響併發訪問,但是硬碟資源有限,所以很容易發生某個請求需要等待硬碟先完成昂貴的壓縮操作。對吞吐量和平均響應時間的影響通常很小,但是日誌結構化儲存引擎在更高百分位的響應時間(請參閱 “[描述效能](ch1.md#描述效能)”)有時會相當長,而 B 樹的行為則相對更具有可預測性【28】。 日誌結構儲存的缺點是壓縮過程有時會干擾正在進行的讀寫操作。儘管儲存引擎嘗試增量地執行壓縮以儘量不影響併發訪問,但是硬碟資源有限,所以很容易發生某個請求需要等待硬碟先完成昂貴的壓縮操作。對吞吐量和平均響應時間的影響通常很小,但是日誌結構化儲存引擎在更高百分位的響應時間(請參閱 “[描述效能](/tw/ch1#描述效能)”)有時會相當長,而 B 樹的行為則相對更具有可預測性【28】。
壓縮的另一個問題出現在高寫入吞吐量時:硬碟的有限寫入頻寬需要在初始寫入(記錄日誌和重新整理記憶體表到硬碟)和在後臺執行的壓縮執行緒之間共享。寫入空資料庫時,可以使用全硬碟頻寬進行初始寫入,但資料庫越大,壓縮所需的硬碟頻寬就越多。 壓縮的另一個問題出現在高寫入吞吐量時:硬碟的有限寫入頻寬需要在初始寫入(記錄日誌和重新整理記憶體表到硬碟)和在後臺執行的壓縮執行緒之間共享。寫入空資料庫時,可以使用全硬碟頻寬進行初始寫入,但資料庫越大,壓縮所需的硬碟頻寬就越多。
如果寫入吞吐量很高,並且壓縮沒有仔細配置好,有可能導致壓縮跟不上寫入速率。在這種情況下,硬碟上未合併段的數量不斷增加,直到硬碟空間用完,讀取速度也會減慢,因為它們需要檢查更多的段檔案。通常情況下,即使壓縮無法跟上,基於 SSTable 的儲存引擎也不會限制傳入寫入的速率所以你需要進行明確的監控來檢測這種情況【29,30】。 如果寫入吞吐量很高,並且壓縮沒有仔細配置好,有可能導致壓縮跟不上寫入速率。在這種情況下,硬碟上未合併段的數量不斷增加,直到硬碟空間用完,讀取速度也會減慢,因為它們需要檢查更多的段檔案。通常情況下,即使壓縮無法跟上,基於 SSTable 的儲存引擎也不會限制傳入寫入的速率所以你需要進行明確的監控來檢測這種情況【29,30】。
B 樹的一個優點是每個鍵只存在於索引中的一個位置,而日誌結構化的儲存引擎可能在不同的段中有相同鍵的多個副本。這個方面使得 B 樹在想要提供強大的事務語義的資料庫中很有吸引力:在許多關係資料庫中,事務隔離是透過在鍵範圍上使用鎖來實現的,在 B 樹索引中這些鎖可以直接附加到樹上【5】。在 [第七章](ch7.md) 中,我們將更詳細地討論這一點。 B 樹的一個優點是每個鍵只存在於索引中的一個位置,而日誌結構化的儲存引擎可能在不同的段中有相同鍵的多個副本。這個方面使得 B 樹在想要提供強大的事務語義的資料庫中很有吸引力:在許多關係資料庫中,事務隔離是透過在鍵範圍上使用鎖來實現的,在 B 樹索引中這些鎖可以直接附加到樹上【5】。在 [第七章](/tw/ch7) 中,我們將更詳細地討論這一點。
B 樹在資料庫架構中是非常根深蒂固的,為許多工作負載都提供了始終如一的良好效能,所以它們不可能在短期內消失。在新的資料庫中,日誌結構化索引變得越來越流行。沒有簡單易行的辦法來判斷哪種型別的儲存引擎對你的使用場景更好,所以需要透過一些測試來得到相關經驗。 B 樹在資料庫架構中是非常根深蒂固的,為許多工作負載都提供了始終如一的良好效能,所以它們不可能在短期內消失。在新的資料庫中,日誌結構化索引變得越來越流行。沒有簡單易行的辦法來判斷哪種型別的儲存引擎對你的使用場景更好,所以需要透過一些測試來得到相關經驗。
@ -298,7 +299,7 @@ B 樹在資料庫架構中是非常根深蒂固的,為許多工作負載都提
到目前為止,我們只討論了鍵值索引,它們就像關係模型中的 **主鍵primary key** 索引。主鍵唯一標識關係表中的一行,或文件資料庫中的一個文件或圖形資料庫中的一個頂點。資料庫中的其他記錄可以透過其主鍵(或 ID引用該行 / 文件 / 頂點,索引就被用於解析這樣的引用。 到目前為止,我們只討論了鍵值索引,它們就像關係模型中的 **主鍵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 樹和日誌結構索引都可以用作次級索引。 次級索引可以很容易地從鍵值索引構建。次級索引主要的不同是鍵不是唯一的即可能有許多行文件頂點具有相同的鍵。這可以透過兩種方式來解決將匹配行識別符號的列表作為索引裡的值就像全文索引中的記錄列表或者透過向每個鍵新增行識別符號來使鍵唯一。無論哪種方式B 樹和日誌結構索引都可以用作次級索引。
@ -368,7 +369,7 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
在早期的業務資料處理過程中,一次典型的資料庫寫入通常與一筆 *商業交易commercial transaction* 相對應:賣個貨、向供應商下訂單、支付員工工資等等。但隨著資料庫開始應用到那些不涉及到錢的領域,術語 **交易 / 事務transaction** 仍留了下來,用於指代一組讀寫操作構成的邏輯單元。 在早期的業務資料處理過程中,一次典型的資料庫寫入通常與一筆 *商業交易commercial transaction* 相對應:賣個貨、向供應商下訂單、支付員工工資等等。但隨著資料庫開始應用到那些不涉及到錢的領域,術語 **交易 / 事務transaction** 仍留了下來,用於指代一組讀寫操作構成的邏輯單元。
> 事務不一定具有 ACID原子性一致性隔離性和永續性屬性。事務處理只是意味著允許客戶端進行低延遲的讀取和寫入 —— 而不是隻能定期執行(例如每天一次)的批處理作業。我們在 [第七章](ch7.md) 中討論 ACID 屬性,在 [第十章](ch10.md) 中討論批處理。 > 事務不一定具有 ACID原子性一致性隔離性和永續性屬性。事務處理只是意味著允許客戶端進行低延遲的讀取和寫入 —— 而不是隻能定期執行(例如每天一次)的批處理作業。我們在 [第七章](/tw/ch7) 中討論 ACID 屬性,在 [第十章](/tw/ch10) 中討論批處理。
即使資料庫開始被用於許多不同型別的資料,比如部落格文章的評論、遊戲中的動作、地址簿中的聯絡人等等,基本的訪問模式仍然類似於處理商業交易。應用程式通常使用索引透過某個鍵找少量記錄。根據使用者的輸入來插入或更新記錄。由於這些應用程式是互動式的,這種訪問模式被稱為 **線上事務處理OLTP, OnLine Transaction Processing** 即使資料庫開始被用於許多不同型別的資料,比如部落格文章的評論、遊戲中的動作、地址簿中的聯絡人等等,基本的訪問模式仍然類似於處理商業交易。應用程式通常使用索引透過某個鍵找少量記錄。根據使用者的輸入來插入或更新記錄。由於這些應用程式是互動式的,這種訪問模式被稱為 **線上事務處理OLTP, OnLine Transaction Processing**
@ -400,9 +401,9 @@ SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079
這些 OLTP 系統往往對業務運作至關重要,因而通常會要求 **高可用****低延遲**。所以 DBA 會密切關注他們的 OLTP 資料庫,他們通常不願意讓業務分析人員在 OLTP 資料庫上執行臨時的分析查詢,因為這些查詢通常開銷巨大,會掃描大部分資料集,這會損害同時在執行的事務的效能。 這些 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 至資料倉庫的簡化提綱** **圖 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 用於資料倉庫的星型模式的示例** **圖 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-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 按列儲存關係型資料,而不是行** **圖 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 壓縮的點陣圖索引儲存佈局** **圖 3-11 壓縮的點陣圖索引儲存佈局**
@ -535,9 +536,9 @@ WHERE product_sk = 31 AND store_sk = 3
相反,資料的排序需要對一整行統一操作,即使它們的儲存方式是按列的。資料庫管理員可以根據他們對常用查詢的瞭解,來選擇表格中用來排序的列。例如,如果查詢通常以日期範圍為目標,例如“上個月”,則可以將 `date_key` 作為第一個排序鍵。這樣查詢最佳化器就可以只掃描近1個月範圍的行了這比掃描所有行要快得多。 相反,資料的排序需要對一整行統一操作,即使它們的儲存方式是按列的。資料庫管理員可以根據他們對常用查詢的瞭解,來選擇表格中用來排序的列。例如,如果查詢通常以日期範圍為目標,例如“上個月”,則可以將 `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 資料庫中不經常使用物化檢視的原因。在讀取繁重的資料倉庫中,它們可能更有意義(它們是否實際上改善了讀取效能取決於使用場景)。 當底層資料發生變化時,物化檢視需要更新,因為它是資料的非規範化副本。資料庫可以自動完成該操作,但是這樣的更新使得寫入成本更高,這就是在 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 資料立方的兩個維度,透過求和聚合**
想象一下,現在每個事實都只有兩個維度表的外部索引鍵 —— 在 [圖 3-12](../img/fig3-12.png) 中分別是日期和產品。你現在可以繪製一個二維表格,一個軸線上是日期,另一個軸線上是產品。每個單元格包含具有該日期 - 產品組合的所有事實的屬性(例如 `net_price`)的聚合(例如 `SUM`)。然後,你可以沿著每行或每列應用相同的彙總,並獲得減少了一個維度的彙總(按產品的銷售額,無論日期,或者按日期的銷售額,無論產品)。 想象一下,現在每個事實都只有兩個維度表的外部索引鍵 —— 在 [圖 3-12](/img/fig3-12.png) 中分別是日期和產品。你現在可以繪製一個二維表格,一個軸線上是日期,另一個軸線上是產品。每個單元格包含具有該日期 - 產品組合的所有事實的屬性(例如 `net_price`)的聚合(例如 `SUM`)。然後,你可以沿著每行或每列應用相同的彙總,並獲得減少了一個維度的彙總(按產品的銷售額,無論日期,或者按日期的銷售額,無論產品)。
一般來說,事實往往有兩個以上的維度。在圖 3-9 中有五個維度:日期、產品、商店、促銷和客戶。要想象一個五維超立方體是什麼樣子是很困難的,但是原理是一樣的:每個單元格都包含特定日期 - 產品 - 商店 - 促銷 - 客戶組合的銷售額。這些值可以在每個維度上求和彙總。 一般來說,事實往往有兩個以上的維度。在圖 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. 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. 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. 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) 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 年) > —— 以弗所的赫拉克利特,為柏拉圖所引(公元前 360 年)
>
-------------------
[TOC] 應用程式不可避免地隨時間而變化。新產品的推出,對需求的深入理解,或者商業環境的變化,總會伴隨著 **功能feature** 的增增改改。[第一章](/tw/ch1) 介紹了 **可演化性evolvability** 的概念:應該盡力構建能靈活適應變化的系統(請參閱 “[可演化性:擁抱變化](/tw/ch1#可演化性:擁抱變化)”)。
應用程式不可避免地隨時間而變化。新產品的推出,對需求的深入理解,或者商業環境的變化,總會伴隨著 **功能feature** 的增增改改。[第一章](ch1.md) 介紹了 **可演化性evolvability** 的概念:應該盡力構建能靈活適應變化的系統(請參閱 “[可演化性:擁抱變化](ch1.md#可演化性:擁抱變化)”)。
在大多數情況下,修改應用程式的功能也意味著需要更改其儲存的資料:可能需要使用新的欄位或記錄型別,或者以新方式展示現有資料。 在大多數情況下,修改應用程式的功能也意味著需要更改其儲存的資料:可能需要使用新的欄位或記錄型別,或者以新方式展示現有資料。
我們在 [第二章](ch2.md) 討論的資料模型有不同的方法來應對這種變化。關係資料庫通常假定資料庫中的所有資料都遵循一個模式:儘管可以更改該模式(透過模式遷移,即 `ALTER` 語句),但是在任何時間點都有且僅有一個正確的模式。相比之下,**讀時模式**schema-on-read**無模式**,即 schemaless資料庫不會強制一個模式因此資料庫可以包含在不同時間寫入的新老資料格式的混合請參閱 “[文件模型中的模式靈活性](ch2.md#文件模型中的模式靈活性)” )。 我們在 [第二章](/tw/ch2) 討論的資料模型有不同的方法來應對這種變化。關係資料庫通常假定資料庫中的所有資料都遵循一個模式:儘管可以更改該模式(透過模式遷移,即 `ALTER` 語句),但是在任何時間點都有且僅有一個正確的模式。相比之下,**讀時模式**schema-on-read**無模式**,即 schemaless資料庫不會強制一個模式因此資料庫可以包含在不同時間寫入的新老資料格式的混合請參閱 “[文件模型中的模式靈活性](/tw/ch2#文件模型中的模式靈活性)” )。
當資料 **格式format****模式schema** 發生變化時,通常需要對應用程式程式碼進行相應的更改(例如,為記錄新增新欄位,然後修改程式開始讀寫該欄位)。但在大型應用程式中,程式碼變更通常不會立即完成: 當資料 **格式format****模式schema** 發生變化時,通常需要對應用程式程式碼進行相應的更改(例如,為記錄新增新欄位,然後修改程式開始讀寫該欄位)。但在大型應用程式中,程式碼變更通常不會立即完成:
@ -45,7 +46,7 @@
1. 在記憶體中,資料儲存在物件、結構體、列表、陣列、散列表、樹等中。這些資料結構針對 CPU 的高效訪問和操作進行了最佳化(通常使用指標)。 1. 在記憶體中,資料儲存在物件、結構體、列表、陣列、散列表、樹等中。這些資料結構針對 CPU 的高效訪問和操作進行了最佳化(通常使用指標)。
2. 如果要將資料寫入檔案,或透過網路傳送,則必須將其 **編碼encode** 為某種自包含的位元組序列例如JSON 文件)。由於每個程序都有自己獨立的地址空間,一個程序中的指標對任何其他程序都沒有意義,所以這個位元組序列表示會與通常在記憶體中使用的資料結構完全不同 [^i]。 2. 如果要將資料寫入檔案,或透過網路傳送,則必須將其 **編碼encode** 為某種自包含的位元組序列例如JSON 文件)。由於每個程序都有自己獨立的地址空間,一個程序中的指標對任何其他程序都沒有意義,所以這個位元組序列表示會與通常在記憶體中使用的資料結構完全不同 [^i]。
[^i]: 除一些特殊情況外,例如某些記憶體對映檔案或直接在壓縮資料上操作(如 “[列壓縮](ch3.md#列壓縮)” 中所述)。 [^i]: 除一些特殊情況外,例如某些記憶體對映檔案或直接在壓縮資料上操作(如 “[列壓縮](/tw/ch3#列壓縮)” 中所述)。
所以,需要在兩種表示之間進行某種型別的翻譯。從記憶體中表示到位元組序列的轉換稱為 **編碼Encoding** (也稱為 **序列化serialization****編組marshalling**),反過來稱為 **解碼Decoding**[^ii]**解析Parsing****反序列化deserialization****反編組unmarshalling**[^譯i]。 所以,需要在兩種表示之間進行某種型別的翻譯。從記憶體中表示到位元組序列的轉換稱為 **編碼Encoding** (也稱為 **序列化serialization****編組marshalling**),反過來稱為 **解碼Decoding**[^ii]**解析Parsing****反序列化deserialization****反編組unmarshalling**[^譯i]。
@ -53,7 +54,7 @@
[^譯i]: Marshal 與 Serialization 的區別Marshal 不僅傳輸物件的狀態,而且會一起傳輸物件的方法(相關程式碼)。 [^譯i]: Marshal 與 Serialization 的區別Marshal 不僅傳輸物件的狀態,而且會一起傳輸物件的方法(相關程式碼)。
> #### 術語衝突 > #### 術語衝突
> 不幸的是,在 [第七章](ch7.md) **事務Transaction** 的上下文裡,**序列化Serialization** 這個術語也出現了,而且具有完全不同的含義。儘管序列化可能是更常見的術語,為了避免術語過載,本書中堅持使用 **編碼Encoding** 表達此含義。 > 不幸的是,在 [第七章](/tw/ch7) **事務Transaction** 的上下文裡,**序列化Serialization** 這個術語也出現了,而且具有完全不同的含義。儘管序列化可能是更常見的術語,為了避免術語過載,本書中堅持使用 **編碼Encoding** 表達此含義。
這是一個常見的問題,因而有許多庫和編碼格式可供選擇。首先讓我們概覽一下。 這是一個常見的問題,因而有許多庫和編碼格式可供選擇。首先讓我們概覽一下。
@ -113,7 +114,7 @@ JSON 比 XML 簡潔,但與二進位制格式相比還是太佔空間。這一
在下面的章節中,能達到比這好得多的結果,只用 32 個位元組對相同的記錄進行編碼。 在下面的章節中,能達到比這好得多的結果,只用 32 個位元組對相同的記錄進行編碼。
![](../img/fig4-1.png) ![](/img/fig4-1.png)
**圖 4-1 使用 MessagePack 編碼的記錄(例 4-1** **圖 4-1 使用 MessagePack 編碼的記錄(例 4-1**
@ -141,27 +142,27 @@ message Person {
``` ```
Thrift 和 Protocol Buffers 每一個都帶有一個程式碼生成工具它採用了類似於這裡所示的模式定義並且生成了以各種程式語言實現模式的類【18】。你的應用程式程式碼可以呼叫此生成的程式碼來對模式的記錄進行編碼或解碼。 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 二進位制協議編碼的記錄** **圖 4-2 使用 Thrift 二進位制協議編碼的記錄**
[^iii]: 實際上Thrift 有三種二進位制協議BinaryProtocol、CompactProtocol 和 DenseProtocol儘管 DenseProtocol 只支援 C ++ 實現所以不算作跨語言【18】。除此之外它還有兩種不同的基於 JSON 的編碼格式【19】。真逗 [^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 壓縮協議編碼的記錄** **圖 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 編碼的記錄** **圖 4-4 使用 Protobuf 編碼的記錄**
@ -183,7 +184,7 @@ Thrift CompactProtocol 編碼在語義上等同於 BinaryProtocol但是如 [
如何改變欄位的資料型別?這也許是可能的 —— 詳細資訊請查閱相關的文件 —— 但是有一個風險,值將失去精度或被截斷。例如,假設你將一個 32 位的整數變成一個 64 位的整數。新程式碼可以輕鬆讀取舊程式碼寫入的資料,因為解析器可以用零填充任何缺失的位。但是,如果舊程式碼讀取由新程式碼寫入的資料,則舊程式碼仍使用 32 位變數來儲存該值。如果解碼的 64 位值不適合 32 位,則它將被截斷。 如何改變欄位的資料型別?這也許是可能的 —— 詳細資訊請查閱相關的文件 —— 但是有一個風險,值將失去精度或被截斷。例如,假設你將一個 32 位的整數變成一個 64 位的整數。新程式碼可以輕鬆讀取舊程式碼寫入的資料,因為解析器可以用零填充任何缺失的位。但是,如果舊程式碼讀取由新程式碼寫入的資料,則舊程式碼仍使用 32 位變數來儲存該值。如果解碼的 64 位值不適合 32 位,則它將被截斷。
Protobuf 的一個奇怪的細節是,它沒有列表或陣列資料型別,而是有一個欄位的重複標記(`repeated`,這是除必需和可選之外的第三個選項)。如 [圖 4-4](../img/fig4-4.png) 所示,重複欄位的編碼正如它所說的那樣:同一個欄位標記只是簡單地出現在記錄中。這具有很好的效果,可以將可選(單值)欄位更改為重複(多值)欄位。讀取舊資料的新程式碼會看到一個包含零個或一個元素的列表(取決於該欄位是否存在)。讀取新資料的舊程式碼只能看到列表的最後一個元素。 Protobuf 的一個奇怪的細節是,它沒有列表或陣列資料型別,而是有一個欄位的重複標記(`repeated`,這是除必需和可選之外的第三個選項)。如 [圖 4-4](/img/fig4-4.png) 所示,重複欄位的編碼正如它所說的那樣:同一個欄位標記只是簡單地出現在記錄中。這具有很好的效果,可以將可選(單值)欄位更改為重複(多值)欄位。讀取舊資料的新程式碼會看到一個包含零個或一個元素的列表(取決於該欄位是否存在)。讀取新資料的舊程式碼只能看到列表的最後一個元素。
Thrift 有一個專用的列表資料型別,它使用列表元素的資料型別進行引數化。這不允許 Protocol Buffers 所做的從單值到多值的演變,但是它具有支援巢狀列表的優點。 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 相同)進行編碼。 如果你檢查位元組序列,你可以看到沒有什麼可以識別字段或其資料型別。編碼只是由連在一起的值組成。一個字串只是一個長度字首,後跟 UTF-8 位元組,但是在被包含的資料中沒有任何內容告訴你它是一個字串。它可以是一個整數,也可以是其他的整數。整數使用可變長度編碼(與 Thrift 的 CompactProtocol 相同)進行編碼。
![](../img/fig4-5.png) ![](/img/fig4-5.png)
**圖 4-5 使用 Avro 編碼的記錄** **圖 4-5 使用 Avro 編碼的記錄**
@ -235,11 +236,11 @@ record Person {
當一個應用程式想要解碼一些資料(從一個檔案或資料庫讀取資料、從網路接收資料等)時,它希望資料在某個模式中,這就是 Reader 模式。這是應用程式程式碼所依賴的模式,在應用程式的構建過程中,程式碼可能已經從該模式生成。 當一個應用程式想要解碼一些資料(從一個檔案或資料庫讀取資料、從網路接收資料等)時,它希望資料在某個模式中,這就是 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 模式中宣告的預設值填充。 例如,如果 Writer 模式和 Reader 模式的欄位順序不同,這是沒有問題的,因為模式解析透過欄位名匹配欄位。如果讀取資料的程式碼遇到出現在 Writer 模式中但不在 Reader 模式中的欄位,則忽略它。如果讀取資料的程式碼需要某個欄位,但是 Writer 模式不包含該名稱的欄位,則使用在 Reader 模式中宣告的預設值填充。
![](../img/fig4-6.png) ![](/img/fig4-6.png)
**圖 4-6 一個 Avro Reader 解決讀寫模式的差異** **圖 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 所示。例如,如果將資料庫值解碼為應用程式中的模型物件,稍後重新編碼這些模型物件,那麼未知欄位可能會在該翻譯過程中丟失。解決這個問題不是一個難題,你只需要意識到它。 前面討論的編碼格式支援未知欄位的儲存,但是有時候需要在應用程式層面保持謹慎,如圖 4-7 所示。例如,如果將資料庫值解碼為應用程式中的模型物件,稍後重新編碼這些模型物件,那麼未知欄位可能會在該翻譯過程中丟失。解決這個問題不是一個難題,你只需要意識到它。
![](../img/fig4-7.png) ![](/img/fig4-7.png)
**圖 4-7 當較舊版本的應用程式更新以前由較新版本的應用程式編寫的資料時,如果不小心,資料可能會丟失。** **圖 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 ### 服務中的資料流REST與RPC
@ -378,7 +379,7 @@ Web 瀏覽器不是唯一的客戶端型別。例如,在移動裝置或桌面
此外,伺服器本身可以是另一個服務的客戶端(例如,典型的 Web 應用伺服器充當資料庫的客戶端)。這種方法通常用於將大型應用程式按照功能區域分解為較小的服務,這樣當一個服務需要來自另一個服務的某些功能或資料時,就會向另一個服務發出請求。這種構建應用程式的方式傳統上被稱為 **面向服務的體系結構service-oriented architectureSOA**,最近被改進和更名為 **微服務架構**【31,32】。 此外,伺服器本身可以是另一個服務的客戶端(例如,典型的 Web 應用伺服器充當資料庫的客戶端)。這種方法通常用於將大型應用程式按照功能區域分解為較小的服務,這樣當一個服務需要來自另一個服務的某些功能或資料時,就會向另一個服務發出請求。這種構建應用程式的方式傳統上被稱為 **面向服務的體系結構service-oriented architectureSOA**,最近被改進和更名為 **微服務架構**【31,32】。
在某些方面,服務類似於資料庫:它們通常允許客戶端提交和查詢資料。但是,雖然資料庫允許使用我們在 [第二章](ch2.md) 中討論的查詢語言進行任意查詢,但是服務公開了一個特定於應用程式的 API它只允許由服務的業務邏輯應用程式程式碼預定的輸入和輸出【33】。這種限制提供了一定程度的封裝服務能夠對客戶可以做什麼和不可以做什麼施加細粒度的限制。 在某些方面,服務類似於資料庫:它們通常允許客戶端提交和查詢資料。但是,雖然資料庫允許使用我們在 [第二章](/tw/ch2) 中討論的查詢語言進行任意查詢,但是服務公開了一個特定於應用程式的 API它只允許由服務的業務邏輯應用程式程式碼預定的輸入和輸出【33】。這種限制提供了一定程度的封裝服務能夠對客戶可以做什麼和不可以做什麼施加細粒度的限制。
面向服務 / 微服務架構的一個關鍵設計目標是透過使服務獨立部署和演化來使應用程式更易於更改和維護。例如,每個服務應該由一個團隊擁有,並且該團隊應該能夠經常釋出新版本的服務,而不必與其他團隊協調。換句話說,我們應該期望伺服器和客戶端的舊版本和新版本同時執行,因此伺服器和客戶端使用的資料編碼必須在不同版本的服務 API 之間相容 —— 這正是我們在本章所一直在談論的。 面向服務 / 微服務架構的一個關鍵設計目標是透過使服務獨立部署和演化來使應用程式更易於更改和維護。例如,每個服務應該由一個團隊擁有,並且該團隊應該能夠經常釋出新版本的服務,而不必與其他團隊協調。換句話說,我們應該期望伺服器和客戶端的舊版本和新版本同時執行,因此伺服器和客戶端使用的資料編碼必須在不同版本的服務 API 之間相容 —— 這正是我們在本章所一直在談論的。
@ -415,8 +416,8 @@ Web 服務僅僅是透過網路進行 API 請求的一系列技術的最新版
所有這些都是基於 **遠端過程呼叫RPC** 的思想,該過程呼叫自 20 世紀 70 年代以來一直存在【42】。RPC 模型試圖向遠端網路服務發出請求,看起來與在同一程序中呼叫程式語言中的函式或方法相同(這種抽象稱為位置透明)。儘管 RPC 起初看起來很方便但這種方法根本上是有缺陷的【43,44】。網路請求與本地函式呼叫非常不同 所有這些都是基於 **遠端過程呼叫RPC** 的思想,該過程呼叫自 20 世紀 70 年代以來一直存在【42】。RPC 模型試圖向遠端網路服務發出請求,看起來與在同一程序中呼叫程式語言中的函式或方法相同(這種抽象稱為位置透明)。儘管 RPC 起初看起來很方便但這種方法根本上是有缺陷的【43,44】。網路請求與本地函式呼叫非常不同
* 本地函式呼叫是可預測的,並且成功或失敗僅取決於受你控制的引數。網路請求是不可預測的:請求或響應可能由於網路問題會丟失,或者遠端計算機可能很慢或不可用,這些問題完全不在你的控制範圍之內。網路問題很常見,因此必須有所準備,例如重試失敗的請求。 * 本地函式呼叫是可預測的,並且成功或失敗僅取決於受你控制的引數。網路請求是不可預測的:請求或響應可能由於網路問題會丟失,或者遠端計算機可能很慢或不可用,這些問題完全不在你的控制範圍之內。網路問題很常見,因此必須有所準備,例如重試失敗的請求。
* 本地函式呼叫要麼返回結果,要麼丟擲異常,或者永遠不返回(因為進入無限迴圈或程序崩潰)。網路請求有另一個可能的結果:由於超時,它返回時可能沒有結果。在這種情況下,你根本不知道發生了什麼:如果你沒有得到來自遠端服務的響應,你無法知道請求是否透過(我們將在 [第八章](ch8.md) 更詳細地討論這個問題)。 * 本地函式呼叫要麼返回結果,要麼丟擲異常,或者永遠不返回(因為進入無限迴圈或程序崩潰)。網路請求有另一個可能的結果:由於超時,它返回時可能沒有結果。在這種情況下,你根本不知道發生了什麼:如果你沒有得到來自遠端服務的響應,你無法知道請求是否透過(我們將在 [第八章](/tw/ch8) 更詳細地討論這個問題)。
* 如果你重試失敗的網路請求,可能會發生請求實際上已經完成,只是響應丟失的情況。在這種情況下,重試將導致該操作被執行多次,除非你在協議中建立資料去重機制(**冪等性**,即 idempotence。本地函式呼叫時沒有這樣的問題。在 [第十一章](ch11.md) 更詳細地討論冪等性) * 如果你重試失敗的網路請求,可能會發生請求實際上已經完成,只是響應丟失的情況。在這種情況下,重試將導致該操作被執行多次,除非你在協議中建立資料去重機制(**冪等性**,即 idempotence。本地函式呼叫時沒有這樣的問題。在 [第十一章](/tw/ch11) 更詳細地討論冪等性)
* 每次呼叫本地函式時,通常需要大致相同的時間來執行。網路請求比函式呼叫要慢得多,而且其延遲也是非常可變的:好的時候它可能會在不到一毫秒的時間內完成,但是當網路擁塞或者遠端服務超載時,可能需要幾秒鐘的時間才能完成相同的操作。 * 每次呼叫本地函式時,通常需要大致相同的時間來執行。網路請求比函式呼叫要慢得多,而且其延遲也是非常可變的:好的時候它可能會在不到一毫秒的時間內完成,但是當網路擁塞或者遠端服務超載時,可能需要幾秒鐘的時間才能完成相同的操作。
* 呼叫本地函式時,可以高效地將引用(指標)傳遞給本地記憶體中的物件。當你發出一個網路請求時,所有這些引數都需要被編碼成可以透過網路傳送的一系列位元組。如果引數是像數字或字串這樣的基本型別倒是沒關係,但是對於較大的物件很快就會出現問題。 * 呼叫本地函式時,可以高效地將引用(指標)傳遞給本地記憶體中的物件。當你發出一個網路請求時,所有這些引數都需要被編碼成可以透過網路傳送的一系列位元組。如果引數是像數字或字串這樣的基本型別倒是沒關係,但是對於較大的物件很快就會出現問題。
* 客戶端和服務可以用不同的程式語言實現,所以 RPC 框架必須將資料型別從一種語言翻譯成另一種語言。這可能會變得很醜陋,因為不是所有的語言都具有相同的型別 —— 例如回想一下 JavaScript 的數字大於 $2^{53}$ 的問題(請參閱 “[JSON、XML 和二進位制變體](#JSON、XML和二進位制變體)”)。用單一語言編寫的單個程序中不存在此問題。 * 客戶端和服務可以用不同的程式語言實現,所以 RPC 框架必須將資料型別從一種語言翻譯成另一種語言。這可能會變得很醜陋,因為不是所有的語言都具有相同的型別 —— 例如回想一下 JavaScript 的數字大於 $2^{53}$ 的問題(請參閱 “[JSON、XML 和二進位制變體](#JSON、XML和二進位制變體)”)。用單一語言編寫的單個程序中不存在此問題。
@ -429,7 +430,7 @@ Web 服務僅僅是透過網路進行 API 請求的一系列技術的最新版
這種新一代的 RPC 框架更加明確的是遠端請求與本地函式呼叫不同。例如Finagle 和 Rest.li 使用 futurespromises來封裝可能失敗的非同步操作。`Futures` 還可以簡化需要並行發出多項服務並將其結果合併的情況【45】。gRPC 支援流其中一個呼叫不僅包括一個請求和一個響應還可以是隨時間的一系列請求和響應【46】。 這種新一代的 RPC 框架更加明確的是遠端請求與本地函式呼叫不同。例如Finagle 和 Rest.li 使用 futurespromises來封裝可能失敗的非同步操作。`Futures` 還可以簡化需要並行發出多項服務並將其結果合併的情況【45】。gRPC 支援流其中一個呼叫不僅包括一個請求和一個響應還可以是隨時間的一系列請求和響應【46】。
其中一些框架還提供服務發現,即允許客戶端找出在哪個 IP 地址和埠號上可以找到特定的服務。我們將在 “[請求路由](ch6.md#請求路由)” 中回到這個主題。 其中一些框架還提供服務發現,即允許客戶端找出在哪個 IP 地址和埠號上可以找到特定的服務。我們將在 “[請求路由](/tw/ch6#請求路由)” 中回到這個主題。
使用二進位制編碼格式的自定義 RPC 協議可以實現比通用的 JSON over REST 更好的效能。但是RESTful API 還有其他一些顯著的優點:方便實驗和除錯(只需使用 Web 瀏覽器或命令列工具 curl無需任何程式碼生成或軟體安裝即可向其請求能被所有主流的程式語言和平臺所支援還有大量可用的工具伺服器、快取、負載平衡器、代理、防火牆、監控、除錯工具、測試工具等的生態系統。 使用二進位制編碼格式的自定義 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框架 #### 分散式的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. 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. “[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. 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. 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 > —— 道格拉斯・亞當斯1992
------
[TOC] 複製意味著在透過網路連線的多臺機器上保留相同資料的副本。正如在 [第二部分](/tw/part-ii) 的介紹中所討論的那樣,我們希望能複製資料,可能出於各種各樣的原因:
複製意味著在透過網路連線的多臺機器上保留相同資料的副本。正如在 [第二部分](part-ii.md) 的介紹中所討論的那樣,我們希望能複製資料,可能出於各種各樣的原因:
* 使得資料與使用者在地理上接近(從而減少延遲) * 使得資料與使用者在地理上接近(從而減少延遲)
* 即使系統的一部分出現故障,系統也能繼續工作(從而提高可用性) * 即使系統的一部分出現故障,系統也能繼續工作(從而提高可用性)
* 伸縮可以接受讀請求的機器數量(從而提高讀取吞吐量) * 伸縮可以接受讀請求的機器數量(從而提高讀取吞吐量)
本章將假設你的資料集非常小,每臺機器都可以儲存整個資料集的副本。在 [第六章](ch6.md) 中將放寬這個假設,討論對單個機器來說太大的資料集的分割(分片)。在後面的章節中,我們將討論複製資料系統中可能發生的各種故障,以及如何處理這些故障。 本章將假設你的資料集非常小,每臺機器都可以儲存整個資料集的副本。在 [第六章](/tw/ch6) 中將放寬這個假設,討論對單個機器來說太大的資料集的分割(分片)。在後面的章節中,我們將討論複製資料系統中可能發生的各種故障,以及如何處理這些故障。
如果複製中的資料不會隨時間而改變,那複製就很簡單:將資料複製到每個節點一次就萬事大吉。複製的困難之處在於處理複製資料的 **變更change**,這就是本章所要講的。我們將討論三種流行的變更復制演算法:**單領導者single leader單主****多領導者multi leader多主** 和 **無領導者leaderless無主**。幾乎所有分散式資料庫都使用這三種方法之一。 如果複製中的資料不會隨時間而改變,那複製就很簡單:將資料複製到每個節點一次就萬事大吉。複製的困難之處在於處理複製資料的 **變更change**,這就是本章所要講的。我們將討論三種流行的變更復制演算法:**單領導者single leader單主****多領導者multi leader多主** 和 **無領導者leaderless無主**。幾乎所有分散式資料庫都使用這三種方法之一。
@ -36,7 +38,7 @@
[^i]: 不同的人對 **熱hot**、**溫warm** 和 **冷cold** 備份伺服器有不同的定義。例如在 PostgreSQL 中,**熱備hot standby** 指的是能接受客戶端讀請求的副本。而 **溫備warm standby** 只是追隨領導者,但不處理客戶端的任何查詢。就本書而言,這些差異並不重要。 [^i]: 不同的人對 **熱hot**、**溫warm** 和 **冷cold** 備份伺服器有不同的定義。例如在 PostgreSQL 中,**熱備hot standby** 指的是能接受客戶端讀請求的副本。而 **溫備warm standby** 只是追隨領導者,但不處理客戶端的任何查詢。就本書而言,這些差異並不重要。
![](../img/fig5-1.png) ![](/img/fig5-1.png)
**圖 5-1 基於領導者的(主/從)複製** **圖 5-1 基於領導者的(主/從)複製**
@ -48,13 +50,13 @@
想象一下 [圖 5-1](fig5-1.png) 中發生的場景,即網站的使用者更新他們的個人頭像。在某個時間點,客戶向主庫傳送更新請求;不久之後主庫就收到了請求。在某個時間點,主庫又會將資料變更轉發給自己的從庫。最終,主庫通知客戶更新成功。 想象一下 [圖 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 基於領導者的複製:一個同步從庫和一個非同步從庫**
在 [圖 5-2](../img/fig5-2.png) 的示例中,從庫 1 的複製是同步的:在向用戶報告寫入成功並使結果對其他使用者可見之前,主庫需要等待從庫 1 的確認,確保從庫 1 已經收到寫入操作。而從庫 2 的複製是非同步的:主庫傳送訊息,但不等待該從庫的響應。 在 [圖 5-2](/img/fig5-2.png) 的示例中,從庫 1 的複製是同步的:在向用戶報告寫入成功並使結果對其他使用者可見之前,主庫需要等待從庫 1 的確認,確保從庫 1 已經收到寫入操作。而從庫 2 的複製是非同步的:主庫傳送訊息,但不等待該從庫的響應。
在這幅圖中,從庫 2 處理訊息前存在一個顯著的延遲。通常情況下,複製的速度相當快:大多數資料庫系統能在不到一秒內完成從庫的同步,但它們不能提供複製用時的保證。有些情況下,從庫可能落後主庫幾分鐘或更久,例如:從庫正在從故障中恢復,系統正在最大容量附近執行,或者當節點間存在網路問題時。 在這幅圖中,從庫 2 處理訊息前存在一個顯著的延遲。通常情況下,複製的速度相當快:大多數資料庫系統能在不到一秒內完成從庫的同步,但它們不能提供複製用時的保證。有些情況下,從庫可能落後主庫幾分鐘或更久,例如:從庫正在從故障中恢復,系統正在最大容量附近執行,或者當節點間存在網路問題時。
@ -70,7 +72,7 @@
> >
> 對於非同步複製系統而言,主庫故障時會丟失資料可能是一個嚴重的問題,因此研究人員仍在研究不丟資料但仍能提供良好效能和可用性的複製方法。例如,**鏈式複製chain replication**【8,9】是同步複製的一種變體已經在一些系統如 Microsoft Azure Storage【10,11】中成功實現。 > 對於非同步複製系統而言,主庫故障時會丟失資料可能是一個嚴重的問題,因此研究人員仍在研究不丟資料但仍能提供良好效能和可用性的複製方法。例如,**鏈式複製chain replication**【8,9】是同步複製的一種變體已經在一些系統如 Microsoft Azure Storage【10,11】中成功實現。
> >
> 複製的一致性與 **共識**consensus使幾個節點就某個值達成一致之間有著密切的聯絡[第九章](ch9.md) 將詳細地探討這一領域的理論。本章主要討論實踐中的資料庫常用的簡單複製形式。 > 複製的一致性與 **共識**consensus使幾個節點就某個值達成一致之間有著密切的聯絡[第九章](/tw/ch9) 將詳細地探討這一領域的理論。本章主要討論實踐中的資料庫常用的簡單複製形式。
> >
### 設定新從庫 ### 設定新從庫
@ -105,8 +107,8 @@
故障切換可以手動進行(通知管理員主庫掛了,並採取必要的步驟來建立新的主庫)或自動進行。自動的故障切換過程通常由以下步驟組成: 故障切換可以手動進行(通知管理員主庫掛了,並採取必要的步驟來建立新的主庫)或自動進行。自動的故障切換過程通常由以下步驟組成:
1. 確認主庫失效。有很多事情可能會出錯:崩潰、停電、網路問題等等。沒有萬無一失的方法來檢測出現了什麼問題,所以大多數系統只是簡單使用 **超時Timeout** :節點頻繁地相互來回傳遞訊息,如果一個節點在一段時間內(例如 30 秒)沒有響應,就認為它掛了(因為計劃內維護而故意關閉主庫不算)。 1. 確認主庫失效。有很多事情可能會出錯:崩潰、停電、網路問題等等。沒有萬無一失的方法來檢測出現了什麼問題,所以大多數系統只是簡單使用 **超時Timeout** :節點頻繁地相互來回傳遞訊息,如果一個節點在一段時間內(例如 30 秒)沒有響應,就認為它掛了(因為計劃內維護而故意關閉主庫不算)。
2. 選擇一個新的主庫。這可以透過選舉過程(主庫由剩餘副本以多數選舉產生)來完成,或者可以由之前選定的 **控制器節點controller node** 來指定新的主庫。主庫的最佳人選通常是擁有舊主庫最新資料副本的從庫(以最小化資料損失)。讓所有的節點同意一個新的領導者,是一個 **共識** 問題,將在 [第九章](ch9.md) 詳細討論。 2. 選擇一個新的主庫。這可以透過選舉過程(主庫由剩餘副本以多數選舉產生)來完成,或者可以由之前選定的 **控制器節點controller node** 來指定新的主庫。主庫的最佳人選通常是擁有舊主庫最新資料副本的從庫(以最小化資料損失)。讓所有的節點同意一個新的領導者,是一個 **共識** 問題,將在 [第九章](/tw/ch9) 詳細討論。
3. 重新配置系統以啟用新的主庫。客戶端現在需要將它們的寫請求傳送給新主庫(將在 “[請求路由](ch6.md#請求路由)” 中討論這個問題)。如果舊主庫恢復,可能仍然認為自己是主庫,而沒有意識到其他副本已經讓它失去領導權了。系統需要確保舊主庫意識到新主庫的存在,併成為一個從庫。 3. 重新配置系統以啟用新的主庫。客戶端現在需要將它們的寫請求傳送給新主庫(將在 “[請求路由](/tw/ch6#請求路由)” 中討論這個問題)。如果舊主庫恢復,可能仍然認為自己是主庫,而沒有意識到其他副本已經讓它失去領導權了。系統需要確保舊主庫意識到新主庫的存在,併成為一個從庫。
故障切換的過程中有很多地方可能出錯: 故障切換的過程中有很多地方可能出錯:
@ -114,15 +116,15 @@
* 如果資料庫需要和其他外部儲存相協調,那麼丟棄寫入內容是極其危險的操作。例如在 GitHub 【13】的一場事故中一個過時的 MySQL 從庫被提升為主庫。資料庫使用自增 ID 作為主鍵,因為新主庫的計數器落後於老主庫的計數器,所以新主庫重新分配了一些已經被老主庫分配掉的 ID 作為主鍵。這些主鍵也在 Redis 中使用,主鍵重用使得 MySQL 和 Redis 中的資料產生不一致,最後導致一些私有資料洩漏到錯誤的使用者手中。 * 如果資料庫需要和其他外部儲存相協調,那麼丟棄寫入內容是極其危險的操作。例如在 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 #### 傳輸預寫式日誌WAL
在 [第三章](ch3.md) 中,我們討論了儲存引擎如何在磁碟上表示資料,我們也發現了通常會將寫操作追加到日誌中: 在 [第三章](/tw/ch3) 中,我們討論了儲存引擎如何在磁碟上表示資料,我們也發現了通常會將寫操作追加到日誌中:
* 對於日誌結構儲存引擎(請參閱 “[SSTables 和 LSM 樹](ch3.md#SSTables和LSM樹)”),日誌是主要的儲存位置。日誌段在後臺壓縮,並進行垃圾回收。 * 對於日誌結構儲存引擎(請參閱 “[SSTables 和 LSM 樹](/tw/ch3#SSTables和LSM樹)”),日誌是主要的儲存位置。日誌段在後臺壓縮,並進行垃圾回收。
* 對於覆寫單個磁碟塊的 [B 樹](ch3.md#B樹),每次修改都會先寫入 **預寫式日誌Write Ahead Log, WAL**,以便崩潰後索引可以恢復到一個一致的狀態。 * 對於覆寫單個磁碟塊的 [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 上的常見模式),一個有吸引力的選擇是建立很多從庫,並將讀請求分散到所有的從庫上去。這樣能減小主庫的負載,並允許由附近的副本來處理讀請求。 基於領導者的複製要求所有寫入都由單個節點處理但只讀查詢可以由任何一個副本來處理。所以對於讀多寫少的場景Web 上的常見模式),一個有吸引力的選擇是建立很多從庫,並將讀請求分散到所有的從庫上去。這樣能減小主庫的負載,並允許由附近的副本來處理讀請求。
@ -206,7 +208,7 @@
但對於非同步複製,問題就來了。如 [圖 5-3](fig5-3.png) 所示:如果使用者在寫入後馬上就檢視資料,則新資料可能尚未到達副本。對使用者而言,看起來好像是剛提交的資料丟失了,所以他們不高興是可以理解的。 但對於非同步複製,問題就來了。如 [圖 5-3](fig5-3.png) 所示:如果使用者在寫入後馬上就檢視資料,則新資料可能尚未到達副本。對使用者而言,看起來好像是剛提交的資料丟失了,所以他們不高興是可以理解的。
![](../img/fig5-3.png) ![](/img/fig5-3.png)
**圖 5-3 使用者寫入後從舊副本中讀取資料。需要寫後讀 (read-after-write) 的一致性來防止這種異常** **圖 5-3 使用者寫入後從舊副本中讀取資料。需要寫後讀 (read-after-write) 的一致性來防止這種異常**
@ -218,7 +220,7 @@
* 如果應用中的大部分內容都可能被使用者編輯,那這種方法就沒用了,因為大部分內容都必須從主庫讀取(讀伸縮就沒效果了)。在這種情況下可以使用其他標準來決定是否從主庫讀取。例如可以跟蹤上次更新的時間,在上次更新後的一分鐘內,從主庫讀。還可以監控從庫的複製延遲,防止向任何滯後主庫超過一分鐘的從庫發出查詢。 * 如果應用中的大部分內容都可能被使用者編輯,那這種方法就沒用了,因為大部分內容都必須從主庫讀取(讀伸縮就沒效果了)。在這種情況下可以使用其他標準來決定是否從主庫讀取。例如可以跟蹤上次更新的時間,在上次更新後的一分鐘內,從主庫讀。還可以監控從庫的複製延遲,防止向任何滯後主庫超過一分鐘的從庫發出查詢。
* 客戶端可以記住最近一次寫入的時間戳,系統需要確保從庫在處理該使用者的讀取請求時,該時間戳前的變更都已經傳播到了本從庫中。如果當前從庫不夠新,則可以從另一個從庫讀取,或者等待從庫追趕上來。這裡的時間戳可以是邏輯時間戳(表示寫入順序的東西,例如日誌序列號)或實際的系統時鐘(在這種情況下,時鐘同步變得至關重要,請參閱 “[不可靠的時鐘](ch8.md#不可靠的時鐘)”)。 * 客戶端可以記住最近一次寫入的時間戳,系統需要確保從庫在處理該使用者的讀取請求時,該時間戳前的變更都已經傳播到了本從庫中。如果當前從庫不夠新,則可以從另一個從庫讀取,或者等待從庫追趕上來。這裡的時間戳可以是邏輯時間戳(表示寫入順序的東西,例如日誌序列號)或實際的系統時鐘(在這種情況下,時鐘同步變得至關重要,請參閱 “[不可靠的時鐘](/tw/ch8#不可靠的時鐘)”)。
* 如果你的副本分佈在多個數據中心(為了在地理上接近使用者或者出於可用性目的),還會有額外的複雜性。任何需要由主庫提供服務的請求都必須路由到包含該主庫的資料中心。 * 如果你的副本分佈在多個數據中心(為了在地理上接近使用者或者出於可用性目的),還會有額外的複雜性。任何需要由主庫提供服務的請求都必須路由到包含該主庫的資料中心。
@ -234,9 +236,9 @@
在從非同步從庫讀取時可能發生的異常的第二個例子是使用者可能會遇到 **時光倒流moving backward in time** 在從非同步從庫讀取時可能發生的異常的第二個例子是使用者可能會遇到 **時光倒流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 使用者首先從新副本讀取,然後從舊副本讀取。時間看上去回退了。為了防止這種異常,我們需要單調的讀取。** **圖 5-4 使用者首先從新副本讀取,然後從舊副本讀取。時間看上去回退了。為了防止這種異常,我們需要單調的讀取。**
@ -257,7 +259,7 @@
這兩句話之間有因果關係Cake 夫人聽到了 Poons 先生的問題並回答了這個問題。 這兩句話之間有因果關係Cake 夫人聽到了 Poons 先生的問題並回答了這個問題。
現在想象第三個人正在透過從庫來聽這個對話。Cake 夫人說的內容是從一個延遲很低的從庫讀取的,但 Poons 先生所說的內容,從庫的延遲要大的多(見 [圖 5-5](../img/fig5-5.png))。於是,這個觀察者會聽到以下內容: 現在想象第三個人正在透過從庫來聽這個對話。Cake 夫人說的內容是從一個延遲很低的從庫讀取的,但 Poons 先生所說的內容,從庫的延遲要大的多(見 [圖 5-5](/img/fig5-5.png))。於是,這個觀察者會聽到以下內容:
*Mrs. Cake* *Mrs. Cake*
> 通常約十秒鐘Mr. Poons. > 通常約十秒鐘Mr. Poons.
@ -267,13 +269,13 @@
對於觀察者來說,看起來好像 Cake 夫人在 Poons 先生提問前就回答了這個問題。這種超能力讓人印象深刻但也會把人搞糊塗。【25】。 對於觀察者來說,看起來好像 Cake 夫人在 Poons 先生提問前就回答了這個問題。這種超能力讓人印象深刻但也會把人搞糊塗。【25】。
![](../img/fig5-5.png) ![](/img/fig5-5.png)
**圖 5-5 如果某些分割槽的複製速度慢於其他分割槽,那麼觀察者可能會在看到問題之前先看到答案。** **圖 5-5 如果某些分割槽的複製速度慢於其他分割槽,那麼觀察者可能會在看到問題之前先看到答案。**
要防止這種異常,需要另一種型別的保證:**一致字首讀consistent prefix reads**【23】。這個保證的意思是說如果一系列寫入按某個順序發生那麼任何人讀取這些寫入時也會看見它們以同樣的順序出現。 要防止這種異常,需要另一種型別的保證:**一致字首讀consistent prefix reads**【23】。這個保證的意思是說如果一系列寫入按某個順序發生那麼任何人讀取這些寫入時也會看見它們以同樣的順序出現。
這是 **分割槽partitioned****分片sharded** 資料庫中的一個特殊問題,我們將在 [第六章](ch6.md) 中討論分割槽資料庫。如果資料庫總是以相同的順序應用寫入,而讀取總是看到一致的字首,那麼這種異常不會發生。但是在許多分散式資料庫中,不同的分割槽獨立執行,因此不存在 **全域性的寫入順序**:當用戶從資料庫中讀取資料時,可能會看到資料庫的某些部分處於較舊的狀態,而某些則處於較新的狀態。 這是 **分割槽partitioned****分片sharded** 資料庫中的一個特殊問題,我們將在 [第六章](/tw/ch6) 中討論分割槽資料庫。如果資料庫總是以相同的順序應用寫入,而讀取總是看到一致的字首,那麼這種異常不會發生。但是在許多分散式資料庫中,不同的分割槽獨立執行,因此不存在 **全域性的寫入順序**:當用戶從資料庫中讀取資料時,可能會看到資料庫的某些部分處於較舊的狀態,而某些則處於較新的狀態。
一種解決方案是,確保任何因果相關的寫入都寫入相同的分割槽,但在一些應用中可能無法高效地完成這種操作。還有一些顯式跟蹤因果依賴關係的演算法,我們將在 “[“此前發生” 的關係和併發](#“此前發生”的關係和併發)” 一節中回到這個話題。 一種解決方案是,確保任何因果相關的寫入都寫入相同的分割槽,但在一些應用中可能無法高效地完成這種操作。還有一些顯式跟蹤因果依賴關係的演算法,我們將在 “[“此前發生” 的關係和併發](#“此前發生”的關係和併發)” 一節中回到這個話題。
@ -285,7 +287,7 @@
如果應用程式開發人員不必擔心微妙的複製問題,並可以信賴他們的資料庫 “做了正確的事情”,那該多好呀。這就是 **事務transaction** 存在的原因:**資料庫透過事務提供強大的保證**,所以應用程式可以更加簡單。 如果應用程式開發人員不必擔心微妙的複製問題,並可以信賴他們的資料庫 “做了正確的事情”,那該多好呀。這就是 **事務transaction** 存在的原因:**資料庫透過事務提供強大的保證**,所以應用程式可以更加簡單。
單節點事務已經存在了很長時間。然而在走向分散式(複製和分割槽)資料庫時,許多系統放棄了事務,聲稱事務在效能和可用性上的代價太高,並斷言在可伸縮系統中最終一致性是不可避免的。這個敘述有一些道理,但過於簡單了,本書其餘部分將提出更為細緻的觀點。我們將在 [第七章](ch7.md) 和 [第九章](ch9.md) 回到事務的話題,並將在 [第三部分](part-iii.md) 討論一些替代機制。 單節點事務已經存在了很長時間。然而在走向分散式(複製和分割槽)資料庫時,許多系統放棄了事務,聲稱事務在效能和可用性上的代價太高,並斷言在可伸縮系統中最終一致性是不可避免的。這個敘述有一些道理,但過於簡單了,本書其餘部分將提出更為細緻的觀點。我們將在 [第七章](/tw/ch7) 和 [第九章](/tw/ch9) 回到事務的話題,並將在 [第三部分](/tw/part-iii) 討論一些替代機制。
## 多主複製 ## 多主複製
@ -294,7 +296,7 @@
基於領導者的複製有一個主要的缺點:只有一個主庫,而且所有的寫入都必須透過它 [^iv]。如果出於任何原因(例如和主庫之間的網路連線中斷)無法連線到主庫,就無法向資料庫寫入。 基於領導者的複製有一個主要的缺點:只有一個主庫,而且所有的寫入都必須透過它 [^iv]。如果出於任何原因(例如和主庫之間的網路連線中斷)無法連線到主庫,就無法向資料庫寫入。
[^iv]: 如果資料庫被分割槽(見 [第六章](ch6.md)),每個分割槽都有一個主庫。不同的分割槽的主庫可能在不同的節點上,但是每個分割槽都必須有一個主庫。 [^iv]: 如果資料庫被分割槽(見 [第六章](/tw/ch6)),每個分割槽都有一個主庫。不同的分割槽的主庫可能在不同的節點上,但是每個分割槽都必須有一個主庫。
基於領導者的複製模型的自然延伸是允許多個節點接受寫入。複製仍然以同樣的方式發生:處理寫入的每個節點都必須將該資料變更轉發給所有其他節點。我們將其稱之為 **多領導者配置**multi-leader configuration也稱多主、多活複製即 master-master replication 或 active/active replication。在這種情況下每個主庫同時是其他主庫的從庫。 基於領導者的複製模型的自然延伸是允許多個節點接受寫入。複製仍然以同樣的方式發生:處理寫入的每個節點都必須將該資料變更轉發給所有其他節點。我們將其稱之為 **多領導者配置**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 跨多個數據中心的多主複製** **圖 5-6 跨多個數據中心的多主複製**
@ -328,7 +330,7 @@
有些資料庫預設情況下支援多主配置,但使用外部工具實現也很常見,例如用於 MySQL 的 Tungsten Replicator 【26】用於 PostgreSQL 的 BDR【27】以及用於 Oracle 的 GoldenGate 【19】。 有些資料庫預設情況下支援多主配置,但使用外部工具實現也很常見,例如用於 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】。 由於多主複製在許多資料庫中都屬於改裝的功能所以常常存在微妙的配置缺陷且經常與其他資料庫功能之間出現意外的反應。比如自增主鍵、觸發器、完整性約束等都可能會有麻煩。因此多主複製往往被認為是危險的領域應儘可能避免【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 兩個主庫同時更新同一記錄引起的寫入衝突** **圖 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** 的方式解決衝突,這意味著所有副本必須在所有變更復制完成時收斂至一個相同的最終值。 如果每個副本只是按照它看到寫入的順序寫入,那麼資料庫最終將處於不一致的狀態:最終值將是在主庫 1 的 C 和主庫 2 的 B。這是不可接受的每個複製方案都必須確保資料最終在所有副本中都是相同的。因此資料庫必須以一種 **收斂convergent** 的方式解決衝突,這意味著所有副本必須在所有變更復制完成時收斂至一個相同的最終值。
@ -388,7 +390,7 @@
* 給每個寫入一個唯一的 ID例如時間戳、長隨機數、UUID 或者鍵和值的雜湊),挑選最高 ID 的寫入作為勝利者,並丟棄其他寫入。如果使用時間戳,這種技術被稱為 **最後寫入勝利LWW, last write wins**。雖然這種方法很流行但是很容易造成資料丟失【35】。我們將在本章末尾的 [檢測併發寫入](#檢測併發寫入) 一節更詳細地討論 LWW。 * 給每個寫入一個唯一的 ID例如時間戳、長隨機數、UUID 或者鍵和值的雜湊),挑選最高 ID 的寫入作為勝利者,並丟棄其他寫入。如果使用時間戳,這種技術被稱為 **最後寫入勝利LWW, last write wins**。雖然這種方法很流行但是很容易造成資料丟失【35】。我們將在本章末尾的 [檢測併發寫入](#檢測併發寫入) 一節更詳細地討論 LWW。
* 為每個副本分配一個唯一的 IDID 編號更高的寫入具有更高的優先順序。這種方法也意味著資料丟失。 * 為每個副本分配一個唯一的 IDID 編號更高的寫入具有更高的優先順序。這種方法也意味著資料丟失。
* 以某種方式將這些值合併在一起 - 例如,按字母順序排序,然後連線它們(在 [圖 5-7](../img/fig5-7.png) 中,合併的標題可能類似於 “B/C” * 以某種方式將這些值合併在一起 - 例如,按字母順序排序,然後連線它們(在 [圖 5-7](/img/fig5-7.png) 中,合併的標題可能類似於 “B/C”
* 用一種可保留所有資訊的顯式資料結構來記錄衝突,並編寫解決衝突的應用程式程式碼(也許透過提示使用者的方式)。 * 用一種可保留所有資訊的顯式資料結構來記錄衝突,並編寫解決衝突的應用程式程式碼(也許透過提示使用者的方式)。
@ -404,7 +406,7 @@
當檢測到衝突時,所有衝突寫入被儲存。下一次讀取資料時,會將這些多個版本的資料返回給應用程式。應用程式可以提示使用者或自動解決衝突,並將結果寫回資料庫。例如 CouchDB 就以這種方式工作。 當檢測到衝突時,所有衝突寫入被儲存。下一次讀取資料時,會將這些多個版本的資料返回給應用程式。應用程式可以提示使用者或自動解決衝突,並將結果寫回資料庫。例如 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 三種可以在多主複製中使用的拓撲示例。** **圖 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】。當一個節點收到用自己的識別符號標記的資料更改時該資料更改將被忽略因為節點知道它已經被處理過。 在環形和星形拓撲中寫入可能需要在到達所有副本之前透過多個節點。因此節點需要轉發從其他節點收到的資料更改。為了防止無限複製迴圈每個節點被賦予一個唯一的識別符號並且在複製日誌中每次寫入都會使用其經過的所有節點的識別符號進行標記【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 使用多主複製時,寫入可能會以錯誤的順序到達某些副本。**
在 [圖 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】。 要正確排序這些事件,可以使用一種稱為 **版本向量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 法定寫入,法定讀取,並在節點中斷後讀修復。** **圖 5-10 法定寫入,法定讀取,並在節點中斷後讀修復。**
@ -492,7 +494,7 @@
* 讀修復Read repair * 讀修復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 * 反熵過程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$中受益。這會使得讀取速度更快,但缺點是隻要有一個不可用的節點就會導致所有的資料庫寫入都失敗。 在 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 + r > n$ 允許系統容忍不可用的節點,如下所示:
* 如果 $w < n$當節點不可用時我們仍然可以處理寫入 * 如果 $w < n$當節點不可用時我們仍然可以處理寫入
* 如果 $r < n$當節點不可用時我們仍然可以處理讀取 * 如果 $r < n$當節點不可用時我們仍然可以處理讀取
* 對於 $n = 3w = 2r = 2$,我們可以容忍一個不可用的節點。 * 對於 $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 決定我們等待多少個節點,即在我們認為讀或寫成功之前,有多少個節點需要報告成功。 * 通常,讀取和寫入操作始終並行傳送到所有 n 個副本。引數 w 和 r 決定我們等待多少個節點,即在我們認為讀或寫成功之前,有多少個節點需要報告成功。
![](../img/fig5-11.png) ![](/img/fig5-11.png)
**圖 5-11 如果 $w + r > n$,讀取 r 個副本,至少有一個副本必然包含了最近的成功寫入。** **圖 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】。 通常r 和 w 被選為多數(超過 $n/2$ )節點,因為這確保了 $w + r > n$,同時仍然容忍多達 $n/2$ 個節點故障。但是法定人數不一定必須是大多數重要的是讀寫使用的節點至少有一個節點的交集。其他法定人數的配置是可能的這使得分散式演算法的設計有一定的靈活性【45】。
@ -546,11 +548,11 @@
* 如果寫操作與讀操作同時發生,寫操作可能僅反映在某些副本上。在這種情況下,不確定讀取返回的是舊值還是新值。 * 如果寫操作與讀操作同時發生,寫操作可能僅反映在某些副本上。在這種情況下,不確定讀取返回的是舊值還是新值。
* 如果寫操作在某些副本上成功,而在其他節點上失敗(例如,因為某些節點上的磁碟已滿),在小於 w 個副本上寫入成功。所以整體判定寫入失敗但整體寫入失敗並沒有在寫入成功的副本上回滾。這意味著一個寫入雖然報告失敗後續的讀取仍然可能會讀取這次失敗寫入的值【47】。 * 如果寫操作在某些副本上成功,而在其他節點上失敗(例如,因為某些節點上的磁碟已滿),在小於 w 個副本上寫入成功。所以整體判定寫入失敗但整體寫入失敗並沒有在寫入成功的副本上回滾。這意味著一個寫入雖然報告失敗後續的讀取仍然可能會讀取這次失敗寫入的值【47】。
* 如果攜帶新值的節點發生故障,需要從其他帶有舊值的副本進行恢復,則儲存新值的副本數可能會低於 w從而打破法定人數條件。 * 如果攜帶新值的節點發生故障,需要從其他帶有舊值的副本進行恢復,則儲存新值的副本數可能會低於 w從而打破法定人數條件。
* 即使一切工作正常,有時也會不幸地出現關於 **時序timing** 的邊緣情況,我們將在 “[線性一致性和法定人數](ch9.md#線性一致性和法定人數)” 中看到這點。 * 即使一切工作正常,有時也會不幸地出現關於 **時序timing** 的邊緣情況,我們將在 “[線性一致性和法定人數](/tw/ch9#線性一致性和法定人數)” 中看到這點。
因此儘管法定人數似乎保證讀取返回最新的寫入值但在實踐中並不那麼簡單。Dynamo 風格的資料庫通常針對可以忍受最終一致性的用例進行最佳化。你可以透過引數 w 和 r 來調整讀取到陳舊值的機率,但把它們當成絕對的保證是不明智的。 因此儘管法定人數似乎保證讀取返回最新的寫入值但在實踐中並不那麼簡單。Dynamo 風格的資料庫通常針對可以忍受最終一致性的用例進行最佳化。你可以透過引數 w 和 r 來調整讀取到陳舊值的機率,但把它們當成絕對的保證是不明智的。
尤其是,因為通常得不到 “[複製延遲問題](#複製延遲問題)” 中討論的那些保證(讀己之寫,單調讀,一致字首讀),前面提到的異常可能會發生在應用程式中。更強有力的保證通常需要 **事務****共識**。我們將在 [第七章](ch7.md) 和 [第九章](ch9.md) 回到這些話題。 尤其是,因為通常得不到 “[複製延遲問題](#複製延遲問題)” 中討論的那些保證(讀己之寫,單調讀,一致字首讀),前面提到的異常可能會發生在應用程式中。更強有力的保證通常需要 **事務****共識**。我們將在 [第七章](/tw/ch7) 和 [第九章](/tw/ch9) 回到這些話題。
#### 監控陳舊度 #### 監控陳舊度
@ -595,17 +597,17 @@ Riak 將客戶端和資料庫節點之間的所有通訊保持在一個本地的
Dynamo 風格的資料庫允許多個客戶端同時寫入相同的鍵Key這意味著即使使用嚴格的法定人數也會發生衝突。這種情況與多主複製相似請參閱 “[處理寫入衝突](#處理寫入衝突)”),但在 Dynamo 風格的資料庫中,在 **讀修復****提示移交** 期間也可能會產生衝突。 Dynamo 風格的資料庫允許多個客戶端同時寫入相同的鍵Key這意味著即使使用嚴格的法定人數也會發生衝突。這種情況與多主複製相似請參閱 “[處理寫入衝突](#處理寫入衝突)”),但在 Dynamo 風格的資料庫中,在 **讀修復****提示移交** 期間也可能會產生衝突。
其問題在於,由於可變的網路延遲和部分節點的故障,事件可能以不同的順序到達不同的節點。例如,[圖 5-12](../img/fig5-12.png) 顯示了兩個客戶機 A 和 B 同時寫入三節點資料儲存中的鍵 X 其問題在於,由於可變的網路延遲和部分節點的故障,事件可能以不同的順序到達不同的節點。例如,[圖 5-12](/img/fig5-12.png) 顯示了兩個客戶機 A 和 B 同時寫入三節點資料儲存中的鍵 X
* 節點 1 接收來自 A 的寫入,但由於暫時中斷,未接收到來自 B 的寫入。 * 節點 1 接收來自 A 的寫入,但由於暫時中斷,未接收到來自 B 的寫入。
* 節點 2 首先接收來自 A 的寫入,然後接收來自 B 的寫入。 * 節點 2 首先接收來自 A 的寫入,然後接收來自 B 的寫入。
* 節點 3 首先接收來自 B 的寫入,然後從 A 寫入。 * 節點 3 首先接收來自 B 的寫入,然後從 A 寫入。
![](../img/fig5-12.png) ![](/img/fig5-12.png)
**圖 5-12 併發寫入 Dynamo 風格的資料儲存:沒有明確定義的順序。** **圖 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, last write wins**,是 Cassandra 唯一支援的衝突解決方法【53】也是 Riak 中的一個可選特徵【35】。
LWW 實現了最終收斂的目標,但以 **永續性** 為代價:如果同一個鍵有多個併發寫入,即使它們反饋給客戶端的結果都是成功的(因為它們被寫入 w 個副本也只有一個寫入將被保留而其他寫入將被默默地丟棄。此外LWW 甚至可能會丟棄不是併發的寫入,我們將在 “[有序事件的時間戳](ch8.md#有序事件的時間戳)” 中進行討論。 LWW 實現了最終收斂的目標,但以 **永續性** 為代價:如果同一個鍵有多個併發寫入,即使它們反饋給客戶端的結果都是成功的(因為它們被寫入 w 個副本也只有一個寫入將被保留而其他寫入將被默默地丟棄。此外LWW 甚至可能會丟棄不是併發的寫入,我們將在 “[有序事件的時間戳](/tw/ch8#有序事件的時間戳)” 中進行討論。
在類似快取的一些情況下寫入丟失可能是可以接受的。但如果資料丟失不可接受LWW 是解決衝突的一個很爛的選擇。 在類似快取的一些情況下寫入丟失可能是可以接受的。但如果資料丟失不可接受LWW 是解決衝突的一個很爛的選擇。
@ -639,7 +641,7 @@ LWW 實現了最終收斂的目標,但以 **永續性** 為代價:如果同
> #### 併發性、時間和相對性 > #### 併發性、時間和相對性
> >
> 如果兩個操作 **“同時”** 發生,似乎應該稱為併發 —— 但事實上,它們在字面時間上重疊與否並不重要。由於分散式系統中的時鐘問題,現實中是很難判斷兩個事件是否是 **同時** 發生的,這個問題我們將在 [第八章](ch8.md) 中詳細討論。 > 如果兩個操作 **“同時”** 發生,似乎應該稱為併發 —— 但事實上,它們在字面時間上重疊與否並不重要。由於分散式系統中的時鐘問題,現實中是很難判斷兩個事件是否是 **同時** 發生的,這個問題我們將在 [第八章](/tw/ch8) 中詳細討論。
> >
> 為了定義併發性,確切的時間並不重要:如果兩個操作都意識不到對方的存在,就稱這兩個操作 **併發**而不管它們實際發生的物理時間。人們有時把這個原理和物理學中的狹義相對論聯絡起來【54】該理論引入了資訊不能比光速更快的思想。因此如果兩個事件發生的時間差小於光透過它們之間的距離所需要的時間那麼這兩個事件不可能相互影響。 > 為了定義併發性,確切的時間並不重要:如果兩個操作都意識不到對方的存在,就稱這兩個操作 **併發**而不管它們實際發生的物理時間。人們有時把這個原理和物理學中的狹義相對論聯絡起來【54】該理論引入了資訊不能比光速更快的思想。因此如果兩個事件發生的時間差小於光透過它們之間的距離所需要的時間那麼這兩個事件不可能相互影響。
> >
@ -650,7 +652,7 @@ LWW 實現了最終收斂的目標,但以 **永續性** 為代價:如果同
我們來看一個演算法,它可以確定兩個操作是否為併發的,還是一個在另一個之前。簡單起見,我們從一個只有一個副本的資料庫開始。一旦我們知道了如何在單個副本上完成這項工作,我們可以將該方法推廣到具有多個副本的無主資料庫。 我們來看一個演算法,它可以確定兩個操作是否為併發的,還是一個在另一個之前。簡單起見,我們從一個只有一個副本的資料庫開始。一旦我們知道了如何在單個副本上完成這項工作,我們可以將該方法推廣到具有多個副本的無主資料庫。
[圖 5-13](../img/fig5-13.png) 顯示了兩個客戶端同時向同一購物車新增專案。(如果這樣的例子讓你覺得無趣,那麼可以想象一下兩個空中交通管制員同時把飛機新增到他們正在跟蹤的區域。)最初,購物車是空的。然後客戶端向資料庫發出五次寫入: [圖 5-13](/img/fig5-13.png) 顯示了兩個客戶端同時向同一購物車新增專案。(如果這樣的例子讓你覺得無趣,那麼可以想象一下兩個空中交通管制員同時把飛機新增到他們正在跟蹤的區域。)最初,購物車是空的。然後客戶端向資料庫發出五次寫入:
1. 客戶端 1 將牛奶加入購物車。這是該鍵的第一次寫入,伺服器成功儲存了它併為其分配版本號 1最後將值與版本號一起回送給客戶端。 1. 客戶端 1 將牛奶加入購物車。這是該鍵的第一次寫入,伺服器成功儲存了它併為其分配版本號 1最後將值與版本號一起回送給客戶端。
2. 客戶端 2 將雞蛋加入購物車,不知道客戶端 1 同時添加了牛奶(客戶端 2 認為它的雞蛋是購物車中的唯一物品)。伺服器為此寫入分配版本號 2並將雞蛋和牛奶儲存為兩個單獨的值。然後它將這兩個值 **都** 返回給客戶端 2 ,並附上版本號 2。 2. 客戶端 2 將雞蛋加入購物車,不知道客戶端 1 同時添加了牛奶(客戶端 2 認為它的雞蛋是購物車中的唯一物品)。伺服器為此寫入分配版本號 2並將雞蛋和牛奶儲存為兩個單獨的值。然後它將這兩個值 **都** 返回給客戶端 2 ,並附上版本號 2。
@ -658,13 +660,13 @@ LWW 實現了最終收斂的目標,但以 **永續性** 為代價:如果同
4. 同時,客戶端 2 想要加入火腿,不知道客戶端 1 剛剛加了麵粉。客戶端 2 在最近一次響應中從伺服器收到了兩個值 [牛奶] 和 [雞蛋],所以客戶端 2 現在合併這些值,並新增火腿形成一個新的值 [雞蛋,牛奶,火腿]。它將這個值傳送到伺服器,帶著之前的版本號 2 。伺服器檢測到新值會覆蓋版本 2 的值 [雞蛋],但新值也會與版本 3 的值 [牛奶,麵粉] **併發**,所以剩下的兩個值是版本 3 的 [牛奶,麵粉],和版本 4 的 [雞蛋,牛奶,火腿]。 4. 同時,客戶端 2 想要加入火腿,不知道客戶端 1 剛剛加了麵粉。客戶端 2 在最近一次響應中從伺服器收到了兩個值 [牛奶] 和 [雞蛋],所以客戶端 2 現在合併這些值,並新增火腿形成一個新的值 [雞蛋,牛奶,火腿]。它將這個值傳送到伺服器,帶著之前的版本號 2 。伺服器檢測到新值會覆蓋版本 2 的值 [雞蛋],但新值也會與版本 3 的值 [牛奶,麵粉] **併發**,所以剩下的兩個值是版本 3 的 [牛奶,麵粉],和版本 4 的 [雞蛋,牛奶,火腿]。
5. 最後,客戶端 1 想要加培根。它之前從伺服器接收到了版本 3 的 [牛奶,麵粉] 和 [雞蛋],所以它合併這些,新增培根,並將最終值 [牛奶,麵粉,雞蛋,培根] 連同版本號 3 發往伺服器。這會覆蓋版本 3 的值 [牛奶,麵粉](請注意 [雞蛋] 已經在上一步被覆蓋),但與版本 4 的值 [雞蛋,牛奶,火腿] 併發,所以伺服器將保留這兩個併發值。 5. 最後,客戶端 1 想要加培根。它之前從伺服器接收到了版本 3 的 [牛奶,麵粉] 和 [雞蛋],所以它合併這些,新增培根,並將最終值 [牛奶,麵粉,雞蛋,培根] 連同版本號 3 發往伺服器。這會覆蓋版本 3 的值 [牛奶,麵粉](請注意 [雞蛋] 已經在上一步被覆蓋),但與版本 4 的值 [雞蛋,牛奶,火腿] 併發,所以伺服器將保留這兩個併發值。
![](../img/fig5-13.png) ![](/img/fig5-13.png)
**圖 5-13 在同時編輯購物車時捕獲兩個客戶端之間的因果關係。** **圖 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 中的因果依賴關係圖。** **圖 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】的能以合理方式自動進行兄弟合併的資料結構家族包括對保留刪除的支援。 因為在應用程式程式碼中做兄弟合併是複雜且容易出錯的,所以有一些資料結構被設計出來用於自動執行這種合併,比如在 “[自動衝突解決](#自動衝突解決)” 中討論過的那些。舉例來說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】。我們不會深入細節但是它的工作方式與我們在購物車示例中看到的非常相似。 所有副本的版本號集合稱為 **版本向量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. 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. 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) 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 > —— Grace Murray Hopper未來的計算機及其管理1962
>
-------------
[TOC] 在 [第五章](/tw/ch5) 中,我們討論了複製 —— 即資料在不同節點上的副本,對於非常大的資料集,或非常高的吞吐量,僅僅進行復制是不夠的:我們需要將資料進行 **分割槽partitions**,也稱為 **分片sharding**[^i]。
在 [第五章](ch5.md) 中,我們討論了複製 —— 即資料在不同節點上的副本,對於非常大的資料集,或非常高的吞吐量,僅僅進行復制是不夠的:我們需要將資料進行 **分割槽partitions**,也稱為 **分片sharding**[^i]。 [^i]: 正如本章所討論的,分割槽是一種有意將大型資料庫分解成小型資料庫的方式。它與 **網路分割槽network partitions, netsplits** 無關,這是節點之間網路故障的一種。我們將在 [第八章](/tw/ch8) 討論這些錯誤。
[^i]: 正如本章所討論的,分割槽是一種有意將大型資料庫分解成小型資料庫的方式。它與 **網路分割槽network partitions, netsplits** 無關,這是節點之間網路故障的一種。我們將在 [第八章](ch8.md) 討論這些錯誤。
> #### 術語澄清 > #### 術語澄清
> >
@ -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](#分割槽再平衡),如果想要新增或刪除叢集中的節點,則必須進行再平衡。最後,我們將概述資料庫如何將請求路由到正確的分割槽並執行查詢。 在本章中,我們將首先介紹分割大型資料集的不同方法,並觀察索引如何與分割槽配合。然後我們將討論 [分割槽再平衡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 組合使用複製和分割槽:每個節點充當某些分割槽的主庫,其他分割槽充當從庫。** **圖 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 印刷版百科全書按照關鍵字範圍進行分割槽**
鍵的範圍不一定均勻分佈,因為資料也很可能不均勻分佈。例如在 [圖 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】。 分割槽邊界可以由管理員手動選擇,也可以由資料庫自動選擇(我們會在 “[分割槽再平衡](#分割槽再平衡)” 中更詳細地討論分割槽邊界的選擇。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】。 然而Key Range 分割槽的缺點是某些特定的訪問模式會導致熱點。如果主鍵是時間戳則分割槽對應於時間範圍例如給每天分配一個分割槽。不幸的是由於我們在測量發生時將資料從感測器寫入資料庫因此所有寫入操作都會轉到同一個分割槽即今天的分割槽這樣分割槽可能會因寫入而過載而其他分割槽則處於空閒狀態【5】。
@ -80,9 +82,9 @@
出於分割槽的目的雜湊函式不需要多麼強壯的加密演算法例如Cassandra 和 MongoDB 使用 MD5Voldemort 使用 Fowler-Noll-Vo 函式。許多程式語言都有內建的簡單雜湊函式(它們用於散列表),但是它們可能不適合分割槽:例如,在 Java 的 `Object.hashCode()` 和 Ruby 的 `Object#hash`同一個鍵可能在不同的程序中有不同的雜湊值【6】。 出於分割槽的目的雜湊函式不需要多麼強壯的加密演算法例如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 按雜湊鍵分割槽** **圖 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** > 正如我們將在 “[分割槽再平衡](#分割槽再平衡)” 中所看到的,這種特殊的方法對於資料庫實際上並不是很好,所以在實際中很少使用(某些資料庫的文件仍然會使用一致性雜湊的說法,但是它往往是不準確的)。因為有可能產生混淆,所以最好避免使用一致性雜湊這個術語,而只是把它稱為 **雜湊分割槽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 等搜尋伺服器的基石。 次級索引是關係型資料庫的基礎,並且在文件資料庫中也很普遍。許多鍵值儲存(如 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 列表中。 你想讓使用者搜尋汽車,允許他們透過顏色和廠商過濾,所以需要一個在顏色和廠商上的次級索引(文件資料庫中這些是 **欄位field**,關係資料庫中這些是 **列column** )。如果你聲明瞭索引,則資料庫可以自動執行索引 [^ii]。例如,無論何時將紅色汽車新增到資料庫,資料庫分割槽都會自動將其新增到索引條目 `color:red` 的文件 ID 列表中。
[^ii]: 如果資料庫僅支援鍵值模型,則你可能會嘗試在應用程式程式碼中建立從值到文件 ID 的對映來實現次級索引。如果沿著這條路線走下去,請萬分小心,確保你的索引與底層資料保持一致。競爭條件和間歇性寫入失敗(其中一些更改已儲存,但其他更改未儲存)很容易導致資料不同步 - 請參閱 “[多物件事務的需求](ch7.md#多物件事務的需求)”。 [^ii]: 如果資料庫僅支援鍵值模型,則你可能會嘗試在應用程式程式碼中建立從值到文件 ID 的對映來實現次級索引。如果沿著這條路線走下去,請萬分小心,確保你的索引與底層資料保持一致。競爭條件和間歇性寫入失敗(其中一些更改已儲存,但其他更改未儲存)很容易導致資料不同步 - 請參閱 “[多物件事務的需求](/tw/ch7#多物件事務的需求)”。
![](../img/fig6-4.png) ![](/img/fig6-4.png)
**圖 6-4 基於文件的次級索引進行分割槽** **圖 6-4 基於文件的次級索引進行分割槽**
在這種索引方法中,每個分割槽是完全獨立的:每個分割槽維護自己的次級索引,僅覆蓋該分割槽中的文件。它不關心儲存在其他分割槽的資料。無論何時你需要寫入資料庫(新增,刪除或更新文件),只需處理包含你正在編寫的文件 ID 的分割槽即可。出於這個原因,**文件分割槽索引** 也被稱為 **本地索引**(而不是將在下一節中描述的 **全域性索引**)。 在這種索引方法中,每個分割槽是完全獨立的:每個分割槽維護自己的次級索引,僅覆蓋該分割槽中的文件。它不關心儲存在其他分割槽的資料。無論何時你需要寫入資料庫(新增,刪除或更新文件),只需處理包含你正在編寫的文件 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)的次級索引進行分割槽 ### 基於關鍵詞(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 基於關鍵詞對次級索引進行分割槽** **圖 6-5 基於關鍵詞對次級索引進行分割槽**
@ -160,11 +162,11 @@ Cassandra 採取了折衷的策略【11, 12, 13】。Cassandra 中的表可以
關鍵詞分割槽的全域性索引優於文件分割槽索引的地方點是它可以使讀取更有效率:不需要 **分散 / 收集** 所有分割槽,客戶端只需要向包含關鍵詞的分割槽發出請求。全域性索引的缺點在於寫入速度較慢且較為複雜,因為寫入單個文件現在可能會影響索引的多個分割槽(文件中的每個關鍵詞可能位於不同的分割槽或者不同的節點上) 。 關鍵詞分割槽的全域性索引優於文件分割槽索引的地方點是它可以使讀取更有效率:不需要 **分散 / 收集** 所有分割槽,客戶端只需要向包含關鍵詞的分割槽發出請求。全域性索引的缺點在於寫入速度較慢且較為複雜,因為寫入單個文件現在可能會影響索引的多個分割槽(文件中的每個關鍵詞可能位於不同的分割槽或者不同的節點上) 。
理想情況下,索引總是最新的,寫入資料庫的每個文件都會立即反映在索引中。但是,在關鍵詞分割槽索引中,這需要跨分割槽的分散式事務,並不是所有資料庫都支援(請參閱 [第七章](ch7.md) 和 [第九章](ch9.md))。 理想情況下,索引總是最新的,寫入資料庫的每個文件都會立即反映在索引中。但是,在關鍵詞分割槽索引中,這需要跨分割槽的分散式事務,並不是所有資料庫都支援(請參閱 [第七章](/tw/ch7) 和 [第九章](/tw/ch9))。
在實踐中,對全域性次級索引的更新通常是 **非同步**也就是說如果在寫入之後不久讀取索引剛才所做的更改可能尚未反映在索引中。例如Amazon DynamoDB 聲稱在正常情況下其全域性次級索引會在不到一秒的時間內更新但在基礎架構出現故障的情況下可能會有延遲【20】。 在實踐中,對全域性次級索引的更新通常是 **非同步**也就是說如果在寫入之後不久讀取索引剛才所做的更改可能尚未反映在索引中。例如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 #### 反面教材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這似乎是將每個鍵分配給一個節點的簡單方法。 也許你想知道為什麼我們不使用 ***取模mod***(許多程式語言中的 % 運算子)。例如,`hash(key) mod 10` 會返回一個介於 0 和 9 之間的數字(如果我們將雜湊寫為十進位制數,雜湊模 10 將是最後一個數字)。如果我們有 10 個節點,編號為 0 到 9這似乎是將每個鍵分配給一個節點的簡單方法。
@ -201,11 +203,11 @@ Cassandra 採取了折衷的策略【11, 12, 13】。Cassandra 中的表可以
幸運的是,有一個相當簡單的解決方案:建立比節點更多的分割槽,併為每個節點分配多個分割槽。例如,執行在 10 個節點的叢集上的資料庫可能會從一開始就被拆分為 1,000 個分割槽,因此大約有 100 個分割槽被分配給每個節點。 幸運的是,有一個相當簡單的解決方案:建立比節點更多的分割槽,併為每個節點分配多個分割槽。例如,執行在 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 將新節點新增到每個節點具有多個分割槽的資料庫叢集。** **圖 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】。 每個分割槽分配給一個節點,每個節點可以處理多個分割槽,就像固定數量的分割槽一樣。大型分割槽拆分後,可以將其中的一半轉移到另一個節點,以平衡負載。在 HBase 中,分割槽檔案的傳輸透過 HDFS底層使用的分散式檔案系統來實現【3】。
@ -265,21 +267,21 @@ Cassandra 和 Ketama 使用的第三種方法是使分割槽數與節點數成
以上所有情況中的關鍵問題是:作出路由決策的元件(可能是節點之一,還是路由層或客戶端)如何瞭解分割槽 - 節點之間的分配關係變化? 以上所有情況中的關鍵問題是:作出路由決策的元件(可能是節點之一,還是路由層或客戶端)如何瞭解分割槽 - 節點之間的分配關係變化?
![](../img/fig6-7.png) ![](/img/fig6-7.png)
**圖 6-7 將請求路由到正確節點的三種不同方式。** **圖 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 跟蹤分割槽分配給節點。** **圖 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】。 Couchbase 不會自動進行再平衡,這簡化了設計。通常情況下,它配置了一個名為 moxi 的路由層它會從叢集節點了解路由變化【32】。
@ -291,7 +293,7 @@ Couchbase 不會自動進行再平衡,這簡化了設計。通常情況下,
然而,通常用於分析的 **大規模並行處理MPP, Massively parallel processing** 關係型資料庫產品在其支援的查詢型別方面要複雜得多。一個典型的資料倉庫查詢包含多個連線過濾分組和聚合操作。MPP 查詢最佳化器將這個複雜的查詢分解成許多執行階段和分割槽,其中許多可以在資料庫叢集的不同節點上並行執行。涉及掃描大規模資料集的查詢特別受益於這種並行執行。 然而,通常用於分析的 **大規模並行處理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. 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. 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. “[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) 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 > —— James Corbett 等人SpannerGoogle 的全球分散式資料庫2012
------
[TOC]
在資料系統的殘酷現實中,很多事情都可能出錯: 在資料系統的殘酷現實中,很多事情都可能出錯:
- 資料庫軟體、硬體可能在任意時刻發生故障(包括寫操作進行到一半時)。 - 資料庫軟體、硬體可能在任意時刻發生故障(包括寫操作進行到一半時)。
@ -31,14 +32,14 @@
本章將研究許多出錯案例,並探索資料庫用於防範這些問題的演算法。尤其會深入 **併發控制** 的領域,討論各種可能發生的競爭條件,以及資料庫如何實現 **讀已提交read committed****快照隔離snapshot isolation** 和 **可序列化serializability** 等隔離級別。 本章將研究許多出錯案例,並探索資料庫用於防範這些問題的演算法。尤其會深入 **併發控制** 的領域,討論各種可能發生的競爭條件,以及資料庫如何實現 **讀已提交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 異乎尋常地相似。 現今,幾乎所有的關係型資料庫和一些非關係資料庫都支援 **事務**。其中大多數遵循 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】。另一方面資料庫廠商有時將事務保證作為 “重要應用” 和 “有價值資料” 的基本要求。這兩種觀點都是 **純粹的誇張** 隨著這種新型分散式資料庫的炒作人們普遍認為事務是可伸縮性的對立面任何大型系統都必須放棄事務以保持良好的效能和高可用性【5,6】。另一方面資料庫廠商有時將事務保證作為 “重要應用” 和 “有價值資料” 的基本要求。這兩種觀點都是 **純粹的誇張**
@ -70,9 +71,9 @@ ACID 原子性的定義特徵是:**能夠在錯誤時中止事務,丟棄該
一致性這個詞被賦予太多含義: 一致性這個詞被賦予太多含義:
* 在 [第五章](ch5.md) 中,我們討論了副本一致性,以及非同步複製系統中的最終一致性問題(請參閱 “[複製延遲問題](ch5.md#複製延遲問題)”)。 * 在 [第五章](/tw/ch5) 中,我們討論了副本一致性,以及非同步複製系統中的最終一致性問題(請參閱 “[複製延遲問題](/tw/ch5#複製延遲問題)”)。
* [一致性雜湊](ch6.md#一致性雜湊) 是某些系統用於重新分割槽的一種分割槽方法。 * [一致性雜湊](/tw/ch6#一致性雜湊) 是某些系統用於重新分割槽的一種分割槽方法。
* 在 [CAP 定理](ch9.md#CAP定理) 中,一致性一詞用於表示 [線性一致性](ch9.md#線性一致性)。 * 在 [CAP 定理](/tw/ch9#CAP定理) 中,一致性一詞用於表示 [線性一致性](/tw/ch9#線性一致性)。
* 在 ACID 的上下文中,**一致性** 是指資料庫在應用程式的特定概念中處於 “良好狀態”。 * 在 ACID 的上下文中,**一致性** 是指資料庫在應用程式的特定概念中處於 “良好狀態”。
很不幸,這一個詞就至少有四種不同的含義。 很不幸,這一個詞就至少有四種不同的含義。
@ -89,11 +90,11 @@ ACID 一致性的概念是,**對資料的一組特定約束必須始終成立*
大多數資料庫都會同時被多個客戶端訪問。如果它們各自讀寫資料庫的不同部分,這是沒有問題的,但是如果它們訪問相同的資料庫記錄,則可能會遇到 **併發** 問題(**競爭條件**,即 race conditions 大多數資料庫都會同時被多個客戶端訪問。如果它們各自讀寫資料庫的不同部分,這是沒有問題的,但是如果它們訪問相同的資料庫記錄,則可能會遇到 **併發** 問題(**競爭條件**,即 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】。 ACID 意義上的隔離性意味著,**同時執行的事務是相互隔離的**:它們不能相互冒犯。傳統的資料庫教科書將隔離性形式化為 **可序列化Serializability**這意味著每個事務可以假裝它是唯一在整個資料庫上執行的事務。資料庫確保當多個事務被提交時結果與它們序列執行一個接一個是一樣的儘管實際上它們可能是併發執行的【10】。
![](../img/fig7-1.png) ![](/img/fig7-1.png)
**圖 7-1 兩個客戶之間的競爭狀態同時遞增計數器** **圖 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#可靠性)」),任何僅儲存在記憶體中的資料都會丟失,故記憶體資料庫仍然要和磁碟寫入打交道。 > * 一個相關性故障(停電,或一個特定輸入導致所有節點崩潰的 Bug可能會一次性摧毀所有副本請參閱「[可靠性](/tw/ch1#可靠性)」),任何僅儲存在記憶體中的資料都會丟失,故記憶體資料庫仍然要和磁碟寫入打交道。
> * 在非同步複製系統中,當主庫不可用時,最近的寫入操作可能會丟失(請參閱「[處理節點宕機](ch5.md#處理節點宕機)」)。 > * 在非同步複製系統中,當主庫不可用時,最近的寫入操作可能會丟失(請參閱「[處理節點宕機](/tw/ch5#處理節點宕機)」)。
> * 當電源突然斷電時,特別是固態硬碟,有證據顯示有時會違反應有的保證:甚至 fsync 也不能保證正常工作【12】。硬碟韌體可能有錯誤就像任何其他型別的軟體一樣【13,14】。 > * 當電源突然斷電時,特別是固態硬碟,有證據顯示有時會違反應有的保證:甚至 fsync 也不能保證正常工作【12】。硬碟韌體可能有錯誤就像任何其他型別的軟體一樣【13,14】。
> * 儲存引擎和檔案系統之間的微妙互動可能會導致難以追蹤的錯誤並可能導致磁碟上的檔案在崩潰後被損壞【15,16】。 > * 儲存引擎和檔案系統之間的微妙互動可能會導致難以追蹤的錯誤並可能導致磁碟上的檔案在崩潰後被損壞【15,16】。
> * 磁碟上的資料可能會在沒有檢測到的情況下逐漸損壞【17】。如果資料已損壞一段時間副本和最近的備份也可能損壞。這種情況下需要嘗試從歷史備份中恢復資料。 > * 磁碟上的資料可能會在沒有檢測到的情況下逐漸損壞【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 ```sql
SELECT COUNT*FROM emails WHERE recipient_id = 2 AND unread_flag = true 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]: 可以說郵件應用中的錯誤計數器並不是什麼特別重要的問題。但換種方式來看,你可以把未讀計數器換成客戶賬戶餘額,把郵件收發看成支付交易。 [^ii]: 可以說郵件應用中的錯誤計數器並不是什麼特別重要的問題。但換種方式來看,你可以把未讀計數器換成客戶賬戶餘額,把郵件收發看成支付交易。
![](../img/fig7-2.png) ![](/img/fig7-2.png)
**圖 7-2 違反隔離性:一個事務讀取另一個事務的未被執行的寫入(“髒讀”)。** **圖 7-2 違反隔離性:一個事務讀取另一個事務的未被執行的寫入(“髒讀”)。**
[圖 7-3](../img/fig7-3.png) 說明了對原子性的需求:如果在事務過程中發生錯誤,郵箱和未讀計數器的內容可能會失去同步。在原子事務中,如果對計數器的更新失敗,事務將被中止,並且插入的電子郵件將被回滾。 [圖 7-3](/img/fig7-3.png) 說明了對原子性的需求:如果在事務過程中發生錯誤,郵箱和未讀計數器的內容可能會失去同步。在原子事務中,如果對計數器的更新失敗,事務將被中止,並且插入的電子郵件將被回滾。
![](../img/fig7-3.png) ![](/img/fig7-3.png)
**圖 7-3 原子性確保發生錯誤時,事務先前的任何寫入都會被撤消,以避免狀態不一致** **圖 7-3 原子性確保發生錯誤時,事務先前的任何寫入都會被撤消,以避免狀態不一致**
多物件事務需要某種方式來確定哪些讀寫操作屬於同一個事務。在關係型資料庫中,通常基於客戶端與資料庫伺服器的 TCP 連線:在任何特定連線上,`BEGIN TRANSACTION` 和 `COMMIT` 語句之間的所有內容,被認為是同一事務的一部分.[^iii] 多物件事務需要某種方式來確定哪些讀寫操作屬於同一個事務。在關係型資料庫中,通常基於客戶端與資料庫伺服器的 TCP 連線:在任何特定連線上,`BEGIN TRANSACTION` 和 `COMMIT` 語句之間的所有內容,被認為是同一事務的一部分.[^iii]
[^iii]: 這並不完美。如果 TCP 連線中斷,則事務必須中止。如果中斷發生在客戶端請求提交之後,但在伺服器確認提交發生之前,客戶端並不知道事務是否已提交。為了解決這個問題,事務管理器可以透過一個唯一事務識別符號來對操作進行分組,這個識別符號並未繫結到特定 TCP 連線。後續再 “[資料庫的端到端原則](ch12.md#資料庫的端到端原則)” 一節將回到這個主題。 [^iii]: 這並不完美。如果 TCP 連線中斷,則事務必須中止。如果中斷發生在客戶端請求提交之後,但在伺服器確認提交發生之前,客戶端並不知道事務是否已提交。為了解決這個問題,事務管理器可以透過一個唯一事務識別符號來對操作進行分組,這個識別符號並未繫結到特定 TCP 連線。後續再 “[資料庫的端到端原則](/tw/ch12#資料庫的端到端原則)” 一節將回到這個主題。
另一方面,許多非關係資料庫並沒有將這些操作組合在一起的方法。即使存在多物件 API例如某鍵值儲存可能具有在一個操作中更新幾個鍵的 multi-put 操作),但這並不一定意味著它具有事務語義:該命令可能在一些鍵上成功,在其他的鍵上失敗,使資料庫處於部分更新的狀態。 另一方面,許多非關係資料庫並沒有將這些操作組合在一起的方法。即使存在多物件 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** 的增量。但這就太吹毛求疵了。 [^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 資料庫基於這樣的哲學:如果資料庫有違反其原子性,隔離性或永續性的危險,則寧願完全放棄事務,而不是留下半成品。 事務的一個關鍵特性是如果發生錯誤它可以中止並安全地重試。ACID 資料庫基於這樣的哲學:如果資料庫有違反其原子性,隔離性或永續性的危險,則寧願完全放棄事務,而不是留下半成品。
然而並不是所有的系統都遵循這個哲學。特別是具有 [無主複製](ch5.md#無主複製) 的資料儲存,主要是在 “盡力而為” 的基礎上進行工作。可以概括為 “資料庫將做盡可能多的事,執行遇到錯誤時,它不會撤消它已經完成的事情” —— 所以,從錯誤中恢復是應用程式的責任。 然而並不是所有的系統都遵循這個哲學。特別是具有 [無主複製](/tw/ch5#無主複製) 的資料儲存,主要是在 “盡力而為” 的基礎上進行工作。可以概括為 “資料庫將做盡可能多的事,執行遇到錯誤時,它不會撤消它已經完成的事情” —— 所以,從錯誤中恢復是應用程式的責任。
錯誤發生不可避免,但許多軟體開發人員傾向於只考慮樂觀情況,而不是錯誤處理的複雜性。例如,像 Rails 的 ActiveRecord 和 Django 這樣的 **物件關係對映ORM, object-relation Mapping** 框架不會重試中斷的事務 —— 這個錯誤通常會導致一個從堆疊向上傳播的異常,所以任何使用者輸入都會被丟棄,使用者拿到一個錯誤資訊。這實在是太恥辱了,因為中止的重點就是允許安全的重試。 錯誤發生不可避免,但許多軟體開發人員傾向於只考慮樂觀情況,而不是錯誤處理的複雜性。例如,像 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】。 設想一個事務已經將一些資料寫入資料庫,但事務還沒有提交或中止。另一個事務可以看到未提交的資料嗎?如果是的話,那就叫做 **髒讀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-4 沒有髒讀:使用者 2 只有在使用者 1 的事務已經提交後才能看到 x 的新值。**
為什麼要防止髒讀,有幾個原因: 為什麼要防止髒讀,有幾個原因:
- 如果事務需要更新多個物件,髒讀取意味著另一個事務可能會只看到一部分更新。例如,在 [圖 7-2](../img/fig7-2.png) 中,使用者看到新的未讀電子郵件,但看不到更新的計數器。這就是電子郵件的髒讀。看到處於部分更新狀態的資料庫會讓使用者感到困惑,並可能導致其他事務做出錯誤的決定。 - 如果事務需要更新多個物件,髒讀取意味著另一個事務可能會只看到一部分更新。例如,在 [圖 7-2](/img/fig7-2.png) 中,使用者看到新的未讀電子郵件,但看不到更新的計數器。這就是電子郵件的髒讀。看到處於部分更新狀態的資料庫會讓使用者感到困惑,並可能導致其他事務做出錯誤的決定。
- 如果事務中止,則所有寫入操作都需要回滾(如 [圖 7-3](../img/fig7-3.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-5](/img/fig7-5.png)以一個二手車銷售網站為例Alice 和 Bob 兩個人同時試圖購買同一輛車。購買汽車需要兩次資料庫寫入:網站上的商品列表需要更新,以反映買家的購買,銷售發票需要傳送給買家。在 [圖 7-5](/img/fig7-5.png) 的情況下,銷售是屬於 Bob 的(因為他成功更新了商品列表),但發票卻寄送給了 Alice因為她成功更新了發票表。讀已提交會防止這樣的事故。
- 但是,讀已提交併不能防止 [圖 7-1](../img/fig7-1.png) 中兩個計數器增量之間的競爭狀態。在這種情況下,第二次寫入發生在第一個事務提交後,所以它不是一個髒寫。這仍然是不正確的,但是出於不同的原因,在 “[防止丟失更新](#防止丟失更新)” 中將討論如何使這種計數器增量安全。 - 但是,讀已提交併不能防止 [圖 7-1](/img/fig7-1.png) 中兩個計數器增量之間的競爭狀態。在這種情況下,第二次寫入發生在第一個事務提交後,所以它不是一個髒寫。這仍然是不正確的,但是出於不同的原因,在 “[防止丟失更新](#防止丟失更新)” 中將討論如何使這種計數器增量安全。
![](../img/fig7-5.png) ![](/img/fig7-5.png)
**圖 7-5 如果存在髒寫,來自不同事務的衝突寫入可能會混淆在一起** **圖 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】。 [^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 觀察資料庫處於不一致的狀態** **圖 7-6 讀取偏差Alice 觀察資料庫處於不一致的狀態**
@ -297,7 +298,7 @@ Alice 在銀行有 1000 美元的儲蓄,分為兩個賬戶,每個 500 美元
這種異常被稱為 **不可重複讀nonrepeatable read****讀取偏差read skew**:如果 Alice 在事務結束時再次讀取賬戶 1 的餘額她將看到與她之前的查詢中看到的不同的值600 美元)。在讀已提交的隔離條件下,**不可重複讀** 被認為是可接受的Alice 看到的帳戶餘額確實在閱讀時已經提交了。 這種異常被稱為 **不可重複讀nonrepeatable read****讀取偏差read skew**:如果 Alice 在事務結束時再次讀取賬戶 1 的餘額她將看到與她之前的查詢中看到的不同的值600 美元)。在讀已提交的隔離條件下,**不可重複讀** 被認為是可接受的Alice 看到的帳戶餘額確實在閱讀時已經提交了。
> 不幸的是,術語 **偏差skew** 這個詞是過載的:以前使用它是因為熱點的不平衡工作量(請參閱 “[負載偏斜與熱點消除](ch6.md#負載偏斜與熱點消除)”),而這裡偏差意味著異常的時序。 > 不幸的是,術語 **偏差skew** 這個詞是過載的:以前使用它是因為熱點的不平衡工作量(請參閱 “[負載偏斜與熱點消除](/tw/ch6#負載偏斜與熱點消除)”),而這裡偏差意味著異常的時序。
對於 Alice 的情況,這不是一個長期持續的問題。因為如果她幾秒鐘後重新整理銀行網站的頁面,她很可能會看到一致的帳戶餘額。但是有些情況下,不能容忍這種暫時的不一致: 對於 Alice 的情況,這不是一個長期持續的問題。因為如果她幾秒鐘後重新整理銀行網站的頁面,她很可能會看到一致的帳戶餘額。但是有些情況下,不能容忍這種暫時的不一致:
@ -307,7 +308,7 @@ Alice 在銀行有 1000 美元的儲蓄,分為兩個賬戶,每個 500 美元
* 分析查詢和完整性檢查 * 分析查詢和完整性檢查
有時,你可能需要執行一個查詢,掃描大部分的資料庫。這樣的查詢在分析中很常見(請參閱 “[事務處理還是分析?](ch3.md#事務處理還是分析?)”),也可能是定期完整性檢查(即監視資料損壞)的一部分。如果這些查詢在不同時間點觀察資料庫的不同部分,則可能會返回毫無意義的結果。 有時,你可能需要執行一個查詢,掃描大部分的資料庫。這樣的查詢在分析中很常見(請參閱 “[事務處理還是分析?](/tw/ch3#事務處理還是分析?)”),也可能是定期完整性檢查(即監視資料損壞)的一部分。如果這些查詢在不同時間點觀察資料庫的不同部分,則可能會返回毫無意義的結果。
**快照隔離snapshot isolation**【28】是這個問題最常見的解決方案。想法是每個事務都從資料庫的 **一致快照consistent snapshot** 中讀取 —— 也就是說,事務可以看到事務開始時在資料庫中提交的所有資料。即使這些資料隨後被另一個事務更改,每個事務也只能看到該特定時間點的舊資料。 **快照隔離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 來實現 **讀已提交** 隔離級別。一種典型的方法是 **讀已提交** 為每個查詢使用單獨的快照,而 **快照隔離** 對整個事務使用相同的快照。 如果一個數據庫只需要提供 **讀已提交** 的隔離級別,而不提供 **快照隔離**,那麼保留一個物件的兩個版本就足夠了:已提交的版本和被覆蓋但尚未提交的版本。不過支援快照隔離的儲存引擎通常也使用 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 溢位(回捲)不會影響到資料。 [^vii]: 事實上,事務 ID 是 32 位整數,所以大約會在 40 億次事務之後溢位。PostgreSQL 的 Vacuum 過程會清理老舊的事務 ID確保事務 ID 溢位(回捲)不會影響到資料。
![](../img/fig7-7.png) ![](/img/fig7-7.png)
**圖 7-7 使用多版本物件實現快照隔離** **圖 7-7 使用多版本物件實現快照隔離**
@ -335,7 +336,7 @@ Alice 在銀行有 1000 美元的儲蓄,分為兩個賬戶,每個 500 美元
[^譯註ii]: 在 PostgreSQL 中,`created_by` 的實際名稱為 `xmin``deleted_by` 的實際名稱為 `xmax` [^譯註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在當前事務開始之後開始的的事務所做的任何寫入都被忽略而不管這些事務是否已經提交。 3. 由具有較晚事務 ID在當前事務開始之後開始的的事務所做的任何寫入都被忽略而不管這些事務是否已經提交。
4. 所有其他寫入,對應用都是可見的。 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】。 在實踐中許多實現細節決定了多版本併發控制的效能。例如如果同一物件的不同版本可以放入同一個頁面中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 樹;它們只能建立新的樹根。但這種方法也需要一個負責壓縮和垃圾收集的後臺程序。 使用僅追加的 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** 了前面的寫入)這種模式發生在各種不同的情況下: 如果應用從資料庫中讀取一些值,修改它並寫回修改的值(讀取 - 修改 - 寫入序列),則可能會發生丟失更新的問題。如果兩個事務同時執行,則其中一個的修改可能會丟失,因為第二個寫入的內容並沒有包括第一個事務的修改(有時會說後面寫入 **狠揍clobber** 了前面的寫入)這種模式發生在各種不同的情況下:
@ -399,7 +400,7 @@ UPDATE counters SET value = value + 1 WHERE key = 'foo';
類似地,像 MongoDB 這樣的文件資料庫提供了對 JSON 文件的一部分進行本地修改的原子操作Redis 提供了修改資料結構(如優先順序佇列)的原子操作。並不是所有的寫操作都可以用原子操作的方式來表達,例如 wiki 頁面的更新涉及到任意文字編輯 [^viii],但是在可以使用原子操作的情況下,它們通常是最好的選擇。 類似地,像 MongoDB 這樣的文件資料庫提供了對 JSON 文件的一部分進行本地修改的原子操作Redis 提供了修改資料結構(如優先順序佇列)的原子操作。並不是所有的寫操作都可以用原子操作的方式來表達,例如 wiki 頁面的更新涉及到任意文字編輯 [^viii],但是在可以使用原子操作的情況下,它們通常是最好的選擇。
[^viii]: 將文字文件的編輯表示為原子的變化流是可能的,儘管相當複雜。請參閱 “[自動衝突解決](ch5.md#自動衝突解決)”。 [^viii]: 將文字文件的編輯表示為原子的變化流是可能的,儘管相當複雜。請參閱 “[自動衝突解決](/tw/ch5#自動衝突解決)”。
原子操作通常透過在讀取物件時,獲取其上的排它鎖來實現。以便更新完成之前沒有其他事務可以讀取它。這種技術有時被稱為 **遊標穩定性cursor stability**【36,37】。另一個選擇是簡單地強制所有的原子操作在單一執行緒上執行。 原子操作通常透過在讀取物件時,獲取其上的排它鎖來實現。以便更新完成之前沒有其他事務可以讀取它。這種技術有時被稱為 **遊標穩定性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】。 原子操作可以在複製的上下文中很好地工作,尤其當它們具有可交換性時(即,可以在不同的副本上以不同的順序應用它們,且仍然可以得到相同的結果)。例如,遞增計數器或向集合新增元素是可交換的操作。這是 Riak 2.0 資料型別背後的思想它可以防止複製副本丟失更新。當不同的客戶端同時更新一個值時Riak 自動將更新合併在一起以免丟失更新【39】。
另一方面最後寫入勝利LWW的衝突解決方法很容易丟失更新如 “[最後寫入勝利(丟棄併發寫入)](ch5.md#最後寫入勝利(丟棄併發寫入))” 中所述。不幸的是LWW 是許多複製資料庫中的預設方案。 另一方面最後寫入勝利LWW的衝突解決方法很容易丟失更新如 “[最後寫入勝利(丟棄併發寫入)](/tw/ch5#最後寫入勝利(丟棄併發寫入))” 中所述。不幸的是LWW 是許多複製資料庫中的預設方案。
### 寫入偏差與幻讀 ### 寫入偏差與幻讀
@ -470,9 +471,9 @@ UPDATE wiki_pages SET content = '新內容'
首先想象一下這個例子你正在為醫院寫一個醫生輪班管理程式。醫院通常會同時要求幾位醫生待命但底線是至少有一位醫生在待命。醫生可以放棄他們的班次例如如果他們自己生病了只要至少有一個同事在這一班中繼續工作【40,41】。 首先想象一下這個例子你正在為醫院寫一個醫生輪班管理程式。醫院通常會同時要求幾位醫生待命但底線是至少有一位醫生在待命。醫生可以放棄他們的班次例如如果他們自己生病了只要至少有一個同事在這一班中繼續工作【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 寫入偏差導致應用程式錯誤的示例** **圖 7-8 寫入偏差導致應用程式錯誤的示例**
@ -595,7 +596,7 @@ COMMIT;
- **兩階段鎖定2PL, two-phase locking**,幾十年來唯一可行的選擇(請參閱 “[兩階段鎖定](#兩階段鎖定)”) - **兩階段鎖定2PL, two-phase locking**,幾十年來唯一可行的選擇(請參閱 “[兩階段鎖定](#兩階段鎖定)”)
- 樂觀併發控制技術,例如 **可序列化快照隔離**serializable snapshot isolation請參閱 “[可序列化快照隔離](#可序列化快照隔離)”) - 樂觀併發控制技術,例如 **可序列化快照隔離**serializable snapshot isolation請參閱 “[可序列化快照隔離](#可序列化快照隔離)”)
現在將主要在單節點資料庫的背景下討論這些技術;在 [第九章](ch9.md) 中,我們將研究如何將它們推廣到涉及分散式系統中多個節點的事務。 現在將主要在單節點資料庫的背景下討論這些技術;在 [第九章](/tw/ch9) 中,我們將研究如何將它們推廣到涉及分散式系統中多個節點的事務。
### 真的序列執行 ### 真的序列執行
@ -605,8 +606,8 @@ COMMIT;
兩個進展引發了這個反思: 兩個進展引發了這個反思:
- RAM 足夠便宜了,許多場景現在都可以將完整的活躍資料集儲存在記憶體中(請參閱 “[在記憶體中儲存一切](ch3.md#在記憶體中儲存一切)”)。當事務需要訪問的所有資料都在記憶體中時,事務處理的執行速度要比等待資料從磁碟載入時快得多。 - RAM 足夠便宜了,許多場景現在都可以將完整的活躍資料集儲存在記憶體中(請參閱 “[在記憶體中儲存一切](/tw/ch3#在記憶體中儲存一切)”)。當事務需要訪問的所有資料都在記憶體中時,事務處理的執行速度要比等待資料從磁碟載入時快得多。
- 資料庫設計人員意識到 OLTP 事務通常很短,而且只進行少量的讀寫操作(請參閱 “[事務處理還是分析?](ch3.md#事務處理還是分析?)”)。相比之下,長時間執行的分析查詢通常是隻讀的,因此它們可以在序列執行迴圈之外的一致快照(使用快照隔離)上執行。 - 資料庫設計人員意識到 OLTP 事務通常很短,而且只進行少量的讀寫操作(請參閱 “[事務處理還是分析?](/tw/ch3#事務處理還是分析?)”)。相比之下,長時間執行的分析查詢通常是隻讀的,因此它們可以在序列執行迴圈之外的一致快照(使用快照隔離)上執行。
序列執行事務的方法在 VoltDB/H-Store、Redis 和 Datomic 中實現【46,47,48】。設計用於單執行緒執行的系統有時可以比支援併發的系統性能更好因為它可以避免鎖的協調開銷。但是其吞吐量僅限於單個 CPU 核的吞吐量。為了充分利用單一執行緒,需要有與傳統形式的事務不同的結構。 序列執行事務的方法在 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 的示例事務)** **圖 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】。 由於跨分割槽事務具有額外的協調開銷所以它們比單分割槽事務慢得多。VoltDB 報告的吞吐量大約是每秒 1000 個跨分割槽寫入比單分割槽吞吐量低幾個數量級並且不能透過增加更多的機器來增加吞吐量【49】。
事務是否可以是劃分至單個分割槽很大程度上取決於應用資料的結構。簡單的鍵值資料通常可以非常容易地進行分割槽,但是具有多個次級索引的資料可能需要大量的跨分割槽協調(請參閱 “[分割槽與次級索引](ch6.md#分割槽與次級索引)”)。 事務是否可以是劃分至單個分割槽很大程度上取決於應用資料的結構。簡單的鍵值資料通常可以非常容易地進行分割槽,但是具有多個次級索引的資料可能需要大量的跨分割槽協調(請參閱 “[分割槽與次級索引](/tw/ch6#分割槽與次級索引)”)。
#### 序列執行小結 #### 序列執行小結
@ -661,7 +662,7 @@ VoltDB 還使用儲存過程進行復制:但不是將事務的寫入結果從
- 寫入吞吐量必須低到能在單個 CPU 核上處理,如若不然,事務需要能劃分至單個分割槽,且不需要跨分割槽協調。 - 寫入吞吐量必須低到能在單個 CPU 核上處理,如若不然,事務需要能劃分至單個分割槽,且不需要跨分割槽協調。
- 跨分割槽事務是可能的,但是它們能被使用的程度有很大的限制。 - 跨分割槽事務是可能的,但是它們能被使用的程度有很大的限制。
[^x]: 如果事務需要訪問不在記憶體中的資料,最好的解決方案可能是中止事務,非同步地將資料提取到記憶體中,同時繼續處理其他事務,然後在資料載入完畢時重新啟動事務。這種方法被稱為 **反快取anti-caching**,正如前面在 “[在記憶體中儲存一切](ch3.md#在記憶體中儲存一切)” 中所述。 [^x]: 如果事務需要訪問不在記憶體中的資料,最好的解決方案可能是中止事務,非同步地將資料提取到記憶體中,同時繼續處理其他事務,然後在資料載入完畢時重新啟動事務。這種方法被稱為 **反快取anti-caching**,正如前面在 “[在記憶體中儲存一切](/tw/ch3#在記憶體中儲存一切)” 中所述。
### 兩階段鎖定 ### 兩階段鎖定
@ -671,14 +672,14 @@ VoltDB 還使用儲存過程進行復制:但不是將事務的寫入結果從
> #### 2PL不是2PC > #### 2PL不是2PC
> >
> 請注意雖然兩階段鎖定2PL聽起來非常類似於兩階段提交2PC但它們是完全不同的東西。我們將在 [第九章](ch9.md) 討論 2PC。 > 請注意雖然兩階段鎖定2PL聽起來非常類似於兩階段提交2PC但它們是完全不同的東西。我們將在 [第九章](/tw/ch9) 討論 2PC。
之前我們看到鎖通常用於防止髒寫(請參閱 “[沒有髒寫](#沒有髒寫)” 一節):如果兩個事務同時嘗試寫入同一個物件,則鎖可確保第二個寫入必須等到第一個寫入完成事務(中止或提交),然後才能繼續。 之前我們看到鎖通常用於防止髒寫(請參閱 “[沒有髒寫](#沒有髒寫)” 一節):如果兩個事務同時嘗試寫入同一個物件,則鎖可確保第二個寫入必須等到第一個寫入完成事務(中止或提交),然後才能繼續。
兩階段鎖定類似,但是鎖的要求更強得多。只要沒有寫入,就允許多個事務同時讀取同一個物件。但物件只要有寫入(修改或刪除),就需要 **獨佔訪問exclusive access** 許可權: 兩階段鎖定類似,但是鎖的要求更強得多。只要沒有寫入,就允許多個事務同時讀取同一個物件。但物件只要有寫入(修改或刪除),就需要 **獨佔訪問exclusive access** 許可權:
- 如果事務 A 讀取了一個物件,並且事務 B 想要寫入該物件,那麼 B 必須等到 A 提交或中止才能繼續(這確保 B 不能在 A 底下意外地改變物件)。 - 如果事務 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 提供了可序列化的性質,它可以防止早先討論的所有競爭條件,包括丟失更新和寫入偏差。 在 2PL 中,寫入不僅會阻塞其他寫入,也會阻塞讀,反之亦然。快照隔離使得 **讀不阻塞寫,寫也不阻塞讀**(請參閱 “[實現快照隔離](#實現快照隔離)”),這是 2PL 和快照隔離之間的關鍵區別。另一方面,因為 2PL 提供了可序列化的性質,它可以防止早先討論的所有競爭條件,包括丟失更新和寫入偏差。
@ -703,7 +704,7 @@ VoltDB 還使用儲存過程進行復制:但不是將事務的寫入結果從
傳統的關係資料庫不限制事務的持續時間,因為它們是為等待人類輸入的互動式應用而設計的。因此,當一個事務需要等待另一個事務時,等待的時長並沒有限制。即使你保證所有的事務都很短,如果有多個事務想要訪問同一個物件,那麼可能會形成一個佇列,所以事務可能需要等待幾個其他事務才能完成。 傳統的關係資料庫不限制事務的持續時間,因為它們是為等待人類輸入的互動式應用而設計的。因此,當一個事務需要等待另一個事務時,等待的時長並沒有限制。即使你保證所有的事務都很短,如果有多個事務想要訪問同一個物件,那麼可能會形成一個佇列,所以事務可能需要等待幾個其他事務才能完成。
因此,執行 2PL 的資料庫可能具有相當不穩定的延遲,如果在工作負載中存在爭用,那麼可能高百分位點處的響應會非常的慢(請參閱 “[描述效能](ch1.md#描述效能)”)。可能只需要一個緩慢的事務,或者一個訪問大量資料並獲取許多鎖的事務,就能把系統的其他部分拖慢,甚至迫使系統停機。當需要穩健的操作時,這種不穩定性是有問題的。 因此,執行 2PL 的資料庫可能具有相當不穩定的延遲,如果在工作負載中存在爭用,那麼可能高百分位點處的響應會非常的慢(請參閱 “[描述效能](/tw/ch1#描述效能)”)。可能只需要一個緩慢的事務,或者一個訪問大量資料並獲取許多鎖的事務,就能把系統的其他部分拖慢,甚至迫使系統停機。當需要穩健的操作時,這種不穩定性是有問題的。
基於鎖實現的讀已提交隔離級別可能發生死鎖,但在基於 2PL 實現的可序列化隔離級別中,它們會出現的頻繁的多(取決於事務的訪問模式)。這可能是一個額外的效能問題:當事務由於死鎖而被中止並被重試時,它需要從頭重做它的工作。如果死鎖很頻繁,這可能意味著巨大的浪費。 基於鎖實現的讀已提交隔離級別可能發生死鎖,但在基於 2PL 實現的可序列化隔離級別中,它們會出現的頻繁的多(取決於事務的訪問模式)。這可能是一個額外的效能問題:當事務由於死鎖而被中止並被重試時,它需要從頭重做它的工作。如果死鎖很頻繁,這可能意味著巨大的浪費。
@ -784,9 +785,9 @@ WHERE room_id = 123 AND
#### 檢測舊MVCC讀取 #### 檢測舊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 快照讀取過時的值** **圖 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 在可序列化快照隔離中,檢測一個事務何時修改另一個事務的讀取。** **圖 7-11 在可序列化快照隔離中,檢測一個事務何時修改另一個事務的讀取。**
在兩階段鎖定的上下文中,我們討論了索引範圍鎖(請參閱 “[索引範圍鎖](#索引範圍鎖)”),它允許資料庫鎖定與某個搜尋查詢匹配的所有行的訪問權,例如 `WHERE shift_id = 1234`。可以在這裡使用類似的技術,除了 SSI 鎖不會阻塞其他事務。 在兩階段鎖定的上下文中,我們討論了索引範圍鎖(請參閱 “[索引範圍鎖](#索引範圍鎖)”),它允許資料庫鎖定與某個搜尋查詢匹配的所有行的訪問權,例如 `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. 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. 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. 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. 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] > —— Kyle Kingsbury, Carly Rae Jepsen 《網路分割槽的危害》2013 年)[^譯著1]
---------
[TOC] 最近幾章中反覆出現的主題是,系統如何處理錯誤的事情。例如,我們討論了 **副本故障切換**(“[處理節點中斷](/tw/ch5#處理節點宕機)”),**複製延遲**(“[複製延遲問題](/tw/ch5#複製延遲問題)”)和事務控制(“[弱隔離級別](/tw/ch7#弱隔離級別)”)。當我們瞭解可能在實際系統中出現的各種邊緣情況時,我們會更好地處理它們。
最近幾章中反覆出現的主題是,系統如何處理錯誤的事情。例如,我們討論了 **副本故障切換**(“[處理節點中斷](ch5.md#處理節點宕機)”),**複製延遲**(“[複製延遲問題](ch5.md#複製延遲問題)”)和事務控制(“[弱隔離級別](ch7.md#弱隔離級別)”)。當我們瞭解可能在實際系統中出現的各種邊緣情況時,我們會更好地處理它們。
但是,儘管我們已經談了很多錯誤,但之前幾章仍然過於樂觀。現實更加黑暗。我們現在將悲觀主義最大化,假設任何可能出錯的東西 **都會** 出錯 [^i]。(經驗豐富的系統運維會告訴你,這是一個合理的假設。如果你問得好,他們可能會一邊治療心理創傷一邊告訴你一些可怕的故事) 但是,儘管我們已經談了很多錯誤,但之前幾章仍然過於樂觀。現實更加黑暗。我們現在將悲觀主義最大化,假設任何可能出錯的東西 **都會** 出錯 [^i]。(經驗豐富的系統運維會告訴你,這是一個合理的假設。如果你問得好,他們可能會一邊治療心理創傷一邊告訴你一些可怕的故事)
@ -24,7 +27,7 @@
使用分散式系統與在一臺計算機上編寫軟體有著根本的區別主要的區別在於有許多新穎和刺激的方法可以使事情出錯【1,2】。在這一章中我們將瞭解實踐中出現的問題理解我們能夠依賴和不可以依賴的東西。 使用分散式系統與在一臺計算機上編寫軟體有著根本的區別主要的區別在於有許多新穎和刺激的方法可以使事情出錯【1,2】。在這一章中我們將瞭解實踐中出現的問題理解我們能夠依賴和不可以依賴的東西。
最後,作為工程師,我們的任務是構建能夠完成工作的系統(即滿足使用者期望的保證),儘管一切都出錯了。在 [第九章](ch9.md) 中,我們將看看一些可以在分散式系統中提供這種保證的演算法的例子。但首先,在本章中,我們必須瞭解我們面臨的挑戰。 最後,作為工程師,我們的任務是構建能夠完成工作的系統(即滿足使用者期望的保證),儘管一切都出錯了。在 [第九章](/tw/ch9) 中,我們將看看一些可以在分散式系統中提供這種保證的演算法的例子。但首先,在本章中,我們必須瞭解我們面臨的挑戰。
本章對分散式系統中可能出現的問題進行徹底的悲觀和沮喪的總結。我們將研究網路的問題(“[不可靠的網路](#不可靠的網路)”); 時鐘和時序問題(“[不可靠的時鐘](#不可靠的時鐘)”); 我們將討論他們可以避免的程度。所有這些問題的後果都是困惑的,所以我們將探索如何思考一個分散式系統的狀態,以及如何推理發生的事情(“[知識、真相與謊言](#知識、真相與謊言)”)。 本章對分散式系統中可能出現的問題進行徹底的悲觀和沮喪的總結。我們將研究網路的問題(“[不可靠的網路](#不可靠的網路)”); 時鐘和時序問題(“[不可靠的時鐘](#不可靠的時鐘)”); 我們將討論他們可以避免的程度。所有這些問題的後果都是困惑的,所以我們將探索如何思考一個分散式系統的狀態,以及如何推理發生的事情(“[知識、真相與謊言](#知識、真相與謊言)”)。
@ -67,11 +70,11 @@
* 系統越大其元件之一就越有可能壞掉。隨著時間的推移壞掉的東西得到修復新的東西又壞掉但是在一個有成千上萬個節點的系統中有理由認為總是有一些東西是壞掉的【7】。當錯誤處理的策略只由簡單放棄組成時一個大的系統最終會花費大量時間從錯誤中恢復而不是做有用的工作【8】。 * 系統越大其元件之一就越有可能壞掉。隨著時間的推移壞掉的東西得到修復新的東西又壞掉但是在一個有成千上萬個節點的系統中有理由認為總是有一些東西是壞掉的【7】。當錯誤處理的策略只由簡單放棄組成時一個大的系統最終會花費大量時間從錯誤中恢復而不是做有用的工作【8】。
* 如果系統可以容忍發生故障的節點,並繼續保持整體工作狀態,那麼這對於運營和維護非常有用:例如,可以執行滾動升級(請參閱 [第四章](ch4.md)),一次重新啟動一個節點,同時繼續給使用者提供不中斷的服務。在雲環境中,如果一臺虛擬機器執行不佳,可以殺死它並請求一臺新的虛擬機器(希望新的虛擬機器速度更快)。 * 如果系統可以容忍發生故障的節點,並繼續保持整體工作狀態,那麼這對於運營和維護非常有用:例如,可以執行滾動升級(請參閱 [第四章](/tw/ch4)),一次重新啟動一個節點,同時繼續給使用者提供不中斷的服務。在雲環境中,如果一臺虛擬機器執行不佳,可以殺死它並請求一臺新的虛擬機器(希望新的虛擬機器速度更快)。
* 在地理位置分散的部署中(保持資料在地理位置上接近使用者以減少訪問延遲),通訊很可能透過網際網路進行,與本地網路相比,通訊速度緩慢且不可靠。超級計算機通常假設它們的所有節點都靠近在一起。 * 在地理位置分散的部署中(保持資料在地理位置上接近使用者以減少訪問延遲),通訊很可能透過網際網路進行,與本地網路相比,通訊速度緩慢且不可靠。超級計算機通常假設它們的所有節點都靠近在一起。
如果要使分散式系統工作,就必須接受部分故障的可能性,並在軟體中建立容錯機制。換句話說,我們需要從不可靠的元件構建一個可靠的系統(正如 “[可靠性](ch1.md#可靠性)” 中所討論的那樣,沒有完美的可靠性,所以我們需要理解我們可以實際承諾的極限)。 如果要使分散式系統工作,就必須接受部分故障的可能性,並在軟體中建立容錯機制。換句話說,我們需要從不可靠的元件構建一個可靠的系統(正如 “[可靠性](/tw/ch1#可靠性)” 中所討論的那樣,沒有完美的可靠性,所以我們需要理解我們可以實際承諾的極限)。
即使在只有少數節點的小型系統中,考慮部分故障也是很重要的。在一個小系統中,很可能大部分元件在大部分時間都正常工作。然而,遲早會有一部分系統出現故障,軟體必須以某種方式處理。故障處理必須是軟體設計的一部分,並且作為軟體的運維,你需要知道在發生故障的情況下,軟體可能會表現出怎樣的行為。 即使在只有少數節點的小型系統中,考慮部分故障也是很重要的。在一個小系統中,很可能大部分元件在大部分時間都正常工作。然而,遲早會有一部分系統出現故障,軟體必須以某種方式處理。故障處理必須是軟體設計的一部分,並且作為軟體的運維,你需要知道在發生故障的情況下,軟體可能會表現出怎樣的行為。
@ -86,16 +89,16 @@
> >
> 雖然這個系統可以比它的底層部分更可靠但它的可靠性總是有限的。例如糾錯碼可以處理少量的單位元錯誤但是如果你的訊號被幹擾所淹沒那麼透過通道可以得到多少資料是有根本性的限制的【13】。TCP 可以隱藏資料包的丟失,重複和重新排序,但是它不能神奇地消除網路中的延遲。 > 雖然這個系統可以比它的底層部分更可靠但它的可靠性總是有限的。例如糾錯碼可以處理少量的單位元錯誤但是如果你的訊號被幹擾所淹沒那麼透過通道可以得到多少資料是有根本性的限制的【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. 請求可能已經丟失(可能有人拔掉了網線)。 1. 請求可能已經丟失(可能有人拔掉了網線)。
2. 請求可能正在排隊,稍後將交付(也許網路或接收方過載)。 2. 請求可能正在排隊,稍後將交付(也許網路或接收方過載)。
@ -104,7 +107,7 @@
5. 遠端節點可能已經處理了請求,但是網路上的響應已經丟失(可能是網路交換機配置錯誤)。 5. 遠端節點可能已經處理了請求,但是網路上的響應已經丟失(可能是網路交換機配置錯誤)。
6. 遠端節點可能已經處理了請求,但是響應已經被延遲,並且稍後將被傳遞(可能是網路或者你自己的機器過載)。 6. 遠端節點可能已經處理了請求,但是響應已經被延遲,並且稍後將被傳遞(可能是網路或者你自己的機器過載)。
![](../img/fig8-1.png) ![](/img/fig8-1.png)
**圖 8-1 如果傳送請求並沒有得到響應則無法區分a請求是否丟失b遠端節點是否關閉c響應是否丟失。** **圖 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】。如果軟體被置於意料之外的情況下它可能會做出出乎意料的事情。 如果網路故障的錯誤處理沒有定義與測試武斷地講各種錯誤可能都會發生例如即使網路恢復【20】叢集可能會發生 **死鎖**永久無法為請求提供服務甚至可能會刪除所有的資料【21】。如果軟體被置於意料之外的情況下它可能會做出出乎意料的事情。
處理網路故障並不意味著容忍它們:如果你的網路通常是相當可靠的,一個有效的方法可能是當你的網路遇到問題時,簡單地向用戶顯示一條錯誤資訊。但是,你確實需要知道你的軟體如何應對網路問題,並確保系統能夠從中恢復。有意識地觸發網路問題並測試系統響應(這是 Chaos Monkey 背後的想法;請參閱 “[可靠性](ch1.md#可靠性)”)。 處理網路故障並不意味著容忍它們:如果你的網路通常是相當可靠的,一個有效的方法可能是當你的網路遇到問題時,簡單地向用戶顯示一條錯誤資訊。但是,你確實需要知道你的軟體如何應對網路問題,並確保系統能夠從中恢復。有意識地觸發網路問題並測試系統響應(這是 Chaos Monkey 背後的想法;請參閱 “[可靠性](/tw/ch1#可靠性)”)。
### 檢測故障 ### 檢測故障
許多系統需要自動檢測故障節點。例如: 許多系統需要自動檢測故障節點。例如:
* 負載平衡器需要停止向已死亡的節點轉發請求(從輪詢列表移出,即 out of rotation * 負載平衡器需要停止向已死亡的節點轉發請求(從輪詢列表移出,即 out of rotation
* 在單主複製功能的分散式資料庫中,如果主庫失效,則需要將從庫之一升級為新主庫(請參閱 “[處理節點宕機](ch5.md#處理節點宕機)”)。 * 在單主複製功能的分散式資料庫中,如果主庫失效,則需要將從庫之一升級為新主庫(請參閱 “[處理節點宕機](/tw/ch5#處理節點宕機)”)。
不幸的是,網路的不確定性使得很難判斷一個節點是否工作。在某些特定的情況下,你可能會收到一些反饋資訊,明確告訴你某些事情沒有成功: 不幸的是,網路的不確定性使得很難判斷一個節點是否工作。在某些特定的情況下,你可能會收到一些反饋資訊,明確告訴你某些事情沒有成功:
@ -154,7 +157,7 @@
長時間的超時意味著長時間等待,直到一個節點被宣告死亡(在這段時間內,使用者可能不得不等待,或者看到錯誤資訊)。短的超時可以更快地檢測到故障,但有更高地風險誤將一個節點宣佈為失效,而該節點實際上只是暫時地變慢了(例如由於節點或網路上的負載峰值)。 長時間的超時意味著長時間等待,直到一個節點被宣告死亡(在這段時間內,使用者可能不得不等待,或者看到錯誤資訊)。短的超時可以更快地檢測到故障,但有更高地風險誤將一個節點宣佈為失效,而該節點實際上只是暫時地變慢了(例如由於節點或網路上的負載峰值)。
過早地宣告一個節點已經死了是有問題的:如果這個節點實際上是活著的,並且正在執行一些動作(例如,傳送一封電子郵件),而另一個節點接管,那麼這個動作可能會最終執行兩次。我們將在 “[知識、真相與謊言](#知識、真相與謊言)” 以及 [第九章](ch9.md) 和 [第十一章](ch11.md) 中更詳細地討論這個問題。 過早地宣告一個節點已經死了是有問題的:如果這個節點實際上是活著的,並且正在執行一些動作(例如,傳送一封電子郵件),而另一個節點接管,那麼這個動作可能會最終執行兩次。我們將在 “[知識、真相與謊言](#知識、真相與謊言)” 以及 [第九章](/tw/ch9) 和 [第十一章](/tw/ch11) 中更詳細地討論這個問題。
當一個節點被宣告死亡時,它的職責需要轉移到其他節點,這會給其他節點和網路帶來額外的負擔。如果系統已經處於高負荷狀態,則過早宣告節點死亡會使問題更嚴重。特別是如果節點實際上沒有死亡,只是由於過載導致其響應緩慢;這時將其負載轉移到其他節點可能會導致 **級聯失效**(即 cascading failure表示在極端情況下所有節點都宣告對方死亡所有節點都將停止工作 當一個節點被宣告死亡時,它的職責需要轉移到其他節點,這會給其他節點和網路帶來額外的負擔。如果系統已經處於高負荷狀態,則過早宣告節點死亡會使問題更嚴重。特別是如果節點實際上沒有死亡,只是由於過載導致其響應緩慢;這時將其負載轉移到其他節點可能會導致 **級聯失效**(即 cascading failure表示在極端情況下所有節點都宣告對方死亡所有節點都將停止工作
@ -166,12 +169,12 @@
在駕駛汽車時由於交通擁堵道路交通網路的通行時間往往不盡相同。同樣計算機網路上資料包延遲的可變性通常是由於排隊【25】 在駕駛汽車時由於交通擁堵道路交通網路的通行時間往往不盡相同。同樣計算機網路上資料包延遲的可變性通常是由於排隊【25】
* 如果多個不同的節點同時嘗試將資料包傳送到同一目的地,則網路交換機必須將它們排隊並將它們逐個送入目標網路鏈路(如 [圖 8-2](../img/fig8-2.png) 所示)。在繁忙的網路鏈路上,資料包可能需要等待一段時間才能獲得一個插槽(這稱為網路擁塞)。如果傳入的資料太多,交換機佇列填滿,資料包將被丟棄,因此需要重新發送資料包 - 即使網路執行良好。 * 如果多個不同的節點同時嘗試將資料包傳送到同一目的地,則網路交換機必須將它們排隊並將它們逐個送入目標網路鏈路(如 [圖 8-2](/img/fig8-2.png) 所示)。在繁忙的網路鏈路上,資料包可能需要等待一段時間才能獲得一個插槽(這稱為網路擁塞)。如果傳入的資料太多,交換機佇列填滿,資料包將被丟棄,因此需要重新發送資料包 - 即使網路執行良好。
* 當資料包到達目標機器時,如果所有 CPU 核心當前都處於繁忙狀態,則來自網路的傳入請求將被作業系統排隊,直到應用程式準備好處理它為止。根據機器上的負載,這可能需要一段任意的時間。 * 當資料包到達目標機器時,如果所有 CPU 核心當前都處於繁忙狀態,則來自網路的傳入請求將被作業系統排隊,直到應用程式準備好處理它為止。根據機器上的負載,這可能需要一段任意的時間。
* 在虛擬化環境中,正在執行的作業系統經常暫停幾十毫秒,因為另一個虛擬機器正在使用 CPU 核心。在這段時間內,虛擬機器不能從網路中消耗任何資料,所以傳入的資料被虛擬機器監視器 【26】排隊緩衝進一步增加了網路延遲的可變性。 * 在虛擬化環境中,正在執行的作業系統經常暫停幾十毫秒,因為另一個虛擬機器正在使用 CPU 核心。在這段時間內,虛擬機器不能從網路中消耗任何資料,所以傳入的資料被虛擬機器監視器 【26】排隊緩衝進一步增加了網路延遲的可變性。
* TCP 執行 **流量控制**flow control也稱為 **擁塞避免**,即 congestion avoidance**背壓**,即 backpressure其中節點會限制自己的傳送速率以避免網路鏈路或接收節點過載【27】。這意味著甚至在資料進入網路之前在傳送者處就需要進行額外的排隊。 * TCP 執行 **流量控制**flow control也稱為 **擁塞避免**,即 congestion avoidance**背壓**,即 backpressure其中節點會限制自己的傳送速率以避免網路鏈路或接收節點過載【27】。這意味著甚至在資料進入網路之前在傳送者處就需要進行額外的排隊。
![](../img/fig8-2.png) ![](/img/fig8-2.png)
**圖 8-2 如果有多臺機器將網路流量傳送到同一目的地,則其交換機佇列可能會被填滿。在這裡,埠 1,2 和 4 都試圖傳送資料包到埠 3** **圖 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. 這個快取條目何時到期? 7. 這個快取條目何時到期?
8. 日誌檔案中此錯誤訊息的時間戳是什麼? 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 客戶端 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】。此方案可能導致一定數量的資料被悄悄丟棄而未嚮應用報告任何錯誤。 * 資料庫寫入可能會神秘地消失具有滯後時鐘的節點無法覆蓋之前具有快速時鐘的節點寫入的值直到節點之間的時鐘偏差消逝【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】。 * 兩個節點很可能獨立地生成具有相同時間戳的寫入,特別是在時鐘僅具有毫秒解析度的情況下。為了解決這樣的衝突,還需要一個額外的 **決勝值**tiebreaker可以簡單地是一個大隨機數但這種方法也可能會導致違背因果關係【53】。
因此,儘管透過保留 “最近” 的值並放棄其他值來解決衝突是很誘惑人的,但是要注意,“最近” 的定義取決於本地的 **日曆時鐘**,這很可能是不正確的。即使用嚴格同步的 NTP 時鐘,一個數據包也可能在時間戳 100 毫秒(根據傳送者的時鐘)時傳送,並在時間戳 99 毫秒(根據接收者的時鐘)處到達 —— 看起來好像資料包在傳送之前已經到達,這是不可能的。 因此,儘管透過保留 “最近” 的值並放棄其他值來解決衝突是很誘惑人的,但是要注意,“最近” 的定義取決於本地的 **日曆時鐘**,這很可能是不正確的。即使用嚴格同步的 NTP 時鐘,一個數據包也可能在時間戳 100 毫秒(根據傳送者的時鐘)時傳送,並在時間戳 99 毫秒(根據接收者的時鐘)處到達 —— 看起來好像資料包在傳送之前已經到達,這是不可能的。
NTP 同步是否能足夠準確,以至於這種不正確的排序不會發生?也許不能,因為 NTP 的同步精度本身,除了石英鐘漂移這類誤差源之外,還受到網路往返時間的限制。為了進行正確的排序,你需要一個比測量物件(即網路延遲)要精確得多的時鐘。 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則該寫入對於快照事務是不可見的。在單節點資料庫上一個簡單的計數器就足以生成事務 ID。
但是當資料庫分佈在許多機器上,也許可能在多個數據中心中時,由於需要協調,(跨所有分割槽)全域性單調遞增的事務 ID 會很難生成。事務 ID 必須反映因果關係:如果事務 B 讀取由事務 A 寫入的值,則 B 必須具有比 A 更大的事務 ID否則快照就無法保持一致。在有大量的小規模、高頻率的事務情景下在分散式系統中建立事務 ID 成為一個難以處理的瓶頸 [^vi]。 但是當資料庫分佈在許多機器上,也許可能在多個數據中心中時,由於需要協調,(跨所有分割槽)全域性單調遞增的事務 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 嗎?如果我們能夠獲得足夠好的同步性,那麼這種方法將具有很合適的屬性:更晚的事務會有更大的時間戳。當然,問題在於時鐘精度的不確定性。 我們可以使用同步時鐘的時間戳作為事務 ID 嗎?如果我們能夠獲得足夠好的同步性,那麼這種方法將具有很合適的屬性:更晚的事務會有更大的時間戳。當然,問題在於時鐘精度的不確定性。
@ -419,7 +422,7 @@ while (true) {
> #### 即時是真的嗎? > #### 即時是真的嗎?
> >
> 在嵌入式系統中,即時是指系統經過精心設計和測試,以滿足所有情況下的特定時間保證。這個含義與 Web 上對即時術語的模糊使用相反,後者描述了伺服器將資料推送到客戶端以及沒有嚴格的響應時間限制的流處理(見 [第十一章](ch11.md))。 > 在嵌入式系統中,即時是指系統經過精心設計和測試,以滿足所有情況下的特定時間保證。這個含義與 Web 上對即時術語的模糊使用相反,後者描述了伺服器將資料推送到客戶端以及沒有嚴格的響應時間限制的流處理(見 [第十一章](/tw/ch11))。
例如,如果車載感測器檢測到當前正在經歷碰撞,你肯定不希望安全氣囊釋放系統因為 GC 暫停而延遲彈出。 例如,如果車載感測器檢測到當前正在經歷碰撞,你肯定不希望安全氣囊釋放系統因為 GC 暫停而延遲彈出。
@ -435,7 +438,7 @@ while (true) {
一個新興的想法是將 GC 暫停視為一個節點的短暫計劃中斷,並在這個節點收集其垃圾的同時,讓其他節點處理來自客戶端的請求。如果執行時可以警告應用程式一個節點很快需要 GC 暫停,那麼應用程式可以停止向該節點發送新的請求,等待它完成處理未完成的請求,然後在沒有請求正在進行時執行 GC。這個技巧向客戶端隱藏了 GC 暫停並降低了響應時間的高百分比【70,71】。一些對延遲敏感的金融交易系統【72】使用這種方法。 一個新興的想法是將 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 後的節點最初甚至沒有意識到已經經過了整整一分鐘,而且自己已被宣告死亡。從它自己的角度來看,從最後一次與其他節點交談以來,幾乎沒有經過任何時間。 第三種情況,想象一個正在經歷長時間 **垃圾收集暫停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 認為它仍然具有有效的租約,即使它已經過期,從而破壞了儲存中的檔案** **圖 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 只允許以增加防護令牌的順序進行寫操作,從而保證儲存安全** **圖 8-5 只允許以增加防護令牌的順序進行寫操作,從而保證儲存安全**
我們假設每次鎖定伺服器授予鎖或租約時,它還會返回一個 **防護令牌fencing token**,這個數字在每次授予鎖定時都會增加(例如,由鎖定服務增加)。然後,我們可以要求客戶端每次向儲存服務傳送寫入請求時,都必須包含當前的防護令牌。 我們假設每次鎖定伺服器授予鎖或租約時,它還會返回一個 **防護令牌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】。 如果將 ZooKeeper 用作鎖定服務,則可將事務標識 `zxid` 或節點版本 `cversion` 用作防護令牌。由於它們保證單調遞增因此它們具有所需的屬性【74】。
@ -514,7 +517,7 @@ while (true) {
> ### 拜占庭將軍問題 > ### 拜占庭將軍問題
> >
> 拜占庭將軍問題是對所謂 “兩將軍問題” 的泛化【78】它想象兩個將軍需要就戰鬥計劃達成一致的情況。由於他們在兩個不同的地點建立了營地他們只能透過信使進行溝通信使有時會被延遲或丟失就像網路中的資訊包一樣。我們將在 [第九章](ch9.md) 討論這個共識問題。 > 拜占庭將軍問題是對所謂 “兩將軍問題” 的泛化【78】它想象兩個將軍需要就戰鬥計劃達成一致的情況。由於他們在兩個不同的地點建立了營地他們只能透過信使進行溝通信使有時會被延遲或丟失就像網路中的資訊包一樣。我們將在 [第九章](/tw/ch9) 討論這個共識問題。
> >
> 在這個問題的拜占庭版本里,有 n 位將軍需要同意,他們的努力因為有一些叛徒在他們中間而受到阻礙。大多數的將軍都是忠誠的,因而發出了真實的資訊,但是叛徒可能會試圖透過傳送虛假或不真實的資訊來欺騙和混淆他人(在試圖保持未被發現的同時)。事先並不知道叛徒是誰。 > 在這個問題的拜占庭版本里,有 n 位將軍需要同意,他們的努力因為有一些叛徒在他們中間而受到阻礙。大多數的將軍都是忠誠的,因而發出了真實的資訊,但是叛徒可能會試圖透過傳送虛假或不真實的資訊來欺騙和混淆他人(在試圖保持未被發現的同時)。事先並不知道叛徒是誰。
> >
@ -543,7 +546,7 @@ Web 應用程式確實需要預期受終端使用者控制的客戶端(如 Web
### 系統模型與現實 ### 系統模型與現實
已經有很多演算法被設計以解決分散式系統問題 —— 例如,我們將在 [第九章](ch9.md) 討論共識問題的解決方案。為了有用,這些演算法需要容忍我們在本章中討論的分散式系統的各種故障。 已經有很多演算法被設計以解決分散式系統問題 —— 例如,我們將在 [第九章](/tw/ch9) 討論共識問題的解決方案。為了有用,這些演算法需要容忍我們在本章中討論的分散式系統的各種故障。
演算法的編寫方式不應該過分依賴於執行的硬體和軟體配置的細節。這就要求我們以某種方式將我們期望在系統中發生的錯誤形式化。我們透過定義一個系統模型來做到這一點,這個模型是一個抽象,描述一個演算法可以假設的事情。 演算法的編寫方式不應該過分依賴於執行的硬體和軟體配置的細節。這就要求我們以某種方式將我們期望在系統中發生的錯誤形式化。我們透過定義一個系統模型來做到這一點,這個模型是一個抽象,描述一個演算法可以假設的事情。
@ -619,7 +622,7 @@ Web 應用程式確實需要預期受終端使用者控制的客戶端(如 Web
例如,在崩潰 - 恢復crash-recovery模型中的演算法通常假設穩定儲存器中的資料在崩潰後可以倖存。但是如果磁碟上的資料被破壞或者由於硬體錯誤或錯誤配置導致資料被清除會發生什麼情況【91】如果伺服器存在韌體錯誤並且在重新啟動時無法識別其硬碟驅動器即使驅動器已正確連線到伺服器那又會發生什麼情況【92】 例如,在崩潰 - 恢復crash-recovery模型中的演算法通常假設穩定儲存器中的資料在崩潰後可以倖存。但是如果磁碟上的資料被破壞或者由於硬體錯誤或錯誤配置導致資料被清除會發生什麼情況【91】如果伺服器存在韌體錯誤並且在重新啟動時無法識別其硬碟驅動器即使驅動器已正確連線到伺服器那又會發生什麼情況【92】
法定人數演算法(請參閱 “[讀寫的法定人數](ch5.md#讀寫的法定人數)”)依賴節點來記住它聲稱儲存的資料。如果一個節點可能患有健忘症,忘記了以前儲存的資料,這會打破法定條件,從而破壞演算法的正確性。也許需要一個新的系統模型,在這個模型中,我們假設穩定的儲存大多能在崩潰後倖存,但有時也可能會丟失。但是那個模型就變得更難以推理了。 法定人數演算法(請參閱 “[讀寫的法定人數](/tw/ch5#讀寫的法定人數)”)依賴節點來記住它聲稱儲存的資料。如果一個節點可能患有健忘症,忘記了以前儲存的資料,這會打破法定條件,從而破壞演算法的正確性。也許需要一個新的系統模型,在這個模型中,我們假設穩定的儲存大多能在崩潰後倖存,但有時也可能會丟失。但是那個模型就變得更難以推理了。
演算法的理論描述可以簡單宣稱一些事是不會發生的 —— 在非拜占庭式系統中,我們確實需要對可能發生和不可能發生的故障做出假設。然而,真實世界的實現,仍然會包括處理 “假設上不可能” 情況的程式碼,即使程式碼可能就是 `printf("Sucks to be you")``exit(666)`實際上也就是留給運維來擦屁股【93】。這可以說是計算機科學和軟體工程間的一個差異 演算法的理論描述可以簡單宣稱一些事是不會發生的 —— 在非拜占庭式系統中,我們確實需要對可能發生和不可能發生的故障做出假設。然而,真實世界的實現,仍然會包括處理 “假設上不可能” 情況的程式碼,即使程式碼可能就是 `printf("Sucks to be you")``exit(666)`實際上也就是留給運維來擦屁股【93】。這可以說是計算機科學和軟體工程間的一個差異
@ -644,13 +647,13 @@ Web 應用程式確實需要預期受終端使用者控制的客戶端(如 Web
如果你習慣於在理想化的數學完美的單機環境同一個操作總能確定地返回相同的結果中編寫軟體那麼轉向分散式系統的凌亂的物理現實可能會有些令人震驚。相反如果能夠在單臺計算機上解決一個問題那麼分散式系統工程師通常會認為這個問題是平凡的【5】現在單個計算機確實可以做很多事情【95】。如果你可以避免開啟潘多拉的盒子把東西放在一臺機器上那麼通常是值得的。 如果你習慣於在理想化的數學完美的單機環境同一個操作總能確定地返回相同的結果中編寫軟體那麼轉向分散式系統的凌亂的物理現實可能會有些令人震驚。相反如果能夠在單臺計算機上解決一個問題那麼分散式系統工程師通常會認為這個問題是平凡的【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. 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. 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? [^譯著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) > —— Jay Kreps, 關於 Kafka 與 Jepsen 的若干筆記 (2013)
---------------
[TOC] 正如 [第八章](/tw/ch8) 所討論的,分散式系統中的許多事情可能會出錯。處理這種故障的最簡單方法是簡單地讓整個服務失效,並向用戶顯示錯誤訊息。如果無法接受這個解決方案,我們就需要找到容錯的方法 —— 即使某些內部元件出現故障,服務也能正常執行。
正如 [第八章](ch8.md) 所討論的,分散式系統中的許多事情可能會出錯。處理這種故障的最簡單方法是簡單地讓整個服務失效,並向用戶顯示錯誤訊息。如果無法接受這個解決方案,我們就需要找到容錯的方法 —— 即使某些內部元件出現故障,服務也能正常執行 在本章中,我們將討論構建容錯分散式系統的演算法和協議的一些例子。我們將假設 [第八章](/tw/ch8) 的所有問題都可能發生:網路中的資料包可能會丟失、重新排序、重複推送或任意延遲;時鐘只是盡其所能地近似;且節點可以暫停(例如,由於垃圾收集)或隨時崩潰
在本章中,我們將討論構建容錯分散式系統的演算法和協議的一些例子。我們將假設 [第八章](ch8.md) 的所有問題都可能發生:網路中的資料包可能會丟失、重新排序、重複推送或任意延遲;時鐘只是盡其所能地近似;且節點可以暫停(例如,由於垃圾收集)或隨時崩潰。 構建容錯系統的最好方法,是找到一些帶有實用保證的通用抽象,實現一次,然後讓應用依賴這些保證。這與 [第七章](/tw/ch7) 中的事務處理方法相同:透過使用事務,應用可以假裝沒有崩潰(原子性),沒有其他人同時訪問資料庫(隔離),儲存裝置是完全可靠的(永續性)。即使發生崩潰,競態條件和磁碟故障,事務抽象隱藏了這些問題,因此應用不必擔心它們。
構建容錯系統的最好方法,是找到一些帶有實用保證的通用抽象,實現一次,然後讓應用依賴這些保證。這與 [第七章](ch7.md) 中的事務處理方法相同:透過使用事務,應用可以假裝沒有崩潰(原子性),沒有其他人同時訪問資料庫(隔離),儲存裝置是完全可靠的(永續性)。即使發生崩潰,競態條件和磁碟故障,事務抽象隱藏了這些問題,因此應用不必擔心它們。
現在我們將繼續沿著同樣的路線前進,尋求可以讓應用忽略分散式系統部分問題的抽象概念。例如,分散式系統最重要的抽象之一就是 **共識consensus****就是讓所有的節點對某件事達成一致**。正如我們在本章中將會看到的那樣,要可靠地達成共識,且不被網路故障和程序故障所影響,是一個令人驚訝的棘手問題。 現在我們將繼續沿著同樣的路線前進,尋求可以讓應用忽略分散式系統部分問題的抽象概念。例如,分散式系統最重要的抽象之一就是 **共識consensus****就是讓所有的節點對某件事達成一致**。正如我們在本章中將會看到的那樣,要可靠地達成共識,且不被網路故障和程序故障所影響,是一個令人驚訝的棘手問題。
一旦達成共識,應用可以將其用於各種目的。例如,假設你有一個單主複製的資料庫。如果主庫掛掉,並且需要故障切換到另一個節點,剩餘的資料庫節點可以使用共識來選舉新的領導者。正如在 “[處理節點宕機](ch5.md#處理節點宕機)” 中所討論的那樣,重要的是隻有一個領導者,且所有的節點都認同其領導。如果兩個節點都認為自己是領導者,這種情況被稱為 **腦裂split brain**,它經常會導致資料丟失。正確實現共識有助於避免這種問題。 一旦達成共識,應用可以將其用於各種目的。例如,假設你有一個單主複製的資料庫。如果主庫掛掉,並且需要故障切換到另一個節點,剩餘的資料庫節點可以使用共識來選舉新的領導者。正如在 “[處理節點宕機](/tw/ch5#處理節點宕機)” 中所討論的那樣,重要的是隻有一個領導者,且所有的節點都認同其領導。如果兩個節點都認為自己是領導者,這種情況被稱為 **腦裂split brain**,它經常會導致資料丟失。正確實現共識有助於避免這種問題。
在本章後面的 “[分散式事務與共識](#分散式事務與共識)” 中,我們將研究解決共識和相關問題的演算法。但首先,我們首先需要探索可以在分散式系統中提供的保證和抽象的範圍。 在本章後面的 “[分散式事務與共識](#分散式事務與共識)” 中,我們將研究解決共識和相關問題的演算法。但首先,我們首先需要探索可以在分散式系統中提供的保證和抽象的範圍。
@ -28,11 +31,11 @@
## 一致性保證 ## 一致性保證
在 “[複製延遲問題](ch5.md#複製延遲問題)” 中,我們看到了資料庫複製中發生的一些時序問題。如果你在同一時刻檢視兩個資料庫節點,則可能在兩個節點上看到不同的資料,因為寫請求在不同的時間到達不同的節點。無論資料庫使用何種複製方法(單主複製、多主複製或無主複製),都會出現這些不一致情況。 在 “[複製延遲問題](/tw/ch5#複製延遲問題)” 中,我們看到了資料庫複製中發生的一些時序問題。如果你在同一時刻檢視兩個資料庫節點,則可能在兩個節點上看到不同的資料,因為寫請求在不同的時間到達不同的節點。無論資料庫使用何種複製方法(單主複製、多主複製或無主複製),都會出現這些不一致情況。
大多數複製的資料庫至少提供了 **最終一致性**這意味著如果你停止向資料庫寫入資料並等待一段不確定的時間那麼最終所有的讀取請求都會返回相同的值【1】。換句話說不一致性是暫時的最終會自行解決假設網路中的任何故障最終都會被修復。最終一致性的一個更好的名字可能是 **收斂convergence**因為我們預計所有的副本最終會收斂到相同的值【2】。 大多數複製的資料庫至少提供了 **最終一致性**這意味著如果你停止向資料庫寫入資料並等待一段不確定的時間那麼最終所有的讀取請求都會返回相同的值【1】。換句話說不一致性是暫時的最終會自行解決假設網路中的任何故障最終都會被修復。最終一致性的一個更好的名字可能是 **收斂convergence**因為我們預計所有的副本最終會收斂到相同的值【2】。
然而,這是一個非常弱的保證 —— 它並沒有說什麼時候副本會收斂。在收斂之前讀操作可能會返回任何東西或什麼都沒有【1】。例如如果你寫入了一個值然後立即再次讀取這並不能保證你能看到剛才寫入的值因為讀請求可能會被路由到另外的副本上。請參閱 “[讀己之寫](ch5.md#讀己之寫)” )。 然而,這是一個非常弱的保證 —— 它並沒有說什麼時候副本會收斂。在收斂之前讀操作可能會返回任何東西或什麼都沒有【1】。例如如果你寫入了一個值然後立即再次讀取這並不能保證你能看到剛才寫入的值因為讀請求可能會被路由到另外的副本上。請參閱 “[讀己之寫](/tw/ch5#讀己之寫)” )。
對於應用開發人員而言最終一致性是很困難的因為它與普通單執行緒程式中變數的行為有很大區別。對於後者如果將一個值賦給一個變數然後很快地再次讀取不可能讀到舊的值或者讀取失敗。資料庫表面上看起來像一個你可以讀寫的變數但實際上它有更複雜的語義【3】。 對於應用開發人員而言最終一致性是很困難的因為它與普通單執行緒程式中變數的行為有很大區別。對於後者如果將一個值賦給一個變數然後很快地再次讀取不可能讀到舊的值或者讀取失敗。資料庫表面上看起來像一個你可以讀寫的變數但實際上它有更複雜的語義【3】。
@ -40,7 +43,7 @@
本章將探索資料系統可能選擇提供的更強一致性模型。它不是免費的:具有較強保證的系統可能會比保證較差的系統具有更差的效能或更少的容錯性。儘管如此,更強的保證能夠吸引人,因為它們更容易用對。只有見過不同的一致性模型後,才能更好地決定哪一個最適合自己的需求。 本章將探索資料系統可能選擇提供的更強一致性模型。它不是免費的:具有較強保證的系統可能會比保證較差的系統具有更差的效能或更少的容錯性。儘管如此,更強的保證能夠吸引人,因為它們更容易用對。只有見過不同的一致性模型後,才能更好地決定哪一個最適合自己的需求。
**分散式一致性模型** 和我們之前討論的事務隔離級別的層次結構有一些相似之處【4,5】請參閱 “[弱隔離級別](ch7.md#弱隔離級別)”)。儘管兩者有一部分內容重疊,但它們大多是無關的問題:事務隔離主要是為了 **避免由於同時執行事務而導致的競爭狀態**,而分散式一致性主要關於 **在面對延遲和故障時如何協調副本間的狀態** **分散式一致性模型** 和我們之前討論的事務隔離級別的層次結構有一些相似之處【4,5】請參閱 “[弱隔離級別](/tw/ch7#弱隔離級別)”)。儘管兩者有一部分內容重疊,但它們大多是無關的問題:事務隔離主要是為了 **避免由於同時執行事務而導致的競爭狀態**,而分散式一致性主要關於 **在面對延遲和故障時如何協調副本間的狀態**
本章涵蓋了廣泛的話題,但我們將會看到這些領域實際上是緊密聯絡在一起的: 本章涵蓋了廣泛的話題,但我們將會看到這些領域實際上是緊密聯絡在一起的:
@ -57,11 +60,11 @@
在一個線性一致的系統中,只要一個客戶端成功完成寫操作,所有客戶端從資料庫中讀取資料必須能夠看到剛剛寫入的值。要維護資料的單個副本的假象,系統應保障讀到的值是最近的、最新的,而不是來自陳舊的快取或副本。換句話說,線性一致性是一個 **新鮮度保證recency guarantee**。為了闡明這個想法,我們來看看一個非線性一致系統的例子。 在一個線性一致的系統中,只要一個客戶端成功完成寫操作,所有客戶端從資料庫中讀取資料必須能夠看到剛剛寫入的值。要維護資料的單個副本的假象,系統應保障讀到的值是最近的、最新的,而不是來自陳舊的快取或副本。換句話說,線性一致性是一個 **新鮮度保證recency guarantee**。為了闡明這個想法,我們來看看一個非線性一致系統的例子。
![](../img/fig9-1.png) ![](/img/fig9-1.png)
**圖 9-1 這個系統是非線性一致的,導致了球迷的困惑** **圖 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 驚呼最後得分 **之後**,點選了重新整理按鈕(啟動了他的查詢),因此他希望查詢結果至少與愛麗絲一樣新鮮。但他的查詢返回了陳舊結果,這一事實違背了線性一致性的要求。 如果 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 如果讀取請求與寫入請求併發,則可能會返回舊值或新值**
為了簡單起見,[圖 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` * $read(x)⇒v$表示客戶端請求讀取暫存器 `x` 的值,資料庫返回值 `v`
* $write(x,v)⇒r$ 表示客戶端請求將暫存器 `x` 設定為值 `v` ,資料庫返回響應 `r` (可能正確,可能錯誤)。 * $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 的第一個讀操作,完成於寫操作開始之前,因此必須返回舊值 `0`
* 客戶端 A 的最後一個讀操作,開始於寫操作完成之後。如果資料庫是線性一致性的,它必然返回新值 `1`:因為讀操作和寫操作一定是在其各自的起止區間內的某個時刻被處理。如果在寫入結束後開始讀取,則讀取處理一定發生在寫入完成之後,因此它必須看到寫入的新值。 * 客戶端 A 的最後一個讀操作,開始於寫操作完成之後。如果資料庫是線性一致性的,它必然返回新值 `1`:因為讀操作和寫操作一定是在其各自的起止區間內的某個時刻被處理。如果在寫入結束後開始讀取,則讀取處理一定發生在寫入完成之後,因此它必須看到寫入的新值。
@ -94,31 +97,31 @@
[^ii]: 如果讀取(與寫入同時發生時)可能返回舊值或新值,則稱該暫存器為 **常規暫存器regular register**【7,25】 [^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 任何一個讀取返回新值後,所有後續讀取(在相同或其他客戶端上)也必須返回新值。** **圖 9-3 任何一個讀取返回新值後,所有後續讀取(在相同或其他客戶端上)也必須返回新值。**
在一個線性一致的系統中,我們可以想象,在 `x` 的值從 `0` 自動翻轉到 `1` 的時候(在寫操作的開始和結束之間)必定有一個時間點。因此,如果一個客戶端的讀取返回新的值 `1`,即使寫操作尚未完成,所有後續讀取也必須返回新值。 在一個線性一致的系統中,我們可以想象,在 `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 將讀取和寫入看起來已經生效的時間點進行視覺化。客戶端 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 的讀請求在網路上略有延遲,所以它在兩次寫入之後才到達資料庫。 * 第一個客戶端 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` )。 * 此模型不假設有任何事務隔離另一個客戶端可能隨時更改值。例如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】。 這就是線性一致性背後的直覺。正式的定義【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】。不管這個鎖是如何實現的它必須是線性一致的所有節點必須就哪個節點擁有鎖達成一致否則就沒用了。 一個使用單主複製的系統需要確保領導者真的只有一個而不是幾個腦裂。一種選擇領導者的方法是使用鎖每個節點在啟動時嘗試獲取鎖成功者成為領導者【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】。請參閱 “[使用全序廣播實現線性一致的儲存](#使用全序廣播實現線性一致的儲存)”。 [^iii]: 嚴格地說ZooKeeper 和 etcd 提供線性一致性的寫操作但讀取可能是陳舊的因為預設情況下它們可以由任何一個副本提供服務。你可以選擇請求線性一致性讀取etcd 稱之為 **法定人數讀取quorum read**【16】而在 ZooKeeper 中,你需要在讀取之前呼叫 `sync()`【15】。請參閱 “[使用全序廣播實現線性一致的儲存](#使用全序廣播實現線性一致的儲存)”。
@ -170,27 +173,27 @@
如果想要確保銀行賬戶餘額永遠不會為負數,或者不會出售比倉庫裡的庫存更多的物品,或者兩個人不會都預定了航班或劇院裡同一時間的同一個位置。這些約束條件都要求所有節點都同意一個最新的值(賬戶餘額,庫存水平,座位佔用率)。 如果想要確保銀行賬戶餘額永遠不會為負數,或者不會出售比倉庫裡的庫存更多的物品,或者兩個人不會都預定了航班或劇院裡同一時間的同一個位置。這些約束條件都要求所有節點都同意一個最新的值(賬戶餘額,庫存水平,座位佔用率)。
在實際應用中,寬鬆地處理這些限制有時是可以接受的(例如,如果航班超額預訂,你可以將客戶轉移到不同的航班併為其提供補償)。在這種情況下,可能不需要線性一致性,我們將在 “[及時性與完整性](ch12.md#及時性與完整性)” 中討論這種寬鬆的約束。 在實際應用中,寬鬆地處理這些限制有時是可以接受的(例如,如果航班超額預訂,你可以將客戶轉移到不同的航班併為其提供補償)。在這種情況下,可能不需要線性一致性,我們將在 “[及時性與完整性](/tw/ch12#及時性與完整性)” 中討論這種寬鬆的約束。
然而一個硬性的唯一性約束關係型資料庫中常見的那種需要線性一致性。其他型別的約束如外部索引鍵或屬性約束可以不需要線性一致性【19】。 然而一個硬性的唯一性約束關係型資料庫中常見的那種需要線性一致性。其他型別的約束如外部索引鍵或屬性約束可以不需要線性一致性【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 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]: 對單主資料庫進行分割槽(分片),使得每個分割槽有一個單獨的領導者,不會影響線性一致性,因為線性一致性只是對單一物件的保證。交叉分割槽事務是一個不同的問題(請參閱 “[分散式事務與共識](#分散式事務與共識)”)。 [^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 非線性一致的執行,儘管使用了嚴格的法定人數**
在 [圖 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】。 而且這種方式只能實現線性一致的讀寫不能實現線性一致的比較和設定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 網路中斷迫使在線性一致性和可用性之間做出選擇。** **圖 9-7 網路中斷迫使在線性一致性和可用性之間做出選擇。**
@ -286,7 +289,7 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
在分散式系統中有更多有趣的 “不可能” 的結果【41】且 CAP 定理現在已經被更精確的結果取代【2,42】所以它現在基本上成了歷史古蹟了。 在分散式系統中有更多有趣的 “不可能” 的結果【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】。線性一致的速度很慢 —— 這始終是事實,而不僅僅是網路故障期間。 許多分散式資料庫也是如此:它們是 **為了提高效能** 而選擇了犧牲線性一致性而不是為了容錯【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** 這一主題在本書中反覆出現,這表明它可能是一個重要的基礎性概念。讓我們簡要回顧一下其它曾經出現過 **順序** 的上下文: **順序ordering** 這一主題在本書中反覆出現,這表明它可能是一個重要的基礎性概念。讓我們簡要回顧一下其它曾經出現過 **順序** 的上下文:
* 在 [第五章](ch5.md) 中我們看到,領導者在單主複製中的主要目的就是,在複製日誌中確定 **寫入順序order of write**—— 也就是從庫應用這些寫入的順序。如果不存在一個領導者,則併發操作可能導致衝突(請參閱 “[處理寫入衝突](ch5.md#處理寫入衝突)”)。 * 在 [第五章](/tw/ch5) 中我們看到,領導者在單主複製中的主要目的就是,在複製日誌中確定 **寫入順序order of write**—— 也就是從庫應用這些寫入的順序。如果不存在一個領導者,則併發操作可能導致衝突(請參閱 “[處理寫入衝突](/tw/ch5#處理寫入衝突)”)。
* 在 [第七章](ch7.md) 中討論的 **可序列化**,是關於事務表現的像按 **某種先後順序some sequential order** 執行的保證。它可以字面意義上地以 **序列順序serial order** 執行事務來實現,或者允許並行執行,但同時防止序列化衝突來實現(透過鎖或中止事務)。 * 在 [第七章](/tw/ch7) 中討論的 **可序列化**,是關於事務表現的像按 **某種先後順序some sequential order** 執行的保證。它可以字面意義上地以 **序列順序serial order** 執行事務來實現,或者允許並行執行,但同時防止序列化衝突來實現(透過鎖或中止事務)。
* 在 [第八章](ch8.md) 討論過的在分散式系統中使用時間戳和時鐘(請參閱 “[依賴同步時鐘](ch8.md#依賴同步時鐘)”)是另一種將順序引入無序世界的嘗試,例如,確定兩個寫入操作哪一個更晚發生。 * 在 [第八章](/tw/ch8) 討論過的在分散式系統中使用時間戳和時鐘(請參閱 “[依賴同步時鐘](/tw/ch8#依賴同步時鐘)”)是另一種將順序引入無序世界的嘗試,例如,確定兩個寫入操作哪一個更晚發生。
事實證明,順序、線性一致性和共識之間有著深刻的聯絡。儘管這個概念比本書其他部分更加理論化和抽象,但對於明確系統的能力範圍(可以做什麼和不可以做什麼)而言是非常有幫助的。我們將在接下來的幾節中探討這個話題。 事實證明,順序、線性一致性和共識之間有著深刻的聯絡。儘管這個概念比本書其他部分更加理論化和抽象,但對於明確系統的能力範圍(可以做什麼和不可以做什麼)而言是非常有幫助的。我們將在接下來的幾節中探討這個話題。
@ -317,12 +320,12 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
**順序** 反覆出現有幾個原因,其中一個原因是,它有助於保持 **因果關係causality**。在本書中我們已經看到了幾個例子,其中因果關係是很重要的: **順序** 反覆出現有幾個原因,其中一個原因是,它有助於保持 **因果關係causality**。在本書中我們已經看到了幾個例子,其中因果關係是很重要的:
* 在 “[一致字首讀](ch5.md#一致字首讀)”([圖 5-5](../img/fig5-5.png))中,我們看到一個例子:一個對話的觀察者首先看到問題的答案,然後才看到被回答的問題。這是令人困惑的,因為它違背了我們對 **因cause****果effect** 的直覺:如果一個問題被回答,顯然問題本身得先在那裡,因為給出答案的人必須先看到這個問題(假如他們並沒有預見未來的超能力)。我們認為在問題和答案之間存在 **因果依賴causal dependency** * 在 “[一致字首讀](/tw/ch5#一致字首讀)”([圖 5-5](/img/fig5-5.png))中,我們看到一個例子:一個對話的觀察者首先看到問題的答案,然後才看到被回答的問題。這是令人困惑的,因為它違背了我們對 **因cause****果effect** 的直覺:如果一個問題被回答,顯然問題本身得先在那裡,因為給出答案的人必須先看到這個問題(假如他們並沒有預見未來的超能力)。我們認為在問題和答案之間存在 **因果依賴causal dependency**
* [圖 5-9](../img/fig5-9.png) 中出現了類似的模式,我們看到三位領導者之間的複製,並注意到由於網路延遲,一些寫入可能會 “壓倒” 其他寫入。從其中一個副本的角度來看,好像有一個對尚不存在的記錄的更新操作。這裡的因果意味著,一條記錄必須先被建立,然後才能被更新。 * [圖 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 不知道彼此。 * 在 “[檢測併發寫入](/tw/ch5#檢測併發寫入)” 中我們觀察到,如果有兩個操作 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) 所示)。 * 在事務快照隔離的上下文中(“[快照隔離和可重複讀](/tw/ch7#快照隔離和可重複讀)”),我們說事務是從一致性快照中讀取的。但此語境中 “一致” 到底又是什麼意思?這意味著 **與因果關係保持一致consistent with causality**如果快照包含答案它也必須包含被回答的問題【48】。在某個時間點觀察整個資料庫與因果關係保持一致意味著因果上在該時間點之前發生的所有操作其影響都是可見的但因果上在該時間點之後發生的操作其影響對觀察者不可見。**讀偏差read skew** 意味著讀取的資料處於違反因果關係的狀態(不可重複讀,如 [圖 7-6](/img/fig7-6.png) 所示)。
* 事務之間 **寫偏差write skew** 的例子(請參閱 “[寫入偏差與幻讀](ch7.md#寫入偏差與幻讀)”)也說明了因果依賴:在 [圖 7-8](../img/fig7-8.png) 中,愛麗絲被允許離班,因為事務認為鮑勃仍在值班,反之亦然。在這種情況下,離班的動作因果依賴於對當前值班情況的觀察。[可序列化快照隔離](ch7.md#可序列化快照隔離) 透過跟蹤事務之間的因果依賴來檢測寫偏差。 * 事務之間 **寫偏差write skew** 的例子(請參閱 “[寫入偏差與幻讀](/tw/ch7#寫入偏差與幻讀)”)也說明了因果依賴:在 [圖 7-8](/img/fig7-8.png) 中,愛麗絲被允許離班,因為事務認為鮑勃仍在值班,反之亦然。在這種情況下,離班的動作因果依賴於對當前值班情況的觀察。[可序列化快照隔離](/tw/ch7#可序列化快照隔離) 透過跟蹤事務之間的因果依賴來檢測寫偏差。
* 在愛麗絲和鮑勃看球的例子中([圖 9-1](../img/fig9-1.png)),在聽到愛麗絲驚呼比賽結果後,鮑勃從伺服器得到陳舊結果的事實違背了因果關係:愛麗絲的驚呼因果依賴於得分宣告,所以鮑勃應該也能在聽到愛麗斯驚呼後查詢到比分。相同的模式在 “[跨通道的時序依賴](#跨通道的時序依賴)” 一節中,以 “影像大小調整服務” 的偽裝再次出現。 * 在愛麗絲和鮑勃看球的例子中([圖 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** 會在這些併發建立的提交相融合時建立。 如果你熟悉像 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 為了確定因果依賴,我們需要一些方法來描述系統中節點的 “知識”。如果節點在發出寫入 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 並不能告訴你哪個操作先發生,或者操作是否為併發的。 [^vii]: 與因果關係不一致的全序很容易建立,但沒啥用。例如你可以為每個操作生成隨機的 UUID並按照字典序比較 UUID以定義操作的全序。這是一個有效的全序但是隨機的 UUID 並不能告訴你哪個操作先發生,或者操作是否為併發的。
在單主複製的資料庫中(請參閱 “[領導者與追隨者](ch5.md#領導者與追隨者)”),複製日誌定義了與因果一致的寫操作。主庫可以簡單地為每個操作自增一個計數器,從而為複製日誌中的每個操作分配一個單調遞增的序列號。如果一個從庫按照它們在複製日誌中出現的順序來應用寫操作,那麼從庫的狀態始終是因果一致的(即使它落後於領導者)。 在單主複製的資料庫中(請參閱 “[領導者與追隨者](/tw/ch5#領導者與追隨者)”),複製日誌定義了與因果一致的寫操作。主庫可以簡單地為每個操作自增一個計數器,從而為複製日誌中的每個操作分配一個單調遞增的序列號。如果一個從庫按照它們在複製日誌中出現的順序來應用寫操作,那麼從庫的狀態始終是因果一致的(即使它落後於領導者)。
#### 非因果序列號生成器 #### 非因果序列號生成器
如果主庫不存在(可能因為使用了多主資料庫或無主資料庫,或者因為使用了分割槽的資料庫),如何為操作生成序列號就沒有那麼明顯了。在實踐中有各種各樣的方法: 如果主庫不存在(可能因為使用了多主資料庫或無主資料庫,或者因為使用了分割槽的資料庫),如何為操作生成序列號就沒有那麼明顯了。在實踐中有各種各樣的方法:
* 每個節點都可以生成自己獨立的一組序列號。例如有兩個節點,一個節點只能生成奇數,而另一個節點只能生成偶數。通常,可以在序列號的二進位制表示中預留一些位,用於唯一的節點識別符號,這樣可以確保兩個不同的節點永遠不會生成相同的序列號。 * 每個節點都可以生成自己獨立的一組序列號。例如有兩個節點,一個節點只能生成奇數,而另一個節點只能生成偶數。通常,可以在序列號的二進位制表示中預留一些位,用於唯一的節點識別符號,這樣可以確保兩個不同的節點永遠不會生成相同的序列號。
*可以將日曆時鐘物理時鐘的時間戳附加到每個操作上【55】。這種時間戳並不連續但是如果它具有足夠高的解析度那也許足以提供一個操作的全序關係。這一事實應用於* 最後寫入勝利 * 的衝突解決方法中(請參閱 “[有序事件的時間戳](ch8.md#有序事件的時間戳)”)。 *可以將日曆時鐘物理時鐘的時間戳附加到每個操作上【55】。這種時間戳並不連續但是如果它具有足夠高的解析度那也許足以提供一個操作的全序關係。這一事實應用於* 最後寫入勝利 * 的衝突解決方法中(請參閱 “[有序事件的時間戳](/tw/ch8#有序事件的時間戳)”)。
* 可以預先分配序列號區塊。例如,節點 A 可能要求從序列號 1 到 1,000 區塊的所有權,而節點 B 可能要求序列號 1,001 到 2,000 區塊的所有權。然後每個節點可以獨立分配所屬區塊中的序列號,並在序列號告急時請求分配一個新的區塊。 * 可以預先分配序列號區塊。例如,節點 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 之間的數字。這裡序列號與因果關係也是不一致的。 * 在分配區塊的情況下,某個操作可能會被賦予一個範圍在 1,001 到 2,000 內的序列號,然而一個因果上更晚的操作可能被賦予一個範圍在 1 到 1,000 之間的數字。這裡序列號與因果關係也是不一致的。
@ -417,9 +420,9 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
儘管剛才描述的三個序列號生成器與因果不一致但實際上有一個簡單的方法來產生與因果關係一致的序列號。它被稱為蘭伯特時間戳萊斯利・蘭伯特Leslie Lamport於 1978 年提出【56】現在是分散式系統領域中被引用最多的論文之一。 儘管剛才描述的三個序列號生成器與因果不一致但實際上有一個簡單的方法來產生與因果關係一致的序列號。它被稱為蘭伯特時間戳萊斯利・蘭伯特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 時間戳提供了與因果關係一致的全序。** **圖 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 核上,那麼定義一個操作全序是很容易的:可以簡單認為就是 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是另一個同義詞。 [^ix]: “原子廣播” 是一個傳統的術語,非常混亂,而且與 “原子” 一詞的其他用法不一致:它與 ACID 事務中的原子性沒有任何關係,只是與原子操作(在多執行緒程式設計的意義上 或原子暫存器線性一致儲存有間接的聯絡。全序組播total order multicast是另一個同義詞。
@ -480,19 +483,19 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
像 ZooKeeper 和 etcd 這樣的共識服務實際上實現了全序廣播。這一事實暗示了全序廣播與共識之間有著緊密聯絡,我們將在本章稍後進行探討。 像 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】。因此共識問題與線性一致暫存器問題密切相關。 [^x]: 從形式上講,線性一致讀寫暫存器是一個 “更容易” 的問題。全序廣播等價於共識【67】而共識問題在非同步的崩潰 - 停止模型【68】中沒有確定性的解決方案而線性一致的讀寫暫存器 **可以** 在這種模型中實現【23,24,25】。然而支援諸如 **比較並設定CAS, compare-and-set**,或 **自增並返回increment-and-get** 的原子操作使它等價於共識問題【28】。因此共識問題與線性一致暫存器問題密切相關。
@ -516,7 +519,7 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
* 你可以透過在日誌中追加一條訊息然後讀取日誌直到該訊息被讀回才執行實際的讀取操作。訊息在日誌中的位置因此定義了讀取發生的時間點etcd 的法定人數讀取有些類似這種情況【16】 * 你可以透過在日誌中追加一條訊息然後讀取日誌直到該訊息被讀回才執行實際的讀取操作。訊息在日誌中的位置因此定義了讀取發生的時間點etcd 的法定人數讀取有些類似這種情況【16】
* 如果日誌允許以線性一致的方式獲取最新日誌訊息的位置,則可以查詢該位置,等待該位置前的所有訊息都傳達到你,然後執行讀取。(這是 Zookeeper `sync()` 操作背後的思想【15】 * 如果日誌允許以線性一致的方式獲取最新日誌訊息的位置,則可以查詢該位置,等待該位置前的所有訊息都傳達到你,然後執行讀取。(這是 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**。你也許會認為這不會太難。不幸的是,許多出故障的系統都是因為錯誤地輕信這個問題很容易解決。 **共識** 是分散式計算中最重要也是最基本的問題之一。從表面上看似乎很簡單:非正式地講,目標只是 **讓幾個節點達成一致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】。**非阻塞** 原子提交則要比共識更為困難 —— 請參閱 “[三階段提交](#三階段提交)”。 [^xii]: 原子提交的形式化與共識稍有不同:原子事務只有在 **所有** 參與者投票提交的情況下才能提交,如果有任何參與者需要中止,則必須中止。共識則允許就 **任意一個** 被參與者提出的候選值達成一致。然而原子提交和共識可以相互簡化為對方【70,71】。**非阻塞** 原子提交則要比共識更為困難 —— 請參閱 “[三階段提交](#三階段提交)”。
@ -557,7 +560,7 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
> >
> 你可能已經聽說過以作者 FischerLynch 和 Paterson 命名的 FLP 結果【68】它證明如果存在節點可能崩潰的風險則不存在 **總是** 能夠達成共識的演算法。在分散式系統中,我們必須假設節點可能會崩潰,所以可靠的共識是不可能的。然而這裡我們正在討論達成共識的演算法,到底是怎麼回事? > 你可能已經聽說過以作者 FischerLynch 和 Paterson 命名的 FLP 結果【68】它證明如果存在節點可能崩潰的風險則不存在 **總是** 能夠達成共識的演算法。在分散式系統中,我們必須假設節點可能會崩潰,所以可靠的共識是不可能的。然而這裡我們正在討論達成共識的演算法,到底是怎麼回事?
> >
> 答案是 FLP 結果是在 **非同步系統模型** 中被證明的(請參閱 “[系統模型與現實](ch8.md#系統模型與現實)”),而這是一種限制性很強的模型,它假定確定性演算法不能使用任何時鐘或超時。如果允許演算法使用 **超時** 或其他方法來識別可疑的崩潰節點即使懷疑有時是錯誤的則共識變為一個可解的問題【67】。即使僅僅允許演算法使用隨機數也足以繞過這個不可能的結果【69】。 > 答案是 FLP 結果是在 **非同步系統模型** 中被證明的(請參閱 “[系統模型與現實](/tw/ch8#系統模型與現實)”),而這是一種限制性很強的模型,它假定確定性演算法不能使用任何時鐘或超時。如果允許演算法使用 **超時** 或其他方法來識別可疑的崩潰節點即使懷疑有時是錯誤的則共識變為一個可解的問題【67】。即使僅僅允許演算法使用隨機數也足以繞過這個不可能的結果【69】。
> >
> 因此,雖然 FLP 是關於共識不可能性的重要理論結果,但現實中的分散式系統通常是可以達成共識的。 > 因此,雖然 FLP 是關於共識不可能性的重要理論結果,但現實中的分散式系統通常是可以達成共識的。
@ -568,17 +571,17 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
### 原子提交與兩階段提交 ### 原子提交與兩階段提交
在 [第七章](ch7.md) 中我們瞭解到,事務原子性的目的是在多次寫操作中途出錯的情況下,提供一種簡單的語義。事務的結果要麼是成功提交,在這種情況下,事務的所有寫入都是持久化的;要麼是中止,在這種情況下,事務的所有寫入都被回滾(即撤消或丟棄)。 在 [第七章](/tw/ch7) 中我們瞭解到,事務原子性的目的是在多次寫操作中途出錯的情況下,提供一種簡單的語義。事務的結果要麼是成功提交,在這種情況下,事務的所有寫入都是持久化的;要麼是中止,在這種情況下,事務的所有寫入都被回滾(即撤消或丟棄)。
原子性可以防止失敗的事務攪亂資料庫,避免資料庫陷入半成品結果和半更新狀態。這對於多物件事務(請參閱 “[單物件和多物件操作](ch7.md#單物件和多物件操作)”)和維護次級索引的資料庫尤其重要。每個次級索引都是與主資料相分離的資料結構 —— 因此,如果你修改了一些資料,則還需要在次級索引中進行相應的更改。原子性確保次級索引與主資料保持一致(如果索引與主資料不一致,就沒什麼用了)。 原子性可以防止失敗的事務攪亂資料庫,避免資料庫陷入半成品結果和半更新狀態。這對於多物件事務(請參閱 “[單物件和多物件操作](/tw/ch7#單物件和多物件操作)”)和維護次級索引的資料庫尤其重要。每個次級索引都是與主資料相分離的資料結構 —— 因此,如果你修改了一些資料,則還需要在次級索引中進行相應的更改。原子性確保次級索引與主資料保持一致(如果索引與主資料不一致,就沒什麼用了)。
#### 從單節點到分散式原子提交 #### 從單節點到分散式原子提交
對於在單個數據庫節點執行的事務,原子性通常由儲存引擎實現。當客戶端請求資料庫節點提交事務時,資料庫將使事務的寫入持久化(通常在預寫式日誌中,請參閱 “[讓 B 樹更可靠](ch3.md#讓B樹更可靠)”),然後將提交記錄追加到磁碟中的日誌裡。如果資料庫在這個過程中間崩潰,當節點重啟時,事務會從日誌中恢復:如果提交記錄在崩潰之前成功地寫入磁碟,則認為事務被提交;否則來自該事務的任何寫入都被回滾。 對於在單個數據庫節點執行的事務,原子性通常由儲存引擎實現。當客戶端請求資料庫節點提交事務時,資料庫將使事務的寫入持久化(通常在預寫式日誌中,請參閱 “[讓 B 樹更可靠](/tw/ch3#讓B樹更可靠)”),然後將提交記錄追加到磁碟中的日誌裡。如果資料庫在這個過程中間崩潰,當節點重啟時,事務會從日誌中恢復:如果提交記錄在崩潰之前成功地寫入磁碟,則認為事務被提交;否則來自該事務的任何寫入都被回滾。
因此,在單個節點上,事務的提交主要取決於資料持久化落盤的 **順序**首先是資料然後是提交記錄【72】。事務提交或終止的關鍵決定時刻是磁碟完成寫入提交記錄的時刻在此之前仍有可能中止由於崩潰但在此之後事務已經提交即使資料庫崩潰。因此是單一的裝置連線到單個磁碟的控制器且掛載在單臺機器上使得提交具有原子性。 因此,在單個節點上,事務的提交主要取決於資料持久化落盤的 **順序**首先是資料然後是提交記錄【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】但從資料庫的角度來看這是一個單獨的事務因此任何關於跨事務正確性的保證都是應用自己的問題。 提交事務的結果有可能透過事後執行另一個補償事務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】。 **兩階段提交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的成功執行** **圖 9-9 兩階段提交2PC的成功執行**
> #### 不要把2PC和2PL搞混了 > #### 不要把2PC和2PL搞混了
> >
> 兩階段提交2PC和兩階段鎖定請參閱 “[兩階段鎖定](ch7.md#兩階段鎖定)”是兩個完全不同的東西。2PC 在分散式資料庫中提供原子提交,而 2PL 提供可序列化的隔離等級。為了避免混淆,最好把它們看作完全獨立的概念,並忽略名稱中不幸的相似性。 > 兩階段提交2PC和兩階段鎖定請參閱 “[兩階段鎖定](/tw/ch7#兩階段鎖定)”是兩個完全不同的東西。2PC 在分散式資料庫中提供原子提交,而 2PL 提供可序列化的隔離等級。為了避免混淆,最好把它們看作完全獨立的概念,並忽略名稱中不幸的相似性。
2PC 使用一個通常不會出現在單節點事務中的新元件:**協調者**coordinator也稱為 **事務管理器**,即 transaction manager。協調者通常在請求事務的相同應用程序中以庫的形式實現例如嵌入在 Java EE 容器中),但也可以是單獨的程序或服務。這種協調者的例子包括 Narayana、JOTM、BTM 或 MSDTC。 2PC 使用一個通常不會出現在單節點事務中的新元件:**協調者**coordinator也稱為 **事務管理器**,即 transaction manager。協調者通常在請求事務的相同應用程序中以庫的形式實現例如嵌入在 Java EE 容器中),但也可以是單獨的程序或服務。這種協調者的例子包括 Narayana、JOTM、BTM 或 MSDTC。
@ -638,9 +641,9 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
如果協調者在傳送 **準備** 請求之前失敗,參與者可以安全地中止事務。但是,一旦參與者收到了準備請求並投了 “是”,就不能再單方面放棄 —— 必須等待協調者回答事務是否已經提交或中止。如果此時協調者崩潰或網路出現故障,參與者什麼也做不了只能等待。參與者的這種事務狀態稱為 **存疑in doubt** 的或 **不確定uncertain** 的。 如果協調者在傳送 **準備** 請求之前失敗,參與者可以安全地中止事務。但是,一旦參與者收到了準備請求並投了 “是”,就不能再單方面放棄 —— 必須等待協調者回答事務是否已經提交或中止。如果此時協調者崩潰或網路出現故障,參與者什麼也做不了只能等待。參與者的這種事務狀態稱為 **存疑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 不知道是否提交或中止** **圖 9-10 參與者投贊成票後,協調者崩潰。資料庫 1 不知道是否提交或中止**
@ -652,7 +655,7 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
兩階段提交被稱為 **阻塞blocking**- 原子提交協議,因為存在 2PC 可能卡住並等待協調者恢復的情況。理論上,可以使一個原子提交協議變為 **非阻塞nonblocking** 的,以便在節點失敗時不會卡住。但是讓這個協議能在實踐中工作並沒有那麼簡單。 兩階段提交被稱為 **阻塞blocking**- 原子提交協議,因為存在 2PC 可能卡住並等待協調者恢復的情況。理論上,可以使一個原子提交協議變為 **非阻塞nonblocking** 的,以便在節點失敗時不會卡住。但是讓這個協議能在實踐中工作並沒有那麼簡單。
作為 2PC 的替代方案,已經提出了一種稱為 **三階段提交3PC** 的演算法【13,80】。然而3PC 假定網路延遲有界,節點響應時間有限;在大多數具有無限網路延遲和程序暫停的實際系統中(見 [第八章](ch8.md)),它並不能保證原子性。 作為 2PC 的替代方案,已經提出了一種稱為 **三階段提交3PC** 的演算法【13,80】。然而3PC 假定網路延遲有界,節點響應時間有限;在大多數具有無限網路延遲和程序暫停的實際系統中(見 [第八章](/tw/ch8)),它並不能保證原子性。
通常,非阻塞原子提交需要一個 **完美的故障檢測器perfect failure detector**【67,71】—— 即一個可靠的機制來判斷一個節點是否已經崩潰。在具有無限延遲的網路中超時並不是一種可靠的故障檢測機制因為即使沒有節點崩潰請求也可能由於網路問題而超時。出於這個原因2PC 仍然被使用,儘管大家都清楚可能存在協調者故障的問題。 通常,非阻塞原子提交需要一個 **完美的故障檢測器perfect failure detector**【67,71】—— 即一個可靠的機制來判斷一個節點是否已經崩潰。在具有無限延遲的網路中超時並不是一種可靠的故障檢測機制因為即使沒有節點崩潰請求也可能由於網路問題而超時。出於這個原因2PC 仍然被使用,儘管大家都清楚可能存在協調者故障的問題。
@ -683,7 +686,7 @@ CAP 定理的正式定義僅限於很狹隘的範圍【30】它只考慮了
然而,只有當所有受事務影響的系統都使用同樣的 **原子提交協議atomic commit protocol** 時,這樣的分散式事務才是可能的。例如,假設處理訊息的副作用是傳送一封郵件,而郵件伺服器並不支援兩階段提交:如果訊息處理失敗並重試,則可能會發送兩次或更多次的郵件。但如果處理訊息的所有副作用都可以在事務中止時回滾,那麼這樣的處理流程就可以安全地重試,就好像什麼都沒有發生過一樣。 然而,只有當所有受事務影響的系統都使用同樣的 **原子提交協議atomic commit protocol** 時,這樣的分散式事務才是可能的。例如,假設處理訊息的副作用是傳送一封郵件,而郵件伺服器並不支援兩階段提交:如果訊息處理失敗並重試,則可能會發送兩次或更多次的郵件。但如果處理訊息的所有副作用都可以在事務中止時回滾,那麼這樣的處理流程就可以安全地重試,就好像什麼都沒有發生過一樣。
在 [第十一章](ch11.md) 中將再次回到 “恰好一次” 訊息處理的主題。讓我們先來看看允許這種異構分散式事務的原子提交協議。 在 [第十一章](/tw/ch11) 中將再次回到 “恰好一次” 訊息處理的主題。讓我們先來看看允許這種異構分散式事務的原子提交協議。
#### XA事務 #### 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 的青睞),所有持久狀態都儲存在資料庫中,因此具有應用伺服器可隨意按需新增刪除的優點。但是,當協調者成為應用伺服器的一部分時,它會改變部署的性質。突然間,協調者的日誌成為持久系統狀態的關鍵部分 —— 與資料庫本身一樣重要,因為協調者日誌是為了在崩潰後恢復存疑事務所必需的。這樣的應用伺服器不再是無狀態的了。 * 許多伺服器端應用都是使用無狀態模式開發的(受 HTTP 的青睞),所有持久狀態都儲存在資料庫中,因此具有應用伺服器可隨意按需新增刪除的優點。但是,當協調者成為應用伺服器的一部分時,它會改變部署的性質。突然間,協調者的日誌成為持久系統狀態的關鍵部分 —— 與資料庫本身一樣重要,因為協調者日誌是為了在崩潰後恢復存疑事務所必需的。這樣的應用伺服器不再是無狀態的了。
* 由於 XA 需要相容各種資料系統,因此它必須是所有系統的最小公分母。例如,它不能檢測不同系統間的死鎖(因為這將需要一個標準協議來讓系統交換每個事務正在等待的鎖的資訊),而且它無法與 SSI請參閱 [可序列化快照隔離](ch7.md#可序列化快照隔離))協同工作,因為這需要一個跨系統定位衝突的協議。 * 由於 XA 需要相容各種資料系統,因此它必須是所有系統的最小公分母。例如,它不能檢測不同系統間的死鎖(因為這將需要一個標準協議來讓系統交換每個事務正在等待的鎖的資訊),而且它無法與 SSI請參閱 [可序列化快照隔離](/tw/ch7#可序列化快照隔離))協同工作,因為這需要一個跨系統定位衝突的協議。
* 對於資料庫內部的分散式事務(不是 XA限制沒有這麼大 —— 例如,分散式版本的 SSI 是可能的。然而仍然存在問題2PC 成功提交一個事務需要所有參與者的響應。因此,如果系統的 **任何** 部分損壞,事務也會失敗。因此,分散式事務又有 **擴大失效amplifying failures** 的趨勢,這又與我們構建容錯系統的目標背道而馳。 * 對於資料庫內部的分散式事務(不是 XA限制沒有這麼大 —— 例如,分散式版本的 SSI 是可能的。然而仍然存在問題2PC 成功提交一個事務需要所有參與者的響應。因此,如果系統的 **任何** 部分損壞,事務也會失敗。因此,分散式事務又有 **擴大失效amplifying failures** 的趨勢,這又與我們構建容錯系統的目標背道而馳。
這些事實是否意味著我們應該放棄保持幾個系統相互一致的所有希望?不完全是 —— 還有其他的辦法,可以讓我們在沒有異構分散式事務的痛苦的情況下實現同樣的事情。我們將在 [第十一章](ch11.md) 和 [第十二章](ch12.md) 回到這些話題。但首先,我們應該概括一下關於 **共識** 的話題。 這些事實是否意味著我們應該放棄保持幾個系統相互一致的所有希望?不完全是 —— 還有其他的辦法,可以讓我們在沒有異構分散式事務的痛苦的情況下實現同樣的事情。我們將在 [第十一章](/tw/ch11) 和 [第十二章](/tw/ch12) 回到這些話題。但首先,我們應該概括一下關於 **共識** 的話題。
### 容錯共識 ### 容錯共識
@ -759,15 +762,15 @@ XA 事務解決了保持多個參與者(資料系統)相互一致的現實
如果你不關心容錯,那麼滿足前三個屬性很容易:你可以將一個節點硬編碼為 “獨裁者”,並讓該節點做出所有的決定。但如果該節點失效,那麼系統就無法再做出任何決定。事實上,這就是我們在兩階段提交的情況中所看到的:如果協調者失效,那麼存疑的參與者就無法決定提交還是中止。 如果你不關心容錯,那麼滿足前三個屬性很容易:你可以將一個節點硬編碼為 “獨裁者”,並讓該節點做出所有的決定。但如果該節點失效,那麼系統就無法再做出任何決定。事實上,這就是我們在兩階段提交的情況中所看到的:如果協調者失效,那麼存疑的參與者就無法決定提交還是中止。
**終止** 屬性形式化了容錯的思想。它實質上說的是,一個共識演算法不能簡單地永遠閒坐著等死 —— 換句話說,它必須取得進展。即使部分節點出現故障,其他節點也必須達成一項決定(**終止** 是一種 **活性屬性**,而另外三種是 **安全屬性** —— 請參閱 “[安全性和活性](ch8.md#安全性和活性)”)。 **終止** 屬性形式化了容錯的思想。它實質上說的是,一個共識演算法不能簡單地永遠閒坐著等死 —— 換句話說,它必須取得進展。即使部分節點出現故障,其他節點也必須達成一項決定(**終止** 是一種 **活性屬性**,而另外三種是 **安全屬性** —— 請參閱 “[安全性和活性](/tw/ch8#安全性和活性)”)。
共識的系統模型假設,當一個節點 “崩潰” 時,它會突然消失而且永遠不會回來。(不像軟體崩潰,想象一下地震,包含你的節點的資料中心被山體滑坡所摧毀,你必須假設節點被埋在 30 英尺以下的泥土中,並且永遠不會重新上線)在這個系統模型中,任何需要等待節點恢復的演算法都不能滿足 **終止** 屬性。特別是2PC 不符合終止屬性的要求。 共識的系統模型假設,當一個節點 “崩潰” 時,它會突然消失而且永遠不會回來。(不像軟體崩潰,想象一下地震,包含你的節點的資料中心被山體滑坡所摧毀,你必須假設節點被埋在 30 英尺以下的泥土中,並且永遠不會重新上線)在這個系統模型中,任何需要等待節點恢復的演算法都不能滿足 **終止** 屬性。特別是2PC 不符合終止屬性的要求。
當然如果 **所有** 的節點都崩潰了,沒有一個在執行,那麼所有演算法都不可能決定任何事情。演算法可以容忍的失效數量是有限的:事實上可以證明,任何共識演算法都需要至少佔總體 **多數majority** 的節點正確工作以確保終止屬性【67】。多數可以安全地組成法定人數請參閱 “[讀寫的法定人數](ch5.md#讀寫的法定人數)”)。 當然如果 **所有** 的節點都崩潰了,沒有一個在執行,那麼所有演算法都不可能決定任何事情。演算法可以容忍的失效數量是有限的:事實上可以證明,任何共識演算法都需要至少佔總體 **多數majority** 的節點正確工作以確保終止屬性【67】。多數可以安全地組成法定人數請參閱 “[讀寫的法定人數](/tw/ch5#讀寫的法定人數)”)。
因此 **終止** 屬性取決於一個假設,**不超過一半的節點崩潰或不可達**。然而即使多數節點出現故障或存在嚴重的網路問題,絕大多數共識的實現都能始終確保安全屬性得到滿足 —— 一致同意完整性和有效性【92】。因此大規模的中斷可能會阻止系統處理請求但是它不能透過使系統做出無效的決定來破壞共識系統。 因此 **終止** 屬性取決於一個假設,**不超過一半的節點崩潰或不可達**。然而即使多數節點出現故障或存在嚴重的網路問題,絕大多數共識的實現都能始終確保安全屬性得到滿足 —— 一致同意完整性和有效性【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】。因此如果在一個提案的表決過程中沒有出現更高的紀元編號。那麼現任領導者就可以得出這樣的結論沒有發生過更高時代的領導選舉因此可以確定自己仍然在領導。然後它就可以安全地對提議值做出決定。 因此,我們有兩輪投票:第一次是為了選出一位領導者,第二次是對領導者的提議進行表決。關鍵的洞察在於,這兩次投票的 **法定人群** 必須相互 **重疊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 模型執行良好的一個例子是,如果你有幾個程序例項或服務,需要選擇其中一個例項作為主庫或首選服務。如果領導者失敗,其他節點之一應該接管。這對單主資料庫當然非常實用,但對作業排程程式和類似的有狀態系統也很好用。 ZooKeeper/Chubby 模型執行良好的一個例子是,如果你有幾個程序例項或服務,需要選擇其中一個例項作為主庫或首選服務。如果領導者失敗,其他節點之一應該接管。這對單主資料庫當然非常實用,但對作業排程程式和類似的有狀態系統也很好用。
另一個例子是,當你有一些分割槽資源(資料庫、訊息流、檔案儲存、分散式 Actor 系統等),並需要決定將哪個分割槽分配給哪個節點時。當新節點加入叢集時,需要將某些分割槽從現有節點移動到新節點,以便重新平衡負載(請參閱 “[分割槽再平衡](ch6.md#分割槽再平衡)”)。當節點被移除或失效時,其他節點需要接管失效節點的工作。 另一個例子是,當你有一些分割槽資源(資料庫、訊息流、檔案儲存、分散式 Actor 系統等),並需要決定將哪個分割槽分配給哪個節點時。當新節點加入叢集時,需要將某些分割槽從現有節點移動到新節點,以便重新平衡負載(請參閱 “[分割槽再平衡](/tw/ch6#分割槽再平衡)”)。當節點被移除或失效時,其他節點需要接管失效節點的工作。
這類任務可以透過在 ZooKeeper 中明智地使用原子操作,臨時節點與通知來實現。如果設計得當,這種方法允許應用自動從故障中恢復而無需人工干預。不過這並不容易,儘管已經有不少在 ZooKeeper 客戶端 API 基礎之上提供更高層工具的庫,例如 Apache Curator 【17】。但它仍然要比嘗試從頭實現必要的共識演算法要好得多這樣的嘗試鮮有成功記錄【107】。 這類任務可以透過在 ZooKeeper 中明智地使用原子操作,臨時節點與通知來實現。如果設計得當,這種方法允許應用自動從故障中恢復而無需人工干預。不過這並不容易,儘管已經有不少在 ZooKeeper 客戶端 API 基礎之上提供更高層工具的庫,例如 Apache Curator 【17】。但它仍然要比嘗試從頭實現必要的共識演算法要好得多這樣的嘗試鮮有成功記錄【107】。
@ -880,7 +883,7 @@ ZooKeeper、etcd 和 Consul 也經常用於服務發現 —— 也就是找出
ZooKeeper 和它的小夥伴們可以看作是成員資格服務membership services研究的悠久歷史的一部分這個歷史可以追溯到 20 世紀 80 年代並且對建立高度可靠的系統例如空中交通管制非常重要【110】。 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. 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. 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. 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) 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)
## 後記 ## 後記

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 見背壓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 沒有任何已知的上限或大小。反義詞是邊界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 * 複製Replication
在幾個不同的節點上儲存資料的相同副本,可能放在不同的位置。複製提供了冗餘:如果一些節點不可用,剩餘的節點仍然可以提供資料服務。複製也有助於改善效能。[第五章](ch5.md) 將討論複製。 在幾個不同的節點上儲存資料的相同副本,可能放在不同的位置。複製提供了冗餘:如果一些節點不可用,剩餘的節點仍然可以提供資料服務。複製也有助於改善效能。[第五章](/tw/ch5) 將討論複製。
* 分割槽 (Partitioning) * 分割槽 (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 一個數據庫切分為兩個分割槽,每個分割槽都有兩個副本** **圖 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) * [第五章:複製](/tw/ch5)
6. [分割槽](ch6.md) * [領導者與追隨者](/tw/ch5#領導者與追隨者)
7. [事務](ch7.md) * [複製延遲問題](/tw/ch5#複製延遲問題)
8. [分散式系統的麻煩](ch8.md) * [多主複製](/tw/ch5#多主複製)
9. [一致性與共識](ch9.md) * [無主複製](/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. 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. 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. 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. 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) |

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) * [第十章:批處理](/tw/ch10)
11. [流處理](ch11.md) * [使用Unix工具的批處理](/tw/ch10#使用Unix工具的批處理)
12. [資料系統的未來](ch12.md) * [MapReduce和分散式檔案系統](/tw/ch10#MapReduce和分散式檔案系統)
* [MapReduce之後](/tw/ch10#MapReduce之後)
* [本章小結](/tw/ch10#本章小結)
------ * [第十一章:流處理](/tw/ch11)
* [傳遞事件流](/tw/ch11#傳遞事件流)
| 上一章 | 目錄 | 下一章 | * [資料庫與流](/tw/ch11#資料庫與流)
| ------------------------------ | ------------------------------- | ------------------------- | * [流處理](/tw/ch11#流處理)
| [第九章:一致性與共識](ch9.md) | [設計資料密集型應用](README.md) | [第十章:批處理](ch10.md) | * [本章小結](/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即時 如果近幾年從業於軟體工程,特別是伺服器端和後端系統開發,那麼你很有可能已經被大量關於資料儲存和處理的時髦詞彙轟炸過了: 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)

View file

@ -1,76 +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,4 +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,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) |