Fixer un délai pour une transaction dans MySQL / InnoDB

11

Cela découle de cette question connexe , où je voulais savoir comment forcer deux transactions à se produire séquentiellement dans un cas trivial (où les deux ne fonctionnent que sur une seule ligne). J'ai obtenu une réponse - utiliser SELECT ... FOR UPDATEcomme première ligne des deux transactions - mais cela conduit à un problème: si la première transaction n'est jamais validée ou annulée, la deuxième transaction sera bloquée indéfiniment. La innodb_lock_wait_timeoutvariable définit le nombre de secondes après lesquelles le client essayant d'effectuer la deuxième transaction se verra dire "Désolé, essayez à nouveau" ... mais pour autant que je sache, il essaiera à nouveau jusqu'au prochain redémarrage du serveur. Donc:

  1. Il doit sûrement y avoir un moyen de forcer un ROLLBACKsi une transaction prend une éternité? Dois-je recourir à l'utilisation d'un démon pour tuer de telles transactions, et si oui, à quoi ressemblerait un tel démon?
  2. Si une connexion est interrompue par wait_timeoutou en cours interactive_timeoutde transaction, la transaction est-elle annulée? Existe-t-il un moyen de tester cela à partir de la console?

Clarification : innodb_lock_wait_timeoutdéfinit le nombre de secondes pendant lesquelles une transaction attendra qu'un verrou soit libéré avant d'abandonner; ce que je veux, c'est un moyen de forcer le déverrouillage.

Mise à jour 1 : voici un exemple simple qui montre pourquoi innodb_lock_wait_timeoutn'est pas suffisant pour garantir que la deuxième transaction n'est pas bloquée par la première:

START TRANSACTION;
SELECT SLEEP(55);
COMMIT;

Avec le paramètre par défaut de innodb_lock_wait_timeout = 50, cette transaction se termine sans erreur après 55 secondes. Et si vous ajoutez un UPDATEavant la SLEEPligne, puis lancez une deuxième transaction à partir d'un autre client qui essaie sur SELECT ... FOR UPDATEla même ligne, c'est la deuxième transaction qui expire, pas celle qui s'est endormie.

Ce que je recherche, c'est un moyen de forcer la fin du sommeil réparateur de cette transaction.

Mise à jour 2 : En réponse aux préoccupations de hobodave sur la réalité de l'exemple ci-dessus, voici un scénario alternatif: un DBA se connecte à un serveur en direct et s'exécute

START TRANSACTION
SELECT ... FOR UPDATE

où la deuxième ligne verrouille une ligne dans laquelle l'application écrit fréquemment. Ensuite, le DBA est interrompu et s'éloigne, oubliant de mettre fin à la transaction. L'application s'arrête jusqu'à ce que la ligne soit déverrouillée. Je voudrais minimiser le temps de blocage de l'application suite à cette erreur.

Trevor Burnham
la source
Vous venez de mettre à jour votre question pour qu'elle soit complètement en conflit avec votre question initiale. Vous déclarez en gras "Si la première transaction n'est jamais validée ou annulée, alors la deuxième transaction sera bloquée indéfiniment." Vous réfutez cela avec votre dernière mise à jour: "c'est la deuxième transaction qui expire, pas celle qui s'est endormie." -- sérieusement?!
hobodave
Je suppose que ma formulation aurait pu être plus claire. Par «bloqué indéfiniment», je voulais dire que la deuxième transaction expirerait avec un message d'erreur qui dit «essayez de redémarrer la transaction»; mais cela serait vain, car cela expirerait à nouveau. La deuxième transaction est donc bloquée - elle ne se terminera jamais, car elle expirera toujours.
Trevor Burnham
@Trevor: Votre formulation était parfaitement claire. Vous avez simplement mis à jour votre question pour poser une tout autre chose, ce qui est extrêmement mauvais.
hobodave
La question a toujours été la même: c'est un problème que la deuxième transaction soit bloquée indéfiniment lorsque la première transaction n'est jamais validée ou annulée. Je veux forcer un ROLLBACKsur la première transaction si cela prend plus de nsecondes pour terminer. Existe-t-il un moyen de le faire?
Trevor Burnham
1
@TrevorBurnham Je me demande aussi pourquoi MYSQLn'a pas de configuration pour empêcher ce scénario. Parce qu'il n'est pas acceptable de bloquer le serveur en raison de l'irresponsabilité des clients. Je n'ai trouvé aucune difficulté à comprendre votre question et elle est si pertinente.
Dinoop paloli

Réponses:

3

