RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

PHPアプリケーションをシンボリックリンク切替でデプロイするときのポイント

この記事は ラクス Advent Calendar 2024 の14日目の記事(予定)です。

はじめに

こんにちは、kasuke18 です。 楽楽販売のDevOpsチームメンバーとして、リリース周りなどインフラチームと協働する部分を主に担当しています。

私達のプロダクトでは、シンボリックリンク切替による無停止デプロイを導入しました。 導入にあたって私達のコンテキスト(特にサーバ構成などのアーキテクチャ)に合うか検証のうえ、必要なアプリケーション改修を実施しました。 その検証内容と改修ポイントをご紹介します。参考になれば幸いです。

シンボリックリンク切替によるデプロイについて

このデプロイフローの導入前は、rsyncによるファイルの直接差し替えで対応していました。 PHPのファイル読込は遅延読込されるため、タイミングによっては処理中のプロセスがエラーになってしまうケースがあります。 (例えば新しいクラスを含むコードをデプロイする際、その新ファイルの配置より先に利用個所のロジックが処理されてしまうと未定義としてエラーになる) そのため顧客影響を避けることから、リリース作業を夜遅くに実施するなど運用コストが高い状態が続いていました。

これを改善するため、各所で導入事例のある「シンボリックリンク切替によるデプロイ」を検討しました。

上記のようなデプロイフロー・具体的なデプロイの手順(mvコマンドでシンボリックリンクを上書きする)などは、導入事例を参考に、割とそのまま利用できる部分が多かった記憶があります。 https://developers.prtimes.jp/2022/05/31/prtimes-deploy-improvement/

とはいえ単純にデプロイ手順を変えるだけでよいのか、具体的にはアプリケーションの作り込みを変える必要があるのか、といった観点では私達のコンテキストに近しい事例があまり見つからず、導入するにあたって本当に問題ないのかを検証しました。

今回の改善における無停止デプロイのスコープ

シンボリックリンク切替という手法なので、サーバ上のアプリケーションコードの差し替え部分にのみ影響します。改善効果が期待できる範囲もサーバ上のPHPプロセスが担う処理に限定されます。

逆にフロントエンド↔バックエンド間のインタフェースの変更(リクエストパラメータの増減)やPHPプロセス↔データベース間のインタフェースの変更(DBテーブル定義の変更)には対応できません。これらの改善には別の手段が必要になりますのでご留意ください。

検証したこと

まず私達のプロダクトへ導入できないか検討するにあたって、他社の事例などを参考にさせていただきました。

例えば下記の導入事例などを見ると、realpathキャッシュというもの原因で問題が発生するリスクが説明されています。 https://www.klab.com/jp/blog/tech/2016/1062120304.html

これだけを見ても単に導入すると、痛い目に遭う可能性が高そうです。 またOPcacheの利用有無やサーバソフトウェアなど、実行環境による違いによる挙動の変化もあり得るため、私達のコンテキストに合った検証が必要でした。

ちなみに今回検証を行った構成は以下のとおりです。

検証におけるゴール

今回の検証では、上記のような変更があったとしても「1つのPHPプロセス内では、デプロイ前・後のどちらかのコードのみが実行される」ということを担保することを目的に検証を進めました。

一番リスクなのは新旧コードが入り混じって実行されることで、その場合はなにかエラーが発生したとしてもまともに調査することができません。 新バージョンの反映に多少の遅延が生まれることは許容し、それよりも1つのPHPプロセス内での一貫性を優先しました。

検証観点

以下の3段階で検証を行い、新旧コードが入り混じって実行されることがないかを確認しました。

  • ①realpathキャッシュの動作検証
  • ②アプリケーションコードの動作検証
  • ③本番想定のアクセス下の動作検証

①realpathキャッシュの動作検証

まずは私達のコンテキストにおけるrealpathキャッシュの効き方を把握するため、ミニマムなサンプルコードを作成しました。

sleep中にシンボリックリンクを切り替えてrealpathキャッシュの内容やロードされたクラスのファイルパスがどうなるかをするかを確認します。

