Kafka消息写入流程

Kafka消息写入流程

0,写入消息简要流程图

在这里插入图片描述

1,从示例开始

在Kafka中,Producer实例是线程安全的,通常一个Producer的进程只需要生成一个Producer实例.
这样比一个进程中生成多个Producer实例的效率反而会更高.
在Producer的配置中,可以配置Producer的每个batch的内存缓冲区的大小默认16kb,或者多少ms提交一次,
这种设计参考了Tcp的Nagle算法,让网络传输尽可能的发送大的数据块.

非事务型示例

Kafka 3.0开始,是否启用冥等性的enable.idempotence 配置默认为true.

此配置只能保证单分区上的幂等性,即一个幂等性Producer能够保证某个主题的一个分区上不出现重复消息,它无法保证多个分区的幂等性.

//构建生成`KafkaProducer`的配置项.
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("linger.ms", 200);props.put("batch.size", 16384);
//serializer建议使用byteArray/byteBuffer.
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
//生成实例并向kafka发送消息.
Producer<String, String> producer = new KafkaProducer<>(props);
for (int i = 0; i < 100; i++)
     producer.send(new ProducerRecord<String, String>("my-topic", Integer.toString(i), 
             Integer.toString(i)));
//所有操作结束,关闭producer.
producer.close();

事务型示例

  • 设置事务型Producer有2个要求(后续在分析kafka中的事务实现):
    • 和幂等性Producer一样,开启enable.idempotence = true.
    • 设置Producer端参数transactional.id. 此配置设置一个transactionId,当然最好能代表业务场景.
    • 在brokerServer的配置中,min.insync.replicas配置的值必须大于1.
//构建生成事务型`KafkaProducer`的配置项.
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("transactional.id", "my-transactional-id");
//serializer建议使用byteArray/byteBuffer.
Producer<String, String> producer = new KafkaProducer<>(props, new StringSerializer(), new StringSerializer());
//此时KafkaProducer的api并没有变化,只是通过producer直接开始事务即可.
producer.initTransactions();
try {
   
     producer.beginTransaction();
     for (int i = 0; i < 100; i++)
         producer.send(new ProducerRecord<>("my-topic", Integer.toString(i), 
              Integer.toString(i)));
     producer.commitTransaction();
} catch (ProducerFencedException | OutOfOrderSequenceException | AuthorizationException e) {
   
     producer.close();
} catch (KafkaException e) {
   
     // For all other exceptions, just abort the transaction and try again.
     producer.abortTransaction();
}
producer.close();

2,KafkaProducer初始化

a,配置项

"bootstrap.servers" :  brokerServer链接信息的配置,host:port,多个用","号分开.
"buffer.memory" : 默认值32mb,Producer端的内存缓冲区的大小.
"max.block.ms" : 默认值1分钟,当内存缓冲区被填满(生产速度大于了网络传输速度),producer的backOff时间.
"batch.size" : 默认值(16kb),内存缓冲区内每一个batch的大小,当producer写入达到一个batch后,此batch将会被提交.
"linger.ms" : 默认值(0),"batch.size"配合使用,当batch未达到大小,batch的最大内存缓冲时间.
   这个配置在根据node节点范围内有效,即对应node的partition中只要有一个超时,就会处理所有partition.
"request.timeout.ms" 默认(30),producer等待请求响应的超时时间,应该大于broker中的`replica.lag.time.max.ms`配置时间.
"delivery.timeout.ms" 默认(2分钟),send数据后(添加到内存缓冲区的时间),等待ack的超时时间,
    这个值应该大于requestTimeout+lingerMs的和.
"retry.backoff.ms" 默认值(100ms),请求失败后的重试间隔时间.
"max.request.size" 默认值(1mb),单次网络请求的send数据的上限(建议是batchSize的倍数).
"enable.idempotence" 默认值(true),是否启用冥等性.
"transactional.id" 没有默认值,配置一个字符串值,用于记录此producer对应的事务ID.
   跨多个producer的冥等性保证,但是broker节点最少需要三个.
