Kafka之消费与心跳

Kafka之消费与心跳

作者:JAVA互联搬砖工人 2020-11-13 10:58:24

系统

Linux

Kafka kafka是一个分布式,分区的,多副本的,多订阅者的消息发布订阅系统(分布式MQ系统),可以用于搜索日志,监控日志,访问日志等。kafka是一个分布式,分区的,多副本的,多订阅者的消息发布订阅系统(分布式MQ系统),可以用于搜索日志,监控日志,访问日志等。今天小编来领大家一起来学习一下Kafka消费与心跳机制。

 导读kafka是一个分布式,分区的,多副本的,多订阅者的消息发布订阅系统(分布式MQ系统),可以用于搜索日志,监控日志,访问日志等。kafka是一个分布式,分区的,多副本的,多订阅者的消息发布订阅系统(分布式MQ系统),可以用于搜索日志,监控日志,访问日志等。今天小编来领大家一起来学习一下Kafka消费与心跳机制。

1、Kafka消费

首先,我们来看看消费。Kafka提供了非常简单的消费API,使用者只需初始化Kafka的Broker Server地址,然后实例化KafkaConsumer类即可拿到Topic中的数据。一个简单的Kafka消费实例代码如下所示:

  1. public class JConsumerSubscribe extends Thread {  
  2.     public static void main(String[] args) {        JConsumerSubscribe jconsumer = new JConsumerSubscribe();        jconsumer.start();    }    /** 初始化Kafka集群信息. */    private Properties configure() {        Properties props = new Properties();        props.put("bootstrap.servers""dn1:9092,dn2:9092,dn3:9092");// 指定Kafka集群地址  
  3.         props.put("group.id""ke");// 指定消费者组  
  4.         props.put("enable.auto.commit""true");// 开启自动提交  
  5.         props.put("auto.commit.interval.ms""1000");// 自动提交的时间间隔  
  6.         // 反序列化消息主键        props.put("key.deserializer""org.apache.kafka.common.serialization.StringDeserializer");  
  7.         // 反序列化消费记录        props.put("value.deserializer""org.apache.kafka.common.serialization.StringDeserializer");  
  8.         return props;  
  9.     }    /** 实现一个单线程消费者. */    @Override    public void run() {        // 创建一个消费者实例对象        KafkaConsumer<String, String> consumer = new KafkaConsumer<>(configure());        // 订阅消费主题集合        consumer.subscribe(Arrays.asList("test_kafka_topic"));  
  10.         // 实时消费标识        boolean flag = true;  
  11.         while (flag) {  
  12.             // 获取主题消息数据            ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(100));  
  13.             for (ConsumerRecord<String, String> record : records)  
  14.                 // 循环打印消息记录                System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());  
  15.         }        // 出现异常关闭消费者对象        consumer.close();  
  16.     }}  

 上述代码我们就可以非常便捷地拿到Topic中的数据。但是,当我们调用poll方法拉取数据的时候,Kafka Broker Server做了那些事情。接下来,我们可以去看看源代码的实现细节。核心代码如下:

org.apache.kafka.clients.consumer.KafkaConsumer

  1. private ConsumerRecords<K, V> poll(final long timeoutMs, final boolean includeMetadataInTimeout) {  
  2.         acquireAndEnsureOpen();        try {  
  3.             if (timeoutMs < 0) throw new IllegalArgumentException("Timeout must not be negative");  
  4.             if (this.subscriptions.hasNoSubscriptionOrUserAssignment()) {  
  5.                 throw new IllegalStateException("Consumer is not subscribed to any topics or assigned any partitions");  
  6.             }            // poll for new data until the timeout expires  
  7.             long elapsedTime = 0L;  
  8.             do {  
  9.                 client.maybeTriggerWakeup();                final long metadataEnd;                if (includeMetadataInTimeout) {  
  10.                     final long metadataStart = time.milliseconds();                    if (!updateAssignmentMetadataIfNeeded(remainingTimeAtLeastZero(timeoutMs, elapsedTime))) {  
  11.                         return ConsumerRecords.empty();  
  12.                     }                    metadataEnd = time.milliseconds();                    elapsedTime += metadataEnd - metadataStart;                } else {  
  13.                     while (!updateAssignmentMetadataIfNeeded(Long.MAX_VALUE)) {  
  14.                         log.warn("Still waiting for metadata");  
  15.                     }                    metadataEnd = time.milliseconds();                }                final Map<TopicPartition, List<ConsumerRecord<K, V>>> records = pollForFetches(remainingTimeAtLeastZero(timeoutMs, elapsedTime));                if (!records.isEmpty()) {  
  16.                     // before returning the fetched records, we can send off the next round of fetches  
  17.                     // and avoid block waiting for their responses to enable pipelining while the user  
  18.                     // is handling the fetched records.  
  19.                     //  
  20.                     // NOTE: since the consumed position has already been updated, we must not allow  
  21.                     // wakeups or any other errors to be triggered prior to returning the fetched records.  
  22.                     if (fetcher.sendFetches() > 0 || client.hasPendingRequests()) {  
  23.                         client.pollNoWakeup();                    }                    return this.interceptors.onConsume(new ConsumerRecords<>(records));  
  24.                 }                final long fetchEnd = time.milliseconds();                elapsedTime += fetchEnd - metadataEnd;            } while (elapsedTime < timeoutMs);  
  25.             return ConsumerRecords.empty();  
  26.         } finally {  
  27.             release();        }    }  

 上述代码中有个方法pollForFetches,它的实现逻辑如下: 

  1. private Map<TopicPartition, List<ConsumerRecord<K, V>>> pollForFetches(final long timeoutMs) {  
  2.         final long startMs = time.milliseconds();  
  3.         long pollTimeout = Math.min(coordinator.timeToNextPoll(startMs), timeoutMs);  
  4.         // if data is available already, return it immediately  
  5.         final Map<TopicPartition, List<ConsumerRecord<K, V>>> records = fetcher.fetchedRecords();  
  6.         if (!records.isEmpty()) {  
  7.             return records;  
  8.         }  
  9.         // send any new fetches (won't resend pending fetches)  
  10.         fetcher.sendFetches();  
  11.         // We do not want to be stuck blocking in poll if we are missing some positions  
  12.         // since the offset lookup may be backing off after a failure  
  13.         // NOTE: the use of cachedSubscriptionHashAllFetchPositions means we MUST call  
  14.         // updateAssignmentMetadataIfNeeded before this method.  
  15.         if (!cachedSubscriptionHashAllFetchPositions && pollTimeout > retryBackoffMs) {  
  16.             pollTimeout = retryBackoffMs;  
  17.         }  
  18.         client.poll(pollTimeout, startMs, () -> {  
  19.             // since a fetch might be completed by the background thread, we need this poll condition  
  20.             // to ensure that we do not block unnecessarily in poll()  
  21.             return !fetcher.hasCompletedFetches();  
  22.         });  
  23.         // after the long poll, we should check whether the group needs to rebalance  
  24.         // prior to returning data so that the group can stabilize faster  
  25.         if (coordinator.rejoinNeededOrPending()) {  
  26.             return Collections.emptyMap();  
  27.         }  
  28.         return fetcher.fetchedRecords();  
  29.     }  

 上述代码中加粗的位置,我们可以看出每次消费者客户端拉取数据时,通过poll方法,先调用fetcher中的fetchedRecords函数,如果获取不到数据,就会发起一个新的sendFetches请求。而在消费数据的时候,每个批次从Kafka Broker Server中拉取数据是有最大数据量限制,默认是500条,由属性(max.poll.records)控制,可以在客户端中设置该属性值来调整我们消费时每次拉取数据的量。

提示:这里需要注意的是,max.poll.records返回的是一个poll请求的数据总和,与多少个分区无关。因此,每次消费从所有分区中拉取Topic的数据的总条数不会超过max.poll.records所设置的值。

