KafkaProducer#Sender线程源码

特征

生命周期

  • Sender在KafkaProducer创建时就已创建,在waitOnMetadata()方法执行时会唤醒一次Sender线程,在消息累加器达到发送条件时会唤醒一次。
  • Sender实现了Runnable接口,并运行在单独的ioThread中,其run()调用了重载的run(long)

字段与方法

Sender

属性

  • Logger log
  • KafkaClient client
    kafka 网络通信客户端,主要封装与 broker 的网络通信。
  • RecordAccumulator accumulator
  • ProducerMetadata metadata
  • boolean guaranteeMessageOrder
    是否需要保证消息的顺序性。
  • int maxRequestSize
    调用 send 方法发送的最大请求大小,包括 key、消息体序列化后的消息总大小不能超过该值。通过参数 max.request.size 来设置。
  • short acks
  • int retries
  • Time time
    时间工具类
  • boolean running
    该线程状态,为 true 表示运行中。
  • boolean forceClose
    是否强制关闭,此时会忽略正在发送中的消息。
  • Sender.SenderMetrics sensors
    消息发送相关的统计指标收集器。
  • int requestTimeoutMs
    请求的超时时间。
  • long retryBackoffMs
  • ApiVersions apiVersions
  • TransactionManager transactionManager
    事务处理器。
  • Map<TopicPartition, List> inFlightBatches
    正在执行发送相关的消息批次。

方法

run()

Sender#Run

public void run() {
        this.log.debug("Starting Kafka producer I/O thread.");

        while(this.running) {
            try {
            	//1. Sender 线程在运行状态下主要的业务处理方法,将消息缓存区中的消息向 broker 发送。
                this.runOnce();
            } catch (Exception var5) {
                this.log.error("Uncaught error in kafka producer I/O thread: ", var5);
            }
        }

        this.log.debug("Beginning shutdown of Kafka producer I/O thread, sending remaining records.");

		//2. 如果主动关闭 Sender 线程,如果不是强制关闭,则如果缓存区还有消息待发送,再次调用 runOnce 方法将剩余的消息发送完毕后再退出。
        while(!this.forceClose && (this.accumulator.hasUndrained() || this.client.inFlightRequestCount() > 0 || this.hasPendingTransactionalRequests())) {
            try {
                this.runOnce();
            } catch (Exception var4) {
                this.log.error("Uncaught error in kafka producer I/O thread: ", var4);
            }
        }

        while(!this.forceClose && this.transactionManager != null && this.transactionManager.hasOngoingTransaction()) {
            if (!this.transactionManager.isCompleting()) {
                this.log.info("Aborting incomplete transaction due to shutdown");
                this.transactionManager.beginAbort();
            }

            try {
                this.runOnce();
            } catch (Exception var3) {
                this.log.error("Uncaught error in kafka producer I/O thread: ", var3);
            }
        }

		//3. 如果强制关闭 Sender 线程,则拒绝未完成提交的消息。
        if (this.forceClose) {
            if (this.transactionManager != null) {
                this.log.debug("Aborting incomplete transactional requests due to forced shutdown");
                this.transactionManager.close();
            }

            this.log.debug("Aborting incomplete batches due to forced shutdown");
            this.accumulator.abortIncompleteBatches();
        }

        try {
        	//4. 关闭 Kafka Client 即网络通信对象。
            this.client.close();
        } catch (Exception var2) {
            this.log.error("Failed to close network client", var2);
        }

        this.log.debug("Shutdown of Kafka producer I/O thread has completed.");
    }

接下来详解上面4步

runOnce()

Sender#runOnce

void runOnce() {
        ···//事务相关

        long currentTimeMs = this.time.milliseconds();
        long pollTimeout = this.sendProducerData(currentTimeMs); // @1
        this.client.poll(pollTimeout, currentTimeMs); // @2
    }

@1:sendProducerData(currentTimeMs)发送数据。
@2:利用网络客户端发送网络请求。

sendProducerData()

Sender#sendProducerData

Cluster cluster = this.metadata.fetch(); // @1
ReadyCheckResult result = this.accumulator.ready(cluster, now); // @2
step1:从累加区中获取已达发送条件的信息

@1:从队列元数据中获取集群信息。
@2:根据集群信息和当前时间判断哪些 topic 的哪些分区(节点)已经达到发送条件,获取达到条件的节点列表,以待后续根据网络条件进行筛选。
Sender#sendProducerData

