Como depurar o tempo limite de espera de bloqueio excedido no MySQL?

269

Nos meus logs de erro de produção, ocasionalmente vejo:

SQLSTATE [HY000]: Erro geral: 1205 Tempo limite de espera de bloqueio excedido; tente reiniciar a transação

Eu sei qual consulta está tentando acessar o banco de dados naquele momento, mas existe uma maneira de descobrir qual consulta teve o bloqueio naquele momento preciso?

Matt McCormick
fonte
1
I fortemente sugerir todos para dar resposta de Eirik um tiro
kommradHomer

Respostas:

261

O que revela isso é a palavra transação . É evidente pela declaração que a consulta estava tentando alterar pelo menos uma linha em uma ou mais tabelas do InnoDB.

Como você conhece a consulta, todas as tabelas que estão sendo acessadas são candidatas a culpadas.

A partir daí, você poderá executar SHOW ENGINE INNODB STATUS\G

Você deve poder ver as tabelas afetadas

Você obtém todos os tipos de informações adicionais sobre bloqueio e mutex.

Aqui está uma amostra de um dos meus 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)

Você deve considerar aumentar o valor do tempo limite de espera de bloqueio para o InnoDB configurando innodb_lock_wait_timeout , o padrão é 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)

Você pode configurá-lo para um valor mais alto /etc/my.cnfpermanentemente com esta linha

[mysqld]
innodb_lock_wait_timeout=120

e reinicie o mysql. Se você não pode reiniciar o mysql neste momento, execute o seguinte:

SET GLOBAL innodb_lock_wait_timeout = 120; 

Você também pode configurá-lo para a duração da sua sessão

SET innodb_lock_wait_timeout = 120; 

seguido pela sua consulta

RolandoMySQLDBA
fonte
5
Para o InnoDB embutido, a innodb_lock_wait_timeoutvariável pode ser definida apenas na inicialização do servidor. Para o InnoDB Plugin, ele pode ser definido na inicialização ou alterado no tempo de execução e possui valores globais e de sessão.
Timo Huovinen
1
Oi @rolandomysqldba, você pode por favor me dar uma sugestão no meu post: stackoverflow.com/questions/18267565/…
Manish Sapkal
2
Eu recebo este erro ao tentar executar a primeira 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 Toda vez que o mysqld é reiniciado, você deve executar SET GLOBAL innodb_lock_wait_timeout = 120;novamente. Se /etc/my.cnftiver a opção, innodb_lock_wait_timeoutestá definido para você. Nem todo mundo tem o privilégio SUPER de alterá-lo globalmente para todos os outros ( dev.mysql.com/doc/refman/5.6/en/… ) #
RolandoMySQLDBA
3
@IulianOnofrei o caractere \ G é um recurso especial da linha de comando do MySQL e altera a maneira como a saída é exibida. Para outros clientes MySQL, use um ponto-e-vírgula comum.
thenickdude
83

Como alguém mencionado em um dos muitos threads SO referentes a esse problema: Às vezes, o processo que bloqueou a tabela aparece como adormecido na lista de processos! Eu estava arrancando meus cabelos até matar todos os fios adormecidos que estavam abertos no banco de dados em questão (nenhum estava ativo no momento). Isso finalmente desbloqueou a tabela e deixou a consulta de atualização em execução.

O comentarista disse algo semelhante a "Às vezes, um thread do MySQL bloqueia uma tabela e dorme enquanto espera que algo não relacionado ao MySQL aconteça".

Após revisar novamente o show engine innodb statuslog (depois de rastrear o cliente responsável pelo bloqueio), notei que o encadeamento preso em questão estava listado na parte inferior da lista de transações, abaixo das consultas ativas que estavam prestes a ocorrer um erro devido ao bloqueio congelado:

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

(não tem certeza se a mensagem "Trx read view" está relacionada ao bloqueio congelado, mas diferente das outras transações ativas, essa não aparece com a consulta emitida e afirma que a transação está "limpando", mas tem várias bloqueios de linha)

A moral da história é que uma transação pode estar ativa mesmo que o segmento esteja inativo.

Eric L.
fonte
2
Não posso dizer que você salvou minha vida, mas você colocou minha mente em paz com certeza. Lendo sua resposta, encontrei um tópico assustador que fica ativo por 3260 segundos e não aparece em nenhum lugar. depois de matá-lo, todos os meus problemas foram resolvidos!
usar o seguinte código
Este foi o meu problema. Uma transação inativa com tempo de 20.000 segundos que impedia que o trabalho atrasado em um aplicativo Rails fosse executado corretamente. Obrigado @Eirik
bigtex777
Alguma idéia de por que uma transação adormecida não é eliminada de qualquer maneira? Tipo, existe um tempo limite em que você pode definir que uma transação deve terminar dentro?
patrickdavey
1
Outros comandos que podem ser úteis na sua pesquisa de transações de bloqueio: show processlist;para mostrar uma lista exaustiva dos processos atualmente em execução, o que é bom porque é uma versão condensada do show engine innodb status\g. Além disso, se o seu banco de dados estiver em uma instância do Amazon RDS, você poderá usar CALL mysql.rds_kill(<thread_id>);para matar threads. Tem permissões superiores eu acho, porque me permitiu matar mais processos do que simples kill <thread_id>;- nota estes devem ser executados dentro MySQL CLI
nickang
1
Alguém tem uma fonte para isso - talvez uma página de documentação informando que os bloqueios sejam colocados antes da fase COMMIT? Não consegui encontrar nada, apesar de ver esse problema exato e ser resolvido matando o fio adormecido que estava travando.
Erin Schoonover
42

