阅读 41

kafka

概念

Kafka 是一款分布式消息发布和订阅系统,具有高性能、高吞吐量的特点而被广泛应用与大数据传输场景

名称描述
Topic主题每条发布到 Kafka 集群的消息都有一个类别,这个类别被称为 Topic。
Partition分区topic物理上的分组,一个topic可以分为多个partition,每个partition是一个有序的队列。
LogSegment段每个分区又被划分为多个日志分段(LogSegment)组成,日志段是Kafka日志对象分片的最小单位;LogSegment算是一个逻辑概念,对应一个具体的日志文件(“.log”的数据文件)和两个索引文件(“.index”和“.timeindex”,分别表示偏移量索引文件和消息时间戳索引文件)组成
Offset偏移量每个partition都由一系列有序的、不可变的消息组成,这些消息被连续的追加到partition中。partition中的每个消息都有一个连续的序列号叫做offset,用于在partition内唯一标识消息(并不表示消息在磁盘上的物理位置),offset的顺序不跨分区
Message消息消息是Kafka中存储的最小最基本的单位,即为一个commit log,由一个固定长度的消息头和一个可变长度的消息体组成
Producer生产者向主题发布消息的客户端应用程序称为生产者
Consumer消费者订阅主题消息的客户端程序称为消费者
Consumer Group每个Consumer属于一个特定的Consumer Group(可为每个Consumer指定group name,若不指定group name则属于默认的group)
Broker消息中间件处理结点,一个Kafka节点就是一个broker,多个broker可以组成一个Kafka集群。
Replica副本每个分区可以有多个副本,并且在副本集合中会存在一个leader的副本,所有的读写请求都是由leader副本来进行处理。剩余的其他副本都做为follower副本,follower副本会从leader副本同步消息日志。每个 副本对象都有两个重要的属性:LEO和HW。
ISR副本同步队列可用且消息量与 leader 相差不多的副本集合,这是整个副本集合的一个子集。包含了 leader 副本和所有与 leader 副本保持同步的 follower 副本,如何判定是否与leader 同步是根据副本的属性LEO和HW
LEO日志末端位移即日志末端位移(log end offset),记录了该副本底层日志(log)中下一条消息的位移值。如果 LEO=10,那么表示该副本保存了 10 条消息,位移值范围是[0, 9]
HW水位值即水位值(HighWatermark),partition对应ISR中最小的LEO作为HW,consumer最多只能消费到HW所在的位置。小于等于HW值的所有消息都被认为是“已备份”的(replicated)。

架构

image.png 一个典型的 kafka 集群包含若干 Producer、若干个 Broker(kafka 支持水平扩展)、若干个 ConsumerGroup,以及一个 zookeeper 集群。Kafka通过Zookeeper管理集群配置,选举leader,

以及在Consumer Group发生变化时进行rebalance。Producer使用push模式将消息发布到broker,Consumer使用pull模式从broker订阅并消费消息。Producer 使用 push 模式将消息发布到 broker,

consumer 通过监听使用 pull 模式从broker 订阅并消费消息。

原理

消息存储

在Kafka文件存储中,同一个topic下有多个不同partition,每个partition为一个目录,partiton命名规则为topic名称+有序序号。

每个partion(目录)相当于一个巨型文件被平均分配到多个大小相等segment(段)数据文件中。但每个段segment file消息数量不一定相等,这种特性方便old segment file快速被删除。

每个partiton只需要支持顺序读写就行了,segment文件生命周期由服务端配置参数决定。

segment file组成:由2大部分组成,分别为index file和data file,此2个文件一一对应,成对出现,后缀”.index”和“.log”分别表示为segment索引文件、数据文件。

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

segment data file由许多message组成,一个 Message由一个固定长度的 header和一个变长的消息体 body组成,header中包含offset, body是由 N个字节构成的一个消息体,包含了具体key/value消息

