kafka入门详解及api使用

一、 Kafka特性与使用场景:

Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者在网站中的所有动作流数据。 这种动作(网页浏览,搜索和其他用户的行动)是在现代网络上的许多社会功能的一个关键因素。 这些数据通常是由于吞吐量的要求而通过处理日志和日志聚合来解决。 对于像Hadoop一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。Kafka的目的是通过Hadoop的并行加载机制来统一线上和离线的消息处理,也是为了通过集群来提供实时的消息。

1.1 kafka具有以下特性:

  • 高吞吐量、低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个topic可以分多个partition, consumer group 对partition进行consume操作。
  • 可扩展性:kafka集群支持热扩展
  • 持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失
  • 容错性:允许集群中节点失败(若副本数量为n,则允许n-1个节点失败)
  • 高并发:支持数千个客户端同时读写

1.2 Kafka的使用场景:

  • 日志收集:一个公司可以用Kafka可以收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer,例如hadoop、Hbase、Solr等。
  • 消息系统:解耦和生产者和消费者、缓存消息等。
  • 用户活动跟踪:Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到hadoop、数据仓库中做离线分析和挖掘。
  • 运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告。
  • 流式处理:比如spark streaming和storm
  • 事件源

二、windows下zk和kafka环境搭建

1.1 下载
zk下载:https://zookeeper.apache.org/releases.html
kafka下载:https://kafka.apache.org/downloads
1.2 启动
前提:JDK环境安装配置好

zk启动执行:zkServer.cmd

  • zookeeper windows闪退,修改配置文件zoo_sample.cfg改为zoo.cfg或者修改zkEnv.cmd配置文件改为zoo_sample.cfg

kafka启动执行:kafka-server-start.bat
配置文件主要为server.properties;如果自定义了zk端口等需要同步更改这里面的zk配置,包括其他一些kafka的配置也是从这里改,这里说一下kafka的数据目录参数(名字叫log.dirs其实是kafka的数据保存目录并非是日志文件目录) 如:log.dirs=/tmp/kafka-logs 我们可以自定义数据目录方便熟悉kafka是如何保存数据的。

  • kafka windows下运行闪退可能是内存不足,修改文件kafka-server-start.bat修改内存为256M(默认值比较大)
  • 启动需要指定配置文件:.\bin\windows\kafka-server-start.bat .\config\server.properties
  • kafka运行在windows下启动报错。。。其他程序正在使用,删除配置文件制定的logs目录下\kafka-logs\这个文件夹就好了
  • kafka运行在windows下启动报错。。。命令太长,缩减文件夹嵌套层数和名字长度

三、kafka相关概念

3.1 kafka相关概念图
kafka相关概念图

  • Producer:Producer即生产者,消息的产生者,是消息的入口。

  • Broker:Broker是kafka实例,每个服务器上有一个或多个kafka的实例,我们姑且认为每个broker对应一台服务器。每个kafka集群内的broker都有一个不重复的编号,如图中的broker-0、broker-1等……

  • Topic:消息的主题,可以理解为消息的分类,kafka的数据就保存在topic。在每个broker上都可以创建多个topic。

  • Partition:Topic的分区,每个topic可以有多个分区,分区的作用是做负载,提高kafka的吞吐量。同一个topic在不同的分区的数据是不重复的,partition的表现形式就是一个一个的文件夹!

  • Replication:每一个分区都有多个副本,副本的作用是做备胎。当主分区(Leader)故障的时候会选择一个备胎(Follower)上- 位,成为Leader。在kafka中默认副本的最大数量是10个,且副本的数量不能大于Broker的数量,follower和leader绝对是在不同的机器,同一机器对同一个分区也只可能存放一个副本(包括自己)。

  • Message:每一条发送的消息主体。

  • Consumer:消费者,即消息的消费方,是消息的出口。

  • Consumer Group:我们可以将多个消费组组成一个消费者组,在kafka的设计中同一个分区的数据只能被消费者组中的某一个消费者消费。同一个消费者组的消费者可以消费同一个topic的不同分区的数据,这也是为了提高kafka的吞吐量!

  • Zookeeper:kafka集群依赖zookeeper来保存集群的的元信息,来保证系统的可用性。