Étant donné que votre question est posée ici sur ServerFault, il est logique de supposer que vous recherchez une solution MySQL à un problème MySQL, en particulier dans le domaine des connaissances dans lesquelles un administrateur système et / ou un DBA auraient une expertise. En tant que tel, les éléments suivants section répond à vos questions:

Si la première transaction n'est jamais validée ou annulée, la deuxième transaction sera bloquée indéfiniment

Non, ce ne sera pas le cas. Je pense que vous ne comprenez pas innodb_lock_wait_timeout. Il fait exactement ce dont vous avez besoin.

Il retournera avec une erreur comme indiqué dans le manuel:

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
  • Par définition, ce n'est pas indéfini . Si votre application se reconnecte et se bloque à plusieurs reprises, votre application est "bloquée indéfiniment", pas la transaction. La deuxième transaction bloque très définitivement pendant innodb_lock_wait_timeoutquelques secondes.

Par défaut, la transaction ne sera pas annulée. Il est de la responsabilité de votre code d'application de décider comment gérer cette erreur, que ce soit une nouvelle tentative ou une annulation.

Si vous souhaitez une restauration automatique, cela est également expliqué dans le manuel:

La transaction en cours n'est pas annulée. (Pour restaurer la totalité de la transaction, démarrez le serveur avec l' --innodb_rollback_on_timeoutoption.


RE: Vos nombreuses mises à jour et commentaires

Premièrement, vous avez déclaré dans vos commentaires que vous "vouliez" dire que vous vouliez un moyen de temporiser la première transaction qui se bloquait indéfiniment. Cela ne ressort pas de votre question initiale et entre en conflit avec "Si la première transaction n'est jamais validée ou annulée, alors la deuxième transaction sera bloquée indéfiniment".

Néanmoins, je peux également répondre à cette question. Le protocole MySQL n'a pas de "délai d'expiration de requête". Cela signifie que vous ne pouvez pas expirer la première transaction bloquée. Vous devez attendre qu'il soit terminé ou tuer la session. Lorsque la session est interrompue, le serveur annule automatiquement la transaction.

La seule autre alternative serait d'utiliser ou d'écrire une bibliothèque mysql qui utilise des E / S non bloquantes qui permettraient à votre application de tuer le thread / fork faisant la requête après N secondes. L'implémentation et l'utilisation d'une telle bibliothèque dépassent le cadre de ServerFault. C'est une question appropriée pour StackOverflow .

Deuxièmement, vous avez déclaré ce qui suit dans vos commentaires:

En fait, j'étais plus préoccupé par ma question par un scénario dans lequel l'application cliente se bloque (par exemple, se retrouve pris dans une boucle infinie) au cours d'une transaction que par un scénario dans lequel la transaction prend beaucoup de temps de la part de MySQL.

Ce n'était pas du tout apparent dans votre question initiale, et ce n'est toujours pas le cas. Cela n'a pu être discerné qu'après avoir partagé cette friandise assez importante dans le commentaire.

Si c'est réellement le problème que vous essayez de résoudre, alors je crains que vous ne l'ayez posé sur le mauvais forum. Vous avez décrit un problème de programmation au niveau de l'application qui nécessite une solution de programmation, que MySQL ne peut pas fournir, et qui sort du cadre de cette communauté. Votre dernière réponse répond à la question "Comment empêcher un programme Ruby de boucler indéfiniment?". Cette question est hors sujet pour cette communauté et doit être posée sur StackOverflow .

Hobodave
la source
Désolé, mais bien que cela soit vrai lorsque la transaction attend un verrou (comme le innodb_lock_wait_timeoutsuggèrent le nom et la documentation de ), ce n'est pas le cas dans la situation que j'ai posée: où le client se bloque ou se bloque avant d'obtenir la modification pour faire un COMMITou ROLLBACK.
Trevor Burnham
@Trevor: Vous avez tout simplement tort. C'est trivial à tester de votre côté. Je viens de le tester de mon côté. Veuillez reprendre votre downvote.
hobodave
@hobo, juste parce que votre droit ne signifie pas qu'il doit l'aimer.
Chris S
Chris, pourriez-vous expliquer comment il a raison? Comment la deuxième transaction se produira-t-elle si aucun COMMITou ROLLBACKmessage n'est envoyé pour résoudre la première transaction? Hobodave, il serait peut-être utile de présenter votre code de test.
Trevor Burnham
2
@Trevor: Vous ne faites pas un peu de sens. Vous voulez sérialiser les transactions, non? Oui, vous l'avez dit dans votre autre question et répétez cela ici dans cette question. Si la première transaction n'est pas terminée, comment diable vous attendriez-vous à ce que la deuxième transaction se termine? Vous lui avez explicitement dit d'attendre que le verrou soit libéré sur cette ligne. Il faut donc attendre. Que ne comprenez-vous pas à ce sujet?
hobodave
1

