Kafka 实践建议

本文详细介绍了Kafka中的分区机制、数据复制策略,包括主从复制、集群区别,以及消费者组的消费模式、广播与单播、再均衡和消费偏移量管理。重点讨论了如何通过调整分区数量、消息发送确认和手动/自动提交策略来优化性能和保证数据一致性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、分区 Partiton

在使用 Kafka 作为消息队列时,不管是发布还是订阅都需要指定主题(Topic),但这里的主题只是一个逻辑上的概念,实际上 Kafka 基本存储单元是分区( Parition )。

在一个Topic中会有一个或多个Partiton,不同的Partiton可位于不同的服务器节点上,物理上一个Partition对应于一个文件夹。Partition不能再多个服务器节点之间再细分,也不能再一台服务器的多个磁盘上再细分,所以其大小会受挂载点可用空间的限制。

Partition内包含一个或多个Segment,每个Segment又包含一个数据文件和一个与之对应的索引文件。虽然物理上最小单位是Segment,但Kafka并不提供一个Partition内不同Segment的并行处理能力

对于写操作,每次只会写Partition内的一个Segment;对于读操作,也只会顺序读取同一个Partiton内的不同Segemt。从逻辑上看,可以把一个Partition当作一个非常长的数组,使用时通过这个数组的索引(offset)访问数据。

Partition 的并行处理

  • 不同的 Partition 可位于不同的机器上,因此可以实现机器间的并行处理。
  • 一个Partiton对于一个文件夹,多个Partiton也可位于同一个服务器上,可以在同一个服务器上使用不同的Partition对应不同的磁盘,实现磁盘间的并行处理。

一般通过增加 Partition 的数量来提高系统的并行吞吐量。但不能无限增加Partition的数量,因为消费消息时还会受到消费者组的制约。由于消息数据是以Partition为单位分配的,在不考虑Rebalance时同一个Partition的数据只会被同一个消费者消费。如果消费者的数量多于Partiton的数量,就会存在部分消费者不能消费该Topic的情况,此时再增加消费者并不能提高系统的并行吞吐量。

站在生产者和 Broker 的角度,对不同 Partition 的写操作是完全并行的,可是对于消费者其并发数则取决 Partition 数量

二、复制

Kafka 还使用 ZooKeeper 实现了去中心化的集群功能,简单地讲, 其运行机制是利用 ZooKeeper 维护集群信息,每个 Broker 例都会被设置一个唯一的标识符,Broker 在启动时会通过创建临时节点的方式把自己的唯一标识符注册到 ZooKeeper 中,Kafka中的其他组件会监视 ZooKeeper 里的 /brokers/ids 路径,所以当集群中有 Broker 加入或退出时,其他组件就会收到通知。

为了让 Kafka 在集群中某些节点不能继续提供服务的情况下,集群对外依然可用,即生产者可继续发送消息 ,消费者能继续消费消息,所以需要提供一种集群间数据的复制机制。

Kafka 中是通过使用 ZooKeeper 提供的 leader 选举方式实现数据复制方案的 ,其基本原理是:    

首先选举出一个leader,其他副本作为follower,所有的写操作都写发给leader,然后再由leader把消息发给follwer。可以说复制功能 Kafka 架构的核心之一,因为它可以在个别节点不可用时还能保证 Kafka 整体的可用性。

Kafka中的复制操作也是针对分区的 。一个分区有多个副本,副本被保存在 Broker 上,每个 Broker 都可以保存上千个属于不同主题和分区的副本。

副本有两种类型: leader 副本( 每个分区都会有)和 follower 副本(除 leader 副本以外的其他副 本)。为了保证一致性,所有生产者和消费者的请求都会经过 leader。而 follower 不处理客户端 的请求,它的职责是从 leader 复制消息数据,使自己和 leader 的状态保持一致,如果 leader 节点岩机,那么某个 follower 就会被选为 leader 续对外提供服务。

主从复制和集群的区别

  • 集群是多台机器做同一件事情(高性能HPC) 或者任意一台机器处理一个事情(负载均衡LBC)
  • 主从属于高可用的范畴: 主机挂了,备用马上接替。平时只使用主机(高可用HA)

三、消息发送

1. 消息发送方式

  • 立即发送:只需要把消息发送到服务端,而不关心消息发送的结果
  • 同步发送:调用send方法发送消息后,获取该方法返回的Future结果,根据该对象的结果查看send方法是否调用成功
  • 异步发送:先注册一个回调函数,通过调用另一个 send 方法发送消息时把回调函数作为入参传入,这样当生产者接收到 Kafka 服务器的响应时会触发执行回调函数
String topic = "test-topic";
Producer<String,String> producer = new KafkaProducer<String,String>(props);
//立即发送
producer.send(new ProducerRecord(topic,"idea-key2","java-message 1"));

