1. KAFKA 简介

kafka:基于发布订阅模式的消息队列

kafka优点:削峰,解耦,高并发,高性能,可热拓展。

2. zookeeper & Kafka安装

版本信息

zookeeperscala:2.11kafka:0.11

安装步骤

  • 下载Scala以及Kafka压缩包
  • 复制到虚拟主机指定文件夹下并执行解压操作
# 先安装Scala
tar -zxvf scala-2.11.6.tgz
# 重命名
mv scala-2.11.6 scala
# 修改配置文件
vim /etc/profile
# 添加Scala环境变量
export SCALA_HOME=/usr/scala/scala-2.11.6
export PATH=$PATH:$SCALA_HOME/bin
# 添加完成以后执行配置文件
source /etc/profile
# zookeeper 安装
wget http://mirror.bit.edu.cn/apache/zookeeper/stable/apache-zookeeper-3.5.8-bin.tar.gz
# 安装后解压并重命名
tar -zxvf apache-zookeeper-3.5.8-bin.tar.gz
mv apache-zookeeper-3.5.8-bin zookeeper
# Kafka解压
tar -zxvf kafka_2.11-0.11.0.0.tgz
# 解压后重命名
mv kafka_2.11-0.11.0.0 kafka
# 配置上述软件的环境变量
vim /etc/profile
# 修改如下
KAFKA_HOME=/usr/local/kafka
SCALA_HOME=/usr/local/scala
ZOOKEEPER_HOME=/usr/local/zookeeper
PATH=$PATH:$JAVA_HOME/bin:$JAVA_HOME/jre/bin:$SCALA_HOME/bin:$KAFKA_HOME/bin:$ZOOKEEPER_HOME
export PATH CLASSPATH JAVA_HOME KAFKA_HOME SCALA_HOME ZOOKEEPER_HOME
# 执行配置文件
source /etc/profile

3. Zookeeper 说明

zoo.cfg配置文件

在zookeeper解压完成以后,conf文件夹有一个配置的样本文件 zoo_example.cfg 的配置文件。我们需要复制该文件内容到zoo.cfg文件中,并完成其他参数的设置。

# 服务器之间或客户端与服务器之间维持心跳的时间间隔,单位毫秒
tickTime=2000
# leader和flower之间初始连接最多心跳数
initLimit=10
# leader和flower之间请求与应答之间最多心跳数
syncLimit=5
# 数据保存目录
dataDir=/usr/local/zookeeper/data
# 日志保存目录
dataLogDir=/usr/local/zookeeper/logs
# 配置zookeeper端口号
clientPort=2181
# 配置zookeeper集群的其他服务的地址
# 集群信息(服务器编号,服务器地址,LF通信端口,选举端口)
server.1=hadoop1:2888:3888
server.2=hadoop2:2888:3888
server.3=hadoop3:2888:3888

在修改完成以后,还需要在配置的dataDir目录下创建myid配置文件,文件内容为上述server后面的序号。

本案例里面有三个服务,因此对应的序号分别是1,2,3。需要在三台机器上面分别配置好myid文件。

其他注意事项

1. zookeeper运行日志文件位于dataLogDir指定的目录下,文件名一般为 zookeeper-root-server-xxxxx.out
2. zookeeper启动文件位于bin目录下,启动命令为 zkServer.sh start, 关闭命令为 zkServer.sh stop
3. 第一次启动后可以查看下日志文件的状态,确保集群是启动成功的。不然后面坑多,难排查。

zk客户端使用说明

在kafka集群中,很多信息都保存在zookeeper中。因此,使用zookeeper的客户端查看相关信息就非常重要。

# 在服务器上启动zkCli.sh
sh zkCli.sh

启动成功后会显示这样的用户信息 [zk: localhost:2181(CONNECTED) 0]

4. Kafka 说明

servier.properties文件说明

# broker的id,必须是独一无二的整数
broker.id=0
# 是否可以删除主题
delete.topic.enable=true
# 设置kafka暂存数据的路径,而不是日志文件
log.dirs=/tmp/kafka-logs
# 配置所有的zookeeper节点的位置
zookeeper.connect=hadoop1:2181,hadoop2:2181,hadoop3:2181

Kafka的基本使用

  1. 启动与关闭
# daemon表示后端启动,无需占用前端窗口。启动时需要指定配置文件
kafka-server-start.sh -daemon /usr/local/kafka/config/server.properties
# kafka日志文件位于logs文件夹下,名称为server.log。如果kafka启动失败,可以从这里排查原因
# 关闭kafka
kafka-server-stop.sh
  1. 其他使用
# 查看主题
bin/kafka-topics.sh --list --zookeeper hadoop1:2181
# 创建主题
kafka-topics.sh --create --zookeeper hadoop1:2181 --topic hello --replication-factor 1 --partitions 3
# 控制台生产者(主要用于调试)
bin/kafka-console-producer.sh --broker-list hadoop1:9092 -topic atest
# 控制台消费者(主要用于调试)
bin/kafka-console-consumer.sh --zookeeper hadoop2:2181 --topic atest
# 查看offset情况(修改consumer.properties配置文件)
bin/kafka-console-consumer.sh --topic __consumer_offsets --zookeeper hadoop3:2181 --formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter" --consumer.config config/consumer.properties --from-beginning
# 上述指令如果出现提示:Found previous offset information for this group Avengers. Please use --delete-consumer-offsets to delete previous offsets metadata,在原有命令中加入 --delete-consumer-offsets即可

