官方文档:http://kafka.apache.org/

一、topic

主题是将记录发布到的类别或订阅源名称。Kafka中的主题始终是多用户的;也就是说,一个主题可以有零个,一个或多个消费者来订阅写入该主题的数据。在kafka中,topic是一个存储消息的逻辑概念,可以认为是一个消息集合。

kafka tool新增topic kafka topic offset_kafka tool新增topic

二、partition

每个topic可以划分多个分区(至少包含一个),同一个topic下包含的消息是不同的。每个消息在被添加到分区时,都会被分配一个offset(偏移量,下图的数字),它是消息在此分区的唯一编号,kafka通过offset保证消息在分区内的顺序,offset的顺序不跨分区,即kafka只保证在同一个分区内的消息是有序的,也可用这一特性只设置一个分区来实现有序队列。

每一条消息发送到broker节点时,会根据设置的分区规则选择将消息存储到哪一个分区,如果分区规则设置合理,那么消息会均匀分布在不同分区中,类似数据库中将数据进行了分片处理。

kafka tool新增topic kafka topic offset_缓存_02

topic是逻辑存在于kafka,而partition是以文件形式物理上存在于kafka。

例如创建一个名为test、分区为3、副本数为1(即不存在备份副本)的topic,kafka的数据目录(/tmp/kafka/-log)会存在三个目录,test-0~2,命名规则是<topic_name>-<partition_id>

sh kafka-topics.sh --create --zookeeper [zookeeper-ip]:2181 --replication-factor 1 --partitions 3 --topic test

三、消息分发

消息分发策略

消息是kafka中最基本的单元,在kafka中,一条消息由key和value组成,在发送一条消息时我们可以指定key,producer会根据key和partition机制类判断这条消息应该发送并存储到哪个分区中,我们可以根据需要进行自定义扩展partition机制。

默认分发机制

默认情况下,kafka采用的是hash取模的分区算法,如果key为null,则会随机分发到一个partition,这个随机算法是在"metadata.max.age.ms"的时间范围内随机选择一个,对于这个时间段内,如果key为null,则消息只会发送到一个分区中,这个值默认情况下10分钟更新一次。

四、消息消费

生产环境下一个topic会有多个分区,多个分区的好处在于,一方面能够对broker上的数据进行分片有效的减少消息的容量从而提升io性能,另一方面为了提高消费端的消费能力,通过多个consumer去消费一个topic达到负载均衡的目的。同时在kafka中存在consumer group的概念,也就是group id 一样的consumer,例如有三个gourp id相同的consumer对应了一个topic下三个partition,则会分别每个consumer消费一个partition。

kafka tool新增topic kafka topic offset_缓存_03

consumer和partition的数量建议
  1. 如果consumer比partition多,是浪费,因为kafka的设计是在一个partition上是不允许并发的,所以consumer数不要大于partition数
  2. 如果consumer比partition少,一个consumer会对应于多个partitions,这里主要合理分配consumer数和partition数,否则会导致partition里面的数据被取的不均匀。最好partiton数目是consumer数目的整数倍,所以partition数目很重要,比如取24,就很容易设定consumer数目
  3. 如果consumer从多个partition读到数据,不保证数据间的顺序性,kafka只保证在一个partition上数据是有序的,但多个partition,根据你读的顺序会有不同
  4. 增减consumer,broker,partition会导致rebalance,所以rebalance后consumer对应的partition会发生变化
分区分配策略
当出现一下情况的时候,kafka会进行一次分区重新分配操作,kafka分区的rebalance
  1. 同一个consumer group内增加了消费者
  2. 消费者离开当前consumer group,停机或宕机
  3. topic分区数量发生了变化
    当触发了重新负载均衡时就会涉及的分区分配策略了
  1. RangeAssignor(范围分区):范围策略是对每个主题而言的,首先对同一个主题里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序。
  2. RoundRobinAssignor(轮询分区):轮询分区策略是把所有partition和所有consumer线程都列出来,然后按照hashcode进行排序。最后通过轮询算法分配partition给消费线程。如果所有consumer实例的订阅是相同的,那么partition会均匀分布。
  3. StrickyAssignor(粘滞分区):粘滞分区主要目的是使分区的分配尽可能的均匀并且尽可能和上次分配保持相同。当两者发生冲突时, 第 一 个目的优先于第二个目的。 鉴于这两个目的, StickyAssignor分配策略的具体实现要比RangeAssignor和RoundRobinAssi gn or这两种分配策略要复杂得多。
