Kafka源码分析2-Producer初始化

1,324 阅读10分钟

欢迎大家关注 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,在使用时,只需要如下两步:

  1. 初始化 KafkaProducer 实例;
  2. 调用 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源码设计与实现

Kafka 源码分析系列