Kafka最全面教程系统化讲解-高级

在阅读本文时,请先读前面的文章Kafka最全面教程系统化讲解-基础

集群成员

  • Kafka使用 zookeeper来维护集群成员的信息。每个 broker都有个唯一标识符, 这个标识符可以在配置文件里指定, 也可以自动生成。 在 broker启动的时候, 它通过创建临时节点把自己的 ID注册到 zoo-keeper。 Kafka组件订阅 Zookeeper的/brokers/ids路径(broker在 zookeeper上的注册路径) , 当有 broker加入集群或退出集群时, 这些组件就可以获得通知 。
  • 如果你要启动另一个具有相同 ID的 broker, 会得到一个错误。新 broker会试着进行注册,但不会成功, 因为 zookeeper里已经有一个具有相同 ID的 broker。
  • 在 broker停机、 出现网络分区或长时间垃圾回收停顿时, broker会从 Zookeeper上断开连接, 此时 broker在启动时创建的临时节点会自动从 Zookeeper上移除。 监听 broker列表的Kafka 组件会被告知该 broker已移除。
  • 在关闭 broker时, 它对应的节点也会消失, 不过它的 ID会继续存在于其他数据结构中 。 例如,主题的副本列表里就可能包含这些ID。在完全关闭一个broker之后, 如果使用相同的ID启动另一个全新的 broker, 它会立刻加入集群, 并拥有与旧 broker 相同的分区和主题。

Broker Controller

  • 控制器其实就是一个 broker, 只不过它除了具有一般 broker的功能之外, 还负责分区首领的选举。 集群里第一个启动的 broker通过在Zookeeper里创建一个临时节点/controuer让自己成为控制器。 其他 broker在启动时也会尝试创建这个节点,不过它们会收到一个“节点已存在”的异常,然后“意识”到控制器节点已存在, 也就是说集群里已经有一个控制器了 。 其他 broker在控制器节点上创建Zookeeperwatch对象,这样它们就可以收到这个节点的变更通知。这种方式可以确保集群里一次只有一个控制器存在。
  • 如果控制器被关闭或者与 Zookeeper断开连接, zookeeper上的临时节点就会消失。 集群里的其他 broker通过 watch对象得到控制器节点消失的通知, 它们会尝试让自己成为新的控制器。 第一个在 Zookeeper里成功创建控制器节点的 broker就会成为新的控制器, 其他节点会收到“节点已存在”的异常,然后在新的控制器节点上再次创建watch对象。
  • 当控制器发现一个 broker已经离开集群,它就知道,那些失去首领的分区需要一个新首领 (这些分区的首领刚好是在这个 broker上)。 控制器遍历这些分区, 并确定谁应该成为新首领 (简单来说就是分区副本列表里的下一个副本) , 然后向所有包含新首领或现有跟随者的 broker发送请求。该请求消息包含了谁是新首领以及谁是分区跟随者的信息。随后,新首领开始处理来自生产者和消费者的情求,而跟随者开始从新首领那里复制消息。
  • 当控制器发现一个 broker加入集群时, 它会使用 broker ID来检査新加入的 broker是否包含现有分区的副本。 如果有, 控制器就把变更通知发送给新加入的 broker和其他 broker, 新 broker上的副本开始从首领那里复制消息。
  • 简而言之, Kafka使用 Zookeeper的临时节点来选举控制器,并在节点加入集群或退出集群时通知控制器。 控制器负责在节点加入或离开集群时进行分区首领选举。

Zookeeper作用

Kafka集群中有一个broker会被选举为Controller,负责管理集群broker的上下线,所有topic的分区副本分配和leader选举等工作。

Controller的管理工作都是依赖于Zookeeper的。
以下为partition的leader选举过程:
在这里插入图片描述


分区副本

Kafka使用主题来组织数据, 每个主题被分为若干个分区,每个分区有多个副本。那些副本被保存在 broker上, 每个 broker可以保存成百上千个属于不同主题和分区的副本。

leader

