関口宏司のLuceneブログ

OSS検索ライブラリのLuceneおよびそのサブプロジェクト(Solr/Tika/Mahoutなど)について
<< 検索窓の設置@ロンウイットホームページ | main | Lucene 4.4 の FST を使って FSA を構築する >>
64ビットプラットフォームで絶対お勧めのLuceneのMMapDirectory (Uwe氏のブログより)
前々回にUweさんのブログを紹介したが、その際にUweさん本人より「MMapDirectoryの記事もお奨めだよ!」と背中を押されたので、ここに日本語訳を掲載する:
心配無用 - よくありがちな誤解を解く

Apache LuceneとSolrは、バージョン3.1では64ビット版WindowsおよびSolarisの両システム、バージョン3.3では64ビットLinuxシステムにおいてMMapDirectoryをデフォルトで採用している。ただし、システムがここ数バージョンとは異なる動作をするようになったため、今回の変更にLuceneとSolrのユーザが混乱している。LuceneとSolrのメーリングリストでは、消費する物理メモリが突然3倍になった理由に疑問を抱くユーザや、リソース使用量に不満を訴えるシステム管理者の投稿が多数見られる。また、MMapDirectoryの代わりに、動作の遅いSimpleFSDirectoryやNIOFSDirectory(JVMバグ#6265734に起因してWindowsではさらに遅い)を有効にするようsolrconfig.xmlの設定を推奨する意見がコンサルタントからも出始めている。慎重に検討した結果これらのプラットフォームではMMapDirectoryの使用がベストだと判断したLuceneコミッターからすれば、これはどちらかと言えば不快な反応になる。Lucene/Solrがこれまでより格段に優れたパフォーマンスを発揮することが分かっているからだ。今回の変更の背景に関して間違った情報が出回っているため、この素晴らしい検索エンジンが最適でない形で各所にインストールされてしまう。

このブログでは、カーネル内の仮想メモリ処理に関する基本的な事実と、それを主にLuceneのパフォーマンス向上に利用するための方法について説明しようと思う(「仮想メモリ超入門」である)。また、さまざまな人々が投稿するブログやメーリングリストが間違っていて、MMapDirectoryの用途に矛盾するという理由についても明らかにしていく。後半では、mmapエラーなどや、Javaのこなれないヒープアロケーションに起因した最適でないパフォーマンスの回避に必要な詳しいコンフィギュレーションと必要な設定を紹介する。

仮想メモリ[1]

まずOSのカーネルを考えてみたい。ソフトウェアでI/O処理を行うのは無知なアプローチだ。これは1970年代から行われており、そのパターンは単純なものだ。ディスク上のデータを処理するときは常にシステムカーネルでsyscallを実行し、ポインタを何らかのバッファ(例:Javaのbyte[]アレイ)に渡し、ディスクとの間でバイトをやりとりし、バッファの中身をパースしてプログラムを実行する。(処理能力を消費しすぎる可能性があることから)あまり多くのsyscallを行いたくない場合はソフトウェアでバッファを大きく取って内部でのディスクとのデータ同期を減らすようにする。(RAMDirectoryを使用するなどして)Luceneインデックス全体をJavaのヒープメモリに読み込むようアドバイスするのにはこのような理由もある。

しかし、Linux、Windows(NT以降)、MacOS X、あるいはSolarisといったモダンOSはすべて、洗練されたファイルシステムキャッシュとメモリ管理機能を使用することで1970年代式のコードより格段に優れたアプローチを用意している。「仮想メモリ」と呼ばれるこの機能は、Luceneインデックスのように巨大で広大な空間を占めるデータ構造処理の優れた代替手段となっている。仮想メモリはコンピュータアーキテクチャに不可欠な部分となっており、インプリメンテーションにはハードウェアサポートが必須で、MMU(メモリ管理ユニット)としてCPUに組み込まれているのが一般的だ。その仕組みはかなりシンプルで、各プロセスが独自に仮想アドレス空間を取得し、そこにライブラリ、ヒープ、そしてスタックの各空間をマッピングする。だいたいの場合、このアドレス空間はオフセット0から始まり、アドレスポインタの再配置が一切不要であるためプログラムコードの読み込みが簡単になっている。どのプロセスからも断片化していない連続した大きなアドレス空間が作業空間として見える。このアドレス空間は物理メモリと一切関連性がないことから「仮想メモリ」と呼ばれ、プロセスからもそのように見える。この広大なアドレス空間は、ソフトウェアを使って実メモリであるかのようにアクセスできる。ほかのプロセスもメモリを消費し、独自に仮想アドレス空間を持っていることは意識する必要がない。基盤OSはCPU内のMMUと連携し、最初のアクセス時にこれらの仮想アドレスを実メモリにマッピングする。これはページテーブルと呼ばれるもので行われ、それがMMUハードウェア内のTLB(頻繁にアクセスされるページをキャッシュする変換索引バッファ)によってバックアップされている。こうすることで、OSは実行中のすべてのプロセスの必要メモリを利用可能な実メモリに分散し、実行中のプログラムから完全に透過的に見えるようにすることができる。

仮想メモリ図(Wikipediaより引用[1]、http://en.wikipedia.org/wiki/File:Virtual_memory.svg。CC BY-SA 3.0許諾)
仮想メモリ図(Wikipediaより引用[1]、http://en.wikipedia.org/wiki/File:Virtual_memory.svg。CC BY-SA 3.0許諾)

この仮想化を利用すればOSはもう1つのことができるようになる。物理メモリが十分にないときは、プロセスが使わなくなったページを「スワップアウト」したり、物理メモリを別のプロセス用に解放したり、もっと重要なファイルシステム操作をキャッシングできるようになる。ページアウトされた仮想アドレスにプロセスがアクセスしようとすると、それがメインメモリに再ロードされ、プロセスが利用可能になる。プロセスは何もする必要がなく、完全に透過的になっている。これは利用可能なメモリ容量の把握が一切必要ないためアプリケーションにとっては素晴らしいことだが、メモリの負荷が非常に高いLuceneのようなアプリケーションでは問題にもつながる。

Luceneと仮想メモリ

ここで、インデックス全体もしくはその大半を「メモリ」(これが仮想メモリにすぎないことは先刻承知だ)に読み込む例を見ていく。RAMDirectoryをアロケートし、そこにインデックスファイルをすべて読み込むとOSの足を引っ張ることになる。OSはディスクアクセスを最適化しようとするため、すべてのディスクI/Oが物理メモリにキャッシング済みだ。これらのキャッシュの内容をすべて自分の仮想アドレス空間にコピーして膨大な量の物理メモリを消費している(しかも、コピー処理の開始を待たなくてはならない)。物理メモリには制限があるので、OSはもちろん大きなRAMDirectoryをスワップアウトする判断を下す必要があるのだが、それはどこに行くのだろうか?再びディスク上(OSのスワップファイル上)に戻るのだ。実際、われわれはディスクから読み込むものすべてをページアウトさせるOSカーネルと格闘している[2]。したがって、インデックスの読み込み時間最適化にRAMDirectoryは得策ではない。加えて、RAMDirectoryの方がガーベッジコレクションや並行処理関連で問題が多いのだ。スワップ領域にデータがあるため、自分のヒープ管理下にあるメモリの解放はJavaのガーベッジコレクタにとって大変な作業となる。そのためにディスクI/Oが増加し、インデックスアクセス速度が低下し、ガーベッジコレクタの動作が原因で検索コードの待ち時間が分単位に達する。

一方、もしインデックスのバッファリングにRAMDirectoryを使わずNIOFSDirectoryやSimpleFSDirectoryを使うと、ディスクやファイルシステムのキャッシュとJavaヒープ内のバッファとの間でデータブロックをコピーするためにコードがOSカーネルに対して多数のsyscallを行う必要が出るなど、さらに代償を払わなくてはならなくなる。検索リクエストがある度に何度もくり返して行う必要があるのだ。

メモリマッピングファイル

前述の問題に対するソリューションが、仮想メモリと「mmap」[3] というカーネル機能を使ってディスクファイルにアクセスするMMapDirectoryだ。

先のアプローチでは、ファイルシステムキャッシュとローカルJavaヒープの間でデータをコピーするのにsyscallに頼っていたが、ファイルシステムキャッシュに直接アクセスすればいいのではないか。これこそまさにmmapの仕事なのだ。

基本的に、mmapはLuceneインデックスをスワップファイルとして処理するようなことをする。mmap () syscallはOSカーネルに指示を出し、インデックスファイル全体を前述の仮想アドレス空間に仮想的にマッピングし、それらをLuceneのプロセスが利用可能なRAMのように見せる。そうすれば、大きなbyte[]アレイ(JavaではJavaコードが安全に利用できるようByteBufferインターフェースによってカプセル化される)になるかのようにディスク上のインデックスファイルをアクセスできるようになる。Luceneのコードからこの仮想アドレス空間にアクセスすればsyscallsは一切不要で、プロセッサのMMUとTLBがマッピングをすべて代わりに処理してくれる。データがディスク上にしかない場合は、MMUが割り込みを発生させ、OSカーネルがデータをファイルシステムキャッシュに読み込む。既にキャッシュされている場合は、MMU/TLBがそれをファイルシステムキャッシュ内の物理メモリに直接マッピングする。これでネイティブメモリアクセスに変わることになる。バッファのページイン/ページアウトを気にする必要はなく、これらはすべてOSカーネルによって処理される。さらに、並行処理の問題も全くなく、標準のbyte[]アレイに対する唯一のオーバーヘッドはJavaのByteBufferインターフェースによって生じるラッピングだけとなる(それでも本物のbyte[]アレイより速度は落ちるが、Javaからmmapを使う方法はこれしかなく、Luceneに付属するどのディレクトリインプリメンテーションよりもこちらの方がはるかに高速だ)。また、OSキャッシュ上で直接処理を行うため物理メモリも全く無駄にせず、前述のJava GC問題がすべて回避される。

Lucene/Solrアプリケーションへの影響は?

OSの足を引っ張るような開発は避けるべきなので、ヒープスペース(-Xmx Javaオプション)のアロケートはできるだけ小さく抑えたい。また、インデックスアクセスがOSキャッシュへの直接渡しに依存することを覚えておきたい。これは、Javaのガーベッジコレクタともかなり相性が良い。

OSカーネルがファイルシステムキャッシュとして利用できるよう可能な限り多くの物理メモリを解放する。また、Luceneのコードがそこで直接動作するためディスクとメモリ間のページング/スワッピング回数が減少することを覚えておきたい。ただし、Luceneアプリケーションにヒープをアロケートしすぎるとパフォーマンスに支障が生じる。MMapDirectoryではLuceneがこれを必要としないのだ。

これが64ビットのOSやJava仮想マシンでしか予想通りの動作をしないのはなぜだろうか?

32ビットプラットフォームの制限の1つに、ポインタサイズがあり、ポインタは4 Gバイトである0から232-1までのアドレスにしかアクセスすることができない。大半のOSは残りのアドレス空間をデバイスなどのために予約しておくため、アドレス空間を3 Gバイトに制限している。つまり、プロセスに与えられるリニアアドレス空間の合計は3 Gバイトに制限され、これより大きいファイルは大きいbyte[]アレイとしてこの「小さい」アドレス空間にマッピングできないことになる。そして、この大型ファイルをマッピングすると、仮想空間(「番地」に相当するようなアドレス)はなくなってしまう。既存のシステムの物理メモリサイズがこの大きさを既に超えていることから、リソースを無駄にせずファイルをマッピングするアドレス空間はない(ここでは物理メモリではなく「アドレス空間」)。

だが、64ビットプラットフォームでは異なってくる。264-1は巨大な数字で、1800京バイトを越えるため、実際のところはアドレス空間に制限はない。残念ながら、大半のハードウェア(MMU、CPUのバスシステム)やOSはこのアドレス空間をユーザモードアプリケーション(Windows:43ビット)用に47ビットに制限している[4]。しかし、それでもTバイト単位のデータをマッピングするためのスペースは残っている。

一般的な誤解

仮想メモリに関するこれまでの説明を注意深く読んでいれば、以下が正しいことは簡単に検証できる。

MMapDirectoryは余計なメモリを消費せず、マッピングされたインデックスファイルのサイズはサーバが搭載する物理メモリの制限を受けない。mmap ()ファイルではメモリではなくアドレス空間だけを予約する。64ビットプラットフォームのアドレス空間は無償で利用できることを覚えておきたい。

MMapDirectoryはインデックス全体を物理メモリに読み込むことはしない。では、なぜそうしなければならないのだろうか?OSには、容易なアクセスを可能にするためファイルをアドレス空間にマッピングするよう要求しているだけで、それ以上の要求は決してしていない。JavaとOSはファイル全体をRAMに読み込むオプション(十分な余裕があれば)を任意で提供するが、Luceneはこのオプションを使わない(今後のバージョンで追加の可能性はある)。

MMapDirectoryは「top」から膨大な量のメモリが伝わるとサーバに過負荷を与えない。「top」(Linux)にはメモリ関連の3つのコラムがある「VIRT」、「RES」、および「SHR」だ。最初(VIRT、仮想)のものはアロケートされた仮想アドレス空間を伝える(さらに、これは64ビットプラットフォームでは無償だ)。IndexWriterでマージが行われている場合、この数字はインデックスサイズや物理メモリの数倍にもなる。IndexReaderが1つしかオープンされていない場合は、アロケートされたヒープスペース(-Xmx)にインデックスサイズを加えたものとほぼ同じになり、プロセスが使用する物理メモリにはならない。2番目のコラム(RES、常駐)のメモリはプロセスが処理用にアロケートした(物理)メモリ量を示し、Javaヒープスペース以下に収まる。最後のコラム(SHR、共有)はアロケートされた仮想アドレス空間がほかのプロセスと共有しているサイズを示す。MMapDirectoryを使用する複数のJavaアプリケーションが同じインデックスをアクセスしている場合はこの数字が大きくなっていく。一般的には、共有システムライブラリ、JARファイル、そしてプロセス実行イメージ本体(これもmmapされる)が必要とするスペースが見えるようになる。

MMapDirectoryを最適利用するためのmyOSand Java VMのコンフィギュレーション

まず第一に、LinuxディストリビューションとSolaris/Windowsのデフォルト設定は全く問題ない。しかし、(理解不足から)何でも自分でコントロールしたがる偏執的なシステム管理者もおり、これによりアプリケーションがアロケート可能な仮想アドレス空間の最大量が制限を受けてしまう。そこで、「ulimit -v」「ulimit -m」の両方が「unlimited」を表示することを確認したい。そうしないと、MMapDirectoryがインデックスのオープン時に「mmap failed」を表示する可能性がある。それぞれのセグメントが多い膨大な数のインデックスを持つシステムでもこのエラーが発生する場合は、/etc/sysctl.confのカーネルパラメータをチューニングする必要があるかもしれない。vm.max_map_countのデフォルト値は65530だが、これを引き上げる必要があるかもしれない。WindowsとSolarisの両システムでも同様のセッティングが可能だと思うが、その使用方法は読者ご自身でお調べいただきたい。

Java VMのコンフィギュレーションに関してはメモリ要件を検討し直したい。ヒープスペースは本当に必要な量だけ用意して、OSにはできるだけ多くを残しておきたい。経験則として、Lucene/Solrが動作するJavaに物理メモリの四分の一以上をヒープスペースとして使ってはならず、残りのメモリはOSのキャッシュ用に空けておきたい。サーバ上で動作するアプリケーションが増えたらそれに合わせて調整を行う。従来通り、物理メモリは多ければ多い方が良いが、インデックスサイズと同量までの物理メモリは不要だ。頻繁に使用されるページはカーネルがインデックスからうまくページインしてくれる。

システムを最適にコンフィギュレーションしたかどうか調べるには、「top」(正しい説明は前記参照)と同様のコマンドである「iotop」(Ubuntu Linuxなどでは「apt-get install iotop」でインストール可能)の両方を見る。Luceneプロセスでシステムのスワップイン/スワップアウトが多い場合は、おそらく使いすぎなのでヒープサイズを減らす。ディスクI/Oが多い場合はmmappedされたファイルが四六時中ページイン/アウトされないようRUM(Simon Willnauer語録)を追加購入したい。そして、最終手段はSSDになる。

mmapをお楽しみあれ。



| 関口宏司 | Luceneクラス解説 | 23:15 | comments(0) | trackbacks(0) |









http://lucene.jugem.jp/trackback/474
+ Solrによるブログ内検索
+ PROFILE
  12345
6789101112
13141516171819
20212223242526
2728293031  
<< August 2017 >>
+ LINKS
検索エンジン製品 - 比較のポイント
商用検索エンジンを購入した企業担当者は読まないでください。ショックを受けますから・・・
>>製品比較 10のポイント
+ Lucene&Solrデモ
+ ThinkIT記事
+ RECOMMEND
Apache Solr入門 ―オープンソース全文検索エンジン
Apache Solr入門 ―オープンソース全文検索エンジン (JUGEMレビュー »)
関口 宏司,三部 靖夫,武田 光平,中野 猛,大谷 純
+ RECOMMEND
Lucene in Action
Lucene in Action (JUGEMレビュー »)
Erik Hatcher,Otis Gospodnetic,Mike McCandless
FastVectorHighlighterについて解説記事を寄稿しました。
+ RECOMMEND
+ SELECTED ENTRIES
+ RECENT COMMENTS
+ RECENT TRACKBACK
+ CATEGORIES
+ ARCHIVES
+ MOBILE
qrcode
+ SPONSORED LINKS