Kafka タイム ホイール (TimingWheel) と Kafka での遅延操作

Kafka 関連のインタビューの質問: https://blog.csdn.net/qq_28900249/article/details/90346599

Kafka には、プロダクションの遅延、プルの遅延、削除の遅延など、遅延操作が多数あります。Kafkaでは遅延機能の実装にJDK付属のTimerやDelayQueueを使用せず、タイムホイールに基づいて遅延機能を実装するためのタイマー(SystemTimer)をカスタマイズします。JDK の Timer および DelayQueue の挿入および削除操作の平均時間計算量は O(nlog(n)) ですが、これでは Kafka の高パフォーマンス要件を満たすことができません。挿入および削除操作の時間計算量は、タイム ホイールに基づいて O に削減できます。 (1)。タイム ホイールのアプリケーションは Kafka に固有のものではなく、他にも多くのアプリケーション シナリオがあり、Netty、Akka、Quartz、Zookeeper などのコンポーネントにタイム ホイールの痕跡があります。

以下の図を参照すると、Kafka のタイミング ホイール (TimingWheel) は、タイミング タスクを格納するための循環キューです。最下層は配列によって実装され、配列内の各要素はタイミング タスク リスト (TimerTaskList) を格納できます。TimerTaskList は循環二重リンク リストであり、リンク リスト内の各項目は、リアルタイム タイマー タスク TimerTask をカプセル化するタイマー タスク エントリ (TimerTaskEntry) を表します。

図1

タイム ホイールは複数のタイム グリッドで構成され、各タイム グリッドは現在のタイム ホイールの基本タイム スパン (tickMs) を表しますタイム ホイールのタイム グリッドの数は固定されており、wheelSize で表すことができます。タイム ホイール全体の全体のタイム スパン (間隔) は、ticketMs × WheelSize の式で計算できます。タイム ホイールには、タイム ホイールの現在時刻を示すために使用されるダイヤル ポインタ (currentTime) もあり、currentTime は、tickM の整数倍です。currentTime は、タイム ホイール全体を期限切れの部分と期限切れになっていない部分に分割できます。currentTime が現在指している時間グリッドも期限切れの部分に属します。つまり、期限切れになるだけであり、この時間グリッドに対応する TimerTaskList のすべてのタスクは、処理される。

タイムホイールのtickMs=1ms、wheelSize=20とすると、間隔は20msと計算できます。最初に、ダイヤル ポインタ currentTime はタイム スロット 0 を指し、2ms のタイミングを持つタスクが挿入され、タイム スロット 2 の TimerTaskList に保存されます。時間が経つにつれて、ポインタ currentTime は進み続け、2ms 後にタイムスロット 2 に到達すると、それに応じてタイムスロット 2 に対応する TimeTaskList 内のタスクを期限切れにする必要があります。このとき、8msのタイミングの別のタスクが挿入されると、それはタイムグリッド10に格納され、currentTimeはさらに8ms後のタイムグリッド10を指すことになります。19msのタイミングのタスクが同時に挿入された場合はどうなるでしょうか? 新しい TimerTaskEntry は元の TimerTaskList を再利用するため、期限切れのタイム スロット 1 に挿入されます。つまり、タイム ホイール全体のスパンは変化せず、ポインタ currentTime が継続的に進むにつれて、現在のタイム ホイールが処理できる期間も逆方向に進み、全体の時間範囲は currentTime と currentTime+interval の間になります。 。

このときタイミングが350msのタスクがあった場合はどうなるでしょうか?WheelSizeのサイズを直接拡張しますか?Kafka には、数万ミリ秒、さらには数十万ミリ秒のスケジュールされたタスクがあります。このホイールサイズの拡張に最終的な利益はありません。すべてのスケジュールされたタスクの有効期限に 100 万などの上限が設定されている場合でも、ミリ秒の場合、ホイールサイズは 100 万ミリ秒になります。タイム ホイールは多くのメモリ領域を占有するだけでなく、効率も低下します。この目的のために、Kafka は階層型タイム ホイールの概念を導入し、タスクの期限が現在のタイム ホイールで表される時間範囲を超えると、それを上位レベルのタイム ホイールに追加しようとします。

図2

上の図を参照すると、前のケース、最初の層のタイム ホイールのティックMs = 1ms、wheelSize = 20、interval = 20msを再利用します。2 番目の層のタイム ホイールの TickM は、1 番目の層のタイム ホイールの間隔 (20ms) です。タイム ホイールの各層の WheelSize は固定されており、20 であるため、タイム ホイールの 2 番目の層の全体的なタイム スパン間隔は 400 ミリ秒です。類推すると、この 400 ミリ秒は 3 番目の層のティックのサイズでもあり、3 番目の層のタイム ホイールの全体のタイム スパンは 8000 ミリ秒になります。

前述の 350ms タイミング タスクの場合、第 1 レベルのタイム ホイールでは条件を満たせないことは明らかなので、第 2 レベルのタイム ホイールにアップグレードされ、最終的に第 2 レベルのタイム グリッド 17 に対応する TimerTaskList に挿入されます。 -レベルタイムホイール。この時点で 450ms のタイミングを持つ別のタスクがある場合、明らかに第 2 レベルのタイム ホイールでは条件を満たすことができないため、第 3 レベルのタイム ホイールにアップグレードされ、最終的にタイム グリッド 1 の TimerTaskList に挿入されます。 3 番目のレベルのタイム ホイールの中央。有効期限が間隔 [400ms、800ms] である複数のタスク (446ms、455ms、473ms のタイミング タスクなど) は、第 3 レベルのタイム ホイールのタイム グリッド 1 に配置され、TimerTaskList は時間に対応することに注意してください。グリッド 1 タイムアウト期間は 400 ミリ秒です。時間が経つと、次の TimerTaskList の期限が切れると、当初 450 ミリ秒にスケジュールされていたタスクには 50 ミリ秒が残っており、このタスクの期限切れ操作は実行できません。ここでは、タイム ホイールをダウングレードする操作を示します。これにより、残り時間が 50 ミリ秒のタイミング タスクが階層タイム ホイールに再送信されます。この時点で、タイム ホイールの最初の層の全体的なタイム スパンは十分ではありませんが、2 番目の層はレイヤーだけで十分なので、タスクは第 2 レベルのタイム ホイールの有効期限が [40ms、60ms) であるタイム グリッドに配置されます。さらに 40 ミリ秒後、この時点でタスクは再び「認識」されますが、まだ 10 ミリ秒が残っており、適切な操作をすぐに実行することはできません。したがって、タイム ホイールの別のダウングレードがあります。このタスクは、タイム ホイールの最初の層の有効期限が [10ms, 11ms) であるタイム グリッドに追加されます。さらに 10ms 後、タスクは実際に期限切れになり、対応する期限切れ操作。

デザインは人生から生まれます。私たちの一般的な時計は 3 層構造のタイムホイールで、最初の層のタイムホイールは、tickMs=1s、wheelSize=60、interval=1min (秒)、2 層目は、tickMs=1min、wheelSize=60、interval= です。 1 時間、これは分です。3 番目のレイヤーは、tickMs=1 時間、wheelSize は 12、間隔は 12 時間、これは時計です。

Kafka の最初のレベルのタイム ホイールのパラメーターは上記の場合と同じです。tickMs=1ms、wheelSize=20、interval=20ms で、各レベルの WheelSize も 20 に固定されているため、各レベルの TickMs と intervalレベルもそれに応じて計算できます。Kafka には、TimingWheel を実装する際の細かい詳細がいくつかあります。

TimingWheel が作成されるとき、現在のシステム時刻は、第 1 レベルのタイム ホイールの開始時刻 (startMs) として使用されます。ここでの現在のシステム時刻は、単純に System.currentTimeMillis () を呼び出すのではなく、Time.SYSTEM.hiResClockMs を呼び出します。 currentTimeMillis() メソッドの時間精度はオペレーティング システムの特定の実装に依存するため、一部のオペレーティング システムではミリ秒レベルの精度を達成できず、Time.SYSTEM.hiResClockMs は基本的に System.nanoTime()/1_000_000 を使用して精度を変換します。ミリ秒レベルまで。ミリ秒の精度を達成できる操作は他にもいくつかありますが、著者はそれをお勧めしません。System.nanoTime()/1_000_000 が最も効果的な方法です。(これについてアイデアがある場合は、メッセージ エリアで議論してください。)
TimingWheel の各双方向循環リンク リスト TimerTaskList にはセンチネル ノード (センチネル) があり、センチネル ノードの導入により境界条件を簡素化できます。センチネル ノードはダミー ノードとも呼ばれます。追加のリンク リスト ノードです。このノードは最初のノードです。値フィールドには何も格納されませんが、操作の便宜のために導入されています。リンク リストにセンチネル ノードがある場合、線形リストの最初の要素がリンク リストの 2 番目のノードになる必要があります。
タイム ホイールの最初の層を除いて、他の高レベル タイム ホイールの開始時刻 (startMs) は、この層のタイム ホイールが作成されたときに前の最初のラウンドの currentTime に設定されます。各レイヤーの currentTime は、tickM の整数倍である必要があります。満たされていない場合は、タイム ホイールのタイム グリッドの有効期限の時間範囲に対応するように、currentTime が TickM の整数倍にトリミングされます。トリミング方法は次のとおりです: currentTime = startMs - (startMs %ticMs)。currentTime は時間の経過とともに推奨されますが、tickM の整数倍であるという確立された事実は変わりません。ある瞬間の時刻をtimeMsとすると、この時のタイムホイールのcurrentTime=timeMs-(timeMs%ticMs)となり、時間が進むごとに各レベルのタイムホイールのcurrentTimeもこれに応じて進みます。方式。
Kafka のタイマーは、TimingWheel の最初のレベルのタイム ホイールの参照を保持するだけでよく、他の高レベルのタイム ホイールを直接保持することはありませんが、各タイム ホイールには、より高いレベルを指す参照 (overflowWheel) があります。このレベルで呼び出される , は、タイマーが各レベルのタイム ホイールの基準を間接的に保持していることを実現できます。
タイム ホイールの詳細についてはここで説明します。各コンポーネントでのタイム ホイールの実装は同様です。ここの読者は、この記事で説明されているシナリオ、つまり「時間が経つにつれて」または「時間が経つにつれて」に興味を持つでしょう。では、カフカでは時間はどのように進むのでしょうか? JDKでscheduleAtFixedRateを使用してタイムホイールを1秒ごとに進めるのと似ていますか? これは明らかに不合理であり、TimingWheel はその意味のほとんどを失っています。

Kafka のタイマーは、JDK の DelayQueue を使用してタイム ホイールを進めます。具体的な方法は、使用される各 TimerTaskList が DelayQueue に追加されることです。「使用される各 TimerTaskList」とは、特に非セントリー ノード TimerTaskEntry を持つ TimerTaskList を指します。DelayQueue は、TimerTaskList に対応するタイムアウト期限に従ってソートされ、期限が最も短い TimerTaskList が DelayQueue の先頭にランク付けされます。Kafka には、DelayQueue の期限切れタスクのリストを取得するためのスレッドが存在します。興味深いのは、このスレッドに対応する名前が "ExpiredOperationReaper" と呼ばれていることです。これは、直訳すると "期限切れのオペレーション ハーベスタ" と、 「SkimpyOffsetMap」。戦います。「ハーベスター」スレッドは、DelayQueue 内のタイムアウトしたタスク リスト TimerTaskList を取得した後、TimerTaskList の有効期限に従ってタイム ホイールの時間を進めるか、取得した TimerTaskList に対して対応する操作を実行できます。期限切れ操作 期限切れ操作が実行され、ダウングレードされたタイム ホイールがダウングレードされます。

記事の冒頭で DelayQueue は Kafka のような高パフォーマンスのタイミング タスクには適さないと明記されていますが、なぜここで DelayQueue が紹介されているのでしょうか? タイミング タスク項目の TimerTaskEntry の挿入および削除操作については、TimingWheel の時間計算量は O(1) であり、パフォーマンスは DelayQueue よりもはるかに高いことに注意してください。TimerTaskEntry を DelayQueue に直接挿入すると、パフォーマンスをサポートするのは明らかに困難です。 。いくつかの TimerTaskEntry を特定のルールに従って TimerTaskList グループに分割し、その TimerTaskList を DelayQueue に挿入したとしても、この TimerTaskList に別の TimerTaskEntry を追加する場合にどう対処するかを想像してみてください。DelayQueue の場合、そのような操作は明らかに無力になります。

分析から、 Kafka の TimingWheel は TimerTaskEntry の挿入と削除の操作を実行するために特別に使用され、DelayQueue は時間の進行のタスクを特別に担当していることがわかりますDelayQueue の最初のタイムアウト タスク リストの有効期限が 200 ミリ秒で、2 番目のタイムアウト タスクが 840 ミリ秒であることをもう一度想像してください。ここで、DelayQueue の先頭の取得に必要な時間計算量は O(1) だけです。タイミング プッシュが 1 秒ごとに使用される場合、最初の超過タスク リストを取得するときに実行される 200 プッシュのうち 199 は「空のプッシュ」であり、2 番目のタイムアウト タスクを取得するときに 639 の「空のプッシュ」を実行する必要があります。マシンのパフォーマンス リソースを理由もなく消費します。ここでは、DelayQueue を使用して、少量のスペースを時間と交換し、「正確な進歩」を実現します。Kafka のタイマーは「人を知り、それを上手に使う」と表現できます。TimingWheel を使用して最適なタスクの追加および削除操作を実行し、DelayQueue を使用して最適な時間進行作業を実行すると、相互に補完します。

 

インタビューの質問は大まかに次のようなものです:消費者はニュースを得るために Kafka にアクセスしますが、Kafka には提供する新しいニュースがないので、Kafka はそれをどのように処理しますか?

以下の図に示すように、2 つのフォロワー コピーがリーダー コピーの最新の位置にプルされており、この時点でリーダー コピーにプル リクエストが送信されますが、リーダー コピーには新しいメッセージが書き込まれていないため、どうすればよいですかこのときリーダーコピーは毛糸でしょうか?空のプル結果をフォロワー コピーに直接返すことはできますが、リーダー コピーに新しいメッセージが書き込まれていない場合、フォロワー コピーは常にプル リクエストを送信し、常に空のプル結果を受信することになり、リソースが無駄に消費されます。

ここに画像の説明を挿入
これには、Kafka の遅延操作の概念が含まれます。Kafka がプル リクエストを処理するとき、最初にログ ファイルを 1 回読み取ります。十分なメッセージ (fetchMinBytes、パラメータ fetch.min.bytes で構成され、デフォルト値は 1) を収集できない場合は、遅延プル フェッチ操作が作成されます。 (DelayedFetch) 十分な数のメッセージがフェッチされるまで待機します。遅延プル操作が実行されると、ログ ファイルが再度読み取られ、プル結果が後続のコピーに返されます。

遅延操作は、メッセージをプルするときの固有の操作だけではなく、データ削除の遅延や生成の遅延など、Kafka には多くの遅延操作があります。

遅延プロダクション (メッセージ) の場合、プロデューサー クライアントを使用してメッセージを送信するときに acks パラメーターが -1 に設定されている場合、メッセージが正しく送信される前に、ISR セット内のすべてのレプリカがメッセージの受信を確認するまで待機する必要があることを意味します。応答を受信した結果、またはタイムアウト例外をキャッチした結果。

ここに画像の説明を挿入

パーティションにリーダー、フォロワー 1、フォロワー 2 の 3 つのコピーがあり、これらはすべてパーティションの ISR セット内にあるとします。説明を簡単にするために、ここでは ISR セットの拡張と縮小を考慮しません。Kafka はクライアントのプロダクション リクエストを受信すると、上の図に示すように、メッセージ 3 と 4 をリーダー コピーのローカル ログ ファイルに書き込みます。

クライアントは acks を -1 に設定しているため、follower1 と follower2 の両方のコピーがメッセージ 3 とメッセージ 4 を受信して​​、送信されたメッセージが正しく受信されたことをクライアントに通知するまで待つ必要があります。一定の期間内に、follower1 のコピーまたは follower2 のコピーがメッセージ 3 とメッセージ 4 を完全にプルできない場合は、タイムアウト例外をクライアントに返す必要があります。実稼働リクエストのタイムアウト期間はパラメータ request.timeout.ms で構成され、デフォルト値は 30000 (30 秒) です。

ここに画像の説明を挿入

ここに画像の説明を挿入

では、メッセージ 3 とメッセージ 4 がフォロワー 1 のコピーとフォロワー 2 のコピーに書き込まれるのを待って、対応する応答結果をクライアントに返すというアクションは誰が実行するのでしょうか。リーダー コピーのローカル ログ ファイルにメッセージを書き込んだ後、Kafka は遅延実稼働操作 (DelayedProduce) を作成して、すべてのコピーへのメッセージの通常の書き込みまたはタイムアウトを処理し、対応する応答結果をクライアントに返します。

遅延操作は、応答結果の返信を遅らせる必要があります。まず、タイムアウト期間 (layMs) を持たせる必要があります。このタイムアウト期間内に所定のタスクが完了しない場合は、強制的に完了して応答を返す必要があります。結果はクライアントに。第二に、遅延操作はタイミング操作とは異なります。タイミング操作は特定の時間後に実行される操作を指しますが、遅延操作は設定されたタイムアウト時間よりも前に完了できるため、遅延操作は外部イベントのトリガーをサポートできます。

実稼働操作が遅延した場合、その外部イベントは、メッセージが書き込まれるパーティションの HW (最高水準点) の増加です。つまり、フォロワー コピーはリーダー コピーと同期をとり続け、HW の成長をさらに促進します。HW が成長するたびに、遅延した本番操作が完了できるかどうかを確認し、完了できれば実行します。応答結果を顧客端末に返しますが、それでもタイムアウト期間内に完了しない場合は強制的に実行されます。

記事冒頭の遅延プル操作を振り返ってみると同様で、タイムアウトトリガーや外部イベントトリガーによっても実行されます。タイムアウトのトリガーは理解しやすいです。つまり、タイムアウトが経過するまで待機して、2 回目のログ ファイルの読み取り操作をトリガーします。プル リクエストはフォロワー コピーだけでなくコンシューマ クライアントによっても開始されるため、外部イベントのトリガーは少し複雑になり、この 2 つのケースに対応する外部イベントも異なります。フォロワー コピーの遅延プルの場合、その外部イベントは、メッセージがリーダー コピーのローカル ログ ファイルに追加されることです。コンシューマー クライアントの遅延プルの場合、その外部イベントは単に次のように理解できます。 HWの成長。
 

おすすめ

転載: blog.csdn.net/qq_35240226/article/details/106474749