kafka的使用及工作原理

記錄Kafka的使用

什麼是kafka

  • 定義

  • Kafka是分佈式的基於發佈/訂閱模式的消息隊列,主要應用於大數據實時處理領域

  • 消息隊列

  • 削峯:高峯期將任務緩存在消息隊列中,持續處理

  • 解耦:不需要兩邊的處理同時在線

  • 可恢復性:當系統一部分組件失效時,不會影響到整個系統

  • 緩衝:有助於控制和優化數據流經過系統的速度,解決生產消息和消費消息速度不一致的問題

  • 異步通信:允許用戶將一個消息放入隊列,但並不立即處理它

  • 消息隊列的模式

  • 1.點對點模式

特點:一對一,消費者主動獲取消息,消息被消費後清除

消息生產者可以對應多個消費者,但是消息只能被一個消費者消費
  • 2.發佈/訂閱模式
特點:一對多,生產者推送數據,消費者消費數據之後不會清除消息

消息生產者將消息發佈到topic中,同時有多個消費者消費該消息

發佈/訂閱模式又分爲
MQ主動的推送模式
消費者主動的獲取模式
  • kafka基礎架構
  • 1.Producer:消息生產者,就是向kafka broker發消息的客戶端
  • 2.Consumer:消息消費者,就是向kafka broker拉取消息的客戶端
  • 3.Consumer Group:多個Consumer組成一個group,消費者組內的每一個消費者負責消費不同分區的數據,一個分區只能由一個消費者消費,消費者組之間互不影響.消費者組是邏輯上的一個訂閱者
  • 4.Broker:一臺kafka服務器就是一個broker,一個集羣由多個broker組成.一個broker可以容納多個topic
  • 5.Topic:一個主題,生產者和消費者面向的都是一個主題
  • 6.Partition:一個TOPIC可以分佈在不同的BROKER中,由多個Partition組成,每個partition都是一個有序的隊列
  • 7.Replica:副本,Broker中維護一個TOPIC的某個partition(Leader)同時還維護了該TOPIC其他partition的follower
  • 8.Leader:每個Topic都由一個若干個Partition組成,而每一個Partition又有1到brokers數量的副本,其中只有主副本(Leader)才能對外提供服務,其他的副本(Follower)只是用來保存數據
  • 9.Follower:每個分區多個副本中的從,主要用於數據備份.當Leader發生故障時,某個follower會成爲新的leader
  • 10.註冊中心,kafka依賴於zookeeper,作爲註冊中心

kafka快速入門

  • 安裝
    https://www.cnblogs.com/zikai/p/9627736.html
  • 常用命令(命令行)
  • 1.kafka-server-start:啓動kafka
通常啓動kafka需要指定配置文件
bin/kafka-server-start.sh -daemon config/server.properties

這裏使用守護進程啓動,使用jps指令可以看到正在執行的kafka
  • 2.kafka-server-stop:停止kafuka服務
bin/kafka-server-stop.sh
  • 3.kafka-topics :主題相關操作
1.kafka-topics --list:查看當前所有主題
  ./kafka-topics.sh --list --zookeeper 7.223.145.184:2181 注意必須跟zk的地址
2.kafka-topics --create:創建主題
  ./kafka-topics.sh --create --zookeeper 7.223.145.184:2181 --topic first --partitions 2 --replication-factor 1
  創建一個名稱爲first的主題,該主題有2個partition,和1個副本
  
  副本數量最多和Broker數量一樣.副本都會存在kafka所在的磁盤log.dirs配置的位置
3.kafka-topics --delete:刪除主題
  ./kafka-topics.sh --delete --zookeeper 7.223.145.184:2181 --topic first
4.kafka-topics --describe:查看主題詳情
  ./kafka-topics.sh --describe --zookeeper 7.223.145.184:2181 --topic first
  • 4.kafka-console-producer: 生產者
