みなさん、こんにちは。
RocketMQやKafkaのメッセージがディスクに保存されることは周知の事実ですが、なぜディスク上のメッセージの読み書きが速いのでしょうか?何か最適化があるのでしょうか?RocketMQとKafkaの2つの実装は、どちらもメッセージをディスクに保存しますが、何か違いはありますか?それぞれの長所と短所を教えてください。
今すぐご確認ください。
ストレージメディア - ディスク
一般的に、メッセージミドルウェアのメッセージはローカルファイルに格納されます。効率の観点から、ローカルファイルに直接格納するのが最も高速で安定しているからです。結局のところ、データベースや他のサードパーティのストレージを置くと、それはセキュリティというより依存であり、ネットワークのオーバーヘッドがあります。
では、メッセージをディスク・ファイルに保存する際のプロセスのボトルネックは、ディスクの書き込みと読み出しです。ディスクは読み書きに比較的時間がかかるということを知った上で、ディスクを記憶媒体として使用することで高いスループットを達成するにはどうすればよいでしょうか?
シーケンシャルリード/ライト
答えは逐次読み書きです。
まず、ページキャッシュについて理解しましょう。ページキャッシュは、オペレーティングシステムがディスクのI/O操作を減らすために、ディスクの一種のキャッシュとして使用します。
ディスクに書き込むと、実際にはページキャッシュに書き込まれ、ディスクへの書き込みがメモリへの書き込みになります。書き込まれたページはダーティ・ページとなり、オペレーティング・システムは適切なタイミングでダーティ・ページをディスクに書き込みます。
読み込みの際、ページキャッシュがヒットすればそのまま戻り、ページキャッシュがミスすればページミス割り込みを発生させ、ディスクからページキャッシュにデータをロードしてからデータを返します。
と読み取り時間では、ページキャッシュに隣接するディスクブロックを読み取る読み取るときにローカリティの原則に従って、事前に読み取られます。書き込み時間では、書き込み後に書き込まれ、書き込みもページキャッシュですので、格納されているいくつかの小さな書き込み操作を大規模な書き込みに組み合わせることができますし、ディスクをスワイプします。
また、ディスクの構造によっては、シーケンシャルI/O時にヘッドがトラックを変更する必要がほとんどないか、トラックを変更する時間が非常に短くなります。
インターネット上のいくつかのテスト結果によると、ディスクのシーケンシャル書き込みは、メモリのランダム書き込みよりも高速です。
もちろん、この種の書き込みにはデータ損失のリスクがあります。たとえば、マシンが突然電源を失った場合、まだフラッシュされていないダーティページが失われます。しかし、fsyncを呼び出して強制的にディスクをフラッシュすることは可能ですが、これは大きなパフォーマンス・ロスです。
したがって、一般的には、ディスクを同期的にスワイプするのではなく、マルチコピーメカニズムによってメッセージの信頼性を確保することが推奨されます。
シーケンシャルI/Oはディスクの構造に適応し、プリリードとポストライトもあることがわかります。 RocketMQとKafkaは、どちらもシーケンシャルな書き込みとシーケンシャルに近い読み込みを行います。どちらもメッセージの書き込みにファイルの追記を使用し、ログファイルの最後にしか新しいメッセージを書き込めません。
mmap-ファイルメモリーマッピング
上記から、ディスク・ファイルにアクセスするとページ・キャッシュにデータがロードされることがわかりますが、ページ・キャッシュはカーネル空間に属しており、ユーザー空間からアクセスすることはできないため、データをユーザー空間のバッファにコピーする必要があります。
データにアクセスするためには、ページキャッシュから別のコピープロセスを経る必要があることがわかります。そのため、mmapを使用して最適化を行い、メモリマップファイルを使用してコピーを回避することもできます。
簡単に言えば、ファイルのマッピングは、直接ページキャッシュにプログラムの仮想ページをマップすることですので、カーネルの状態を持っている必要はありませんし、ユーザーの状態にコピーするだけでなく、重複したデータの生成を避けるために。また、ファイルを読み込んだり、書き込むには、読み取りまたは書き込みメソッドを呼び出す必要はありません、あなたは直接操作する方法のマッピングされたアドレスとオフセットを介して行うことができます。
sendfile-
メッセージはディスク上にあるので、コンシューマはメッセージを取り出そうとするとき、ディスクから取り出さなければなりません。まず、ファイル送信の一般的な処理の仕組みを見てみましょう。
DMAとは何かと簡単に言うと、正式名称Direct Memory Access(ダイレクトメモリアクセス)で、グラフィックカードやネットワークカードなどがDMAを使用するように、CPUの介入なしに、独立してシステムメモリを直接読み書きすることができます。
データが実際には冗長であることがわかります。では、mmapの後にファイルを送信するプロセスがどのように機能するか見てみましょう。
コンテキスト・スイッチの回数は変わりませんが、データのコピーが1つ減っていることがわかります。
しかし、データはまだ冗長なので、ページキャッシュからネットワークカードに直接データをコピーすることが可能です。Linux 2.1のsendfileを見てみましょう。
送信の必要性は1つのシステムコールで満たされるため、read + writeやmmap + writeに比べてコンテキストスイッチは確かに少なくなりますが、データにはまだ冗長性があるように思えます。そう、だからLinux 2.4バージョンのsendfile + DMAの "spread-and-gather "は本当に冗長性がないのです。
FileChannal.transferTo()
これはしばしばゼロコピーと呼ばれ、Javaではsendfileがその基礎となります。
上記のポイントがRocketMQとKafkaにどのように当てはまるか見てみましょう。
RocketMQ およびKafkaアプリケーション
RocketMQ
つまり、コミットログファイルには、メッセージがどのトピックのどのキューに属しているかに関係なく、このブローカーに割り当てられたすべてのメッセージが含まれます。
そして、コンシューマーは CosumerQueue からメッセージの実際の物理アドレスを取得し、CommitLogにアクセスしてメッセージを取得します。CosumerQueue はメッセージのインデックスと考えることができます。
RocketMQでは、CommitLogもCosumerQueueもmmapを使用します。
メッセージを送信するときにデフォルトで使われるのは、データをヒープ・メモリーにコピーしてから送信する方法です。コードを見てください。
コンシューマがメッセージを取り出すときのコードを見てください。
ご覧のように、RocketMQはデフォルトでメッセージをヒープバッファにコピーし、それをレスポンスボディに詰め込んで送信します。しかし、ヒープを経由せず、真のゼロコピーを使用する代わりに、メッセージを mapedBuffer 経由で SocketBuffer に送信するように設定することもできます。
そのため、RocketMQは逐次書き込み、mmap、sendfileなし、SocketBufferへのページキャッシュのコピーを使用します。
CommitLogのメッセージは混在して保存されているため、CommitLogからメッセージを読み出すときは厳密にランダムです。**一般的に、メッセージは保存されるとすぐに消費されるので、この時点ではまだページキャッシュにあるはずで、ディスクを読む必要はありません。
そして、前述のように、ページキャッシュは定期的にフラッシュされます。これは制御不可能であり、メモリは限られています。
そのため、RocketMQには、ファイルの事前割り当てとファイルのウォーミングという最適化があります。
ファイルの事前割り当て
AllocateMappedFileService
RocketMQではバックグラウンドスレッドでAllocateRequestを常時処理していますが、実はこのAllocateRequestはメッセージの書き込み中にジッターが発生しないように、次のファイルをあらかじめ用意しておく事前確保リクエストです。メッセージの書き込み中に次のファイルが確保されてジッターが発生するのを防ぐためです。
ファイルウォーミング
madvise(MADV_WILLNEED)
warmMappedFileメソッドは、現在マップされているファイルを受け取り、各ページを複数回トラバースし、0バイトを書き込み、mlockと.NETを呼び出します。
madvise(MADV_WILLNEED)
もう一度this.mlockを見てみると、内部的には実際にmlockを呼び出し、.
mlock:物理メモリでプロセスが使用するアドレス空間の一部または全部をロックし、スワップ空間にスワップされるのを防ぐことができます。
madvise:OSに、このファイルは近い将来アクセスされるから、数ページ先を読んでおくといいよ、というアドバイスを与えてください。
RocketMQ
ディスクの逐次書き込みは、全体として逐次読み出しであり、真のゼロコピーではなく、mmapを使用します。 madvise(MADV_WILLNEED)
ページ・キャッシュの不確実性とmmapの不活性ロードのため、ファイルの事前割り当てとファイル・ウォーミングが使用されます。つまり、各ページに0バイトが書き込まれ、その後mlockとmlockが呼び出されます。
Kafka
KafkaのログストレージはRocketMQと異なり、パーティションごとに1ファイルです。
Kafkaのメッセージ書き込みも、1つのパーティションに対してはシーケンシャル、パーティション数が少なければ全体的にシーケンシャルで、ログファイルにはmmapを使わず、インデックスファイルにはmmapを使いますが、Kafkaはメッセージ送信にゼロコピーを使います。
メッセージの書き込みには mmap はあまり役に立ちません。メッセージを送信する場合は、mmap+write よりも sendfile の方が効率的です。
FileChannel.transferTo
Kafkaのメッセージ送信のソースコードを見てみましょう。最後の呼び出しはtoで、一番下の行はsendfileです。
Kafkaのソースコードを見る限り、RocketMQのmlockなどに似た操作は見当たりません。そもそもログがmmapを使っていないのが原因だと思いますし、スワップはLinuxのシステムパラメータvm.swappinessで調整できるようになっています。
メモリが本当に少ないと仮定すると、0に設定すると、メモリが不足してスワップできなくなった場合、プロセスが突然中断します。1に設定すると、少なくとも少しの間プロセスを停止させることができます。
RocketMQ & Kafka
まず、どちらもシーケンシャルな書き込みですが、RocketMQはメッセージを1つのファイルに保存するのに対し、Kafkaはパーティションごとに1つのファイルに保存します。
マイグレーションやデータレプリケーションのレベルでは、パーティションごとに1ファイルの方が柔軟です。
しかし、パーティションが増えると、書き込みのために複数のファイルを頻繁に行き来する必要があり、各ファイルではシーケンシャル書き込みですが、グローバルではランダム書き込みとしてカウントされ、読み込みでも同様にランダム読み込みとしてカウントされます。RocketMQでは1つのファイルに対してこのような問題は発生しません。
メッセージの送信に関しては、RocketMQはmmap + writeとpreheatsを使用して、ページ割り込みの欠落による大きなmmapファイルに関連するパフォーマンスの問題を軽減しています。一方、Kafkaはsendfileを使用し、SocketBufferにコピーするページキャッシュが少ないため、kafkaの方が効率的に送信できると思います。
また、スワップ問題はシステム・パラメーターで設定することもできます。
最後に
この記事の途中でRocketMQについて書くのに行き詰まりました。 まだソースコードに慣れていないので、少し混乱しています。 丁維さんのアドバイスのおかげです。そうでなければ行き詰まるところでした。
最後に、Ding WeiとZhou Jifengの "RocketMQ Technology Insider: RocketMQ Architecture Design and Implementation Principles "をお勧めします。RocketMQに興味のある方はぜひご覧ください。
この記事を読んで、もしどこかに間違いがあればご連絡ください!
私はYES、少しから億の点へ、また次の記事でお会いしましょう。