大数据组件使用 总文章

  • kafka 生产/消费API、offset管理/原理、kafka命令
  • kafka 命令、API
  • Kafka 安装、原理、使用
  • mapreduce 实时消费 kafka 数据

1.使用控制台运行

1.创建一个topic主题  
        cd /root/kafka 
        chmod 777 /root/kafka/bin/kafka-topics.sh
        格式:bin/kafka-topics.sh --create --zookeeper IP:2181 --replication-factor 副本数 --partitions 分片数/分区数 --topic 主题名
        例子:bin/kafka-topics.sh --create --zookeeper NODE1:2181 --replication-factor 1 --partitions 1 --topic order

kafka队列增加分区数 kafka增加分区命令_zookeeper

    2.编写代码启动一个生产者,生产数据

cd /root/kafka 
        chmod 777 /root/kafka/bin/kafka-console-producer.sh
        格式:bin/kafka-console-producer.sh --broker-list IP:9092 --topic 主题名
        例子:bin/kafka-console-producer.sh --broker-list NODE1:9092 --topic order

kafka队列增加分区数 kafka增加分区命令_zookeeper_02

    3.编写代码启动给一个消费者,消费数据

cd /root/kafka 
        chmod 777 /root/kafka/bin/kafka-console-consumer.sh
        格式:bin/kafka-console-consumer.sh --zookeeper IP:2181 --from-beginning --topic 主题名
        例子:bin/kafka-console-consumer.sh --zookeeper NODE1:2181 --from-beginning --topic order

kafka队列增加分区数 kafka增加分区命令_kafka_03

    4.Kafka 常用操作命令

1.查看当前服务器中的所有 topic
            cd /root/kafka
            格式:bin/kafka-topics.sh --list --zookeeper zookeeper的IP:2181
            例子:bin/kafka-topics.sh --list --zookeeper NODE1:2181
        2.创建 topic
            cd /root/kafka
            格式:bin/kafka-topics.sh --create --zookeeper zookeeper的IP:2181 --replication-factor 副本数 --partitions 分片数/分区数 --topic 主题名
            例子:bin/kafka-topics.sh --create --zookeeper NODE1:2181 --replication-factor 1 --partitions 1 --topic test
        3.删除 topic
            需要在kafka集群中的每个kafka服务器中的 vim /root/kafka/config/server.properties 中设置 delete.topic.enable=true 否则只是标记删除或者直接重启
            cd /root/kafka
            格式:bin/kafka-topics.sh --delete --zookeeper zookeeper的IP:2181 --topic 主题名
            例子:bin/kafka-topics.sh --delete --zookeeper NODE1:2181 --topic test
        4.通过 shell 命令发送消息
            cd /root/kafka
            格式:bin/kafka-console-producer.sh --broker-list kafka的IP:9092 --topic 主题名
            例子:bin/kafka-console-producer.sh --broker-list NODE1:9092 --topic test
        5.通过 shell 消费消息
            cd /root/kafka
            格式:bin/kafka-console-consumer.sh --zookeeper zookeeper的IP:2181 --from-beginning --topic 主题名
            例子:bin/kafka-console-consumer.sh --zookeeper NODE1:2181 --from-beginning --topic test
        6.查看消费位置
            cd /root/kafka
            格式:bin/kafka-run-class.sh kafka.tools.ConsumerOffsetChecker --zookeeper zookeeper的IP:2181 --group testGroup
            例子:bin/kafka-run-class.sh kafka.tools.ConsumerOffsetChecker --zookeeper NODE1:2181 --group testGroup
        7.查看某个 Topic 的详情
            cd /root/kafka
            格式:bin/kafka-topics.sh --topic 主题名 --describe --zookeeper zookeeper的IP:2181
            例子:bin/kafka-topics.sh --topic 主题名 --describe --zookeeper NODE1:2181
        8.对分区数进行修改
            cd /root/kafka
            格式:bin/kafka-topics.sh --zookeeper zookeeper的IP --alter --partitions 2 --topic 主题名
            例子:bin/kafka-topics.sh --zookeeper NODE1 --alter --partitions 分片数/分区数 --topic 主题名
