前言
本文簡單介紹了Apache Kafka服務(wù)端的一些設(shè)計(jì)嚼蚀,因?yàn)闆]有詳細(xì)的介紹一些基礎(chǔ)概念和適合有對(duì)Kafka有一定了解的同學(xué)們閱讀链快。如果想深入了解Kafka凉敲,推薦閱讀Kafka官方文檔和源代碼着倾。
Kafka被設(shè)計(jì)出來的目標(biāo)是做為海量實(shí)時(shí)數(shù)據(jù)傳輸?shù)姆植际綌?shù)據(jù)流平臺(tái)拧簸,主要用來傳輸和聚合日志數(shù)據(jù)劲绪、追蹤網(wǎng)站活動(dòng)、傳輸監(jiān)控?cái)?shù)據(jù)和作為消息隊(duì)列等盆赤。為了滿足這些功能珠叔,Kafka需要具有如下特性:
- 高吞吐
作為海量數(shù)據(jù)的傳輸平臺(tái),Kafka需要極大的吞吐量來保證海量數(shù)據(jù)的傳輸弟劲。 - 低延遲
實(shí)時(shí)數(shù)據(jù)處理最重要的指標(biāo)之一就是延遲,需要低延遲來保證它作為消息隊(duì)列時(shí)的性能姥芥。 - 高可用
為了避免服務(wù)器故障兔乞、JVM崩潰等問題帶來的數(shù)據(jù)丟失情況,需要具備較好的容錯(cuò)性凉唐。 - 多次消費(fèi)
據(jù)統(tǒng)計(jì)庸追,在LinkedIn公司內(nèi)Kafka中的每條消息平均要被消費(fèi)5次以上。為了支持海量數(shù)據(jù)的可重復(fù)消費(fèi)台囱,Kafka需要很大的容量淡溯。
為了實(shí)現(xiàn)這些特性,Kafka使用了相當(dāng)多的”黑科技“簿训。下面讓我們一一解讀一下咱娶。
一、順序?qū)懭?/h2>
可以說强品,Kafka是重度依賴文件系統(tǒng)的膘侮,它會(huì)把所有的數(shù)據(jù)寫入到硬盤上〉拈唬可是按照我們平時(shí)的理解琼了,對(duì)硬盤的讀寫不是很慢么?其實(shí)還真不一定夫晌。要理解影響硬盤讀寫速度的因素雕薪,首先我們要了解硬盤的結(jié)構(gòu)昧诱。
硬盤結(jié)構(gòu)
首先,我們來看一張硬盤的結(jié)構(gòu)圖:
如上圖所示盏档,磁盤主要由磁盤盤片、傳動(dòng)手臂纲熏、讀寫磁頭和主軸組成妆丘。為了更好的利用盤片資源,每張盤片的兩面都可以記錄信息局劲,所以每張盤片會(huì)對(duì)應(yīng)上下兩個(gè)磁頭讀寫數(shù)據(jù)勺拣。由于單張盤片能存儲(chǔ)的數(shù)據(jù)量有限,所以一般磁盤都有多個(gè)盤片鱼填。盤面被分為許多扇形區(qū)域药有,稱為扇區(qū)。圍繞著盤面中心的不同半徑的同心圓被稱為磁道苹丸。不同盤片間相同半徑的磁道組成的圓柱體稱為柱面愤惰。如下圖所示:
硬盤的讀寫過程
磁盤中的數(shù)據(jù)全部存儲(chǔ)在磁盤的盤片上面赘理,讀取數(shù)據(jù)時(shí)轉(zhuǎn)動(dòng)主軸到指定位置宦言,傳動(dòng)手臂進(jìn)行伸展,最后由讀寫磁頭完成實(shí)際的讀寫操作商模。那么為什么大家會(huì)覺得硬盤的讀寫很慢呢奠旺?因?yàn)橐淮斡脖PIO需要以下三個(gè)步驟:
- 尋道
磁盤要想讀寫數(shù)據(jù),首先要找到正確的磁道施流。讀寫磁頭移動(dòng)到需要被讀寫的磁道上的時(shí)間被稱為尋道時(shí)間响疚。 - 旋轉(zhuǎn)
旋轉(zhuǎn)、跳躍瞪醋,磁盤閉著眼忿晕。要想讀寫數(shù)據(jù),光找到正確的磁道還不夠银受,硬盤要通過主軸的旋轉(zhuǎn)找到正確的扇區(qū)践盼。磁盤通過旋轉(zhuǎn)找到正確扇區(qū)的時(shí)間被稱為旋轉(zhuǎn)延遲。我們平時(shí)經(jīng)常聽到的這種磁盤7200轉(zhuǎn)宾巍,那種磁盤15000轉(zhuǎn)宏侍,指的就是磁盤的轉(zhuǎn)速(每分鐘能轉(zhuǎn)多少圈)。轉(zhuǎn)的越快蜀漆,旋轉(zhuǎn)延遲越短谅河,IO速度越快。 - 數(shù)據(jù)傳輸
到這里才是數(shù)據(jù)才能真正進(jìn)行讀寫。數(shù)據(jù)傳輸?shù)乃俣群芸毂了#靡恍┑拇疟P通常能達(dá)到百兆甚至幾百兆每秒吐限。
看到這里我們知道了,一次完整的磁盤IO時(shí)間實(shí)際上為:
尋道時(shí)間 + 旋轉(zhuǎn)延遲 + 數(shù)據(jù)傳輸時(shí)間
一般的磁盤操作褂始,絕大部分的時(shí)間花在了前兩個(gè)步驟上诸典。也就是說,對(duì)磁盤進(jìn)行順序讀寫很快(因?yàn)榛静挥眠M(jìn)行前兩個(gè)步驟)崎苗,而隨機(jī)讀寫就很慢了狐粱。根據(jù)Kafka官方給出的數(shù)據(jù),在7200rpm/s的SATA RAID-5磁盤陣列上進(jìn)行順序?qū)懭胨俣冗_(dá)到600MB/sec胆数,而隨機(jī)寫入大概只有100KB/sec肌蜻,相差了6000倍!而Kafka正是使用了順序讀寫必尼,才能獲得如此高的性能蒋搜。
二、Page Cache與Memory Map
如果僅僅使用順序讀寫判莉,那么Kafka也不會(huì)有現(xiàn)在這么好的性能豆挽。事實(shí)上,Kafka充分利用了現(xiàn)代操作系統(tǒng)中的文件緩存系統(tǒng)券盅。
在現(xiàn)代操作系統(tǒng)中帮哈,為了彌補(bǔ)硬盤寫入的速度的不足,系統(tǒng)越來越激進(jìn)的使用內(nèi)存作為文件系統(tǒng)的緩存锰镀,甚至?xí)褂盟锌臻e的內(nèi)存作為磁盤緩存(即page cache)但汞。Page cache提供了預(yù)讀和回寫功能。簡單來說互站,預(yù)讀就是當(dāng)順序讀取文件內(nèi)容時(shí),page cache會(huì)提前將當(dāng)前讀取頁面之后的幾個(gè)頁面也加載到page cache當(dāng)中僵缺,這樣程序相當(dāng)于直接讀取cache中的內(nèi)容胡桃,而不必直接與磁盤交互】某保回寫就是當(dāng)磁盤進(jìn)行寫入時(shí)翠胰,會(huì)寫入到page cache當(dāng)中,由操作系統(tǒng)在恰當(dāng)?shù)臅r(shí)候再寫入磁盤自脯。很多人不知道的是之景,所有我們的常規(guī)IO操作全部都要經(jīng)過page cache,這個(gè)特性是在操作系統(tǒng)層面決定的膏潮,很難取消掉锻狗。
有了page cache,一切看起來都很美好∏峒停可實(shí)際情況是油额,這里面仍然存在一些問題。首先刻帚,當(dāng)我們使用常規(guī)方式讀取文件內(nèi)容時(shí)潦嘶,系統(tǒng)內(nèi)核必須將page cache中的文件內(nèi)容復(fù)制到user buffer中。這不僅浪費(fèi)了CPU時(shí)間崇众,而且還將導(dǎo)致系統(tǒng)的物理內(nèi)存中出現(xiàn)兩份數(shù)據(jù)掂僵,浪費(fèi)了物理內(nèi)存空間。另外顷歌,由于Kafka是構(gòu)建在JVM上的锰蓬,對(duì)于JVM比較了解的同學(xué)都會(huì)知道這樣兩條規(guī)律:
- JVM中對(duì)象消耗的內(nèi)存非常大,經(jīng)常會(huì)達(dá)到實(shí)際數(shù)據(jù)的兩倍甚至更多衙吩。
- 隨著數(shù)據(jù)量的增長互妓,JVM的垃圾回收將會(huì)越來越慢,甚至不可忍受坤塞。
所以基于以上考慮冯勉,Kafka并沒有使用常規(guī)的磁盤操作,而是使用了Memory-mapped files摹芙。當(dāng)使用Memory-mapped files時(shí)灼狰,系統(tǒng)內(nèi)核會(huì)將程序的virtual memory直接映射到page cache,使我們可以把文件數(shù)據(jù)當(dāng)做內(nèi)存數(shù)據(jù)一樣操作浮禾。這樣不僅避免了數(shù)據(jù)在內(nèi)核空間和用戶空間之間復(fù)制交胚,也避免了使用java對(duì)象帶來的一些問題,從而極大提高了Kafka讀寫效率盈电。在java的NIO中提供了使用memory-mapped files的api蝴簇,即MappedByteBuffer(繼承自ByteBuffer),感興趣的同學(xué)可以去深入研究匆帚。關(guān)于page cache和memory-mapped files熬词,可以閱讀這篇博客:Page Cache, the Affair Between Memory and Files。
三吸重、Zero-Copy
按照前兩節(jié)所講述的互拾,我們使用順序讀寫最大化磁盤性能;使用page cache和高效的memory-mapped files嚎幸,避免對(duì)磁盤進(jìn)行直接操作颜矿。按道理來講,性能上應(yīng)該非常出色了嫉晶。但是盡管如此骑疆,還是有兩個(gè)問題影響著系統(tǒng)的性能:頻繁的小數(shù)據(jù)量網(wǎng)絡(luò)IO操作和過多的字節(jié)拷貝田篇。
為了避免頻繁的網(wǎng)絡(luò)往返帶來的性能開銷,Kafka將消息組合在一起形成一個(gè)“消息集”封断。使用這種方式可以將消息分批發(fā)送斯辰,而不是單條發(fā)送,從而分?jǐn)偭司W(wǎng)絡(luò)往返的開銷坡疼。當(dāng)數(shù)據(jù)量巨大的時(shí)候彬呻,這種方式可以極大的提升網(wǎng)絡(luò)IO的性能。Kafka的生產(chǎn)者和消費(fèi)者都是采用這種方式向Kafka發(fā)送數(shù)據(jù)和從Kafka拉取數(shù)據(jù)的柄瑰。
接下來我們來介紹一下zero-copy闸氮。Kafka使用了Linux的系統(tǒng)調(diào)用sendfile來發(fā)送系統(tǒng)中的消息,為了了解sendfile系統(tǒng)調(diào)用帶來的優(yōu)勢(shì)教沾,我們先來了解一下通過socket發(fā)送數(shù)據(jù)的傳統(tǒng)方式:
由上圖我們可以看到,如果要將磁盤上的數(shù)據(jù)發(fā)送出去授翻,需要經(jīng)過以下四個(gè)步驟:
- 操作系統(tǒng)從磁盤讀取數(shù)據(jù)或悲,并寫入到內(nèi)核空間的page cache中。
- 應(yīng)用程序從內(nèi)核空間讀取數(shù)據(jù)堪唐,并復(fù)制到用戶空間中巡语。
- 應(yīng)用程序?qū)⒂脩艨臻g中的數(shù)據(jù)寫回到內(nèi)核空間的socket緩沖中去。
- 操作系統(tǒng)將socket緩沖中的數(shù)據(jù)復(fù)制到網(wǎng)卡緩沖中淮菠,并經(jīng)過網(wǎng)卡發(fā)送出去男公。
可以看到這種傳統(tǒng)發(fā)送數(shù)據(jù)的方式經(jīng)過了四次數(shù)據(jù)復(fù)制和兩次系統(tǒng)調(diào)用,效率很差合陵。那么使用sendfile系統(tǒng)調(diào)用后是什么情況呢枢赔?
從圖中我們可以看到拥知,使用sendfile可以直接從page cache復(fù)制數(shù)據(jù)到網(wǎng)卡緩沖踏拜,避免了不必要的系統(tǒng)調(diào)用和數(shù)據(jù)復(fù)制,非常高效低剔。
由于Kafka的一個(gè)topic往往有多個(gè)消費(fèi)者組在消費(fèi)速梗,所以采用zero-copy的方式,讓數(shù)據(jù)只從磁盤讀取到page cache一次户侥,就可以服務(wù)所有的消費(fèi)了。通過使用page cache和sendfile峦嗤,在消費(fèi)者消費(fèi)Kafka中數(shù)據(jù)的時(shí)候蕊唐,磁盤幾乎沒有任何讀取活動(dòng),全部的數(shù)據(jù)都來自于page cache中烁设。
在java中替梨,java.nio.channels.FileChannel類提供了transferTo()方法來實(shí)現(xiàn)zero copy(當(dāng)然還取決與操作系統(tǒng)钓试,在Unix和多數(shù)Linux上transferTo()方法會(huì)進(jìn)行sendfile系統(tǒng)調(diào)用)。
四副瀑、端到端批量壓縮
很多時(shí)候弓熏,數(shù)據(jù)傳輸?shù)男阅芷款i不在于CPU或硬盤,而在于網(wǎng)絡(luò)帶寬糠睡。這種情況在遠(yuǎn)距離的公網(wǎng)傳輸中最為常見挽鞠。為了解決這個(gè)問題,Kafka提供了端到端的批量壓縮功能狈孔。雖然用戶也可以對(duì)每條消息自行壓縮信认,但是一些數(shù)據(jù)格式可能導(dǎo)致單條壓縮的壓縮比較低。舉例來說均抽,在一批JSON數(shù)據(jù)中嫁赏,字段名稱其實(shí)是重復(fù)的,單條壓縮會(huì)造成很多冗余油挥。
而Kafka把一批消息抽象為“消息集”(上節(jié)講到過)潦蝇,producer對(duì)數(shù)據(jù)集進(jìn)行壓縮,這些數(shù)據(jù)將會(huì)以被壓縮的格式傳輸?shù)椒?wù)器并寫入到數(shù)據(jù)日志中深寥,只有當(dāng)消費(fèi)者讀取這些數(shù)據(jù)后它們才會(huì)被解壓縮攘乒。Kafka目前支持GZIP,Snappy和LZ4壓縮方式翩迈。
五持灰、ISR
Kafka使用ISR機(jī)制來保證系統(tǒng)的高可用。在創(chuàng)建topic時(shí)负饲,我們可以通過設(shè)置replication-factor參數(shù)來控制topic的復(fù)制因子(之后通過Kafka提供的工具也可以動(dòng)態(tài)改變這個(gè)參數(shù))堤魁。比如在下面創(chuàng)建topic的語句中:
bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 3 --partitions 5 --topic test
我們?cè)O(shè)置了 replication-factor 為3,即有三個(gè)副本返十。副本的作用就是當(dāng)集群中的某個(gè)服務(wù)器發(fā)生故障時(shí)妥泉,系統(tǒng)可以自動(dòng)使用其他服務(wù)器上的副本提供服務(wù),不會(huì)影響到消息的生產(chǎn)和消費(fèi)洞坑。
副本的單位是partition盲链。每個(gè)partition會(huì)有一個(gè)leader,零或多個(gè)follower迟杂。所有l(wèi)eader和follower的數(shù)量加在一起就是replication-factor參數(shù)的值刽沾。比如上面設(shè)置了replication-factor為3,那么這個(gè)topic中的每個(gè)partition就有1個(gè)leader和2個(gè)follower排拷。在對(duì)topic的partition進(jìn)行讀寫時(shí)侧漓,所有的讀寫操作都會(huì)去直接請(qǐng)求leader,follower只是被動(dòng)的去同步leader中的消息监氢。而follower中的消息布蔗,不論是消息的順序還是offset全部與leader相同藤违。當(dāng)然,由于消息先被寫入leader纵揍,follower再去拉取數(shù)據(jù)顿乒,所以同步上會(huì)存在很小一段時(shí)間的延遲。
與一般的分布式系統(tǒng)不同泽谨,Kafka沒有使用“alive”或者“failed”來標(biāo)志副本的存活情況璧榄,而是使用了一個(gè)新的概念:“in-sync”。所有在“in-sync”狀態(tài)的replication(副本)構(gòu)成了這個(gè)partition的“同步副本隊(duì)列”隔盛,即ISR犹菱。那么Kafka如何判斷一個(gè)replication是否在“in-sync”狀態(tài)下呢?
- 副本所在的節(jié)點(diǎn)必須持有 Zookeeper的session吮炕。
- 副本復(fù)制leader上寫入消息的位置不能“落后太多”腊脱。
如果違反了其中任意一條,那么這個(gè)副本會(huì)被暫時(shí)移出ISR隊(duì)列龙亲,當(dāng)它重新滿足這兩條要求時(shí)陕凹,又會(huì)被加入進(jìn)來。當(dāng)然鳄炉,如果leader掛掉杜耙,那么會(huì)有一個(gè)follower被選舉成為新的leader,為partition的讀寫提供服務(wù)拂盯。使用命令:
bin/kafka-topics.sh --describe --zookeeper localhost:2181 --topic my-topic
可以看到此topic下每個(gè)partition的replicas和ISR情況佑女。
總結(jié)
作為目前最流行的分布式消息系統(tǒng),Apache Kafka的很多設(shè)計(jì)都非常精妙谈竿,值得我們學(xué)習(xí)和借鑒团驱。由于篇幅所限,本文只是簡單的列舉了一些Kafka服務(wù)端設(shè)計(jì)中的主要內(nèi)容空凸,還有許多其他的內(nèi)容沒有寫出來嚎花,而這些設(shè)計(jì)的具體代碼實(shí)現(xiàn)也遠(yuǎn)比本文中這些三言兩句的復(fù)述要復(fù)雜的多。學(xué)習(xí)任何開源項(xiàng)目最好的途徑就是官方文檔與源代碼呀洲,歡迎各位感興趣的同學(xué)去深入挖掘和研究紊选。