PHP使用APCu做用户缓存

PHP加速器,有很多,目前的现状,可参考 https://en.wikipedia.org/wiki/List_of_PHP_accelerators

APC

先说APC,全称Alternative PHP Cache,参考 http://pecl.php.net/package/APC ,这是APCu的前生,更新到2012年3.1.13版本,就没有再更新了,为什么没有更新了,一说它有一些BUG,比如进程意外结束不能解锁等,但是所有软件都是有BUG的,根本的原因应该是受到Zend OpCache的冲击,科班出身的Zend OpCache已经做得很好了,这外来的孩子变没得玩了。

APC提供的功能,主要有二,一是 OpCode Cache, 一是 user cache。

  1. OpCode Cache
    OpCode就是缓存PHP源文件分析后编译成可供解释器执行的中间码,类似于JAVA的Bytecodes,类似于CPU执行的汇编指令,此功能推荐用Zend OpCache ( https://github.com/zendtech/ZendOptimizerPlus )替代。
  2. user cache
    用户可以通过APC接口设置一些缓存项,这功能非常有用,比如,载入一个资源文件,每个PHP进程都要打开文件,读文件,如果我将这个资源设置到Cache里,从Cache读取,自然是比原有的方式快的。
    很遗憾,Zend OpCache没有提供user cache操作接口,所以,APC的这个优秀功能以阉割了OpCode Cache的方式被保留了下来,叫做APCu(APC User Cache)。
    对于user cache,APCu使用的是共享内存,本机使用,速度非常快,缺点是不能跨主机,如果你想要跨主机的方案,可以考虑Memcache或redis,但是走网络,速度相较APCu慢。

APCu 安装

安装的关键是APCu版本要和自己的PHP版本匹配,我的PHP版本是5.6.15,通过APCu官网 http://pecl.php.net/package/APCu 看不出应该下载哪个版本,我是点开为windows预编译的描述 https://pecl.php.net/package/APCu/4.0.11/windows 后才发现,4.0.11适合PHP 5.6,而5.x版本应该是为PHP 7.x准备的。于是下载 https://pecl.php.net/get/apcu-4.0.11.tgz ,解压,进入解压后的目录。
编译安装和之前安装xdebug类似,参考 https://mnstory.net/2017/10/14/phpstorm-remote-debug-with-xdebug ,首先执行phpize:

1
2
3
4
5
[compiler apcu-4.0.11]# $inst/php5/bin/phpize
Configuring for:
PHP Api Version: 20131106
Zend Module Api No: 20131226
Zend Extension Api No: 220131226

然后执行configure:

1
[compiler apcu-4.0.11]# ./configure --with-php-config=$inst/php5/bin/php-config

再执行make install:

1
2
3
[compiler apcu-4.0.11]# make && make install
...
Installing shared extensions: $inst/php5/lib/php/extensions/no-debug-non-zts-20131226/

从上述Installing shared extensions目录中取出编译好的apcu.so,复制到目标主机的$phproot/lib/php/extensions目录,修改php.ini,添加:

1
2
3
4
5
[apc]
extension = apcu.so
apc.enabled = on
apc.shm_size = 256M
apc.enable_cli = on

重启Apache后,可以通过phpinfo()查看是否加载了APCu模块。

使用

我有多种Cache切换的需求,所以,先封装了一个CacheDriver抽象类,然后在 APCuCacheDriver 里面实现get和put方法。

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
<?php
abstract class CacheDriver {
protected $prefix;
public function __construct($prefix) {
$this->prefix = $prefix;
}
abstract public function get($id, $paramKey);
# set ttl at put stage, has one benefit: cacher system can delete timeout entry automatically
abstract public function put($id, $paramKey, $data, $ttl);
//abstract public function delete($id);
}
class APCuCacheDriver extends CacheDriver {
public function __construct($prefix = "your-prefix-or-table-name") {
parent::__construct($prefix);
}
public function get($id, $paramKey) {
$success = false;
$stored = apc_fetch($id, $success);
if ($success === false || count($stored) !== 4) {
ldebug("cache $id ".($success === false ? "not found":"found, but result ".print_r($stored, true)." is not right"));
return null;
}
list($updateTime, $ttl, $cacheParamKey, $data) = $stored;
$now = time();
if ($now < $updateTime || (($now - $updateTime) > $ttl)) {
ldebug("cache $id found, but timeout ".($now - $updateTime)." out of ttl $ttl");
return null;
}
if ($paramKey && $cacheParamKey != $paramKey) {
ldebug("cache $id found, but paramKey not equal, r=$paramKey, c=$cacheParamKey");
return null;
}
return unserialize($data);
}
public function put($id, $paramKey, $data, $ttl) {
if (null == $data) {
lerror("put $id to cache, found invalid param, null data");
return false;
}
return apcu_store (
$id,
array(time(), $ttl, $paramKey, serialize($data)),
$ttl
);
}
}
class Cacher {
private $driver;
public function __construct() {
$this->driver = new APCuCacheDriver();
}
public function call($callable, $params, $cacheParams = array()) {
// no need cache
if (empty($cacheParams) || !array_key_exists('id', $cacheParams)) {
return call_user_func_array($callable, $params);
}
$startTime = time();
$paramKey = array_key_exists('paramKey', $cacheParams) ? $cacheParams['paramKey'] : json_encode($params);
$ttl = array_key_exists('ttl', $cacheParams) ? $cacheParams['ttl'] : 180;
$data = $this->driver->get($cacheParams['id'], $paramKey);
if (null != $data) {
ldebug("Hit ".$cacheParams['id'].": cost " .(time() - $startTime). " seconds. data=".print_r($data, true));
return $data;
}
$data = call_user_func_array($callable, $params);
if (null != $data) {
$this->driver->put($cacheParams['id'], $paramKey, $data, $ttl);
}
linfo("NOT Hit ".$cacheParams['id'].": cost " .(time() - $startTime). " seconds. data=".print_r($data, true));
return $data;
}
}

上面的paramKey参数是我自己的需求,类似于同样的ID下,可能会有不同的请求参数,这样Cache Entry即便命中,也只能作废,一般情况不需要这么考虑。剩下要做的,就是调用Cacher API:

1
2
3
4
5
6
7
8
9
10
11
12
// an example, when use elasticsearch, call it like this!
function query($params = array(), $cacheParams = array())
{
static $es = null;
if (null == $es) {
$es = ClientBuilder::create()->build();
}
$cacher = new Cacher();
return $cacher->call(array($es, 'search'), array($params), $cacheParams);
}
query($queryParams, array('id'=>'your-cache-id'));

Cache效果报表

https://raw.githubusercontent.com/krakjoe/apcu/master/apc.php 文件下载到 Apache 的 Document Root下,编辑下文件,如果是自己临时使用,可以关闭认证:

1
defaults('USE_AUTHENTICATION',0);

然后浏览器打开,即可看到APCu缓存相关信息:
view-host-stats

查看用户缓存项:
user-cache-entries