読者です 読者をやめる 読者になる 読者になる

夢とガラクタの集積場

落ちこぼれ三流エンジニアである管理人の夢想=『夢』と、潰えた夢=『ガラクタ』の集積場です。

Apache Kafkaってそもそも何か確認してみます(その4

1年位間があいてしまっていますが、ベースデザインの章の続きを読んでみました。
あと、個々の言葉を訳しても時間がかかるので、要点のみ抽出してとりあえず最後まで読み切ります。

尚、ページ自体は下記の場所に移動していました。
http://kafka.apache.org/07/design.html

6.メッセージの永続化/保持

一定の時間内に満たせるよう収める

メッセージング·システムのメタデータに使用される永続的なデータ構造は、多くの場合、Btreeである。
Btree構成は最も汎用性の高いデータ構造が利用可能であり、
メッセージングシステムにおけるトランザクションと非トランザクションの処理を幅広くサポートすることが可能。

但し、BtreeのアクセスにはO(logn)のコストがかかる。
通常であればO(logn)の時間は定数として扱われるが、ディスクを使用するというアクセス形態の場合これは真では無い。
ディスクからデータを取得するには10ミリ秒程の時間がかかり、
かつ1つのファイルをシークしている最中に別のファイルをシークすることはできない。
それを考えると、Btreeへのアクセスを行う場合それだけでディスクに高い負荷をかけることにつながる。

ストレージシステムは、物理ディスク操作とキャッシュ操作を混在しているため見える性能は大きく変動する。
さらにBtreeでは各操作にツリー全体をロックを避けるために非常に洗練されたページまたは行ロックの実装を必要とする。
実際に実現するには行ロックにかなり高いコストかけるか、または他の全ての読み取りを直列化する必要がある。

ディスクに依存して上記のロック構造を実現する場合ディスクの性能向上/密度向上を享受することができない。
細切れのアクセスが大量に発生するためである。

Kafkaの場合、ロギングソリューションと同様に永続的キューはシンプルなリード上に構築されており、後からデータを追加することが可能。
この構造はBtreeが保持するような豊富な機能を持たない代わりに、
ディスクにアクセスする際の速度をデータのサイズに依存しないO(1)にすることができ、お互いにブロックも発生させない。
O(1)にしたことで、性能を実データサイズと分離することができる。
結果、一般的なSATAの1TB+のディスク容量を最大限に活用することが可能。

=====
わかることとしては、Kafkaはメッセージングシステムで通常使用されるストレージ上のBtreeの構造を持たずに、
通常のロギングシステムと同じまとまったディスク上にキューのデータをシーケンシャルに出力することで
多彩な機能を犠牲にする代わりにディスクを用いた場合の性能を引き出している・・・ということですね。
後は、その上でどこまでの機能を保持するかを確認する必要がありそうです。
=====

7.効率を最大化

Kafka開発にあたっておいた仮定はメッセージの量が非常に高いということ。
また、全てのメッセージが1回、または複数回読まれると仮定している。
そのため、Kafkaでは「メッセージの消費」ではなく、「メッセージの生成」に最適化した構造を取る。

上記を実現するために立ちはだかる障害として、下記2点がある。

  1. 大量のネットワークIO
  2. 過度のメモリ上のバイトコピー

これらの問題を前提としたうえで効率化を図るため、
APIはGroup Messageに "MessageSet"として抽象化を加えて構築されている。
このAPI構成によって、複数のメッセージを同時にネットワーク転送することが可能で、
1メッセージごとに通信が発生するよりもネットワークのコストを抑えることができる。

尚、このMessageSetの実装においてはバイト配列またはファイルをラップした非常に薄いAPIを提供する。
メッセージのシリアライズ/デシリアライズは必要な場合のみ行われ、かつ遅延実行される。
(必要がない場合デシリアライズされないようにしている)

Brokerプロセスによって永続化されるメッセージログは単純にディスクに出力されたMessageSetのディレクトリのみ。
この単純な構造によって、BrokerプロセスとConsumerプロセスで同一のファイル形式を共有することが可能。
(また、Producerプロセスから受信したメッセージを検証した上で追記することが可能←?)

これらの永続化メッセージとネットワーク転送を共通フォーマット化することで各操作の最適化が可能。
UNIX系オペレーティング·システムではソケットにページキャッシュから
データを転送するための高度に最適化されたコードパスを提供している。
Linuxではsendfileシステムコールを使用して行われる。
JavaはFileChannel.transferTo APIでこのシステムコールを利用可能。
=====
つまりは、ファイル上に出力されているものとネットワーク転送するものの形式を合わせることで
最適化されたシステムコールを活用でき、それによって効率化が可能・・・ということのようです。
=====

sendfileを利用することによる利点を理解するためには、
ソケットからファイルへのデータの転送のための共通のデータパスを理解することが重要である。
通常のアプリケーションにおいてはファイル→ソケットへの転送は下記のフローで行われる。

  1. オペレーティングシステムは、カーネル空間でページキャッシュにディスクからデータを読み出す
  2. アプリケーションは、ユーザ空間バッファにカーネル空間からデータを読み出す
  3. アプリケーションは、ユーザ空間バッファからカーネル空間に対してデータを書き込み
  4. オペレーティングシステムカーネル空間中のソケットバッファからNICバッファにデータをコピーする

これは明らかに非効率的である。
4つのコピー、2つのシステムコールを挟んでしまうため。
sendfileを用いればOSが直接ネットワークにページキャッシュからデータを送信することができ、複数回のコピーが回避される。
この最適化されたパスに必要となるのはNICバッファに対するコピーのみとなる。

Kafkaは一般的な使用例は、トピック上に対して複数のConsumerがいると仮定している。
上記のコピー最適化を使用して、データは一度だけページキャッシュにコピーしている。
通常のアプリケーションのようにメモリに格納し、要求があるたびにカーネル空間にコピーがされることはない。
このことによって、メッセージがネットワーク転送速度の限界に近い速度で消費されることを可能にする。

Javaでsendfileとゼロコピーサポートの詳細背景については、以下の記事を参照。

https://www.ibm.com/developerworks/linux/library/j-zerocopy/

=====
ファイルフォーマットとネットワーク転送される際のフォーマットを同一にすることで
データのメモリ上のコピー回数を抑えて最適化されたデータ転送が可能になる・・と。
これでどれほど差が出るかわかりませんが、原理だけ聞くとかなり高速化しそうに見えますね。
=====