#1、分区目录文件,topic为test,分区数为3
drwxr-x--- 2 root root 4096 Jul 26 19:35 test-0
drwxr-x--- 2 root root 4096 Jul 24 20:15 test-1
drwxr-x--- 2 root root 4096 Jul 24 20:15 test-2
#2、分区目录中的日志数据文件和日志索引文件,logsegment大小默认为1G
-rw-r----- 1 root root 512K Jul 24 19:51 00000000000000000000.index
-rw-r----- 1 root root 1.0G Jul 24 19:51 00000000000000000000.log
-rw-r----- 1 root root 768K Jul 24 19:51 00000000000000000000.timeindex
-rw-r----- 1 root root 512K Jul 24 20:03 00000000000022372103.index
-rw-r----- 1 root root 1.0G Jul 24 20:03 00000000000022372103.log
-rw-r----- 1 root root 768K Jul 24 20:03 00000000000022372103.timeindex

#3、dump具体日志数据内容

Dumping /tmp/kafka-logs/test-0/00000000000000000000.log
Starting offset: 0
baseOffset: 0 lastOffset: 0 count: 1 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position: 0 CreateTime: 1578819790356 size: 85 magic: 2 compresscodec: NONE crc: 2416942596 isvalid: true
| offset: 0 CreateTime: 1578819790356 keysize: -1 valuesize: 17 sequence: -1 headerKeys: [] payload: This is a meesage
baseOffset: 1 lastOffset: 1 count: 1 baseSequence: -1 lastSequence: -1 producerId: -1 producerEpoch: -1 partitionLeaderEpoch: 0 isTransactional: false isControl: false position: 85 CreateTime: 1578819797987 size: 91 magic: 2 compresscodec: NONE crc: 3617710870 isvalid: true
| offset: 1 CreateTime: 1578819797987 keysize: -1 valuesize: 23 sequence: -1 headerKeys: [] payload: This is another message

#4、dump具体index索引内容

Dumping /tmp/kafka-logs/test-0/00000000000000000000.index
offset: 0 position: 0
offset: 917 position: 17800

#5、dump具体时间戳索引文件内容

Dumping /tmp/kafka-logs/test-0/00000000000000000000.timeindex
timestamp: 0 offset: 0
Found timestamp mismatch in :/tmp/kafka-logs/test-0/00000000000000000000.timeindex
Index timestamp: 0, log timestamp: 1578819790356
Index timestamp: 917, log timestamp: 1578819720459

复制代码

副本机制

partition副本分配算法

如果我们对于一个 topic,在集群中创建多个 partition,那么 partition 是如何分布的呢?

  1. 将所有 N Broker 和待分配的 i 个 Partition 排序
  2. 将第 i 个 Partition 分配到第(i mod n)个 Broker 上
  3. 将第 i 个 Partition 的第 j 个副本分配到第((i + j) mod n)个Broker 上

image.png

副本协同机制

读写操作都只会由 leader 节点来接收和处理。follower 副本只负责同步数据以及当 leader 副本所在的 broker 挂了以后,会从 follower 副本中选取新的leader。Leader节点的切换基于 Zookeepe的Watcher机制,当 leader节点宕机的时候,其他 ISR 中的 follower节点会竞争的在 zk 中创建一个文件目录 (只会有一个 follower节点创建成功 ),创建成功的 followerf节点成为 leader节点。

写请求首先由 Leader 副本处理,之后 follower 副本会从leader 上拉取写入的消息,这个过程会有一定的延迟,导致 follower 副本中保存的消息略少于 leader 副本,但是只要没有超出阈值都可以容忍。但是如果一个 follower 副本出现异常,比如宕机、网络断开等原因长时间没有同步到消息,那这个时候,leader 就会把它踢出去。kafka 通过 ISR集合来维护一个分区副本信息