消费者:
旧版:kafka-console-consumer --zookeeper cdh01:2181,cdh02:2181,cdh03:2181 --topic test --from-beginning

新版:新版中仍然使用旧版的消费者命令会报错:zookeeper is not a recognized option
		kafka-console-consumer --bootstrap-server cdh01:9092,cdh02:9092,cdh03:9092 --topic test --from-beginning
		指定消费者组:kafka-console-consumer --bootstrap-server cdh01:9092,cdh02:9092,cdh03:9092 --group testGroup --topic test --from-beginning

查看消费位置
指定消费者组:
	kafka-consumer-groups --bootstrap-server cdh01:9092,cdh02:9092,cdh03:9092 --describe --group testGroup 
	kafka-consumer-groups --bootstrap-server cdh01:9092,cdh02:9092,cdh03:9092 --describe --group testGroup --all-topics

 

2.使用Java api运行

1.java工程-maven,依赖
        <dependency>
                <groupId>org.apache.kafka</groupId>
                <artifactId>kafka-clients</artifactId>
                <version>0.11.0.1</version>
        </dependency>

    2.生产者(先启动消费者等待接收数据,然后再启动生产者进行发送数据)
                //kafka生产者:订单的生产者代码
                public class OrderProducer
                {
                    public static void main(String[] args) throws InterruptedException
                    {
                        // 通过Properties配置文件的方式 配置"连接集群"的信息
                        Properties props = new Properties();
                        props.put("bootstrap.servers", "NODE1:9092");
                        props.put("acks", "all");
                        props.put("retries", 0);
                        props.put("batch.size", 16384);
                        props.put("linger.ms", 1);
                        props.put("buffer.memory", 33554432);
                        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
                        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
    
                        //kafka生产者
                        KafkaProducer<String, String> kafkaProducer = new KafkaProducer<String, String>(props);
    
                        for (int i = 0; i < 10000; i++)
                        {
                            //数据分发策略:可以指定数据发往哪个partition,当ProducerRecord 的构造参数中有partition的时候,就可以发送到对应partition上
                            // new ProducerRecord<String, String>("topic主题名",partition,"key", "value")
                            ProducerRecord<String, String> partition = new ProducerRecord<String, String>("order", 0, "key", "订单"+i);
    
                            //数据分发策略:如果生产者没有指定partition,但是发送消息中有key,根据key的hash值的方式发送数据到那个partition分区/分片中
                            // new ProducerRecord<String, String>("topic主题名","key", "value")
                            ProducerRecord<String, String> key = new ProducerRecord<String, String>("order", "key", "value"+i);
    
                            //数据分发策略:既没有指定partition,也没有key的情况下如何发送数据。使用轮询的方式发送数据到那个partition分区/分片中
                            // new ProducerRecord<String, String>("topic主题名", "value")
                            ProducerRecord<String, String> value = new ProducerRecord<String, String>("order", "订单信息!"+i);
    
                            //kafka生产者发送数据
                            kafkaProducer.send(value);
                        }
                    }
                }


    3.消费者(先启动消费者等待接收数据,然后再启动生产者进行发送数据)    
                    //kafka消费者:订单的消费者代码
                    public class OrderConsumer
                    {
                        public static void main(String[] args)
                        {
                            //1.通过Properties配置文件的方式 配置"连接集群"的信息
                            Properties props = new Properties();
                            props.put("bootstrap.servers", "NODE1:9092");
                            //配置group.id:多个Consumer的group.id都相同的话,表示多个Consumer都在同一个消费组group中
                            props.put("group.id", "test");
                            props.put("enable.auto.commit", "true");
                            props.put("auto.commit.interval.ms", "1000");
                            props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
                            props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
                    
                            // Kafka消费者
                            KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(props);
                    
                            //2.订阅某个topic主题下的相关消息数据用于消费。可以订阅多个topic主题,封装到一个 List中,可以使用Arrays.asList进行封装
                            kafkaConsumer.subscribe(Arrays.asList("order"));
                    
                            while (true)
                            {
                                //jdk queue的操作方法:offer插入元素、poll获取元素。
                                //blockingqueue的操作方法:put插入元素,take获取元素。
                                ConsumerRecords<String, String> consumerRecords = kafkaConsumer.poll(100);
                    
                                for (ConsumerRecord<String, String> record : consumerRecords)
                                {
                                    //此处需要等待到有数据才能消费进行获取打印
                                    System.out.println("消费的数据为:" + record.value());
                                }
                            }
                        }
                    }

  3.topic主体中的分区partition 和 group.id消费者组 的关系

