用日誌構建堅固的數據基礎設施/爲什麼雙寫不好

1. 備註

    本文譯自https://www.confluent.io/blog/using-logs-to-build-a-solid-data-infrastructure-or-why-dual-writes-are-a-bad-idea/,作者Martin Kleppmann,May 29, 2015

    爲了更好的組織語言和理解,符合我們的閱讀習慣,原文的部分段落被合併或者分割,並標註大的章節。原文配圖並未完整引用,但不影響示意、閱讀和內容完整性。爲體現完整性,不刪減文字,保持原文文字內容。翻譯純屬喜愛、分享和收藏。


2. 引言

    本文是我在Craft Conference 2015會議演講的修訂記錄。視頻PPT 請點擊鏈接查看。

    數據庫怎麼可靠地存儲數據在磁盤?用日誌。

    數據庫備份怎麼和另一個備份同步?用日誌。

    分佈式算法比如 Raft 是怎麼達成一致性的?用日誌。

    在一個系統比如 Apache Kafka中數據是怎麼記錄的?用日誌。

    程序的數據基礎設施是怎麼在大規模下保持強健的?猜猜看……

    日誌無所不在。我談的不是純文本日誌文件比如 syslog 或者 log4j,而是僅附加的、完全序列化的記錄。它結構非常簡單,但是你如果熟悉常規數據庫的話會感覺到有點奇怪。不過,一旦你用日誌的方式來理解,讓大規模數據系統變得穩定、可擴展、可維護的問題就會突然變得更容易處理。

    從在 LinkedIn 和其他初創項目中構建可擴展系統的經驗展開,這個演講將探索爲什麼日誌是個好想法 —— 讓維護檢索索引和緩存更容易,讓程序更具擴展性、更健壯,開放數據用以更加豐富的分析,同時避免資源競爭、不一致性和其他令人討厭的問題。


