异步通信原理
观察者模式
:::info
观察者模式
- 观察者模式(Observer),又叫"发布-订阅"模式(
**Publish-Subscribe**
)。 - 定义对象间一种"一对多"的依赖关系。每当一个对象(目标对象)改变状态,则所有依赖于它的对象(观察者对象)都会得到通知并自动更新。
:::
生产者消费者模式
:::info
1、传统模式
- 生产者直接将消息传递给指定的消费者。
- 缺点:
- 耦合性高。当生产者或消费者任意一方发生变化,都需要重写业务逻辑。
- 缺点:
2、生产者消费者模式
- 通过一个容器来解决生产者和消费者的强耦合性关系。
- 生产者和消费者彼此之间不直接通信,而是通过阻塞队列来进行通信。
:::
:::tips
生产者消费者模式的数据传递流程
- 生产者消费者模式。即:N个线程进行生产,同时N个线程进行消费。两种角色通过内存缓冲区进行通信。
- 生产者负责向缓冲区里面添加(生产)数据。
- 消费者负责向缓冲区里面取出(消费)数据。【遵循"先进先出"原则】
:::
MQ
消息传递的方式
点对点消息传递
:::tips
点对点消息传递
- 在点对点消息系统中,消息持久化到一个队列中。此时,将有一个或多个消费者消费队列中的数据。
- 但是一条数据只能被消费一次。
- 当一个消费者消费了队列中的某条数据后,该数据将从队列中删除。
- 该模式即使有多个消费者同时消费数据,也能保证数据处理的顺序。
:::
发布订阅消息传递
:::tips
发布订阅消息传递
- 在发布订阅消息系统中,消息被持久化到一个Topic中。
- 消费者可以订阅一个或多个Topic,消费者可以消费该Topic中的所有数据。
- 同一条数据可以被多个消费者消费,数据被消费后,不会立马删除。
- 在发布订阅消息系统中,消息的生产者称为"发布者",消费者称为"订阅者"。
Kafka采取拉取模式(Poll
),由自己控制消费速度,消费者可以按照任意的偏移量进行消费。
:::
Kafka
概念
:::tips
官网
概念
- Kafka是一种高吞吐量的分布式发布订阅的消息系统。
- Kafka本质上是一个消息队列(MQ:
**Message Queue**
)。主要用来处理大量数据下的消息队列,一般用来做日志处理。
消息队列的优点:
一、解耦
- 允许我们独立扩展或修改队列两边的处理过程。
- 可恢复性。即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。
二、异步
- 异步处理,不需要让流程走完才返回结果。可以将消息发送到消息队列中,然后返回结果,消费者只需要从消息队列中拉取消费处理即可。
三、削峰
- 缓冲。有助于解决生产消息和消费消息的处理速度不一致的情况。
- 高流量的时候,使用消息队列作为中间件,可以将流量的高峰保存在消息队列中,从而防止系统的高请求,减轻服务器的请求处理压力。
:::
优缺点
:::success
优点
1、高吞吐量、低延迟
- Kafka单机性能TPS
**100万/秒**
,并且生产和消费的延迟都很低。
2、可扩展性
- Kafka集群支持热扩展。
3、持久性、可靠性
- Kafka中的消息被持久化到本地磁盘中,并且支持数据备份,防止数据丢失。
4、容错性
- Kafka允许集群节点失败。(若副本数量为"
N
",则允许"N-1
"个节点失败)
5、高并发
- Kafka支持千个客户端同时读写。
6、解耦、异步、削峰
7、顺序保证
- Kafka可以保证一个Partition内的消息的有序性。
缺点
- 仅支持同一分区内消息有序,无法实现全局消息有序。
- 使用短轮询的方式,实时性取决于轮询的间隔时间。
- 由于是批量发送,数据并非真正的实时。
- 无法弹性扩容。对Partition的读写都在Partition Leader所在的Broker。如果该Broker压力过大,也无法通过新增Broker来解决问题。
- 扩展性成本高。集群中新增的Broker只会处理新Topic。如果要分担老Topic-Partition的压力,需要手动迁移Partition,这里会占用大量集群宽带。
- 消费者新加入和退出会造成整个消费组"Rebalance"。导致数据重复消费,影响消费速度。
- 消费失败,不支持重试。
- Partition过多会使得性能显著下降。Zookeeper压力大,Broker上Partition过多,让磁盘顺序写几乎退化成随机写。
:::
Kafka架构
Producer
Producer
- 消息生产者,向Kafka中发布消息的角色。
Consumer
Consumer
- 消息消费者,从Kafka中拉取消息消费的客户端。
- 消费者可以从Broker中读取数据,消费多个Topic中的数据。
Consumer Group
Consumer Group
- 消费者组。将多个消费者集中到一起去处理某个Topic的数据,可以更快的提高数据的消费能力。
- 整个消费者组共享一组偏移量(防止数据被重复读取),因为一个Topic有多个Partition。
- 消费者组,一组中存在多个消费者。消费者消费Broker中当前Topic的不同分区中的消息。
- 消息者组之间互不影响,所有的消费者都属于某个消费者组。
- 每个Consumer属于一个特定的
Consumer Group
。- 一个分区中的消息,只能由组内的一个消费者所消费。
Broker
:::success
Broker
- 一台Kafka服务器,就是一个Broker。
- 一个群集由多个Broker组成,一个Broker可以容纳多个Topic。
:::
Topic
:::success
Topic
-
主题。可以理解为一个队列。Topic将消息分类,生产者和消费者都是面向一个Topic。
-
一个Topic可以分成若干个Partition。
-
一个Partition物理上由多个Segment组成。
-
**一个Segment文件由两部分组成: ➯ **
**LogSegment文件**
**.index**
文件:表示Segment的索引文件。**.index**
文件中的元数据指向对应的**.log**
文件中Message的物理偏移量,默认大小为10M。- Partition全局的第一个Segment从0开始,后续每个Segment文件中为上一个Segment文件最后一条消息的Offset值。
- 数值大小为64位,其中20位数字字符长度,没有数字用"0"填充。
**.log**
文件:表示Segment的数据文件。**.timeindex**
文件:时间戳索引。
:::
:::success
第一个Segment文件
:::
第一个Segment从0开始
:::tips
00000000000000000000.index
:::
:::warning
00000000000000000000.log
:::
:::success
第二个Segment文件
:::
文件命名以第一个segment的最后一条消息的offset组成
:::tips
00000000000000000512.index
:::
:::warning
00000000000000000512.log
:::
:::success
第三个Segment文件
:::
文件命名以上一个segment的最后一条消息的offset组成
:::tips
00000000000000000913.index
:::
:::warning
00000000000000000913.log
:::
:::success
- 配置文件中Segment的参数有两个:
**log.segment.bytes**
:单个Segment可容纳的最大数据量,默认为**1G**
。**log.segment.ms**
:Kafka在commit
一个未写满的Segment前,所等待的时间,默认为**7天**
。
:::
:::tips
消息都具有固定的物理结构,包括:Offset(8 Bytes
)、消息体的大小(4 Bytes
)、crc32(4 Bytes
)、magic(1 Byte
)、attributes(1 Byte
)、key length(4 Bytes
)、key(K Bytes
)、payload(N Bytes
)等等字段,可以确定一条消息的大小,即读取到哪里截止。
:::
Partition
:::success
Partition
- 分区。为了实现扩展性,提高并发能力,一个非常大的Topic可以分布到多个Broker上,一个Topic可以分为多个Partition。
- 当生产者产生数据的时候,根据分配策略,选择分区,然后将消息追加到指定的分区的末尾队列中。
- Partition的分配策略:
- 如果指定Partition,则直接使用。
- 如果未指定Partition,但是指定了"Key"。通过对"Key"的"Value"进行Hash,先出一个Partition。
- 如果Partition和Key都未指定,则使用轮询的方式选出一个Partition。
- Partition的分配策略:
- 同一个Topic在不同的分区的数据是不重复的。
- 每个Partition是一个有序的队列。(分区内有序,不能保证全局有序)每条消息都会有一个自增的编号。
- 标识顺序。
- 用于标识消息的偏移量。
- 每个Partition都有自己独立的编号。
- 每个Partition中的数据使用多个Segment文件存储。
- 如果Topic有多个Partition,消费数据时就不能保证数据的顺序。如果要求严格保证消息的消费顺序的情况下,需要将Partition的数目设置为1。
:::
Replication
:::success
Replication
- 副本。为了保证集群中某个节点发生故障,节点上的Partition数据不丢失,Kafka可以正常工作。Kafka提供了副本机制,一个Topic的每个分区有若干个副本,一个Leader(1)和Follower(N)。
- Leader:负责写入和读取数据。
- Follower:只负责备份数据。
- Kafka默认副本的最大数量是10个,并且副本的数量不能大于Broker的数量。
**副本数N = 主 + 备**
:::
Leader
:::success
Leader
- 每个分区多个副本的主角色,负责写入和读取数据。
:::
Follower
:::success
Follower
- 每个分区多个副本的从角色,实时的从Leader中同步数据,并且Follower与Leader保持数据同步。
- 如果Leader发生故障,则从Follower中选举出一个新的Leader。
:::
Offset
:::success
Offset
- 偏移量。消费才消费的位置信息,可以监控数据消费到什么位置。当消费者发生故障再次重新恢复时,可以从消费位置继续消费。
- 偏移量决定读取数据的位置,不会有线程安全问题。消费者通过偏移量来决定下次读取的消息。
- 消息被消费之后,并不会被马上删除。这样多个业务就可以重复使用Kafka的消息。
- 消息最终还是会被删除的。默认删除的时间是
**7天**
(7*24小时)。
:::
Zookeeper
:::success
Zookeeper
- Kafka集群能够正常工作,需要依赖于Zookeeper,Zookeeper帮助Kafka存储和管理集群信息。
- 帮助选举。
:::
Kafka数据存储
Kafka数据分区的目的
:::tips
分区的原因?
- 方便在集群中扩展。每个Partition可以通过调整以适应它所在的机器。
- **提高并发的读写效率。**一个Topic可以由多个Partition组成,可以将Partition作为读写单位,并发读写,提高效率。
:::
:::info
分区原则:
- 将Partition发送的数据封装成一个
**ProducerRecord**
对象。**ProducerRecord**
对象包含:topic
:String类型,NotNull。partition
:int类型。timestamp
:long类型。key
:String类型。value
:String类型。headers
:Array类型。
:::
生产者数据安全
ACK机制
:::info
ACK机制原理
- 为了保证
Producer
发送的数据,能可靠的发送到指定的Topic
。 - 要求
**Topic**
的每个**Partition**
收到**Producer**
发送的数据后,都需要向**Producer**
发送**ACK**
确认收到。 - 如果Producer收到ACK消息后,就会进行下一轮的发送。否则,就会重新发送数据。
:::
关键词
AR
:::warning
**Assigned Replicas**
,用来标识副本的全集。**AR = ISR + OSR**
:::
ISR
:::warning
**In-Sync Replicas**
,加入同步队列的副本。**ISR = Leader + 没有落后太多的副本**
(**落后 < 4000条**
)
**Leader**
维护了一个动态的**In-Sync Replicas**
集合(ISR
和Leader
保持同步的Follower
集合)- 当
**ISR**
集合中的**Follower**
完成数据的同步之后,**Leader**
就会给**Follower**
发送**ACK**
。 - 如果
**Follower**
长时间未向**Leader**
同步数据,则该**Follower**
将被踢出**ISR**
集合。判断标准:- 超过10秒钟没有同步数据。
replica.lag.time.max.ms=10000
- 副节点与主节点相差超过4000条数据。
rerplica.lag.max.messages=4000
- 超过10秒钟没有同步数据。
**Leader**
发生故障后,就会从**ISR**
中选举出新的**Leader**
。- Kafka采用一种降级措施来处理:
- 选举第一个恢复的节点作为
**Leader**
提供服务,以新的数据为基准,这个措施被称为脏**Leader**
选举。
:::
- 选举第一个恢复的节点作为
- Kafka采用一种降级措施来处理:
OSR
:::warning
**Out-Sync Replicas**
,离开同步队列的副本。
:::
ACK应答机制(可靠性级别)
:::info
Kafka提供了三种可靠性级别,通过设置acks参数的值,用户可以根据可靠性和延迟的要求进行权衡。
**request.required.acks**
参数表示的是生产者生产消息时,写入到副本的严格程度,决定了生产者如何在性能和可靠性之间做取舍。
:::
**acks=1**
【默认】
:::warning
- 表示数据发送到Kafka后,需要经过Leader成功接收到数据并得到确认后,才算发送成功。
- 弊端:
- 如果Follower同步成功之前,Leader故障,将会丢失数据。
:::
- 如果Follower同步成功之前,Leader故障,将会丢失数据。
**acks=0**
:::warning
- 表示Producer将数据发送出去了就不管了,无需等待来自Broker的确认而继续发送下一条消息。
- 这种情况下数据传输效率最高,但是数据的可靠性最低。
- 弊端:
- 当Broker宕机时,有可能丢失数据。
:::
- 当Broker宕机时,有可能丢失数据。
**acks=-1**
:::warning
- 表示Producer需要等待ISR中所有的Follower都确认接收到数据后,才算发送完成。
- 这种情况下,数据绝对不会丢失,可靠性最高,但是性能最低。
- 弊端:
- 当Broker发送ACK时,如果Leader发生故障,则会造成数据重复。
:::
- 当Broker发送ACK时,如果Leader发生故障,则会造成数据重复。
Kafka故障处理
关键词
LEO
:::warning
**Log End Offset**
,当前节点的最后偏移量。每个副本最大的Offset。
:::
HW
:::warning
**HightWatermark**
,高水位线。消费者能看到的最大Offset,**ISR**
队列中最小的**LEO**
。
:::
Leader故障
- Leader发生故障后,会从ISR中选出一个新的Leader。
- 为了保证多个副本之间数据的一致性,其余的Follower会先将各自的
**log**
文件,高于HW的部分截取掉,然后从新的Leader中同步数据。
- 这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。
Follower故障
- Follower发生故障后,会被临时踢出
**ISR**
集合。- 待该Follower恢复后,Follower会先读取本地磁盘记录的上次的HW,并将log文件高于HW的部分截取掉,从HW处开始,向Leader进行数据同步操作。
- 等该Follower的LEO大于或等于该Partition的HW时(即:Follower向Leader同步完成后),就可以重新加入
**ISR**
队列中。
Exactly Once
Exactly Once
只有一次
**At Least Once + 幂等性 = Exactly Once**
- 启用幂等性,需要设置Producer的参数:
**enable.idompotence=true**
- 开启幂等性的Producer在初始化时,会被分配一个
**PID**
,发往同一个Partition的消息会附带Sequence Number
。- 而Borker端会对
<PID, Partition, SeqNumber>
做缓存,当具有相同主键的消息提交时,Broker只会持久化一条。- 但是PID重启后就会变化,同时不同的Partition也具有不同主键,所以幂等性无法保证跨分区会话的
**Exactly Once**
。- Producer不论向Server发送多少重复数据,Server端都只会持久化一条。
At Least Once
至少一次
**ACK = -1**
- 可以保证数据不丢失,但是不能保证数据不重复。
At Most Once
最多一次
**ACK = 0**
- 可以保证数据不重复,但是不能保证数据不丢失。
消费者数据安全
消费方式
:::info
- Consumer采用
**Pull**
**(拉取)**模式从Broker中读取数据。- 原因?
- Consumer采用
Push
(推送)模式,Broker给Consumer推送消息的速率是由Broker决定的,很难适应消费速率不同的消费者。 - 而
Pull
模式则可以根据Consumer的消费能力以适当的速率消费消息。
- Consumer采用
Pull
模式的缺点:Pull
模式不足之处是,如果Kafka没有数据,消费者可能会陷入循环中,一直返回空数据。- 消费者从Broker主动拉取数据,需要维护一个长轮询,消费者在消费数据时会传入一个时长参数
**timeout**
。如果当前没有数据可供消费,Consumer会等待timeout
时间之后再返回。
:::
- 原因?
分区分配策略
:::tips
将分区的所有权从一个消费者移到另一个消费者称为重新平衡(rebalance)。
分区分配的时机:
- 新增消费者。
- 减少消费者。(宕机、故障、下线…)
- 订阅的主题,新增Partition。
Kafka有三种分配策略:
- RangeAssignor,默认方式。
- RoundRobinAssignor,轮询的方式。
- StickyAssignor,粘性。
:::
RangeAssignor【默认】
:::warning
- Range方式是按照主题来分的,不会产生轮询方式消费混乱的问题。
- 原理:
**Topic的分区数 / 订阅此Topic的消费者数量**
,进行平均分配,以保证分区尽可能均匀地分配给所有的消费者。- 如果不够平均分配(不能整除),按照字典序靠前的消费者会被多分配一个分区。
- 缺陷:
- 可以明显的看到这样的分配并不均匀,如果将类似的情形扩大,有可能会出现部分消费者过载的情况。
- 如果同一个消费组内的消费者所订阅的Topic 是不相同的,那么在执行分区分配的时候就不是完全的平均分配,有可能会导致分区分配的不均匀。
- 如果某个消费者没有订阅消费组内的某个Topic,那么在分配分区的时候此消费者将分配不到这个Topic的任何分区。
:::
RoundRobinAssignor
:::warning
- RoundRobinAssignor,原理是将消费组内所有消费者以及消费者所订阅的所有Topic的Partition按照字典序排序,然后通过轮询方式将所有分区作为一个整体进行
**Hash**
排序,消费者方式逐个将分区分配给每个消费者。 - 消费者组内分配分区个数最大差别为
**1**
。是按照组来分的,可以解决多个消费者消费数据不均衡的问题。
- 缺陷:
- 当消费者组内订阅不同主题时,可能造成消费混乱。
:::
- 当消费者组内订阅不同主题时,可能造成消费混乱。
StickyAssignor
:::warning
粘性分配策略的目的:
- 分区的分配要尽可能的均匀。
- 分区的分配尽可能的与上次分配的保持相同。
- 当两者发生冲突时,第一个目标优先于第二个目标。
:::
Offset的维护
生产端的Offset
:::warning
:::
消费端的Offset
:::warning
Offset的作用
- 由于Consumer在消费过程中可能会出现断电、宕机等故障,Consumer恢复后,需要从故障前的位置继续消费。
- 所以Consumer需要实时记录自己消费到了哪个Offset,以便故障恢复后继续消费。
Kafka0.9版本
- 之前,Consumer默认将
**Offset**
保存在**Zookeeper**
中。 - 之后,Consumer默认将
**Offset**
保存在**Kafka**
一个内置的**Topic**
中,该**Topic**
为**__consumer_offsets**
。
:::