coordinator

kafka提供了一个为coordinator的角色用于对consumer group进行管理,当consumer group第一个consumer启动时它会去和kafka sever确定谁是他们组的coordinator,之后改组的consumer都和coordinator进行协调通信。

consumer group如何确定自己的coordinator是谁呢, 消费者向kafka集群中的任意一个broker发送一个GroupCoordinatorRequest请求,服务端会返回一个负载最小的broker节点的id,并将该broker设置为coordinator

join: 表示加入到consumer group中,在这一步中,所有的成员都会向coordinator发送joinGroup的请求。一旦所有成员都发送了joinGroup请求,那么coordinator会选择一个consumer担任leader角色,并把组成员信息和订阅信息发送消费者

Synchronizing Group State

完成分区分配之后,就进入了Synchronizing Group State阶段,主要逻辑是向GroupCoordinator发送SyncGroupRequest请求,并且处理SyncGroupResponse响应,简单来说,就是leader将消费者对应的partition分配方案同步给consumer group 中的所有consumer。

每个消费者都会向coordinator发送syncgroup请求,不过只有leader节点会发送分配方案,其他消费者则不会。当leader把方案发给coordinator以后,coordinator会把结果设置到SyncGroupResponse中。这样所有成员都知道自己应该消费哪个分区。

消费端位置保存

在kafka中,提供了一个consumer_offsets_* 的一个topic,把offset信息写入到这个topic中。consumer_offsets——按保存了每个consumer group某一时刻提交的offset信息。这样就保证了consumer group知道的自己offset(消费偏移量),kafka的consumer_offsets 默认有50个分区。

五、分区副本机制

正常情况下topic会有多个partition,并且在kafka集群情况下这个多个分区会均匀分布在不同的broker下。但这些分区仍然是单点的,就有可能出现单点故障问题。所以kafka为了提高partition的可靠性提供了relirelica(副本)机制的概念,通过副本机制实行冗余备份。

每个分区可以有多个副本,并且在副本集合中会存在一个leader的副本,所有的读写请求都是由leader副本来进行处理。剩余的其他副本都做为follower副本,follower副本会从leader副本同步消息日志。这个有点类似zookeeper中leader和follower的概念,但是具体的时间方式还是有比较大的差异。所以我们可以认为,副本集会存在一主多从的关系。一般情况下一个分区的多个副本会分布在不同的kafka节点,当leader副本出现故障后,会重新选取分区副本的leader从而提供可用性。

例如:创建三个分区每个分区一组2从副本的topic

sh kafka-topics.sh --create --zookeeper [zookeeper-ip]:2181 --replication-factor 3 --partitions 3 --topic test

查看topic分区副本状态

sh kafka-topics.sh --zookeeper [zookeeper-ip]:2181 --describe --topic test
副本的leader选举

leader副本:

响应clients端读写请求的副本。Kafka副本对象都有两个重要的属性:LEO和HW。

follower副本:

被动地备份leader副本中的数据,不能响应clients端读写请求。Kafka副本对象都有两个重要的属性:LEO和HW。

ISR:

ISR表示目前“可用且消息量与leader相差不多的副本集合,这是整个副本集合的一个子集”。ISR数据保存在Zookeeper的 /brokers/topics//partitions//state节点中,怎么去理解可用和相差不多这两个词呢?具体来说,ISR集合中的副本必须满足两个条件:

  1. 副本所在节点必须维持着与zookeeper的连接
  2. 副本最后一条消息的offset与leader副本的最后一条消息的offset之间的差值不能超过指定的阈值(replica.lag.time.max.ms) replica.lag.time.max.ms:如果该follower在此时间间隔内一直没有追上过leader的所有消息,则该follower就会被剔除isr列表

follower副本把leader副本LEO之前的日志全部同步完成时,则认为follower副本已经追赶上了leader副本,这个时候会更新这个副本的lastCaughtUpTimeMs标识,kafk副本管理器会启动一个副本过期检查的定时任务,这个任务会定期检查当前时间与副本的lastCaughtUpTimeMs的差值是否大于参数replica.lag.time.max.ms 的值,如果大于,则会把这个副本踢出ISR集合

