微信搜索superit|邀请体验:大数据, 数据管理、OLAP分析与可视化平台 | 赞助作者:赞助作者

Kafka java api-生产者代码、高性能吞吐

kafka aide_941 25℃

Kafka java api-生产者代码、高性能吞吐

前面做过命令行让生产者发送消息,现在使用java api来进行消息的生产,以及解释kafka高性能是如何实现(来源于学习资料)。
使用shell创建topic和发送消息如下:

#参数:zookeeper连接地址和端口号,副本数(包括自身),使用几个partition,topic的名称

[root@mini1 bin]# ./kafka-topics.sh --create --zookeeper mini1:2181 --replication-factor 2 --partitions 3 --topic orderMq
Created topic "orderMq".
[root@mini1 bin]# kafka-console-producer.sh --broker-list mini1:9092 --topic orderMq
hello tom
hi jerry
spring 
hhaah
xixi
nini

下面使用java api来发送消息
注:如果topic已经存在那么肯定就不创建了,但是不存在则会创建。

public class KafkaProducerSimple {
    public static void main(String[] args) {
        /**
         * 1、指定当前kafka producer生产的数据的目的地
         *  创建topic可以输入以下命令,在kafka集群的任一节点进行创建。
         *  bin/kafka-topics.sh --create --zookeeper mini1:2181 --replication-factor 2 --partitions 3 --topic test
         */
        String TOPIC = "orderMQ";
        /**
         * 2、读取配置文件
         */
        Properties props = new Properties();
        /*
         * key.serializer.class默认为serializer.class  key的序列化使用哪个类
         */
        props.put("serializer.class", "kafka.serializer.StringEncoder");
        /*
         * kafka broker对应的主机,格式为host1:port1,host2:port2
         */
        props.put("metadata.broker.list", "mini1:9092,mini2:9092,mini3:9092");
        /*
         * request.required.acks,设置发送数据是否需要服务端的反馈,有三个值0,1,-1
         * 0,意味着producer永远不会等待一个来自broker的ack,这就是0.7版本的行为。
         * 这个选项提供了最低的延迟,但是持久化的保证是最弱的,当server挂掉的时候会丢失一些数据。
         * 1,意味着在leader replica已经接收到数据后,producer会得到一个ack。
         * 这个选项提供了更好的持久性,因为在server确认请求成功处理后,client才会返回。
         * 如果刚写到leader上,还没来得及复制leader就挂了,那么消息才可能会丢失。
         * -1,意味着在所有的ISR都接收到数据后,producer才得到一个ack。
         * 这个选项提供了最好的持久性,只要还有一个replica存活,那么数据就不会丢失
         */
        props.put("request.required.acks", "1");
        /*
         * 可选配置,如果不配置,则使用默认的partitioner partitioner.class
         * 默认值:kafka.producer.DefaultPartitioner
         * 用来把消息分到各个partition中,默认行为是对key进行hash。
         */
        props.put("partitioner.class", "com.scu.kafka.MyLogPartitioner");
//        props.put("partitioner.class", "kafka.producer.DefaultPartitioner");
        /**
         * 3、通过配置文件,创建生产者
         */
        Producer<String, String> producer = new Producer<String, String>(new ProducerConfig(props));
        /**
         * 4、通过for循环生产数据
         */
        for (int messageNo = 1; messageNo < 100000; messageNo++) {
            /**
             * 5、调用producer的send方法发送数据
             * 注意:这里需要指定 partitionKey,用来配合自定义的MyLogPartitioner进行数据分发
             */
            producer.send(new KeyedMessage<String, String>(TOPIC, messageNo + "", "appid" + UUID.randomUUID() + "itcast"));
        }
    }
}
public class MyLogPartitioner implements Partitioner {
    private static Logger logger = Logger.getLogger(MyLogPartitioner.class);

    public MyLogPartitioner(VerifiableProperties props) {
    }

    /**
     *
     * @param obj 传来的key 用它来进行hash分到partition
     * @param numPartitions 几个partition 如果集群中已存在该topic,那么partition数为原本存在数,否则默认是2
     * @return 生产到哪个partition
     */
    public int partition(Object obj, int numPartitions) {
 //使用下面被注释掉的代码,则类似于hadoop的partition分发方式,hash取模去发到对应序号的partition,这里使用1则表示发送到orderMQ-1的topic
//        return Integer.parseInt(obj.toString())%numPartitions;
        return 1;
    }

}

启动kafka集群,执行main方法,去集群中查看。

