kafka入门学习
kafka概述
kafka是一个分布式的基于发布/订阅模式的消息队列(Message Queue),主要用于大数据的实时处理领域。
使用消息队列的好处
解耦、可恢复性、缓冲、灵活性&峰值处理能力、异步通信
消息队列的两种模式
1.点对点模式
2. 发布/订阅模式
Kafka架构
Kafka下载安装
下载地址:https://www.apache.org/dyn/closer.cgi?path=/kafka/2.5.0/kafka_2.13-2.5.0.tgz
进入config目录对server.properties进行配置
修改配置
// broker 的全局唯一编号,不能重复 broker.id=0 // kafka运行日志存放的路径 其实也是数据 log.dirs=/home/software/kafka/kafka-logs // 配置zookeeper集群地址 zookeeper.connect=hadoop01:2181,hadoop02:2181,hadoop03:2181 // 删除topic功能 delete.topic.enable=true // hostname、port都会广播给producer、consumer。如果你没有配置了这个属性的话,则使用listeners的值,如果listeners的值也没有配置的话,则使用
// java.net.InetAddress.getCanonicalHostName()返回值(这里也就是返回localhost了)。 advertised.listeners=PLAINTEXT://192.168.2.144:9092
编辑kafka集群启动/关闭脚本 在bin目录下创建一个文件kafka.sh并授权
chmod 777 kafka.sh
#!/bin/bash case $1 in "start"){ for i in hadoop01 hadoop02 hadoop03 do echo "********$i********" ssh $i "/home/software/kafka/bin/kafka-server-start.sh -daemon /home/software/kafka/config/server.properties" done };; "stop"){ for i in hadoop01 hadoop02 hadoop03 do echo "********$i********" ssh $i "/home/software/kafka/bin/kafka-server-stop.sh" done };; esac
kafka集群启动
启动前先启动zookeeper集群
在kafka bin目录下执行 kafka.sh
在bin目录下执行:
创建自定义的topic
./kafka-topics.sh --create --zookeeper hadoop01:2181 --replication-factor 1 --partitions 1 --topic mayun
查看所有的topic
./kafka-topics.sh --list --zookeeper hadoop01:2181
启动producer
./kafka-console-producer.sh --broker-list hadoop01:9092,hadoop02:9092,hadoop03:9092 --topic mayun
启动consumer
./kafka-console-consumer.sh --bootstrap-server hadoop01 --topic mayun --from-beginning
kafka分区策略
1)分区原因
(1)方便在集群中扩展,每个Partition可以通过调整以适应它所在的机器,而一个topic又可以有多个Partition组成,因此整个集群可以适应任意大小的数据了。
(2) 可以提高并发,因为可以以Partition为单位读写了。
2)分区原则
我们需要将producer发送的数据封装成一个ProducerRecord对象
public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value, Iterable<Header> headers) public ProducerRecord(String topic, Integer partition, Long timestamp, K key, V value) public ProducerRecord(String topic, Integer partition, K key, V value, Iterable<Header> headers)
// 指定分区 public ProducerRecord(String topic, Integer partition, K key, V value)
// key的hash值与topic的partiton数进行取余得到partition值 public ProducerRecord(String topic, K key, V value)
// 第一次调用随机整数,将这个值与topic可用的partition总数取余得到partition值,之后轮询,也就是round-robin算法 public ProducerRecord(String topic, V value)
kafka文件存储机制
kafka生产者
数据可靠性保证
为保证producer发送的数据,即可靠的发送指定的topic,topic的每个partition收到producer发送的数据后,都需要向producer发送ack(acknowledgement确认收到),
如果producer收到ack,就会进行下一轮的发送,否则重新发送数据。
选用第二种方案后做出优化
ISR:Leader维护了一个动态的in-sync replica set(ISR),意为和leader保存同步的follower集合,当ISR中的follower完成数据同步之后,follower就会给leader发送ack,
如果follower长时间未向leader同步数据,则该follower将被踢出ISR,该时间阀值由replica.lag.time.max.ms参数设定,leader发生故障之后,就会从ISR中选举新的leader。
ack参数配置
ack:
0: producer不等待broker的ack,这一操作提供了最低延迟,broker一接收到还没有写入磁盘就已经返回,当broker故障时有可能丢失数据。
1: producer等待broker的ack,partition的leader落盘成功后返回ack,如果在follower同步成功之前broker故障,那么会丢失数据。
-1(all):producer等待broker的ack,partition的leader和follower(ISR)全部落盘成功之后才返回ack,但如果在follower同步完成后,
broker发送ack之前,leader发生故障,那么会造成数据重复。
幂等性:
要启用幂等性,只需要将Producer的参数中enable.idompotence设置为true即可(ack 默认 -1),kafka的幂等性实现其实就是将原来下游需要做的去重放在了数据的上游。
开启幂等性的producer在初始化的时候会被分配一个PID,发往同一partition的消息会附带Sequence Number。而Broker端会对<PID,Partition,SeqNumber>做缓存,
当具有相同主键的消息提交时,Broker只会持久化一条。
但是PID重启就会变化,同时不同的Partition也具有不同的主键,所有幂等性无法保证跨分区会话的Exactly Once。
kafka事务:
Kafka 的事务处理,主要是允许应用可以把消费和生产的 batch 处理(涉及多个 Partition)在一个原子单元内完成,操作要么全部完成、要么全部失败。为了实现这种机制,我们需要应用能提供一个唯一 id,即使故障恢复后也不会改变,这个 id 就是 TransactionnalId(也叫 txn.id),txn.id 可以跟内部的 PID 1:1 分配,它们不同的是 txn.id 是用户提供的,而 PID 是 Producer 内部自动生成的(并且故障恢复后这个 PID 会变化),有了 txn.id 这个机制,就可以实现多 partition、跨会话的 EOS 语义。
当用户使用 Kafka 的事务性时,Kafka 可以做到的保证:
- 跨会话的幂等性写入:即使中间故障,恢复后依然可以保持幂等性;
- 跨会话的事务恢复:如果一个应用实例挂了,启动的下一个实例依然可以保证上一个事务完成(commit 或者 abort);
- 跨多个 Topic-Partition 的幂等性写入,Kafka 可以保证跨多个 Topic-Partition 的数据要么全部写入成功,要么全部失败,不会出现中间状态。
上面是从 Producer 的角度来看,那么如果从 Consumer 角度呢?Consumer 端很难保证一个已经 commit 的事务的所有 msg 都会被消费,有以下几个原因:
- 对于 compacted topic,在一个事务中写入的数据可能会被新的值覆盖;
- 一个事务内的数据,可能会跨多个 log segment,如果旧的 segmeng 数据由于过期而被清除,那么这个事务的一部分数据就无法被消费到了;
- Consumer 在消费时可以通过 seek 机制,随机从一个位置开始消费,这也会导致一个事务内的部分数据无法消费;
- Consumer 可能没有订阅这个事务涉及的全部 Partition。
事务性示例:
Kafka 事务性的使用方法也非常简单,用户只需要在 Producer 的配置中配置 transactional.id
,通过 initTransactions()
初始化事务状态信息,再通过 beginTransaction()
标识一个事务的开始,然后通过 commitTransaction()
或 abortTransaction()
对事务进行 commit 或 abort,示例如下所示:
Properties props = new Properties(); props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("client.id", "ProducerTranscationnalExample"); props.put("bootstrap.servers", "localhost:9092"); props.put("transactional.id", "test-transactional"); props.put("acks", "all"); KafkaProducer producer = new KafkaProducer(props); producer.initTransactions(); try { String msg = "matt test"; producer.beginTransaction(); producer.send(new ProducerRecord(topic, "0", msg.toString())); producer.send(new ProducerRecord(topic, "1", msg.toString())); producer.send(new ProducerRecord(topic, "2", msg.toString())); producer.commitTransaction(); } catch (ProducerFencedException e1) { e1.printStackTrace(); producer.close(); } catch (KafkaException e2) { e2.printStackTrace(); producer.abortTransaction(); } producer.close();
故障处理:
HW之前的数据才对consumer可见
1) follower故障
follower发生故障后会被临时提出ISR,待该follower恢复后,follower会读取本地磁盘记录的上次HW,并将log文件高于HW的部分截取掉,
从HW开始向leader进行同步,等该follwer的LEO大于等于该Partition的HW,即follower追上leader之后,就可以重新加入ISR了。
2)leader故障
leader故障之后,会从ISR中选出一个新的leader,之后,为保证多个副本之间的数据一致性,其余的follower会先将各自的log文件高于HW的部分截取掉,
然后从新的leader同步数据。
注意:这只能保证副本之间数据的一致性,并不能保证数据不丢失或者不重复。
kafka消费者
kafka消费方式
consumer采用pull(拉)模式从broker中读取数据
push(推)模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的
pull模式不足之处是,如果kafka没有数据,消费者可能会陷入循环汇总,一直返回空数据。
分区分配策略
一个consumer group 中有多个consumer,一个topic有多个partition,所以必然会涉及到partition的分配问题,即确定那个partition由那个consumer来消费
kafka有两种分区策略,RoundRobin、Range
kafka offset机制
Consumer消费者的offset存储机制(key:消费者组+主题+分区 value:offset)
Consumer在从broker读取消息后,可以选择commit,该操作会在Kakfa中保存该Consumer在该 Partition中读取的消息的offset。
该Consumer下一次再读该Partition时会从下一条开始读取。 通过这一特性可以保证同一消费者从Kafka中不会重复消费数据。
旧版本的Kafka是把消费者的offset存到Zookeeper,这种机制缺点是对zookeeper造成较大的负载 新版本的Kafka的offset是由kafka自己来管理
进入kafka-logs目录查看,会发现多个很多目录,这是因为kafka默认会生成50个 __consumer_offsets 的目录,用于存储消费者消费的offset位置。
Kafka会使用下面公式计算该消费者group位移保存在__consumer_offsets的哪个目录上: Math.abs(groupID.hashCode()) % 50
kafka 高效读写数据
1)顺序读写磁盘
为了使得Kafka的吞吐率可以线性提高,物理上把Topic分成一个或多个Partition,每个Partition在物理上对 应一个文件夹,该文件夹下存储这个Partition的所有消息和索引文件。
因为每条消息都被append到该Partition中,属于顺序写磁盘,因此效率非常高(经验证,顺序写磁盘效率比 随机写内存还要高,这是Kafka高吞吐率的一个很重要的保证)。
2)零拷贝复制技术(来源 https://www.jianshu.com/p/835ec2d4c170)
kafka中的消费者在读取服务端的数据时,需要将服务端的磁盘文件通过网络发送到消费者进程,网络发送需要经过几种网络节点。如下图所示:
传统的读取文件数据并发送到网络的步骤如下:
(1)操作系统将数据从磁盘文件中读取到内核空间的页面缓存;
(2)应用程序将数据从内核空间读入用户空间缓冲区;
(3)应用程序将读到数据写回内核空间并放入socket缓冲区;
(4)操作系统将数据从socket缓冲区复制到网卡接口,此时数据才能通过网络发送。
通常情况下,Kafka的消息会有多个订阅者,生产者发布的消息会被不同的消费者多次消费,为了优化这个流程,Kafka使用了“零拷贝技术”,如下图所示:
“零拷贝技术”只用将磁盘文件的数据复制到页面缓存中一次,然后将数据从页面缓存直接发送到网络中(发送给不同的订阅者时,都可以使用同一个页面缓存),避免了重复复制操作。
如果有10个消费者,传统方式下,数据复制次数为4*10=40次,而使用“零拷贝技术”只需要1+10=11次,一次为从磁盘复制到页面缓存,10次表示10个消费者各自读取一次页面缓存。
Kafka API
消息发送流程
Kafka的Producer发送消息采用的是异步发送的方式,在消息发送的过程中,涉及到了两个线程--main线程和Sender线程,以及一个线程共享变量--RecordAccumulator。
main线程将消息发送给RecordAccumulator,Sender 线程不断从RecordAccumulator中拉取消息发送到Kafka broker
API普通生产者
/** * 普通生产者 */ public class MyProducer { public static void main(String[] args) { // 创建kafka生产者配置信息 Properties properties = new Properties(); // 指定kafka连接集群 properties.put("bootstrap.servers", "192.168.2.144:9092"); // ack应答级别 properties.put("acks", "all"); // 重试次数 properties.put("retries", 3); // 批次大小 properties.put("batch.size", 16384); // 等待时间 properties.put("linger.ms", 1); // RecordAccumulator缓冲区大小 properties.put("buffer.memory", 33554432); // key,value 序列化类 properties.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); properties.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); //创建生产者对象 KafkaProducer<String, String> producer = new KafkaProducer<String, String>(properties); // 发送数据 for (int i = 0; i < 20; i++) { producer.send(new ProducerRecord<String, String>("mayun", "money" + i)); } // 关闭资源 producer.close(); } }
回调生产者
/** * 回调生产者 */ public class CallBackProducer { public static void main(String[] args) { // 创建配置信息 Properties properties = new Properties(); properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.2.144:9092"); properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer"); properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer"); // 创建生产者对象 KafkaProducer<String, String> producer = new KafkaProducer<>(properties); // 发送数据 for (int i = 0; i < 10; i++) { producer.send(new ProducerRecord<>("mayun", "money" + i), (recordMetadata, e) -> { if (e == null) { System.out.println(recordMetadata.partition() + "---" + recordMetadata.offset()); } else { e.printStackTrace(); } }); } producer.close(); } }
API普通消费者
public class MyConsumer { public static void main(String[] args) { // 创建配置信息 Properties properties = new Properties(); // 配置集群 properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.2.144:9092"); // 开启自动提交 properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, true); // 自动提交延时 properties.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000"); properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer"); properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringSerializer"); // 消费组 properties.put(ConsumerConfig.GROUP_ID_CONFIG, "abt"); // 创建消费者 KafkaConsumer<String, String> consumer = new KafkaConsumer<>(properties); // 订阅主题 consumer.subscribe(Arrays.asList("maYun","maHuaTen")); // 获取数据 ConsumerRecords<String, String> consumerRecords = consumer.poll(100); // 解析并打印consumerRecords for (ConsumerRecord<String, String> consumerRecord : consumerRecords) { System.out.println(consumerRecord.key() + "---" + consumerRecord.value()); } // 关闭连接 consumer.close(); }