ほぼすべての PHP プログラマーがコードを公開しており、そのコードは ftp または rsync を介して同期したり、svn または git を介して更新したりできます。活発なプロジェクトでは、1 日に数回コードがリリースされることがありますが、詳細に注意を払う人はほとんどいないのが現実で、実際には多くの落とし穴があり、知らないうちに落とし穴にはまっている可能性があります。
適切に実装されたパブリッシング システムは、少なくともアトミック パブリッシングをサポートする必要があります。各バージョンが独立した状態を表す場合、リリース期間中は、すべてのリクエストは単一の状態でのみ実行できます。これは、アトミック リリースのサポートと呼ばれます。逆に、リクエストがリリース中に異なる状態にまたがる場合、それはアトミック リリースとは言えません。例として、a.php と b.php という 2 つの PHP ファイルをリクエストに含める必要があるとします。a.php の組み込みが完了したら、コードを解放し、次に b.php を含めます。が適切に処理されない場合、古いバージョンの a.php と新しいバージョンの b.php が同じリクエスト内に同時に存在する可能性があります。つまり、アトミック リリースが実装されていません。
オープンソースの世界には、Ruby コミュニティの capistrano など、優れたコード公開ツールが多数あります。そのプロセスは、大まかに言うと、コードを新しいディレクトリに公開し、実際のリリース ディレクトリにソフト リンクすることです。
.├── current -> releases/v1└── releases ├── v1 │ ├── foo.php │ └── bar.php └── v2 ├── foo.php └── bar.php
ただし、PHP 自体の特殊性を考慮すると、上記のプロセスを単純に適用するだけでは真のアトミック パブリッシングを実現することは困難です。理由を明確にするためには、PHP の 2 つのキャッシュの概念も理解する必要があります:
まずオペコード キャッシュについて話しましょう。以前は誰もが APC を使用していましたが、現在ではほとんどのキャッシュが使用されています。言うまでもなく、誰もがその機能に精通していますが、唯一注意する必要があるのは、apc と zend オペコードではキャッシュ キーの選択肢が異なるということです。apc はファイルの i ノードを選択し、zend オペコードは選択します。ファイルのパスを選択します。
realpath キャッシュについてもう一度話しましょう。その機能は、ほとんどの場合、ファイル情報を取得するための IO 操作をバッファすることであるため、多くの人はその存在を知りません。 realpath キャッシュはプロセス レベルです。つまり、各 php-fpm プロセスには独自の独立した realpath キャッシュがあります。
関連する技術的な詳細は非常に簡単なので、次の情報を注意深く読むことをお勧めします:
実際、このような問題が発生する主な理由は、ソフト リンクが新しい場所を指していても、古いデータがまだリアルパス キャッシュに保存されている場合、オペコード キャッシュがリアルパス キャッシュを通じてファイル情報を取得するためです。オペコード キャッシュはまだ新しいコードの存在を認識できません。デフォルトでは、realpath_cache_ttl キャッシュの有効期間は 2 分です。つまり、コードが公開されてから有効になるまでに 2 分かかる場合があります。リリースをできるだけ早く有効にするには、リアルパス キャッシュをプロセス単位でクリアする必要があります:
<?php$key = 'php.pid_' . getmypid();if (($rev = apc_fetch($key)) != DEPLOY_VERSION) { if($rev < DEPLOY_VERSION) { apc_store($key, DEPLOY_VERSION); } clearstatcache(true);}?>
これを分析した後、よく考えてみてください。PHP においてアトミック パブリッシングが厄介な問題である理由は、最終的にはソフト リンクとキャッシュの間の矛盾によるものです。オペコード キャッシュであってもリアルパス キャッシュであっても、これらは PHP に固有のキャッシュ機能であり、客観的なニーズに基づいて回避することはできません。では、ソフト リンクを回避してマジノの防御線にする方法はあるのでしょうか。答えは nginx の $realpath_root です:
fastcgi_param SCRIPT_FILENAME $realpath_root $fastcgi_script_name;fastcgi_param DOCUMENT_ROOT $realpath_root;
在本例中,压测发现使用 $realpath_root 后,性能下降了大约 5% 左右,不过明眼人一下就能发现,虽然 $realpath_root 导致了 lstat 和 readlink 操作,但是 lstat 操作的次数是和目录深度成正比的,也就是说目录越深,执行的 lstat 次数越多,性能下降也就越大。如果能够降低发布目录的深度,那么可以预计性能下降能够控制在 1% 左右。
结尾介绍一下 Deployer ,它是 PHP 中做得比较好的工具,有很多特色,比如支持并行发布,具体演示如下图,左边是串行,右边是并行,使用「vvv」能得到更详细信息:
deploy
不过 Deployer 在原子发布上有一点瑕疵,具体见 deploy:release 代码:
<?phprun("cd {{deploy_path}} && if [ -h release ]; then rm release; fi");run("ln -s $releasePath {{deploy_path}}/release");?>
也就是说,在切换软链接的时候,它是先删除再创建,是一个两步操作,理论上如果有请求在这两步中间进入的话,那么将会出现找不到文件的错误。
问题到这里,大部分人会觉得使用「ln -sfn」就好了,实际上也是错误的:
shell> strace ln -sfn releases/foo currentsymlink("releases/foo", "current") = -1 EEXIST (File exists)unlink("current") = 0symlink("releases/foo", "current") = 0
通过 strace 我们能清晰的看到,虽然表面上使用「ln -sfn」是一步操作,但是内部依然是按照先删除再创建的逻辑执行的,实际上这里应该搭配使用「ln & mv」:
shell> ln -sfn releases/foo current.tmpshell> mv -fT current.tmp current
先通过 ln 创建一个临时的软链接,再通过 mv 实现原子操作,此时如果使用 strace 监控,会发现 mv 的「T」选项实际上仅仅执行了一个 rename 操作,所以是原子的。