[root@mini1 orderMQ-1]# ll
总用量 14296
-rw-r--r--. 1 root root 10485760 11月 22 07:53 00000000000000000000.index
-rw-r--r--. 1 root root 14610099 11月 22 07:53 00000000000000000000.log
[root@mini1 orderMQ-1]# ll
总用量 14696
-rw-r--r--. 1 root root 10485760 11月 22 07:53 00000000000000000000.index
-rw-r--r--. 1 root root 15012813 11月 22 07:53 00000000000000000000.log
[root@mini1 orderMQ-1]# ll
总用量 15184
-rw-r--r--. 1 root root 10485760 11月 22 07:53 00000000000000000000.index
-rw-r--r--. 1 root root 15513339 11月 22 07:53 00000000000000000000.log
[root@mini1 orderMQ-1]# ll
总用量 15448
-rw-r--r--. 1 root root 10485760 11月 22 07:53 00000000000000000000.index
-rw-r--r--. 1 root root 15783297 11月 22 07:53 00000000000000000000.log
[root@mini1 orderMQ-1]# ll
总用量 16288
-rw-r--r--. 1 root root 10485760 11月 22 07:53 00000000000000000000.index
-rw-r--r--. 1 root root 16643559 11月 22 07:53 00000000000000000000.log
[root@mini1 orderMQ-1]# ll
总用量 16600
-rw-r--r--. 1 root root 10485760 11月 22 07:53 00000000000000000000.index
-rw-r--r--. 1 root root 16961019 11月 22 07:53 00000000000000000000.log

看到消息在不断增加,由于文件内容乱码就不看了。

kafka高性能吞吐
(来源于学习资料自己并不咋懂网络只能去感受了)
kafka实现高性能吞吐主要就是2个原因:1、使用了pageCache,2、使用了sendfile技术

(1)、pageCache是把尽可能多的空闲内存都当做了磁盘缓存来使用,生成的消息大部分存储在缓存中,大数据环境下,生产和消费都是非常快的,那么基本上大部分操作都能同时在缓存中进行,所以吞吐性能高。具体介绍如下:

不同于Redis和MemcacheQ等内存消息队列,Kafka的设计是把所有的Message都要写入速度低容量大的硬盘,以此来换取更强的存储能力。实际上,Kafka使用硬盘并没有带来过多的性能损失,“规规矩矩”的抄了一条“近道”。

首先,说“规规矩矩”是因为Kafka在磁盘上只做Sequence I/O,由于消息系统读写的特殊性,这并不存在什么问题。关于磁盘I/O的性能,引用一组Kafka官方给出的测试数据(Raid-5,7200rpm):

Sequence I/O: 600MB/s
Random I/O: 100KB/s

所以通过只做Sequence I/O的限制,规避了磁盘访问速度低下对性能可能造成的影响。

接下来我们再聊一聊Kafka是如何“抄近道的”。

首先,Kafka重度依赖底层操作系统提供的PageCache功能。当上层有写操作时,操作系统只是将数据写入PageCache,同时标记Page属性为Dirty。

当读操作发生时,先从PageCache中查找,如果发生缺页才进行磁盘调度,最终返回需要的数据。实际上PageCache是把尽可能多的空闲内存都当做了磁盘缓存来使用。同时如果有其他进程申请内存,回收PageCache的代价又很小,所以现代的OS都支持PageCache。

使用PageCache功能同时可以避免在JVM内部缓存数据,JVM为我们提供了强大的GC能力,同时也引入了一些问题不适用与Kafka的设计。

(2)sendfile技术
PageCache还只是第一步,Kafka为了进一步的优化性能还采用了Sendfile技术。在解释Sendfile之前,首先介绍一下传统的网络I/O操作流程,大体上分为以下4步。

OS 从硬盘把数据读到内核区的PageCache。
用户进程把数据从内核区Copy到用户区。
然后用户进程再把数据写入到Socket,数据流入内核区的Socket Buffer上。
OS 再把数据从Buffer中Copy到网卡的Buffer上,这样完成一次发送。

整个过程共经历两次Context Switch,四次System Call。同一份数据在内核Buffer与用户Buffer之间重复拷贝,效率低下。其中2、3两步没有必要,完全可以直接在内核区完成数据拷贝。这也正是Sendfile所解决的问题,经过Sendfile优化后,整个I/O过程就变成了下面这个样子。

通过以上的介绍不难看出,Kafka的设计初衷是尽一切努力在内存中完成数据交换,无论是对外作为一整个消息系统,或是内部同底层操作系统的交互。如果Producer和Consumer之间生产和消费进度上配合得当,完全可以实现数据交换零I/O。这也就是我为什么说Kafka使用“硬盘”并没有带来过多性能损失的原因。

转载请注明:SuperIT » Kafka java api-生产者代码、高性能吞吐

喜欢 (0)or分享 (0)