kafka consumer是一个单纯的单线程程序,因此相对于producer会更好理解些。阅读consumer代码的关键是理解回调,因为consumer中使用了大量的回调函数。参看kafka中的回调函数

1 整体流程

从KafkaConsumer#pollOnce(..)入口来看consumer的整体流程

private Map<TopicPartition, List<ConsumerRecord<K, V>>> pollOnce(long timeout) {
coordinator.ensureCoordinatorReady(); // 发送获取coordinator请求,直到获取到coordinator if (subscriptions.partitionsAutoAssigned())
coordinator.ensurePartitionAssignment(); // 发送joinGroup和syncGroup,直到获取到consumer被分配的parttion信息;并启动心跳 if (!subscriptions.hasAllFetchPositions())
updateFetchPositions(this.subscriptions.missingFetchPositions()); // 拉取offset信息和commited信息,以便拉取数据的时候直到从哪开始拉取 long now = time.milliseconds(); client.executeDelayedTasks(now); Map<TopicPartition, List<ConsumerRecord<K, V>>> records = fetcher.fetchedRecords(); // 从本地数据结构中读取,并不是发送请求 if (!records.isEmpty()) // 如果获取到就直接返回
return records; fetcher.sendFetches(); // 发送拉取数据请求
client.poll(timeout, now); // 真正的发送
return fetcher.fetchedRecords(); // 从本地数据结构中读取,并不是发送请求
}

2 Reblance joinGroup和syncGroup

consumer需要向coordinator发送请求,来知道自己负责消费哪些topic的哪些partiton。这个过程可以分为两个请求:

  1. joinGroup。joinGroup请求加入消费组,一旦coordinator确定了所有成员都发送了joinGroup,就会返回给客户端response,response中包括memberid、generation、consumer是否是leader等信息。
  2. syncGroup。如果consumer是leader的话,他会在本地将已经分配好的partiton信息附加到request中,告诉coordinator,我是这样分配的。这里需要注意consumer分区的分配是放在consumer端的。如果是普通的非leader consumer,那么就是简单的请求。无论是leader还是普通的消费者, coordinator都会返回consumer需要消费的parttion列表。

joinGroup和syncGroup的主要逻辑在AbstractCoordinator#ensureActiveGroup(..),在发送join和sync之前会提交一把offset,这样做是为了防止reblance造成的重复消费。

发送sync请求是在join请求的回调函数中,即AbstractCoordinator#JoinGroupResponseHandler(..),也就是说当join请求返回后,调用response的时候会发送一次sync请求。

private class JoinGroupResponseHandler extends CoordinatorResponseHandler<JoinGroupResponse, ByteBuffer> {

        @Override
public JoinGroupResponse parse(ClientResponse response) {
return new JoinGroupResponse(response.responseBody());
} @Override
public void handle(JoinGroupResponse joinResponse, RequestFuture<ByteBuffer> future) {
Errors error = Errors.forCode(joinResponse.errorCode());
if (error == Errors.NONE) {
log.debug("Received successful join group response for group {}: {}", groupId, joinResponse.toStruct());
AbstractCoordinator.this.memberId = joinResponse.memberId(); // 读取response中的memberid
AbstractCoordinator.this.generation = joinResponse.generationId(); // generationId
AbstractCoordinator.this.rejoinNeeded = false;
AbstractCoordinator.this.protocol = joinResponse.groupProtocol();
sensors.joinLatency.record(response.requestLatencyMs()); // 发送sync请求
if (joinResponse.isLeader()) {
onJoinLeader(joinResponse).chain(future);
} else {
onJoinFollower().chain(future);
}
// 省略其他
}
}
}

需要注意的是,kafka一个group可以消费多个topic,假设如果有两个topic:TopicA和TopicB,他们分别都有一个消费组名字都叫test,如果TopicA的test内消费者数量变化引起reblance,会造成TopicB的test也会reblance的。可以看下这里:http://www.cnblogs.com/dongxiao-yang/p/5417956.html