每个分区都有一个首领副本。为了保证一致性,所有生产者请求和消费者请求都会经过这个副本 。

follower

首领以外的副本都是跟随者副本。跟随者副本不处理来自客户端的请求,它们唯一一的任务就是从首领那里复制消息, 保持与首领一致的状态 。 如果首领发生崩溃, 其中的一个跟随者会被提升为新首领 。

工作机制

  • 首领的另一个任务是搞清楚哪个跟随者的状态与自己是一致的。 跟随者为了保持与首领的状态一致,在有新消息到达时尝试从首领那里复制消息, 不过有各种原因会导致同步失败。例如,网络拥塞导致复制变慢, broker发生崩演导致复制滞后,直到重启broker后复制才会继续。

  • 为了与首领保持同步, 跟随者向首领发送获取数据的请求, 这种请求与消费者为了读取消息而发送的请求是一样的。首领将响应消息发给跟随者。请求消息里包含了跟随者想要获取消息的偏移量, 而且这些偏移量总是有序的 。

    一个跟随者副本先请求消息1,接着请求消息2,然后请求消息3,在收到这3个请求的响应之前,它是不会发送第4个请求消息的。如果跟随者发送了请求消息4,那么首领就知道它已经收到了前面3个请求的响应。通过査看每个跟随者请求的最新偏移量, 首领就会知道每个跟随者复制的进度。

  • 如果跟随者在10s内没有请求任何消息,或者虽然在请求消息,但在10s内没有请求最新的数据,那么它就会被认为是不同步的。跟随者的正常不活跃时间或在成为不同步副本之前的时间可以通过replica.lag.time.max.ms参数来配置的。 这个时间间隔直接影响着首领选举期间的客户端行为和数据保留机制 。

  • 如果一个副本无法与首领保持一致,在首领发生失效时,它就不可能成为新首领,因为它没有包含全部的消息。相反,持续请求得到的最新消息副本被称为同步副本。在首领发生失效时,只有同步副本才有可能被选为新首领。

    扫描二维码关注公众号,回复: 11370633 查看本文章
  • 除了当前首领之外, 每个分区都有一个优先副本(首选首领),创建主题时选定的首领分区就是分区的优先副本。 之所以把它叫作优先副本, 是因为在创建分区时, 需要在 broker之间均衡首领副本。 因此, 我们希望首选首领在成为真正的首领时, broker间的负载最终会得到均衡。 默认情况下, Kafka的 auto.leader.rebalance.enable被设为 true,它会检査优先副本是不是当前首领,如果不是,并且该副本是同步的, 那么就会触发首领选举, 让优先副本成为当前首领。


处理请求

  • broker的大部分工作是处理客户端、分区副本和控制器发送给分区首领的请求。 Kafka提供了一个二进制协议(基于TCP),指定了请求消息的格式以及 broker如何对请求作出响应——包括成功处理请求或在处理请求过程中遇到错误。
  • 客户端发起连接并发送请求,broker处理请求并作出响应。 broker按照请求到达的顺序来处理它们这种顺序保证让Kaka具有了消息队列的特性,同时保证保存的消息也是有序的。
  • 所有的请求消息都包含一个标准消息头:
    • Request type(也就是 API key)
    • Request version( broker可以处理不同版本的客户端请求,并根据客户端版本作出不同的响应)
    • Correlation id-一个具有唯一性的数字,用于标识请求消息,同时也会出现在响应消息和错误日志里(用于诊断问题)
    • Client Id用于标识发送请求的客户端
  • broker会在它所监听的每一个端口上运行一个 Acceptor线程,这个线程会创建一个连接并把它交给 Processor线程去处理。 Processor线程(也被叫作“网络线程”)的数量是可配置的。网络线程负责从客户端获取请求消息,把它们放进请求队列,然后从响应队列获取响应消息,把它们发送给客户端。
    在这里插入图片描述
  • 请求消息被放到请求队列后,IO线程会负责处理它们。比较常见的请求类型有:
    • 生产请求:生产者发送的请求,它包含客户端要写入 broker的消息。
    • 获取请求:在消费者和跟随者副本需要从 broker读取消息时发送的请求。
      在这里插入图片描述

