kafka從入門到關門

閒不住的程式設計師發表於2020-10-31

kafka簡介

Kafka是一個分散式的基於釋出/訂閱模式的訊息佇列,主要應用於大資料實時處理領域。

場景比較

傳統同步方式就是按流程一步一步進行,因為傳送簡訊這個操作沒有那麼高的實時性,如果都在傳送簡訊後才提示頁面註冊成功,如果人數很多的情況下,就可能有人需要一直等待,那麼這種是不合理的。採用非同步的方式,寫入資料庫成功就提示使用者成功,簡訊通知由後續程式通知,不會造成使用者等待。
在這裡插入圖片描述

Kafka基礎架構

在這裡插入圖片描述
kafka叢集是依賴於zookeeper的,如對於zk不熟悉的同學可以參考,

此圖中的術語檢視 術語介紹 章節。

環境搭建以及目錄結構

本人是在windows下搭建的三個kafka節點,具體linux下安裝,後續文章將會更新,本次以介紹為主。
在這裡插入圖片描述

  • bin目錄存放可執行指令碼,包含windows的bat和linux的shell
  • config 配置檔案,包含生產者和消費者以及一些其他配置
  • libs 一些java的庫
  • log kafka真正的資料儲存
  • logs 系統執行的一些日誌
  • site-docs html格式的一些文件

log目錄詳解
在這裡插入圖片描述

  • 紅色部分是系統自己維護的offset,在低版本之前這些是儲存在zookeeper(以下簡稱zk)中,但是zk不適合做頻繁的讀寫功能,因此在高版本的kafka中將維護的offset儲存在kafka自身目錄下。
  • 黑色部分是使用者自定義的topic,test-0 代表的是test的第一個分割槽,同理test-1,…test-n ,分割槽個數可以自定義。
  • 紫色部分是系統中一些checkpoint的日誌檔案,便於恢復資料使用。

術語

  • broker(代理)也稱作節點,例如我們搭建的偽分散式的一個資料夾就是一個節點
    在這裡插入圖片描述
  • topic 主題
    傳送到kafka的訊息有千萬條,如果沒有型別標識這些訊息,那麼我們將無從快速的查詢到需要消費的訊息,因此這裡的topic也就是訊息對應的類別。
  • 分割槽(Partition)
    物理上的一個概念,近似的理解為一個資料夾,為了實現擴充套件性,一個非常大的topic可以分佈到多個broker(即伺服器)上,一個topic可以分為多個partition,每個partition是一個有序的佇列。提高了併發性。
    • leader
      每個分割槽多個副本的“主”,生產者傳送資料的物件,以及消費者消費資料的物件都是leader。
    • follower
      每個分割槽多個副本中的“從”,實時從leader中同步資料,保持和leader資料的同步。leader發生故障時,某個follower會成為新的follower。
  • Producer
    生成者,生產訊息到kafka佇列中。
  • 消費者
    訊息消費者,向Kafka broker讀取訊息的客戶端
  • Consumer Group
    每個Consumer屬於一個特定的Consumer Group,每一個分割槽最多有同一個組裡面的一個消費者對應,可能比較繞嘴,後面會詳細介紹。
  • 副本(replica)
    每個分割槽的副本,分佈在不同的 Broker 上, 為保證叢集中的某個節點發生故障時,該節點上的partition資料不丟失,且kafka仍然能夠繼續工作,kafka提供了副本機制,一個topic的每個分割槽都有若干個副本,一個leader和若干個follower。
    對應目錄就是 我們在第一個節點下存在分割槽檔案,
    在這裡插入圖片描述
    那麼第二臺上也存在分割槽檔案,
    在這裡插入圖片描述

分割槽原則

(1)指明 partition 的情況下,直接將指明的值直接作為 partiton 值;
(2)沒有指明 partition 值但有 key 的情況下,將 key 的 hash 值與 topic 的 partition 數進行取餘得到 partition 值;
(3)既沒有 partition 值又沒有 key 值的情況下,第一次呼叫時隨機生成一個整數(後面每次呼叫在這個整數上自增),將這個值與 topic 可用的 partition 總數取餘得到 partition 值,也就是常說的 round-robin 演算法。

Kafka命令列操作

以下操作都是基於window操作的,linux版本的只需要把執行的bat換成sh即可。

  • 啟動zk叢集
