导语 | 本文推选自腾讯云开发者社区-【技思广益 · 腾讯技术人原创集】专栏。该专栏是腾讯云开发者社区为腾讯技术人与宽泛开发者打造的分享交换窗口。栏目邀约腾讯技术人分享原创的技术积淀,与宽泛开发者互启迪共成长。本文作者是腾讯后端开发工程师刘国强。
应用kafka能够对系统解耦、流量削峰、缓冲,能够实现零碎间的异步通信等。在流动追踪、消息传递、度量指标、日志记录和流式解决等场景中非常适合应用kafka。这篇文章次要介绍下kafka中的基本概念。
kafka的整体构造
下图展现了很多对于kafka的细节,临时先不必关注:
图中展现出了kafka的一些重要组件,接下来一一介绍一下。
(一)Broker
服务代理节点。其实就是一个kafka实例或服务节点,多个broker形成了kafka cluster。
(二)Producer
生产者。也就是写入音讯的一方,将音讯写入broker中。
(三)Consumer
消费者。也就是读取音讯的一方,从broker中读取音讯。
(四)Consumer Group
生产组。一个或多个消费者形成一个生产组,不同的生产组能够订阅同一个主题的音讯且互不影响。
(五)ZooKeeper
kafka应用zookeeper来治理集群的元数据,以及控制器的选举等操作。
(六)Topic
主题。每一个音讯都属于某个主题,kafka通过主题来划分音讯,是一个逻辑上的分类。
(七)Partition
分区。同一个主题下的音讯还能够持续分成多个分区,一个分区只属于一个主
题。
(八)Replica
正本。一个分区能够有多个副原本进步容灾性。
(九)Leader and Follower
分区有了多个正本,那么就须要有同步形式。kafka应用一主多从进行音讯同步,主正本提供读写的能力,而从正本不提供读写,仅仅作为主正本的备份。
(十)Offset
偏移。分区中的每一条音讯都有一个所在分区的偏移量,这个偏移量惟一标识了该音讯在以后这个分区的地位,并保障了在这个分区的程序性,不过不保障跨分区的程序性。
简略来说,作为音讯零碎的kafka实质上还是一个数据系统。既然是一个数据系统,那么就要解决两个基本问题:
- 当咱们把数据交给kafka的时候,kafka怎么存储;
- 当咱们向kafka要回数据的时候,kafka怎么返回。
音讯如何存储(逻辑层面)
目前大多数数据系统将数据存储在磁盘的格局有追加日志型以及B+树型。而kafka采纳了追加日志的格局将数据存储在磁盘上,整体的构造如下图:
追加日志的格局能够带来写性能的晋升(毕竟只须要往日志文件前面追加就能够了),然而同时对读的反对不是很敌对。为了晋升读性能,kafka须要额定的操作。
对于kafka的数据是如何存储的是一个比拟大的问题,这里先从逻辑层面开始。
(一)Topic+Partition的两层构造
kafka对音讯进行了两个层级的分类,别离是topic主题和partition分区。
将一个主题划分成多个分区的益处是不言而喻的。多个分区能够为kafka提供可伸缩性、程度扩大的能力,同时对分区进行冗余还能够进步数据可靠性。
不同的分区还能够部署在不同的broker上,加上冗余正本就进步了可靠性。
(二)Offset
对于追加日志格局,新来的数据只须要往文件开端追加即可。
对于有多个分区的主题来说,每一个音讯都有对应须要追加到的分区(分区器),这个音讯在所在的分区中都有一个惟一标识,就是offset偏移量:
这样的构造具备如下的特点:
- 分区进步了写性能,和数据可靠性;
- 音讯在分区内保障程序性,但跨分区不保障。
逻辑层面上晓得了kafka是如何存储音讯之后,再来看看作为使用者,如何写入以及读取数据。
如何写入数据
接下来从使用者的角度来看看,如何将数据写入kafka。
(一)整体流程
生产者将音讯写入kafka的整体流程如下图:
在生产端次要有两个线程:main和sender,两者通过共享内存RecordAccumulator通信。
各步骤如下:
- KafkaProducer创立音讯;
- 生产者拦截器在音讯发送之前做一些筹备工作,比方过滤不符合要求的音讯、批改音讯的内容等;
- 序列化器将音讯转换成字节数组的模式;
- 分区器计算该音讯的指标分区,而后数据会存储在RecordAccumulator中;
- 发送线程获取数据进行发送;
- 创立具体的申请;
- 如果申请过多,会将局部申请缓存起来;
- 将筹备好的申请进行发送;
- 发送到kafka集群;
- 接管响应;
- 清理数据。
在音讯累加器RecordAccumulator中来进行缓存,缓存大小通过参数buffer.memory配置,默认32MB。累加器依据分区来治理每一个音讯,其中音讯又被组织成ProducerBatch的模式(通过batch.size管制大小,默认1MB),为了进步吞吐量升高网络申请次数,ProducerBatch中可能蕴含一个或多个音讯。
当音讯不多时一个Batch可能没有填满,但不会期待太长时间,能够通过linger.ms管制等待时间,默认0。增大这个值能够进步吞吐量,然而会减少提早。
当生产音讯的速度过快导致缓存满了的时候,持续发送音讯可能会有阻塞或异样,通过参数max.block.ms管制,默认60秒。
数据达到发送线程创立好申请之后,须要对其进行重新组合,依据须要发送到的broker节点分组,每个节点就是一个连贯,每个连贯能够缓存的申请数通过max.in.flight.requests.per.connection管制,默认5。每个申请的大小通过max.reqeust.size管制,默认1MB。
(二)发送形式
音讯的发送有三种形式:
- 发后即忘(fire and forget):只管发送不论后果,性能最高,可靠性也最差;
- 同步(sync):等集群确认音讯写入胜利再返回,可靠性最高,性能差很多;
- 异步(async):指定一个callback,kafka返回响应后调用来实现异步发送的确认。
其中前两个是同步发送,后一个是异步发送。不过这里的异步发送没有提供callback的能力。
那么生产者发送音讯之后kafka怎么才算确认呢?这波及到acks参数:
- acks = 1, 默认值1,示意只有分区的leader正本胜利写入就算胜利;
- acks=0,生产者不须要期待任何服务端的响应,可能会失落数据;
- acks=-1或acks=all,须要全副处于同步状态的正本确认写入胜利,可靠性最强,性能也差。
(三)生产者重要参数
如何读取音讯
(一)生产音讯
生产模式
音讯的生产一般来说有两种模式:推模式和拉模式,而kafka中的生产是基于拉模式的。消费者通过一直地调用poll来获取音讯进行生产,基本模式如下(伪代码):
while(true) { records := consumer.Pull() for record := range records { // do something with record }}
位移提交
kafka中的音讯都有一个offset惟一标识,对于消费者来说,每生产完一个音讯须要告诉kafka,这样下次拉取音讯的时候才不会拉到已生产的数据(不思考反复生产的状况)。这个消费者已生产的音讯地位就是生产位移,比方:
假如9527以后拉取到音讯的最大偏移量且曾经生产完,那么这个消费者的生产位移就是9527,而要提交的生产位移是9528,示意下一条须要拉取的音讯的地位。
消费者一次可能拉取到多条音讯,那么就会有一个提交的形式问题。kafka默认应用的是主动提交,即五秒主动将拉到的每个分区中最大的音讯位移(相干参数是enable.auto.commit和auto.commit.interval.ms)。不过这可能导致反复生产以及数据失落的问题。
先看反复生产:
上一次提交的生产位移是9527,阐明9526及之前的音讯都曾经被生产了;以后这次pull拉取到的音讯是9527、0528和9529,因而,这次生产胜利后要提交的惟一就是9530;消费者以后正在解决音讯9528,如果此时消费者挂掉,如果此时还没有提交9530,那么9527到9529之间的音讯都会被调配到下一个消费者,导致音讯9527反复解决。
上面看一下音讯失落。还是下面的图,如果消费者刚拉取到9527到9529这三个音讯,刚好主动提交了9530,而此时消费者挂了,那么还没有解决就提交了,导致这三条音讯失落。
(二)分区调配策略
音讯在kafka的存储是分多个分区的,那么消费者音讯分区的音讯也就有一个分区调配策略。拿最开始的图来说就是上面consumer group这部分:
一共有三个分区,生产组1有四个生产组,所以有一个处于闲暇状态;生产组2有两个生产组,所以有一个生产组须要解决两个分区。
kafka消费者的分区调配策略通过参数partition.assigment.strategy来配置,有如下几种:
- Range:依照消费者的总数和分区总数进行整除运算来调配,不过是依照主题粒度的,所以可能会不平均。比方:
- RoundRobin:将生产组内所有消费者及消费者订阅的所有主题的分区依照字典序排序,而后通过轮询形式这个将分区一次调配给每个消费者。比方:
- Sticky:这个策略比较复杂,目标是分区的调配尽可能平均,以及调配要尽可能和上次保持一致。
(三)再平衡
消费者之间的协调是通过消费者协调器(ConsumerCoordinator)和组协调器(GroupCoordinator)来实现的。其中一项就是消费者的再平衡。
上面几种状况会导致消费者再平衡的产生:
- 有新的消费者退出;
- 有消费者下线;
- 有消费者被动退出;
- 生产组对应的组协调器节点发生变化;
- 订阅的主题或分区产生数量变动。
再平衡会通过上面几个步骤:
- FindCoordinator:消费者查找组协调器所在的机器,而后建设连贯;
- JoinGroup:消费者向组协调器发动退出组的申请;
- SyncGroup:组协调器将分区调配计划同步给所有的消费者;
- Heartbeat:消费者进入失常状态,开始心跳。
如何存储音讯(物理层面)
在后面介绍了逻辑层面kafka是如何存储数据的,接下来在物理层面持续。还是这张图:
(一)日志文件
kafka应用日志追加的形式来存储数据,新来的数据只有往日志文件的开端追加即可,这样的形式进步了写的性能。
然而文件也不能始终追加吧,因而,kafka中的log文件对应着多个日志分段LogSegment。
采纳分段的形式不便对其进行清理。而kafka有两种日志清理策略:
- 日志删除(Log Retention):依照肯定策略间接删除日志分段;
- 日志压缩(Log Compaction):对每个音讯的key进行整合,只保留同一个key下最新的value。
日志删除
日志删除策略有过期工夫和日志大小。默认保留工夫是7天,默认大小是1GB。
尽管默认保留工夫是7天,然而也有可能保留工夫更长。因为以后沉闷的日志分段是不会删除的,如果数据量很少,以后沉闷日志分段始终没能持续拆分,那么就不会删除。
kafka会有一个工作周期性地执行,对满足删除条件的日志进行删除。
日志压缩
日志压缩针对的是key,具备雷同key的多个value值只保留最近的一个。
同时,日志压缩会产生小文件,为了防止小文件过多,kafka在清理的时候还会对其进行合并:
(二)日志索引
日志追加进步了写的性能,然而对于读就不是很敌对了。为了进步读的性能,就须要升高一点写的性能,在读写之间做一点均衡。也就是在写的时候保护一个索引。
kafka保护了两种索引:偏移量索引和工夫戳索引。
偏移量索引
为了可能疾速定位给定音讯在日志文件中的地位,一个简略的方法就是保护一个映射,key就是音讯的偏移量,value就是在日志文件中的偏移量,这样只须要一次文件读取就能够找到对应的音讯了。
不过当音讯量微小的时候这个映射也会变很大,kafka保护的是一个稠密索引(sparse index),即不是所有的音讯都有一个对应的地位,对于没有地位映射的音讯来说,一个二分查找就能够解决了。
下图就是偏移量索引的原理:
比方要找offset是37的音讯所在的地位,先看索引中没有对应的记录,就找不大于37的最大offset是31,而后在日志中从1050开始按序查找37的音讯。
工夫戳索引
工夫戳索引就是能够依据工夫戳找到对应的偏移量。工夫戳索引是一个二级索引,现依据工夫戳找到偏移量,而后就能够应用偏移量索引找到对应的音讯地位了。原理如下图:
(三)零拷贝
kafka将数据存储在磁盘上,同时应用日志追加的形式来晋升性能。为了进一步晋升性能,kafka应用了零拷贝的技术。
零拷贝简略来说就是在内核态间接将文件内容复制到网卡设施上,缩小了内核态与用户态之间的切换。
非零拷贝:
零拷贝:
kafka的可靠性
kafka通过多正本的形式实现程度扩大,进步容灾性以及可靠性等。这里看看kafka的多正本机制。
(一)一些概念
下图展现了正本同步的一些重要概念(单个分区视角):
AR: Assigned Replicas
所有的正本统称为AR。
ISR: In-Sync Replicas
ISR是AR的一个子集,即所有和主正本放弃同步的正本汇合
OSR: Out-of-Sync Replicas
OSR也是AR的一个子集,所有和主正本未保持一致的正本汇合。所以AR=ISR+OSR。
kafka通过一些算法来断定从正本是否放弃同步,处于生效的正本也能够通过追上主副原本从新进入ISR。
LEO: Log End Offset
LEO是下一个音讯将要写入的offset偏移,在LEO之前的音讯都曾经写入日志了,每一个正本都有一个本人的LEO。
HW: High Watermark
所有和主正本放弃同步的正本中,最小的那个LEO就是HW,这个offset意味着在这之前的音讯都曾经被所有的ISR写入日志了,消费者能够拉取了,这时即便主正本生效其中一个ISR正本成为主正本音讯也不会失落。
(二)主正本HW与LEO的更新
LEO和HW都是音讯的偏移量,其中HW是所有ISR中最小的那个LEO。下图展现了音讯从生产者到主正本再同步到从正本的过程:
- 生产者将音讯发送给leader;
- leader追加音讯到日志中,并更新本人的偏移量信息,同时leader也保护着follower的信息(比方LEO等);
- follower向leader申请同步,同时携带本人的LEO等信息;
- leader读取日志,拉取保留的每个follower的信息(LEO);
- leader将数据返回给follower,同时还有本人的HW;
- follower拿到数据之后追加到本人的日志中,同时依据返回的HW更新本人的HW,办法就是取本人的LEO和HW的最小值。
从下面这个过程能够看出,一次同步过程之后leader的HW并没有增长,只有再经验一次同步,follower携带上一次更新的LEO给leader之后,leader能力更新HW,这个时候村能确认音讯的确是被所有的ISR正本写入胜利了。
leader的HW很重要,因为这个值间接决定了消费者能够生产的数据。
(三)Leader Epoch
思考上面的场景,初始时leader以保留了两条音讯,此时LEO=2,HW=1:
正在上传图片...
在sync 1中follower拉取数据,追加之后还须要再申请leader一次(sync 2)能力更新leader和follower的HW。
这样在更新HW中就会有一个间隙,当sync 1胜利之后sync 2之前follower挂掉了,那么重启之后的HW还是1,follower就会截断日志导致m2失落,而此时leader也挂掉的话这个follower就会成为leader,m2就彻底失落了(即便原来的leader重启之后也扭转不了)。
为了解决这个问题,kafka引入了leader epoch的概念,其实这就是一个版本号,在follower同步申请中不仅仅传递本人的LEO,还会带上以后的LE,当leader变更一次,这个值就会增1。
因为有了LE的信息,follower在解体重启之后就不会轻易截断日志,而是会申请最新的信息,防止了上述情况下数据失落的问题。
这篇文章通过简略的语言、简略的图,简略地形容了kafka中的一些重要概念。其实kafka是一个简单的零碎,须要更多的学习能力深刻理解kafka。
如果你是腾讯技术内容创作者,腾讯云开发者社区诚邀您退出【腾讯云原创分享打算】,支付礼品,助力职级降职。