Kafka學習

Hiway發表於2019-04-24

其他更多java基礎文章:
java基礎學習(目錄)


學習資料:kafka資料可靠性深度解讀

Kafka概述

  • Kafka是一個分散式訊息佇列。Kafka對訊息儲存時根據Topic進行歸類,傳送訊息者稱為Producer,訊息接受者稱為Consumer,此外kafka叢集有多個kafka例項組成,每個例項(server)成為broker。
  • 無論是kafka叢集,還是producer和consumer都依賴於zookeeper叢集儲存一些meta資訊,來保證系統可用性。
  • 在流式計算中,Kafka一般用來快取資料,Spark通過消費Kafka的資料進行計算。

Kafka架構

Kafka學習

  • Producer :訊息生產者,就是向kafka broker發訊息的客戶端。
  • Consumer :訊息消費者,向kafka broker取訊息的客戶端
  • Topic :可以理解為一個佇列。
  • Consumer Group (CG):這是kafka用來實現一個topic訊息的廣播(發給所有的consumer)和單播(發給任意一個consumer)的手段。一個topic可以有多個CG。topic的訊息會複製(不是真的複製,是概念上的)到所有的CG,但每個partion只會把訊息發給該CG中的一個consumer。如果需要實現廣播,只要每個consumer有一個獨立的CG就可以了。要實現單播只要所有的consumer在同一個CG。用CG還可以將consumer進行自由的分組而不需要多次傳送訊息到不同的topic。
  • Broker :一臺kafka伺服器就是一個broker。一個叢集由多個broker組成。一個broker可以容納多個topic。
  • Partition:為了實現擴充套件性,一個非常大的topic可以分佈到多個broker(即伺服器)上,一個topic可以分為多個partition,每個partition是一個有序的佇列。partition中的每條訊息都會被分配一個有序的id(offset)。kafka只保證按一個partition中的順序將訊息發給consumer,不保證一個topic的整體(多個partition間)的順序。

分割槽

訊息傳送時都被髮送到一個topic,其本質就是一個目錄,而topic是由一些Partition Logs(分割槽日誌)組成,其組織結構如下圖所示:

Kafka學習

Kafka學習

我們可以看到,每個Partition中的訊息都是有序的,生產的訊息被不斷追加到Partition log上,其中的每一個訊息都被賦予了一個唯一的offset值。

  • 分割槽的原因
    1. 方便在叢集中擴充套件,每個Partition可以通過調整以適應它所在的機器,而一個topic又可以有多個Partition組成,因此整個叢集就可以適應任意大小的資料了;
    2. 可以提高併發,因為可以以Partition為單位讀寫了。
  • 分割槽的原則
    1. 指定了patition,則直接使用;
    2. 未指定patition但指定key,通過對key的value進行hash出一個patition
    3. patition和key都未指定,使用輪詢選出一個patition。

副本(Replication)

同一個partition可能會有多個replication(對應 server.properties 配置中的 default.replication.factor=N)。沒有replication的情況下,一旦broker 當機,其上所有 patition 的資料都不可被消費,同時producer也不能再將資料存於其上的patition。引入replication之後,同一個partition可能會有多個replication,而這時需要在這些replication之間選出一個leader,producer和consumer只與這個leader互動,其它replication作為follower從leader 中複製資料。

寫入流程

Kafka學習

  1. producer先從zookeeper的 "/brokers/.../state"節點找到該partition的leader
  2. producer將訊息傳送給該leader
  3. leader將訊息寫入本地log
  4. followers從leader pull訊息,寫入本地log後向leader傳送ACK
  5. leader收到所有ISR中的replication的ACK後,增加HW(high watermark,最後commit 的offset)並向producer傳送ACK

ACK,HW,ISR等可以閱讀kafka資料可靠性深度解讀學習
簡單來說:

  • HW是HighWatermark的縮寫,是指consumer能夠看到的此partition的位置
  • ISR (In-Sync Replicas),這個是指副本同步佇列。所有的副本(replicas)統稱為Assigned Replicas,即AR。ISR是AR中的一個子集,由leader維護ISR列表,follower從leader同步資料有一些延遲(包括延遲時間replica.lag.time.max.ms和延遲條數replica.lag.max.messages兩個維度),任意一個超過閾值都會把follower剔除出ISR, 存入OSR(Outof-Sync Replicas)列表,新加入的follower也會先存放在OSR中。AR=ISR+OSR。
  • ack機制:在kafka傳送資料的時候,每次傳送訊息都會有一個確認反饋機制,確保訊息正常的能夠被收到。

Kafka消費過程分析

kafka提供了兩套consumer API:高階Consumer API和低階API。

高階API

  • 高階API優點
    • 高階API 寫起來簡單
    • 不需要去自行去管理offset,系統通過zookeeper自行管理
    • 不需要管理分割槽,副本等情況,系統自動管理
    • 消費者斷線會自動根據上一次記錄在zookeeper中的offset去接著獲取資料(預設設定1分鐘更新一下zookeeper中存的的offset)
    • 可以使用group來區分對同一個topic的不同程式訪問分離開來(不同的group記錄不同的offset,這樣不同程式讀取同一個topic才不會因為offset互相影響)
  • 高階API缺點
    • 不能自行控制offset(對於某些特殊需求來說)
    • 不能細化控制如分割槽、副本、zk等

低階API

  • 低階 API 優點
    • 能夠開發者自己控制offset,想從哪裡讀取就從哪裡讀取。
    • 自行控制連線分割槽,對分割槽自定義進行負載均衡
    • 對zookeeper的依賴性降低(如:offset不一定非要靠zk儲存,自行儲存offset即可,比如存在檔案或者記憶體中)
  • 低階API缺點
    • 太過複雜,需要自行控制offset,連線哪個分割槽,找到分割槽leader 等。