3. 開場

   大家好,我是Martin Kleppmann,致力於超大規模數據系統,尤其是你在互聯網公司裏發現的那種系統;曾任職在LinkedIn,貢獻於一個叫着 Samza 的開源流處理系統。在 LinkedIn 工作期間,我和同事們對怎樣構建操作上強健、穩定和高性能的應用程序有些心得。具體而言,得以和比如Jay Kreps、Chris Riccomini和Neha Narkhede一起共事。他們發現一個特別的基於日誌的、運行相當給力的程序架構類型。在這個演講中我將描述該方式,演示在多種不同計算領域中類似的模式。今天我要講的並不是新的事物,有人早已聽聞這些方法,但並非應該的那麼廣泛。你如果致力於不僅僅運用一個數據庫的非普通的程序,可能會發現這些方法非常實用。

    此刻,我正在休假來給 O’Reilly 寫一本書《設計數據集中的程序》。這本書試圖收集最近十年中我們習得的關於數據系統的重要基礎性經驗,覆蓋數據庫架構、緩存、索引、批處理和流處理。該書並不關於某個特定的數據庫或者工具,而是運用在實踐中的整個不同工具和算法、之間權衡和優劣。本演講的一部分基於我爲寫這本書所做的研究。你如果感覺有意思,可在本書中找到更多的細節和背景。前面七個章節可見於當前的早期版本

    假設你正在搞一個網頁應用。最簡單情況下,它可能是典型的三層架構:客戶端(瀏覽器、移動 app 或者兩者),它們發送請求到網頁程序。網頁程序也就是程序代碼或者業務邏輯承載者。程序要保存什麼都需要保存在數據庫中,查詢之前存儲的東西都需要查詢數據庫。該方式簡單、容易理解並且運行相當好。


    但是事情常常不會長久的這麼簡單下去。或許你有了更多的用戶,發送更多請求,數據庫變得緩慢,然後你增加緩存以求提高速度,比如 Memcached 或者 Redis;或許你需要爲程序增加全文檢索功能,並且數據庫內置基礎搜索工具不夠好時,最終你會設立一個單獨的索引服務比如 Elasticsearch 或者 Solr;或許你需要些圖形操作,而這些在關係型和文檔數據庫中並不高效,比如社交功能或者推薦,所以你給系統增加一個單獨的圖形索引;或許你要把巨量操作移出網頁請求流而成爲一個異步後臺處理,所以你添加一個消息隊列以發送工作任務到後臺程序。如此情形後續將變得更加複雜。


    截至目前,系統的其他部分又變慢了,所以你添加另一個緩存。更多的緩存總是讓速度更快,對吧?但是現在你有一大堆的程序和服務,所以你需要增加一個監控系統來查看他們到底是否在正在運行,而這個監控系統又是另一個自治的系統。然後,你需要發送通知,比如郵件或者推送通知到用戶,所以你得在後臺程序之外串接一個通知系統,並且可能需要一些獨立的數據庫來保存事務。現在你有很多數據需要分析,並且不能在主數據庫上進行大量的業務分析,所以增加 Hadoop 或者數據倉庫,並從數據庫中導出數據到其中。現在業務分析搞定了,那麼假設搜索系統掛了,你知道所有數據都在HDFS,你應該在 Hadoop 中創建檢索索引並推送到搜索服務器,然後整個系統將會越來越複雜錯亂。

    我們是怎麼變得如此的?怎麼會如此複雜,什麼都在彼此調用,沒人知道是怎麼回事?

    一路上我們所做的某個決定並非不好。沒有一個數據庫或者工具能夠做完程序所需要的所有事情。我們爲一個任務用最好的工具,爲具備多個功能的程序用多個工具。同樣,隨着系統體量的增加,爲了程序的可管理性我們需要一種把程序分解爲更小單元的方法,這就是微服務。不過如果你的系統變成一堆彼此調用而混亂的組件,那這本身也就不可管理了。


    簡單地有多個不同的存儲系統,這本身不是問題,如果他們都彼此獨立,那麼也沒啥大不了。真正的問題是他們有相同或者相關的數據,但不同的格式。比如在全文檢索中的文檔一般也存在數據庫中,因爲檢索索引並不作爲記錄系統;緩存中的數據和某些數據庫中的數據重複(或者夾雜了其他數據或者呈現爲 HTML 格式或者其他)—— 緩存定義如此。同樣,非規格化又是另一種格式的重複數據,類似於緩存。如果某些值重新計算太費力,你可存儲在某些地方,但是你還得在底層數據發生變化時保持更新。聚合,比如計數、求和或者一堆記錄的平均值(常從監控系統或者分析系統中獲取)又是一種冗餘數據。我並不是說這些重複數據不好,根本不是。緩存、索引和其他冗餘數據格式都是獲取良好讀性能所必須的。但是,讓這些不同呈現方式和存儲系統之間的數據保持同步又是一個大的挑戰。


    找不到一個更好的詞,我稱這個問題爲“數據集成”,表示“確保數據保存在正確的地方”。一點數據不論何時在一個地方發生變化時,它都需要在其他地方(備份或者派生的數據)對應地變化。


