Cosa potrebbe causare strani timeout di query tra PHP e MySQL?


11

Sono lo sviluppatore senior di un'applicazione Software-as-a-Service utilizzata da molti clienti diversi. Il nostro software funziona su un cluster di server applicazioni Apache / PHP, alimentato da un back-end MySQL. In una particolare istanza del software, il codice PHP per interrogare l'elenco dei nomi delle categorie è scaduto quando il cliente ha più di 29 categorie . So che questo non ha senso; non c'è nulla di speciale nel numero 30 che potrebbe spezzare questo e altri clienti hanno più di 30 categorie, tuttavia, il problema è riproducibile al 100% quando questa installazione ha 30 o più categorie e scompare quando ci sono meno di 30 categorie.

La tabella in questione è:

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 ;

Il codice in questione interroga ricorsivamente la tabella per recuperare tutte le categorie. Emette a

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

Quindi ripete questa query per ogni riga restituita, ma utilizzando WHERE parent=$category_idogni volta. (Sono sicuro che questa procedura potrebbe essere migliorata, ma questa è probabilmente un'altra domanda)

Per quanto ne so, la seguente query è sospesa per sempre:

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

Posso eseguire perfettamente questa query nel client mysql sul server e posso eseguirla anche in PHPMyAdmin senza problemi.

Si noti che non è quella query specifica che è il problema. Se ho DELETE FROM categories WHERE id=22quindi una query diversa simile a quella sopra si bloccherà. Inoltre, la query sopra restituisce zero righe quando la eseguo manualmente .

Sospettavo che la tabella può essere danneggiato, e ho provato REPAIR TABLEe OPTIMIZE TABLEma inferiore di questi segnalato problemi né risolto il problema. Ho lasciato cadere il tavolo e ricreato, ma il problema è tornato. Questa è esattamente la stessa struttura della tabella e il codice PHP che altri clienti utilizzano senza problemi per nessun altro, inclusi i clienti che hanno più di 30 categorie.

Il codice PHP non ricorre per sempre. (Questo non è un ciclo infinito)

Il server MySQL esegue CentOS Linux con mysqld Ver 5.0.92-community per pc-linux-gnu su i686 (MySQL Community Edition (GPL))

Il carico sul server MySQL è basso: media del carico: 0,58, 0,75, 0,73, Cpu: 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 trascurabile in uso (448k)

Come posso risolvere questo problema? Qualche suggerimento su cosa potrebbe succedere?

AGGIORNAMENTO: ho modificato TRUNCEla tabella e inserito 30 righe di dati fittizi:

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

Nessun genitore , tutte le categorie sono ai massimi livelli. il problema è ancora lì. La seguente query, eseguita da PHP, non riesce:

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

Ecco il 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)

AGGIORNAMENTO # 2: ora ho provato tutti i seguenti:

  1. Ho copiato questa tabella e i dati in un sito diverso con lo stesso software. Il problema non ha seguito la tabella. Sembra essere limitato a questo unico database.
  2. Ho cambiato l'indice come suggerito dalla risposta di gbn. Il problema è rimasto.
  3. Ho lasciato cadere la tabella e ricreato come InnoDBtabella e ho inserito le stesse 30 righe di test sopra. Il problema è rimasto.

Sospetto che debba essere qualcosa con questo database ...

AGGIORNAMENTO # 3: Ho completamente eliminato il database e ricreato con un nuovo nome, importando i suoi dati. Il problema rimane.

Ho scoperto che l'effettiva dichiarazione PHP che si blocca è una chiamata a mysql_query(). Le dichiarazioni dopo questa non vengono mai eseguite.

Mentre quella chiamata si blocca, MySQL elenca il thread come inattivo!

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

AGGIORNAMENTO # 4: l' ho ridotto alla combinazione di due tabelle, la categoriestabella sopra dettagliata e una media_imagestabella con 556 righe. Se la media_imagestabella contiene meno di 556 righe o la categoriestabella contiene meno di 30 righe, il problema scompare. È come se fosse una sorta di limite di MySQL che sto colpendo qui ...

AGGIORNAMENTO # 5: Ho appena provato a spostare il database su un altro server MySQL del tutto e il problema è andato via ... Quindi è legato al mio server di database di produzione ...

AGGIORNAMENTO N. 6: Ecco il codice PHP pertinente che si blocca ogni volta:

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

Questo codice è in produzione e funziona perfettamente su tutte le altre installazioni. Solo su una installazione, si blocca a $res = @mysql_query($q,$this->_link);. Lo so perché vedo il mysql_querylog di debug e non il res =, e quando ho straceil processo PHP, è bloccatoread(

AGGIORNAMENTO # qualunque sia-odio-odio-questo- & (# ^ & -issue! Questo è ora iniziato a accadere a due miei clienti. Ho appena fatto fuoco tcpdumpe sembra che la risposta di MySQL non sia mai stata inviata completamente. Il flusso TCP sembra bloccarsi prima di poter inviare l'intera risposta MySQL. (Sto ancora indagando)

AGGIORNAMENTO # Sono-completamente-impazzito-ma-funziona-ora-un-tipo: ok, questo non ha senso, ma ho trovato una soluzione. Se assegno un secondo indirizzo IP all'interfaccia del server MySQL eth2e utilizzo un IP per il traffico NFS e il secondo IP per MySQL, il problema scompare. È come se in qualche modo ... sovraccaricassi l'indirizzo IP se sia il traffico NFS + MySQL sia quello IP. Ma questo ha senso zero perché non è possibile "sovraccaricare" un indirizzo IP. Saturando sicuramente un'interfaccia, ma è la stessa interfaccia.

Hai idea di cosa diavolo sta succedendo qui? Questa è probabilmente una domanda unix.SE o ServerFault a questo punto ... (Almeno ora funziona ...)

AGGIORNAMENTO # why-oh-why: il problema persiste. Ha iniziato a succedere anche usando due IP diversi. Posso continuare a creare nuovi IP privati, ma chiaramente qualcosa non va.


Bene, ecco un link alla potenziale "altra domanda" su come fare una query gerarchica ricorsiva all'interno di mysql.
Derek Downey,

@DTest, lo aggiungerò tra un momento. Grazie per l'altro link!
Josh,


Ciao Josh. Hai detto che le query vengono eseguite normalmente all'interno del tuo client MySQL e in PHPMyAdmin? solo l'applicazione PHP si blocca?
marcio,

@marcioAlmada sì, è corretto. Sono estremamente confuso da tutta questa situazione.
Josh,

Risposte:


5

Per una profilazione generale di ciò che sta esattamente accadendo nel piano di query, puoi provare PROFILING

Fondamentalmente ti aiuterà a determinare dove si trova il riaggancio.

Ovviamente, funziona solo se hai compilato MySQL enable-profiling.


3

Idee (non sono sicuro se si applica a MyISAM, però, lavoro con InnoDB)

Cambia l'indice "parent" in modo che sia su 3 colonne: parent, order, name. Questo corrisponde a WHERE .. ORDER BY

Rimuovi SELECT *. Prendi solo le colonne che ti servono. Aggiungi eventuali altre colonne all'indice "padre"

Ciò consentirà all'ottimizzatore di utilizzare solo l'indice perché ora copre. Allo stato attuale, devi leggere l'intera tabella perché gli indici non sono utili per quella query


Il problema persiste dopo aver modificato l' parentindice in(parent, order, name)
Josh,

3

Vorrei controllare diverse cose sul server DB di produzione

  • Verifica n. 1: assicurarsi che il volume di dati su cui è installato / var / lib / mysql non presenti blocchi danneggiati. Ciò potrebbe richiedere tempi di inattività per eseguire fsck (controllo del file system)
  • Verifica n. 2: assicurarsi che la tabella non sia pesante con DML (INSERT / UPDATE / DELETE) o SELECTs
  • Controlla # 3: assicurati che PHP stia emettendo mysql_close () correttamente e che l'app non si basi su Apache per chiudere la connessione DB. Altrimenti, potresti avere una sorta di race condition quando PHP potrebbe tentare di utilizzare DB Connection Resources che sono state effettivamente chiuse da MySQL.
  • Verifica n. 4: assicurati che il sistema operativo del DB Server non abbia una scorta di TIME_WAIT nell'elenco di connessioni di netstat chiuse agli occhi di PHP e MySQL, ma il sistema operativo è ancora aggrappato. Puoi vederlo connetstat | grep -i mysql | grep TIME_WAIT
  • Verifica n. 5: assicurati di non utilizzare mysql_pconnect . Esiste ancora una segnalazione di bug aperta sulle connessioni persistenti che non si chiudono correttamente . Odio immaginare di provare ad accedere a queste connessioni.
  • Controllo n. 6: assicurarsi che il throughput del traffico DB tramite bilanciamento del carico, switch, firewall e server DNS siano identici per il server DB di produzione e altri server esterni. Personalmente, odio usare i nomi DNS nella colonna host di mysql.user e mysql.db. Di solito i client li eliminano e li sostituiscono con IP difficili. Aggiungo anche skip-host-cachee skip-name-resolveaggiro l'uso di DNS da parte di mysqld. Potrei quindi fare riferimento alla risposta di @ marcioAlmada come checkpoint per guardare oltre.

Se ritieni che nessuno di questi controlli sia utile, commenta APPENA POSSIBILE e fammi sapere in modo da poter rimuovere la mia risposta.


Penso sicuramente che questa sia una risposta utile! Io non sicuro sto chiudendo tutte le connessioni, in modo da poter provare che. Non credo che /varabbia blocchi danneggiati (è su un RAID10) ma potrei facilmente sbagliarmi. Controllerò netstat, buona idea lì! Non sto usando mysql_pconnectma controllerò rete / dns / ecc.
Josh,

@Josh: se vedi blocchi danneggiati, ci saranno molti messaggi su di essi dmesg. A meno che tu non abbia un RAID hardware, nel qual caso controlla il tuo programma di monitoraggio dei raid hardware.
derobert,

Quando ciò accade, a volte (ma non sempre) vedrò una singola TIME_WAITconnessione MySQL. Non ci sono molti numeri in alcun modo ... La tabella non è piena di attività.
Josh

2

a) Ciao Josh. Hai detto che le query vengono eseguite normalmente all'interno del tuo client MySQL e in PHPMyAdmin? solo l'applicazione PHP si blocca?
b) @marcioAlmada sì, è corretto

Direi che hai colpito schrödinbug . Potresti provare a fare una die()ricerca prima o prima e a cercare il codice per if statementscui ciò accade molto raramente. È difficile dire cosa si blocca quando non abbiamo il tuo codice.

EDIT: attualmente direi che potrebbe essere questa linea

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

che (suppongo) crea una connessione ogni volta che viene chiamata la funzione. Questo potrebbe essere il problema. Qual è il tuo max_connections in my.cnf?


So esattamente dove si blocca: non passa mai una chiamata amysql_query()
Josh,

1
Potresti pubblicare + - 10 righe del tuo codice?
Genesi,

fatto. Ho intenzione di eseguire il debug di questo tcpdump nei prossimi giorni. Se questo è davvero un problema con PHP, dovrei pubblicare una nuova domanda su SO.
Josh,

@Josh: AGGIORNATO la mia risposta
genesi,

Grazie @genesi ... ma non è così, per due motivi. 1. che il codice viene chiamato solo se sto usando la mia "stabilire automaticamente un database link" caratteristica, che è fatto impostando $this->_linkad una costante: self::AUTO_LINK. 2. Anche se lo fossi, quel codice è in un if:, if($this->_link == self::AUTO_LINK)e la riga successiva $this->_link = DFStdLib::database_connect();cambia il valore di $this->_linkcosì ifnon verrebbe eseguito di nuovo. Sono sicuro che esiste una sola connessione al database per thread. (Vedi la lista dei processi)
Josh,

1

Sono quasi convinto che si tratti di un problema PHP piuttosto che di un problema MySQL, ma perché funziona quando cambio server MySQL?

Alcuni tentativi:

  • I firewall ?? Esiste un firewall che blocca l'applicazione e impedisce che venga inoltrata una richiesta al server del database di produzione o viceversa?

  • Stai utilizzando un nome di dominio nella configurazione della connessione o un indirizzo IP? L'uso di un nome di dominio potrebbe rallentare un po 'l'interazione del database e questo, combinato con un breve tempo massimo di esecuzione dello script PHP , causerebbe un hangout per sempre

Quest'ultimo suggerimento sembra spiegare lo strano comportamento delle variabili quando si cambia server di database. Uno potrebbe rispondere molto più velocemente dell'altro e poiché per ogni record trovato avrai una query secondaria, quell'ipotesi spiegherebbe perché l'applicazione ritarda solo con un certo numero di risultati interrogati (> 30).

Almeno siamo arrivati ​​a una conclusione primaria. Sicuramente il problema non riguarda il server MySQL. Ho dato un'occhiata alla documentazione e sembra che non ci siano limiti di funzionalità adatti alla tua situazione specifica, inoltre non ho mai avuto problemi con le tabelle ricorsive e la quantità specifica di voci.

Spero possa aiutare.


0

Hai provato ad aggiornare il comando mysql_query () per essere un driver PHP5 nativo? mysqli :: query ()? Non sono sicuro che questo farebbe qualsiasi cosa, ma potrebbe valere la pena provare.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.