日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

探討kafka的分區(qū)數(shù)與多線程消費

 陳永正的圖書館 2016-11-18

若要轉(zhuǎn)載,請標(biāo)明出處,謝謝!

=========================================================

首先,容我吐一口老血。。。。。。叫喊

kafka算是很麻煩的一件事兒,起因是最近需要采集大量的數(shù)據(jù),原先是只用了典型的high-level Consumer的API,最經(jīng)典的不過如下:

 

 Properties props = new Properties();    
        props.put("zookeeper.connect", "xxxx:2181");    
        props.put("zookeeper.connectiontimeout.ms", "1000000");    
        props.put("group.id", "test_group");  
        props.put("zookeeper.session.timeout.ms", "40000");  
        props.put("zookeeper.sync.time.ms", "200");  
        props.put("auto.commit.interval.ms", "1000");  
    
        ConsumerConfig consumerConfig = new ConsumerConfig(props);    
        ConsumerConnector consumerConnector = Consumer.createJavaConsumerConnector(consumerConfig);    
    
     
        Map<String, Integer> topicCountMap = new HashMap<String, Integer>();    
        topicCountMap.put("test", new Integer(1));    
        //key--topic    
        Map<String, List<KafkaStream<byte[], byte[]>>> consumerMap =   
                consumerConnector.createMessageStreams(topicCountMap);  
        KafkaStream<byte[], byte[]> stream = consumerMap.get("test").get(0);  
        ConsumerIterator<byte[], byte[]> it = stream.iterator();  
        StringBuffer sb = new StringBuffer();  
        while(it.hasNext()) {  
            try {  
                String msg = new String(it.next().message(), "utf-8").trim();  
                System.out.println("receive:" + msg);  
            } catch (UnsupportedEncodingException e) {  
                e.printStackTrace();  
            }  
        }  

 這是典型的kafka消費端消費數(shù)據(jù)的代碼,但可以看出這是十分典型的單線程消費。在本地玩玩熟悉kafka還行,(就跟入門java學(xué)會寫main方法打印hello world一樣~~~~),問題是學(xué)的東西必須真正應(yīng)用到實際中,你不可能只在單線程采集里原地打轉(zhuǎn)吧。。so,多線程采集迫在眉急?。?!

 

 

本人研究卡夫卡多線程消費還是耗了一段時間的,希望把過程盡可能完整地記錄下來,以便各位同行有需要可以參考。。

 

首先,最好理解kafka的基本原理和一些基本概念,閱讀官網(wǎng)文檔很有必要,這樣才會有一個比較清晰的概念,而不是跟無頭蒼蠅一樣亂撞——出了錯去網(wǎng)上查是灰常痛苦滴?。?/p>

http://kafka./documentation.html

 

好了,大概說下卡夫卡的“分區(qū)·”的概念吧:



 這張圖比較清晰地描述了“分區(qū)”的概念,對于某一個topic的消息來說,我們可以把這組消息發(fā)送給若干個分區(qū),就相當(dāng)于一組消息分發(fā)一樣。

分區(qū)、Offset、消費線程、group.id的關(guān)系

1)一組(類)消息通常由某個topic來歸類,我們可以把這組消息“分發(fā)”給若干個分區(qū)(partition),每個分區(qū)的消息各不相同;

2)每個分區(qū)都維護著他自己的偏移量(Offset),記錄著該分區(qū)的消息此時被消費的位置;

3)一個消費線程可以對應(yīng)若干個分區(qū),但一個分區(qū)只能被具體某一個消費線程消費;

4)group.id用于標(biāo)記某一個消費組,每一個消費組都會被記錄他在某一個分區(qū)的Offset,即不同consumer group針對同一個分區(qū),都有“各自”的偏移量。

 

說完概念,必須要注意的一點是,必須確認(rèn)卡夫卡的server.properties里面的一個屬性num.partitions必須被設(shè)置成大于1的值,否則消費端再怎么折騰,也用不了多線程哦。我這里的環(huán)境下,該屬性值被設(shè)置成10了。

 