"transaction.timeout.ms" 默认值(1分钟),用于配置transaction的超时时间.
"acks" 默认值(all/-1),可配置(all,0,1),producer响应ack的状态
   0=>表示不管broker是否写入成功.
   1=>表示只需要leader写入成功(这可能在副本切换时导致数据丢失)
   all/-1 => 需要所有副本都写入成功,冥等性必须设置为此值.
"max.in.flight.requests.per.connection" 默认值(5),单个node可同时进行的请求的数量,
    如果启用"enable.idempotence",这个值必须小于或等于5.
"metadata.max.age.ms" 默认值(5分钟),定时刷新metadata的时间周期.
"metadata.max.idle.ms" 默认值(5分钟),metadata的空闲时间,当超过这个时间metadata会被丢弃下次请求时重新获取.
"partitioner.class" 用于对record进行partition的区分, Partitioner接口的实现.
"partitioner.ignore.keys" 默认值(false),当设置为false,同时key不为null的情况下,使用hash分区,
     如果指定了partitioner.class,这个配置无效.
"partitioner.adaptive.partitioning.enable" 默认值(true),是否让处理更快的partition分区更多的处理消息.
"partitioner.availability.timeout.ms" 默认值0,与上面的配置配合使用
     如果Partition无法在指定的超时时间处理producer的消息,则认为parition不可用.
"compression.type" record压缩算法,可配置zstd,lz4,snappy, gzip

b,KafkaProducer实例初始化

Step=>1

根据是否配置enable.idempotence,默认值true,如果配置为true时,初始化TransactionManager实例.

this.transactionManager = configureTransactionState(config, logContext);
//初始化TransactionManager实例.
private TransactionManager configureTransactionState(ProducerConfig config,
                                                     LogContext logContext) {
   
    TransactionManager transactionManager = null;
    //只有`enable.idempotence`配置为`true`时,TransactionManager实例才会被初始化.
    if (config.getBoolean(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG)) {
   
        final String transactionalId = config.getString(ProducerConfig.TRANSACTIONAL_ID_CONFIG);
        final int transactionTimeoutMs = config.getInt(ProducerConfig.TRANSACTION_TIMEOUT_CONFIG);
        final long retryBackoffMs = config.getLong(ProducerConfig.RETRY_BACKOFF_MS_CONFIG);
        transactionManager = new TransactionManager(
            logContext,transactionalId,
            transactionTimeoutMs,retryBackoffMs,apiVersions
        );
        //根据是否配置`transactional.id`来判断是否开启事务.
        if (transactionManager.isTransactional())
            log.info("Instantiated a transactional producer.");
        else
            log.info("Instantiated an idempotent producer.");
    } else {
   
        // ignore unretrieved configurations related to producer transaction
        config.ignore(ProducerConfig.TRANSACTION_TIMEOUT_CONFIG);
    }
    return transactionManager;
}

Step=>2

生成用于producer使用的内存缓冲区RecordAccumulator,

所有对Producer的send操作都将向此accumulator的内存缓冲区内添加,由专门的Sender线程来负责发送并释放内存.
其内部的BufferPool即是accumulator使用的内存池,每一个batch都需要向此内存池申请内存.

在kafka中所有的消息写入都是以batch为基础,标准batch的大小由batch.size配置,默认为16kb.

boolean enableAdaptivePartitioning = partitioner == null &&
    config.getBoolean(ProducerConfig.PARTITIONER_ADPATIVE_PARTITIONING_ENABLE_CONFIG);
RecordAccumulator.PartitionerConfig partitionerConfig = new RecordAccumulator.PartitionerConfig(
    enableAdaptivePartitioning,
    config.getLong(ProducerConfig.PARTITIONER_AVAILABILITY_TIMEOUT_MS_CONFIG)
);
this.accumulator = new RecordAccumulator(logContext,
        config.getInt(ProducerConfig.BATCH_SIZE_CONFIG),
        this.compressionType, lingerMs(config),
        retryBackoffMs, deliveryTimeoutMs,partitionerConfig,
        metrics,PRODUCER_METRIC_GROUP_NAME,time,
        apiVersions,transactionManager,
         //环形内存缓冲区,其内部分为池化内存与非池化内存.
        new BufferPool(this.totalMemorySize, 
            config.getInt(ProducerConfig.BATCH_SIZE_CONFIG),
            metrics, time, PRODUCER_METRIC_GROUP_NAME
        )
);