补充:replication-factor指定数量以后,该主题会在指定数量的broker上创建该主题对应的分区。

kafka Xmx kafka xmx作用_kafka Xmx

注意:在创建主题时,如果可用的broker数量小于指定的replication-factor数量,则该创建将会失败并提示以下错误:

kafka Xmx kafka xmx作用_数据_02


broker数量必须大于等于replication-factor数量。

创建的partition会随机分配到不同的broker上。每个broker可以对应一个topic的多个partition,但是不会出现重复的partitions。

# 描述一个主题
bin/kafka-topics.sh --describe --topic atest --zookeeper hadoop1:2181

kafka Xmx kafka xmx作用_数据_03

5. Kafka 架构

kafka Xmx kafka xmx作用_zookeeper_04

主要说明:

  1. 生产者指定主题发送消息,默认以轮询的方式均匀的发送到对应主题的各个partition
  2. 消费者以消费者组(Consumer Group)形式组织,当分区数与消费者数量相同时,为每一个消费者指定一个分区。
  3. 0.9版本以后,offset存在与broker本地
  4. Kafka通过Zookeeper管理集群配置,选举leader,以及在Consumer Group发生变化时进行rebalance。
  5. Producer使用push模式将消息发布到broker,Consumer使用pull模式从broker订阅并消费消息。

Zookeeper在Kafka中的作用

Zookeeper作用:管理broker、consumer

  1. kafka使用zookeeper来实现动态的集群扩展,不需要更改客户端(producer和consumer)的配置。broker会在zookeeper注册并保持相关的元数据(topic,partition信息等)更新。Broker端使用zookeeper来注册broker信息,以及监测partition leader存活性实现leader的选举。
  2. 客户端(producer和consumer)会在zookeeper上注册相关的watcher。一旦zookeeper发生变化,客户端能及时感知并作出相应调整。这样就保证了添加或去除 broker时,各broker间仍能自动实现负载均衡。
  3. Consumer端使用zookeeper用来注册consumer信息,其中包括consumer消费的partition列表等,同时也用来发现broker列表,并和partition leader建立socket连接,并获取消息。同时Consumer Group的Rebalance也是在zookeeper实现。

partition存储解析

data文件夹解析:

其中一个partition文件夹的截图:

kafka Xmx kafka xmx作用_数据_05


其中xxx.index文件用于维护该分区的offset,xxx.log文件用于存储该分区的消息。

在kafka/config/server.properties中有如下参数:

# The minimum age of a log file to be eligible for deletion due to age
# 消息的存留时间(默认7天)
log.retention.hours=168

# The maximum size of a log segment file. When this size is reached a new log segment will be created.
# .log文件的大小(默认1G),超过该大小时将会新建一个新的.log文件
log.segment.bytes=1073741824

为了防止log文件过大,kafka引入了分片(Segment)–对应xxxxxx.log文件和索引(index)–对应xxxxxxx.index文件。两者一一对应

xxxxxx表示为当前文件中的消息的起始索引(offset)。

在.index文件中存储着offset以及消息对应的起始地址和消息的大小,通过offset找到消息起始地址和消息大小后就可以快速的在.log文件中对应到消息数据。

kafka Xmx kafka xmx作用_zookeeper_06

6. Kafka 生产者

分区策略

  1. 为什么要分区
  • 方便在集群中拓展,每个partition可以通过调整适应所在机器。一个主题可以有多个partition,这样就可以适应不同大小的数据
  • 提高并发性,每个partition可以单独读写
  1. 分区原则
  • 指明partition的情况,直接发送到对应的partition。
  • 没有指明partition,但是有key值:将key的hash值对partition的总数取余然后得到对应的partition。
  • 上述两个都没有的情况,采用round-robin算法:在第一次时候随机取一个整数,后续在该整数基础上递增,对partition进行取余,得到对应的partition号。

