[php] 如何正确发布 PHP 代码

如何正确发布PHP代码

几乎每个 PHP 程序员都发布过代码,多是经过 FTP 或者 **rsync ** 同步的,也多是经过 svn 或者 git 更新的。一个活跃的项目可能天天都要发布若干次代码,可是现实倒是不多有人注意其中的细节,实际上这里面有好多坑,极可能你就在坑中却浑然不知。php

一个正确实现的发布系统至少应该支持原子发布。若是说每个版本都表示一个独立的状态的话,那么在发布期间,任何一次请求只能在单一状态下被执行。如此称之为支持原子发布;反之若是在发布期间,一次请求跨越不一样的状态,那么就不能称之为原子发布。咱们不妨举个例子来讲明一下:假设一次请求须要 include 两个 PHP 文件,分别是 a.phpb.php,当 include a.php 完成后,发布代码,接着 include b.php,若是处理不当的话,那么就可能会致使旧版本的 a.php 和新版本的 b.php 同时存在于同一个请求之中,换句话说就是没有实现原子发布。html

开源世界里有不少不错的发布代码工具,好比 ruby 社区的 capistrano,其流程大体就是发布代码到一个全新的目录,而后再软连接到真正的发布目录。node

├── current -> releases/v1
└── releases
    ├── v1
    │   ├── foo.php
    │   └── bar.php
    └── v2
        ├── foo.php
        └── bar.php

不过鉴于 PHP 自己的特殊性,若是只是简单套用上面的流程,那么将很难实现真正的原子发布。要理清个中原因,还须要了解一下 PHP 中的两个 Cache 的概念:nginx

  • opcode cache
  • realpath cache

先聊聊 opcode cache,基本就是 apc 或者 zend opcode,关于它的做用,你们都已经很熟悉,没必要多言,须要注意的是 apc 的 bug 不少,好比开启了 apc.enable_cli 配置后就会有不少灵异问题,因此说 opcode cache 仍是尽量使用 zend opcache 吧,若是须要缓存数据,能够用 apcu。此外 apczend opcode 对缓存键的选择有所差别:apc 选择的是文件的 inodezend opcode 选择的是文件的 pathgit

再聊聊 realpath cache,它的做用是缓冲获取文件信息的 IO 操做,大多数时候它对咱们而言是透明的,以致于不少人都不知道它的存在,须要注意的是 realpath cache 是进程级别的,也就是说,每个 php-fpm 进程都有本身独立的 realpath cache程序员

假设在发布代码期间,opcode cache 或者 realpath cache 里的数据出现过时,那么就会出现一部分缓存是旧文件,一部分缓存是新文件的非原子发布的状况,为了不出现这种状况,咱们应该保证缓存过时时间足够长,最好是除非咱们手动刷新,不然永远不过时,对应到配置上就是:关闭 apc.statopcache.validate_timestamps 配置,设置足够大的 realpath_cache_sizerealpath_cache_ttl 配置,必要的监控老是有好处的。github

相关的技术细节特别琐碎,建议你们仔细阅读以下资料:web

在采用软连接发布代码的时候,一般遇到的第一个问题多半是新代码不生效!即使调用了 apc_clear_cache 或者 opcache_reset 方法也无效,重启 php-fpm 天然是可以解决问题,不过对脚本语言来讲重启过重了!难道除了重启就没有别的办法了么?shell

事实上之因此会出现这样的问题,主要是由于 opcode cache 是经过 realpath cache 获取文件信息,即使软连接已经指向了新位置,可是若是 realpath cache 里还保存着旧数据的话,opcode cache 依然没法知道新代码的存在,缺省状况下,realpath_cache_ttl 缓存有效期是两分钟,这意味着发布代码后,可能要两分钟才能生效。为了让发布尽快生效,须要以进程为单位清除 realpath cacheapi

<?php

	$key = 'php.pid_' . getmypid();
	
	if (($rev = apc_fetch($key)) != DEPLOY_VERSION) {
	    if($rev < DEPLOY_VERSION) {
	        apc_store($key, DEPLOY_VERSION);
	    }
	    
	    clearstatcache(true);
	}

