欢迎大家关注 github.com/hsfxuebao/j… ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈
书接上文 Kafka源码分析1-环境准备 ,本篇重点分析Producer初始化过程。
1.Producer使用
下面是producer的简单使用:
public class ProducerTest {
private static String topicName;
private static int msgNum;
private static int key;
public static void main(String[] args) {
Properties props = new Properties();
props.put("bootstrap.servers", "127.100.0.1:9092,127.100.0.2:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
topicName = "test";
msgNum = 10; // 发送的消息数
Producer<String, String> producer = new KafkaProducer<>(props);
for (int i = 0; i < msgNum; i++) {
String msg = i + " This is matt's blog.";
producer.send(new ProducerRecord<String, String>(topicName, msg));
}
producer.close();
}
}
从上面的代码可以看出 Kafka 为用户提供了非常简单的 API,在使用时,只需要如下两步:
- 初始化
KafkaProducer
实例; - 调用
send
接口发送数据。
2.Producer属性
我们先来看一下,KafkaProducer
的属性都有哪些
public class KafkaProducer<K, V> implements Producer<K, V> {
private static final Logger log = LoggerFactory.getLogger(KafkaProducer.class);
// 用于生产者客户端名称的生成,自增序列器
private static final AtomicInteger PRODUCER_CLIENT_ID_SEQUENCE = new AtomicInteger(1);
// JMX中显示的前缀
private static final String JMX_PREFIX = "kafka.producer";
// 生产者客户端的名称
private String clientId;
// 分区器
private final Partitioner partitioner;
// 消息的最大长度,包含了消息头、序列化后的key和序列化后的value的长度
private final int maxRequestSize;
// 发送单个消息的缓冲区大小
private final long totalMemorySize;
// 集群元数据信息
private final Metadata metadata;
// 用于存放消息的缓冲
private final RecordAccumulator accumulator;
// 发送消息的Sender任务
private final Sender sender;
// 性能监控相关
private final Metrics metrics;
// 发送消息的线程,sender对象会在该线程中运行
private final Thread ioThread;
// 压缩算法
private final CompressionType compressionType;
// 错误记录器
private final Sensor errors;
// 用于时间相关操作
private final Time time;
// 键和值序列化器
private final Serializer<K> keySerializer;
private final Serializer<V> valueSerializer;
// 生产者配置集
private final ProducerConfig producerConfig;
// 等待更新Kafka集群元数据的最大时长
private final long maxBlockTimeMs;
// 消息的超时时间,也就是从消息发送到收到ACK响应的最长时长
private final int requestTimeoutMs;
// 拦截器集合
private final ProducerInterceptors<K, V> interceptors;
}
相关注释已经标注,这里不再赘述。
3.Producer初始化
KafkaProducer
构造器
public KafkaProducer(Properties properties) {
this(new ProducerConfig(properties), null, null);
}
private KafkaProducer(ProducerConfig config, Serializer<K> keySerializer, Serializer<V> valueSerializer) {
try {
log.trace("Starting the Kafka producer");
// 从原始配置拷贝一份副本
Map<String, Object> userProvidedConfigs = config.originals();
this.producerConfig = config;
this.time = Time.SYSTEM;
// 获取clientId
clientId = config.getString(ProducerConfig.CLIENT_ID_CONFIG);
if (clientId.length() <= 0)
// 如果clientId没指定,则使用"producer-序列化"的形式表示
clientId = "producer-" + PRODUCER_CLIENT_ID_SEQUENCE.getAndIncrement();
Map<String, String> metricTags = new LinkedHashMap<String, String>();
metricTags.put("client-id", clientId);
// metric一些东西,监控
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<MetricsReporter> 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
long retryBackoffMs = config.getLong(ProducerConfig.RETRY_BACKOFF_MS_CONFIG);
// 设置序列化器
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<ProducerInterceptor<K, V>> interceptorList = (List) (new ProducerConfig(userProvidedConfigs, false)).getConfiguredInstances(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,
ProducerInterceptor.class);
this.interceptors = interceptorList.isEmpty() ? null : new ProducerInterceptors<>(interceptorList);
ClusterResourceListeners clusterResourceListeners = configureClusterResourceListeners(keySerializer, valueSerializer, interceptorList, reporters);
/** 创建Metadata集群元数据对象,生产者从服务端拉取kafka元数据信息
* 需要发送网络请求,重试
* metadata.max.age.ms 生产者每隔一段时间更新自己的元数据,默认5分钟
*/
this.metadata = new Metadata(retryBackoffMs, config.getLong(ProducerConfig.METADATA_MAX_AGE_CONFIG), true, clusterResourceListeners);
/**
* MAX_REQUEST_SIZE_CONFIG 生产者往服务端发送一条消息最大的size
* 默认1m 如果超过这个大小,消息就发送不出去
*/
this.maxRequestSize = config.getInt(ProducerConfig.MAX_REQUEST_SIZE_CONFIG);
/**
* buffer.memory 缓存大小,默认32M
*/
this.totalMemorySize = config.getLong(ProducerConfig.BUFFER_MEMORY_CONFIG);
/**
* kafka可以压缩数据,设置压缩格式
* 提高系统的吞吐率
* 一次发送出去的消息越多,生产者需要消耗更多的cpu
*/
this.compressionType = CompressionType.forName(config.getString(ProducerConfig.COMPRESSION_TYPE_CONFIG));
....(省略部分代码)
/**
* 创建RecordAccumulator,它是一个发送消息数据的记录缓冲器,用于批量发送消息数据
* batch.size单位是字节,默认16k 用于指定达到多少字节批量发送一次
*/
this.accumulator = new RecordAccumulator(config.getInt(ProducerConfig.BATCH_SIZE_CONFIG),
this.totalMemorySize,
this.compressionType,
config.getLong(ProducerConfig.LINGER_MS_CONFIG),
retryBackoffMs,
metrics,
time);
List<InetSocketAddress> addresses = ClientUtils.parseAndValidateAddresses(config.getList(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG));
/**
* 会将创建KafkaProducer时配置的broker列表传入
* 该update()方法同时会更新Metadata对象的一些属性,
* 并通知所有MetadataUpdate Listener监听器,自己要开始更新数据了
* 同时唤醒等待Metadata更新完成的线程
*/
//TODO product初始化时, update方法初始化的时候并没有去服务端拉取元数据。
this.metadata.update(Cluster.bootstrap(addresses), time.milliseconds());
// 根据配置的协议,创建不同的ChannelBuilder
ChannelBuilder channelBuilder = ClientUtils.createChannelBuilder(config.values());
//TODO 初始化了一个重要的管理网路的组件。
/**
*connections.max.idle.ms: 默认值是9分钟,一个网络连接最多空闲多久,超过这个空闲时间,就关闭这个网络连接。
*
* max.in.flight.requests.per.connection:默认是5,producer -> broker 。
* 发送数据的时候,其实是有多个网络连接。每个网络连接可以忍受 producer端发送给broker消息 然后消息没有响应的个数。
* 因为kafka有重试机制,所以有可能会造成数据乱序,如果想要保证有序,这个值要把设置为1.
*
* send.buffer.bytes:socket发送数据的缓冲区的大小,默认值是128K
* receive.buffer.bytes:socket接受数据的缓冲区的大小,默认值是32K。
*/
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,
true);
// 创建sender线程
//retries:重试的次数
/***
* acks:
* 0:
* producer发送数据到broker后,就完了,没有返回值,不管写成功还是写失败都不管了。
* 1:
* producer发送数据到broker后,数据成功写入leader partition以后返回响应。
* 数据 -> broker(leader partition)
* -1:
* producer发送数据到broker后,数据要写入到leader partition里面,并且数据同步到所有的
* follower 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,
Time.SYSTEM,
this.requestTimeoutMs);
String ioThreadName = "kafka-producer-network-thread" + (clientId.length() > 0 ? " | " + clientId : "");
/**
* 创建了一个线程,然后里面传进去了一个sender对象。
* 把业务的代码和关于线程的代码给隔离开来。
* 关于线程的这种代码设计的方式,其实也值得大家积累的。
*/
this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
// 启动sender线程
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);
}
}
我们先来看一看KafkaProducer
初始化的时候会涉及到哪些内部的核心组件,默认情况下,一个jvm内部,如果你要是搞多个KafkaProducer
的话,每个都默认会生成一个client.id,producer-自增长的数字,producer-1。
(1)核心组件:Partitioner
后面用来决定,你发送的每条消息是路由到Topic的哪个分区里去的
(2)核心组件:Metadata
,这个是对于生产端来说非常核心的一个组件,他是用来从broker集群去拉取元数据的Topics(Topic -> Partitions(Leader+Followers,ISR)),后面如果写消息到Topic,才知道这个Topic有哪些Partitions,Partition Leader所在的Broker
后面肯定会每隔一小段时间就再次发送请求刷新元数据,metadata.max.age.ms,默认是5分钟,默认每隔5分钟一定会强制刷新一下
还有就是我们猜测,在发送消息的时候,如果发现你要写入的某个Topic对应的元数据不在本地,那么他是不是肯定会通过这个组件,发送请求到broker尝试拉取这个topic对应的元数据,如果你在集群里增加了一台broker,也会涉及到元数据的变化
(3)核心参数:每个请求的最大大小(1mb),缓冲区的内存大小(32mb),重试时间间隔(100ms),缓冲区填满之后的阻塞时间(60s),请求超时时间(30s)
(4)核心组件:RecordAccumulator
,缓冲区,负责消息的复杂的缓冲机制,发送到每个分区的消息会被打包成batch,一个broker上的多个分区对应的多个batch会被打包成一个request,batch size(16kb)
默认情况下,如果光光是考虑batch的机制的话,那么必须要等到足够多的消息打包成一个batch,才能通过request发送到broker上去;但是有一个问题,如果你发送了一条消息,但是等了很久都没有达到一个batch大小
所以说要设置一个linger.ms,如果在指定时间范围内,都没凑出来一个batch把这条消息发送出去,那么到了这个linger.ms指定的时间,比如说5ms,如果5ms还没凑出来一个batch,那么就必须立即把这个消息发送出去
(5)核心行为:初始化的时候,直接调用Metadata组件的方法,去broker上拉取了一次集群的元数据过来,后面每隔5分钟会默认刷新一次集群元数据,但是在发送消息的时候,如果没找到某个Topic的元数据,一定也会主动去拉取一次的
(6)核心组件:网络通信的组件,NetworkClient,一个网络连接最多空闲多长时间(9分钟),每个连接最多有几个request没收到响应(5个),重试连接的时间间隔(50ms),Socket发送缓冲区大小(128kb),Socket接收缓冲区大小(32kb)
(7)核心组件:Sender线程,负责从缓冲区里获取消息发送到broker上去,request最大大小(1mb),acks(1,只要leader写入成功就认为成功),重试次数(0,无重试),请求超时的时间(30s),线程类叫做“KafkaThread”,线程名字叫做“kafka-producer-network-thread”,此处线程直接被启动
(8)核心组件:序列化组件,拦截器组件
Producer初始化会不会真实的去拉取集群的元数据呢?
-
wait(),释放锁,然后进入一个休眠等待再次被人唤醒获取锁的状态
-
此时如果有人获取锁之后,调用notifyAll(),就会把之前调用wait()方法进入休眠的线程给唤醒,让他们再次尝试获取锁
-
在KafkaProducer初始化的时候,并没有真正的去某一个broker上去拉取元数据的,但是他肯定是对集群元数据做了一个初始化的,把你配置的那些broker地址转化为了Node,放在Cluster对象实例里
sender线程初始化
/**
* 创建了一个线程,然后里面传进去了一个sender对象。
* 把业务的代码和关于线程的代码给隔离开来。
* 关于线程的这种代码设计的方式,其实也值得大家积累的。
*/
this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
public KafkaThread(final String name, Runnable runnable, boolean daemon) {
super(runnable, name);
configureThread(name, daemon);
}
private void configureThread(final String name, boolean daemon) {
setDaemon(daemon);
setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
public void uncaughtException(Thread t, Throwable e) {
log.error("Uncaught exception in " + name + ": ", e);
}
});
}
- 在设计一些后台线程的时候,可以参照这种模式,把线程以及线程执行的逻辑给切分开来,Sender就是Runnable线程执行的逻辑,KafkaThread其实代表了这个线程本身,线程的名字,未捕获异常的处理,daemon线程的设置
- 后台线程和网络通信的组件要切分开来,线程负责业务逻辑,网络通信组件就专门进行网络请求和响应,封装NIO之类的东西
下面,我们重点看一下在 props 属性是如何转化成类对象的,也就是 ProducerConfig对象,
ProducerConfig
对象 使用父类AbstractConfig构造器:
public AbstractConfig(ConfigDef definition, Map<?, ?> originals, boolean doLog) {
/* check that all the keys are really strings */
for (Object key : originals.keySet())
if (!(key instanceof String))
throw new ConfigException(key.toString(), originals.get(key), "Key must be a string.");
this.originals = (Map<String, ?>) originals;
// 解析方法 重点
this.values = definition.parse(this.originals);
this.used = Collections.synchronizedSet(new HashSet<String>());
if (doLog)
logAll();
}
该方法的调用链如下:
parse(Map<?, ?> props) ->
parseType(String name, Object value, Type type)
public static Object parseType(String name, Object value, Type type) {
try {
if (value == null) return null;
String trimmed = null;
if (value instanceof String)
trimmed = ((String) value).trim();
switch (type) {
case BOOLEAN:
if (value instanceof String) {
if (trimmed.equalsIgnoreCase("true"))
return true;
else if (trimmed.equalsIgnoreCase("false"))
return false;
else
throw new ConfigException(name, value, "Expected value to be either true or false");
} else if (value instanceof Boolean)
return value;
else
throw new ConfigException(name, value, "Expected value to be either true or false");
case PASSWORD:
if (value instanceof Password)
return value;
else if (value instanceof String)
return new Password(trimmed);
else
throw new ConfigException(name, value, "Expected value to be a string, but it was a " + value.getClass().getName());
case STRING:
if (value instanceof String)
return trimmed;
else
throw new ConfigException(name, value, "Expected value to be a string, but it was a " + value.getClass().getName());
case INT:
if (value instanceof Integer) {
return (Integer) value;
} else if (value instanceof String) {
return Integer.parseInt(trimmed);
} else {
throw new ConfigException(name, value, "Expected value to be a 32-bit integer, but it was a " + value.getClass().getName());
}
case SHORT:
if (value instanceof Short) {
return (Short) value;
} else if (value instanceof String) {
return Short.parseShort(trimmed);
} else {
throw new ConfigException(name, value, "Expected value to be a 16-bit integer (short), but it was a " + value.getClass().getName());
}
case LONG:
if (value instanceof Integer)
return ((Integer) value).longValue();
if (value instanceof Long)
return (Long) value;
else if (value instanceof String)
return Long.parseLong(trimmed);
else
throw new ConfigException(name, value, "Expected value to be a 64-bit integer (long), but it was a " + value.getClass().getName());
case DOUBLE:
if (value instanceof Number)
return ((Number) value).doubleValue();
else if (value instanceof String)
return Double.parseDouble(trimmed);
else
throw new ConfigException(name, value, "Expected value to be a double, but it was a " + value.getClass().getName());
case LIST:
if (value instanceof List)
return (List<?>) value;
else if (value instanceof String)
if (trimmed.isEmpty())
return Collections.emptyList();
else
return Arrays.asList(trimmed.split("\\s*,\\s*", -1));
else
throw new ConfigException(name, value, "Expected a comma separated list.");
case CLASS:
if (value instanceof Class)
return (Class<?>) value;
else if (value instanceof String)
return Class.forName(trimmed, true, Utils.getContextOrKafkaClassLoader());
else
throw new ConfigException(name, value, "Expected a Class instance or class name.");
default:
throw new IllegalStateException("Unknown type.");
}
} catch (NumberFormatException e) {
throw new ConfigException(name, value, "Not a number of type " + type);
} catch (ClassNotFoundException e) {
throw new ConfigException(name, value, "Class " + value + " could not be found.");
}
}
在 parseType 方法中,我们就可以将用户传进来的参数props
解析成对应的类。
4.最后
本文简单的对KafkaProducer
初始化进行简单分析,解析参数这一块后面如果有用到,大家可以借鉴一下kafka的源码。下篇将对Producer的主流程进行剖析。
参考文档:
史上最详细kafka源码注释(kafka-0.10.2.0-src)
kafka技术内幕-图文详解Kafka源码设计与实现