数据可靠性策略

  1. 副本数据同步策略

    Kafka采用第二种同步策略,因为该同步策略数据冗余度较低。
  2. ISR机制(in-sync replica set)
    Leader 维护了一个动态的 in-sync replica set (ISR),意为和 leader 保持同步的 follower 集合。当 ISR 中的 follower 完成数据的同步之后, leader 就会给生产者发送 ack。如果 follower长 时 间 未 向 leader 同 步 数 据 , 则 该 follower 将 被 踢 出 ISR,该时间阈值由replica.lag.time.max.ms参数设定。 Leader 发生故障之后,就会从 ISR 中选举新的 leader。
  3. ACK应答机制(生产者角度)
    对于某些不太重要的数据,对数据的可靠性要求不是很高,能够容忍数据的少量丢失,所以没必要等 ISR 中的 follower 全部接收成功。所以 Kafka 为用户提供了三种可靠性级别,用户根据对可靠性和延迟的要求进行权衡,选择以下的配置。
    acks 参数配置:
    0 - At Most Once: producer 不等待 broker 的 ack,这一操作提供了一个最低的延迟, broker 一接收到还没有写入磁盘就已经返回,当 broker 故障时有可能丢失数据
    1:producer 等待 broker 的 ack, partition 的 leader 落盘成功后返回 ack,如果在 follower同步成功之前 leader 故障,那么将会丢失数据
    -1(all)- At Least Once: producer 等待 broker 的 ack, partition 的 leader 和 follower 全部落盘成功后才返回 ack。但是如果在 follower 同步完成后, broker 发送 ack 之前, leader 发生故障,那么会造成数据重复
    Exactly Once 语义
    将服务器的 ACK 级别设置为-1,可以保证 Producer 到 Server 之间不会丢失数据,即 At Least Once 语义。
    At Least Once 可以保证数据不丢失,但是不能保证数据不重复;
    相对的,将服务器 ACK 级别设置为 0,可以保证生产者每条消息只会被发送一次,即 At Most Once 语义。
    At Most Once可以保证数据不重复,但是不能保证数据不丢失。
    但是,对于一些非常重要的信息,比如说交易数据,下游数据消费者要求数据既不重复也不丢失,即 Exactly Once 语义。
    在 0.11 版本以前的 Kafka,对此是无能为力的,只能保证数据不丢失,再在下游消费者对数据做全局去重。对于多个下游应用的情况每个都需要单独做全局去重,这就对性能造成了很大影响。
    0.11 版本的 Kafka,引入了一项重大特性:幂等性。所谓的幂等性就是指 Producer 不论向 Server 发送多少次重复数据, Server 端都只会持久化一条。幂等性结合 At Least Once 语义,就构成了 Kafka 的 Exactly Once 语义。即:
    At Least Once + 幂等性 = Exactly Once
    要启用幂等性,只需要将 Producer 的参数中 enable.idompotence 设置为 true 即可。 Kafka的幂等性实现其实就是将原来下游需要做的去重放在了数据上游。开启幂等性的 Producer 在初始化的时候会被分配一个 PID,发往同一 Partition 的消息会附带 Sequence Number。而Broker 端会对<PID, Partition, SeqNumber>做缓存,当具有相同主键的消息提交时, Broker 只会持久化一条。但是 PID 重启就会变化,同时不同的 Partition 也具有不同主键,所以幂等性无法保证跨分区跨会话的 Exactly Once。
  4. 故障处理细节(消费者角度)
  • follower故障
    当follower发生故障时,leader会将其踢出ISR。当follower从故障中恢复时,会查询本地的HW,将HW以后的全部丢弃并从leader开始继续同步。当该follower同步到最新的HW后(即该follower的LEO>=当前HW时),leader将其加入到ISR中。
  • leader故障
    leader发生故障时,followers选出新的leader后,其他follower会将超过HW的部分丢弃,并从新的leader开始同步HW以后的数据。如果故障leader恢复,则参考follower的故障恢复步骤进行同步。

注意,上述的故障恢复机制只能保证数据在follower和leader之间的一致性,不能保证生产者的消息不会丢失。

7. Kafka 消费者

消费方式

Consumer采用pull方式从broker中拉取数据。

为什么不采取push方式:push方式难以适应不同消费速率的消费者,对于消费者的性能要求比较高。而pull消费方式速度掌握在消费者手里,可以根据自身能力进行消费。

pull机制的缺点:如果kafka的消息消费完了,而消费者还是会循环消费,返回空数据。针对这一点,可以在消费者消费数据时传入一个时长参数timeout,使得消费者在没有数据消费时等待timeout时间再消费。

分区分配策略

由于消费者这边有consumer group概念,而topic又有不同的partition。如何保证每个partition中的消息都得到消费且不会出现重复消费,分区分配的策略就非常必要。

两种策略的指定:通过设置 partition.assignment.strategy,参数选择 range 或 roundrobin

  • RoundRobin轮询
    在轮询策略下,主题是以组的形式消费。消费者对于主题的指定是无法保证的。
    该组中对应的主题,会以partition为单位,轮询分配组里的消费者进行消费。
    比如一个消费者组中,consumer1消费的主题是t1,consumer2消费的主题是t2。每个主题都有三个分区,在这种情况下,该组需要消费的集合是{t1-p0,t1-p1,t1-p2,t2-p0,t2-p1,t2-p2},kafka排序规则是:主题名+分区号的hash值进行排序。
    可能的分配方式是,consumer1消费{t1-p0,t1-p2,t2-p1},consumer2消费{t1-p1,t2-p0,t2-p2}
    RoundRobin适用条件:一个消费者组中所有消费者消费的都是同一个主题
    优点:由于是轮询的分配策略,每个消费者消费的分区数差距在1个以内。
    缺点:不能给消费者指定某一个主题消费。而是根据组里所有主题分配。
  • Range
    Range策略是kafka中默认的策略。它可以指定消费者组中不同消费者消费不同的主题。
    在该策略下,消费者组会给指定了主题的消费者分配对应的分区,而没有指定主题的消费者则消费不到该主题。
    Range策略下,假设有{t1-p0},{t2-p0,t2-p1},{t3-p0,t3-p1,t3-p2} 三个主题,一个消费者组有两消费者C1,C2。
    其中C1消费主题t1,t2;C2消费主题t2,t3;
    在Range策略下,首先将分区数量除以总的消费者数量,如果不能整除,则前面的消费者承担多出的部分,根据相邻分配的原则,可能的方案是:C1分配到{t1-p0,t2-p0}两个分区,C2分配到{t2-p1,t3-p0,t3-p1,t3-p2}四个分区。
    优点:每个消费者可以只消费自己指定的主题。不会消费其他主题。
    缺点:每个消费者分配的分区数量不均匀。

