ヒント:この記事はカフカ2.2.1バージョンに基づいています。この記事は、コード手段が、ソースコードに興味を持っていない場合は、メッセージングプロセスを探求するステップバイステップのソースに基づいており、あなたはテキストメッセージて送信メッセージをローカルキャッシュストレージ構造をフローチャート見るために最後までスキップすることができます。
以上のことからカフカプロデューサープロデューサー知人次のように、sendメソッドKafkaProducerにメッセージを送信することができ、sendメソッドの宣言は次のとおりです。
Future<RecordMetadata> send(ProducerRecord<K, V> record)
Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback)
上記のAPIから見ることができ、パッケージProducerRecordに送信するKafkaProducer最初のメッセージのニーズを使用して、それは未来の典型的なデザインパターン、今後のオブジェクトを返すときに、ユーザーがメッセージを送信します。呼び出し可能なインターフェイスは、送信時に送信のためのコールバック・メッセージを指定することができます。
クラス図を学習する前に、我々は最初のProducerRecordメッセージをカプセル化するためのメッセージングプロセスを見て、メッセージがカフカの抽象化であるかについて知って最初に。
1、ProducerRecord図クラス
我々ProducerRecordコア属性、メッセージを構成、すなわち6つのコア要素の最初のルック:
- 文字列のトピック
メッセージは、テーマを属します。 - 整数パーティションの
すべての以前の2が指定されていない場合は、キーを指定した場合の主題のメッセージキューは、人為的に、指定できる数は、その後、総数のhashCode剰余選択したパーティションにキューにキーを使用して、それが話題をポーリングしますパーティション。 - ヘッダーは、ヘッダー
メッセージ、別々に格納されたメッセージ本文の追加属性を。 - Kキー
この値が指定されている場合、メッセージキーは、パーティションは、キュー番号ハッシュコード値を法を選択するために使用されます。 - V値
メッセージ本体。 - 長いタイムスタンプ
構成情報message.timestamp.typeトピックの値によるメッセージのタイムスタンプは、異なる値を付与します。- CREATETIME
クライアントがメッセージを送信し、送信タイムスタンプ。 - LogAppendTimeの
タイムスタンプを追加するためのメッセージブローカ。
- CREATETIME
キーと値のペアのシリーズですヘッダー。
ProducerRecordを理解した後、私たちはカフカのメッセージングプロセスを模索し始めました。
2、カフカ追加のメッセージ・フロー
KafkaProducer送信方法、およびメッセージ・ブローカに直接送信されず、メッセージは2つのステップに非同期カフカ、すなわち送信される、機能メソッドはメモリに追加メッセージを送ることである(パーティションのキューバッファ)、次に説明しますでカフカBrokerに大量に送られた特別なスレッドの非同期キャッシュされたメッセージを送信します。
メッセージKafkaProducer#は、追加の入り口を送ります
public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
// intercept the record, which can be potentially modified; this method does not throw exceptions
ProducerRecord<K, V> interceptedRecord = this.interceptors.onSend(record); // @1
return doSend(interceptedRecord, callback); // @2
}
1 @コード:最初のメッセージ・インターセプタを行う、インターセプタはinterceptor.classesによって指定され、リスト<string>を入力し、各要素は、パス定義インターセプタの完全修飾クラス名です。
2 @コード:doSend方法、我々はコールバックのタイミングを見てする必要があること、フォローアップコールを実行します。
次に、我々はdoSend方法を見て。
2.1 DoSend
KafkaProducer#DoSend
ClusterAndWaitTime clusterAndWaitTime;
try {
clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), maxBlockTimeMs);
} catch (KafkaException e) {
if (metadata.isClosed())
throw new KafkaException("Producer closed while send in progress", e);
throw e;
}
long remainingWaitMs = Math.max(0, maxBlockTimeMs - clusterAndWaitTime.waitedOnMetadataMs);
ステップ1:メタデータ、時間のかかる方法でリターンを引っ張って、トピックパーティションのリスト、そうでない場合はトピックのローカルパーティション情報、遠位取得ブローカーへの必要性を取得します。メッセージ部分の送信時間の間に待機時間最大損失を差し引かれます。
ヒント:この記事では、この方法の詳細な調査を実施する予定はありませんが、同期メカニズムカフカのメタデータを分析するための特別なフォローアップの記事があるだろう、ネームサーバと同様に同様のRocketMQを捧げました。
KafkaProducer#DoSend
byte[] serializedKey;
try {
serializedKey = keySerializer.serialize(record.topic(), record.headers(), record.key());
} catch (ClassCastException cce) {
throw new SerializationException("Can't convert key of class " + record.key().getClass().getName() +
" to class " + producerConfig.getClass(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG).getName() +
" specified in key.serializer", cce);
}
ステップ2:シリアルキー。注:シリアル化の方法、入ってくる話題はあるものの、ヘッダこれら二つの性質が、唯一のシリアル化されたキーに参加します。
KafkaProducer#DoSend
byte[] serializedValue;
try {
serializedValue = valueSerializer.serialize(record.topic(), record.headers(), record.value());
} catch (ClassCastException cce) {
throw new SerializationException("Can't convert value of class " + record.value().getClass().getName() +
" to class " + producerConfig.getClass(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG).getName() +
" specified in value.serializer", cce);
}
ステップ3:シリアル化のメッセージ本文の内容。
KafkaProducer#DoSend
int partition = partition(record, serializedKey, serializedValue, cluster);
tp = new TopicPartition(record.topic(), partition);
STEP4:メッセージが負荷宛てパーティションパーティションのアルゴリズムに従って計算されます。次のようにDefaultPartitionerのデフォルトの実装クラスは、ルーティングアルゴリズムは次のとおりです。
- 指定されたキー場合は、キーがパーティションモジュロハッシュコードの番号が使用されています。
- キーが指定されていない場合、すべてのポーリング地区。
KafkaProducer#DoSend
setReadOnly(record.headers());
Header[] headers = record.headers().toArray();
STEP5:メッセージヘッダー情報(RecordHeaders)場合、読み取り専用に設定されています。
KafkaProducer#DoSend
int serializedSize = AbstractRecords.estimateSizeInBytesUpperBound(apiVersions.maxUsableProduceMagic(),
compressionType, serializedKey, serializedValue, headers);
ensureValidRecordSize(serializedSize);
STEP5:バージョン番号がメッセージ・プロトコルに従ってメッセージの長さを計算するために使用され、例外がスローされた場合よりも、指定された長さを超えています。
KafkaProducer#DoSend
long timestamp = record.timestamp() == null ? time.milliseconds() : record.timestamp();
log.trace("Sending record {} with callback {} to topic {} partition {}", record, callback, record.topic(), partition);
Callback interceptCallback = new InterceptorCallback<>(callback, this.interceptors, tp);
STEP6:最初の初期化メッセージのタイムスタンプ、及び着信呼び出し可能(コールバック)は、インターセプタチェーンに加えました。
KafkaProducer#DoSend
if (transactionManager != null && transactionManager.isTransactional())
transactionManager.maybeAddPartitionToTransaction(tp);
ステップ7:トランザクションプロセッサが空でない場合、関連するトランザクション管理の実装は、このセクションが解析されると推定し、対応する記事に従って、メッセージの実装の詳細に関連する事項を考慮していません。
KafkaProducer#DoSend
RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey, serializedValue, headers, interceptCallback, remainingWaitMs);
if (result.batchIsFull || result.newBatchCreated) {
log.trace("Waking up the sender since topic {} partition {} is either full or getting a new batch", record.topic(), partition);
this.sender.wakeup();
}
return result.future;
ステップ8:メッセージを探求するために、この記事の必要性の焦点になるバッファゾーンに追加されます。現在のバッファがいっぱいになるか、新しいバッファゾーン、(メッセージスレッド)を作成目覚め送信者である場合には、最終的には、バッファゾーンブローカーサーバーにメッセージを送信未来を返します。ここでは、またdoSendメソッドの実行が完了した後に、そのメッセージは、必ずしもブローカーに正常に送信されていないことを学ぶことができ、ここから、古典的な将来設計パターンです。
KafkaProducer#DoSend
} catch (ApiException e) {
log.debug("Exception occurred during message send:", e);
if (callback != null)
callback.onCompletion(null, e);
this.errors.record();
this.interceptors.onSendError(record, tp, e);
return new FutureFailure(e);
} catch (InterruptedException e) {
this.errors.record();
this.interceptors.onSendError(record, tp, e);
throw new InterruptException(e);
} catch (BufferExhaustedException e) {
this.errors.record();
this.metrics.sensor("buffer-exhausted-records").record();
this.interceptors.onSendError(record, tp, e);
throw e;
} catch (KafkaException e) {
this.errors.record();
this.interceptors.onSendError(record, tp, e);
throw e;
} catch (Exception e) {
// we notify interceptor about all exceptions, since onSend is called before anything else in this method
this.interceptors.onSendError(record, tp, e);
throw e;
}
ステップ9は:異常の様々なため、関連する情報を収集します。
RecordAccumulator:次のメッセージが送信バッファプロデューサー、その実装クラスに追加する方法に焦点を当てます。
2.2 RecordAccumulatorの追記方法は、詳細
RecordAccumulator#追記
public RecordAppendResult append(TopicPartition tp,
long timestamp,
byte[] key,
byte[] value,
Header[] headers,
Callback callback,
long maxTimeToBlock) throws InterruptedException {
この方法を導入する前に、我々は最初のメソッドのパラメータを見てください。
- TPのTopicPartitionの
トピックとそのトピックのパーティションに送信されたパーティション情報。 - 長いタイムスタンプ
タイムスタンプ時に送信するクライアント。 - バイト[]キーを
キーメッセージ。 - バイト[]の値
メッセージ本文。 - ヘッダ[]ヘッダー
メッセージヘッダ、メッセージは、追加の属性として理解することができます。 - コールバックコールバック
コールバックメソッド。 - 長いmaxTimeToBlockの
メッセージの追加タイムアウト。
RecordAccumulator#追記
Deque<ProducerBatch> dq = getOrCreateDeque(tp);
synchronized (dq) {
if (closed)
throw new KafkaException("Producer closed while send in progress");
RecordAppendResult appendResult = tryAppend(timestamp, key, value, headers, callback, dq);
if (appendResult != null)
return appendResult;
}
ステップ1:tryAppendがメッセージバッファに追加メソッドを呼び出して、トピックおよびパーティションに応じて両端キューカフカを得る、そうでない場合は、1を作成してみてください。カフカは、各パーティションは、各トピックのためにキャッシュに追加最初のメッセージをメッセージバッファを作成し、直ちにメッセージAPIリターンを送信し、キャッシュ内の別のスレッドによって送信者のメッセージを定期的にブローカーに送信するであろう。ArrayQequeを使用して、このキャッシュ領域を実現。追加の成功した場合、そのキャッシュにメッセージを追加するtryAppend方式の試行を呼び出し、結果が返されます。
次のプロセスを説明する前に、ストレージ構造カフカの両端キューで我々初見:
RecordAccumulator#追記
int size = Math.max(this.batchSize, AbstractRecords.estimateSizeInBytesUpperBound(maxUsableMagic, compression, key, value, headers));
log.trace("Allocating a new {} byte message buffer for topic {} partition {}", size, tp.topic(), tp.partition());
buffer = free.allocate(size, maxTimeToBlock);
ステップ2:成功への第一歩は、それが現在入手可能ProducerBatchであることを示す、追加されていない場合は、ProducerBatchを作成する必要があり、バッファプールはありませんためにメモリに残っている場合ので、作成ProducerBatchの準備をするためにアプリケーションbatch.sizeメモリ空間内のバッファプールを開始指定された時間内にメモリには適用されない場合は、例外がスローされ、最もmaxTimeToBlockを待ちます。
RecordAccumulator#追記
synchronized (dq) {
// Need to check if producer is closed again after grabbing the dequeue lock.
if (closed)
throw new KafkaException("Producer closed while send in progress");
// 省略部分代码
MemoryRecordsBuilder recordsBuilder = recordsBuilder(buffer, maxUsableMagic);
ProducerBatch batch = new ProducerBatch(tp, recordsBuilder, time.milliseconds());
FutureRecordMetadata future = Utils.notNull(batch.tryAppend(timestamp, key, value, headers, callback, time.milliseconds()));
dq.addLast(batch);
incomplete.add(batch);
// Don't deallocate this buffer in the finally block as it's being used in the record batch
buffer = null;
return new RecordAppendResult(future, dq.size() > 1 || batch.isFull(), true);
}
ステップ3:新しいバッチProducerBatchを作成し、バッチにメッセージを書き込み、その結果が追加されて返され、次のキーポイントがあります。
- 创建 ProducerBatch ,其内部持有一个 MemoryRecordsBuilder对象,该对象负责将消息写入到内存中,即写入到 ProducerBatch 内部持有的内存,大小等于 batch.size。
- 将消息追加到 ProducerBatch 中。
- 将新创建的 ProducerBatch 添加到双端队列的末尾。
- 将该批次加入到 incomplete 容器中,该容器存放未完成发送到 broker 服务器中的消息批次,当 Sender 线程将消息发送到 broker 服务端后,会将其移除并释放所占内存。
- 返回追加结果。
纵观 RecordAccumulator append 的流程,基本上就是从双端队列获取一个未填充完毕的 ProducerBatch(消息批次),然后尝试将其写入到该批次中(缓存、内存中),如果追加失败,则尝试创建一个新的 ProducerBatch 然后继续追加。
接下来我们继续探究如何向 ProducerBatch 中写入消息。
2.3 ProducerBatch tryAppend方法详解
ProducerBatch #tryAppend
public FutureRecordMetadata tryAppend(long timestamp, byte[] key, byte[] value, Header[] headers, Callback callback, long now) {
if (!recordsBuilder.hasRoomFor(timestamp, key, value, headers)) { // @1
return null;
} else {
Long checksum = this.recordsBuilder.append(timestamp, key, value, headers); // @2
this.maxRecordSize = Math.max(this.maxRecordSize, AbstractRecords.estimateSizeInBytesUpperBound(magic(),
recordsBuilder.compressionType(), key, value, headers)); // @3
this.lastAppendTime = now; //
FutureRecordMetadata future = new FutureRecordMetadata(this.produceFuture, this.recordCount,
timestamp, checksum,
key == null ? -1 : key.length,
value == null ? -1 : value.length,
Time.SYSTEM); // @4
// we have to keep every future returned to the users in case the batch needs to be
// split to several new batches and resent.
thunks.add(new Thunk(callback, future)); // @5
this.recordCount++;
return future;
}
}
代码@1:首先判断 ProducerBatch 是否还能容纳当前消息,如果剩余内存不足,将直接返回 null。如果返回 null ,会尝试再创建一个新的ProducerBatch。
代码@2:通过 MemoryRecordsBuilder 将消息写入按照 Kafka 消息格式写入到内存中,即写入到 在创建 ProducerBatch 时申请的 ByteBuffer 中。本文先不详细介绍 Kafka 各个版本的消息格式,后续会专门写一篇文章介绍 Kafka 各个版本的消息格式。
代码@3:更新 ProducerBatch 的 maxRecordSize、lastAppendTime 属性,分别表示该批次中最大的消息长度与最后一次追加消息的时间。
代码@4:构建 FutureRecordMetadata 对象,这里是典型的 Future模式,里面主要包含了该条消息对应的批次的 produceFuture、消息在该批消息的下标,key 的长度、消息体的长度以及当前的系统时间。
代码@5:将 callback 、本条消息的凭证(Future) 加入到该批次的 thunks 中,该集合存储了 一个批次中所有消息的发送回执。
流程执行到这里,KafkaProducer 的 send 方法就执行完毕了,返回给调用方的就是一个 FutureRecordMetadata 对象。
源码的阅读比较枯燥,接下来用一个流程图简单的阐述一下消息追加的关键要素,重点关注一下各个 Future。
2.4 Kafka 消息追加流程图与总结
上面的消息发送,其实用消息追加来表达更加贴切,因为 Kafka 的 send 方法,并不会直接向 broker 发送消息,而是首先先追加到生产者的内存缓存中,其内存存储结构如下:ConcurrentMap< TopicPartition, Deque< ProducerBatch>> batches,那我们自然而然的可以得知,Kafka 的生产者为会每一个 topic 的每一个 分区单独维护一个队列,即 ArrayDeque,内部存放的元素为 ProducerBatch,即代表一个批次,即 Kafka 消息发送是按批发送的。其缓存结果图如下:
KafkaProducer 的 send 方法最终返回的 FutureRecordMetadata ,是 Future 的子类,即 Future 模式。那 kafka 的消息发送怎么实现异步发送、同步发送的呢?
プロジェクト関係者だけでsendメソッドは、実際には暗黙の結果を返す取得するには、同期伝送を使用する必要がある場合、メッセージがブローカに送信されていない場合は、戻り値の後、答えは、そのget()メソッドを呼び出すと、今回の方法をお送りしますこの方法は、ウェイクアップメッセージブローカーが得られる結果と、結果メッセージを返すまでである、ブロックされます。あなたは非同期に送信する必要がある場合は、送信(ProducerRecord <K、V>のレコード、Callbackコールバック)を使用することが推奨されていますが、メソッドの呼び出しを取得することはできません。コールバックは、ブローカーの呼び出し、およびインターセプタの結果の後に応答を受け取ることになります。
プロセスに関する追加情報は、メッセージには、バッファゾーンに追加され、ここで紹介し、ブローカーに送信されますどのようなそれを終了?次の記事でウィルの詳細。
ヘルプポイントへの記事は参考になりましである場合には、トラブルの賞賛、あなたの認識と支援していただきありがとうございます。
著者:
丁偉、「RocketMQ技術インサイダー」の著者、RocketMQコミュニティ説教、公共数:ミドルウェア関心サークル擁護、Javaソースコード解析のセット、Javaと契約(JUC)が公開されている、ネッティー、Mycat、ダボ、 RocketMQ、MyBatisのと他のソース列。高品質の技術交流のコミュニティを構築するために、私の知識の惑星へようこそ。