声明: 1. 本文为我的个人复习总结, 并非那种从零基础开始普及知识 内容详细全面, 言辞官方的文章
2. 由于是个人总结, 所以用最精简的话语来写文章
3. 若有错误不当之处, 请指出
消息队列:
作用(优点):
-
异步处理 使用微信 进行建行卡支付时, 如果没资金不足等问题, 就先给用户响应支付成功, 后续再进行真正的建行卡扣钱操作
这样使响应速度较快, 系统可用性提高, 用户体验感更好
-
业务解耦 A系统不想直接对接下游的一堆系统BCD, 就让A发给MQ, 然后下游系统去消费MQ即可, 这样下游系统扩展了E系统, 也不会对A系统产生影响
-
流量削峰 双十一流量削峰填谷, tomcat根据自己能力去消费数据, 而不是一下子接收全部请求
-
一些业务的失败重试机制可以借助MQ完成
缺点:
-
系统的可用性降低
很多服务都依赖于MQ,一旦MQ故障,系统崩溃 -
系统变复杂了
中途经过MQ再到目的地, 没有直接到达目的地快。且MQ自身原因可能导致数据重复, 丢失, 乱序等
-
一致性问题
异步处理时, 系统A给BCD发送,只有都成功才返回成功,结果BC成功,但是D失败,但是返回页面结果是成功; 这时需要后续D的几次重试或人工处理
两种模式:
- 点对点模式
- 发布/订阅模式(Kafka用的是这个)
Kafka是微批处理, 高吞吐; RabbitMQ特点是低延时
Kafka架构:
特点:
-
ZooKeeper在Kafka集群里的作用:
- 存储 topic信息 & brokerId & controller等集群相关信息
- 负责管理集群broker的上下线
- 接收客户端的请求, 代理转发给Kafka Server;
- 维护Controller节点
- 负责leader选举, Controller里写的是谁, 谁就是leader
- 负责topic的创建工作
- 负责更新metadata cache
新版本ZooKeeper被移除了
-
自带的位移主题
_consumer_offsets
用来保存Consumer消费topic时提交的offset -
消息共享性: 消费者组之间互不影响
-
消息互斥性: 同组的消费者不能消费同一条消息, 消费者组内每个消费者负责消费不同分区的数据
-
这里的leader不是Server, 而是副本
-
在leader正常工作时, follower是不参与工作的
-
topic是逻辑上的概念, partition是物理上的概念
Kafka文件存储机制:
为防止log文件过大导致数据定位效率低下,Kafka采取了segment分片和索引机制
segment命名特点:
- 文件夹的命名规则: topic的名称+分区序号
- index和log文件命名规则: 以当前segment的第一条消息的offset命名
索引 指向对应数据文件中message的物理偏移地址, 是稀疏索引
生产者端:
为什么要分区?
- 增加存储利用率, 每个partition可以通过调整以适应它所在的机器, 这样整个集群更能适应任意大小的数据
- 提高并发读写性能, 以partition为单位进行并行读写
分区的原则:
-
指明partition时, 就用指定的
-
没有指明partition值但有key时, partition值=hash(key)%partition数量
-
没有指明partition也没有key时, 轮询调度, 第一次调用时随机生成一个整数num,
partition值=num%partition, 之后的每次都会
++num
自增
数据可靠性的保证:
可靠性 和 延迟 之间折中做权衡
AR: 分区的所有副本
-
ISR: 与leader保持同步的follower集合
-
用作ACK应答:
当ISR中的所有follower都完成数据同步之后,就会给producer发送ack
如果follower长时间未向leader同步数据,则该follower将被踢出ISR
-
用作发生故障时选举新的leader:
leader发生故障之后,就会从ISR中选举新的leader
关于踢出ISR的条件:
-
在老版本里是 延迟时间 和 延迟条数, 一个条件满足即可加入ISR
这样不好, 比如延迟条数阈值是10条, 而当一次性提交15条数据, 那么ISR中的节点此时与leader延迟条数超过10条, 会被踢出ISR; 那么等这些被踢出的机器开始同步完成数据后, 又被重新加入ISR; 这样频繁退出和加入, 浪费集群资源, 期间还要和ZooKeeper沟通通信, 浪费时间
-
所以在新版本里, 只有延迟时间这一个条件限制
-
-
ACK应答机制:
Producer端 acks参数配置:
-
acks=0(At Most Once):producer不等待broker的ack, 相当于禁用ack
-
acks=1:leader落盘成功后就返回ack
如果发送完ack后, leader立马挂掉, 那数据就丢失了
-
acks=-1(At Least Once):partition的leader和ISR里的follower全部落盘成功后才返回ack。
如果在follower同步完成后,broker发送ack之前,leader发生故障,则会造成数据重复
-
-
故障处理
高水位HW, 和LEO
-
leader故障
从ISR中选出一个新的leader,
为保证多个副本之间的数据一致性, 其余的follower会先将自己高于HW的部分截掉, 然后从新的leader同步数据
-
follower故障
follower发生故障后会被临时踢出ISR,
等到该follower恢复后,它会读取本地磁盘记录的上次的HW,并截取掉高于上次HW的部分,
然后开始同步leader的数据
等到该follower的LEO大于等于该partition的HW, 即follower追上leader之后, 就可以重新加入ISR了
这只能保证副本之间的数据一致性,并不能保证数据不丢失 或 不重复
-
Exactly Once生产:
Exactly Once(精准一次性) = At Least Once(acks=-1) + 幂等性
启用幂等性:
Producer端 设置参数enable.idompotence=true,
结合Producer端事务, 并且Broker会把<PID, partition, SeqNumber>当作联合主键(以此做幂等去重),
可以跨分区, 跨会话(进程重启)
消费者端:
consumer采用pull(拉)模式 消费数据
push & pull:
A的数据发给B
-
A push推给 B:
A更加劳累, B可以做自己的事, 被A主动通知时再处理A发来的数据
优点: 获取到数据时效性更高, 延迟低
缺点: 难适应 不同消费速率的消费者
-
B pull拉 A
B得不停每隔一小会就询问A是否有数据了, 对B来说很劳累 优点: 容易适应 不同消费速率的消费者
缺点: 因为是每隔一小会才询问, 所以时效性低, 延迟高
分区分配策略:
即确定partition由消费者组的哪个consumer来消费
- RoundRobin: 轮询策略
- Range: 记作num=partition个数/某个消费者组的消费者总个数, 然后每个消费者取num个, 最后若有剩余的分区 分配给其中一个消费者即可
可见RoundRobin比较均匀, 而Range会在除不尽的情况下 导致其中一个分区的负载重一点
offset的维护:
offset提交 参数设置:
auto.offset.reset:
- latest 接着上次消费的offset位置 继续消费
- earliest 从头开始消费
- none 自己维护提交offset
事务:
Producer事务:
能够保证消息原子性
地写入到多个分区中, 这批消息要么全部成功, 要么全部失败
全局唯一的Transaction ID可保证事务 跨分区跨会话
Consumer事务:
可以设置事务的隔离级别read_committed(只读取已提交的数据) 和 read_uncommitted(可读取未提交的数据)
如果想实现精准一次性消费, 需要自己手动维护offset(借助MySQL的事务机制, 实现 消费数据 和 提交offset 要么都成功, 要么都失败)
-
先提交offset后消费,有可能造成数据丢失,
少消费
; -
先消费后提交offset,有可能会造成数据的
重复消费
消息重复 要 好于消息丢失, 因为即便重复了, 后续可以做去重
选举策略:
-
OfflinePartition Leader(最常见):当有分区上线时(创建了新分区,或之前的下线分区重新上线)
-
ReassignPartition Leader:手动执行kafka-reassign-partitions命令时
或调用Admin的alterPartitionReassignments方法
-
PreferredReplicaPartition Leader:手动执行kafka-preferred-replica-election命令时
-
ControlledShutdownPartition Leader:当Broker正常关闭时
高吞吐/读写数据较快/高性能 的原因:
-
顺序写磁盘
-
零拷贝:
数据不用再拷贝到用户态, 即省去了用户态和内核态间的切换
数据拷贝到页缓存后, 再直接拷贝到Socket缓冲区
-
批处理 节省 网络IO和创建连接 的损耗
-
有索引结构 使用稀疏索引存放物理地址
-
partition分区可以并行读写
优化:
-
partition数量配置
-
日志保留策略设置
-
文件刷写到磁盘策略
# 每当producer写入10000条消息时,刷写数据到磁盘 log.flush.interval.messages=10000 # 每1秒钟,刷写数据到磁盘 log.flush.interval.ms=1000
-
网络配置
-
IO操作线程配置
-
异步提交(前面的消息提交得不到应答 提交不成功, 不会阻碍后面的消息提交)
-
compact, 对重复的日志进行清理, 再把小的segment文件合成大的segment文件
-
开启压缩
-
内存
-
消息大小上限设置(默认1MB)
面试题:
-
Kafka中的ISR、AR又代表什么?
ISR:与leader保持同步的follower集合
AR:分区的所有副本 -
Kafka中的HW、LEO等分别代表什么?
LEO:每个副本最后一条消息的offset
HW:一个分区中所有副本最小的HW -
如何提高Kafka里数据的消费速度?
增加 分区数量 和 消费者数量, 并且使分区数=消费者数, 一个消费者消费一个分区; 避免有些消费者负载过重 或 有些消费者消费不到数据
-
Kafka中的分区器、序列化器、拦截器之间的处理顺序是什么?
拦截器 -> 序列化器 -> 分区器 -
Kafka生产者客户端的整体结构是什么样子的?使用了几个线程来处理?分别是什么?
-
"消费组中的消费者个数如果超过topic的分区,那么就会有消费者消费不到数据"这句话是否正确?
正确
-
消费者提交消费位移时提交的是当前消费到的最新消息的offset还是offset+1?
offset+1
-
有哪些情形会造成重复消费?
先消费,后提交offset
-
那些情景会造成消息漏消费?
先提交offset,后消费
-
当你使用kafka-topics.sh创建(删除)了一个topic之后,Kafka背后会执行什么逻辑?
-
会在zookeeper中的/brokers/topics节点下创建一个新的topic节点,如:/brokers/topics/first
-
触发Controller的监听程序
-
Controller 负责topic的创建工作,并更新metadata cache
-
-
topic的分区数可不可以增加?如果可以怎么增加?如果不可以,那又是为什么?
可以增加
bin/kafka-topics.sh --zookeeper localhost:2181/kafka --alter --topic topic-config --partitions 3
-
topic的分区数可不可以减少?如果可以怎么减少?如果不可以,那又是为什么?
不可以减少,因为被删除的分区数据难以处理
-
Kafka有内部的topic吗?如果有是什么?有什么所用?
有, 是_consumer_offsets,保存消费者offset
-
Kafka分区分配的概念?
一个topic多个分区,一个消费者组多个消费者,故需要将分区分配个消费者(roundrobin、range)
-
简述Kafka的日志目录结构?
- 文件夹的命名规则: topic的名称+分区序号
- index和log文件命名规则: 以当前segment的第一条消息的offset命名
-
如果我指定了一个offset,Kafka Controller怎么查找到对应的消息?
索引存的是消息的物理地址, 并不是为每条消息都创建索引, 而是使用稀疏索引
-
Kafka Controller的作用?
-
负责leader选举, Controller里写的是谁, 谁就是leader
-
负责topic的创建工作
-
负责更新metadata cache
-
-
Kafka中有那些地方需要选举?这些地方的选举策略又有哪些?
- ISR
- Controller(先到先得)
-
失效副本是指什么?有那些应对措施?
不能及时与leader同步,暂时踢出ISR,等其追上leader之后再重新加入
-
为什么Kafka不支持读写分离?
因为主写从读有 2 个很明 显的缺点:
-
数据一致性问题
-
延时问题
-
-
为什么复制在Kafka中至关重要?
复制可以确保消息不会丢失
-
为什么需要消息队列, mysql 不能满足需求吗?
1. mysql没有消息主题 发布/订阅模式, 不能使 只订阅了主题的消费者才能看到相关消息 2. 数据只是日志信息, 没必要使用表结构 3. 消息队列是短期内临时存储,而mysql是长期存储; 实际业务中消息并不需要长期存储,只需要在短期内给消费者提供服务就行了
-
Kafka 对比 Flume
1. Kafka高吞吐, 且在消息分发方面 有主题发布/订阅 模式, 还有消费者组, 擅于分发消息, 还可以指定Offset进行消费数据 2. Flume支持丰富多样的 数据采集端和数据输出端
-
数据传输的事务定义有哪三种?
1. 至少一次 2. 至多一次 3. 精准一次
-
消费者故障,出现活锁问题如何解决?
活锁: 消费者能正常向zk发送心跳,但是不 poll 消息
产生原因: poll的时间间隔过长
解决: 配置 max.poll.interval.ms 活跃监测机制
如果客户端调用 poll 的间隔过大了, 大于配置的最大间隔,就断开当前客户端连接,让其它的消费者 过来消费
-
分布式系统中最难解决的两个问题是:
- 消息的精确一次性投递
- 消息的顺序性
-
kafka 分布式(不是单机)的情况下,如何保证消息的顺序性?
在某些业务场景下,我们需要保证对于有逻辑关联的多条MQ消息被按顺序处理:
比如对于某一条数据,正常处理顺序是
新增-删除
,最终结果是数据被删除;如果消息没有按序消费,处理顺序可能是
删除-新增
,最终数据没有被删掉解决方式有两种:
-
使用单分区
生产者使用同步提交的方式, 这样分区内是有序的
-
使用多分区
主要需要考虑如下三点:
-
生产有序
生产者只有一个, 且使用同步提交(前面提交的得到应答 提交完成了, 后面的才能继续提交)
-
在Kafka中有序
生产者在写数据的时候,可以指定一个key
比如在订单 topic 中我们可以指定订单 id 作为 key, 那么相同订单 id 的数据,一定会被分发到同一个 partition中去,而且这个 partition中的数据一定是有顺序的
-
消费有序
然后开启N个线程,每个线程分别消费一个分区的数据即可,这样就能保证顺序性。
-
-
-
kafka 如何不消费重复数据?比如扣款,我们不能重复的去扣
使用Exactly Once(精准一次性) = At Least Once(acks=-1) + 幂等性