【总结】消息队列 - Kafka

阶段性总结以备不时之需,总结有误的地方,欢迎指正交流。

1.为什么使用消息队列

异步处理、应用解耦、流量削锋

2.使用消息队列的缺点

  • 系统可用性降低 - 如果消息系统挂掉就会导致系统服务不可用。
  • 系统复杂性增加 - 需要考虑很多方面的问题:一致性问题、如何保证消息不被重复消费、如何保证消息可靠性传输等。

3.kafka相关概念

1)producer(生产者):
消息生产者,发布消息到 kafka 集群的终端或服务。producer是能够发布消息到话题的任何对象。
2)broker(服务代理):
broker是已发布的消息保存在一组服务器中,它们被称为代理(Broker)或Kafka集群。broker是kafka 集群中包含的服务器。
3)topic(话题):
topic是特定类型的消息流。消息是字节的有效负载(Payload),话题是消息的分类名或种子(Feed)名。每条发布到 kafka 集群的消息属于的类别,即 kafka 是面向 topic 的。
4)consumer(消费者):
consumer是从kafka集群中消费消息的终端或服务。可以订阅一个或多个话题,并从Broker拉数据,从而消费这些已发布的消息。
5)partition:
partition 是物理上的概念,每个 topic 包含一个或多个 partition。kafka 分配的单位是 partition。
6)Consumer group:
high-level consumer API 中,每个 consumer 都属于一个 consumer group,每条消息只能被 consumer group 中的一个 Consumer 消费,但可以被多个 consumer group 消费。
7)replica:
partition 的副本,保障 partition 的高可用。
8)leader:
replica 中的一个角色, producer 和 consumer 只跟 leader 交互。
9)follower:
replica 中的一个角色,从 leader 中复制数据。
10)controller:
kafka 集群中的其中一个服务器,用来进行 leader election 以及 各种 failover。
11)zookeeper:
kafka 通过 zookeeper 来存储集群的 meta 信息。

4.kafka可以脱离zookeeper单独使用吗?

zk是一个分布式的协调组件,早期版本的kafka用zk做meta信息存储、consumer的消费状态管理、group的管理以及offset的值管理。考虑到zk本身的一些因素以及整个架构较大概率存在单点问题,新版本中逐渐弱化了zk的作用。新的consumer使用了kafka内部的group coordination协议,也减少了对zk的依赖,但是broker依然依赖于zk,zk在kafka中还用来选举controller和检测broker是否存活等等。

5.Zookeeper基本原理&ZAP协议解释

ZooKeeper是一个分布式应用程序协调服务。提供的功能包括:配置维护、域名服务、分布式同步、组服务等。

Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议。Zab协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步)。当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和Server具有相同的系统状态。

每个Server在工作过程中有三种状态:
LOOKING:当前Server不知道leader是谁,正在搜寻
LEADING:当前Server即为选举出来的leader
FOLLOWING:leader已经选举出来,当前Server与之同步

6.什么情况下会导致kafka运行变慢?

cpu性能瓶颈、磁盘读写瓶颈、网络瓶颈

7.Kafka与传统消息的区别

Kafka持久化日志,这些日志可以被重复读取和无限期保留
Kafka是一个分布式系统,它以集群的方式运行,可以灵活伸缩,在内部通过复制数据提升容错能力和高可用性
Kafka支持实时的流式处理

8.Kafka生产策略

当我们在发送一条消息时,我们可以指定这个key ,那么 producer则会根据 key 和 partition 机制,来判断当前这条消息应该发送并存储到哪个 partition 中。如果 Kafka 中的 key 为 null 默认情况下,Kafka 采用的是 hash 取模的分区算法。如果 key 为 null 的话,则会随机的分配一个分区。这个随机是在这个参数 "metadata.max.age.ms"的时间范围内随机选择一个。对于这个时间段内,如果 key 为 null,则只会发送到唯一的分区,这个值默认情况下是 10 分钟更新一次。

9.Kafka生产者客户端中使用了几个线程来处理?分别是什么?

2个,主线程和Sender线程。主线程负责创建消息,然后通过分区器、序列化器、拦截器作用之后缓存到累加器RecordAccumulator中。Sender线程负责将RecordAccumulator中消息发送到kafka中.

10.Kafka中的分区器、序列化器、拦截器处理顺序

