Kafka源碼分析-Producer(1)-KafkaProducer分析

一.Kafka發(fā)送消息的整體流程:

kafka生產(chǎn)者整體架構(gòu) (1).png

步驟:
1.ProducerInterceptors對消息進(jìn)行攔截。
2.Serializer對消息的key和value進(jìn)行序列化。
3.Partitioner為消息選擇合適的Partition。
4.RecordAccumulator收集消息,實現(xiàn)批量發(fā)送。
5.Sender從RecordAccumulator獲取消息。
6.構(gòu)造ClientRequest。
7.將ClientRequest交給NetworkClient,準(zhǔn)備發(fā)送。
8.NetworkClient將請求送入KafkaChannel的緩存。
9.執(zhí)行網(wǎng)絡(luò)I/O,發(fā)送請求。
10.收到響應(yīng),調(diào)用ClientRequest的回調(diào)函數(shù)。
11.調(diào)用RecordBatch的回調(diào)函數(shù),最終調(diào)用每個消息上注冊的回調(diào)函數(shù)。
消息發(fā)送過程中,涉及兩個線程協(xié)同工作。主線程首先將業(yè)務(wù)數(shù)據(jù)封裝成ProducerRecord對象,之后調(diào)用send()方法將消息放入RecordAccumulator(消息收集器,也是主線程和sender線程共享的緩沖區(qū))中暫存。Sender線程負(fù)責(zé)將消息信息構(gòu)成請求,最終執(zhí)行網(wǎng)絡(luò)I/O的線程,它從RecordAccumulator中取出消息并批量發(fā)送出去。KafkaProducer是線程安全的,多個線程可以共享使用一個KafkaProducer對象。
KafkaProducer實現(xiàn)了Producer接口,在Producer接口中定義了KafkaProducer對外提供的API,分為四類方法:

  • send()方法:發(fā)送消息,實際上是將消息放入RecordAccumulator暫存,等待發(fā)送。
  • flush()方法:刷新操作,等待RecordAccumulator所有信息發(fā)送完,在刷新完成前會阻塞調(diào)用線程。
  • partitionFor()方法:在KafkaProducer中維護(hù)了一個Metadata對象用于存儲Kafka集群的元數(shù)據(jù),Metadata中的元素會定期更新。partitionFor()方法負(fù)責(zé)從Metadata中獲取指定Topic分區(qū)信息。
  • close()方法:關(guān)閉Producer對象,主要操作是設(shè)置close標(biāo)志,等待RecordAccumulator中的消息清空,關(guān)閉Sender線程。

二.KafkaProducer分析:

KafkaProducer重要的字段:

KafkaProducer.jpeg
  • clientId:這個生產(chǎn)者的唯一標(biāo)識。
  • partitioner: 分區(qū)選擇器,根據(jù)一定的策略,將消息路由到合適的分區(qū)。
  • maxRequestSize:消息的最大長度,這個長度包括消息頭,序列化后的key和序列化后的value的長度。
  • totalMemorySize:發(fā)送單個消息的緩沖區(qū)大小。
  • accumulator:RecordAccumulator,用于收集并緩存消息,等待Sender線程發(fā)送。
  • sender: 發(fā)送消息的Sender任務(wù),實現(xiàn)了Runnable接口,在ioThread線程中運(yùn)行。
  • ioThread:執(zhí)行Sender任務(wù)發(fā)送消息的線程,稱為“Sender線程”。
  • compressionType:壓縮算法,可選項有none,gzip,snappy,lz4。這是針對RecordAccumulator中多條消息進(jìn)行的壓縮,所以消息越多,壓縮效果越好。
  • keySerializer: key的序列化器。
  • valueSerializer: value的序列化器。
  • metadata :整個Kafka集群的元數(shù)據(jù)。
  • maxBlockTimeMs: 等待更新Kafka集群元數(shù)據(jù)的最長時長。
  • requestTimeoutMs: 消息的超時時間,也就是從消息發(fā)送到收到ACK響應(yīng)的最長時長。
  • interceptor: ProducerInterceptor集合,ProducerInterceptor可以在消息發(fā)送前堆其進(jìn)行攔截或修改;也可以先于用戶的Callback,對ACK響應(yīng)進(jìn)行預(yù)處理。
  • producerConfig:配置對象,使用反射初始化KafkaProducer配置的相對對象。