生产请求和获取请求都必须发送给分区的首领副本。

  • 如果broker 收到一个针对特定分区的请求,而该分区的首领在另一个broker 上,那么发送请求的客户端会收到一个“非分区首领”的错误响应。当针对特定分区的获取请求被发送到一个不含有该分区首领的broker上,也会出现同样的错误。Kafka 客户端要自己负责把生产请求和获取请求发送到正确的broker 上。
  • 那么客户端怎么知道该往哪里发送请求呢?客户端使用了另一种请求类型,也就是元数据请求。这种请求包含了客户端感兴趣的主题列表。服务器端的响应消息里指明了这些主题所包含的分区、每个分区都有哪些副本, 以及哪个副本是首领。元数据请求可以发送给任意一个broker ,因为所有broker 都缓存了这些信息。
  • 一般情况下,客户端会把这些信息缓存起来,并直接往目标broker 上发送生产请求和获取请求。它们需要时不时地通过发送元数据请求来刷新这些信息(刷新的时间间隔通过meta.max.age.ms参数来配置),从而知道元数据是否发生了变更, 比如,在新broker 加入集群时,部分副本会被移动到新的broker 上。另外,如果客户端收到“非首领”错误,它会在尝试重发请求之前先刷新元数据,因为这个错误说明了客户端正在使用过期的元数据信息,之前的请求被发到了错误的broker 上。

生产请求

我们曾经说过, acks这个配置参数,该参数指定了需要多少个 broker确认才可以认为一个消息写入是成功的。不同的配置对“写入成功”的界定是不一样的,如果acks=1,那么只要首领收到消息就认为写入成功;如果acks=all,那么需要所有同步副本收到消息才算写入成功; 如果 acks=0, 那么生产者在把消息发出去之后, 完全不需要等待 broker的响应。
包含首领副本的 broker在收到生产请求时, 会对请求做一些验证。

  • 发送数据的用户是否有主题写入权限?
  • 请求里包含的acks值是否有效(只允许出现0、1或all) ?
  • 如果 acks=all, 是否有足够多的同步副本保证消息已经被安全写入?

之后,消息被写入本地磁盘。在Linux系统上,消息会被写到文件系统缓存里,并不保证它们何时会被刷新到磁盘上。Kafka不会一直等待数据被写到磁盘上,它依赖复制功能来保证消息的持久性。在消息被写入分区的首领之后, broker开始检査 acks配置参数一如果 acks被设为 0或1, 那么 broker立即返回响应;如果 acks被设为 all,那么请求会被保存在一个叫作炼狱的缓冲区里, 直到首领发现所有跟随者副本都复制了消息, 响应才会被返回给客户端。

获取请求

broker处理获取请求的方式与处理生产请求的方式很相似。客户端发送请求,向 broker请求主题分区里具有特定偏移量的消息, 好像在说: “请把主题 Test分区 0偏移量从53开始的消息以及主题 Test分区3偏移量从64开始的消息发给我。”客户端还可以指定 broker最多可以从一个分区里返回多少数据。 这个限制是非常重要的, 因为客户端需要为 broker返回的数据分配足够的内存。 如果没有这个限制, broker返回的大量数据有可能耗尽客户端的内存。

我们之前讨论过,请求需要先到达指定的分区首领上,然后客户端通过査询元数据来确保请求的路由是正确的。首领在收到请求时,它会先检査请求是否有效,比如,指定的偏移量在分区上是否存在?如果客户端请求的是已经被删除的数据,或者请求的偏移量不存在, 那么 broker将返回一个错误。