3 heartBeat

在发送完joinGroup后会启动heartBeat。HeartbeatTask实现了DelayedTask。heatbeat定时向coordinator发送心跳信息,如果返回ILLEGAL_GENERATION,说明coordinator已经重新进行了reblance,这个时候consuemr就需要再次发送join和sync请求。如下HeartbeatCompletionHandler

private class HeartbeatCompletionHandler extends CoordinatorResponseHandler<HeartbeatResponse, Void> {
@Override
public HeartbeatResponse parse(ClientResponse response) {
return new HeartbeatResponse(response.responseBody());
} @Override
public void handle(HeartbeatResponse heartbeatResponse, RequestFuture<Void> future) {
sensors.heartbeatLatency.record(response.requestLatencyMs());
Errors error = Errors.forCode(heartbeatResponse.errorCode());
if (error == Errors.NONE) {
log.debug("Received successful heartbeat response for group {}", groupId);
future.complete(null);
} else if (error == Errors.GROUP_COORDINATOR_NOT_AVAILABLE
|| error == Errors.NOT_COORDINATOR_FOR_GROUP) {
log.debug("Attempt to heart beat failed for group {} since coordinator {} is either not started or not valid.",
groupId, coordinator);
coordinatorDead();
future.raise(error);
} else if (error == Errors.REBALANCE_IN_PROGRESS) {
log.debug("Attempt to heart beat failed for group {} since it is rebalancing.", groupId);
AbstractCoordinator.this.rejoinNeeded = true;
future.raise(Errors.REBALANCE_IN_PROGRESS);
} else if (error == Errors.ILLEGAL_GENERATION) { // 服务端已经是新一代了,客户端需要reblance。
log.debug("Attempt to heart beat failed for group {} since generation id is not legal.", groupId);
AbstractCoordinator.this.rejoinNeeded = true; // rejoinNeeded置为true,下次拉取的时候会重新发送join和sync请求
future.raise(Errors.ILLEGAL_GENERATION);
} else if (error == Errors.UNKNOWN_MEMBER_ID) {
log.debug("Attempt to heart beat failed for group {} since member id is not valid.", groupId);
memberId = JoinGroupRequest.UNKNOWN_MEMBER_ID;
AbstractCoordinator.this.rejoinNeeded = true;
future.raise(Errors.UNKNOWN_MEMBER_ID);
} else if (error == Errors.GROUP_AUTHORIZATION_FAILED) {
future.raise(new GroupAuthorizationException(groupId));
} else {
future.raise(new KafkaException("Unexpected error in heartbeat response: " + error.message()));
}
}
}

4 DelayedTask

DelayedTask是一个接口,只有一个run方法,实现了DelayedTask的类只有两个:AutoCommitTask和HeartbeatTask。两个都是定时请求的任务。那么consumer单线程是如何实现定时提交的呢?原来Consumer会将AutoCommitTask和HeartbeatTask放在ConsumerNetworkClient#DelayedTaskQueue中,DelayedTaskQueue中包含一个PriorityQueue,会将DelayedTask封装成Entry并根据时间优先级排序。每次poll的时候都会从DelayedTaskQueue中获取第一个,根据第一个Entry剩余时间来确定poll阻塞时间。

ConsumerNetworkClient调用schedule将DelayedTaskQueue放到ConsumerNetworkClient#DelayedTaskQueue中

public void schedule(DelayedTask task, long at) {
delayedTasks.add(task, at); // DelayedTaskQueue#add
}

DelayedTaskQueue#add

public class DelayedTaskQueue {

    private PriorityQueue<Entry> tasks; // 优先级队列

    public DelayedTaskQueue() {
tasks = new PriorityQueue<Entry>();
} /**
* Schedule a task for execution in the future.
*
* @param task the task to execute
* @param at the time at which to
*/
public void add(DelayedTask task, long at) {
tasks.add(new Entry(task, at));
}
// ...
}

