kafka 文檔 (三)producer(生產者)和高級消費者

API

Producer API

此處只簡介一個procedure的例子

生產類是用來創建新消息的主題和可選的分區。

如果使用Java你需要包括幾個包和支持類:

import kafka.javaapi.producer.Producer;

import kafka.producer.KeyedMessage;

import kafka.producer.ProducerConfig;

 

第一步首先定義producer如何找到集羣,如何序列化消息和爲消息選擇適合的分區。下面吧這些定義在一個標準的JAVA  Properties類中

Properties props = new Properties();
 
props.put("metadata.broker.list","broker1:9092,broker2:9092");
props.put("serializer.class","kafka.serializer.StringEncoder");
props.put("partitioner.class","example.producer.SimplePartitioner");
props.put("request.required.acks","1");
ProducerConfig config = new ProducerConfig(props);


1.        metadata.broker.list 定義了生產者如何找到一個或多個Broker去確定每個topicLeader這不需要你設置集羣中全套的brokers但至少應包括兩個,第一個經紀人不可用可以替換。不需要擔心需要指出broker爲主題的領袖(分區),生產者知道如何連接到代理請求元數據並連接到正確的broker。

2.  第二個屬性“序列化類定義“。定義使用什麼序列化程序傳遞消息。在我們的例子中,我們使用一個卡夫卡提供的簡單的字符串編碼器。請注意,encoder必須和下一步的keyedmessage使用相同的類型

可以適當的定義"key.serializer.class"根據key改變序列化類。默認的是與"serializer.class"相同

3.        第三個屬性partitioner.class 定義了決定topic中的分區發送規則。這個屬性是可選的,但是對於你的特殊的分區實現是重要的。如果存在key將使用kafka默認的分組規則,如果keynull 則使用隨機的分區發送策略。

4.        最後一個屬性“request.required.acks”將設置kafka知否需要broker的迴應。如果不設置可能將導致數據丟失。

1.1         此處可以設置爲0 生產者不等待broker的迴應。會有最低能的延遲和最差的保證性(在服務器失敗後會導致信息丟失)

1.2         此處可以設置爲1生產者會收到leader的迴應在leader寫入之後。(在當前leader服務器爲複製前失敗可能會導致信息丟失)

1.3         此處可以設置爲-1生產者會收到leader的迴應在全部拷貝完成之後。

之後可以定義生產者

Producer<String, String> producer =new Producer<String, String>(config);

此處泛型的第一個type是分區的key的類型。第二個是消息的類型。與上面Properties中定義的對應。


現在定義messgae

Random rnd = new Random();
long runtime = new Date().getTime();
String ip = “192.168.2.” +rnd.nextInt(255);
String msg = runtime + “,www.example.com,”+ ip;

此處模擬一個website的訪問記錄。之後想broker中寫入信息.

KeyedMessage<String, String> data =new KeyedMessage<String, String>("page_visits",

ip, msg);

producer.send(data);

此處的“page_visits”是要寫入的Topic。此處我們將IP設置爲分區的key值。注意如果你沒有設置鍵值,即使你定義了一個分區類,kafka也將使用隨機發送.

Full Code:

import java.util.*;
 
import kafka.javaapi.producer.Producer;
import kafka.producer.KeyedMessage;
import kafka.producer.ProducerConfig;
 
public class TestProducer {
   public static void main(String[] args) {
       long events = Long.parseLong(args[0]);
       Random rnd = new Random();
 
       Properties props = new Properties();
       props.put("metadata.broker.list","broker1:9092,broker2:9092 ");
       props.put("serializer.class","kafka.serializer.StringEncoder");
       props.put("partitioner.class","example.producer.SimplePartitioner");
       props.put("request.required.acks", "1");
       ProducerConfig config = new ProducerConfig(props);
       Producer<String, String> producer = new Producer<String,String>(config);
       for (long nEvents = 0; nEvents < events; nEvents++) {
               long runtime = newDate().getTime(); 
               String ip = “192.168.2.” +rnd.nextInt(255);
               String msg = runtime +“,www.example.com,” + ip;
               KeyedMessage<String,String> data = new KeyedMessage<String, String>("page_visits",ip(key), msg);
               producer.send(data);
       }
       producer.close();
    }
}
 
Partitioning Code: (分區函數)
import kafka.producer.Partitioner;
import kafka.utils.VerifiableProperties;
public class SimplePartitioner implementsPartitioner<String> {
   public SimplePartitioner (VerifiableProperties props) {
    }
   public int partition(String key, int a_numPartitions) {
       int partition = 0;
       int offset = key.lastIndexOf('.');
       if (offset > 0) {
          partition = Integer.parseInt( key.substring(offset+1)) %a_numPartitions;
       }
      return partition;
  }
}

上面分區的作用是相同的IP將發送至相同的分區。但此時你的消費者需要知道如何去處理這樣的規則消息。

使用前需要建立topic

bin/kafka-create-topic.sh --topicpage_visits --replica 3 --zookeeper localhost:2181 --partition 5

可以使用下面的工具驗證你發送的消息

