web-dev-qa-db-ja.com

ディレクトリを一時的にキャッシュおよび書き込みバッファリングします(NFS共有でのビルドプロセスを高速化するため)

概要概要

この質問は次のように構成されています。
最初に、なぜこのトピックに興味があるのか​​、そしてそれが私が扱っている問題をどのように解決するのかについて、いくつかの背景を説明します。次に、ファイルシステムのキャッシュに関する実際のスタンドアロンの質問をします。そのため、動機付け(C++プロジェクトのビルドセットアップ)に興味がない場合は、最初のセクションをスキップしてください。

最初の問題:共有ライブラリのリンク

プロジェクトのビルド時間を短縮する方法を探しています。セットアップは次のとおりです。ディレクトリ(workareaと呼びます)はNFS共有にあります。最初は、ソースコードとメイクファイルのみが含まれています。次に、ビルドプロセスは、最初に_workarea/lib_に静的ライブラリを作成し、次に_workarea/dll_の静的ライブラリを使用して_workarea/lib_に共有ライブラリを作成します。共有ライブラリの作成中に、これらは書き込まれるだけでなく、たとえば、 nmは、リンク時にシンボルが欠落していないことを確認します。多くのジョブを並行して使用すると(たとえば、make -j20またはmake-j 40)、ビルド時間はリンク時間によってすぐに支配されます。この場合、リンクのパフォーマンスはファイルシステムのパフォーマンスによって制限されます。たとえば、20個のジョブと並行してリンクする場合、NFS共有では約35秒かかりますが、RAMドライブでは5秒しかかかりません。rsyncを使用してdllをNFS共有にコピーして戻すには、さらに時間がかかることに注意してください。 6秒なので、RAMドライブで作業し、後でNFSに同期する方が、NFS共有で直接作業するよりもはるかに高速です。明示的にコピー/リンクせずに高速パフォーマンスを実現する方法を探しています。 NFS共有とRAMドライブの間のファイル。
NFS共有はすでにキャッシュを使用していますが、このキャッシュは読み取りアクセスのみをキャッシュできることに注意してください。
AFAIK、NFSでは、NFSサーバーが書き込みの完了を確認するまで、NFSクライアントは書き込みを確認できないため、クライアントはローカル書き込みバッファーを使用できず、書き込みスループットは(スパイクの場合でも)ネットワークによって制限されます。速度。これにより、セットアップでの合計書き込みスループットが約80MB /秒に効果的に制限されます。
ただし、読み取りキャッシュが使用されるため、読み取りパフォーマンスははるかに優れています。 NFSで_workarea/lib_とリンク(dllのコンテンツを作成)し、_workarea/dll_がRAMドライブへのシンボリックリンクである場合、パフォーマンスは依然として良好です-約5秒ビルドプロセスは、NFS共有にある_workarea/*_で終了する必要があることに注意してください。高速インクリメンタルビルドを可能にするには、libが共有(または永続マウント)にある必要があり、dllがNFSにある必要があります。これらのdllを使用してジョブを開始するコンピューティングマシンからアクセスされます。
したがって、以下の問題の解決策を_workarea/dll_に適用し、おそらく_workarea/lib_(コンパイル時間を改善するために後者)にも適用したいと思います。以下の高速セットアップ時間の要件は、必要な場合にのみデータをコピーして、高速インクリメンタルビルドを実行する必要があるためです。

更新

ビルドのセットアップについてもう少し具体的にすべきだったでしょう。詳細は次のとおりです。コンパイル単位は、一時ディレクトリ(/ tmp内)の.oファイルにコンパイルされます。次に、これらはlibを使用してarの静的ライブラリにマージされます。完全なビルドプロセスは段階的です。

  • コンパイル単位は、コンパイル単位自体(.Cファイル)または含まれているヘッダーが変更された場合にのみ再コンパイルされます(makeに含まれているコンパイラー生成の依存関係ファイルを使用)。
  • 静的ライブラリは、そのコンパイル単位の1つが再コンパイルされた場合にのみ更新されます。
  • 共有ライブラリは、静的ライブラリの1つが変更された場合にのみ再リンクされます。共有ライブラリのシンボルは、それが依存する共有ライブラリによって提供されるシンボルが変更された場合、または共有ライブラリ自体が更新された場合にのみ再チェックされます。

それでも、複数のコンパイラ(gccclang)、コンパイラバージョン、コンパイルモード(releasedebug)、C++標準(_C++97_、_C++11_)などがあるため、完全またはほぼ完全な再構築が頻繁に必要になります。変更(例:libubsan)を使用できます。すべての組み合わせで、異なるlibディレクトリとdllディレクトリが効果的に使用されるため、セットアップを切り替えて、そのセットアップの最後のビルドに基づいて段階的にビルドできます。また、インクリメンタルビルドの場合、多くの場合、数個のファイルを再コンパイルする必要があり、時間はほとんどかかりませんが、(おそらく大きな)共有ライブラリの再リンクがトリガーされ、はるかに時間がかかります。

アップデート2

その間に、nocto NFSマウントオプションについて学びました。これは、Linuxがnoctoを使用している場合でも、書き込みバッファを常にclose()でフラッシュするため、Linuxを除く基本的にすべてのNFS実装で問題を解決できるようです。他にもいくつか試しました。たとえば、asyncが有効になっている別のローカルNFSサーバーを使用して、書き込みバッファーとして機能し、メインのNFSマウントをエクスポートできますが、残念ながら、この場合、NFSサーバー自体は書き込みバッファリングを行いません。 asyncは、サーバーが基盤となるファイルシステムを強制的に安定したストレージにフラッシュしないことを意味しているようです。基盤となるファイルシステムが書き込みバッファーを使用する場合は、書き込みバッファーが暗黙的に使用されます(ファイルの場合は明らかにそうです)。物理ドライブ上のシステム)。
noctoを使用してメインNFS共有をマウントし、書き込みバッファーを提供し、別のNFSサーバーを介してこのバッファーマウントを提供する同じボックスで非Linux仮想マシンを使用するオプションについても考えましたが、まだしていません。それをテストし、そのような解決策を避けたいと思います。
キャッシュとして機能するFuseベースのファイルシステムラッパーもいくつか見つかりましたが、これらのいずれも書き込みバッファリングを実装していませんでした。

ディレクトリのキャッシュとバッファリング

いくつかのディレクトリを考えてみましょう。それをorigと呼びましょう。これは、低速のファイルシステムにあります。 NFS共有。短い期間(たとえば、秒または分ですが、これはとにかく問題ではありません)、高速ファイルシステムにあるディレクトリorigを使用して、cacheの完全にキャッシュおよびバッファリングされたビューを作成したいと思います。ローカルハードドライブまたはRAMドライブ。キャッシュは、マウント_cached_view_などを介してアクセス可能であり、root権限を必要としない必要があります。キャッシュの存続期間中は、 origへの直接の読み取りまたは書き込みアクセスはありません(もちろん、キャッシュ自体のほかに)。完全にキャッシュおよびバッファリングされるとは、次のことを意味します。

  1. 読み取りクエリは、クエリをorigのファイルシステムに一度転送し、その結果をキャッシュしてそれ以降使用することで回答されます。
  2. 書き込みクエリはcacheに書き込まれ、完了時に直接確認されます。つまり、キャッシュは書き込みバッファでもあります。これは、書き込まれたファイルでclose()が呼び出された場合でも発生するはずです。次に、バックグラウンドで、書き込みは(おそらくキューを使用して)origに転送されます。もちろん、書き込まれたデータへの読み取りクエリは、cacheのデータを使用して応答されます。

さらに、私は必要です:

  1. キャッシュは、キャッシュをシャットダウンする機能を提供します。これにより、origへのすべての書き込みがフラッシュされます。フラッシュの実行時間は、すべてのファイルではなく、書き込まれたファイルのサイズのみに依存する必要があります。その後、再び安全にorigにアクセスできます。
  2. セットアップ時間は速いです、例えば。キャッシュの初期化は、orig内のファイルの数にのみ依存し、orig内のファイルのサイズには依存しない場合があるため、origcacheに1回コピーすることはできません。

最後に、別のファイルシステムをキャッシュとして使用せず、メインメモリにキャッシュするだけのソリューションでも問題ありません(サーバーには十分なRAMがあります)。たとえば、の組み込みキャッシュを使用することに注意してください。 AFAIK NFSは書き込みバッファーを許可しないため、NFSはオプションではありません(最初のセクションを参照)。

私のセットアップでは、origの内容をcacheにシンボリックリンクしてから、cacheを操作することで少し悪い動作をエミュレートできます(すべての書き込み操作は実際にファイルを新しいファイルに置き換えます。この場合、シンボリックリンクは更新されたバージョンに置き換えられます)。その後、変更されたファイルをorigに同期します。これは、上記の要件を正確に満たしていません。読み取りは1回だけ行われるのではなく、ファイルはシンボリックリンクに置き換えられます。これはもちろん、一部のアプリケーションに違いをもたらします。
これは(私の単純な設定でも)これを解決する正しい方法ではないと思います。おそらく誰かがよりクリーンな(そしてより速い!)解決策を知っているでしょう。

7
jasilvan

うわー、まだ誰も「オーバーレイ」に答えていないことに驚いた。

実際、私には2つの提案があります。 1つ目は、overlayfsを使用することです。これは、基本的にはまさにあなたが説明しているものですが、注意点が1つあります。オーバーレイ(Linux 3.18以降の標準)を使用すると、仮想的にマージされた2つのディレクトリツリーから読み取り、そのうちの1つだけに書き込むことができます。あなたがすることは、高速ストレージ(tmpfsのような)を取り、それをNFSボリュームにオーバーレイし、次に2つのオーバーレイされたマージでコンパイルを実行することです。完了すると、NFS上のファイルへの書き込みはゼロになり、他のファイルシステムがすべての変更を保持します。変更を保持したい場合は、NFSにrsyncして戻すことができます。気にしないファイルを除外したり、結果からいくつかのファイルを手動で選択したりすることもできます。

私の小さなプロジェクトでoverlayfsの比較的単純な例を見ることができます: https://github.com/nrdvana/squash-portage/blob/master/squash-portage.sh そのスクリプトも示していますoverlayfsを持たない古いカーネルを使用している場合のUnionFSの使用方法。

私の場合、Gentooがソフトウェアライブラリを更新するために使用するrsyncコマンドには、何百万もの小さなディスク書き込みがあるため、めちゃくちゃ長い時間がかかります。私はoverlayfsを使用してすべての変更をtmpfsに書き込み、次にmksquashfsを使用してツリーの圧縮イメージを作成します。次に、tmpfsを破棄し、その場所に圧縮イメージをマウントします。

私の2番目の提案は、「ツリー外」ビルドです。 1つのツリーにソースコードとメイクファイルがあり、automakeに、最初のツリーをミラーリングする別のツリーにすべての中間ファイルを生成するように指示します。

運が良ければ、ビルドツール(automakeなど)ですでにこれを実行できます。運が悪ければ、メイクファイルをいじくり回すのに頭を悩ませる必要があるかもしれません。

2
M Conrad