PHP用户级缓存APCu原理以及用法
一、APC 可选PHP缓存
APC,全称是Alternative PHP Cache,官方翻译叫”可选PHP缓存”。它为我们提供了缓存和优化PHP的中间代码的框架。 APC的缓存分两部分:系统缓存和用户数据缓存。
系统缓存是指APC把PHP文件源码的编译结果缓存起来,然后在每次调用时先对比时间标记。如果未过期,则使用缓存的中间代码运行。默认缓存 3600s。但是这样仍会浪费大量CPU时间。因此可以在php.ini中设置system缓存为永不过期(apc.ttl=0)。不过如果这样设置,改变php代码后需要重启WEB服务器。apc系统缓存目前已经不在维护,如需缓存php字节码可使用opcache扩展。
用户数据缓存由用户在编写PHP代码时用apc_store和apc_fetch函数操作读取、写入的键值对缓存。如果数据量不大的话,可以一试。如果数据量大或者需要跨机器缓存,使用类似memcache或redis等更加专著的内存缓存方案会更好。
APC 扩展其实都是基于 opcode caching ,也就是 PHP 自身的 opcode 来实现的缓存能力。
二、APCu
APCu 是 PHP 版的内存键值存储。 键是 string 类型且值可以为 PHP 任何变量。 APCu 仅支持用户空间(userland)级别的变量缓存。
apcu是基于共享内存技术建设的,多个cgi进程之间访问apcu中的cache可以完全等同于访问自己进程的一块内存一样,不需要发任何的网络请求。而redis和memcache等独立服务的缓存需要进行网络请求,因此apcu的速度远高于redis。
apcu的适用场景和局限性:
1、以扩展的方式接入,跟php这门语言有很强的耦合,而redis作为独立服务存在,使用协议接入
2、apcu受限于单机内存的限制,扩展受阻,而目前的redis集群模式已经可以做到动态扩容,理论上无容量风险。
3、apcu数据存于单机内存,多机器之间的数据无法共享,这使其使用场景有限。
所以一般用apcu来缓存数据量小、但是读取量大或者瞬间读取量大的场景,例如对redis中的热数据做二级缓存以避免缓存雪崩带来的影响和缓解redis服务器的压力。
APCu相关ini配置:
具体配置含义可以参考
https://www.php.net/manual/zh/apcu.configuration.php#ini.apcu.shm-segments
这里只介绍几个最常用的配置:
-
apc.enabled 开启apc 设置为0关闭,1为开启
-
apc.shm_segments 共享内存块数
-
apc.shm_size 共享内存大小,可以设置1G那么显然共享内存的总数就是apc.shm_segments*apc.shm_size
-
apc.num_files_hint 允许多少个opcode被缓存(也就是项目里面有几个php文件)
-
apc.ttl opcode缓存的过期时间,设置为0表示不过期
-
apc.enable_cli apcu缓存是否在cli模式可用,默认不可用。
// APCu的数据存储中检索缓存的信息
apcu_cache_info();
// 检索APCu共享内存分配信息
apcu_sma_info();
// APCu key信息
apcu_key_info();
// 当前环境APCu是否可用
apcu_enabled();
// 设置一个缓存,没有失效时间。再次apcu_add()同一个key,值不会覆盖
apcu_add();
// 删除指定key
apcu_delete();
// 获取指定key缓存
apcu_fetch();
// 设置一个缓存,带有失效时间。失效后还会占用内存空间,需使用apcu_delete()才可以彻底删除。
apcu_store();
// 更新一个key的值
apcu_cas();
// 自增
apcu_inc();
// 自减
apcu_dec();
// 判断key是否存在
apcu_exists();
// 以原子方式获取或生成缓存
apcu_entry();
// 清除全部缓存
apcu_clear_cache();
三、APCu内存共享
APCu用户缓存默认使用mmap实现,因此APCu内的数据可以作为共享内存被多个进程读写。
需要注意:APCu段中的数据只能被父子进程共享,而无法被两个独立的进程共享。
在CLI模式下,即使开启了APCu用户缓存,两个独立的cli进程之间也无法通过APCu共享内存。
在FPM模式下,一个进程往APCu内存段中存储的数据可以被另一个fpm进程访问到,因为这些fpm worker进程都是由fpm master进程fork出来的。
master进程在创建时就已经向系统申请内存通过mmap映射的方式创建了一个APCu用户缓存。
master进程fork子进程时,该APCu用户缓存区域会映射到所有的worker子进程的虚拟内存空间中,这些worker进程通过该ACPu内存段的指针对用户缓存进行共享。
四、关于mmap
参考文章:
1、mmap原理
一般来说,修改一个文件的内容需要如下3个步骤:
把文件内容读入到内存中。
修改内存中的内容。
把内存的数据写入到文件中。
read(fd, buf, 1024); // 读取文件的内容到buf
... // 修改buf的内容
write(fd, buf, 1024); // 把buf的内容写入到文件
Refernece
-
认真分析mmap:是什么 为什么 怎么用
APCu扩展
介绍
APCU的前身是APC,主要用途有两项
- 将PHP代码编译之后所产生的bytecode暂存在共享内存内供重复使用,以提升应用的运行效率。(Opcode Cache)
- 提供用户数据缓存功能。(User Data Cache)
其中第一点是其主要功能,因为PHP的运行机制——每次接受一个请求时都要初始化所有的资源(将源代码转换成Opcode(即001011机器操作吗),……),执行代码,然后释放资源;所以启用Opcache Cache后,在第一次请求时将编译后的机器操作码暂存在共享内存内供重复使用,这样就可以在初始化资源阶段减少CPU和内存的消耗。
但是PHP从PHP 5.5开始,使用ZendOptimizerPlus(更名为Opcache)作为内置的Opcode Cache实现。所以现在APCU的主要功能便不再有意义了,而且其官方也随后表示不再维护APC了。
虽然apc可以通过关闭Opcache Cache而只使用用户数据缓存功能但是APC的User Data Cache使用的存储机制是和Opcode Cache一样的, 这样的场景要求数据严格正确, 所以锁会比较多,因此APCU作为缓存用户数据的扩展被启用。
他的作用和redis和memcached重合,测试表明, APC的User Data Cache的效率和本地memcached几乎相当(在单机性能上,APCu通常比Memcached更高。memcached本身的设计就是为了分布式应用,大规模内存缓存,集群,易扩展等,如果只有一台服务器且内存足够缓存用户数据时推荐apcu)
APCU缓存和memcahce/redis不一样的地方。你通过memcache/redis存储一个数据,在缓存有效期内,同一机器上的不同的PHP进程(FPM+CLI)都是能够取到这份数据的。
对的,注意关键字 “同一机器上的不同的PHP进程”,对于APCU而言,PHP-FPM(PHP5.3.3+)模式下所有的php-fpm进程(即使是不同的pool)属于同一个父进程,所以是可以共享缓存数据的;但是cli(命令行)模式每次都是单独一个全新进程,因而和php-fpm模式的进程是不能共享缓存数据的。所以如果你的业务场景需要在cli和php-fpm两种模式下共享数据一定要小心了,可能memcache或者redis才是你更好的选择
APCu 配置选项
名字 | 默认 | 可修改范围 | 说明 |
---|---|---|---|
apc.enabled | "1" | PHP_INI_SYSTEM | 启用缓存扩展 |
apc.shm_segments | "1" | PHP_INI_SYSTEM | 分配共享内存块的数量(建议值为1) |
apc.shm_size | "32M" | PHP_INI_SYSTEM | 设置每个共享内存块的大小(以MB为单位,建议值为128~256) |
apc.entries_hint | "4096" | PHP_INI_SYSTEM | 关于可能存储的不同变量的数量“提示”。如果不确定,设置为零或省略 |
apc.ttl | "0" | PHP_INI_SYSTEM | 设置缓存超时时间,单位是秒。0表示永不超时。建议值为7200~36000。 |
apc.gc_ttl | "3600" | PHP_INI_SYSTEM | 缓存条目在垃圾回收表中能够存在的秒数,此值提供了一个安全措施,即使一个服务器进程在执行缓存的源文件时崩溃,而且该源文件已经被修改,为旧版本分配的内存也不会被回收,直到达到此ttl值为止,设置为0禁用此功能 |
apc.mmap_file_mask | NULL | PHP_INI_SYSTEM | 如果在安装模块时使用–enable-mmap(默认启用)为apc编译了mmap(映射)支持,这里的值就是传递给mmap模块的mktemp风格的文件掩码(建议值为"/tmp/apc.xxxxxx"),以确定内存映射区域是文件支持的还是共享内存支持的。对于直接文件支持的内存映射,将它设置为/tmp/apc。XXXXXX(正好6x)。要使用posix风格的shm_open/mmap就需要设置成"/apc.shm.xxxxxx"的样子。例如/ apc.shm。您还可以将它设置为/dev/zero,以使用内核的/dev/zero接口来访问匿名映射的内存。不定义它将强制生成一个匿名的mmap(映射)。它将只使用一个内存段,这与使用多个内存段的SHM (SysV共享内存)支持构建APCu不同。MMAP不像/proc/sys/kernel/ shmmaxde那样有最大限制。一般来说,MMAP支持是被推荐的,因为它将在重新启动web服务器时更快地回收内存,并且总而言之,在启动时减少了内存分配的影响。 |
apc.slam_defense | "1" | PHP_INI_SYSTEM | 在非常繁忙的服务器上,无论是启动服务还是修改文件,都可能由于多个进程企图同时缓存一个文件而导致竞争条件。这个指令用于设置进程在处理未被缓存的文件时跳过缓存步骤的百分率。比如设为75表示在遇到未被缓存的文件时有75%的概率不进行缓存,从而减少碰撞几率。设为 0 禁用这个特性。 |
apc.enable_cli | "0" | PHP_INI_SYSTEM | 主要用于测试和调试,通过设置此选项,可以为cli版本的php启用apc功能。在正常情况下,在每个CLI请求上创建、填充和销毁APC缓存并不理想,但是对于各种测试场景,能够轻松地为CLI版本的PHP启用APC是很有用的 |
apc.use_request_time | "1" | PHP_INI_ALL | 为生存时间TTL使用SAPI请求启动时间 |
apc.serializer | "default" | PHP_INI_SYSTEM | 用于配置APC以使用第三方序列化程序 |
apc.coredump_unmap | "0" | PHP_INI_SYSTEM | 启用APC的信号句柄,例如SIGSEGV信号,当信号写入核心文件。当这些信号被接收,APC将试图取消映射的共享内存段,从核心文件中排除它。此设置可以提高系统的稳定性,当接受到致命的信号或者采用APC的大型共享内存段配置方式此功能是潜在的危险。如果发生致命错误取消映射一个共享内存段致命的信号句柄, 可能会导致不可预知的结果虽然有些内核可能会提供了便利,忽略各类共享内存时生成核心转储文件,这些实现可能也忽略了重要的共享内存段,比如 Apache scoreboard |
apc.preload_path | NULL | PHP_INI_SYSTEM | 可选地,设置APC将在启动时加载缓存数据的目录的路径 |
如果APCu在工作,那么缓存的全计数号(在左边)将显示缓存达到最大容量的次数,并且必须强制清除上一个apc中没有访问的任何条目
配置示例
//修改php.ini加入如下内容[请根据环境自行调整]:
extension= "apcu.so" ;
;开启功能
apc.enabled= 1
;为编译器缓冲区分配的共享内存块数量(建议值为1)。
apc.shm_segments= 1
;每个共享内存块的大小(以MB为单位,建议值为128~256)。
apc.shm_size= 128M
;优化级别(建议值为0 );正整数值表示启用优化器,值越高则使用越激进的优化。
apc.optimization= 0
;Web服务器上可能被包含或被请求的不同源文件的大致数量(建议值为1024~4096)。如果不能确定,则设为0。
apc.num_files_hint= 0
;缓冲区中超时时间,单位是秒。0表示永不超时。建议值为7200~36000。
apc.ttl= 0
;缓存条目在垃圾回收表中超时秒数。
apc.gc_ttl= 3600
//配置变更后,需重启httpd或者php-fpm服务。
函数使用
增加
//////////////// 在数据存储中缓存一个新变量, 变量已存在则不会被设置 返回false ////////////////
// apcu_add( string $key , mixed $var [, int $ttl = 0 ] ) bool
$bar = 'BAR';
apcu_add('foo', $bar);
var_dump(apcu_fetch('foo')); //输出:BAR
$bar = 'NEVER GETS SET';
apcu_add('foo', $bar,0);
var_dump(apcu_fetch('foo')); //输出:BAR`
//////////////// 在数据存储中缓存多个新变量,变量以存在则不会被设置 ////////////////
// apcu_add( array $values [, mixed $unused = NULL [, int $ttl = 0 ]] ) : array
apcu_add(['foo'=>'BAR','tool'=>'BBT']);
apcu_add(['foo'=>'BAR','tool'=>'BBT'],null,0);
保存更新
// apcu_store - 在数据存储中缓存一个变量 用法同apcu_add,区别就是他会覆盖存在的缓存变量
查找
// 从缓存中获取已存储的变量
// apcu_fetch( mixed $key [, bool &$success ] ) : mixed
$bar = 'BAR';
apcu_store('foo', $bar);
var_dump(apcu_fetch('foo')); //BAR
修改
// 用新值更新旧值
// apcu_cas( string $key , int $old , int $new ) : bool
apcu_store('foobar', 2);
apcu_cas('foobar', 2, 1)
自减
// 减少存储的值(递减,必须数值型)
// apcu_dec( string $key [, int $step = 1 [, bool &$success [, int $ttl = 0 ]]] )
apcu_store('anumber', 42);
echo apcu_dec('anumber'), PHP_EOL;
echo apcu_dec('anumber', 10), PHP_EOL;
echo apcu_dec('anumber', 10, $success), PHP_EOL;
自增
// apcu_inc增加存储的值(递增,必须数值型)
apcu_store('anumber', 42);
echo apcu_inc('anumber'), PHP_EOL;
echo apcu_inc('anumber', 10), PHP_EOL;
echo apcu_inc('anumber', 10, $success), PHP_EOL;
删除
// apcu_delete( mixed $key ) : mixed -删除存储变量从缓存
$bar = 'BAR';
apcu_store('foo', $bar);
apcu_delete('foo');
// 删除多个
apcu_delete(['foo', 'bar', 'baz']);
清空
// 清除APCu缓存 成功返回true
apcu_clear_cache();
是否存在
// -检查某个变量是否被缓存
// apcu_exists( mixed $keys ) : mixed
$fruit = 'apple';
apcu_store('foo', $fruit);
if (apcu_exists('foo')) {
echo "Foo exists: ";
echo apcu_fetch('foo');
} else {
echo "Foo does not exist";
}
// 是否可用: APCu在当前环境中是否可用 返回bool
apcu_enabled();
读取&生成
// 自动读取或生成一个缓存条目
// apcu_entry( string $key , callable $generator [, int $ttl = 0 ] ) : mixed
$config = apcu_entry("config", function($key) {
return [
"fruit" => apcu_entry("config.fruit", function($key){return ["apples","pears"];}),
"people" => apcu_entry("config.people", function($key){return ["bob","joe","niki"];})
];
});
var_dump($config);
//以上例程会输出:
[
"fruit"=>["apples","pears"],
"people"=>["bob","joe","niki"]
]
查看运行信息
// 检索APCu共享内存分配信息
// apcu_sma_info([ bool $limited = FALSE ] ) : array
print_r(apcu_sma_info());
//以上例程的输出类似于:
Array
(
[num_seg] => 1
[seg_size] => 31457280
[avail_mem] => 31448408
[block_lists] => Array
(
[0] => Array
(
[0] => Array
(
[size] => 31448408
[offset] => 8864
)
)
)
)
查看存储信息
// 从APC的数据存储中检索缓存的信息和元数据
// apcu_cache_info([ bool $limited = FALSE ] ) 如果limited为真,则返回值将排除缓存项的单个列表。这在尝试优化统计信息收集调用时非常有用
print_r(apcu_cache_info());
// 输出类似于:
Array
(
[num_slots] => 2000
[ttl] => 0
[num_hits] => 9
[num_misses] => 3
[start_time] => 1123958803
[cache_list] => Array
(
[0] => Array
(
[filename] => /path/to/apcu_test.php
[device] => 29954
[inode] => 1130511
[type] => file
[num_hits] => 1
[mtime] => 1123960686
[creation_time] => 1123960696
[deletion_time] => 0
[access_time] => 1123962864
[ref_count] => 1
[mem_size] => 677
)
[1] => Array (...iterates for each cached file)
)
注意:php-cli模式运行不能保存到下次运行(即命令行执行时,缓存无效)
在FastCGI模式下重启后将会清除缓存
APCUIterator类
APCUIterator implements Iterator {
public __construct ([ mixed $search = NULL [, int $format = APC_ITER_ALL [, int $chunk_size = 100 [, int $list = APC_LIST_ACTIVE ]]]] )
public current ( void ) : mixed - 得到当前项
public getTotalCount ( void ) : int - 得到总数
public getTotalHits ( void ) : int - 获取缓存总命中数
public getTotalSize ( void ) : int - 获取总缓存大小
public key ( void ) : string - 得到迭代器关键字
public next ( void ) : bool - 移动指针到下一项
public rewind ( void ) : void - 复原指针到初始位置
public valid ( void ) : bool - 检查当前位置是否有效
}