//同步发送
try{
    producer.send(new ProducerRecord(topic,"idea-key2","java-message 2")).get();

}catch(Exception e){
    logger.error("消息发送失败",e);

}

//异步发送
producer.send(new ProducerRecord(topic,"idea-key2","java-message 3"),new Callback(){
    @Override
    public void onCompletion(RecordMetadata metadata,Exception exception){
        if(e!=null){
            logger.error("消息发送失败",e);
        }
    
    }
});

由于 Kafka 自身的高可用性,生产者也提供了自动重试等机制,所以在大部分情况下立即发送的方式是会成功的。

通常我们需要根据实际的业务场景选择用哪种方式:

  • 如果比较关心消息发送的结果,则可以用同步发送的方式;
  • 如果除了关心消息发送的结果,还注重发送端的性能,则可以选择异步发送的方式。

当然,消息发送还需要考虑失败的情况,在 Kafka 的消息发 送过程中错误一般有两种:

  • 一种是可重试的错误,比如服务器连接错误,这种错误可通过把生产者配置成自动重试的方式来解决,如果多次重试还是不行,应用将会收到一个重试异常;
  • 另一种是不能通过重试来解决的错误,对于这种情况会直接抛出异常给生产者。

2. 消息发送确认

上面的消息发送方式是站在消息生产者的角度宏观看其消息数据发送的情况的。上面提到过消息数据是存储在分区中的,而分区又可能有 副本 ,所以一条消息被发送到 Broker 之后。何时算投递成功呢? 这里 Kafka 提供了三种模式。

  • 不等 Broker 确认 ,消息被发送出去就认为是成功。这种方式延迟最小,但是不能保证消息已经被成功投递到 Broker
  • 由 leader 确认,leader 确认接收到消息就认为投递是成功的,然后再由其他副本通过异步方式拉取。这种方式相对比较折中
  • 由所有的 leader 和 follower 都确认接收到消息才认为投递是成功 。采用这方式投递的可靠性最高,但相对会损伤性能

消息发送确认模式是通过生产者的初始化属性设置的

四、 消费者组

消费者组( Consumer Group )是 Kafka 提供的可扩展且具有容错性的消费机制,在一个消费者组内可有多个消费者,它们共享一个唯一标识,即分组 ID。组内的所有消费者协调消费它们订阅的主题下的所有分区的消息,但一个分区只能由同一个消费者组里的一个消费者来消费。

1. 广播和单播

消费者组可以用来实现 Topic 消息的广播和单擂。一个 Topic 可以有多个消费者组, Topic 的消息会被复制到所有的消费者组中, 但每个消费者组只会把消息发送给一个消费者组里的某一个消费者。

  • 如果要实现广播方式,只需为每个消费者都分配一个单独的消费者组接口
  • 如果要实现单播方式,则需要把所有的消费者都设置在同一个消费者组

2. 再均衡

  • 如果消费者组有一个新消费者加入,则新消费者读取 是原本由其他消费者读取的消息
  • 如果消费者因为某种原因离开了消费者组 ,则原本由它读取的分区消息将会由消费者组里的其 他消费者读取

这种分区所有权从一个消费者转移到另一个消费者的行为就叫作再均衡。再均衡本质上是 种协议,规定了一个消费者组下的所有消费者如何达成一致来分配主题下的每个分区。

例如某个消费者组下有 20 个消费者 ,它们订阅了一个主题有 100 个分区,在正常情况下 Kafka 会为每个消费者平均分配5个分区,这种分配的过程就是 Rebalance。

触发再均衡的场景有三种:

  • 一是消费者组内成员发生变更(例如有新消费者加入组、原来的消费者离开组 )
  • 二是订阅的主题数量发生变更(例如订阅主题时使用的是正则表达式, 此时如 果新建了某个主题正好匹配该正则表达式)
  • 三是订阅主题的分区数量发生变更

源码设计:消费者每次调用 poll 方法时都会向被指派为群组协调器 Broker 发送心跳信息,群组协调器会根据这个心跳信息判断该消费者的活性,如果超过指定时间没有收到对应的心跳信息, 群组协调器会认为该消费者己经死亡,因此会将该消费者负责的分区分派给其他消费者消费。

再均衡是一种很重要的设计,它为消费者组带来了高可用性和可伸缩性。不过, 在一般情况下并不希望它发生,因为再均衡操作影响的范围是整个消费者组,即消费者组中的所有消费者全部暂停消费直到再均衡完成,而且如果 Topic 的分区越长, 这个过程就会越慢。在实际工作中一般会通过控制发送心跳频率( heartbeat. interval.ms )和会话过期时间 session timeout.ms )来尽量避免这种情况的发生。

五、消费偏移量

从设计上来说,由于 Kafka 服务端并不保存消息的状态,所以在消费消息时就需要消费者自己去做很多事情。

