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

img
img
img

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

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

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

在这里插入图片描述

文章目录

往期推荐

Kafka从成神到成仙系列


一、引言

初学一个技术,怎么了解该技术的源码至关重要。

对我而言,最佳的阅读源码的方式,那就是:不求甚解,观其大略

你如果进到庐山里头,二话不说,蹲下头来,弯下腰,就对着某棵树某棵小草猛研究而不是说先把庐山的整体脉络研究清楚了,那么你的学习方法肯定效率巨低而且特别痛苦。

最重要的还是慢慢地打击你的积极性,说我的学习怎么那么不 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 则不会移除
}

如果你感觉参数有点多,难以看懂,就记住一个 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) {


![img](https://img-blog.csdnimg.cn/img_convert/690e8084d81c2ffbdd75dc4dd7f2e006.png)
![img](https://img-blog.csdnimg.cn/img_convert/a3a32ec6324e5eb68473fb42e770425d.png)
![img](https://img-blog.csdnimg.cn/img_convert/3492adbe20408b85388b7f67565c2a88.png)

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

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

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

;
    selector.send(request.request());
}

public void send(Send send) {


[外链图片转存中...(img-dPeEA44E-1715701384815)]
[外链图片转存中...(img-NYK9RD7y-1715701384815)]
[外链图片转存中...(img-zLtZsy4p-1715701384815)]

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

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

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

  • 8
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值