如果请求的偏移量存在, broker将按照客户端指定的数量上限从分区里读取消息, 再把消息返回给客户端。 Kafka使用零复制技术向客户端发送消息一一也就是说, Kafka直接把消息从文件(或者更确切地说是 Linux文件系统缓存)里发送到网络通道,而不需要经过任何中间缓冲区。 这是 Kafka与其他大部分数据库系统不一样的地方, 其他数据库在将数据发送给客户端之前会先把它们保存在本地缓存里。 这项技术避免了字节复制, 也不需要管理内存缓冲区, 从而获得更好的性能。

客户端除了可以设置 broker返回数据的上限, 也可以设置下限。 例如, 如果把下限设置为10KB,就好像是在告诉broker:“等到有10KB数据的时候再把它们发送给我。”在主题消息流量不是很大的情况下,这样可以减少 CPU和网络开销。 客户端发送一个请求, broker 等到有足够的数据时才把它们返回给客户端, 然后客户端再发出情求, 而不是让客户端每隔几毫秒就发送一次请求,每次只能得到很少的数据甚至没有数据。对比这两种情况, 它们最终读取的数据总量是一样的, 但前者的来回传送次数更少, 因此开销也更小。
在这里插入图片描述
当然,我们不会让客户端一直等待broker累积数据。在等待了一段时间之后,就可以把可用的数据拿回处理,而不是一直等待下去。所以,客户端可以定义一个超时时间,告诉 broker: “如果你无法在 K毫秒内累积满足要求的数据量, 那么就把当前这些数据返回给我。”

ISR

设想以下情景:leader收到数据,所有follower都开始同步数据,但有一个follower,因为某种故障,迟迟不能与leader进行同步,那leader就要一直等下去,直到它完成同步,才能发送ack。这个问题怎么解决呢?

Kafka的数据复制是以Partition为单位的。而多个备份间的数据复制,通过Follower向Leader拉取数据完成。从一这点来讲,有点像Master-Slave方案。不同的是,Kafka既不是完全的同步复制,也不是完全的异步复制,而是基于ISR的动态复制方案。

ISR,也即In-Sync Replica。每个Partition的Leader都会维护这样一个列表,该列表中,包含了所有与之同步的Replica(包含Leader自己)。每次数据写入时,只有ISR中的所有Replica都复制完,Leader才会将其置为Commit,它才能被Consumer所消费。

这种方案,与同步复制非常接近。但不同的是,这个ISR是由Leader动态维护的。如果Follower不能紧“跟上”Leader,它将被Leader从ISR中移除,待它又重新“跟上”Leader后,会被Leader再次加加ISR中。每次改变ISR后,Leader都会将最新的ISR持久化到Zookeeper中。

至于如何判断某个Follower是否“跟上”Leader,不同版本的Kafka的策略稍微有些区别。
从0.9.0.0版本开始,replica.lag.max.messages被移除,故Leader不再考虑Follower落后的消息条数。另外,Leader不仅会判断Follower是否在replica.lag.time.max.ms时间内向其发送Fetch请求,同时还会考虑Follower是否在该时间内与之保持同步。

示例
在这里插入图片描述

  • 在第一步中,Leader A总共收到3条消息,但由于ISR中的Follower只同步了第1条消息(m1),故只有m1被Commit,也即只有m1可被Consumer消费。此时Follower B与Leader A的差距是1,而Follower C与Leader A的差距是2,虽然有消息的差距,但是满足同步副本的要求保留在ISR中。同步副本概念参见后文“复制”章节。
  • 在第二步中,由于旧的Leader A宕机,新的Leader B在replica.lag.time.max.ms时间内未收到来自A的Fetch请求,故将A从ISR中移除,此时ISR={B,C}。同时,由于此时新的Leader B中只有2条消息,并未包含m3(m3从未被任何Leader所Commit),所以m3无法被Consumer消费。

LEO&HW

并不是所有保存在分区首领上的数据都可以被客户端读取。大部分客户端只能读取已经被写入所有同步副本的消息。 分区首领知道每个消息会被复制到哪个副本上, 在消息还没有被写入所有同步副本之前, 是不会发送给消费者的,尝试获取这些消息的请求会得到空的响应而不是错误。
在这里插入图片描述

因为还没有被足够多副本复制的消息被认为是“不安全”的,如果首领发生崩横,另一个副本成为新首领,那么这些消息就丢失了。如果我们允许消费者读取这些消息,可能就会破坏一致性。试想,一个消费者读取并处理了这样的一个消息,而另一个消费者发现这个消息其实并不存在。所以,我们会等到所有同步副本复制了这些消息,才允许消费者读取它们。这也意味着,如果broker间的消息复制因为某些原因变慢,那么消息到达消费者的时间也会随之变长(因为我们会先等待消息复制完毕) 。延迟时间可以通过参数replica.lag.time.max.ms来配置,它指定了副本在复制消息时可被允许的最大延迟时间。

  • LEO:指的是每个副本最大的offset;

  • HW:指的是消费者能见到的最大的offset,ISR队列中最小的LEO。
    在这里插入图片描述

  • 如果follower故障
    follower发生故障后会被临时踢出ISR,待该follower恢复后,follower会读取本地磁盘记录的上次的HW,并将log文件高于HW的部分截取掉,从HW开始向leader进行同步。等该follower的LEO大于等于该Partition的HW,即follower追上leader之后,就可以重新加入ISR了。

  • 如果leader故障
    leader发生故障之后,会从ISR中选出一个新的leader,之后,为保证多个副本之间的数据一致性,其余的follower会先将各自的log文件高于HW的部分截掉,然后从新的leader同步数据。
    注意:这只能保证副本之间的数据一致性,并不能保证数据不丢失或者不重复。

ISR相关配置说明

Broker的min.insync.replicas参数指定了Broker所要求的ISR最小长度,默认值为1。也即极限情况下ISR可以只包含Leader。但此时如果Leader宕机,则该Partition不可用,可用性得不到保证。

只有被ISR中所有Replica同步的消息才被Commit,但Producer发布数据时,Leader并不需要ISR中的所有Replica同步该数据才确认收到数据。Producer可以通过acks参数指定最少需要多少个Replica确认收到该消息才视为该消息发送成功。acks的默认值是1,即Leader收到该消息后立即告诉Producer收到该消息,此时如果在ISR中的消息复制完该消息前Leader宕机,那该条消息会丢失。而如果将该值设置为0,则Producer发送完数据后,立即认为该数据发送成功,不作任何等待,而实际上该数据可能发送失败,并且Producer的Retry机制将不生效。更推荐的做法是,将acks设置为all或者-1,此时只有ISR中的所有Replica都收到该数据(也即该消息被Commit),Leader才会告诉Producer该消息发送成功,从而保证不会有未知的数据丢失。


物理存储

在这里插入图片描述
Kafka中消息是以topic进行分类的,生产者生产消息,消费者消费消息,都是面向topic的。
topic是逻辑上的概念,而partition是物理上的概念,每个partition对应于一个log文件,该log文件中存储的就是producer生产的数据。Kafka的基本存储单元是分区。分区无法在多个broker间进行再细分,也无法在同一个broker的多个磁盘上进行再细分。

在配置 Kafka的时候, 管理员指定了一个用于存储分区的目录清单——也就是log.dirs参数的值 (不要把它与存放错误日志的目录混淆了, 日志目录是配置在1og4j.properties文件里的)。 该参数一般会包含每个挂载点的目录。

分区分配

在创建主题时, Kafka首先会决定如何在 broker间分配分区。

假设你有6个 broker, 打算创建一个包含10个分区的主题,并且复制系数为3。那么 Kafka就会有30个分区副本,它们可以被分配给6个 broker。 在进行分区分配时, 我们要达到如下的目标。

  • 在 broker间平均地分布分区副本。对于我们的例子来说, 就是要保证每个 broker可以分到5个副本。
  • 确保每个分区的每个副本分布在不同的 broker上。假设分区 0的首领副本在 broker2上,那么可以把跟随者副本放在 broker3和 broker4上, 但不能放在 broker2上,也不能两个都放在 broker3上。
  • 如果为 broker指定了机架信息,那么尽可能把每个分区的副本分配到不同机架的 broker上 。 这样做是为了保证一个机架的不可用不会导致整体的分区不可用 。