分区器:根据键值确定消息应该处于哪个分区中,默认情况下使用轮询分区,可以自行实现分区器接口自定义分区逻辑

序列化器:键序列化器和值序列化器,将键和值都转为二进制流 还有反序列化器 将二进制流转为指定类型数据

拦截器:两个方法 doSend()方法会在序列化之前完成 onAcknowledgement()方法在消息确认或失败时调用 可以添加多个拦截器按顺序执行

调用顺序: 拦截器doSend() -> 序列化器 -> 分区器

11.producer delivery guarantee

- At most once 消息可能会丢,但绝不会重复传输 — 读取消息,写log,处理消息。如果处理消息失败,log已经写入,则无法再次处理失败的消息。
- At least one 消息绝不会丢,但可能会重复传输 — 读取消息,处理消息,写log。如果消息处理成功,写log失败,则消息会被处理两次。
- Exactly once 每条消息肯定会被传输一次且仅传输一次 — 读取消息,同时处理消息并把result和log同时写入。这样保证result和log同时更新或同时失败。

12.kafka如何保证写入数据不丢失

  • 每个partition里面至少有一个follower在isr列表里,跟上leader的数据同步。

  • 每次写入数据的时候,都至少保证写入leader成功,同时至少还有一个isr列表里面的follower也写入成功,才算这个数据写入成功。

  • 如果不满足上面两个条件,就一直写入失败,让生产者不断重试,知道满足上面两个条件。

13.kafka的isr机制是什么

isr - In-Sync Replicas 副本同步队列 ar - Assigned Replicas 所有副本

自动给每个partition维护一个isr列表,在这个列表里一定会有leader,然后还会包含和leader保持同步的follower。如果follower因为一些原因导致不能及时从leader同步数据,那么就会从isr列表中踢出去。

14.kafka同步发送、异步发送的优缺点,如何控制?

kafka同步生产者:这个生产者写一条消息的时候,它就立马发送到某个分区去。follower还需要从leader拉取消息到本地,follower再向leader发送确认,leader再向客户端发送确认。由于这一套流程之后,客户端才能得到确认,所以很慢。
kafka异步生产者:这个生产者写一条消息的时候,先是写到某个缓冲区,这个缓冲区里的数据还没写到broker集群里的某个分区的时候,它就返回到client去了。虽然效率快,但是不能保证消息一定被发送出去了。

客户端向topic发送数据分为两种方式:
producer.type=sync 同步模式 
producer.type=async 异步模式 

15.如果leader crash时,ISR为空怎么办?

kafka在Broker端提供了一个配置参数:unclean.leader.election,这个参数有两个值:

true(默认):允许不同步副本成为leader,由于不同步副本的消息较为滞后,此时成为leader,可能会出现消息不一致的情况。

false:不允许不同步副本成为leader,此时如果发生ISR列表为空,会一直等待旧leader恢复,降低了可用性。

16.kafka unclean 配置代表啥,会对 spark streaming 消费有什么影响?

unclean.leader.election.enable 为true的话,意味着非ISR集合的broker 也可以参与选举,这样有可能就会丢数据,spark streaming在消费过程中拿到的 end offset 会突然变小,导致 spark streaming job挂掉。如果unclean.leader.election.enable参数设置为true,就有可能发生数据丢失和数据不一致的情况,Kafka的可靠性就会降低;而如果unclean.leader.election.enable参数设置为false,Kafka的可用性就会降低。

17.如何保证消息队列的高可用?(清晰画出所用mq的集群架构)

【总结】消息队列 - Kafka_第1张图片

18.如何保证消息的顺序性?

kafka只保证partition内消息有序,针对多分区的情况,需要根据实际的业务场景应用代码来实现,例如:生产者通过算法将相同业务含义的数据发送到同一个partition中;消费者采用多线程处理消息时,可以将fetch的消息放到内存队列。

19.如何实现每秒几十万的高并发写入?

  • 页缓存技术+磁盘顺序写:先写入os cache 再写入磁盘;用顺序写的方式将数据追加到文件的末尾。

  • 零拷贝技术:先从os cache中读取,没有的话再从磁盘读取,其中省去了从os cache拷贝到应用缓存,再从应用缓存拷贝到socket缓存的步骤,直接从os cache发送到网卡上面,只拷贝数据描述符到socket缓存。

  • 批量处理:合并小的请求,以流式方式进行交互。

  • 处理压缩

