2.Kafka源码深入解析之生产端初始化

上一节我们讲到了在KafkaProducer初始化的时候,初始化了三个组件:

  • 分区器Partitioner
  • 序列化器Serializer
  • 拦截器Interceptor
  1. 接下来我们要讲到第一个非常核心的组件:MetaData,我们想一下,当一条消息要写入broker里,是不是要先知道这条数据要写入哪个分区里,这个分区在哪个broker上,MetaData是用来从broker集群去拉取元数据的Topics(Topic -> Partitions(Leader+Followers,ISR))
this.metadata = new Metadata(retryBackoffMs, config.getLong(ProducerConfig.METADATA_MAX_AGE_CONFIG),
                    true, true, clusterResourceListeners);

/**
* max.request.size:生产者往服务端发送消息的时候,规定一条消息最大是多大,默认是1m
*/
this.maxRequestSize = config.getInt(ProducerConfig.MAX_REQUEST_SIZE_CONFIG);
/**
             * recordAccumlator 缓存区大小:buffer.memory 默认值是32m
             */
this.totalMemorySize = config.getLong(ProducerConfig.BUFFER_MEMORY_CONFIG);
/**
             * 默认不压缩
             * 可以用下面压缩:gzip,lax4,snappy
             * 提交吞吐量,但消耗cpu
             */
this.compressionType = CompressionType.forName(config.getString(ProducerConfig.COMPRESSION_TYPE_CONFIG));

 /**
  * max.block.ms
  * 当buffer满了,或者metadata获取不到,或者化没完成分区函数完成
  * 等情况下的最大阻塞时间:默认是60s
  */
 this.maxBlockTimeMs = config.getLong(ProducerConfig.MAX_BLOCK_MS_CONFIG);
 this.requestTimeoutMs = config.getInt(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG);
 /**
  * 当设置了transactionId,必须是idempotenceEnabled是开启的
  */
 this.transactionManager = configureTransactionState(config, logContext, log);
 /**
  * public static final String RETRIES_CONFIG = "retries";
  * 这个是发送失败后重启几次,默认是不重启
  * 如果配置了,就按配置的值重启
  * 如果没有配置,但开启了幂等,那么重启次数是Integer.Max
  */
 int retries = configureRetries(config, transactionManager != null, log);

 /**
  * max.in.flight.requests.per.connection
  * 这个参数的意思是说生产者会把同一分区的每条消息打包成一个batch
  * 每个request请求是把多个分区对应的多个batch打包成一个request发送给broker
  * 这个时候,最多有几个request都没收到响应,
  * 每个连接broker都有一个connection,每个connection最多有几个request没收到request
  * 这个参数默认值:5,就是说最多有5个request没到响应的请教放在requests集合里
  */
 int maxInflightRequests = configureInflightRequests(config, transactionManager != null);
 /**
  *  0:不会重试,直接发布就不管了
  *  1:leader发送成功就表示发送成功
  *  -1/all: 所有的isr的副本发送成功,才表示发送成功
  */
 short acks = configureAcks(config, transactionManager != null, log);

 this.apiVersions = new ApiVersions();

这上面配置了几个非常重要的参数值,以及参数说明,用在哪些地方,默认值大家要记住

  • max.request.size
  • buffer.memory
  • max.block.ms
  • retries
  • request.timeout.ms
  • max.in.flight.requests.per.connection
  • acks
  • linger.ms

接下来我们看这个Metadata组件的作用:其实就是拉元数据过来,保存在本地缓存起来

