¿Cómo depurar el tiempo de espera de bloqueo excedido en MySQL?

269

En mis registros de errores de producción ocasionalmente veo:

SQLSTATE [HY000]: Error general: 1205 Tiempo de espera de bloqueo excedido; intente reiniciar la transacción

Sé qué consulta está intentando acceder a la base de datos en ese momento, pero ¿hay alguna manera de averiguar qué consulta tenía el bloqueo en ese momento preciso?

Matt McCormick
fuente
1
Les sugiero a todos que den una oportunidad a la respuesta de Eirik
kommradHomer

Respuestas:

261

Lo que delata esto es la palabra transacción . Es evidente por la declaración que la consulta intentaba cambiar al menos una fila en una o más tablas InnoDB.

Como conoce la consulta, todas las tablas a las que se accede son candidatos para ser el culpable.

A partir de ahí, deberías poder correr SHOW ENGINE INNODB STATUS\G

Debería poder ver las tablas afectadas

Obtiene todo tipo de información adicional de bloqueo y mutex.

Aquí hay una muestra de uno de mis clientes:

mysql> show engine innodb status\G
*************************** 1. row ***************************
  Type: InnoDB
  Name:
Status:
=====================================
110514 19:44:14 INNODB MONITOR OUTPUT
=====================================
Per second averages calculated from the last 4 seconds
----------
SEMAPHORES
----------
OS WAIT ARRAY INFO: reservation count 9014315, signal count 7805377
Mutex spin waits 0, rounds 11487096053, OS waits 7756855
RW-shared spins 722142, OS waits 211221; RW-excl spins 787046, OS waits 39353
------------------------
LATEST FOREIGN KEY ERROR
------------------------
110507 21:41:35 Transaction:
TRANSACTION 0 606162814, ACTIVE 0 sec, process no 29956, OS thread id 1223895360 updating or deleting, thread declared inside InnoDB 499
mysql tables in use 1, locked 1
14 lock struct(s), heap size 3024, 8 row lock(s), undo log entries 1
MySQL thread id 3686635, query id 124164167 10.64.89.145 viget updating
DELETE FROM file WHERE file_id in ('6dbafa39-7f00-0001-51f2-412a450be5cc' )
Foreign key constraint fails for table `backoffice`.`attachment`:
,
  CONSTRAINT `attachment_ibfk_2` FOREIGN KEY (`file_id`) REFERENCES `file` (`file_id`)
Trying to delete or update in parent table, in index `PRIMARY` tuple:
DATA TUPLE: 17 fields;
 0: len 36; hex 36646261666133392d376630302d303030312d353166322d343132613435306265356363; asc 6dbafa39-7f00-0001-51f2-412a450be5cc;; 1: len 6; hex 000024214f7e; asc   $!O~;; 2: len 7; hex 000000400217bc; asc    @   ;; 3: len 2; hex 03e9; asc   ;; 4: len 2; hex 03e8; asc   ;; 5: len 36; hex 65666635323863622d376630302d303030312d336632662d353239626433653361333032; asc eff528cb-7f00-0001-3f2f-529bd3e3a302;; 6: len 40; hex 36646234376337652d376630302d303030312d353166322d3431326132346664656366352e6d7033; asc 6db47c7e-7f00-0001-51f2-412a24fdecf5.mp3;; 7: len 21; hex 416e67656c73204e6f7720436f6e666572656e6365; asc Angels Now Conference;; 8: len 34; hex 416e67656c73204e6f7720436f6e666572656e6365204a756c7920392c2032303131; asc Angels Now Conference July 9, 2011;; 9: len 1; hex 80; asc  ;; 10: len 8; hex 8000124a5262bdf4; asc    JRb  ;; 11: len 8; hex 8000124a57669dc3; asc    JWf  ;; 12: SQL NULL; 13: len 5; hex 8000012200; asc    " ;; 14: len 1; hex 80; asc  ;; 15: len 2; hex 83e8; asc   ;; 16: len 4; hex 8000000a; asc     ;;

