Kafka面試題系列(進(jìn)階篇)
點(diǎn)擊上方藍(lán)色字體,選擇“設(shè)為星標(biāo)”
回復(fù)”資源“獲取更多資源

Kafka目前有哪些內(nèi)部topic,它們都有什么特征?各自的作用又是什么?
__consumer_offsets:作用是保存 Kafka 消費(fèi)者的位移信息
__transaction_state:用來(lái)存儲(chǔ)事務(wù)日志消息
優(yōu)先副本是什么?它有什么特殊的作用?
所謂的優(yōu)先副本是指在AR集合列表中的第一個(gè)副本。
理想情況下,優(yōu)先副本就是該分區(qū)的leader 副本,所以也可以稱(chēng)之為 preferred leader。Kafka 要確保所有主題的優(yōu)先副本在 Kafka 集群中均勻分布,這樣就保證了所有分區(qū)的 leader 均衡分布。以此來(lái)促進(jìn)集群的負(fù)載均衡,這一行為也可以稱(chēng)為“分區(qū)平衡”。
Kafka有哪幾處地方有分區(qū)分配的概念?簡(jiǎn)述大致的過(guò)程及原理
生產(chǎn)者的分區(qū)分配是指為每條消息指定其所要發(fā)往的分區(qū)??梢跃帉?xiě)一個(gè)具體的類(lèi)實(shí)現(xiàn)org.apache.kafka.clients.producer.Partitioner接口。
消費(fèi)者中的分區(qū)分配是指為消費(fèi)者指定其可以消費(fèi)消息的分區(qū)。Kafka 提供了消費(fèi)者客戶(hù)端參數(shù) partition.assignment.strategy 來(lái)設(shè)置消費(fèi)者與訂閱主題之間的分區(qū)分配策略。
分區(qū)副本的分配是指為集群制定創(chuàng)建主題時(shí)的分區(qū)副本分配方案,即在哪個(gè) broker 中創(chuàng)建哪些分區(qū)的副本。kafka-topics.sh 腳本中提供了一個(gè) replica-assignment 參數(shù)來(lái)手動(dòng)指定分區(qū)副本的分配方案。
簡(jiǎn)述Kafka的日志目錄結(jié)構(gòu)