/**
             * 核心行为:初始化的时候,直接调用Metadata组件的方法,去broker上
             * 拉取一次集群的元数据过来,后面每隔5分钟刷新一次元数据,但是发送消息的时候
             * ,如果没有找到某个topic的元数据,也一定会拉取一次的
             *
             * 实际上我们看了一下update方法,在kafakPrducer初始化的时候并没有真正的
             * 去拉取topic的元数据,但是他肯定是对集群元数据做了一个初始化的,
             * 把你配置的那些broker地址转化为了Node,放在Cluster对象实例化
             *
             * 在发送消息的时候,如果发现你要写入的某个Topic对应的元数据不在本地,那么他是不是肯定会通过这个组件,
             * 发送请求到broker尝试拉取这个topic对应的元数据,
             * 如果你在集群里增加了一台broker,也会涉及到元数据的变化

             * 这里的意思其实仅仅把我们配置的那个broker的地址放了进去,在客户端缓存
             * 集群元数据的时候,采用了哪些数据结构:
             * 我们看代码实际化了一个Cluster对象,里面有List nodes;
             * 每个node包括了node_id实际上是我们kafka配置文件里的brokerid,
             *    private final int id;
             *     private final String idString;
             *     private final String host;
             *     private final int port;
             *     private final String rack;
             *
             *
             */
List addresses = ClientUtils.parseAndValidateAddresses(config.getList(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG));
this.metadata.update(Cluster.bootstrap(addresses), Collections.emptySet(), time.milliseconds());
            ChannelBuilder channelBuilder = ClientUtils.createChannelBuilder(config);

2. 接下来我们来看另一个核心组件:RecordAccumulator,从名字也可以看出来,就是一个消费缓存器,我们知道Kafka其实是批量发送消息到Broker的,为啥要批量发,而不是一条一条的发呢?很明显,减少网络请求,提高吞吐呀

/**
             * 核心组件:RecordAccumulator,缓冲区,负责消息的复杂的缓冲机制,
             * 发送到每个分区的消息会被打包成batch,
             * 一个broker上的多个分区对应的多个batch会被打包成一个request,batch size(16kb)
             * 一个request请求其实是多个batch打包后发送出去给broker的,
             * 每个request默认最大长度 :max.request.size = 1M
             *
             * linger.ms,默认没有时间
             * 默认情况下,如果光光是考虑batch的机制的话,那么必须要等到足够多的消息打包成一个batch,
             * 才能通过request发送到broker上去;
             * 但是有一个问题,如果你发送了一条消息,但是等了很久都没有达到一个batch大小
             * 所以说要设置一个linger.ms,如果在指定时间范围内,
             * 都没凑出来一个batch把这条消息发送出去,
             * 那么到了这个linger.ms指定的时间,比如说5ms,
             * 如果5ms还没凑出来一个batch,那么就必须立即把这个消息发送出去
             */
            /**
             *"batch.size";
             * 这个batch的意思是发送给broker中topic中的同一个分区的数据
             * 会打包成一个batch,batch默认大小:16k
             *
             * retries:配置生产者重试的次数,默认值为0,即在发生异常的时候不进行任何重试动作。
             * retry.backoff.ms:设定两次重试之间的时间间隔,避免无效的频繁重试,默认值为100
             *
             * linger.ms:batch不管大小达没达到16k,在这个时间内都要发出去,默认是0,表示不延迟
             * 这样实际工作中,要设置的,这样就是批量发送
             */
            this.accumulator = new RecordAccumulator(logContext,
                    config.getInt(ProducerConfig.BATCH_SIZE_CONFIG),
                    this.totalMemorySize,
                    this.compressionType,
                    config.getLong(ProducerConfig.LINGER_MS_CONFIG),
                    retryBackoffMs,
                    metrics,
                    time,
                    apiVersions,
                    transactionManager);

3. 最后一个核心组件:网络通信的组件,NetworkClient,这个一看就是网络连接用的呀,哈哈,必须的,是生产端与broker通信用的组件,其实关于Kafka这块自己封装的通信非常有必要学一下,这是世界级工业代码,比任何人都写的好吧,完全可以拿出来放在我们的系统里嘛,这也是我们学习的一种方法