D:\study\kafka\apache-zookeeper-3.5.7-bin\bin\zkServer
D:\study\kafka\apache-zookeeper-3.5.7-bin-2\bin\zkServer
D:\study\kafka\apache-zookeeper-3.5.7-bin-3\bin\zkServer
  • 啟動kafka 叢集
@echo start kafka
start /D "D:\study\kafka\kafka_2.11-2.2.0\bin\windows" kafka-server-start.bat D:\study\kafka\kafka_2.11-2.2.0\config\server.properties
start /D "D:\study\kafka\kafka_2.11-2.2.0-2\bin\windows" kafka-server-start.bat D:\study\kafka\kafka_2.11-2.2.0-2\config\server.properties
start /D "D:\study\kafka\kafka_2.11-2.2.0-3\bin\windows" kafka-server-start.bat D:\study\kafka\kafka_2.11-2.2.0-3\config\server.properties
  • cmd下輸入 jps檢視是否啟動成功
    在這裡插入圖片描述
  • 建立topic
 kafka-topics.bat --create --zookeeper 127.0.0.1:2181 --replication-factor 1 --partitions 1 --topic test
  • 檢視topic
D:\study\kafka\kafka_2.11-2.2.0-3\bin\windows\kafka-topics.bat --list --zookeeper localhost:2181
  • 生產訊息
D:\study\kafka\kafka_2.11-2.2.0-3\bin\windows\kafka-console-producer.bat --broker-list localhost:9092 --topic test
  • 消費訊息
D:\study\kafka\kafka_2.11-2.2.0-3\bin\windows\kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic test2 --from-beginning
  • 獲取某個主題最新的offset
kafka-run-class.bat kafka.tools.GetOffsetShell --broker-list localhost:9092 --topic test2
  • 檢視kafka日誌檔案內容即生產者生產的訊息(初學者常用)
D:\study\kafka\kafka_2.11-2.2.0-3\bin\windows\kafka-run-class.bat kafka.tools.DumpLogSegments --files D:\study\kafka\kafka_2.11-2.2.0\log\test2-0\00000000000000000000.log  --print-data-log

kafka檔案儲存機制

在這裡插入圖片描述
由於生產者生產的訊息會不斷追加到log檔案末尾,為防止log檔案過大導致資料定位效率低下,Kafka採取了分片和索引機制,將每個partition分為多個segment。每個segment對應兩個檔案——“.index”檔案和“.log”檔案。這些檔案位於一個資料夾下,該資料夾的命名規則為:topic名稱+分割槽序號 ,與上面目錄詳情中可以對應上了。
兩者之間儲存關係如下圖:
在這裡插入圖片描述
index 顧名思義儲存的就是索引位置,log儲存的具體內容。
索引中的後設資料指向日誌中物理偏移量。

傳輸中可靠性的保證

為保證producer傳送的資料,能可靠的傳送到指定的topic,topic的每個partition收到producer傳送的資料後,都需要向producer傳送ack(acknowledgement確認收到),如果producer收到ack,就會進行下一輪的傳送,否則重新傳送資料。
在這裡插入圖片描述
那麼問題來了,啥時候傳送ack,或者說達到了什麼條件才會傳送ack?

  • 副本複製策略

前文中講到了分割槽是由多個副本的,也就是副本之間也需要同步資料,那麼副本之間是如何同步資料的呢?又是如何保證資料一致性的呢?
其實想想一個複製多個無法就兩種情況:

  • 有一個老大複製完了,在複製給其他人。
  • 等所有的小弟全部複製 完成。

那很明顯了kafka中有以下兩種方案

方案優點缺點
半數以上完成同步,就傳送ack延遲低選舉新的leader時,容忍n臺節點的故障,需要2n+1個副本
全部完成同步,才傳送ack選舉新的leader時,容忍n臺節點的故障,需要n+1個副本延遲高

Kafka選擇了第二種方案,原因如下:
1.同樣為了容忍n臺節點的故障,第一種方案需要2n+1個副本,而第二種方案只需要n+1個副本,而Kafka的每個分割槽都有大量的資料,第一種方案會造成大量資料的冗餘。
2.雖然第二種方案的網路延遲會比較高,但網路延遲對Kafka的影響較小。
但是採取第二種方式也是有很大弊端的,試想以下場景:

leader收到生產者生產的訊息,所有的follower開始同步資料了,但是有一個follower中間因為某種故障,斷開連線了,遲遲同步不了,那麼leader就要一直等待。
那ack怎麼發出來呢?
Leader維護了一個in-sync-replica set 簡稱 ISR,意思為和leader保持同步的follower同步的集合。當ISR中的follower同步完成資料之後,
leader將會給follower傳送ack,如果出現這種遲遲不響應的情況,則該follower將會被提出ISR,改時間由replica.lag.time.max.ms引數設定。同理當Leader故障時則會產生新的Leader,這個選舉過程在Zk中完成的。選區Leader的過程將在下章 zk與kafka的關係中詳細講解。
  • ack策略
    上面講到了,要等所有的ISR中的副本全部相應成功才會相應給生產者此次生產已經完整的寫入佇列。
    那這裡的ack說的是leader和follower之間的相應,並不是leader和生產者之間的ack。
    對於某些不太重要的資料,對資料的可靠性要求不是很高,能夠容忍資料的少量丟失,所以沒必要等ISR中的follower全部接收成功
    ack的三種取值對應關係:
    0:producer不等待broker的ack,這一操作提供了一個最低的延遲,broker一接收到還沒有寫入磁碟就已經返回,當broker故障時有可能丟失資料;
    1:producer等待broker的ack,partition的leader落盤成功後返回ack,如果在follower同步成功之前leader故障,那麼將會丟失資料;
    -1(all):producer等待broker的ack,partition的leader和follower全部落盤成功後才 返回ack。但是如果在follower同步完成後,broker傳送ack之前,leader發生故障,那麼會造成資料重複。

消費者的消費方式

生活中的例子:
你去參加高考,查成績的時候,兩種方式,一種是 你主動打電話或者登陸網站去查詢 另外一種是被動模式,比較牛逼,很有資訊,我非等老師查完成績然後打電話給我說,你考的這麼爛,你家裡人知道麼。
這裡的主動就是 pull,英文翻譯拖拽,由人自動發起
被動就是push,英文翻譯就是推送,被動接受,下圖無關,放鬆心情
在這裡插入圖片描述

Counsumer採取的消費模式就是主動模式,Consumer說我不喜歡被動,男人嘛就要主動一點。那為啥不選擇push的方式被動呢主要是因為難適應消費速率不同的消費者,被動模式沒有主動權,可能一個消費者正在消費突然間大把的訊息進來,雖然有分割槽的概念,但是會出現網路擁堵或者拒絕連線的情況。
那採取pull模式就能解決這個問題,隨之而來的問題就是,pull就是一直不斷的親求,假如佇列中沒有資料的話,有很多空請求會一直輪詢,因此Kafka的消費者在消費資料時會傳入一個時長引數timeout,如果當前沒有資料可供消費,consumer會等待一段時間之後再返回,這段時長即為timeout。

kafka為啥吞吐量這麼高

  • 順序寫磁碟
    我們生活都遇到過這種情況,當我們拷貝一個大檔案的時候,會比較快,哪怕檔案有幾個G,每秒鐘可能複製幾十M甚至百M。但是我們複製大量的小檔案的時候就會特別慢。這就要從磁碟的硬體結構來說了,當我們拷貝大檔案的時候,是磁頭定址一次,一塊區域一塊區域的順序讀寫,因此會很快。但是當我們多檔案拷貝的時候我們的磁碟就需要不停的查詢磁頭的位置,時間都浪費在定址上了。因此kafka在設計的時候就避開了硬體的短板,採用順序讀寫的方式,這是保證高速寫的一個重要因素。
    Kafka的producer生產資料,要寫入到log檔案中,寫的過程是一直追加到檔案末端,為順序寫。官網有資料表明,同樣的磁碟,順序寫能到到600M/s,而隨機寫只有100k/s。這與磁碟的機械機構有關,順序寫之所以快,是因為其省去了大量磁頭定址的時間。

在這裡插入圖片描述

  • 零拷貝
    學過Linux的都知道,要想從磁碟中拷貝一個檔案,大致要經歷這麼幾個簡單的過程。圖示:
    在這裡插入圖片描述

基本操作就是迴圈的從磁碟讀入檔案內容到緩衝區,再將緩衝區的內容傳送到socket。但是由於Linux的I/O操作預設是緩衝I/O。這裡面主要使用的也就是read和write兩個系統呼叫,我們並不知道作業系統在其中做了什麼。實際上在以上I/O操作中,發生了多次的資料拷貝。

