一篇并不起眼的Kafka面試題
點擊上方藍色字體,選擇“設為星標”
回復”面試“獲取更多驚喜

為什么要使用 kafka?
緩沖和削峰:上游數據時有突發(fā)流量,下游可能扛不住,或者下游沒有足夠多的機器來保證冗余,kafka在中間可以起到一個緩沖的作用,把消息暫存在kafka中,下游服務就可以按照自己的節(jié)奏進行慢慢處理。
解耦和擴展性:項目開始的時候,并不能確定具體需求。消息隊列可以作為一個接口層,解耦重要的業(yè)務流程。只需要遵守約定,針對數據編程即可獲取擴展能力。
冗余:可以采用一對多的方式,一個生產者發(fā)布消息,可以被多個訂閱topic的服務消費到,供多個毫無關聯的業(yè)務使用。
健壯性:消息隊列可以堆積請求,所以消費端業(yè)務即使短時間死掉,也不會影響主要業(yè)務的正常進行。
異步通信:很多時候,用戶不想也不需要立即處理消息。消息隊列提供了異步處理機制,允許用戶把一個消息放入隊列,但并不立即處理它。想向隊列中放入多少消息就放多少,然后在需要的時候再去處理它們。
kafka的數據可靠性怎么保證
為保證producer發(fā)送的數據,能可靠的發(fā)送到指定的topic,topic的每個partition收到producer發(fā)送的數據后,都需要向producer發(fā)送ack(acknowledgement確認收到),如果producer收到ack,就會進行下一輪的發(fā)送,否則重新發(fā)送數據。所以引出ack機制。
ack應答機制
Kafka為用戶提供了三種可靠性級別,用戶根據對可靠性和延遲的要求進行權衡,選擇以下的配置。acks參數配置:
0:producer不等待broker的ack,這一操作提供了一個最低的延遲,broker一接收到還沒有寫入磁盤就已經返回,當broker故障時有可能丟失數據。
1:producer等待broker的ack,partition的leader落盤成功后返回ack,如果在follower同步成功之前l(fā)eader故障,那么將會丟失數據。

-1(all):producer等待broker的ack,partition的leader和follower全部落盤成功后才返回ack。但是如果在follower同步完成后,broker發(fā)送ack之前,leader發(fā)生故障,那么會造成數據重復。

Kafka的數據是放在磁盤上還是內存上,為什么速度會快?
kafka使用的是磁盤存儲。
速度快是因為:
順序寫入:因為硬盤是機械結構,每次讀寫都會尋址->寫入,其中尋址是一個“機械動作”,它是耗時的。所以硬盤 “討厭”隨機I/O, 喜歡順序I/O。為了提高讀寫硬盤的速度,Kafka就是使用順序I/O。
Memory Mapped Files(內存映射文件):64位操作系統(tǒng)中一般可以表示20G的數據文件,它的工作原理是直接利用操作系統(tǒng)的Page來實現文件到物理內存的直接映射。完成映射之后你對物理內存的操作會被同步到硬盤上。
Kafka高效文件存儲設計:Kafka把topic中一個parition大文件分成多個小文件段,通過多個小文件段,就容易定期清除或刪除已經消費完文件,減少磁盤占用。通過索引信息可以快速定位 message和確定response的 大 小。通過index元數據全部映射到memory(內存映射文件), 可以避免segment file的IO磁盤操作。通過索引文件稀疏存儲,可以大幅降低index文件元數據占用空間大小。
注:
Kafka解決查詢效率的手段之一是將數據文件分段,比如有100條Message,它們的offset是從0到99。假設將數據文件分成5段,第一段為0-19,第二段為20-39,以此類推,每段放在一個單獨的數據文件里面,數據文件以該段中 小的offset命名。這樣在查找指定offset的Message的時候,用二分查找就可以定位到該Message在哪個段中。
為數據文件建 索引數據文件分段 使得可以在一個較小的數據文件中查找對應offset的Message 了,但是這依然需要順序掃描才能找到對應offset的Message。為了進一步提高查找的效率,Kafka為每個分段后的數據文件建立了索引文件,文件名與數據文件的名字是一樣的,只是文件擴展名為.index。
副本數據同步策略

