Kafka設(shè)計(jì)解析(六)- Kafka高性能架構(gòu)之道
原創(chuàng)文章,轉(zhuǎn)載請(qǐng)務(wù)必將下面這段話置于文章開頭處。
本文轉(zhuǎn)發(fā)自技術(shù)世界,原文鏈接 http://www.jasongj.com/kafka/high_throughput/
簡(jiǎn)介
? 本文從宏觀架構(gòu)層面和微觀實(shí)現(xiàn)層面分析了Kafka如何實(shí)現(xiàn)高性能。包含Kafka如何利用Partition實(shí)現(xiàn)并行處理和提供水平擴(kuò)展能力,如何通過(guò)ISR實(shí)現(xiàn)可用性和數(shù)據(jù)一致性的動(dòng)態(tài)平衡,如何使用NIO和Linux的sendfile實(shí)現(xiàn)零拷貝以及如何通過(guò)順序讀寫和數(shù)據(jù)壓縮實(shí)現(xiàn)磁盤的高效利用。
宏觀架構(gòu)層面
利用Partition實(shí)現(xiàn)并行處理
Partition提供并行處理的能力
Kafka是一個(gè)Pub-Sub的消息系統(tǒng),無(wú)論是發(fā)布還是訂閱,都須指定Topic。如《Kafka設(shè)計(jì)解析(一)- Kafka背景及架構(gòu)介紹》一文所述,Topic只是一個(gè)邏輯的概念。每個(gè)Topic都包含一個(gè)或多個(gè)Partition,不同Partition可位于不同節(jié)點(diǎn)。同時(shí)Partition在物理上對(duì)應(yīng)一個(gè)本地文件夾,每個(gè)Partition包含一個(gè)或多個(gè)Segment,每個(gè)Segment包含一個(gè)數(shù)據(jù)文件和一個(gè)與之對(duì)應(yīng)的索引文件。在邏輯上,可以把一個(gè)Partition當(dāng)作一個(gè)非常長(zhǎng)的數(shù)組,可通過(guò)這個(gè)“數(shù)組”的索引(offset)去訪問(wèn)其數(shù)據(jù)。
Topic是邏輯概念
Partition在物理上對(duì)應(yīng)一個(gè)本地文件夾
Partition = Segment + Segment + Segment + .....
Segment = 數(shù)據(jù)文件 + 索引文件(offset)
一方面,由于不同Partition可位于不同機(jī)器,因此可以充分利用集群優(yōu)勢(shì),實(shí)現(xiàn)機(jī)器間的并行處理。另一方面,由于Partition在物理上對(duì)應(yīng)一個(gè)文件夾,即使多個(gè)Partition位于同一個(gè)節(jié)點(diǎn),也可通過(guò)配置讓同一節(jié)點(diǎn)上的不同Partition置于不同的disk drive上,從而實(shí)現(xiàn)磁盤間的并行處理,充分發(fā)揮多磁盤的優(yōu)勢(shì)。
不同的Partition可以位于不同的機(jī)器-------實(shí)現(xiàn)機(jī)器間的并行處理
Partition在物理上對(duì)應(yīng)一個(gè)文件夾-------實(shí)現(xiàn)磁盤間的并行處理
利用多磁盤的具體方法是,將不同磁盤mount到不同目錄,然后在server.properties中,將log.dirs
設(shè)置為多目錄(用逗號(hào)分隔)。Kafka會(huì)自動(dòng)將所有Partition盡可能均勻分配到不同目錄也即不同目錄(也即不同disk)上。
Kafka自動(dòng)將所有Partition盡可能均勻分配到不同的disk上
注:雖然物理上最小單位是Segment,但Kafka并不提供同一Partition內(nèi)不同Segment間的并行處理。因?yàn)閷?duì)于寫而言,每次只會(huì)寫Partition內(nèi)的一個(gè)Segment,而對(duì)于讀而言,也只會(huì)順序讀取同一Partition內(nèi)的不同Segment。
Partition是最小并發(fā)粒度
如同《Kafka設(shè)計(jì)解析(四)- Kafka Consumer設(shè)計(jì)解析》一文所述,多Consumer消費(fèi)同一個(gè)Topic時(shí),同一條消息只會(huì)被同一Consumer Group內(nèi)的一個(gè)Consumer所消費(fèi)。而數(shù)據(jù)并非按消息為單位分配,而是以Partition為單位分配,也即同一個(gè)Partition的數(shù)據(jù)只會(huì)被一個(gè)Consumer所消費(fèi)(在不考慮Rebalance的前提下)。
同一個(gè)Partition只會(huì)被一個(gè)Consumer消費(fèi)
Partition個(gè)數(shù)決定了可能的最大并行度
如果Consumer的個(gè)數(shù)多于Partition的個(gè)數(shù),那么會(huì)有部分Consumer無(wú)法消費(fèi)該Topic的任何數(shù)據(jù),也即當(dāng)Consumer個(gè)數(shù)超過(guò)Partition后,增加Consumer并不能增加并行度。
簡(jiǎn)而言之,Partition個(gè)數(shù)決定了可能的最大并行度。如下圖所示,由于Topic 2只包含3個(gè)Partition,故group2中的Consumer 3、Consumer 4、Consumer 5 可分別消費(fèi)1個(gè)Partition的數(shù)據(jù),而Consumer 6消費(fèi)不到Topic 2的任何數(shù)據(jù)。
以Spark消費(fèi)Kafka數(shù)據(jù)為例,如果所消費(fèi)的Topic的Partition數(shù)為N,則有效的Spark最大并行度也為N。即使將Spark的Executor數(shù)設(shè)置為N+M,最多也只有N個(gè)Executor可同時(shí)處理該Topic的數(shù)據(jù)。
ISR實(shí)現(xiàn)可用性與數(shù)據(jù)一致性的動(dòng)態(tài)平衡
CAP理論
CAP理論是指,分布式系統(tǒng)中,一致性、可用性和分區(qū)容忍性最多只能同時(shí)滿足兩個(gè)。
一致性:consistency
- 通過(guò)某個(gè)節(jié)點(diǎn)的寫操作結(jié)果對(duì)后面通過(guò)其它節(jié)點(diǎn)的讀操作可見(jiàn)
- 如果更新數(shù)據(jù)后,并發(fā)訪問(wèn)情況下后續(xù)讀操作可立即感知該更新,稱為強(qiáng)一致性
- 如果允許之后部分或者全部感知不到該更新,稱為弱一致性
- 若在之后的一段時(shí)間(通常該時(shí)間不固定)后,一定可以感知到該更新,稱為最終一致性
可用性:availability
- 任何一個(gè)沒(méi)有發(fā)生故障的節(jié)點(diǎn)必須在有限的時(shí)間內(nèi)返回合理的結(jié)果
分區(qū)容忍性:Patience
- 部分節(jié)點(diǎn)宕機(jī)或者無(wú)法與其它節(jié)點(diǎn)通信時(shí),各分區(qū)間還可保持分布式系統(tǒng)的功能
常用數(shù)據(jù)復(fù)制及一致性方案
Master-Slave
- RDBMS的讀寫分離即為典型的Master-Slave方案
- 同步復(fù)制可保證強(qiáng)一致性但會(huì)影響可用性
- 異步復(fù)制可提供高可用性但會(huì)降低一致性
WNR
- 主要用于去中心化的分布式系統(tǒng)中。DynamoDB與Cassandra即采用此方案或其變種
- N代表總副本數(shù),W代表每次寫操作要保證的最少寫成功的副本數(shù),R代表每次讀至少要讀取的副本數(shù)
- 當(dāng)W+R>N時(shí),可保證每次讀取的數(shù)據(jù)至少有一個(gè)副本擁有最新的數(shù)據(jù)
- 多個(gè)寫操作的順序難以保證,可能導(dǎo)致多副本間的寫操作順序不一致。Dynamo通過(guò)向量時(shí)鐘保證最終一致性
Paxos及其變種
- Google的Chubby,Zookeeper的原子廣播協(xié)議(Zab),RAFT等
基于ISR的數(shù)據(jù)復(fù)制方案
如《 Kafka High Availability(上)》一文所述,Kafka的數(shù)據(jù)復(fù)制是以Partition為單位的。而多個(gè)備份間的數(shù)據(jù)復(fù)制,通過(guò)Follower向Leader拉取數(shù)據(jù)完成。從一這點(diǎn)來(lái)講,Kafka的數(shù)據(jù)復(fù)制方案接近于上文所講的Master-Slave方案。不同的是,Kafka既不是完全的同步復(fù)制,也不是完全的異步復(fù)制,而是基于ISR的動(dòng)態(tài)復(fù)制方案。
給予ISR的動(dòng)態(tài)復(fù)制方案(接近于Master-Slave方案)
ISR,也即In-sync Replica。每個(gè)Partition的Leader都會(huì)維護(hù)這樣一個(gè)列表,該列表中,包含了所有與之同步的Replica(包含Leader自己)。每次數(shù)據(jù)寫入時(shí),只有ISR中的所有Replica都復(fù)制完,Leader才會(huì)將其置為Commit,它才能被Consumer所消費(fèi)。
ISR由Leader維護(hù)
這種方案,與同步復(fù)制非常接近。但不同的是,這個(gè)ISR是由Leader動(dòng)態(tài)維護(hù)的。如果Follower不能緊“跟上”Leader,它將被Leader從ISR中移除,待它又重新“跟上”Leader后,會(huì)被Leader再次加加ISR中。每次改變ISR后,Leader都會(huì)將最新的ISR持久化到Zookeeper中。
至于如何判斷某個(gè)Follower是否“跟上”Leader,不同版本的Kafka的策略稍微有些區(qū)別。
- 對(duì)于0.8.*版本,如果Follower在
replica.lag.time.max.ms
時(shí)間內(nèi)未向Leader發(fā)送Fetch請(qǐng)求(也即數(shù)據(jù)復(fù)制請(qǐng)求),則Leader會(huì)將其從ISR中移除。如果某Follower持續(xù)向Leader發(fā)送Fetch請(qǐng)求,但是它與Leader的數(shù)據(jù)差距在replica.lag.max.messages
以上,也會(huì)被Leader從ISR中移除。 - 從0.9.0.0版本開始,
replica.lag.max.messages
被移除,故Leader不再考慮Follower落后的消息條數(shù)。另外,Leader不僅會(huì)判斷Follower是否在replica.lag.time.max.ms
時(shí)間內(nèi)向其發(fā)送Fetch請(qǐng)求,同時(shí)還會(huì)考慮Follower是否在該時(shí)間內(nèi)與之保持同步。 - 0.10.* 版本的策略與0.9.*版一致
對(duì)于0.8.版本的replica.lag.max.messages
參數(shù),很多讀者曾留言提問(wèn),既然只有ISR中的所有Replica復(fù)制完后的消息才被認(rèn)為Commit,那為何會(huì)出現(xiàn)Follower與Leader差距過(guò)大的情況。原因在于,Leader并不需要等到前一條消息被Commit才接收后一條消息。事實(shí)上,Leader可以按順序接收大量消息,最新的一條消息的Offset被記為High Watermark。而只有被ISR中所有Follower都復(fù)制過(guò)去的消息才會(huì)被Commit,Consumer只能消費(fèi)被Commit的消息。由于Follower的復(fù)制是嚴(yán)格按順序的,所以被Commit的消息之前的消息肯定也已經(jīng)被Commit過(guò)。換句話說(shuō),High Watermark標(biāo)記的是Leader所保存的最新消息的offset,而Commit Offset標(biāo)記的是最新的可被消費(fèi)的(已同步到ISR中的Follower)消息。而Leader對(duì)數(shù)據(jù)的接收與Follower對(duì)數(shù)據(jù)的復(fù)制是異步進(jìn)行的,因此會(huì)出現(xiàn)Commit Offset與High Watermark存在一定差距的情況。0.8.版本中replica.lag.max.messages
限定了Leader允許的該差距的最大值。
Leader接受數(shù)據(jù)和復(fù)制副本數(shù)據(jù)是異步進(jìn)行的,Leader按順序接受完消息,將最新的一條消息的offset(偏移)記為High Watermark,而Follower復(fù)制結(jié)束后,commit后標(biāo)記commit offset,所以當(dāng)這兩者的差距拉大,就會(huì)出現(xiàn)Follower未能跟上Leader。
Kafka基于ISR的數(shù)據(jù)復(fù)制方案原理如下圖所示。
如上圖所示,在第一步中,Leader A總共收到3條消息,故其high watermark為3,但由于ISR中的Follower只同步了第1條消息(m1),故只有m1被Commit,也即只有m1可被Consumer消費(fèi)。此時(shí)Follower B與Leader A的差距是1,而Follower C與Leader A的差距是2,均未超過(guò)默認(rèn)的replica.lag.max.messages
,故得以保留在ISR中。在第二步中,由于舊的Leader A宕機(jī),新的Leader B在replica.lag.time.max.ms
時(shí)間內(nèi)未收到來(lái)自A的Fetch請(qǐng)求,故將A從ISR中移除,此時(shí)ISR={B,C}。同時(shí),由于此時(shí)新的Leader B中只有2條消息,并未包含m3(m3從未被任何Leader所Commit),所以m3無(wú)法被Consumer消費(fèi)。第四步中,F(xiàn)ollower A恢復(fù)正常,它先將宕機(jī)前未Commit的所有消息全部刪除,然后從最后Commit過(guò)的消息的下一條消息開始追趕新的Leader B,直到它“趕上”新的Leader,才被重新加入新的ISR中。
藍(lán)色線 = commit offset
使用ISR方案的原因
- 由于Leader可移除不能及時(shí)與之同步的Follower,故與同步復(fù)制相比可避免最慢的Follower拖慢整體速度,也即ISR提高了系統(tǒng)可用性。
- ISR中的所有Follower都包含了所有Commit過(guò)的消息,而只有Commit過(guò)的消息才會(huì)被Consumer消費(fèi),故從Consumer的角度而言,ISR中的所有Replica都始終處于同步狀態(tài),從而與異步復(fù)制方案相比提高了數(shù)據(jù)一致性。
- ISR可動(dòng)態(tài)調(diào)整,極限情況下,可以只包含Leader,極大提高了可容忍的宕機(jī)的Follower的數(shù)量。與
Majority Quorum
方案相比,容忍相同個(gè)數(shù)的節(jié)點(diǎn)失敗,所要求的總節(jié)點(diǎn)數(shù)少了近一半。
避免最慢的Follower拖慢系統(tǒng)的速度
ISR中的所有Replica都處于同步狀態(tài)
極限狀態(tài)下,ISRz中可以只包含Leader
ISR相關(guān)配置說(shuō)明
- Broker的
min.insync.replicas
參數(shù)指定了Broker所要求的ISR最小長(zhǎng)度,默認(rèn)值為1。也即極限情況下ISR可以只包含Leader。但此時(shí)如果Leader宕機(jī),則該P(yáng)artition不可用,可用性得不到保證。 - 只有被ISR中所有Replica同步的消息才被Commit,但Producer發(fā)布數(shù)據(jù)時(shí),Leader并不需要ISR中的所有Replica同步該數(shù)據(jù)才確認(rèn)收到數(shù)據(jù)。Producer可以通過(guò)
acks
參數(shù)指定最少需要多少個(gè)Replica確認(rèn)收到該消息才視為該消息發(fā)送成功。acks
的默認(rèn)值是1,即Leader收到該消息后立即告訴Producer收到該消息,此時(shí)如果在ISR中的消息復(fù)制完該消息前Leader宕機(jī),那該條消息會(huì)丟失。而如果將該值設(shè)置為0,則Producer發(fā)送完數(shù)據(jù)后,立即認(rèn)為該數(shù)據(jù)發(fā)送成功,不作任何等待,而實(shí)際上該數(shù)據(jù)可能發(fā)送失敗,并且Producer的Retry機(jī)制將不生效。更推薦的做法是,將acks
設(shè)置為all
或者-1
,此時(shí)只有ISR中的所有Replica都收到該數(shù)據(jù)(也即該消息被Commit),Leader才會(huì)告訴Producer該消息發(fā)送成功,從而保證不會(huì)有未知的數(shù)據(jù)丟失。
min.insync.replicas: 1 指定Broker所要求的ISR最小的長(zhǎng)度
acks 指定最少需要多少個(gè)Replica確認(rèn)收到該消息才視為成功
具體實(shí)現(xiàn)層面
高效使用磁盤
順序?qū)懘疟P
根據(jù)《一些場(chǎng)景下順序?qū)懘疟P快于隨機(jī)寫內(nèi)存》所述,將寫磁盤的過(guò)程變?yōu)轫樞驅(qū)懀蓸O大提高對(duì)磁盤的利用率。
Kafka的整個(gè)設(shè)計(jì)中,Partition相當(dāng)于一個(gè)非常長(zhǎng)的數(shù)組,而Broker接收到的所有消息順序?qū)懭脒@個(gè)大數(shù)組中。同時(shí)Consumer通過(guò)Offset順序消費(fèi)這些數(shù)據(jù),并且不刪除已經(jīng)消費(fèi)的數(shù)據(jù),從而避免了隨機(jī)寫磁盤的過(guò)程。
由于磁盤有限,不可能保存所有數(shù)據(jù),實(shí)際上作為消息系統(tǒng)Kafka也沒(méi)必要保存所有數(shù)據(jù),需要?jiǎng)h除舊的數(shù)據(jù)。而這個(gè)刪除過(guò)程,并非通過(guò)使用“讀-寫”模式去修改文件,而是將Partition分為多個(gè)Segment,每個(gè)Segment對(duì)應(yīng)一個(gè)物理文件,通過(guò)刪除整個(gè)文件的方式去刪除Partition內(nèi)的數(shù)據(jù)。這種方式清除舊數(shù)據(jù)的方式,也避免了對(duì)文件的隨機(jī)寫操作。
Kafka順序存寫數(shù)據(jù),故刪除時(shí)刪除對(duì)應(yīng)的Segment(物理文件,disk),避免對(duì)文件的隨機(jī)寫操作。
通過(guò)如下代碼可知,Kafka刪除Segment的方式,是直接刪除Segment對(duì)應(yīng)的整個(gè)log文件和整個(gè)index文件而非刪除文件中的部分內(nèi)容。
/**
* Delete this log segment from the filesystem.
*
* @throws KafkaStorageException if the delete fails.
*/
def delete() {
val deletedLog = log.delete()
val deletedIndex = index.delete()
val deletedTimeIndex = timeIndex.delete()
if(!deletedLog && log.file.exists)
throw new KafkaStorageException("Delete of log " + log.file.getName + " failed.")
if(!deletedIndex && index.file.exists)
throw new KafkaStorageException("Delete of index " + index.file.getName + " failed.")
if(!deletedTimeIndex && timeIndex.file.exists)
throw new KafkaStorageException("Delete of time index " + timeIndex.file.getName + " failed.")
}
充分利用Page Cache(分頁(yè)緩存)
使用Page Cache的好處如下
- I/O Scheduler會(huì)將連續(xù)的小塊寫組裝成大塊的物理寫從而提高性能
- I/O Scheduler會(huì)嘗試將一些寫操作重新按順序排好,從而減少磁盤頭的移動(dòng)時(shí)間
- 充分利用所有空閑內(nèi)存(非JVM內(nèi)存)。如果使用應(yīng)用層Cache(即JVM堆內(nèi)存),會(huì)增加GC負(fù)擔(dān)
- 讀操作可直接在Page Cache內(nèi)進(jìn)行。如果消費(fèi)和生產(chǎn)速度相當(dāng),甚至不需要通過(guò)物理磁盤(直接通過(guò)Page Cache)交換數(shù)據(jù)
- 如果進(jìn)程重啟,JVM內(nèi)的Cache會(huì)失效,但Page Cache仍然可用
小塊寫--->大塊寫
寫操作按順序排好
利用所有空閑內(nèi)存
讀操作可直接在Page Cache內(nèi)進(jìn)行(不是JVM內(nèi)存)
Broker收到數(shù)據(jù)后,寫磁盤時(shí)只是將數(shù)據(jù)寫入Page Cache,并不保證數(shù)據(jù)一定完全寫入磁盤。從這一點(diǎn)看,可能會(huì)造成機(jī)器宕機(jī)時(shí),Page Cache內(nèi)的數(shù)據(jù)未寫入磁盤從而造成數(shù)據(jù)丟失。但是這種丟失只發(fā)生在機(jī)器斷電等造成操作系統(tǒng)不工作的場(chǎng)景,而這種場(chǎng)景完全可以由Kafka層面的Replication機(jī)制去解決。如果為了保證這種情況下數(shù)據(jù)不丟失而強(qiáng)制將Page Cache中的數(shù)據(jù)Flush到磁盤,反而會(huì)降低性能。也正因如此,Kafka雖然提供了flush.messages
和flush.ms
兩個(gè)參數(shù)將Page Cache中的數(shù)據(jù)強(qiáng)制Flush到磁盤,但是Kafka并不建議使用。
斷電會(huì)導(dǎo)致Page Cache數(shù)據(jù)丟失
可設(shè)置采用Flush到磁盤,但影響性能
如果數(shù)據(jù)消費(fèi)速度與生產(chǎn)速度相當(dāng),甚至不需要通過(guò)物理磁盤交換數(shù)據(jù),而是直接通過(guò)Page Cache交換數(shù)據(jù)。同時(shí),F(xiàn)ollower從Leader Fetch數(shù)據(jù)時(shí),也可通過(guò)Page Cache完成。下圖為某Partition的Leader節(jié)點(diǎn)的網(wǎng)絡(luò)/磁盤讀寫信息。
從上圖可以看到,該Broker每秒通過(guò)網(wǎng)絡(luò)從Producer接收約35MB數(shù)據(jù),雖然有Follower從該Broker Fetch數(shù)據(jù),但是該Broker基本無(wú)讀磁盤。這是因?yàn)樵揃roker直接從Page Cache中將數(shù)據(jù)取出返回給了Follower。
支持多Disk Drive
Broker的log.dirs
配置項(xiàng),允許配置多個(gè)文件夾。如果機(jī)器上有多個(gè)Disk Drive,可將不同的Disk掛載到不同的目錄,然后將這些目錄都配置到log.dirs
里。Kafka會(huì)盡可能將不同的Partition分配到不同的目錄,也即不同的Disk上,從而充分利用了多Disk的優(yōu)勢(shì)。
零拷貝
Kafka中存在大量的網(wǎng)絡(luò)數(shù)據(jù)持久化到磁盤(Producer到Broker)和磁盤文件通過(guò)網(wǎng)絡(luò)發(fā)送(Broker到Consumer)的過(guò)程。這一過(guò)程的性能直接影響Kafka的整體吞吐量。對(duì)比傳統(tǒng)模式的拷貝來(lái)看看kafka如何實(shí)現(xiàn)零拷貝
傳統(tǒng)模式下的四次拷貝與四次上下文切換
以將磁盤文件通過(guò)網(wǎng)絡(luò)發(fā)送為例。傳統(tǒng)模式下,一般使用如下偽代碼所示的方法先將文件數(shù)據(jù)讀入內(nèi)存,然后通過(guò)Socket將內(nèi)存中的數(shù)據(jù)發(fā)送出去。
buffer = File.read
Socket.send(buffer)
這一過(guò)程實(shí)際上發(fā)生了四次數(shù)據(jù)拷貝。首先通過(guò)系統(tǒng)調(diào)用將文件數(shù)據(jù)讀入到內(nèi)核態(tài)Buffer(DMA拷貝),然后應(yīng)用程序將內(nèi)存態(tài)Buffer數(shù)據(jù)讀入到用戶態(tài)Buffer(CPU拷貝),接著用戶程序通過(guò)Socket發(fā)送數(shù)據(jù)時(shí)將用戶態(tài)Buffer數(shù)據(jù)拷貝到內(nèi)核態(tài)Buffer(CPU拷貝),最后通過(guò)DMA拷貝將數(shù)據(jù)拷貝到NIC Buffer(網(wǎng)卡緩沖)。同時(shí),還伴隨著四次上下文切換,如下圖所示。
sendfile和transferTo實(shí)現(xiàn)零拷貝
Linux 2.4+內(nèi)核通過(guò)sendfile
系統(tǒng)調(diào)用,提供了零拷貝。數(shù)據(jù)通過(guò)DMA拷貝到內(nèi)核態(tài)Buffer后,直接通過(guò)DMA(Direct Memory Access,直接內(nèi)存存取)拷貝到NIC Buffer,無(wú)需CPU拷貝。這也是零拷貝這一說(shuō)法的來(lái)源。除了減少數(shù)據(jù)拷貝外,因?yàn)檎麄€(gè)讀文件-網(wǎng)絡(luò)發(fā)送由一個(gè)sendfile
調(diào)用完成,整個(gè)過(guò)程只有兩次上下文切換,因此大大提高了性能。零拷貝過(guò)程如下圖所示。
從具體實(shí)現(xiàn)來(lái)看,Kafka的數(shù)據(jù)傳輸通過(guò)TransportLayer來(lái)完成,其子類PlaintextTransportLayer
通過(guò)Java NIO的FileChannel的transferTo
和transferFrom
方法實(shí)現(xiàn)零拷貝,如下所示。
@Override
public long transferFrom(FileChannel fileChannel, long position, long count) throws IOException {
return fileChannel.transferTo(position, count, socketChannel);
}
注: transferTo
和transferFrom
并不保證一定能使用零拷貝。實(shí)際上是否能使用零拷貝與操作系統(tǒng)相關(guān),如果操作系統(tǒng)提供sendfile
這樣的零拷貝系統(tǒng)調(diào)用,則這兩個(gè)方法會(huì)通過(guò)這樣的系統(tǒng)調(diào)用充分利用零拷貝的優(yōu)勢(shì),否則并不能通過(guò)這兩個(gè)方法本身實(shí)現(xiàn)零拷貝。
減少網(wǎng)絡(luò)開銷
批處理
批處理是一種常用的用于提高I/O性能的方式。對(duì)Kafka而言,批處理既減少了網(wǎng)絡(luò)傳輸?shù)腛verhead(天花板),又提高了寫磁盤的效率。
Kafka 0.8.1及以前的Producer區(qū)分同步Producer和異步Producer。同步Producer的send方法主要分兩種形式。一種是接受一個(gè)KeyedMessage作為參數(shù),一次發(fā)送一條消息。另一種是接受一批KeyedMessage作為參數(shù),一次性發(fā)送多條消息。而對(duì)于異步發(fā)送而言,無(wú)論是使用哪個(gè)send方法,實(shí)現(xiàn)上都不會(huì)立即將消息發(fā)送給Broker,而是先存到內(nèi)部的隊(duì)列中,直到消息條數(shù)達(dá)到閾值或者達(dá)到指定的Timeout才真正的將消息發(fā)送出去,從而實(shí)現(xiàn)了消息的批量發(fā)送。
Kafka 0.8.2開始支持新的Producer API,將同步Producer和異步Producer結(jié)合。雖然從send接口來(lái)看,一次只能發(fā)送一個(gè)ProducerRecord,而不能像之前版本的send方法一樣接受消息列表,但是send方法并非立即將消息發(fā)送出去,而是通過(guò)batch.size
和linger.ms
控制實(shí)際發(fā)送頻率,從而實(shí)現(xiàn)批量發(fā)送。
由于每次網(wǎng)絡(luò)傳輸,除了傳輸消息本身以外,還要傳輸非常多的網(wǎng)絡(luò)協(xié)議本身的一些內(nèi)容(稱為Overhead),所以將多條消息合并到一起傳輸,可有效減少網(wǎng)絡(luò)傳輸?shù)腛verhead,進(jìn)而提高了傳輸效率。
從零拷貝章節(jié)的圖中可以看到,雖然Broker持續(xù)從網(wǎng)絡(luò)接收數(shù)據(jù),但是寫磁盤并非每秒都在發(fā)生,而是間隔一段時(shí)間寫一次磁盤,并且每次寫磁盤的數(shù)據(jù)量都非常大(最高達(dá)到718MB/S)。
數(shù)據(jù)壓縮降低網(wǎng)絡(luò)負(fù)載
Kafka從0.7開始,即支持將數(shù)據(jù)壓縮后再傳輸給Broker。除了可以將每條消息單獨(dú)壓縮然后傳輸外,Kafka還支持在批量發(fā)送時(shí),將整個(gè)Batch的消息一起壓縮后傳輸。數(shù)據(jù)壓縮的一個(gè)基本原理是,重復(fù)數(shù)據(jù)越多壓縮效果越好。因此將整個(gè)Batch的數(shù)據(jù)一起壓縮能更大幅度減小數(shù)據(jù)量,從而更大程度提高網(wǎng)絡(luò)傳輸效率。
Broker接收消息后,并不直接解壓縮,而是直接將消息以壓縮后的形式持久化到磁盤。Consumer Fetch到數(shù)據(jù)后再解壓縮。因此Kafka的壓縮不僅減少了Producer到Broker的網(wǎng)絡(luò)傳輸負(fù)載,同時(shí)也降低了Broker磁盤操作的負(fù)載,也降低了Consumer與Broker間的網(wǎng)絡(luò)傳輸量,從而極大得提高了傳輸效率,提高了吞吐量。
1 單條/Batch壓縮---傳輸----持久化到磁盤---Consumer Fetch到數(shù)據(jù)后再解壓縮
2 降低了Broker磁盤操作的負(fù)擔(dān),降低了Consumer與Broker間的網(wǎng)絡(luò)傳輸量 提高了傳輸效率 提高了 吞吐率
高效的序列化方式
Kafka消息的Key和Payload(或者說(shuō)Value)的類型可自定義,只需同時(shí)提供相應(yīng)的序列化器和反序列化器即可。因此用戶可以通過(guò)使用快速且緊湊的序列化-反序列化方式(如Avro,Protocal Buffer)來(lái)減少實(shí)際網(wǎng)絡(luò)傳輸和磁盤存儲(chǔ)的數(shù)據(jù)規(guī)模,從而提高吞吐率。這里要注意,如果使用的序列化方法太慢,即使壓縮比非常高,最終的效率也不一定高。
Kafka系列文章
Kafka設(shè)計(jì)解析(一)- Kafka簡(jiǎn)介及架構(gòu)介紹
Kafka設(shè)計(jì)解析(二)- Kafka High Availability (上)
Kafka設(shè)計(jì)解析(三)- Kafka High Availability (下)
Kafka設(shè)計(jì)解析(四)- Kafka Consumer設(shè)計(jì)解析
Kafka設(shè)計(jì)解析(五)- Kafka性能測(cè)試方法及Benchmark報(bào)告
Kafka設(shè)計(jì)解析(六)- Kafka高性能架構(gòu)之道
Kafka設(shè)計(jì)解析(七)- Kafka Stream