parallel-fs-ops.template.yml|添付ファイル (2.8 KB)
Discourse サイトが大量のアップロードライブラリを蓄積すると、 painfully 明白になるスケーリング問題について話し合いましょう。
以前、巨大な uploads ディレクトリ全体で chown コマンドを実行するには数分かかっていましたが、今では数秒で完了します!
背景
Discourse の再構築では、以下のような再帰的な操作が実行されることがあります:
chown -R ...
chmod -R ...
より具体的には、templates/web.template.yml 内のこの行です:
- chown -R discourse:www-data /shared/log/rails /shared/uploads /shared/backups /shared/tmp
これらのコマンドはファイルシステムを直列に、1つずつたどります。
小規模なインストールであれば完全に合理的です。しかし、shared/uploads に数十万、あるいは数百万のファイルが含まれている場合、再帰的な所有権と権限の操作がデプロイの大部分を占める可能性があります。CPU、ストレージ、ネットワークの容量が利用可能であっても、1つのプロセスがツリー全体を1つの inode ずつたどるためです。
アップロードが多いコミュニティでは、その結果として以下のような問題が生じる可能性があります:
- 非常に長い再構築時間
- より長いメンテナンスウィンドウ
- デプロイやセキュリティ更新の遅延
- 高速または分散型ストレージの低い利用率
- 巨大なファイルツリーを処理している間、デプロイが停止したように見えること
- NFS、JuiceFS、CephFS およびその他のリモートファイルシステムでの特に痛みを伴うパフォーマンス
厄介なことに、これらのファイルの多くは独立しています。それらの権限は並行して処理できます。
解決策:並列ファイルシステム操作テンプレート
私は、再帰的な chmod および chown 操作を、並列の find と xargs パイプラインに透明に置き換える pups テンプレートを作成しました。
ラッパーは、再帰的な操作をインターセプトするたびに自身をアナウンスします:
echo "[parallel-fs-ops] chmod -R override active: $*" >&2
そして:
echo "[parallel-fs-ops] chown -R override active: $*" >&2
この角括弧付きのプレフィックスにより、デプロイログ内でこの最適化を簡単に特定できます。
デプロイの外観
デプロイの初期段階で、テンプレートは後続のファイルシステム操作を処理するバイナリを確認します:
[parallel-fs-ops] chmod -> /usr/local/bin/chmod
[parallel-fs-ops] chown -> /usr/local/bin/chown
上流のテンプレートが後で再帰的な権限変更を実行すると、デプロイ出力には以下のような行が含まれます:
[parallel-fs-ops] chmod -R override active: -R 0755 /var/www/discourse/public
再帰的な所有権変更では、以下が生成されます:
[parallel-fs-ops] chown -R override active: -R discourse:www-data /shared/log/rails /shared/uploads /shared/backups /shared/tmp
アップロードが多いインストールでは、以下のようなものが見えるかもしれません:
[parallel-fs-ops] chown -R override active: -R discourse:www-data /shared/log/rails /shared/uploads /shared/backups /shared/tmp
正確なパスと引数は使用されているテンプレートに依存しますが、重要な部分は視覚的なマーカーです:
[parallel-fs-ops]
テンプレートがない場合、デプロイは再帰的なファイルシステム操作の間、長時間停止しているように見える可能性があります。テンプレートがある場合、ログは以下を伝えます:
- ラッパーが正しくインストールされた。
- 再帰的な操作が検出された。
- 並列実装が有効になっている。
- 処理されている元の引数が表示されている。
これは、遅い並列ファイルシステム走査と、ハングしたビルドを区別できるため、トラブルシューティング中に特に価値があります。
操作が終了すると、デプロイは通常の pups 出力で続行されます。ラッパー自体はファイルごとに1行ずつ出力しないため、数百万のアップロードを含むツリーであっても、デプロイログが洪水のように埋まることはありません。
テンプレート
run:
- file:
path: /usr/local/bin/chmod
chmod: "+x"
contents: |
#!/bin/bash
if [[ "$*" =~ (^|[[:space:]])-R([[:space:]]|$) ]]; then
echo "[parallel-fs-ops] chmod -R override active: $*" >&2
args=()
for arg in "$@"; do
[[ "$arg" != "-R" ]] && args+=("$arg")
done
mode="${args[0]}"
targets=("${args[@]:1}")
[[ ${#targets[@]} -eq 0 ]] && targets=(".")
find "${targets[@]}" -print0 |
xargs -0 -n 32 -P 128 /bin/chmod "$mode"
else
exec /bin/chmod "$@"
fi
- file:
path: /usr/local/bin/chown
chmod: "+x"
contents: |
#!/bin/bash
if [[ "$*" =~ (^|[[:space:]])-R([[:space:]]|$) ]]; then
echo "[parallel-fs-ops] chown -R override active: $*" >&2
args=()
for arg in "$@"; do
[[ "$arg" != "-R" ]] && args+=("$arg")
done
owner="${args[0]}"
targets=("${args[@]:1}")
[[ ${#targets[@]} -eq 0 ]] && targets=(".")
find "${targets[@]}" -print0 |
xargs -0 -n 32 -P 128 /bin/chown "$owner"
else
exec /bin/chown "$@"
fi
- exec:
cmd: |
echo "[parallel-fs-ops] chmod -> $(command -v chmod)"
echo "[parallel-fs-ops] chown -> $(command -v chown)"
このテンプレートは /usr/local/bin にラッパーをインストールします。これは通常、PATH で /bin より前に表示されます。
通常の非再帰的な操作が要求された場合、ラッパーは標準ユーティリティに直接委譲します:
exec /bin/chmod "$@"
-R が存在する場合、再帰フラグを削除し、null 区切りでターゲットを安全に列挙し、バッチを並行して処理します:
find "${targets[@]}" -print0 |
xargs -0 -n 32 -P 128 /bin/chmod "$mode"
これは、pups が /bin/sh を介してコマンドを呼び出す場合にも機能します。呼び出し元シェルが Dash であっても、実行可能ファイルが起動される際にラッパーの Bash シェバンが尊重されます。
なぜこれが多くのアップロードがある場合に最も重要なのか
アップロードが多いコミュニティこそ、デプロイの動作が優雅にスケーリングする必要がある場所です。
長期間運営されているフォーラムには、以下が含まれる可能性があります:
- 年々蓄積された投稿に埋め込まれた画像
- アバターやプロフィール背景
- 元画像と最適化された画像のバリアント
- ビデオやオーディオの添付ファイル
- ドキュメントやアーカイブ
- セキュアアップロード
- プラグインによって管理されるメディア
- マルチサイトアップロードツリー
アプリケーションコードの量は比較的安定しているかもしれませんが、アップロードされたファイルシステムオブジェクトの数は増え続けています。ファイルシステム走査——コンパイルやコンテナ作成ではなく——が最終的にデプロイコストの主要因になる可能性があります。
これは独特のスケーリング問題です:コミュニティがより成功し、コンテンツが豊富になるほど、ルーティンの運用作業のコストが高くなる可能性があります。
なぜテンプレートが必要なのか
.bashrc を変更したり BASH_ENV を設定したりしても、これを確実に解決するわけではありません。pups は /bin/sh を介して run コマンドを実行し、Dash は Bash の設定を読み込もうとせず、Bash 固有の関数も理解しません。
テンプレートは、後続の再帰的な操作(上流テンプレートからのものを含む)が並列実装を介して解決されるように、ラッパーを十分に早期にインストールするための再現可能な方法を提供します:
templates:
- "templates/postgres.template.yml"
- "templates/redis.template.yml"
- "templates/web.template.yml"
- "containers/parallel-fs-ops.template.yml"
設定可能なオプション
-P 128 は、その並行性を維持できるストレージにのみ適しています。それは普遍的なデフォルトではありません。
システムに応じて、適切な並行レベルは以下のようなものかもしれません:
- 遅いローカルディスクには
-P 4 - 汎用ホストには
-P 8または-P 16 - 高速 SSD ベースのストレージには
-P 32 - 並行メタデータ操作から恩恵を受ける分散ファイルシステムにはより高い値
並行性が多すぎると、ファイルシステムをオーバーwhelm し、メタデータサーバーを飽和させ、デプロイパフォーマンスを悪化させる可能性があります。したがって、バッチサイズと並行性は設定可能であるべきです。
このテンプレートは実用的なワークアラウンドですが、より大きな提案はより広範です:
Discourse は、デプロイ中の大規模な再帰ファイルシステム操作に対して、設定可能な並行性を公式にサポートできますか?
上流の実装では、以下を行うことができます:
- 既知の大規模ディレクトリツリーのみを並列化する
- 変更されていないアップロードツリーを不要に走査しない
- 並行性を設定可能にする
- ローカルファイルシステムとネットワークベースファイルシステムを検出する
chmodおよびchownの引数セマンティクスを完全に保持する- 非常に大きなツリーに対して定期的な進捗を表示する
- タイミングを記録し、管理者がデプロイのボトルネックを特定できるようにする
重要な注意
上記のラッパーは、私たちのビルドプロセスで使用される再帰コマンド形式に焦点を当てています。これは、chmod や chown のすべての可能性のあるオプション組み合わせの完全な再実装ではありません。
本番環境で使用する前に、サイトのテンプレートによって生成される正確なコマンドに対してテストする必要があります。オペレーターは保守的な並行性から開始し、ストレージへの影響を測定すべきです。
しかし、根本的な問題は現実のものです:コミュニティが巨大なアップロードツリーを蓄積した場合、直列の再帰メタデータ操作はうまくスケーリングしません。
幸運を祈ります。コメントや提案(他の人の努力と重複している場合でも、その指針も歓迎です)をいただければ幸いです。
Cheers!(乾杯!)