ISR 集合中的副本必须满足两个条件

  1. 副本所在节点必须维持着与 zookeeper 的连接(ISR 数 据 保 存 在 Zookeeper 的/brokers/topics/<topic>/partitions/<partitionId>/state 节点中)
  2. 副本最后一条消息的 offset 与 leader 副本的最后一条消息的offset之间的差值不能超过指定的阈值(replica.lag.time.max.ms)

replica.lag.time.max.ms:如果该 follower 在此时间间隔内一直没有追上过 leader 的所有消息,则该 follower 就会被剔除 isr 列表

每个 replica 都有 HW,leader 和 follower 各自维护更新自己的 HW 的状态。一条消息只有被 ISR 里的所有 Follower 都从 Leader 复制过去才会被认为已提交。这样就避免了部分数据被写进了Leader,还没来得及被任何 Follower 复制就宕机了,而造成数据丢失(Consumer 无法消费这些数据)。而对于Producer 而言,它可以选择是否等待消息 commit,这可以通过 acks 来设置。

image.png

数据同步过程

Producer 在 发 布 消 息 到 某 个 Partition 时 , 先 通 过ZooKeeper 找到该 Partition 的 Leader 【 get/brokers/topics/<topic>/partitions/2/state】,然后无论该Topic 的 Replication Factor 为多少,Producer 只将该消息发送到该 Partition 的Leader。

Leader 会将该消息写入其本地 Log。每个 Follower都从 Leader pull 数据。这种方式上,Follower 存储的数据顺序与 Leader 保持一致。Follower 在收到该消息并写入其Log 后,向 Leader 发送 ACK。

一旦 Leader 收到了 ISR 中的所有 Replica 的 ACK,该消息就被认为已经 commit 了,Leader 将增加 HW(HighWatermark)并且向 Producer 发送ACK。

实际数据同步过程更为复杂,有兴趣可以详细了解。 image.png

ISR设计原理

在所有的分布式存储中,冗余备份是一种常见的设计方式,而常用的模式有同步复制和异步复制,按照 kafka 这个副本模型来说

如果采用同步复制,那么需要要求所有能工作的 Follower 副本都复制完,这条消息才会被认为提交成功,一旦有一个follower 副本出现故障,就会导致 HW 无法完成递增,消息就无法提交,消费者就获取不到消息。这种情况下,故障的Follower 副本会拖慢整个系统的性能,设置导致系统不可用

如果采用异步复制,leader 副本收到生产者推送的消息后,就认为次消息提交成功。follower 副本则异步从 leader 副本同步。这种设计虽然避免了同步复制的问题,但是假设所有follower 副本的同步速度都比较慢他们保存的消息量远远落后于 leader 副本。而此时 leader 副本所在的 broker 突然宕机,则会重新选举新的 leader 副本,而新的 leader 副本中没有原来 leader 副本的消息。这就出现了消息的丢失。

kafka 权衡了同步和异步的两种策略,采用 ISR 集合,巧妙解决了两种方案的缺陷:当 follower 副本延迟过高,leader 副本则会把该 follower 副本提出 ISR 集合,消息依然可以快速提交。当 leader 副本所在的 broker 突然宕机,会优先将 ISR 集合中follower 副本选举为 leader,新 leader 副本包含了 HW 之前的全部消息,这样就避免了消息的丢失。

消息分发

从创建一个ProducerRecord 对象开始,ProducerRecord 是 Kafka 中的一个核心类,它代表了一组 Kafka 需要发送的 key/value 键值对,它由记录要发送到的主题名称(Topic Name),可选的分区号(Partition Number)以及可选的键值对构成。

在发送 ProducerRecord 时,我们需要将键值对对象由序列化器转换为字节数组,这样它们才能够在网络上传输。然后消息到达了分区器。如果发送过程中指定了有效的分区号,那么在发送记录时将使用该分区。

如果发送过程中未指定分区,则将使用key 的 hash 函数映射指定一个分区。如果发送的过程中既没有分区号也没有,则将以循环的方式分配一个分区。选好分区后,生产者就知道向哪个主题和分区发送数据了。