4. 雙寫

    那麼我們怎麼同步這些不同的數據系統呢?這裏有些不同的技術。一個流行的辦法爲雙寫。


    雙寫很簡單。你的程序來負責更新不同地方的數據。比如,一個用戶提交數據到網頁程序,網頁程序的某些代碼首先寫入數據到數據庫,再廢棄或者刷新緩存中對應的實體,然後重新索引全文檢索的文檔,如此等等(或者並行處理這些事情,但並不影響我們的示意)。

    雙寫方式很流行,因爲它很容易實現,並且起初或多或少總能發揮作用。但是我想爭議的是它並非是一個好主意,因爲有些重要的問題。首先一個就是資源競爭。下圖表示兩個客戶端雙寫到兩個數據庫。時間先後從左到右,如黑色剪頭所指。


    這裏第一個客戶端(藍綠色)設置鍵值 X=A。首先發送一個請求到第一個數據倉庫 —— 可能是個數據庫,數據庫迴應說寫入成功;然後發動請求到第二個數據倉庫 —— 也許是個檢索索引,也設置 X=A。同時,另一個客戶端(紅色)也想設置同一個鍵X爲另一個不同的值 B。第二個客戶端用同樣的方式來處理,首先發送請求 X=B 到第一個數據倉庫,然後 X=B 到第二個數據倉庫。

    這些寫操作都成功了。但是,看看每個數據倉庫中都存儲了什麼值。


    第一個數據倉庫中,值首先由藍綠色客戶端設置爲 A,然後由紅色客戶端設置爲 B,所以最終值爲 B。第二個數據倉庫中,兩個請求不同次序的到達:值首先設置爲 B,然後設置爲 A,所以最終值爲 A。現在兩個數據倉庫彼此不一致,並且永久保持不一致直到以後再複寫鍵X 爲止。最糟糕的是,可能甚至你都不會發現數據庫和檢索索引已經不同步了,因爲沒有錯誤發生。可能六個月後當你做完全不同的事情時才發現你的數據庫和索引不匹配,並且不知道是怎麼發生的。僅此已足以讓大家摒棄雙寫。等等,還有更多……


    我們看看非規格化數據。比如你有個用戶可以彼此發送消息或郵件的程序,每個用戶有一個收件箱。當一個新消息發送時,要做兩件事:在用戶收件箱中添加該消息到消息列表中;增加用戶未讀消息數。因總是在用戶界面顯示消息數所以設立個單獨的計數,雖然每次需要顯示該數值時掃描消息列表也不會太慢,但是該計算爲非規格化信息,它從收件箱實際消息衍生而來,每次消息變化時你都需要對應的更新該計數。簡單而言,一個客戶端,一個數據庫。想想隨着時間的推移會發生什麼:首先客戶端在收件人收件箱中插入新消息,然後發送請求增加未讀數。


    就在此時,出現一個問題 —— 或許數據庫掛了,或者一個進程崩潰,或者網絡中斷,或者網線拔錯了。不管什麼原因,更新未讀數失敗了。現在數據庫不一致了:消息已添加到收件箱,但是未讀數並未更新,除非你間歇性從零開始重新計算計數值或者收回插入的新消息,否則它將永遠持續不一致。

    當然你會爭辯說這個問題早在幾十年前就通過事務解決了。原子性,也就是“ACID”中的Atomicity,表示在一個事務中做幾個更改,他們要麼都發生要麼一個都不發生。原子性的目的在於精確地解決這個問題,在寫入過程中如果發生錯誤,你不必擔心半途而廢的修改導致數據的不一致性。包裝兩個寫入到一個事務中的傳統做法在支持它的數據庫中可行,但是許多新生代的數據庫中卻不然,所以需要你自行解決。同時,如果這些非規格化的信息存儲在另一個數據庫中,比如郵件存儲在一個數據庫中而未讀數在 Redis 中時,你將無法綁定這些寫入操作在單個事務中。如果一個寫入成功而另一個失敗,你就很難清除這個不一致問題。

    一些系統支持分佈式事務,基於比如兩階段提交。但是當代很多數據倉庫都不支持它,即便支持,分佈式事務是好是壞還未有定論。所以我們說雙寫程序需要自行處理部分失敗,而這個比較困難。


5. 日誌

    回到最初的問題,怎麼確保數據保存在正確的地方?怎麼存儲一份同樣的數據備份在幾個不同的存儲系統中,並且讓他們在數據變化時持續同步?我們看到,雙寫並非想要的解決方案,因爲它會因資源競爭和部分失敗引入非一致性問題。我們怎麼做才更好呢?


    我是個極簡主義者。簡單方案的偉大之處在於你有機會理解他們並且說服自己那是正確的。這樣的話,我看到的最簡單的方案就是把寫入操作置於固定次序,並且依照固定次序保持下來。如果序列化地寫入,沒有併發,那麼就已經排除了資源競爭的可能性。此外,如果你記錄了寫入操作的次序,從部分失敗中回恢復將會變得容易得多,我後面會演示給大家。我提議的極簡方案就是任何時候寫入數據時,我們就附加這個寫入操作到記錄序列的尾端。這個序列是完全有序的,僅可附加(從不更改已有記錄,只是在尾端增加新記錄),並且是永久的(持續存儲在磁盤中)。

    前面的圖片呈現了這麼一個數據結構示例:從左到右,記錄了首先寫入 X=5,然後 Y=8,然後 X=6,以此類推。這個數據結構我們稱之爲日誌。日誌有趣的地方在於它出現在多個不同的計算領域。雖然看起來非常簡單感覺不大可能行得通,但事實證明非常強大。


   當我們說“日誌”時,你可能首先想到的是從 log4j 或者 syslog 中看到的純文本的應用日誌。比如,上面的是一行 Nginx 訪問日誌,表示某個 IP 地址在某個時刻訪問了某個文件,還包括引用、用戶瀏覽器、返回碼和一些其他的東西。當然這是其中一種日誌。我這裏說的日誌是更綜合的東西,指完全次序化的、僅附加的、持久化的任何一種數據結構。任何僅附加的文件。


   剩下的時間我將說說一些日誌在實踐中怎麼運用的例子。其實日誌已經在今天的多個數據庫和系統中展現。一旦瞭解了日誌在多個不同系統中如何使用,我們將會處於更好的場地來理解他們怎麼幫助我們解決數據集成的問題。


