工具使用集|Kafka:官方文档简明解读(续)

前言

kafka 官方文档浅读,好吃力。已经有大半个月没有更新了,记录一下kafak相关的内容。

konwlage

Kafka不是一个传统的消息传递系统,而更类似于一个持久性的分布式发布订阅消息日志。

那么kafka一般将数据持久化在哪里呢?

kafka的持久化数据存储通常存储在称为"日志目录"(log directory)的文件系统路径上。

每个Kafka主题(topic)都有一个或多个分区(partition),每个分区在磁盘上都有一个对应的日志目录。这些日志目录包含了分区中的消息数据。

Kafka的持久化存储是通过以下方式实现的:

  1. 消息日志: 每个分区都有一个消息日志,它是一个不断追加的顺序文件,其中包含了所有已发布到该分区的消息。这些消息在磁盘上持久存储,确保数据的持久性。
  2. 分段日志(Segmented Logs): 为了管理长期的消息存储,Kafka会将消息日志分成多个分段(segment),每个分段都有一个最大大小限制。当一个分段达到其大小限制时,Kafka会创建一个新的分段,从而实现了消息数据的分段存储。旧的分段在满足一定条件后可以被清理或删除。
  3. 索引文件: 为了支持高效的消息检索,Kafka还维护了索引文件,它们包含了消息偏移量(offset)与消息在日志文件中位置的映射关系,以便快速查找特定消息。

一般我们都认为存储系统中对磁盘的读写较慢,那么kafak使用文件系统来作为存储形式。它的优势在哪里?

  • 性能优势: Kafka利用了现代操作系统对于文件系统缓存的高度优化,将消息数据写入文件系统,充分利用了操作系统的页缓存。这意味着所有的读写操作都可以在内存中进行,通常是O(1)时间复杂度,因此不会受到磁盘性能的严重限制。这种设计能够充分利用主内存和高速缓存,从而提供了高吞吐量和低延迟的性能。
  • 容量扩展性: 文件系统的使用允许Kafka轻松扩展存储容量。只需添加更多的硬盘驱动器或扩展文件系统,就可以处理更多的数据,而不需要更改应用程序代码

对于kafka来说,它较为经常使用的数据结构是什么?

Kafka采用了一种设计,将消息立即写入文件系统的持久日志中,而不是依赖复杂的B树等数据结构。这种设计使得所有操作都是O(1),即常数时间。

通过将消息写入追加日志,Kafka充分利用了现代操作系统对于文件系统缓存的优化,从而将读写操作加速到接近内存速度。这种设计非常适合处理实时数据流,因为它提供了高吞吐量和低延迟的性能,同时保持数据的持久性。

那Kafka在提高效率方面一般都采用了什么方式?

  1. 消息集(Message Set)抽象: Kafka的协议基于消息集的抽象构建,自然地将一组消息组合在一起。这允许网络请求将多个消息组合在一起,摊销网络往返的开销,而不是逐个发送消息。服务器也会一次性附加消息的块到其日志中,消费者可以一次获取大块的线性数据。
  2. 批处理(Batching): 批处理将多个小的I/O操作合并为一个大的操作,从而显著提高效率。通过批处理,Kafka可以将随机的消息写入转换为线性写入,这有助于提高性能。
  3. 避免字节复制: Kafka采用了一种标准的二进制消息格式,生产者、代理和消费者之间共享这个格式,因此数据块可以在它们之间传输而无需修改。这避免了在数据传输中的不必要字节复制,特别是在高负载情况下,这对性能有显著影响。
  4. 页缓存和sendfile: Kafka使用操作系统的页缓存和sendfile系统调用来实现零拷贝数据传输。这通过避免多次复制和系统调用,从文件到套接字的数据传输路径变得高效,提高了数据传输的速度。这意味着数据只需从磁盘读取到页缓存一次,然后可以在多次消费中重复使用,而无需每次读取时都将数据复制到用户空间。

消息集,它是不是一压缩操作呢?那么如何保证在网络传输的过程中保证其完整性呢?

不一定,虽然压缩可以是一种将多个消息组合在一起的方式之一,但消息集(Message Set)的抽象并不仅仅用于压缩操作。消息集的主要目的是将多个消息组织在一起,以减少网络通信的开销和提高传输效率。

  • Kafka使用一种特定的二进制消息格式,这种格式包括消息的元数据和消息体