20.如何保证消息的可靠性传输?

生产者:0 - 不等kafka消息确认就认为成功;1 - 等待首领确认消息认为成功;all - 消息被写入所有ISR同步副本后,同步副本发送ack给leader认为成功。

消费者:如果设置了自动提交,系统会自动提交offset,可能会引起并未消费就提交了offset,导致消息丢失 — 消费者可以手动确认或者通过更改offset重新消费。

kafka:保证至少有最小同步副本的副本收到消息,才会给客户端确认。

21.如何保证消息不被重复消费?

原因:消息被消费后需要告知消息队列自己已经消费过该条消息了,但是由于网络传输等等故障,确认信息没有传送给消息队列,导致消息队列不知道自己已经消费过该消息了,再次将消息分发给其他消费者。

方案:再设计消息格式的时候设计一个traceID,只要消息被消费就将消息写入redis(需要考虑写入数据库失败之后要删除redis中的消息)— 同时数据库用唯一主键作为兜底策略。

22.kafka数据保留策略:

时间和大小不论那个满足条件,都会清空数据 —因为Kafka读取特定消息的时间复杂度为O(1),即与文件大小无关,所以这里删除过期文件与提高 Kafka 性能无关。

- 基于时间:log.retention.hours=168
- 基于大小:log.retention.bytes=1073741824

23.消费的消息一直处理失败怎么办?

MQ自动重试 —但是这样会导致消息大量积压

方案:设计一个异常队列用来存放失败的消息,设置重试次数,达到重试次数上限后将消息标志为处理失败放入异常队列,当服务恢复正常后,再重新从异常队列里面消费消息。

24.Kafka中的消息是否会丢失和重复消费?

要确定Kafka的消息是否丢失或重复,从两个方面分析入手:消息发送和消息消费。

(1)、消息发送

         Kafka消息发送有两种方式:同步(sync)和异步(async),默认是同步方式,可通过producer.type属性进行配置。Kafka通过配置request.required.acks属性来确认消息的生产:

0---类似于rpc的oneway方式,不等主同步完成就返回;

1---写完leader就返回;

-1---写完leader,且等另外2个副本同步完再返回,如果默认超时时间(10s)内也没有同步完成也会返回;

综上所述,有6种消息生产的情况,下面分情况来分析消息丢失的场景:

(1)acks=0,不和Kafka集群进行消息接收确认,则当网络异常、缓冲区满了等情况时,消息可能丢失;

(2)acks=1、同步模式下,只有Leader确认接收成功后但挂掉了,副本没有同步,数据可能丢失;

(2)、消息消费

Kafka消息消费有两个consumer接口,Low-level API和High-level API:

Low-level API:消费者自己维护offset等值,可以实现对Kafka的完全控制;

High-level API:封装了对parition和offset的管理,使用简单;

如果使用高级接口High-level API,可能存在一个问题就是当消息消费者从集群中把消息取出来、并提交了新的消息offset值后,还没来得及消费就挂掉了,那么下次再消费时之前没消费成功的消息就“诡异”的消失了;

解决办法:

        针对消息丢失:同步模式下,确认机制设置为-1,即让消息写入Leader和Follower之后再确认消息发送成功;异步模式下,为防止缓冲区满,可以在配置文件设置不限制阻塞超时时间,当缓冲区满时让生产者一直处于阻塞状态;

        针对消息重复:将消息的唯一标识保存到外部介质中,每次消费时判断是否处理过即可。

25.为什么Kafka不支持读写分离?

在 Kafka 中,生产者写入消息、消费者读取消息的操作都是与 leader 副本进行交互的,从 而实现的是一种主写主读的生产消费模型。

Kafka 并不支持主写从读,因为主写从读有 2 个很明 显的缺点:

(1)数据一致性问题。数据从主节点转到从节点必然会有一个延时的时间窗口,这个时间 窗口会导致主从节点之间的数据不一致。某一时刻,在主节点和从节点中 A 数据的值都为 X, 之后将主节点中 A 的值修改为 Y,那么在这个变更通知到从节点之前,应用读取从节点中的 A 数据的值并不为最新的 Y,由此便产生了数据不一致的问题。