AutoCommitTask和HeartbeatTask为了能够一直执行,会在回调函数中将自己重新加入到DelayedTaskQueue中,并指定下次执行的时间。这样就可以不停的执行了。以heartbeat为例

private class HeartbeatTask implements DelayedTask {

        private boolean requestInFlight = false;

        public void reset() {
// start or restart the heartbeat task to be executed at the next chance
long now = time.milliseconds();
heartbeat.resetSessionTimeout(now);
client.unschedule(this); if (!requestInFlight)
client.schedule(this, now);
} @Override
public void run(final long now) {
if (generation < 0 || needRejoin() || coordinatorUnknown()) {
// no need to send the heartbeat we're not using auto-assignment or if we are
// awaiting a rebalance
return;
} if (heartbeat.sessionTimeoutExpired(now)) {
// we haven't received a successful heartbeat in one session interval
// so mark the coordinator dead
coordinatorDead();
return;
} if (!heartbeat.shouldHeartbeat(now)) {
// we don't need to heartbeat now, so reschedule for when we do
client.schedule(this, now + heartbeat.timeToNextHeartbeat(now));
} else {
heartbeat.sentHeartbeat(now);
requestInFlight = true; RequestFuture<Void> future = sendHeartbeatRequest();
future.addListener(new RequestFutureListener<Void>() {
@Override
public void onSuccess(Void value) {
requestInFlight = false;
long now = time.milliseconds();
heartbeat.receiveHeartbeat(now);
long nextHeartbeatTime = now + heartbeat.timeToNextHeartbeat(now); // 回调中再次加入,实现了循环定时执行
client.schedule(HeartbeatTask.this, nextHeartbeatTime);
} @Override
public void onFailure(RuntimeException e) {
requestInFlight = false;
client.schedule(HeartbeatTask.this, time.milliseconds() + retryBackoffMs);
}
});
}
}
}

5 updateFetchPositions

updateFetchPositions 用于更新commited和offset信息。客户端的消费状态是保存在SubscriptionState中的。SubscriptionState有一下主要属性

public class SubscriptionState {
private Pattern subscribedPattern;
// 消费者订阅的topic
private final Set<String> subscription;
private final Set<String> groupSubscription;
private final Set<TopicPartition> userAssignment;
// 消费状态
private final Map<TopicPartition, TopicPartitionState> assignment;
private boolean needsPartitionAssignment;
private boolean needsFetchCommittedOffsets;
private final OffsetResetStrategy defaultResetStrategy;
private ConsumerRebalanceListener listener;
// ...省略
} private static class TopicPartitionState {
private Long position; // 消费位置,从coordinator拉取的时候会带上该字段
private OffsetAndMetadata committed; // 已经提交的offset
private boolean paused; // whether this partition has been paused by the user
private OffsetResetStrategy resetStrategy; // the strategy to use if the offset needs resetting
}

消费状态信息最终被保存在TopicPartitionState中,topicPartitionState中有两个重要的属性:committed和position。需要注意的是commited和position其实表示下一次需要消费的位置,比如0-10的offsetc都已经提交了,那么从coordinator拉取到的committed是11而不是10;position也是一样的,如果已经消费到15,那么position的值是16。更多可见consumer提交offset原理

6 几个重要的参数

  1. fetch.min.bytes 一个parttion拉取的最小字节数。consumer是批量从broker拉取消息的,fetch.min.bytes表示最小拉取多少字节才返回。默认值是1
  2. fetch.max.wait.ms 拉取数据的时候最长等待时间,与fetch.min.bytes配合使用。等待fetch.max.wait.ms时间后,还没有得到fetch.min.bytes大小的数据则返回。默认值500.
  3. max.partition.fetch.bytes 一个partiton最多拉取字节数。默认值1048576,即1M。

以上参数都是放到request中。如下Fetcher#createFetchRequests(..)