​ 元数据包括消息的偏移量、大小、时间戳等信息。这个消息格式是标准化的,生产者、代理和消费者都使用相同的格式来编码和解码消息。

  • Kafka使用复制机制来确保消息的可靠性。每个分区通常都有多个副本,这些副本分布在不同的服务器上。Kafka使用同步复制和ISR(In-Sync Replicas)的概念来确保消息被正确复制并保持一致性。只有当所有的副本都确认接收消息后,生产者才会认为消息已成功发送。

Kafka提供了一种高效的批处理压缩格式。多个消息可以被组合成一个批次,然后以压缩形式发送到服务器。这个消息批次将以压缩形式写入,并在日志中保持压缩状态,只有在消费者端才会进行解压缩。

Kafka中的生产者(Producer)以及如何实现负载均衡?

随机选择,实现一种随机的负载均衡

通过某种语义分区函数来实现

Kafka通过允许用户指定一个分区关键字并使用该关键字进行哈希分区来公开语义分区的接口

如果选择的关键字是用户ID,那么所有属于同一个用户的数据将被发送到同一个分区。这将允许消费者对其消费行为做出与数据局部性有关的假设。这种分区方式专门设计用于允许消费者进行局部感知的处理。

kafak 的异步发送消息

通过,我们在编写插入sql语句的时候,如果遇到大量的插入我们会选择批量插入,来提高效率和减少与数据库I/O的次数。

Kafka的生产者支持异步发送消息,其中批处理是提高效率的关键因素。为了启用批处理,Kafka生产者会尝试在内存中累积数据,并在单个请求中发送较大的消息批次。

具体的批处理配置包括:

  • 累积消息数量:可以配置生产者累积一定数量的消息,然后将它们一起发送。
  • 最大等待延迟:可以配置生产者等待一定的时间,然后发送已累积的消息,即使消息数量未达到设定的阈值。

这种批处理机制有助于减少I/O操作次数,提高了性能和效率。用户可以根据应用场景的需求来配置批处理的参数,以在吞吐量和延迟之间找到平衡点。

kafka如何有效的管理获取的数据?

Kafka的消费者通过发出获取请求来获取要消费的分区的数据。这些获取请求包含了消费者在分区日志中的偏移量,以确定从哪个位置开始获取数据。

消费者可以随时控制偏移量的位置,这意味着它可以自由地重播数据,如果需要的话。这对于处理消息丢失或者重新处理数据非常有用。

这种获取机制允许消费者灵活地管理其消费进度,并确保它们能够获取到他们感兴趣的特定消息。

Kafka如何选择的数据传输方式?

Kafka 遵循大多数消息传递系统所共享的更传统的设计,其中数据从生产者推送到代理并由消费者从代理中提取。 --- 主动拉取数据

我们都知道,消息队列的的传输方式一般分为:

  • 推送:

在推模型中,数据由生产者主动推送到代理(broker),然后再由代理主动推送给消费者。

优势:

  1. 即时性: 推送模型可以实现数据的实时传输,一旦数据可用,就立即将其推送给消费者,不需要等待消费者请求。
  2. 低延迟: 由于数据立即推送给消费者,因此可以实现较低的传输延迟,适用于对数据及时性要求较高的场景。
  3. 简化消费者逻辑: 在推送模型中,消费者不需要定期轮询数据,而是等待数据推送。这简化了消费者的逻辑实现。
  4. 适用于流式处理: 推送模型适用于流式处理场景,其中数据不断产生并需要实时处理。
  5. 避免资源浪费: 没有不必要的轮询请求,可以避免资源浪费。

缺点:

  1. 不适用于高负载差异: 推送模型在面对多样化的消费者速率时可能会出现问题。如果生产者产生数据的速率远远快于消费者处理数据的速率,消费者可能会被压倒,导致性能下降。
  2. 难以控制消费进度: 在推送模型中,消费者通常无法控制数据的传输速率,而是被动接收数据。这意味着消费者无法灵活地控制自己的消费进度,可能会导致数据处理的不均匀。
  3. 潜在的资源浪费: 如果数据被频繁地推送给消费者,而消费者无法及时处理,可能会导致资源浪费,包括网络带宽和存储空间。
  4. 复杂性增加: 在推送模型中,需要实现推送和数据传输的机制,这可能会增加系统的复杂性。
  5. 难以应对消费者故障: 推送模型中,如果消费者出现故障或崩溃,可能需要额外的机制来处理数据丢失或重新传输。
  • 拉去:

拉模型中,数据由生产者主动推送到代理(broker),消费者主动从代理拉取数据。