Dans une situation similaire, mon équipe a décidé d'utiliser pt-kill ( https://www.percona.com/doc/percona-toolkit/2.1/pt-kill.html ). Il peut signaler ou tuer des requêtes qui (entre autres) sont en cours d'exécution trop longtemps. Il est alors possible de l'exécuter dans un cron toutes les X minutes.

Le problème était qu'une requête dont le temps d'exécution était inopinément long (par exemple, indéfini) a verrouillé une procédure de sauvegarde qui a tenté de vider les tables avec un verrou en lecture, qui à son tour a verrouillé toutes les autres requêtes sur "l'attente des tables à vider" qui n'ont jamais expiré car ils n'attendent pas de verrou, ce qui a entraîné aucune erreur dans l'application, ce qui a finalement entraîné aucune alerte aux administrateurs et l'arrêt de l'application.

Le correctif était évidemment de corriger la requête initiale, mais afin d'éviter que la situation ne se produise accidentellement à l'avenir, il serait formidable s'il était possible de tuer automatiquement (ou de signaler les transactions / requêtes qui durent plus longtemps que la durée spécifiée, quel auteur de la question semble se poser. C'est faisable avec pt-kill.

Krešimir Nesek
la source
0

Une solution simple sera d'avoir une procédure stockée pour tuer les requêtes qui prennent plus de temps que votre timeouttemps requis et d'utiliser l' innodb_rollback_on_timeoutoption avec elle.

Sony Mathew
la source
-2

Après avoir parcouru Google pendant un certain temps, il semble qu'il n'y ait aucun moyen direct de définir un délai par transaction. Voici la meilleure solution que j'ai pu trouver:

La commande

SHOW PROCESSLIST;

vous donne un tableau avec toutes les commandes en cours d'exécution et le temps (en secondes) depuis leur démarrage. Lorsqu'un client a cessé de donner des commandes, il est décrit comme donnant une Sleepcommande, la Timecolonne étant le nombre de secondes écoulées depuis l'exécution de sa dernière commande. Vous pouvez donc entrer manuellement et KILLtout ce qui a une Timevaleur sur, disons, 5 secondes si vous êtes sûr qu'aucune requête ne devrait prendre plus de 5 secondes sur votre base de données. Par exemple, si le processus avec l'ID 3 a une valeur de 12 dans sa Timecolonne, vous pouvez faire

KILL 3;

La documentation de la syntaxe KILL suggère qu'une transaction effectuée par le thread avec cet identifiant serait annulée (même si je n'ai pas testé cela, soyez donc prudent).

Mais comment automatiser cela et tuer tous les scripts d'heures supplémentaires toutes les 5 secondes? Le premier commentaire sur cette même page nous donne un indice, mais le code est en PHP; il semble que nous ne pouvons pas faire un SELECTon SHOW PROCESSLISTdepuis le client MySQL. En supposant que nous ayons un démon PHP que nous exécutons toutes les $MAX_TIMEsecondes, cela pourrait ressembler à ceci:

$result = mysql_query("SHOW FULL PROCESSLIST");
while ($row=mysql_fetch_array($result)) {
  $process_id=$row["Id"];
    if ($row["Time"] > $MAX_TIME) {
      $sql="KILL $process_id";
      mysql_query($sql);
  }
}

Notez que cela aurait pour effet secondaire de forcer toutes les connexions client inactives à se reconnecter, pas seulement celles qui se comportent mal.

Si quelqu'un a une meilleure réponse, j'aimerais l'entendre.

Edit: Il existe au moins un risque sérieux d'utilisation KILLde cette manière. Supposons que vous utilisez le script ci-dessus pour KILLchaque connexion inactive pendant 10 secondes. Après qu'une connexion a été inactive pendant 10 secondes, mais avant l' KILLémission, l'utilisateur dont la connexion est interrompue émet une START TRANSACTIONcommande. Ils sont ensuite KILLédités. Ils envoient un UPDATEet puis, réfléchissant à deux fois, émettent un ROLLBACK. Cependant, étant donné que la KILLtransaction a été annulée, la mise à jour a été effectuée immédiatement et ne peut pas être annulée! Voir cet article pour un cas de test.