3.2 同一个消费者组中一个分区只能被一个消费者消费,一个消费者可以消费多个分区的数据。每个分区有自己的offset
在这里插入图片描述
3.3 kafka的数据文件
kafka的数据文件保存在server.properties文件中,log.dirs指定的目录下
在这里插入图片描述
其中.index文件保存着.log文件对应的消息的稀疏索引类似于跳表,可以快速定位到某个offset的数据在数据文件中的位置;.log文件达到一定大小后,新的消息会重新生成一个.log文件,log文件名称就是该文件保存的消息的初始offset前面填充0;

3.4 消息的查找过程:
在这里插入图片描述
1、 先找到offset的368801message所在的segment文件(利用二分法查找),这里找到的就是在第二个segment文件。

2、 打开找到的segment中的.index文件(也就是368796.index文件,该文件起始偏移量为368796+1,我们要查找的offset为368801的message在该index内的偏移量为368796+5=368801,所以这里要查找的相对offset为5)。

由于该文件采用的是稀疏索引的方式存储着相对offset及对应message物理偏移量的关系,所以直接找相对offset为5的索引找不到,这里同样利用二分法查找相对offset小于或者等于指定的相对offset的索引条目中最大的那个相对offset,所以找到的是相对offset为4的这个索引。

3、 根据找到的相对offset为4的索引确定message存储的物理偏移位置为256。打开数据文件,从位置为256的那个地方开始顺序扫描直到找到offset为368801的那条Message。

这套机制是建立在offset为有序的基础上,利用segment+有序offset+稀疏索引+二分查找+顺序查找等多种手段来高效的查找数据!至此,消费者就能拿到需要处理的数据进行处理了。那每个消费者又是怎么记录自己消费的位置呢?

在早期的版本中,消费者将消费到的offset维护zookeeper中,consumer每间隔一段时间上报一次,这里容易导致重复消费,且性能不好!在新的版本中消费者消费到的offset已经直接维护在kafk集群的__consumer_offsets这个topic中

四、同步机制

Kafka中主题的每个Partition有一个预写式日志文件,每个Partition都由一系列有序的、不可变的消息组成,这些消息被连续的追加到Partition中,Partition中的每个消息都有一个连续的序列号叫做offset, 确定它在分区日志中唯一的位置。、
Kafka每个topic的partition有N个副本,其中N是topic的复制因子。Kafka通过多副本机制实现故障自动转移,当Kafka集群中一个Broker失效情况下仍然保证服务可用。在Kafka中发生复制时确保partition的预写式日志有序地写到其他节点上。N个replicas中。其中一个replica为leader,其他都为follower,leader处理partition的所有读写请求,与此同时,follower会被动定期地去复制leader上的数据,以保证leader挂掉之后follower可以被选为主。

4.1 副本同步队列 ISR (in-Sync Replicas)

所谓同步,必须满足如下两个条件:

  • 副本节点必须能与zookeeper保持会话(心跳机制)
  • 副本能复制leader上的所有写操作,并且不能落后太多。(卡住或滞后的副本控制是由 replica.lag.time.max.ms 配置)

默认情况下Kafka对应的topic的replica数量为1,即每个partition都有一个唯一的leader,为了确保消息的可靠性,通常应用中将其值(由broker的参数offsets.topic.replication.factor指定)大小设置为大于1,比如3。 所有的副本(replicas)统称为Assigned Replicas,即AR。ISR是AR中的一个子集,由leader维护ISR列表,follower从leader同步数据有一些延迟。任意一个超过阈值都会把follower剔除出ISR, 存入OSR(Outof-Sync Replicas)列表,新加入的follower也会先存放在OSR中。AR=ISR+OSR。

4.2 HW和LEO
在这里插入图片描述
HW俗称高水位,是HighWatermark的缩写,取一个partition对应的ISR中最小的LEO作为HW,consumer最多只能消费到HW所在的位置。另外每个replica都有HW,leader和follower各自负责更新自己的HW的状态。对于leader新写入的消息,consumer不能立刻消费,leader会等待该消息被所有ISR中的replicas同步后更新HW,此时消息才能被consumer消费。这样就保证了如果leader所在的broker失效,该消息仍然可以从新选举的leader中获取。对于来自内部broKer的读取请求,没有HW的限制。
hw和leo只是为了保证副本之间数据的同步性,不是为了保证数据不丢失。