Devido à popularidade do MySQL, não é de admirar que o tempo de espera do bloqueio seja excedido; tente reiniciar a exceção da transação recebe muita atenção no SO.

Quanto mais contenção você tiver, maior a chance de conflitos, que um mecanismo de banco de dados resolverá ao atingir o tempo limite de uma das transações em conflito. Além disso, transações de longa execução que modificaram (por exemplo, UPDATEou DELETE) um grande número de entradas (que bloqueiam para evitar anomalias de gravação suja, conforme explicado no livro Java Persistence de alto desempenho ) têm mais probabilidade de gerar conflitos com outras transações.

Embora InnoDB MVCC, você ainda pode solicitar bloqueios explícitos usando a FOR UPDATEcláusula . Entretanto, diferentemente de outros bancos de dados populares (Oracle, MSSQL, PostgreSQL, DB2), o MySQL usa REPEATABLE_READcomo o nível de isolamento padrão .

Agora, os bloqueios que você adquiriu (modificando linhas ou usando bloqueio explícito) são mantidos durante a transação em execução no momento. Se você deseja uma boa explicação da diferença entre REPEATABLE_READe READ COMMITTEDno que diz respeito ao bloqueio, leia este artigo da Percona .

Em REPEATABLE READ, todos os bloqueios adquiridos durante uma transação são mantidos pelo período de duração da transação.

Em READ COMMITTED, os bloqueios que não correspondiam à verificação são liberados após a conclusão da declaração.

...

Isso significa que, em READ COMMITTED, outras transações estão livres para atualizar linhas que não teriam sido capazes de atualizar (em REPEATABLE READ) depois que a instrução UPDATE for concluída.

Portanto: quanto mais restritivo o nível de isolamento ( REPEATABLE_READ, SERIALIZABLE) , maior a chance de conflito. Este não é um problema "per se", é uma troca.

Você pode obter resultados muito bons READ_COMMITED, pois precisa de prevenção de atualizações perdidas no nível do aplicativo ao usar transações lógicas que abrangem várias solicitações HTTP. A abordagem de bloqueio otimista visa as atualizações perdidas que podem ocorrer mesmo se você usar o SERIALIZABLEnível de isolamento e reduzir a contenção de bloqueio, permitindo o uso READ_COMMITED.

Vlad Mihalcea
fonte
4
O tempo limite de espera de bloqueio não é diferente do deadlock? Por exemplo, se um encadeamento retém um bloqueio por 60 segundos por motivos legítimos, pode ocorrer o tempo limite de espera do bloqueio. Não é verdade que, se houver realmente um impasse, o MySQL detectará isso e matará uma transação instantaneamente e isso não está relacionado ao tempo limite de espera de bloqueio?
ColinM
1
Você está certo. O banco de dados detecta o bloqueio após o tempo limite e mata um processo em espera; portanto, uma transação vence enquanto a outra falha. Mas quanto mais tempo você mantém um bloqueio, menos escalável o aplicativo fica. Mesmo se você não encontrar dead-locks, ainda aumentará a parte serializável do comportamento do tempo de execução do aplicativo.
21714 Vlad Vladinhalcea
19

Para o registro, a exceção de tempo limite de espera de bloqueio acontece também quando há um impasse e o MySQL não pode detectá-lo, então o tempo limite é excedido. Outro motivo pode ser uma consulta de execução extremamente longa, que é mais fácil de resolver / reparar, no entanto, e não descreverei esse caso aqui.

O MySQL geralmente é capaz de lidar com conflitos, se eles forem construídos "adequadamente" em duas transações. O MySQL simplesmente mata / reverte a transação que possui menos bloqueios (é menos importante, pois afetará menos linhas) e deixa a outra terminar.

Agora, vamos supor que existem dois processos A e B e 3 transações:

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 é uma configuração muito infeliz, porque o MySQL não pode ver que há um impasse (dentro de três transações). Então, o que o MySQL faz é ... nada! Apenas espera, pois não sabe o que fazer. Ele espera até que o primeiro bloqueio adquirido exceda o tempo limite (Processo A Transação 1: Bloqueios X), e isso desbloqueia o Bloqueio X, que desbloqueia a Transação 2, etc.

A arte é descobrir o que (qual consulta) causa o primeiro bloqueio (bloqueio X). Você poderá ver facilmente ( show engine innodb status) que a transação 3 aguarda a transação 2, mas não verá qual transação a transação 2 está aguardando (transação 1). O MySQL não imprimirá nenhum bloqueio ou consulta associada à transação 1. A única dica será que, no final da lista de transações (da show engine innodb statusimpressão), você verá a transação 1 aparentemente sem fazer nada (mas, na verdade, aguardando a transação 3 terminar).

