几乎每隔 4 分钟,我就会在 php 错误日志中看到以下内存不足错误:
01-Jul-2014 21:50:03 UTC] PHP Fatal error: Allowed memory size of 268435456 bytes
exhausted (tried to allocate 72 bytes) in /home/[sitename]/public_html/wp-includes
/wp-db.php on line 1938
错误消息似乎支持 PHP 认为 php.ini 设置的 memory_limit = 256M 是正确的。但是,我在 wordpress 中使用了几个内存监控插件,它们都报告说网站在稳定状态下使用了 ~35MB 的 RAM,而且在发生 OOME 之前,它似乎根本没有增长。内存之前被设置在较低的水平,并反复增加而没有解决症状。它几乎总是恰好 4 分钟。偶尔它恰好是 3 分钟,或 3 分 30 秒等。我安装了一个 wordpress cron 插件来查看是否有计划以 4 分钟为间隔运行,但似乎什么都没有。
我检查了 httpd.conf 文件并确认没有 RLimitMEM 设置。我还通过 apachectl -V 确认我正在查看正确的 httpd.conf 文件。Top 说系统有半 GB 的可用 RAM。我发现访问日志中的条目与 php 错误日志中的 OOME 之间没有关联。
该服务器托管着大量网站。我不负责管理该服务器,但我一直在帮助解决相关网站上的一些问题。
如果您能就如何继续解决此问题提出任何建议,我将不胜感激。
答案1
我终于明白了。
问题在于网站上安装的两个插件之间存在冲突(具体来说,是两个插件的配置方式)。iThemes Security 插件(http://ithemes.com/security) 配置为定期进行站点备份。进行数据库备份的代码会转储数据库中的每个表,并假设每个表的内容都完全适合内存。与此无关的是,网站上还安装了另一个名为重定向 (http://urbangiraffe.com/plugins/redirection/) 用于维护重定向。此插件具有配置选项,用于记录重定向以及 404 响应。不幸的是,这些日志被设置为永不过期,并且由于指向我们网站的僵尸网络流量量,已积累了近 90,000 个重定向日志和 30,000 个 404 日志。由于 iThemes Security 插件在尝试创建备份时尝试将整个表加载到内存中,因此 wp_redirection_logs 表消耗了 php 的所有可用内存并使该过程崩溃。我怀疑 iThemes Security 在每个可用机会都尝试重新运行失败的备份,导致每 3-4 分钟出现一次错误。
我通过将重定向日志设置更改为 5 天后重定向条目过期并且根本不记录 404 错误来修复此问题。然后我不得不反复刷新日志页面以删除过期的条目。内存不足错误不再发生。
[编辑] 从那时起,我听到其他 wordpress 用户在使用 iThemse Security 插件对各种 wordpress 表执行 SELECT * 时遇到类似错误。下面是我如何调试此问题的说明,以防您的问题类似但不完全相同:
在进行常规修复(增加 php 内存并确保确实有效,确认我的内存使用率在一段时间内保持稳定且较低)之后,我解决问题的方法是向 wp-db.php 添加一些调试日志语句,这样我就可以查看错误发生时发生了什么。以下是我为帮助缩小问题范围而进行的代码更改(在尝试此操作之前,请务必备份 wp-db.php,以便您可以轻松地恢复设置,以防您在编辑文件时弄乱某些东西):
function get_results( $query = null, $output = OBJECT ) {
$this->func_call = "\$db->get_results(\"$query\", $output)";
if ( $query )
$this->query( $query );
else
return null;
$new_array = array();
if ( $output == OBJECT ) {
// Return an integer-keyed array of row objects
return $this->last_result;
} elseif ( $output == OBJECT_K ) {
// Return an array of row objects with keys from column 1
// (Duplicates are discarded)
foreach ( $this->last_result as $row ) {
$var_by_ref = get_object_vars( $row );
$key = array_shift( $var_by_ref );
if ( ! isset( $new_array[ $key ] ) )
$new_array[ $key ] = $row;
}
return $new_array;
} elseif ( $output == ARRAY_A || $output == ARRAY_N ) {
// Return an integer-keyed array of...
if ( $this->last_result ) {
$emited = false;
foreach( (array) $this->last_result as $row ) {
if ( $output == ARRAY_N ) {
// ...integer-keyed row arrays
if (!$emitted) {
error_log("Current Mem: " . memory_get_usage() . ", eak mem: " . memory_get_peak_usage());
error_log($query);
$emitted = true;
}
$new_array[] = array_values( get_object_vars( $row ) );
} else {
// ...column name-keyed row arrays
$new_array[] = get_object_vars( $row );
}
}
}
return $new_array;
} elseif ( strtoupper( $output ) === OBJECT ) {
// Back compat for OBJECT being previously case insensitive.
return $this->last_result;
}
return null;
}
这是添加的 6 行代码;第一行将 $emitted 变量分配给 false 以跟踪我们是否已经记录了该请求,然后是 5 行 if 子句以实际记录。
这样做的目的是在我们开始将查询结果读入内存之前,打印出 php 当前和峰值内存消耗,并打印出已执行的查询。在我们开始读取结果之前,内存会让您了解是否有合理的内存可用。如果可用内存接近您的限制(几 MB 以内),则问题可能出在其他地方,而您实际上没有足够的空间来运行合理大小的查询。如果像我一样,在运行查询之前您有大量可用内存,那么请查看在内存耗尽之前运行的查询是什么(我的日志条目都在 php 错误日志中,但如果您的日志条目分散在 iThemes 日志和 php 错误日志中,则根据时间戳在两者之间建立关联。)在内存错误之前立即运行的查询就是导致您崩溃的查询。
在我的情况下,它是一个 SELECT * FROM wp_redirection_logs;。该表已经失控,因为我的网站上的重定向插件配置错误,日志条目永不过期。通过阅读 iThemes 安全插件的代码,很明显备份操作对数据库中以 wp_ 前缀开头的每个表执行 SELECT * FROM 查询(如果您的站点由于多站点或其他原因配置为使用不同的前缀,则为其他前缀。)iThemes 安全的其他领域(如 404 错误日志)似乎也会针对可能超出可用内存的表发出 SELECT * 查询。找到查询后,您可以开始推断错误的原因,并可能像我一样修剪不必要的数据库内容以解决问题。
如果您按照这些步骤操作并报告,我会很乐意提供建议。