再谈Kafka

Kafka概念

kafka是一个分布式基于发布-订阅模式的高吞吐量、高水平拓展的消息流中间件。
【重点:高吞吐量】
kafka的基础架构中主要包含的角色有:broker、生产者、消费者;当前还包含zookeeper。
生产者:负责发送消息。
消费者:负责处理消费消息。
broker:负责存储缓冲消息,创建topic,有partition分区和replication的概念。

【在0.9版本之前,kafka创建集群,需要创建到zk中,zk中保留了消息被消费的进度或者说偏移量,0.9版本之后,kafka自己维护了一个topic,专用来存储偏移量,至于为什么要这么干,往下看】

Kafka工作流程

架构图
在这里插入图片描述
同一个消费组中的消费者不能消费同一个partition中的数据,消费者主要提高消费能力,消费组中的消费者个数建议要小于partition的个数,避免资源的空闲浪费。

工作流程图
在这里插入图片描述
每个partition都会有一个log和index文件,log文件中存储的就是生产者生成的数据,生成的数据会不断的追加到该log文件的末端,并且每一条数据都会有自己的offset,消费者都会实时记录自己消费到了哪个offset,以便出错的时候能从上次的位置继续消费,这个offset便在index中保存,offset只在分区内有序,但是在分区中无顺序的。
在log文件追加中,log文件体积会越来越大,导致数据定位效率低下,因此kafka采用了分片和索引的机制,将每个partition分成了多个segment,每个segment对应了2个文件,log和index。
在这里插入图片描述
在index中第一列数据为offset,第二列为log中文件对应的偏移量,如果要消费offset为5的数据,首先根据二分法查找到在哪个index文件中,然后通过index的offset找到log的offset,这样就可以定位到数据。

生产者和消费者

kafka的生产者分区作用就是为了提高并发,提升性能;在发送消息到broker的时候根据轮询的策略分配,比如消息1去p1,消息2去p2,消息3去p3,消息4去p1,消息5去p2。。。。

如何保证数据可靠性?
根据ack,在生产者发送消息到partition,partition收到消息之后会返回ack来确认时候接收成功,否则就会重新发送。
但是partition何时会向partition发送ack呢?
答:确保follower和leader同步完成,leader发送ack给生产者,确保就算leader挂掉,这个时候消息也不会丢失。
1、半数的follower已同步,发送ack确认
2、leader与follower全部同步完成,发送ack

kafka采用的第二种方式,但是如果一个follower突然故障,那么就一直无法同步数据,那么leader就会一直等待follower恢复,这样的做效率很低。所以kafka自己维护了一个动态列表ISR来维护这个维度的leader和follower;如果发现有follower故障,则剔除这个follower,如果leader故障,则重新选举新的leader。但是这种情况也会有问题,比如说kafka发送消息是批量的,会一瞬间leader接收完成,但是follower没有反应过来就已经被提剔除了,然后数据保存到zk中,造成频繁的操作zk、频繁的加入剔除ISR,所以为了平衡可靠性和延迟,kafka设置了一个参数acks。
acks=0:kafka只管发送数据,不接收ack,这种方式数据丢失率极高。
acks=1:kakfa发送消息后,等待leader落盘返回ack,如果leader返回ack后故障了,那么follower还没有同步,数据会丢失一部分。
acks=-1:leader和follower数据同步全部落盘,返回ack,会有重复现象,比如leader和follower落盘完成,返回ack失败,数据会重新消费;也有丢失现象,比如ISR列表只有leader一个节点,接收数据后返回ack,如果此时leader故障,数据会丢失。

如何保证消费数据的一致性?
通过HW(截取高水位):消费者能看到的最大的offset;【有点木桶效应的意思】
LEO:每个follower的最大offset
在这里插入图片描述
如何保证数据存储的一致性?
Follower故障之后,会被临时剔除ISR,待follower恢复之后,会读取本地磁盘记录的HW,并将log中高于HW的部分去除掉,从HW位置进行与leader的同步,同步完成之后,再次加入ISR.
Leader故障之后,会从ISR中重新选出一个leader,为了保证数据的一致性,所有的follower会将自己log中高于HW的部分截掉,然后所有的follower从新的leader同步数据。

幂等性的设计?
Ack设置为-1,则可以保证数据不丢失,但是会出现数据重复(at least once)
Ack设置为0,则可以保证数据不重复,但是不能保证数据不丢失(at most once)
但是如果鱼和熊掌兼得,该怎么办?这个时候就就引入了Exactl once(精准一次)
在0.11版本后,引入幂等性解决kakfa集群内部的数据重复,在0.11版本之前,在消费者处自己做处理
如果启用了幂等性,则ack默认就是-1,kafka就会为每个生产者分配一个pid,并未每条消息分配seqnumber,如果pid、partition、seqnumber三者一样,则kafka认为是重复数据,就不会落盘保存;但是如果生产者挂掉后,也会出现有数据重复的现象;所以幂等性解决在单次会话的单个分区的数据重复,但是在分区间或者跨会话的是数据重复的是无法解决的。