詳細は長くなるので割愛しますが、下記のような関数を利用しました。

  • realpath_cache_get(): この関数の実行時点のrealpathキャッシュ内容を取得
  • ReflectionClass::getFileName: クラスが定義されているファイルを取得
// require_once("/app/symlink/SampleClass1.php");
// var_dump(realpath_cache_get());
["/app/symlink/SampleClass1.php"]=> // キャッシュのキーはrequire_onceに指定したパス
array(4) {
  ["key"]=>
  int(3616146466484062189)
  ["is_dir"]=>
  bool(false)
  ["realpath"]=>
  string(28) "/app/before/SampleClass1.php" // シンボリックリンク解決済のパスがキャッシュされる
  ["expires"]=>
  int(1733634941)
}

// var_dump((new ReflectionClass("SampleClass1"))->getFileName());
string(28) "/app/before/SampleClass1.php" // シンボリックリンク解決済のパスが取得できる

②アプリケーションコードの動作検証

ここでは実際のアプリケーションが実行されるパターンを洗い出し、新旧コードが混在することがないかを確認しました。 例えば私達のプロダクトでは、Webアクセス・CLI実行(バッチ)・メール受信からの起動 などのパターンがありました。

それぞれパターンについて、処理の何処かでsleepを仕込み、実行中にシンボリックリンクの切替を行い検証します。

③本番想定のアクセス下の動作検証

アプリケーションコードを利用するという点では②と同じですが、検証したいポイントが異なります。

②ではsleepを仕込んだ単発処理で、理論的上問題がないかを検証しました。 それに対して③では、アプリケーションの通常利用中にシンボリックリンクを継続的に切り替えて問題がないかを検証します。

イメージとしては同じバージョンのディレクトリを2つ用意し、そのうち片方にメソッド・クラスのインタフェース変更を行い、従来のrsyncによるデプロイではエラーになる状態を作ります。 その状態でシンボリックリンクの向き先を新旧切り替え続け、アプリケーション操作でエラーが発生しないかを確認します。 またアプリケーション操作は、リリース時に利用している動作確認ツールがありましたので、それを流し続けることで実現しました。

アプリケーションの改修内容

詳細は長くなるため、検証結果はまた別の機会にします。 特に①のサンプルコードも合わせて記載できるように頑張ります。

検証結果を踏まえて、アプリケーションの改修が必要な点は、requireに指定されるファイルパスが、すべて「シンボリックリンク解決済みのパス」となるようにすることです。

例えばComposerで生成されたautoloaderを利用している場合、autoloader経由で読み込まれるファイルは特別な考慮を必要としません。 autoload*.phpでは__DIR__を基にパスを組み立てているため、シンボリックリンク切替の影響を受けません。(PHP__DIR__で取得できるパスはシンボリックリンク解決済みのパス)

一方で、autoloader自体を読み込む際のパス指定には注意が必要です。

// OK例
require_once(__DIR__ . "/vendor/autoload.php");
// NG例
require_once("/path/to/project/vendor/autoload.php");
require_once(ini_get("user_dir") . "/vendor/autoload.php");

上記NG例のようにシンボリックリンクを含むパスになっていると、その処理タイミングでシンボリックリンク切替が重なると、新旧入り混じった状態でアプリケーションが動いてしまう可能性があります。

特にWebアクセスにおいては、realpathキャッシュがリクエストをまたいで共有されるケースがある(処理されたApacheのプロセスが同じ場合は共有される)ため、分かりづらい形でエラーになってしまうことがあります。

おわりに

私達のプロダクトで「シンボリックリンク切替によるデプロイ」を導入したときの検証内容・アプリケーション改修内容をご紹介しました。

realpathキャッシュの挙動調査やApacheのプロセスとの関連など、記事のボリュームの都合で書ききれなかった部分については、今後別記事で詳しくご紹介します。

Copyright © RAKUS Co., Ltd. All rights reserved.