Qu'est-ce qui pourrait provoquer d'étranges délais d'expiration des requêtes entre PHP et MySQL?

11

Je suis le développeur principal d'une application Software-as-a-Service utilisée par de nombreux clients différents. Notre logiciel fonctionne sur un cluster de serveurs d'applications Apache / PHP, alimenté par un backend MySQL. Sur une instance particulière du logiciel, le code PHP pour interroger la liste des noms de catégories expire lorsque le client a plus de 29 catégories . Je sais que cela n'a aucun sens; il n'y a rien de spécial dans le nombre 30 qui casserait cela et d'autres clients ont beaucoup plus de 30 catégories, cependant, le problème est reproductible à 100% lorsque cette installation a 30 catégories ou plus et disparaît lorsqu'il y a moins de 30 catégories.

Le tableau en question est:

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 ;

Le code en question interroge récursivement la table pour récupérer toutes les catégories. Il délivre un

SELECT * FROM `categories` WHERE `parent`=0 ORDER BY `order`,`name`

Et puis répète cette requête pour chaque ligne retournée, mais en utilisant à WHERE parent=$category_idchaque fois. (Je suis sûr que cette procédure pourrait être améliorée, mais c'est probablement une autre question)

Pour autant que je sache, la requête suivante est suspendue pour toujours:

SELECT * FROM `categories` WHERE `parent`=22 ORDER BY `order`,`name`

Je peux parfaitement exécuter cette requête dans le client mysql sur le serveur, et je peux également l'exécuter dans PHPMyAdmin sans problème.

Notez que ce n'est pas cette requête spécifique qui est le problème. Si je DELETE FROM categories WHERE id=22puis une autre requête similaire à celle ci-dessus va se bloquer. En outre, la requête ci-dessus renvoie zéro ligne lorsque je l'exécute manuellement .

Je soupçonnais que la table était peut-être corrompue, et j'ai essayé REPAIR TABLE, OPTIMIZE TABLEmais aucun de ces problèmes signalés ni résolu le problème. J'ai laissé tomber la table et recréé, mais le problème est revenu. C'est exactement la même structure de table et le code PHP que d'autres clients utilisent sans aucun problème pour quiconque, y compris les clients qui ont beaucoup plus de 30 catégories.

Le code PHP n'est pas récurrent pour toujours. (Ce n'est pas une boucle infinie)

Le serveur MySQL exécute CentOS linux avec la communauté mysqld Ver 5.0.92 pour pc-linux-gnu sur i686 (MySQL Community Edition (GPL))

La charge sur le serveur MySQL est faible: charge moyenne: 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. Swap négligeable utilisé (448k)

Comment puis-je résoudre ce problème? Avez-vous des suggestions sur ce qui pourrait se passer?

MISE À JOUR: J'ai TRUNCEédité le tableau et inséré 30 lignes de données fictives:

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);

Pas de parents du tout , toutes les catégories sont au plus haut niveau. problème est toujours là. La requête suivante, exécutée par PHP, échoue:

SELECT * FROM `categories` WHERE `parent`=22 ORDER BY `order`,`name`

Voici le 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)

MISE À JOUR # 2: J'ai maintenant essayé tout ce qui suit:

  1. J'ai copié ce tableau et ces données sur un autre site avec le même logiciel. Le problème n'a pas suivi le tableau. Il semble être limité à cette seule base de données.
  2. J'ai changé l'index comme suggéré par la réponse de gbn. Le problème est resté.
  3. J'ai laissé tomber le tableau et recréé comme un InnoDBtableau et inséré les mêmes 30 lignes de test ci-dessus. Le problème est resté.

Je suppose que ça doit être quelque chose avec cette base de données ...

MISE À JOUR # 3: J'ai complètement supprimé la base de données et l'ai recréée sous un nouveau nom, en important ses données. Le problème demeure.

J'ai trouvé que l'instruction PHP réelle qui se bloque est un appel à mysql_query(). Les instructions après cela ne sont jamais exécutées.

Pendant que cet appel se bloque, MySQL répertorie le thread comme étant en veille!

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                  |  
+-------+------------------+-----------------------------+----------------------+---------+------+-------+-----------------------+

MISE À JOUR # 4: Je l'ai réduit à la combinaison de deux tableaux, le categoriestableau détaillé ci-dessus et un media_imagestableau avec 556 lignes. Si le media_imagestableau contient moins de 556 lignes ou s'il categoriescontient moins de 30 lignes, le problème disparaît. C'est comme une sorte de limite MySQL que je frappe ici ...

MISE À JOUR # 5: J'ai juste essayé de déplacer la base de données vers un autre serveur MySQL et le problème a disparu ... Donc, c'est lié à mon serveur de base de données de production ...

MISE À JOUR # 6: Voici le code PHP pertinent qui se bloque à chaque fois:

    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");