优点:

  1. 消费者控制: 拉模型允许消费者完全控制数据的获取进度和速率。消费者可以自主地决定何时拉取数据,从而更好地适应其处理能力和需求。
  2. 避免压力过载: 在拉模型中,如果消费者的处理速度跟不上数据产生速度,它可以选择不拉取新数据,避免被数据压倒,从而提高了系统的稳定性。
  3. 适应不同速率: 拉模型适用于处理多样化的消费者速率,消费者可以根据自身处理能力和需求来调整拉取数据的频率。
  4. 批处理优势: 拉模型通常有助于实现数据的批处理,因为消费者可以在拉取数据时获取一批数据,而不需要一条一条地处理。这有助于提高数据处理的效率。
  5. 避免资源浪费: 消费者在拉模型中只在需要时才获取数据,避免了不必要的资源浪费,如网络带宽和存储空间。
  6. 适用于不可靠网络: 拉模型在不可靠的网络环境中更具优势,因为消费者可以等待数据就绪后再进行拉取,而不必频繁尝试推送。

缺点:

  1. 延迟: 在拉模型中,消费者需要主动轮询或拉取数据,这可能导致一定的延迟,因为数据不会立即被推送给消费者。
  2. 复杂性增加: 拉模型通常需要消费者实现轮询或拉取数据的逻辑,这增加了消费者端的复杂性。
  3. 可能导致忙等待: 如果不加控制,消费者可能会在没有数据可用时陷入忙等待状态,不断轮询数据,这会浪费资源。
  4. 难以处理大规模数据: 在处理大规模数据时,拉模型可能需要大量的轮询操作,从而增加了系统的负载和资源消耗。
  5. 不适用于低延迟需求: 对于对低延迟要求极高的应用,拉模型可能无法满足需求,因为消费者需要等待数据就绪后才能拉取,无法实现实时性。
  6. 不适用于流式数据: 在流式数据场景下,拉模型可能不如推模型适用,因为流式数据需要快速传递和实时处理,而拉模型可能引入一定的延迟。

Kafka如何跟踪消息消费状态?

通常,当消息传递到了代理中,如何确保消息消费的进度,一般代理会保存代理上已使用的消息的元数据,也就是说,当消息被分发给消费者时,代理要么立即在本地记录该事实,要么等待消费者的确认。

但是这样设计也可以存在问题:

代理在每次通过网络分发消息时立即将其记录为已消费,那么如果消费者无法处理该消息(例如因为崩溃或请求超时或其他原因),该消息将丢失。

解决这个问题,是通过添加了确认功能,这意味着消息在发送时仅被标记为已发送,而不是被消费;代理等待消费者的特定确认以将消息记录为已消费

但是还这样设计还存在另一个问题:

首先,如果消费者处理消息但在发送确认之前失败,那么该消息将被消费两次。

假设消费者成功接收并处理了消息,但在发送确认之前经历了故障,如崩溃、网络问题或超时。在这种情况下,代理并不知道消息已被消费,因为它没有收到确认。因此,代理会将消息重新发送给其他消费者,以确保消息得到处理。这导致了消息被消费两次的情况。

第二个问题,如果消息已被发送但从未收到确认,代理必须决定如何处理这些消息。这可能需要维护额外的状态信息。

解决方式:

传统消息系统必须处理上述问题,通常采用以下方式来解决:

  • 消息去重: 针对消息重复的问题,消息系统需要实施去重机制,以确保即使消息被多次消费,最终结果也是一致的。这通常需要在消费者端进行处理。
  • 处理未确认消息: 对于未确认消息,系统可能需要引入一种超时机制,以便在一定时间内没有收到确认时,将消息重新发送给消费者或采取其他措施。
  • 维护多个状态: 管理多个状态可能需要更多的内存和计算资源,因此需要仔细考虑性能和资源消耗。

而kafka的设置是:

Kafka的消息确认方式:

  • Kafka将主题(Topic)分成一组完全有序的分区(Partitions)。
  • 每个分区在任何给定时刻都由一个消费者组中的一个消费者消费。
  • 每个消费者在分区中都有一个消费位置,这个位置表示它要消费的下一条消息的偏移量(Offset)。
  • Kafka通过跟踪每个消费者在每个分区中的偏移量来维护消费状态。
  • 这种状态非常小,因为只需一个整数来表示每个分区中的消费位置。
  • 这个状态可以定期检查和更新,使消息确认的成本非常低。