Step=>3

根据BOOTSTRAP_SERVERS_CONFIG配置,初始化ProducerMetadata实例,此实例用于维护metadata在producer端的cache信息.

List<InetSocketAddress> addresses = ClientUtils.parseAndValidateAddresses(
        config.getList(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG),
        config.getString(ProducerConfig.CLIENT_DNS_LOOKUP_CONFIG));
if (metadata != null) {
   
    this.metadata = metadata;
} else {
   
    this.metadata = new ProducerMetadata(retryBackoffMs,
            config.getLong(ProducerConfig.METADATA_MAX_AGE_CONFIG),
            config.getLong(ProducerConfig.METADATA_MAX_IDLE_CONFIG),
            logContext,
            clusterResourceListeners,
            Time.SYSTEM);
    this.metadata.bootstrap(addresses);
}

Step=>4

生成producer向broker端发起请求的NetworkClient实例,并根据实例初始化并启动Sender线程.
此线程用于将RecordAccumulator中已经完成的batch发送到对应partition的leaderBroker端.

注意:此线程是一个守护线程(daemon).

this.sender = newSender(logContext, kafkaClient, this.metadata);
String ioThreadName = NETWORK_THREAD_PREFIX + " | " + clientId;
this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
this.ioThread.start();
//newSender生成网络处理线程的实现.
Sender newSender(LogContext logContext, KafkaClient kafkaClient, ProducerMetadata metadata) {
   
    int maxInflightRequests = producerConfig.getInt(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION);
    int requestTimeoutMs = producerConfig.getInt(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG);
    ChannelBuilder channelBuilder = ClientUtils.createChannelBuilder(producerConfig, time, logContext);
    ProducerMetrics metricsRegistry = new ProducerMetrics(this.metrics);
    Sensor throttleTimeSensor = Sender.throttleTimeSensor(metricsRegistry.senderMetrics);
    //生成用于向`broker`发起网络请求的NetworkClient实例.
    KafkaClient client = kafkaClient != null ? kafkaClient : new NetworkClient(
            new Selector(producerConfig.getLong(ProducerConfig.CONNECTIONS_MAX_IDLE_MS_CONFIG),
                    this.metrics, time, "producer", channelBuilder, logContext),
            metadata,
            clientId,
            maxInflightRequests,
            producerConfig.getLong(ProducerConfig.RECONNECT_BACKOFF_MS_CONFIG),
            producerConfig.getLong(ProducerConfig.RECONNECT_BACKOFF_MAX_MS_CONFIG),
            producerConfig.getInt(ProducerConfig.SEND_BUFFER_CONFIG),
            producerConfig.getInt(ProducerConfig.RECEIVE_BUFFER_CONFIG),
            requestTimeoutMs,
            producerConfig.getLong(ProducerConfig.SOCKET_CONNECTION_SETUP_TIMEOUT_MS_CONFIG),
            producerConfig.getLong(ProducerConfig.SOCKET_CONNECTION_SETUP_TIMEOUT_MAX_MS_CONFIG),
            time,
            true,
            apiVersions,
            throttleTimeSensor,
            logContext);
    //生成用于发送网络请求的线程.
    short acks = Short.parseShort(producerConfig.getString(ProducerConfig.ACKS_CONFIG));
    return new Sender(logContext,
            client,
            metadata,
            this.accumulator,
            maxInflightRequests == 1,
            producerConfig.getInt(ProducerConfig.MAX_REQUEST_SIZE_CONFIG),//单次请求的最大bytes.
            acks,
            producerConfig.getInt(ProducerConfig.RETRIES_CONFIG), //此配置通常保持默认值.
            metricsRegistry.senderMetrics,
            time,
            requestTimeoutMs,
            producerConfig.getLong(ProducerConfig.RETRY_BACKOFF_MS_CONFIG),
            this.transactionManager,
            apiVersions);
}

c,更新Metadata

