浅谈PHP性能优化-APCu

一、前言

提到PHP,大家的第一印象都是简单易用好上手。只需几个小时即可大致了解它的结构,然后几分钟的时间即可搞定一个web服务,这就是PHP的魅力。凭借这一点PHP成为七猫飞速发展的见证者,在开山之初立下汗马功劳。但提到性能,就不禁唉了一声,于是各种性能优化呼之欲出,便有了APCu。

二、APCu是什么

官网有这么一段话来介绍:APCu is APC stripped of opcode caching. 译为:APCu是阉割了操作码缓存(opcode cache)的APC。APC的主要作用有两点,一是缓存代码编译后产生的操作码来进行重复使用(opcode cache),二是提供用户数据缓存的能力,使用上类似于memcache和redis。这样就清晰了,APCu是提供给我们用来缓存数据的,说直白点和gocache的作用一样。那为什么要阉割掉操作码缓存?(这是另外的事情了,因为操作码缓存太重要,后被加入到php内核中去了)

简单画了一下典型的web架构,从图中能看出APCu所处的位置,在PHP服务中APCu处于redis之前,当APCu中数据存在时则直接使用,不存在时则从redis中读取后写入APCu。

APCu是php进程开辟出来的一块共享内存空间,不同php进程访问apcu就像访问自己进程中的数据一样,而redis是需要走网络请求的,访问内存的速度大概在纳秒级别 ,网络请求的速度在毫秒级别,同机架或同机房的机器可能在1/10ms左右,还不是考虑服务稳定、网络稳定的情况下。

简单的对redis和APCu进行一下读取和写入测试。

$start = microtime(true);
// 写入apcu数据
for ($i=0;$i<100000;$i++) {
    $res = apcu_add('1aa'.$i, '1', 1000);
}
$end = microtime(true) - $start;
print_r("Apcu写入耗时:".$end. PHP_EOL);
$start = microtime(true);

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

// 写入redis数据
for ($i=0;$i<100000;$i++) {
    $res = $redis->set('1aa'.$i, '1');
}
$end = microtime(true) - $start;
print_r("Redis写入耗时:".$end. PHP_EOL);

测试结果:
APCu写入100000次耗时:0.7秒
Redis写入100000次耗时:8.9秒
APCu读取100000次耗时:0.7秒
Redis读取100000次耗时:8.7秒

该测试基于redis部署在我本机,基本没有网络开销的情况下,APCu的读和写的速度都是redis的12倍,如果再加上网络开销,那速度差距只会更大。

三、常用配置

apc.enabled
APCu功能开关,默认为1,设置为0则关闭APCu功能。有时我们在测试的时候想及时看到数据就会找运维或自行修改php.ini中的该参数来打开或关闭功能

apc.shm_size  
APCu可使用的内存大小,默认32M。该参数相当重要,当我们APCu内存不够用的时候,写入会失败,就需要调大该参数,但也不建议调的太大,太大会影响到机器的可使用内存

apc.ttl  
默认的缓存过期时间,当设置缓存的时候未指定过期时间,则使用该默认值

四、探索发现

在对APCu进行测试时,我发现了一个问题,php-fpm的运行模式以及APCu所处的位置如下图:

当我们有多个php项目时,由nginx进行转发到php服务监听的端口,当有请求到达该端口时,再由php的master指派相应的worker进程进行处理,APCu就是master在启动时开辟出来的一块共享内存空间。

基于这个实现原理,那本机php的所有worker进程都是可以读写该共享内存空间的,这样当我们一台机器上部署了多个php服务时,这每个php服务用到的共享内存都是同一个,那一旦有key同名就会有数据混乱的问题。因为我们一台测试环境的web机上部署了很多套php代码,于是我打开了测试环境进行验证。

在a.com的项目中写入一个key,然后在b.com项目中读取该key,完全可以读的到,由此也验证了这个观点。(a.com和b.com是两个不同的项目)

所以目前我们多个php项目直接部署在同一台web机上都会有这个问题,会造成key相同时缓存混乱。

针对该问题的解决方案:

  1. php项目使用docker部署,这样就能隔离php进程,每个项目互不影响。由于需要将各个php项目进行docker部署,所以该方案改动较大。
  2. APCu的key基于项目进行加密,这样就算不同项目的key一致但基于项目进行加密后也会不一致。改动较大,各个php项目都需要改动。
  3. 用到APCu的php项目使用单独的php,web机上使用多个php进程来进行隔离,比如:a项目使用php7.1,b项目使用php7.2来进行隔离。优点:不需要改动代码,直接在部署时隔离。

截止发稿前,该问题及解决方案已反馈给相关开发和运维。大家以后在使用APCu时也需要留意一下这个问题,避免带来不必要的问题。

五、拓展

作为缓存组件,APCu官方向我们提供了几种管理的方法,类似于redis的keys *和info命令。

apcu_cache_info函数
该方法会列举出当前apcu中所有的缓存,以及一些详细信息。和redis的keys *功能类似

  1. info APCu的key
  2. ttl key的过期时间
  3. num_hits  key的命中次数统计
  4. creation_time  key的创建时间
  5. deletion_time  key的删除时间
  6. mem_size  key的大小

可视化页面
APCu还提供了一个可视化页面来方便的查看APCu的内存使用、key数量、缓存命中等信息。

其中关键信息:

  1. Cached Variables key数量及大小
  2. Hits  缓存总命中次数
  3. Misses 缓存总未命中次数
  4. Runtime Settings 运行时配置
  5. Host Status Diagrams  内存占用饼图和命中与未命中柱状图

六、总结

  1. APCu是PHP进程开辟的共享内存空间,所以多个php项目可以同时使用,存在重不同项目key同名时造成数据混乱的风险。
  2. APCu适合缓存一些数据量小,变动频次低,但访问量大的数据。比如:公共配置等。
  3. APCu在内存中,无法持久化,重启php进程会导致数据失效。
  4. APCu在本机中,所以多机器间无法共享,也受限于单机内存,扩展不易。

看似简单的一个APCu,如果不去深入了解他,会给我们带来意想不到的问题。

参考:

  1. https://www.php.net/manual/zh/book.apcu.php
展示评论