PHP 使用 OpCache 的用户态缓存(未按预期工作)

PHP userland cache with OpCache (not working as expected)

我想使用 PHP OpCache 作为用户态缓存(如 APCu、Redis、Memcache)作为后备,在没有更好的缓存解决方案的情况下。

想法是将要缓存的数据存储到运行时创建的 php 文件中,然后使用 include 读取数据。这样,OpCache应该是把编译后的文件缓存在内存中,结果就是内存缓存。

<?php

/**
 * Simple php cache using php generated files and opcache
 */
class DiskCache {

    const DEFAULT_TTL = 3600;

    /**
     * @var callable
     */
    private static $emptyErrorHandler;

    /**
     * @var string
     */
    protected $cacheDir;

    /**
     * @var int
     */
    protected $defaultTtl;


    /**
     * Constructor
     * @param string  $cacheDir where to store cache files
     * @param integer $ttl      time to live
     */
    public function __construct($cacheDir = null, $ttl = self::DEFAULT_TTL) {

        if( empty($cacheDir) ){
            $cacheDir = sys_get_temp_dir();
        }

        $cacheDir = realpath(rtrim($cacheDir, DIRECTORY_SEPARATOR));

        if( !is_dir($cacheDir) ) {
            throw new InvalidArgumentException('Provided cache dir is not a directory');
        }

        if( !(is_readable($cacheDir) && is_writable($cacheDir)) ) {
            throw new InvalidArgumentException('Provided cache dir is not writable and readable');
        }

        $this->cacheDir   = $cacheDir;
        $this->defaultTtl = (int) $ttl;

        self::$emptyErrorHandler = function(){};
    }

    /**
     * Read cache
     * @param string  $key  the key
     * @return mixed|false  cached data
     */
    public function read($key) {

        $fileName = $this->getCacheFilename($key);

        set_error_handler(self::$emptyErrorHandler);

        $cached = include $fileName;

        restore_error_handler();

        if( $cached && isset($cached['timestamp'], $cached['ttl'], $cached['data']) ) {
            if((time() - $cached['timestamp']) < $cached['ttl']){
                return $cached['data'];
            }
        }

        if( $cached ) {
            $this->delete($key);
        }

        return false;
    }

    /**
     * Write cache
     * @param string  $key    the key
     * @param mixed   $data   the data
     * @param integer $ttl    time to live
     * @return boolean
     */
    public function write($key, $data, $ttl = null) {

        $ttl = $ttl > 0 ? (int) $ttl : $this->defaultTtl;
        $fileName = $this->getCacheFilename($key);
        $code = null;
        $result = false;

        $value = array(
            'timestamp' => time(),
            'ttl'       => $ttl,
            'data'      => $data
        );

        if (is_object($data) && method_exists($data, '__set_state')) {
            $value = var_export($value, true);
            $code  = sprintf('<?php return %s;', $value);
        } else {
            $value = var_export(serialize($value), true);
            $code  = sprintf('<?php return unserialize(%s);', $value);
        }

        if( $code ){
            $result = @file_put_contents($fileName, $code, LOCK_EX);
        }

        return (boolean) $result;
    }

    /**
     * Delete cache
     * @param string $key
     * @return boolean
     */
    public function delete($key) {
        $fileName = $this->getCacheFilename($key);
        return @unlink($fileName);
    }

    /**
     * Return the cache filename
     * @param string $key
     * @throws InvalidArgumentException
     * @return string
     */
    public function getCacheFilename($key){
        if( empty($key) ) {
            throw new InvalidArgumentException('key is empty');
        }
        return $this->cacheDir . DIRECTORY_SEPARATOR . md5($key). '.php';
    }   
}

我已经在 test.php 页面中以这种方式进行了测试:

<?php
$cache = new DiskCache(__DIR__);
echo PHP_EOL;
var_dump($cache->write('test', array('a', 'b', 'c')));
echo PHP_EOL;
var_dump($cache->read('test'));
echo PHP_EOL;
print_r(opcache_get_status());

这是输出:

bool(true)

array(3) {
  [0]=>
  string(1) "a"
  [1]=>
  string(1) "b"
  [2]=>
  string(1) "c"
}