Trevor Burnham
la source
2
Ce commentaire est destiné aux futurs lecteurs de cette réponse. Veuillez ne pas le faire, même si c'est la réponse acceptée.
hobodave
OK, plutôt que de downvoting et de laisser un commentaire sarcastique, pourquoi ne pas expliquer pourquoi ce serait une mauvaise idée? La personne qui a publié le script PHP d'origine a déclaré que cela empêchait son serveur de se bloquer; s'il y a une meilleure façon d'atteindre le même objectif, montrez-moi.
Trevor Burnham
6
Le commentaire n'est pas sournois. C'est un avertissement pour tous les futurs lecteurs de ne faire aucune tentative d'utiliser votre "solution". Tuer automatiquement les transactions de longue durée est une idée unilatéralement terrible. Cela ne devrait jamais être fait . Voilà l'explication. Tuer des séances devrait être une exception et non une norme. La cause première de votre problème artificiel est une erreur utilisateur. Vous ne créez pas de solutions techniques pour les administrateurs de base de données qui verrouillent des lignes et qui s'en vont ensuite. Vous les renvoyez.
hobodave
-2

Comme l'a suggéré hobodave dans un commentaire, il est possible dans certains clients (mais pas, apparemment, l' mysqlutilitaire de ligne de commande) de fixer une limite de temps pour une transaction. Voici une démonstration utilisant ActiveRecord dans Ruby:

require 'rubygems'
require 'timeout'
require 'active_record'

Timeout::timeout(5) {
  Foo.transaction do
    Foo.create(:name => 'Bar')
    sleep 10
  end
}

Dans cet exemple, la transaction expire après 5 secondes et est automatiquement annulée . ( Mise à jour en réponse au commentaire de hobodave: si la base de données met plus de 5 secondes pour répondre, la transaction sera annulée dès qu'elle le fera, pas plus tôt.) Si vous vouliez vous assurer que toutes vos transactions expirent après nquelques secondes, vous pouvez créer un wrapper autour d'ActiveRecord. Je suppose que cela s'applique également aux bibliothèques les plus populaires en Java, .NET, Python, etc., mais je ne les ai pas encore testées. (Si c'est le cas, veuillez poster un commentaire sur cette réponse.)

Les transactions dans ActiveRecord ont également l'avantage d'être sûres si un KILLest émis, contrairement aux transactions effectuées à partir de la ligne de commande. Voir /dba/1561/mysql-client-believes-theyre-in-a-transaction-gets-killed-wreaks-havoc .

Il ne semble pas possible d'appliquer un temps de transaction maximum côté serveur, sauf via un script comme celui que j'ai publié dans mon autre réponse.

Trevor Burnham
la source
Vous avez oublié qu'ActiveRecord utilise des E / S synchrones (bloquantes). Tout ce que ce script fait, c'est interrompre la commande Ruby sleep. Si votre Foo.createcommande était bloquée pendant 5 minutes, le Timeout ne le tuerait pas. Ceci est incompatible avec l'exemple fourni dans votre question. Un SELECT ... FOR UPDATEpourrait facilement bloquer pendant de longues périodes, comme n'importe quelle autre requête.
hobodave
Il convient de noter que presque toutes les bibliothèques clientes mysql utilisent des E / S bloquantes, d'où la suggestion dans ma réponse que vous devez utiliser ou écrire la vôtre qui utilise des E / S non bloquantes. Voici une démonstration simple du Timeout étant inutile: gist.github.com/856100
hobodave
En fait, j'étais plus préoccupé par ma question par un scénario dans lequel l'application cliente se bloque (par exemple, se retrouve pris dans une boucle infinie) au cours d'une transaction que par un scénario dans lequel la transaction prend beaucoup de temps de la part de MySQL. Mais merci, c'est un bon point. J'ai mis à jour la question pour la noter.)
Trevor Burnham
Je ne peux pas dupliquer vos résultats revendiqués. J'ai mis à jour l'essentiel pour utiliser ActiveRecord::Base#find_by_sql: gist.github.com/856100 Encore une fois, c'est parce que AR utilise les E / S de blocage.
hobodave
@hobodave Oui, cette modification était erronée et a été corrigée. Pour être clair: la timeoutméthode annulera la transaction, mais pas tant que la base de données MySQL ne répondra pas à une requête. Ainsi, la timeoutméthode convient pour empêcher les transactions longues côté client ou les transactions longues qui consistent en plusieurs requêtes relativement courtes, mais pas pour les transactions longues dans lesquelles une seule requête est le coupable.
Trevor Burnham