./kafka-console-producer.sh --topic first --broker-list 7.223.143.235:9092 
創建一個生產者.鏈接到我們的集羣(7.223.143.235:9092,此時我只有一個kafka,集羣也是一樣的,只需要鏈接到任意一臺)
此時我們就可以輸入消息了
  • 5.kafka-console-consumer: 消費者
1.消費
./kafka-console-consumer.sh --topic first --bootstrap-server 7.223.143.235:9092
./kafka-console-consumer.sh --topic first --zookeeper 7.223.145.184:2181(上古版本可用,新版本已不支持)
爲了能夠實現續傳,kafka記錄了某個消費者group對某個主題的消費情況(消費到哪裏了?)
kafka在上古時期是通過zookeeper來存儲偏移量的,存放在/consumers/[group_id]/offsets/[topic]/[broker_id-part_id]節點下
新版本的kafka將這些消息存在kafka集羣的consumer主題中,不再依賴zookeeper,該主題默認是50個分區1個副本
2.從頭消費
./kafka-console-consumer.sh --topic first --bootstrap-server 7.223.143.235:9092 --from-beginning
我們利用--from-beginning參數可以從頭消費

kafka架構深入

  • kafka架構及工作過程
  • kafka的架構如下圖,作如下假設
    在這裏插入圖片描述
  • 1.當前的topic爲A,有3個partition(0,1,2),Replica數爲2,每個broker保存的副本如圖所示
  • 2.每個partition當中存在6/4/5條消息,途中的編號爲消息的offset,此時我們的producer一共產生了15條消息,可見,每個partition都有單獨的offset編號,從而我們也可以看到,kafka不能保證全局有序
  • 3.Follower會自動備份Leader中的消息,這就是kafka的容災策略
  • 4.Consumer Group:正如圖所示,每一個消費者對應一個分區,消費者每消費一條數據,就會記錄下對應已消費的offset值(同樣也是partition隔離的),以便於出錯時可以從上次的位置繼續消費
根據Kafka的規定
1.每一個partition只能被同一個組中的一個consumer消費
2.同一個組中的consumer可以消費多個partition
所以最理想的情況是一個partition對應一個consumer(同一個consumer group中),這就是我們的分區依據
比如我們生產的速度是10kB/s,消費者性能的極限速度是1kB/s.
那麼我們需要10臺consumer才能穩定消費,此時我們可以將topic設置爲10個分區,同時將10個consumer組成一個consumer group