Array
(
    [opcache_enabled] => 1
    [cache_full] => 
    [restart_pending] => 
    [restart_in_progress] => 
    [memory_usage] => Array
        (
            [used_memory] => 123832
            [free_memory] => 66748632
            [wasted_memory] => 236400
            [current_wasted_percentage] => 0.35226345062256
        )

    [opcache_statistics] => Array
        (
            [num_cached_scripts] => 1
            [num_cached_keys] => 2
            [max_cached_keys] => 3907
            [hits] => 17
            [start_time] => 1513796280
            [last_restart_time] => 0
            [oom_restarts] => 0
            [hash_restarts] => 0
            [manual_restarts] => 0
            [misses] => 190
            [blacklist_misses] => 0
            [blacklist_miss_ratio] => 0
            [opcache_hit_rate] => 8.2125603864734
        )

    [scripts] => Array
        (
            [C:\DevEnv\htdocs\test.php] => Array
                (
                    [full_path] => C:\DevEnv\htdocs\test.php
                    [hits] => 1
                    [memory_consumption] => 12704
                    [last_used] => Wed Dec 20 20:49:08 2017
                    [last_used_timestamp] => 1513799348
                    [timestamp] => 1513799344
                )

        )

)

OpCache 似乎不缓存在运行时创建的 php 文件。唯一一个缓存文件是test.php,见:

[scripts] => Array
            (
                [C:\DevEnv\htdocs\test.php] => Array( .. )
            )

进入php.ini opcache 已启用

[opcache]
zend_extension=C:\DevEnv\PHP.6.24\ext\php_opcache.dll
; Determines if Zend OPCache is enabled
opcache.enable=1

; Determines if Zend OPCache is enabled for the CLI version of PHP
opcache.enable_cli=0

; The OPcache shared memory storage size.
;opcache.memory_consumption=64

; The amount of memory for interned strings in Mbytes.
;opcache.interned_strings_buffer=4

; The maximum number of keys (scripts) in the OPcache hash table.
; Only numbers between 200 and 100000 are allowed.
;opcache.max_accelerated_files=2000

; The maximum percentage of "wasted" memory until a restart is scheduled.
;opcache.max_wasted_percentage=5

; When this directive is enabled, the OPcache appends the current working
; directory to the script key, thus eliminating possible collisions between
; files with the same name (basename). Disabling the directive improves
; performance, but may break existing applications.
opcache.use_cwd=1

; When disabled, you must reset the OPcache manually or restart the
; webserver for changes to the filesystem to take effect.
opcache.validate_timestamps=1

; How often (in seconds) to check file timestamps for changes to the shared
; memory storage allocation. ("1" means validate once per second, but only
; once per request. "0" means always validate)
opcache.revalidate_freq=1

; Enables or disables file search in include_path optimization
;opcache.revalidate_path=0

; If disabled, all PHPDoc comments are dropped from the code to reduce the
; size of the optimized code.
;opcache.save_comments=1

; If disabled, PHPDoc comments are not loaded from SHM, so "Doc Comments"
; may be always stored (save_comments=1), but not loaded by applications
; that don't need them anyway.
;opcache.load_comments=1

; If enabled, a fast shutdown sequence is used for the accelerated code
;opcache.fast_shutdown=0

; Allow file existence override (file_exists, etc.) performance feature.
;opcache.enable_file_override=0

; A bitmask, where each bit enables or disables the appropriate OPcache
; passes
;opcache.optimization_level=0xffffffff

;opcache.inherited_hack=1
;opcache.dups_fix=0

; The location of the OPcache blacklist file (wildcards allowed).
; Each OPcache blacklist file is a text file that holds the names of files
; that should not be accelerated. The file format is to add each filename
; to a new line. The filename may be a full path or just a file prefix
; (i.e., /var/www/x  blacklists all the files and directories in /var/www
; that start with 'x'). Line starting with a ; are ignored (comments).
;opcache.blacklist_filename=

; Allows exclusion of large files from being cached. By default all files
; are cached.
;opcache.max_file_size=0

; Check the cache checksum each N requests.
; The default value of "0" means that the checks are disabled.
;opcache.consistency_checks=0

; How long to wait (in seconds) for a scheduled restart to begin if the cache
; is not being accessed.
;opcache.force_restart_timeout=180

; OPcache error_log file name. Empty string assumes "stderr".
;opcache.error_log=

; All OPcache errors go to the Web server log.
; By default, only fatal errors (level 0) or errors (level 1) are logged.
; You can also enable warnings (level 2), info messages (level 3) or
; debug messages (level 4).
;opcache.log_verbosity_level=1

; Preferred Shared Memory back-end. Leave empty and let the system decide.
;opcache.preferred_memory_model=

; Protect the shared memory from unexpected writing during script execution.
; Useful for internal debugging only.
;opcache.protect_memory=0

我做错了什么?

不幸的是,我认为这不会成功。毕竟 APCu 存在是有原因的。

PHP 操作码缓存使用文件时间戳来确定文件自缓存以来是否已更改;在许多系统上,这些时间戳只有 1 秒的粒度。如果一个文件在一秒内被多次修改,修改将被遗漏。