5.1 存儲引擎B-Trees

    有四個運用日誌的地方。首先就是數據庫存儲引擎的內部機制。


    還記得算法課上的 B-Trees 麼?他們是存儲引擎中廣泛應用的數據結構,幾乎所有的關係型數據庫和許多非關係型數據庫都在使用。簡而言之,一個 B-Tree由 pages 構成,page 爲磁盤中固定大小的塊,通常是4KB 或者8KB大小。當查找某個鍵時,從根部的 page 開始。page 包含指向其他 pages 的指針,每個指針標記有一定範圍的鍵。比如一個鍵在0和100之間,順着第一個指針;如果在100和300之間,就順着第二個指針;以此類推。指針指引到另一個 page,這個page進一步分解鍵的範圍到子範圍,最終定位到包含所尋找的鍵的 page。

    如果想插入一個新的鍵值對到B-Tree 會發生什麼呢?你得把它插入到鍵範圍包含這個鍵的 page 中。如果這個 page 有足夠的空閒空間,那沒問題;如果 page 滿了,那這個 page 就需要分裂爲兩個獨立的 pages。


    當分裂一個 page 時,需要寫入至少3個 pages 到磁盤。兩個 pages 由分裂而生,還有一個父page(來更新指針到分裂後的 pages )。不過,這些 pages 可以存儲在磁盤中多個不同的位置。

    問題就來了,如果數據庫在部分 pages 寫入磁盤後,操作半途中崩潰了(或者斷電,或者其他問題發生)會發生什麼?這種情況下,在一些 pages 中有老數據(分裂之前),其他 pages 中有新數據(分裂後)。這樣很可能就會有不指向任何東西的掛起的指針或者 pages。換言之,一個破損的索引。


5.1.1 WAL

    存儲引擎已經處理這個問題幾十年了,那麼他們怎麼讓 B-Tree 可靠的呢?答案是用寫入前日誌(write-ahead log,WAL)。寫入前日誌是種特殊的日誌,比如磁盤中的僅附加性文件。存儲引擎對 B-Tree 做任何修改之前,它首先把這個修改寫入 WAL。只有在寫入 WAL 之後,並且持久存入磁盤後,它才允許去更改實際的 B-Tree。這就使得 B-Tree 可靠了。如果數據庫在數據正在附加到 WAL 時崩潰了,沒關係,因爲 B-Tree 都還尚未被觸及;如果 B-Tree 正在被更改時,也沒關係,因爲 WAL 包含有將要修改的信息。數據庫崩潰後運行時,它就用 WAL 來恢復 B-Tree 並進入一致狀態。這就是第一個演示日誌是個好辦法的實例。


    當今存儲引擎並不止步於 B-Trees。如果我們把所有東西都寫入日誌,可能就會把日誌當着主要的存儲媒介,這就是所謂的日誌結構存儲,使用於 HBase 和 Cassandra,以及出現在Riak中的一個變種。在日誌結構存儲中我們並非總是附加到同一個文件,因爲文件將會變得太大,也很困難去查找需要的鍵。代替之,日誌被分解爲片,有時存儲引擎會合並分片和丟棄重複的鍵。分片也可按照鍵來內部排序,使得查找鍵更容易,合併分片更簡單。然後,這些分片還是日誌,他們只能次序寫入,寫入後不可更改。正如你所見,日誌在存儲引擎中發揮着重要的功能。


5.2 數據庫複製

    第二個日誌運用實例,數據庫複製(備份)。

    複製是許多數據庫的一個功能,允許保留一份相同數據的備份在幾個不同的節點上。利於分散負載,也意味着一個節點掛掉,還可故障轉移到另一個節點。


    有幾個不同的方式來實現複製,但有個共同的選擇就是指定某個節點爲領導者(leader)(also known as primary or master),其他複製爲跟隨者(follower)(also known as standby or slave)。我不喜歡 master/slave 這個術語,所以下面只用領導者/跟隨者(leader/follower)。

    當客戶端要向數據庫寫入時,它需要和領導者會話。只讀客戶端連接領導者和跟隨者都行(雖然跟隨者往往是異步的,所以如果最近的寫入並未執行的話可能會有些許延遲數據)。

    那麼客戶端寫入數據到領導者時,數據又是如何跑到跟隨者那去的呢?驚奇的是,他們用日誌。他們用複製日誌,這個實際上和寫入前日誌完全一致(Postgre 就是如此)或者是個單獨的複製日誌(MySQL 如此)。


    複製日誌是這麼運行的,在數據寫入領導的同時,也會附加到複製日誌。跟隨者依照寫入的次序讀取日誌,並應用到自身的那份數據備份中。最後,每個跟隨者都依照領導者的處理次序去處理同樣的寫入操作,這樣就會保有同樣的一份數據。即便領導者進行並行寫入,日誌中的寫入照樣嚴格排序。如此,日誌其實就排解了寫入的併發 —— “消除所有寫入流中的非定論性”,跟隨者也不用質疑寫入的次序。

    那麼之前討論到的雙寫資源競呢?資源競爭在基於領導者的複製中不會發生,因爲客戶端不會直接寫入跟隨者。跟隨者唯一處理的寫入操作來源於接收到的複製日誌。並且由於日誌已解決好寫入的次序問題,也就不存在寫入先後的歧義。至於雙寫資源競爭的第二個問題呢?這個照樣會發生,一個跟隨者從事務中成功處理第一個寫入操作,但是第二個失敗了(也許磁盤滿了,或者網絡中斷)。


    如果領導者和跟隨者之間的網絡中斷了,複製日誌將無法從領導者流傳到跟隨者。就像之前討論到的,這將導致不一致的備份。那麼數據庫複製如何從這樣的錯誤中恢復並避免非一致性呢?請注意,日誌有個很好的特徵。因爲領導者只是附加日誌,我們可以給每一個記錄添加一個只增的序列號(日誌位置或者偏移量)。而且,跟隨者只按照序列順序來處理(比如從左到右,依照不斷增加的日誌位置次序),如此我們就可以用一個數字來描述一個跟隨者當前的狀態,也就是最後一條被處理記錄的位置。只要你知道了跟隨者當前日誌中的位置,你就可以肯定之前的記錄已處理完畢,之後的記錄都還尚未處理,這也使得錯誤恢復非常簡單。跟隨者如果和領導者失聯,或者崩潰了,它也就只需要存儲複製日誌中已處理的日誌位置;在連接恢復時,跟隨者就向領導者索要從之前處理的最後日誌位置之後的複製日誌。這樣,跟隨者就可以追上之前失聯時缺失的寫入操作,並且不會丟失任何數據或者收到重複數據。完全次序化的日誌比起單獨跟蹤每一個寫入來說恢復就容易得多了。


5.3 分佈式一致性

    第三個實踐實例在另一個領域,分佈式一致性。


    達成一致性在分佈式系統中是個衆所周知也常常被討論的問題。現實生活中的一個例子就是讓一羣朋友關於在哪裏吃午飯達成共識。這是高度文明中的一個特徵,一個很困難的問題,尤其當部分朋友還不在意(所以他們不迴應你的提問)或者挑食。

    在計算領域,分佈式數據庫系統中你想達成一致的一個實例就是,你想讓所有數據庫都一致同意哪個節點來作爲數據庫某個分區(分片)的領導者。讓他們同意誰是領導者非常重要,如果兩個節點都認爲他們是領導者,他們都接受客戶端的寫入操作。然後其中一個發現它不是領導者,那麼所接受的寫入都將會丟失。這就是所謂的腦裂,會導致煩躁的數據丟失


    有幾種實現一致性的算法。Paxos 也許是最知名的,也有 Zab(Zookeeper 使用),Raft 和其他的。這些算法很敏銳,有些並不明顯的微妙之處。本演講中我只是簡單的走一遍 Raft 算法的一部分。

    在一致系統中有多個節點(該算法爲三個)來負責決策某個變量應該是什麼值。一個客戶端發送一個值到一個 Raft 節點,比如 X=8(也可表示節點 X 是分區8的領導者),這個節點收集其他節點的選票。如果大多數節點同意該值爲X=8,那麼該節點將提交這個值。提交之後呢?Raft 中,這個值將會附加在日誌的末端。Raft 不僅讓多個節點針對某個值達成共識,而且實質上還持續構建這些值的日誌。所有 Raft 節點都確保他們的日誌中擁有完全一樣的提交數據順序,而客戶端消費這些日誌。


    一旦新值被提交、附加到日誌並複製到其他節點,最初發送提議這個值的客戶端將收到一個迴應說系統已達成共識並且提交的值現在已成爲 Raft 日誌的一部分。

    理論上講,一致性問題和原子廣播 —— 創建一次性交付日誌 —— 是可彼此減少的。也即是 Raft 的日誌應用不僅是一個便捷的實現細節,也還反射出一致性問題的一個重要屬性。


5.4 Kafka

    好了,我們已看到日誌在許多計算領域中是個不斷復現的主題,包括存儲引擎、數據庫複製和一致性。作爲第四個也是最後一個例子,我來講講又一個圍繞日誌概念的系統,Apache Kafka。Kafka 有趣的地方是它對你並不隱藏日誌,不把日誌作爲實現細節,而是暴露給你,所以你也可以據此創建程序。之前你可能聽說過 Kafka,它是個開源項目,最初由 LinkedIn 開發,現在作爲一個Apache 項目,有很多貢獻者和用戶。



    Kafka的典型應用就是作爲消息隊列,所以可以與 AMQP、JMS 和其他的消息系統相提並論。 Kafka 有兩種客戶端,生產者(發送消息到 Kafka)和消費者(訂閱 Kafka 的消息流)。舉例而言,生產者可以是你的網頁服務器或者移動 App,發送到 Kafka 的數據可以是日誌信息,比如表示用戶在某個時間點擊某個鏈接的事件。客戶端就是各式各樣的處理程序,比如生成分析報告、監控異常行爲、爲用戶生成個人推薦等等。

    Kafka 與其他消息隊列相比的不同之處在於它的數據用日誌來構成。實際上,它有很多日誌。Kafka 的數據流分割爲分區,每個分區就是一個日誌(完全序列化的消息)。不同分區彼此獨立,分區間也就不需要次序保障,使得不同分區可被不同服務器處理,這對 Kafka 的擴展性非常重要。每個分區都存儲在磁盤並在幾個服務器間複製,持久且能承載機器故障而不會造成數據丟失。生產和消費日誌非常類似於前面在數據庫複製中看到的內容。

  • 每個發送到 Kafka 的消息都附加到分區的尾部,Kafka 只支持附加到日誌尾部這個寫入操作,不可能更改已有消息。
  • 每個分區中,消息有單向遞增的偏移量(日誌位置),客戶端消費Kafka的消息就從某個偏移量開始順序讀取消息,這個偏移量由消費者管理。


6. 日誌最佳實踐

    回到演講開始所說的數據集成問題。假設你有一堆的不同數據倉庫、緩存和索引需要保持彼此同步。既然我們已經看到實踐中的幾個日誌程序示例,那麼我們知道怎麼去更好的構建這些系統了嗎?


6.1 停用雙寫

    首先,停止使用雙寫。正如討論所得,雙寫可能造成數據不一致,除非你仔細考慮過程序中潛在的資源競爭和部分失敗問題。請注意這個不一致性並非異步系統中常被提及的一種“最終一致”,我這裏說的是永久不一致。如果雙寫到兩個不同的數據倉庫,由於資源競爭或者部分失敗,造成的不一致是不會簡單的自行解決。你必須採取明確的行動去搜索不匹配的數據再解決掉(這個很難,因爲數據在不斷地變化)。


