先贴一个Kafka大牛的博客链接 https://blog.csdn.net/u013256816/article/details/89529752
Kafka作为一个消息系统主要是用来解决应用解耦问题、异步消息问题、流量削峰问题。同时也提供了流处理等能力。
消息队列主要有两种消息模型: 点对点和发布订阅模式。
(1)点对点:每条消息只发送给一个消费者。
(2)发布订阅:多个消费者订阅主题,主题的每条记录会发布给所有的消费者。
Kafka的分区模型:
Kafka集群由多个消息代理服务器组成,发布到集群的每个消息都有一个类别,这个类别用topic来表示,我们一般根据消息的类型来设置不同的主题。Kafka集群为每个主题维护了分布式的分区日志文件,每个分区都是一个有序的、不可变的记录序列,新的消息会不断追加到日志当中。分区中设有一个偏移量能唯一的定位分区中的每一条消息
Kafka的消费模型:
推送(push)和拉取模型(pull),
push:推送模型的消息系統由消息代理记录消费者的消费状态,延时低,消息代理将消息推送到消费者后,标记这条消息已消费,这种方法无法很好的保证消息的处理语义,会造成消息丢失。并且很难适应速率不同的消费者容易出现拥塞问题或者压垮消费者问题。需要处理超时和ack问题。
pull:消费者按实际处理能力获取相应量的数据。
Kafka采用的是拉取模型,由消费者自己记录消费状态,每个消费者互相独立的顺序读取每个分区 消息,消费者控制偏移量的方式可以让消费者按照任意的顺序消费信息。
Kafka的分布式模型:
Kafka主题的分区日志都分布式的存储在集群上,同时为了故障容错,每个分区以副本的方式复制到多个代理节点上,一个座位主(leader)副本,其他作为备份(follower)副本,主副本负责所有客户端的读写操作, 备份副本从主副本中同步数据。
分区中的所有副本统称为AR(Assigned Replicas),与leader保持一定程度同步的副本叫做ISR(In-Sync Replicas), 与leader滞后过多的叫OSR(Out-of-Sync Replicas), AR = ISR + OSR.
https://www.cnblogs.com/warehouse/p/9534007.html(Kafka为何移除replica.lag.max.messages参数的讨论)
HW(High Watermark) : 标识了一个特定的消息偏移量offset,消费者只能拉取到这个offset之前的消息, 这是因为Kafka生产者的一个参数叫ack, = 1时认为leader副本收到消息就是成功的,acks = all那么则需要写入到所有的ISR副本才认为成功, acks = 0则不需要生产者不需要等待服务器的响应来确认副本收到消息。在消息写入后, 在这里 7、8偏移量的消息就是还没有完成acks参数所需的配置导致的。
Kafka的生产者和消费者相对于服务端而言都是客户端,生产者发布消息时根据消息是否有键来采用不同的分区策略,消息没有键时通过轮询的方式进行客户端负载均衡,有键时根据分区语义确保相同键的消息总是发送到同一个分区。
Kafka的消费者通过订阅主题来消费消息,每个消费者都会设置一个消费者组名称,因为生产者发布到主题的每条消息都只会发送给一个消费组当中的一个消费者,所以要实现传统的点对点模型,可以让每个消费者都拥有相同的消费者组名称,这样消息就会负载均衡到所有的消费者,如果实现发布订阅模型,每个消费者的消费者组名称不一样,这样每条消息就会广播到所有的消费者。 Kafka会将分区平衡的分配给所有消费者,并且维护一个消费者列表,当消费者组离开一个消费者或增加时都会触发再均衡操作。
Kafka的零拷贝技术
传统的读取文件数据并发送到网络的步骤如下:
(1)操作系统将数据从磁盘文件中读取到内核空间的页面缓存;
(2)应用程序将数据从内核空间读入用户空间缓冲区;
(3)应用程序将读到数据写回内核空间并放入socket缓冲区;
(4)操作系统将数据从socket缓冲区复制到网卡接口,此时数据才能通过网络发送。
零拷贝技术”只用将磁盘文件的数据复制到页面缓存中一次,然后将数据从页面缓存直接发送到网络中(发送给不同的订阅者时,都可以使用同一个页面缓存),避免了重复复制操作。
如果有10个消费者,传统方式下,数据复制次数为4*10=40次,而使用“零拷贝技术”只需要1+10=11次,一次为从磁盘复制到页面缓存,10次表示10个消费者各自读取一次页面缓存。
消息状态语义
对于推送模型的消息队列来说,当消息从消息代理发送给消费者时,消息代理会在本地记录这条消息“已消费”,但是如果消费者没能处理这条消息(网络原因、请求超时、消费者挂掉)就会导致消息丢失,解决这个问题的办法是添加应答机制,但是还会存在问题就是消费者处理完消息失败了,导致应答没有返回给消息代理,导致消息代理重复发送消息。并且消息代理需要保存每条消息的多种状态。需要在客户端和服务端做一些复杂的状态一致性保证。
Kafka采用的是拉取模型的消费状态处理,将状态交由消费者处理,消费者记录每个分区的消费进度(偏移量),所以如果10000条消息,传统需要记录10000条消息的状态,采用Kafka的分区机制的话假设10个分区,那么只要保存10个分区的消费状态。
Kafka生产者和消费者
Kafka生产者将消息直接发送给分区主副本所在的消息代理节点,不需要经过任何中间路由层,这是因为所有消息代理节点会保留一份相同的元数据,这份元数据记录了每个主题分区对应的主副本节点,生产者客户端在发送消息之前会向任意一个代理节点请求元数据来确定这条消息对应的目标节点。
整个生产者客户端由两个线程协调运行(主线程和Sender线程),主线程用KafkaProducer创建消息,通过拦截器、序列化器、分区器的作用之后缓存到消息累加器。 Sender线程从消息累加器当中获取消息发送到Kafka当中。 消息累加器主要是缓存消息让Sender线程可以批量发送消息减少网络传输资源的消耗。
消费者分区分配策略:
(1)range是默认的分配策略,它是基于topic级别粒度的,根据数字顺序排序可用分区,以字段排序消费者,然后分区数量除以消费者总数,来确定每个消费者的分区数量,如果除不尽的话就把排在前面的消费者额外多分配一个分区。
例如 有两个消费者C0和C1, 两个主题T0和T1,每个主题3个分区。 那么分区情况是这样
C0:[T0P0, T0P1, T1P0, T1P1]
C1:[T0P2, T1P2]
// 源码注释流程
// 1. 根据主题获取它的分区数量 number
// 2. 消费者按字典排序
// 3. 分区数量 / 消费者数量 = numPartitionPerConsumer
// 4. 分区数量 % 消费者数量 = consumerWithExtraPartition
/* 5. for 循环 对每个消费者进行分配分区,每个消费者的起始分区索引和结束分区索引进行计算,这就需要获
得两个变量 start 和 length,代表该消费者的起始分区索引和分区个数。
起始分区索引回忆下range策略,那么就是在这个topic内每个消费者的分区数量, 然后再考虑排在前面的消费
者可以额外获得一个分区, 所以start要考虑上一个消费者他有没有额外的这个分区*/
for (int i = 0, n = consumersForTopic.size(); i < n; i++) {
int start = numPartitionsPerConsumer * i + Math.min(i, consumersWithExtraPartition);
int length = numPartitionsPerConsumer + (i + 1 > consumersWithExtraPartition ? 0 : 1);
// 分配分区
assignment.get(consumersForTopic.get(i)).addAll(partitions.subList(start, start + length));
}
显然消费者组消费的主题的分区个数不能除尽消费者时, 这样的主题越多, 这个消费者组的分区分配越不均衡。
(2) Round Robin: 它是基于所有可用的消费者和所有可用的分区, 而不局限于topic,然后进行轮询消费者分配分区,这意味着现在同一个消费者组也可以订阅不同的topic, 对上面的Range例子 RoundRobin的策略会导致下述结果
C0:[T0P0, T0P2, T1P1]
C1:[T0P1, T1P0, T1P2]
但是对于同一个消费者组各个消费者实例订阅不同的topic 有可能会出现如下糟糕的情况(但是我不明白为什么会有这种情况出现, 因为kafka订阅主题的单位明明是消费者组, 翻了书和各个文章, 对于这里的概念都没有描述清楚, 所以暂时先搁置了):
假设有3个主题t0,t1,t2;其中,t0有1个分区p0,t1有2个分区p0和p1,t2有3个分区p0,p1和p2;有3个消费者C0,C1和C2;C0订阅t0,C1订阅t0和t1,C2订阅t0,t1和t2。结果会是这样不均衡。
C0: [t0p0]
C1: [t1p0]
C2: [t1p1, t2p0, t2p1, t2p2]
(3)StickyAssignor分配策略:
- 分区的分配要尽可能的均匀;
- 分区的分配尽可能的与上次分配的保持相同。
初始的时候表现和RoundRobin是一样的,但是如果出现消费者脱离消费者组时,RoundRobin分区策略会进行重新轮询分配,而如果是Sticky的话,他会保留原来的分区策略, 再把脱离的消费者所拥有的分区进行轮询分配给还在消费者组里的消费者,并且保持分区分配尽可能均匀。
Kafka消费者偏移量管理
首先需要明白一个理念:如果消费者一直处于运行状态,那么偏移量是没有什么用处的,只有在消费者发生崩溃或者有新的消费者加入了消费者组后触发再均衡,每个消费者可能会分配到新的分区,而不是之前处理的那个,所以消费者需要读取每个分区最后一次提交的偏移量。提交的偏移量和客户端处理的偏移量会有偏差, 导致消息重复处理或消息丢失,所以需要了解怎样去处理偏移量。
Sync()提交会导致在broker对提交请求响应之前程序会一直阻塞,会限制应用程序的吞吐量,可以通过降低提交频率来提升吞吐量, 但是这样会导致发生再均衡时重复消息的数量。
fire-and-forget:发后即忘,只负责发送消息,而不在意是否正确到达。性能好,但是可靠性差。
commitAsync:因为Kafka的Send方法本身是个异步方法,想变成同步就在后面调个.get()就可以了。
再均衡解释:在消费者关闭崩溃或者有新的消费者加入消费者组时分区进行重分配,它为消费者群组带来了高可用性和伸缩性,在再均衡期间,消费者无法读取消息,造成整个群组的一小段时间不可用。
再均衡触发原理:消费者通过向被指派为群组协调器的broker发送心跳来维持它们和群组的从属关系和它们对分区所有权的关系。只要消费者以正常时间发送心跳,就被认为是活跃的,说明它还在读取分区里的消息,消费者会在轮询消息或提交偏移量时发送心跳,如果消费者停止发送心跳的时间足够长,会话就会过期,群组协调器认为它已死亡就会触发再均衡。
Kafka Producer是线程安全的, 多个线程可以共享一个Producer,但是消费者不是, poll(方法里acquire() , release()是为了防范多线程调用, 如果出现多线程调用就抛异常)。
https://cloud.tencent.com/developer/article/1336564 偏移量参考
https://www.jianshu.com/p/e4879aed5d43 Kafka面试题
Kafka的序列化:
生产者需要用序列化器把对象转成字节数组才能通过网络发送给 Kafka,消费者再用反序列化器把Kafka收到的字节数组转成相应的对象。
Kafka的拦截器:
生产者拦截器:可以用来在消息发送前做一些准备工作,比如按照某个规则过滤某些不符合要求的消息、修改消息的内容。实现ProducerInterceptor接口,其中的onSend()方法用来实现对消息的定制化操作,onAcknowledgment()在消息被应答之前或者消息发送失败时调用, 它运行在生产者的IO线程当中,所以不能太复杂影响消息发送速度。
Kafka的幂等性实现
为了实现Producer的幂等性,Kafka引入了Producer ID(即PID)和Sequence Number。
PID。每个新的Producer在初始化的时候会被分配一个唯一的PID,这个PID对用户是不可见的。
Sequence Numbler。(对于每个PID,该Producer发送数据的每个<Topic, Partition>都对应一个从0开始单调递增的Sequence Number
Broker端在缓存中保存了这seq number,对于接收的每条消息,如果其序号比Broker缓存中序号大于1则接受它,否则将其丢弃。
这样就可以实现了消息重复提交了。但是,只能保证单个Producer对于同一个<Topic, Partition>的Exactly Once语义。不能保证同一个Producer一个topic不同的partion幂等。
问题总结:
1.CAP理论:
● 一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)
● 可用性(A):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)
● 分区容错性(P):以实际效果而言,分区相当于对通信的时限要求。系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择。
一般来说分区容错性是无法避免的, 所以我们必须在C和A之间做出抉择。 为什么C和A不能同时成立呢,就是因为回出现通信失败(也就是分区差错)。要保证一致性,那么必须锁定分区的读和写操作,等数据同步后再开放读写,在锁定期间它就不具备读写能力, 所以不可用。 反之亦然。
HBase选择了C(一致性)与P(分区可容忍性),Cassandra选择了A(可用性)与P(分区可容忍性)。
2. Kafka为什么高性能:
(1)架构层面:利用Partition实现并行处理,Kafka的每个topic拥有多个partition,且位于不同的节点,通过Partition的设计来保证横向扩展能力(讲人话就是加机器)。 通过加消费者数量的办法来加快消费速度,另外生产者也是可以通过多线程来发送消息的,通过增加生产者数量和生产者线程数量来增加写入速度。
(2) CAP理论中可用性和数据一致性的动态平衡。
看到第一个问题已经阐述过了 可用性和一致性是不能同时存在的。 Kafka每个分区的leader会维护一个In-Sync Replica,包含了所有与之同步的副本,每次ISR当中的副本复制完毕后leader才会将该消息设置为已提交,才能被consumer消费。通过这样一个设定,在追求性能与高可用性的时候可以动态调整这个ISR的数量。
注: 为什么要把这个也当做高性能的依据呢, 因为CAP理论对中间件的设计来说是必需的, 那么你怎么样减少这个设计带来的损耗实际上也相当于侧面在提高性能了。
附:Kafka副本管理—— 为何去掉replica.lag.max.messages参数
https://www.cnblogs.com/huxi2b/p/5903354.html
实现层面
(3)高效利用磁盘
Kafka将写磁盘的过程变为了顺序写来大程度的提高对磁盘的利用率,具体的实现是设计了偏移量这样一个东西,Consumer通过offset来顺序消费分区数据,且不删除所有已消费的数据,(就是指定时间或者指定日志大小)避免随机写的过程.Kafka删旧数据的话只会删除整个Segment对应的log和index文件,而不是删除部分内容。 另外拉取模型的架构特性,以及offset的设计也可以大幅度降低维护消息状态的成本, 将消息状态维护的粒度级别变成了分区个数.
(4)充分利用Page Cache
顺便学习下什么是Page Cache 和 Mmap
①Page Cache
Linux系统内核为文件系统文件设置了一个缓存,对文件读写的数据内容都缓存在这里。这个缓存成为 page cache(页缓存)。
也就是说存储的数据在I/O完成后并不回收,而一直保存在内存当中,除非内存紧张才开始回收占用的内存。(使用Page Cache的IO都是Buffer IO, 另外direct IO 可以绕过Page Cache机制).
对于系统的所有文件I/O请求,操作系统都是通过page cache机制实现的,对于操作系统而言,磁盘文件都是由一系列的数据块顺序组成,数据块的大小随系统不同而不同,x86 linux系统下是4KB(一个标准页面大小)。内核在处理文件I/O请求时,首先到page cache中查找(page cache中的每一个数据块都设置了文件以及偏移信息),如果未命中,则启动磁盘I/O,将磁盘文件中的数据块加载到page cache中的一个空闲块。之后再copy到用户缓冲区中。
很明显,同一块文件数据,在内存中保存了两份,这既占用了不必要的内存空间、冗余的拷贝、以及造成的CPU cache利用率不高。针对此问题,操作系统提供了内存映射机制(linux中mmap、windows中Filemapping)。
mmap系统调用是将硬盘文件映射到用内存中,其实就是将page cache中的页直接映射到用户进程地址空间中,从而进程可以直接访问自身地址空间的虚拟地址来访问page cache中的页,从而省去了内核空间到用户空间的copy。
在使用mmap调用的时候,系统并不是马上为其分配内存,而是添加一个虚拟内存空间到该进程当中(VMA),当程序访问到目标空间时,产生缺页中断,在缺页中断中从pageCache当中查找要访问的文件块,没有命中的话就启动磁盘IO从磁盘中加载到Page Cache,然后将文件块在pageCache中的物理页映射到进程的mmap地址空间,程序退出时这个页面还是存在的, 只有物理内存不足了内核才会主动清理page Cache,当进程调用write修改文件时,由于page cache的存在,修改并不是马上更新到磁盘,而只是暂时更新到page caches中,同时mark 目标page为dirty,当内核主动释放pagecaches时,才将更新写入磁盘(主动调用sync时,也会更新到磁盘)。
Page Cache的优点:
I/O Scheduler会将连续的小块写组装成大块的物理写从而减少IO次数。提高性能。
I/O Scheduler会尝试将一些写操作重新按顺序排好,从而减少磁头移动时间。
充分利用所有空闲内存(非JVM内存)。
读操作可以直接在Page Cache内进行。如果消费和生产速度相当,甚至不需要通过物理磁盘交换数据。
如果进程重启,JVM内的Cache会失效,但Page Cache仍然可用(因为PageCache是操作系统来管理的)。
FileChannel:mmap只有在写小数据的时候能发挥性能优势, 在大文件读写时还是要用FileChannel, 这是因为FileChannel当中利用ByteBuffer作为内存缓冲区,可以精准控制写盘的大小 (对这句话的解释还存在疑问, 我的理解是写盘的大小决定权在Page Cache)
2019.6.23:问了杜总, 理解变成了这样(bytebuffer每次写入pageCache, 4kb在pageCache当中进行对齐,然后如果一条数据是3kb, 那么它在pageCache当中占了一页的内存空间(4kb假设), 存入磁盘也是一页的空间)这样性能就没有利用起来,我个人理解这也是block io 和 stream io 效率差距的原因.
(5) 零拷贝技术
上面有提到, 另外注意Kafka的索引文件使用mmap+write 方式,data文件使用sendfile 。
(6)批处理减少网络开销
减少了网络传输的负载,提高了写盘的效率,数据压缩以及高效的序列化方式