讀文有感:Kafka 官方設(shè)計(jì)文檔

-? ? ?數(shù)據(jù)持久化:無(wú)懼文件系統(tǒng)? ? -
磁盤(pán)的讀寫(xiě)速度,取決于如何讀寫(xiě)。對(duì)于線(xiàn)性讀寫(xiě)方式,操作系統(tǒng)做了充分的優(yōu)化:提前讀 - 預(yù)取若干數(shù)據(jù)塊,滯后寫(xiě) - 將小的邏輯寫(xiě)操作合并成一個(gè)大的物理寫(xiě)操作。
研究表明:順序讀寫(xiě)磁盤(pán)(sequential disk access)的速度有些時(shí)候比隨機(jī)訪問(wèn)內(nèi)存還要快。
現(xiàn)代操作系統(tǒng)激進(jìn)地盡可能將空閑內(nèi)存用作磁盤(pán)緩存。所有磁盤(pán)讀寫(xiě)都經(jīng)過(guò)操作系統(tǒng)提供的統(tǒng)一緩存。這個(gè)特性沒(méi)法輕易關(guān)閉,除非直接 I/O (direct I/O),因此,如果程序在用戶(hù)進(jìn)程中進(jìn)行數(shù)據(jù)緩存,緩存的數(shù)據(jù)通常也是和操作系統(tǒng)頁(yè)緩存重復(fù)的,緩存兩遍,沒(méi)啥意義,也浪費(fèi)內(nèi)存。
而且,Kafka 是構(gòu)建在 JVM 之上的,了解 Java 內(nèi)存使用方式的人應(yīng)該都知道:
對(duì)象的內(nèi)存開(kāi)銷(xiāo)非常高,通常是實(shí)際數(shù)據(jù)大小的2倍(甚至更多)
隨著堆上數(shù)據(jù)量增大,Java 的 GC 表現(xiàn)也會(huì)更糟糕
因此,使用文件系統(tǒng)并依賴(lài)于操作系統(tǒng)內(nèi)存頁(yè)緩存,優(yōu)于在程序中維護(hù)一塊內(nèi)存緩存或其它結(jié)構(gòu)。至少操作系統(tǒng)內(nèi)存頁(yè)緩存的可用內(nèi)存翻倍了。另外,如果使用緊湊的字節(jié)結(jié)構(gòu)來(lái)緩存數(shù)據(jù),相比使用對(duì)象,可用內(nèi)存可能還會(huì)翻倍。在 32GB 內(nèi)存的機(jī)器上這么搞,緩存可用到 20-30GB,還不會(huì)對(duì) GC 造成了什么壞影響。并且,即使服務(wù)重啟,這塊緩存空間也是熱的(除非機(jī)器重啟),用戶(hù)進(jìn)程內(nèi)的內(nèi)存緩存在服務(wù)重啟后得重建(10GB的數(shù)據(jù)緩存可能需要10分鐘左右)。
這樣也可以簡(jiǎn)化代碼邏輯,因?yàn)榫彺婧臀募到y(tǒng)之間的一致性由操作系統(tǒng)來(lái)保證了。
這樣一分析,設(shè)計(jì)就簡(jiǎn)單了:我們反其道而行之,所有數(shù)據(jù)都直接寫(xiě)到文件系統(tǒng)上持久化日志文件中,不需要在程序中使用內(nèi)存緩存,也不必確保將數(shù)據(jù)刷到磁盤(pán)。這實(shí)際意味著數(shù)據(jù)轉(zhuǎn)移到了內(nèi)核的內(nèi)存頁(yè)緩存。

-? ? ?常亮?xí)r間就能搞定? ? -
B 樹(shù)的 O(log N) 時(shí)間復(fù)雜度,對(duì)于磁盤(pán)操作來(lái)說(shuō),并不能等同于常量時(shí)間復(fù)雜度。
Kafka 采用日志文件方式,確保讀寫(xiě)操作的時(shí)間復(fù)雜度是 O(1)。
Kafka 不會(huì)在消息一被消費(fèi)就立即刪除,而是保留一段時(shí)間,這樣對(duì)于消費(fèi)者來(lái)說(shuō)也更靈活一些。

