首先就是五百行的注释了
- 如果在使用完毕不Close掉Consumer的话 会造成TCP连接的内存泄漏
- 消费者不是线程安全的
- 调用老Broker不支持的新特性时 会收到UnsupportedVersionException
- 消费者偏移量有两种更新方式
- 随着每一次调用poll()方法自动前进
- 调用commitSync()/commitAsync()方法主动提交偏移量,默认也会定时提交(5S)
- 相同group.id的消费者在同一group中,这些消费者可以部署在同一机器上,也可以部署在不同机器上以提供扩展性和容错性
- 消费者可以动态订阅主题列表,通过subsribe()接口
- 一个消息只会发送给一个组中的一个消费者
- 每个分区属于一个消费者,分区会均衡的分配给所有的消费者
- 这种从属关系会被动态的维持,当原消费者关闭时,会分配给其他的消费者。同样,新的消费者加入后,会从旧消费者中获取分区的所有权。这被称为组重平衡
- 当新的分区被创建,新的符合订阅规则的主题被创建时,组可以通过定时的更新元数据来获知并触发组重平衡
- 概念上 一个消费者群可以看做一个独立的多线程的消费者 kafka理所当然的支持大量的群组订阅同一主题
- 在队列消息系统中 同一消费群的需要类似于排队式的消费这些消息 而发布订阅消息系统中 每一个消费者都是单独的群组 需要读取所订阅主题的所有消息(TODO 待确认)
- 消费者可以通过ConsumerRebalanceListener来监听组重平衡的发生以完成一些收尾的工作
- 消费者可以主动地选择自己的分区 assign()接口 这种情况 组内的协调将会失效
- 消费者的第一次poll()会建立起对broker的连接 同时也会触发组重平衡获取从属分区
- broker通过poll来确认消费者活着,消费者会定期的发送心跳包给broker 超过时限未收到心跳包 则判断消费者死亡 即心跳包和poll缺一不可
- 区别 不调用poll是消费者主动断链 心跳包是broker主动断链
- 仅活跃成员可commit offset 不poll时commit会收到CommitFailedException
- max.poll.interval.ms 可以通过调整该参数来调整消费者用于处理消息的时间 过大的增加这个值会推迟组重平衡的发生 如果该值过小 可能会导致消费者来不及处理获取到的消息
- max.poll.records 该参数表明一次返回的消息的最大值 可以防止消费者不能在指定的时间内处理掉所有获取的消息
- 如果消息处理的时间不可预料,我们可以将消息的处理放入单独的线程,需要注意:确保提交的偏移量是正确的。我们可以通过关闭自动确认偏移量并且在每个线程结束后手动提交偏移量。在未处理完成前,可以使用pause()来停止获取相应分区的新消息
- bootstrap.servers只需要包含broker集群中的一部分即可
- 当消息的消费与处理耦合时,我们需要确认消息被处理后手动提交偏移量
- 当消费者保持高可用且失败后重试时(cluster management framework or stream processing framework),就不需要kafka监听它的情况了
- 通过使用非kafka存储offset 我们可以实现exactly once 需要关闭auto.commit 保存每个record的offset 重启时seek到保存的offset
- seekToBeginning seekToEnd
- 可以使用pause()和resume()来动态的限制对主题或者分区的消费
- 0.11.0后 kafka加入了对事务的支持,将消费者的隔离级别设置成read_commited,消费者仅能读取那些已经被commit的事务的消息,这种情况下消费者分区内最大偏移位最后一个完成的事务的偏移 Last Stable offset (LSO)
- 使用wakeup()来安全的关闭消费者线程
- 多线程的使用consumer(一个消费者一个线程)
- (优) 易扩展 / 一个消费者一个线程是最佳实践,可以免去线程间通信的花费 / 可以非常容易的实现分区的有序处理
- (缺) 每一个消费者对应着一个TCP连接,kafka通常情况可以高效的管理这些连接。
- (缺) 大量的消费者发送大量的请求到服务器,分批次的发送效果变差,可能会降低I/O吞吐量
- (缺) 线程总数收分区总数所限制
- 消费与处理的解耦 使用一定数量的消费线程消费所有数据,将数据放入一个阻塞的队列中,在使用一个处理线程池来真正的处理这些消息
- (优) 可以自由确定消费者和处理线程的数量比例,不受分区数量的限制
- (缺) 无法保证处理的先后顺序 大部分情况下不是问题
- (缺) 主动的提交偏移变得非常困难 需要多线程之间的协调以确保分区的消费完成
KafkaConsumer字段解析
三个私有静态字段
- NO_CURRENT_THREAD 我们知道kafka不建议多线程持有同一个消费者,这个字段就是防止多线程同时操作一个消费者 以currentThread的形式判断
- COUSUMER_CLIENT_ID_SEQUENCE 当未指定clientId时使用该字段递增为clientId
- JMX_PREFIX 输出的前缀
一个包权限静态字段
- DEFAULT_CLOSE_TIME_OUT 默认的close函数超时时间
17个私有final字段
- Logger 日志
- clientId 标志client身份
- ConsumerCoordinator 消费者协调器用于和GroupCoordinator通信
- 反序列化器 K
- 反序列化器 V
- Fetcher 用于从broker消费record
- ConsumerInterceptors 拦截器 消费时所有的record要先通过拦截器过滤
- Time 时间
- ConsumerNetworkClient
- SubscriptionState 用于保存订阅类型 订阅的主题等信息
- Metadata 元数据
- retryBackoffMs 重试时间
- requestTimeoutMs 请求超时时间
- closed 关闭标志
- List<PartitionAssignor> assignors 分区分配策略
- currentThread 持有消费者的线程id
- refCount 重入次数
源码!!!
按照使用的顺序来梳理咱们的源码,首先就是构造函数了!
private KafkaConsumer(ConsumerConfig config,
Deserializer<K> keyDeserializer,
Deserializer<V> valueDeserializer) {
try {
// clientId init
// groupId init
// log init
// requestTime init (need to bigger than sessionTimeOut && fetchMaxWaitMs)
// time init (Time.System)
// metrics init
// retryBackoffMs init
// load interceptors and make sure they get clientId
// get Key&Value deserializer from config
// 拦截器实现ClusterResourceListener接口就可以监听元数据的变化
// metadata init
// 将服务器ip加入元数据
ChannelBuilder channelBuilder = ClientUtils.createChannelBuilder(config);
IsolationLevel isolationLevel = IsolationLevel.valueOf(
config.getString(ConsumerConfig.ISOLATION_LEVEL_CONFIG).toUpperCase(Locale.ROOT));
Sensor throttleTimeSensor = Fetcher.throttleTimeSensor(metrics, metricsRegistry.fetcherMetrics);
int heartbeatIntervalMs = config.getInt(ConsumerConfig.HEARTBEAT_INTERVAL_MS_CONFIG);
NetworkClient netClient = new NetworkClient(
new Selector(config.getLong(ConsumerConfig.CONNECTIONS_MAX_IDLE_MS_CONFIG), metrics, time, metricGrpPrefix, channelBuilder, logContext),
this.metadata,
clientId,
100, // a fixed large enough value will suffice for max in-flight requests
config.getLong(ConsumerConfig.RECONNECT_BACKOFF_MS_CONFIG),
config.getLong(ConsumerConfig.RECONNECT_BACKOFF_MAX_MS_CONFIG),
config.getInt(ConsumerConfig.SEND_BUFFER_CONFIG),
config.getInt(ConsumerConfig.RECEIVE_BUFFER_CONFIG),
config.getInt(ConsumerConfig.REQUEST_TIMEOUT_MS_CONFIG),
time,
true,
new ApiVersions(),
throttleTimeSensor,
logContext);
this.client = new ConsumerNetworkClient(
logContext,
netClient,
metadata,
time,
retryBackoffMs,
config.getInt(ConsumerConfig.REQUEST_TIMEOUT_MS_CONFIG),
heartbeatIntervalMs); //Will avoid blocking an extended period of time to prevent heartbeat thread starvation
OffsetResetStrategy offsetResetStrategy = OffsetResetStrategy.valueOf(config.getString(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG).toUpperCase(Locale.ROOT));
this.subscriptions = new SubscriptionState(offsetResetStrategy);
this.assignors = config.getConfiguredInstances(
ConsumerConfig.PARTITION_ASSIGNMENT_STRATEGY_CONFIG,
PartitionAssignor.class);
this.coordinator = new ConsumerCoordinator(logContext,
this.client,
groupId,
config.getInt(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG),
config.getInt(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG),
heartbeatIntervalMs,
assignors,
this.metadata,
this.subscriptions,
metrics,
metricGrpPrefix,
this.time,
retryBackoffMs,
config.getBoolean(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG),
config.getInt(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG),
this.interceptors,
config.getBoolean(ConsumerConfig.EXCLUDE_INTERNAL_TOPICS_CONFIG),
config.getBoolean(ConsumerConfig.LEAVE_GROUP_ON_CLOSE_CONFIG));
this.fetcher = new Fetcher<>(
logContext,
this.client,
config.getInt(ConsumerConfig.FETCH_MIN_BYTES_CONFIG),
config.getInt(ConsumerConfig.FETCH_MAX_BYTES_CONFIG),
config.getInt(ConsumerConfig.FETCH_MAX_WAIT_MS_CONFIG),
config.getInt(ConsumerConfig.MAX_PARTITION_FETCH_BYTES_CONFIG),
config.getInt(ConsumerConfig.MAX_POLL_RECORDS_CONFIG),
config.getBoolean(ConsumerConfig.CHECK_CRCS_CONFIG),
this.keyDeserializer,
this.valueDeserializer,
this.metadata,
this.subscriptions,
metrics,
metricsRegistry.fetcherMetrics,
this.time,
this.retryBackoffMs,
this.requestTimeoutMs,
isolationLevel);
config.logUnused();
AppInfoParser.registerAppInfo(JMX_PREFIX, clientId, metrics);
log.debug("Kafka consumer initialized");
} catch (Throwable t) {
// call close methods if internal objects are already constructed
// this is to prevent resource leak. see KAFKA-2121
close(0, true);
// now propagate the exception
throw new KafkaException("Failed to construct kafka consumer", t);
}
}
poll函数
最重要的当属poll函数了
public ConsumerRecords<K, V> poll(long timeout) {
acquireAndEnsureOpen();
try {
if (timeout < 0)
throw new IllegalArgumentException("Timeout must not be negative");
if (this.subscriptions.hasNoSubscriptionOrUserAssignment())
throw new IllegalStateException("Consumer is not subscribed to any topics or assigned any partitions");
// poll for new data until the timeout expires
long start = time.milliseconds();
long remaining = timeout;
do {
Map<TopicPartition, List<ConsumerRecord<K, V>>> records = pollOnce(remaining);
if (!records.isEmpty()) {
// before returning the fetched records, we can send off the next round of fetches
// and avoid block waiting for their responses to enable pipelining while the user
// is handling the fetched records.
//
// NOTE: since the consumed position has already been updated, we must not allow
// wakeups or any other errors to be triggered prior to returning the fetched records.
if (fetcher.sendFetches() > 0 || client.hasPendingRequests())
client.pollNoWakeup();
return this.interceptors.onConsume(new ConsumerRecords<>(records));
}
long elapsed = time.milliseconds() - start;
remaining = timeout - elapsed;
} while (remaining > 0);
return ConsumerRecords.empty();
} finally {
release();
}
}
消息的读取实际发生在pollOnce函数中
private Map<TopicPartition, List<ConsumerRecord<K, V>>> pollOnce(long timeout) {
client.maybeTriggerWakeup();
long startMs = time.milliseconds();
coordinator.poll(startMs, timeout);
// Lookup positions of assigned partitions
boolean hasAllFetchPositions = updateFetchPositions();
// if data is available already, return it immediately
Map<TopicPartition, List<ConsumerRecord<K, V>>> records = fetcher.fetchedRecords();
if (!records.isEmpty())
return records;
// send any new fetches (won't resend pending fetches)
fetcher.sendFetches();
long nowMs = time.milliseconds();
long remainingTimeMs = Math.max(0, timeout - (nowMs - startMs));
long pollTimeout = Math.min(coordinator.timeToNextPoll(nowMs), remainingTimeMs);
// We do not want to be stuck blocking in poll if we are missing some positions
// since the offset lookup may be backing off after a failure
if (!hasAllFetchPositions && pollTimeout > retryBackoffMs)
pollTimeout = retryBackoffMs;
client.poll(pollTimeout, nowMs, new PollCondition() {
@Override
public boolean shouldBlock() {
// since a fetch might be completed by the background thread, we need this poll condition
// to ensure that we do not block unnecessarily in poll()
return !fetcher.hasCompletedFetches();
}
});
// after the long poll, we should check whether the group needs to rebalance
// prior to returning data so that the group can stabilize faster
if (coordinator.needRejoin())
return Collections.emptyMap();
return fetcher.fetchedRecords();
}
首先是协调器更新信息
判断是否已经有已获取的信息,如果有 直接返回已有的消息 如果没有 调用fetcher.sendFetches()发送一个异步的获取消息的请求
计算剩余时间,调用CousumerNetworkClient.poll(),
public void poll(long timeout, long now, PollCondition pollCondition, boolean disableWakeup) {
//·······
if (pendingCompletion.isEmpty() && (pollCondition == null || pollCondition.shouldBlock())) {
// if there are no requests in flight, do not block longer than the retry backoff
if (client.inFlightRequestCount() == 0)
timeout = Math.min(timeout, retryBackoffMs);
client.poll(Math.min(maxPollTimeoutMs, timeout), now);
now = time.milliseconds();
} else {
client.poll(0, now);
}
//········
}
会调用NetworkClient.poll()
public List<ClientResponse> poll(long timeout, long now) {
//······
long metadataTimeout = metadataUpdater.maybeUpdate(now);
try {
this.selector.poll(Utils.min(timeout, metadataTimeout, requestTimeoutMs));
} catch (IOException e) {
log.error("Unexpected error during I/O", e);
}
//······
}
更新元数据,调用seletor.poll()
public void poll(long timeout) throws IOException {
//······
if (hasStagedReceives() || !immediatelyConnectedKeys.isEmpty() || (madeReadProgressLastCall && dataInBuffers))
timeout = 0;
/* check ready keys */
long startSelect = time.nanoseconds();
int numReadyKeys = select(timeout);
long endSelect = time.nanoseconds();
this.sensors.selectTime.record(endSelect - startSelect, time.milliseconds());
if (numReadyKeys > 0 || !immediatelyConnectedKeys.isEmpty() || dataInBuffers) {
Set<SelectionKey> readyKeys = this.nioSelector.selectedKeys();
// Poll from channels that have buffered data (but nothing more from the underlying socket)
if (dataInBuffers) {
keysWithBufferedRead.removeAll(readyKeys); //so no channel gets polled twice
Set<SelectionKey> toPoll = keysWithBufferedRead;
keysWithBufferedRead = new HashSet<>(); //poll() calls will repopulate if needed
pollSelectionKeys(toPoll, false, endSelect);
}
// Poll from channels where the underlying socket has more data
pollSelectionKeys(readyKeys, false, endSelect);
// Clear all selected keys so that they are included in the ready count for the next select
readyKeys.clear();
pollSelectionKeys(immediatelyConnectedKeys, true, endSelect);
immediatelyConnectedKeys.clear();
} else {
madeReadProgressLastPoll = true; //no work is also "progress"
}
//······
}
在timeout时限内获取ready的channel,如果可写就写入。
回到pollOnce 检查是否需要重平衡,返回fetcher获取的消息,回到poll(),如果 消息不为空,交给拦截器,返回。为空如果未超时继续重试。
在查看selector.poll代码时 注释中提到,kafka可以选择明文和SSL加密两种方式传输数据,如果选择了SSL加密:
- 需要额外的缓冲区用于加解密
- requestedSize失效 无法限制单次获取的消息大小
另有一个stagedReceives的双端队列用于存放channel的response,每次读取时都尽可能多的读取放入该队列,但每次poll只从中获取一个(保证顺序),如果队列中已有元素,超时时间就将被设置为0
subscribe函数
subscriptions
- subscription 最近一次订阅的topic集合
- groupSubscription 订阅的所有的topic集合
如果传入的topic集合为空,会取消所有已订阅的主题,使用时需要小心判断是否为空。
RebalanceListener的处理与常见的参数不同,为空时不为null,而是一个实现的空listener,不要想当然的传null值。
如果是以正则模式订阅主题,需要从元数据中获取集群中所有的主题(如果在consumer_config中设置了排斥内部主题excludeInternalTopics如_consumer_offset就无法订阅该种主题 由协调器过滤)
assign
同样 订阅的分区为空时 等同于unsubscribe()
手动指定分区后,不接受分区重平衡
assign区别于增量的subscribe() 它是替换的。
如果开启了auto-commit 会在函数调用前有一个异步的commit
commitSync
所有的commit都是有coordinator来完成 而偏移存放在subcriptions的assignment中
seekToBeginnig()
如果传入的集合为空 isEmpty()为true 就会将所有的主题均从头开始
网友评论