KafkaProducer的構(gòu)造函數(shù):

KafkaProducer端配置加載

自定義屬性

自定義屬性,比如ProducerConfig.BOOTSTRAP_SERVERS_CONFIG:A list of host/port pairs。表示用于初始化連接Kafka cluster的ip:port,用來獲得全部Kafka cluster的列表。所以不用全部寫,但是最好多寫幾個,為了防止一個掛了。

props.put(ProducerConfig.CLIENT_ID_CONFIG, "testConstructorClose");
        props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
        props.put(ProducerConfig.METRIC_REPORTER_CLASSES_CONFIG, MockMetricsReporter.class.getName());
        props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.IntegerSerializer");
        props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,"org.apache.kafka.common.serialization.StringSerializer");
KafkaProducer producer=new KafkaProducer<>(props);

加載默認(rèn)屬性和自定義屬性

實例化類ProducerConfig,類ProducerConfig里有個靜態(tài)屬性ConfigDef CONFIG = new ConfigDef()。

/**
     * A producer is instantiated by providing a set of key-value pairs as configuration. Valid configuration strings
     * are documented <a >here</a>.
     * @param properties   The producer configs
     */
    public KafkaProducer(Properties properties) {
        this(new ProducerConfig(properties), null, null);
    }

加載默認(rèn)屬性:

static {
        CONFIG = new ConfigDef().define(BOOTSTRAP_SERVERS_CONFIG, Type.LIST, Importance.HIGH, CommonClientConfigs.BOOSTRAP_SERVERS_DOC)
                                .define(BUFFER_MEMORY_CONFIG, Type.LONG, 32 * 1024 * 1024L, atLeast(0L), Importance.HIGH, BUFFER_MEMORY_DOC)
......

最后把自定義的屬性value覆蓋默認(rèn)的屬性value:

public Map<String, Object> parse(Map<?, ?> props) {
        // Check all configurations are defined
        List<String> undefinedConfigKeys = undefinedDependentConfigs();
        if (!undefinedConfigKeys.isEmpty()) {
            String joined = Utils.join(undefinedConfigKeys, ",");
            throw new ConfigException("Some configurations in are referred in the dependents, but not defined: " + joined);
        }
        // parse all known keys
        Map<String, Object> values = new HashMap<>();
        for (ConfigKey key : configKeys.values()) {
            Object value;
            // props map contains setting - assign ConfigKey value
            if (props.containsKey(key.name)) {
                value = parseType(key.name, props.get(key.name), key.type);
                // props map doesn't contain setting, the key is required because no default value specified - its an error
            } else if (key.defaultValue == NO_DEFAULT_VALUE) {
                throw new ConfigException("Missing required configuration \"" + key.name + "\" which has no default value.");
            } else {
                // otherwise assign setting its default value
                value = key.defaultValue;
            }
            if (key.validator != null) {
                key.validator.ensureValid(key.name, value);
            }
            values.put(key.name, value);
        }
        return values;
    }

KafkaProducer的構(gòu)造函數(shù)開始

KafkaProducer的構(gòu)造函數(shù)會初始化上面的字段,幾個重要的字段介紹下。

 private KafkaProducer(ProducerConfig config, Serializer<K> keySerializer, Serializer<V> valueSerializer) {
        try {
            log.trace("Starting the Kafka producer");
         ......
//通過反射機(jī)制實例化配置的partitioner類。
            this.partitioner = config.getConfiguredInstance(ProducerConfig.PARTITIONER_CLASS_CONFIG, Partitioner.class);
 
//創(chuàng)建并更新kafka集群的元數(shù)據(jù)
            this.metadata = new Metadata(retryBackoffMs, config.getLong(ProducerConfig.METADATA_MAX_AGE_CONFIG));
  List<InetSocketAddress> addresses = ClientUtils.parseAndValidateAddresses(config.getList(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG));
            this.metadata.update(Cluster.bootstrap(addresses), time.milliseconds());           
      
//創(chuàng)建RecordAccumulator
            this.accumulator = new RecordAccumulator(config.getInt(ProducerConfig.BATCH_SIZE_CONFIG),
                    this.totalMemorySize,
                    this.compressionType,
                    config.getLong(ProducerConfig.LINGER_MS_CONFIG),
                    retryBackoffMs,
                    metrics,
                    time);
          
           ......
ChannelBuilder channelBuilder = ClientUtils.createChannelBuilder(config.values());
//創(chuàng)建NetworkClient,這個是KafkaProducer網(wǎng)絡(luò)I/O的核心,后面會講到。
            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);
            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對應(yīng)的線程
            this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
            this.ioThread.start();

//通過反射機(jī)制實例化配置的keySerializer類,valueSerializer類
            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)).getConfiguredInstances(ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,
                    ProducerInterceptor.class);
            this.interceptors = interceptorList.isEmpty() ? null : new ProducerInterceptors<>(interceptorList);

            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的send()方法:

KafkaProducer的send()方法的調(diào)用流程:

KafkaProducer send()方法的調(diào)用流程.jpg
  • 調(diào)用ProducerInterceptors.onSend()方法,通過ProducerInterceptor對消息進(jìn)行攔截或修改。
  • 調(diào)用waitOnMetadata()方法獲取Kafka集群的信息,底層會喚醒Send線程更新Metadata中保存的Kafka集群元數(shù)據(jù)。
  • 調(diào)用Serializer.serialize()方法序列化消息的key和value。
  • 調(diào)用partition()為消息選擇合適的分區(qū)。
  • 調(diào)用RecordAccumulator.append()方法,將消息追加到RecordAccumulator中。
  • 喚醒Sender線程,由Sender線程將RecordAccumulator中緩存的消息發(fā)出去。

三.ProducerInterceptors&ProducerInterceptor

ProducerInterceptors是一個ProducerInterceptor集合,方法onSend(),onAcknowledgement(),onSendError(),實際上是循環(huán)調(diào)用其封裝的ProducerInterceptor集合的對應(yīng)方法。
ProducerInterceptor對象可以在消息發(fā)送之前對其進(jìn)行攔截或修改,也可以先于用戶的Callback,對ACK響應(yīng)進(jìn)行預(yù)處理。可以把它想象成java web的filter。創(chuàng)建ProducerInterceptor類,只要實現(xiàn)ProducerInterceptor接口,創(chuàng)建其對象并添加到ProducerInterceptors中即可。

四.Kafka集群元數(shù)據(jù)

Leader副本的動態(tài)變化

在生產(chǎn)者的角度來看,分區(qū)的數(shù)量以及Leader副本的分布是動態(tài)變化的。比如:

  • 在運(yùn)行過程中,Leader副本隨時都有可能出現(xiàn)故障而導(dǎo)致Leader副本的重新選舉,新的Leader副本會在其他Broker上繼續(xù)對外提供服務(wù)。
  • 當(dāng)需要提高某Topic的并行處理消息的能力時,我們可以通過增加其分區(qū)的數(shù)量來實現(xiàn)。

KafkaProducer發(fā)送消息時的路由方法:

KafkaProducer要將此消息追加到指定Topic的某個分區(qū)的Leader副本中,首先需要知道Topic的分區(qū)數(shù)量,經(jīng)過路由后確定目標(biāo)分區(qū)。然后KafkaProducer需要知道目標(biāo)分區(qū)的Leader副本所在服務(wù)器的地址,端口等信息,才能建立連接,將消息發(fā)送到Kafka中。

KafkaProducer元數(shù)據(jù):

在KafkaProducer維護(hù)了Kafka集群的元數(shù)據(jù):包括某個topic中有哪幾個分區(qū),每個分區(qū)的Leader副本分配哪個節(jié)點(diǎn)上,F(xiàn)ollower副本分配在哪些節(jié)點(diǎn)上,哪些副本在ISR集合中以及這些節(jié)點(diǎn)的網(wǎng)絡(luò)地址,端口。
在KafkaProducer中,使用Node,TopicPartition,PartitionInfo這三個類封裝了Kafka集群的相關(guān)數(shù)據(jù)。


image.png
222222.jpeg
333333.jpeg
  • Node表示集群中的一個節(jié)點(diǎn)。Node記錄了這個節(jié)點(diǎn)的host,ip,port等信息。
  • TopicPartition表示某Topic的一個分區(qū),其中的topic字段是Topic的名稱,partition字段是此分區(qū)在Topic中的分區(qū)編號(ID)。
  • PartitionInfo表示一個分區(qū)的詳細(xì)信息。其中topic字段和partition字段的含義與TopicPartition中的相同,除此之外,leader字段保持了Leader副本所在節(jié)點(diǎn)的id,replica字段記錄了全部副本所在的節(jié)點(diǎn)信息,inSyncReplicas字段記錄了ISR集合中所有副本所在的節(jié)點(diǎn)信息。
    通過這三個類的組合,我們可以完整表示出KafkaProducer需要的集群元數(shù)據(jù)。這些元數(shù)據(jù)保存在了Cluster這個類中,并按照不同的映射方式進(jìn)行存放,方便查詢。Cluster類的核心字段如下:
    image.png
  • nodes: Kafka集群中節(jié)點(diǎn)信息列表。
  • nodesById: BrokerId與Node節(jié)點(diǎn)之間的對于關(guān)系,方便按照BrokerId進(jìn)行查詢。
  • partitionsByTopicPartition: 記錄了TopicPartition與PartitionInfo的映射關(guān)系。
  • partitionsByTopic: 記錄了Topic名稱和PartitionInfo的映射關(guān)系,可以按照Topic的名稱查詢?nèi)康姆謪^(qū)詳細(xì)信息。
  • availablePartitionsByTopic: 記錄了Topic名稱和PartitionInfo的映射關(guān)系,這里的List<PartitionInfo>中存放的分區(qū)必須是有Leader副本的Partition,而partitionsByTopic中記錄的分區(qū)則不一定有Leader副本,因為有些中間狀態(tài),如Leader副本宕機(jī)而觸發(fā)的選舉過程中,分區(qū)不一定有Leader副本。
  • partitionByNode:記錄了Node與PartitionInfo的映射關(guān)系,可以按照節(jié)點(diǎn)id查詢其分布的全部分區(qū)的詳細(xì)信息。

Cluster的方法比較簡單,主要是針對以上的操作,方便集群元數(shù)據(jù)的查詢,例如partitionsForTopic方法:

/**
     * Get the list of partitions for this topic
     * @param topic The topic name
     * @return A list of partitions
     */
    public List<PartitionInfo> partitionsForTopic(String topic) {
        return this.partitionsByTopic.get(topic);
    }

注意:Node,TopicPartition,PartitionInfo,Cluster的所有字段都是private final修飾的,且只提供了查詢方法,并未提供任何修改方法,這就保證了這四個類的對象都是不可變性對象,它們就是線程安全的對象。
Metadata中封裝了Cluster對象,并保存Cluster數(shù)據(jù)的最后更新時間,版本號(version),是否需要更新等待信息。

MetaData核心字段:

Metadata中封裝了Cluster對象,并保存Cluster數(shù)據(jù)的最后更新時間,版本號(version),是否需要更新等待信息。