为了实现这个目标, 我们先随机选择一个 broker(假设是4) , 然后使用轮询的方式给每个broker分配分区来确定首领分区的位置。于是,首领分区 0会在 broker4上,首领分区l会在 broker5上, 首领分区2会在broker 0上(只有6个 broker), 并以此类推。然后, 我们从分区首领开始,依次分配跟随者副本。如果分区0的首领在broker4上,那么它的第一个跟随者副本会在 broker5上,第二个跟随者副本会在 broker 0上。分区1的首领在broker5上,那么它的第一个跟随者副本在 broker0上,第二个跟随者副本在 broker1上。

为分区和副本选好合适的 broker之后, 接下来要决定这些分区应该使用哪个目录。 我们单独为每个分区分配目录, 规则很简单: 计算每个目录里的分区数量, 新的分区总是被添加到数量最小的那个目录里。 也就是说, 如果添加了一个新磁量, 所有新的分区都会被创建到这个磁盘上。因为在完成分配工作之前,新磁盘的分区数量总是最少的。

topic中partition存储分布

假设实验环境中Kafka集群只有一个broker,xxx/message-folder为数据文件存储根目录,在Kafka broker中server.properties文件配置(参数log.dirs=xxx/message-folder),例如创建2个topic名 称分别为report_push、launch_info, partitions数量都为partitions=4,存储路径和目录规则为:

  • xxx/message-folder
    • |–report_push-0/
    • |–report_push-1/
    • |–report_push-2/
    • |–report_push-3/
    • |–launch_info-0/
    • |–launch_info-1/
    • |–launch_info-2/
    • |–launch_info-3/

在Kafka文件存储中,同一个topic下有多个不同partition,每个partition为一个目录,partiton命名规则为topic名称+有序序号,第一个partiton序号从0开始,序号最大值为partitions数量减1。

消息发送时都被发送到一个topic,其本质就是一个目录,而topic由是由一些Partition组成,其组织结构如下图所示:
在这里插入图片描述
在这里插入图片描述
我们可以看到,Partition是一个Queue的结构,每个Partition中的消息都是有序的,生产的消息被不断追加到Partition上,其中的每一个消息都被赋予了一个唯一的offset值。

把消息日志以Partition的形式存放有多重考虑:

  • 第一,方便在集群中扩展,每个Partition可以通过调整以适应它所在的机器,而一个topic又可以有多个Partition组成,因此整个集群就可以适应任意大小的数据了;
  • 第二就是可以提高并发,因为可以以Partition为单位读写了。

partiton中segment存储分布

每个partition(目录)相当于一个巨型文件被平均分配到多个大小相等segment(段)数据文件中。但每个段segment file消息数量不一定相等,这种特性方便old segment file快速被删除。每个partiton只需要支持顺序读写就行了,segment文件生命周期由服务端配置参数决定。
这样做的好处就是能快速删除无用文件,有效提高磁盘利用率。
在这里插入图片描述
producer发message到某个topic,message会被均匀的分布到多个partition上(随机或根据用户指定的回调函数进行分布),kafka broker收到message往对应partition的最后一个segment上添加该消息,当某个segment上的消息条数达到配置值或消息发布时间超过阈值时,segment上的消息会被flush到磁盘,只有flush到磁盘上的消息consumer才能消费,segment达到一定的大小后将不会再往该segment写数据,broker会创建新的segment。

  • 每个part在内存中对应一个index,记录每个segment中的第一条消息偏移。
  • 每个segment由2大部分组成,分别为index file和data file,此2个文件一一对应,成对出现,后缀".index"和“.log”分别表示为segment索引文件、数据文件。
  • segment文件命名规则:partion全局的第一个segment从0开始,后续每个segment文件名为上一个全局partion的最大offset(偏移message数)。数值最大为64位long大小,19位数字字符长度,没有数字用0填充。
  • 每个segment中存储很多条消息,消息id由其逻辑位置决定,即从消息id可直接定位到消息的存储位置,避免id到位置的额外映射。

