開源組件系列(4):分佈式消息隊列(Kafka)

目錄

(一)消息隊列概述

(二)Kafka基本架構

(三)Kafka組件介紹

(四)Kafka關鍵技術點

(五)Kafka示例任務


(一)消息隊列概述

不論是系統產生的數據日誌,還是對應的數據系統,從來都不是單一的對應關係,而是多種數據日誌對應多套數據系統的複雜關聯。假設我們將採集的數據日誌直接傳輸到目標數據系統中,一旦因爲業務擴展而產生的新的數據系統建設需求,那麼依賴關係勢必變得非常混亂。

傳統日誌採集方式存在如下幾個問題:

1.數據日誌與數據系統之間的耦合度太高,當數據日誌或者數據系統需要擴展時,需要修改非常多的依賴關係;

2.數據日誌的產生速度與數據系統的處理速度不對等,如果遇到秒殺等場景,很容易引起系統崩潰;

3.依賴關係衆多導致併發很高,對於網絡壓力影響很大,容易成爲系統瓶頸。

 

爲了降低數據日誌和數據系統之間的耦合性,因此設計了消息隊列,成爲兩者之間的“中間件”。

以Kafka爲代表的消息隊列有如下幾方面的有點:

1.緩衝數據壓力:當數據日誌短時間內增加較多時,消息隊列能夠將數據系統無法處理的部分緩衝期來,防止系統壓力過大;

2.降低耦合度:消息隊列支持生產者/消費者模式,支持下游訂閱數據,如果需要新增數據日誌或數據系統,只需要修改配置文件,不需要修改系統代碼;

3.優秀的擴展性:消息隊列多采用分佈式架構設計,數據經過分片同時寫到多個節點中,避免單節點的瓶頸問題,並在秒殺等場景時提供動態擴展能力;

4.良好的容錯性:數據日誌在Kafka中會持久化到磁盤上,並通過分佈式的多副本策略來避免數據丟失。

 

(二)Kafka基本架構

 

Kafka作爲一個集羣中間件,需要運行在一臺或者多臺服務器上,Kafka通過Topic對存儲的流數據進行分類,每條記錄中包含一個Key,一個Value和一個Timestamp。在運行時,Producer將數據寫入到Broker中,由Broker負責構建分佈式的消息存儲系統了,將消息劃分爲多個Topic,然後再由Consumer從Broker讀取數據並進行處理。Kafka採用了push – pull的架構,即收到數據後,直接將數據push給對應的Broker,再由Consumer從Broker中將數據pull出來。

 

(三)Kafka組件介紹

 

Kafka主要由Producer、Broker、Consumer及Zookeeper組成。相關組件的介紹如下:

1.Producer

由用戶使用Kafka相關的SDK進行開發,Producer負責將數據發送給Broker。在Kafka中,每條數據被稱爲一個“消息”,由“三元組”組成。“三元組”包括:Topic、Key及Message。

(1)Topic:表示該條消息所述的Topic,是一種邏輯上的切分概念,一個Topic可以分給多個不同的Broker;

(2)Key:表示該條消息的主鍵,Kafka會根據每條數據的Key將消息分到不同的分區(Partition)中,默認是哈希取模的算法,用戶也可以自行定義相關的分區算法;

(3)Message:表示該條消息的值,通常爲字節數組,也可以使用String、JSON、Avro、Thrif、Protobuf等結構。

2.Broker

在分佈式的Kafka中,出於容錯的考慮,Broker一般有多個,負責接收Producer和Consumer的請求,並將消息持久化到本次磁盤。Broker以Topic爲單位將消息分成不同的分區(Partition),每個分區可以有多個副本,通過數據冗餘的方式來實現容錯。當分區(Partition)存在多個副本時,其中會有一個Leader,對外提供讀寫請求,其他的都是Follower,不提供讀寫服務,只是同步Leader數據,並且在Leader出問題時,選出一個成爲新的Leader。這種容錯方式與Mysql的主備比較類似。

