一、什么是消息中间件/消息队列?
消息队列(Message Queue)一般大家习惯简称为MQ。主要特点为异步处理,也就是说消息的发送者和接收者不需要同时与消息队列交互。消息会保存在队列中,直到接收者取回它。消息队列是消息中间件的一种实现方式。
典型的消息中间件包含 3 部分 :
producer(发布者)
broker(消息中间件)
consumer(消费者)
使用消息中间件的优点:
- 解耦:消息写入中间件,需要消息的系统自己从消息队列中订阅
- 异步:将消息写入消息队列,非必要的业务逻辑以异步的方式运行,加快响应速度
- 削峰:并发量大时,系统可以按照数据库能处理的并发量,从消息队列中慢慢拉取消息。
分布式
存储海量数据
多副本 保证数据安全
高可用 一台机器挂掉 另一台机器迅速替代
高吞吐 数据快速读取
高并发 每台机器读取都快
纪录偏移量
可扩展
消息中间件解决了什么问题?
数据快速写入、快速并且安全存储、快速被读取消费、消费者组、可以记录偏移量
xshell中 ctrl+r 可以搜索历史命令 history | grep
二、Kafka特点
1.解耦:
允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。
2.冗余:
消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的"插入-获取-删除"范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。
3.扩展性:
因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。
4.灵活性 & 峰值处理能力:
在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见。如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。
5.可恢复性:
系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。
6.顺序保证:
在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。(Kafka 保证一个 Partition 内的消息的有序性)
7.缓冲:
有助于控制和优化数据流经过系统的速度,解决生产消息和消费消息的处理速度不一致的情况。
8.异步通信:
很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。
三、Kafka相关概念
1.producer:
消息生产者,发布消息到 kafka 集群的Borker(下面的Leader分区)。
2.broker:
kafka 集群中安装Kafka的服务器(容器),broker要有唯一的ID。
3.topic:
每条发布到 kafka 集群的消息属于的类别,即 kafka 是面向 topic 的(相当于数据库中的表)
4.partition:
partition 是物理上的概念,每个 topic 包含一个或多个 partition。kafka 分配的单位是 partition。
5.consumer:
从 kafka 集群中消费消息的终端或服务。
6.Consumer group:
high-level consumer API 中,每个 consumer 都属于一个 consumer group,每条消息只能被 consumer group 中的一个 Consumer 消费,但可以被多个 consumer group 消费。
7.replication:
partition 的副本,保障 partition 的高可用。
8.leader:
replica 中的一个角色, producer 和 consumer 只跟 leader 交互(leader分区负责读写)。
9.follower:
replica 中的一个角色,从 leader 中复制数据(follower分区负责同步数据)。
10.zookeeper:
kafka 通过 zookeeper 来存储集群的 meta 信息
四、Kafka架构
每个分区的副本有leader和follower之分,当leader副本挂掉后,zookeeper会快速从follower副本中选举出一位新的leader副本,实现高可用。
- Leader副本负责读写
- Follower副本负责同步数据
- 在同一个消费者组中的多个消费者,消费的数据没有交叉重叠,不同消费者读不同分区,不同消费者组的消费者没有任何关系
- 数据被消费,不是从kafka中删除,而是标记被指定的消费者(消费者组)消费了,并记录偏移量
- 如果一个消费者组中的消费者的数量少于分区的数量,一个消费者可以读取多个分区
- 如果一个消费者组中的消费者的数量大于分区的数量,会有部分消费者不读数据
- 当一个消费者组中的消费者数量与指定Topic的分区数量一致时,效率最佳
- 创建Topic时分区的数量通常与机器的配置和数量有关(机器的数量*每台机器cpu的cores)
- 消费者读取一个分区中的数据可以保证顺序,如果读取多个分区中的数据没法保证数据的顺序 (分区内有序)
- 生产者push数据到broker,消费者从broker上pull数据
- 分区保证并行度、吞吐量,副本保证数据安全
- 数据以log形式记录到磁盘,以record格式保存
五、Kafka安装部署
1、上传安装包并解压
2、修改配置文件
vi $KAFKA_HOME/config/server.properties
#指定broker的id
broker.id=1
#数据存储的目录 目录不存在的话自己手动创建
log.dirs=/opt/apps/kafka-2.6.2/data/kafka
#指定zk地址
zookeeper.connect=linux1:2181,linux2:2181,linux3:2181
#可以删除topic的数据(生成环境不用配置) 不设置的话不会删除 只会标记删除
#delete.topic.enable=true
3、分发集群
for i in {1..3}; do scp -r kafka_2.12-2.6.2/ linux$i:$PWD; done
4、修改其他节点Kafka的broker.id
5、启动zk
zkServer.sh start
6、在所有节点分别启动Kafka
kafka-server-start.sh -daemon /opt/apps/kafka-2.6.2/config/server.properties
-daemon 守护进程
7、查看Kafka进程信息
jps
8、查看Kafka的topic
#老的api
kafka-topics.sh --list --zookeeper localhost:2181
#新的
kafka-topics.sh --list --bootstrap-server linux1:9092,linux2:9092,linux3:9092
–bootstrap-server 指定broker地址和端口号
9、创建topic
#老的
kafka-topics.sh --zookeeper localhost:2181 --create --topic hellokafka --replication-factor 3 --partitions 3
#新的
kafka-topics.sh --create --topic test2 --partitions 3 --replication-factor 3 --bootstrap-server linux1:9092,linux2:9092,linux3:9092
–topic 后面跟topic名字
–partitions 后面跟分区数
–replication-factor 后面跟副本数
创建topic时,副本数不能超过brokers的个数,分区数可以
创建topic时命名不要有下划线和 . 否则会警告
10、启动一个命令行生产者
kafka-console-producer.sh --topic first --broker-list centos201:9092
kafka-console-producer.sh --bootstrap-server linux1:9092,linux2:9092,linux3:9092 --topic test
11、启动一个命令行消费者
kafka-console-consumer.sh --topic first --zookeeper centos201:2181
kafka-console-consumer.sh --bootstrap-server linux1:9092,linux2:9092,linux3:9092 --from-beginning --topic test
–from-beginning 消费以前产生的所有数据,如果不加,就是消费消费者启动后产生的数据
12、删除topic
kafka-topics.sh --delete --topic test --zookeeper localhost:2181
配置文件没有配置 delete.topic.enable=true 的话不会真的删除,只是标记删除
13、查看topic详细信息
kafka-topics.sh --zookeeper localhost:2181 --describe --topic test
14、查看某个topic的偏移量
kafka-console-consumer.sh --topic __consumer_offsets --bootstrap-server linux1:9092,linux2:9092,linux3:9092 --formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter" --consumer.config /bigdata/kafka_2.12-2.6.2/config/consumer.properties --from-beginning | grep 组id
六、Scala操作Kafka
导入依赖
<dependency>
<groupId>org.scala-lang</groupId>
<artifactId>scala-library</artifactId>
<version>2.12.12</version>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>2.6.2</version>
</dependency>
1、创建生产者
object ScalaProducer {
def main(args: Array[String]): Unit = {
//创建配置对象
val properties = new Properties
//配置参数
//连接kafka节点,往哪些broker中写
properties.setProperty("bootstrap.servers", "linux1:9092,linux2:9092,linux3:9092")
//需要将数据序列化才能写入kafka
//指定key序列化方式
properties.setProperty("key.serializer", "org.apache.kafka.common.serialization.StringSerializer")
//指定value序列化方式
//会将字符串变成byteArray写进去
properties.setProperty("value.serializer", classOf[StringSerializer].getName) // 两种指定序列化方式写法都行
//创建一个生产者,指定key、value的类型
//key可以决定分区,value即为写入到kafka中的数据
val producer = new KafkaProducer[String, String](properties)
//创建ProducerRecord对象封装数据,指定key、value的类型
//不设置partition和key,只设置totic、value,数据被写到随机分区
val record1 = new ProducerRecord[String,String]("lxl", "hello world1")
//指定topic、partition、value,key设为null,数据被写到指定分区
val record2 = new ProducerRecord[String,String]("lxl",0,null, "hello world2")
//指定topic、key、value,数据根据key的hash值决定写到哪个分区
val record3 = new ProducerRecord[String,String]("lxl", "kafka","hello world3")
//发送数据
producer.send(record1 )
//释放资源
producer.close()
}
}
2、创建消费者
object ScalaConsumer {
def main(args: Array[String]): Unit = {
//创建配置文件对象
val properties = new Properties
//配置要连接的kafka的brokers
properties.setProperty("bootstrap.servers","linux1:9092,linux2:9092,linux3:9092")
//配置key反序列化方式
properties.setProperty("key.deserializer","org.apache.kafka.common.serialization.StringDeserializer")
//配置value反序列化方式
properties.setProperty("value.deserializer",classOf[StringDeserializer].getName)
//必须指定消费者的组ID,否则报错
properties.setProperty("group.id", "g0001")
//指定读取数据的偏移量
//earliest:如果没有历史偏移量,从最开始读取数据,如果对应的这个组ID记录过偏移量,会接着偏移量继续读
//latest: 消费者启动后产生的数据才读取(默认的)
//none: 如果指定group.id没有历史偏移量,抛出异常
properties.setProperty("auto.offset.reset", "earliest")
//默认情况下,消费者消费完数据后,会自动更新偏移量
//设置此参数,消费者不自动提交偏移量
properties.setProperty("enable.auto.commit", "false")
//创建消费者对象
val consumer = new KafkaConsumer[String, String](properties)
//订阅指定的topic,可以指定一个或多个topic
val topics = util.Arrays.asList("lxl")
consumer.subscribe(topics)
while(true){
//拉取数据 参数是最大阻塞时间,如果消费者从buffer中经历timeout毫秒后拉不到数据,就返回个空消息
val consumerRecords: ConsumerRecords[String, String] = consumer.poll(Duration.ofSeconds(5))
consumerRecords.forEach(x=>println(x))
consumerRecords.forEach(x=>println(x.value()))
}
}
}
3、数据写入Kafka时机
生产者调用send()方法时,并没有立即将数据写入Kafka,而是先在客户端的Buffer中缓存,当达到一定的大小或到达一定时间或者调用了flush、close才会将数据写入Kafka中。
4、数据写入分区的策略
- 如果生产者写入数据时不指定分区编号和key,默认决定分区的策略是轮询。当数据达到指定的大小、达到一定时间或调用flush,就会将数据写入到分区内,然后切换下一个分区(目的是为了让数据均由分散到多个分区,就为了负载均衡)
- 写入数据时指定了分区编号,会将数据写入到指定的分区编号中
- 没有指定分区编号,指定了非null的key,可以保证相同key的是一定写入到同一个分区,但是同一分区中可能有多个不同的key(类似HashPartition,基于key的哈希值来选择分区)
- 如果指定了分区编号,同时又指定了非null的key,优先使用分区编号
- 分区编号从0 开始
5、同一消费者组中消费者与分区关系
- 消费者数量少于分区数时,一个消费者可以对应多个分区,一个分区只能对应一个消费者
- 消费者数量多于分区数时,一个消费者对应一个分区,剩余消费者空跑
- 当有消费者上下线时,会触发消费者分区重新分配
- 消费者分区分配策略:https://www.cnblogs.com/cjsblog/p/9664536.html
6、设置消费者不自动提交偏移量
消费者默认自动提交偏移量:enable.auto.commit 默认就是true
偏移量会记录在 __consumer_offsets 这个topic中,记录一组消费者的偏移量
自动提交偏移量的时间周期:auto.commit.interval.ms:默认是5000毫秒
设置Kafka不自动提交偏移量,以后我们编程可以精准控制
enable.auto.commit:false
查看偏移量
kafka-console-consumer.sh --topic __consumer_offsets --bootstrap-server linux1:9092,linux2:9092,linux3:9092 --formatter "kafka.coordinator.group.GroupMetadataManager\$OffsetsMessageFormatter" --consumer.config /opt/apps/kafka-2.6.2/config/consumer.properties --from-beginning
7、Kafka事务
(1)生产者
/**
* 说明Kafka生产者的事务,可以保证数据的正确性和安全性
*
*/
object ProducerTransactionDemo {
def main(args: Array[String]): Unit = {
//指定broker的地址
val properties = new Properties
// 连接kafka节点
properties.setProperty("bootstrap.servers", "node-1.51doit.cn:9092,node-2.51doit.cn:9092,node-3.51doit.cn:9092")
//指定key序列化方式
properties.setProperty("key.serializer", "org.apache.kafka.common.serialization.StringSerializer")
//指定value序列化方式
properties.setProperty("value.serializer", classOf[StringSerializer].getName) // 两种写法都行
//如果要开启事务,必须为生产者设置一个事务ID(每一次都要有一个事务ID)
properties.setProperty("transactional.id", "doit1123123123")
//创建一个生产者
val producer = new KafkaProducer[String, String](properties)
//初始化事务
producer.initTransactions()
//开启事务(要使用事务)
producer.beginTransaction()
try {
val producerRecord1 = new ProducerRecord[String, String]("worldcount", null, "hello kafka 777777")
producer.send(producerRecord1)
//先将第一条数据刷到Kafka中
producer.flush() //将数据从客户端写入到Kafka的Broker中(并没有提交事务)
val i = 1 / 0
val producerRecord2 = new ProducerRecord[String, String]("worldcount", null, "hello kafka 888888")
producer.send(producerRecord2)
//提交事务
producer.commitTransaction()
} catch {
case e: Exception => {
//放弃事务
producer.abortTransaction()
}
}
producer.close()
}
}
(2)消费者
/**
* 设置消费者只读取已经提交事务的数据
* isolation.level = read_committed 只读已经提交事务的数据
* isolation.level = read_uncommitted 读取全部的数据
* 默认是read_uncommitted
*
*/
object ConsumerReadCommitedDemo {
def main(args: Array[String]): Unit = {
//指定broker的地址
val properties = new Properties
// 连接kafka节点
properties.setProperty("bootstrap.servers", "node-1.51doit.cn:9092,node-2.51doit.cn:9092,node-3.51doit.cn:9092")
//指定key反序列化方式
properties.setProperty("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer")
//指定value反序列化方式
properties.setProperty("value.deserializer", classOf[StringDeserializer].getName) // 两种写法都行
//指定消费者的组ID
properties.setProperty("group.id", "g00011")
//指定读取数据的偏移量
//earliest:如果没有历史偏移量,从最开始读取数据,如果对应的这个组ID记录过偏移量,会接着偏移量继续读
//latest: 消费者启动后产生的数据才读取(默认的)
//none: 如果指定group.id没有历史偏移量,抛出异常
properties.setProperty("auto.offset.reset", "earliest")
//消费者不自动提交偏移量
properties.setProperty("enable.auto.commit", "false")
//读取已经提交事务的数据
properties.setProperty("isolation.level", "read_committed")
//默认情况下,消费者消息完数据后,会自动更新偏移量
val kafkaConsumer: KafkaConsumer[String, String] = new KafkaConsumer[String, String](properties)
//读取数据
val topics = util.Arrays.asList("worldcount")
//订阅指定的topic,可以指定一个或多个topic
kafkaConsumer.subscribe(topics)
while (true) {
val consumerRecords: ConsumerRecords[String, String] = kafkaConsumer.poll(Duration.ofSeconds(5))
//println("----------------> " + System.currentTimeMillis())
val iterator = consumerRecords.iterator()
while (iterator.hasNext) {
val record: ConsumerRecord[String, String] = iterator.next()
//val line = record.value()
println(record)
}
}
不开启事务时,数据写过去就是commited
开启了事务但没提交事务,数据写过去是uncommited
放弃事务后,客户端缓存中没有刷写到Kafka中的事务数据不会再刷写过去
设置消费者只读取已经提交事务的数据
isolation.level = read_committed 只读已经提交事务的数据
isolation.level = read_uncommitted 读取全部的数据
默认是read_uncommitted
8、Kafka的ACK机制(应答机制)
acks可以配置三种参数:0、1、-1/all,默认为all
- 0:生产者向Leader分区发送数据,不用等待任何响应,就可以继续发送数据。不会重试
- 1:生产者将数据发送到Leader,Leader分区将数据落盘即响应Producer。若Follower还没同步数据Leader宕机,则数据丢失。
- -1/all:不但要Leader将数据落盘,还要等待所有的 ISR(同步副本) 数据落盘,然后Follower响应Leader,Leader响应生产者,生产者才会继续写数据。此时依然有风险,同步副本有可能只有Leader自己。可以配置将最小同步副本数设为2及以上。
分区中的所有副本统称为AR(Assigned Repllicas)。所有与leader副本保持一定程度同步的副本(包括Leader)组成ISR(In-Sync Replicas),ISR集合是AR集合中的一个子集。消息会先发送到leader副本,然后follower副本才能从leader副本中拉取消息进行同步,同步期间内follower副本相对于leader副本而言会有一定程度的滞后。前面所说的“一定程度”是指可以忍受的滞后范围,这个范围可以通过参数进行配置。与leader副本同步滞后过多的副本(不包括leader)副本,组成OSR(Out-Sync Relipcas),由此可见:AR=ISR+OSR。在正常情况下,所有的follower副本都应该与leader副本保持一定程度的同步,即AR=ISR,OSR集合为空。
replica.lag.max.messages设置为4,表明只要follower落后leader不超过3,就不会从同步副本列表中移除。replica.lag.time.max设置为500 ms,表明只要follower向leader发送请求时间间隔不超过500 ms,就不会被标记为死亡,也不会从同步副本列中移除。
acks如果等于 -1/all ,最安全,但是效率低
acks如果等于 1 ,相对安全,效率较高
acks如果等于 0 ,最不安全,效率最高
acks如果等于 1或-1/all,需要配合retries参数使用。当发送失败时客户端会进行重试,重试的次数由retries指定,此参数默认设置为0,最大为 Integer.MAX_VALUE。
如果设置retries大于0而没有设置max.in.flight.requests.per.connection=1则意味着放弃发送消息的顺序性。
max.in.flight.requests.per.connection 默认为5
在单个连接中,producer客户端在阻塞之前,可以允许未被确认的最大请求数,即当一个连接中未被确认的请求数超过了该设置,那么该producer客户端将会阻塞。换句话说,就是生产者在收到服务器晌应之前可以发送多少个消息。它的值越高,就会占用越多的内存,不过也会提升吞吐量。把它设为1可以保证消息是按照发送的顺序写入服务器的,即使发生了重试。
如果没有启用幂等功能,但仍然希望按顺序发送消息,则应将此设置配置为1。但是,如果已经启用了幂等,则无需显式定义此配置。
/**
* 保证生产者写入数据是安全的
* 设置acks 为 1 或 -1/all
* 同时还要设置retries大于0
*/
object ProducerAckDemo {
def main(args: Array[String]): Unit = {
//指定broker的地址
val properties = new Properties
// 连接kafka节点
properties.setProperty("bootstrap.servers", "node-1.51doit.cn:9092,node-2.51doit.cn:9092,node-3.51doit.cn:9092")
//指定key序列化方式
properties.setProperty("key.serializer", "org.apache.kafka.common.serialization.StringSerializer")
//指定value序列化方式
properties.setProperty("value.serializer", classOf[StringSerializer].getName) // 两种写法都行
//设置ACK
properties.setProperty("acks", "1")
//设置retries
properties.setProperty("retries", Integer.MAX_VALUE.toString)
//创建一个生产者
val producer = new KafkaProducer[String, String](properties)
val producerRecord = new ProducerRecord[String, String]("worldcount", null, "hello kafka")
producer.send(producerRecord)
producer.close()
}
}