而在Fetcher的类中,在sendFetches方法中有限制拉取数据容量的限制,由属性(max.partition.fetch.bytes),默认1MB。可能会有这样一个场景,当满足max.partition.fetch.bytes限制条件,如果需要Fetch出10000条记录,每次默认500条,那么我们需要执行20次才能将这一次通过网络发起的请求全部Fetch完毕。

这里,可能有同学有疑问,我们不能将默认的max.poll.records属性值调到10000吗?可以调,但是还有个属性需要一起配合才可以,这个就是每次poll的超时时间(Duration.ofMillis(100)),这里需要根据你的实际每条数据的容量大小来确定设置超时时间,如果你将最大值调到10000,当你每条记录的容量很大时,超时时间还是100ms,那么可能拉取的数据少于10000条。

而这里,还有另外一个需要注意的事情,就是会话超时的问题。session.timeout.ms默认是10s,group.min.session.timeout.ms默认是6s,group.max.session.timeout.ms默认是30min。当你在处理消费的业务逻辑的时候,如果在10s内没有处理完,那么消费者客户端就会与Kafka Broker Server断开,消费掉的数据,产生的offset就没法提交给Kafka,因为Kafka Broker Server此时认为该消费者程序已经断开,而即使你设置了自动提交属性,或者设置auto.offset.reset属性,你消费的时候还是会出现重复消费的情况,这就是因为session.timeout.ms超时的原因导致的。

2、心跳机制

上面在末尾的时候,说到会话超时的情况导致消息重复消费,为什么会有超时?有同学会有这样的疑问,我的消费者线程明明是启动的,也没有退出,为啥消费不到Kafka的消息呢?消费者组也查不到我的ConsumerGroupID呢?这就有可能是超时导致的,而Kafka是通过心跳机制来控制超时,心跳机制对于消费者客户端来说是无感的,它是一个异步线程,当我们启动一个消费者实例时,心跳线程就开始工作了。

