你如果进到庐山里头,二话不说,蹲下头来,弯下腰,就对着某棵树某棵小草猛研究而不是说先把庐山的整体脉络研究清楚了,那么你的学习方法肯定效率巨低而且特别痛苦。
最重要的还是慢慢地打击你的积极性,说我的学习怎么那么不 happy 啊,怎么那么没劲那,因为你的学习方法错了,大体读明白,先拿来用,用着用着,很多道理你就明白了。
先从整体上把关源码,再去扣一些细节问题。
举个简单的例子:
如果你刚接触 HashMap,你刚有兴趣去看其源码,在看 HashMap 的时候,有一个知识:当链表长度达到 8 之后,就变为了红黑树,小于 6 就变成了链表,当然,还和当前的长度有关。
这个时候,如果你去深究红黑树、为什么是 8 不是别的,又去查 泊松分布,最终会慢慢的搞死自己。
所以,正确的做法,我们先把这一部分给略过去,知道这个概念即可,等后面我们把整个庐山看完之后,再回过头抠细节。
当然,本章我们讲述 Kafka中的生产者怎么获取的元数据
二、Kafka为何不用 Netty
上篇文章我们讲述到,我们的 producer 会将消息发送至 RecordAccumulator
中,然后启动 Sender
线程
我们大概率可以猜测到,我们的 Sender
线程是和我们的 Broker
进行通信的,提到通信,不得不说一下 Netty
大家都知道,Netty
是一个优秀的 I/O
框架,但 Kafka
在通信方面并没有采用 Netty
,让人比较难以理解
当然,博主也查到了关于 kafka
开发者的回答:
一共两个原因:
1、由于性能问题,kafka 的通信过程并不需要 netty 那么庞大的通信体系
2、kafka客户端原始时期,需要让用户将整个东西作为依赖项包含其内,如果引入了 netty,那么每个人依赖的版本号不同,将会产生巨大的兼容问题
3、kafka的安全层和一些另外的问题,需要 kafka 自己来解决,而这些烦恼的问题,netty 中已经解决了
博主感觉,极大概率由于历史原因,现在就算换成 netty,一些代码也不容易重构,更何况现在 kafka 自研的 I/O 通信模型反响还可以,所以 kafka 一直都没使用 netty 的想法。
三、元数据是什么
如果我们的 Sender
线程想要连接 Broker
,最大的一个环节就是获取 Broker
的元数据
而元数据的获取,是通过 Sender
线程来获取的
Sender 线程是一个不断轮询的线程,类似我们之前提到的 EventLoop 线程
我们首先解释一下 元数据(Metadata ) 是个什么东西:
// 这个类被 client 线程和后台 sender 所共享,它只保存了所有 topic 的部分数据,当我们请求一个它上面没有的 topic meta 时,它会通过发送 metadata update 来更新 meta 信息,
// 如果 topic meta 过期策略是允许的,那么任何 topic 过期的话都会被从集合中移除,
// 但是 consumer 是不允许 topic 过期的因为它明确地知道它需要管理哪些 topic
public final class Metadata {
private static final Logger log = LoggerFactory.getLogger(Metadata.class);
public static final long TOPIC\_EXPIRY\_MS = 5 \* 60 \* 1000;
private static final long TOPIC\_EXPIRY\_NEEDS\_UPDATE = -1L;
private final long refreshBackoffMs; // metadata 更新失败时,为避免频繁更新 meta,最小的间隔时间,默认 100ms
private final long metadataExpireMs; // metadata 的过期时间, 默认 60,000ms
private int version; // 每更新成功1次,version自增1,主要是用于判断 metadata 是否更新
private long lastRefreshMs; // 最近一次更新时的时间(包含更新失败的情况)
private long lastSuccessfulRefreshMs; // 最近一次成功更新的时间(如果每次都成功的话,与前面的值相等, 否则,lastSuccessulRefreshMs < lastRefreshMs)
private Cluster cluster; // 集群中一些 topic 的信息
private boolean needUpdate; // 是都需要更新 metadata
/\* Topics with expiry time \*/
private final Map<String, Long> topics; // topic 与其过期时间的对应关系
private final List<Listener> listeners; // 事件监控者
private final ClusterResourceListeners clusterResourceListeners; //当接收到 metadata 更新时, ClusterResourceListeners的列表
private boolean needMetadataForAllTopics; // 是否强制更新所有的 metadata
private final boolean topicExpiryEnabled; // 默认为 true, Producer 会定时移除过期的 topic,consumer 则不会移除
}
如果你感觉参数有点多,难以看懂,就记住一个 cluster
和 needUpdate
就好了,其他的不太重要
我们进一步看下 cluster
的参数:主要是 broker、topic、partition 的一些对应信息
public final class Cluster {
// 从命名直接就看出了各个变量的用途
private final boolean isBootstrapConfigured;
private final List<Node> nodes; // node 列表
private final Set<String> unauthorizedTopics; // 未认证的 topic 列表
private final Set<String> internalTopics; // 内置的 topic 列表
private final Map<TopicPartition, PartitionInfo> partitionsByTopicPartition; // partition 的详细信息
private final Map<String, List<PartitionInfo>> partitionsByTopic; // topic 与 partition 的对应关系
private final Map<String, List<PartitionInfo>> availablePartitionsByTopic; // 可用(leader 不为 null)的 topic 与 partition 的对应关系
private final Map<Integer, List<PartitionInfo>> partitionsByNode; // node 与 partition 的对应关系
private final Map<Integer, Node> nodesById; // node 与 id 的对应关系
private final ClusterResource clusterResource;
}
// org.apache.kafka.common.PartitionInfo
// topic-partition: 包含 topic、partition、leader、replicas、isr
public class PartitionInfo {
private final String topic;
private final int partition;
private final Node leader;
private final Node[] replicas;
private final Node[] inSyncReplicas;
}
四、元数据的请求及获取
1、Producer 元数据获取
我们这块的元数据已经了解的差不多了,我们来看看元数据是怎么请求及获取的
当我们第一次发送 Kafka
消息时,会有一个 waitOnMetadata(record.topic(), this.maxBlockTimeMs)
方法
/\*\*
\* 第一次发送消息时,这里会判断当前是否拿到了元数据
\* 如果没有拿到元数据信息,这里会堵塞循环并唤醒 Sender 线程,让其帮忙更新元数据
\*/
long waitedOnMetadataMs = waitOnMetadata(record.topic(), this.maxBlockTimeMs);
// 等待元数据的更新
private long waitOnMetadata(String topic, long maxWaitMs) throws InterruptedException {
// 判断是否有元数据,没有的话,则一直循环堵塞
while (metadata.fetch().partitionsForTopic(topic) == null) {
int version = metadata.requestUpdate();
// 唤醒我们的 Sender 线程,让其更新元数据
sender.wakeup();
metadata.awaitUpdate(version, remainingWaitMs);
}
return time.milliseconds() - begin;
}
这里如同我们的注释所说,会唤醒我们的 Sender
线程,让其帮忙更新我们的 Metadata
2、Sender 获取元数据
2.1 第一次 poll 调用
当 Sender
线程收到唤醒时,第一轮直接调用 this.client.poll(pollTimeout, now)
方法,如下:
void run(long now) {
// 获取元数据中的 Cluster 信息
Cluster cluster = metadata.fetch();
this.client.poll(pollTimeout, now);
}
public List<ClientResponse> poll(long timeout, long now) {
// 判断是否需要更新 metadata,如果需要就更新
long metadataTimeout = metadataUpdater.maybeUpdate(now);
try {
this.selector.poll(Utils.min(timeout, metadataTimeout, requestTimeoutMs));
}
}
我们看一下 metadataUpdater.maybeUpdate
做了什么
@Override
public long maybeUpdate(long now) {
// 根据当前更新的时间判断是否需要更新
long timeToNextMetadataUpdate = metadata.timeToNextUpdate(now);
long timeToNextReconnectAttempt = Math.max(this.lastNoNodeAvailableMs + metadata.refreshBackoff() - now, 0);
long waitForMetadataFetch = this.metadataFetchInProgress ? Integer.MAX\_VALUE : 0;
long metadataTimeout = Math.max(Math.max(timeToNextMetadataUpdate, timeToNextReconnectAttempt),
waitForMetadataFetch);
if (metadataTimeout == 0) {
// 选择一个当前链接建立最少的node
Node node = leastLoadedNode(now);
// 更新元数据
maybeUpdate(now, node);
}
return metadataTimeout;
}
// 判断当前是否可以发送请求,可以的话将 metadata 请求加入到发送列表中
private void maybeUpdate(long now, Node node) {
String nodeConnectionId = node.idString();
// 通道是否已经准备完毕
if (canSendRequest(nodeConnectionId)) {
this.metadataFetchInProgress = true;
MetadataRequest metadataRequest;
if (metadata.needMetadataForAllTopics())
metadataRequest = MetadataRequest.allTopics();
else
metadataRequest = new MetadataRequest(new ArrayList<>(metadata.topics()));
ClientRequest clientRequest = request(now, nodeConnectionId, metadataRequest);
doSend(clientRequest, now);
} else if (connectionStates.canConnect(nodeConnectionId, now)) {
// 初始化链接
initiateConnect(node, now);
} else {
this.lastNoNodeAvailableMs = now;
}
}
}
// 主要做的初始化与Broker 的连接
private void initiateConnect(Node node, long now) {
String nodeConnectionId = node.idString();
try {
this.connectionStates.connecting(nodeConnectionId, now);
selector.connect(nodeConnectionId,
new InetSocketAddress(node.host(), node.port()),
this.socketSendBuffer,
this.socketReceiveBuffer);
}
}
这里会进入一个 selector.connect
方法,在这个里面进行与 Broker
的连接
public void connect(String id, InetSocketAddress address, int sendBufferSize, int receiveBufferSize) throws IOException {
// 经典的 NIO 的实现(之前netty中聊过)
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
Socket socket = socketChannel.socket();
socket.setKeepAlive(true);
// 发送的buffer
if (sendBufferSize != Selectable.USE\_DEFAULT\_BUFFER\_SIZE)
socket.setSendBufferSize(sendBufferSize);
// 接受的buffer
if (receiveBufferSize != Selectable.USE\_DEFAULT\_BUFFER\_SIZE)
socket.setReceiveBufferSize(receiveBufferSize);
// 设置TCP的状态
socket.setTcpNoDelay(true);
boolean connected;
try {
// 直接连接服务端(这也是netty讲过的)
connected = socketChannel.connect(address);
} catch (UnresolvedAddressException e) {
socketChannel.close();
throw new IOException("Can't resolve address: " + address, e);
}
// 注册接受事件(这个事件只能客户端注册)
SelectionKey key = socketChannel.register(nioSelector, SelectionKey.OP\_CONNECT);
}
2.2 第二次 poll 调用
这个时候已经与我们的 Broker
建立了连接,我们下一步要找在哪里发送的请求,也就是 write
方法。
当我们第二次调用 poll
方法时, 再次到达 maybeUpdate
方法时,这个时候我们会走 canSendRequest(nodeConnectionId)
分支
// 由于上述我们已经建立了连接,这里已经可以发送了
if (canSendRequest(nodeConnectionId)) {
this.metadataFetchInProgress = true;
MetadataRequest metadataRequest;
if (metadata.needMetadataForAllTopics())
metadataRequest = MetadataRequest.allTopics();
else
metadataRequest = new MetadataRequest(new ArrayList<>(metadata.topics()));
ClientRequest clientRequest = request(now, nodeConnectionId, metadataRequest);
doSend(clientRequest, now);
}
一开始博主以为这里的 doSend
已经完成了发送,实际上并没有,大家一定要注意这一点
private void doSend(ClientRequest request, long now) {
request.setSendTimeMs(now);
this.inFlightRequests.add(request);
selector.send(request.request());
}
public void send(Send send) {
KafkaChannel channel = channelOrFail(send.destination());
this.send = send;
this.transportLayer.addInterestOps(SelectionKey.OP\_WRITE);
}
这里的 doSend
主要将我们本次请求放在 inFlight
这个组件里面(inFlight:主要控制当前发送的请求量
)
另外,这里注册了一个 SelectionKey.OP_WRITE
事件,这个我们之前 Netty
大结局时讲过,有兴趣的可以去看一下
这里简单说一下,注册了 SelectionKey.OP_WRITE
事件,当内核的缓存区有空闲时,会触发该事件。
当注册完事件后,我们会调用 this.selector.poll(Utils.min(timeout, metadataTimeout, requestTimeoutMs))
方法,正式将我们的请求发送至 Broker
这里当前所有准备好的 key
拿出来,执行 pollSelectionKeys
去执行每一个 key
public void poll(long timeout) throws IOException {
/\* check ready keys \*/
long startSelect = time.nanoseconds();
// 执行 nioSelector.selectNow() 方法,得到所有触发的 key
int readyKeys = select(timeout);
long endSelect = time.nanoseconds();
currentTimeNanos = endSelect;
this.sensors.selectTime.record(endSelect - startSelect, time.milliseconds());
if (readyKeys > 0 || !immediatelyConnectedKeys.isEmpty()) {
pollSelectionKeys(this.nioSelector.selectedKeys(), false);
pollSelectionKeys(immediatelyConnectedKeys, true);
}
}
循环去遍历每一个 key
,由于我们前面注册了 OP_WRITE
,这里一定有一个写的 key
,通过 channel.write()
方法写入到内核并发送至服务端
private void pollSelectionKeys(Iterable<SelectionKey> selectionKeys, boolean isImmediatelyConnected) {
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
iterator.remove();
KafkaChannel channel = channel(key);
try {
if (channel.ready() && key.isWritable()) {
Send send = channel.write();
if (send != null) {
this.completedSends.add(send);
this.sensors.recordBytesSent(channel.id(), send.size());
![img](https://img-blog.csdnimg.cn/img_convert/e8bc5a847095c17f8ac7bbc967f0616d.png)
![img](https://img-blog.csdnimg.cn/img_convert/16c05089ec417f993418f9e4565e56c1.png)
![img](https://img-blog.csdnimg.cn/img_convert/4aa5e0572b8d0c8ce648ae4fe4fe885b.png)
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**
this.sensors.recordBytesSent(channel.id(), send.size());
[外链图片转存中...(img-b2Tq00IO-1714170233261)]
[外链图片转存中...(img-pR7NHcES-1714170233262)]
[外链图片转存中...(img-4AgE1utv-1714170233262)]
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**
**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**