Iterator iter;
if (!result.unknownLeaderTopics.isEmpty()) {
    iter = result.unknownLeaderTopics.iterator();

    while(iter.hasNext()) {
        String topic = (String)iter.next();
        this.metadata.add(topic, now);
    }

    this.log.debug("Requesting metadata update due to unknown leader topics from the batched records: {}", result.unknownLeaderTopics);
    this.metadata.requestUpdate(); // @1
}
step2:根据是否存在无路由信息的主题决定是否去broker拉取路由信息(分区的 leader 节点信息)

@1:拉去路由信息(主题的leader节点)
Sender#sendProducerData

iter = result.readyNodes.iterator();
long notReadyTimeout = 9223372036854775807L;
while(iter.hasNext()) {
    Node node = (Node)iter.next();
    if (!this.client.ready(node, now)) {
        iter.remove();
        notReadyTimeout = Math.min(notReadyTimeout, this.client.pollDelayMs(node, now));
    }
}
step3:移除在网络层面没有准备好的分区并计算该分区将处于未就绪状态的时间。

1、在网络环节没有准备好的标准如下:

  • 分区没有未完成的更新元素数据请求(metadata)。
  • 当前生产者与对端 broker 已建立连接并完成了 TCP 的三次握手。
  • 如果启用 SSL、ACL 等机制,相关状态都已就绪。
  • 该分区对应的连接正在处理中的请求数时是否超过设定值,默认为 5,可通过属性 max.in.flight.requests.per.connection 来设置。

2、client pollDelayMs 预估分区在接下来多久的时间间隔内都将处于未转变好状态(not ready),其标准如下:

  • 如果已与对端的 TCP 连接已创建好,并处于已连接状态,此时如果没有触发限流,则返回0,如果有触发限流,则返回限流等待时间。
  • 如果还位于对端建立 TCP 连接,则返回 Long.MAX_VALUE,因为连接建立好后,会唤醒发送线程的。

Sender#sendProducerData

Map<Integer, List<ProducerBatch>> batches = this.accumulator.drain(cluster, result.readyNodes, this.maxRequestSize, now); // @1
step4:根据过滤后的分区,从消息累加器中取出ProducerBatch并组成 nodeId:List

@1:创建clientRequest
注意:抽取后的 ProducerBatch 将不能再追加消息了,就算还有剩余空间可用。

Sender#sendProducerData

this.addToInflightBatches(batches);
List expiredBatches;
Iterator var11;
ProducerBatch expiredBatch;
if (this.guaranteeMessageOrder) {
    Iterator var9 = batches.values().iterator();
    while(var9.hasNext()) {
        expiredBatches = (List)var9.next();
        var11 = expiredBatches.iterator();
        while(var11.hasNext()) {
            expiredBatch = (ProducerBatch)var11.next();
            this.accumulator.mutePartition(expiredBatch.topicPartition);
        }
    }
}
step5:将抽取的 ProducerBatch 加入到 inFlightBatches 数据结构

Map<TopicPartition, List< ProducerBatch >> inFlightBatches,即按照 topic-partition 为键,存放已抽取的 ProducerBatch,这个属性的含义就是存储待发送的消息批次。可以根据该数据结构得知在消息发送时以分区为维度反馈 Sender 线程的“积压情况”,max.in.flight.requests.per.connection 就是来控制积压的最大数量,如果积压达到这个数值,针对该队列的消息发送会限流。

Sender#sendProducerData

this.accumulator.resetNextBatchExpiryTime();
List<ProducerBatch> expiredInflightBatches = this.getExpiredInflightBatches(now);
expiredBatches = this.accumulator.expiredBatches(now);
expiredBatches.addAll(expiredInflightBatches);
step6:从 inflightBatches 的 batches 中查找已过期的消息批次(ProducerBatch)

判断是否过期的标准是系统当前时间与 ProducerBatch 创建时间之差是否超过120s,过期时间可以通过参数 delivery.timeout.ms 设置。

Sender#sendProducerData

