Azure App Service on Linuxでファイルアクセスを高速化する方法(ローカルディスク活用編):Tech TIPS
Azure App Serviceで大きめのファイルをアクセスしたとき、少し遅いと感じたことはないだろうか? データファイルの配置を変えることで3倍以上高速化した手法とその注意点を紹介する。
対象:Azure App Service on Linux
Azure App Serviceで「サーバ内でのファイルアクセスが少し遅い」と感じたことはないだろうか?
筆者の場合、数十MB単位のデータファイルをApp Service上で取り扱ったとき、他のWebサーバのサービスに比べて遅く感じたことがある。
そこで、比較的簡単な方法でApp Service内でのファイルアクセスを高速化する方法を紹介したい。筆者が特定のWebアプリのために、あれこれ実験した結果であり、適用できる用途や環境は限られるかもしれない。それでも同じ悩みを抱える構築/運用担当の助けになれば幸いだ。
対象はApp Service on Linuxとする。スタックはPHP 8.4としており、他のスタックでは設定や各種ファイル名などが異なる場合があることを了承いただきたい。
App Serviceの「ディスク」は遅い!?
App Serviceでは通常、Azure Storageによる「共有ストレージ(共有ディスク)」にデータファイルが格納される。ファイルシステムのパスとしては[/home]ディレクトリ以下がこの共有ディスクに割り当てられている。Webサイトのルート(ドキュメントルート)は[/home/site/wwwroot]ディレクトリなので、Webサイトのコンテンツも共有ディスクに格納される。
この共有ディスクはApp Serviceをスケールアウトしたとき、複数のインスタンスから共有される。そのため、どのインスタンスからでも同じデータファイルを取得できるというメリットがある。
その一方で、共有という仕組みがあるせいなのか、アクセス速度は相対的に遅めのようだ。KB単位の小さなファイルはまだしも、数十MB単位の大きなファイルへのアクセスだと、筆者には他のWebサーバのサービスと比べて遅く感じられることがある。
この遅さを「回避」する方法の一つとして、インスタンスごとに割り当てられている「ローカルディスク」を共有ディスクの代わりに用いる、という方法が考えられる。ローカルディスクにはApp Serviceを実行するOSなどのプログラムが格納されている他、一時的なデータやログなどを保存する場所としても使われる。共有ディスクに比べると、インスタンスに「近い」ストレージのため、より高速にアクセスできる。
より高速な「ローカルディスク」にデータファイルをコピーするには
ここでは、共有ストレージ上の[/home/site/wwwroot]ディレクトリ以下に元のデータファイルを配置しつつ、その一時的なコピーをローカルディスク上の[/var/opt]ディレクトリ以下に配置することにする。
ここで注意が必要なのは、App Serviceが再起動されると[/var/opt]以下のファイルは初期化され、コピーしたファイルも消えることだ(再起動後も維持されるのは[/home]以下に限られる)。
そこで、App Serviceの「スタートアップスクリプト」(デフォルトでは[/home/startup.sh])を使って、App Serviceの起動時に自動で[/home/site/wwwroot]以下から[/var/opt]以下へデータファイルをコピーする。
以下では、ファイルベースのデータベース管理システム「SQLite3」のDBファイルをローカルディスクにコピーする例を挙げている。
#!/usr/bin/env bash
# <既存のスタートアップスクリプトのコード>
# 共有ディスク上にある元のDBファイルのありか
SQLITE3_DB_DIR_SRC="/home/site/wwwroot/data/sqlite_db"
# ローカルディスク上のコピー先
SQLITE3_DB_DIR_OPT="/var/opt/poimap/data/sqlite_db"
# 元のDBファイルのディレクトリが存在する場合
if [ -d ${SQLITE3_DB_DIR_SRC} ]; then
# ローカルディスク上のコピー先ディレクトリを作成
mkdir -p ${SQLITE3_DB_DIR_OPT}
# ローカルディスクにコピー
cp -pv ${SQLITE3_DB_DIR_SRC}/*.db ${SQLITE3_DB_DIR_OPT}/
# WebサーバやPHPだけが更新できるように所有者やパーミッションを変更
chown -R www-data:www-data ${SQLITE3_DB_DIR_OPT}
chmod 644 ${SQLITE3_DB_DIR_OPT}/*
fi
ファイルのパス: /home/startup.sh
注意が必要なのは、「/var/opt」以下はデフォルトでrootしか書き込めず、WebサーバやPHPなどからそこにファイルをコピーできない、という点だ。そのままだと、データファイルを更新できるのは前述のスタートアップスクリプト、すなわちApp Serviceの再起動時に限られてしまう。
それでは不自由なので、スタートアップスクリプトの実行中にコピー先のディレクトリやファイルの所有者を、rootからWebサーバの実行ユーザーである「www-data」に変えておく。スタートアップスクリプトはrootで実行されるため、[/var/opt]以下の所有者やパーミッションを変更できる。変更方法は上記リストの「chown」「chmod」コマンドの行を参照していただきたい。
後は元のデータファイルを更新する際に、ローカルディスクにもコピーすればよい。参考までに、「id」を指定して呼び出すと対象のファイルをローカルディスクにコピーするPHPのプログラム例を記しておく。
function main():bool {
$project_name = "poimap"; // プロジェクト名
// クエリパラメータからidを取得
$id = filter_input(INPUT_GET, "id", FILTER_VALIDATE_REGEXP, [
"options" => ["regexp" => '/^[a-z\d\-_]+$/'],
]);
if (empty($id)) {
throw new RuntimeException("不正なIDが指定されました", 400);
}
// idに対応するファイルパスを取得
$src_dir = "/home/site/wwwroot/data/sqlite_db"; // コピー元
$dest_dir = "/var/opt/$project_name/data/sqlite_db"; // コピー先
$file_name = "poi_{$id}.db"; // コピー対象のファイル名
// ソースファイルが存在するか確認
if (!file_exists("$src_dir/$file_name")) {
throw new RuntimeException("ソースファイルが見つかりません", 400);
}
// コピー先ディレクトリが存在しない場合は作成
if (!is_dir($dest_dir)) {
if (!mkdir($dest_dir, 0755, true)) {
throw new RuntimeException("コピー先ディレクトリの作成に失敗しました", 500);
}
}
// ファイルをコピー
if (!copy("$src_dir/$file_name", "$dest_dir/$file_name")) {
throw new RuntimeException("ファイルのコピーに失敗しました", 500);
}
return true;
}
try {
if (main()) {
$result = [
"status" => "success", // 正常終了
"message" => "ファイルの更新に成功しました"
];
} else {
throw new RuntimeException("ファイルの更新に失敗しました", 500);
}
} catch (RuntimeException $ex) {
error_log($ex->getMessage(), 0); // エラーログに記録
$result = [
"status" => "error", // エラー
"message" => $ex->getMessage(),
];
http_response_code($ex->getCode() ?: 500); // HTTPステータスコードを設定
} finally {
header('Content-Type: application/json; charset=UTF-8');
echo json_encode($result);
}
このリストをPHPファイルとして保存してドキュメントルート以下に配置し、クエリパラメータで「id=<ID>」のように指定しつつ呼び出すと、「poi_<ID>.db」というファイルがローカルディスク上の指定ディレクトリにコピーされる。
上記リストを保存したPHPファイルは、第三者に呼び出されないように認証あるいはソースIPアドレス制限などで保護した方が無難だろう。
データファイルをローカルディスクにコピーする仕組みを実装したら、データファイルを参照するプログラム側でそのパスをローカルディスクの方に変更する。ただ、ファイルのコピーや更新に失敗する可能性があるので、ローカルディスク側に対象ファイルが存在しなかったり更新日時が古かったりしたら、共有ディスク側の元ファイルを参照するようにプログラムを改修するのが望ましいだろう。
ローカルディスクは標準の共有ディスクより3倍以上速い!?
ここまで説明してきた手法で共有ディスクからローカルディスクに場所を変えたときの性能向上の例を紹介しよう。前述のSQLite3のDBファイルを共有ディスクとローカルディスクそれぞれに配置し、切り替えながら、そのデータベースを参照するAPIの応答時間をそれぞれ計測した。
対象のAPIは、Webベースのマップアプリから呼び出され、地図上でチェーン店の店舗があるところにピンを立てるのに利用されている。具体的には、呼び出し時に指定された地図上の矩形範囲内に存在する店舗の情報(地点情報)をデータベースから抽出して、その座標(緯度と経度)などをクライアントに返す。エンドユーザーがマップを移動したりズームレベルを変えたりするたびに呼び出されるため、その応答時間が長くなるとピンが立つまでに時間がかかり、アプリ全体の反応も鈍くなって使い勝手が悪くなる。

DBファイルのありかとAPIの応答速度の違い
ファイルベースのDBMS「SQLite3」のDBファイルを、共有ディスク上の[/home/site/]以下とローカルディスク上の[/var/opt/]以下にそれぞれ配置したときに、そのデータベースを参照するAPIの応答時間を測定した(APIについては本文参照)。横軸が応答時間で単位はmsec(ミリ秒)。縦軸はAPIが返す地点情報の個数(この数値が大きいほどDBMSに負担がかかる)。グラフの棒が短い(数値が小さい)ほど性能が優れている。App ServiceのスケールはP1V3で、スタックはPHP 8.4。DBの圧縮やインメモリDBは未使用。10回測定した平均値を掲載している。
このようにDBファイルのありかを変えただけで、APIの応答時間が3分の1以下、つまり3倍以上速くなっていることが分かる。特に1730地点の方は、246msec≒4分の1秒ほどの差が付いている。このAPIをマップアプリで用いたとき、明らかにエンドユーザーがその速度差を体感できるレベルだ。
もちろん、ここまで性能が向上するかどうかはデータファイルのサイズや使用方法などに依存する。実際、同じDBを参照する別のAPIでは、共有ディスクとローカルディスクで有意な性能差が表れなかった。
【注意】ローカルディスクを浪費すると深刻なエラーが生じる恐れがある
ローカルディスクもストレージの一種なので、当然ならがらその容量には上限がある。速いからといってローカルディスクにどんどんデータファイルをコピーした結果、満杯になるとApp Serviceが開始できなくなる、といったトラブルが生じるようだ。
Microsoft Learnの「Azure App Service on LinuxのFAQ」には、「インスタンスごとに15GBの制限があります」という記述がある。これにはOSなども含まれるはずなので、本記事のような使い方で消費できる容量はもっと少ないと思われる。GB単位で消費するのは避けた方がよさそうだ。
【注意】データファイルの一部を書き換える用途には向いていない
インスタンス内でデータファイルの一部をときどき書き換えなければいけない場合、各インスタンスのローカルディスク上のデータファイルがいつも同じ内容になるように同期させる必要がある。そうしないと、インスタンスによってクライアントへのレスポンスが変わってしまうというトラブルを引き起こしてしまう。
そのためローカルディスクを活用するなら、データファイルは読み出し専用か、ファイル単位で容易に更新できるような用途が望ましい。
【推奨】直接クライアントへ返すファイルなら「キャッシュ」を活用すべき
直接クライアントに返すデータファイルのアクセスを高速化したい場合、クライアントデバイスのローカルキャッシュやキャッシュサーバのような別の手段を検討した方がよいだろう。
HTTPレスポンスでキャッシュの期間を適切に設定しておけば、初回はともかく、次回からのアクセスはクライアント側だけで完結するため、大幅に高速化できる。
またAzure Front DoorやAzure CDNのようなサービスをApp Serviceの前段に組み込み、そこで対象のファイルをキャッシュすれば、高速化はもとより、App Serviceの負荷も減らせる。
Copyright© Digital Advantage Corp. All Rights Reserved.