diff --git a/content/tw/ch1.md b/content/tw/ch1.md index 3d30bd1..2715999 100644 --- a/content/tw/ch1.md +++ b/content/tw/ch1.md @@ -4,36 +4,41 @@ weight: 101 breadcrumbs: false --- -> *沒有解決方案,只有利弊權衡。[…] 但你要努力獲得最好的權衡,這就是你所能期望的全部。* +> *沒有完美的解決方案,只有權衡取捨。[…] 你能做的就是努力獲得最佳的權衡,這就是你所能期望的一切。* > > [Thomas Sowell](https://www.youtube.com/watch?v=2YUtKr8-_Fg),接受 Fred Barnes 採訪(2005) -資料是當今許多應用程式開發的核心。隨著 Web 和移動應用、軟體即服務(SaaS)以及雲服務的出現,將來自許多不同使用者的資料儲存在共享的基於伺服器的資料基礎設施中已經變得很正常。來自使用者活動、業務交易、裝置和感測器的資料需要被儲存並可供分析使用。當用戶與應用程式互動時,他們既讀取儲存的資料,也生成更多的資料。 +> [!TIP] 早期讀者注意事項 +> 透過早期釋出電子書,您可以在書籍最早期的形式中獲得內容——作者在撰寫時的原始和未經編輯的內容——以便您可以在這些技術正式釋出之前就充分利用它們。 +> +> 這將是最終書籍的第 1 章。本書的 GitHub 倉庫是 https://github.com/ept/ddia2-feedback。 +> 如果您想積極參與審閱和評論本草稿,請在 GitHub 上聯絡我們。 -少量資料可以在單臺機器上儲存和處理,通常相當容易處理。然而,隨著資料量或查詢速率的增長,需要將其分佈在多臺機器上,這帶來了許多挑戰。隨著應用程式需求變得更加複雜,僅將所有內容儲存在一個系統中已經不夠,可能需要組合多個提供不同功能的儲存或處理系統。 +資料是當今許多應用程式開發的核心。隨著 Web 和移動應用、軟體即服務(SaaS)以及雲服務的興起,將來自不同使用者的資料儲存在共享的基於伺服器的資料基礎設施中已成為常態。來自使用者活動、業務交易、裝置和感測器的資料需要被儲存並可供分析使用。當用戶與應用程式互動時,他們既讀取已儲存的資料,也生成更多的資料。 -如果資料管理是開發應用程式的主要挑戰之一,我們稱該應用程式為 **資料密集型**(data-intensive)[^1]。 -雖然在 **計算密集型**(compute-intensive)系統中的挑戰是並行化某些非常大的計算,但在資料密集型應用中,我們通常更擔心諸如儲存和處理大資料量、管理資料變更、確保在故障和併發情況下的一致性以及確保服務高可用等問題。 +少量的資料可以儲存和處理在單臺機器上,通常相當容易處理。然而,隨著資料量或查詢速率的增長,資料需要分佈在多臺機器上,這帶來了許多挑戰。隨著應用程式需求變得更加複雜,將所有內容儲存在一個系統中已經不夠,可能需要組合多個提供不同能力的儲存或處理系統。 + +如果資料管理是開發應用程式的主要挑戰之一,我們就稱應用程式為 **資料密集型(data-intensive)** 的 [^1]。雖然在 **計算密集型(compute-intensive)** 系統中,挑戰是並行化某些非常大規模的計算,但在資料密集型應用中,我們通常更關心諸如儲存和處理大量資料、管理資料變更、在面對故障和併發時確保一致性,以及確保服務高可用等問題。 這些應用程式通常由提供常用功能的標準構建塊構建而成。例如,許多應用程式需要: -* 儲存資料,以便它們或另一個應用程式以後可以再次找到它(**資料庫**) +* 儲存資料,以便它們或其他應用程式以後能再次找到(**資料庫**) * 記住昂貴操作的結果,以加快讀取速度(**快取**) * 允許使用者按關鍵字搜尋資料或以各種方式過濾資料(**搜尋索引**) -* 處理事件和資料變更(**流處理**) -* 定期處理大量累積的資料(**批處理**) +* 一旦事件和資料變更發生就立即處理(**流處理**) +* 定期處理累積的大量資料(**批處理**) -在構建應用程式時,我們通常會採用幾個軟體系統或服務,例如資料庫或 API,並用一些應用程式程式碼將它們粘合在一起。如果你正在做這些資料系統設計的目標,那麼這個過程可能會很容易。 +在構建應用程式時,我們通常會採用幾個軟體系統或服務,例如資料庫或 API,並用一些應用程式程式碼將它們粘合在一起。如果你正在做資料系統設計的工作,那麼這個過程可能會相當容易。 -然而,隨著你的應用程式變得更加雄心勃勃,挑戰就會出現。有許多具有不同特性的資料庫系統,適合不同的目的——你如何選擇使用哪一個?有各種快取方法、構建搜尋索引的幾種方法等等——你如何權衡它們的利弊?你需要找出哪些工具和哪些方法最適合手頭的任務,當需要做單個工具無法單獨完成的事情時,組合工具可能會很困難。 +然而,隨著你的應用程式變得更加雄心勃勃,挑戰就會出現。有許多具有不同特性的資料庫系統,適合不同的目的——你如何選擇使用哪一個?有各種快取方法、構建搜尋索引的幾種方式等等——你如何推理它們的權衡?你需要找出哪些工具和哪些方法最適合手頭的任務,當你需要做單個工具無法單獨完成的事情時,組合工具可能會很困難。 -本書是一本指南,幫助你決定使用哪些技術以及如何組合它們。正如你將看到的,沒有一種方法從根本上優於其他方法;每種方法都有利弊。透過本書,你將學會提出正確的問題來評估和比較資料系統,這樣你就能找出哪種方法最能滿足你特定應用程式的需求。 +本書是一個指南,幫助你決定使用哪些技術以及如何組合它們。正如你將看到的,沒有一種方法從根本上優於其他方法;一切都有利弊。透過本書,你將學會提出正確的問題來評估和比較資料系統,以便你能找出哪種方法最能滿足你特定應用程式的需求。 -我們將透過檢視當今組織中資料的一些典型使用方式來開始我們的旅程。這裡的許多想法起源於 **企業軟體**(即大型組織(如大公司和政府)的軟體需求和工程實踐),因為歷史上只有大型組織擁有需要複雜技術解決方案的大資料量。如果你的資料量足夠小,你可以簡單地將其儲存在電子表格中!然而,最近小公司和初創企業管理大資料量並構建資料密集型系統也變得很常見。 +我們將透過觀察當今組織中資料的一些典型使用方式來開始我們的旅程。這裡的許多想法起源於 **企業軟體**(即大型組織(如大公司和政府)的軟體需求和工程實踐),因為歷史上只有大型組織擁有需要複雜技術解決方案的大資料量。如果你的資料量足夠小,你可以簡單地將其儲存在電子表格中!然而,最近小公司和初創公司管理大資料量並構建資料密集型系統也變得很常見。 -資料系統的關鍵挑戰之一是不同的人需要用資料做非常不同的事情。如果你在一家公司工作,你和你的團隊會有一套優先事項,而另一個團隊可能有完全不同的目標,即使你們可能使用相同的資料集!此外,這些目標可能沒有明確表達,這可能導致關於正確方法的誤解和分歧。 +資料系統的關鍵挑戰之一是不同的人需要用資料做非常不同的事情。如果你在一家公司工作,你和你的團隊將有一套優先事項,而另一個團隊可能有完全不同的目標,即使你們可能在處理相同的資料集!此外,這些目標可能沒有明確闡述,這可能導致對正確方法的誤解和分歧。 -為了幫助你理解可以做出哪些選擇,本章比較了幾個對比的概念,並探討了它們的權衡: +為了幫助你瞭解可以做出哪些選擇,本章比較了幾個對比概念,並探討了它們的權衡: * 事務型系統和分析型系統之間的區別(["分析型與事務型系統"](/tw/ch1#sec_introduction_analytics)); * 雲服務和自託管系統的利弊(["雲服務與自託管"](/tw/ch1#sec_introduction_cloud)); @@ -44,143 +49,136 @@ breadcrumbs: false > [!TIP] 術語:前端和後端 -本書中我們將討論的大部分內容都與 **後端開發** 有關。為了解釋這個術語:對於 Web 應用程式,在 Web 瀏覽器中執行的客戶端程式碼稱為 **前端**,而處理使用者請求的伺服器端程式碼稱為 **後端**。移動應用類似於前端,因為它們提供使用者介面,通常透過網際網路與伺服器端後端通訊。前端有時會在使用者裝置上本地管理資料 [^2],但最大的資料基礎設施挑戰通常在於後端:前端只需要處理一個使用者的資料,而後端代表 **所有** 使用者管理資料。 +本書中我們將討論的大部分內容都與 **後端開發** 有關。為了解釋這個術語:對於 Web 應用程式,在 Web 瀏覽器中執行的客戶端程式碼稱為 **前端**,處理使用者請求的伺服器端程式碼稱為 **後端**。移動應用類似於前端,它們提供使用者介面,通常透過網際網路與伺服器端後端通訊。前端有時在使用者裝置上本地管理資料 [^2],但最大的資料基礎設施挑戰通常在於後端:前端只需要處理一個使用者的資料,而後端代表 **所有** 使用者管理資料。 -後端服務通常可透過 HTTP(有時是 WebSocket)訪問;它通常由一些應用程式程式碼組成,這些程式碼在一個或多個數據庫中讀寫資料,有時還與其他資料系統(如快取或訊息佇列)介面(我們可能統稱為 **資料基礎設施**)。應用程式程式碼通常是 **無狀態的**(即,當它完成處理一個 HTTP 請求時,它會忘記關於該請求的所有內容),任何需要從一個請求持續到另一個請求的資訊都需要儲存在客戶端或伺服器端資料基礎設施中。 +後端服務通常可透過 HTTP(有時是 WebSocket)訪問;它通常由一些應用程式程式碼組成,這些程式碼在一個或多個數據庫中讀取和寫入資料,有時還與其他資料系統(如快取或訊息佇列)介面(我們可能將其統稱為 **資料基礎設施**)。應用程式程式碼通常是 **無狀態的**(即,當它完成處理一個 HTTP 請求時,它會忘記關於該請求的所有內容),任何需要從一個請求持續到另一個請求的資訊都需要儲存在客戶端或伺服器端資料基礎設施中。 ## 分析型與事務型系統 {#sec_introduction_analytics} -如果你在企業中從事資料系統工作,你可能會遇到幾種不同型別的資料工作人員。第一類是構建處理讀取和更新資料請求的服務的 **後端工程師**;這些服務通常直接或透過其他服務間接地為外部使用者提供服務(參見["微服務和無伺服器"](/tw/ch1#sec_introduction_microservices))。有時服務是供組織其他部分內部使用的。 +如果你在企業中從事資料系統工作,你可能會遇到幾種不同型別的資料工作者。第一類是 **後端工程師**,他們構建處理讀取和更新資料請求的服務;這些服務通常直接或間接地透過其他服務為外部使用者提供服務(參見["微服務與 Serverless"](/tw/ch1#sec_introduction_microservices))。有時服務是供組織其他部分內部使用的。 -除了管理後端服務的團隊外,通常還有另外兩組人需要訪問組織的資料:**業務分析師**,他們生成有關組織活動的報告,以幫助管理層做出更好的決策(**商業智慧** 或 **BI**),以及 **資料科學家**,他們在資料中尋找新穎的見解或建立由資料分析和機器學習/AI 支援的面向使用者的產品功能(例如,電子商務網站上的"購買了 X 的人也購買了 Y"推薦、預測分析如風險評分或垃圾郵件過濾,以及搜尋結果排名)。 +除了管理後端服務的團隊外,通常還有兩類人需要訪問組織的資料:**業務分析師**,他們生成關於組織活動的報告,以幫助管理層做出更好的決策(**商業智慧** 或 **BI**);以及 **資料科學家**,他們在資料中尋找新的見解,或建立由資料分析和機器學習/AI 支援的面向使用者的產品功能(例如,電子商務網站上的"購買了 X 的人也購買了 Y"推薦、風險評分或垃圾郵件過濾等預測分析,以及搜尋結果排名)。 -雖然業務分析師和資料科學家傾向於使用不同的工具並以不同的方式操作,但他們有一些共同點:兩者都執行 **分析**,這意味著他們檢視使用者和後端服務生成的資料,但他們通常不修改這些資料(除了可能修復錯誤)。他們可能會建立派生資料集,其中原始資料已經以某種方式處理過。這導致了兩種系統型別之間的分離——這種區別將貫穿本書: +儘管業務分析師和資料科學家傾向於使用不同的工具並以不同的方式操作,但他們有一些共同點:兩者都執行 **分析**,這意味著他們檢視使用者和後端服務生成的資料,但他們通常不修改這些資料(除了可能修復錯誤)。他們可能建立派生資料集,其中原始資料已經以某種方式處理過。這導致了兩種型別系統之間的分離——我們將在本書中使用這種區別: -* **事務型系統** 由後端服務和資料基礎設施組成,在這裡建立資料,例如透過為外部使用者提供服務。在這裡,應用程式程式碼基於使用者執行的操作讀取和修改其資料庫中的資料。 +* **事務型系統** 由後端服務和資料基礎設施組成,在這裡建立資料,例如透過服務外部使用者。在這裡,應用程式程式碼基於使用者執行的操作讀取和修改其資料庫中的資料。 * **分析型系統** 服務於業務分析師和資料科學家的需求。它們包含來自事務型系統的只讀資料副本,並針對分析所需的資料處理型別進行了最佳化。 -正如我們將在下一節看到的,事務型和分析型系統通常由於充分的理由而保持分離。隨著這些系統的成熟,出現了兩個新的專門角色:**資料工程師** 和 **分析工程師**。資料工程師是知道如何整合事務型和分析型系統的人,並且更廣泛地負責組織的資料基礎設施 [^3]。 -分析工程師對資料進行建模和轉換,使其對組織中的業務分析師和資料科學家更有用 [^4]。 +正如我們將在下一節中看到的,事務型系統和分析型系統通常出於充分的理由而保持分離。隨著這些系統的成熟,出現了兩個新的專業角色:**資料工程師** 和 **分析工程師**。資料工程師是知道如何整合事務型系統和分析型系統的人,並更廣泛地負責組織的資料基礎設施 [^3]。分析工程師對資料進行建模和轉換,使其對組織中的業務分析師和資料科學家更有用 [^4]。 -許多工程師專門從事事務型或分析型方面。然而,本書涵蓋了事務型和分析型資料系統,因為兩者在組織內資料的生命週期中都發揮著重要作用。我們將深入探討用於向內部和外部使用者提供服務的資料基礎設施,以便你能夠更好地與分界線另一邊的同事合作。 +許多工程師專注於事務型或分析型方面。然而,本書涵蓋了事務型和分析型資料系統,因為兩者在組織內資料生命週期中都扮演著重要角色。我們將深入探討用於向內部和外部使用者提供服務的資料基礎設施,以便你能更好地與這一分界線另一邊的同事合作。 ### 事務處理與分析的特徵 {#sec_introduction_oltp} -在商業資料處理的早期,對資料庫的寫入通常對應於正在發生的 **商業交易**:進行銷售、向供應商下訂單、支付員工工資等。隨著資料庫擴充套件到不涉及資金交換的領域,術語 **事務** 仍然保留了下來,指的是形成邏輯單元的一組讀寫操作。 +在商業資料處理的早期,對資料庫的寫入通常對應於發生的 **商業交易**:進行銷售、向供應商下訂單、支付員工工資等。隨著資料庫擴充套件到不涉及金錢交換的領域,**事務** 這個術語仍然保留了下來,指的是形成邏輯單元的一組讀取和寫入。 > [!NOTE] -> [第 8 章](/tw/ch8#ch_transactions) 詳細探討了我們所說的事務的含義。本章寬泛地使用該術語來指代低延遲的讀寫操作。 +> [第 8 章](/tw/ch8#ch_transactions) 詳細探討了我們所說的事務的含義。本章鬆散地使用該術語來指代低延遲的讀取和寫入。 -儘管資料庫開始用於許多不同型別的資料——社交媒體上的帖子、遊戲中的動作、地址簿中的聯絡人等等——基本訪問模式仍然類似於處理商業交易。事務型系統通常透過某個鍵查詢少量記錄(這稱為 **點查詢**)。基於使用者的輸入插入、更新或刪除記錄。因為這些應用程式是互動式的,這種訪問模式被稱為 **聯機事務處理**(OLTP)。 +儘管資料庫開始用於許多不同型別的資料——社交媒體上的帖子、遊戲中的移動、地址簿中的聯絡人等等——基本的訪問模式仍然類似於處理商業交易。事務型系統通常透過某個鍵查詢少量記錄(這稱為 **點查詢**)。基於使用者的輸入插入、更新或刪除記錄。因為這些應用程式是互動式的,這種訪問模式被稱為 **聯機事務處理**(OLTP)。 -然而,資料庫也越來越多地用於分析,與 OLTP 相比具有非常不同的訪問模式。通常,分析查詢會掃描大量記錄,並計算聚合統計資訊(如計數、總和或平均值),而不是將單個記錄返回給使用者。例如,超市連鎖店的業務分析師可能想要回答以下分析查詢: +然而,資料庫也越來越多地用於分析,與 OLTP 相比,分析具有非常不同的訪問模式。通常,分析查詢會掃描大量記錄,並計算聚合統計資訊(如計數、求和或平均值),而不是將單個記錄返回給使用者。例如,超市連鎖店的業務分析師可能想要回答以下分析查詢: * 我們每家商店在一月份的總收入是多少? -* 在我們最近的促銷期間,我們賣出的香蕉比平時多了多少? -* 哪個品牌的嬰兒食品最常與品牌 X 紙尿褲一起購買? +* 在我們最近的促銷期間,我們賣出了比平時多多少香蕉? +* 哪個品牌的嬰兒食品最常與 X 品牌尿布一起購買? -這些查詢產生的報告對商業智慧很重要,幫助管理層決定下一步做什麼。為了將這種使用資料庫的模式與事務處理區分開來,它被稱為 **聯機分析處理**(OLAP)[^5]。 -OLTP 和分析之間的區別並不總是明確的,但一些典型特徵列在[表 1-1](/tw/ch1#tab_oltp_vs_olap) 中。 +這些型別查詢產生的報告對商業智慧很重要,幫助管理層決定下一步做什麼。為了將這種使用資料庫的模式與事務處理區分開來,它被稱為 **聯機分析處理**(OLAP)[^5]。OLTP 和分析之間的區別並不總是很明確,但[表 1-1](/tw/ch1#tab_oltp_vs_olap) 列出了一些典型特徵。 -{{< figure id="tab_oltp_vs_olap" title="表 1-1. 比較事務型和分析型系統的特徵" class="w-full my-4" >}} +{{< figure id="tab_oltp_vs_olap" title="表 1-1. 事務型系統和分析型系統特徵比較" class="w-full my-4" >}} -| 屬性 | 事務型系統(OLTP) | 分析型系統(OLAP) | -|--------|-----------------|---------------| -| 主要讀取模式 | 點查詢(按鍵獲取單個記錄) | 對大量記錄進行聚合 | -| 主要寫入模式 | 建立、更新和刪除單個記錄 | 批次匯入(ETL)或事件流 | -| 人類使用者示例 | Web/移動應用程式的終端使用者 | 內部分析師,用於決策支援 | -| 機器使用示例 | 檢查操作是否被授權 | 檢測欺詐/濫用模式 | -| 查詢型別 | 固定的查詢集,由應用程式預定義 | 分析師可以進行任意查詢 | -| 資料代表 | 資料的最新狀態(當前時間點) | 隨時間發生的事件歷史 | -| 資料集大小 | GB 到 TB | TB 到 PB | +| 屬性 | 事務型系統(OLTP) | 分析型系統(OLAP) | +|-----------------|----------------------------------------|-----------------------------------| +| 主要讀取模式 | 點查詢(透過鍵獲取單個記錄) | 對大量記錄進行聚合 | +| 主要寫入模式 | 建立、更新和刪除單個記錄 | 批次匯入(ETL)或事件流 | +| 人類使用者示例 | Web/移動應用程式的終端使用者 | 內部分析師,用於決策支援 | +| 機器使用示例 | 檢查操作是否被授權 | 檢測欺詐/濫用模式 | +| 查詢型別 | 固定的查詢集,由應用程式預定義 | 分析師可以進行任意查詢 | +| 資料代表 | 資料的最新狀態(當前時間點) | 隨時間發生的事件歷史 | +| 資料集大小 | GB 到 TB | TB 到 PB | > [!NOTE] -> **OLAP** 中 **聯機** 的含義不清楚;它可能指的是查詢不僅用於預定義的報告,而且分析師使用 OLAP 系統互動式地進行探索性查詢。 +> OLAP 中 **online** 的含義不明確;它可能指的是查詢不僅用於預定義的報告,而且分析師可以互動式地使用 OLAP 系統進行探索性查詢。 -對於事務型系統,通常不允許使用者構建自定義 SQL 查詢並在資料庫上執行它們,因為這可能會允許他們讀取或修改他們無權訪問的資料。此外,他們可能會編寫執行成本昂貴的查詢,從而影響其他使用者的資料庫效能。出於這些原因,OLTP 系統主要執行嵌入到應用程式程式碼中的固定查詢集,僅偶爾使用一次性自定義查詢進行維護或故障排除。另一方面,分析資料庫通常賦予使用者手動編寫任意 SQL 查詢的自由,或使用資料視覺化或儀表板工具(如 Tableau、Looker 或 Microsoft Power BI)自動生成查詢。 +在事務型系統中,通常不允許使用者構建自定義 SQL 查詢並在資料庫上執行它們,因為這可能會允許他們讀取或修改他們沒有許可權訪問的資料。此外,他們可能編寫執行成本高昂的查詢,從而影響其他使用者的資料庫效能。出於這些原因,OLTP 系統主要執行嵌入到應用程式程式碼中的固定查詢集,只偶爾使用一次性自定義查詢進行維護或故障排除。另一方面,分析資料庫通常讓使用者可以自由地手動編寫任意 SQL 查詢,或使用 Tableau、Looker 或 Microsoft Power BI 等資料視覺化或儀表板工具自動生成查詢。 -還有一種專為分析工作負載(聚合多條記錄的查詢)設計但嵌入到面向使用者產品中的系統型別。這一類別被稱為 **產品分析** 或 **即時分析**,專為此類用途設計的系統包括 Pinot、Druid 和 ClickHouse [^6]。 +還有一種型別的系統是為分析工作負載(對許多記錄進行聚合的查詢)設計的,但嵌入到面向使用者的產品中。這一類別被稱為 **產品分析** 或 **即時分析**,為這種用途設計的系統包括 Pinot、Druid 和 ClickHouse [^6]。 ### 資料倉庫 {#sec_introduction_dwh} -起初,相同的資料庫用於事務處理和分析查詢。SQL 在這方面相當靈活:它對兩種型別的查詢都很有效。然而,在 20 世紀 80 年代末和 90 年代初,公司停止將其 OLTP 系統用於分析目的,轉而在單獨的資料庫系統上執行分析的趨勢出現了。這個單獨的資料庫被稱為 **資料倉庫**。 +起初,相同的資料庫既用於事務處理,也用於分析查詢。SQL 在這方面相當靈活:它對兩種型別的查詢都很有效。然而,在 20 世紀 80 年代末和 90 年代初,公司傾向於停止使用其 OLTP 系統進行分析目的,而是在單獨的資料庫系統上執行分析。這個單獨的資料庫被稱為 **資料倉庫**。 -一家大型企業可能有幾十個甚至幾百個聯機事務處理系統:為面向客戶的網站提供支援的系統、控制實體店中的銷售點(收銀臺)系統、跟蹤倉庫庫存、規劃車輛路線、管理供應商、管理員工以及執行許多其他任務的系統。這些系統中的每一個都很複雜,需要一個團隊來維護它,因此這些系統最終大多獨立於彼此執行。 +一家大型企業可能有幾十個,甚至上百個聯機事務處理系統:為面向客戶的網站提供動力的系統、控制實體店中的銷售點(收銀臺)系統、跟蹤倉庫中的庫存、規劃車輛路線、管理供應商、管理員工以及執行許多其他任務。這些系統中的每一個都很複雜,需要一個團隊來維護它,因此這些系統最終主要是相互獨立地執行。 -由於幾個原因,業務分析師和資料科學家直接查詢這些 OLTP 系統通常是不可取的: +出於幾個原因,業務分析師和資料科學家直接查詢這些 OLTP 系統通常是不可取的: -* 感興趣的資料可能分散在多個事務型系統中,使得在單個查詢中組合這些資料集變得困難(這個問題被稱為 **資料孤島**); -* 適合 OLTP 的模式和資料佈局型別不太適合分析(參見["用於分析的星型和雪花型模式"](/tw/ch3#sec_datamodels_analytics)); +* 感興趣的資料可能分佈在多個事務型系統中,使得在單個查詢中組合這些資料集變得困難(稱為 **資料孤島** 的問題); +* 適合 OLTP 的模式和資料佈局不太適合分析(參見["星型和雪花型:分析模式"](/tw/ch3#sec_datamodels_analytics)); * 分析查詢可能相當昂貴,在 OLTP 資料庫上執行它們會影響其他使用者的效能;以及 * OLTP 系統可能位於使用者出於安全或合規原因不允許直接訪問的單獨網路中。 -相比之下,**資料倉庫** 是一個單獨的資料庫,分析師可以隨心所欲地查詢,而不會影響 OLTP 操作 [^7]。 -正如我們將在[第 4 章](/tw/ch4#ch_storage)中看到的,資料倉庫通常以與 OLTP 資料庫非常不同的方式儲存資料,以最佳化分析中常見的查詢型別。 +相比之下,**資料倉庫** 是一個單獨的資料庫,分析師可以隨心所欲地查詢,而不會影響 OLTP 操作 [^7]。正如我們將在[第 4 章](/tw/ch4#ch_storage)中看到的,資料倉庫通常以與 OLTP 資料庫非常不同的方式儲存資料,以最佳化分析中常見的查詢型別。 -資料倉庫包含公司中所有各種 OLTP 系統中資料的只讀副本。資料從 OLTP 資料庫中提取(使用定期資料轉儲或連續更新流),轉換為分析友好的模式,清理,然後載入到資料倉庫中。將資料匯入資料倉庫的過程稱為 **提取-轉換-載入**(ETL),如[圖 1-1](/tw/ch1#fig_dwh_etl) 所示。有時 **轉換** 和 **載入** 步驟的順序會交換(即,轉換在資料倉庫中完成,在載入之後),導致 **ELT**。 +資料倉庫包含公司中所有各種 OLTP 系統中資料的只讀副本。資料從 OLTP 資料庫中提取(使用定期資料轉儲或連續更新流),轉換為分析友好的模式,清理,然後載入到資料倉庫中。這種將資料匯入資料倉庫的過程稱為 **提取-轉換-載入**(ETL),如[圖 1-1](/tw/ch1#fig_dwh_etl) 所示。有時 **轉換** 和 **載入** 步驟的順序會互換(即,在載入後在資料倉庫中進行轉換),從而產生 **ELT**。 {{< figure src="/fig/ddia_0101.png" id="fig_dwh_etl" caption="圖 1-1. ETL 到資料倉庫的簡化概述。" class="w-full my-4" >}} -在某些情況下,ETL 過程的資料來源是外部 SaaS 產品,如客戶關係管理(CRM)、電子郵件營銷或信用卡處理系統。在這些情況下,你無法直接訪問原始資料庫,因為它只能透過軟體供應商的 API 訪問。將這些外部系統的資料帶入你自己的資料倉庫可以實現透過 SaaS API 無法進行的分析。用於 SaaS API 的 ETL 通常由專門的資料聯結器服務(如 Fivetran、Singer 或 AirByte)實現。 +在某些情況下,ETL 過程的資料來源是外部 SaaS 產品,如客戶關係管理(CRM)、電子郵件營銷或信用卡處理系統。在這些情況下,你無法直接訪問原始資料庫,因為它只能透過軟體供應商的 API 訪問。將這些外部系統的資料匯入你自己的資料倉庫可以實現透過 SaaS API 無法實現的分析。SaaS API 的 ETL 通常由專門的資料聯結器服務(如 Fivetran、Singer 或 AirByte)實現。 -一些資料庫系統提供 **混合事務/分析處理**(HTAP),旨在在單個系統中啟用 OLTP 和分析,而無需從一個系統到另一個系統的 ETL [^8] [^9]。 -然而,許多 HTAP 系統內部由一個 OLTP 系統和一個單獨的分析系統組成,隱藏在一個公共介面後面——因此兩者之間的區別對於理解這些系統的工作原理仍然很重要。 +一些資料庫系統提供 **混合事務/分析處理**(HTAP),旨在在單個系統中啟用 OLTP 和分析,而無需從一個系統 ETL 到另一個系統 [^8] [^9]。然而,許多 HTAP 系統內部由一個 OLTP 系統與一個單獨的分析系統耦合組成,隱藏在公共介面後面——因此兩者之間的區別對於理解這些系統如何工作仍然很重要。 -此外,即使 HTAP 存在,由於不同的目標和要求,事務型和分析型系統之間的分離也很常見。特別是,每個事務型系統都有自己的資料庫被認為是良好的實踐(參見["微服務和無伺服器"](/tw/ch1#sec_introduction_microservices)),導致數百個獨立的事務型資料庫;另一方面,企業通常只有一個數據倉庫,以便業務分析師可以在單個查詢中組合來自多個事務型系統的資料。 +此外,儘管 HTAP 存在,但由於目標和要求不同,事務型系統和分析型系統之間的分離是常見的。特別是,每個事務型系統擁有自己的資料庫被認為是良好的做法(參見["微服務與 Serverless"](/tw/ch1#sec_introduction_microservices)),導致數百個單獨的事務型資料庫;另一方面,企業通常有一個單一的資料倉庫,以便業務分析師可以在單個查詢中組合來自多個事務型系統的資料。 -因此,HTAP 並不能取代資料倉庫。相反,它在同一個應用程式既需要執行掃描大量行的分析查詢,又需要以低延遲讀取和更新單個記錄的場景中很有用。例如,欺詐檢測可能涉及此類工作負載 [^10]。 +因此,HTAP 不會取代資料倉庫。相反,它在同一應用程式需要既執行掃描大量行的分析查詢,又以低延遲讀取和更新單個記錄的場景中很有用。例如,欺詐檢測可能涉及此類工作負載 [^10]。 -事務型和分析型系統之間的分離是更廣泛趨勢的一部分:隨著工作負載變得更加苛刻,系統變得更加專業化並針對特定工作負載進行最佳化。通用系統可以輕鬆處理小資料量,但規模越大,系統往往變得越專業化 [^11]。 +事務型系統和分析型系統之間的分離是更廣泛趨勢的一部分:隨著工作負載變得更加苛刻,系統變得更加專業化並針對特定工作負載進行最佳化。通用系統可以舒適地處理小資料量,但規模越大,系統往往變得越專業化 [^11]。 #### 從資料倉庫到資料湖 {#from-data-warehouse-to-data-lake} -資料倉庫通常使用透過 SQL 查詢的 **關係** 資料模型(參見[第 3 章](/tw/ch3#ch_datamodels)),可能使用專門的商業智慧軟體。這個模型非常適合業務分析師需要進行的查詢型別,但不太適合資料科學家的需求,他們可能需要執行以下任務: +資料倉庫通常使用透過 SQL 查詢的 **關係** 資料模型(參見[第 3 章](/tw/ch3#ch_datamodels)),可能使用專門的商業智慧軟體。這個模型很適合業務分析師需要進行的查詢型別,但不太適合資料科學家的需求,他們可能需要執行以下任務: -* 將資料轉換為適合訓練機器學習模型的形式;通常這需要將資料庫表的行和列轉換為稱為 **特徵** 的數值向量或矩陣。以最大化訓練模型效能的方式執行此轉換的過程稱為 **特徵工程**,它通常需要難以使用 SQL 表達的自定義程式碼。 +* 將資料轉換為適合訓練機器學習模型的形式;這通常需要將資料庫表的行和列轉換為稱為 **特徵** 的數值向量或矩陣。以最大化訓練模型效能的方式執行這種轉換的過程稱為 **特徵工程**,它通常需要難以用 SQL 表達的自定義程式碼。 * 獲取文字資料(例如,產品評論)並使用自然語言處理技術嘗試從中提取結構化資訊(例如,作者的情感或他們提到的主題)。同樣,他們可能需要使用計算機視覺技術從照片中提取結構化資訊。 -雖然已經努力將機器學習運算子新增到 SQL 資料模型 [^12] 並在關係基礎上構建高效的機器學習系統 [^13],但許多資料科學家更喜歡不在關係資料庫(如資料倉庫)中工作。相反,許多人更喜歡使用 Python 資料分析庫(如 pandas 和 scikit-learn)、統計分析語言(如 R)以及分散式分析框架(如 Spark)[^14]。 -我們將在["資料框、矩陣和陣列"](/tw/ch3#sec_datamodels_dataframes)中進一步討論這些。 +儘管已經努力將機器學習運算元新增到 SQL 資料模型 [^12] 並在關係基礎上構建高效的機器學習系統 [^13],但許多資料科學家不喜歡在資料倉庫等關係資料庫中工作。相反,許多人更喜歡使用 Python 資料分析庫(如 pandas 和 scikit-learn)、統計分析語言(如 R)和分散式分析框架(如 Spark)[^14]。我們將在["資料框、矩陣和陣列"](/tw/ch3#sec_datamodels_dataframes)中進一步討論這些。 -因此,組織面臨著以適合資料科學家使用的形式提供資料的需求。答案是 **資料湖**:一箇中央資料儲存庫,儲存可能對分析有用的任何資料的副本,透過 ETL 過程從事務型系統獲得。與資料倉庫的區別在於,資料湖只包含檔案,而不強加任何特定的檔案格式或資料模型。資料湖中的檔案可能是資料庫記錄的集合,使用檔案格式(如 Avro 或 Parquet)編碼(參見[第 5 章](/tw/ch5#ch_encoding)),但它們同樣可以包含文字、影像、影片、感測器讀數、稀疏矩陣、特徵向量、基因組序列或任何其他型別的資料 [^15]。 -除了更加靈活之外,這通常也比關係資料儲存更便宜,因為資料湖可以使用商品化的檔案儲存,如物件儲存(參見["雲原生系統架構"](/tw/ch1#sec_introduction_cloud_native))。 +因此,組織面臨著以適合資料科學家使用的形式提供資料的需求。答案是 **資料湖**:一個集中的資料儲存庫,儲存任何可能對分析有用的資料副本,透過 ETL 過程從事務型系統獲得。與資料倉庫的區別在於,資料湖只是包含檔案,而不強制任何特定的檔案格式或資料模型。資料湖中的檔案可能是資料庫記錄的集合,使用 Avro 或 Parquet 等檔案格式編碼(參見[第 5 章](/tw/ch5#ch_encoding)),但它們同樣可以包含文字、影像、影片、感測器讀數、稀疏矩陣、特徵向量、基因組序列或任何其他型別的資料 [^15]。除了更靈活之外,這通常也比關係資料儲存更便宜,因為資料湖可以使用商品化的檔案儲存,如物件儲存(參見["雲原生系統架構"](/tw/ch1#sec_introduction_cloud_native))。 -ETL 過程已經被推廣為 **資料管道**,在某些情況下,資料湖已成為從事務型系統到資料倉庫路徑上的中間站。資料湖包含由事務型系統產生的"原始"形式的資料,沒有轉換為關係資料倉庫模式。這種方法的優點是資料的每個消費者都可以將原始資料轉換為最適合其需求的形式。它被稱為 **壽司原則**:"原始資料更好"[^16]。 +ETL 過程已經泛化為 **資料管道**,在某些情況下,資料湖已成為從事務型系統到資料倉庫路徑上的中間站。資料湖包含事務型系統產生的"原始"形式的資料,沒有轉換為關係資料倉庫模式。這種方法的優勢在於,每個資料消費者都可以將原始資料轉換為最適合其需求的形式。它被稱為 **壽司原則**:"原始資料更好"[^16]。 -除了將資料從資料湖載入到單獨的資料倉庫之外,還可以直接在資料湖中的檔案上執行典型的資料倉庫工作負載(SQL 查詢和業務分析),以及資料科學/機器學習工作負載。這種架構被稱為 **資料湖倉**,它需要一個查詢執行引擎和一個元資料(例如,模式管理)層來擴充套件資料湖的檔案儲存 [^17]。 +除了從資料湖載入資料到單獨的資料倉庫之外,還可以直接在資料湖中的檔案上執行典型的資料倉庫工作負載(SQL 查詢和業務分析),以及資料科學/機器學習工作負載。這種架構被稱為 **資料湖倉**,它需要一個查詢執行引擎和一個元資料(例如,模式管理)層來擴充套件資料湖的檔案儲存 [^17]。 -Apache Hive、Spark SQL、Presto 和 Trino 是這種方法的示例。 +Apache Hive、Spark SQL、Presto 和 Trino 是這種方法的例子。 #### 超越資料湖 {#beyond-the-data-lake} -隨著分析實踐的成熟,組織越來越關注分析系統和資料管道的管理和操作,例如 DataOps 宣言中所捕獲的 [^18]。 -其中一部分是治理、隱私以及符合 GDPR 和 CCPA 等法規的問題,我們將在["資料系統、法律與社會"](/tw/ch1#sec_introduction_compliance)和[待補充連結]中討論。 +隨著分析實踐的成熟,組織越來越關注分析系統和資料管道的管理和運維,例如 DataOps 宣言所捕獲的 [^18]。其中一部分是治理、隱私和遵守 GDPR 和 CCPA 等法規的問題,我們將在["資料系統、法律與社會"](/tw/ch1#sec_introduction_compliance)和[待補充連結]中討論。 -此外,分析資料越來越多地不僅作為檔案和關係表提供,還作為事件流提供(參見[待補充連結])。使用基於檔案的資料分析,你可以定期(例如,每天)重新執行分析以響應資料的變化,但流處理允許分析系統以秒級的速度響應事件。根據應用程式及其時間敏感性,流處理方法可能很有價值,例如識別和阻止潛在的欺詐或濫用活動。 +此外,分析資料越來越多地不僅作為檔案和關係表提供,還作為事件流(參見[待補充連結])。使用基於檔案的資料分析,你可以定期(例如,每天)重新執行分析以響應資料的變化,但流處理允許分析系統以秒級的速度響應事件。根據應用程式及其時間敏感性,流處理方法可能很有價值,例如識別和阻止潛在的欺詐或濫用活動。 -在某些情況下,分析系統的輸出被提供給事務型系統(這個過程有時被稱為 **反向 ETL** [^19])。例如,在分析系統上訓練的機器學習模型可能會部署到生產環境中,以便它可以為終端使用者生成推薦,例如"購買了 X 的人也購買了 Y"。這種分析系統的部署輸出也被稱為 **資料產品** [^20]。 -機器學習模型可以使用專門的工具(如 TFX、Kubeflow 或 MLflow)部署到事務型系統。 +在某些情況下,分析系統的輸出被提供給事務型系統(這個過程有時被稱為 **反向 ETL** [^19])。例如,在分析系統中訓練的機器學習模型可能會部署到生產環境中,以便為終端使用者生成推薦,例如"購買了 X 的人也購買了 Y"。這種分析系統的部署輸出也被稱為 **資料產品** [^20]。機器學習模型可以使用 TFX、Kubeflow 或 MLflow 等專門工具部署到事務型系統。 ### 權威資料來源與派生資料 {#sec_introduction_derived} -與事務型和分析型系統的區別相關,本書還區分了 **權威記錄系統** 和 **派生資料系統**。這些術語很有用,因為它們可以幫助你理清資料在系統中的流動: +與事務型系統和分析型系統之間的區別相關,本書還區分了 **權威記錄系統** 和 **派生資料系統**。這些術語很有用,因為它們可以幫助你澄清資料在系統中的流動: 權威記錄系統 -: 權威記錄系統(SoR, System of Record),也稱為 **權威資料來源**,儲存某些資料的權威或 **規範** 版本。當新資料進入時,例如作為使用者輸入,它首先被寫入這裡。每個事實只表示一次(表示通常是 **規範化的**;參見["規範化、反規範化和連線"](/tw/ch3#sec_datamodels_normalization))。如果另一個系統和權威記錄系統之間存在任何差異,那麼權威記錄系統中的值(按定義)是正確的。 +: 權威記錄系統,也稱為 **權威資料來源**,儲存某些資料的權威或 **規範** 版本。當新資料進入時,例如作為使用者輸入,它首先寫入這裡。每個事實只表示一次(表示通常是 **規範化** 的;參見["規範化、反規範化和連線"](/tw/ch3#sec_datamodels_normalization))。如果另一個系統與權威記錄系統之間存在任何差異,那麼權威記錄系統中的值(根據定義)是正確的。 派生資料系統 -: 派生系統中的資料是透過獲取另一個系統中的一些現有資料並以某種方式轉換或處理它的結果。如果你丟失了派生資料,你可以從原始源重新建立它。一個經典的例子是快取:如果存在,可以從快取中提供資料,但如果快取不包含你需要的內容,你可以回退到底層資料庫。反規範化的值、索引、物化檢視、轉換的資料表示以及在資料集上訓練的模型也屬於這一類別。 +: 派生系統中的資料是從另一個系統獲取一些現有資料並以某種方式轉換或處理它的結果。如果你丟失了派生資料,你可以從原始源重新建立它。一個經典的例子是快取:如果存在,可以從快取提供資料,但如果快取不包含你需要的內容,你可以回退到底層資料庫。反規範化值、索引、物化檢視、轉換的資料表示和在資料集上訓練的模型也屬於這一類別。 -從技術上講,派生資料是 **冗餘的**,因為它複製了現有資訊。然而,它對於在讀查詢上獲得良好效能通常是必不可少的。你可以從單個源派生幾個不同的資料集,使你能夠從不同的"視角"檢視資料。 +從技術上講,派生資料是 **冗餘** 的,因為它複製了現有資訊。然而,它通常對於在讀取查詢上獲得良好效能至關重要。你可以從單個源派生幾個不同的資料集,使你能夠從不同的"視角"檢視資料。 -分析系統通常是派生資料系統,因為它們是在其他地方建立的資料的消費者。事務型服務可能包含權威記錄系統和派生資料系統的混合。權威記錄系統是資料首次寫入的主要資料庫,而派生資料系統是加速常見讀取操作的索引和快取,特別是對於權威記錄系統無法有效回答的查詢。 +分析系統通常是派生資料系統,因為它們是在其他地方建立的資料的消費者。事務型服務可能包含權威記錄系統和派生資料系統的混合。權威記錄系統是首先寫入資料的主資料庫,而派生資料系統是加速常見讀取操作的索引和快取,特別是對於權威記錄系統無法有效回答的查詢。 -大多數資料庫、儲存引擎和查詢語言本質上不是權威記錄系統或派生系統。資料庫只是一個工具:如何使用它取決於你。權威記錄系統和派生資料系統之間的區別不取決於工具,而是取決於你在應用程式中如何使用它。透過明確哪些資料是從哪些其他資料派生的,你可以為原本令人困惑的系統架構帶來清晰度。 +大多數資料庫、儲存引擎和查詢語言本質上不是權威記錄系統或派生系統。資料庫只是一個工具:如何使用它取決於你。權威記錄系統和派生資料系統之間的區別不取決於工具,而取決於你如何在應用程式中使用它。透過明確哪些資料是從哪些其他資料派生的,你可以為原本混亂的系統架構帶來清晰度。 -當一個系統中的資料是從另一個系統中的資料派生的時,你需要一個過程來在權威記錄系統中的原始資料發生變化時更新派生資料。不幸的是,許多資料庫的設計基於你的應用程式只需要使用那一個數據庫的假設,它們不容易整合多個系統以傳播此類更新。在[待補充連結]中,我們將討論 **資料整合** 的方法,這允許我們組合多個數據系統來實現單個系統無法完成的事情。 +當一個系統中的資料來源自另一個系統中的資料時,你需要一個過程來在權威記錄系統中的原始資料發生變化時更新派生資料。不幸的是,許多資料庫的設計基於這樣的假設:你的應用程式只需要使用那一個數據庫,它們不易於整合多個系統以傳播此類更新。在[待補充連結]中,我們將討論 **資料整合** 的方法,這允許我們組合多個數據系統來實現單個系統無法做到的事情。 + +這就結束了我們對分析和事務處理的比較。在下一節中,我們將研究另一個你可能已經看到多次爭論的權衡。 -這就是我們對分析和事務處理比較的結束。在下一節中,我們將研究另一個你可能已經看到多次辯論的權衡。 @@ -188,243 +186,247 @@ Apache Hive、Spark SQL、Presto 和 Trino 是這種方法的示例。 對於組織需要做的任何事情,首要問題之一是:應該在內部完成,還是應該外包?應該自建還是購買? -最終,這是一個關於業務優先順序的問題。管理智慧認為,作為組織核心競爭力或競爭優勢的事情應該在內部完成,而非核心、常規或普通的事情應該留給供應商 [^21]。 -舉一個極端的例子,大多數公司不自己發電(除非他們是能源公司,撇開緊急備用電源不談),因為從電網購買電力更便宜。 +歸根結底,這是一個關於業務優先順序的問題。公認的管理智慧是,作為組織核心競爭力或競爭優勢的事物應該在內部完成,而非核心、例行或常見的事物應該留給供應商 [^21]。 +舉一個極端的例子,大多數公司不會自己發電(除非他們是能源公司,而且不考慮緊急備用電源),因為從電網購買電力更便宜。 -對於軟體,需要做出兩個重要決定:誰構建軟體以及誰部署它。外包每個決定的程度有多種可能性,如[圖 1-2](/tw/ch1#fig_cloud_spectrum) 所示。一個極端是你自己編寫和執行的定製軟體;另一個極端是廣泛使用的雲服務或軟體即服務(SaaS)產品,由外部供應商實施和運營,你只能透過 Web 介面或 API 訪問。 +對於軟體,需要做出的兩個重要決定是誰構建軟體和誰部署它。有一系列可能性,每個決定都在不同程度上外包,如[圖 1-2](/tw/ch1#fig_cloud_spectrum) 所示。 +一個極端是你自己編寫並在內部執行的定製軟體;另一個極端是廣泛使用的雲服務或軟體即服務(SaaS)產品,由外部供應商實施和運營,你只能透過 Web 介面或 API 訪問。 -{{< figure src="/fig/ddia_0102.png" id="fig_cloud_spectrum" caption="圖 1-2. 軟體型別及其操作的範圍。" class="w-full my-4" >}} +{{< figure src="/fig/ddia_0102.png" id="fig_cloud_spectrum" caption="圖 1-2. 軟體型別及其運維的範圍。" class="w-full my-4" >}} -中間地帶是你 **自託管** 的現成軟體(開源或商業),即自己部署——例如,如果你下載 MySQL 並將其安裝在你控制的伺服器上。這可能在你自己的硬體上(通常稱為 **本地部署**,即使伺服器實際上在租用的資料中心機架中,而不是字面上在你自己的場所),或者在雲中的虛擬機器上(**基礎設施即服務** 或 IaaS)。沿著這個範圍還有更多的點,例如,採用開源軟體並執行它的修改版本。 +中間地帶是你 **自託管** 的現成軟體(開源或商業),即自己部署——例如,如果你下載 MySQL 並將其安裝在你控制的伺服器上。 +這可能在你自己的硬體上(通常稱為 **本地部署**,即使伺服器實際上在租用的資料中心機架中而不是字面上在你自己的場所) +,或者在雲中的虛擬機器上(**基礎設施即服務** 或 IaaS)。沿著這個範圍還有更多的點,例如,採用開源軟體並執行其修改版本。 -與這個範圍分開的還有 **如何** 部署服務的問題,無論是在雲中還是本地——例如,你是否使用像 Kubernetes 這樣的編排框架。然而,部署工具的選擇超出了本書的範圍,因為其他因素對資料系統的架構有更大的影響。 +與這個範圍分開的還有 **如何** 部署服務的問題,無論是在雲中還是在本地——例如,是否使用 Kubernetes 等編排框架。 +然而,部署工具的選擇超出了本書的範圍,因為其他因素對資料系統的架構有更大的影響。 ### 雲服務的利弊 {#sec_introduction_cloud_tradeoffs} -使用雲服務而不是自己執行類似的軟體,本質上是將該軟體的操作外包給雲提供商。有支援和反對雲服務的充分論據。雲提供商聲稱使用他們的服務可以節省你的時間和金錢,並且與設定自己的基礎設施相比,可以讓你更快地行動。 +使用雲服務而不是自己執行對應的軟體,本質上是將該軟體的運維外包給雲提供商。 +使用雲服務有充分的支援和反對理由。雲提供商聲稱,使用他們的服務可以節省你的時間和金錢,並相比自建基礎設施讓你更敏捷。 -雲服務是否實際上比自託管更便宜、更容易,很大程度上取決於你的技能和系統的工作負載。如果你已經有設定和操作所需系統的經驗,並且你的負載相當可預測(即,你需要的機器數量不會劇烈波動),那麼購買自己的機器並自己執行軟體通常更便宜 [^22] [^23]。 +雲服務實際上是否比自託管更便宜、更容易,很大程度上取決於你的技能和系統的工作負載。 +如果你已經有設定和運維所需系統的經驗,並且你的負載相當可預測(即,你需要的機器數量不會劇烈波動), +那麼購買自己的機器並自己在上面執行軟體通常更便宜 [^22] [^23]。 -另一方面,如果你需要一個你還不知道如何部署和操作的系統,那麼採用雲服務通常比自己學習管理系統更容易、更快。如果你必須專門僱用和培訓員工來維護和作業系統,那可能會變得非常昂貴。當你使用雲時,你仍然需要一個運維團隊(參見["雲時代的運維"](/tw/ch1#sec_introduction_operations)),但外包基本系統管理可以讓你的團隊專注於更高層次的問題。 +另一方面,如果你需要一個你還不知道如何部署和運維的系統,那麼採用雲服務通常比學習自己管理系統更容易、更快。 +如果你必須專門僱用和培訓員工來維護和運營系統,那可能會變得非常昂貴。 +使用雲時你仍然需要一個運維團隊(參見["雲時代的運維"](/tw/ch1#sec_introduction_operations)),但外包基本的系統管理可以讓你的團隊專注於更高層次的問題。 -當你將系統的操作外包給專門執行該服務的公司時,這可能會帶來更好的服務,因為提供商從為許多客戶提供服務中獲得了運營專業知識。另一方面,如果你自己執行服務,你可以配置和調整它以在你的特定工作負載上表現良好;雲服務不太可能願意代表你進行此類自定義。 +當你將系統的運維外包給專門運維該服務的公司時,可能會帶來更好的服務,因為供應商在向許多客戶提供服務中獲得了專業運維知識。 +另一方面,如果你自己運維服務,你可以配置和調整它,以專門針對你特定的工作負載進行最佳化,而云服務不太可能願意替你進行此類定製。 -如果你的系統負載隨時間變化很大,雲服務特別有價值。如果你配置機器以能夠處理峰值負載,但這些計算資源大部分時間都處於閒置狀態,系統就變得不太划算。在這種情況下,雲服務的優勢在於它們可以更容易地根據需求變化擴大或縮小你的計算資源。 +如果你的系統負載隨時間變化很大,雲服務特別有價值。如果你配置機器以能夠處理峰值負載,但這些計算資源大部分時間都處於空閒狀態,系統就變得不太具有成本效益。 +在這種情況下,雲服務的優勢在於它們可以更容易地根據需求變化向上或向下擴充套件你的計算資源。 -例如,分析系統通常具有極其可變的負載:快速執行大型分析查詢需要並行的大量計算資源,但一旦查詢完成,這些資源就會閒置,直到使用者進行下一個查詢。預定義的查詢(例如,用於每日報告)可以排隊和排程以平滑負載,但對於互動式查詢,你希望它們完成得越快,工作負載就越可變。如果你的資料集如此之大,以至於快速查詢它需要大量的計算資源,使用雲可以節省資金,因為你可以將未使用的資源返回給提供商,而不是讓它們閒置。對於較小的資料集,這種差異不太顯著。 +例如,分析系統通常具有極其可變的負載:快速執行大型分析查詢需要並行使用大量計算資源,但一旦查詢完成,這些資源就會處於空閒狀態,直到使用者進行下一個查詢。 +預定義的查詢(例如,每日報告)可以排隊和排程以平滑負載,但對於互動式查詢,你希望它們完成得越快,工作負載就變得越可變。 +如果你的資料集如此之大,以至於快速查詢需要大量的計算資源,使用雲可以節省資金,因為你可以將未使用的資源返回給供應商,而不是讓它們閒置。對於較小的資料集,這種差異不太顯著。 雲服務的最大缺點是你無法控制它: -* 如果它缺少你需要的功能,你所能做的就是禮貌地詢問供應商是否會新增它;你通常不能自己實現它。 -* 如果服務宕機,你所能做的就是等待它恢復。 -* 如果你使用服務的方式觸發了錯誤或導致效能問題,你將很難診斷問題。使用你自己執行的軟體,你可以從作業系統獲取效能指標和除錯資訊,以幫助你理解其行為,並且你可以檢視伺服器日誌,但使用供應商託管的服務,你通常無法訪問這些內部資訊。 -* 此外,如果服務關閉或變得不可接受地昂貴,或者如果供應商決定以你不喜歡的方式更改他們的產品,你將任由他們擺佈——繼續執行軟體的舊版本通常不是一個選項,因此你將被迫遷移到替代服務 [^24]。 - 如果有其他服務公開相容的 API,這種風險會得到緩解,但對於許多雲服務,沒有標準 API,這提高了切換成本,使供應商鎖定成為一個問題。 -* 雲提供商需要被信任以保持資料安全,這可能會使遵守隱私和安全法規的過程複雜化。 +* 如果它缺少你需要的功能,你所能做的就是禮貌地詢問供應商是否會新增它;你通常無法自己實現它。 +* 如果服務宕機,你所能做的就是等它恢復。 +* 如果你以觸發錯誤或導致效能問題的方式使用服務,你將很難診斷問題。對於你自己執行的軟體,你可以從作業系統獲取效能指標和除錯資訊來幫助你理解其行為,你可以檢視伺服器日誌,但對於供應商託管的服務,你通常無法訪問這些內部資訊。 +* 此外,如果服務關閉或變得無法接受地昂貴,或者如果供應商決定以你不喜歡的方式更改他們的產品,你就受制於他們 —— 繼續執行舊版本的軟體通常不是一個可行選項,所以你將被迫遷移到替代服務 [^24]。 + 如果有暴露相容 API 的替代服務,這種風險會得到緩解,但對於許多雲服務,沒有標準 API,這增加了切換成本,使供應商鎖定成為一個問題。 +* 雲供應商需要被信任以保持資料安全,這可能會使遵守隱私和安全法規的過程複雜化。 + +儘管有所有這些風險,組織在雲服務之上構建新應用程式或採用混合方法(在系統的某些部分使用雲服務)變得越來越流行。 +然而,雲服務不會取代所有內部資料系統:許多較舊的系統早於雲,對於任何具有現有云服務無法滿足的專業要求的服務,內部系統仍然是必要的。 +例如,對延遲非常敏感的應用程式(如高頻交易)需要完全控制硬體。 -儘管存在所有這些風險,組織在雲服務之上構建新應用程式或採用混合方法(雲服務用於系統的某些方面)變得越來越流行。然而,雲服務不會取代所有內部資料系統:許多較舊的系統早於雲,對於任何具有現有云服務無法滿足的專業要求的服務,內部系統仍然是必要的。例如,對延遲非常敏感的應用程式(如高頻交易)需要對硬體的完全控制。 ### 雲原生系統架構 {#sec_introduction_cloud_native} -除了具有不同的經濟模型(訂閱服務而不是購買硬體和許可軟體在其上執行),雲的興起還對資料系統在技術層面上的實現產生了深遠的影響。術語 **雲原生** 用於描述旨在利用雲服務的架構。 +除了具有不同的經濟模型(訂閱服務而不是購買硬體和許可軟體在其上執行)之外,雲的興起也對資料系統在技術層面的實現產生了深遠的影響。 +術語 **雲原生** 用於描述旨在利用雲服務的架構。 -原則上,幾乎任何你可以自託管的軟體也可以作為雲服務提供,實際上,許多流行的資料系統現在都有託管服務。然而,從頭開始設計為雲原生的系統已被證明具有幾個優勢:在相同硬體上具有更好的效能、更快地從故障中恢復、能夠快速擴充套件計算資源以匹配負載,以及支援更大的資料集 [^25] [^26] [^27]。 -[表 1-2](/tw/ch1#tab_cloud_native_dbs) 列出了兩種型別系統的一些示例。 +原則上,幾乎任何你可以自託管的軟體也可以作為雲服務提供,實際上,許多流行的資料系統現在都有託管服務。 +然而,從頭開始設計為雲原生的系統已被證明具有幾個優勢:在相同硬體上具有更好的效能、從故障中更快恢復、 +能夠快速擴充套件計算資源以匹配負載,以及支援更大的資料集 [^25] [^26] [^27]。[表 1-2](/tw/ch1#tab_cloud_native_dbs) 列出了兩種型別系統的一些示例。 -{{< figure id="#tab_cloud_native_dbs" title="表 1-2. 自託管和雲原生資料庫系統的示例" class="w-full my-4" >}} +{{< figure id="#tab_cloud_native_dbs" title="表 1-2. 自託管和雲原生資料庫系統示例" class="w-full my-4" >}} -| 類別 | 自託管系統 | 雲原生系統 | -|----------|---------------------------|---------------------------------------------------------------------| -| 事務型/OLTP | MySQL、PostgreSQL、MongoDB | AWS Aurora [^25]、Azure SQL DB Hyperscale [^26]、Google Cloud Spanner | -| 分析型/OLAP | Teradata、ClickHouse、Spark | Snowflake [^27]、Google BigQuery、Azure Synapse Analytics | +| 類別 | 自託管系統 | 雲原生系統 | +|------------------|----------------------------|----------------------------------------------------------------------| +| 事務型/OLTP | MySQL、PostgreSQL、MongoDB | AWS Aurora [^25]、Azure SQL DB Hyperscale [^26]、Google Cloud Spanner | +| 分析型/OLAP | Teradata、ClickHouse、Spark | Snowflake [^27]、Google BigQuery、Azure Synapse Analytics | #### 雲服務的分層 {#layering-of-cloud-services} -許多自託管資料系統具有非常簡單的系統要求:它們在傳統作業系統(如 Linux 或 Windows)上執行,將資料作為檔案儲存在檔案系統上,並透過標準網路協議(如 TCP/IP)進行通訊。少數系統依賴於特殊硬體,如 GPU(用於機器學習)或 RDMA 網路介面,但總的來說,自託管軟體傾向於使用非常通用的計算資源:CPU、RAM、檔案系統和 IP 網路。 +許多自託管資料系統的系統要求非常簡單:它們在傳統作業系統(如 Linux 或 Windows)上執行,將資料儲存為檔案系統上的檔案,並透過 TCP/IP 等標準網路協議進行通訊。 +少數系統依賴於特殊硬體,如 GPU(用於機器學習)或 RDMA 網路介面,但總的來說,自託管軟體傾向於使用非常通用的計算資源:CPU、RAM、檔案系統和 IP 網路。 -在雲中,這種型別的軟體可以在基礎設施即服務環境中執行,使用一個或多個具有一定 CPU、記憶體、磁碟和網路頻寬分配的虛擬機器(或 **例項**)。與物理機器相比,雲實例可以更快地配置,並且有更多種類的大小,但在其他方面它們類似於傳統計算機:你可以在其上執行任何你喜歡的軟體,但你負責自己管理它。 +在雲中,這種型別的軟體可以在基礎設施即服務環境中執行,使用一個或多個虛擬機器(或 **例項**),分配一定的 CPU、記憶體、磁碟和網路頻寬。 +與物理機器相比,雲實例可以更快地配置,並且有更多種類的大小,但除此之外,它們與傳統計算機類似:你可以在上面執行任何你喜歡的軟體,但你負責自己管理它。 -相比之下,雲原生服務的關鍵思想不僅是使用由你的作業系統管理的計算資源,還要基於較低級別的雲服務構建更高級別的服務。例如: +相比之下,雲原生服務的關鍵思想是不僅使用由作業系統管理的計算資源,還基於較低級別的雲服務構建更高級別的服務。例如: * **物件儲存** 服務(如 Amazon S3、Azure Blob Storage 和 Cloudflare R2)儲存大檔案。它們提供比典型檔案系統更有限的 API(基本檔案讀寫),但它們的優勢在於隱藏了底層物理機器:服務自動將資料分佈在許多機器上,因此你不必擔心任何一臺機器上的磁碟空間用完。即使某些機器或其磁碟完全故障,也不會丟失資料。 -* 許多其他服務又建立在物件儲存和其他雲服務之上:例如,Snowflake 是一個基於雲的分析資料庫(資料倉庫),它依賴於 S3 進行資料儲存 [^27],而其他一些服務又建立在 Snowflake 之上。 +* 許多其他服務反過來建立在物件儲存和其他雲服務之上:例如,Snowflake 是一個基於雲的分析資料庫(資料倉庫),依賴於 S3 進行資料儲存 [^27],而一些其他服務反過來建立在 Snowflake 之上。 -與計算中的抽象一樣,沒有一個你應該使用的正確答案。作為一般規則,更高級別的抽象往往更面向特定用例。如果你的需求與為其設計更高級別系統的情況相匹配,使用現有的更高級別系統可能會以比從較低級別系統自己構建它少得多的麻煩提供你需要的東西。另一方面,如果沒有滿足你需求的高階系統,那麼從較低級別的元件自己構建它是唯一的選擇。 +與計算中的抽象一樣,沒有一個正確的答案告訴你應該使用什麼。作為一般規則,更高級別的抽象往往更面向特定的用例。如果你的需求與為其設計更高級別系統的情況相匹配,使用現有的更高級別系統可能會以比從較低級別系統自己構建它少得多的麻煩提供你需要的東西。另一方面,如果沒有滿足你需求的高階系統,那麼從較低級別的元件自己構建它是唯一的選擇。 #### 儲存與計算的分離 {#sec_introduction_storage_compute} -在傳統計算中,磁碟儲存被認為是持久的(我們假設一旦某些東西被寫入磁碟,它就不會丟失)。為了容忍單個硬碟的故障,RAID(獨立磁碟冗餘陣列)通常用於在連線到同一臺機器的多個磁碟上維護資料副本。RAID 可以在硬體中或由作業系統在軟體中執行,並且它對訪問檔案系統的應用程式是透明的。 +在傳統計算中,磁碟儲存被認為是持久的(我們假設一旦某些東西被寫入磁碟,它就不會丟失)。為了容忍單個硬碟的故障,通常使用 RAID(獨立磁碟冗餘陣列)在連線到同一臺機器的幾個磁碟上維護資料副本。RAID 可以在硬體中執行,也可以由作業系統在軟體中執行,它對訪問檔案系統的應用程式是透明的。 -在雲中,計算例項(虛擬機器)也可能有本地磁碟連線,但云原生系統通常將這些磁碟更像臨時快取,而不像長期儲存。這是因為如果關聯的例項發生故障,或者如果例項被更大或更小的例項(在不同的物理機器上)替換以適應負載變化,本地磁碟將變得不可訪問。 +在雲中,計算例項(虛擬機器)也可能有本地磁碟連線,但云原生系統通常將這些磁碟更多地視為臨時快取,而不是長期儲存。這是因為如果相關例項失敗,或者如果例項被更大或更小的例項(在不同的物理機器上)替換以適應負載變化,本地磁碟就會變得不可訪問。 -作為本地磁碟的替代方案,雲服務還提供虛擬磁碟儲存,可以從一個例項分離並附加到另一個例項(Amazon EBS、Azure 託管磁碟和 Google Cloud 中的持久磁碟)。這樣的虛擬磁碟實際上不是物理磁碟,而是由一組單獨的機器提供的雲服務,它模擬磁碟(**塊裝置**,其中每個塊通常為 4 KiB 大小)的行為。這項技術使得在雲中執行傳統的基於磁碟的軟體成為可能,但塊裝置模擬引入了可以在從頭開始為雲設計的系統中避免的開銷 [^25]。它還使應用程式對網路故障非常敏感,因為虛擬塊裝置上的每個 I/O 實際上都是網路呼叫 [^28]。 +作為本地磁碟的替代方案,雲服務還提供可以從一個例項分離並附加到另一個例項的虛擬磁碟儲存(Amazon EBS、Azure 託管磁碟和 Google Cloud 中的持久磁碟)。這種虛擬磁碟實際上不是物理磁碟,而是由一組單獨的機器提供的雲服務,它模擬磁碟的行為(**塊裝置**,其中每個塊通常為 4 KiB 大小)。這項技術使得在雲中執行傳統的基於磁碟的軟體成為可能,但塊裝置模擬引入了在從頭為雲設計的系統中可以避免的開銷 [^25]。它還使應用程式對網路故障非常敏感,因為虛擬塊裝置上的每個 I/O 實際上都是網路呼叫 [^28]。 -為了解決這個問題,雲原生服務通常避免使用虛擬磁碟,而是建立在針對特定工作負載最佳化的專用儲存服務上。物件儲存服務(如 S3)專為長期儲存相當大的檔案而設計,大小從數百千位元組到幾千兆位元組不等。儲存在資料庫中的單個行或值通常比這小得多;因此,雲資料庫通常在單獨的服務中管理較小的值,並在物件儲存中儲存較大的資料塊(包含許多單個值)[^26] [^29]。我們將在[第 4 章](/tw/ch4#ch_storage)中看到這樣做的方法。 +為了解決這個問題,雲原生服務通常避免使用虛擬磁碟,而是建立在針對特定工作負載最佳化的專用儲存服務之上。物件儲存服務(如 S3)設計用於長期儲存相當大的檔案,大小從數百千位元組到幾千兆位元組不等。資料庫中儲存的單個行或值通常比這小得多;因此,雲資料庫通常在單獨的服務中管理較小的值,並將較大的資料塊(包含許多單個值)儲存在物件儲存中 [^26] [^29]。我們將在[第 4 章](/tw/ch4#ch_storage)中看到這樣做的方法。 -在傳統系統架構中,同一臺計算機負責儲存(磁碟)和計算(CPU 和 RAM),但在雲原生系統中,這兩個職責已經在某種程度上分離或 **解耦** [^9] [^27] [^30] [^31]: -例如,S3 只儲存檔案,如果你想分析該資料,你必須在 S3 之外的某個地方執行分析程式碼。這意味著透過網路傳輸資料,我們將在["分散式與單節點系統"](/tw/ch1#sec_introduction_distributed)中進一步討論。 +在傳統的系統架構中,同一臺計算機負責儲存(磁碟)和計算(CPU 和 RAM),但在雲原生系統中,這兩個職責已經在某種程度上分離或 **解耦** [^9] [^27] [^30] [^31]:例如,S3 只儲存檔案,如果你想分析該資料,你必須在 S3 之外的某個地方執行分析程式碼。這意味著透過網路傳輸資料,我們將在["分散式與單節點系統"](/tw/ch1#sec_introduction_distributed)中進一步討論。 -此外,雲原生系統通常是 **多租戶** 的,這意味著不是每個客戶都有一個單獨的機器,而是來自多個不同客戶的資料和計算由同一服務在同一共享硬體上處理 [^32]。 +此外,雲原生系統通常是 **多租戶** 的,這意味著不是每個客戶都有一臺單獨的機器,而是來自幾個不同客戶的資料和計算由同一服務在同一共享硬體上處理 [^32]。 -多租戶可以實現更好的硬體利用率、更容易的可擴充套件性以及雲提供商更容易的管理,但它還需要仔細的工程設計,以確保一個客戶的活動不會影響其他客戶的系統性能或安全性 [^33]。 +多租戶可以實現更好的硬體利用率、更容易的可伸縮性和雲提供商更容易的管理,但它也需要仔細的工程設計,以確保一個客戶的活動不會影響其他客戶的系統性能或安全性 [^33]。 ### 雲時代的運維 {#sec_introduction_operations} -傳統上,管理組織伺服器端資料基礎設施的人被稱為 **資料庫管理員**(DBA)或 **系統管理員**(sysadmins)。最近,許多組織試圖將軟體開發和運維的角色整合到具有後端服務和資料基礎設施共同責任的團隊中;**DevOps** 理念指導了這一趨勢。**站點可靠性工程師**(SRE)是 Google 對這一想法的實現 [^34]。 +傳統上,管理組織伺服器端資料基礎設施的人員被稱為 **資料庫管理員**(DBA)或 **系統管理員**(sysadmins)。最近,許多組織已經嘗試將軟體開發和運維的角色整合到團隊中,共同負責後端服務和資料基礎設施;**DevOps** 理念引導了這一趨勢。**站點可靠性工程師**(SRE)是 Google 對這個想法的實現 [^34]。 -運維的作用是確保服務可靠地交付給使用者(包括配置基礎設施和部署應用程式),並確保穩定的生產環境(包括監控和診斷可能影響可靠性的任何問題)。對於自託管系統,運維傳統上涉及在單個機器級別上的大量工作,例如容量規劃(例如,監控可用磁碟空間並在空間用完之前新增更多磁碟)、配置新機器、將服務從一臺機器移動到另一臺機器以及安裝作業系統補丁。 +運維的作用是確保服務可靠地交付給使用者(包括配置基礎設施和部署應用程式),並確保穩定的生產環境(包括監控和診斷可能影響可靠性的任何問題)。對於自託管系統,運維傳統上涉及大量在單個機器級別的工作,例如容量規劃(例如,監控可用磁碟空間並在空間用完之前新增更多磁碟)、配置新機器、將服務從一臺機器移動到另一臺機器,以及安裝作業系統補丁。 -許多雲服務提供了一個隱藏實際實現服務的單個機器的 API。例如,雲端儲存用 **計量計費** 取代了固定大小的磁碟,你可以儲存資料而無需提前規劃容量需求,然後根據實際使用的空間收費。此外,許多雲服務保持高可用,即使單個機器發生故障(參見["可靠性和容錯"](/tw/ch2#sec_introduction_reliability))。 +許多雲服務提供了一個 API,隱藏了實際實現服務的單個機器。例如,雲端儲存用 **計量計費** 替換固定大小的磁碟,你可以儲存資料而無需提前規劃容量需求,然後根據實際使用的空間收費。此外,許多雲服務保持高可用性,即使單個機器發生故障(參見["可靠性與容錯"](/tw/ch2#sec_introduction_reliability))。 從單個機器到服務的重點轉移伴隨著運維角色的變化。提供可靠服務的高階目標保持不變,但流程和工具已經發展。DevOps/SRE 理念更加強調: -* 自動化——偏好可重複的流程而不是手動一次性作業, -* 偏好短暫的虛擬機器和服務而不是長期執行的伺服器, -* 實現頻繁的應用程式更新, +* 自動化——優先考慮可重複的流程而不是手動的一次性工作, +* 優先考慮短暫的虛擬機器和服務而不是長期執行的伺服器, +* 啟用頻繁的應用程式更新, * 從事件中學習,以及 * 保留組織關於系統的知識,即使個人來來去去 [^35]。 -隨著雲服務的興起,角色出現了分歧:基礎設施公司的運維團隊專門為大量客戶提供可靠服務的細節,而服務的客戶在基礎設施上花費盡可能少的時間和精力 [^36]。 +隨著雲服務的興起,角色出現了分叉:基礎設施公司的運維團隊專門研究向大量客戶提供可靠服務的細節,而服務的客戶在基礎設施上花費盡可能少的時間和精力 [^36]。 -雲服務的客戶仍然需要運維,但他們專注於不同的方面,例如為給定任務選擇最合適的服務、將不同的服務相互整合以及從一個服務遷移到另一個服務。即使計量計費消除了傳統意義上的容量規劃需求,瞭解你為哪個目的使用哪些資源仍然很重要,這樣你就不會在不需要的雲資源上浪費金錢:容量規劃變成了財務規劃,效能最佳化變成了成本最佳化 [^37]。 +雲服務的客戶仍然需要運維,但他們專注於不同的方面,例如為給定任務選擇最合適的服務、將不同服務相互整合,以及從一個服務遷移到另一個服務。即使計量計費消除了傳統意義上的容量規劃需求,瞭解你為哪個目的使用哪些資源仍然很重要,這樣你就不會在不需要的雲資源上浪費金錢:容量規劃變成了財務規劃,效能最佳化變成了成本最佳化 [^37]。 -此外,雲服務確實有資源限制或 **配額**(例如你可以同時執行的最大程序數),你需要在遇到它們之前瞭解並規劃它們 [^38]。 +此外,雲服務確實有資源限制或 **配額**(例如你可以同時執行的最大程序數),你需要在遇到它們之前瞭解並規劃這些 [^38]。 -採用雲服務可能比執行自己的基礎設施更容易、更快,儘管即使在這裡,學習如何使用它也有成本,也許還要解決其限制。隨著越來越多的供應商提供針對不同用例的越來越廣泛的雲服務,不同服務之間的整合成為一個特別的挑戰 [^39] [^40]。 +採用雲服務可能比執行自己的基礎設施更容易、更快,儘管即使在這裡,學習如何使用它也有成本,也許還要解決其限制。隨著越來越多的供應商提供針對不同用例的更廣泛的雲服務,不同服務之間的整合成為一個特別的挑戰 [^39] [^40]。 ETL(參見["資料倉庫"](/tw/ch1#sec_introduction_dwh))只是故事的一部分;事務型雲服務也需要相互整合。目前,缺乏促進這種整合的標準,因此它通常涉及大量的手動工作。 -其他不能完全外包給雲服務的運維方面包括維護應用程式及其使用的庫的安全性、管理你自己的服務之間的互動、監控服務的負載以及追蹤問題的原因,例如效能下降或中斷。雖然雲正在改變運維的角色,但對運維的需求仍然很大。 +無法完全外包給雲服務的其他運維方面包括維護應用程式及其使用的庫的安全性、管理你自己的服務之間的互動、監控服務的負載,以及追蹤問題的原因,例如效能下降或中斷。雖然雲正在改變運維的角色,但對運維的需求比以往任何時候都大。 ## 分散式與單節點系統 {#sec_introduction_distributed} -涉及多臺透過網路通訊的機器的系統稱為 **分散式系統**。參與分散式系統的每個程序稱為一個 **節點**。你可能希望系統分散式的原因有很多: +涉及多臺機器透過網路通訊的系統稱為 **分散式系統**。參與分散式系統的每個程序稱為 **節點**。你可能希望系統分散式的原因有多種: -固有分散式系統 +固有的分散式系統 : 如果應用程式涉及兩個或多個互動使用者,每個使用者使用自己的裝置,那麼系統不可避免地是分散式的:裝置之間的通訊必須透過網路進行。 雲服務之間的請求 : 如果資料儲存在一個服務中但在另一個服務中處理,則必須透過網路從一個服務傳輸到另一個服務。 -容錯/高可用 -: 如果你的應用程式需要繼續工作,即使一臺機器(或幾臺機器、網路或整個資料中心)宕機,你可以使用多臺機器來提供冗餘。當一個失敗時,另一個可以接管。參見["可靠性和容錯"](/tw/ch2#sec_introduction_reliability)和[第 6 章](/tw/ch6#ch_replication)關於複製。 +容錯/高可用性 +: 如果你的應用程式需要在一臺機器(或幾臺機器、網路或整個資料中心)發生故障時繼續工作,你可以使用多臺機器為你提供冗餘。當一臺故障時,另一臺可以接管。參見["可靠性與容錯"](/tw/ch2#sec_introduction_reliability)和[第 6 章](/tw/ch6#ch_replication)關於複製的內容。 -可擴充套件性 -: 如果你的資料量或計算需求增長超過單臺機器可以處理的範圍,你可以潛在地將負載分散到多臺機器上。參見["可擴充套件性"](/tw/ch2#sec_introduction_scalability)。 +可伸縮性 +: 如果你的資料量或計算需求增長超過單臺機器的處理能力,你可以潛在地將負載分散到多臺機器上。參見["可伸縮性"](/tw/ch2#sec_introduction_scalability)。 延遲 -: 如果你在世界各地都有使用者,你可能希望在全球各個地區都有伺服器,以便每個使用者都可以從地理上靠近他們的伺服器獲得服務。這避免了使用者必須等待網路資料包在世界各地傳輸才能回答他們的請求。參見["描述效能"](/tw/ch2#sec_introduction_percentiles)。 +: 如果你在世界各地都有使用者,你可能希望在全球各個地區都有伺服器,以便每個使用者都可以從地理位置接近他們的伺服器獲得服務。這避免了使用者必須等待網路資料包繞地球半圈才能回答他們的請求。參見["描述效能"](/tw/ch2#sec_introduction_percentiles)。 彈性 -: 如果你的應用程式在某些時候繁忙而在其他時候空閒,雲部署可以根據需求擴大或縮小,因此你只需為正在積極使用的資源付費。這在單臺機器上更困難,它需要配置以處理最大負載,即使在幾乎不使用時也是如此。 +: 如果你的應用程式在某些時候很忙,在其他時候很空閒,雲部署可以根據需求向上或向下擴充套件,因此你只需為實際使用的資源付費。這在單臺機器上更困難,它需要配置以處理最大負載,即使在幾乎不使用的時候也是如此。 使用專用硬體 -: 系統的不同部分可以利用不同型別的硬體來匹配其工作負載。例如,物件儲存可能使用具有許多磁碟但很少 CPU 的機器,而資料分析系統可能使用具有大量 CPU 和記憶體但沒有磁碟的機器,機器學習系統可能使用具有 GPU 的機器(對於訓練深度神經網路和其他機器學習任務,GPU 比 CPU 高效得多)。 +: 系統的不同部分可以利用不同型別的硬體來匹配其工作負載。例如,物件儲存可能使用具有許多磁碟但很少 CPU 的機器,而資料分析系統可能使用具有大量 CPU 和記憶體但沒有磁碟的機器,機器學習系統可能使用具有 GPU 的機器(GPU 在訓練深度神經網路和其他機器學習任務方面比 CPU 效率高得多)。 法律合規 -: 一些國家有資料駐留法,要求關於其管轄範圍內人員的資料必須在該國家地理範圍內儲存和處理 [^41]。 -這些規則的範圍各不相同——例如,在某些情況下,它僅適用於醫療或金融資料,而其他情況則更廣泛。因此,在多個此類管轄區擁有使用者的服務必須將其資料分佈在多個位置的伺服器上。 +: 一些國家有資料駐留法律,要求其管轄範圍內的人員資料必須在該國地理範圍內儲存和處理 [^41]。這些規則的範圍各不相同——例如,在某些情況下,它僅適用於醫療或金融資料,而其他情況則更廣泛。因此,在幾個這樣的管轄區域中擁有使用者的服務將不得不將他們的資料分佈在幾個位置的伺服器上。 可持續性 -: 如果你在何時何地執行作業方面有靈活性,你可能能夠在可再生電力充足的時間和地點執行它們,並避免在電網承受壓力時執行它們。這可以減少你的碳排放,並讓你在電力便宜時利用便宜的電力 [^42] [^43]。 +: 如果你對執行作業的地點和時間有靈活性,你可能能夠在可再生電力充足的時間和地點執行它們,並避免在電網緊張時執行它們。這可以減少你的碳排放,並允許你在電力可用時利用廉價的電力 [^42] [^43]。 這些原因既適用於你自己編寫的服務(應用程式程式碼),也適用於由現成軟體(如資料庫)組成的服務。 ### 分散式系統的問題 {#sec_introduction_dist_sys_problems} -分散式系統也有缺點。透過網路進行的每個請求和 API 呼叫都需要處理失敗的可能性:網路可能中斷,或者服務可能過載或崩潰,因此任何請求都可能超時而不收到響應。在這種情況下,我們不知道服務是否收到了請求,簡單地重試它可能不安全。我們將在[第 9 章](/tw/ch9#ch_distributed)中詳細討論這些問題。 - -雖然資料中心網路很快,但呼叫另一個服務仍然比在同一程序中呼叫函式慢得多 [^44]。 +分散式系統也有缺點。透過網路進行的每個請求和 API 呼叫都需要處理失敗的可能性:網路可能中斷,或者服務可能過載或崩潰,因此任何請求都可能超時而沒有收到響應。在這種情況下,我們不知道服務是否收到了請求,簡單地重試它可能不安全。我們將在[第 9 章](/tw/ch9#ch_distributed)中詳細討論這些問題。 +儘管資料中心網路很快,但呼叫另一個服務仍然比在同一程序中呼叫函式慢得多 [^44]。 在處理大量資料時,與其將資料從儲存傳輸到處理它的單獨機器,不如將計算帶到已經擁有資料的機器上可能更快 [^45]。 +更多的節點並不總是更快:在某些情況下,單臺計算機上的簡單單執行緒程式可以比具有 100 多個 CPU 核心的叢集表現得更好 [^46]。 -更多的節點並不總是更快:在某些情況下,一臺計算機上的簡單單執行緒程式可以比擁有超過 100 個 CPU 核心的叢集表現得明顯更好 [^46]。 +故障排除分散式系統通常很困難:如果系統響應緩慢,你如何找出問題所在?在 **可觀測性** [^47] [^48] 的標題下開發了診斷分散式系統問題的技術,這涉及收集有關係統執行的資料,並允許以允許分析高階指標和單個事件的方式進行查詢。**追蹤** 工具(如 OpenTelemetry、Zipkin 和 Jaeger)允許你跟蹤哪個客戶端為哪個操作呼叫了哪個伺服器,以及每次呼叫花費了多長時間 [^49]。 -故障排除分散式系統通常很困難:如果系統響應緩慢,你如何找出問題所在?在 **可觀測性** 的標題下開發了診斷分散式系統問題的技術 [^47] [^48], -這涉及收集有關係統執行的資料,並允許以允許分析高階指標和單個事件的方式對其進行查詢。**追蹤** 工具(如 OpenTelemetry、Zipkin 和 Jaeger)允許你跟蹤哪個客戶端為哪個操作呼叫了哪個伺服器,以及每個呼叫花費了多長時間 [^49]。 +資料庫提供了各種機制來確保資料一致性,正如我們將在[第 6 章](/tw/ch6#ch_replication)和[第 8 章](/tw/ch8#ch_transactions)中看到的。然而,當每個服務都有自己的資料庫時,維護這些不同服務之間的資料一致性就成了應用程式的問題。分散式事務(我們在[第 8 章](/tw/ch8#ch_transactions)中探討)是確保一致性的一種可能技術,但它們在微服務上下文中很少使用,因為它們違背了使服務彼此獨立的目標,而且許多資料庫不支援它們 [^50]。 -資料庫提供了各種機制來確保資料一致性,正如我們將在[第 6 章](/tw/ch6#ch_replication)和[第 8 章](/tw/ch8#ch_transactions)中看到的。然而,當每個服務都有自己的資料庫時,維護這些不同服務之間資料的一致性就成了應用程式的問題。 -分散式事務(我們在[第 8 章](/tw/ch8#ch_transactions)中探討)是確保一致性的一種可能技術,但它們很少在微服務環境中使用,因為它們與使服務彼此獨立的目標背道而馳,並且許多資料庫不支援它們 [^50]。 - -由於所有這些原因,如果你可以在單臺機器上做某事,與設定分散式系統相比,這通常要簡單得多,成本也低得多 [^23] [^46] [^51]。 -CPU、記憶體和磁碟已經變得更大、更快、更可靠。與單節點資料庫(如 DuckDB、SQLite 和 KùzuDB)結合使用時,許多工作負載現在可以在單個節點上執行。我們將在[第 4 章](/tw/ch4#ch_storage)中更多地探討這個主題。 +出於所有這些原因,如果你可以在單臺機器上做某事,與設定分散式系統相比,這通常要簡單得多,成本也更低 [^23] [^46] [^51]。CPU、記憶體和磁碟已經變得更大、更快、更可靠。當與 DuckDB、SQLite 和 KùzuDB 等單節點資料庫結合使用時,許多工作負載現在可以在單個節點上執行。我們將在[第 4 章](/tw/ch4#ch_storage)中進一步探討這個主題。 ### 微服務與 Serverless {#sec_introduction_microservices} -將系統分佈在多臺機器上的最常見方法是將它們分為客戶端和伺服器,並讓客戶端向伺服器發出請求。最常見的是,HTTP 用於此通訊,正如我們將在["透過服務的資料流:REST 和 RPC"](/tw/ch5#sec_encoding_dataflow_rpc)中討論的。同一程序可能既是伺服器(處理傳入請求)又是客戶端(向其他服務發出出站請求)。 +在多臺機器上分佈系統的最常見方式是將它們分為客戶端和伺服器,並讓客戶端向伺服器發出請求。最常見的是使用 HTTP 進行此通訊,正如我們將在["流經服務的資料流:REST 和 RPC"](/tw/ch5#sec_encoding_dataflow_rpc)中討論的。同一程序可能既是伺服器(處理傳入請求)又是客戶端(向其他服務發出出站請求)。 -這種構建應用程式的方式傳統上被稱為 **面向服務的架構**(SOA);最近,這個想法已經被細化為 **微服務** 架構 [^52] [^53]。 -在這種架構中,服務有一個明確定義的目的(例如,在 S3 的情況下,這將是檔案儲存);每個服務公開一個可以由客戶端透過網路呼叫的 API,每個服務有一個負責其維護的團隊。因此,複雜的應用程式可以分解為多個互動服務,每個服務由單獨的團隊管理。 +這種構建應用程式的方式傳統上被稱為 **面向服務架構**(SOA);最近,這個想法已經被細化為 **微服務** 架構 [^52] [^53]。在這種架構中,服務有一個明確定義的目的(例如,在 S3 的情況下,這將是檔案儲存);每個服務公開一個可以由客戶端透過網路呼叫的 API,每個服務有一個負責其維護的團隊。因此,複雜的應用程式可以分解為多個互動服務,每個服務由單獨的團隊管理。 -將複雜的軟體分解為多個服務有幾個優點:每個服務可以獨立更新,減少團隊之間的協調工作;每個服務可以分配它需要的硬體資源;透過將實現細節隱藏在 API 後面,服務所有者可以自由更改實現而不影響客戶端。在資料儲存方面,每個服務擁有自己的資料庫並且不在服務之間共享資料庫是很常見的:共享資料庫將有效地使整個資料庫結構成為服務 API 的一部分,然後該結構將難以更改。共享資料庫還可能導致一個服務的查詢對其他服務的效能產生負面影響。 +將複雜的軟體分解為多個服務有幾個優點:每個服務可以獨立更新,減少團隊之間的協調工作;每個服務可以分配它需要的硬體資源;透過將實現細節隱藏在 API 後面,服務所有者可以自由地更改實現而不影響客戶端。在資料儲存方面,每個服務通常有自己的資料庫,而不在服務之間共享資料庫:共享資料庫實際上會使整個資料庫結構成為服務 API 的一部分,然後該結構將很難更改。共享資料庫還可能導致一個服務的查詢對其他服務的效能產生負面影響。 -另一方面,擁有許多服務本身可能會滋生複雜性:每個服務都需要基礎設施來部署新版本、調整分配的硬體資源以匹配負載、收集日誌、監控服務健康狀況,並在出現問題時向值班工程師發出警報。**編排** 框架(如 Kubernetes)已成為部署服務的流行方式,因為它們為這種基礎設施提供了基礎。在開發期間測試服務可能會很複雜,因為你還需要執行它所依賴的所有其他服務。 +另一方面,擁有許多服務本身可能會帶來複雜性:每個服務都需要用於部署新版本、調整分配的硬體資源以匹配負載、收集日誌、監控服務健康狀況以及在出現問題時向值班工程師發出警報的基礎設施。**編排** 框架(如 Kubernetes)已成為部署服務的流行方式,因為它們為這種基礎設施提供了基礎。在開發期間測試服務可能很複雜,因為你還需要執行它所依賴的所有其他服務。 -微服務 API 的演進可能具有挑戰性。呼叫 API 的客戶端期望 API 具有某些欄位。開發人員可能希望隨著業務需求的變化向 API 新增或刪除欄位,但這樣做可能會導致客戶端失敗。更糟糕的是,這種失敗通常直到開發週期的後期才被發現,當更新的服務 API 部署到暫存或生產環境時。API 描述標準(如 OpenAPI 和 gRPC)有助於管理客戶端和伺服器 API 之間的關係;我們將在[第 5 章](/tw/ch5#ch_encoding)中進一步討論這些。 +微服務 API 的演進可能具有挑戰性。呼叫 API 的客戶端期望 API 具有某些欄位。開發人員可能希望根據業務需求的變化向 API 新增或刪除欄位,但這樣做可能會導致客戶端失敗。更糟糕的是,這種失敗通常直到開發週期的後期才被發現,當更新的服務 API 部署到暫存或生產環境時。API 描述標準(如 OpenAPI 和 gRPC)有助於管理客戶端和伺服器 API 之間的關係;我們將在[第 5 章](/tw/ch5#ch_encoding)中進一步討論這些。 微服務主要是人員問題的技術解決方案:允許不同的團隊獨立取得進展,而無需相互協調。這在大公司中很有價值,但在沒有很多團隊的小公司中,使用微服務可能是不必要的開銷,最好以最簡單的方式實現應用程式 [^52]。 -**無伺服器** 或 **函式即服務**(FaaS)是部署服務的另一種方法,其中基礎設施的管理外包給雲供應商 [^33]。 -使用虛擬機器時,你必須明確選擇何時啟動或關閉例項;相比之下,使用無伺服器模型,雲提供商根據對你的服務的傳入請求自動分配和釋放硬體資源 [^54]。 -無伺服器部署將更多的運營負擔轉移到雲提供商,並透過使用而不是機器例項實現靈活計費。為了提供這些好處,許多無伺服器基礎設施提供商對函式執行施加時間限制、限制執行時環境,並且在首次呼叫函式時可能會遭受緩慢的啟動時間。術語"無伺服器"也可能具有誤導性:每個無伺服器函式執行仍在伺服器上執行,但後續執行可能在不同的伺服器上執行。此外,BigQuery 和各種 Kafka 產品等基礎設施已採用"無伺服器"術語來表示其服務自動擴充套件,並且它們按使用而不是機器例項計費。 +**Serverless** 或 **函式即服務**(FaaS)是部署服務的另一種方法,其中基礎設施的管理外包給雲供應商 [^33]。使用虛擬機器時,你必須明確選擇何時啟動或關閉例項;相比之下,使用 serverless 模型,雲提供商根據對你服務的傳入請求自動分配和釋放硬體資源 [^54]。Serverless 部署將更多的運營負擔轉移到雲提供商,並透過使用量而不是機器例項實現靈活的計費。為了提供這些好處,許多 serverless 基礎設施提供商對函式執行施加時間限制,限制執行時環境,並且在首次呼叫函式時可能會遭受緩慢的啟動時間。術語"serverless"也可能具有誤導性:每個 serverless 函式執行仍然在伺服器上執行,但後續執行可能在不同的伺服器上執行。此外,BigQuery 和各種 Kafka 產品等基礎設施已經採用"serverless"術語來表示他們的服務自動擴充套件,並且他們按使用量而不是機器例項計費。 -就像雲端儲存用計量計費模型取代了容量規劃(提前決定購買多少磁碟)一樣,無伺服器方法正在為程式碼執行帶來計量計費:你只需為應用程式程式碼實際執行的時間付費,而不必提前配置資源。 +就像雲端儲存用計量計費模型取代了容量規劃(提前決定購買多少磁碟)一樣,serverless 方法正在為程式碼執行帶來計量計費:你只為應用程式程式碼實際執行的時間付費,而不必提前配置資源。 ### 雲計算與超級計算 {#id17} -雲計算不是構建大規模計算系統的唯一方式;另一種選擇是 **高效能計算**(HPC),也稱為 **超級計算**。雖然有重疊,但與雲計算和企業資料中心繫統相比,HPC 通常有不同的優先順序並使用不同的技術。其中一些差異是: +雲計算不是構建大規模計算系統的唯一方式;另一種選擇是 **高效能計算**(HPC),也稱為 **超級計算**。儘管有重疊,但與雲計算和企業資料中心繫統相比,HPC 通常有不同的優先順序並使用不同的技術。其中一些差異是: -* 超級計算機通常用於計算密集型科學計算任務,如天氣預報、氣候建模、分子動力學(模擬原子和分子的運動)、複雜最佳化問題和求解偏微分方程。另一方面,雲計算往往用於線上服務、業務資料系統以及需要以高可用性為使用者請求提供服務的類似系統。 -* 超級計算機通常執行大型批處理作業,這些作業會定期將其計算狀態檢查點到磁碟。如果節點發生故障,常見的解決方案是簡單地停止整個叢集工作負載,修復故障節點,然後從最後一個檢查點重新啟動計算 [^55] [^56]。 - 對於雲服務,通常不希望停止整個叢集,因為服務需要以最小的中斷持續為使用者提供服務。 -* 超級計算機節點通常透過共享記憶體和遠端直接記憶體訪問(RDMA)進行通訊,這支援高頻寬和低延遲,但假設系統使用者之間具有高度信任 [^57]。 - 在雲計算中,網路和機器通常由相互不信任的組織共享,需要更強的安全機制,如資源隔離(例如,虛擬機器)、加密和身份驗證。 -* 雲資料中心網路通常基於 IP 和乙太網,以 Clos 拓撲排列以提供高二分頻寬——網路整體效能的常用度量 [^55] [^58]。 - 超級計算機通常使用專門的網路拓撲,如多維網格和環面 [^59], - 這些拓撲為具有已知通訊模式的 HPC 工作負載產生更好的效能。 -* 雲計算允許節點分佈在多個地理區域,而超級計算機通常假設其所有節點都緊密相鄰。 +* 超級計算機通常用於計算密集型科學計算任務,例如天氣預報、氣候建模、分子動力學(模擬原子和分子的運動)、複雜最佳化問題和求解偏微分方程。另一方面,雲計算往往用於線上服務、業務資料系統和需要以高可用性為使用者請求提供服務的類似系統。 +* 超級計算機通常執行大型批處理作業,定期將其計算狀態檢查點到磁碟。如果節點發生故障,常見的解決方案是簡單地停止整個叢集工作負載,修復故障節點,然後從最後一個檢查點重新啟動計算 [^55] [^56]。對於雲服務,通常不希望停止整個叢集,因為服務需要以最小的中斷持續為使用者提供服務。 +* 超級計算機節點通常透過共享記憶體和遠端直接記憶體訪問(RDMA)進行通訊,這支援高頻寬和低延遲,但假設系統使用者之間有高度的信任 [^57]。在雲計算中,網路和機器通常由相互不信任的組織共享,需要更強的安全機制,如資源隔離(例如,虛擬機器)、加密和身份驗證。 +* 雲資料中心網路通常基於 IP 和乙太網,以 Clos 拓撲排列以提供高對分頻寬——這是網路整體效能的常用度量 [^55] [^58]。超級計算機通常使用專門的網路拓撲,例如多維網格和環面 [^59],這為具有已知通訊模式的 HPC 工作負載產生更好的效能。 +* 雲計算允許節點分佈在多個地理區域,而超級計算機通常假設它們的所有節點都靠近在一起。 -大規模分析系統有時與超級計算共享一些特徵,如果你在這個領域工作,瞭解這些技術可能是值得的。然而,本書主要關注需要持續可用的服務,如["可靠性和容錯"](/tw/ch2#sec_introduction_reliability)中所討論的。 +大規模分析系統有時與超級計算共享一些特徵,如果你在這個領域工作,瞭解這些技術可能是值得的。然而,本書主要關注需要持續可用的服務,如["可靠性與容錯"](/tw/ch2#sec_introduction_reliability)中所討論的。 ## 資料系統、法律與社會 {#sec_introduction_compliance} -到目前為止,你已經在本章中看到,資料系統的架構不僅受技術目標和要求的影響,還受它們支援的組織的人類需求的影響。越來越多的資料系統工程師意識到,僅為自己的業務需求服務是不夠的:我們還對整個社會負有責任。 +到目前為止,你已經在本章中看到,資料系統的架構不僅受到技術目標和要求的影響,還受到它們所支援的組織的人力需求的影響。越來越多的資料系統工程師認識到,僅服務於自己企業的需求是不夠的:我們還對整個社會負有責任。 -一個特別關注的問題是儲存關於人及其行為的資料的系統。自 2018 年以來,**通用資料保護條例**(GDPR)賦予許多歐洲國家的居民對其個人資料的更大控制和法律權利,類似的隱私法規已在世界各地的各個國家和州採用,例如加州消費者隱私法案(CCPA)。關於 AI 的法規,如 **歐盟 AI 法案**,對個人資料的使用方式施加了進一步的限制。 +一個特別的關注點是儲存有關人員及其行為資料的系統。自 2018 年以來,**通用資料保護條例**(GDPR)賦予了許多歐洲國家居民對其個人資料更大的控制權和法律權利,類似的隱私法規已在世界各地的各個國家和州採用,包括例如加州消費者隱私法(CCPA)。關於 AI 的法規,例如 **歐盟 AI 法案**,對個人資料的使用方式施加了進一步的限制。 -此外,即使在不直接受監管的領域,人們也越來越認識到計算機系統對人和社會的影響。社交媒體改變了個人消費新聞的方式,這影響了他們的政治觀點,因此可能影響選舉結果。自動化系統越來越多地做出對個人產生深遠影響的決定,例如決定誰應該獲得貸款或保險、誰應該被邀請參加工作面試,或者誰應該被懷疑犯罪 [^60]。 +此外,即使在不直接受法規約束的領域,人們也越來越認識到計算機系統對人和社會的影響。社交媒體改變了個人消費新聞的方式,這影響了他們的政治觀點,因此可能影響選舉結果。自動化系統越來越多地做出對個人產生深遠影響的決策,例如決定誰應該獲得貸款或保險覆蓋,誰應該被邀請參加工作面試,或者誰應該被懷疑犯罪 [^60]。 -從事此類系統工作的每個人都有責任考慮道德影響並確保它們符合相關法律。沒有必要讓每個人都成為法律和道德專家,但對法律和道德原則的基本認識與對分散式系統的一些基礎知識一樣重要。 +每個從事此類系統工作的人都有責任考慮道德影響並確保他們遵守相關法律。沒有必要讓每個人都成為法律和道德專家,但對法律和道德原則的基本認識與分散式系統中的一些基礎知識同樣重要。 -法律考慮正在影響資料系統設計的基礎 [^61]。 -例如,GDPR 授予個人根據要求刪除其資料的權利(有時稱為 **被遺忘權**)。然而,正如我們將在本書中看到的,許多資料系統依賴於不可變構造(如僅追加日誌)作為其設計的一部分;我們如何確保刪除應該是不可變的檔案中間的某些資料?我們如何處理已納入派生資料集(參見["權威記錄系統與派生資料"](/tw/ch1#sec_introduction_derived))的資料的刪除,例如機器學習模型的訓練資料?回答這些問題會帶來新的工程挑戰。 +法律考慮正在影響資料系統設計的基礎 [^61]。例如,GDPR 授予個人在請求時刪除其資料的權利(有時稱為 **被遺忘權**)。然而,正如我們將在本書中看到的,許多資料系統依賴不可變構造(如僅追加日誌)作為其設計的一部分;我們如何確保刪除應該不可變的檔案中間的某些資料?我們如何處理已被納入派生資料集(參見["權威資料來源與派生資料"](/tw/ch1#sec_introduction_derived))的資料刪除,例如機器學習模型的訓練資料?回答這些問題會帶來新的工程挑戰。 -目前,我們沒有關於哪些特定技術或系統架構應被視為"符合 GDPR"的明確指導方針。該法規故意不強制要求特定技術,因為隨著技術的進步,這些技術可能會迅速變化。相反,法律文字闡述了需要解釋的高階原則。這意味著如何遵守隱私法規的問題沒有簡單的答案,但我們將透過這個視角來看待本書中的一些技術。 +目前,我們對於哪些特定技術或系統架構應被視為"符合 GDPR"沒有明確的指導方針。法規故意不強制要求特定技術,因為隨著技術的進步,這些技術可能會迅速變化。相反,法律文字規定了需要解釋的高階原則。這意味著如何遵守隱私法規的問題沒有簡單的答案,但我們將透過這個視角來看待本書中的一些技術。 -一般來說,我們儲存資料是因為我們認為其價值大於儲存它的成本。然而,值得記住的是,儲存成本不僅僅是你為 Amazon S3 或其他服務支付的賬單:成本效益計算還應考慮到如果資料被洩露或被對手破壞的責任和聲譽損害風險,以及如果資料的儲存和處理被發現不符合法律的法律成本和罰款風險 [^51]。 +一般來說,我們儲存資料是因為我們認為其價值大於儲存它的成本。然而,值得記住的是,儲存成本不僅僅是你為 Amazon S3 或其他服務支付的賬單:成本效益計算還應該考慮到如果資料被洩露或被對手入侵的責任和聲譽損害風險,以及如果資料的儲存和處理被發現不符合法律的法律成本和罰款風險 [^51]。 -政府或警察部隊也可能迫使公司交出資料。當存在資料可能揭示犯罪行為的風險時(例如,在幾個中東和非洲國家的同性戀,或在幾個美國州尋求墮胎),儲存該資料會給使用者帶來真正的安全風險。例如,前往墮胎診所的旅行很容易透過位置資料揭示,甚至可能透過使用者 IP 地址隨時間的日誌(表示大致位置)。 +政府或警察部隊也可能迫使公司交出資料。當存在資料可能暴露犯罪行為的風險時(例如,在幾個中東和非洲國家的同性戀,或在幾個美國州尋求墮胎),儲存該資料會為使用者創造真正的安全風險。例如,去墮胎診所的旅行很容易被位置資料揭示,甚至可能透過使用者 IP 地址隨時間的日誌(表示大致位置)。 -一旦考慮到所有風險,可能合理地決定某些資料根本不值得儲存,因此應該刪除。這種 **資料最小化** 原則(有時用德語術語 **Datensparsamkeit** 稱呼)與"大資料"理念相反,後者是推測性地儲存大量資料,以防將來有用 [^62]。 -但它符合 GDPR,該法規要求個人資料只能為指定的明確目的收集,該資料以後不得用於任何其他目的,並且資料不得保留超過收集目的所需的時間 [^63]。 +一旦考慮到所有風險,可能合理地決定某些資料根本不值得儲存,因此應該刪除。這個 **資料最小化** 原則(有時以德語術語 **Datensparsamkeit** 為人所知)與"大資料"哲學相反,後者是投機性地儲存大量資料,以防將來有用 [^62]。但它符合 GDPR,該法規要求個人資料只能為指定的、明確的目的收集,這些資料以後不得用於任何其他目的,並且資料不得保留超過收集目的所需的時間 [^63]。 -企業也注意到了隱私和安全問題。信用卡公司要求支付處理企業遵守嚴格的支付卡行業(PCI)標準。處理器經常接受獨立審計師的評估,以驗證持續合規性。軟體供應商也受到了更多的審查。許多買家現在要求他們的供應商遵守服務組織控制(SOC)第 2 類標準。與 PCI 合規性一樣,供應商接受第三方審計以驗證遵守情況。 +企業也注意到了隱私和安全問題。信用卡公司要求支付處理企業遵守嚴格的支付卡行業(PCI)標準。處理商經常接受獨立審計師的評估,以驗證持續的合規性。軟體供應商也受到了更多的審查。現在許多買家要求他們的供應商遵守服務組織控制(SOC)型別 2 標準。與 PCI 合規性一樣,供應商接受第三方審計以驗證遵守情況。 -一般來說,重要的是要平衡你的業務需求與你正在收集和處理其資料的人的需求。這個話題還有很多內容;在[待補充連結]中,我們將更深入地探討道德和法律合規主題,包括偏見和歧視問題。 +一般來說,重要的是平衡你的業務需求與你收集和處理其資料的人的需求。這個話題還有很多內容;在[待補充連結]中,我們將更深入地探討道德和法律合規性的主題,包括偏見和歧視的問題。 ## 總結 {#summary} -本章的主題是理解權衡:也就是說,認識到對於許多問題沒有一個正確的答案,而是有幾種不同的方法,每種方法都有各種利弊。我們探討了影響資料系統架構的一些最重要的選擇,並介紹了本書其餘部分所需的術語。 +本章的主題是理解權衡:也就是說,認識到對於許多問題,沒有一個正確的答案,而是有幾種不同的方法,每種方法都有各種利弊。我們探討了影響資料系統架構的一些最重要的選擇,並介紹了本書其餘部分所需的術語。 -我們首先區分了事務型(事務處理,OLTP)和分析型(OLAP)系統,並看到了它們的不同特徵:不僅管理具有不同訪問模式的不同型別的資料,而且還服務於不同的受眾。我們遇到了資料倉庫和資料湖的概念,它們透過 ETL 從事務型系統接收資料饋送。在[第 4 章](/tw/ch4#ch_storage)中,我們將看到事務型和分析型系統由於需要服務的不同型別的查詢,通常使用非常不同的內部資料佈局。 +我們首先區分了事務型(事務處理,OLTP)和分析型(OLAP)系統,並看到了它們的不同特徵:不僅管理不同型別的資料和不同的訪問模式,而且服務於不同的受眾。我們遇到了資料倉庫和資料湖的概念,它們透過 ETL 從事務型系統接收資料饋送。在[第 4 章](/tw/ch4#ch_storage)中,我們將看到事務型和分析型系統通常使用非常不同的內部資料佈局,因為它們需要服務的查詢型別不同。 -然後,我們將雲服務(一個相對較新的發展)與傳統的自託管軟體正規化進行了比較,後者以前主導著資料系統架構。這些方法中哪一個更具成本效益在很大程度上取決於你的特定情況,但不可否認的是,雲原生方法正在為資料系統的架構帶來重大變化,例如它們分離儲存和計算的方式。 +然後,我們將雲服務(一個相對較新的發展)與傳統的自託管軟體正規化進行了比較,後者以前主導了資料系統架構。這些方法中哪一種更具成本效益在很大程度上取決於你的特定情況,但不可否認的是,雲原生方法正在為資料系統的架構帶來重大變化,例如它們分離儲存和計算的方式。 -雲系統本質上是分散式的,我們簡要地研究了分散式系統與使用單臺機器相比的一些權衡。在某些情況下,你無法避免分散式,但如果可能將其保留在單臺機器上,建議不要急於使系統分散式。在[第 9 章](/tw/ch9#ch_distributed)中,我們將更詳細地介紹分散式系統的挑戰。 +雲系統本質上是分散式的,我們簡要地研究了分散式系統與使用單臺機器相比的一些權衡。有些情況下你無法避免分散式,但如果可能在單臺機器上保持系統,建議不要急於使系統分散式。在[第 9 章](/tw/ch9#ch_distributed)中,我們將更詳細地介紹分散式系統的挑戰。 -最後,我們看到資料系統架構不僅由部署系統的業務需求決定,還由保護其資料被處理的人的權利的隱私法規決定——這是許多工程師傾向於忽視的一個方面。我們如何將法律要求轉化為技術實現還不太清楚,但在我們瀏覽本書的其餘部分時,記住這個問題很重要。 +最後,我們看到資料系統架構不僅由部署系統的企業的需求決定,還由保護資料被處理的人的權利的隱私法規決定——這是許多工程師容易忽視的一個方面。我們如何將法律要求轉化為技術實現還不太瞭解,但在我們閱讀本書的其餘部分時,記住這個問題很重要。 ### 參考 @@ -491,4 +493,4 @@ CPU、記憶體和磁碟已經變得更大、更快、更可靠。與單節點 [^60]: Cathy O’Neil: *Weapons of Math Destruction: How Big Data Increases Inequality and Threatens Democracy*. Crown Publishing, 2016. ISBN: 9780553418811 [^61]: Supreeth Shastri, Vinay Banakar, Melissa Wasserman, Arun Kumar, and Vijay Chidambaram. [Understanding and Benchmarking the Impact of GDPR on Database Systems](https://www.vldb.org/pvldb/vol13/p1064-shastri.pdf). *Proceedings of the VLDB Endowment*, volume 13, issue 7, pages 1064–1077, March 2020. [doi:10.14778/3384345.3384354](https://doi.org/10.14778/3384345.3384354) [^62]: Martin Fowler. [Datensparsamkeit](https://www.martinfowler.com/bliki/Datensparsamkeit.html). *martinfowler.com*, December 2013. Archived at [perma.cc/R9QX-CME6](https://perma.cc/R9QX-CME6) -[^63]: [Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016 (General Data Protection Regulation)](https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32016R0679&from=EN). *Official Journal of the European Union* L 119/1, May 2016. \ No newline at end of file +[^63]: [Regulation (EU) 2016/679 of the European Parliament and of the Council of 27 April 2016 (General Data Protection Regulation)](https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CELEX:32016R0679&from=EN). *Official Journal of the European Union* L 119/1, May 2016. diff --git a/content/tw/ch11.md b/content/tw/ch11.md index 475928e..3016b2d 100644 --- a/content/tw/ch11.md +++ b/content/tw/ch11.md @@ -415,9 +415,9 @@ Reduce 側方法的優點是不需要對輸入資料做任何假設:無論其 Google 最初使用 MapReduce 是為其搜尋引擎建立索引,其實現為由 5 到 10 個 MapReduce 作業組成的工作流【1】。雖然 Google 後來也不僅僅是為這個目的而使用 MapReduce 【43】,但如果從構建搜尋索引的角度來看,更能幫助理解 MapReduce。(直至今日,Hadoop MapReduce 仍然是為 Lucene/Solr 構建索引的好方法【44】) -我們在 “[全文搜尋和模糊索引](/tw/ch3#全文搜尋和模糊索引)” 中簡要地瞭解了 Lucene 這樣的全文搜尋索引是如何工作的:它是一個檔案(關鍵詞字典),你可以在其中高效地查詢特定關鍵字,並找到包含該關鍵字的所有文件 ID 列表(文章列表)。這是一種非常簡化的看法 —— 實際上,搜尋索引需要各種額外資料,以便根據相關性對搜尋結果進行排名、糾正拼寫錯誤、解析同義詞等等 —— 但這個原則是成立的。 +我們在 “[全文檢索與模糊索引](/tw/ch3#全文檢索與模糊索引)” 中簡要地瞭解了 Lucene 這樣的全文檢索索引是如何工作的:它是一個檔案(關鍵詞字典),你可以在其中高效地查詢特定關鍵字,並找到包含該關鍵字的所有文件 ID 列表(文章列表)。這是一種非常簡化的看法 —— 實際上,搜尋索引需要各種額外資料,以便根據相關性對搜尋結果進行排名、糾正拼寫錯誤、解析同義詞等等 —— 但這個原則是成立的。 -如果需要對一組固定文件執行全文搜尋,則批處理是一種構建索引的高效方法:Mapper 根據需要對文件集合進行分割槽,每個 Reducer 構建該分割槽的索引,並將索引檔案寫入分散式檔案系統。構建這樣的文件分割槽索引(請參閱 “[分割槽與次級索引](/tw/ch6#分割槽與次級索引)”)並行處理效果拔群。 +如果需要對一組固定文件執行全文檢索,則批處理是一種構建索引的高效方法:Mapper 根據需要對文件集合進行分割槽,每個 Reducer 構建該分割槽的索引,並將索引檔案寫入分散式檔案系統。構建這樣的文件分割槽索引(請參閱 “[分割槽與次級索引](/tw/ch6#分割槽與次級索引)”)並行處理效果拔群。 由於按關鍵字查詢搜尋索引是隻讀操作,因而這些索引檔案一旦建立就是不可變的。 @@ -485,7 +485,7 @@ MapReduce 作業的輸出處理遵循同樣的原理。透過將輸入視為不 MPP 資料庫是單體的,緊密整合的軟體,負責磁碟上的儲存佈局,查詢計劃,排程和執行。由於這些元件都可以針對資料庫的特定需求進行調整和最佳化,因此整個系統可以在其設計針對的查詢型別上取得非常好的效能。而且,SQL 查詢語言允許以優雅的語法表達查詢,而無需編寫程式碼,可以在業務分析師使用的視覺化工具(例如 Tableau)中訪問到。 -另一方面,並非所有型別的處理都可以合理地表達為 SQL 查詢。例如,如果要構建機器學習和推薦系統,或者使用相關性排名模型的全文搜尋索引,或者執行影像分析,則很可能需要更一般的資料處理模型。這些型別的處理通常是特別針對特定應用的(例如機器學習的特徵工程,機器翻譯的自然語言模型,欺詐預測的風險評估函式),因此它們不可避免地需要編寫程式碼,而不僅僅是查詢。 +另一方面,並非所有型別的處理都可以合理地表達為 SQL 查詢。例如,如果要構建機器學習和推薦系統,或者使用相關性排名模型的全文檢索索引,或者執行影像分析,則很可能需要更一般的資料處理模型。這些型別的處理通常是特別針對特定應用的(例如機器學習的特徵工程,機器翻譯的自然語言模型,欺詐預測的風險評估函式),因此它們不可避免地需要編寫程式碼,而不僅僅是查詢。 MapReduce 使工程師能夠輕鬆地在大型資料集上執行自己的程式碼。如果你有 HDFS 和 MapReduce,那麼你 **可以** 在它之上建立一個 SQL 查詢執行引擎,事實上這正是 Hive 專案所做的【31】。但是,你也可以編寫許多其他形式的批處理,這些批處理不必非要用 SQL 查詢表示。 @@ -711,7 +711,7 @@ Spark、Flink 和 Tez 避免將中間狀態寫入 HDFS,因此它們採取了 得益於這個框架,你在批處理作業中的程式碼無需操心實現容錯機制:框架可以保證作業的最終輸出與沒有發生錯誤的情況相同,雖然實際上也許不得不重試各種任務。比起線上服務一邊處理使用者請求一邊將寫入資料庫作為處理請求的副作用,批處理提供的這種可靠性語義要強得多。 -批處理作業的顯著特點是,它讀取一些輸入資料併產生一些輸出資料,但不修改輸入 —— 換句話說,輸出是從輸入衍生出的。最關鍵的是,輸入資料是 **有界的(bounded)**:它有一個已知的,固定的大小(例如,它包含一些時間點的日誌檔案或資料庫內容的快照)。因為它是有界的,一個作業知道自己什麼時候完成了整個輸入的讀取,所以一個工作在做完後,最終總是會完成的。 +批處理作業的顯著特點是,它讀取一些輸入資料併產生一些輸出資料,但不修改輸入 —— 換句話說,輸出是從輸入派生出的。最關鍵的是,輸入資料是 **有界的(bounded)**:它有一個已知的,固定的大小(例如,它包含一些時間點的日誌檔案或資料庫內容的快照)。因為它是有界的,一個作業知道自己什麼時候完成了整個輸入的讀取,所以一個工作在做完後,最終總是會完成的。 在下一章中,我們將轉向流處理,其中的輸入是 **無界的(unbounded)** —— 也就是說,你還有活兒要幹,然而它的輸入是永無止境的資料流。在這種情況下,作業永無完成之日。因為在任何時候都可能有更多的工作湧入。我們將看到,在某些方面上,流處理和批處理是相似的。但是關於無盡資料流的假設也對我們構建系統的方式產生了很多改變。 diff --git a/content/tw/ch12.md b/content/tw/ch12.md index d07b537..577860c 100644 --- a/content/tw/ch12.md +++ b/content/tw/ch12.md @@ -248,7 +248,7 @@ Apache Kafka 【17,18】、Amazon Kinesis Streams 【19】和 Twitter 的 Distri 最近,人們對 **變更資料捕獲(change data capture, CDC)** 越來越感興趣,這是一種觀察寫入資料庫的所有資料變更,並將其提取並轉換為可以複製到其他系統中的形式的過程。CDC 是非常有意思的,尤其是當變更能在被寫入後立刻用於流時。 -例如,你可以捕獲資料庫中的變更,並不斷將相同的變更應用至搜尋索引。如果變更日誌以相同的順序應用,則可以預期搜尋索引中的資料與資料庫中的資料是匹配的。搜尋索引和任何其他衍生資料系統只是變更流的消費者,如 [圖 11-5](/v1/ddia_1105.png) 所示。 +例如,你可以捕獲資料庫中的變更,並不斷將相同的變更應用至搜尋索引。如果變更日誌以相同的順序應用,則可以預期搜尋索引中的資料與資料庫中的資料是匹配的。搜尋索引和任何其他派生資料系統只是變更流的消費者,如 [圖 11-5](/v1/ddia_1105.png) 所示。 ![](/v1/ddia_1105.png) @@ -317,7 +317,7 @@ Kafka Connect【41】致力於將廣泛的資料庫系統的變更資料捕獲 事件日誌本身並不是很有用,因為使用者通常期望看到的是系統的當前狀態,而不是變更歷史。例如,在購物網站上,使用者期望能看到他們購物車裡的當前內容,而不是他們購物車所有變更的一個僅追加列表。 -因此,使用事件溯源的應用需要拉取事件日誌(表示 **寫入** 系統的資料),並將其轉換為適合向用戶顯示的應用狀態(從系統 **讀取** 資料的方式【47】)。這種轉換可以使用任意邏輯,但它應當是確定性的,以便能再次執行,並從事件日誌中衍生出相同的應用狀態。 +因此,使用事件溯源的應用需要拉取事件日誌(表示 **寫入** 系統的資料),並將其轉換為適合向用戶顯示的應用狀態(從系統 **讀取** 資料的方式【47】)。這種轉換可以使用任意邏輯,但它應當是確定性的,以便能再次執行,並從事件日誌中派生出相同的應用狀態。 與變更資料捕獲一樣,重播事件日誌允許讓你重新構建系統的當前狀態。不過,日誌壓縮需要採用不同的方式處理: @@ -358,7 +358,7 @@ $$ **圖 11-6 應用當前狀態與事件流之間的關係** -如果你持久儲存了變更日誌,那麼重現狀態就非常簡單。如果你認為事件日誌是你的記錄系統,而所有的衍生狀態都從它派生而來,那麼系統中的資料流動就容易理解的多。正如帕特・赫蘭(Pat Helland)所說的【52】: +如果你持久儲存了變更日誌,那麼重現狀態就非常簡單。如果你認為事件日誌是你的記錄系統,而所有的派生狀態都從它派生而來,那麼系統中的資料流動就容易理解的多。正如帕特・赫蘭(Pat Helland)所說的【52】: > 事務日誌記錄了資料庫的所有變更。高速追加是更改日誌的唯一方法。從這個角度來看,資料庫的內容其實是日誌中記錄最新值的快取。日誌才是真相,資料庫是日誌子集的快取,這一快取子集恰好來自日誌中每條記錄與索引值的最新值。 @@ -366,9 +366,9 @@ $$ #### 不可變事件的優點 -資料庫中的不變性是一個古老的概念。例如,會計在幾個世紀以來一直在財務記賬中應用不變性。一筆交易發生時,它被記錄在一個僅追加寫入的分類帳中,實質上是描述貨幣、商品或服務轉手的事件日誌。賬目,比如利潤、虧損、資產負債表,是從分類賬中的交易求和衍生而來【53】。 +資料庫中的不變性是一個古老的概念。例如,會計在幾個世紀以來一直在財務記賬中應用不變性。一筆交易發生時,它被記錄在一個僅追加寫入的分類帳中,實質上是描述貨幣、商品或服務轉手的事件日誌。賬目,比如利潤、虧損、資產負債表,是從分類賬中的交易求和派生而來【53】。 -如果發生錯誤,會計師不會刪除或更改分類帳中的錯誤交易 —— 而是新增另一筆交易以補償錯誤,例如退還一筆不正確的費用。不正確的交易將永遠保留在分類帳中,對於審計而言可能非常重要。如果從不正確的分類賬衍生出的錯誤數字已經公佈,那麼下一個會計週期的數字就會包括一個更正。這個過程在會計事務中是很常見的【54】。 +如果發生錯誤,會計師不會刪除或更改分類帳中的錯誤交易 —— 而是新增另一筆交易以補償錯誤,例如退還一筆不正確的費用。不正確的交易將永遠保留在分類帳中,對於審計而言可能非常重要。如果從不正確的分類賬派生出的錯誤數字已經公佈,那麼下一個會計週期的數字就會包括一個更正。這個過程在會計事務中是很常見的【54】。 儘管這種可審計性只在金融系統中尤其重要,但對於不受這種嚴格監管的許多其他系統,也是很有幫助的。如 “[批處理輸出的哲學](/tw/ch11#批處理輸出的哲學)” 中所討論的,如果你意外地部署了將錯誤資料寫入資料庫的錯誤程式碼,當代碼會破壞性地覆寫資料時,恢復要困難得多。使用不可變事件的僅追加日誌,診斷問題與故障恢復就要容易的多。 @@ -376,7 +376,7 @@ $$ #### 從同一事件日誌中派生多個檢視 -此外,透過從不變的事件日誌中分離出可變的狀態,你可以針對不同的讀取方式,從相同的事件日誌中衍生出幾種不同的表現形式。效果就像一個流的多個消費者一樣([圖 11-5](/v1/ddia_1105.png)):例如,分析型資料庫 Druid 使用這種方式直接從 Kafka 攝取資料【55】,Pistachio 是一個分散式的鍵值儲存,使用 Kafka 作為提交日誌【56】,Kafka Connect 能將來自 Kafka 的資料匯出到各種不同的資料庫與索引【41】。這對於許多其他儲存和索引系統(如搜尋伺服器)來說是很有意義的,當系統要從分散式日誌中獲取輸入時亦然(請參閱 “[保持系統同步](#保持系統同步)”)。 +此外,透過從不變的事件日誌中分離出可變的狀態,你可以針對不同的讀取方式,從相同的事件日誌中派生出幾種不同的表現形式。效果就像一個流的多個消費者一樣([圖 11-5](/v1/ddia_1105.png)):例如,分析型資料庫 Druid 使用這種方式直接從 Kafka 攝取資料【55】,Pistachio 是一個分散式的鍵值儲存,使用 Kafka 作為提交日誌【56】,Kafka Connect 能將來自 Kafka 的資料匯出到各種不同的資料庫與索引【41】。這對於許多其他儲存和索引系統(如搜尋伺服器)來說是很有意義的,當系統要從分散式日誌中獲取輸入時亦然(請參閱 “[保持系統同步](#保持系統同步)”)。 新增從事件日誌到資料庫的顯式轉換,能夠使應用更容易地隨時間演進:如果你想要引入一個新功能,以新的方式表示現有資料,則可以使用事件日誌來構建一個單獨的、針對新功能的讀取最佳化檢視,無需修改現有系統而與之共存。並行執行新舊系統通常比在現有系統中執行複雜的模式遷移更容易。一旦不再需要舊的系統,你可以簡單地關閉它並回收其資源【47,57】。 @@ -388,7 +388,7 @@ $$ #### 併發控制 -事件溯源和變更資料捕獲的最大缺點是,事件日誌的消費者通常是非同步的,所以可能會出現這樣的情況:使用者會寫入日誌,然後從日誌衍生檢視中讀取,結果發現他的寫入還沒有反映在讀取檢視中。我們之前在 “[讀己之寫](/tw/ch5#讀己之寫)” 中討論了這個問題以及可能的解決方案。 +事件溯源和變更資料捕獲的最大缺點是,事件日誌的消費者通常是非同步的,所以可能會出現這樣的情況:使用者會寫入日誌,然後從日誌派生檢視中讀取,結果發現他的寫入還沒有反映在讀取檢視中。我們之前在 “[讀己之寫](/tw/ch5#讀己之寫)” 中討論了這個問題以及可能的解決方案。 一種解決方案是將事件追加到日誌時同步執行讀取檢視的更新。而將這些寫入操作合併為一個原子單元需要 **事務**,所以要麼將事件日誌和讀取檢視儲存在同一個儲存系統中,要麼就需要跨不同系統進行分散式事務。或者,你也可以使用在 “[使用全序廣播實現線性一致的儲存](/tw/ch9#使用全序廣播實現線性一致的儲存)” 中討論的方法。 @@ -419,7 +419,7 @@ $$ 2. 你能以某種方式將事件推送給使用者,例如傳送報警郵件或推送通知,或將事件流式傳輸到可即時顯示的儀表板上。在這種情況下,人是流的最終消費者。 3. 你可以處理一個或多個輸入流,併產生一個或多個輸出流。流可能會經過由幾個這樣的處理階段組成的流水線,最後再輸出(選項 1 或 2)。 -在本章的剩餘部分中,我們將討論選項 3:處理流以產生其他衍生流。處理這樣的流的程式碼片段,被稱為 **運算元(operator)** 或 **作業(job)**。它與我們在 [第十一章](/tw/ch11) 中討論過的 Unix 程序和 MapReduce 作業密切相關,資料流的模式是相似的:一個流處理器以只讀的方式使用輸入流,並將其輸出以僅追加的方式寫入一個不同的位置。 +在本章的剩餘部分中,我們將討論選項 3:處理流以產生其他派生流。處理這樣的流的程式碼片段,被稱為 **運算元(operator)** 或 **作業(job)**。它與我們在 [第十一章](/tw/ch11) 中討論過的 Unix 程序和 MapReduce 作業密切相關,資料流的模式是相似的:一個流處理器以只讀的方式使用輸入流,並將其輸出以僅追加的方式寫入一個不同的位置。 流處理中的分割槽和並行化模式也非常類似於 [第十一章](/tw/ch11) 中介紹的 MapReduce 和資料流引擎,因此我們不再重複這些主題。基本的 Map 操作(如轉換和過濾記錄)也是一樣的。 @@ -462,7 +462,7 @@ CEP 的實現包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB #### 維護物化檢視 -我們在 “[資料庫與流](#資料庫與流)” 中看到,資料庫的變更流可以用於維護派生資料系統(如快取、搜尋索引和資料倉庫),並使其與源資料庫保持最新。我們可以將這些示例視作維護 **物化檢視(materialized view)** 的一種具體場景(請參閱 “[聚合:資料立方體和物化檢視](/tw/ch3#聚合:資料立方體和物化檢視)”):在某個資料集上衍生出一個替代檢視以便高效查詢,並在底層資料變更時更新檢視【50】。 +我們在 “[資料庫與流](#資料庫與流)” 中看到,資料庫的變更流可以用於維護派生資料系統(如快取、搜尋索引和資料倉庫),並使其與源資料庫保持最新。我們可以將這些示例視作維護 **物化檢視(materialized view)** 的一種具體場景(請參閱 “[聚合:資料立方體和物化檢視](/tw/ch3#聚合:資料立方體和物化檢視)”):在某個資料集上派生出一個替代檢視以便高效查詢,並在底層資料變更時更新檢視【50】。 同樣,在事件溯源中,應用程式的狀態是透過應用事件日誌來維護的;這裡的應用程式狀態也是一種物化檢視。與流分析場景不同的是,僅考慮某個時間視窗內的事件通常是不夠的:構建物化檢視可能需要任意時間段內的 **所有** 事件,除了那些可能由日誌壓縮丟棄的過時事件(請參閱 “[日誌壓縮](#日誌壓縮)”)。實際上,你需要一個可以一直延伸到時間開端的視窗。 @@ -470,7 +470,7 @@ CEP 的實現包括 Esper【69】、IBM InfoSphere Streams【70】、Apama、TIB #### 在流上搜索 -除了允許搜尋由多個事件構成模式的 CEP 外,有時也存在基於複雜標準(例如全文搜尋查詢)來搜尋單個事件的需求。 +除了允許搜尋由多個事件構成模式的 CEP 外,有時也存在基於複雜標準(例如全文檢索查詢)來搜尋單個事件的需求。 例如,媒體監測服務可以訂閱新聞文章 Feed 與來自媒體的播客,搜尋任何關於公司、產品或感興趣的話題的新聞。這是透過預先構建一個搜尋查詢來完成的,然後不斷地將新聞項的流與該查詢進行匹配。在一些網站上也有類似的功能:例如,當市場上出現符合其搜尋條件的新房產時,房地產網站的使用者可以要求網站通知他們。Elasticsearch 的這種過濾器功能,是實現這種流搜尋的一種選擇【76】。 @@ -612,7 +612,7 @@ GROUP BY follows.follower_id 流連線直接對應於這個查詢中的表連線。時間線實際上是這個查詢結果的快取,每當底層的表發生變化時都會更新 [^iii]。 -[^iii]: 如果你將流視作表的衍生物,如 [圖 11-6](/v1/ddia_1106.png) 所示,而把一個連線看作是兩個表的乘法u·v,那麼會發生一些有趣的事情:物化連線的變化流遵循乘積法則:(u·v)'= u'v + uv'。換句話說,任何推文的變化量都與當前的關注聯絡在一起,任何關注的變化量都與當前的推文相連線【49,50】。 +[^iii]: 如果你將流視作表的派生物,如 [圖 11-6](/v1/ddia_1106.png) 所示,而把一個連線看作是兩個表的乘法u·v,那麼會發生一些有趣的事情:物化連線的變化流遵循乘積法則:(u·v)'= u'v + uv'。換句話說,任何推文的變化量都與當前的關注聯絡在一起,任何關注的變化量都與當前的推文相連線【49,50】。 #### 連線的時間依賴性 @@ -691,13 +691,13 @@ AMQP/JMS 風格的訊息代理 基於日誌的訊息代理 : 代理將一個分割槽中的所有訊息分配給同一個消費者節點,並始終以相同的順序傳遞訊息。並行是透過分割槽實現的,消費者透過存檔最近處理訊息的偏移量來跟蹤工作進度。訊息代理將訊息保留在磁碟上,因此如有必要的話,可以回跳並重新讀取舊訊息。 -基於日誌的方法與資料庫中的複製日誌(請參閱 [第五章](/v1/ch5))和日誌結構儲存引擎(請參閱 [第三章](/v1/ch3))有相似之處。我們看到,這種方法對於消費輸入流,併產生衍生狀態或衍生輸出資料流的系統而言特別適用。 +基於日誌的方法與資料庫中的複製日誌(請參閱 [第五章](/v1/ch5))和日誌結構儲存引擎(請參閱 [第三章](/v1/ch3))有相似之處。我們看到,這種方法對於消費輸入流,併產生派生狀態或派生輸出資料流的系統而言特別適用。 就流的來源而言,我們討論了幾種可能性:使用者活動事件,定期讀數的感測器,和 Feed 資料(例如,金融中的市場資料)能夠自然地表示為流。我們發現將資料庫寫入視作流也是很有用的:我們可以捕獲變更日誌 —— 即對資料庫所做的所有變更的歷史記錄 —— 隱式地透過變更資料捕獲,或顯式地透過事件溯源。日誌壓縮允許流也能保有資料庫內容的完整副本。 -將資料庫表示為流為系統整合帶來了很多強大機遇。透過消費變更日誌並將其應用至衍生系統,你能使諸如搜尋索引、快取以及分析系統這類衍生資料系統不斷保持更新。你甚至能從頭開始,透過讀取從創世至今的所有變更日誌,為現有資料建立全新的檢視。 +將資料庫表示為流為系統整合帶來了很多強大機遇。透過消費變更日誌並將其應用至派生系統,你能使諸如搜尋索引、快取以及分析系統這類派生資料系統不斷保持更新。你甚至能從頭開始,透過讀取從創世至今的所有變更日誌,為現有資料建立全新的檢視。 -像流一樣維護狀態以及訊息重播的基礎設施,是在各種流處理框架中實現流連線和容錯的基礎。我們討論了流處理的幾種目的,包括搜尋事件模式(複雜事件處理),計算分窗聚合(流分析),以及保證衍生資料系統處於最新狀態(物化檢視)。 +像流一樣維護狀態以及訊息重播的基礎設施,是在各種流處理框架中實現流連線和容錯的基礎。我們討論了流處理的幾種目的,包括搜尋事件模式(複雜事件處理),計算分窗聚合(流分析),以及保證派生資料系統處於最新狀態(物化檢視)。 然後我們討論了在流處理中對時間進行推理的困難,包括處理時間與事件時間戳之間的區別,以及當你認為視窗已經完事之後,如何處理到達的掉隊事件的問題。 diff --git a/content/tw/ch13.md b/content/tw/ch13.md index 24f136c..83ac682 100644 --- a/content/tw/ch13.md +++ b/content/tw/ch13.md @@ -36,21 +36,21 @@ breadcrumbs: false ### 組合使用派生資料的工具 -例如,為了處理任意關鍵詞的搜尋查詢,將 OLTP 資料庫與全文搜尋索引整合在一起是很常見的需求。儘管一些資料庫(例如 PostgreSQL)包含了全文索引功能,對於簡單的應用完全夠了【1】,但更複雜的搜尋能力就需要專業的資訊檢索工具了。相反的是,搜尋索引通常不適合作為持久的記錄系統,因此許多應用需要組合這兩種不同的工具以滿足所有需求。 +例如,為了處理任意關鍵詞的搜尋查詢,將 OLTP 資料庫與全文檢索索引整合在一起是很常見的需求。儘管一些資料庫(例如 PostgreSQL)包含了全文索引功能,對於簡單的應用完全夠了【1】,但更複雜的搜尋能力就需要專業的資訊檢索工具了。相反的是,搜尋索引通常不適合作為持久的記錄系統,因此許多應用需要組合這兩種不同的工具以滿足所有需求。 -我們在 “[保持系統同步](/tw/ch12#保持系統同步)” 中接觸過整合資料系統的問題。隨著資料不同表示形式的增加,整合問題變得越來越困難。除了資料庫和搜尋索引之外,也許你需要在分析系統(資料倉庫,或批處理和流處理系統)中維護資料副本;維護從原始資料中衍生的快取,或反規範化的資料版本;將資料灌入機器學習、分類、排名或推薦系統中;或者基於資料變更傳送通知。 +我們在 “[保持系統同步](/tw/ch12#保持系統同步)” 中接觸過整合資料系統的問題。隨著資料不同表示形式的增加,整合問題變得越來越困難。除了資料庫和搜尋索引之外,也許你需要在分析系統(資料倉庫,或批處理和流處理系統)中維護資料副本;維護從原始資料中派生的快取,或反規範化的資料版本;將資料灌入機器學習、分類、排名或推薦系統中;或者基於資料變更傳送通知。 令人驚訝的是,我經常看到軟體工程師做出這樣的陳述:“根據我的經驗,99% 的人只需要 X” 或者 “...... 不需要 X”(對於各種各樣的 X)。我認為這種陳述更像是發言人自己的經驗,而不是技術實際上的實用性。可能對資料執行的操作,其範圍極其寬廣。某人認為雞肋而毫無意義的功能可能是別人的核心需求。當你拉高視角,並考慮跨越整個組織範圍的資料流時,資料整合的需求往往就會變得明顯起來。 #### 理解資料流 -當需要在多個儲存系統中維護相同資料的副本以滿足不同的訪問模式時,你要對輸入和輸出瞭如指掌:哪些資料先寫入,哪些資料表示衍生自哪些來源?如何以正確的格式,將所有資料匯入正確的地方? +當需要在多個儲存系統中維護相同資料的副本以滿足不同的訪問模式時,你要對輸入和輸出瞭如指掌:哪些資料先寫入,哪些資料表示派生自哪些來源?如何以正確的格式,將所有資料匯入正確的地方? 例如,你可能會首先將資料寫入 **記錄系統** 資料庫,捕獲對該資料庫所做的變更(請參閱 “[變更資料捕獲](/tw/ch12#變更資料捕獲)”),然後將變更以相同的順序應用於搜尋索引。如果變更資料捕獲(CDC)是更新索引的唯一方式,則可以確定該索引完全派生自記錄系統,因此與其保持一致(除軟體錯誤外)。寫入資料庫是向該系統提供新輸入的唯一方式。 允許應用程式直接寫入搜尋索引和資料庫引入瞭如 [圖 11-4](/fig/ddia_1204.png) 所示的問題,其中兩個客戶端同時傳送衝突的寫入,且兩個儲存系統按不同順序處理它們。在這種情況下,既不是資料庫說了算,也不是搜尋索引說了算,所以它們做出了相反的決定,進入彼此間永續性的不一致狀態。 -如果你可以透過單個系統來提供所有使用者輸入,從而決定所有寫入的排序,則透過按相同順序處理寫入,可以更容易地衍生出其他資料表示。這是狀態機複製方法的一個應用,我們在 “[全序廣播](/tw/ch9#全序廣播)” 中看到。無論你使用變更資料捕獲還是事件溯源日誌,都不如簡單的基於全序的決策原則更重要。 +如果你可以透過單個系統來提供所有使用者輸入,從而決定所有寫入的排序,則透過按相同順序處理寫入,可以更容易地派生出其他資料表示。這是狀態機複製方法的一個應用,我們在 “[全序廣播](/tw/ch9#全序廣播)” 中看到。無論你使用變更資料捕獲還是事件溯源日誌,都不如簡單的基於全序的決策原則更重要。 基於事件日誌來更新派生資料的系統,通常可以做到 **確定性** 與 **冪等性**(請參閱 “[冪等性](/tw/ch12#冪等性)”),使得從故障中恢復相當容易。 @@ -93,7 +93,7 @@ breadcrumbs: false * 如果你可以記錄一個事件來記錄使用者在做出決定之前所看到的系統狀態,並給該事件一個唯一的識別符號,那麼後面的任何事件都可以引用該事件識別符號來記錄因果關係【4】。我們將在 “[讀也是事件](#讀也是事件)” 中回到這個想法。 * 衝突解決演算法(請參閱 “[自動衝突解決](/tw/ch5#自動衝突解決)”)有助於處理以意外順序傳遞的事件。它們對於維護狀態很有用,但如果行為有外部副作用(例如,給使用者傳送通知),就沒什麼幫助了。 -也許,隨著時間的推移,應用開發模式將出現,使得能夠有效地捕獲因果依賴關係,並且保持正確的衍生狀態,而不會迫使所有事件經歷全序廣播的瓶頸)。 +也許,隨著時間的推移,應用開發模式將出現,使得能夠有效地捕獲因果依賴關係,並且保持正確的派生狀態,而不會迫使所有事件經歷全序廣播的瓶頸)。 ### 批處理與流處理 @@ -105,11 +105,11 @@ breadcrumbs: false Spark 在批處理引擎上執行流處理,將流分解為 **微批次(microbatches)**,而 Apache Flink 則在流處理引擎上執行批處理【5】。原則上,一種型別的處理可以用另一種型別來模擬,但是效能特徵會有所不同:例如,在跳躍或滑動視窗上,微批次可能表現不佳【6】。 -#### 維護衍生狀態 +#### 維護派生狀態 批處理有著很強的函式式風格(即使其程式碼不是用函式式語言編寫的):它鼓勵確定性的純函式,其輸出僅依賴於輸入,除了顯式輸出外沒有副作用,將輸入視作不可變的,且輸出是僅追加的。流處理與之類似,但它擴充套件了運算元以允許受管理的、容錯的狀態(請參閱 “[失敗後重建狀態”](/tw/ch12#失敗後重建狀態))。 -具有良好定義的輸入和輸出的確定性函式的原理不僅有利於容錯(請參閱 “[冪等性](/tw/ch12#冪等性)”),也簡化了有關組織中資料流的推理【7】。無論派生資料是搜尋索引、統計模型還是快取,採用這種觀點思考都是很有幫助的:將其視為從一個東西衍生出另一個的資料管道,透過函式式應用程式碼推送一個系統的狀態變更,並將其效果應用至派生系統中。 +具有良好定義的輸入和輸出的確定性函式的原理不僅有利於容錯(請參閱 “[冪等性](/tw/ch12#冪等性)”),也簡化了有關組織中資料流的推理【7】。無論派生資料是搜尋索引、統計模型還是快取,採用這種觀點思考都是很有幫助的:將其視為從一個東西派生出另一個的資料管道,透過函式式應用程式碼推送一個系統的狀態變更,並將其效果應用至派生系統中。 原則上,派生資料系統可以同步地維護,就像關係資料庫在與索引表寫入操作相同的事務中同步更新次級索引一樣。然而,非同步是使基於事件日誌的系統穩健的原因:它允許系統的一部分故障被抑制在本地。而如果任何一個參與者失敗,分散式事務將中止,因此它們傾向於透過將故障傳播到系統的其餘部分來放大故障(請參閱 “[分散式事務的限制](/tw/ch9#分散式事務的限制)”)。 @@ -117,7 +117,7 @@ Spark 在批處理引擎上執行流處理,將流分解為 **微批次(micro #### 應用演化後重新處理資料 -在維護派生資料時,批處理和流處理都是有用的。流處理允許將輸入中的變化以低延遲反映在衍生檢視中,而批處理允許重新處理大量累積的歷史資料以便將新檢視匯出到現有資料集上。 +在維護派生資料時,批處理和流處理都是有用的。流處理允許將輸入中的變化以低延遲反映在派生檢視中,而批處理允許重新處理大量累積的歷史資料以便將新檢視匯出到現有資料集上。 特別是,重新處理現有資料為維護系統、演化並支援新功能和需求變更提供了一個良好的機制(請參閱 [第四章](/tw/ch4))。沒有重新進行處理,模式演化將僅限於簡單的變化,例如向記錄中新增新的可選欄位或新增新型別的記錄。無論是在寫時模式還是在讀時模式中都是如此(請參閱 “[文件模型中的模式靈活性](/tw/ch2#文件模型中的模式靈活性)”)。另一方面,透過重新處理,可以將資料集重組為一個完全不同的模型,以便更好地滿足新的要求。 @@ -129,7 +129,7 @@ Spark 在批處理引擎上執行流處理,將流分解為 **微批次(micro > > 以這種方式 “再加工” 現有的軌道,讓新舊版本並存,可以在幾年的時間內逐漸改變軌距。然而,這是一項昂貴的事業,這就是今天非標準軌距仍然存在的原因。例如,舊金山灣區的 BART 系統使用了與美國大部分地區不同的軌距。 -衍生檢視允許 **漸進演化(gradual evolution)**。如果你想重新構建資料集,不需要執行突然切換式的遷移。取而代之的是,你可以將舊架構和新架構並排維護為相同基礎資料上的兩個獨立衍生檢視。然後可以開始將少量使用者轉移到新檢視,以測試其效能並發現任何錯誤,而大多數使用者仍然會被路由到舊檢視。你可以逐漸地增加訪問新檢視的使用者比例,最終可以刪除舊檢視【10】。 +派生檢視允許 **漸進演化(gradual evolution)**。如果你想重新構建資料集,不需要執行突然切換式的遷移。取而代之的是,你可以將舊架構和新架構並排維護為相同基礎資料上的兩個獨立派生檢視。然後可以開始將少量使用者轉移到新檢視,以測試其效能並發現任何錯誤,而大多數使用者仍然會被路由到舊檢視。你可以逐漸地增加訪問新檢視的使用者比例,最終可以刪除舊檢視【10】。 這種逐漸遷移的美妙之處在於,如果出現問題,每個階段的過程都很容易逆轉:你始終有一個可以回滾的可用系統。透過降低不可逆損害的風險,你能對繼續前進更有信心,從而更快地改善系統【11】。 @@ -137,11 +137,11 @@ Spark 在批處理引擎上執行流處理,將流分解為 **微批次(micro 如果批處理用於重新處理歷史資料,而流處理用於處理最近的更新,那麼如何將這兩者結合起來?Lambda 架構【12】是這方面的一個建議,引起了很多關注。 -Lambda 架構的核心思想是透過將不可變事件附加到不斷增長的資料集來記錄傳入資料,這類似於事件溯源(請參閱 “[事件溯源](/tw/ch12#事件溯源)”)。為了從這些事件中衍生出讀取最佳化的檢視,Lambda 架構建議並行執行兩個不同的系統:批處理系統(如 Hadoop MapReduce)和獨立的流處理系統(如 Storm)。 +Lambda 架構的核心思想是透過將不可變事件附加到不斷增長的資料集來記錄傳入資料,這類似於事件溯源(請參閱 “[事件溯源](/tw/ch12#事件溯源)”)。為了從這些事件中派生出讀取最佳化的檢視,Lambda 架構建議並行執行兩個不同的系統:批處理系統(如 Hadoop MapReduce)和獨立的流處理系統(如 Storm)。 -在 Lambda 方法中,流處理器消耗事件並快速生成對檢視的近似更新;批處理器稍後將使用同一組事件並生成衍生檢視的更正版本。這個設計背後的原因是批處理更簡單,因此不易出錯,而流處理器被認為是不太可靠和難以容錯的(請參閱 “[容錯](/tw/ch12#容錯)”)。而且,流處理可以使用快速近似演算法,而批處理使用較慢的精確演算法。 +在 Lambda 方法中,流處理器消耗事件並快速生成對檢視的近似更新;批處理器稍後將使用同一組事件並生成派生檢視的更正版本。這個設計背後的原因是批處理更簡單,因此不易出錯,而流處理器被認為是不太可靠和難以容錯的(請參閱 “[容錯](/tw/ch12#容錯)”)。而且,流處理可以使用快速近似演算法,而批處理使用較慢的精確演算法。 -Lambda 架構是一種有影響力的想法,它將資料系統的設計變得更好,尤其是透過推廣這樣的原則:在不可變事件流上建立衍生檢視,並在需要時重新處理事件。但是我也認為它有一些實際問題: +Lambda 架構是一種有影響力的想法,它將資料系統的設計變得更好,尤其是透過推廣這樣的原則:在不可變事件流上建立派生檢視,並在需要時重新處理事件。但是我也認為它有一些實際問題: * 在批處理和流處理框架中維護相同的邏輯是很顯著的額外工作。雖然像 Summingbird【13】這樣的庫提供了一種可以在批處理和流處理的上下文中執行的計算抽象。除錯、調整和維護兩個不同系統的操作複雜性依然存在【14】。 * 由於流管道和批處理管道產生獨立的輸出,因此需要合併它們以響應使用者請求。如果計算是基於滾動視窗的簡單聚合,則合併相當容易,但如果檢視基於更複雜的操作(例如連線和會話化)而匯出,或者輸出不是時間序列,則會變得非常困難。 @@ -179,9 +179,9 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni * 次級索引,使你可以根據欄位的值有效地搜尋記錄(請參閱 “[其他索引結構](/tw/ch3#其他索引結構)”) * 物化檢視,這是一種預計算的查詢結果快取(請參閱 “[聚合:資料立方體和物化檢視](/tw/ch3#聚合:資料立方體和物化檢視)”) * 複製日誌,保持其他節點上資料的副本最新(請參閱 “[複製日誌的實現](/tw/ch5#複製日誌的實現)”) -* 全文搜尋索引,允許在文字中進行關鍵字搜尋(請參閱 “[全文搜尋和模糊索引](/tw/ch3#全文搜尋和模糊索引)”),也內置於某些關係資料庫【1】 +* 全文檢索索引,允許在文字中進行關鍵字搜尋(請參閱 “[全文檢索與模糊索引](/tw/ch3#全文檢索與模糊索引)”),也內置於某些關係資料庫【1】 -在 [第十一章](/tw/ch11) 和 [第十二章](/tw/ch12) 中,出現了類似的主題。我們討論了如何構建全文搜尋索引(請參閱 “[批處理工作流的輸出](/tw/ch11#批處理工作流的輸出)”),瞭解瞭如何維護物化檢視(請參閱 “[維護物化檢視](/tw/ch12#維護物化檢視)”)以及如何將變更從資料庫複製到派生資料系統(請參閱 “[變更資料捕獲](/tw/ch12#變更資料捕獲)”)。 +在 [第十一章](/tw/ch11) 和 [第十二章](/tw/ch12) 中,出現了類似的主題。我們討論了如何構建全文檢索索引(請參閱 “[批處理工作流的輸出](/tw/ch11#批處理工作流的輸出)”),瞭解瞭如何維護物化檢視(請參閱 “[維護物化檢視](/tw/ch12#維護物化檢視)”)以及如何將變更從資料庫複製到派生資料系統(請參閱 “[變更資料捕獲](/tw/ch12#變更資料捕獲)”)。 資料庫中內建的功能與人們用批處理和流處理器構建的派生資料系統似乎有相似之處。 @@ -256,16 +256,16 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni 在本節中,我將詳細介紹這些想法,並探討一些圍繞分拆資料庫和資料流的想法構建應用的方法。 -#### 應用程式碼作為衍生函式 +#### 應用程式碼作為派生函式 -當一個數據集衍生自另一個數據集時,它會經歷某種轉換函式。例如: +當一個數據集派生自另一個數據集時,它會經歷某種轉換函式。例如: * 次級索引是由一種直白的轉換函式生成的派生資料集:對於基礎表中的每行或每個文件,它挑選被索引的列或欄位中的值,並按這些值排序(假設使用 B 樹或 SSTable 索引,按鍵排序,如 [第三章](/tw/ch3) 所述)。 -* 全文搜尋索引是透過應用各種自然語言處理函式而建立的,諸如語言檢測、分詞、詞幹或詞彙化、拼寫糾正和同義詞識別,然後構建用於高效查詢的資料結構(例如倒排索引)。 -* 在機器學習系統中,我們可以將模型視作從訓練資料透過應用各種特徵提取、統計分析函式衍生的資料,當模型應用於新的輸入資料時,模型的輸出是從輸入和模型(因此間接地從訓練資料)中衍生的。 +* 全文檢索索引是透過應用各種自然語言處理函式而建立的,諸如語言檢測、分詞、詞幹或詞彙化、拼寫糾正和同義詞識別,然後構建用於高效查詢的資料結構(例如倒排索引)。 +* 在機器學習系統中,我們可以將模型視作從訓練資料透過應用各種特徵提取、統計分析函式派生的資料,當模型應用於新的輸入資料時,模型的輸出是從輸入和模型(因此間接地從訓練資料)中派生的。 * 快取通常包含將以使用者介面(UI)顯示的形式的資料聚合。因此填充快取需要知道 UI 中引用的欄位;UI 中的變更可能需要更新快取填充方式的定義,並重建快取。 -用於次級索引的衍生函式是如此常用的需求,以致於它作為核心功能被內建至許多資料庫中,你可以簡單地透過 `CREATE INDEX` 來呼叫它。對於全文索引,常見語言的基本語言特徵可能內建到資料庫中,但更複雜的特徵通常需要領域特定的調整。在機器學習中,特徵工程是眾所周知的特定於應用的特徵,通常需要包含很多關於使用者互動與應用部署的詳細知識【35】。 +用於次級索引的派生函式是如此常用的需求,以致於它作為核心功能被內建至許多資料庫中,你可以簡單地透過 `CREATE INDEX` 來呼叫它。對於全文索引,常見語言的基本語言特徵可能內建到資料庫中,但更複雜的特徵通常需要領域特定的調整。在機器學習中,特徵工程是眾所周知的特定於應用的特徵,通常需要包含很多關於使用者互動與應用部署的詳細知識【35】。 當建立派生資料集的函式不是像建立次級索引那樣的標準搬磚函式時,需要自定義程式碼來處理特定於應用的東西。而這個自定義程式碼是讓許多資料庫掙扎的地方,雖然關係資料庫通常支援觸發器、儲存過程和使用者定義的函式,可以用它們來在資料庫中執行應用程式碼,但它們有點像資料庫設計裡的事後反思。(請參閱 “[傳遞事件流](/tw/ch12#傳遞事件流)”)。 @@ -293,16 +293,16 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni 我們在 “[資料庫與流](/tw/ch12#資料庫與流)” 中看到了這一思路,我們討論了將資料庫的變更日誌視為一種我們可以訂閱的事件流。諸如 Actor 的訊息傳遞系統(請參閱 “[訊息傳遞中的資料流](/tw/ch4#訊息傳遞中的資料流)”)也具有響應事件的概念。早在 20 世紀 80 年代,**元組空間(tuple space)** 模型就已經探索了表達分散式計算的方式:觀察狀態變更並作出反應的過程【38,39】。 -如前所述,當觸發器由於資料變更而被觸發時,或次級索引更新以反映索引表中的變更時,資料庫內部也發生著類似的情況。分拆資料庫意味著將這個想法應用於在主資料庫之外,用於建立派生資料集:快取、全文搜尋索引、機器學習或分析系統。我們可以為此使用流處理和訊息傳遞系統。 +如前所述,當觸發器由於資料變更而被觸發時,或次級索引更新以反映索引表中的變更時,資料庫內部也發生著類似的情況。分拆資料庫意味著將這個想法應用於在主資料庫之外,用於建立派生資料集:快取、全文檢索索引、機器學習或分析系統。我們可以為此使用流處理和訊息傳遞系統。 需要記住的重要一點是,維護派生資料不同於執行非同步任務。傳統的訊息傳遞系統通常是為執行非同步任務設計的(請參閱 “[日誌與傳統的訊息傳遞相比](/tw/ch12#日誌與傳統的訊息傳遞相比)”): -* 在維護派生資料時,狀態變更的順序通常很重要(如果多個檢視是從事件日誌衍生的,則需要按照相同的順序處理事件,以便它們之間保持一致)。如 “[確認與重新傳遞](/tw/ch12#確認與重新傳遞)” 中所述,許多訊息代理在重傳未確認訊息時沒有此屬性,雙寫也被排除在外(請參閱 “[保持系統同步](/tw/ch12#保持系統同步)”)。 -* 容錯是派生資料的關鍵:僅僅丟失單個訊息就會導致派生資料集永遠與其資料來源失去同步。訊息傳遞和衍生狀態更新都必須可靠。例如,許多 Actor 系統預設在記憶體中維護 Actor 的狀態和訊息,所以如果執行 Actor 的機器崩潰,狀態和訊息就會丟失。 +* 在維護派生資料時,狀態變更的順序通常很重要(如果多個檢視是從事件日誌派生的,則需要按照相同的順序處理事件,以便它們之間保持一致)。如 “[確認與重新傳遞](/tw/ch12#確認與重新傳遞)” 中所述,許多訊息代理在重傳未確認訊息時沒有此屬性,雙寫也被排除在外(請參閱 “[保持系統同步](/tw/ch12#保持系統同步)”)。 +* 容錯是派生資料的關鍵:僅僅丟失單個訊息就會導致派生資料集永遠與其資料來源失去同步。訊息傳遞和派生狀態更新都必須可靠。例如,許多 Actor 系統預設在記憶體中維護 Actor 的狀態和訊息,所以如果執行 Actor 的機器崩潰,狀態和訊息就會丟失。 穩定的訊息排序和容錯訊息處理是相當嚴格的要求,但與分散式事務相比,它們開銷更小,執行更穩定。現代流處理元件可以提供這些排序和可靠性保證,並允許應用程式碼以流運算元的形式執行。 -這些應用程式碼可以執行任意處理,包括資料庫內建衍生函式通常不提供的功能。就像透過管道連結的 Unix 工具一樣,流運算元可以圍繞著資料流構建大型系統。每個運算元接受狀態變更的流作為輸入,併產生其他狀態變化的流作為輸出。 +這些應用程式碼可以執行任意處理,包括資料庫內建派生函式通常不提供的功能。就像透過管道連結的 Unix 工具一樣,流運算元可以圍繞著資料流構建大型系統。每個運算元接受狀態變更的流作為輸入,併產生其他狀態變化的流作為輸出。 #### 流處理器和服務 @@ -339,7 +339,7 @@ Unix 和關係資料庫以非常不同的哲學來處理資訊管理問題。Uni #### 物化檢視和快取 -全文搜尋索引就是一個很好的例子:寫路徑更新索引,讀路徑在索引中搜索關鍵字。讀寫都需要做一些工作。寫入需要更新文件中出現的所有關鍵詞的索引條目。讀取需要搜尋查詢中的每個單詞,並應用布林邏輯來查詢包含查詢中所有單詞(AND 運算子)的文件,或者每個單詞(OR 運算子)的任何同義詞。 +全文檢索索引就是一個很好的例子:寫路徑更新索引,讀路徑在索引中搜索關鍵字。讀寫都需要做一些工作。寫入需要更新文件中出現的所有關鍵詞的索引條目。讀取需要搜尋查詢中的每個單詞,並應用布林邏輯來查詢包含查詢中所有單詞(AND 運算子)的文件,或者每個單詞(OR 運算子)的任何同義詞。 如果沒有索引,搜尋查詢將不得不掃描所有文件(如 grep),如果有著大量文件,這樣做的開銷巨大。沒有索引意味著寫入路徑上的工作量較少(沒有要更新的索引),但是在讀取路徑上需要更多工作。 @@ -490,7 +490,7 @@ COMMIT; [例 12-2]() 依賴於 `request_id` 列上的唯一約束。如果一個事務嘗試插入一個已經存在的 ID,那麼 `INSERT` 失敗,事務被中止,使其無法生效兩次。即使在較弱的隔離級別下,關係資料庫也能正確地維護唯一性約束(而在 “[寫入偏差與幻讀](/tw/ch7#寫入偏差與幻讀)” 中討論過,應用級別的 **檢查 - 然後 - 插入** 可能會在不可序列化的隔離下失敗)。 -除了抑制重複的請求之外,[例 12-2]() 中的請求表表現得就像一種事件日誌,暗示著事件溯源的想法(請參閱 “[事件溯源](/tw/ch12#事件溯源)”)。更新賬戶餘額事實上不必與插入事件發生在同一個事務中,因為它們是冗餘的,而能由下游消費者從請求事件中衍生出來 —— 只要該事件被恰好處理一次,這又一次可以使用請求 ID 來強制執行。 +除了抑制重複的請求之外,[例 12-2]() 中的請求表表現得就像一種事件日誌,暗示著事件溯源的想法(請參閱 “[事件溯源](/tw/ch12#事件溯源)”)。更新賬戶餘額事實上不必與插入事件發生在同一個事務中,因為它們是冗餘的,而能由下游消費者從請求事件中派生出來 —— 只要該事件被恰好處理一次,這又一次可以使用請求 ID 來強制執行。 #### 端到端原則 @@ -563,7 +563,7 @@ COMMIT; 2. 流處理器讀取請求日誌。對於每個請求訊息,它向輸出流發出兩條訊息:付款人賬戶 A 的借記指令(按 A 分割槽),收款人 B 的貸記指令(按 B 分割槽)。被發出的訊息中會帶有原始的請求 ID。 3. 後續處理器消費借記 / 貸記指令流,按照請求 ID 除重,並將變更應用至賬戶餘額。 -步驟 1 和步驟 2 是必要的,因為如果客戶直接傳送貸記與借記指令,則需要在這兩個分割槽之間進行原子提交,以確保兩者要麼都發生或都不發生。為了避免對分散式事務的需要,我們首先將請求持久化記錄為單條訊息,然後從這第一條訊息中衍生出貸記指令與借記指令。幾乎在所有資料系統中,單物件寫入都是原子性的(請參閱 “[單物件寫入](/tw/ch7#單物件寫入)),因此請求要麼出現在日誌中,要麼就不出現,無需多分割槽原子提交。 +步驟 1 和步驟 2 是必要的,因為如果客戶直接傳送貸記與借記指令,則需要在這兩個分割槽之間進行原子提交,以確保兩者要麼都發生或都不發生。為了避免對分散式事務的需要,我們首先將請求持久化記錄為單條訊息,然後從這第一條訊息中派生出貸記指令與借記指令。幾乎在所有資料系統中,單物件寫入都是原子性的(請參閱 “[單物件寫入](/tw/ch7#單物件寫入)),因此請求要麼出現在日誌中,要麼就不出現,無需多分割槽原子提交。 如果流處理器在步驟 2 中崩潰,則它會從上一個存檔點恢復處理。這樣做時,它不會跳過任何請求訊息,但可能會多次處理請求併產生重複的貸記與借記指令。但由於它是確定性的,因此它只是再次生成相同的指令,而步驟 3 中的處理器可以使用端到端請求 ID 輕鬆地對其除重。 @@ -589,7 +589,7 @@ COMMIT; * 完整性(Integrity) - 完整性意味著沒有損壞;即沒有資料丟失,並且沒有矛盾或錯誤的資料。尤其是如果某些派生資料集是作為底層資料之上的檢視而維護的(請參閱 “[從事件日誌中派生出當前狀態](/tw/ch12#從事件日誌中派生出當前狀態)”),這種衍生必須是正確的。例如,資料庫索引必須正確地反映資料庫的內容 —— 缺失某些記錄的索引並不是很有用。 + 完整性意味著沒有損壞;即沒有資料丟失,並且沒有矛盾或錯誤的資料。尤其是如果某些派生資料集是作為底層資料之上的檢視而維護的(請參閱 “[從事件日誌中派生出當前狀態](/tw/ch12#從事件日誌中派生出當前狀態)”),這種派生必須是正確的。例如,資料庫索引必須正確地反映資料庫的內容 —— 缺失某些記錄的索引並不是很有用。 如果完整性被違背,這種不一致是永久的:在大多數情況下,等待與重試並不能修復資料庫損壞。相反的是,需要顯式地檢查與修復。在 ACID 事務的上下文中(請參閱 “[ACID 的含義](/tw/ch7#ACID的含義)”),一致性通常被理解為某種特定於應用的完整性概念。原子性和永續性是保持完整性的重要工具。 @@ -611,7 +611,7 @@ ACID 事務通常既提供及時性(例如線性一致性)也提供完整性 正如我們在上一節看到的那樣,可靠的流處理系統可以在無需分散式事務與原子提交協議的情況下保持完整性,這意味著它們有潛力達到與後者相當的正確性,同時還具備好得多的效能與運維穩健性。為了達成這種正確性,我們組合使用了多種機制: * 將寫入操作的內容表示為單條訊息,從而可以輕鬆地被原子寫入 —— 與事件溯源搭配效果拔群(請參閱 “[事件溯源](/tw/ch12#事件溯源)”)。 -* 使用與儲存過程類似的確定性衍生函式,從這一訊息中衍生出所有其他的狀態變更(請參閱 “[真的序列執行](/tw/ch7#真的序列執行)” 和 “[應用程式碼作為衍生函式](/ch13#應用程式碼作為衍生函式)”) +* 使用與儲存過程類似的確定性派生函式,從這一訊息中派生出所有其他的狀態變更(請參閱 “[真的序列執行](/tw/ch7#真的序列執行)” 和 “[應用程式碼作為派生函式](/ch13#應用程式碼作為派生函式)”) * 將客戶端生成的請求 ID 傳遞透過所有的處理層次,從而允許端到端的除重,帶來冪等性。 * 使訊息不可變,並允許派生資料能隨時被重新處理,這使從錯誤中恢復更加容易(請參閱 “[不可變事件的優點](/tw/ch12#不可變事件的優點)”) @@ -645,7 +645,7 @@ ACID 事務通常既提供及時性(例如線性一致性)也提供完整性 例如,這種系統可以使用多領導者配置運維,跨越多個數據中心,在區域間非同步複製。任何一個數據中心都可以持續獨立執行,因為不需要同步的跨區域協調。這樣的系統的及時性保證會很弱 —— 如果不引入協調它是不可能是線性一致的 —— 但它仍然可以提供有力的完整性保證。 -在這種情況下,可序列化事務作為維護衍生狀態的一部分仍然是有用的,但它們只能在小範圍內執行,在那裡它們工作得很好【8】。異構分散式事務(如 XA 事務,請參閱 “[實踐中的分散式事務](/tw/ch9#實踐中的分散式事務)”)不是必需的。同步協調仍然可以在需要的地方引入(例如在無法恢復的操作之前強制執行嚴格的約束),但是如果只是應用的一小部分地方需要它,沒必要讓所有操作都付出協調的代價。【43】。 +在這種情況下,可序列化事務作為維護派生狀態的一部分仍然是有用的,但它們只能在小範圍內執行,在那裡它們工作得很好【8】。異構分散式事務(如 XA 事務,請參閱 “[實踐中的分散式事務](/tw/ch9#實踐中的分散式事務)”)不是必需的。同步協調仍然可以在需要的地方引入(例如在無法恢復的操作之前強制執行嚴格的約束),但是如果只是應用的一小部分地方需要它,沒必要讓所有操作都付出協調的代價。【43】。 另一種審視協調與約束的角度是:它們減少了由於不一致而必須做出的道歉數量,但也可能會降低系統的效能和可用性,從而可能增加由於宕機中斷而需要做出的道歉數量。你不可能將道歉數量減少到零,但可以根據自己的需求尋找最佳平衡點 —— 既不存在太多不一致性,又不存在太多可用性問題。 @@ -693,9 +693,9 @@ ACID 意義下的一致性(請參閱 “[一致性](/tw/ch7#一致性)”) 如果一個事務在一個數據庫中改變了多個物件,在這一事實發生後,很難說清這個事務到底意味著什麼。即使你捕獲了事務日誌(請參閱 “[變更資料捕獲](/tw/ch12#變更資料捕獲)”),各種表中的插入、更新和刪除操作並不一定能清楚地表明 **為什麼** 要執行這些變更。決定這些變更的是應用邏輯中的呼叫,而這一應用邏輯稍縱即逝,無法重現。 -相比之下,基於事件的系統可以提供更好的可審計性。在事件溯源方法中,系統的使用者輸入被表示為一個單一不可變事件,而任何其導致的狀態變更都衍生自該事件。衍生可以實現為具有確定性與可重複性,因而相同的事件日誌透過相同版本的衍生程式碼時,會導致相同的狀態變更。 +相比之下,基於事件的系統可以提供更好的可審計性。在事件溯源方法中,系統的使用者輸入被表示為一個單一不可變事件,而任何其導致的狀態變更都派生自該事件。派生可以實現為具有確定性與可重複性,因而相同的事件日誌透過相同版本的派生程式碼時,會導致相同的狀態變更。 -顯式處理資料流(請參閱 “[批處理輸出的哲學](/tw/ch11#批處理輸出的哲學)”)可以使資料的 **來龍去脈(provenance)** 更加清晰,從而使完整性檢查更具可行性。對於事件日誌,我們可以使用雜湊來檢查事件儲存沒有被破壞。對於任何衍生狀態,我們可以重新執行從事件日誌中衍生它的批處理器與流處理器,以檢查是否獲得相同的結果,或者,甚至並行執行冗餘的衍生流程。 +顯式處理資料流(請參閱 “[批處理輸出的哲學](/tw/ch11#批處理輸出的哲學)”)可以使資料的 **來龍去脈(provenance)** 更加清晰,從而使完整性檢查更具可行性。對於事件日誌,我們可以使用雜湊來檢查事件儲存沒有被破壞。對於任何派生狀態,我們可以重新執行從事件日誌中派生它的批處理器與流處理器,以檢查是否獲得相同的結果,或者,甚至並行執行冗餘的派生流程。 具有確定性且定義良好的資料流,也使除錯與跟蹤系統的執行變得容易,以便確定它 **為什麼** 做了某些事情【4,69】。如果出現意想之外的事情,那麼重現導致意外事件的確切事故現場的診斷能力 —— 一種時間旅行除錯功能是非常有價值的。 @@ -875,13 +875,13 @@ ACID 意義下的一致性(請參閱 “[一致性](/tw/ch7#一致性)”) 在本章中,我們討論了設計資料系統的新方式,而且也包括了我的個人觀點,以及對未來的猜測。我們從這樣一種觀察開始:沒有單種工具能高效服務所有可能的用例,因此應用必須組合使用幾種不同的軟體才能實現其目標。我們討論了如何使用批處理與事件流來解決這一 **資料整合(data integration)** 問題,以便讓資料變更在不同系統之間流動。 -在這種方法中,某些系統被指定為記錄系統,而其他資料則透過轉換衍生自記錄系統。透過這種方式,我們可以維護索引、物化檢視、機器學習模型、統計摘要等等。透過使這些衍生和轉換操作非同步且鬆散耦合,能夠防止一個區域中的問題擴散到系統中不相關部分,從而增加整個系統的穩健性與容錯性。 +在這種方法中,某些系統被指定為記錄系統,而其他資料則透過轉換派生自記錄系統。透過這種方式,我們可以維護索引、物化檢視、機器學習模型、統計摘要等等。透過使這些派生和轉換操作非同步且鬆散耦合,能夠防止一個區域中的問題擴散到系統中不相關部分,從而增加整個系統的穩健性與容錯性。 -將資料流表示為從一個數據集到另一個數據集的轉換也有助於演化應用程式:如果你想變更其中一個處理步驟,例如變更索引或快取的結構,則可以在整個輸入資料集上重新執行新的轉換程式碼,以便重新衍生輸出。同樣,出現問題時,你也可以修復程式碼並重新處理資料以便恢復。 +將資料流表示為從一個數據集到另一個數據集的轉換也有助於演化應用程式:如果你想變更其中一個處理步驟,例如變更索引或快取的結構,則可以在整個輸入資料集上重新執行新的轉換程式碼,以便重新派生輸出。同樣,出現問題時,你也可以修復程式碼並重新處理資料以便恢復。 這些過程與資料庫內部已經完成的過程非常類似,因此我們將資料流應用的概念重新改寫為,**分拆(unbundling)** 資料庫元件,並透過組合這些鬆散耦合的元件來構建應用程式。 -衍生狀態可以透過觀察底層資料的變更來更新。此外,衍生狀態本身可以進一步被下游消費者觀察。我們甚至可以將這種資料流一路傳送至顯示資料的終端使用者裝置,從而構建可動態更新以反映資料變更,並在離線時能繼續工作的使用者介面。 +派生狀態可以透過觀察底層資料的變更來更新。此外,派生狀態本身可以進一步被下游消費者觀察。我們甚至可以將這種資料流一路傳送至顯示資料的終端使用者裝置,從而構建可動態更新以反映資料變更,並在離線時能繼續工作的使用者介面。 接下來,我們討論了如何確保所有這些處理在出現故障時保持正確。我們看到可伸縮的強完整性保證可以透過非同步事件處理來實現,透過使用端到端操作識別符號使操作冪等,以及透過非同步檢查約束。客戶端可以等到檢查透過,或者不等待繼續前進,但是可能會冒有違反約束需要道歉的風險。這種方法比使用分散式事務的傳統方法更具可伸縮性與可靠性,並且在實踐中適用於很多業務流程。 diff --git a/content/tw/ch2.md b/content/tw/ch2.md index e438970..1d95dfb 100644 --- a/content/tw/ch2.md +++ b/content/tw/ch2.md @@ -1,43 +1,44 @@ --- title: "2. 定義非功能性需求" weight: 102 -math: true breadcrumbs: false --- ![](/map/ch01.png) -> *網際網路做得如此之好,以至於大多數人認為它是一種自然資源,就像太平洋一樣,而不是人造的東西。上一次有這種規模的技術如此沒有錯誤是什麼時候?* +> *網際網路做得如此之好,以至於大多數人都把它想象成像太平洋一樣的自然資源,而不是人造的東西。上一次出現這種規模且無差錯的技術是什麼時候?* > -> [Alan Kay](https://www.drdobbs.com/architecture-and-design/interview-with-alan-kay/240003442), -> 在接受 *Dr Dobb's Journal* 採訪時(2012) +> [艾倫・凱](https://www.drdobbs.com/architecture-and-design/interview-with-alan-kay/240003442), +> 在接受 *Dr Dobb's Journal* 採訪時(2012 年) -如果你正在構建一個應用程式,你將受到一系列需求的驅動。在你的列表頂部,最可能的是應用程式必須提供的功能:你需要什麼螢幕和什麼按鈕,以及每個操作應該做什麼以實現你的軟體目的。這些是你的 **功能性需求**。 +如果你正在構建一個應用程式,你將會被一系列需求所驅動。在你的需求列表中,最重要的可能是應用程式必須提供的功能:需要哪些介面和按鈕,以及每個操作應該做什麼,以實現軟體的目的。這些是你的 ***功能性需求***。 -此外,你可能還有一些 **非功能性需求**:例如,應用程式應該快速、可靠、安全、合規,並且易於維護。這些需求可能沒有明確寫下來,因為它們可能看起來有些顯而易見,但它們和應用程式的功能一樣重要:一個慢得難以忍受或不可靠的應用程式可能等同於不存在。 +此外,你可能還有一些 ***非功能性需求***:例如,應用程式應該快速、可靠、安全、合規,並且易於維護。 +這些需求可能沒有明確寫下來,因為它們看起來有些顯而易見,但它們與應用程式的功能同樣重要:一個慢得讓人無法忍受或不可靠的應用程式還不如不存在。 -許多非功能性需求,如安全性,超出了本書的範圍。但是有一些非功能性需求我們將考慮,本章將幫助你為自己的系統闡明它們: +許多非功能性需求,比如安全性,超出了本書的範圍。但我們將考慮一些非功能性需求,本章將幫助你為自己的系統闡明它們: * 如何定義和衡量系統的 **效能**(參見 ["描述效能"](/tw/ch2#sec_introduction_percentiles)); -* 服務 **可靠** 意味著什麼——即使在出現問題時也能繼續正確工作(參見 ["可靠性和容錯"](/tw/ch2#sec_introduction_reliability)); -* 透過在系統負載增長時有效新增計算能力的方法,使系統具有 **可擴充套件性**(參見 ["可擴充套件性"](/tw/ch2#sec_introduction_scalability));以及 -* 使系統在長期內更容易維護(參見 ["可維護性"](/tw/ch2#sec_introduction_maintainability))。 +* 服務 **可靠** 意味著什麼——即即使出現問題也能繼續正確工作(參見 ["可靠性與容錯"](/tw/ch2#sec_introduction_reliability)); +* 透過在系統負載增長時新增計算能力的有效方法,使系統具有 **可伸縮性**(參見 ["可伸縮性"](/tw/ch2#sec_introduction_scalability));以及 +* 使系統長期更 **易於維護**(參見 ["可運維性"](/tw/ch2#sec_introduction_maintainability))。 + +本章介紹的術語在後續章節中也很有用,當我們深入研究資料密集型系統的實現細節時。然而,抽象定義可能相當枯燥;為了使這些想法更具體,我們將從一個案例研究開始本章,研究社交網路服務可能如何工作,這將提供效能和可伸縮性的實際案例。 -本章介紹的術語在後續章節中也很有用,當我們深入瞭解資料密集型系統的實現細節時。然而,抽象定義可能相當枯燥;為了讓這些想法更具體,我們將以一個社交網路服務如何工作的案例研究開始本章,這將提供效能和可擴充套件性的實際示例。 ## 案例研究:社交網路首頁時間線 {#sec_introduction_twitter} -想象一下,你被賦予實現一個 X(前 Twitter)風格的社交網路的任務,使用者可以釋出訊息並關注其他使用者。這將是對這種服務實際工作方式的巨大簡化 [^1] [^2] [^3],但它將有助於說明大規模系統中出現的一些問題。 +想象一下,你被賦予了實現一個類似 X(前身為 Twitter)風格的社交網路的任務,使用者可以釋出訊息並關注其他使用者。這將是對這種服務實際工作方式的巨大簡化 [^1] [^2] [^3],但它將有助於說明大規模系統中出現的一些問題。 -假設使用者每天釋出 5 億條訊息,平均每秒 5,700 條帖子。偶爾,速率可能飆升至每秒 150,000 條帖子 [^4]。我們還假設平均使用者關注 200 人,並有 200 個粉絲(儘管範圍很廣:大多數人只有少數粉絲,而少數名人如巴拉克·奧巴馬有超過 1 億粉絲)。 +假設使用者每天釋出 5 億條帖子,或平均每秒 5,700 條帖子。偶爾,速率可能飆升至每秒 150,000 條帖子 [^4]。我們還假設平均每個使用者關注 200 人並有 200 個粉絲(儘管範圍非常廣泛:大多數人只有少數粉絲,而少數名人如巴拉克・奧巴馬有超過 1 億粉絲)。 ### 表示使用者、帖子與關注關係 {#id20} -想象我們將所有資料儲存在關係資料庫中,如 [圖 2-1](/tw/ch2#fig_twitter_relational) 所示。我們有一個使用者表,一個帖子表,和一個關注關係表。 +假設我們將所有資料儲存在關係資料庫中,如 [圖 2-1](/tw/ch2#fig_twitter_relational) 所示。我們有一個使用者表、一個帖子表和一個關注關係表。 -{{< figure src="/fig/ddia_0201.png" id="fig_twitter_relational" caption="圖 2-1. 使用者可以相互關注的社交網路的簡單關係模式。" class="w-full my-4" >}} +{{< figure src="/fig/ddia_0201.png" id="fig_twitter_relational" caption="圖 2-1. 社交網路的簡單關係模式,使用者可以相互關注。" class="w-full my-4" >}} -假設我們的社交網路必須支援的主要讀取操作是 **主頁時間線**,它顯示你關注的人最近的帖子(為簡單起見,我們將忽略廣告、來自你未關注的人的建議帖子和其他擴充套件)。我們可以編寫以下 SQL 查詢來獲取特定使用者的主頁時間線: +假設我們的社交網路必須支援的主要讀取操作是 *首頁時間線*,它顯示你關注的人最近釋出的帖子(為簡單起見,我們將忽略廣告、來自你未關注的人的推薦帖子和其他擴充套件)。我們可以編寫以下 SQL 查詢來獲取特定使用者的首頁時間線: ```sql SELECT posts.*, users.* FROM posts @@ -48,103 +49,103 @@ SELECT posts.*, users.* FROM posts LIMIT 1000 ``` -要執行此查詢,資料庫將使用 `follows` 表查詢 `current_user` 關注的所有人,查詢這些使用者的最近帖子,並按時間戳排序以獲取任何被關注使用者的最新 1,000 條帖子。 +要執行此查詢,資料庫將使用 `follows` 表找到 `current_user` 關注的所有人,查詢這些使用者最近的帖子,並按時間戳排序以獲取被關注使用者的最新 1,000 條帖子。 -帖子應該是及時的,所以假設在某人釋出帖子後,我們希望他們的粉絲能夠在 5 秒內看到它。一種方法是使用者的客戶端在使用者線上時每 5 秒重複上述查詢(這稱為 **輪詢**)。如果我們假設有 1000 萬用戶同時線上並登入,那意味著每秒執行查詢 200 萬次。即使你增加輪詢間隔,這也是很多的。 +帖子應該是及時的,所以假設在某人釋出帖子後,我們希望他們的粉絲能夠在 5 秒內看到它。一種方法是讓使用者的客戶端每 5 秒重複上述查詢(這稱為 *輪詢*)。如果我們假設有 1000 萬用戶同時線上登入,這意味著每秒執行 200 萬次查詢。即使增加輪詢間隔,這也是很大的負載。 -此外,上述查詢相當昂貴:如果你關注 200 人,它需要獲取這 200 人中每個人的最近帖子列表,併合並這些列表。每秒 200 萬次時間線查詢意味著資料庫需要每秒從某個傳送者查詢最近的帖子 4 億次——一個巨大的數字。這是平均情況。一些使用者關注數萬個帳戶;對他們來說,這個查詢執行起來非常昂貴,並且很難快速執行。 +此外,上述查詢相當昂貴:如果你關注 200 人,它需要獲取這 200 人中每個人的最近帖子列表,併合並這些列表。每秒 200 萬次時間線查詢意味著資料庫需要每秒查詢某個傳送者的最近帖子 4 億次——這是一個巨大的數字。這是平均情況。一些使用者關注數萬個賬戶;對他們來說,這個查詢執行起來非常昂貴,而且很難快速完成。 ### 時間線的物化與更新 {#sec_introduction_materializing} -我們如何做得更好?首先,伺服器主動將新帖子推送給當前線上的任何粉絲,而不是輪詢,會更好。其次,我們應該預先計算上述查詢的結果,以便可以從快取中提供使用者對其主頁時間線的請求。 +我們如何做得更好?首先,與其輪詢,不如伺服器主動向當前線上的任何粉絲推送新帖子。其次,我們應該預先計算上述查詢的結果,以便可以從快取中提供使用者的首頁時間線請求。 -想象一下,對於每個使用者,我們儲存一個包含其主頁時間線的資料結構,即他們關注的人的最近帖子。每次使用者釋出帖子時,我們查詢他們的所有粉絲,並將該帖子插入每個粉絲的主頁時間線——就像將訊息投遞到郵箱一樣。現在當用戶登入時,我們可以簡單地給他們我們預先計算的主頁時間線。此外,要接收有關其時間線上任何新帖子的通知,使用者的客戶端只需訂閱新增到其主頁時間線的帖子流。 +想象一下,我們為每個使用者儲存一個包含其首頁時間線的資料結構,即他們關注的人的最近帖子。每次使用者釋出帖子時,我們查詢他們的所有粉絲,並將該帖子插入到每個粉絲的首頁時間線中——就像向郵箱投遞訊息一樣。現在當用戶登入時,我們可以簡單地給他們這個預先計算的首頁時間線。此外,要接收時間線上任何新帖子的通知,使用者的客戶端只需訂閱新增到其首頁時間線的帖子流。 -這種方法的缺點是,每次使用者釋出帖子時,我們現在需要做更多的工作,因為主頁時間線是需要更新的派生資料。該過程如 [圖 2-2](/tw/ch2#fig_twitter_timelines) 所示。當一個初始請求導致執行幾個下游請求時,我們使用術語 **扇出** 來描述請求數量增加的因子。 +這種方法的缺點是,現在每次使用者釋出帖子時我們需要做更多的工作,因為首頁時間線是需要更新的派生資料。該過程如 [圖 2-2](/tw/ch2#fig_twitter_timelines) 所示。當一個初始請求導致幾個下游請求被執行時,我們使用術語 *扇出* 來描述請求數量增加的因子。 -{{< figure src="/fig/ddia_0202.png" id="fig_twitter_timelines" caption="圖 2-2. 扇出:將新帖子投遞給釋出帖子的使用者的每個粉絲。" class="w-full my-4" >}} +{{< figure src="/fig/ddia_0202.png" id="fig_twitter_timelines" caption="圖 2-2. 扇出:將新帖子傳遞給釋出帖子的使用者的每個粉絲。" class="w-full my-4" >}} -以每秒 5,700 條帖子的速率,如果平均帖子到達 200 個粉絲(即扇出因子為 200),我們將需要每秒執行超過 100 萬次主頁時間線寫入。這很多,但與我們否則必須執行的每秒 4 億次按傳送者的帖子查詢相比,這仍然是一個顯著的節省。 +以每秒 5,700 條帖子的速率,如果平均帖子到達 200 個粉絲(即扇出因子為 200),我們將需要每秒執行超過 100 萬次首頁時間線寫入。這很多,但與我們本來需要的每秒 4 億次每個傳送者的帖子查詢相比,這仍然是一個顯著的節省。 -如果由於某些特殊事件導致帖子速率激增,我們不必立即進行時間線投遞——我們可以將它們排隊,並接受帖子暫時需要更長時間才能顯示在粉絲的時間線中。即使在這種負載峰值期間,時間線仍然可以快速載入,因為我們只是從快取中提供它們。 +如果由於某些特殊事件導致帖子速率激增,我們不必立即進行時間線交付——我們可以將它們排隊,並接受帖子在粉絲的時間線中顯示會暫時花費更長時間。即使在這種負載峰值期間,時間線仍然可以快速載入,因為我們只是從快取中提供它們。 -這種預先計算和更新查詢結果的過程稱為 **物化**,時間線快取是 **物化檢視** 的一個例子(我們將在 [Link to Come] 中進一步討論這個概念)。物化檢視加速了讀取,但作為回報,我們必須在寫入時做更多的工作。對於大多數使用者來說,寫入的成本是適度的,但社交網路還必須考慮一些極端情況: +這種預先計算和更新查詢結果的過程稱為 *物化*,時間線快取是 *物化檢視* 的一個例子(我們將在 [後續章節] 中進一步討論這個概念)。物化檢視加速了讀取,但作為回報,我們必須在寫入時做更多的工作。對於大多數使用者來說,寫入成本是適度的,但社交網路還必須考慮一些極端情況: -* 如果使用者關注了大量帳戶,並且這些帳戶釋出了很多內容,那麼該使用者的物化時間線將有很高的寫入率。然而,在這種情況下,使用者實際上不太可能閱讀其時間線中的所有帖子,因此可以簡單地丟棄其時間線寫入的一部分,只向用戶顯示他們關注的帳戶的帖子樣本 [^5]。 -* 當擁有大量粉絲的名人帳戶釋出帖子時,我們必須做大量工作將該帖子插入其數百萬粉絲的主頁時間線。在這種情況下,不能丟棄其中一些寫入。解決此問題的一種方法是將名人帖子與其他人的帖子分開處理:我們可以透過單獨儲存名人帖子並在讀取物化時間線時將它們合併,從而節省將它們新增到數百萬時間線的工作。儘管有這些最佳化,在社交網路上處理名人可能需要大量基礎設施 [^6]。 +* 如果使用者關注非常多的賬戶,並且這些賬戶釋出很多內容,該使用者的物化時間線將有很高的寫入率。然而,在這種情況下,使用者實際上不太可能閱讀其時間線中的所有帖子,因此可以簡單地丟棄其時間線的一些寫入,只向用戶顯示他們關注的賬戶的帖子樣本 [^5]。 +* 當擁有大量粉絲的名人賬戶釋出帖子時,我們必須做大量工作將該帖子插入到他們數百萬粉絲的每個首頁時間線中。在這種情況下,丟棄一些寫入是不可接受的。解決這個問題的一種方法是將名人帖子與其他人的帖子分開處理:我們可以透過將名人帖子單獨儲存並在讀取時與物化時間線合併,來節省將它們新增到數百萬時間線的工作。儘管有這些最佳化,在社交網路上處理名人仍然需要大量基礎設施 [^6]。 ## 描述效能 {#sec_introduction_percentiles} -大多數關於軟體效能的討論考慮兩種主要型別的指標: +大多數關於軟體效能的討論都考慮兩種主要的度量型別: 響應時間 -: 從使用者發出請求到收到請求答案的經過時間。測量單位是秒(或毫秒,或微秒)。 +: 從使用者發出請求到收到所請求答案的經過時間。測量單位是秒(或毫秒,或微秒)。 吞吐量 -: 系統正在處理的每秒請求數,或每秒資料量。對於給定的硬體資源分配,存在可以處理的 **最大吞吐量**。測量單位是"每秒的某物"。 +: 系統正在處理的每秒請求數,或每秒資料量。對於給定的硬體資源分配,存在可以處理的 *最大吞吐量*。測量單位是"每秒某物"。 -在社交網路案例研究中,"每秒帖子數"和"每秒時間線寫入數"是吞吐量指標,而"載入主頁時間線所需的時間"或"帖子投遞給粉絲所需的時間"是響應時間指標。 +在社交網路案例研究中,"每秒帖子數"和"每秒時間線寫入數"是吞吐量指標,而"載入首頁時間線所需的時間"或"帖子傳遞給粉絲的時間"是響應時間指標。 -吞吐量和響應時間之間通常存在聯絡;[圖 2-3](/tw/ch2#fig_throughput) 勾畫了線上服務的這種關係示例。當請求吞吐量低時,服務的響應時間低,但隨著負載增加,響應時間增加。這是因為 **排隊**:當請求到達高負載系統時,CPU 很可能已經在處理較早的請求,因此傳入的請求需要等待較早的請求完成。隨著吞吐量接近硬體可以處理的最大值,排隊延遲急劇增加。 +吞吐量和響應時間之間通常存在聯絡;線上服務的這種關係示例如 [圖 2-3](/tw/ch2#fig_throughput) 所示。當請求吞吐量較低時,服務具有較低的響應時間,但隨著負載增加,響應時間也會增加。這是因為 *排隊*:當請求到達高負載系統時,CPU 很可能已經在處理先前的請求,因此傳入請求需要等待先前請求完成。隨著吞吐量接近硬體可以處理的最大值,排隊延遲急劇增加。 -{{< figure src="/fig/ddia_0203.png" id="fig_throughput" caption="圖 2-3. 當服務的吞吐量接近其容量時,由於排隊,響應時間急劇增加。" class="w-full my-4" >}} +{{< figure src="/fig/ddia_0203.png" id="fig_throughput" caption="圖 2-3. 隨著服務的吞吐量接近其容量,由於排隊,響應時間急劇增加。" class="w-full my-4" >}} -------- > [!TIP] 當過載系統無法恢復時 -如果系統接近過載,吞吐量被推到接近極限,它有時會進入惡性迴圈,變得效率更低,因此更加過載。例如,如果有一長串請求等待處理,響應時間可能會增加到客戶端超時並重新發送其請求的程度。這導致請求率進一步增加,使問題變得更糟——**重試風暴**。即使負載再次降低,這樣的系統也可能保持過載狀態,直到重新啟動或以其他方式重置。這種現象被稱為 **亞穩態故障**,它可能導致生產系統中的嚴重中斷 [^7] [^8]。 +如果系統接近過載,吞吐量被推到極限附近,它有時會進入惡性迴圈,變得效率更低,從而更加過載。例如,如果有很長的請求佇列等待處理,響應時間可能會增加到客戶端超時並重新發送請求的程度。這導致請求率進一步增加,使問題變得更糟——*重試風暴*。即使負載再次降低,這樣的系統也可能保持過載狀態,直到重新啟動或以其他方式重置。這種現象稱為 *亞穩態故障*,它可能導致生產系統的嚴重中斷 [^7] [^8]。 -為了避免重試使服務過載,你可以在客戶端增加並隨機化連續重試之間的時間(**指數退避** [^9] [^10]),並暫時停止向最近返回錯誤或超時的服務傳送請求(使用 **熔斷器** [^11] [^12] 或 **令牌桶** 演算法 [^13])。伺服器還可以檢測何時接近過載並開始主動拒絕請求(**負載卸除** [^14]),併發送要求客戶端減速的響應(**背壓** [^1] [^15])。排隊和負載均衡演算法的選擇也可能產生影響 [^16]。 +為了避免重試使服務過載,你可以在客戶端增加並隨機化連續重試之間的時間(*指數退避* [^9] [^10]),並暫時停止向最近返回錯誤或超時的服務傳送請求(使用 *熔斷器* [^11] [^12] 或 *令牌桶* 演算法 [^13])。伺服器還可以檢測何時接近過載並開始主動拒絕請求(*負載卸除* [^14]),併發送響應要求客戶端減速(*背壓* [^1] [^15])。排隊和負載均衡演算法的選擇也可能產生影響 [^16]。 -------- -就效能指標而言,響應時間通常是使用者最關心的,而吞吐量決定了所需的計算資源(例如,你需要多少伺服器),因此決定了服務特定工作負載的成本。如果吞吐量可能增長超過當前硬體可以處理的範圍,則需要擴充套件容量;如果透過新增計算資源可以顯著增加其最大吞吐量,則稱系統是 **可擴充套件的**。 +就效能指標而言,響應時間通常是使用者最關心的,而吞吐量決定了所需的計算資源(例如,你需要多少伺服器),因此決定了服務特定工作負載的成本。如果吞吐量可能會增長超出當前硬體可以處理的範圍,則需要擴充套件容量;如果系統的最大吞吐量可以透過新增計算資源顯著增加,則稱系統為 *可伸縮的*。 -在本節中,我們將主要關注響應時間,我們將在 ["可擴充套件性"](/tw/ch2#sec_introduction_scalability) 中回到吞吐量和可擴充套件性。 +在本節中,我們將主要關注響應時間,我們將在 ["可伸縮性"](/tw/ch2#sec_introduction_scalability) 中回到吞吐量和可伸縮性。 ### 延遲與響應時間 {#id23} -"延遲"和"響應時間"有時可以互換使用,但在本書中,我們將以特定方式使用這些術語(如 [圖 2-4](/tw/ch2#fig_response_time) 所示): +"延遲"和"響應時間"有時可互換使用,但在本書中我們將以特定方式使用這些術語(如 [圖 2-4](/tw/ch2#fig_response_time) 所示): -* **響應時間** 是客戶端看到的;它包括系統中任何地方產生的所有延遲。 -* **服務時間** 是服務主動處理使用者請求的持續時間。 -* **排隊延遲** 可能發生在流程的幾個點:例如,在收到請求後,它可能需要等到 CPU 可用才能處理;如果同一臺機器上的其他任務通過出站網路介面傳送大量資料,響應資料包可能需要在傳送到網路之前進行緩衝。 -* **延遲** 是請求未被主動處理的時間的總稱,即它是 **潛在的** 時間。特別是,**網路延遲** 或 **網路延遲** 是指請求和響應在網路中傳輸所花費的時間。 +* *響應時間* 是客戶端看到的;它包括系統中任何地方產生的所有延遲。 +* *服務時間* 是服務主動處理使用者請求的持續時間。 +* *排隊延遲* 可能發生在流程中的幾個點:例如,在收到請求後,它可能需要等待直到 CPU 可用才能被處理;如果同一臺機器上的其他任務通過出站網路介面傳送大量資料,響應資料包可能需要在傳送之前進行緩衝。 +* *延遲* 是一個涵蓋請求未被主動處理時間的總稱,即在此期間它是 *潛在的*。特別是,*網路延遲* 或 *網路延遲* 指的是請求和響應在網路中傳輸所花費的時間。 {{< figure src="/fig/ddia_0204.png" id="fig_response_time" caption="圖 2-4. 響應時間、服務時間、網路延遲和排隊延遲。" class="w-full my-4" >}} -在 [圖 2-4](/tw/ch2#fig_response_time) 中,時間從左到右流動,每個通訊節點顯示為水平線,請求或響應訊息顯示為從一個節點到另一個節點的粗對角箭頭。你將在本書的過程中經常遇到這種風格的圖表。 +在 [圖 2-4](/tw/ch2#fig_response_time) 中,時間從左到右流動,每個通訊節點顯示為水平線,請求或響應訊息顯示為從一個節點到另一個節點的粗對角箭頭。你將在本書中經常遇到這種風格的圖表。 -響應時間可能從一個請求到下一個請求顯著變化,即使你一遍又一遍地發出相同的請求。許多因素可能會增加隨機延遲:例如,上下文切換到後臺程序、網路資料包丟失和 TCP 重傳、垃圾回收暫停、強制從磁碟讀取的頁面錯誤、伺服器機架中的機械振動 [^17],或許多其他原因。我們將在 ["超時和無界延遲"](/tw/ch9#sec_distributed_queueing) 中更詳細地討論這個主題。 +響應時間可能會因請求而異,即使你一遍又一遍地發出相同的請求。許多因素可能會增加隨機延遲:例如,上下文切換到後臺程序、網路資料包丟失和 TCP 重傳、垃圾回收暫停、強制從磁碟讀取的缺頁錯誤、伺服器機架中的機械振動 [^17],或許多其他原因。我們將在 ["超時與無界延遲"](/tw/ch9#sec_distributed_queueing) 中更詳細地討論這個主題。 -排隊延遲通常佔響應時間變化的很大一部分。由於伺服器只能並行處理少量事物(例如,受其 CPU 核心數量的限制),只需要少量慢速請求就可以阻止後續請求的處理——這種效應稱為 **隊頭阻塞**。即使那些後續請求具有快速的服務時間,由於等待先前請求完成的時間,客戶端也會看到緩慢的整體響應時間。排隊延遲不是服務時間的一部分,因此在客戶端測量響應時間很重要。 +排隊延遲通常佔響應時間變化的很大一部分。由於伺服器只能並行處理少量事務(例如,受其 CPU 核心數的限制),只需要少量慢請求就可以阻塞後續請求的處理——這種效應稱為 *隊頭阻塞*。即使那些後續請求的服務時間很快,由於等待先前請求完成的時間,客戶端仍會看到緩慢的整體響應時間。排隊延遲不是服務時間的一部分,因此在客戶端測量響應時間很重要。 ### 平均值、中位數與百分位數 {#id24} -因為響應時間從一個請求到下一個請求各不相同,我們需要將其視為不是單個數字,而是可以測量的值的 **分佈**。在 [圖 2-5](/tw/ch2#fig_lognormal) 中,每個灰色條代表對服務的請求,其高度顯示該請求花費了多長時間。大多數請求相當快,但偶爾有 **異常值** 需要更長時間。網路延遲的變化也稱為 **抖動**。 +因為響應時間因請求而異,我們需要將其視為值的 *分佈*,而不是單個數字。在 [圖 2-5](/tw/ch2#fig_lognormal) 中,每個灰色條表示對服務的請求,其高度顯示該請求花費的時間。大多數請求相當快,但偶爾會有 *異常值* 需要更長時間。網路延遲的變化也稱為 *抖動*。 -{{< figure src="/fig/ddia_0205.png" id="fig_lognormal" caption="圖 2-5. 說明平均值和百分位數:對服務的 100 個請求樣本的響應時間。" class="w-full my-4" >}} +{{< figure src="/fig/ddia_0205.png" id="fig_lognormal" caption="圖 2-5. 說明平均值和百分位數:100 個服務請求的響應時間樣本。" class="w-full my-4" >}} -報告服務的 **平均** 響應時間是很常見的(技術上,**算術平均值**:即,將所有響應時間相加,然後除以請求數)。平均響應時間對於估計吞吐量限制很有用 [^18]。然而,如果你想知道你的"典型"響應時間,平均值不是一個很好的指標,因為它不會告訴你有多少使用者實際經歷了那種延遲。 +報告服務的 *平均* 響應時間是常見的(技術上是 *算術平均值*:即,將所有響應時間相加,然後除以請求數)。平均響應時間對於估計吞吐量限制很有用 [^18]。然而,如果你想知道你的"典型"響應時間,平均值不是一個很好的指標,因為它不能告訴你有多少使用者實際經歷了那種延遲。 -通常使用 **百分位數** 會更好。如果你將響應時間列表從最快到最慢排序,那麼 **中位數** 是中間點:例如,如果你的中位數響應時間是 200 毫秒,這意味著你的一半請求在不到 200 毫秒內返回,一半請求需要更長時間。這使中位數成為一個很好的指標,如果你想知道使用者通常需要等待多長時間。中位數也稱為 **第 50 百分位數**,有時縮寫為 **p50**。 +通常使用 *百分位數* 更好。如果你將響應時間列表從最快到最慢排序,那麼 *中位數* 就在中間:例如,如果你的中位響應時間是 200 毫秒,這意味著一半的請求在不到 200 毫秒內返回,一半的請求花費的時間更長。這使得中位數成為了解使用者通常需要等待多長時間的良好指標。中位數也稱為 *第 50 百分位*,有時縮寫為 *p50*。 -為了弄清楚你的異常值有多糟糕,你可以檢視更高的百分位數:**第 95**、**第 99** 和 **第 99.9** 百分位數是常見的(縮寫為 **p95**、**p99** 和 **p999**)。它們是響應時間閾值,95%、99% 或 99.9% 的請求比該特定閾值快。例如,如果第 95 百分位響應時間是 1.5 秒,這意味著 100 個請求中有 95 個需要不到 1.5 秒,100 個請求中有 5 個需要 1.5 秒或更長時間。這在 [圖 2-5](/tw/ch2#fig_lognormal) 中有所說明。 +為了弄清異常值有多糟糕,你可以檢視更高的百分位數:*第 95*、*99* 和 *99.9* 百分位數很常見(縮寫為 *p95*、*p99* 和 *p999*)。它們是 95%、99% 或 99.9% 的請求比該特定閾值快的響應時間閾值。例如,如果第 95 百分位響應時間是 1.5 秒,這意味著 100 個請求中的 95 個花費不到 1.5 秒,100 個請求中的 5 個花費 1.5 秒或更長時間。這在 [圖 2-5](/tw/ch2#fig_lognormal) 中有所說明。 -響應時間的高百分位數,也稱為 **尾延遲**,很重要,因為它們直接影響使用者對服務的體驗。例如,亞馬遜根據第 99.9 百分位數描述內部服務的響應時間要求,即使它隻影響 1,000 個請求中的 1 個。這是因為請求最慢的客戶通常是那些在其帳戶上擁有最多資料的客戶,因為他們進行了許多購買——也就是說,他們是最有價值的客戶 [^19]。透過確保網站對他們來說速度快,保持這些客戶滿意很重要。 +響應時間的高百分位數,也稱為 *尾部延遲*,很重要,因為它們直接影響使用者的服務體驗。例如,亞馬遜在描述內部服務的響應時間要求時使用第 99.9 百分位,即使它隻影響 1,000 個請求中的 1 個。這是因為請求最慢的客戶通常是那些賬戶上資料最多的客戶,因為他們進行了許多購買——也就是說,他們是最有價值的客戶 [^19]。確保網站對他們來說速度快對於保持這些客戶的滿意度很重要。 -另一方面,最佳化第 99.99 百分位數(10,000 個請求中最慢的 1 個)被認為對亞馬遜的目的來說太昂貴且收益不足。在非常高的百分位數下減少響應時間很困難,因為它們容易受到你無法控制的隨機事件的影響,而且收益遞減。 +另一方面,最佳化第 99.99 百分位(10,000 個請求中最慢的 1 個)被認為太昂貴,對亞馬遜的目的沒有足夠的好處。在非常高的百分位數上減少響應時間很困難,因為它們很容易受到你無法控制的隨機事件的影響,而且收益遞減。 -------- > [!TIP] 響應時間對使用者的影響 -直覺上似乎很明顯,快速服務比慢速服務對使用者更好 [^20]。然而,令人驚訝的是,很難獲得可靠的資料來量化延遲對使用者行為的影響。 +直覺上似乎很明顯,快速服務比慢速服務對使用者更好 [^20]。然而,要獲得可靠的資料來量化延遲對使用者行為的影響是令人驚訝地困難的。 -一些經常被引用的統計資料是不可靠的。2006 年,谷歌報告說,搜尋結果從 400 毫秒減慢到 900 毫秒與流量和收入下降 20% 相關 [^21]。然而,2009 年穀歌的另一項研究報告說,延遲增加 400 毫秒僅導致每天搜尋減少 0.6% [^22],同年必應發現載入時間增加兩秒使廣告收入減少 4.3% [^23]。來自這些公司的較新資料似乎沒有公開。 +一些經常被引用的統計資料是不可靠的。2006 年,谷歌報告說,搜尋結果從 400 毫秒減慢到 900 毫秒與流量和收入下降 20% 相關 [^21]。然而,2009 年穀歌的另一項研究報告說,延遲增加 400 毫秒導致每天搜尋減少僅 0.6% [^22],同年必應發現載入時間增加兩秒將廣告收入減少 4.3% [^23]。這些公司的較新資料似乎沒有公開。 -Akamai 最近的一項研究 [^24] 聲稱響應時間增加 100 毫秒使電子商務網站的轉化率降低多達 7%;然而,仔細檢查後,同一研究顯示,非常 **快** 的頁面載入時間也與較低的轉化率相關!這個看似矛盾的結果可以解釋為載入最快的頁面通常是那些沒有有用內容的頁面(例如,404 錯誤頁面)。然而,由於該研究沒有努力將頁面內容的影響與載入時間的影響分開,其結果可能沒有意義。 +Akamai 最近的一項研究 [^24] 聲稱響應時間增加 100 毫秒將電子商務網站的轉化率降低多達 7%;然而,仔細檢查後,同一研究顯示,非常 *快* 的頁面載入時間也與較低的轉化率相關!這個看似矛盾的結果是因為載入最快的頁面通常是那些沒有有用內容的頁面(例如,404 錯誤頁面)。然而,由於該研究沒有努力將頁面內容的影響與載入時間的影響分開,其結果可能沒有意義。 雅虎的一項研究 [^25] 比較了快速載入與慢速載入搜尋結果的點選率,控制了搜尋結果的質量。它發現當快速和慢速響應之間的差異為 1.25 秒或更多時,快速搜尋的點選次數增加 20-30%。 @@ -152,271 +153,294 @@ Akamai 最近的一項研究 [^24] 聲稱響應時間增加 100 毫秒使電子 ### 響應時間指標的應用 {#sec_introduction_slo_sla} -高百分位數在後端服務中尤其重要,這些服務在服務單個終端使用者請求時被多次呼叫。即使你並行進行呼叫,終端使用者請求仍然需要等待最慢的並行呼叫完成。只需要一個慢速呼叫就能使整個終端使用者請求變慢,如 [圖 2-6](/tw/ch2#fig_tail_amplification) 所示。即使只有一小部分後端呼叫很慢,如果終端使用者請求需要多個後端呼叫,獲得慢速呼叫的機會也會增加,因此更高比例的終端使用者請求最終會變慢(一種稱為 **尾延遲放大** 的效應 [^26])。 +高百分位數在被多次呼叫作為服務單個終端使用者請求的一部分的後端服務中尤其重要。即使你並行進行呼叫,終端使用者請求仍然需要等待最慢的並行呼叫完成。只需要一個慢呼叫就可以使整個終端使用者請求變慢,如 [圖 2-6](/tw/ch2#fig_tail_amplification) 所示。即使只有一小部分後端呼叫很慢,如果終端使用者請求需要多個後端呼叫,獲得慢呼叫的機會就會增加,因此更高比例的終端使用者請求最終會變慢(這種效應稱為 *尾部延遲放大* [^26])。 -{{< figure src="/fig/ddia_0206.png" id="fig_tail_amplification" caption="圖 2-6. 當需要幾個後端呼叫來服務請求時,只需要一個慢速後端請求就能減慢整個終端使用者請求。" class="w-full my-4" >}} +{{< figure src="/fig/ddia_0206.png" id="fig_tail_amplification" caption="圖 2-6. 當需要幾個後端呼叫來服務請求時,只需要一個慢的後端請求就可以減慢整個終端使用者請求。" class="w-full my-4" >}} -百分位數通常用於 **服務級別目標**(SLO)和 **服務級別協議**(SLA)中,作為定義服務預期效能和可用性的方式 [^27]。例如,SLO 可能為服務設定目標,使中位數響應時間小於 200 毫秒,第 99 百分位數低於 1 秒,並且至少 99.9% 的有效請求導致非錯誤響應。SLA 是一份合同,規定如果未滿足 SLO 會發生什麼(例如,客戶可能有權獲得退款)。至少這是基本思想;在實踐中,為 SLO 和 SLA 定義良好的可用性指標並不簡單 [^28] [^29]。 +百分位數通常用於 *服務級別目標*(SLO)和 *服務級別協議*(SLA),作為定義服務預期效能和可用性的方式 [^27]。例如,SLO 可能設定服務的中位響應時間小於 200 毫秒且第 99 百分位低於 1 秒的目標,以及至少 99.9% 的有效請求導致非錯誤響應的目標。SLA 是一份合同,規定如果不滿足 SLO 會發生什麼(例如,客戶可能有權獲得退款)。這至少是基本想法;實際上,為 SLO 和 SLA 定義良好的可用性指標並不簡單 [^28] [^29]。 -------- > [!TIP] 計算百分位數 -如果你想將響應時間百分位數新增到服務的監控儀表板中,你需要持續高效地計算它們。例如,你可能希望保留過去 10 分鐘內請求響應時間的滾動視窗。每分鐘,你計算該視窗中值的中位數和各種百分位數,並將這些指標繪製在圖表上。 +如果你想將響應時間百分位數新增到服務的監控儀表板中,你需要持續有效地計算它們。例如,你可能希望保留過去 10 分鐘內請求的響應時間的滾動視窗。每分鐘,你計算該視窗中值的中位數和各種百分位數,並在圖表上繪製這些指標。 -最簡單的實現是保留時間視窗內所有請求的響應時間列表,並每分鐘對該列表進行排序。如果這對你來說效率太低,有一些演算法可以以最小的 CPU 和記憶體成本計算百分位數的良好近似值。開源百分位數估計庫包括 HdrHistogram、t-digest [^30] [^31]、OpenHistogram [^32] 和 DDSketch [^33]。 +最簡單的實現是在時間視窗內保留所有請求的響應時間列表,並每分鐘對該列表進行排序。如果這對你來說效率太低,有一些演算法可以以最小的 CPU 和記憶體成本計算百分位數的良好近似值。開源百分位數估計庫包括 HdrHistogram、t-digest [^30] [^31]、OpenHistogram [^32] 和 DDSketch [^33]。 -請注意,對百分位數求平均值,例如,減少時間解析度或組合來自多臺機器的資料,在數學上是沒有意義的——聚合響應時間資料的正確方法是新增直方圖 [^34]。 +請注意,平均百分位數,例如,減少時間解析度或組合來自多臺機器的資料,在數學上是沒有意義的——聚合響應時間資料的正確方法是新增直方圖 [^34]。 -------- ## 可靠性與容錯 {#sec_introduction_reliability} -每個人都對什麼是可靠或不可靠有直觀的想法。對於軟體,典型的期望包括: +每個人都對某物是否可靠或不可靠有直觀的想法。對於軟體,典型的期望包括: * 應用程式執行使用者期望的功能。 -* 它可以容忍使用者犯錯誤或以意外方式使用軟體。 -* 它的效能對於所需的用例、預期的負載和資料量來說足夠好。 +* 它可以容忍使用者犯錯誤或以意想不到的方式使用軟體。 +* 在預期的負載和資料量下,其效能足以滿足所需的用例。 * 系統防止任何未經授權的訪問和濫用。 -如果所有這些事情加在一起意味著"正確工作",那麼我們可以將 **可靠性** 理解為大致意思是"即使出現問題也能繼續正確工作"。為了更準確地說明出現問題,我們將區分 **故障** 和 **失效** [^35] [^36] [^37]: +如果所有這些加在一起意味著"正確工作",那麼我們可以將 *可靠性* 大致理解為"即使出現問題也能繼續正確工作"。為了更準確地說明出現問題,我們將區分 *故障* 和 *失效* [^35] [^36] [^37]: 故障 -: 故障是系統的特定 **部分** 停止正確工作:例如,如果單個硬碟驅動器出現故障,或單臺機器崩潰,或系統依賴的外部服務出現中斷。 +: 故障是指系統的某個特定 *部分* 停止正確工作:例如,如果單個硬碟驅動器發生故障,或單臺機器崩潰,或外部服務(系統所依賴的)發生中斷。 失效 -: 失效是系統 **作為一個整體** 停止向用戶提供所需的服務;換句話說,當它不滿足服務級別目標(SLO)時。 +: 失效是指 *整個* 系統停止向用戶提供所需的服務;換句話說,當它不滿足服務級別目標(SLO)時。 -故障和失效之間的區別可能令人困惑,因為它們是同一件事,只是在不同的級別。例如,如果硬碟驅動器停止工作,我們說硬碟驅動器已經失效:如果系統僅包含那一個硬碟驅動器,它已經停止提供所需的服務。然而,如果你談論的系統包含許多硬碟驅動器,那麼從更大系統的角度來看,單個硬碟驅動器的失效只是一個故障,更大的系統可能能夠透過在另一個硬碟驅動器上擁有資料副本來容忍該故障。 +故障和失效之間的區別可能會令人困惑,因為它們在不同層面上是同一件事。例如,如果硬碟驅動器停止工作,我們說硬碟驅動器已失效:如果系統僅由該一個硬碟驅動器組成,它已停止提供所需的服務。然而,如果你正在談論的系統包含許多硬碟驅動器,那麼從更大系統的角度來看,單個硬碟驅動器的失效只是一個故障,並且更大的系統可能能夠透過在另一個硬碟驅動器上擁有資料副本來容忍該故障。 ### 容錯 {#id27} -如果系統儘管發生某些故障仍繼續向用戶提供所需的服務,我們稱該系統為 **容錯的**。如果系統無法容忍某個部分出現故障,我們稱該部分為 **單點故障**(SPOF),因為該部分的故障會升級導致整個系統的失效。 +如果系統在發生某些故障時仍繼續向用戶提供所需的服務,我們稱系統為 *容錯的*。如果系統不能容忍某個部分變得有故障,我們稱該部分為 *單點故障*(SPOF),因為該部分的故障會升級導致整個系統的失效。 -例如,在社交網路案例研究中,可能發生的故障是在扇出過程中,參與更新物化時間線的機器崩潰或變得不可用。為了使此過程容錯,我們需要確保另一臺機器可以接管此任務,而不會錯過任何應該投遞的帖子,也不會複製任何帖子。(這個想法被稱為 **精確一次語義**,我們將在 [Link to Come] 中詳細研究它。) +例如,在社交網路案例研究中,可能發生的故障是在扇出過程中,參與更新物化時間線的機器崩潰或變得不可用。為了使這個過程容錯,我們需要確保另一臺機器可以接管這項任務,而不會錯過任何應該交付的帖子,也不會複製任何帖子。(這個想法被稱為 *精確一次語義*,我們將在 [後續章節] 中詳細研究它。) -容錯總是限於一定數量的某些型別的故障。例如,系統可能能夠容忍最多兩個硬碟驅動器同時故障,或最多三個節點中的一個崩潰。容忍任何數量的故障是沒有意義的:如果所有節點都崩潰,就無能為力了。如果整個地球(及其上的所有伺服器)被黑洞吞噬,容忍該故障將需要太空網路託管——祝你獲得該預算專案的批准好運。 +容錯總是限於某些型別的某些數量的故障。例如,系統可能能夠容忍最多兩個硬碟驅動器同時故障,或最多三個節點中的一個崩潰。如果所有節點都崩潰,沒有什麼可以做的,這沒有意義容忍任何數量的故障。如果整個地球(及其上的所有伺服器)被黑洞吞噬,容忍該故障將需要在太空中進行網路託管——祝你獲得批准該預算專案的好運。 -違反直覺的是,在這種容錯系統中,透過故意觸發故障來 **增加** 故障率可能是有意義的——例如,透過在沒有警告的情況下隨機殺死單個程序。這稱為 **故障注入**。許多關鍵錯誤實際上是由於錯誤處理不當造成的 [^38];透過故意引發故障,你確保容錯機制得到持續鍛鍊和測試,這可以增加你對故障在自然發生時將被正確處理的信心。**混沌工程** 是一門學科,旨在透過故意注入故障等實驗來提高對容錯機制的信心 [^39]。 +反直覺地,在這種容錯系統中,透過故意觸發故障來 *增加* 故障率是有意義的——例如,在沒有警告的情況下隨機殺死單個程序。這稱為 *故障注入*。許多關鍵錯誤實際上是由於錯誤處理不當造成的 [^38];透過故意引發故障,你確保容錯機制不斷得到鍛鍊和測試,這可以增加你對故障自然發生時將被正確處理的信心。*混沌工程* 是一門旨在透過故意注入故障等實驗來提高對容錯機制的信心的學科 [^39]。 -儘管我們通常更喜歡容忍故障而不是預防故障,但有些情況下預防比治療更好(例如,因為不存在治療方法)。例如,安全事項就是這種情況:如果攻擊者已經入侵了系統並獲得了對敏感資料的訪問,該事件無法撤消。然而,本書主要處理可以治癒的故障型別,如以下各節所述。 +儘管我們通常更喜歡容忍故障而不是預防故障,但在預防比治療更好的情況下(例如,因為不存在治療方法)。安全問題就是這種情況:如果攻擊者已經破壞了系統並獲得了對敏感資料的訪問,該事件無法撤消。然而,本書主要涉及可以治癒的故障型別,如以下部分所述。 ### 硬體與軟體故障 {#sec_introduction_hardware_faults} -當我們考慮系統故障的原因時,硬體故障很快就會浮現在腦海中: +當我們想到系統失效的原因時,硬體故障很快就會浮現在腦海中: -* 大約 2-5% 的磁性硬碟驅動器每年故障 [^40] [^41];在擁有 10,000 個磁碟的儲存叢集中,我們因此應該預期平均每天有一個磁碟故障。最近的資料表明磁碟變得更可靠,但故障率仍然很高 [^42]。 -* 大約 0.5-1% 的固態驅動器(SSD)每年故障 [^43]。少量的位錯誤會自動糾正 [^44],但不可糾正的錯誤大約每年每個驅動器發生一次,即使在相當新的驅動器中(即,經歷過很少磨損);這個錯誤率高於磁性硬碟驅動器 [^45]、[^46]。 -* 其他硬體元件,如電源、RAID 控制器和記憶體模組也會故障,儘管頻率低於硬碟驅動器 [^47] [^48]。 -* 大約千分之一的機器有一個 CPU 核心偶爾會計算錯誤的結果,可能是由於製造缺陷 [^49] [^50] [^51]。在某些情況下,錯誤的計算會導致崩潰,但在其他情況下,它會導致程式簡單地返回錯誤的結果。 -* RAM 中的資料也可能被損壞,要麼是由於宇宙射線等隨機事件,要麼是由於永久性物理缺陷。即使使用帶有糾錯碼(ECC)的記憶體,超過 1% 的機器在給定年份中遇到不可糾正的錯誤,這通常會導致機器崩潰和受影響的記憶體模組需要更換 [^52]。此外,某些病理記憶體訪問模式可以高機率翻轉位 [^53]。 -* 整個資料中心可能變得不可用(例如,由於停電或網路配置錯誤)甚至被永久破壞(例如,因火災、洪水或地震 [^54])。太陽風暴在太陽噴射大量帶電粒子時在長距離電線中感應大電流,可能會損壞電網和海底網路電纜 [^55]。儘管這種大規模故障很少見,但如果服務無法容忍資料中心的丟失,它們的影響可能是災難性的 [^56]。 +* 大約 2-5% 的磁性硬碟驅動器每年發生故障 [^40] [^41];在擁有 10,000 個磁碟的儲存叢集中,我們因此應該期望平均每天有一個磁碟故障。最近的資料表明磁碟變得更可靠,但故障率仍然很顯著 [^42]。 +* 大約 0.5-1% 的固態硬碟(SSD)每年發生故障 [^43]。少量位錯誤會自動糾正 [^44],但不可糾正的錯誤大約每年每個驅動器發生一次,即使在相當新的驅動器中(即,經歷很少磨損);這個錯誤率高於磁性硬碟驅動器 [^45]、[^46]。 +* 其他硬體元件,如電源、RAID 控制器和記憶體模組也會發生故障,儘管頻率低於硬碟驅動器 [^47] [^48]。 +* 大約千分之一的機器有一個 CPU 核心偶爾計算錯誤的結果,可能是由於製造缺陷 [^49] [^50] [^51]。在某些情況下,錯誤的計算會導致崩潰,但在其他情況下,它會導致程式簡單地返回錯誤的結果。 +* RAM 中的資料也可能被損壞,要麼是由於宇宙射線等隨機事件,要麼是由於永久性物理缺陷。即使使用糾錯碼(ECC)的記憶體,超過 1% 的機器在給定年份遇到不可糾正的錯誤,這通常會導致機器崩潰和受影響的記憶體模組需要更換 [^52]。此外,某些病理記憶體訪問模式可以以高機率翻轉位 [^53]。 +* 整個資料中心可能變得不可用(例如,由於停電或網路配置錯誤)甚至被永久摧毀(例如,由火災、洪水或地震 [^54])。太陽風暴,當太陽噴射大量帶電粒子時,會在長距離電線中感應出大電流,可能會損壞電網和海底網路電纜 [^55]。儘管這種大規模故障很少見,但如果服務不能容忍資料中心的丟失,它們的影響可能是災難性的 [^56]。 -這些事件足夠罕見,以至於在處理小型系統時你通常不需要擔心它們,只要你可以輕鬆更換有故障的硬體。然而,在大規模系統中,硬體故障發生得足夠頻繁,以至於它們成為正常系統操作的一部分。 +這些事件足夠罕見,你在處理小型系統時通常不需要擔心它們,只要你可以輕鬆更換變得有故障的硬體。然而,在大規模系統中,硬體故障發生得足夠頻繁,以至於它們成為正常系統執行的一部分。 #### 透過冗餘容忍硬體故障 {#tolerating-hardware-faults-through-redundancy} -我們對不可靠硬體的第一反應通常是向單個硬體元件新增冗餘,以降低系統的故障率。磁碟可以設定為 RAID 配置(將資料分佈在同一臺機器中的多個磁碟上,以便故障磁碟不會導致資料丟失),伺服器可能有雙電源和可熱插拔的 CPU,資料中心可能有電池和柴油發電機作為備用電源。這種冗餘通常可以使機器無中斷執行多年。 +我們對不可靠硬體的第一反應通常是向各個硬體元件新增冗餘,以降低系統的故障率。磁碟可以設定為 RAID 配置(將資料分佈在同一臺機器的多個磁碟上,以便故障磁碟不會導致資料丟失),伺服器可能有雙電源和可熱插拔的 CPU,資料中心可能有電池和柴油發電機作為備用電源。這種冗餘通常可以使機器不間斷執行多年。 -當元件故障是獨立的時,冗餘最有效,即一個故障的發生不會改變另一個故障發生的可能性。然而,經驗表明,元件故障之間通常存在顯著相關性 [^41] [^57] [^58];整個伺服器機架或整個資料中心的不可用仍然比我們希望的更頻繁。 +當元件故障獨立時,冗餘最有效,即一個故障的發生不會改變另一個故障發生的可能性。然而,經驗表明,元件故障之間通常存在顯著的相關性 [^41] [^57] [^58];整個伺服器機架或整個資料中心的不可用仍然比我們希望的更頻繁地發生。 -硬體冗餘增加了單臺機器的正常執行時間;然而,如 ["分散式與單節點系統"](/tw/ch1#sec_introduction_distributed) 中所討論的,使用分散式系統有一些優勢,例如能夠容忍一個數據中心的完全中斷。出於這個原因,雲系統傾向於較少關注單個機器的可靠性,而是旨在透過在軟體級別容忍故障節點來使服務高度可用。雲提供商使用 **可用區** 來識別哪些資源在物理上位於同一位置;同一地方的資源比地理上分離的資源更有可能同時故障。 +硬體冗餘增加了單臺機器的正常執行時間;然而,如 ["分散式與單節點系統"](/tw/ch1#sec_introduction_distributed) 中所討論的,使用分散式系統有一些優勢,例如能夠容忍一個數據中心的完全中斷。出於這個原因,雲系統傾向於較少關注單個機器的可靠性,而是旨在透過在軟體級別容忍故障節點來使服務高度可用。雲提供商使用 *可用區* 來識別哪些資源在物理上位於同一位置;同一地方的資源比地理上分離的資源更可能同時發生故障。 -我們在本書中討論的容錯技術旨在容忍整個機器、機架或可用區的丟失。它們通常透過允許一個數據中心的機器在另一個數據中心的機器故障或變得無法訪問時接管來工作。我們將在 [第 6 章](/tw/ch6#ch_replication)、[第 10 章](/tw/ch10#ch_consistency) 以及本書的其他各個點討論這種容錯技術。 +我們在本書中討論的容錯技術旨在容忍整個機器、機架或可用區的丟失。它們通常透過允許一個數據中心的機器在另一個數據中心的機器發生故障或變得不可達時接管來工作。我們將在 [第 6 章](/tw/ch6)、[第 10 章](/tw/ch10) 以及本書的其他各個地方討論這種容錯技術。 -可以容忍整個機器丟失的系統也具有操作優勢:如果你需要重新啟動機器(例如,應用作業系統安全補丁),單伺服器系統需要計劃停機時間,而多節點容錯系統可以透過一次重新啟動一個節點來打補丁,而不會影響使用者的服務。這稱為 **滾動升級**,我們將在 [第 5 章](/tw/ch5#ch_encoding) 中進一步討論它。 +能夠容忍整個機器丟失的系統也具有運營優勢:如果你需要重新啟動機器(例如,應用作業系統安全補丁),單伺服器系統需要計劃停機時間,而多節點容錯系統可以一次修補一個節點,而不影響使用者的服務。這稱為 *滾動升級*,我們將在 [第 5 章](/tw/ch5) 中進一步討論它。 #### 軟體故障 {#software-faults} -儘管硬體故障可能是弱相關的,但它們仍然大多是獨立的:例如,如果一個磁碟故障,同一臺機器中的其他磁碟很可能在一段時間內仍然正常。另一方面,軟體故障通常高度相關,因為許多節點執行相同的軟體並因此具有相同的錯誤是很常見的 [^59] [^60]。這種故障更難預測,它們往往比不相關的硬體故障導致更多的系統故障 [^47]。例如: +儘管硬體故障可能是弱相關的,但它們大多仍然是獨立的:例如,如果一個磁碟發生故障,同一臺機器中的其他磁碟很可能在一段時間內還能正常工作。另一方面,軟體故障通常高度相關,因為許多節點執行相同的軟體並因此具有相同的錯誤是常見的 [^59] [^60]。這種故障比不相關的硬體故障更難預料,並且它們往往導致比硬體故障更多的系統失效 [^47]。例如: -* 導致每個節點在特定情況下同時故障的軟體錯誤。例如,2012 年 6 月 30 日,閏秒導致許多 Java 應用程式由於 Linux 核心中的錯誤而同時掛起,導致許多網際網路服務癱瘓 [^61]。由於韌體錯誤,某些型號的所有 SSD 在精確執行 32,768 小時(不到 4 年)後突然故障,使其上的資料無法恢復 [^62]。 -* 使用某些共享、有限資源的失控程序,例如 CPU 時間、記憶體、磁碟空間、網路頻寬或執行緒 [^63]。例如,在處理大型請求時消耗過多記憶體的程序可能會被作業系統殺死。客戶端庫中的錯誤可能導致比預期更高的請求量 [^64]。 -* 系統依賴的服務變慢、無響應或開始返回損壞的響應。 -* 不同系統之間的互動導致在每個系統單獨測試時不會發生的緊急行為 [^65]。 -* 級聯故障,其中一個元件中的問題導致另一個元件過載並減速,這反過來又導致另一個元件癱瘓 [^66] [^67]。 +* 在特定情況下導致每個節點同時失效的軟體錯誤。例如,2012 年 6 月 30 日,閏秒導致許多 Java 應用程式由於 Linux 核心中的錯誤而同時掛起 [^61]。由於韌體錯誤,某些型號的所有 SSD 在精確執行 32,768 小時(不到 4 年)後突然失效,使其上的資料無法恢復 [^62]。 +* 使用某些共享、有限資源(如 CPU 時間、記憶體、磁碟空間、網路頻寬或執行緒)的失控程序 [^63]。例如,處理大請求時消耗過多記憶體的程序可能會被作業系統殺死。客戶端庫中的錯誤可能導致比預期更高的請求量 [^64]。 +* 系統所依賴的服務變慢、無響應或開始返回損壞的響應。 +* 不同系統之間的互動導致在隔離測試每個系統時不會發生的緊急行為 [^65]。 +* 級聯故障,其中一個元件中的問題導致另一個元件過載和減速,這反過來又導致另一個元件崩潰 [^66] [^67]。 -導致這些軟體故障的錯誤通常會潛伏很長時間,直到它們被一組不尋常的情況觸發。在這些情況下,會發現軟體對其環境做出了某種假設——雖然該假設通常是正確的,但由於某種原因它最終不再正確 [^68] [^69]。 +導致這些型別軟體故障的錯誤通常會潛伏很長時間,直到它們被一組不尋常的環境觸發。在這些情況下,軟體對其環境做出了某種假設——雖然該假設通常是正確的,但它最終由於某種原因不再成立 [^68] [^69]。 -軟體中系統性故障的問題沒有快速解決方案。許多小事情可以幫助:仔細考慮系統中的假設和互動;徹底測試;程序隔離;允許程序崩潰並重新啟動;避免反饋迴圈,如重試風暴(參見 ["當過載系統無法恢復時"](/tw/ch2#sidebar_metastable));測量、監控和分析生產中的系統行為。 +軟體中的系統故障沒有快速解決方案。許多小事情可以幫助:仔細考慮系統中的假設和互動;徹底測試;程序隔離;允許程序崩潰和重新啟動;避免反饋迴圈,如重試風暴(參見 ["當過載系統無法恢復時"](/tw/ch2#sidebar_metastable));測量、監控和分析生產中的系統行為。 ### 人類與可靠性 {#id31} -人類設計和構建軟體系統,保持系統執行的操作員也是人類。與機器不同,人類不只是遵循規則;他們的力量在於在完成工作時具有創造性和適應性。然而,這一特性也導致了不可預測性,有時儘管有最好的意圖,但會犯錯誤,導致故障。例如,一項對大型網際網路服務的研究發現,操作員的配置更改是中斷的主要原因,而硬體故障(伺服器或網路)僅在 10-25% 的中斷中發揮作用 [^70]。 +人類設計和構建軟體系統,保持系統執行的操作員也是人類。與機器不同,人類不只是遵循規則;他們的力量是創造性和適應性地完成工作。然而,這一特徵也導致不可預測性,有時會導致失效的錯誤,儘管有最好的意圖。例如,一項對大型網際網路服務的研究發現,操作員的配置更改是中斷的主要原因,而硬體故障(伺服器或網路)僅在 10-25% 的中斷中發揮作用 [^70]。 -將這些問題標記為"人為錯誤"並希望透過更嚴格的程式和規則合規來更好地控制人類行為來解決它們是很誘人的。然而,為錯誤責怪人們是適得其反的。我們所說的"人為錯誤"實際上不是事件的原因,而是人們盡力完成工作的社會技術系統中問題的症狀 [^71]。通常,複雜系統具有緊急行為,其中元件之間的意外互動也可能導致故障 [^72]。 +將這些問題標記為"人為錯誤"並希望透過更嚴格的程式和規則合規性來更好地控制人類行為來解決它們是很誘人的。然而,責怪人們的錯誤是適得其反的。我們所說的"人為錯誤"實際上不是事件的原因,而是人們在社會技術系統中盡力做好工作的問題的症狀 [^71]。通常,複雜系統具有緊急行為,元件之間的意外互動也可能導致故障 [^72]。 -各種技術措施可以幫助最小化人為錯誤的影響,包括徹底測試(手寫測試和對大量隨機輸入的 **屬性測試**)[^38]、用於快速恢復配置更改的回滾機制、新程式碼的逐步推出、詳細而清晰的監控、用於診斷生產問題的可觀察性工具(參見 ["分散式系統的問題"](/tw/ch1#sec_introduction_dist_sys_problems)),以及設計良好的介面,鼓勵"正確的事情"並阻止"錯誤的事情"。 +各種技術措施可以幫助最小化人為錯誤的影響,包括徹底測試(手寫測試和對大量隨機輸入的 *屬性測試*)[^38]、快速回滾配置更改的回滾機制、新程式碼的逐步推出、詳細和清晰的監控、用於診斷生產問題的可觀測性工具(參見 ["分散式系統的問題"](/tw/ch1#sec_introduction_dist_sys_problems)),以及鼓勵"正確的事情"並阻止"錯誤的事情"的精心設計的介面。 -然而,這些事情需要時間和金錢的投資,在日常業務的實用現實中,組織通常優先考慮創收活動,而不是增加其對錯誤的彈性的措施。如果在更多功能和更多測試之間有選擇,許多組織可以理解地選擇功能。鑑於這種選擇,當不可避免地發生可預防的錯誤時,責怪犯錯誤的人是沒有意義的——問題是組織的優先事項。 +然而,這些事情需要時間和金錢的投資,在日常業務的務實現實中,組織通常優先考慮創收活動而不是增加其抵禦錯誤的韌性的措施。如果在更多功能和更多測試之間有選擇,許多組織可以理解地選擇功能。鑑於這種選擇,當可預防的錯誤不可避免地發生時,責怪犯錯誤的人是沒有意義的——問題是組織的優先事項。 -越來越多的組織正在採用 **無責備事後分析** 的文化:在事件發生後,鼓勵參與的人員分享關於發生的事情的全部細節,而不必擔心受到懲罰,因為這使組織中的其他人能夠學習如何在未來防止類似的問題 [^73]。這個過程可能會發現需要改變業務優先順序、需要投資被忽視的領域、需要改變相關人員的激勵措施,或者需要引起管理層注意的其他系統性問題。 +越來越多的組織正在採用 *無責備事後分析* 的文化:事件發生後,鼓勵相關人員充分分享發生的事情的細節,而不用擔心懲罰,因為這允許組織中的其他人學習如何在未來防止類似的問題 [^73]。這個過程可能會發現需要改變業務優先順序、需要投資於被忽視的領域、需要改變相關人員的激勵措施,或者需要引起管理層注意的其他一些系統性問題。 -作為一般原則,在調查事件時,你應該對簡單的答案持懷疑態度。"鮑勃在部署該更改時應該更加小心"是沒有成效的,但"我們必須用 Haskell 重寫後端"也不是。相反,管理層應該藉此機會從每天使用它的人的角度瞭解社會技術系統如何工作的細節,並根據這些反饋採取措施改進它 [^71]。 +作為一般原則,在調查事件時,你應該對簡單化的答案持懷疑態度。"鮑勃在部署該更改時應該更加小心"是沒有成效的,但"我們必須用 Haskell 重寫後端"也不是。相反,管理層應該藉此機會從每天與之合作的人的角度瞭解社會技術系統如何工作的細節,並根據這些反饋採取措施改進它 [^71]。 -------- > [!TIP] 可靠性有多重要? -可靠性不僅適用於核電站和空中交通管制——更平凡的應用程式也應該可靠地工作。業務應用程式中的錯誤會導致生產力損失(如果數字報告不正確,還會帶來法律風險),電子商務網站的中斷可能會在收入損失和聲譽損害方面造成巨大成本。 +可靠性不僅僅適用於核電站和空中交通管制——更平凡的應用程式也應該可靠地工作。業務應用程式中的錯誤會導致生產力損失(如果數字報告不正確,還會有法律風險),電子商務網站的中斷可能會在收入和聲譽損害方面造成巨大成本。 -在許多應用程式中,幾分鐘甚至幾小時的臨時中斷是可以容忍的 [^74],但永久的資料丟失或損壞將是災難性的。考慮一位父母,他將所有孩子的照片和影片儲存在你的照片應用程式中 [^75]。如果該資料庫突然損壞,他們會有什麼感覺?他們會知道如何從備份中恢復嗎? +在許多應用程式中,幾分鐘甚至幾小時的臨時中斷是可以容忍的 [^74],但永久資料丟失或損壞將是災難性的。考慮一位家長在你的照片應用程式中儲存他們孩子的所有照片和影片 [^75]。如果該資料庫突然損壞,他們會有什麼感覺?他們會知道如何從備份中恢復嗎? -作為不可靠軟體如何傷害人們的另一個例子,考慮郵局地平線醜聞。1999 年至 2019 年間,數百名管理英國郵局分支機構的人因盜竊或欺詐而被定罪,因為會計軟體顯示他們的賬戶有短缺。最終,很明顯,這些短缺中的許多是由於軟體中的錯誤造成的,許多定罪此後已被推翻 [^76]。導致這一可能是英國曆史上最大的司法不公的是,英國法律假定計算機正確執行(因此,計算機產生的證據是可靠的),除非有相反的證據 [^77]。軟體工程師可能會嘲笑軟體可能永遠沒有錯誤的想法,但這對那些因不可靠的計算機系統而被錯誤監禁、宣佈破產,甚至因錯誤定罪而自殺的人來說是很少的安慰。 +作為不可靠軟體如何傷害人們的另一個例子,考慮郵局地平線醜聞。在 1999 年至 2019 年期間,管理英國郵局分支機構的數百人因會計軟體顯示其賬戶短缺而被判盜竊或欺詐罪。最終變得清楚,許多這些短缺是由於軟體中的錯誤,許多定罪已被推翻 [^76]。導致這一可能是英國曆史上最大的司法不公的是,英國法律假設計算機正確執行(因此,計算機產生的證據是可靠的),除非有相反的證據 [^77]。軟體工程師可能會嘲笑軟體可能無錯誤的想法,但這對那些因不可靠的計算機系統而被錯誤監禁、宣佈破產甚至自殺的人來說,這是很少的安慰。 -在某些情況下,我們可能會選擇犧牲可靠性以降低開發成本(例如,在為未經證實的市場開發原型產品時)——但我們應該非常清楚何時在偷工減料,並牢記潛在的後果。 +在某些情況下,我們可能選擇犧牲可靠性以降低開發成本(例如,在為未經證實的市場開發原型產品時)——但我們應該非常清楚何時走捷徑並牢記潛在的後果。 -------- ## 可伸縮性 {#sec_introduction_scalability} -即使系統今天可靠地工作,這並不意味著它將來一定會可靠地工作。降級的一個常見原因是負載增加:也許系統已經從 10,000 個併發使用者增長到 100,000 個併發使用者,或者從 100 萬增長到 1000 萬。也許它正在處理比以前大得多的資料量。 +即使系統今天可靠地工作,這並不意味著它將來必然會可靠地工作。降級的一個常見原因是負載增加:也許系統已經從 10,000 個併發使用者增長到 100,000 個併發使用者,或者從 100 萬增長到 1000 萬。也許它正在處理比以前大得多的資料量。 -**可擴充套件性** 是我們用來描述系統處理負載增加的能力的術語。有時,在討論可擴充套件性時,人們會發表類似"你不是谷歌或亞馬遜。停止擔心規模,只使用關係資料庫"的評論。這個格言是否適用於你取決於你正在構建的應用程式型別。 +*可伸縮性* 是我們用來描述系統應對負載增加能力的術語。有時,在討論可伸縮性時,人們會發表評論,如"你不是谷歌或亞馬遜。停止擔心規模,只使用關係資料庫。"這個格言是否適用於你取決於你正在構建的應用程式型別。 -如果你正在構建一個目前只有少數使用者的新產品,也許在一家初創公司,那麼首要的工程目標通常是保持系統儘可能簡單和靈活,以便你可以輕鬆修改和調整產品的功能,因為你瞭解更多關於客戶需求的資訊 [^78]。在這樣的環境中,擔心未來可能需要的假設規模是適得其反的:在最好的情況下,對可擴充套件性的投資是浪費的努力和過早的最佳化;在最壞的情況下,它們會將你鎖定在不靈活的設計中,並使你更難發展你的應用程式。 +如果你正在構建一個目前只有少數使用者的新產品,也許是在初創公司,首要的工程目標通常是保持系統儘可能簡單和靈活,以便你可以在瞭解更多關於客戶需求時輕鬆修改和調整產品的功能 [^78]。在這種環境中,擔心未來可能需要的假設規模是適得其反的:在最好的情況下,對可伸縮性的投資是浪費的努力和過早的最佳化;在最壞的情況下,它們會將你鎖定在不靈活的設計中,並使你的應用程式更難發展。 -原因是可擴充套件性不是一維標籤:說"X 是可擴充套件的"或"Y 不擴充套件"是沒有意義的。相反,討論可擴充套件性意味著考慮以下問題: +原因是可伸縮性不是一維標籤:說"X 是可伸縮的"或"Y 不伸縮"是沒有意義的。相反,討論可伸縮性意味著考慮諸如以下問題: -* "如果系統以特定方式增長,我們有哪些選擇來應對增長?" +* "如果系統以特定方式增長,我們有什麼選擇來應對增長?" * "我們如何新增計算資源來處理額外的負載?" -* "根據當前的增長預測,我們何時會達到當前架構的極限?" +* "基於當前的增長預測,我們何時會達到當前架構的極限?" -如果你成功地使你的應用程式流行,因此處理越來越多的負載,你將瞭解你的效能瓶頸在哪裡,因此你將知道你需要在哪些維度上擴充套件。那時是開始擔心可擴充套件性技術的時候了。 +如果你成功地使你的應用程式受歡迎,因此處理越來越多的負載,你將瞭解你的效能瓶頸在哪裡,因此你將知道需要沿著哪些維度進行伸縮。那時是開始擔心可伸縮性技術的時候。 ### 描述負載 {#id33} -首先,我們需要簡潔地描述系統上的當前負載;只有這樣我們才能討論增長問題(如果我們的負載翻倍會發生什麼?)。通常這將是吞吐量的度量:例如,服務的每秒請求數、每天到達的新資料的千兆位元組數,或每小時購物車結賬的數量。有時你關心某些可變數量的峰值,例如 ["案例研究:社交網路主頁時間線"](/tw/ch2#sec_introduction_twitter) 中同時線上使用者的數量。 +首先,我們需要簡潔地描述系統上的當前負載;只有這樣我們才能討論增長問題(如果我們的負載翻倍會發生什麼?)。通常這將是吞吐量的度量:例如,對服務的每秒請求數、每天到達多少千兆位元組的新資料,或每小時購物車結賬的數量。有時你關心某個變數數量的峰值,例如 ["案例研究:社交網路首頁時間線"](/tw/ch2#sec_introduction_twitter) 中同時線上使用者的數量。 -通常,負載的其他統計特徵也會影響訪問模式,從而影響可擴充套件性要求。例如,你可能需要知道資料庫中讀取與寫入的比率、快取的命中率或每個使用者的資料項數(例如,社交網路案例研究中的粉絲數)。也許平均情況對你很重要,或者你的瓶頸可能由少數極端情況主導。這一切都取決於你特定應用程式的細節。 +通常還有其他影響訪問模式並因此影響可伸縮性要求的負載統計特徵。例如,你可能需要知道資料庫中的讀寫比率、快取的命中率或每個使用者的資料項數量(例如,社交網路案例研究中的粉絲數量)。也許平均情況對你很重要,或者也許你的瓶頸由少數極端情況主導。這一切都取決於你特定應用程式的細節。 -一旦你描述了系統上的負載,你就可以研究負載增加時會發生什麼。你可以從兩個方面來看: +一旦你描述了系統上的負載,你就可以調查當負載增加時會發生什麼。你可以從兩個方面來看待它: -* 當你以某種方式增加負載並保持系統資源(CPU、記憶體、網路頻寬等)不變時,你的系統性能如何受到影響? +* 當你以某種方式增加負載並保持系統資源(CPU、記憶體、網路頻寬等)不變時,系統的效能如何受到影響? * 當你以某種方式增加負載時,如果你想保持效能不變,你需要增加多少資源? -通常我們的目標是在滿足 SLA 要求的同時保持系統的效能(參見 ["響應時間指標的使用"](/tw/ch2#sec_introduction_slo_sla)),同時最小化執行系統的成本。所需的計算資源越多,成本就越高。可能某些型別的硬體比其他型別更具成本效益,隨著新型硬體的出現,這些因素可能會隨著時間而改變。 +通常我們的目標是在最小化執行系統成本的同時保持系統性能在 SLA 的要求範圍內(參見 ["響應時間指標的應用"](/tw/ch2#sec_introduction_slo_sla))。所需的計算資源越多,成本就越高。可能某些型別的硬體比其他型別更具成本效益,這些因素可能會隨著新型別硬體的出現而隨時間變化。 -如果你可以加倍資源以處理兩倍的負載,同時保持效能相同,我們說你有 **線性可擴充套件性**,這被認為是一件好事。偶爾可以用少於兩倍的資源處理兩倍的負載,這是由於規模經濟或峰值負載的更好分佈 [^79] [^80]。更有可能的是,成本增長速度快於線性,效率低下可能有很多原因。例如,如果你有大量資料,那麼處理單個寫入請求可能涉及比你有少量資料時更多的工作,即使請求的大小相同。 +如果你可以將資源翻倍以處理兩倍的負載,同時保持效能不變,我們說你有 *線性可伸縮性*,這被認為是好事。偶爾,由於規模經濟或峰值負載的更好分佈,可以用不到兩倍的資源處理兩倍的負載 [^79] [^80]。更可能的是,成本增長速度快於線性,並且效率低下可能有許多原因。例如,如果你有大量資料,那麼處理單個寫請求可能涉及比你有少量資料時更多的工作,即使請求的大小相同。 ### 共享記憶體、共享磁碟與無共享架構 {#sec_introduction_shared_nothing} -增加服務硬體資源的最簡單方法是將其移動到更強大的機器。單個 CPU 核心不再變得明顯更快,但你可以購買(或租用雲實例)具有更多 CPU 核心、更多 RAM 和更多磁碟空間的機器。這種方法稱為 **垂直擴充套件** 或 **縱向擴充套件**。 +增加服務硬體資源的最簡單方法是將其移動到更強大的機器。單個 CPU 核心不再變得顯著更快,但你可以購買一臺機器(或租用雲實例)具有更多 CPU 核心、更多 RAM 和更多磁碟空間。這種方法稱為 *縱向伸縮* 或 *向上擴充套件*。 -你可以透過使用多個程序或執行緒在單臺機器上獲得並行性。屬於同一程序的所有執行緒都可以訪問相同的 RAM,因此這種方法也稱為 **共享記憶體架構**。共享記憶體方法的問題是成本增長速度快於線性:具有兩倍硬體資源的高階機器通常成本遠遠超過兩倍。由於瓶頸,兩倍大小的機器通常可以處理不到兩倍的負載。 +你可以透過使用多個程序或執行緒在單臺機器上獲得並行性。屬於同一程序的所有執行緒都可以訪問相同的 RAM,因此這種方法也稱為 *共享記憶體架構*。共享記憶體方法的問題是成本增長速度快於線性:具有兩倍硬體資源的高階機器通常成本遠遠超過兩倍。由於瓶頸,兩倍大小的機器通常可以處理不到兩倍的負載。 -另一種方法是 **共享磁碟架構**,它使用具有獨立 CPU 和 RAM 的幾臺機器,但將資料儲存在機器之間共享的磁碟陣列上,它們透過快速網路連線:**網路附加儲存**(NAS)或 **儲存區域網路**(SAN)。這種架構傳統上用於本地資料倉庫工作負載,但爭用和鎖定的開銷限制了共享磁碟方法的可擴充套件性 [^81]。 +另一種方法是 *共享磁碟架構*,它使用幾臺具有獨立 CPU 和 RAM 的機器,但將資料儲存在機器之間共享的磁碟陣列上,這些機器透過快速網路連線:*網路附加儲存*(NAS)或 *儲存區域網路*(SAN)。這種架構傳統上用於本地資料倉庫工作負載,但爭用和鎖定的開銷限制了共享磁碟方法的可伸縮性 [^81]。 -相比之下,**無共享架構** [^82](也稱為 **水平擴充套件** 或 **橫向擴充套件**)已經獲得了很大的普及。在這種方法中,我們使用具有多個節點的分散式系統,每個節點都有自己的 CPU、RAM 和磁碟。節點之間的任何協調都在軟體級別透過傳統網路完成。 +相比之下,*無共享架構* [^82](也稱為 *橫向伸縮* 或 *向外擴充套件*)已經獲得了很大的流行。在這種方法中,我們使用具有多個節點的分散式系統,每個節點都有自己的 CPU、RAM 和磁碟。節點之間的任何協調都在軟體級別透過傳統網路完成。 -無共享的優點是它具有線性擴充套件的潛力,它可以使用提供最佳價效比的任何硬體(特別是在雲中),它可以在負載增加或減少時更輕鬆地調整其硬體資源,並且它可以透過在多個數據中心和區域分佈系統來實現更高的容錯性。缺點是它需要顯式分片(參見 [第 7 章](/tw/ch7#ch_sharding)),並且會產生分散式系統的所有複雜性([第 9 章](/tw/ch9#ch_distributed))。 +無共享的優點是它有線性伸縮的潛力,它可以使用提供最佳價效比的任何硬體(特別是在雲中),它可以隨著負載的增加或減少更容易地調整其硬體資源,並且它可以透過在多個數據中心和地區分佈系統來實現更大的容錯。缺點是它需要顯式分片(參見 [第 7 章](/tw/ch7)),並且它會產生分散式系統的所有複雜性([第 9 章](/tw/ch9))。 -一些雲原生資料庫系統使用單獨的服務進行儲存和事務執行(參見 ["儲存和計算分離"](/tw/ch1#sec_introduction_storage_compute)),多個計算節點共享對同一儲存服務的訪問。該模型與共享磁碟架構有一些相似之處,但它避免了舊系統的可擴充套件性問題:它不提供檔案系統(NAS)或塊裝置(SAN)抽象,而是提供專門為資料庫的特定需求設計的專用 API [^83]。 +一些雲原生資料庫系統為儲存和事務執行使用單獨的服務(參見 ["儲存與計算分離"](/tw/ch1#sec_introduction_storage_compute)),多個計算節點共享對同一儲存服務的訪問。這個模型與共享磁碟架構有一些相似之處,但它避免了舊系統的可伸縮性問題:它不是提供檔案系統(NAS)或塊裝置(SAN)抽象,而是儲存服務提供專門為資料庫特定需求設計的 API [^83]。 ### 可伸縮性原則 {#id35} -大規模執行的系統架構通常高度特定於應用程式——沒有通用的、一刀切的可擴充套件架構(非正式地稱為 **萬金油**)。例如,設計用於處理每秒 100,000 個請求(每個 1 kB 大小)的系統與設計用於每分鐘 3 個請求(每個 2 GB 大小)的系統看起來非常不同——即使兩個系統具有相同的資料吞吐量(100 MB/秒)。 +在大規模執行的系統架構通常對應用程式高度特定——沒有通用的、一刀切的可伸縮架構(非正式地稱為 *萬金油*)。例如,設計用於處理每秒 100,000 個請求(每個 1 kB 大小)的系統與設計用於每分鐘 3 個請求(每個 2 GB 大小)的系統看起來非常不同——即使兩個系統具有相同的資料吞吐量(100 MB/秒)。 -此外,適合一個負載級別的架構不太可能應對 10 倍的負載。如果你正在處理快速增長的服務,因此你可能需要在每個數量級負載增加時重新考慮你的架構。由於應用程式的需求可能會發展,通常不值得提前規劃未來的擴充套件需求超過一個數量級。 +此外,適合一個負載級別的架構不太可能應對 10 倍的負載。如果你正在開發快速增長的服務,因此很可能你需要在每個數量級的負載增加時重新考慮你的架構。由於應用程式的需求可能會演變,通常不值得提前規劃超過一個數量級的未來伸縮需求。 -可擴充套件性的一個良好通用原則是將系統分解為可以在很大程度上相互獨立執行的較小元件。這是微服務背後的基本原則(參見 ["微服務和無伺服器"](/tw/ch1#sec_introduction_microservices))、分片([第 7 章](/tw/ch7#ch_sharding))、流處理([Link to Come])和無共享架構。然而,挑戰在於知道在哪裡劃定應該在一起的事物和應該分開的事物之間的界限。微服務的設計指南可以在其他書籍中找到 [^84],我們在 [第 7 章](/tw/ch7#ch_sharding) 中討論無共享系統的分片。 +可伸縮性的一個良好通用原則是將系統分解為可以在很大程度上相互獨立執行的較小元件。這是微服務背後的基本原則(參見 ["微服務與無伺服器"](/tw/ch1#sec_introduction_microservices))、分片([第 7 章](/tw/ch7))、流處理([後續章節])和無共享架構。然而,挑戰在於知道在哪裡劃分應該在一起的事物和應該分開的事物之間的界限。微服務的設計指南可以在其他書籍中找到 [^84],我們在 [第 7 章](/tw/ch7) 中討論無共享系統的分片。 -另一個好原則是不要讓事情變得比必要的更複雜。如果單機資料庫可以完成工作,它可能比複雜的分散式設定更可取。自動擴充套件系統(根據需求自動新增或刪除資源)很酷,但如果你的負載相當可預測,手動擴充套件的系統可能會有更少的操作意外(參見 ["操作:自動或手動再平衡"](/tw/ch7#sec_sharding_operations))。具有五個服務的系統比具有五十個服務的系統更簡單。良好的架構通常涉及方法的實用混合。 +另一個好原則是不要讓事情變得比必要的更複雜。如果單機資料庫可以完成工作,它可能比複雜的分散式設定更可取。自動伸縮系統(根據需求自動新增或刪除資源)很酷,但如果你的負載相當可預測,手動伸縮的系統可能會有更少的操作意外(參見 ["操作:自動或手動再平衡"](/tw/ch7#sec_sharding_operations))。具有五個服務的系統比具有五十個服務的系統更簡單。良好的架構通常涉及方法的務實混合。 ## 可運維性 {#sec_introduction_maintainability} -軟體不會磨損或遭受材料疲勞,因此它不會像機械物體那樣破壞。但是應用程式的需求經常變化,軟體執行的環境發生變化(例如其依賴項和底層平臺),並且它有需要修復的錯誤。 +軟體不會磨損或遭受材料疲勞,因此它不會像機械物體那樣以同樣的方式損壞。但應用程式的要求經常變化,軟體執行的環境發生變化(例如其依賴項和底層平臺),並且它有需要修復的錯誤。 -人們普遍認識到,軟體的大部分成本不在於其初始開發,而在於其持續維護——修復錯誤、保持系統執行、調查故障、使其適應新平臺、為新用例修改它、償還技術債務以及新增新功能 [^85] [^86]。 +人們普遍認為,軟體的大部分成本不在其初始開發中,而在其持續維護中——修復錯誤、保持其系統執行、調查故障、將其適應新平臺、為新用例修改它、償還技術債務和新增新功能 [^85] [^86]。 -然而,維護也很困難。如果系統已成功執行很長時間,它可能會使用今天很少有工程師理解的過時技術(例如大型機和 COBOL 程式碼);關於系統如何以及為什麼以某種方式設計的機構知識可能會隨著人們離開組織而丟失;可能有必要修復其他人的錯誤。此外,計算機系統通常與它支援的人類組織交織在一起,這意味著這種 **遺留** 系統的維護既是人的問題,也是技術問題 [^87]。 +然而,維護也很困難。如果系統已成功執行很長時間,它可能使用今天不多工程師理解的過時技術(如大型機和 COBOL 程式碼);關於系統如何以及為何以某種方式設計的機構知識可能已經隨著人們離開組織而丟失;可能需要修復其他人的錯誤。此外,計算機系統通常與它支援的人類組織交織在一起,這意味著此類 *遺留* 系統的維護既是人的問題,也是技術問題 [^87]。 -我們今天建立的每個系統如果有足夠的價值能夠存活很長時間,有一天會成為遺留系統。為了最大限度地減少未來需要維護我們軟體的幾代人的痛苦,我們應該在設計時考慮維護問題。儘管我們不能總是預測哪些決定可能在未來造成維護麻煩,但在本書中,我們將關注幾個廣泛適用的原則: +如果我們今天建立的每個系統都足夠有價值以長期生存,它有一天將成為遺留系統。為了最小化需要維護我們軟體的未來幾代人的痛苦,我們應該在設計時考慮維護問題。儘管我們不能總是預測哪些決定可能會在未來造成維護難題,但在本書中,我們將注意幾個廣泛適用的原則: -可運維性 -: 使組織能夠輕鬆地保持系統平穩執行。 +可運維性(Operability) +: 使組織容易保持系統平穩執行。 -簡單性 -: 透過使用易於理解、一致的模式和結構來實現系統,並避免不必要的複雜性,使新工程師能夠輕鬆理解系統。 +簡單性(Simplicity) +: 透過使用易於理解、一致的模式和結構來實施它,並避免不必要的複雜性,使新工程師容易理解系統。 -可演化性 -: 使工程師能夠輕鬆地在未來對系統進行更改,隨著需求的變化而調整和擴充套件它以適應意外的用例。 +可演化性(Evolvability) +: 使工程師將來容易對系統進行更改,隨著需求變化而適應和擴充套件它以用於未預料的用例。 ### 可運維性:讓運維更輕鬆 {#id37} -我們之前在 ["雲時代的運維"](/tw/ch1#sec_introduction_operations) 中討論了運維的作用,我們看到人類流程對於可靠運維至少與軟體工具一樣重要。事實上,有人建議"良好的運維通常可以解決糟糕(或不完整)軟體的限制,但良好的軟體無法透過糟糕的運維可靠地執行" [^60]。 +我們之前在 ["雲時代的運維"](/tw/ch1#sec_introduction_operations) 中討論了運維的角色,我們看到人類流程對於可靠運維至少與軟體工具一樣重要。 +事實上,有人提出 “良好的運維通常可以解決糟糕(或不完整)軟體的侷限性,但再好的軟體碰上糟糕的運維也難以可靠地執行” [^60]。 -在由數千臺機器組成的大規模系統中,手動維護將是不合理的昂貴,自動化是必不可少的。然而,自動化可能是一把雙刃劍:總會有邊緣情況(例如罕見的故障場景)需要運維團隊的手動干預。由於無法自動處理的情況是最複雜的問題,更大的自動化需要一個 **更** 熟練的運維團隊來解決這些問題 [^88]。 +在由數千臺機器組成的大規模系統中,手動維護將是不合理地昂貴的,自動化是必不可少的。然而,自動化可能是一把雙刃劍: +總會有邊緣情況(如罕見的故障場景)需要運維團隊的手動干預。由於無法自動處理的情況是最複雜的問題,更大的自動化需要一個 **更** 熟練的運維團隊來解決這些問題 [^88]。 -此外,如果自動化系統出錯,它通常比依賴操作員手動執行某些操作的系統更難排除故障。出於這個原因,更多的自動化並不總是對可操作性更好。然而,一定量的自動化很重要,最佳點將取決於你特定應用程式和組織的具體情況。 +此外,如果自動化系統出錯,通常比依賴操作員手動執行某些操作的系統更難排除故障。出於這個原因,更多的自動化並不總是對可操作性更好。 +然而,一定程度的自動化很重要,最佳點將取決於你特定應用程式和組織的細節。 -良好的可操作性意味著使例行任務變得容易,允許運維團隊將精力集中在高價值活動上。資料系統可以做各種事情來使例行任務變得容易,包括 [^89]: +良好的可操作性意味著使常規任務變得容易,使運維團隊能夠將精力集中在高價值活動上。 +資料系統可以做各種事情來使常規任務變得容易,包括 [^89]: -* 允許監控工具檢查系統的關鍵指標,並支援可觀察性工具(參見 ["分散式系統的問題"](/tw/ch1#sec_introduction_dist_sys_problems))以深入瞭解系統的執行時行為。各種商業和開源工具可以在這裡提供幫助 [^90]。 -* 避免對單個機器的依賴(允許在系統作為一個整體繼續不間斷執行的同時關閉機器進行維護) -* 提供良好的文件和易於理解的操作模型("如果我做 X,Y 將發生") +* 允許監控工具檢查系統的關鍵指標,並支援可觀測性工具(參見 ["分散式系統的問題"](/tw/ch1#sec_introduction_dist_sys_problems))以深入瞭解系統的執行時行為。各種商業和開源工具可以在這裡提供幫助 [^90]。 +* 避免對單個機器的依賴(允許在系統整體繼續不間斷執行的同時關閉機器進行維護) +* 提供良好的文件和易於理解的操作模型("如果我做 X,Y 將會發生") * 提供良好的預設行為,但也給管理員在需要時覆蓋預設值的自由 -* 在適當的情況下自我修復,但也在需要時為管理員提供對系統狀態的手動控制 -* 表現出可預測的行為,最大限度地減少意外 +* 在適當的地方自我修復,但也在需要時給管理員手動控制系統狀態 +* 表現出可預測的行為,最小化意外 ### 簡單性:管理複雜度 {#id38} -小型軟體專案可以擁有令人愉快的簡單和富有表現力的程式碼,但隨著專案變大,它們通常變得非常複雜且難以理解。這種複雜性減慢了每個需要在系統上工作的人的速度,進一步增加了維護成本。陷入複雜性的軟體專案有時被描述為 **大泥球** [^91]。 +小型軟體專案可以有令人愉快地簡單和富有表現力的程式碼,但隨著專案變大,它們通常變得非常複雜且難以理解。 +這種複雜性減慢了需要在系統上工作的每個人,進一步增加了維護成本。陷入複雜性的軟體專案有時被描述為 *大泥球* [^91]。 -當複雜性使維護變得困難時,預算和時間表經常被超支。在複雜的軟體中,進行更改時引入錯誤的風險也更大:當系統對開發人員來說更難理解和推理時,隱藏的假設、意外的後果和意外的互動更容易被忽視 [^69]。相反,降低複雜性極大地提高了軟體的可維護性,因此簡單性應該是我們構建的系統的關鍵目標。 +當複雜性使維護困難時,預算和時間表經常超支。在複雜軟體中,進行更改時引入錯誤的風險也更大: +當系統對開發人員來說更難理解和推理時,隱藏的假設、意外的後果和意外的互動更容易被忽視 [^69]。 +相反,降低複雜性極大地提高了軟體的可維護性,因此簡單性應該是我們構建的系統的關鍵目標。 -簡單的系統更容易理解,因此我們應該嘗試以最簡單的方式解決給定的問題。不幸的是,說起來容易做起來難。某事是否簡單通常是主觀品味問題,因為沒有客觀的簡單性標準 [^92]。例如,一個系統可能會在簡單介面後面隱藏複雜的實現,而另一個系統可能有一個向其使用者公開更多內部細節的簡單實現——哪個更簡單? +簡單系統更容易理解,因此我們應該嘗試以儘可能簡單的方式解決給定問題。不幸的是,這說起來容易做起來難。 +某物是否簡單通常是主觀的品味問題,因為沒有客觀的簡單性標準 [^92]。例如,一個系統可能在簡單介面後面隱藏複雜的實現, +而另一個系統可能有一個向用戶公開更多內部細節的簡單實現——哪一個更簡單? -推理複雜性的一種嘗試是將其分為兩類,**基本** 和 **偶然** 複雜性 [^93]。想法是基本複雜性是應用程式問題域中固有的,而偶然複雜性僅由於我們工具的限制而產生。不幸的是,這種區別也是有缺陷的,因為隨著我們的工具發展,基本和偶然之間的界限會發生變化 [^94]。 +推理複雜性的一種嘗試是將其分為兩類,**本質複雜性** 和 **偶然複雜性** [^93]。 +這個想法是,本質複雜性是應用程式問題域中固有的,而偶然複雜性僅由於我們工具的限制而產生。 +不幸的是,這種區別也有缺陷,因為本質和偶然之間的邊界隨著我們工具的發展而變化 [^94]。 -我們管理複雜性的最佳工具之一是 **抽象**。良好的抽象可以在乾淨、易於理解的外觀後面隱藏大量的實現細節。良好的抽象也可以用於各種不同的應用程式。這種重用不僅比多次重新實現類似的東西更有效,而且還導致更高質量的軟體,因為抽象元件中的質量改進使所有使用它的應用程式受益。 +我們管理複雜性的最佳工具之一是 **抽象**。良好的抽象可以在乾淨、易於理解的外觀後面隱藏大量實現細節。良好的抽象也可以用於各種不同的應用程式。 +這種重用不僅比多次重新實現類似的東西更有效,而且還導致更高質量的軟體,因為抽象元件中的質量改進使所有使用它的應用程式受益。 -例如,高階程式語言是隱藏機器程式碼、CPU 暫存器和系統呼叫的抽象。SQL 是隱藏複雜的磁碟和記憶體資料結構、來自其他客戶端的併發請求以及崩潰後不一致的抽象。當然,在高階語言中程式設計時,我們仍在使用機器程式碼;我們只是不 **直接** 使用它,因為程式語言抽象使我們免於思考它。 +例如,高階程式語言是隱藏機器程式碼、CPU 暫存器和系統呼叫的抽象。SQL 是一種隱藏複雜的磁碟上和記憶體中資料結構、來自其他客戶端的併發請求以及崩潰後不一致性的抽象。 +當然,在用高階語言程式設計時,我們仍在使用機器程式碼;我們只是不 *直接* 使用它,因為程式語言抽象使我們免於考慮它。 -應用程式程式碼的抽象,旨在降低其複雜性,可以使用 **設計模式** [^95] 和 **領域驅動設計**(DDD)[^96] 等方法建立。本書不是關於這種特定於應用程式的抽象,而是關於你可以在其上構建應用程式的通用抽象,例如資料庫事務、索引和事件日誌。如果你想使用 DDD 等技術,你可以在本書中描述的基礎之上實現它們。 +應用程式程式碼的抽象,旨在降低其複雜性,可以使用諸如 *設計模式* [^95] 和 *領域驅動設計*(DDD)[^96] 等方法建立。 +本書不是關於此類特定於應用程式的抽象,而是關於你可以在其上構建應用程式的通用抽象,例如資料庫事務、索引和事件日誌。如果你想使用像 DDD 這樣的技術,你可以在本書中描述的基礎之上實現它們。 ### 可演化性:讓變化更容易 {#sec_introduction_evolvability} -你的系統需求極不可能永遠保持不變。它們更有可能處於不斷變化中:你學習新事實,以前意料之外的用例出現,業務優先順序發生變化,使用者請求新功能,新平臺取代舊平臺,法律或監管要求發生變化,系統的增長迫使架構變化等。 +你的系統需求將保持不變的可能性極小。它們更可能處於不斷變化中: +你學習新事實、以前未預料的用例出現、業務優先順序發生變化、使用者請求新功能、 +新平臺取代舊平臺、法律或監管要求發生變化、系統增長迫使架構變化等。 -就組織流程而言,**敏捷** 工作模式為適應變化提供了一個框架。敏捷社群還開發了在頻繁變化的環境中開發軟體時有用的技術工具和流程,例如測試驅動開發(TDD)和重構。在本書中,我們尋找在由幾個具有不同特徵的不同應用程式或服務組成的系統級別提高敏捷性的方法。 +在組織流程方面,*敏捷* 工作模式為適應變化提供了框架。敏捷社群還開發了在頻繁變化的環境中開發軟體時有用的技術工具和流程, +例如測試驅動開發(TDD)和重構。在本書中,我們搜尋在由具有不同特徵的幾個不同應用程式或服務組成的系統級別增加敏捷性的方法。 -你可以修改資料系統並使其適應不斷變化的需求的難易程度與其簡單性及其抽象密切相關:鬆散耦合、簡單的系統通常比緊密耦合、複雜的系統更容易修改。由於這是一個如此重要的想法,我們將使用不同的詞來指代資料系統級別的敏捷性:**可演化性** [^97]。 +你可以修改資料系統並使其適應不斷變化的需求的容易程度與其簡單性及其抽象密切相關:鬆散耦合、簡單系統通常比緊密耦合、複雜系統更容易修改。 +由於這是一個如此重要的想法,我們將使用不同的詞來指代資料系統級別的敏捷性:*可演化性* [^97]。 -使變化變得困難的一個主要因素是當某些行動是不可逆的,因此需要非常小心地採取該行動 [^98]。例如,假設你正在從一個數據庫遷移到另一個數據庫:如果你無法在新資料庫出現問題時切換回舊系統,那麼風險比你可以輕鬆返回時要高得多。最小化不可逆性可以提高靈活性。 +使大型系統中的變化困難的一個主要因素是某些操作不可逆,因此需要非常謹慎地採取該操作 [^98]。 +例如,假設你正在從一個數據庫遷移到另一個數據庫:如果在新資料庫出現問題時無法切換回舊系統,風險就會高得多,而如果你可以輕鬆返回。最小化不可逆性提高了靈活性。 ## 總結 {#summary} -在本章中,我們研究了幾個非功能性需求的示例:效能、可靠性、可擴充套件性和可維護性。透過這些主題,我們還遇到了我們在本書其餘部分所需的原則和術語。我們從一個如何在社交網路中實現主頁時間線的案例研究開始,這說明了大規模出現的一些挑戰。 +在本章中,我們研究了幾個非功能性需求的例子:效能、可靠性、可伸縮性和可維護性。 +透過這些主題,我們還遇到了我們在本書其餘部分需要的原則和術語。我們從社交網路中首頁時間線如何實現的案例研究開始,這說明了規模上出現的一些挑戰。 -我們討論了如何衡量效能(例如,使用響應時間百分位數)、系統上的負載(例如,使用吞吐量指標)以及它們如何在 SLA 中使用。可擴充套件性是一個密切相關的概念:即,確保負載增長時效能保持不變。我們看到了一些可擴充套件性的一般原則,例如將任務分解為可以獨立執行的較小部分,我們將在以下章節中深入探討可擴充套件性技術的技術細節。 +我們討論了如何衡量效能(例如,使用響應時間百分位數)、系統上的負載(例如,使用吞吐量指標),以及它們如何在 SLA 中使用。 +可伸縮性是一個密切相關的概念:即,在負載增長時確保效能保持不變。我們看到了可伸縮性的一些一般原則,例如將任務分解為可以獨立執行的較小部分,我們將在以下章節中深入研究可伸縮性技術的技術細節。 -為了實現可靠性,你可以使用容錯技術,即使某些元件(例如,磁碟、機器或其他服務)出現故障,系統也可以繼續提供其服務。我們看到了可能發生的硬體故障的示例,並將它們與軟體故障區分開來,軟體故障可能更難處理,因為它們通常高度相關。實現可靠性的另一個方面是建立對人類犯錯誤的韌性,我們看到無責備事後分析作為從事件中學習的技術。 +為了實現可靠性,你可以使用容錯技術,即使某個元件(例如,磁碟、機器或其他服務)出現故障,系統也可以繼續提供其服務。 +我們看到了可能發生的硬體故障的例子,並將它們與軟體故障區分開來,軟體故障可能更難處理,因為它們通常是強相關的。 +實現可靠性的另一個方面是建立對人類犯錯誤的韌性,我們看到無責備事後分析作為從事件中學習的技術。 -最後,我們研究了可維護性的幾個方面,包括支援運維團隊的工作、管理複雜性以及使應用程式的功能隨著時間的推移易於發展。關於如何實現這些事情沒有簡單的答案,但有一件事可以幫助,那就是使用經過驗證在實踐中有價值的易於理解的構建塊來構建應用程式。本書的其餘部分將涵蓋一系列已被證明在實踐中有價值的構建塊。 +最後,我們研究了可維護性的幾個方面,包括支援運維團隊的工作、管理複雜性以及隨著時間的推移使應用程式功能易於演化。 +實現這些目標沒有簡單的答案,但有一件事可以幫助,那就是使用提供有用抽象的易於理解的構建塊來構建應用程式。本書的其餘部分將涵蓋一系列在實踐中被證明有價值的構建塊。 - -### 參考 +### 參考 {#參考} [^1]: Mike Cvet. [How We Learned to Stop Worrying and Love Fan-In at Twitter](https://www.youtube.com/watch?v=WEgCjwyXvwc). At *QCon San Francisco*, December 2016. [^2]: Raffi Krikorian. [Timelines at Scale](https://www.infoq.com/presentations/Twitter-Timeline-Scalability/). At *QCon San Francisco*, November 2012. Archived at [perma.cc/V9G5-KLYK](https://perma.cc/V9G5-KLYK) -[^3]: Twitter. [Twitter’s Recommendation Algorithm](https://blog.twitter.com/engineering/en_us/topics/open-source/2023/twitter-recommendation-algorithm). *blog.twitter.com*, March 2023. Archived at [perma.cc/L5GT-229T](https://perma.cc/L5GT-229T) +[^3]: Twitter. [Twitter's Recommendation Algorithm](https://blog.twitter.com/engineering/en_us/topics/open-source/2023/twitter-recommendation-algorithm). *blog.twitter.com*, March 2023. Archived at [perma.cc/L5GT-229T](https://perma.cc/L5GT-229T) [^4]: Raffi Krikorian. [New Tweets per second record, and how!](https://blog.twitter.com/engineering/en_us/a/2013/new-tweets-per-second-record-and-how) *blog.twitter.com*, August 2013. Archived at [perma.cc/6JZN-XJYN](https://perma.cc/6JZN-XJYN) -[^5]: Jaz Volpert. [When Imperfect Systems are Good, Actually: Bluesky’s Lossy Timelines](https://jazco.dev/2025/02/19/imperfection/). *jazco.dev*, February 2025. Archived at [perma.cc/2PVE-L2MX](https://perma.cc/2PVE-L2MX) -[^6]: Samuel Axon. [3% of Twitter’s Servers Dedicated to Justin Bieber](https://mashable.com/archive/justin-bieber-twitter). *mashable.com*, September 2010. Archived at [perma.cc/F35N-CGVX](https://perma.cc/F35N-CGVX) +[^5]: Jaz Volpert. [When Imperfect Systems are Good, Actually: Bluesky's Lossy Timelines](https://jazco.dev/2025/02/19/imperfection/). *jazco.dev*, February 2025. Archived at [perma.cc/2PVE-L2MX](https://perma.cc/2PVE-L2MX) +[^6]: Samuel Axon. [3% of Twitter's Servers Dedicated to Justin Bieber](https://mashable.com/archive/justin-bieber-twitter). *mashable.com*, September 2010. Archived at [perma.cc/F35N-CGVX](https://perma.cc/F35N-CGVX) [^7]: Nathan Bronson, Abutalib Aghayev, Aleksey Charapko, and Timothy Zhu. [Metastable Failures in Distributed Systems](https://sigops.org/s/conferences/hotos/2021/papers/hotos21-s11-bronson.pdf). At *Workshop on Hot Topics in Operating Systems* (HotOS), May 2021. [doi:10.1145/3458336.3465286](https://doi.org/10.1145/3458336.3465286) [^8]: Marc Brooker. [Metastability and Distributed Systems](https://brooker.co.za/blog/2021/05/24/metastable.html). *brooker.co.za*, May 2021. Archived at [perma.cc/7FGJ-7XRK](https://perma.cc/7FGJ-7XRK) [^9]: Marc Brooker. [Exponential Backoff And Jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/). *aws.amazon.com*, March 2015. Archived at [perma.cc/R6MS-AZKH](https://perma.cc/R6MS-AZKH) [^10]: Marc Brooker. [What is Backoff For?](https://brooker.co.za/blog/2022/08/11/backoff.html) *brooker.co.za*, August 2022. Archived at [perma.cc/PW9N-55Q5](https://perma.cc/PW9N-55Q5) [^11]: Michael T. Nygard. [*Release It!*](https://learning.oreilly.com/library/view/release-it-2nd/9781680504552/), 2nd Edition. Pragmatic Bookshelf, January 2018. ISBN: 9781680502398 -[^12]: Frank Chen. [Slowing Down to Speed Up – Circuit Breakers for Slack’s CI/CD](https://slack.engineering/circuit-breakers/). *slack.engineering*, August 2022. Archived at [perma.cc/5FGS-ZPH3](https://perma.cc/5FGS-ZPH3) +[^12]: Frank Chen. [Slowing Down to Speed Up – Circuit Breakers for Slack's CI/CD](https://slack.engineering/circuit-breakers/). *slack.engineering*, August 2022. Archived at [perma.cc/5FGS-ZPH3](https://perma.cc/5FGS-ZPH3) [^13]: Marc Brooker. [Fixing retries with token buckets and circuit breakers](https://brooker.co.za/blog/2022/02/28/retries.html). *brooker.co.za*, February 2022. Archived at [perma.cc/MD6N-GW26](https://perma.cc/MD6N-GW26) -[^14]: David Yanacek. [Using load shedding to avoid overload](https://aws.amazon.com/builders-library/using-load-shedding-to-avoid-overload/). Amazon Builders’ Library, *aws.amazon.com*. Archived at [perma.cc/9SAW-68MP](https://perma.cc/9SAW-68MP) +[^14]: David Yanacek. [Using load shedding to avoid overload](https://aws.amazon.com/builders-library/using-load-shedding-to-avoid-overload/). Amazon Builders' Library, *aws.amazon.com*. Archived at [perma.cc/9SAW-68MP](https://perma.cc/9SAW-68MP) [^15]: Matthew Sackman. [Pushing Back](https://wellquite.org/posts/lshift/pushing_back/). *wellquite.org*, May 2016. Archived at [perma.cc/3KCZ-RUFY](https://perma.cc/3KCZ-RUFY) [^16]: Dmitry Kopytkov and Patrick Lee. [Meet Bandaid, the Dropbox service proxy](https://dropbox.tech/infrastructure/meet-bandaid-the-dropbox-service-proxy). *dropbox.tech*, March 2018. Archived at [perma.cc/KUU6-YG4S](https://perma.cc/KUU6-YG4S) [^17]: Haryadi S. Gunawi, Riza O. Suminto, Russell Sears, Casey Golliher, Swaminathan Sundararaman, Xing Lin, Tim Emami, Weiguang Sheng, Nematollah Bidokhti, Caitie McCaffrey, Gary Grider, Parks M. Fields, Kevin Harms, Robert B. Ross, Andree Jacobson, Robert Ricci, Kirk Webb, Peter Alvaro, H. Birali Runesha, Mingzhe Hao, and Huaicheng Li. [Fail-Slow at Scale: Evidence of Hardware Performance Faults in Large Production Systems](https://www.usenix.org/system/files/conference/fast18/fast18-gunawi.pdf). At *16th USENIX Conference on File and Storage Technologies*, February 2018. [^18]: Marc Brooker. [Is the Mean Really Useless?](https://brooker.co.za/blog/2017/12/28/mean.html) *brooker.co.za*, December 2017. Archived at [perma.cc/U5AE-CVEM](https://perma.cc/U5AE-CVEM) -[^19]: Giuseppe DeCandia, Deniz Hastorun, Madan Jampani, Gunavardhan Kakulapati, Avinash Lakshman, Alex Pilchin, Swaminathan Sivasubramanian, Peter Vosshall, and Werner Vogels. [Dynamo: Amazon’s Highly Available Key-Value Store](https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf). At *21st ACM Symposium on Operating Systems Principles* (SOSP), October 2007. [doi:10.1145/1294261.1294281](https://doi.org/10.1145/1294261.1294281) +[^19]: Giuseppe DeCandia, Deniz Hastorun, Madan Jampani, Gunavardhan Kakulapati, Avinash Lakshman, Alex Pilchin, Swaminathan Sivasubramanian, Peter Vosshall, and Werner Vogels. [Dynamo: Amazon's Highly Available Key-Value Store](https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf). At *21st ACM Symposium on Operating Systems Principles* (SOSP), October 2007. [doi:10.1145/1294261.1294281](https://doi.org/10.1145/1294261.1294281) [^20]: Kathryn Whitenton. [The Need for Speed, 23 Years Later](https://www.nngroup.com/articles/the-need-for-speed/). *nngroup.com*, May 2020. Archived at [perma.cc/C4ER-LZYA](https://perma.cc/C4ER-LZYA) [^21]: Greg Linden. [Marissa Mayer at Web 2.0](https://glinden.blogspot.com/2006/11/marissa-mayer-at-web-20.html). *glinden.blogspot.com*, November 2005. Archived at [perma.cc/V7EA-3VXB](https://perma.cc/V7EA-3VXB) [^22]: Jake Brutlag. [Speed Matters for Google Web Search](https://services.google.com/fh/files/blogs/google_delayexp.pdf). *services.google.com*, June 2009. Archived at [perma.cc/BK7R-X7M2](https://perma.cc/BK7R-X7M2) @@ -424,19 +448,19 @@ Akamai 最近的一項研究 [^24] 聲稱響應時間增加 100 毫秒使電子 [^24]: Akamai Technologies, Inc. [The State of Online Retail Performance](https://web.archive.org/web/20210729180749/https%3A//www.akamai.com/us/en/multimedia/documents/report/akamai-state-of-online-retail-performance-spring-2017.pdf). *akamai.com*, April 2017. Archived at [perma.cc/UEK2-HYCS](https://perma.cc/UEK2-HYCS) [^25]: Xiao Bai, Ioannis Arapakis, B. Barla Cambazoglu, and Ana Freire. [Understanding and Leveraging the Impact of Response Latency on User Behaviour in Web Search](https://iarapakis.github.io/papers/TOIS17.pdf). *ACM Transactions on Information Systems*, volume 36, issue 2, article 21, April 2018. [doi:10.1145/3106372](https://doi.org/10.1145/3106372) [^26]: Jeffrey Dean and Luiz André Barroso. [The Tail at Scale](https://cacm.acm.org/research/the-tail-at-scale/). *Communications of the ACM*, volume 56, issue 2, pages 74–80, February 2013. [doi:10.1145/2408776.2408794](https://doi.org/10.1145/2408776.2408794) -[^27]: Alex Hidalgo. [*Implementing Service Level Objectives: A Practical Guide to SLIs, SLOs, and Error Budgets*](https://www.oreilly.com/library/view/implementing-service-level/9781492076803/). O’Reilly Media, September 2020. ISBN: 1492076813 +[^27]: Alex Hidalgo. [*Implementing Service Level Objectives: A Practical Guide to SLIs, SLOs, and Error Budgets*](https://www.oreilly.com/library/view/implementing-service-level/9781492076803/). O'Reilly Media, September 2020. ISBN: 1492076813 [^28]: Jeffrey C. Mogul and John Wilkes. [Nines are Not Enough: Meaningful Metrics for Clouds](https://research.google/pubs/pub48033/). At *17th Workshop on Hot Topics in Operating Systems* (HotOS), May 2019. [doi:10.1145/3317550.3321432](https://doi.org/10.1145/3317550.3321432) [^29]: Tamás Hauer, Philipp Hoffmann, John Lunney, Dan Ardelean, and Amer Diwan. [Meaningful Availability](https://www.usenix.org/conference/nsdi20/presentation/hauer). At *17th USENIX Symposium on Networked Systems Design and Implementation* (NSDI), February 2020. [^30]: Ted Dunning. [The t-digest: Efficient estimates of distributions](https://www.sciencedirect.com/science/article/pii/S2665963820300403). *Software Impacts*, volume 7, article 100049, February 2021. [doi:10.1016/j.simpa.2020.100049](https://doi.org/10.1016/j.simpa.2020.100049) -[^31]: David Kohn. [How percentile approximation works (and why it’s more useful than averages)](https://www.timescale.com/blog/how-percentile-approximation-works-and-why-its-more-useful-than-averages/). *timescale.com*, September 2021. Archived at [perma.cc/3PDP-NR8B](https://perma.cc/3PDP-NR8B) +[^31]: David Kohn. [How percentile approximation works (and why it's more useful than averages)](https://www.timescale.com/blog/how-percentile-approximation-works-and-why-its-more-useful-than-averages/). *timescale.com*, September 2021. Archived at [perma.cc/3PDP-NR8B](https://perma.cc/3PDP-NR8B) [^32]: Heinrich Hartmann and Theo Schlossnagle. [Circllhist — A Log-Linear Histogram Data Structure for IT Infrastructure Monitoring](https://arxiv.org/pdf/2001.06561.pdf). *arxiv.org*, January 2020. [^33]: Charles Masson, Jee E. Rim, and Homin K. Lee. [DDSketch: A Fast and Fully-Mergeable Quantile Sketch with Relative-Error Guarantees](https://www.vldb.org/pvldb/vol12/p2195-masson.pdf). *Proceedings of the VLDB Endowment*, volume 12, issue 12, pages 2195–2205, August 2019. [doi:10.14778/3352063.3352135](https://doi.org/10.14778/3352063.3352135) -[^34]: Baron Schwartz. [Why Percentiles Don’t Work the Way You Think](https://orangematter.solarwinds.com/2016/11/18/why-percentiles-dont-work-the-way-you-think/). *solarwinds.com*, November 2016. Archived at [perma.cc/469T-6UGB](https://perma.cc/469T-6UGB) +[^34]: Baron Schwartz. [Why Percentiles Don't Work the Way You Think](https://orangematter.solarwinds.com/2016/11/18/why-percentiles-dont-work-the-way-you-think/). *solarwinds.com*, November 2016. Archived at [perma.cc/469T-6UGB](https://perma.cc/469T-6UGB) [^35]: Walter L. Heimerdinger and Charles B. Weinstock. [A Conceptual Framework for System Fault Tolerance](https://resources.sei.cmu.edu/asset_files/TechnicalReport/1992_005_001_16112.pdf). Technical Report CMU/SEI-92-TR-033, Software Engineering Institute, Carnegie Mellon University, October 1992. Archived at [perma.cc/GD2V-DMJW](https://perma.cc/GD2V-DMJW) [^36]: Felix C. Gärtner. [Fundamentals of fault-tolerant distributed computing in asynchronous environments](https://dl.acm.org/doi/pdf/10.1145/311531.311532). *ACM Computing Surveys*, volume 31, issue 1, pages 1–26, March 1999. [doi:10.1145/311531.311532](https://doi.org/10.1145/311531.311532) [^37]: Algirdas Avižienis, Jean-Claude Laprie, Brian Randell, and Carl Landwehr. [Basic Concepts and Taxonomy of Dependable and Secure Computing](https://hdl.handle.net/1903/6459). *IEEE Transactions on Dependable and Secure Computing*, volume 1, issue 1, January 2004. [doi:10.1109/TDSC.2004.2](https://doi.org/10.1109/TDSC.2004.2) [^38]: Ding Yuan, Yu Luo, Xin Zhuang, Guilherme Renna Rodrigues, Xu Zhao, Yongle Zhang, Pranay U. Jain, and Michael Stumm. [Simple Testing Can Prevent Most Critical Failures: An Analysis of Production Failures in Distributed Data-Intensive Systems](https://www.usenix.org/system/files/conference/osdi14/osdi14-paper-yuan.pdf). At *11th USENIX Symposium on Operating Systems Design and Implementation* (OSDI), October 2014. -[^39]: Casey Rosenthal and Nora Jones. [*Chaos Engineering*](https://learning.oreilly.com/library/view/chaos-engineering/9781492043850/). O’Reilly Media, April 2020. ISBN: 9781492043867 +[^39]: Casey Rosenthal and Nora Jones. [*Chaos Engineering*](https://learning.oreilly.com/library/view/chaos-engineering/9781492043850/). O'Reilly Media, April 2020. ISBN: 9781492043867 [^40]: Eduardo Pinheiro, Wolf-Dietrich Weber, and Luiz Andre Barroso. [Failure Trends in a Large Disk Drive Population](https://www.usenix.org/legacy/events/fast07/tech/full_papers/pinheiro/pinheiro_old.pdf). At *5th USENIX Conference on File and Storage Technologies* (FAST), February 2007. [^41]: Bianca Schroeder and Garth A. Gibson. [Disk failures in the real world: What does an MTTF of 1,000,000 hours mean to you?](https://www.usenix.org/legacy/events/fast07/tech/schroeder/schroeder.pdf) At *5th USENIX Conference on File and Storage Technologies* (FAST), February 2007. [^42]: Andy Klein. [Backblaze Drive Stats for Q2 2021](https://www.backblaze.com/blog/backblaze-drive-stats-for-q2-2021/). *backblaze.com*, August 2021. Archived at [perma.cc/2943-UD5E](https://perma.cc/2943-UD5E) @@ -446,7 +470,7 @@ Akamai 最近的一項研究 [^24] 聲稱響應時間增加 100 毫秒使電子 [^46]: Jacob Alter, Ji Xue, Alma Dimnaku, and Evgenia Smirni. [SSD failures in the field: symptoms, causes, and prediction models](https://dl.acm.org/doi/pdf/10.1145/3295500.3356172). At *International Conference for High Performance Computing, Networking, Storage and Analysis* (SC), November 2019. [doi:10.1145/3295500.3356172](https://doi.org/10.1145/3295500.3356172) [^47]: Daniel Ford, François Labelle, Florentina I. Popovici, Murray Stokely, Van-Anh Truong, Luiz Barroso, Carrie Grimes, and Sean Quinlan. [Availability in Globally Distributed Storage Systems](https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Ford.pdf). At *9th USENIX Symposium on Operating Systems Design and Implementation* (OSDI), October 2010. [^48]: Kashi Venkatesh Vishwanath and Nachiappan Nagappan. [Characterizing Cloud Computing Hardware Reliability](https://www.microsoft.com/en-us/research/wp-content/uploads/2010/06/socc088-vishwanath.pdf). At *1st ACM Symposium on Cloud Computing* (SoCC), June 2010. [doi:10.1145/1807128.1807161](https://doi.org/10.1145/1807128.1807161) -[^49]: Peter H. Hochschild, Paul Turner, Jeffrey C. Mogul, Rama Govindaraju, Parthasarathy Ranganathan, David E. Culler, and Amin Vahdat. [Cores that don’t count](https://sigops.org/s/conferences/hotos/2021/papers/hotos21-s01-hochschild.pdf). At *Workshop on Hot Topics in Operating Systems* (HotOS), June 2021. [doi:10.1145/3458336.3465297](https://doi.org/10.1145/3458336.3465297) +[^49]: Peter H. Hochschild, Paul Turner, Jeffrey C. Mogul, Rama Govindaraju, Parthasarathy Ranganathan, David E. Culler, and Amin Vahdat. [Cores that don't count](https://sigops.org/s/conferences/hotos/2021/papers/hotos21-s01-hochschild.pdf). At *Workshop on Hot Topics in Operating Systems* (HotOS), June 2021. [doi:10.1145/3458336.3465297](https://doi.org/10.1145/3458336.3465297) [^50]: Harish Dattatraya Dixit, Sneha Pendharkar, Matt Beadon, Chris Mason, Tejasvi Chakravarthy, Bharath Muthiah, and Sriram Sankar. [Silent Data Corruptions at Scale](https://arxiv.org/abs/2102.11245). *arXiv:2102.11245*, February 2021. [^51]: Diogo Behrens, Marco Serafini, Sergei Arnautov, Flavio P. Junqueira, and Christof Fetzer. [Scalable Error Isolation for Distributed Systems](https://www.usenix.org/conference/nsdi15/technical-sessions/presentation/behrens). At *12th USENIX Symposium on Networked Systems Design and Implementation* (NSDI), May 2015. [^52]: Bianca Schroeder, Eduardo Pinheiro, and Wolf-Dietrich Weber. [DRAM Errors in the Wild: A Large-Scale Field Study](https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/35162.pdf). At *11th International Joint Conference on Measurement and Modeling of Computer Systems* (SIGMETRICS), June 2009. [doi:10.1145/1555349.1555372](https://doi.org/10.1145/1555349.1555372) @@ -463,17 +487,17 @@ Akamai 最近的一項研究 [^24] 聲稱響應時間增加 100 毫秒使電子 [^63]: Lorin Hochstein. [awesome limits](https://github.com/lorin/awesome-limits). *github.com*, November 2020. Archived at [perma.cc/3R5M-E5Q4](https://perma.cc/3R5M-E5Q4) [^64]: Caitie McCaffrey. [Clients Are Jerks: AKA How Halo 4 DoSed the Services at Launch & How We Survived](https://www.caitiem.com/2015/06/23/clients-are-jerks-aka-how-halo-4-dosed-the-services-at-launch-how-we-survived/). *caitiem.com*, June 2015. Archived at [perma.cc/MXX4-W373](https://perma.cc/MXX4-W373) [^65]: Lilia Tang, Chaitanya Bhandari, Yongle Zhang, Anna Karanika, Shuyang Ji, Indranil Gupta, and Tianyin Xu. [Fail through the Cracks: Cross-System Interaction Failures in Modern Cloud Systems](https://tianyin.github.io/pub/csi-failures.pdf). At *18th European Conference on Computer Systems* (EuroSys), May 2023. [doi:10.1145/3552326.3587448](https://doi.org/10.1145/3552326.3587448) -[^66]: Mike Ulrich. [Addressing Cascading Failures](https://sre.google/sre-book/addressing-cascading-failures/). In Betsy Beyer, Jennifer Petoff, Chris Jones, and Niall Richard Murphy (ed). [*Site Reliability Engineering: How Google Runs Production Systems*](https://www.oreilly.com/library/view/site-reliability-engineering/9781491929117/). O’Reilly Media, 2016. ISBN: 9781491929124 +[^66]: Mike Ulrich. [Addressing Cascading Failures](https://sre.google/sre-book/addressing-cascading-failures/). In Betsy Beyer, Jennifer Petoff, Chris Jones, and Niall Richard Murphy (ed). [*Site Reliability Engineering: How Google Runs Production Systems*](https://www.oreilly.com/library/view/site-reliability-engineering/9781491929117/). O'Reilly Media, 2016. ISBN: 9781491929124 [^67]: Harri Faßbender. [Cascading failures in large-scale distributed systems](https://blog.mi.hdm-stuttgart.de/index.php/2022/03/03/cascading-failures-in-large-scale-distributed-systems/). *blog.mi.hdm-stuttgart.de*, March 2022. Archived at [perma.cc/K7VY-YJRX](https://perma.cc/K7VY-YJRX) [^68]: Richard I. Cook. [How Complex Systems Fail](https://www.adaptivecapacitylabs.com/HowComplexSystemsFail.pdf). Cognitive Technologies Laboratory, April 2000. Archived at [perma.cc/RDS6-2YVA](https://perma.cc/RDS6-2YVA) [^69]: David D. Woods. [STELLA: Report from the SNAFUcatchers Workshop on Coping With Complexity](https://snafucatchers.github.io/). *snafucatchers.github.io*, March 2017. Archived at [archive.org](https://web.archive.org/web/20230306130131/https%3A//snafucatchers.github.io/) [^70]: David Oppenheimer, Archana Ganapathi, and David A. Patterson. [Why Do Internet Services Fail, and What Can Be Done About It?](https://static.usenix.org/events/usits03/tech/full_papers/oppenheimer/oppenheimer.pdf) At *4th USENIX Symposium on Internet Technologies and Systems* (USITS), March 2003. -[^71]: Sidney Dekker. [*The Field Guide to Understanding ‘Human Error’, 3rd Edition*](https://learning.oreilly.com/library/view/the-field-guide/9781317031833/). CRC Press, November 2017. ISBN: 9781472439055 +[^71]: Sidney Dekker. [*The Field Guide to Understanding 'Human Error', 3rd Edition*](https://learning.oreilly.com/library/view/the-field-guide/9781317031833/). CRC Press, November 2017. ISBN: 9781472439055 [^72]: Sidney Dekker. [*Drift into Failure: From Hunting Broken Components to Understanding Complex Systems*](https://www.taylorfrancis.com/books/mono/10.1201/9781315257396/drift-failure-sidney-dekker). CRC Press, 2011. ISBN: 9781315257396 [^73]: John Allspaw. [Blameless PostMortems and a Just Culture](https://www.etsy.com/codeascraft/blameless-postmortems/). *etsy.com*, May 2012. Archived at [perma.cc/YMJ7-NTAP](https://perma.cc/YMJ7-NTAP) [^74]: Itzy Sabo. [Uptime Guarantees — A Pragmatic Perspective](https://world.hey.com/itzy/uptime-guarantees-a-pragmatic-perspective-736d7ea4). *world.hey.com*, March 2023. Archived at [perma.cc/F7TU-78JB](https://perma.cc/F7TU-78JB) [^75]: Michael Jurewitz. [The Human Impact of Bugs](http://jury.me/blog/2013/3/14/the-human-impact-of-bugs). *jury.me*, March 2013. Archived at [perma.cc/5KQ4-VDYL](https://perma.cc/5KQ4-VDYL) -[^76]: Mark Halper. [How Software Bugs led to ‘One of the Greatest Miscarriages of Justice’ in British History](https://cacm.acm.org/news/how-software-bugs-led-to-one-of-the-greatest-miscarriages-of-justice-in-british-history/). *Communications of the ACM*, January 2025. [doi:10.1145/3703779](https://doi.org/10.1145/3703779) +[^76]: Mark Halper. [How Software Bugs led to 'One of the Greatest Miscarriages of Justice' in British History](https://cacm.acm.org/news/how-software-bugs-led-to-one-of-the-greatest-miscarriages-of-justice-in-british-history/). *Communications of the ACM*, January 2025. [doi:10.1145/3703779](https://doi.org/10.1145/3703779) [^77]: Nicholas Bohm, James Christie, Peter Bernard Ladkin, Bev Littlewood, Paul Marshall, Stephen Mason, Martin Newby, Steven J. Murdoch, Harold Thimbleby, and Martyn Thomas. [The legal rule that computers are presumed to be operating correctly – unforeseen and unjust consequences](https://www.benthamsgaze.org/wp-content/uploads/2022/06/briefing-presumption-that-computers-are-reliable.pdf). Briefing note, *benthamsgaze.org*, June 2022. Archived at [perma.cc/WQ6X-TMW4](https://perma.cc/WQ6X-TMW4) [^78]: Dan McKinley. [Choose Boring Technology](https://mcfunley.com/choose-boring-technology). *mcfunley.com*, March 2015. Archived at [perma.cc/7QW7-J4YP](https://perma.cc/7QW7-J4YP) [^79]: Andy Warfield. [Building and operating a pretty big storage system called S3](https://www.allthingsdistributed.com/2023/07/building-and-operating-a-pretty-big-storage-system.html). *allthingsdistributed.com*, July 2023. Archived at [perma.cc/7LPK-TP7V](https://perma.cc/7LPK-TP7V) @@ -481,7 +505,7 @@ Akamai 最近的一項研究 [^24] 聲稱響應時間增加 100 毫秒使電子 [^81]: Ben Stopford. [Shared Nothing vs. Shared Disk Architectures: An Independent View](http://www.benstopford.com/2009/11/24/understanding-the-shared-nothing-architecture/). *benstopford.com*, November 2009. Archived at [perma.cc/7BXH-EDUR](https://perma.cc/7BXH-EDUR) [^82]: Michael Stonebraker. [The Case for Shared Nothing](https://dsf.berkeley.edu/papers/hpts85-nothing.pdf). *IEEE Database Engineering Bulletin*, volume 9, issue 1, pages 4–9, March 1986. [^83]: Panagiotis Antonopoulos, Alex Budovski, Cristian Diaconu, Alejandro Hernandez Saenz, Jack Hu, Hanuma Kodavalla, Donald Kossmann, Sandeep Lingam, Umar Farooq Minhas, Naveen Prakash, Vijendra Purohit, Hugh Qu, Chaitanya Sreenivas Ravella, Krystyna Reisteter, Sheetal Shrotri, Dixin Tang, and Vikram Wakade. [Socrates: The New SQL Server in the Cloud](https://www.microsoft.com/en-us/research/uploads/prod/2019/05/socrates.pdf). At *ACM International Conference on Management of Data* (SIGMOD), pages 1743–1756, June 2019. [doi:10.1145/3299869.3314047](https://doi.org/10.1145/3299869.3314047) -[^84]: Sam Newman. [*Building Microservices*, second edition](https://www.oreilly.com/library/view/building-microservices-2nd/9781492034018/). O’Reilly Media, 2021. ISBN: 9781492034025 +[^84]: Sam Newman. [*Building Microservices*, second edition](https://www.oreilly.com/library/view/building-microservices-2nd/9781492034018/). O'Reilly Media, 2021. ISBN: 9781492034025 [^85]: Nathan Ensmenger. [When Good Software Goes Bad: The Surprising Durability of an Ephemeral Technology](https://themaintainers.wpengine.com/wp-content/uploads/2021/04/ensmenger-maintainers-v2.pdf). At *The Maintainers Conference*, April 2016. Archived at [perma.cc/ZXT4-HGZB](https://perma.cc/ZXT4-HGZB) [^86]: Robert L. Glass. [*Facts and Fallacies of Software Engineering*](https://learning.oreilly.com/library/view/facts-and-fallacies/0321117425/). Addison-Wesley Professional, October 2002. ISBN: 9780321117427 [^87]: Marianne Bellotti. [*Kill It with Fire*](https://learning.oreilly.com/library/view/kill-it-with/9781098128883/). No Starch Press, April 2021. ISBN: 9781718501188 diff --git a/content/tw/ch3.md b/content/tw/ch3.md index 3ad13f0..37da4dd 100644 --- a/content/tw/ch3.md +++ b/content/tw/ch3.md @@ -6,85 +6,101 @@ breadcrumbs: false ![](/map/ch02.png) -> *語言的邊界就是思想的邊界。* +> *語言的邊界就是世界的邊界。* > -> 路德維希·維特根斯坦,《邏輯哲學論》(1922) +> 路德維希・維特根斯坦,《邏輯哲學論》(1922) -資料模型可能是軟體開發中最重要的部分了,因為它們有著如此深遠的影響:不僅影響著軟體的編寫方式,而且影響著我們 **思考問題** 的方式。 +資料模型或許是開發軟體最重要的部分,因為它們有著深遠的影響:不僅影響軟體的編寫方式,還影響我們 **思考問題** 的方式。 -大多數應用程式都是透過一層層疊加的資料模型構建的。對於每一層,關鍵問題是:它如何用下一層的資料模型來 **表示**?例如: +大多數應用程式都是透過層層疊加的資料模型來構建的。對於每一層來說的關鍵問題是:如何用更低層次的資料模型來 **表示** 它?例如: -1. 作為應用程式開發人員,你觀察現實世界(裡面有人員、組織、貨物、行為、資金流動、感測器等),並用物件或資料結構以及操作這些資料結構的 API 來進行建模。這些結構通常是特定於你的應用程式的。 -2. 當你想要儲存這些資料結構時,你用通用的資料模型來表達它們,比如 JSON 或 XML 文件、關係資料庫中的表,或圖中的頂點和邊。這些資料模型是本章的主題。 -3. 構建資料庫軟體的工程師決定了如何用記憶體、磁碟或網路上的位元組來表示文件/關係/圖資料。這種表示方式可能允許資料以各種方式被查詢、搜尋、操作和處理。我們將在[第 4 章](/tw/ch4#ch_storage)中討論這些儲存引擎的設計。 -4. 在更低的層次上,硬體工程師已經想出瞭如何用電流、光脈衝、磁場等來表示位元組。 +1. 作為應用程式開發者,你觀察現實世界(其中有人員、組織、貨物、行為、資金流動、感測器等),並用物件或資料結構,以及操作這些資料結構的 API 來建模。這些結構通常是特定於應用程式的。 +2. 當你想要儲存這些資料結構時,你用通用的資料模型來表達它們,例如 JSON 或 XML 文件、關係資料庫中的表,或者圖中的頂點和邊。這些資料模型是本章的主題。 +3. 構建你的資料庫軟體的工程師決定了如何用記憶體、磁碟或網路上的位元組來表示文件/關係/圖資料。這種表示可能允許以各種方式查詢、搜尋、操作和處理資料。我們將在 [第 4 章](/tw/ch4#ch_storage) 中討論這些儲存引擎的設計。 +4. 在更低的層次上,硬體工程師已經想出瞭如何用電流、光脈衝、磁場等來表示位元組的方法。 -在複雜的應用程式中可能有更多的中間層,比如基於 API 構建的 API,但基本思想仍然相同:每一層透過提供一個清晰的資料模型來隱藏下層的複雜性。這些抽象使不同的人群——例如,資料庫供應商的工程師和使用其資料庫的應用程式開發人員——能夠有效地協作。 +在複雜的應用程式中可能有更多的中間層,例如基於 API 之上的 API,但基本思想仍然相同:每一層透過提供一個簡潔的資料模型來隱藏下層的複雜性。這些抽象允許不同的人群 —— 例如,資料庫供應商的工程師和使用他們資料庫的應用程式開發者 —— 有效地合作。 -實踐中廣泛使用了幾種不同的資料模型,通常用於不同的目的。某些型別的資料和某些查詢在一種模型中很容易表達,在另一種模型中卻很彆扭。在本章中,我們將透過比較關係模型、文件模型、基於圖的資料模型、事件溯源和資料框來探討這些權衡。我們還將簡要了解允許你使用這些模型的查詢語言。這種比較將幫助你決定何時使用哪種模型。 +在實踐中廣泛使用著幾種不同的資料模型,通常用於不同的目的。某些型別的資料和某些查詢在一種模型中很容易表達,而在另一種模型中則很困難。在本章中,我們將透過比較關係模型、文件模型、基於圖的資料模型、事件溯源和資料框來探討這些權衡。我們還將簡要介紹允許你使用這些模型的查詢語言。這種比較將幫助你決定何時使用哪種模型。 -------- > [!TIP] 術語:宣告式查詢語言 - -本章中的許多查詢語言(如 SQL、Cypher、SPARQL 或 Datalog)都是 **宣告式** 的,這意味著你指定所需資料的模式——結果必須滿足什麼條件,以及你希望如何轉換資料(例如,排序、分組和聚合)——但不指定 **如何** 實現該目標。資料庫系統的查詢最佳化器可以決定使用哪些索引和哪些連線演算法,以及以什麼順序執行查詢的各個部分。 - -相比之下,在大多數程式語言中,你必須編寫一個 **演算法**——即告訴計算機以什麼順序執行哪些操作。宣告式查詢語言很有吸引力,因為它通常比顯式演算法更簡潔、更容易編寫。但更重要的是,它還隱藏了查詢引擎的實現細節,這使得資料庫系統可以在不需要更改任何查詢的情況下引入效能改進 [^1]。 - -例如,資料庫可能能夠跨多個 CPU 核心和機器並行執行宣告式查詢,而你無需擔心如何實現這種並行性 [^2]。在手寫演算法中,要自己實現這種並行執行將需要大量的工作。 +> +> 本章中的許多查詢語言(如 SQL、Cypher、SPARQL 或 Datalog)都是 **宣告式** 的,這意味著你指定所需資料的模式 —— +> 結果必須滿足什麼條件,以及你希望如何轉換資料(例如,排序、分組和聚合)—— 但不指定 **如何** 實現該目標。 +> 資料庫系統的查詢最佳化器可以決定使用哪些索引和哪些連線演算法,以及以什麼順序執行查詢的各個部分。 +> +> 相比之下,使用大多數程式語言,你必須編寫一個 **演算法** —— 即告訴計算機以什麼順序執行哪些操作。 +> 宣告式查詢語言很有吸引力,因為它通常更簡潔,比顯式演算法更容易編寫。 +> 但更重要的是,它還隱藏了查詢引擎的實現細節,這使得資料庫系統可以在不需要更改任何查詢的情況下引入效能改進 [^1]。 +> +> 例如,資料庫可能能夠跨多個 CPU 核心和機器並行執行宣告式查詢,而你無需擔心如何實現該並行性 [^2]。 +> 在手寫演算法中,實現這種並行執行將需要大量的工作。 -------- ## 關係模型與文件模型 {#sec_datamodels_history} -如今最知名的資料模型可能是 SQL,它基於 Edgar Codd 於 1970 年提出的關係模型 [^3]:資料被組織成 **關係**(SQL 中稱為 **表**),其中每個關係是 **元組**(SQL 中的 **行**)的無序集合。 +今天最廣為人知的資料模型可能是 SQL,它基於 Edgar Codd 在 1970 年提出的關係模型 [^3]: +資料被組織成 **關係**(在 SQL 中稱為 **表**),其中每個關係是 **元組**(在 SQL 中稱為 **行**)的無序集合。 -關係模型最初是一個理論性的提議,當時許多人懷疑它是否能夠有效地實現。然而,到 20 世紀 80 年代中期,關係資料庫管理系統(RDBMS)和 SQL 已經成為大多數需要儲存和查詢某種規則結構資料的人的首選工具。幾十年後,許多資料管理用例仍然由關係資料主導——例如,商業分析(參見["星型和雪花型:分析模式"](/tw/ch3#sec_datamodels_analytics))。 +關係模型最初是一個理論提議,當時許多人懷疑它是否能夠高效實現。 +然而,到 20 世紀 80 年代中期,關係資料庫管理系統(RDBMS)和 SQL 已成為大多數需要儲存和查詢具有某種規則結構的資料的人的首選工具。 +許多資料管理用例在幾十年後仍然由關係資料主導 —— 例如,商業分析(參見 ["星型與雪花型:分析模式"](/tw/ch3#sec_datamodels_analytics))。 -多年來,有許多競爭性的資料儲存和查詢方法。在 20 世紀 70 年代和 80 年代初,**網路模型** 和 **層次模型** 是主要的替代方案,但關係模型最終佔據了主導地位。物件資料庫在 20 世紀 80 年代末和 90 年代初出現又消失。XML 資料庫出現在 2000 年代初,但只在小眾領域被採用。關係模型的每個競爭者在其時代都產生了大量的炒作,但從未持續 [^4]。相反,SQL 已經發展到在其關係核心之外包含其他資料型別——例如,增加了對 XML、JSON 和圖資料的支援 [^5]。 +多年來,出現了許多與資料儲存和查詢相關的競爭方法。在 20 世紀 70 年代和 80 年代初,**網狀模型** 和 **層次模型** 是主要的替代方案,但關係模型最終戰勝了它們。 +物件資料庫在 20 世紀 80 年代末和 90 年代初出現又消失。XML 資料庫在 21 世紀初出現,但只獲得了小眾的採用。 +每個關係模型的競爭者在其時代都產生了大量的炒作,但都沒有持續下去 [^4]。 +相反,SQL 已經發展到在其關係核心之外納入其他資料型別 —— 例如,增加了對 XML、JSON 和圖資料的支援 [^5]。 -在 2010 年代,**NoSQL** 是試圖推翻關係資料庫主導地位的最新流行語。NoSQL 指的不是單一技術,而是圍繞新資料模型、模式靈活性、可擴充套件性以及向開源許可模式轉變的一系列鬆散的想法。一些資料庫將自己標榜為 **NewSQL**,因為它們旨在提供 NoSQL 系統的可擴充套件性以及傳統關係資料庫的資料模型和事務保證。NoSQL 和 NewSQL 的想法在資料系統設計中非常有影響力,但隨著這些原則被廣泛採用,這些術語的使用已經淡化。 +在 2010 年代,**NoSQL** 是試圖推翻關係資料庫主導地位的最新流行詞。 +NoSQL 指的不是單一技術,而是圍繞新資料模型、模式靈活性、可伸縮性以及向開源許可模式轉變的一系列鬆散的想法。 +一些資料庫將自己標榜為 *NewSQL*,因為它們旨在提供 NoSQL 系統的可伸縮性以及傳統關係資料庫的資料模型和事務保證。 +NoSQL 和 NewSQL 的想法在資料系統設計中產生了很大的影響,但隨著這些原則被廣泛採用,這些術語的使用已經減少。 -NoSQL 運動的一個持久影響是 **文件模型** 的普及,它通常將資料表示為 JSON。這個模型最初由專門的文件資料庫(如 MongoDB 和 Couchbase)推廣,儘管大多數關係資料庫現在也添加了 JSON 支援。與關係表相比(關係表通常被視為具有剛性和不靈活的模式),JSON 文件被認為更加靈活。 +NoSQL 運動的一個持久影響是 **文件模型** 的流行,它通常將資料表示為 JSON。 +這個模型最初由專門的文件資料庫(如 MongoDB 和 Couchbase)推廣,儘管大多數關係資料庫現在也增加了 JSON 支援。 +與通常被視為具有嚴格和不靈活模式的關係表相比,JSON 文件被認為更加靈活。 -文件和關係資料的優缺點已經被廣泛討論;讓我們來研究一下這場辯論的一些關鍵要點。 +文件和關係資料的優缺點已經被廣泛討論;讓我們來看看該辯論的一些關鍵點。 ### 物件關係不匹配 {#sec_datamodels_document} -今天的大部分應用程式開發都是用物件導向的程式語言完成的,這導致了對 SQL 資料模型的一個常見批評:如果資料儲存在關係表中,則需要在應用程式程式碼中的物件和資料庫的表、行、列模型之間建立一個笨拙的轉換層。模型之間的這種脫節有時被稱為 **阻抗不匹配**。 +如今,大部分應用程式開發都是使用物件導向的程式語言完成的,這導致了對 SQL 資料模型的常見批評:如果資料儲存在關係表中,則需要在應用程式程式碼中的物件和資料庫的表、行、列模型之間建立一個笨拙的轉換層。這種模型之間的脫節有時被稱為 *阻抗不匹配*。 -------- > [!NOTE] -> **阻抗不匹配** 這個術語借用自電子學。每個電路的輸入和輸出都有一定的阻抗(對交流電的阻力)。當你將一個電路的輸出連線到另一個電路的輸入時,如果兩個電路的輸出和輸入阻抗匹配,則連線上的功率傳輸將被最大化。阻抗不匹配可能導致訊號反射和其他問題。 +> 術語 *阻抗不匹配* 借自電子學。每個電路的輸入和輸出都有一定的阻抗(對交流電的阻力)。當你將一個電路的輸出連線到另一個電路的輸入時,如果兩個電路的輸出和輸入阻抗匹配,則透過連線的功率傳輸將最大化。阻抗不匹配可能導致訊號反射和其他問題。 -------- #### 物件關係對映(ORM) {#object-relational-mapping-orm} -像 ActiveRecord 和 Hibernate 這樣的物件關係對映(ORM)框架減少了這個轉換層所需的樣板程式碼量,但它們經常受到批評 [^6]。一些常被提到的問題是: +物件關係對映(ORM)框架(如 ActiveRecord 和 Hibernate)減少了這個轉換層所需的樣板程式碼量,但它們經常受到批評 [^6]。一些常見的問題包括: -* ORM 很複雜,不能完全隱藏兩個模型之間的差異,因此開發人員最終仍然需要考慮資料的關係表示和物件表示。 -* ORM 通常只用於 OLTP 應用程式開發(參見["描述事務處理和分析"](/tw/ch1#sec_introduction_oltp));為分析目的提供資料的資料工程師仍然需要使用底層的關係表示,因此在使用 ORM 時關係模式的設計仍然很重要。 -* 許多 ORM 只適用於關係型 OLTP 資料庫。擁有多樣化資料系統(如搜尋引擎、圖資料庫和 NoSQL 系統)的組織可能會發現 ORM 支援不足。 -* 一些 ORM 會自動生成關係模式,但這些模式對於直接訪問關係資料的使用者來說可能很彆扭,而且在底層資料庫上可能效率低下。自定義 ORM 的模式和查詢生成可能很複雜,並且會抵消使用 ORM 的好處。 -* ORM 使得很容易意外地編寫低效的查詢,例如 **N+1 查詢問題** [^7]。例如,假設你想在頁面上顯示使用者評論列表,所以你執行一個查詢返回 *N* 條評論,每條評論包含其作者的 ID。要顯示評論作者的姓名,你需要在 users 表中查詢 ID。在手寫 SQL 中,你可能會在查詢中執行這個連線,並將作者姓名與每條評論一起返回,但使用 ORM 時,你可能最終會為每條 *N* 條評論在 users 表上進行單獨的查詢來查詢其作者,總共產生 *N*+1 個數據庫查詢,這比在資料庫中執行連線要慢。為了避免這個問題,你可能需要告訴 ORM 在獲取評論的同時獲取作者資訊。 +* ORM 很複雜,無法完全隱藏兩種模型之間的差異,因此開發人員仍然需要考慮資料的關係和物件表示。 +* ORM 通常僅用於 OLTP 應用程式開發(參見 ["表徵事務處理和分析"](/tw/ch1#sec_introduction_oltp));為分析目的提供資料的資料工程師仍然需要使用底層的關係表示,因此在使用 ORM 時,關係模式的設計仍然很重要。 +* 許多 ORM 僅適用於關係型 OLTP 資料庫。擁有多樣化資料系統(如搜尋引擎、圖資料庫和 NoSQL 系統)的組織可能會發現 ORM 支援不足。 +* 一些 ORM 會自動生成關係模式,但這些模式對於直接訪問關係資料的使用者來說可能很尷尬,並且在底層資料庫上可能效率低下。自定義 ORM 的模式和查詢生成可能很複雜,並否定了首先使用 ORM 的好處。 +* ORM 使得意外編寫低效查詢變得容易,例如 *N+1 查詢問題* [^7]。例如,假設你想在頁面上顯示使用者評論列表,因此你執行一個返回 *N* 條評論的查詢,每條評論都包含其作者的 ID。要顯示評論作者的姓名,你需要在使用者表中查詢 ID。在手寫 SQL 中,你可能會在查詢中執行此連線並返回每個評論的作者姓名,但使用 ORM 時,你可能最終會為 *N* 條評論中的每一條在使用者表上進行單獨的查詢以查詢其作者,總共產生 *N*+1 個數據庫查詢,這比在資料庫中執行連線要慢。為了避免這個問題,你可能需要告訴 ORM 在獲取評論的同時獲取作者資訊。 -儘管如此,ORM 也有優勢: +然而,ORM 也有優勢: -* 對於非常適合關係模型的資料,持久化關係表示和記憶體物件表示之間的某種轉換是不可避免的,ORM 減少了這種轉換所需的樣板程式碼量。複雜的查詢可能仍然需要在 ORM 之外處理,但 ORM 可以幫助處理簡單和重複的情況。 -* 一些 ORM 幫助快取資料庫查詢的結果,這可以幫助減少資料庫的負載。 +* 對於非常適合關係模型的資料,持久關係和記憶體物件表示之間的某種轉換是不可避免的,ORM 減少了這種轉換所需的樣板程式碼量。複雜的查詢可能仍然需要在 ORM 之外處理,但 ORM 可以幫助處理簡單和重複的情況。 +* 一些 ORM 有助於快取資料庫查詢的結果,這可以幫助減少資料庫的負載。 * ORM 還可以幫助管理模式遷移和其他管理活動。 #### 用於一對多關係的文件資料模型 {#the-document-data-model-for-one-to-many-relationships} -並非所有資料都適合關係表示;讓我們看一個例子來探討關係模型的侷限性。[圖 3-1](/tw/ch3#fig_obama_relational) 說明了如何在關係模式中表達簡歷(LinkedIn 個人資料)。整個資料可以透過唯一識別符號 `user_id` 來識別。像 `first_name` 和 `last_name` 這樣的欄位每個使用者只出現一次,因此可以將它們建模為 `users` 表上的列。 +並非所有資料都很適合關係表示;讓我們透過一個例子來探討關係模型的侷限性。[圖 3-1](/tw/ch3#fig_obama_relational) 說明了如何在關係模式中表達簡歷(LinkedIn 個人資料)。整個個人資料可以透過唯一識別符號 `user_id` 來識別。像 `first_name` 和 `last_name` 這樣的欄位每個使用者只出現一次,因此它們可以建模為 `users` 表上的列。 -大多數人在職業生涯中有多份工作(職位),人們可能有不同數量的教育經歷和任意數量的聯絡資訊。表示這種 **一對多關係** 的一種方法是將職位、教育和聯絡資訊放在單獨的表中,並引用 `users` 表的外部索引鍵,如[圖 3-1](/tw/ch3#fig_obama_relational) 所示。 +大多數人在職業生涯中有多份工作(職位),人們可能有不同數量的教育經歷和任意數量的聯絡資訊。表示這種 *一對多關係* 的一種方法是將職位、教育和聯絡資訊放在單獨的表中,並使用外部索引鍵引用 `users` 表,如 [圖 3-1](/tw/ch3#fig_obama_relational) 所示。 {{< figure src="/fig/ddia_0301.png" id="fig_obama_relational" caption="圖 3-1. 使用關係模式表示 LinkedIn 個人資料。" class="w-full my-4" >}} -表示相同資訊的另一種方式(這可能更自然,更貼近應用程式程式碼中的物件結構)是作為 JSON 文件,如[示例 3-1](/tw/ch3#fig_obama_json) 所示。 +另一種表示相同資訊的方式,可能更自然並且更接近應用程式程式碼中的物件結構,是作為 JSON 文件,如 [示例 3-1](/tw/ch3#fig_obama_json) 所示。 {{< figure id="fig_obama_json" title="示例 3-1. 將 LinkedIn 個人資料表示為 JSON 文件" class="w-full my-4" >}} @@ -111,38 +127,38 @@ NoSQL 運動的一個持久影響是 **文件模型** 的普及,它通常將 } ``` -一些開發人員認為 JSON 模型減少了應用程式程式碼和儲存層之間的阻抗不匹配。然而,正如我們將在[第 5 章](/tw/ch5#ch_encoding)中看到的,JSON 作為資料編碼格式也存在問題。缺乏模式通常被認為是一個優勢;我們將在["文件模型中的模式靈活性"](/tw/ch3#sec_datamodels_schema_flexibility)中討論這一點。 +一些開發人員認為 JSON 模型減少了應用程式程式碼和儲存層之間的阻抗不匹配。然而,正如我們將在 [第 5 章](/tw/ch5#ch_encoding) 中看到的,JSON 作為資料編碼格式也存在問題。缺乏模式通常被認為是一個優勢;我們將在 ["文件模型中的模式靈活性"](/tw/ch3#sec_datamodels_schema_flexibility) 中討論這個問題。 -與[圖 3-1](/tw/ch3#fig_obama_relational) 中的多表模式相比,JSON 表示具有更好的 **區域性**(參見["讀寫的資料區域性"](/tw/ch3#sec_datamodels_document_locality))。如果你想在關係示例中獲取個人資料,你需要執行多個查詢(按 `user_id` 查詢每個表)或在 `users` 表及其從屬表之間執行混亂的多路連線 [^8]。在 JSON 表示中,所有相關資訊都在一個地方,使查詢既更快又更簡單。 +與 [圖 3-1](/tw/ch3#fig_obama_relational) 中的多表模式相比,JSON 表示具有更好的 *區域性*(參見 ["讀寫的資料區域性"](/tw/ch3#sec_datamodels_document_locality))。如果你想在關係示例中獲取個人資料,你需要執行多個查詢(透過 `user_id` 查詢每個表)或在 `users` 表與其從屬表之間執行混亂的多路連線 [^8]。在 JSON 表示中,所有相關資訊都在一個地方,使查詢既更快又更簡單。 -從使用者個人資料到使用者職位、教育歷史和聯絡資訊的一對多關係意味著資料中存在樹形結構,JSON 表示使這種樹形結構明確(參見[圖 3-2](/tw/ch3#fig_json_tree))。 +從使用者個人資料到使用者職位、教育歷史和聯絡資訊的一對多關係暗示了資料中的樹形結構,而 JSON 表示使這種樹形結構變得明確(見 [圖 3-2](/tw/ch3#fig_json_tree))。 -{{< figure src="/fig/ddia_0302.png" id="fig_json_tree" caption="圖 3-2. 形成樹形結構的一對多關係。" class="w-full my-4" >}} +{{< figure src="/fig/ddia_0302.png" id="fig_json_tree" caption="圖 3-2. 一對多關係形成樹狀結構。" class="w-full my-4" >}} -------- > [!NOTE] -> 這種關係有時被稱為 **一對少** 而不是 **一對多**,因為簡歷通常只有少量的職位 [^9] [^10]。在可能有大量相關項的情況下——比如名人社交媒體帖子上的評論,可能有數千條——將它們全部嵌入同一文件中可能太笨拙,因此[圖 3-1](/tw/ch3#fig_obama_relational) 中的關係方法更可取。 +> 這種型別的關係有時被稱為 *一對少* 而不是 *一對多*,因為簡歷通常有少量的職位 [^9] [^10]。在可能存在真正大量相關專案的情況下 —— 比如名人社交媒體帖子上的評論,可能有成千上萬條 —— 將它們全部嵌入同一個文件中可能太笨拙了,因此 [圖 3-1](/tw/ch3#fig_obama_relational) 中的關係方法更可取。 -------- ### 規範化、反規範化與連線 {#sec_datamodels_normalization} -在前一節的[示例 3-1](/tw/ch3#fig_obama_json) 中,`region_id` 被給出為 ID,而不是純文字字串 `"Washington, DC, United States"`。為什麼? +在前一節的 [示例 3-1](/tw/ch3#fig_obama_json) 中,`region_id` 被給出為 ID,而不是純文字字串 `"Washington, DC, United States"`。為什麼? -如果使用者介面有一個用於輸入地區的自由文字欄位,將其儲存為純文字字串是有意義的。但是,擁有標準化的地理區域列表並讓使用者從下拉列表或自動完成中選擇是有優勢的: +如果使用者介面有一個用於輸入地區的自由文字欄位,將其儲存為純文字字串是有意義的。但是,擁有標準化的地理區域列表並讓使用者從下拉列表或自動完成中選擇有其優勢: -* 跨個人資料的風格和拼寫一致 -* 避免歧義(如果有多個同名的地方)(如果字串只是 "Washington",它是指 DC 還是州?) -* 易於更新——名稱只儲存在一個地方,因此如果需要更改(例如,由於政治事件導致的城市名稱更改),很容易全面更新 -* 本地化支援——當網站被翻譯成其他語言時,標準化列表可以被本地化,因此可以用檢視者的語言顯示地區 -* 更好的搜尋——例如,搜尋美國東海岸的人可以匹配這個個人資料,因為區域列表可以編碼華盛頓位於東海岸的事實(這從字串 `"Washington, DC"` 中並不明顯) +* 跨個人資料的風格和拼寫一致性 +* 避免歧義,如果有幾個同名的地方(如果字串只是 "Washington",它是指 DC 還是州?) +* 易於更新 —— 名稱只儲存在一個地方,因此如果需要更改(例如,由於政治事件而更改城市名稱),可以輕鬆地全面更新 +* 本地化支援 —— 當網站被翻譯成其他語言時,標準化列表可以被本地化,因此區域可以用檢視者的語言顯示 +* 更好的搜尋 —— 例如,搜尋美國東海岸的人可以匹配此個人資料,因為區域列表可以編碼華盛頓位於東海岸的事實(這從字串 `"Washington, DC"` 中並不明顯) -是儲存 ID 還是文字字串是 **規範化** 的問題。當你使用 ID 時,你的資料更加規範化:對人類有意義的資訊(如文字 *Washington, DC*)只儲存在一個地方,所有引用它的地方都使用 ID(它只在資料庫內有意義)。當你直接儲存文字時,你在每個使用它的記錄中複製了對人類有意義的資訊;這種表示是 **反規範化** 的。 +無論你儲存 ID 還是文字字串,這都是 *規範化* 的問題。當你使用 ID 時,你的資料更加規範化:對人類有意義的資訊(如文字 *Washington, DC*)只儲存在一個地方,所有引用它的地方都使用 ID(它只在資料庫中有意義)。當你直接儲存文字時,你在使用它的每條記錄中都複製了對人類有意義的資訊;這種表示是 *反規範化* 的。 -使用 ID 的優勢在於,因為它對人類沒有意義,所以永遠不需要更改:即使它標識的資訊發生變化,ID 也可以保持不變。任何對人類有意義的東西在未來的某個時候可能需要更改——如果該資訊被複制,所有冗餘副本都需要更新。這需要更多的程式碼、更多的寫操作、更多的磁碟空間,並且存在不一致的風險(某些資訊副本被更新但其他副本沒有)。 +使用 ID 的優勢在於,因為它對人類沒有意義,所以永遠不需要更改:即使它標識的資訊發生變化,ID 也可以保持不變。任何對人類有意義的東西將來某個時候可能需要更改 —— 如果該資訊被複制,所有冗餘副本都需要更新。這需要更多的程式碼、更多的寫操作、更多的磁碟空間,並且存在不一致的風險(其中一些資訊副本被更新但其他的沒有)。 -規範化表示的缺點是,每次你想顯示包含 ID 的記錄時,都必須執行額外的查詢以將 ID 解析為人類可讀的內容。在關係資料模型中,這是使用 **連線** 完成的,例如: +規範化表示的缺點是,每次要顯示包含 ID 的記錄時,都必須進行額外的查詢以將 ID 解析為人類可讀的內容。在關係資料模型中,這是使用 *連線* 完成的,例如: ```sql SELECT users.*, regions.region_name @@ -151,7 +167,7 @@ SELECT users.*, regions.region_name WHERE users.id = 251; ``` -文件資料庫可以儲存規範化和反規範化的資料,但它們通常與反規範化相關聯——部分是因為 JSON 資料模型使得儲存額外的反規範化欄位變得容易,部分是因為許多文件資料庫對連線的支援較弱,使得規範化不方便。一些文件資料庫根本不支援連線,因此你必須在應用程式程式碼中執行它們——也就是說,你首先獲取包含 ID 的文件,然後執行第二個查詢以將該 ID 解析為另一個文件。在 MongoDB 中,也可以使用聚合管道中的 `$lookup` 運算子執行連線: +文件資料庫可以儲存規範化和反規範化的資料,但它們通常與反規範化相關聯 —— 部分是因為 JSON 資料模型使得儲存額外的反規範化欄位變得容易,部分是因為許多文件資料庫中對連線的弱支援使得規範化不方便。一些文件資料庫根本不支援連線,因此你必須在應用程式程式碼中執行它們 —— 也就是說,你首先獲取包含 ID 的文件,然後執行第二個查詢將該 ID 解析為另一個文件。在 MongoDB 中,也可以使用聚合管道中的 `$lookup` 運算子執行連線: ```mongodb-json db.users.aggregate([ @@ -167,24 +183,24 @@ db.users.aggregate([ #### 規範化的權衡 {#trade-offs-of-normalization} -在簡歷示例中,雖然 `region_id` 欄位是對標準化區域集的引用,但 `organization`(人工作過的公司或政府)的名稱和 `school_name`(他們學習的地方)只是字串。這種表示是反規範化的:許多人可能在同一家公司工作過,但沒有 ID 將他們聯絡起來。 +在簡歷示例中,雖然 `region_id` 欄位是對標準化區域集的引用,但 `organization`(人工作的公司或政府)和 `school_name`(他們學習的地方)的名稱只是字串。這種表示是反規範化的:許多人可能在同一家公司工作過,但沒有 ID 將他們聯絡起來。 -也許組織和學校應該是實體,個人資料應該引用它們的 ID 而不是它們的名稱?引用區域 ID 的相同論點也適用於此。例如,假設我們想要除了名稱之外還包括學校或公司的徽標: +也許組織和學校應該是實體,個人資料應該引用它們的 ID 而不是它們的名稱?引用區域 ID 的相同論點也適用於此。例如,假設我們想在他們的名字之外包括學校或公司的標誌: -* 在反規範化的表示中,我們會在每個人的個人資料中包含徽標的影像 URL;這使得 JSON 文件自包含,但如果我們需要更改徽標,就會很頭疼,因為我們現在需要找到舊 URL 的所有出現並更新它們 [^9]。 -* 在規範化的表示中,我們將建立一個代表組織或學校的實體,並在該實體上儲存其名稱、徽標 URL 以及可能的其他屬性(描述、新聞源等)一次。每個提到該組織的簡歷都只需引用其 ID,更新徽標很容易。 +* 在反規範化表示中,我們會在每個人的個人資料中包含標誌的影像 URL;這使得 JSON 文件自包含,但如果我們需要更改標誌,就會產生麻煩,因為我們現在需要找到舊 URL 的所有出現並更新它們 [^9]。 +* 在規範化表示中,我們將建立一個代表組織或學校的實體,並在該實體上儲存其名稱、標誌 URL 以及可能的其他屬性(描述、新聞提要等)一次。然後,每個提到該組織的簡歷都會簡單地引用其 ID,更新標誌很容易。 -作為一般原則,規範化的資料通常寫入更快(因為只有一個副本),但查詢更慢(因為需要連線);反規範化的資料通常讀取更快(連線更少),但寫入更昂貴(更多副本需要更新,使用更多磁碟空間)。你可能會發現將反規範化視為派生資料的一種形式很有幫助(["記錄系統和派生資料"](/tw/ch1#sec_introduction_derived)),因為你需要設定一個過程來更新資料的冗餘副本。 +作為一般原則,規範化資料通常寫入更快(因為只有一個副本),但查詢更慢(因為它需要連線);反規範化資料通常讀取更快(連線更少),但寫入更昂貴(更多副本要更新,使用更多磁碟空間)。你可能會發現將反規範化視為派生資料的一種形式很有幫助(["記錄系統和派生資料"](/tw/ch1#sec_introduction_derived)),因為你需要設定一個過程來更新資料的冗餘副本。 -除了執行所有這些更新的成本之外,你還需要考慮如果程序在進行更新的中途崩潰,資料庫的一致性。提供原子事務的資料庫(參見["原子性"](/tw/ch8#sec_transactions_acid_atomicity))使保持一致性變得更容易,但並非所有資料庫都提供跨多個文件的原子性。也可以透過流處理來確保一致性,我們將在 [連結待定] 中討論。 +除了執行所有這些更新的成本之外,如果程序在進行更新的過程中崩潰,你還需要考慮資料庫的一致性。提供原子事務的資料庫(參見 ["原子性"](/tw/ch8#sec_transactions_acid_atomicity))使保持一致性變得更容易,但並非所有資料庫都在多個文件之間提供原子性。透過流處理確保一致性也是可能的,我們將在 [待補充連結] 中討論。 -規範化往往更適合 OLTP 系統,其中讀取和更新都需要快速;分析系統通常使用反規範化資料表現更好,因為它們批次執行更新,只讀查詢的效能是主要關注點。此外,在中小規模的系統中,規範化的資料模型通常是最好的,因為你不必擔心保持資料的多個副本彼此一致,執行連線的成本是可以接受的。然而,在非常大規模的系統中,連線的成本可能會成為問題。 +規範化往往更適合 OLTP 系統,其中讀取和更新都需要快速;分析系統通常使用反規範化資料表現更好,因為它們批次執行更新,只讀查詢的效能是主要關注點。此外,在中小規模的系統中,規範化資料模型通常是最好的,因為你不必擔心保持資料的多個副本相互一致,執行連線的成本是可以接受的。然而,在非常大規模的系統中,連線的成本可能會成為問題。 #### 社交網路案例研究中的反規範化 {#denormalization-in-the-social-networking-case-study} -在["案例研究:社交網路主頁時間線"](/tw/ch2#sec_introduction_twitter)中,我們比較了規範化表示([圖 2-1](/tw/ch2#fig_twitter_relational))和反規範化表示(預計算、物化的時間線):這裡,`posts` 和 `follows` 之間的連線太昂貴了,物化時間線是該連線結果的快取。將新帖子插入關注者時間線的扇出過程是我們保持反規範化表示一致的方式。 +在 ["案例研究:社交網路主頁時間線"](/tw/ch2#sec_introduction_twitter) 中,我們比較了規範化表示([圖 2-1](/tw/ch2#fig_twitter_relational))和反規範化表示(預計算的物化時間線):這裡,`posts` 和 `follows` 之間的連線太昂貴了,物化時間線是該連線結果的快取。將新帖子插入關注者時間線的扇出過程是我們保持反規範化表示一致的方式。 -然而,X(前 Twitter)的物化時間線實現實際上並不儲存每個帖子的實際文字:每個條目實際上只儲存帖子 ID、釋出它的使用者的 ID,以及一些額外的資訊來識別轉發和回覆 [^11]。換句話說,它是(大約)以下查詢的預計算結果: +然而,X(前 Twitter)的物化時間線實現實際上並不儲存每個帖子的實際文字:每個條目實際上只儲存帖子 ID、釋出者的使用者 ID,以及一些額外的資訊來識別轉發和回覆 [^11]。換句話說,它是(大約)以下查詢的預計算結果: ```sql SELECT posts.id, posts.sender_id @@ -195,23 +211,23 @@ SELECT posts.id, posts.sender_id LIMIT 1000 ``` -這意味著每當讀取時間線時,服務仍然需要執行兩個連線:透過 ID 查詢帖子以獲取實際的帖子內容(以及喜歡和回覆數等統計資訊),並透過 ID 查詢傳送者的個人資料(獲取他們的使用者名稱、頭像和其他詳細資訊)。這個透過 ID 查詢人類可讀資訊的過程稱為 **水合**(hydrating)ID,它本質上是在應用程式程式碼中執行的連線 [^11]。 +這意味著每當讀取時間線時,服務仍然需要執行兩個連線:透過 ID 查詢帖子以獲取實際的帖子內容(以及點贊數和回覆數等統計資訊),並透過 ID 查詢傳送者的個人資料(以獲取他們的使用者名稱、個人資料圖片和其他詳細資訊)。這個透過 ID 查詢人類可讀資訊的過程稱為 *hydrating* ID,它本質上是在應用程式程式碼中執行的連線 [^11]。 -在預計算時間線中只儲存 ID 的原因是它們引用的資料變化很快:熱門帖子的點贊和回覆數可能每秒變化多次,一些使用者經常更改他們的使用者名稱或頭像。由於時間線在檢視時應該顯示最新的點贊數和頭像,因此將這些資訊反規範化到物化時間線中是沒有意義的。此外,這種反規範化會顯著增加儲存成本。 +在預計算時間線中僅儲存 ID 的原因是它們引用的資料變化很快:熱門帖子的點贊數和回覆數可能每秒變化多次,一些使用者定期更改他們的使用者名稱或個人資料照片。由於時間線在檢視時應該顯示最新的點贊數和個人資料圖片,因此將此資訊反規範化到物化時間線中是沒有意義的。此外,這種反規範化會顯著增加儲存成本。 -這個例子表明,在讀取資料時必須執行連線並不像有時聲稱的那樣,是建立高效能、可擴充套件服務的障礙。水合帖子 ID 和使用者 ID 實際上是一個相當容易擴充套件的操作,因為它很好地並行化,成本不取決於你關注的賬戶數或你擁有的關注者數。 +這個例子表明,在讀取資料時必須執行連線並不像有時聲稱的那樣,是建立高效能、可擴充套件服務的障礙。Hydrating 帖子 ID 和使用者 ID 實際上是一個相當容易擴充套件的操作,因為它可以很好地並行化,並且成本不取決於你關注的帳戶數量或你擁有的關注者數量。 -如果你需要決定是否在應用程式中反規範化某些內容,社交網路案例研究表明選擇並不是立即顯而易見的:最可擴充套件的方法可能涉及反規範化一些東西並保持其他東西規範化。你必須仔細考慮資訊變化的頻率,以及讀取和寫入的成本(這可能由異常值主導,例如在典型社交網路的情況下具有許多關注/關注者的使用者)。規範化和反規範化本質上並不好或壞——它們只是在讀取和寫入效能以及實現工作量方面的權衡。 +如果你需要決定是否在應用程式中反規範化某些內容,社交網路案例研究表明選擇並不是立即顯而易見的:最可擴充套件的方法可能涉及反規範化某些內容並保持其他內容規範化。你必須仔細考慮資訊更改的頻率以及讀寫成本(這可能由異常值主導,例如在典型社交網路的情況下擁有許多關注/關注者的使用者)。規範化和反規範化本質上並不好或壞 —— 它們只是在讀寫效能以及實施工作量方面的權衡。 ### 多對一與多對多關係 {#sec_datamodels_many_to_many} -雖然[圖 3-1](/tw/ch3#fig_obama_relational) 中的 `positions` 和 `education` 是一對多或一對少關係的例子(一份簡歷有多個職位,但每個職位只屬於一份簡歷),`region_id` 欄位是 **多對一** 關係的例子(許多人住在同一個地區,但我們假設每個人在任何時候只住在一個地區)。 +雖然 [圖 3-1](/tw/ch3#fig_obama_relational) 中的 `positions` 和 `education` 是一對多或一對少關係的例子(一份簡歷有多個職位,但每個職位只屬於一份簡歷),但 `region_id` 欄位是 *多對一* 關係的例子(許多人住在同一個地區,但我們假設每個人在任何時候只住在一個地區)。 -如果我們為組織和學校引入實體,並從簡歷中透過 ID 引用它們,那麼我們也有 **多對多** 關係(一個人為幾個組織工作過,一個組織有幾個過去或現在的員工)。在關係模型中,這種關係通常表示為 **關聯表** 或 **連線表**,如[圖 3-3](/tw/ch3#fig_datamodels_m2m_rel) 所示:每個職位將一個使用者 ID 與一個組織 ID 關聯起來。 +如果我們為組織和學校引入實體,並透過 ID 從簡歷中引用它們,那麼我們也有 *多對多* 關係(一個人曾為多個組織工作,一個組織有多個過去或現在的員工)。在關係模型中,這種關係通常表示為 *關聯表* 或 *連線表*,如 [圖 3-3](/tw/ch3#fig_datamodels_m2m_rel) 所示:每個職位將一個使用者 ID 與一個組織 ID 關聯起來。 {{< figure src="/fig/ddia_0303.png" id="fig_datamodels_m2m_rel" caption="圖 3-3. 關係模型中的多對多關係。" class="w-full my-4" >}} -多對一和多對多關係不容易適應一個自包含的 JSON 文件;它們更適合規範化的表示。在文件模型中,一種可能的表示在[示例 3-2](/tw/ch3#fig_datamodels_m2m_json) 中給出,並在[圖 3-4](/tw/ch3#fig_datamodels_many_to_many) 中說明:每個虛線矩形內的資料可以分組到一個文件中,但與組織和學校的連結最好表示為對其他文件的引用。 +多對一和多對多關係不容易適應一個自包含的 JSON 文件;它們更適合規範化表示。在文件模型中,一種可能的表示如 [示例 3-2](/tw/ch3#fig_datamodels_m2m_json) 所示,並在 [圖 3-4](/tw/ch3#fig_datamodels_many_to_many) 中說明:每個虛線矩形內的資料可以分組到一個文件中,但到組織和學校的連結最好表示為對其他文件的引用。 {{< figure id="fig_datamodels_m2m_json" title="示例 3-2. 透過 ID 引用組織的簡歷。" class="w-full my-4" >}} @@ -230,68 +246,68 @@ SELECT posts.id, posts.sender_id {{< figure src="/fig/ddia_0304.png" id="fig_datamodels_many_to_many" caption="圖 3-4. 文件模型中的多對多關係:每個虛線框內的資料可以分組到一個文件中。" class="w-full my-4" >}} -多對多關係通常需要在"兩個方向"上查詢:例如,查詢特定人員工作過的所有組織,以及查詢在特定組織工作過的所有人員。啟用此類查詢的一種方法是在兩側儲存 ID 引用,即簡歷包括該人工作過的每個組織的 ID,組織文件包括提到該組織的簡歷的 ID。這種表示是反規範化的,因為關係儲存在兩個地方,它們可能彼此不一致。 +多對多關係通常需要"雙向"查詢:例如,找到特定人員工作過的所有組織,以及找到在特定組織工作過的所有人員。啟用此類查詢的一種方法是在兩邊都儲存 ID 引用,即簡歷包含該人工作過的每個組織的 ID,組織文件包含提到該組織的簡歷的 ID。這種表示是反規範化的,因為關係儲存在兩個地方,可能會相互不一致。 -規範化的表示只在一個地方儲存關係,並依賴 **二級索引**(我們將在[第 4 章](/tw/ch4#ch_storage)中討論)來允許在兩個方向上有效地查詢關係。在[圖 3-3](/tw/ch3#fig_datamodels_m2m_rel) 的關係模式中,我們會告訴資料庫在 `positions` 表的 `user_id` 和 `org_id` 列上建立索引。 +規範化表示僅在一個地方儲存關係,並依賴 *二級索引*(我們將在 [第 4 章](/tw/ch4#ch_storage) 中討論)來允許有效地雙向查詢關係。在 [圖 3-3](/tw/ch3#fig_datamodels_m2m_rel) 的關係模式中,我們會告訴資料庫在 `positions` 表的 `user_id` 和 `org_id` 列上建立索引。 -在[示例 3-2](/tw/ch3#fig_datamodels_m2m_json) 的文件模型中,資料庫需要索引 `positions` 陣列內物件的 `org_id` 欄位。許多文件資料庫和支援 JSON 的關係資料庫能夠在文件內的值上建立這樣的索引。 +在 [示例 3-2](/tw/ch3#fig_datamodels_m2m_json) 的文件模型中,資料庫需要索引 `positions` 陣列內物件的 `org_id` 欄位。許多文件資料庫和具有 JSON 支援的關係資料庫能夠在文件內的值上建立此類索引。 ### 星型與雪花型:分析模式 {#sec_datamodels_analytics} -資料倉庫(參見["資料倉庫"](/tw/ch1#sec_introduction_dwh))通常是關係型的,資料倉庫中表結構有幾個廣泛使用的約定:**星型模式**、**雪花型模式**、**維度建模** [^12] 和 **一大表**(OBT)。這些結構針對業務分析師的需求進行了最佳化。ETL 過程將資料從作業系統轉換為此模式。 +資料倉庫(參見 ["資料倉庫"](/tw/ch1#sec_introduction_dwh))通常是關係型的,並且資料倉庫中表結構有一些廣泛使用的約定:*星型模式*、*雪花模式*、*維度建模* [^12],以及 *一張大表*(OBT)。這些結構針對業務分析師的需求進行了最佳化。ETL 過程將來自運營系統的資料轉換為此模式。 -[圖 3-5](/tw/ch3#fig_dwh_schema) 展示了一個可能在雜貨零售商資料倉庫中找到的星型模式示例。模式的中心是所謂的 **事實表**(在此示例中,它稱為 `fact_sales`)。事實表的每一行代表在特定時間發生的事件(這裡,每一行代表客戶購買產品)。如果我們分析的是網站流量而不是零售銷售,每一行可能代表使用者的頁面瀏覽或點選。 +[圖 3-5](/tw/ch3#fig_dwh_schema) 顯示了一個可能在雜貨零售商的資料倉庫中找到的星型模式示例。模式的中心是所謂的 *事實表*(在此示例中,它稱為 `fact_sales`)。事實表的每一行代表在特定時間發生的事件(這裡,每一行代表客戶購買產品)。如果我們分析的是網站流量而不是零售銷售,每一行可能代表使用者的頁面檢視或點選。 {{< figure src="/fig/ddia_0305.png" id="fig_dwh_schema" caption="圖 3-5. 用於資料倉庫的星型模式示例。" class="w-full my-4" >}} -通常,事實被捕獲為單個事件,因為這允許以後進行最大靈活性的分析。然而,這意味著事實表可能變得非常大。一個大型企業的資料倉庫中可能有許多 PB 的交易歷史,主要表示為事實表。 +通常,事實被捕獲為單個事件,因為這允許以後最大的分析靈活性。然而,這意味著事實表可能變得非常大。一個大型企業可能在其資料倉庫中有許多 PB 的交易歷史,主要表示為事實表。 -事實表中的一些列是屬性,例如產品的銷售價格和從供應商處購買的成本(允許計算利潤率)。事實表中的其他列是對其他表的外部索引鍵引用,稱為 **維度表**。由於事實表中的每一行都代表一個事件,維度代表事件的 **誰**、**什麼**、**哪裡**、**何時**、**如何** 和 **為什麼**。 +事實表中的一些列是屬性,例如產品售出的價格和從供應商那裡購買它的成本(允許計算利潤率)。事實表中的其他列是對其他表的外部索引鍵引用,稱為 *維度表*。由於事實表中的每一行代表一個事件,維度代表事件的 *誰*、*什麼*、*哪裡*、*何時*、*如何* 和 *為什麼*。 -例如,在[圖 3-5](/tw/ch3#fig_dwh_schema) 中,其中一個維度是所售產品。`dim_product` 表中的每一行代表一種待售產品型別,包括其庫存單位(SKU)、描述、品牌名稱、類別、脂肪含量、包裝大小等。`fact_sales` 表中的每一行使用外部索引鍵來指示在該特定交易中銷售的產品。查詢通常涉及對多個維度表的多個連線。 +例如,在 [圖 3-5](/tw/ch3#fig_dwh_schema) 中,其中一個維度是售出的產品。`dim_product` 表中的每一行代表一種待售產品型別,包括其庫存單位(SKU)、描述、品牌名稱、類別、脂肪含量、包裝尺寸等。`fact_sales` 表中的每一行使用外部索引鍵來指示在該特定交易中售出了哪種產品。查詢通常涉及對多個維度表的多個連線。 -甚至日期和時間通常使用維度表表示,因為這允許編碼有關日期的額外資訊(例如公共假期),允許查詢區分假期和非假期的銷售。 +即使日期和時間也經常使用維度表表示,因為這允許編碼有關日期的附加資訊(例如公共假期),允許查詢區分假期和非假期的銷售。 -[圖 3-5](/tw/ch3#fig_dwh_schema) 是星型模式的一個例子。該名稱來自這樣一個事實:當表關係被視覺化時,事實表位於中間,被其維度表包圍;與這些表的連線就像星星的光芒。 +[圖 3-5](/tw/ch3#fig_dwh_schema) 是星型模式的一個例子。該名稱來自這樣一個事實:當表關係被視覺化時,事實表位於中間,被其維度表包圍;到這些表的連線就像星星的光芒。 -這個模板的一個變體被稱為 **雪花型模式**,其中維度進一步分解為子維度。例如,品牌和產品類別可能有單獨的表,`dim_product` 表中的每一行可以將品牌和類別作為外部索引鍵引用,而不是將它們作為字串儲存在 `dim_product` 表中。雪花型模式比星型模式更規範化,但星型模式通常更受歡迎,因為它們對分析師來說更簡單 [^12]。 +這個模板的一個變體被稱為 *雪花模式*,其中維度被進一步分解為子維度。例如,品牌和產品類別可能有單獨的表,`dim_product` 表中的每一行都可以將品牌和類別作為外部索引鍵引用,而不是將它們作為字串儲存在 `dim_product` 表中。雪花模式比星型模式更規範化,但星型模式通常更受歡迎,因為它們對分析師來說更簡單 [^12]。 -在典型的資料倉庫中,表通常相當寬:事實表通常有 100 多列,有時有幾百列。維度表也可能很寬,因為它們包括可能與分析相關的所有元資料——例如,`dim_store` 表可能包括每個商店提供哪些服務的詳細資訊、是否有店內烘焙店、面積、商店首次開業的日期、最後一次改造的時間、離最近的高速公路有多遠等。 +在典型的資料倉庫中,表通常非常寬:事實表通常有超過 100 列,有時有幾百列。維度表也可能很寬,因為它們包括所有可能與分析相關的元資料 —— 例如,`dim_store` 表可能包括每個商店提供哪些服務的詳細資訊、是否有店內麵包房、平方英尺、商店首次開業的日期、最後一次改造的時間、距離最近的高速公路有多遠等。 -星型或雪花型模式主要由多對一關係組成(例如,許多銷售發生在一個特定產品、一個特定商店),表示為事實表具有指向維度表的外部索引鍵,或維度指向子維度。原則上,可能存在其他型別的關係,但它們通常被反規範化以簡化查詢。例如,如果客戶一次購買幾種不同的產品,則不會明確表示該多項交易;相反,事實表中為每個購買的產品都有一個單獨的行,這些事實都恰好具有相同的客戶 ID、商店 ID 和時間戳。 +星型或雪花模式主要由多對一關係組成(例如,許多銷售發生在一個特定產品,在一個特定商店),表示為事實表對維度表的外部索引鍵,或維度對子維度的外部索引鍵。原則上,其他型別的關係可能存在,但它們通常被反規範化以簡化查詢。例如,如果客戶一次購買多種不同的產品,則該多項交易不會被明確表示;相反,事實表中為每個購買的產品都有一個單獨的行,這些事實都恰好具有相同的客戶 ID、商店 ID 和時間戳。 -一些資料倉庫模式進一步採用反規範化,完全省略維度表,而是將維度中的資訊摺疊到事實表上的反規範化列中(本質上是預計算事實表和維度表之間的連線)。這種方法被稱為 **一大表**(OBT),雖然它需要更多的儲存空間,但有時可以實現更快的查詢 [^13]。 +一些資料倉庫模式進一步進行反規範化,完全省略維度表,將維度中的資訊摺疊到事實表上的反規範化列中(本質上是預計算事實表和維度表之間的連線)。這種方法被稱為 *一張大表*(OBT),雖然它需要更多的儲存空間,但有時可以實現更快的查詢 [^13]。 -在分析的上下文中,這種反規範化是沒有問題的,因為資料通常代表不會更改的歷史資料日誌(除了偶爾糾正錯誤)。OLTP 系統中反規範化出現的資料一致性和寫入開銷問題在分析中並不那麼緊迫。 +在分析的背景下,這種反規範化是沒有問題的,因為資料通常代表不會改變的歷史資料日誌(除了偶爾糾正錯誤)。OLTP 系統中反規範化出現的資料一致性和寫入開銷問題在分析中並不那麼緊迫。 ### 何時使用哪種模型 {#sec_datamodels_document_summary} -支援文件資料模型的主要論點是模式靈活性、由於區域性而獲得更好的效能,以及對於某些應用程式來說,它更接近應用程式使用的物件模型。關係模型透過提供對連線、多對一和多對多關係的更好支援來反駁。讓我們更詳細地研究這些論點。 +文件資料模型的主要論點是模式靈活性、由於區域性而獲得更好的效能,以及對於某些應用程式來說,它更接近應用程式使用的物件模型。關係模型透過為連線、多對一和多對多關係提供更好的支援來反擊。讓我們更詳細地研究這些論點。 -如果應用程式中的資料具有類似文件的結構(即一對多關係的樹,通常一次載入整個樹),那麼使用文件模型可能是個好主意。將類似文件的結構 **分解** 為多個表的關係技術(如[圖 3-1](/tw/ch3#fig_obama_relational) 中的 `positions`、`education` 和 `contact_info`)可能導致繁瑣的模式和不必要的複雜應用程式程式碼。 +如果你的應用程式中的資料具有類似文件的結構(即一對多關係的樹,通常一次載入整個樹),那麼使用文件模型可能是個好主意。將類似文件的結構 *切碎*(shredding)為多個表的關係技術(如 [圖 3-1](/tw/ch3#fig_obama_relational) 中的 `positions`、`education` 和 `contact_info`)可能導致繁瑣的模式和不必要複雜的應用程式程式碼。 -文件模型有侷限性:例如,你不能直接引用文件中的巢狀項,而是需要說類似"使用者 251 的職位列表中的第二項"。如果你確實需要引用巢狀項,關係方法效果更好,因為你可以透過其 ID 直接引用任何項。 +文件模型有侷限性:例如,你不能直接引用文件中的巢狀項,而是需要說類似"使用者 251 的職位列表中的第二項"之類的話。如果你確實需要引用巢狀項,關係方法效果更好,因為你可以透過其 ID 直接引用任何項。 -一些應用程式允許使用者選擇專案的順序:例如,想象一個待辦事項列表或問題跟蹤器,使用者可以拖放任務來重新排序它們。文件模型很好地支援這樣的應用程式,因為專案(或它們的 ID)可以簡單地儲存在 JSON 陣列中以確定它們的順序。在關係資料庫中,沒有表示這種可重新排序列表的標準方法,使用了各種技巧:按整數列排序(在中間插入時需要重新編號)、ID 的連結串列或分數索引 [^14] [^15] [^16]。 +一些應用程式允許使用者選擇專案的順序:例如,想象一個待辦事項列表或問題跟蹤器,使用者可以拖放任務來重新排序它們。文件模型很好地支援此類應用程式,因為專案(或它們的 ID)可以簡單地儲存在 JSON 陣列中以確定它們的順序。在關係資料庫中,沒有表示此類可重新排序列表的標準方法,並且使用各種技巧:按整數列排序(在插入中間時需要重新編號)、ID 的連結串列或分數索引 [^14] [^15] [^16]。 #### 文件模型中的模式靈活性 {#sec_datamodels_schema_flexibility} -大多數文件資料庫和關係資料庫中的 JSON 支援不對文件中的資料強制執行任何模式。關係資料庫中的 XML 支援通常帶有可選的模式驗證。沒有模式意味著可以向文件新增任意鍵和值,並且在讀取時,客戶端不能保證文件可能包含哪些欄位。 +大多數文件資料庫以及關係資料庫中的 JSON 支援不會對文件中的資料強制執行任何模式。關係資料庫中的 XML 支援通常帶有可選的模式驗證。沒有模式意味著可以將任意鍵和值新增到文件中,並且在讀取時,客戶端不能保證文件可能包含哪些欄位。 -文件資料庫有時被稱為 **無模式**,但這是誤導性的,因為讀取資料的程式碼通常假定某種結構——即存在隱式模式,但資料庫不強制執行 [^17]。更準確的術語是 **讀時模式**(資料的結構是隱式的,只有在讀取資料時才被解釋),與 **寫時模式**(關係資料庫的傳統方法,其中模式是顯式的,資料庫確保在寫入資料時所有資料都符合它)形成對比 [^18]。 +文件資料庫有時被稱為 *無模式*,但這是誤導性的,因為讀取資料的程式碼通常假設某種結構 —— 即存在隱式模式,但資料庫不強制執行 [^17]。更準確的術語是 *讀時模式*(資料的結構是隱式的,只有在讀取資料時才解釋),與 *寫時模式*(關係資料庫的傳統方法,其中模式是顯式的,資料庫確保所有資料在寫入時都符合它)形成對比 [^18]。 -讀時模式類似於程式語言中的動態(執行時)型別檢查,而寫時模式類似於靜態(編譯時)型別檢查。正如靜態和動態型別檢查的倡導者對它們的相對優點有很大的爭論 [^19],資料庫中模式的執行是一個有爭議的話題,通常沒有正確或錯誤的答案。 +讀時模式類似於程式語言中的動態(執行時)型別檢查,而寫時模式類似於靜態(編譯時)型別檢查。正如靜態和動態型別檢查的倡導者對它們的相對優點有很大的爭論 [^19],資料庫中模式的強制執行是一個有爭議的話題,通常沒有正確或錯誤的答案。 -當應用程式想要更改其資料格式時,方法之間的差異特別明顯。例如,假設你當前在一個欄位中儲存每個使用者的全名,而你希望分別儲存名字和姓氏 [^20]。在文件資料庫中,你只需開始使用新欄位編寫新文件,並在應用程式中有程式碼處理讀取舊文件的情況。例如: +當應用程式想要更改其資料格式時,這些方法之間的差異特別明顯。例如,假設你當前在一個欄位中儲存每個使用者的全名,而你想要分別儲存名字和姓氏 [^20]。在文件資料庫中,你只需開始編寫具有新欄位的新文件,並在應用程式中編寫處理讀取舊文件時的程式碼。例如: ```mongodb-json if (user && user.name && !user.first_name) { - // 2023年12月08日之前編寫的文件沒有 first_name + // 2023 年 12 月 8 日之前寫入的文件沒有 first_name user.first_name = user.name.split(" ")[0]; } ``` -這種方法的缺點是,應用程式中從資料庫讀取的每個部分現在都需要處理可能很久以前寫入的舊格式文件。另一方面,在寫時模式資料庫中,你通常會執行如下的 **遷移**: +這種方法的缺點是,從資料庫讀取的應用程式的每個部分現在都需要處理可能很久以前寫入的舊格式的文件。另一方面,在寫時模式資料庫中,你通常會執行 *遷移*,如下所示: ```sql ALTER TABLE users ADD COLUMN first_name text DEFAULT NULL; @@ -301,32 +317,32 @@ UPDATE users SET first_name = substring_index(name, ' ', 1); -- MySQL 在大多數關係資料庫中,新增具有預設值的列即使在大表上也是快速且無問題的。然而,在大表上執行 `UPDATE` 語句可能會很慢,因為每一行都需要重寫,其他模式操作(例如更改列的資料型別)通常也需要複製整個表。 -存在各種工具允許在後臺執行此類模式更改而不停機 [^21] [^22] [^23] [^24],但在大型資料庫上執行此類遷移在操作上仍然具有挑戰性。透過僅新增具有預設值 `NULL` 的 `first_name` 列(這很快),並在讀取時填充它,就像你在文件資料庫中所做的那樣,可以避免複雜的遷移。 +存在各種工具允許在後臺執行此類模式更改而無需停機 [^21] [^22] [^23] [^24],但在大型資料庫上執行此類遷移在操作上仍然具有挑戰性。透過僅新增預設值為 `NULL` 的 `first_name` 列(這很快)並在讀取時填充它,可以避免複雜的遷移,就像你在文件資料庫中所做的那樣。 -如果集合中的項由於某種原因並非都具有相同的結構(即資料是異構的),讀時模式方法是有利的——例如,因為: +如果集合中的專案由於某種原因並非都具有相同的結構(即資料是異構的),則讀時模式方法是有利的 —— 例如,因為: * 有許多不同型別的物件,將每種型別的物件放在自己的表中是不切實際的。 -* 資料的結構由你無法控制且可能隨時更改的外部系統確定。 +* 資料的結構由你無法控制且可能隨時更改的外部系統決定。 -在這樣的情況下,模式可能弊大於利,無模式文件可能是更自然的資料模型。但在所有記錄都應具有相同結構的情況下,模式是記錄和執行該結構的有用機制。我們將在[第 5 章](/tw/ch5#ch_encoding)中更詳細地討論模式和模式演化。 +在這樣的情況下,模式可能弊大於利,無模式文件可能是更自然的資料模型。但在所有記錄都應具有相同結構的情況下,模式是記錄和強制該結構的有用機制。我們將在 [第 5 章](/tw/ch5#ch_encoding) 中更詳細地討論模式和模式演化。 #### 讀寫的資料區域性 {#sec_datamodels_document_locality} -文件通常儲存為單個連續字串,編碼為 JSON、XML 或其二進位制變體(例如 MongoDB 的 BSON)。如果你的應用程式經常需要訪問整個文件(例如,在網頁上呈現它),這種 **儲存區域性** 具有效能優勢。如果資料分散在多個表中,如[圖 3-1](/tw/ch3#fig_obama_relational),則需要多個索引查詢來檢索所有資料,這可能需要更多的磁碟尋道並花費更多時間。 +文件通常儲存為單個連續字串,編碼為 JSON、XML 或二進位制變體(如 MongoDB 的 BSON)。如果你的應用程式經常需要訪問整個文件(例如,在網頁上渲染它),則這種 *儲存區域性* 具有效能優勢。如果資料分佈在多個表中,如 [圖 3-1](/tw/ch3#fig_obama_relational) 所示,則需要多次索引查詢才能檢索所有資料,這可能需要更多的磁碟尋道並花費更多時間。 -區域性優勢僅在你同時需要文件的大部分時才適用。資料庫通常需要載入整個文件,如果你只需要訪問大文件的一小部分,這可能會浪費。在更新文件時,通常需要重寫整個文件。出於這些原因,通常建議你保持文件相當小,並避免對文件進行頻繁的小更新。 +區域性優勢僅在你同時需要文件的大部分時才適用。資料庫通常需要載入整個文件,如果你只需要訪問大文件的一小部分,這可能會浪費。在文件更新時,通常需要重寫整個文件。由於這些原因,通常建議你保持文件相當小,並避免頻繁對文件進行小的更新。 -然而,將相關資料儲存在一起以獲得區域性的想法並不限於文件模型。例如,Google 的 Spanner 資料庫在關係資料模型中提供了相同的區域性屬性,允許模式宣告表的行應該交錯(巢狀)在父表中 [^25]。Oracle 允許相同的功能,使用稱為 **多表索引叢集表** 的功能 [^26]。由 Google 的 Bigtable 推廣並在例如 HBase 和 Accumulo 中使用的 **寬列** 資料模型具有 **列族** 的概念,它具有管理區域性的類似目的 [^27]。 +然而,將相關資料儲存在一起以獲得區域性的想法並不限於文件模型。例如,Google 的 Spanner 資料庫在關係資料模型中提供相同的區域性屬性,允許模式宣告表的行應該交錯(巢狀)在父表中 [^25]。Oracle 允許相同的功能,使用稱為 *多表索引叢集表* 的功能 [^26]。由 Google 的 Bigtable 推廣並在 HBase 和 Accumulo 等中使用的 *寬列* 資料模型具有 *列族* 的概念,其目的類似於管理區域性 [^27]。 #### 文件的查詢語言 {#query-languages-for-documents} -關係資料庫和文件資料庫之間的另一個區別是你用來查詢它的語言或 API。大多數關係資料庫使用 SQL 查詢,但文件資料庫更加多樣化。一些只允許透過主鍵進行鍵值訪問,而其他的還提供二級索引來查詢文件內的值,有些提供豐富的查詢語言。 +關係資料庫和文件資料庫之間的另一個區別是你用來查詢它的語言或 API。大多數關係資料庫使用 SQL 查詢,但文件資料庫更加多樣化。一些只允許透過主鍵進行鍵值訪問,而另一些還提供二級索引來查詢文件內的值,有些提供豐富的查詢語言。 -XML 資料庫通常使用 XQuery 和 XPath 查詢,它們旨在允許複雜查詢,包括跨多個文件的連線,並且還將其結果格式化為 XML [^28]。JSON Pointer [^29] 和 JSONPath [^30] 為 JSON 提供了與 XPath 等效的功能。 +XML 資料庫通常使用 XQuery 和 XPath 查詢,它們旨在允許複雜的查詢,包括跨多個文件的連線,並將其結果格式化為 XML [^28]。JSON Pointer [^29] 和 JSONPath [^30] 為 JSON 提供了等效於 XPath 的功能。 -MongoDB 的聚合管道(我們在["規範化、反規範化和連線"](/tw/ch3#sec_datamodels_normalization)中看到了其用於連線的 `$lookup` 運算子)是 JSON 文件集合查詢語言的一個例子。 +MongoDB 的聚合管道,我們在 ["規範化、反規範化與連線"](/tw/ch3#sec_datamodels_normalization) 中看到了其用於連線的 `$lookup` 運算子,是 JSON 文件集合查詢語言的一個例子。 -讓我們看另一個例子來感受這種語言——這次是聚合,這對於分析特別需要。想象你是一名海洋生物學家,每次在海洋中看到動物時,你都會向資料庫新增觀察記錄。現在你想生成一份報告,說明你每月看到了多少鯊魚。在 PostgreSQL 中,你可能會這樣表達該查詢: +讓我們看另一個例子來感受這種語言 —— 這次是聚合,這對分析特別需要。想象你是一名海洋生物學家,每次你在海洋中看到動物時,你都會向資料庫新增一條觀察記錄。現在你想生成一份報告,說明你每個月看到了多少條鯊魚。在 PostgreSQL 中,你可能會這樣表達該查詢: ```sql SELECT date_trunc('month', observation_timestamp) AS observation_month, ❶ @@ -338,7 +354,7 @@ GROUP BY observation_month; ❶ : `date_trunc('month', timestamp)` 函式確定包含 `timestamp` 的日曆月,並返回表示該月開始的另一個時間戳。換句話說,它將時間戳向下舍入到最近的月份。 -此查詢首先過濾觀察結果以僅顯示 `Sharks` 家族中的物種,然後按發生的日曆月對觀察結果進行分組,最後將該月所有觀察中看到的動物數量相加。同樣的查詢可以使用 MongoDB 的聚合管道表達如下: +此查詢首先過濾觀察結果以僅顯示 `Sharks` 家族中的物種,然後按它們發生的日曆月對觀察結果進行分組,最後將該月所有觀察中看到的動物數量相加。可以使用 MongoDB 的聚合管道表達相同的查詢,如下所示: ```mongodb-json db.observations.aggregate([ @@ -357,57 +373,58 @@ db.observations.aggregate([ #### 文件和關係資料庫的融合 {#convergence-of-document-and-relational-databases} -文件資料庫和關係資料庫最初是非常不同的資料管理方法,但隨著時間的推移,它們變得更加相似 [^31]。關係資料庫添加了對 JSON 型別和查詢運算子的支援,以及索引文件內屬性的能力。一些文件資料庫(如 MongoDB、Couchbase 和 RethinkDB)添加了對連線、二級索引和宣告式查詢語言的支援。 +文件資料庫和關係資料庫最初是非常不同的資料管理方法,但隨著時間的推移,它們變得更加相似 [^31]。關係資料庫增加了對 JSON 型別和查詢運算子的支援,以及索引文件內屬性的能力。一些文件資料庫(如 MongoDB、Couchbase 和 RethinkDB)增加了對連線、二級索引和宣告式查詢語言的支援。 -模型的這種融合對應用程式開發人員來說是個好訊息,因為當你可以在同一個資料庫中組合兩者時,關係模型和文件模型都能發揮最佳效果。許多文件資料庫需要對其他文件的關係式引用,許多關係資料庫在某些部分中模式靈活性是有益的。關係-文件混合是一個強大的組合。 +模型的這種融合對應用程式開發人員來說是個好訊息,因為當你可以在同一個資料庫中組合兩者時,關係模型和文件模型效果最好。許多文件資料庫需要對其他文件的關係式引用,許多關係資料庫在模式靈活性有益的部分。關係-文件混合是一個強大的組合。 -------- > [!NOTE] -> Codd 對關係模型的原始描述 [^3] 實際上允許在關係模式中使用類似 JSON 的東西。他稱之為 **非簡單域**。這個想法是,行中的值不必只是像數字或字串這樣的原始資料型別,它也可以是巢狀關係(表)——所以你可以將任意巢狀的樹結構作為值,就像 30 多年後新增到 SQL 的 JSON 或 XML 支援一樣。 +> Codd 對關係模型的原始描述 [^3] 實際上允許在關係模式中存在類似於 JSON 的東西。他稱之為 *非簡單域*。這個想法是,行中的值不必只是原始資料型別(如數字或字串),但它也可以是巢狀關係(表)—— 所以你可以有一個任意巢狀的樹結構作為值,很像 30 多年後新增到 SQL 的 JSON 或 XML 支援。 -------- + ## 圖資料模型 {#sec_datamodels_graph} -我們之前看到,關係的型別是區分不同資料模型的重要特徵。如果你的應用程式主要有一對多關係(樹形結構資料)並且記錄之間的其他關係很少,文件模型是合適的。 +我們之前看到,關係型別是不同資料模型之間的重要區別特徵。如果你的應用程式主要具有一對多關係(樹形結構資料)並且記錄之間很少有其他關係,則文件模型是合適的。 -但是,如果你的資料中多對多關係非常普遍呢?關係模型可以處理多對多關係的簡單情況,但隨著資料中的連線變得更加複雜,將資料建模為圖變得更加自然。 +但是,如果你的資料中多對多關係非常常見呢?關係模型可以處理多對多關係的簡單情況,但隨著資料內部連線變得更加複雜,開始將資料建模為圖變得更加自然。 -圖由兩種物件組成:**頂點**(也稱為 **節點** 或 **實體**)和 **邊**(也稱為 **關係** 或 **弧**)。許多型別的資料可以建模為圖。典型的例子包括: +圖由兩種物件組成:*頂點*(也稱為 *節點* 或 *實體*)和 *邊*(也稱為 *關係* 或 *弧*)。許多型別的資料可以建模為圖。典型的例子包括: 社交圖 -: 頂點是人,邊表示哪些人彼此認識。 +: 頂點是人,邊表示哪些人相互認識。 網頁圖 : 頂點是網頁,邊表示指向其他頁面的 HTML 連結。 道路或鐵路網路 -: 頂點是交叉路口,邊代表它們之間的道路或鐵路線。 +: 頂點是交叉點,邊表示它們之間的道路或鐵路線。 -眾所周知的演算法可以在這些圖上執行:例如,地圖導航應用程式在道路網路中搜索兩點之間的最短路徑,PageRank 可以用於網頁圖來確定網頁的受歡迎程度,從而確定其在搜尋結果中的排名 [^32]。 +眾所周知的演算法可以在這些圖上執行:例如,地圖導航應用程式搜尋道路網路中兩點之間的最短路徑,PageRank 可用於網頁圖以確定網頁的受歡迎程度,從而確定其在搜尋結果中的排名 [^32]。 -圖可以用幾種不同的方式表示。在 **鄰接列表** 模型中,每個頂點儲存其一條邊之外的鄰居頂點的 ID。或者,你可以使用 **鄰接矩陣**,這是一個二維陣列,其中每一行和每一列對應一個頂點,當行頂點和列頂點之間沒有邊時值為零,如果有邊則值為一。鄰接列表適合圖遍歷,矩陣適合機器學習(參見["資料框、矩陣和陣列"](/tw/ch3#sec_datamodels_dataframes))。 +圖可以用幾種不同的方式表示。在 *鄰接表* 模型中,每個頂點儲存其相距一條邊的鄰居頂點的 ID。或者,你可以使用 *鄰接矩陣*,這是一個二維陣列,其中每一行和每一列對應一個頂點,當行頂點和列頂點之間沒有邊時值為零,如果有邊則值為一。鄰接表適合圖遍歷,矩陣適合機器學習(參見 ["資料框、矩陣與陣列"](/tw/ch3#sec_datamodels_dataframes))。 -在剛才給出的例子中,圖中的所有頂點分別代表相同型別的事物(人、網頁或道路交叉口)。然而,圖不限於這種 **同質** 資料:圖的一個同樣強大的用途是提供一種一致的方式在單個數據庫中儲存完全不同型別的物件。例如: +在剛才給出的示例中,圖中的所有頂點都表示相同型別的事物(分別是人、網頁或道路交叉點)。然而,圖不限於這種 *同質* 資料:圖的一個同樣強大的用途是提供一種一致的方式在單個數據庫中儲存完全不同型別的物件。例如: -* Facebook 維護一個包含許多不同型別頂點和邊的單一圖:頂點代表人、位置、事件、簽到和使用者發表的評論;邊表示哪些人是彼此的朋友、哪個簽到發生在哪個位置、誰評論了哪個帖子、誰參加了哪個事件等 [^33]。 -* 知識圖被搜尋引擎用來記錄搜尋查詢中經常出現的實體的事實,如組織、人員和地點 [^34]。這些資訊透過爬取和分析網站上的文字獲得;一些網站,如 Wikidata,也以結構化形式釋出圖資料。 +* Facebook 維護一個包含許多不同型別頂點和邊的單一圖:頂點表示人員、位置、事件、簽到和使用者發表的評論;邊表示哪些人彼此是朋友、哪個簽到發生在哪個位置、誰評論了哪個帖子、誰參加了哪個事件等等 [^33]。 +* 知識圖被搜尋引擎用來記錄搜尋查詢中經常出現的實體(如組織、人員和地點)的事實 [^34]。這些資訊透過爬取和分析網站上的文字獲得;一些網站(如 Wikidata)也以結構化形式釋出圖資料。 -有幾種不同但相關的方式來構建和查詢圖中的資料。在本節中,我們將討論 **屬性圖** 模型(由 Neo4j、Memgraph、KùzuDB [^35] 等實現)和 **三元組儲存** 模型(由 Datomic、AllegroGraph、Blazegraph 等實現)。這些模型在它們可以表達的內容上相當相似,一些圖資料庫(如 Amazon Neptune)支援兩種模型。 +在圖中構建和查詢資料有幾種不同但相關的方式。在本節中,我們將討論 *屬性圖* 模型(由 Neo4j、Memgraph、KùzuDB [^35] 和其他 [^36] 實現)和 *三元組儲存* 模型(由 Datomic、AllegroGraph、Blazegraph 和其他實現)。這些模型在它們可以表達的內容方面相當相似,一些圖資料庫(如 Amazon Neptune)支援兩種模型。 -我們還將研究四種圖查詢語言(Cypher、SPARQL、Datalog 和 GraphQL),以及 SQL 對查詢圖的支援。還存在其他圖查詢語言,如 Gremlin [^37],但這些將給我們一個代表性的概述。 +我們還將檢視圖的四種查詢語言(Cypher、SPARQL、Datalog 和 GraphQL),以及用於查詢圖的 SQL 支援。還存在其他圖查詢語言,如 Gremlin [^37],但這些將為我們提供代表性的概述。 -為了說明這些不同的語言和模型,本節使用[圖 3-6](/tw/ch3#fig_datamodels_graph) 中顯示的圖作為執行示例。它可以取自社交網路或家譜資料庫:它顯示了兩個人,來自愛達荷州的 Lucy 和來自法國聖洛的 Alain。他們已婚並住在倫敦。每個人和每個位置都表示為頂點,它們之間的關係表示為邊。這個例子將幫助演示一些在圖資料庫中很容易但在其他模型中很困難的查詢。 +為了說明這些不同的語言和模型,本節使用 [圖 3-6](/tw/ch3#fig_datamodels_graph) 中顯示的圖作為執行示例。它可能取自社交網路或家譜資料庫:它顯示了兩個人,來自愛達荷州的 Lucy 和來自法國聖洛的 Alain。他們已婚並住在倫敦。每個人和每個位置都表示為頂點,它們之間的關係表示為邊。此示例將幫助演示一些在圖資料庫中很容易但在其他模型中很困難的查詢。 {{< figure src="/fig/ddia_0306.png" id="fig_datamodels_graph" caption="圖 3-6. 圖結構資料示例(框表示頂點,箭頭表示邊)。" class="w-full my-4" >}} ### 屬性圖 {#id56} -在 **屬性圖**(也稱為 **標記屬性圖**)模型中,每個頂點包含: +在 *屬性圖*(也稱為 *標記屬性圖*)模型中,每個頂點包含: * 唯一識別符號 -* 標籤(字串)來描述此頂點代表什麼型別的物件 +* 標籤(字串),描述此頂點表示的物件型別 * 一組出邊 * 一組入邊 * 屬性集合(鍵值對) @@ -415,12 +432,12 @@ db.observations.aggregate([ 每條邊包含: * 唯一識別符號 -* 邊開始的頂點(**尾頂點**) -* 邊結束的頂點(**頭頂點**) +* 邊開始的頂點(*尾頂點*) +* 邊結束的頂點(*頭頂點*) * 描述兩個頂點之間關係型別的標籤 * 屬性集合(鍵值對) -你可以將圖儲存視為由兩個關係表組成,一個用於頂點,一個用於邊,如[示例 3-3](/tw/ch3#fig_graph_sql_schema) 所示(此模式使用 PostgreSQL `jsonb` 資料型別來儲存每個頂點或邊的屬性)。每條邊都儲存頭頂點和尾頂點;如果你想要頂點的入邊或出邊集,可以分別透過 `head_vertex` 或 `tail_vertex` 查詢 `edges` 表。 +你可以將圖儲存視為由兩個關係表組成,一個用於頂點,一個用於邊,如 [示例 3-3](/tw/ch3#fig_graph_sql_schema) 所示(此模式使用 PostgreSQL `jsonb` 資料型別來儲存每個頂點或邊的屬性)。每條邊都儲存頭頂點和尾頂點;如果你想要頂點的入邊或出邊集,可以分別透過 `head_vertex` 或 `tail_vertex` 查詢 `edges` 表。 {{< figure id="fig_graph_sql_schema" title="示例 3-3. 使用關係模式表示屬性圖" class="w-full my-4" >}} @@ -443,32 +460,32 @@ CREATE INDEX edges_tails ON edges (tail_vertex); CREATE INDEX edges_heads ON edges (head_vertex); ``` -這個模型的一些重要方面是: +此模型的一些重要方面是: -1. 任何頂點都可以有一條邊將其與任何其他頂點連線。沒有模式限制哪些事物可以或不能相關聯。 -2. 給定任何頂點,你可以有效地找到其入邊和出邊,從而 **遍歷** 圖——即透過頂點鏈跟隨路徑——向前和向後。(這就是為什麼[示例 3-3](/tw/ch3#fig_graph_sql_schema) 在 `tail_vertex` 和 `head_vertex` 列上都有索引。) -3. 透過使用不同的標籤來表示不同型別的頂點和關係,你可以在單個圖中儲存幾種不同型別的資訊,同時仍然保持乾淨的資料模型。 +1. 任何頂點都可以有一條邊將其與任何其他頂點連線。沒有限制哪些型別的事物可以或不能關聯的模式。 +2. 給定任何頂點,你可以有效地找到其入邊和出邊,從而 *遍歷* 圖 —— 即透過頂點鏈跟隨路徑 —— 向前和向後。(這就是為什麼 [示例 3-3](/tw/ch3#fig_graph_sql_schema) 在 `tail_vertex` 和 `head_vertex` 列上都有索引。) +3. 透過對不同型別的頂點和關係使用不同的標籤,你可以在單個圖中儲存幾種不同型別的資訊,同時仍保持簡潔的資料模型。 -edges 表就像我們在["多對一和多對多關係"](/tw/ch3#sec_datamodels_many_to_many)中看到的多對多關聯表/連線表,推廣到允許在同一表中儲存許多不同型別的關係。標籤和屬性上也可能有索引,允許有效地找到具有某些屬性的頂點或邊。 +邊表就像我們在 ["多對一與多對多關係"](/tw/ch3#sec_datamodels_many_to_many) 中看到的多對多關聯表/連線表,泛化為允許在同一表中儲存許多不同型別的關係。標籤和屬性上也可能有索引,允許有效地找到具有某些屬性的頂點或邊。 -------- > [!NOTE] -> 圖模型的一個侷限性是邊只能將兩個頂點相互關聯,而關係連線表可以透過在單行上有多個外部索引鍵引用來表示三向甚至更高度的關係。這種關係可以透過建立對應於連線表每一行的額外頂點以及與該頂點的邊來在圖中表示,或者使用 **超圖**。 +> 圖模型的一個限制是邊只能將兩個頂點相互關聯,而關係連線表可以透過在單行上具有多個外部索引鍵引用來表示三元或甚至更高階的關係。此類關係可以透過為連線表的每一行建立一個額外的頂點,以及到/從該頂點的邊,或者使用 *超圖* 在圖中表示。 -------- -這些特性為資料建模提供了很大的靈活性,如[圖 3-6](/tw/ch3#fig_datamodels_graph) 所示。該圖顯示了一些在傳統關係模式中難以表達的事情,例如不同國家的不同地區結構(法國有 **省** 和 **大區**,而美國有 **縣** 和 **州**)、歷史的怪癖如國中之國(暫時忽略主權國家和民族的複雜性),以及資料的不同粒度(Lucy 的當前居住地被指定為城市,而她的出生地僅在州級別指定)。 +這些功能為資料建模提供了極大的靈活性,如 [圖 3-6](/tw/ch3#fig_datamodels_graph) 所示。該圖顯示了一些在傳統關係模式中難以表達的內容,例如不同國家的不同區域結構(法國有 *省* 和 *大區*,而美國有 *縣* 和 *州*)、歷史的怪癖(如國中之國)(暫時忽略主權國家和民族的複雜性),以及不同粒度的資料(Lucy 的當前居住地指定為城市,而她的出生地僅在州級別指定)。 -你可以想象擴充套件圖以包括關於 Lucy 和 Alain 或其他人的許多其他事實。例如,你可以使用它來表示他們有哪些食物過敏(透過為每種過敏原引入一個頂點,以及人和過敏原之間的邊來表示過敏),並將過敏原與一組顯示哪些食物含有哪些物質的頂點連結。然後你可以編寫查詢來找出每個人吃什麼是安全的。圖有利於可演化性:當你嚮應用程式新增功能時,圖可以輕鬆擴充套件以適應應用程式資料結構的變化。 +你可以想象擴充套件圖以包括有關 Lucy 和 Alain 或其他人的許多其他事實。例如,你可以使用它來指示他們有哪些食物過敏(透過為每個過敏原引入一個頂點,並在人和過敏原之間設定邊以指示過敏),並將過敏原與顯示哪些食物含有哪些物質的一組頂點連結。然後你可以編寫查詢來找出每個人可以安全食用的食物。圖適合可演化性:隨著你嚮應用程式新增功能,圖可以輕鬆擴充套件以適應應用程式資料結構的變化。 ### Cypher 查詢語言 {#id57} -**Cypher** 是屬性圖的查詢語言,最初為 Neo4j 圖資料庫建立,後來發展為開放標準 **openCypher** [^38]。除了 Neo4j,Cypher 還得到 Memgraph、KùzuDB [^35]、Amazon Neptune、Apache AGE(在 PostgreSQL 中儲存)等的支援。它以電影《駭客帝國》中的角色命名,與密碼學中的密碼無關 [^39]。 +*Cypher* 是用於屬性圖的查詢語言,最初為 Neo4j 圖資料庫建立,後來作為 *openCypher* 發展為開放標準 [^38]。除了 Neo4j,Cypher 還得到 Memgraph、KùzuDB [^35]、Amazon Neptune、Apache AGE(在 PostgreSQL 中儲存)等的支援。它以電影《駭客帝國》中的角色命名,與密碼學中的密碼無關 [^39]。 -[示例 3-4](/tw/ch3#fig_cypher_create) 顯示了將[圖 3-6](/tw/ch3#fig_datamodels_graph) 左側部分插入圖資料庫的 Cypher 查詢。圖的其餘部分可以類似地新增。每個頂點都被賦予一個像 `usa` 或 `idaho` 這樣的符號名稱。該名稱不儲存在資料庫中,而僅在查詢內部使用以在頂點之間建立邊,使用箭頭表示法:`(idaho) -[:WITHIN]-> (usa)` 建立一個標記為 `WITHIN` 的邊,`idaho` 作為尾節點,`usa` 作為頭節點。 +[示例 3-4](/tw/ch3#fig_cypher_create) 顯示了將 [圖 3-6](/tw/ch3#fig_datamodels_graph) 的左側部分插入圖資料庫的 Cypher 查詢。圖的其餘部分可以類似地新增。每個頂點都被賦予一個符號名稱,如 `usa` 或 `idaho`。該名稱不儲存在資料庫中,僅在查詢內部使用以在頂點之間建立邊,使用箭頭符號:`(idaho) -[:WITHIN]-> (usa)` 建立一條標記為 `WITHIN` 的邊,其中 `idaho` 作為尾節點,`usa` 作為頭節點。 -{{< figure id="fig_cypher_create" title="示例 3-4. [圖 3-6](/tw/ch3#fig_datamodels_graph) 中資料的子集,表示為 Cypher 查詢" class="w-full my-4" >}} +{{< figure link="#fig_datamodels_graph" id="fig_cypher_create" title="示例 3-4. 圖 3-6 中資料的子集,表示為 Cypher 查詢" class="w-full my-4" >}} ``` CREATE @@ -480,9 +497,9 @@ CREATE (lucy) -[:BORN_IN]-> (idaho) ``` -當[圖 3-6](/tw/ch3#fig_datamodels_graph) 的所有頂點和邊都新增到資料庫後,我們可以開始提出有趣的問題:例如,**查詢從美國移民到歐洲的所有人的姓名**。也就是說,找到所有具有 `BORN_IN` 邊指向美國境內某個位置,並且也有 `LIVING_IN` 邊指向歐洲境內某個位置的頂點,並返回每個這些頂點的 `name` 屬性。 +當 [圖 3-6](/tw/ch3#fig_datamodels_graph) 的所有頂點和邊都新增到資料庫後,我們可以開始提出有趣的問題:例如,*查詢所有從美國移民到歐洲的人的姓名*。也就是說,找到所有具有指向美國境內位置的 `BORN_IN` 邊,以及指向歐洲境內位置的 `LIVING_IN` 邊的頂點,並返回每個頂點的 `name` 屬性。 -[示例 3-5](/tw/ch3#fig_cypher_query) 顯示瞭如何在 Cypher 中表達該查詢。相同的箭頭表示法用於 `MATCH` 子句以在圖中查詢模式:`(person) -[:BORN_IN]-> ()` 匹配由標記為 `BORN_IN` 的邊相關的任意兩個頂點。該邊的尾頂點繫結到變數 `person`,頭頂點未命名。 +[示例 3-5](/tw/ch3#fig_cypher_query) 顯示瞭如何在 Cypher 中表達該查詢。相同的箭頭符號用於 `MATCH` 子句中以在圖中查詢模式:`(person) -[:BORN_IN]-> ()` 匹配由標記為 `BORN_IN` 的邊相關的任意兩個頂點。該邊的尾頂點繫結到變數 `person`,頭頂點未命名。 {{< figure id="fig_cypher_query" title="示例 3-5. Cypher 查詢查詢從美國移民到歐洲的人" class="w-full my-4" >}} @@ -493,32 +510,32 @@ MATCH RETURN person.name ``` -查詢可以按如下方式閱讀: +查詢可以這樣理解: -> 查詢滿足以下 **兩個** 條件的任何頂點(稱為 `person`): +> 找到滿足以下 *兩個* 條件的任何頂點(稱為 `person`): > -> 1. `person` 有一條出邊 `BORN_IN` 指向某個頂點。從該頂點,你可以跟隨一系列出邊 `WITHIN`,直到最終到達型別為 `Location` 的頂點,其 `name` 屬性等於 `"United States"`。 -> 2. 同一個 `person` 頂點也有一條出邊 `LIVES_IN`。跟隨該邊,然後是一系列出邊 `WITHIN`,你最終到達型別為 `Location` 的頂點,其 `name` 屬性等於 `"Europe"`。 +> 1. `person` 有一條出邊 `BORN_IN` 指向某個頂點。從那個頂點,你可以跟隨一條出邊 `WITHIN` 鏈,直到最終到達一個型別為 `Location` 的頂點,其 `name` 屬性等於 `"United States"`。 +> 2. 同一個 `person` 頂點也有一條出邊 `LIVES_IN`。跟隨該邊,然後是一條出邊 `WITHIN` 鏈,你最終到達一個型別為 `Location` 的頂點,其 `name` 屬性等於 `"Europe"`。 > > 對於每個這樣的 `person` 頂點,返回 `name` 屬性。 -有幾種可能的執行查詢的方式。這裡給出的描述建議你從掃描資料庫中的所有人開始,檢查每個人的出生地和居住地,並僅返回滿足條件的那些人。 +有幾種可能的執行查詢的方法。這裡給出的描述建議你從掃描資料庫中的所有人開始,檢查每個人的出生地和居住地,並僅返回符合條件的人。 -但同樣地,你可以從兩個 `Location` 頂點開始並向後工作。如果 `name` 屬性上有索引,你可以有效地找到代表美國和歐洲的兩個頂點。然後你可以透過跟隨所有傳入的 `WITHIN` 邊來分別找到美國和歐洲的所有位置(州、地區、城市等)。最後,你可以尋找可以透過其中一個位置頂點的傳入 `BORN_IN` 或 `LIVES_IN` 邊找到的人。 +但等效地,你可以從兩個 `Location` 頂點開始並向後工作。如果 `name` 屬性上有索引,你可以有效地找到表示美國和歐洲的兩個頂點。然後你可以透過跟隨所有傳入的 `WITHIN` 邊來查詢美國和歐洲各自的所有位置(州、地區、城市等)。最後,你可以尋找可以透過位置頂點之一的傳入 `BORN_IN` 或 `LIVES_IN` 邊找到的人。 ### SQL 中的圖查詢 {#id58} -[示例 3-3](/tw/ch3#fig_graph_sql_schema) 建議圖資料可以在關係資料庫中表示。但是,如果我們將圖資料放入關係結構中,我們是否也可以使用 SQL 查詢它? +[示例 3-3](/tw/ch3#fig_graph_sql_schema) 建議圖資料可以在關係資料庫中表示。但如果我們將圖資料放入關係結構中,我們還能使用 SQL 查詢它嗎? -答案是肯定的,但有一些困難。你在圖查詢中遍歷的每條邊實際上都是與 `edges` 表的連線。在關係資料庫中,你通常事先知道查詢中需要哪些連線。另一方面,在圖查詢中,你可能需要遍歷可變數量的邊才能找到你要查詢的頂點——也就是說,連線的數量不是事先固定的。 +答案是肯定的,但有一些困難。你在圖查詢中遍歷的每條邊實際上都是與 `edges` 表的連線。在關係資料庫中,你通常事先知道查詢中需要哪些連線。另一方面,在圖查詢中,你可能需要遍歷可變數量的邊才能找到你要查詢的頂點 —— 也就是說,連線的數量不是預先固定的。 -在我們的例子中,這發生在 Cypher 查詢中的 `() -[:WITHIN*0..]-> ()` 模式中。一個人的 `LIVES_IN` 邊可能指向任何型別的位置:街道、城市、地區、地區、州等。城市可能 `WITHIN` 一個地區,地區 `WITHIN` 一個州,州 `WITHIN` 一個國家等。`LIVES_IN` 邊可能直接指向你要查詢的位置頂點,或者它可能在位置層次結構中相距幾個級別。 +在我們的示例中,這發生在 Cypher 查詢中的 `() -[:WITHIN*0..]-> ()` 模式中。一個人的 `LIVES_IN` 邊可能指向任何型別的位置:街道、城市、地區、地區、州等。一個城市可能在一個地區 `WITHIN`,一個地區在一個州 `WITHIN`,一個州在一個國家 `WITHIN`,等等。`LIVES_IN` 邊可能直接指向你要查詢的位置頂點,或者它可能在位置層次結構中相距幾個級別。 -在 Cypher 中,`:WITHIN*0..` 非常簡潔地表達了這一事實:它意味著"跟隨 `WITHIN` 邊,零次或多次"。它就像正則表示式中的 `*` 運算子。 +在 Cypher 中,`:WITHIN*0..` 非常簡潔地表達了這個事實:它意味著"跟隨 `WITHIN` 邊,零次或多次"。它就像正則表示式中的 `*` 運算子。 -自 SQL:1999 以來,這種查詢中可變長度遍歷路徑的想法可以使用稱為 **遞迴公用表表達式**(`WITH RECURSIVE` 語法)的東西來表達。[示例 3-6](/tw/ch3#fig_graph_sql_query) 顯示了相同的查詢——查詢從美國移民到歐洲的人的姓名——使用此技術用 SQL 表達。然而,與 Cypher 相比,語法非常笨拙。 +自 SQL:1999 以來,查詢中可變長度遍歷路徑的想法可以使用稱為 *遞迴公用表表達式*(`WITH RECURSIVE` 語法)的東西來表達。[示例 3-6](/tw/ch3#fig_graph_sql_query) 顯示了相同的查詢 —— 查詢從美國移民到歐洲的人的姓名 —— 使用此技術在 SQL 中表達。然而,與 Cypher 相比,語法非常笨拙。 -{{< figure id="fig_graph_sql_query" title="示例 3-6. 與[示例 3-5](/tw/ch3#fig_cypher_query) 相同的查詢,使用遞迴公用表表達式用 SQL 編寫" class="w-full my-4" >}} +{{< figure link="#fig_cypher_query" id="fig_graph_sql_query" title="示例 3-6. 與 示例 3-5 相同的查詢,使用遞迴公用表表達式在 SQL 中編寫" class="w-full my-4" >}} ```sql WITH RECURSIVE @@ -543,14 +560,14 @@ WITH RECURSIVE WHERE edges.label = 'within' ), - -- born_in_usa 是在美國出生的所有人的頂點 ID 集合 + -- born_in_usa 是所有在美國出生的人的頂點 ID 集合 born_in_usa(vertex_id) AS ( ❹ SELECT edges.tail_vertex FROM edges JOIN in_usa ON edges.head_vertex = in_usa.vertex_id WHERE edges.label = 'born_in' ), - -- lives_in_europe 是住在歐洲的所有人的頂點 ID 集合 + -- lives_in_europe 是所有居住在歐洲的人的頂點 ID 集合 lives_in_europe(vertex_id) AS ( ❺ SELECT edges.tail_vertex FROM edges JOIN in_europe ON edges.head_vertex = in_europe.vertex_id @@ -559,44 +576,44 @@ WITH RECURSIVE SELECT vertices.properties->>'name' FROM vertices - -- 連線以找到既在美國出生 *又* 住在歐洲的人 + -- 連線以找到那些既在美國出生 *又* 居住在歐洲的人 JOIN born_in_usa ON vertices.vertex_id = born_in_usa.vertex_id ❻ JOIN lives_in_europe ON vertices.vertex_id = lives_in_europe.vertex_id; ``` -❶: 首先找到 `name` 屬性值為 `"United States"` 的頂點,並使其成為頂點集合 `in_usa` 的第一個元素。 +❶: 首先找到 `name` 屬性值為 `"United States"` 的頂點,並使其成為頂點集 `in_usa` 的第一個元素。 ❷: 從集合 `in_usa` 中的頂點跟隨所有傳入的 `within` 邊,並將它們新增到同一集合中,直到訪問了所有傳入的 `within` 邊。 -❸: 從 `name` 屬性值為 `"Europe"` 的頂點開始執行相同操作,並構建頂點集合 `in_europe`。 +❸: 從 `name` 屬性值為 `"Europe"` 的頂點開始執行相同操作,並構建頂點集 `in_europe`。 -❹: 對於集合 `in_usa` 中的每個頂點,跟隨傳入的 `born_in` 邊以找到在美國某個地方出生的人。 +❹: 對於集合 `in_usa` 中的每個頂點,跟隨傳入的 `born_in` 邊以查詢在美國某個地方出生的人。 -❺: 類似地,對於集合 `in_europe` 中的每個頂點,跟隨傳入的 `lives_in` 邊以找到住在歐洲的人。 +❺: 類似地,對於集合 `in_europe` 中的每個頂點,跟隨傳入的 `lives_in` 邊以查詢居住在歐洲的人。 -❻: 最後,透過連線它們來交叉在美國出生的人集合和住在歐洲的人集合。 +❻: 最後,透過連線它們來將在美國出生的人的集合與居住在歐洲的人的集合相交。 -4 行 Cypher 查詢在 SQL 中需要 31 行這一事實表明,正確選擇資料模型和查詢語言可以產生多大的差異。這只是開始;還有更多細節需要考慮,例如處理迴圈以及在廣度優先或深度優先遍歷之間進行選擇 [^40]。 +4 行 Cypher 查詢需要 31 行 SQL 的事實表明,正確選擇資料模型和查詢語言可以產生多大的差異。這只是開始;還有更多細節需要考慮,例如,處理迴圈,以及在廣度優先或深度優先遍歷之間進行選擇 [^40]。 -Oracle 對遞迴查詢有不同的 SQL 擴充套件,它稱之為 **分層** [^41]。 +Oracle 對遞迴查詢有不同的 SQL 擴充套件,它稱之為 *層次* [^41]。 -然而,情況可能正在改善:在撰寫本文時,有計劃將名為 GQL 的圖查詢語言新增到 SQL 標準 [^42] [^43],它將提供受 Cypher、GSQL [^44] 和 PGQL [^45] 啟發的語法。 +然而,情況可能正在改善:在撰寫本文時,有計劃向 SQL 標準新增一種名為 GQL 的圖查詢語言 [^42] [^43],它將提供受 Cypher、GSQL [^44] 和 PGQL [^45] 啟發的語法。 ### 三元組儲存與 SPARQL {#id59} -三元組儲存模型與屬性圖模型基本等效,使用不同的詞來描述相同的想法。儘管如此,還是值得討論,因為有各種用於三元組儲存的工具和語言,它們可以成為構建應用程式的工具箱的寶貴補充。 +三元組儲存模型大多等同於屬性圖模型,使用不同的詞來描述相同的想法。儘管如此,它仍值得討論,因為有各種三元組儲存的工具和語言,它們可以成為構建應用程式工具箱的寶貴補充。 -在三元組儲存中,所有資訊都以非常簡單的三部分語句的形式儲存:(**主語**、**謂語**、**賓語**)。例如,在三元組(*Jim*、*likes*、*bananas*)中,*Jim* 是主語,*likes* 是謂語(動詞),*bananas* 是賓語。 +在三元組儲存中,所有資訊都以非常簡單的三部分語句的形式儲存:(*主語*、*謂語*、*賓語*)。例如,在三元組(*Jim*、*likes*、*bananas*)中,*Jim* 是主語,*likes* 是謂語(動詞),*bananas* 是賓語。 -三元組的主語相當於圖中的頂點。賓語是兩種事物之一: +三元組的主語等同於圖中的頂點。賓語是兩種東西之一: -1. 原始資料型別的值,如字串或數字。在這種情況下,三元組的謂語和賓語相當於主語頂點上屬性的鍵和值。使用[圖 3-6](/tw/ch3#fig_datamodels_graph) 中的例子,(*lucy*、*birthYear*、*1989*)就像具有屬性 `{"birthYear": 1989}` 的頂點 `lucy`。 +1. 原始資料型別的值,如字串或數字。在這種情況下,三元組的謂語和賓語等同於主語頂點上屬性的鍵和值。使用 [圖 3-6](/tw/ch3#fig_datamodels_graph) 中的示例,(*lucy*、*birthYear*、*1989*)就像一個頂點 `lucy`,其屬性為 `{"birthYear": 1989}`。 2. 圖中的另一個頂點。在這種情況下,謂語是圖中的邊,主語是尾頂點,賓語是頭頂點。例如,在(*lucy*、*marriedTo*、*alain*)中,主語和賓語 *lucy* 和 *alain* 都是頂點,謂語 *marriedTo* 是連線它們的邊的標籤。 > [!NOTE] -> 準確地說,提供類似三元組資料模型的資料庫通常需要在每個元組上儲存一些額外的元資料。例如,AWS Neptune 透過向每個三元組新增圖 ID 來使用四元組(4元組)[^46];Datomic 使用 5 元組,透過事務 ID 和表示刪除的布林值擴充套件每個三元組 [^47]。由於這些資料庫保留了上述解釋的基本 **主語-謂語-賓語** 結構,本書仍然稱它們為三元組儲存。 +> 準確地說,提供類似三元組資料模型的資料庫通常需要在每個元組上儲存一些額外的元資料。例如,AWS Neptune 使用四元組(4-tuples),透過向每個三元組新增圖 ID [^46];Datomic 使用 5 元組,用事務 ID 和一個表示刪除的布林值擴充套件每個三元組 [^47]。由於這些資料庫保留了上面解釋的基本 *主語-謂語-賓語* 結構,本書仍然稱它們為三元組儲存。 -[示例 3-7](/tw/ch3#fig_graph_n3_triples) 顯示了與[示例 3-4](/tw/ch3#fig_cypher_create) 相同的資料,以稱為 **Turtle** 的格式編寫為三元組,它是 **Notation3**(**N3**)的子集 [^48]。 +[示例 3-7](/tw/ch3#fig_graph_n3_triples) 顯示了與 [示例 3-4](/tw/ch3#fig_cypher_create) 中相同的資料,以稱為 *Turtle* 的格式編寫為三元組,它是 *Notation3*(*N3*)的子集 [^48]。 {{< figure link="#fig_datamodels_graph" id="fig_graph_n3_triples" title="示例 3-7. 圖 3-6 中資料的子集,表示為 Turtle 三元組" class="w-full my-4" >}} @@ -618,11 +635,11 @@ _:namerica :name "North America". _:namerica :type "continent". ``` -在這個例子中,圖的頂點寫為 `_:someName`。該名稱在此檔案之外沒有任何意義;它的存在只是因為否則我們不會知道哪些三元組引用同一個頂點。當謂語表示邊時,賓語是頂點,如 `_:idaho :within _:usa`。當謂語是屬性時,賓語是字串文字,如 `_:usa :name "United States"`。 +在此示例中,圖的頂點寫為 `_:someName`。該名稱在此檔案之外沒有任何意義;它的存在只是因為否則我們不知道哪些三元組引用同一個頂點。當謂語表示邊時,賓語是頂點,如 `_:idaho :within _:usa`。當謂語是屬性時,賓語是字串字面量,如 `_:usa :name "United States"`。 -一遍又一遍地重複相同的主語相當重複,但幸運的是,你可以使用分號對同一主語說多件事。這使得 Turtle 格式相當可讀:參見[示例 3-8](/tw/ch3#fig_graph_n3_shorthand)。 +一遍又一遍地重複相同的主語相當重複,但幸運的是,你可以使用分號來表達關於同一主語的多個內容。這使得 Turtle 格式非常易讀:見 [示例 3-8](/tw/ch3#fig_graph_n3_shorthand)。 -{{< figure id="fig_graph_n3_shorthand" title="示例 3-8. 編寫[示例 3-7](/tw/ch3#fig_graph_n3_triples) 中資料的更簡潔方式" class="w-full my-4" >}} +{{< figure link="#fig_graph_n3_triples" id="fig_graph_n3_shorthand" title="示例 3-8. 編寫 示例 3-7 中資料的更簡潔方式" class="w-full my-4" >}} ``` @prefix : . @@ -636,17 +653,17 @@ _:namerica a :Location; :name "North America"; :type "continent". > [!TIP] 語義網 -三元組儲存的一些研究和開發工作是由 **語義網** 激發的,這是 2000 年代初的一項努力,透過不僅以人類可讀的網頁釋出資料,而且以標準化、機器可讀的格式釋出資料,促進網際網路範圍的資料交換。雖然最初設想的語義網沒有成功 [^49] [^50],但語義網專案的遺產在幾個特定技術中繼續存在:**連結資料** 標準如 JSON-LD [^51]、生物醫學科學中使用的 **本體** [^52]、Facebook 的開放圖譜協議 [^53](用於連結展開 [^54])、Wikidata 等知識圖,以及由 [`schema.org`](https://schema.org/) 維護的結構化資料標準化詞彙表。 +一些三元組儲存的研究和開發工作是由 *語義網* 推動的,這是 2000 年代初的一項努力,旨在透過不僅以人類可讀的網頁形式釋出資料,還以標準化的機器可讀格式釋出資料來促進網際網路範圍的資料交換。儘管最初設想的語義網沒有成功 [^49] [^50],但語義網專案的遺產在幾項特定技術中繼續存在:*連結資料* 標準(如 JSON-LD [^51])、生物醫學科學中使用的 *本體* [^52]、Facebook 的開放圖協議 [^53](用於連結展開 [^54])、知識圖(如 Wikidata)以及由 [`schema.org`](https://schema.org/) 維護的結構化資料的標準化詞彙表。 -三元組儲存是另一種在其原始用例之外找到用途的語義網技術:即使你對語義網沒有興趣,三元組也可以是應用程式的良好內部資料模型。 +三元組儲存是另一種在其原始用例之外找到用途的語義網技術:即使你對語義網沒有興趣,三元組也可以成為應用程式的良好內部資料模型。 -------- #### RDF 資料模型 {#the-rdf-data-model} -我們在[示例 3-8](/tw/ch3#fig_graph_n3_shorthand) 中使用的 Turtle 語言實際上是在 **資源描述框架**(RDF)[^55] 中編碼資料的一種方式,這是為語義網設計的資料模型。RDF 資料也可以用其他方式編碼,例如(更冗長地)在 XML 中,如[示例 3-9](/tw/ch3#fig_graph_rdf_xml) 所示。像 Apache Jena 這樣的工具可以在不同的 RDF 編碼之間自動轉換。 +我們在 [示例 3-8](/tw/ch3#fig_graph_n3_shorthand) 中使用的 Turtle 語言實際上是在 *資源描述框架*(RDF)[^55] 中編碼資料的一種方式,這是為語義網設計的資料模型。RDF 資料也可以用其他方式編碼,例如(更冗長地)用 XML,如 [示例 3-9](/tw/ch3#fig_graph_rdf_xml) 所示。像 Apache Jena 這樣的工具可以在不同的 RDF 編碼之間自動轉換。 -{{< figure id="fig_graph_rdf_xml" title="示例 3-9. [示例 3-8](/tw/ch3#fig_graph_n3_shorthand) 的資料,使用 RDF/XML 語法表示" class="w-full my-4" >}} +{{< figure link="#fig_graph_n3_shorthand" id="fig_graph_rdf_xml" title="示例 3-9. 示例 3-8 的資料,使用 RDF/XML 語法表示" class="w-full my-4" >}} ```xml ``` -RDF 有一些怪癖,因為它是為網際網路範圍的資料交換而設計的。三元組的主語、謂語和賓語通常是 URI。例如,謂語可能是 URI,如 `` 或 ``,而不僅僅是 `WITHIN` 或 `LIVES_IN`。這種設計背後的原因是你應該能夠將你的資料與其他人的資料結合,如果他們對單詞 `within` 或 `lives_in` 賦予不同的含義,你不會發生衝突,因為他們的謂語實際上是 `` 和 ``。 +RDF 有一些怪癖,因為它是為網際網路範圍的資料交換而設計的。三元組的主語、謂語和賓語通常是 URI。例如,謂語可能是一個 URI,如 `` 或 ``,而不僅僅是 `WITHIN` 或 `LIVES_IN`。這種設計背後的原因是,你應該能夠將你的資料與其他人的資料結合起來,如果他們給單詞 `within` 或 `lives_in` 附加了不同的含義,你不會發生衝突,因為他們的謂語實際上是 `` 和 ``。 -URL `` 不一定需要解析為任何內容——從 RDF 的角度來看,它只是一個名稱空間。為了避免與 `http://` URL 的潛在混淆,本節中的示例使用不可解析的 URI,如 `urn:example:within`。幸運的是,你只需在檔案頂部指定一次此字首,然後就可以忘記它。 +URL `` 不一定需要解析為任何內容 —— 從 RDF 的角度來看,它只是一個名稱空間。為了避免與 `http://` URL 的潛在混淆,本節中的示例使用不可解析的 URI,如 `urn:example:within`。幸運的是,你只需在檔案頂部指定一次此字首,然後就可以忘記它。 #### SPARQL 查詢語言 {#the-sparql-query-language} -**SPARQL** 是使用 RDF 資料模型的三元組儲存的查詢語言 [^56]。(它是 **SPARQL 協議和 RDF 查詢語言** 的縮寫,發音為"sparkle"。)它早於 Cypher,由於 Cypher 的模式匹配借鑑自 SPARQL,它們看起來非常相似。 +*SPARQL* 是使用 RDF 資料模型的三元組儲存的查詢語言 [^56]。(它是 *SPARQL Protocol and RDF Query Language* 的首字母縮略詞,發音為 "sparkle"。)它早於 Cypher,由於 Cypher 的模式匹配是從 SPARQL 借用的,它們看起來非常相似。 -與以前相同的查詢——查詢從美國搬到歐洲的人——在 SPARQL 中與在 Cypher 中一樣簡潔(參見[示例 3-10](/tw/ch3#fig_sparql_query))。 +與之前相同的查詢 —— 查詢從美國搬到歐洲的人 —— 在 SPARQL 中與在 Cypher 中一樣簡潔(見 [示例 3-10](/tw/ch3#fig_sparql_query))。 -{{< figure id="fig_sparql_query" title="示例 3-10. 與[示例 3-5](/tw/ch3#fig_cypher_query) 相同的查詢,用 SPARQL 表示" class="w-full my-4" >}} +{{< figure id="fig_sparql_query" title="示例 3-10. 與 [示例 3-5](/tw/ch3#fig_cypher_query) 相同的查詢,用 SPARQL 表示" class="w-full my-4" >}} ``` PREFIX : @@ -706,7 +723,7 @@ SELECT ?personName WHERE { ?person :bornIn / :within* ?location. # SPARQL ``` -因為 RDF 不區分屬性和邊,而是對兩者都使用謂語,所以你可以使用相同的語法來匹配屬性。在以下表達式中,變數 `usa` 繫結到任何具有 `name` 屬性且其值為字串 `"United States"` 的頂點: +因為 RDF 不區分屬性和邊,而只是對兩者都使用謂語,所以你可以使用相同的語法來匹配屬性。在以下表達式中,變數 `usa` 繫結到任何具有 `name` 屬性且其值為字串 `"United States"` 的頂點: ``` (usa {name:'United States'}) # Cypher @@ -718,13 +735,13 @@ SPARQL 得到 Amazon Neptune、AllegroGraph、Blazegraph、OpenLink Virtuoso、A ### Datalog:遞迴關係查詢 {#id62} -Datalog 是一種比 SPARQL 或 Cypher 更古老的語言:它起源於 20 世紀 80 年代的學術研究 [^57] [^58] [^59]。它在軟體工程師中不太為人所知,在主流資料庫中也沒有得到廣泛支援,但它應該更為人所知,因為它是一種非常有表現力的語言,對於複雜查詢特別強大。幾個小眾資料庫,包括 Datomic、LogicBlox、CozoDB 和 LinkedIn 的 LIquid [^60] 使用 Datalog 作為他們的查詢語言。 +Datalog 是一種比 SPARQL 或 Cypher 更古老的語言:它源於 20 世紀 80 年代的學術研究 [^57] [^58] [^59]。它在軟體工程師中不太為人所知,並且在主流資料庫中沒有得到廣泛支援,但它應該更為人所知,因為它是一種非常有表現力的語言,對於複雜查詢特別強大。幾個小眾資料庫,包括 Datomic、LogicBlox、CozoDB 和 LinkedIn 的 LIquid [^60] 使用 Datalog 作為它們的查詢語言。 -Datalog 實際上基於關係資料模型而不是圖,但它出現在本書的圖資料庫部分,因為圖上的遞迴查詢是 Datalog 的特殊優勢。 +Datalog 實際上基於關係資料模型,而不是圖,但它出現在本書的圖資料庫部分,因為圖上的遞迴查詢是 Datalog 的特殊優勢。 -Datalog 資料庫的內容由 **事實** 組成,每個事實對應於關係表中的一行。例如,假設我們有一個包含位置的表 *location*,它有三列:*ID*、*name* 和 *type*。美國是一個國家的事實可以寫為 `location(2, "United States", "country")`,其中 `2` 是美國的 ID。一般來說,語句 `table(val1, val2, …​)` 意味著 `table` 包含一行,其中第一列包含 `val1`,第二列包含 `val2`,依此類推。 +Datalog 資料庫的內容由 *事實* 組成,每個事實對應於關係表中的一行。例如,假設我們有一個包含位置的表 *location*,它有三列:*ID*、*name* 和 *type*。美國是一個國家的事實可以寫成 `location(2, "United States", "country")`,其中 `2` 是美國的 ID。一般來說,語句 `table(val1, val2, …​)` 意味著 `table` 包含一行,其中第一列包含 `val1`,第二列包含 `val2`,依此類推。 -[示例 3-11](/tw/ch3#fig_datalog_triples) 顯示瞭如何在 Datalog 中編寫[圖 3-6](/tw/ch3#fig_datamodels_graph) 左側的資料。圖的邊(`within`、`born_in` 和 `lives_in`)表示為兩列連線表。例如,Lucy 的 ID 為 100,愛達荷州的 ID 為 3,因此關係"Lucy 出生在愛達荷州"表示為 `born_in(100, 3)`。 +[示例 3-11](/tw/ch3#fig_datalog_triples) 顯示瞭如何在 Datalog 中編寫 [圖 3-6](/tw/ch3#fig_datamodels_graph) 左側的資料。圖的邊(`within`、`born_in` 和 `lives_in`)表示為兩列連線表。例如,Lucy 的 ID 是 100,愛達荷州的 ID 是 3,所以關係"Lucy 出生在愛達荷州"表示為 `born_in(100, 3)`。 {{< figure id="fig_datalog_triples" title="示例 3-11. [圖 3-6](/tw/ch3#fig_datamodels_graph) 中資料的子集,表示為 Datalog 事實" class="w-full my-4" >}} @@ -740,9 +757,9 @@ person(100, "Lucy"). born_in(100, 3). /* Lucy 出生在愛達荷州 */ ``` -現在我們已經定義了資料,我們可以編寫與以前相同的查詢,如[示例 3-12](/tw/ch3#fig_datalog_query) 所示。它看起來與 Cypher 或 SPARQL 中的等效查詢有點不同,但不要讓它嚇到你。Datalog 是 Prolog 的子集,如果你學過計算機科學,你可能以前見過這種程式語言。 +現在我們已經定義了資料,我們可以編寫與之前相同的查詢,如 [示例 3-12](/tw/ch3#fig_datalog_query) 所示。它看起來與 Cypher 或 SPARQL 中的等效查詢有點不同,但不要讓這嚇倒你。Datalog 是 Prolog 的子集,這是一種程式語言,如果你學過計算機科學,你可能見過它。 -{{< figure id="fig_datalog_query" title="示例 3-12. 與[示例 3-5](/tw/ch3#fig_cypher_query) 相同的查詢,用 Datalog 表示" class="w-full my-4" >}} +{{< figure id="fig_datalog_query" title="示例 3-12. 與 [示例 3-5](/tw/ch3#fig_cypher_query) 相同的查詢,用 Datalog 表示" class="w-full my-4" >}} ```sql within_recursive(LocID, PlaceName) :- location(LocID, PlaceName, _). /* 規則 1 */ @@ -760,13 +777,13 @@ us_to_europe(Person) :- migrated(Person, "United States", "Europe"). /* 規則 4 /* us_to_europe 包含行 "Lucy"。 */ ``` -Cypher 和 SPARQL 直接用 `SELECT` 開始,但 Datalog 一次只邁出一小步。我們定義從底層事實派生新虛擬表的 **規則**。這些派生表就像(虛擬)SQL 檢視:它們不儲存在資料庫中,但你可以像查詢包含儲存事實的表一樣查詢它們。 +Cypher 和 SPARQL 直接用 `SELECT` 開始,但 Datalog 一次只邁出一小步。我們定義 *規則* 從底層事實派生新的虛擬表。這些派生表就像(虛擬)SQL 檢視:它們不儲存在資料庫中,但你可以像查詢包含儲存事實的表一樣查詢它們。 -在[示例 3-12](/tw/ch3#fig_datalog_query) 中,我們定義了三個派生表:`within_recursive`、`migrated` 和 `us_to_europe`。虛擬表的名稱和列由每個規則的 `:-` 符號之前出現的內容定義。例如,`migrated(PName, BornIn, LivingIn)` 是一個具有三列的虛擬表:一個人的姓名、他們出生地的名稱和他們居住地的名稱。 +在 [示例 3-12](/tw/ch3#fig_datalog_query) 中,我們定義了三個派生表:`within_recursive`、`migrated` 和 `us_to_europe`。虛擬表的名稱和列由每個規則的 `:-` 符號之前出現的內容定義。例如,`migrated(PName, BornIn, LivingIn)` 是一個具有三列的虛擬表:一個人的姓名、他們出生地的名稱和他們居住地的名稱。 -虛擬表的內容由規則的 `:-` 符號之後的部分定義,我們在其中嘗試在表中查詢與某種模式匹配的行。例如,`person(PersonID, PName)` 匹配行 `person(100, "Lucy")`,變數 `PersonID` 繫結到值 `100`,變數 `PName` 繫結到值 `"Lucy"`。如果系統可以為 `:-` 運算子右側的 **所有** 模式找到匹配,則規則適用。當規則適用時,就好像 `:-` 的左側被新增到資料庫中(變數被它們匹配的值替換)。 +虛擬表的內容由規則的 `:-` 符號之後的部分定義,我們在其中嘗試查詢表中匹配某種模式的行。例如,`person(PersonID, PName)` 匹配行 `person(100, "Lucy")`,變數 `PersonID` 繫結到值 `100`,變數 `PName` 繫結到值 `"Lucy"`。如果系統可以為 `:-` 運算子右側的 *所有* 模式找到匹配項,則規則適用。當規則適用時,就好像 `:-` 的左側被新增到資料庫中(變數被它們匹配的值替換)。 -因此,應用規則的一種可能方式是(如[圖 3-7](/tw/ch3#fig_datalog_naive) 所示): +因此,應用規則的一種可能方式是(如 [圖 3-7](/tw/ch3#fig_datalog_naive) 所示): 1. `location(1, "North America", "continent")` 存在於資料庫中,因此規則 1 適用。它生成 `within_recursive(1, "North America")`。 2. `within(2, 1)` 存在於資料庫中,前一步生成了 `within_recursive(1, "North America")`,因此規則 2 適用。它生成 `within_recursive(2, "North America")`。 @@ -774,21 +791,21 @@ Cypher 和 SPARQL 直接用 `SELECT` 開始,但 Datalog 一次只邁出一小 透過重複應用規則 1 和 2,`within_recursive` 虛擬表可以告訴我們資料庫中包含的北美(或任何其他位置)的所有位置。 -{{< figure link="#fig_datalog_query" src="/fig/ddia_0307.png" id="fig_datalog_naive" title="圖 3-7. 使用[示例 3-12](/tw/ch3#fig_datalog_query) 中的 Datalog 規則確定愛達荷州在北美。" class="w-full my-4" >}} +{{< figure link="#fig_datalog_query" src="/fig/ddia_0307.png" id="fig_datalog_naive" title="圖 3-7. 使用示例 3-12 中的 Datalog 規則確定愛達荷州在北美。" class="w-full my-4" >}} -> 圖 3-7. 使用[示例 3-12](/tw/ch3#fig_datalog_query) 中的 Datalog 規則確定愛達荷州在北美。 +> 圖 3-7. 使用 [示例 3-12](/tw/ch3#fig_datalog_query) 中的 Datalog 規則確定愛達荷州在北美。 -現在規則 3 可以找到出生在某個位置 `BornIn` 並住在某個位置 `LivingIn` 的人。規則 4 呼叫規則 3,其中 `BornIn = 'United States'` 和 `LivingIn = 'Europe'`,並僅返回匹配搜尋的人的姓名。透過查詢虛擬 `us_to_europe` 表的內容,Datalog 系統最終得到與早期 Cypher 和 SPARQL 查詢相同的答案。 +現在規則 3 可以找到出生在某個位置 `BornIn` 並居住在某個位置 `LivingIn` 的人。規則 4 使用 `BornIn = 'United States'` 和 `LivingIn = 'Europe'` 呼叫規則 3,並僅返回匹配搜尋的人的姓名。透過查詢虛擬 `us_to_europe` 表的內容,Datalog 系統最終得到與早期 Cypher 和 SPARQL 查詢相同的答案。 -與本章討論的其他查詢語言相比,Datalog 方法需要不同的思維方式。它允許逐條規則地構建複雜查詢,一個規則引用其他規則,類似於你將程式碼分解為相互呼叫的函式的方式。就像函式可以遞迴一樣,Datalog 規則也可以呼叫自己,就像[示例 3-12](/tw/ch3#fig_datalog_query) 中的規則 2,它在 Datalog 查詢中啟用圖遍歷。 +與本章討論的其他查詢語言相比,Datalog 方法需要不同型別的思維。它允許逐條規則地構建複雜查詢,一個規則引用其他規則,類似於你將程式碼分解為相互呼叫的函式的方式。就像函式可以遞迴一樣,Datalog 規則也可以呼叫自己,如 [示例 3-12](/tw/ch3#fig_datalog_query) 中的規則 2,這使得 Datalog 查詢中的圖遍歷成為可能。 ### GraphQL {#id63} -GraphQL 是一種查詢語言,設計上比本章中看到的其他查詢語言更具限制性。GraphQL 的目的是允許在使用者裝置上執行的客戶端軟體(例如移動應用程式或 JavaScript Web 應用程式前端)請求具有特定結構的 JSON 文件,其中包含呈現其使用者介面所需的欄位。GraphQL 介面允許開發人員快速更改客戶端程式碼中的查詢,而無需更改伺服器端 API。 +GraphQL 是一種查詢語言,從設計上講,它比我們在本章中看到的其他查詢語言限制性更強。GraphQL 的目的是允許在使用者裝置上執行的客戶端軟體(如移動應用程式或 JavaScript Web 應用程式前端)請求具有特定結構的 JSON 文件,其中包含渲染其使用者介面所需的欄位。GraphQL 介面允許開發人員快速更改客戶端程式碼中的查詢,而無需更改伺服器端 API。 -GraphQL 的靈活性是有代價的。採用 GraphQL 的組織通常需要工具將 GraphQL 查詢轉換為對內部服務的請求,這些服務通常使用 REST 或 gRPC(參見[第 5 章](/tw/ch5#ch_encoding))。授權、速率限制和效能挑戰是其他問題 [^61]。GraphQL 的查詢語言也受到限制,因為 GraphQL 來自不受信任的來源。該語言不允許任何可能執行成本高昂的內容,否則使用者可以透過執行大量昂貴的查詢對伺服器執行拒絕服務攻擊。特別是,GraphQL 不允許遞迴查詢(與 Cypher、SPARQL、SQL 或 Datalog 不同),並且它不允許任意搜尋條件,例如"查找出生在美國現在住在歐洲的人"(除非服務所有者特別選擇提供此類搜尋功能)。 +GraphQL 的靈活性是有代價的。採用 GraphQL 的組織通常需要工具將 GraphQL 查詢轉換為對內部服務的請求,這些服務通常使用 REST 或 gRPC(參見 [第 5 章](/tw/ch5#ch_encoding))。授權、速率限制和效能挑戰是額外的關注點 [^61]。GraphQL 的查詢語言也受到限制,因為 GraphQL 來自不受信任的來源。該語言不允許任何可能執行成本高昂的操作,否則使用者可能透過執行大量昂貴的查詢對伺服器執行拒絕服務攻擊。特別是,GraphQL 不允許遞迴查詢(與 Cypher、SPARQL、SQL 或 Datalog 不同),並且不允許任意搜尋條件,如"查詢在美國出生並現在居住在歐洲的人"(除非服務所有者特別選擇提供此類搜尋功能)。 -儘管如此,GraphQL 還是很有用的。[示例 3-13](/tw/ch3#fig_graphql_query) 顯示瞭如何使用 GraphQL 實現群聊應用程式(如 Discord 或 Slack)。查詢請求使用者有權訪問的所有頻道,包括頻道名稱和每個頻道中最近的 50 條訊息。對於每條訊息,它請求時間戳、訊息內容以及訊息傳送者的姓名和頭像 URL。此外,如果訊息是對另一條訊息的回覆,查詢還請求傳送者姓名和它回覆的訊息內容(可能以較小的字型呈現在回覆上方,以提供一些上下文)。 +儘管如此,GraphQL 還是很有用的。[示例 3-13](/tw/ch3#fig_graphql_query) 顯示瞭如何使用 GraphQL 實現 Discord 或 Slack 等群聊應用程式。查詢請求使用者有權訪問的所有頻道,包括頻道名稱和每個頻道中的 50 條最新訊息。對於每條訊息,它請求時間戳、訊息內容以及訊息傳送者的姓名和個人資料圖片 URL。此外,如果訊息是對另一條訊息的回覆,查詢還會請求傳送者姓名和它所回覆的訊息內容(可能以較小的字型呈現在回覆上方,以提供一些上下文)。 {{< figure id="fig_graphql_query" title="示例 3-13. 群聊應用程式的示例 GraphQL 查詢" class="w-full my-4" >}} @@ -814,9 +831,9 @@ query ChatApp { } ``` -[示例 3-14](/tw/ch3#fig_graphql_response) 顯示了對[示例 3-13](/tw/ch3#fig_graphql_query) 中查詢的響應可能是什麼樣子。響應是一個反映查詢結構的 JSON 文件:它包含恰好請求的那些屬性,不多也不少。這種方法的優點是伺服器不需要知道客戶端需要哪些屬性來呈現使用者介面;相反,客戶端可以簡單地請求它需要的內容。例如,此查詢不為 `replyTo` 訊息的傳送者請求頭像 URL,但如果使用者介面更改為新增該頭像,客戶端很容易將所需的 `imageUrl` 屬性新增到查詢中,而無需更改伺服器。 +[示例 3-14](/tw/ch3#fig_graphql_response) 顯示了對 [示例 3-13](/tw/ch3#fig_graphql_query) 中查詢的響應可能是什麼樣子。響應是一個反映查詢結構的 JSON 文件:它正好包含請求的那些屬性,不多也不少。這種方法的優點是伺服器不需要知道客戶端需要哪些屬性來渲染使用者介面;相反,客戶端可以簡單地請求它需要的內容。例如,此查詢不會為 `replyTo` 訊息的傳送者請求個人資料圖片 URL,但如果使用者介面更改為新增該個人資料圖片,客戶端可以很容易地將所需的 `imageUrl` 屬性新增到查詢中,而無需更改伺服器。 -{{< figure id="fig_graphql_response" title="示例 3-14. 對[示例 3-13](/tw/ch3#fig_graphql_query) 中查詢的可能響應" class="w-full my-4" >}} +{{< figure link="#fig_graphql_query" id="fig_graphql_response" title="示例 3-14. 對 示例 3-13 中查詢的可能響應" class="w-full my-4" >}} ```json { @@ -843,111 +860,111 @@ query ChatApp { ... ``` -在[示例 3-14](/tw/ch3#fig_graphql_response) 中,訊息傳送者的姓名和影像 URL 直接嵌入在訊息物件中。如果同一使用者傳送多條訊息,此資訊會在每條訊息上重複。原則上,可以減少這種重複,但 GraphQL 做出了設計選擇,接受更大的響應大小,以便根據資料更簡單地呈現使用者介面。 +在 [示例 3-14](/tw/ch3#fig_graphql_response) 中,訊息傳送者的姓名和影像 URL 直接嵌入在訊息物件中。如果同一使用者傳送多條訊息,此資訊會在每條訊息上重複。原則上,可以減少這種重複,但 GraphQL 做出了接受更大響應大小的設計選擇,以便更簡單地基於資料渲染使用者介面。 -`replyTo` 欄位類似:在[示例 3-14](/tw/ch3#fig_graphql_response) 中,第二條訊息是對第一條訊息的回覆,內容("Hey!…")和傳送者 Aaliyah 在 `replyTo` 下重複。可以改為返回被回覆訊息的 ID,但如果該 ID 不在返回的最近 50 條訊息中,客戶端將不得不向伺服器發出額外的請求。複製內容使處理資料變得更加簡單。 +`replyTo` 欄位類似:在 [示例 3-14](/tw/ch3#fig_graphql_response) 中,第二條訊息是對第一條訊息的回覆,內容("Hey!…")和傳送者 Aaliyah 在 `replyTo` 下重複。可以改為返回被回覆訊息的 ID,但如果該 ID 不在返回的 50 條最新訊息中,客戶端就必須向伺服器發出額外的請求。重複內容使得處理資料變得更加簡單。 -伺服器的資料庫可以以更規範化的形式儲存資料,並執行必要的連線來處理查詢。例如,伺服器可能儲存訊息以及傳送者的使用者 ID 和它回覆的訊息的 ID;當它收到像上面這樣的查詢時,伺服器會解析這些 ID 以找到它們引用的記錄。但是,客戶端只能要求伺服器執行 GraphQL 模式中明確提供的連線。 +伺服器的資料庫可以以更規範化的形式儲存資料,並執行必要的連線來處理查詢。例如,伺服器可能儲存訊息以及傳送者的使用者 ID 和它所回覆的訊息的 ID;當它收到如上所示的查詢時,伺服器將解析這些 ID 以查詢它們引用的記錄。但是,客戶端只能要求伺服器執行 GraphQL 模式中明確提供的連線。 -即使對 GraphQL 查詢的響應看起來類似於文件資料庫的響應,即使它的名稱中有"graph",GraphQL 也可以在任何型別的資料庫——關係、文件或圖——之上實現。 +即使對 GraphQL 查詢的響應看起來類似於文件資料庫的響應,即使它的名稱中有"graph",GraphQL 也可以在任何型別的資料庫之上實現 —— 關係型、文件型或圖型。 ## 事件溯源與 CQRS {#sec_datamodels_events} -在我們迄今為止討論的所有資料模型中,資料的查詢形式與寫入形式相同——無論是 JSON 文件、表中的行,還是圖中的頂點和邊。然而,在複雜的應用中,有時很難找到一種能夠滿足所有不同查詢和展示需求的單一資料表示。在這種情況下,以一種形式寫入資料,然後從中派生出針對不同型別讀取最佳化的多種表示,可能會很有益處。 +在我們迄今為止討論的所有資料模型中,資料以與寫入相同的形式被查詢 —— 無論是 JSON 文件、表中的行,還是圖中的頂點和邊。然而,在複雜的應用程式中,有時很難找到一種能夠滿足所有不同查詢和呈現資料方式的單一資料表示。在這種情況下,以一種形式寫入資料,然後從中派生出針對不同型別讀取最佳化的多種表示形式可能是有益的。 -我們之前在[「記錄系統和派生資料」](/tw/ch1#sec_introduction_derived)中看到過這個想法,ETL(參見[「資料倉庫」](/tw/ch1#sec_introduction_dwh))就是這種派生過程的一個例子。現在我們將進一步深入這個想法。如果我們無論如何都要從一種資料表示派生出另一種,我們可以選擇分別針對寫入和讀取最佳化的不同表示。如果你只想最佳化寫入,而不關心高效查詢,你會如何建模你的資料? +我們之前在 ["記錄系統和派生資料"](/tw/ch1#sec_introduction_derived) 中看到了這個想法,ETL(參見 ["資料倉庫"](/tw/ch1#sec_introduction_dwh))就是這種派生過程的一個例子。現在我們將進一步深入這個想法。如果我們無論如何都要從一種資料表示派生出另一種,我們可以選擇分別針對寫入和讀取最佳化的不同表示。如果你只想為寫入最佳化資料建模,而不關心高效查詢,你會如何建模? -也許寫入資料最簡單、最快、最具表現力的方式是**事件日誌**:每次你想寫入一些資料時,你將其編碼為自包含的字串(可能是 JSON),包含時間戳,然後將其追加到事件序列中。這個日誌中的事件是**不可變的**:你永遠不會更改或刪除它們,你只會向日志追加更多事件(這些事件可能會取代之前的事件)。一個事件可以包含任意屬性。 +也許寫入資料的最簡單、最快速和最具表現力的方式是 *事件日誌*:每次你想寫入一些資料時,你將其編碼為自包含的字串(可能是 JSON),包括時間戳,然後將其追加到事件序列中。此日誌中的事件是 *不可變的*:你永遠不會更改或刪除它們,你只會向日志追加更多事件(這可能會取代早期事件)。事件可以包含任意屬性。 -[圖 3-8](/tw/ch3#fig_event_sourcing) 顯示了一個可能來自會議管理系統的示例。會議可能是一個複雜的業務領域:不僅個人參會者可以註冊並透過信用卡支付,公司也可以批次訂購座位,透過發票支付,然後稍後將座位分配給個人。一定數量的座位可能會為演講者、贊助商、志願者助手等預留。預訂也可能被取消,與此同時,會議組織者可能透過將活動移至不同的房間來改變活動的容量。在所有這些情況下,簡單地計算可用座位數就成為一個具有挑戰性的查詢。 +[圖 3-8](/tw/ch3#fig_event_sourcing) 顯示了一個可能來自會議管理系統的示例。會議可能是一個複雜的業務領域:不僅個人參與者可以註冊並用信用卡付款,公司也可以批次訂購座位,透過發票付款,然後再將座位分配給個人。一些座位可能為演講者、贊助商、志願者助手等保留。預訂也可能被取消,與此同時,會議組織者可能透過將其移至不同的房間來更改活動的容量。在所有這些情況發生時,簡單地計算可用座位數量就成為一個具有挑戰性的查詢。 -{{< figure src="/fig/ddia_0308.png" id="fig_event_sourcing" title="圖 3-8. 使用不可變事件日誌作為事實來源,並從中派生物化檢視。" class="w-full my-4" >}} +{{< figure src="/fig/ddia_0308.png" id="fig_event_sourcing" title="圖 3-8. 使用不可變事件日誌作為真相源,並從中派生物化檢視。" class="w-full my-4" >}} -在[圖 3-8](/tw/ch3#fig_event_sourcing) 中,會議狀態的每個變化(例如組織者開放註冊,或參會者進行和取消註冊)首先被儲存為事件。每當一個事件被追加到日誌時,幾個**物化檢視**(也稱為**投影**或**讀模型**)也會被更新以反映該事件的影響。在會議示例中,可能有一個物化檢視收集與每個預訂狀態相關的所有資訊,另一個為會議組織者的儀表板計算圖表,第三個為製作參會者徽章的印表機生成檔案。 +在 [圖 3-8](/tw/ch3#fig_event_sourcing) 中,會議狀態的每個變化(例如組織者開放註冊,或參與者進行和取消註冊)首先被儲存為事件。每當事件追加到日誌時,幾個 *物化檢視*(也稱為 *投影* 或 *讀模型*)也會更新以反映該事件的影響。在會議示例中,可能有一個物化檢視收集與每個預訂狀態相關的所有資訊,另一個為會議組織者的儀表板計算圖表,第三個為列印參與者徽章的印表機生成檔案。 -使用事件作為事實來源,並將每個狀態變化表示為事件的想法被稱為**事件溯源** [^62] [^63]。維護單獨的讀最佳化表示並從寫最佳化表示派生它們的原則被稱為**命令查詢責任分離(CQRS)** [^64]。這些術語起源於領域驅動設計(DDD)社群,儘管類似的想法已經存在很長時間了,例如在**狀態機複製**中(參見[「使用共享日誌」](/tw/ch10#sec_consistency_smr))。 +使用事件作為真相源,並將每個狀態變化表達為事件的想法被稱為 *事件溯源* [^62] [^63]。維護單獨的讀最佳化表示並從寫最佳化表示派生它們的原則稱為 *命令查詢責任分離(CQRS)* [^64]。這些術語起源於領域驅動設計(DDD)社群,儘管類似的想法已經存在很長時間了,例如 *狀態機複製*(參見 ["使用共享日誌"](/tw/ch10#sec_consistency_smr))。 -當用戶的請求進來時,它被稱為**命令**,首先需要被驗證。只有當命令被執行並被確定為有效(例如,有足夠的可用座位來滿足請求的預訂)後,它才成為事實,相應的事件被新增到日誌中。因此,事件日誌應該只包含有效事件,構建物化檢視的事件日誌消費者不允許拒絕事件。 +當用戶的請求進來時,它被稱為 *命令*,首先需要驗證。只有在命令已執行並確定有效(例如,請求的預訂有足夠的可用座位)後,它才成為事實,相應的事件被新增到日誌中。因此,事件日誌應該只包含有效事件,構建物化檢視的事件日誌消費者不允許拒絕事件。 -在以事件溯源風格建模資料時,建議你使用過去時命名事件(例如,"座位已被預訂"),因為事件是記錄過去發生的事實。即使使用者後來決定更改或取消,他們曾經持有預訂的事實仍然是真實的,更改或取消是稍後新增的單獨事件。 +在以事件溯源風格建模資料時,建議你使用過去時態命名事件(例如,"座位已預訂"),因為事件是記錄過去發生的事情的記錄。即使使用者後來決定更改或取消,他們以前持有預訂的事實仍然是真實的,更改或取消是稍後新增的單獨事件。 -事件溯源與[「星型和雪花型:分析模式」](/tw/ch3#sec_datamodels_analytics)中討論的星型模式事實表之間的相似之處在於,兩者都是過去發生的事件集合。然而,事實表中的行都具有相同的列集,而在事件溯源中可能有許多不同的事件型別,每種都有不同的屬性。此外,事實表是無序集合,而在事件溯源中事件的順序很重要:如果先進行預訂然後取消,以錯誤的順序處理這些事件是沒有意義的。 +事件溯源與星型模式事實表之間的相似之處(如 ["星型與雪花型:分析模式"](/tw/ch3#sec_datamodels_analytics) 中所討論的)是兩者都是過去發生的事件的集合。然而,事實表中的行都具有相同的列集,而在事件溯源中可能有許多不同的事件型別,每種都有不同的屬性。此外,事實表是無序集合,而在事件溯源中事件的順序很重要:如果先進行預訂然後取消,以錯誤的順序處理這些事件將沒有意義。 事件溯源和 CQRS 有幾個優點: -* 對於開發系統的人來說,事件更好地傳達了**為什麼**某事發生的意圖。例如,理解"預訂被取消"這個事件比理解"`bookings` 表第 4001 行的 `active` 列被設定為 `false`,與該預訂相關的三行從 `seat_assignments` 表中被刪除,代表退款的一行被插入到 `payments` 表中"要容易得多。當物化檢視處理取消事件時,這些行修改可能仍然會發生,但當它們由事件驅動時,更新的原因變得更加清晰。 -* 事件溯源的一個關鍵原則是物化檢視以可重現的方式從事件日誌派生:你應該始終能夠刪除物化檢視並透過使用相同的程式碼以相同的順序處理相同的事件來重新計算它們。如果檢視維護程式碼中有錯誤,你可以刪除檢視並使用新程式碼重新計算它。找到錯誤也更容易,因為你可以根據需要多次重新執行檢視維護程式碼並檢查其行為。 -* 你可以擁有多個針對應用程式所需特定查詢最佳化的物化檢視。它們可以儲存在與事件相同的資料庫中或不同的資料庫中,具體取決於你的需求。它們可以使用任何資料模型,並且可以為快速讀取進行反規範化。你甚至可以只將檢視儲存在記憶體中而避免持久化,只要在服務重啟時從事件日誌重新計算檢視是可以接受的。 -* 如果你決定以新的方式呈現現有資訊,很容易從現有事件日誌構建新的物化檢視。你還可以透過新增新型別的事件或向現有事件型別新增新屬性(任何舊事件保持不變)來演進系統以支援新功能。你還可以將新行為連結到現有事件(例如,當會議參會者取消時,他們的座位可以提供給等候名單上的下一個人)。 -* 如果事件被錯誤地寫入,你可以再次刪除它,然後你可以在沒有刪除的事件的情況下重建檢視。另一方面,在直接更新和刪除資料的資料庫中,已提交的事務通常很難逆轉。因此,事件溯源可以減少系統中不可逆操作的數量,使其更容易更改(參見[「可演化性:讓變更變得容易」](/tw/ch2#sec_introduction_evolvability))。 +* 對於開發系統的人來說,事件更好地傳達了 *為什麼* 發生某事的意圖。例如,理解事件"預訂已取消"比理解"`bookings` 表第 4001 行的 `active` 列被設定為 `false`,與該預訂相關的三行從 `seat_assignments` 表中刪除,並且在 `payments` 表中插入了一行代表退款"更容易。當物化檢視處理取消事件時,這些行修改仍可能發生,但當它們由事件驅動時,更新的原因變得更加清晰。 +* 事件溯源的關鍵原則是物化檢視以可重現的方式從事件日誌派生:你應該始終能夠刪除物化檢視並透過以相同順序處理相同事件,使用相同程式碼來重新計算它們。如果檢視維護程式碼中有錯誤,你可以刪除檢視並使用新程式碼重新計算它。查詢錯誤也更容易,因為你可以隨意重新執行檢視維護程式碼並檢查其行為。 +* 你可以有多個物化檢視,針對應用程式所需的特定查詢進行最佳化。它們可以儲存在與事件相同的資料庫中,也可以儲存在不同的資料庫中,具體取決於你的需求。它們可以使用任何資料模型,並且可以為快速讀取而反規範化。你甚至可以只在記憶體中保留檢視並避免持久化它,只要可以在服務重新啟動時從事件日誌重新計算檢視即可。 +* 如果你決定以新方式呈現現有資訊,很容易從現有事件日誌構建新的物化檢視。你還可以透過新增新型別的事件或向現有事件型別新增新屬性(任何舊事件保持未修改)來發展系統以支援新功能。你還可以將新行為連結到現有事件(例如,當會議參與者取消時,他們的座位可以提供給等候名單上的下一個人)。 +* 如果事件被錯誤寫入,你可以再次刪除它,然後可以在沒有刪除事件的情況下重建檢視。另一方面,在直接更新和刪除資料的資料庫中,已提交的事務通常很難撤銷。因此,事件溯源可以減少系統中不可逆操作的數量,使其更容易更改(參見 ["可演化性:讓變更變得容易"](/tw/ch2#sec_introduction_evolvability))。 * 事件日誌還可以作為系統中發生的所有事情的審計日誌,這在需要此類可審計性的受監管行業中很有價值。 然而,事件溯源和 CQRS 也有缺點: -* 如果涉及外部資訊,你需要小心。例如,假設一個事件包含以一種貨幣給出的價格,對於其中一個檢視,它需要轉換為另一種貨幣。由於匯率可能會波動,在處理事件時從外部源獲取匯率會有問題,因為如果你在另一個日期重新計算物化檢視,你會得到不同的結果。為了使事件處理邏輯具有確定性,你要麼需要在事件本身中包含匯率,要麼有辦法查詢事件中指示的時間戳處的歷史匯率,確保此查詢對於相同的時間戳始終返回相同的結果。 -* 事件不可變的要求在事件包含使用者個人資料時會產生問題,因為使用者可能會行使他們的權利(例如,根據 GDPR)請求刪除他們的資料。如果事件日誌是基於每個使用者的,你可以刪除該使用者的整個日誌,但如果你的事件日誌包含與多個使用者相關的事件,這就不起作用了。你可以嘗試將個人資料儲存在實際事件之外,或使用你以後可以選擇刪除的金鑰對其進行加密,但這也使得在需要時重新計算派生狀態變得更加困難。 -* 如果有外部可見的副作用,重新處理事件需要謹慎——例如,你可能不希望每次重建物化檢視時都重新發送確認電子郵件。 +* 如果涉及外部資訊,你需要小心。例如,假設一個事件包含以一種貨幣給出的價格,對於其中一個檢視,它需要轉換為另一種貨幣。由於匯率可能會波動,在處理事件時從外部源獲取匯率會有問題,因為如果你在另一個日期重新計算物化檢視,你會得到不同的結果。為了使事件處理邏輯具有確定性,你要麼需要在事件本身中包含匯率,要麼有一種方法來查詢事件中指示的時間戳處的歷史匯率,確保此查詢始終為相同的時間戳返回相同的結果。 +* 事件不可變的要求會在事件包含使用者的個人資料時產生問題,因為使用者可能行使他們的權利(例如,根據 GDPR)請求刪除他們的資料。如果事件日誌是基於每個使用者的,你可以刪除該使用者的整個日誌,但如果你的事件日誌包含與多個使用者相關的事件,這就不起作用了。你可以嘗試將個人資料儲存在實際事件之外,或者使用金鑰對其進行加密,你可以稍後選擇刪除該金鑰,但這也使得在需要時更難重新計算派生狀態。 +* 如果存在外部可見的副作用,重新處理事件需要小心 —— 例如,你可能不希望每次重建物化檢視時都重新發送確認電子郵件。 -你可以在任何資料庫之上實現事件溯源,但也有一些專門設計用於支援此模式的系統,例如 EventStoreDB、MartenDB(基於 PostgreSQL)和 Axon Framework。你還可以使用訊息代理(如 Apache Kafka)來儲存事件日誌,流處理器可以保持物化檢視是最新的;我們將在 [Link to Come] 中回到這些主題。 +你可以在任何資料庫之上實現事件溯源,但也有一些專門設計來支援這種模式的系統,例如 EventStoreDB、MartenDB(基於 PostgreSQL)和 Axon Framework。你還可以使用訊息代理(如 Apache Kafka)來儲存事件日誌,流處理器可以使物化檢視保持最新;我們將在 [待補充連結] 中返回這些主題。 -唯一重要的要求是事件儲存系統必須保證所有物化檢視以與它們在日誌中出現的完全相同的順序處理事件;正如我們將在[第 10 章](/tw/ch10#ch_consistency)中看到的,這在分散式系統中並不總是容易實現的。 +唯一重要的要求是事件儲存系統必須保證所有物化檢視以與它們在日誌中出現的完全相同的順序處理事件;正如我們將在 [第 10 章](/tw/ch10#ch_consistency) 中看到的,這在分散式系統中並不總是容易實現。 ## 資料框、矩陣與陣列 {#sec_datamodels_dataframes} -本章到目前為止我們看到的資料模型通常既用於事務處理也用於分析目的(參見[「分析型與事務型系統」](/tw/ch1#sec_introduction_analytics))。還有一些資料模型你可能會在分析或科學背景下遇到,但很少出現在 OLTP 系統中:資料框和多維數值陣列(如矩陣)。 +到目前為止,我們在本章中看到的資料模型通常用於事務處理和分析目的(參見 ["分析與運營系統"](/tw/ch1#sec_introduction_analytics))。還有一些資料模型你可能會在分析或科學環境中遇到,但很少出現在 OLTP 系統中:資料框和多維數字陣列(如矩陣)。 -資料框是 R 語言、Python 的 Pandas 庫、Apache Spark、ArcticDB、Dask 和其他系統支援的資料模型。它們是資料科學家準備資料以訓練機器學習模型的流行工具,但它們也廣泛用於資料探索、統計資料分析、資料視覺化和類似目的。 +資料框是 R 語言、Python 的 Pandas 庫、Apache Spark、ArcticDB、Dask 和其他系統支援的資料模型。它們是資料科學家為訓練機器學習模型準備資料的流行工具,但它們也廣泛用於資料探索、統計資料分析、資料視覺化和類似目的。 -乍一看,資料框類似於關係資料庫中的表或電子表格。它支援對資料框內容執行批次操作的類關係運算元:例如,對所有行應用函式、基於某些條件過濾行、按某些列分組行並聚合其他列,以及基於某個鍵將一個數據框中的行與另一個數據框連線(關係資料庫稱為**連線**的操作在資料框中通常稱為**合併**)。 +乍一看,資料框類似於關係資料庫中的表或電子表格。它支援對資料框內容執行批次操作的類關係運算符:例如,將函式應用於所有行、基於某些條件過濾行、按某些列對行進行分組並聚合其他列,以及基於某個鍵將一個數據框中的行與另一個數據框連線(關係資料庫稱為 *連線* 的操作在資料框上通常稱為 *合併*)。 -資料框通常不是透過 SQL 等宣告式查詢來操作,而是透過一系列修改其結構和內容的命令來操作。這符合資料科學家的典型工作流程,他們會逐步"整理"資料,使其成為能夠找到他們所提問題答案的形式。這些操作通常在資料科學家的私有資料集副本上進行,通常在他們的本地機器上,儘管最終結果可能會與其他使用者共享。 +資料框通常不是透過宣告式查詢(如 SQL)而是透過一系列修改其結構和內容的命令來操作的。這符合資料科學家的典型工作流程,他們逐步"整理"資料,使其成為能夠找到他們所提問題答案的形式。這些操作通常在資料科學家的資料集私有副本上進行,通常在他們的本地機器上,儘管最終結果可能與其他使用者共享。 -資料框 API 還提供了遠遠超出關係資料庫所提供的各種操作,資料模型的使用方式通常與典型的關係資料建模截然不同 [^65]。例如,資料框的一個常見用途是將資料從類關係表示轉換為矩陣或多維陣列表示,這是許多機器學習演算法期望的輸入形式。 +資料框 API 還提供了遠遠超出關係資料庫提供的各種操作,資料模型的使用方式通常與典型的關係資料建模非常不同 [^65]。例如,資料框的常見用途是將資料從類似關係的表示轉換為矩陣或多維陣列表示,這是許多機器學習演算法期望的輸入形式。 -[圖 3-9](/tw/ch3#fig_dataframe_to_matrix) 顯示了這種轉換的簡單示例。左邊我們有一個不同使用者如何對各種電影進行評分(1 到 5 分)的關係表,右邊資料已被轉換為矩陣,其中每列是一部電影,每行是一個使用者(類似於電子表格中的**透視表**)。矩陣是**稀疏的**,這意味著許多使用者-電影組合沒有資料,但這沒關係。這個矩陣可能有數千列,因此不太適合關係資料庫,但資料框和提供稀疏陣列的庫(如 Python 的 NumPy)可以輕鬆處理這樣的資料。 +[圖 3-9](/tw/ch3#fig_dataframe_to_matrix) 顯示了這種轉換的簡單示例。左側是不同使用者如何評價各種電影的關係表(評分為 1 到 5),右側資料已轉換為矩陣,其中每列是一部電影,每行是一個使用者(類似於電子表格中的 *資料透視表*)。矩陣是 *稀疏* 的,這意味著許多使用者-電影組合沒有資料,但這沒關係。這個矩陣可能有數千列,因此不太適合關係資料庫,但資料框和提供稀疏陣列的庫(如 Python 的 NumPy)可以輕鬆處理此類資料。 {{< figure src="/fig/ddia_0309.png" id="fig_dataframe_to_matrix" title="圖 3-9. 將電影評分的關係資料庫轉換為矩陣表示。" class="w-full my-4" >}} -矩陣只能包含數字,各種技術用於將非數值資料轉換為矩陣中的數字。例如: +矩陣只能包含數字,各種技術用於將非數字資料轉換為矩陣中的數字。例如: -* 日期(在[圖 3-9](/tw/ch3#fig_dataframe_to_matrix) 的示例矩陣中被省略)可以縮放為某個合適範圍內的浮點數。 -* 對於只能取一小組固定值之一的列(例如,電影資料庫中電影的型別),通常使用**獨熱編碼**:我們為每個可能的值建立一列(一個用於"喜劇",一個用於"戲劇",一個用於"恐怖"等),對於代表電影的每一行,我們在與該電影型別對應的列中放置 1,在所有其他列中放置 0。這種表示也很容易推廣到適合多種型別的電影。 +* 日期(在 [圖 3-9](/tw/ch3#fig_dataframe_to_matrix) 的示例矩陣中省略了)可以縮放為某個合適範圍內的浮點數。 +* 對於只能取一小組固定值之一的列(例如,電影資料庫中電影的型別),通常使用 *獨熱編碼*:我們為每個可能的值建立一列(一個用於"喜劇",一個用於"劇情",一個用於"恐怖"等),對於代表電影的每一行,我們在對應於該電影型別的列中放置 1,在所有其他列中放置 0。這種表示也很容易推廣到適合多種型別的電影。 -一旦資料以數字矩陣的形式存在,它就適合進行線性代數運算,這構成了許多機器學習演算法的基礎。例如,[圖 3-9](/tw/ch3#fig_dataframe_to_matrix) 中的資料可能是推薦使用者可能喜歡的電影的系統的一部分。資料框足夠靈活,允許資料從關係形式逐漸演變為矩陣表示,同時讓資料科學家控制最適合實現資料分析或模型訓練過程目標的表示。 +一旦資料以數字矩陣的形式存在,它就適合線性代數運算,這構成了許多機器學習演算法的基礎。例如,[圖 3-9](/tw/ch3#fig_dataframe_to_matrix) 中的資料可能是推薦使用者可能喜歡的電影系統的一部分。資料框足夠靈活,允許資料從關係形式逐漸演變為矩陣表示,同時讓資料科學家控制最適合實現資料分析或模型訓練過程目標的表示。 -還有一些資料庫(如 TileDB [^66])專門儲存大型多維數值陣列;它們被稱為**陣列資料庫**,最常用於科學資料集,如地理空間測量(規則間隔網格上的柵格資料)、醫學成像或天文望遠鏡的觀測 [^67]。資料框也在金融行業用於表示**時間序列資料**,如資產價格和交易隨時間的變化 [^68]。 +還有像 TileDB [^66] 這樣專門儲存大型多維數字陣列的資料庫;它們被稱為 *陣列資料庫*,最常用於科學資料集,如地理空間測量(規則間隔網格上的柵格資料)、醫學成像或天文望遠鏡的觀測 [^67]。資料框在金融行業也用於表示 *時間序列資料*,如資產價格和隨時間變化的交易 [^68]。 ## 總結 {#summary} -資料模型是一個龐大的主題,在本章中,我們快速瀏覽了各種不同的模型。我們沒有空間詳細介紹每個模型的所有細節,但希望這個概覽已經足以激發你的興趣,讓你進一步瞭解最適合你應用需求的模型。 +資料模型是一個巨大的主題,在本章中,我們快速瀏覽了各種不同的模型。我們沒有空間深入每個模型的所有細節,但希望這個概述足以激發你的興趣,找出最適合你的應用需求的模型。 -**關係模型**儘管已有半個多世紀的歷史,仍然是許多應用的重要資料模型——特別是在資料倉庫和商業分析中,關係星型或雪花型模式和 SQL 查詢無處不在。然而,關係資料的幾種替代方案也在其他領域變得流行: +*關係模型* 儘管已有半個多世紀的歷史,但對許多應用來說仍然是一個重要的資料模型——特別是在資料倉庫和商業分析中,關係星型或雪花模式和 SQL 查詢無處不在。然而,關係資料的幾種替代方案也在其他領域變得流行: -* **文件模型**針對資料以自包含 JSON 文件形式出現的用例,其中一個文件與另一個文件之間的關係很少。 -* **圖資料模型**走向相反的方向,針對任何事物都可能與任何事物相關的用例,以及查詢可能需要遍歷多個跳轉以找到感興趣的資料(可以使用 Cypher、SPARQL 或 Datalog 中的遞迴查詢來表達)。 -* **資料框**將關係資料推廣到大量列,從而在資料庫和構成許多機器學習、統計資料分析和科學計算基礎的多維陣列之間架起橋樑。 +* *文件模型* 針對資料以獨立的 JSON 文件形式出現的用例,以及一個文件與另一個文件之間的關係很少的情況。 +* *圖資料模型* 走向相反的方向,針對任何東西都可能與一切相關的用例,以及查詢可能需要遍歷多個跳躍才能找到感興趣的資料(可以使用 Cypher、SPARQL 或 Datalog 中的遞迴查詢來表達)。 +* *資料框* 將關係資料推廣到大量列,從而在資料庫和構成大量機器學習、統計資料分析和科學計算基礎的多維陣列之間提供橋樑。 -在某種程度上,一個模型可以用另一個模型來模擬——例如,圖資料可以在關係資料庫中表示——但結果可能會很彆扭,正如我們在 SQL 中對遞迴查詢的支援中看到的那樣。 +在某種程度上,一個模型可以用另一個模型來模擬——例如,圖資料可以在關係資料庫中表示——但結果可能很彆扭,正如我們在 SQL 中對遞迴查詢的支援中看到的那樣。 -因此,為每個資料模型開發了各種專門的資料庫,提供針對特定模型最佳化的查詢語言和儲存引擎。然而,資料庫也有一種趨勢是透過新增對其他資料模型的支援來擴充套件到相鄰的領域:例如,關係資料庫已經以 JSON 列的形式添加了對文件資料的支援,文件資料庫已經添加了類似關係的連線,SQL 中對圖資料的支援也在逐漸改善。 +因此,為每個資料模型開發了各種專業資料庫,提供針對特定模型最佳化的查詢語言和儲存引擎。然而,資料庫也有透過新增對其他資料模型的支援來擴充套件到相鄰領域的趨勢:例如,關係資料庫以 JSON 列的形式添加了對文件資料的支援,文件資料庫添加了類似關係的連線,SQL 中對圖資料的支援也在逐步改進。 -我們討論的另一個模型是**事件溯源**,它將資料表示為不可變事件的僅追加日誌,這對於在複雜業務領域中建模活動可能是有利的。僅追加日誌很適合寫入資料(正如我們將在[第 4 章](/tw/ch4#ch_storage)中看到的);為了支援高效查詢,事件日誌透過 CQRS 轉換為讀最佳化的物化檢視。 +我們討論的另一個模型是 *事件溯源*,它將資料表示為不可變事件的僅追加日誌,這對於建模複雜業務領域中的活動可能是有利的。僅追加日誌有利於寫入資料(正如我們將在 [第 4 章](/tw/ch4#ch_storage) 中看到的);為了支援高效查詢,事件日誌透過 CQRS 轉換為讀最佳化的物化檢視。 -非關係資料模型的一個共同點是,它們通常不會對儲存的資料強制執行模式,這可以使應用程式更容易適應不斷變化的需求。然而,你的應用程式很可能仍然假設資料具有某種結構;這只是一個問題,即模式是顯式的(在寫入時強制執行)還是隱式的(在讀取時假設)。 +非關係資料模型的一個共同點是,它們通常不會對儲存的資料強制執行模式,這可以使應用更容易適應不斷變化的需求。然而,你的應用很可能仍然假設資料具有某種結構;這只是模式是顯式的(在寫入時強制執行)還是隱式的(在讀取時假設)的問題。 -儘管我們已經涵蓋了很多內容,但仍有一些資料模型未被提及。舉幾個簡短的例子: +儘管我們涵蓋了很多內容,但仍有資料模型未被提及。僅舉幾個簡短的例子: -* 研究基因組資料的研究人員經常需要執行**序列相似性搜尋**,這意味著取一個很長的字串(代表 DNA 分子)並將其與大型字串資料庫進行匹配,這些字串相似但不相同。這裡描述的資料庫都不能處理這種用法,這就是為什麼研究人員編寫了專門的基因組資料庫軟體,如 GenBank [^69]。 -* 許多金融系統使用帶有複式記賬的**賬本**作為其資料模型。這種型別的資料可以在關係資料庫中表示,但也有像 TigerBeetle 這樣專門針對這種資料模型的資料庫。加密貨幣和區塊鏈通常基於分散式賬本,它們也將價值轉移內建到其資料模型中。 -* **全文搜尋**可以說是一種經常與資料庫一起使用的資料模型。資訊檢索是一個大型的專門主題,我們不會在本書中詳細介紹,但我們將在[「全文搜尋」](/tw/ch4#sec_storage_full_text)中涉及搜尋索引和向量搜尋。 +* 研究基因組資料的研究人員通常需要執行 *序列相似性搜尋*,這意味著獲取一個非常長的字串(代表 DNA 分子)並將其與相似但不相同的大量字串資料庫進行匹配。這裡描述的資料庫都無法處理這種用法,這就是研究人員編寫了像 GenBank [^69] 這樣的專門基因組資料庫軟體的原因。 +* 許多金融系統使用具有複式記賬的 *賬本* 作為其資料模型。這種型別的資料可以在關係資料庫中表示,但也有像 TigerBeetle 這樣專門研究這種資料模型的資料庫。加密貨幣和區塊鏈通常基於分散式賬本,它們的資料模型中也內建了價值轉移。 +* *全文檢索* 可以說是一種經常與資料庫一起使用的資料模型。資訊檢索是一個大型的專業主題,我們不會在本書中詳細介紹,但我們將在 ["全文檢索"](/tw/ch4#sec_storage_full_text) 中涉及搜尋索引和向量搜尋。 -我們現在必須就此打住。在下一章中,我們將討論在**實現**本章描述的資料模型時涉及的一些權衡。 +我們現在必須到此為止了。在下一章中,我們將討論在 *實現* 本章中描述的資料模型時出現的一些權衡。 -### 參考 +### 參考文獻 [^1]: Jamie Brandon. [Unexplanations: query optimization works because sql is declarative](https://www.scattered-thoughts.net/writing/unexplanations-sql-declarative/). *scattered-thoughts.net*, February 2024. Archived at [perma.cc/P6W2-WMFZ](https://perma.cc/P6W2-WMFZ) [^2]: Joseph M. Hellerstein. [The Declarative Imperative: Experiences and Conjectures in Distributed Logic](https://www2.eecs.berkeley.edu/Pubs/TechRpts/2010/EECS-2010-90.pdf). Tech report UCB/EECS-2010-90, Electrical Engineering and Computer Sciences, University of California at Berkeley, June 2010. Archived at [perma.cc/K56R-VVQM](https://perma.cc/K56R-VVQM) diff --git a/content/tw/ch4.md b/content/tw/ch4.md index 32aa20f..d3b7660 100644 --- a/content/tw/ch4.md +++ b/content/tw/ch4.md @@ -6,20 +6,20 @@ breadcrumbs: false ![](/map/ch03.png) -> *生活中的一大痛苦是,每個人給事物起的名字都有一點點不對。這讓世界上的一切都比換個名字後更難理解。計算機主要並不是在算術意義上進行計算。[...] 它們主要是歸檔系統。* +> *生活的苦惱之一是,每個人對事物的命名都有些偏差。這讓我們理解世界變得比本該有的樣子困難一些,要是命名方式不同就好了。計算機的主要功能並不是傳統意義上的計算,比如算術運算。[……] 它們主要是歸檔系統。* > -> [Richard Feynman](https://www.youtube.com/watch?v=EKWGGDXe5MA&t=296s), -> *特立獨行的思維* 研討會(1985) +> [理查德·費曼](https://www.youtube.com/watch?v=EKWGGDXe5MA&t=296s), +> *特立獨行的思考* 研討會(1985) -在最基礎的層面上,資料庫需要做兩件事:當你給它一些資料時,它應該儲存這些資料,當你稍後再次詢問時,它應該把資料返回給你。 +在最基礎的層面上,資料庫需要做兩件事:當你給它一些資料時,它應該儲存這些資料;當你之後再詢問時,它應該把資料返回給你。 -在 [第三章](/tw/ch3#ch_datamodels) 中,我們討論了資料模型和查詢語言——即你向資料庫提供資料的格式,以及稍後再次請求資料的介面。在本章中,我們從資料庫的角度討論同樣的問題:資料庫如何儲存你提供給它的資料,以及當你請求時如何再次找到資料。 +在 [第 3 章](/tw/ch3#ch_datamodels) 中,我們討論了資料模型和查詢語言 —— 即你向資料庫提供資料的格式,以及之後再次請求資料的介面。在本章中,我們從資料庫的角度討論同樣的問題:資料庫如何儲存你提供的資料,以及當你請求時如何再次找到這些資料。 -作為應用程式開發人員,為什麼要關心資料庫內部如何處理儲存和檢索?你可能不會從頭開始實現自己的儲存引擎,但你 *確實* 需要從眾多可用的儲存引擎中選擇一個適合你應用程式的。為了配置儲存引擎在你的工作負載型別上表現良好,你需要對儲存引擎在底層做什麼有個大致的瞭解。 +作為應用開發者,你為什麼要關心資料庫內部如何處理儲存和檢索?你可能不會從頭開始實現自己的儲存引擎,但你 *確實* 需要從眾多可用的儲存引擎中選擇一個適合你應用的。為了讓儲存引擎在你的工作負載型別上表現良好,你需要對儲存引擎在底層做了什麼有個大致的瞭解。 -特別是,針對事務性工作負載(OLTP)最佳化的儲存引擎與針對分析最佳化的儲存引擎之間存在很大差異(我們在 ["分析與作業系統"](/tw/ch1#sec_introduction_analytics) 中介紹了這種區別)。本章首先研究兩種 OLTP 儲存引擎系列:寫出不可變資料檔案的 *日誌結構* 儲存引擎,以及像 *B 樹* 這樣就地更新資料的儲存引擎。這些結構既用於鍵值儲存,也用於二級索引。 +特別是,針對事務型工作負載(OLTP)最佳化的儲存引擎和針對分析型工作負載最佳化的儲存引擎之間存在巨大差異(我們在 ["分析型與事務型系統"](/tw/ch1#sec_introduction_analytics) 中介紹了這種區別)。本章首先研究兩種用於 OLTP 的儲存引擎家族:寫入不可變資料檔案的 *日誌結構* 儲存引擎,以及像 *B 樹* 這樣就地更新資料的儲存引擎。這些結構既用於鍵值儲存,也用於二級索引。 -稍後在 ["分析型資料儲存"](/tw/ch4#sec_storage_analytics) 中,我們將討論一系列針對分析最佳化的儲存引擎,在 ["多維和全文索引"](/tw/ch4#sec_storage_multidimensional) 中,我們將簡要介紹用於更高階查詢(如文字檢索)的索引。 +隨後在 ["分析型資料儲存"](#sec_storage_analytics) 中,我們將討論一系列針對分析最佳化的儲存引擎;在 ["多維索引與全文索引"](#sec_storage_multidimensional) 中,我們將簡要介紹用於更高階查詢(如文字檢索)的索引。 ## OLTP 系統的儲存與索引 {#sec_storage_oltp} @@ -37,9 +37,9 @@ db_get () { } ``` -這兩個函式實現了一個鍵值儲存。你可以呼叫 `db_set key value`,它會在資料庫中儲存 `key` 和 `value`。鍵和值可以是(幾乎)任何你喜歡的東西——例如,值可以是 JSON 文件。然後你可以呼叫 `db_get key`,它會查詢與該特定鍵關聯的最新值並返回它。 +這兩個函式實現了一個鍵值儲存。你可以呼叫 `db_set key value`,它將在資料庫中儲存 `key` 和 `value`。鍵和值可以是(幾乎)任何你喜歡的內容 —— 例如,值可以是一個 JSON 文件。然後你可以呼叫 `db_get key`,它會查詢與該特定鍵關聯的最新值並返回它。 -它確實有效: +它確實能工作: ```bash $ db_set 12 '{"name":"London","attractions":["Big Ben","London Eye"]}' @@ -50,7 +50,7 @@ $ db_get 42 {"name":"San Francisco","attractions":["Golden Gate Bridge"]} ``` -儲存格式非常簡單:一個文字檔案,每行包含一個鍵值對,用逗號分隔(大致類似於 CSV 檔案,忽略轉義問題)。每次呼叫 `db_set` 都會追加到檔案末尾。如果你多次更新一個鍵,值的舊版本不會被覆蓋——你需要檢視檔案中鍵的最後一次出現來找到最新值(因此 `db_get` 中有 `tail -n 1`): +儲存格式非常簡單:一個文字檔案,每行包含一個鍵值對,用逗號分隔(大致類似 CSV 檔案,忽略轉義問題)。每次呼叫 `db_set` 都會追加到檔案末尾。如果你多次更新一個鍵,舊版本的值不會被覆蓋 —— 你需要檢視檔案中鍵的最後一次出現來找到最新值(因此 `db_get` 中使用了 `tail -n 1`): ```bash $ db_set 42 '{"name":"San Francisco","attractions":["Exploratorium"]}' @@ -65,303 +65,303 @@ $ cat database ``` -對於如此簡單的東西,`db_set` 函式實際上具有相當好的效能,因為追加到檔案通常非常高效。與 `db_set` 所做的類似,許多資料庫內部使用 *日誌*,這是一個僅追加的資料檔案。真正的資料庫需要處理更多問題(如處理併發寫入、回收磁碟空間以免日誌無限增長,以及在崩潰恢復時處理部分寫入的記錄),但基本原理是相同的。日誌非常有用,我們將在本書中多次遇到它們。 +對於如此簡單的實現,`db_set` 函式實際上有相當好的效能,因為追加到檔案通常非常高效。與 `db_set` 所做的類似,許多資料庫內部使用 *日誌*,這是一個僅追加的資料檔案。真正的資料庫有更多問題要處理(如處理併發寫入、回收磁碟空間以防日誌無限增長,以及從崩潰中恢復時處理部分寫入的記錄),但基本原理是相同的。日誌非常有用,我們將在本書中多次遇到它們。 --------- > [!NOTE] -> *日誌* 這個詞通常用於指應用程式日誌,其中應用程式輸出描述正在發生的事情的文字。在本書中,*日誌* 用於更一般的意義:磁碟上僅追加的記錄序列。它不必是人類可讀的;它可能是二進位制的,僅供資料庫系統內部使用。 +> *日誌* 這個詞通常用於指應用程式日誌,應用程式輸出描述正在發生什麼的文字。在本書中,*日誌* 用於更一般的含義:磁碟上僅追加的記錄序列。它不一定是人類可讀的;它可能是二進位制的,僅供資料庫系統內部使用。 -------- -另一方面,如果你的資料庫中有大量記錄,`db_get` 函式的效能會很糟糕。每次你想查詢一個鍵時,`db_get` 必須從頭到尾掃描整個資料庫檔案,尋找鍵的出現。用算法術語來說,查詢的成本是 *O*(*n*):如果你將資料庫中的記錄數 *n* 翻倍,查詢需要兩倍的時間。這不好。 +另一方面,如果你的資料庫中有大量記錄,`db_get` 函式的效能會很糟糕。每次你想查詢一個鍵時,`db_get` 必須從頭到尾掃描整個資料庫檔案,尋找該鍵的出現。用算法術語來說,查詢的成本是 *O*(*n*):如果你的資料庫中的記錄數 *n* 翻倍,查詢時間也會翻倍。這並不好。 -為了高效地找到資料庫中特定鍵的值,我們需要一種不同的資料結構:*索引*。在本章中,我們將研究一系列索引結構並比較它們;總體思路是以特定方式(例如,按某個鍵排序)構建資料,使定位所需資料更快。如果你想以幾種不同的方式搜尋相同的資料,你可能需要在資料的不同部分上建立幾個不同的索引。 +為了高效地找到資料庫中特定鍵的值,我們需要一個不同的資料結構:*索引*。在本章中,我們將研究一系列索引結構並瞭解它們的比較;一般思想是以特定方式(例如,按某個鍵排序)構建資料,使定位所需資料更快。如果你想以幾種不同的方式搜尋相同的資料,你可能需要在資料的不同部分上建立幾個不同的索引。 -索引是從主資料派生的 *額外* 結構。許多資料庫允許你新增和刪除索引,這不會影響資料庫的內容;它隻影響查詢的效能。維護額外的結構會產生開銷,特別是在寫入時。對於寫入,很難超越簡單追加到檔案的效能,因為這是最簡單的寫入操作。任何型別的索引通常都會減慢寫入速度,因為每次寫入資料時也需要更新索引。 +索引是從主資料派生出的 *額外* 結構。許多資料庫允許你新增和刪除索引,這不會影響資料庫的內容;它隻影響查詢的效能。維護額外的結構會產生開銷,特別是在寫入時。對於寫入,很難超越簡單地追加到檔案的效能,因為這是最簡單的寫入操作。任何型別的索引通常都會減慢寫入速度,因為每次寫入資料時也需要更新索引。 -這是儲存系統中的一個重要權衡:精心選擇的索引可以加速讀查詢,但每個索引都會消耗額外的磁碟空間並減慢寫入速度,有時會大幅減慢 [^1]。因此,資料庫通常不會預設為所有內容建立索引,而是需要你——編寫應用程式或管理資料庫的人——使用你對應用程式典型查詢模式的瞭解來手動選擇索引。然後你可以選擇為你的應用程式帶來最大收益的索引,而不會在寫入時引入超過必要的開銷。 +這是儲存系統中的一個重要權衡:精心選擇的索引加快了讀查詢速度,但每個索引都會消耗額外的磁碟空間並減慢寫入速度,有時會大幅減慢 [^1]。因此,資料庫通常不會預設為所有內容建立索引,而是要求你 —— 編寫應用程式或管理資料庫的人 —— 使用你對應用程式典型查詢模式的瞭解來手動選擇索引。然後你可以選擇為你的應用程式帶來最大收益的索引,而不會引入超過必要的寫入開銷。 ### 日誌結構儲存 {#sec_storage_log_structured} -首先,讓我們假設你想繼續將資料儲存在 `db_set` 寫入的僅追加檔案中,只是想加速讀取。你可以透過在記憶體中保留一個雜湊對映來做到這一點,其中每個鍵都對映到檔案中可以找到該鍵最新值的位元組偏移量,如 [圖 4-1](/tw/ch4#fig_storage_csv_hash_index) 所示。 +首先,讓我們假設你想繼續將資料儲存在 `db_set` 寫入的僅追加檔案中,你只是想加快讀取速度。一種方法是在記憶體中保留一個雜湊對映,其中每個鍵都對映到檔案中可以找到該鍵最新值的位元組偏移量,如 [圖 4-1](#fig_storage_csv_hash_index) 所示。 -{{< figure src="/fig/ddia_0401.png" id="fig_storage_csv_hash_index" caption="圖 4-1. 以類似 CSV 格式儲存鍵值對日誌,使用記憶體中的雜湊對映進行索引。" class="w-full my-4" >}} +{{< figure src="/fig/ddia_0401.png" id="fig_storage_csv_hash_index" caption="圖 4-1. 以類似 CSV 格式儲存鍵值對日誌,使用記憶體雜湊對映建立索引。" class="w-full my-4" >}} -每當你向檔案追加新的鍵值對時,你也會更新雜湊對映以反映剛剛寫入的資料的偏移量。當你想查詢一個值時,你使用雜湊對映找到日誌檔案中的偏移量,定址到該位置,然後讀取值。如果資料檔案的該部分已經在檔案系統快取中,讀取根本不需要任何磁碟 I/O。 +每當你向檔案追加新的鍵值對時,你也會更新雜湊對映以反映剛剛寫入資料的偏移量。當你想查詢一個值時,你使用雜湊對映找到日誌檔案中的偏移量,尋找到該位置,然後讀取值。如果資料檔案的那部分已經在檔案系統快取中,讀取根本不需要任何磁碟 I/O。 -這種方法要快得多,但仍然存在幾個問題: +這種方法速度更快,但仍然存在幾個問題: -* 你永遠不會釋放被覆蓋的舊日誌條目佔用的磁碟空間;如果你持續寫入資料庫,可能會耗盡磁碟空間。 -* 雜湊對映不是持久化的,所以當你重啟資料庫時必須重建它——例如,透過掃描整個日誌檔案來找到每個鍵的最新位元組偏移量。如果你有大量資料,這會使重啟變慢。 -* 雜湊表必須適合記憶體。原則上,你可以在磁碟上維護雜湊表,但不幸的是很難使磁碟上的雜湊對映表現良好。它需要大量的隨機訪問 I/O,當它變滿時擴充套件成本很高,雜湊衝突需要繁瑣的邏輯 [^2]。 -* 範圍查詢效率不高。例如,你不能輕鬆掃描 `10000` 和 `19999` 之間的所有鍵——你必須在雜湊對映中單獨查詢每個鍵。 +* 你永遠不會釋放被覆蓋的舊日誌條目佔用的磁碟空間;如果你不斷寫入資料庫,可能會耗盡磁碟空間。 +* 雜湊對映不是持久化的,所以當你重啟資料庫時必須重建它 —— 例如,透過掃描整個日誌檔案來找到每個鍵的最新位元組偏移量。如果你有大量資料,這會使重啟變慢。 +* 雜湊表必須適合記憶體。原則上,你可以在磁碟上維護雜湊表,但不幸的是,很難讓磁碟上的雜湊對映表現良好。它需要大量的隨機訪問 I/O,當它變滿時擴充套件成本高昂,雜湊衝突需要複雜的邏輯 [^2]。 +* 範圍查詢效率不高。例如,你不能輕鬆掃描 `10000` 和 `19999` 之間的所有鍵 —— 你必須在雜湊對映中單獨查詢每個鍵。 #### SSTable 檔案格式 {#the-sstable-file-format} -實際上,雜湊表很少用於資料庫索引,相反,保持資料按 *鍵排序* 的結構更為常見 [^3]。這種結構的一個例子是 *排序字串表*,簡稱 *SSTable*,如 [圖 4-2](/tw/ch4#fig_storage_sstable_index) 所示。這種檔案格式也儲存鍵值對,但它確保它們按鍵排序,每個鍵在檔案中只出現一次。 +實際上,雜湊表很少用於資料庫索引,相反,保持資料 *按鍵排序* 的結構更為常見 [^3]。這種結構的一個例子是 *排序字串表*(*Sorted String Table*),簡稱 *SSTable*,如 [圖 4-2](#fig_storage_sstable_index) 所示。這種檔案格式也儲存鍵值對,但它確保它們按鍵排序,每個鍵在檔案中只出現一次。 {{< figure src="/fig/ddia_0402.png" id="fig_storage_sstable_index" caption="圖 4-2. 帶有稀疏索引的 SSTable,允許查詢跳轉到正確的塊。" class="w-full my-4" >}} -現在你不需要在記憶體中保留所有鍵:你可以將 SSTable 中的鍵值對分組到幾千位元組的 *塊* 中,然後在索引中儲存每個塊的第一個鍵。這種只儲存部分鍵的索引稱為 *稀疏* 索引。該索引儲存在 SSTable 的單獨部分中,例如使用不可變 B 樹、trie 或其他允許查詢快速查詢特定鍵的資料結構 [^4]。 +現在你不需要在記憶體中保留所有鍵:你可以將 SSTable 中的鍵值對分組為幾千位元組的 *塊*,然後在索引中儲存每個塊的第一個鍵。這種只儲存部分鍵的索引稱為 *稀疏* 索引。這個索引儲存在 SSTable 的單獨部分,例如使用不可變 B 樹、字典樹或其他允許查詢快速查詢特定鍵的資料結構 [^4]。 -例如,在 [圖 4-2](/tw/ch4#fig_storage_sstable_index) 中,一個塊的第一個鍵是 `handbag`,下一個塊的第一個鍵是 `handsome`。現在假設你要查詢鍵 `handiwork`,它不出現在稀疏索引中。由於排序,你知道 `handiwork` 必須出現在 `handbag` 和 `handsome` 之間。這意味著你可以定址到 `handbag` 的偏移量,並從那裡掃描檔案,直到找到 `handiwork`(或沒有,如果鍵不在檔案中)。幾千位元組的塊可以非常快速地掃描。 +例如,在 [圖 4-2](#fig_storage_sstable_index) 中,一個塊的第一個鍵是 `handbag`,下一個塊的第一個鍵是 `handsome`。現在假設你要查詢鍵 `handiwork`,它沒有出現在稀疏索引中。由於排序,你知道 `handiwork` 必須出現在 `handbag` 和 `handsome` 之間。這意味著你可以尋找到 `handbag` 的偏移量,然後從那裡掃描檔案,直到找到 `handiwork`(或沒有,如果該鍵不在檔案中)。幾千位元組的塊可以非常快速地掃描。 -此外,每個記錄塊可以被壓縮(如 [圖 4-2](/tw/ch4#fig_storage_sstable_index) 中的陰影區域所示)。除了節省磁碟空間外,壓縮還減少了 I/O 頻寬的使用,代價是使用更多的 CPU 時間。 +此外,每個記錄塊都可以壓縮(在 [圖 4-2](#fig_storage_sstable_index) 中用陰影區域表示)。除了節省磁碟空間外,壓縮還減少了 I/O 頻寬使用,代價是使用更多一點的 CPU 時間。 #### 構建和合並 SSTable {#constructing-and-merging-sstables} -SSTable 檔案格式比僅追加日誌更適合讀取,但它使寫入更加困難。我們不能簡單地追加到末尾,因為那樣檔案將不再排序(除非鍵恰好按升序寫入)。如果每次在中間某處插入鍵時都必須重寫整個 SSTable,寫入將變得太昂貴。 +SSTable 檔案格式在讀取方面比僅追加日誌更好,但它使寫入更加困難。我們不能簡單地追加到末尾,因為那樣檔案就不再排序了(除非鍵恰好按升序寫入)。如果我們每次在中間某處插入鍵時都必須重寫整個 SSTable,寫入將變得太昂貴。 我們可以用 *日誌結構* 方法解決這個問題,這是僅追加日誌和排序檔案之間的混合: -1. 當寫入到來時,將其新增到記憶體中的有序對映資料結構,如紅黑樹、跳錶 [^5] 或 trie [^6]。使用這些資料結構,你可以按任何順序插入鍵,高效地查詢它們,並按排序順序讀回它們。這個記憶體中的資料結構稱為 *記憶體表*。 -2. 當記憶體表變得大於某個閾值——通常是幾兆位元組——將其按排序順序作為 SSTable 檔案寫入磁碟。我們將這個新的 SSTable 檔案稱為資料庫的最新 *段*,它與舊段一起作為單獨的檔案儲存。每個段都有其內容的單獨索引。當新段被寫入磁碟時,資料庫可以繼續寫入新的記憶體表例項,當 SSTable 寫入完成時,舊記憶體表的記憶體被釋放。 -3. 為了讀取某個鍵的值,首先嘗試在記憶體表和最新的磁碟段中查詢鍵。如果不在那裡,檢視下一個較舊的段,等等,直到找到鍵或到達最舊的段。如果鍵不出現在任何段中,它就不存在於資料庫中。 -4. 不時地,在後臺執行合併和壓縮過程來合併段檔案並丟棄被覆蓋或刪除的值。 +1. 當寫入操作到來時,將其新增到記憶體中的有序對映資料結構中,例如紅黑樹、跳錶 [^5] 或字典樹 [^6]。使用這些資料結構,你可以按任意順序插入鍵,高效地查詢它們,並按排序順序讀回它們。這個記憶體資料結構稱為 *記憶體表*(*memtable*)。 +2. 當記憶體表變得大於某個閾值(通常是幾兆位元組)時,將其按排序順序作為 SSTable 檔案寫入磁碟。我們將這個新的 SSTable 檔案稱為資料庫的最新 *段*,它與舊段一起作為單獨的檔案儲存。每個段都有自己內容的單獨索引。當新段被寫入磁碟時,資料庫可以繼續寫入新的記憶體表例項,當 SSTable 寫入完成時,舊記憶體表的記憶體被釋放。 +3. 為了讀取某個鍵的值,首先嘗試在記憶體表和最新的磁碟段中找到該鍵。如果沒有找到,就在下一個較舊的段中查詢,依此類推,直到找到鍵或到達最舊的段。如果鍵沒有出現在任何段中,則它不存在於資料庫中。 +4. 不時地在後臺執行合併和壓實過程,以合併段檔案並丟棄被覆蓋或刪除的值。 -合併段的工作類似於 *歸併排序* 演算法 [^5]。該過程如 [圖 4-3](/tw/ch4#fig_storage_sstable_merging) 所示:並排開始讀取輸入檔案,檢視每個檔案中的第一個鍵,將最低鍵(根據排序順序)複製到輸出檔案,然後重複。如果同一個鍵出現在多個輸入檔案中,只保留較新的值。這產生了一個新的合併段檔案,也按鍵排序,每個鍵一個值,並且使用最少的記憶體,因為我們可以一次迭代一個鍵的 SSTable。 +合併段的工作方式類似於 *歸併排序* 演算法 [^5]。該過程如 [圖 4-3](#fig_storage_sstable_merging) 所示:並排開始讀取輸入檔案,檢視每個檔案中的第一個鍵,將最低的鍵(根據排序順序)複製到輸出檔案,然後重複。如果同一個鍵出現在多個輸入檔案中,只保留較新的值。這會產生一個新的合併段檔案,也按鍵排序,每個鍵只有一個值,並且它使用最少的記憶體,因為我們可以一次遍歷一個鍵的 SSTable。 -{{< figure src="/fig/ddia_0403.png" id="fig_storage_sstable_merging" caption="圖 4-3. 合併幾個 SSTable 段,只保留每個鍵的最新值。" class="w-full my-4" >}} +{{< figure src="/fig/ddia_0403.png" id="fig_storage_sstable_merging" caption="圖 4-3. 合併多個 SSTable 段,僅保留每個鍵的最新值。" class="w-full my-4" >}} -為了確保記憶體表中的資料在資料庫崩潰時不會丟失,儲存引擎在磁碟上保留一個單獨的日誌,每次寫入都立即追加到該日誌。此日誌不按鍵排序,但這並不重要,因為它的唯一目的是在崩潰後恢復記憶體表。每次記憶體表被寫出到 SSTable 後,日誌的相應部分可以被丟棄。 +為了確保資料庫崩潰時記憶體表中的資料不會丟失,儲存引擎在磁碟上保留一個單獨的日誌,每次寫入都會立即追加到該日誌中。此日誌不按鍵排序,但這無關緊要,因為它的唯一目的是在崩潰後恢復記憶體表。每次記憶體表被寫出到 SSTable 後,日誌的相應部分就可以丟棄。 -如果你想刪除一個鍵及其關聯的值,你必須向資料檔案追加一個稱為 *墓碑* 的特殊刪除記錄。當日誌段合併時,墓碑告訴合併過程丟棄已刪除鍵的任何先前值。一旦墓碑被合併到最舊的段中,它就可以被丟棄。 +如果你想刪除一個鍵及其關聯的值,你必須向資料檔案追加一個稱為 *墓碑*(*tombstone*)的特殊刪除記錄。當日誌段合併時,墓碑告訴合併過程丟棄已刪除鍵的任何先前值。一旦墓碑合併到最舊的段中,它就可以被丟棄。 -這裡描述的演算法基本上就是 RocksDB [^7]、Cassandra、Scylla 和 HBase [^8] 中使用的演算法,所有這些都受到 Google 的 Bigtable 論文 [^9] 的啟發(該論文引入了術語 *SSTable* 和 *memtable*)。 +這裡描述的演算法本質上就是 RocksDB [^7]、Cassandra、Scylla 和 HBase [^8] 中使用的演算法,它們都受到 Google 的 Bigtable 論文 [^9] 的啟發(該論文引入了 *SSTable* 和 *memtable* 這兩個術語)。 -該演算法最初於 1996 年以 *日誌結構合併樹* 或 *LSM 樹* 的名稱發表 [^10],建立在早期日誌結構檔案系統工作的基礎上 [^11]。因此,基於合併和壓縮排序檔案原理的儲存引擎通常稱為 *LSM 儲存引擎*。 +該演算法最初於 1996 年以 *日誌結構合併樹*(*Log-Structured Merge-Tree*)或 *LSM 樹*(*LSM-Tree*)[^10] 的名稱釋出,建立在早期日誌結構檔案系統工作的基礎上 [^11]。因此,基於合併和壓實排序檔案原理的儲存引擎通常被稱為 *LSM 儲存引擎*。 -在 LSM 儲存引擎中,段檔案是一次寫入的(透過寫出記憶體表或透過合併一些現有段),此後它是不可變的。段的合併和壓縮可以在後臺執行緒中完成,當它進行時,我們仍然可以使用舊段檔案繼續提供讀取服務。當合並過程完成時,我們將讀取請求切換到使用新的合併段而不是舊段,然後可以刪除舊段檔案。 +在 LSM 儲存引擎中,段檔案是一次性寫入的(透過寫出記憶體表或合併一些現有段),此後它是不可變的。段的合併和壓實可以在後臺執行緒中完成,當它進行時,我們仍然可以使用舊的段檔案繼續提供讀取服務。當合並過程完成時,我們將讀取請求切換到使用新的合併段而不是舊段,然後可以刪除舊的段檔案。 段檔案不一定必須儲存在本地磁碟上:它們也非常適合寫入物件儲存。例如,SlateDB 和 Delta Lake [^12] 採用了這種方法。 -擁有不可變段檔案也簡化了崩潰恢復:如果在寫出記憶體表或合併段時發生崩潰,資料庫可以刪除未完成的 SSTable 並重新開始。持久化寫入到記憶體表的日誌如果在寫入記錄的中途發生崩潰,或者磁碟已滿,可能包含不完整的記錄;這些通常透過在日誌中包含校驗和來檢測,並丟棄損壞或不完整的日誌條目。我們將在 [第 8 章](/tw/ch8#ch_transactions) 中更多地討論永續性和崩潰恢復。 +具有不可變段檔案也簡化了崩潰恢復:如果在寫出記憶體表或合併段時發生崩潰,資料庫可以刪除未完成的 SSTable 並重新開始。將寫入持久化到記憶體表的日誌如果在寫入記錄的過程中發生崩潰,或者磁碟已滿,可能包含不完整的記錄;這些通常透過在日誌中包含校驗和來檢測,並丟棄損壞或不完整的日誌條目。我們將在 [第 8 章](/tw/ch8#ch_transactions) 中更多地討論永續性和崩潰恢復。 #### 布隆過濾器 {#bloom-filters} -使用 LSM 儲存,讀取很久以前最後更新或不存在的鍵可能會很慢,因為儲存引擎需要檢查幾個段檔案。為了加速此類讀取,LSM 儲存引擎通常在每個段中包含一個 *布隆過濾器* [^13],它提供了一種快速但近似的方法來檢查特定鍵是否出現在特定 SSTable 中。 +使用 LSM 儲存,讀取很久以前更新的鍵或不存在的鍵可能會很慢,因為儲存引擎需要檢查多個段檔案。為了加快此類讀取,LSM 儲存引擎通常在每個段中包含一個 *布隆過濾器*(*Bloom filter*)[^13],它提供了一種快速但近似的方法來檢查特定鍵是否出現在特定 SSTable 中。 -[圖 4-4](/tw/ch4#fig_storage_bloom) 顯示了一個包含兩個鍵和 16 位的布隆過濾器示例(實際上,它會包含更多鍵和更多位)。對於 SSTable 中的每個鍵,我們計算一個雜湊函式,生成一組數字,然後將其解釋為位陣列的索引 [^14]。我們將對應於這些索引的位設定為 1,其餘保留為 0。例如,鍵 `handbag` 雜湊到數字(2、9、4),所以我們將第 2、9 和 4 位設定為 1。然後將點陣圖作為 SSTable 的一部分儲存,與鍵的稀疏索引一起。這需要一些額外的空間,但與 SSTable 的其餘部分相比,布隆過濾器通常很小。 +[圖 4-4](#fig_storage_bloom) 顯示了一個包含兩個鍵和 16 位的布隆過濾器示例(實際上,它會包含更多的鍵和更多的位)。對於 SSTable 中的每個鍵,我們計算一個雜湊函式,產生一組數字,然後將其解釋為位陣列的索引 [^14]。我們將對應於這些索引的位設定為 1,其餘保持為 0。例如,鍵 `handbag` 雜湊為數字 (2, 9, 4),所以我們將第 2、9 和 4 位設定為 1。然後將點陣圖與鍵的稀疏索引一起儲存為 SSTable 的一部分。這需要一點額外的空間,但與 SSTable 的其餘部分相比,布隆過濾器通常很小。 -{{< figure src="/fig/ddia_0404.png" id="fig_storage_bloom" caption="圖 4-4. 布隆過濾器提供了一種快速的機率檢查,以確定特定鍵是否存在於特定 SSTable 中。" class="w-full my-4" >}} +{{< figure src="/fig/ddia_0404.png" id="fig_storage_bloom" caption="圖 4-4. 布隆過濾器提供了一種快速的機率檢查,用於判斷特定鍵是否存在於特定 SSTable 中。" class="w-full my-4" >}} -當我們想知道一個鍵是否出現在 SSTable 中時,我們像以前一樣計算該鍵的相同雜湊,並檢查這些索引處的位。例如,在 [圖 4-4](/tw/ch4#fig_storage_bloom) 中,我們正在查詢鍵 `handheld`,它雜湊到(6、11、2)。其中一個位是 1(即第 2 位),而另外兩個是 0。這些檢查可以使用所有 CPU 支援的位運算極其快速地進行。 +當我們想知道一個鍵是否出現在 SSTable 中時,我們像以前一樣計算該鍵的相同雜湊,並檢查這些索引處的位。例如,在 [圖 4-4](#fig_storage_bloom) 中,我們查詢鍵 `handheld`,它雜湊為 (6, 11, 2)。其中一個位是 1(即第 2 位),而另外兩個是 0。這些檢查可以使用所有 CPU 都支援的位運算非常快速地進行。 -如果至少有一個位是 0,我們知道鍵肯定不出現在 SSTable 中。如果查詢中的位都是 1,鍵很可能在 SSTable 中,但也可能巧合地所有這些位都被其他鍵設定為 1。這種看起來好像鍵存在,即使它不存在的情況,稱為 *誤報*。 +如果至少有一個位是 0,我們知道該鍵肯定不在 SSTable 中。如果查詢中的位都是 1,那麼該鍵很可能在 SSTable 中,但也有可能是巧合,所有這些位都被其他鍵設定為 1。這種看起來鍵存在但實際上不存在的情況稱為 *假陽性*(*false positive*)。 -誤報的機率取決於鍵的數量、每個鍵設定的位數和布隆過濾器中的總位數。你可以使用線上計算器工具來計算適合你應用程式的正確引數 [^15]。作為經驗法則,你需要為 SSTable 中的每個鍵分配 10 位布隆過濾器空間,以獲得 1% 的誤報機率,每為每個鍵分配額外的 5 位,機率降低十倍。 +假陽性的機率取決於鍵的數量、每個鍵設定的位數和布隆過濾器中的總位數。你可以使用線上計算器工具為你的應用計算出正確的引數 [^15]。作為經驗法則,你需要為 SSTable 中的每個鍵分配 10 位布隆過濾器空間以獲得 1% 的假陽性機率,每為每個鍵分配額外的 5 位,機率就會降低十倍。 -在 LSM 儲存引擎的上下文中,誤報沒有問題: +在 LSM 儲存引擎的上下文中,假陽性沒有問題: * 如果布隆過濾器說鍵 *不* 存在,我們可以安全地跳過該 SSTable,因為我們可以確定它不包含該鍵。 -* 如果布隆過濾器說鍵 *存在*,我們必須查閱稀疏索引並解碼鍵值對塊以檢查鍵是否真的在那裡。如果是誤報,我們做了一些不必要的工作,但除此之外沒有造成傷害——我們只是繼續搜尋下一個最舊的段。 +* 如果布隆過濾器說鍵 *存在*,我們必須查詢稀疏索引並解碼鍵值對塊以檢查鍵是否真的在那裡。如果是假陽性,我們做了一些不必要的工作,但除此之外沒有害處 —— 我們只是繼續使用下一個最舊的段進行搜尋。 #### 壓實策略 {#sec_storage_lsm_compaction} -一個重要的細節是 LSM 儲存如何選擇何時執行壓縮,以及在壓縮中包含哪些 SSTable。許多基於 LSM 的儲存系統允許你配置使用哪種壓縮策略,一些常見的選擇是 [^16] [^17]: +一個重要的細節是 LSM 儲存如何選擇何時執行壓實,以及在壓實中包括哪些 SSTable。許多基於 LSM 的儲存系統允許你配置使用哪種壓實策略,一些常見的選擇是 [^16] [^17]: -大小分層壓實 -: 較新和較小的 SSTable 依次合併成較舊和較大的 SSTable。包含較舊資料的 SSTable 可能變得非常大,合併它們需要大量臨時磁碟空間。這種策略的優點是它可以處理非常高的寫入吞吐量。 +分層壓實(Size-tiered compaction) +: 較新和較小的 SSTable 依次合併到較舊和較大的 SSTable 中。包含較舊資料的 SSTable 可能變得非常大,合併它們需要大量的臨時磁碟空間。這種策略的優點是它可以處理非常高的寫入吞吐量。 -分層壓實 -: 鍵範圍被分成更小的 SSTable,較舊的資料被移動到單獨的"級別"中,這允許壓縮更漸進地進行,並使用比大小分層策略更少的磁碟空間。這種策略對於讀取比大小分層壓縮更有效,因為儲存引擎需要讀取更少的 SSTable 來檢查它們是否包含鍵。 +分級壓實(Leveled compaction) +: 鍵範圍被分成較小的 SSTable,較舊的資料被移動到單獨的"級別"中,這允許壓實更增量地進行,並且比分層策略使用更少的磁碟空間。這種策略對於讀取比分層壓實更有效,因為儲存引擎需要讀取更少的 SSTable 來檢查它們是否包含該鍵。 -作為經驗法則,如果你主要有寫入和很少讀取,大小分層壓縮效能更好,而如果你的工作負載以讀取為主,分層壓縮效能更好。如果你頻繁寫入少量鍵,很少寫入大量鍵,那麼分層壓縮也可能有優勢 [^18]。 +作為經驗法則,如果你主要有寫入而讀取很少,分層壓實表現更好,而如果你的工作負載以讀取為主,分級壓實表現更好。如果你頻繁寫入少量鍵,而很少寫入大量鍵,那麼分級壓實也可能有優勢 [^18]。 -儘管有許多細微之處,LSM 樹的基本思想——保持在後臺合併的 SSTable 級聯——簡單而有效。我們在 ["比較 B 樹和 LSM 樹"](/tw/ch4#sec_storage_btree_lsm_comparison) 中更詳細地討論它們的效能特徵。 +儘管有許多細微之處,但 LSM 樹的基本思想 —— 保持在後臺合併的 SSTable 級聯 —— 簡單而有效。我們將在 ["比較 B 樹與 LSM 樹"](#sec_storage_btree_lsm_comparison) 中更詳細地討論它們的效能特徵。 -------- > [!TIP] 嵌入式儲存引擎 -> -> 許多資料庫作為接受網路查詢的服務執行,但也有 *嵌入式* 資料庫不公開網路 API。相反,它們是在與你的應用程式程式碼相同的程序中執行的庫,通常讀取和寫入本地磁碟上的檔案,你透過正常的函式呼叫與它們互動。嵌入式儲存引擎的例子包括 RocksDB、SQLite、LMDB、DuckDB 和 KùzuDB [^19]。 -> -> 嵌入式資料庫在移動應用程式中非常常用,用於儲存本地使用者的資料。在後端,如果資料足夠小以適合單臺機器,並且沒有很多併發事務,它們可能是合適的選擇。例如,在多租戶系統中,如果每個租戶足夠小且彼此完全分離(即,你不需要執行組合來自多個租戶的資料的查詢),你可能可以為每個租戶使用單獨的嵌入式資料庫例項 [^20]。 -> -> 我們在本章中討論的儲存和檢索方法既用於嵌入式資料庫,也用於客戶端-伺服器資料庫。在 [第 6 章](/tw/ch6#ch_replication) 和 [第 7 章](/tw/ch7#ch_sharding) 中,我們將討論跨多臺機器擴充套件資料庫的技術。 + +許多資料庫作為接受網路查詢的服務執行,但也有 *嵌入式* 資料庫不公開網路 API。相反,它們是在與應用程式程式碼相同的程序中執行的庫,通常讀取和寫入本地磁碟上的檔案,你透過正常的函式呼叫與它們互動。嵌入式儲存引擎的例子包括 RocksDB、SQLite、LMDB、DuckDB 和 KùzuDB [^19]。 + +嵌入式資料庫在移動應用中非常常用,用於儲存本地使用者的資料。在後端,如果資料足夠小以適合單臺機器,並且沒有太多併發事務,它們可能是一個合適的選擇。例如,在多租戶系統中,如果每個租戶足夠小且完全與其他租戶分離(即,你不需要執行合併多個租戶資料的查詢),你可能可以為每個租戶使用單獨的嵌入式資料庫例項 [^20]。 + +我們在本章討論的儲存和檢索方法既用於嵌入式資料庫,也用於客戶端-伺服器資料庫。在 [第 6 章](/tw/ch6#ch_replication) 和 [第 7 章](/tw/ch7#ch_sharding) 中,我們將討論跨多臺機器擴充套件資料庫的技術。 -------- ### B 樹 {#sec_storage_b_trees} -日誌結構方法很流行,但它不是鍵值儲存的唯一形式。按鍵讀取和寫入資料庫記錄的最廣泛使用的結構是 *B 樹*。 +日誌結構方法很流行,但它不是鍵值儲存的唯一形式。按鍵讀取和寫入資料庫記錄最廣泛使用的結構是 *B 樹*。 -B 樹於 1970 年引入 [^21],不到 10 年後被稱為"無處不在" [^22],它們經受住了時間的考驗。它們仍然是幾乎所有關係資料庫中的標準索引實現,許多非關係資料庫也使用它們。 +B 樹於 1970 年引入 [^21],不到 10 年後就被稱為"無處不在"[^22],它們經受住了時間的考驗。它們仍然是幾乎所有關係資料庫中的標準索引實現,許多非關係資料庫也使用它們。 -像 SSTable 一樣,B 樹按鍵排序鍵值對,這允許高效的鍵值查詢和範圍查詢。但相似之處到此為止:B 樹具有非常不同的設計理念。 +像 SSTable 一樣,B 樹按鍵保持鍵值對排序,這允許高效的鍵值查詢和範圍查詢。但相似之處到此為止:B 樹有著非常不同的設計理念。 -我們之前看到的日誌結構索引將資料庫分解為可變大小的 *段*,通常為幾兆位元組或更大,一次寫入然後不可變。相比之下,B 樹將資料庫分解為固定大小的 *塊* 或 *頁*,並可能就地覆蓋頁。頁傳統上大小為 4 KiB,但 PostgreSQL 現在預設使用 8 KiB,MySQL 預設使用 16 KiB。 +我們之前看到的日誌結構索引將資料庫分解為可變大小的 *段*,通常為幾兆位元組或更大,寫入一次後就不可變。相比之下,B 樹將資料庫分解為固定大小的 *塊* 或 *頁*,並可能就地覆蓋頁。頁傳統上大小為 4 KiB,但 PostgreSQL 現在預設使用 8 KiB,MySQL 預設使用 16 KiB。 -每個頁可以使用頁號標識,這允許一個頁引用另一個頁——類似於指標,但在磁碟上而不是在記憶體中。如果所有頁都儲存在同一個檔案中,將頁號乘以頁大小會給我們頁所在檔案中的位元組偏移量。我們可以使用這些頁引用來構建頁樹,如 [圖 4-5](/tw/ch4#fig_storage_b_tree) 所示。 +每個頁都可以使用頁號來標識,這允許一個頁引用另一個頁 —— 類似於指標,但在磁碟上而不是在記憶體中。如果所有頁都儲存在同一個檔案中,將頁號乘以頁大小就給我們檔案中頁所在位置的位元組偏移量。我們可以使用這些頁引用來構建頁樹,如 [圖 4-5](#fig_storage_b_tree) 所示。 -{{< figure src="/fig/ddia_0405.png" id="fig_storage_b_tree" caption="圖 4-5. 使用 B 樹索引查詢鍵 251。從根頁開始,我們首先跟隨引用到鍵 200-300 的頁,然後是鍵 250-270 的頁。" class="w-full my-4" >}} +{{< figure src="/fig/ddia_0405.png" id="fig_storage_b_tree" caption="圖 4-5. 使用 B 樹索引查詢鍵 251。從根頁開始,我們首先跟隨引用到鍵 200–300 的頁,然後是鍵 250–270 的頁。" class="w-full my-4" >}} -一個頁被指定為 B 樹的 *根*;每當你想在索引中查詢一個鍵時,你從這裡開始。該頁包含幾個鍵和對子頁的引用。每個子負責一個連續的鍵範圍,引用之間的鍵指示這些範圍之間的邊界在哪裡。(這種結構有時稱為 B+ 樹,但我們不需要將其與其他 B 樹變體區分開來。) +一個頁被指定為 B 樹的 *根*;每當你想在索引中查詢一個鍵時,你就從這裡開始。該頁包含幾個鍵和對子頁的引用。每個子負責一個連續的鍵範圍,引用之間的鍵指示這些範圍之間的邊界在哪裡。(這種結構有時稱為 B+ 樹,但我們不需要將其與其他 B 樹變體區分開來。) -在 [圖 4-5](/tw/ch4#fig_storage_b_tree) 的示例中,我們正在查詢鍵 251,所以我們知道需要跟隨 200 和 300 之間的頁引用。這將我們帶到一個外觀相似的頁,進一步將 200-300 範圍分解為子範圍。最終我們到達包含單個鍵的頁(*葉頁*),它要麼內聯包含每個鍵的值,要麼包含對可以找到值的頁的引用。 +在 [圖 4-5](#fig_storage_b_tree) 的例子中,我們正在查詢鍵 251,所以我們知道我們需要跟隨邊界 200 和 300 之間的頁引用。這將我們帶到一個看起來相似的頁,該頁進一步將 200–300 範圍分解為子範圍。最終我們到達包含單個鍵的頁(*葉頁*),該頁要麼內聯包含每個鍵的值,要麼包含對可以找到值的頁的引用。 -B 樹的一頁中對子頁的引用數稱為 *分支因子*。例如,在 [圖 4-5](/tw/ch4#fig_storage_b_tree) 中,分支因子為六。實際上,分支因子取決於儲存頁引用和範圍邊界所需的空間量,但通常是幾百。 +B 樹的一個頁中對子頁的引用數稱為 *分支因子*。例如,在 [圖 4-5](#fig_storage_b_tree) 中,分支因子為六。實際上,分支因子取決於儲存頁引用和範圍邊界所需的空間量,但通常為幾百。 -如果你想更新 B 樹中現有鍵的值,你搜索包含該鍵的葉頁,並用包含新值的版本覆蓋磁碟上的該頁。如果你想新增新鍵,你需要找到其範圍包含新鍵的頁並將其新增到該頁。如果頁中沒有足夠的空閒空間來容納新鍵,則頁分成兩個半滿頁,並更新父頁以考慮鍵範圍的新細分。 +如果你想更新 B 樹中現有鍵的值,你搜索包含該鍵的葉頁,並用包含新值的版本覆蓋磁碟上的該頁。如果你想新增一個新鍵,你需要找到其範圍包含新鍵的頁並將其新增到該頁。如果頁中沒有足夠的空閒空間來容納新鍵,則頁被分成兩個半滿的頁,並更新父頁以說明鍵範圍的新細分。 {{< figure src="/fig/ddia_0406.png" id="fig_storage_b_tree_split" caption="圖 4-6. 透過在邊界鍵 337 上分割頁來增長 B 樹。父頁被更新以引用兩個子頁。" class="w-full my-4" >}} -在 [圖 4-6](/tw/ch4#fig_storage_b_tree_split) 的示例中,我們想插入鍵 334,但範圍 333-345 的頁已滿。因此,我們將其分成範圍 333-337(包括新鍵)的頁和 337-344 的頁。我們還必須更新父頁以引用兩個子頁,它們之間的邊界值為 337。如果父頁沒有足夠的空間用於新引用,它也可能需要分割,分割可以一直持續到樹的根。當根分割時,我們在其上方建立一個新根。刪除鍵(可能需要合併節點)更複雜 [^5]。 +在 [圖 4-6](#fig_storage_b_tree_split) 的例子中,我們想插入鍵 334,但範圍 333–345 的頁已經滿了。因此,我們將其分成範圍 333–337(包括新鍵)的頁和 337–344 的頁。我們還必須更新父頁以引用兩個子頁,它們之間的邊界值為 337。如果父頁沒有足夠的空間容納新引用,它也可能需要被分割,分割可以一直持續到樹的根。當根被分割時,我們在它上面建立一個新根。刪除鍵(可能需要合併節點)更複雜 [^5]。 -該演算法確保樹保持 *平衡*:具有 *n* 個鍵的 B 樹始終具有 *O*(log *n*) 的深度。大多數資料庫可以適合三或四層深的 B 樹,因此你不需要跟隨很多頁引用來找到你要查詢的頁。(具有 500 的分支因子的 4 KiB 頁的四層樹可以儲存多達 250 TB。) +這個演算法確保樹保持 *平衡*:具有 *n* 個鍵的 B 樹始終具有 *O*(log *n*) 的深度。大多數資料庫可以適合三或四層深的 B 樹,所以你不需要跟隨許多頁引用來找到你要查詢的頁。(具有 500 分支因子的 4 KiB 頁的四層樹可以儲存多達 250 TB。) #### 使 B 樹可靠 {#sec_storage_btree_wal} -B 樹的基本底層寫入操作是用新資料覆蓋磁碟上的頁。假設覆蓋不會改變頁的位置;即,當頁被覆蓋時,對該頁的所有引用保持不變。這與日誌結構索引(如 LSM 樹)形成鮮明對比,後者只追加到檔案(並最終刪除過時的檔案)但從不就地修改檔案。 +B 樹的基本底層寫操作是用新資料覆蓋磁碟上的頁。假設覆蓋不會改變頁的位置;即,當頁被覆蓋時,對該頁的所有引用保持不變。這與日誌結構索引(如 LSM 樹)形成鮮明對比,後者只追加到檔案(並最終刪除過時的檔案),但從不就地修改檔案。 -一次覆蓋幾個頁,如在頁分割中,是一個危險的操作:如果資料庫在只寫入了一些頁後崩潰,你最終會得到一個損壞的樹(例如,可能有一個 *孤立* 頁不是任何父頁的子頁)。如果硬體不能原子地寫入整個頁,你也可能最終得到部分寫入的頁(這稱為 *撕裂頁* [^23])。 +一次覆蓋多個頁,如在頁分割中,是一個危險的操作:如果資料庫在只寫入了部分頁後崩潰,你最終會得到一個損壞的樹(例如,可能有一個 *孤立* 頁,它不是任何父頁的子頁)。如果硬體不能原子地寫入整個頁,你也可能最終得到部分寫入的頁(這稱為 *撕裂頁*(*torn page*)[^23])。 -為了使資料庫對崩潰具有彈性,B 樹實現通常包括磁碟上的額外資料結構:*預寫日誌*(WAL)。這是一個僅追加檔案,在將每個 B 樹修改應用於樹本身的頁之前,必須將其寫入該檔案。當資料庫在崩潰後恢復時,此日誌用於將 B 樹恢復到一致狀態 [^2] [^24]。在檔案系統中,等效機制稱為 *日誌記錄*。 +為了使資料庫對崩潰具有彈性,B 樹實現通常包括磁碟上的額外資料結構:*預寫日誌*(*write-ahead log*,WAL)。這是一個僅追加檔案,每個 B 樹修改必須在應用於樹本身的頁之前寫入其中。當資料庫在崩潰後恢復時,此日誌用於將 B 樹恢復到一致狀態 [^2] [^24]。在檔案系統中,等效機制稱為 *日誌記錄*(*journaling*)。 -為了提高效能,B 樹實現通常不會立即將每個修改的頁寫入磁碟,而是首先在記憶體中緩衝 B 樹頁一段時間。預寫日誌然後還確保在崩潰的情況下資料不會丟失:只要資料已寫入 WAL,並使用 `fsync()` 系統呼叫重新整理到磁碟,資料將是持久的,因為資料庫將能夠在崩潰後恢復它 [^25]。 +為了提高效能,B 樹實現通常不會立即將每個修改的頁寫入磁碟,而是首先將 B 樹頁緩衝在記憶體中一段時間。預寫日誌還確保在崩潰的情況下資料不會丟失:只要資料已寫入 WAL,並使用 `fsync()` 系統呼叫重新整理到磁碟,資料就是持久的,因為資料庫將能夠在崩潰後恢復它 [^25]。 #### B 樹變體 {#b-tree-variants} -由於 B 樹已經存在了很長時間,多年來已經開發了許多變體。僅舉幾例: +由於 B 樹已經存在了很長時間,多年來已經開發了許多變體。僅舉幾個例子: -* 一些資料庫(如 LMDB)使用寫時複製方案 [^26],而不是覆蓋頁並維護 WAL 進行崩潰恢復。修改的頁被寫入不同的位置,並建立樹中父頁的新版本,指向新位置。這種方法對於併發控制也很有用,正如我們將在 ["快照隔離和可重複讀"](/tw/ch8#sec_transactions_snapshot_isolation) 中看到的。 -* 我們可以透過不儲存整個鍵,而是縮寫它來節省頁中的空間。特別是在樹內部的頁上,鍵只需要提供足夠的資訊來充當鍵範圍之間的邊界。將更多鍵打包到頁中允許樹具有更高的分支因子,從而減少層級。 -* 為了加速按排序順序掃描鍵範圍,一些 B 樹實現嘗試佈局樹,使葉頁在磁碟上按順序出現,減少磁碟尋道次數。然而,隨著樹的增長,很難維持該順序。 -* 已向樹添加了額外的指標。例如,每個葉頁可能具有對其左右兄弟頁的引用,這允許按順序掃描鍵而無需跳回父頁。 +* 一些資料庫(如 LMDB)使用寫時複製方案 [^26],而不是覆蓋頁並維護 WAL 以進行崩潰恢復。修改的頁被寫入不同的位置,並建立樹中父頁的新版本,指向新位置。這種方法對於併發控制也很有用,我們將在 ["快照隔離和可重複讀"](/tw/ch8#sec_transactions_snapshot_isolation) 中看到。 +* 我們可以透過不儲存整個鍵而是縮寫它來節省頁中的空間。特別是在樹內部的頁中,鍵只需要提供足夠的資訊來充當鍵範圍之間的邊界。在頁中打包更多鍵允許樹具有更高的分支因子,從而減少層數。 +* 為了加快按排序順序掃描鍵範圍,一些 B 樹實現嘗試佈局樹,使葉頁按順序出現在磁碟上,減少磁碟尋道次數。然而,隨著樹的增長,很難維持這種順序。 +* 已向樹添加了其他指標。例如,每個葉頁可能有對其左右兄弟頁的引用,這允許按順序掃描鍵而無需跳回父頁。 ### 比較 B 樹與 LSM 樹 {#sec_storage_btree_lsm_comparison} -作為經驗法則,LSM 樹更適合寫入密集型應用程式,而 B 樹對於讀取更快 [^27] [^28]。然而,基準測試通常對工作負載的細節很敏感。你需要使用你的特定工作負載測試系統才能進行有效比較。此外,這不是 LSM 和 B 樹之間的嚴格二選一選擇:儲存引擎有時融合了兩種方法的特徵,例如透過擁有多個 B 樹並以 LSM 風格合併它們。在本節中,我們將簡要討論在衡量儲存引擎效能時值得考慮的幾件事。 +作為經驗法則,LSM 樹更適合寫入密集型應用,而 B 樹對讀取更快 [^27] [^28]。然而,基準測試通常對工作負載的細節很敏感。你需要使用特定的工作負載測試系統,以便進行有效的比較。此外,這不是 LSM 和 B 樹之間的嚴格二選一選擇:儲存引擎有時會混合兩種方法的特徵,例如具有多個 B 樹並以 LSM 風格合併它們。在本節中,我們將簡要討論在衡量儲存引擎效能時值得考慮的幾件事。 #### 讀取效能 {#read-performance} -在 B 樹中,查詢鍵涉及在 B 樹的每個層級讀取一頁。由於層級數通常很小,這意味著從 B 樹讀取通常很快且具有可預測的效能。在 LSM 儲存引擎中,讀取通常必須檢查處於不同壓縮階段的幾個不同 SSTable,但布隆過濾器有助於減少所需的實際磁碟 I/O 運算元。兩種方法都可以表現良好,哪個更快取決於儲存引擎和工作負載的細節。 +在 B 樹中,查詢鍵涉及在 B 樹的每個級別讀取一個頁。由於級別數通常很小,這意味著從 B 樹讀取通常很快並且具有可預測的效能。在 LSM 儲存引擎中,讀取通常必須檢查處於不同壓實階段的幾個不同 SSTable,但布隆過濾器有助於減少所需的實際磁碟 I/O 運算元。兩種方法都可以表現良好,哪個更快取決於儲存引擎的細節和工作負載。 範圍查詢在 B 樹上簡單而快速,因為它們可以使用樹的排序結構。在 LSM 儲存上,範圍查詢也可以利用 SSTable 排序,但它們需要並行掃描所有段並組合結果。布隆過濾器對範圍查詢沒有幫助(因為你需要計算範圍內每個可能鍵的雜湊,這是不切實際的),使得範圍查詢在 LSM 方法中比點查詢更昂貴 [^29]。 -如果記憶體表填滿,高寫入吞吐量可能會在日誌結構儲存引擎中導致延遲峰值。如果資料無法足夠快地寫入磁碟,可能是因為壓縮過程無法跟上傳入寫入,就會發生這種情況。許多儲存引擎,包括 RocksDB,在這種情況下執行 *背壓*:它們暫停所有讀取和寫入,直到記憶體表被寫入磁碟 [^30] [^31]。 +如果記憶體表填滿,高寫入吞吐量可能會導致日誌結構儲存引擎中的延遲峰值。如果資料無法足夠快地寫入磁碟,可能是因為壓實過程無法跟上傳入的寫入,就會發生這種情況。許多儲存引擎,包括 RocksDB,在這種情況下執行 *背壓*:它們暫停所有讀取和寫入,直到記憶體表被寫入磁碟 [^30] [^31]。 -關於讀取吞吐量,現代 SSD(特別是 NVMe)可以並行執行許多獨立的讀取請求。LSM 樹和 B 樹都能夠提供高讀取吞吐量,但儲存引擎需要精心設計以利用這種並行性 [^32]。 +關於讀取吞吐量,現代 SSD(特別是 NVMe)可以並行執行許多獨立的讀請求。LSM 樹和 B 樹都能夠提供高讀取吞吐量,但儲存引擎需要仔細設計以利用這種並行性 [^32]。 #### 順序與隨機寫入 {#sidebar_sequential} -使用 B 樹,如果應用程式寫入分散在整個鍵空間中的鍵,則產生的磁碟操作也是隨機分散的,因為儲存引擎需要覆蓋的頁可能位於磁碟上的任何位置。另一方面,日誌結構儲存引擎一次寫入整個段檔案(要麼寫出記憶體表,要麼壓縮現有段),這比 B 樹中的頁大得多。 +使用 B 樹時,如果應用程式寫入的鍵分散在整個鍵空間中,生成的磁碟操作也會隨機分散,因為儲存引擎需要覆蓋的頁可能位於磁碟的任何位置。另一方面,日誌結構儲存引擎一次寫入整個段檔案(無論是寫出記憶體表還是壓實現有段),這比 B 樹中的頁大得多。 -許多小的、分散的寫入模式(如在 B 樹中發現的)稱為 *隨機寫入*,而較少的大寫入模式(如在 LSM 樹中發現的)稱為 *順序寫入*。磁碟通常具有比隨機寫入吞吐量更高的順序寫入吞吐量,這意味著日誌結構儲存引擎通常可以在相同硬體上處理比 B 樹更高的寫入吞吐量。這種差異在旋轉磁碟硬碟(HDD)上特別大;在今天大多數資料庫使用的固態硬碟(SSD)上,差異較小,但仍然很明顯(參見 ["SSD 上的順序與隨機寫入"](/tw/ch4#sidebar_sequential))。 +許多小的、分散的寫入模式(如 B 樹中的)稱為 *隨機寫入*,而較少的大寫入模式(如 LSM 樹中的)稱為 *順序寫入*。磁碟通常具有比隨機寫入更高的順序寫入吞吐量,這意味著日誌結構儲存引擎通常可以在相同硬體上處理比 B 樹更高的寫入吞吐量。這種差異在旋轉磁碟硬碟(HDD)上特別大;在今天大多數資料庫使用的固態硬碟(SSD)上,差異較小,但仍然明顯(參見 ["SSD 上的順序與隨機寫入"](#sidebar_sequential))。 -------- > [!TIP] SSD 上的順序與隨機寫入 -> -> 在旋轉磁碟硬碟(HDD)上,順序寫入比隨機寫入快得多:隨機寫入必須機械地將磁頭移動到新位置,並等待碟片的正確部分透過磁頭下方,這需要幾毫秒——在計算時間尺度上是永恆的。然而,SSD(固態硬碟)包括 NVMe(非易失性記憶體快速,即連線到 PCI Express 匯流排的快閃記憶體)現在已經在許多用例中取代了 HDD,它們不受此類機械限制。 -> -> 儘管如此,SSD 的順序寫入吞吐量也高於隨機寫入。原因是快閃記憶體可以一次讀取或寫入一頁(通常為 4 KiB),但只能一次擦除一個塊(通常為 512 KiB)。塊中的某些頁可能包含有效資料,而其他頁可能包含不再需要的資料。在擦除塊之前,控制器必須首先將包含有效資料的頁移動到其他塊中;這個過程稱為 *垃圾收集*(GC)[^33]。 -> -> 順序寫入工作負載一次寫入更大的資料塊,因此整個 512 KiB 塊很可能屬於單個檔案;當該檔案稍後再次刪除時,可以擦除整個塊而無需執行任何 GC。另一方面,使用隨機寫入工作負載,塊更可能包含有效和無效資料頁的混合,因此 GC 必須在擦除塊之前執行更多工作 [^34] [^35] [^36]。 -> -> GC 消耗的寫入頻寬隨後不可用於應用程式。此外,GC 執行的額外寫入會導致快閃記憶體磨損;因此,隨機寫入比順序寫入更快地磨損驅動器。 + +在旋轉磁碟硬碟(HDD)上,順序寫入比隨機寫入快得多:隨機寫入必須機械地將磁頭移動到新位置,並等待碟片的正確部分經過磁頭下方,這需要幾毫秒 —— 在計算時間尺度上是永恆的。然而,SSD(固態硬碟)包括 NVMe(非易失性記憶體快速,即連線到 PCI Express 匯流排的快閃記憶體)現在已經在許多用例中超越了 HDD,它們不受這種機械限制。 + +儘管如此,SSD 對順序寫入的吞吐量也高於隨機寫入。原因是快閃記憶體可以一次讀取或寫入一頁(通常為 4 KiB),但只能一次擦除一個塊(通常為 512 KiB)。塊中的某些頁可能包含有效資料,而其他頁可能包含不再需要的資料。在擦除塊之前,控制器必須首先將包含有效資料的頁移動到其他塊中;這個過程稱為 *垃圾回收*(GC)[^33]。 + +順序寫入工作負載一次寫入更大的資料塊,因此整個 512 KiB 塊很可能屬於單個檔案;當該檔案稍後再次被刪除時,整個塊可以被擦除而無需執行任何 GC。另一方面,對於隨機寫入工作負載,塊更可能包含有效和無效資料頁的混合,因此 GC 必須在塊可以擦除之前執行更多工作 [^34] [^35] [^36]。 + +GC 消耗的寫入頻寬就不能用於應用程式。此外,GC 執行的額外寫入會導致快閃記憶體磨損;因此,隨機寫入比順序寫入更快地磨損驅動器。 -------- #### 寫放大 {#write-amplification} -使用任何型別的儲存引擎,來自應用程式的一個寫入請求都會在底層磁碟上變成多個 I/O 操作。使用 LSM 樹,首先將值寫入日誌以獲得永續性,然後在記憶體表寫入磁碟時再次寫入,並且每次鍵值對成為壓縮的一部分時再次寫入。(如果值明顯大於鍵,可以透過將值與鍵分開儲存,並僅對包含鍵和值引用的 SSTable 執行壓縮來減少此開銷 [^37]。) +對於任何型別的儲存引擎,來自應用程式的一次寫請求都會轉換為底層磁碟上的多個 I/O 操作。對於 LSM 樹,一個值首先被寫入日誌以保證永續性,然後在記憶體表寫入磁碟時再次寫入,並且每次鍵值對參與壓即時再次寫入。(如果值明顯大於鍵,可以透過將值與鍵分開儲存,並僅對包含鍵和值引用的 SSTable 執行壓實來減少這種開銷 [^37]。) -B 樹索引必須至少寫入每條資料兩次:一次到預寫日誌,一次到樹頁本身。此外,它們有時需要寫出整個頁,即使該頁中只有幾個位元組發生了變化,以確保 B 樹在崩潰或斷電後可以正確恢復 [^38] [^39]。 +B 樹索引必須至少寫入每條資料兩次:一次寫入預寫日誌,一次寫入樹頁本身。此外,它們有時需要寫出整個頁,即使該頁中只有幾個位元組發生了變化,以確保 B 樹在崩潰或斷電後可以正確恢復 [^38] [^39]。 -如果你將某些工作負載中寫入磁碟的總位元組數除以你僅寫入沒有索引的僅追加日誌時必須寫入的位元組數,你就得到 *寫放大*。(有時寫放大是根據 I/O 操作而不是位元組定義的。)在寫入密集型應用程式中,瓶頸可能是資料庫可以寫入磁碟的速率。在這種情況下,寫放大越高,它可以在可用磁碟頻寬內處理的每秒寫入越少。 +如果你獲取在某個工作負載中寫入磁碟的總位元組數,然後除以如果你只是寫入沒有索引的僅追加日誌需要寫入的位元組數,你就得到了 *寫放大*。(有時寫放大是根據 I/O 操作而不是位元組來定義的。)在寫入密集型應用程式中,瓶頸可能是資料庫可以寫入磁碟的速率。在這種情況下,寫放大越高,它在可用磁碟頻寬內可以處理的每秒寫入次數就越少。 -寫放大是 LSM 樹和 B 樹中的問題。哪個更好取決於各種因素,例如鍵和值的長度,以及你覆蓋現有鍵與插入新鍵的頻率。對於典型工作負載,LSM 樹往往具有較低的寫放大,因為它們不必寫入整個頁,並且可以壓縮 SSTable 的塊 [^40]。這是使 LSM 儲存引擎非常適合寫入密集型工作負載的另一個因素。 +寫放大是 LSM 樹和 B 樹中的問題。哪個更好取決於各種因素,例如鍵和值的長度,以及你覆蓋現有鍵與插入新鍵的頻率。對於典型的工作負載,LSM 樹往往具有較低的寫放大,因為它們不必寫入整個頁,並且可以壓縮 SSTable 的塊 [^40]。這是使 LSM 儲存引擎非常適合寫入密集型工作負載的另一個因素。 -除了影響吞吐量外,寫放大還與 SSD 的磨損有關:具有較低寫放大的儲存引擎將更慢地磨損 SSD。 +除了影響吞吐量,寫放大也與 SSD 的磨損有關:寫放大較低的儲存引擎將更慢地磨損 SSD。 -在測量儲存引擎的寫入吞吐量時,重要的是執行實驗足夠長的時間,以使寫放大的影響變得明顯。寫入空 LSM 樹時,還沒有壓縮在進行,因此所有磁碟頻寬都可用於新寫入。隨著資料庫的增長,新寫入需要與壓縮共享磁碟頻寬。 +在測量儲存引擎的寫入吞吐量時,重要的是要執行足夠長的實驗,以便寫放大的影響變得清晰。當寫入空的 LSM 樹時,還沒有進行壓實,因此所有磁碟頻寬都可用於新寫入。隨著資料庫的增長,新寫入需要與壓實共享磁碟頻寬。 #### 磁碟空間使用 {#disk-space-usage} -B 樹會隨著時間的推移變得 *碎片化*:例如,如果刪除了大量鍵,資料庫檔案可能包含許多不再被 B 樹使用的頁。對 B 樹的後續新增可以使用那些空閒頁,但它們不能輕易地返回給作業系統,因為它們在檔案中間,因此它們仍然佔用檔案系統上的空間。因此,資料庫需要一個後臺程序來移動頁以更好地放置它們,例如 PostgreSQL 中的 vacuum 程序 [^25]。 +B 樹可能會隨著時間的推移變得 *碎片化*:例如,如果刪除了大量鍵,資料庫檔案可能包含許多 B 樹不再使用的頁。對 B 樹的後續新增可以使用這些空閒頁,但它們不能輕易地返回給作業系統,因為它們在檔案的中間,所以它們仍然佔用檔案系統上的空間。因此,資料庫需要一個後臺過程來移動頁以更好地放置它們,例如 PostgreSQL 中的真空過程 [^25]。 -碎片化在 LSM 樹中不是問題,因為壓縮過程無論如何都會定期重寫資料檔案,並且 SSTable 沒有未使用空間的頁。此外,鍵值對塊可以在 SSTable 中更好地壓縮,因此通常在磁碟上產生比 B 樹更小的檔案。被覆蓋的鍵和值繼續消耗空間,直到它們被壓縮刪除,但使用分層壓縮時此開銷相當低 [^40] [^41]。大小分層壓縮(參見 ["壓縮策略"](/tw/ch4#sec_storage_lsm_compaction))使用更多磁碟空間,特別是在壓縮期間暫時使用。 +碎片化在 LSM 樹中不太成問題,因為壓實過程無論如何都會定期重寫資料檔案,而且 SSTable 沒有未使用空間的頁。此外,SSTable 中的鍵值對塊可以更好地壓縮,因此通常比 B 樹在磁碟上產生更小的檔案。被覆蓋的鍵和值繼續消耗空間,直到它們被壓實刪除,但使用分級壓即時,這種開銷相當低 [^40] [^41]。分層壓實(參見 ["壓實策略"](#sec_storage_lsm_compaction))使用更多的磁碟空間,特別是在壓實期間臨時使用。 -在磁碟上有某些資料的多個副本也可能是一個問題,當你需要刪除某些資料,並確信它真的已被刪除(可能是為了遵守資料保護法規)。例如,在大多數 LSM 儲存引擎中,已刪除的記錄可能仍然存在於較高級別,直到代表刪除的墓碑透過所有壓縮級別傳播,這可能需要很長時間。專門的儲存引擎設計可以更快地傳播刪除 [^42]。 +在磁碟上有一些資料的多個副本也可能是一個問題,當你需要刪除一些資料,並確信它真的已被刪除(也許是為了遵守資料保護法規)。例如,在大多數 LSM 儲存引擎中,已刪除的記錄可能仍然存在於較高級別中,直到代表刪除的墓碑透過所有壓實級別傳播,這可能需要很長時間。專門的儲存引擎設計可以更快地傳播刪除 [^42]。 -另一方面,SSTable 段檔案的不可變性質很有用,如果你想在某個時間點拍攝資料庫快照(例如,用於備份或建立用於測試的資料庫副本):你可以寫出記憶體表並記錄該時間點存在的段檔案。只要你不刪除快照的一部分檔案,就不需要實際複製它們。在頁被覆蓋的 B 樹中,有效地拍攝這樣的快照更加困難。 +另一方面,SSTable 段檔案的不可變性質在你想在某個時間點對資料庫進行快照時很有用(例如,用於備份或建立資料庫副本以進行測試):你可以寫出記憶體表並記錄該時間點存在的段檔案。只要你不刪除快照的一部分的檔案,你就不需要實際複製它們。在其頁被覆蓋的 B 樹中,有效地進行這樣的快照更困難。 ### 多列索引與二級索引 {#sec_storage_index_multicolumn} -到目前為止,我們只討論了鍵值索引,它們類似於關係模型中的 *主鍵* 索引。主鍵唯一標識關係表中的一行、文件資料庫中的一個文件或圖資料庫中的一個頂點。資料庫中的其他記錄可以透過其主鍵(或 ID)引用該行/文件/頂點,索引用於解析此類引用。 +到目前為止,我們只討論了鍵值索引,它們就像關係模型中的 *主鍵* 索引。主鍵唯一標識關係表中的一行,或文件資料庫中的一個文件,或圖資料庫中的一個頂點。資料庫中的其他記錄可以透過其主鍵(或 ID)引用該行/文件/頂點,索引用於解析此類引用。 -擁有 *二級索引* 也很常見。在關係資料庫中,你可以使用 `CREATE INDEX` 命令在同一個表上建立幾個二級索引,允許你按主鍵以外的列進行搜尋。例如,在 [第 3 章](/tw/ch3#ch_datamodels) 的 [圖 3-1](/tw/ch3#fig_obama_relational) 中,你很可能在 `user_id` 列上有一個二級索引,以便你可以在每個表中找到屬於同一使用者的所有行。 +擁有 *二級索引* 也非常常見。在關係資料庫中,你可以使用 `CREATE INDEX` 命令在同一個表上建立多個二級索引,允許你按主鍵以外的列進行搜尋。例如,在 [第 3 章](/tw/ch3#ch_datamodels) 的 [圖 3-1](/tw/ch3#fig_obama_relational) 中,你很可能在 `user_id` 列上有一個二級索引,以便你可以在每個表中找到屬於同一使用者的所有行。 -二級索引可以很容易地從鍵值索引構建。主要區別在於,在二級索引中,索引值不一定是唯一的;也就是說,同一個索引條目下可能有許多行(文件、頂點)。這可以透過兩種方式解決:要麼使索引中的每個值成為匹配行識別符號列表(如全文索引中的釋出列表),要麼透過向其附加行識別符號使每個條目唯一。具有就地更新的儲存引擎(如 B 樹)和日誌結構儲存都可以用於實現索引。 +二級索引可以很容易地從鍵值索引構建。主要區別在於,在二級索引中,索引值不一定是唯一的;也就是說,同一索引條目下可能有許多行(文件、頂點)。這可以透過兩種方式解決:要麼使索引中的每個值成為匹配行識別符號的列表(如全文索引中的倒排列表),要麼透過向其追加行識別符號使每個條目唯一。具有就地更新的儲存引擎(如 B 樹)和日誌結構儲存都可用於實現索引。 #### 在索引中儲存值 {#sec_storage_index_heap} 索引中的鍵是查詢搜尋的內容,但值可以是幾種東西之一: * 如果實際資料(行、文件、頂點)直接儲存在索引結構中,則稱為 *聚簇索引*。例如,在 MySQL 的 InnoDB 儲存引擎中,表的主鍵始終是聚簇索引,在 SQL Server 中,你可以為每個表指定一個聚簇索引 [^43]。 -* 或者,值可以是對實際資料的引用:要麼是相關行的主鍵(InnoDB 對二級索引執行此操作),要麼是對磁碟上位置的直接引用。在後一種情況下,儲存行的地方稱為 *堆檔案*,它以無特定順序儲存資料(它可能是僅追加的,或者它可能跟蹤已刪除的行以便稍後用新資料覆蓋它們)。例如,Postgres 使用堆檔案方法 [^44]。 -* 兩者之間的中間地帶是 *覆蓋索引* 或 *包含列的索引*,除了在堆或主鍵聚簇索引上儲存完整行外,還在索引中儲存表的 *某些* 列 [^45]。這允許僅使用索引回答某些查詢,而無需解析主鍵或檢視堆檔案(在這種情況下,索引稱為 *覆蓋* 查詢)。這可以使某些查詢更快,但資料的重複意味著索引使用更多磁碟空間並減慢寫入速度。 +* 或者,值可以是對實際資料的引用:要麼是相關行的主鍵(InnoDB 對二級索引這樣做),要麼是對磁碟上位置的直接引用。在後一種情況下,儲存行的地方稱為 *堆檔案*,它以無特定順序儲存資料(它可能是僅追加的,或者它可能跟蹤已刪除的行以便稍後用新資料覆蓋它們)。例如,Postgres 使用堆檔案方法 [^44]。 +* 兩者之間的折中是 *覆蓋索引* 或 *包含列的索引*,它在索引中儲存表的 *某些* 列,除了在堆上或主鍵聚簇索引中儲存完整行 [^45]。這允許僅使用索引來回答某些查詢,而無需解析主鍵或檢視堆檔案(在這種情況下,索引被稱為 *覆蓋* 查詢)。這可以使某些查詢更快,但資料的重複意味著索引使用更多的磁碟空間並減慢寫入速度。 -到目前為止討論的索引只將單個鍵對映到值。如果你需要同時查詢表的多個列(或文件中的多個欄位),請參閱 ["多維和全文索引"](/tw/ch4#sec_storage_multidimensional)。 +到目前為止討論的索引只將單個鍵對映到值。如果你需要同時查詢表的多個列(或文件中的多個欄位),請參見 ["多維索引與全文索引"](#sec_storage_multidimensional)。 -在不更改鍵的情況下更新值時,堆檔案方法可以允許記錄就地覆蓋,前提是新值不大於舊值。如果新值更大,情況會更複雜,因為它可能需要移動到堆中有足夠空間的新位置。在這種情況下,要麼所有索引都需要更新以指向記錄的新堆位置,要麼在舊堆位置留下轉發指標 [^2]。 +當更新值而不更改鍵時,堆檔案方法可以允許記錄就地覆蓋,前提是新值不大於舊值。如果新值更大,情況會更複雜,因為它可能需要移動到堆中有足夠空間的新位置。在這種情況下,要麼所有索引都需要更新以指向記錄的新堆位置,要麼在舊堆位置留下轉發指標 [^2]。 ### 全記憶體儲存 {#sec_storage_inmemory} -本章到目前為止討論的資料結構都是對磁碟限制的回答。與主記憶體相比,磁碟很難處理。使用磁碟和 SSD,如果你想在讀取和寫入方面獲得良好的效能,需要仔細布局磁碟上的資料。然而,我們容忍這種尷尬,因為磁碟有兩個顯著的優勢:它們是持久的(如果電源關閉,它們的內容不會丟失),並且它們每千兆位元組的成本低於 RAM。 +本章到目前為止討論的資料結構都是對磁碟限制的回應。與主記憶體相比,磁碟很難處理。對於磁碟和 SSD,如果你想在讀取和寫入上獲得良好的效能,磁碟上的資料需要仔細布局。然而,我們容忍這種尷尬,因為磁碟有兩個顯著的優勢:它們是持久的(如果斷電,其內容不會丟失),並且它們每千兆位元組的成本比 RAM 低。 -隨著 RAM 變得更便宜,每千兆位元組成本的論點被削弱了。許多資料集根本不是那麼大,因此將它們完全儲存在記憶體中是完全可行的,可能分佈在幾臺機器上。這導致了 *記憶體資料庫* 的發展。 +隨著 RAM 變得更便宜,每千兆位元組成本的論點被侵蝕。許多資料集根本不是那麼大,因此將它們完全保留在記憶體中是完全可行的,可能分佈在幾臺機器上。這導致了 *記憶體資料庫* 的發展。 -一些記憶體鍵值儲存,如 Memcached,僅用於快取使用,如果機器重新啟動,資料丟失是可以接受的。但其他記憶體資料庫旨在永續性,這可以透過特殊硬體(如電池供電的 RAM)、將更改日誌寫入磁碟、將定期快照寫入磁碟或將記憶體狀態複製到其他機器來實現。 +一些記憶體鍵值儲存,例如 Memcached,僅用於快取,如果機器重新啟動,資料丟失是可以接受的。但其他記憶體資料庫旨在實現永續性,這可以透過特殊硬體(例如電池供電的 RAM)、將更改日誌寫入磁碟、將定期快照寫入磁碟或將記憶體狀態複製到其他機器來實現。 -當記憶體資料庫重新啟動時,它需要重新載入其狀態,要麼從磁碟,要麼透過網路從副本(除非使用特殊硬體)。儘管寫入磁碟,它仍然是記憶體資料庫,因為磁碟僅用作永續性的僅追加日誌,讀取完全從記憶體提供服務。寫入磁碟還具有操作優勢:磁碟上的檔案可以輕鬆備份、檢查和由外部實用程式分析。 +當記憶體資料庫重新啟動時,它需要重新載入其狀態,要麼從磁碟,要麼透過網路從副本(除非使用特殊硬體)。儘管寫入磁碟,它仍然是一個記憶體資料庫,因為磁碟僅用作永續性的僅追加日誌,讀取完全從記憶體提供。寫入磁碟還具有操作優勢:磁碟上的檔案可以輕鬆備份、檢查和由外部實用程式分析。 -VoltDB、SingleStore 和 Oracle TimesTen 等產品是具有關係模型的記憶體資料庫,供應商聲稱透過消除管理磁碟資料結構相關的所有開銷,它們可以提供巨大的效能改進 [^46] [^47]。RAMCloud 是一個開源的、具有永續性的記憶體鍵值儲存(對記憶體中的資料以及磁碟上的資料使用日誌結構方法)[^48]。 +VoltDB、SingleStore 和 Oracle TimesTen 等產品是具有關係模型的記憶體資料庫,供應商聲稱,透過消除管理磁碟資料結構相關的所有開銷,它們可以提供巨大的效能改進 [^46] [^47]。RAMCloud 是一個開源的記憶體鍵值儲存,具有永續性(對記憶體中的資料以及磁碟上的資料使用日誌結構方法)[^48]。 Redis 和 Couchbase 透過非同步寫入磁碟提供弱永續性。 -反直覺的是,記憶體資料庫的效能優勢不是因為它們不需要從磁碟讀取。即使基於磁碟的儲存引擎,如果你有足夠的記憶體,也可能永遠不需要從磁碟讀取,因為作業系統無論如何都會在記憶體中快取最近使用的磁碟塊。相反,它們可以更快,因為它們可以避免以可以寫入磁碟的形式編碼記憶體資料結構的開銷 [^49]。 +反直覺的是,記憶體資料庫的效能優勢不是因為它們不需要從磁碟讀取。即使是基於磁碟的儲存引擎,如果你有足夠的記憶體,也可能永遠不需要從磁碟讀取,因為作業系統無論如何都會在記憶體中快取最近使用的磁碟塊。相反,它們可以更快,因為它們可以避免將記憶體資料結構編碼為可以寫入磁碟的形式的開銷 [^49]。 -除了效能,記憶體資料庫的另一個有趣領域是提供難以用基於磁碟的索引實現的資料模型。例如,Redis 為各種資料結構(如優先順序佇列和集合)提供類似資料庫的介面。因為它將所有資料儲存在記憶體中,所以它的實現相對簡單。 +除了效能,記憶體資料庫的另一個有趣領域是提供難以使用基於磁碟的索引實現的資料模型。例如,Redis 為各種資料結構(例如優先佇列和集合)提供類似資料庫的介面。因為它將所有資料保留在記憶體中,其實現相對簡單。 ## 分析型資料儲存 {#sec_storage_analytics} -資料倉庫的資料模型最常見的是關係型,因為 SQL 通常非常適合分析查詢。有許多圖形化資料分析工具可以生成 SQL 查詢、視覺化結果,並允許分析師探索資料(透過 *下鑽* 和 *切片切塊* 等操作)。 +資料倉庫的資料模型最常見的是關係型,因為 SQL 通常非常適合分析查詢。有許多圖形資料分析工具可以生成 SQL 查詢、視覺化結果,並允許分析師探索資料(透過 *下鑽* 和 *切片切塊* 等操作)。 -表面上,資料倉庫和關係型 OLTP 資料庫看起來很相似,因為它們都有 SQL 查詢介面。然而,系統的內部可能看起來完全不同,因為它們針對非常不同的查詢模式進行了最佳化。許多資料庫供應商現在專注於支援事務處理或分析工作負載,但不是兩者都支援。 +表面上,資料倉庫和關係型 OLTP 資料庫看起來很相似,因為它們都有 SQL 查詢介面。然而,系統的內部可能看起來完全不同,因為它們針對非常不同的查詢模式進行了最佳化。許多資料庫供應商現在專注於支援事務處理或分析工作負載,但不是兩者兼而有之。 -一些資料庫,如 Microsoft SQL Server、SAP HANA 和 SingleStore,在同一產品中支援事務處理和資料倉庫。然而,這些混合事務和分析處理(HTAP)資料庫(在 ["資料倉庫"](/tw/ch1#sec_introduction_dwh) 中介紹)越來越多地變成兩個獨立的儲存和查詢引擎,它們恰好可以透過通用的 SQL 介面訪問 [^50] [^51] [^52] [^53]。 +一些資料庫,如 Microsoft SQL Server、SAP HANA 和 SingleStore,在同一產品中支援事務處理和資料倉庫。然而,這些混合事務和分析處理(HTAP)資料庫(在 ["資料倉庫"](/tw/ch1#sec_introduction_dwh) 中介紹)越來越多地成為兩個獨立的儲存和查詢引擎,它們恰好可以透過通用的 SQL 介面訪問 [^50] [^51] [^52] [^53]。 ### 雲資料倉庫 {#sec_cloud_data_warehouses} -Teradata、Vertica 和 SAP HANA 等資料倉庫供應商既銷售商業許可的本地倉庫,也銷售基於雲的解決方案。但隨著他們的許多客戶遷移到雲,新的雲資料倉庫如 Google Cloud BigQuery、Amazon Redshift 和 Snowflake 也已被廣泛採用。與傳統資料倉庫不同,雲資料倉庫利用可擴充套件的雲基礎設施,如物件儲存和無伺服器計算平臺。 +Teradata、Vertica 和 SAP HANA 等資料倉庫供應商既銷售商業許可下的本地倉庫,也銷售基於雲的解決方案。但隨著他們的許多客戶轉向雲,新的雲資料倉庫(如 Google Cloud BigQuery、Amazon Redshift 和 Snowflake)也變得廣泛採用。與傳統資料倉庫不同,雲資料倉庫利用可擴充套件的雲基礎設施,如物件儲存和無伺服器計算平臺。 -雲資料倉庫往往與其他雲服務整合得更好,並且更具彈性。例如,許多雲倉庫支援自動日誌攝取,並提供與資料處理框架(如 Google Cloud 的 Dataflow 或 Amazon Web Services 的 Kinesis)的輕鬆整合。這些倉庫也更具彈性,因為它們將查詢計算與儲存層解耦 [^54]。資料持久化在物件儲存而不是本地磁碟上,這使得獨立調整儲存容量和查詢計算資源變得容易,正如我們之前在 ["雲原生系統架構"](/tw/ch1#sec_introduction_cloud_native) 中看到的。 +雲資料倉庫往往與其他雲服務更好地整合,並且更具彈性。例如,許多雲倉庫支援自動日誌攝取,並提供與資料處理框架(如 Google Cloud 的 Dataflow 或 Amazon Web Services 的 Kinesis)的輕鬆整合。這些倉庫也更具彈性,因為它們將查詢計算與儲存層解耦 [^54]。資料持久儲存在物件儲存而不是本地磁碟上,這使得可以獨立調整儲存容量和查詢的計算資源,正如我們之前在 ["雲原生系統架構"](/tw/ch1#sec_introduction_cloud_native) 中看到的。 -開源資料倉庫如 Apache Hive、Trino 和 Apache Spark 也隨著雲而發展。隨著分析資料儲存轉移到物件儲存上的資料湖,開源倉庫已經開始分解 [^55]。以下元件以前整合在單個系統(如 Apache Hive)中,現在通常作為單獨的元件實現: +Apache Hive、Trino 和 Apache Spark 等開源資料倉庫也隨著雲的發展而發展。隨著分析資料儲存轉移到物件儲存上的資料湖,開源倉庫已經開始分解 [^55]。以下元件以前整合在單個系統(如 Apache Hive)中,現在通常作為單獨的元件實現: 查詢引擎 -: 查詢引擎如 Trino、Apache DataFusion 和 Presto 解析 SQL 查詢,將它們最佳化為執行計劃,並針對資料執行它們。執行通常需要並行的分散式資料處理任務。一些查詢引擎提供內建任務執行,而其他查詢引擎選擇使用第三方執行框架,如 Apache Spark 或 Apache Flink。 +: Trino、Apache DataFusion 和 Presto 等查詢引擎解析 SQL 查詢,將其最佳化為執行計劃,並針對資料執行它們。執行通常需要並行、分散式資料處理任務。一些查詢引擎提供內建任務執行,而其他選擇使用第三方執行框架,如 Apache Spark 或 Apache Flink。 儲存格式 -: 儲存格式決定了表的行如何編碼為檔案中的位元組,然後通常儲存在物件儲存或分散式檔案系統中 [^12]。然後,查詢引擎可以訪問此資料,但使用資料湖的其他應用程式也可以訪問。此類儲存格式的示例包括 Parquet、ORC、Lance 或 Nimble,我們將在下一節中看到更多相關內容。 +: 儲存格式確定表的行如何編碼為檔案中的位元組,然後通常儲存在物件儲存或分散式檔案系統中 [^12]。然後查詢引擎可以訪問這些資料,但使用資料湖的其他應用程式也可以訪問。此類儲存格式的示例包括 Parquet、ORC、Lance 或 Nimble,我們將在下一節中看到更多關於它們的內容。 表格式 -: 以 Apache Parquet 和類似儲存格式編寫的檔案一旦寫入通常是不可變的。為了支援行插入和刪除,使用表格式,如 Apache Iceberg 或 Databricks 的 Delta 格式。表格式指定一種檔案格式,該格式定義哪些檔案構成表以及表的模式。此類格式還提供高階功能,如時間旅行(查詢表在先前時間點的狀態的能力)、垃圾收集,甚至事務。 +: 以 Apache Parquet 和類似儲存格式編寫的檔案一旦編寫通常是不可變的。為了支援行插入和刪除,使用 Apache Iceberg 或 Databricks 的 Delta 格式等表格式。表格式指定定義哪些檔案構成表以及表模式的檔案格式。此類格式還提供高階功能,例如時間旅行(查詢表在以前時間點的能力)、垃圾回收,甚至事務。 資料目錄 -: 就像表格式定義哪些檔案組成表一樣,資料目錄定義哪些表組成資料庫。目錄用於建立、重新命名和刪除表。與儲存和表格式不同,資料目錄如 Snowflake 的 Polaris 和 Databricks 的 Unity Catalog 通常作為獨立服務執行,可以使用 REST 介面查詢。Apache Iceberg 也提供目錄,可以在客戶端內執行或作為單獨的程序執行。查詢引擎在讀取和寫入表時使用目錄資訊。傳統上,目錄和查詢引擎是整合的,但將它們解耦使資料發現和資料治理系統(在 ["資料系統、法律和社會"](/tw/ch1#sec_introduction_compliance) 中討論)也能訪問目錄的元資料。 +: 就像表格式定義哪些檔案構成表一樣,資料目錄定義哪些表組成資料庫。目錄用於建立、重新命名和刪除表。與儲存和表格式不同,Snowflake 的 Polaris 和 Databricks 的 Unity Catalog 等資料目錄通常作為可以使用 REST 介面查詢的獨立服務執行。Apache Iceberg 也提供目錄,可以在客戶端內執行或作為單獨的程序執行。查詢引擎在讀取和寫入表時使用目錄資訊。傳統上,目錄和查詢引擎已經整合,但將它們解耦使資料發現和資料治理系統(在 ["資料系統、法律和社會"](/tw/ch1#sec_introduction_compliance) 中討論)也能夠訪問目錄的元資料。 ### 列式儲存 {#sec_storage_column} -如 ["星型和雪花型:分析模式"](/tw/ch3#sec_datamodels_analytics) 中所討論的,資料倉庫按照慣例通常使用關係模式,其中有一個包含對維度表的外部索引鍵引用的大型事實表。如果你的事實表中有數萬億行和 PB 級資料,有效地儲存和查詢它們就成為一個具有挑戰性的問題。維度表通常要小得多(數百萬行),因此在本節中我們將重點關注事實的儲存。 +如 ["星型和雪花型:分析模式"](/tw/ch3#sec_datamodels_analytics) 中所討論的,資料倉庫按照慣例通常使用帶有大型事實表的關係模式,該表包含對維度表的外部索引鍵引用。如果你的事實表中有數萬億行和數 PB 的資料,有效地儲存和查詢它們就成為一個具有挑戰性的問題。維度表通常要小得多(數百萬行),因此在本節中我們將重點關注事實的儲存。 -儘管事實表通常超過 100 列寬,但典型的資料倉庫查詢一次只訪問其中的 4 或 5 列(分析很少需要 `"SELECT *"` 查詢)[^52]。以 [示例 4-1](/tw/ch4#fig_storage_analytics_query) 中的查詢為例:它訪問大量行(2024 日曆年中每次有人購買水果或糖果的情況),但它只需要訪問 `fact_sales` 表的三列:`date_key`、`product_sk` 和 `quantity`。查詢忽略所有其他列。 +儘管事實表通常有超過 100 列,但典型的資料倉庫查詢一次只訪問其中的 4 或 5 列(分析很少需要 `"SELECT *"` 查詢)[^52]。以 [示例 4-1](#fig_storage_analytics_query) 中的查詢為例:它訪問大量行(2024 日曆年期間每次有人購買水果或糖果的情況),但它只需要訪問 `fact_sales` 表的三列:`date_key`、`product_sk` 和 `quantity`。查詢忽略所有其他列。 {{< figure id="fig_storage_analytics_query" title="示例 4-1. 分析人們是否更傾向於購買新鮮水果或糖果,取決於星期幾" class="w-full my-4" >}} @@ -379,40 +379,40 @@ GROUP BY dim_date.weekday, dim_product.category; ``` -我們如何有效地執行這個查詢? +我們如何高效地執行這個查詢? -在大多數 OLTP 資料庫中,儲存以 *行式* 方式佈局:表的一行中的所有值彼此相鄰儲存。文件資料庫類似:整個文件通常作為一個連續的位元組序列儲存。你可以在 [圖 4-1](/tw/ch4#fig_storage_csv_hash_index) 的 CSV 示例中看到這一點。 +在大多數 OLTP 資料庫中,儲存是以 *面向行* 的方式佈局的:表中一行的所有值彼此相鄰儲存。文件資料庫類似:整個文件通常作為一個連續的位元組序列儲存。你可以在 [圖 4-1](#fig_storage_csv_hash_index) 的 CSV 示例中看到這一點。 -為了處理像 [示例 4-1](/tw/ch4#fig_storage_analytics_query) 這樣的查詢,你可能在 `fact_sales.date_key` 和/或 `fact_sales.product_sk` 上有索引,它們告訴儲存引擎在哪裡找到特定日期或特定產品的所有銷售。但是,行式儲存引擎仍然需要將所有這些行(每行由 100 多個屬性組成)從磁碟載入到記憶體中,解析它們,並過濾掉不符合所需條件的行。這可能需要很長時間。 +為了處理像 [示例 4-1](#fig_storage_analytics_query) 這樣的查詢,你可能在 `fact_sales.date_key` 和/或 `fact_sales.product_sk` 上有索引,告訴儲存引擎在哪裡找到特定日期或特定產品的所有銷售。但是,面向行的儲存引擎仍然需要將所有這些行(每行包含超過 100 個屬性)從磁碟載入到記憶體中,解析它們,並過濾掉不符合所需條件的行。這可能需要很長時間。 -*列式*(或 *列式*)儲存背後的想法很簡單:不要將一行中的所有值儲存在一起,而是將每個 *列* 中的所有值儲存在一起 [^56]。如果每列單獨儲存,查詢只需要讀取和解析該查詢中使用的那些列,這可以節省大量工作。[圖 4-7](/tw/ch4#fig_column_store) 使用 [圖 3-5](/tw/ch3#fig_dwh_schema) 中事實表的擴充套件版本顯示了這一原理。 +*面向列*(或 *列式*)儲存背後的想法很簡單:不要將一行中的所有值儲存在一起,而是將每 *列* 中的所有值儲存在一起 [^56]。如果每列單獨儲存,查詢只需要讀取和解析該查詢中使用的那些列,這可以節省大量工作。[圖 4-7](#fig_column_store) 使用 [圖 3-5](/tw/ch3#fig_dwh_schema) 中事實表的擴充套件版本展示了這一原理。 -------- > [!NOTE] -> 列儲存在關係資料模型中最容易理解,但它同樣適用於非關係資料。例如,Parquet [^57] 是一種支援文件資料模型的列式儲存格式,基於 Google 的 Dremel [^58],使用一種稱為 *切碎* 或 *條帶化* 的技術 [^59]。 +> 列儲存在關係資料模型中最容易理解,但它同樣適用於非關係資料。例如,Parquet [^57] 是一種列式儲存格式,它支援基於 Google 的 Dremel [^58] 的文件資料模型,使用一種稱為 *分解*(*shredding*)或 *條帶化*(*striping*)的技術 [^59]。 -------- {{< figure src="/fig/ddia_0407.png" id="fig_column_store" caption="圖 4-7. 按列而不是按行儲存關係資料。" class="w-full my-4" >}} -列式儲存佈局依賴於每列以相同順序儲存行。因此,如果你需要重新組裝整行,你可以從每個單獨列中取出第 23 個條目,並將它們組合在一起形成表的第 23 行。 +面向列的儲存佈局依賴於每列以相同順序儲存行。因此,如果你需要重新組裝整行,你可以從每個單獨的列中取出第 23 個條目,並將它們組合在一起形成表的第 23 行。 -實際上,列式儲存引擎實際上並不會一次儲存整個列(可能包含數萬億行)。相反,它們將表分成數千或數百萬行的塊,並在每個塊內分別儲存每列的值 [^60]。由於許多查詢限制在特定日期範圍內,因此通常使每個塊包含特定時間戳範圍的行。然後查詢只需要在與所需日期範圍重疊的那些塊中載入它需要的列。 +實際上,列式儲存引擎並不真的一次儲存整個列(可能包含數萬億行)。相反,它們將表分解為數千或數百萬行的塊,並且在每個塊內,它們分別儲存每列的值 [^60]。由於許多查詢都限制在特定的日期範圍內,因此通常使每個塊包含特定時間戳範圍的行。然後查詢只需要在與所需日期範圍重疊的那些塊中載入它需要的列。 -列式儲存現在幾乎用於所有分析資料庫 [^60],從大規模雲資料倉庫(如 Snowflake [^61])到單節點嵌入式資料庫(如 DuckDB [^62]),以及產品分析系統(如 Pinot [^63] 和 Druid [^64])。它用於儲存格式,如 Parquet、ORC [^65] [^66]、Lance [^67] 和 Nimble [^68],以及記憶體分析格式,如 Apache Arrow [^65] [^69] 和 Pandas/NumPy [^70]。一些時間序列資料庫,如 InfluxDB IOx [^71] 和 TimescaleDB [^72],也基於列式儲存。 +列式儲存如今幾乎用於所有分析資料庫 [^60],從大規模雲資料倉庫(如 Snowflake [^61])到單節點嵌入式資料庫(如 DuckDB [^62]),以及產品分析系統(如 Pinot [^63] 和 Druid [^64])。它用於儲存格式,如 Parquet、ORC [^65] [^66]、Lance [^67] 和 Nimble [^68],以及記憶體分析格式,如 Apache Arrow [^65] [^69] 和 Pandas/NumPy [^70]。一些時間序列資料庫,如 InfluxDB IOx [^71] 和 TimescaleDB [^72],也基於面向列的儲存。 #### 列壓縮 {#sec_storage_column_compression} -除了只從磁碟載入查詢所需的那些列外,我們還可以透過壓縮資料進一步減少對磁碟吞吐量和網路頻寬的需求。幸運的是,列式儲存通常非常適合壓縮。 +除了只從磁碟載入查詢所需的那些列之外,我們還可以透過壓縮資料進一步減少對磁碟吞吐量和網路頻寬的需求。幸運的是,面向列的儲存通常非常適合壓縮。 -看看 [圖 4-7](/tw/ch4#fig_column_store) 中每列的值序列:它們通常看起來相當重複,這是壓縮的好兆頭。根據列中的資料,可以使用不同的壓縮技術。在資料倉庫中特別有效的一種技術是 *點陣圖編碼*,如 [圖 4-8](/tw/ch4#fig_bitmap_index) 所示。 +看看 [圖 4-7](#fig_column_store) 中每列的值序列:它們看起來經常重複,這是壓縮的良好跡象。根據列中的資料,可以使用不同的壓縮技術。在資料倉庫中特別有效的一種技術是 *點陣圖編碼*,如 [圖 4-8](#fig_bitmap_index) 所示。 {{< figure src="/fig/ddia_0408.png" id="fig_bitmap_index" caption="圖 4-8. 單列的壓縮、點陣圖索引儲存。" class="w-full my-4" >}} -通常,列中不同值的數量與行數相比很小(例如,零售商可能有數十億筆銷售交易,但只有 100,000 種不同的產品)。我們現在可以將具有 *n* 個不同值的列轉換為 *n* 個單獨的點陣圖:每個不同值一個位圖,每行一位。如果行具有該值,則位為 1,否則為 0。 +通常,列中不同值的數量與行數相比很小(例如,零售商可能有數十億條銷售交易,但只有 100,000 種不同的產品)。我們現在可以將具有 *n* 個不同值的列轉換為 *n* 個單獨的點陣圖:每個不同值一個位圖,每行一位。如果該行具有該值,則該位為 1,否則為 0。 -一種選擇是使用每行一位儲存這些點陣圖。然而,這些點陣圖通常包含大量零(我們說它們是 *稀疏* 的)。在這種情況下,點陣圖可以額外進行遊程編碼:計算連續零或一的數量並存儲該數字,如 [圖 4-8](/tw/ch4#fig_bitmap_index) 底部所示。*咆哮點陣圖* 等技術在兩種位圖表示之間切換,使用更緊湊的表示 [^73]。這可以使列的編碼非常高效。 +一種選擇是使用每行一位來儲存這些點陣圖。然而,這些點陣圖通常包含大量零(我們說它們是 *稀疏* 的)。在這種情況下,點陣圖可以另外進行遊程編碼:計算連續零或一的數量並存儲該數字,如 [圖 4-8](#fig_bitmap_index) 底部所示。諸如 *咆哮點陣圖*(*roaring bitmaps*)之類的技術在兩種位圖表示之間切換,使用最緊湊的表示 [^73]。這可以使列的編碼非常高效。 像這樣的點陣圖索引非常適合資料倉庫中常見的查詢型別。例如: @@ -420,179 +420,178 @@ GROUP BY : 載入 `product_sk = 31`、`product_sk = 68` 和 `product_sk = 69` 的三個點陣圖,並計算三個點陣圖的按位 *OR*,這可以非常高效地完成。 `WHERE product_sk = 30 AND store_sk = 3:` -: 載入 `product_sk = 30` 和 `store_sk = 3` 的點陣圖,並計算按位 *AND*。這有效是因為列以相同順序包含行,因此一列點陣圖中的第 *k* 位對應於另一列點陣圖中第 *k* 位的同一行。 +: 載入 `product_sk = 30` 和 `store_sk = 3` 的點陣圖,並計算按位 *AND*。這有效是因為列以相同的順序包含行,所以一列點陣圖中的第 *k* 位對應於另一列點陣圖中第 *k* 位的同一行。 -點陣圖也可用於回答圖查詢,例如查詢社交網路中被使用者 *X* 關注且也關注使用者 *Y* 的所有使用者 [^74]。列式資料庫還有各種其他壓縮方案,你可以在參考文獻中找到 [^75]。 +點陣圖也可用於回答圖查詢,例如查詢社交網路中被使用者 *X* 關注並且也關注使用者 *Y* 的所有使用者 [^74]。列式資料庫還有各種其他壓縮方案,你可以在參考文獻中找到 [^75]。 -------- > [!NOTE] -> 不要將列式資料庫與 *寬列*(也稱為 *列族*)資料模型混淆,在該模型中,一行可以有數千列,並且不需要所有行都具有相同的列 [^9]。儘管名稱相似,寬列資料庫是行式的,因為它們將一行中的所有值儲存在一起。Google 的 Bigtable、Apache Accumulo 和 HBase 是寬列模型的示例。 +> 不要將面向列的資料庫與 *寬列*(也稱為 *列族*)資料模型混淆,在該模型中,一行可以有數千列,並且不需要所有行都有相同的列 [^9]。儘管名稱相似,寬列資料庫是面向行的,因為它們將一行中的所有值儲存在一起。Google 的 Bigtable、Apache Accumulo 和 HBase 是寬列模型的例子。 -------- #### 列儲存中的排序順序 {#sort-order-in-column-storage} -在列儲存中,行儲存的順序不一定重要。最簡單的是按插入順序儲存它們,因為插入新行只意味著追加到每列。然而,我們可以選擇施加順序,就像我們之前對 SSTable 所做的那樣,並將其用作索引機制。 +在列儲存中,行的儲存順序並不一定重要。最簡單的是按插入順序儲存它們,因為這樣插入新行只需追加到每列。但是,我們可以選擇強制執行順序,就像我們之前對 SSTable 所做的那樣,並將其用作索引機制。 -請注意,獨立排序每列是沒有意義的,因為那樣我們就不再知道列中的哪些項屬於同一行。我們只能重建一行,因為我們知道一列中的第 *k* 項與另一列中的第 *k* 項屬於同一行。 +請注意,獨立排序每列是沒有意義的,因為那樣我們就不再知道列中的哪些項屬於同一行。我們只能重建一行,因為我們知道一列中的第 *k* 個項與另一列中的第 *k* 個項屬於同一行。 -相反,資料需要一次排序整行,即使它是按列儲存的。資料庫管理員可以使用他們對常見查詢的瞭解來選擇表應該按哪些列排序。例如,如果查詢經常針對日期範圍(如上個月),那麼將 `date_key` 作為第一個排序鍵可能是有意義的。然後查詢只能掃描上個月的行,這將比掃描所有行快得多。 +相反,資料需要一次排序整行,即使它是按列儲存的。資料庫管理員可以使用他們對常見查詢的瞭解來選擇表應按哪些列排序。例如,如果查詢經常針對日期範圍(例如上個月),則將 `date_key` 作為第一個排序鍵可能是有意義的。然後查詢可以只掃描上個月的行,這將比掃描所有行快得多。 -第二列可以確定在第一列中具有相同值的任何行的排序順序。例如,如果 `date_key` 是 [圖 4-7](/tw/ch4#fig_column_store) 中的第一個排序鍵,那麼 `product_sk` 作為第二個排序鍵可能是有意義的,這樣同一天同一產品的所有銷售在儲存中組合在一起。這將有助於需要在特定日期範圍內按產品分組或過濾銷售的查詢。 +第二列可以確定在第一列中具有相同值的任何行的排序順序。例如,如果 `date_key` 是 [圖 4-7](#fig_column_store) 中的第一個排序鍵,那麼 `product_sk` 作為第二個排序鍵可能是有意義的,這樣同一天同一產品的所有銷售都在儲存中分組在一起。這將有助於需要在某個日期範圍內按產品分組或過濾銷售的查詢。 -排序順序的另一個優點是它可以幫助列的壓縮。如果主排序列沒有很多不同的值,那麼排序後,它將有很長的序列,其中相同的值連續重複多次。簡單的遊程編碼(就像我們在 [圖 4-8](/tw/ch4#fig_bitmap_index) 中用於點陣圖的那樣)可以將該列壓縮到幾千位元組——即使表有數十億行。 +排序順序的另一個優點是它可以幫助壓縮列。如果主排序列沒有許多不同的值,那麼排序後,它將有很長的序列,其中相同的值在一行中重複多次。簡單的遊程編碼,就像我們在 [圖 4-8](#fig_bitmap_index) 中用於點陣圖的那樣,可以將該列壓縮到幾千位元組 —— 即使表有數十億行。 -該壓縮效果在第一個排序鍵上最強。第二個和第三個排序鍵將更加混亂,因此不會有如此長的重複值執行。排序優先順序更低的列基本上以隨機順序出現,因此它們可能不會壓縮得那麼好。但是,對前幾列進行排序總體上仍然是一種勝利。 +該壓縮效果在第一個排序鍵上最強。第二和第三個排序鍵將更加混亂,因此不會有如此長的重複值執行。排序優先順序較低的列基本上以隨機順序出現,因此它們可能不會壓縮得那麼好。但是,讓前幾列排序仍然是整體上的勝利。 #### 寫入列式儲存 {#writing-to-column-oriented-storage} -我們在 ["表徵事務處理和分析"](/tw/ch1#sec_introduction_oltp) 中看到,資料倉庫中的讀取往往包括對大量行的聚合;列式儲存、壓縮和排序都有助於使這些讀取查詢更快。資料倉庫中的寫入往往是資料的批次匯入,通常透過 ETL 過程。 +我們在 ["事務處理和分析的特徵"](/tw/ch1#sec_introduction_oltp) 中看到,資料倉庫中的讀取往往包括大量行的聚合;列式儲存、壓縮和排序都有助於使這些讀取查詢更快。資料倉庫中的寫入往往是資料的批次匯入,通常透過 ETL 過程。 -使用列式儲存,在排序表中間某處寫入單個行將非常低效,因為你必須從插入位置開始重寫所有壓縮列。然而,一次批次寫入許多行可以分攤重寫這些列的成本,使其高效。 +使用列式儲存,在排序表的中間某處寫入單個行將非常低效,因為你必須從插入位置開始重寫所有壓縮列。但是,一次批次寫入許多行會分攤重寫這些列的成本,使其高效。 -通常使用日誌結構方法批次執行寫入。所有寫入首先進入行式、排序的記憶體儲存。當積累了足夠的寫入時,它們與磁碟上的列編碼檔案合併,並批次寫入新檔案。由於舊檔案保持不可變,新檔案一次寫入,物件儲存非常適合儲存這些檔案。 +通常使用日誌結構方法以批次執行寫入。所有寫入首先進入面向行的、排序的記憶體儲存。當積累了足夠的寫入時,它們將與磁碟上的列編碼檔案合併,並批次寫入新檔案。由於舊檔案保持不可變,新檔案一次寫入,物件儲存非常適合儲存這些檔案。 -查詢需要檢查磁碟上的列資料和記憶體中的最近寫入,並將兩者結合起來。查詢執行引擎向用戶隱藏了這種區別。從分析師的角度來看,透過插入、更新或刪除修改的資料會立即反映在後續查詢中。Snowflake、Vertica、Apache Pinot、Apache Druid 和許多其他系統都這樣做 [^61] [^63] [^64] [^76]。 +查詢需要檢查磁碟上的列資料和記憶體中的最近寫入,並將兩者結合起來。查詢執行引擎對使用者隱藏了這種區別。從分析師的角度來看,已透過插入、更新或刪除修改的資料會立即反映在後續查詢中。Snowflake、Vertica、Apache Pinot、Apache Druid 和許多其他系統都這樣做 [^61] [^63] [^64] [^76]。 ### 查詢執行:編譯與向量化 {#sec_storage_vectorized} -複雜的分析 SQL 查詢被分解為由多個階段組成的 *查詢計劃*,稱為 *運算元*,這些運算元可能分佈在多臺機器上以進行並行執行。查詢規劃器可以透過選擇使用哪些運算元、以什麼順序執行它們以及在哪裡執行每個運算元來執行大量最佳化。 +用於分析的複雜 SQL 查詢被分解為由多個階段組成的 *查詢計劃*,稱為 *運算元*,這些運算元可能分佈在多臺機器上以並行執行。查詢規劃器可以透過選擇使用哪些運算元、以何種順序執行它們以及在哪裡執行每個運算元來執行大量最佳化。 -在每個運算元中,查詢引擎需要對列中的值執行各種操作,例如查詢值在特定值集中的所有行(可能作為連線的一部分),或檢查值是否大於 15。它還需要檢視同一行的幾列,例如查詢所有產品是香蕉且商店是特定感興趣商店的銷售交易。 +在每個運算元內,查詢引擎需要對列中的值執行各種操作,例如查詢值在特定值集中的所有行(可能作為連線的一部分),或檢查值是否大於 15。它還需要檢視同一行的幾列,例如查詢產品是香蕉且商店是特定感興趣商店的所有銷售交易。 -對於需要掃描數百萬行的資料倉庫查詢,我們不僅需要擔心它們需要從磁碟讀取的資料量,還需要擔心執行複雜運算元所需的 CPU 時間。最簡單的運算元型別就像程式語言的直譯器:在迭代每行時,它檢查表示查詢的資料結構,以找出需要對哪些列執行哪些比較或計算。不幸的是,這對於許多分析目的來說太慢了。出現了兩種高效查詢執行的替代方法 [^77]: +對於需要掃描數百萬行的資料倉庫查詢,我們不僅需要擔心它們需要從磁碟讀取的資料量,還需要擔心執行複雜運算元所需的 CPU 時間。最簡單的運算元型別就像程式語言的直譯器:在遍歷每一行時,它檢查表示查詢的資料結構,以找出需要對哪些列執行哪些比較或計算。不幸的是,這對許多分析目的來說太慢了。高效查詢執行的兩種替代方法已經出現 [^77]: 查詢編譯 -: 查詢引擎獲取 SQL 查詢並生成用於執行它的程式碼。程式碼逐行迭代,檢視感興趣列中的值,執行所需的任何比較或計算,並在滿足所需條件時將必要的值複製到輸出緩衝區。查詢引擎將生成的程式碼編譯為機器程式碼(通常使用現有編譯器,如 LLVM),然後在已載入到記憶體中的列編碼資料上執行它。這種程式碼生成方法類似於 Java 虛擬機器(JVM)和類似執行時中使用的即時(JIT)編譯方法。 +: 查詢引擎獲取 SQL 查詢並生成用於執行它的程式碼。程式碼逐行迭代,檢視感興趣列中的值,執行所需的任何比較或計算,如果滿足所需條件,則將必要的值複製到輸出緩衝區。查詢引擎將生成的程式碼編譯為機器程式碼(通常使用現有編譯器,如 LLVM),然後在已載入到記憶體中的列編碼資料上執行它。這種程式碼生成方法類似於 Java 虛擬機器(JVM)和類似執行時中使用的即時(JIT)編譯方法。 向量化處理 -: 查詢被解釋,而不是編譯,但透過批次處理列中的許多值而不是逐行迭代來加快速度。一組固定的預定義運算元內置於資料庫中;我們可以向它們傳遞引數並獲得一批結果 [^50] [^75]。 +: 查詢被解釋,而不是編譯,但透過批次處理列中的許多值而不是逐行迭代來提高速度。一組固定的預定義運算元內建在資料庫中;我們可以向它們傳遞引數並獲得一批結果 [^50] [^75]。 -例如,我們可以將 `product_sk` 列和"香蕉"的 ID 傳遞給相等運算元,並獲得一個位圖(輸入列中每個值一位,如果是香蕉則為 1);然後我們可以將 `store_sk` 列和感興趣商店的 ID 傳遞給同一個相等運算元,並獲得另一個位圖;然後我們可以將兩個點陣圖傳遞給"按位 AND"運算元,如 [圖 4-9](/tw/ch4#fig_bitmap_and) 所示。結果將是一個位圖,其中包含特定商店中所有香蕉銷售的 1。 +例如,我們可以將 `product_sk` 列和"香蕉"的 ID 傳遞給相等運算元,並獲得一個位圖(輸入列中每個值一位,如果是香蕉則為 1);然後我們可以將 `store_sk` 列和感興趣商店的 ID 傳遞給相同的相等運算元,並獲得另一個位圖;然後我們可以將兩個點陣圖傳遞給"按位 AND"運算元,如 [圖 4-9](#fig_bitmap_and) 所示。結果將是一個位圖,包含特定商店中所有香蕉銷售的 1。 {{< figure src="/fig/ddia_0409.png" id="fig_bitmap_and" caption="圖 4-9. 兩個點陣圖之間的按位 AND 適合向量化。" class="w-full my-4" >}} -這兩種方法在實現方面非常不同,但在實踐中都在使用 [^77]。兩者都可以透過利用現代 CPU 的特性來實現非常好的效能: +這兩種方法在實現方面非常不同,但兩者都在實踐中使用 [^77]。兩者都可以透過利用現代 CPU 的特性來實現非常好的效能: -* 優先順序記憶體訪問而不是隨機訪問以減少快取未命中 [^78], -* 在緊密的內部迴圈中完成大部分工作(即,使用少量指令且沒有函式呼叫)以保持 CPU 指令處理管道繁忙併避免分支錯誤預測, -* 利用並行性,如多執行緒和單指令多資料(SIMD)指令 [^79] [^80],以及 -* 直接對壓縮資料進行操作,而無需將其解碼為單獨的記憶體表示,這節省了記憶體分配和複製成本。 +* 優先選擇順序記憶體訪問而不是隨機訪問以減少快取未命中 [^78], +* 在緊密的內部迴圈中完成大部分工作(即,具有少量指令且沒有函式呼叫)以保持 CPU 指令處理管道繁忙併避免分支預測錯誤, +* 利用並行性,例如多執行緒和單指令多資料(SIMD)指令 [^79] [^80],以及 +* 直接對壓縮資料進行操作,而無需將其解碼為單獨的記憶體表示,這可以節省記憶體分配和複製成本。 ### 物化檢視與多維資料集 {#sec_storage_materialized_views} -我們之前在 ["物化和更新時間線"](/tw/ch2#sec_introduction_materializing) 中遇到了 *物化檢視*:在關係資料模型中,它們是類似表的物件,其內容是某些查詢的結果。區別在於物化檢視是查詢結果的實際副本,寫入磁碟,而虛擬檢視只是編寫查詢的快捷方式。當你從虛擬檢視讀取時,SQL 引擎會即時將其擴充套件為檢視的底層查詢,然後處理擴充套件的查詢。 +我們之前在 ["物化和更新時間線"](/tw/ch2#sec_introduction_materializing) 中遇到了 *物化檢視*:在關係資料模型中,它們是表狀物件,其內容是某些查詢的結果。區別在於物化檢視是查詢結果的實際副本,寫入磁碟,而虛擬檢視只是編寫查詢的快捷方式。當你從虛擬檢視讀取時,SQL 引擎會即時將其擴充套件為檢視的基礎查詢,然後處理擴充套件的查詢。 -當底層資料發生變化時,物化檢視需要相應地更新。一些資料庫可以自動執行此操作,還有像 Materialize 這樣專門從事物化檢視維護的系統 [^81]。執行此類更新意味著寫入時需要更多工作,但物化檢視可以提高重複需要執行相同查詢的工作負載中的讀取效能。 +當基礎資料更改時,物化檢視需要相應更新。一些資料庫可以自動執行此操作,還有像 Materialize 這樣專門從事物化檢視維護的系統 [^81]。執行此類更新意味著寫入時需要更多工作,但物化檢視可以改善在重複需要執行相同查詢的工作負載中的讀取效能。 -*物化聚合* 是一種可以在資料倉庫中有用的物化檢視型別。如前所述,資料倉庫查詢通常涉及聚合函式,如 SQL 中的 `COUNT`、`SUM`、`AVG`、`MIN` 或 `MAX`。如果許多不同的查詢使用相同的聚合,每次都處理原始資料可能是浪費的。為什麼不快取查詢最常使用的一些計數或總和呢?*資料立方體* 或 *OLAP 立方體* 透過建立按不同維度分組的聚合網格來做到這一點 [^82]。[圖 4-10](/tw/ch4#fig_data_cube) 顯示了一個示例。 +*物化聚合* 是一種可以在資料倉庫中有用的物化檢視型別。如前所述,資料倉庫查詢通常涉及聚合函式,例如 SQL 中的 `COUNT`、`SUM`、`AVG`、`MIN` 或 `MAX`。如果許多不同的查詢使用相同的聚合,每次都處理原始資料可能會很浪費。為什麼不快取查詢最常使用的一些計數或總和?*多維資料集* 或 *OLAP 立方體* 透過建立按不同維度分組的聚合網格來做到這一點 [^82]。[圖 4-10](#fig_data_cube) 顯示了一個示例。 -{{< figure src="/fig/ddia_0410.png" id="fig_data_cube" caption="圖 4-10. 資料立方體的兩個維度,透過求和聚合資料。" class="w-full my-4" >}} +{{< figure src="/fig/ddia_0410.png" id="fig_data_cube" caption="圖 4-10. 多維資料集的兩個維度,透過求和聚合資料。" class="w-full my-4" >}} -現在想象每個事實只有兩個維度表的外部索引鍵——在 [圖 4-10](/tw/ch4#fig_data_cube) 中,這些是 `date_key` 和 `product_sk`。你現在可以繪製一個二維表,日期沿一個軸,產品沿另一個軸。每個單元格包含具有該日期-產品組合的所有事實的屬性(例如,`net_price`)的聚合(例如,`SUM`)。然後,你可以沿著每行或列應用相同的聚合,並獲得已減少一個維度的摘要(無論日期如何的產品銷售,或無論產品如何的日期銷售)。 +現在假設每個事實只有兩個維度表的外部索引鍵 —— 在 [圖 4-10](#fig_data_cube) 中,這些是 `date_key` 和 `product_sk`。你現在可以繪製一個二維表,日期沿著一個軸,產品沿著另一個軸。每個單元格包含具有該日期-產品組合的所有事實的屬性(例如 `net_price`)的聚合(例如 `SUM`)。然後,你可以沿著每行或列應用相同的聚合,並獲得已減少一個維度的摘要(不管日期的產品銷售,或不管產品的日期銷售)。 一般來說,事實通常有兩個以上的維度。在 [圖 3-5](/tw/ch3#fig_dwh_schema) 中有五個維度:日期、產品、商店、促銷和客戶。很難想象五維超立方體會是什麼樣子,但原理保持不變:每個單元格包含特定日期-產品-商店-促銷-客戶組合的銷售。然後可以沿著每個維度重複彙總這些值。 -物化資料立方體的優點是某些查詢變得非常快,因為它們已經有效地被預先計算。例如,如果你想知道昨天每家商店的總銷售額,你只需要檢視適當維度的總計——無需掃描數百萬行。 +物化多維資料集的優點是某些查詢變得非常快,因為它們已經有效地預先計算了。例如,如果你想知道昨天每個商店的總銷售額,你只需要檢視適當維度的總計 —— 不需要掃描數百萬行。 -缺點是資料立方體沒有查詢原始資料的靈活性。例如,無法計算成本超過 100 美元的商品所佔銷售額的比例,因為價格不是維度之一。因此,大多數資料倉庫嘗試保留儘可能多的原始資料,並僅將聚合(如資料立方體)用作某些查詢的效能提升。 +缺點是多維資料集沒有與查詢原始資料相同的靈活性。例如,沒有辦法計算成本超過 100 美元的商品的銷售比例,因為價格不是維度之一。因此,大多數資料倉庫儘可能多地保留原始資料,並僅將聚合(如多維資料集)用作某些查詢的效能提升。 ## 多維索引與全文索引 {#sec_storage_multidimensional} -我們在本章前半部分看到的 B 樹和 LSM 樹允許對單個屬性進行範圍查詢:例如,如果鍵是使用者名稱,你可以使用它們作為索引來高效地查詢所有以 L 開頭的名字。但有時,按單個屬性搜尋是不夠的。 +我們在本章前半部分看到的 B 樹和 LSM 樹允許對單個屬性進行範圍查詢:例如,如果鍵是使用者名稱,你可以使用它們作為索引來高效查詢所有以 L 開頭的名稱。但有時,按單個屬性搜尋是不夠的。 -最常見的多列索引型別稱為 *聯合索引*,它透過將一列追加到另一列來簡單地將幾個欄位組合成一個鍵(索引定義指定欄位連線的順序)。這就像老式的紙質電話簿,它提供了從(*姓*,*名*)到電話號碼的索引。由於排序順序,索引可用於查詢所有具有特定姓氏的人,或所有具有特定 *姓-名* 組合的人。但是,如果你想查詢所有具有特定名字的人,索引就沒用了。 +最常見的多列索引型別稱為 *聯合索引*,它透過將一列追加到另一列來將幾個欄位組合成一個鍵(索引定義指定欄位以何種順序連線)。這就像老式的紙質電話簿,它提供從(*姓氏*、*名字*)到電話號碼的索引。由於排序順序,索引可用於查詢具有特定姓氏的所有人,或具有特定 *姓氏-名字* 組合的所有人。但是,如果你想查詢具有特定名字的所有人,索引是無用的。 -另一方面,*多維索引* 允許你一次查詢多個列。這在地理空間資料中特別重要。例如,餐廳搜尋網站可能有一個包含每家餐廳緯度和經度的資料庫。當用戶在地圖上檢視餐廳時,網站需要搜尋使用者當前檢視的矩形地圖區域內的所有餐廳。這需要像下面這樣的二維範圍查詢: +另一方面,*多維索引* 允許你一次查詢多個列。在地理空間資料中這尤其重要。例如,餐廳搜尋網站可能有一個包含每個餐廳的緯度和經度的資料庫。當用戶在地圖上檢視餐廳時,網站需要搜尋使用者當前檢視的矩形地圖區域內的所有餐廳。這需要像以下這樣的二維範圍查詢: ```sql SELECT * FROM restaurants WHERE latitude > 51.4946 AND latitude < 51.5079 AND longitude > -0.1162 AND longitude < -0.1004; ``` -緯度和經度列上的聯合索引無法有效地回答這種查詢:它可以給你緯度範圍內的所有餐廳(但在任何經度),或經度範圍內的所有餐廳(但在南北極之間的任何地方),但不能同時滿足兩者。 +緯度和經度列上的聯合索引無法有效地回答這種查詢:它可以為你提供緯度範圍內的所有餐廳(但在任何經度),或經度範圍內的所有餐廳(但在北極和南極之間的任何地方),但不能同時提供兩者。 -一種選擇是使用空間填充曲線將二維位置轉換為單個數字,然後使用常規 B 樹索引 [^83]。更常見的是,使用專門的空間索引,如 R 樹或 Bkd 樹 [^84];它們劃分空間,使附近的資料點往往分組在同一子樹中。例如,PostGIS 使用 PostgreSQL 的通用搜索樹索引設施將地理空間索引實現為 R 樹 [^85]。也可以使用規則間隔的三角形、正方形或六邊形網格 [^86]。 +一種選擇是使用空間填充曲線將二維位置轉換為單個數字,然後使用常規 B 樹索引 [^83]。更常見的是,使用專門的空間索引,如 R 樹或 Bkd 樹 [^84];它們劃分空間,使附近的資料點傾向於分組在同一子樹中。例如,PostGIS 使用 PostgreSQL 的通用搜索樹索引設施將地理空間索引實現為 R 樹 [^85]。也可以使用規則間隔的三角形、正方形或六邊形網格 [^86]。 -多維索引不僅僅用於地理位置。例如,在電子商務網站上,你可以在(*紅*、*綠*、*藍*)維度上使用三維索引來搜尋特定顏色範圍內的產品,或者在天氣觀測資料庫中,你可以在(*日期*、*溫度*)上有一個二維索引,以便有效地搜尋 2013 年期間溫度在 25 到 30℃ 之間的所有觀測。使用一維索引,你必須掃描 2013 年的所有記錄(無論溫度如何),然後按溫度過濾它們,或者反之亦然。二維索引可以同時按時間戳和溫度縮小範圍 [^87]。 +多維索引不僅用於地理位置。例如,在電子商務網站上,你可以在維度(*紅色*、*綠色*、*藍色*)上使用三維索引來搜尋某個顏色範圍內的產品,或者在天氣觀測資料庫中,你可以在(*日期*、*溫度*)上有一個二維索引,以便有效地搜尋 2013 年期間溫度在 25 到 30°C 之間的所有觀測。使用一維索引,你必須掃描 2013 年的所有記錄(不管溫度),然後按溫度過濾它們,反之亦然。二維索引可以同時按時間戳和溫度縮小範圍 [^87]。 ### 全文檢索 {#sec_storage_full_text} -全文搜尋允許你透過可能出現在文字中任何位置的關鍵字搜尋文字文件集合(網頁、產品描述等)[^88]。資訊檢索是一個大的專業主題,通常涉及特定於語言的處理:例如,幾種亞洲語言在單詞之間沒有空格或標點符號,因此將文字分割成單詞需要一個指示哪些字元序列構成單詞的模型。全文搜尋還經常涉及匹配相似但不相同的單詞(如拼寫錯誤或單詞的不同語法形式)和同義詞。這些問題超出了本書的範圍。 +全文檢索允許你透過可能出現在文字中任何位置的關鍵字搜尋文字文件集合(網頁、產品描述等)[^88]。資訊檢索是一個大的專業主題,通常涉及特定於語言的處理:例如,幾種亞洲語言在單詞之間沒有空格或標點符號,因此將文字分割成單詞需要一個指示哪些字元序列構成單詞的模型。全文檢索還經常涉及匹配相似但不相同的單詞(例如拼寫錯誤或單詞的不同語法形式)和同義詞。這些問題超出了本書的範圍。 -然而,在其核心,你可以將全文搜尋視為另一種多維查詢:在這種情況下,可能出現在文字中的每個單詞(*詞項*)是一個維度。包含詞項 *x* 的文件在維度 *x* 中的值為 1,不包含 *x* 的文件的值為 0。搜尋提到"紅蘋果"的文件意味著查詢在 *紅* 維度中查詢 1,同時在 *蘋果* 維度中查詢 1。因此,維度數可能非常大。 +然而,在其核心,你可以將全文檢索視為另一種多維查詢:在這種情況下,可能出現在文字中的每個單詞(*詞項*)是一個維度。包含詞項 *x* 的文件在維度 *x* 中的值為 1,不包含 *x* 的文件的值為 0。搜尋提到"紅蘋果"的文件意味著查詢在 *紅* 維度中查詢 1,同時在 *蘋果* 維度中查詢 1。維度數量可能因此非常大。 -許多搜尋引擎用來回答此類查詢的資料結構稱為 *倒排索引*。這是一個鍵值結構,其中鍵是詞項,值是包含該詞項的所有文件的 ID 列表(*釋出列表*)。如果文件 ID 是連續數字,釋出列表也可以表示為稀疏點陣圖,如 [圖 4-8](/tw/ch4#fig_bitmap_index) 所示:詞項 *x* 的點陣圖中的第 *n* 位是 1,如果 ID 為 *n* 的文件包含詞項 *x* [^89]。 +許多搜尋引擎用來回答此類查詢的資料結構稱為 *倒排索引*。這是一個鍵值結構,其中鍵是詞項,值是包含該詞項的所有文件的 ID 列表(*倒排列表*)。如果文件 ID 是順序數字,倒排列表也可以表示為稀疏點陣圖,如 [圖 4-8](#fig_bitmap_index):詞項 *x* 的點陣圖中的第 *n* 位是 1,如果 ID 為 *n* 的文件包含詞項 *x* [^89]。 -現在,查詢包含詞項 *x* 和 *y* 的所有文件類似於搜尋匹配兩個條件的行的向量化資料倉庫查詢([圖 4-9](/tw/ch4#fig_bitmap_and)):載入詞項 *x* 和 *y* 的兩個點陣圖並計算它們的按位 AND。即使點陣圖是遊程編碼的,這也可以非常高效地完成。 +查詢包含詞項 *x* 和 *y* 的所有文件現在類似於搜尋匹配兩個條件的行的向量化資料倉庫查詢([圖 4-9](#fig_bitmap_and)):載入詞項 *x* 和 *y* 的兩個點陣圖並計算它們的按位 AND。即使點陣圖是遊程編碼的,這也可以非常高效地完成。 -例如,Elasticsearch 和 Solr 使用的全文索引引擎 Lucene 就是這樣工作的 [^90]。它將詞項到釋出列表的對映儲存在類似 SSTable 的排序檔案中,這些檔案使用我們在本章前面看到的相同日誌結構方法在後臺合併 [^91]。PostgreSQL 的 GIN 索引型別也使用釋出列表來支援全文搜尋和 JSON 文件內的索引 [^92] [^93]。 +例如,Elasticsearch 和 Solr 使用的全文索引引擎 Lucene 就是這樣工作的 [^90]。它將詞項到倒排列表的對映儲存在類似 SSTable 的排序檔案中,這些檔案使用我們在本章前面看到的相同日誌結構方法在後臺合併 [^91]。PostgreSQL 的 GIN 索引型別也使用倒排列表來支援全文檢索和 JSON 文件內的索引 [^92] [^93]。 -不是將文字分解為單詞,另一種方法是查詢長度為 *n* 的所有子字串,稱為 *n*-gram。例如,字串 `"hello"` 的三元組(*n* = 3)是 `"hel"`、`"ell"` 和 `"llo"`。如果我們為所有三元組構建倒排索引,我們可以搜尋至少三個字元長的任意子字串的文件。三元組索引甚至允許在搜尋查詢中使用正則表示式;缺點是它們相當大 [^94]。 +除了將文字分解為單詞,另一種選擇是查詢長度為 *n* 的所有子字串,稱為 *n* 元語法。例如,字串 `"hello"` 的三元語法(*n* = 3)是 `"hel"`、`"ell"` 和 `"llo"`。如果我們為所有三元語法構建倒排索引,我們可以搜尋至少三個字元長的任意子字串的文件。三元語法索引甚至允許在搜尋查詢中使用正則表示式;缺點是它們相當大 [^94]。 -為了處理文件或查詢中的拼寫錯誤,Lucene 能夠在一定編輯距離內搜尋單詞的文字(編輯距離為 1 意味著已新增、刪除或替換了一個字母)[^95]。它透過將詞項集儲存為鍵上字元的有限狀態自動機(類似於 *trie* [^96])來實現這一點,並將其轉換為 *Levenshtein 自動機*,該自動機支援在給定編輯距離內高效搜尋單詞 [^97]。 +為了處理文件或查詢中的拼寫錯誤,Lucene 能夠在一定編輯距離內搜尋文字中的單詞(編輯距離為 1 意味著已新增、刪除或替換了一個字母)[^95]。它透過將詞項集儲存為字元上的有限狀態自動機(類似於 *字典樹* [^96])並將其轉換為 *萊文斯坦自動機* 來實現,該自動機支援在給定編輯距離內高效搜尋單詞 [^97]。 ### 向量嵌入 {#id92} -語義搜尋超越了同義詞和拼寫錯誤,試圖理解文件概念和使用者意圖。例如,如果你的幫助頁面包含標題為"取消訂閱"的頁面,使用者在搜尋"如何關閉我的賬戶"或"終止合同"時仍應能夠找到該頁面,即使它們使用完全不同的詞,但在含義上很接近。 +語義搜尋超越了同義詞和拼寫錯誤,試圖理解文件概念和使用者意圖。例如,如果你的幫助頁面包含標題為"取消訂閱"的頁面,使用者在搜尋"如何關閉我的帳戶"或"終止合同"時仍應能夠找到該頁面,即使它們使用完全不同的單詞,但在含義上很接近。 -為了理解文件的語義——它的含義——語義搜尋索引使用嵌入模型將文件轉換為浮點值的向量,稱為 *向量嵌入*。向量表示多維空間中的一個點,每個浮點值表示文件沿一個維度軸的位置。當嵌入的輸入文件在語義上相似時,嵌入模型生成的向量嵌入彼此接近(在這個多維空間中)。 +為了理解文件的語義 —— 它的含義 —— 語義搜尋索引使用嵌入模型將文件轉換為浮點值向量,稱為 *向量嵌入*。向量表示多維空間中的一個點,每個浮點值表示文件沿著一個維度軸的位置。嵌入模型生成的向量嵌入在(這個多維空間中)彼此接近,當嵌入的輸入文件在語義上相似時。 -------- > [!NOTE] -> 我們在 ["查詢執行:編譯和向量化"](/tw/ch4#sec_storage_vectorized) 中看到了術語 *向量化處理*。語義搜尋中的向量具有不同的含義。在向量化處理中,向量指的是可以使用專門最佳化的程式碼處理的一批位。在嵌入模型中,向量是表示多維空間中位置的浮點數列表。 +> 我們在 ["查詢執行:編譯與向量化"](#sec_storage_vectorized) 中看到了術語 *向量化處理*。語義搜尋中的向量有不同的含義。在向量化處理中,向量指的是可以用特別最佳化的程式碼處理的一批位。在嵌入模型中,向量是表示多維空間中位置的浮點數列表。 -------- 例如,關於農業的維基百科頁面的三維向量嵌入可能是 `[0.1, 0.22, 0.11]`。關於蔬菜的維基百科頁面會非常接近,可能嵌入為 `[0.13, 0.19, 0.24]`。關於星型模式的頁面可能有 `[0.82, 0.39, -0.74]` 的嵌入,相對較遠。我們可以透過觀察看出前兩個向量比第三個更接近。 -嵌入模型使用更大的向量(通常超過 1,000 個數字),但原理是相同的。我們不試圖理解各個數字的含義;它們只是嵌入模型指向抽象多維空間中位置的方式。搜尋引擎使用距離函式(如餘弦相似度或歐幾里得距離)來測量向量之間的距離。餘弦相似度測量兩個向量角度的餘弦以確定它們有多接近,而歐幾里得距離測量空間中兩點之間的直線距離。 +嵌入模型使用更大的向量(通常超過 1,000 個數字),但原理是相同的。我們不試圖理解各個數字的含義;它們只是嵌入模型指向抽象多維空間中位置的一種方式。搜尋引擎使用距離函式(如餘弦相似度或歐幾里得距離)來測量向量之間的距離。餘弦相似度測量兩個向量角度的餘弦以確定它們的接近程度,而歐幾里得距離測量空間中兩點之間的直線距離。 -許多早期的嵌入模型,如 Word2Vec [^98]、BERT [^99] 和 GPT [^100],處理文字資料。此類模型通常實現為神經網路。研究人員繼續為影片、音訊和影像建立嵌入模型。最近,模型架構已變得 *多模態*:單個模型可以為多種模態(如文字和影像)生成向量嵌入。 +許多早期的嵌入模型,如 Word2Vec [^98]、BERT [^99] 和 GPT [^100] 都處理文字資料。這些模型通常實現為神經網路。研究人員繼續為影片、音訊和影像建立嵌入模型。最近,模型架構已經變成 *多模態* 的:單個模型可以為多種模態(如文字和影像)生成向量嵌入。 -當用戶輸入查詢時,語義搜尋引擎使用嵌入模型生成向量嵌入。使用者的查詢和相關上下文(如使用者的位置)被輸入嵌入模型。在嵌入模型生成查詢的向量嵌入後,搜尋引擎必須使用向量索引找到具有相似向量嵌入的文件。 +語義搜尋引擎在使用者輸入查詢時使用嵌入模型生成向量嵌入。使用者的查詢和相關上下文(例如使用者的位置)被輸入到嵌入模型中。嵌入模型生成查詢的向量嵌入後,搜尋引擎必須使用向量索引找到具有相似向量嵌入的文件。 -向量索引儲存文件集合的向量嵌入。要查詢索引,你傳入查詢的向量嵌入,索引返回其向量最接近查詢向量的文件。由於我們之前看到的 R 樹對於具有許多維度的向量效果不佳,因此使用專門的向量索引,例如: +向量索引儲存文件集合的向量嵌入。要查詢索引,你傳入查詢的向量嵌入,索引返回其向量最接近查詢向量的文件。由於我們之前看到的 R 樹不適用於多維向量,因此使用專門的向量索引,例如: -平面索引 -: 向量按原樣儲存在索引中。查詢必須讀取每個向量並測量其與查詢向量的距離。平面索引是準確的,但測量查詢和每個向量之間的距離很慢。 +平面索引(Flat indexes) +: 向量按原樣儲存在索引中。查詢必須讀取每個向量並測量其與查詢向量的距離。平面索引是準確的,但測量查詢與每個向量之間的距離很慢。 倒排檔案(IVF)索引 -: 向量空間被聚類成向量的分割槽(稱為 *質心*),以減少必須比較的向量數量。IVF 索引比平面索引更快,但只能給出近似結果:查詢和文件可能落入不同的分割槽,即使它們彼此接近。IVF 索引上的查詢首先定義 *探針*,這只是要檢查的分割槽數。使用更多探針的查詢將更準確,但會更慢,因為必須比較更多向量。 +: 向量空間被聚類為向量的分割槽(稱為 *質心*),以減少必須比較的向量數量。IVF 索引比平面索引更快,但只能給出近似結果:即使查詢和文件彼此接近,它們也可能落入不同的分割槽。對 IVF 索引的查詢首先定義 *探針*,這只是要檢查的分割槽數。使用更多探針的查詢將更準確,但會更慢,因為必須比較更多向量。 分層可導航小世界(HNSW) -: HNSW 索引維護向量空間的多個層,如 [圖 4-11](/tw/ch4#fig_vector_hnsw) 所示。每一層都表示為一個圖,其中節點表示向量,邊表示與附近向量的接近度。查詢首先在最頂層定位最近的向量,該層具有少量節點。然後查詢移動到下面層中的同一節點,並跟隨該層中的邊,該層連線更密集,尋找更接近查詢向量的向量。該過程一直持續到到達最後一層。與 IVF 索引一樣,HNSW 索引是近似的。 +: HNSW 索引維護向量空間的多個層,如 [圖 4-11](#fig_vector_hnsw) 所示。每一層都表示為一個圖,其中節點表示向量,邊表示與附近向量的接近度。查詢首先在最頂層定位最近的向量,該層具有少量節點。然後查詢移動到下面一層的同一節點,並跟隨該層中的邊,該層連線更密集,尋找更接近查詢向量的向量。該過程繼續直到到達最後一層。與 IVF 索引一樣,HNSW 索引是近似的。 {{< figure src="/fig/ddia_0411.png" id="fig_vector_hnsw" caption="圖 4-11. 在 HNSW 索引中搜索最接近給定查詢向量的資料庫條目。" class="w-full my-4" >}} -許多流行的向量資料庫實現 IVF 和 HNSW 索引。Facebook 的 Faiss 庫有每種的許多變體 [^101],PostgreSQL 的 pgvector 也支援兩者 [^102]。IVF 和 HNSW 演算法的完整細節超出了本書的範圍,但它們的論文是很好的資源 [^103] [^104]。 - +許多流行的向量資料庫實現了 IVF 和 HNSW 索引。Facebook 的 Faiss 庫有每種的許多變體 [^101],PostgreSQL 的 pgvector 也支援兩者 [^102]。IVF 和 HNSW 演算法的完整細節超出了本書的範圍,但它們的論文是極好的資源 [^103] [^104]。 ## 總結 {#summary} -在本章中,我們試圖深入瞭解資料庫如何執行儲存和檢索。當你將資料儲存在資料庫中時會發生什麼,以及當你稍後再次查詢資料時資料庫會做什麼? +在本章中,我們試圖深入瞭解資料庫如何執行儲存和檢索。當你在資料庫中儲存資料時會發生什麼,當你稍後再次查詢資料時資料庫會做什麼? -["分析與作業系統"](/tw/ch1#sec_introduction_analytics) 介紹了事務處理(OLTP)和分析(OLAP)之間的區別。在本章中,我們看到為 OLTP 最佳化的儲存引擎與為分析最佳化的儲存引擎看起來非常不同: +["分析型與事務型系統"](/tw/ch1#sec_introduction_analytics) 介紹了事務處理(OLTP)和分析(OLAP)之間的區別。在本章中,我們看到為 OLTP 最佳化的儲存引擎與為分析最佳化的儲存引擎看起來非常不同: * OLTP 系統針對大量請求進行了最佳化,每個請求讀取和寫入少量記錄,並且需要快速響應。記錄通常透過主鍵或二級索引訪問,這些索引通常是從鍵到記錄的有序對映,也支援範圍查詢。 -* 資料倉庫和類似的分析系統針對掃描大量記錄的複雜讀取查詢進行了最佳化。它們通常使用列式儲存佈局和壓縮,以最大限度地減少此類查詢需要從磁碟讀取的資料量,並使用查詢的即時編譯或向量化來最大限度地減少處理資料所花費的 CPU 時間。 +* 資料倉庫和類似的分析系統針對掃描大量記錄的複雜讀取查詢進行了最佳化。它們通常使用帶有壓縮的列式儲存佈局,以最小化此類查詢需要從磁碟讀取的資料量,並使用查詢的即時編譯或向量化來最小化處理資料所花費的 CPU 時間。 -在 OLTP 方面,我們看到了兩種主要思想流派的儲存引擎: +在 OLTP 方面,我們看到了兩個主要思想流派的儲存引擎: -* 日誌結構方法,只允許追加到檔案和刪除過時的檔案,但從不更新已寫入的檔案。SSTable、LSM 樹、RocksDB、Cassandra、HBase、Scylla、Lucene 等屬於這一組。一般來說,日誌結構儲存引擎往往提供高寫入吞吐量。 -* 就地更新方法,將磁碟視為一組可以覆蓋的固定大小頁面。B 樹是這種理念的最大例子,用於所有主要的關係 OLTP 資料庫以及許多非關係資料庫。作為經驗法則,B 樹往往更適合讀取,提供比日誌結構儲存更高的讀取吞吐量和更低的響應時間。 +* 日誌結構方法,只允許追加到檔案和刪除過時檔案,但從不更新已寫入的檔案。SSTable、LSM 樹、RocksDB、Cassandra、HBase、Scylla、Lucene 等屬於這一組。一般來說,日誌結構儲存引擎往往提供高寫入吞吐量。 +* 就地更新方法,將磁碟視為一組可以覆蓋的固定大小頁。B 樹是這種理念的最大例子,用於所有主要的關係型 OLTP 資料庫以及許多非關係型資料庫。作為經驗法則,B 樹往往更適合讀取,提供比日誌結構儲存更高的讀取吞吐量和更低的響應時間。 -然後,我們研究了可以同時搜尋多個條件的索引:多維索引(如 R 樹)可以同時按緯度和經度搜索地圖上的點,全文搜尋索引可以搜尋同一文字中出現的多個關鍵字。最後,向量資料庫用於文字文件和其他媒體的語義搜尋;它們使用具有更多維度的向量,並透過比較向量相似性來查詢相似文件。 +然後我們查看了可以同時搜尋多個條件的索引:多維索引(如 R 樹)可以同時按緯度和經度搜索地圖上的點,全文檢索索引可以搜尋出現在同一文字中的多個關鍵字。最後,向量資料庫用於文字文件和其他媒體的語義搜尋;它們使用具有大量維度的向量,並透過比較向量相似性來查詢相似文件。 -作為應用程式開發人員,如果你掌握了有關儲存引擎內部的這些知識,你就能更好地瞭解哪種工具最適合你的特定應用程式。如果你需要調整資料庫的調優引數,這種理解使你能夠想象更高或更低的值可能產生什麼效果。 +作為應用程式開發人員,如果你掌握了有關儲存引擎內部的這些知識,你就能更好地知道哪種工具最適合你的特定應用程式。如果你需要調整資料庫的調優引數,這種理解使你能夠想象更高或更低的值可能產生什麼影響。 -儘管本章不能讓你成為調整任何特定儲存引擎的專家,但希望它為你提供了足夠的詞彙和想法,讓你能夠理解所選資料庫的文件。 +儘管本章不能讓你成為調優任何特定儲存引擎的專家,但它希望為你提供了足夠的詞彙和想法,使你能夠理解你選擇的資料庫的文件。 diff --git a/content/tw/ch5.md b/content/tw/ch5.md index dec5f59..f511870 100644 --- a/content/tw/ch5.md +++ b/content/tw/ch5.md @@ -34,7 +34,7 @@ breadcrumbs: false 向前相容性的另一個挑戰如 [圖 5-1](/tw/ch5#fig_encoding_preserve_field) 所示。假設你向記錄模式添加了一個欄位,新程式碼建立了包含該新欄位的記錄並將其儲存在資料庫中。隨後,舊版本的程式碼(尚不知道新欄位)讀取記錄,更新它,然後寫回。在這種情況下,理想的行為通常是舊程式碼保持新欄位不變,即使它無法解釋。但是,如果記錄被解碼為不顯式保留未知欄位的模型物件,資料可能會丟失,如 [圖 5-1](/tw/ch5#fig_encoding_preserve_field) 所示。 -{{< figure src="/fig/ddia_0501.png" id="fig_encoding_preserve_field" caption="當舊版本的應用程式更新之前由新版本應用程式寫入的資料時,如果不小心,資料可能會丟失。" class="w-full my-4" >}} +{{< figure src="/fig/ddia_0501.png" id="fig_encoding_preserve_field" caption="圖 5-1. 當舊版本的應用程式更新之前由新版本應用程式寫入的資料時,如果不小心,資料可能會丟失。" class="w-full my-4" >}} 在本章中,我們將研究幾種編碼資料的格式,包括 JSON、XML、Protocol Buffers 和 Avro。特別是,我們將研究它們如何處理模式變化,以及它們如何支援新舊資料和程式碼需要共存的系統。然後我們將討論這些格式如何用於資料儲存和通訊:在資料庫、Web 服務、REST API、遠端過程呼叫(RPC)、工作流引擎以及事件驅動系統(如 actor 和訊息佇列)中。 diff --git a/content/tw/ch7.md b/content/tw/ch7.md index 2c02342..f8ba11f 100644 --- a/content/tw/ch7.md +++ b/content/tw/ch7.md @@ -297,7 +297,7 @@ Cassandra、ScyllaDB 和 Riak 採用不同的方法:它們在節點之間使 但是,如果你想要所有結果並且事先不知道它們的分割槽鍵,你需要將查詢傳送到所有分片,並組合你收到的結果,因為匹配的記錄可能分散在所有分片中。在 [圖 7-9](/tw/ch7#fig_sharding_local_secondary) 中,紅色汽車出現在分片 0 和分片 1 中。 -這種查詢分片資料庫的方法有時稱為 *分散/聚集*,它可能使二級索引上的讀取查詢相當昂貴。即使並行查詢分片,分散/聚集也容易導致尾部延遲放大(見 ["響應時間指標的使用"](/tw/ch2#sec_introduction_slo_sla))。它還限制了應用程式的可擴充套件性:新增更多分片讓你儲存更多資料,但如果每個分片無論如何都必須處理每個查詢,它不會增加你的查詢吞吐量。 +這種查詢分片資料庫的方法有時稱為 *分散/聚集*,它可能使二級索引上的讀取查詢相當昂貴。即使並行查詢分片,分散/聚集也容易導致尾部延遲放大(見 ["響應時間指標的使用"](/tw/ch2#sec_introduction_slo_sla))。它還限制了應用程式的可伸縮性:新增更多分片讓你儲存更多資料,但如果每個分片無論如何都必須處理每個查詢,它不會增加你的查詢吞吐量。 儘管如此,本地二級索引被廣泛使用 [^31]:例如,MongoDB、Riak、Cassandra [^32]、Elasticsearch [^33]、SolrCloud 和 VoltDB [^34] 都使用本地二級索引。 @@ -309,7 +309,7 @@ Cassandra、ScyllaDB 和 Riak 採用不同的方法:它們在節點之間使 {{< figure src="/fig/ddia_0710.png" id="fig_sharding_global_secondary" caption="圖 7-10. 全域性二級索引反映來自所有分片的資料,並且本身按索引值進行分片。" class="w-full my-4" >}} -這種索引也稱為 *基於詞項分割槽* [^30]:回憶一下 ["全文搜尋"](/tw/ch4#sec_storage_full_text),在全文搜尋中,*詞項* 是你可以搜尋的文字中的關鍵字。這裡我們將其推廣為指二級索引中你可以搜尋的任何值。 +這種索引也稱為 *基於詞項分割槽* [^30]:回憶一下 ["全文檢索"](/tw/ch4#sec_storage_full_text),在全文檢索中,*詞項* 是你可以搜尋的文字中的關鍵字。這裡我們將其推廣為指二級索引中你可以搜尋的任何值。 全域性索引使用詞項作為分割槽鍵,因此當你查詢特定詞項或值時,你可以找出需要查詢哪個分片。和以前一樣,分片可以包含連續的詞項範圍(如 [圖 7-10](/tw/ch7#fig_sharding_global_secondary)),或者你可以基於詞項的雜湊將詞項分配給分片。 diff --git a/content/tw/ch8.md b/content/tw/ch8.md index 8259050..edd6fc6 100644 --- a/content/tw/ch8.md +++ b/content/tw/ch8.md @@ -643,7 +643,7 @@ VoltDB 還使用儲存過程進行復制:它不是將事務的寫入從一個 但是,對於需要訪問多個分片的任何事務,資料庫必須協調它所涉及的所有分片之間的事務。儲存過程需要在所有分片上同步執行,以確保整個系統的可序列化。 -由於跨分片事務具有額外的協調開銷,因此它們比單分片事務慢得多。VoltDB 報告的跨分片寫入吞吐量約為每秒 1,000 次,這比其單分片吞吐量低幾個數量級,並且無法透過新增更多機器來增加[^61]。最近的研究探索了使多分片事務更具可擴充套件性的方法[^63]。 +由於跨分片事務具有額外的協調開銷,因此它們比單分片事務慢得多。VoltDB 報告的跨分片寫入吞吐量約為每秒 1,000 次,這比其單分片吞吐量低幾個數量級,並且無法透過新增更多機器來增加[^61]。最近的研究探索了使多分片事務更具可伸縮性的方法[^63]。 事務是否可以是單分片的很大程度上取決於應用程式使用的資料結構。簡單的鍵值資料通常可以很容易地分片,但具有多個二級索引的資料可能需要大量的跨分片協調(參見["分片和二級索引"](/tw/ch7#sec_sharding_secondary_indexes))。 @@ -1006,7 +1006,7 @@ XA 的最大問題可以透過以下方式解決: 因此,實現精確一次處理只需要資料庫中的事務——跨資料庫和訊息代理的原子性對於此用例不是必需的。在資料庫中記錄訊息 ID 使訊息處理*冪等*,因此可以安全地重試訊息處理而不會重複其副作用。流處理框架(如 Kafka Streams)中使用類似的方法來實現精確一次語義,我們將在[待補充連結]中看到。 -但是,資料庫內的內部分散式事務對於此類模式的可擴充套件性仍然有用:例如,它們將允許訊息 ID 儲存在一個分片上,而訊息處理更新的主資料儲存在其他分片上,並確保跨這些分片的事務提交的原子性。 +但是,資料庫內的內部分散式事務對於此類模式的可伸縮性仍然有用:例如,它們將允許訊息 ID 儲存在一個分片上,而訊息處理更新的主資料儲存在其他分片上,並確保跨這些分片的事務提交的原子性。 diff --git a/content/tw/ch9.md b/content/tw/ch9.md index 3fc7fc6..0578bba 100644 --- a/content/tw/ch9.md +++ b/content/tw/ch9.md @@ -677,7 +677,7 @@ DST 提供了超越可重放性的幾個優勢。Antithesis 等工具試圖透 事實上,使事物確定性是一個簡單但強大的想法,在分散式系統設計中一再出現。除了確定性模擬測試,我們在過去的章節中已經看到了幾種使用確定性的方法: -* 事件溯源的一個關鍵優勢(見 ["事件溯源和 CQRS"](/tw/ch3#sec_datamodels_events))是你可以確定性地重放事件日誌以重建衍生的物化檢視。 +* 事件溯源的一個關鍵優勢(見 ["事件溯源和 CQRS"](/tw/ch3#sec_datamodels_events))是你可以確定性地重放事件日誌以重建派生的物化檢視。 * 工作流引擎(見 ["持久執行和工作流"](/tw/ch5#sec_encoding_dataflow_workflows))依賴於工作流定義是確定性的,以提供持久執行語義。 * *狀態機複製*,我們將在 ["使用共享日誌"](/tw/ch10#sec_consistency_smr) 中討論,透過在每個副本上獨立執行相同的確定性事務序列來複制資料。我們已經看到了這個想法的兩個變體:基於語句的複製(見 ["複製日誌的實現"](/tw/ch6#sec_replication_implementation))和使用儲存過程的序列事務執行(見 ["儲存過程的利弊"](/tw/ch8#sec_transactions_stored_proc_tradeoffs))。 @@ -699,7 +699,7 @@ DST 提供了超越可重放性的幾個優勢。Antithesis 等工具試圖透 如果你習慣於在單臺計算機的理想數學完美環境中編寫軟體,其中相同的操作總是確定性地返回相同的結果,那麼轉向分散式系統混亂的物理現實可能會有點震驚。相反,分散式系統工程師通常會認為如果一個問題可以在單臺計算機上解決,那它就是微不足道的 [^4],而且單臺計算機現在確實可以做很多事情。如果你可以避免開啟潘多拉的盒子,只需將事情保持在單臺機器上,例如使用嵌入式儲存引擎(見 ["嵌入式儲存引擎"](/tw/ch4#sidebar_embedded)),通常值得這樣做。 -然而,正如在 ["分散式系統與單節點系統"](/tw/ch1#sec_introduction_distributed) 中討論的,可擴充套件性並不是使用分散式系統的唯一原因。容錯和低延遲(透過將資料在地理上放置在靠近使用者的位置)是同樣重要的目標,而這些事情無法透過單個節點實現。分散式系統的力量在於,原則上它們可以在服務層面永遠執行而不被中斷,因為所有故障和維護都可以在節點層面處理。(實際上,如果錯誤的配置更改被推送到所有節點,仍然會讓分散式系統崩潰。) +然而,正如在 ["分散式系統與單節點系統"](/tw/ch1#sec_introduction_distributed) 中討論的,可伸縮性並不是使用分散式系統的唯一原因。容錯和低延遲(透過將資料在地理上放置在靠近使用者的位置)是同樣重要的目標,而這些事情無法透過單個節點實現。分散式系統的力量在於,原則上它們可以在服務層面永遠執行而不被中斷,因為所有故障和維護都可以在節點層面處理。(實際上,如果錯誤的配置更改被推送到所有節點,仍然會讓分散式系統崩潰。) 在本章中,我們還探討了網路、時鐘和程序的不可靠性是否是不可避免的自然法則。我們看到它不是:可以在網路中提供硬即時響應保證和有界延遲,但這樣做非常昂貴,並導致硬體資源利用率降低。大多數非安全關鍵系統選擇便宜和不可靠而不是昂貴和可靠。 diff --git a/content/tw/glossary.md b/content/tw/glossary.md index 19fa2e2..0613fd6 100644 --- a/content/tw/glossary.md +++ b/content/tw/glossary.md @@ -103,7 +103,7 @@ breadcrumbs: false ## **全文檢索(full-text search)** - 透過任意關鍵字來搜尋文字,通常具有附加特徵,例如匹配類似的拼寫詞或同義詞。全文索引是一種支援這種查詢的次級索引。請參閱“[全文搜尋和模糊索引](/tw/ch3#全文搜尋和模糊索引)”。 + 透過任意關鍵字來搜尋文字,通常具有附加特徵,例如匹配類似的拼寫詞或同義詞。全文索引是一種支援這種查詢的次級索引。請參閱“[全文檢索與模糊索引](/tw/ch3#全文檢索與模糊索引)”。 ## **圖(graph)** @@ -233,7 +233,7 @@ breadcrumbs: false ## **記錄系統(system of record)** - 一個儲存主要權威版本資料的系統,也被稱為真相的來源。首先在這裡寫入資料變更,其他資料集可以從記錄系統衍生。請參閱[第三部分](/tw/part-iii)的介紹。 + 一個儲存主要權威版本資料的系統,也被稱為真相的來源。首先在這裡寫入資料變更,其他資料集可以從記錄系統派生。請參閱[第三部分](/tw/part-iii)的介紹。 ## **超時(timeout)** diff --git a/content/tw/part-iii.md b/content/tw/part-iii.md index 063dd02..1f1c57f 100644 --- a/content/tw/part-iii.md +++ b/content/tw/part-iii.md @@ -24,15 +24,15 @@ breadcrumbs: false 派生資料系統(Derived data systems) : **派生系統** 中的資料,通常是另一個系統中的現有資料以某種方式進行轉換或處理的結果。如果丟失派生資料,可以從原始來源重新建立。 - 典型的例子是 **快取(cache)**:如果資料在快取中,就可以由快取提供服務;如果快取不包含所需資料,則降級由底層資料庫提供。非規範化的值,索引和物化檢視亦屬此類。在推薦系統中,預測彙總資料通常衍生自使用者日誌。 + 典型的例子是 **快取(cache)**:如果資料在快取中,就可以由快取提供服務;如果快取不包含所需資料,則降級由底層資料庫提供。非規範化的值,索引和物化檢視亦屬此類。在推薦系統中,預測彙總資料通常派生自使用者日誌。 -從技術上講,派生資料是 **冗餘的(redundant)**,因為它重複了已有的資訊。但是派生資料對於獲得良好的只讀查詢效能通常是至關重要的。它通常是非規範化的。可以從單個源頭衍生出多個不同的資料集,使你能從不同的 “視角” 洞察資料。 +從技術上講,派生資料是 **冗餘的(redundant)**,因為它重複了已有的資訊。但是派生資料對於獲得良好的只讀查詢效能通常是至關重要的。它通常是非規範化的。可以從單個源頭派生出多個不同的資料集,使你能從不同的 “視角” 洞察資料。 並不是所有的系統都在其架構中明確區分 **記錄系統** 和 **派生資料系統**,但是這是一種有用的區分方式,因為它明確了系統中的資料流:系統的哪一部分具有哪些輸入和哪些輸出,以及它們如何相互依賴。 大多數資料庫,儲存引擎和查詢語言,本質上既不是記錄系統也不是派生系統。資料庫只是一個工具:如何使用它取決於你自己。**記錄系統和派生資料系統之間的區別不在於工具,而在於應用程式中的使用方式。** -透過梳理資料的衍生關係,可以清楚地理解一個令人困惑的系統架構。這將貫穿本書的這一部分。 +透過梳理資料的派生關係,可以清楚地理解一個令人困惑的系統架構。這將貫穿本書的這一部分。 ## 章節概述 diff --git a/content/tw/preface.md b/content/tw/preface.md index b1d9a4a..0876338 100644 --- a/content/tw/preface.md +++ b/content/tw/preface.md @@ -72,7 +72,7 @@ breadcrumbs: false 2. 在 [第二部分](/tw/part-ii) 中,我們從討論儲存在一臺機器上的資料轉向討論分佈在多臺機器上的資料。這對於可伸縮性通常是必需的,但帶來了各種獨特的挑戰。我們首先討論複製([第五章](/tw/ch5))、分割槽 / 分片([第六章](/tw/ch6))和事務([第七章](/tw/ch7))。然後我們將探索關於分散式系統問題的更多細節([第八章](/tw/ch8)),以及在分散式系統中實現一致性與共識意味著什麼([第九章](/tw/ch9))。 -3. 在 [第三部分](/tw/part-iii) 中,我們討論那些從其他資料集衍生出一些資料集的系統。派生資料經常出現在異構系統中:當沒有單個數據庫可以把所有事情都做的很好時,應用需要整合幾種不同的資料庫、快取、索引等。在 [第十一章](/tw/ch11) 中我們將從一種派生資料的批處理方法開始,然後在此基礎上建立在 [第十二章](/tw/ch12) 中討論的流處理。最後,在 [第十三章](/ch13) 中,我們將所有內容彙總,討論在將來構建可靠、可伸縮和可維護的應用程式的方法。 +3. 在 [第三部分](/tw/part-iii) 中,我們討論那些從其他資料集派生出一些資料集的系統。派生資料經常出現在異構系統中:當沒有單個數據庫可以把所有事情都做的很好時,應用需要整合幾種不同的資料庫、快取、索引等。在 [第十一章](/tw/ch11) 中我們將從一種派生資料的批處理方法開始,然後在此基礎上建立在 [第十二章](/tw/ch12) 中討論的流處理。最後,在 [第十三章](/ch13) 中,我們將所有內容彙總,討論在將來構建可靠、可伸縮和可維護的應用程式的方法。 ## 參考文獻與延伸閱讀 diff --git a/content/tw/toc.md b/content/tw/toc.md index 65a1585..3ec07c9 100644 --- a/content/tw/toc.md +++ b/content/tw/toc.md @@ -20,514 +20,91 @@ breadcrumbs: false ## [1. 資料系統架構中的權衡](/tw/ch1) - [分析型與事務型系統](/tw/ch1#sec_introduction_analytics) - - [事務處理與分析的特徵](/tw/ch1#sec_introduction_oltp) - - [資料倉庫](/tw/ch1#sec_introduction_dwh) - - [從資料倉庫到資料湖](/tw/ch1#from-data-warehouse-to-data-lake) - - [超越資料湖](/tw/ch1#beyond-the-data-lake) - - [權威資料來源與派生資料](/tw/ch1#sec_introduction_derived) - [雲服務與自託管](/tw/ch1#sec_introduction_cloud) - - [雲服務的利弊](/tw/ch1#sec_introduction_cloud_tradeoffs) - - [雲原生系統架構](/tw/ch1#sec_introduction_cloud_native) - - [雲服務的分層](/tw/ch1#layering-of-cloud-services) - - [儲存與計算的分離](/tw/ch1#sec_introduction_storage_compute) - - [雲時代的運維](/tw/ch1#sec_introduction_operations) - [分散式與單節點系統](/tw/ch1#sec_introduction_distributed) - - [分散式系統的問題](/tw/ch1#sec_introduction_dist_sys_problems) - - [微服務與 Serverless](/tw/ch1#sec_introduction_microservices) - - [雲計算與超級計算](/tw/ch1#id17) - [資料系統、法律與社會](/tw/ch1#sec_introduction_compliance) - [總結](/tw/ch1#summary) - - [參考](/tw/ch1#參考) ## [2. 定義非功能性需求](/tw/ch2) - [案例研究:社交網路首頁時間線](/tw/ch2#sec_introduction_twitter) - - [表示使用者、帖子與關注關係](/tw/ch2#id20) - - [時間線的物化與更新](/tw/ch2#sec_introduction_materializing) - [描述效能](/tw/ch2#sec_introduction_percentiles) - - [延遲與響應時間](/tw/ch2#id23) - - [平均值、中位數與百分位數](/tw/ch2#id24) - - [響應時間指標的應用](/tw/ch2#sec_introduction_slo_sla) - [可靠性與容錯](/tw/ch2#sec_introduction_reliability) - - [容錯](/tw/ch2#id27) - - [硬體與軟體故障](/tw/ch2#sec_introduction_hardware_faults) - - [透過冗餘容忍硬體故障](/tw/ch2#tolerating-hardware-faults-through-redundancy) - - [軟體故障](/tw/ch2#software-faults) - - [人類與可靠性](/tw/ch2#id31) - [可伸縮性](/tw/ch2#sec_introduction_scalability) - - [描述負載](/tw/ch2#id33) - - [共享記憶體、共享磁碟與無共享架構](/tw/ch2#sec_introduction_shared_nothing) - - [可伸縮性原則](/tw/ch2#id35) - [可運維性](/tw/ch2#sec_introduction_maintainability) - - [可運維性:讓運維更輕鬆](/tw/ch2#id37) - - [簡單性:管理複雜度](/tw/ch2#id38) - - [可演化性:讓變化更容易](/tw/ch2#sec_introduction_evolvability) - [總結](/tw/ch2#summary) - - [參考](/tw/ch2#參考) ## [3. 資料模型與查詢語言](/tw/ch3) - [關係模型與文件模型](/tw/ch3#sec_datamodels_history) - - [物件關係不匹配](/tw/ch3#sec_datamodels_document) - - [物件關係對映(ORM)](/tw/ch3#object-relational-mapping-orm) - - [用於一對多關係的文件資料模型](/tw/ch3#the-document-data-model-for-one-to-many-relationships) - - [規範化、反規範化與連線](/tw/ch3#sec_datamodels_normalization) - - [規範化的權衡](/tw/ch3#trade-offs-of-normalization) - - [社交網路案例研究中的反規範化](/tw/ch3#denormalization-in-the-social-networking-case-study) - - [多對一與多對多關係](/tw/ch3#sec_datamodels_many_to_many) - - [星型與雪花型:分析模式](/tw/ch3#sec_datamodels_analytics) - - [何時使用哪種模型](/tw/ch3#sec_datamodels_document_summary) - - [文件模型中的模式靈活性](/tw/ch3#sec_datamodels_schema_flexibility) - - [讀寫的資料區域性](/tw/ch3#sec_datamodels_document_locality) - - [文件的查詢語言](/tw/ch3#query-languages-for-documents) - - [文件和關係資料庫的融合](/tw/ch3#convergence-of-document-and-relational-databases) - [圖資料模型](/tw/ch3#sec_datamodels_graph) - - [屬性圖](/tw/ch3#id56) - - [Cypher 查詢語言](/tw/ch3#id57) - - [SQL 中的圖查詢](/tw/ch3#id58) - - [三元組儲存與 SPARQL](/tw/ch3#id59) - - [RDF 資料模型](/tw/ch3#the-rdf-data-model) - - [SPARQL 查詢語言](/tw/ch3#the-sparql-query-language) - - [Datalog:遞迴關係查詢](/tw/ch3#id62) - - [GraphQL](/tw/ch3#id63) - [事件溯源與 CQRS](/tw/ch3#sec_datamodels_events) - [資料框、矩陣與陣列](/tw/ch3#sec_datamodels_dataframes) - [總結](/tw/ch3#summary) - - [參考](/tw/ch3#參考) ## [4. 儲存與檢索](/tw/ch4) - [OLTP 系統的儲存與索引](/tw/ch4#sec_storage_oltp) - - [日誌結構儲存](/tw/ch4#sec_storage_log_structured) - - [SSTable 檔案格式](/tw/ch4#the-sstable-file-format) - - [構建和合並 SSTable](/tw/ch4#constructing-and-merging-sstables) - - [布隆過濾器](/tw/ch4#bloom-filters) - - [壓實策略](/tw/ch4#sec_storage_lsm_compaction) - - [B 樹](/tw/ch4#sec_storage_b_trees) - - [使 B 樹可靠](/tw/ch4#sec_storage_btree_wal) - - [B 樹變體](/tw/ch4#b-tree-variants) - - [比較 B 樹與 LSM 樹](/tw/ch4#sec_storage_btree_lsm_comparison) - - [讀取效能](/tw/ch4#read-performance) - - [順序與隨機寫入](/tw/ch4#sidebar_sequential) - - [寫放大](/tw/ch4#write-amplification) - - [磁碟空間使用](/tw/ch4#disk-space-usage) - - [多列索引與二級索引](/tw/ch4#sec_storage_index_multicolumn) - - [在索引中儲存值](/tw/ch4#sec_storage_index_heap) - - [全記憶體儲存](/tw/ch4#sec_storage_inmemory) - [分析型資料儲存](/tw/ch4#sec_storage_analytics) - - [雲資料倉庫](/tw/ch4#sec_cloud_data_warehouses) - - [列式儲存](/tw/ch4#sec_storage_column) - - [列壓縮](/tw/ch4#sec_storage_column_compression) - - [列儲存中的排序順序](/tw/ch4#sort-order-in-column-storage) - - [寫入列式儲存](/tw/ch4#writing-to-column-oriented-storage) - - [查詢執行:編譯與向量化](/tw/ch4#sec_storage_vectorized) - - [物化檢視與多維資料集](/tw/ch4#sec_storage_materialized_views) - [多維索引與全文索引](/tw/ch4#sec_storage_multidimensional) - - [全文檢索](/tw/ch4#sec_storage_full_text) - - [向量嵌入](/tw/ch4#id92) - [總結](/tw/ch4#summary) - - [參考](/tw/ch4#參考) ## [5. 編碼與演化](/tw/ch5) - [編碼資料的格式](/tw/ch5#sec_encoding_formats) - - [特定語言的格式](/tw/ch5#id96) - - [JSON、XML 及其二進位制變體](/tw/ch5#sec_encoding_json) - - [JSON 模式](/tw/ch5#json-schema) - - [二進位制編碼](/tw/ch5#binary-encoding) - - [Protocol Buffers](/tw/ch5#sec_encoding_protobuf) - - [欄位標籤與模式演化](/tw/ch5#field-tags-and-schema-evolution) - - [Avro](/tw/ch5#sec_encoding_avro) - - [寫入者模式與讀取者模式](/tw/ch5#the-writers-schema-and-the-readers-schema) - - [模式演化規則](/tw/ch5#schema-evolution-rules) - - [但什麼是寫入者模式?](/tw/ch5#but-what-is-the-writers-schema) - - [動態生成的模式](/tw/ch5#dynamically-generated-schemas) - - [模式的優點](/tw/ch5#sec_encoding_schemas) - [資料流的模式](/tw/ch5#sec_encoding_dataflow) - - [流經資料庫的資料流](/tw/ch5#sec_encoding_dataflow_db) - - [不同時間寫入的不同值](/tw/ch5#different-values-written-at-different-times) - - [歸檔儲存](/tw/ch5#archival-storage) - - [流經服務的資料流:REST 與 RPC](/tw/ch5#sec_encoding_dataflow_rpc) - - [Web 服務](/tw/ch5#sec_web_services) - - [遠端過程呼叫(RPC)的問題](/tw/ch5#sec_problems_with_rpc) - - [負載均衡器、服務發現和服務網格](/tw/ch5#sec_encoding_service_discovery) - - [RPC 的資料編碼與演化](/tw/ch5#data-encoding-and-evolution-for-rpc) - - [持久化執行與工作流](/tw/ch5#sec_encoding_dataflow_workflows) - - [持久化執行](/tw/ch5#durable-execution) - - [事件驅動的架構](/tw/ch5#sec_encoding_dataflow_msg) - - [訊息代理](/tw/ch5#message-brokers) - - [分散式 actor 框架](/tw/ch5#distributed-actor-frameworks) - [總結](/tw/ch5#summary) - - [參考](/tw/ch5#參考) ## [6. 複製](/tw/ch6) - [單主複製](/tw/ch6#sec_replication_leader) - - [同步複製與非同步複製](/tw/ch6#sec_replication_sync_async) - - [設定新的副本](/tw/ch6#sec_replication_new_replica) - - [處理節點故障](/tw/ch6#sec_replication_failover) - - [從節點故障:追趕恢復](/tw/ch6#follower-failure-catch-up-recovery) - - [領導者故障:故障轉移](/tw/ch6#leader-failure-failover) - - [複製日誌的實現](/tw/ch6#sec_replication_implementation) - - [基於語句的複製](/tw/ch6#statement-based-replication) - - [預寫日誌(WAL)傳輸](/tw/ch6#write-ahead-log-wal-shipping) - - [邏輯(基於行)日誌複製](/tw/ch6#logical-row-based-log-replication) - [複製延遲的問題](/tw/ch6#sec_replication_lag) - - [讀己之寫](/tw/ch6#sec_replication_ryw) - - [單調讀](/tw/ch6#sec_replication_monotonic_reads) - - [一致字首讀](/tw/ch6#sec_replication_consistent_prefix) - - [複製延遲的解決方案](/tw/ch6#id131) - [多主複製](/tw/ch6#sec_replication_multi_leader) - - [跨地域執行](/tw/ch6#sec_replication_multi_dc) - - [多主複製拓撲](/tw/ch6#sec_replication_topologies) - - [不同拓撲的問題](/tw/ch6#problems-with-different-topologies) - - [同步引擎與本地優先軟體](/tw/ch6#sec_replication_offline_clients) - - [即時協作、離線優先和本地優先應用](/tw/ch6#real-time-collaboration-offline-first-and-local-first-apps) - - [同步引擎的利弊](/tw/ch6#pros-and-cons-of-sync-engines) - - [處理寫入衝突](/tw/ch6#sec_replication_write_conflicts) - - [衝突避免](/tw/ch6#conflict-avoidance) - - [最後寫入者勝(丟棄併發寫入)](/tw/ch6#sec_replication_lww) - - [手動衝突解決](/tw/ch6#manual-conflict-resolution) - - [自動衝突解決](/tw/ch6#automatic-conflict-resolution) - - [CRDT 與操作變換](/tw/ch6#sec_replication_crdts) - - [什麼是衝突?](/tw/ch6#what-is-a-conflict) - [無主複製](/tw/ch6#sec_replication_leaderless) - - [當節點故障時寫入資料庫](/tw/ch6#id287) - - [追趕錯過的寫入](/tw/ch6#sec_replication_read_repair) - - [讀寫仲裁](/tw/ch6#sec_replication_quorum_condition) - - [仲裁一致性的侷限](/tw/ch6#sec_replication_quorum_limitations) - - [監控陳舊性](/tw/ch6#monitoring-staleness) - - [單主與無主複製的效能](/tw/ch6#sec_replication_leaderless_perf) - - [多地區操作](/tw/ch6#multi-region-operation) - - [檢測併發寫入](/tw/ch6#sec_replication_concurrent) - - ["先發生"關係與併發](/tw/ch6#sec_replication_happens_before) - - [捕獲先發生關係](/tw/ch6#capturing-the-happens-before-relationship) - - [版本向量](/tw/ch6#version-vectors) - [總結](/tw/ch6#summary) - - [參考](/tw/ch6#參考) ## [7. 分片](/tw/ch7) - [分片的利與弊](/tw/ch7#sec_sharding_reasons) - - [面向多租戶的分片](/tw/ch7#sec_sharding_multitenancy) - [鍵值資料的分片](/tw/ch7#sec_sharding_key_value) - - [按鍵的範圍分片](/tw/ch7#sec_sharding_key_range) - - [重新平衡鍵範圍分片資料](/tw/ch7#rebalancing-key-range-sharded-data) - - [按鍵的雜湊分片](/tw/ch7#sec_sharding_hash) - - [雜湊取模節點數](/tw/ch7#hash-modulo-number-of-nodes) - - [固定數量的分片](/tw/ch7#fixed-number-of-shards) - - [按雜湊範圍分片](/tw/ch7#sharding-by-hash-range) - - [一致性雜湊](/tw/ch7#sec_sharding_consistent_hashing) - - [傾斜的工作負載與緩解熱點](/tw/ch7#sec_sharding_skew) - - [運維:自動/手動再均衡](/tw/ch7#sec_sharding_operations) - [請求路由](/tw/ch7#sec_sharding_routing) - [分片與二級索引](/tw/ch7#sec_sharding_secondary_indexes) - - [本地二級索引](/tw/ch7#id166) - - [全域性二級索引](/tw/ch7#id167) - [總結](/tw/ch7#summary) - - [References](/tw/ch7#references) ## [8. 事務](/tw/ch8) - [事務到底是什麼?](/tw/ch8#sec_transactions_overview) - - [ACID 的含義](/tw/ch8#sec_transactions_acid) - - [原子性](/tw/ch8#sec_transactions_acid_atomicity) - - [一致性](/tw/ch8#sec_transactions_acid_consistency) - - [隔離性](/tw/ch8#sec_transactions_acid_isolation) - - [永續性](/tw/ch8#durability) - - [單物件與多物件操作](/tw/ch8#sec_transactions_multi_object) - - [單物件寫入](/tw/ch8#sec_transactions_single_object) - - [多物件事務的需求](/tw/ch8#sec_transactions_need) - - [處理錯誤和中止](/tw/ch8#handling-errors-and-aborts) - [弱隔離級別](/tw/ch8#sec_transactions_isolation_levels) - - [讀已提交](/tw/ch8#sec_transactions_read_committed) - - [沒有髒讀](/tw/ch8#no-dirty-reads) - - [沒有髒寫](/tw/ch8#sec_transactions_dirty_write) - - [實現讀已提交](/tw/ch8#sec_transactions_read_committed_impl) - - [快照隔離與可重複讀](/tw/ch8#sec_transactions_snapshot_isolation) - - [多版本併發控制(MVCC)](/tw/ch8#sec_transactions_snapshot_impl) - - [觀察一致快照的可見性規則](/tw/ch8#sec_transactions_mvcc_visibility) - - [索引與快照隔離](/tw/ch8#indexes-and-snapshot-isolation) - - [快照隔離、可重複讀和命名混淆](/tw/ch8#snapshot-isolation-repeatable-read-and-naming-confusion) - - [防止丟失更新](/tw/ch8#sec_transactions_lost_update) - - [原子寫操作](/tw/ch8#atomic-write-operations) - - [顯式鎖定](/tw/ch8#explicit-locking) - - [自動檢測丟失的更新](/tw/ch8#automatically-detecting-lost-updates) - - [條件寫入(比較並設定)](/tw/ch8#sec_transactions_compare_and_set) - - [衝突解決與複製](/tw/ch8#conflict-resolution-and-replication) - - [寫偏斜與幻讀](/tw/ch8#sec_transactions_write_skew) - - [描述寫偏斜](/tw/ch8#characterizing-write-skew) - - [更多寫偏斜的例子](/tw/ch8#more-examples-of-write-skew) - - [導致寫偏斜的幻讀](/tw/ch8#sec_transactions_phantom) - - [物化衝突](/tw/ch8#materializing-conflicts) - [可序列化](/tw/ch8#sec_transactions_serializability) - - [實際序列執行](/tw/ch8#sec_transactions_serial) - - [將事務封裝在儲存過程中](/tw/ch8#encapsulating-transactions-in-stored-procedures) - - [儲存過程的利弊](/tw/ch8#sec_transactions_stored_proc_tradeoffs) - - [分片](/tw/ch8#sharding) - - [序列執行總結](/tw/ch8#summary-of-serial-execution) - - [兩階段鎖定(2PL)](/tw/ch8#sec_transactions_2pl) - - [兩階段鎖定的實現](/tw/ch8#implementation-of-two-phase-locking) - - [兩階段鎖定的效能](/tw/ch8#performance-of-two-phase-locking) - - [謂詞鎖](/tw/ch8#predicate-locks) - - [索引範圍鎖](/tw/ch8#sec_transactions_2pl_range) - - [可序列化快照隔離(SSI)](/tw/ch8#sec_transactions_ssi) - - [悲觀併發控制與樂觀併發控制](/tw/ch8#pessimistic-versus-optimistic-concurrency-control) - - [基於過時前提的決策](/tw/ch8#decisions-based-on-an-outdated-premise) - - [檢測陳舊的 MVCC 讀取](/tw/ch8#detecting-stale-mvcc-reads) - - [檢測影響先前讀取的寫入](/tw/ch8#sec_detecting_writes_affect_reads) - - [可序列化快照隔離的效能](/tw/ch8#performance-of-serializable-snapshot-isolation) - [分散式事務](/tw/ch8#sec_transactions_distributed) - - [兩階段提交(2PC)](/tw/ch8#sec_transactions_2pc) - - [系統性的承諾](/tw/ch8#a-system-of-promises) - - [協調器故障](/tw/ch8#coordinator-failure) - - [三階段提交](/tw/ch8#three-phase-commit) - - [跨不同系統的分散式事務](/tw/ch8#sec_transactions_xa) - - [精確一次訊息處理](/tw/ch8#sec_transactions_exactly_once) - - [XA 事務](/tw/ch8#xa-transactions) - - [存疑時持有鎖](/tw/ch8#holding-locks-while-in-doubt) - - [從協調器故障中恢復](/tw/ch8#recovering-from-coordinator-failure) - - [XA 事務的問題](/tw/ch8#problems-with-xa-transactions) - - [資料庫內部的分散式事務](/tw/ch8#sec_transactions_internal) - - [再談精確一次訊息處理](/tw/ch8#exactly-once-message-processing-revisited) - [總結](/tw/ch8#summary) - [參考](/tw/ch8#參考) ## [9. 分散式系統的麻煩](/tw/ch9) - [故障與部分失效](/tw/ch9#sec_distributed_partial_failure) - [不可靠的網路](/tw/ch9#sec_distributed_networks) - - [TCP 的侷限性](/tw/ch9#sec_distributed_tcp) - - [網路故障的實踐](/tw/ch9#sec_distributed_network_faults) - - [檢測故障](/tw/ch9#id307) - - [超時和無界延遲](/tw/ch9#sec_distributed_queueing) - - [網路擁塞和排隊](/tw/ch9#network-congestion-and-queueing) - - [同步與非同步網路](/tw/ch9#sec_distributed_sync_networks) - - [我們不能簡單地使網路延遲可預測嗎?](/tw/ch9#can-we-not-simply-make-network-delays-predictable) - [不可靠的時鐘](/tw/ch9#sec_distributed_clocks) - - [單調時鐘與日曆時鐘](/tw/ch9#sec_distributed_monotonic_timeofday) - - [日曆時鐘](/tw/ch9#time-of-day-clocks) - - [單調時鐘](/tw/ch9#monotonic-clocks) - - [時鐘同步和準確性](/tw/ch9#sec_distributed_clock_accuracy) - - [對同步時鐘的依賴](/tw/ch9#sec_distributed_clocks_relying) - - [用於事件排序的時間戳](/tw/ch9#sec_distributed_lww) - - [帶置信區間的時鐘讀數](/tw/ch9#clock-readings-with-a-confidence-interval) - - [用於全域性快照的同步時鐘](/tw/ch9#sec_distributed_spanner) - - [程序暫停](/tw/ch9#sec_distributed_clocks_pauses) - - [響應時間保證](/tw/ch9#sec_distributed_clocks_realtime) - - [限制垃圾回收的影響](/tw/ch9#sec_distributed_gc_impact) - [知識、真相和謊言](/tw/ch9#sec_distributed_truth) - - [多數派原則](/tw/ch9#sec_distributed_majority) - - [分散式鎖和租約](/tw/ch9#sec_distributed_lock_fencing) - - [隔離殭屍程序和延遲請求](/tw/ch9#sec_distributed_fencing_tokens) - - [多副本隔離](/tw/ch9#fencing-with-multiple-replicas) - - [拜占庭故障](/tw/ch9#sec_distributed_byzantine) - - [弱形式的謊言](/tw/ch9#weak-forms-of-lying) - - [系統模型與現實](/tw/ch9#sec_distributed_system_model) - - [定義演算法的正確性](/tw/ch9#defining-the-correctness-of-an-algorithm) - - [安全性與活性](/tw/ch9#sec_distributed_safety_liveness) - - [將系統模型對映到現實世界](/tw/ch9#mapping-system-models-to-the-real-world) - - [形式化方法和隨機測試](/tw/ch9#sec_distributed_formal) - - [模型檢查與規範語言](/tw/ch9#model-checking-and-specification-languages) - - [故障注入](/tw/ch9#sec_fault_injection) - - [確定性模擬測試](/tw/ch9#deterministic-simulation-testing) - [總結](/tw/ch9#summary) - - [參考](/tw/ch9#參考) ## [10. 一致性與共識](/tw/ch10) - [線性一致性](/tw/ch10#sec_consistency_linearizability) - - [什麼使系統具有線性一致性?](/tw/ch10#sec_consistency_lin_definition) - - [依賴線性一致性](/tw/ch10#sec_consistency_linearizability_usage) - - [鎖定與領導者選舉](/tw/ch10#locking-and-leader-election) - - [約束與唯一性保證](/tw/ch10#sec_consistency_uniqueness) - - [跨通道時序依賴](/tw/ch10#cross-channel-timing-dependencies) - - [實現線性一致性系統](/tw/ch10#sec_consistency_implementing_linearizable) - - [線性一致性與仲裁](/tw/ch10#sec_consistency_quorum_linearizable) - - [線性一致性的代價](/tw/ch10#sec_linearizability_cost) - - [CAP 定理](/tw/ch10#the-cap-theorem) - - [線性一致性與網路延遲](/tw/ch10#linearizability-and-network-delays) - [ID 生成器和邏輯時鐘](/tw/ch10#sec_consistency_logical) - - [邏輯時鐘](/tw/ch10#sec_consistency_timestamps) - - [Lamport 時間戳](/tw/ch10#lamport-timestamps) - - [混合邏輯時鐘](/tw/ch10#hybrid-logical-clocks) - - [Lamport/混合邏輯時鐘 vs. 向量時鐘](/tw/ch10#lamporthybrid-logical-clocks-vs-vector-clocks) - - [線性一致的 ID 生成器](/tw/ch10#sec_consistency_linearizable_id) - - [實現線性一致的 ID 生成器](/tw/ch10#implementing-a-linearizable-id-generator) - - [使用邏輯時鐘強制約束](/tw/ch10#enforcing-constraints-using-logical-clocks) - [共識](/tw/ch10#sec_consistency_consensus) - - [共識的多面性](/tw/ch10#sec_consistency_faces) - - [單值共識](/tw/ch10#single-value-consensus) - - [比較並設定作為共識](/tw/ch10#compare-and-set-as-consensus) - - [共享日誌作為共識](/tw/ch10#sec_consistency_shared_logs) - - [獲取並增加作為共識](/tw/ch10#fetch-and-add-as-consensus) - - [原子提交作為共識](/tw/ch10#atomic-commitment-as-consensus) - - [共識的實踐](/tw/ch10#sec_consistency_total_order) - - [使用共享日誌](/tw/ch10#sec_consistency_smr) - - [從單主複製到共識](/tw/ch10#from-single-leader-replication-to-consensus) - - [共識的微妙之處](/tw/ch10#subtleties-of-consensus) - - [共識的利弊](/tw/ch10#pros-and-cons-of-consensus) - [總結](/tw/ch10#summary) - - [參考文獻](/tw/ch10#參考文獻) ## [第十一章:批處理](/tw/ch11) - [使用Unix工具的批處理](/tw/ch11#使用unix工具的批處理) - - [簡單日誌分析](/tw/ch11#簡單日誌分析) - - [命令鏈與自定義程式](/tw/ch11#命令鏈與自定義程式) - - [排序 VS 記憶體中的聚合](/tw/ch11#排序-vs-記憶體中的聚合) - - [Unix哲學](/tw/ch11#unix哲學) - - [統一的介面](/tw/ch11#統一的介面) - - [邏輯與佈線相分離](/tw/ch11#邏輯與佈線相分離) - - [透明度和實驗](/tw/ch11#透明度和實驗) - [MapReduce和分散式檔案系統](/tw/ch11#mapreduce和分散式檔案系統) - - [MapReduce作業執行](/tw/ch11#mapreduce作業執行) - - [分散式執行MapReduce](/tw/ch11#分散式執行mapreduce) - - [MapReduce工作流](/tw/ch11#mapreduce工作流) - - [Reduce側連線與分組](/tw/ch11#reduce側連線與分組) - - [示例:使用者活動事件分析](/tw/ch11#示例使用者活動事件分析) - - [排序合併連線](/tw/ch11#排序合併連線) - - [把相關資料放在一起](/tw/ch11#把相關資料放在一起) - - [分組](/tw/ch11#分組) - - [處理偏斜](/tw/ch11#處理偏斜) - - [Map側連線](/tw/ch11#map側連線) - - [廣播雜湊連線](/tw/ch11#廣播雜湊連線) - - [分割槽雜湊連線](/tw/ch11#分割槽雜湊連線) - - [Map側合併連線](/tw/ch11#map側合併連線) - - [MapReduce工作流與Map側連線](/tw/ch11#mapreduce工作流與map側連線) - - [批處理工作流的輸出](/tw/ch11#批處理工作流的輸出) - - [建立搜尋索引](/tw/ch11#建立搜尋索引) - - [鍵值儲存作為批處理輸出](/tw/ch11#鍵值儲存作為批處理輸出) - - [批處理輸出的哲學](/tw/ch11#批處理輸出的哲學) - - [Hadoop與分散式資料庫的對比](/tw/ch11#hadoop與分散式資料庫的對比) - - [儲存多樣性](/tw/ch11#儲存多樣性) - - [處理模型的多樣性](/tw/ch11#處理模型的多樣性) - - [針對頻繁故障設計](/tw/ch11#針對頻繁故障設計) - [MapReduce之後](/tw/ch11#mapreduce之後) - - [物化中間狀態](/tw/ch11#物化中間狀態) - - [資料流引擎](/tw/ch11#資料流引擎) - - [容錯](/tw/ch11#容錯) - - [關於物化的討論](/tw/ch11#關於物化的討論) - - [圖與迭代處理](/tw/ch11#圖與迭代處理) - - [Pregel處理模型](/tw/ch11#pregel處理模型) - - [容錯](/tw/ch11#容錯) - - [並行執行](/tw/ch11#並行執行) - - [高階API和語言](/tw/ch11#高階api和語言) - - [向宣告式查詢語言的轉變](/tw/ch11#向宣告式查詢語言的轉變) - - [專業化的不同領域](/tw/ch11#專業化的不同領域) - [本章小結](/tw/ch11#本章小結) - [參考文獻](/tw/ch11#參考文獻) ## [第十二章:流處理](/tw/ch12) - [傳遞事件流](/tw/ch12#傳遞事件流) - - [訊息傳遞系統](/tw/ch12#訊息傳遞系統) - - [直接從生產者傳遞給消費者](/tw/ch12#直接從生產者傳遞給消費者) - - [訊息代理](/tw/ch12#訊息代理) - - [訊息代理與資料庫的對比](/tw/ch12#訊息代理與資料庫的對比) - - [多個消費者](/tw/ch12#多個消費者) - - [確認與重新傳遞](/tw/ch12#確認與重新傳遞) - - [分割槽日誌](/tw/ch12#分割槽日誌) - - [使用日誌進行訊息儲存](/tw/ch12#使用日誌進行訊息儲存) - - [日誌與傳統的訊息傳遞相比](/tw/ch12#日誌與傳統的訊息傳遞相比) - - [消費者偏移量](/tw/ch12#消費者偏移量) - - [磁碟空間使用](/tw/ch12#磁碟空間使用) - - [當消費者跟不上生產者時](/tw/ch12#當消費者跟不上生產者時) - - [重播舊訊息](/tw/ch12#重播舊訊息) - [資料庫與流](/tw/ch12#資料庫與流) - - [保持系統同步](/tw/ch12#保持系統同步) - - [變更資料捕獲](/tw/ch12#變更資料捕獲) - - [變更資料捕獲的實現](/tw/ch12#變更資料捕獲的實現) - - [初始快照](/tw/ch12#初始快照) - - [日誌壓縮](/tw/ch12#日誌壓縮) - - [變更流的API支援](/tw/ch12#變更流的api支援) - - [事件溯源](/tw/ch12#事件溯源) - - [從事件日誌中派生出當前狀態](/tw/ch12#從事件日誌中派生出當前狀態) - - [命令和事件](/tw/ch12#命令和事件) - - [狀態、流和不變性](/tw/ch12#狀態流和不變性) - - [不可變事件的優點](/tw/ch12#不可變事件的優點) - - [從同一事件日誌中派生多個檢視](/tw/ch12#從同一事件日誌中派生多個檢視) - - [併發控制](/tw/ch12#併發控制) - - [不變性的侷限性](/tw/ch12#不變性的侷限性) - [流處理](/tw/ch12#流處理) - - [流處理的應用](/tw/ch12#流處理的應用) - - [複合事件處理](/tw/ch12#複合事件處理) - - [流分析](/tw/ch12#流分析) - - [維護物化檢視](/tw/ch12#維護物化檢視) - - [在流上搜索](/tw/ch12#在流上搜索) - - [訊息傳遞和RPC](/tw/ch12#訊息傳遞和rpc) - - [時間推理](/tw/ch12#時間推理) - - [事件時間與處理時間](/tw/ch12#事件時間與處理時間) - - [知道什麼時候準備好了](/tw/ch12#知道什麼時候準備好了) - - [你用的是誰的時鐘?](/tw/ch12#你用的是誰的時鐘) - - [視窗的型別](/tw/ch12#視窗的型別) - - [流連線](/tw/ch12#流連線) - - [流流連線(視窗連線)](/tw/ch12#流流連線視窗連線) - - [流表連線(流擴充)](/tw/ch12#流表連線流擴充) - - [表表連線(維護物化檢視)](/tw/ch12#表表連線維護物化檢視) - - [連線的時間依賴性](/tw/ch12#連線的時間依賴性) - - [容錯](/tw/ch12#容錯) - - [微批次與存檔點](/tw/ch12#微批次與存檔點) - - [原子提交再現](/tw/ch12#原子提交再現) - - [冪等性](/tw/ch12#冪等性) - - [失敗後重建狀態](/tw/ch12#失敗後重建狀態) - [本章小結](/tw/ch12#本章小結) - [參考文獻](/tw/ch12#參考文獻) ## [第十三章:資料系統的未來](/ch13) - [資料整合](/ch13#資料整合) - - [組合使用派生資料的工具](/ch13#組合使用派生資料的工具) - - [理解資料流](/ch13#理解資料流) - - [派生資料與分散式事務](/ch13#派生資料與分散式事務) - - [全序的限制](/ch13#全序的限制) - - [排序事件以捕獲因果關係](/ch13#排序事件以捕獲因果關係) - - [批處理與流處理](/ch13#批處理與流處理) - - [維護衍生狀態](/ch13#維護衍生狀態) - - [應用演化後重新處理資料](/ch13#應用演化後重新處理資料) - - [Lambda架構](/ch13#lambda架構) - - [統一批處理和流處理](/ch13#統一批處理和流處理) - [分拆資料庫](/ch13#分拆資料庫) - - [組合使用資料儲存技術](/ch13#組合使用資料儲存技術) - - [建立索引](/ch13#建立索引) - - [一切的元資料庫](/ch13#一切的元資料庫) - - [開展分拆工作](/ch13#開展分拆工作) - - [分拆系統vs整合系統](/ch13#分拆系統vs整合系統) - - [少了什麼?](/ch13#少了什麼) - - [圍繞資料流設計應用](/ch13#圍繞資料流設計應用) - - [應用程式碼作為衍生函式](/ch13#應用程式碼作為衍生函式) - - [應用程式碼和狀態的分離](/ch13#應用程式碼和狀態的分離) - - [資料流:應用程式碼與狀態變化的互動](/ch13#資料流應用程式碼與狀態變化的互動) - - [流處理器和服務](/ch13#流處理器和服務) - - [觀察派生資料狀態](/ch13#觀察派生資料狀態) - - [物化檢視和快取](/ch13#物化檢視和快取) - - [有狀態、可離線的客戶端](/ch13#有狀態可離線的客戶端) - - [將狀態變更推送給客戶端](/ch13#將狀態變更推送給客戶端) - - [端到端的事件流](/ch13#端到端的事件流) - - [讀也是事件](/ch13#讀也是事件) - - [多分割槽資料處理](/ch13#多分割槽資料處理) - [將事情做正確](/ch13#將事情做正確) - - [資料庫的端到端原則](/ch13#資料庫的端到端原則) - - [正好執行一次操作](/ch13#正好執行一次操作) - - [抑制重複](/ch13#抑制重複) - - [操作識別符號](/ch13#操作識別符號) - - [端到端原則](/ch13#端到端原則) - - [在資料系統中應用端到端思考](/ch13#在資料系統中應用端到端思考) - - [強制約束](/ch13#強制約束) - - [唯一性約束需要達成共識](/ch13#唯一性約束需要達成共識) - - [基於日誌訊息傳遞中的唯一性](/ch13#基於日誌訊息傳遞中的唯一性) - - [多分割槽請求處理](/ch13#多分割槽請求處理) - - [及時性與完整性](/ch13#及時性與完整性) - - [資料流系統的正確性](/ch13#資料流系統的正確性) - - [寬鬆地解釋約束](/ch13#寬鬆地解釋約束) - - [無協調資料系統](/ch13#無協調資料系統) - - [信任但驗證](/ch13#信任但驗證) - - [維護完整性,儘管軟體有Bug](/ch13#維護完整性儘管軟體有bug) - - [不要盲目信任承諾](/ch13#不要盲目信任承諾) - - [驗證的文化](/ch13#驗證的文化) - - [為可審計性而設計](/ch13#為可審計性而設計) - - [端到端原則重現](/ch13#端到端原則重現) - - [用於可審計資料系統的工具](/ch13#用於可審計資料系統的工具) - [做正確的事情](/ch13#做正確的事情) - - [預測性分析](/ch13#預測性分析) - - [偏見與歧視](/ch13#偏見與歧視) - - [責任與問責](/ch13#責任與問責) - - [反饋迴圈](/ch13#反饋迴圈) - - [隱私和追蹤](/ch13#隱私和追蹤) - - [監視](/ch13#監視) - - [同意與選擇的自由](/ch13#同意與選擇的自由) - - [隱私與資料使用](/ch13#隱私與資料使用) - - [資料資產與權力](/ch13#資料資產與權力) - - [回顧工業革命](/ch13#回顧工業革命) - - [立法與自律](/ch13#立法與自律) - [本章小結](/ch13#本章小結) - [參考文獻](/ch13#參考文獻)