A técnica de como encontrar qual consulta SQL faz com que o bloqueio (Bloqueio X) seja concedido para uma determinada transação que está aguardando é descrita aqui Tracking MySQL query history in long running transactions

Se você está se perguntando qual é o processo e a transação exatamente no exemplo. O processo é um processo PHP. Transação é uma transação conforme definida por innodb-trx-table . No meu caso, eu tinha dois processos PHP, em cada um deles iniciei uma transação manualmente. A parte interessante foi que, embora eu tenha iniciado uma transação em um processo, o MySQL usou internamente duas transações separadas (não faço idéia do porquê, talvez algum desenvolvedor do MySQL possa explicar).

O MySQL está gerenciando suas próprias transações internamente e decidiu (no meu caso) usar duas transações para lidar com todas as solicitações SQL provenientes do processo PHP (Processo A). A declaração de que a transação 1 está aguardando a conclusão da transação 3 é algo interno do MySQL. O MySQL "sabia" que a Transação 1 e a Transação 3 foram realmente instanciadas como parte de uma solicitação de "transação" (do Processo A). Agora toda a "transação" foi bloqueada porque a Transação 3 (uma subparte de "transação") foi bloqueada. Como "transação" não pôde concluir a Transação 1 (também uma subparte da "transação") foi marcada como não concluída também. Isso é o que eu quis dizer com "A transação 1 aguarda a conclusão da transação 3".

Tomas Bilka
fonte
14

O grande problema dessa exceção é que geralmente não é reproduzível em um ambiente de teste e não estamos por perto para executar o status do mecanismo innodb quando ocorre no prod. Então, em um dos projetos, coloquei o código abaixo em um bloco catch para essa exceção. Isso me ajudou a entender o status do mecanismo quando a exceção aconteceu. Isso ajudou muito.

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
fonte
11

Dê uma olhada na página de manual do pt-deadlock-loggerutilitário :

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

Extrai informações do engine innodb statusmencionado acima e também pode ser usado para criar um daemonque é executado a cada 30 segundos.

Andrei Sura
fonte
3
esta ferramenta agora faz parte do kit de ferramentas Percona
Brad Mace
Os tempos limite de espera de bloqueio não são os mesmos que os deadlocks, especificamente o innodb não mostra nenhuma informação sobre eles porque não são detectados, então não acho que pt-deadlock-logger seja de ajuda.
Jay Paroline 22/17
Tempos limite de bloqueio e impasses estão relacionados embora - ver dev.mysql.com/doc/refman/5.7/en/innodb-deadlock-detection.html
Andrei Sura
11

Extrapolando da resposta de Rolando acima, são estas que estão bloqueando sua 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

Se você precisar executar sua consulta e não puder esperar que os outros sejam executados, mate-os usando o ID do thread do MySQL:

kill 5341773 <replace with your thread id>

(de dentro do mysql, não do shell, obviamente)

Você precisa encontrar os IDs de thread em:

show engine innodb status\G

comando e descubra qual é o que está bloqueando o banco de dados.

Ellert van Koperen
fonte
1
Como você sabe que é 5341773? Não vejo o que distingue esse dos outros.
Wodin
Não, provavelmente não é esse threadID, foi um exemplo. Você precisa encontrar os IDs de encadeamento do comando "show engine innodb status \ G" e descobrir qual é o que está bloqueando o banco de dados.
Ellert van Koperen
1
Obrigado. Então, em outras palavras, não há como dizer qual é o nome sem matá-los um por um?
Wodin 8/15
Na lista de transações, você pode ver quais estão em execução e por quanto tempo. Portanto, não há necessidade de eliminá-los um por um; essa lista geralmente oferece uma boa idéia do que está acontecendo.
Ellert van Koperen
10

Aqui está o que eu finalmente tive que fazer para descobrir o que "outra consulta" causou o problema de tempo limite de bloqueio. No código do aplicativo, rastreamos todas as chamadas pendentes do banco de dados em um thread separado dedicado a esta tarefa. Se qualquer chamada de banco de dados demorar mais de N segundos (para nós, são 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;

Com o exposto, conseguimos identificar consultas simultâneas que bloqueavam as linhas que causavam o conflito. No meu caso, eram declarações como as INSERT ... SELECTque, ao contrário de SELECTs simples, bloqueiam as linhas subjacentes. Em seguida, você pode reorganizar o código ou usar um isolamento de transação diferente, como a leitura não confirmada.

Boa sorte!

Slawomir
fonte
9

Você pode usar:

show full processlist

que listará todas as conexões no MySQL e o estado atual da conexão, bem como a consulta que está sendo executada. Há também uma variante mais curta show processlist;que exibe a consulta truncada, bem como as estatísticas de conexão.

Gerrit Brink
fonte
-2

Ative o MySQL general.log (uso intensivo de disco) e use mysql_analyse_general_log.pl para extrair transações de execução longa, por exemplo, com:

--min-duration = seu valor innodb_lock_wait_timeout

Desative general.log depois disso.

mick
fonte