bin/kafka-console-consumer.sh --zookeeperlocalhost:2181 --topic page_visits --from-beginning

High Level Consumer API

頂層接口:

class Consumer {
/**
  *  創建java的消費者與kafka的connect
  *  @param config  至少需要提供consumer的groupId和zookeeper.connect.
   */
public statickafka.javaapi.consumer.ConsumerConnector createJavaConsumerConnector(config:ConsumerConfig);
}
ConsumerConnector:
public interfacekafka.javaapi.consumer.ConsumerConnector {
  /**
  *  爲每一個主題創建一個泛型的消息流
  *  @param topicCountMap  提供topic和Stream的一一對應
  *  @param decoder 解析器 
  *  @return Map   <topic ,List<#streams>>
  *                   此處的KafkaStream提供對內容的Iterable讀取
   */
 public <K,V> Map<String, List<KafkaStream<K,V>>>
    createMessageStreams(Map<String,Integer> topicCountMap, Decoder<K> keyDecoder, Decoder<V>valueDecoder);
 
  /**
  *  同上.
   */
 public Map<String, List<KafkaStream<byte[], byte[]>>>createMessageStreams(Map<String, Integer> topicCountMap);
 
  /**
   *          建一個匹配的通配符主題的消息流的List
  *  @param topicFilter一個topicfilter指定Consumer訂閱的話題(
  *  包含了一個白名單和黑名單).
  *  @param numStreams messagestreams的數量
  *  @param keyDecoder message key解析器
  *  @param valueDecoder a message解析器
  *  @return 同上
   */
 public <K,V> List<KafkaStream<K,V>>
   createMessageStreamsByFilter(TopicFilter topicFilter, int numStreams,Decoder<K> keyDecoder, Decoder<V> valueDecoder);
 
 ………………………….(其餘接口類似,是上述方法的重載方法)
 
  /**
  *  提交本連接器所連接的所有分區和主題
   */
 public void commitOffsets();
 
  /**
  *  停止當前Consumer
   */
 public void shutdown();
}

 

e.g  example