Consumer Group(CG)消费者组:
        这是 kafka 用来实现一个 topic(主题) 消息的广播(发给所有的 consumer消费者)和 单播(发给任意一个 consumer消费者)的手段。
        一个 topic(主题) 可以有多个 CG消费者组。topic(主题) 的消息会复制(不是真的复制,是概念上的)到所有的 CG消费者组,
        但每个 partion(分区) 只会把消息发给该 CG 中的一个consumer消费者。
        如果需要实现广播,只要每个 consumer消费者 有一个独立的 CG 就可以了。
        要实现单播只要所有的 consumer 在同一个 CG。
        用 CG消费者组 还可以将 consumer消费者 进行自由的分组,而不需要多次发送消息到不同的 topic(主题)。
    Broker:一台 kafka 服务器就是一个 broker(中间人)。一个kafka集群(cluster) 由多个 broker(中间人) 组成。一个 broker 可以容纳多个 topic(主题)。
    Partition(分区):
        为了实现扩展性,一个非常大的 topic(主题) 可以分布到多个 broker(即服务器)上,一个topic(主题) 可以分为多个 partition(分区),
        每个 partition(分区) 是一个有序的队列(Queue)。partition(分区) 中的每条消息都会被分配一个有序的 id(offset)。
        kafka 只保证按一个 partition(分区) 中的顺序 将消息发给 consumer消费者,不保证一个 topic 的整体(多个 partition分区 间)的顺序

 

情况一:同一个topic的一个partition只能被同一个customerGroup的一个customer消费;group里多于partition数量的customer会空闲;

kafka队列增加分区数 kafka增加分区命令_IP_04

情况二:同一个topic的partition数量多于同一个customerGroup的customer数量时,会有一个customer消费多个partition,这样也就没法保证customer接收到的消息的顺序性,kafka只保证在一个partition上数据是有序的,无法保证topic全局数据的顺序性;

kafka队列增加分区数 kafka增加分区命令_kafka队列增加分区数_05

情况三:一个topic 的partitions被多个customerGroup消费时,可以并行重复消费;

kafka队列增加分区数 kafka增加分区命令_kafka队列增加分区数_06

kafka topic partition被同一个GROUPID的多个消费者消费,只有一个能收到消息的原因一般有如下:
            1.只有一个partition分区;
              同一个topic的同一个partition分区只允许同一个customerGroup的一个消费者消费信息,一个partition上不允许同一个消费者组的多个消费者并发,
              但同一个partition上是可以多个不同消费者组种的消费者并发消费的;
            2.多个partition分区,但是,消息在生产时只发往到了一个partiton上;
              key的hashCode%partitionNum相同导致,或者自定义了分区策略;
              导致这种严重的数据倾斜;

 