如此在 apc 环境下基本就能工做了,可是在 zend opcode 环境下还可能有问题。由于在缺省状况下 opcache.revalidate_path 是关闭的,此时会缓存未解析的符号连接的值,这会致使即使软连接指向修改了,也没法生效,因此在使用 zend opcode 的时候,若是使用了软连接,视状况可能须要把 opcache.revalidate_path 激活。

详细介绍参考:PHP’s OPCache extension review

BTW:若是须要手动重置 opcode cache,须要注意的是由于它是基于 SAPI 的概念,因此不能直接在命令行下调用 apc_clear_cache 或者 opcache_reset 方法来重置缓存,固然办法老是有的,那就是使用 CacheTool 在命令行下模拟 fastcgi 请求。

分析到这里,咱们不妨反思一下:在 PHP 中原子发布之因此是一个棘手的问题,归根结底是由于软连接和缓存之间的的矛盾。不论是 opcode cache 仍是 realpath cache,都是 PHP 固有的缓存特性,基于客观须要没法绕开,如此说来是否有办法绕开软连接,使其成为马奇诺防线呢?答案是 NGINX$realpath_root

fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
	fastcgi_param DOCUMENT_ROOT $realpath_root;

有了 $realpath_root,即使 DOCUMENT_ROOT 目录中含有软连接,NGINX 也会把软连接指向的真正的路径发给 PHP,也就是说,对 PHP 而言,软连接已经不存在了!不过做为代价,每一次请求,NGINX 都要经过相对昂贵的 IO 操做获取 $realpath_root 的值,经过 strace 命令咱们能监控这一过程,下图从 currentfoo 的过程:

realpath

在本例中,压测发现使用 $realpath_root 后,性能降低了大约 **5% **左右,不过明眼人一下就能发现,虽然 $realpath_root 致使了 lstatreadlink 操做,可是 lstat 操做的次数是和目录深度成正比的,也就是说目录越深,执行的 lstat 次数越多,性能降低也就越大。若是可以下降发布目录的深度,那么能够预计还能下降一些性能损耗。

结尾介绍一下 Deployer,它是 PHP 中作得比较好的工具,有不少特点,好比支持并行发布,具体演示以下图,左边是串行,右边是并行,使用「vvv」能获得更详细信息:

deploy

不过 Deployer 在原子发布上有一点瑕疵,具体见 release/symlink 代码:

<?php

// deploy:release
run("cd {{deploy_path}} && if [ -h release ]; then rm release; fi");
run("ln -s $releasePath {{deploy_path}}/release");
// deploy:symlink
run("cd {{deploy_path}} && ln -sfn {{release_path}} current");
run("cd {{deploy_path}} && rm release");

?>

release 的时候,它是先删除再建立,是一个两步的非原子操做,在 symlink 的时候,看上去「ln -sfn」是单步原子操做,实际上也是错误的:

shell> strace ln -sfn releases/foo current
symlink("releases/foo", "current")      = -1 EEXIST (File exists)
unlink("current")                       = 0
symlink("releases/foo", "current")      = 0

经过 strace 咱们能清晰的看到,虽然表面上使用「ln -sfn」是一步操做,可是内部依然是按照先删除再建立的逻辑执行的,实际上这里应该搭配使用「ln & mv」

shell> ln -sfn releases/foo current.tmp
shell> mv -fT current.tmp current

先经过 ln 建立一个临时的软连接,再经过 mv 实现原子操做,此时若是使用 strace 监控,会发现 mv「T」 选项实际上仅仅执行了一个 rename 操做,因此是原子的。

BTW:在使用「ln -sfn」先后,若是使用 stat 查看新旧文件的 inode 的话,可能会发现它们拥有同样的 inode 值,看上去和咱们的结论相悖,其实否则,实际上只是复用删除值而已(若是想验证,注意 Linux 会复用,Mac 不会复用)。

听说一千我的的心中就有一千个哈姆雷特,不过我但愿全部的 PHP 程序员在发布 PHP 代码的时候都能采用一种方法,那就是本文介绍的方法,正确的方法。

原文转自老王的火丁笔记,原文地址:如何正确发布PHP代码 ;若有侵权请告知删除。