Rebalance(再平衡)

当以下任意条件触发时,会导致rebalance

  • 分区数增加
  • 新的消费者加入
  • 现有消费者退出、宕机或者取消订阅

重新分配的过程中,原有的已经分配的分区会回收再进行重新分配,也就是说即使正常消费的消费者,其分区也会被重新分配。

offset的维护

offset的维护对于kafka的可恢复运行很重要。每个分区都会一个对应的offset,用于记录该分区消费到的位置。以便于消费者能够继续上次消费后的位置继续消费。

kafka Xmx kafka xmx作用_zookeeper_07

8. Kafka 高效读写数据

顺序写磁盘

Kafka 的 producer 生产数据,要写入到 log 文件中,写的过程是一直追加到文件末端,为顺序写。 官网有数据表明,同样的磁盘,顺序写能到 600M/s,而随机写只有 100K/s。这与磁盘的机械机构有关,顺序写之所以快,是因为其节省大量磁头寻址的时间

零复制技术

kafka Xmx kafka xmx作用_kafka_08

零复制技术减少了文件IO过程中COPY的次数,将原来的4次拷贝减少到2次拷贝。

文件读写代码如下:

read(file, tmp_buf, len);
write(socket, tmp_buf, len);

上述两步代码在kernel 1.0版本中对应的步骤如下:

  1. 执行read()函数,将文件从磁盘拷贝到内核缓冲区
  2. 文件数据从内核缓冲区拷贝到用户进程缓冲区
  3. 执行write函数,文件数据从用户进程缓冲区拷贝到socket相关的缓冲区
  4. 数据从socket相关缓冲区拷贝到相关协议引擎

为什么需要执行这么复杂的步骤?

内核缓冲区是为了从操作系统级别优化磁盘IO的效率

用户进程缓冲区是为了减少系统调用的次数

9. Kafka 事务

Kafka 从 0.11 版本开始引入了事务支持。事务可以保证 Kafka 在 Exactly Once 语义的基础上,生产和消费可以跨分区和会话,要么全部成功,要么全部失败。

Producer 事务

为了实现跨分区跨会话的事务,需要引入一个全局唯一的 Transaction ID,并将 Producer获得的PID 和Transaction ID 绑定。这样当Producer 重启后就可以通过正在进行的 TransactionID 获得原来的 PID。

为了管理 Transaction, Kafka 引入了一个新的组件Transaction Coordinator。 Producer 就是通过和 Transaction Coordinator 交互获得 Transaction ID 对应的任务状态。 Transaction Coordinator 还负责将事务所有写入 Kafka 的一个内部 Topic,这样即使整个服务重启,由于事务状态得到保存,进行中的事务状态可以得到恢复,从而继续进行。

Consumer 事务

上述事务机制主要是从 Producer 方面考虑,对于 Consumer 而言,事务的保证就会相对较弱,尤其时无法保证 Commit 的信息被精确消费。这是由于 Consumer 可以通过 offset 访问任意信息,而且不同的 Segment File 生命周期不同,同一事务的消息可能会出现重启后被删除的情况。

10. Kafka API 使用

Producer API

发消息的流程

Kafka 的 Producer 发送消息采用的是异步发送的方式。

在消息发送的过程中,涉及到了两个线程——main 线程和 Sender 线程,以及一个线程共享变量RecordAccumulator

main 线程将消息发送给 RecordAccumulator, Sender 线程不断从RecordAccumulator 中拉取消息发送到 Kafka broker。

kafka Xmx kafka xmx作用_kafka_09

相关参数:
batch.size: 只有数据积累到 batch.size 之后, sender 才会发送数据。
linger.ms: 如果数据迟迟未达到 batch.size, sender 等待 linger.time 之后就会发送数据。

异步发送 API

  • 首先导入依赖
    依赖版本尽量与集群的版本保持一致
<dependency>
            <groupId>org.apache.kafka</groupId>
            <artifactId>kafka-clients</artifactId>
            <version>0.11.0.0</version>
</dependency>
  • 编写API

主要用到的类如下:

KafkaProducer: 需要创建一个生产者对象,用来发送数据。

ProducerConfig:获取需要配置的参数。

ProducerRecord:每条数据需要封装成一个ProducerRecord对象。

  • 不带回调函数方式API
