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
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。
好的,我了解了你的要求。以下是我对你的问题的回答: 首先,根据你的要求,我们需要动态建立3个缓冲区和3个邮箱。这可以使用RT-Thread的内存管理和邮箱功能来实现。可以使用以下代码: ```c #define MAIL_POOL_SIZE 16 static uint32_t mail_pool_buf_1[MAIL_POOL_SIZE]; static uint32_t mail_pool_buf_2[MAIL_POOL_SIZE]; static uint32_t mail_pool_buf_3[MAIL_POOL_SIZE]; static struct rt_mailbox mb_1; static struct rt_mailbox mb_2; static struct rt_mailbox mb_3; void init_mailbox(void) { rt_mailbox_init(&mb_1, "mb_1", &mail_pool_buf_1[0], MAIL_POOL_SIZE, sizeof(uint32_t)); rt_mailbox_init(&mb_2, "mb_2", &mail_pool_buf_2[0], MAIL_POOL_SIZE, sizeof(uint32_t)); rt_mailbox_init(&mb_3, "mb_3", &mail_pool_buf_3[0], MAIL_POOL_SIZE, sizeof(uint32_t)); } ``` 接下来,我们需要动态创建3个线程来发送邮件。每个sender线程都需要一个数据帧结构体data_frame。数据帧结构体定义如下: ```c typedef struct { uint32_t time_slice_ID; uint32_t data; } data_frame; ``` 可以使用以下代码来创建sender线程: ```c #define THREAD_STACK_SIZE 512 #define THREAD_PRIORITY 25 static rt_thread_t sender_thread_1 = RT_NULL; static rt_thread_t sender_thread_2 = RT_NULL; static rt_thread_t sender_thread_3 = RT_NULL; static data_frame data_frame_1; static data_frame data_frame_2; static data_frame data_frame_3; void sender_thread_entry(void *parameter) { rt_uint32_t index = *(rt_uint32_t *)parameter; rt_mailbox_t mb; data_frame *frame; switch (index) { case 1: mb = &mb_1; frame = &data_frame_1; break; case 2: mb = &mb_2; frame = &data_frame_2; break; case 3: mb = &mb_3; frame = &data_frame_3; break; default: return; } frame->time_slice_ID = 0; frame->data = 0; while (1) { /* 更新数据帧中的时间片和数据 */ frame->time_slice_ID++; frame->data = index; /* 发送数据帧到邮箱中 */ rt_err_t result = rt_mb_send_wait(mb, (rt_uint32_t)frame, RT_WAITING_FOREVER); if (result != RT_EOK) { rt_kprintf("send email failed: %d\n", result); } /* 等待200ms */ rt_thread_mdelay(200); } } void init_sender_threads(void) { rt_err_t result; result = rt_thread_init(&sender_thread_1, "sender_1", sender_thread_entry, (void *)1, &sender_thread_stack_1[0], THREAD_STACK_SIZE, THREAD_PRIORITY, 5); if (result == RT_EOK) { rt_thread_startup(&sender_thread_1); } result = rt_thread_init(&sender_thread_2, "sender_2", sender_thread_entry, (void *)2, &sender_thread_stack_2[0], THREAD_STACK_SIZE, THREAD_PRIORITY, 5); if (result == RT_EOK) { rt_thread_startup(&sender_thread_2); } result = rt_thread_init(&sender_thread_3, "sender_3", sender_thread_entry, (void *)3, &sender_thread_stack_3[0], THREAD_STACK_SIZE, THREAD_PRIORITY, 5); if (result == RT_EOK) { rt_thread_startup(&sender_thread_3); } } ``` 最后,我们需要创建3个线程来接收邮件并通过串口打印出来。可以使用以下代码来创建这些线程: ```c #define RECEIVER_STACK_SIZE 512 #define RECEIVER_PRIORITY 25 static rt_thread_t receiver_thread_1 = RT_NULL; static rt_thread_t receiver_thread_2 = RT_NULL; static rt_thread_t receiver_thread_3 = RT_NULL; void receiver_thread_entry(void *parameter) { rt_uint32_t index = *(rt_uint32_t *)parameter; rt_mailbox_t mb; data_frame *frame; switch (index) { case 1: mb = &mb_1; break; case 2: mb = &mb_2; break; case 3: mb = &mb_3; break; default: return; } while (1) { /* 从邮箱中接收数据帧 */ rt_uint32_t value; rt_err_t result = rt_mb_recv(mb, &value, RT_WAITING_FOREVER); if (result == RT_EOK) { frame = (data_frame *)value; rt_kprintf("receiver_%d: time_slice_ID=%d, data=%d\n", index, frame->time_slice_ID, frame->data); } } } void init_receiver_threads(void) { rt_err_t result; result = rt_thread_init(&receiver_thread_1, "receiver_1", receiver_thread_entry, (void *)1, &receiver_thread_stack_1[0], RECEIVER_STACK_SIZE, RECEIVER_PRIORITY, 5); if (result == RT_EOK) { rt_thread_startup(&receiver_thread_1); } result = rt_thread_init(&receiver_thread_2, "receiver_2", receiver_thread_entry, (void *)2, &receiver_thread_stack_2[0], RECEIVER_STACK_SIZE, RECEIVER_PRIORITY, 5); if (result == RT_EOK) { rt_thread_startup(&receiver_thread_2); } result = rt_thread_init(&receiver_thread_3, "receiver_3", receiver_thread_entry, (void *)3, &receiver_thread_stack_3[0], RECEIVER_STACK_SIZE, RECEIVER_PRIORITY, 5); if (result == RT_EOK) { rt_thread_startup(&receiver_thread_3); } } ``` 最后,在main函数中调用init_mailbox、init_sender_threads和init_receiver_threads函数即可完成任务。 ```c int main(void) { init_mailbox(); init_sender_threads(); init_receiver_threads(); return 0; } ``` 以上就是这个任务的实现。希望能够帮到你!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值