一、Kafka Producer
import kafka.producer.Partitioner;
import kafka.utils.VerifiableProperties;
public class HashPartitioner implements Partitioner {
public HashPartitioner(VerifiableProperties verifiableProperties) {}
@Override
public int partition(Object key, int numPartitions) {
if ((key instanceof Integer)) {
return Math.abs(Integer.parseInt(key.toString())) % numPartitions;
}
return Math.abs(key.hashCode() % numPartitions);
}
}
import java.util.Properties;
import kafka.javaapi.producer.Producer;
import kafka.producer.KeyedMessage;
import kafka.producer.ProducerConfig;
import kafka.serializer.StringEncoder;
public class KafkaProducer {
static private final String TOPIC = "topic1";
static private final String BROKER_LIST = "192.168.56.21:9092";
public static void main(String[] args) throws Exception {
Producer<String, String> producer = initProducer();
sendOne(producer, TOPIC);
producer.close();
}
/**
* Product泛型:第一个是发送的消息的key的类型,第二个是Payload的类型
* @return
*/
private static Producer<String, String> initProducer() {
Properties props = new Properties();
props.put("metadata.broker.list", BROKER_LIST);
// props.put("serializer.class", "kafka.serializer.StringEncoder");
// 序列话方式,Payload最后以二进制方式存在文件中,所以要指定如何把我们的数据转换成字节数组
// key也有序列方式,默认不设置的话和payload的一样
props.put("serializer.class", StringEncoder.class.getName());
props.put("partitioner.class", HashPartitioner.class.getName());
// props.put("partitioner.class", "kafka.producer.DefaultPartitioner");
// props.put("compression.codec", "0");
// 指定发送类型,sync/async;之后的参数都是给异步用的
props.put("producer.type", "sync");
props.put("batch.num.messages", "3");
props.put("queue.buffer.max.ms", "1000");
props.put("queue.buffering.max.messages", "1000");
props.put("queue.enqueue.timeout.ms", "2000");
ProducerConfig config = new ProducerConfig(props);
Producer<String, String> producer = new Producer<String, String>(config);
return producer;
}
public static void sendOne(Producer<String, String> producer, String topic) throws InterruptedException {
for (int i = 0; i < 20; i++) {
KeyedMessage<String, String> message = new KeyedMessage<String, String>(topic, i + "", "test " + i);
producer.send(message);
//Thread.sleep(1000);
}
System.out.println("send over");
}
}
二、Kafka Consumer
kafka Consumer在消费消息时的消费顺序是同一个partition中按照接收顺序去消费,并且这个顺序是broker接收顺序,而不是producer发送顺序,是partiton级别的顺序消费,但是不同的partition之间是不保证顺序消费的。另外,在0.8及之前的版本中,Consumer分为high level和low level两种。
2.1 High Level Consumer
2.1.1 Consumer Group
每个High Level Consumer实例都属于一个Consumer Group,实例化时,必须用group.id配置项来指定。启动Consumer之后会在Zookeeper上创建/consumers/[group.id]节点。Consumer Group是整个Kafka集群全局唯一的,而非针对某个Topic的;而且对于每条消息,只能被同一个Consumer Group里的一个Consumer消费;不同Consumer Group中的多个Consumer可消费同一条消息。
2.1.2 Consumer Rebalance
High Level Consumer启动时将其ID(由配置项consumer.id指定,没指定的话则自动生成一个)注册到所属的Consumer Group下,在Zookeeper上的路径为/consumers/[group.id]/ids/[group.id_consumer.id],并在/consumers/[group.id]/ids上注册Watcher,在/brokers/ids上注册Watcher;如果Consumer通过Topic Filter创建消息流,则还会在/brokers/topics上也注册Watcher;然后强制自己在其Consumer Group内启动Rebalance流程,另外,在consumer运行过程中,一旦注册的这些Watcher被触发后也会做Rebalance,Consumer Rebalance算法为:
- 将目标Topic下的所有Partirtion排序,存于Pt
- 对某Consumer Group下所有Consumer排序,存于Cg,第i个Consumer记为Ci(i从0开始)
- N=size(Pt)/size(Cg),向上取整
- 解除Ci对原来分配的Partition的消费权
- 将第i * N到(i + 1) * N − 1个Partition分配给Ci
所以,Rebalance的结果就是同一个partition的消息只会被相同Consumer Group中的一个consumer去消费,或者说将topic的数据分配给不同的consumer时,是按照partition来分配的,而不是按照消息去分配的,再换句话说只要确定了某个partition是被某个consumer消费的,那这个partition 里面的所有数据都是被这个consumer消费的;既然是按照partition分配的,所以当一个Consumer Group中的consumer数量多于某个Topic的partiton数量的时候,有些consumer实际上是分配不到消费权的,也就不会消费到任何消息。
如果不做Rebalance,假如正在消费的consumer挂了,那么这部分数据就没consumer去消费了,或者新加了一个consumer,也不能做到负载均衡;Consumer Rebalance要的效果是尽可能地将不同的partition均匀分配给不同的consumer。另外,这种Rebalance具有以下的一些缺点:
- Herd Effect:任何Broker或者Consumer的增减都会触发Rebalance
- Split Brain:每个Consumer分别单独通过Zookeeper判断哪些Broker和Consumer宕机,同时Consumer在同一时刻从Zookeeper看到的View可能不完全一样,这是由Zookeeper的特性决定的
- 调整结果不可控:所有Consumer分别进行Rebalance,彼此不知道对应的Rebalance是否成功;可能造成本来应该分配给这个consumer的partition,Rebalance失败了,就没有拿到消费权,就会造成这个partition暂时不能被消费;当然kafaka对Rebalance有一些重试机制,也可以由参数来控制可以重试的次数,每次的间隔是多少等等
- 每个consumer都要去做一些监控,Zookeeper的压力也会比较大;而且每个consumer都要自己去做响应的Rebalance逻辑,每个consumer自己做决策,虽然算法一样,理论上最终的结果都一样,但是很有可能造成脑裂的情况;所以从整个集群来说最终的结果可能就不一样
2.1.3 Log Compaction
High Level Consumer将从某个Partition读取的最后一条消息的offset存于Zookeeper中,以记录上次消费某个Partition中的最后一条消息;且这个offset是基于客户程序提供给Kafka的名字来保存的,这个名字就是Consumer Group。
由于Zookeeper的读性能优于写,如果有大量的consumer,则会大量的写Zookeeper,这样会造成Zookeeper的负载大,甚至成为kafka的瓶颈,所以不希望完全依赖于Zookeeper;另外为了实现更多的其它功能,更多的对offset的把控,所以从0.8.2开始同时支持将offset存于Zookeeper中和专用的Kafka Topic中。当存储于专用Topic时,将已消费消息的offset作为payload存储,groupid + topicname + partitionid组合作为key,可以通过这种方式读到每个Topic的partiton对于某个Conumser Group已消费消息的offset是多少。
另外,这个专用的Topic的数据清除策略不是采用普通Topic按时间和大小策略来清除的,也不能采用这种清除方案,因为有可能某个partition对应的Consumer Group中的某个consumer已消费的消息的offset一直没更新,已消费的消息的offset还存在于最旧的segment中,如果安装普通Topic的清除策略直接删除掉整个segment,后来想去读这个consumer对这个partition的已消费的消息的offset是多少就读取不到了。另外,假如已经写了很多的offset到专用的Topic中,当某个consumer重启之后可能就需要遍历全部数据才能找到最近已读取的消息的offset,这样对于启动consumer来说就很慢了,所以基于这些原因,专用Topic采用Log Compaction(压缩)的方式,对key相同的数据只保留最后一条数据。Log Compaction的效果在于:Cleaner Point(清除点)左边的offset不是连续的,右边的offset是连续的,因为左边的就是被Log Comaction之后的结果,右边的是不会被处理的,因为右边部分可能就是正在写的最新的sgmenet。所以对于Log Compaction而言,不会处理最新的segment。在Log Compaction之后的数据就很少了,在配合上一些索引文件,就可以很方便的得到某个Consumer Group对于某个Partition已消费消息的最大offset是多少。
2.1.4 High Level Consumer例子
package com.cenmee.tech.kafka;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import kafka.consumer.Consumer;
import kafka.consumer.ConsumerConfig;
import kafka.consumer.ConsumerIterator;
import kafka.consumer.KafkaStream;
import kafka.javaapi.consumer.ConsumerConnector;
import kafka.message.MessageAndMetadata;
public class KafkaHighLevelConsumer {
public static void main(String[] args) {
String topic = "topic1";
String groupId = "group1";
String consumerId = "consumer1";
Properties props = new Properties();
props.put("zookeeper.connect", "192.168.56.21:2181");
props.put("zookeeper.session.timeout.ms", "3600000");
props.put("group.id", groupId);
props.put("client.id", "test");
// 可以不指定consumerid,会自动生成
props.put("consumer.id", consumerId);
// smallest会去找这个consumer上次消费到的地方,然后启动后从这个值的后面一条开始读,
// 否则从最大的offset之后开始读取数据,比如offset已经到100了,则会从101开始读取
props.put("auto.offset.reset", "smallest");
// high level consumer默认的是自动提交offset的,而且默认是1分钟自动提交一次,内部会启动一个scheleder来做offset的提交的
// 如果不自动提交,也不手动提交,那么consumer下一次消费数据的时候回继续从上一次offset位置继续往后读,就会出现重复数据
props.put("auto.commit.enable", "true");
props.put("auto.commit.interval.ms", "60000");
// 已被消费的offset默认是存储在Zookeeper上的,设置成kafka时,下面的参数才生效,作用是同时将offset存于zookeeper和kafka
// 利用这一点可做kafka的升级,完成从zookeeper迁移到kafka中,因为不需要知道offset的历史
// 只需要知道最新的offset即可:当所有的consumer对应的partition里面的offset都已经存了两份,此时就可以改为只写kafka了
props.put("offsets.storage", "kafka");
props.put("dual.commit.enabled", "true");
ConsumerConfig consumerConfig = new ConsumerConfig(props);
ConsumerConnector consumerConnector = Consumer.createJavaConsumerConnector(consumerConfig);
Map<String, Integer> topicCountMap = new HashMap<String, Integer>();
topicCountMap.put(topic, 1);
Map<String, List<KafkaStream<byte[], byte[]>>> consumerMap =
consumerConnector.createMessageStreams(topicCountMap);
KafkaStream<byte[], byte[]> stream1 = consumerMap.get(topic).get(0);
ConsumerIterator<byte[], byte[]> it1 = stream1.iterator();
// hasNext是阻塞的,没有新数据就会被阻塞
while (it1.hasNext()) {
MessageAndMetadata<byte[], byte[]> messageAndMetadata = it1.next();
String message =
String.format("Consumer ID:%s, Topic:%s, GroupID:%s, PartitionID:%s, Offset:%s, Message Key:%s, Message Payload: %s",
consumerId,
messageAndMetadata.topic(), groupId, messageAndMetadata.partition(),
messageAndMetadata.offset(), new String(messageAndMetadata.key()),new String(messageAndMetadata.message()));
System.out.println(message);
// 手动提交offset
// consumerConnector.commitOffsets();
}
}
}
2.2 Low Level Consumer
使用Low Level Consumer (Simple Consumer)的主要原因是希望比Consumer Group更好的控制数据的消费,比如同一条消息读多次,方便Replay、只消费某个Topic的部分Partition等等。与High Level Consumer相比,Low Level Consumer要求用户做大量的额外工作,比如在应用程序中跟踪处理offset,并决定下一次消费哪条消息,获知每个Partition的Leader,处理Leader的变化,处理多Consumer的协作等等。
Low Level Consumer都是手工管理offset的,完全由consumer应用程序决定下一次消费消息的起始offset,也就是指定要消费的Topic的哪个Partition,从哪个offset开始之后的多少个字节的消息,不是数量哦,但是并不知道消息的大小,怎么指定?Kafka是这样设计的,如果指定的是n字节,则最多拿n字节,如果每条消息都超过n字节,将拿不到任何消息,所以可以把这个上限设置大一点,而且消息字节数是包含一些元信息的,不仅仅是发送的消息本身的大小。
import kafka.api.FetchRequest;
import kafka.api.FetchRequestBuilder;
import kafka.javaapi.FetchResponse;
import kafka.javaapi.consumer.SimpleConsumer;
import kafka.javaapi.message.ByteBufferMessageSet;
import kafka.message.MessageAndOffset;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
public class KafkaLowLevelConsumer {
public static void main(String[] args) throws UnsupportedEncodingException {
final String topic = "topic1";
final String clientId = "LowLevelConsumer";
SimpleConsumer consumer = new SimpleConsumer("192.168.56.24",
9092, 100000, 64 * 100000, clientId);
// 如果要消费的partition不在指定的机器上,实际上是获取不到任何数据的
FetchRequest fetchRequest = new FetchRequestBuilder().clientId(clientId).addFetch(topic, 0, 0, 100000)
.addFetch(topic, 1, 0, 100000).addFetch(topic, 2, 0, 100).addFetch(topic, 3, 0, 100).build();
FetchResponse fetchResponse = consumer.fetch(fetchRequest);
ByteBufferMessageSet messageSet = fetchResponse.messageSet(topic, 2);
for (MessageAndOffset messageAndOffset : messageSet) {
ByteBuffer payload = messageAndOffset.message().payload();
long offset = messageAndOffset.offset();
byte[] bytes = new byte[payload.limit()];
payload.get(bytes);
System.out.println("offset:" + offset + ", message:" + new String(bytes, "UTF-8"));
}
}
}
三、新API
从Kafka 2.11_0.10.1.0开始提供了新的API,在新的API中主要包括以下几点变化:
- API包名统一规划到org.apache.kafka.clients子包下
- High/Low Level Consumer使用方式统一
- 可以将offset存于Kafka和Zookeeper之外
- 增加Rebalance回调接口
- 增加消费流程控制,比如暂停和恢复消费
- 增加发送回调接口
- 重构Partititoner接口
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import java.util.Map;
public class NewHashPartitioner implements Partitioner {
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valyeBytes, Cluster cluster) {
if (null == keyBytes) {
return 0;
}
final int partitionSize = cluster.partitionsForTopic(topic).size();
int hashCode = 0;
if (key instanceof Integer) {
hashCode = (Integer) key;
} else if (key instanceof Long) {
hashCode = ((Long)key).intValue();
} else {
hashCode = key.hashCode();
}
hashCode = hashCode & 0x7FFFFFFF;
return hashCode % partitionSize;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> map) {
}
}
支持发送回调的Producer
import org.apache.kafka.clients.producer.*;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.common.serialization.StringSerializer;
import java.util.Properties;
public class KafkaNewProducer {
public static void main(String[] args) {
Properties properties = new Properties();
properties.put("bootstrap.servers", "192.168.56.21:9092");
properties.put("key.serializer", StringSerializer.class.getName());
properties.put("value.serializer", StringSerializer.class.getName());
properties.put("partitioner.class", NewHashPartitioner.class.getName());
properties.put("acks", "all");
properties.put("retries", "3");
properties.put("batch.size", "16384");
properties.put("linger.ms", "1");
properties.put("buffer.memory", "33554432");
Producer<String, String> producer = new KafkaProducer<String, String>(properties);
for (int i = 0; i < 10; i++) {
ProducerRecord<String, String> record = new ProducerRecord<String, String>("topic1",
Integer.toString(i), Integer.toString(i));
producer.send(record, new Callback() {
@Override
public void onCompletion(RecordMetadata recordMetadata, Exception e) {
if (null != recordMetadata) {
System.out.printf("Send record partition: %d, offset: %d, keySize: %d, valueSize: %d%n",
recordMetadata.partition(), recordMetadata.offset(), recordMetadata.serializedKeySize(),
recordMetadata.serializedValueSize());
}
if (null != e) {
e.printStackTrace();
}
}
});
}
producer.close();
}
}
新版本API中的Rebalance可以通过subscribe方法注册监听器,也支持使用assign方法手动分配要消费的Partition,可以理解为分别对应老版本中的High/Low Level Consumer,一个是自动Rebalance,一个是手动分配。另外,新的consumer起来之后如果没有调用poll实际上是不会做Rebalance的,同样的如果希望consumer一直能分配到parition,需要在一定的时间内不停的执行poll方法,才能知道这个consumer还活着且接受assign的状态,如果设置的两次间隔之间比较小,但是每次获取的数据很大,比如花了远超过间隔时间的时间去处理上一次拿到的数据,可能会造成不会再分配partiton给consumer了,所以这两个参数一定要设置好
import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;
import org.apache.kafka.common.serialization.StringDeserializer;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
public class KafkaNewConsumer {
public static void main(String[] args) {
final String clientId = "client1";
Properties properties = new Properties();
properties.put("bootstrap.servers", "192.168.56.21:9092");
properties.put("group.id", "group1");
properties.put("client.id", clientId);
properties.put("enable.auto.commit", "true");
properties.put("auto.commit.interval.ms", "60000");
properties.put("key.deserializer", StringDeserializer.class.getName());
properties.put("value.deserializer", StringDeserializer.class.getName());
properties.put("auto.offset.reset", "earliest");
properties.put("max.poll.interval.ms", "300000");
properties.put("max.poll.records", "500");
KafkaConsumer<String, String> consumer = new KafkaConsumer(properties);
consumer.subscribe(Arrays.asList("topic1"), new ConsumerRebalanceListener() {
@Override
public void onPartitionsRevoked(Collection<TopicPartition> partitions) {
for (TopicPartition partition : partitions) {
System.out.printf("Revoked partition for client %s : %s-%s %n",
clientId, partition.topic(), partition.partition());
}
}
@Override
public void onPartitionsAssigned(Collection<TopicPartition> partitions) {
for (TopicPartition partition : partitions) {
System.out.printf("Assigned partition for client %s : %s-%s %n",
clientId, partition.topic(), partition.partition());
}
}
});
// // assign方法,此时的group.id已经没什么作用了;即使两个使用assign的consumer都订阅了同样的topic相同的partition,
// // 它们都可以消费到消息的,即使groupid一样
// consumer.assign(Arrays.asList(new TopicPartition("topic1", 0),
// new TopicPartition("topic1", 1)));
AtomicLong atomicLong = new AtomicLong(0);
while (true) {
// poll也是阻塞,可以指定超时时间;设置为0,不阻塞,立即返回
ConsumerRecords<String, String> consumerRecords = consumer.poll(100);
// wakeup方法将consumer从poll阻塞中唤醒,抛出WakeupException
// consumer.wakeup();
// // 暂停和恢复消费
// consumer.pause(Arrays.asList(new TopicPartition("topic1", 0)));
// consumer.resume(Arrays.asList(new TopicPartition("topic1", 0)));
for (ConsumerRecord<String, String> consumerRecord : consumerRecords) {
System.out.printf("client: %s, topic: %s, partition: %d, offset: %d, key: %s, value: %s%n",
clientId, consumerRecord.topic(), consumerRecord.partition(), consumerRecord.offset(),
consumerRecord.key(), consumerRecord.value());
// // 手动异步提交
// if (0 == atomicLong.getAndIncrement() % 10) {
// // consumer.commitAsync();
// consumer.commitAsync(new OffsetCommitCallback() {
// @Override
// public void onComplete(Map<TopicPartition, OffsetAndMetadata> offsets, Exception e) {
// for (Map.Entry<TopicPartition, OffsetAndMetadata> entry : offsets.entrySet()) {
// System.out.printf("Commit %s-%d-%d %n", entry.getKey().topic(), entry.getKey().partition(),
// entry.getValue().offset());
// }
// if (null != e) {
// e.printStackTrace();
// }
// }
// });
// }
}
// Set<TopicPartition> partitions = consumerRecords.partitions();
// for (TopicPartition partition : partitions) {
// List<ConsumerRecord<String, String>> records = consumerRecords.records(partition);
// for (ConsumerRecord<String, String> record : records) {
// System.out.printf("client: %s, topic: %s, partition: %d, offset: %d, key: %s, value: %s%n",
// clientId, record.topic(), record.partition(), record.offset(),
// record.key(), record.value());
// }
// long lastOffset = records.get(records.size() - 1).offset();
// // 提交特定的partiton的offset,且可以指定提交的offset是多少
// consumer.commitAsync(Collections.singletonMap(partition, new OffsetAndMetadata(lastOffset + 1)),
// new OffsetCommitCallback() {
// @Override
// public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
//
// }
// });
// }
}
}
}