ProducerRecord 还有关联的时间戳,如果用户没有提供时间戳,那么生产者将会在记录中使用当前的时间作为时间戳。Kafka 最终使用的时间戳取决于 topic 主题配置的时间戳类型。

如果将主题配置为使用 CreateTime,则生产者记录中的时间戳将由 broker 使用。

如果将主题配置为使用LogAppendTime,则生产者记录中的时间戳在将消息添加到其日志中时,将由 broker 重写。

然后,这条消息被存放在一个记录批次里,这个批次里的所有消息会被发送到相同的主题和分区上。由一个独立的线程负责把它们发到 Kafka Broker 上。

Kafka Broker 在收到消息时会返回一个响应,如果写入成功,会返回一个 RecordMetaData 对象,它包含了主题和分区信息,以及记录在分区里的偏移量,上面两种的时间戳类型也会返回给用户。

如果写入失败,会返回一个错误。生产者在收到错误之后会尝试重新发送消息,几次之后如果还是失败的话,就返回错误消息

image.png

消息消费

消息消费---模式

Kafka有两种模式消费数据:队列 和发布订阅 ;在队列模式下,一条数据只会发 送给 customer group中的一个 customer 进行消费;在发布订阅模式下,一条数据会发送给多个 customer进行消费。 Kafka的Customer基于 offset对kafka中的数据进行消费,对于一个 customer group中 的所有 customer共享一个 offset偏移量。 Kafka中通过控制 Customer的参数 {group.id}来决定 kafka是什么数据消费模式,如 果所有消费者的该参数值是相同,那么此时kafka就是类似于队列模式,数据只会发送到一个 customer,此时类似于负载均衡,否则就是订阅分布模式。 Kafka的数据是按照分区进行排序(插入的顺序 ),也就是每个分区中的数据有序的,但是多个分区之间做不到全局有序。在 Consumer进行数据消费的时候, 也是对分区有序但是不保证所有数据的序性 (多个分区之间 )。

消息消费—分区分配策略

在 kafka 中,存在两种分区分配策略,一种是 Range(默认)、另 一 种 另 一 种 还 是 RoundRobin ( 轮 询 )。 通 过partition.assignment.strategy 这个参数来设置。 Range 策略是对每个主题而言的,首先对同一个主题里面的分区按照序号进行排序,并对消费者按照字母顺序进行排序。假设我们有 10 个分区,3 个消费者,排完序的分区将会是 0, 1, 2, 3, 4, 5, 6, 7, 8, 9;消费者线程排完序将会是C1-0, C2-0, C3-0。C1-0 将消费 0, 1, 2, 3 分区,C2-0 将消费 4, 5, 6 分区,C3-0 将消费 7, 8, 9 分区。轮询分区策略是把所有 partition 和所有 consumer 线程都列出来,然后按照 hashcode 进行排序。最后通过轮询算法分配 partition 给消费线程。

当出现以下几种情况时,kafka 会进行一次分区分配操作,也就是 kafka consumer 的 rebalance

  1. 同一个 consumer group 内新增了消费者
  2. 消费者离开当前所属的 consumer group,比如主动停机或者宕机
  3. topic 新增了分区(也就是分区数量发生了变化)

kafka consuemr 的 rebalance 机制规定了一个 consumer group 下的所有 consumer 如何达成一致来分配订阅 topic的每个分区,具体实现是通过zk实现的,在此不写了,有兴趣的可以自己了解一下。

消息消费—消费位置

在 kafka 中,提供了一个__consumer_offsets_* 的一个topic , 把 offset 信 息 写 入 到 这 个 topic 中 。__consumer_offsets——按保存了每个 consumer group某一时刻提交的 offset 信息。__consumer_offsets 默认有50 个分区。

消息消费---如何通过offset查找message

查找的算法是

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

比如说,我们要查找 offset=2490 这条消息,那么先找到00000000000000000000.index, 然后找到[2487,49111]这个索引,再到 log 文件中,根据 49111 这个 position 开始查找,比较每条消息的 offset 是否大于等于 2490。最后查找到对应的消息以后返回