Broker能夠保證統一Topic下的同一Partition內消息是有序的,但無法保證Partition之間全局有序。這意味着,Comsumer在消費某個Topic下的消息時,可能得到與寫入順序不同的消息序列。

Brokder以追加的方式將消息寫到磁盤中,並且每個分區中的消息被賦予了唯一整數標識,稱之爲偏移量(Offset)。Broker只提供基於Offset的讀取方式,並不會維護各個Consumer當前已消費的Offset值,而是由Consumer各自維護當前讀取的進度。Broker中保存的數據是有有效期的,比如7天,一旦超過了有效期,對應的數據將被釋放以釋放磁盤空間。只要數據在有效期內,Consumer可以重複讀取而不受限制。

3.Consumer

負責從Broker中拉取消息並進行處理,每個Consumer維護最後一個已讀消息的Offset,並在下次請求開始時從這個Offset開始讀取消息,這種機制使得Broker的吞吐效率很高。值得注意的是,Kafka允許多個Consumer構成一個Consumer Group,共同讀取一個Topic中的數據。

4.Zookeeper

Zookeeper負責提供分佈式的協調服務,所有Broker會向Zookeeper進行註冊,並彙報相關狀態,使Consumer及時獲取這些數據。當一個Consumer宕機後,其他Consumer會通過Zookeeper發現這一故障,並自動分攤對應的數據負載,觸發容錯機制。

 

(四)Kafka關鍵技術點

 

1.提供可控的可靠性級別:

Producer可通過兩種方式向Broker發送數據:同步或異步,其中異步方式通過批處理來處理數據,大大提高了數據的寫入效率。當Producer向Broker發送數據時,可通過設置該數據的應答方式,控制寫性能與可靠性級別。當可靠性級別提升時,寫性能會下降;反之,可靠性級別下降時,寫性能會提高很多。Kafka提供三種消息應答方式:

0:無需對消息進行確認,Producer發送消息後馬上返回,無需等待對方寫入成功;

1:當Producer發送消息後,需要等到Leader Partition寫成功後纔會返回,但對應的Follower Partition不一定寫成功,這種方式屬於性能可靠性比較折中的一種方式,能夠在比較高效的情況下,保證數據至少成功寫入一個節點;

2:當Producer發送消息後,需要等到所有的Partition寫成功後才返回,如果設置的消息副本數大於1,意味着被成功寫入了多個節點,可靠性很高,但寫性能比較低。

 

2.數據多副本:

Broker允許爲每個Topic中的數據存放多個副本,以達到容錯的目的。Kafka採用了強一致的數據複製策略。在數據存入時,會首先寫入到Leader Partition,之後由Leader Partition將消息同步給其他副本。Broker的負載均衡實際上就是對Leader Partition的負載均衡,即保證Leader Partition在各個Broker上數據儘可能相近。

 

3.高效的持久化機制:

爲了應對大數據的應用場景,Broker直接將消息持計劃到磁盤上而不是內存中,這就要求必須採用非常高效的數據寫入和存儲方式。由於順序寫入的速度要遠高於隨機寫,因此Kafka用順序寫配合Offset的方式組織數據,能夠達到很好的讀寫速度。

 

4.數據傳輸優化:

爲了優化Broker與Consumer之間的網絡數據傳輸效率,Kafka引入了比較多的優化技術,最典型的是批處理和Zero-copy。

批處理:爲了降低單條消息傳輸帶來的網絡開銷,Broker將多條消息組裝在一起,一併發送給Consumer,並且將格式進行了統一設計,保證了數據存儲和發送時的一致,避免額外轉換帶來的開銷。

Zero-copy:一條數據在磁盤上從讀取到發送需要經過四次拷貝與兩次系統調用,四次拷貝順序依次爲:內核態reader buffer – 用戶態應用程序buffer – 內核態socket buffer – 網卡NIC buffer,通過Zero-copy優化之後,數據只需要經過三次拷貝便可以發送出去,省去了用戶態應用程序buffer的過程。

 

