1. 概述
一、Kafka Producer包含哪些部分
其實我們討論producer時,指的是用戶接觸的clients.producer這個util包,其中包含了發送數據到哪臺機器、怎樣序列化、分批發送與拒絕消息等等的發送策略。
但這些討論不包括Kafka接到這些消息後怎樣處理的問題,這些是broker(即kafka server端)需要去處理的部分。
二、Producer與Broker的關係
producer會維護將要發送的topic表,在必要時向broker要這些topic的元信息,並在client端維護這些元信息,來決定每條消息的去向。
從broker角度來看,它提供了元信息給producer以後,就無法控制producer的行爲了。可以這麼理解——broker本身是無狀態的機器,主要提供的是元信息的消息讀寫的接口。
三、Producer發送消息的流程簡介
- 在發送消息時,等待元信息的更新
- 將key\value序列化爲byte[],計算出數據的大小是否超出限制
- 計算出key對應的partition,以確認將消息發往哪裏
- 使用
accumulator
將數據放到topic\partition對應的緩衝區中 - 使用NetworkClient類(背後是nio selector)定期的把緩衝區中的內容發出
注:本文基於Kafka 0.9版本,KafkaProducer的代碼更新時間在2015-12-06
2. 字段與主要方法
2.1 KafkaProducer中的字段
private static final AtomicInteger PRODUCER_CLIENT_ID_SEQUENCE = new AtomicInteger(1);
private String clientId; // client.id參數:用於日誌與打點,以及發請求帶的參數
private final Partitioner partitioner; // partitioner.class參數:指定一個Partitioner類用於計算key對應的分片,可自定義
private final int maxRequestSize; // max.request.size參數:單條消息的內存byte數大於此值時,拋出RecordTooLargeException異常
private final long totalMemorySize; // buffer.memory參數:同上,但額外作爲accumulator->BufferPool中的`The maximum amount of memory that this buffer pool can allocate`,似乎是accumulator中暫存消息最多可用的內存量。
private final Metadata metadata; // 維護了一些topic的元信息
private final RecordAccumulator accumulator; // 用於堆積消息,按情況batch發送
private final Sender sender; // 發送消息
private final Thread ioThread; // 把this.sender包裝成一個Thread
private final CompressionType compressionType; //compression.type參數,壓縮數據的方式,主要用在accumulator內。有none\gzip\snappy\lz4幾種
private final Metrics metrics; // TODO 監控信息?
private final Sensor errors; // TODO 監控信息?
private final Time time; // KafkaProducer對象的創建時間
private final Serializer<K> keySerializer; // key序列化方法
private final Serializer<V> valueSerializer; // value序列化方法
private final ProducerConfig producerConfig; // 外部把配置傳入,但沒多大用處
private final long maxBlockTimeMs; // 來源不明TODO,用處似乎是限制send的一些IO操作總時間(waitOnMetadata堵塞的時間,與accumulator.append中waitMemory堵塞的時間之和,要低於此值)
private final int requestTimeoutMs; //參數`timeout.ms` or `request.timeout.ms`,用途不明TODO
2.2 KafkaProducer提供的api
2.2.1 發送一條Kafka消息
public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback)
- 該方法做一定預處理後(詳見第3節),將消息塞進accumulator緩衝區後,等待異步發送並返回,在消息實際發送完,即取到請求的返回值後,執行callback參數的回調方法。
- 返回體中包裝的RecordMetadata類型,帶有消息最終發到的partition與offset,可用future.get()堵塞到消息發送完成。
- 官方建議:由於callback會執行一個IO線程,因此建議處理速度足夠快,否則會影響到其它線程。如果需要執行堵塞或計算量大的邏輯,建議另開一個Executor來併發處理callback來的數據。
- 可能拋出的異常:InterruptException(堵塞時被打斷)、SerializationException(序列化失敗)、BufferExhaustedException(buffer空間用盡)
2.2.2 刷新緩存
public void flush()
此方法會將accumulator緩衝區中,尚未發出的那些消息變成可發送狀態。調用此方法會讓當前線程堵塞住,並保證調用flush()之前曾發送的所有消息都執行完成,即kafka服務端確實收到這些消息。
值得注意的是,在調用flush陷入堵塞時,其它線程仍然可以繼續調用send發送消息。
以下是官方舉例的一個使用case:
// 消費100條消息,打入另一個topic後,確保所有數據發送成功,再commit消費進度
for(ConsumerRecord<String, String> record: consumer.poll(100))
producer.send(new ProducerRecord("my-topic", record.key(), record.value());
producer.flush();
consumer.commit();
2.2.3 topic的partition信息
public List<PartitionInfo> partitionsFor(String topic)
可獲取指定topic的partition元信息,若本地metadata緩存中沒有此topic,則堵塞等待更新元信息。
超時時間定爲maxBlockTimeMs,堵塞可被打斷拋出InterruptException。
2.2.4 指標信息
public Map<MetricName, ? extends Metric> metrics()
獲取監控信息,或說kafka使用過程中的打點。(TODO待補充)
2.2.5 關閉producer
public void close(long timeout, TimeUnit timeUnit)
關閉當前producer,堵塞當前線程等待所有sent請求完成。
3. 發送細節與機制詳述
3.1 獲取Metadata
以下爲Metadata中的字段
private final long refreshBackoffMs; // 更新失敗時最小的再次刷新間隔時間
private final long metadataExpireMs; // 過期時間, 默認60s
private int version; // 每次更新version自增
private long lastRefreshMs; // 最近的更新時的時間
private long lastSuccessfulRefreshMs; // 最近成功更新的時間
private Cluster cluster; // 保存topic與partition、結點與partition等等的關係
private boolean needUpdate; // 需要更新 metadata
private final Map<String, Long> topics; // topic與對應的過期時間的對應關係
private final List<Listener> listeners; // 事件監控者
private boolean needMetadataForAllTopics; // 是否強制更新所有的 metadata
producer在發送前執行waitOnMetadata(String topic, long maxWaitMs)
來獲取元信息。此方法判斷緩存中是否有topic信息,沒有的話進入以下循環:
int version = metadata.requestUpdate(); // 告訴metadata信息需要更新了
sender.wakeup(); // 喚醒sender來更新metadata
metadata.awaitUpdate(version, remainingWaitMs); // 進入堵塞直到版本更新
上述的awaitUpdate操作中,metadata會堵塞住,等待sender的一系列請求成功後調用Metadata.update來喚醒自己。sender是kafka client的一個network util包裝,後面再詳細介紹。
如果這個過程超時了,或者topic unauthorized(TODO:想想unauthorized是什麼情況?),就開始拋出異常。如果沒有異常的順利完成,topic對應的partition信息就會被producer收到並cache在內存中,producer可用此信息來發送消息。
3.2 消息的發送與緩衝區
一、計算partition與結點
經過3.1中討論的元信息獲取,producer就獲取了這條消息topic有幾個partition(分片)以及這些分片對應的結點信息。
需要發往哪一個partition:
- 觀察消息是否指定了partition,未指定的話走partition類計算數據分片;
- 默認partitioner類:如果消息帶有key則按hash(keyBytes)%numPartitions,不帶有key則按round-robin輪流發往各個partition
- 可以通過partitioner.class配置,自定義partitioner類,按照業務需求來自定義partition計算方式。
確認了消息發往哪一個partition,也就能從元信息中找到partition對應的leader結點了。
二、accumulator提供的消息緩衝
確認了消息要發送的結點後,下一步就是將消息內容與結點信息塞進accumulator。
accumulator是一個buffer,負責把producer發送的消息累積在那裏,直到特定條件時發出。其buffer採用一個map<TopicPartition, Deque >來存儲,每一個topic\partition下都是一個RecordBatch隊列。
隊列中每一個RecordBatch元素,都代表一組kafka消息。在一個RecordBatch滿了,或者其它線程調用flush()\close()等操作時,producer都會喚醒sender,來負責把buffer中的RecordBatch發到kafka服務端去。
3.3 消息緩衝的flush機制
在accumulator中使用了一個AtomicInteger flushesInProgress
來表示是否在flush狀態。
在Sender類(在4.2中會詳細介紹Sender類)每次迭代時,會先算出現在要往哪些結點發送消息。計算邏輯是一個緩衝區滿了、超時、沒空間,或當前在flush狀態,就會認爲消息需要發出去了。
因此:當我們將flushesInProgress加一,則會在Sender下一輪迭代時,將所有待發數據的結點全標成待發送的。flush()流程示意如下:
- 將flushesInProgress++
- 將sender喚醒去幹活
- 等待所有未完成的緩衝消息發送完成
- 將flushesInProgress–
因此我們可以理解爲,flush()操作會將當前所有緩存都發出一次。這就保證了在flush()前所有消息都會被髮送完成,flush狀態才結束;但保持flush狀態的過程中新加入的消息,我們無法確定其狀態。
4. 拓展:Sender
前面提到更新metadata,或發送accumulator中的批量消息,以及flush(),此類網絡I/O操作,都會執行一個相同的代碼sender.wakeup()
。所以這個萬能的sender到底是什麼東西呢?
4.1 初始化與啓動
下面是producer的構造器中,把Sender初始化出來的地方:
this.sender = new Sender(client,
this.metadata,
this.accumulator,
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 : "");
this.ioThread = new KafkaThread(ioThreadName, this.sender, true);
this.ioThread.start();
producer構造了一個Runnable的sender的成員變量,並開啓線程執行。
4.2 Sender做了什麼
其主要工作是不斷的循環以下操作:
- 通過accumulator中可發出的數據包,以及metadata信息,算出現在準備發送消息的結點。(若找不到leader信息,標記一下更新metadata)
- "倒出"accumulator中的數據(把超時未發的數據打個記錄),把數據包和對應結點打包創建出ClientRequest。
- 把這些請求塞進sender中維護的client(使用nio維護的請求隊列),並通過
client.poll
來發送出去。(TODO 網絡相關的操作補充) - poll()的時候會執行一個
metadataUpdater.maybeUpdate(now)
的操作:- 先計算出下次刷新時間:根據是否請求刷新元數據(見本節第1條及3.1節中的刷新策略),或
metadata.max.age.ms
配置中的最久沒有成功刷新的時間,並考慮到retry.backoff.ms
配置中的最短刷新間隔,得出一個下次刷新的時間。 - 考慮timeToNextReconnectAttempt,以及是否正在獲取metadata信息,來更新下次刷新時間。
- 如果已經到了可刷新時間點,獲取leastLoadedNode,並將打向這個node的請求塞進client,也就搭上了這一波client.poll網絡請求的車。
- 先計算出下次刷新時間:根據是否請求刷新元數據(見本節第1條及3.1節中的刷新策略),或
可以舉個例子幫助解釋:
- 第一輪迭代中,如果需要某些node A的元信息並發現缺失了,此時sender會請求更新metadata。那麼在當前這一輪迭代結束的poll(now)操作中,通過metadataUpdater.mayBeUpdate(now)發現需要更新metadata,併發出更新的網絡請求。
- 第二輪迭代中:由於metadata的更新請求只發出沒處理,因此依然沒有缺失node的信息。但是在這一輪迭代結束的poll()中,如果metadata請求返回了,就可以通過返回值補上node A的信息。
- 第三輪迭代中:可正常獲取node A的信息,並向A發消息了。
5. 其它
TODO,待補充kafka 0.9到現在的更新內容