當應用程式訪問某塊資料時,作業系統首先會檢查,是不是最近訪問過此檔案,檔案內容是否快取在核心緩衝區,如果是,作業系統則直接根據read系統呼叫提供的buf地址,將核心緩衝區的內容拷貝到buf所指定的使用者空間緩衝區中去。如果不是,作業系統則首先將磁碟上的資料拷貝的核心緩衝區,這一步目前主要依靠DMA來傳輸,然後再把核心緩衝區上的內容拷貝到使用者緩衝區中。如此繁瑣的過程 ,大大降低了傳輸效率。

零拷貝一句話總結 :讓資料傳輸不需要經過user space。
在這裡插入圖片描述
只用將磁碟檔案的資料複製到頁面快取中一次,然後將資料從頁面快取直接傳送到網路中(傳送給不同的訂閱者時,都可以使用同一個頁面快取),避免了重複複製操作。

Zookeeper與Kafka中的關係

Kafka叢集中有一個broker會被選舉為Controller,負責管理叢集broker的上下線,所有topic的分割槽副本分配和leader選舉等工作。
Controller的管理工作都是依賴於Zookeeper的。

Leader選取的過程:
在這裡插入圖片描述
我們可以親自去看下zk目錄下和kafka相關的目錄檔案
我們啟動zk的客戶端,Windows下直接在cmd中切換到zk的bin目錄執行zkcli.cmd

檢視/brokers/ids
在這裡插入圖片描述
剛好是三個節點,0,1,2
當有一個節點掉線之後,zk /brokers/ids 這個目錄下的儲存的斷開的節點就會被移除,這時候,節點總數發生了變化,kafkaController發現節點發生變化後,便會獲取ISR裡面的節點,並從中選取一個作為新的leader,ISR的獲取方式
在這裡插入圖片描述
圖上我們可以很清晰的看到當前的leader就是第0個節點。

以下是其他目錄對應的kafka的儲存資訊,這裡就不一一展開,需要看的可以自己使用zk命令自己去檢視研究。
在這裡插入圖片描述
此圖是轉自csdn部落格上的一個大神 幽靈之使。

offset的維護

由於consumer在消費過程中可能會出現斷電當機等故障,consumer恢復後,需要從故障前的位置的繼續消費,所以consumer需要實時記錄自己消費到了哪個offset,以便故障恢復後繼續消費。
可以參考目錄章節的__consumer__offset 圖片,ps:是在0.9版本之後,之前的版本儲存在zk中。

訊息傳送流程

生產者傳送訊息採取的是非同步 傳送,傳送過程中涉及到了兩個執行緒,main-Sender,以及一個執行緒共享變數-RecordAccumulator
關於執行緒共享變數可以簡單的理解為Java中的volitate。

主要流程:
main執行緒將訊息傳送給RecordAccumulator,Sender執行緒不斷從RecordAccumulator中拉取訊息傳送到Kafka broker。

奉上醜照:
在這裡插入圖片描述

Java版API

新增maven依賴
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.11.0.0</version>
</dependency>
//核心程式碼 不帶回撥函式的API 
     Properties props = new Properties();
        props.put("bootstrap.servers", "hadoop102:9092");//kafka叢集,broker-list
        props.put("acks", "all");
        props.put("retries", 1);//重試次數
        props.put("batch.size", 16384);//批次大小
        props.put("linger.ms", 1);//等待時間
        props.put("buffer.memory", 33554432);//RecordAccumulator緩衝區大小
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

        Producer<String, String> producer = new KafkaProducer<>(props);
        for (int i = 0; i < 100; i++) {
            producer.send(new ProducerRecord<String, String>("first", Integer.toString(i), Integer.toString(i)));
        }
        producer.close();
    }

//帶回撥函式的 就是在ProducerRecord 第三個引數加一個回撥引數
//回撥函式會在producer收到ack時呼叫,為非同步呼叫,該方法有兩個引數,分別是RecordMetadata和Exception,如果Exception為null,說明訊息傳送成功,如果Exception不為null,說明訊息傳送失敗。

producer.send(new ProducerRecord<String, String>("first", Integer.toString(i), Integer.toString(i)), new Callback() {
                //回撥函式,該方法會在Producer收到ack時呼叫,為非同步呼叫
                @Override
                public void onCompletion(RecordMetadata metadata, Exception exception) {
                    if (exception == null) {
                        System.out.println("success->" + metadata.offset());
                    } else {
                        exception.printStackTrace();
                    }
                }
            });

相關文章