重構(gòu)一下上述經(jīng)典的消費端代碼:

 

public class KafakConsumer implements Runnable {
	
	private ConsumerConfig consumerConfig;
	private static String topic="blog";
	Properties props;
	final int a_numThreads = 6;
	
	public KafakConsumer() {
		props = new Properties();    
        props.put("zookeeper.connect", "xxx:2181,yyy:2181,zzz:2181");  
//		props.put("zookeeper.connect", "localhost:2181");    
//        props.put("zookeeper.connectiontimeout.ms", "30000");    
        props.put("group.id", "blog");  
        props.put("zookeeper.session.timeout.ms", "400");
        props.put("zookeeper.sync.time.ms", "200");
        props.put("auto.commit.interval.ms", "1000"); 
        props.put("auto.offset.reset", "smallest");
        consumerConfig = new ConsumerConfig(props);
	}
	

	@Override
	public void run() {
		
	    Map<String, Integer> topicCountMap = new HashMap<String, Integer>();
	    topicCountMap.put(topic, new Integer(a_numThreads));
	    ConsumerConfig consumerConfig = new ConsumerConfig(props);
		ConsumerConnector consumer = kafka.consumer.Consumer.createJavaConsumerConnector(consumerConfig);
	    Map<String, List<KafkaStream<byte[], byte[]>>> consumerMap = consumer.createMessageStreams(topicCountMap);
	    List<KafkaStream<byte[], byte[]>> streams = consumerMap.get(topic);
	    ExecutorService executor = Executors.newFixedThreadPool(a_numThreads);
	    for (final KafkaStream stream : streams) {
	        executor.submit(new KafkaConsumerThread(stream));
	    }

	}
	

	public static void main(String[] args) {
		System.out.println(topic);
		Thread t = new Thread(new KafakConsumer());
		t.start();
	}
}

 從這段重構(gòu)的代碼可以看出,KafkaStream<byte[], byte[]> stream = consumerMap.get("test").get(0); 這行代碼已被廢棄,得到List<KafkaStream<byte[], byte[]>>之后不再是得到他的頭元素get(0)就ok了,而且topicCountMap.put(topic, new Integer(a_numThreads));的第二個參數(shù)也不再是1.

 

 其中,具體消費線程KafkaConsumerThread代碼為:

public class KafkaConsumerThread implements Runnable {
	
	private KafkaStream<byte[], byte[]> stream;
	
	public KafkaConsumerThread(KafkaStream<byte[], byte[]> stream) {
		this.stream = stream;
	}

	@Override
	public void run() {
		ConsumerIterator<byte[], byte[]> it = stream.iterator();
		while (it.hasNext()) {
			MessageAndMetadata<byte[], byte[]> mam = it.next();
			System.out.println(Thread.currentThread().getName() + ": partition[" + mam.partition() + "]," 
			+ "offset[" + mam.offset() + "], " + new String(mam.message()));

		}
	}

}

 

 

編寫生產(chǎn)端(Producer)的代碼:

 

public class KafkaProducer implements Runnable {
	
	private Producer<String, String> producer = null;
	
	private ProducerConfig config = null;
	
	
	public KafkaProducer() {
		Properties props = new Properties();
		props.put("zookeeper.connect", "*****:2181,****:2181,****:2181");
		
//		props.put("zookeeper.connect", "localhost:2181");
		
		// 指定序列化處理類,默認(rèn)為kafka.serializer.DefaultEncoder,即byte[]
		props.put("serializer.class", "kafka.serializer.StringEncoder");

		// 同步還是異步,默認(rèn)2表同步,1表異步。異步可以提高發(fā)送吞吐量,但是也可能導(dǎo)致丟失未發(fā)送過去的消息
		props.put("producer.type", "sync");

		// 是否壓縮,默認(rèn)0表示不壓縮,1表示用gzip壓縮,2表示用snappy壓縮。壓縮后消息中會有頭來指明消息壓縮類型,故在消費者端消息解壓是透明的無需指定。
		props.put("compression.codec", "1");

		// 指定kafka節(jié)點列表,用于獲取metadata(元數(shù)據(jù)),不必全部指定
		props.put("broker.list", "****:6667,***:6667,****:6667");
		
		config = new ProducerConfig(props);
	}