因为通过偏移量来进行相应的数据移动,也就意味着:

  • 它们可以故意将自己的消费位置回退到旧的偏移量,并重新消费之前的数据。

kafka的消息传递语义?

  • At most once---Messages may be lost but are never redelivered. (最多一次传递一次消息,如果丢失kafka不会处理,即使消息在传递过程中丢失)
  • At least once---Messages are never lost but may be redelivered. (至少一次传递消息,kafka会确保消息一定送达,它可以重新传递消息)
  • Exactly once---this is what people actually want, each message is delivered once and only once.(恰好一次 kafka认为能够保证消息一定能否完成的进行传递。这种是理想状态)

以上问题需要从两个方面进行讨论:

1、发布消息的持久性保证 2、消费消息时的保证

我们来看看 kafak 时如何保证消息的持久性的吧!

从生产者角度来看

kafka 发送消息时,消息被提交到日志中,一旦提交了已发布的消息,只要有分区去复制消息就意味着消息将不会丢失。但是,有一个问题,无法确定错误时在消息提交之前发送还是消息提交之后。

  • Kafka的语义是比较直观的。当发布一条消息时,有一个概念,即消息被"提交"到日志中。一旦消息被提交,只要复制该消息的分区的至少一个代理(broker)仍然处于"alive"状态,该消息就不会丢失。

那么如何解决这个就是一个问题,在0.11.0.0 版本之前,kafka使用重新发送来解决消息丢失等问题。

  • 这里提供了至少一次的消息传递语义

但是,这样也会有新的问题出现就是消息的重复插入,因为有可能原始请求实际已经成功,那么重新发送的消息则会再次写入日志中。kafka 在0.11.0.0 版本之后(包括该版本)支持幂等插入,通常是为消息附加一个 id 值,并使用该 id 来删除重复的消息。或者使用事务来进行处理

  • 幂等操作
  • 事务处理

从消费者角度来看

所有的副本都是具有完全相同日志和偏移量,那么如果有一个消费者崩溃,消费者要如何处理消息并更新消息的位置呢?

  • 读取消息,保存位置,然后处理消息
    • 读取消息:首先,消费者从Kafka主题中读取一批消息。
    • 保存位置:在读取这些消息后,消费者会记录下当前已处理的最后一条消息的位置(偏移量)。这个位置通常存储在消费者的内存中,以便知道从哪里继续处理下一批消息。
    • 处理消息:接下来,消费者开始处理这批消息,执行其业务逻辑等操作。

现在,让我们考虑一种可能的情况:如果在消费者已经读取了消息、保存了位置,但在完成消息处理之前,消费者遇到了故障或崩溃,那么在这种情况下,下一个接管消息处理任务的新进程将从上次保存的位置开始处理消息。尽管在这个位置之前的一些消息已经被读取,但由于尚未完成处理,这些消息可能不会被标记为已处理。

  • 读取消息,处理消息,然后保存位置
    • 读取消息:首先,消费者从Kafka主题中读取一批消息。
    • 处理消息:接下来,消费者开始处理这批消息,执行其业务逻辑等操作。
    • 保存位置:在消息处理完成后,消费者会记录下当前已处理的最后一条消息的位置(偏移量)。这个位置通常存储在消费者的内存中,以便知道从哪里继续处理下一批消息

现在,让我们考虑一种可能的情况:如果在消费者已经读取了消息并成功处理完它们后,但在保存处理位置之前,消费者遇到了故障或崩溃,那么在这种情况下,下一个接管消息处理任务的新进程将重新处理从上次保存的位置开始的消息。这意味着一些已经被成功处理的消息可能会被再次处理。

以上两种情况对应于"至多一次"语义,消息将会被重新发送。这种处理方式通常用于那些可以容忍偶尔处理重复消息的应用场景,例如在消息处理操作是幂等的情况下。幂等意味着多次应用相同的操作不会产生不同的结果,因此重复处理相同的消息不会引发问题。但需要注意,对于某些应用场景,特别是需要严格的消息处理语义的情况,可能需要使用其他处理方式

那如果时恰好一次语义,可以通过Kafka 0.11.0.0版本中引入的事务性生产者功能。消费者的位置信息存储为一条消息写入Kafka的方式,因此可以将偏移量与处理后的数据一起在同一事务中写入Kafka。如果事务中止,消费者的位置将回滚到旧值,同时输出主题上的数据对其他消费者不可见,这取决于它们的"隔离级别"。在默认的"read_uncommitted"隔离级别下,即使消息属于已中止的事务,所有消息对消费者都是可见的,但在"read_committed"下,消费者将只返回已提交的事务中的消息(以及不属于事务的消息)。