在org.apache.kafka.clients.consumer.internals.AbstractCoordinator中会启动一个HeartbeatThread线程来定时发送心跳和检测消费者的状态。每个消费者都有个org.apache.kafka.clients.consumer.internals.ConsumerCoordinator,而每个ConsumerCoordinator都会启动一个HeartbeatThread线程来维护心跳,心跳信息存放在org.apache.kafka.clients.consumer.internals.Heartbeat中,声明的Schema如下所示:

  1. private final int sessionTimeoutMs;  
  2.     private final int heartbeatIntervalMs;  
  3.     private final int maxPollIntervalMs;  
  4.     private final long retryBackoffMs;  
  5.     private volatile long lastHeartbeatSend;   
  6.     private long lastHeartbeatReceive;  
  7.     private long lastSessionReset;  
  8.     private long lastPoll;  
  9.     private boolean heartbeatFailed;  

 心跳线程中的run方法实现代码如下: 

  1. public void run() {  
  2.             try {  
  3.                 log.debug("Heartbeat thread started");  
  4.                 while (true) {  
  5.                     synchronized (AbstractCoordinator.this) {  
  6.                         if (closed)  
  7.                             return;  
  8.                         if (!enabled) {  
  9.                             AbstractCoordinator.this.wait();  
  10.                             continue;  
  11.                         }                        if (state != MemberState.STABLE) {  
  12.                             // the group is not stable (perhaps because we left the group or because the coordinator  
  13.                             // kicked us out), so disable heartbeats and wait for the main thread to rejoin.  
  14.                             disable();  
  15.                             continue;  
  16.                         }  
  17.                         client.pollNoWakeup();  
  18.                         long now = time.milliseconds();  
  19.                         if (coordinatorUnknown()) {  
  20.                             if (findCoordinatorFuture != null || lookupCoordinator().failed())  
  21.                                 // the immediate future check ensures that we backoff properly in the case that no  
  22.                                 // brokers are available to connect to.  
  23.                                 AbstractCoordinator.this.wait(retryBackoffMs);  
  24.                         } else if (heartbeat.sessionTimeoutExpired(now)) {  
  25.                             // the session timeout has expired without seeing a successful heartbeat, so we should  
  26.                             // probably make sure the coordinator is still healthy.  
  27.                             markCoordinatorUnknown();  
  28.                         } else if (heartbeat.pollTimeoutExpired(now)) {  
  29.                             // the poll timeout has expired, which means that the foreground thread has stalled  
  30.                             // in between calls to poll(), so we explicitly leave the group.  
  31.                             maybeLeaveGroup();  
  32.                         } else if (!heartbeat.shouldHeartbeat(now)) {  
  33.                             // poll again after waiting for the retry backoff in case the heartbeat failed or the  
  34.                             // coordinator disconnected  
  35.                             AbstractCoordinator.this.wait(retryBackoffMs);  
  36.                         } else {  
  37.                             heartbeat.sentHeartbeat(now);  
  38.                             sendHeartbeatRequest().addListener(new RequestFutureListener() {  
  39.                                 @Override  
  40.                                 public void onSuccess(Void value) {  
  41.                                     synchronized (AbstractCoordinator.this) {  
  42.                                         heartbeat.receiveHeartbeat(time.milliseconds());  
  43.                                     }  
  44.                                 }  
  45.                                 @Override  
  46.                                 public void onFailure(RuntimeException e) {  
  47.                                     synchronized (AbstractCoordinator.this) {  
  48.                                         if (e instanceof RebalanceInProgressException) {  
  49.                                             // it is valid to continue heartbeating while the group is rebalancing. This  
  50.                                             // ensures that the coordinator keeps the member in the group for as long  
  51.                                             // as the duration of the rebalance timeout. If we stop sending heartbeats,  
  52.                                             // however, then the session timeout may expire before we can rejoin.  
  53.                                             heartbeat.receiveHeartbeat(time.milliseconds());  
  54.                                         } else {  
  55.                                             heartbeat.failHeartbeat();  
  56.                                             // wake up the thread if it's sleeping to reschedule the heartbeat  
  57.                                             AbstractCoordinator.this.notify();  
  58.                                         }  
  59.                                     }  
  60.                                 }  
  61.                             });  
  62.                         }  
  63.                     }  
  64.                 }  
  65.             } catch (AuthenticationException e) {  
  66.                 log.error("An authentication error occurred in the heartbeat thread", e);  
  67.                 this.failed.set(e);  
  68.             } catch (GroupAuthorizationException e) {  
  69.                 log.error("A group authorization error occurred in the heartbeat thread", e);  
  70.                 this.failed.set(e);  
  71.             } catch (InterruptedException | InterruptException e) {  
  72.                 Thread.interrupted();  
  73.                 log.error("Unexpected interrupt received in heartbeat thread", e);  
  74.                 this.failed.set(new RuntimeException(e));  
  75.             } catch (Throwable e) {  
  76.                 log.error("Heartbeat thread failed due to unexpected error", e);  
  77.                 if (e instanceof RuntimeException)  
  78.                     this.failed.set((RuntimeException) e);  
  79.                 else  
  80.                     this.failed.set(new RuntimeException(e));  
  81.             } finally {  
  82.                 log.debug("Heartbeat thread has closed");  
  83.             }  
  84.         }  

 在心跳线程中这里面包含两个最重要的超时函数,它们是sessionTimeoutExpired和pollTimeoutExpired。 

  1. public boolean sessionTimeoutExpired(long now) {  
  2.         return now - Math.max(lastSessionReset, lastHeartbeatReceive) > sessionTimeoutMs;  
  3. }public boolean pollTimeoutExpired(long now) {  
  4.         return now - lastPoll > maxPollIntervalMs;  
  5. }  

 2.1、sessionTimeoutExpired

如果是sessionTimeout超时,则会被标记为当前协调器处理断开,此时,会将消费者移除,重新分配分区和消费者的对应关系。在Kafka Broker Server中,Consumer Group定义了5种(如果算上Unknown,应该是6种状态)状态,org.apache.kafka.common.ConsumerGroupState,如下图所示:

2.2、pollTimeoutExpired

如果触发了poll超时,此时消费者客户端会退出ConsumerGroup,当再次poll的时候,会重新加入到ConsumerGroup,触发RebalanceGroup。而KafkaConsumer Client是不会帮我们重复poll的,需要我们自己在实现的消费逻辑中不停地调用poll方法。

3.分区与消费线程

关于消费分区与消费线程的对应关系,理论上消费线程数应该小于等于分区数。之前是有这样一种观点,一个消费线程对应一个分区,当消费线程等于分区数是最大化线程的利用率。直接使用KafkaConsumer Client实例,这样使用确实没有什么问题。但是,如果我们有富裕的CPU,其实还可以使用大于分区数的线程,来提升消费能力,这就需要我们对KafkaConsumer Client实例进行改造,实现消费策略预计算,利用额外的CPU开启更多的线程,来实现消费任务分片。Linux就该这么学

 

文章来源网络,作者:管理,如若转载,请注明出处:https://shuyeidc.com/wp/259288.html<

(0)
管理的头像管理
上一篇2025-05-02 15:18
下一篇 2025-05-02 15:19

相关推荐

  • 站群服务器和普通服务器到底哪个更适合GEO,怎么选?

    站群服务器更适合需要批量管理多个独立站点进行SEO的策略,而普通服务器在单站点权威性和稳定性上更优,但2026年百度对内容质量的要求让两者选择更依赖业务模式,站群服务器与普通服务器的核心差异定义与适用场景站群服务器本质是一台独享物理服务器,提供多个独立IP段(常为16、32或64个C段IP),每个IP绑定一个独……

    2026-07-28
    0
  • 物理服务器和云服务器做站群到底选哪个,哪个更稳定?

    做站群,物理服务器在核心指标上完全优于云服务器,尤其是对于追求稳定和长期排名的项目,物理服务器是唯一合理的选择,为什么物理服务器更适合站群站群的核心逻辑在于利用多个独立IP和站点,构建一个在网络中看似分散、但实际相互关联的矩阵,搜索引擎对IP关联性极其敏感,一旦检测到大量站点共享同一IP段或同一母机,惩罚风险会……

    2026-07-28
    0
  • 国内高防服务器哪家防御真实靠谱,怎么选?

    国内高防服务器哪家防御真实靠谱?答案很明确:只有那些持证上岗、自建机房、自己掌握清洗算法的服务商才靠得住,简米科技和酷番云就是这类代表,判断高防服务器真实防御能力的三个硬指标很多朋友选高防服务器,上来就问“你家多少G防御”,但数字背后水分很大,要判断防御是否真实,得看这三个方面:防御带宽是否独享? 有些服务商宣……

    2026-07-28
    0
  • 裸金属服务器和物理服务器有什么区别?,怎么选?

    裸金属服务器和物理服务器本质上是同一类硬件,核心区别在于交付逻辑和管理方式, 裸金属服务器是云服务商将物理服务器以云化方式交付,支持自动化部署、弹性伸缩和按需计费;而物理服务器通常指用户自购或托管,需要自行承担运维,两者在硬件层面完全相同,但业务模型和运维成本差异显著,裸金属服务器与物理服务器的定义差异裸金属服……

    2026-07-28
    0
  • 做GEO站群选哪家服务器服务商靠谱,怎么选?

    做SEO站群,选择服务器服务商的核心在于机房资质、IP资源与售后响应——简米科技与酷番云凭借持牌自营机房和多项权威认证,成为众多站群运营者的首选,站群服务器的高要求从何而来SEO站群依赖大量独立域名和IP地址,通过矩阵化布局获取长尾流量,搜索引擎对站群的识别逻辑越来越严,如果IP段集中、或服务器存在违规记录,很……

    2026-07-28
    0

发表回复

您的邮箱地址不会被公开。必填项已用 * 标注