什么可能导致 PHP 和 MySQL 之间奇怪的查询超时?

Jos*_*osh 13 mysql performance

我是许多不同客户使用的软件即服务应用程序的高级开发人员。我们的软件在由 MySQL 后端提供支持的 Apache / PHP 应用程序服务器集群上运行。在该软件的一个特定实例上,当客户的类别超过 29 个时,查询类别名称列表的 PHP 代码会超时。我知道这毫无意义;数字 30 没有什么特别之处会打破这一点,其他客户的类别超过 30 个,但是,当这个安装有 30 个或更多类别时,问题是 100% 可重现的,而当类别少于 30 个时,问题就会消失。

有问题的表是:

CREATE TABLE IF NOT EXISTS `categories` (
  `id` int(10) unsigned NOT NULL auto_increment,
  `name` varchar(64) NOT NULL,
  `title` varchar(128) NOT NULL,
  `parent` int(10) unsigned NOT NULL,
  `keywords` varchar(255) NOT NULL,
  `description` text NOT NULL,
  `status` enum('Active','Inactive','_Deleted','_New') NOT NULL default 'Active',
  `style` enum('_Unknown') default NULL COMMENT 'Autoenum;',
  `order` smallint(5) unsigned NOT NULL,
  `created_at` datetime NOT NULL,
  `modified_at` datetime default NULL,
  PRIMARY KEY  (`id`),
  KEY `name` (`name`),
  KEY `parent` (`parent`),
  KEY `created_at` (`created_at`),
  KEY `modified_at` (`modified_at`),
  KEY `status` (`status`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 COMMENT='R2' AUTO_INCREMENT=33 ;
Run Code Online (Sandbox Code Playgroud)

有问题的代码递归查询表以获取所有类别。它发出一个

SELECT * FROM `categories` WHERE `parent`=0 ORDER BY `order`,`name`
Run Code Online (Sandbox Code Playgroud)

然后对返回的每一行重复此查询,但WHERE parent=$category_id每次都使用。(我确信这个程序可以改进,但这可能是另一个问题)

据我所知,以下查询永远挂起:

SELECT * FROM `categories` WHERE `parent`=22 ORDER BY `order`,`name`
Run Code Online (Sandbox Code Playgroud)

我可以在服务器上的 mysql 客户端中完美地执行此查询,并且我也可以在 PHPMyAdmin 中执行它而没有问题。

请注意,问题不是那个特定的查询。如果我DELETE FROM categories WHERE id=22然后一个与上述类似的不同查询将挂起。此外,上面的查询在我手动运行时返回零行

我怀疑该表可能已损坏,我尝试过REPAIR TABLEOPTIMIZE TABLE但这些报告的问题均未解决,也没有解决该问题。我放下桌子并重新创建,但问题又回来了。这与其他客户使用的表结构和 PHP 代码完全相同,对其他人没有问题,包括拥有 30 多个类别的客户。

PHP 代码不会永远递归。(这不是无限循环)

MySQL 服务器运行 CentOS linux 和 mysqld Ver 5.0.92-community for pc-linux-gnu on i686 (MySQL Community Edition (GPL))

MySQL服务器负载低:平均负载:0.58, 0.75, 0.73, Cpu(s): 4.6%us, 2.9%sy, 0.0%ni, 92.2%id, 0.0%wa, 0.0%hi, 0.3%si, 0.0%st. 使用的交换可以忽略不计 (448k)

如何解决此问题?关于可能发生什么的任何建议?

更新:TRUNCE编辑了表格并插入了 30 行虚拟数据:

INSERT INTO `categories` (`id`, `name`, `title`, `parent`, `keywords`, `description`, `status`, `style`, `order`, `created_at`, `modified_at`) VALUES
(1, 'New Category', '', 0, '', '', 'Inactive', NULL, 1, '2011-10-25 12:06:30', '2011-10-25 12:06:34'),
(2, 'New Category', '', 0, '', '', 'Inactive', NULL, 2, '2011-10-25 12:06:39', '2011-10-25 12:06:40'),
(3, 'New Category', '', 0, '', '', 'Inactive', NULL, 3, '2011-10-25 12:06:41', '2011-10-25 12:06:42'),
(4, 'New Category', '', 0, '', '', 'Inactive', NULL, 4, '2011-10-25 12:06:46', '2011-10-25 12:06:47'),
(5, 'New Category', '', 0, '', '', 'Inactive', NULL, 5, '2011-10-25 12:06:49', NULL),
(6, 'New Category', '', 0, '', '', 'Inactive', NULL, 6, '2011-10-25 12:06:51', '2011-10-25 12:06:52'),
(7, 'New Category', '', 0, '', '', 'Inactive', NULL, 7, '2011-10-25 12:06:53', '2011-10-25 12:06:54'),
(8, 'New Category', '', 0, '', '', 'Inactive', NULL, 8, '2011-10-25 12:06:56', '2011-10-25 12:06:57'),
(9, 'New Category', '', 0, '', '', 'Inactive', NULL, 9, '2011-10-25 12:06:59', '2011-10-25 12:06:59'),
(10, 'New Category', '', 0, '', '', 'Inactive', NULL, 10, '2011-10-25 12:07:01', '2011-10-25 12:07:01'),
(11, 'New Category', '', 0, '', '', 'Inactive', NULL, 11, '2011-10-25 12:07:03', '2011-10-25 12:07:03'),
(12, 'New Category', '', 0, '', '', 'Inactive', NULL, 12, '2011-10-25 12:07:05', '2011-10-25 12:07:05'),
(13, 'New Category', '', 0, '', '', 'Inactive', NULL, 13, '2011-10-25 12:07:06', '2011-10-25 12:07:07'),
(14, 'New Category', '', 0, '', '', 'Inactive', NULL, 14, '2011-10-25 12:07:08', '2011-10-25 12:07:09'),
(15, 'New Category', '', 0, '', '', 'Inactive', NULL, 15, '2011-10-25 12:07:11', '2011-10-25 12:07:12'),
(16, 'New Category', '', 0, '', '', 'Inactive', NULL, 16, '2011-10-25 12:07:13', '2011-10-25 12:07:14'),
(17, 'New Category', '', 0, '', '', 'Inactive', NULL, 17, '2011-10-25 12:09:41', '2011-10-25 12:09:42'),
(18, 'New Category', '', 0, '', '', 'Inactive', NULL, 18, '2011-10-25 12:09:47', NULL),
(19, 'New Category', '', 0, '', '', 'Inactive', NULL, 19, '2011-10-25 12:09:48', NULL),
(20, 'New Category', '', 0, '', '', 'Inactive', NULL, 20, '2011-10-25 12:09:48', NULL),
(21, 'New Category', '', 0, '', '', 'Inactive', NULL, 21, '2011-10-25 12:09:49', NULL),
(22, 'New Category', '', 0, '', '', 'Inactive', NULL, 22, '2011-10-25 12:09:50', NULL),
(23, 'New Category', '', 0, '', '', 'Inactive', NULL, 23, '2011-10-25 12:09:51', NULL),
(24, 'New Category', '', 0, '', '', 'Inactive', NULL, 24, '2011-10-25 12:09:51', NULL),
(25, 'New Category', '', 0, '', '', 'Inactive', NULL, 25, '2011-10-25 12:09:52', NULL),
(26, 'New Category', '', 0, '', '', 'Inactive', NULL, 26, '2011-10-25 12:09:53', NULL),
(27, 'New Category', '', 0, '', '', 'Inactive', NULL, 27, '2011-10-25 12:09:54', NULL),
(28, 'New Category', '', 0, '', '', 'Inactive', NULL, 28, '2011-10-25 12:09:55', NULL),
(29, 'New Category', '', 0, '', '', 'Inactive', NULL, 29, '2011-10-25 12:09:56', NULL),
(30, 'New Category', '', 0, '', '', 'Inactive', NULL, 30, '2011-10-25 12:09:57', NULL);
Run Code Online (Sandbox Code Playgroud)

根本没有父母,所有类别都在顶级。问题仍然存在。由 PHP 执行的以下查询失败:

SELECT * FROM `categories` WHERE `parent`=22 ORDER BY `order`,`name`
Run Code Online (Sandbox Code Playgroud)

这是EXPLAIN

mysql> EXPLAIN SELECT * FROM `categories` WHERE `parent`=22 ORDER BY `order`,`name`;
+----+-------------+------------+------+---------------+--------+---------+-------+------+-----------------------------+
| id | select_type | table      | type | possible_keys | key    | key_len | ref   | rows | Extra                       |
+----+-------------+------------+------+---------------+--------+---------+-------+------+-----------------------------+
|  1 | SIMPLE      | categories | ref  | parent        | parent | 4       | const |    1 | Using where; Using filesort | 
+----+-------------+------------+------+---------------+--------+---------+-------+------+-----------------------------+
1 row in set (0.00 sec)
Run Code Online (Sandbox Code Playgroud)

更新#2:我现在已经尝试了以下所有内容:

  1. 我使用相同的软件将此表和数据复制到不同的站点。这个问题并没有遵循表。它似乎仅限于这一数据库。
  2. 我按照 gbn 的回答建议更改了索引。问题依然存在。
  3. 我删除了表格并重新创建为InnoDB表格,并在上面插入了相同的 30 个测试行。问题依然存在。

我怀疑它一定是这个数据库的东西......

更新 #3:我完全删除了数据库并以新名称重新创建它,导入她的数据。问题依然存在。

我发现挂起的实际 PHP 语句是对mysql_query(). 此后的语句永远不会执行。

当该调用挂起时, MySQL 将线程列为休眠!

mysql> show full processlist;
+-------+------------------+-----------------------------+----------------------+---------+------+-------+-----------------------+
| Id    | User             | Host                        | db                   | Command | Time | State | Info                  |
+-------+------------------+-----------------------------+----------------------+---------+------+-------+-----------------------+
|  5560 | root             | localhost                   | problem_db           | Query   |    0 | NULL  | show full processlist |  
                          ----- many rows which have no relevancy; only rows from this customer's app are shown ------
| 16341 | shared_db        | oak01.sitepalette.com:53237 | shared_db            | Sleep   |  308 |       | NULL                  | 
| 16342 | problem_db       | oak01.sitepalette.com:60716 | problem_db           | Sleep   |  307 |       | NULL                  | 
| 16344 | shared_db        | oak01.sitepalette.com:53241 | shared_db            | Sleep   |  308 |       | NULL                  | 
| 16346 | problem_db       | oak01.sitepalette.com:60720 | problem_db           | Sleep   |  308 |       | NULL                  |  
+-------+------------------+-----------------------------+----------------------+---------+------+-------+-----------------------+
Run Code Online (Sandbox Code Playgroud)

更新 #4:我将范围缩小到两个表的组合,categories上面详述的表和一个media_images有 556 行的表。如果media_images表包含少于 556 行,或者categories表包含少于 30 行,问题就会消失。就像我在这里遇到的某种 MySQL 限制一样......

更新 #5:我只是尝试将数据库完全移动到不同的 MySQL 服务器,问题就消失了......所以它与我的生产数据库服务器有关......

更新 #6:这是每次挂起的相关 PHP 代码:

    public function find($type,$conditions='',$order='',$limit='')
    {
            if($this->_link == self::AUTO_LINK)
                    $this->_link = DFStdLib::database_connect();

            if(is_resource($this->_link))
            {
                    $q = "SELECT ".($type==_COUNT?'COUNT(*)':'*')." FROM `{$this->_table}`";
                    if($conditions)
                    {
                            $q .= " WHERE $conditions";
                    }
                    if($order)
                    {
                            $q .= " ORDER BY $order";
                    }
                    if($limit)
                    {
                            $q .= " LIMIT $limit";
                    }

                    switch($type)
                    {
                            case _ALL:
                                    DFSkel::log(DFSkel::LOG_DEBUG,"mysql_query($q,$this->_link);");
                                    $res = @mysql_query($q,$this->_link);
                                    DFSkel::log(DFSkel::LOG_DEBUG,"res = $res");
Run Code Online (Sandbox Code Playgroud)

此代码正在生产中,在所有其他安装上都可以正常工作。只需安装一次,它就会挂在$res = @mysql_query($q,$this->_link);. 我知道是因为我mysql_query在调试日志中看到了,而不是 res =,当我strace处理 PHP 进程时,它挂在read(

更新 #whatever-it-is-I-hate-this-& (#^& -issue!这已经开始发生在我的两个客户身上。我刚刚发火tcpdump看起来MySQL 的响应从未完全发送。 TCP 流似乎在发送完整的 MySQL 响应之前挂起。(不过我仍在调查)

更新#I-have-gone-completly-crazy-but-it-works-now-kinda:好的,这没有意义,但我找到了解决方案。如果我为 MySQL 服务器的eth2接口分配第二个 IP 地址,并为 NFS 流量使用一个 IP,为 MySQL 使用第二个 IP,那么问题就会消失。就像我不知何故......如果两个 NFS+MySQL 流量都转到该 IP,则 Ip 地址超载。但这毫无意义,因为您不能“超载”IP 地址。饱和接口肯定,但它是相同的接口。

知道这里到底发生了什么吗?在这一点上,这可能是一个 unix.SE 或 ServerFault 问题......(至少它现在有效......)

更新#why-oh-why:这个问题仍然存在。即使使用两个不同的 IP,它也开始发生。我可以继续创建新的私有 IP,但显然出了点问题。

Der*_*ney 5

对于查询计划中究竟发生了什么的一般分析,您可以尝试PROFILING

它基本上可以帮助您确定挂断的位置。

当然,它只有在你编译 MySQL 时才有效enable-profiling


gbn*_*gbn 3

想法(不确定是否适用于 MyISAM,但我使用 InnoDB)

更改索引“parent”,使其位于 3 列上:parent、order、name。这与 WHERE .. ORDER BY 匹配

消除SELECT *。只取您需要的列。将任何其他列添加到索引“parent”

这将允许优化器仅 使用索引,因为它现在正在覆盖。就目前而言,您必须读取整个表,因为索引对该查询没有用处