消费者
消费队列有两种消费方式:
push:push模式很难适应消费速率不同的消费者,因为消费者发送速率是由broker决定的,他的目标是尽可能以最快的速度传递消息,这样很容易造成消费者来不及处理消息,典型的表现就是拒绝服务及网络阻塞。
pull:如果kafka没有数据,消费者可能会陷入死循环,一直返回空数据。

分区分配策略:
一个消费组中由很多个消费者,一个topic有多个partition,所以必然会设计到partiton的分配问题,来确定是哪个partition哪个消费者来消费。
kafka提供了两种方式,一种是轮询对于topic组生效,一种是对于单个topic生效。
轮询:必须是消费组里面的消费者都订阅相同的topic。同一个消费者组里的消费者不能同时消费同一个分区。
Range:是按照单个topic来划分的,默认的分配方式。
分区策略的触发条件是在消费者个数发生变化的时候,会触发分区策略调整。

offset的维护
如果消费者在消费过程中可能出现断电宕机等故障,消费者恢复后,需要从故障前的位置继续消费,所以消费者需要实时记录自己消费哪个offset,以便故障恢复后继续消费。
Offset保存的位置有2个,一个是zk,一个是kafka,zk中由消费组、topic、partition来确定offset。确保消费者挂掉,其他的消费者还能获取这个offset。

kafka的高效读写机制

1、分布式部署
采用多节点的并行操作,每个topic下由多个分区partition,根据分区数了可以来确定并发数。
2、顺序写磁盘
相比随机写入来说,顺序写入能达到600M/s,随机写入只有100k/s,顺序写入只是省去了大量磁头寻址的时间。
3、零拷贝技术
正常情况下
程序使用read()系统调用,系统由用户态转换为内核态,磁盘中有数据用DMA的方式读取到内核缓冲区,DMA过程中CPU不需要参与数据的读写,而是DMA处理器直接将硬盘数据通过总线传输到内存中。这个是第一次上下文切换
系统由内核态转换为用户态,当程序要读取的数据已经完成写入内核缓冲区以后,程序会将数据由内核缓冲区写入用户缓冲区,这个过程需要cpu参与数据的读写。这个是第二次上下文切换
程序使用writer()系统调用,系统由用户态切换到内核态,数据从用户态缓冲区写入到网络缓冲区,这个过程需要cpu参与数据的读写。这个是第三次上下文切换
系统由内核态切换到用户态,网络缓冲区的数据通过DMA的方式传输到网卡的驱动中。这个是第四次上下文切换。

4次的用户态和内核态的切换,其中有2次需要在CPU内存中进行数据读写的过程,这种拷贝过程比较消耗资源。

零拷贝技术实际的意义是不参加任何的CPU切换,但是这样的过程需要硬件的支持才能实现。系统调用sendfile()函数发起后,磁盘数据可以通过DMA的方式将数据读取到内核缓冲区,内核缓冲区通过DMA聚合网络缓冲区,然后一次性发送到网卡种,因此它是直接在内核空间完成文件读取并转换到磁盘,没有数据到jvm这一环,所以程序无法操作该文件数据,但是它效率很高。这种情况下cpu没有参与一次的数据拷贝。

直接内存:是mmap直接将文件映射到内核空间的内存,返回一个操作地址,它解决了文件数据需要拷贝到jvm才能进行操作的窘境,而是在内核空间直接进行操作,省去了内核空间拷贝到用户空间这一步操作。直接内存是由MappedByteBuffer实现的,核心就是map()方法,该方法把文件映射到内存种,获得内存地址addr,然后通过这个addr构造MappedByteBuffrt类,以暴露各种文件的操作api。堆外内存不受MinorGC控制,只能在发生Full GC时才能实现回收,但是DirectByteBuffer改善了这个情况,它时MappedByteBuffer的子类,同时也实现了DirectBuffer接口,维护了一个Cleaner对象来完成内存回收,因此它既可以通过Full GC来完成内存回收,也可以调用Clean()方法来进行回收。直接内存的大小可以通过jvm参数来设置:-XX:MaxDirectMemorySize。并且NIO的MappedByteBuffer还有一个兄弟叫做HeapByteBuffer,它用来在堆中申请内存,本质是一个数组,受GC管控。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值