Apache Kafka是數據庫嗎?


最近思路有些枯竭,找些務虛的話題來湊。本文內容完全來自於Martin Kelppmann在2019年Kafka倫敦峰會上的演講。順便提一句,Kelppmann是《Designing Data-Intensive Applications》的作者。提起DDIA的大名想必各位應該是有所耳聞的。

Apache Kafka是數據庫嗎?你可能會覺得奇怪,Kafka怎么可能是數據庫呢?它沒有schema,也沒有表,更沒有索引。它僅僅是生產消息流、消費消息流而已。從這個角度來說Kafka的確不像數據庫,至少不像我們熟知的關系型數據庫。那么到底什么是數據庫呢?或者說什么特性使得一個系統可以被稱為數據庫?經典的教科書是這么說的:數據庫是提供 ACID 特性的,即atomicity、consistency、isolation和durability。好了,現在問題演變成了Apache Kafka支持ACID嗎?如果它支持,Kafka又是怎么支持的呢?要回答這些問題,我們依次討論下ACID。

1、持久性(durability)

我們先從最容易的持久性開始說起,因為持久性最容易理解。在80年代持久性指的是把數據寫入到磁帶中,這是一種很古老的存儲設備,現在應該已經絕跡了。目前實現持久性更常見的做法是將數據寫入到物理磁盤上,而這也只能實現單機的持久性。當演進到分布式系統時代后,持久性指的是將數據通過備份機制拷貝到多台機器的磁盤上。很多數據庫廠商都有自己的分布式系統解決方案,如GreenPlum和Oracle RAC。它們都提供了這種多機備份的持久性。和它們類似,Apache Kafka天然也是支持這種持久性的,它提供的副本機制在實現原理上幾乎和數據庫廠商的方案是一樣的。

2、原子性(atomicity)

數據庫中的原子性和多線程領域內的原子性不是一回事。我們知道在Java中有AtomicInteger這樣的類能夠提供線程安全的整數操作服務,這里的atomicity關心的是在多個線程並發的情況下如何保證正確性的問題。而在數據庫領域,原子性關心的是如何應對錯誤或異常情況,特別是對於事務的處理。如果服務發生故障,之前提交的事務要保證已經持久化,而當前運行的事務要終止(abort),它執行的所有操作都要回滾,最終的狀態就好像該事務從未運行過那樣。舉個實際的例子,比如下面這張圖:

在異質分布式系統中一個比較經典的問題就是如何確保不同系統之間的數據同步。比如這個圖中如何確保數據庫、緩存和搜索索引之間的數據一致性就是一個關於原子性的問題:app寫入數據庫的寫更新如何同步到cache和Index中,更關鍵的是如何確保這些寫更新與之前寫數據庫是原子性的,要么它們全部寫入成功,要么全部寫入失敗。我之前在知乎上也回答過一個類似的帖子,是關於“如何保持mysql和redis中數據一致性”的。令人意外地收獲了近100個贊,感覺比我回答10個Kafka問題得到的贊還要多,這也足見這種一致性問題是多么地受歡迎。

 

顯然,要實現這種分布式場景下的數據一致性並不容易。一個典型的異常場景就是當發生cache寫入成功,而Index寫入失敗時,應用程序應該如何處理?如下圖所示:

讓app重試似乎是一個可行的選擇,但重試的頻率該怎么設定呢?更要命的是,如果因為網絡的問題使得Index其實寫入成功,但response返回失敗,此時app重試有可能發生重復生產數據的問題,這還需要Index端有數據去重的能力。如果是撤銷數據庫和cache之前的寫入呢? 如下圖所示:

似乎這個方法也是可行的,但這就有了linearizability的問題了:即用戶在某個時刻T看到了這個寫入帶來的新值,但在之后的某個時刻T1該值又變回了之前的老值,這必然造成用戶的困擾,因此也不是一個好辦法。

 

實際上,解決這個問題的常見做法是采用兩階段提交(2PC)這樣分布式事務。不過2PC是出了名的慢,而且存在單點故障的隱患(coordinator),更重要的是它要求所有系統都要支持XA,但像Redis和ElasticSearch這樣的系統本質上是不支持XA的,因此也就不能使用2PC來保證原子性。