下面文件列表是在Kafka broker上做的一个实验,创建一个topicXXX包含1 partition,设置每个segment大小为500MB,并启动producer向Kafka broker写入大量数据,如下图2所示segment文件列表形象说明了上述2个规则:
在这里插入图片描述
以上述图2中一对segment file文件为例,说明segment中index<—->data file对应关系物理结构如下:
在这里插入图片描述
上述图3中索引文件存储大量元数据,数据文件存储大量消息,索引文件中元数据指向对应数据文件中message的物理偏移地址。其中以索引文件中 元数据3,497为例,依次在数据文件中表示第3个message(在全局partiton表示第368772个message)、以及该消息的物理偏移 地址为497。
从上述图3了解到segment data file由许多message组成,下面详细说明message物理结构如下(不同的kafka版本,消息结构不同,尤其是0.11.x前后):
在这里插入图片描述

文件管理

保留数据是 Kafka的一个基本特性, Kafka不会一直保留数据, 也不会等到所有消费者都读取了消息之后才删除消息。 相反, Kafka管理员为每个主题配置了数据保留期限, 规定数据被删除之前可以保留多长时间, 或者清理数据之前可以保留的数据量大小 。

因为在一个大文件里査找和删除消息是很费时的, 也很容易出错, 所以分区分成若干个片段。默认情况下,每个片段包含1GB或一周的数据,以较小的那个为准。在broker 往分区写入数据时,如果达到片段上限,就关闭当前文件,并打开一个新文件。

当前正在写入数据的片段叫作活跃片段。 活动片段永远不会被删除, 所以如果你要保留数据1天,但片段里包含了5天的数据,那么这些数据会被保留5天,因为在片段被关闭之前这些数据无法被删除。如果你要保留数据一周,而且每天使用一个新片段,那么你就会看到,每天在使用一个新片段的同时会删除一个最老的片段一所以大部分时间该分区会有7个片段存在。

文件格式

Kafka的消息和偏移量保存在文件里。保存在磁盘上的数据格式与从生产者发送过来或者发送给消费者的消息格式是一样的 。 因为使用了相同的消息格式进行磁盘存储和网络传输, Kafka可以使用零复制技术给消费者发送消息, 同时避免了对生产者已经压缩过的消息进行解压和再圧缩。
除了键、值和偏移量外,消息里还包含了消息大小、校验和、消息格式版本号、压缩算法(snappy、 Gzip或Lz4)和时间戳(在0.10.0版本里引入的)。时间戳可以是生产者发送消息的时间, 也可以是消息到达 broker的时间, 这个是可配置的。

如果生产者发送的是圧缩过的消息, 那么同一个批次的消息会被压缩在一起, 被当作 “包装消息”进行发送。于是, broker就会收到一个这样的消息,然后再把它发送给消费者。 消费者在解压这个消息之后, 会看到整个批次的消息, 它们都有自己的时间戳和偏移量。

如果在生产者端使用了压缩功能(极力推荐),那么发送的批次越大,就意味着在网络传输和磁盘存储方面会获得越好的压缩性能, 同时意味着如果修改了消费者使用的消息格式 (例如, 在消息里增加了时间戳) , 那么网络传输和磁盘存储的格式也要随之修改, 而且 broker要知道如何处理包含了两种消息格式的文件。

Kafka附带了一个叫 DumpLogSegment的工具, 可以用它査看片段的内容。 它可以显示每个消息的偏移量、校验和、魔术数字节、消息大小和压缩算法。

超时数据清理

Kafka将数据持久化到了硬盘上,允许你配置一定的策略对数据清理,清理的策略有两个,删除和压缩。

删除