private Map<Node, FetchRequest> createFetchRequests() {
// create the fetch info
Cluster cluster = metadata.fetch();
Map<Node, Map<TopicPartition, FetchRequest.PartitionData>> fetchable = new HashMap<>();
for (TopicPartition partition : fetchablePartitions()) {
Node node = cluster.leaderFor(partition);
if (node == null) {
metadata.requestUpdate();
} else if (this.client.pendingRequestCount(node) == 0) {
// if there is a leader and no in-flight requests, issue a new fetch
Map<TopicPartition, FetchRequest.PartitionData> fetch = fetchable.get(node);
if (fetch == null) {
fetch = new HashMap<>();
fetchable.put(node, fetch);
} long position = this.subscriptions.position(partition);
fetch.put(partition, new FetchRequest.PartitionData(position, this.fetchSize)); // fetchSize即max.partition.fetch.bytes
log.trace("Added fetch request for partition {} at offset {}", partition, position);
}
} // create the fetches
Map<Node, FetchRequest> requests = new HashMap<>();
for (Map.Entry<Node, Map<TopicPartition, FetchRequest.PartitionData>> entry : fetchable.entrySet()) {
Node node = entry.getKey();
// maxWaitMs即fetch.max.wait.ms,minBytes即fetch.min.byte
FetchRequest fetch = new FetchRequest(this.maxWaitMs, this.minBytes, entry.getValue());
requests.put(node, fetch);
}
return requests;
}
  1. max.poll.records 返回的最大record数。与以上三个参数不同,该参数不会放到fetch request中,拉取的records会放在本地变量中,该参数表示将本地变量中多少records返回。

Fetcher拉取的所有消息都会被放到放到records中,record是一个List,存放了所有partiton的record,max.poll.records参数就用来配置每次从list中返回多少条record的,注意是所有partiton的。

Fetcher#fetchedRecords(..)

public Map<TopicPartition, List<ConsumerRecord<K, V>>> fetchedRecords() {
if (this.subscriptions.partitionAssignmentNeeded()) {
return Collections.emptyMap();
} else {
Map<TopicPartition, List<ConsumerRecord<K, V>>> drained = new HashMap<>();
throwIfOffsetOutOfRange();
throwIfUnauthorizedTopics();
throwIfRecordTooLarge(); int maxRecords = maxPollRecords;
Iterator<PartitionRecords<K, V>> iterator = records.iterator();
while (iterator.hasNext() && maxRecords > 0) {
PartitionRecords<K, V> part = iterator.next();
maxRecords -= append(drained, part, maxRecords); // maxRecords就是max.poll.records
if (part.isConsumed())
iterator.remove();
}
return drained;
}
}
  1. 另外在调用consumer api的时候需要制定timeout时间,如果超过timeout仍然没有消息则返回空的records。
while (true) {
ConsumerRecords<String, String> records = consumer.poll(1000); // timeout时间
// System.out.println("begin for 2");
for (ConsumerRecord<String, String> record : records) {
// System.out.println("hello");
System.out.println(record.partition() + " " + record.offset());
}
}