選擇最后一個的原因:
同樣為了容忍n臺節(jié)點的故障,第一種方案需要2n+1個副本,而第二種方案只需要n+1個副本,而Kafka的每個分區(qū)都有大量的數據,第一種方案會造成大量數據的冗余。
雖然第二種方案的網絡延遲會比較高,但網絡延遲對Kafka的影響較小。
ISR
如果采用全部完成同步,才發(fā)送ack的副本的同步策略的話:提出問題:leader收到數據,所有follower都開始同步數據,但有一個follower,因為某種故障,遲遲不能與leader進行同步,那leader就要一直等下去,直到它完成同步,才能發(fā)送ack。這個問題怎么解決呢?
Leader維護了一個動態(tài)的in-sync replica set (ISR),意為和leader保持同步的follower集合。當ISR中的follower完成數據的同步之后,leader就會給follower發(fā)送ack。如果follower長時間未向leader同步數據,則該follower將被踢出ISR,該時間閾值由replica.lag.time.max.ms參數設定。Leader發(fā)生故障之后,就會從ISR中選舉新的leader。
故障處理(LEO與HW)

LEO:指的是每個副本最大的offset。
HW:指的是消費者能見到的最大的offset,ISR隊列中最小的LEO。
kafka的消費分區(qū)分配策略
一個consumer group中有多個consumer,一個topic有多個partition,所以必然會涉及到partition的分配問題,即確定那個partition由哪個consumer來消費 Kafka有三種分配策略,一是RoundRobin,一是Range。高版本還有一個StickyAssignor策略 將分區(qū)的所有權從一個消費者移到另一個消費者稱為重新平衡(rebalance)。當以下事件發(fā)生時,Kafka 將會進行一次分區(qū)分配:
同一個 Consumer Group 內新增消費者。
消費者離開當前所屬的Consumer Group,包括shuts down或crashes。
Range分區(qū)分配策略
Range是對每個Topic而言的(即一個Topic一個Topic分),首先對同一個Topic里面的分區(qū)按照序號進行排序,并對消費者按照字母順序進行排序。然后用Partitions分區(qū)的個數除以消費者線程的總數來決定每個消費者線程消費幾個分區(qū)。如果除不盡,那么前面幾個消費者線程將會多消費一個分區(qū)。假設n=分區(qū)數/消費者數量,m=分區(qū)數%消費者數量,那么前m個消費者每個分配n+1個分區(qū),后面的(消費者數量-m)個消費者每個分配n個分區(qū)。假如有10個分區(qū),3個消費者線程,把分區(qū)按照序號排列
0,1,2,3,4,5,6,7,8,9
消費者線程為
C1-0,C2-0,C2-1
那么用partition數除以消費者線程的總數來決定每個消費者線程消費幾個partition,如果除不盡,前面幾個消費者將會多消費一個分區(qū)。在我們的例子里面,我們有10個分區(qū),3個消費者線程,10/3 = 3,而且除除不盡,那么消費者線程C1-0將會多消費一個分區(qū),所以最后分區(qū)分配的結果看起來是這樣的:
C1-0:0,1,2,3
C2-0:4,5,6
C2-1:7,8,9
如果有11個分區(qū)將會是:
C1-0:0,1,2,3
C2-0:4,5,6,7
C2-1:8,9,10
假如我們有兩個主題T1,T2,分別有10個分區(qū),最后的分配結果將會是這樣:
C1-0:T1(0,1,2,3) T2(0,1,2,3)
C2-0:T1(4,5,6) T2(4,5,6)
C2-1:T1(7,8,9) T2(7,8,9)
RoundRobinAssignor分區(qū)分配策略
RoundRobinAssignor策略的原理是將消費組內所有消費者以及消費者所訂閱的所有topic的partition按照字典序排序,然后通過輪詢方式逐個將分區(qū)以此分配給每個消費者. 使用RoundRobin策略有兩個前提條件必須滿足:
同一個消費者組里面的所有消費者的num.streams(消費者消費線程數)必須相等;每個消費者訂閱的主題必須相同。加入按照 hashCode 排序完的topic-partitions組依次為
T1-5, T1-3, T1-0, T1-8, T1-2, T1-1, T1-4, T1-7, T1-6, T1-9
我們的消費者線程排序為
C1-0, C1-1, C2-0, C2-1
最后分區(qū)分配的結果為:
C1-0 將消費 T1-5, T1-2, T1-6 分區(qū)
C1-1 將消費 T1-3, T1-1, T1-9 分區(qū)
C2-0 將消費 T1-0, T1-4 分區(qū)
C2-1 將消費 T1-8, T1-7 分區(qū)
StickyAssignor分區(qū)分配策略
Kafka從0.11.x版本開始引入這種分配策略,它主要有兩個目的:
分區(qū)的分配要盡可能的均勻,分配給消費者者的主題分區(qū)數最多相差一個 分區(qū)的分配盡可能的與上次分配的保持相同。當兩者發(fā)生沖突時,第一個目標優(yōu)先于第二個目標。鑒于這兩個目的,StickyAssignor策略的具體實現要比RangeAssignor和RoundRobinAssignor這兩種分配策略要復雜很多。
假設消費組內有3個消費者
C0、C1、C2
它們都訂閱了4個主題:
t0、t1、t2、t3
并且每個主題有2個分區(qū),也就是說整個消費組訂閱了
t0p0、t0p1、t1p0、t1p1、t2p0、t2p1、t3p0、t3p1這8個分區(qū)
最終的分配結果如下:
消費者C0:t0p0、t1p1、t3p0
消費者C1:t0p1、t2p0、t3p1
消費者C2:t1p0、t2p1
這樣初看上去似乎與采用RoundRobinAssignor策略所分配的結果相同
此時假設消費者C1脫離了消費組,那么消費組就會執(zhí)行再平衡操作,進而消費分區(qū)會重新分配。如果采用RoundRobinAssignor策略,那么此時的分配結果如下:
消費者C0:t0p0、t1p0、t2p0、t3p0
消費者C2:t0p1、t1p1、t2p1、t3p1
如分配結果所示,RoundRobinAssignor策略會按照消費者C0和C2進行重新輪詢分配。而如果此時使用的是StickyAssignor策略,那么分配結果為:
消費者C0:t0p0、t1p1、t3p0、t2p0
消費者C2:t1p0、t2p1、t0p1、t3p1
可以看到分配結果中保留了上一次分配中對于消費者C0和C2的所有分配結果,并將原來消費者C1的“負擔”分配給了剩余的兩個消費者C0和C2,最終C0和C2的分配還保持了均衡。
如果發(fā)生分區(qū)重分配,那么對于同一個分區(qū)而言有可能之前的消費者和新指派的消費者不是同一個,對于之前消費者進行到一半的處理還要在新指派的消費者中再次復現一遍,這顯然很浪費系統(tǒng)資源。StickyAssignor策略如同其名稱中的“sticky”一樣,讓分配策略具備一定的“粘性”,盡可能地讓前后兩次分配相同,進而減少系統(tǒng)資源的損耗以及其它異常情況的發(fā)生。
到目前為止所分析的都是消費者的訂閱信息都是相同的情況,我們來看一下訂閱信息不同的情況下的處理。
舉例,同樣消費組內有3個消費者:
C0、C1、C2
集群中有3個主題:
t0、t1、t2
這3個主題分別有
1、2、3個分區(qū)
也就是說集群中有
t0p0、t1p0、t1p1、t2p0、t2p1、t2p2這6個分區(qū)
消費者C0訂閱了主題t0
消費者C1訂閱了主題t0和t1
消費者C2訂閱了主題t0、t1和t2
如果此時采用RoundRobinAssignor策略:
消費者C0:t0p0
消費者C1:t1p0
消費者C2:t1p1、t2p0、t2p1、t2p2
如果此時采用的是StickyAssignor策略:
消費者C0:t0p0
消費者C1:t1p0、t1p1
消費者C2:t2p0、t2p1、t2p2
此時消費者C0脫離了消費組,那么RoundRobinAssignor策略的分配結果為:
消費者C1:t0p0、t1p1
消費者C2:t1p0、t2p0、t2p1、t2p2
StickyAssignor策略,那么分配結果為:
消費者C1:t1p0、t1p1、t0p0
消費者C2:t2p0、t2p1、t2p2
可以看到StickyAssignor策略保留了消費者C1和C2中原有的5個分區(qū)的分配:
t1p0、t1p1、t2p0、t2p1、t2p2。
從結果上看StickyAssignor策略比另外兩者分配策略而言顯得更加的優(yōu)異,這個策略的代碼實現也是異常復雜。
kafka事務是怎么實現的
Kafka從0.11版本開始引入了事務支持。事務可以保證Kafka在Exactly Once語義的基礎上,生產和消費可以跨分區(qū)和會話,要么全部成功,要么全部失敗。
Producer事務
為了實現跨分區(qū)跨會話的事務,需要引入一個全局唯一的Transaction ID,并將Producer獲得的PID和Transaction ID綁定。這樣當Producer重啟后就可以通過正在進行的Transaction ID獲得原來的PID。為了管理Transaction,Kafka引入了一個新的組件Transaction Coordinator。Producer就是通過和Transaction Coordinator交互獲得Transaction ID對應的任務狀態(tài)。Transaction Coordinator還負責將事務所有寫入Kafka的一個內部Topic,這樣即使整個服務重啟,由于事務狀態(tài)得到保存,進行中的事務狀態(tài)可以得到恢復,從而繼續(xù)進行。
Consumer事務
對于Consumer而言,事務的保證就會相對較弱,尤其時無法保證Commit的信息被精確消費。這是由于Consumer可以通過offset訪問任意信息,而且不同的Segment File生命周期不同,同一事務的消息可能會出現重啟后被刪除的情況。
Exactly Once語義
將服務器的ACK級別設置為-1,可以保證Producer到Server之間不會丟失數據,即At Least Once語義。相對的,將服務器ACK級別設置為0,可以保證生產者每條消息只會被發(fā)送一次,即At Most Once語義。
At Least Once可以保證數據不丟失,但是不能保證數據不重復;
相對的,At Most Once可以保證數據不重復,但是不能保證數據不丟失。
但是,對于一些非常重要的信息,比如說交易數據,下游數據消費者要求數據既不重復也不丟失,即Exactly Once語義。在0.11版本以前的Kafka,對此是無能為力的,只能保證數據不丟失,再在下游消費者對數據做全局去重。對于多個下游應用的情況,每個都需要單獨做全局去重,這就對性能造成了很大影響。
0.11版本的Kafka,引入了一項重大特性:冪等性。
開啟冪等性enable.idempotence=true。
所謂的冪等性就是指Producer不論向Server發(fā)送多少次重復數據,Server端都只會持久化一條。冪等性結合At Least Once語義,就構成了Kafka的Exactly Once語義。即:
At Least Once + 冪等性 = Exactly Once
Kafka的冪等性實現其實就是將原來下游需要做的去重放在了數據上游。開啟冪等性的Producer在初始化的時候會被分配一個PID,發(fā)往同一Partition的消息會附帶Sequence Number。而Broker端會對< PID, Partition, SeqNumber>做緩存,當具有相同主鍵的消息提交時,Broker只會持久化一條。
但是PID重啟就會變化,同時不同的Partition也具有不同主鍵,所以冪等性無法保證跨分區(qū)跨會話的Exactly Once。
補充,在流式計算中怎么Exactly Once語義?以flink為例
souce:使用執(zhí)行ExactlyOnce的數據源,比如kafka等
內部使用FlinkKafakConsumer,并開啟CheckPoint,偏移量會保存到StateBackend中,并且默認會將偏移量寫入到topic中去,即 _ consumer_offsets Flink設置CheckepointingModel.EXACTLY_ONCE
sink
存儲系統(tǒng)支持覆蓋也即冪等性:如Redis,Hbase,ES等 存儲系統(tǒng)不支持覆:需要支持事務(預寫式日志或者兩階段提交),兩階段提交可參考Flink集成的kafka sink的實現。
Kafka為什么不支持讀寫分離?
在 Kafka 中,生產者寫入消息、消費者讀取消息的操作都是與 leader 副本進行交互的,從 而實現的是一種主寫主讀的生產消費模型。Kafka 并不支持主寫從讀,因為主寫從讀有 2 個很明顯的缺點:
數據一致性問題:數據從主節(jié)點轉到從節(jié)點必然會有一個延時的時間窗口,這個時間 窗口會導致主從節(jié)點之間的數據不一致。某一時刻,在主節(jié)點和從節(jié)點中 A 數據的值都為 X, 之后將主節(jié)點中 A 的值修改為 Y,那么在這個變更通知到從節(jié)點之前,應用讀取從節(jié)點中的 A 數據的值并不為最新的 Y,由此便產生了數據不一致的問題。
延時問題:類似 Redis 這種組件,數據從寫入主節(jié)點到同步至從節(jié)點中的過程需要經歷 網絡→主節(jié)點內存→網絡→從節(jié)點內存 這幾個階段,整個過程會耗費一定的時間。而在 Kafka 中,主從同步會比 Redis 更加耗時,它需要經歷 網絡→主節(jié)點內存→主節(jié)點磁盤→網絡→從節(jié) 點內存→從節(jié)點磁盤 這幾個階段。對延時敏感的應用而言,主寫從讀的功能并不太適用。
Kafka的數據是放在磁盤上還是內存上,為什么速度會快?
Kafka使用的是磁盤存儲。
速度快是因為:
順序寫入:因為硬盤是機械結構,每次讀寫都會尋址->寫入,其中尋址是一個“機械動作”,它是耗時的。所以硬盤 “討厭”隨機I/O, 喜歡順序I/O。為了提高讀寫硬盤的速度,Kafka就是使用順序I/O。
Memory Mapped Files(內存映射文件):64位操作系統(tǒng)中一般可以表示20G的數據文件,它的工作原理是直接利用操作系統(tǒng)的Page來實現文件到物理內存的直接映射。完成映射之后你對物理內存的操作會被同步到硬盤上。
Kafka高效文件存儲設計:Kafka把topic中一個parition大文件分成多個小文件段,通過多個小文件段,就容易定期清除或刪除已經消費完文件,減少磁盤占用。通過索引信息可以快速定位message和確定response的 大 小。通過index元數據全部映射到memory(內存映射文件),可以避免segment file的IO磁盤操作。通過索引文件稀疏存儲,可以大幅降低index文件元數據占用空間大小。
Kafka解決查詢效率的手段之一是將數據文件分段,比如有100條Message,它們的offset是從0到99。假設將數據文件分成5段,第一段為0-19,第二段為20-39,以此類推,每段放在一個單獨的數據文件里面,數據文件以該段中 小的offset命名。這樣在查找指定offset的 Message的時候,用二分查找就可以定位到該Message在哪個段中。
為數據文件建 索引數據文件分段 使得可以在一個較小的數據文件中查找對應offset的Message 了,但是這依然需要順序掃描才能找到對應offset的Message。為了進一步提高查找的效率,Kafka為每個分段后的數據文件建立了索引文件,文件名與數據文件的名字是一樣的,只是文件擴展名為.index。

你好,我是王知無,一個大數據領域的硬核原創(chuàng)作者。
做過后端架構、數據中間件、數據平臺&架構、算法工程化。
專注大數據領域實時動態(tài)&技術提升&個人成長&職場進階,歡迎關注。