Ce code est en production et fonctionne très bien sur toutes les autres installations. Sur une seule installation, il se bloque $res = @mysql_query($q,$this->_link);. Je sais parce que je vois le mysql_querydans le journal de débogage, et non le res =, et quand je stracele processus PHP, il est suspendu àread(

MISE À JOUR # quoi que ce soit-je-je-déteste-& (# ^ & -issue! Cela a maintenant commencé à arriver à deux de mes clients. Je viens de démarrer tcpdumpet il semble que la réponse de MySQL ne soit jamais envoyée complètement. Le flux TCP semble juste se bloquer avant que la réponse complète de MySQL puisse être envoyée (j'étudie toujours cependant).

MISE À JOUR # Je-suis-allé-complètement-fou-mais-ça-marche-maintenant-un peu: Ok, cela n'a aucun sens, mais j'ai trouvé une solution. Si j'attribue une deuxième adresse IP à l' eth2interface du serveur MySQL et que j'utilise une IP pour le trafic NFS et la deuxième IP pour MySQL, le problème disparaît. C'est comme si j'étais en quelque sorte ... en train de surcharger l'adresse IP si les deux trafics NFS + MySQL vont tous les deux vers cette IP. Mais cela n'a aucun sens car vous ne pouvez pas "surcharger" une adresse IP. Saturer une interface bien sûr, mais c'est la même interface.

Une idée de ce qui se passe ici? Il s'agit probablement d'une question unix.SE ou ServerFault à ce stade ... (Au moins, cela fonctionne maintenant ...)

UPDATE # why-oh-why: Ce problème persiste. Cela a commencé à se produire même en utilisant deux adresses IP différentes. Je peux continuer à créer de nouvelles adresses IP privées, mais il est clair que quelque chose ne va pas.

Josh
la source
Eh bien, voici un lien vers la potentielle `` autre question '' sur la réalisation d'une requête hiérarchique récursive dans mysql.
Derek Downey
@DTest sûr, je vais ajouter cela dans un instant. Merci pour l'autre lien!
Josh
Nous essayons activement de résoudre ce problème dans le chat pour quiconque trouve cette question.
Josh
Salut Josh. Vous avez dit que les requêtes s'exécutaient normalement dans votre client MySQL et dans PHPMyAdmin? seule l'application PHP se bloque?
marcio
@marcioAlmada oui, c'est exact. Je suis extrêmement confus par toute cette situation.
Josh

Réponses:

5

Pour un profil général de ce qui se passe exactement dans le plan de requête, vous pouvez essayer PROFILING

Cela vous aidera essentiellement à déterminer où se trouve le raccrochage.

Bien sûr, cela ne fonctionne que si vous avez compilé MySQL avec enable-profiling.

Derek Downey
la source
3

Idées (je ne sais pas si s'applique à MyISAM, je travaille avec InnoDB)

Modifiez l'index "parent" pour qu'il soit sur 3 colonnes: parent, ordre, nom. Cela correspond à OERE .. COMMANDER PAR

Retirez SELECT *. Prenez uniquement les colonnes dont vous avez besoin. Ajoutez toutes les autres colonnes à l'index "parent"

Cela permettra à l'optimiseur d'utiliser uniquement l'index car il couvre désormais. En l'état, vous devez lire la table entière car les index ne sont pas utiles pour cette requête

gbn
la source
Le problème persiste après avoir changé l' parentindex en(parent, order, name)
Josh
3

Je voudrais vérifier plusieurs choses sur le serveur DB de production

  • Vérification n ° 1: assurez-vous que le volume de données sur lequel / var / lib / mysql est monté n'a pas de blocs défectueux. Cela peut nécessiter un temps d'arrêt pour effectuer fsck (vérification du système de fichiers)
  • Contrôle n ° 2: assurez-vous que le tableau n'est pas lourd avec DML (INSERT / UPDATE / DELETE) ou SELECTs
  • Contrôle n ° 3: assurez-vous que PHP émet correctement mysql_close () et que l'application ne s'appuie pas sur Apache pour fermer la connexion DB pour vous. Sinon, vous pourriez avoir une sorte de condition de concurrence critique lorsque PHP pourrait tenter d'utiliser des ressources de connexion DB qui ont été effectivement fermées par MySQL.
  • Contrôle n ° 4: assurez-vous que le système d'exploitation du serveur DB n'a pas de stock de TIME_WAIT dans la liste netstat des connexions qui ont été fermées aux yeux de PHP et MySQL, mais le système d'exploitation est toujours accroché. Vous pouvez le voir avecnetstat | grep -i mysql | grep TIME_WAIT
  • Contrôle n ° 5: assurez-vous que vous n'utilisez pas mysql_pconnect . Il existe toujours un rapport de bogue ouvert sur les connexions persistantes qui ne se ferment pas correctement . Je déteste imaginer essayer d'accéder à ces connexions.
  • Contrôle n ° 6: assurez-vous que le débit du trafic DB via les équilibreurs de charge, les commutateurs, les pare-feu et les serveurs DNS est identique pour le serveur DB de production et les autres serveurs externes. Personnellement, je déteste utiliser des noms DNS dans la colonne hôte de mysql.user et mysql.db. J'ai généralement des clients les supprimer et les remplacer par des adresses IP dures. J'ajoute également skip-host-cacheet skip-name-resolvepour contourner l'utilisation de mysqld du DNS. Je pourrais ainsi comprendre la réponse de @ marcioAlmada comme un point de contrôle à examiner.

Si vous pensez qu'aucune de ces vérifications n'est utile, veuillez commenter dès que possible et faites le moi savoir afin que je puisse supprimer ma réponse.

RolandoMySQLDBA
la source
Je pense vraiment que c'est une réponse utile! Je ne suis pas sûr de fermer toutes les connexions, je peux donc essayer. Je ne pense pas qu'il y /varait de mauvais blocs (c'est sur un RAID10) mais je peux facilement me tromper. Je vais vérifier netstat, bonne idée là-bas! Je n'utilise pas mysql_pconnectmais vérifierai le réseau / DNS / etc.
Josh
@Josh: Si vous voyez de mauvais blocs, il y aura beaucoup de messages à leur sujet dmesg. À moins que vous n'ayez un RAID matériel, dans ce cas, vérifiez votre programme de surveillance du matériel.
derobert
Lorsque cela se produit, je verrai parfois (mais pas toujours) une seule TIME_WAITconnexion MySQL. Il n'y en a en aucun cas un grand nombre ... La table n'est pas chargée d'activité.
Josh
2

a) Salut Josh. Vous avez dit que les requêtes s'exécutaient normalement dans votre client MySQL et dans PHPMyAdmin? seule l'application PHP se bloque?
b) @marcioAlmada oui, c'est exact

Je dirais que vous avez frappé schrödinbug . Vous pourriez essayer die()après ou avant votre requête et essayer de parcourir votre code pour if statementsce qui se produit très rarement. Il est difficile de dire ce qui se bloque lorsque nous n'avons pas votre code.

EDIT: Je dirais actuellement que ce pourrait être cette ligne

$this->_link = DFStdLib::database_connect();

qui (je suppose) crée une connexion chaque fois que la fonction est appelée. C'est peut-être le problème. Quelles sont vos max_connections dans my.cnf?

genèse
la source
Je sais exactement où il se bloque: il ne passe jamais un appel àmysql_query()
Josh
1
Pourriez-vous publier + - 10 lignes de votre code?
genesis
terminé. Je vais déboguer avec tcpdump dans les prochains jours. Si ce vraiment est un problème de PHP, alors je posterai une nouvelle question sur le SO.
Josh
@Josh: MISE À JOUR de ma réponse
genesis
Merci @genesis ... mais ce n'est pas ça, pour deux raisons. 1. que le code est appelé uniquement si je me sers de mon « établir automatiquement un lien de base de données » caractéristique, qui se fait par la mise $this->_linkà une constante: self::AUTO_LINK. 2. Même si je l'étais, ce code est dans un if:, if($this->_link == self::AUTO_LINK)et la ligne suivante $this->_link = DFStdLib::database_connect();change la valeur de $this->_linkafin que le ifne soit pas exécuté à nouveau. Je suis sûr qu'il n'y a qu'une seule connexion à la base de données par thread. (Voir la liste des processus)
Josh
1

Je suis presque convaincu qu'il s'agit d'un problème PHP plutôt que d'un problème MySQL, mais pourquoi cela fonctionne-t-il lorsque je change de serveur MySQL?

Quelques tentatives:

  • Pare-feu ?? Existe-t-il un pare-feu bloquant votre application et l'empêchant de faire une demande à votre serveur de base de données de production ou vice versa?

  • Utilisez-vous un nom de domaine dans votre configuration de connexion ou une adresse IP? L'utilisation d'un nom de domaine pourrait ralentir un peu l'interaction de la base de données, ce qui, combiné à un court délai d'exécution maximal du script PHP , entraînerait un hangout permanent

Cette dernière suggestion semble expliquer l'étrange comportement des variables lors du changement de serveur de base de données. L'un pourrait répondre beaucoup plus rapidement que l'autre, et puisque pour chaque enregistrement trouvé, vous aurez une requête secondaire, cette hypotèse expliquerait pourquoi l'application ne retarde qu'avec un certain nombre de résultats interrogés (> 30).

Au moins, nous sommes arrivés à une conclusion principale. Le problème n'est certainement pas avec le serveur MySQL lui-même. J'ai regardé la documentation et il ne semble pas y avoir de limites de fonctionnalités adaptées à votre situation spécifique, je n'ai jamais eu de problème avec les tableaux récursifs et la quantité spécifique d'entrées.

J'espère que cela pourra aider.

marcio
la source
0

Avez-vous essayé de mettre à jour la commande mysql_query () pour devenir un pilote PHP5 natif? mysqli :: query ()? Je ne suis pas sûr que cela ferait quoi que ce soit, mais cela pourrait valoir le coup.

DevelumPHP
la source