public class MyProducer {
    public static void main(String[] args) {
        Properties properties = new Properties();
        //配置集群地址
  properties.put("bootstrap.servers","192.168.60.131:9092,192.168.60.131:9092,192.168.60.131:9092");
        //acks表示消息的确认机制
        properties.put("acks","all");
        //重试次数
        properties.put("retries",1);
        //批次的大小
        properties.put("batch.size",16384);
        //等待时间
        properties.put("linger.ms",1);
        //缓冲区大小
        properties.put("buffer.memory",33554432);
        //序列化方式
        properties.put("key.serializer","org.apache.kafka.common.serialization.StringSerializer");
        properties.put("value.serializer","org.apache.kafka.common.serialization.StringSerializer");
		//创建生产者
        Producer<String,String> producer = new KafkaProducer<String, String>(properties);
		//发送消息
        for (int i = 0; i < 10; i++) {
            System.out.println(i);
            //第一个参数为主题,如果该主题不存在,则集群会自动创建该主题
            producer.send(new ProducerRecord<String, String>("ccc","hello-" + i));
        }
        //关闭流资源
        producer.close();
    }
}

带回调函数的方式

回调函数会在 producer 收到 ack 时调用,为异步调用, 该方法有两个参数,分别是RecordMetadataException,如果 Exception 为 null,说明消息发送成功,如果Exception 不为 null,说明消息发送失败。
注意:消息发送失败会自动重试,不需要我们在回调函数中手动重试。

public class MyProducer {
    public static void main(String[] args) {
        Properties props = new Properties();       props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.60.131:9092,192.168.60.131:9092,192.168.60.131:9092");
        props.put(ProducerConfig.ACKS_CONFIG,"all");
        props.put(ProducerConfig.RETRIES_CONFIG,1);
        props.put(ProducerConfig.BATCH_SIZE_CONFIG,16834);
        props.put(ProducerConfig.LINGER_MS_CONFIG,1);
        props.put(ProducerConfig.BUFFER_MEMORY_CONFIG,33554432);
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
        Producer<String,String> producer = new KafkaProducer(props);
        for (int i = 0; i < 10; i++) {
            producer.send(new ProducerRecord<String, String>("atest", "hello-" + i), new Callback() {
                public void onCompletion(RecordMetadata recordMetadata, Exception e) {
                    if (e == null){
                        System.out.println("success->" + recordMetadata.offset());
                    }else{
                        System.out.println("消息发送失败");
                        e.printStackTrace();
                    }
                }
            });
        }
        producer.close();
    }
}

同步发送API

同步发送的意思就是,一条消息发送之后,会阻塞当前线程, 直至返回 ack。
由于 send 方法返回的是一个 Future 对象,根据 Futrue 对象的特点,我们也可以实现同步发送的效果,只需在调用 Future 对象的 get 方法即可。

RecordMetadata ccc = producer.send(new ProducerRecord<String, String>("ccc", "hello-" + i)).get();
System.out.println("success->" + ccc.partition()+ ":" + ccc.offset());

自定义分区器 Partitioner

通过Partitioner接口,可以找到默认的分区器DefaultPartitioner(在接口源码处CTRL+H呼出)

源码如下:

public class DefaultPartitioner implements Partitioner {
    private final ConcurrentMap<String, AtomicInteger> topicCounterMap = new ConcurrentHashMap();
    public DefaultPartitioner() {
    }

    public void configure(Map<String, ?> configs) {
    }
	/**
		分区策略的实现
	*/
    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();
        //判断是否设置了key
        if (keyBytes == null) {
            //没有key,则使用roundrobin方式进行分区分配
            int nextValue = this.nextValue(topic);
            //获取存活的partition数量
            List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
            if (availablePartitions.size() > 0) {
                //返回模运算后的partition序号
                int part = Utils.toPositive(nextValue) % availablePartitions.size();
                return ((PartitionInfo)availablePartitions.get(part)).partition();
            } else {
                return Utils.toPositive(nextValue) % numPartitions;
            }
        } else {
            //有key值,则根据key值进行模运算返回分区号
            return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
        }
    }
	/**
		round-robin策略下的值
		第一次回随机创建一个值,后面每次调用都会递增
	*/
    private int nextValue(String topic) {
        AtomicInteger counter = (AtomicInteger)this.topicCounterMap.get(topic);
        if (null == counter) {
            counter = new AtomicInteger(ThreadLocalRandom.current().nextInt());
            AtomicInteger currentCounter = (AtomicInteger)this.topicCounterMap.putIfAbsent(topic, counter);
            if (currentCounter != null) {
                counter = currentCounter;
            }
        }
        return counter.getAndIncrement();
    }

    public void close() {
    }
}

自定义分区器的实现和使用步骤如下:

  • 创建一个自定义类,实现partition接口,实现接口的方法
public class MyPartitioner implements Partitioner {
    public int partition(String s, Object o, byte[] bytes, Object o1, byte[] bytes1, Cluster cluster) {
        //此处根据业务需求实现分区策略
        return 0;
    }
    public void close() {

    }
    public void configure(Map<String, ?> map) {

    }
}
  • 创建完成以后,在初始化生产者时,在properties中添加一个键值对
properties.put("partitioner.class","im.hwp.partitioner.MyPartitioner");
  • 再次发送消息,就会根据自定义的分区器的分区策略进行发送

Consumer API