消費者組

消費者是以consumer group消費者組的方式工作,由一個或者多個消費者組成一個組,共同消費一個topic。每個分割槽在同一時間只能由group中的一個消費者讀取,但是多個group可以同時消費這個partition。

Kafka學習
在圖中,有一個由三個消費者組成的group,有一個消費者讀取主題中的兩個分割槽,另外兩個分別讀取一個分割槽。某個消費者讀取某個分割槽,也可以叫做某個消費者是某個分割槽的擁有者。 在這種情況下,消費者可以通過水平擴充套件的方式同時讀取大量的訊息。另外,如果一個消費者失敗了,那麼其他的group成員會自動負載均衡讀取之前失敗的消費者讀取的分割槽。

JAVA中使用Kafka

生產者

import org.apache.kafka.clients.producer.*;

import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

public class KafkaProducerDemo {

    public static void main(String[] args){
        //test();
        test2();
    }

    public static void test(){
        Properties props= new Properties();
        props.put("bootstrap.servers", "172.26.40.181:9092");
        props.put("acks", "all");
        props.put("retries", 0);
        props.put("batch.size", 16384);
        props.put("linger.ms", 1);
        props.put("buffer.memory", 33554432);
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

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

        producer.close();
    }

    /**
     * 帶回撥函式
     */
    public static void test2(){
        Properties props = new Properties();
        // Kafka服務端的主機名和埠號
        props.put("bootstrap.servers", "172.26.40.181:9092");
        // 等待所有副本節點的應答
        props.put("acks", "all");
        // 訊息傳送最大嘗試次數
        props.put("retries", 0);
        // 一批訊息處理大小
        props.put("batch.size", 16384);
        // 增加服務端請求延時
        props.put("linger.ms", 1);
// 傳送快取區記憶體大小
        props.put("buffer.memory", 33554432);
        // key序列化
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        // value序列化
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

        //攔截器
        List<String> interceptor = new ArrayList<>();
        interceptor.add("com.hiway.practice.kafka.TimeInterceptor");
        interceptor.add("com.hiway.practice.kafka.CounterInterceptor");
        props.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,interceptor);

        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(props);

        for (int i = 0; i < 50; i++) {

            kafkaProducer.send(new ProducerRecord<String, String>("first", "hello" + i), new Callback() {

                @Override
                public void onCompletion(RecordMetadata metadata, Exception exception) {

                    if (metadata != null) {

                        System.err.println(metadata.partition() + "---" + metadata.offset());
                    }
                }
            });
        }

        kafkaProducer.close();
    }

}
複製程式碼

消費者

import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;

import java.util.Arrays;
import java.util.Properties;

public class CustomNewConsumer {

    public static void main(String[] args) {

        Properties props = new Properties();
        // 定義kakfa 服務的地址,不需要將所有broker指定上
        props.put("bootstrap.servers", "172.26.40.181:9092");
        // 制定consumer group
        props.put("group.id", "test");
        // 是否自動確認offset
        props.put("enable.auto.commit", "true");
        // 自動確認offset的時間間隔
        props.put("auto.commit.interval.ms", "1000");
        // key的序列化類
        props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        // value的序列化類
        props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

        // 定義consumer
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);

        // 消費者訂閱的topic, 可同時訂閱多個
        consumer.subscribe(Arrays.asList("first"));

        while (true) {
            // 讀取資料,讀取超時時間為100ms
            ConsumerRecords<String, String> records = consumer.poll(100);

            for (ConsumerRecord<String, String> record : records)
                System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
        }
    }
}
複製程式碼

攔截器

public class TimeInterceptor implements ProducerInterceptor<String,String> {


    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
        return new ProducerRecord<>(record.topic(),record.partition(),record.timestamp(),record.key(),System.currentTimeMillis() + "," + record.value().toString());
    }

    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {

    }

    @Override
    public void close() {

    }

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

    }
}

public class CounterInterceptor implements ProducerInterceptor<String, String> {
    private int errorCounter = 0;
    private int successCounter = 0;

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

    }

    @Override
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> record) {
        return record;
    }

    @Override
    public void onAcknowledgement(RecordMetadata metadata, Exception exception) {
        // 統計成功和失敗的次數
        if (exception == null) {
            successCounter++;
        } else {
            errorCounter++;
        }
    }

    @Override
    public void close() {
        // 儲存結果
        System.out.println("Successful sent: " + successCounter);
        System.out.println("Failed sent: " + errorCounter);
    }
}
複製程式碼

錯誤總結

1. Uncaught error in kafka producer I/O thread錯誤
這個問題主要是伺服器上的kafka版本和IDEA中的kafka版本不一致導致的。

2.producer傳送資料到叢集上無反應

將kafka/config/server.properties檔案中advertised.listeners改為如下屬性。172.26.40.181是我虛擬機器的IP。改完後重啟,OK了。Java端的程式碼終於能通訊了 advertised.listeners=PLAINTEXT://172.26.40.181:9092 advertised.listeners上的註釋是這樣的:

#Hostname and port the broker will advertise to producers and consumers. If not set,
# it uses the value for "listeners" if configured. Otherwise, it will use the value
# returned from java.net.InetAddress.getCanonicalHostName().
複製程式碼

意思就是說:hostname、port都會廣播給producer、consumer。如果你沒有配置了這個屬性的話,則使用listeners的值,如果listeners的值也沒有配置的話,則使用 java.net.InetAddress.getCanonicalHostName()返回值(這裡也就是返回localhost了)。

相關文章