Kafka 中的消息是以主題為基本單位進(jìn)行歸類(lèi)的,各個(gè)主題在邏輯上相互獨(dú)立。每個(gè)主題又可以分為一個(gè)或多個(gè)分區(qū)。不考慮多副本的情況,一個(gè)分區(qū)對(duì)應(yīng)一個(gè)日志(Log)。為了防止 Log 過(guò)大,Kafka 又引入了日志分段(LogSegment)的概念,將 Log 切分為多個(gè) LogSegment,相當(dāng)于一個(gè)巨型文件被平均分配為多個(gè)相對(duì)較小的文件。
Log 和 LogSegment 也不是純粹物理意義上的概念,Log 在物理上只以文件夾的形式存儲(chǔ),而每個(gè) LogSegment 對(duì)應(yīng)于磁盤(pán)上的一個(gè)日志文件和兩個(gè)索引文件,以及可能的其他文件(比如以“.txnindex”為后綴的事務(wù)索引文件)
Kafka中有哪些索引文件?
每個(gè)日志分段文件對(duì)應(yīng)了兩個(gè)索引文件,主要用來(lái)提高查找消息的效率。
偏移量索引文件用來(lái)建立消息偏移量(offset)到物理地址之間的映射關(guān)系,方便快速定位消息所在的物理文件位置
時(shí)間戳索引文件則根據(jù)指定的時(shí)間戳(timestamp)來(lái)查找對(duì)應(yīng)的偏移量信息。
如果我指定了一個(gè)offset,Kafka怎么查找到對(duì)應(yīng)的消息?
Kafka是通過(guò)seek() 方法來(lái)指定消費(fèi)的,在執(zhí)行seek() 方法之前要去執(zhí)行一次poll()方法,等到分配到分區(qū)之后會(huì)去對(duì)應(yīng)的分區(qū)的指定位置開(kāi)始消費(fèi),如果指定的位置發(fā)生了越界,那么會(huì)根據(jù)auto.offset.reset 參數(shù)設(shè)置的情況進(jìn)行消費(fèi)。
如果我指定了一個(gè)timestamp,Kafka怎么查找到對(duì)應(yīng)的消息?
Kafka提供了一個(gè) offsetsForTimes() 方法,通過(guò) timestamp 來(lái)查詢(xún)與此對(duì)應(yīng)的分區(qū)位置。offsetsForTimes() 方法的參數(shù) timestampsToSearch 是一個(gè) Map 類(lèi)型,key 為待查詢(xún)的分區(qū),而 value 為待查詢(xún)的時(shí)間戳,該方法會(huì)返回時(shí)間戳大于等于待查詢(xún)時(shí)間的第一條消息對(duì)應(yīng)的位置和時(shí)間戳,對(duì)應(yīng)于 OffsetAndTimestamp 中的 offset 和 timestamp 字段。
聊一聊你對(duì)Kafka的Log Retention的理解
日志刪除(Log Retention):按照一定的保留策略直接刪除不符合條件的日志分段。
我們可以通過(guò) broker 端參數(shù) log.cleanup.policy 來(lái)設(shè)置日志清理策略,此參數(shù)的默認(rèn)值為“delete”,即采用日志刪除的清理策略。
基于時(shí)間
日志刪除任務(wù)會(huì)檢查當(dāng)前日志文件中是否有保留時(shí)間超過(guò)設(shè)定的閾值(retentionMs)來(lái)尋找可刪除的日志分段文件集合(deletableSegments)retentionMs 可以通過(guò) broker 端參數(shù) log.retention.hours、log.retention.minutes 和 log.retention.ms 來(lái)配置,其中 log.retention.ms 的優(yōu)先級(jí)最高,log.retention.minutes 次之,log.retention.hours 最低。默認(rèn)情況下只配置了 log.retention.hours 參數(shù),其值為168,故默認(rèn)情況下日志分段文件的保留時(shí)間為7天。
刪除日志分段時(shí),首先會(huì)從 Log 對(duì)象中所維護(hù)日志分段的跳躍表中移除待刪除的日志分段,以保證沒(méi)有線(xiàn)程對(duì)這些日志分段進(jìn)行讀取操作。然后將日志分段所對(duì)應(yīng)的所有文件添加上“.deleted”的后綴(當(dāng)然也包括對(duì)應(yīng)的索引文件)。最后交由一個(gè)以“delete-file”命名的延遲任務(wù)來(lái)刪除這些以“.deleted”為后綴的文件,這個(gè)任務(wù)的延遲執(zhí)行時(shí)間可以通過(guò) file.delete.delay.ms 參數(shù)來(lái)調(diào)配,此參數(shù)的默認(rèn)值為60000,即1分鐘。基于日志大小
日志刪除任務(wù)會(huì)檢查當(dāng)前日志的大小是否超過(guò)設(shè)定的閾值(retentionSize)來(lái)尋找可刪除的日志分段的文件集合(deletableSegments)。
retentionSize 可以通過(guò) broker 端參數(shù) log.retention.bytes 來(lái)配置,默認(rèn)值為-1,表示無(wú)窮大。注意 log.retention.bytes 配置的是 Log 中所有日志文件的總大小,而不是單個(gè)日志分段(確切地說(shuō)應(yīng)該為 .log 日志文件)的大小。單個(gè)日志分段的大小由 broker 端參數(shù) log.segment.bytes 來(lái)限制,默認(rèn)值為1073741824,即 1GB。
這個(gè)刪除操作和基于時(shí)間的保留策略的刪除操作相同。基于日志起始偏移量
基于日志起始偏移量的保留策略的判斷依據(jù)是某日志分段的下一個(gè)日志分段的起始偏移量 baseOffset 是否小于等于 logStartOffset,若是,則可以刪除此日志分段。
如上圖所示,假設(shè) logStartOffset 等于25,日志分段1的起始偏移量為0,日志分段2的起始偏移量為11,日志分段3的起始偏移量為23,通過(guò)如下動(dòng)作收集可刪除的日志分段的文件集合 deletableSegments:
從頭開(kāi)始遍歷每個(gè)日志分段,日志分段1的下一個(gè)日志分段的起始偏移量為11,小于 logStartOffset 的大小,將日志分段1加入 deletableSegments。
日志分段2的下一個(gè)日志偏移量的起始偏移量為23,也小于 logStartOffset 的大小,將日志分段2加入 deletableSegments。
日志分段3的下一個(gè)日志偏移量在 logStartOffset 的右側(cè),故從日志分段3開(kāi)始的所有日志分段都不會(huì)加入 deletableSegments。
收集完可刪除的日志分段的文件集合之后的刪除操作同基于日志大小的保留策略和基于時(shí)間的保留策略相同
聊一聊你對(duì)Kafka的Log Compaction的理解
日志壓縮(Log Compaction):針對(duì)每個(gè)消息的 key 進(jìn)行整合,對(duì)于有相同 key 的不同 value 值,只保留最后一個(gè)版本。
如果要采用日志壓縮的清理策略,就需要將 log.cleanup.policy 設(shè)置為“compact”,并且還需要將 log.cleaner.enable (默認(rèn)值為 true)設(shè)定為 true。
如下圖所示,Log Compaction 對(duì)于有相同 key 的不同 value 值,只保留最后一個(gè)版本。如果應(yīng)用只關(guān)心 key 對(duì)應(yīng)的最新 value 值,則可以開(kāi)啟 Kafka 的日志清理功能,Kafka 會(huì)定期將相同 key 的消息進(jìn)行合并,只保留最新的 value 值。
聊一聊你對(duì)Kafka底層存儲(chǔ)的理解
頁(yè)緩存
頁(yè)緩存是操作系統(tǒng)實(shí)現(xiàn)的一種主要的磁盤(pán)緩存,以此用來(lái)減少對(duì)磁盤(pán) I/O 的操作。具體來(lái)說(shuō),就是把磁盤(pán)中的數(shù)據(jù)緩存到內(nèi)存中,把對(duì)磁盤(pán)的訪(fǎng)問(wèn)變?yōu)閷?duì)內(nèi)存的訪(fǎng)問(wèn)。
當(dāng)一個(gè)進(jìn)程準(zhǔn)備讀取磁盤(pán)上的文件內(nèi)容時(shí),操作系統(tǒng)會(huì)先查看待讀取的數(shù)據(jù)所在的頁(yè)(page)是否在頁(yè)緩存(pagecache)中,如果存在(命中)則直接返回?cái)?shù)據(jù),從而避免了對(duì)物理磁盤(pán)的 I/O 操作;如果沒(méi)有命中,則操作系統(tǒng)會(huì)向磁盤(pán)發(fā)起讀取請(qǐng)求并將讀取的數(shù)據(jù)頁(yè)存入頁(yè)緩存,之后再將數(shù)據(jù)返回給進(jìn)程。
同樣,如果一個(gè)進(jìn)程需要將數(shù)據(jù)寫(xiě)入磁盤(pán),那么操作系統(tǒng)也會(huì)檢測(cè)數(shù)據(jù)對(duì)應(yīng)的頁(yè)是否在頁(yè)緩存中,如果不存在,則會(huì)先在頁(yè)緩存中添加相應(yīng)的頁(yè),最后將數(shù)據(jù)寫(xiě)入對(duì)應(yīng)的頁(yè)。被修改過(guò)后的頁(yè)也就變成了臟頁(yè),操作系統(tǒng)會(huì)在合適的時(shí)間把臟頁(yè)中的數(shù)據(jù)寫(xiě)入磁盤(pán),以保持?jǐn)?shù)據(jù)的一致性。
用過(guò) Java 的人一般都知道兩點(diǎn)事實(shí):對(duì)象的內(nèi)存開(kāi)銷(xiāo)非常大,通常會(huì)是真實(shí)數(shù)據(jù)大小的幾倍甚至更多,空間使用率低下;Java 的垃圾回收會(huì)隨著堆內(nèi)數(shù)據(jù)的增多而變得越來(lái)越慢。基于這些因素,使用文件系統(tǒng)并依賴(lài)于頁(yè)緩存的做法明顯要優(yōu)于維護(hù)一個(gè)進(jìn)程內(nèi)緩存或其他結(jié)構(gòu),至少我們可以省去了一份進(jìn)程內(nèi)部的緩存消耗,同時(shí)還可以通過(guò)結(jié)構(gòu)緊湊的字節(jié)碼來(lái)替代使用對(duì)象的方式以節(jié)省更多的空間。
此外,即使 Kafka 服務(wù)重啟,頁(yè)緩存還是會(huì)保持有效,然而進(jìn)程內(nèi)的緩存卻需要重建。這樣也極大地簡(jiǎn)化了代碼邏輯,因?yàn)榫S護(hù)頁(yè)緩存和文件之間的一致性交由操作系統(tǒng)來(lái)負(fù)責(zé),這樣會(huì)比進(jìn)程內(nèi)維護(hù)更加安全有效。
零拷貝
除了消息順序追加、頁(yè)緩存等技術(shù),Kafka 還使用零拷貝(Zero-Copy)技術(shù)來(lái)進(jìn)一步提升性能。所謂的零拷貝是指將數(shù)據(jù)直接從磁盤(pán)文件復(fù)制到網(wǎng)卡設(shè)備中,而不需要經(jīng)由應(yīng)用程序之手。零拷貝大大提高了應(yīng)用程序的性能,減少了內(nèi)核和用戶(hù)模式之間的上下文切換。對(duì) Linux 操作系統(tǒng)而言,零拷貝技術(shù)依賴(lài)于底層的 sendfile() 方法實(shí)現(xiàn)。對(duì)應(yīng)于 Java 語(yǔ)言,F(xiàn)ileChannal.transferTo() 方法的底層實(shí)現(xiàn)就是 sendfile() 方法。
聊一聊Kafka的延時(shí)操作的原理
Kafka 中有多種延時(shí)操作,比如延時(shí)生產(chǎn),還有延時(shí)拉?。―elayedFetch)、延時(shí)數(shù)據(jù)刪除(DelayedDeleteRecords)等。
延時(shí)操作創(chuàng)建之后會(huì)被加入延時(shí)操作管理器(DelayedOperationPurgatory)來(lái)做專(zhuān)門(mén)的處理。延時(shí)操作有可能會(huì)超時(shí),每個(gè)延時(shí)操作管理器都會(huì)配備一個(gè)定時(shí)器(SystemTimer)來(lái)做超時(shí)管理,定時(shí)器的底層就是采用時(shí)間輪(TimingWheel)實(shí)現(xiàn)的。
聊一聊Kafka控制器的作用
在 Kafka 集群中會(huì)有一個(gè)或多個(gè) broker,其中有一個(gè) broker 會(huì)被選舉為控制器(Kafka Controller),它負(fù)責(zé)管理整個(gè)集群中所有分區(qū)和副本的狀態(tài)。當(dāng)某個(gè)分區(qū)的 leader 副本出現(xiàn)故障時(shí),由控制器負(fù)責(zé)為該分區(qū)選舉新的 leader 副本。當(dāng)檢測(cè)到某個(gè)分區(qū)的 ISR 集合發(fā)生變化時(shí),由控制器負(fù)責(zé)通知所有broker更新其元數(shù)據(jù)信息。當(dāng)使用 kafka-topics.sh 腳本為某個(gè) topic 增加分區(qū)數(shù)量時(shí),同樣還是由控制器負(fù)責(zé)分區(qū)的重新分配。
Kafka的舊版Scala的消費(fèi)者客戶(hù)端的設(shè)計(jì)有什么缺陷?