1.发送到Kafka的消息会根据key,发送到对应topic的partition中,有默认的分发规则(也可以自己重写分发规则),
      基本上就是相同的key发送到一个partition中,不同的key有可能发送到相同的partition中。

    2.group是消费者中的概念,按照group(组)对消费者进行区分。
      对于每个group,需要先指定订阅哪个topic的消息,然后该topic下的partition会平均分配到group下面的consumer上。
      所以会出现以下这些情况:
        1.一个topic被多个group订阅,那么一条消息就会被不同group中的多个consumer处理。
          如果需要实现广播,只要每个 consumer消费者 有一个独立的 CG 就可以了。
          一个 topic(主题) 可以有多个 CG消费者组。topic(主题) 的消息会复制(不是真的复制,是概念上的)到所有的 CG消费者组,
          但每个 partion(分区) 只会把消息发给该 CG 中的一个consumer消费者。
        2.同一个group中,每个partition只会被一个consumer处理,这个consumer处理的消息不一定是同一个key的。所以需要在处理的地方判断。
           要实现单播只要所有的 consumer 在同一个 CG。

    3.例子,现在有一个用户信息修改的回调消息扔到消息队列里,有两个业务要处理,一个是更新数据库,一个是更新es索引信息
        topic:user_update_topic
        key:user_update_key_cid //cid标志公司的区分信息
        这样子的话,同一个公司的用户更新会被分配到一个partition中,同一个公司的用户更新能保证前后顺序不变

        group1中的consumer更新数据库,group:consumer1_group
        group2中的consumer更新es索引信息,group:consumer2_group
        group1、group2这个两个group会分别消费这个topic下的数据,对于每个group,内部的consumer会平分topic下的partition,
        相当于group中的每个consumer会处理多个公司的数据,但处理的公司不会有重叠。
        
        以上是topic中partition多余group中的消费者的时候,如果group下面有3个消费者,但是分区partition只有一个,那么三个消费者中只有一个会消费消息。

 

消费者组 (Consumer Group)
        consumer group是kafka提供的可扩展且具有容错性的消费者机制。
        既然是一个组,那么组内必然可以有多个消费者或消费者实例(consumer instance),它们共享一个公共的ID,即group ID。
        组内的所有消费者协调在一起来消费订阅主题(subscribed topics)的所有分区(partition)。
        当然,每个分区只能由同一个消费组内的一个consumer来消费(当然该分区还可以同时分配给其他group中的某个consumer来消费)。
        个人认为,理解consumer group记住下面这三个特性就好了:
            1.consumer group下可以有一个或多个consumer instance,consumer instance可以是一个进程,也可以是一个线程
            2.group.id是一个字符串,唯一标识一个consumer group
            3.consumer group下订阅的topic下的每个分区只能分配给某个group下的一个consumer,
              当然该分区还可以同时分配给其他group中的某个consumer来消费。
    kafka consumer:
        消费者可以从多个broker中读取数据。消费者可以消费多个topic中的数据。
            因为Kafka的broker是无状态的,所以consumer必须使用partition offset来记录消费了多少数据。
        如果一个consumer指定了一个topic的offset,意味着该consumer已经消费了该offset之前的所有数据。
        consumer可以通过指定offset,从topic的指定位置开始消费数据。consumer的offset存储在Zookeeper中。
    
    offset:
        用来保存消费进度。offset表示在当前topic,当前groupID下消费到的位置。
        offset为earliest并不代表offset=1。在不进行过期配置的情况下,kafka消息默认7天时间就会过期。
        过期后其offset也就随之发生变化,使得用数字进行配置的消费进度并不准确。
            1.earliest:自动重置到最早的offset。
            2.latest:看上去重置到最晚的offset。
            3.none:如果边更早的offset也没有的话,就抛出异常给consumer,告诉consumer在整个consumer group中都没有发现有这样的offset。
    
    groupID:
        一个字符串用来指示一组consumer所在的组。相同的groupID表示在一个组里。
        相同的groupID消费记录offset时,记录的是同一个offset。
        所以,此处需要注意 
            (1)如果多个地方都使用相同的groupid,可能造成个别消费者消费不到的情况
            (2)如果单个消费者消费能力不足的话,可以启动多个相同groupid的consumer消费,处理相同的逻辑。

  