如果我們想要多個consumer消費一個partition,怎麼辦呢?
我們可以設定多個consumer group,來消費同一個partition
https://www.cnblogs.com/sa-dan/p/8080197.html
  • kafka文件存儲機制
    文件存儲
  • 由於生產者產生的消息會不斷追加到log文件末尾,爲了有效防止log文件過大導致數據定位效率低下,Kafka引入了分片索引機制,將每個partition分爲多個segment(默認是1GB),每個segment對應兩個文件"xx.index"和"xx.log".其中前綴是當前segment的第一條消息的offset.
  • 如下圖所示,index存放的內容是每條消息offset和消息在Log文件中存儲位置的映射關係
    在這裏插入圖片描述
  • kafka分區操作
  • 本節闡明生產者如何讓生產的消息選擇分區(Java)
  • 1.生產者可以直接指明選擇的分區
    ProducerRecord(@NotNull String topic,Integer partition,String key,String value)
    
  • 2.如果值是key,value形式的,可以使用key的hash與分去數取餘
    ProducerRecord(@NotNull String topic,String key,String value)
    
  • 3.如果只有value,則通過輪詢
    ProducerRecord(@NotNull String topic,Integer partition,String value)
    
  • 數據可靠性
  • 本節闡明如何確保生產者的消息成功傳給kafka,保證數據不丟失
  • 1.topic的每個partition收到produicer發送的數據之後,都需要向producer發送ack,如果producer收到ack,就會進行下一輪的發送,否則重新發送數據
  • 2.ISR(in-sync relplica)
    Kafka的同步機制
    kafka在發送ack之前,要求全部的follower都同步完成
    這種方案的缺點是延遲高.優點是容災需要的節點少(選舉新leader的時候,容忍n節點故障,需要N+1個副本)
    
    zk選用的方案是半數機制,這種機制的優點是延遲低,但容災需要的節點多(選舉新leader,容忍N節點故障,需要2*N+1個副本)
    
    爲了防止某個follower長時間不響應導致同步困難,kafka維護了一個ISR用來解決這個問題
    Leader維護了一個動態的ISR
    當ISR中的follower長時間未向leader同步,則該follower將被踢出ISR
    該時長由replica.lag.time.max.ms決定
    Leader發生故障後會從ISR中選舉新的Leader
    
  • 3.ack應答機制
  • kafka有三種ack機制
  • 0:接收到信息直接ack
    效率最高,容錯率最低
    
  • 1:接收到信息leader落地完成後ack
    效率高.但也容易丟失數據
    
  • -1:所有isr中的副本同步完成後ack
    效率最低.
    在極端情況下也會丟失數據:isr中只有leader並且down
    在某些情況下會造成數據重複:數據同步完成後還未返回ack的時候leader掛掉
    
  • 消費數據一致性
  • 由於kafka有多種ack機制,在某些機制下同步還沒有完成就接收到了新的數據,有可能出現數據不同步,此時如何對消費者保證數據的一致性
    在這裏插入圖片描述
  • 可見如果此時leader掛掉了,選擇了follower1,則此時最大的OFFSET爲15,會導致數據不一致
  • 爲此kafka引入了兩個概念HW和LEO,consumer可以收到的OFFSET爲HW
LEO:Log End Offset,每個副本中最大的OFFSET
HW:High Watermark,類似於木桶效應,ISR中最小的LEO
  • 基於HW的故障處理(保證partition中的數據一致性)
1.follower故障:follwer從故障恢復後,會讀取本地次胖記錄的上次的HW,並將Log文件高於HW的部分丟棄,
			  從HW開始向leader重新同步,當follower的LEO>=該partition的HW時,Follwer就可以重新加入ISR了
2.leader故障:leader發生故障後,會從ISR中選出一個新的leader,爲保證多個副本之間的數據一致性,
			 其餘的follower會將各自的log文件高於HW的部分截掉,然後從新的leader同步數據
  • Exactly Once
  • 前面介紹了不同的ack機制,都存在不同的問題
ACK 0: at most once -> 數據可能丟失
ACK -1: at last once -> 數據可能重複
  • kafka在0.11版本之後解決了數據重複的問題
冪等性開關開啓(Producer的參數enable.idompotence = true)後,該producer在初始化時會被分配一個PID
發往同一個Partition的消息會附帶Sequence Number,Broker會對<PID,Partition,SeqNumber>作緩存這就是Kafka去重的依據
注意:重啓PID會變化,同時kafka只能保證同一個partition中的exactly once

消費者

  • 消費方式
  • 消費者採用pull(拉取)的方式從broker中獲取數據
pull模式不足之處是,如果kafka沒有數據,消費者可能會陷入循環中,一直返回空數據
針對這一點,kafka在消費數據時會傳入一個市場參數(timeout)
如果沒有數據可以消費,consumer會等待一段時間之後再返回,這段市場即爲timeout
  • 分區分配策略
  • 一個consumer group中有多個consumer,一個topic有多個partition,所以必然會涉及到partiton的分配問題,即決定某個partition由哪個consumer消費’’
  • Kafka有兩種分配策略,一是RoundRobin,一是Range

== 加圖==

  • 1.RoundRobin:輪詢分配.在輪詢策略中,會將消費者組監聽的所有主題所有partition進行排序後輪詢交付給consumber
優點:可以使得所有的consumer監聽的partition個數差別最小
缺點:無法保證消費者消費的主題
  • 2.Range(默認):範圍分配是按照單個主題來劃分
優點:保證消費者組的每一個消費者都能消費所有的主題
缺點:消費者監聽partition個數不對等

Kafka API

Producer API

  • 消息發送流程
  • kafka的Producer發送消息採用的是異步發送的方式.在消息發送的過程中.涉及到兩個線程–main和sender線程.以及兩個線程的共享變量–RecordAccumulator,sender線程不斷從RecordAccumulator中拉取消息發送到Kafka broker

== 加圖==

  • 相關參數
1.batch size:只有數據累積到batch.size之後,sender纔會發送數據
2.linger.ms:如果數據遲遲未達到bath.size,sender等待linger.time之後就會發送數據
3.我們從ProducerConfig類中可以找到所有的配置
  • 普通生產者
public class MyProducer {
    public static void main(String[] args) {
        //創建生產者的配置信息
        Properties properties = new Properties();
        properties.put("bootstrap.servers","7.223.143.235:9092");
        //ack應答級別-1
        properties.put("ack","all");
        //重複次數
        properties.put("retries",3);
        //批次大小 16kb
        properties.put("batch.size",16384);
        //等待時間
        properties.put("linger.ms",1);
        //RecordAccumulator緩衝區大小
        properties.put("buffer.memory",33554432);
        //序列化類
        properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
        properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
        //創建生產者
        KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
        //發送數據
        for (int i = 0; i < 10; i++) {
            producer.send(new ProducerRecord<String,String>("third","value--"+i));
            System.out.println(i);
        }
        //關閉資源
        producer.close();
    }
}
  • 帶回調函數的生產者
public class CallBackProducer {
    public static void main(String[] args) {
        //創建生產者的配置信息
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "7.223.143.235:9092");
        //序列化類
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        //創建生產者
        KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
        //發送數據
        for (int i = 0; i < 10; i++) {
            producer.send(new ProducerRecord<String, String>("third", "value--" + i), (metadata, exception) -> {
                if (exception == null) {
                    System.out.println(metadata.partition() + "--" + metadata.offset());
                } else {
                    exception.printStackTrace();
                }
            });
        }
        //關閉資源
        producer.close();
    }
}
  • 生產者分區策略
public class CallBackProducer {
    public static void main(String[] args) {
        //創建生產者的配置信息
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "7.223.143.235:9092");
        //序列化類
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        //創建生產者
        KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
        //發送數據
        for (int i = 0; i < 10; i++) {
            //指定內容都發送到0號分區
            producer.send(new ProducerRecord<String, String>("third", 0,"key","value--" + i), (metadata, exception) -> {
                if (exception == null) {
                    System.out.println(metadata.partition() + "--" + metadata.offset());
                } else {
                    exception.printStackTrace();
                }
            });
            //通過key的hash值確定分區
			 producer.send(new ProducerRecord<String, String>("third", i%3+"","value--" + i), (metadata, exception) -> {
                if (exception == null) {
                    System.out.println(metadata.partition() + "--" + metadata.offset());
                } else {
                    exception.printStackTrace();
                }
            });
        }
        //關閉資源
        producer.close();
    }
}
  • 自定義分區器
public class MyPartitioner implements Partitioner {
    @Override
    public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
        return key.toString().hashCode() % cluster.partitionCountForTopic(topic);
    }

    @Override
    public void close() {

    }

    @Override
    public void configure(Map<String, ?> map) {

    }
}

public class PartitionProducer {
    public static void main(String[] args) {
        //創建生產者的配置信息
        Properties properties = new Properties();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "7.223.143.235:9092");
        //序列化類
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");
        //配置分區器
        properties.put(ProducerConfig.PARTITIONER_CLASS_CONFIG,"xx.xx.xx.MyProducer");
        //創建生產者
        KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties);
        //發送數據
        for (int i = 0; i < 10; i++) {
            producer.send(new ProducerRecord<String, String>("third", "value--" + i))
        }
        //關閉資源
        producer.close();
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章