訊息佇列Kafka學習總結

友德發表於2018-07-11

分享的目的

  1. 更深入瞭解訊息中介軟體Kafka的系統架構;
  2. 更好的使用訊息中介軟體Kafka解決實際專案中的問題;
  3. 通過Kafka的設計架構原理,和使用場景,能夠更快速掌握研究其它類似的訊息中介軟體,如RocketMQ, Notify, ActiviteMQ, 能夠在實際的業務中更好使用這些訊息中介軟體

分享大綱

Kafka系統架構;
Kafka開發;
Kafka引數調優;

Kafka系統架構

Kafka介紹

Kafka是一種高吞吐量的分散式釋出訂閱訊息系統,有如下特性:

  1. 通過O(1)的磁碟資料結構提供訊息的持久化,這種結構對於即使數以海量的訊息儲存也能夠保持長時間的穩定效能(高效能);
  2. 高吞吐量:即使是非常普通的硬體Kafka也可以支援每秒數百萬的訊息(高併發);
  3. 支援通過Kafka伺服器和消費機叢集來分割槽訊息(高可靠);
  4. 支援Hadoop並行資料載入;
  5. 支援各種語言豐富的客戶端(java, C++, python, erlang, .Net, go, Clojure, Scala);
    image.png

Kafka架構

Kafka基本概念介紹

Broker:

Kafka叢集包含一個或多個伺服器,這種伺服器被稱為broker;

Topic

每條釋出到Kafka叢集的訊息都有一個類別,這個類別被稱為Topic,實際開發中通過稱為佇列, topic名稱,即叫做Kafka隊名稱。(物理上不同Topic的訊息分開儲存,邏輯上一個Topic的訊息雖然儲存於一個或多個broker上但使用者只需指定訊息的Topic即可生產或消費資料而不必關心資料存於何處);

Partition

Partition是物理上的概念,每個Topic包含一個或多個Partition;

Segment

partition物理上由多個segment組成;

Producer

負責釋出訊息到Kafka broker;

Consumer

訊息消費者,向Kafka broker讀取訊息的客戶端;

Consumer Group

每個Consumer屬於一個特定的Consumer Group可為每個Consumer指定group name,若不指定group name則屬於預設的group, 例如 在電商中發貨中心、交易中心分別是兩個Kafka consumer group。

Kafka系統核心架構

image.png

Kafka topic在服務端的結構

image.png

  1. 一般情況下,一個topic下包含一個或多個Partition, 2-3個複本,這個在建立topic的時候可以指定;
  2. 多個Partition可以提高讀寫的吞吐量, 多個副本提高Kafka的可靠性;
  3. 在資料不需要保序的情況下,建立多個Partition最好; 在區域性保序的情況下,同一個Partition消費保序即可; 在合局有消費保序的情況下,一個topic對應一個Partition, 如資料庫實時同步場景;
  4. 每一個Partition理論上是一個無長的佇列,包含多個檔案,檔案以時間戳+最小的offset命名,不能修改,只能以只能是以append的方式寫入,會通過NIO記憶體對映檔案的方持久化到硬碟上, 檔案的大小固定,大小達到閾值以後,關閉舊檔案 ,重新新建一個記憶體對映檔案 ;在 flush硬碟的時候是順序IO,因此寫入Partition的速度會非常非常快, 過期的檔案 根據儲存時間,後臺非同步執行緒定期清除,釋放磁碟空間;

Partition offset

image.png

  1. 一個消費叢集,即group 對應一個topic下Partition的一個消費offset;
  2. 兩個不同的消費叢集對應的同一個Partition 下的消費offset互不影響;
  3. 可以通過時間戳定位Partition的offset, 這個一般在特殊的情況下才會這樣這樣做,大部分情況下,zk上都會有記錄;
  4. 同一個Partition 只能嚴格順序消費;

Kafka事物

資料傳輸的事務定義通常有以下三種級別

1.

最多一次: 訊息不會被重複傳送,最多被傳輸一次,但也有可能一次不傳輸;

2.

最少一次: 訊息不會被漏傳送,最少被傳輸一次,但也有可能被重複傳輸;

3.

精確的一次(Exactly once): 不會漏傳輸也不會重複傳輸,每個訊息都傳輸被一次而且僅僅被傳輸一次,這是大家所期望的,但是很難做到;

Kafka的事物解決方案:

1.

當釋出訊息時,Kafka有一個committed的概念,一旦訊息被提交了,只要訊息被寫入的分割槽的所在的副本broker是活動的,資料就不會丟失,但是如果producer釋出訊息時發生了網路錯誤, Kafka現在也沒一個完美的解決方案;

2.

如果consumer崩潰了,會有另外一個consumer接著消費訊息,它需要從一個合適的 offset 繼續處理。這種情況下可以有以下選擇:

2. 1

consumer可以先讀取訊息,然後將offset寫入日誌檔案中,然後再處理訊息。這存在一種可能就是在儲存offset後還沒處理訊息就crash了,新的consumer繼續從這個offset處理那麼就會有些訊息永遠不會被處理,這就是上面說的“最多一次”。

2.2

consumer可以先讀取訊息,處理訊息,最後記錄offset,當然如果在記錄offset之前就crash了,新的consumer會重複的消費一些訊息,這就是上面說的“最少一次”;

2.3

“精確一次”可以通過將提交分為兩個階段來解決:儲存了offset後提交一次,訊息處理成功之後再提交一次。但是還有個更簡單的做法:將訊息的offset和訊息被處理後的結果儲存在一起;

Kafka一次訊息的生命週期

image.png

Kafka zk結點介紹

broker註冊結點

broker/ids/[0…N]

topic結點

broker/topics/[topic]

Broker/topics/[topic]/3->2

broker id 為3的broker為某個topic提供了2個分割槽進行訊息的儲存

消費分割槽與消費者的關係

/consumers/[group_id]/owners/[topic]/[broker_id-partition_id]

訊息消費進度Offset記錄

/consumers/[group_id]/offsets/[topic]/[broker_id-partition_id]

消費者註冊

/consumers/[group_id]/ids/[consumer_id]

消費者監聽:

/consumers/[group_id]/ids
/brokers/ids/[0…N]

Kafka日誌檔案

Kafka所有日誌檔案均儲存在server.properties檔案配置引數log.dirs下,假設 log.dirs配置為/export/kafka/log/, 同一個topic下有多個不同partition,每個partition為一個目錄,目錄的命名規範為: topic名稱+有序序號,第一個partition序號從0開始,序號最大值為partitions數量減1,例如: 一個名為trade為的topic, 有4個partition, 則在/export/kafka/log/目錄下有下面的資訊:
trade-0 trade-1 trade-2 trade-3
partition中檔案儲存方式:每個partion目錄相當於一個巨型檔案被平均分配到多個大小相等segment資料檔案中。但每個段segment file訊息數量不一定相等,這種特性方便old segment file快速被刪除。每個partiton只需要支援順序讀寫就行了,segment檔案生命週期由服務端配置引數決定。這樣做的好處就是能快速刪除無用檔案,有效提高磁碟利用率, 下面的partition中檔案儲存方式:
image.png

partition中segment檔案儲存結構

  1. segment file組成:由2大部分組成,分別為index file和data file,此2個檔案一一對應,成對出現,字尾”.index”和“.log”分別表示為segment索引檔案、資料檔案.
  2. segment檔案命名規則:partition全域性的第一個segment從0開始,後續每個segment檔名為上一個segment檔案最後一條訊息的offset值。數值最大為64位long大小,19位數字字元長度,沒有數字用0填充,示例如下。
    image.png

partition中通過offset查詢message

第一步查詢segment file;
第二步通過segment file查詢message;
示例: 查詢為offset=368776的message, 如下圖所示;
image.png

  1. 定位segment file: 其中00000000000000000000.index表示最開始的檔案,起始偏移量(offset)為0.

    第二個檔案00000000000000368769.index的訊息量起始偏移量為368770 = 368769 + 1.第三個檔案
    00000000000000737337.index的起始偏移量為737338=737337 + 1,其他後續檔案依次類推,以起
    始偏移量命名並排序這些檔案,只要根據offset 二分查詢檔案列表,就可以快速定位到具體檔案。當
    offset=368776時定位到00000000000000368769.index|log
  2. 在segment file中查詢:當offset=368776時,依次定位到00000000000000368769.index的後設資料物理

    位置和00000000000000368769.log的物理偏移地址,然後再通過00000000000000368769.log順序
    查詢直到offset=368776為止;
    

Kafka 負載均衡

生產者的負載均衡

四層的負載均衡,不常用, 使用zookeeper進行負載均衡, 常 用, 其中partitioner.class 的配置決定了Kafka生產者的負載均衡, 有三種情況:

  1. kafka.producer.DefaultPartitioner (Hash模式)
  2. kafka.producer.ByteArrayPartitioner (Hash模式)
  3. 不指定隨機輪詢模式

消費者負載均衡

partition和consumer 對應好, 平均
1: 5個partition, 5個consumer, (1, 1, 1, 1, 1)
2: 8個 partition, 5個consumer, (2, 2, 2, 1, 1)
3: 5個 partition, 8個consumer, (1, 1, 1, 1, 1, 0, 0, 0)

Kafka線上部署

image.png

