关于上线系统的一些想法 (for php)

0X00. 起因

最近在升级php7, php7的opcache的一个选项opcache.revalidate_freq的默认值是60,之前线上php5.5.6中opcache的opcache.revalidate_freq设置的为2。然后在上线代码的时候总是会出现php的error, 这种情况被称为”瞬报”。 当上线的文件较多的时候,这种错误可能会持续很长时间,但是op也不太care, 总觉得无所谓,所以一直也没处理,只需要在上线的时候在群里通报一声:”我在上线,有php-error的话属于瞬报,无视即可”。好尴尬,哈哈。

0X01. 现象

上线的时候,收到php-error后,通常都很紧张,这尼玛难道有问题,要回滚啊!!!赶紧看看php-error是什么造成的。打开一看尼玛A文件的a方法调用B文件中的b方法是出错,说B文件中不存在b这个方法,怎么可能我tmd这次上线新加的b方法啊,连水都没顾得上喝,赶紧去打开B文件看,果然B文件里面有b方法。

0X02. 思考原因

出现这种问题的原因是什么呢?罪魁祸首是什么呢?当然最根本的原因就是上线是非原子的。所谓上线代码的原子性即你上线的文件要同时生效不能存在你上线的A文件调用的是上线之前版本的B文件,这样导致最终的结果是跟预期相悖的,也是不可预测的。

那么导致这个问题出现的原因有几种呢?

  • opcache缓存问题
  • php文件cache问题

0X03. opcache缓存问题

说到opcache,目前最常用的有后面几种apc、 eaccelerator、 ZendOpcache,apc 的 bug 很多,比如开启了apc.enable_cli 配置后就会有很多灵异问题。随着ZendOpcache被内置到官方的PHP版本中,ZendOpcache使用也越来越广泛,当然效果也是非常好的。所以还是使用ZendOpcache吧。此外apc和ZendOpcache对文件的缓存键也不同,apc按照文件的inode,Zendopcache选择了文件的path。

接上面,如果A的opcache先过期,B的后过期,就可能导致上面说的那种非”原子发布”导致不可预知的问题。

0X04. php文件cache问题

这里说的不是普通的文件缓存,说的是realpath缓存,php在include或者requrire文件的时候并不是每次都去通过stat系统调用判断文件是否存在,如果存在就加载进内存,加载过之后php对文件会进行一个优化,PHP会根据文件的path去缓存该文件的stats信息,通过如下代码可以验证

1
2
3
4
5
$f = @file_get_contents('/tmp/bar.php');
echo "hello";
var_dump(realpath_cache_get());

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
hello
array(5) {
["/home/julien.pauli/www/realpath_example.php"]=>
array(4) {
["key"]=>
float(1.7251638834424E+19)
["is_dir"]=>
bool(false)
["realpath"]=>
string(43) "/home/julien.pauli/www/realpath_example.php"
["expires"]=>
int(1404137986)
}
["/home"]=>
array(4) {
["key"]=>
int(4353355791257440477)
["is_dir"]=>
bool(true)
["realpath"]=>
string(5) "/home"
["expires"]=>
int(1404137986)
}
["/home/julien.pauli"]=>
array(4) {
["key"]=>
int(159282770203332178)
["is_dir"]=>
bool(true)
["realpath"]=>
string(18) "/home/julien.pauli"
["expires"]=>
int(1404137986)
}
["/tmp"]=>
array(4) {
["key"]=>
float(1.6709564980243E+19)
["is_dir"]=>
bool(true)
["realpath"]=>
string(4) "/tmp"
["expires"]=>
int(1404137986)
}
["/home/julien.pauli/www"]=>
array(4) {
["key"]=>
int(5178407966190555102)
["is_dir"]=>
bool(true)
["realpath"]=>
string(22) "/home/julien.pauli/www"
["expires"]=>
int(1404137986)

你可以使用clearstatcache(true)清空这个array,强制PHP去通过stat()系统调用获取文件的信息,尽管之前已经被访问过。另外realpath是进程级别的,非共享内存级别。更多reaplpath cache相关的东西可以参考 http://jpauli.github.io/2014/06/30/realpath-cache.html

这块主要是影响那些通过修改软链接形式的上线系统,因为realpath cache的存在可能导致代码不生效。

0X05. 解决方案

以上两种问题都需要考虑如何解决,如果是通过修改软链接的形式上线的,需要同时处理realpath cache、opcache的缓存,否则只需要处理opcache缓存即可。

当然方案都是两种一种是自动过期、一种是手动过期。

  • realpath cache

自动过期

可以通过修改php.ini里 realpath_cache_size、realpath_cache_ttl实现。

当然如果你使用的nginx + php-fpm的形式,你可以通过修改nginx的fastcgi_params实现,配置如下:

1
2
fastcgi_param SCRIPT_FILENAME $realpath_root $fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;

这样当你修改网站目录的软链时,nginx会自己判断你网站根目录的真实路径,当然代价还是stat的系统调用,只不过把php的stat转移到了nginx。

软链的方法推荐使用ln -sfn处理。

手动过期
可以通过调用clearstatcache(true)去清除

  • opcache

自动过期

如果你使用的是ZendOpcache 你可以设置opcache.validate_timestamps=1,opcache.revalidate_freq=0 当然代价是每次请求都回去检查文件的最后修改时间,无谓增加一堆stat系统调用,性能可想而知。

手动过期

可以通过设置opcache.validate_timestamps=0去关闭ZendOpcache的自动检测文件是否更新操作,上线完毕后手工调用opcache_reset()或者opcache_invalidate(string $script [, boolean $force = FALSE ] ),需要注意的是opcache_reset是重置所有的opcache,或者opcache_invalidate可以针对单个文件。当然最粗暴的方法就是重启fpm进行(Kill SIGUSR2强制也可以),性能会有些许损失,还是建议通过清楚opcache的方式去处理。

BTW:如果需要手动重置 opcode cache,需要注意的是因为它是基于 SAPI 的概念,所以不能直接在命令行下调用 apc_clear_cache 或者 opcache_reset 方法来重置缓存,当然办法总是有的,那就是使用 CacheTool 在命令行下模拟 fastcgi 请求。

0X06. 总结

把opcache、realpath尽可能的设置的时间长一些或者永不自动过期,通过手动清除的方式清楚去清除。