kafka consumer代码梳理的更多相关文章

  1. kafka consumer 代码示例

    使用者小组 使得许多进程的多台机器 在逻辑上作为一个单个的使用者 出现. 我们使用中,一种常见的情况是,我们按照逻辑划分出多个使用者小组,每个小组都是有作为一个逻辑整体的多台使用者计算机组成的集群. ...

  2. 使用kafka consumer api时,中文乱码问题

    使用Intelli idea调试kafka low consumer时,由于broker存储的message有中文, idea中console端是可以正确显示的 然后mvn package打包到服务器 ...

  3. kafka consumer 0.8.2.1示例代码

    package test_kafka; import java.util.ArrayList; import java.util.HashMap; import java.util.List; imp ...

  4. 读Kafka Consumer源码

    最近一直在关注阿里的一个开源项目:OpenMessaging OpenMessaging, which includes the establishment of industry guideline ...

  5. 【原创】Kafka Consumer多线程实例

    Kafka 0.9版本开始推出了Java版本的consumer,优化了coordinator的设计以及摆脱了对zookeeper的依赖.社区最近也在探讨正式用这套consumer API替换Scala ...

  6. 【原创】kafka consumer源代码分析

    顾名思义,就是kafka的consumer api包. 一.ConsumerConfig.scala Kafka consumer的配置类,除了一些默认值常量及验证参数的方法之外,就是consumer ...

  7. 【原创】Kafka Consumer多线程实例续篇

    在上一篇<Kafka Consumer多线程实例>中我们讨论了KafkaConsumer多线程的两种写法:多KafkaConsumer多线程以及单KafkaConsumer多线程.在第二种 ...

  8. Kafka设计解析(二十)Apache Flink Kafka consumer

    转载自 huxihx,原文链接 Apache Flink Kafka consumer Flink提供了Kafka connector用于消费/生产Apache Kafka topic的数据.Flin ...

  9. 【译】Apache Flink Kafka consumer

    Flink提供了Kafka connector用于消费/生产Apache Kafka topic的数据.Flink的Kafka consumer集成了checkpoint机制以提供精确一次的处理语义. ...

随机推荐

  1. 日常问题记录 E138: Can't write viminfo file /root/.viminfo!

    问题是因为磁盘不足导致 看磁盘占用情况 排查:du --max-depth=1 -h   这个命令可以查看当前目录下哪个目录占用最大,然后再进一步看

  2. 最近好忙 (没有)35讲GUI

    0: g.integerbox 表示只输入整数的对话框 1:. g.multenterbox 用来表示多个输入框. import easygui as g msg = "请填写以下联系方式& ...

  3. ☕Java 面向对象进阶内容

    目录 == 和 equals 方法 封装 多态 抽象类和抽象方法 抽象方法 抽象类 抽象类的使用要点 接口 接口使用 内部类 String 字符串常量拼接时的优化 String Pool String ...

  4. CDH5.16.2离线安装(详细)

    目录 01 Coudera Manager 02 环境准备 03 CM安装 01 Coudera Manager 概念:拥有集群自动化安装.中心化管理.集群监控.报警功能的一个工具,使集群安装从几天时 ...

  5. OS模块的补充使用---执行终端命令

    Python基础至os模块 由于近期的项目需要用到对应的终端命令去调用其他程序,因此温习一下os.system()函数: 参考文献:https://zhuanlan.zhihu.com/p/51716 ...

  6. SHA算法(及示例)演变历史

    安全散列算法(英语:Secure Hash Algorithm,缩写为SHA)是一个密码散列函数家族,是FIPS所认证的安全散列算法.能计算出一个数字消息所对应到的,长度固定的字符串(又称消息摘要)的 ...

  7. 学习java Markdown语法

    Markdown学习 标题: #+标题名字+回车 二级标题 ##+标题名字+回车 三级四级一次类推,最多六级 字体 HELLO,World ! HELLO,World ! HELLO,World ! ...

  8. gitee 的使用

    Git安装教程(windows)   Git是当今最流行的版本控制软件,它包含了许多高级工具,这里小编就讲一下Git的安装. 下载地址:https://git-scm.com/downloads 首先 ...

  9. mysql 完整备份和恢复

    mysql 完整备份和恢复   一.MySQL完整备份操作 1.直接打包数据库文件夹 创建数据库auth: MariaDB [(none)]> create database auth;Quer ...

  10. pandas常用操作详解——.loc与.iloc函数的使用及区别

    loc与iloc功能介绍:数据切片.通过索引来提取数据集中相应的行数据or列数据(可以是多行or多列) 总结: 不同:1. loc函数通过调用index名称的具体值来取数据2. iloc函数通过行序号 ...