1.一般情況下,根據併發量,一個Kafka叢集有3-5臺broker機器,阿里是一條業務線7-8臺物理機,記憶體是192G,為了減少訊息的堆積,一個topic下128+個Partition, 預設是消費叢集機器數量的2-3倍, 例如線上消費叢集是64臺,partition建議設定為128 – 192臺之間, Note however that there cannot be more consumer instances in a consumer group than partitions.
2.一主多備部署:一個備的broker在和主的broker在同一個機房,另一個備的broker部署在同城的另一個機房, 進一步增加高可靠性.

  1. 為防止訊息堆積,建議同一個topic的消費叢集的消費能力不能小於生產的叢集.
  2. 為了提高網路的利用率,建議一次性傳送的訊息儘可能的大,避免小包網路傳輸.

Kafka 保證

  1. 訊息通過生產者被髮送出去,將會按訊息傳送時的順序追加到到一個指定的topic partition,也是說,記錄M1和記錄M2被同一個生產者傳送,並且M1要先於M2傳送,那麼在日誌檔案中M1將會有比M2更小的offset(生產保序)
  2. 一個消費者例項順序消費儲存在日誌中的記錄(消費保序)
  3. 對於一個有N個複本的topic,最多情況下N-1個broker server丟失資料,也可以保證在記錄被提交的情況下,不會丟資料(高可用);

Kafka開發

Kafka應用開發

kafka java客戶端maven依賴

<dependency>
  <groupId>org.apache.kafka</groupId>
  <artifactId>kafka_2.9.2</artifactId>
  <version>0.8.2.2</version>
</dependency>

<dependency>
  <groupId>org.apache.kafka</groupId>
  <artifactId>kafka-clients</artifactId>
  <version>0.10.0.0</version>
</dependency>

Kafka producer程式碼示例

public class KafkaProducerTest {  
      
    private static final Logger LOG = LoggerFactory.getLogger(KafkaProducerTest.class);  
      
    private static Properties properties = null;  
      
    //  kafka連續配置 項
    static {  
        properties = new Properties();  
       properties.put("bootstrap.servers", "centos.master:9092,centos.slave1:9092,centos.slave2:9092");
         properties.put("producer.type", "sync");  
         properties.put("request.required.acks", "1");  
         properties.put("serializer.class", "kafka.serializer.DefaultEncoder");  
         properties.put("partitioner.class", "kafka.producer.DefaultPartitioner");  
         properties.put("key.serializer", "org.apache.kafka.common.serialization.ByteArraySerializer");  
         properties.put("value.serializer", "org.apache.kafka.common.serialization.ByteArraySerializer");  
    }
public void produce() {  
       KafkaProducer<byte[], byte[]> kafkaProducer = new KafkaProducer<byte[],byte[]>(properties);  
        ProducerRecord<byte[],byte[]> kafkaRecord = new ProducerRecord<byte[],byte[]>(  
                "test", "kkk".getBytes(), "vvv".getBytes());  
        kafkaProducer.send(kafkaRecord, new Callback() {  
            public void onCompletion(RecordMetadata metadata, Exception e) {  
                if(null != e) {  
                    LOG.info("the offset of the send record is {}", metadata.offset());  
                    LOG.error(e.getMessage(), e);  
                }  
                LOG.info("complete!");  
            }  
        });  
        kafkaProducer.close();  
    }  
  
    public static void main(String[] args) {  
        KafkaProducerTest kafkaProducerTest = new KafkaProducerTest();  
        for (int i = 0; i < 10; i++) {  
            kafkaProducerTest.produce();  
        }  
    }  
}  

Kafka Consumer程式碼示例

public class ConsumerSample {

   public static void main(String[] args) {
                
       Properties props = new Properties();
      props.put("zk.connect", "localhost:2181");
     props.put("zk.connectiontimeout.ms", "1000000");
      props.put("key.serializer", "org.apache.kafka.common.serialization.ByteArraySerializer");  
      props.put("value.serializer", "org.apache.kafka.common.serialization.ByteArraySerializer");  
     props.put("groupid", "test_group");

     ConsumerConfig consumerConfig = new ConsumerConfig(props);
     ConsumerConnector consumerConnector = 
               Consumer.createJavaConsumerConnector(consumerConfig);

      HashMap<String, Integer> map = new HashMap<String, Integer>();
      map.put("test-topic", 4);
      Map<String, List<KafkaStream<Message>>> topicMessageStreams = 
   consumerConnector.createMessageStreams(map);
      List<KafkaStream<Message>> streams = topicMessageStreams.get("test-topic");
         ExecutorService executor = Executors.newFixedThreadPool(4); 
     for (final KafkaStream<Message> stream : streams) {
             executor.submit(new Runnable() {
            public void run() {
                  for (MessageAndMetadata msgAndMetadata : stream) {
                  System.out.println("topic: " + msgAndMetadata.topic());
                  Message message = (Message) msgAndMetadata.message();
                  ByteBuffer buffer = message.payload();
                  buffer.get(bytes);
                  String tmp = new String(bytes);
                  System.out.println("message content: " + tmp);
                 }
            }
       });
      }
  }
}