如上圖,舊版消費(fèi)者客戶(hù)端每個(gè)消費(fèi)組()在 ZooKeeper 中都維護(hù)了一個(gè) /consumers//ids 路徑,在此路徑下使用臨時(shí)節(jié)點(diǎn)記錄隸屬于此消費(fèi)組的消費(fèi)者的唯一標(biāo)識(shí)(consumerIdString),/consumers//owner 路徑下記錄了分區(qū)和消費(fèi)者的對(duì)應(yīng)關(guān)系,/consumers//offsets 路徑下記錄了此消費(fèi)組在分區(qū)中對(duì)應(yīng)的消費(fèi)位移。
每個(gè)消費(fèi)者在啟動(dòng)時(shí)都會(huì)在 /consumers//ids 和 /brokers/ids 路徑上注冊(cè)一個(gè)監(jiān)聽(tīng)器。當(dāng) /consumers//ids 路徑下的子節(jié)點(diǎn)發(fā)生變化時(shí),表示消費(fèi)組中的消費(fèi)者發(fā)生了變化;當(dāng) /brokers/ids 路徑下的子節(jié)點(diǎn)發(fā)生變化時(shí),表示 broker 出現(xiàn)了增減。這樣通過(guò) ZooKeeper 所提供的 Watcher,每個(gè)消費(fèi)者就可以監(jiān)聽(tīng)消費(fèi)組和 Kafka 集群的狀態(tài)了。
這種方式下每個(gè)消費(fèi)者對(duì) ZooKeeper 的相關(guān)路徑分別進(jìn)行監(jiān)聽(tīng),當(dāng)觸發(fā)再均衡操作時(shí),一個(gè)消費(fèi)組下的所有消費(fèi)者會(huì)同時(shí)進(jìn)行再均衡操作,而消費(fèi)者之間并不知道彼此操作的結(jié)果,這樣可能導(dǎo)致 Kafka 工作在一個(gè)不正確的狀態(tài)。與此同時(shí),這種嚴(yán)重依賴(lài)于 ZooKeeper 集群的做法還有兩個(gè)比較嚴(yán)重的問(wèn)題。
羊群效應(yīng)(Herd Effect):所謂的羊群效應(yīng)是指ZooKeeper 中一個(gè)被監(jiān)聽(tīng)的節(jié)點(diǎn)變化,大量的 Watcher 通知被發(fā)送到客戶(hù)端,導(dǎo)致在通知期間的其他操作延遲,也有可能發(fā)生類(lèi)似死鎖的情況。
腦裂問(wèn)題(Split Brain):消費(fèi)者進(jìn)行再均衡操作時(shí)每個(gè)消費(fèi)者都與 ZooKeeper 進(jìn)行通信以判斷消費(fèi)者或broker變化的情況,由于 ZooKeeper 本身的特性,可能導(dǎo)致在同一時(shí)刻各個(gè)消費(fèi)者獲取的狀態(tài)不一致,這樣會(huì)導(dǎo)致異常問(wèn)題發(fā)生。
消費(fèi)再均衡的原理是什么?(提示:消費(fèi)者協(xié)調(diào)器和消費(fèi)組協(xié)調(diào)器)
就目前而言,一共有如下幾種情形會(huì)觸發(fā)再均衡的操作:
有新的消費(fèi)者加入消費(fèi)組。
有消費(fèi)者宕機(jī)下線(xiàn)。消費(fèi)者并不一定需要真正下線(xiàn),例如遇到長(zhǎng)時(shí)間的GC、網(wǎng)絡(luò)延遲導(dǎo)致消費(fèi)者長(zhǎng)時(shí)間未向 GroupCoordinator 發(fā)送心跳等情況時(shí),GroupCoordinator 會(huì)認(rèn)為消費(fèi)者已經(jīng)下線(xiàn)。
有消費(fèi)者主動(dòng)退出消費(fèi)組(發(fā)送 LeaveGroupRequest 請(qǐng)求)。比如客戶(hù)端調(diào)用了 unsubscrible() 方法取消對(duì)某些主題的訂閱。
消費(fèi)組所對(duì)應(yīng)的 GroupCoorinator 節(jié)點(diǎn)發(fā)生了變更。
消費(fèi)組內(nèi)所訂閱的任一主題或者主題的分區(qū)數(shù)量發(fā)生變化。
GroupCoordinator 是 Kafka 服務(wù)端中用于管理消費(fèi)組的組件。而消費(fèi)者客戶(hù)端中的 ConsumerCoordinator 組件負(fù)責(zé)與 GroupCoordinator 進(jìn)行交互。
第一階段(FIND_COORDINATOR)
消費(fèi)者需要確定它所屬的消費(fèi)組對(duì)應(yīng)的 GroupCoordinator 所在的 broker,并創(chuàng)建與該 broker 相互通信的網(wǎng)絡(luò)連接。如果消費(fèi)者已經(jīng)保存了與消費(fèi)組對(duì)應(yīng)的 GroupCoordinator 節(jié)點(diǎn)的信息,并且與它之間的網(wǎng)絡(luò)連接是正常的,那么就可以進(jìn)入第二階段。否則,就需要向集群中的某個(gè)節(jié)點(diǎn)發(fā)送 FindCoordinatorRequest 請(qǐng)求來(lái)查找對(duì)應(yīng)的 GroupCoordinator,這里的“某個(gè)節(jié)點(diǎn)”并非是集群中的任意節(jié)點(diǎn),而是負(fù)載最小的節(jié)點(diǎn)。
第二階段(JOIN_GROUP)
在成功找到消費(fèi)組所對(duì)應(yīng)的 GroupCoordinator 之后就進(jìn)入加入消費(fèi)組的階段,在此階段的消費(fèi)者會(huì)向 GroupCoordinator 發(fā)送 JoinGroupRequest 請(qǐng)求,并處理響應(yīng)。
選舉消費(fèi)組的leader
如果消費(fèi)組內(nèi)還沒(méi)有 leader,那么第一個(gè)加入消費(fèi)組的消費(fèi)者即為消費(fèi)組的 leader。如果某一時(shí)刻 leader 消費(fèi)者由于某些原因退出了消費(fèi)組,那么會(huì)重新選舉一個(gè)新的 leader
選舉分區(qū)分配策略
收集各個(gè)消費(fèi)者支持的所有分配策略,組成候選集 candidates。
每個(gè)消費(fèi)者從候選集 candidates 中找出第一個(gè)自身支持的策略,為這個(gè)策略投上一票。
計(jì)算候選集中各個(gè)策略的選票數(shù),選票數(shù)最多的策略即為當(dāng)前消費(fèi)組的分配策略。
第三階段(SYNC_GROUP)
leader 消費(fèi)者根據(jù)在第二階段中選舉出來(lái)的分區(qū)分配策略來(lái)實(shí)施具體的分區(qū)分配,在此之后需要將分配的方案同步給各個(gè)消費(fèi)者,通過(guò) GroupCoordinator 這個(gè)“中間人”來(lái)負(fù)責(zé)轉(zhuǎn)發(fā)同步分配方案的。
第四階段(HEARTBEAT)
進(jìn)入這個(gè)階段之后,消費(fèi)組中的所有消費(fèi)者就會(huì)處于正常工作狀態(tài)。在正式消費(fèi)之前,消費(fèi)者還需要確定拉取消息的起始位置。假設(shè)之前已經(jīng)將最后的消費(fèi)位移提交到了 GroupCoordinator,并且 GroupCoordinator 將其保存到了 Kafka 內(nèi)部的 __consumer_offsets 主題中,此時(shí)消費(fèi)者可以通過(guò) OffsetFetchRequest 請(qǐng)求獲取上次提交的消費(fèi)位移并從此處繼續(xù)消費(fèi)。
消費(fèi)者通過(guò)向 GroupCoordinator 發(fā)送心跳來(lái)維持它們與消費(fèi)組的從屬關(guān)系,以及它們對(duì)分區(qū)的所有權(quán)關(guān)系。只要消費(fèi)者以正常的時(shí)間間隔發(fā)送心跳,就被認(rèn)為是活躍的,說(shuō)明它還在讀取分區(qū)中的消息。心跳線(xiàn)程是一個(gè)獨(dú)立的線(xiàn)程,可以在輪詢(xún)消息的空檔發(fā)送心跳。如果消費(fèi)者停止發(fā)送心跳的時(shí)間足夠長(zhǎng),則整個(gè)會(huì)話(huà)就被判定為過(guò)期,GroupCoordinator 也會(huì)認(rèn)為這個(gè)消費(fèi)者已經(jīng)死亡,就會(huì)觸發(fā)一次再均衡行為。
Kafka中的冪等是怎么實(shí)現(xiàn)的?
為了實(shí)現(xiàn)生產(chǎn)者的冪等性,Kafka 為此引入了 producer id(以下簡(jiǎn)稱(chēng) PID)和序列號(hào)(sequence number)這兩個(gè)概念。
每個(gè)新的生產(chǎn)者實(shí)例在初始化的時(shí)候都會(huì)被分配一個(gè) PID,這個(gè) PID 對(duì)用戶(hù)而言是完全透明的。對(duì)于每個(gè) PID,消息發(fā)送到的每一個(gè)分區(qū)都有對(duì)應(yīng)的序列號(hào),這些序列號(hào)從0開(kāi)始單調(diào)遞增。生產(chǎn)者每發(fā)送一條消息就會(huì)將
broker 端會(huì)在內(nèi)存中為每一對(duì)
文章不錯(cuò)?點(diǎn)個(gè)【在看】吧!??