/**
             * 核心组件:网络通信的组件,NetworkClient,
             * connections.max.idle.ms,一个网络连接最多空闲多长时间(9分钟),
             *  max.in.flight.requests.per.connection 每个连接最多有几个request没收到响应(5个),
             * reconnect.backoff.ms: 重试连接的时间间隔(50ms),
             * send.buffer.bytes:  Socket发送缓冲区大小(128kb),
             * receive.buffer.bytes:   Socket接收缓冲区大小(32kb)
             */
            NetworkClient client = new NetworkClient(
                    new Selector(config.getLong(ProducerConfig.CONNECTIONS_MAX_IDLE_MS_CONFIG),
                            this.metrics, time, "producer", channelBuilder, logContext),
                    this.metadata,
                    clientId,
                    maxInflightRequests,
                    config.getLong(ProducerConfig.RECONNECT_BACKOFF_MS_CONFIG),
                    config.getLong(ProducerConfig.RECONNECT_BACKOFF_MAX_MS_CONFIG),
                    config.getInt(ProducerConfig.SEND_BUFFER_CONFIG),
                    config.getInt(ProducerConfig.RECEIVE_BUFFER_CONFIG),
                    this.requestTimeoutMs,
                    time,
                    true,
                    apiVersions,
                    throttleTimeSensor,
                    logContext);

4.我们知道Kafka发消息默认是异步的,就是说主线程在生产消息,放在我们上面说的缓冲器里,另一个线程去拉消息到Broker,那么下面不是初始化这个sender线程,接着,我们可以看到newKafkaThread一个线程,名称叫kafka-producer-network-thread,同时把上面我们新建的sender线程放进去,这里我们可以看出一种方法,业务逻辑写在我们的线程类里,用另一个线程包装起来,这是线程类与业务逻辑分开的编程技巧,值得大家学习,工业级代码就是不一样,最后可以看到,线程启动,Kafka producer started!

/**
             * 核心组件:Sender线程,负责从缓冲区里获取消息发送到broker上去,
             * request最大大小(1mb),
             * acks(1,只要leader写入成功就认为成功),默认是1
             * 重试次数(0,无重试),这个在生产上是一定要设置的,可以重启次数为3
             * ,请求超时的时间(30s),
             */
            this.sender = new Sender(logContext,
                    client,
                    this.metadata,
                    this.accumulator,
                    maxInflightRequests == 1,
                    config.getInt(ProducerConfig.MAX_REQUEST_SIZE_CONFIG),
                    acks,
                    retries,
                    metricsRegistry.senderMetrics,
                    Time.SYSTEM,
                    this.requestTimeoutMs,
                    config.getLong(ProducerConfig.RETRY_BACKOFF_MS_CONFIG),
                    this.transactionManager,
                    apiVersions);

String ioThreadName = NETWORK_THREAD_PREFIX + " | " + clientId;
            /**
             * 实际上这里是新建一个thread,把sender放里面,
             * start启动线程
             *
             * 这里sender是一个runable线程
             * 线程类叫做“KafkaThread”,
             * 线程名字叫做“kafka-producer-network-thread”,此处线程直接被启动
             * 这里定义了一个kafkaThread,直接启动!
             */
            this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
            this.ioThread.start();
            this.errors = this.metrics.sensor("errors");
            config.logUnused();
            AppInfoParser.registerAppInfo(JMX_PREFIX, clientId, metrics);
            log.debug("Kafka producer started");
        } catch (Throwable t) {
            // call close methods if internal objects are already constructed this is to prevent resource leak. see KAFKA-2121
            close(0, TimeUnit.MILLISECONDS, true);
            // now propagate the exception
            throw new KafkaException("Failed to construct kafka producer", t);
        }

到这里,我们讨论的KafkaProducer初始化工作,全部完成了,用了两小节详细解析了他在初始化时做的工作,是不是感觉背后做了好多事,这是我们写一行API无法知道的,本次解析完全是按源码每行代码非常详细的分析出来的结果,希望能给大家带来不一样的收获

下一篇,我们就进入生产者发送消息的解析章节

你可能感兴趣的:(2.Kafka源码深入解析之生产端初始化)