Kafka開發注意事項

  1. 生產端注意設定好producer.type和partitioner.class 這兩個引數,第一個引數 對寫入吞吐量影響巨大,要結合實際的業務場景來設定,第二個引數關係到消費的結果是不是正常的,也要結合實際的業務場景來設定.
  2. 生產端和消費端的key.serializer 和 value.serializer分別設定一樣,否則消費端可能會產生亂碼.
  3. 根據實際的業務場景,設定好生產端和消費端的其它配置引數.

Kafka常見的使用模式

Consumer 消費訊息模式

  1. 推模式
  2. 拉模式(常見)

Producer 釋出訊息模式

區域性保序模式(hash對映)
全部保序模式 (只有一個Partition)
不保序模式(輪訓模式)

Kafka的使用場景

Messaging, 大規模分散式網站

非同步,解耦、削峰

Website Activity Tracking

通過agent定時收集每臺主機的syslog資訊

Metrics

Log Aggregation

Event Sourcing

Commit Log

異地機房的資料近實時同步(全域性保序和區域性保序)

Kafka引數調優

Broker 引數調優

  1. log.dirs Kafka資料存放的目錄。可以指定多個目錄,中間用逗號分隔,當新partition被建立的時會被存放到 當前存放partition最少的目錄.
  2. num.io.threads 伺服器用來執行讀寫請求的IO執行緒數,此引數的數量至少要等於伺服器上磁碟的數量.
  3. queued.max.requests I/O執行緒可以處理請求的佇列大小,若實際請求數超過此大小,網路執行緒將停止接收新的請求,建議500-1000.
  4. num.partitions 預設partition數量,如果topic在建立時沒有指定partition數量,預設使用此值,建議修改為consumer 數量的1-3倍.
  5. log.segment.bytes Segment檔案的大小,超過此值將會自動新建一個segment,此值可以被topic級別的引數覆蓋, 建議1G ~ 5G.
  6. default.replication.factor 預設副本數量,建議改為2.
  7. num.replica.fetchers Leader處理replica fetch訊息的執行緒數量, 建議設定大點2-4.
  8. offsets.topic.num.partitions offset提交主題分割槽的數量,建議設定為100 ~ 200.

Producer 引數調優

  1. request.required.acks :用來控制一個produce請求怎樣才能算完成, 主要是來表示寫入資料的持久化的,有三個值(0, 1, -1), 持久化的程度依次增高.
  2. producer.type : 同步非同步模式。async表示非同步,sync表示同步。如果設定成非同步模式,可以允許生產者以batch的形式push資料,這樣會極大的提高broker效能,推薦設定為非同步.
  3. partitioner.class : Partition類,預設對key進行hash, 即 kafka.producer.DefaultPartitioner.
  4. compression.codec :指定producer訊息的壓縮格式,可選引數為: “none”, “gzip” and “snappy”.
  5. queue.buffering.max.ms :啟用非同步模式時,producer快取訊息的時間。比如我們設定成1000時,它會快取1秒的資料再一次傳送出去,這樣可以極大的增加broker吞吐量,但也會造成時效性的降低.
  6. queue.buffering.max.messages:採用非同步模式時producer buffer 佇列裡最大快取的訊息數量,如果超過這個數值,producer就會阻塞或者丟掉訊息.
  7. batch.num.messages:採用非同步模式時,一個batch快取的訊息數量。達到這個數量值時producer才會傳送訊息.

Consumer引數調優

  1. fetch.message.max.bytes:查詢topic-partition時允許的最大訊息大小,consumer會為每個partition快取此大小的訊息到記憶體,因此,這個引數可以控制consumer的記憶體使用量。這個值應該至少比server允許的最大訊息大小大,以免producer傳送的訊息大於consumer允許的訊息.
  2. num.consumer.fetchers:拉資料的執行緒數量,為了保序,建議一個,用預設值.
  3. auto.commit.enable:如果此值設定為true,consumer會週期性的把當前消費的offset值儲存到zookeeper,當consumer失敗重啟之後將會使用此值作為新開始消費的值.
  4. auto.commit.interval.ms: Consumer提交offset值到zookeeper的週期.

Kafka常見問題總結

客戶端消費出現空指標異常

 重新設定消費的partition offset

客戶端無法消費

網路不通
jar包衝突(netty, slf4j)

同類產品

RocketMq
JMQ

其它訊息產品

notify
ActiveMQ(Apache)
Hornet(Jboss)


相關文章