6.2 使用日誌

    我們需要比雙寫更好的方法來讓不同數據倉庫中的數據保持同步。


    我要提議的是,與其讓程序直接寫入多個數據倉庫,不如讓程序只附加數據到日誌(比如Kafka)。所有數據的不同呈現方式,數據庫、緩存、索引,都由順序消費日誌來構成。每個需要保持同步的數據倉庫都是日誌的獨立消費者,每個消費者在日誌中提取數據,每次一條記錄,然後寫入到自己的數據倉庫。日誌確保消費者看到的記錄都在相同的次序;通過讓寫入操作在相同次序,資源競爭的問題將得以消除。這個就很像之前講到的數據庫複製。

    部分失敗的問題?如果其中一個倉儲有問題一時半會兒不能接受寫入操作將會怎麼樣呢?這個問題日誌也解決了。每個消費者跟蹤已處理的日誌位置,當這個消費者的錯誤解決後,它可從日誌中上次處理的位置開始重新處理記錄。通過這樣的方式,數據倉庫並不會丟失任何更新,即便離線一段時間後。這對解耦系統組件很有幫助,即便一個數據倉庫有問題,系統的其他部分也並不受影響。


6.2.1 日誌問題 —— 延遲

    日誌 —— 把寫入操作放入完全序列中的簡單粗暴想法 —— 還有問題。只有一個遺留問題。日誌的消費者都是異步更新他們的數據倉庫,所以他們都是最終一致的。讀取這些數據倉庫就像讀取數據庫的跟隨者,他們可能會些許延後於最新的寫入,所以你不能保證所寫即所讀(當然並非線性問題)。

    我認爲,這個問題可以通過在日誌頂層分層事務協議來克服,但這是個研究性領域還遠未能在生產系統中廣泛運用。當下,一個更好的選擇是在數據庫中提取日誌。


    更改數據捕捉,最近我就在寫這方面的東西(並運用於PostGreSQL)。只要你只寫入到單個數據庫(不搞雙寫),並且從數據庫中拿取寫入日誌(按照提交到數據庫的順序),那麼它就將會和直接寫入日誌的方式一樣發揮功效。

    由於位於日誌之前的這個數據庫同步執行寫入操作,你就可以通過它來執行要求“即時一致性”的讀取操作(線性化),並強制約束(比如要求餘額永不負數)。途經數據庫也意味着你不必把日誌當着記錄系統(日誌可能因爲使用新技術讓場景可怕)。你如果有比較瞭解並喜歡的已有數據庫,那就可以從這個數據庫中提取變化日誌,也能獲取面向未來架構的所有優點。在即將來臨的會議演講中我將談及這個話題更多的細節。


7. 結尾

    在演講結束時,我給大家出一個思考題。


    我們用的大多數 API 都有讀寫端。在RESTful 中,GET 表示讀(比如無副作用的操作),POST、PUT 和 DELETE 表示寫。如果只寫入一個系統,這些寫操作都沒問題,但是有多個系統時,你一旦上線雙寫就將面臨前面提及的問題。想象下API沒有這些寫入端的系統,保留所有 GET 請求,但是禁止所有 POST、PUT 和 DELETE。唯一寫入系統的途徑就是附加他們到日誌中,並讓系統去消費日誌(日誌必須在系統之外,然後同一個日誌能有多個消費者)。

    舉例,想象一個 Elasticsearch 變種,你不能通過 REST API 去寫文檔,只能通過發送到 Kafka 來寫。Elasticsearch 可以內置一個 Kafka 消費者來獲取文檔並添加到索引。這就可簡化 Elasticsearch 的內部機制,因爲它不用再擔心併發控制,複製實現也會更簡單,並且和其他消費同一個日誌的系統也沒啥關係。

    我最喜歡面向日誌架構的功能是,如果構建一個衍生數據倉庫,你就開啓一個新的從頭消費的消費者,通過日誌歷史把所有寫入應用於數據倉庫。當消費到最後時,你就能有個全新的數據視圖,而且簡單地繼續消費日誌就能讓它保持更新。這讓已有數據試驗新的呈現方式變得非常容易,比如換種索引方式。你可構建已有數據的實驗性新的索引或者視圖而不妨礙任何已有數據。如果測驗行得通,你可轉移用戶讀取新的視圖;如果不,丟棄他們即可。這就給你了極大的自由來試驗和調整你的程序。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章