PHP OPCacheのせいですか?







開発のキャリアを始めたとき、 Phil Karltonに起因するフレーズを読んで非常に驚きました。 フレーズの本質を理解していなかったので、これを信じられないほど取りました。 しかし、少し後に私は理解し始めました。







私たちの生産インフラストラクチャで、さほど前に遭遇した問題について話したいです。 展開が成功した直後に、新しいリリースで変更されたページを更新すると、新しいコードがしばらく表示されませんでした。 実際、これはPHPで作成されたWebアプリケーションでは珍しいことではありません。 私たちはこれに直面していましたが、新しい実稼働環境への移行後、問題はより顕著になりました。 したがって、調査することにしました。







展開手順



私たちの技術は主にPHPで書かれており、 SymfonyZendフレームワークも使用しています。 コードを実稼働環境に送信するには、 shark-do内部プロジェクトを使用します。その作成者はLucaチームのリーダーです。







サメの哲学:







「これを行うことができれば、bashで行うことができます。」

プロジェクトは、タスクを識別してアルゴリズムに従って実行できるbashスクリプトです。 各プロジェクトには、たとえば、不要なファイルの削除、構成ファイルの生成など、さまざまな段階を管理するための独自のアルゴリズムがあります。







たとえば、1日5回以上、 shark-do deploy collaboratori



コマンドを使用して、作業中のcollaboratoriプロジェクトの展開タスクを起動します。 通常、展開は次の手順で構成されます。







  1. 最後のコミットはmasterブランチから取得されます。
  2. フォルダーが構成され、不要なファイルが削除され、リリースの作成が開始されます。
  3. パラメーターが設定され、リンカーのインストールが開始され、リソースがダウンロードされて追加されます。
  4. リリースアーカイブが作成された後、要塞マシンに転送されて展開されます。
  5. インフラストラクチャのREST APIを使用して、リリースのロールバックを開始するためにAnsibleプロシージャが呼び出されます。
  6. システムが新しいリリースに切り替わり、古いリリースがクリアされて要塞マシンから削除されます。
  7. 新しいリリースはNew Relicで祝われ、Slackチャネルに展開タスクの完了に関する通知が表示されます。


5番目のステップを検討してください。 Ansibleスクリプトは応答します:









各展開手順は多くの必要な操作で構成されていますが、転換点は現在のプロジェクトフォルダーの変更です。これは、以前のリリースフォルダーから新しいものへのシンボリックリンクを使用して行われます。 現在のプロジェクトフォルダは、特定のWebアプリケーションのドキュメントのルート場所です。







例:







 ln -sf /var/www/{APP_NAME}/releases/@YYYYMMDDHHIISS /var/www/{APP_NAME}/current
      
      





-s



、シンボリックリンクを作成するために使用され、 -f



は、ターゲットが既に存在する場合にそのようなリンクの作成を強制するために使用されます。 {APP_NAME}



はプロジェクトの名前です。







PHPの標準的な展開戦略を使用します。 1つのアプリケーションのリリースは運用サーバーに保存され、シンボリックリンクを介して現在のバージョンを参照します。 これにより、作業トラフィックに影響を与えることなく、 アトミックかつ安全に展開できます。







最後に、ラウンドロビンポリシーを使用したバランサーの背後には、15台のフロントエンドサーバーがあります(以前の2.5倍)。 質問:リリース切り替え後はどうなりますか?







PHP OPCache(?)のせい



いくつかの注意点:PHPスクリプトの流れについては掘り下げませんが、問題についての私の理由を理解しやすくするための主な事項について説明します。 また、PHP 7のみを検討します。







PHPコードの実行方法を覚えておくと便利な場合があります。 スクリプトが開始されると、ソースコードは4つの段階を経ます。













最初のフェーズは、 PHP字句解析プログラムによって制御されます 。 彼は、 function



return



static



などの言語キーワードを、通常tokensと呼ばれる個々の部分と一致させる責任がありstatic