-? ? ?效率? ? -
對(duì)于 Kafka 這類(lèi)系統(tǒng)而言,即使像前述那樣消除了糟糕的磁盤(pán)訪問(wèn)模式,也會(huì)遇到兩個(gè)導(dǎo)致數(shù)據(jù)效率低的問(wèn)題:過(guò)多的小 I/O 操作,以及過(guò)多的字節(jié)拷貝。
小 I/O 問(wèn)題在客戶(hù)端與服務(wù)端之間,以及服務(wù)端內(nèi)部的數(shù)據(jù)持久化操作中都會(huì)發(fā)生。對(duì)此,Kafka 協(xié)議建立在 “消息集” (即一批消息)的抽象之上,這樣網(wǎng)絡(luò)請(qǐng)求讀寫(xiě)的是一批一批的消息,減少了網(wǎng)絡(luò)往返的時(shí)間開(kāi)銷(xiāo)(注:消息處理的實(shí)時(shí)性會(huì)相對(duì)差一點(diǎn))。服務(wù)端也是一次將一批消息寫(xiě)到日志文件中,消費(fèi)者也按序一次獲取一批消息。這一簡(jiǎn)單的優(yōu)化可以將吞吐能力提升幾個(gè)數(shù)量級(jí)。
對(duì)于過(guò)多的字節(jié)拷貝問(wèn)題,在消息量大的時(shí)候,影響比較明顯。Kafka 采用了一種標(biāo)準(zhǔn)化的二進(jìn)制消息格式,producer、broker、consumer 都使用這種格式,這樣數(shù)據(jù)塊在傳輸期間不需要變動(dòng)。
broker 維護(hù)的消息日志只是一個(gè)目錄下的一堆文件,文件內(nèi)容是按序?qū)懭氲南⒓?,消息集的?shù)據(jù)格式同于 producer、consumer 使用的。共用一種數(shù)據(jù)格式方便了一個(gè)重要的操作優(yōu)化:持久化日志塊的網(wǎng)絡(luò)傳輸。對(duì)于從內(nèi)存頁(yè)緩存(pagecache)到網(wǎng)絡(luò)套接字(socket)的數(shù)據(jù)傳輸操作,現(xiàn)代 UNIX 操作系統(tǒng)提供了一種高度優(yōu)化的代碼執(zhí)行路徑。Linux 中使用 sendfile 系統(tǒng)調(diào)用?可以利用這個(gè)優(yōu)化。
要理解 sendfile 的收益,需要先理解從文件到套接字傳輸數(shù)據(jù)的常規(guī)代碼執(zhí)行路徑:
操作系統(tǒng)從磁盤(pán)將數(shù)據(jù)讀到內(nèi)核空間的內(nèi)存頁(yè)緩存(pagecache)
應(yīng)用程序從內(nèi)核空間將數(shù)據(jù)讀到用戶(hù)空間緩沖區(qū)
應(yīng)用程序?qū)?shù)據(jù)從用戶(hù)空間緩沖區(qū)讀到內(nèi)核空間的套接字緩沖區(qū)
操作系統(tǒng)將數(shù)據(jù)從套接字緩沖區(qū)讀到 NIC 緩沖區(qū),網(wǎng)卡從 NIC 緩沖區(qū)讀取數(shù)據(jù)通過(guò)網(wǎng)絡(luò)發(fā)出去
這一代碼執(zhí)行路徑,涉及 4 次數(shù)據(jù)拷貝和 2 次系統(tǒng)調(diào)用,很顯然是低效的。使用 sendfile,可以避免內(nèi)核空間和用戶(hù)空間之間一些不必要的數(shù)據(jù)拷貝,操作系統(tǒng)可以直接將數(shù)據(jù)從內(nèi)存頁(yè)緩存發(fā)送到網(wǎng)絡(luò)。
進(jìn)一步了解 sendfile 以及 Java 平臺(tái)如何支持零拷貝,可以閱讀這篇文章(https://developer.ibm.com/articles/j-zerocopy/)。

-? ? ?生產(chǎn)者(The Producer)? ? -
負(fù)載均衡
消息應(yīng)該發(fā)到哪個(gè)分區(qū)(partition)由客戶(hù)端根據(jù)哈希算法(或者隨機(jī))決定,并且消息是直接由 producer 發(fā)到目標(biāo)分區(qū)的 leader broker,沒(méi)有任何中間路由層。
所有 Kafka 節(jié)點(diǎn)都可以響應(yīng)元數(shù)據(jù)請(qǐng)求 - 告知客戶(hù)端(producer 或 consumer)哪些服務(wù)節(jié)點(diǎn)還存活以及某個(gè) topic 的各個(gè)分區(qū) leader 分別是哪個(gè)節(jié)點(diǎn)(疑惑:如果某個(gè)分區(qū) leader 節(jié)點(diǎn)掛掉之后,客戶(hù)端如何獲知?何時(shí)可以獲知?)
消息交付語(yǔ)義
producer 和 consumer 之間的消息交付語(yǔ)義,分 3 種:
最多消費(fèi)一次 - 消息可能會(huì)丟失,但不會(huì)被重復(fù)消費(fèi) 最少消費(fèi)一次 - 消息不會(huì)丟,但可能被重復(fù)消費(fèi) 僅消費(fèi)一次 - 每個(gè)消息都會(huì)被消費(fèi)且僅消費(fèi)一次
acks:acks=0:表示 producer 不需要等分區(qū) leader broker 返回任何響應(yīng),將消息存入套接字緩沖區(qū)(socket buffer)就當(dāng)做消息已經(jīng)發(fā)送成功。所以可靠性是沒(méi)有保證的。 acks=1:表示 分區(qū) leader broker 將消息寫(xiě)入自己的本地日志文件,就向 producer 響應(yīng)成功,不必等待分區(qū)副本 broker 同步好消息。 acks=-1 或 acks=all:表示 分區(qū) leader broker 需要等待所有同步副本 broker 同步好消息并響應(yīng)成功,才向 producer 響應(yīng)成功
節(jié)點(diǎn)必須維持與 Zookeeper 的 session 連接(通過(guò) Zookeeper 的心跳機(jī)制) 如果是一個(gè)從節(jié)點(diǎn)(follower),則必須不斷從 leader 節(jié)點(diǎn)同步消息數(shù)據(jù),且同步進(jìn)度沒(méi)有落后太多
如果 consumer 讀取消息后,先向 kafka 提交消費(fèi)位置,再處理消息;如果該 consumer 掛掉或重啟,會(huì)可能導(dǎo)致丟消息,從而只能滿(mǎn)足“最多處理一次”交付語(yǔ)義。 如果 consumer 讀取消息后,是先處理,再提交消費(fèi)位置;如果該 consumer 掛掉或重啟,則可能導(dǎo)致重復(fù)消費(fèi)消息,從而只能滿(mǎn)足“最少處理一次”交付語(yǔ)義。

-? ? ?復(fù)制? ? -
replica.lag.time.max.ms?配置參數(shù)判定。
-? ? ?可用性和持久性保證? ? -
acks=all?并不是要求所有的副本都確認(rèn)寫(xiě)入成功,而是在當(dāng)前同步中副本(ISR)都確認(rèn)寫(xiě)入成功時(shí),分區(qū) leader 就向 producer 響應(yīng)成功。例如:某個(gè) topic 被設(shè)置為 2 個(gè)副本,然后其中一個(gè)副本節(jié)點(diǎn)掛掉,此時(shí)要求?acks=all?的寫(xiě)操作也會(huì)成功。如果剩下的副本節(jié)點(diǎn)也掛了,那么就會(huì)丟消息啦。禁用臟 leader 選舉 指定一個(gè)最小 ISR 集大?。?/span> min.insync.replicas?參數(shù)設(shè)置):只有當(dāng) ISR 集大小大于設(shè)定的最小值,分區(qū) [leader] 才會(huì)接受消息寫(xiě)入。這個(gè)設(shè)置只有當(dāng) producer 使用?acks=all?時(shí)才會(huì)生效。(注:在我們生產(chǎn)環(huán)境中,分區(qū)副本數(shù)通常申請(qǐng)為 3(包含 leader),那么?min.insync.replicas?應(yīng)該設(shè)定為 2,但默認(rèn)是 1。使用 1,那么當(dāng)分區(qū)只有一個(gè)副本(即 leader),producer 也能寫(xiě)入成功,但如果這個(gè)副本又掛了,就會(huì)丟數(shù)據(jù)。)

-? ? ?副本管理? ? -

-? ? ?消費(fèi)者消費(fèi)進(jìn)度跟蹤? ? -
作者:xiayf
來(lái)源:
http://blog.xiayf.cn/2019/10/13/reading-kafka-design/