But in child table `backoffice`.`attachment`, in index `PRIMARY`, there is a record:
PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 30; hex 36646261666133392d376630302d303030312d353166322d343132613435; asc 6dbafa39-7f00-0001-51f2-412a45;...(truncated); 1: len 30; hex 38666164663561652d376630302d303030312d326436612d636164326361; asc 8fadf5ae-7f00-0001-2d6a-cad2ca;...(truncated); 2: len 6; hex 00002297b3ff; asc   "   ;; 3: len 7; hex 80000040070110; asc    @   ;; 4: len 2; hex 0000; asc   ;; 5: len 30; hex 416e67656c73204e6f7720436f6e666572656e636520446f63756d656e74; asc Angels Now Conference Document;;

------------
TRANSACTIONS
------------
Trx id counter 0 620783814
Purge done for trx's n:o < 0 620783800 undo n:o < 0 0
History list length 35
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 0 0, not started, process no 29956, OS thread id 1192212800
MySQL thread id 5341758, query id 189708501 127.0.0.1 lwdba
show innodb status
---TRANSACTION 0 620783788, not started, process no 29956, OS thread id 1196472640
MySQL thread id 5341773, query id 189708353 10.64.89.143 viget
---TRANSACTION 0 0, not started, process no 29956, OS thread id 1223895360
MySQL thread id 5341667, query id 189706152 10.64.89.145 viget
---TRANSACTION 0 0, not started, process no 29956, OS thread id 1227888960
MySQL thread id 5341556, query id 189699857 172.16.135.63 lwdba
---TRANSACTION 0 620781112, not started, process no 29956, OS thread id 1222297920
MySQL thread id 5341511, query id 189696265 10.64.89.143 viget
---TRANSACTION 0 620783736, not started, process no 29956, OS thread id 1229752640
MySQL thread id 5339005, query id 189707998 10.64.89.144 viget
---TRANSACTION 0 620783785, not started, process no 29956, OS thread id 1198602560
MySQL thread id 5337583, query id 189708349 10.64.89.145 viget
---TRANSACTION 0 620783469, not started, process no 29956, OS thread id 1224161600
MySQL thread id 5333500, query id 189708478 10.64.89.144 viget
---TRANSACTION 0 620781240, not started, process no 29956, OS thread id 1198336320
MySQL thread id 5324256, query id 189708493 10.64.89.145 viget
---TRANSACTION 0 617458223, not started, process no 29956, OS thread id 1195141440
MySQL thread id 736, query id 175038790 Has read all relay log; waiting for the slave I/O thread to update it
--------
FILE I/O
--------
I/O thread 0 state: waiting for i/o request (insert buffer thread)
I/O thread 1 state: waiting for i/o request (log thread)
I/O thread 2 state: waiting for i/o request (read thread)
I/O thread 3 state: waiting for i/o request (write thread)
Pending normal aio reads: 0, aio writes: 0,
 ibuf aio reads: 0, log i/o's: 0, sync i/o's: 0
Pending flushes (fsync) log: 0; buffer pool: 0
519878 OS file reads, 18962880 OS file writes, 13349046 OS fsyncs
0.00 reads/s, 0 avg bytes/read, 6.25 writes/s, 4.50 fsyncs/s
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 1, free list len 1190, seg size 1192,
174800 inserts, 174800 merged recs, 54439 merges
Hash table size 35401603, node heap has 35160 buffer(s)
0.50 hash searches/s, 11.75 non-hash searches/s
---
LOG
---
Log sequence number 28 1235093534
Log flushed up to   28 1235093534
Last checkpoint at  28 1235091275
0 pending log writes, 0 pending chkp writes
12262564 log i/o's done, 3.25 log i/o's/second
----------------------
BUFFER POOL AND MEMORY
----------------------
Total memory allocated 18909316674; in additional pool allocated 1048576
Dictionary memory allocated 2019632
Buffer pool size   1048576
Free buffers       175763
Database pages     837653
Modified db pages  6
Pending reads 0
Pending writes: LRU 0, flush list 0, single page 0
Pages read 770138, created 108485, written 7795318
0.00 reads/s, 0.00 creates/s, 4.25 writes/s
Buffer pool hit rate 1000 / 1000
--------------
ROW OPERATIONS
--------------
0 queries inside InnoDB, 0 queries in queue
1 read views open inside InnoDB
Main thread process no. 29956, id 1185823040, state: sleeping
Number of rows inserted 6453767, updated 4602534, deleted 3638793, read 388349505551
0.25 inserts/s, 1.25 updates/s, 0.00 deletes/s, 2.75 reads/s
----------------------------
END OF INNODB MONITOR OUTPUT
============================

1 row in set, 1 warning (0.00 sec)

Debería considerar aumentar el valor de tiempo de espera de bloqueo para InnoDB configurando innodb_lock_wait_timeout , el valor predeterminado es 50 segundos

mysql> show variables like 'innodb_lock_wait_timeout';
+--------------------------+-------+
| Variable_name            | Value |
+--------------------------+-------+
| innodb_lock_wait_timeout | 50    |
+--------------------------+-------+
1 row in set (0.01 sec)

Puede establecerlo en un valor más alto de forma /etc/my.cnfpermanente con esta línea

[mysqld]
innodb_lock_wait_timeout=120

y reinicie mysql. Si no puede reiniciar mysql en este momento, ejecute esto:

SET GLOBAL innodb_lock_wait_timeout = 120; 

También puede configurarlo para la duración de su sesión

SET innodb_lock_wait_timeout = 120; 

seguido de su consulta

RolandoMySQLDBA
fuente
55
Para el InnoDB incorporado, la innodb_lock_wait_timeoutvariable solo se puede configurar al iniciar el servidor. Para InnoDB Plugin, se puede configurar al inicio o cambiarse en tiempo de ejecución, y tiene valores globales y de sesión.
Timo Huovinen
1
Hola @rolandomysqldba, ¿pueden darme su sugerencia en esta publicación? Stackoverflow.com/questions/18267565/…
Manish Sapkal
2
Recibo este error cuando intento ejecutar la primera consulta:SQL Error (1064): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '\G' at line 1
Iulian Onofrei
1
@Pacerier Cada vez que se reinicia mysqld, debe SET GLOBAL innodb_lock_wait_timeout = 120;volver a ejecutarlo . Si /etc/my.cnftiene la opción, innodb_lock_wait_timeoutestá configurado para usted. No todos tienen el privilegio SUPER de cambiarlo globalmente para todos los demás ( dev.mysql.com/doc/refman/5.6/en/… )
RolandoMySQLDBA
3
@IulianOnofrei el carácter \ G es una característica especial de la línea de comando de MySQL y cambia la forma en que se muestra la salida. Para otros clientes MySQL, solo use un punto y coma regular.
thenickdude
83

Como alguien mencionó en uno de los muchos subprocesos SO relacionados con este problema: ¡A veces el proceso que ha bloqueado la tabla aparece como inactivo en la lista de procesos! Me estaba arrancando el pelo hasta que maté todos los hilos dormidos que estaban abiertos en la base de datos en cuestión (ninguno estaba activo en ese momento). Eso finalmente desbloqueó la tabla y dejó que se ejecutara la consulta de actualización.

El comentarista dijo algo parecido a "A veces un hilo de MySQL bloquea una mesa, luego duerme mientras espera que suceda algo no relacionado con MySQL".

Después de volver a revisar el show engine innodb statusregistro (una vez que rastreé al cliente responsable del bloqueo), noté que el hilo atascado en cuestión estaba en la parte inferior de la lista de transacciones, debajo de las consultas activas que estaban a punto de error fuera debido a la cerradura congelada:

------------------
---TRANSACTION 2744943820, ACTIVE 1154 sec(!!)
2 lock struct(s), heap size 376, 2 row lock(s), undo log entries 1
MySQL thread id 276558, OS thread handle 0x7f93762e7710, query id 59264109 [ip] [database] cleaning up
Trx read view will not see trx with id >= 2744943821, sees < 2744943821

(no estoy seguro si el mensaje "Vista de lectura de Trx" está relacionado con el bloqueo congelado, pero a diferencia de las otras transacciones activas, esta no aparece con la consulta que se emitió y en su lugar afirma que la transacción se está "limpiando", pero tiene múltiples bloqueos de fila)

La moraleja de la historia es que una transacción puede estar activa aunque el hilo esté inactivo .

Eric L.
fuente
2
No puedo decir que me salvaste la vida, pero seguro que decidiste por la paz. Al leer su respuesta, encontré un hilo espeluznante que está activo durante 3260 segundos y que no aparece en ningún lado. ¡Después de matarlo, todos mis problemas fueron resueltos!
kommradHomer
Este fue mi problema. Una transacción inactiva con un tiempo de 20,000 segundos que impedía que el trabajo retrasado en una aplicación Rails se ejecutara correctamente. Gracias @Eirik
bigtex777
¿Alguna idea de por qué una transacción suspendida no se anula de todos modos? Por ejemplo, ¿hay un tiempo de espera que pueda establecer para que una transacción finalice?
patrickdavey
1
Otros comandos que pueden ser útiles en su búsqueda de transacciones de bloqueo: show processlist;para mostrar una lista exhaustiva de los procesos que se ejecutan actualmente, lo cual es bueno porque es una versión condensada de show engine innodb status\g. Además, si su base de datos está en una instancia de Amazon RDS, puede usarla CALL mysql.rds_kill(<thread_id>);para eliminar subprocesos. Creo que tiene permisos más altos, porque me permitió matar más procesos que los simples kill <thread_id>;: tenga en cuenta que estos deberían ejecutarse dentro de MySQL CLI
Nickang
1
Alguien tiene una fuente para esto, ¿tal vez una página de documentación que indique que los bloqueos se colocan antes de la fase de COMPROMISO? No pude encontrar nada, a pesar de ver este problema exacto y se resolvió al matar el hilo dormido que sujetaba las cerraduras.
Erin Schoonover
42

Debido a la popularidad de MySQL, no es de extrañar que se haya excedido el tiempo de espera de bloqueo; Intentar reiniciar la excepción de transacción recibe tanta atención en SO.

Cuanta más contención tenga, mayor será la posibilidad de puntos muertos, que un motor de DB resolverá al agotar el tiempo de una de las transacciones bloqueadas. Además, las transacciones de larga duración que han modificado (por ejemplo, UPDATEo DELETE) una gran cantidad de entradas (que toman bloqueos para evitar anomalías de escritura sucia como se explica en el libro de Persistencia Java de alto rendimiento ) tienen más probabilidades de generar conflictos con otras transacciones.

Aunque InnoDB MVCC, aún puede solicitar bloqueos explícitos utilizando la FOR UPDATEcláusula . Sin embargo, a diferencia de otras bases de datos populares (Oracle, MSSQL, PostgreSQL, DB2), MySQL usa REPEATABLE_READcomo nivel de aislamiento predeterminado .

Ahora, los bloqueos que adquirió (ya sea modificando filas o utilizando bloqueo explícito), se mantienen durante la transacción que se ejecuta actualmente. Si quieres una buena explicación de la diferencia entre REPEATABLE_READy READ COMMITTEDen cuanto a la fijación, por favor lea este artículo Percona .

En REPEATABLE READ, cada bloqueo adquirido durante una transacción se mantiene durante la duración de la transacción.

En READ COMMITTED, los bloqueos que no coincidían con el escaneo se liberan después de que se completa la DECLARACIÓN.

...

Esto significa que en READ COMMITTED otras transacciones son libres de actualizar filas que no habrían podido actualizar (en REPEATABLE READ) una vez que se completa la instrucción UPDATE.

Por lo tanto: cuanto más restrictivo sea el nivel de aislamiento ( REPEATABLE_READ, SERIALIZABLE) mayor será la posibilidad de un punto muerto. Esto no es un problema "per se", es una compensación.

Puede obtener muy buenos resultados READ_COMMITED, ya que necesita prevención de actualizaciones perdidas a nivel de aplicación cuando utiliza transacciones lógicas que abarcan múltiples solicitudes HTTP. El enfoque de bloqueo optimista apunta a las actualizaciones perdidas que pueden ocurrir incluso si usa el SERIALIZABLEnivel de aislamiento mientras reduce la contención de bloqueo al permitirle usar READ_COMMITED.

Vlad Mihalcea
fuente
44
¿No es el tiempo de espera de bloqueo diferente al punto muerto? Por ejemplo, si un subproceso mantiene un bloqueo durante 60 segundos por razones legítimas, puede ocurrir el tiempo de espera de bloqueo. ¿No es cierto que si realmente hay un punto muerto, MySQL lo detectará y eliminará una transacción al instante y esto no está relacionado con el tiempo de espera de bloqueo?
ColinM
1
Tienes razón. La base de datos detecta el bloqueo muerto después del tiempo de espera y elimina un proceso de espera, por lo que una transacción gana mientras que la otra falla. Pero cuanto más tiempo mantenga presionado un bloqueo, menos escalable será la aplicación. Incluso si no se encuentra con puntos muertos, aumentará la parte serializable del comportamiento de tiempo de ejecución de su aplicación.
Vlad Mihalcea
19

Para el registro, la excepción de tiempo de espera de bloqueo también ocurre cuando hay un punto muerto y MySQL no puede detectarlo, por lo que simplemente se agota el tiempo de espera. Otra razón podría ser una consulta de ejecución extremadamente larga, que es más fácil de resolver / reparar, sin embargo, y no describiré este caso aquí.

MySQL generalmente puede lidiar con puntos muertos si se construyen "correctamente" dentro de dos transacciones. MySQL luego simplemente mata / revierte la transacción que posee menos bloqueos (es menos importante ya que afectará menos filas) y deja que la otra termine.

Ahora, supongamos que hay dos procesos A y B y 3 transacciones:

Process A Transaction 1: Locks X
Process B Transaction 2: Locks Y
Process A Transaction 3: Needs Y => Waits for Y
Process B Transaction 2: Needs X => Waits for X
Process A Transaction 1: Waits for Transaction 3 to finish

(see the last two paragraph below to specify the terms in more detail)

=> deadlock 

Esta es una configuración muy desafortunada porque MySQL no puede ver que hay un punto muerto (abarcado en 3 transacciones). Entonces, lo que hace MySQL es ... ¡nada! Simplemente espera, ya que no sabe qué hacer. Espera hasta que el primer bloqueo adquirido exceda el tiempo de espera (Proceso A, Transacción 1: Bloqueos X), luego esto desbloqueará el Bloqueo X, que desbloquea la Transacción 2, etc.

El arte es descubrir qué (qué consulta) causa el primer bloqueo (Bloqueo X). Podrá ver fácilmente ( show engine innodb status) que la Transacción 3 espera la Transacción 2, pero no verá qué transacción está esperando la Transacción 2 (Transacción 1). MySQL no imprimirá ningún bloqueo o consulta asociada con la Transacción 1. La única pista será que en la parte inferior de la lista de transacciones (de la show engine innodb statuscopia impresa), verá que la Transacción 1 aparentemente no hace nada (pero de hecho espera que la Transacción 3 terminar).

Aquí se describe la técnica sobre cómo encontrar qué consulta SQL hace que se otorgue el bloqueo (Bloqueo X) para una transacción determinada que está esperando Tracking MySQL query history in long running transactions

Si se pregunta cuál es el proceso y la transacción exactamente en el ejemplo. El proceso es un proceso PHP. Transacción es una transacción definida por innodb-trx-table . En mi caso, tenía dos procesos PHP, en cada uno comencé una transacción manualmente. La parte interesante fue que, aunque comencé una transacción en un proceso, MySQL utilizó internamente dos transacciones separadas (no tengo idea de por qué, tal vez algunos desarrolladores de MySQL puedan explicarlo).

MySQL está administrando sus propias transacciones internamente y decidió (en mi caso) usar dos transacciones para manejar todas las solicitudes SQL provenientes del proceso PHP (Proceso A). La afirmación de que la Transacción 1 está esperando que finalice la Transacción 3 es una cuestión interna de MySQL. MySQL "sabía" que la Transacción 1 y la Transacción 3 en realidad se instanciaron como parte de una solicitud de "transacción" (del Proceso A). Ahora toda la "transacción" fue bloqueada porque la Transacción 3 (una subparte de "transacción") fue bloqueada. Debido a que "transacción" no pudo finalizar la transacción 1 (también una subparte de la "transacción") también se marcó como no finalizada. Esto es lo que quise decir con "la transacción 1 espera a que finalice la transacción 3".

Tomás Bilka
fuente
14

El gran problema con esta excepción es que generalmente no es reproducible en un entorno de prueba y no estamos para ejecutar el estado del motor innodb cuando ocurre en prod. Entonces, en uno de los proyectos, puse el código a continuación en un bloque catch para esta excepción. Eso me ayudó a detectar el estado del motor cuando ocurrió la excepción. Eso ayudó mucho.

Statement st = con.createStatement();
ResultSet rs =  st.executeQuery("SHOW ENGINE INNODB STATUS");
while(rs.next()){
    log.info(rs.getString(1));
    log.info(rs.getString(2));
    log.info(rs.getString(3));
}
Maruthi
fuente
11

Eche un vistazo a la página de manual de la pt-deadlock-loggerutilidad :

brew install percona-toolkit
pt-deadlock-logger --ask-pass server_name

Extrae información de lo engine innodb statusmencionado anteriormente y también se puede usar para crear una daemonque se ejecuta cada 30 segundos.

Andrei Sura
fuente
3
esta herramienta ahora es parte del kit de herramientas de Percona
Brad Mace
Los tiempos de espera de bloqueo no son lo mismo que los puntos muertos, específicamente innodb no muestra ninguna información sobre ellos porque no son puntos muertos detectados, por lo que no creo que pt-deadlock-logger sea de ayuda.
Jay Paroline
Sin embargo, los tiempos de espera de bloqueo y los puntos muertos están relacionados: consulte dev.mysql.com/doc/refman/5.7/en/innodb-deadlock-detection.html
Andrei Sura
11

Extrapolando la respuesta de Rolando anterior, estos son los que están bloqueando su consulta:

---TRANSACTION 0 620783788, not started, process no 29956, OS thread id 1196472640
MySQL thread id 5341773, query id 189708353 10.64.89.143 viget

Si necesita ejecutar su consulta y no puede esperar a que se ejecuten los demás, elimínelos utilizando el ID de hilo de MySQL:

kill 5341773 <replace with your thread id>

(desde mysql, no el shell, obviamente)

Tienes que encontrar las ID de hilo en:

show engine innodb status\G

comando y averiguar cuál es el que está bloqueando la base de datos.

Ellert van Koperen
fuente
1
¿Cómo sabes que es 5341773? No veo lo que distingue a uno de los otros.
Wodin
No, probablemente no sea ese threadID, fue un ejemplo. Debe encontrar las ID de subproceso del comando "show engine innodb status \ G" y averiguar cuál es el que está bloqueando la base de datos.
Ellert van Koperen
1
Gracias. En otras palabras, ¿no hay forma de saber cuál es sin, por ejemplo, matarlos uno por uno?
Wodin
En la lista de transacciones puede ver cuáles se están ejecutando y por cuánto tiempo. Entonces, no es necesario matarlos uno por uno, esa lista generalmente le da una idea bastante buena de lo que está sucediendo.
Ellert van Koperen
10

Esto es lo que finalmente tuve que hacer para averiguar qué "otra consulta" causó el problema del tiempo de espera de bloqueo. En el código de la aplicación, rastreamos todas las llamadas a la base de datos pendientes en un hilo separado dedicado a esta tarea. Si cualquier llamada a la base de datos tarda más de N segundos (para nosotros son 30 segundos), registramos:

-- Pending InnoDB transactions
SELECT * FROM information_schema.innodb_trx ORDER BY trx_started; 

-- Optionally, log what transaction holds what locks
SELECT * FROM information_schema.innodb_locks;

Con lo anterior, pudimos identificar consultas concurrentes que bloquearon las filas que causaron el punto muerto. En mi caso, fueron declaraciones como las INSERT ... SELECTque, a diferencia de los SELECT simples, bloquean las filas subyacentes. Luego puede reorganizar el código o usar un aislamiento de transacción diferente como lectura no confirmada.

¡Buena suerte!

Slawomir
fuente
9

Puedes usar:

show full processlist

que enumerará todas las conexiones en MySQL y el estado actual de la conexión, así como la consulta que se está ejecutando. También hay una variante más corta show processlist;que muestra la consulta truncada, así como las estadísticas de conexión.

Gerrit Brink
fuente
-2

Active MySQL general.log (uso intensivo de disco) y use mysql_analyse_general_log.pl para extraer transacciones de larga ejecución, por ejemplo con:

--min-Duration = su valor innodb_lock_wait_timeout

Desactivar general.log después de eso.

mick
fuente