若要轉(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ū)、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===================== |
|