Consumer 消费数据时的可靠性是很容易保证的,因为数据在 Kafka 中是持久化的,故不用担心数据丢失问题。
由于 consumer 在消费过程中可能会出现断电宕机等故障, consumer 恢复后,需要从故障前的位置的继续消费,所以 consumer 需要实时记录自己消费到了哪个 offset,以便故障恢复后继续消费。
所以 offset 的维护是 Consumer 消费数据是必须考虑的问题

自动提交offset

自动提交是以指定的时间间隔为单位提交Offset,通过该参数设置 ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG

public class MyConsumer {
    public static void main(String[] args) {
        Properties pros = new Properties();
        //集群信息
 pros.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.60.131:9092,192.168.60.132:9092,192.168.60.133:9092");
        //开启自动提交
        pros.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"true");
        //自动提交的时间间隔
        pros.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000");
        //key,value反序列化
pros.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer");        pros.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer");
        //消费者组
        pros.put(ConsumerConfig.GROUP_ID_CONFIG,"cg");
        //创建消费者
        KafkaConsumer<String,String> kafkaConsumer = new KafkaConsumer<String,String>(pros);
        //订阅主题
        kafkaConsumer.subscribe(Collections.singletonList("hello"));
        while (true){
            //获取消息
            ConsumerRecords<String, String> msg = kafkaConsumer.poll(100);
            //解析并打印
            for (ConsumerRecord<String, String> consumerRecord : msg) {
                System.out.println(consumerRecord.partition()+ ":" + consumerRecord.offset() + ":" + consumerRecord.value());
            }
        }
    }
}

手动提交Offset

  • 同步提交方式 commitSync
    同步提交方式中,提交offset过程会阻塞后续的消费任务,等提交成功以后才会继续消费。一般生产环境不会这么干~
public class MyConsumer {
    public static void main(String[] args) {
        Properties pros = new Properties();
        //集群信息
        pros.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.60.131:9092,192.168.60.132:9092,192.168.60.133:9092");
        //关闭自动提交
        pros.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"false");
        //key,value反序列化
        pros.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer");
        pros.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer");
        //消费者组
        pros.put(ConsumerConfig.GROUP_ID_CONFIG,"cg");
        //创建消费者
        KafkaConsumer<String,String> kafkaConsumer = new KafkaConsumer<String,String>(pros);
        //订阅主题
        kafkaConsumer.subscribe(Collections.singletonList("hello"));

        while (true){
            //获取消息
            ConsumerRecords<String, String> msg = kafkaConsumer.poll(100);
            //解析并打印
            for (ConsumerRecord<String, String> consumerRecord : msg) {
                System.out.println(consumerRecord.partition()+ ":" + consumerRecord.offset() + ":" + consumerRecord.value());
            }
            //同步提交offset
            kafkaConsumer.commitSync();
        }



    }
}
  • 异步提交offset commitSync
public class MyConsumer {
    public static void main(String[] args) {
        Properties pros = new Properties();
        //集群信息
        pros.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.60.131:9092,192.168.60.132:9092,192.168.60.133:9092");
        //关闭自动提交
        pros.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"false");
 pros.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer");
        pros.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringDeserializer");
        //消费者组
        pros.put(ConsumerConfig.GROUP_ID_CONFIG,"cg");
        //创建消费者
        KafkaConsumer<String,String> kafkaConsumer = new KafkaConsumer<String,String>(pros);
        //订阅主题
        kafkaConsumer.subscribe(Collections.singletonList("hello"));

        while (true){
            //获取消息
            ConsumerRecords<String, String> msg = kafkaConsumer.poll(100);
            //解析并打印
            for (ConsumerRecord<String, String> consumerRecord : msg) {
                System.out.println(consumerRecord.partition()+ ":" + consumerRecord.offset() + ":" + consumerRecord.value());
            }
            kafkaConsumer.commitAsync(new OffsetCommitCallback() {
                public void onComplete(Map<TopicPartition, OffsetAndMetadata> map, Exception e) {
                    if (map != null){
                        System.out.println("提交成功,"+map.toString());
                    }
                }
            });
        }
    }
}

注意,该回调函数返回的Map<TopicPartition, OffsetAndMetadata>中,包括了该主题下所有分区的offset信息,而这些offset表示的是消费者下一次应该消费的offset的起始序号。

{
    hello-2=OffsetAndMetadata{offset=180, metadata=''}, 
	hello-0=OffsetAndMetadata{offset=0, metadata=''}, 
	hello-1=OffsetAndMetadata{offset=10, metadata=''}
}

两种消费方式可能出现的问题

  1. 数据漏消费
    在自动提交的方式下,消费者拿到N条消息。在处理这N条消息的过程中,由于自动提交的机制,集群端的offset已经更新。而消费者如果在这N条消息没有处理完成的情况下宕机,则下次重新启动时,这N条消息就无法再次被消费从而造成漏消费情况。
  2. 数据重复消费
    在手动提交的方式下,消费者拿到N条消息,如果消费过程中已经处理了M条,此时消费者宕机,而手动提交的程序还未执行。则下次启动的时候消费过的这M条消息就会被重复消费。

自定义offset存储

offset 的维护是相当繁琐的, 因为需要考虑到消费者的 Rebalace。
当有新的消费者加入消费者组、 已有的消费者推出消费者组或者所订阅的主题的分区发生变化,就会触发到分区的重新分配,重新分配的过程叫做 Rebalance。
消费者发生 Rebalance 之后,每个消费者消费的分区就会发生变化。消费者首先要获取到自己被分配的分区,并能够定位到该分区最新提交的offset位置继续消费。