。 多くの場合、各トークンには次のフェーズに必要なメタデータが追加されます。







2番目のフェーズは、PHPパーサーによって制御されます。 彼は、1つ以上のトークンの分析と、それらを言語構造のテンプレートと比較する責任があります。 たとえば、 $foo + 5



バイナリの「加算」演算として認識され、 $foo



と数値5



オペランドとして認識されます。 パーサーは、 抽象構文ツリー(AST)を再帰的に構築します 。 通常、字句解析プログラムとパーサーの作業は1つのタスクと見なされます。







3番目のフェーズはコンパイルです。 ASTは、命令コードの順序付きシーケンスに変換されます。 各オペコードは、 Zend仮想マシンの低レベルの操作と考えることができます。 サポートされているオペコードの完全なリストは、 ここにあります







最後に、最後のフェーズは実行です。 Zend VMは、オペコードに記述されている各タスクを実行し、結果を生成します。







最初の3つのフェーズ(字句解析、パーサー、コンパイラ)は「パイプライン」に結合されます。 さらに、第3フェーズではより多くの時間がかかり、より多くのリソース(メモリとプロセッサ)を消費します。 コンパイルフェーズの重みを減らすために、PHP 5.5でZend OPCache拡張機能が導入されました。 コンパイルフェーズの出力(オペコード)を共有メモリ(shm、mmapなど)にキャッシュするため、各PHPスクリプトは一度だけコンパイルされ、コンパイルフェーズなしで異なるリクエストを実行できます。 開発用ではない環境でコードがめったに変更されない場合、PHPの実行速度は少なくとも2倍になります。







OPCache拡張機能もオペコードの最適化を担当しますが、これはすでにこの記事の範囲外です。







上記に関連して、実稼働環境で遭遇した奇妙な動作の原因はOPCacheにあると考えるのが論理的です。 この仮定をテストするために、Dockerコンテナー、PHP 7.0およびApache 2.4から簡単なデモ環境を作成しました。 完全なコードはこちらからダウンロードできます







作業を簡素化するために、いくつかのスクリプトを作成しました。









GitHubリポジトリのクローンを作成するだけで、Dockerがすでにインストールされているかどうかを確認できます。







 git clone https://github.com/salvatorecordiano/facile-it-realpath_cache cd facile-it-realpath_cache docker pull salvatorecordiano/realpath_cache
      
      





キャッシュの問題を再現するには、これらのコマンドを3つの異なるコマンドラインで並行して実行する必要があります。







 # start the container with production configuration ./start.sh production # start switching the current release ./release-switcher.sh # start watching the current web server response ./release-watcher.sh
      
      





実行結果:









実稼働構成での実行。







キャッシュの問題が繰り返されました。リリースを切り替えた後、HTTPリクエストの後に正しいコードが表示されません。







OPCacheを無効にして、テストを繰り返します。







 # start the container with production configuration and opcache disabled ./start.sh production-no-opcache # start switching the current release ./release-switcher.sh # start watching the current web server response ./release-watcher.sh
      
      







実稼働no-opcache構成での実行。







驚くべきことに、問題が残っていたため、 OPCacheは何のせいでもないという仮定は誤りでした。







realpath_cache:本当の犯人



おそらくinclude/require



関数またはPHPスタートアップを使用するとき、 realpath_cacheについて考える必要があります。 実際のパスキャッシュを使用する、ファイルとフォルダーのパスアクセス許可をキャッシュできるため、ディスクの検索に費やす時間が短縮され、パフォーマンスが向上します。 Symfony、Zend、Laravelなどの多くのサードパーティライブラリまたはフレームワークを使用する場合、これは非常に多くのファイルを使用するため、非常に便利です。







キャッシュメカニズムは、PHP 5.1.0で登場しました。 現在、この機能は公式ドキュメントでは言及されていませんが、関数realpath_cache_get()



realpath_cache_size()



clearstatcache()



およびphp.ini



パラメーターrealpath_cache_size