Sender线程在不断轮询过程中,在调用执行到NetworkClient.poll函数或sendProducerData时,
会执行metadataUpdater(实现类DefaultMetadataUpdater)中的maybeUpdate函数,
此函数判断当前Producer中cache的metadata是否过期,过期时间由metadata.max.age.ms配置(默认5分钟).
(注意:如果是Producer初始化后的第一次轮询时,也表示超时.)
如果metadataCache过期后会发起MetadataRequest请求,来获取producer需要的metadata信息(topicInfo,brokers).

//这里会向随机的一个由"bootstrap.servers"配置的broker节点(或metadataCache中的节点)发起请求(如果超时).
//==>`metadataupdater`的实现在Producer端默认为`NetworkClient.DefaultMetadataUpdater`.
long metadataTimeout = metadataUpdater.maybeUpdate(now);
..........
void sendInternalMetadataRequest(MetadataRequest.Builder builder, String nodeConnectionId, long now) {
   
    ClientRequest clientRequest = newClientRequest(nodeConnectionId, builder, now, true);
    doSend(clientRequest, true, now);
}
//生成向broker请求的metadataRequest信息
protected MetadataRequest.Builder newMetadataRequestBuilder() {
   
    return MetadataRequest.Builder.allTopics();
}

在Producer端,发起MetadataRequest请求时,会设置topics参数的值为null,表示获取集群中所有的topic信息.

如果ProducerMetadata实例中的newTopics容器不为空时,会只请求此部分的topics的metadata信息.

此请求在Broker端接收到后,会直接由KafkaApis中的handleTopicMetadataRequest进行处理.
而此请求的返回信息包含当前cluster中所有的topics信息与当前处于active状态的所有borker节点.
Producer发起的Metadata请求,在broker端成功响应后,
会交由NetworkClient中DefaultMetadataUpdater实例的handleSuccessfulResponse处理程序处理.
而在handleSuccessfulResponse处理程序中,其核心处理代码如下所示

this.metadata.update(inProgress.requestVersion, response, inProgress.isPartialUpdate, now);

可以看到,当DefaultMetadataUpdater接收到broker的响应后,直接交给了ProducerMetadata实例进行处理.
而在Metadata.update的处理程序中,主要根据请求的响应重新生成MetadataCache实例,如下所示:

*=>1*, 更新metadata的刷新时间,此时间用于判断metadata是否过期.

//更新metadata的刷新时间.
this.lastRefreshMs = nowMs;
this.updateVersion += 1;
//判断是否是部分更新(newTopics容器不为空时,表示部分更新)
if (!isPartialUpdate) {
   
    this.needFullUpdate = false;
    this.lastSuccessfulRefreshMs = nowMs;
}

*=>2*, 根据Metadata请求的响应结果(clusterId,activeBrokers,topics,partitions)生成MetadataCache.

//根据请求的响应,生成MetadataCache.
this.cache = handleMetadataResponse(response, isPartialUpdate, nowMs);
//handleMetadataResponse中生成MetadataCache实例.
Map<Integer, Node> nodes = metadataResponse.brokersById();
//`isPartialUpdate == true`表示是增量更新(即partition的Leader发生切换后的增量metadata更新)
//==>或者producer要写入的record对应的topic在当前cache中不存在(新创建)
if (isPartialUpdate)
    return this.cache.mergeWith(metadataResponse.clusterId(), nodes, partitions,
        unauthorizedTopics, invalidTopics, internalTopics, metadataResponse.controller(), topicIds,
        (topic, isInternal) -> !topics.contains(topic) && retainTopic(topic, isInternal, nowMs));
//全是更新,直接根据response生成metadataCache.
else
    return new MetadataCache(metadataResponse.clusterId(), nodes, partitions,
        unauthorizedTopics, invalidTopics, internalTopics, metadataResponse.controller(), topicIds);

d,InitProducerId(非事务)

client端发起请求

所谓的非事务场景即是Producer端配置有冥等性enable.idempotence == true,但事务idtransactional.id未配置的情况下,
此时Producer端会通过TransactionManager组件来初始化获取当前Producer的ProducerId.
即:当Sender线程启动后,runOne函数轮询过程时,
会在执行如下代码片段时判断ProducerId是否初始化,如果未初始化,发起InitProducerId请求:

if (transactionManager != null) {
   
    try {
   
        .....................
        //非事务场景下,获取冥等性支持的ProducerId的值(如果还未获取).
        //==>或broker响应`UNKNOWN_PRODUCER_ID`或`OUT_OF_ORDER_SEQUENCE_NUMBER`错误代码时.
        //==>此时会把`InitProducerId`请求生成
        transactionManager.bumpIdempotentEpochAndResetIdIfNeeded();
        //处理TransactionManager组件相关的请求,如`InitProducerId`等,向broker端发起请求.
        if (maybeSendAndPollTransactionalRequest()) {
   
            return;
        }
    } catch (AuthenticationException e) {
   
        log.trace("Authentication exception while processing transactional request", e);
        transactionManager.authenticationFailed(e);
    }
}

当ProducerId未初始化时,在执行bumpIdempotentEpochAndResetIdIfNeeded函数时会生成InitProducerId请求.

如下部分是函数实现的部分代码片段:

if (currentState != State.INITIALIZING && !hasProducerId()) {
   
    transitionTo(State.INITIALIZING);
    InitProducerIdRequestData requestData = new InitProducerIdRequestData()
            .setTransactionalId(null)
            .setTransactionTimeoutMs(Integer.MAX_VALUE);
    InitProducerIdHandler handler = new InitProducerIdHandler(
          new InitProducerIdRequest.Builder(requestData), false)
    ;
    enqueueRequest(handler);
}

可以看到,针对InitProducerIdRequest请求的处理程序实现为InitProducerIdHandler.
在非事务的场景下,InitProducerIdHandlercoordinatorTypenull.因此:
maybeSendAndPollTransactionalRequest函数发送请求时,会从metadataCache中随机获取一个broker节点.
并通过此broker节点发起InitProducerId请求,如下代码片段:

//非事务场景下,直接调用NetworkClient中的leastLoadedNode来随机获取一个broker发起请求.
targetNode = coordinatorType != null ?
        transactionManager.coordinator(coordinatorType) :
        client.leastLoadedNode(time.milliseconds());
............
long currentTimeMs = time.milliseconds();
ClientRequest clientRequest = client.newClientRequest(
    targetNode.idString(), requestBuilder, currentTimeMs,
    true, requestTimeoutMs, nextRequestHandler);
log.debug("Sending transactional request {} to node {} with correlation ID {}"..);
client.send(clientRequest, currentTimeMs);
transactionManager.setInFlightCorrelationId(clientRequest.correlationId());

此请求在broker端将由TransactionCoordinator中的handleInitProducerId处理程序进行处理.

broker端处理请求

当broker端接收到InitProducerId请求后,会交由TransactionCoordinator组件来进行处理.

此组件在每个broker进程中都包含一个,在没有事务的场景下,此组件用于管理producer对应的produdcerId信息(随机到任意的broker节点),

而在有事务的场景下,每个broker实例中的transactionCoordinator负责一组分配到此broker的事务id.

TransactionCoordinator.handleInitProducerId(非事务场景)

//非事务场景下,Producer获取ProducerId的处理.
if (transactionalId == null) {
   
  val producerId = producerIdManager.generateProducerId()
  responseCallback(InitProducerIdResult(producerId, producerEpoch = 0, Errors.NONE))
}

从上面的代码片段中可以看到,在TransactionCoordinator组件中,在处理非事务场景下的producerId的申请时,

只是简单的直接通过调用ProducerIdManager中的generateProducerId函数来生成producerId,并响应给Producer端.

而在ProducerIdManager组件中,会首先向activeController发起一个AllocateProducerIds请求.
在controller端会由ProducerIdControlManager组件进行处理.
AllocateProducerIds请求会给broker生成一个连续的producerId的数据块.
ProducerIdManagergenerateProducerId分配producerId达到连续数据块的90%时,会重新向controller请求一个新的数据块.

ProducerIdManager.generateProducerId函数

verride def generateProducerId(): Long = {
   
  this synchronized {
   
    //broker启动后首次生成p
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值