自定义offset存储的实现主要是根据ConsumerRebalanceListener这个类,重载其中的两方法实现offset的提交和offset的获取。

public class CustomConsumer {
    private static Map<TopicPartition, Long> currentOffset = new HashMap<TopicPartition, Long>();
    public static void main(String[] args) {
        Properties pros = new Properties();
        //集群信息
        pros.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.60.131:9092,192.168.60.132:9092,192.168.60.133:9092");
        pros.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
        //key,value反序列化
        pros.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
        pros.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer");
        //消费者组
        pros.put(ConsumerConfig.GROUP_ID_CONFIG, "cg");
        //创建消费者
        final KafkaConsumer<String, String> kafkaConsumer = new KafkaConsumer<String, String>(pros);
        //订阅主题
        kafkaConsumer.subscribe(Collections.singletonList("hello"), new ConsumerRebalanceListener(){
            //该方法在rebalance之前调用,提交offset
            public void onPartitionsRevoked(Collection<TopicPartition> collection) {
                commitOffset(currentOffset);
            }
            //该方法在rebalance之后调用,获取offset
            public void onPartitionsAssigned(Collection<TopicPartition> collection) {
                currentOffset.clear();
                for (TopicPartition partition : collection) {
                    kafkaConsumer.seek(partition, getOffset(partition)); //定位到最近提交的 offset 位置继续消费
                }
            }
        });
        while (true) {
            //获取消息
            ConsumerRecords<String, String> msg = kafkaConsumer.poll(100);
            //解析并打印
            for (ConsumerRecord<String, String> consumerRecord : msg) {
                System.out.println(consumerRecord.partition() + ":" + consumerRecord.offset() + ":" + consumerRecord.value());
                currentOffset.put(new TopicPartition(consumerRecord.topic(),
consumerRecord.partition()), consumerRecord.offset());
            }
            
            commitOffset(currentOffset);//异步提交
        }
    }
    //获取某分区的最新 offset
    private static long getOffset(TopicPartition partition) {
        return 0;
    }
    //提交该消费者所有分区的 offset
    private static void commitOffset(Map<TopicPartition, Long> currentOffset) {
        //此处根据需求设置对应的offset存储方式
    }
}

Kafka Interceptor

拦截器原理

Producer 拦截器(interceptor)是在 Kafka 0.10 版本被引入的,主要用于实现 clients 端的定制化控制逻辑。
对于 producer 而言, interceptor 使得用户在消息发送前以及 producer 回调逻辑前有机会对消息做一些定制化需求,比如修改消息等。同时, producer 允许用户指定多个 interceptor按序作用于同一条消息从而形成一个拦截链(interceptor chain)。

Intercetpor 的实现接口是org.apache.kafka.clients.producer.ProducerInterceptor,其定义的方法包括:

  1. configure(configs):
    获取配置信息和初始化数据时调用。
  2. onSend(ProducerRecord):
    该方法封装进 KafkaProducer.send 方法中,即它运行在用户主线程中。 Produer 确保在消息被序列化以及计算分区前调用该方法。 用户可以在该方法中对消息做任何操作,但最好
    保证不要修改消息所属的 topic 和分区, 否则会影响目标分区的计算。
  3. onAcknowledgement(RecordMetadata, Exception):
    该方法会在消息从 RecordAccumulator 成功发送到 Kafka Broker 之后,或者在发送过程中失败时调用。 并且通常都是在 producer 回调逻辑触发之前。 onAcknowledgement 运行在producer 的 IO 线程中,因此不要在该方法中放入很重的逻辑,否则会拖慢producer 的消息发送效率。
  4. close:
    关闭 interceptor,主要用于执行一些资源清理工作如前所述, interceptor 可能被运行在多个线程中,因此在具体实现时用户需要自行确保线程安全。另外倘若指定了多个 interceptor,则 producer 将按照指定顺序调用它们,并仅仅是捕获每个 interceptor 可能抛出的异常记录到错误日志中而非在向上传递。这在使用过程中要特别留意。

拦截器案例

  1. 需求
    实现两个拦截器,第一个拦截器用于在消息发送的时给消息体追加一个时间戳前缀。另外一个拦截器用于统计发送成功的消息数量和发送失败的消息数量。
  2. 代码实现
    这里要注意,无论是否实现onSend方法,该方法都需要有一个ProducerRecord对象的返回值且不能是null,否则会报空指针异常;
  • TimeInterceptor
public class TimeInterceptor implements ProducerInterceptor<String,String> {
    /**
     * 追加时间戳到消息体的前面,因此需要在消息发送之前操作。重载onSend方法实现该功能
     * @param producerRecord
     * @return
     */
    public ProducerRecord<String,String> onSend(ProducerRecord<String,String> producerRecord) {
        String value = producerRecord.value();
        return new ProducerRecord<String, String>(producerRecord.topic(), producerRecord.partition(),producerRecord.timestamp(), (String) producerRecord.key(), System.currentTimeMillis() + value);
    }
    public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {}
    public void close() {}
    public void configure(Map<String, ?> map) {}
}
  • CounterInterceptor