およびrealpath_cache_ttl



を除きます。 外部ソースからは、2014年にJulien Paulが書いた古い投稿しか見つかりませんでした。 有名なPHP開発者であるPaulieが、パス解決メカニズムの仕組みを説明しています。







ファイルにアクセスすると、PHPはstat()



、Unixシステムコールを使用してパスの解決を試みますstat()



適用されるファイル属性(アクセス権、拡張子、およびその他のメタデータ)を返します。 Unixの世界では、iノードはファイルやディレクトリなどのファイルシステムオブジェクトを記述するために使用されるデータ構造です。 PHPは、システムコールの結果を、権限や所有者などの例外を除き、 realpath_cache_bucket



というデータ構造に入れます。 したがって、同じファイルに再度アクセスしようとすると、メモリ内のバケットを検索するとき(バケット検索)、別の遅いシステムコールが保存されます。 詳細を知りたい場合は、 PHPソースコードをご覧ください。







realpath_cache_get



関数は、PHP 5.3.2で登場しました。 これにより、実際のパスのキャッシュエントリで構成される配列を取得できます。 配列の各要素では、キーは解決されたパスであり、値はkey



is_dir



realpath



is_dir



などのデータを持つ別の配列です。







次に、出力print_r(realpath_cache_get())



が来ます。 テストドッカー環境で:







 Array ( [/var/www/html] => Array ( [key] => 1438560323331296433 [is_dir] => 1 [realpath] => /var/www/html [expires] => 1504549899 ) [/var/www] => Array ( [key] => 1.5408950988325E+19 [is_dir] => 1 [realpath] => /var/www [expires] => 1504549899 ) [/var] => Array ( [key] => 1.6710127960665E+19 [is_dir] => 1 [realpath] => /var [expires] => 1504549899 ) [/var/www/html/release1] => Array ( [key] => 7631224517412515240 [is_dir] => 1 [realpath] => /var/www/html/release1 [expires] => 1504549899 ) [/var/www/current] => Array ( [key] => 1.7062595747834E+19 [is_dir] => 1 [realpath] => /var/www/html/release1 [expires] => 1504549899 ) [/var/www/current/index.php] => Array ( [key] => 6899135167081162414 [is_dir] => 0 [realpath] => /var/www/html/release1/index.php [expires] => 1504549899 ) )
      
      





ここに:









前の例では6つのパスがありましたが、それらはすべて/var/www/current/index.php



パスの解決に関連しています。 PHPは1つのパスのみを解決するために6つのキャッシュキーを作成しました。 そのため、パスは部分に分割され、それぞれが連続的に解決されます。 /var/www/current



/var/www/html/release1



へのシンボリックリンクであるため、この例では、「実際の」パスは/var/www/html/release1/index.php



です。







ジュリアンパウリの投稿には次のようにも書かれています。







「このパスのキャッシュはプロセスにアタッチされており、共有メモリに収まりません。」

これは、 プロセスごとにキャッシュが古くなっていることを意味しますPHP-FPMを使用してWebサーバー全体をクリーンアップする場合、プール内の各ワーカーのキャッシュが期限切れになるのを待つ必要があります。 これは、 production-no-opcache



を使用したテスト中に何が起こるかを理解するのに役立ちproduction-no-opcache



。 シンボリックリンクを受け取った後にOPCacheを無効にしても、PHPはすべてのプロセスにパスの陳腐化をゆっくりと通知します。







実際の運用環境では、多くのWebアプリケーションをホストする15台のフロントエンドサーバーがあることを考慮する必要がありました。 各サーバーには1つのPHP-FPMプールがあり、各プールは35のワーカーと1つのマスタープロセスで構成されています。 これは、なぜ新しい環境で「奇妙な動作」がより顕著になったのかを説明しています。 realpath_cache_size



およびrealpath_cache_ttl



を使用して、Webアプリケーションの現在のパスのキャッシュの効果を調整できrealpath_cache_ttl



。最初のrealpath_cache_ttl



