mirror of
https://github.com/Vonng/ddia.git
synced 2025-01-05 15:30:06 +08:00
update zh-tw content
This commit is contained in:
parent
5a68cd84bb
commit
9683eae3e7
@ -110,7 +110,7 @@
|
||||
* [本章小結](ch10.md#本章小結)
|
||||
* [第十一章:流處理](ch11.md)
|
||||
* [傳遞事件流](ch11.md#傳遞事件流)
|
||||
* [流與資料庫](ch11.md#流與資料庫)
|
||||
* [資料庫與流](ch11.md#資料庫與流)
|
||||
* [流處理](ch11.md#流處理)
|
||||
* [本章小結](ch11.md#本章小結)
|
||||
* [第十二章:資料系統的未來](ch12.md)
|
||||
|
@ -293,7 +293,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
|
||||
當我們在批處理的語境中討論連線時,我們指的是在資料集中解析某種關聯的全量存在。 例如我們假設一個作業是同時處理所有使用者的資料,而非僅僅是為某個特定使用者查詢資料(而這能透過索引更高效地完成)。
|
||||
|
||||
#### 示例:分析使用者活動事件
|
||||
#### 示例:使用者活動事件分析
|
||||
|
||||
[圖10-2](../img/fig10-2.png)給出了一個批處理作業中連線的典型例子。左側是事件日誌,描述登入使用者在網站上做的事情(稱為**活動事件(activity events)**或**點選流資料(clickstream data)**),右側是使用者資料庫。 你可以將此示例看作是星型模式的一部分(參閱“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”):事件日誌是事實表,使用者資料庫是其中的一個維度。
|
||||
|
||||
@ -568,7 +568,7 @@ top5.each{|count, url| puts "#{count} #{url}" } # 5
|
||||
- 另一種可能是接受多個輸入,並以相同的方式進行分割槽,但跳過排序。當記錄的分割槽重要但順序無關緊要時,這省去了分割槽雜湊連線的工作,因為構建散列表還是會把順序隨機打亂。
|
||||
- 對於廣播雜湊連線,可以將一個運算元的輸出,傳送到連線運算元的所有分割槽。
|
||||
|
||||
這種型別的處理引擎是基於像Dryad 【67】和Nephele 【68】這樣的研究系統,與MapReduce模型相比,它有幾個優點:
|
||||
這種型別的處理引擎是基於像Dryad【67】和Nephele【68】這樣的研究系統,與MapReduce模型相比,它有幾個優點:
|
||||
|
||||
- 排序等昂貴的工作只需要在實際需要的地方執行,而不是預設地在每個Map和Reduce階段之間出現。
|
||||
- 沒有不必要的Map任務,因為Mapper所做的工作通常可以合併到前面的Reduce運算元中(因為Mapper不會更改資料集的分割槽)。
|
||||
|
254
zh-tw/ch11.md
254
zh-tw/ch11.md
@ -10,7 +10,7 @@
|
||||
|
||||
[TOC]
|
||||
|
||||
在[第10章](ch10.md)中,我們討論了批處理技術,它讀取一組檔案作為輸入,並生成一組新的檔案作為輸出。輸出是**衍生資料(derived data)**的一種形式;也就是說,如果需要,可以透過再次執行批處理過程來重新建立資料集。我們看到了如何使用這個簡單而強大的想法來建立搜尋索引,推薦系統,做分析等等。
|
||||
在[第10章](ch10.md)中,我們討論了批處理技術,它讀取一組檔案作為輸入,並生成一組新的檔案作為輸出。輸出是**衍生資料(derived data)**的一種形式;也就是說,如果需要,可以透過再次執行批處理過程來重新建立資料集。我們看到了如何使用這個簡單而強大的想法來建立搜尋索引、推薦系統、做分析等等。
|
||||
|
||||
然而,在[第10章](ch10.md)中仍然有一個很大的假設:即輸入是有界的,即已知和有限的大小,所以批處理知道它何時完成輸入的讀取。例如,MapReduce核心的排序操作必須讀取其全部輸入,然後才能開始生成輸出:可能發生這種情況:最後一條輸入記錄具有最小的鍵,因此需要第一個被輸出,所以提早開始輸出是不可行的。
|
||||
|
||||
@ -19,19 +19,19 @@
|
||||
日常批處理中的問題是,輸入的變更只會在一天之後的輸出中反映出來,這對於許多急躁的使用者來說太慢了。為了減少延遲,我們可以更頻繁地執行處理 —— 比如說,在每秒鐘的末尾 —— 或者甚至更連續一些,完全拋開固定的時間切片,當事件發生時就立即進行處理,這就是**流處理(stream processing)**背後的想法。
|
||||
|
||||
一般來說,“流”是指隨著時間的推移逐漸可用的資料。這個概念出現在很多地方:Unix的stdin和stdout,程式語言(惰性列表)【2】,檔案系統API(如Java的`FileInputStream`),TCP連線,透過網際網路傳送音訊和影片等等。
|
||||
在本章中,我們將把**事件流(event stream)**視為一種資料管理機制:無界限,增量處理,與[上一章](ch10.md)中批次資料相對應。我們將首先討論怎樣表示、儲存、透過網路傳輸流。在“[資料庫和流](#資料庫和流)”中,我們將研究流和資料庫之間的關係。最後在“[流處理](#流處理)”中,我們將研究連續處理這些流的方法和工具,以及它們用於應用構建的方式。
|
||||
|
||||
在本章中,我們將把**事件流(event stream)**視為一種資料管理機制:無界限,增量處理,與上一章中的批次資料相對應。我們將首先討論怎樣表示、儲存、透過網路傳輸流。在“[資料庫與流](#資料庫與流)”中,我們將研究流和資料庫之間的關係。最後在“[流處理](#流處理)”中,我們將研究連續處理這些流的方法和工具,以及它們用於應用構建的方式。
|
||||
|
||||
|
||||
## 傳遞事件流
|
||||
|
||||
在批處理領域,作業的輸入和輸出是檔案(也許在分散式檔案系統上)。流處理領域中的等價物看上去是什麼樣子的?
|
||||
|
||||
當輸入是一個檔案(一個位元組序列),第一個處理步驟通常是將其解析為一系列記錄。在流處理的上下文中,記錄通常被叫做 **事件(event)** ,但它本質上是一樣的:一個小的,自包含的,不可變的物件,包含某個時間點發生的某件事情的細節。一個事件通常包含一個來自時鐘的時間戳,以指明事件發生的時間(參見“[單調鍾與時鐘](ch8.md#單調鍾與時鐘)”)。
|
||||
當輸入是一個檔案(一個位元組序列),第一個處理步驟通常是將其解析為一系列記錄。在流處理的上下文中,記錄通常被叫做 **事件(event)** ,但它本質上是一樣的:一個小的、自包含的、不可變的物件,包含某個時間點發生的某件事情的細節。一個事件通常包含一個來自日曆時鐘的時間戳,以指明事件發生的時間(參見“[單調鍾與日曆時鐘](ch8.md#單調鍾與日曆時鐘)”)。
|
||||
|
||||
例如,發生的事件可能是使用者採取的行動,例如檢視頁面或進行購買。它也可能來源於機器,例如對溫度感測器或CPU利用率的週期性測量。在“[使用Unix工具進行批處理](ch10.md#使用Unix工具進行批處理)”的示例中,Web伺服器日誌的每一行都是一個事件。
|
||||
例如,發生的事件可能是使用者採取的行動,例如檢視頁面或進行購買。它也可能來源於機器,例如對溫度感測器或CPU利用率的週期性測量。在“[使用Unix工具的批處理](ch10.md#使用Unix工具的批處理)”的示例中,Web伺服器日誌的每一行都是一個事件。
|
||||
|
||||
事件可能被編碼為文字字串或JSON,或者某種二進位制編碼,如[第4章](ch4.md)所述。這種編碼允許你儲存一個事件,例如將其附加到一個檔案,將其插入關係表,或將其寫入文件資料庫。它還允許你透過網路將事件傳送到另一個節點以進行處理。
|
||||
事件可能被編碼為文字字串或JSON,或者某種二進位制編碼,如[第4章](ch4.md)所述。這種編碼允許你儲存一個事件,例如將其追加到一個檔案,將其插入關係表,或將其寫入文件資料庫。它還允許你透過網路將事件傳送到另一個節點以進行處理。
|
||||
|
||||
在批處理中,檔案被寫入一次,然後可能被多個作業讀取。類似地,在流處理術語中,一個事件由 **生產者(producer)** (也稱為 **釋出者(publisher)** 或 **傳送者(sender)** )生成一次,然後可能由多個 **消費者(consumer)** ( **訂閱者(subscribers)** 或 **接收者(recipients)** )進行處理【3】。在檔案系統中,檔名標識一組相關記錄;在流式系統中,相關的事件通常被聚合為一個 **主題(topic)** 或 **流(stream)** 。
|
||||
|
||||
@ -39,24 +39,24 @@
|
||||
|
||||
但當我們想要進行低延遲的連續處理時,如果資料儲存不是為這種用途專門設計的,那麼輪詢開銷就會很大。輪詢的越頻繁,能返回新事件的請求比例就越低,而額外開銷也就越高。相比之下,最好能在新事件出現時直接通知消費者。
|
||||
|
||||
資料庫在傳統上對這種通知機制支援的並不好,關係型資料庫通常有 **觸發器(trigger)** ,它們可以對變化作出反應(如,插入表中的一行),但是它們的功能非常有限,並且在資料庫設計中有些後顧之憂【4,5】。相應的是,已經開發了專門的工具來提供事件通知。
|
||||
資料庫在傳統上對這種通知機制支援的並不好,關係型資料庫通常有 **觸發器(trigger)** ,它們可以對變化(如,插入表中的一行)作出反應,但是它們的功能非常有限,並且在資料庫設計中有些後顧之憂【4,5】。相應的是,已經開發了專門的工具來提供事件通知。
|
||||
|
||||
|
||||
### 訊息系統
|
||||
### 訊息傳遞系統
|
||||
|
||||
向消費者通知新事件的常用方式是使用**訊息傳遞系統(messaging system)**:生產者傳送包含事件的訊息,然後將訊息推送給消費者。我們之前在“[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)”中介紹了這些系統,但現在我們將詳細介紹這些系統。
|
||||
向消費者通知新事件的常用方式是使用**訊息傳遞系統(messaging system)**:生產者傳送包含事件的訊息,然後將訊息推送給消費者。我們之前在“[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)”中談到了這些系統,但現在我們將詳細介紹這些系統。
|
||||
|
||||
像生產者和消費者之間的Unix管道或TCP連線這樣的直接通道,是實現訊息傳遞系統的簡單方法。但是,大多數訊息傳遞系統都在這一基本模型上進行擴充套件。特別的是,Unix管道和TCP將恰好一個傳送者與恰好一個接收者連線,而一個訊息傳遞系統允許多個生產者節點將訊息傳送到同一個主題,並允許多個消費者節點接收主題中的訊息。
|
||||
像生產者和消費者之間的Unix管道或TCP連線這樣的直接通道,是實現訊息傳遞系統的簡單方法。但是,大多數訊息傳遞系統都在這一基本模型上進行了擴充套件。特別的是,Unix管道和TCP將恰好一個傳送者與恰好一個接收者連線,而一個訊息傳遞系統允許多個生產者節點將訊息傳送到同一個主題,並允許多個消費者節點接收主題中的訊息。
|
||||
|
||||
在這個**釋出/訂閱**模式中,不同的系統採取各種各樣的方法,並沒有針對所有目的的通用答案。為了區分這些系統,問一下這兩個問題會特別有幫助:
|
||||
|
||||
1. **如果生產者傳送訊息的速度比消費者能夠處理的速度快會發生什麼?**一般來說,有三種選擇:系統可以丟掉訊息,將訊息放入緩衝佇列,或使用**背壓(backpressure)**(也稱為**流量控制(flow control);**即阻塞生產者,以免其傳送更多的訊息)。例如Unix管道和TCP使用背壓:它們有一個固定大小的小緩衝區,如果填滿,傳送者會被阻塞,直到接收者從緩衝區中取出資料(參見“[網路擁塞和排隊](ch8.md#網路擁塞和排隊)”)。
|
||||
1. **如果生產者傳送訊息的速度比消費者能夠處理的速度快會發生什麼?**一般來說,有三種選擇:系統可以丟掉訊息,將訊息放入緩衝佇列,或使用**背壓(backpressure)**(也稱為**流量控制(flow control)**;即阻塞生產者,以免其傳送更多的訊息)。例如Unix管道和TCP就使用了背壓:它們有一個固定大小的小緩衝區,如果填滿,傳送者會被阻塞,直到接收者從緩衝區中取出資料(參見“[網路擁塞和排隊](ch8.md#網路擁塞和排隊)”)。
|
||||
|
||||
如果訊息被快取在佇列中,那麼理解佇列增長會發生什麼是很重要的。當佇列裝不進記憶體時系統會崩潰嗎?還是將訊息寫入磁碟?如果是這樣,磁碟訪問又會如何影響訊息傳遞系統的效能【6】?
|
||||
|
||||
2. **如果節點崩潰或暫時離線,會發生什麼情況? —— 是否會有訊息丟失?**與資料庫一樣,永續性可能需要寫入磁碟**和/或**複製的某種組合(參閱“[複製和永續性](ch7.md#複製和永續性)”),這是有代價的。如果你能接受有時訊息會丟失,則可能在同一硬體上獲得更高的吞吐量和更低的延遲。
|
||||
2. **如果節點崩潰或暫時離線,會發生什麼情況? —— 是否會有訊息丟失?**與資料庫一樣,永續性可能需要寫入磁碟和/或複製的某種組合(參閱“[複製和永續性](ch7.md#複製和永續性)”),這是有代價的。如果你能接受有時訊息會丟失,則可能在同一硬體上獲得更高的吞吐量和更低的延遲。
|
||||
|
||||
是否可以接受訊息丟失取決於應用。例如,對於週期傳輸的感測器讀數和指標,偶爾丟失的資料點可能並不重要,因為更新的值會在短時間內發出。但要注意,如果大量的訊息被丟棄,可能無法立刻意識到指標已經不正確了【7】。如果你正在對事件計數,那麼更重要的是它們能夠可靠送達,因為每個丟失的訊息都意味著使計數器的錯誤擴大。
|
||||
是否可以接受訊息丟失取決於應用。例如,對於週期傳輸的感測器讀數和指標,偶爾丟失的資料點可能並不重要,因為更新的值會在短時間內發出。但要注意,如果大量的訊息被丟棄,可能無法立刻意識到指標已經不正確了【7】。如果你正在對事件計數,那麼它們能夠可靠送達是更重要的,因為每個丟失的訊息都意味著使計數器的錯誤擴大。
|
||||
|
||||
我們在[第10章](ch10.md)中探討的批處理系統的一個很好的特性是,它們提供了強大的可靠性保證:失敗的任務會自動重試,失敗任務的部分輸出會自動丟棄。這意味著輸出與沒有發生故障一樣,這有助於簡化程式設計模型。在本章的後面,我們將研究如何在流處理的上下文中提供類似的保證。
|
||||
|
||||
@ -67,9 +67,9 @@
|
||||
* UDP組播廣泛應用於金融行業,例如股票市場,其中低時延非常重要【8】。雖然UDP本身是不可靠的,但應用層的協議可以恢復丟失的資料包(生產者必須記住它傳送的資料包,以便能按需重新發送資料包)。
|
||||
* 無代理的訊息庫,如ZeroMQ 【9】和nanomsg採取類似的方法,透過TCP或IP多播實現釋出/訂閱訊息傳遞。
|
||||
* StatsD 【10】和Brubeck 【7】使用不可靠的UDP訊息傳遞來收集網路中所有機器的指標並對其進行監控。 (在StatsD協議中,只有接收到所有訊息,才認為計數器指標是正確的;使用UDP將使得指標處在一種最佳近似狀態【11】。另請參閱“[TCP與UDP](ch8.md#TCP與UDP)”
|
||||
* 如果消費者在網路上公開了服務,生產者可以直接傳送HTTP或RPC請求(參閱“[透過服務進行資料流:REST和RPC](ch4.md#透過服務進行資料流:REST和RPC)”)將訊息推送給使用者。這就是webhooks背後的想法【12】,一種服務的回撥URL被註冊到另一個服務中,並且每當事件發生時都會向該URL發出請求。
|
||||
* 如果消費者在網路上公開了服務,生產者可以直接傳送HTTP或RPC請求(參閱“[服務中的資料流:REST與RPC](ch4.md#服務中的資料流:REST與RPC)”)將訊息推送給使用者。這就是webhooks背後的想法【12】,一種服務的回撥URL被註冊到另一個服務中,並且每當事件發生時都會向該URL發出請求。
|
||||
|
||||
儘管這些直接訊息傳遞系統在設計它們的環境中執行良好,但是它們通常要求應用程式碼意識到訊息丟失的可能性。它們的容錯程度極為有限:即使協議檢測到並重傳在網路中丟失的資料包,它們通常也只是假設生產者和消費者始終線上。
|
||||
儘管這些直接訊息傳遞系統在設計它們的環境中執行良好,但是它們通常要求應用程式碼意識到訊息丟失的可能性。它們的容錯程度極為有限:即使協議檢測到並重傳在網路中丟失的資料包,它們通常也只是假設生產者和消費者始終線上。
|
||||
|
||||
如果消費者處於離線狀態,則可能會丟失其不可達時傳送的訊息。一些協議允許生產者重試失敗的訊息傳遞,但當生產者崩潰時,它可能會丟失訊息緩衝區及其本應傳送的訊息,這種方法可能就沒用了。
|
||||
|
||||
@ -81,22 +81,22 @@
|
||||
|
||||
排隊的結果是,消費者通常是**非同步(asynchronous)**的:當生產者傳送訊息時,通常只會等待代理確認訊息已經被快取,而不等待訊息被消費者處理。向消費者遞送訊息將發生在未來某個未定的時間點 —— 通常在幾分之一秒之內,但有時當訊息堆積時會顯著延遲。
|
||||
|
||||
#### 訊息代理與資料庫對比
|
||||
#### 訊息代理與資料庫的對比
|
||||
|
||||
有些訊息代理甚至可以使用XA或JTA參與兩階段提交協議(參閱“[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”)。這個功能與資料庫在本質上非常相似,儘管訊息代理和資料庫之間仍存在實踐上很重要的差異:
|
||||
|
||||
* 資料庫通常保留資料直至顯式刪除,而大多數訊息代理在訊息成功遞送給消費者時會自動刪除訊息。這樣的訊息代理不適合長期的資料儲存。
|
||||
* 由於它們很快就能刪除訊息,大多數訊息代理都認為它們的工作集相當小—— 即佇列很短。如果代理需要緩衝很多訊息,比如因為消費者速度較慢(如果記憶體裝不下訊息,可能會溢位到磁碟),每個訊息需要更長的處理時間,整體吞吐量可能會惡化【6】。
|
||||
* 資料庫通常支援二級索引和各種搜尋資料的方式,而訊息代理通常支援按照某種模式匹配主題,訂閱其子集。機制並不一樣,對於客戶端選擇想要了解的資料的一部分,這是兩種基本的方式。
|
||||
* 資料庫通常支援二級索引和各種搜尋資料的方式,而訊息代理通常支援按照某種模式匹配主題,訂閱其子集。雖然機制並不一樣,但對於客戶端選擇想要了解的資料的一部分,都是基本的方式。
|
||||
* 查詢資料庫時,結果通常基於某個時間點的資料快照;如果另一個客戶端隨後向資料庫寫入一些改變了查詢結果的內容,則第一個客戶端不會發現其先前結果現已過期(除非它重複查詢或輪詢變更)。相比之下,訊息代理不支援任意查詢,但是當資料發生變化時(即新訊息可用時),它們會通知客戶端。
|
||||
|
||||
這是關於訊息代理的傳統觀點,它被封裝在諸如JMS 【14】和AMQP 【15】的標準中,並且被諸如RabbitMQ,ActiveMQ,HornetQ,Qpid,TIBCO企業訊息服務,IBM MQ,Azure Service Bus和Google Cloud Pub/Sub實現 【16】。
|
||||
這是關於訊息代理的傳統觀點,它被封裝在諸如JMS 【14】和AMQP 【15】的標準中,並且被諸如RabbitMQ、ActiveMQ、HornetQ、Qpid、TIBCO企業訊息服務、IBM MQ、Azure Service Bus和Google Cloud Pub/Sub所實現 【16】。
|
||||
|
||||
#### 多個消費者
|
||||
|
||||
當多個消費者從同一主題中讀取訊息時,有使用兩種主要的訊息傳遞模式,如[圖11-1](../img/fig11-1.png)所示:
|
||||
當多個消費者從同一主題中讀取訊息時,有兩種主要的訊息傳遞模式,如[圖11-1](../img/fig11-1.png)所示:
|
||||
|
||||
***負載均衡(load balance)***
|
||||
***負載均衡(load balancing)***
|
||||
|
||||
每條訊息都被傳遞給消費者**之一**,所以處理該主題下訊息的工作能被多個消費者共享。代理可以為消費者任意分配訊息。當處理訊息的代價高昂,希望能並行處理訊息時,此模式非常有用(在AMQP中,可以透過讓多個客戶端從同一個佇列中消費來實現負載均衡,而在JMS中則稱之為**共享訂閱(shared subscription)**)。
|
||||
|
||||
@ -108,15 +108,15 @@
|
||||
|
||||
**圖11-1 (a)負載平衡:在消費者間共享消費主題;(b)扇出:將每條訊息傳遞給多個消費者。**
|
||||
|
||||
兩種模式可以組合使用:例如,兩個獨立的消費者組可以每組各訂閱一個主題,每一組都共同收到所有訊息,但在每一組內部,每條訊息僅由單個節點處理。
|
||||
兩種模式可以組合使用:例如,兩個獨立的消費者組可以每組各訂閱同一個主題,每一組都共同收到所有訊息,但在每一組內部,每條訊息僅由單個節點處理。
|
||||
|
||||
#### 確認與重新交付
|
||||
#### 確認與重新傳遞
|
||||
|
||||
消費隨時可能會崩潰,所以有一種可能的情況是:代理向消費者遞送訊息,但消費者沒有處理,或者在消費者崩潰之前只進行了部分處理。為了確保訊息不會丟失,訊息代理使用**確認(acknowledgments)**:客戶端必須顯式告知代理訊息處理完畢的時間,以便代理能將訊息從佇列中移除。
|
||||
消費者隨時可能會崩潰,所以有一種可能的情況是:代理向消費者遞送訊息,但消費者沒有處理,或者在消費者崩潰之前只進行了部分處理。為了確保訊息不會丟失,訊息代理使用**確認(acknowledgments)**:客戶端必須顯式告知代理訊息處理完畢的時間,以便代理能將訊息從佇列中移除。
|
||||
|
||||
如果與客戶端的連線關閉,或者代理超出一段時間未收到確認,代理則認為訊息沒有被處理,因此它將訊息再遞送給另一個消費者。 (請注意可能發生這樣的情況,訊息**實際上是**處理完畢的,但**確認**在網路中丟失了。需要一種原子提交協議才能處理這種情況,正如在“[實踐中的分散式事務](ch9.md#實踐中的分散式事務)”中所討論的那樣)
|
||||
|
||||
當與負載均衡相結合時,這種重傳行為對訊息的順序有種有趣的影響。在[圖11-2](../img/fig11-2.png)中,消費者通常按照生產者傳送的順序處理訊息。然而消費者2在處理訊息m3時崩潰,與此同時消費者1正在處理訊息m4。未確認的訊息m3隨後被重新發送給消費者1,結果消費者1按照m4,m3,m5的順序處理訊息。因此m3和m4的交付順序與以生產者1的傳送順序不同。
|
||||
當與負載均衡相結合時,這種重傳行為對訊息的順序有種有趣的影響。在[圖11-2](../img/fig11-2.png)中,消費者通常按照生產者傳送的順序處理訊息。然而消費者2在處理訊息m3時崩潰,與此同時消費者1正在處理訊息m4。未確認的訊息m3隨後被重新發送給消費者1,結果消費者1按照m4,m3,m5的順序處理訊息。因此m3和m4的交付順序與生產者1的傳送順序不同。
|
||||
|
||||
![](../img/fig11-2.png)
|
||||
|
||||
@ -132,7 +132,7 @@
|
||||
|
||||
這種思維方式上的差異對建立衍生資料的方式有巨大影響。如[第10章](ch10.md)所述,批處理過程的一個關鍵特性是,你可以反覆執行它們,試驗處理步驟,不用擔心損壞輸入(因為輸入是隻讀的)。而 AMQP/JMS風格的訊息傳遞並非如此:收到訊息是具有破壞性的,因為確認可能導致訊息從代理中被刪除,因此你不能期望再次運行同一個消費者能得到相同的結果。
|
||||
|
||||
如果你將新的消費者新增到訊息系統,通常只能接收到消費者註冊之後開始傳送的訊息。先前的任何訊息都隨風而逝,一去不復返。作為對比,你可以隨時為檔案和資料庫新增新的客戶端,且能讀取任意久遠的資料(只要應用沒有顯式覆蓋或刪除這些資料)。
|
||||
如果你將新的消費者新增到訊息傳遞系統,通常只能接收到消費者註冊之後開始傳送的訊息。先前的任何訊息都隨風而逝,一去不復返。作為對比,你可以隨時為檔案和資料庫新增新的客戶端,且能讀取任意久遠的資料(只要應用沒有顯式覆蓋或刪除這些資料)。
|
||||
|
||||
為什麼我們不能把它倆雜交一下,既有資料庫的持久儲存方式,又有訊息傳遞的低延遲通知?這就是**基於日誌的訊息代理(log-based message brokers)** 背後的想法。
|
||||
|
||||
@ -142,7 +142,7 @@
|
||||
|
||||
同樣的結構可以用於實現訊息代理:生產者透過將訊息追加到日誌末尾來發送訊息,而消費者透過依次讀取日誌來接收訊息。如果消費者讀到日誌末尾,則會等待新訊息追加的通知。 Unix工具`tail -f` 能監視檔案被追加寫入的資料,基本上就是這樣工作的。
|
||||
|
||||
為了伸縮超出單個磁碟所能提供的更高吞吐量,可以對日誌進行**分割槽**(在[第6章](ch6.md)的意義上)。不同的分割槽可以託管在不同的機器上,且每個分割槽都拆分出一份能獨立於其他分割槽進行讀寫的日誌。一個主題可以定義為一組攜帶相同型別訊息的分割槽。這種方法如[圖11-3](../img/fig11-3.png)所示。
|
||||
為了伸縮超出單個磁碟所能提供的更高吞吐量,可以對日誌進行**分割槽**(按[第6章](ch6.md)的定義)。不同的分割槽可以託管在不同的機器上,使得每個分割槽都有一份能獨立於其他分割槽進行讀寫的日誌。一個主題可以定義為一組攜帶相同型別訊息的分割槽。這種方法如[圖11-3](../img/fig11-3.png)所示。
|
||||
|
||||
在每個分割槽內,代理為每個訊息分配一個單調遞增的序列號或**偏移量(offset)**(在[圖11-3](../img/fig11-3.png)中,框中的數字是訊息偏移量)。這種序列號是有意義的,因為分割槽是僅追加寫入的,所以分割槽內的訊息是完全有序的。沒有跨不同分割槽的順序保證。
|
||||
|
||||
@ -156,20 +156,20 @@
|
||||
|
||||
基於日誌的方法天然支援扇出式訊息傳遞,因為多個消費者可以獨立讀取日誌,而不會相互影響 —— 讀取訊息不會將其從日誌中刪除。為了在一組消費者之間實現負載平衡,代理可以將整個分割槽分配給消費者組中的節點,而不是將單條訊息分配給消費者客戶端。
|
||||
|
||||
每個客戶端消費指派分割槽中的**所有**訊息。然後使用分配的分割槽中的所有訊息。通常情況下,當一個使用者被指派了一個日誌分割槽時,它會以簡單的單執行緒方式順序地讀取分割槽中的訊息。這種粗粒度的負載均衡方法有一些缺點:
|
||||
然後每個客戶端將消費被指派分割槽中的**所有**訊息。通常情況下,當一個使用者被指派了一個日誌分割槽時,它會以簡單的單執行緒方式順序地讀取分割槽中的訊息。這種粗粒度的負載均衡方法有一些缺點:
|
||||
|
||||
* 共享消費主題工作的節點數,最多為該主題中的日誌分割槽數,因為同一個分割槽內的所有訊息被遞送到同一個節點[^i]。
|
||||
* 如果某條訊息處理緩慢,則它會阻塞該分割槽中後續訊息的處理(一種行首阻塞的形式;請參閱“[描述效能](ch1.md#描述效能)”)。
|
||||
|
||||
因此在訊息處理代價高昂,希望逐條並行處理,以及訊息的順序並沒有那麼重要的情況下,JMS/AMQP風格的訊息代理是可取的。另一方面,在訊息吞吐量很高,處理迅速,順序很重要的情況下,基於日誌的方法表現得非常好。
|
||||
|
||||
[^i]: 設計一種負載均衡方案是可行的,在這種方案中,兩個消費者透過讀取全部訊息來共享處理分割槽的工作,但是其中一個只考慮具有偶數偏移量的訊息,而另一個消費者只處理奇數編號的偏移量。或者你可以將訊息攤到一個執行緒池中來處理,但這種方法會使消費者偏移量管理變得複雜。一般來說,單執行緒處理單分割槽是合適的,可以透過增加更多分割槽來提高並行度。
|
||||
[^i]: 要設計一種負載均衡方案也是有可能的,在這種方案中,兩個消費者透過讀取全部訊息來共享分割槽處理的工作,但是其中一個只考慮具有偶數偏移量的訊息,而另一個消費者只處理奇數編號的偏移量。或者你可以將訊息攤到一個執行緒池中來處理,但這種方法會使消費者偏移量管理變得複雜。一般來說,單執行緒處理單分割槽是合適的,可以透過增加更多分割槽來提高並行度。
|
||||
|
||||
#### 消費者偏移量
|
||||
|
||||
順序消費一個分割槽使得判斷訊息是否已經被處理變得相當容易:所有偏移量小於消費者的當前偏移量的訊息已經被處理,而具有更大偏移量的訊息還沒有被看到。因此,代理不需要跟蹤確認每條訊息,只需要定期記錄消費者的偏移即可。在這種方法減少了額外簿記開銷,而且在批處理和流處理中採用這種方法有助於提高基於日誌的系統的吞吐量。
|
||||
順序消費一個分割槽使得判斷訊息是否已經被處理變得相當容易:所有偏移量小於消費者的當前偏移量的訊息已經被處理,而具有更大偏移量的訊息還沒有被看到。因此,代理不需要跟蹤確認每條訊息,只需要定期記錄消費者的偏移即可。這種方法減少了額外簿記開銷,而且在批處理和流處理中採用這種方法有助於提高基於日誌的系統的吞吐量。
|
||||
|
||||
實際上,這種偏移量與單領導者資料庫複製中常見的日誌序列號非常相似,我們在“[設定新從庫](ch5.md#設定新從庫)”中討論了這種情況。在資料庫複製中,日誌序列號允許跟隨者斷開連線後,重新連線到領導者,並在不跳過任何寫入的情況下恢復複製。這裡原理完全相同:訊息代理的表現得像一個主庫,而消費者就像一個從庫。
|
||||
實際上,這種偏移量與單領導者資料庫複製中常見的日誌序列號非常相似,我們在“[設定新從庫](ch5.md#設定新從庫)”中討論了這種情況。在資料庫複製中,日誌序列號允許跟隨者斷開連線後,重新連線到領導者,並在不跳過任何寫入的情況下恢復複製。這裡原理完全相同:訊息代理表現得像一個主庫,而消費者就像一個從庫。
|
||||
|
||||
如果消費者節點失效,則失效消費者的分割槽將指派給其他節點,並從最後記錄的偏移量開始消費訊息。如果消費者已經處理了後續的訊息,但還沒有記錄它們的偏移量,那麼重啟後這些訊息將被處理兩次。我們將在本章後面討論這個問題的處理方法。
|
||||
|
||||
@ -177,7 +177,7 @@
|
||||
|
||||
如果只追加寫入日誌,則磁碟空間終究會耗盡。為了回收磁碟空間,日誌實際上被分割成段,並不時地將舊段刪除或移動到歸檔儲存。 (我們將在後面討論一種更為複雜的磁碟空間釋放方式)
|
||||
|
||||
這就意味著如果一個慢消費者跟不上訊息產生的速率而落後的太多,它的消費偏移量指向了刪除的段,那麼它就會錯過一些訊息。實際上,日誌實現了一個有限大小的緩衝區,當緩衝區填滿時會丟棄舊訊息,它也被稱為**迴圈緩衝區(circular buffer)**或**環形緩衝區(ring buffer)**。不過由於緩衝區在磁碟上,因此可能相當的大。
|
||||
這就意味著如果一個慢消費者跟不上訊息產生的速率而落後得太多,它的消費偏移量指向了刪除的段,那麼它就會錯過一些訊息。實際上,日誌實現了一個有限大小的緩衝區,當緩衝區填滿時會丟棄舊訊息,它也被稱為**迴圈緩衝區(circular buffer)**或**環形緩衝區(ring buffer)**。不過由於緩衝區在磁碟上,因此緩衝區可能相當的大。
|
||||
|
||||
讓我們做個簡單計算。在撰寫本文時,典型的大型硬碟容量為6TB,順序寫入吞吐量為150MB/s。如果以最快的速度寫訊息,則需要大約11個小時才能填滿磁碟。因而磁碟可以緩衝11個小時的訊息,之後它將開始覆蓋舊的訊息。即使使用多個磁碟和機器,這個比率也是一樣的。實踐中的部署很少能用滿磁碟的寫入頻寬,所以通常可以儲存一個幾天甚至幾周的日誌緩衝區。
|
||||
|
||||
@ -185,15 +185,15 @@
|
||||
|
||||
#### 當消費者跟不上生產者時
|
||||
|
||||
在“[訊息傳遞系統](#訊息傳遞系統)”中,如果消費者無法跟上生產者傳送資訊的速度時,我們討論了三種選擇:丟棄資訊,進行緩衝或施加背壓。在這種分類法裡,基於日誌的方法是緩衝的一種形式,具有很大,但大小固定的緩衝區(受可用磁碟空間的限制)。
|
||||
在“[訊息傳遞系統](#訊息傳遞系統)”中,如果消費者無法跟上生產者傳送資訊的速度時,我們討論了三種選擇:丟棄資訊,進行緩衝或施加背壓。在這種分類法裡,基於日誌的方法是緩衝的一種形式,具有很大但大小固定的緩衝區(受可用磁碟空間的限制)。
|
||||
|
||||
如果消費者遠遠落後,而所要求的資訊比保留在磁碟上的資訊還要舊,那麼它將不能讀取這些資訊,所以代理實際上丟棄了比緩衝區容量更大的舊資訊。你可以監控消費者落後日誌頭部的距離,如果落後太多就發出報警。由於緩衝區很大,因而有足夠的時間讓人類運維來修復慢消費者,並在訊息開始丟失之前讓其趕上。
|
||||
如果消費者遠遠落後,而所要求的資訊比保留在磁碟上的資訊還要舊,那麼它將不能讀取這些資訊,所以代理實際上丟棄了比緩衝區容量更大的舊資訊。你可以監控消費者落後日誌頭部的距離,如果落後太多就發出報警。由於緩衝區很大,因而有足夠的時間讓運維人員來修復慢消費者,並在訊息開始丟失之前讓其趕上。
|
||||
|
||||
即使消費者真的落後太多開始丟失訊息,也只有那個消費者受到影響;它不會中斷其他消費者的服務。這是一個巨大的運維優勢:你可以實驗性地消費生產日誌,以進行開發,測試或除錯,而不必擔心會中斷生產服務。當消費者關閉或崩潰時,會停止消耗資源,唯一剩下的只有消費者偏移量。
|
||||
|
||||
這種行為也與傳統的資訊代理形成了鮮明對比,在那種情況下,你需要小心地刪除那些消費者已經關閉的佇列—— 否則那些佇列就會累積不必要的訊息,從其他仍活躍的消費者那裡佔走記憶體。
|
||||
這種行為也與傳統的訊息代理形成了鮮明對比,在那種情況下,你需要小心地刪除那些消費者已經關閉的佇列—— 否則那些佇列就會累積不必要的訊息,從其他仍活躍的消費者那裡佔走記憶體。
|
||||
|
||||
#### 重播舊資訊
|
||||
#### 重播舊訊息
|
||||
|
||||
我們之前提到,使用AMQP和JMS風格的訊息代理,處理和確認訊息是一個破壞性的操作,因為它會導致訊息在代理上被刪除。另一方面,在基於日誌的訊息代理中,使用訊息更像是從檔案中讀取資料:這是隻讀操作,不會更改日誌。
|
||||
|
||||
@ -203,23 +203,23 @@
|
||||
|
||||
|
||||
|
||||
## 流與資料庫
|
||||
## 資料庫與流
|
||||
|
||||
我們已經在訊息代理和資料庫之間進行了一些比較。儘管傳統上它們被視為單獨的工具類別,但是我們看到基於日誌的訊息代理已經成功地從資料庫中獲取靈感並將其應用於訊息傳遞。我們也可以反過來:從訊息傳遞和流中獲取靈感,並將它們應用於資料庫。
|
||||
|
||||
我們之前曾經說過,事件是某個時刻發生的事情的記錄。發生的事情可能是使用者操作(例如鍵入搜尋查詢)或讀取感測器,但也可能是**寫入資料庫**。某些東西被寫入資料庫的事實是可以被捕獲,儲存和處理的事件。這一觀察結果表明,資料庫和資料流之間的聯絡不僅僅是磁碟日誌的物理儲存 —— 而是更深層的聯絡。
|
||||
我們之前曾經說過,事件是某個時刻發生的事情的記錄。發生的事情可能是使用者操作(例如鍵入搜尋查詢)或讀取感測器,但也可能是**寫入資料庫**。某些東西被寫入資料庫的事實是可以被捕獲、儲存和處理的事件。這一觀察結果表明,資料庫和資料流之間的聯絡不僅僅是磁碟日誌的物理儲存 —— 而是更深層的聯絡。
|
||||
|
||||
事實上,複製日誌(參閱“[複製日誌的實現](ch5.md#複製日誌的實現)”)是資料庫寫入事件的流,由主庫在處理事務時生成。從庫將寫入流應用到它們自己的資料庫副本,從而最終得到相同資料的精確副本。複製日誌中的事件描述發生的資料更改。
|
||||
事實上,複製日誌(參閱“[複製日誌的實現](ch5.md#複製日誌的實現)”)是一個由資料庫寫入事件組成的流,由主庫在處理事務時生成。從庫將寫入流應用到它們自己的資料庫副本,從而最終得到相同資料的精確副本。複製日誌中的事件描述發生的資料更改。
|
||||
|
||||
我們還在“[全序廣播](ch9.md#全序廣播)”中遇到了狀態機複製原理,其中指出:如果每個事件代表對資料庫的寫入,並且每個副本按相同的順序處理相同的事件,則副本將達到相同的最終狀態 (假設處理一個事件是一個確定性的操作)。這是事件流的又一種場景!
|
||||
我們還在“[全序廣播](ch9.md#全序廣播)”中遇到了狀態機複製原理,其中指出:如果每個事件代表對資料庫的寫入,並且每個副本按相同的順序處理相同的事件,則副本將達到相同的最終狀態 (假設事件處理是一個確定性的操作)。這是事件流的又一種場景!
|
||||
|
||||
在本節中,我們將首先看看異構資料系統中出現的一個問題,然後探討如何透過將事件流的想法帶入資料庫來解決這個問題。
|
||||
|
||||
### 保持系統同步
|
||||
|
||||
正如我們在本書中所看到的,沒有一個系統能夠滿足所有的資料儲存,查詢和處理需求。在實踐中,大多數重要應用都需要組合使用幾種不同的技術來滿足所有的需求:例如,使用OLTP資料庫來為使用者請求提供服務,使用快取來加速常見請求,使用全文索引搜尋處理搜尋查詢,使用資料倉庫用於分析。每一個元件都有自己的資料副本,以自己的表示儲存,並根據自己的目的進行最佳化。
|
||||
正如我們在本書中所看到的,沒有一個系統能夠滿足所有的資料儲存、查詢和處理需求。在實踐中,大多數重要應用都需要組合使用幾種不同的技術來滿足所有的需求:例如,使用OLTP資料庫來為使用者請求提供服務,使用快取來加速常見請求,使用全文索引來處理搜尋查詢,使用資料倉庫用於分析。每一種技術都有自己的資料副本,並根據自己的目的進行儲存方式的最佳化。
|
||||
|
||||
由於相同或相關的資料出現在了不同的地方,因此相互間需要保持同步:如果某個專案在資料庫中被更新,它也應當在快取,搜尋索引和資料倉庫中被更新。對於資料倉庫,這種同步通常由ETL程序執行(參見“[資料倉庫](ch3.md#資料倉庫)”),通常是先取得資料庫的完整副本,然後執行轉換,並批次載入到資料倉庫中 —— 換句話說,批處理。我們在“[批次工作流的輸出](ch10.md#批次工作流的輸出)”中同樣看到了如何使用批處理建立搜尋索引,推薦系統和其他衍生資料系統。
|
||||
由於相同或相關的資料出現在了不同的地方,因此相互間需要保持同步:如果某個專案在資料庫中被更新,它也應當在快取、搜尋索引和資料倉庫中被更新。對於資料倉庫,這種同步通常由ETL程序執行(參見“[資料倉庫](ch3.md#資料倉庫)”),通常是先取得資料庫的完整副本,然後執行轉換,並批次載入到資料倉庫中 —— 換句話說,批處理。我們在“[批處理工作流的輸出](ch10.md#批處理工作流的輸出)”中同樣看到了如何使用批處理建立搜尋索引、推薦系統和其他衍生資料系統。
|
||||
|
||||
如果週期性的完整資料庫轉儲過於緩慢,有時會使用的替代方法是**雙寫(dual write)**,其中應用程式碼在資料變更時明確寫入每個系統:例如,首先寫入資料庫,然後更新搜尋索引,然後使快取項失效(甚至同時執行這些寫入)。
|
||||
|
||||
@ -231,9 +231,9 @@
|
||||
|
||||
除非有一些額外的併發檢測機制,例如我們在“[檢測併發寫入](ch5.md#檢測併發寫入)”中討論的版本向量,否則你甚至不會意識到發生了併發寫入 —— 一個值將簡單地以無提示方式覆蓋另一個值。
|
||||
|
||||
雙重寫入的另一個問題是,其中一個寫入可能會失敗,而另一個成功。這是一個容錯問題,而不是一個併發問題,但也會造成兩個系統互相不一致的結果。確保它們要麼都成功要麼都失敗,是原子提交問題的一個例子,解決這個問題的代價是昂貴的(參閱“[原子提交和兩階段提交(2PC)](ch7.md#原子提交和兩階段提交(2PC))”)。
|
||||
雙重寫入的另一個問題是,其中一個寫入可能會失敗,而另一個成功。這是一個容錯問題,而不是一個併發問題,但也會造成兩個系統互相不一致的結果。確保它們要麼都成功要麼都失敗,是原子提交問題的一個例子,解決這個問題的代價是昂貴的(參閱“[原子提交與兩階段提交(2PC)](ch7.md#原子提交與兩階段提交(2PC))”)。
|
||||
|
||||
如果你只有一個單領導者複製的資料庫,那麼這個領導者決定了寫入順序,而狀態機複製方法可以在資料庫副本上工作。然而,在[圖11-4](../img/fig11-4.png)中,沒有單個主庫:資料庫可能有一個領導者,搜尋索引也可能有一個領導者,但是兩者都不追隨對方,所以可能會發生衝突(參見“[多領導者複製](ch5.md#多領導者複製)“)。
|
||||
如果你只有一個單領導者複製的資料庫,那麼這個領導者決定了寫入順序,而狀態機複製方法可以在資料庫副本上工作。然而,在[圖11-4](../img/fig11-4.png)中,沒有單個主庫:資料庫可能有一個領導者,搜尋索引也可能有一個領導者,但是兩者都不追隨對方,所以可能會發生衝突(參見“[多主複製](ch5.md#多主複製)“)。
|
||||
|
||||
如果實際上只有一個領導者 —— 例如,資料庫 —— 而且我們能讓搜尋索引成為資料庫的追隨者,情況要好得多。但這在實踐中可能嗎?
|
||||
|
||||
@ -241,7 +241,7 @@
|
||||
|
||||
大多數資料庫的複製日誌的問題在於,它們一直被當做資料庫的內部實現細節,而不是公開的API。客戶端應該透過其資料模型和查詢語言來查詢資料庫,而不是解析複製日誌並嘗試從中提取資料。
|
||||
|
||||
數十年來,許多資料庫根本沒有記錄在檔的,獲取變更日誌的方式。由於這個原因,捕獲資料庫中所有的變更,然後將其複製到其他儲存技術(搜尋索引,快取,資料倉庫)中是相當困難的。
|
||||
數十年來,許多資料庫根本沒有記錄在檔的獲取變更日誌的方式。由於這個原因,捕獲資料庫中所有的變更,然後將其複製到其他儲存技術(搜尋索引、快取或資料倉庫)中是相當困難的。
|
||||
|
||||
最近,人們對**變更資料捕獲(change data capture, CDC)** 越來越感興趣,這是一種觀察寫入資料庫的所有資料變更,並將其提取並轉換為可以複製到其他系統中的形式的過程。 CDC是非常有意思的,尤其是當變更能在被寫入後立刻用於流時。
|
||||
|
||||
@ -253,21 +253,21 @@
|
||||
|
||||
#### 變更資料捕獲的實現
|
||||
|
||||
我們可以將日誌消費者叫做**衍生資料系統**,正如在第三部分的[介紹](part-iii.md)中所討論的:儲存在搜尋索引和資料倉庫中的資料,只是**記錄系統**資料的額外檢視。變更資料捕獲是一種機制,可確保對記錄系統所做的所有更改都反映在衍生資料系統中,以便衍生系統具有資料的準確副本。
|
||||
我們可以將日誌消費者叫做**衍生資料系統**,正如在[第三部分](part-iii.md)的介紹中所討論的:儲存在搜尋索引和資料倉庫中的資料,只是**記錄系統**資料的額外檢視。變更資料捕獲是一種機制,可確保對記錄系統所做的所有更改都反映在衍生資料系統中,以便衍生系統具有資料的準確副本。
|
||||
|
||||
從本質上說,變更資料捕獲使得一個數據庫成為領導者(被捕獲變化的資料庫),並將其他元件變為追隨者。基於日誌的訊息代理非常適合從源資料庫傳輸變更事件,因為它保留了訊息的順序(避免了[圖11-2](../img/fig11-2.png)的重新排序問題)。
|
||||
|
||||
資料庫觸發器可用來實現變更資料捕獲(參閱“[基於觸發器的複製](ch5.md#基於觸發器的複製)”),透過註冊觀察所有變更的觸發器,並將相應的變更項寫入變更日誌表中。但是它們往往是脆弱的,而且有顯著的效能開銷。解析複製日誌可能是一種更穩健的方法,但它也很有挑戰,例如應對模式變更。
|
||||
資料庫觸發器可用來實現變更資料捕獲(參閱“[基於觸發器的複製](ch5.md#基於觸發器的複製)”),透過註冊觀察所有變更的觸發器,並將相應的變更項寫入變更日誌表中。但是它們往往是脆弱的,而且有顯著的效能開銷。解析複製日誌可能是一種更穩健的方法,但它也很有挑戰,例如如何應對模式變更。
|
||||
|
||||
LinkedIn的Databus 【25】,Facebook的Wormhole 【26】和Yahoo!的Sherpa【27】大規模地應用這個思路。 Bottled Water使用解碼WAL的API實現了PostgreSQL的CDC 【28】,Maxwell和Debezium透過解析binlog對MySQL做了類似的事情【29,30,31】,Mongoriver讀取MongoDB oplog 【32,33】 ,而GoldenGate為Oracle提供類似的功能【34,35】。
|
||||
LinkedIn的Databus【25】,Facebook的Wormhole【26】和Yahoo!的Sherpa【27】大規模地應用這個思路。 Bottled Water使用解碼WAL的API實現了PostgreSQL的CDC【28】,Maxwell和Debezium透過解析binlog對MySQL做了類似的事情【29,30,31】,Mongoriver讀取MongoDB oplog【32,33】,而GoldenGate為Oracle提供類似的功能【34,35】。
|
||||
|
||||
像訊息代理一樣,變更資料捕獲通常是非同步的:記錄資料庫系統不會等待消費者應用變更再進行提交。這種設計具有的運維優勢是,新增緩慢的消費者不會過度影響記錄系統。不過,所有複製延遲可能有的問題在這裡都可能出現(參見“[複製延遲問題](ch5.md#複製延遲問題)”)。
|
||||
|
||||
#### 初始快照
|
||||
|
||||
如果你擁有**所有**對資料庫進行變更的日誌,則可以透過重放該日誌,來重建資料庫的完整狀態。但是在許多情況下,永遠保留所有更改會耗費太多磁碟空間,且重放過於費時,因此日誌需要被截斷。
|
||||
如果你擁有**所有**對資料庫進行變更的日誌,則可以透過重播該日誌,來重建資料庫的完整狀態。但是在許多情況下,永遠保留所有更改會耗費太多磁碟空間,且重播過於費時,因此日誌需要被截斷。
|
||||
|
||||
例如,構建新的全文索引需要整個資料庫的完整副本 —— 僅僅應用最近變更的日誌是不夠的,因為這樣會丟失最近未曾更新的專案。因此,如果你沒有完整的歷史日誌,則需要從一個一致的快照開始,如先前上的“[設定新的從庫](ch5.md#設定新的從庫)”中所述。
|
||||
例如,構建新的全文索引需要整個資料庫的完整副本 —— 僅僅應用最近變更的日誌是不夠的,因為這樣會丟失最近未曾更新的專案。因此,如果你沒有完整的歷史日誌,則需要從一個一致的快照開始,如先前的“[設定新從庫](ch5.md#設定新從庫)”中所述。
|
||||
|
||||
資料庫的快照必須與變更日誌中的已知位置或偏移量相對應,以便在處理完快照後知道從哪裡開始應用變更。一些CDC工具集成了這種快照功能,而其他工具則把它留給你手動執行。
|
||||
|
||||
@ -275,38 +275,38 @@
|
||||
|
||||
如果你只能保留有限的歷史日誌,則每次要新增新的衍生資料系統時,都需要做一次快照。但**日誌壓縮(log compaction)** 提供了一個很好的備選方案。
|
||||
|
||||
我們之前在日誌結構儲存引擎的上下文中討論了“[Hash索引](ch3.md#Hash索引)”中的日誌壓縮(參見[圖3-2](../img/fig3-2.png)的示例)。原理很簡單:儲存引擎定期在日誌中查詢具有相同鍵的記錄,丟掉所有重複的內容,並只保留每個鍵的最新更新。這個壓縮與合併過程在後臺執行。
|
||||
我們之前在“[雜湊索引](ch3.md#雜湊索引)”中關於日誌結構儲存引擎的上下文中討論了日誌壓縮(參見[圖3-2](../img/fig3-2.png)的示例)。原理很簡單:儲存引擎定期在日誌中查詢具有相同鍵的記錄,丟掉所有重複的內容,並只保留每個鍵的最新更新。這個壓縮與合併過程在後臺執行。
|
||||
|
||||
在日誌結構儲存引擎中,具有特殊值NULL(**墓碑(tombstone)**)的更新表示該鍵被刪除,並會在日誌壓縮過程中被移除。但只要鍵不被覆蓋或刪除,它就會永遠留在日誌中。這種壓縮日誌所需的磁碟空間僅取決於資料庫的當前內容,而不取決於資料庫中曾經發生的寫入次數。如果相同的鍵經常被覆蓋寫入,則先前的值將最終將被垃圾回收,只有最新的值會保留下來。
|
||||
|
||||
在基於日誌的訊息代理與變更資料捕獲的上下文中也適用相同的想法。如果CDC系統被配置為,每個變更都包含一個主鍵,且每個鍵的更新都替換了該鍵以前的值,那麼只需要保留對鍵的最新寫入就足夠了。
|
||||
|
||||
現在,無論何時需要重建衍生資料系統(如搜尋索引),你可以從壓縮日誌主題0偏移量處啟動新的消費者,然後依次掃描日誌中的所有訊息。日誌能保證包含資料庫中每個鍵的最新值(也可能是一些較舊的值)—— 換句話說,你可以使用它來獲取資料庫內容的完整副本,而無需從CDC源資料庫取一個快照。
|
||||
現在,無論何時需要重建衍生資料系統(如搜尋索引),你可以從壓縮日誌主題的零偏移量處啟動新的消費者,然後依次掃描日誌中的所有訊息。日誌能保證包含資料庫中每個鍵的最新值(也可能是一些較舊的值)—— 換句話說,你可以使用它來獲取資料庫內容的完整副本,而無需從CDC源資料庫取一個快照。
|
||||
|
||||
Apache Kafka支援這種日誌壓縮功能。正如我們將在本章後面看到的,它允許訊息代理被當成永續性儲存使用,而不僅僅是用於臨時訊息。
|
||||
|
||||
#### 變更流的API支援
|
||||
|
||||
越來越多的資料庫開始將變更流作為第一類的介面,而不像傳統上要去做加裝改造,費工夫逆向工程一個CDC。例如,RethinkDB允許查詢訂閱通知,當查詢結果變更時獲得通知【36】,Firebase 【37】和CouchDB 【38】基於變更流進行同步,該變更流同樣可用於應用。而Meteor使用MongoDB oplog訂閱資料變更,並改變了使用者介面【39】。
|
||||
越來越多的資料庫開始將變更流作為第一等的介面,而不像傳統上要去做加裝改造,或者費工夫逆向工程一個CDC。例如,RethinkDB允許查詢訂閱通知,當查詢結果變更時獲得通知【36】,Firebase 【37】和CouchDB 【38】基於變更流進行同步,該變更流同樣可用於應用。而Meteor使用MongoDB oplog訂閱資料變更,並改變了使用者介面【39】。
|
||||
|
||||
VoltDB允許事務以流的形式連續地從資料庫中匯出資料【40】。資料庫將關係資料模型中的輸出流表示為一個表,事務可以向其中插入元組,但不能查詢。已提交事務按照提交順序寫入這個特殊表,而流則由該表中的元組日誌構成。外部消費者可以非同步消費該日誌,並使用它來更新衍生資料系統。
|
||||
|
||||
Kafka Connect 【41】致力於將廣泛的資料庫系統的變更資料捕獲工具與Kafka整合。一旦變更事件進入Kafka中,它就可以用於更新衍生資料系統,比如搜尋索引,也可以用於本章稍後討論的流處理系統。
|
||||
Kafka Connect【41】致力於將廣泛的資料庫系統的變更資料捕獲工具與Kafka整合。一旦變更事件進入Kafka中,它就可以用於更新衍生資料系統,比如搜尋索引,也可以用於本章稍後討論的流處理系統。
|
||||
|
||||
### 事件溯源
|
||||
|
||||
我們在這裡討論的想法和**事件溯源( Event Sourcing)** 之間有一些相似之處,這是一個在 **領域驅動設計(domain-driven design, DDD)** 社群中折騰出來的技術。我們將簡要討論事件溯源,因為它包含了一些關於流處理系統的有用想法。
|
||||
我們在這裡討論的想法和**事件溯源(Event Sourcing)** 之間有一些相似之處,這是一個在 **領域驅動設計(domain-driven design, DDD)** 社群中折騰出來的技術。我們將簡要討論事件溯源,因為它包含了一些關於流處理系統的有用想法。
|
||||
|
||||
與變更資料捕獲類似,事件溯源涉及到**將所有對應用狀態的變更** 儲存為變更事件日誌。最大的區別是事件溯源將這一想法應用到了幾個不同的抽象層次上:
|
||||
與變更資料捕獲類似,事件溯源涉及到**將所有對應用狀態的變更**儲存為變更事件日誌。最大的區別是事件溯源將這一想法應用到了一個不同的抽象層次上:
|
||||
|
||||
* 在變更資料捕獲中,應用以**可變方式(mutable way)** 使用資料庫,任意更新和刪除記錄。變更日誌是從資料庫的底層提取的(例如,透過解析複製日誌),從而確保從資料庫中提取的寫入順序與實際寫入的順序相匹配,從而避免[圖11-4](../img/fig11-4.png)中的競態條件。寫入資料庫的應用不需要知道CDC的存在。
|
||||
* 在變更資料捕獲中,應用以**可變方式(mutable way)**使用資料庫,可以任意更新和刪除記錄。變更日誌是從資料庫的底層提取的(例如,透過解析複製日誌),從而確保從資料庫中提取的寫入順序與實際寫入的順序相匹配,從而避免[圖11-4](../img/fig11-4.png)中的競態條件。寫入資料庫的應用不需要知道CDC的存在。
|
||||
* 在事件溯源中,應用邏輯顯式構建在寫入事件日誌的不可變事件之上。在這種情況下,事件儲存是僅追加寫入的,更新與刪除是不鼓勵的或禁止的。事件被設計為旨在反映應用層面發生的事情,而不是底層的狀態變更。
|
||||
|
||||
事件源是一種強大的資料建模技術:從應用的角度來看,將使用者的行為記錄為不可變的事件更有意義,而不是在可變資料庫中記錄這些行為的影響。事件代理使得應用隨時間演化更為容易,透過事實更容易理解事情發生的原因,使得除錯更為容易,並有利於防止應用Bug(請參閱“[不可變事件的優點](#不可變事件的優點)”)。
|
||||
事件溯源是一種強大的資料建模技術:從應用的角度來看,將使用者的行為記錄為不可變的事件更有意義,而不是在可變資料庫中記錄這些行為的影響。事件溯源使得應用隨時間演化更為容易,透過更容易理解事情發生的原因來幫助除錯的進行,並有利於防止應用Bug(請參閱“[不可變事件的優點](#不可變事件的優點)”)。
|
||||
|
||||
例如,儲存“學生取消選課”事件以中性的方式清楚地表達了單個行為的意圖,而副作用“從登錄檔中刪除了一個條目,而一條取消原因被新增到學生反饋表“則嵌入了很多有關稍後資料使用方式的假設。如果引入一個新的應用功能,例如“將位置留給等待列表中的下一個人” —— 事件溯源方法允許將新的副作用輕鬆地連結至現有事件之後。
|
||||
例如,儲存“學生取消選課”事件以中性的方式清楚地表達了單個行為的意圖,而其副作用“從登記表中刪除了一個條目,而一條取消原因的記錄被新增到學生反饋表“則嵌入了很多有關稍後對資料的使用方式的假設。如果引入一個新的應用功能,例如“將位置留給等待列表中的下一個人” —— 事件溯源方法允許將新的副作用輕鬆地從現有事件中脫開。
|
||||
|
||||
事件溯源類似於**編年史(chronicle)** 資料模型【45】,事件日誌與星型模式中的事實表之間也存在相似之處(參閱“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”) 。
|
||||
事件溯源類似於**編年史(chronicle)**資料模型【45】,事件日誌與星型模式中的事實表之間也存在相似之處(參閱“[星型和雪花型:分析的模式](ch3.md#星型和雪花型:分析的模式)”) 。
|
||||
|
||||
諸如Event Store【46】這樣的專業資料庫已經被開發出來,供使用事件溯源的應用使用,但總的來說,這種方法獨立於任何特定的工具。傳統的資料庫或基於日誌的訊息代理也可以用來構建這種風格的應用。
|
||||
|
||||
@ -316,34 +316,34 @@
|
||||
|
||||
因此,使用事件溯源的應用需要拉取事件日誌(表示**寫入**系統的資料),並將其轉換為適合向用戶顯示的應用狀態(從系統**讀取**資料的方式【47】)。這種轉換可以使用任意邏輯,但它應當是確定性的,以便能再次執行,並從事件日誌中衍生出相同的應用狀態。
|
||||
|
||||
與變更資料捕獲一樣,重放事件日誌允許讓你重新構建系統的當前狀態。不過,日誌壓縮需要採用不同的方式處理:
|
||||
與變更資料捕獲一樣,重播事件日誌允許讓你重新構建系統的當前狀態。不過,日誌壓縮需要採用不同的方式處理:
|
||||
|
||||
* 用於記錄更新的CDC事件通常包含記錄的**完整新版本**,因此主鍵的當前值完全由該主鍵的最近事件確定,而日誌壓縮可以丟棄相同主鍵的先前事件。
|
||||
* 另一方面,事件溯源在更高層次進行建模:事件通常表示使用者操作的意圖,而不是因為操作而發生的狀態更新機制。在這種情況下,後面的事件通常不會覆蓋先前的事件,所以你需要完整的歷史事件來重新構建最終狀態。這裡進行同樣的日誌壓縮是不可能的。
|
||||
|
||||
使用事件溯源的應用通常有一些機制,用於儲存從事件日誌中匯出的當前狀態快照,因此它們不需要重複處理完整的日誌。然而這只是一種效能最佳化,用來加速讀取,提高從崩潰中恢復的速度;真正的目的是系統能夠永久儲存所有原始事件,並在需要時重新處理完整的事件日誌。我們將在“[不變性的限制](#不變性的限制)”中討論這個假設。
|
||||
使用事件溯源的應用通常有一些機制,用於儲存從事件日誌中匯出的當前狀態快照,因此它們不需要重複處理完整的日誌。然而這只是一種效能最佳化,用來加速讀取,提高從崩潰中恢復的速度;真正的目的是系統能夠永久儲存所有原始事件,並在需要時重新處理完整的事件日誌。我們將在“[不變性的限制](#不變性的限制)”中討論這個假設。
|
||||
|
||||
#### 命令和事件
|
||||
|
||||
事件溯源的哲學是仔細區分**事件(event)**和**命令(command)**【48】。當來自使用者的請求剛到達時,它一開始是一個命令:在這個時間點上它仍然可能可能失敗,比如,因為違反了一些完整性條件。應用必須首先驗證它是否可以執行該命令。如果驗證成功並且命令被接受,則它變為一個持久化且不可變的事件。
|
||||
|
||||
例如,如果使用者試圖註冊特定使用者名稱,或預定飛機或劇院的座位,則應用需要檢查使用者名稱或座位是否已被佔用。 (先前在“[容錯概念](ch8.md#容錯概念)”中討論過這個例子)當檢查成功時,應用可以生成一個事件,指示特定的使用者名稱是由特定的使用者ID註冊的,座位已經預留給特定的顧客。
|
||||
例如,如果使用者試圖註冊特定使用者名稱,或預定飛機或劇院的座位,則應用需要檢查使用者名稱或座位是否已被佔用。(先前在“[容錯共識](ch8.md#容錯共識)”中討論過這個例子)當檢查成功時,應用可以生成一個事件,指示特定的使用者名稱是由特定的使用者ID註冊的,或者座位已經預留給特定的顧客。
|
||||
|
||||
在事件生成的時刻,它就成為了**事實(fact)**。即使客戶稍後決定更改或取消預訂,他們之前曾預定了某個特定座位的事實仍然成立,而更改或取消是之後新增的單獨的事件。
|
||||
|
||||
事件流的消費者不允許拒絕事件:當消費者看到事件時,它已經成為日誌中不可變的一部分,並且可能已經被其他消費者看到了。因此任何對命令的驗證,都需要在它成為事件之前同步完成。例如,透過使用一個可自動驗證命令的可序列化事務來發布事件。
|
||||
事件流的消費者不允許拒絕事件:當消費者看到事件時,它已經成為日誌中不可變的一部分,並且可能已經被其他消費者看到了。因此任何對命令的驗證,都需要在它成為事件之前同步完成。例如,透過使用一個可以原子性地自動驗證命令併發布事件的可序列事務。
|
||||
|
||||
或者,預訂座位的使用者請求可以拆分為兩個事件:第一個是暫時預約,第二個是驗證預約後的獨立的確認事件(如“[使用全序廣播實現線性一致的儲存](ch9.md#使用全序廣播實現線性一致的儲存)”中所述) 。這種分割方式允許驗證發生在一個非同步的過程中。
|
||||
|
||||
### 狀態,流和不變性
|
||||
### 狀態、流和不變性
|
||||
|
||||
我們在[第10章](ch10.md)中看到,批處理因其輸入檔案不變性而受益良多,你可以在現有輸入檔案上執行實驗性處理作業,而不用擔心損壞它們。這種不變性原則也是使得事件溯源與變更資料捕獲如此強大的原因。
|
||||
|
||||
我們通常將資料庫視為應用程式當前狀態的儲存 —— 這種表示針對讀取進行了最佳化,而且通常對於服務查詢而言是最為方便的表示。狀態的本質是,它會變化,所以資料庫才會支援資料的增刪改。這又是如何符合不變性的呢?
|
||||
我們通常將資料庫視為應用程式當前狀態的儲存 —— 這種表示針對讀取進行了最佳化,而且通常對於服務查詢而言是最為方便的表示。狀態的本質是,它會變化,所以資料庫才會支援資料的增刪改。這又該如何匹配不變性呢?
|
||||
|
||||
只要你的狀態發生了變化,那麼這個狀態就是這段時間中事件修改的結果。例如,當前可用的座位列表是已處理預訂產生的結果,當前帳戶餘額是帳戶中的借與貸的結果,而Web伺服器的響應時間圖,是所有已發生Web請求的獨立響應時間的聚合結果。
|
||||
只要你的狀態發生了變化,那麼這個狀態就是這段時間中事件修改的結果。例如,當前可用的座位列表是你已處理的預訂所產生的結果,當前帳戶餘額是帳戶中的借與貸的結果,而Web伺服器的響應時間圖,是所有已發生Web請求的獨立響應時間的聚合結果。
|
||||
|
||||
無論狀態如何變化,總是有一系列事件導致了這些變化。即使事情已經執行與回滾,這些事件出現是始終成立的。關鍵的想法是:可變的狀態與不可變事件的僅追加日誌相互之間並不矛盾:它們是一體兩面,互為陰陽的。所有變化的日誌—— **變化日誌(change log)**,表示了隨時間演變的狀態。
|
||||
無論狀態如何變化,總是有一系列事件導致了這些變化。即使事情已經執行與回滾,這些事件出現是始終成立的。關鍵的想法是:可變的狀態與不可變事件的僅追加日誌相互之間並不矛盾:它們是一體兩面,互為陰陽的。所有變化的日誌—— **變化日誌(changelog)**,表示了隨時間演變的狀態。
|
||||
|
||||
如果你傾向於數學表示,那麼你可能會說,應用狀態是事件流對時間求積分得到的結果,而變更流是狀態對時間求微分的結果,如[圖11-6](../img/fig11-6.png)所示【49,50,51】。這個比喻有一些侷限性(例如,狀態的二階導似乎沒有意義),但這是考慮資料的一個實用出發點。
|
||||
$$
|
||||
@ -362,35 +362,35 @@ $$
|
||||
|
||||
#### 不可變事件的優點
|
||||
|
||||
資料庫中的不變性是一個古老的概念。例如,會計在幾個世紀以來一直在財務記賬中應用不變性。一筆交易發生時,它被記錄在一個僅追加寫入的分類帳中,實質上是描述貨幣,商品或服務轉手的事件日誌。賬目,比如利潤、虧損、資產負債表,是從分類賬中的交易求和衍生而來【53】。
|
||||
資料庫中的不變性是一個古老的概念。例如,會計在幾個世紀以來一直在財務記賬中應用不變性。一筆交易發生時,它被記錄在一個僅追加寫入的分類帳中,實質上是描述貨幣、商品或服務轉手的事件日誌。賬目,比如利潤、虧損、資產負債表,是從分類賬中的交易求和衍生而來【53】。
|
||||
|
||||
如果發生錯誤,會計師不會刪除或更改分類帳中的錯誤交易 —— 而是新增另一筆交易以補償錯誤,例如退還一比不正確的費用。不正確的交易將永遠保留在分類帳中,對於審計而言可能非常重要。如果從不正確的分類賬衍生出的錯誤數字已經公佈,那麼下一個會計週期的數字就會包括一個更正。這個過程在會計事務中是很常見的【54】。
|
||||
如果發生錯誤,會計師不會刪除或更改分類帳中的錯誤交易 —— 而是新增另一筆交易以補償錯誤,例如退還一筆不正確的費用。不正確的交易將永遠保留在分類帳中,對於審計而言可能非常重要。如果從不正確的分類賬衍生出的錯誤數字已經公佈,那麼下一個會計週期的數字就會包括一個更正。這個過程在會計事務中是很常見的【54】。
|
||||
|
||||
儘管這種可審計性在金融系統中尤其重要,但對於不受這種嚴格監管的許多其他系統,也是很有幫助的。如“[批處理輸出的哲學](ch10.md#批處理輸出的哲學)”中所討論的,如果你意外地部署了將錯誤資料寫入資料庫的錯誤程式碼,當代碼會破壞性地覆寫資料時,恢復要困難得多。使用不可變事件的僅追加日誌,診斷問題與故障恢復就要容易的多。
|
||||
儘管這種可審計性只在金融系統中尤其重要,但對於不受這種嚴格監管的許多其他系統,也是很有幫助的。如“[批處理輸出的哲學](ch10.md#批處理輸出的哲學)”中所討論的,如果你意外地部署了將錯誤資料寫入資料庫的錯誤程式碼,當代碼會破壞性地覆寫資料時,恢復要困難得多。使用不可變事件的僅追加日誌,診斷問題與故障恢復就要容易的多。
|
||||
|
||||
不可變的事件也包含了比當前狀態更多的資訊。例如在購物網站上,顧客可以將物品新增到他們的購物車,然後再將其移除。雖然從履行訂單的角度,第二個事件取消了第一個事件,但對分析目的而言,知道客戶考慮過某個特定項而之後又反悔,可能是很有用的。也許他們會選擇在未來購買,或者他們已經找到了替代品。這個資訊被記錄在事件日誌中,但對於移出購物車就刪除記錄的資料庫而言,這個資訊在移出購物車時可能就丟失【42】。
|
||||
不可變的事件也包含了比當前狀態更多的資訊。例如在購物網站上,顧客可以將物品新增到他們的購物車,然後再將其移除。雖然從履行訂單的角度,第二個事件取消了第一個事件,但對分析目的而言,知道客戶考慮過某個特定項而之後又反悔,可能是很有用的。也許他們會選擇在未來購買,或者他們已經找到了替代品。這個資訊被記錄在事件日誌中,但對於移出購物車就刪除記錄的資料庫而言,這個資訊在移出購物車時可能就丟失了【42】。
|
||||
|
||||
#### 從同一事件日誌中派生多個檢視
|
||||
|
||||
此外,透過從不變的事件日誌中分離出可變的狀態,你可以針對不同的讀取方式,從相同的事件日誌中衍生出幾種不同的表現形式。效果就像一個流的多個消費者一樣([圖11-5](../img/fig11-5.png)):例如,分析型資料庫Druid使用這種方式直接從Kafka攝取資料【55】,Pistachio是一個分散式的鍵值儲存,使用Kafka作為提交日誌【56】,Kafka Connect能將來自Kafka的資料匯出到各種不同的資料庫與索引【41】。這對於許多其他儲存和索引系統(如搜尋伺服器)來說是很有意義的,當系統要從分散式日誌中獲取輸入時亦然(參閱“[保持系統同步](#保持系統同步)”)。
|
||||
|
||||
新增從事件日誌到資料庫的顯式轉換,能夠使應用更容易地隨時間演進:如果你想要引入一個新功能,以新的方式表示現有資料,則可以使用事件日誌來構建一個單獨的,針對新功能的讀取最佳化檢視,無需修改現有系統而與之共存。並行執行新舊系統通常比在現有系統中執行復雜的模式遷移更容易。一旦不再需要舊的系統,你可以簡單地關閉它並回收其資源【47,57】。
|
||||
新增從事件日誌到資料庫的顯式轉換,能夠使應用更容易地隨時間演進:如果你想要引入一個新功能,以新的方式表示現有資料,則可以使用事件日誌來構建一個單獨的、針對新功能的讀取最佳化檢視,無需修改現有系統而與之共存。並行執行新舊系統通常比在現有系統中執行復雜的模式遷移更容易。一旦不再需要舊的系統,你可以簡單地關閉它並回收其資源【47,57】。
|
||||
|
||||
如果你不需要擔心如何查詢與訪問資料,那麼儲存資料通常是非常簡單的。模式設計,索引和儲存引擎的許多複雜性,都是希望支援某些特定查詢和訪問模式的結果(參見[第3章](ch3.md))。出於這個原因,透過將資料寫入的形式與讀取形式相分離,並允許幾個不同的讀取檢視,你能獲得很大的靈活性。這個想法有時被稱為**命令查詢責任分離(command query responsibility segregation, CQRS)**【42,58,59】。
|
||||
如果你不需要擔心如何查詢與訪問資料,那麼儲存資料通常是非常簡單的。模式設計、索引和儲存引擎的許多複雜性,都是希望支援某些特定查詢和訪問模式的結果(參見[第3章](ch3.md))。出於這個原因,透過將資料寫入的形式與讀取形式相分離,並允許幾個不同的讀取檢視,你能獲得很大的靈活性。這個想法有時被稱為**命令查詢責任分離(command query responsibility segregation, CQRS)**【42,58,59】。
|
||||
|
||||
資料庫和模式設計的傳統方法是基於這樣一種謬論,資料必須以與查詢相同的形式寫入。如果可以將資料從針對寫入最佳化的事件日誌轉換為針對讀取最佳化的應用狀態,那麼有關規範化和非規範化的爭論就變得無關緊要了(參閱“[多對一和多對多的關係](ch2.md#多對一和多對多的關係)”):在針對讀取最佳化的檢視中對資料進行非規範化是完全合理的,因為翻譯過程提供了使其與事件日誌保持一致的機制。
|
||||
|
||||
在“[描述負載](ch1.md#描述負載)”中,我們討論了推特主頁時間線,它是特定使用者關注人群所發推特的快取(類似郵箱)。這是**針對讀取最佳化的狀態**的又一個例子:主頁時間線是高度非規範化的,因為你的推文與所有粉絲的時間線都構成了重複。然而,扇出服務保持了這種重複狀態與新推特以及新關注關係的同步,從而保證了重複的可管理性。
|
||||
在“[描述負載](ch1.md#描述負載)”中,我們討論了推特主頁時間線,它是特定使用者關注的人群所發推特的快取(類似郵箱)。這是**針對讀取最佳化的狀態**的又一個例子:主頁時間線是高度非規範化的,因為你的推文與你所有粉絲的時間線都構成了重複。然而,扇出服務保持了這種重複狀態與新推特以及新關注關係的同步,從而保證了重複的可管理性。
|
||||
|
||||
#### 併發控制
|
||||
|
||||
事件溯源和變更資料捕獲的最大缺點是,事件日誌的消費者通常是非同步的,所以可能會出現這樣的情況:使用者會寫入日誌,然後從日誌衍生檢視中讀取,結果發現他的寫入還沒有反映在讀取檢視中。我們之前在在“[讀己之寫](ch5.md#讀己之寫)”中討論了這個問題以及可能的解決方案。
|
||||
事件溯源和變更資料捕獲的最大缺點是,事件日誌的消費者通常是非同步的,所以可能會出現這樣的情況:使用者會寫入日誌,然後從日誌衍生檢視中讀取,結果發現他的寫入還沒有反映在讀取檢視中。我們之前在“[讀己之寫](ch5.md#讀己之寫)”中討論了這個問題以及可能的解決方案。
|
||||
|
||||
一種解決方案是將事件附加到日誌時同步執行讀取檢視的更新。而將這些寫入操作合併為一個原子單元需要**事務**,所以要麼將事件日誌和讀取檢視儲存在同一個儲存系統中,要麼就需要跨不同系統進行分散式事務。或者,你也可以使用在“[使用全序廣播實現線性一致的儲存](ch9.md#使用全序廣播實現線性一致的儲存)”中討論的方法。
|
||||
一種解決方案是將事件追加到日誌時同步執行讀取檢視的更新。而將這些寫入操作合併為一個原子單元需要**事務**,所以要麼將事件日誌和讀取檢視儲存在同一個儲存系統中,要麼就需要跨不同系統進行分散式事務。或者,你也可以使用在“[使用全序廣播實現線性一致的儲存](ch9.md#使用全序廣播實現線性一致的儲存)”中討論的方法。
|
||||
|
||||
另一方面,從事件日誌匯出當前狀態也簡化了併發控制的某些部分。許多對於多物件事務的需求(參閱“[單物件和多物件操作](ch7.md#單物件和多物件操作)”)源於單個使用者操作需要在多個不同的位置更改資料。透過事件溯源,你可以設計一個自包含的事件以表示一個使用者操作。然後使用者操作就只需要在一個地方進行單次寫入操作 —— 即將事件附加到日誌中 —— 這個還是很容易使原子化的。
|
||||
|
||||
如果事件日誌與應用狀態以相同的方式分割槽(例如,處理分割槽3中的客戶事件只需要更新分割槽3中的應用狀態),那麼直接使用單執行緒日誌消費者就不需要寫入併發控制了。它從設計上一次只處理一個事件(參閱“[真的的序列執行](ch7.md#真的的序列執行)”)。日誌透過在分割槽中定義事件的序列順序,消除了併發性的不確定性【24】。如果一個事件觸及多個狀態分割槽,那麼需要做更多的工作,我們將在[第12章](ch12.md)討論。
|
||||
如果事件日誌與應用狀態以相同的方式分割槽(例如,處理分割槽3中的客戶事件只需要更新分割槽3中的應用狀態),那麼直接使用單執行緒日誌消費者就不需要寫入併發控制了。它從設計上一次只處理一個事件(參閱“[真的序列執行](ch7.md#真的序列執行)”)。日誌透過在分割槽中定義事件的序列順序,消除了併發性的不確定性【24】。如果一個事件觸及多個狀態分割槽,那麼需要做更多的工作,我們將在[第12章](ch12.md)討論。
|
||||
|
||||
#### 不變性的限制
|
||||
|
||||
@ -402,7 +402,7 @@ $$
|
||||
|
||||
在這種情況下,僅僅在日誌中新增另一個事件來指明先前的資料應該被視為刪除是不夠的 —— 你實際上是想改寫歷史,並假裝資料從一開始就沒有寫入。例如,Datomic管這個特性叫**切除(excision)** 【62】,而Fossil版本控制系統有一個類似的概念叫**避免(shunning)** 【63】。
|
||||
|
||||
真正刪除資料是非常非常困難的【64】,因為副本可能存在於很多地方:例如,儲存引擎,檔案系統和SSD通常會向一個新位置寫入,而不是原地覆蓋舊資料【52】,而備份通常是特意做成不可變的,防止意外刪除或損壞。刪除更多的是“使取回資料更困難”,而不是“使取回資料不可能”。無論如何,有時你必須得嘗試,正如我們在“[立法與自律](ch12.md#立法與自律)”中所看到的。
|
||||
真正刪除資料是非常非常困難的【64】,因為副本可能存在於很多地方:例如,儲存引擎,檔案系統和SSD通常會向一個新位置寫入,而不是原地覆蓋舊資料【52】,而備份通常是特意做成不可變的,防止意外刪除或損壞。刪除操作更多的是指“使取回資料更困難”,而不是指“使取回資料不可能”。無論如何,有時你必須得嘗試,正如我們在“[立法與自律](ch12.md#立法與自律)”中所看到的。
|
||||
|
||||
|
||||
|
||||
@ -412,19 +412,19 @@ $$
|
||||
|
||||
剩下的就是討論一下你可以用流做什麼 —— 也就是說,你可以處理它。一般來說,有三種選項:
|
||||
|
||||
1. 你可以將事件中的資料寫入資料庫,快取,搜尋索引或類似的儲存系統,然後能被其他客戶端查詢。如[圖11-5](../img/fig11-5.png)所示,這是資料庫與系統其他部分發生變更保持同步的好方法 —— 特別是當流消費者是寫入資料庫的唯一客戶端時。如“[批處理工作流的輸出](ch10.md#批處理工作流的輸出)”中所討論的,它是寫入儲存系統的流等價物。
|
||||
1. 你可以將事件中的資料寫入資料庫、快取、搜尋索引或類似的儲存系統,然後能被其他客戶端查詢。如[圖11-5](../img/fig11-5.png)所示,這是資料庫與系統其他部分所發生的變更保持同步的好方法 —— 特別是當流消費者是寫入資料庫的唯一客戶端時。如“[批處理工作流的輸出](ch10.md#批處理工作流的輸出)”中所討論的,它是寫入儲存系統的流等價物。
|
||||
2. 你能以某種方式將事件推送給使用者,例如傳送報警郵件或推送通知,或將事件流式傳輸到可實時顯示的儀表板上。在這種情況下,人是流的最終消費者。
|
||||
3. 你可以處理一個或多個輸入流,併產生一個或多個輸出流。流可能會經過由幾個這樣的處理階段組成的流水線,最後再輸出(選項1或2)。
|
||||
|
||||
在本章的剩餘部分中,我們將討論選項3:處理流以產生其他衍生流。處理這樣的流的程式碼片段,被稱為**運算元(operator)**或**作業(job)**。它與我們在[第10章](ch10.md)中討論過的Unix程序和MapReduce作業密切相關,資料流的模式是相似的:一個流處理器以只讀的方式使用輸入流,並將其輸出以僅追加的方式寫入一個不同的位置。
|
||||
在本章的剩餘部分中,我們將討論選項3:處理流以產生其他衍生流。處理這樣的流的程式碼片段,被稱為**運算元(operator)**或**作業(job)**。它與我們在[第10章](ch10.md)中討論過的Unix程序和MapReduce作業密切相關,資料流的模式是相似的:一個流處理器以只讀的方式使用輸入流,並將其輸出以僅追加的方式寫入一個不同的位置。
|
||||
|
||||
流處理中的分割槽和並行化模式也非常類似於[第10章](ch10.md)中介紹的MapReduce和資料流引擎,因此我們不再重複這些主題。基本的Map操作(如轉換和過濾記錄)也是一樣的。
|
||||
|
||||
與批次作業相比的一個關鍵區別是,流不會結束。這種差異會帶來很多隱含的結果。正如本章開始部分所討論的,排序對無界資料集沒有意義,因此無法使用**排序合併聯接**(請參閱“[Reduce端連線與分組](ch10.md#減少連線和分組)”)。容錯機制也必須改變:對於已經運行了幾分鐘的批處理作業,可以簡單地從頭開始重啟失敗任務,但是對於已經執行數年的流作業,重啟後從頭開始跑可能並不是一個可行的選項。
|
||||
與批次作業相比的一個關鍵區別是,流不會結束。這種差異會帶來很多隱含的結果。正如本章開始部分所討論的,排序對無界資料集沒有意義,因此無法使用**排序合併連線**(請參閱“[Reduce側連線與分組](ch10.md#Reduce側連線與分組)”)。容錯機制也必須改變:對於已經運行了幾分鐘的批處理作業,可以簡單地從頭開始重啟失敗任務,但是對於已經執行數年的流作業,重啟後從頭開始跑可能並不是一個可行的選項。
|
||||
|
||||
### 流處理的應用
|
||||
|
||||
長期以來,流處理一直用於監控目的,如果某個事件發生,單位希望能得到警報。例如:
|
||||
長期以來,流處理一直用於監控目的,如果某個事件發生,組織希望能得到警報。例如:
|
||||
|
||||
* 欺詐檢測系統需要確定信用卡的使用模式是否有意外地變化,如果信用卡可能已被盜刷,則鎖卡。
|
||||
* 交易系統需要檢查金融市場的價格變化,並根據指定的規則進行交易。
|
||||
@ -435,13 +435,13 @@ $$
|
||||
|
||||
#### 複合事件處理
|
||||
|
||||
**複合事件處理(complex, event processing, CEP)** 是20世紀90年代為分析事件流而開發出的一種方法,尤其適用於需要搜尋某些事件模式的應用【65,66】。與正則表示式允許你在字串中搜索特定字元模式的方式類似,CEP允許你指定規則以在流中搜索某些事件模式。
|
||||
**複合事件處理(complex event processing, CEP)** 是20世紀90年代為分析事件流而開發出的一種方法,尤其適用於需要搜尋某些事件模式的應用【65,66】。與正則表示式允許你在字串中搜索特定字元模式的方式類似,CEP允許你指定規則以在流中搜索某些事件模式。
|
||||
|
||||
CEP系統通常使用高層次的宣告式查詢語言,比如SQL,或者圖形使用者介面,來描述應該檢測到的事件模式。這些查詢被提交給處理引擎,該引擎消費輸入流,並在內部維護一個執行所需匹配的狀態機。當發現匹配時,引擎發出一個**複合事件(complex event)**(因此得名),並附有檢測到的事件模式詳情【67】。
|
||||
CEP系統通常使用高層次的宣告式查詢語言,比如SQL,或者圖形使用者介面,來描述應該檢測到的事件模式。這些查詢被提交給處理引擎,該引擎消費輸入流,並在內部維護一個執行所需匹配的狀態機。當發現匹配時,引擎發出一個**複合事件(complex event)**(CEP因此得名),並附有檢測到的事件模式詳情【67】。
|
||||
|
||||
在這些系統中,查詢和資料之間的關係與普通資料庫相比是顛倒的。通常情況下,資料庫會持久儲存資料,並將查詢視為臨時的:當查詢進入時,資料庫搜尋與查詢匹配的資料,然後在查詢完成時丟掉查詢。 CEP引擎反轉了角色:查詢是長期儲存的,來自輸入流的事件不斷流過它們,搜尋匹配事件模式的查詢【68】。
|
||||
|
||||
CEP的實現包括Esper 【69】,IBM InfoSphere Streams 【70】,Apama,TIBCO StreamBase和SQLstream。像Samza這樣的分散式流處理元件,支援使用SQL在流上進行宣告式查詢【71】。
|
||||
CEP的實現包括Esper【69】,IBM InfoSphere Streams【70】,Apama,TIBCO StreamBase和SQLstream。像Samza這樣的分散式流處理元件,支援使用SQL在流上進行宣告式查詢【71】。
|
||||
|
||||
#### 流分析
|
||||
|
||||
@ -451,37 +451,35 @@ $$
|
||||
* 滾動計算一段時間視窗內某個值的平均值
|
||||
* 將當前的統計值與先前的時間區間的值對比(例如,檢測趨勢,當指標與上週同比異常偏高或偏低時報警)
|
||||
|
||||
這些統計值通常是在固定時間區間內進行計算的,例如,你可能想知道在過去5分鐘內服務每秒查詢次數的均值,以及此時間段內響應時間的第99百分位點。在幾分鐘內取平均,能抹平秒和秒之間的無關波動,且仍然能向你展示流量模式的時間圖景。聚合的時間間隔稱為**視窗(window)**,我們將在“[理解時間](#理解時間)”中更詳細地討論視窗。
|
||||
這些統計值通常是在固定時間區間內進行計算的,例如,你可能想知道在過去5分鐘內服務每秒查詢次數的均值,以及此時間段內響應時間的第99百分位點。在幾分鐘內取平均,能抹平秒和秒之間的無關波動,且仍然能向你展示流量模式的時間圖景。聚合的時間間隔稱為**視窗(window)**,我們將在“[時間推理](#時間推理)”中更詳細地討論視窗。
|
||||
|
||||
流分析系統有時會使用概率演算法,例如Bloom filter(我們在“[效能最佳化](ch3.md#效能最佳化)”中遇到過)來管理成員資格,HyperLogLog 【72】用於基數估計以及各種百分比估計演算法(請參閱“[實踐中的百分位點](ch1.md#實踐中的百分位點)“)。概率演算法產出近似的結果,但比起精確演算法的優點是記憶體使用要少得多。使用近似演算法有時讓人們覺得流處理系統總是有損的和不精確的,但這是錯誤看法:流處理並沒有任何內在的近似性,而概率演算法只是一種最佳化【73】。
|
||||
流分析系統有時會使用概率演算法,例如Bloom filter(我們在“[效能最佳化](ch3.md#效能最佳化)”中遇到過)來管理成員資格,HyperLogLog【72】用於基數估計以及各種百分比估計演算法(請參閱“[實踐中的百分位點](ch1.md#實踐中的百分位點)“)。概率演算法產出近似的結果,但比起精確演算法的優點是記憶體使用要少得多。使用近似演算法有時讓人們覺得流處理系統總是有損的和不精確的,但這是錯誤看法:流處理並沒有任何內在的近似性,而概率演算法只是一種最佳化【73】。
|
||||
|
||||
許多開源分散式流處理框架的設計都是針對分析設計的:例如Apache Storm,Spark Streaming,Flink,Concord,Samza和Kafka Streams 【74】。託管服務包括Google Cloud Dataflow和Azure Stream Analytics。
|
||||
|
||||
#### 維護物化檢視
|
||||
|
||||
我們在“[資料庫和資料流](#資料庫和資料流)”中看到,資料庫的變更流可以用於維護衍生資料系統(如快取,搜尋索引和資料倉庫),使其與源資料庫保持最新。我們可以將這些示例視作維護**物化檢視(materialized view)** 的一種具體場景(參閱“[聚合:資料立方體和物化檢視](ch3.md#聚合:資料立方體和物化檢視)”):在某個資料集上衍生出一個替代檢視以便高效查詢,並在底層資料變更時更新檢視【50】。
|
||||
我們在“[資料庫與流](#資料庫與流)”中看到,資料庫的變更流可以用於維護衍生資料系統(如快取、搜尋索引和資料倉庫),並使其與源資料庫保持最新。我們可以將這些示例視作維護**物化檢視(materialized view)** 的一種具體場景(參閱“[聚合:資料立方體和物化檢視](ch3.md#聚合:資料立方體和物化檢視)”):在某個資料集上衍生出一個替代檢視以便高效查詢,並在底層資料變更時更新檢視【50】。
|
||||
|
||||
同樣,在事件溯源中,應用程式的狀態是透過**應用(apply)**事件日誌來維護的;這裡的應用狀態也是一種物化檢視。與流分析場景不同的是,僅考慮某個時間視窗內的事件通常是不夠的:構建物化檢視可能需要任意時間段內的**所有**事件,除了那些可能由日誌壓縮丟棄的過時事件(請參閱“[日誌壓縮](#日誌壓縮)“)。實際上,你需要一個可以一直延伸到時間開端的視窗。
|
||||
同樣,在事件溯源中,應用程式的狀態是透過應用事件日誌來維護的;這裡的應用程式狀態也是一種物化檢視。與流分析場景不同的是,僅考慮某個時間視窗內的事件通常是不夠的:構建物化檢視可能需要任意時間段內的**所有**事件,除了那些可能由日誌壓縮丟棄的過時事件(請參閱“[日誌壓縮](#日誌壓縮)“)。實際上,你需要一個可以一直延伸到時間開端的視窗。
|
||||
|
||||
原則上講,任何流處理元件都可以用於維護物化檢視,儘管“永遠執行”與一些面向分析的框架假設的“主要在有限時間段視窗上執行”背道而馳, Samza和Kafka Streams支援這種用法,建立在Kafka對日誌壓縮comp的支援上【75】。
|
||||
原則上講,任何流處理元件都可以用於維護物化檢視,儘管“永遠執行”與一些面向分析的框架假設的“主要在有限時間段視窗上執行”背道而馳, Samza和Kafka Streams支援這種用法,建立在Kafka對日誌壓縮的支援上【75】。
|
||||
|
||||
#### 在流上搜索
|
||||
|
||||
除了允許搜尋由多個事件構成模式的CEP外,有時也存在基於複雜標準(例如全文搜尋查詢)來搜尋單個事件的需求。
|
||||
|
||||
例如,媒體監測服務可以訂閱新聞文章Feed與來自媒體的播客,搜尋任何關於公司,產品或感興趣的話題的新聞。這是透過預先構建一個搜尋查詢來完成的,然後不斷地將新聞項的流與該查詢進行匹配。在一些網站上也有類似的功能:例如,當市場上出現符合其搜尋條件的新房產時,房地產網站的使用者可以要求網站通知他們。 Elasticsearch的這種過濾器功能,是實現這種流搜尋的一種選擇【76】。
|
||||
例如,媒體監測服務可以訂閱新聞文章Feed與來自媒體的播客,搜尋任何關於公司、產品或感興趣的話題的新聞。這是透過預先構建一個搜尋查詢來完成的,然後不斷地將新聞項的流與該查詢進行匹配。在一些網站上也有類似的功能:例如,當市場上出現符合其搜尋條件的新房產時,房地產網站的使用者可以要求網站通知他們。Elasticsearch的這種過濾器功能,是實現這種流搜尋的一種選擇【76】。
|
||||
|
||||
傳統的搜尋引擎首先索引檔案,然後在索引上跑查詢。相比之下,搜尋一個數據流則反了過來:查詢被儲存下來,文件從查詢中流過,就像在CEP中一樣。在簡單的情況就是,你可以為每個文件測試每個查詢。但是如果你有大量查詢,這可能會變慢。為了最佳化這個過程,可以像對文件一樣,為查詢建立索引。因而收窄可能匹配的查詢集合【77】。
|
||||
傳統的搜尋引擎首先索引檔案,然後在索引上跑查詢。相比之下,搜尋一個數據流則反了過來:查詢被儲存下來,文件從查詢中流過,就像在CEP中一樣。最簡單的情況就是,你可以為每個文件測試每個查詢。但是如果你有大量查詢,這可能會變慢。為了最佳化這個過程,可以像對文件一樣,為查詢建立索引。因而收窄可能匹配的查詢集合【77】。
|
||||
|
||||
#### 訊息傳遞和RPC
|
||||
|
||||
在“[訊息傳遞資料流](ch4.md#訊息傳遞資料流)”中我們討論過,訊息傳遞系統可以作為RPC的替代方案,即作為一種服務間通訊的機制,比如在Actor模型中所使用的那樣。儘管這些系統也是基於訊息和事件,但我們通常不會將其視作流處理元件:
|
||||
在“[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)”中我們討論過,訊息傳遞系統可以作為RPC的替代方案,即作為一種服務間通訊的機制,比如在Actor模型中所使用的那樣。儘管這些系統也是基於訊息和事件,但我們通常不會將其視作流處理元件:
|
||||
|
||||
* Actor框架主要是管理模組通訊的併發和分散式執行的一種機制,而流處理主要是一種資料管理技術。
|
||||
|
||||
|
||||
* Actor之間的交流往往是短暫的,一對一的;而事件日誌則是持久的,多訂閱者的。
|
||||
* Actor可以以任意方式進行通訊(允許包括迴圈的請求/響應),但流處理通常配置在無環流水線中,其中每個流都是一個特定作業的輸出,由良好定義的輸入流中派生而來。
|
||||
* Actor之間的交流往往是短暫的、一對一的;而事件日誌則是持久的、多訂閱者的。
|
||||
* Actor可以以任意方式進行通訊(包括迴圈的請求/響應模式),但流處理通常配置在無環流水線中,其中每個流都是一個特定作業的輸出,由良好定義的輸入流中派生而來。
|
||||
|
||||
也就是說,RPC類系統與流處理之間有一些交叉領域。例如,Apache Storm有一個稱為**分散式RPC**的功能,它允許將使用者查詢分散到一系列也處理事件流的節點上;然後這些查詢與來自輸入流的事件交織,而結果可以被彙總併發回給使用者【78】(另參閱“[多分割槽資料處理](ch12.md#多分割槽資料處理)”)。
|
||||
|
||||
@ -489,21 +487,21 @@ $$
|
||||
|
||||
### 時間推理
|
||||
|
||||
流處理通常需要與時間打交道,尤其是用於分析目的時候,會頻繁使用時間視窗,例如“過去五分鐘的平均值”。“最後五分鐘”的含義看上去似乎是清晰而無歧義的,但不幸的是,這個概念非常棘手。
|
||||
流處理通常需要與時間打交道,尤其是用於分析目的時候,會頻繁使用時間視窗,例如“過去五分鐘的平均值”。“過去五分鐘”的含義看上去似乎是清晰而無歧義的,但不幸的是,這個概念非常棘手。
|
||||
|
||||
在批處理中過程中,大量的歷史事件迅速收縮。如果需要按時間來分析,批處理器需要檢查每個事件中嵌入的時間戳。讀取執行批處理機器的系統時鐘沒有任何意義,因為處理執行的時間與事件實際發生的時間無關。
|
||||
在批處理中過程中,大量的歷史事件被快速地處理。如果需要按時間來分析,批處理器需要檢查每個事件中嵌入的時間戳。讀取執行批處理機器的系統時鐘沒有任何意義,因為處理執行的時間與事件實際發生的時間無關。
|
||||
|
||||
批處理可以在幾分鐘內讀取一年的歷史事件;在大多數情況下,感興趣的時間線是歷史中的一年,而不是處理中的幾分鐘。而且使用事件中的時間戳,使得處理是**確定性**的:在相同的輸入上再次執行相同的處理過程會得到相同的結果(參閱“[故障容錯](ch10.md#故障容錯)”)。
|
||||
批處理可以在幾分鐘內讀取一年的歷史事件;在大多數情況下,感興趣的時間線是歷史中的一年,而不是處理中的幾分鐘。而且使用事件中的時間戳,使得處理是**確定性**的:在相同的輸入上再次執行相同的處理過程會得到相同的結果(參閱“[容錯](ch10.md#容錯)”)。
|
||||
|
||||
另一方面,許多流處理框架使用處理機器上的本地系統時鐘(**處理時間(processing time)**)來確定**視窗**【79】。這種方法的優點是簡單,事件建立與事件處理之間的延遲可以忽略不計。然而,如果存在任何顯著的處理延遲 —— 即,事件處理顯著地晚於事件實際發生的時間,處理就失效了。
|
||||
另一方面,許多流處理框架使用處理機器上的本地系統時鐘(**處理時間(processing time)**)來確定**視窗(windowing)**【79】。這種方法的優點是簡單,如果事件建立與事件處理之間的延遲可以忽略不計,那也是合理的。然而,如果存在任何顯著的處理延遲 —— 即,事件處理顯著地晚於事件實際發生的時間,這種處理方式就失效了。
|
||||
|
||||
#### 事件時間與處理時間
|
||||
|
||||
很多原因都可能導致處理延遲:排隊,網路故障(參閱“[不可靠的網路](ch8.md#不可靠的網路)”),效能問題導致訊息代理/訊息處理器出現爭用,流消費者重啟,重新處理過去的事件(參閱“[重放舊訊息](#重放舊訊息)”),或者在修復程式碼BUG之後從故障中恢復。
|
||||
很多原因都可能導致處理延遲:排隊,網路故障(參閱“[不可靠的網路](ch8.md#不可靠的網路)”),效能問題導致訊息代理/訊息處理器出現爭用,流消費者重啟,從故障中恢復時重新處理過去的事件(參閱“[重播舊訊息](#重播舊訊息)”),或者在修復程式碼BUG之後。
|
||||
|
||||
而且,訊息延遲還可能導致無法預測訊息順序。例如,假設使用者首先發出一個Web請求(由Web伺服器A處理),然後發出第二個請求(由伺服器B處理)。 A和B發出描述它們所處理請求的事件,但是B的事件在A的事件發生之前到達訊息代理。現在,流處理器將首先看到B事件,然後看到A事件,即使它們實際上是以相反的順序發生的。
|
||||
|
||||
有一個類比也許能幫助理解,“星球大戰”電影:第四集於1977年發行,第五集於1980年,第六集於1983年,緊隨其後的是1999年的第一集,2002年的第二集,和2005年的三集,以及2015年的第七集【80】[^ii]。如果你按照按照它們上映的順序觀看電影,你處理電影的順序與它們敘事的順序就是不一致的。 (集數編號就像事件時間戳,而你觀看電影的日期就是處理時間)作為人類,我們能夠應對這種不連續性,但是流處理演算法需要專門編寫,以適應這種時機與順序的問題。
|
||||
有一個類比也許能幫助理解,“星球大戰”電影:第四集於1977年發行,第五集於1980年,第六集於1983年,緊隨其後的是1999年的第一集,2002年的第二集,和2005年的第三集,以及2015年的第七集【80】[^ii]。如果你按照按照它們上映的順序觀看電影,你處理電影的順序與它們敘事的順序就是不一致的。 (集數編號就像事件時間戳,而你觀看電影的日期就是處理時間)作為人類,我們能夠應對這種不連續性,但是流處理演算法需要專門編寫,以適應這種時序與順序的問題。
|
||||
|
||||
[^ii]: 感謝Flink社群的Kostas Kloudas提出這個比喻。
|
||||
|
||||
@ -522,7 +520,7 @@ $$
|
||||
在一段時間沒有看到任何新的事件之後,你可以超時並宣佈一個視窗已經就緒,但仍然可能發生這種情況:某些事件被緩衝在另一臺機器上,由於網路中斷而延遲。你需要能夠處理這種在視窗宣告完成之後到達的 **滯留(straggler)** 事件。大體上,你有兩種選擇【1】:
|
||||
|
||||
1. 忽略這些滯留事件,因為在正常情況下它們可能只是事件中的一小部分。你可以將丟棄事件的數量作為一個監控指標,並在出現大量丟訊息的情況時報警。
|
||||
2. 釋出一個**更正(correction)**,一個包括滯留事件的更新視窗值。更新的視窗與包含散兵隊員的價值。你可能還需要收回以前的輸出。
|
||||
2. 釋出一個**更正(correction)**,一個包括滯留事件的更新視窗值。你可能還需要收回以前的輸出。
|
||||
|
||||
在某些情況下,可以使用特殊的訊息來指示“從現在開始,不會有比t更早時間戳的訊息了”,消費者可以使用它來觸發視窗【81】。但是,如果不同機器上的多個生產者都在生成事件,每個生產者都有自己的最小時間戳閾值,則消費者需要分別跟蹤每個生產者。在這種情況下,新增和刪除生產者都是比較棘手的。
|
||||
|
||||
@ -540,7 +538,7 @@ $$
|
||||
|
||||
透過從第三個時間戳中減去第二個時間戳,可以估算裝置時鐘和伺服器時鐘之間的偏移(假設網路延遲與所需的時間戳精度相比可忽略不計)。然後可以將該偏移應用於事件時間戳,從而估計事件實際發生的真實時間(假設裝置時鐘偏移在事件發生時與送往伺服器之間沒有變化)。
|
||||
|
||||
這並不是流處理獨有的問題,批處理有著完全一樣的時間推理問題。只是在流處理的上下文中,我們更容易意識到時間的流逝。
|
||||
這並不是流處理獨有的問題,批處理有著完全一樣的時 間推理問題。只是在流處理的上下文中,我們更容易意識到時間的流逝。
|
||||
|
||||
#### 視窗的型別
|
||||
|
||||
@ -552,7 +550,7 @@ $$
|
||||
|
||||
***跳動視窗(Hopping Window)***
|
||||
|
||||
跳動視窗也有著固定的長度,但允許視窗重疊以提供一些平滑。例如,一個帶有1分鐘跳躍步長的5分鐘視窗將包含`10:03:00`至`10:07:59`之間的事件,而下一個視窗將覆蓋`10:04:00`至`10:08:59`之間的事件,等等。透過首先計算1分鐘的滾動視窗,然後在幾個相鄰視窗上進行聚合,可以實現這種跳動視窗。
|
||||
跳動視窗也有著固定的長度,但允許視窗重疊以提供一些平滑。例如,一個帶有1分鐘跳躍步長的5分鐘視窗將包含`10:03:00`至`10:07:59`之間的事件,而下一個視窗將覆蓋`10:04:00`至`10:08:59`之間的事件,等等。透過首先計算1分鐘的滾動視窗(tunmbling window),然後在幾個相鄰視窗上進行聚合,可以實現這種跳動視窗。
|
||||
|
||||
***滑動視窗(Sliding Window)***
|
||||
|
||||
@ -560,7 +558,7 @@ $$
|
||||
|
||||
***會話視窗(Session window)***
|
||||
|
||||
與其他視窗型別不同,會話視窗沒有固定的持續時間,而定義為:將同一使用者出現時間相近的所有事件分組在一起,而當用戶一段時間沒有活動時(例如,如果30分鐘內沒有事件)視窗結束。會話切分是網站分析的常見需求(參閱“[GROUP BY](ch10.md#GROUP\ BY)”)。
|
||||
與其他視窗型別不同,會話視窗沒有固定的持續時間,而定義為:將同一使用者出現時間相近的所有事件分組在一起,而當用戶一段時間沒有活動時(例如,如果30分鐘內沒有事件)視窗結束。會話切分是網站分析的常見需求(參閱“[分組](ch10.md#分組)”)。
|
||||
|
||||
### 流式連線
|
||||
|
||||
@ -578,17 +576,17 @@ $$
|
||||
|
||||
為了實現這種型別的連線,流處理器需要維護**狀態**:例如,按會話ID索引最近一小時內發生的所有事件。無論何時發生搜尋事件或點選事件,都會被新增到合適的索引中,而流處理器也會檢查另一個索引是否有具有相同會話ID的事件到達。如果有匹配事件就會發出一個表示搜尋結果被點選的事件;如果搜尋事件直到過期都沒看見有匹配的點選事件,就會發出一個表示搜尋結果未被點選的事件。
|
||||
|
||||
#### 流表連線(流擴充套件)
|
||||
#### 流表連線(流擴充)
|
||||
|
||||
在“[示例:使用者活動事件分析](ch10.md#示例:使用者活動事件分析)”([圖10-2](../img/fig10-2.png))中,我們看到了連線兩個資料集的批處理作業示例:一組使用者活動事件和一個使用者檔案資料庫。將使用者活動事件視為流,並在流處理器中連續執行相同的連線是很自然的想法:輸入是包含使用者ID的活動事件流,而輸出還是活動事件流,但其中使用者ID已經被擴充套件為使用者的檔案資訊。這個過程有時被稱為 使用資料庫的資訊來**擴充(enriching)** 活動事件。
|
||||
在“[示例:使用者活動事件分析](ch10.md#示例:使用者活動事件分析)”([圖10-2](../img/fig10-2.png))中,我們看到了連線兩個資料集的批處理作業示例:一組使用者活動事件和一個使用者檔案資料庫。將使用者活動事件視為流,並在流處理器中連續執行相同的連線是很自然的想法:輸入是包含使用者ID的活動事件流,而輸出還是活動事件流,但其中使用者ID已經被擴充套件為使用者的檔案資訊。這個過程有時被稱為使用資料庫的資訊來**擴充(enriching)** 活動事件。
|
||||
|
||||
要執行此聯接,流處理器需要一次處理一個活動事件,在資料庫中查詢事件的使用者ID,並將檔案資訊新增到活動事件中。資料庫查詢可以透過查詢遠端資料庫來實現。但正如在“[示例:分析使用者活動事件](ch10.md#示例:分析使用者活動事件)”一節中討論的,此類遠端查詢可能會很慢,並且有可能導致資料庫過載【75】。
|
||||
要執行此連線,流處理器需要一次處理一個活動事件,在資料庫中查詢事件的使用者ID,並將檔案資訊新增到活動事件中。資料庫查詢可以透過查詢遠端資料庫來實現。但正如在“[示例:使用者活動事件分析](ch10.md#示例:使用者活動事件分析)”一節中討論的,此類遠端查詢可能會很慢,並且有可能導致資料庫過載【75】。
|
||||
|
||||
另一種方法是將資料庫副本載入到流處理器中,以便在本地進行查詢而無需網路往返。這種技術與我們在“[Map端連線](ch10.md#Map端連線)”中討論的雜湊連線非常相似:如果資料庫的本地副本足夠小,則可以是記憶體中的散列表,比較大的話也可以是本地磁碟上的索引。
|
||||
另一種方法是將資料庫副本載入到流處理器中,以便在本地進行查詢而無需網路往返。這種技術與我們在“[Map側連線](ch10.md#Map側連線)”中討論的雜湊連線非常相似:如果資料庫的本地副本足夠小,則可以是記憶體中的散列表,比較大的話也可以是本地磁碟上的索引。
|
||||
|
||||
與批處理作業的區別在於,批處理作業使用資料庫的時間點快照作為輸入,而流處理器是長時間執行的,且資料庫的內容可能隨時間而改變,所以流處理器資料庫的本地副本需要保持更新。這個問題可以透過變更資料捕獲來解決:流處理器可以訂閱使用者檔案資料庫的更新日誌,如同活躍事件流一樣。當增添或修改檔案時,流處理器會更新其本地副本。因此,我們有了兩個流之間的連線:活動事件和檔案更新。
|
||||
與批處理作業的區別在於,批處理作業使用資料庫的時間點快照作為輸入,而流處理器是長時間執行的,且資料庫的內容可能隨時間而改變,所以流處理器資料庫的本地副本需要保持更新。這個問題可以透過變更資料捕獲來解決:流處理器可以訂閱使用者檔案資料庫的更新日誌,如同活動事件流一樣。當增添或修改檔案時,流處理器會更新其本地副本。因此,我們有了兩個流之間的連線:活動事件和檔案更新。
|
||||
|
||||
流表連線實際上非常類似於流流連線;最大的區別在於對於表的變更日誌流,連線使用了一個可以回溯到“時間起點”的視窗(概念上是無限的視窗),新版本的記錄會覆蓋更早的版本。對於輸入的流,連線可能壓根兒就沒有維護視窗。
|
||||
流表連線實際上非常類似於流流連線;最大的區別在於對於表的變更日誌流,連線使用了一個可以回溯到“時間起點”的視窗(概念上是無限的視窗),新版本的記錄會覆蓋更早的版本。對於輸入的流,連線可能壓根兒就沒有維護任何視窗。
|
||||
|
||||
#### 表表連線(維護物化檢視)
|
||||
|
||||
@ -601,7 +599,7 @@ $$
|
||||
* 當用戶$u_1$開始關注使用者$u_2$時,$u_2$最近的推文將被新增到$u_1$的時間線上。
|
||||
* 當用戶$u_1$取消關注使用者$u_2$時,$u_2$的推文將從$u_1$的時間線中移除。
|
||||
|
||||
要在流處理器中實現這種快取維護,你需要推文事件流(傳送與刪除)和關注關係事件流(關注與取消關注)。流處理需要為維護一個數據庫,包含每個使用者的粉絲集合。以便知道當一條新推文到達時,需要更新哪些時間線【86】。
|
||||
要在流處理器中實現這種快取維護,你需要推文事件流(傳送與刪除)和關注關係事件流(關注與取消關注)。流處理需要維護一個數據庫,包含每個使用者的粉絲集合。以便知道當一條新推文到達時,需要更新哪些時間線【86】。
|
||||
|
||||
觀察這個流處理過程的另一種視角是:它維護了一個連線了兩個表(推文與關注)的物化檢視,如下所示:
|
||||
|
||||
@ -613,9 +611,9 @@ JOIN follows ON follows.followee_id = tweets.sender_id
|
||||
GROUP BY follows.follower_id
|
||||
```
|
||||
|
||||
流連線直接對應於這個查詢中的表連線。時間線實際上是這個查詢結果的快取,每當基礎表發生變化時都會更新[^iii]。
|
||||
流連線直接對應於這個查詢中的表連線。時間線實際上是這個查詢結果的快取,每當底層的表發生變化時都會更新[^iii]。
|
||||
|
||||
[^iii]: 如果你將流視作表的衍生物,如[圖11-6](../img/fig11-6.png)所示,而把一個連線看作是兩個表的乘法u·v,那麼會發生一些有趣的事情:物化連線的變化流遵循乘積法則:(u·v)'= u'v + uv'(u·v)'= u'v + uv'。 換句話說,任何推文的變化量都與當前的關注聯絡在一起,任何關注的變化量都與當前的推文相連線【49,50】。
|
||||
[^iii]: 如果你將流視作表的衍生物,如[圖11-6](../img/fig11-6.png)所示,而把一個連線看作是兩個表的乘法u·v,那麼會發生一些有趣的事情:物化連線的變化流遵循乘積法則:(u·v)'= u'v + uv'。 換句話說,任何推文的變化量都與當前的關注聯絡在一起,任何關注的變化量都與當前的推文相連線【49,50】。
|
||||
|
||||
#### 連線的時間依賴性
|
||||
|
||||
@ -625,7 +623,7 @@ GROUP BY follows.follower_id
|
||||
|
||||
這就產生了一個問題:如果不同流中的事件發生在近似的時間範圍內,則應該按照什麼樣的順序進行處理?在流表連線的例子中,如果使用者更新了它們的檔案,哪些活動事件與舊檔案連線(在檔案更新前處理),哪些又與新檔案連線(在檔案更新之後處理)?換句話說:你需要對一些狀態做連線,如果狀態會隨著時間推移而變化,那應當使用什麼時間點來連線呢【45】?
|
||||
|
||||
這種時序依賴可能出現在很多地方。例如銷售東西需要對發票應用適當的稅率,這取決於所處的國家/州,產品型別,銷售日期(因為稅率會隨時變化)。當連線銷售額與稅率表時,你可能期望的是使用銷售時的稅率參與連線。如果你正在重新處理歷史資料,銷售時的稅率可能和現在的稅率有所不同。
|
||||
這種時序依賴可能出現在很多地方。例如銷售東西需要對發票應用適當的稅率,這取決於所處的國家/州,產品型別,銷售日期(因為稅率時不時會變化)。當連線銷售額與稅率表時,你可能期望的是使用銷售時的稅率參與連線。如果你正在重新處理歷史資料,銷售時的稅率可能和現在的稅率有所不同。
|
||||
|
||||
如果跨越流的事件順序是未定的,則連線會變為不確定性的【87】,這意味著你在同樣輸入上重跑相同的作業未必會得到相同的結果:當你重跑任務時,輸入流上的事件可能會以不同的方式交織。
|
||||
|
||||
@ -645,7 +643,7 @@ GROUP BY follows.follower_id
|
||||
|
||||
微批次也隱式提供了一個與批次大小相等的滾動視窗(按處理時間而不是事件時間戳分窗)。任何需要更大視窗的作業都需要顯式地將狀態從一個微批次轉移到下一個微批次。
|
||||
|
||||
Apache Flink則使用不同的方法,它會定期生成狀態的滾動存檔點並將其寫入持久儲存【92,93】。如果流運算元崩潰,它可以從最近的存檔點重啟,並丟棄從最近檢查點到崩潰之間的所有輸出。存檔點會由訊息流中的 **壁障(barrier)** 觸發,類似於微批次之間的邊界,但不會強制一個特定的視窗大小。
|
||||
Apache Flink則使用不同的方法,它會定期生成狀態的滾動存檔點並將其寫入持久儲存【92,93】。如果流運算元崩潰,它可以從最近的存檔點重啟,並丟棄從最近檢查點到崩潰之間的所有輸出。存檔點會由訊息流中的**壁障(barrier)**觸發,類似於微批次之間的邊界,但不會強制一個特定的視窗大小。
|
||||
|
||||
在流處理框架的範圍內,微批次與存檔點方法提供了與批處理一樣的**恰好一次語義**。但是,只要輸出離開流處理器(例如,寫入資料庫,向外部訊息代理髮送訊息,或傳送電子郵件),框架就無法拋棄失敗批次的輸出了。在這種情況下,重啟失敗任務會導致外部副作用發生兩次,只有微批次或存檔點不足以阻止這一問題。
|
||||
|
||||
@ -663,23 +661,23 @@ GROUP BY follows.follower_id
|
||||
|
||||
冪等操作是多次重複執行與單次執行效果相同的操作。例如,將鍵值儲存中的某個鍵設定為某個特定值是冪等的(再次寫入該值,只是用同樣的值替代),而遞增一個計數器不是冪等的(再次執行遞增意味著該值遞增兩次)。
|
||||
|
||||
即使一個操作不是天生冪等的,往往可以透過一些額外的元資料做成冪等的。例如,在使用來自Kafka的訊息時,每條訊息都有一個持久的,單調遞增的偏移量。將值寫入外部資料庫時可以將這個偏移量帶上,這樣你就可以判斷一條更新是不是已經執行過了,因而避免重複執行。
|
||||
即使一個操作不是天生冪等的,往往可以透過一些額外的元資料做成冪等的。例如,在使用來自Kafka的訊息時,每條訊息都有一個持久的、單調遞增的偏移量。將值寫入外部資料庫時可以將這個偏移量帶上,這樣你就可以判斷一條更新是不是已經執行過了,因而避免重複執行。
|
||||
|
||||
Storm的Trident基於類似的想法來處理狀態【78】。依賴冪等性意味著隱含了一些假設:重啟一個失敗的任務必須以相同的順序重放相同的訊息(基於日誌的訊息代理能做這些事),處理必須是確定性的,沒有其他節點能同時更新相同的值【98,99】。
|
||||
Storm的Trident基於類似的想法來處理狀態【78】。依賴冪等性意味著隱含了一些假設:重啟一個失敗的任務必須以相同的順序重播相同的訊息(基於日誌的訊息代理能做這些事),處理必須是確定性的,沒有其他節點能同時更新相同的值【98,99】。
|
||||
|
||||
當從一個處理節點故障切換到另一個節點時,可能需要進行**防護(fencing)**(參閱“[領導和鎖](ch8.md#領導和鎖)”),以防止被假死節點干擾。儘管有這麼多注意事項,冪等操作是一種實現**恰好一次語義**的有效方式,僅需很小的額外開銷。
|
||||
當從一個處理節點故障切換到另一個節點時,可能需要進行**防護(fencing)**(參閱“[領導者和鎖](ch8.md#領導者和鎖)”),以防止被假死節點干擾。儘管有這麼多注意事項,冪等操作是一種實現**恰好一次語義**的有效方式,僅需很小的額外開銷。
|
||||
|
||||
#### 失敗後重建狀態
|
||||
|
||||
任何需要狀態的流處理 —— 例如,任何視窗聚合(例如計數器,平均值和直方圖)以及任何用於連線的表和索引,都必須確保在失敗之後能恢復其狀態。
|
||||
|
||||
一種選擇是將狀態儲存在遠端資料儲存中,並進行復制,然而正如在“[流表連線](#流表連線)”中所述,每個訊息都要查詢遠端資料庫可能會很慢。另一種方法是在流處理器本地儲存狀態,並定期複製。然後當流處理器從故障中恢復時,新任務可以讀取狀態副本,恢復處理而不丟失資料。
|
||||
一種選擇是將狀態儲存在遠端資料儲存中,並進行復制,然而正如在“[流表連線(流擴充)](#流表連線(流擴充))”中所述,每個訊息都要查詢遠端資料庫可能會很慢。另一種方法是在流處理器本地儲存狀態,並定期複製。然後當流處理器從故障中恢復時,新任務可以讀取狀態副本,恢復處理而不丟失資料。
|
||||
|
||||
例如,Flink定期捕獲運算元狀態的快照,並將它們寫入HDFS等持久儲存中【92,93】。 Samza和Kafka Streams透過將狀態變更傳送到具有日誌壓縮功能的專用Kafka主題來複制狀態變更,這與變更資料捕獲類似【84,100】。 VoltDB透過在多個節點上對每個輸入訊息進行冗餘處理來複制狀態(參閱“[真的序列執行](ch7.md#真的序列執行)”)。
|
||||
|
||||
在某些情況下,甚至可能都不需要複製狀態,因為它可以從輸入流重建。例如,如果狀態是從相當短的視窗中聚合而成,則簡單地重放該視窗中的輸入事件可能是足夠快的。如果狀態是透過變更資料捕獲來維護的資料庫的本地副本,那麼也可以從日誌壓縮的變更流中重建資料庫(參閱“[日誌壓縮](#日誌壓縮)”)。
|
||||
在某些情況下,甚至可能都不需要複製狀態,因為它可以從輸入流重建。例如,如果狀態是從相當短的視窗中聚合而成,則簡單地重播該視窗中的輸入事件可能是足夠快的。如果狀態是透過變更資料捕獲來維護的資料庫的本地副本,那麼也可以從日誌壓縮的變更流中重建資料庫(參閱“[日誌壓縮](#日誌壓縮)”)。
|
||||
|
||||
然而,所有這些權衡取決於底層基礎架構的效能特徵:在某些系統中,網路延遲可能低於磁碟訪問延遲,網路頻寬可能與磁碟頻寬相當。沒有針對所有情況的普世理想權衡,隨著儲存和網路技術的發展,本地狀態與遠端狀態的優點也可能會互換。
|
||||
然而,所有這些權衡取決於底層基礎架構的效能特徵:在某些系統中,網路延遲可能低於磁碟訪問延遲,網路頻寬也可能與磁碟頻寬相當。沒有針對所有情況的普適理想權衡,隨著儲存和網路技術的發展,本地狀態與遠端狀態的優點也可能會互換。
|
||||
|
||||
|
||||
|
||||
@ -691,19 +689,19 @@ GROUP BY follows.follower_id
|
||||
|
||||
***AMQP/JMS風格的訊息代理***
|
||||
|
||||
代理將單條訊息分配給消費者,消費者在成功處理單條訊息後確認訊息。訊息被確認後從代理中刪除。這種方法適合作為一種非同步形式的RPC(另請參閱“[訊息傳遞資料流](ch4.md#訊息傳遞資料流)”),例如在任務佇列中,訊息處理的確切順序並不重要,而且訊息在處理完之後,不需要回頭重新讀取舊訊息。
|
||||
代理將單條訊息分配給消費者,消費者在成功處理單條訊息後確認訊息。訊息被確認後從代理中刪除。這種方法適合作為一種非同步形式的RPC(另請參閱“[訊息傳遞中的資料流](ch4.md#訊息傳遞中的資料流)”),例如在任務佇列中,訊息處理的確切順序並不重要,而且訊息在處理完之後,不需要回頭重新讀取舊訊息。
|
||||
|
||||
***基於日誌的訊息代理***
|
||||
|
||||
代理將一個分割槽中的所有訊息分配給同一個消費者節點,並始終以相同的順序傳遞訊息。並行是透過分割槽實現的,消費者透過存檔最近處理訊息的偏移量來跟蹤工作進度。訊息代理將訊息保留在磁碟上,因此如有必要的話,可以回跳並重新讀取舊訊息。
|
||||
|
||||
基於日誌的方法與資料庫中的複製日誌(參見[第5章](ch5.md))和日誌結構儲存引擎(請參閱[第3章](ch3.md))有相似之處。我們看到,這種方法對於消費輸入流,產生衍生狀態與衍生輸出資料流的系統而言特別適用。
|
||||
基於日誌的方法與資料庫中的複製日誌(參見[第5章](ch5.md))和日誌結構儲存引擎(請參閱[第3章](ch3.md))有相似之處。我們看到,這種方法對於消費輸入流,併產生衍生狀態或衍生輸出資料流的系統而言特別適用。
|
||||
|
||||
就流的來源而言,我們討論了幾種可能性:使用者活動事件,定期讀數的感測器,和Feed資料(例如,金融中的市場資料)能夠自然地表示為流。我們發現將資料庫寫入視作流也是很有用的:我們可以捕獲變更日誌 —— 即對資料庫所做的所有變更的歷史記錄 —— 隱式地透過變更資料捕獲,或顯式地透過事件溯源。日誌壓縮允許流也能保有資料庫內容的完整副本。
|
||||
|
||||
將資料庫表示為流為系統整合帶來了很多強大機遇。透過消費變更日誌並將其應用至衍生系統,你能使諸如搜尋索引,快取,以及分析系統這類衍生資料系統不斷保持更新。你甚至能從頭開始,透過讀取從創世至今的所有變更日誌,為現有資料建立全新的檢視。
|
||||
將資料庫表示為流為系統整合帶來了很多強大機遇。透過消費變更日誌並將其應用至衍生系統,你能使諸如搜尋索引、快取以及分析系統這類衍生資料系統不斷保持更新。你甚至能從頭開始,透過讀取從創世至今的所有變更日誌,為現有資料建立全新的檢視。
|
||||
|
||||
像流一樣維護狀態,以及訊息重放的基礎設施,是在各種流處理框架中實現流連線和容錯的基礎。我們討論了流處理的幾種目的,包括搜尋事件模式(複雜事件處理),計算分窗聚合(流分析),以及保證衍生資料系統處於最新狀態(物化檢視)。
|
||||
像流一樣維護狀態以及訊息重播的基礎設施,是在各種流處理框架中實現流連線和容錯的基礎。我們討論了流處理的幾種目的,包括搜尋事件模式(複雜事件處理),計算分窗聚合(流分析),以及保證衍生資料系統處於最新狀態(物化檢視)。
|
||||
|
||||
然後我們討論了在流處理中對時間進行推理的困難,包括處理時間與事件時間戳之間的區別,以及當你認為視窗已經完事之後,如何處理到達的掉隊事件的問題。
|
||||
|
||||
@ -721,7 +719,7 @@ GROUP BY follows.follower_id
|
||||
|
||||
兩個輸入流都是資料庫變更日誌。在這種情況下,一側的每一個變化都與另一側的最新狀態相連線。結果是兩表連線所得物化檢視的變更流。
|
||||
|
||||
最後,我們討論了在流處理中實現容錯和恰好一次語義的技術。與批處理一樣,我們需要放棄任何部分失敗任務的輸出。然而由於流處理長時間執行並持續產生輸出,所以不能簡單地丟棄所有的輸出。相反,可以使用更細粒度的恢復機制,基於微批次,存檔點,事務,或冪等寫入。
|
||||
最後,我們討論了在流處理中實現容錯和恰好一次語義的技術。與批處理一樣,我們需要放棄任何失敗任務的部分輸出。然而由於流處理長時間執行並持續產生輸出,所以不能簡單地丟棄所有的輸出。相反,可以使用更細粒度的恢復機制,基於微批次、存檔點、事務或冪等寫入。
|
||||
|
||||
## 參考文獻
|
||||
|
||||
|
@ -50,7 +50,7 @@
|
||||
|
||||
#### 衍生資料與分散式事務
|
||||
|
||||
保持不同資料系統彼此一致的經典方法涉及分散式事務,如“[原子提交和兩階段提交(2PC)](ch9.md#原子提交和兩階段提交(2PC))”中所述。與分散式事務相比,使用衍生資料系統的方法如何?
|
||||
保持不同資料系統彼此一致的經典方法涉及分散式事務,如“[原子提交與兩階段提交(2PC)](ch9.md#原子提交與兩階段提交(2PC))”中所述。與分散式事務相比,使用衍生資料系統的方法如何?
|
||||
|
||||
在抽象層面,它們透過不同的方式達到類似的目標。分散式事務透過**鎖**進行互斥來決定寫入的順序(參閱“[兩階段鎖定(2PL)](ch7.md#兩階段鎖定(2PL))”),而CDC和事件溯源使用日誌進行排序。分散式事務使用原子提交來確保變更只生效一次,而基於日誌的系統通常基於**確定性重試**和**冪等性**。
|
||||
|
||||
@ -289,11 +289,11 @@
|
||||
|
||||
從資料流的角度思考應用,意味著重新協調應用程式碼和狀態管理之間的關係。將資料庫視作被應用操縱的被動變數,取而代之的是更多地考慮狀態,狀態變更和處理它們的程式碼之間的相互作用與協同關係。應用程式碼透過在另一個地方觸發狀態變更來響應狀態變更。
|
||||
|
||||
我們在“[流與資料庫](ch11.md#流與資料庫)”中看到了這一思路,我們討論了將資料庫的變更日誌視為一種我們可以訂閱的事件流。諸如Actor的訊息傳遞系統(參閱“[訊息傳遞資料流](ch4.md#訊息傳遞資料流)”)也具有響應事件的概念。早在20世紀80年代,**元組空間(tuple space)** 模型就已經探索了表達分散式計算的方式:觀察狀態變更並作出反應【38,39】。
|
||||
我們在“[資料庫與流](ch11.md#資料庫與流)”中看到了這一思路,我們討論了將資料庫的變更日誌視為一種我們可以訂閱的事件流。諸如Actor的訊息傳遞系統(參閱“[訊息傳遞資料流](ch4.md#訊息傳遞資料流)”)也具有響應事件的概念。早在20世紀80年代,**元組空間(tuple space)** 模型就已經探索了表達分散式計算的方式:觀察狀態變更並作出反應【38,39】。
|
||||
|
||||
如前所述,當觸發器由於資料變更而被觸發時,或次級索引更新以反映索引表中的變更時,資料庫內部也發生著類似的情況。分拆資料庫意味著將這個想法應用於在主資料庫之外,用於建立衍生資料集:快取,全文搜尋索引,機器學習或分析系統。我們可以為此使用流處理和訊息傳遞系統。
|
||||
|
||||
需要記住的重要一點是,維護衍生資料不同於執行非同步任務。傳統訊息系統通常是為執行非同步任務設計的(參閱“[與傳統訊息傳遞相比的日誌](ch11.md#與傳統訊息傳遞相比的日誌)”):
|
||||
需要記住的重要一點是,維護衍生資料不同於執行非同步任務。傳統的訊息傳遞系統通常是為執行非同步任務設計的(參閱“[與傳統訊息傳遞相比的日誌](ch11.md#與傳統訊息傳遞相比的日誌)”):
|
||||
|
||||
* 在維護衍生資料時,狀態變更的順序通常很重要(如果多個檢視是從事件日誌衍生的,則需要按照相同的順序處理事件,以便它們之間保持一致)。如“[確認與重傳](ch11.md#確認與重傳)”中所述,許多訊息代理在重傳未確認訊息時沒有此屬性,雙寫也被排除在外(參閱“[保持系統同步](ch11.md#保持系統同步)”)。
|
||||
|
||||
@ -864,7 +864,7 @@ COMMIT;
|
||||
>
|
||||
> 我們應該設法讓他們感到驕傲。
|
||||
|
||||
#### 立法和自律
|
||||
#### 立法與自律
|
||||
|
||||
資料保護法可能有助於維護個人的權利。例如,1995年的“歐洲資料保護指示”規定,個人資料必須“為特定的,明確的和合法的目的收集,而不是以與這些目的不相符的方式進一步處理”,並且資料必須“就收集的目的而言適當,相關,不過分。“【107】。
|
||||
|
||||
|
@ -208,7 +208,7 @@ SELECT COUNT(*)FROM emails WHERE recipient_id = 2 AND unread_flag = true
|
||||
- 如果事務實際上成功了,但是在伺服器試圖向客戶端確認提交成功時網路發生故障(所以客戶端認為提交失敗了),那麼重試事務會導致事務被執行兩次——除非你有一個額外的應用級除重機制。
|
||||
- 如果錯誤是由於負載過大造成的,則重試事務將使問題變得更糟,而不是更好。為了避免這種正反饋迴圈,可以限制重試次數,使用指數退避演算法,並單獨處理與過載相關的錯誤(如果允許)。
|
||||
- 僅在臨時性錯誤(例如,由於死鎖,異常情況,臨時性網路中斷和故障切換)後才值得重試。在發生永久性錯誤(例如,違反約束)之後重試是毫無意義的。
|
||||
- 如果事務在資料庫之外也有副作用,即使事務被中止,也可能發生這些副作用。例如,如果你正在傳送電子郵件,那你肯定不希望每次重試事務時都重新發送電子郵件。如果你想確保幾個不同的系統一起提交或放棄,**二階段提交(2PC, two-phase commit)** 可以提供幫助(“[原子提交和兩階段提交(2PC)](ch9.md#原子提交與二階段提交(2PC))”中將討論這個問題)。
|
||||
- 如果事務在資料庫之外也有副作用,即使事務被中止,也可能發生這些副作用。例如,如果你正在傳送電子郵件,那你肯定不希望每次重試事務時都重新發送電子郵件。如果你想確保幾個不同的系統一起提交或放棄,**兩階段提交(2PC, two-phase commit)** 可以提供幫助(“[原子提交與兩階段提交(2PC)](ch9.md#原子提交與兩階段提交(2PC))”中將討論這個問題)。
|
||||
- 如果客戶端程序在重試中失效,任何試圖寫入資料庫的資料都將丟失。
|
||||
|
||||
## 弱隔離級別
|
||||
|
@ -574,7 +574,7 @@
|
||||
|
||||
|
||||
|
||||
### 原子提交與二階段提交(2PC)
|
||||
### 原子提交與兩階段提交(2PC)
|
||||
|
||||
在[第7章](ch7.md)中我們瞭解到,事務原子性的目的是在多次寫操作中途出錯的情況下,提供一種簡單的語義。事務的結果要麼是成功提交,在這種情況下,事務的所有寫入都是持久化的;要麼是中止,在這種情況下,事務的所有寫入都被回滾(即撤消或丟棄)。
|
||||
|
||||
|
@ -18,13 +18,13 @@
|
||||
|
||||
1.在併發操作的上下文中:描述一個在單個時間點看起來生效的操作,所以另一個併發程序永遠不會遇到處於“半完成”狀態的操作。另見隔離。
|
||||
|
||||
2.在事務的上下文中:將一些寫入操作分為一組,這組寫入要麼全部提交成功,要麼遇到錯誤時全部回滾。參見“[原子性(Atomicity)](ch7.md#原子性(Atomicity))”和“[原子提交與二階段提交(2PC)](ch9.md#原子提交與二階段提交(2PC))”。
|
||||
2.在事務的上下文中:將一些寫入操作分為一組,這組寫入要麼全部提交成功,要麼遇到錯誤時全部回滾。參見“[原子性(Atomicity)](ch7.md#原子性(Atomicity))”和“[原子提交與兩階段提交(2PC)](ch9.md#原子提交與兩階段提交(2PC))”。
|
||||
|
||||
|
||||
|
||||
### 背壓(backpressure)
|
||||
|
||||
接收方接收資料速度較慢時,強制降低傳送方的資料傳送速度。也稱為流量控制。請參閱“[訊息系統](ch11.md#訊息系統)”。
|
||||
接收方接收資料速度較慢時,強制降低傳送方的資料傳送速度。也稱為流量控制。請參閱“[訊息傳遞系統](ch11.md#訊息傳遞系統)”。
|
||||
|
||||
|
||||
|
||||
@ -362,7 +362,7 @@
|
||||
|
||||
### 兩階段提交(2PC, two-phase commit)
|
||||
|
||||
一種確保多個數據庫節點全部提交或全部中止事務的演算法。 請參閱[原子提交與二階段提交(2PC)](ch9.md#原子提交與二階段提交(2PC))”。
|
||||
一種確保多個數據庫節點全部提交或全部中止事務的演算法。 請參閱[原子提交與兩階段提交(2PC)](ch9.md#原子提交與兩階段提交(2PC))”。
|
||||
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user