public class CounterInterceptor implements ProducerInterceptor<String, String> {
    private int succerrCounter;
    private int errorCounter;
 
    public ProducerRecord<String, String> onSend(ProducerRecord<String, String> producerRecord) {
        return producerRecord;
    }
    public void onAcknowledgement(RecordMetadata recordMetadata, Exception e) {
        if (e != null) {
            errorCounter++;
        } else {
            succerrCounter++;
        }
    }
    public void close() {
        System.out.println("消息发送成功:" + succerrCounter);
        System.out.println("消息发送失败" + errorCounter);
    }
    public void configure(Map<String, ?> map) {}
}
  • Producer代码中添加对应的拦截器配置
List<String> interceptors = new ArrayList<String>();
interceptors.add("im.hwp.interceptor.TimeInterceptor");
interceptors.add("im.hwp.interceptor.CounterInterceptor");
properties.put(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,interceptors);

11. Kafka 监控

kafka-eagle的安装

  1. 下载 Kafka Eagle 压缩包 http://download.kafka-eagle.org/
  2. 将文件传输到服务器,两次解压。
  3. 修改配置文件,添加eagle相关的环境变量
vim /etc/profile
# 添加环境变量
KE_HOME=/usr/local/kafka-eagle
PATH=$PATH:$KE_HOME/bin
export PATH KE_HOME
# 最后执行下配置文件
source /etc/profile
  1. 修改eagle/conf/system-config.properties配置文件,需要修改的部分如下
######################################
# multi zookeeper & kafka cluster list
# 由于eagle支持多个集群的监控,因此demo里面是配置了两个集群
# 第二个集群用不上,所以注释掉。
######################################
kafka.eagle.zk.cluster.alias=cluster1
cluster1.zk.list=hadoop1:2181,hadoop2:2181,hadoop3:2181
# cluster2.zk.list=xdn10:2181,xdn11:2181,xdn12:2181

######################################
# kafka offset storage
# kafkaoffset的存储方式设置,老版本的存储位于zookeeper,kafka新版本位于本地的kafka
# 同样注释第二个集群的信息
######################################
cluster1.kafka.eagle.offset.storage=kafka
# cluster2.kafka.eagle.offset.storage=zk


######################################
# kafka sqlite jdbc driver address
# 该配置中一定要保证/usr/local/kafka-eagle/eagle/db 这路径存在,否则会出错
######################################
kafka.eagle.driver=org.sqlite.JDBC
kafka.eagle.url=jdbc:sqlite:/usr/local/kafka-eagle/eagle/db/ke.db
kafka.eagle.username=root
kafka.eagle.password=www.kafka-eagle.org

######################################
# kafka mysql jdbc driver address
# 该设置是可选的,配置过程会出现较多的问题
# 由于创建时无法自动生成对应的数据库表,所以我放弃了该方式,用了上面的配置
######################################
# kafka.eagle.driver=com.mysql.cj.jdbc.Driver
# kafka.eagle.url=jdbc:mysql://192.168.80.1:3306/ke?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&serverTimezone=UTC
# kafka.eagle.username=root
# kafka.eagle.password=123
  1. 修改所有kafka节点下的Kafka/bin/kafka-server-start.sh 文件
if [ "x$KAFKA_HEAP_OPTS" = "x" ]; then
export KAFKA_HEAP_OPTS="-server -Xms2G -Xmx2G -XX:PermSize=128m
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:ParallelGCThreads=8 -
XX:ConcGCThreads=5 -XX:InitiatingHeapOccupancyPercent=70"
export JMX_PORT="9999"
#export KAFKA_HEAP_OPTS="-Xmx1G -Xms1G"
fi
  1. 修改后重启kafka
  2. 启动eagle
ke.sh start

启动完成以后会出现以下信息,如果出现错误的话,日志可以在logs/error.log下查看

[2020-12-14 16:41:02] INFO: [Job done!]
Welcome to
    __ __    ___     ____    __ __    ___            ______    ___    ______    __     ______
   / //_/   /   |   / __/   / //_/   /   |          / ____/   /   |  / ____/   / /    / ____/
  / ,<     / /| |  / /_    / ,<     / /| |         / __/     / /| | / / __    / /    / __/
 / /| |   / ___ | / __/   / /| |   / ___ |        / /___    / ___ |/ /_/ /   / /___ / /___
/_/ |_|  /_/  |_|/_/     /_/ |_|  /_/  |_|       /_____/   /_/  |_|\____/   /_____//_____/


Version 2.0.3 -- Copyright 2016-2020
*******************************************************************
* Kafka Eagle Service has started success.
* Welcome, Now you can visit 'http://192.168.60.131:8048'
* Account:admin ,Password:123456
*******************************************************************
* <Usage> ke.sh [start|status|stop|restart|stats] </Usage>
* <Usage> https://www.kafka-eagle.org/ </Usage>
*******************************************************************

kafka-eagle的使用

安装启动完成以后,可以通过 http://192.168.60.131:8048 该地址访问可视化控制的前端页面

kafka Xmx kafka xmx作用_kafka_10