log.cleanup.policy=delete启用删除策略,直接删除,删除后的消息不可恢复。可配置以下两个策略:

  • 清理超过指定时间清理: log.retention.hours=16
  • 超过指定大小后,删除旧的消息:log.retention.bytes=1073741824

为了避免在删除时阻塞读操作,采用了copy-on-write形式的实现,删除操作进行时,读取操作的二分查找功能实际是在一个静态的快照副本上进行的,这类似于Java的CopyOnWriteArrayList。

压缩

将数据压缩,只保留每个key最后一个版本的数据。

  • 首先在broker的配置中设置log.cleaner.enable=true启用cleaner,这个默认是关闭的。
  • 在topic的配置中设置log.cleanup.policy=compact启用压缩策略。

在这里插入图片描述
如上图,在整个数据流中,每个Key都有可能出现多次,压缩时将根据Key将消息聚合,只保留最后一次出现时的数据。这样,无论什么时候消费消息,都能拿到每个Key的最新版本的数据。

压缩后的offset可能是不连续的,比如上图中没有5和7,因为这些offset的消息被merge了,当从这些offset消费消息时,将会拿到比这个offset大的offset对应的消息,比如,当试图获取offset为5的消息时,实际上会拿到offset为6的消息,并从这个位置开始消费。
这种策略只适合特殊场景,比如消息的key是用户ID,消息体是用户的资料,通过这种压缩策略,整个消息集里就保存了所有用户最新的资料。
压缩策略支持删除,当某个Key的最新版本的消息没有内容时,这个Key将被删除,这也符合以上逻辑。

上述过期清理内容来自 https://blog.csdn.net/honglei915/article/details/49683065

压缩策略具体实现

每个日志片段可以分为以下两个部分 :

  • 干净的部分,这些消息之前被清理过, 每个键只有一个对应的值, 这个值是上一次清理时保留下来的。
  • 污浊的部分,这些消息是在上一次清理之后写入的。

为了清理分区, 清理线程会读取分区的污独部分, 并在内存里创建一个 map。 map里的每个元素包含了消息键的散列值和消息的偏移量,键的散列值是16B,加上偏移量总共是24B。如果要清理一个1GB的日志片段,并假设每个消息大小为1KB,那么这个片段就包含_一百万个消息,而我们只需要用24MB的 map就可以清理这个片段。 (如果有重复的键, 可以重用散列项, 从而使用更少的内存。)

清理线程在创建好偏移量map后,开始从干净的片段处读取消息,从最旧的消息开始,把它们的内容与 map里的内容进行比对。它会检査消息的键是否存在于 map中, 如果不存在, 那么说明消息的值是最新的,就把消息复制到替換片段上。如果键已存在,消息会被忽略, 因为在分区的后部已经有一个具有相同键的消息存在。在复制完所有的消息之后,我们就将替換片段与原始片段进行交换,然后开始清理下一个片段。完成整个清理过程之后,每个键对应一个不同的消息一这些消息的值都是最新的。

高效读写数据

1. 顺序写磁盘

Kafka的producer生产数据,要写入到log文件中,写的过程是一直追加到文件末端,为顺序写。官网有数据表明,同样的磁盘,顺序写能到600M/s,而随机写只有100K/s。这与磁盘的机械机构有关,顺序写之所以快,是因为其省去了大量磁头寻址的时间。
在这里插入图片描述

2. 稀疏索引

segment index file采取稀疏索引存储方式,减少索引文件大小,通过mmap将所有index映射到内存。

  • 稀疏索引要求数据是按索引字段顺序存储的;
  • 稀疏索引比稠密索引节省了更多的存储空间,但查找起来需要消耗更多的时间。
    举例如下图:
    在这里插入图片描述

3. 零复制技术

在这里插入图片描述
提示:网卡简称NIC(Network Interface Card)

监控

Kafka Manager

kafka-manager-1.3.3.15.zip

Kafka Monitor

KafkaOffsetMonitor-assembly-0.4.6.jar

猜你喜欢

转载自blog.csdn.net/weixin_43956062/article/details/106788612