produce方面:
        如果有多个分区,发送的时候按照key值hashCode%partitionNum哈希取模分区数来决定该条信息发往哪个partition, 
        这里可以自定义partition的分发策略,只要实现Partitioner接口就好,可以自定义成随机分发或者fangwang发往指定分区;
    
    customer方面:
        对于topic中的partition来说,一个partition只允许一个customer来消费,同一个partition上不允许customer并发;
    
    Partition数量 > customer数量时:
        一个consumer会对应于多个partitions,这里主要合理分配consumer数和partition数,否则会导致partition里面的数据被取的不均匀 。
        最好partiton数目是consumer数目的整数倍,所以partition数目很重要,比如取24,就很容易设定consumer数目 。
    
    Partition数量 < customer数量时:
        就会有剩余的customer闲置,造成浪费;
    
    如果一个consumer从多个partition读到数据,不保证数据间的顺序性,kafka只保证在一个partition上数据是有序的,但多个partition,根据你读的顺序会有不同;
    kafka只保证在一个上数据是有序的(每个partition都有segment文件记录消息的顺序性),无法保证topic全局数据的顺序行;
    增减consumer,broker,partition会导致rebalance,所以rebalance后consumer对应的partition会发生变化。

4.storm消费kafka时并行度设置问题

1.首先明确的一点是,storm的并行度都是executor即线程级别的并行;
      包括work(进程),executor(线程)的设置,具体体现在works,spout,bolt设置上,同一个executor上设置多个task还是会串行化执行,并不能提高执行效率,
      这也是由于并行是线程并行,一个线程的多个task肯定是有先后执行顺序的,有顺序那就不是并行;
      关于node,work,executor,task关系和work,spout,bolt,并行度设置网上有很多资料,挺详细;
    
    2.这里记录下我遇到的自己关系的另外两个问题:
        一个是从kafka消费消息是spout并行度设置,另一个ack响应开启的是线程还是进程以及如何设置其数量;
        1.第一个问题:
            其实理解了上面kafka customer和partition的关系第一个问题也就解决了,spout的并发度实例数量设置最好和partition数量一样,
              这样能保证一个spout消费者实例对应一个partition,即实现了一个partition中消息消费的顺序性(有时消息的顺序性要求并不是很高)
              也能很好地提高整个topology的执行效率,至少对拓扑执行效率来说,瓶颈不会卡在spout(数据源)这里;
        2.第二个问题:
            通过Storm UI发现,work和spout,bolt并行度不变的情况下,多开几个acker_executors,works的数量并没有增加,反而是executors数量增加,
            这样就确定了acker_executors如其名一样只是线程,并不像有些网友说的ack的执行是会单独开启ack进程再在该进程里运行ack响应线程。
            他其实就是一个普通的ack线程,运行在已有的work进程里;

    3.另外通过测试发现,我设置了4个work,4个spout,4个bolt,没有设置acker_executors,Storm UI上显示Num workers是4,Num executors却是12
     (4个spout,4个bolt 这里一共是8个executors),所以默认情况下一个work里会有一个acker_executors。

    4.默认情况下 一个work会有一个executor,一个executor会有一个task,如果设置了他们的数量,就会按照设置的数量来生成对应实例;
      如开了4个work,2个spout,3个bolt,那spout和bolt的executors一共就会有5个(spout executors 2个,bolt executors 3个,),
      相当于有2个work里的每个work都有1个spout executor和1个bolt executor,另外还有1个work里只有1个bolt executor,另外还有一个work里啥也没有;
      其实这种配置会导致多开一个啥活也不干的work进程,有些浪费;

5.kafka 多个消费者在同一个groupID消费者组中,只有一个消费者能收到消息,解决方案如下  