将数据从Kafka消费者写入到外部系统时,面临的一些协调和一致性的问题。例如,在将数据从Kafka主题读取后,消费者需要将这些数据写入外部系统,比如HDFS或其他存储系统。此时面临的问题是如何协调消费者当前的处理位置(偏移量)与将数据写入外部系统的存储位置之间的一致性。

  • 传统的两阶段提交:一种传统的方法是引入两阶段提交,其中第一阶段涉及将数据写入外部系统,第二阶段涉及将消费者的位置(偏移量)进行更新。这确保了数据写入和位置的更新是原子性的,要么都成功,要么都失败。但这种方法可能在某些情况下变得复杂,尤其是当外部系统不支持两阶段提交时。
  • 更简单的方法:更简单和通用的方法是将消费者的偏移量与输出数据存储在同一位置。这意味着消费者在将数据写入外部系统的同时,也将自己的处理位置(偏移量)写入相同的位置。这样做的好处是许多外部系统都不支持复杂的两阶段提交,而这种方法避免了这种复杂性。

复制机制

Kafka允许将每个主题的分区的日志复制到可配置数量的服务器上(可以在每个主题的基础上设置复制因子,默认复制因子为1)。这样可以在服务器集群中的节点发生故障时,自动切换到这些副本,以确保消息在故障发生时仍然可用。

在Kafka中,复制的基本单位是主题分区。在正常情况下,每个分区都有一个单一的领导者(leader)和零个或多个追随者(follower)。复制因子包括领导者在内的所有副本。所有写操作都发送到分区的领导者,而读操作可以发送到分区的领导者或追随者。

追随者可以像读取kafka的消费者一样消费来自领导的消息,并将其应用到自己的日志。这样做的好处就是让追随者从领导者那里拉取有一个很好的特性,即允许追随者自然地将他们应用于其日志的日志条目分批在一起。

kafka通过一个名为"控制器"的特殊节点来负责管理集群中broker(代理)的注册。而且broker需要满足以下条件:

  • 代理必须与控制器保持活动会话,以便定期接收元数据更新。 作为追随者的经纪人必须复制领导者的写入,并且不能落后"太远"。
  • "活动会话"的含义取决于集群配置。
    • 对于 KRaft 集群,通过向控制器发送定期心跳来维护活动会话。如果控制器在broker.session.timeout.ms配置的超时时间到期之前未能收到心跳,则该节点被视为离线。
    • 对于使用 Zookeeper 的集群,活动性是通过临时节点的存在来间接确定的,临时节点是由代理在其 Zookeeper 会话初始化时创建的。如果代理在 Zookeeper.session.timeout.ms 到期之前未能向 Zookeeper 发送心跳后丢失会话,则该节点将被删除。然后,控制器将通过 Zookeeper 监视注意到节点删除,并将代理标记为离线。

我们将满足这两个条件的节点称为"同步",以避免"活动"或"失败"的模糊性。

同步副本(ISR):满足上述两个存活性条件的节点被称为"同步副本"(ISR)。领导者会跟踪"同步副本"的集合,这就是ISR。如果这两个条件中的任何一个不能满足,那么代理将被从ISR中移除。例如,如果追随者失败,那么控制器将注意到会话丢失并将代理从ISR中移除。

总结:Kafka 提供的保证是,只要至少有一个同步副本始终处于活动状态,已提交的消息就不会丢失。

复制日志:仲裁、ISR 和状态机

Kafka分区的核心:Kafka中的分区实际上是一个复制日志(replicated log)。这个复制日志是分布式数据系统中的基本原语之一,可以用于实现其他基于状态机的分布式系统。

当然,在分区复制日志的时候可能存在一个问题,即领导者崩溃,我们需要从追随者中选取一个新的领导者,如果选择所需的确认数量和必须比较的日志数量来选举领导者,以确保存在重叠,那么这称为仲裁

通常别的系统会采用投票的方式进行。多数投票的缺点是,不需要多次失败就会导致没有可供选举的领导人。容忍一次故障需要三份数据,容忍两次故障需要五份数据。

