一、消息队列的简介
1.1 为什么要有消息队列
峰值处理能力(消峰能力):消息系统可顶住峰值流量,业务系统可根据处理能力从消息系统中获取并处理对应量的请求
解耦 :各系统之间通过消息系统这个统一的接口交换数据,无须了解彼此的存在
冗余 :部分消息系统具有消息持久化能力,可规避消息处理前丢失的风险
扩展 :消息系统是统一的数据接口,各系统可独立扩展
可恢复性:系统中部分键失效并不会影响整个系统,它恢复会仍然可从消息系统中获取并处理数据
异步通信:在不需要立即处理请求的场景下,可以将请求放入消息系统,合适的时候再处理
1.2 消息队列是什么
- 消息(Message)
网络中的两台计算机或者两个通讯设备之间传递的数据。例如说:文本、音乐、视频等内容。
- 队列 (Queue)
一种特殊的线性表(数据元素首尾相接),特殊之处在于只允许在首部删除元素和在尾部追加元素(FIFO)。入队、出队。
- 消息队列(MQ)
消息+队列,保存消息的队列。消息的传输过程中的容器;主要提供生产、消费接口供外部调用做数据的存储和获取。
1.3 消息队列的分类
MQ主要分为两类:点对点(p2p)、发布订阅(Pub/Sub)
1)Peer-to-Peer
1. 一般基于Pull或者Polling接收数据
2. 发送到队列中的消息被一个而且仅仅一个接收者所接受,即使有多个接收者在同一个队列中侦听同一消息。
3. 支持异步“即发即收”的消息传递方式,也支持同步请求/应答传送方式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RY8nqi5i-1615727785629)(ClassNotes.assets/1568684012083.png)]
2)发布订阅
1. 发布到同一个主题的消息,可被多个订阅者所接收
2. 发布/订阅即可基于Push消费数据,也可基于Pull或者Polling消费数据
3. 解耦能力比P2P模型更强
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-07BPohwi-1615727785631)(ClassNotes.assets/1568684181173.png)]
3) p2p和发布订阅MQ的比较
-
共同点:
消息生产者生产消息发送到queue中,然后消息消费者从queue中读取并且消费消息。
-
不同点:
p2p模型 包括:消息队列(Queue)、发送者(Sender)、接收者(Receiver) 一个生产者生产的消息只有一个消费者(Consumer)(即一旦被消费,消息就不在消息队列中)。比如说打电话。 pub/Sub 包含:消息队列(Queue)、主题(Topic)、发布者(Publisher)、订阅者(Subscriber) 每个消息可以有多个消费者,彼此互不影响。比如我发布一个微博:关注我的人都能够看到。
1.4 常用的消息队列框架
-
RabbitMQ
Erlang编写,支持多协议AMQP,XMPP,SMTP,STOMP。支持负载均衡、数据持久化。同时支持Peer-to-Peer和发布/订阅模式。
-
Redis
基于Key-Value对的NoSQL数据库,同时支持MQ功能,可做轻量级队列服务使用。就入队操作而言,Redis对短消息(小于10kb)的性能比RabbitMQ好,长消息性能比RabbitMQ差。
-
ZeroMQ
轻量级,不需要单独的消息服务器或中间件,应用程序本身扮演该角色,Peer-to-Peer。它实质上是一个库,需要开发人员自己组合多种技术,使用复杂度高。
-
ActiveMQ
JMS实现,Peer-to-Peer,支持持久化、XA(分布式)事务
-
Kafka/Jafka
高性能跨语言的分布式发布/订阅消息系统,数据持久化,全分布式,同时支持在线和离线处理
-
MetaQ/RocketMQ
纯Java实现,发布/订阅消息系统,支持本地事务和XA分布式事务
二、KAFKA的简介
2.1 KAFKA是什么
1. 是一个分布式的流处理平台
2. 是apache旗下的顶级开源项目
3. 是用scala语言写的(scala语言开发的另外一款比较出名的软件是spark)
4. 有四大特征:
- 高吞吐量: 每秒可以处理数百万级的消息的生产和消费
- 持久化:本身有一套完善的存储机制,可以做消息的持久化
- 分布式:消息可以存储在分布式集群上,消息有副本冗余,某一台宕机,生产者和消费者转而使用其它机器上的消体
- 健壮性: 稳定,功能强大
2.2 kafka的设计目标和核心概念
2.2.1 设计目标
1. 高吞吐量
2. 持久化
3. 分布式
4. 在线处理和离线处理
2.2.2 核心概念
一、Kafka的组成部分:
- Topic:主题,Kafka处理的消息的不同分类。
- Broker:消息服务器代理,也就是kafka集群的一个节点,主要存储消息数据。
存在硬盘中。每个topic都是有分区的。
- Partition:Topic物理上的分组,一个topic在broker中被分为1个或者多个partition,
分区在创建topic的时候指定。
- Message:消息,是通信的基本单位,每个消息都属于一个partition
二、Kafka服务相关
- Producer:消息和数据的生产者,向Kafka的一个topic发布消息。
- Consumer:消息和数据的消费者,定于topic并处理其发布的消息。
- Zookeeper:协调kafka的正常运行。
三、KAFKA的安装部署
配置步骤:
1)上传,解压,更名,配置环境变量
[root@mei01 ~]# tar -zxvf kafka_2.11-1.1.1.tgz -C /usr/local/
[root@mei01 ~]# cd /usr/local/
[root@mei01 local]# mv kafka_2.11-1.1.1/ kafka
[root@mei01 local]# vim /etc/profile
...省略....
# spark environment
export KAFKA_HOME=/usr/local/kafka
export PATH=$KAFKA_HOME/bin:$PATH
[root@mei01 kafka]# source /etc/profile
2)修改配置文件
[root@mei01 local]# vim kafka/config/server.properties
-->找到以下key进行修改。
#kafka的每一个服务节点的唯一表示,用整数来表示,不能出现重复
broker.id=10
#设置一个主题topic的分区数量
num.partitions=3
#配置kafka的消息存储的本地路径
log.dirs=/usr/local/kafka/data/kafka-logs
#配置kafka要使用zookeeper服务
zookeeper.connect=mei01:2181,mei02:2181,mei03:2181/kafka-2020
3)分发到其他节点,并修改broker.id
[root@mei01 local]# scp -r kafka/ mei03:/usr/local/
[root@mei01 local]# scp -r kafka/ mei03:/usr/local/
[root@mei01 local]# scp /etc/profile mei02:/etc/
[root@mei01 local]# scp /etc/profile mei03:/etc/
千万别忘记修改broker.id
4)启动kafka
1. 先启动zookeeper
注意:你的是三台机器,应该都启动
2. 再启动三台机器上的kafka的服务
[root@mei01 ~]# kafka-server-start.sh -daemon /usr/local/kafka/config/server.properties
[root@mei02 ~]# kafka-server-start.sh -daemon /usr/local/kafka/config/server.properties
[root@mei03 ~]# kafka-server-start.sh -daemon /usr/local/kafka/config/server.properties
3. 查看服务项:jps
注意:如果出错了,去${KAFKA_HOME}/logs/目录下查看相关文件:server.log
四、KAFKA在zookeeper中的目录
1. 在zookeeper中的位置由server.properties里的zookeeper.connect决定。
注意:如果不在mei01:2181,mei02:2181,mei03:2181这个串后面维护一个根znode,那么kafka生成的所有znode都直接在/下。显得很乱,所以建议维护一个根znode.
2. 重要的znode说明:
/kafka-2020
/cluster
/id {"version":"1","id":"Pks8sWZUT6GBJHqyVGQ5OA"}
#代表的是一个kafka集群包含集群的版本,和集群的id
/controller {"version":1,"brokerid":11,"timestamp":"1564976668049"}
#controller是kafka中非常重要的一个角色,意为控制器,控制partition的leader选举,topic的
#crud操作。brokerid意为由其id对应的broker承担controller的角色。
/controller_epoch 2
#代表的是controller的纪元,换句话说是代表controller的更迭,每当controller的brokerid更换
#一次,controller_epoch就+1.
/brokers
/ids [11, 12, 13]
#存放当前kafka的broker实例列表
/topics [hadoop, __consumer_offsets]
#当前kafka中的topic列表
/seqid #系统的序列id
/consumers
#老版本用于存储kafka消费者的信息,主要保存对应的offset,新版本中基本不用,此时用户的消费信
#息,保存在一个系统的topic中:__consumer_offsets
/config
#存放配置信息
五、KAFKA的基本操作
5.1 topic的CRUD
5.1.1) 说明
kafka的消息都是按照不同的主题进行分开存储的。
5.1.2)主题的创建
参数说明
--alter 修改主题的分区,副本,配置信息
--create Create a new topic.
--delete Delete a topic
--describe List details for the given topics.
--list List all available topics.
--partitions <Integer: # of partitions> 指定分区数量,在修改时消息的分区和顺序会受到影响
--replication-factor <Integer: 每一个分区的副本因子
replication factor>
--topic <String: topic> 增删改查时指定的主题名称
--zookeeper <String: hosts> 所需要的zookeeper服务器列表
练习1:
]# kafka-topics.sh \
--zookeeper mei01:2181,mei02:2181,mei03:2181/kafka-2020 \
--create \
--topic pet \
--partitions 3 \
--replication-factor 3
练习2:创建时的副本数不能大于broker的数量
]# kafka-topics.sh \
--zookeeper mei01:2181,mei02:2181,mei03:2181/kafka-2020 \
--create \
--topic pig \
--partitions 3 \
--replication-factor 4
Error while executing topic command : Replication factor: 4 larger than available brokers: 3.
5.1.3 ) 指定主题的查看
[root@mei01 ~]# kafka-topics.sh --zookeeper mei01:2181,mei02:2181,mei03:2181/kafka-2020 --describe --topic pet
Topic:pet PartitionCount:3 ReplicationFactor:3 Configs:
Topic: pet Partition: 0 Leader: 11 Replicas: 11,12,10 Isr: 11,12,10
Topic: pet Partition: 1 Leader: 12 Replicas: 12,10,11 Isr: 12,10,11
Topic: pet Partition: 2 Leader: 10 Replicas: 10,11,12 Isr: 10,11,12
#解析
Partition: 一个主题的分区号
Leader:指的是一个分区的所有副本中的领导者
Replicas:一个分区的所有的副本的位置信息,位于哪些broker上(与创建时的副本因子数量相同)
Isr: 一个分区现有存活的副本的位置信息。
5.1.4)列出所有的主题
[root@mei01 ~]# kafka-topics.sh --zookeeper mei01:2181,mei02:2181,mei03:2181/kafka-2020 --list
5.1.5)修改主题
[root@mei01 ~]# kafka-topics.sh \
--zookeeper mei01:2181,mei02:2181,mei03:2181/kafka-2020 \
--alter \
--topic pet \
--partitions 4 \
注意:
- 分区数量只能增大,不能减少
- 不能修改副本因子
5.1.6)删除主题
[root@mei01 ~]# kafka-topics.sh \
--zookeeper mei01:2181,mei02:2181,mei03:2181/kafka-2020 \
--delete \
--topic pet
5.2 生产和消费消息
1) 启动生产者
[root@mei01 ~]# kafka-console-producer.sh --broker-list mei01:9092,mei02:9092,mei03:9092 \
--topic pet
查看kafka-console-producer.sh有哪些参数,直接在命令行输入此脚本,回车即可
2) 启动消费者
案例1:默认情况下,消费者获取的是启动后生产者生产的信息,
[root@mei03 ~]# kafka-console-consumer.sh --bootstrap-server mei01:9092,mei02:9092,mei03:9092 --topic pet
案例2: 如果想要获取启动之前的所有消息,需要加参数–from-beginning。
[root@mei03 ~]# kafka-console-consumer.sh --bootstrap-server mei01:9092,mei02:9092,mei03:9092 --topic pet --from-beginning
5.3 消费信息的offset
offset:
1. 是kafka的topic中的一个partition中的每一条消息的标识,如何区分该条消息在kafka对应的partition的位置,就是用该偏移量。
2. offset的数据类型是Long,8个字节长度。
3. offset在分区内是有序的,分区间是不一定有序。
4. 如果想要kafka中的数据全局有序,就只能让partition个数为1。
参考下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DYlqdsoV-1615727785633)(…/…/day01/%25E6%2596%2587%25E6%25A1%25A3/Kafka/assets/log_anatomy.png)]
案例演示1:指定分区,获取所有的数据
[root@mei03 ~]# kafka-console-consumer.sh --bootstrap-server mei01:9092,mei02:9092,mei03:9092 --topic pet --offset earliest
offset有三种值:
earliest:从分区的第一条消息开始获取
latest:获取启动后,最近生产者产生的信息。
none: 指的是什么都不做,空闲
案例2:指定一个非负数的自然数作为偏移量开始消费消息
root@mei01 ~]# kafka-console-consumer.sh --bootstrap-server mei01:9092,mei02:9092,mei03:9092 --topic pet --partition 0 --offset 0
5.4 消费信息的无序说明
1. 分区间的消息是无序的,分区内的数据是有序的。
2. 可以使用--from-beginning 来显示效率。
获取所有分区的全部消息时,不一定是先获取哪一个分区的数据。
5.5 消费者组与Partition(重要)
1. 消费者是可以分组的。
2. 生产者的信息可以被多个组的消费者同时消费,组与组之前不受影响
3. 同一组内的消费者,一个消费者只能消费一个分区的数据,前提是分区数等于组内消费者数量
消费者的数量<分区的数量:尽可能的均分,有一个负载均衡的机制。
比如有6个分区,4个消费者:有两个消费者各处理一个,另外两个消费者各处理2个
比如有4个分区,2个消费者:各处理两个 分区号0,1,2,3 一个处理0和2,一个处理1,3.
比如有3个分区,2个消费者:一个消费者处理1个分区,另一个处理两个分区
消费者的数量>分区数量: 一个消费者处理一个分区的数据,剩下的消费者闲置。
案例1:设置消费者所属组
kafka-console-consumer.sh --bootstrap-server mei01:9092,mei02:9092,mei03:9092 --topic pet --group g1
在组内,kafka的topic的partition个数,代表了kafka的topic的并行度,同一时间最多可以有多个线程来消费topic的数据,所以如果要想提高kafka的topic的消费能力,应该增大partition的个数。
六、kafka的编程api
maven依赖
<!-- https://mvnrepository.com/artifact/org.apache.kafka/kafka-clients -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>1.1.1</version>
</dependency>
<!-- 下面的依赖,包含了上面的kafka-clients,所以只需要引入下面即可 -->
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.11</artifactId>
<version>1.1.1</version>
</dependency>
6.1 kafka生产者的api操作
6.1.1 简单编程
package com.qf.kafka.day01
import java.util.Properties
import org.apache.kafka.clients.producer.{KafkaProducer, ProducerRecord}
/**
* 练习kafka的生产者的API操作
* 1. 要创建一个生产者对象
* 2. 要指定主题,获取消息对象(记录对象)
* 3. 发送数据
*/
object _01ProducerTest {
def main(args: Array[String]): Unit = {
//设置配置信息
val prop = new Properties()
/* prop.setProperty("bootstrap.servers",
"mei01:9092,mei02:9092,mei03:9092")
prop.setProperty("acks","0")
prop.setProperty("key.serializer",
"org.apache.kafka.common.serialization.IntegerSerializer")
prop.setProperty("value.serializer",
"org.apache.kafka.common.serialization.StringSerializer")*/
// 不使用上面的配置,加载配置文件里的属性信息,这样更方便维护
prop.load(_01ProducerTest.getClass
.getClassLoader.getResourceAsStream("producer.properties"))
//获取kafka的生产者对象
val producer = new KafkaProducer[Integer,String](prop)
//获取一个记录对象
val message = new ProducerRecord[Integer, String]("pet","nishizuibangde")
//调用send方法向集群上的指定主题发送数据
producer.send(message)
//释放资源
producer.close()
}
}
6.1.2 send方法的解析
1. 是一个异步发送数据的方法,当数据到达缓冲区立即返回。
6.1.3 幂等机制
1. 为了避免生产者在重试时出现重复的数据保存到消息队列中,引入了幂等性。
2. 什么时候会出现重复发送:
当生产者没有收到broker的确认信息时,会认为发送失败,然后再次发送。就会出现重复发送的数据。
3. 什么是幂等性:
多次发送的数据,相当于一次发送的数据。去掉重复性。保证Exactly-Once(仅一次)
Kafka为了实现幂等性,它在底层设计架构中引入了ProducerID和SequenceNumber。
那这两个概念的用途是什么呢?
- ProducerID:在每个新的Producer初始化时,会被分配一个唯一的ProducerID,
这个ProducerID对客户端使用者是不可见的。
- SequenceNumber:对于每个ProducerID,Producer发送数据的每个Topic和Partition
都对应一个从0开始单调递增的SequenceNumber值。
重新尝试发送时,SequenceNumber是不递增的。
4. 如何使用幂等性?
非常简单:只需要在producer.properties里
设置如下属性即可
enable.idempotence=true
注意:
- 重试次数要大于0
- 还要使用acks机制
6.1.4 事务机制
使用事务机制来保证数据的安全性和一致性时
- 1:需要开启幂等性
- 2:要保证你的副本数至少大于等于3
- 3:同时要求在写入一条数据过程中,必须要有一半以上副本写入成功才行
6.2 kafka消费者的api操作
案例1:
package com.qf.kafka.day02;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
public class _01ConsumerTest {
public static void main(String[] args) throws IOException {
//创建配置对象
Properties properties = new Properties();
//读取配置文件
InputStream is = _01ConsumerTest.class.getClassLoader().getResourceAsStream("consumer.properties");
//将配置属性加载到配置对象上
properties.load(is);
//创建消费者
KafkaConsumer<Integer, String> c = new KafkaConsumer<Integer, String>(properties);
//消费者订阅主题
List<String> topics = new ArrayList<String>();
topics.add("pet");
c.subscribe(topics);
while (true){
//拉取消息
ConsumerRecords<Integer, String> messages = c.poll(1000);
//遍历消息
Iterator<ConsumerRecord<Integer, String>> it = messages.iterator();
while(it.hasNext()){
//获取当前消息
ConsumerRecord<Integer, String> next = it.next();
//打印
System.out.println(next.topic()+"\t"+next.partition()+"\t"+next.offset()+"\t"+next.key()+"\t"+next.value());
}
}
}
}
案例2:
package com.qf.kafka.day02;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
import org.apache.kafka.common.TopicPartition;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
/**
* 消费者指定分区中的偏移量来消费消息
*/
public class _02ConsumerTest {
public static void main(String[] args) throws IOException {
//创建配置对象
Properties properties = new Properties();
//读取配置文件
InputStream is = _02ConsumerTest.class.getClassLoader().getResourceAsStream("consumer.properties");
//将配置属性加载到配置对象上
properties.load(is);
//创建消费者
KafkaConsumer<Integer, String> c = new KafkaConsumer<Integer, String>(properties);
//消费者订阅主题,指定分区
List<TopicPartition> list = new ArrayList<>();
TopicPartition p1 = new TopicPartition("pet",0);
TopicPartition p2 = new TopicPartition("pet",1);
list.add(p1);
list.add(p2);
c.assign(list);
//定位偏移量
c.seek(p1,10);
c.seek(p2,20);
while (true){
//拉取消息
ConsumerRecords<Integer, String> messages = c.poll(1000);
//遍历消息
Iterator<ConsumerRecord<Integer, String>> it = messages.iterator();
while(it.hasNext()){
//获取当前消息
ConsumerRecord<Integer, String> next = it.next();
//打印
System.out.println(next.topic()+"\t"+next.partition()+"\t"+next.offset()+"\t"+next.key()+"\t"+next.value());
}
}
}
}
6.3 分区策略
6.3.1 默认的分区策略
/**
* The default partitioning strategy:
* <ul>
* <li>If a partition is specified in the record, use it
* <li>If no partition is specified but a key is present choose a partition based on a hash of the key
* <li>If no partition or key is present choose a partition in a round-robin fashion
*/
public class DefaultPartitioner implements Partitioner {
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 = nextValue(topic);
List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
if (availablePartitions.size() > 0) {
int part = Utils.toPositive(nextValue) % availablePartitions.size();
return availablePartitions.get(part).partition();
} else {
// no partitions are available, give a non-available partition
return Utils.toPositive(nextValue) % numPartitions;
}
} else {
// hash the keyBytes to choose a partition
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
}
1、如果指定了partition,那么直接进入该partition
2、如果没有指定partition,但是指定了key,使用key的hash选择partition
3、如果既没有指定partition,也没有指定key,使用轮询的方式进入partition
6.3.2 自定义随机分区器
package com.qf.kafka.day02;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import java.util.Map;
/**
* 自定义随机分区器
* 1. 实现Partitioner接口
* 2. 重写partition方法
*/
public class RandParitioner implements Partitioner {
/**
* 计算当前消息的分区号
* @param topic
* @param key
* @param keyBytes
* @param value
* @param valueBytes
* @param cluster
* @return
*/
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
//先获取分区的数量
Integer count = cluster.partitionCountForTopic(topic);
//随机一个分区号,应该是一个0~count-1的自然数
int number = (int) (Math.random() * count);
return number;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> configs) {
}
}
测试:
package com.qf.kafka.day02;
import com.qf.kafka.day01._02SendMethodParse;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.concurrent.Future;
public class _TestRandPartioner {
public static void main(String[] args) throws IOException {
InputStream is = _02SendMethodParse.class.getClassLoader().getResourceAsStream("producer.properties");
//获取配置对象
Properties properties = new Properties();
//加载流对象里的配置信息
properties.load(is);
properties.setProperty("partitioner.class","com.qf.kafka.day02.RandParitioner");
//获取kafka的生产者对象
KafkaProducer producer = new KafkaProducer<Integer,String>(properties);
Future<RecordMetadata> send = null;
ProducerRecord message = null;
for(int i = 0;i<10;i++){
//获取一个记录对象
message = new ProducerRecord<Integer, String>("pet",15,""+i);
send = producer.send(message);
}
//释放资源
producer.close();
}
}
6.3.3 自定义hash分区器
package com.qf.kafka.day02;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import java.util.Map;
public class HashPartitioner implements Partitioner {
/**
* 使用key的hash值来计算消息的分区号
* @param topic
* @param key
* @param keyBytes
* @param value
* @param valueBytes
* @param cluster
* @return
*/
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
//获取分区数量
Integer count = cluster.partitionCountForTopic(topic);
//获取key的hash执行取模运算
int number = key.toString().hashCode() % count;
return number;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> configs) {
}
}
测试:
package com.qf.kafka.day02;
import com.qf.kafka.day01._02SendMethodParse;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.concurrent.Future;
/**
* 测试自定义的hash分区器
* 将写好的hash分区器 设置到配置文件producer.properties中的属性partitioner.class上
*/
public class _TestHashPartioner {
public static void main(String[] args) throws IOException {
InputStream is = _02SendMethodParse.class.getClassLoader().getResourceAsStream("producer.properties");
//获取配置对象
Properties properties = new Properties();
//加载流对象里的配置信息
properties.load(is);
//获取kafka的生产者对象
KafkaProducer producer = new KafkaProducer<Integer,String>(properties);
Future<RecordMetadata> send = null;
ProducerRecord message = null;
for(int i = 0;i<10;i++){
//获取一个记录对象
message = new ProducerRecord<Integer, String>("pet",i,"hello"+i);
send = producer.send(message);
}
//释放资源
producer.close();
}
}
6.3.4 自定义轮询分区器
package com.qf.kafka.day02;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 自定义一个轮询分区器
*/
public class RoundPartitioner implements Partitioner {
//获取一个全局的计数器
private AtomicInteger atomic = new AtomicInteger();
//计算当前消息的分区号
@Override
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
//获取主题的分区数量
Integer count = cluster.partitionCountForTopic(topic);
//获取计数器上的自增值
int andIncrement = atomic.getAndIncrement();
//对分区数量取模运算,就可以得到 0,1,2,0,1,2.....轮询的效果
int i = andIncrement % count;
return i;
}
@Override
public void close() {
}
@Override
public void configure(Map<String, ?> configs) {
}
}
测试:
package com.qf.kafka.day02;
import com.qf.kafka.day01._02SendMethodParse;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import java.util.concurrent.Future;
/**
* 测试自定义的轮询分区器
* 将写好的轮询分区器 设置到配置文件producer.properties中的属性partitioner.class上
*/
public class _TestRoundPartioner {
public static void main(String[] args) throws IOException {
InputStream is = _02SendMethodParse.class.getClassLoader().getResourceAsStream("producer.properties");
//获取配置对象
Properties properties = new Properties();
//加载流对象里的配置信息
properties.load(is);
//获取kafka的生产者对象
KafkaProducer producer = new KafkaProducer<Integer,String>(properties);
Future<RecordMetadata> send = null;
ProducerRecord message = null;
for(int i = 0;i<13;i++){
//获取一个记录对象
message = new ProducerRecord<Integer, String>("pet",15,""+i);
send = producer.send(message);
}
//释放资源
producer.close();
}
}
七、Kafka与flume的整合
7.1 说明
flume是一个数据采集框架,可以采集行为数据,到hdfs或者是hbase或hive上,做离线分析。
flume还可以将数据采集到kafka上,做实时分析。
7.2 案例演示
7.2.1 案例1:
将flume采集到的数据落地到kafka上,因此sink源要是kafka的(是生产者身份)
[root@mei01 flumeconf]# vim syslog-mem-kafka.properties
#定义agent和三大核心组件的名称
a1.sources = r1
a1.channels = c1
a1.sinks = s1
#关联
a1.sources.r1.channels = c1
a1.sinks.s1.channel = c1
#定义source源
a1.sources.r1.type = syslogtcp
a1.sources.r1.host = mei01
a1.sources.r1.port = 10086
#定义channel源
a1.channels.c1.type = memory
a1.channels.c1.capacity= 1000
a1.channels.c1.transactionCapacity=100
#定义sink源
a1.sinks.s1.type = org.apache.flume.sink.kafka.KafkaSink
a1.sinks.s1.kafka.bootstrap.servers = mei01:9092,mei02:9092,mei03:9092
#主题必须提前存在
a1.sinks.s1.kafka.topic = pet
a1.sinks.s1.kafka.producer.acks = 1
#缓冲区flush到磁盘的时间阈值
a1.sinks.s1.kafka.producer.linger.ms = 1
#a1.sinks.k1.kafka.producer.compression.type = snappy
启动:
[root@mei01 flumeconf]# flume-ng agent -c ../conf -f syslog-mem-kafka.properties -n a1 -Dflume.root.logger=INFO,console &
测试:
1. 先启动一个消费者,来准备消费消息
2. 测试数据
[root@mei01 flumeconf]# echo "aaaaa" | nc mei01 10086
7.2.2 案例2:
使用kafka的source源来消费kafka消息队列里的数据,落地到hdfs
说明:kafka的source源是从kafka集群上读取数据,也就是消费者的身份,将数据封装成flume的event。
[root@mei01 flumeconf]# vim kafka-mem-hdfs.properties
#定义agent和三大核心组件的名称
a1.sources = r1
a1.channels = c1
a1.sinks = s1
#关联
a1.sources.r1.channels = c1
a1.sinks.s1.channel = c1
#定义source源
a1.sources.r1.type = org.apache.flume.source.kafka.KafkaSource
a1.sources.r1.kafka.bootstrap.servers = mei01:9092,mei02:9092,mei03:9092
a1.sources.r1.kafka.consumer.group.id = g1
# 消费的主题,多个主题使用逗号分隔
a1.sources.r1.kafka.topics = pet
#定义channel源
a1.channels.c1.type = memory
a1.channels.c1.capacity= 1000
a1.channels.c1.transactionCapacity=100
#定义sink源
a1.sinks.s1.type = hdfs
a1.sinks.s1.hdfs.path = hdfs://mei01:8020/kafka/pet/%y%m%d/%h%M
#主题必须提前存在
a1.sinks.s1.hdfs.filePrefix = FlumeData
a1.sinks.s1.hdfs.fileSuffix = .kafka
a1.sinks.s1.hdfs.rollInterval=0
a1.sinks.s1.hdfs.rollSize = 0
# 满足10条一滚动文件,有第11条才会真的滚动
a1.sinks.s1.hdfs.rollCount = 10
a1.sinks.s1.hdfs.writeFormat=Text
a1.sinks.s1.hdfs.fileType=DataStream
a1.sinks.s1.hdfs.useLocalTimeStamp =true
启动
[root@mei01 flumeconf]# flume-ng agent -c ../conf -f kafka-mem-hdfs.properties -n a1 -Dflume.root.logger=INFO,console &
测试:
1. 开启一个生产者,使用生产者发送消息
方式1:使用api
方式2:使用shell
八、Kafka的架构
8.1 Kafka的组织架构模型
8.1.1 Kafka的相关术语
一、从集群模型上说:
broker: kafka集群中的每一个存储节点,用来在磁盘上持久化(几天,几小时)存储消息数据的。可以保证消息数据的安全性(重用性:消费者可以重新消费消息)
zookeeper:维护一个集群的broker的controller角色以及宕机后的重写选举,还有其他元数据,比如broker的动态上下线,主题及其分区信息,分区副本的leader角色
producer:生产者,用于生产消息的
consumer:消费者,用于消费消息的
二、从消息上说:
topic:生产者生产的消息是按照主题分类的。
partiton:生产者为每一个主题生产的消息又被划分为不同的分区。(提高消息消费的并行度)
replication:每一个分区的数据都有N个副本。提高消息数据的安全性,可靠性
leader:N个副本中的老大,负责写和读数据,做一个应答机制。
follower:同步leader写的数据
isr:现存活的副本
segment: 指的是分区内的数据按照段来存储,(方便根据offset查找消息,也方便删除旧数据)
offset: 每个分区里的消息的偏移量。在目录下是以offset.log来命名的
8.1.2 Kafka的集群架构
通常,一个典型的Kafka集群中包含
1. 若干Producer(可以是web前端产生的Page View,或者是服务器日志,系统CPU、Memory等),
2. 若干broker(Kafka支持水平扩展,一般broker数量越多,集群吞吐率越高),
3. 若干Consumer Group,
4. 以及一个Zookeeper集群。Kafka通过Zookeeper管理集群配置,选举leader,以及在Consumer Group发生变化时进行rebalance。
5. Producer使用push模式将消息发布到broker,
6. Consumer使用pull模式从broker订阅并消费消息(消费者能消费多少信息应该由自己说的算,而不是被动的,应该是主动的)
8.1.3 Kafka的分布式模型
1. 一个主题的消息是分不同分区的。
2. 每一个分区的数据有多个副本,分别存在不同的机器上,来保证数据的安全性,提高容错性。
3. 这些副本中的某一个作为leader,进行数据的读和写操作,其他的作为follower来同步leader的数据
4. 如果leader挂掉了,follower中某一个会成为leader,来继续工作。体现了kafka的去中心化。
5. 消费者的并行度是由分区数来决定的。
8.2 Kafka的文件存储机制
8.2.1 文件结构
1. 在kafka中,主题topic是一个逻辑概念
2. 每一个主题的所有消息是按照partition进行物理存储的。
3. partition就是主题的一个分区,对应的是磁盘上的目录,目录名是topicName-序列,序列从0开始
4. 分区内的数据是分segment进行存储的。
5. segment对应两个文件,分别是xxxxxxxx.index和xxxxxxxxx.log文件。
- .index文件存储的是key-value对,并且是稀疏存储。key是.log文件中的第几条消息的这个数字,value是这个消息的针对于这个文件的偏移量。
- .log文件存储的是真正的消息, 大小阈值,默认是1G。
- xxxxxxxxx指的是上一个segment的最后的消息索引
8.2.2 分区
1)为什么要分区
kafka可以将一个主题的消息来分目录进行管理。 本质上分区就是分目录
2)分区的好处
1. 方便在集群中扩展,一个分区可以存在不同的节点上,进行分布式存储
2. 多个生产者可以向同一个主题的不同的分区同时写数据,做到并发
3. 消费者组内的成员可以同时消费不同分区里的数据
注意:同一个分区的副本不可能在同一台broker上
3)分区策略
假设现在有3个broker,4个分区。 不考虑多副本的情况。
算法:将第i个Partition分配到第(i mode n)个broker上。因此分布如下:
Topic:test3 PartitionCount:4 ReplicationFactor:2 Configs:
Topic: test3 Partition: 0 Leader: 10 Replicas:10 Isr:10
Topic: test3 Partition: 1 Leader: 11 Replicas:11 Isr:11
Topic: test3 Partition: 2 Leader: 12 Replicas:12 Isr:12
Topic: test3 Partition: 3 Leader: 10 Replicas:10 Isr:10
4)副本分布策略
1. 一旦leader确定了,其他的副本一定不是在leader这台机器上,而是尽可能的分散到其他节点上。
2. 目的就是负载均衡。
5)消息分配策略(默认情况下,当然可以指定分区器)
1、如果指定了partition,那么直接进入该partition
2、如果没有指定partition,但是指定了key,使用key的hash选择partition
3、如果既没有指定partition,也没有指定key,使用轮询的方式进入partition
8.3 分区的文件存储机制
1. 分区的本质是一个目录
2. 分区内的文件是按照segment存储的,也就是说,一个分区有N个segment(段,抽象概念)。
3. segment大小是1G。
4. segment对应的文件是按照顺序读写的,因此速度特别快
5. 分段存储,方便删除旧段的文件(否则,只有一个文件时,还需要进行切分才能删除旧数据),提高磁盘利用率
6. segment对应三个文件:
- xxxx.index
- xxxx.log
- xxxx.timeindex
8.4 segment的物理结构
1. 分区里的文件是按照segment进行物理存储的,但是segment是一个抽象概念
2. segment在物理上分三个文件存储,其中两个非常重要,一个是xxxx.index.一个是xxx.log
3. 文件名: offset.index offset.log offset指的是之前的所有段文件里的总条数的数字
4. offset.log的文件结构:
offset: 0
position: 0
CreateTime: 1606877011740
isvalid: true
keysize: -1 key的长度,如果没有key,是-1
valuesize: 350 value的长度
magic: 2 服务协议版本号
compresscodec: NONE
producerId: -1
producerEpoch: -1
sequence: -1
isTransactional: false
headerKeys: []
payload: 真正的value数据
5. offset.index的文件结构:有一堆键值对 offset-position
offset: 0 指的是对应的log文件中的第几条消息 第一条就是0
position: 0 指的是对应的log文件中的第几条消息的开始字节位置(针对于这个文件的偏移量)
注意:offset.index文件是稀疏存储的文件格式。
8.4 Kafka的消息查找流程
举例说明:查找消息索引为1041的消息。
段文件分布如下:
00000000000000000000.index 00000000000000000000.log
00000000000000001034.index 00000000000000001034.log
00000000000000003067.index 00000000000000003067.log
00000000000000005148.index 00000000000000005148.log
索引文件: log文件:
00000000000000001034.index 00000000000000001034.log
1034 0 offset:1034 postition:0 valuesize:101
1035 200 offset:1035 postition:200 valuesize:101
1036 400 offset:1036 postition:400 valuesize:101
1039 1000 offset:1037 postition:600 valuesize:101
1042 1600 offset:1038 postition:800 valuesize:101
offset:1039 postition:1000 valuesize:101
offset:1040 postition:1200 valuesize:101
offset:1041 postition:1400 valuesize:101
offset:1042 postition:1600 valuesize:101
offset:1043 postition:1800 valuesize:101
offset:1044 postition:2000 valuesize:101
步骤如下:
第一步:先判断要查找的消息索引在哪一个段。
使用索引和文件名上的索引比较,来确定在哪一个段里。
1041和1034以及3067比较,发现在1034里
第二步:在索引文件中查找偏移量:1041-1034 = 7,查看是否有此偏移量,如果没有,那就确定范围。也就是5对应的1000这个偏移量。
第三步:在对应的log文件中通过偏移量1000找到1039这个索引,继续往下找1041就可以了。
8.5 消费者组的概念
1. kafka因为多分区的概念,提出了消费者组的概念
2. 消费者组具有可扩展和容错机制
3. 消费者组有唯一标识 groupid
4. 组与组之间订阅的主题没有任何关系。
5. 三大特征:
- 组内有多个消费者,消费者可以是一个进程,也可以是一个线程
- 公用一个唯一组标识
- 一个分区只能被组内的一个消费者消费
8.6 消费者对offset的维护
8.6.1 说明
消费者在消费数据时,发生了宕机之类的故障后,再重写启动时,消费的数据是否要重头开始,还是要从之前宕机的位置开始读取呢?
如果从头读取时,有一部分消息一定出现了重复消费。
如果从宕机时的消费位置开始读取,就不会出现重复消费。 所以kafka采取这种机制,就涉及到了offset的维护。
8.6.2 offset的维护
两种方式:
方式1:kafka的自动提交(enable.auto.commit = true)。 也叫at most once。最多提交一次。不会出现重复提交
方式2:消费者的手动提交(enable.auto.commit = false,需要调用consumer.commitSync()以及配合事务一起使用),也叫at least once。最少提交一次,会出现重复提交。
offset是如何维护记录的呢?
0.9.0版本以前,将这些offset存在zookeeper里,但是zookeeper不适合维护这样的大批量的数据。因此在0.9.0版本以后,kafka专门维护了一个主题__consumer_offsets。用来记录所有消费者对所有主题里的分区读取的offset即时位置。
内部结构包括: groupid topicName-partition offset
8.7 push和pull的优缺点
1. push模式的目标是尽可能以最快速度传递消息,因此缺点是推送消息时很难适应消费者的消费速率以及消费信息量,容易造成consumer来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。
2. pull模式的目标是能消费多少数据是有自己决定的,但是缺点是如果没有数据被拉取,则陷入循环中,会占用资源。
kafka的生产者采用了push模式将生产的消息push到broker上,也就可以避免直接与消费者直接交互。
kafka的消费者采用了pull模式拉取broker里的数据,同时kafka提供了一个timeout参数,来增加一次循环的等待时间,减少循环次数。
8.8 kafka的ack机制
Kafka为用户提供了三种可靠性级别,用户根据对可靠性和延迟的要求进行权衡,选择以下的配置。
1. ack=0
生产者生产消息发送到leader后,就继续发送其他的消息,不需要leader的ack。
缺点:数据可能丢失,不会出现重复。
2. ack=1
生产者生产消息发送到leader,leader会将消费落地到磁盘,然后就向生产者进行ack。
缺点:数据可能丢失(leader落地了,并ack了,但是ack后,follower还为同步,leader挂了)
数据可能重复(leader落地了,但是ack失败了,生产者会再次重复发送消息)
3. ack=-1(all)
生产者生产消息发送到leader,leader会将消费落地到磁盘,follower会同步leader里的数据,所有的follower同步成功后会向leader进行汇报,leader再向生产者进行ack.
缺点:数据会重复(ack失败了,会重复发送此消息)
8.9 kafka的数据保障
8.9.1 说明
问题:生产者如何发送新的一条消息啊? kafka引入了ack机制。ack机制有三种策略。一个是0,一个是1,还有一个是-1(all).
0和1 不细说了,参考8.8
-1的情况如下:
正常情况下,副本数据同步策略有以下两种方案,进行比较
方案 | 优点 | 缺点 |
---|---|---|
半数以上完成同步,就发送ack | 延迟低 | 选举新的leader时,容忍n台节点的故障,需要2n+1个副本 |
全部完成同步,才发送ack | 选举新的leader时,容忍n台节点的故障,需要n+1个副本 | 延迟高 |
Kafka针对于-1选择了第二种方案,原因如下:
1.同样为了容忍n台节点的故障,第一种方案需要2n+1个副本,而第二种方案只需要n+1个副本,而Kafka的每个分区都有大量的数据,第一种方案会造成大量数据的冗余。
2.虽然第二种方案的网络延迟会比较高,但网络延迟对Kafka的影响较小。
8.9.2 ISR(in-sync replica set)
设想一个问题:
-1 是所有的follower都要同步成功后,leader才会向生产者发送ack。但是可能某一个follower宕机了,那么生产者就迟迟不会收到leader的ack. 耽误发送下一条新的消息。
因此kafka提供了一个ISR机制,即ISR是一个已经同步数据的所有副本,当某一个follower迟迟没有向leader发送汇报时,就会将此follower踢出ISR。这样就可以避免leader迟迟不向生产者发送ack。
ISR中包含了所有的存活副本,包括leader和follower。
如果leader宕机了,会从ISR中的所有follower中选举出一个新的leader,继续工作。follower为了保证数据的一致性,会先将高于水位线的消息截取掉,然后从新的leader上重新同步数据。
如果follower宕机了,会将其踢出ISR。回复后,会将高于水位线以上的消息截取掉,重新从leader的水位线开始同步,当同步成功后,再加入到ISR中
8.10 ExActly Once语义
8.10.1 三种语义
1. at most once :最多一次 数据不可能重复,ack=0
2. at least once :最少一次 数据可能重复 ack=-1|1
3. exactly once :正好一次 数据不可能重复 ,需要配合ack=-1以及幂等机制
也就是:idempotent + at least once = exactly once
在producer的配置文件中添加enable.idempotence=true
8.10.2 幂等机制的简介
1. 为了避免生产者在重试时出现重复的数据保存到消息队列中,引入了幂等性。
2. 什么时候会出现重复发送:
当生产者没有收到broker的确认信息时,会认为发送失败,然后再次发送。就会出现重复发送的数据。
3. 什么是幂等性:
多次发送的数据,相当于一次发送的数据。去掉重复性。保证Exactly-Once(仅一次)
Kafka为了实现幂等性,它在底层设计架构中引入了ProducerID和SequenceNumber。
那这两个概念的用途是什么呢?
- ProducerID:在每个新的Producer初始化时,会被分配一个唯一的ProducerID,
这个ProducerID对客户端使用者是不可见的。
- SequenceNumber:对于每个ProducerID,Producer发送数据的每个Topic和Partition
都对应一个从0开始单调递增的SequenceNumber值。
重新尝试发送时,SequenceNumber是不递增的。broker会将收到的SequenceNumber和之前缓存过的SequenceNumber进行比较,如果相同,则认为是同一个,就不存储了。如果发现SequenceNumber大于缓存的SequenceNumber则认为是新的消息,就存储。
4. 如何使用幂等性?
非常简单:只需要在producer.properties里
设置如下属性即可
enable.idempotence=true
注意:
- 重试次数要大于0
- 还要使用acks机制
8.11 zookeeper在kafka中的作用
1. zookeeper在kafka中启动一个框架协调作用,维护broker的controller角色的选举
如果选举:
启动后的所有broker都会向zookeeper注册一个临时节点/controller, 谁注册成功谁就是controller
注册成功的那一个会启动一个KafkaController进程。
KafkaController负责管理集群broker的上下线,所有topic的分区副本分配和leader选举等工作
2.Controller的管理工作都是依赖于Zookeeper的。
Controller的管理工作都是依赖于Zookeeper的。
以下为partition和Controller的leader选举过程:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hpnaNiWr-1615727785636)(ClassNotes.assets/1577975440646.png)]
1. at most once :最多一次 数据不可能重复,ack=0
2. at least once :最少一次 数据可能重复 ack=-1|1
3. exactly once :正好一次 数据不可能重复 ,需要配合ack=-1以及幂等机制
也就是:idempotent + at least once = exactly once
在producer的配置文件中添加enable.idempotence=true
8.10.2 幂等机制的简介
1. 为了避免生产者在重试时出现重复的数据保存到消息队列中,引入了幂等性。
2. 什么时候会出现重复发送:
当生产者没有收到broker的确认信息时,会认为发送失败,然后再次发送。就会出现重复发送的数据。
3. 什么是幂等性:
多次发送的数据,相当于一次发送的数据。去掉重复性。保证Exactly-Once(仅一次)
Kafka为了实现幂等性,它在底层设计架构中引入了ProducerID和SequenceNumber。
那这两个概念的用途是什么呢?
- ProducerID:在每个新的Producer初始化时,会被分配一个唯一的ProducerID,
这个ProducerID对客户端使用者是不可见的。
- SequenceNumber:对于每个ProducerID,Producer发送数据的每个Topic和Partition
都对应一个从0开始单调递增的SequenceNumber值。
重新尝试发送时,SequenceNumber是不递增的。broker会将收到的SequenceNumber和之前缓存过的SequenceNumber进行比较,如果相同,则认为是同一个,就不存储了。如果发现SequenceNumber大于缓存的SequenceNumber则认为是新的消息,就存储。
4. 如何使用幂等性?
非常简单:只需要在producer.properties里
设置如下属性即可
enable.idempotence=true
注意:
- 重试次数要大于0
- 还要使用acks机制
8.11 zookeeper在kafka中的作用
1. zookeeper在kafka中启动一个框架协调作用,维护broker的controller角色的选举
如果选举:
启动后的所有broker都会向zookeeper注册一个临时节点/controller, 谁注册成功谁就是controller
注册成功的那一个会启动一个KafkaController进程。
KafkaController负责管理集群broker的上下线,所有topic的分区副本分配和leader选举等工作
2.Controller的管理工作都是依赖于Zookeeper的。
Controller的管理工作都是依赖于Zookeeper的。
以下为partition和Controller的leader选举过程:
[外链图片转存中…(img-hpnaNiWr-1615727785636)]