は、PHPが使用するバケットのサイズを決定します。 これは整数であり、この値を大きくすると、膨大な数のファイルを処理するWebアプリケーションに役立ちます。 2番目のパラメーターrealpath_cache_ttl



は、既に述べたように、現在のパスに関する情報をキャッシュする期間(秒単位)です。







すべてが明確になったので、サイズとライフタイムを調整することで、OPCacheを再度有効にし、実際のパスのキャッシュを無効にできます。







 realpath_cache_size=0k realpath_cache_ttl=-1
      
      





テストを再度実行します。







 # start the container with production configuration, opcache enabled and realpath_cache disabled ./start.sh production-no-realpath-cache # start switching the current release ./release-switcher.sh # start watching the current web server response ./release-watcher.sh
      
      







構成production-no-realpath-cacheを使用した実行。







PHPはすべてのパスを許可するように強制されており、パフォーマンスに悪影響を与えるため、本番環境では最後の構成を強くお勧めません







おわりに



私は、神秘的なキャッシュの問題を解決すること、OPCacheとこのパスのキャッシュについて学んだこと、およびそれらの違いについて話をしたかったのです。 記事の冒頭で説明したスクリプトが考案されましたが、たとえば、リクエストがコードの1つのバージョンで始まる場合、実行中に他のファイルにアクセスしようとし、その後のバージョンのコードで更新、移動、削除された場合、実際の問題が発生する可能性があります。 最悪の場合、2つの連続したリリースの互換性を確保する必要がありますが、説明されている条件では、これを達成するのは非常に困難です。







アトミック展開戦略を実装する必要があります(厳密な意味で)。 たとえば、デプロイされたリリースごとにコンテナまたは新しいPHP-FPMサンドボックスを使用できます。 後者の場合、同時に動作するFPMプールを保持できるようにするには、メモリ量を少なくとも2倍にする必要があります。







mod_realdoc



というApacheモジュールを使用して、アトミックデプロイメントをサポートすることもできます。 それはラスマス・ラードルフによって書かれました。 このモジュールにはトリックが実装されています。リクエストの開始時に、シンボリックリンクDOCUMENT_ROOT



を使用して実際のパスが呼び出され、リクエスト全体の絶対パスが実際のドキュメントルートとして設定されます。 したがって、シンボリックリンクが変更される前に開始される要求は、以前のシンボリックリンクターゲットに関連して実行されます。 このモジュールの主な欠点は、 Apache Multi-Processing Module(MPM) preforkを使用する必要があることです このプリフォークは、フォークベースを使用する非スレッドサーバーを実装します。 サーバーは新しいプロセスを作成し、リクエストを処理するためにそれらを保持します。 これは各問題を分離するのに最適なMPMであるため、問題が発生しても、ある要求が他の要求に影響を与えることはありません。 しかし、サーバーの負荷が高い場合、MPMはリクエストごとに1つのプロセスを使用するため、より破損しやすく、同時リクエストの結果として十分なリソースがないため、サーバープロセスが解放されるまで待機する必要があります。 realpath(__FILE__)



でメインルートフォルダーを定義すると、アプリケーションのフロントコントローラーでPHPレベルでmod_realdoc



と同じ結果をmod_realdoc



ことができます。







PHPの前にnginxを使用する場合、幸運です! クエリの実行時にシンボリックリンクが更新されないようにするには、nginxにシンボリックリンクを有効にし、それらをDOCUMENT_ROOT



割り当てる必要があります。 サーバーブロック内の数行のコードを変更するだけで十分です。







 # default configuration fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $document_root; # configuration with real path resolution fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $realpath_root;
      
      





その結果、nginxはシンボリックリンクを許可し、PHPから非表示にします。







これらは、この方法でキャッシュの問題に対処する方法のほんの一部です。 普遍的な「正しい」方法はありません。 要件とインフラストラクチャに応じて、理想的なソリューションを見つける必要があります。







参照資料






All Articles