1.业务需求:
        将一个业务逻辑分拆出来单独部署多个服务器上,然后与主程序之间通过kafka队列通信,每个业务实例上都有一个消费者在监听队列,
        且他们的groupid相同。我们的原意是主程序 往 队列上发送的命令参数(数据值)会被其中随机的一个消费者收到,从而实现一个负载均衡的效果,
        但是后来发现主程序发送往队列上的控制消息(数据值)始终是被其中固定的一个服务器上的消费者收到,其他的消费者从头到尾没有收到过。
        这样自然就达不到我们负载均衡的效果了。

    2.后来发现,造成这个结果的原因可能有两个:
        1.分区数(Partition)设成了1
        2.发送消息的时候指定了key值

    3.原因一的解决方案:
        1.原因:分区数(Partition)设成了1
        2.分析:
            因为kafka为了保证了消息的一致性,同一个分区的消息只会被于此关联的同一个消费者接收到(大致就是这个意思)。
            如果只有一个分区的话,如果第一条消息被其中一个消费者收到后,后面的消息始终会被这个消费者收到。所以应保证分区的数目大于消费者的数目。
 
        3.解决方案:
            #查看partition数据
            bin/kafka-topics.sh --zookeeper zk1,zk2,zk3 --describe --partitions 20 --topic topic_test

            #增加partition数目
            #增加partition数目后,如果使用的是java的kafka-client的Producer的话,会由于producer内部的缓存机制,
            #导致过几分钟后才被producer感知到partition数目的变化
            bin/kafka-topics.sh --zookeeper zk1,zk2,zk3 --alter --partitions 20 --topic topic_test

    4.原因二的解决方案:
        1.原因:发送消息的时候指定了key值
        2.分析:
            我们改了partitions 的数目后,再次测试,还是始终被其中一个消费者收到。
            后来查看了producer的send方法,发现了是因为我们把key设置为了””,也就是指定了key值为空字符串。

        3.send方法发送 代码分析:
            producer.send(new ProducerRecord<String, String>("test","",""),new Callback() {
                  @Override
                  public void onCompletion(RecordMetadata metadata, Exception exception) {
                    // TODO Auto-generated method stub

                  }
                });

        4.send方法的实现 和 partition方法的调用 代码分析:
            @Override
               public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
                   try {
                       // first make sure the metadata for the topic is available
                       long waitedOnMetadataMs = waitOnMetadata(record.topic(), this.maxBlockTimeMs);
                       long remainingWaitMs = Math.max(0, this.maxBlockTimeMs - waitedOnMetadataMs);
                       byte[] serializedKey;
                       try {
                           serializedKey = keySerializer.serialize(record.topic(), record.key());
                       } catch (ClassCastException cce) {
                           throw new SerializationException("Can't convert key of class " + record.key().getClass().getName() +
                                   " to class " + producerConfig.getClass(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG).getName() +
                                   " specified in key.serializer");
                       }
                       byte[] serializedValue;
                       try {
                           serializedValue = valueSerializer.serialize(record.topic(), record.value());
                       } catch (ClassCastException cce) {
                           throw new SerializationException("Can't convert value of class " + record.value().getClass().getName() +
                                   " to class " + producerConfig.getClass(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG).getName() +
                                   " specified in value.serializer");
                       }
                      //可以看出 生产者往哪个partition分区 发送消息,实际是由 partition()方法得出的
                       int partition = partition(record, serializedKey, serializedValue, metadata.fetch());
                       int serializedSize = Records.LOG_OVERHEAD + Record.recordSize(serializedKey, serializedValue);
                       ensureValidRecordSize(serializedSize);
                       TopicPartition tp = new TopicPartition(record.topic(), partition);
                       log.trace("Sending record {} with callback {} to topic {} partition {}", record, callback, record.topic(), partition);
                       RecordAccumulator.RecordAppendResult result = accumulator.append(tp, serializedKey, serializedValue, callback, remainingWaitMs);
                       if (result.batchIsFull || result.newBatchCreated) {
                           log.trace("Waking up the sender since topic {} partition {} is either full or getting a new batch", record.topic(), partition);
                           this.sender.wakeup();
                       }
                       return result.future;
                       // handling exceptions and record the errors;
                       // for API exceptions return them in the future,
                       // for other exceptions throw directly
                   } catch (ApiException e) {
                       log.debug("Exception occurred during message send:", e);
                       if (callback != null)
                           callback.onCompletion(null, e);
                       this.errors.record();
                       return new FutureFailure(e);
                   } catch (InterruptedException e) {
                       this.errors.record();
                       throw new InterruptException(e);
                   } catch (BufferExhaustedException e) {
                       this.errors.record();
                       this.metrics.sensor("buffer-exhausted-records").record();
                       throw e;
                   } catch (KafkaException e) {
                       this.errors.record();
                       throw e;
                   }
               }

        5.partition方法的实现 和 ProducerRecord对象 代码分析:
              private int partition(ProducerRecord<K, V> record, byte[] serializedKey , byte[] serializedValue, Cluster cluster) {
                  Integer partition = record.partition();
                  if (partition != null) {
                      List<PartitionInfo> partitions = cluster.partitionsForTopic(record.topic());
                      int numPartitions = partitions.size();
                      // they have given us a partition, use it
                      if (partition < 0 || partition >= numPartitions)
                          throw new IllegalArgumentException("Invalid partition given with record: " + partition
                                                             + " is not in the range [0..."
                                                             + numPartitions
                                                             + "].");
                      return partition;
                  }
                  return this.partitioner.partition(record.topic(), record.key(), serializedKey, record.value(), serializedValue,
                      cluster);
              }

          如果发送的时候已经指定了parition的话,就会发送到指定的partition上。
          怎么指定呢??我们发送的是一个ProducerRecoed对象。看它的构造方法,第二个参数就是partition。
            public ProducerRecord(String topic, Integer partition, K key, V value) {
                   if (topic == null)
                       throw new IllegalArgumentException("Topic cannot be null");
                   this.topic = topic;
                   this.partition = partition;
                   this.key = key;
                   this.value = value;
               }

          如果没有指定的parition的话,就调用:
            this.partitioner.partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster)

          如果没有配置partitioner.class的话,那么partitioner默认是org.apache.kafka.clients.producer.internals.DefaultPartitioner的一个对象(参考文章末尾),
          查看一下它的partition()方法:
            public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
                   List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
                   int numPartitions = partitions.size();
                   if (keyBytes == null) {
                       int nextValue = counter.getAndIncrement();
                       List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
                       if (availablePartitions.size() > 0) {
                           int part = DefaultPartitioner.toPositive(nextValue) % availablePartitions.size();
                           return availablePartitions.get(part).partition();
                       } else {
                           // no partitions are available, give a non-available partition
                           return DefaultPartitioner.toPositive(nextValue) % numPartitions;
                       }
                   } else {
                       // hash the keyBytes to choose a partition
                       return DefaultPartitioner.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
                   }
               }

          从上面代码就看出了如果指定了key值的话,partition的值实际上是由Utils.murmur2(keyBytes)哈希计算出来,这样自然每次都是被同一个消费者接收到。
          如果没有指定的话,就会通过轮询的方式逐个发送。这里有个问题就是,如果我们每次都打印出partition的值的话,
          可能会看到peoducer发送并不一定会按照1、2、3、4、5这样的顺序发送,这个从上面代码中能看出最终返回的不是part的值,
          而是availablePartitions.get(part).partition()的值,而availablePartitions里的PartitionInfo的顺序本身就不一定是严格按照顺序排列的。

          在org.apache.kafka.clients.producer.ProducerConfig类中可以看到kafkaproducer的配置以及默认值,其中有这么一行:
            .define(PARTITIONER_CLASS_CONFIG,
                                        Type.CLASS,
                                        DefaultPartitioner.class.getName(),
                                        Importance.MEDIUM, PARTITIONER_CLASS_DOC)

        其中静态变量 PARTITIONER_CLASS_CONFIG的值是”partitioner.class”,可以看出其默认对应的类是 DefaultPartitioner.class,当然,
        这里可以自定义partition的分发策略,只要实现Partitioner接口就好,例如下面代码实现了随机选择partition。
            public class MyPartitioner implements Partitioner{
              @Override
              public void configure(Map<String, ?> configs) {
                // TODO Auto-generated method stub
              }

              @Override
              public int partition(String topic, Object key, byte[] keyBytes,
                  Object value, byte[] valueBytes, Cluster cluster) {
                      List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
                      int numPartitions = partitions.size();
                      int randomNum = new Random().nextInt(numPartitions);
                      return partitions.get(randomNum).partition();
              }

              @Override
              public void close() {
                // TODO Auto-generated method stub
              }
            }