if (!expiredBatches.isEmpty()) {
            this.log.trace("Expired {} batches in accumulator", expiredBatches.size());
        }

        var11 = expiredBatches.iterator();

        while(var11.hasNext()) {
            expiredBatch = (ProducerBatch)var11.next();
            String errorMessage = "Expiring " + expiredBatch.recordCount + " record(s) for " + expiredBatch.topicPartition + ":" + (now - expiredBatch.createdMs) + " ms has passed since batch creation";
            this.failBatch(expiredBatch, -1L, -1L, new TimeoutException(errorMessage), false);
            if (this.transactionManager != null && expiredBatch.inRetry()) {
                this.transactionManager.markSequenceUnresolved(expiredBatch);
            }
        }
step7:将已超时的消息批次添加到返回的凭证中

通知该批消息发送失败,即通过设置 KafkaProducer#send 方法返回的凭证中的 FutureRecordMetadata 中的 ProduceRequestResult result,使之调用其 get 方法不会阻塞。

Sender#sendProducerData

this.sensors.updateProduceRequestMetrics(batches)
step8:收集统计指标

Sender#sendProducerData

long pollTimeout = Math.min(result.nextReadyCheckDelayMs, notReadyTimeout);
pollTimeout = Math.min(pollTimeout, this.accumulator.nextExpiryTimeMs() - now);
pollTimeout = Math.max(pollTimeout, 0L);
if (!result.readyNodes.isEmpty()) {
    this.log.trace("Nodes with data ready to send: {}", result.readyNodes);
    pollTimeout = 0L;
}
step9:设置下一次的发送延时

Sender#sendProducerData

this.sendProduceRequests(batches, now); // @1
private void sendProduceRequests(Map<Integer, List<ProducerBatch>> collated, long now) {
    for (Map.Entry<Integer, List<ProducerBatch>> entry : collated.entrySet())
        sendProduceRequest(now, entry.getKey(), acks, requestTimeoutMs, entry.getValue());
}
step10:按照 brokerId 分别构建发送请求

每一个 broker 会将多个 ProducerBatch 一起封装成一个请求进行发送,同一时间,每一个 与 broker 连接只会只能发送一个请求。
@1:注意,这里只是构建请求,并最终会通过 NetworkClient#send 方法,将该批数据设置到 NetworkClient 的待发送队列中,此时并没有触发真正的网络调用。

runOnce()的NetworkClient#poll发送网络请求

NetworkClient#poll

 public List<ClientResponse> poll(long timeout, long now) {
    ensureActive();

    if (!abortedSends.isEmpty()) {
        // If there are aborted sends because of unsupported version exceptions or disconnects,
        // handle them immediately without waiting for Selector#poll.
        List<ClientResponse> responses = new ArrayList<>();
        handleAbortedSends(responses);
        completeResponses(responses);
        return responses;
    }

    long metadataTimeout = metadataUpdater.maybeUpdate(now);   // @1
    try {
        this.selector.poll(Utils.min(timeout, metadataTimeout, defaultRequestTimeoutMs));    // @2
    } catch (IOException e) {
        log.error("Unexpected error during I/O", e);
    }

    // process completed actions
    long updatedNow = this.time.milliseconds();
    List<ClientResponse> responses = new ArrayList<>();            // @3
    handleCompletedSends(responses, updatedNow);
    handleCompletedReceives(responses, updatedNow);
    handleDisconnections(responses, updatedNow);
    handleConnections();
    handleInitiateApiVersionRequests(updatedNow);
    handleTimedOutRequests(responses, updatedNow);
    completeResponses(responses);                                               // @4
    return responses;
}

@1:尝试更新元数据。
@2:触发真正的网络通讯,该方法中会通过收到调用 NIO 中的 Selector#select() 方法,对通道的读写就绪事件进行处理,当写事件就绪后,就会将通道中的消息发送到远端的 broker。
@3:收集消息发送,消息接收、断开连接、API版本,超时等结果。
@4:依次对结果进行唤醒,此时会将响应结果设置到 KafkaProducer#send 方法返回的凭证中,从而唤醒发送客户端,完成一次完整的消息发送流程。

此处缺少一张Sender主要流程,以后再补吧。

Sender线程流程图

总结

KafkaProducer.send()负责向RecordAccumulator中添加消息,Sender线程负责网络IO操作向Broker发送消息。

  1. 使用RecordAccumulator从RecordAccumulator的缓存中筛选出向那些node发送消息,返回结果brokerId.List
  2. 然后调用NetworkClient.ready(node,now)过滤网络层面不符合的node
  3. 生成ProduceRequest网络请求(每个node节点对应一个请求)
  4. 调用NetworkClient#Selector.send()发送请求
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值