深入学习Kafka-学习笔记

196 阅读32分钟

Kafka 学习

本文内容主要来自《深入理解Kafka》一书,部分内容参考自 Kafka中文文档

Kafka定位:分布式流处理平台,高吞吐,可持久化,可水平扩展,支持流数据处理

Kafka快的原因:

  • 消费快:顺序读写 + OS Cache
  • 读消息时的“零拷贝”(sendfile,page cache direct to socket buffer
  • Topic分区(partition)分段(segment) + index稀疏索引
  • 消息批量发送与拉取
  • 数据压缩:Producer 可将数据压缩后发送给 broker,从而减少网络传输代价

Kafka 中 ZooKeeper 的作用

管理元数据

  • 如topic,partition信息等

注册中心

  • 保存 broker 的相关信息:broker 在启动时,会通过在 /brokers/ids/ 下用逻辑broker id创建一个znode来注册它自己。尝试注册一个已存在的 broker id 将会出错。由于broker在Zookeeper中用的是临时znode来注册,因此如果broker关闭或宕机,节点将消失(通知consumer不再可用)。

  • 每个broker在它自己的topic下注册,维护和存储该topic分区的数据。

  • topic的consumer也在zookeeper中注册自己,以便相互协调和平衡数据的消耗。

  • 除了由所有consumer共享的group_id,每个consumer都有一个临时且唯一的consumer_id(主机名的形式:uuid)用于识别。组中的每个consumer用consumer_id注册znode。这个id只是用来识别在组里目前活跃的consumer,这是个临时节点,如果consumer在处理中挂掉,它就会消失。

随着Kafka版本的升级迭代,ZooKeeper在Kafa中的作用正在变弱,Kafka官方的终极目标是使Kafka不再依赖第三方组件运行,以降低复杂度和使用难度。从 2.8.0 版本开始,Kafka可以在修改配置后完全不使用ZooKeeper运行Kafka集群,而是使用提供的 KRaft (可以理解为特殊角色的broker)进行集群管理,不过由于此功能还不完善,这个版本还是默认使用ZooKeeper。

同时要注意的是,不同版本的Kafka消息格式不尽相同,如果升级版本的话务必要做版本兼容测试。

Kafka 的基本概念

分区(Partition)

分区在 Kafka 中用于保存消息,同一 Topic 下的不同分区保存的消息是不同的,分区在存储层面可以看做一个可追加(顺序写)的日志文件。对于 Kafka 中的分区来说,它的每条消息都有唯一的 offset,用来表示消息在分区中的对应位置。由于分区间写入数据的不同,它们锁对应的offset值也不同。

kafka 的一个Topic内分区offset结构

Kafka 中的分区可以分布在不同的 broker 上,也就是说,一个主题可以横跨多个 broker,Kafka 根据 分区规则 选择消息存储到哪个具体的分区(类似多线程处理),以此来提供比单个 broker 更强大的性能。与之相似的,RocketMQ 的一个Topic同样包含一个或多个队列(RocketMQ称之为queue,含义与partition一样)可以在创建或变更Topic时修改,不支持自定义修改。

分区的副本(Replica)

img

为了保证高可用,Kafka 采用主从策略,为每个分区创建了一主(Leader)多从(Replica)的多个分区(每个分区都是一个副本,总的副本数是包含 leader 的总和),生产者根据消息的topic和key值,确定了消息要发往哪个分区(partition)之后(假设是p1),会找到partition对应的leader 副本(也就是broker2里的p1),然后将消息发给leader副本,leader副本负责消息的读写,而 follower 副本则从 leader 副本批量拉取消息日志到自己的日志文件。follower 副本不提供消息的读写,并且消息可能存在一定的滞后。

分区中的所有副本统称为 AR(Assigned Replicas),由于 follower 副本的数据是在 leader 收到消息后从 leader 中拉取的,这样同步期间 follower 副本的数据可能会有一定的滞后性。这里,所有与 leader 副本保持一定程度同步的副本(指可以承受的滞后范围,这个范围可以由参数配置)组成 ISR(In-Sync Replicas),与 leader 同步滞后过多的副本称为 OSR(Out-of-Sync Replicas)。AR=ISR+OSR(ISR 包含leader 副本)。默认只有 ISR 中的副本才可能成为新的 leader。 Leader会追踪所有 “in sync” 的节点。如果有节点挂掉了, 或是写超时, 或是心跳超时, leader 就会把它从同步副本列表中移除。 同步超时和写超时的时间由 replica.lag.time.max.ms 配置确定。

故障转移:一旦某一个partition的leader挂掉了,那么只需从活着的 ISR 中提拔一个replica出来,让它成为leader即可,系统依旧可以正常运行。与大多数分布式系统一样,自动处理故障需要精确定义节点 “alive” 的概念。Kafka 判断节点是否存活有两种方式。

  1. 节点必须可以维护和 ZooKeeper 的连接,Zookeeper 通过心跳机制检查每个节点的连接。
  2. 如果节点是个 follower ,它必须能及时的同步 leader 的写操作,并且延时不能太久。

只有当消息被所有的 ISR副本 节点加入到日志中时, 才算是提交, 只有提交的消息才会被 consumer 消费, 这样就不用担心一旦 leader 挂掉了消息会丢失。另一方面, producer 也 可以选择是否等待消息被提交,这取决他们的设置在延迟时间和持久性之间的权衡,这个选项是由 producer 使用的 acks 设置控制(见“消息发送的确认”部分)。 请注意,Topic 可以设置同步备份的最小数量, producer 请求确认消息是否被写入到所有的备份时, 可以用最小同步数量判断。如果 producer 对同步的备份数没有严格的要求,即使同步的备份数量低于 最小同步数量(例如,仅仅只有 leader 同步了数据),消息也会被提交,然后被消费。

消费者组**(Consumer Group)**

每个消费者都有一个对应的消费者组,在同一组中的每个consumer共享一个group_id,消费者组中的消费者遵循下面的规则(核心):

  • 同一个组的Consumer不能消费同一个分区(意味着同一个topic)的数据
  • 同一个分区的数据可以被订阅了这个 topic 的其他消费者组的Consumer消费

Kafka使用Consumer Group实现了两种模型:如果所有实例都属于同一个Group,那么它实现的就是消息队列模型;如果分别属于不同模型,那么实现的就是发布/订阅模型

理想情况下,Consumer实例的数量应该等于该Group订阅topic的分区数量(即消费者组内的每个Consumer各自对应一个topic的分区)。

消息队列的两种模式

点对点模式(Point-to-Point)

  • 如果在一个 topic 中,所有消费者都隶属于同一个消费者组,那么消息会被均衡的投递给每个消费者,每条消息都只会被一个消费者处理
  • 消费者消费数据之后,消息被清除

发布/订阅模式(Pub/Sub)

  • 如果所有消费者都隶属于不同的消费者组,那么消息会被广播给所有的消费者,即每条消息都会被所有消费者处理
  • 消费者消费数据之后,消息不会被清除
  • 由消费者主动拉取topic中的消息(基于长轮询) ,消费速度由被订阅的topic队列决定

高水位(High Watermark,简称 HW)

它标识了一个特定的 offset,这个offset值以分区为单位,消费者只能消费分区中这个 offset 之前的消息。例如:offset 为 6,则消费者只能消费 offset 为 0至 5 之间的消息,

LEO(Log End Offset)

它标识当前日志文件的下一条待写入消息的 offset,分区 ISR 集合中的每个副本都会维护自身的 LEO,最小的LEO值等于 HW(即分区中的所有ISR副本数据都同步到了最新offset值)

Kafka将下次要消费的消息offset保存在Kafka本地,默认保存七天。

Kafka 的可用性保证

Kafka 对于数据不会丢失的保证,是基于至少一个节点在保持同步状态,一旦分区上的所有备份节点都挂了,就无法保证了。

但是,假设一旦所有的备份都挂了,怎么去保证数据不会丢失,这里有两种实现的方法:

  1. 等待一个 ISR 的副本重新恢复正常服务,并选择这个副本作为领 leader (它有极大可能拥有全部数据)。
  2. 选择第一个重新恢复正常服务的副本(不一定是 ISR 中的)作为leader。

这是可用性和一致性之间的简单妥协,如果我只等待 ISR 的备份节点,那么只要 ISR 备份节点都挂了,我们的服务将一直会不可用,如果它们的数据损坏了或者丢失了,那就会是长久的宕机。另一方面,如果不是 ISR 中的节点恢复服务并且我们允许它成为 leader , 那么它的数据就是可信的来源,即使它不能保证记录了每一个已经提交的消息。 Kafka 默认选择第二种策略,当所有的 ISR 副本都挂掉时,会选择一个可能不同步的备份作为 leader ,可以配置属性 unclean.leader.election.enable 禁用此策略,那么就会使用第 一种策略即停机时间优于不同步。

在大多数投票算法当中,如果大多数服务器永久性的挂了,那么您要么选择丢失100%的数据,要么违背数据的一致性选择一个存活的服务器作为数据可信的来源。

消息发送时的ack确认机制也提高了系统可用性,详见#生产者的消息发送#部分

消息数据的物理存储

Kafka在参数 log.dir 指定的目录下,会创建名称格式为【主题名字-分区名】的文件夹。文件夹内所有文件均以log文件中保存的第一条消息所对应的offset值减 1作为文件名,数值最大为64位long大小,19位数字字符长度,没有数字就用 0 填充,只有文件扩展名不同。log文件的切分时机由大小参数log.segment.bytes(默认值1G)和时间参数log.roll.hours(默认值7天)共同决定。文件夹中的文件及作用如下:

  • .log 文件:保存 序列化后的消息内容
  • .index 文件:保存消息在分区中的offset值 和 消息在.log文件中的偏移量
  • .timeindex 文件:时间索引文件
  • .snapshot:Kafka对幂等型或者事务型producer所生成的快照文件
  • leader-epoch-checkpoint:格式为 (leader 版本 offset),offset 是每一代 leader 写入的第一条消息的位移值

日志中有两个配置参数: M 是在 OS 强制写文件到磁盘之前的消息条数, S 是强制写盘的秒数.这提供了一个在系统崩溃时最多丢失 M 条或者 S 秒消息的保证.

一组文件名相同的 log、index 和 timeindex 文件称为一个 segment ,即每个分区又由多个 segment 操作,这样设计的优点是:当执行消息清理操作时,直接删除较老的 segment 文件即可。

index 文件的搜索是使用二分查找法查找在内存中保存的每个文件的偏移量来完成的。通过index 文件中的索引信息可以快速定位到message。通过index元数据全部映射到memory,可以避免segment file的IO磁盘操作;通过索引文件稀疏存储(不是为每条数据都创建索引,而是某一区间的数据只创建一个索引,稀疏索引的粒度由log.index.interval.bytes参数来决定,默认为4KB),可以大幅降低index文件元数据占用空间大小。

消息的 index 文件物理结构如下:

长度(byte)参数含义
4消息在分区中的offset值(文件名 offset 值+当前保存的 offset 值)
4消息在 .log 物理文件中的偏移量

消息的 log 文件物理结构如下:

长度(byte)参数含义备注
8offsetmessage在partition的位置
4message size消息大小
4CRC32循环冗余校验
1"magic"Kafka服务程序协议版本号
1"attributes"表示为独立版本、或标识压缩类型、或编码类型
4key length表示key的长度,当key为一1时, K byte key字段不填
Kkey实际消息的 key,可能不存在
4payload length表示消费消息长度
valuepayload实际消息数据

消息的分区策略

生产者发送的消息会被包装为一个ProducerRecord对象,该对象的成员变量及完整的构造方法如下(分区策略参考文章):

public class ProducerRecord<K, V> {
    private final String topic;
    // 对应broker中的分区号(分区号的数据类型只能为整型,broker配置文件中也声明了必须为整型)
    private final Integer partition;
    // 消息的头,大多用来设定与应用相关的信息,可以不设置
    private final Headers headers;
    // 消息的键,主要用来计算消息要发往的分区号,同一个 key 的消息会被发往相同分区;
    // 有key 的消息支持日志压缩
    private final K key;
    // 消息的正文,如果为空则表示特定的消息:墓碑消息
    private final V value;
    private final Long timestamp;
    
    public ProducerRecord(@NotNull String topic, Integer partition,
                          Long timestamp, String key, String value,
                          @Nullable Iterable<Header> headers) {
        //省略赋值过程...
    }
}

生产者在将消息发送到某个Topic ,需要经过拦截器(修改消息内容、过滤消息、统计发送结果)、序列化器和分区器(Partitioner)的一系列作用之后才能发送到对应的Broker,在发往Broker之前是需要确定它所发往的分区。

  1. 如果消息 ProducerRecord 指定了partition字段,则使用指定的分区号,而不使用分区器。

  2. 如果消息 ProducerRecord 没有指定 partition 字段,并且消息的key不为空,则利用分区器进行分区的选择 。Kafka 的默认分区器使用称之为murmur的Hash算法(非加密型Hash函数,具备高运算性能及低碰撞率)来计算分区分配。将hash值与topic可用的分区总数取余得到partition值。

  3. 如果既没有指定分区,且消息的key也是空,则用轮询的方式选择一个分区:第一次调用时生成一个随机整数(之后每次调用都对该数+1),将这个值与topic可用的分区总数取余得到partition值。

生产者的整体架构

img

整个生产者客户端由两个线程协调运行,这两个线程分别为主线程和Sender线程(发送线程)。在主线程中由KafkaProducer创建消息,然后通过可能的拦截器(Interceptor)、序列化器(Serializer)和分区器(Partitioner)的作用之后缓存到消息累加器(RecordAccumulator,也称为消息收集器)中。Sender线程负责从RecordAccumulator中获取消息并将其发送到Kafka中。

RecordAccumulator主要用来缓存消息以便Sender线程可以批量发送,进而减少网络传输的资源消耗提升性能。RecordAccumulator缓存的大小可以通过生产者客户端参数buffer.memory的配置,默认值为32MB。如果生产者发送消息的速度超过发送到服务器的速度,则会导致生产者空间不足,这个时候KafkaProducer的send()方法调用要么被阻塞,要么抛出异常,这个取决于参数max.block.ms的配置,此参数的默认值为60000,即60秒。

主线程中发送过来的消息都会被追回到RecordAccumulator的某个双端队列(Deque)中,在RecordAccumulator的内部为每个分区都维护了一个双端队列,队列中的内容就是ProducerBatch(ProducerBatch是指一个消息批次,ProducerRecord会被包含在ProducerBatch中,这样可以减少网络请求的次数以提升整体的吞量),即 Deque<ProducerBatch>

private final ConcurrentMap<TopicPartition, Deque<ProducerBatch>> batches;

从上述代码中可知使用了一个线程安全的ConcurrentMap来维护着每个主题分区的消息队列。消息写入缓存时,追回到双端队列的尾部;Sender读取消息时,从双端队列的头部读取。

消息的发送

生产者直接发送数据到主分区的服务器上,不需要经过任何中间路由。 为了让生产者实现这个功能,所有的 Kafka 服务器节点都能响应这样的元数据请求: 哪些服务器是活着的,主题的哪些分区是主分区,分配在哪个服务器上。

批处理是提升性能的一个主要驱动,为了允许批量处理,Kafka 生产者会尝试在内存中汇总数据,并用一次请求批次提交信息。 批处理,不仅仅可以配置指定的消息数量,也可以指定等待特定的延迟时间(如 64k10ms),这允许汇总更多的数据后再发送,在服务器端也会减少更多的IO操作。 该缓冲是可配置的。

消息发送的三种模式

  • 发后即忘:向 broker 发送消息而不关心消息是否正确到达,这种模式在某些情况下(比如发生不可重试异常时)会丢失消息,性能最高,可靠性最差

  • 同步发送:生产者发送消息并阻塞式的等待发送结果,程序收到发送结果后才能继续向下执行。消息要么发送成功,要么抛出异常。

  • 异步发送:一般是在 send() 方法里制定一个 Callback 的回调函数,Kafka 在返回响应时调用该函数来实现异步的发送确认。

消息发送的确认

向 Kafka 写数据时,producers 会发送 ack 是否提交完成,ack 请求中会携带acks参数,broker 根据参数值决定何时返回 发送成功 的响应(参数值为字符串类型,不是整型)

  • acks = 0:不等待broker返回确认消息
  • acks = 1:leader节点保存成功就返回
  • acks = -1(all):所有备份都保存成功返回。请注意. 设置 “ack = all” 并不能保证所有的副本都写入了消息。默认情况下,当 acks = all 时,只要 ISR 副本同步完成,就会返回消息已经写入。

可能产生的异常

Kafka 中的生产者一般会发生两种异常:可重试的异常和不可重试的异常。对于可重试的异常,如果配置了 retries 参数,那么只要在规定重试次数中恢复了,则不会抛出异常(超出重试次数同样会抛出异常),retries 的默认次数为 0 。

请注意,允许重试而不将 MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION 设置为 1 可能会更改记录的顺序,因为如果将两个批次发送到单个分区,并且第一个批次失败并重试,但第二个批次成功,则第二个批次中的记录可能会在第一个批次前面。

另外请注意,如果在成功确认之前由 DELIVERY_TIMEOUT_MS_CONFIG 配置的超时首先到期,则在重试次数用完之前,produce请求将失败。 通常不需要设置 retries 参数,而是使用 DELIVERY_TIMEOUT_MS_CONFIG 来控制重试行为。

调用 send() 返回后报告成功或失败的时间上限。 这限制了记录在发送之前延迟的总时间、等待broker确认的时间(如果可预期的话)以及允许重试发送失败的时间。 如果遇到不可恢复的错误、重试次数已用尽或者消息被添加到过期批次中,生产者可能会报异常。此配置的值应大于或等于 request.timeout.mslinger.ms 时间之和

生产者拦截器

生产者拦截器可以用来在消息发送前做一些准备工作,比如按照某个规则过滤不需要的消息、修改消息内容等,也可以用来在发送消息前做一些定制化操作,比如统计类工作等。生产者拦截器需要实现 org.apache.Kafka.clients.producer.ProducerInterceptor 接口,接口中定义的方法如下:

    // 在将消息序列化和计算分区前调用,一般来说不要修改 topic、key 和 partition 的值,否则可能会与预想的结果出现偏差
    ProducerRecord<K, V> onSend(ProducerRecord<K, V> var1);

    // 本方法在收到消息应答之前或消息发送失败时调用,优先于用户定义的 Callback 之前执行。
    // 这个方法运行在 Producer 的 I/O 线程中,所以这个方法执行的越快越好,否则可能会影响消息发送速度
    void onAcknowledgement(RecordMetadata var1, Exception var2);

		// 主要用于在关闭拦截器时执行一些资源的清理工作
    void close();

在这三个方法中抛出的异常都会被 catch 并记录到日志中,并且不会向上抛出。

消息的消费

Kafka 中的消费基于拉模式(消费者主动向服务端请求拉取消息)。pull 与 push 模式的权衡

Kafka 中的消息消费是个不断轮询(重复调用 poll() 方法 )的过程,消费者通过向 broker 发出一个“fetch”请求来获取它想要消费的 partition。consumer 的每个请求都在 log 中指定了对应的 offset,并接收从该位置开始的一个消息集合。对于 poll() 方法而言,如果分区中没有可供消费的消息,则返回空的消息集合。poll() 方法的定义如下:

// 如果消费者缓冲区中没有数据则阻塞式等待,直到超时为止
public ConsumerRecords<K,V> poll(final Duration timeout)

Kafka 的 Consumer 可以使用 pauser()resume() 方法来分别实现暂停某些分区在拉取操作时返回数据给客户端 和 恢复某些分区向客户端返回数据的操作,还提供了提供了一个无参的 paused() 方法来返回被暂停的分区集合。

offset

对于消费者而言,每个消费者都有一个 offset,用来表示当前消费的分区中某个消息所在的位置。在每次调用 poll() 方法时,为了保证消息不会被重复消费,就必须记录上一次消费时的消息 offset,并且必须持久化保存。在新的消费者客户端中,offset 保存在 Kafka 内部的主题 __consumer_offsets 中。消费者在消费完消息之后需要执行消费位移的持久化保存。消费者在消费消息时需要提交的 offset 值应该是“下次要拉取的消息的位置“,即 lastConsumedOffset+1,broker 在本地保存的 offset 也是这个值(存疑)。

每当消费者查不到所记录的 offset 时,或者是 offset 位移越界时,就会根据消费者客户端参数 auto.offset.reset 的配置来决定从何处开始进行消费,这个参数的默认值为 latest,表示从分区末尾开始消费(即忽略分区中已有的消息);如果参数值配置为 earliest,那么消费者会从分区起始处,即从 0 开始消费;如果参数值为”none“,则抛出 NoOffsetForPartitionException 异常。

Kafka 支持从分区的指定 offset 开始消费,但是只能对消费者已经分配到的分区执行这种操作。

消息消费的确认/位移提交

消费者位移提交的时机把握很有讲究,有可能会造成重复消费或消息丢失的现象。思考两个典型场景:如果在拉取到消息A之后就进行消费位移的提交,这个时候消费者可能会出现异常导致消息A实际并没有消费,在故障恢复后,重新拉取的消息不会包含前面已经提交过的消息A,也就是消息丢失; 另外一种情况是,消费位移提交是在消费完所有拉取到的消息(B)之后才进行,当消费者消费了部分拉取到的消息后遇到了异常,在故障恢复后,重新拉取的消息会包含前面已经消费过的消息B,也就是说,消息B又重新消费了一遍,造成了重复消费。

在kafka中默认的消费位移提交方式为 自动提交 ,这个由参数 enable.auto.commit 配置,默认为true。这个默认的自动提交是定期提交,定期提交的周期由 auto.commit.interval.ms 配置,默认为5秒,参数生效的前提是 enable.auto.commit 为true。在默认方式下,消费者每隔5秒会将拉取到的每个分区中最大的消费位移进行提交,在每次真正向服务器发起拉取请求之前会检查是否可以进行位移提交,如果可以,那么就会提交上一次轮询的位移。

自动位移提交无法做到精确的位移管理,这个时候可以使用 手动提交,手动提交需要将参数 enable.auto.commit 配置为false。手动提交可以细分为 同步提交 和 异步提交 两种,对应KafkaConsumer中的 commitSync()commitAsync() 。同步提交时,线程会阻塞等待提交结果的返回,这会造成程序的吞吐量降低,并且同步提交支持自动失败重试。异步提交时线程不会被阻塞,可能在提交位移的结果返回前就开始了新一次的拉取操作,并且无法自动失败重试。额外的,异步提交还有回调函数(callback)来实现提交操作后的逻辑,如异常处理和日志记录。

Kafka也支持更细粒度、更精准的提交,即 在提交方法中包含分区和offset值等相关信息,同步异步均有相关实现。

再均衡

再均衡(Rebalance)是指分区的所属权从一个消费者转移到另一个消费者的行为,它为消费者组的高可用性和伸缩性提供保障。在再均衡期间,消费者组内的消费者无法读取消息(即:消费者组不可用)。另外,当一个分区被重新分配给另一个消费者时,消费者当前的状态也会丢失,比如:消费者没来得及提交的消息 offset 值,这会导致消息的重复消费。一般情况下,应尽量避免再均衡的发生。

消费者拦截器

消费者拦截器主要在消费到消息或在提交消费位移时进行一些定制化的操作。消费者拦截器需要实现 org.apache.Kafka.clients.consumer.ConsumerInterceptor 接口,这个接口的定义如下:

    // 消息被消费前调用本方法,如果本方法抛出异常,那么异常会被记录到日志,并且不会再向上抛出
    ConsumerRecords<K, V> onConsume(ConsumerRecords<K, V> var1);

    // 提交完消息 offset 值后调用本方法,可以使用本方法来追踪所提交的消息 offset 值
    void onCommit(Map<TopicPartition, OffsetAndMetadata> var1);

    // interceptor关闭时调用
    void close();

消息的多线程消费

大概可以分成两种模式:一个线程拉取多个线程消费 和 多个线程按分区拉取并各自消费

待完善todo,暂时 参考

事务消息

todo 暂时参考 这里

Topic 的管理

Kafka 主题的管理主要使用 Kafka-topics.sh 脚本,其实质是调用了 Kafka.admin.TopicCommand 类来执行主题的管理操作。同时,还要其他方式能实现主题的管理。

Topic 的创建

如果 broker 端配置参数 auto.create.topics.enable 为 true (默认为 true),那么当生产者向一个尚未创建的topic 发送消息时,或者当一个消费者开始从未知主题中读取消息时,broker 将会自动创建一个分区数为 num.partitions (默认值为 1)、副本因子为 default.replication.factor (默认值为 1)的topic 。

Kafka 会在 log.dirlog.dirs 参数所配置的目录下创建topic分区,默认情况下这个目录为 /tmp/Kafka-logs/。

要创建一个名称为“test-topic”的主题,其命令如下: bin/Kafka-topics.sh --zookeeper localhost:2181 --create --topic test_topic --partitions 1 --replication-factor 2 --if-not-exists

要注意的点

  • 创建主题时,如果创建同名主题就会报出 TopicExistsException 异常。Kafka-topics.sh脚本提供了一个 --if-not-exists 参数,带上这个参数后如果创建重复主题则不做任何处理且不报错。
  • 创建的主题名称不应该包含 ”.“ ,因为 Kafka 在内部做埋点时会将 ”.“ 改成下划线 ”“ ,如果topic 的名称混用了 ”.“ 和 “” ,则可能会出现 topic 名称重复的问题。
  • 同样不推荐使用双下划线 "__" 主题,因为双下划线主题一般默认为是 Kafka 的内部主题
  • 主题不能为空,也不能为只有点号 “.” ,且长度不能超过 249

Topic 的查看

查看 topic 信息的一些常用命令如下:

  • list 命令查看当前所有可用 topic:bin/Kafka-topics.sh --zookeeper 127.0.0.1:2181 --list
  • describe 命令查看Kafka指定topic的分配细节:bin/Kafka-topics.sh --zookeeper 127.0.0.1:2181 --topic test_topic --describe
  • describe 命令查看Kafka所有topic的分配细节:bin/Kafka-topics.sh --zookeeper 127.0.0.1:2181 --describe
  • 显示某个消费组的消费详情(旧版本 kakfka,仅支持offset存储在zookeeper上的): bin/kafka-run-class.sh kafka.tools.ConsumerOffsetChecker --zookeeper localhost:2181 --group test
  • 显示某个消费组的消费详情(新版本 kafka,0.10.1.0版本+): bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group my-group

Topic 的修改

topic 的修改功能是由 Kafka-topics.sh 脚本中的 alter 指令提供的。

增加分区数(分区扩容)

增加分区数使用如下命令: bin/Kafka-topics.sh --zookeeper localhost:2181 --alter --topic test-topic --partition 3

需要注意的是,增加分区数会使根据消息的 key 值计算分区的行为受到影响(原本会发往分区 p1 的消息,增加分区后变成了应该发往分区 p2,详细情况参照 #生产者的分区策略# ),并且不支持减少分区

修改 topic 的配置

以下命令演示了将 topic 的 max.message.bytes 配置值修改为 20000: ``bin/Kafka-topics.sh --zookeeper localhost:2181 --alter --topic test-topic --config max.message.bytes=20000`

可以通过 delete-config 参数来删除之前覆盖的配置,使其恢复到原来的默认值,如: bin/Kafka-topics.sh --zookeeper localhost:2181 --alter --topic test-topic --delete-config max.message.bytes

Topic 的删除

要注意的是,必须将 delete.topic.enable 参数值设置为 true 才能删除主题,否则删除操作将被忽略。topic 的删除操作是不可逆的。

如果试图删除 Kafka 的内部主题,那么删除时会报错。截止 2.0.0 版本,Kafka 共包含2 个内部主题,分别为 __consumer_offsets__transaction_state。尝试删除不存在的 topic 也会报错。这里可以使用 --if-exists 参数来忽略异常。

Kafka-topics.sh 脚本中的 delete 指令可以用来删除主题,如: bin/Kafka-topics.sh --zookeeper localhost:2181 --delete --topic test-topic

Kafka-topics.sh 脚本中删除 topic 的行为本质上只是在 ZooKeeper 中的 /admin/delete_topics 路径下创建一个与待删除 topic 同名的节点,以此标记该主题为待删除状态。与创建 topic 相同的是,真正删除 topic 的动作也是由 Kafka 的控制器完成的。根据这一原理,我们可以直接通过 ZooKeeper 的客户端来删除主题。

我们还可以手动删除 topic,主题中的元数据存储在 ZooKeeper 中的 /brokers/topics 和 /config/topics 路径下,主题中的消息存储在 log/dir 或 log.dirs 参数配置的路径下,只需要手动删除这些内容即可。

kafka自带压测命令

bin/kafka-producer-perf-test.sh --topic test --num-records 100 --record-size 1 --throughput 100  --producer-props bootstrap.servers=localhost:9092

日志压缩

todo,官网参照

优雅停机

Kafka集群将自动检测到任何 broker 关机或故障,并为该机器上的分区选择新的 leader。对于配置更改而故意停机,Kafka支持更优雅的停止服务器的机制,而不仅仅是杀死它。 当一个服务器正常停止时,它将采取两种优化措施:

  1. 它将所有日志同步到磁盘,以避免在重新启动时需要进行任何日志恢复活动(即验证日志尾部的所有消息的校验和)。由于日志恢复需要时间,所以从侧面加速了重新启动操作。
  2. 它将在关闭之前将以该服务器为 leader 的任何分区迁移到其他副本。这将使 leader 角色传递更快,并将每个分区不可用的时间缩短到几毫秒。

只要服务器的停止不是通过直接杀死,同步日志就会自动发生,但控制 leader 迁移需要使用特殊的设置:

controlled.shutdown.enable=true

请注意,只有当 broker 托管的分区具有副本(即,复制因子大于1 且至少其中一个副本处于活动状态)时,对关闭的控制才会成功。 因为关闭最后一个副本会使 topic 分区不可用。

每当一个 borker 停止或崩溃时,该 borker 上的分区的leader 会转移到其他副本。这意味着,在 broker 重新启动时,默认情况下,它将只是所有分区的跟随者,这意味着它不会用于客户端的读取和写入。这里有一个首选副本的概念,详情参照 官方文档

注意事项

  • Kafka不需要太多的操作系统层面的调优,但是有两个潜在重要的操作系统级别的配置:
    • 文件描述符限制: Kafka把文件描述符用于日志段和打开连接。如果一个broker上有许多分区,则考虑broker至少 (number_of_partitions)*(partition_size/segment_size) 个文件描述符来跟踪所有的日志段和broker所创建的连接。我们推荐每一个broker一开始至少配置100000个文件描述符。

    • 最大套接字缓冲区大小:可以增加以实现数据中心之间的高性能数据传输,如此处所述。

Kafka 与 RocketMQ 的异同

不同点

  • 元数据管理:RocketMQ 的服务发现和元数据管理由 RocketMQ 自身组件完成,Kafka 则需要借助 ZooKeeper 完成,不过Kafka随着版本升级,也在倾向于依靠自身完成上述功能。
  • 分区机制:RocketMQ采用CommitLog+ConsumeQueue,单个broker所有topic在CommitLog中顺序写,Page Cache只需保持最新的页面即可。同时每个topic下的每个queue都有一个对应的ConsumeQueue文件作为索引。ConsumeQueue占用Page Cache极少,刷盘影响较小。
  • 存储机制:RocketMQ支持异步刷盘,同步刷盘,同步Replication,异步Replication。Kafka使用异步刷盘,异步Replication。
  • 延时消息:RocketMQ支持固定延时等级的延时消息,等级可配置。kfaka不支持延时消息。
  • 消息过了分类:RocketMQ执行过滤是在Broker端,支持tag过滤及自定义过滤逻辑。Kafka不支持Broker端的消息过滤,一般配置为消费单一topic数据。
  • 消费失败处理:RocketMQ通过DLQ来记录所有消费失败的消息。Kafka无DLQ。Spring等第三方工具有实现,方式为将失败消息写入一个专门的topic。
  • Kafka 的分区可以有多个副本,副本能实现对应分区的故障转移,RocketMQ 没有
  • 高可用:等等

相同点

  • Topic、Broker、Partition(Queue)、Producer、Consumer、offset含义均相同
  • 两者均利用了操作系统的 Page Cache 机制,同时通过顺序写尽可能降低随机读写,将读写控制在很小的范围内,尽可能减少缺页中断,进而减少对磁盘的访问。

消费者offset跟踪

consumer跟踪每个分区已消费的offset,并定期提交,以便在重启的情况下可以从这些offset中恢复。Kafka提供了一个选项在指定的broker中来存储所有给定的consumer组的offset,称为offset manager。例如,该consumer组的所有consumer实例向offset manager(broker)发送提交和获取offset请求。高级别的consumer将会自动处理这些过程。如果你使用低级别的consumer,你将需要手动管理offset。如果你使用简单的Scala consumer,将可拿到offset manager,并显式的提交或获取offset。对于包含offset manager的consumer可以通过发送GroupCoordinatorRequest到任意kafka broker,并接受GroupCoordinatorResponse响应,consumer可以继续向offset manager broker提交或获取offset。如果offset manager位置变动,consumer需要重新发现offset manager。如果你想手动管理你的offset,你可以看看OffsetCommitRequest 和 OffsetFetchRequest的源码是如何实现的。

当offset manager接收到一个OffsetCommitRequest,它将追加请求到一个特定的压缩名为__consumer_offsets的kafka topic中,当offset topic的所有副本接收offset之后,offset manager将发送一个提交offset成功的响应给consumer。万一offset无法在规定的时间内复制(__consumer_offsets的某些 follower 副本没有将消息成功同步到自身的对应分区),offset将提交失败,consumer在回退之后可重试该提交。broker会定期压缩offset topic,因为只需要保存每个分区最近的offset。offset manager会缓存offset在内存表中,以便offset快速获取。

当offset manager接收一个offset的获取请求,将从offset缓存中返回最新的的offset。如果offset manager刚启动或新的consumer组刚成为offset manager(成为offset topic分区的leader),则需要加载offset topic的分区到缓存中,在这种情况下,offset将获取失败,并报出OffsetsLoadInProgress异常,consumer回滚后,重试OffsetFetchRequest。

(__consumer_offsets)