.

LEO:

日志末端位移(long end offset),记录了该副本底层日志中下一条消息的位移值。也就是说,如果LEO=10,那么表示该副本保留了10条信息,位移值范围是[0,9]。

HW:

日志水位值(Hight Water),对于同一个副本而言,其HW不会大于LEO。小于HW值的所有消息被认为是已经被副本备份的消息。

从生产者发出的 一 条消息首先会被写入分区的leader 副本,不过还需要等待ISR集合中的所有follower副本都同步完之后才能被认为已经提交,之后才会更新分区的HW, 进而消费者可以消费到这条消息。

kafka tool新增topic kafka topic offset_kafka_04

选举过程:

  1. Kafka coordinator会监听ZooKeeper的/brokers/ids节点路径,一旦发现有broker挂了,执行下面的逻辑。这里暂时先不考虑Kafka coordinator所在broker挂了的情况,Kafka coordinator挂了,各个broker会重新leader选举出新的Kafka coordinator。
  2. leader副本在该broker上的分区就要重新进行leader选举,目前的选举策略是:
    a) 优先从ISR列表中选出第一个作为leader副本,这个叫优先副本,理想情况下有限副本就是该分区的leader副本
    b) 如果ISR列表为空,则查看该topic的unclean.leader.election.enable配置。unclean.leader.election.enable:为true则代表允许选用非ISR列表的副本作为leader,那么此时就意味着数据可能丢失,为false的话,则表示不允许,直接抛出NoReplicaOnlineException异常,造成leader副本选举失败。
    c) 如果上述配置为true,则从其他副本中选出一个作为leader副本,并且ISR列表只包含该leader副本。一旦选举成功,则将选举后的leader和isr和其他副本信息写入到该分区的对应的zk路径上。同zookeeper一样副本选举也有epoch的朝代区分。

副本选举通俗表述为:

  1. 等待ISR中的任一个Replica“活”过来,并且选它作为Leader
  2. 选择第一个“活”过来的Replica(不一定是ISR中的)作为Leader

六、消息存储

首先我们需要了解的是,kafka是使用日志文件的方式来保存生产者和发送者的消息,每条消息都有一个offset值来表示它在分区中的偏移量。Kafka中存储的一般都是海量的消息数据,为了避免日志文件过大,Log并不是直接对应在一个磁盘上的日志文件,而是对应磁盘上的一个目录,这个目录的命名规则是<topic_name>_<partition_id>。一个topic的多个partition在物理磁盘上的保存路径为/tmp/kafka-logs/topic_partition,包含日志文件、索引文件和时间索引文件。

LogSegment

kafka是通过分段的方式将Log分为多个LogSegment,LogSegment是一个逻辑上的概念,一个LogSegment对应磁盘上的一个日志文件和一个索引文件,其中日志文件是用来记录消息的。索引文件是用来保存消息的索引。

segment文件命名规则:partion全局的第一个segment从0开始,后续每个segment文件名为上一个segment文件最后一条消息的offset值进行递增。数值最大为64位long大小,20位数字字符长度,没有数字用0填充。

查找算法:

  1. 根据offset的值,查找segment段中的index索引文件。由于索引文件命名是以上一个文件的最后一个offset进行命名的,所以,使用二分查找算法能够根据offset快速定位到指定的索引文件。
  2. 找到索引文件后,根据offset进行定位,找到索引文件中的符合范围的索引。(kafka采用稀疏索引的方式来提高查找性能)
  3. 得到position以后,再到对应的log文件中,从position出开始查找offset对应的消息,将每条消息的offset与目标offset进行比较,直到找到消息

查看kafka日志消息内容

sh kafka-run-class.sh kafka.tools.DumpLogSegments --files /tmp/kafka-logs/test- 0/00000000000000000000.log --print-data-log

日志清除策略:

前面提到过,日志的分段存储,一方面能够减少单个文件内容的大小,另一方面,方便kafka进行日志清理。日志的清理策略有两个:

  1. 根据消息的保留时间,当消息在kafka中保存的时间超过了指定的时间,就会触发清理过程
  2. 根据topic存储的数据大小,当topic所占的日志文件大小大于一定的阀值,则可以开始删除最旧的消息。kafka会启动一个后台线程,定期检查是否存在可以删除的消息
通过log.retention.bytes和log.retention.hours这两个参数来设置,当其中任意一个达到要求,都会执行删除,默认的保留时间是:7天。

日志压缩策略:

Kafka还提供了“日志压缩(Log Compaction)”功能,通过这个功能可以有效的减少日志文件的大小,缓解磁盘紧张的情况,在很多实际场景中,消息的key和value的值之间的对应关系是不断变化的,就像数据库中的数据会不断被修改一样,消费者只关心key对应的最新的value。因此,我们可以开启kafka的日志压缩功能,服务端会在后台启动启动Cleaner线程池,定期将相同的key进行合并,只保留最新的value值。

磁盘存储的性能优化

零拷贝:

消息从发送到落地保存,broker维护的消息日志本身就是文件目录,每个文件都是二进制保存,生产者和消费者使用相同的格式来处理。在消费者获取消息时,服务器先从硬盘读取数据到内存,然后把内存中的数据原封不动的通过socket发送给消费者。虽然这个操作描述起来很简单,但实际上经历了很多步骤。

  1. 操作系统将数据从磁盘读入到内核空间的页缓存。
  2. 应用程序将数据从内核空间读入到用户空间缓存中。
  3. 应用程序将数据写回到内核空间到socket缓存中。
  4. 操作系统将数据从socket缓冲区复制到网卡缓冲区,以便将数据经网络发出。

通过“零拷贝”技术,可以去掉这些没必要的数据复制操作,同时也会减少上下文切换次数。现代的unix操作系统提供一个优化的代码路径,用于将数据从页缓存传输到socket;在Linux中,是通过sendfile系统调用来完成的。Java提供了访问这个系统调用的方法:FileChannel.transferTo API使用sendfile,只需要一次拷贝就行,允许操作系统将数据直接从页缓存发送到网络上。所以在这个优化的路径中,只有最后一步将数据拷贝到网卡缓存中是需要的。

“零拷贝”减少了将内核空间的页缓存数据拷贝到用户空间和将用户空间数据拷贝到内核空间的socket缓存。

页缓存:

页缓存是操作系统实现的一种主要的磁盘缓存,但凡设计到缓存的,基本都是为了提升i/o性能,所以页缓存是用来减少磁盘I/O操作的。

  1. 访问磁盘的速度要远低于访问内存的速度,若从处理器L1和L2高速缓存访问则速度更快。
  2. 数据一旦被访问,就很有可能短时间内再次访问。正是由于基于访问内存比磁盘快的多,所以磁盘的内存缓存将给系统存储性能带来质的飞越。

当 一 个进程准备读取磁盘上的文件内容时, 操作系统会先查看待读取的数据所在的页(page)是否在页缓存(pagecache)中,如果存在(命中)则直接返回数据, 从而避免了对物理磁盘的I/0操作;如果没有命中, 则操作系统会向磁盘发起读取请求并将读取的数据页存入页缓存, 之后再将数据返回给进程。同样,如果 一 个进程需要将数据写入磁盘, 那么操作系统也会检测数据对应的页是否在页缓存中,如果不存在, 则会先在页缓存中添加相应的页, 最后将数据写入对应的页。 被修改过后的页也就变成了脏页, 操作系统会在合适的时间把脏页中的数据写入磁盘, 以保持数据的 一 致性Kafka中大量使用了页缓存, 这是Kafka实现高吞吐的重要因素之 一 。 虽然消息都是先被写入页缓存,然后由操作系统负责具体的刷盘任务的, 但在Kafka中同样提供了同步刷盘及间断性强制刷盘(fsync), 可以通过 log.flush.interval.messages 和 log.flush.interval.ms 参数来控制。同步刷盘能够保证消息的可靠性,避免因为宕机导致页缓存数据还未完成同步时造成的数据丢失。但是实际使用上,我们没必要去考虑这样的因素以及这种问题带来的损失,消息可靠性可以由多副本来解决,同步刷盘会带来性能的影响。 刷盘的操作由操作系统去完成即可.