(2)延时问题。类似 Redis 这种组件,数据从写入主节点到同步至从节点中的过程需要经 历网络→主节点内存→网络→从节点内存这几个阶段,整个过程会耗费一定的时间。而在 Kafka 中,主从同步会比 Redis 更加耗时,它需要经历网络→主节点内存→主节点磁盘→网络→从节 点内存→从节点磁盘这几个阶段。对延时敏感的应用而言,主写从读的功能并不太适用。

26.Kafka是如何选择Leader的?

如果某个分区的Leader不可用,Kafka就会从ISR集合中选择一个副本作为新的Leader。

27.KafkaConsumer是非线程安全的,那么怎么样实现多线程消费?

  • 在每个线程中新建一个KafkaConsumer,每个线程的consumer都不是一个对象,就不会存在线程安全了。

  • 单线程创建KafkaConsumer,要不直接把他搞个单例都可以,多个处理线程处理消息(难点在于是否要考虑消息顺序性,offset的提交方式)

28.“消费组中的消费者个数如果超过topic的分区,那么就会有消费者消费不到数据”这句话是否正确?如果不正确,那么有没有什么hack的手段?

也对也不对。
因为通过自定义分区分配策略,可以将一个consumer指定消费所有partition。
但是如果你不去指定consumer的分区,就会出现有消费者消费不到数据。
因为一个分区partition为了确保消息的顺序性,只能有一个消费者。

29.Kafka 分配 Replica 的算法如下

【总结】消息队列 - Kafka_第2张图片

随机分配一个Broker作为起始点,Partitions和Replicas分布算法如下(实际情况中partition编号从0开始):

  • 将所有N Broker和待分配的i个Partition排序。

  •  将第i个Partition分配到第(i mod n)个Broker上。

  •  将第i个Partition的第j个Replica分配到第((i + j) mod n)个Broker上。

30.kafka分区分配策略

同一个consumer group中的consumer对于一个topic中多partition的消息消费,存在着一定的分区分配策略。kafka的分区分配策略有两种:一种是 RangeAssignor 分配策略(范围分区)另一种是 RoundRobinAssignor分配策略(轮询分区)默认采用 Range 范围分区。

RangeAssignor分区:Range 范围分区策略是对每个 topic 而言的。首先对同一个 topic 里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序。假如现在有 10 个分区,3 个消费者,排序后的分区将会是0,1,2,3,4,5,6,7,8,9;消费者排序完之后将会是C1-0,C2-0,C3-0。通过 partitions数/consumer数 来决定每个消费者应该消费几个分区。如果除不尽,那么前面几个消费者将会多消费 1 个分区。 例如:如果有11 个分区的话,C1-0 将消费0,1,2,3 分区,C2-0 将消费4,5,6,7分区 C3-0 将消费 8,9,10 分区

Range分区弊端:只是针对 1 个 topic 而言,C1-0消费者多消费1个分区影响不是很大。如果有 N 多个 topic,那么针对每个 topic,消费者 C1-0 都将多消费 1 个分区,topic越多,C1-0 消费的分区会比其他消费者明显多消费 N 个分区。这就是 Range 范围分区的一个很明显的弊端了 。

RoundRobinAssignor分区:RoundRobin 轮询分区策略,是把所有的 partition 和所有的 consumer 都列出来,然后按照 hascode 进行排序,最后通过轮询算法来分配 partition 给到各个消费者。

  • 如果同一消费组内,所有的消费者订阅的消息都是相同的,那么 RoundRobin 策略的分区分配会是均匀的。

  • 如果同一消费者组内,所订阅的消息是不相同的,那么在执行分区分配的时候,就不是完全的轮询分配,有可能会导致分区分配的不均匀。如果某个消费者没有订阅消费组内的某个 topic,那么在分配分区的时候,此消费者将不会分配到这个 topic 的任何分区。

RoundRobin轮询分区的弊端:如果想要使用RoundRobin 轮询分区策略,必须满足如下两个条件: ①每个消费者订阅的主题,必须是相同的;②每个主题的消费者实例都是相同的。

31.什么时候触发分区分配策略?

① 同一个 consumer 消费者组 group.id 中,新增了消费者进来,会执行 Rebalance 操作。  

② 消费者离开当期所属的 consumer group组。比如 主动停机  或者  宕机。

③ 分区数量发生变化时(即 topic 的分区数量发生变化时)。

④ 消费者主动取消订阅 。

32.kafka如何实现延迟队列?

