Kafka是一個(gè)分布式的基于發(fā)布、訂閱的消息系統(tǒng),具有著高吞吐、高容錯(cuò)、高可靠以及高性能等特性,主要用于應(yīng)用解耦、流量削峰、異步消息等場(chǎng)景。
為了讓大家更加深入的了解Kafka內(nèi)部實(shí)現(xiàn)原理,文中將會(huì)從主題與日志開(kāi)始介紹消息的存儲(chǔ)、刪除以及檢索,然后介紹其副本機(jī)制的實(shí)現(xiàn)原理,最后介紹生產(chǎn)與消費(fèi)的實(shí)現(xiàn)原理以便更合理的應(yīng)用于實(shí)際業(yè)務(wù)。( 另外,本文較長(zhǎng),建議分享后慢慢閱讀 : )
1. 引言
Kafka是一個(gè)分布式的基于發(fā)布、訂閱的消息系統(tǒng),有著強(qiáng)大的消息處理能力,相比與其他消息系統(tǒng),具有以下特性:
快速數(shù)據(jù)持久化,實(shí)現(xiàn)了O(1)時(shí)間復(fù)雜度的數(shù)據(jù)持久化能力。
高吞吐,能在普通的服務(wù)器上達(dá)到10W每秒的吞吐速率。
高可靠,消息持久化以及副本系統(tǒng)的機(jī)制保證了消息的可靠性,消息可以多次消費(fèi)。
高擴(kuò)展,與其他分布式系統(tǒng)一樣,所有組件均支持分布式、自動(dòng)實(shí)現(xiàn)負(fù)載均衡,可以快速便捷的擴(kuò)容系統(tǒng)。
離線與實(shí)時(shí)處理能力并存,提供了在線與離線的消息處理能力。
正是因其具有這些的優(yōu)秀特性而廣泛用于應(yīng)用解耦、流量削峰、異步消息等場(chǎng)景,比如消息中間件、日志聚合、流處理等等。
本文將從以下幾個(gè)方面去介紹kafka:
1. 第一章簡(jiǎn)單介紹下kafka作為分布式的消息發(fā)布與訂閱系統(tǒng)所具備的特征與優(yōu)勢(shì)
2. 第二章節(jié)介紹kafka系統(tǒng)的主題與日志,了解消息如何存放、如何檢索以及如何刪除
3. 第三章節(jié)介紹kafka副本機(jī)制以了解kafka內(nèi)部如何實(shí)現(xiàn)消息的高可靠
4. 第四章節(jié)將會(huì)從消息的生產(chǎn)端去介紹消息的分區(qū)算法以及冪等特性的具體實(shí)現(xiàn)
5. 第五章節(jié)將從消息的消費(fèi)端去了解消費(fèi)組、消費(fèi)位移以及重平衡機(jī)制具體實(shí)現(xiàn)
6. 最后章節(jié)簡(jiǎn)單總結(jié)下本文
2. 主題與日志
2.1 主題
主題是存儲(chǔ)消息的一個(gè)邏輯概念,可以簡(jiǎn)單理解為一類消息的集合,由使用方去創(chuàng)建。Kafka中的主題一般會(huì)有多個(gè)訂閱者去消費(fèi)對(duì)應(yīng)主題的消息,也可以存在多個(gè)生產(chǎn)者往主題中寫(xiě)入消息。
每個(gè)主題又可以劃分成多個(gè)分區(qū),每個(gè)分區(qū)存儲(chǔ)不同的消息。當(dāng)消息添加至分區(qū)時(shí),會(huì)為其分配一個(gè)位移offset(從0開(kāi)始遞增),并保證分區(qū)上唯一,消息在分區(qū)上的順序由offset保證,即同一個(gè)分區(qū)內(nèi)的消息是有序的,如下圖所示
同一個(gè)主題的不同分區(qū)會(huì)分配在不同的節(jié)點(diǎn)上(broker),分區(qū)時(shí)保證Kafka集群具有水平擴(kuò)展的基礎(chǔ)。
以主題nginx_access_log為例,分區(qū)數(shù)為3,如上圖所示。分區(qū)在邏輯上對(duì)應(yīng)一個(gè)日志(Log),物理上對(duì)應(yīng)的是一個(gè)文件夾。
drwxr-xr-x2rootroot409610月1120:07nginx_access_log-0/
drwxr-xr-x2rootroot409610月1120:07nginx_access_log-1/
drwxr-xr-x2rootroot409610月1120:07nginx_access_log-2/
消息寫(xiě)入分區(qū)時(shí),實(shí)際上是將消息寫(xiě)入分區(qū)所在的文件夾中。日志又分成多個(gè)分片(Segment),每個(gè)分片由日志文件與索引文件組成,每個(gè)分片大小是有限的(在kafka集群的配置文件log.segment.bytes配置,默認(rèn)為1073741824byte,即1GB),當(dāng)分片大小超過(guò)限制則會(huì)重新創(chuàng)建一個(gè)新的分片,外界消息的寫(xiě)入只會(huì)寫(xiě)入最新的一個(gè)分片(順序IO)。
-rw-r--r--1rootroot1835920 10月 11 19:1800000000000000000000.index
-rw-r--r--1rootroot1073741684 10月 11 19:1800000000000000000000.log
-rw-r--r--1rootroot2737884 10月 11 19:1800000000000000000000.timeindex
-rw-r--r--1rootroot1828296 10月 11 19:3000000000000003257573.index
-rw-r--r--1rootroot1073741513 10月 11 19:3000000000000003257573.log
-rw-r--r--1rootroot2725512 10月 11 19:3000000000000003257573.timeindex
-rw-r--r--1rootroot1834744 10月 11 19:4200000000000006506251.index
-rw-r--r--1rootroot1073741771 10月 11 19:4200000000000006506251.log
-rw-r--r--1rootroot2736072 10月 11 19:4200000000000006506251.timeindex
-rw-r--r--1rootroot1832152 10月 11 19:5400000000000009751854.index
-rw-r--r--1rootroot1073740984 10月 11 19:5400000000000009751854.log
-rw-r--r--1rootroot2731572 10月 11 19:5400000000000009751854.timeindex
-rw-r--r--1rootroot1808792 10月 11 20:0600000000000012999310.index
-rw-r--r--1rootroot1073741584 10月 11 20:0600000000000012999310.log
-rw-r--r--1rootroot10 10月 11 19:5400000000000012999310.snapshot
-rw-r--r--1rootroot2694564 10月 11 20:0600000000000012999310.timeindex
-rw-r--r--1rootroot10485760 10月 11 20:0900000000000016260431.index
-rw-r--r--1rootroot278255892 10月 11 20:0900000000000016260431.log
-rw-r--r--1rootroot10 10月 11 20:0600000000000016260431.snapshot
-rw-r--r--1rootroot10485756 10月 11 20:0900000000000016260431.timeindex
-rw-r--r--1rootroot8 10月 11 19:03leader-epoch-checkpoint
個(gè)分片包含多個(gè)不同后綴的日志文件,分片中的第一個(gè)消息的offset將作為該分片的基準(zhǔn)偏移量,偏移量固定長(zhǎng)度為20,不夠前面補(bǔ)齊0,然后將其作為索引文件以及日志文件的文件名,如00000000000003257573.index、00000000000003257573.log、00000000000003257573.timeindex、相同文件名的文件組成一個(gè)分片(忽略后綴名),除了.index、.timeindex、.log后綴的日志文件外其他日志文件,對(duì)應(yīng)含義如下:
2.2 日志索引
首先介紹下.index文件,這里以文件00000000000003257573.index為例,首先我們可以通過(guò)以下命令查看該索引文件的內(nèi)容,可以看到輸出結(jié)構(gòu)為,實(shí)際上索引文件中保存的并不是offset而是相對(duì)位移,比如第一條消息的相對(duì)位移則為0,格式化輸出時(shí)加上了基準(zhǔn)偏移量。
如上圖所示,<114,17413>表示該分片相對(duì)位移為114的消息,其位移為3257573+114,即3257687,position表示對(duì)應(yīng)offset在.log文件的物理地址,通過(guò).index索引文件則可以獲取對(duì)應(yīng)offset所在的物理地址。
索引采用稀疏索引的方式構(gòu)建,并不保證分片中的每個(gè)消息都在索引文件有映射關(guān)系(.timeindex索引也是類似),主要是為了節(jié)省磁盤(pán)空間、內(nèi)存空間,因?yàn)樗饕募罱K會(huì)映射到內(nèi)存中。
# 查看該分片索引文件的前10條記錄
bin/kafka-dump-log.sh--files/tmp/kafka-logs/nginx_access_log-1/00000000000003257573.index|head -n10
Dumping /tmp/kafka-logs/nginx_access_log-1/00000000000003257573.index
offset:3257687position:17413
offset:3257743position:33770
offset:3257799position:50127
offset:3257818position:66484
offset:3257819position:72074
offset:3257871position:87281
offset:3257884position:91444
offset:3257896position:95884
offset:3257917position:100845
# 查看該分片索引文件的后10條記錄
$ bin/kafka-dump-log.sh--files/tmp/kafka-logs/nginx_access_log-1/00000000000003257573.index|tail -n10
offset:6506124position:1073698512
offset:6506137position:1073702918
offset:6506150position:1073707263
offset:6506162position:1073711499
offset:6506176position:1073716197
offset:6506188position:1073720433
offset:6506205position:1073725654
offset:6506217position:1073730060
offset:6506229position:1073734174
offset:6506243position:1073738288
比如查看offset為6506155的消息:首先根據(jù)offset找到對(duì)應(yīng)的分片,65061所對(duì)應(yīng)的分片為00000000000003257573,然后通過(guò)二分法在00000000000003257573.index文件中找到不大于6506155的最大索引值,得到<offset: 6506150, position: 1073707263>,然后從00000000000003257573.log的1073707263位置開(kāi)始順序掃描找到offset為650155的消息
Kafka從0.10.0.0版本起,為分片日志文件中新增了一個(gè).timeindex的索引文件,可以根據(jù)時(shí)間戳定位消息。同樣我們可以通過(guò)腳本kafka-dump-log.sh查看時(shí)間索引的文件內(nèi)容。
# 查看該分片時(shí)間索引文件的前10條記錄
bin/kafka-dump-log.sh--files/tmp/kafka-logs/nginx_access_log-1/00000000000003257573.timeindex |head -n10
Dumping /tmp/kafka-logs/nginx_access_log-1/00000000000003257573.timeindex
timestamp:1570792689308offset:3257685
timestamp:1570792689324offset:3257742
timestamp:1570792689345offset:3257795
timestamp:1570792689348offset:3257813
timestamp:1570792689357offset:3257867
timestamp:1570792689361offset:3257881
timestamp:1570792689364offset:3257896
timestamp:1570792689368offset:3257915
timestamp:1570792689369offset:3257927
# 查看該分片時(shí)間索引文件的前10條記錄
bin/kafka-dump-log.sh--files/tmp/kafka-logs/nginx_access_log-1/00000000000003257573.timeindex |tail -n10
Dumping /tmp/kafka-logs/nginx_access_log-1/00000000000003257573.timeindex
timestamp:1570793423474offset:6506136
timestamp:1570793423477offset:6506150
timestamp:1570793423481offset:6506159
timestamp:1570793423485offset:6506176
timestamp:1570793423489offset:6506188
timestamp:1570793423493offset:6506204
timestamp:1570793423496offset:6506214
timestamp:1570793423500offset:6506228
timestamp:1570793423503offset:6506240
timestamp:1570793423505offset:6506248
比如我想查看時(shí)間戳1570793423501開(kāi)始的消息:
1.首先定位分片,將1570793423501與每個(gè)分片的最大時(shí)間戳進(jìn)行對(duì)比(最大時(shí)間戳取時(shí)間索引文件的最后一條記錄時(shí)間,如果時(shí)間為0則取該日志分段的最近修改時(shí)間),直到找到大于或等于1570793423501的日志分段,因此會(huì)定位到時(shí)間索引文件00000000000003257573.timeindex,其最大時(shí)間戳為1570793423505;
2.通過(guò)二分法找到大于或等于1570793423501的最大索引項(xiàng),即(6506240為offset,相對(duì)位移為3247667);
3.根據(jù)相對(duì)位移3247667去索引文件中找到不大于該相對(duì)位移的最大索引值<3248656,1073734174>;
4.從日志文件00000000000003257573.log的1073734174位置處開(kāi)始掃描,查找不小于1570793423501的數(shù)據(jù)。
2.3 日志刪除
與其他消息中間件不同的是,Kafka集群中的消息不會(huì)因?yàn)橄M(fèi)與否而刪除,跟日志一樣消息最終會(huì)落盤(pán),并提供對(duì)應(yīng)的策略周期性(通過(guò)參數(shù)log.retention.check.interval.ms來(lái)設(shè)置,默認(rèn)為5分鐘)執(zhí)行刪除或者壓縮操作(broker配置文件log.cleanup.policy參數(shù)如果為“delete”則執(zhí)行刪除操作,如果為“compact”則執(zhí)行壓縮操作,默認(rèn)為“delete”)。
2.3.1 基于時(shí)間的日志刪除
當(dāng)消息在集群保留時(shí)間超過(guò)設(shè)定閾值(log.retention.hours,默認(rèn)為168小時(shí),即七天),則需要進(jìn)行刪除。這里會(huì)根據(jù)分片日志的最大時(shí)間戳來(lái)判斷該分片的時(shí)間是否滿足刪除條件,最大時(shí)間戳首先會(huì)選取時(shí)間戳索引文件中的最后一條索引記錄,如果對(duì)應(yīng)的時(shí)間戳值大于0則取該值,否則為最近一次修改時(shí)間。
這里不直接選取最后修改時(shí)間的原因是避免分片日志的文件被無(wú)意篡改而導(dǎo)致其時(shí)間不準(zhǔn)。
如果恰好該分區(qū)下的所有日志分片均已過(guò)期,那么會(huì)先生成一個(gè)新的日志分片作為新消息的寫(xiě)入文件,然后再執(zhí)行刪除參數(shù)。
2.3.2 基于空間的日志刪除
首先會(huì)計(jì)算待刪除的日志大小diff(totalSize-log.rentention.bytes),然后從最舊的一個(gè)分片開(kāi)始查看可以執(zhí)行刪除操作的文件集合(如果diff-segment.size>=0,則滿足刪除條件),最后執(zhí)行刪除操作。
2.3.3 基于日志起始偏移量的日志刪除
一般情況下,日志文件的起始偏移量(logStartOffset)會(huì)等于第一個(gè)日志分段的baseOffset,但是其值會(huì)因?yàn)閯h除消息請(qǐng)求而增長(zhǎng),logStartOffset的值實(shí)際上是日志集合中的最小消息,而小于這個(gè)值的消息都會(huì)被清理掉。如上圖所示,我們假設(shè)logStartOffset=7421048,日志刪除流程如下:
1. 從最舊的日志分片開(kāi)始遍歷,判斷其下一個(gè)分片的baseOffset是否小于或等于logStartOffset值,如果滿足,則需要?jiǎng)h除,因此第一個(gè)分片會(huì)被刪除。
2. 分片二的下一個(gè)分片baseOffset=6506251<7421048,所以分片二也需要?jiǎng)h除。
3. 分片三的下一個(gè)分片baseOffset=9751854>7421048,所以分片三不會(huì)被刪除。
2.4 日志壓縮
前面提到當(dāng)broker配置文件log.cleanup.policy參數(shù)值設(shè)置為“compact”時(shí),則會(huì)執(zhí)行壓縮操作,這里的壓縮跟普通意義的壓縮不一樣,這里的壓縮是指將相同key的消息只保留最后一個(gè)版本的value值,如下圖所示,壓縮之前offset是連續(xù)遞增,壓縮之后offset遞增可能不連續(xù),只保留5條消息記錄。
Kafka日志目錄下cleaner-offset-checkpoint文件,用來(lái)記錄每個(gè)主題的每個(gè)分區(qū)中已經(jīng)清理的偏移量,通過(guò)這個(gè)偏移量可以將分區(qū)中的日志文件分成兩個(gè)部分:clean表示已經(jīng)壓縮過(guò);dirty表示還未進(jìn)行壓縮,如下圖所示(active segment不會(huì)參與日志的壓縮操作,因?yàn)闀?huì)有新的數(shù)據(jù)寫(xiě)入該文件)。
-rw-r--r--1rootroot410月1119:02cleaner-offset-checkpoint
drwxr-xr-x2rootroot409610月1120:07nginx_access_log-0/
drwxr-xr-x2rootroot409610月1120:07nginx_access_log-1/
drwxr-xr-x2rootroot409610月1120:07nginx_access_log-2/
-rw-r--r--1rootroot09月1809:50.lock
-rw-r--r--1rootroot410月1611:19log-start-offset-checkpoint
-rw-r--r--1rootroot549月1809:50meta.properties
-rw-r--r--1rootroot151810月1611:19recovery-point-offset-checkpoint
-rw-r--r--1rootroot151810月1611:19replication-offset-checkpoint
#catcleaner-offset-checkpoint
nginx_access_log05033168
nginx_access_log15033166
nginx_access_log25033168
日志壓縮時(shí)會(huì)根據(jù)dirty部分?jǐn)?shù)據(jù)占日志文件的比例(cleanableRatio)來(lái)判斷優(yōu)先壓縮的日志,然后為dirty部分的數(shù)據(jù)建立key與offset映射關(guān)系(保存對(duì)應(yīng)key的最大offset)存入SkimpyoffsetMap中,然后復(fù)制segment分段中的數(shù)據(jù),只保留SkimpyoffsetMap中記錄的消息。
壓縮之后的相關(guān)日志文件大小會(huì)減少,為了避免出現(xiàn)過(guò)小的日志文件與索引文件,壓縮時(shí)會(huì)對(duì)所有的segment進(jìn)行分組(一個(gè)組的分片大小不會(huì)超過(guò)設(shè)置的log.segment.bytes值大小),同一個(gè)分組的多個(gè)分片日志壓縮之后變成一個(gè)分片。
如上圖所示,所有消息都還沒(méi)壓縮前clean checkpoint值為0,表示該分區(qū)的數(shù)據(jù)還沒(méi)進(jìn)行壓縮,第一次壓縮后,之前每個(gè)分片的日志文件大小都有所減少,同時(shí)會(huì)移動(dòng)clean checkpoint的位置到這一次壓縮結(jié)束的offset值。第二次壓縮時(shí),會(huì)將前兩個(gè)分片{0.5GB,0.4GB}組成一個(gè)分組,{0.7GB,0.2GB}組成一個(gè)分組進(jìn)行壓縮,以此類推。
如上圖所示,日志壓縮的主要流程如下:
1. 計(jì)算deleteHorizonMs值:當(dāng)某個(gè)消息的value值為空時(shí),該消息會(huì)被保留一段時(shí)間,超時(shí)之后會(huì)在下一次的得日志壓縮中被刪除,所以這里會(huì)計(jì)算deleteHorizonMs,根據(jù)該值確定可以刪除value值為空的日志分片。(deleteHorizonMs = clean部分的最后一個(gè)分片的lastModifiedTime - deleteRetionMs,deleteRetionMs通過(guò)配置文件log.cleaner.delete.retention.ms配置,默認(rèn)為24小時(shí))。
2. 確定壓縮dirty部分的offset范圍[firstDirtyOffset,endOffset):其中firstDirtyOffset表示dirty的起始位移,一般會(huì)等于clear checkpoint值,firstUncleanableOffset表示不能清理的最小位移,一般會(huì)等于活躍分片的baseOffset,然后從firstDirtyOffset位置開(kāi)始遍歷日志分片,并填充key與offset的映射關(guān)系至SkimpyoffsetMap中,當(dāng)該map被填充滿或到達(dá)上限firstUncleanableOffset時(shí),就可以確定日志壓縮上限endOffset。
3. 將(logStartOffset,endOffset)中的日志分片進(jìn)行分組,然后按照分組的方式進(jìn)行壓縮。
3. 副本
Kafka支持消息的冗余備份,可以設(shè)置對(duì)應(yīng)主題的副本數(shù)(--replication-factor參數(shù)設(shè)置主題的副本數(shù)可在創(chuàng)建主題的時(shí)候指定,offsets.topic.replication.factor設(shè)置消費(fèi)主題_consumer_offsets副本數(shù),默認(rèn)為3),每個(gè)副本包含的消息一樣(但不是完全一致,可能從副本的數(shù)據(jù)較主副本稍微有些落后)。
每個(gè)分區(qū)的副本集合中會(huì)有一個(gè)副本被選舉為主副本(leader),其他為從副本,所有的讀寫(xiě)請(qǐng)求由主副本對(duì)外提供,從副本負(fù)責(zé)將主副本的數(shù)據(jù)同步到自己所屬分區(qū),如果主副本所在分區(qū)宕機(jī),則會(huì)重新選舉出新的主副本對(duì)外提供服務(wù)。
3.1 ISR集合
ISR(In-Sync Replica)集合,表示目前可以用的副本集合,每個(gè)分區(qū)中的leader副本會(huì)維護(hù)此分區(qū)的ISR集合。這里的可用是指從副本的消息量與主副本的消息量相差不大,加入至ISR集合中的副本必須滿足以下幾個(gè)條件:
1. 副本所在節(jié)點(diǎn)需要與ZooKeeper維持心跳。
2. 從副本的最后一條消息的offset需要與主副本的最后一條消息offset差值不超過(guò)設(shè)定閾值(replica.lag.max.messages)或者副本的LEO落后于主副本的LEO時(shí)長(zhǎng)不大于設(shè)定閾值(replica.lag.time.max.ms),官方推薦使用后者判斷,并在新版本kafka0.10.0移除了replica.lag.max.messages參數(shù)。
如果從副本不滿足以上的任意條件,則會(huì)將其提出ISR集合,當(dāng)其再次滿足以上條件之后又會(huì)被重新加入集合中。ISR的引入主要是解決同步副本與異步復(fù)制兩種方案各自的缺陷(同步副本中如果有個(gè)副本宕機(jī)或者超時(shí)就會(huì)拖慢該副本組的整體性能;如果僅僅使用異步副本,當(dāng)所有的副本消息均遠(yuǎn)落后于主副本時(shí),一旦主副本宕機(jī)重新選舉,那么就會(huì)存在消息丟失情況)
3.2 HW&LEO
HW(High Watermark)是一個(gè)比較特殊的offset標(biāo)記,消費(fèi)端消費(fèi)時(shí)只能拉取到小于HW的消息而HW及之后的消息對(duì)于消費(fèi)者來(lái)說(shuō)是不可見(jiàn)的,該值由主副本管理,當(dāng)ISR集合中的全部從副本都拉取到HW指定消息之后,主副本會(huì)將HW值+1,即指向下一個(gè)offset位移,這樣可以保證HW之前消息的可靠性。
LEO(Log End Offset)表示當(dāng)前副本最新消息的下一個(gè)offset,所有副本都存在這樣一個(gè)標(biāo)記,如果是主副本,當(dāng)生產(chǎn)端往其追加消息時(shí),會(huì)將其值+1。當(dāng)從副本從主副本成功拉取到消息時(shí),其值也會(huì)增加。
3.2.1 從副本更新LEO與HW
從副本的數(shù)據(jù)是來(lái)自主副本,通過(guò)向主副本發(fā)送fetch請(qǐng)求獲取數(shù)據(jù),從副本的LEO值會(huì)保存在兩個(gè)地方,一個(gè)是自身所在的節(jié)點(diǎn)),一個(gè)是主副本所在節(jié)點(diǎn),自身節(jié)點(diǎn)保存LEO主要是為了更新自身的HW值,主副本保存從副本的LEO也是為了更新其HW。
當(dāng)從副本每寫(xiě)入一條新消息就會(huì)增加其自身的LEO,主副本收到從副本的fetch請(qǐng)求,會(huì)先從自身的日志中讀取對(duì)應(yīng)數(shù)據(jù),在數(shù)據(jù)返回給從副本之前會(huì)先去更新其保存的從副本LEO值。一旦從副本數(shù)據(jù)寫(xiě)入完成,就會(huì)嘗試更新自己的HW值,比較LEO與fetch響應(yīng)中主副本的返回HW,取最小值作為新的HW值。
3.2.2 主副本更新LEO與HW
主副本有日志寫(xiě)入時(shí)就會(huì)更新其自身的LEO值,與從副本類似。而主副本的HW值是分區(qū)的HW值,決定分區(qū)數(shù)據(jù)對(duì)應(yīng)消費(fèi)端的可見(jiàn)性,以下四種情況,主副本會(huì)嘗試更新其HW值:
1. 副本成為主副本:當(dāng)某個(gè)副本成為主副本時(shí),kafka會(huì)嘗試更新分區(qū)的HW值。
2. broker出現(xiàn)奔潰導(dǎo)致副本被踢出ISR集合:如果有broker節(jié)點(diǎn)奔潰則會(huì)看是否影響對(duì)應(yīng)分區(qū),然后會(huì)去檢查分區(qū)的HW值是否需要更新。
3. 生成端往主副本寫(xiě)入消息時(shí):消息寫(xiě)入會(huì)增加其LEO值,此時(shí)會(huì)查看是否需要修改HW值。
4. 主副本接受到從副本的fetch請(qǐng)求時(shí):主副本在處理從副本的fetch請(qǐng)求時(shí)會(huì)嘗試更新分區(qū)HW值。
前面是去嘗試更新HW,但是不一定會(huì)更新,主副本上保存著從副本的LEO值與自身的LEO值,這里會(huì)比較所有滿足條件的副本LEO值,并選擇最小的LEO值最為分區(qū)的HW值,其中滿足條件的副本是指滿足以下兩個(gè)條件之一:
1. 副本在ISR集合中
2. 副本的LEO落后于主副本的LEO時(shí)長(zhǎng)不大于設(shè)定閾值(replica.lag.time.max.ms,默認(rèn)為10s)
3.3 數(shù)據(jù)丟失場(chǎng)景
前面提到如果僅僅依賴HW來(lái)進(jìn)行日志截?cái)嘁约八坏呐袛鄷?huì)存在問(wèn)題,如上圖所示,假定存在兩個(gè)副本A、副本B,最開(kāi)始A為主副本,B為從副本,且參數(shù)min.insync.replicas=1,即ISR只有一個(gè)副本時(shí)也會(huì)返回成功:
1. 初始情況為主副本A已經(jīng)寫(xiě)入了兩條消息,對(duì)應(yīng)HW=1,LEO=2,LEOB=1,從副本B寫(xiě)入了一條消息,對(duì)應(yīng)HW=1,LEO=1。
2. 此時(shí)從副本B向主副本A發(fā)起fetchOffset=1請(qǐng)求,主副本收到請(qǐng)求之后更新LEOB=1,表示副本B已經(jīng)收到了消息0,然后嘗試更新HW值,min(LEO,LEOB)=1,即不需要更新,然后將消息1以及當(dāng)前分區(qū)HW=1返回給從副本B,從副本B收到響應(yīng)之后寫(xiě)入日志并更新LEO=2,然后更新其HW=1,雖然已經(jīng)寫(xiě)入了兩條消息,但是HW值需要在下一輪的請(qǐng)求才會(huì)更新為2。
3. 此時(shí)從副本B重啟,重啟之后會(huì)根據(jù)HW值進(jìn)行日志截?cái)啵聪?會(huì)被刪除。
4. 從副本B向主副本A發(fā)送fetchOffset=1請(qǐng)求,如果此時(shí)主副本A沒(méi)有什么異常,則跟第二步驟一樣沒(méi)有什么問(wèn)題,假設(shè)此時(shí)主副本也宕機(jī)了,那么從副本B會(huì)變成主副本。
5. 當(dāng)副本A恢復(fù)之后會(huì)變成從副本并根據(jù)HW值進(jìn)行日志截?cái)啵窗严?丟失,此時(shí)消息1就永久丟失了。
3.4 數(shù)據(jù)不一致場(chǎng)景
如圖所示,假定存在兩個(gè)副本A、副本B,最開(kāi)始A為主副本,B為從副本,且參數(shù)min.insync.replicas=1,即ISR只有一個(gè)副本時(shí)也會(huì)返回成功:
1. 初始狀態(tài)為主副本A已經(jīng)寫(xiě)入了兩條消息對(duì)應(yīng)HW=1,LEO=2,LEOB=1,從副本B也同步了兩條消息,對(duì)應(yīng)HW=1,LEO=2。
2. 此時(shí)從副本B向主副本發(fā)送fetchOffset=2請(qǐng)求,主副本A在收到請(qǐng)求后更新分區(qū)HW=2并將該值返回給從副本B,如果此時(shí)從副本B宕機(jī)則會(huì)導(dǎo)致HW值寫(xiě)入失敗。
3. 我們假設(shè)此時(shí)主副本A也宕機(jī)了,從副本B先恢復(fù)并成為主副本,此時(shí)會(huì)發(fā)生日志截?cái)啵槐A粝?,然后對(duì)外提供服務(wù),假設(shè)外部寫(xiě)入了一個(gè)消息1(這個(gè)消息與之前的消息1不一樣,用不同的顏色標(biāo)識(shí)不同消息)。
4. 等副本A起來(lái)之后會(huì)變成從副本,不會(huì)發(fā)生日志截?cái)啵驗(yàn)镠W=2,但是對(duì)應(yīng)位移1的消息其實(shí)是不一致的
3.5 leader epoch機(jī)制
HW值被用于衡量副本備份成功與否以及出現(xiàn)失敗情況時(shí)候的日志截?cái)嘁罁?jù)可能會(huì)導(dǎo)致數(shù)據(jù)丟失與數(shù)據(jù)不一致情況,因此在新版的Kafka(0.11.0.0)引入了leader epoch概念。
leader epoch表示一個(gè)鍵值對(duì)<epoch, offset>,其中epoch表示leader主副本的版本號(hào),從0開(kāi)始編碼,當(dāng)leader每變更一次就會(huì)+1,offset表示該epoch版本的主副本寫(xiě)入第一條消息的位置。
比如<0,0>表示第一個(gè)主副本從位移0開(kāi)始寫(xiě)入消息,<1,100>表示第二個(gè)主副本版本號(hào)為1并從位移100開(kāi)始寫(xiě)入消息,主副本會(huì)將該信息保存在緩存中并定期寫(xiě)入到checkpoint文件中,每次發(fā)生主副本切換都會(huì)去從緩存中查詢?cè)撔畔ⅲ旅婧?jiǎn)單介紹下leader epoch的工作原理:
1. 每條消息會(huì)都包含一個(gè)4字節(jié)的leader epoch number值
2. 每個(gè)log目錄都會(huì)創(chuàng)建一個(gè)leader epoch sequence文件用來(lái)存放主副本版本號(hào)以及開(kāi)始位移。
3. 當(dāng)一個(gè)副本成為主副本之后,會(huì)在leader epoch sequence文件末尾添加一條新的記錄,然后每條新的消息就會(huì)變成新的leader epoch值。
4. 當(dāng)某個(gè)副本宕機(jī)重啟之后,會(huì)進(jìn)行以下操作:
5. 從leader epoch sequence文件中恢復(fù)所有的leader epoch。
6. 向分區(qū)主副本發(fā)送LeaderEpoch請(qǐng)求,請(qǐng)求包含了從副本的leader epoch sequence文件中的最新leader epoch值。
7. 主副本返回從副本對(duì)應(yīng)LeaderEpoch的lastOffset,返回的lastOffset分為兩種情況,一種是返回比從副本請(qǐng)求中l(wèi)eader epoch版本大1的開(kāi)始位移,另外一種是與請(qǐng)求中的leader epoch相等則直接返回當(dāng)前主副本的LEO值。
8. 如果從副本的leader epoch開(kāi)始位移大于從leader中返回的lastOffset,那么會(huì)將從副本的leader epoch sequence值保持跟主副本一致。
9. 從副本截?cái)啾镜叵⒌街鞲北痉祷氐腖astOffset所在位移處。
10. 從副本開(kāi)始從主副本開(kāi)始拉取數(shù)據(jù)。
11. 在獲取數(shù)據(jù)時(shí),如果從副本發(fā)現(xiàn)消息中的leader epoch值比自身的最新leader epoch值大,則會(huì)將該leader epoch 值寫(xiě)到leader epoch sequence文件,然后繼續(xù)同步文件。
下面看下leader epoch機(jī)制如何避免前面提到的兩種異常場(chǎng)景
3.5.1 數(shù)據(jù)丟失場(chǎng)景解決
1. 如圖所示,當(dāng)從副本B重啟之后向主副本A發(fā)送offsetsForLeaderEpochRequest,epoch主從副本相等,則A返回當(dāng)前的LEO=2,從副本B中沒(méi)有任何大于2的位移,因此不需要截?cái)唷?/p>
2. 當(dāng)從副本B向主副本A發(fā)送fetchoffset=2請(qǐng)求時(shí),A宕機(jī),所以從副本B成為主副本,并更新epoch值為<epoch=1, offset=2>,HW值更新為2。
3. 當(dāng)A恢復(fù)之后成為從副本,并向B發(fā)送fetcheOffset=2請(qǐng)求,B返回HW=2,則從副本A更新HW=2。
4. 主副本B接受外界的寫(xiě)請(qǐng)求,從副本A向主副本A不斷發(fā)起數(shù)據(jù)同步請(qǐng)求。
從上可以看出引入leader epoch值之后避免了前面提到的數(shù)據(jù)丟失情況,但是這里需要注意的是如果在上面的第一步,從副本B起來(lái)之后向主副本A發(fā)送offsetsForLeaderEpochRequest請(qǐng)求失敗,即主副本A同時(shí)也宕機(jī)了,那么消息1就會(huì)丟失,具體可見(jiàn)下面數(shù)據(jù)不一致場(chǎng)景中有提到。
3.5.2 數(shù)據(jù)不一致場(chǎng)景解決
1. 從副本B恢復(fù)之后向主副本A發(fā)送offsetsForLeaderEpochRequest請(qǐng)求,由于主副本也宕機(jī)了,因此副本B將變成主副本并將消息1截?cái)啵藭r(shí)接受到新消息1的寫(xiě)入。
2. 副本A恢復(fù)之后變成從副本并向主副本A發(fā)送offsetsForLeaderEpochRequest請(qǐng)求,請(qǐng)求的epoch值小于主副本B,因此主副本B會(huì)返回epoch=1時(shí)的開(kāi)始位移,即lastoffset=1,因此從副本A會(huì)截?cái)嘞?。
3. 從副本A從主副本B拉取消息,并更新epoch值<epoch=1, offset=1>。
可以看出epoch的引入避免的數(shù)據(jù)不一致,但是兩個(gè)副本均宕機(jī),則還是存在數(shù)據(jù)丟失的場(chǎng)景,前面的所有討論都是建立在min.insync.replicas=1的前提下,因此需要在數(shù)據(jù)的可靠性與速度方面做權(quán)衡。
4. 生產(chǎn)者
4.1 消息分區(qū)選擇
生產(chǎn)者的作用主要是生產(chǎn)消息,將消息存入到Kafka對(duì)應(yīng)主題的分區(qū)中,具體某個(gè)消息應(yīng)該存入哪個(gè)分區(qū),有以下三個(gè)策略決定(優(yōu)先級(jí)由上到下,依次遞減):
1. 如果消息發(fā)送時(shí)指定了消息所屬分區(qū),則會(huì)直接發(fā)往指定分區(qū)。
2. 如果沒(méi)有指定消息分區(qū),但是設(shè)置了消息的key,則會(huì)根據(jù)key的哈希值選擇分區(qū)。
3. 如果前兩者均不滿足,則會(huì)采用輪詢的方式選擇分區(qū)。
4.2 ack參數(shù)設(shè)置及意義
生產(chǎn)端往kafka集群發(fā)送消息時(shí),可以通過(guò)request.required.acks參數(shù)來(lái)設(shè)置數(shù)據(jù)的可靠性級(jí)別
1:默認(rèn)為1,表示在ISR中的leader副本成功接收到數(shù)據(jù)并確認(rèn)后再發(fā)送下一條消息,如果主節(jié)點(diǎn)宕機(jī)則可能出現(xiàn)數(shù)據(jù)丟失場(chǎng)景,詳細(xì)分析可參考前面提到的副本章節(jié)。
0:表示生產(chǎn)端不需要等待節(jié)點(diǎn)的確認(rèn)就可以繼續(xù)發(fā)送下一批數(shù)據(jù),這種情況下數(shù)據(jù)傳輸效率最高,但是數(shù)據(jù)的可靠性最低。
-1:表示生產(chǎn)端需要等待ISR中的所有副本節(jié)點(diǎn)都收到數(shù)據(jù)之后才算消息寫(xiě)入成功,可靠性最高,但是性能最低,如果服務(wù)端的min.insync.replicas值設(shè)置為1,那么在這種情況下允許ISR集合只有一個(gè)副本,因此也會(huì)存在數(shù)據(jù)丟失的情況。
4.3 冪等特性
所謂的冪等性,是指一次或者多次請(qǐng)求某一個(gè)資源對(duì)于資源本身應(yīng)該具有同樣的結(jié)果(網(wǎng)絡(luò)超時(shí)等問(wèn)題除外),通俗一點(diǎn)的理解就是同一個(gè)操作任意執(zhí)行多次產(chǎn)生的影響或效果與一次執(zhí)行影響相同,冪等的關(guān)鍵在于服務(wù)端能否識(shí)別出請(qǐng)求是否重復(fù),然后過(guò)濾掉這些重復(fù)請(qǐng)求,通常情況下需要以下信息來(lái)實(shí)現(xiàn)冪等特性:
1. 唯一標(biāo)識(shí):判斷某個(gè)請(qǐng)求是否重復(fù),需要有一個(gè)唯一性標(biāo)識(shí),然后服務(wù)端就能根據(jù)這個(gè)唯一標(biāo)識(shí)來(lái)判斷是否為重復(fù)請(qǐng)求。
2. 記錄已經(jīng)處理過(guò)的請(qǐng)求:服務(wù)端需要記錄已經(jīng)處理過(guò)的請(qǐng)求,然后根據(jù)唯一標(biāo)識(shí)來(lái)判斷是否是重復(fù)請(qǐng)求,如果已經(jīng)處理過(guò),則直接拒絕或者不做任何操作返回成功。
kafka中Producer端的冪等性是指當(dāng)發(fā)送同一條消息時(shí),消息在集群中只會(huì)被持久化一次,其冪等是在以下條件中才成立:
3. 只能保證生產(chǎn)端在單個(gè)會(huì)話內(nèi)的冪等,如果生產(chǎn)端因?yàn)槟承┰蛞馔鈷斓羧缓笾貑ⅲ藭r(shí)是沒(méi)辦法保證冪等的,因?yàn)檫@時(shí)沒(méi)辦法獲取到之前的狀態(tài)信息,即無(wú)法做到垮會(huì)話級(jí)別的冪等。
4. 冪等性不能垮多個(gè)主題分區(qū),只能保證單個(gè)分區(qū)內(nèi)的冪等,涉及到多個(gè)消息分區(qū)時(shí),中間的狀態(tài)并沒(méi)有同步。
如果要支持垮會(huì)話或者垮多個(gè)消息分區(qū)的情況,則需要使用kafka的事務(wù)性來(lái)實(shí)現(xiàn)。
為了實(shí)現(xiàn)生成端的冪等語(yǔ)義,引入了Producer ID(PID)與Sequence Number的概念:
5. Producer ID(PID):每個(gè)生產(chǎn)者在初始化時(shí)都會(huì)分配一個(gè)唯一的PID,PID的分配對(duì)于用戶來(lái)說(shuō)是透明的。
6. Sequence Number(序列號(hào)):對(duì)于給定的PID而言,序列號(hào)從0開(kāi)始單調(diào)遞增,每個(gè)主題分區(qū)均會(huì)產(chǎn)生一個(gè)獨(dú)立序列號(hào),生產(chǎn)者在發(fā)送消息時(shí)會(huì)給每條消息添加一個(gè)序列號(hào)。broker端緩存了已經(jīng)提交消息的序列號(hào),只有比緩存分區(qū)中最后提交消息的序列號(hào)大1的消息才會(huì)被接受,其他會(huì)被拒絕。
4.3.1 生產(chǎn)端消息發(fā)送流程
下面簡(jiǎn)單介紹下支持冪等的消息發(fā)送端工作流程
1. 生產(chǎn)端通過(guò)Kafkaproducer會(huì)將數(shù)據(jù)添加到RecordAccumulator中,數(shù)據(jù)添加時(shí)會(huì)判斷是否需要新建一個(gè)ProducerBatch。
2. 生產(chǎn)端后臺(tái)啟動(dòng)發(fā)送線程,會(huì)判斷當(dāng)前的PID是否需要重置,重置的原因是因?yàn)槟承┫⒎謪^(qū)的batch重試多次仍然失敗最后因?yàn)槌瑫r(shí)而被移除,這個(gè)時(shí)候序列號(hào)無(wú)法連續(xù),導(dǎo)致后續(xù)消息無(wú)法發(fā)送,因此會(huì)重置PID,并將相關(guān)緩存信息清空,這個(gè)時(shí)候消息會(huì)丟失。
3. 發(fā)送線程判斷是否需要新申請(qǐng)PID,如果需要?jiǎng)t會(huì)阻塞直到獲取到PID信息。
4. 發(fā)送線程在調(diào)用sendProducerData()方法發(fā)送數(shù)據(jù)時(shí),會(huì)進(jìn)行以下判斷:
? ?判斷主題分區(qū)是否可以繼續(xù)發(fā)送、PID是否有效、如果是重試batch需要判斷之前的batch是否發(fā)送完成,如果沒(méi)有發(fā)送完成則會(huì)跳過(guò)當(dāng)前主題分區(qū)的消息發(fā)送,直到前面的batch發(fā)送完成。
? ?如果對(duì)應(yīng)ProducerBatch沒(méi)有分配對(duì)應(yīng)的PID與序列號(hào)信息,則會(huì)在這里進(jìn)行設(shè)置。
4.3.2 服務(wù)端消息接受流程
服務(wù)端(broker)在收到生產(chǎn)端發(fā)送的數(shù)據(jù)寫(xiě)請(qǐng)求之后,會(huì)進(jìn)行一些判斷來(lái)決定是否可以寫(xiě)入數(shù)據(jù),這里也主要介紹關(guān)于冪等相關(guān)的操作流程。
1. 如果請(qǐng)求設(shè)置了冪等特性,則會(huì)檢查是否對(duì)ClusterResource有IdempotentWrite權(quán)限,如果沒(méi)有,則會(huì)返回錯(cuò)誤CLUSTER_AUTHORIZATION_FAILED。
2. 檢查是否有PID信息。
3. 根據(jù)batch的序列號(hào)檢查該batch是否重復(fù),服務(wù)端會(huì)緩存每個(gè)PID對(duì)應(yīng)主題分區(qū)的最近5個(gè)batch信息,如果有重復(fù),則直接返回寫(xiě)入成功,但是不會(huì)執(zhí)行真正的數(shù)據(jù)寫(xiě)入操作。
4. 如果有PID且非重復(fù)batch,則進(jìn)行以下操作:
? ?判斷該P(yáng)ID是否已經(jīng)存在緩存中。
? ?如果不存在則判斷序列號(hào)是否是從0開(kāi)始,如果是則表示為新的PID,在緩存中記錄PID的信息(包括PID、epoch以及序列號(hào)信息),然后執(zhí)行數(shù)據(jù)寫(xiě)入操作;如果不存在但是序列號(hào)不是從0開(kāi)始,則直接返回錯(cuò)誤,表示PID在服務(wù)端以及過(guò)期或者PID寫(xiě)的數(shù)據(jù)已經(jīng)過(guò)期。
? ?如果PID存在,則會(huì)檢查PID的epoch版本是否與服務(wù)端一致,如果不一致且序列號(hào)不是從0開(kāi)始,則返回錯(cuò)誤。如果epoch不一致但是序列號(hào)是從0開(kāi)始,則可以正常寫(xiě)入。
? ?如果epoch版本一致,則會(huì)查詢緩存中最近一次序列號(hào)是否連續(xù),不連續(xù)則會(huì)返回錯(cuò)誤,否則正常寫(xiě)入。
5. 消費(fèi)者
消費(fèi)者主要是從Kafka集群拉取消息,然后進(jìn)行相關(guān)的消費(fèi)邏輯,消費(fèi)者的消費(fèi)進(jìn)度由其自身控制,增加消費(fèi)的靈活性,比如消費(fèi)端可以控制重復(fù)消費(fèi)某些消息或者跳過(guò)某些消息進(jìn)行消費(fèi)。
5.1 消費(fèi)組
多個(gè)消費(fèi)者可以組成一個(gè)消費(fèi)組,每個(gè)消費(fèi)者只屬于一個(gè)消費(fèi)組。消費(fèi)組訂閱主題的每個(gè)分區(qū)只會(huì)分配給該消費(fèi)組中的某個(gè)消費(fèi)者處理,不同的消費(fèi)組之間彼此隔離無(wú)依賴。同一個(gè)消息只會(huì)被消費(fèi)組中的一個(gè)消費(fèi)者消費(fèi),如果想要讓同一個(gè)消息被多個(gè)消費(fèi)者消費(fèi),那么每個(gè)消費(fèi)者需要屬于不同的消費(fèi)組,且對(duì)應(yīng)消費(fèi)組中只有該一個(gè)消費(fèi)者,消費(fèi)組的引入可以實(shí)現(xiàn)消費(fèi)的“獨(dú)占”或“廣播”效果。
消費(fèi)組下可以有多個(gè)消費(fèi)者,個(gè)數(shù)支持動(dòng)態(tài)變化。
消費(fèi)組訂閱主題下的每個(gè)分區(qū)只會(huì)分配給消費(fèi)組中的一個(gè)消費(fèi)者。
group.id標(biāo)識(shí)消費(fèi)組,相同則屬于同一消費(fèi)組。
不同消費(fèi)組之間相互隔離互不影響。
如圖所示,消費(fèi)組1中包含兩個(gè)消費(fèi)者,其中消費(fèi)者1分配消費(fèi)分區(qū)0,消費(fèi)者2分配消費(fèi)分區(qū)1與分區(qū)2。此外消費(fèi)組的引入還支持消費(fèi)者的水平擴(kuò)展及故障轉(zhuǎn)移,比如從上圖我們可以看出消費(fèi)者2的消費(fèi)能力不足,相對(duì)消費(fèi)者1來(lái)說(shuō)消費(fèi)進(jìn)度比較落后,我們可以往消費(fèi)組里面增加一個(gè)消費(fèi)者以提高其整體的消費(fèi)能力,如下圖所示。
假設(shè)消費(fèi)者1所在機(jī)器出現(xiàn)宕機(jī),消費(fèi)組會(huì)發(fā)送重平衡,假設(shè)將分區(qū)0分配給消費(fèi)者2進(jìn)行消費(fèi),如下圖所示。同個(gè)消費(fèi)組中消費(fèi)者的個(gè)數(shù)不是越多越好,最大不能超過(guò)主題對(duì)應(yīng)的分區(qū)數(shù),如果超過(guò)則會(huì)出現(xiàn)超過(guò)的消費(fèi)者分配不到分區(qū)的情況,因?yàn)榉謪^(qū)一旦分配給消費(fèi)者就不會(huì)再變動(dòng),除非組內(nèi)消費(fèi)者個(gè)數(shù)出現(xiàn)變動(dòng)而發(fā)生重平衡。
5.2 消費(fèi)位移
5.2.1 消費(fèi)位移主題
Kafka 0.9開(kāi)始將消費(fèi)端的位移信息保存在集群的內(nèi)部主題(__consumer_offsets)中,該主題默認(rèn)為50個(gè)分區(qū),每條日志項(xiàng)的格式都是:<TopicPartition, OffsetAndMetadata>,其key為主題分區(qū)主要存放主題、分區(qū)以及消費(fèi)組信息,value為OffsetAndMetadata對(duì)象主要包括位移、位移提交時(shí)間、自定義元數(shù)據(jù)等信息。
只有消費(fèi)組往kafka中提交位移才會(huì)往這個(gè)主題中寫(xiě)入數(shù)據(jù),如果消費(fèi)端將消費(fèi)位移信息保存在外部存儲(chǔ),則不會(huì)有消費(fèi)位移信息,下面可以通過(guò)kafka-console-consumer.sh腳本查看主題消費(fèi)位移信息。
# bin/kafka-console-consumer.sh --topic __consumer_offsets --bootstrap-server localhost:9092--formatter"kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter"--consumer.config config/consumer.properties --from-beginning
[consumer-group01,nginx_access_log,2]::OffsetAndMetadata(offset=17104625, leaderEpoch=Optional.[0], metadata=, commitTimestamp=1573475863555, expireTimestamp=None)
[consumer-group01,nginx_access_log,1]::OffsetAndMetadata(offset=17103024, leaderEpoch=Optional.[0], metadata=, commitTimestamp=1573475863555, expireTimestamp=None)
[consumer-group01,nginx_access_log,0]::OffsetAndMetadata(offset=17107771, leaderEpoch=Optional.[0], metadata=, commitTimestamp=1573475863555, expireTimestamp=None)
5.2.2 消費(fèi)位移自動(dòng)提交
消費(fèi)端可以通過(guò)設(shè)置參數(shù)enable.auto.commit來(lái)控制是自動(dòng)提交還是手動(dòng),如果值為true則表示自動(dòng)提交,在消費(fèi)端的后臺(tái)會(huì)定時(shí)的提交消費(fèi)位移信息,時(shí)間間隔由auto.commit.interval.ms(默認(rèn)為5秒)。
但是如果設(shè)置為自動(dòng)提交會(huì)存在以下幾個(gè)問(wèn)題:
1. 可能存在重復(fù)的位移數(shù)據(jù)提交到消費(fèi)位移主題中,因?yàn)槊扛?秒會(huì)往主題中寫(xiě)入一條消息,不管是否有新的消費(fèi)記錄,這樣就會(huì)產(chǎn)生大量的同key消息,其實(shí)只需要一條,因此需要依賴前面提到日志壓縮策略來(lái)清理數(shù)據(jù)。
2. 重復(fù)消費(fèi),假設(shè)位移提交的時(shí)間間隔為5秒,那么在5秒內(nèi)如果發(fā)生了rebalance,則所有的消費(fèi)者會(huì)從上一次提交的位移處開(kāi)始消費(fèi),那么期間消費(fèi)的數(shù)據(jù)則會(huì)再次被消費(fèi)。
5.2.3 消費(fèi)位移手動(dòng)提交
手動(dòng)提交需要將enable.auto.commit值設(shè)置為false,然后由業(yè)務(wù)消費(fèi)端來(lái)控制消費(fèi)進(jìn)度,手動(dòng)提交又分為以下三種類型:
1. 同步手動(dòng)提交位移:如果調(diào)用的是同步提交方法commitSync(),則會(huì)將poll拉取的最新位移提交到kafka集群,提交成功前會(huì)一直等待提交成功。
2. 異步手動(dòng)提交位移:調(diào)用異步提交方法commitAsync(),在調(diào)用該方法之后會(huì)立刻返回,不會(huì)阻塞,然后可以通過(guò)回調(diào)函數(shù)執(zhí)行相關(guān)的異常處理邏輯。
3. 指定提交位移:指定位移提交也分為異步跟同步,傳參為Map<TopicPartition, OffsetAndMetadata>,其中key為消息分區(qū),value為位移對(duì)象。
5.3 分組協(xié)調(diào)者
分組協(xié)調(diào)者(Group Coordinator)是一個(gè)服務(wù),kafka集群中的每個(gè)節(jié)點(diǎn)在啟動(dòng)時(shí)都會(huì)啟動(dòng)這樣一個(gè)服務(wù),該服務(wù)主要是用來(lái)存儲(chǔ)消費(fèi)分組相關(guān)的元數(shù)據(jù)信息,每個(gè)消費(fèi)組均會(huì)選擇一個(gè)協(xié)調(diào)者來(lái)負(fù)責(zé)組內(nèi)各個(gè)分區(qū)的消費(fèi)位移信息存儲(chǔ),選擇的主要步驟如下:
1. 首選確定消費(fèi)組的位移信息存入哪個(gè)分區(qū):前面提到默認(rèn)的__consumer_offsets主題分區(qū)數(shù)為50,通過(guò)以下算法可以計(jì)算出對(duì)應(yīng)消費(fèi)組的位移信息應(yīng)該存入哪個(gè)分區(qū)partition = Math.abs(groupId.hashCode() % groupMetadataTopicPartitionCount)其中g(shù)roupId為消費(fèi)組的id,這個(gè)由消費(fèi)端指定,groupMetadataTopicPartitionCount為主題分區(qū)數(shù)。
2. 根據(jù)partition尋找該分區(qū)的leader所對(duì)應(yīng)的節(jié)點(diǎn)broker,該broker的Coordinator即為該消費(fèi)組的Coordinator。
5.4 重平衡機(jī)制
5.4.1 重平衡發(fā)生場(chǎng)景
以下幾種場(chǎng)景均會(huì)觸發(fā)重平衡操作:
1. 新的消費(fèi)者加入到消費(fèi)組中。
2. 消費(fèi)者被動(dòng)下線。比如消費(fèi)者長(zhǎng)時(shí)間的GC、網(wǎng)絡(luò)延遲導(dǎo)致消費(fèi)者長(zhǎng)時(shí)間未向Group Coordinator發(fā)送心跳請(qǐng)求,均會(huì)認(rèn)為該消費(fèi)者已經(jīng)下線并踢出。
3. 消費(fèi)者主動(dòng)退出消費(fèi)組。
4. 消費(fèi)組訂閱的任意一個(gè)主題分區(qū)數(shù)出現(xiàn)變化。
5. 消費(fèi)者取消某個(gè)主題的訂閱。
5.4.2 重平衡操作流程
重平衡的實(shí)現(xiàn)可以分為以下幾個(gè)階段:
1. 查找Group Coordinator:消費(fèi)者會(huì)從kafka集群中選擇一個(gè)負(fù)載最小的節(jié)點(diǎn)發(fā)送GroupCoorinatorRequest請(qǐng)求,并處理返回響應(yīng)GroupCoordinatorResponse。其中請(qǐng)求參數(shù)中包含消費(fèi)組的id,響應(yīng)中包含Coordinator所在節(jié)點(diǎn)id、host以及端口號(hào)信息。
2. Join group:當(dāng)消費(fèi)者拿到協(xié)調(diào)者的信息之后會(huì)往協(xié)調(diào)者發(fā)送加入消費(fèi)組的請(qǐng)求JoinGroupRequest,當(dāng)所有的消費(fèi)者都發(fā)送該請(qǐng)求之后,協(xié)調(diào)者會(huì)從中選擇一個(gè)消費(fèi)者作為leader角色,然后將組內(nèi)成員信息、訂閱等信息發(fā)給消費(fèi)者(響應(yīng)格式JoinGroupResponse見(jiàn)下表),leader負(fù)責(zé)消費(fèi)方案的分配。
JoinGroupRequest請(qǐng)求數(shù)據(jù)格式
JoinGroupResponse響應(yīng)數(shù)據(jù)格式式
1. Synchronizing Group State階段:當(dāng)leader消費(fèi)者完成消費(fèi)方案的分配后會(huì)發(fā)送SyncGroupRequest請(qǐng)求給協(xié)調(diào)者,其他非leader節(jié)點(diǎn)也會(huì)發(fā)送該請(qǐng)求,只是請(qǐng)求參數(shù)為空,然后協(xié)調(diào)者將分配結(jié)果作為響應(yīng)SyncGroupResponse發(fā)給各個(gè)消費(fèi)者,請(qǐng)求及相應(yīng)的數(shù)據(jù)格式如下表所示:
SyncGroupRequest請(qǐng)求數(shù)據(jù)格式
SyncGroupResponse響應(yīng)數(shù)據(jù)格式
5.4.3 分區(qū)分配策略
Kafka提供了三個(gè)分區(qū)分配策略:RangeAssignor、RoundRobinAssignor以及StickyAssignor,下面簡(jiǎn)單介紹下各個(gè)算法的實(shí)現(xiàn)。
1. RangeAssignor:kafka默認(rèn)會(huì)采用此策略進(jìn)行分區(qū)分配,主要流程如下
假設(shè)一個(gè)消費(fèi)組中存在兩個(gè)消費(fèi)者{C0,C1},該消費(fèi)組訂閱了三個(gè)主題{T1,T2,T3},每個(gè)主題分別存在三個(gè)分區(qū),一共就有9個(gè)分區(qū){TP1,TP2,...,TP9}。通過(guò)以上算法我們可以得到D=4,R=1,那么消費(fèi)組C0將消費(fèi)的分區(qū)為{TP1,TP2,TP3,TP4,TP5},C1將消費(fèi)分區(qū){TP6,TP7,TP8,TP9}。這里存在一個(gè)問(wèn)題,如果不能均分,那么前面的幾個(gè)消費(fèi)者將會(huì)多消費(fèi)一個(gè)分區(qū)。
將所有訂閱主題下的分區(qū)進(jìn)行排序得到集合TP={TP0,Tp1,...,TPN+1}。
對(duì)消費(fèi)組中的所有消費(fèi)者根據(jù)名字進(jìn)行字典排序得到集合CG={C0,C1,...,CM+1}。
計(jì)算D=N/M,R=N%M。
消費(fèi)者Ci獲取消費(fèi)分區(qū)起始位置=D*i+min(i,R),Ci獲取的分區(qū)總數(shù)=D+(if (i+1>R)0 else 1)。
2. RoundRobinAssignor:使用該策略需要滿足以下兩個(gè)條件:1) 消費(fèi)組中的所有消費(fèi)者應(yīng)該訂閱主題相同;2) 同一個(gè)消費(fèi)組的所有消費(fèi)者在實(shí)例化時(shí)給每個(gè)主題指定相同的流數(shù)。
對(duì)所有主題的所有分區(qū)根據(jù)主題+分區(qū)得到的哈希值進(jìn)行排序。
對(duì)所有消費(fèi)者按字典排序。
通過(guò)輪詢的方式將分區(qū)分配給消費(fèi)者。
3. StickyAssignor:該分配方式在0.11版本開(kāi)始引入,主要是保證以下特性:1) 盡可能的保證分配均衡;2) 當(dāng)重新分配時(shí),保留盡可能多的現(xiàn)有分配。其中第一條的優(yōu)先級(jí)要大于第二條。
如果你也在學(xué)習(xí),在入門(mén)學(xué)習(xí)的過(guò)程當(dāng)中有遇見(jiàn)學(xué)習(xí),行業(yè)方面的問(wèn)題,或者說(shuō)缺乏系統(tǒng)的學(xué)習(xí)路線和系統(tǒng)學(xué)習(xí)視頻,你可以加入我的 學(xué)習(xí)交流群:322708204 里面有我根據(jù)今年市場(chǎng)技術(shù)棧要求錄制的Java精講視頻教程,群文件里面有我這幾年整理的學(xué)習(xí)手冊(cè),面試題,開(kāi)發(fā)工具,PDF文檔書(shū)籍,你都可以自行去下載:
https://shimo.im/docs/g9hrHVqXw6HjYX3y/
6. 總結(jié)
在本文中,我們圍繞Kafka的特性,詳細(xì)介紹了其原理實(shí)現(xiàn),通過(guò)主題與日志的深入剖析,了解了Kafka內(nèi)部消息的存放、檢索以及刪除機(jī)制。副本系統(tǒng)中的ISR概念的引入解決同步副本與異步復(fù)制兩種方案各自的缺陷,lead epoch機(jī)制的出現(xiàn)解決了數(shù)據(jù)丟失以及數(shù)據(jù)不一致問(wèn)題。生產(chǎn)端的分區(qū)選擇算法實(shí)現(xiàn)了數(shù)據(jù)均衡,冪等特性的支持則解決了之前存在的重復(fù)消息問(wèn)題。
最后介紹了消費(fèi)端的相關(guān)原理,消費(fèi)組機(jī)制實(shí)現(xiàn)了消費(fèi)端的消息隔離,既有廣播也有獨(dú)占的場(chǎng)景支持,而重平衡機(jī)制則保證的消費(fèi)端的健壯性與擴(kuò)展性。