5.可控的消息傳遞語義:

在消息隊列中,根據接受者可能受到的重複消息次數,消息傳遞語義可以分爲三種:

1.at most once:發送者將消息發送給消費者後,立即返回,不關心消費者是否成功收到消息;

2.at least once:發送者將消息發送給消費者後,等待確認,如果未收到確認消息,則會重發消息;

3.exactly once:消費者會且只會收到同一條消息一次,通常有兩種方式實現這種語義:兩段鎖協議和支持冪等操作。

 

(五)Kafka示例任務

 

Step1:下載代碼

tar -xzf kafka_2.11-1.0.0.tgz
cd kafka_2.11-1.0.0

Step2:啓動服務器

> bin/zookeeper-server-start.sh config/zookeeper.properties
[2013-04-22 15:01:37,495] INFO Reading configuration from: config/zookeeper.properties (org.apache.zookeeper.server.quorum.QuorumPeerConfig)
...

> bin/kafka-server-start.sh config/server.properties
[2013-04-22 15:01:47,028] INFO Verifying properties (kafka.utils.VerifiableProperties)
[2013-04-22 15:01:47,051] INFO Property socket.send.buffer.bytes is overridden to 1048576 (kafka.utils.VerifiableProperties)
...

Step3:創建一個Topic

> bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test

> bin/kafka-topics.sh --list --zookeeper localhost:2181

test

Step4:發送一些消息

> bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test
This is a message
This is another message

Step5:啓動一個Consumer

> bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning
This is a message
This is another message

Step6:設置多代理集羣

> cp config/server.properties config/server-1.properties
> cp config/server.properties config/server-2.properties

config/server-1.properties:
    broker.id=1
    listeners=PLAINTEXT://:9093
    log.dir=/tmp/kafka-logs-1
 
config/server-2.properties:
    broker.id=2
    listeners=PLAINTEXT://:9094
    log.dir=/tmp/kafka-logs-2

> bin/kafka-server-start.sh config/server-1.properties &
...
> bin/kafka-server-start.sh config/server-2.properties &
...

> bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 3 --partitions 1 --topic my-replicated-topic


> bin/kafka-topics.sh --describe --zookeeper localhost:2181 --topic my-replicated-topic
Topic:my-replicated-topic   PartitionCount:1    ReplicationFactor:3 Configs:
    Topic: my-replicated-topic  Partition: 0    Leader: 1   Replicas: 1,2,0 Isr: 1,2,0

Java Producer示例

public class Producer {
    public static String topic = "duanjt_test";//定義主題

    public static void main(String[] args) throws InterruptedException {
        Properties p = new Properties();
        p.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.23.76:9092,192.168.23.77:9092");//kafka地址,多個地址用逗號分割
        p.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        p.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<>(p);

        try {
            while (true) {
                String msg = "Hello," + new Random().nextInt(100);
                ProducerRecord<String, String> record = new ProducerRecord<String, String>(topic, msg);
                kafkaProducer.send(record);
                System.out.println("消息發送成功:" + msg);
                Thread.sleep(500);
            }
        } finally {
            kafkaProducer.close();
        }

    }
}

Java Consumer示例

public class Consumer {
    public static void main(String[] args) {
        Properties p = new Properties();
        p.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.23.76:9092");
        p.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        p.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        p.put(ConsumerConfig.GROUP_ID_CONFIG, "duanjt_test");

        KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(p);
        kafkaConsumer.subscribe(Collections.singletonList(Producer.topic));// 訂閱消息

        while (true) {
            ConsumerRecords<String, String> records = kafkaConsumer.poll(100);
            for (ConsumerRecord<String, String> record : records) {
                System.out.println(String.format("topic:%s,offset:%d,消息:%s", //
                        record.topic(), record.offset(), record.value()));
            }
        }
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章