数字营销 · Web开发 · 基础设施

WordPress中获取随机文章导致服务器压力过大的问题分析

分析 WordPress 使用 ORDER BY RAND() 获取随机文章时造成 MySQL IO 和服务器负载过高的问题,并对比一种更轻量的随机文章获取方法。

最近发现有个服务器上的网站打开速度很慢,有时候甚至出现完全打不开的情况。查看了一下服务器状态,发现服务器负载很大,最高时候,5分钟平均 load average 达到了 20,而服务器是 8 核 CPU。接着查看了一下服务的 IO 情况,发现 MySQL 读写非常频繁,如下图:

服务IO状态

这显然是不正常的情况。接着,我又查看了一下 MySQL 的状态,发现出现了 Copying to tmp table 查询语句为:

SELECT ID, post_title, guid

问题应该就出现在这里了。大量临时表的读写显然会导致磁盘 IO 增加。按理说,WordPress 官方应该对这些查询做过一些优化,如果一个 WordPress 网站本身就很容易导致如此大的服务器压力,它应该也不会如此流行。于是百度搜索了一下:

SELECT ID, post_title, guid

发现网上有很多获取 WordPress 随机文章的教程,都包含类似的 SQL 查询,完整的语句大致是:

SELECT ID, post_title, guid
FROM XXX
WHERE post_status = 'publish'
AND post_title != ''
AND post_password = ''
AND post_type = 'post'
ORDER BY RAND()
LIMIT XXX

这里有一个 RAND() 函数,非常显眼。我猜想,当查询记录数量比较大的时候,使用:

ORDERBY RAND()

很可能会导致查询速度变慢。查询了一下相关资料,也发现 ORDER BY RAND() 在数据量比较大的情况下,确实容易产生比较大的查询开销。所以,服务器 IO 过高的问题基本已经找到了原因。但是,随机文章这个功能本身还是需要的。既然直接使用这个查询会影响性能,那么有没有其他方法呢?

肯定是有的。

这里我把原来的方法和我重新做的方法进行一下对比。下面是具体的函数。实验一,是网上比较常见的获取随机文章的方法;实验二,是我重新设计的方法。整个数据表大约有 9 万条记录,下面是测试代码。

实验一 ORDER BY RAND查询

$host = "localhost";
$username = "******";
$passwd = "******";
$dbname = "******";
$table = "en_posts";

$cono = mysql_pconnect(
$host,
$username,
$passwd
);

if(!$cono){
exit("数据库连接失败");
}

mysql_query("set names utf8");
mysql_select_db($dbname);


// 实验一,原来的查询
$begin = microtime(TRUE);

$sql = "SELECT ID, post_title, guid
from $table
where post_status = 'publish'
and post_title != ''
AND post_password = ''
AND post_type = 'post'
ORDER BY RAND()
LIMIT 0,5";

$result = mysql_query($sql, $cono);

while($list = mysql_fetch_row($result)){
print_r($list);
}

// 结束执行
$end = microtime(TRUE);

echo "\n<br/>实验一,执行时间:" .
($end - $begin) .
"秒";

实验二多次查询

// 实验二,新的查询
$begin = microtime(TRUE);

function getlist($num){

global $table;

$arr = array();

$count = mysql_fetch_row(
mysql_query(
"SELECT MIN(ID),MAX(ID) from $table"
)
);

$startid = $count[0];
$endid = $count[1];

$querycount = 0;

for($i = 0; $i < $num; $i++){

// 安全设置,保证不出现死循环
if($querycount - $num > 5){
break;
}

$id = rand(
$startid,
$endid
);

$sql = "SELECT ID, post_title, guid
from $table
where ID = $id
and post_status = 'publish'
and post_title != ''
AND post_password = ''
AND post_type = 'post'";

$result = mysql_query($sql);

if(!$result){
$i--;
continue;
}

$arr[] = mysql_fetch_row($result);

$querycount++;
}

return $arr;
}

print_r(getlist(5));

$end = microtime(TRUE);

echo "\n<br/>实验二,执行时间:" .
($end - $begin) .
"秒";
mysql_close($cono);

两次查询的结果如下:

执行结果对比
执行结果对比

可以看到,同样是随机取 5 条记录:实验一用了 35 秒多;实验二只用了 不到 60 毫秒。两者之间的差距非常大。

WordPress中的随机文章函数

经过测试之后,我把新的随机文章方法写成了一个 WordPress 函数。把下面的函数放到主题的 functions.php 中,就可以直接在页面中调用:

  function random_posts(
$posts_num = 5,
$before = '<li>',
$after = '</li>'
){

global $wpdb;

$count = $wpdb->get_results(
"SELECT MIN(ID) as min,
MAX(ID) as max
from $wpdb->posts"
);

$startid = $count[0]->min;
$endid = $count[0]->max;

$querycount = 0;

for($i = 0; $i < $posts_num; $i++){

// 安全设置,保证不出现死循环
if($querycount - $posts_num > $posts_num){
break;
}

$id = rand(
$startid,
$endid
);

$sql = "SELECT ID, post_title, guid
from $wpdb->posts
where ID = $id
and post_status = 'publish'
and post_title != ''
AND post_password = ''
AND post_type = 'post'";

$randposts = $wpdb->get_results($sql);

if(!$randposts){
$i--;
continue;
}

$output = '';

foreach($randposts as $randpost){

$post_title =
stripslashes($randpost->post_title);

$permalink =
get_permalink($randpost->ID);

$output .=
$before .
'<a href="' .
$permalink .
'" rel="bookmark" title="' .
$post_title .
'">' .
$post_title .
'</a>' .
$after;
}

echo $output;

$querycount++;
}
}

再去测试,发现速度要快很多,服务器的 IO 也降低了不少。当然,这种方法也有一个问题:

在文章数量比较少的时候,可能会出现重复文章。

因为这里是根据文章 ID 的范围随机生成一个 ID,如果随机到的 ID 并不存在,或者不是符合条件的文章,就会重新进行查询。但是对于文章数量比较多的情况,例如上面测试的 9 万条记录,这种方式在实际使用中效果还是比较明显的。

总结

通过这次问题,我主要得到两个结论:

  1. MySQL 在执行某些大查询时,如果临时表无法完全放入内存,可能会使用磁盘临时表,从而增加磁盘 IO,并导致查询速度变慢。
  2. 使用网上找到的代码时一定要谨慎。

特别是一些看起来非常简单的 SQL,在数据量比较小的时候可能完全感觉不到问题,但是当数据量增加之后,就可能给服务器带来很大的压力。这次就是一个比较典型的例子。

评论0

欢迎分享你的看法,也欢迎补充不同的实践经验。