Kafka Fetch Session剖析
1.概述
最近有同学留言在使用Kafka的过程中遇到一些问题,比如在拉取的Topic中的数据时会抛出一些异常,今天笔者就为大家来分享一下Kafka的Fetch流程。
2.内容
2.1 背景
首先,我们来了解一下,Fetch Session的目标。Kafka在1.1.0以后的版本中优化了Fetch问题,引入了Fetch Session,Kafka由Broker来提供服务(通信、数据交互等)。每个分区会有一个Leader Broker,Broker会定期向Leader Broker发送Fetch请求,来获取数据,而对于分区数较大的Topic来说,需要发出的Fetch请求就会很大。这样会有一个问题:
- Follower感兴趣的分区集很少改变,然而每个FetchRequest必须枚举Follower感兴趣的所有分区集合;
- 当上一个FetchRequest只会分区中没有任何改变,仍然必须发回关于该分区的所有元数据,其中包括分区ID、分区的起始Offset、以及能够请求的最大字节数等。
并且,这些问题与系统中现存分区的数量成线性比例,例如,假设Kafka集群中有100000个分区,其中大多数分区很少接收新消息。该系统中的Broker仍然会来回发送非常大的FetchRequest和FetchResponse,即使每秒添加的实际消息数据很少。随着分区数量的增长,Kafka使用越来越多的网络带宽来回传递这些消息。
当Kafka被调整为较低延迟时,这些低效会变得更严重。如果我们将每秒发送的FetchRequest数量增加一倍,我们应该期望在缩短的轮询间隔内有更多的分区没有改变。而且,我们无法在每个FetchRequest和FetchResponse中分摊每个分区发送元数据的所需要的带宽资源,这将会导致Kafka需要使用更多的网络带宽。
2.2 优化
为了优化上述问题,Kafka增加了增量拉取分区的概念,从而减少客户端每次拉取都需要拉取全部分区的问题。Fetch Session与网络编程中的Session类似,可以认为它是有状态的,这里的状态值的是知道它需要拉取哪些分区的数据,比如第一次拉取的分区0中的数据,后续分区0中没有了数据,就不需要拉取分区0了,FetchSession数据结构如下
class FetchSession(val id: Int, // sessionid是随机32位数字,用于鉴权,防止客户端伪造
val privileged: Boolean, // 是否授权
val partitionMap: FetchSession.CACHE_MAP,// 缓存数据CachedPartitionMap
val creationMs: Long, // 创建Session的时间
var lastUsedMs: Long, // 上次使用会话的时间,由FetchSessionCache更新
var epoch: Int) // 获取会话序列号
需要注意的是,Fetch Epoch是一个单调递增的32位计数器,它在处理请求N之后,Broker会接收请求N+1,序列号总是大于0,在达到最大值后,它会回到1。
如果Fetch Session支持增量Fetch,那么它将维护增量Fetch中每个分区的信息,关于每个分区,它需要维护:
- Topic名称
- 分区ID
- 该分区的最大字节数
- Fetch偏移量
- HighWaterMark
- FetcherLogStartOffset
- LeaderLogStartOffset
其中,Topic名称、分区ID来自于TopicPartition,最大字节数、Fetch偏移量、FetcherLogStartOffset来自于最近的FetcherRequest,HighWaterMark、LocalLogStartOffset来自于本地的Leader。因为Follower活着Consumer发出的请求都会与分区Leader进行交互,所以FetchSession也是记录在Leader节点上的。
对于客户端来说,什么时候一个分区会被包含到增量的拉取请求中:
- Client通知Broker,分区的maxBytes,fetchOffset,LogStartOffset改变了;
- 分区在之前的增量拉取会话中不存在,Client想要增加这个分区,从而来拉取新的分区;
- 分区在增量拉取会话中,Client要删除。
对于服务端来说,增量分区包含到增量的拉取响应中:
- Broker通知Client分区的HighWaterMark或者brokerLogStartOffset改变了;
- 分区有新的数据
在Fetch.java类中,方法sendFetches(): prepareFetchRequests创建FetchSessionHandler.FetchRequestData。 构建拉取请求通过FetchSessionHandler.Builder,builder.add(partition, PartitionData)会添加next: 即要拉取的分区。构建时调用Builder.build(),针对Full进行拉取,代码片段如下:
if (nextMetadata.isFull()) { // epoch为0或者-1
if (log.isDebugEnabled()) {
log.debug("Built full fetch {} for node {} with {}.",
nextMetadata, node, partitionsToLogString(next.keySet()));
}
sessionPartitions = next; // next为之前动态增加的分区
next = null; // 本地全量拉取,下次next为null
Map<TopicPartition, PartitionData> toSend =
Collections.unmodifiableMap(new LinkedHashMap<>(sessionPartitions));
return new FetchRequestData(toSend, Collections.emptyList(), toSend, nextMetadata);
}
收到响应结果后,调用FetchSessionHandler.handleResponse()方法。 假如第一次是全量拉取,响应结果没有出错时,nextMetadata.isFull()仍然为true。 服务端创建了一个新的session(随机的唯一ID),客户端的Fetch SessionId会设置为服务端返回的sessionId, 并且epoch会增加1。这样下次客户端的拉取就不再是全量,而是增量了(toSend, toForget两个集合容器,分别表示要拉取的和不需要拉取的)。 当服务端正常处理(这次不会生成新的session),客户端也正常处理响应,则sessionId不会增加,但是epoch会增加1。
public boolean handleResponse(FetchResponse<?> response) {
if (response.error() != Errors.NONE) {
log.info("Node {} was unable to process the fetch request with {}: {}.",
node, nextMetadata, response.error()); // 当集群session超过最大阀值,会出现这个异常信息
if (response.error() == Errors.FETCH_SESSION_ID_NOT_FOUND) {
nextMetadata = FetchMetadata.INITIAL;
} else {
nextMetadata = nextMetadata.nextCloseExisting();
}
return false;
}
if (nextMetadata.isFull()) {
if (response.responseData().isEmpty() && response.throttleTimeMs() > 0) {
// Normally, an empty full fetch response would be invalid. However, KIP-219
// specifies that if the broker wants to throttle the client, it will respond
// to a full fetch request with an empty response and a throttleTimeMs
// value set. We don't want to log this with a warning, since it's not an error.
// However, the empty full fetch response can't be processed, so it's still appropriate
// to return false here.
if (log.isDebugEnabled()) {
log.debug("Node {} sent a empty full fetch response to indicate that this " +
"client should be throttled for {} ms.", node, response.throttleTimeMs());
}
nextMetadata = FetchMetadata.INITIAL;
return false;
}
String problem = verifyFullFetchResponsePartitions(response);
if (problem != null) {
log.info("Node {} sent an invalid full fetch response with {}", node, problem);
nextMetadata = FetchMetadata.INITIAL;
return false;
} else if (response.sessionId() == INVALID_SESSION_ID) {
if (log.isDebugEnabled())
log.debug("Node {} sent a full fetch response{}", node, responseDataToLogString(response));
nextMetadata = FetchMetadata.INITIAL;
return true;
} else {
// The server created a new incremental fetch session.
if (log.isDebugEnabled())
log.debug("Node {} sent a full fetch response that created a new incremental " +
"fetch session {}{}", node, response.sessionId(), responseDataToLogString(response));
nextMetadata = FetchMetadata.newIncremental(response.sessionId());
return true;
}
} else {
String problem = verifyIncrementalFetchResponsePartitions(response);
if (problem != null) {
log.info("Node {} sent an invalid incremental fetch response with {}", node, problem);
nextMetadata = nextMetadata.nextCloseExisting();
return false;
} else if (response.sessionId() == INVALID_SESSION_ID) {
// The incremental fetch session was closed by the server.
if (log.isDebugEnabled())
log.debug("Node {} sent an incremental fetch response closing session {}{}",
node, nextMetadata.sessionId(), responseDataToLogString(response));
nextMetadata = FetchMetadata.INITIAL;
return true;
} else {
// The incremental fetch session was continued by the server.
// We don't have to do anything special here to support KIP-219, since an empty incremental
// fetch request is perfectly valid.
if (log.isDebugEnabled())
log.debug("Node {} sent an incremental fetch response with throttleTimeMs = {} " +
"for session {}{}", node, response.throttleTimeMs(), response.sessionId(),
responseDataToLogString(response));
nextMetadata = nextMetadata.nextIncremental();
return true;
}
}
}
Broker处理拉取请求是,会创建不同类型的FetchContext,类型如下:
- SessionErrorContext:拉取会话错误(例如,epoch不相等)
- SessionlessFetchContext:不需要拉取会话
- IncrementalFetchContext:增量拉取
- FullFetchContext:全量拉取
在KafkaApis#handleFetchRequest()中,代码片段如下:
val fetchContext = fetchManager.newContext(
fetchRequest.metadata,
fetchRequest.fetchData,
fetchRequest.toForget,
fetchRequest.isFromFollower) // ...... if (fetchRequest.isFromFollower) {
// We've already evaluated against the quota and are good to go. Just need to record it now.
unconvertedFetchResponse = fetchContext.updateAndGenerateResponseData(partitions)
val responseSize = KafkaApis.sizeOfThrottledPartitions(versionId, unconvertedFetchResponse, quotas.leader)
quotas.leader.record(responseSize)
trace(s"Sending Fetch response with partitions.size=${unconvertedFetchResponse.responseData.size}, " +
s"metadata=${unconvertedFetchResponse.sessionId}")
requestHelper.sendResponseExemptThrottle(request, createResponse(0), Some(updateConversionStats))
}
2.3 Fetch Session缓存
因为Fetch Session使用的是Leader上的内存,所以我们需要限制在任何给定时间内的内存量,因此,每个Broker将只创建有限数量的增量Fetch Session。以下,有两个公共参数,用来配置Fetch Session的缓存:
- max.incremental.fetch.session.cache.slots:用来限制每台Broker上最大Fetch Session数量,默认1000
- min.incremental.fetch.session.eviction.ms:从缓存中逐步增量获取会话之前等待的最短时间,默认120000
这里需要注意的时候,该属性属于read-only。Kafka Broker配置中有三种类型,它们分别是:
类型 | 说明 |
read-only | 修改参数值后,需要重启Broker才能生效 |
per-broker | 修改参数值后,只会在对应的Broker上生效,不需要重启,属于动态参数 |
cluster-wide | 修改参数值后,整个集群范围内会生效,不需要重启,属于动态参数 |
当服务器收到创建增量Fetch Session请求时,它会将新的Session与先有的Session进行比较,只有在下列情况下,新Session才会有效:
- 新Session在Follower里面;
- 现有Session已停止,且超过最小等待时间;
- 现有Session已停止,且超过最小等待时间,并且新Session有更多的分区。
这样可以实现如下目标:
- Follower优先级高于消费者;
- 随着时间的推移,非活跃的Session将被替换;
- 大请求(从增量中收益更多)被优先处理;
- 缓存抖动是有限的,避免了昂贵的Session重建时。
2.4 公共接口
新增了如下错误类型:
- FetchSessionIdNotFound:当客户端请求引用服务器不知道的Fetch Session时,服务器将使用此错误代码进行响应。如果存在客户端错误,或者服务器退出了Fetch Session,也会出现这种错误;
- InvalidFetchSessionEpochException:当请求的Fetch Session Epoch与预期不相同时,服务器将使用此错误代码来进行响应。
2.5 FetchRequest元数据含义
请求SessionID | 请求SessionEpoch | 含义 |
0 | -1 | 全量拉取(没有使用或者创建Session时) |
0 | 0 | 全量拉取(如果是新的Session,Epoch从1开始) |
$ID | 0 | 关闭表示为$ID的增量Fetch Session,并创建一个新的全量Fetch(如果是新的Session,Epoch从1开始) |
$ID | $EPOCH | 如果ID和EPOCH是正确的,创建一个增量Fetch |
2.6 FetchResponse元数据含义
Request SessionID | 含义 |
0 | 没有Fetch Session是创建新的 |
$ID | 下一个请求会使增量Fetch请求,并且SessionID是$ID |
3.总结
Client和Broker的Fetch过程可以总结如下图所示:
4.结束语
这篇博客就和大家分享到这里,如果大家在研究学习的过程当中有什么问题,可以加群进行讨论或发送邮件给我,我会尽我所能为您解答,与君共勉!
另外,博主出书了《Kafka并不难学》和《Hadoop大数据挖掘从入门到进阶实战》,喜欢的朋友或同学, 可以在公告栏那里点击购买链接购买博主的书进行学习,在此感谢大家的支持。关注下面公众号,根据提示,可免费获取书籍的教学视频。
Kafka Fetch Session剖析的更多相关文章
- 关于Kafka Fetch Session的讨论
Kafka在1.1.0版本引入了fetch session的概念,旨在降低“无效”FETCH请求对集群带宽资源的占用.故事的背景是这样的: 众所周知,Kafka的broker和consumer都会定期 ...
- Kafka日志压缩剖析
1.概述 最近有些同学在学习Kafka时,问到Kafka的日志压缩(Log Compaction)问题,对于Kafka的日志压缩有些疑惑,今天笔者就为大家来剖析一下Kafka的日志压缩的相关内容. 2 ...
- Kafka 源码剖析
1.概述 在对Kafka使用层面掌握后,进一步提升分析其源码是极有必要的.纵观Kafka源码工程结构,不算太复杂,代码量也不算大.分析研究其实现细节难度不算太大.今天笔者给大家分析的是其核心处理模块, ...
- Apache Kafka 源码剖析
Getting Start 下载 http://kafka.apache.org/ 优点和应用场景 Kafka消息驱动,符合发布-订阅模式,优点和应用范围都共通 发布-订阅模式优点 解耦合 : 两个应 ...
- Kafka底层原理剖析(近万字建议收藏)
Kafka 简介 Apache Kafka 是一个分布式发布-订阅消息系统.是大数据领域消息队列中唯一的王者.最初由 linkedin 公司使用 scala 语言开发,在2010年贡献给了Apache ...
- JavaWeb项目架构之Kafka分布式日志队列
架构.分布式.日志队列,标题自己都看着唬人,其实就是一个日志收集的功能,只不过中间加了一个Kafka做消息队列罢了. kafka介绍 Kafka是由Apache软件基金会开发的一个开源流处理平台,由S ...
- Kafka单节点及集群配置安装
一.单节点 1.上传Kafka安装包到Linux系统[当前为Centos7]. 2.解压,配置conf/server.property. 2.1配置broker.id 2.2配置log.dirs 2. ...
- kafka channle的应用案例
kafka channle的应用案例 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 最近在新公司负责大数据平台的建设,平台搭建完毕后,需要将云平台(我们公司使用的Ucloud的 ...
- Kafka丢失数据问题优化总结
数据丢失是一件非常严重的事情事,针对数据丢失的问题我们需要有明确的思路来确定问题所在,针对这段时间的总结,我个人面对kafka 数据丢失问题的解决思路如下: 是否真正的存在数据丢失问题,比如有很多时候 ...
随机推荐
- 推荐系统中的nlp知识
都是转自其他博客,好好学习! 概述: https://blog.csdn.net/starzhou/article/details/73930117 tf-idf https://blog.csdn. ...
- [LeetCode]231. Power of Two判断是不是2\3\4的幂
/* 用位操作,乘2相当于左移1位,所以2的幂只有最高位是1 所以问题就是判断你是不是只有最高位是1,怎判断呢 这些数-1后形成的数,除了最高位,后边都是1,如果n&n-1就可以判断了 如果是 ...
- 深入理解Redis系列之持久化
redis持久化配置 redis.conf // RDB配置 save 900 1 save 300 10 save 60 10000 // AOF配置 appendonly yes //AOF三种同 ...
- webapplicationContext之ServletContext等相关概念说明
1)ServletContext是一个全局的储存信息的空间,所有用户共用一个,其信息必须是线程安全且共享的. ServletContext有一个接口定义:ServletContext接口.此接口定义了 ...
- llinux文件相关指令
一---导读 首先我们来看这样一个小案例,假设张三要出差,按照 这样的路线进行 北京->上海,之后回到北京.再按照北京->天津->石家庄这样的路线进行出差(北京是根据地).假设现在张 ...
- WPF时间长度自定义选择控件TimeSpanBox
以下控件采用https://www.cnblogs.com/cssmystyle/archive/2011/01/17/1937361.html部分代码 以下控件采用https://www.cnblo ...
- 【转载】一种git commit前自动格式化的方式
查看原文 简介 这个系列为了解决一个问题:自动化的去管理代码风格和格式 前提:Linux,C语言,Clang 如何在每次commit的时候,将代码风格自动格式化后再提交commit,且格式化的内容必须 ...
- Sentry(v20.12.1) K8S 云原生架构探索,SENTRY FOR JAVASCRIPT Source Maps 详解
系列 Sentry-Go SDK 中文实践指南 一起来刷 Sentry For Go 官方文档之 Enriching Events Snuba:Sentry 新的搜索基础设施(基于 ClickHous ...
- java进阶(31)--TreeSet集合、TreeMap集合、自平衡二叉树
一.TreeSet集合简单 1.TreeSet集合底层是一个TreeMap 2.TreeMap集合底层是一个二叉树 3.放到TreeSet集合的元素等同于放到TreeMap集合的Key部分 4.Tre ...
- session、cookie、token的区别
从安全性优先级来说: 1.优先级 Cookie<session<token 2. 安全性 Cookie: ①cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺 ...