DUICUO

Kafka が高速である6つの理由

[[335450]]

この記事はWeChat公式アカウント「JavaKeeper」からの転載であり、著者の発言は正確です。転載の許可については、JavaKeeper公式アカウントにお問い合わせください。

Kafka は、メッセージキュー (MQ) として使用する場合でも、ストレージ層として使用する場合でも、本質的には 2 つの機能を持ちます (非常にシンプルに見えますが)。1 つ目は、プロデューサーによって生成されたデータがブローカーに保存されることです。2 つ目は、コンシューマーがブローカーからデータを読み取ることです。Kafka の速度は、読み取りと書き込みの操作に反映されています。Kafka がなぜこれほど高速なのか、その理由を説明しましょう。

1. パーティションを使用して並列処理を実装します。

Kafka は Pub-Sub メッセージング システムであり、公開とサブスクライブの両方でトピックを指定する必要があることは周知の事実です。

トピックは単なる論理的な概念です。各トピックには1つ以上のパーティションが含まれており、異なるパーティションは異なるノードに存在する場合があります。

異なるパーティションを異なるマシンに配置できるため、クラスタリングの利点を最大限に活用し、マシン間の並列処理を実現できます。また、各パーティションは物理的にフォルダに対応しているため、複数のパーティションが同一ノード上に存在していても、同一ノード上の異なるパーティションを異なるディスクに配置するように構成できます。これにより、ディスク間の並列処理が実現され、複数ディスクの利点を最大限に活用できます。

並列処理により速度は確実に向上します。複数のワーカーは 1 つのワーカーよりも確実に高速に動作します。

異なるディスクにデータを並行して書き込むことはできますか?ディスクの読み取り/書き込み速度を制御できますか?

まず、ディスク/IO について簡単に説明しましょう。

ハードドライブのパフォーマンスを制限する要因は何でしょうか?ディスクI/O特性に基づいてシステムを設計するにはどうすればよいでしょうか?ハードドライブの主な内部コンポーネントは、ディスクプラッタ、ドライブアーム、読み取り/書き込みヘッド、そしてスピンドルモーターです。実際のデータはプラッタに書き込まれ、読み取りと書き込みは主にドライブアーム上の読み取り/書き込みヘッドによって行われます。動作中、スピンドルはディスクプラッタを回転させ、ドライブアームが伸長することで読み取り/書き込みヘッドがプラッタ上で読み取り/書き込み操作を実行できるようになります。ハードドライブの物理構造は次の図に示されています。

1枚のプラッタの容量には限りがあるため、ハードドライブは通常2枚以上のプラッタを備えています。各プラッタには2つの面があり、どちらも情報を記録できるため、1枚のプラッタは2つの読み取り/書き込みヘッドに対応します。プラッタは多数のセクター状の領域に分割されており、それぞれがセクターと呼ばれます。プラッタ表面上で、プラッタの中心を中心とする半径の異なる同心円はトラックと呼ばれ、異なるプラッタ上の同じ半径のトラックによって形成されるシリンダはシリンダと呼ばれます。トラックとシリンダはどちらも半径の異なる円を表しており、多くの場合、トラックとシリンダは同じ意味で使用されます。ハードディスクのプラッタを垂直から見た図を次の図に示します。

画像ソース: commons.wikimedia.org ディスクに影響を与える主な要因はディスクサービス時間、つまりディスクがI/O要求を完了するのにかかる時間です。これは、シーク時間、回転待ち時間、データ転送時間の3つの部分で構成されます。機械式ハードドライブは、シーケンシャル読み取りおよび書き込みのパフォーマンスは優れていますが、ランダム読み取りおよび書き込みのパフォーマンスは劣っています。これは主に、読み取り/書き込みヘッドが正しいトラックに移動するのに時間がかかるためです。ランダム読み取りおよび書き込み中は、読み取り/書き込みヘッドを常に移動する必要があり、ヘッドのシークに時間が浪費されるため、パフォーマンスは高くありません。ディスクを測定するための主な重要な指標は、IOPSとスループットです。KafkaやHBaseなどの多くのオープンソースフレームワークでは、ランダムI/Oは、追加書き込みを通じて可能な限りシーケンシャルI/Oに変換され、シーク時間と回転待ち時間が短縮され、IOPSが最大化されます。興味のある学生は、ディスクI/Oについて読むことができます[1]。

ディスクの読み取りと書き込みの速度は、ディスクの使用方法、つまり、順次読み取りと書き込みかランダム読み取りと書き込みかによって異なります。

2. シーケンシャルディスク書き込み

画像ソース: kafka.apache.org

Kafka では、各パーティションは順序付けられた不変のメッセージシーケンスです。新しいメッセージはパーティションの末尾に継続的に追加されます。これをシーケンシャル書き込みと呼びます。

「ずっと昔、誰かがベンチマークテストをしました: 『1秒あたり200万回の書き込み (3台の安価なマシンで)』 http://ifeve.com/benchmarking-apache-kafka-2-million-writes-second-three-cheap-machines/」

ディスク容量の制限により、すべてのデータを保存することは不可能です。実際、メッセージングシステムであるKafkaは、すべてのデータを保存する必要はなく、古いデータは削除する必要があります。さらに、Kafkaはシーケンシャルな書き込み操作を行うため、様々な削除戦略を用いてデータを削除する際に、読み書きパターンを用いてファイルを変更することはありません。その代わりに、パーティションを複数のセグメント(それぞれが物理ファイルに対応)に分割し、パーティション内のファイル全体を削除します。この古いデータの消去方法により、ファイルへのランダムな書き込み操作を回避できます。

3. ページキャッシュを最大限に活用する

キャッシュ層を導入する目的は、Linuxオペレーティングシステムにおけるディスクアクセスのパフォーマンスを向上させることです。キャッシュ層は、ディスク上の一部のデータをメモリにキャッシュします。データ要求が到着した際、データがキャッシュ内に存在し、かつ最新であれば、データはユーザープログラムに直接渡されます。これにより、基盤となるディスクに対する操作が不要になり、パフォーマンスが向上します。また、キャッシュ層は、ディスクIOPSが200を超える主な理由の一つでもあります。Linux実装では、ファイルキャッシュはページキャッシュとバッファキャッシュの2つの層に分かれています。各ページキャッシュには複数のバッファキャッシュが含まれています。ページキャッシュは主にファイルシステム上のファイルデータのキャッシュとして使用され、特にプロセスがファイルの読み取り/書き込み操作を実行する際に使用されます。バッファキャッシュは主に、システムがブロックデバイスへの読み取り/書き込みを行う際にブロックデータをキャッシュするシステム向けに設計されています。

ページキャッシュを使用する利点:

  • I/O スケジューラは、連続する小さな書き込みブロックをより大きな物理書き込みブロックに組み立てることで、パフォーマンスを向上させます。
  • I/O スケジューラは一部の書き込み操作の順序を変更し、ディスク ヘッドの移動時間を短縮します。
  • 空きメモリ(JVM以外のメモリ)を最大限に活用してください。アプリケーションレベルのキャッシュ(JVMヒープメモリなど)を使用すると、GCの負荷が増加します。
  • 読み取り操作はページキャッシュ内で直接実行できます。消費速度と生成速度が同程度であれば、物理ディスクを介してデータを交換する必要がない場合もあります(ページキャッシュを介して直接交換できます)。
  • プロセスが再起動すると、JVM の内部キャッシュは無効になりますが、ページ キャッシュは引き続き利用できます。

ブローカーはデータを受信すると、ディスクへの書き込み時にページキャッシュにのみデータを書き込みますが、データが完全にディスクに書き込まれることは保証されません。この観点から、マシンがクラッシュした場合、ページキャッシュ内のデータがディスクに書き込まれず、データ損失が発生する可能性があります。ただし、この種の損失は、停電などオペレーティングシステムが動作を停止した場合にのみ発生し、これらのシナリオはKafkaのレプリケーションメカニズムによって完全に処理できます。このような状況でデータ損失を確実に防止するためにページキャッシュからディスクへのデータのフラッシュを強制すると、実際にはパフォーマンスが低下します。そのため、Kafkaにはページキャッシュからディスクへのデータのフラッシュを強制するための`flush.messages`および`flush.ms`パラメータが用意されていますが、Kafkaではこれらのパラメータの使用は推奨されていません。

4. ゼロコピー技術

Kafka では、大量のネットワークデータをディスクに永続化(プロデューサーからブローカーへ)し、ネットワーク経由でディスクファイルを転送(ブローカーからコンシューマーへ)します。このプロセスのパフォーマンスは、Kafka 全体のスループットに直接影響します。

オペレーティングシステムの中核はカーネルであり、通常のアプリケーションから独立しています。カーネルは保護されたメモリ空間にアクセスでき、また、基盤となるハードウェアデバイスにアクセスする権限も持っています。ユーザープロセスによるカーネルの直接操作を防ぎ、カーネルのセキュリティを確保するために、オペレーティングシステムは仮想メモリをカーネル空間とユーザー空間の2つの部分に分割します。

従来のLinuxシステムでは、標準I/Oインターフェース(読み取りや書き込みなど)はデータコピー操作に基づいています。つまり、I/O操作はカーネルアドレス空間のバッファとユーザーアドレス空間のバッファ間でのデータコピーを伴います。そのため、標準I/OはキャッシュI/Oとも呼ばれます。このアプローチの利点は、要求されたデータが既にカーネルのキャッシュに格納されている場合、実際のI/O操作が削減されることです。しかし、欠点は、データのコピー処理によってCPUオーバーヘッドが発生することです。

Kafkaの生成と消費を次の2つのプロセスに簡略化します[2]。

  • ネットワーク データはディスクに保存されます (プロデューサーからブローカーへ)。
  • ディスク ファイルはネットワーク経由で送信されます (ブローカーからコンシューマーへ)。

4.1 ネットワークデータをディスクに保存する(プロデューサーからブローカーへ)

従来のモデルでは、ネットワークからファイルにデータを転送するには、4 つのデータ コピー、4 つのコンテキスト スイッチ、および 2 つのシステム コールが必要です。

  1. data = socket.read ( ) // ネットワークデータを読み取る
  2. ファイル file = new File()
  3. file.write(data) // ディスクに保存
  4. ファイル.フラッシュ()

このプロセスには、実際には 4 つのデータのコピーが含まれます。

  1. まず、DMA コピーを使用してネットワーク データがカーネル モード ソケット バッファーにコピーされます。
  2. 次に、アプリケーションはカーネル モード バッファーからユーザー モード (CPU コピー) にデータを読み取ります。
  3. 次に、ユーザー プログラムはユーザー モード バッファーをカーネル モードにコピーします (CPU コピー)。
  4. 最後に、DMA コピーを使用してデータをディスク ファイルにコピーします。

DMA(ダイレクトメモリアクセス)は、CPUの介入なしに周辺機器とシステムメモリ間の双方向データ転送を可能にするハードウェアメカニズムです。DMAを使用すると、システムCPUは実際のI/Oデータ転送から解放され、システムスループットが大幅に向上します。

同時に、下の図に示すように 4 つのコンテキスト スイッチが行われます。

ディスクへのデータの永続化は通常リアルタイムではなく、これはKafkaプロデューサーのデータの永続化にも当てはまります。Kafkaデータはリアルタイムでディスクに書き込まれるのではなく、最新のオペレーティングシステムのページングストレージを最大限に活用して、メモリ(前のセクションで説明したページキャッシュ)を使用したI/O効率を向上させます。

Kafka の場合、プロデューサーが生成したデータをブローカーに保存するプロセス(ソケットバッファからのネットワークデータの読み取りを含む)は、実際にはカーネル空間で直接完了できます。ソケットバッファからアプリケーションプロセスバッファにネットワークデータを読み込む必要はありません。この場合、アプリケーションプロセスバッファが実質的にブローカーであり、ブローカーはプロデューサーからデータを受信して​​永続化します。

この特定のシナリオでは、ソケット バッファーからネットワーク データを受信し、アプリケーション プロセスが中間処理なしでそれを直接保存する必要がある場合、mmap メモリ ファイル マッピングを使用できます。

メモリマップファイル(mmap、MMFileとも呼ばれる)は、カーネル内の読み取りバッファのアドレスをユーザー空間バッファにマッピングするために使用されます。これにより、カーネルバッファとアプリケーションバッファ間でメモリを共有できるようになり、カーネル読み取りバッファからユーザーバッファへのデータのコピーが不要になります。これは、オペレーティングシステムのページを直接使用してファイルを物理メモリにマッピングすることで機能します。マッピング後、物理メモリ上の操作はハードドライブに同期されます。この方法は、ユーザー空間からカーネル空間へのデータコピーのオーバーヘッドを排除することで、I/Oパフォーマンスを大幅に向上させます。

mmap には重大な欠点があります。それは信頼性が低いことです。mmap に書き込まれたデータは実際にはディスクに書き込まれません。オペレーティングシステムは、プログラムが明示的に `flush` を呼び出したときにのみデータを書き込みます。Kafka は、明示的にフラッシュするかどうかを制御するためのパラメータ `producer.type` を提供しています。Kafka が mmap への書き込み直後にフラッシュしてから Producer に戻る場合、同期 (sync) と呼ばれます。一方、mmap への書き込み直後に `flush` を呼び出さずに Producer に戻る場合、非同期 (async) と呼ばれます。デフォルトは同期です。