第三個方法是采用基於日志結構的消息隊列來實現,比如使用Kafka來做,如下圖所示:

在這個架構中app僅僅是向Kafka寫入消息,而下面的數據庫、cache和index作為獨立的consumer消費這個日志——Kafka分區的順序性保證了app端更新操作的順序性。如果某個consumer消費速度慢於其他consumer也沒關系,畢竟消息依然在Kafka中保存着。總而言之,有了Kafka所有的異質系統都能以相同的順序應用app端的更新操作,從而實現了數據的最終一致性。這種方法有個專屬的名字,叫capture data change,也稱CDC。

3、隔離性(isolation)

在傳統的關系型數據庫中最強的隔離級別通常是指serializability,國內一般翻譯成可串行化或串行化。表達的思想就是連接數據庫的每個客戶端在執行各自的事務時數據庫會給它們一個假象:仿佛每個客戶端的事務都順序執行的,即執行完一個事務之后再開始執行下一個事務。其實數據庫端同時會處理多個事務,但serializability保證了它們就像單獨執行一樣。舉個例子,在一個論壇系統中,每個新用戶都需要注冊一個唯一的用戶名。一個簡單的app實現邏輯大概是這樣的:

1) 首先,發起SQL查詢:select count(*) from user_accounts where username = 'jane',查看是否存在名為jane的用戶;

2. 如果返回0, 則執行 insert into user_accounts(username, ...) values("janes", ...) 注冊用戶

顯然存在某個特殊的時刻,使得兩個新用戶同時發現某個用戶名可用,從而最終注冊了相同的用戶名,如下圖所示:

這種就不是serializability級別的隔離,如果要實現這種唯一性,你就需要提高數據庫的隔離級別到serializability。針對這個需求,我們可以使用Kafka來幫助實現嗎?當然是可以的!如下圖所示:

如果把用戶名作為key,那么顯然請求同一個用戶名的用戶必然訪問Kafka主題的同一個分區上,此時根據Kafka分區消息寫入前后順序來確定誰先誰后就是一個自然的選擇。數據庫讀取Kafka分區中的注冊消息,發現紅色標識的用戶最先寫入了key=jane的消息,那么當它再次讀到key=jane的消息時就能明確拒絕綠色用戶發起的請求,因為jane用戶名已經被注冊了。當然要實現這一整套的流程,你需要的不僅是Kafka,更要是一套相應的流處理管道,比如使用Kafka Streams。但無論如何,Kafka可以被用來實現這種事務的隔離性。依托Kafka的好處在於它不僅實現了serializability,而且依靠Kafka的分區機制,它能處理多個不同的用戶名注冊,因而也實現了scalability。

4、一致性(consistency)

最后說說一致性。按照Kelppmann大神的原話,這是一個很奇怪的屬性:在所有ACID特性中,其他三項特性的確屬於數據庫層面需要實現或保證的,但只有一致性是由用戶來保證的。嚴格來說,它不屬於數據庫的特性,而應該屬於使用數據庫的一種方式。坦率說第一次聽到這句話時我本人還是有點震驚的,因為從沒有往這個方面考慮過,但仔細想想還真是這么回事。比如剛才的注冊用戶名的例子中我們要求每個用戶名是唯一的。這種一致性約束是由我們用戶做出的,而不是數據庫本身。數據庫本身並不關心或並不知道用戶名是否應該是唯一的。針對Kafka而言,這種一致性又意味着什么呢?Kelppmann沒有具體展開,但我個人認為他應該指的是linearizability、消息順序之間的一致性以及分布式事務。幸運的是,Kafka的備份機制實現了linearizability和total order broadcast,而且在Kafka 0.11開始也支持分布式事務了。

 

至此,我們說完了經典數據庫中的ACID特性以及在Kafka中是如何支持它們的。現在你覺得Kafka是數據庫了嗎:) 這是個開放的問題,我們可以一起討論下~~

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM