四、Kafka Producer和Consumer

一、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) {
//
//                            }
//                        });
//            }
        }

    }
}

 

转载于:https://my.oschina.net/u/3438627/blog/1859640

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值