Kafka采用了一种稍微不同的方法来选择其"ISR"(In-Sync Replicas)集合,这是与领导者竞选有关的。Kafka动态地维护一个与领导者同步的副本集合,只有这个集合中的成员才有资格竞选领导者。写入Kafka分区的消息只有在所有同步副本都接收到该消息后才被视为已提交。ISR集合在群集元数据中持久存在,每当它发生变化时都会进行更新。

  • Kafka的设计允许客户端选择是否阻塞消息提交的确认,以及更低的复制因子带来的额外吞吐量和磁盘空间节省是否值得。
  • Kafka的设计不要求崩溃的节点恢复所有数据。这是因为磁盘错误是实际运行中的持久性数据系统中最常见的问题,即使磁盘错误不会使数据完全丢失,也不希望要求在每次写操作上都使用fsync,因为这可能会降低性能。
  • 文中提到了Kafka在领导者选举中的一种行为,即在所有副本均失效的情况下。Kafka默认情况下会等待一个一致的副本恢复,以保持一致性,但也提供了一种不等待的方式,以支持某些需要更高可用性而愿意牺牲一致性的用例。

可用性和耐用性保证

kafka 在写入数据时,生产者会选择是否等待副本确认消息。

在Kafka中,可以通过acks参数来指定这个确认的级别,具体有以下几个选项:

  1. 0:表示不需要任何确认。生产者发送消息后不会等待任何来自副本的确认,直接将消息发送出去,这是最低的可用性,但也是最低的可靠性选项。
  2. 1:表示需要至少一个副本的确认。生产者发送消息后,会等待至少一个副本确认收到消息后才继续。这提供了一定的可靠性,但仍然具有较高的可用性。
  3. -1(或all):表示需要所有的同步副本确认。生产者发送消息后,会等待所有当前的同步副本都确认接收到消息后才继续。这提供了最高的可靠性,但可能降低可用性。

注意:

  • 默认情况下,当使用acks=all时,确认会在所有当前的同步副本都接收到消息后发生。这意味着如果某个主题配置为只有两个副本,而一个副本失效(即只剩下一个同步副本),那么使用acks=all的写入将成功。然而,如果剩下的同步副本也失败,这些写入可能会丢失。
  • 为了优先选择消息的可靠性而不是可用性,Kafka提供了两个主题级别的配置选项:
  • 禁用不干净的领导者选举:如果所有副本都不可用,那么分区将保持不可用,直到最近的领导者重新可用。这实际上更倾向于牺牲可用性来降低消息丢失的风险。
  • 指定最小的ISR大小:分区只会接受写入,如果ISR的大小高于一定的最小值,以防止消息被写入只有一个副本的情况下,该副本后来变为不可用。这个设置只会在生产者使用acks=all的情况下生效,它保证了消息将被至少这么多个同步副本确认。这个设置提供了一种一致性和可用性之间的权衡,较高的最小ISR大小可以提供更好的一致性,但会降低可用性,因为如果同步副本的数量降到最小阈值以下,分区将不可用于写入。

日志压缩

日志压缩是一种用于数据保留的机制,确保Kafka始终保留每个消息键的最新已知值,而不仅仅是按照时间或大小固定周期丢弃旧的日志数据。

通常情况下,日志数据保留可以采用两种不同的方式,一种是按照固定时间或大小丢弃旧的数据,适用于临时事件数据,如日志记录。另一种方式是日志压缩,适用于具有主键的可变数据的变更日志,例如数据库表的更改。

文本指出,如果我们保留完整的日志并尝试记录每个更改,那么对于频繁更新单个记录的系统来说,这将不切实际,因为日志会无限增长。而日志压缩允许有选择地删除具有相同主键的较早记录,以确保至少保留每个键的最后状态。

Kafka允许根据主题设置保留策略,这意味着对于某些主题,可以采用时间或大小为基础的数据保留策略,而对于其他主题,可以使用日志压缩保留策略。

日志压缩基础知识

日志的头部与传统的 Kafka 日志相同。它具有密集、连续的偏移量并保留所有消息。日志压缩添加了处理日志尾部的选项。上图显示了一根带有压紧尾部的原木。请注意,日志尾部的消息保留首次写入时分配的原始偏移量,并且永远不会改变。另请注意,即使具有该偏移量的消息已被压缩,所有偏移量仍保留在日志中的有效位置;在这种情况下,该位置与日志中出现的下一个最高偏移量无法区分。

日志压缩还支持删除操作。如果一条消息具有一个键(key)和一个空的有效载荷(payload),则它将被视为从日志中删除。这样的记录有时被称为墓碑(tombstone)。这个删除标记将导致任何具有相同键的先前消息被删除(以及任何具有相同键的新消息),但删除标记本身在一段时间后会被清理出日志,以释放空间。不再保留删除操作的时间点被标记为上图中的"删除保留点"。

