【Kafka从成神到升仙系列 三】你真的了解 Kafka 的元数据嘛_kafka是基于netty吗(2)

本文详细解析了Kafka中元数据的请求过程,包括Producer首次发送消息时的等待、Sender线程的介入、元数据的更新策略以及与Broker的连接建立。内容涵盖Metadata类结构、Cluster信息、PartitionInfo和如何通过poll方法获取和处理元数据响应。
摘要由CSDN通过智能技术生成

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

我们首先解释一下 元数据(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 则不会移除
}

如果你感觉参数有点多,难以看懂,就记住一个 clusterneedUpdate 就好了,其他的不太重要

我们进一步看下 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());
                }
            }
}

2.3 第三次 poll 调用

当我们第三次调用 poll 时,这个时候我们发送的请求的响应回来了

// 处理响应
long updatedNow = this.time.milliseconds();
List<ClientResponse> responses = new ArrayList<>();
// 处理 inFlight 组件
handleCompletedSends(responses, updatedNow);
// 处理 metadata
handleCompletedReceives(responses, updatedNow);
// 处理断开的连接
handleDisconnections(responses, updatedNow);
// 记录任何新完成的连接
handleConnections();
// 处理超时的连接
handleTimedOutRequests(responses, updatedNow);

其他的我们暂时不研究,主要看 handleCompletedReceives 对于元数据的处理

private void handleCompletedReceives(List<ClientResponse> responses, long now) {
    for (NetworkReceive receive : this.selector.completedReceives()) {
        String source = receive.source();
        ClientRequest req = inFlightRequests.completeNext(source);
        Struct body = parseResponse(receive.payload(), req.request().header());
        if (!metadataUpdater.maybeHandleCompletedReceive(req, now, body))
            responses.add(new ClientResponse(req, now, false, body));
    }
}
// 如果当前的是 metadata 响应,则更新其元数据
public boolean maybeHandleCompletedReceive(ClientRequest req, long now, Struct body) {
    short apiKey = req.request().header().apiKey();
    if (apiKey == ApiKeys.METADATA.id && req.isInitiatedByNetworkClient()) {
        handleResponse(req.request().header(), body, now);
        return true;
    }
    return false;
}
// 拿到当前响应里面的各种信息,进行封装保存
private void handleResponse(RequestHeader header, Struct body, long now) {
    this.metadataFetchInProgress = false;
    MetadataResponse response = new MetadataResponse(body);
    Cluster cluster = response.cluster();
    // check if any topics metadata failed to get updated
    Map<String, Errors> errors = response.errors();
    if (cluster.nodes().size() > 0) {
        this.metadata.update(cluster, now);
    } else {
        this.metadata.failedUpdate(now);
    }
}

这里可以给大家看一下题主拉取的元数据的信息

{
    "brokers":[
        {
            "node\_id":"2",


![img](https://img-blog.csdnimg.cn/img_convert/738497464e95a01bb754c044e950f4ee.png)
![img](https://img-blog.csdnimg.cn/img_convert/5d7cc2c36c53b5458a04d1e515814aef.png)
![img](https://img-blog.csdnimg.cn/img_convert/038a2237e9c940f88b6255d4d9bcb4fa.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[需要这份系统化资料的朋友,可以戳这里获取](https://bbs.csdn.net/topics/618545628)**

数据的信息



{
“brokers”:[
{
“node_id”:“2”,

[外链图片转存中…(img-E5APT8v6-1714990808549)]
[外链图片转存中…(img-BUj5GZDl-1714990808550)]
[外链图片转存中…(img-CVeG6sts-1714990808550)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化资料的朋友,可以戳这里获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值