了解KafkaProducer的初始化过程,重点掌握初始化过程中涉及到的核心参数。
- 初始化代码 -
这篇文章主要分析KafkaProducer初始化的时候都初始化了啥?我们还是以场景驱动的方式。上一次我们的代码运行到:
//TODO 核心代码 producer = new KafkaProducer<>(props);
点击过去,看到如下代码:
public KafkaProducer(Properties properties) { this(new ProducerConfig(properties), null, null); }
直接再点击过去
看到的这个方法就是KafkaProducer初始化的方法,也是我们今天主要分析的方法了。
private KafkaProducer(ProducerConfig config, Serializer keySerializer, Serializer valueSerializer) { try { log.trace("Starting the Kafka producer"); //获取用户自定义的配置 Map userProvidedConfigs = config.originals(); this.producerConfig = config; this.time = new SystemTime(); //获取client id,如果没有就自动生成一个 clientId = config.getString(ProducerConfig.CLIENT_ID_CONFIG); if (clientId.length() <= 0) clientId = "producer-" + PRODUCER_CLIENT_ID_SEQUENCE.getAndIncrement(); //跟监控信息有关,这个跟我们分析源码的主流程关系不大 //所以关于metric的东西可以不用关心 Map metricTags = new LinkedHashMap(); metricTags.put("client-id", clientId); MetricConfig metricConfig = new MetricConfig().samples(config.getInt(ProducerConfig.METRICS_NUM_SAMPLES_CONFIG)) .timeWindow(config.getLong(ProducerConfig.METRICS_SAMPLE_WINDOW_MS_CONFIG), TimeUnit.MILLISECONDS) .tags(metricTags); List reporters = config.getConfiguredInstances(ProducerConfig.METRIC_REPORTER_CLASSES_CONFIG, MetricsReporter.class); reporters.add(new JmxReporter(JMX_PREFIX)); this.metrics = new Metrics(metricConfig, reporters, time); //使用反射获取一个分区器 this.partitioner = config.getConfiguredInstance(ProducerConfig.PARTITIONER_CLASS_CONFIG, Partitioner.class); //重要参数,这个参数的默认值是100ms //代表的意思是Producer支持重试,两次重试之间的时间间隔 long retryBackoffMs = config.getLong(ProducerConfig.RETRY_BACKOFF_MS_CONFIG); //设置key 和 value的序列化器 if (keySerializer == null) { this.keySerializer = config.getConfiguredInstance(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, Serializer.class); this.keySerializer.configure(config.originals(), true); } else { config.ignore(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG); this.keySerializer = keySerializer; } if (valueSerializer == null) { this.valueSerializer = config.getConfiguredInstance(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, Serializer.class); this.valueSerializer.configure(config.originals(), false); } else { config.ignore(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG); this.valueSerializer = valueSerializer; } // load interceptors and make sure they get clientId userProvidedConfigs.put(ProducerConfig.CLIENT_ID_CONFIG, clientId); List> interceptorList = (List) (new ProducerConfig(userProvidedConfigs)).getConfiguredInstances(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG, ProducerInterceptor.class); //设置拦截器,不过拦截器不是Kafka的重要的功能 this.interceptors = interceptorList.isEmpty() ? null : new ProducerInterceptors<>(interceptorList); ClusterResourceListeners clusterResourceListeners = configureClusterResourceListeners(keySerializer, valueSerializer, interceptorList, reporters); //重要参数,指的是Producer多久去更新一次Kafka的元数据 //默认是5分钟 this.metadata = new Metadata(retryBackoffMs, config.getLong(ProducerConfig.METADATA_MAX_AGE_CONFIG), true, clusterResourceListeners); //重要参数,这个参数开发的时候一般我们要进行设置,它指的是一个请求最大多大 //我们也可以理解为代表的意思是一条消息最大允许多大,这个的默认值是1M //1M这个值有些偏小。可以根据大家的公司的情况来,笔者在的公司 //这个值设置为10M this.maxRequestSize = config.getInt(ProducerConfig.MAX_REQUEST_SIZE_CONFIG); //重要参数 //消息在发送在之前都需要缓存起来,这地方指的就是缓存的内存大小 //默认是32M,一般情况32M可以满足需求,但是也可以根据公司的情况 //进行调优,不过现在先不着急,因为我估计大家对这个参数理解还是很蒙圈 //我们后面的文章会详细讲到关于这个参数的使用。 this.totalMemorySize = config.getLong(ProducerConfig.BUFFER_MEMORY_CONFIG); //压缩类型,kafka支持多种压缩类型 this.compressionType = CompressionType.forName(config.getString(ProducerConfig.COMPRESSION_TYPE_CONFIG)); /* check for user defined settings. * If the BLOCK_ON_BUFFER_FULL is set to true,we do not honor METADATA_FETCH_TIMEOUT_CONFIG. * This should be removed with release 0.9 when the deprecated configs are removed. */ if (userProvidedConfigs.containsKey(ProducerConfig.BLOCK_ON_BUFFER_FULL_CONFIG)) { log.warn(ProducerConfig.BLOCK_ON_BUFFER_FULL_CONFIG + " config is deprecated and will be removed soon. " + "Please use " + ProducerConfig.MAX_BLOCK_MS_CONFIG); boolean blockOnBufferFull = config.getBoolean(ProducerConfig.BLOCK_ON_BUFFER_FULL_CONFIG); if (blockOnBufferFull) { this.maxBlockTimeMs = Long.MAX_VALUE; } else if (userProvidedConfigs.containsKey(ProducerConfig.METADATA_FETCH_TIMEOUT_CONFIG)) { log.warn(ProducerConfig.METADATA_FETCH_TIMEOUT_CONFIG + " config is deprecated and will be removed soon. " + "Please use " + ProducerConfig.MAX_BLOCK_MS_CONFIG); this.maxBlockTimeMs = config.getLong(ProducerConfig.METADATA_FETCH_TIMEOUT_CONFIG); } else { this.maxBlockTimeMs = config.getLong(ProducerConfig.MAX_BLOCK_MS_CONFIG); } } else if (userProvidedConfigs.containsKey(ProducerConfig.METADATA_FETCH_TIMEOUT_CONFIG)) { log.warn(ProducerConfig.METADATA_FETCH_TIMEOUT_CONFIG + " config is deprecated and will be removed soon. " + "Please use " + ProducerConfig.MAX_BLOCK_MS_CONFIG); this.maxBlockTimeMs = config.getLong(ProducerConfig.METADATA_FETCH_TIMEOUT_CONFIG); } else { this.maxBlockTimeMs = config.getLong(ProducerConfig.MAX_BLOCK_MS_CONFIG); } /* check for user defined settings. * If the TIME_OUT config is set use that for request timeout. * This should be removed with release 0.9 */ if (userProvidedConfigs.containsKey(ProducerConfig.TIMEOUT_CONFIG)) { log.warn(ProducerConfig.TIMEOUT_CONFIG + " config is deprecated and will be removed soon. Please use " + ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG); this.requestTimeoutMs = config.getInt(ProducerConfig.TIMEOUT_CONFIG); } else { this.requestTimeoutMs = config.getInt(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG); } //创建RecordAccumulator 对象 //对象里面传进去了我们刚刚分析的重要的参数 this.accumulator = new RecordAccumulator(config.getInt(ProducerConfig.BATCH_SIZE_CONFIG), this.totalMemorySize, this.compressionType, config.getLong(ProducerConfig.LINGER_MS_CONFIG), retryBackoffMs, metrics, time); List addresses = ClientUtils.parseAndValidateAddresses(config.getList(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG)); //这段代码看起来想是去获取集群的数据 //但是其实代码里没有去获取到元数据,我们后面的文章去分析。 this.metadata.update(Cluster.bootstrap(addresses), time.milliseconds()); ChannelBuilder channelBuilder = ClientUtils.createChannelBuilder(config.values()); //重要,构建了一个NetworkdClient对象,这个是Kafka的网络核心,里面传了很多重要的参数 //MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION 每个连接允许最多有多个请求没收到响应,默认是5个。 //如下的几个参数是跟网络相关的参数,大家可以积累,以后在自己的项目中也可以这么设置 //CONNECTIONS_MAX_IDLE_MS_CONFIG 一个网络连接,最多空闲多久,就要把回收掉 默认是9分钟。这个也可以根据情况去设置 //RECONNECT_BACKOFF_MS_CONFIG 重试建立连接的时间间隔 //SEND_BUFFER_CONFIG soket发送缓冲区的大小,默认是128K // RECEIVE_BUFFER_CONFIG socket接收缓冲区的大小,默认是32K //上面的有个参数需要引起大家的注意哈: //MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION 对应的配置参数是max.in.flight.requests.per.connection //这个参数指的是限制Producer客户端在单个连接上能够发送的未响应请求的个数,默认是5个 //大家要注意我们的Kafka是有重试机制,那么里面这儿可以允许放5个发送了但是未响应的请求 //假如里面的排在前面的消息返回响应说是失败了,然后要重新发送,那么这样的话,消息的顺序就打乱了 //所以如果这个值 大于 1 就会有 消息顺序被打乱的风险。 //特别关心消息顺序的同学要注意,需要把这个参数设置为1 NetworkClient client = new NetworkClient( new Selector(config.getLong(ProducerConfig.CONNECTIONS_MAX_IDLE_MS_CONFIG), this.metrics, time, "producer", channelBuilder), this.metadata, clientId, config.getInt(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION), config.getLong(ProducerConfig.RECONNECT_BACKOFF_MS_CONFIG), config.getInt(ProducerConfig.SEND_BUFFER_CONFIG), config.getInt(ProducerConfig.RECEIVE_BUFFER_CONFIG), this.requestTimeoutMs, time); //重要,我们要注意,这个里面把上面创建的client对象传进去了。 //从缓冲区里获取数据,然后发送出去 //RETRIES_CONFIG对应的参数是retries ,代表的是重试的次数,一般开发的时候我们建议要设置这个值,默认是0,也就是不重试 //ACKS_CONFIG acks,这个的值由 0 1 all(-1) 三个数 //0 代表 生成者把消息发送给Broker以后,就立马返回。至于有没有写成功也不关心 //1 代表 生成者把消息发送给Broker以后,需要等leader partition写入成功以后 返回响应。 //1- 代表需要所有的partition都写入成功以后才返回响应。 //注:0 1 两个都会有可能造成数据丢失。0 的结果很好想,我只是发送到服务端,然后立马返回 //服务端那儿有可能就会写失败。写失败了数据就丢了。1 发送到服务端以后还要等leader partition写入 //成功以后才会返回响应,但是大家想如果这个时候leader partition在的broker要是宕机了 // 这条数据也就会丢失,其实这个情况是很有可能发生的,因为我们维护集群的时候,有些参数就是需要 //重启集群才能生效,重启服务的过程中,肯定就会导致部分leader partition 宕机。 //所以如果不允许丢数据的同学,可以重点关注这个参数。 this.sender = new Sender(client, this.metadata, this.accumulator, config.getInt(ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION) == 1, config.getInt(ProducerConfig.MAX_REQUEST_SIZE_CONFIG), (short) parseAcks(config.getString(ProducerConfig.ACKS_CONFIG)), config.getInt(ProducerConfig.RETRIES_CONFIG), this.metrics, new SystemTime(), clientId, this.requestTimeoutMs); String ioThreadName = "kafka-producer-network-thread" + (clientId.length() > 0 ? " | " + clientId : ""); //里面放进去了sender对象 //里面的代码设计也挺有意思,文章结尾的时候我们分析一下 this.ioThread = new KafkaThread(ioThreadName, this.sender, true); //启动了这个线程,启动起来以后会有很多很多的事要干,这个我们就只能 //后面的文章再去分析了。 this.ioThread.start(); this.errors = this.metrics.sensor("errors"); config.logUnused(); AppInfoParser.registerAppInfo(JMX_PREFIX, clientId); 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); } }
初始化的时候涉及到很多工作要做,里面的其余代码我们后面的文章再去分析。我们接下来简单看一下里面有一个有意思的代码。
this.ioThread = new KafkaThread(ioThreadName, this.sender, true); //启动了这个线程 this.ioThread.start();
代码点过去发现代码如下:
public class KafkaThread extends Thread { private final Logger log = LoggerFactory.getLogger(getClass()); public KafkaThread(final String name, Runnable runnable, boolean daemon) { super(runnable, name); setDaemon(daemon); setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { public void uncaughtException(Thread t, Throwable e) { log.error("Uncaught exception in " + name + ": ", e); } }); }}
KafkaThread 里面是对线程做了简单的封装,个人感觉代码这样设计会比较清晰,把线程和业务代码分离,KafkaThread对线程进行封装,业务逻辑在Sender里面。
- 总结 -
今天我们主要学习的是KafkaProducer初始化过程,初始化过程中涉及到了很多参数,而且了解这些参数对于我们理解kafka和项目开发都比较重要,大家要重点掌握。 下一篇文章我们分析,KafkaProducer初始化代码里面的这段代码: this.metadata.update(Cluster.bootstrap(addresses), time.milliseconds());
看起来像是去拉取元数据,但是到底有没有拉取元数据我们下次进行分析。
大家加油!
- 关注“大数据观止” -