	@Override
	public void run() {
		producer = new Producer<String, String>(config);
//		for(int i=0; i<10; i++) {
//			String sLine = "I'm number " + i;
//			KeyedMessage<String, String> msg = new KeyedMessage<String, String>("group1", sLine);
//			producer.send(msg);
//		}
		for(int i = 1; i <= 6; i++){ //往6個分區(qū)發(fā)數(shù)據(jù) 
		      List<KeyedMessage<String, String>> messageList = new ArrayList<KeyedMessage<String, String>>();  
		      for(int j = 0; j < 6; j++){ //每個分區(qū)6條訊息 
		    	  messageList.add(new KeyedMessage<String, String>
		    	  //String topic, String partition, String message
		    	  	("blog", "partition[" + i + "]", "message[The " + i + " message]"));
		      }  
		      producer.send(messageList);  
		    }  
 	
	}
	
	public static void main(String[] args) {
		Thread t = new Thread(new KafkaProducer());
		t.start();
	}
}

 上述生產(chǎn)端代碼相對傳統(tǒng)的發(fā)送端代碼也做了改進,首先是用了批量發(fā)送(源碼):

 

 

public void send(List<KeyedMessage<K, V>> messages)
  {
    underlying().send(JavaConversions..MODULE$.asScalaBuffer(messages).toSeq());
  }

 而不是:

 

 

public void send(KeyedMessage<K, V> message)
  {
    underlying().send(Predef..MODULE$.wrapRefArray((Object[])new KeyedMessage[] { message }));
  }

 第二,KeyedMessage用的構(gòu)造函數(shù):

 

 

public KeyedMessage(String topic, K key, V message) { this(topic, key, key, message); }

第二個參數(shù)表示分區(qū)的key。 

 

而非:

 

public KeyedMessage(String topic, V message) {
    this(topic, null, null, message);
  }

 

 

分別run一下生產(chǎn)和消費的代碼,可以看到消費端打印結(jié)果:

pool-2-thread-5: partition[5],offset[0], message[The 5 message]

pool-2-thread-1: partition[2],offset[0], message[The 2 message]

pool-2-thread-2: partition[1],offset[0], message[The 1 message]

pool-2-thread-5: partition[4],offset[0], message[The 4 message]

pool-2-thread-1: partition[3],offset[0], message[The 3 message]

pool-2-thread-4: partition[6],offset[0], message[The 6 message]

 

可以看到,6個分區(qū)的數(shù)據(jù)全部被消費了,但是不妨看下消費線程:pool-2-thread-1線程同時消費了partition[2]和partition[3]的數(shù)據(jù);pool-2-thread-2消費了partiton[1]的數(shù)據(jù);pool-2-thread-4消費了partiton[6]的數(shù)據(jù);而pool-2-thread-5則消費了partitoin[4]和partition[5]的數(shù)據(jù)。

 

從上述消費情況來看,驗證了消費線程和分區(qū)的對應(yīng)情況——即:一個分區(qū)只能被一個線程消費,但一個消費線程可以消費多個分區(qū)的數(shù)據(jù)!雖然我指定了線程池的線程數(shù)為6,但并不是所有的線程都去消費了,這當(dāng)然跟線程池的調(diào)度有關(guān)系了。并不是一個消費線程對應(yīng)地去消費一個分區(qū)的數(shù)據(jù)。

 

我們不妨仔細(xì)看下消費端啟動日志部分,這對我們理解kafka的啟動生成和消費的原理有益:

【限于篇幅,啟動日志略,只分析關(guān)鍵部分】

 

消費端的啟動日志表明:

1)Consumer happy_Connor-PC-1445916157267-b9cce79d rebalancing the following partitions: ArrayBuffer(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) for topic blog with consumers: List(happy_Connor-PC-1445916157267-b9cce79d-0, happy_Connor-PC-1445916157267-b9cce79d-1, happy_Connor-PC-1445916157267-b9cce79d-2, happy_Connor-PC-1445916157267-b9cce79d-3, happy_Connor-PC-1445916157267-b9cce79d-4, happy_Connor-PC-1445916157267-b9cce79d-5)

happy_Connor-PC-1445916157267-b9cce79d表示一個消費組,該topic可以使用10個分區(qū):the following partitions: ArrayBuffer(0, 1, 2, 3, 4, 5, 6, 7, 8, 9) for topic blog。然后定義了6個消費線程,List(happy_Connor-PC-1445916157267-b9cce79d-0, happy_Connor-PC-1445916157267-b9cce79d-1, happy_Connor-PC-1445916157267-b9cce79d-2, happy_Connor-PC-1445916157267-b9cce79d-3, happy_Connor-PC-1445916157267-b9cce79d-4, happy_Connor-PC-1445916157267-b9cce79d-5)。消費線程的個數(shù)由topicCountMap.put(String topic, Integer count)的第二個參數(shù)決定。但真正去消費的線程還是由線程池的調(diào)度機制來決定;

2)線程由zookeeper來聲明它擁有1個或多個分區(qū);

3)真正有數(shù)據(jù)存在的分區(qū)是由生產(chǎn)發(fā)送端來決定,即使你的kafka設(shè)置了10個分區(qū),消費端在消費的時候,消費線程雖然會根據(jù)zookeeper的某種機制來聲明它所消費的分區(qū),但實際消費過程中,還是會消費真正存在數(shù)據(jù)的分區(qū)。(本例中,你只往6個分區(qū)push了數(shù)據(jù),所以即使你聲明了10個分區(qū),你也只能消費6個分區(qū)的數(shù)據(jù))。

 

如果把topicCountMap的第二個參數(shù)Integer值改成1,發(fā)送端改成往7個分區(qū)發(fā)數(shù)據(jù)再測試,可得到消費端的打印結(jié)果:

pool-2-thread-1: partition[6],offset[0], message[The 6 message]
pool-2-thread-1: partition[3],offset[0], message[The 3 message]
pool-2-thread-1: partition[2],offset[0], message[The 2 message]
pool-2-thread-1: partition[5],offset[0], message[The 5 message]
pool-2-thread-1: partition[4],offset[0], message[The 4 message]
pool-2-thread-1: partition[7],offset[0], message[The 7 message]
pool-2-thread-1: partition[1],offset[0], message[The 1 message]

 可以看出,如果你topicCountMap的值改成1,而 List<KafkaStream<byte[], byte[]>>的size由Integer值決定,此時為1,可以看出,線程池中只能使用一個線程來發(fā)送,還是單線程的效果。若要用多線程消費,Integer的值必須大于1.

 

下面再來模擬一些狀況:

狀況一:往大于實際分區(qū)數(shù)的分區(qū)發(fā)數(shù)據(jù),比如發(fā)送端的第一層循環(huán)設(shè)為11:

可看到消費端此時雖能正常的完全消費這10個分區(qū)的數(shù)據(jù),但生產(chǎn)端會報異常:

No partition metadata for topic blog4 due to kafka.common.LeaderNotAvailableException}] for topic [blog4]: class kafka.common.LeaderNotAvailableException 

這說明,你往partition11發(fā)送失敗,因為卡夫卡已經(jīng)設(shè)置了10個分區(qū),你再往不存在的分區(qū)數(shù)發(fā)當(dāng)然會報錯了。

 

狀況二:發(fā)送端用傳統(tǒng)的發(fā)送方法,即KeyedMessage的構(gòu)造函數(shù)只有topic和Message

 

//針對topic創(chuàng)建一個分區(qū)并發(fā)送數(shù)據(jù)
		List<KeyedMessage<String, String>> messageList = new ArrayList<KeyedMessage<String, String>>();
		for(int i = 1; i <= 10; i++){
			messageList.add(new KeyedMessage<String, String>("blog6",  "我是發(fā)送的內(nèi)容message"+i));
		} 
		producer.send(messageList); 

 

消費端打印結(jié)果:

pool-2-thread-1: partition[7],offset[0], 我是發(fā)送的內(nèi)容message1

pool-2-thread-1: partition[7],offset[1], 我是發(fā)送的內(nèi)容message2

pool-2-thread-1: partition[7],offset[2], 我是發(fā)送的內(nèi)容message3

pool-2-thread-1: partition[7],offset[3], 我是發(fā)送的內(nèi)容message4

pool-2-thread-1: partition[7],offset[4], 我是發(fā)送的內(nèi)容message5

pool-2-thread-1: partition[7],offset[5], 我是發(fā)送的內(nèi)容message6

pool-2-thread-1: partition[7],offset[6], 我是發(fā)送的內(nèi)容message7

pool-2-thread-1: partition[7],offset[7], 我是發(fā)送的內(nèi)容message8

pool-2-thread-1: partition[7],offset[8], 我是發(fā)送的內(nèi)容message9

pool-2-thread-1: partition[7],offset[9], 我是發(fā)送的內(nèi)容message10

 

這表明,只用了1個消費線程消費1個分區(qū)的數(shù)據(jù)。這說明,如果發(fā)送端發(fā)送數(shù)據(jù)沒有指定分區(qū),即用的是

public KeyedMessage(String topic,V message) { this(topic, key, key, message); }
sendMessage(KeyedMessage(String topic,V message))

的話,同樣達不到多線程消費的效果!

 

狀況三:將線程池的大小設(shè)置成比topicCountMap的value值?。?/p>

  topicCountMap.put(topic, new Integer(7));
  //......................
  ExecutorService executor = Executors.newFixedThreadPool(5);

 發(fā)送端往9個分區(qū)發(fā)送數(shù)據(jù),run一下,會發(fā)現(xiàn)消費端打印結(jié)果:

pool-2-thread-3: partition[7],offset[0], message[The 7 message]

pool-2-thread-5: partition[1],offset[0], message[The 1 message]

pool-2-thread-4: partition[4],offset[0], message[The 4 message]

pool-2-thread-2: partition[3],offset[0], message[The 3 message]

pool-2-thread-4: partition[5],offset[0], message[The 5 message]

pool-2-thread-1: partition[8],offset[0], message[The 8 message]

pool-2-thread-2: partition[2],offset[0], message[The 2 message]

你會發(fā)現(xiàn):雖然我生產(chǎn)發(fā)送端往9個分區(qū)發(fā)送了數(shù)據(jù),但實際上只消費掉了7個分區(qū)的數(shù)據(jù)。(如果你再跑一邊,可能又是6個分區(qū)的數(shù)據(jù))——這說明,有的分區(qū)的數(shù)據(jù)沒有被消費,原因只可能是線程不夠。so,當(dāng)線程池中的大小小于分區(qū)數(shù)時,會出現(xiàn)有的分區(qū)沒有被采集的情況。建議設(shè)置:實際發(fā)送分區(qū)數(shù)(一般就等于設(shè)置的分區(qū)數(shù))= topicCountMap的value = 線程池大小  否則極易出現(xiàn)reblance的異常?。?!

 

好了,折騰這么久。我們可以看出,卡夫卡如果想要多線程消費提高效率的話,就可以從分區(qū)數(shù)上下手,分區(qū)數(shù)就是用來做并行消費的而且生產(chǎn)端的發(fā)送代碼也很有講究。(這只是針對某一個topic而言,當(dāng)然實際情況中,你可以一個topic一個線程,同樣達到多線程效果,當(dāng)然這是后話了)

 

========================The end=====================

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多