Kafka并没有使用JDK自带的Timer或者DelayQueue来实现延迟的功能,而是基于时间轮自定义了一个用于实现延迟功能的定时器(SystemTimer)。JDK的Timer和DelayQueue插入和删除操作的平均时间复杂度为O(nlog(n)),并不能满足Kafka的高性能要求,而基于时间轮可以将插入和删除操作的时间复杂度都降为O(1)。时间轮的应用并非Kafka独有,其应用场景还有很多,在Netty、Akka、Quartz、Zookeeper等组件中都存在时间轮的踪影。

底层使用数组实现,数组中的每个元素可以存放一个TimerTaskList对象。TimerTaskList是一个环形双向链表,在其中的链表项TimerTaskEntry中封装了真正的定时任务TimerTask.

Kafka中到底是怎么推进时间的呢?Kafka中的定时器借助了JDK中的DelayQueue来协助推进时间轮。具体做法是对于每个使用到的TimerTaskList都会加入到DelayQueue中。Kafka中的TimingWheel专门用来执行插入和删除TimerTaskEntry的操作,而DelayQueue专门负责时间推进的任务。再试想一下,DelayQueue中的第一个超时任务列表的expiration为200ms,第二个超时任务为840ms,这里获取DelayQueue的队头只需要O(1)的时间复杂度。如果采用每秒定时推进,那么获取到第一个超时的任务列表时执行的200次推进中有199次属于“空推进”,而获取到第二个超时任务时有需要执行639次“空推进”,这样会无故空耗机器的性能资源,这里采用DelayQueue来辅助以少量空间换时间,从而做到了“精准推进”。Kafka中的定时器真可谓是“知人善用”,用TimingWheel做最擅长的任务添加和删除操作,而用DelayQueue做最擅长的时间推进工作,相辅相成。

33.kafka幂等和事务

幂等和事务是Kafka 0.11.0.0版本引入的两个特性,以此来实现EOS(exactly once semantics,精确一次处理语义)。

幂等:Kafka引入了producer id(以下简称PID)和序列号(sequence number)这两个概念。每个新的生产者实例在初始化的时候都会被分配一个PID,这个PID对用户而言是完全透明的。broker端会在内存中为每个维护一个序列号。对于收到的每一条消息,只有当它的序列号的值(SN_new)比broker端中维护的对应的序列号的值(SN_old)大1(即SN_new = SN_old + 1)时,broker才会接收它。

幂等的不足:只能保证单个Producer对于同一个的幂等。不能保证同一个Producer一个topic不同的partion幂等。

事务:Kafka 的事务处理,主要是允许应用可以把消费和生产的 batch 处理(涉及多个 Partition)在一个原子单元内完成,操作要么全部完成、要么全部失败。为了实现这种机制,我们需要应用能提供一个唯一 transactionalId,即使故障恢复后也不会改变,transactionalId与PID一一对应,两者之间所不同的是transactionalId由用户显式设置,而PID是由Kafka内部分配的。

Producer端的设置:

开启enable.idempotence = true

设置Producer端参数 transactional.id

Consumer端的设置:设置isolation.level参数,目前有两个取值:

read_uncommitted:默认值表明Consumer端无论事务型Producer提交事务还是终止事务,其写入的消息都可以读取。

read_committed:表明Consumer只会读取事务型Producer成功提交事务写入的消息。注意,非事务型Producer写入的所有消息都能看到。

34.kafka存储策略

1)kafka以topic来进行消息管理,每个topic包含多个partition,每个partition对应一个逻辑log,有多个segment组成。
2)每个segment中存储多条消息(见下图),消息id由其逻辑位置决定,即从消息id可直接定位到消息的存储位置,避免id到位置的额外映射。
3)每个part在内存中对应一个index,记录每个segment中的第一条消息偏移。
4)发布者发到某个topic的消息会被均匀的分布到多个partition上(或根据用户指定的路由规则进行分布),broker收到发布消息往对应partition的最后一个segment上添加该消息,当某个segment上的消息条数达到配置值或消息发布时间超过阈值时,segment上的消息会被flush到磁盘,只有flush到磁盘上的消息订阅者才能订阅到,segment达到一定的大小后将不会再往该segment写数据,broker会创建新的segment。

【总结】消息队列 - Kafka_第3张图片

 

你可能感兴趣的:(消息中间件,学习笔记,kafka,消息队列)