压缩操作是在后台进行的,定期复制日志段。清理操作不会阻塞读取,并且可以通过限制使用不超过可配置的I/O吞吐量来进行节流,以避免影响生产者和消费者。实际的压缩日志段过程类似于文本中所描述的步骤。

kafka 实施

Kafka的网络层实现:使用了Java的NIO技术来处理网络通信。

Kafka网络层的线程模型。它包括一个单独的接收器线程(acceptor thread)和N个处理器线程(processor threads)。这些处理器线程负责处理一定数量的连接。这种设计在其他地方已经经过充分的测试,被证明实现简单且快速。

Kafka消息格式和记录批次的实现细节:

Messages(消息)

消息由可变长度标头、可变长度不透明密钥字节数组和可变长度不透明值字节数组组成。

消息(又名记录)始终是批量写入的。一批消息的技术术语是记录批次,记录批次包含一条或多条记录。

Kafka中消息的存储和索引方式:

  1. 日志(Log)和分区(Partitions):Kafka中的每个主题(Topic)可以被分成多个分区,每个分区都有一个独立的日志。例如,描述的是一个名为"my-topic"的主题,该主题有两个分区,分别命名为"my-topic-0"和"my-topic-1"。
  2. 日志文件的格式:Kafka的日志文件包含一系列的"日志条目"(Log Entries),每个日志条目由一个4字节整数N表示消息长度,后跟着N字节的消息内容组成。这是一种用于在磁盘上持久化存储消息的格式。
  3. 消息的唯一标识:每个消息都通过一个64位整数偏移量(Offset)唯一标识,该偏移量表示该消息在所有曾经发送到该主题分区的消息流中的字节位置。这个偏移量是消息的唯一身份标识符。
  4. 日志文件命名:每个日志文件的名称都以它所包含的第一个消息的偏移量命名。例如,第一个文件可能被命名为"00000000000000000000.log",而后续的文件名会依次递增。
  5. 消息记录的二进制格式:这段文字提到了消息记录(Records)的二进制格式。消息记录也被称为"记录批次"(Record Batches),它们包含一组消息。这些记录的格式是版本化的,并且用于在生产者、代理(broker)和客户端之间传输,无需在传输时进行复制或转换。
  6. 消息偏移量作为消息ID:这里提到了一个不寻常的设计决策,即将消息的偏移量作为消息的唯一标识符。最初的想法是使用生产者生成的全局唯一标识符(GUID),并在每个代理上维护GUID到偏移量的映射。然而,这种全局唯一性在消费者端不提供价值,因为每个服务器上都必须维护一个ID。此外,从随机ID到偏移量的映射会引入复杂的索引结构,需要与磁盘同步,从而需要一个重量级的持久性随机访问数据结构。因此,为了简化查找结构,决定使用每个分区的原子计数器,它可以与分区ID和节点ID结合使用,从而唯一标识消息。这使查找结构更简单,尽管每个消费者请求仍然可能需要多次查找。然而,由于偏移量对于消费者API是隐藏的,这个决策最终是一个实现细节,而且它采用了更高效的方法。

写入(Writes):

  1. 序列化追加(Serial Appends):Kafka允许序列化地追加消息到日志中,这些消息总是追加到最后一个文件。当日志文件达到一个可配置的大小限制(例如1GB)时,会触发文件滚动(Roll Over)操作,生成一个新的文件。
  2. 配置参数:Kafka的日志可以通过两个配置参数来进行控制。参数M表示在强制将文件刷新到磁盘之前要写入的消息数量,而参数S表示在多少秒后强制刷新。这两个参数一起提供了在系统崩溃时最多可能丢失的消息数量或时间的持久性保证。

读取(Reads):

  1. 读取方式:消息的读取是通过提供消息的64位逻辑偏移量和最大块大小S来完成的。这将返回一个迭代器,用于遍历包含在S字节缓冲区中的消息。S应该足够大以容纳任何单个消息,但在出现异常大的消息时,可以多次重试读取,每次将缓冲区大小加倍,直到成功读取消息。
  2. 消息和缓冲区大小限制:可以指定最大的消息大小和缓冲区大小,以便服务器拒绝大于某个大小的消息,同时为客户端提供了需要读取的最大消息的上限。通常,读取缓冲区会以不完整消息结尾,但这可以通过消息大小限制来检测。
  3. 读取过程:实际从偏移量读取数据的过程包括首先定位存储数据的日志段文件,然后计算全局偏移值的文件特定偏移量,最后从文件偏移量处读取数据。搜索操作是通过针对每个文件维护的内存范围的简单二进制搜索变体来执行的。

