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 秒的粒度。如果一个文件在一秒内被多次修改,修改将被遗漏。
我想使用 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 秒的粒度。如果一个文件在一秒内被多次修改,修改将被遗漏。