1. 爲什使用高級消費者(High Level Consumer)

         有時消費者從卡夫卡讀取消息不在乎處理消息的偏移量邏輯,只是消費消息內部的信息。高級消費者提供了消費信息的方法而屏蔽了大量的底層細節。

         首先要知道的是,高級消費者從zookeeper的特殊分區存儲最新偏離。這個偏移當kafka啓動時準備完畢。這一般是指消費者羣體(Consumer group[此處的意思,kafka中的消息是發送到Consumer group中的任一個consumer上的,kafka保存的是整體的偏移。此處不知是否理解正確請大蝦指點。]

         請小心,對於kafka集羣消費羣體的名字是全局的,任何的邏輯的消費者應該被關閉,然後運行新的代碼。當一個新的進程擁有相同的消費者羣的名字,卡夫卡將會增加進程的線程消費topic並且引發的重新平衡(reblannce在這個重新平衡中,卡夫卡將分配現有分區到所有可用線程,可能移動一個分區到另一個進程的消費分區。如果此時同時擁有舊的的新的代碼邏輯,將會有一部分邏輯進入舊得Consumer而另一部分進入新的Consumer中的情況.

2. Designing a High Level Consumer

瞭解使用高層次消費者的第一件事是,它可以(而且應該!)是一個多線程的應用。線程圍繞在你的主題分區的數量,有一些非常具體的規則:

1.       如果你提供比在主題分區多的線程數量,一些線程將不會看到消息

2.       如果你提供的分區比你擁有的線程多,線程將從多個分區接收數據

3.       如果你每個線程上有多個分區,對於你以何種順序收到消息是沒有保證的。舉個栗子,你可能從分區10上獲取5條消息和分區11上的6條消息,然後你可能一直從10上獲取消息,即使11上也擁有數據。

4.      添加更多的進程/線程將使卡夫卡重新平衡,可能改變一個分區到線程的分配。

這裏是一個簡單的消費者栗子:

package com.test.groups;
 
import kafka.consumer.ConsumerIterator;
import kafka.consumer.KafkaStream;
 
public class ConsumerTest implements Runnable {
    privateKafkaStream m_stream;
    private intm_threadNumber;
 
    publicConsumerTest(KafkaStream a_stream, int a_threadNumber) {
       m_threadNumber = a_threadNumber;
        m_stream =a_stream;
    }
 
    public void run() {
       ConsumerIterator<byte[], byte[]> it = m_stream.iterator();
        while(it.hasNext())
System.out.println("Thread " + m_threadNumber+ ": " + new String(it.next().message()));
       System.out.println("Shutting down Thread: " + m_threadNumber);
    }
}

在這裏有趣的是,(it.hasnext())。這個代碼將從卡夫卡讀取直到你停下來。

3. Config

不像simpleconsumer高層消費者爲你很多的提供需要bookkeeping(?)和錯誤處理。但是你要告訴卡夫卡這些信息。下面的方法定義了創建高級消費者基礎配置:

private static ConsumerConfigcreateConsumerConfig(String a_zookeeper, String a_groupId) {
        Propertiesprops = new Properties();
       props.put("zookeeper.connect", a_zookeeper);
        props.put("group.id", a_groupId);
       props.put("zookeeper.session.timeout.ms", "400");
       props.put("zookeeper.sync.time.ms", "200");
       props.put("auto.commit.interval.ms", "1000");
        return newConsumerConfig(props);
    }

zookeeper.connect  指定zookeeper集羣中的一個實例,kafka利用zookeeper儲存topic的分區偏移值。

Groupid 消費者所屬的Consumer Group(消費者羣體)

zookeeper.session.timeout.ms zookeeper的超時處理

auto.commit.interval.ms   屬性自動提交的間隔。這將替代消息被消費後提交。如果發生錯誤,你將從新獲得未更新的消息。

4.使用線程池處理消息

public void run(int a_numThreads) {
   Map<String, Integer> topicCountMap = new HashMap<String,Integer>();
   topicCountMap.put(topic, new Integer(a_numThreads));
   Map<String, List<KafkaStream<byte[], byte[]>>>consumerMap = consumer.createMessageStreams(topicCountMap);
   List<KafkaStream<byte[], byte[]>> streams =consumerMap.get(topic);
   // now launch all the threads
   executor = Executors.newFixedThreadPool(a_numThreads);
 
   // now create an object to consume the messages
   int threadNumber = 0;
   for (final KafkaStream stream : streams) {
       executor.submit(new ConsumerTest(stream, threadNumber));
       threadNumber++;
    }
}

首先我們創建一個map,告訴kafka提供給哪個topic多少線程。consumer.createmessagestreams是我們如何把這個信息傳遞給卡夫卡。返回的是一個包含kafkastream 的以topic 爲鍵listmap結合。(注意,這裏我們只向卡夫卡註冊一個話題,但我們可以爲map中多添加一個元素的)

最後,我們創建的線程池和通過一項新的consumertest對象,每個線程運轉我們的業務邏輯。

5.   清理和異常處理

Kafka在每次處理後不會立即更新zookeeper上的偏移值,她會休息上一段時間後提交。在這段時間內,你的消費者可能已經消費了一些消息,但並沒有提交到zookeeper上。這樣你可能會重複消費數據。

同時一些時候,broker失敗從新選取leader是也可能會導致重複消費消息。

爲了避免這種情況應該清理完成後再關閉,而不是直接使用kill命令。

e.g

try {
   Thread.sleep(10000);
} catch (InterruptedException ie) {
}
example.shutdown();

full code

package com.test.groups;
 
import kafka.consumer.ConsumerConfig;
import kafka.consumer.KafkaStream;
importkafka.javaapi.consumer.ConsumerConnector;
 
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
importjava.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class ConsumerGroupExample {
   private final ConsumerConnector consumer;
   private final String topic;
   private  ExecutorService executor;
 
   public ConsumerGroupExample(String a_zookeeper, String a_groupId, Stringa_topic) {
       consumer = kafka.consumer.Consumer.createJavaConsumerConnector(
               createConsumerConfig(a_zookeeper, a_groupId));
       this.topic = a_topic;
    }
 
   public void shutdown() {
       if (consumer != null) consumer.shutdown();
       if (executor != null) executor.shutdown();
    }
 
   public void run(int a_numThreads) {
       Map<String, Integer> topicCountMap = new HashMap<String,Integer>();
       topicCountMap.put(topic, new Integer(a_numThreads));
       Map<String, List<KafkaStream<byte[], byte[]>>>consumerMap = consumer.createMessageStreams(topicCountMap);
       List<KafkaStream<byte[], byte[]>> streams =consumerMap.get(topic);
 
       // now launch all the threads
       //
       executor = Executors.newFixedThreadPool(a_numThreads);
 
       // now create an object to consume the messages
       //
       int threadNumber = 0;
       for (final KafkaStream stream : streams) {
           executor.submit(new ConsumerTest(stream, threadNumber));
           threadNumber++;
       }
    }
 
   private static ConsumerConfig createConsumerConfig(String a_zookeeper,String a_groupId) {
       Properties props = new Properties();
       props.put("zookeeper.connect", a_zookeeper);
       props.put("group.id", a_groupId);
       props.put("zookeeper.session.timeout.ms", "400");
       props.put("zookeeper.sync.time.ms", "200");
       props.put("auto.commit.interval.ms", "1000");
 
       return new ConsumerConfig(props);
    }
 
   public static void main(String[] args) {
       String zooKeeper = args[0];
       String groupId = args[1];
       String topic = args[2];
       int threads = Integer.parseInt(args[3]);
 
       ConsumerGroupExample example = new ConsumerGroupExample(zooKeeper,groupId, topic);
       example.run(threads);
 
       try {
           Thread.sleep(10000);
       } catch (InterruptedException ie) {
 
       }
       example.shutdown();
    }
}

此處的啓動命令需提供

server01.myco.com1:2181 group3   myTopic  4

1.      server01.myco.com1:2181 zookeeper 的端口和地址

2.      group3   Consumer Group Name

3.      myTopic  consumer消費消息的message

4.      消費topic的線程數


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