Kafka
1. 消息队列
1.1 消息队列定义
消息队列(MessageQueue):是一种异步的服务间通信方式,是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。
简单点说:消息队列MQ用于实现两个系统之间或者两个模块之间传递消息数据时,实现数据缓存。
1.2 消息队列概述
-
生产者: 负责往消息队列中写数据
-
消费者: 负责从消息队列中读数据
-
消息队列: 临时存放两个系统之间需要传递的数据
-
主题: 数据的分类,用于区分消息队列中不同的业务的数据
-
订阅发布模式:
生产者往消息队列中生产数据,将数据写入对应的主题中;消费者可以订阅主题,如果主题中出现新的数据,消费就可以立即消费,消费成功以后,不会立即主动删除数据。
特点:一个消费者可以订阅多个主题,一个主题可以被多个消费者订阅
消息队列的优点与缺点:
优点 | 缺点 |
---|---|
实现了架构解耦 | 架构运维更加复杂,一旦消息队列故障,整个系统全部崩溃 |
实现异步,提高传输性能 | 数据保证更加复杂 |
限流削峰 |
2. Kafka概念
2.1 Kafka的定义
Kafka: 是基于订阅发布模式的分布式实时消息队列系统。
2.2 基础角色
- Broker:Kafka是一个分布式集群,多台机器构成,每台Kafka的节点就是一个Broker。
- Broker:就是Kafka节点,负责 存储 Kafka中的 数据 提供 读写
- 进程:Kafka
- 架构:公平分布式主从架构,基于Zookeeper选举主节点
- 主:Broker【也叫 Controller】是Kafka主节点,负责管理Kafka的节点、Topic、元数据,负责存储数据,对外提供读写
- 从:Broker:负责存储数据,对外提供读写
- Kafka 3.0版本新特性:可以摆脱ZK,独立运行主从节点了
- Producer:生产者
- 功能:负责往Kafka写数据的客户端
- Consumer:消费者
- 功能:负责从Kafka中读取数据
- Consumer Group:消费者组,Kafka的订阅发布模式中必须以消费者组的形式从Kafka中消费数据
- 任何一个消费者必须属于某一个消费者组
- 一个消费者组中可以有多个消费者:多个消费者共同并行消费数据,提高消费性能
- 消费者组中多个消费者消费的数据是不一样的
- 整个消费者组中所有消费者消费的数据加在一起是一份完整的数据
- zk功能:
- 辅助选举Controller:启动时所有Broker都会到ZK中创建临时节点,谁创建成功谁就是主节点
- 存储Kafka元数据
- Kafka 3.0版本新特性:可以摆脱ZK,独立运行了
2.3 主题与分区(Topic & Partition)
- Topic:数据主题,用于区分不同的数据,对数据进行分类
- Kafka是分布式存储,所以Topic也是分布式的,Topic是Kafka中分布式的数据存储对象,写入Topic的数据会分布式的存储在Kakfa中
- Partition:数据分区,用于实现Topic的分布式存储,对Topic的数据进行划分
- 每个Topic都可以对应多个分区,每个分区存储在不同的Kafka节点Broker上。如上图:Topic名称为T1,T1有三个分区:P0、P1、P2。
- Kafka中一个分区的副本个数最多只能等于机器的个数
- Kafka将一个分区的多个副本,(由Controller即主broker)划分为两种角色(Leader与Follower)
- Leader副本: 负责对外提供读写,生产者和消费者只对leader副本进行读写
- Follower副本: 与Leader同步数据,如果leader故障,从follower选举新的leader副本对外提供读写
- Kafka的主节点【controller】根据机器的健康状态、数据的完整性来选择Leader和Follower副本
HDFS | Kafka | |
---|---|---|
节点 | NameNode + DataNode:普通分布式主从架构 | Broker:公平分布式主从架构(根据zk选举划分成 Leader 与 Follower) |
对象 | 文件 | Topic |
划分 | Block块 | Partition分区 |
规则 | 按照大小:BlockSize=128M | 自己决定 |
安全 | 副本机制:默认3份 | 副本机制:自己决定 |
2.4 分片与偏移量(Segment & Offset)
- Segment:对每个分区的数据进行了更细的划分,用于存储Kafka中的数据文件并记录索引
- 先写入的数据会生成一对Segment文件
- 每个Segment对应两种【三个】文件
- x.log:存储数据
- x.index 与 x.timeindex:对应.log的文件的索引
- Segment文件的名字就是这个Segment记录的offset的最小
- Offset:是每条数据在自己分区中的偏移量
- 写入分区的顺序就是offset偏移量,Offset是分区级别的,每个分区的offset独立管理,都从0开始
- Kafka写入数据按照KV来写入数据,数据存储的结构(offset, Key ,Value)
- 消费者数据的读取都是按照Offset来读取数据
2.5 Kafka 概念总结
Kafka 概念 | 介绍 &解释 |
---|---|
Producer | Kafka生产者,负责往Kakfa写数据的客户端 |
Consumer | Kafka消费者,负责从Kafka读数据的客户端 |
ConsumerGroup | 消费者组,必须以消费组的形式才能消费,一个消费者组中可以包含多个消费者,任何一个消费者都必须属于某个消费者组 |
Broker | Kafka节点,每个节点叫做一个Broker,通过zk选举主节点又叫Controller |
Topic | 主题,用于区分不同的数据,实现数据分类,一个Topic可以对应多个分区 |
Partition | 分区,用于实现Kafka中Topic的分布式存储,每个分区可以分布在不同节点上,每个分区可以有多份 |
Replication | 副本,保证Kafka中分区的数据安全,副本个数小于等于节点个数,(由主节点Controller划分)两种角色:Leader,Follower |
Segment | 分区文件段,将分区中的数据按一定规则细分,加快查询性能.由两种文件组成,.log数据文件和.index索引文件 |
KV | Kafka中写入数据也是KV结构 |
Offset | 每条数据在分区中的偏移量,第N条件数据的offset = N-1,用于保证消费者按照offset顺序消费,一致性 |
3. Kafka Shell命令
3.1 Topic 创建 & 查看
- 创建Topic
kafka-topics.sh \ --create \ --topic zimo \ --partitions 3 \ --replication-factor 2 \ --bootstrap-server node1:9092,node2:9092,node3:9092
- 查看Topic列表
kafka-topics.sh \ --list -bootstrap-server node1:9092,node2:9092,node3:9092
命令 | 功能 |
---|---|
–create | 创建 |
–topic | topic名称 |
–partitions | 指定topic的分区数 |
–replication-factors | 指定分区副本个数 |
–bootstrap-server | 指定服务端地址 |
–list | 查看 |
3.2 Topic 描述 & 删除
-
描述Topic
kafka-topics.sh \ --describe \ --topic bigdata01 \ --bootstrap-server node1:9092,node2:9092,node3:9092
-
删除Topic
kafka-topics.sh \ --delete \ --topic bigdata02 \ --bootstrap-server node1:9092,node2:9092,node3:9092
命令 | 功能 |
---|---|
–describe | 描述 |
–delete | 删除 |
3.3 生产者 & 消费者
- 生产者
kafka-console-producer.sh \ --topic bigdata01 \ --broker-list node1:9092,node2:9092,node3:9092
- 消费者
kafka-console-consumer.sh \ --topic bigdata01 \ --bootstrap-server node1:9092,node2:9092,node3:9092 \ --from-beginning
命令 | 功能 |
---|---|
–from-beginning | 从每个分区的最初开始消费,默认从最新的offset进行消费 |
3.4 生产者 & 消费者(py开发)
- 生产者
from kafka import KafkaProducer """ TODO1:构建Kafka生产者客户端对象,并且指定生产者的配置 bootstrap_servers:指定的是服务端的地址 acks:应答机制,用于决定生产者写入数据到Kafka是同步还是异步 0:生产者生产一条数据写入Kafka,不用等待Kafka的回复,直接生产下一条【快,数据容易丢失】 1:生产者生产一条数据写入Kafka,等待Kafka确保这个分区的Leader已经写入,就返回一个ack,生产者收到ack就发送下一条【性能和安全做了权衡】 all/-1:生产者生产一条数据写入Kafka,等待Kafka确保这个分区的所有ISR副本都已经写入成功,再返回ack,生产者收到ack就发送下一条【安全,性能最差】 retries:重试机制,如果生产者长时间没有收到ack,就认为数据丢失,会重新发送一份数据写入,直到收到ack 应答机制+重试机制保证生产者生产数据不丢失 """ producer = KafkaProducer(bootstrap_servers=['node1:9092', 'node2:9092', 'node3:9092'], acks=1) """ TODO2:实现通过生产者对象调用生产方法将数据生产写入Kafka中 方法:send:用于将数据写入Kafka 参数: topic:指定要写入的Topic key:指定写入的Key,决定了写入数据的分区规则 value:指定写入的Value,也就是真正需要传递的数据 """ # 每次从0 - 9 中取一个值 for i in range(0, 10): # 调用方法写入数据:topic、key、value rs = producer.send(topic="bigdata01", key=f"{i}".encode("UTF-8"), value=f"itcast{i}".encode("UTF-8")) # 调用方法写入数据:topic、value # rs = producer.send(topic="bigdata01", value=f"itcast{i}".encode("UTF-8")) # 调用方法写入数据:topic、key、value、partition # rs = producer.send(topic="bigdata01", key=f"{i}".encode("UTF-8"), value=f"itcast{i}".encode("UTF-8"), partition=0) # 获取返回结果 metadata = rs.get(timeout=10) # 打印生产结果 print(f"数据:itcast{i} topic:{metadata.topic} partition:{metadata.partition} offset:{metadata.offset}") """ TODO3:生产者不是直接将数据写入Kafka,而是先放到本地一个缓存中,当缓存达到一定大小或者超过一定的时间才会真正的写入Kafka flush:强制将本地缓存的数据提交到Kafka """ producer.flush() # 如果报错 : SyntaxError: invalid syntax # 这是因为python3.7以上版本把async作为关键字了,与kafka冲突
- 消费者
from kafka import KafkaConsumer """ TODO:1-构建一个消费者对象,指定一些配置信息 topics:需要消费的Topic group_id:指定当前消费者属于哪个消费者组,消费组的id bootstrap_servers:指定服务端地址 auto_offset_reset:用于指定第一次消费从什么位置消费, earliest-最早位置,latest-最近位置 enable_auto_commit:自动提交,开启后每个消费者会自动将自己负责的分区的commitoffset提交到__consumer_offsets这个topic中 还有一个参数是提交时间间隔:自动提交是按照时间间隔自动提交,1000ms = 1s """ consumer = KafkaConsumer( "bigdata01", group_id="cg1", bootstrap_servers=['node1:9092', 'node2:9092', 'node3:9092'], auto_offset_reset='earliest', enable_auto_commit=True ) """ TODO:2-实现消费,这是一个死循环,表示源源不断的去消费,如果没有数据数据就等待,有数据就立即消费,不停的 message:表示消费到的每一条数据 topic:当前数据所属的Topic partition:当前数据属于这个Topic哪个分区 offset:当前数据属于这个分区的哪个offset key:获取数据中的key value:获取数据中的value """ # 不断消费Kafka中的每条数据 for message in consumer: # 从每条数据中获取所有信息 topic = message.topic partition = message.partition offset = message.offset key = message.key value = message.value.decode("UTF-8") # 打印每条数据的内容 print(f"Topic={topic} Partition={partition} Offset={offset} Key={key} Value={value}")
4. Kafka 的消费过程
4.1 消费者消费数据的规则
- 首次消费:
第一次消费规则【消费者组id在Kafka元数据中不存在】:由属性决定auto.offset.reset = latest | earliest latest:默认的值,从Topic每个分区的最新的位置开始消费 earliest:从最早的位置开始消费,每个分区的offset为0开始消费
- 后续消费:
第二次消费开始【消费者组已经在Kafka中存在】:根据上一次消费的每个分区Offset位置+1继续进行消费consumer offset 消费者已经消费到这个分区的offset commit offset 消费者下一个要消费的offset 关系 commit offset = consumer offset + 1
4.2 消费者如何知道消费位置
- 内存:
- 每个消费者维护自己下一次要消费的commit offset放在自己的内存中。
- 一旦消费者故障,内存数据会丢失,offset就丢失了
- 持久化处理:
- Kafka默认方案:让每个消费者将自己负责的每个分区的commit offset存储在一个 __consumer_offsets 的topic中。
- Spark/Flink默认方案:checkpoint,将自己每次消费成功以后的commit offset存储在HDFS文件中
- 自动提交:
- 根据时间周期(每1s提交记录一次)来提交下一次要消费的offset,记录在__consumer_offsets中
- 数据丢失的情况:如果刚消费,还没处理,就达到提交周期,记录了当前的offset。最后处理失败,需要重新消费处理,Kafka中已经记录消费过了,从上次消费的后面进行消费
- 数据重复的情况:如果消费并处理成功,但是没有提交offset,程序故障重启,kafka中记录的还是之前的offset,重新又消一遍数据重复问题。
- 手动提交:
- 根据处理的结果来实现手动提交,确认成功以后,再手动提交。
保证消费的不丢失(持久化处理)不重复(手动提交)精准的一次性语义
5. 负载均衡规则
5.1 生产者生产负载均衡规则
Kafka生产者生产数据的分区规则:
- 判断是否指定了某个分区,如果指定了分区,就写入指定的分区,如果没有执行2
- 判断是否自定义分区器【自定义分区规则】,如果有,就调用自定义分区器。如果没有就调用默认分区器
- 默认分区器:
- 判断是否指定了Key,没有指定Key,就随机选择一个可用的分区
- 如果指定了Key,就按照Key的MUR值取模分区个数,决定分区
5.2 消费者消费负载均衡规则
基本规则:
- 一个分区的数据只能由这个消费者组中的某一个消费者消费
- 一个消费者可以消费多个分区的数据
- 最优情况:消费者组的消费者个数等于分区个数
分配策略: 决定了多个分区如何分配给多个消费者
属性: partition.assignment.strategy =org.apache.kafka.clients.consumer.分配策略
-
RangeAssignor:范围分配,默认的分配策略
规则: 针对这个消费者组订阅的每个Topic进行分配,每个消费者消费一定的范围,如果不能均分,优先分配给编号小的.
举例: 三个消费者,消费1个Topic,Topic1有8个分区消费者 分区 consumer 1 T1【0,1,2】 consumer 2 T1【3,4,5】 consumer 3 T1【6,7】 -
RoundRobinAssignor:轮询分配,常见于Kafka2.0之前的版本
规则: 对整个消费者组订阅的所有Topic按照名称和顺序进行排序,然后轮询分配
优点: 不论多个Topic能不能均分,都可以相对均衡的将分区分配每个消费者
缺点: 如果某个消费者故障,无法恢复,轮询分配会重新对整体进行分配
举例: 三个消费者,消费1个Topic,Topic1有8个分区消费者 分区 consumer 1 T1【0,3,6】 consumer 2 T1【1,4,7】 consumer 3 T1【2,5】 -
StickyAssignor:黏性分配,2.0之后建议使用
规则: 底层根据算法来进行平均分配,均衡度要比轮询更加均衡
优点: 除了比轮询更加均衡以外,在出现故障转移的时候,每个分区保持不动,只增加多出来的分区
举例: 三个消费者,消费1个Topic,Topic1有7个分区,后消费者3挂了
consumer 3挂掉之前:消费者 分区 consumer 1 T1【0,3,6】 consumer 2 T1【1,4】 consumer 3 T1【2,5】 consumer 3挂掉之后:
消费者 分区 consumer 1 T1【0,3,6,5】 consumer 2 T1【1,4,2】
6. Kafka数据存储机制
6.1 写入过程
- 生产者生产每一条数据,将数据放入一个batch批次中,如果batch满了或者达到一定的时间,提交写入请求
- 生产者根据分区规则构建数据分区,获取对应的元数据,将请求提交给leader副本所在的Broker
- 先写入这台Broker的PageCache【页缓存】中,Kafka也用了内存机制来实现数据的快速的读写
- Kafka使用OS内存:只有操作系统故障,重启机器,内存数据才会清空
- 操作系统的后台的自动将页缓存中的数据SYNC同步到磁盘文件中:最新的Segment的.log中
- 占用操作内存达到10%或数据写入超过30s,顺序写磁盘,速度可以媲美写内存
- 其他的Follower到Leader中同步数据,Follower同步完成会返回ACK给Leader
6.2 Segment 设计
加快查询效率
- 通过将分区的数据根据Offset划分多个比较小的Segment文件
- 在检索数据时,可以根据Offset快速定位数据所在的Segment
- 加载单个Segment文件查询数据,可以提高查询效率
减少删除数据IO
- 删除数据时,Kafka以Segment为单位删除某个Segment的数据,避免一条一条删除,增加IO负载,性能较差
Segment的划分规则
-
按照时间周期生成:
#如果达到7天,重新生成一个新的Segment log.roll.hours = 168
-
按照文件大小生成:
#如果一个Segment存储达到1G,就构建一个新的Segment log.segment.bytes = 1073741824
Segment文件的命名规则
- 以当前文件存储的最小offset来命名的
6.3 读取过程
- 消费者根据Topic、Partition、Offset提交给Kafka请求读取数据
- Kafka根据元数据信息,找到对应的这个分区对应的Leader副本节点
- 请求Leader副本所在的Broker,先读PageCache(通过零拷贝机制【Zero Copy】读取PageCache)
- 如果PageCache中没有,读取Segment文件段,先根据offset找到要读取的那个Segment
- 将.log文件对应的.index文件加载到内存中,根据.index中索引的信息找到Offset在.log文件中的最近物理位置
- 读取.log,根据索引找到对应Offset的数据,读取连续的一个批次的数据
6.4 kafka读写数据快的原因
写数据:
- 先写入pagecache页缓存,操作系统的缓存
- 顺序写
读数据:
- 首先尝试读页缓存 如果没有读segment片段
- 通过索引等确定消息位置之后 通过零拷贝技术读取数据。
6.5 数据清理
属性配置:
#开启清理
log.cleaner.enable = true
#清理规则
log.cleanup.policy = delete
基于存活时间规则:最常用的方式
# 清理周期
# 单位越小,优先级越高
log.retention.ms
log.retention.minutes
log.retention.hours=168/7天
# 检查周期,要搭配清理周期来修改
log.retention.check.interval.ms=300000
# Segment文件最后修改时间如果满足条件将被删除
基于文件大小规则
#删除文件阈值,如果整个数据文件大小,超过阈值的一个segment大小,将会删除最老的segment,-1表示不使用这种规则
log.retention.bytes = -1
7. Kafka消息队列的一次性语义
7.1 一次性语义
- at-most-once: 至多一次,允许为0次,可能数据丢失
- at-least-once: 至少一次,允许多次,可能数据重复
- exactly-once: 有且仅有一次,只有1次,精准数据传输一次性语义
7.2 Kafka如何保证生产一次性语义
-
保证生产数据不丢失:ACK + 重试机制
ACK机制:acks = 0/1/all/-1
- 0:不等待ack,直接发送下一条
优点:速度快 缺点:数据易丢失 - 1:生产者将数据写入Kafka,Kafka等待这个分区Leader副本,返回ack,发送下一条
优点:性能和安全做了中和的选项。缺点:依旧存在一定概率的数据丢失的情况 - all/-1:生产者将数据写入Kafka,Kafka等待这个分区所有ISR【可用】副本同步成功,返回ack,发送下一条
优点:安全。缺点:性能比较差
重试机制: retries = 3 发送失败的重试次数
- 0:不等待ack,直接发送下一条
-
数据重复的情况 :ACK丢失
幂等性机制:一个操作被执行多次,结果是一致的f(x) = f(f(x))
在每条数据中增加一个数据id,当前这一条数据会比上一条数据id多1,Kafka会根据id进行判断是否写入过了- 如果没有写入:写入kafka
- 如果已经写入:直接返回ack
7.3 Kafka如何保证消费一次性语义
消费者是根据offset来持续消费,只要保证任何场景下消费者都能知道这个分区的commit Offset即可
commit offset每个消费者只保存在自己的内存中,如果消费者一旦故障,这个分区的commit offset就丢失。因此将每个分区的commit offset存储在一种可靠外部存储中,手动管理offset
- step1:第一次消费根据属性进行消费
- step2:消费分区数据,处理分区数据
- step3:处理成功:将处理成功的分区的Offset进行额外的存储
- step4:如果消费者故障,可以从外部存储读取上一次消费的offset向Kafka进行请求
8. Kafka分区副本概念
缩写 | 全称 | 解释 |
---|---|---|
AR | All - Replicas(所有副本) | 指的是一个分区在所有节点上的副本(AR = ISR + OSR) |
ISR | In - Sync - Replicas(可用副本) | 所有正在与Leader同步的Follower副本 |
OSR | Out - Sync - Replicas(不可用副本) | 长时间没有与Leader副本同步数据的follower副本 |
LW | low_watermark(最低消费的offset) | 一般为0,消费者能够消费到的最小的offset |
HW | high_watermark(最高消费的offset ) | 当前这个分区所有副本同步的最低位置+1,消费者能消费到的最大位置 |
LEO | Log End Offset (下一个待写的offset) | 当前已有的最新offset + 1 |
一个主题有3个分区,分区part0如下 | 在分区有3个副本,数据如下 |
---|---|
part0: Leader - node1 | 0-a, 1-b, 2-c, 3-d, 4-e, 5-f, 6-g, 7-h, 8-i |
part0:Follower - node2 | 0-a, 1-b, 2-c, 3-d, 4-e, 5-f, 6-g |
part0:Follower - node2 | 0-a, 1-b, 2-c, 3-d, 4-e, 5-f |