内容输出来源:拉钩教育Java就业训练营
1 消息队列
1.1 什么是消息队列
消息队列是一种帮助开发人员解决系统间异步通信的中间件,常用于解决系统解耦和请求的削峰平谷的问题。
我们可以把消息队列比作是一个存放消息的容器,当我们需要使用消息的时候可以取出消息供自己使用。消息队列是分布式系统中重要的组件,使用消息队列主要是为了通过异步处理提高系统性能和削峰、降低系统耦合性。队列 Queue 是一种先进先出的数据结构,所以消费消息时也是按照顺序来消费的。比如生产者发送消息1,2,3…对于消费者就会按照1,2,3…的顺序来消费。
1.2 消息队列的应用场景
应用耦合:多应用间通过消息队列对同一消息进行处理,避免调用接口失败导致整个过程失败;
异步处理:多应用对消息队列中同一消息进行处理,应用间并发处理消息,相比串行处理,减少处理时间;
限流削峰:广泛应用于秒杀或抢购活动中,避免流量过大导致应用系统挂掉的情况;
消息驱动的系统:系统分为消息队列、消息生产者、消息消费者,生产者负责产生消息,消费者(可能有多个)负责对消息进行处理
1.2.1 异步处理
场景:
用户为了使用某个应用,进行注册,系统需要发送注册邮件并验证短信。
对这两个子系统操作的处理方式有两种:串行及并行。
涉及到三个子系统:注册系统、邮件系统、短信系统
串行方式:新注册信息生成后,先发送注册邮件,再发送验证短信
并行处理:新注册信息写入后,由发短信和发邮件并行处理
引入消息队列, 在来看整体的执行效率
在写入消息队列后立即返回成功给客户端,则总的响应时间依赖于写入消息队列的时间,而写入消息队列的时间本身是可以很快的,基本可以忽略不计,相比串行提高了2倍,相比并行提高了一倍
1.2.2 应用耦合
场景:上传一张图片,人脸识别系统会对该图片进行人脸识别。
一般的做法是,服务器接收到图片后,图片上传系统立即调用人脸识别系统,调用完成后再返回成功,如下图所示:
若使用消息队列:
客户端上传图片后,图片上传系统将图片信息批次写入消息队列,直接返回成功;
人脸识别系统则定时从消息队列中取数据,完成对新增图片的识别。
图片上传系统并不需要关心人脸识别系统是否对这些图片信息的处理、以及何时对这些图片信息进行处理。
1.2.3 限流消峰
场景:购物网站开展秒杀活动,一般由于瞬时访问量过大,服务器接收过大,会导致流量暴增,相关系统无法处理请求甚至崩溃。
而加入消息队列后,系统可以从消息队列中取数据,相当于消息队列做了一次缓冲。
请求先入消息队列,而不是由业务处理系统直接处理,做了一次缓冲,极大地减少了业务处理系统的压力;
队列长度可以做限制,事实上,秒杀时,后入队列的用户无法秒杀到商品,这些请求可以直接被抛弃,返回活动已结束或商品已售完信息;
1.2.4 消息事件驱动的系统
场景:用户新上传了一批照片 ---->人脸识别系统需要对这个用户的所有照片进行聚类 -------> 由对账系统重新生成用户的人脸索引(加快查询)。
这三个子系统间由消息队列连接起来,前一个阶段的处理结果放入队列中,后一个阶段从队列中获取消息继续处理。
优点:
避免了直接调用下一个系统导致当前系统失败;
每个子系统对于消息的处理方式可以更为灵活,可以选择收到消息时就处理,可以选择定时处理,也可以划分时间段按不同处理速度处理;
1.3 消息队列的两种模式
消息队列包括两种模式,点对点模式(point to point, queue)和发布/订阅模式(publish/subscribe,topic)
1.3.1 点对点模式
点对点模式下包括三个角色:
消息队列
发送者 (生产者)
接收者(消费者)
每个消息都被发送到一个特定的队列,接收者从队列中获取消息。队列保留着消息,可以放在内存 中也可以持久化,直到他们被消费或超时。
特点:
每个消息只有一个消费者,一旦被消费,消息就不再在消息队列中;
发送者和接收者间没有依赖性,发送者发送消息之后,不管有没有接收者在运行,都不会影响到发送者下次发送消息;
接收者在成功接收消息之后需向队列应答成功,以便消息队列删除当前接收的消息;
1.3.2 发布/订阅模式
发布/订阅模式下包括三个角色:
角色主题(Topic)
发布者(Publisher)
订阅者(Subscriber)
消息生产者(发布)将消息发布到topic中,同时有多个消息消费者(订阅)消费该消息。和点对点方式不同,发布到topic的消息会被多个订阅者消费。
特点:
每个消息可以有多个订阅者;
发布者和订阅者之间有时间上的依赖性
为了消费消息,订阅者必须保持在线运行。
2 kafka
2.1 什么是kafka
是一个分布式、分区的、多副本的、多订阅者,基于zookeeper协调的分布式日志系统(也可以当做MQ系统),常见可以用于web/nginx日志、访问日志,消息服务等等,Linkedin于2010年贡献给了Apache基金会并成为顶级开源项目。
Kafka主要设计目标:
- 以时间复杂度为O(1)的方式提供消息持久化能力,即使对TB级以上数据也能保证常数时间的访问性能。
- 高吞吐率。即使在非常廉价的商用机器上也能做到单机支持每秒100K条消息的传输。
- 支持普通服务器每秒百万级写入请求
- Memory mapped Files
- 支持Kafka Server间的消息分区,及分布式消费,同时保证每个partition内的消息顺序传输。
- 同时支持离线数据处理和实时数据处理。
- Scale out:支持在线水平扩展
2.2 kafka的特点
(1)解耦。Kafka具备消息系统的优点,只要生产者和消费者数据两端遵循接口约束,就可以自行扩展或修改数据处理的业务过程。
(2)高吞吐量、低延迟。即使在非常廉价的机器上,Kafka也能做到每秒处理几十万条消息,而它的延迟最低只有几毫秒。
(3)持久性。Kafka可以将消息直接持久化在普通磁盘上,且磁盘读写性能优异。
(4)扩展性。Kafka集群支持热扩展,Kaka集群启动运行后,用户可以直接向集群添。
(5)容错性。Kafka会将数据备份到多台服务器节点中,即使Kafka集群中的某一台加新的Kafka服务节点宕机,也不会影响整个系统的功能。
(6)支持多种客户端语言。Kafka支持Java、.NET、PHP、Python等多种语言。
(7) 支持多生产者和多消费者
2.3 应用场景
-
消息处理(MQ)
KafKa可以代替传统的消息队列软件
- KafKa的append来实现消息的追加,保证消息都是有序的有先来后到的顺序,
- 稳定性强队列在使用中最怕丢失数据,KafKa能做到理论上的写成功不丢失
- 分布式容灾好
- 容量大相对于内存队列,KafKa的容量受硬盘影响数据量不会影响到KafKa的速度
-
分布式日志系统(Log)
- KafKa的集群备份机制能做到n/2的可用,当n/2以下的机器宕机时存储的日志不会丢失
- KafKa可以对消息进行分组分片
- KafKa非常容易做到实时日志查询
-
流式处理
- 流式处理就是指实时地处理一个或多个事件流。
- 流式的处理框架(spark, storm , flink) 从主题中读取数据, 对其进行处理, 并将处理后的结果数据写入新的主题, 供用户和应用程序使用,kafka的强耐久性在流处理的上下文中也非常的有用
3 kafka架构
Kafka Cluster:由多个服务器组成。每个服务器单独的名字broker(
kafka broker:kafka集群中包含的服务器
Kafka Producer:消息生产者、发布消息到 kafka 集群的终端或服务。
Kafka consumer:消息消费者、负责消费数据。
Kafka Topic: 主题,一类消息的名称。存储数据时将一类数据存放在某个topci下,消费数据也是消费一类数据。
4 docker下kafka集群搭建
hostname | ip addr | port | listener |
---|---|---|---|
zoo1 | 192.168.0.11 | 2184:2181 | |
zoo2 | 192.168.0.12 | 2185:2181 | |
zoo3 | 192.168.0.13 | 2186:2181 | |
kafka1 | 192.168.0.14 | 9092:9092 | kafka1 |
kafka2 | 192.168.0.15 | 9093:9092 | kafka1 |
kafka3 | 192.168.0.16 | 9094:9092 | kafka1 |
kafka-manager | 192.168.0.17 | 9000:9000 | |
宿主机 | 192.168.200.20 |
4.1 准备环境
1) 克隆VM,修改IP地址为192.168.200.20
修改网络ip配置: vi /etc/sysconfig/network-scrpits/ifcfg-ens33
2) 安装docker - compose
#curl 是一种命令行工具,作用是发出网络请求,然后获取数据
curl -L https://github.com/docker/compose/releases/download/1.8.0/run.sh > /usr/local/bin/docker-compose
#chmod(change mode)命令是控制用户对文件的权限的命令
chmod +x /usr/local/bin/docker-compose
#查看版本
docker-compose --version
3)拉取镜像
#拉取Zookeeper镜像
docker pull zookeeper:3.4
#拉取kafka镜像
docker pull wurstmeister/kafka
#拉取kafka-manager镜像
docker pull sheepkiller/kafka-manager:latest
4.2 搭建过程
Zookeeper、Kafka、Kafka-Manager各一个镜像文件
docker-compose-zookeeper.yml
version: '2'
services:
zoo1:
image: zookeeper:3.4
restart: always
hostname: zoo1
container_name: zoo1
ports:
- 2184:2181
environment:
ZOO_MY_ID: 1
ZOO_SERVERS: server.1=zoo1:2888:3888 server.2=zoo2:2888:3888 server.3=zoo3:2888:3888
networks:
kafka:
ipv4_address: 192.168.0.11
zoo2:
image: zookeeper:3.4
restart: always
hostname: zoo2
container_name: zoo2
ports:
- 2185:2181
environment:
ZOO_MY_ID: 2
ZOO_SERVERS: server.1=zoo1:2888:3888 server.2=0.0.0.0:2888:3888 server.3=zoo3:2888:3888
networks:
kafka:
ipv4_address: 192.168.0.12
zoo3:
image: zookeeper:3.4
restart: always
hostname: zoo3
container_name: zoo3
ports:
- 2186:2181
environment:
ZOO_MY_ID: 3
ZOO_SERVERS: server.1=zoo1:2888:3888 server.2=zoo2:2888:3888 server.3=0.0.0.0:2888:3888
networks:
kafka:
ipv4_address: 192.168.0.13
networks:
kafka:
external:
name: kafka
docker-compose-kafka.yml
version: '2'
services:
kafka1:
image: wurstmeister/kafka
restart: always
hostname: kafka1
container_name: kafka1
privileged: true
ports:
- 9092:9092
environment:
KAFKA_ADVERTISED_HOST_NAME: kafka1
KAFKA_LISTENERS: PLAINTEXT://kafka1:9092
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka1:9092
KAFKA_ADVERTISED_PORT: 9092
KAFKA_ZOOKEEPER_CONNECT: zoo1:2181,zoo2:2181,zoo3:2181
external_links:
- zoo1
- zoo2
- zoo3
networks:
kafka:
ipv4_address: 192.168.0.14
kafka2:
image: wurstmeister/kafka
restart: always
hostname: kafka2
container_name: kafka2
privileged: true
ports:
- 9093:9093
environment:
KAFKA_ADVERTISED_HOST_NAME: kafka2
KAFKA_LISTENERS: PLAINTEXT://kafka2:9093
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka2:9093
KAFKA_ADVERTISED_PORT: 9093
KAFKA_ZOOKEEPER_CONNECT: zoo1:2181,zoo2:2181,zoo3:2181
external_links:
- zoo1
- zoo2
- zoo3
networks:
kafka:
ipv4_address: 192.168.0.15
kafka3:
image: wurstmeister/kafka
restart: always
hostname: kafka3
container_name: kafka3
privileged: true
ports:
- 9094:9094
environment:
KAFKA_ADVERTISED_HOST_NAME: kafka3
KAFKA_LISTENERS: PLAINTEXT://kafka3:9094
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka3:9094
KAFKA_ADVERTISED_PORT: 9094
KAFKA_ZOOKEEPER_CONNECT: zoo1:2181,zoo2:2181,zoo3:2181
external_links:
- zoo1
- zoo2
- zoo3
networks:
kafka:
ipv4_address: 192.168.0.16
networks:
kafka:
external:
name: kafka
docker-compose-manager.yml
version: '2'
services:
kafka-manager:
image: sheepkiller/kafka-manager:latest
restart: always
container_name: kafka-manager
hostname: kafka-manager
ports:
- 9000:9000
environment:
ZK_HOSTS: zoo1:2181,zoo2:2181,zoo3:2181
KAFKA_BROKERS: kafka1:9092,kafka2:9092,kafka3:9092
APPLICATION_SECRET: letmein
KM_ARGS: -Djava.net.preferIPv4Stack=true
networks:
kafka:
ipv4_address: 192.168.0.17
networks:
kafka:
external:
name: kafka
将上述三个文件上传至docker宿主机中并进行部署
docker-compose -f /home/docker-compose-zookeeper.yml up -d
docker-compose -f /home/docker-compose-kafka.yml up -d
docker-compose -f /home/docker-compose-manager.yml up -d
5 kafka基本操作
5.1 创建topic
创建一个名字为test的主题, 有一个分区,有三个副本。一个主题下可以有多个分区,每个分区可以用对应的副本。
#登录到Kafka容器
docker exec -it 9218e985e160 /bin/bash
#切换到bin目录
cd opt/kafka/bin/
#执行创建
kafka-topics.sh --create --zookeeper zoo1:2181 --replication-factor 3 --partitions 1 --topic test
–create:新建命令
–zookeeper:Zookeeper节点,一个或多个
–replication-factor:指定副本,每个分区有三个副本。
–partitions:1
5.2 生产者生产数据
运行 producer,然后在控制台输入一些消息以发送到服务器。
kafka-console-producer.sh --broker-list kafka1:9092,kafka2:9093,kafka3:9094 --topic test
5.3 消费者消费数据
kafka-console-consumer.sh --bootstrap-server kafka1:9092, kafka2:9093, kafka3:9094 --topic test --from-beginning
5.4 运行describe的命令
运行describe查看topic的相关详细信息
#查看topic主题详情,Zookeeper节点写一个和全部写,效果一致
kafka-topics.sh --describe --zookeeper zoo1:2181,zoo2:2181,zoo3:2181 --topic test
leader:是负责给定分区的所有读取和写入的节点。每个节点将成为随机选择的分区部分的领导者。
replicas:显示给定partiton所有副本所存储节点的节点列表,不管该节点是否是leader或者是否存活。
isr:副本都已同步的的节点集合,这个集合中的所有节点都是存活状态,并且跟leader同步
5.5 增加topic分区数
任意kafka服务器执行以下命令可以增加topic分区数
kafka-topics.sh --zookeeper zkhost:port --alter --topic test --partitions 8
5.6 增加配置
flush.messages:此项配置指定时间间隔:强制进行fsync日志,默认值为None
如果这个选项设置为1,那么每条消息之后都需要进行fsync,如果设置为5,则每5条消息就需要进行一次fsync。
动态修改kakfa的配置
kafka-topics.sh --zookeeper zoo1:2181 --alter --topic test --config flush.messages=1
5.7 删除配置
动态删除kafka集群配置
kafka-topics.sh --zookeeper zoo1:2181 --alter --topic test --delete-config flush.messages
5.8 删除topic
目前删除topic在默认情况只是打上一个删除的标记,在重新启动kafka后才删除。如果需要立即删除,则需要在
server.properties中配置:
delete.topic.enable=true(集群中的所有实例节点),一个主题会在不同的kafka节点中分配分组信息和副本信息
然后执行以下命令进行删除topic
kafka-topics.sh --zookeeper zoo1:2181 --delete --topic test
6 Java API操作kafka
修改Windows的Host文件:
目录:C:\Windows\System32\drivers\etc (win10)
添加:
192.168.200.20 kafka1
192.168.200.20 kafka2
192.168.200.20 kafka3
6.1 生产者
public class ProducerDemo {
public static String topic = "lagou";//定义主题
public static void main(String[] args) {
//构造一个消息生产者对象,关于kafka集群的配置
Properties properties = new Properties();
//指定集群节点
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.200.20:9092,192.168.200.20:9093,192.168.200.20:9094");
//发送消息,网络传输,对key和value指定对应的序列化类
properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
//创建消息生产者
KafkaProducer<String,String> producer = new KafkaProducer<String, String>(properties);
//发送100条消息
for(int i=1;i<=100;i++){
//设置消息内容
String msg = "hello,"+i;
//构建一个消息对象
ProducerRecord<String,String> record1 = new ProducerRecord<>(topic,msg);
//发送
producer.send(record1);
System.out.println("消息发送成功,msg"+msg);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//关闭消息生产者对象
producer.close();
}
}
6.2 消费者
public class ConsumerDemo {
public static String topic = "lagou";//定义主题
public static void main(String[] args) {
//属性对象
Properties properties = new Properties();
//指定集群节点
properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,"192.168.200.20:9092,192.168.200.20:9093,192.168.200.20:9094");
//发反序列化类
properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
//指定分组名称
properties.put(ConsumerConfig.GROUP_ID_CONFIG, "lagou_grop1");
//消息消费者对象
KafkaConsumer<String,String> consumer1 = new KafkaConsumer<String, String>(properties);
//订阅消息
consumer1.subscribe(Collections.singletonList(topic));
while (true){
ConsumerRecords<String, String> records = consumer1.poll(500);
for(ConsumerRecord<String,String> record : records){
System.out.println("主题:" + record.topic()+",偏移量" + record.offset()+",消息"+ record.value());
}
}
}
}
7 kafka原理
7.1 分区副本机制
kafka有三层结构:kafka有多个主题,每个主题有多个分区,每个分区又有多条消息。
分区机制:主要解决了单台服务器存储容量有限和单台服务器并发数限制的问题 一个分片的不同副本不能放到同一个broker上。
当主题数据量非常大的时候,一个服务器存放不了,就将数据分成两个或者多个部分,存放在多台服务器上。每个服务器上的数据,叫做一个分片
分区对于 Kafka 集群的好处是:实现负载均衡,高存储能力、高伸缩性。分区对于消费者来说,可以提高并发度,提高效率。
副本:副本备份机制解决了数据存储的高可用问题
当数据只保存一份的时候,有丢失的风险。为了更好的容错和容灾,将数据拷贝几份,保存到不同的机器上。
多个follower副本通常存放在和leader副本不同的broker中。通过这样的机制实现了高可用,当某台机器挂掉后,其他follower副本也能迅速”转正“,开始对外提供服务。
7.2 kafka保证数据不丢失机制
从Kafka的大体角度上可以分为数据生产者,Kafka集群,还有就是消费者,而要保证数据的不丢失也要从这三个角度去考虑。
7.2.1 消息生产者
消息生产者保证数据不丢失:消息确认机制(ACK机制),参考值有三个:0,1,-1
//producer无需等待来自broker的确认而继续发送下一批消息。
//这种情况下数据传输效率最高,但是数据可靠性确是最低的。
properties.put(ProducerConfig.ACKS_CONFIG,"0");
//producer只要收到一个分区副本成功写入的通知就认为推送消息成功了。
//这里有一个地方需要注意,这个副本必须是leader副本。
//只有leader副本成功写入了,producer才会认为消息发送成功。
properties.put(ProducerConfig.ACKS_CONFIG,"1");
//ack=-1,简单来说就是,producer只有收到分区内所有副本的成功写入的通知才认为推送消息成功了。
properties.put(ProducerConfig.ACKS_CONFIG,"-1");
7.2.2 消息消费者
什么时候消费者丢失数据呢?
由于Kafka consumer默认是自动提交位移的(先更新位移,再消费消息),如果消费程序出现故障,没消费完毕,则丢失了消息,此时,broker并不知道。
解决方案:
enable.auto.commit=false 关闭自动提交位移
在消息被完整处理之后再手动提交位移
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"false");
7.3 消息存储及查询机制
kafka 使用日志文件的方式来保存生产者消息,每条消息都有一个 offset 值来表示它在分区中的偏移量。
Kafka 中存储的一般都是海量的消息数据,为了避免日志文件过大,一个分片 并不是直接对应在一个磁盘上的日志文件,而是对应磁盘上的一个目录,这个目录的命名规则是<topic_name>_<partition_id>。
7.3.1 消息存储机制
7.3.2 通过 offset 查找 message
存储的结构:一个主题 --> 多个分区 ----> 多个日志段(多个文件)
第一步:查询segment file:
segment file命名规则跟offset有关,根据segment file可以知道它的起始偏移量,因为Segment file的命名规则是上一个segment文件最后一条消息的offset值。所以只要根据offset 二分查找文件列表,就可以快速定位到具体文件。
第二步通过segment file查找message:
自动提交位移
在消息被完整处理之后再手动提交位移
properties.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG,"false");
7.3 消息存储及查询机制
kafka 使用日志文件的方式来保存生产者消息,每条消息都有一个 offset 值来表示它在分区中的偏移量。
Kafka 中存储的一般都是海量的消息数据,为了避免日志文件过大,一个分片 并不是直接对应在一个磁盘上的日志文件,而是对应磁盘上的一个目录,这个目录的命名规则是<topic_name>_<partition_id>。
7.3.1 消息存储机制
[外链图片转存中…(img-lwg1SSgF-1623073457855)]
7.3.2 通过 offset 查找 message
存储的结构:一个主题 --> 多个分区 ----> 多个日志段(多个文件)
第一步:查询segment file:
segment file命名规则跟offset有关,根据segment file可以知道它的起始偏移量,因为Segment file的命名规则是上一个segment文件最后一条消息的offset值。所以只要根据offset 二分查找文件列表,就可以快速定位到具体文件。
第二步通过segment file查找message:
通过第一步定位到segment file,当offset=5000时,依次定位到00000000000000000000.index的元数据物理位置和00000000000000000000.log的物理偏移地址,然后再通过00000000000000000000.log顺序查找直到offset=5000为止。