4.3 ack机制
Kafka的ack机制,指的是producer的消息发送确认机制,这直接影响到Kafka集群的吞吐量和消息可靠性。而吞吐量和可靠性就像鱼与熊掌不可兼得,只能平衡。
ack有3个可选值,分别是1,0,-1。

  • ack=1,简单来说就是,producer只要收到一个分区副本成功写入的通知就认为推送消息成功了。这里有一个地方需要注意,这个副本必须是leader副本。只有leader副本成功写入了,producer才会认为消息发送成功。
    注意,ack的默认值就是1。这个默认值其实就是吞吐量与可靠性的一个折中方案。生产上我们可以根据实际情况进行调整,比如如果你要追求高吞吐量,那么就要放弃可靠性。
  • ack=0,简单来说就是,producer发送一次就不再发送了,不管是否发送成功。
  • ack=-1,简单来说就是,producer只有收到分区内所有副本的成功写入的通知才认为推送消息成功了。

五、kafka高性能之道

5.1顺序写磁盘
将写磁盘的过程变为顺序写,可极大提高对磁盘的利用率。

  • Kafka的整个设计中,Partition相当于一个非常长的数组,而Broker接收到的所有消息顺序写入这个大数组中。同时Consumer通过Offset顺序消费这些数据,并且不删除已经消费的数据,从而避免了随机写磁盘的过程。由于磁盘有限,不可能保存所有数据,实际上作为消息系统Kafka也没必要保存所有数据,需要删除旧的数据。而这个删除过程,并非通过使用“读-写”模式去修改文件,而是将Partition分为多个Segment,每个Segment对应一个物理文件,通过删除整个文件的方式去删除Partition内的数据。这种方式清除旧数据的方式,也避免了对文件的随机写操作。

5.2.充分利用PageCache
使用Page Cache的好处如下:

  • 1.I/O Scheduler会将连续的小块写组装成大块的物理写从而提高性能;

  • 2.I/O Scheduler会尝试将一些写操作重新按顺序排好,从而减少磁盘头的移动时间;

  • 3.充分利用所有空闲内存(非JVM内存)。如果使用应用层Cache(即JVM堆内存),会增加GC负担;

  • 4.读操作可直接在Page Cache内进行。如果消费和生产速度相当,甚至不需要通过物理磁盘(直接通过Page Cache)交换数据;

  • 5.如果进程重启,JVM内的Cache会失效,但Page Cache仍然可用。

  • Broker收到数据后,写磁盘时只是将数据写入Page Cache,并不保证数据一定完全写入磁盘。从这一点看,可能会造成机器宕机时,Page Cache内的数据未写入磁盘从而造成数据丢失。但是这种丢失只发生在机器断电等造成操作系统不工作的场景,而这种场景完全可以由Kafka层面的Replication机制去解决。如果为了保证这种情况下数据不丢失而强制将Page Cache中的数据Flush到磁盘,反而会降低性能。也正因如此,Kafka虽然提供了flush.messages和flush.ms两个参数将Page Cache中的数据强制Flush到磁盘,但是Kafka并不建议使用。如果数据消费速度与生产速度相当,甚至不需要通过物理磁盘交换数据,而是直接通过Page Cache交换数据。同时,Follower从Leader Fetch数据时,也可通过Page Cache完成。

5.3.支持多Disk Drive

  • Broker的log.dirs配置项,允许配置多个文件夹。如果机器上有多个Disk Drive,可将不同的Disk挂载到不同的目录,然后将这些目录都配置到log.dirs里。Kafka会尽可能将不同的Partition分配到不同的目录,也即不同的Disk上,从而充分利用了多Disk的优势。

5.4 零拷贝

  • Kafka中存在大量的网络数据持久化到磁盘(Producer到Broker)和磁盘文件通过网络发送(Broker到Consumer)的过程。这一过程的性能直接影响Kafka的整体吞吐量。传统模式下要进行数据的四次拷贝,但是Kafka通过零拷贝技术将其减为了一次,大大增加了效率

六、kafka api

6.1需要知道的几点

  • 自动提交是按时间段自动提交offset,其提交的是pool收到的消息的最后一个offset,如果拉取了100条消息还没处理完,此时执行了自动提交后机器宕机,则会丢失数据;自动提交默认是开启的,可以使用如下代码禁用:props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, “false”);

  • rebalance后消费者会从分区已提交的offset位点继续消费

  • auto.offset.reset:

- earliest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,从头开始消费;
- latest:当各分区下有已提交的offset时,从提交的offset开始消费;无提交的offset时,消费新产生的该分区下的数据
- none:topic各分区都存在已提交的offset时,从offset后开始消费;只要有一个分区不存在已提交的offset,则抛出异常
  • 实际应用中,消费到的数据处理时长不宜超过max.poll.interval.ms,否则会触发rebalance
  • 如果处理消费到的数据耗时,可以尝试通过减小max.poll.records的方式减小单次拉取的记录数(默认是500条)
  • 业务如果想不丢数据且不重复消费,需要自己记录offset自己手动提交offset,并做消息重复过滤

使用kafka需引入依赖

<dependency>
    <groupId>org.apache.kafka</groupId>
    <artifactId>kafka-clients</artifactId>
    <version>2.0.1</version>
</dependency>

6.2 生产者代码


public class MyKafkaProducer {

    public static void main(String[] args) throws Exception, InterruptedException {
        Properties props = new Properties();
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");
        props.put(ProducerConfig.ACKS_CONFIG, "all");// leader 确认机制 0 1 all
        props.put(ProducerConfig.RETRIES_CONFIG, 1);// 重试次数
        props.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384);// 生产者批发送大小
        props.put(ProducerConfig.LINGER_MS_CONFIG, 1);// 生产者达不到批发送大小,最短等待时间
        props.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432);// RecordAccumulator 缓冲区大小
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");// key的序列化器
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer");// value的序列化器

        Producer<String, String> producer = new KafkaProducer<>(props);

        try {
            for (int i = 0; i < 100; i++) {
                //指定了key会按keyHash对分区数取余决定发到哪个分区,没有指定key会自动生成一个自增id
                ProducerRecord<String, String> record = new ProducerRecord<>("testoffset", Integer.toString(i), Integer.toString(i));
                //默认都是异步发送,如果想同步发送producer.send(xxx).get()单条发送
                producer.send(record, new Callback() {
                    @Override
                    public void onCompletion(RecordMetadata recordMetadata, Exception exception) {
                        if (exception != null) {
                            //当存在异常的时候,失败逻辑
                            System.out.println("发送异常");
                        } else {
                            //发送成功之后这里的RecordMetadata内包含此条消息写入到JDQ集群的分区和对应的位点信息等
                            System.out.println("success");
                        }
                    }
                });
            }
            /**
             *  flush :  执行flush代表内存中的消息数据都会send到服务端,可根据callback来判断成功与否
             *  自己控制flush
             *  注意:这里可发送一批数据之后再掉flush,达到批量的效果
             */
            producer.flush();
        } finally {
            producer.close(); //关闭消费者
        }
    }
}

6.3 消费者代码

public class MyKafkaConsumer {

    /**
     * 参考方法:
     * <p>
     * consumer.partitionsFor(topic)
     * 查询topic的分区信息,当本地没有这个topic的元数据信息的时候会往服务端发送的远程请求
     * 注意: 没有权限的topic的时候会抛出异常(org.apache.kafka.common.errors.TopicAuthorizationException)
     * consumer.assignment()
     *      查询指定方式或订阅方式分配到当前消费者的分区信息,还未订阅或重新分配时为空
     * <p>
     * consumer.position(new TopicPartition(topic, 0))
     * 获取下次拉取的数据的offset, 如果没有offset记录则会抛出异常
     * <p>
     * consumer.committed(new TopicPartition(topic, 0))
     * 获取已提交的位点信息,如果没有查询到则返回null
     * <p>
     * consumer.beginningOffsets(Arrays.asList(new TopicPartition(topic, 0)));
     * consumer.endOffsets(Arrays.asList(new TopicPartition(topic, 0)));
     * consumer.offsetsForTimes(timestampsToSearch);
     * 查询最小,最大,或者任意时间的位点信息
     * <p>
     * consumer.seek(new TopicPartition(topic, 0), 10972);
     * consumer.seekToBeginning(Arrays.asList(new TopicPartition(topic, 0)));
     * consumer.seekToEnd(Arrays.asList(new TopicPartition(topic, 0)));
     * 指定offset消费,seek调用需要写到ConsumerRebalanceListener中否则会报错No current assignment for partition
     * 或者可以使用assign手动分配分区就可以直接使用seek方法,但是assign不会进行rebalance
     * <p>
     * consumer.assign(Arrays.asList(new TopicPartition(topic, 0)));
     * 手动分配消费的topic和分区进行消费,这里不会出发group management操作,指定分区消费数据
     * 和subscribe方法互斥,如果assign 之后或者之后调用subscribe 则会报错,不允许再进行分配,2方法不能一起使用
     * <p>
     * consumer.subscribe(topics);
     * 自动发布消费的topic进行消费,这里触发group management操作
     * 和assign方法互斥,如果subscribe 之后或者之后调用assign 则会报错,不允许再进行分配,2方法不能一起使用
     * <p>
     * 注: group management
     * 根据group 进行topic分区内部的消费rebanlance
     * 例如消费的topic包含3个分区,启动了4个相同鉴权的客户端消费
     * 分区0 -- consumer1   分区1 -- consumer2   分区2 --- consumer3    consumer4则会空跑不消费数据
     * 当分区consumer1挂掉的时候则会出现rebalance,之后变为
     * 分区0 -- consumer2   分区1 -- consumer3   分区2 --- consumer4
     */
    private static AtomicBoolean running = new AtomicBoolean(true);
    public static void main(String[] args) throws Exception {
        Properties props = new Properties();
        props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "127.0.0.1:9092");//
        props.put(ConsumerConfig.GROUP_ID_CONFIG, "miner"); //消费者组
        props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "10000");
        props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); //自动提交
        props.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, "1"); //每次最多拉取1条

        //(1)实际应用中,消费到的数据处理时长不宜超过max.poll.interval.ms,否则会触发rebalance
        //(2)如果处理消费到的数据耗时,可以尝试通过减小max.poll.records的方式减小单次拉取的记录数(默认是500条)
        //指定consumer两次poll的最大时间间隔(默认5分钟),如果超过了该间隔consumer client会主动向coordinator发起LeaveGroup请求,触发rebalance;然后consumer重新发送JoinGroup请求
        props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, "10000"); //两次pool最大时间间隔
        //props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000"); //自动提交最短时间
        //key反序列化类
        props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
        //value反序列化类
        props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);

        Collection<String> topics = Arrays.asList("testoffset");
        consumer.subscribe(topics, new ConsumerRebalanceListener() {
            @Override
            public void onPartitionsRevoked(Collection<TopicPartition> collection) {
                System.out.println("====rebalance前");
            }

            @Override
            public void onPartitionsAssigned(Collection<TopicPartition> collection) {
                System.out.println("====rebalance后");
                for (TopicPartition topicPartition:collection) {
                    //指定offset拉取
                    consumer.seekToBeginning(Collections.singletonList(new TopicPartition("testoffset", 0)));
                }
            }
        });
        //手动指定分区方式订阅
        /*List<TopicPartition> list = Collections.singletonList(new TopicPartition("testoffset", 0));
        consumer.assign(list);
        List<PartitionInfo> partitionInfos = consumer.partitionsFor("testoffset");
        for (PartitionInfo partitionInfo:partitionInfos) {
            int partitionNumber = partitionInfo.partition();
            OffsetAndMetadata offsetAndMetadata = consumer.committed(new TopicPartition("testoffset", partitionNumber));
            long offset = offsetAndMetadata.offset();
            TopicPartition topicPartition = new TopicPartition("testoffset", partitionNumber);
            consumer.seek(topicPartition, 0);
        }*/
        //查看已提交的offset
/*        OffsetAndMetadata testoffset = consumer.committed(new TopicPartition("testoffset", 0));
        System.out.println("===1:" + testoffset.metadata());
        System.out.println("===2:" + testoffset.offset());*/

        while (true) {
            //没有分到分区的消费者record size=0
            ConsumerRecords<String, String> records = consumer.poll(Duration.ofSeconds(3)); //拉取数据
            if (records != null && !records.isEmpty()) {
                for (ConsumerRecord<String, String> record : records) {
                    System.out.printf("====:partition = %s,offset = %d, key = %s, value= %s%n", record.partition(), record.offset(), record.key(), record.value());
                }
            }
            Thread.sleep(1000);
            if(!running.get()){
                break;
            }
        }
        //异步提交
        consumer.commitAsync(new OffsetCommitCallback() {
            @Override
            public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {

            }
        });

        consumer.close();
    }
}
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值