删除(Deletes):

  1. 数据删除:数据以一个日志段(log segment)为单位进行删除。日志管理器(log manager)使用两个指标来确定哪些段可以删除:时间和大小。对于基于时间的策略,会考虑记录的时间戳,具有最大时间戳的段文件(记录的顺序不相关)定义了整个段的保留时间。默认情况下,大小限制策略是禁用的。当启用时,日志管理器会持续删除最旧的段文件,直到分区的总大小再次在配置的限制范围内。如果同时启用了这两种策略,那么满足任一策略的段都将被删除。
  2. 读取锁定:为了在允许修改段列表的同时避免锁定读取,Kafka使用了一种写时复制(copy-on-write)风格的段列表实现。这提供了一致的视图,允许在不变的静态快照视图上执行二进制搜索,而删除操作仍然在进行中。

保证(Guarantees):

  1. 配置参数M:日志提供了一个配置参数M,用于控制在强制刷新到磁盘之前写入的最大消息数量。
  2. 日志恢复:在启动时,Kafka执行日志恢复过程,遍历最新日志段中的所有消息,并验证每个消息条目是否有效。消息条目有效的条件是其大小和偏移量的总和小于文件的长度,并且消息负载的CRC32与消息中存储的CRC匹配。如果检测到损坏,日志将被截断到最后一个有效偏移量。
  3. 处理不同类型的损坏:需要处理两种不同类型的损坏情况,一种是截断,即由于崩溃导致未写入的块丢失,另一种是损坏,即将无意义的块添加到文件中。CRC用于检测并防止损坏,但未写入的消息当然会丢失。

消费者偏移量跟踪

  1. 偏移量提交:Kafka 消费者追踪每个分区中已经消费的消息的最大偏移量,并且有能力提交这些偏移量,以便在重新启动时从这些偏移量处继续消费。消费者可以选择将属于同一个消费者组的所有偏移量存储在一个名为 "group coordinator" 的特定代理(broker)中。消费者组中的任何消费者实例都应该将其偏移量提交和获取发送到该组协调器(代理)。消费者组的分配是基于它们的组名称进行的。消费者可以通过向任何 Kafka 代理发出 "FindCoordinatorRequest" 并读取 "FindCoordinatorResponse" 来查找它的协调器,该响应会包含协调器的详细信息。然后,消费者可以继续向协调器代理提交或获取偏移量。如果协调器移动,消费者需要重新发现协调器。偏移量的提交可以由消费者实例自动或手动完成。
  2. 偏移量提交的过程:当协调器代理接收到 "OffsetCommitRequest" 时,它将请求追加到一个名为 "__consumer_offsets" 的特殊压缩的 Kafka 主题中。仅当偏移量的所有副本都接收到偏移量后,代理才会向消费者发送成功的偏移量提交响应。如果偏移量在可配置的超时内未能复制,偏移量提交将失败,消费者可以在退避后重试提交。代理定期压缩偏移量主题,因为它只需要维护每个

写在最后: 有些乱,后面有空的时候再去啃啃kafka,再整理和更新一下文章

相关推荐
315356691319 分钟前
我开源了一套springboot3快速开发模板
后端·github
我崽不熬夜1 小时前
为什么Java中的设计模式会让你的代码更优雅?
java·后端·设计模式
先做个垃圾出来………1 小时前
简单的 Flask 后端应用
后端·python·flask
音元系统1 小时前
项目开发中途遇到困难的解决方案
后端·目标跟踪·中间件·服务发现
丘山子2 小时前
判断 Python 代码是否由 LLM 生成的几个小技巧
后端·python·面试
LaoZhangAI2 小时前
2025全面评测:Flux AI图像生成器6大模型全解析【专业测评】
前端·后端
豌豆花下猫2 小时前
Python 潮流周刊#107:无 GIL Python 被正式批准(摘要)
后端·python·ai
发愤图强的羔羊2 小时前
SpringBoot + Beetl 实现动态数据库DDL
后端
我崽不熬夜2 小时前
从基础到精通:探索 Object 类的 5 个关键方法!
java·后端·java ee
椒哥2 小时前
爬虫框架playwright使用小技巧
后端·爬虫·python