消费者每次调用 poll 方法时,该方法总是返回由生产者写入 Kafka 中但还没有被消费者消费的消息。Kafka 在设计上有一不同于其他 JMS 队列的地方是生产者的消息并不需要消费者确认,而消息在分区中又都是顺序排列的,那么必然就可以通过个偏移 (offset)来确定每一条消息的位置,偏移量在消费消息的过程中起着很重要的作用。

我们把更新分区当前位置的操作叫作提交。Kafka 中有一个叫做 _consumer_offset的特殊主题又来保存消息在每个分区的偏移量,消费者每次消费时,都会往这个主体在发送消息,消息包含每个分区的偏移量。

  • 如果消费者一直处于运行状态,那么偏移量没什么用(这个分区只有自己在消费)
  • 如果消费者崩溃或者有新的消费者加入消费者组从而触发再均衡操作,再均衡之后该分区的消费者若不是之前的那个,那么新的消费者如何得知该分区的消息已经被之前的消费者消费到哪个位置了呢?在这种情况下提交偏移量就有用了 。再均衡之后,为了能继续之前的工作,消费者需要读取每个分区最后一次提交的偏移量,然后再从偏移量开始继续往下消费消息

发生错误的情况:

  • 如果所提交的偏移量小于客户端处理的最后一条消息的偏移量,那么两个偏移量之间的消息就会被重复处理:
  • 如果所提交的偏移量大于客户端处理的最后一条消息的偏移 ,那么两个偏移量之间的消息就会被丢掉。

提交方式

1. 自动提交

Kafka 默认会定期自动提交偏移量(可通过消费者的属性 enable.auto.commit 来修改,默认 true ),提交的时间间隔默认是 5 秒(可通过消费者的属性 auto.commit.interval.ms 来修改) 这种自动提交的方式看起来很简便,但会产生重复处理消息的问题。

假设自动提交的时间间隔是 5 秒,在最后一次提交之后的第 3 秒发生了再均衡,再均衡完成之后消费者从最后一次提交的偏移量位置开始读取消息,此时得到的偏移量己经落后实际消费情况 3秒(上一个消费者已经消费了三秒钟消息,但没有提交最新的偏移量),从而导致在这3秒内己消费的消息会被重复消费。当然,你可以通过再缩短提交的时间间隔(例如把 3 秒改成 1 秒〉来更频繁地提交偏移量 ,从而减小可能出现重复消息的时间间隔,但这还是完全避免消息重复消费的。所以自动提交虽然方便,但没有给开发留有避免重复处理消息的空间

2. 手动提交

由于自动提交可能导致出现一些不可控的情况,所以很多开发者通过在程序中自己决定何时提交的方式来消除丢失消息的可能,并在发生再均衡时减少重复消息的数量。在进行手动提交之前需要先关闭消费者的自动提交配置,然后使用 commitSync 方法提交偏移量。

commitSync 方法会提交由 poll 返回的最新偏移量,所以在处理完记录后要确保调用了 commitSync 方法,否则还是会发生重复处理等问题。示例中是接收一批消息并处理完之后提交 一次偏移 ,当然也可以每处理一条消息就提交一次偏移 ,这样相对来说可以减少重复消息的数量,但会降低消费端的吞吐量。

3. 异步提交

使用 commitSync 方法提交偏移量有一个不足之处,就是该方法在 Broker 对提交请求做出回应前是阻塞的。因此,采用这种方式每提交一次偏移量就等待一次限制了消费端的吞吐量, 如果通过降低提交频率来保证吞吐量, 又有增加消息重复消费概率的风险(在此消费者调用poll 和 commitSync 之间,可能会再均衡,出现消费相同消息的情况)。所以 Kafka 还提供了另一种方式,即异步提交方式,消费者只管发送提交请求,而不需要等待 Broker 立即回应。

这里使用了 commitAsync 方法来提交最后一个偏移量。它与 commitSync 方法不同的是, commitAync 成功提交或碰到无法恢复的错误之前会一直重试,这 commitAsync 不好的个地方。

因为可能在它收到服务器响应时已经有一个更大的偏移提交成功了,如果进行重试可能会导致偏移量被覆盖。

举个例子,发起一个异步提交 commitA ,此时的提交偏移 1000 ,随后又发起了异步提交 commitB 且偏移 2000,如果 commitA 提交失败 commitB 提交成功, 此时 commitA 进行重试并成功的话, 会把实际上 经成功提交的偏移 2000 回滚到 1000 ,导致消息重复消费。

commitAsync 同名方法,另外两个方法支持回调, Broker 做出响应之后会执行回调,回调常用于记录提交错误或者生成某些应用度指标

while(true){
    ConsumerRecords<String,String> records = consumer.poll(100);
    // 消费消息...
    consumer.commitAsync(new OffsetCommitCallback(){
        @Override
        public void onComplete(Map<TopicPartition,OffsetAndMetadata> offsets,Exception e){
            if(e!=null){
                logger.error("消息提交出错,offset:{}",offsets,e);
            }
        }
    
    })
    
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值