Kafka架构
Kafka内部分析
Kafka架构图
通常,一个典型的Kafka集群中包含若干Producer(可以是web前端产生的Page View,或者是服务器日志,系统CPU、Memory等),若干broker(Kafka支持水平扩展,一般broker数量越多,集群吞吐率越高),若干Consumer Group,以及一个Zookeeper集群。Kafka通过Zookeeper管理集群配置,选举leader,以及在Consumer Group发生变化时进行rebalance。Producer使用push模式将消息发布到broker,Consumer使用pull模式从broker订阅并消费消息。
Kafka分布式模型
kafka分布式主要是指分区被分布在多台server(broker)上,同时每个分区都有leader和follower(不是必须),即老大和小弟的角色,这儿是老大负责处理,小弟负责同步,小弟可以变成老大,形成分布式模型。
kafka的分区日志(message)被分布在kafka集群的服务器上,每一个服务器处理数据和共享分区请求。每一个分区是被复制到一系列配置好的服务器上来进行容错。
每个分区有一个server节点来作为leader和零个或者多个server节点来作为followers。leader处理指定分区的所有读写请求,同时follower被动复制leader。如果leader失败,follwers中的一个将会自动地变成一个新的leader。每一个服务器都能作为分区的一个leader和作为其它分区的follower,因此kafka集群能被很好地平衡。kafka集群是一个去中心化的集群。官网:http://kafka.apache.org/intro.html//intro_distribution。
kafka消费的并行度就是kaka topic分区的个数,或者分区的个数决定了同一时间同一消费者组内最多可以有多少个消费者消费数据。
Kafka文件存储
Kafka文件存储结构
在kafka集群中,分单个broker和多个broker。每个broker中有多个topic,topic数量可以自己设定。在每个topic中又有1到多个partition,每个partition为一个分区。kafka分区命名规则为topic的名称+有序序号,这个序号从0开始依次增加。
每个partition中有多个segment file。创建分区时,默认会生成一个segment file,kafka默认每个segment file的大小是1G。当生产者往partition中存储数据时,内存中存不下了,就会往segment file里面刷新。在存储数据时,会先生成一个segment file,当这个segment file到1G之后,再生成第二个segment file 以此类推。每个segment file对应两个文件,分别是以.log结尾的数据文件和以.index结尾的索引文件。在服务器上,每个partition是一个目录,每个segment是分区目录下的一个文件。
每个segment file也有自己的命名规则,每个名字有20个字符,不够用0填充。每个名字从0开始命名,下一个segment file文件的名字就是,当前segment file中.log文件中第一条数据的offset。
在.index文件中,存储的是key-value格式的,key代表在.log中按顺序开始第n条消息,value代表该消息的位置偏移。但是在.index中不是对每条消息都做记录,它是每隔一些消息记录一次,避免占用太多内存。即使消息不在index记录中,在已有的记录中查找,范围也大大缩小了。
.index中存放的消息索引是一个稀疏索引列表。
topic中的partition
为什么要分区
可以想象,如果一个topic就一个分区,要是这个分区有1T数据,那么kafka就想把大文件划分到更多的目录来管理,这就是kafka所谓的分区。
分区的好处
1)方便在集群中扩展。因为一个topic由一个或者多个partition构成,而每个节点中通常可以存储多个partition,这样就方便分区存储与移动,也就增加其扩展性。同时也可以增加其topic的数据量。
2)可以提高并发。因为一个主题多个partition,而每个主题读写数据时,其实就是读写不同的partition,所以增加其并发。
单节点partition的存储分布
Kafka集群只有一个broker,默认/var/log/kafka-log为数据文件存储根目录,在Kafka broker中server.properties文件配置(参数log.dirs=/opt/module/kafka/data),例如创建2个topic名称分别为test-1、test-2, partitions数量都为partitions=4。
存储路径和目录规则为:
|–test-1-0 |–test-1-1 |–test-1-2 |–test-1-3 |–test-2-0
|–test-2-1 |–test-2-2 |–test-2-3
在Kafka文件存储中,同一个topic下有多个不同partition,每个partition为一个目录,partiton命名规则为:topic名称+分区编号(有序),第一个partiton序号从0开始,序号最大值为partitions数量减1。
多节点partition存储分布
Kafka多节点partition存储分布
数据分配策略
前面已经讲过了。
1)如果指定了partition,进入该partition。
2)如果没有指定该partition,但是指定key,通过key的字节数组信息的hashcode值和partition数求模确定partition。
3)如果都没有指定,通过轮询方式进入对应的partition。
Kafka的发送与存储策略
partition中文件存储
如图是一个partition-0的一个存储示意图。
1)每个分区一个目录,该目录中是一堆segment file(默认一个segment是1G),该目录和file都是物理存储于磁盘。
2)每个partition(目录)相当于一个巨型文件被平均分配到多个大小相等segment(段)数据文件中。但每个段segment file消息数量不一定相等,这种特性方便old segment file快速被删除。
3)每个partiton只需要支持顺序读写就行了,segment文件生命周期由服务端配置参数决定。
4)这样做的好处就是能快速删除无用文件,有效提高磁盘利用率。
kafka分区中的segemnt
通过上一节我们知道Kafka文件系统是以partition方式存储,下面深入分析partitiion中segment file组成和物理结构
索引文件与数据文件的关系
既然它们是一一对应成对出现,必然有关系。索引文件中元数据指向对应数据文件中message的物理偏移地址
比如索引文件中3,497代表:数据文件中的第三个message,它的偏移地址为497。再来看数据文件中,Message 368772表示:在全局partiton中是第368772个message。
注:segment index file采取稀疏索引存储方式,它减少索引文件大小,通过mmap可以直接内存操作,稀疏索引为数据文件的每个对应message设置一个元数据指针,它比稠密索引节省了更多的存储空间,但查找起来更耗时。
通过上面两张图,我们已经知道topic、partition、segment、.log、.index等文件的关系,下面深入看看segment相关组成原理。
1)segment file组成:由2大部分组成,分别为index file和log file(即数据文件),这2个文件一一对应,成对出现,后缀".index"和“.log”分别表示为segment索引文件、数据文件。
2)segment文件命名规则:partition全局的第一个segment从0开始,后续每个segment文件名为当前segment文件段.log文件当中第一条数据的offset。数值最大为64位long大小,20位数字字符长度,不够的左边用0填充。
3)查看segment文件内容如下:
查看.log脚本:
${KAFKA_HOME}/bin/kafka-run-class.sh kafka.tools.DumpLogSegments
–files 00000000000000000000.log --print-data-log
结果:
Starting offset: 0 offset: 0 position: 0 CreateTime: 1577994283622
isvalid: true keysize: -1 valuesize: 1 magic: 2 compresscodec: NONE
producerId: -1 producerEpoch: -1 sequence: -1 isTransactional: false
headerKeys: [] payload: a offset: 1 position: 69 CreateTime:
1577994466159 isvalid: true keysize: -1 valuesize: 1 magic: 2
compresscodec: NONE producerId: -1 producerEpoch: -1 sequence: -1
isTransactional: false headerKeys: [] payload: 1 offset: 2 position:
138 CreateTime: 1577994474463 isvalid: true keysize: -1 valuesize: 1
magic: 2 compresscodec: NONE producerId: -1 producerEpoch: -1
sequence: -1 isTransactional: false headerKeys: [] payload: 4
查看.index脚本:
${KAFKA_HOME}/bin/kafka-run-class.sh kafka.tools.DumpLogSegments
–files 00000000000000000000.index --print-data-log
结果:
Dumping 00000000000000000000.index offset: 0 position: 0
Kafka中的push和pull
一个较早问题是我们应该考虑是消费者从broker中pull数据还是broker将数据push给消费者。kakfa遵守传统设计和借鉴很多消息系统,这儿kafka选择producer向broker去push消息,并由consumer从broker pull消息。一些logging-centric system,比如Facebook的Scribe和Cloudera的Flume,采用非常不同的push模式。事实上,push模式和pull模式各有优劣。
1)push模式很难适应消费速率不同的消费者,因为消息发送速率是由broker决定的。push模式的目标是尽可能以最快速度传递消息,但是这样很容易造成consumer来不及处理消息,典型的表现就是拒绝服务以及网络拥塞。而pull模式则可以根据consumer的消费能力以适当的速率消费消息。
2)pull模式不足之处是,如果kafka没有数据,消费者可能会陷入循环中,一直返回空数据。针对这一点,Kafka的消费者在消费数据时会传入一个时长参数timeout,如果当前没有数据可供消费,consumer会等待一段时间之后再返回,这段时长即为timeout。
kafka中数据发送保障
为保证producer发送的数据,能可靠的发送到指定的topic,topic的每个partition收到producer发送的数据后,都需要向producer发送ack(acknowledgement确认收到),如果producer收到ack,就会进行下一轮的发送,否则重新发送数据
副本数据同步策略:
Kafka选择了第二种方案,原因如下:
1)同样为了容忍n台节点的故障,第一种方案需要2n+1个副本,而第二种方案只需要n+1个副本,而Kafka的每个分区都有大量的数据,第一种方案会造成大量数据的冗余。
2)虽然第二种方案的网络延迟会比较高,但网络延迟对Kafka的影响较小。
ISR:采用第二种方案之后,设想以下情景:leader收到数据,所有follower都开始同步数据,但有一个follower,因为某种故障,迟迟不能与leader进行同步,那leader就要一直等下去,直到它完成同步,才能发送ack。这个问题怎么解决呢?
Leader维护了一个动态的in-sync replica set (ISR),意为和leader保持同步的follower集合。当ISR中的follower完成数据的同步之后,leader就会给Producer发送ack。如果follower长时间未向leader同步数据,则该follower将被踢出ISR,该时间阈值由replica.lag.time.max.ms参数设定。Leader发生故障之后,就会从ISR中选举新的leader。
需要注意的是:
1)生产者发送到特定主题分区的消息是将按照发送的顺序来追加。也就是说,如果消息M1和消息M2由相同的生产者发送,并且M1是先发送的,那么M1的偏移量将比M2低,并出现在日志的前面。
2)消费者是按照存储在日志中记录顺序来查询消息。
3)对于具有n个副本的主题,我们将容忍最多N-1个服务器失败故障,从而不会丢失提交到日志的任何消息记录。
ack应答机制
对于某些不太重要的数据,对数据的可靠性要求不是很高,能够容忍数据的少量丢失,所以没必要等ISR中的follower全部接收成功。
所以Kafka为用户提供了三种可靠性级别,用户根据对可靠性和延迟的要求进行权衡,选择以下的配置
故障处理细节,如图所示。
故障机制分为了follower和leader故障,如图-31所示的,具体解释如下:
1)follower故障:follower发生故障后会被临时踢出ISR,待该follower恢复后,follower会读取本地磁盘记录的上次的HW,并将log文件高于HW的部分截取掉,从HW开始向leader进行同步。等该follower的LEO大于等于该Partition的HW,即follower追上leader之后,就可以重新加入ISR了。
2)leader故障:leader发生故障之后,会从ISR中选出一个新的leader,之后,为保证多个副本之间的数据一致性,其余的follower会先将各自的log文件高于HW的部分截掉,然后从新的leader同步数据。
注意:这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。
Kafka msg flush and retention
Flush策略
# Log Flush Policy
# Messages are immediately written to the filesystem but by default we only fsync() to sync
# the OS cache lazily. The following configurations control the flush of data to disk.
# There are a few important trade-offs here:
# 1. Durability: Unflushed data may be lost if you are not using replication.
# 2. Latency: Very large flush intervals may lead to latency spikes when the flush does occur as there will be a lot of data to flush.
# 3. Throughput: The flush is generally the most expensive operation, and a small flush interval may lead to excessive seeks.
# The settings below allow one to configure the flush policy to flush data after a period of time or
# every N messages (or both). This can be done globally and overridden on a per-topic basis.
# The number of messages to accept before forcing a flush of data to disk
// 每当每一个topic接收到10000条message的时候,就会将数据flush到磁盘
log.flush.interval.messages=10000
# The maximum amount of time a message can sit in a log before we force a flush
//每隔1s flush一次数据
log.flush.interval.ms=1000
//每隔1s flush一次数据
log.flush.interval.ms=1000
为了提供kafka的读写数据能力,首先接收数据到kafka内存,不可能无限制的保存在内存,所以必然会将数据flush到磁盘(partition的segement)文件,在flush的时候做了Durability(持久性)和Latency(延迟)和Throughput(吞吐量)的权衡与取舍。
Retention策略
# Log Retention Policy
# The following configurations control the disposal of log segments. The policy can
# be set to delete segments after a period of time, or after a given size has accumulated.
# A segment will be deleted whenever *either* of these criteria are met. Deletion always happens
# from the end of the log.
# The minimum age of a log file to be eligible for deletion due to age
// 日志最小的保留时间:7天,超过这个时间,数据可能会被清理掉
log.retention.hours=168
//log.retention.days=7
# A size-based retention policy for logs. Segments are pruned(裁剪) from the log unless the remaining
# segments drop below log.retention.bytes. Functions independently of log.retention.hours.
// segement文件如果超过log.retention.bytes的配置,将会可能被裁剪,直到小于log.retention.bytes配置
#log.retention.bytes=1073741824
# The maximum size of a log segment file. When this size is reached a new log segment will be created.
// 一个segment文件最大的大小,超过log.segment.bytes一个G,将会创建一个新的segment文件
log.segment.bytes=1073741824
# The interval at which log segments are checked to see if they can be deleted according
# to the retention policies
// 每隔5分钟,检测一次retention策略是否达到
log.retention.check.interval.ms=300000
// 每隔5分钟,检测一次retention策略是否达到
log.retention.check.interval.ms=300000
partition对应的文件,就保存在一个个的segment文件中,每一个文件默认大小是1G,但是log.retention.check.interval.ms监测频率是5分钟一次。
所以segment文件可能会超过1G,此时就会启动retion策略,将文件裁剪到log.retention.bytes配置,如果超过了log.segment.bytes=1G配置,将会创建一个新的segment文件;默认情况,segment文件会保留7天。