888888.jpeg
  • topics:記錄了當(dāng)前已知的所有topic,在cluster字段中記錄了Topic最新的元數(shù)據(jù)。
  • version:表示Kafka集群元數(shù)據(jù)的版本號。Kafka集群元數(shù)據(jù)每更新成功一次,version的值加1。通過新舊版本號的比較,判斷集群元數(shù)據(jù)是否更新完成。
  • metadataExpireMs: 每隔多久,更新一次。默認(rèn)是300*1000,也就是5分鐘。
  • refreshBackOffMs:兩次發(fā)出更新Cluster保存的元數(shù)據(jù)信息的最小時間差,默認(rèn)為100ms。這是為了防止更新操作過于頻繁而造成網(wǎng)絡(luò)阻塞和增加服務(wù)端的壓力。在Kafka中與重試操作有關(guān)的操作中,都有“退避(backoff)時間”設(shè)計的身影。
  • lastRefreshMs:記錄上一次更新元數(shù)據(jù)的時間戳(也包含更新失敗的情況)。
  • lastSuccessfulRefreshMs: 上一次成功更新的時間戳。如果每次都成功,則lastSuccessfulRefreshMs,lastRefreshMs相等。否則,lastRefreshMs>lastSuccessfulRefreshMs。
  • cluster:記錄Kafka集群的元數(shù)據(jù)。
  • needUpdate:標(biāo)識是否強(qiáng)制更新Cluster,這是觸發(fā)Sender線程更新集群元數(shù)據(jù)的條件之一。
  • listeners: 監(jiān)聽Metadata更新的監(jiān)聽器集合。自定義Metadata監(jiān)聽實現(xiàn)Metadata.Listener.onMetadataUpdate()方法即可,在更新Metadata中的cluster字段之前,會通知listener集合中全部Listener對象。
  • needMetadataForAllTopics:是否需要更新全部Topic的元數(shù)據(jù),一般情況下,KafkaProducer只維護(hù)它用到的Topic元素,是集群中全部Topic的子集。
    MetaData的方法比較簡單,主要是操作上面的幾個字段,主要介紹主線程用的requestUpdate()和awaitUpdate()。requestUpdate()方法將needUpdate字段修改為true,這樣當(dāng)Sender線程運(yùn)行時會更新Metadata記錄的集群元數(shù)據(jù),然后返回version字段的值。awaitUpdate()是通過version來判斷元數(shù)據(jù)是否更新完成,更新未完成則阻塞等待:
/**
     * Request an update of the current cluster metadata info, return the current version before the update
     */
    public synchronized int requestUpdate() {
        //needUpdate設(shè)置為true,表示需要強(qiáng)制更新Cluster
        this.needUpdate = true;
        //返回當(dāng)前Kafka集群元數(shù)據(jù)的版本號
        return this.version;
    }
 /**
     * Wait for metadata update until the current version is larger than the last version we know of
     */
    public synchronized void awaitUpdate(final int lastVersion, final long maxWaitMs) throws InterruptedException {
        if (maxWaitMs < 0) {
            throw new IllegalArgumentException("Max time to wait for metadata updates should not be < 0 milli seconds");
        }
        long begin = System.currentTimeMillis();
        long remainingWaitMs = maxWaitMs;
        while (this.version <= lastVersion) {
            if (remainingWaitMs != 0)
                wait(remainingWaitMs);
            long elapsed = System.currentTimeMillis() - begin;
            if (elapsed >= maxWaitMs)
                throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms.");
            remainingWaitMs = maxWaitMs - elapsed;
        }
    }

解釋:
maxWaitMs:更新metadata版本需要的最長時間。
remainingWaitMs:wait需要等待的時間。
elapsed = System.currentTimeMillis() - begin:本次循環(huán)更新消耗的時間。

Metadata中的字段可以由主線程讀,Sender線程更新,因此它必須是線程安全的,所以上面的方法都使用synchronized同步。Sender線程的內(nèi)存會在后面介紹。

KafkaProducer.waitOnMetadata()方法分析:

這個方法觸發(fā)了Kafka元數(shù)據(jù)的更新,并阻塞主線程等待更新完畢。步驟:

 /**
     * Wait for cluster metadata including partitions for the given topic to be available.
     * @param topic The topic we want metadata for
     * @param maxWaitMs The maximum time in ms for waiting on the metadata
     * @return The amount of time we waited in ms
     */
    private long waitOnMetadata(String topic, long maxWaitMs) throws InterruptedException {
        // 查看Metadata中是否包含指定Topic的元數(shù)據(jù),若不包含,則將Topic添加到topics集合中。
        if (!this.metadata.containsTopic(topic))
            this.metadata.add(topic);
       //成功獲取分區(qū)的詳細(xì)信息
        if (metadata.fetch().partitionsForTopic(topic) != null)
            return 0;

        long begin = time.milliseconds();
        long remainingWaitMs = maxWaitMs;
        while (metadata.fetch().partitionsForTopic(topic) == null) {
            log.trace("Requesting metadata update for topic {}.", topic);
            //設(shè)置needupdate,獲取當(dāng)前元數(shù)據(jù)版本號
            int version = metadata.requestUpdate();
            sender.wakeup();//喚醒Sender線程
            //阻塞等待元數(shù)據(jù)更新完畢
            metadata.awaitUpdate(version, remainingWaitMs);
            long elapsed = time.milliseconds() - begin;
            if (elapsed >= maxWaitMs)//超時檢驗
                throw new TimeoutException("Failed to update metadata after " + maxWaitMs + " ms.");
            //權(quán)限檢驗
            if (metadata.fetch().unauthorizedTopics().contains(topic))
                throw new TopicAuthorizationException(topic);
            remainingWaitMs = maxWaitMs - elapsed;
        }
        return time.milliseconds() - begin;
    }

1.查看Metadata中是否包含指定Topic的元數(shù)據(jù),若不包含,則將Topic添加到topics集合中,下次更新時會從服務(wù)端獲得指定Topic元數(shù)據(jù)。
2.嘗試獲取Topic中分區(qū)的詳細(xì)信息,失敗后會調(diào)用requestUpdate()方法設(shè)置Metadata.needUpdate字段,并得到當(dāng)前元數(shù)據(jù)版本號。
3.喚醒Sender線程,由Sender線程更新Metadata中保存的Kafka集群元數(shù)據(jù)。
4.主線程調(diào)用awaitUpdate()方法,等待Sender線程完成更新。
5.從Metadata中獲取指定Topic分區(qū)的詳細(xì)信息(即PartitionInfo集合)。若失敗,回到步驟2繼續(xù)嘗試,若等待時間超時,則拋出異常。

主線程喚醒的Sender線程會調(diào)用update()去服務(wù)端拉取Cluster信息:

/**
     * Update the cluster metadata
     */
    public synchronized void update(Cluster cluster, long now) {
        this.needUpdate = false;
        this.lastRefreshMs = now;
        this.lastSuccessfulRefreshMs = now;
        this.version += 1;

        for (Listener listener: listeners)
            listener.onMetadataUpdate(cluster);

        // Do this after notifying listeners as subscribed topics' list can be changed by listeners
        this.cluster = this.needMetadataForAllTopics ? getClusterForCurrentTopics(cluster) : cluster;

        notifyAll();
        log.debug("Updated cluster metadata version {} to {}", this.version, this.cluster);
    }

五.Serializer&Deserializer:

image.png

Kafka已經(jīng)提供了java基本類型的Serializer實現(xiàn)和Deserializer實現(xiàn),我們也可以自定義Serializer實現(xiàn)和Deserializer實現(xiàn),只要實現(xiàn)Serializer接口和Deserializer接口。介紹下Serializer,Deserializer是逆操作。
configure()方法是序列化之前的配置,如StringSerializer.configure()方法內(nèi)會選擇合適的的編碼類型(encoding),默認(rèn)是UTF-8; serializer()方法是真正進(jìn)行序列化的地方,將傳入的java對象序列化成byte[]。

六.Partitioner:

Partitioner是為消息選擇分區(qū)的分區(qū)器。
業(yè)務(wù)邏輯可以控制消息路由到哪個分區(qū),也可以不用關(guān)心分區(qū)的選擇。
先調(diào)用KafkaProducer.partition()方法:

/**
     * computes partition for given record.
     * if the record has partition returns the value otherwise
     * calls configured partitioner class to compute the partition.
     */
    private int partition(ProducerRecord<K, V> record, byte[] serializedKey , byte[] serializedValue, Cluster cluster) {
        Integer partition = record.partition();
        if (partition != null) {
            List<PartitionInfo> partitions = cluster.partitionsForTopic(record.topic());
            int lastPartition = partitions.size() - 1;
            // they have given us a partition, use it
            if (partition < 0 || partition > lastPartition) {
                throw new IllegalArgumentException(String.format("Invalid partition given with record: %d is not in the range [0...%d].", partition, lastPartition));
            }
            return partition;
        }
        return this.partitioner.partition(record.topic(), record.key(), serializedKey, record.value(), serializedValue,
            cluster);
    }

如果業(yè)務(wù)代碼沒有定義消息路由到哪個partition,那么調(diào)用Partitioner接口的默認(rèn)實現(xiàn)DefaultPartitioner。
當(dāng)創(chuàng)建KafkaProducer時傳入的key/value配置項會保存到AbstractConfig的originals字段中。AbstractConfig的核心方法是getConfiguredInstance()方法,功能是通過反射的機(jī)制實例化originals字段中指定的類。
獲取分區(qū)對象partitioner,通過反射加載默認(rèn)配置。默認(rèn)配置在ProducerConfig類的靜態(tài)屬性Config里:

 .define(PARTITIONER_CLASS_CONFIG,
                                        Type.CLASS,
                                        DefaultPartitioner.class.getName(),
                                        Importance.MEDIUM, PARTITIONER_CLASS_DOC)

反射獲取對象:

 this.partitioner = config.getConfiguredInstance(ProducerConfig.PARTITIONER_CLASS_CONFIG, Partitioner.class);


設(shè)計Configurable接口的目的是統(tǒng)一反射后的初始化過程,對外提供統(tǒng)一的初始化接口。在AbstractConfig.getConfiguredInstance方法中通過發(fā)射構(gòu)造出來的對象,都是通過無參構(gòu)造函數(shù)構(gòu)造的,需要初始化的字段個數(shù)和類型各種各樣,Configurable接口的configure()方法封裝了對象初始化過程且只有一個參數(shù)(originals字段),這樣對外接口實現(xiàn)了統(tǒng)一。**這個設(shè)計注意積累。**
KafkaProducer.partition()方法負(fù)責(zé)在ProduceRecord中沒有明確指定分區(qū)編號的時候,圍棋選擇合適的分區(qū):如果消息沒有key,會根據(jù)counter與Partition個數(shù)取模來確定分區(qū)編號,counter不斷遞增,確保消息不會都發(fā)到一個partition里;如果有key就對key進(jìn)行hash。

private final AtomicInteger counter = new AtomicInteger(new Random().nextInt());
public int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster) {
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
int numPartitions = partitions.size();
if (keyBytes == null) {
int nextValue = counter.getAndIncrement();
List<PartitionInfo> availablePartitions = cluster.availablePartitionsForTopic(topic);
if (availablePartitions.size() > 0) {
int part = DefaultPartitioner.toPositive(nextValue) % availablePartitions.size();
return availablePartitions.get(part).partition();
} else {
// no partitions are available, give a non-available partition
return DefaultPartitioner.toPositive(nextValue) % numPartitions;
}
} else {
// hash the keyBytes to choose a partition
return DefaultPartitioner.toPositive(Utils.murmur2(keyBytes)) % numPartitions;
}
}

counter用不用int而用
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,606評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,582評論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,540評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,028評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,801評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,223評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,294評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,442評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,976評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,800評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,996評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,543評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,233評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,926評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,702評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,991評論 2 374