image.png

日志策略

日志清除策略

前面提到过,日志的分段存储,一方面能够减少单个文件内容的大小,另一方面,方便 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 值。日志的压缩原理是

image.png

性能设计---kafka为何如此之快

顺序读写:Kafka最核心的思想是使用磁盘,而不是使用内存。相对于内存的随机读写,使用硬盘的顺序读写会更好更快,所以kafka使用磁盘而不是内存。Kafka中用到了sendfile机制,随机读写是每秒k级别的,如果是线性读写可能能到每秒上G。

零拷贝:kafka会把数据立即写入文件系统的持久化日志中,不是先写在缓存中,再flush到磁盘中。也就是说,数据过来的时候,是传输在os kernel的页面缓存中,由os刷新到磁盘中。在os采用sendfile的机制,os可以从页面缓存一步发送数据到网络中

  • 消息压缩: kafka支持gzip和Snappy对数据进行压缩
  • 分批发送:生产者发送多个消息到 broker 上的同一个分区时,为了减少网络请求带来的性能开销,通过批量的方式来提交消息,可以通过这个参数来控制批量提交的字节数大小,默认大小是 16384byte,也就是 16kb,意味着当一批消息大小达到指定的 batch.size 的时候会统一发送
  • 读取特定消息的时间复杂度为O(1): 数据存储采用topic-partition-record的三层体系,是个树状数据结构。对于树的存储,比较常用的是B tree,运行时间是O(logN),但是在因为需要锁定机制,在磁盘层面,在高速交换、数据规模比较大的时候,性能损耗还是比较厉害的。Kafka的方式是把所有消息看成普通的日志,理念就是把日志内容简单的追加,采用offset读取数据,优势是性能完全是线性的,和数据大小没有关系,同时,读取操作和写入操作不会互相阻塞,性能能永远达到最大化。Kafka读取特定消息的时间复杂度为O(1),即与文件大小无关,所以这里删除文件与Kafka性能无关,选择怎样的删除策略只与磁盘以及具体的需求有关。

特性

  • 高吞吐、低延迟:kakfa 最大的特点就是收发消息非常快,kafka 每秒可以处理几十万条消息,它的最低延迟只有几毫秒。
  • 高伸缩性: 每个主题(topic) 包含多个分区(partition),主题中的分区可以分布在不同的主机(broker)中。
  • 持久性、可靠性: Kafka 能够允许数据的持久化存储,消息被持久化到磁盘,并支持数据备份防止数据丢失,Kafka 底层的数据存储是基于 Zookeeper 存储的,Zookeeper 我们知道它的数据能够持久存储。
  • 容错性: 允许集群中的节点失败,某个节点宕机,Kafka 集群能够正常工作
  • 高并发: 支持数千个客户端同时读写

场景

  • 活动跟踪:Kafka 可以用来跟踪用户行为,比如我们经常回去淘宝购物,你打开淘宝的那一刻,你的登陆信息,登陆次数都会作为消息传输到 Kafka
  • 传递消息:Kafka 另外一个基本用途是传递消息,应用程序向用户发送通知就是通过传递消息来实现的,这些应用组件可以生成消息,而不需要关心消息的格式,也不需要关心消息是如何发送的。
  • 度量指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告。
  • 日志记录:Kafka 的基本概念来源于提交日志,比如我们可以把数据库的更新发送到 Kafka 上,用来记录数据库的更新时间,通过kafka以统一接口服务的方式开放给各种consumer,例如hadoop、Hbase、Solr等。
  • 流式处理:流式处理是有一个能够提供多种应用程序的领域。
  • 限流削峰:Kafka 多用于互联网领域某一时刻请求特别多的情况下,可以把请求写入Kafka 中,避免直接请求后端程序导致服务崩溃。
文章分类
后端
文章标签