网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事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 则不会移除
}
如果你感觉参数有点多,难以看懂,就记住一个 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());
}
}
}
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%以上大数据知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新