ゼロコピー技術とは、コンピュータが演算を実行する際に、CPUがメモリ領域から別のメモリ領域にデータをコピーする必要がないため、コンテキストスイッチとCPUのコピー時間が短縮される技術です。その機能は、ネットワークデバイスからユーザープログラム空間へのデータパケットの送信中に発生するデータコピーとシステムコールの回数を削減し、CPUの関与をゼロにすることで、この点におけるCPUの負荷を完全に排除することです。現在、ゼロコピー技術には主に3つの種類があります[3]。

  • 直接 I/O: データは、ユーザー アドレス空間と I/O デバイス間でカーネルを介して直接転送されます。カーネルは、仮想メモリの構成などの必要な補助タスクのみを実行します。
  • カーネルとユーザー空間間のデータのコピーを回避する: アプリケーションがデータにアクセスする必要がない場合、カーネル空間からユーザー空間へのデータのコピーを回避できます。
    • mmap
    • 送信ファイル
    • スプライス&ティー
    • ソックマップ
  • コピーオンライト: この技術により、事前にデータをコピーするのではなく、変更が必要な場合にのみデータをコピーできます。

4.2 ディスクファイルはネットワーク経由で送信されます(ブローカーからコンシューマーへ)

従来の方法では、最初にディスクから読み取り、次にソケット経由で送信するため、基本的に 4 つのコピーが行われます。

  1. バッファ= File.read   
  2. ソケット.send(バッファ)

このプロセスは、上で説明したメッセージ生成プロセスと比較できます。

  1. まず、ファイル データはシステム コール (DMA コピー) を介してカーネル モード バッファーに読み込まれます。
  2. 次に、アプリケーションはメモリ レベルのバッファーからユーザー レベルのバッファー (CPU コピー) にデータを読み取ります。
  3. 次に、ユーザー プログラムがソケット経由でデータを送信すると、ユーザー モード バッファーからカーネル モード バッファーにデータがコピーされます (CPU コピー)。
  4. 最後に、データは DMA 経由で NIC バッファにコピーされます。

Linux 2.4以降のカーネルは、sendfileシステムコールを通じてゼロコピー機能を提供します。データはDMA経由でカーネルモードバッファにコピーされ、その後DMA経由でNICバッファに直接コピーされるため、CPUによるコピーは不要になります。これが「ゼロコピー」という用語の由来です。データのコピー回数が減るだけでなく、ファイル読み取りからネットワークへの送信プロセス全体が1回のsendfileコールで完了し、コンテキストスイッチは2回のみで済むため、パフォーマンスが大幅に向上します。

Kafka のアプローチは、NIO の transferTo/transferFrom メソッドを使用してオペレーティングシステムの sendfile を呼び出すことでゼロコピーを実現することです。これにより、カーネルデータのコピーが合計 2 回、コンテキストスイッチが 2 回、システムコールが 1 回発生し、CPU によるデータコピーが不要になります。

5. バッチ処理

多くの場合、システムのボトルネックとなるのは CPU やディスクではなく、ネットワーク I/O です。

そのため、オペレーティングシステムが提供する低レベルのバッチ処理に加えて、Kafkaクライアントとブローカーは、ネットワーク経由でデータを送信する前に、複数のレコード(読み取りと書き込みを含む)をバッチ処理で蓄積します。レコードのバッチ処理により、ネットワークのラウンドトリップのオーバーヘッドが軽減され、より大きなデータパケットが使用されるため、帯域幅の利用率が向上します。

6. データ圧縮

プロデューサーはブローカーに送信する前にデータを圧縮することで、ネットワーク転送コストを削減できます。現在サポートされている圧縮アルゴリズムには、Snappy、Gzip、LZ4などがあります。データ圧縮は通常、最適化手法としてバッチ処理と組み合わせて使用​​されます。

要約 | 次回、面接官に Kafka が高速な理由を尋ねられたら、このように答えます。

  • パーティション並列処理
  • ディスクの機能を最大限に活用するために、ディスクに順番に書き込みます。
  • 最新のオペレーティング システムのページ キャッシュを利用して、メモリの I/O 効率を向上させます。
  • ゼロコピー技術を採用しました。
    • プロデューサーによって生成されたデータは、高速な順次書き込みを実現するために mmap ファイル マッピングを使用してブローカーに保存されます。
    • 顧客はsendfileを使用してブローカーからデータを読み取ります。ディスクファイルをOSカーネルバッファに読み込んだ後、データはネットワーク転送用のNIOバッファに転送され、CPU消費を削減します。

参考文献

[1] Meituan – ディスクI/Oのすべて: https://tech.meituan.com/2017/05/19/about-desk-io.html

[2] Kafkaゼロコピー: https://zhuanlan.zhihu.com/p/78335525

[3] Linux - ゼロコピー: https://cllc.fun/2020/03/18/linux-zero-copy/